Skip to content
Merged
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
34 changes: 10 additions & 24 deletions ZygorGuidesViewer/ZygorGuidesViewer/Automation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,17 @@ function Automation:ExecuteBundledScript(goal)
end

function Automation:ConfigureActionButton(goal)
if not self.actionButton then return end
local attributes={type=nil,item=nil,macrotext=nil}
if goal and goal.itemID then attributes.type="item" attributes.item="item:"..goal.itemID
elseif goal and goal.script and goal.script:match("VehicleExit") then attributes.type="macro" attributes.macrotext="/leavevehicle" end
ZGV.Compat.UI:RunOutOfCombat("zygor-action-button",function()
Automation.actionButton:SetAttribute("type",attributes.type)
Automation.actionButton:SetAttribute("item",attributes.item)
Automation.actionButton:SetAttribute("macrotext",attributes.macrotext)
if attributes.type then Automation.actionButton:Show() else Automation.actionButton:Hide() end
end)
-- ModernActionBar owns the one authoritative secure quest-action surface.
-- The old automation button duplicated item actions at UIParent center and
-- displayed the complete actionbar sprite sheet as a striped square.
if ZGV.ActionBar and type(ZGV.ActionBar.Refresh)=="function" then
return ZGV.ActionBar:Refresh()
end
return false,"actionbar-unavailable"
end

function Automation:RefreshActionButton()
local goal=self:FindGoal()
self:ConfigureActionButton(goal)
return self:ConfigureActionButton(self:FindGoal())
end

function Automation:CaptureAbandon()
Expand Down Expand Up @@ -178,16 +174,6 @@ end

function Automation:OnStartup()
self.abandoned=ZGV.db.char.abandoned or {}
self.actionButton=ZGV.Compat.UI:CreateSecureActionButton("ZygorGuidesViewerActionButton",UIParent)
if self.actionButton then
self.actionButton:SetWidth(36) self.actionButton:SetHeight(36)
self.actionButton:SetPoint("CENTER",UIParent,"CENTER",0,-180)
local texture=self.actionButton:CreateTexture(nil,"ARTWORK")
texture:SetAllPoints(self.actionButton)
texture:SetTexture(ZGV.SKINDIR.."actionbar.tga")
self.actionButton.texture=texture
self.actionButton:Hide()
end
if type(hooksecurefunc)=="function" and type(SetAbandonQuest)=="function" then
hooksecurefunc("SetAbandonQuest",function() Automation:CaptureAbandon() end)
end
Expand All @@ -196,8 +182,8 @@ function Automation:OnStartup()
-- after the confirmed AbandonQuest call, so cancelling the dialog is safe.
hooksecurefunc("AbandonQuest",function() Automation:ConfirmAbandon() end)
end
ZGV:RegisterCallback("ZGV_STEP_CHANGED",self,"RefreshActionButton")
ZGV:RegisterCallback("ZGV_GUIDE_CHANGED",self,"RefreshActionButton")
-- ModernActionBar registers the guide/step callbacks. Keeping a second set
-- here would rebuild the same protected buttons twice for every change.
self:RefreshActionButton()
end

Expand Down
18 changes: 18 additions & 0 deletions ZygorGuidesViewer/ZygorGuidesViewer/Conditions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ function Conditions:Faction(value)
return tostring(value or ""):gsub("[_%s%-]",""):lower()==self.factionName
end

-- GetBindLocation is the authoritative 3.3.5a source for the Hearthstone's
-- destination. Return nil when the client cannot provide it so old clients do
-- not lose guide steps merely because the capability is unavailable.
function Conditions:Bound(name)
if type(GetBindLocation)~="function" then return nil end
local current=GetBindLocation("player")
if not current or current=="" then current=GetBindLocation() end
if not current or current=="" then return nil end
local function compactLocation(value)
return tostring(value or ""):lower():gsub("[^%a%d]","")
end
local wanted=compactLocation(name)
if wanted=="" then return current end
return compactLocation(current)==wanted
end

function Conditions:CompletedQuest(...)
for i=1,select("#",...) do if questCall("IsCompleted",select(i,...)) then return true end end
return false
Expand Down Expand Up @@ -276,6 +292,8 @@ bind("money","Money")
bind("havebuff","HaveBuff")
bind("hasbuff","HaveBuff")
bind("faction","Faction")
bind("bound","Bound")
bind("hearthbound","Bound")
safe.level=function() return UnitLevel("player") or 0 end
safe.isdead=function() return UnitIsDeadOrGhost("player") and true or false end
safe.ontaxi=function() return UnitOnTaxi and UnitOnTaxi("player") or false end
Expand Down
2 changes: 1 addition & 1 deletion ZygorGuidesViewer/ZygorGuidesViewer/Core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ ZGV.version = "8.1.0-wotlk.13"
-- Bump this whenever a client-facing package is deployed. It is included in
-- the first session diagnostic so a reload can be verified from the client
-- log rather than inferred from file timestamps.
ZGV.buildRevision = "20260715-travel-refresh-01"
ZGV.buildRevision = "20260716-hearth-bind-awareness-01"
ZGV.interface = 30300
ZGV.targetBuild = 12340
ZGV.DIR = "Interface\\AddOns\\ZygorGuidesViewer"
Expand Down
11 changes: 8 additions & 3 deletions ZygorGuidesViewer/ZygorGuidesViewer/Navigation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,14 @@ local function cooldownReady(getter,id)
if type(getter)~="function" then return true end
local start,duration,enabled=getter(id)
if enabled==0 then return false end
return not (tonumber(start) and tonumber(start)>0 and tonumber(duration) and tonumber(duration)>0)
start,duration=tonumber(start) or 0,tonumber(duration) or 0
-- Spell cooldown APIs report the shared global cooldown as if every spell
-- were unavailable. It must not continuously remove/re-add Astral Recall
-- from the route graph whenever the player casts an unrelated ability.
if duration>0 and duration<=1.6 then return true end
if start<=0 or duration<=0 then return true end
local now=type(GetTime)=="function" and GetTime() or 0
return start+duration<=now+.05
end

-- The 3.3.5 client reports the character's bind by its inn/city name, not a
Expand Down Expand Up @@ -808,11 +815,9 @@ function Navigation:GetAvailableTravelPorts(data)
local available=enabled
if available and port.item then
available=type(GetItemCount)=="function" and (tonumber(GetItemCount(port.item)) or 0)>0
if available and type(IsUsableItem)=="function" then available=IsUsableItem(port.item) and true or false end
if available then available=cooldownReady(GetItemCooldown,port.item) end
elseif available and port.spell then
available=type(IsSpellKnown)=="function" and IsSpellKnown(port.spell)
if available and type(IsUsableSpell)=="function" then available=IsUsableSpell(port.spell) and true or false end
if available then available=cooldownReady(GetSpellCooldown,port.spell) end
else
available=false
Expand Down
26 changes: 22 additions & 4 deletions ZygorGuidesViewer/ZygorGuidesViewer/Parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,11 @@ function Parser:ParseGoal(base, mods, inheritedMap)
text=action=="text" and displayText(content) or actionText(action,displayBody,count),
target=name, targetID=id, sourceBody=actionBody,
}
-- Authored hearth steps often use a separate `use Hearthstone` goal plus a
-- prose/condition line. Preserve the named destination so Runtime can
-- compare it with the live GetBindLocation result before offering the item.
local hearthDestination=content:match("^[Hh]earth%s+to%s+(.+)$")
if hearthDestination then goal.hearthDestination=displayText(hearthDestination) end

if QUEST_ACTIONS[action] then
goal.questID=id
Expand Down Expand Up @@ -1115,6 +1120,7 @@ function Parser:_ParseGuide(guide, stack)
goal.num=#current.goals+1
current.goals[#current.goals+1]=goal
lastGoal=goal
if goal.hearthDestination then current.hearthDestination=goal.hearthDestination end
if goal.questID then
if goal.daily then
ZGV.dailyQuests=ZGV.dailyQuests or {}
Expand Down Expand Up @@ -1301,12 +1307,24 @@ function Parser:_ParseGuide(guide, stack)
-- A leading pipe makes a standalone modifier a step modifier. This
-- is the old parser's chunk-count behaviour and is vital for the
-- legacy Wrath packs' standalone "only Race" lines.
current.onlyIf=addCondition(current.onlyIf,mods.onlyIf)
current.stickyIf=addCondition(current.stickyIf,mods.stickyIf)
-- `|nohearth |only if ...` is different: the condition scopes the
-- travel restriction, not the quest goal or the whole step. If it
-- leaks into either applicability check, leaving the named subzone
-- can skip an unfinished objective (notably the Bladespire half of
-- A Curse Upon Both of Your Clans) and jump directly to turn-in.
local travelOnly=mods.noHearth and true or false
if not travelOnly then
current.onlyIf=addCondition(current.onlyIf,mods.onlyIf)
current.stickyIf=addCondition(current.stickyIf,mods.stickyIf)
end
if mods.delay then current.delay=mods.delay end
if mods.noHearth then current.travelConfig=current.travelConfig or {}; current.travelConfig.use_hearth=false end
if mods.noHearth then
current.travelConfig=current.travelConfig or {}
current.travelConfig.use_hearth=false
current.travelConfig.noHearthIf=mods.onlyIf
end
addMarkers(mods)
if lastGoal then
if lastGoal and not travelOnly then
applyModifiers(lastGoal,mods)
if lastGoal.questID then
if lastGoal.daily then ZGV.dailyQuests=ZGV.dailyQuests or {}; ZGV.dailyQuests[lastGoal.questID]=true end
Expand Down
47 changes: 42 additions & 5 deletions ZygorGuidesViewer/ZygorGuidesViewer/Runtime.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
local ZGV = ZygorGuidesViewer
local Runtime = ZGV:RegisterModule("Runtime",{
currentGuide=nil,currentStep=1,manual={},killCounts={},collectionBaselines={},talked={},gossiped={},interacted={},discoveries={},autoHistory={},lastAdvance=0,autoAdvanceDelay=0.65,
currentGuide=nil,currentStep=1,manual={},arrivals={},killCounts={},collectionBaselines={},talked={},gossiped={},interacted={},discoveries={},autoHistory={},lastAdvance=0,autoAdvanceDelay=0.65,
})

local function questEntry(id)
Expand Down Expand Up @@ -51,6 +51,33 @@ function Runtime:ManualKey(stepIndex,goalIndex)
return (self.currentGuide and self.currentGuide.id or "none")..":"..tostring(stepIndex)..":"..tostring(goalIndex)
end

function Runtime:ClearStepArrivals(stepIndex,guide)
guide=guide or self.currentGuide
if not guide or not stepIndex then return end
local prefix=tostring(guide.id or "none")..":"..tostring(stepIndex)..":"
for key in pairs(self.arrivals or {}) do
if key:sub(1,#prefix)==prefix then self.arrivals[key]=nil end
end
end

function Runtime:CompleteArrival(goal,stepIndex,goalIndex,reason)
self.arrivals=self.arrivals or {}
local key=self:ManualKey(stepIndex,goalIndex)
if self.arrivals[key] then return true,"arrival" end
local complete=false
if reason=="map-transition" and ZGV.Navigation and ZGV.Navigation.IsMapTransitionComplete then
complete=ZGV.Navigation:IsMapTransitionComplete(goal.mapTransition)
elseif ZGV.Navigation and goal.destination then
complete=ZGV.Navigation:IsArrived(goal.destination)
end
if complete then
self.arrivals[key]={time=type(GetTime)=="function" and GetTime() or 0,reason=reason}
ZGV:LogInfo("progress","arrival credit for "..key.." at "..tostring(goal.text or goal.destination and goal.destination.mapKey or "waypoint"))
return true,"arrival"
end
return false,reason or "position"
end

function Runtime:UsesCollectionBaseline(goal)
if not goal or goal.questID or goal.objective or goal.allowExistingItems or goal.allowexistingitems then return false end
return goal.action=="collect" or goal.action=="buy" or goal.action=="create"
Expand Down Expand Up @@ -135,7 +162,12 @@ end
-- class/race branches disappear atomically and cannot leave a talk or gossip
-- objective behind to block the guide.
function Runtime:IsStepApplicable(step)
if not step or not step.onlyIf or step.onlyIf=="" then return true end
if not step then return true end
if step.hearthDestination and ZGV.Conditions and type(ZGV.Conditions.Bound)=="function" then
local matches=ZGV.Conditions:Bound(step.hearthDestination)
if matches==false then return false end
end
if not step.onlyIf or step.onlyIf=="" then return true end
local result,err=ZGV.Conditions:Evaluate(step.onlyIf,self.currentGuide)
if err then
self.currentGuide.conditionIssues=self.currentGuide.conditionIssues or {}
Expand Down Expand Up @@ -226,9 +258,9 @@ function Runtime:IsGoalComplete(goal,stepIndex,goalIndex)
-- such as Terokkar's Shadowy Disguise never reaches its own aura check.
elseif action=="goto" then
if goal.mapTransition and ZGV.Navigation and ZGV.Navigation.IsMapTransitionComplete then
return ZGV.Navigation:IsMapTransitionComplete(goal.mapTransition),"map-transition"
return self:CompleteArrival(goal,stepIndex,goalIndex,"map-transition")
end
if ZGV.Navigation then return ZGV.Navigation:IsArrived(goal.destination),"position" end
if ZGV.Navigation then return self:CompleteArrival(goal,stepIndex,goalIndex,"position") end
elseif action=="skill" or action=="reachskill" then
return ZGV.Conditions:Skill(goal.skillName)>=(goal.skillRank or 1),"skill"
elseif action=="skillmax" then
Expand Down Expand Up @@ -300,7 +332,7 @@ function Runtime:IsGoalComplete(goal,stepIndex,goalIndex)
end
return true,"equipment"
elseif action=="text" and goal.mapTransition and ZGV.Navigation and ZGV.Navigation.IsMapTransitionComplete then
return ZGV.Navigation:IsMapTransitionComplete(goal.mapTransition),"map-transition"
return self:CompleteArrival(goal,stepIndex,goalIndex,"map-transition")
elseif action=="text" or action=="map" or action=="label" or action=="info" or action=="image" or action=="webheader" or action=="webinfo" or action=="webimage" then
return true,"informational"
end
Expand Down Expand Up @@ -576,6 +608,7 @@ function Runtime:SelectGuide(value,step)
if not guide then ZGV:Print("Guide not found: "..tostring(value)) return false end
local parsed,err=ZGV.Parser:ParseGuide(guide)
if not parsed then ZGV:LogError("guide parse",err) return false end
self.arrivals={}
self.currentGuide=parsed
local selected=tonumber(step) or self:ResolveStepJump(parsed,0,step) or 1
self.currentStep=math.max(1,math.min(selected,#parsed.steps))
Expand Down Expand Up @@ -618,6 +651,8 @@ function Runtime:SetStep(index,manual,loopGuard)
return self:SetStep(index,true,true)
end
end
local previousStep=self.currentStep
self:ClearStepArrivals(previousStep)
self.currentStep=index
ZGV.db.profile.currentStep=index
self.lastAdvance=GetTime()
Expand Down Expand Up @@ -676,6 +711,7 @@ function Runtime:ResetCurrentGuide()
if not self.currentGuide then return end
local prefix=self.currentGuide.id..":"
for key in pairs(self.manual) do if key:sub(1,#prefix)==prefix then self.manual[key]=nil end end
for key in pairs(self.arrivals or {}) do if key:sub(1,#prefix)==prefix then self.arrivals[key]=nil end end
for key in pairs(self.killCounts) do if key:sub(1,#prefix)==prefix then self.killCounts[key]=nil end end
for key in pairs(self.collectionBaselines) do if key:sub(1,#prefix)==prefix then self.collectionBaselines[key]=nil end end
for key in pairs(self.talked) do if key:sub(1,#prefix)==prefix then self.talked[key]=nil end end
Expand Down Expand Up @@ -1059,6 +1095,7 @@ function Runtime:OnEvent(event,...)
end

function Runtime:OnStartup()
self.arrivals={}
self.killCounts=ZGV.db.char.killProgress or self.killCounts or {}
ZGV.db.char.killProgress=self.killCounts
self.collectionBaselines=ZGV.db.char.collectionBaselines or self.collectionBaselines or {}
Expand Down
15 changes: 15 additions & 0 deletions tools/tests/lua/test_buff_tag_conditions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,33 @@ end
function GetSpellInfo(id)
if id==32756 then return "Shadowy Disguise" end
end
local bindLocation="Shattrath City"
function GetBindLocation() return bindLocation end

dofile(addon.."Conditions.lua")
local Conditions=assert(ZygorGuidesViewer.Conditions)
assert(Conditions:HaveBuff("Shadowy Disguise##32756"),"name-plus-ID buff tags resolve through UnitBuff")
assert(Conditions:HaveBuff(32756),"numeric buff tags resolve through GetSpellInfo")
assert(Conditions:Bound("Shattrath City"),"bind condition accepts the live Hearthstone destination")
assert(not Conditions:Bound("Thunderlord Stronghold"),"bind condition rejects a different authored destination")

dofile(addon.."Parser.lua")
local Parser=assert(ZygorGuidesViewer.Parser)
local modifiers=Parser:ParseModifiers({"havebuff Shadowy Disguise##32756"})
local goal=Parser:ParseGoal("talk Scout Neftis##18714",modifiers)
assert(goal.haveBuff==32756,"havebuff modifier preserves its spell ID")
assert(goal.spellID==32756,"havebuff modifier exposes the spell ID to Runtime")
local hearthGuide=assert(Parser:ParseEntry({
id="hearth-bind",title="Hearth bind",header={},raw=[[
step
use Hearthstone##6948
Hearth to Thunderlord Stronghold |complete subzone("Thunderlord Stronghold") |q 10505
step
talk Gor'drek##21117 |goto Blade's Edge Mountains/0 52.32,57.75
]]
}))
assert(hearthGuide.steps[1].hearthDestination=="Thunderlord Stronghold",
"hearth instruction preserves its expected bind destination on the step")

-- Reproduce the critical part of the Terokkar "Who Are They?" branch. The
-- player must stop on the actual buff objective until the disguise is active;
Expand Down
16 changes: 16 additions & 0 deletions tools/tests/lua/test_navigation_routes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ GetItemCooldown=function() return 0,0,1 end
IsSpellKnown=function(spell) return spell==556 end
IsUsableSpell=function() return true end
GetSpellCooldown=function() return 0,0,1 end
GetTime=function() return 100 end

dofile(repo.."/ZygorGuidesViewer/ZygorGuidesViewer/Navigation.lua")
local Navigation=assert(ZGV.Navigation,"navigation module did not load")
Expand Down Expand Up @@ -109,6 +110,21 @@ route=assert(Navigation:FindRoute(nil,{key="Stormwind City/0",x=.50,y=.60,title=
assertEqual(route.path[1].mode,"astral","ready Astral Recall is preferred over Hearthstone")
assertEqual(route.path[1].label,"Stormwind","Astral Recall names the bound destination")

-- The spell API exposes the global cooldown through GetSpellCooldown. It is
-- not Astral Recall's actual cooldown and must not make the route graph churn
-- every time the player casts an unrelated spell.
GetSpellCooldown=function() return 99.5,1.5,1 end
local ports=Navigation:GetAvailableTravelPorts(ZGV.Data.routes)
local sawAstral=false
for _,port in ipairs(ports) do if port.mode=="astral" then sawAstral=true end end
assertEqual(sawAstral,true,"global cooldown does not remove Astral Recall from routing")
GetSpellCooldown=function() return 50,900,1 end
ports=Navigation:GetAvailableTravelPorts(ZGV.Data.routes)
sawAstral=false
for _,port in ipairs(ports) do if port.mode=="astral" then sawAstral=true end end
assertEqual(sawAstral,false,"real Astral Recall cooldown removes the travel option")
GetSpellCooldown=function() return 0,0,1 end

-- Regression from live testing: an active Nagrand goal in Zangarmarsh must
-- prefer the learned Garadar -> Zabra'jin flight over the two-border detour
-- through Blade's Edge Mountains.
Expand Down
Loading
Loading