QuestieOcto.Objectives = QuestieOcto.Objectives or {} local O = QuestieOcto.Objectives O.byQuest={} O.ready=false O.running=false O.pending=false O.generation=0 O.stats={ quests=0, creature=0, object=0, item=0, areaTrigger=0, itemSources=0, irTargets=0, mapped=0, unmapped=0, direct=0, fallback=0, fuzzy=0, single=0, typeMismatch=0, rebuilds=0, dependencyWaits=0 } local MIN_DROP_CHANCE=1 local function ResetStats() local rebuilds=O.stats.rebuilds or 0 local waits=O.stats.dependencyWaits or 0 O.stats={ quests=0, creature=0, object=0, item=0, areaTrigger=0, itemSources=0, irTargets=0, mapped=0, unmapped=0, direct=0, fallback=0, fuzzy=0, single=0, typeMismatch=0, rebuilds=rebuilds, dependencyWaits=waits } end local function DependenciesReady() return QuestieOcto.DatabaseAPI and QuestieOcto.DatabaseAPI:IsReady() and QuestieOcto.QuestLog and QuestieOcto.QuestLog.snapshot end local function AddSource(list,kind,id,itemID,chance,vendor) table.insert(list,{kind=kind,id=id,itemID=itemID,chance=chance,vendor=vendor and true or false}) end local function AddUniqueSource(list,seen,kind,id,itemID,chance,vendor) local key=kind..":"..tostring(id) if seen[key] then return end seen[key]=true AddSource(list,kind,id,itemID,chance,vendor) O.stats.itemSources=O.stats.itemSources+1 end -- pfQuest SearchItemID semantics: direct creature/object drops above the -- configured threshold, reference-loot owners, and vendors. The original -- pfQuest default threshold is 1%, which we preserve here without adding a -- new player-facing option. local function ResolveItemSources(itemID) local result={} local seen={} local sources=QuestieOcto.DatabaseAPI:GetItemSources(itemID) if sources and sources.Creature then for creatureID,chance in pairs(sources.Creature) do chance=tonumber(chance) or 0 if chance>=MIN_DROP_CHANCE then AddUniqueSource(result,seen,"creature",creatureID,itemID,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 chance>=MIN_DROP_CHANCE and chance>0 then AddUniqueSource(result,seen,"gameObject",objectID,itemID,chance,false) end end end if sources and sources.Reference then for refID,chance in pairs(sources.Reference) do chance=tonumber(chance) or 0 if chance>=MIN_DROP_CHANCE then local ref=QuestieOcto.DatabaseAPI:GetReferenceLootRaw(refID) if ref and ref["U"] then for creatureID in pairs(ref["U"]) do AddUniqueSource(result,seen,"creature",creatureID,itemID,chance,false) end end if ref and ref["O"] then for objectID in pairs(ref["O"]) do AddUniqueSource(result,seen,"gameObject",objectID,itemID,chance,false) end end end end end if sources and sources.Vendor then for creatureID in pairs(sources.Vendor) do AddUniqueSource(result,seen,"creature",creatureID,itemID,nil,true) end end return result end local function ItemIDFromLink(link) if not link then return nil end local _,_,id=string.find(link,"item:(%d+)") return tonumber(id) end local function PlayerHasItem(itemID) itemID=tonumber(itemID) if not itemID then return false end if GetContainerNumSlots and GetContainerItemLink then for bag=0,4 do local slots=GetContainerNumSlots(bag) or 0 for slot=1,slots do if ItemIDFromLink(GetContainerItemLink(bag,slot))==itemID then return true end end end end if GetInventoryItemLink then for slot=1,19 do if ItemIDFromLink(GetInventoryItemLink("player",slot))==itemID then return true end end end return false end -- pfQuest only reacts to bag changes that affect registered quest-item -- dependencies. Normal objective counts refresh the Tracker directly; map -- objectives only rebuild when their todo/done semantics actually change. local function CollectIRItemIDs() local tracked={} for questID in pairs(QuestieOcto.QuestLog.active or {}) do local q=QuestieOcto.QuestModel:Get(questID) if q and q.objectives and q.objectives.irItems then for _,itemID in pairs(q.objectives.irItems) do itemID=tonumber(itemID) if itemID and itemID>0 then tracked[itemID]=true end end end end return tracked end local function SnapshotIRPresence() local state={} local tracked=CollectIRItemIDs() for itemID in pairs(tracked) do state[itemID]=PlayerHasItem(itemID) and true or false end return state end local function RefreshIRPresence() O.irPresence=SnapshotIRPresence() end local function IRPresenceChanged() local previous=O.irPresence or {} local current=SnapshotIRPresence() for itemID,present in pairs(current) do if previous[itemID]~=present then O.irPresence=current return true end end for itemID in pairs(previous) do if current[itemID]==nil then O.irPresence=current return true end end O.irPresence=current return false end local function BuildIRTargets(q) local targets={} local skipCreature={} local skipObject={} for _,itemID in pairs(q.objectives.irItems or {}) do local requirement=QuestieOcto.DatabaseAPI:GetQuestItemRequirementRaw(itemID) if requirement then local hasItem=PlayerHasItem(itemID) for signedID in pairs(requirement) do signedID=tonumber(signedID) if signedID and signedID<0 then local id=math.abs(signedID) skipObject[id]=true if hasItem then table.insert(targets,{kind="gameObject",id=id,itemID=itemID,ir=true}) end elseif signedID and signedID>0 then skipCreature[signedID]=true if hasItem then table.insert(targets,{kind="creature",id=signedID,itemID=itemID,ir=true}) end end end end end return targets,skipCreature,skipObject end local function NormalizeLogType(typ) typ=string.lower(tostring(typ or "")) if typ=="monster" or typ=="creature" or typ=="kill" then return "creature" end if typ=="item" then return "item" end if typ=="object" or typ=="gameobject" then return "gameObject" end return typ end -- Sparse presentation-only corrections for capture/transform objectives where -- the server awards quest credit against a temporary NPC entry that has no -- natural spawn points. Keep the authoritative live objective ID for progress -- and completion; only map/minimap guidance uses the actionable source NPC. -- -- Plagued Lands (2118): Tortoise darkshore.cpp handles spell 9439 by changing -- Rabid Thistle Bear (2164) into Captured Rabid Thistle Bear (11836), then -- awards KilledMonsterCredit for 11836. The player must therefore find 2164, -- while 11836 remains the correct completion target in the quest log/database. local CREATURE_OBJECTIVE_SOURCE_OVERRIDES={ [2118]={ [11836]=2164 }, } local function PresentationCreatureSourceID(questID,objectiveID) local byQuest=CREATURE_OBJECTIVE_SOURCE_OVERRIDES[tonumber(questID)] local sourceID=byQuest and byQuest[tonumber(objectiveID)] return tonumber(sourceID) or tonumber(objectiveID) end local function Levenshtein(str1,str2) str1=string.lower(tostring(str1 or "")) str2=string.lower(tostring(str2 or "")) local len1=string.len(str1) local len2=string.len(str2) local matrix={} local cost=0 if len1==0 then return len2 end if len2==0 then return len1 end if str1==str2 then return 0 end for i=0,len1 do matrix[i]={}; matrix[i][0]=i end for j=0,len2 do matrix[0][j]=j end for i=1,len1 do for j=1,len2 do if string.byte(str1,i)==string.byte(str2,j) then cost=0 else cost=1 end matrix[i][j]=math.min(matrix[i-1][j]+1,matrix[i][j-1]+1,matrix[i-1][j-1]+cost) end end return matrix[len1][len2] end local function MergeState(entry,row) entry.objectiveIndex=row.index entry.objectiveID=row.objectiveID entry.objectiveText=row.text entry.rawObjectiveText=row.rawText or row.text entry.objectiveType=row.type entry.current=row.current entry.required=row.required entry.complete=row.complete and true or false return entry end local function ObjectiveTypesMatch(logType,dbType) local kind=NormalizeLogType(logType) if kind=="creature" then return dbType=="monster" end if kind=="gameObject" then return dbType=="object" end if kind=="item" then return dbType=="item" end return false end local function EntryFromLiveObjectiveID(q,row) local id=tonumber(row and row.objectiveID) if not q or not id then return nil end local typ=string.lower(tostring(row.type or "")) local creatureMatch=nil local objectMatch=nil local itemMatch=nil for i=1,table.getn(q.objectiveData or {}) do local entry=q.objectiveData[i] if entry and tonumber(entry.id)==id then if entry.kind=="creature" then creatureMatch=entry elseif entry.kind=="gameObject" then objectMatch=entry elseif entry.kind=="item" then itemMatch=entry end end end if typ=="item" then return itemMatch end if typ=="object" or typ=="gameobject" then return objectMatch end if typ=="monster" or typ=="creature" or typ=="kill" then -- pfQuest notes that Vanilla can return "monster" for some object-style -- objectives; let the live ClassicAPI ID disambiguate the namespace. return creatureMatch or objectMatch end return creatureMatch or objectMatch or itemMatch end local function CandidateName(kind,id) if kind=="creature" then return QuestieOcto.DatabaseAPI:GetCreatureName(id) end if kind=="gameObject" then return QuestieOcto.DatabaseAPI:GetObjectName(id) end if kind=="item" then return QuestieOcto.DatabaseAPI:GetItemName(id) end return nil end local function CandidateIDs(q,kind) if kind=="creature" then return q.objectives.creature or {} end if kind=="gameObject" then return q.objectives.gameObject or {} end if kind=="item" then return q.objectives.item or {} end return {} end local function BestSameTypeCandidate(q,kind,row,used) local ids=CandidateIDs(q,kind) local count=0 local onlyID=nil for _,id in pairs(ids) do if not used[kind..":"..tostring(id)] then count=count+1; onlyID=id end end if count==1 then O.stats.single=O.stats.single+1; return onlyID end local bestID=nil local bestDistance=999999 local desc=row.text or "" for _,id in pairs(ids) do if not used[kind..":"..tostring(id)] then local name=CandidateName(kind,id) if name then local distance=Levenshtein(desc,name) if distance0 then affected[questID]=true end end if next(affected) then O:RefreshQuests(affected) end end end,"objective-ir-bag-check") end)