Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port Spell Queueing + Separate Rotation Timer from Cata #1191

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
16 changes: 15 additions & 1 deletion proto/apl.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ message APLListItem {
APLAction action = 3; // The action to be performed.
}

// NextIndex: 24
// NextIndex: 25
message APLAction {
APLValue condition = 1; // If set, action will only execute if value is true or != 0.

Expand Down Expand Up @@ -119,6 +119,7 @@ message APLValue {
APLValueGCDTimeToReady gcd_time_to_ready = 18;

// Autoattack values
APLValueAutoTimeSinceLast auto_time_since_last = 75;
APLValueAutoTimeToNext auto_time_to_next = 40;
APLValueAutoSwingTime auto_swing_time = 64;

Expand Down Expand Up @@ -392,6 +393,19 @@ message APLValueEnergyThreshold {
message APLValueGCDIsReady {}
message APLValueGCDTimeToReady {}

message APLValueAutoTimeSinceLast {
enum AttackType
{
Unknown = 0;
Any = 1;
Melee = 2;
MainHand = 3;
OffHand = 4;
Ranged = 5;
}
AttackType auto_type = 1;
}

message APLValueAutoTimeToNext {
enum AttackType
{
Expand Down
20 changes: 16 additions & 4 deletions sim/core/apl.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package core
import (
"fmt"
"slices"
"time"

"github.com/wowsims/sod/sim/core/proto"
"google.golang.org/protobuf/encoding/protojson"
Expand Down Expand Up @@ -32,6 +31,9 @@ type APLRotation struct {
// Used to avoid recursive APL loops.
inLoop bool

// Used to override MCD restrictions within sequences.
inSequence bool

// Validation warnings that occur during proto parsing.
// We return these back to the user for display in the UI.
curWarnings []string
Expand Down Expand Up @@ -194,6 +196,10 @@ func (apl *APLRotation) DoNextAction(sim *Simulation) {
return
}

if !apl.unit.RotationTimer.IsReady(sim) {
return
}

i := 0
apl.inLoop = true

Expand All @@ -210,9 +216,15 @@ func (apl *APLRotation) DoNextAction(sim *Simulation) {
apl.unit.Log(sim, "No available actions!")
}

gcdReady := apl.unit.GCD.IsReady(sim)
if gcdReady {
apl.unit.WaitUntil(sim, sim.CurrentTime+time.Millisecond*50)
// Schedule the next rotation evaluation based on either the GCD or reaction time
if apl.unit.RotationTimer.IsReady(sim) {
nextEvaluation := sim.CurrentTime + apl.unit.ReactionTime

if !apl.unit.IsMoving() {
nextEvaluation = max(nextEvaluation, apl.unit.NextGCDAt())
}

apl.unit.WaitUntil(sim, nextEvaluation)
}
}

Expand Down
30 changes: 11 additions & 19 deletions sim/core/apl_actions_casting.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ func (rot *APLRotation) newActionCastSpell(config *proto.APLActionCastSpell) APL
}
}
func (action *APLActionCastSpell) IsReady(sim *Simulation) bool {
return action.spell.CanCast(sim, action.target.Get()) && (!action.spell.Flags.Matches(SpellFlagMCD) || action.spell.Unit.GCD.IsReady(sim) || action.spell.DefaultCast.GCD == 0)
return action.spell.CanCastOrQueue(sim, action.target.Get()) && (!action.spell.Flags.Matches(SpellFlagMCD) || action.spell.Flags.Matches(SpellFlagOffGCD) || action.spell.Unit.GCD.IsReady(sim) || action.spell.Unit.Rotation.inSequence)
}
func (action *APLActionCastSpell) Execute(sim *Simulation) {
action.spell.Cast(sim, action.target.Get())
action.spell.CastOrQueue(sim, action.target.Get())
}
func (action *APLActionCastSpell) String() string {
return fmt.Sprintf("Cast Spell(%s)", action.spell.ActionID)
Expand Down Expand Up @@ -80,20 +80,12 @@ func (action *APLActionChannelSpell) GetAPLValues() []APLValue {
return []APLValue{action.interruptIf}
}
func (action *APLActionChannelSpell) IsReady(sim *Simulation) bool {
return action.spell.CanCast(sim, action.target.Get())
return action.spell.CanCastOrQueue(sim, action.target.Get())
}
func (action *APLActionChannelSpell) Execute(sim *Simulation) {
action.spell.Cast(sim, action.target.Get())

if action.instantInterrupt {
dot := action.spell.Unit.ChanneledDot
if dot != nil {
dot.Cancel(sim)
}
} else {
action.spell.Unit.Rotation.interruptChannelIf = action.interruptIf
action.spell.Unit.Rotation.allowChannelRecastOnInterrupt = action.allowRecast
}
action.spell.CastOrQueue(sim, action.target.Get())
action.spell.Unit.Rotation.interruptChannelIf = action.interruptIf
action.spell.Unit.Rotation.allowChannelRecastOnInterrupt = action.allowRecast
}
func (action *APLActionChannelSpell) String() string {
return fmt.Sprintf("Channel Spell(%s, interruptIf=%s)", action.spell.ActionID, action.interruptIf)
Expand Down Expand Up @@ -150,7 +142,7 @@ func (action *APLActionMultidot) IsReady(sim *Simulation) bool {
for i := int32(0); i < action.maxDots; i++ {
target := sim.Raid.AllPlayerUnits[i]
dot := action.spell.Dot(target)
if (!dot.IsActive() || dot.RemainingDuration(sim) < maxOverlap) && action.spell.CanCast(sim, target) {
if (!dot.IsActive() || dot.RemainingDuration(sim) < maxOverlap) && action.spell.CanCastOrQueue(sim, target) {
action.nextTarget = target
return true
}
Expand All @@ -159,7 +151,7 @@ func (action *APLActionMultidot) IsReady(sim *Simulation) bool {
for i := int32(0); i < action.maxDots; i++ {
target := sim.Encounter.TargetUnits[i]
dot := action.spell.Dot(target)
if (!dot.IsActive() || dot.RemainingDuration(sim) < maxOverlap) && action.spell.CanCast(sim, target) {
if (!dot.IsActive() || dot.RemainingDuration(sim) < maxOverlap) && action.spell.CanCastOrQueue(sim, target) {
action.nextTarget = target
return true
}
Expand All @@ -168,7 +160,7 @@ func (action *APLActionMultidot) IsReady(sim *Simulation) bool {
return false
}
func (action *APLActionMultidot) Execute(sim *Simulation) {
action.spell.Cast(sim, action.nextTarget)
action.spell.CastOrQueue(sim, action.nextTarget)
}
func (action *APLActionMultidot) String() string {
return fmt.Sprintf("Multidot(%s)", action.spell.ActionID)
Expand Down Expand Up @@ -221,15 +213,15 @@ func (action *APLActionMultishield) IsReady(sim *Simulation) bool {
for i := int32(0); i < action.maxShields; i++ {
target := sim.Raid.AllPlayerUnits[i]
shield := action.spell.Shield(target)
if (!shield.IsActive() || shield.RemainingDuration(sim) < maxOverlap) && action.spell.CanCast(sim, target) {
if (!shield.IsActive() || shield.RemainingDuration(sim) < maxOverlap) && action.spell.CanCastOrQueue(sim, target) {
action.nextTarget = target
return true
}
}
return false
}
func (action *APLActionMultishield) Execute(sim *Simulation) {
action.spell.Cast(sim, action.nextTarget)
action.spell.CastOrQueue(sim, action.nextTarget)
}
func (action *APLActionMultishield) String() string {
return fmt.Sprintf("Multishield(%s)", action.spell.ActionID)
Expand Down
2 changes: 1 addition & 1 deletion sim/core/apl_actions_misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ type APLActionCustomRotation struct {
lastExecutedAt time.Duration
}

func (rot *APLRotation) newActionCustomRotation(config *proto.APLActionCustomRotation) APLActionImpl {
func (rot *APLRotation) newActionCustomRotation(_ *proto.APLActionCustomRotation) APLActionImpl {
agent := rot.unit.Env.GetAgentFromUnit(rot.unit)
if agent == nil {
panic("Agent not found for custom rotation")
Expand Down
79 changes: 66 additions & 13 deletions sim/core/apl_actions_sequences.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package core
import (
"fmt"
"strings"
"time"

"github.com/wowsims/sod/sim/core/proto"
)
Expand Down Expand Up @@ -42,11 +43,31 @@ func (action *APLActionSequence) Reset(*Simulation) {
action.curIdx = 0
}
func (action *APLActionSequence) IsReady(sim *Simulation) bool {
return action.curIdx < len(action.subactions) && action.subactions[action.curIdx].IsReady(sim)
action.unit.Rotation.inSequence = true
isReady := (action.curIdx < len(action.subactions)) && action.subactions[action.curIdx].IsReady(sim)
action.unit.Rotation.inSequence = false
return isReady
}
func (action *APLActionSequence) Execute(sim *Simulation) {
action.unit.Rotation.inSequence = true
action.subactions[action.curIdx].Execute(sim)
action.curIdx++

if action.unit.CanQueueSpell(sim) {
// Only advance to the next step in the sequence if we actually cast a spell rather than simply queueing one up.
action.curIdx++
} else {
// If we did queue up a spell, then modify the queue action to advance the sequence when it fires.
queueAction := action.unit.QueuedSpell.queueAction
oldFunc := queueAction.OnAction
queueAction.OnAction = func(sim *Simulation) {
oldFunc(sim)
action.curIdx++
queueAction.OnAction = oldFunc
}
action.unit.SetRotationTimer(sim, queueAction.NextActionAt+time.Duration(1))
}

action.unit.Rotation.inSequence = false
}
func (action *APLActionSequence) String() string {
return "Sequence(" + strings.Join(MapSlice(action.subactions, func(subaction *APLAction) string { return fmt.Sprintf("(%s)", subaction) }), "+") + ")"
Expand Down Expand Up @@ -122,38 +143,70 @@ func (action *APLActionStrictSequence) Reset(*Simulation) {
action.curIdx = 0
}
func (action *APLActionStrictSequence) IsReady(sim *Simulation) bool {
if !action.unit.GCD.IsReady(sim) {
return false
}
action.unit.Rotation.inSequence = true

// fmt.Println(sim.CurrentTime, "In sequence", action.unit.GCD.TimeToReady(sim) > MaxSpellQueueWindow, !action.subactions[0].IsReady(sim))

// if action.unit.GCD.TimeToReady(sim) > MaxSpellQueueWindow {
// action.unit.Rotation.inSequence = false
// return false
// }
if !action.subactions[0].IsReady(sim) {
action.unit.Rotation.inSequence = false
return false
}
for _, spell := range action.subactionSpells {
if !spell.IsReady(sim) {
action.unit.Rotation.inSequence = false
return false
}
}

return true
}
func (action *APLActionStrictSequence) Execute(sim *Simulation) {
action.unit.Rotation.pushControllingAction(action)
}
func (action *APLActionStrictSequence) relinquishControl() {
action.curIdx = 0
action.unit.Rotation.inSequence = false
action.unit.Rotation.popControllingAction(action)
}
func (action *APLActionStrictSequence) advanceSequence() {
action.curIdx++
if action.curIdx == len(action.subactions) {
action.relinquishControl()
}
}
func (action *APLActionStrictSequence) GetNextAction(sim *Simulation) *APLAction {
if action.subactions[action.curIdx].IsReady(sim) {
nextAction := action.subactions[action.curIdx]

action.curIdx++
if action.curIdx == len(action.subactions) {
action.curIdx = 0
action.unit.Rotation.popControllingAction(action)
}
// fmt.Println(sim.CurrentTime, nextAction, nextAction.IsReady(sim))
action.advanceSequence()

// if nextAction.IsReady(sim) {
// action.advanceSequence()
// } else {
// pa := &PendingAction{
// NextActionAt: action.unit.NextGCDAt(),
// Priority: ActionPriorityLow,

// OnAction: func(sim *Simulation) {
// if action.unit.Rotation.inSequence {
// action.advanceSequence()
// }
// },
// }
// sim.AddPendingAction(pa)
// action.unit.SetRotationTimer(sim, pa.NextActionAt+time.Duration(1))
// }

return nextAction
} else if action.unit.GCD.IsReady(sim) {
} else if action.unit.GCD.TimeToReady(sim) <= MaxSpellQueueWindow {
// If the GCD is ready when the next subaction isn't, it means the sequence is bad
// so reset and exit the sequence.
action.curIdx = 0
action.unit.Rotation.popControllingAction(action)
action.relinquishControl()
return action.unit.Rotation.getNextAction(sim)
} else {
// Return nil to wait for the GCD to become ready.
Expand Down
2 changes: 1 addition & 1 deletion sim/core/apl_actions_timing.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (action *APLActionWait) Execute(sim *Simulation) {

pa := &PendingAction{
Priority: ActionPriorityLow,
OnAction: action.unit.gcdAction.OnAction,
OnAction: action.unit.rotationAction.OnAction,
NextActionAt: action.curWaitTime,
}
sim.AddPendingAction(pa)
Expand Down
4 changes: 3 additions & 1 deletion sim/core/apl_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ func (rot *APLRotation) newAPLValue(config *proto.APLValue) APLValue {
case *proto.APLValue_GcdTimeToReady:
return rot.newValueGCDTimeToReady(config.GetGcdTimeToReady())

// Auto attacks
// Auto attacks
case *proto.APLValue_AutoTimeSinceLast:
return rot.newValueAutoTimeSinceLast(config.GetAutoTimeSinceLast())
case *proto.APLValue_AutoTimeToNext:
return rot.newValueAutoTimeToNext(config.GetAutoTimeToNext())
case *proto.APLValue_AutoSwingTime:
Expand Down
35 changes: 35 additions & 0 deletions sim/core/apl_values_auto_attacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ import (
"github.com/wowsims/sod/sim/core/proto"
)

type APLValueAutoTimeSinceLast struct {
DefaultAPLValueImpl
unit *Unit
autoType proto.APLValueAutoTimeSinceLast_AttackType
}

func (rot *APLRotation) newValueAutoTimeSinceLast(config *proto.APLValueAutoTimeSinceLast) APLValue {
return &APLValueAutoTimeSinceLast{
unit: rot.unit,
autoType: config.AutoType,
}
}
func (value *APLValueAutoTimeSinceLast) Type() proto.APLValueType {
return proto.APLValueType_ValueTypeDuration
}
func (value *APLValueAutoTimeSinceLast) GetDuration(sim *Simulation) time.Duration {
duration := time.Duration(0)
switch value.autoType {
case proto.APLValueAutoTimeSinceLast_Melee:
return max(duration, sim.CurrentTime-value.unit.AutoAttacks.LastAutoAt())
case proto.APLValueAutoTimeSinceLast_MainHand:
return max(duration, sim.CurrentTime-value.unit.AutoAttacks.LastMainhandAutoAt())
case proto.APLValueAutoTimeSinceLast_OffHand:
return max(duration, sim.CurrentTime-value.unit.AutoAttacks.LastOffhandAutoAt())
case proto.APLValueAutoTimeSinceLast_Ranged:
return max(duration, sim.CurrentTime-value.unit.AutoAttacks.LastRangedAutoAt())
default:
// defaults to Any
return max(duration, sim.CurrentTime-value.unit.AutoAttacks.LastAnyAutoAt())
}
}
func (value *APLValueAutoTimeSinceLast) String() string {
return "Auto Time Since Last"
}

type APLValueAutoTimeToNext struct {
DefaultAPLValueImpl
unit *Unit
Expand Down
6 changes: 3 additions & 3 deletions sim/core/apl_values_gcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type APLValueGCDIsReady struct {
unit *Unit
}

func (rot *APLRotation) newValueGCDIsReady(config *proto.APLValueGCDIsReady) APLValue {
func (rot *APLRotation) newValueGCDIsReady(_ *proto.APLValueGCDIsReady) APLValue {
return &APLValueGCDIsReady{
unit: rot.unit,
}
Expand All @@ -20,7 +20,7 @@ func (value *APLValueGCDIsReady) Type() proto.APLValueType {
return proto.APLValueType_ValueTypeBool
}
func (value *APLValueGCDIsReady) GetBool(sim *Simulation) bool {
return value.unit.GCD.IsReady(sim)
return value.unit.GCD.IsReady(sim) || (value.unit.GCD.TimeToReady(sim) <= MaxSpellQueueWindow)
}
func (value *APLValueGCDIsReady) String() string {
return "GCD Is Ready"
Expand All @@ -31,7 +31,7 @@ type APLValueGCDTimeToReady struct {
unit *Unit
}

func (rot *APLRotation) newValueGCDTimeToReady(config *proto.APLValueGCDTimeToReady) APLValue {
func (rot *APLRotation) newValueGCDTimeToReady(_ *proto.APLValueGCDTimeToReady) APLValue {
return &APLValueGCDTimeToReady{
unit: rot.unit,
}
Expand Down
Loading
Loading