mirror of
https://github.com/Blaststar/Questie-Octo-Vanilla.git
synced 2026-10-20 04:10:49 +00:00
240 lines
7.0 KiB
Lua
240 lines
7.0 KiB
Lua
QuestieOcto.ItemStarts = QuestieOcto.ItemStarts or {}
|
|
local I = QuestieOcto.ItemStarts
|
|
|
|
I.byQuest={}
|
|
I.ready=false
|
|
I.running=false
|
|
I.generation=0
|
|
local function NewStats()
|
|
return { quests=0, starterItems=0, creatureSources=0, objectSources=0 }
|
|
end
|
|
|
|
I.stats=NewStats()
|
|
|
|
-- Quest-starting items can intentionally be rare. A positive recorded
|
|
-- drop chance is enough to make the source useful guidance; filtering at 1%
|
|
-- caused legitimate starters such as Captain Sander's Treasure Map (0.75%)
|
|
-- to vanish after the full item-start resolver replaced the zone bootstrap.
|
|
local function PositiveDropChance(chance)
|
|
chance=tonumber(chance) or 0
|
|
return chance>0
|
|
end
|
|
|
|
function I:IsPositiveDropChance(chance)
|
|
return PositiveDropChance(chance)
|
|
end
|
|
|
|
local function RecountStats(byQuest)
|
|
local stats=NewStats()
|
|
for _,resolved in pairs(byQuest or {}) do
|
|
stats.quests=stats.quests+1
|
|
for _,item in pairs(resolved.items or {}) do
|
|
stats.starterItems=stats.starterItems+1
|
|
stats.creatureSources=stats.creatureSources+table.getn(item.creatureSources or {})
|
|
stats.objectSources=stats.objectSources+table.getn(item.objectSources or {})
|
|
end
|
|
end
|
|
return stats
|
|
end
|
|
|
|
local function AddCreature(entry,seen,creatureID,chance,vendor)
|
|
local key=tostring(creatureID)
|
|
if seen[key] then return end
|
|
seen[key]=true
|
|
table.insert(entry.creatureSources,{
|
|
kind="creature",id=creatureID,chance=chance,vendor=vendor and true or false,
|
|
rank=QuestieOcto.DatabaseAPI:GetCreatureRank(creatureID),
|
|
faction=QuestieOcto.DatabaseAPI:GetCreatureFaction(creatureID)
|
|
})
|
|
end
|
|
|
|
local function AddObject(entry,seen,objectID,chance)
|
|
local key=tostring(objectID)
|
|
if seen[key] then return end
|
|
seen[key]=true
|
|
table.insert(entry.objectSources,{kind="gameObject",id=objectID,chance=chance})
|
|
end
|
|
|
|
local function ResolveSources(entry,itemID)
|
|
local sources=QuestieOcto.DatabaseAPI:GetItemSources(itemID)
|
|
local seenCreature={}
|
|
local seenObject={}
|
|
|
|
if sources and sources.Creature then
|
|
for creatureID,chance in pairs(sources.Creature) do
|
|
chance=tonumber(chance) or 0
|
|
if PositiveDropChance(chance) then AddCreature(entry,seenCreature,creatureID,chance,false) end
|
|
end
|
|
end
|
|
|
|
if sources and sources.GameObject then
|
|
for objectID,chance in pairs(sources.GameObject) do
|
|
chance=tonumber(chance) or 0
|
|
if PositiveDropChance(chance) then AddObject(entry,seenObject,objectID,chance) end
|
|
end
|
|
end
|
|
|
|
if sources and sources.Reference then
|
|
for refID,chance in pairs(sources.Reference) do
|
|
chance=tonumber(chance) or 0
|
|
if PositiveDropChance(chance) then
|
|
local ref=QuestieOcto.DatabaseAPI:GetReferenceLootRaw(refID)
|
|
if ref and ref["U"] then
|
|
for creatureID in pairs(ref["U"]) do AddCreature(entry,seenCreature,creatureID,chance,false) end
|
|
end
|
|
if ref and ref["O"] then
|
|
for objectID in pairs(ref["O"]) do AddObject(entry,seenObject,objectID,chance) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if sources and sources.Vendor then
|
|
for creatureID in pairs(sources.Vendor) do AddCreature(entry,seenCreature,creatureID,nil,true) end
|
|
end
|
|
end
|
|
|
|
function I:ResolveQuest(questID)
|
|
local q=QuestieOcto.QuestModel:Get(questID)
|
|
if not q or not q.starts.item then return nil end
|
|
|
|
local result={ questID=questID, items={} }
|
|
|
|
for _,itemID in pairs(q.starts.item) do
|
|
local entry={
|
|
itemID=itemID,
|
|
name=QuestieOcto.DatabaseAPI:GetItemName(itemID),
|
|
creatureSources={},
|
|
objectSources={}
|
|
}
|
|
|
|
ResolveSources(entry,itemID)
|
|
table.insert(result.items,entry)
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
function I:Rebuild()
|
|
if not QuestieOcto.AvailableQuests.ready then return end
|
|
|
|
self.generation=self.generation+1
|
|
local generation=self.generation
|
|
self.running=true
|
|
if not self.ready then self.ready=false end
|
|
|
|
-- Transactional rebuild: keep the published item-start cache alive until the
|
|
-- complete replacement is resolved. This prevents downstream consumers from
|
|
-- seeing a temporary empty item-start set during filter changes.
|
|
local replacement={}
|
|
local ids={}
|
|
for questID in pairs(QuestieOcto.AvailableQuests.available) do
|
|
local q=QuestieOcto.QuestModel:Get(questID)
|
|
if q and q.starts.item then table.insert(ids,questID) end
|
|
end
|
|
table.sort(ids)
|
|
|
|
local pos=1
|
|
local function step()
|
|
if generation~=I.generation then return end
|
|
local count=0
|
|
while pos<=table.getn(ids) and count<64 do
|
|
local questID=ids[pos]
|
|
pos=pos+1
|
|
local resolved=I:ResolveQuest(questID)
|
|
if resolved then replacement[questID]=resolved end
|
|
count=count+1
|
|
end
|
|
if pos<=table.getn(ids) then
|
|
QuestieOcto.Scheduler:Enqueue(step,"itemstart-resolve")
|
|
return
|
|
end
|
|
|
|
I.byQuest=replacement
|
|
I.stats=RecountStats(replacement)
|
|
I.running=false
|
|
I.ready=true
|
|
QuestieOcto:SendMessage("ITEM_STARTS_READY")
|
|
end
|
|
QuestieOcto.Scheduler:Enqueue(step,"itemstart-resolve")
|
|
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
|
|
|
|
function I:RefreshAvailability(changedQuests)
|
|
if not self.ready or self.running then
|
|
self:Rebuild()
|
|
return
|
|
end
|
|
|
|
local changed=NormalizeChangedQuests(changedQuests)
|
|
if not next(changed) then
|
|
QuestieOcto:SendMessage("ITEM_STARTS_CHANGED",changed)
|
|
return
|
|
end
|
|
|
|
self.generation=self.generation+1
|
|
local generation=self.generation
|
|
self.running=true
|
|
|
|
-- Shallow-copy the published quest cache and resolve only quests that crossed
|
|
-- the availability boundary. Publish once at the end so Nodes never observes
|
|
-- a half-updated item-start set.
|
|
local replacement={}
|
|
for questID,resolved in pairs(self.byQuest or {}) do replacement[questID]=resolved end
|
|
|
|
local ids={}
|
|
for questID in pairs(changed) do table.insert(ids,questID) end
|
|
table.sort(ids)
|
|
local pos=1
|
|
|
|
local function step()
|
|
if generation~=I.generation then return end
|
|
local count=0
|
|
while pos<=table.getn(ids) and count<32 do
|
|
local questID=ids[pos]
|
|
pos=pos+1
|
|
replacement[questID]=nil
|
|
|
|
if QuestieOcto.AvailableQuests.available[questID] then
|
|
local q=QuestieOcto.QuestModel:Get(questID)
|
|
if q and q.starts.item then
|
|
local resolved=I:ResolveQuest(questID)
|
|
if resolved then replacement[questID]=resolved end
|
|
end
|
|
end
|
|
count=count+1
|
|
end
|
|
|
|
if pos<=table.getn(ids) then
|
|
QuestieOcto.Scheduler:Enqueue(step,"itemstart-availability-patch")
|
|
return
|
|
end
|
|
|
|
I.byQuest=replacement
|
|
I.stats=RecountStats(replacement)
|
|
I.running=false
|
|
I.ready=true
|
|
QuestieOcto:SendMessage("ITEM_STARTS_CHANGED",changed)
|
|
end
|
|
|
|
QuestieOcto.Scheduler:Enqueue(step,"itemstart-availability-patch")
|
|
end
|
|
|
|
function I:OnAvailableReady(changedQuests)
|
|
if self.ready and changedQuests then
|
|
self:RefreshAvailability(changedQuests)
|
|
else
|
|
self:Rebuild()
|
|
end
|
|
end
|
|
|
|
QuestieOcto:RegisterMessage("AVAILABLE_QUESTS_READY",I,"OnAvailableReady")
|