mirror of
https://github.com/Blaststar/Questie-Octo-Vanilla.git
synced 2026-10-19 20:00:49 +00:00
920 lines
32 KiB
Lua
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")
|