Files

920 lines
32 KiB
Lua

QuestieOcto.Nodes = QuestieOcto.Nodes or {}
local N = QuestieOcto.Nodes
N.ready=false
N.running=false
N.generation=0
N.nodes={}
N.byMap={}
N.questMaps={}
N.nodeSequence=0
N.stateRevision=0
N.stats={
total=0,
availableCreature=0,
availableObject=0,
itemStart=0,
objectiveCreature=0,
objectiveObject=0,
objectiveItemSource=0,
objectiveArea=0,
turnin=0,
flightMaster=0,
auctioneer=0,
banker=0,
mailbox=0,
battlemaster=0,
innkeeper=0,
meetingStone=0,
repair=0,
spiritHealer=0,
stableMaster=0,
vendor=0,
classTrainer=0,
professionTrainer=0,
rareMob=0,
}
local function NewStats()
return {
total=0,
availableCreature=0,
availableObject=0,
itemStart=0,
objectiveCreature=0,
objectiveObject=0,
objectiveItemSource=0,
objectiveArea=0,
turnin=0,
flightMaster=0,
auctioneer=0,
banker=0,
mailbox=0,
battlemaster=0,
innkeeper=0,
meetingStone=0,
repair=0,
spiritHealer=0,
stableMaster=0,
vendor=0,
classTrainer=0,
professionTrainer=0,
rareMob=0,
}
end
local function ResetStats()
N.stats=NewStats()
end
local function CurrentStats()
return N.buildStats or N.stats
end
local function CurrentNodes()
return N.buildNodes or N.nodes
end
local function CurrentByMap()
return N.buildByMap or N.byMap
end
local function CurrentQuestMaps()
return N.buildQuestMaps or N.questMaps
end
local function ApplyIconScaleKey(node)
return node
end
local function IsPresentationEvent(q)
if not q or not q.eventID or not QuestieOcto.EventAvailability then return false end
if QuestieOcto.EventAvailability.IsPresentationEventForQuest then
return QuestieOcto.EventAvailability:IsPresentationEventForQuest(q) and true or false
end
return QuestieOcto.EventAvailability:IsPresentationEvent(q.eventID) and true or false
end
local function AddNode(node)
node=ApplyIconScaleKey(node)
CurrentStats().total=CurrentStats().total+1
N.nodeSequence=(N.nodeSequence or 0)+1
node.nodeID=N.nodeSequence
table.insert(CurrentNodes(),node)
local coords=node.coords
if coords then
-- A canonical node belongs to a map once, even if the source has dozens
-- of spawn coordinates on that map. Clustering handles those coordinates
-- later. 0.1.5 incorrectly inserted the same node once per coordinate,
-- causing hundreds of duplicate render work items.
local seenMaps={}
for _,coord in pairs(coords) do
if type(coord)=="table" and tonumber(coord[3]) then
local mapID=tonumber(coord[3])
if not seenMaps[mapID] then
seenMaps[mapID]=true
local byMap=CurrentByMap()
byMap[mapID]=byMap[mapID] or {}
table.insert(byMap[mapID],node)
if tonumber(node.questID) and tonumber(node.questID)>0 then
local questMaps=CurrentQuestMaps()
questMaps[node.questID]=questMaps[node.questID] or {}
questMaps[node.questID][mapID]=true
end
end
end
end
end
end
local function ApplyObjectiveState(node,state)
if state then
node.objectiveIndex=state.objectiveIndex
node.objectiveText=state.objectiveText
node.objectiveType=state.objectiveType
node.current=state.current
node.required=state.required
node.objectiveComplete=state.complete and true or false
end
return node
end
local function ConditionalCreatureCoords(q,creatureID,role)
local marker=q and q.conditionalMapMarker or nil
if marker and tonumber(marker.creatureID)==tonumber(creatureID)
and (role=="available" or role=="turnin") then
return marker.coords
end
return nil
end
local function AddCreatureNode(questID,role,creatureID,itemID,chance,objectiveState,vendor)
local q=QuestieOcto.QuestModel:Get(questID)
local node={
questID=questID,role=role,event=IsPresentationEvent(q),eventID=q and q.eventID or nil,pvp=q and q.pvp or false,repeatable=q and q.presentationRepeatable or false,sourceKind="creature",sourceID=creatureID,
sourceName=QuestieOcto.DatabaseAPI:GetCreatureName(creatureID),
sourceRank=QuestieOcto.DatabaseAPI:GetCreatureRank(creatureID),
respawnSeconds=QuestieOcto.DatabaseAPI:GetCreatureRespawnSeconds(creatureID),
itemID=itemID,itemName=itemID and QuestieOcto.DatabaseAPI:GetItemName(itemID) or nil,
chance=chance,vendor=vendor and true or false,coords=ConditionalCreatureCoords(q,creatureID,role) or QuestieOcto.DatabaseAPI:GetCreatureCoords(creatureID),
conditionalOffer=q and q.conditionalOffer or nil
}
AddNode(ApplyObjectiveState(node,objectiveState))
end
local function AddObjectNode(questID,role,objectID,itemID,chance,objectiveState)
local q=QuestieOcto.QuestModel:Get(questID)
local node={
questID=questID,role=role,event=IsPresentationEvent(q),eventID=q and q.eventID or nil,pvp=q and q.pvp or false,repeatable=q and q.presentationRepeatable or false,sourceKind="gameObject",sourceID=objectID,
sourceName=QuestieOcto.DatabaseAPI:GetObjectName(objectID),
itemID=itemID,itemName=itemID and QuestieOcto.DatabaseAPI:GetItemName(itemID) or nil,
chance=chance,coords=QuestieOcto.DatabaseAPI:GetObjectCoords(objectID)
}
AddNode(ApplyObjectiveState(node,objectiveState))
end
local function AddAreaTriggerNode(questID,source)
if not source or not source.mapID or not source.x or not source.y then return end
local q=QuestieOcto.QuestModel:Get(questID)
AddNode(ApplyObjectiveState({
questID=questID,role="objectiveArea",event=IsPresentationEvent(q),eventID=q and q.eventID or nil,pvp=q and q.pvp or false,repeatable=q and q.presentationRepeatable or false,
sourceKind="areaTrigger",sourceID=source.id,sourceName="Exploration Mark",
coords={{source.x,source.y,source.mapID}}
},source))
end
local function BuildAvailableQuestNodes(questID)
local q=QuestieOcto.QuestModel:Get(questID)
if not q then return end
if q.starts.creature then
for _,id in pairs(q.starts.creature) do
AddCreatureNode(questID,"available",id,nil,nil)
CurrentStats().availableCreature=CurrentStats().availableCreature+1
end
end
if q.starts.gameObject then
for _,id in pairs(q.starts.gameObject) do
AddObjectNode(questID,"available",id,nil,nil)
CurrentStats().availableObject=CurrentStats().availableObject+1
end
end
end
local function BuildItemStartQuestNodes(questID,resolved,availableSet)
-- ItemStarts is a derived cache; the published AvailableQuests snapshot is
-- the authoritative visibility gate. Never let an older item-start cache
-- bypass completion/event/repeatable filtering during an async refresh.
if not availableSet[questID] or not resolved then return end
for _,item in pairs(resolved.items or {}) do
for _,src in pairs(item.creatureSources or {}) do
AddCreatureNode(questID,"itemStart",src.id,item.itemID,src.chance,nil,src.vendor)
CurrentStats().itemStart=CurrentStats().itemStart+1
end
for _,src in pairs(item.objectSources or {}) do
AddObjectNode(questID,"itemStart",src.id,item.itemID,src.chance)
CurrentStats().itemStart=CurrentStats().itemStart+1
end
end
end
local function PlayerFactionCode()
local playerFaction=UnitFactionGroup and UnitFactionGroup("player") or nil
if playerFaction=="Alliance" then return "A" end
if playerFaction=="Horde" then return "H" end
return nil
end
local function FactionAllows(allowed,factionCode)
return type(allowed)=="string" and factionCode and string.find(allowed,factionCode,1,true) and true or false
end
local function TrackingMeta(metaKey)
return QuestieOcto.DatabaseAPI and QuestieOcto.DatabaseAPI.GetTrackingMeta and QuestieOcto.DatabaseAPI:GetTrackingMeta(metaKey) or nil
end
local function BuildServiceCreatureNodes(metaKey,role,statKey)
local list=TrackingMeta(metaKey)
if not list then return end
local factionCode=PlayerFactionCode()
if not factionCode then return end
for creatureID,allowed in pairs(list) do
creatureID=tonumber(creatureID)
if creatureID and creatureID>0 and FactionAllows(allowed,factionCode) then
local coords=QuestieOcto.DatabaseAPI:GetCreatureCoords(creatureID)
if coords and next(coords) then
AddNode({
questID=0,
role=role,
sourceKind="creature",
sourceID=creatureID,
sourceName=QuestieOcto.DatabaseAPI:GetCreatureName(creatureID),
coords=coords,
serviceFaction=allowed
})
CurrentStats()[statKey]=(CurrentStats()[statKey] or 0)+1
end
end
end
end
local function BuildServiceObjectNodes(metaKey,role,statKey)
local list=TrackingMeta(metaKey)
if not list then return end
local factionCode=PlayerFactionCode()
if not factionCode then return end
for signedObjectID,allowed in pairs(list) do
local objectID=math.abs(tonumber(signedObjectID) or 0)
if objectID>0 and FactionAllows(allowed,factionCode) then
local coords=QuestieOcto.DatabaseAPI:GetObjectCoords(objectID)
if coords and next(coords) then
AddNode({
questID=0,
role=role,
sourceKind="gameObject",
sourceID=objectID,
sourceName=QuestieOcto.DatabaseAPI:GetObjectName(objectID),
coords=coords,
serviceFaction=allowed
})
CurrentStats()[statKey]=(CurrentStats()[statKey] or 0)+1
end
end
end
end
local function BuildMailboxNodes()
BuildServiceObjectNodes("mailbox","mailbox","mailbox")
end
local optionalServiceSettings={
battlemaster={"showMapBattlemaster","showMinimapBattlemaster"},
innkeeper={"showMapInnkeeper","showMinimapInnkeeper"},
meetingStone={"showMapMeetingStone","showMinimapMeetingStone"},
repair={"showMapRepair","showMinimapRepair"},
spiritHealer={"showMapSpiritHealer","showMinimapSpiritHealer"},
stableMaster={"showMapStableMaster","showMinimapStableMaster"},
vendor={"showMapVendor","showMinimapVendor"},
classTrainer={"showMapClassTrainer","showMinimapClassTrainer"},
professionTrainer={"showMapProfessionTrainer","showMinimapProfessionTrainer"},
}
local function OptionalServiceEnabled(role)
local keys=optionalServiceSettings[role]
if not keys or not QuestieOcto.MinimapSettings then return true end
return QuestieOcto.MinimapSettings:Get(keys[1]) or QuestieOcto.MinimapSettings:Get(keys[2])
end
local function BuildOptionalServiceCreatureNodes(metaKey,role,statKey)
if OptionalServiceEnabled(role) then BuildServiceCreatureNodes(metaKey,role,statKey) end
end
local function BuildOptionalServiceObjectNodes(metaKey,role,statKey)
if OptionalServiceEnabled(role) then BuildServiceObjectNodes(metaKey,role,statKey) end
end
local function ProcessServiceCreatureSlice(metaKey,role,statKey,cursor,limit)
local list=TrackingMeta(metaKey)
if not list then return nil,true end
local factionCode=PlayerFactionCode()
if not factionCode then return nil,true end
local count=0
while count<(limit or 128) do
local rawID,allowed=next(list,cursor)
if rawID==nil then return nil,true end
cursor=rawID
local creatureID=tonumber(rawID)
if creatureID and creatureID>0 and FactionAllows(allowed,factionCode) then
local coords=QuestieOcto.DatabaseAPI:GetCreatureCoords(creatureID)
if coords and next(coords) then
AddNode({
questID=0,
role=role,
sourceKind="creature",
sourceID=creatureID,
sourceName=QuestieOcto.DatabaseAPI:GetCreatureName(creatureID),
coords=coords,
serviceFaction=allowed
})
CurrentStats()[statKey]=(CurrentStats()[statKey] or 0)+1
end
end
count=count+1
end
return cursor,false
end
local function ProcessServiceObjectSlice(metaKey,role,statKey,cursor,limit)
local list=TrackingMeta(metaKey)
if not list then return nil,true end
local factionCode=PlayerFactionCode()
if not factionCode then return nil,true end
local count=0
while count<(limit or 128) do
local rawID,allowed=next(list,cursor)
if rawID==nil then return nil,true end
cursor=rawID
local objectID=math.abs(tonumber(rawID) or 0)
if objectID>0 and FactionAllows(allowed,factionCode) then
local coords=QuestieOcto.DatabaseAPI:GetObjectCoords(objectID)
if coords and next(coords) then
AddNode({
questID=0,
role=role,
sourceKind="gameObject",
sourceID=objectID,
sourceName=QuestieOcto.DatabaseAPI:GetObjectName(objectID),
coords=coords,
serviceFaction=allowed
})
CurrentStats()[statKey]=(CurrentStats()[statKey] or 0)+1
end
end
count=count+1
end
return cursor,false
end
-- Trainers are stored two levels deep: meta["trainers"][class] and
-- meta["professions"][profession] each hold a flat {[creatureID]=faction} table.
-- We flatten that lazily so the incremental builder can pause between frames:
-- the cursor is {groupKey=<class/profession>, innerCursor=<creatureID>}. Each
-- emitted node is tagged with trainerKey=<group> so the renderer can pick the
-- right icon out of the ringed class/profession atlas.
local function ProcessTrainerSlice(metaKey,role,statKey,cursor,limit)
local root=TrackingMeta(metaKey)
if type(root)~="table" then return nil,true end
local factionCode=PlayerFactionCode()
if not factionCode then return nil,true end
cursor=cursor or {}
local groupKey=cursor.groupKey
local innerCursor=cursor.innerCursor
local count=0
while count<(limit or 128) do
if groupKey==nil then
groupKey=next(root,nil)
innerCursor=nil
if groupKey==nil then return nil,true end
end
local group=root[groupKey]
local rawID,allowed
if type(group)=="table" then rawID,allowed=next(group,innerCursor) end
if rawID==nil then
-- Group exhausted (or not a table): advance to the next group. Empty
-- profession tables fall straight through here without costing a slot.
groupKey=next(root,groupKey)
innerCursor=nil
if groupKey==nil then return nil,true end
else
innerCursor=rawID
local creatureID=tonumber(rawID)
if creatureID and creatureID>0 and FactionAllows(allowed,factionCode) then
local coords=QuestieOcto.DatabaseAPI:GetCreatureCoords(creatureID)
if coords and next(coords) then
AddNode({
questID=0,
role=role,
sourceKind="creature",
sourceID=creatureID,
sourceName=QuestieOcto.DatabaseAPI:GetCreatureName(creatureID),
coords=coords,
serviceFaction=allowed,
trainerKey=tostring(groupKey)
})
CurrentStats()[statKey]=(CurrentStats()[statKey] or 0)+1
end
end
count=count+1
end
end
return {groupKey=groupKey,innerCursor=innerCursor},false
end
local function ProcessRareMobSlice(cursor,limit)
local list=TrackingMeta("rares")
if not list then return nil,true end
local count=0
while count<(limit or 128) do
local rawID,rareLevel=next(list,cursor)
if rawID==nil then return nil,true end
cursor=rawID
local creatureID=tonumber(rawID)
if creatureID and creatureID>0 then
local coords=QuestieOcto.DatabaseAPI:GetCreatureCoords(creatureID)
if coords and next(coords) then
AddNode({
questID=0,
role="rareMob",
sourceKind="creature",
sourceID=creatureID,
sourceName=QuestieOcto.DatabaseAPI:GetCreatureName(creatureID),
sourceRank=QuestieOcto.DatabaseAPI:GetCreatureRank(creatureID),
respawnSeconds=QuestieOcto.DatabaseAPI:GetCreatureRespawnSeconds(creatureID),
rareLevel=tonumber(rareLevel),
coords=coords
})
CurrentStats().rareMob=CurrentStats().rareMob+1
end
end
count=count+1
end
return cursor,false
end
local function BuildRareMobNodes()
local list=TrackingMeta("rares")
if not list then return end
for creatureID,rareLevel in pairs(list) do
creatureID=tonumber(creatureID)
if creatureID and creatureID>0 then
local coords=QuestieOcto.DatabaseAPI:GetCreatureCoords(creatureID)
if coords and next(coords) then
AddNode({
questID=0,
role="rareMob",
sourceKind="creature",
sourceID=creatureID,
sourceName=QuestieOcto.DatabaseAPI:GetCreatureName(creatureID),
sourceRank=QuestieOcto.DatabaseAPI:GetCreatureRank(creatureID),
respawnSeconds=QuestieOcto.DatabaseAPI:GetCreatureRespawnSeconds(creatureID),
rareLevel=tonumber(rareLevel),
coords=coords
})
CurrentStats().rareMob=CurrentStats().rareMob+1
end
end
end
end
local function BuildPermanentMapNodes()
-- Questie 3.3.5/7/8 model Auctioneer, Banker and Flight Master as
-- townsfolk map categories. pfQuest supplies the Vanilla/Turtle-compatible
-- faction lists, mailbox object IDs, rare-mob list and spawn coordinates.
BuildServiceCreatureNodes("flight","flightMaster","flightMaster")
BuildServiceCreatureNodes("auctioneer","auctioneer","auctioneer")
BuildServiceCreatureNodes("banker","banker","banker")
BuildMailboxNodes()
BuildOptionalServiceCreatureNodes("battlemaster","battlemaster","battlemaster")
BuildOptionalServiceCreatureNodes("innkeeper","innkeeper","innkeeper")
BuildOptionalServiceObjectNodes("meetingstone","meetingStone","meetingStone")
BuildOptionalServiceCreatureNodes("repair","repair","repair")
BuildOptionalServiceCreatureNodes("spirithealer","spiritHealer","spiritHealer")
BuildOptionalServiceCreatureNodes("stablemaster","stableMaster","stableMaster")
BuildOptionalServiceCreatureNodes("vendor","vendor","vendor")
BuildRareMobNodes()
end
local function BuildActiveQuestNodes(questID)
local state=QuestieOcto.QuestLog.active[questID]
local q=QuestieOcto.QuestModel:Get(questID)
if not state or not q then return end
if state.complete then
for _,id in pairs(q.finishes.creature or {}) do
AddCreatureNode(questID,"turnin",id,nil,nil)
CurrentStats().turnin=CurrentStats().turnin+1
end
for _,id in pairs(q.finishes.gameObject or {}) do
AddObjectNode(questID,"turnin",id,nil,nil)
CurrentStats().turnin=CurrentStats().turnin+1
end
return
end
local resolved=QuestieOcto.Objectives.byQuest[questID]
if not resolved then return end
for _,src in pairs(resolved.creature or {}) do
if not src.complete then
AddCreatureNode(questID,"objectiveCreature",src.id,src.itemID,nil,src)
CurrentStats().objectiveCreature=CurrentStats().objectiveCreature+1
end
end
for _,src in pairs(resolved.gameObject or {}) do
if not src.complete then
AddObjectNode(questID,"objectiveObject",src.id,src.itemID,nil,src)
CurrentStats().objectiveObject=CurrentStats().objectiveObject+1
end
end
for _,item in pairs(resolved.item or {}) do
if not item.complete then
for _,src in pairs(item.sources or {}) do
if src.kind=="creature" then
AddCreatureNode(questID,"objectiveItemSource",src.id,item.itemID,src.chance,item,src.vendor)
else
AddObjectNode(questID,"objectiveItemSource",src.id,item.itemID,src.chance,item)
end
CurrentStats().objectiveItemSource=CurrentStats().objectiveItemSource+1
end
end
end
for _,src in pairs(resolved.areaTrigger or {}) do
if not src.complete then
AddAreaTriggerNode(questID,src)
CurrentStats().objectiveArea=(CurrentStats().objectiveArea or 0)+1
end
end
end
local function BuildActiveNodes()
for questID in pairs(QuestieOcto.QuestLog.active or {}) do
BuildActiveQuestNodes(questID)
end
end
local function StatKeyForNode(node)
if not node then return nil end
if node.role=="available" then
return node.sourceKind=="gameObject" and "availableObject" or "availableCreature"
end
if node.role=="itemStart" then return "itemStart" end
if node.role=="objectiveCreature" then return "objectiveCreature" end
if node.role=="objectiveObject" then return "objectiveObject" end
if node.role=="objectiveItemSource" then return "objectiveItemSource" end
if node.role=="objectiveArea" then return "objectiveArea" end
if node.role=="turnin" then return "turnin" end
if node.role=="flightMaster" then return "flightMaster" end
if node.role=="auctioneer" then return "auctioneer" end
if node.role=="banker" then return "banker" end
if node.role=="mailbox" then return "mailbox" end
if node.role=="battlemaster" then return "battlemaster" end
if node.role=="innkeeper" then return "innkeeper" end
if node.role=="meetingStone" then return "meetingStone" end
if node.role=="repair" then return "repair" end
if node.role=="spiritHealer" then return "spiritHealer" end
if node.role=="stableMaster" then return "stableMaster" end
if node.role=="vendor" then return "vendor" end
if node.role=="classTrainer" then return "classTrainer" end
if node.role=="professionTrainer" then return "professionTrainer" end
if node.role=="rareMob" then return "rareMob" end
return nil
end
local function DecrementNodeStats(node)
N.stats.total=math.max(0,(N.stats.total or 0)-1)
local key=StatKeyForNode(node)
if key then N.stats[key]=math.max(0,(N.stats[key] or 0)-1) end
end
local function NormalizeChangedQuests(changedQuests)
local changed={}
for questID in pairs(changedQuests or {}) do
questID=tonumber(questID)
if questID and questID>0 then changed[questID]=true end
end
return changed
end
local function RemoveChangedQuestNodes(changed)
local affectedMaps={}
-- Update global node/stat accounting once per canonical node. A canonical
-- source can belong to more than one map, so decrementing while filtering
-- per-map arrays would count the same node more than once.
local keptGlobal={}
for _,node in pairs(N.nodes or {}) do
if changed[tonumber(node.questID)] then
DecrementNodeStats(node)
else
table.insert(keptGlobal,node)
end
end
N.nodes=keptGlobal
-- Remove only changed quests from map indexes. Keep the old map IDs in the
-- affected set so a quest that disappears entirely can still remove its
-- prepared/map presentation from those zones.
for questID in pairs(changed) do
for mapID in pairs(N.questMaps[questID] or {}) do
affectedMaps[mapID]=true
local kept={}
for _,node in pairs(N.byMap[mapID] or {}) do
if tonumber(node.questID)~=questID then table.insert(kept,node) end
end
N.byMap[mapID]=kept
end
N.questMaps[questID]=nil
end
return affectedMaps
end
local function SortAffectedMaps(affectedMaps)
for mapID in pairs(affectedMaps or {}) do
local mapNodes=N.byMap[mapID]
if mapNodes then
table.sort(mapNodes,function(a,b)
if a.questID~=b.questID then return a.questID<b.questID end
if a.role~=b.role then return tostring(a.role)<tostring(b.role) end
if a.sourceKind~=b.sourceKind then return tostring(a.sourceKind)<tostring(b.sourceKind) end
return tonumber(a.sourceID or 0)<tonumber(b.sourceID or 0)
end)
end
end
end
function N:RefreshQuests(changedQuests)
if not self.ready or self.running or not QuestieOcto.Objectives.ready then
self:Rebuild()
return
end
local changed=NormalizeChangedQuests(changedQuests)
if not next(changed) then return end
local affectedMaps=RemoveChangedQuestNodes(changed)
-- Objective/progress refreshes re-add only the active semantic state.
for questID in pairs(changed) do
BuildActiveQuestNodes(questID)
for mapID in pairs(self.questMaps[questID] or {}) do affectedMaps[mapID]=true end
end
SortAffectedMaps(affectedMaps)
self.stateRevision=(self.stateRevision or 0)+1
QuestieOcto:SendMessage("NODES_CHANGED",affectedMaps,changed)
end
-- Availability filters used to rebuild the complete canonical node graph even
-- when only a handful of quests crossed the visible/hidden boundary. On dense
-- continent maps that forced every existing icon through another render pass
-- and looked like flicker. Patch just the changed quests instead.
function N:RefreshAvailability(changedQuests)
if not self.ready or self.running or not QuestieOcto.Objectives.ready
or not QuestieOcto.ItemStarts.ready then
self:Rebuild()
return
end
local changed=NormalizeChangedQuests(changedQuests)
if not next(changed) then return end
local affectedMaps=RemoveChangedQuestNodes(changed)
local availableSet=QuestieOcto.AvailableQuests.available or {}
local itemStarts=QuestieOcto.ItemStarts.byQuest or {}
for questID in pairs(changed) do
-- This normally covers available quests only, but rebuilding the current
-- semantic state makes the patch safe if another producer races the filter.
if QuestieOcto.QuestLog.active and QuestieOcto.QuestLog.active[questID] then
BuildActiveQuestNodes(questID)
elseif availableSet[questID] then
BuildAvailableQuestNodes(questID)
BuildItemStartQuestNodes(questID,itemStarts[questID],availableSet)
end
for mapID in pairs(self.questMaps[questID] or {}) do affectedMaps[mapID]=true end
end
SortAffectedMaps(affectedMaps)
self.stateRevision=(self.stateRevision or 0)+1
QuestieOcto:SendMessage("NODES_CHANGED",affectedMaps,changed)
end
function N:Rebuild()
if not QuestieOcto.AvailableQuests.ready
or not QuestieOcto.Objectives.ready
or not QuestieOcto.ItemStarts.ready
or not QuestieOcto.DatabaseAPI:IsReady()
or not QuestieOcto.QuestLog.snapshot then
self.ready=false
return
end
self.generation=self.generation+1
local generation=self.generation
-- Build into private buffers. The published node set stays live until the
-- replacement is complete, preventing map/minimap disappearance during an
-- asynchronous rebuild.
local hadReady=self.ready and true or false
if not hadReady then self.ready=false end
self.running=true
self.buildNodes={}
self.buildByMap={}
self.buildQuestMaps={}
self.buildStats=NewStats()
-- Capture the input snapshots. A newer publication triggers another rebuild
-- and increments generation, while this build can finish/cancel safely
-- without walking a table that changes underneath next().
local availableSet=QuestieOcto.AvailableQuests.available or {}
local itemStartSet=QuestieOcto.ItemStarts.byQuest or {}
local availableCursor=nil
local itemStartCursor=nil
local function Publish()
if generation~=N.generation then return end
N.nodes=N.buildNodes or {}
N.byMap=N.buildByMap or {}
N.questMaps=N.buildQuestMaps or {}
N.stats=N.buildStats or N.stats
N.buildNodes=nil
N.buildByMap=nil
N.buildQuestMaps=nil
N.buildStats=nil
N.running=false
N.ready=true
N.stateRevision=(N.stateRevision or 0)+1
QuestieOcto:SendMessage("NODES_READY")
end
local function SortMaps()
if generation~=N.generation then return end
local mapIDs={}
for mapID in pairs(N.buildByMap or {}) do table.insert(mapIDs,mapID) end
table.sort(mapIDs)
local pos=1
local function SortStep()
if generation~=N.generation then return end
local count=0
while pos<=table.getn(mapIDs) and count<4 do
local mapNodes=N.buildByMap[mapIDs[pos]]
pos=pos+1
if mapNodes then
table.sort(mapNodes,function(a,b)
if a.questID~=b.questID then return a.questID<b.questID end
if a.role~=b.role then return tostring(a.role)<tostring(b.role) end
if a.sourceKind~=b.sourceKind then return tostring(a.sourceKind)<tostring(b.sourceKind) end
return tonumber(a.sourceID or 0)<tonumber(b.sourceID or 0)
end)
end
count=count+1
end
if pos<=table.getn(mapIDs) then
QuestieOcto.Scheduler:Enqueue(SortStep,"nodes-sort")
else
Publish()
end
end
QuestieOcto.Scheduler:Enqueue(SortStep,"nodes-sort")
end
-- Keep service tracking incremental too. Vendor/repair metadata is large in
-- pfQuest, and enabling an opt-in category must not turn into one long frame.
local permanentBuilders={
{kind="creature",metaKey="flight",role="flightMaster",statKey="flightMaster"},
{kind="creature",metaKey="auctioneer",role="auctioneer",statKey="auctioneer"},
{kind="creature",metaKey="banker",role="banker",statKey="banker"},
{kind="object",metaKey="mailbox",role="mailbox",statKey="mailbox"},
{kind="creature",metaKey="battlemaster",role="battlemaster",statKey="battlemaster",optional=true},
{kind="creature",metaKey="innkeeper",role="innkeeper",statKey="innkeeper",optional=true},
{kind="object",metaKey="meetingstone",role="meetingStone",statKey="meetingStone",optional=true},
{kind="creature",metaKey="repair",role="repair",statKey="repair",optional=true},
{kind="creature",metaKey="spirithealer",role="spiritHealer",statKey="spiritHealer",optional=true},
{kind="creature",metaKey="stablemaster",role="stableMaster",statKey="stableMaster",optional=true},
{kind="creature",metaKey="vendor",role="vendor",statKey="vendor",optional=true},
{kind="trainer",metaKey="trainers",role="classTrainer",statKey="classTrainer",optional=true},
{kind="trainer",metaKey="professions",role="professionTrainer",statKey="professionTrainer",optional=true},
{kind="rare"},
}
local function PermanentStep(index,cursor)
if generation~=N.generation then return end
local job=permanentBuilders[index]
if not job then SortMaps(); return end
if job.optional and not OptionalServiceEnabled(job.role) then
QuestieOcto.Scheduler:Enqueue(function() PermanentStep(index+1,nil) end,"nodes-permanent")
return
end
local nextCursor=nil
local done=true
if job.kind=="creature" then
nextCursor,done=ProcessServiceCreatureSlice(job.metaKey,job.role,job.statKey,cursor,128)
elseif job.kind=="object" then
nextCursor,done=ProcessServiceObjectSlice(job.metaKey,job.role,job.statKey,cursor,128)
elseif job.kind=="trainer" then
nextCursor,done=ProcessTrainerSlice(job.metaKey,job.role,job.statKey,cursor,128)
elseif job.kind=="rare" then
nextCursor,done=ProcessRareMobSlice(cursor,128)
end
if done then
QuestieOcto.Scheduler:Enqueue(function() PermanentStep(index+1,nil) end,"nodes-permanent")
else
QuestieOcto.Scheduler:Enqueue(function() PermanentStep(index,nextCursor) end,"nodes-permanent")
end
end
local function ItemStartStep()
if generation~=N.generation then return end
local count=0
while count<24 do
local questID,resolved=next(itemStartSet,itemStartCursor)
if questID==nil then
PermanentStep(1)
return
end
itemStartCursor=questID
BuildItemStartQuestNodes(questID,resolved,availableSet)
count=count+1
end
QuestieOcto.Scheduler:Enqueue(ItemStartStep,"nodes-itemstart")
end
local function AvailableStep()
if generation~=N.generation then return end
local count=0
while count<32 do
local questID=next(availableSet,availableCursor)
if questID==nil then
QuestieOcto.Scheduler:Enqueue(ItemStartStep,"nodes-itemstart")
return
end
availableCursor=questID
BuildAvailableQuestNodes(questID)
count=count+1
end
QuestieOcto.Scheduler:Enqueue(AvailableStep,"nodes-available")
end
QuestieOcto.Scheduler:Enqueue(function()
if generation~=N.generation then return end
BuildActiveNodes()
QuestieOcto.Scheduler:Enqueue(AvailableStep,"nodes-available")
end,"nodes-active")
end
function N:GetMapNodes(mapID)
return self.byMap[tonumber(mapID)] or {}
end
function N:OnInputReady()
self:Rebuild()
end
function N:OnObjectivesChanged(changedQuests)
self:RefreshQuests(changedQuests)
end
function N:OnItemStartsChanged(changedQuests)
self:RefreshAvailability(changedQuests)
end
QuestieOcto:RegisterMessage("OBJECTIVES_READY",N,"OnInputReady")
QuestieOcto:RegisterMessage("OBJECTIVES_CHANGED",N,"OnObjectivesChanged")
QuestieOcto:RegisterMessage("ITEM_STARTS_READY",N,"OnInputReady")
QuestieOcto:RegisterMessage("ITEM_STARTS_CHANGED",N,"OnItemStartsChanged")