mirror of
https://github.com/Blaststar/Questie-Octo-Vanilla.git
synced 2026-10-20 20:30:49 +00:00
660 lines
22 KiB
Lua
660 lines
22 KiB
Lua
QuestieOcto.PreparedMap = QuestieOcto.PreparedMap or {}
|
|
local P = QuestieOcto.PreparedMap
|
|
|
|
P.cache={}
|
|
P.readyMaps={}
|
|
P.cacheRevision={}
|
|
P.worldItemStartCache={}
|
|
P.worldItemStartRevision={}
|
|
P.cacheDensity={}
|
|
P.stateRevision=1
|
|
P.running=false
|
|
P.generation=0
|
|
P.dirtyGeneration=0
|
|
P.stats={preparedMaps=0,descriptors=0,currentMap=nil,currentReady=false,stateRevision=1,revisionBumps=0,densitySignature="unknown"}
|
|
|
|
local function CurrentDensitySignature()
|
|
local settings=QuestieOcto.MinimapSettings
|
|
if not settings or not settings.Get then return "clustered:clustered" end
|
|
return tostring(settings:Get("objectiveNodeDensity") or "clustered")..":"..
|
|
tostring(settings:Get("itemStartDensity") or "clustered")
|
|
end
|
|
|
|
function P:GetDensitySignature(mapID)
|
|
mapID=tonumber(mapID)
|
|
if not mapID then return nil end
|
|
return self.cacheDensity[mapID]
|
|
end
|
|
|
|
function P:GetCurrentDensitySignature()
|
|
return CurrentDensitySignature()
|
|
end
|
|
|
|
local function ExactRole(role)
|
|
return role=="available" or role=="turnin" or role=="objectiveArea" or role=="flightMaster"
|
|
or role=="auctioneer" or role=="banker" or role=="mailbox"
|
|
or role=="battlemaster" or role=="innkeeper" or role=="meetingStone"
|
|
or role=="repair" or role=="spiritHealer" or role=="stableMaster" or role=="vendor"
|
|
or role=="classTrainer" or role=="professionTrainer"
|
|
or role=="rareMob"
|
|
end
|
|
|
|
local function DescriptorKey(node,x,y)
|
|
-- Questie 5/6/3.3.5 places available and complete frames on the same source
|
|
-- coordinate, with the complete texture one draw level above available.
|
|
-- pfQuest likewise resolves coincident quest nodes by visual layer. Collapse
|
|
-- those two semantic entries into one prepared pin so the tooltip retains
|
|
-- both quests while Questie-Octo's turn-in priority selects the complete icon.
|
|
if node.role=="available" or node.role=="turnin" then
|
|
return "exact:quest-source:"..tostring(node.sourceKind)..":"..tostring(node.sourceID)..":"..
|
|
string.format("%.2f",x)..":"..string.format("%.2f",y)
|
|
end
|
|
|
|
return "exact:"..tostring(node.questID)..":"..tostring(node.role)..":"..
|
|
tostring(node.sourceKind)..":"..tostring(node.sourceID)..":"..
|
|
string.format("%.2f",x)..":"..string.format("%.2f",y)
|
|
end
|
|
|
|
local function AreaKey(node,area)
|
|
return "area:"..tostring(node.sourceKind)..":"..tostring(node.sourceID)..":"..
|
|
tostring(area.key)
|
|
end
|
|
|
|
local function FullPointKey(prefix,node,x,y)
|
|
-- Full Nodes means every UNIQUE spawn coordinate. Multiple active quests or
|
|
-- item objectives can reference the exact same spawn; pfQuest stores those
|
|
-- references in one coordinate slot and combines their tooltip metadata.
|
|
-- Keep item-start and objective namespaces compatible by sharing the same
|
|
-- quest-coordinate key so one physical spawn never allocates duplicate pins.
|
|
return "full:quest:"..string.format("%.2f",x)..":"..string.format("%.2f",y)
|
|
end
|
|
|
|
local function AddNormal(plan,slots,node,x,y,clusterCount,kind,key)
|
|
local slot=slots[key]
|
|
if not slot then
|
|
slot={
|
|
type="nodeSlot",
|
|
x=x,
|
|
y=y,
|
|
key=key,
|
|
coordKey=string.format("%.2f:%.2f",x,y),
|
|
entries={}
|
|
}
|
|
slots[key]=slot
|
|
table.insert(plan,slot)
|
|
end
|
|
|
|
table.insert(slot.entries,{
|
|
node=node,
|
|
clusterCount=clusterCount or 1,
|
|
kind=kind
|
|
})
|
|
end
|
|
|
|
function P:BuildPlanFromNodes(mapID,nodes)
|
|
mapID=tonumber(mapID)
|
|
if not mapID then return nil end
|
|
|
|
local plan={}
|
|
local slots={}
|
|
|
|
for _,node in pairs(nodes or {}) do
|
|
if node.role~="itemStart" then
|
|
if ExactRole(node.role) then
|
|
local points=QuestieOcto.Clustering:PointsForNodeOnMap(node,mapID)
|
|
|
|
for _,point in pairs(points) do
|
|
AddNormal(
|
|
plan,slots,node,point.x,point.y,1,"exact",
|
|
DescriptorKey(node,point.x,point.y)
|
|
)
|
|
end
|
|
else
|
|
local points=QuestieOcto.Clustering:PointsForNodeOnMap(node,mapID)
|
|
|
|
if QuestieOcto.MinimapSettings:Get("objectiveNodeDensity")=="full" then
|
|
for _,point in pairs(points) do
|
|
AddNormal(
|
|
plan,slots,node,point.x,point.y,1,"objectiveFull",
|
|
FullPointKey("objective-full",node,point.x,point.y)
|
|
)
|
|
end
|
|
else
|
|
local areas=QuestieOcto.Clustering:BuildAreas(
|
|
points,QuestieOcto.Clustering.objectiveRadius
|
|
)
|
|
|
|
for _,area in pairs(areas) do
|
|
AddNormal(plan,slots,node,area.x,area.y,area.n,"objective",AreaKey(node,area))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if QuestieOcto.MinimapSettings:Get("itemStartDensity")=="full" then
|
|
for _,node in pairs(nodes or {}) do
|
|
if node.role=="itemStart" then
|
|
local points=QuestieOcto.Clustering:PointsForNodeOnMap(node,mapID)
|
|
for _,point in pairs(points) do
|
|
AddNormal(
|
|
plan,slots,node,point.x,point.y,1,"itemStartFull",
|
|
FullPointKey("itemstart-full",node,point.x,point.y)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
local itemAreas=QuestieOcto.ItemStartAreas:BuildForMap(nodes or {},mapID)
|
|
for _,area in pairs(itemAreas) do
|
|
table.insert(plan,{
|
|
type="itemStartArea",
|
|
area=area,
|
|
key="itemarea:"..tostring(area.key)
|
|
})
|
|
end
|
|
end
|
|
|
|
table.sort(plan,function(a,b)
|
|
return tostring(a.key)<tostring(b.key)
|
|
end)
|
|
|
|
return plan
|
|
end
|
|
|
|
|
|
|
|
local function IsWorldMapUltraRareItemStart(node)
|
|
return node and node.role=="itemStart" and QuestieOcto.ItemStartAreas
|
|
and QuestieOcto.ItemStartAreas:IsZoneWideRareChance(node.chance) and true or false
|
|
end
|
|
|
|
function P:BuildWorldItemStartPlanFromNodes(mapID,nodes)
|
|
mapID=tonumber(mapID)
|
|
if not mapID then return nil end
|
|
|
|
local plan={}
|
|
local slots={}
|
|
local density=QuestieOcto.MinimapSettings:Get("itemStartDensity")
|
|
|
|
if density=="full" then
|
|
for _,node in pairs(nodes or {}) do
|
|
if node.role=="itemStart" and not IsWorldMapUltraRareItemStart(node) then
|
|
local points=QuestieOcto.Clustering:PointsForNodeOnMap(node,mapID)
|
|
for _,point in pairs(points) do
|
|
AddNormal(
|
|
plan,slots,node,point.x,point.y,1,"itemStartFull",
|
|
FullPointKey("itemstart-full",node,point.x,point.y)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
local normalAreas=QuestieOcto.ItemStartAreas:BuildForMap(
|
|
nodes or {},mapID,
|
|
function(node) return not IsWorldMapUltraRareItemStart(node) end
|
|
)
|
|
for _,area in pairs(normalAreas) do
|
|
table.insert(plan,{
|
|
type="itemStartArea",
|
|
area=area,
|
|
key="itemarea:"..tostring(area.key)
|
|
})
|
|
end
|
|
end
|
|
|
|
local rareAreas=QuestieOcto.ItemStartAreas:BuildZoneWideRareForMap(nodes or {},mapID)
|
|
for _,area in pairs(rareAreas) do
|
|
table.insert(plan,{
|
|
type="itemStartArea",
|
|
area=area,
|
|
key="itemrarearea:"..tostring(area.key)
|
|
})
|
|
end
|
|
|
|
table.sort(plan,function(a,b) return tostring(a.key)<tostring(b.key) end)
|
|
return plan
|
|
end
|
|
|
|
function P:SetPreparedMap(mapID,plan,worldItemStartPlan,densitySignature)
|
|
mapID=tonumber(mapID)
|
|
if not mapID or not plan then return nil end
|
|
|
|
local wasReady=self.readyMaps[mapID] and true or false
|
|
local old=self.cache[mapID]
|
|
local oldCount=old and table.getn(old) or 0
|
|
|
|
self.cache[mapID]=plan
|
|
self.readyMaps[mapID]=true
|
|
self.cacheRevision[mapID]=self.stateRevision
|
|
self.worldItemStartCache[mapID]=worldItemStartPlan or {}
|
|
self.worldItemStartRevision[mapID]=self.stateRevision
|
|
self.cacheDensity[mapID]=densitySignature or CurrentDensitySignature()
|
|
|
|
if not wasReady then
|
|
self.stats.preparedMaps=self.stats.preparedMaps+1
|
|
end
|
|
|
|
self.stats.descriptors=self.stats.descriptors-oldCount+table.getn(plan)
|
|
|
|
if tonumber(self.stats.currentMap)==mapID then
|
|
self.stats.currentReady=true
|
|
end
|
|
|
|
QuestieOcto:SendMessage("PREPARED_MAP_READY",mapID)
|
|
return plan
|
|
end
|
|
|
|
function P:BuildMap(mapID)
|
|
mapID=tonumber(mapID)
|
|
if not mapID or not QuestieOcto.Nodes.ready then return nil end
|
|
|
|
local nodes=QuestieOcto.Nodes:GetMapNodes(mapID)
|
|
local plan=self:BuildPlanFromNodes(mapID,nodes)
|
|
local worldItemStartPlan=self:BuildWorldItemStartPlanFromNodes(mapID,nodes)
|
|
return self:SetPreparedMap(mapID,plan,worldItemStartPlan,CurrentDensitySignature())
|
|
end
|
|
|
|
function P:Get(mapID)
|
|
mapID=tonumber(mapID)
|
|
if not mapID then return nil end
|
|
if self.cacheRevision[mapID]~=self.stateRevision then return nil end
|
|
if self.cacheDensity[mapID]~=CurrentDensitySignature() then return nil end
|
|
return self.cache[mapID]
|
|
end
|
|
|
|
function P:GetWorldItemStarts(mapID)
|
|
mapID=tonumber(mapID)
|
|
if not mapID then return nil end
|
|
if self.worldItemStartRevision[mapID]~=self.stateRevision then return nil end
|
|
if self.cacheDensity[mapID]~=CurrentDensitySignature() then return nil end
|
|
return self.worldItemStartCache[mapID]
|
|
end
|
|
|
|
function P:IsReady(mapID)
|
|
mapID=tonumber(mapID)
|
|
return mapID
|
|
and self.readyMaps[mapID]
|
|
and self.cacheRevision[mapID]==self.stateRevision
|
|
and self.cacheDensity[mapID]==CurrentDensitySignature()
|
|
and true or false
|
|
end
|
|
|
|
local function RemoveQuestFromDescriptor(desc,questID)
|
|
if not desc then return false,0 end
|
|
|
|
if desc.type=="itemStartArea" and desc.area then
|
|
if tonumber(desc.area.questID)==questID then return true,1 end
|
|
return false,0
|
|
end
|
|
|
|
if desc.type=="nodeSlot" then
|
|
local kept={}
|
|
local removed=0
|
|
for _,entry in pairs(desc.entries or {}) do
|
|
if entry.node and tonumber(entry.node.questID)==questID then
|
|
removed=removed+1
|
|
else
|
|
table.insert(kept,entry)
|
|
end
|
|
end
|
|
desc.entries=kept
|
|
return table.getn(kept)==0,removed
|
|
end
|
|
|
|
if desc.type=="node" and desc.node and tonumber(desc.node.questID)==questID then
|
|
return true,1
|
|
end
|
|
|
|
return false,0
|
|
end
|
|
|
|
function P:RemoveQuest(questID)
|
|
questID=tonumber(questID)
|
|
if not questID then return 0 end
|
|
|
|
self.stateRevision=self.stateRevision+1
|
|
self.stats.stateRevision=self.stateRevision
|
|
self.stats.revisionBumps=(self.stats.revisionBumps or 0)+1
|
|
self.stats.currentReady=false
|
|
self.lastRevisionReason="quest-remove"
|
|
|
|
local removed=0
|
|
for mapID,plan in pairs(self.cache) do
|
|
local filtered={}
|
|
for _,desc in pairs(plan or {}) do
|
|
local drop,count=RemoveQuestFromDescriptor(desc,questID)
|
|
removed=removed+(count or 0)
|
|
if not drop then table.insert(filtered,desc) end
|
|
end
|
|
|
|
self.cache[mapID]=filtered
|
|
if self.readyMaps[mapID] then
|
|
self.cacheRevision[mapID]=self.stateRevision
|
|
end
|
|
end
|
|
|
|
for mapID,plan in pairs(self.worldItemStartCache or {}) do
|
|
local filtered={}
|
|
for _,desc in pairs(plan or {}) do
|
|
local drop,count=RemoveQuestFromDescriptor(desc,questID)
|
|
removed=removed+(count or 0)
|
|
if not drop then table.insert(filtered,desc) end
|
|
end
|
|
self.worldItemStartCache[mapID]=filtered
|
|
if self.readyMaps[mapID] then self.worldItemStartRevision[mapID]=self.stateRevision end
|
|
end
|
|
|
|
-- `descriptors` counts coordinate slots, not quest references, in the new
|
|
-- pfQuest-style prepared representation. Recalculate cheaply after an
|
|
-- immediate quest removal rather than subtracting removed metadata entries.
|
|
local descriptorCount=0
|
|
for _,plan in pairs(self.cache) do descriptorCount=descriptorCount+table.getn(plan or {}) end
|
|
self.stats.descriptors=descriptorCount
|
|
if self.stats.currentMap and self.readyMaps[self.stats.currentMap] then
|
|
self.stats.currentReady=true
|
|
end
|
|
|
|
return removed
|
|
end
|
|
|
|
function P:BumpStateRevision(reason)
|
|
self.stateRevision=self.stateRevision+1
|
|
self.stats.stateRevision=self.stateRevision
|
|
self.stats.revisionBumps=(self.stats.revisionBumps or 0)+1
|
|
self.stats.currentReady=false
|
|
self.lastRevisionReason=reason
|
|
end
|
|
|
|
function P:Invalidate()
|
|
self.generation=self.generation+1
|
|
self.dirtyGeneration=(self.dirtyGeneration or 0)+1
|
|
self.cache={}
|
|
self.readyMaps={}
|
|
self.cacheRevision={}
|
|
self.worldItemStartCache={}
|
|
self.worldItemStartRevision={}
|
|
self.cacheDensity={}
|
|
self.running=false
|
|
self.stats.preparedMaps=0
|
|
self.stats.descriptors=0
|
|
self.stats.currentReady=false
|
|
end
|
|
|
|
function P:PrepareAll(reason)
|
|
if not QuestieOcto.Nodes.ready then return end
|
|
|
|
-- A full preparation pass supersedes every queued incremental prepared-map
|
|
-- patch. Density changes in particular must not let an older dirty job publish
|
|
-- a mixed Clustered/Full plan after the authoritative rebuild has started.
|
|
self.dirtyGeneration=(self.dirtyGeneration or 0)+1
|
|
local densitySignature=CurrentDensitySignature()
|
|
self.stats.densitySignature=densitySignature
|
|
self.lastPrepareReason=reason or "full"
|
|
|
|
-- Transactional map-plan rebuild. Keep every currently published plan alive
|
|
-- until that specific map's replacement is ready; density/filter toggles
|
|
-- must never invalidate the live cache first and make pins blink off/on.
|
|
self.generation=self.generation+1
|
|
local generation=self.generation
|
|
self.running=true
|
|
|
|
local current=QuestieOcto.API:GetBestMapForPlayer()
|
|
self.stats.currentMap=current
|
|
|
|
-- Density changes are shared by the minimap and World Map, but the two can
|
|
-- be looking at different maps. The minimap always follows `current`, while
|
|
-- the World Map can stay open on any selected zone. Rebuild both visible
|
|
-- contexts first so Clustered <-> Full Nodes changes are immediate on each
|
|
-- presentation instead of waiting for the displayed World Map zone to be
|
|
-- reached by the background PrepareAll pass.
|
|
local displayed=nil
|
|
if QuestieOcto.Map then
|
|
if WorldMapFrame and WorldMapFrame:IsVisible() and QuestieOcto.Map.GetDisplayedMapID then
|
|
displayed=QuestieOcto.Map:GetDisplayedMapID()
|
|
elseif tonumber(QuestieOcto.Map.mapID) and tonumber(QuestieOcto.Map.mapID)>0 then
|
|
-- The options window can be used while the World Map is hidden. Preserve
|
|
-- the last selected zone as a priority target so reopening that same zone
|
|
-- never sees the previous density plan.
|
|
displayed=tonumber(QuestieOcto.Map.mapID)
|
|
end
|
|
end
|
|
|
|
local mapSet={}
|
|
for mapID in pairs(QuestieOcto.Nodes.byMap or {}) do mapSet[tonumber(mapID)]=true end
|
|
-- Include formerly populated maps so a rebuild that legitimately removes
|
|
-- every node from a map publishes an empty plan rather than leaving stale pins.
|
|
for mapID in pairs(self.readyMaps or {}) do mapSet[tonumber(mapID)]=true end
|
|
|
|
local ids={}
|
|
for mapID in pairs(mapSet) do if mapID then table.insert(ids,mapID) end end
|
|
table.sort(ids)
|
|
|
|
local function publishMap(mapID)
|
|
if generation~=P.generation then return false end
|
|
local nodes=QuestieOcto.Nodes:GetMapNodes(mapID)
|
|
local plan=P:BuildPlanFromNodes(mapID,nodes) or {}
|
|
local worldItemStartPlan=P:BuildWorldItemStartPlanFromNodes(mapID,nodes) or {}
|
|
P:SetPreparedMap(mapID,plan,worldItemStartPlan,densitySignature)
|
|
return true
|
|
end
|
|
|
|
-- Publish the open World Map zone first, then the player's current zone for
|
|
-- the minimap. Either may be the same map. SetPreparedMap broadcasts
|
|
-- PREPARED_MAP_READY, so both renderers immediately consume the replacement.
|
|
if displayed and mapSet[tonumber(displayed)] then
|
|
publishMap(tonumber(displayed))
|
|
end
|
|
if current and tonumber(current)~=tonumber(displayed) and mapSet[tonumber(current)] then
|
|
publishMap(tonumber(current))
|
|
end
|
|
|
|
local pos=1
|
|
local function step()
|
|
if generation~=P.generation then return end
|
|
|
|
local requested=QuestieOcto.ZoneBootstrap and QuestieOcto.ZoneBootstrap.requestedMapID
|
|
if requested and not P.readyMaps[tonumber(requested)] and QuestieOcto.ZoneBootstrap.running then
|
|
QuestieOcto.Scheduler:Enqueue(step,"prepare-maps-yield")
|
|
return
|
|
end
|
|
|
|
local count=0
|
|
while pos<=table.getn(ids) and count<1 do
|
|
local mapID=ids[pos]
|
|
pos=pos+1
|
|
if tonumber(mapID)~=tonumber(current) and tonumber(mapID)~=tonumber(displayed) then
|
|
publishMap(mapID)
|
|
count=count+1
|
|
end
|
|
end
|
|
|
|
if pos<=table.getn(ids) then
|
|
QuestieOcto.Scheduler:Enqueue(step,"prepare-maps")
|
|
else
|
|
P.running=false
|
|
QuestieOcto:SendMessage("PREPARED_MAPS_COMPLETE")
|
|
end
|
|
end
|
|
|
|
QuestieOcto.Scheduler:Enqueue(step,"prepare-maps")
|
|
end
|
|
|
|
function P:OnNodesReady()
|
|
self:PrepareAll("nodes-ready")
|
|
end
|
|
|
|
QuestieOcto:RegisterMessage("NODES_READY",P,"OnNodesReady")
|
|
|
|
local function DescriptorKeyValue(desc)
|
|
return desc and tostring(desc.key or "") or ""
|
|
end
|
|
|
|
local function ClonePreparedDescriptor(desc)
|
|
if not desc then return nil end
|
|
if desc.type~="nodeSlot" then return desc end
|
|
|
|
-- PatchMaps removes quest entries from a descriptor. Clone the slot first so
|
|
-- the currently published PreparedMap (which the minimap may still be using)
|
|
-- is never mutated before its replacement is complete and published.
|
|
local copy={}
|
|
for key,value in pairs(desc) do
|
|
if key~="entries" then copy[key]=value end
|
|
end
|
|
copy.entries={}
|
|
for _,entry in pairs(desc.entries or {}) do table.insert(copy.entries,entry) end
|
|
return copy
|
|
end
|
|
|
|
local function MergePreparedDescriptor(byKey,plan,desc)
|
|
if not desc then return end
|
|
local key=DescriptorKeyValue(desc)
|
|
local existing=byKey[key]
|
|
|
|
if existing and existing.type=="nodeSlot" and desc.type=="nodeSlot" then
|
|
for _,entry in pairs(desc.entries or {}) do table.insert(existing.entries,entry) end
|
|
return
|
|
end
|
|
|
|
-- Item-start areas are quest-specific and exact/full node-slot keys are
|
|
-- canonical. A same-key non-slot descriptor is safely replaced.
|
|
if existing then
|
|
for i,current in ipairs(plan) do
|
|
if current==existing then plan[i]=desc; break end
|
|
end
|
|
byKey[key]=desc
|
|
return
|
|
end
|
|
|
|
byKey[key]=desc
|
|
table.insert(plan,desc)
|
|
end
|
|
|
|
function P:PatchMaps(mapSet,changedQuests)
|
|
if not QuestieOcto.Nodes.ready then return end
|
|
|
|
local changed={}
|
|
for questID in pairs(changedQuests or {}) do
|
|
questID=tonumber(questID)
|
|
if questID and questID>0 then changed[questID]=true end
|
|
end
|
|
if not next(changed) then
|
|
-- Compatibility/fallback for an older producer that did not provide the
|
|
-- reverse quest set: rebuilding just the affected maps is still bounded.
|
|
return self:RebuildMaps(mapSet)
|
|
end
|
|
|
|
local ids={}
|
|
for mapID in pairs(mapSet or {}) do
|
|
mapID=tonumber(mapID)
|
|
if mapID then table.insert(ids,mapID) end
|
|
end
|
|
table.sort(ids)
|
|
if table.getn(ids)==0 then return end
|
|
|
|
-- Incremental node publications may arrive close together (for example two
|
|
-- quests completing in adjacent frames). They all read the latest canonical
|
|
-- Nodes state and are safe to interleave. Do NOT advance dirtyGeneration here:
|
|
-- doing so cancelled the older patch outright and could leave one quest's map
|
|
-- state stale until an unrelated option forced a rebuild. Full PrepareAll /
|
|
-- Invalidate still advance dirtyGeneration and therefore remain authoritative.
|
|
local generation=self.dirtyGeneration
|
|
local pos=1
|
|
|
|
local function step()
|
|
if generation~=P.dirtyGeneration then return end
|
|
local mapID=ids[pos]
|
|
pos=pos+1
|
|
|
|
if mapID then
|
|
local existing=P:Get(mapID)
|
|
if not existing then
|
|
-- First visit / stale cache: build the authoritative map once.
|
|
P:BuildMap(mapID)
|
|
else
|
|
-- pfQuest-style reverse update: remove only the changed quests' metadata
|
|
-- from existing coordinate slots, then merge descriptors produced by
|
|
-- those quests' new semantic state. Unrelated coordinates/clusters are
|
|
-- never rediscovered or reclustered.
|
|
local plan={}
|
|
local byKey={}
|
|
for _,desc in pairs(existing) do
|
|
local candidate=ClonePreparedDescriptor(desc)
|
|
local drop=false
|
|
for questID in pairs(changed) do
|
|
local remove=RemoveQuestFromDescriptor(candidate,questID)
|
|
if remove then drop=true end
|
|
end
|
|
if not drop then
|
|
table.insert(plan,candidate)
|
|
byKey[DescriptorKeyValue(candidate)]=candidate
|
|
end
|
|
end
|
|
|
|
local changedNodes={}
|
|
for _,node in pairs(QuestieOcto.Nodes:GetMapNodes(mapID) or {}) do
|
|
if changed[tonumber(node.questID)] then table.insert(changedNodes,node) end
|
|
end
|
|
local delta=P:BuildPlanFromNodes(mapID,changedNodes) or {}
|
|
for _,desc in pairs(delta) do MergePreparedDescriptor(byKey,plan,desc) end
|
|
|
|
table.sort(plan,function(a,b) return DescriptorKeyValue(a)<DescriptorKeyValue(b) end)
|
|
local allNodes=QuestieOcto.Nodes:GetMapNodes(mapID)
|
|
local worldItemStartPlan=P:BuildWorldItemStartPlanFromNodes(mapID,allNodes) or {}
|
|
P:SetPreparedMap(mapID,plan,worldItemStartPlan,CurrentDensitySignature())
|
|
end
|
|
end
|
|
|
|
if pos<=table.getn(ids) then
|
|
QuestieOcto.Scheduler:Enqueue(step,"prepare-dirty-maps")
|
|
end
|
|
end
|
|
|
|
QuestieOcto.Scheduler:Enqueue(step,"prepare-dirty-maps")
|
|
end
|
|
|
|
function P:RebuildMaps(mapSet)
|
|
if not QuestieOcto.Nodes.ready then return end
|
|
|
|
local ids={}
|
|
for mapID in pairs(mapSet or {}) do
|
|
mapID=tonumber(mapID)
|
|
if mapID then table.insert(ids,mapID) end
|
|
end
|
|
table.sort(ids)
|
|
if table.getn(ids)==0 then return end
|
|
|
|
-- Incremental node publications may arrive close together (for example two
|
|
-- quests completing in adjacent frames). They all read the latest canonical
|
|
-- Nodes state and are safe to interleave. Do NOT advance dirtyGeneration here:
|
|
-- doing so cancelled the older patch outright and could leave one quest's map
|
|
-- state stale until an unrelated option forced a rebuild. Full PrepareAll /
|
|
-- Invalidate still advance dirtyGeneration and therefore remain authoritative.
|
|
local generation=self.dirtyGeneration
|
|
local pos=1
|
|
|
|
local function step()
|
|
if generation~=P.dirtyGeneration then return end
|
|
local mapID=ids[pos]
|
|
pos=pos+1
|
|
if mapID then
|
|
local nodes=QuestieOcto.Nodes:GetMapNodes(mapID)
|
|
local plan=P:BuildPlanFromNodes(mapID,nodes) or {}
|
|
local worldItemStartPlan=P:BuildWorldItemStartPlanFromNodes(mapID,nodes) or {}
|
|
P:SetPreparedMap(mapID,plan,worldItemStartPlan,CurrentDensitySignature())
|
|
end
|
|
|
|
if pos<=table.getn(ids) then
|
|
QuestieOcto.Scheduler:Enqueue(step,"prepare-dirty-maps")
|
|
end
|
|
end
|
|
|
|
QuestieOcto.Scheduler:Enqueue(step,"prepare-dirty-maps")
|
|
end
|
|
|
|
function P:OnNodesChanged(mapSet,changedQuests)
|
|
self:PatchMaps(mapSet,changedQuests)
|
|
end
|
|
|
|
QuestieOcto:RegisterMessage("NODES_CHANGED",P,"OnNodesChanged")
|