Dateien nach "/" hochladen
This commit is contained in:
+581
@@ -0,0 +1,581 @@
|
||||
-- Freewood-OctoWoW-Codex: Verfuegbarkeit von Quests (noch nicht annehmbar) und Voraussetzungen lernen
|
||||
--
|
||||
-- Der Server verraet nicht, welche Quests man jetzt annehmen koennte oder welche Vorquest fehlt. Das Spiel liefert nur:
|
||||
-- * die Angebotsliste eines NPCs oder Objekts, waehrend man mit ihm spricht (GOSSIP_SHOW, QUEST_GREETING, QUEST_DETAIL)
|
||||
-- * welche Quests der Charakter erledigt hat (IsQuestFlaggedCompleted / GetAllCompletedQuestIDs)
|
||||
-- Daraus entsteht ein Vergleich zweier Zeitpunkte:
|
||||
-- Negativ-Beobachtung: Eine Quest sollte hier angeboten werden, wird es aber nicht (oder der Spieler markiert sie als "noch nicht
|
||||
-- moeglich"). Gemerkt werden Stufe und die Menge der erledigten Quests.
|
||||
-- Positiv-Beobachtung: Spaeter wird sie angeboten oder angenommen. Die Vorquest muss unter den seither erledigten Quests liegen,
|
||||
-- und hat sich nur die Stufe geaendert, ist das die Mindeststufe.
|
||||
-- Die Markierung "noch nicht moeglich" gilt je Charakter (db.avail[Charakter]). Die gelernten Angaben stehen in den Quest-Daten
|
||||
-- und werden geteilt: pi (Kandidat -> Anzahl), po (Beobachtungen), mi (Stufenbereich lo/hi/n). Als Vorquest gilt ein Kandidat, der in
|
||||
-- allen Beobachtungen vorkam: fuer den eigenen Charakter sofort, fuer andere erst ab zwei Beobachtungen.
|
||||
|
||||
local ADDON, NS = ...
|
||||
|
||||
local FW = _G.Freewood
|
||||
local Codex = NS and NS.Codex
|
||||
if not FW or not Codex then
|
||||
return
|
||||
end
|
||||
|
||||
local Avail = { probe = nil }
|
||||
Codex.Avail = Avail
|
||||
NS.Avail = Avail
|
||||
|
||||
local MAX_DELTA = 6 -- mehr neu erledigte Quests sind keine brauchbare Beobachtung
|
||||
local MAX_PI = 8
|
||||
local NEG_KEEP = 60 * 24 * 3600
|
||||
local function Now() return _G.time and _G.time() or 0 end
|
||||
|
||||
local function SelfKey()
|
||||
return Codex.Trust and Codex.Trust.SelfKey and Codex.Trust.SelfKey() or nil
|
||||
end
|
||||
|
||||
local function Char(create)
|
||||
local db = Codex.db
|
||||
if not db then return nil end
|
||||
local key = SelfKey()
|
||||
if not key then return nil end
|
||||
db.avail = db.avail or {}
|
||||
local c = db.avail[key]
|
||||
if not c and create then
|
||||
c = { blocked = {}, neg = {} }
|
||||
db.avail[key] = c
|
||||
end
|
||||
return c
|
||||
end
|
||||
|
||||
local function PlayerLevel()
|
||||
local lv = _G.UnitLevel and _G.UnitLevel("player")
|
||||
return tonumber(lv) or 1
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Erledigte Quests
|
||||
------------------------------------------------------------------------
|
||||
|
||||
-- Menge der erledigten Quest-IDs des Charakters; Ersatz: bekannte IDs einzeln abfragen
|
||||
function Avail.Completed()
|
||||
local set = {}
|
||||
local QL = _G.C_QuestLog
|
||||
if QL and QL.GetAllCompletedQuestIDs then
|
||||
local ok, list = pcall(QL.GetAllCompletedQuestIDs)
|
||||
if ok and type(list) == "table" then
|
||||
for _, id in pairs(list) do set[id] = true end
|
||||
if next(set) ~= nil then return set, true end
|
||||
end
|
||||
end
|
||||
for id in pairs(Codex.V().quests) do
|
||||
if Codex.QuestStatus(id) == "done" then set[id] = true end
|
||||
end
|
||||
local c = Char(false)
|
||||
for id in pairs(c and c.neg or {}) do
|
||||
if Codex.QuestStatus(id) == "done" then set[id] = true end
|
||||
end
|
||||
return set, false
|
||||
end
|
||||
|
||||
local function ToList(set)
|
||||
local out = {}
|
||||
for id in pairs(set) do out[#out + 1] = id end
|
||||
table.sort(out)
|
||||
return out
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Markieren
|
||||
------------------------------------------------------------------------
|
||||
|
||||
-- Eine Quest als "noch nicht annehmbar" merken (src: "manual" oder "scan"). Rueckgabe: true, wenn neu
|
||||
function Avail.Block(id, src)
|
||||
id = tonumber(id)
|
||||
local c = Char(true)
|
||||
if not id or not c then return false end
|
||||
if Codex.QuestStatus(id) ~= nil then return false end
|
||||
local fresh = c.blocked[id] == nil
|
||||
local lv = PlayerLevel()
|
||||
local done = Avail.Completed()
|
||||
c.blocked[id] = { lv = lv, t = Now(), n = #ToList(done), src = src or "manual" }
|
||||
-- Schnappschuss fuer die spaetere Auswertung; die juengste Negativ-Beobachtung gilt (sie grenzt am engsten ein)
|
||||
c.neg[id] = { lv = lv, t = Now(), done = ToList(done) }
|
||||
FW:Fire("CODEX_AVAIL_CHANGED", id)
|
||||
return fresh
|
||||
end
|
||||
|
||||
function Avail.Unblock(id)
|
||||
local c = Char(false)
|
||||
if c and c.blocked[id] then
|
||||
c.blocked[id] = nil
|
||||
FW:Fire("CODEX_AVAIL_CHANGED", id)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Avail.IsBlocked(id)
|
||||
local c = Char(false)
|
||||
return c ~= nil and c.blocked[id] ~= nil
|
||||
end
|
||||
|
||||
function Avail.BlockedCount()
|
||||
local c = Char(false)
|
||||
local n = 0
|
||||
for _ in pairs(c and c.blocked or {}) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
-- Liste { id, lv, t, src, title }, sortiert nach Titel
|
||||
function Avail.List()
|
||||
local out = {}
|
||||
local c = Char(false)
|
||||
local V = Codex.V()
|
||||
for id, b in pairs(c and c.blocked or {}) do
|
||||
out[#out + 1] = { id = id, lv = b.lv, t = b.t, src = b.src, title = Codex.QuestTitle(V.quests[id], id) }
|
||||
end
|
||||
table.sort(out, function(a, b)
|
||||
if a.title ~= b.title then return a.title < b.title end
|
||||
return a.id < b.id
|
||||
end)
|
||||
return out
|
||||
end
|
||||
|
||||
function Avail.Count()
|
||||
local c = Char(false)
|
||||
local n = 0
|
||||
for _ in pairs(c and c.blocked or {}) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
function Avail.ClearAll()
|
||||
local c = Char(false)
|
||||
if c then c.blocked = {} FW:Fire("CODEX_AVAIL_CHANGED") end
|
||||
end
|
||||
|
||||
-- Aenderung am Charakter (Stufe, abgegebene Quest): markierte Quests koennten jetzt frei sein und kommen zurueck
|
||||
function Avail.Release()
|
||||
local c = Char(false)
|
||||
if not c or next(c.blocked) == nil then return 0 end
|
||||
local lv = PlayerLevel()
|
||||
local count = #ToList(Avail.Completed())
|
||||
local freed = 0
|
||||
for id, b in pairs(c.blocked) do
|
||||
if lv > (b.lv or 0) or count > (b.n or 0) or Codex.QuestStatus(id) ~= nil then
|
||||
c.blocked[id] = nil
|
||||
freed = freed + 1
|
||||
end
|
||||
end
|
||||
if freed > 0 then FW:Fire("CODEX_AVAIL_CHANGED") end
|
||||
return freed
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Auswerten (Positiv-Beobachtung)
|
||||
------------------------------------------------------------------------
|
||||
|
||||
local function Bracket(q, lo, hi)
|
||||
local mi = q.mi
|
||||
if type(mi) == "table" and mi.lo and mi.hi then
|
||||
local nlo, nhi = math.max(mi.lo, lo), math.min(mi.hi, hi)
|
||||
if nhi > nlo then
|
||||
q.mi = { lo = nlo, hi = nhi, n = (mi.n or 1) + 1 }
|
||||
return
|
||||
end
|
||||
end
|
||||
q.mi = { lo = lo, hi = hi, n = 1 }
|
||||
end
|
||||
|
||||
-- Die Quest ist jetzt verfuegbar (angeboten oder angenommen): Unterschied zum Zeitpunkt der Negativ-Beobachtung auswerten
|
||||
function Avail.Observed(id)
|
||||
local c = Char(false)
|
||||
local neg = c and c.neg[id]
|
||||
if not neg then return false end
|
||||
c.neg[id] = nil
|
||||
local V = Codex.V()
|
||||
local q = Codex.db.quests[id]
|
||||
if not q then
|
||||
local title = V.quests[id] and V.quests[id].t
|
||||
if not title then return false end -- ohne bekannten Titel laesst sich der Datensatz nicht teilen
|
||||
q = Codex:Quest(id, true)
|
||||
q.t = title
|
||||
end
|
||||
local now = Avail.Completed()
|
||||
local before = {}
|
||||
for _, p in ipairs(neg.done or {}) do before[p] = true end
|
||||
local delta = {}
|
||||
for p in pairs(now) do
|
||||
if not before[p] and p ~= id then delta[#delta + 1] = p end
|
||||
end
|
||||
table.sort(delta)
|
||||
local lv = PlayerLevel()
|
||||
local changed = false
|
||||
if #delta == 0 then
|
||||
if lv > (neg.lv or lv) then
|
||||
Bracket(q, neg.lv, lv)
|
||||
changed = true
|
||||
end
|
||||
elseif #delta <= MAX_DELTA then
|
||||
q.pi = q.pi or {}
|
||||
q.po = (q.po or 0) + 1
|
||||
local n = 0
|
||||
for _ in pairs(q.pi) do n = n + 1 end
|
||||
for _, p in ipairs(delta) do
|
||||
if q.pi[p] or n < MAX_PI then
|
||||
if not q.pi[p] then n = n + 1 end
|
||||
q.pi[p] = (q.pi[p] or 0) + 1
|
||||
end
|
||||
end
|
||||
changed = true
|
||||
end
|
||||
if changed then
|
||||
Codex.chainRev = (Codex.chainRev or 0) + 1
|
||||
FW:Fire("CODEX_CHANGED", "quest", id)
|
||||
end
|
||||
Avail.Unblock(id)
|
||||
return changed
|
||||
end
|
||||
|
||||
-- Vorquests, die sich aus den Beobachtungen ergeben: Kandidaten, die in allen Beobachtungen vorkamen.
|
||||
-- Fuer die eigene Beobachtung gilt schon eine, sonst braucht es zwei (own = true, wenn der Datensatz eigene Beobachtungen traegt)
|
||||
function Avail.InferredPre(q, own)
|
||||
local out = {}
|
||||
if type(q) ~= "table" or type(q.pi) ~= "table" then return out end
|
||||
local po = q.po or 0
|
||||
if po < (own and 1 or 2) then return out end
|
||||
for p, n in pairs(q.pi) do
|
||||
if n >= po then out[#out + 1] = p end
|
||||
end
|
||||
table.sort(out)
|
||||
return out
|
||||
end
|
||||
|
||||
-- Mindeststufe aus den Beobachtungen: nur wenn der Bereich genau eine Stufe breit ist (und zwei Beobachtungen oder eigene)
|
||||
function Avail.InferredMin(q, own)
|
||||
local mi = type(q) == "table" and q.mi or nil
|
||||
if type(mi) ~= "table" or not mi.lo or not mi.hi then return nil end
|
||||
if mi.hi - mi.lo ~= 1 then return nil end
|
||||
if (mi.n or 1) < (own and 1 or 2) then return nil end
|
||||
return mi.hi
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Scan beim Geber
|
||||
------------------------------------------------------------------------
|
||||
|
||||
-- Angebotene Quests des gerade angesprochenen NPCs oder Objekts: Menge von IDs und Menge von Titeln
|
||||
local function ReadOffers()
|
||||
local ids, titles = {}, {}
|
||||
local any = false
|
||||
local G = _G.C_GossipInfo
|
||||
if G and G.GetAvailableQuests then
|
||||
local ok, list = pcall(G.GetAvailableQuests)
|
||||
if ok and type(list) == "table" then
|
||||
any = true
|
||||
for _, e in ipairs(list) do
|
||||
if type(e) == "table" then
|
||||
if type(e.questID) == "number" then ids[e.questID] = true end
|
||||
if type(e.title) == "string" then titles[e.title] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Quest-Begruessung (alte Form): nur Titel
|
||||
if _G.GetNumAvailableQuests and _G.GetAvailableTitle then
|
||||
local ok, n = pcall(_G.GetNumAvailableQuests)
|
||||
if ok and type(n) == "number" and n > 0 then
|
||||
any = true
|
||||
for i = 1, n do
|
||||
local ok2, t = pcall(_G.GetAvailableTitle, i)
|
||||
if ok2 and type(t) == "string" then titles[t] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
return ids, titles, any
|
||||
end
|
||||
Avail.ReadOffers = ReadOffers
|
||||
|
||||
-- Wer wird gerade angesprochen: kind ("npc"/"object"), id oder nil
|
||||
local function Interaction()
|
||||
local guid = _G.UnitGUID and _G.UnitGUID("npc")
|
||||
if guid == nil then return nil end
|
||||
local ok, kind, id = pcall(Codex.ParseGUID, guid)
|
||||
if ok and kind then return kind, id end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function Probe(text)
|
||||
if Avail.probe and Now() < Avail.probe then FW:Print("Probe: " .. text) end
|
||||
end
|
||||
|
||||
-- Angebotsliste ausgewertet: ids/titles = was angeboten wird
|
||||
function Avail.ScanOffers(kind, npcID, ids, titles)
|
||||
local V = Codex.V()
|
||||
local scanned, blocked, seen = 0, 0, 0
|
||||
-- Positiv: angebotene Quests mit Negativ-Beobachtung
|
||||
for id in pairs(ids) do
|
||||
seen = seen + 1
|
||||
Avail.Observed(id)
|
||||
Avail.Unblock(id)
|
||||
end
|
||||
-- Negativ: Quests dieses Gebers, die nach den Daten annehmbar waeren, aber nicht angeboten werden
|
||||
if kind and npcID then
|
||||
for id, q in pairs(V.quests) do
|
||||
local g = q.give
|
||||
if type(g) == "table" and g.id == npcID and g.k == kind then
|
||||
scanned = scanned + 1
|
||||
local title = Codex.QuestTitle(q, id)
|
||||
if not ids[id] and not titles[title] and Codex.QuestStatus(id) == nil
|
||||
and not Avail.IsBlocked(id) and Codex.QuestAvailable(id) then
|
||||
if Avail.Block(id, "scan") then blocked = blocked + 1 end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return seen, scanned, blocked
|
||||
end
|
||||
|
||||
function Avail:OnGossip()
|
||||
local ok, err = pcall(function()
|
||||
local kind, id = Interaction()
|
||||
local ids, titles, any = ReadOffers()
|
||||
if not any then
|
||||
Probe("Gossip: keine Angebotsliste lesbar")
|
||||
return
|
||||
end
|
||||
local n = 0
|
||||
for _ in pairs(ids) do n = n + 1 end
|
||||
local t = 0
|
||||
for _ in pairs(titles) do t = t + 1 end
|
||||
local seen, scanned, blocked = Avail.ScanOffers(kind, id, ids, titles)
|
||||
Probe(string.format("Gossip: %s %s, angeboten %d (IDs) / %d (Titel), Quests dieses Gebers in den Daten %d, neu als 'noch nicht moeglich' %d",
|
||||
tostring(kind), tostring(id), n, t, scanned, blocked))
|
||||
end)
|
||||
if not ok then Probe("Fehler: " .. tostring(err)) end
|
||||
end
|
||||
|
||||
function Avail:OnQuestDetail()
|
||||
pcall(function()
|
||||
local id = tonumber(_G.GetQuestID and _G.GetQuestID())
|
||||
if id then
|
||||
Avail.Observed(id)
|
||||
Avail.Unblock(id)
|
||||
Probe("Quest-Angebot " .. id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Avail:OnQuestAccepted(a, b)
|
||||
pcall(function()
|
||||
local id = tonumber(b) or tonumber(a)
|
||||
if id then
|
||||
Avail.Observed(id)
|
||||
Avail.Unblock(id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Avail:OnChange()
|
||||
pcall(Avail.Release)
|
||||
end
|
||||
|
||||
function Avail.Prune()
|
||||
local c = Char(false)
|
||||
if not c then return end
|
||||
local now = Now()
|
||||
for id, neg in pairs(c.neg) do
|
||||
if now - (neg.t or 0) > NEG_KEEP then c.neg[id] = nil end
|
||||
end
|
||||
end
|
||||
|
||||
function Avail:Start()
|
||||
FW:RegisterEvent("GOSSIP_SHOW", self.OnGossip, self)
|
||||
FW:RegisterEvent("QUEST_GREETING", self.OnGossip, self)
|
||||
FW:RegisterEvent("QUEST_DETAIL", self.OnQuestDetail, self)
|
||||
FW:RegisterEvent("QUEST_ACCEPTED", self.OnQuestAccepted, self)
|
||||
FW:RegisterEvent("PLAYER_LEVEL_UP", self.OnChange, self)
|
||||
FW:RegisterEvent("QUEST_TURNED_IN", self.OnChange, self)
|
||||
C_Timer.After(6, function() Avail.Prune() Avail.Release() end)
|
||||
-- Antworten auf RequestLoadQuestByID zaehlen (nur fuer den Titel-Test; Ereignis kann fehlen)
|
||||
pcall(FW.RegisterEvent, FW, "QUEST_DATA_LOAD_RESULT", function() Avail.loadEvents = (Avail.loadEvents or 0) + 1 end, Avail)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Test-Befehl: /fwquestprobe [QuestID] zeigt, was Forever liefert
|
||||
------------------------------------------------------------------------
|
||||
|
||||
function Avail.RunProbe(id)
|
||||
local function Say(t) FW:Print("Probe: " .. t) end
|
||||
id = tonumber(id) or 96652
|
||||
local G, QL = _G.C_GossipInfo, _G.C_QuestLog
|
||||
Say("C_GossipInfo.GetAvailableQuests: " .. tostring(G and G.GetAvailableQuests ~= nil))
|
||||
Say("GetNumAvailableQuests / GetAvailableTitle (Begruessung): " .. tostring(_G.GetNumAvailableQuests ~= nil) .. " / " .. tostring(_G.GetAvailableTitle ~= nil))
|
||||
Say("C_QuestLog.GetAllCompletedQuestIDs: " .. tostring(QL and QL.GetAllCompletedQuestIDs ~= nil))
|
||||
if QL and QL.GetAllCompletedQuestIDs then
|
||||
local ok, list = pcall(QL.GetAllCompletedQuestIDs)
|
||||
Say(" erledigte Quests laut Spiel: " .. (ok and type(list) == "table" and #list or ("Fehler " .. tostring(list))))
|
||||
end
|
||||
Say("C_QuestLog.RequestLoadQuestByID: " .. tostring(QL and QL.RequestLoadQuestByID ~= nil) .. ", GetTitleForQuestID: " .. tostring(QL and QL.GetTitleForQuestID ~= nil))
|
||||
if QL and QL.GetTitleForQuestID then
|
||||
local ok, t = pcall(QL.GetTitleForQuestID, id)
|
||||
Say(" Titel zu " .. id .. " sofort: " .. tostring(ok and t))
|
||||
end
|
||||
if QL and QL.RequestLoadQuestByID then
|
||||
local ok, err = pcall(QL.RequestLoadQuestByID, id)
|
||||
Say(" RequestLoadQuestByID(" .. id .. "): " .. (ok and "aufgerufen" or tostring(err)))
|
||||
C_Timer.After(2, function()
|
||||
local ok2, t2 = pcall(QL.GetTitleForQuestID, id)
|
||||
Say(" Titel zu " .. id .. " nach 2 s: " .. tostring(ok2 and t2))
|
||||
end)
|
||||
end
|
||||
-- Was verraet das Spiel ueber eine geladene Quest-ID ausser dem Titel? Bekannte Abfragen einzeln versuchen.
|
||||
C_Timer.After(3, function()
|
||||
local found = 0
|
||||
local calls = {
|
||||
{ "C_QuestLog", "GetQuestDifficultyLevel" }, { "C_QuestLog", "GetQuestTagInfo" }, { "C_QuestLog", "GetQuestType" },
|
||||
{ "C_QuestLog", "GetSuggestedGroupSize" }, { "C_QuestLog", "IsQuestFlaggedCompleted" }, { "C_QuestLog", "GetQuestObjectives" },
|
||||
{ "C_QuestLog", "GetLogIndexForQuestID" }, { "C_QuestLog", "IsOnQuest" }, { "C_QuestLog", "IsQuestReplayable" },
|
||||
{ "C_QuestLog", "GetQuestInfo" }, { "C_QuestLog", "GetZoneStoryInfo" }, { "C_QuestLog", "GetQuestLogRewardMoney" },
|
||||
{ "C_QuestLog", "GetRequiredMoney" }, { "C_QuestLog", "IsFailed" }, { "C_QuestLog", "ReadyForTurnIn" },
|
||||
{ "C_QuestLog", "GetQuestDetailsTheme" }, { "C_QuestLog", "IsQuestCalling" }, { "C_QuestLog", "IsQuestTrivial" },
|
||||
{ "C_QuestLog", "GetQuestLevel" }, { "C_QuestLog", "GetMinLevel" }, { "C_QuestLog", "GetMaxLevel" },
|
||||
}
|
||||
for _, c in ipairs(calls) do
|
||||
local tbl = _G[c[1]]
|
||||
local fn = tbl and tbl[c[2]]
|
||||
if type(fn) == "function" then
|
||||
local ok, a, b = pcall(fn, id)
|
||||
local function Str(v)
|
||||
if type(v) == "table" then
|
||||
local parts, n = {}, 0
|
||||
for k, x in pairs(v) do
|
||||
n = n + 1
|
||||
if n > 6 then parts[#parts + 1] = "..." break end
|
||||
parts[#parts + 1] = tostring(k) .. "=" .. tostring(x)
|
||||
end
|
||||
return "{" .. table.concat(parts, ", ") .. "}"
|
||||
end
|
||||
return tostring(v)
|
||||
end
|
||||
if ok and a ~= nil then
|
||||
found = found + 1
|
||||
Say(" " .. c[2] .. "(" .. id .. ") = " .. Str(a) .. (b ~= nil and (", " .. Str(b)) or ""))
|
||||
elseif not ok then
|
||||
Say(" " .. c[2] .. " Fehler: " .. tostring(a))
|
||||
end
|
||||
end
|
||||
end
|
||||
Say(" weitere Angaben zur Quest " .. id .. ": " .. found .. " Abfragen mit Ergebnis")
|
||||
end)
|
||||
Avail.probe = Now() + 300
|
||||
Say("Die naechsten 5 Minuten wird bei jedem Gespraech mit einem NPC oder Objekt die Angebotsliste gemeldet.")
|
||||
local kind, nid = Interaction()
|
||||
if kind then Avail:OnGossip() end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Titel-Test: /fwquesttitles [von] [bis] fragt Quest-Titel beim Server ab (gedrosselt, nur Anzeige, nichts wird gespeichert)
|
||||
------------------------------------------------------------------------
|
||||
|
||||
local SCAN_MAX = 500 -- IDs je Lauf
|
||||
local SCAN_BATCH = 20 -- Anfragen je Schub
|
||||
local SCAN_PAUSE = 0.5 -- Sekunden zwischen den Schueben
|
||||
|
||||
-- Rueckgabe: Liste { id, title } der gefundenen Titel (nach dem Lauf an callback(list, requested, events))
|
||||
function Avail.TitleScan(lo, hi, callback)
|
||||
local QL = _G.C_QuestLog
|
||||
if not (QL and QL.RequestLoadQuestByID and QL.GetTitleForQuestID) then return false, "api" end
|
||||
lo, hi = tonumber(lo), tonumber(hi)
|
||||
if not (lo and hi) or lo < 1 then return false, "range" end
|
||||
if hi < lo then lo, hi = hi, lo end
|
||||
if hi - lo + 1 > SCAN_MAX then hi = lo + SCAN_MAX - 1 end
|
||||
if Avail.scanning then return false, "busy" end
|
||||
Avail.scanning = true
|
||||
local events0 = Avail.loadEvents or 0
|
||||
local before = {}
|
||||
for id = lo, hi do
|
||||
local ok, t = pcall(QL.GetTitleForQuestID, id)
|
||||
if ok and type(t) == "string" and t ~= "" then before[id] = true end
|
||||
end
|
||||
local nextID, requested = lo, 0
|
||||
local function Step()
|
||||
local stop = math.min(nextID + SCAN_BATCH - 1, hi)
|
||||
for id = nextID, stop do
|
||||
if pcall(QL.RequestLoadQuestByID, id) then requested = requested + 1 end
|
||||
end
|
||||
nextID = stop + 1
|
||||
if nextID <= hi then
|
||||
C_Timer.After(SCAN_PAUSE, Step)
|
||||
else
|
||||
C_Timer.After(3, function()
|
||||
local list = {}
|
||||
for id = lo, hi do
|
||||
local ok, t = pcall(QL.GetTitleForQuestID, id)
|
||||
if ok and type(t) == "string" and t ~= "" then list[#list + 1] = { id = id, title = t, cached = before[id] or nil } end
|
||||
end
|
||||
Avail.scanning = false
|
||||
Avail.lastScan = { lo = lo, hi = hi, list = list }
|
||||
if callback then callback(list, requested, (Avail.loadEvents or 0) - events0, lo, hi) end
|
||||
end)
|
||||
end
|
||||
end
|
||||
Step()
|
||||
return true, lo, hi
|
||||
end
|
||||
|
||||
SLASH_FREEWOODQUESTTITLES1 = "/fwquesttitles"
|
||||
SlashCmdList["FREEWOODQUESTTITLES"] = function(msg)
|
||||
local a, b = (msg or ""):match("(%d+)%D+(%d+)")
|
||||
a, b = tonumber(a), tonumber(b)
|
||||
if not a then
|
||||
a = tonumber((msg or ""):match("%d+")) or 96600
|
||||
b = a + 99
|
||||
end
|
||||
local ok, lo, hi = Avail.TitleScan(a, b, function(list, requested, events, l, h)
|
||||
local V = Codex.V()
|
||||
local known, new = 0, 0
|
||||
for _, e in ipairs(list) do
|
||||
if V.quests[e.id] then known = known + 1 else new = new + 1 end
|
||||
end
|
||||
FW:Print(string.format("Titel-Test %d-%d: %d Anfragen, %d Antworten (Ereignis %d), davon %d dem Codex schon bekannt, %d neu",
|
||||
l, h, requested, #list, events, known, new))
|
||||
for i = 1, math.min(#list, 12) do
|
||||
local e = list[i]
|
||||
FW:Print(string.format(" #%d %s%s", e.id, e.title, V.quests[e.id] and "" or " (neu)"))
|
||||
end
|
||||
if #list > 12 then FW:Print(" ... und " .. (#list - 12) .. " weitere") end
|
||||
end)
|
||||
if not ok then
|
||||
FW:Print("Titel-Test nicht moeglich: " .. tostring(lo))
|
||||
else
|
||||
FW:Print(string.format("Titel-Test %d-%d laeuft (gedrosselt, dauert etwa %d s) ...", lo, hi, math.ceil((hi - lo + 1) / SCAN_BATCH * SCAN_PAUSE) + 3))
|
||||
end
|
||||
end
|
||||
|
||||
SLASH_FREEWOODQUESTPROBE1 = "/fwquestprobe"
|
||||
SlashCmdList["FREEWOODQUESTPROBE"] = function(msg)
|
||||
Avail.RunProbe((msg or ""):match("%d+"))
|
||||
end
|
||||
|
||||
-- Markierungen verwalten: /fwblock (Liste), /fwblock <ID> (aufheben), /fwblock alle (alle aufheben)
|
||||
SLASH_FREEWOODBLOCK1 = "/fwblock"
|
||||
SlashCmdList["FREEWOODBLOCK"] = function(msg)
|
||||
msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local L = FW.L
|
||||
if msg == "alle" or msg == "all" then
|
||||
Avail.ClearAll()
|
||||
FW:Print(L.CODEX_AVAIL_CLEARED)
|
||||
return
|
||||
end
|
||||
local id = tonumber(msg:match("%d+"))
|
||||
if id then
|
||||
Avail.Unblock(id)
|
||||
FW:Print(string.format(L.CODEX_AVAIL_UNBLOCKED, id))
|
||||
return
|
||||
end
|
||||
local list = Avail.List()
|
||||
FW:Print(string.format(L.CODEX_AVAIL_LIST, #list))
|
||||
for _, e in ipairs(list) do
|
||||
FW:Print(string.format(" %s (#%d, Stufe %s)", e.title, e.id, tostring(e.lv)))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,799 @@
|
||||
-- Freewood-OctoWoW-Codex: Quest-Katalog (Titel und Stufe aller Quest-IDs, die der Server kennt)
|
||||
--
|
||||
-- Der Server liefert zu jeder Quest-ID auf Anfrage (C_QuestLog.RequestLoadQuestByID) den Titel und die Stufe
|
||||
-- (GetTitleForQuestID, GetQuestDifficultyLevel), auch fuer Quests, die man nie gesehen hat. Der Katalog fragt die IDs
|
||||
-- gedrosselt ab und merkt sich nur "ID gibt es" mit Titel und Stufe. Geber, Ort, Ziele und Vorquests liefert er nicht;
|
||||
-- die lernt der Codex weiter beim Spielen.
|
||||
--
|
||||
-- /fwquestkatalog setzt den Lauf dort fort, wo er aufgehoert hat (Quest-IDs 1 bis 120000)
|
||||
-- /fwquestkatalog <von> <bis> nur diesen Bereich
|
||||
-- /fwquestkatalog stop|status|neu
|
||||
-- /fwkatalog <q|i|n> [von bis | stop | status | neu | test <id>] dasselbe fuer Quests (q), Items (i) und NPCs (n)
|
||||
--
|
||||
-- Items: C_Item.RequestLoadItemDataByID, danach GetItemInfo (Qualitaet, Stufe, Mindeststufe, Verkaufspreis, Name).
|
||||
-- NPCs: es gibt keine Abfrage nur mit ID; der Name kommt aus dem Tooltip eines Kreatur-Links (C_TooltipInfo.GetHyperlink).
|
||||
--
|
||||
-- Quest-Einzelheiten (0.1.57): C_QuestLog.GetQuestDetails liefert aus derselben Antwort Ziele (Kreatur/Objekt/Item mit ID
|
||||
-- und Anzahl), feste und Wahl-Belohnungen, Geld, Startitem, gelehrten Zauber, Typ, Flags und die Texte. Ablage in
|
||||
-- db.cat.qd[id]. Nicht dabei (schickt der Server nicht): Geber, Abgabe, Vorquests, Rasse, Klasse, Zeitlimit.
|
||||
-- /fwquestkatalog details holt die Einzelheiten fuer alle bekannten Quest-IDs nach
|
||||
--
|
||||
-- Ablage: db.cat = { loc = Sprache, pos/posi/posn = naechste ID, q = { [id] = "Stufe|Titel" }, i = { [id] = "Qualitaet|Stufe|Mindeststufe|Preis|Name" }, n = { [id] = "Name" } }. Die Startdaten (Freewood-OctoWoW-ForeverData)
|
||||
-- bringen einen fertigen Katalog mit (extra.cat); der eigene geht vor.
|
||||
|
||||
local ADDON, NS = ...
|
||||
|
||||
local FW = _G.Freewood
|
||||
local Codex = NS and NS.Codex
|
||||
if not FW or not Codex then
|
||||
return
|
||||
end
|
||||
|
||||
local Cat = { rev = 0 }
|
||||
Codex.Catalog = Cat
|
||||
NS.Catalog = Cat
|
||||
|
||||
local MAX_ID = 250000 -- Quests (Standard, wenn kein Bereich angegeben ist)
|
||||
local BATCH = 10 -- Anfragen je Schub
|
||||
local PAUSE = 1.0 -- Sekunden zwischen den Schueben (ca. 10 Anfragen/s)
|
||||
local HARVEST = 4 -- Sekunden bis zum Einsammeln der Antworten eines Schubs
|
||||
|
||||
local function Now() return _G.GetTime and _G.GetTime() or 0 end
|
||||
|
||||
local function Store()
|
||||
local db = Codex.db
|
||||
if not db then return nil end
|
||||
db.cat = db.cat or {}
|
||||
local c = db.cat
|
||||
c.q = c.q or {}
|
||||
c.i = c.i or {}
|
||||
c.n = c.n or {}
|
||||
c.qd = c.qd or {}
|
||||
c.c = c.c or {}
|
||||
c.o = c.o or {}
|
||||
if not c.loc then c.loc = _G.GetLocale and _G.GetLocale() or nil end
|
||||
return c
|
||||
end
|
||||
|
||||
local function Parse(s)
|
||||
if type(s) ~= "string" then return nil end
|
||||
local lv, t = s:match("^(%d+)|(.+)$")
|
||||
if not t then return nil end
|
||||
lv = tonumber(lv)
|
||||
return t, (lv and lv > 0) and lv or nil
|
||||
end
|
||||
|
||||
-- Mitgelieferter Katalog (Startdaten)
|
||||
local function Builtin()
|
||||
local f = Codex.Forever
|
||||
if f and f.Disabled and f.Disabled() then return nil end
|
||||
local g = _G.FreewoodForeverData
|
||||
local ex = g and g.ready and type(g.extra) == "table" and g.extra.cat
|
||||
if type(ex) == "table" and type(ex.q) == "table" then return ex end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Titel, Stufe der Quest aus dem Katalog (eigener zuerst, dann die Startdaten)
|
||||
function Cat.Get(id)
|
||||
if type(id) ~= "number" then return nil end
|
||||
local db = Codex.db
|
||||
local own = db and db.cat and db.cat.q and db.cat.q[id]
|
||||
local t, lv = Parse(own)
|
||||
if t then return t, lv or (Codex.WdbLevel and Codex.WdbLevel[id]) end
|
||||
local b = Builtin()
|
||||
if b then
|
||||
t, lv = Parse(b.q[id])
|
||||
if t then return t, lv or (Codex.WdbLevel and Codex.WdbLevel[id]) end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Einzelheiten einer Quest (entpackt) oder nil:
|
||||
-- { req = { { kind = "monster"|"object"|"item", id, count } }, rew = { { id, count } }, choice = { ... },
|
||||
-- money, src, spell, flags, qtype, poi = { map, x, y }, desc, obj, done, objText = { [Nr] = Text } }
|
||||
local detCache = setmetatable({}, { __mode = "k" })
|
||||
local function Unpack(list)
|
||||
local out = {}
|
||||
if type(list) ~= "string" then return out end
|
||||
for id, n in list:gmatch("(%d+)x(%d+)") do out[#out + 1] = { id = tonumber(id), count = tonumber(n) } end
|
||||
return out
|
||||
end
|
||||
|
||||
function Cat.Details(id)
|
||||
local db = Codex.db
|
||||
local r = db and db.cat and db.cat.qd and db.cat.qd[id]
|
||||
if type(r) ~= "table" then return nil end
|
||||
local hit = detCache[r]
|
||||
if hit then return hit end
|
||||
local d = { req = {}, rew = Unpack(r.w), choice = Unpack(r.c), money = r.m, src = r.s, spell = r.sp, flags = r.f,
|
||||
qtype = r.t, desc = r.d, obj = r.o, done = r.e, objText = {} }
|
||||
if type(r.r) == "string" then
|
||||
for code, rid, n in r.r:gmatch("(%a)(%d+)x(%d+)") do
|
||||
rid = tonumber(rid)
|
||||
-- Objekte kommen als negative Zahl (32 Bit) an: 2147483648 - ID
|
||||
if code == "o" and rid > 1073741824 then rid = 2147483648 - rid end
|
||||
d.req[#d.req + 1] = { kind = code == "m" and "monster" or (code == "o" and "object" or "item"), id = rid, count = tonumber(n) }
|
||||
end
|
||||
end
|
||||
if type(r.ot) == "string" then
|
||||
for nr, t in r.ot:gmatch("(%d+)=([^;]*)") do d.objText[tonumber(nr)] = t end
|
||||
end
|
||||
if type(r.p) == "string" then
|
||||
local m, x, y = r.p:match("^(%d+):([%d%.]+):([%d%.]+)$")
|
||||
if m then d.poi = { map = tonumber(m), x = tonumber(x), y = tonumber(y) } end
|
||||
end
|
||||
detCache[r] = d
|
||||
return d
|
||||
end
|
||||
|
||||
function Cat.DetailCount()
|
||||
local n = 0
|
||||
local db = Codex.db
|
||||
for _ in pairs(db and db.cat and db.cat.qd or {}) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
-- Rueckverweise aus den Einzelheiten: { monster = { [id] = { questID, ... } }, object = ..., item = ... (Ziel-Item),
|
||||
-- reward = ... (feste Belohnung oder Wahl), src = ... (Startitem) } - gebaut je Stand des Katalogs
|
||||
local revIdx, revIdxRev
|
||||
function Cat.Reverse()
|
||||
if revIdx and revIdxRev == Cat.rev then return revIdx end
|
||||
local idx = { monster = {}, object = {}, item = {}, reward = {}, src = {} }
|
||||
local db = Codex.db
|
||||
local function Add(group, id, qid)
|
||||
local t = idx[group][id]
|
||||
if not t then t = {} idx[group][id] = t end
|
||||
if t[#t] ~= qid then t[#t + 1] = qid end
|
||||
end
|
||||
local ids = {}
|
||||
for qid in pairs(db and db.cat and db.cat.qd or {}) do
|
||||
if type(qid) == "number" then ids[#ids + 1] = qid end
|
||||
end
|
||||
table.sort(ids)
|
||||
for _, qid in ipairs(ids) do
|
||||
local d = Cat.Details(qid)
|
||||
if d then
|
||||
for _, r in ipairs(d.req) do Add(r.kind, r.id, qid) end
|
||||
for _, it in ipairs(d.rew) do Add("reward", it.id, qid) end
|
||||
for _, it in ipairs(d.choice) do Add("reward", it.id, qid) end
|
||||
if d.src then Add("src", d.src, qid) end
|
||||
end
|
||||
end
|
||||
revIdx, revIdxRev = idx, Cat.rev
|
||||
return idx
|
||||
end
|
||||
|
||||
-- Stand des Katalogs (aendert sich mit neuen eigenen Titeln und mit dem Laden oder Abschalten der Startdaten)
|
||||
function Cat.Rev()
|
||||
local b = Builtin()
|
||||
return Cat.rev .. ":" .. (b and tostring(b) or "-")
|
||||
end
|
||||
|
||||
-- Alle IDs des Katalogs (eigene und mitgelieferte), sortiert
|
||||
local idCache, idRev
|
||||
function Cat.IDs()
|
||||
local rev = Cat.Rev()
|
||||
if idCache and idRev == rev then return idCache end
|
||||
local seen, out = {}, {}
|
||||
local db = Codex.db
|
||||
for id in pairs(db and db.cat and db.cat.q or {}) do
|
||||
if type(id) == "number" then seen[id] = true out[#out + 1] = id end
|
||||
end
|
||||
local b = Builtin()
|
||||
if b then
|
||||
for id in pairs(b.q) do
|
||||
if type(id) == "number" and not seen[id] then seen[id] = true out[#out + 1] = id end
|
||||
end
|
||||
end
|
||||
table.sort(out)
|
||||
idCache, idRev = out, rev
|
||||
return out
|
||||
end
|
||||
|
||||
function Cat.Count()
|
||||
local db = Codex.db
|
||||
local own = 0
|
||||
for _ in pairs(db and db.cat and db.cat.q or {}) do own = own + 1 end
|
||||
return own, #Cat.IDs()
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Arten: q = Quests, i = Items, n = NPCs
|
||||
------------------------------------------------------------------------
|
||||
|
||||
local function CleanName(t)
|
||||
t = tostring(t):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|", "/")
|
||||
return t
|
||||
end
|
||||
|
||||
local function ReqQuest(id)
|
||||
local QL = _G.C_QuestLog
|
||||
pcall(QL.RequestLoadQuestByID, id)
|
||||
end
|
||||
|
||||
local function ReadQuest(id)
|
||||
local QL = _G.C_QuestLog
|
||||
local ok, t = pcall(QL.GetTitleForQuestID, id)
|
||||
if not (ok and type(t) == "string" and t ~= "") then return nil end
|
||||
local lv = 0
|
||||
if QL.GetQuestDifficultyLevel then
|
||||
local ok2, v = pcall(QL.GetQuestDifficultyLevel, id)
|
||||
if ok2 and type(v) == "number" and v > 0 and v < 200 then lv = math.floor(v) end
|
||||
end
|
||||
return lv .. "|" .. CleanName(t)
|
||||
end
|
||||
|
||||
-- Einzelheiten einer Quest aus der Server-Antwort (nil, wenn nicht geladen). Kurze Schluessel, Listen als Text:
|
||||
-- r = Ziele "m6x10;o1617x1;i182x1" (m Kreatur, o Objekt, i Item), w = feste Belohnung "60x1", c = Wahl "6076x1;60x1"
|
||||
-- m = Geld, s = Startitem, sp = gelehrter Zauber, f = Flags, t = Typ, p = Kartenpunkt "Karte:x:y"
|
||||
-- d = Beschreibung, o = Zieltext, e = Abschlusstext, ot = Zieltexte je Ziel "1=Text;2=Text" (nur wenn vorhanden)
|
||||
local function Txt(v)
|
||||
if type(v) ~= "string" or v == "" then return nil end
|
||||
return (v:gsub("|", "/"))
|
||||
end
|
||||
|
||||
local function PairsText(list)
|
||||
if type(list) ~= "table" then return nil end
|
||||
local out = {}
|
||||
for i = 1, #list do
|
||||
local it = list[i]
|
||||
if type(it) == "table" and tonumber(it.id) and it.id > 0 then
|
||||
out[#out + 1] = math.floor(it.id) .. "x" .. math.floor(tonumber(it.count) or 1)
|
||||
end
|
||||
end
|
||||
if #out == 0 then return nil end
|
||||
return table.concat(out, ";")
|
||||
end
|
||||
|
||||
local KIND_CODE = { monster = "m", object = "o", item = "i" }
|
||||
|
||||
local function ReadDetails(id)
|
||||
local QL = _G.C_QuestLog
|
||||
if not (QL and QL.GetQuestDetails) then return nil end
|
||||
local ok, d = pcall(QL.GetQuestDetails, id)
|
||||
if not (ok and type(d) == "table") then return nil end
|
||||
local rec = {}
|
||||
local req, ot = {}, {}
|
||||
for i = 1, #(d.requirements or {}) do
|
||||
local r = d.requirements[i]
|
||||
local code = r and KIND_CODE[r.kind]
|
||||
if code and tonumber(r.id) then
|
||||
req[#req + 1] = code .. math.floor(r.id) .. "x" .. math.floor(tonumber(r.count) or 1)
|
||||
local t = Txt(r.text)
|
||||
if t then ot[#ot + 1] = #req .. "=" .. t:gsub(";", ",") end
|
||||
end
|
||||
end
|
||||
if #req > 0 then rec.r = table.concat(req, ";") end
|
||||
if #ot > 0 then rec.ot = table.concat(ot, ";") end
|
||||
rec.w = PairsText(d.rewardItems)
|
||||
rec.c = PairsText(d.choiceItems)
|
||||
local function N(v) v = tonumber(v) return (v and v > 0) and math.floor(v) or nil end
|
||||
rec.m = N(d.rewardMoney)
|
||||
rec.s = N(d.srcItemID)
|
||||
rec.sp = N(d.rewardSpellID)
|
||||
rec.f = N(d.questFlags)
|
||||
rec.t = Txt(d.questType)
|
||||
if type(d.poi) == "table" and N(d.poi.mapID) then
|
||||
rec.p = math.floor(d.poi.mapID) .. ":" .. string.format("%.3f", tonumber(d.poi.x) or 0) .. ":" .. string.format("%.3f", tonumber(d.poi.y) or 0)
|
||||
end
|
||||
rec.d = Txt(d.description)
|
||||
rec.o = Txt(d.objectives)
|
||||
rec.e = Txt(d.completionText)
|
||||
return rec
|
||||
end
|
||||
|
||||
local function ItemInfo(id)
|
||||
local CI = _G.C_Item
|
||||
local get = (CI and CI.GetItemInfo) or _G.GetItemInfo
|
||||
if not get then return nil end
|
||||
return get(id)
|
||||
end
|
||||
|
||||
local function ReqItem(id)
|
||||
local CI = _G.C_Item
|
||||
if CI and CI.RequestLoadItemDataByID then pcall(CI.RequestLoadItemDataByID, id)
|
||||
else pcall(ItemInfo, id) end
|
||||
end
|
||||
|
||||
local function ReadItem(id)
|
||||
local ok, name, _, quality, ilvl, minLvl, _, _, _, _, _, sell = pcall(ItemInfo, id)
|
||||
if not (ok and type(name) == "string" and name ~= "") then return nil end
|
||||
local function N(v) return math.floor(tonumber(v) or 0) end
|
||||
return string.format("%d|%d|%d|%d|%s", N(quality), N(ilvl), N(minLvl), N(sell), CleanName(name))
|
||||
end
|
||||
|
||||
local function UnitLink(id)
|
||||
return "unit:Creature-0-0-0-0-" .. id .. "-0000000000"
|
||||
end
|
||||
|
||||
local function NpcData(id)
|
||||
local TI = _G.C_TooltipInfo
|
||||
if not (TI and TI.GetHyperlink) then return nil end
|
||||
local ok, data = pcall(TI.GetHyperlink, UnitLink(id))
|
||||
if ok and type(data) == "table" then return data end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ReqNpc(id) NpcData(id) end
|
||||
|
||||
local function ReadNpc(id)
|
||||
local data = NpcData(id)
|
||||
local line = data and data.lines and data.lines[1]
|
||||
local name = line and line.leftText
|
||||
if type(name) ~= "string" or name == "" then return nil end
|
||||
return CleanName(name)
|
||||
end
|
||||
|
||||
-- Kreaturen und Objekte ueber die Server-Abfrage (ClassicAPI: C_CreatureInfo, C_GameObjectInfo).
|
||||
-- Ablage: "Feld=Wert;Feld=Wert|Name|Untertitel" - alle einfachen Felder der Antwort werden mitgenommen
|
||||
local function Pack(info)
|
||||
local keys = {}
|
||||
for k, v in pairs(info) do
|
||||
local tv = type(v)
|
||||
if k ~= "name" and k ~= "subName" and type(k) == "string" and (tv == "number" or tv == "boolean" or tv == "table") then
|
||||
keys[#keys + 1] = k
|
||||
end
|
||||
end
|
||||
table.sort(keys)
|
||||
local parts = {}
|
||||
for _, k in ipairs(keys) do
|
||||
local v = info[k]
|
||||
if type(v) == "number" then
|
||||
parts[#parts + 1] = k .. "=" .. (v == math.floor(v) and string.format("%d", v) or string.format("%.3f", v))
|
||||
elseif type(v) == "boolean" then
|
||||
parts[#parts + 1] = k .. "=" .. (v and "1" or "0")
|
||||
else
|
||||
local list = {}
|
||||
for i = 1, 24 do
|
||||
if type(v[i]) == "number" then list[#list + 1] = string.format("%d", math.floor(v[i])) else break end
|
||||
end
|
||||
if #list > 0 then parts[#parts + 1] = k .. "=" .. table.concat(list, ",") end
|
||||
end
|
||||
end
|
||||
local sub = type(info.subName) == "string" and info.subName ~= "" and info.subName or ""
|
||||
return table.concat(parts, ";") .. "|" .. CleanName(info.name) .. "|" .. CleanName(sub)
|
||||
end
|
||||
|
||||
local function Entity(kind, id)
|
||||
local C = kind == "c" and _G.C_CreatureInfo or _G.C_GameObjectInfo
|
||||
local get = C and (kind == "c" and C.GetCreatureInfoByID or C.GetGameObjectInfoByID)
|
||||
if not get then return nil end
|
||||
local ok, info = pcall(get, id)
|
||||
if ok and type(info) == "table" and type(info.name) == "string" and info.name ~= "" then return info end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ReqEntity(kind, id)
|
||||
local C = kind == "c" and _G.C_CreatureInfo or _G.C_GameObjectInfo
|
||||
local req = C and (kind == "c" and C.RequestLoadCreatureByID or C.RequestLoadGameObjectByID)
|
||||
if req then pcall(req, id) end
|
||||
end
|
||||
|
||||
local function ReadEntity(kind, id)
|
||||
local info = Entity(kind, id)
|
||||
return info and Pack(info) or nil
|
||||
end
|
||||
|
||||
local KINDS = {
|
||||
q = { store = "q", pos = "pos", max = MAX_ID, req = ReqQuest, read = ReadQuest, label = "Quests",
|
||||
ready = function() local QL = _G.C_QuestLog return QL and QL.RequestLoadQuestByID and QL.GetTitleForQuestID end },
|
||||
i = { store = "i", pos = "posi", max = 250000, req = ReqItem, read = ReadItem, label = "Items",
|
||||
ready = function() return (_G.C_Item and (_G.C_Item.GetItemInfo or _G.C_Item.RequestLoadItemDataByID)) or _G.GetItemInfo end },
|
||||
n = { store = "n", pos = "posn", max = 250000, req = ReqNpc, read = ReadNpc, label = "NPCs",
|
||||
ready = function() return _G.C_TooltipInfo and _G.C_TooltipInfo.GetHyperlink end },
|
||||
c = { store = "c", pos = "posc", max = 150000, req = function(id) ReqEntity("c", id) end, read = function(id) return ReadEntity("c", id) end,
|
||||
label = "Kreaturen", ready = function() local C = _G.C_CreatureInfo return C and C.RequestLoadCreatureByID and C.GetCreatureInfoByID end },
|
||||
o = { store = "o", pos = "poso", max = 250000, req = function(id) ReqEntity("o", id) end, read = function(id) return ReadEntity("o", id) end,
|
||||
label = "Objekte", ready = function() local C = _G.C_GameObjectInfo return C and C.RequestLoadGameObjectByID and C.GetGameObjectInfoByID end },
|
||||
}
|
||||
Cat.KINDS = KINDS
|
||||
|
||||
-- Item aus dem Katalog: Qualitaet, Stufe, Mindeststufe, Verkaufspreis, Name
|
||||
function Cat.GetItem(id)
|
||||
local function P(s)
|
||||
if type(s) ~= "string" then return nil end
|
||||
local q, il, ml, sp, nm = s:match("^(%d+)|(%d+)|(%d+)|(%d+)|(.+)$")
|
||||
if not nm then return nil end
|
||||
return nm, tonumber(q), tonumber(il), tonumber(ml), tonumber(sp)
|
||||
end
|
||||
local db = Codex.db
|
||||
local r = { P(db and db.cat and db.cat.i and db.cat.i[id]) }
|
||||
if r[1] then return unpack(r) end
|
||||
local b = Builtin()
|
||||
return P(b and b.i and b.i[id])
|
||||
end
|
||||
|
||||
-- NPC-Name aus dem Katalog
|
||||
function Cat.GetNpc(id)
|
||||
local db = Codex.db
|
||||
local own = db and db.cat and db.cat.n and db.cat.n[id]
|
||||
if type(own) == "string" then return own end
|
||||
local b = Builtin()
|
||||
local v = b and b.n and b.n[id]
|
||||
return type(v) == "string" and v or nil
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Lauf
|
||||
------------------------------------------------------------------------
|
||||
|
||||
local function Say(text) FW:Print("Katalog: " .. text) end
|
||||
|
||||
local function CountOf(kind)
|
||||
local c = Codex.db and Codex.db.cat
|
||||
local n = 0
|
||||
for _ in pairs(c and c[KINDS[kind].store] or {}) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
local function Harvest(job, a, b)
|
||||
local K = KINDS[job.kind]
|
||||
local c = Store()
|
||||
if c then
|
||||
local tbl = c[K.store]
|
||||
for i = a, b do
|
||||
local id = job.ids and job.ids[i] or i
|
||||
local entry = K.read(id)
|
||||
if entry and tbl[id] ~= entry then
|
||||
if not tbl[id] then job.found = job.found + 1 end
|
||||
tbl[id] = entry
|
||||
Cat.rev = Cat.rev + 1
|
||||
end
|
||||
if job.kind == "q" and entry and not (c.qd and c.qd[id]) then
|
||||
local det = ReadDetails(id)
|
||||
if det then
|
||||
c.qd[id] = det
|
||||
job.details = (job.details or 0) + 1
|
||||
Cat.rev = Cat.rev + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
job.pending = job.pending - 1
|
||||
if job.nextID > job.hi and job.pending <= 0 and Cat.job == job then
|
||||
Cat.job = nil
|
||||
Say(string.format("%s fertig mit %d-%d: %d neue Eintraege, insgesamt %d eigene%s (%d Sekunden).", K.label, job.lo, job.hi, job.found, CountOf(job.kind),
|
||||
job.kind == "q" and string.format(", %d neue Einzelheiten", job.details or 0) or "", math.floor(Now() - job.t0)))
|
||||
FW:Fire("CODEX_CHANGED", "catalog")
|
||||
if job.onDone then job.onDone() end
|
||||
end
|
||||
end
|
||||
|
||||
local function Step(job)
|
||||
if Cat.job ~= job then return end
|
||||
if job.stop then
|
||||
if job.pending <= 0 then Cat.job = nil end
|
||||
return
|
||||
end
|
||||
if _G.InCombatLockdown and _G.InCombatLockdown() then
|
||||
C_Timer.After(2, function() Step(job) end)
|
||||
return
|
||||
end
|
||||
-- Bremse: bei niedriger Framerate, hoher Latenz oder zu vielen offenen
|
||||
-- Schueben nicht nachlegen, sondern warten (der Client bleibt bedienbar)
|
||||
local fps = _G.GetFramerate and _G.GetFramerate() or 60
|
||||
local lat = 0
|
||||
if _G.GetNetStats then
|
||||
local _, _, home, world = _G.GetNetStats()
|
||||
lat = math.max(tonumber(home) or 0, tonumber(world) or 0)
|
||||
end
|
||||
if fps < 15 or lat > 600 or job.pending >= 5 then
|
||||
job.waits = (job.waits or 0) + 1
|
||||
C_Timer.After(fps < 15 and 4 or 2, function() Step(job) end)
|
||||
return
|
||||
end
|
||||
local K = KINDS[job.kind]
|
||||
local a = job.nextID
|
||||
local b = math.min(a + BATCH - 1, job.hi)
|
||||
for i = a, b do K.req(job.ids and job.ids[i] or i) end
|
||||
job.nextID = b + 1
|
||||
job.pending = job.pending + 1
|
||||
local c = Store()
|
||||
if job.resume and c then c[K.pos] = job.nextID end
|
||||
C_Timer.After(HARVEST, function() Harvest(job, a, b) end)
|
||||
-- Fortschritt alle 10000 IDs
|
||||
if math.floor(a / 10000) ~= math.floor(b / 10000) then
|
||||
Say(string.format("%s: ID %d von %d ...", K.label, b, job.hi))
|
||||
end
|
||||
if job.nextID <= job.hi then
|
||||
C_Timer.After(PAUSE, function() Step(job) end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Rueckgabe: true oder false, Grund ("api", "busy", "range")
|
||||
function Cat.Start(lo, hi, resume, kind, ids)
|
||||
kind = kind or "q"
|
||||
local K = KINDS[kind]
|
||||
if not K then return false, "kind" end
|
||||
if not K.ready() then return false, "api" end
|
||||
if Cat.job then return false, "busy" end
|
||||
if not Store() then return false, "db" end
|
||||
lo = math.max(1, math.floor(tonumber(lo) or 1))
|
||||
hi = math.min(ids and #ids or K.max, math.floor(tonumber(hi) or K.max))
|
||||
if hi < lo then return false, "range" end
|
||||
local job = { kind = kind, lo = lo, hi = hi, nextID = lo, found = 0, pending = 0, t0 = Now(), resume = resume, ids = ids }
|
||||
Cat.job = job
|
||||
Step(job)
|
||||
return true, lo, hi
|
||||
end
|
||||
|
||||
function Cat.Stop()
|
||||
if Cat.job then Cat.job.stop = true end
|
||||
end
|
||||
|
||||
-- Lauf dort fortsetzen, wo er aufgehoert hat (fuer Befehl und Dev-Knopf). Rueckgabe wie Start; "done", wenn alles durch ist
|
||||
function Cat.Resume(restart, kind)
|
||||
kind = kind or "q"
|
||||
local K = KINDS[kind]
|
||||
local c = Store()
|
||||
if restart and c then c[K.pos] = 1 end
|
||||
local pos = c and c[K.pos] or 1
|
||||
if pos > K.max then return false, "done" end
|
||||
return Cat.Start(pos, K.max, true, kind)
|
||||
end
|
||||
|
||||
-- Kreatur / Objekt aus dem Katalog: Name, Untertitel, Felder (Tabelle) - oder nil
|
||||
local entCache = setmetatable({}, { __mode = "v" })
|
||||
function Cat.GetEntity(kind, id)
|
||||
local db = Codex.db
|
||||
local s = db and db.cat and db.cat[kind] and db.cat[kind][id]
|
||||
if type(s) ~= "string" then return nil end
|
||||
local hit = entCache[s]
|
||||
if hit then return hit.name, hit.sub, hit.f end
|
||||
local f, name, sub = s:match("^([^|]*)|([^|]*)|?(.*)$")
|
||||
if not name then return nil end
|
||||
local fields = {}
|
||||
for k, v in f:gmatch("(%w+)=([^;]+)") do fields[k] = tonumber(v) or v end
|
||||
entCache[s] = { name = name, sub = sub ~= "" and sub or nil, f = fields }
|
||||
return name, sub ~= "" and sub or nil, fields
|
||||
end
|
||||
function Cat.GetCreature(id) return Cat.GetEntity("c", id) end
|
||||
function Cat.GetObject(id) return Cat.GetEntity("o", id) end
|
||||
|
||||
-- Ziele aus den Quest-Einzelheiten, die noch nicht im Katalog stehen
|
||||
function Cat.TargetIDs(kind)
|
||||
local rev = Cat.Reverse()[kind == "c" and "monster" or "object"]
|
||||
local have = Codex.db and Codex.db.cat and Codex.db.cat[kind] or {}
|
||||
local ids = {}
|
||||
for id in pairs(rev) do
|
||||
if not have[id] then ids[#ids + 1] = id end
|
||||
end
|
||||
table.sort(ids)
|
||||
return ids
|
||||
end
|
||||
|
||||
-- Holt Kreaturen, danach Objekte, die in Quests als Ziel vorkommen. Rueckgabe wie Start
|
||||
function Cat.StartTargets()
|
||||
local cs, os = Cat.TargetIDs("c"), Cat.TargetIDs("o")
|
||||
local function Objects()
|
||||
if #os > 0 then Cat.Start(1, #os, false, "o", os) end
|
||||
end
|
||||
if #cs > 0 then
|
||||
local ok, a = Cat.Start(1, #cs, false, "c", cs)
|
||||
if ok then Cat.job.onDone = Objects return true, #cs + #os end
|
||||
return false, a
|
||||
end
|
||||
if #os > 0 then
|
||||
local ok, a = Cat.Start(1, #os, false, "o", os)
|
||||
if ok then return true, #os end
|
||||
return false, a
|
||||
end
|
||||
return false, "none"
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Aus dem Quest-Cache des Clients (CodexWdbChain.lua): Zone, Ruf-Ziel, Art
|
||||
------------------------------------------------------------------------
|
||||
local SORTS = {
|
||||
[-1] = { "Epic", "Episch" }, [-22] = { "Seasonal", "Saisonal" }, [-24] = { "Herbalism", "Kräuterkunde" },
|
||||
[-61] = { "Warlock", "Hexenmeister" }, [-81] = { "Warrior", "Krieger" }, [-82] = { "Shaman", "Schamane" },
|
||||
[-101] = { "Fishing", "Angeln" }, [-121] = { "Blacksmithing", "Schmiedekunst" }, [-141] = { "Paladin", "Paladin" },
|
||||
[-161] = { "Mage", "Magier" }, [-162] = { "Rogue", "Schurke" }, [-181] = { "Alchemy", "Alchemie" },
|
||||
[-182] = { "Leatherworking", "Lederverarbeitung" }, [-201] = { "Engineering", "Ingenieurskunst" },
|
||||
[-221] = { "Treasure map", "Schatzkarte" }, [-241] = { "Tournament", "Turnier" }, [-261] = { "Hunter", "Jäger" },
|
||||
[-262] = { "Priest", "Priester" }, [-263] = { "Druid", "Druide" }, [-264] = { "Tailoring", "Schneiderei" },
|
||||
[-284] = { "Special", "Besonderes" }, [-304] = { "Cooking", "Kochkunst" }, [-324] = { "First Aid", "Erste Hilfe" },
|
||||
[-344] = { "Legendary", "Legendär" }, [-364] = { "Darkmoon Faire", "Dunkelmond-Jahrmarkt" },
|
||||
[-365] = { "Ahn'Qiraj War", "Ahn'Qiraj-Krieg" }, [-366] = { "Lunar Festival", "Mondfest" },
|
||||
[-367] = { "Reputation", "Ruf" }, [-368] = { "Invasion", "Invasion" }, [-369] = { "Midsummer", "Sonnenwendfest" },
|
||||
}
|
||||
local FACTIONS = {
|
||||
[21] = "Booty Bay", [47] = "Ironforge", [54] = "Gnomeregan Exiles", [59] = "Thorium Brotherhood", [67] = "Horde",
|
||||
[68] = "Undercity", [69] = "Darnassus", [70] = "Syndicate", [72] = "Stormwind", [76] = "Orgrimmar",
|
||||
[81] = "Thunder Bluff", [87] = "Bloodsail Buccaneers", [92] = "Gelkis Clan Centaur", [93] = "Magram Clan Centaur",
|
||||
[270] = "Zandalar Tribe", [349] = "Ravenholdt", [369] = "Gadgetzan", [470] = "Ratchet", [509] = "League of Arathor",
|
||||
[510] = "The Defilers", [529] = "Argent Dawn", [530] = "Darkspear Trolls", [576] = "Timbermaw Hold",
|
||||
[577] = "Everlook", [609] = "Cenarion Circle", [729] = "Frostwolf Clan", [730] = "Stormpike Guard",
|
||||
[749] = "Hydraxian Waterlords", [809] = "Shen'dralar", [889] = "Warsong Outriders", [890] = "Silverwing Sentinels",
|
||||
[910] = "Brood of Nozdormu",
|
||||
}
|
||||
local STANDINGS = { { 42000, "Exalted", "Ehrfürchtig" }, { 21000, "Revered", "Respektvoll" }, { 9000, "Honored", "Wohlwollend" },
|
||||
{ 3000, "Friendly", "Freundlich" } }
|
||||
local INFOS = { [1] = { "Group", "Gruppe" }, [41] = { "PvP", "PvP" }, [62] = { "Raid", "Schlachtzug" },
|
||||
[81] = { "Dungeon", "Dungeon" }, [82] = { "World event", "Weltereignis" } }
|
||||
|
||||
local function Pick(t) if not t then return nil end return Codex.Lang() == "en" and t[1] or t[2] end
|
||||
|
||||
local zoneNames = {}
|
||||
function Cat.ZoneName(z)
|
||||
if type(z) ~= "number" or z == 0 then return nil end
|
||||
if z < 0 then return Pick(SORTS[z]) end
|
||||
local n = zoneNames[z]
|
||||
if n == nil then
|
||||
n = false
|
||||
local own = Codex.db and Codex.db.cat and Codex.db.cat.a and Codex.db.cat.a[z]
|
||||
if type(own) == "string" then
|
||||
n = own:match("|(.+)$") or own
|
||||
end
|
||||
local CM = _G.C_Map
|
||||
if not n and CM and CM.GetAreaInfo then
|
||||
local ok, name = pcall(CM.GetAreaInfo, z)
|
||||
if ok and type(name) == "string" and name ~= "" then n = name end
|
||||
end
|
||||
if not n and CM and CM.GetMapInfo then
|
||||
local ok, info = pcall(CM.GetMapInfo, z)
|
||||
if ok and type(info) == "table" and type(info.name) == "string" then n = info.name end
|
||||
end
|
||||
zoneNames[z] = n
|
||||
end
|
||||
return n or nil
|
||||
end
|
||||
|
||||
-- Fraktionsname: erst die Spielfunktion (Spielsprache), sonst die feste Liste
|
||||
local factionNames = {}
|
||||
function Cat.FactionName(id)
|
||||
local n = factionNames[id]
|
||||
if n == nil then
|
||||
n = false
|
||||
local f = _G.GetFactionInfoByID or (_G.C_Reputation and _G.C_Reputation.GetFactionInfoByID)
|
||||
if f then
|
||||
local ok, name = pcall(f, id)
|
||||
if ok and type(name) == "string" and name ~= "" then n = name end
|
||||
end
|
||||
if not n then
|
||||
local ok, d = pcall(function() return _G.C_Reputation.GetFactionDataByID(id) end)
|
||||
if ok and type(d) == "table" and type(d.name) == "string" then n = d.name end
|
||||
end
|
||||
factionNames[id] = n
|
||||
end
|
||||
return n or FACTIONS[id]
|
||||
end
|
||||
|
||||
-- { zone = Zonenname/Kategorie, zoneID, rep = "Name: Stufe", info = "Gruppe"/"Dungeon"/... } (nur was bekannt ist)
|
||||
function Cat.QuestMeta(id)
|
||||
local z = Codex.WdbZone and Codex.WdbZone[id]
|
||||
local r = Codex.WdbRep and Codex.WdbRep[id]
|
||||
local i = Codex.WdbInfo and Codex.WdbInfo[id]
|
||||
if not (z or r or i) then return nil end
|
||||
local m = { zoneID = z, zone = z and Cat.ZoneName(z) or nil, info = i and Pick(INFOS[i]) or nil }
|
||||
if r then
|
||||
local st
|
||||
for _, s in ipairs(STANDINGS) do if r[2] >= s[1] then st = Codex.Lang() == "en" and s[2] or s[3] break end end
|
||||
m.rep = (Cat.FactionName(r[1]) or ("#" .. r[1])) .. ": " .. (st or tostring(r[2]))
|
||||
end
|
||||
return m
|
||||
end
|
||||
|
||||
-- Quest-IDs des Katalogs, zu denen noch keine Einzelheiten gespeichert sind
|
||||
function Cat.MissingDetails()
|
||||
local have = Codex.db and Codex.db.cat and Codex.db.cat.qd or {}
|
||||
local ids = {}
|
||||
for _, id in ipairs(Cat.IDs()) do
|
||||
if not have[id] then ids[#ids + 1] = id end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
-- Holt die Einzelheiten (Ziele, Belohnungen, Texte) nach. Rueckgabe: true, Anzahl | false, Grund ("none", "busy", "api" ...)
|
||||
function Cat.StartDetails()
|
||||
local ids = Cat.MissingDetails()
|
||||
if #ids == 0 then return false, "none" end
|
||||
local ok, a = Cat.Start(1, #ids, false, "q", ids)
|
||||
if ok then return true, #ids end
|
||||
return false, a
|
||||
end
|
||||
|
||||
-- Stand fuer die Anzeige: laeuft, naechste ID, letzte ID, eigene Eintraege, gesamt (nur Quests mit Startdaten)
|
||||
function Cat.Progress(kind)
|
||||
kind = kind or "q"
|
||||
local K = KINDS[kind]
|
||||
local c = Codex.db and Codex.db.cat
|
||||
local own, all = CountOf(kind), CountOf(kind)
|
||||
if kind == "q" then own, all = Cat.Count() end
|
||||
local running = Cat.job ~= nil and not Cat.job.stop and Cat.job.kind == kind
|
||||
return running, (Cat.job and Cat.job.kind == kind and Cat.job.nextID) or (c and c[K.pos]) or 1, K.max, own, all
|
||||
end
|
||||
|
||||
local function Status(kind)
|
||||
local K = KINDS[kind]
|
||||
local c = Codex.db and Codex.db.cat
|
||||
local pos = c and c[K.pos] or 1
|
||||
local own, all = CountOf(kind), nil
|
||||
if kind == "q" then own, all = Cat.Count() end
|
||||
local line = string.format("%s: %d eigene Eintraege%s. Lauf bis ID %d von %d.", K.label, own, all and (", " .. all .. " im Katalog (mit Startdaten)") or "", math.min(pos, K.max), K.max)
|
||||
if kind == "q" then line = line .. string.format(" Einzelheiten (Ziele, Belohnungen, Texte): %d.", Cat.DetailCount()) end
|
||||
if Cat.job then line = line .. " Laeuft: " .. KINDS[Cat.job.kind].label .. " (ID " .. Cat.job.nextID .. ")." end
|
||||
Say(line)
|
||||
end
|
||||
|
||||
-- Einzelne ID pruefen (zeigt die Rohantwort, speichert nichts)
|
||||
local function Test(kind, id)
|
||||
local K = KINDS[kind]
|
||||
if not K.ready() then Say(K.label .. ": diese Abfrage bietet das Spiel nicht an.") return end
|
||||
K.req(id)
|
||||
C_Timer.After(2, function()
|
||||
local entry = K.read(id)
|
||||
Say(string.format("%s-Test ID %d: %s", K.label, id, entry or "keine Antwort (unbekannt oder noch nicht geladen)"))
|
||||
end)
|
||||
Say(string.format("%s-Test ID %d: Antwort in 2 Sekunden ...", K.label, id))
|
||||
end
|
||||
|
||||
local function Handle(kind, msg)
|
||||
msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local K = KINDS[kind]
|
||||
if msg == "stop" then
|
||||
if Cat.job then Cat.Stop() Say("wird angehalten (der Stand bleibt erhalten).") else Say("es laeuft nichts.") end
|
||||
return
|
||||
elseif msg == "status" then
|
||||
Status(kind)
|
||||
return
|
||||
end
|
||||
if kind == "q" and msg == "details" then
|
||||
local ok, n = Cat.StartDetails()
|
||||
if ok then
|
||||
Say(string.format("Einzelheiten fuer %d Quests werden geholt (ca. %d Sekunden). Anhalten: stop.", n, math.ceil(n / (BATCH / PAUSE))))
|
||||
elseif n == "none" then
|
||||
Say("alle bekannten Quests haben schon Einzelheiten (" .. Cat.DetailCount() .. ").")
|
||||
elseif n == "busy" then
|
||||
Say("es laeuft schon ein Lauf (stop zum Anhalten).")
|
||||
else
|
||||
Say("nicht moeglich: " .. tostring(n))
|
||||
end
|
||||
return
|
||||
end
|
||||
if (kind == "c" or kind == "o") and msg == "ziele" then
|
||||
local ok, n = Cat.StartTargets()
|
||||
if ok then
|
||||
Say(string.format("%d Ziele aus Quests (Kreaturen, dann Objekte) werden geholt. Anhalten: stop.", n))
|
||||
elseif n == "none" then
|
||||
Say("alle Ziele aus den Quests sind schon im Katalog.")
|
||||
else
|
||||
Say("nicht moeglich: " .. tostring(n))
|
||||
end
|
||||
return
|
||||
end
|
||||
local tid = msg:match("^test%s+(%d+)$")
|
||||
if tid then Test(kind, tonumber(tid)) return end
|
||||
local lo, hi = msg:match("^(%d+)%D+(%d+)$")
|
||||
local ok, a, b
|
||||
if lo then
|
||||
ok, a, b = Cat.Start(tonumber(lo), tonumber(hi), false, kind)
|
||||
else
|
||||
ok, a, b = Cat.Resume(msg == "neu", kind)
|
||||
if a == "done" then
|
||||
Say(K.label .. ": bis ID " .. K.max .. " durchgelaufen. Mit neu beginnt ein neuer Lauf von vorn (vorhandene Eintraege bleiben).")
|
||||
return
|
||||
end
|
||||
end
|
||||
if ok then
|
||||
Say(string.format("%s: Lauf %d-%d gestartet (gedrosselt, ca. %d Anfragen pro Sekunde). Anhalten: stop. Du kannst weiterspielen.",
|
||||
K.label, a, b, math.floor(BATCH / PAUSE)))
|
||||
elseif a == "busy" then
|
||||
Say("es laeuft schon ein Lauf (stop zum Anhalten).")
|
||||
elseif a == "api" then
|
||||
Say(K.label .. ": diese Abfrage bietet diese Version von Forever nicht an.")
|
||||
else
|
||||
Say("Bereich nicht moeglich.")
|
||||
end
|
||||
end
|
||||
|
||||
SLASH_FREEWOODQUESTCAT1 = "/fwquestkatalog"
|
||||
SlashCmdList["FREEWOODQUESTCAT"] = function(msg) Handle("q", msg) end
|
||||
|
||||
SLASH_FREEWOODCAT1 = "/fwkatalog"
|
||||
SlashCmdList["FREEWOODCAT"] = function(msg)
|
||||
msg = (msg or ""):lower():gsub("^%s+", "")
|
||||
local k, rest = msg:match("^(%a)%s*(.*)$")
|
||||
if k == "q" or k == "i" or k == "n" or k == "c" or k == "o" then Handle(k, rest) return end
|
||||
Say("benutze /fwkatalog q|i|n [von bis | stop | status | neu | test <id>] (q = Quests, i = Items, n = NPC-Namen, c = Kreaturen, o = Objekte); q details holt Ziele, Belohnungen und Texte nach; c/o ziele holt nur die Ziele aus den Quests")
|
||||
end
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
-- Freewood-OctoWoW-Codex: Favoriten und Notizen sichern / uebertragen (Export und Import als Text)
|
||||
|
||||
local ADDON, NS = ...
|
||||
|
||||
local FW = _G.Freewood
|
||||
local Codex = NS.Codex
|
||||
if not FW or not Codex or not FW.UI then
|
||||
return
|
||||
end
|
||||
|
||||
local UI = FW.UI
|
||||
local FavUI = {}
|
||||
NS.FavUI = FavUI
|
||||
Codex.FavUI = FavUI
|
||||
|
||||
local win
|
||||
|
||||
local function Build()
|
||||
local L = FW.L
|
||||
local w = UI.CreatePanel(UIParent, "bg", "gold")
|
||||
w:SetSize(520, 190)
|
||||
w:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
w:SetFrameStrata("DIALOG")
|
||||
w:EnableMouse(true)
|
||||
w:Hide()
|
||||
|
||||
local title = UI.CreateLabel(w, L.CODEX_FAVX_TITLE, { font = "heading", size = 14, color = "goldLight" })
|
||||
title:SetPoint("TOPLEFT", w, "TOPLEFT", 16, -14)
|
||||
local note = UI.CreateLabel(w, L.CODEX_FAVX_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
|
||||
note:SetPoint("TOPLEFT", w, "TOPLEFT", 16, -40)
|
||||
note:SetPoint("TOPRIGHT", w, "TOPRIGHT", -16, -40)
|
||||
note:SetHeight(44)
|
||||
note:SetWordWrap(true)
|
||||
|
||||
local box = UI.CreatePanel(w, "bgDeep", "bronze")
|
||||
box:SetHeight(26)
|
||||
box:SetPoint("TOPLEFT", w, "TOPLEFT", 16, -92)
|
||||
box:SetPoint("TOPRIGHT", w, "TOPRIGHT", -16, -92)
|
||||
local edit = CreateFrame("EditBox", nil, box)
|
||||
edit:SetAutoFocus(false)
|
||||
edit:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -2)
|
||||
edit:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -8, 2)
|
||||
edit:SetMaxLetters(0)
|
||||
if not edit:SetFont(UI.GetFontPath("body"), 12, "") then
|
||||
edit:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
|
||||
end
|
||||
edit:SetTextColor(UI.Color("text"))
|
||||
edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
|
||||
w.edit = edit
|
||||
|
||||
local status = UI.CreateLabel(w, "", { size = 11, color = "textDim" })
|
||||
status:SetPoint("TOPLEFT", w, "TOPLEFT", 16, -126)
|
||||
status:SetPoint("TOPRIGHT", w, "TOPRIGHT", -16, -126)
|
||||
w.status = status
|
||||
|
||||
local export = UI.CreateButton(w, L.CODEX_FAVX_EXPORT, 150, 26, function()
|
||||
local text, n = Codex.FavExport()
|
||||
edit:SetText(text)
|
||||
edit:SetFocus()
|
||||
edit:HighlightText()
|
||||
status:SetText(string.format(L.CODEX_FAVX_EXPORTED, n))
|
||||
end, "default")
|
||||
export:SetPoint("BOTTOMLEFT", w, "BOTTOMLEFT", 16, 14)
|
||||
local import = UI.CreateButton(w, L.CODEX_FAVX_IMPORT, 150, 26, function()
|
||||
local added, notes, bad = Codex.FavImport(edit:GetText())
|
||||
if not added then
|
||||
status:SetText(L.CODEX_FAVX_BAD)
|
||||
return
|
||||
end
|
||||
status:SetText(string.format(L.CODEX_FAVX_IMPORTED, added, notes, bad))
|
||||
FW:Fire("CODEX_CHANGED", "import")
|
||||
end, "primary")
|
||||
import:SetPoint("LEFT", export, "RIGHT", 8, 0)
|
||||
local close = UI.CreateButton(w, L.CODEX_FAVX_CLOSE, 100, 26, function() w:Hide() end, "default")
|
||||
close:SetPoint("BOTTOMRIGHT", w, "BOTTOMRIGHT", -16, 14)
|
||||
w.exportButton, w.importButton = export, import
|
||||
return w
|
||||
end
|
||||
|
||||
function FavUI.Open()
|
||||
win = win or Build()
|
||||
win.status:SetText("")
|
||||
win:Show()
|
||||
return win
|
||||
end
|
||||
|
||||
if UI.RegisterSettingsSection then
|
||||
UI:RegisterSettingsSection("codex-fav", {
|
||||
order = 107,
|
||||
height = 110,
|
||||
build = function(parent)
|
||||
local L = FW.L
|
||||
local f, stack = UI.CreateSection(parent, L.CODEX_FAVX_HEADER)
|
||||
local note = UI.CreateLabel(f, L.CODEX_FAVX_SECTION_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
|
||||
stack:Add(note, { fit = true, gap = 8 })
|
||||
local open = UI.CreateButton(f, L.CODEX_FAVX_OPEN, 240, 26, function() FavUI.Open() end, "default")
|
||||
stack:Add(open, { height = 26, gap = 10 })
|
||||
stack:Layout()
|
||||
return f
|
||||
end,
|
||||
refresh = function() end,
|
||||
})
|
||||
end
|
||||
Reference in New Issue
Block a user