Skip to content

Commit

Permalink
Merge pull request #3842 from wowsims/apl
Browse files Browse the repository at this point in the history
Prevent APL prepull crashes, and add warnings instead
  • Loading branch information
jimmyt857 authored Oct 8, 2023
2 parents 949bd38 + 3bcd0c9 commit 5916a25
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 68 deletions.
3 changes: 2 additions & 1 deletion sim/core/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ func ComputeStats(csr *proto.ComputeStatsRequest) *proto.ComputeStatsResult {
if encounter == nil {
encounter = &proto.Encounter{}
}
_, raidStats, encounterStats := NewEnvironment(csr.Raid, encounter)

_, raidStats, encounterStats := NewEnvironment(csr.Raid, encounter, true)

return &proto.ComputeStatsResult{
RaidStats: raidStats,
Expand Down
118 changes: 66 additions & 52 deletions sim/core/apl.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,66 +41,80 @@ func (rot *APLRotation) ValidationWarning(message string, vals ...interface{}) {
rot.curWarnings = append(rot.curWarnings, warning)
}

// Invokes the fn function, and attributes all warnings generated during its invocation
// to the provided warningsList.
func (rot *APLRotation) doAndRecordWarnings(warningsList *[]string, isPrepull bool, fn func()) {
rot.parsingPrepull = isPrepull
fn()
if warningsList != nil {
*warningsList = append(*warningsList, rot.curWarnings...)
}
rot.curWarnings = nil
rot.parsingPrepull = false
}

func (unit *Unit) newAPLRotation(config *proto.APLRotation) *APLRotation {
if config == nil || config.Type != proto.APLRotation_TypeAPL {
return nil
}

rotation := &APLRotation{
unit: unit,
unit: unit,
prepullWarnings: make([][]string, len(config.PrepullActions)),
priorityListWarnings: make([][]string, len(config.PriorityList)),
}

// Parse prepull actions
rotation.parsingPrepull = true
for _, prepullItem := range config.PrepullActions {
if !prepullItem.Hide {
doAtVal := rotation.newAPLValue(prepullItem.DoAtValue)
if doAtVal != nil {
doAt := doAtVal.GetDuration(nil)
if doAt > 0 {
rotation.ValidationWarning("Invalid time for 'Do At', ignoring this Prepull Action")
} else {
action := rotation.newAPLAction(prepullItem.Action)
if action != nil {
rotation.prepullActions = append(rotation.prepullActions, action)
unit.RegisterPrepullAction(doAt, func(sim *Simulation) {
action.Execute(sim)
})
for prepullIdx, prepullItem := range config.PrepullActions {
rotation.doAndRecordWarnings(&rotation.prepullWarnings[prepullIdx], true, func() {
if !prepullItem.Hide {
doAtVal := rotation.newAPLValue(prepullItem.DoAtValue)
if doAtVal != nil {
doAt := doAtVal.GetDuration(nil)
if doAt > 0 {
rotation.ValidationWarning("Invalid time for 'Do At', ignoring this Prepull Action")
} else {
action := rotation.newAPLAction(prepullItem.Action)
if action != nil {
rotation.prepullActions = append(rotation.prepullActions, action)
unit.RegisterPrepullAction(doAt, func(sim *Simulation) {
// Warnings for prepull cast failure are detected by running a fake prepull,
// so this action.Execute needs to record warnings.
rotation.doAndRecordWarnings(&rotation.prepullWarnings[prepullIdx], true, func() {
action.Execute(sim)
})
})
}
}
}
}
}

rotation.prepullWarnings = append(rotation.prepullWarnings, rotation.curWarnings)
rotation.curWarnings = nil
})
}
rotation.parsingPrepull = false

// Parse priority list
var configIdxs []int
for i, aplItem := range config.PriorityList {
if !aplItem.Hide {
action := rotation.newAPLAction(aplItem.Action)
if action != nil {
rotation.priorityList = append(rotation.priorityList, action)
configIdxs = append(configIdxs, i)
rotation.doAndRecordWarnings(&rotation.priorityListWarnings[i], false, func() {
if !aplItem.Hide {
action := rotation.newAPLAction(aplItem.Action)
if action != nil {
rotation.priorityList = append(rotation.priorityList, action)
configIdxs = append(configIdxs, i)
}
}
}

rotation.priorityListWarnings = append(rotation.priorityListWarnings, rotation.curWarnings)
rotation.curWarnings = nil
})
}

// Finalize
for _, action := range rotation.prepullActions {
action.Finalize(rotation)
rotation.curWarnings = nil
for i, action := range rotation.prepullActions {
rotation.doAndRecordWarnings(&rotation.prepullWarnings[i], true, func() {
action.Finalize(rotation)
})
}
for i, action := range rotation.priorityList {
action.Finalize(rotation)

rotation.priorityListWarnings[configIdxs[i]] = append(rotation.priorityListWarnings[configIdxs[i]], rotation.curWarnings...)
rotation.curWarnings = nil
rotation.doAndRecordWarnings(&rotation.priorityListWarnings[i], false, func() {
action.Finalize(rotation)
})
}

// Remove MCDs that are referenced by APL actions, so that the Autocast Other Cooldowns
Expand All @@ -113,23 +127,23 @@ func (unit *Unit) newAPLRotation(config *proto.APLRotation) *APLRotation {
}

// If user has a Prepull potion set but does not use it in their APL settings, we enable it here.
rotation.parsingPrepull = true
prepotSpell := rotation.GetAPLSpell(ActionID{OtherID: proto.OtherAction_OtherActionPotion}.ToProto())
rotation.parsingPrepull = false
if prepotSpell != nil {
found := false
for _, prepullAction := range rotation.allPrepullActions() {
if castSpellAction, ok := prepullAction.impl.(*APLActionCastSpell); ok &&
(castSpellAction.spell == prepotSpell || castSpellAction.spell.Flags.Matches(SpellFlagPotion)) {
found = true
rotation.doAndRecordWarnings(nil, true, func() {
prepotSpell := rotation.GetAPLSpell(ActionID{OtherID: proto.OtherAction_OtherActionPotion}.ToProto())
if prepotSpell != nil {
found := false
for _, prepullAction := range rotation.allPrepullActions() {
if castSpellAction, ok := prepullAction.impl.(*APLActionCastSpell); ok &&
(castSpellAction.spell == prepotSpell || castSpellAction.spell.Flags.Matches(SpellFlagPotion)) {
found = true
}
}
if !found {
unit.RegisterPrepullAction(-1*time.Second, func(sim *Simulation) {
prepotSpell.Cast(sim, nil)
})
}
}
if !found {
unit.RegisterPrepullAction(-1*time.Second, func(sim *Simulation) {
prepotSpell.Cast(sim, nil)
})
}
}
})

return rotation
}
Expand Down
21 changes: 15 additions & 6 deletions sim/core/cast.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ func (cast *Cast) EffectiveTime() time.Duration {
type CastFunc func(*Simulation, *Unit)
type CastSuccessFunc func(*Simulation, *Unit) bool

func (spell *Spell) castFailureHelper(sim *Simulation, message string, vals ...interface{}) bool {
reason := fmt.Sprintf(spell.ActionID.String()+" failed to cast: "+message, vals...)
if sim.CurrentTime < 0 && spell.Unit.IsUsingAPL {
spell.Unit.Rotation.ValidationWarning(reason)
return false
}
panic(reason)
}

func (spell *Spell) makeCastFunc(config CastConfig) CastSuccessFunc {
return func(sim *Simulation, target *Unit) bool {
spell.CurCast = spell.DefaultCast
Expand Down Expand Up @@ -110,26 +119,26 @@ func (spell *Spell) makeCastFunc(config CastConfig) CastSuccessFunc {
if config.CD.Timer != nil {
// By panicking if spell is on CD, we force each sim to properly check for their own CDs.
if !spell.CD.IsReady(sim) {
panic(fmt.Sprintf("Trying to cast %s but is still on cooldown for %s, curTime = %s", spell.ActionID, spell.CD.TimeToReady(sim), sim.CurrentTime))
return spell.castFailureHelper(sim, "still on cooldown for %s, curTime = %s", spell.CD.TimeToReady(sim), sim.CurrentTime)
}
spell.CD.Set(sim.CurrentTime + spell.CurCast.CastTime + spell.CD.Duration)
}

if config.SharedCD.Timer != nil {
// By panicking if spell is on CD, we force each sim to properly check for their own CDs.
if !spell.SharedCD.IsReady(sim) {
panic(fmt.Sprintf("Trying to cast %s but is still on shared cooldown for %s, curTime = %s", spell.ActionID, spell.SharedCD.TimeToReady(sim), sim.CurrentTime))
return spell.castFailureHelper(sim, "still on shared cooldown for %s, curTime = %s", spell.SharedCD.TimeToReady(sim), sim.CurrentTime)
}
spell.SharedCD.Set(sim.CurrentTime + spell.CurCast.CastTime + spell.SharedCD.Duration)
}

// By panicking if spell is on CD, we force each sim to properly check for their own CDs.
if spell.CurCast.GCD != 0 && !spell.Unit.GCD.IsReady(sim) {
panic(fmt.Sprintf("Trying to cast %s but GCD on cooldown for %s, curTime = %s", spell.ActionID, spell.Unit.GCD.TimeToReady(sim), sim.CurrentTime))
return spell.castFailureHelper(sim, "GCD on cooldown for %s, curTime = %s", spell.Unit.GCD.TimeToReady(sim), sim.CurrentTime)
}

if hc := spell.Unit.Hardcast; hc.Expires > sim.CurrentTime {
panic(fmt.Sprintf("Trying to cast %s but casting/channeling %v for %s, curTime = %s", spell.ActionID, hc.ActionID, hc.Expires-sim.CurrentTime, sim.CurrentTime))
return spell.castFailureHelper(sim, "casting/channeling %v for %s, curTime = %s", hc.ActionID, hc.Expires-sim.CurrentTime, sim.CurrentTime)
}

if effectiveTime := spell.CurCast.EffectiveTime(); effectiveTime != 0 {
Expand Down Expand Up @@ -211,7 +220,7 @@ func (spell *Spell) makeCastFuncSimple() CastSuccessFunc {
if spell.CD.Timer != nil {
// By panicking if spell is on CD, we force each sim to properly check for their own CDs.
if !spell.CD.IsReady(sim) {
panic(fmt.Sprintf("Trying to cast %s but is still on cooldown for %s, curTime = %s", spell.ActionID, spell.CD.TimeToReady(sim), sim.CurrentTime))
return spell.castFailureHelper(sim, "still on cooldown for %s, curTime = %s", spell.CD.TimeToReady(sim), sim.CurrentTime)
}

spell.CD.Set(sim.CurrentTime + spell.CD.Duration)
Expand All @@ -220,7 +229,7 @@ func (spell *Spell) makeCastFuncSimple() CastSuccessFunc {
if spell.SharedCD.Timer != nil {
// By panicking if spell is on CD, we force each sim to properly check for their own CDs.
if !spell.SharedCD.IsReady(sim) {
panic(fmt.Sprintf("Trying to cast %s but is still on shared cooldown for %s, curTime = %s", spell.ActionID, spell.SharedCD.TimeToReady(sim), sim.CurrentTime))
return spell.castFailureHelper(sim, "still on shared cooldown for %s, curTime = %s", spell.SharedCD.TimeToReady(sim), sim.CurrentTime)
}

spell.SharedCD.Set(sim.CurrentTime + spell.SharedCD.Duration)
Expand Down
22 changes: 17 additions & 5 deletions sim/core/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ type Environment struct {
prepullActions []PrepullAction
}

func NewEnvironment(raidProto *proto.Raid, encounterProto *proto.Encounter) (*Environment, *proto.RaidStats, *proto.EncounterStats) {
func NewEnvironment(raidProto *proto.Raid, encounterProto *proto.Encounter, runFakePrepull bool) (*Environment, *proto.RaidStats, *proto.EncounterStats) {
env := &Environment{
State: Created,
}

env.construct(raidProto, encounterProto)
raidStats := env.initialize(raidProto, encounterProto)
env.finalize(raidProto, encounterProto, raidStats)
env.finalize(raidProto, encounterProto, raidStats, runFakePrepull)

encounterStats := &proto.EncounterStats{}
for _, target := range env.Encounter.Targets {
Expand Down Expand Up @@ -142,7 +142,7 @@ func (env *Environment) initialize(raidProto *proto.Raid, encounterProto *proto.
}

// The finalization phase.
func (env *Environment) finalize(raidProto *proto.Raid, _ *proto.Encounter, raidStats *proto.RaidStats) {
func (env *Environment) finalize(raidProto *proto.Raid, _ *proto.Encounter, raidStats *proto.RaidStats, runFakePrepull bool) {
for _, finalizeEffect := range env.preFinalizeEffects {
finalizeEffect()
}
Expand Down Expand Up @@ -186,14 +186,26 @@ func (env *Environment) finalize(raidProto *proto.Raid, _ *proto.Encounter, raid

env.setupAttackTables()

env.State = Finalized

if runFakePrepull {
// Runs prepull only, for a single iteration. This lets us detect misconfigured
// prepull spells (e.g. GCD not available) in APL.
sim := newSimWithEnv(env, &proto.SimOptions{
Iterations: 1,
})
sim.Init()
sim.reset()
sim.PrePull()
sim.Cleanup()
}

for partyIdx, party := range env.Raid.Parties {
for _, player := range party.Players {
character := player.GetCharacter()
character.FillPlayerStats(raidStats.Parties[partyIdx].Players[character.PartyIndex])
}
}

env.State = Finalized
}

func (env *Environment) setupAttackTables() {
Expand Down
7 changes: 5 additions & 2 deletions sim/core/sim.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,16 @@ func runSim(rsr *proto.RaidSimRequest, progress chan *proto.ProgressMetrics, ski
}

func NewSim(rsr *proto.RaidSimRequest) *Simulation {
simOptions := rsr.SimOptions
env, _, _ := NewEnvironment(rsr.Raid, rsr.Encounter, false)
return newSimWithEnv(env, rsr.SimOptions)
}

func newSimWithEnv(env *Environment, simOptions *proto.SimOptions) *Simulation {
rseed := simOptions.RandomSeed
if rseed == 0 {
rseed = time.Now().UnixNano()
}

env, _, _ := NewEnvironment(rsr.Raid, rsr.Encounter)
return &Simulation{
Environment: env,
Options: simOptions,
Expand Down
2 changes: 1 addition & 1 deletion sim/rogue/rogue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func GenerateCriticalDamageMultiplierTestCase(
TalentsString: talents,
}, spec), nil, nil, nil)
encounter := core.MakeSingleTargetEncounter(0.0)
env, _, _ := core.NewEnvironment(raid, encounter)
env, _, _ := core.NewEnvironment(raid, encounter, false)
agent := env.Raid.Parties[0].Players[0]
rog := agent.(RogueAgent).GetRogue()
actualMultiplier := 0.0
Expand Down
2 changes: 1 addition & 1 deletion sim/warlock/TestDemonology.results
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ character_stats_results: {
final_stats: 1889.415
final_stats: 1569.7
final_stats: 1337.6
final_stats: 4285.664
final_stats: 4701.97114
final_stats: 109
final_stats: 379
final_stats: 1773.98174
Expand Down

0 comments on commit 5916a25

Please sign in to comment.