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
12 changes: 7 additions & 5 deletions ZygorGuidesViewer/ZygorGuidesViewer/Compat/Taxi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ function Taxi:GetTaxis()
return paths
end
Taxi.GetTaxisEnglish = Taxi.GetTaxis
function Taxi:Startup(saved)
function Taxi:Startup(saved, options)
if type(saved) ~= "table" then return false end
for name, value in pairs(saved) do
if type(name) == "string" then self:RememberKnownName(name) end
Expand All @@ -221,10 +221,12 @@ function Taxi:Startup(saved)
-- started. Notify Navigation now that the learned-node set is usable;
-- otherwise an already selected guide keeps the route calculated before
-- this cache existed until the next /reload or manual waypoint change.
Compat:Fire("TAXI_CACHE_UPDATED", {
nodes = {}, byKey = {}, updatedAt = Compat.Now(), available = true,
restored = true, revision = self.revision,
})
if not (type(options) == "table" and options.silent) then
Compat:Fire("TAXI_CACHE_UPDATED", {
nodes = {}, byKey = {}, updatedAt = Compat.Now(), available = true,
restored = true, revision = self.revision,
})
end
return true
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 = "20260716-hearth-bind-awareness-01"
ZGV.buildRevision = "20260717-flight-arrival-loop-01"
ZGV.interface = 30300
ZGV.targetBuild = 12340
ZGV.DIR = "Interface\\AddOns\\ZygorGuidesViewer"
Expand Down
4 changes: 3 additions & 1 deletion ZygorGuidesViewer/ZygorGuidesViewer/ModernActionBar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ function ActionBar:Create()
-- viewer update an unprotected guide action after the hardware click.
button:SetScript("PostClick",function(self)
local action=self.action
if action and not self.secure then
if action and action.kind=="item" and action.entry then
ZGV.Runtime:RecordItemUse(action.identity,"zygor-action-bar")
elseif action and not self.secure then
if action.kind=="trash" and ZGV.Inventory then ZGV.Inventory:SellGreys()
elseif action.entry then ZGV.Runtime:ActivateGoal(action.entry.stepIndex,action.entry.goalIndex) end
end
Expand Down
41 changes: 39 additions & 2 deletions ZygorGuidesViewer/ZygorGuidesViewer/Navigation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,21 @@ function Navigation:GetTransportArrivalIndex(player)
for index=self.routeIndex or 1,#self.route.path do
local entry=self.route.path[index]
if entry and entry.node and entry.node.mapKey==player.key and transportModes[entry.mode] then
return index
-- A same-zone flight has the same map key before and after boarding.
-- Treating its destination as an arrival while the player is still at
-- the departure flight master forces FindRoute on every arrow update.
-- Only a transport that crosses from a different preceding map can be
-- completed solely from the current map identity; same-zone legs use
-- their normal destination-arrival check after the flight lands.
local previousMap=self.route.from
for previous=index-1,self.routeIndex or 1,-1 do
local prior=self.route.path[previous]
if prior and prior.node and prior.node.mapKey then
previousMap=prior.node.mapKey
break
end
end
if previousMap and previousMap~=entry.node.mapKey then return index end
end
end
return nil
Expand Down Expand Up @@ -1046,7 +1060,22 @@ function Navigation:FindRoute(from,to)
-- are graph-only and deliberately omitted from the player-facing route.
local taxiByMap,taxiSources={},{}
local taxi=ZGV.Compat and ZGV.Compat.Taxi
if player.key~=to.key and navigationOption("useTaxi") and taxi and type(taxi.GetKnownStaticNodes)=="function" then
-- Code-TBC's InitialFlightPaths facade normally restores this cache at
-- ZGV_STARTED. Navigation must not rely on that optional facade, though:
-- a route can be rebuilt during login, after a module reload, or by another
-- consumer before that callback runs. Bind the profile-owned table here so
-- every guide gets the character's learned flight network. This repair is
-- intentionally silent because FindRoute may be running from the taxi-cache
-- notification itself.
local savedTaxi=ZGV.db and ZGV.db.profile and ZGV.db.profile.navigation and ZGV.db.profile.navigation.knownTaxi
if taxi and type(savedTaxi)=="table" and taxi.saved~=savedTaxi and type(taxi.Startup)=="function" then
taxi:Startup(savedTaxi,{silent=true})
end
-- Flight masters can also save time inside one large zone. Keep the
-- ordinary same-map START→FINISH edge below, then let Dijkstra compare it
-- with the learned flight network instead of excluding taxis solely because
-- the two coordinates share a map key.
if navigationOption("useTaxi") and taxi and type(taxi.GetKnownStaticNodes)=="function" then
local function taxiContinent(mapKey)
local record=ZGV.Compat.Map:Resolve(mapKey)
local legacy=record and (record.legacy or record)
Expand Down Expand Up @@ -1197,6 +1226,14 @@ function Navigation:OnStartup()
hooksecurefunc("WorldMapFrame_Update",function() Navigation:UpdateMapPin() end)
end
if ZGV.Compat and ZGV.Compat.On then ZGV.Compat:On("TAXI_CACHE_UPDATED",self,"OnTaxiCache") end
-- Bind after subscribing so an active restored guide is immediately
-- replanned with its learned flight points, even when the legacy facade did
-- not run (for example after a partial addon reload).
local taxi=ZGV.Compat and ZGV.Compat.Taxi
local savedTaxi=ZGV.db and ZGV.db.profile and ZGV.db.profile.navigation and ZGV.db.profile.navigation.knownTaxi
if taxi and type(savedTaxi)=="table" and taxi.saved~=savedTaxi and type(taxi.Startup)=="function" then
taxi:Startup(savedTaxi)
end
if ZGV.Compat and ZGV.Compat.Timer then
self.ticker=ZGV.Compat.Timer:NewTicker(.1,function()
if Navigation.waypoint then
Expand Down
31 changes: 31 additions & 0 deletions ZygorGuidesViewer/ZygorGuidesViewer/Parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,37 @@ function Parser:_ParseGuide(guide, stack)
goal.npcID=goal.npcID or lastGoal.npcID
goal.destination=goal.destination or lastGoal.destination
end
-- Imported guides frequently describe the NPC interaction as a
-- bare `talk` row, then put the coordinate on the immediately
-- following accept/turn-in row. They are one interaction at one
-- NPC, not two navigation stops. Give the talk (and any intervening
-- gossip choice) that same destination so navigation continues from
-- path points to the instruction the player is actually reading.
-- An explicit hand-in coordinate is authoritative; otherwise an
-- explicitly located talk row supplies the shared location.
if (goal.action=="accept" or goal.action=="turnin") and current then
local interactionGoals={}
local index=#current.goals
while index>=1 do
local previous=current.goals[index]
if previous.action=="gossip" then
interactionGoals[#interactionGoals+1]=previous
elseif previous.action=="talk" and not previous.questID then
interactionGoals[#interactionGoals+1]=previous
local destination=goal.destination or previous.destination
if destination then
goal.destination=destination
for _,interactionGoal in ipairs(interactionGoals) do
interactionGoal.destination=destination
end
end
break
else
break
end
index=index-1
end
end
if hadAsterisk then goal.showInBrief=true; goal.showinbrief=true end
if #indent>0 then goal.indent=#indent end
addGoal(goal,mods)
Expand Down
121 changes: 115 additions & 6 deletions ZygorGuidesViewer/ZygorGuidesViewer/Runtime.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ local function itemCount(id)
return ZGV.Conditions and ZGV.Conditions:ItemCount(id,true) or 0
end

local function isItemUseGoal(goal)
return goal and goal.itemID and (goal.action=="use" or (goal.modifiers and goal.modifiers.useItem))
end

-- A reusable quest item often sits beside the one quest objective that
-- counts its repeated uses (for example, curse several buildings with one
-- fetish). GetStepState already joins those companion actions to that
-- objective once it is finished. Do not manually complete the item on its
-- first click or the secure action button disappears before the objective is
-- done. A direct item-use goal without that quest-progress contract still
-- receives immediate credit.
function Runtime:ItemUseTracksQuestProgress(step,goal)
if goal and goal.questID and goal.objective then return true end
local objectives=0
for _,candidate in ipairs((step and step.goals) or {}) do
if candidate.questID and candidate.objective then
objectives=objectives+1
if objectives>1 then return false end
end
end
return objectives==1
end

local function questObjective(goal)
if not goal or not goal.questID then return nil end
local entry=questEntry(goal.questID)
Expand Down Expand Up @@ -721,6 +744,73 @@ function Runtime:ResetCurrentGuide()
self:SetStep(1,true)
end

-- A secure item button is deliberately owned by Blizzard's protected click
-- handler, so its successful use never passes through ActivateGoal. Record
-- the interaction here instead, regardless of whether it came from a bag,
-- the normal action bar, or Zygor's secure action bar. Matching by item ID
-- keeps this scoped to the active guide's authored `use` instructions.
function Runtime:RecordItemUse(itemID,source)
itemID=tonumber(itemID)
local guide=self.currentGuide
if not itemID or not guide then return false end
local changed,matched=false,false
for _,entry in ipairs(self:GetActiveSteps()) do
for goalIndex,goal in ipairs(entry.step.goals) do
if isItemUseGoal(goal) and tonumber(goal.itemID)==itemID and self:IsGoalApplicable(goal) then
matched=true
if not self:ItemUseTracksQuestProgress(entry.step,goal) then
local key=self:ManualKey(entry.index,goalIndex)
if not self.manual[key] then
self.manual[key]={source=source or "item-use",itemID=itemID,time=GetTime()}
changed=true
end
end
end
end
end
if not changed then return matched end
self.autoAdvanceBlocked=nil
ZGV:LogInfo("progress","item-use credit for item "..tostring(itemID).." via "..tostring(source or "unknown"))
ZGV:Fire("ZGV_GOAL_UPDATED",guide,self.currentStep)
local step=guide.steps[self.currentStep]
if step and self:GetStepState(step,self.currentStep).complete then
self:NextStep(true)
else
self:UpdateWaypoint()
end
return true
end

-- ITEM_LOCKED is raised while a bag item's pre-use identity is still
-- available. Keep it briefly because a consumable can disappear before the
-- post-use hook below gets to inspect its slot.
function Runtime:RememberItemLock(bag,slot)
if bag==nil or slot==nil then return end
local container=ZGV.Compat and ZGV.Compat.Container
local itemID=container and container.GetItemID and container:GetItemID(bag,slot)
if not itemID then return end
self.itemLocks=self.itemLocks or {}
self.itemLocks[tostring(bag)..":"..tostring(slot)]={itemID=tonumber(itemID),time=GetTime()}
end

function Runtime:RecordContainerItemUse(bag,slot)
if bag==nil or slot==nil then return false end
local key=tostring(bag)..":"..tostring(slot)
local container=ZGV.Compat and ZGV.Compat.Container
local itemID=container and container.GetItemID and container:GetItemID(bag,slot)
local locked=self.itemLocks and self.itemLocks[key]
if self.itemLocks then self.itemLocks[key]=nil end
if not itemID and locked and GetTime()-locked.time<=5 then itemID=locked.itemID end
return self:RecordItemUse(itemID,"bag")
end

function Runtime:RecordActionUse(slot)
if type(GetActionInfo)~="function" then return false end
local action,itemID=GetActionInfo(slot)
if action~="item" then return false end
return self:RecordItemUse(itemID,"action-bar")
end

function Runtime:ActivateGoal(stepIndex,goalIndex)
local guide=self.currentGuide
local step=guide and guide.steps[stepIndex]
Expand All @@ -733,9 +823,12 @@ function Runtime:ActivateGoal(stepIndex,goalIndex)
local destination=self:ResolveStepJump(guide,stepIndex,goal.nextJump or goal.nextLabel)
if destination then return self:SetStep(destination,true) end
end
if goal.itemID and (goal.action=="use" or goal.modifiers.useItem) and ZGV.Inventory then
if isItemUseGoal(goal) and ZGV.Inventory then
local used=ZGV.Inventory:UseItem(goal.itemID)
if used then return true end
-- The bag-use hook normally records this before Inventory:UseItem
-- returns. Calling the matcher again also covers clients where that
-- event is delayed, and is harmless after the first credit.
if used then return self:RecordItemUse(goal.itemID,"guide-action") or true end
end
self.manual[self:ManualKey(stepIndex,goalIndex)]=true
ZGV:Fire("ZGV_GOAL_UPDATED",guide,stepIndex,goalIndex)
Expand Down Expand Up @@ -897,7 +990,6 @@ function Runtime:RecordTalk(event)
-- opening. Mouseover is a useful fallback for click-to-interact users.
local npcID=npcIDFromGUID(UnitGUID and UnitGUID("target"))
or npcIDFromGUID(UnitGUID and UnitGUID("mouseover"))
if not npcID then return end
local changed=false
for _,entry in ipairs(self:GetActiveSteps()) do
for goalIndex,goal in ipairs(entry.step.goals) do
Expand All @@ -908,7 +1000,14 @@ function Runtime:RecordTalk(event)
changed=true
end
end
if (goal.action=="vendor" or goal.action=="trainer") and tonumber(goal.npcID)==npcID and self:IsGoalApplicable(goal) then
-- TRAINER_SHOW is emitted only after the player opens a trainer window.
-- Build 12340 can clear the selected NPC before that event reaches an
-- addon, which made class-training steps depend on an unavailable GUID.
-- The active guide's trainer instruction is enough context to credit
-- the visit; vendor interactions still require an exact NPC match.
local trainerVisit=goal.action=="trainer" and event=="TRAINER_SHOW"
local matchedNpc=npcID and tonumber(goal.npcID)==npcID
if (trainerVisit or (goal.action=="vendor" and matchedNpc)) and self:IsGoalApplicable(goal) then
local key=self:ManualKey(entry.index,goalIndex)
if not self.interacted[key] then
self.interacted[key]={npcID=npcID,event=event,time=time()}
Expand All @@ -919,7 +1018,7 @@ function Runtime:RecordTalk(event)
end
if changed then
self.autoAdvanceBlocked=nil
ZGV:LogInfo("progress","talk credit for npc "..tostring(npcID).." via "..tostring(event))
ZGV:LogInfo("progress","talk credit for npc "..tostring(npcID or "trainer").." via "..tostring(event))
ZGV:Fire("ZGV_GOAL_UPDATED",guide,self.currentStep)
end
end
Expand Down Expand Up @@ -1072,6 +1171,7 @@ function Runtime:OnEvent(event,...)
if not self.currentGuide then return end
local unit=...
if event=="COMBAT_LOG_EVENT_UNFILTERED" then self:RecordKillFromCombatLog(...) end
if event=="ITEM_LOCKED" then self:RememberItemLock(...) end
if event=="GOSSIP_SHOW" or event=="QUEST_GREETING" or event=="QUEST_DETAIL"
or event=="QUEST_PROGRESS" or event=="QUEST_COMPLETE" or event=="MERCHANT_SHOW"
or event=="TRAINER_SHOW" or event=="TAXIMAP_OPENED" then
Expand Down Expand Up @@ -1116,6 +1216,15 @@ function Runtime:OnStartup()
hooksecurefunc("GetQuestReward",function() Runtime:RecordTurnIn(nil,nil,"GetQuestReward") end)
self.rewardHooked=true
end
if not self.itemUseHooked and type(hooksecurefunc)=="function" then
if type(UseContainerItem)=="function" then
hooksecurefunc("UseContainerItem",function(bag,slot) Runtime:RecordContainerItemUse(bag,slot) end)
end
if type(UseAction)=="function" then
hooksecurefunc("UseAction",function(slot) Runtime:RecordActionUse(slot) end)
end
self.itemUseHooked=true
end
local quest=ZGV.Compat and ZGV.Compat.Quest
if quest then quest:RefreshLog() quest:RefreshCompleted(false) end
local saved=ZGV.db.profile.currentGuide
Expand All @@ -1132,7 +1241,7 @@ function Runtime:OnStartup()
end
end

for _,event in ipairs({"QUEST_LOG_UPDATE","BAG_UPDATE","PLAYER_LEVEL_UP","UNIT_INVENTORY_CHANGED","ACHIEVEMENT_EARNED","UNIT_AURA","PLAYER_DEAD","PLAYER_UNGHOST","COMBAT_LOG_EVENT_UNFILTERED","GOSSIP_SHOW","QUEST_GREETING","QUEST_DETAIL","QUEST_PROGRESS","QUEST_COMPLETE","QUEST_FINISHED","MERCHANT_SHOW","TRAINER_SHOW","TAXIMAP_OPENED","UI_INFO_MESSAGE","SKILL_LINES_CHANGED","TRADE_SKILL_UPDATE"}) do
for _,event in ipairs({"QUEST_LOG_UPDATE","BAG_UPDATE","ITEM_LOCKED","PLAYER_LEVEL_UP","UNIT_INVENTORY_CHANGED","ACHIEVEMENT_EARNED","UNIT_AURA","PLAYER_DEAD","PLAYER_UNGHOST","COMBAT_LOG_EVENT_UNFILTERED","GOSSIP_SHOW","QUEST_GREETING","QUEST_DETAIL","QUEST_PROGRESS","QUEST_COMPLETE","QUEST_FINISHED","MERCHANT_SHOW","TRAINER_SHOW","TAXIMAP_OPENED","UI_INFO_MESSAGE","SKILL_LINES_CHANGED","TRADE_SKILL_UPDATE"}) do
ZGV:RegisterEvent(event,Runtime,"OnEvent")
end

Expand Down
Loading
Loading