From 9ffbcd666e5dda33d36cbed9c1a2db82bb940c5e Mon Sep 17 00:00:00 2001 From: PotS Date: Wed, 1 Jan 2025 12:01:09 +0000 Subject: [PATCH 1/5] fix: P2 enemy reference - An enemy in state 5150 is now correctly removed from the "P2" enemy list - Removed "Default.IgnoreDefeatedEnemies" constant as it breaks some codes and has largely been replaced by the new P2 redirection - Simplified enemy sorting code a little - Changed internal timing of when the "KO" flag is asserted - Fixed p4name, p6name and p8name all returning the same as p2name - EnemyNear now also accounts for standby chars (much like KO chars) --- data/common.const | 1 - src/bytecode.go | 46 +++++------- src/char.go | 177 +++++++++++++++++++++++++++------------------- src/script.go | 7 +- src/system.go | 2 +- 5 files changed, 128 insertions(+), 105 deletions(-) diff --git a/data/common.const b/data/common.const index 4cd8bcb2..2659009a 100644 --- a/data/common.const +++ b/data/common.const @@ -13,7 +13,6 @@ Default.Enable.Tag = 1 ; Backward compatibility toggles Default.LegacyGameDistanceSpec = 1 ; 1 prevents GameWidth/GameHeight from being affected by stage zoom for mugenversion 1.0 chars -Default.IgnoreDefeatedEnemies = 1 ; 1 prevents EnemyNear from redirecting defeated enemies Input.PauseOnHitPause = 1 ; 1 makes inputs to be retained during hit pause ; Rules diff --git a/src/bytecode.go b/src/bytecode.go index 2658961a..45e06643 100644 --- a/src/bytecode.go +++ b/src/bytecode.go @@ -1453,7 +1453,7 @@ func (be BytecodeExp) run(c *Char) BytecodeValue { sys.bcStack.Push(BytecodeSF()) i += int(*(*int32)(unsafe.Pointer(&be[i]))) + 4 case OC_enemynear: - if c = c.enemyNear(sys.bcStack.Pop().ToI()); c != nil { + if c = c.enemyNearTrigger(sys.bcStack.Pop().ToI()); c != nil { i += 4 continue } @@ -2175,48 +2175,38 @@ func (be BytecodeExp) run_const(c *Char, i *int, oc *Char) { *i += 4 case OC_const_p2name: p2 := c.p2() - sys.bcStack.PushB(p2 != nil && p2.gi().nameLow == - sys.stringPool[sys.workingState.playerNo].List[*(*int32)( - unsafe.Pointer(&be[*i]))]) + sys.bcStack.PushB(p2 != nil && + p2.gi().nameLow == sys.stringPool[sys.workingState.playerNo].List[*(*int32)(unsafe.Pointer(&be[*i]))]) *i += 4 case OC_const_p3name: p3 := c.partner(0, false) - sys.bcStack.PushB(p3 != nil && p3.gi().nameLow == - sys.stringPool[sys.workingState.playerNo].List[*(*int32)( - unsafe.Pointer(&be[*i]))]) + sys.bcStack.PushB(p3 != nil && + p3.gi().nameLow == sys.stringPool[sys.workingState.playerNo].List[*(*int32)(unsafe.Pointer(&be[*i]))]) *i += 4 case OC_const_p4name: - p4 := sys.charList.enemyNear(c, 1, true, true, false) - sys.bcStack.PushB(p4 != nil && !(p4.scf(SCF_ko) && p4.scf(SCF_over)) && - p4.gi().nameLow == - sys.stringPool[sys.workingState.playerNo].List[*(*int32)( - unsafe.Pointer(&be[*i]))]) + p4 := sys.charList.enemyNear(c, 1, true, false) + sys.bcStack.PushB(p4 != nil && + p4.gi().nameLow == sys.stringPool[sys.workingState.playerNo].List[*(*int32)(unsafe.Pointer(&be[*i]))]) *i += 4 case OC_const_p5name: p5 := c.partner(1, false) - sys.bcStack.PushB(p5 != nil && p5.gi().nameLow == - sys.stringPool[sys.workingState.playerNo].List[*(*int32)( - unsafe.Pointer(&be[*i]))]) + sys.bcStack.PushB(p5 != nil && + p5.gi().nameLow == sys.stringPool[sys.workingState.playerNo].List[*(*int32)(unsafe.Pointer(&be[*i]))]) *i += 4 case OC_const_p6name: - p6 := sys.charList.enemyNear(c, 2, true, true, false) - sys.bcStack.PushB(p6 != nil && !(p6.scf(SCF_ko) && p6.scf(SCF_over)) && - p6.gi().nameLow == - sys.stringPool[sys.workingState.playerNo].List[*(*int32)( - unsafe.Pointer(&be[*i]))]) + p6 := sys.charList.enemyNear(c, 2, true, false) + sys.bcStack.PushB(p6 != nil && + p6.gi().nameLow == sys.stringPool[sys.workingState.playerNo].List[*(*int32)(unsafe.Pointer(&be[*i]))]) *i += 4 case OC_const_p7name: p7 := c.partner(2, false) - sys.bcStack.PushB(p7 != nil && p7.gi().nameLow == - sys.stringPool[sys.workingState.playerNo].List[*(*int32)( - unsafe.Pointer(&be[*i]))]) + sys.bcStack.PushB(p7 != nil && + p7.gi().nameLow == sys.stringPool[sys.workingState.playerNo].List[*(*int32)(unsafe.Pointer(&be[*i]))]) *i += 4 case OC_const_p8name: - p8 := sys.charList.enemyNear(c, 3, true, true, false) - sys.bcStack.PushB(p8 != nil && !(p8.scf(SCF_ko) && p8.scf(SCF_over)) && - p8.gi().nameLow == - sys.stringPool[sys.workingState.playerNo].List[*(*int32)( - unsafe.Pointer(&be[*i]))]) + p8 := sys.charList.enemyNear(c, 3, true, false) + sys.bcStack.PushB(p8 != nil && + p8.gi().nameLow == sys.stringPool[sys.workingState.playerNo].List[*(*int32)(unsafe.Pointer(&be[*i]))]) *i += 4 case OC_const_stagevar_info_name: sys.bcStack.PushB(sys.stage.nameLow == diff --git a/src/char.go b/src/char.go index 2dc1c86c..b74e0e90 100644 --- a/src/char.go +++ b/src/char.go @@ -20,7 +20,7 @@ const ( SCF_guard SCF_guardbreak SCF_ko - SCF_ko_round_middle + SCF_ko_during_round SCF_over SCF_standby ) @@ -1326,7 +1326,7 @@ func (e *Explod) setPos(c *Char) { case PT_P1: pPos(c) case PT_P2: - if p2 := sys.charList.enemyNear(c, 0, true, true, false); p2 != nil { + if p2 := sys.charList.enemyNear(c, 0, true, false); p2 != nil { pPos(p2) } case PT_Front, PT_Back: @@ -2317,8 +2317,8 @@ type Char struct { targets []int32 hitdefTargets []int32 hitdefTargetsBuffer []int32 - enemynear [2][]*Char - p2enemy []*Char + enemyNearList []*Char // Enemies retrieved by EnemyNear + p2EnemyList []*Char // Enemies retrieved by P2, P4, P6 and P8 pos [3]float32 interPos [3]float32 // Interpolated position. For the visuals when game and logic speed are different oldPos [3]float32 @@ -2496,9 +2496,13 @@ func (c *Char) addChild(ch *Char) { } c.children = append(c.children, ch) } -func (c *Char) enemyNearClear() { - c.enemynear[0] = c.enemynear[0][:0] - c.enemynear[1] = c.enemynear[1][:0] + +// Clear enemy near list. For instance when player positions change +// A new list will be built the next time the redirect is called +// In Mugen, EnemyNear is updated instantly when the character uses PosAdd, but "P2" is not +func (c *Char) enemyNearP2Clear() { + c.enemyNearList = c.enemyNearList[:0] + c.p2EnemyList = c.p2EnemyList[:0] } // Clear character variables upon a new round or creation of a new helper @@ -2533,8 +2537,7 @@ func (c *Char) clearNextRound() { } } c.aimg.timegap = -1 - c.enemyNearClear() - c.p2enemy = c.p2enemy[:0] + c.enemyNearP2Clear() c.targets = c.targets[:0] c.cpucmd = -1 } @@ -3566,16 +3569,17 @@ func (c *Char) enemy(n int32) *Char { //return sys.chars[n*2+int32(^c.playerNo&1)][0] return nil } -func (c *Char) enemyNear(n int32) *Char { - return sys.charList.enemyNear(c, n, false, c.gi().constants["default.ignoredefeatedenemies"] > 0, false) + +// This is only used to simplify the redirection call +func (c *Char) enemyNearTrigger(n int32) *Char { + return sys.charList.enemyNear(c, n, false, false) } + +// Get the "P2" enemy reference func (c *Char) p2() *Char { - p2 := sys.charList.enemyNear(c, 0, true, true, false) - if p2 != nil && p2.scf(SCF_ko) && p2.scf(SCF_over) { - return nil - } - return p2 + return sys.charList.enemyNear(c, 0, true, false) } + func (c *Char) aiLevel() float32 { if c.helperIndex != 0 && c.gi().mugenver[0] == 1 { return 0 @@ -4431,7 +4435,8 @@ func (c *Char) playSound(ffx string, lowpriority bool, loopCount int32, g, n, ch func (c *Char) autoTurn() { if c.helperIndex == 0 { - if e := sys.charList.enemyNear(c, 0, true, true, false); c.rdDistX(e, c).ToF() < 0 && !e.asf(ASF_noturntarget) { + e := c.p2() + if e != nil && c.rdDistX(e, c).ToF() < 0 && !e.asf(ASF_noturntarget) { switch c.ss.stateType { case ST_S: if c.animNo != 5 { @@ -4446,6 +4451,7 @@ func (c *Char) autoTurn() { } } } + func (c *Char) stateChange1(no int32, pn int) bool { if sys.changeStateNest > 2500 { sys.appendToConsole(c.warn() + fmt.Sprintf("state machine stuck in loop (stopped after 2500 loops): %v -> %v -> %v", c.ss.prevno, c.ss.no, no)) @@ -4551,6 +4557,10 @@ func (c *Char) stateChange2() bool { return false } func (c *Char) changeStateEx(no int32, pn int, anim, ctrl int32, ffx string) { + // This is a very specific and undocumented Mugen behavior that probably resulted from Elecbyte misinterpreting fighting games + // It serves very little purpose while negatively affecting some new Ikemen features like NoTurnTarget + // It could be removed in the future + // https://github.com/ikemen-engine/Ikemen-GO/issues/1755 if c.minus <= 0 && c.scf(SCF_ctrl) && sys.roundState() <= 2 && (c.ss.stateType == ST_S || c.ss.stateType == ST_C) && !c.asf(ASF_noautoturn) && sys.stage.autoturn { c.autoTurn() @@ -4723,7 +4733,7 @@ func (c *Char) helperPos(pt PosType, pos [3]float32, facing int32, p[2] = c.pos[2]*(c.localscl/localscl) + pos[2] *dstFacing *= c.facing case PT_P2: - if p2 := sys.charList.enemyNear(c, 0, true, true, false); p2 != nil { + if p2 := sys.charList.enemyNear(c, 0, true, false); p2 != nil { p[0] = p2.pos[0]*(p2.localscl/localscl) + pos[0]*p2.facing p[1] = p2.pos[1]*(p2.localscl/localscl) + pos[1] p[2] = p2.pos[2]*(p2.localscl/localscl) + pos[2] @@ -4948,11 +4958,11 @@ func (c *Char) setPosX(x float32) { // We do this because Mugen is very sensitive to enemy position changes // Perhaps what it does is only calculate who "enemynear" is when the trigger is called? // "P2" enemy reference is less sensitive than this however - c.enemyNearClear() + c.enemyNearP2Clear() if c.player { for i := ^c.playerNo & 1; i < len(sys.chars); i += 2 { for j := range sys.chars[i] { - sys.chars[i][j].enemyNearClear() + sys.chars[i][j].enemyNearP2Clear() } } } @@ -6215,9 +6225,6 @@ func (c *Char) inputWait() bool { // This is not currently reproduced and may not be necessary } -func (c *Char) over() bool { - return c.scf(SCF_over) || c.ss.no == 5150 -} func (c *Char) makeDust(x, y, z float32) { if c.asf(ASF_nomakedust) { return @@ -7296,17 +7303,6 @@ func (c *Char) actionPrepare() { c.airJumpCount = 0 } if !c.hitPause() { - if !sys.roundEnd() { - if c.alive() && c.life > 0 { - c.unsetSCF(SCF_over | SCF_ko_round_middle) - } - if c.ss.no == 5150 || c.scf(SCF_over) { - c.setSCF(SCF_ko_round_middle) - } - } - if c.ss.no == 5150 && c.life <= 0 { - c.setSCF(SCF_over) - } c.specialFlag = 0 c.inputFlag = 0 c.setCSF(CSF_stagebound) @@ -7644,6 +7640,29 @@ func (c *Char) actionFinish() { // Update Z scale // Must be placed after posUpdate() c.zScale = sys.updateZScale(c.pos[2], c.localscl) + if !c.hitPause() && !c.pauseBool { + // Set KO flag + if c.life <= 0 && !sys.gsf(GSF_globalnoko) && !c.asf(ASF_noko) && (!c.ghv.guarded || !c.asf(ASF_noguardko)) { + // KO sound + if !sys.gsf(GSF_nokosnd) && c.alive() { + vo := int32(100) + c.playSound("", false, 0, 11, 0, -1, vo, 0, 1, c.localscl, &c.pos[0], false, 0, 0, 0, 0, false, false) + if c.gi().data.ko.echo != 0 { + c.koEchoTime = 1 + } + } + c.setSCF(SCF_ko) + c.unsetSCF(SCF_ctrl) // This can be seen in Mugen when you F1 a character + } + } + // Over flags (char is finished for the round) + if c.alive() && c.life > 0 && !sys.roundEnd() { + c.unsetSCF(SCF_over | SCF_ko_during_round) + } + if c.ss.no == 5150 { // Actual KO is not required in Mugen + c.setSCF(SCF_over_ko) + sys.charList.p2enemyDelete(c) // Every status change that invalidates the P2 reference must run this + } c.minus = 1 } func (c *Char) track() { @@ -7961,22 +7980,6 @@ func (c *Char) tick() { } } } - if !c.hitPause() && !c.pauseBool { - // Set KO flag - if c.life <= 0 && !sys.gsf(GSF_globalnoko) && !c.asf(ASF_noko) && (!c.ghv.guarded || !c.asf(ASF_noguardko)) { - // KO sound - if !sys.gsf(GSF_nokosnd) && c.alive() { - vo := int32(100) - c.playSound("", false, 0, 11, 0, -1, vo, 0, 1, c.localscl, &c.pos[0], false, 0, 0, 0, 0, false, false) - if c.gi().data.ko.echo != 0 { - c.koEchoTime = 1 - } - } - c.setSCF(SCF_ko) - c.unsetSCF(SCF_ctrl) // This can be seen in Mugen when you F1 a character - sys.charList.p2enemyDelete(c) - } - } // Reset pushed flag // This flag is apparently used to prevent position interpolation when chars push each other c.pushed = false @@ -9520,7 +9523,7 @@ func (cl *CharList) hitDetection(getter *Char, proj bool) { if !proj { getter.inguarddist = false getter.unsetCSF(CSF_gethit) - getter.enemyNearClear() + getter.enemyNearP2Clear() for _, c := range cl.runOrder { // Stop current iteration if this char is disabled @@ -10005,62 +10008,94 @@ func (cl *CharList) getHelperIndex(c *Char, id int32, ex bool) *Char { } return nil } + +// Remove player from P2 references if it becomes invalid (standby etc) func (cl *CharList) p2enemyDelete(c *Char) { for _, e := range cl.runOrder { - for i, p2cl := range e.p2enemy { + for i, p2cl := range e.p2EnemyList { if p2cl == c { - e.p2enemy = e.p2enemy[:i] + e.p2EnemyList = append(e.p2EnemyList[:i], e.p2EnemyList[i+1:]...) break } } } } -func (cl *CharList) enemyNear(c *Char, n int32, p2, ignoreDefeatedEnemy, log bool) *Char { + +// Update enemy near or "P2" lists and return specified index +// The current approach makes the distance calculation loops only be done when necessary, using cached enemies the rest of the time +// In Mugen the P2 enemy reference seems to only refresh at the start of each frame instead +func (cl *CharList) enemyNear(c *Char, n int32, p2list, log bool) *Char { + // Invalid reference if n < 0 { if log { sys.appendToConsole(c.warn() + fmt.Sprintf("has no nearest enemy: %v", n)) } return nil } - cache := &c.enemynear[Btoi(p2)] + // Select EnemyNear or P2 cache + var cache *[]*Char + if p2list { // List for P2 redirects as well as P4, P6 and P8 triggers + cache = &c.p2EnemyList + } else { + cache = &c.enemyNearList + } + // If we already have the Nth enemy cached, then return it if int(n) < len(*cache) { return (*cache)[n] } - if p2 { - cache = &c.p2enemy - } else { - *cache = (*cache)[:0] - } - var add func(*Char, int, float32) - add = func(e *Char, idx int, adddist float32) { + // Else reset the cache and start over + *cache = (*cache)[:0] + // Sort new enemy into cache, swapping if necessary + addEnemy := func(e *Char, idx int) { for i := idx; i <= int(n); i++ { + // Just append to the cache if the index is outside of it if i >= len(*cache) { *cache = append(*cache, e) return } - if AbsF(c.distX(e, c))+adddist < AbsF(c.distX((*cache)[i], c)) { - add((*cache)[i], i+1, adddist) - (*cache)[i] = e - return + // Otherwise compare the distances between the player and the next and previous enemies + distNext := c.distX(e, c) * c.facing + prevEnemy := (*cache)[i] + distPrev := c.distX(prevEnemy, c) * c.facing + // If an enemy is behind the player, an extra distance buffer is added for the "P2" list + // This makes the player turn less frequently when surrounded + // Mugen uses a hardcoded value of 30 pixels. Maybe it could be a character constant instead in Ikemen + if p2list { + if distNext < 0 { + distNext -= 30 + } + if distPrev < 0 { + distPrev -= 30 + } + } + // Swap enemy places if applicable + if AbsF(distNext) < AbsF(distPrev) { + (*cache)[i] = e // Next enemy takes previous enemy place + e = prevEnemy // Previous enemy is sorted in the next loop iteration } } } + // Search valid enemies for _, e := range cl.runOrder { - if e.player && e.teamside != c.teamside && !e.scf(SCF_standby) { - if p2 && !e.scf(SCF_ko_round_middle) { - add(e, 0, 30) + if e.player && e.teamside != c.teamside { + // P2 checks for alive enemies even if they are player type helpers + if p2list && !e.scf(SCF_standby) && !e.scf(SCF_over_ko) { + addEnemy(e, 0) } - if !p2 && e.helperIndex == 0 && (!ignoreDefeatedEnemy || ignoreDefeatedEnemy && (!e.scf(SCF_ko_round_middle) || sys.roundEnd())) { - add(e, 0, 0) + // EnemyNear checks for dead or alive root players + if !p2list && e.helperIndex == 0 { + addEnemy(e, 0) } } } + // If reference exceeds number of valid enemies if int(n) >= len(*cache) { if log { sys.appendToConsole(c.warn() + fmt.Sprintf("has no nearest enemy: %v", n)) } return nil } + // Return Nth enemy return (*cache)[n] } diff --git a/src/script.go b/src/script.go index fa97d115..f786cea1 100644 --- a/src/script.go +++ b/src/script.go @@ -3034,7 +3034,7 @@ func triggerFunctions(l *lua.LState) { if !nilArg(l, 1) { n = int32(numArg(l, 1)) } - if c := sys.debugWC.enemyNear(n); c != nil { + if c := sys.debugWC.enemyNearTrigger(n); c != nil { sys.debugWC, ret = c, true } l.Push(lua.LBool(ret)) @@ -4169,8 +4169,7 @@ func triggerFunctions(l *lua.LState) { l.Push(lua.LString("")) } } else { - if p := sys.charList.enemyNear(sys.debugWC, n/2-1, true, true, false); p != nil && - !(p.scf(SCF_ko) && p.scf(SCF_over)) { + if p := sys.charList.enemyNear(sys.debugWC, n/2-1, true, false); p != nil { l.Push(lua.LString(p.name)) } else { l.Push(lua.LString("")) @@ -5399,7 +5398,7 @@ func triggerFunctions(l *lua.LState) { case "over": l.Push(lua.LBool(sys.debugWC.scf(SCF_over))) case "koroundmiddle": - l.Push(lua.LBool(sys.debugWC.scf(SCF_ko_round_middle))) + l.Push(lua.LBool(sys.debugWC.scf(SCF_ko_during_round))) case "disabled": l.Push(lua.LBool(sys.debugWC.scf(SCF_disabled))) default: diff --git a/src/system.go b/src/system.go index d6480eb5..e5068d51 100644 --- a/src/system.go +++ b/src/system.go @@ -2291,7 +2291,7 @@ func (s *System) fight() (reload bool) { tmp.RawSetString("winHyper", lua.LBool(p[0].winType(WT_Hyper))) tmp.RawSetString("drawgame", lua.LBool(p[0].drawgame())) tmp.RawSetString("ko", lua.LBool(p[0].scf(SCF_ko))) - tmp.RawSetString("ko_round_middle", lua.LBool(p[0].scf(SCF_ko_round_middle))) + tmp.RawSetString("ko_round_middle", lua.LBool(p[0].scf(SCF_ko_during_round))) tbl_roundNo.RawSetInt(p[0].playerNo+1, tmp) } } From d170765a372b89b69acf7d79b052a7f5a4a30797 Mon Sep 17 00:00:00 2001 From: PotS Date: Wed, 1 Jan 2025 12:24:23 +0000 Subject: [PATCH 2/5] refactor: over and ko_round_middle flags - These flags were a bit confusing, so they were renamed "over_alive" and "over_ko". This also allowed simplifying the logic a bit since it became easier to follow --- src/char.go | 6 +++--- src/lifebar.go | 4 ++-- src/script.go | 6 ++---- src/system.go | 10 +++++----- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/char.go b/src/char.go index b74e0e90..752ef011 100644 --- a/src/char.go +++ b/src/char.go @@ -20,8 +20,8 @@ const ( SCF_guard SCF_guardbreak SCF_ko - SCF_ko_during_round - SCF_over + SCF_over_alive // Has reached win or lose poses + SCF_over_ko // Has reached state 5150 SCF_standby ) @@ -7657,7 +7657,7 @@ func (c *Char) actionFinish() { } // Over flags (char is finished for the round) if c.alive() && c.life > 0 && !sys.roundEnd() { - c.unsetSCF(SCF_over | SCF_ko_during_round) + c.unsetSCF(SCF_over_alive | SCF_over_ko) } if c.ss.no == 5150 { // Actual KO is not required in Mugen c.setSCF(SCF_over_ko) diff --git a/src/lifebar.go b/src/lifebar.go index 33c9bf97..2a82d72e 100644 --- a/src/lifebar.go +++ b/src/lifebar.go @@ -335,7 +335,7 @@ func (hb *HealthBar) step(ref int, hbr *HealthBar) { var life float32 = float32(sys.chars[ref][0].life) / float32(sys.chars[ref][0].lifeMax) //redlife := (float32(sys.chars[ref][0].life) + float32(sys.chars[ref][0].redLife)) / float32(sys.chars[ref][0].lifeMax) var redVal int32 = sys.chars[ref][0].redLife - sys.chars[ref][0].life - var getHit bool = (sys.chars[ref][0].receivedHits != 0 || sys.chars[ref][0].ss.moveType == MT_H) && !sys.chars[ref][0].scf(SCF_over) + var getHit bool = (sys.chars[ref][0].receivedHits != 0 || sys.chars[ref][0].ss.moveType == MT_H) && !sys.chars[ref][0].scf(SCF_over_alive) if hbr.toplife > life { hbr.toplife += (life - hbr.toplife) / 2 @@ -3920,7 +3920,7 @@ func (l *Lifebar) step() { cb, cd, cp, dz := [2]int32{}, [2]int32{}, [2]float32{}, [2]bool{} for i, ch := range sys.chars { for _, c := range ch { - if c.alive() || !c.scf(SCF_over) { + if c.alive() || !c.scf(SCF_over_alive) { if c.receivedHits > cb[^i&1] { cb[^i&1] = Clamp(cb[^i&1], c.receivedHits, 999) cd[^i&1] = Max(c.receivedDmg, cd[^i&1]) diff --git a/src/script.go b/src/script.go index f786cea1..e93eaa6e 100644 --- a/src/script.go +++ b/src/script.go @@ -5395,12 +5395,10 @@ func triggerFunctions(l *lua.LState) { case "roundnotskip": l.Push(lua.LBool(sys.gsf(GSF_roundnotskip))) // SystemCharFlag - case "over": - l.Push(lua.LBool(sys.debugWC.scf(SCF_over))) - case "koroundmiddle": - l.Push(lua.LBool(sys.debugWC.scf(SCF_ko_during_round))) case "disabled": l.Push(lua.LBool(sys.debugWC.scf(SCF_disabled))) + case "over": + l.Push(lua.LBool(sys.debugWC.scf(SCF_over_alive) || sys.debugWC.scf(SCF_over_ko))) default: l.RaiseError("\nInvalid argument: %v\n", strArg(l, 1)) } diff --git a/src/system.go b/src/system.go index e5068d51..6489fa83 100644 --- a/src/system.go +++ b/src/system.go @@ -1255,7 +1255,7 @@ func (s *System) action() { for _, p := range s.chars { if len(p) > 0 { if p[0].alive() { - p[0].unsetSCF(SCF_over) + p[0].unsetSCF(SCF_over_alive) if !p[0].scf(SCF_standby) || p[0].teamside == -1 { p[0].setCtrl(true) if p[0].ss.no != 0 && !p[0].asf(ASF_nointroreset) { @@ -1418,7 +1418,7 @@ func (s *System) action() { // Check if this player is ready to proceed to roundstate 4 // TODO: The game should normally only wait for players that are active in the fight // || p[0].teamside == -1 || p[0].scf(SCF_standby) // TODO: This could be manageable from the char's side with an AssertSpecial or such - if p[0].scf(SCF_over) || p[0].ss.no == 5150 || + if p[0].scf(SCF_over_alive) || p[0].scf(SCF_over_ko) || (p[0].scf(SCF_ctrl) && p[0].ss.moveType == MT_I && p[0].ss.stateType != ST_A && p[0].ss.stateType != ST_L) { continue } @@ -1482,8 +1482,8 @@ func (s *System) action() { } } // TODO: These changestates ought to be unhardcoded - if !p[0].scf(SCF_over) && !p[0].hitPause() && p[0].alive() && p[0].animNo != 5 { - p[0].setSCF(SCF_over) + if !p[0].scf(SCF_over_alive) && !p[0].hitPause() && p[0].alive() && p[0].animNo != 5 { + p[0].setSCF(SCF_over_alive) if p[0].win() { p[0].selfState(180, -1, -1, -1, "") } else if p[0].lose() { @@ -2291,7 +2291,7 @@ func (s *System) fight() (reload bool) { tmp.RawSetString("winHyper", lua.LBool(p[0].winType(WT_Hyper))) tmp.RawSetString("drawgame", lua.LBool(p[0].drawgame())) tmp.RawSetString("ko", lua.LBool(p[0].scf(SCF_ko))) - tmp.RawSetString("ko_round_middle", lua.LBool(p[0].scf(SCF_ko_during_round))) + tmp.RawSetString("over_ko", lua.LBool(p[0].scf(SCF_over_ko))) tbl_roundNo.RawSetInt(p[0].playerNo+1, tmp) } } From 3a721f21d90397652eb60ed41f3d479852a9738e Mon Sep 17 00:00:00 2001 From: PotS Date: Tue, 31 Dec 2024 14:23:55 +0000 Subject: [PATCH 3/5] fix: powerbar playing sound 0,0 - Level sounds are now all correctly initialized to -1,-1 --- src/lifebar.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lifebar.go b/src/lifebar.go index 2a82d72e..c1beaa69 100644 --- a/src/lifebar.go +++ b/src/lifebar.go @@ -507,14 +507,19 @@ type PowerBar struct { } func newPowerBar() *PowerBar { - return &PowerBar{ - level_snd: [9][2]int32{{-1}, {-1}, {-1}}, + newBar := &PowerBar{ front: make(map[int32]*AnimLayout), bg0: make(map[int32]*AnimLayout), counter_rounding: 1000, value_rounding: 1, } + // Default power level sounds to -1,-1 + for i := range newBar.level_snd { + newBar.level_snd[i] = [2]int32{-1, -1} + } + return newBar } + func readPowerBar(pre string, is IniSection, sff *Sff, at AnimationTable, f []*Fnt) *PowerBar { pb := newPowerBar() @@ -548,10 +553,8 @@ func readPowerBar(pre string, is IniSection, } // Level sounds. for i := range pb.level_snd { - if !is.ReadI32(fmt.Sprintf("%vlevel%v.snd", pre, i+1), &pb.level_snd[i][0], - &pb.level_snd[i][1]) { - is.ReadI32(fmt.Sprintf("level%v.snd", i+1), &pb.level_snd[i][0], - &pb.level_snd[i][1]) + if !is.ReadI32(fmt.Sprintf("%vlevel%v.snd", pre, i+1), &pb.level_snd[i][0], &pb.level_snd[i][1]) { + is.ReadI32(fmt.Sprintf("level%v.snd", i+1), &pb.level_snd[i][0], &pb.level_snd[i][1]) } } is.ReadBool(pre+"levelbars", &pb.levelbars) From a666e1e493cf413f9efa1d5555aaac17d3fd7eb2 Mon Sep 17 00:00:00 2001 From: PotS Date: Tue, 31 Dec 2024 16:24:13 +0000 Subject: [PATCH 4/5] fix: NoPowerBarDisplay - Fixed power bar still rendering if the currently shown bar belongs to a player with NoPowerBarDisplay asserted (when PowerShare is false), or rendering in the wrong place - Refactored some drawing loops to be less redundant --- src/lifebar.go | 93 +++++++++++++++++--------------------------------- 1 file changed, 31 insertions(+), 62 deletions(-) diff --git a/src/lifebar.go b/src/lifebar.go index c1beaa69..09e0d303 100644 --- a/src/lifebar.go +++ b/src/lifebar.go @@ -560,6 +560,7 @@ func readPowerBar(pre string, is IniSection, is.ReadBool(pre+"levelbars", &pb.levelbars) return pb } + func (pb *PowerBar) step(ref int, pbr *PowerBar, snd *Snd) { pbval := sys.chars[ref][0].getPower() power := float32(pbval) / float32(sys.chars[ref][0].powerMax) @@ -581,8 +582,10 @@ func (pb *PowerBar) step(ref int, pbr *PowerBar, snd *Snd) { // Level sounds // TODO: These probably shouldn't play when the powerbar is invisible if level > pbr.prevLevel { - i := Min(8, level-1) - snd.play(pb.level_snd[i], 100, 0, 0, 0, 0) + i := int(level - 1) + if i >= 0 && i < len(pb.level_snd) { + snd.play(pb.level_snd[i], 100, 0, 0, 0, 0) + } } pbr.prevLevel = level var fv1 int32 @@ -605,6 +608,7 @@ func (pb *PowerBar) step(ref int, pbr *PowerBar, snd *Snd) { pb.front[fv2].Action() pb.shift.Action() } + func (pb *PowerBar) reset() { for _, v := range pb.bg0 { v.Reset() @@ -4098,85 +4102,60 @@ func (l *Lifebar) draw(layerno int16) { if sys.statusDraw && l.active { if !sys.gsf(GSF_nobardisplay) && l.bars { // HealthBar - for ti := range sys.tmode { - for i := range l.order[ti] { - l.hb[l.ref[ti]][i*2+ti].bgDraw(layerno) - } - } for ti := range sys.tmode { for i, v := range l.order[ti] { - l.hb[l.ref[ti]][i*2+ti].draw(layerno, v, l.hb[l.ref[ti]][v], l.fnt[:]) + index := i*2 + ti + l.hb[l.ref[ti]][index].bgDraw(layerno) + l.hb[l.ref[ti]][index].draw(layerno, v, l.hb[l.ref[ti]][v], l.fnt[:]) } } // PowerBar - for ti, tm := range sys.tmode { - for i := range l.order[ti] { - if !sys.chars[i*2+ti][0].asf(ASF_nopowerbardisplay) { - if sys.cfg.Options.Team.PowerShare && (tm == TM_Simul || tm == TM_Tag) { - if i == 0 { - l.pb[l.ref[ti]][i*2+ti].bgDraw(layerno, i*2+ti) - } - } else { - l.pb[l.ref[ti]][i*2+ti].bgDraw(layerno, i*2+ti) - } - } - } - } for ti, tm := range sys.tmode { for i, v := range l.order[ti] { - if !sys.chars[i*2+ti][0].asf(ASF_nopowerbardisplay) { - if sys.cfg.Options.Team.PowerShare && (tm == TM_Simul || tm == TM_Tag) { - if i == 0 { - l.pb[l.ref[ti]][i*2+ti].draw(layerno, i*2+ti, l.pb[l.ref[ti]][i*2+ti], l.fnt[:]) - } - } else { - l.pb[l.ref[ti]][i*2+ti].draw(layerno, v, l.pb[l.ref[ti]][v], l.fnt[:]) + index := i*2 + ti + if sys.cfg.Options.Team.PowerShare && (tm == TM_Simul || tm == TM_Tag) { // Draw player 1 or 2 bars + if i == 0 && !sys.chars[0][0].asf(ASF_nopowerbardisplay) { + l.pb[l.ref[ti]][index].bgDraw(layerno, index) + l.pb[l.ref[ti]][index].draw(layerno, index, l.pb[l.ref[ti]][index], l.fnt[:]) + } + } else { // Draw everyone's bars + if !sys.chars[v][0].asf(ASF_nopowerbardisplay) { + l.pb[l.ref[ti]][index].bgDraw(layerno, index) + l.pb[l.ref[ti]][index].draw(layerno, v, l.pb[l.ref[ti]][v], l.fnt[:]) } } } } // GuardBar - for ti := range sys.tmode { - for i := range l.order[ti] { - l.gb[l.ref[ti]][i*2+ti].bgDraw(layerno) - } - } for ti := range sys.tmode { for i, v := range l.order[ti] { - l.gb[l.ref[ti]][i*2+ti].draw(layerno, v, l.gb[l.ref[ti]][v], l.fnt[:]) + index := i*2 + ti + l.gb[l.ref[ti]][index].bgDraw(layerno) + l.gb[l.ref[ti]][index].draw(layerno, v, l.gb[l.ref[ti]][v], l.fnt[:]) } } // StunBar - for ti := range sys.tmode { - for i := range l.order[ti] { - l.sb[l.ref[ti]][i*2+ti].bgDraw(layerno) - } - } for ti := range sys.tmode { for i, v := range l.order[ti] { - l.sb[l.ref[ti]][i*2+ti].draw(layerno, v, l.sb[l.ref[ti]][v], l.fnt[:]) + index := i*2 + ti + l.sb[l.ref[ti]][index].bgDraw(layerno) + l.sb[l.ref[ti]][index].draw(layerno, v, l.sb[l.ref[ti]][v], l.fnt[:]) } } // LifeBarFace - for ti := range sys.tmode { - for i := range l.order[ti] { - l.fa[l.ref[ti]][i*2+ti].bgDraw(layerno) - } - } for ti := range sys.tmode { for i, v := range l.order[ti] { - l.fa[l.ref[ti]][i*2+ti].draw(layerno, v, l.fa[l.ref[ti]][v]) + index := i*2 + ti + l.fa[l.ref[ti]][index].bgDraw(layerno) + l.fa[l.ref[ti]][index].draw(layerno, v, l.fa[l.ref[ti]][v]) } } // LifeBarName - for ti := range sys.tmode { - for i := range l.order[ti] { - l.nm[l.ref[ti]][i*2+ti].bgDraw(layerno) - } - } for ti := range sys.tmode { for i, v := range l.order[ti] { - l.nm[l.ref[ti]][i*2+ti].draw(layerno, v, l.fnt[:], ti) + index := i*2 + ti + l.nm[l.ref[ti]][index].bgDraw(layerno) + l.nm[l.ref[ti]][index].draw(layerno, v, l.fnt[:], ti) } } // LifeBarTime @@ -4191,12 +4170,6 @@ func (l *Lifebar) draw(layerno int16) { if tm == TM_Turns { if rl := sys.chars[ti][0].ocd().ratioLevel; rl > 0 { l.ra[ti].bgDraw(layerno) - } - } - } - for ti, tm := range sys.tmode { - if tm == TM_Turns { - if rl := sys.chars[ti][0].ocd().ratioLevel; rl > 0 { l.ra[ti].draw(layerno, rl-1) } } @@ -4217,15 +4190,11 @@ func (l *Lifebar) draw(layerno int16) { // LifeBarAiLevel for i := range l.ai { l.ai[i].bgDraw(layerno) - } - for i := range l.ai { l.ai[i].draw(layerno, l.fnt[:], sys.com[sys.chars[i][0].playerNo]) } // LifeBarWinCount for i := range l.wc { l.wc[i].bgDraw(layerno) - } - for i := range l.wc { l.wc[i].draw(layerno, l.fnt[:], i) } } From 3c9ee92ae172ad65243f67348499b6b51f45b2b2 Mon Sep 17 00:00:00 2001 From: PotS Date: Fri, 3 Jan 2025 20:35:01 +0000 Subject: [PATCH 5/5] fix: time over perfects - Fixed time over victories not awarding perfects - Refactored surrounding code a bit --- src/system.go | 95 +++++++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 53 deletions(-) diff --git a/src/system.go b/src/system.go index 6489fa83..7d254ae7 100644 --- a/src/system.go +++ b/src/system.go @@ -1271,88 +1271,75 @@ func (s *System) action() { (s.super <= 0 || !s.superpausebg) && (s.pause <= 0 || !s.pausebg) { s.time-- } + + // Check if round ended by KO or time over and set win types fin := func() bool { + checkPerfect := func(team int) bool { + for i := team; i < MaxSimul*2; i += 2 { + if len(s.chars[i]) > 0 && + s.chars[i][0].life < s.chars[i][0].lifeMax { + return false + } + } + return true + } if s.intro > 0 { return false } + // KO ko := [...]bool{true, true} - for ii := range ko { - for i := ii; i < MaxSimul*2; i += 2 { + for loser := range ko { + // Check if all players or leader on one side are KO + for i := loser; i < MaxSimul*2; i += 2 { if len(s.chars[i]) > 0 && s.chars[i][0].teamside != -1 { if s.chars[i][0].alive() { - ko[ii] = false + ko[loser] = false } else if (s.tmode[i&1] == TM_Simul && s.cfg.Options.Simul.LoseOnKO && s.com[i] == 0) || (s.tmode[i&1] == TM_Tag && s.cfg.Options.Tag.LoseOnKO) { - ko[ii] = true + ko[loser] = true break } } } - if ko[ii] { - i := ii ^ 1 - for ; i < MaxSimul*2; i += 2 { - if len(s.chars[i]) > 0 && s.chars[i][0].life < - s.chars[i][0].lifeMax { - break - } - } - if i >= MaxSimul*2 { - s.winType[ii^1].SetPerfect() + if ko[loser] { + if checkPerfect(loser^1) { + s.winType[loser^1].SetPerfect() } } } + // Time over ft := s.finishType if s.time == 0 { + s.winType[0], s.winType[1] = WT_Time, WT_Time l := [2]float32{} - for i := 0; i < 2; i++ { + for i := 0; i < 2; i++ { // Check life percentage of each team for j := i; j < MaxSimul*2; j += 2 { if len(s.chars[j]) > 0 { if s.tmode[i] == TM_Simul || s.tmode[i] == TM_Tag { - l[i] += (float32(s.chars[j][0].life) / - float32(s.numSimul[i])) / - float32(s.chars[j][0].lifeMax) + l[i] += (float32(s.chars[j][0].life) / float32(s.numSimul[i])) / float32(s.chars[j][0].lifeMax) } else { - l[i] += float32(s.chars[j][0].life) / - float32(s.chars[j][0].lifeMax) + l[i] += float32(s.chars[j][0].life) / float32(s.chars[j][0].lifeMax) } } } } - if l[0] > l[1] { - p := true - for i := 0; i < MaxSimul*2; i += 2 { - if len(s.chars[i]) > 0 && - s.chars[i][0].life < s.chars[i][0].lifeMax { - p = false - break - } + // Some other methods were considered to make the winner decision more fair, like a minimum % difference + // But ultimately a direct comparison seems to be the fairest method + if math.Round(float64(l[0] * 1000)) != math.Round(float64(l[1] * 1000)) && // Convert back to 1000 life points scale then round it to reduce calculation errors + ((l[0] >= float32(1.0)) != (l[1] >= float32(1.0))) { // But make sure the rounding doesn't turn a perfect into a draw game + winner := 0 + if l[0] < l[1] { + winner = 1 } - if p { - s.winType[0].SetPerfect() + if checkPerfect(winner) { + s.winType[winner].SetPerfect() } s.finishType = FT_TO - s.winTeam = 0 - } else if l[0] < l[1] { - p := true - for i := 1; i < MaxSimul*2; i += 2 { - if len(s.chars[i]) > 0 && - s.chars[i][0].life < s.chars[i][0].lifeMax { - p = false - break - } - } - if p { - s.winType[1].SetPerfect() - } - s.finishType = FT_TO - s.winTeam = 1 - } else { + s.winTeam = winner + } else { // Draw game s.finishType = FT_TODraw s.winTeam = -1 } - if !(ko[0] || ko[1]) { - s.winType[0], s.winType[1] = WT_Time, WT_Time - } } if s.intro >= -1 && (ko[0] || ko[1]) { if ko[0] && ko[1] { @@ -1363,16 +1350,16 @@ func (s *System) action() { s.winTeam = int(Btoi(ko[0])) } } + // Update win triggers if finish type was changed if ft != s.finishType { - for i, p := range sys.chars { + for i, p := range s.chars { if len(p) > 0 && ko[^i&1] { for _, h := range p { for _, tid := range h.targets { - if t := sys.playerID(tid); t != nil { + if t := s.playerID(tid); t != nil { if t.ghv.attr&int32(AT_AH) != 0 { s.winTrigger[i&1] = WT_Hyper - } else if t.ghv.attr&int32(AT_AS) != 0 && - s.winTrigger[i&1] == WT_Normal { + } else if t.ghv.attr&int32(AT_AS) != 0 && s.winTrigger[i&1] == WT_Normal { s.winTrigger[i&1] = WT_Special } } @@ -1383,6 +1370,8 @@ func (s *System) action() { } return ko[0] || ko[1] || s.time == 0 } + + // Post round if s.roundEnd() || fin() { rs4t := -s.lifebar.ro.over_waittime s.intro--