---@class DBCompiler local QuestieDBCompiler = QuestieLoader:CreateModule("DBCompiler") ---@type QuestieStreamLib local QuestieStream = QuestieLoader:ImportModule("QuestieStreamLib"):GetStream("raw") ---@type QuestieDB local QuestieDB = QuestieLoader:ImportModule("QuestieDB") ---@type QuestieLib local QuestieLib = QuestieLoader:ImportModule("QuestieLib") ---@type l10n local l10n = QuestieLoader:ImportModule("l10n") --- COMPATIBILITY --- local WOW_PROJECT_ID = QuestieCompat.WOW_PROJECT_ID local type = type local abs, min, floor = math.abs, math.min, math.floor local lshift = bit.lshift -- how fast to run operations (lower = slower but less lag) local TICKS_PER_YIELD = 48 local TICKS_PER_YIELD_DEBUG = TICKS_PER_YIELD * 3 ---@alias CompilerTypes ---| "u8" ---| "u16" ---| "s16" ---| "u24" ---| "u32" ---| "u12pair" ---| "u24pair" ---| "s24pair" ---| "u8string" ---| "u16string" ---| "u8u16array" ---| "u8s16pairs" ---| "u16u16array" ---| "u8s24pairs" ---| "u8u24array" ---| "u16u24array" ---| "u8u16stringarray" ---| "faction" ---| "spawnlist" ---| "trigger" ---| "questgivers" ---| "objective" ---| "spellobjective" ---| "objectives" ---| "reflist" ---| "extraobjective QuestieDBCompiler.supportedTypes = { ["table"] = { ["u12pair"] = true, ["u24pair"] = true, ["s24pair"] = true, ["u8u16array"] = true, ["u16u16array"] = true, ["u8s16pairs"] = true, ["u8s24pairs"] = true, ["spawnlist"] = true, ["trigger"] = true, ["questgivers"] = true, ["objective"] = true, ["spellobjective"] = true, ["objectives"] = true, ["waypointlist"] = true, ["u8u16stringarray"] = true, ["u8u24array"] = true, ["u16u24array"] = true, ["extraobjectives"] = true, ["reflist"] = true }, ["number"] = { ["s8"] = true, ["u8"] = true, ["u16"] = true, ["s16"] = true, ["u24"] = true, ["s24"] = true, ["u32"] = true }, ["string"] = { ["u8string"] = true, ["u16string"] = true, ["faction"] = true } } local refTypes = { "monster", "item", "object" } QuestieDBCompiler.refTypes = refTypes QuestieDBCompiler.refTypesReversed = { monster = 1, item = 2, object = 3 } local readers = {} local skippers = {} readers["s8"] = function(stream) return stream:ReadByte() - 127 end readers["u8"] = QuestieStream.ReadByte readers["u16"] = QuestieStream.ReadShort readers["s16"] = function(stream) return stream:ReadShort() - 32767 end readers["u24"] = QuestieStream.ReadInt24 readers["s24"] = function(stream) return stream:ReadInt24() - 8388607 end readers["u32"] = QuestieStream.ReadInt readers["u12pair"] = function(stream) local a,b = stream:ReadInt12Pair() -- bit of a hack if a == 0 and b == 0 then return nil end return {a, b} end readers["u24pair"] = function(stream) local a,b = stream:ReadInt24(), stream:ReadInt24() -- bit of a hack if a == 0 and b == 0 then return nil end return {a, b} end readers["s24pair"] = function(stream) local a,b = stream:ReadInt24()-8388608, stream:ReadInt24()-8388608 -- bit of a hack if a == 0 and b == 0 then return nil end return {a, b} end readers["u8string"] = function(stream) local ret = stream:ReadTinyString() if ret == "nil" then-- I hate this but we need to support both nil strings and empty strings return nil else return ret end end readers["u16string"] = function(stream) local ret = stream:ReadShortString() if ret == "nil" then-- I hate this but we need to support both nil strings and empty strings return nil else return ret end end readers["u8u16array"] = function(stream) local count = stream:ReadByte() if count == 0 then return nil end local list = {} for i = 1, count do list[i] = stream:ReadShort() end return list end readers["u8s16pairs"] = function(stream) local count = stream:ReadByte() if count == 0 then return nil end local list = {} for i = 1, count do list[i] = {stream:ReadShort() - 32767, stream:ReadShort() - 32767} end return list end readers["u16u16array"] = function(stream) local count = stream:ReadShort() if count == 0 then return nil end local list = {} for i = 1, count do list[i] = stream:ReadShort() end return list end readers["u8s24pairs"] = function(stream) local count = stream:ReadByte() if count == 0 then return nil end local list = {} for i = 1, count do list[i] = {stream:ReadInt24()-8388608, stream:ReadInt24()-8388608} end return list end readers["u8u24array"] = function(stream) local count = stream:ReadByte() if count == 0 then return nil end local list = {} for i = 1, count do list[i] = stream:ReadInt24() end return list end readers["u16u24array"] = function(stream) local count = stream:ReadShort() if count == 0 then return nil end local list = {} for i = 1, count do list[i] = stream:ReadInt24() end return list end readers["u8u16stringarray"] = function(stream) local count = stream:ReadByte() if count == 0 then return nil end local list = {} for i = 1, count do list[i] = stream:ReadShortString() end return list end readers["faction"] = function(stream) local val = stream:ReadByte() if val == 3 then return nil elseif val == 2 then return "AH" elseif val == 1 then return "H" else return "A" end end readers["spawnlist"] = function(stream) local count = stream:ReadByte() if count == 0 then return nil end local spawnlist = {} for _ = 1, count do local zone = stream:ReadShort() local spawnCount = stream:ReadShort() local list = {} for i = 1, spawnCount do local x, y = stream:ReadInt12Pair() if x == 0 and y == 0 then list[i] = {-1, -1} else list[i] = {x / 40.90, y / 40.90} end end spawnlist[zone] = list end return spawnlist end readers["trigger"] = function(stream) if stream:ReadShort() == 0 then return nil else stream._pointer = stream._pointer - 2 end return {stream:ReadTinyStringNil(), readers["spawnlist"](stream)} end local u8u24arrayReader = readers["u8u24array"] readers["questgivers"] = function(stream) return { u8u24arrayReader(stream), u8u24arrayReader(stream), u8u24arrayReader(stream) } end readers["objective"] = function(stream) local count = stream:ReadByte() if count == 0 then return nil end local ret = {} for i = 1, count do ret[i] = {stream:ReadInt24(), stream:ReadTinyStringNil()} end return ret end readers["spellobjective"] = function(stream) local count = stream:ReadByte() if count == 0 then return nil end local ret = {} for i = 1, count do ret[i] = {stream:ReadInt24(), stream:ReadTinyStringNil(), stream:ReadInt24()} end return ret end readers["objectives"] = function(stream) local ret = { readers["objective"](stream), readers["objective"](stream), readers["objective"](stream), readers["u24pair"](stream), } local count = stream:ReadByte() if count == 0 then ret[5] = nil else local killobjectives = {} for i=1, count do local creditCount = stream:ReadByte() local creditList = {} for j=1, creditCount do creditList[j] = stream:ReadInt24() end killobjectives[i] = {creditList, stream:ReadInt24(), stream:ReadTinyStringNil()} end ret[5] = killobjectives end ret[6] = readers["spellobjective"](stream) return ret end readers["reflist"] = function(stream) local count = stream:ReadByte() if count > 0 then local ret = {} for i=1,count do ret[i] = {refTypes[stream:ReadByte()], stream:ReadInt24()} end return ret end end readers["extraobjectives"] = function(stream) local count = stream:ReadByte() if count > 0 then local ret = {} for i=1,count do ret[i] = { readers["spawnlist"](stream), stream:ReadInt24(), stream:ReadShortString(), stream:ReadInt24(), readers["reflist"](stream) } end return ret end return nil end readers["waypointlist"] = function(stream) local count = stream:ReadByte() if count == 0 then return nil end local waypointlist = {} for _ = 1, count do local lists = {} local zone = stream:ReadShort() local listCount = stream:ReadByte() for j = 1, listCount do local spawnCount = stream:ReadShort() local list = {} for i = 1, spawnCount do local x, y = stream:ReadInt12Pair() if x == 0 and y == 0 then list[i] = {-1, -1} else list[i] = {x / 40.90, y / 40.90} end end lists[j] = list end waypointlist[zone] = lists end return waypointlist end QuestieDBCompiler.readers = readers QuestieDBCompiler.writers = { ["s8"] = function(stream, value) stream:WriteByte((value or 0)+127) end, ["u8"] = function(stream, value) stream:WriteByte(value or 0) end, ["u16"] = function(stream, value) stream:WriteShort(value or 0) end, ["s16"] = function(stream, value) stream:WriteShort(32767 + (value or 0)) end, ["u24"] = function(stream, value) stream:WriteInt24(value or 0) end, ["s24"] = function(stream, value) stream:WriteInt24(8388607 + (value or 0)) end, ["u32"] = function(stream, value) stream:WriteInt(value or 0) end, ["u12pair"] = function(stream, value) if value then stream:WriteInt12Pair(value[1] or 0, value[2] or 0) else stream:WriteInt24(0) end end, ["u24pair"] = function(stream, value) if value then stream:WriteInt24(value[1] or 0) stream:WriteInt24(value[2] or 0) else stream:WriteInt24(0) stream:WriteInt24(0) end end, ["s24pair"] = function(stream, value) if value then stream:WriteInt24((value[1] or 0) + 8388608) stream:WriteInt24((value[2] or 0) + 8388608) else stream:WriteInt24(8388608) stream:WriteInt24(8388608) end end, ["u8string"] = function(stream, value) stream:WriteTinyString(value or "nil") -- I hate this but we need to support both nil strings and empty strings --if value then -- stream:WriteTinyString(value) --else -- stream:WriteByte(0) --end end, ["u16string"] = function(stream, value) --if value then -- stream:WriteShortString(value) --else -- stream:WriteShort(0) --end stream:WriteShortString(value or "nil") end, ["u8u16array"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteByte(count) for _,v in pairs(value) do stream:WriteShort(v) end else stream:WriteByte(0) end end, ["u8s16pairs"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteByte(count) for _,v in pairs(value) do stream:WriteShort((v[1] or 0) + 32767) stream:WriteShort((v[2] or 0) + 32767) end else stream:WriteByte(0) end end, ["u16u16array"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteShort(count) for _,v in pairs(value) do stream:WriteShort(v) end else stream:WriteShort(0) end end, ["u8s24pairs"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteByte(count) for _,v in pairs(value) do stream:WriteInt24((v[1] or 0) + 8388608) stream:WriteInt24((v[2] or 0) + 8388608) end else stream:WriteByte(0) end end, ["u8u24array"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteByte(count) for _,v in pairs(value) do stream:WriteInt24(v) end else stream:WriteByte(0) end end, ["u16u24array"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteShort(count) for _,v in pairs(value) do stream:WriteInt24(v) end else stream:WriteShort(0) end end, ["u8u16stringarray"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteByte(count) for _,v in pairs(value) do stream:WriteShortString(v or "nil") end else --print("Missing u8u16stringarray for " .. QuestieDBCompiler.currentEntry) stream:WriteByte(0) end end, ["faction"] = function(stream, value) if value == nil then stream:WriteByte(3) elseif "A" == value then stream:WriteByte(0) elseif "H" == value then stream:WriteByte(1) else stream:WriteByte(2) end end, ["spawnlist"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteByte(count) for zone, spawnlist in pairs(value) do count = 0 for _ in pairs(spawnlist) do count = count + 1 end stream:WriteShort(zone) stream:WriteShort(count) for _, spawn in pairs(spawnlist) do if spawn[1] == -1 and spawn[2] == -1 then -- instance spawn stream:WriteInt24(0) -- 0 instead else stream:WriteInt12Pair(floor(spawn[1] * 40.90), floor(spawn[2] * 40.90)) end end end else --print("Missing spawnlist for " .. QuestieDBCompiler.currentEntry) stream:WriteByte(0) end end, ["trigger"] = function(stream, value) if value then stream:WriteTinyString(value[1]) QuestieDBCompiler.writers["spawnlist"](stream, value[2]) else stream:WriteByte(0) stream:WriteByte(0) end end, ["questgivers"] = function(stream, value) if value then QuestieDBCompiler.writers["u8u24array"](stream, value[1]) QuestieDBCompiler.writers["u8u24array"](stream, value[2]) QuestieDBCompiler.writers["u8u24array"](stream, value[3]) else --print("Missing questgivers for " .. QuestieDBCompiler.currentEntry) TODO: Reintroduce this check in any form stream:WriteByte(0) stream:WriteByte(0) stream:WriteByte(0) end end, ["objective"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteByte(count) for _, pair in pairs(value) do stream:WriteInt24(pair[1]) stream:WriteTinyString(pair[2] or "") end else stream:WriteByte(0) end end, ["spellobjective"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteByte(count) for _, data in pairs(value) do stream:WriteInt24(data[1]) stream:WriteTinyString(data[2] or "") stream:WriteInt24(data[3] or 0) end else stream:WriteByte(0) end end, ["objectives"] = function(stream, value) if value then QuestieDBCompiler.writers["objective"](stream, value[1]) QuestieDBCompiler.writers["objective"](stream, value[2]) QuestieDBCompiler.writers["objective"](stream, value[3]) QuestieDBCompiler.writers["u24pair"](stream, value[4]) local killobjectives = value[5] if type(killobjectives) == "table" and #killobjectives > 0 then stream:WriteByte(#killobjectives) for i=1, #killobjectives do -- iterate over all killobjectives local killobjective = killobjectives[i] local npcIds = killobjective[1] assert(type(npcIds) == "table", "killobjective's npcids is not a table.") assert(#npcIds > 0, "killOojective has 0 npcIDs.") stream:WriteByte(#npcIds) -- write count of creatureIDs for j=1, #npcIds do stream:WriteInt24(npcIds[j]) -- write creatureID end stream:WriteInt24(killobjective[2]) -- write baseCreatureID stream:WriteTinyString(killobjective[3] or "") -- write baseCreatureText end else stream:WriteByte(0) end QuestieDBCompiler.writers["spellobjective"](stream, value[6]) else --print("Missing objective table for " .. QuestieDBCompiler.currentEntry) stream:WriteByte(0) stream:WriteByte(0) stream:WriteByte(0) stream:WriteInt24(0) stream:WriteInt24(0) stream:WriteByte(0) stream:WriteByte(0) end end, ["reflist"] = function(stream, value) stream:WriteByte(#value) for _, v in pairs(value) do stream:WriteByte(QuestieDBCompiler.refTypesReversed[v[1]]) stream:WriteInt24(v[2]) end end, ["extraobjectives"] = function(stream, value) if value then stream:WriteByte(#value) for _, data in pairs(value) do QuestieDBCompiler.writers["spawnlist"](stream, data[1]) stream:WriteInt24(data[2]) -- icon stream:WriteShortString(data[3]) -- description stream:WriteInt24(data[4] or 0) -- objective index (or 0) QuestieDBCompiler.writers["reflist"](stream, data[5] or {}) end else stream:WriteByte(0) end end, ["waypointlist"] = function(stream, value) if value then local count = 0 for _ in pairs(value) do count = count + 1 end stream:WriteByte(count) for zone, spawnlists in pairs(value) do stream:WriteShort(zone) count = 0 for _ in pairs(spawnlists) do count = count + 1 end stream:WriteByte(count) for _, spawnlist in pairs(spawnlists) do count = 0 for _ in pairs(spawnlist) do count = count + 1 end stream:WriteShort(count) for _, spawn in pairs(spawnlist) do if spawn[1] == -1 and spawn[2] == -1 then -- instance spawn stream:WriteInt24(0) -- 0 instead else stream:WriteInt12Pair(floor(spawn[1] * 40.90), floor(spawn[2] * 40.90)) end end end end else --print("Missing spawnlist for " .. QuestieDBCompiler.currentEntry) stream:WriteByte(0) end end } skippers["s8"] = function(stream) stream._pointer = stream._pointer + 1 end skippers["u8"] = function(stream) stream._pointer = stream._pointer + 1 end skippers["u16"] = function(stream) stream._pointer = stream._pointer + 2 end skippers["s16"] = function(stream) stream._pointer = stream._pointer + 2 end skippers["u24"] = function(stream) stream._pointer = stream._pointer + 3 end skippers["s24"] = function(stream) stream._pointer = stream._pointer + 3 end skippers["u32"] = function(stream) stream._pointer = stream._pointer + 4 end skippers["u12pair"] = function(stream) stream._pointer = stream._pointer + 3 end skippers["u24pair"] = function(stream) stream._pointer = stream._pointer + 6 end skippers["s24pair"] = function(stream) stream._pointer = stream._pointer + 6 end skippers["u8string"] = function(stream) stream._pointer = stream:ReadByte() + stream._pointer end skippers["u16string"] = function(stream) stream._pointer = stream:ReadShort() + stream._pointer end skippers["u8u16array"] = function(stream) stream._pointer = stream:ReadByte() * 2 + stream._pointer end skippers["u8s16pairs"] = function(stream) stream._pointer = stream:ReadByte() * 4 + stream._pointer end skippers["u16u16array"] = function(stream) stream._pointer = stream:ReadShort() * 2 + stream._pointer end skippers["u8s24pairs"] = function(stream) stream._pointer = stream:ReadByte() * 6 + stream._pointer end skippers["u8u24array"] = function(stream) stream._pointer = stream:ReadByte() * 3 + stream._pointer end skippers["u16u24array"] = function(stream) stream._pointer = stream:ReadShort() * 3 + stream._pointer end skippers["waypointlist"] = function(stream) local count = stream:ReadByte() for _ = 1, count do stream._pointer = stream._pointer + 2 local listCount = stream:ReadByte() for _ = 1, listCount do stream._pointer = stream:ReadShort() * 3 + stream._pointer end end end skippers["u8u16stringarray"] = function(stream) local count = stream:ReadByte() for _=1,count do stream._pointer = stream:ReadShort() + stream._pointer end end skippers["faction"] = function(stream) stream._pointer = stream._pointer + 1 end skippers["spawnlist"] = function(stream) local count = stream:ReadByte() for _ = 1, count do stream._pointer = stream._pointer + 2 stream._pointer = stream:ReadShort() * 3 + stream._pointer end end local spawnlistSkipper = skippers["spawnlist"] skippers["trigger"] = function(stream) stream._pointer = stream:ReadByte() + stream._pointer spawnlistSkipper(stream) end local u8u24arraySkipper = skippers["u8u24array"] skippers["questgivers"] = function(stream) u8u24arraySkipper(stream) u8u24arraySkipper(stream) u8u24arraySkipper(stream) end skippers["objective"] = function(stream) local count = stream:ReadByte() for _=1,count do stream._pointer = stream._pointer + 3 stream._pointer = stream:ReadByte() + stream._pointer end end skippers["spellobjective"] = function(stream) local count = stream:ReadByte() for _=1,count do stream._pointer = stream._pointer + 3 stream._pointer = stream:ReadByte() + stream._pointer stream._pointer = stream._pointer + 3 end end local objectiveSkipper = skippers["objective"] local spellObjectiveSkipper = skippers["spellobjective"] local u24pairSkipper = skippers["u24pair"] skippers["objectives"] = function(stream) objectiveSkipper(stream) objectiveSkipper(stream) objectiveSkipper(stream) u24pairSkipper(stream) local count = stream:ReadByte() -- killobjectives if count > 0 then for _=1, count do stream._pointer = stream:ReadByte() * 3 + 3 + stream._pointer stream._pointer = stream:ReadByte() + stream._pointer end end spellObjectiveSkipper(stream) end skippers["reflist"] = function(stream) stream._pointer = stream:ReadByte() * 4 + stream._pointer end local reflistSkipper = skippers["reflist"] skippers["extraobjectives"] = function(stream) local count = stream:ReadByte() for _=1,count do spawnlistSkipper(stream) stream._pointer = stream._pointer + 3 stream._pointer = stream:ReadShort() + stream._pointer stream._pointer = stream._pointer + 3 reflistSkipper(stream) end end QuestieDBCompiler.skippers = skippers QuestieDBCompiler.dynamics = { ["u8string"] = true, ["u16string"] = true, ["u8u16array"] = true, ["u8s16pairs"] = true, ["u16u16array"] = true, ["u8s24pairs"] = true, ["u8u24array"] = true, ["u16u24array"] = true, ["u8u16stringarray"] = true, ["spawnlist"] = true, ["trigger"] = true, ["objective"] = true, ["objectives"] = true, ["questgivers"] = true, ["waypointlist"] = true, ["extraobjectives"] = true, ["reflist"] = true } QuestieDBCompiler.statics = { ["u8"] = 1, ["s8"] = 1, ["u16"] = 2, ["s16"] = 2, ["u24"] = 3, ["s24"] = 3, ["u32"] = 4, ["faction"] = 1, ["u12pair"] = 3, ["u24pair"] = 6, ["s24pair"] = 6, } function QuestieDBCompiler:CompileNPCs() QuestieDBCompiler:CompileTableCoroutine(QuestieDB.npcData, QuestieDB.npcCompilerTypes, QuestieDB.npcCompilerOrder, QuestieDB.npcKeys, "npc", "NPC") end function QuestieDBCompiler:CompileObjects() QuestieDBCompiler:CompileTableCoroutine(QuestieDB.objectData, QuestieDB.objectCompilerTypes, QuestieDB.objectCompilerOrder, QuestieDB.objectKeys, "obj", "Object") end function QuestieDBCompiler:CompileQuests() QuestieDBCompiler:CompileTableCoroutine(QuestieDB.questData, QuestieDB.questCompilerTypes, QuestieDB.questCompilerOrder, QuestieDB.questKeys, "quest", "Quest", 28) end function QuestieDBCompiler:CompileItems() QuestieDBCompiler:CompileTableCoroutine(QuestieDB.itemData, QuestieDB.itemCompilerTypes, QuestieDB.itemCompilerOrder, QuestieDB.itemKeys, "item", "Item", 128) end local function equals(a, b) if a == nil and b == nil then return true end if a == nil or b == nil then return false end local ta = type(a) local tb = type(b) if ta ~= tb then return false end if ta == "number" then return abs(a-b) < 0.2 -- 100/4096 is the precision of the data (From Aero) elseif ta == "table" then for k,v in pairs(a) do if not equals(b[k], v) then return false end end for k,v in pairs(b) do if not equals(a[k], v) then return false end end return true else return a == b end end function QuestieDBCompiler:EncodePointerMap(stream, pointerMap) stream:reset() stream:WriteShort(0) -- placeholder local count = 0 for id, ptrs in pairs(pointerMap) do stream:WriteInt24(id) stream:WriteInt24(ptrs) count = count + 1 end stream._pointer = 1 stream:WriteShort(count) return stream:Save() end function QuestieDBCompiler:DecodePointerMap(stream) local count = stream:ReadShort() local ret = {} local i = 0 while i < count do for _ = 1, min(768, count-i) do -- steps per yield ret[stream:ReadInt24()] = stream:ReadInt24() end i = i + 768 coroutine.yield() end return ret end function QuestieDBCompiler:CompileTable(tbl, types, order, lookup) local pointerMap = {} local stream = QuestieStream:GetStream("raw") for id, entry in pairs(tbl) do pointerMap[id] = stream._pointer for _, key in pairs(order) do QuestieDBCompiler.writers[types[key]](stream, entry[lookup[key]]) end end return stream:Save(), QuestieDBCompiler:EncodePointerMap(stream, pointerMap) end function QuestieDBCompiler:CompileTableCoroutine(tbl, types, order, lookup, databaseKey, kind, entriesPerTick) local count = 0 local indexLookup = {}; local max_id = 0 for id in pairs(tbl) do assert(type(id) == "number", "CompileTableCoroutine: tbl id is not a number") if id > max_id then max_id = id end end -- iterate table tbl in numerical order to get ids in order to indexLoopup list. iterating over pairs(tbl) gives ids in non determined order for id=0,max_id do if tbl[id] then count = count + 1 indexLookup[count] = id end end count = count + 1 local index = 0 local pointerMap = {} local stream = Questie.db.profile.debugEnabled and QuestieStream:GetStream("raw_assert") or QuestieStream:GetStream("raw") -- Localize functions local pcall, type = pcall, type local writers = QuestieDBCompiler.writers local supportedTypes = QuestieDBCompiler.supportedTypes while true do coroutine.yield() for _=0,Questie.db.profile.debugEnabled and TICKS_PER_YIELD_DEBUG or (entriesPerTick or TICKS_PER_YIELD) do index = index + 1 if index == count then if Questie.IsSoD then Questie.db.global.sod[databaseKey.."Bin"] = stream:Save() Questie.db.global.sod[databaseKey.."Ptrs"] = QuestieDBCompiler:EncodePointerMap(stream, pointerMap) else Questie.db.global[databaseKey.."Bin"] = stream:Save() Questie.db.global[databaseKey.."Ptrs"] = QuestieDBCompiler:EncodePointerMap(stream, pointerMap) end stream:finished() -- relief memory pressure return end local id = indexLookup[index] -- QuestieDBCompiler.currentEntry = id -- This was only ever used for debugging. local entry = tbl[id] pointerMap[id] = stream._pointer--pointerStart for i=1, #order do local key = order[i] local v = entry[lookup[key]] local t = types[key] if v and not supportedTypes[type(v)][t] then Questie:Error("|cFFFF0000Invalid datatype!|r " .. kind .. "s[" .. tostring(id) .. "]."..key..": \"" .. type(v) .. "\" is not compatible with type \"" .. t .."\"") return end if not writers[t] then Questie:Error("Invalid datatype: " .. key .. " " .. tostring(t)) end --print(key .. "s[" .. tostring(id) .. "]."..key..": \"" .. type(v) .. "\"") local result, errorMessage = pcall(writers[t], stream, v) if not result then Questie:Error("There was an error when compiling data for "..kind.." " .. tostring(id) .. " \""..tostring(key).."\":") Questie:Error(errorMessage) error(errorMessage) end end -- tbl[id] = nil -- quicker gabage collection later end end --I do not know if these assignments are actually needed but I am too scared to remove them -- QuestieDBCompiler.pointerMap = pointerMap -- QuestieDBCompiler.stream = stream -- QuestieDBCompiler.index = index end function QuestieDBCompiler:BuildSkipMap(types, order) -- skip map is used for random access, to read specific fields in an entry without reading the whole entry local skipmap = {} local indexToKey = {} local keyToIndex = {} local ptr = 0 local haveDynamic = false local lastIndex for index = 1, #order do local key = order[index] local typ = types[key] indexToKey[index] = key keyToIndex[key] = index if not haveDynamic then skipmap[key] = ptr end if QuestieDBCompiler.dynamics[typ] then if not haveDynamic then lastIndex = index end haveDynamic = true else --print("static: " .. typ) ptr = ptr + QuestieDBCompiler.statics[typ] end end skipmap = {skipmap, lastIndex, ptr, types, order, indexToKey, keyToIndex} return skipmap end function QuestieDBCompiler:Compile() if QuestieDBCompiler._isCompiling then return end QuestieDBCompiler._isCompiling = true -- some unknown addon that is popular in china causes player_logged_in event to fire many times which triggers db compile multiple times QuestieDBCompiler.startTime = GetTime() QuestieDBCompiler.totalSize = 0 print("\124cFF4DDBFF [6/9] " .. l10n("Updating NPCs") .. "...") QuestieDBCompiler:CompileNPCs() print("\124cFF4DDBFF [7/9] " .. l10n("Updating objects") .. "...") QuestieDBCompiler:CompileObjects() print("\124cFF4DDBFF [8/9] " .. l10n("Updating quests") .. "...") QuestieDBCompiler:CompileQuests() print("\124cFF4DDBFF [9/9] " .. l10n("Updating items") .. "...") QuestieDBCompiler:CompileItems() print("\124cFFAAEEFF"..l10n("Questie DB update complete!")) Questie.db.global.dbCompiledExpansion = WOW_PROJECT_ID if Questie.IsSoD then Questie.db.global.sod.dbCompiledOnVersion = QuestieLib:GetAddonVersionString() Questie.db.global.sod.dbCompiledLang = l10n:GetUILocale() Questie.db.global.sod.dbIsCompiled = true Questie.db.global.sod.dbCompiledCount = (Questie.db.global.sod.dbCompiledCount or 0) + 1 else Questie.db.global.dbCompiledOnVersion = QuestieLib:GetAddonVersionString() Questie.db.global.dbCompiledLang = l10n:GetUILocale() Questie.db.global.dbIsCompiled = true Questie.db.global.dbCompiledCount = (Questie.db.global.dbCompiledCount or 0) + 1 end end function QuestieDBCompiler:ValidateNPCs() local npcBin, npcPtrs if Questie.IsSoD then npcBin = Questie.db.global.sod.npcBin npcPtrs = Questie.db.global.sod.npcPtrs else npcBin = Questie.db.global.npcBin npcPtrs = Questie.db.global.npcPtrs end local validator = QuestieDBCompiler:GetDBHandle(npcBin, npcPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.npcCompilerTypes, QuestieDB.npcCompilerOrder)) local count = 0 for npcId, nonCompiledData in pairs(QuestieDB.npcData) do local compiledData = validator.QueryValidator(npcId, QuestieDB.npcCompilerOrder) for id, key in pairs(QuestieDB.npcCompilerOrder) do local a = compiledData[id] local b = nonCompiledData[QuestieDB.npcKeys[key]] if type(a) == "number" and abs(a-(b or 0)) > 0.2 then Questie:Warning("Nonmatching number at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b) .. " for ID: ".. npcId) return elseif type(a) == "string" and a ~= (b or "") then Questie:Warning("Nonmatching string at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b) .. " for ID: ".. npcId) return elseif type(a) == "table" then if not equals(a, (b or {})) then Questie:Warning("Nonmatching table at " .. key .. " " .. id .. " for ID: ".. npcId) DevTools_Dump({ ["Compiled Table:"] = a, ["Base Table:"] = b }) return end end end if count == TICKS_PER_YIELD_DEBUG then count = 0 coroutine.yield() end count = count + 1 end validator.stream:finished() Questie:Debug(Questie.DEBUG_INFO, "Finished NPCs validation without issues!") end function QuestieDBCompiler:ValidateObjects() local objBin, objPtrs if Questie.IsSoD then objBin = Questie.db.global.sod.objBin objPtrs = Questie.db.global.sod.objPtrs else objBin = Questie.db.global.objBin objPtrs = Questie.db.global.objPtrs end local validator = QuestieDBCompiler:GetDBHandle(objBin, objPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.objectCompilerTypes, QuestieDB.objectCompilerOrder)) local count = 0 for objectId, nonCompiledData in pairs(QuestieDB.objectData) do local compiledData = validator.QueryValidator(objectId, QuestieDB.objectCompilerOrder) for id, key in pairs(QuestieDB.objectCompilerOrder) do local a = compiledData[id] local b = nonCompiledData[QuestieDB.objectKeys[key]] if type(a) == "number" and abs(a-(b or 0)) > 0.2 then Questie:Warning("Nonmatching number at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b) .. " for ID: ".. objectId) return elseif type(a) == "string" and a ~= (b or "") then Questie:Warning("Nonmatching string at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b) .. " for ID: ".. objectId) return elseif type(a) == "table" then if not equals(a, (b or {})) then Questie:Warning("Nonmatching table at " .. key .. " " .. id .. " for ID: ".. objectId) DevTools_Dump({ ["Compiled Table:"] = a, ["Base Table:"] = b }) return end end end if count == TICKS_PER_YIELD_DEBUG then count = 0 coroutine.yield() end count = count + 1 end validator.stream:finished() Questie:Debug(Questie.DEBUG_INFO, "Finished objects validation without issues!") end function QuestieDBCompiler:ValidateItems() local itemBin, objBin, npcBin, objPtrs, itemPtrs, npcPtrs if Questie.IsSoD then itemBin = Questie.db.global.sod.itemBin itemPtrs = Questie.db.global.sod.itemPtrs objBin = Questie.db.global.sod.objBin objPtrs = Questie.db.global.sod.objPtrs npcBin = Questie.db.global.sod.npcBin npcPtrs = Questie.db.global.sod.npcPtrs else itemBin = Questie.db.global.itemBin itemPtrs = Questie.db.global.itemPtrs objBin = Questie.db.global.objBin objPtrs = Questie.db.global.objPtrs npcBin = Questie.db.global.npcBin npcPtrs = Questie.db.global.npcPtrs end local validator = QuestieDBCompiler:GetDBHandle(itemBin, itemPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.itemCompilerTypes, QuestieDB.itemCompilerOrder)) local obj = QuestieDBCompiler:GetDBHandle(objBin, objPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.objectCompilerTypes, QuestieDB.objectCompilerOrder)) local npc = QuestieDBCompiler:GetDBHandle(npcBin, npcPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.npcCompilerTypes, QuestieDB.npcCompilerOrder)) local count = 0 for id, _ in pairs(validator.pointers) do local objDrops, npcDrops = validator.QuerySingle(id, "objectDrops"), validator.QuerySingle(id, "npcDrops") if objDrops then -- validate object drops --print("Validating objs") for _, oid in pairs(objDrops) do if not obj.QuerySingle(oid, "name") then Questie:Error("Missing object " .. tostring(oid) .. " that drops " .. (validator.QuerySingle(id, "name") or "Missing item name!") .. " " .. tostring(id)) return end end end if npcDrops then -- validate npc drops for _, nid in pairs(npcDrops) do --print("Validating npcs") if not npc.QuerySingle(nid, "name") then Questie:Error("Missing npc " .. tostring(nid)) return end end end -- todo fix this test --if false then -- local cnt = 0 for _ in pairs(toValidate) do cnt = cnt + 1 end -- print("toValidate length: " .. cnt) -- --Questie.db.global.__toValidate = toValidate -- local validData = QuestieDB:GetItem(id) -- for id,key in pairs(QuestieDB.itemCompilerOrder) do -- local a = toValidate[id] -- local b = validData[key] -- -- if type(a) == "number" and abs(a-(b or 0)) > 0.2 then -- print("Nonmatching at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b)) -- return -- elseif type(a) == "string" and a ~= (b or "") then -- print("Nonmatching at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b)) -- return -- elseif type(a) == "table" then -- if not equals(a, (b or {})) then -- print("Nonmatching at " .. key .. " " .. id) -- --__nma = a -- --__nmb = b or {} -- return -- end -- else -- print("MATCHING: " .. key) -- end -- end --end if count == TICKS_PER_YIELD_DEBUG then count = 0 coroutine.yield() end count = count + 1 end count = 0 for itemId, nonCompiledData in pairs(QuestieDB.itemData) do local compiledData = validator.QueryValidator(itemId, QuestieDB.itemCompilerOrder) for id, key in pairs(QuestieDB.itemCompilerOrder) do local a = compiledData[id] local b = nonCompiledData[QuestieDB.itemKeys[key]] if type(a) == "number" and abs(a-(b or 0)) > 0.2 then Questie:Warning("Nonmatching number at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b) .. " for ID: ".. itemId) return elseif type(a) == "string" and a ~= (b or "") then Questie:Warning("Nonmatching string at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b) .. " for ID: ".. itemId) return elseif type(a) == "table" then if not equals(a, (b or {})) then Questie:Warning("Nonmatching table at " .. key .. " " .. id .. " for ID: ".. itemId) DevTools_Dump({ ["Compiled Table:"] = a, ["Base Table:"] = b }) return end end end if count == TICKS_PER_YIELD_DEBUG then count = 0 coroutine.yield() end count = count + 1 end validator.stream:finished() obj.stream:finished() npc.stream:finished() Questie:Debug(Questie.DEBUG_INFO, "Finished items validation without issues!") end function QuestieDBCompiler:ValidateQuests() local questBin, questPtrs if Questie.IsSoD then questBin = Questie.db.global.sod.questBin questPtrs = Questie.db.global.sod.questPtrs else questBin = Questie.db.global.questBin questPtrs = Questie.db.global.questPtrs end local validator = QuestieDBCompiler:GetDBHandle(questBin, questPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.questCompilerTypes, QuestieDB.questCompilerOrder)) local playerLevel = UnitLevel("player") --! IF YOU SEE THIS FUNCTION REMOVE IT AND CHANGE IT TO QUESTIELIB FUNCTION INSTEAD local function getTbcLevel(questLevel, requiredLevel, playerLevel) if (questLevel == -1) then local level = playerLevel if (requiredLevel > level) then questLevel = requiredLevel; else questLevel = level; -- We also set the requiredLevel to the player level so the quest is not hidden without "show low level quests" requiredLevel = level; end end return questLevel, requiredLevel end -- We now only compare the nonCompiled data and the compiled data without overrides, it'll have to do. local count = 0 for questId, nonCompiledData in pairs(QuestieDB.questData) do local compiledData = validator.QueryValidator(questId, QuestieDB.questCompilerOrder) for id,key in pairs(QuestieDB.questCompilerOrder) do local a = compiledData[id] local b = nonCompiledData[QuestieDB.questKeys[key]] --Special case for questLevel if (Questie.IsTBC or Questie.IsWotlk) and (key == "questLevel" or key == "requiredLevel") then local questLevel, requiredLevel = getTbcLevel(compiledData[2], compiledData[1], playerLevel) if (key == "questLevel") then a = questLevel elseif (key == "requiredLevel") then a = requiredLevel end questLevel, requiredLevel = getTbcLevel(nonCompiledData[QuestieDB.questKeys["questLevel"]], nonCompiledData[QuestieDB.questKeys["requiredLevel"]], playerLevel) if (key == "questLevel") then b = questLevel elseif (key == "requiredLevel") then b = requiredLevel end end -- if key == "extraObjectives" then -- -- Do nothing -- else if type(a) == "number" and abs(a-(b or 0)) > 0.2 then Questie:Warning("Nonmatching number at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b) .. " for ID: ".. questId) return elseif type(a) == "string" and a ~= (b or "") then Questie:Warning("Nonmatching string at " .. key .. " " .. tostring(a) .. " ~= " .. tostring(b) .. " for ID: ".. questId) return elseif type(a) == "table" then --? This is kind of stupid, but because the compiler always has to write a int24 it will always write 0 for empty tables --? So we have to emulate the same behavior here if key == "extraObjectives" then for i = 1, #b do if not b[i][4] and a[i][4] == 0 then b[i][4] = 0 end end end if not equals(a, (b or {})) then Questie:Warning("Nonmatching table at " .. key .. " " .. id .. " for ID: ".. questId) DevTools_Dump({ ["Compiled Table:"] = a, ["Base Table:"] = b }) return end end end if count == TICKS_PER_YIELD_DEBUG then count = 0 coroutine.yield() end count = count + 1 end validator.stream:finished() Questie:Debug(Questie.DEBUG_INFO, "Finished quests validation without issues!") end function QuestieDBCompiler:GetDBHandle(data, pointers, skipMap, keyToRootIndex, overrides) local handle = {} local map, lastIndex, lastPtr, types, _, indexToKey, keyToIndex = unpack(skipMap) local stream = QuestieStream:GetStream("raw") coroutine.yield() stream:Load(pointers) coroutine.yield() pointers = QuestieDBCompiler:DecodePointerMap(stream) --Questie.db.global.__pointers = pointers coroutine.yield() stream:Load(data) handle.stream = stream if overrides then ---@param id QuestId|ObjectId|ItemId|NpcId|SpellId ---@param key string ---@return any handle.QuerySingle = function(id, key) local override = overrides[id] if override then local kti = keyToRootIndex[key] if kti and override[kti] ~= nil then return override[kti] end end local ptr = pointers[id] if not ptr then --print("Entry not found! " .. id) return nil end if map[key] ~= nil then -- can skip directly stream._pointer = map[key] + ptr else -- need to skip over some variably sized data stream._pointer = lastPtr + ptr local targetIndex = keyToIndex[key] if not targetIndex then Questie:Error("ERROR: Unhandled db key: " .. key) return nil end for i = lastIndex, targetIndex-1 do skippers[types[indexToKey[i]]](stream) -- readers[types[indexToKey[i]]](stream) end end return readers[types[key]](stream) end ---@param id QuestId|ObjectId|ItemId|NpcId|SpellId ---@param keys string[] ---@return table|nil handle.Query = function(id, keys) --if overrides[id] then -- local ret = {} -- for index,key in pairs({...}) do -- local kti = keyToIndex[key] -- if kti then -- ret[index] = overrides[id][kti] -- end -- end -- return ret --end local ptr = pointers[id] local override = overrides[id] if not ptr then if override then local ret = {} for index=1,#keys do local rootIndex = keyToRootIndex[keys[index]] if rootIndex and override[rootIndex] ~= nil then ret[index] = override[rootIndex] end end return ret end return nil end local ret = {} for index=1,#keys do local key = keys[index] local rootIndex = keyToRootIndex[key] if override and rootIndex and override[rootIndex] ~= nil then ret[index] = override[rootIndex] else if map[key] ~= nil then -- can skip directly stream._pointer = map[key] + ptr else -- need to skip over some variably sized data stream._pointer = lastPtr + ptr local targetIndex = keyToIndex[key] if not targetIndex then Questie:Error("ERROR: Unhandled db key: " .. key) return nil end for i = lastIndex, targetIndex-1 do skippers[types[indexToKey[i]]](stream) end end ret[index] = readers[types[key]](stream) end end return ret -- do not unpack the returned table end ---@param id QuestId|ObjectId|ItemId|NpcId|SpellId ---@param keys string[] ---@return table|nil handle.QueryValidator = function(id, keys) local ptr = pointers[id] local override = overrides[id] if not ptr then if override then local ret = {} for index=1,#keys do local rootIndex = keyToRootIndex[keys[index]] if rootIndex and override[rootIndex] ~= nil then ret[index] = override[rootIndex] end end return ret end return nil end local ret = {} for index=1,#keys do local key = keys[index] local rootIndex = keyToRootIndex[key] if override and rootIndex and override[rootIndex] ~= nil then ret[index] = override[rootIndex] else if map[key] ~= nil then -- can skip directly stream._pointer = map[key] + ptr else -- need to skip over some variably sized data stream._pointer = lastPtr + ptr local targetIndex = keyToIndex[key] if not targetIndex then Questie:Error("ERROR: Unhandled db key: " .. key) return nil end for i = lastIndex, targetIndex-1 do local beforeSkipper = stream._pointer skippers[types[indexToKey[i]]](stream) local afterSkipper = stream._pointer stream._pointer = beforeSkipper readers[types[indexToKey[i]]](stream) local afterReader = stream._pointer if(afterSkipper ~= afterReader) then Questie:Error("ERROR: Skipper and reader did not match for key: " .. key, afterSkipper, afterReader, afterReader-afterSkipper) return nil end end end ret[index] = readers[types[key]](stream) end end return ret -- do not unpack the returned table end else handle.QuerySingle = function(id, key) local ptr = pointers[id] if not ptr then --print("Entry not found! " .. id) return nil end if map[key] ~= nil then -- can skip directly stream._pointer = map[key] + ptr else -- need to skip over some variably sized data stream._pointer = lastPtr + ptr local targetIndex = keyToIndex[key] if not targetIndex then Questie:Error("ERROR: Unhandled db key: " .. key) return nil end for i = lastIndex, targetIndex-1 do skippers[types[indexToKey[i]]](stream) -- readers[types[indexToKey[i]]](stream) end end return readers[types[key]](stream) end ---@param id QuestId|ObjectId|ItemId|NpcId|SpellId ---@param keys string[] ---@return table|nil handle.Query = function(id, keys) local ptr = pointers[id] if not ptr then --print("Entry not found! " .. id) return nil end local ret = {} for index=1,#keys do local key = keys[index] if map[key] ~= nil then -- can skip directly stream._pointer = map[key] + ptr else -- need to skip over some variably sized data stream._pointer = lastPtr + ptr local targetIndex = keyToIndex[key] if not targetIndex then Questie:Error("ERROR: Unhandled db key: " .. key) return nil end for i = lastIndex, targetIndex-1 do skippers[types[indexToKey[i]]](stream) end end ret[index] = readers[types[key]](stream) end return ret -- do not unpack the returned table end ---@param id QuestId|ObjectId|ItemId|NpcId|SpellId ---@param keys string[] ---@return table|nil handle.QueryValidator = function(id, keys) local ptr = pointers[id] if not ptr then --print("Entry not found! " .. id) return nil end local ret = {} for index=1,#keys do local key = keys[index] if map[key] ~= nil then -- can skip directly stream._pointer = map[key] + ptr else -- need to skip over some variably sized data stream._pointer = lastPtr + ptr local targetIndex = keyToIndex[key] if not targetIndex then Questie:Error("ERROR: Unhandled db key: " .. key) return nil end for i = lastIndex, targetIndex-1 do local beforeSkipper = stream._pointer skippers[types[indexToKey[i]]](stream) local afterSkipper = stream._pointer stream._pointer = beforeSkipper readers[types[indexToKey[i]]](stream) local afterReader = stream._pointer if(afterSkipper ~= afterReader) then Questie:Error("ERROR: Skipper and reader did not match for key: " .. key, afterSkipper, afterReader, afterReader-afterSkipper) return nil end end end ret[index] = readers[types[key]](stream) end return ret -- do not unpack the returned table end end handle.pointers = pointers return handle end