Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sim/core/apl_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (action *APLAction) Finalize(rot *APLRotation) {
}

func (action *APLAction) IsReady(sim *Simulation) bool {
return (action.condition == nil || action.condition.GetBool(sim)) && action.impl.IsReady(sim)
return action.impl.IsReady(sim) && (action.condition == nil || action.condition.GetBool(sim))
}

func (action *APLAction) Execute(sim *Simulation) {
Expand Down
54 changes: 44 additions & 10 deletions sim/core/apl_values_dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,18 @@ func (value *APLValueDotBaseDuration) String() string {

type APLValueDotIncreaseCheck struct {
DefaultAPLValueImpl
rot *APLRotation
spell *Spell
targetRef UnitReference
baseName string
useBaseValue bool // if true, use the base value before any increases
baseValue float64
baseValueDummyAura *Aura // Used to get the base value at encounter start

// evalGeneration cache: avoids re-computing when the same value node is
// evaluated multiple times in a single getNextAction scan.
cachedGen uint32
cachedValue float64
}

func (rot *APLRotation) newDotIncreaseValue(baseName string, config *proto.APLValueDotPercentIncrease) *APLValueDotIncreaseCheck {
Expand All @@ -278,6 +284,7 @@ func (rot *APLRotation) newDotIncreaseValue(baseName string, config *proto.APLVa
}

return &APLValueDotIncreaseCheck{
rot: rot,
spell: spell,
targetRef: targetRef,
baseName: baseName,
Expand Down Expand Up @@ -316,15 +323,25 @@ func (value *APLValueDotPercentIncrease) Finalize(rot *APLRotation) {
}

func (value *APLValueDotPercentIncrease) GetFloat(sim *Simulation) float64 {
if gen := value.rot.evalGeneration; value.cachedGen == gen {
return value.cachedValue
} else {
value.cachedGen = gen
}

target := value.targetRef.Get()
expectedDamage := TernaryFloat64(value.useBaseValue, value.baseValue, value.spell.ExpectedTickDamageFromCurrentSnapshot(sim, target))

var result float64
if expectedDamage == 0 {
return 1
result = 1
} else {
// Rounding to effectively 3 decimal places as a percentage to avoid floating point errors
result = math.Round((value.spell.ExpectedTickDamage(sim, target)/expectedDamage)*100000)/100000 - 1
}

// Rounding this to effectively 3 decimal places as a percentage to avoid floating point errors
return math.Round((value.spell.ExpectedTickDamage(sim, target)/expectedDamage)*100000)/100000 - 1
value.cachedValue = result
return result
}

type APLValueDotCritPercentIncrease struct {
Expand All @@ -349,12 +366,21 @@ func (value *APLValueDotCritPercentIncrease) Finalize(rot *APLRotation) {
}

func (value *APLValueDotCritPercentIncrease) GetFloat(sim *Simulation) float64 {
if gen := value.rot.evalGeneration; value.cachedGen == gen {
return value.cachedValue
} else {
value.cachedGen = gen
}

currentCritChance := value.getCritChance(true)
var result float64
if currentCritChance == 0 {
return 1
result = 1
} else {
result = value.getCritChance(false)/currentCritChance - 1
}
val := value.getCritChance(false)/currentCritChance - 1
return val
value.cachedValue = result
return result
}

func (value *APLValueDotCritPercentIncrease) getCritChance(useSnapshot bool) float64 {
Expand Down Expand Up @@ -389,13 +415,21 @@ func (value *APLValueDotTickRatePercentIncrease) Finalize(rot *APLRotation) {
}

func (value *APLValueDotTickRatePercentIncrease) GetFloat(sim *Simulation) float64 {
currentTickrate := value.getTickRate(true)
if gen := value.rot.evalGeneration; value.cachedGen == gen {
return value.cachedValue
} else {
value.cachedGen = gen
}

currentTickrate := value.getTickRate(true)
var result float64
if currentTickrate == 0 {
return 1
result = 1
} else {
result = currentTickrate/value.getTickRate(false) - 1
}

return currentTickrate/value.getTickRate(false) - 1
value.cachedValue = result
return result
}

func (value *APLValueDotTickRatePercentIncrease) getTickRate(useSnapshot bool) float64 {
Expand Down
16 changes: 9 additions & 7 deletions sim/core/apl_values_operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,10 @@ func getConstAPLFloatValue(value APLValue) float64 {

type APLValueCompare struct {
DefaultAPLValueImpl
op proto.APLValueCompare_ComparisonOperator
lhs APLValue
rhs APLValue
op proto.APLValueCompare_ComparisonOperator
lhs APLValue
rhs APLValue
lhsType proto.APLValueType // cached at construction, never changes
}

func (value *APLValueCompare) GetInnerValues() []APLValue {
Expand All @@ -296,7 +297,7 @@ func (value *APLValueCompare) Type() proto.APLValueType {
return proto.APLValueType_ValueTypeBool
}
func (value *APLValueCompare) GetBool(sim *Simulation) bool {
switch value.lhs.Type() {
switch value.lhsType {
case proto.APLValueType_ValueTypeBool:
switch value.op {
case proto.APLValueCompare_OpEq:
Expand Down Expand Up @@ -827,9 +828,10 @@ func (rot *APLRotation) newValueCompare(config *proto.APLValueCompare, uuid *pro
}

return &APLValueCompare{
op: config.Op,
lhs: lhs,
rhs: rhs,
op: config.Op,
lhs: lhs,
rhs: rhs,
lhsType: lhs.Type(),
}
}

Expand Down
40 changes: 25 additions & 15 deletions sim/core/aura.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,8 @@ type auraTracker struct {
// All registered auras, both active and inactive.
auras []*Aura

aurasByTag map[string][]*Aura
aurasByTag map[string][]*Aura
aurasByLabel map[string]*Aura

// IDs of Auras that may expire and are currently active, in no particular order.
activeAuras []*Aura
Expand All @@ -440,16 +441,12 @@ func newAuraTracker() auraTracker {
resetEffects: []ResetEffect{},
ExclusiveEffectManager: &ExclusiveEffectManager{},
aurasByTag: make(map[string][]*Aura),
aurasByLabel: make(map[string]*Aura),
}
}

func (at *auraTracker) GetAura(label string) *Aura {
for _, aura := range at.auras {
if aura.Label == label {
return aura
}
}
return nil
return at.aurasByLabel[label]
}
func (at *auraTracker) GetAuras() []*Aura {
return at.auras
Expand Down Expand Up @@ -515,6 +512,7 @@ func (at *auraTracker) registerAura(unit *Unit, aura Aura) *Aura {
newAura.onEncounterStartIndex = Inactive

at.auras = append(at.auras, newAura)
at.aurasByLabel[newAura.Label] = newAura
if newAura.Tag != "" {
at.aurasByTag[newAura.Tag] = append(at.aurasByTag[newAura.Tag], newAura)
}
Expand Down Expand Up @@ -627,16 +625,28 @@ func (at *auraTracker) tryAdvance(sim *Simulation) time.Duration {
}

func (at *auraTracker) advance(sim *Simulation) time.Duration {
restart:
at.minExpires = NeverExpires
for _, aura := range at.activeAuras {
if aura.expires <= sim.CurrentTime {
aura.Deactivate(sim)
goto restart // activeAuras have changed
var toExpire [16]*Aura
for {
n := 0
at.minExpires = NeverExpires
for _, aura := range at.activeAuras {
if aura.expires <= sim.CurrentTime {
if n < len(toExpire) {
toExpire[n] = aura
}
n++
} else {
at.minExpires = min(at.minExpires, aura.expires)
}
}
if n == 0 {
return at.minExpires
}
limit := min(n, len(toExpire))
for i := range limit {
toExpire[i].Deactivate(sim)
}
at.minExpires = min(at.minExpires, aura.expires)
}
return at.minExpires
}

func (at *auraTracker) expireAll(sim *Simulation) {
Expand Down
30 changes: 16 additions & 14 deletions sim/core/runic_power.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ func (rp *runicPowerBar) reset(sim *Simulation) {

if rp.pa != nil {
rp.pa.Cancel(sim)
// Sentinel so the at >= NextActionAt guard in launchPA doesn't skip
// rescheduling at the start of the next simulation iteration.
rp.pa.NextActionAt = NeverExpires
}

for i := range rp.runeMeta {
Expand Down Expand Up @@ -195,8 +198,8 @@ func (rp *runicPowerBar) MaximumRunicPower() float64 {
func (rp *runicPowerBar) maybeFireChange(sim *Simulation, changeType RuneChangeType) {
if changeType != None && rp.onRuneChange != nil {
rp.onRuneChange(sim, changeType, rp.lastRegen)
// Clear regen runes
rp.lastRegen = make([]int8, 0)
// Clear regen runes without reallocating
rp.lastRegen = rp.lastRegen[:0]
}
}

Expand Down Expand Up @@ -974,29 +977,28 @@ func (rp *runicPowerBar) launchPA(sim *Simulation, at time.Duration) {
if at >= rp.pa.NextActionAt {
return
}
// If this new regen is before currently scheduled one, we must cancel old regen and start a new one.
// New regen fires sooner — cancel the current schedule.
// PendingAction.Cancel removes the PA from pendingActions, so rp.pa is
// immediately safe to reset and re-add without creating a duplicate.
rp.pa.Cancel(sim)
}

pa := &PendingAction{
NextActionAt: at,
Priority: ActionPriorityRegen,
}
pa.OnAction = func(sim *Simulation) {
if !pa.cancelled {
// regenerate and revert
if rp.pa == nil {
// First rune spend ever: allocate the PA and closure once per DK lifetime.
pa := &PendingAction{Priority: ActionPriorityRegen}
pa.OnAction = func(sim *Simulation) {
rp.Advance(sim, sim.CurrentTime)

// Check when we need next check
pa.NextActionAt = min(rp.AnySpentRuneReadyAt(), rp.DeathRuneRevertAt())
if pa.NextActionAt < NeverExpires {
sim.AddPendingAction(pa)
}
}
rp.pa = pa
}
rp.pa = pa
sim.AddPendingAction(pa)

rp.pa.cancelled = false
rp.pa.NextActionAt = at
sim.AddPendingAction(rp.pa)
}

func (rp *runicPowerBar) Advance(sim *Simulation, newTime time.Duration) {
Expand Down
20 changes: 19 additions & 1 deletion sim/core/spell.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ type Spell struct {
resultCache SpellResultCache
resultSlice SpellResultSlice

// Per-target expected-damage caches indexed by target.UnitIndex.
// Allocated in finalize() only for spells that define ExpectedTickDamage.
// Replaces the per-AttackTable maps to eliminate pointer hashing and lazy allocation.
expectedInitialDmgCache []ExpectedDamageCalculatorCache
expectedTickDmgCache []ExpectedDamageCalculatorCache
expectedTickSnapshotDmgCache []ExpectedDamageCalculatorCache

dots DotArray
aoeDot *Dot

Expand Down Expand Up @@ -484,11 +491,18 @@ func (spell *Spell) finalize() {
if len(spell.splitSpellMetrics) > 1 && spell.ActionID.Tag != 0 {
panic(spell.ActionID.String() + " has split metrics and a non-zero tag, can only have one!")
}
numUnits := len(spell.Unit.Env.AllUnits)
for i := range spell.splitSpellMetrics {
spell.splitSpellMetrics[i] = make([]SpellMetrics, len(spell.Unit.Env.AllUnits))
spell.splitSpellMetrics[i] = make([]SpellMetrics, numUnits)
}
spell.SpellMetrics = spell.splitSpellMetrics[0]

if spell.expectedInitialDamageInternal != nil || spell.expectedTickDamageInternal != nil {
spell.expectedInitialDmgCache = make([]ExpectedDamageCalculatorCache, numUnits)
spell.expectedTickDmgCache = make([]ExpectedDamageCalculatorCache, numUnits)
spell.expectedTickSnapshotDmgCache = make([]ExpectedDamageCalculatorCache, numUnits)
}

// Set the "static" "default" cost here
if spell.Cost != nil {
spell.DefaultCast.Cost = spell.Cost.GetCurrentCost()
Expand All @@ -507,6 +521,10 @@ func (spell *Spell) reset(sim *Simulation) {
spell.rechargeTimer = nil
spell.charges = spell.MaxCharges
}
// Clear expected-damage caches so values from the previous iteration don't bleed through.
clear(spell.expectedInitialDmgCache)
clear(spell.expectedTickDmgCache)
clear(spell.expectedTickSnapshotDmgCache)
}

func (spell *Spell) SetMetricsSplit(splitIdx int32) {
Expand Down
31 changes: 8 additions & 23 deletions sim/core/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,6 @@ type AttackTable struct {
DamageDoneByCasterExtraMultiplier []DynamicDamageDoneByCaster

ThreatDoneByCasterExtraMultiplier []DynamicThreatDoneByCaster

// Store Expected Initial/DoT Tick values to speed up DoT damage calculations
expectedInitialDamageCache map[*Spell]*ExpectedDamageCalculatorCache
expectedTickDamageCache map[*Spell]*ExpectedDamageCalculatorCache
expectedTickSnapshotDamageCache map[*Spell]*ExpectedDamageCalculatorCache
}

func NewAttackTable(attacker *Unit, defender *Unit) *AttackTable {
Expand All @@ -378,10 +373,6 @@ func NewAttackTable(attacker *Unit, defender *Unit) *AttackTable {
DamageTakenMultiplier: 1,
RangedDamageTakenMultiplier: 1,
HealingDealtMultiplier: 1,

expectedInitialDamageCache: make(map[*Spell]*ExpectedDamageCalculatorCache),
expectedTickDamageCache: make(map[*Spell]*ExpectedDamageCalculatorCache),
expectedTickSnapshotDamageCache: make(map[*Spell]*ExpectedDamageCalculatorCache),
}

if defender.Type == EnemyUnit {
Expand Down Expand Up @@ -429,26 +420,20 @@ func DisableThreatDoneByCaster(index int, attackTable *AttackTable) {
}

func GetCachedExpectedInitialDamage(sim *Simulation, spell *Spell, target *Unit) (bool, *ExpectedDamageCalculatorCache) {
return getCachedExpectedDamageInternal(sim, spell, spell.Unit.AttackTables[target.Index].expectedInitialDamageCache)
return getCachedExpectedDamageSlice(sim, spell, spell.expectedInitialDmgCache, target.UnitIndex)
}

func GetCachedExpectedTickDamage(sim *Simulation, spell *Spell, target *Unit, useSnapshot bool) (bool, *ExpectedDamageCalculatorCache) {
if useSnapshot {
return getCachedExpectedDamageInternal(sim, spell, spell.Unit.AttackTables[target.Index].expectedTickSnapshotDamageCache)
return getCachedExpectedDamageSlice(sim, spell, spell.expectedTickSnapshotDmgCache, target.UnitIndex)
}

return getCachedExpectedDamageInternal(sim, spell, spell.Unit.AttackTables[target.Index].expectedTickDamageCache)
return getCachedExpectedDamageSlice(sim, spell, spell.expectedTickDmgCache, target.UnitIndex)
}

func getCachedExpectedDamageInternal(sim *Simulation, spell *Spell, store map[*Spell]*ExpectedDamageCalculatorCache) (bool, *ExpectedDamageCalculatorCache) {
if store[spell] == nil {
emptyCache := ExpectedDamageCalculatorCache{}
store[spell] = &emptyCache
func getCachedExpectedDamageSlice(sim *Simulation, spell *Spell, store []ExpectedDamageCalculatorCache, idx int32) (bool, *ExpectedDamageCalculatorCache) {
entry := &store[idx]
if (entry.timestamp - sim.CurrentTime).Abs() <= spell.Unit.ReactionTime {
return true, entry
}

if (store[spell].timestamp - sim.CurrentTime).Abs() <= spell.Unit.ReactionTime {
return true, store[spell]
}

return false, store[spell]
return false, entry
}
Loading
Loading