From d5f001294db4041e1274f996cad743853184e736 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Sun, 21 Jan 2024 11:51:49 -0800 Subject: [PATCH 1/2] Remove extraneous calls to WaitForMana() --- sim/hunter/explosive_trap.go | 8 ++------ sim/mage/water_elemental.go | 4 +--- sim/shaman/fire_elemental_pet.go | 1 - sim/warlock/pet.go | 4 +--- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/sim/hunter/explosive_trap.go b/sim/hunter/explosive_trap.go index acd55ed687..1f8999fa1d 100644 --- a/sim/hunter/explosive_trap.go +++ b/sim/hunter/explosive_trap.go @@ -113,11 +113,9 @@ func (hunter *Hunter) registerExplosiveTrapSpell(timer *core.Timer) { hunter.AutoAttacks.DelayRangedUntil(sim, doneAt+time.Millisecond*500) if layTrapAt == sim.CurrentTime { - success := hunter.ExplosiveTrap.Cast(sim, target) + hunter.ExplosiveTrap.Cast(sim, target) if doneAt > hunter.GCD.ReadyAt() { hunter.GCD.Set(doneAt) - } else if !success { - hunter.WaitForMana(sim, hunter.ExplosiveTrap.CurCast.Cost) } } else { // Make sure the GCD doesn't get used while we're waiting. @@ -127,11 +125,9 @@ func (hunter *Hunter) registerExplosiveTrapSpell(timer *core.Timer) { DoAt: layTrapAt, OnAction: func(sim *core.Simulation) { hunter.GCD.Reset() - success := hunter.ExplosiveTrap.Cast(sim, target) + hunter.ExplosiveTrap.Cast(sim, target) if doneAt > hunter.GCD.ReadyAt() { hunter.GCD.Set(doneAt) - } else if !success { - hunter.WaitForMana(sim, hunter.ExplosiveTrap.CurCast.Cost) } }, }) diff --git a/sim/mage/water_elemental.go b/sim/mage/water_elemental.go index 5fd71e77d7..6ff0423806 100644 --- a/sim/mage/water_elemental.go +++ b/sim/mage/water_elemental.go @@ -97,9 +97,7 @@ func (we *WaterElemental) ExecuteCustomRotation(sim *core.Simulation) { return } - if success := spell.Cast(sim, we.CurrentTarget); !success { - we.WaitForMana(sim, spell.CurCast.Cost) - } + spell.Cast(sim, we.CurrentTarget) } // These numbers are just rough guesses based on looking at some logs. diff --git a/sim/shaman/fire_elemental_pet.go b/sim/shaman/fire_elemental_pet.go index 879a5e56dc..6cc7a0a007 100644 --- a/sim/shaman/fire_elemental_pet.go +++ b/sim/shaman/fire_elemental_pet.go @@ -103,7 +103,6 @@ func (fireElemental *FireElemental) ExecuteCustomRotation(sim *core.Simulation) } if fireElemental.FireNova.DefaultCast.Cost > fireElemental.CurrentMana() { - fireElemental.WaitForMana(sim, fireElemental.FireNova.DefaultCast.Cost) return } diff --git a/sim/warlock/pet.go b/sim/warlock/pet.go index 16bcb49c3e..bdcd0c0645 100644 --- a/sim/warlock/pet.go +++ b/sim/warlock/pet.go @@ -303,9 +303,7 @@ func (wp *WarlockPet) ExecuteCustomRotation(sim *core.Simulation) { return } - if success := wp.primaryAbility.Cast(sim, wp.CurrentTarget); !success { - wp.WaitForMana(sim, wp.primaryAbility.CurCast.Cost) - } + wp.primaryAbility.Cast(sim, wp.CurrentTarget) } func (warlock *Warlock) makeStatInheritance() core.PetStatInheritance { From 0e51f4d20747a5ef42d60f04ad2740b7a97a0c37 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Sun, 21 Jan 2024 13:16:42 -0800 Subject: [PATCH 2/2] Fix OOM calcs and more cleanup --- sim/common/gcd_scheduler.go | 300 --------------------------------- sim/common/wait.go | 83 --------- sim/core/apl.go | 3 - sim/core/cast.go | 2 +- sim/core/energy.go | 4 +- sim/core/focus.go | 2 +- sim/core/gcd.go | 90 ---------- sim/core/mana.go | 48 +++++- sim/core/metrics_aggregator.go | 6 +- sim/core/rage.go | 2 +- sim/core/runic_power.go | 2 +- sim/core/spell.go | 12 +- sim/core/unit.go | 24 +-- 13 files changed, 58 insertions(+), 520 deletions(-) delete mode 100644 sim/common/gcd_scheduler.go delete mode 100644 sim/common/wait.go diff --git a/sim/common/gcd_scheduler.go b/sim/common/gcd_scheduler.go deleted file mode 100644 index 4150e9347f..0000000000 --- a/sim/common/gcd_scheduler.go +++ /dev/null @@ -1,300 +0,0 @@ -package common - -// Helper module for planning GCD-bound actions in advance. - -import ( - "fmt" - "time" - - "github.com/wowsims/wotlk/sim/core" -) - -const Unresolved = time.Duration(-1) - -// Returns whether the cast was successful. -type AbilityCaster func(sim *core.Simulation) bool - -type ScheduledAbility struct { - // When to cast this ability. Might not cast at this time if there are conflicts. - DesiredCastAt time.Duration - - // Limits the search window for conflict resolution. - MinCastAt time.Duration - MaxCastAt time.Duration - - // Override the default conflict resolution behavior of searching after the - // desired cast time first. Instead, search before the desired cast time first. - PrioritizeEarlierForConflicts bool - - // How much GCD time will be used by this ability. - Duration time.Duration - - // How to cast this ability. - TryCast AbilityCaster - - // When the ability will be cast. - castAt time.Duration - - // When the ability cast will be completed. - doneAt time.Duration -} - -type GCDScheduler struct { - // Scheduled abilities, sorted from soonest to latest CastAt time. - schedule []ScheduledAbility - - nextAbilityIndex int - - managedMCDIDs []core.ActionID - managedMCDs []*core.MajorCooldown -} - -// Returns the actual time at which the ability will be cast. -func (gs *GCDScheduler) Schedule(newAbility ScheduledAbility) time.Duration { - newAbility.castAt = newAbility.DesiredCastAt - newAbility.doneAt = newAbility.DesiredCastAt + newAbility.Duration - - oldLen := len(gs.schedule) - if oldLen == 0 { - gs.schedule = append(gs.schedule, newAbility) - return newAbility.castAt - } - - // Find the index at which this ability should be inserted, ignoring priority for now. - var desiredIndex = 0 - for _, scheduledAbility := range gs.schedule { - if newAbility.castAt < scheduledAbility.castAt { - break - } - desiredIndex++ - } - - // If the insert was at the end with no overlap, can just append. - if desiredIndex == oldLen && gs.schedule[oldLen-1].doneAt <= newAbility.castAt { - gs.schedule = append(gs.schedule, newAbility) - return newAbility.castAt - } - - conflictBefore := desiredIndex > 0 && gs.schedule[desiredIndex-1].doneAt > newAbility.castAt - conflictAfter := desiredIndex < oldLen && gs.schedule[desiredIndex].castAt < newAbility.doneAt - if !conflictBefore && !conflictAfter { - gs.schedule = append(gs.schedule, newAbility) - copy(gs.schedule[desiredIndex+1:], gs.schedule[desiredIndex:]) - gs.schedule[desiredIndex] = newAbility - return newAbility.castAt - } - - // If we're here, we have a conflict. - var castAt time.Duration - if newAbility.PrioritizeEarlierForConflicts { - castAt = gs.scheduleBefore(newAbility, desiredIndex, conflictAfter) - if castAt == Unresolved { - castAt = gs.scheduleAfter(newAbility, desiredIndex, conflictBefore) - } - } else { - castAt = gs.scheduleAfter(newAbility, desiredIndex, conflictBefore) - if castAt == Unresolved { - castAt = gs.scheduleBefore(newAbility, desiredIndex, conflictAfter) - } - } - return castAt -} - -func (gs *GCDScheduler) scheduleBefore(newAbility ScheduledAbility, desiredIndex int, conflictAfter bool) time.Duration { - curCastAt := newAbility.castAt - if conflictAfter { - curCastAt = gs.schedule[desiredIndex].castAt - newAbility.Duration - } - - curIndex := desiredIndex - for curIndex >= 0 && curCastAt >= newAbility.MinCastAt { - conflictBefore := curIndex > 0 && gs.schedule[curIndex-1].doneAt > curCastAt - if conflictBefore { - curCastAt = gs.schedule[curIndex-1].castAt - newAbility.Duration - curIndex-- - } else { - newAbility.castAt = curCastAt - newAbility.doneAt = curCastAt + newAbility.Duration - - gs.schedule = append(gs.schedule, newAbility) - copy(gs.schedule[curIndex+1:], gs.schedule[curIndex:]) - gs.schedule[curIndex] = newAbility - - return newAbility.castAt - } - } - - return Unresolved -} - -func (gs *GCDScheduler) scheduleAfter(newAbility ScheduledAbility, desiredIndex int, conflictBefore bool) time.Duration { - curCastAt := newAbility.castAt - if conflictBefore { - curCastAt = gs.schedule[desiredIndex-1].doneAt - } - - curIndex := desiredIndex - oldLen := len(gs.schedule) - for curIndex <= oldLen && curCastAt <= newAbility.MaxCastAt { - conflictAfter := curIndex < oldLen && gs.schedule[curIndex].castAt < curCastAt+newAbility.Duration - if conflictAfter { - curCastAt = gs.schedule[curIndex].doneAt - curIndex++ - } else { - newAbility.castAt = curCastAt - newAbility.doneAt = curCastAt + newAbility.Duration - - gs.schedule = append(gs.schedule, newAbility) - copy(gs.schedule[curIndex+1:], gs.schedule[curIndex:]) - gs.schedule[curIndex] = newAbility - - return newAbility.castAt - } - } - - return Unresolved -} - -// Schedules a group of abilities that must be cast back-to-back. -// Most settings are taken from the first ability. -func (gs *GCDScheduler) ScheduleGroup(newAbilities []ScheduledAbility) time.Duration { - if len(newAbilities) == 0 { - panic("Empty ability group!") - } - - totalDuration := time.Duration(0) - for _, newAbility := range newAbilities { - totalDuration += newAbility.Duration - } - - // Schedule a 'group ability' which is just a fake ability for reserving the time slots. - groupAbility := ScheduledAbility{ - DesiredCastAt: newAbilities[0].DesiredCastAt, - MinCastAt: newAbilities[0].MinCastAt, - MaxCastAt: newAbilities[0].MaxCastAt, - PrioritizeEarlierForConflicts: newAbilities[0].PrioritizeEarlierForConflicts, - Duration: totalDuration, - } - groupCastAt := gs.Schedule(groupAbility) - - if groupCastAt == Unresolved { - return Unresolved - } - - // Update internals for the individual abilities, now that we know when they'll be cast. - nextCastAt := groupCastAt - for i := range newAbilities { - newAbilities[i].castAt = nextCastAt - newAbilities[i].doneAt = nextCastAt + newAbilities[i].Duration - nextCastAt = newAbilities[i].doneAt - } - - // Replace the group ability with the individual abilities. - for i, ability := range gs.schedule { - if ability.castAt == groupCastAt { - temp := make([]ScheduledAbility, len(gs.schedule)+len(newAbilities)-1) - for j := 0; j < i; j++ { - temp[j] = gs.schedule[j] - } - for j := 0; j < len(newAbilities); j++ { - temp[i+j] = newAbilities[j] - } - for j := i + 1; j < len(gs.schedule); j++ { - temp[j+len(newAbilities)-1] = gs.schedule[j] - } - gs.schedule = temp - break - } - } - - return groupCastAt -} - -// Takes ownership of a MCD, adding it to the schedule and removing it from the -// character's managed cooldowns. -func (gs *GCDScheduler) ScheduleMCD(character *core.Character, mcdID core.ActionID) { - mcd := character.GetInitialMajorCooldown(mcdID) - if mcd.Spell == nil { - panic("No spell for MCD: " + mcdID.String()) - } - if !mcd.Spell.ActionID.SameAction(mcdID) { - return - } - - mcdIdx := len(gs.managedMCDIDs) - gs.managedMCDIDs = append(gs.managedMCDIDs, mcdID) - gs.managedMCDs = append(gs.managedMCDs, nil) - - mcdAction := ScheduledAbility{ - Duration: core.GCDDefault, - TryCast: func(sim *core.Simulation) bool { - success := gs.managedMCDs[mcdIdx].TryActivate(sim, character) - if success { - character.UpdateMajorCooldowns() - } else { - gs.managedMCDs[mcdIdx].Enable() - gs.managedMCDs[mcdIdx].Spell.DefaultCast.GCD = 0 - } - return success - }, - } - timings := mcd.GetTimings() - curTime := time.Duration(0) - maxDuration := character.Env.GetMaxDuration() - i := 0 - if len(timings) > 0 { - curTime = max(curTime, timings[0]) - } - for curTime <= maxDuration { - ability := mcdAction - ability.DesiredCastAt = curTime - ability.MinCastAt = curTime - ability.MaxCastAt = curTime + time.Second*30 - - actualCastAt := gs.Schedule(ability) - - curTime = actualCastAt + mcd.Spell.CD.Duration - i++ - if len(timings) > i { - curTime = max(curTime, timings[i]) - } - } -} - -func (gs *GCDScheduler) Reset(_ *core.Simulation, character *core.Character) { - gs.nextAbilityIndex = 0 - - for i, mcdID := range gs.managedMCDIDs { - gs.managedMCDs[i] = character.GetMajorCooldown(mcdID) - gs.managedMCDs[i].Disable() - } -} - -// Returns whether the cast was a success. -func (gs *GCDScheduler) DoNextAbility(sim *core.Simulation, character *core.Character) bool { - if gs.nextAbilityIndex >= len(gs.schedule) { - // It's possible for this function to get called near the end of the iteration, - // after the final scheduled ability. - return true - } - - expectedCastAt := gs.schedule[gs.nextAbilityIndex].castAt - if expectedCastAt > sim.CurrentTime { - character.SetGCDTimer(sim, expectedCastAt) - return true - } else if expectedCastAt < sim.CurrentTime { - panic(fmt.Sprintf("Missed scheduled cast! Expected %s but is now %s", expectedCastAt, sim.CurrentTime)) - } - - success := gs.schedule[gs.nextAbilityIndex].TryCast(sim) - gs.nextAbilityIndex++ - - if gs.nextAbilityIndex < len(gs.schedule) { - nextCastAt := gs.schedule[gs.nextAbilityIndex].castAt - if nextCastAt > character.NextGCDAt() { - character.SetGCDTimer(sim, nextCastAt) - } - } - - return success -} diff --git a/sim/common/wait.go b/sim/common/wait.go deleted file mode 100644 index afdf886baf..0000000000 --- a/sim/common/wait.go +++ /dev/null @@ -1,83 +0,0 @@ -package common - -import ( - "time" - - "github.com/wowsims/wotlk/sim/core" - "github.com/wowsims/wotlk/sim/core/proto" -) - -type WaitReason byte - -const ( - WaitReasonNone WaitReason = iota // unknown why we waited - WaitReasonOOM // no mana to cast - WaitReasonRotation // waiting on rotation - WaitReasonOptimal // waiting because its more optimal than casting. -) - -type WaitAction struct { - unit *core.Unit - - duration time.Duration - reason WaitReason -} - -func (action WaitAction) GetActionID() core.ActionID { - return core.ActionID{ - OtherID: proto.OtherAction_OtherActionWait, - } -} - -func (action WaitAction) GetName() string { - return "Wait" -} - -func (action WaitAction) GetTag() int32 { - return 0 -} - -func (action WaitAction) GetUnit() *core.Unit { - return action.unit -} - -func (action WaitAction) GetDuration() time.Duration { - return action.duration -} - -func (action WaitAction) GetManaCost() float64 { - return 0 -} - -func (action WaitAction) Cast(sim *core.Simulation) bool { - switch action.reason { - case WaitReasonNone: - if sim.Log != nil { - action.unit.Log(sim, "Idling for %s seconds, for no particular reason.", action.GetDuration()) - } - case WaitReasonOOM: - action.unit.Metrics.AddOOMTime(sim, action.GetDuration()) - if sim.Log != nil { - action.unit.Log(sim, "Not enough mana to cast, regenerating for %s.", action.GetDuration()) - } - case WaitReasonRotation: - if sim.Log != nil { - action.unit.Log(sim, "Waiting for %s due to rotation / CDs.", action.GetDuration()) - } - case WaitReasonOptimal: - if sim.Log != nil { - action.unit.Log(sim, "Waiting for %s because its more dps.", action.GetDuration()) - } - } - action.unit.SetGCDTimer(sim, sim.CurrentTime+action.GetDuration()) - - return true -} - -func NewWaitAction(sim *core.Simulation, unit *core.Unit, duration time.Duration, reason WaitReason) WaitAction { - return WaitAction{ - unit: unit, - duration: duration, - reason: reason, - } -} diff --git a/sim/core/apl.go b/sim/core/apl.go index 1fffcf8889..15cc3c8084 100644 --- a/sim/core/apl.go +++ b/sim/core/apl.go @@ -209,7 +209,6 @@ func (apl *APLRotation) DoNextAction(sim *Simulation) { i := 0 apl.inLoop = true - apl.unit.StartAPLLoop(sim) for nextAction := apl.getNextAction(sim); nextAction != nil; i, nextAction = i+1, apl.getNextAction(sim) { if i > 1000 { @@ -228,8 +227,6 @@ func (apl *APLRotation) DoNextAction(sim *Simulation) { if gcdReady { apl.unit.WaitUntil(sim, sim.CurrentTime+time.Millisecond*50) } - - apl.unit.DoneAPLLoop(sim, !gcdReady) } func (apl *APLRotation) getNextAction(sim *Simulation) *APLAction { diff --git a/sim/core/cast.go b/sim/core/cast.go index 8a2c34a3c9..08699ed8fb 100644 --- a/sim/core/cast.go +++ b/sim/core/cast.go @@ -93,7 +93,7 @@ func (spell *Spell) makeCastFunc(config CastConfig) CastSuccessFunc { } if spell.Cost != nil { - if !spell.Cost.MeetsRequirement(spell) { + if !spell.Cost.MeetsRequirement(sim, spell) { return spell.castFailureHelper(sim, spell.Cost.CostFailureReason(sim, spell)) } } diff --git a/sim/core/energy.go b/sim/core/energy.go index 34f44e8f32..0497696e49 100644 --- a/sim/core/energy.go +++ b/sim/core/energy.go @@ -135,7 +135,7 @@ func (eb *energyBar) onEnergyGain(sim *Simulation, crossedThreshold bool) { return } - if !sim.Options.Interactive && crossedThreshold && (!eb.unit.IsWaitingForEnergy() || eb.unit.DoneWaitingForEnergy(sim)) { + if !sim.Options.Interactive && crossedThreshold { eb.unit.Rotation.DoNextAction(sim) } } @@ -279,7 +279,7 @@ func newEnergyCost(spell *Spell, options EnergyCostOptions) *EnergyCost { } } -func (ec *EnergyCost) MeetsRequirement(spell *Spell) bool { +func (ec *EnergyCost) MeetsRequirement(_ *Simulation, spell *Spell) bool { spell.CurCast.Cost = spell.ApplyCostModifiers(spell.CurCast.Cost) return spell.Unit.CurrentEnergy() >= spell.CurCast.Cost } diff --git a/sim/core/focus.go b/sim/core/focus.go index 8e3c95783a..f70c4928de 100644 --- a/sim/core/focus.go +++ b/sim/core/focus.go @@ -140,7 +140,7 @@ func newFocusCost(spell *Spell, options FocusCostOptions) *FocusCost { } } -func (fc *FocusCost) MeetsRequirement(spell *Spell) bool { +func (fc *FocusCost) MeetsRequirement(_ *Simulation, spell *Spell) bool { spell.CurCast.Cost = max(0, spell.CurCast.Cost*spell.Unit.PseudoStats.CostMultiplier) return spell.Unit.CurrentFocus() >= spell.CurCast.Cost } diff --git a/sim/core/gcd.go b/sim/core/gcd.go index b6a08c02fe..9dba23f6a8 100644 --- a/sim/core/gcd.go +++ b/sim/core/gcd.go @@ -1,7 +1,6 @@ package core import ( - "fmt" "time" ) @@ -65,101 +64,12 @@ func (unit *Unit) CancelGCDTimer(sim *Simulation) { unit.gcdAction.Cancel(sim) } -func (unit *Unit) IsWaiting() bool { - return unit.waitStartTime != 0 -} -func (unit *Unit) IsWaitingForMana() bool { - return unit.waitingForMana != 0 -} - -func (unit *Unit) IsWaitingForEnergy() bool { - return unit.waitingForEnergy != 0 -} - -// Assumes that IsWaitingForMana() == true -func (unit *Unit) DoneWaitingForMana(sim *Simulation) bool { - if unit.CurrentMana() >= unit.waitingForMana { - unit.Metrics.AddOOMTime(sim, sim.CurrentTime-unit.waitStartTime) - unit.waitStartTime = 0 - unit.waitingForMana = 0 - return true - } - return false -} - -func (unit *Unit) DoneWaitingForEnergy(_ *Simulation) bool { - if unit.CurrentEnergy() >= unit.waitingForEnergy { - unit.waitStartTime = 0 - unit.waitingForEnergy = 0 - return true - } - return false -} - -// Returns true if the unit was waiting for mana but is now finished AND -// the GCD is also ready. -func (unit *Unit) FinishedWaitingForManaAndGCDReady(sim *Simulation) bool { - if !unit.IsWaitingForMana() || !unit.DoneWaitingForMana(sim) { - return false - } - - return unit.GCD.IsReady(sim) -} - func (unit *Unit) WaitUntil(sim *Simulation, readyTime time.Duration) { if readyTime < sim.CurrentTime { panic(unit.Label + ": cannot wait negative time") } - if !unit.IsWaiting() { - unit.waitStartTime = sim.CurrentTime - } unit.SetGCDTimer(sim, readyTime) if sim.Log != nil && readyTime > sim.CurrentTime { unit.Log(sim, "Pausing GCD for %s due to rotation / CDs.", readyTime-sim.CurrentTime) } } - -func (unit *Unit) HardcastWaitUntil(sim *Simulation, readyTime time.Duration, onComplete CastFunc) { - if unit.Hardcast.Expires > sim.CurrentTime { - fmt.Printf("Sim current time: %0.2f\n", sim.CurrentTime.Seconds()) - panic(fmt.Sprintf("Hardcast already in use, will finish at: %0.2f", unit.Hardcast.Expires.Seconds())) - } - - unit.Hardcast.Expires = readyTime - unit.Hardcast.OnComplete = onComplete - unit.newHardcastAction(sim) -} - -func (unit *Unit) WaitForMana(sim *Simulation, desiredMana float64) { - if !unit.IsWaitingForMana() { - unit.waitStartTime = sim.CurrentTime - } - unit.waitingForMana = desiredMana - unit.Metrics.MarkOOM(sim) - if sim.Log != nil { - unit.Log(sim, "Not enough mana to cast, pausing GCD until mana >= %0.01f.", desiredMana) - } -} - -func (unit *Unit) WaitForEnergy(sim *Simulation, desiredEnergy float64) { - if !unit.IsWaitingForEnergy() { - unit.waitStartTime = sim.CurrentTime - } - unit.waitingForEnergy = desiredEnergy - if sim.Log != nil { - unit.Log(sim, "Not enough energy to cast, pausing GCD until energy >= %0.01f.", desiredEnergy) - } -} - -func (unit *Unit) doneIterationGCD(sim *Simulation) { - if unit.IsWaitingForMana() { - unit.Metrics.AddOOMTime(sim, sim.CurrentTime-unit.waitStartTime) - unit.waitStartTime = 0 - unit.waitingForMana = 0 - } else if unit.IsWaitingForEnergy() { - unit.waitStartTime = 0 - unit.waitingForEnergy = 0 - } else if unit.IsWaiting() { - unit.waitStartTime = 0 - } -} diff --git a/sim/core/mana.go b/sim/core/mana.go index 59f245afb5..11a9130088 100644 --- a/sim/core/mana.go +++ b/sim/core/mana.go @@ -23,6 +23,10 @@ type manaBar struct { JowiseManaMetrics *ResourceMetrics ReplenishmentAura *Aura + + // For keeping track of OOM status. + waitingForMana float64 + waitingForManaStartTime time.Duration } // EnableManaBar will setup caster stat dependencies (int->mana and int->spellcrit) @@ -104,11 +108,15 @@ func (unit *Unit) SpendMana(sim *Simulation, amount float64, metrics *ResourceMe unit.Metrics.ManaSpent += amount } -func (mb *manaBar) doneIteration() { +func (mb *manaBar) doneIteration(sim *Simulation) { if mb.unit == nil { return } + if mb.waitingForMana != 0 { + mb.unit.Metrics.AddOOMTime(sim, sim.CurrentTime-mb.waitingForManaStartTime) + } + manaGainSpell := mb.unit.GetSpell(ActionID{OtherID: proto.OtherAction_OtherActionManaGain}) for _, resourceMetrics := range mb.unit.Metrics.resources { @@ -258,6 +266,23 @@ func (mb *manaBar) reset() { } mb.currentMana = mb.unit.MaxMana() + mb.waitingForMana = 0 + mb.waitingForManaStartTime = 0 +} + +func (mb *manaBar) IsOOM() bool { + return mb.waitingForMana != 0 +} +func (mb *manaBar) StartOOMEvent(sim *Simulation, requiredMana float64) { + mb.waitingForManaStartTime = sim.CurrentTime + mb.waitingForMana = requiredMana + mb.unit.Metrics.MarkOOM(sim) +} +func (mb *manaBar) EndOOMEvent(sim *Simulation) { + eventDuration := sim.CurrentTime - mb.waitingForManaStartTime + mb.unit.Metrics.AddOOMTime(sim, eventDuration) + mb.waitingForManaStartTime = 0 + mb.waitingForMana = 0 } type ManaCostOptions struct { @@ -284,9 +309,26 @@ func newManaCost(spell *Spell, options ManaCostOptions) *ManaCost { } } -func (mc *ManaCost) MeetsRequirement(spell *Spell) bool { +func (mc *ManaCost) MeetsRequirement(sim *Simulation, spell *Spell) bool { spell.CurCast.Cost = spell.ApplyCostModifiers(spell.CurCast.Cost) - return spell.Unit.CurrentMana() >= spell.CurCast.Cost + meetsRequirement := spell.Unit.CurrentMana() >= spell.CurCast.Cost + + if spell.CurCast.Cost > 0 { + if meetsRequirement { + if spell.Unit.IsOOM() { + spell.Unit.EndOOMEvent(sim) + } + } else { + if spell.Unit.IsOOM() { + // Continuation of OOM event. + spell.Unit.waitingForMana = min(spell.Unit.waitingForMana, spell.CurCast.Cost) + } else { + spell.Unit.StartOOMEvent(sim, spell.CurCast.Cost) + } + } + } + + return meetsRequirement } func (mc *ManaCost) CostFailureReason(sim *Simulation, spell *Spell) string { return fmt.Sprintf("not enough mana (Current Mana = %0.03f, Mana Cost = %0.03f)", spell.Unit.CurrentMana(), spell.CurCast.Cost) diff --git a/sim/core/metrics_aggregator.go b/sim/core/metrics_aggregator.go index ba6ca1e830..8dab4abeaf 100644 --- a/sim/core/metrics_aggregator.go +++ b/sim/core/metrics_aggregator.go @@ -348,8 +348,10 @@ func (unitMetrics *UnitMetrics) AddFinalPetMetrics(petMetrics *UnitMetrics) { } func (unitMetrics *UnitMetrics) AddOOMTime(sim *Simulation, dur time.Duration) { - unitMetrics.CharacterIterationMetrics.OOMTime += dur - unitMetrics.MarkOOM(sim) + if dur > 0 { + unitMetrics.CharacterIterationMetrics.OOMTime += dur + unitMetrics.MarkOOM(sim) + } } func (unitMetrics *UnitMetrics) MarkOOM(sim *Simulation) { if !unitMetrics.WentOOM { diff --git a/sim/core/rage.go b/sim/core/rage.go index 8bcb23f11e..7efaf4d42d 100644 --- a/sim/core/rage.go +++ b/sim/core/rage.go @@ -210,7 +210,7 @@ func newRageCost(spell *Spell, options RageCostOptions) *RageCost { } } -func (rc *RageCost) MeetsRequirement(spell *Spell) bool { +func (rc *RageCost) MeetsRequirement(_ *Simulation, spell *Spell) bool { spell.CurCast.Cost = spell.ApplyCostModifiers(spell.CurCast.Cost) return spell.Unit.CurrentRage() >= spell.CurCast.Cost } diff --git a/sim/core/runic_power.go b/sim/core/runic_power.go index f4f2864f3a..1dc5516b14 100644 --- a/sim/core/runic_power.go +++ b/sim/core/runic_power.go @@ -831,7 +831,7 @@ func newRuneCost(spell *Spell, options RuneCostOptions) *RuneCostImpl { } } -func (rc *RuneCostImpl) MeetsRequirement(spell *Spell) bool { +func (rc *RuneCostImpl) MeetsRequirement(_ *Simulation, spell *Spell) bool { spell.CurCast.Cost *= spell.CostMultiplier // TODO this looks fishy - multiplying and rune costs don't go well together cost := RuneCost(spell.CurCast.Cost) diff --git a/sim/core/spell.go b/sim/core/spell.go index d8848b13a9..6314a2b110 100644 --- a/sim/core/spell.go +++ b/sim/core/spell.go @@ -498,18 +498,10 @@ func (spell *Spell) CanCast(sim *Simulation, target *Unit) bool { if spell.Cost != nil { // temp hack spell.CurCast.Cost = spell.DefaultCast.Cost - if !spell.Cost.MeetsRequirement(spell) { + if !spell.Cost.MeetsRequirement(sim, spell) { //if sim.Log != nil { // sim.Log("Cant cast because of resource cost") //} - _, isManaCost := spell.Cost.(*ManaCost) - if isManaCost && spell.CurCast.Cost > 0 { - if spell.Unit.ManaRequired > 0 { - spell.Unit.ManaRequired = min(spell.Unit.ManaRequired, spell.CurCast.Cost) - } else { - spell.Unit.ManaRequired = spell.CurCast.Cost - } - } return false } } @@ -600,7 +592,7 @@ func (spell *Spell) TravelTime() time.Duration { type SpellCost interface { // Whether the Unit associated with the spell meets the resource cost // requirements to cast the spell. - MeetsRequirement(*Spell) bool + MeetsRequirement(*Simulation, *Spell) bool // Returns a message for when the cast fails due to lack of resources. CostFailureReason(*Simulation, *Spell) string diff --git a/sim/core/unit.go b/sim/core/unit.go index 250a666f29..fbca1a10a0 100644 --- a/sim/core/unit.go +++ b/sim/core/unit.go @@ -144,11 +144,6 @@ type Unit struct { gcdAction *PendingAction hardcastAction *PendingAction - // Fields related to waiting for certain events to happen. - waitingForEnergy float64 - waitingForMana float64 - waitStartTime time.Duration - // Cached mana return values per tick. manaTickWhileCasting float64 manaTickWhileNotCasting float64 @@ -160,8 +155,6 @@ type Unit struct { // The currently-channeled DOT spell, otherwise nil. ChanneledDot *Dot - - ManaRequired float64 } // Units can be disabled for several reasons: @@ -482,12 +475,10 @@ func (unit *Unit) startPull(sim *Simulation) { } } -// Advance moves time forward counting down auras, and nothing else, currently. func (unit *Unit) doneIteration(sim *Simulation) { unit.Hardcast = Hardcast{} - unit.doneIterationGCD(sim) - unit.manaBar.doneIteration() + unit.manaBar.doneIteration(sim) unit.rageBar.doneIteration() unit.auraTracker.doneIteration(sim) @@ -545,19 +536,6 @@ func (unit *Unit) GetMetadata() *proto.UnitMetadata { return metadata } -func (unit *Unit) StartAPLLoop(_ *Simulation) { - if unit.HasManaBar() { - unit.ManaRequired = 0 - } -} - -func (unit *Unit) DoneAPLLoop(sim *Simulation, usedGCD bool) { - if unit.HasManaBar() && !usedGCD && unit.ManaRequired > 0 { - unit.WaitForMana(sim, unit.ManaRequired) - unit.ManaRequired = 0 - } -} - func (unit *Unit) ExecuteCustomRotation(sim *Simulation) { panic("Unimplemented ExecuteCustomRotation") }