From be1c1d5ab48e7dbd5b0a49128a87118c4b70915a Mon Sep 17 00:00:00 2001 From: Saji Date: Sat, 20 Dec 2025 00:16:23 -0500 Subject: [PATCH 1/4] Fix Alter Time restoration for stacking aura periodic actions - Add OnRestore callback to Aura for proper state restoration - Refactor Activate into public method and internal activate(sim, triggerOnGain) - Update RestoreState to use OnRestore when available - Update NewTemporaryStatBuffWithStacks with OnExpire and OnRestore callbacks Fixes issues with Wushoolay's Final Choice trinket and similar stacking proc trinkets when used with Alter Time. --- sim/core/aura.go | 26 +++++++++++++++++--- sim/core/aura_helpers.go | 52 ++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/sim/core/aura.go b/sim/core/aura.go index 39b79f5ce6..67479a9885 100644 --- a/sim/core/aura.go +++ b/sim/core/aura.go @@ -18,6 +18,7 @@ type OnReset func(aura *Aura, sim *Simulation) type OnDoneIteration func(aura *Aura, sim *Simulation) type OnGain func(aura *Aura, sim *Simulation) type OnExpire func(aura *Aura, sim *Simulation) +type OnRestore func(aura *Aura, sim *Simulation, stacks int32) type OnStacksChange func(aura *Aura, sim *Simulation, oldStacks int32, newStacks int32) type OnEncounterStart func(aura *Aura, sim *Simulation) @@ -87,6 +88,7 @@ type Aura struct { OnDoneIteration OnDoneIteration OnGain OnGain OnExpire OnExpire + OnRestore OnRestore OnStacksChange OnStacksChange // Invoked when the number of stacks of this aura changes. OnApplyEffects OnApplyEffects // Invoked when a spell cast is completing, before apply effects are called @@ -647,6 +649,10 @@ restart: // Adds a new aura to the simulation. If an aura with the same ID already // exists it will be replaced with the new one. func (aura *Aura) Activate(sim *Simulation) { + aura.activate(sim, true) +} + +func (aura *Aura) activate(sim *Simulation, triggerOnGain bool) { if aura == nil { return } @@ -748,7 +754,7 @@ func (aura *Aura) Activate(sim *Simulation) { } // don't invoke possible callbacks until the internal state is consistent - if aura.OnGain != nil { + if triggerOnGain && aura.OnGain != nil { aura.OnGain(aura, sim) } } @@ -1148,8 +1154,22 @@ func (aura *Aura) SaveState(sim *Simulation) AuraState { } func (aura *Aura) RestoreState(state AuraState, sim *Simulation) { - if !aura.active { - aura.Activate(sim) + // If the aura has an OnRestore callback, we need special handling to properly + // restart any periodic actions without causing issues like double-stacking. + if aura.OnRestore != nil { + // Deactivate first to cancel any existing periodic actions + if aura.active { + aura.Deactivate(sim) + } + // Activate without triggering OnGain's immediate effects + aura.activate(sim, false) + // Then call the restore callback to restart periodic actions properly + // Pass the stacks so it can calculate remaining ticks + aura.OnRestore(aura, sim, state.Stacks) + } else { + if !aura.active { + aura.Activate(sim) + } } aura.UpdateExpires(state.RemainingDuration + sim.CurrentTime) diff --git a/sim/core/aura_helpers.go b/sim/core/aura_helpers.go index 3710758c0f..9639acf211 100644 --- a/sim/core/aura_helpers.go +++ b/sim/core/aura_helpers.go @@ -341,6 +341,25 @@ func (character *Character) NewTemporaryStatBuffWithStacks(config TemporaryStatB }) if config.TimePerStack > 0 { + var pa *PendingAction + i + startStackingAction := func(sim *Simulation, tickImmediately bool, numTicks int) { + pa = StartPeriodicAction(sim, PeriodicActionOptions{ + Period: config.TimePerStack, + NumTicks: numTicks, + TickImmediately: tickImmediately, + OnAction: func(sim *Simulation) { + if stackingAura.IsActive() { + if config.DecrementStacks { + stackingAura.RemoveStack(sim) + } else { + stackingAura.AddStack(sim) + } + } + }, + }) + } + aura := character.RegisterAura(Aura{ Label: config.AuraLabel, ActionID: config.ActionID, @@ -352,21 +371,24 @@ func (character *Character) NewTemporaryStatBuffWithStacks(config TemporaryStatB stackingAura.SetStacks(sim, config.MaxStacks) } - StartPeriodicAction(sim, PeriodicActionOptions{ - Period: config.TimePerStack, - NumTicks: int(config.MaxStacks), - TickImmediately: config.TickImmediately, - OnAction: func(sim *Simulation) { - // Aura might not be active because of stuff like mage alter time being cast right before this aura being activated - if stackingAura.IsActive() { - if config.DecrementStacks { - stackingAura.RemoveStack(sim) - } else { - stackingAura.AddStack(sim) - } - } - }, - }) + startStackingAction(sim, config.TickImmediately, int(config.MaxStacks)) + }, + OnExpire: func(aura *Aura, sim *Simulation) { + if pa != nil { + pa.Cancel(sim) + pa = nil + } + }, + OnRestore: func(aura *Aura, sim *Simulation, stacks int32) { + // When restoring (e.g., via Alter Time), we need to restart the periodic action + // but without TickImmediately to avoid adding an extra stack. + // Note: We don't activate the stacking aura here because it will be restored + // separately by Alter Time's restoration loop with the correct duration. + + remainingTicks := int(config.MaxStacks - stacks) + if remainingTicks > 0 { + startStackingAction(sim, false, remainingTicks) + } }, }) return stackingAura, aura From c5ae40e4fb2b32bcffda90af54903d4647520d8d Mon Sep 17 00:00:00 2001 From: Saji Date: Sat, 20 Dec 2025 00:19:54 -0500 Subject: [PATCH 2/4] fix --- sim/core/aura_helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sim/core/aura_helpers.go b/sim/core/aura_helpers.go index 9639acf211..8226ceeb1c 100644 --- a/sim/core/aura_helpers.go +++ b/sim/core/aura_helpers.go @@ -342,7 +342,7 @@ func (character *Character) NewTemporaryStatBuffWithStacks(config TemporaryStatB if config.TimePerStack > 0 { var pa *PendingAction - i + startStackingAction := func(sim *Simulation, tickImmediately bool, numTicks int) { pa = StartPeriodicAction(sim, PeriodicActionOptions{ Period: config.TimePerStack, From 6a779c89ddabe261e9b074601f5aa732dba7be4b Mon Sep 17 00:00:00 2001 From: Saji Date: Sat, 20 Dec 2025 00:50:09 -0500 Subject: [PATCH 3/4] update tests --- sim/mage/arcane/TestArcane.results | 8 ++++---- sim/mage/fire/TestFire.results | 8 ++++---- sim/mage/frost/TestFrost.results | 8 ++++---- sim/monk/brewmaster/TestBrewmaster.results | 6 +++--- sim/monk/windwalker/TestWindwalker.results | 6 +++--- sim/paladin/retribution/TestRetribution.results | 4 ++-- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sim/mage/arcane/TestArcane.results b/sim/mage/arcane/TestArcane.results index ed4b54e452..119ef3f199 100644 --- a/sim/mage/arcane/TestArcane.results +++ b/sim/mage/arcane/TestArcane.results @@ -54,8 +54,8 @@ dps_results: { dps_results: { key: "TestArcane-AllItems-BlackBloodofY'Shaarj-105648" value: { - dps: 250708.14874 - tps: 241227.73721 + dps: 247368.66106 + tps: 238202.20435 } } dps_results: { @@ -495,8 +495,8 @@ dps_results: { dps_results: { key: "TestArcane-AllItems-Wushoolay'sFinalChoice-96785" value: { - dps: 229530.16406 - tps: 221189.14387 + dps: 227481.12544 + tps: 219192.8406 } } dps_results: { diff --git a/sim/mage/fire/TestFire.results b/sim/mage/fire/TestFire.results index 13fd0406a6..fa58804da1 100644 --- a/sim/mage/fire/TestFire.results +++ b/sim/mage/fire/TestFire.results @@ -89,8 +89,8 @@ dps_results: { dps_results: { key: "TestFire-AllItems-BlackBloodofY'Shaarj-105648" value: { - dps: 168331.6727 - tps: 163805.79586 + dps: 167197.44883 + tps: 162730.59082 } } dps_results: { @@ -2392,8 +2392,8 @@ dps_results: { dps_results: { key: "TestFire-AllItems-Wushoolay'sFinalChoice-96785" value: { - dps: 151200.99334 - tps: 147159.65728 + dps: 149762.49072 + tps: 145741.62368 } } dps_results: { diff --git a/sim/mage/frost/TestFrost.results b/sim/mage/frost/TestFrost.results index 217373ed1b..3445bdba95 100644 --- a/sim/mage/frost/TestFrost.results +++ b/sim/mage/frost/TestFrost.results @@ -54,8 +54,8 @@ dps_results: { dps_results: { key: "TestFrost-AllItems-BlackBloodofY'Shaarj-105648" value: { - dps: 175922.50483 - tps: 128279.16303 + dps: 176451.38176 + tps: 128653.32481 } } dps_results: { @@ -495,8 +495,8 @@ dps_results: { dps_results: { key: "TestFrost-AllItems-Wushoolay'sFinalChoice-96785" value: { - dps: 162929.76163 - tps: 119026.63359 + dps: 162234.99754 + tps: 118586.24646 } } dps_results: { diff --git a/sim/monk/brewmaster/TestBrewmaster.results b/sim/monk/brewmaster/TestBrewmaster.results index 2a9ca6994a..341225b2d4 100644 --- a/sim/monk/brewmaster/TestBrewmaster.results +++ b/sim/monk/brewmaster/TestBrewmaster.results @@ -2715,10 +2715,10 @@ dps_results: { dps_results: { key: "TestBrewmaster-AllItems-TickingEbonDetonator-105612" value: { - dps: 280443.12765 - tps: 1.2080022053e+06 + dps: 280460.43133 + tps: 1.20812333106e+06 dtps: 13651.61794 - hps: 31015.91987 + hps: 31018.86531 } } dps_results: { diff --git a/sim/monk/windwalker/TestWindwalker.results b/sim/monk/windwalker/TestWindwalker.results index f24eaf3e91..17e23f9c43 100644 --- a/sim/monk/windwalker/TestWindwalker.results +++ b/sim/monk/windwalker/TestWindwalker.results @@ -2433,9 +2433,9 @@ dps_results: { dps_results: { key: "TestWindwalker-AllItems-TickingEbonDetonator-105612" value: { - dps: 271886.08025 - tps: 259546.47389 - hps: 8823.0909 + dps: 271907.0088 + tps: 259566.55912 + hps: 8825.87634 } } dps_results: { diff --git a/sim/paladin/retribution/TestRetribution.results b/sim/paladin/retribution/TestRetribution.results index fd8999cfc6..4a65c092ec 100644 --- a/sim/paladin/retribution/TestRetribution.results +++ b/sim/paladin/retribution/TestRetribution.results @@ -2393,8 +2393,8 @@ dps_results: { dps_results: { key: "TestRetribution-AllItems-TickingEbonDetonator-105612" value: { - dps: 240898.69088 - tps: 229849.35766 + dps: 240900.50608 + tps: 229851.17286 hps: 22.01849 } } From 6f25113a08afd68589d551fe7e6c23ce8fa391f8 Mon Sep 17 00:00:00 2001 From: Saji Date: Mon, 22 Dec 2025 09:47:25 -0500 Subject: [PATCH 4/4] update to pass full aura state --- sim/core/aura.go | 5 ++--- sim/core/aura_helpers.go | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sim/core/aura.go b/sim/core/aura.go index 67479a9885..67ddeaf056 100644 --- a/sim/core/aura.go +++ b/sim/core/aura.go @@ -18,7 +18,7 @@ type OnReset func(aura *Aura, sim *Simulation) type OnDoneIteration func(aura *Aura, sim *Simulation) type OnGain func(aura *Aura, sim *Simulation) type OnExpire func(aura *Aura, sim *Simulation) -type OnRestore func(aura *Aura, sim *Simulation, stacks int32) +type OnRestore func(aura *Aura, sim *Simulation, state AuraState) type OnStacksChange func(aura *Aura, sim *Simulation, oldStacks int32, newStacks int32) type OnEncounterStart func(aura *Aura, sim *Simulation) @@ -1164,8 +1164,7 @@ func (aura *Aura) RestoreState(state AuraState, sim *Simulation) { // Activate without triggering OnGain's immediate effects aura.activate(sim, false) // Then call the restore callback to restart periodic actions properly - // Pass the stacks so it can calculate remaining ticks - aura.OnRestore(aura, sim, state.Stacks) + aura.OnRestore(aura, sim, state) } else { if !aura.active { aura.Activate(sim) diff --git a/sim/core/aura_helpers.go b/sim/core/aura_helpers.go index 8226ceeb1c..b2aa7ab52b 100644 --- a/sim/core/aura_helpers.go +++ b/sim/core/aura_helpers.go @@ -379,13 +379,13 @@ func (character *Character) NewTemporaryStatBuffWithStacks(config TemporaryStatB pa = nil } }, - OnRestore: func(aura *Aura, sim *Simulation, stacks int32) { + OnRestore: func(aura *Aura, sim *Simulation, state AuraState) { // When restoring (e.g., via Alter Time), we need to restart the periodic action // but without TickImmediately to avoid adding an extra stack. // Note: We don't activate the stacking aura here because it will be restored // separately by Alter Time's restoration loop with the correct duration. - remainingTicks := int(config.MaxStacks - stacks) + remainingTicks := int(config.MaxStacks - state.Stacks) if remainingTicks > 0 { startStackingAction(sim, false, remainingTicks) }