Freewood-OctoWoW-Codex 0.1.136

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016onJCSHLwhs9udhC9ZLzk9
This commit is contained in:
Lacustat
2026-10-27 01:48:57 +01:00
co-authored by Claude Sonnet 5.5
commit d3c46dc9cb
34 changed files with 27357 additions and 0 deletions
+4474
View File
File diff suppressed because it is too large Load Diff
+581
View File
@@ -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
+799
View File
@@ -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
View File
@@ -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
+196
View File
@@ -0,0 +1,196 @@
-- Freewood-OctoWoW-Codex: mitgelieferte Startdaten (Addon Freewood-OctoWoW-ForeverData)
--
-- ForeverData enthaelt, was Spieler in Forever bereits erfasst haben: Quests mit Geber, Abgabe, Zielen, Belohnungen und Ketten,
-- Kreaturen, Objekte, Beute, Wegenetz, Verkehrsverbindungen, Ortspunkte. Die Daten bilden die unterste Schicht:
-- eigene Daten > empfangene Daten > ForeverData > Classic-Vorlage (Freewood-OctoWoW-CodexData)
-- Sie liegen nur im Speicher (nichts davon wird gespeichert oder weitergesendet) und gehen durch dieselbe Pruefung
-- (Share.Sanitize) wie empfangene Datensaetze. Abschalten: /fwforever aus
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Forever = { loaded = false, count = 0 }
Codex.Forever = Forever
NS.Forever = Forever
local DATA_ADDON = "Freewood-OctoWoW-ForeverData"
function Forever.Disabled()
local db = Codex.db
return db and db.settings and db.settings.noForever == true or false
end
-- Zusatztabellen (Item-Qualitaet, Item-Werte, Anzeige-IDs, Namen, Rezepte): nur fehlende Eintraege werden ergaenzt,
-- eigene Werte gehen vor. Einmal je Datenstand (db.foreverRev).
local function FillTable(dst, src, check)
if type(dst) ~= "table" or type(src) ~= "table" then return end
for k, v in pairs(src) do
if dst[k] == nil and check(k, v) then dst[k] = v end
end
end
local function IsId(k) return type(k) == "number" and k >= 1 and k == math.floor(k) and k < 1e9 end
local EXTRAS = {
iq = function(k, v) return IsId(k) and type(v) == "number" and v >= 0 and v <= 8 end,
disp = function(k, v) return IsId(k) and type(v) == "number" and v > 0 end,
vfac = function(k, v) return IsId(k) and (v == "A" or v == "H") end,
wrq = function(k, v) return IsId(k) and type(v) == "table" end,
names = function(k, v) return IsId(k) and type(v) == "string" and #v <= 80 end,
istats = function(k, v)
if not IsId(k) or type(v) ~= "table" then return false end
for sk, sv in pairs(v) do
if sk == "_fx" then
if type(sv) ~= "table" then return false end
elseif type(sk) ~= "string" or #sk > 60 or (type(sv) ~= "number" and type(sv) ~= "string" and type(sv) ~= "boolean") then return false end
end
return true
end,
}
local function MergeExtras(g)
local db = Codex.db
if not db or not g.extra or db.foreverRev == g.rev then return end
for name, check in pairs(EXTRAS) do
if type(g.extra[name]) == "table" then
db[name] = db[name] or {}
FillTable(db[name], g.extra[name], check)
end
end
if type(g.extra.recipes) == "table" then
db.recipes = db.recipes or {}
for prof, list in pairs(g.extra.recipes) do
if type(prof) == "string" and type(list) == "table" then
db.recipes[prof] = db.recipes[prof] or {}
FillTable(db.recipes[prof], list, function(k, v) return type(k) == "string" and #k <= 80 and type(v) == "table" end)
end
end
end
db.foreverRev = g.rev
end
-- Daten des Addons laden (bei Bedarf wird das Addon geladen). Rueckgabe: true, wenn Daten da sind
function Forever.Load()
if Forever.loaded then return true end
if Forever.Disabled() or not Codex.Share or not Codex.Share.SetBuiltin then return false end
local g = _G.FreewoodForeverData
if not (g and g.ready) then
local load = (_G.C_AddOns and _G.C_AddOns.LoadAddOn) or _G.LoadAddOn
if type(load) == "function" then pcall(load, DATA_ADDON) end
g = _G.FreewoodForeverData
end
if not (g and g.ready and type(g.list) == "table") then
Forever.missing = true
return false
end
if Forever.loading then return false end
Forever.loading = true
local list = g.list
local total, slice, from = #list, 600, 1
Codex.Share.BuiltinBegin()
Forever.count = 0
local function Finish()
Codex.Share.BuiltinDone()
pcall(MergeExtras, g)
Forever.version, Forever.rev = g.version, g.rev
Forever.loaded = true
Forever.loading = false
Forever.missing = false
if Codex.chainRev then Codex.chainRev = Codex.chainRev + 1 end
FW:Fire("CODEX_CHANGED", "forever")
end
local function Step()
if not Forever.loading then return end -- zwischendurch ausgeschaltet
local to = math.min(total, from + slice - 1)
Forever.count = Forever.count + Codex.Share.BuiltinAdd(list, from, to)
from = to + 1
if from > total then
Finish()
elseif _G.C_Timer and _G.C_Timer.After then
_G.C_Timer.After(0.05, Step)
else
Step()
end
end
Step()
return true
end
-- Quellen eines Items aus den Startdaten: { u = { {id, chance} }, o = { ... }, v = { Haendler-IDs } } oder nil
local itemCache = {}
function Forever.Item(id)
if not Forever.loaded or Forever.Disabled() then return nil end
local g = _G.FreewoodForeverData
local s = g and type(g.src) == "table" and g.src[id]
if type(s) ~= "string" then return nil end
local c = itemCache[id]
if c then return c end
c = { u = {}, o = {}, v = {} }
for part in s:gmatch("[^;]+") do
local tag, body = part:sub(1, 1), part:sub(2)
if tag == "u" or tag == "o" then
for k, v in body:gmatch("(%d+)=([%d%.]+)") do
local t = c[tag]
t[#t + 1] = { id = tonumber(k), chance = tonumber(v) }
end
elseif tag == "v" then
for k in body:gmatch("%d+") do c.v[#c.v + 1] = tonumber(k) end
end
end
itemCache[id] = c
return c
end
-- Alle Item-IDs mit Quellen in den Startdaten (Tabelle id -> Text; nur lesen)
function Forever.ItemIds()
if not Forever.loaded or Forever.Disabled() then return {} end
local g = _G.FreewoodForeverData
return (g and type(g.src) == "table") and g.src or {}
end
function Forever.ItemHasVendor(id)
local it = Forever.Item(id)
return it ~= nil and #it.v > 0
end
-- Schalter: an = Startdaten nutzen (Standard)
function Forever.SetEnabled(on)
local db = Codex.db
if not db then return end
db.settings.noForever = (not on) or nil
if on then
Forever.Load()
elseif Forever.loading then
Forever.loading = false
Codex.Share.SetBuiltin({})
Forever.count = 0
elseif Forever.loaded then
Codex.Share.SetBuiltin({})
Forever.loaded, Forever.count = false, 0
if Codex.chainRev then Codex.chainRev = Codex.chainRev + 1 end
FW:Fire("CODEX_CHANGED", "forever")
end
end
function Forever.Status()
local L = FW.L
if Forever.Disabled() then return L.CODEX_FOREVER_OFF end
if Forever.loaded then return string.format(L.CODEX_FOREVER_ON, Forever.count, tostring(Forever.version or "?")) end
return L.CODEX_FOREVER_MISSING
end
SLASH_FREEWOODFOREVER1 = "/fwforever"
SlashCmdList["FREEWOODFOREVER"] = function(msg)
msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
if msg == "aus" or msg == "off" then
Forever.SetEnabled(false)
elseif msg == "an" or msg == "on" then
Forever.SetEnabled(true)
end
FW:Print(Forever.Status())
end
+427
View File
@@ -0,0 +1,427 @@
-- Freewood-OctoWoW-Codex: Lern-Hinweise
-- Nach einem Stufenaufstieg (ausserhalb des Kampfes) oder einer neuen Berufsstufe zeigt ein kurzer Hinweis
-- in der Bildschirmmitte, was man jetzt beim Lehrer lernen kann. Grundlage: Lernplan (Lehrer-Daten).
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 Hints = { queue = {}, showing = false, shown = 0 }
NS.Hints = Hints
Codex.Hints = Hints
local DURATION = 5
local FADE = 0.6
local MAX_ROWS = 5
local ROW_H = 24
local WIDTH = 380
------------------------------------------------------------------------
-- Einstellungen
------------------------------------------------------------------------
function Hints.ClassOn()
return Codex.db.settings.hintClass ~= false
end
function Hints.ProfOn()
return Codex.db.settings.hintProf ~= false
end
------------------------------------------------------------------------
-- Inhalt
------------------------------------------------------------------------
local function SpellKnown(id)
if not id then return false end
local SB = _G.C_SpellBook
local fn = (SB and SB.IsSpellKnown) or _G.IsSpellKnown or (_G.C_Spell and _G.C_Spell.IsSpellKnown)
if type(fn) ~= "function" then return false end
local ok, known = pcall(fn, id)
return ok and known == true
end
local function Entry(r)
local cost = Codex.ModePrice(r.pr)
return { nm = r.nm or "?", sp = r.sp, rk = r.rk or (r.sp and Codex.SpellRank(r.sp)) or nil, cost = cost }
end
-- Fähigkeiten deiner Klasse, die auf dieser Stufe beim Lehrer dazukommen
function Hints.ClassEntries(level)
local token = Codex.Time and Codex.Time.PlayerToken()
if not token then return {} end
local plan = Codex.LearnPlan(token)
local g = plan.levels[level]
local list = {}
if g then
for _, sp in ipairs(g.spells) do
if not SpellKnown(sp.sp) then
list[#list + 1] = { nm = sp.nm or "?", sp = sp.sp, rk = sp.rk or (sp.sp and Codex.SpellRank(sp.sp)) or nil, cost = sp.cost }
end
end
end
return list
end
-- Berufslehrer-Fähigkeiten, deren Fertigkeitswert in (von, bis] liegt
function Hints.ProfEntries(skill, from, to)
local list, seen = {}, {}
for _, u in pairs(Codex.V().units) do
if u.tr and u.ro and (u.ro.proftrainer or 0) > 0 then
for key, r in pairs(u.tr) do
if r.sn == skill and r.sr and r.sr > from and r.sr <= to then
local k = r.sp and ("s" .. r.sp) or key
if not seen[k] then
seen[k] = true
list[#list + 1] = Entry(r)
end
end
end
end
end
table.sort(list, function(a, b)
if a.nm ~= b.nm then return a.nm < b.nm end
return (a.rk or "") < (b.rk or "")
end)
return list
end
------------------------------------------------------------------------
-- Anzeige
------------------------------------------------------------------------
local frame
-- gespeicherte Position (verschoben in den Einstellungen) oder Standard: Bildschirmmitte, etwas hoeher
local function ApplyPos(f)
f:ClearAllPoints()
local p = Codex.db.settings.hintPos
if type(p) == "table" and p.point and p.x and p.y then
f:SetPoint(p.point, UIParent, p.rel or p.point, p.x, p.y)
else
f:SetPoint("CENTER", UIParent, "CENTER", 0, 160)
end
end
function Hints.SavePos()
if not frame or not frame.GetPoint then return false end
local point, _, rel, x, y = frame:GetPoint(1)
if not point then return false end
Codex.db.settings.hintPos = { point = point, rel = rel, x = x, y = y }
return true
end
function Hints.ResetPos()
Codex.db.settings.hintPos = nil
if frame then ApplyPos(frame) end
end
local function Build()
local f = CreateFrame("Button", nil, UIParent)
f:SetSize(WIDTH, 80)
f:SetFrameStrata("DIALOG")
f:SetClampedToScreen(true)
f:SetMovable(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function(self)
if Hints.editing then self:StartMoving() end
end)
f:SetScript("OnDragStop", function(self)
self:StopMovingOrSizing()
if Hints.editing then Hints.SavePos() end
end)
ApplyPos(f)
f:Hide()
local bg = UI.CreatePanel(f, "bgDeep", "gold")
bg:SetAllPoints(f)
-- Inhalt auf eigener Ebene ueber dem Hintergrund (ein Kindfenster zeichnet sonst ueber den Texten des Eltern-Fensters)
local c = CreateFrame("Frame", nil, f)
c:SetAllPoints(f)
c:SetFrameLevel((bg:GetFrameLevel() or 1) + 2)
f.title = UI.CreateLabel(c, "", { font = "heading", size = 14, color = "goldLight" })
f.title:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -10)
f.title:SetPoint("TOPRIGHT", f, "TOPRIGHT", -12, -10)
f.title:SetWordWrap(false)
f.rows = {}
for i = 1, MAX_ROWS do
local row = {}
row.icon = c:CreateTexture(nil, "ARTWORK")
row.icon:SetSize(ROW_H - 4, ROW_H - 4)
row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
row.icon:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -(34 + (i - 1) * ROW_H))
row.text = UI.CreateLabel(c, "", { size = 12 })
row.text:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
row.text:SetWidth(WIDTH - 150)
row.text:SetWordWrap(false)
row.cost = UI.CreateLabel(c, "", { size = 11, color = "textDim", justify = "RIGHT" })
row.cost:SetPoint("RIGHT", f, "RIGHT", -12, 0)
row.cost:SetPoint("TOP", row.icon, "TOP", 0, 0)
row.cost:SetPoint("BOTTOM", row.icon, "BOTTOM", 0, 0)
row.cost:SetWidth(120)
f.rows[i] = row
end
f.more = UI.CreateLabel(c, "", { size = 11, color = "textDim" })
f.foot = UI.CreateLabel(c, "", { size = 10, color = "textDim" })
f:SetScript("OnClick", function()
if Hints.editing then return end
Hints.Dismiss()
if f.onClick then f.onClick() end
end)
f:SetScript("OnUpdate", function(self, elapsed)
if Hints.editing then
self.age = 0
self:SetAlpha(1)
return
end
self.age = (self.age or 0) + (elapsed or 0)
if self.age >= DURATION then
Hints.Dismiss()
elseif self.age > DURATION - FADE then
self:SetAlpha(math.max(0, (DURATION - self.age) / FADE))
end
end)
return f
end
local function InCombat()
if _G.InCombatLockdown and _G.InCombatLockdown() then return true end
return _G.UnitAffectingCombat and _G.UnitAffectingCombat("player") or false
end
local function Display(h)
frame = frame or Build()
ApplyPos(frame)
local L = FW.L
frame.title:SetText(h.title)
local n = math.min(#h.entries, MAX_ROWS)
local total = 0
local money = _G.GetMoney and _G.GetMoney() or nil
for i = 1, MAX_ROWS do
local row, e = frame.rows[i], h.entries[i]
if e and i <= n then
local icon = e.sp and Codex.SpellIcon(e.sp) or nil
row.icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
row.icon:Show()
row.text:SetText(e.nm .. (e.rk and (" |cff9a9aa3(" .. e.rk .. ")|r") or ""))
row.cost:SetText((e.cost and e.cost > 0 and Codex.FormatMoney) and Codex.FormatMoney(e.cost) or "")
else
row.icon:Hide()
row.text:SetText("")
row.cost:SetText("")
end
end
for _, e in ipairs(h.entries) do total = total + (e.cost or 0) end
local y = 34 + n * ROW_H + 4
local more = #h.entries - n
frame.more:ClearAllPoints()
frame.more:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -y)
frame.more:SetText(more > 0 and string.format(L.CODEX_HINT_MORE, more) or "")
if more > 0 then y = y + 16 end
local foot = Hints.editing and L.CODEX_HINT_DRAG or L.CODEX_HINT_CLICK
if total > 0 and Codex.FormatMoney and not Hints.editing then
foot = string.format(L.CODEX_HINT_COST, Codex.FormatMoney(total))
if money and money < total then
foot = foot .. " | " .. string.format(L.CODEX_HINT_MISSING, Codex.FormatMoney(total - money))
end
foot = foot .. " | " .. L.CODEX_HINT_CLICK
end
frame.foot:ClearAllPoints()
frame.foot:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -y)
frame.foot:SetText(foot)
frame:SetHeight(y + 26)
frame.onClick = h.onClick
frame.age = 0
frame:SetAlpha(1)
frame:Show()
Hints.showing = true
Hints.shown = Hints.shown + 1
Hints.last = h
end
function Hints.Dismiss()
if Hints.editing then return end
if frame then frame:Hide() end
Hints.showing = false
Hints.Flush()
end
-- naechsten wartenden Hinweis zeigen, wenn kein Kampf ist und keiner laeuft
function Hints.Flush()
if Hints.showing or Hints.editing or InCombat() then return end
local h = table.remove(Hints.queue, 1)
if h then Display(h) end
end
function Hints.Push(h)
if not h or #h.entries == 0 then return false end
Hints.queue[#Hints.queue + 1] = h
Hints.Flush()
return true
end
local function OpenPlan(level)
return function()
UI:Show("codex")
if Codex.ui and Codex.ui.Go then Codex.ui.Go("learn", level) end
end
end
function Hints.OnLevelUp(level)
level = tonumber(level)
if not level or not Hints.ClassOn() or not Codex.db.settings.enabled then return false end
local entries = Hints.ClassEntries(level)
return Hints.Push({ title = string.format(FW.L.CODEX_HINT_CLASS_TITLE, level), entries = entries, onClick = OpenPlan(level) })
end
local prevSkill = {}
function Hints.OnSkill(skill, value)
value = tonumber(value)
if not skill or not value then return false end
local from = prevSkill[skill] or (value - 1)
prevSkill[skill] = value
if not Hints.ProfOn() or not Codex.db.settings.enabled then return false end
local entries = Hints.ProfEntries(skill, from, value)
return Hints.Push({ title = string.format(FW.L.CODEX_HINT_PROF_TITLE, skill, value), entries = entries, onClick = function() UI:Show("codex") end })
end
-- Test aus den Einstellungen: letzte Stufe mit Daten, sonst eine Beispielzeile
function Hints.Demo()
local pl = _G.UnitLevel and _G.UnitLevel("player") or 1
local entries
for lv = pl, 1, -1 do
entries = Hints.ClassEntries(lv)
if #entries > 0 then
pl = lv
break
end
end
if not entries or #entries == 0 then
entries = { { nm = FW.L.CODEX_HINT_DEMO, rk = "1", cost = 10000 } }
end
Hints.queue = {}
Hints.showing = false
if frame then frame:Hide() end
Hints.Push({ title = string.format(FW.L.CODEX_HINT_CLASS_TITLE, pl), entries = entries, onClick = OpenPlan(pl) })
end
-- Verschieben: der Hinweis bleibt stehen, bis der Modus beendet wird; Position wird beim Loslassen gespeichert
function Hints.SetEditMode(on)
on = on and true or false
if on == (Hints.editing == true) then return end
if on then
Hints.queue, Hints.showing = {}, false
if frame then frame:Hide() end
local pl = _G.UnitLevel and _G.UnitLevel("player") or 1
local entries = { { nm = FW.L.CODEX_HINT_DEMO, rk = "1", cost = 10000 }, { nm = FW.L.CODEX_HINT_DEMO, rk = "2", cost = 25000 } }
Hints.editing = true
Display({ title = string.format(FW.L.CODEX_HINT_CLASS_TITLE, pl), entries = entries })
else
Hints.SavePos()
Hints.editing = false
Hints.showing = false
if frame then frame:Hide() end
Hints.Flush()
end
FW:Fire("CODEX_HINT_EDIT")
end
------------------------------------------------------------------------
-- Ereignisse
------------------------------------------------------------------------
-- Muster aus dem Textbaustein des Spiels ("... %s ... %d" oder mit Stellen "%1$s ... %2$d"). Rueckgabe: Muster, Reihenfolge
-- der Platzhalter (Stelle -> Rang in der Fundstelle), sonst nil
local function SkillPattern(fmt)
fmt = fmt or _G.SKILL_RANK_UP
if type(fmt) ~= "string" then return nil end
local out, order, i = { "^" }, {}, 1
while i <= #fmt do
local pos, kind, len = fmt:match("^%%(%d+)%$([sd])()", i)
if pos then
i = len
else
kind, len = fmt:match("^%%([sd])()", i)
pos = #order + 1
if kind then i = len end
end
if kind then
out[#out + 1] = kind == "s" and "(.+)" or "(%d+)"
order[#order + 1] = tonumber(pos)
else
local ch = fmt:sub(i, i)
out[#out + 1] = ch:match("%w") and ch or ("%" .. ch)
i = i + 1
end
end
out[#out + 1] = "$"
return table.concat(out), order
end
Hints.SkillPattern = SkillPattern
FW:RegisterEvent("PLAYER_LEVEL_UP", function(_, level)
if C_Timer and C_Timer.After then
C_Timer.After(1, function() Hints.OnLevelUp(level) end)
else
Hints.OnLevelUp(level)
end
end, Hints)
FW:RegisterEvent("PLAYER_REGEN_ENABLED", function() Hints.Flush() end, Hints)
FW:RegisterEvent("PLAYER_REGEN_DISABLED", function()
if Hints.editing then Hints.SetEditMode(false) end
end, Hints)
FW:RegisterEvent("CHAT_MSG_SKILL", function(_, text)
local pat, order = SkillPattern()
if type(text) ~= "string" or not pat then return end
local caps = { text:match(pat) }
if #caps == 0 then return end
local skill, value = caps[1], caps[2]
if order and order[1] == 2 then skill, value = caps[2], caps[1] end -- Fertigkeit steht an Stelle 2
if skill then Hints.OnSkill(skill, value) end
end, Hints)
------------------------------------------------------------------------
-- Einstellungen: Abschnitt
------------------------------------------------------------------------
if UI.RegisterSettingsSection then
UI:RegisterSettingsSection("codex-hints", {
order = 106,
height = 300,
build = function(parent)
local L = FW.L
local f, stack = UI.CreateSection(parent, L.CODEX_HINT_HEADER)
local note = UI.CreateLabel(f, L.CODEX_HINT_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
stack:Add(note, { fit = true, gap = 8 })
local classBox = UI.CreateCheckbox(f, L.CODEX_HINT_CLASS_SWITCH, function() return Hints.ClassOn() end,
function(on) Codex.db.settings.hintClass = on and true or false end)
stack:Add(classBox, { fit = true, gap = 10 })
local profBox = UI.CreateCheckbox(f, L.CODEX_HINT_PROF_SWITCH, function() return Hints.ProfOn() end,
function(on) Codex.db.settings.hintProf = on and true or false end)
stack:Add(profBox, { fit = true, gap = 4 })
local test = UI.CreateButton(f, L.CODEX_HINT_TEST, 200, 26, function() Hints.Demo() end, "default")
stack:Add(test, { height = 26, gap = 12 })
local move
move = UI.CreateButton(f, L.CODEX_HINT_MOVE, 200, 26, function()
Hints.SetEditMode(not Hints.editing)
end, "default")
stack:Add(move, { height = 26, gap = 6 })
local reset = UI.CreateButton(f, L.CODEX_HINT_RESET, 200, 26, function() Hints.ResetPos() end, "default")
stack:Add(reset, { height = 26, gap = 6 })
stack:Layout()
f.classBox, f.profBox, f.move = classBox, profBox, move
FW:On("CODEX_HINT_EDIT", function()
move:SetLabel(Hints.editing and L.CODEX_HINT_MOVE_DONE or L.CODEX_HINT_MOVE)
move:SetStyle(Hints.editing and "primary" or "default")
end, f)
return f
end,
refresh = function(frame)
frame.classBox:Refresh()
frame.profBox:Refresh()
end,
})
end
+328
View File
@@ -0,0 +1,328 @@
-- Freewood-OctoWoW-Codex: Kreaturen ueber ihre ID nachschlagen (OctoWoW)
--
-- C_CreatureInfo.GetCreatureInfoByID liefert zu einer Kreatur-ID: creatureID, displayID, name (englisch), type (Nummer),
-- family, rank, subName. Ist die Kreatur dem Client noch unbekannt, laedt RequestLoadCreatureByID sie nach
-- (Antwort: Ereignis CREATURE_DATA_LOAD_RESULT).
-- Im Hintergrund werden bei allen Kreaturen, die wir schon kennen, fehlende Angaben ergaenzt:
-- ct Typ (sprachunabhaengig, aus der Typ-Nummer)
-- c Klasse (elite, selten, Boss ...) falls noch keine bekannt
-- ne englischer Name (Ersatz, wenn kein gesehener Name da ist)
-- Es werden nur bestehende Datensaetze ergaenzt, keine neuen angelegt.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Lookup = { started = false }
NS.Lookup = Lookup
Codex.Lookup = Lookup
local TYPE_CODE = {
[1] = "beast", [2] = "dragonkin", [3] = "demon", [4] = "elemental", [5] = "giant", [6] = "undead",
[7] = "humanoid", [8] = "critter", [9] = "mechanical", [10] = "other", [11] = "totem", [12] = "pet", [13] = "gascloud",
}
local RANK_CODE = { [1] = "elite", [2] = "rareelite", [3] = "worldboss", [4] = "rare" }
local TICK = 0.4 -- Abstand zwischen zwei Schritten (Sekunden)
local WAIT = 1.6 -- so lange auf die Antwort einer Anfrage warten
local PER_TICK = 25 -- bereits bekannte Kreaturen je Schritt (nur lesen, kostet fast nichts)
local RETRY_AFTER = 7 * 86400 -- erfolglose IDs nach einer Woche wieder versuchen
local FIRE_EVERY = 40 -- Oberflaeche hoechstens alle so viele Aenderungen auffrischen
local state = {
queue = {}, first = 1, last = 0,
pending = nil, pendingAt = 0,
changed = 0, lastFire = 0, generation = 0,
stats = { read = 0, requested = 0, filled = 0, failed = 0 },
}
Lookup.state = state
local function Now()
return GetTime and GetTime() or 0
end
local function Store()
local db = Codex.db
db.lookup = db.lookup or {}
db.lookup.fail = db.lookup.fail or {}
return db.lookup
end
local function Enabled()
return Codex.db ~= nil and Codex.db.settings ~= nil and Codex.db.settings.lookup ~= false
and _G.C_CreatureInfo ~= nil and _G.C_CreatureInfo.GetCreatureInfoByID ~= nil
end
local function ReadInfo(id)
local ok, info = pcall(_G.C_CreatureInfo.GetCreatureInfoByID, id)
if ok and type(info) == "table" and info.creatureID == id then
return info
end
return nil
end
local function NeedsInfo(u)
return type(u) == "table" and (not u.ct or not u.ne)
end
-- Schreibt die Antwort in den bestehenden Datensatz. Rueckgabe: true, wenn sich etwas geaendert hat
function Lookup.Apply(id, info)
local u = Codex.db.units[id]
if type(u) ~= "table" or type(info) ~= "table" then
return false
end
local changed = false
local code = TYPE_CODE[info.type]
if code and u.ct ~= code then
u.ct = code
changed = true
end
local rc = RANK_CODE[info.rank]
if rc and not u.c then
u.c = rc
changed = true
end
local name = info.name
if type(name) == "string" and name ~= "" and not u.ne then
u.ne = name:sub(1, 60)
changed = true
end
return changed
end
local function Changed(id)
state.changed = state.changed + 1
state.stats.filled = state.stats.filled + 1
if state.changed - state.lastFire >= FIRE_EVERY then
state.lastFire = state.changed
FW:Fire("CODEX_CHANGED", "unit", id)
end
end
-- Warteschlange (mit Zaehlern, nicht table.remove)
local function Push(id)
state.last = state.last + 1
state.queue[state.last] = id
end
local function Pop()
if state.first > state.last then
return nil
end
local id = state.queue[state.first]
state.queue[state.first] = nil
state.first = state.first + 1
if state.first > state.last then
state.first, state.last = 1, 0
end
return id
end
function Lookup.QueueSize()
return state.last - state.first + 1
end
-- Alle bekannten Kreaturen, denen etwas fehlt, in die Schlange stellen
function Lookup.Scan()
if not Enabled() then
return 0
end
local fail = Store().fail
local now = time and time() or 0
local queued = {}
for i = state.first, state.last do
queued[state.queue[i]] = true
end
local ids = {}
for id, u in pairs(Codex.db.units) do
if type(id) == "number" and NeedsInfo(u) and not queued[id] then
local f = fail[id]
if not f or now - f > RETRY_AFTER then
ids[#ids + 1] = id
end
end
end
table.sort(ids)
for i = 1, #ids do
Push(ids[i])
end
return #ids
end
local function Finish(id, info)
state.pending = nil
if info then
if Lookup.Apply(id, info) then
Changed(id)
end
else
state.stats.failed = state.stats.failed + 1
Store().fail[id] = time and time() or 1
end
end
function Lookup.OnLoadResult(_, id, ok)
id = tonumber(id)
if state.pending and id == state.pending then
Finish(id, ReadInfo(id))
end
end
function Lookup.Step()
if not Enabled() then
return
end
local now = Now()
if state.pending then
if now - state.pendingAt < WAIT then
return
end
-- keine Antwort: noch einmal lesen, sonst als erfolglos merken
Finish(state.pending, ReadInfo(state.pending))
end
local n = 0
while n < PER_TICK do
local id = Pop()
if not id then
return
end
local u = Codex.db.units[id]
if NeedsInfo(u) then
local info = ReadInfo(id)
if info then
state.stats.read = state.stats.read + 1
if Lookup.Apply(id, info) then
Changed(id)
end
n = n + 1
else
-- dem Client unbekannt: nachladen lassen und auf die Antwort warten
state.pending, state.pendingAt = id, now
state.stats.requested = state.stats.requested + 1
local ok = pcall(_G.C_CreatureInfo.RequestLoadCreatureByID, id)
if not ok then
Finish(id, nil)
end
return
end
end
end
end
function Lookup:Start()
if self.started then
return
end
self.started = true
state.generation = state.generation + 1
local gen = state.generation
FW:RegisterEvent("CREATURE_DATA_LOAD_RESULT", Lookup.OnLoadResult, Lookup)
local function Loop(delay, fn)
C_Timer.After(delay, function()
if not Lookup.started or gen ~= state.generation then
return
end
fn()
Loop(delay, fn)
end)
end
Loop(TICK, Lookup.Step)
-- erste Runde kurz nach dem Einloggen, danach alle 10 Minuten fuer neu gesehene Kreaturen
C_Timer.After(20, function()
if Lookup.started and gen == state.generation then
Lookup.Scan()
end
end)
Loop(600, Lookup.Scan)
end
function Lookup:Stop()
self.started = false
state.generation = state.generation + 1
FW:UnregisterEvent("CREATURE_DATA_LOAD_RESULT", Lookup)
end
-- /fwlookup Stand anzeigen
-- /fwlookup run sofort neu durchsuchen
-- /fwlookup retry auch fruehere Fehlschlaege noch einmal versuchen
-- /fwlookup id <N> eine einzelne Kreatur nachschlagen und anzeigen
-- /fwlookup clean Reste ohne Daten anzeigen; "clean yes" entfernt sie
function Lookup.Command(msg)
msg = (msg or ""):lower()
if msg == "retry" then
Store().fail = {}
msg = "run"
end
if msg == "run" then
local n = Lookup.Scan()
FW:Print(string.format("Nachschlagen: %d Kreaturen vorgemerkt", n))
return
end
if msg == "clean" or msg == "clean yes" then
-- Reste ohne Beobachtungen: Kreaturen, die der Client nicht kennt (Nachschlagen erfolglos), ohne Kills, Positionen,
-- Rollen, Beute oder sonstige Daten. Erst anzeigen, nur mit "clean yes" loeschen.
local Share = Codex.Share
local list = {}
for id in pairs(Store().fail) do
local u = Codex.db.units[id]
if type(u) == "table" and not (Share and Share.IsShareable and Share.IsShareable("u", u)) and not u.ed and not u.cde and not u.cen
and not u.ro and not u.d and not u.cells and (u.k or 0) == 0 then
list[#list + 1] = id
end
end
table.sort(list)
if #list == 0 then
FW:Print("Aufraeumen: keine Reste gefunden")
return
end
for _, id in ipairs(list) do
local u = Codex.db.units[id]
FW:Print(string.format(" %d %s", id, tostring(u.n or u.ne or "?")))
end
if msg == "clean yes" then
for _, id in ipairs(list) do
Codex.db.units[id] = nil
Store().fail[id] = nil
end
FW:Fire("CODEX_CHANGED", "unit", list[1])
FW:Print(string.format("Aufraeumen: %d Kreaturen entfernt", #list))
else
FW:Print(string.format("Aufraeumen: %d Kreaturen ohne Daten. Zum Entfernen: /fwlookup clean yes", #list))
end
return
end
local want = msg:match("^id%s+(%d+)$")
if want then
local id = tonumber(want)
local info = ReadInfo(id)
if info then
FW:Print(string.format("Kreatur %d: %s, Typ %s, Rang %s, Familie %s, Anzeige %s", id, tostring(info.name),
tostring(info.type), tostring(info.rank), tostring(info.family), tostring(info.displayID)))
else
FW:Print("Kreatur " .. id .. " ist dem Client noch unbekannt (/fwnpc " .. id .. " laedt sie nach)")
end
return
end
local total, withType, withName = 0, 0, 0
for id, u in pairs(Codex.db.units) do
if type(id) == "number" then
total = total + 1
if u.ct then withType = withType + 1 end
if u.ne or u.n then withName = withName + 1 end
end
end
local failed = 0
for _ in pairs(Store().fail) do failed = failed + 1 end
local s = state.stats
FW:Print(string.format("Nachschlagen: %s | Kreaturen %d, mit Typ %d, mit Namen %d | gelesen %d, angefragt %d, ergaenzt %d, erfolglos %d (gemerkt %d) | Schlange %d",
Enabled() and "an" or "aus", total, withType, withName, s.read, s.requested, s.filled, s.failed, failed, Lookup.QueueSize()))
end
if _G.SlashCmdList then
_G.SLASH_FWLOOKUP1 = "/fwlookup"
_G.SlashCmdList["FWLOOKUP"] = function(msg) Lookup.Command(msg) end
end
+764
View File
@@ -0,0 +1,764 @@
-- Freewood-OctoWoW-Codex: Fundorte auf der Weltkarte
-- Zeichnet Punkte mit weissem Rand ueber Blizzards Kartenflaeche: schwarz fuer Kreaturen,
-- rot fuer Objekte, blau fuer Quest-Geber und -Abgabe. Fundorte werden gebuendelt (BIN Zellen je Punkt).
-- Quelle sind die Zellen, die Codex beim Spielen erfasst hat (Raster 200 x 200 je Karte).
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS.Codex
if not FW or not Codex then
return
end
local UI = FW.UI
local GRID = Codex.GRID
local MAX_DOTS = 600
local BIN = 3 -- Zellen je Punkt (3 Zellen = 1,5 % der Kartenbreite)
local DOT_MIN = 9 -- Punktgroesse in Bildschirm-Pixeln (kleinster / groesster Punkt)
local DOT_MAX = 14
local RIM = 1.5 -- weisser Rand in Pixeln
local LAYER_LEVEL = 1500 -- weit ueber Blizzards Kartensymbolen, damit nichts die Punkte verdeckt
local COLORS = {
u = { 0, 0, 0 },
o = { 0.9, 0.1, 0.1 },
p = { 0.2, 0.55, 1 },
}
local ORDER = { u = 1, o = 2, p = 3 }
local MASK = "Interface\\CharacterFrame\\TempPortraitAlphaMask"
local Overlay = { points = nil, label = nil, hooked = false }
Codex.Map = Overlay
local layer, dots, infoButton, StartHover
local segDash = {} -- [Abschnitt] = { sig, tex = {...}, n }: Striche je Abschnitt, damit nur der veraenderte neu gesetzt wird
local DASH_LEN, DASH_GAP, DASH_THICK = 8, 6, 3 -- Pixel
local MAX_DASHES = 900
local atan2 = math.atan2 or function(y, x) return math.atan(y, x) end
-- Runder Punkt: weisse Scheibe mit schwarzer/roter/blauer Scheibe darauf. Rund wird er durch eine
-- Maske aus dem Spiel; fehlt sie, bleiben es Quadrate.
local function MaskAvailable()
if Codex.db and Codex.db.settings.squareDots then
return false
end
if type(GetFileIDFromPath) == "function" then
return GetFileIDFromPath(MASK) ~= nil
end
return true
end
local function NewDot(f)
local dot = {}
local useMask = MaskAvailable()
for _, name in ipairs({ "rim", "fill" }) do
local tex = f:CreateTexture(nil, name == "rim" and "ARTWORK" or "OVERLAY", nil, name == "rim" and 1 or 2)
tex:SetColorTexture(1, 1, 1, 1)
if useMask and f.CreateMaskTexture and tex.AddMaskTexture then
local ok, mask = pcall(f.CreateMaskTexture, f)
if ok and mask then
mask:SetTexture(MASK, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
mask:SetAllPoints(tex)
tex:AddMaskTexture(mask)
tex.mask = mask
end
end
dot[name] = tex
end
return dot
end
local function HideDot(dot)
dot.rim:Hide()
dot.fill:Hide()
end
local API = Codex.MapAPI
local function Canvas()
return API.Canvas()
end
local function EnsureLayer()
if layer then
return layer
end
local canvas = Canvas()
if not canvas then
return nil
end
-- Die Ebene haengt am Kartenfenster selbst (nicht an der Kartenflaeche): dort ist sie sicher sichtbar.
-- Die Punkte werden an die Kartenflaeche geheftet und folgen ihr beim Verschieben und Zoomen.
local wm = _G.WorldMapFrame
if API.Classic() then
-- 1.12-Karte: Ebene direkt auf der Kartenflaeche, Anteile 0..1 gelten fuer die ganze Flaeche
layer = CreateFrame("Frame", nil, canvas)
layer:SetAllPoints(canvas)
layer:SetFrameLevel((canvas:GetFrameLevel() or 1) + 20)
if wm.GetFrameStrata then layer:SetFrameStrata(wm:GetFrameStrata()) end
else
layer = CreateFrame("Frame", nil, wm)
layer:SetAllPoints(wm.ScrollContainer or wm)
layer:SetFrameLevel((wm:GetFrameLevel() or 1) + LAYER_LEVEL)
if wm.GetFrameStrata then
layer:SetFrameStrata(wm:GetFrameStrata())
end
end
if layer.SetClipsChildren then
layer:SetClipsChildren(true)
end
layer:SetScript("OnSizeChanged", function(_, w)
if w and w > 1 and Overlay.points then
Overlay:Refresh()
end
end)
dots = {}
StartHover(layer)
return layer
end
local function EnsureInfoButton()
if infoButton then
return infoButton
end
local wm = _G.WorldMapFrame
if not wm then
return nil
end
infoButton = UI.CreateButton(wm, "", 300, 22, function()
Overlay:Clear()
end, "default")
if API.Classic() then
local canvas = Canvas() or wm
if wm.GetFrameStrata then infoButton:SetFrameStrata(wm:GetFrameStrata()) end
infoButton:SetFrameLevel((canvas:GetFrameLevel() or 1) + 40)
infoButton:SetPoint("TOPLEFT", canvas, "TOPLEFT", 12, -12)
else
infoButton:SetFrameStrata("HIGH")
infoButton:SetFrameLevel((wm:GetFrameLevel() or 1) + LAYER_LEVEL + 100)
local anchor = wm.ScrollContainer or wm
infoButton:SetPoint("TOPLEFT", anchor, "TOPLEFT", 12, -42)
end
infoButton:Hide()
return infoButton
end
local function CurrentMapID()
return API.CurrentMapID(function()
local list, seen = {}, {}
for _, p in ipairs(Overlay.points or {}) do
if p.m and not seen[p.m] then seen[p.m] = true; list[#list + 1] = p.m end
end
return list
end)
end
-- Faktor Kartenflaeche -> Bildschirm (aendert sich beim Zoomen)
function Overlay:CanvasScale()
local canvas = Canvas()
local s = 1
if canvas and canvas.GetEffectiveScale and _G.UIParent then
local a, b = canvas:GetEffectiveScale(), _G.UIParent:GetEffectiveScale()
if type(a) == "number" and type(b) == "number" and b > 0 and a > 0 then
s = a / b
end
end
return s
end
-- Skalierung der Punkte-Ebene relativ zur Oberflaeche
local function LayerScale()
local s = 1
if layer and layer.GetEffectiveScale and _G.UIParent then
local a, b = layer:GetEffectiveScale(), _G.UIParent:GetEffectiveScale()
if type(a) == "number" and type(b) == "number" and b > 0 and a > 0 then
s = a / b
end
end
return s
end
-- Punkte einer Karte zusammenfassen: je Klasse (u, o, p) und BIN x BIN Zellen ein Punkt
function Overlay:Bins(mapID)
local bins = {}
for _, p in ipairs(self.points or {}) do
if p.m == mapID then
local cls = p.pin and "p" or (p.k == "o" and "o" or "u")
local cx = math.min(GRID - 1, math.max(0, math.floor(p.x * GRID)))
local cy = math.min(GRID - 1, math.max(0, math.floor(p.y * GRID)))
local base = math.floor(cy / BIN) * 1000 + math.floor(cx / BIN)
local key = base .. cls
local b = bins[key]
if not b then
b = { sx = 0, sy = 0, n = 0, names = {}, cls = cls, base = base }
bins[key] = b
end
local w = p.n or 1
b.sx, b.sy, b.n = b.sx + p.x * w, b.sy + p.y * w, b.n + w
if p.name then
b.names[p.name] = (b.names[p.name] or 0) + w
end
end
end
local list, maxN, perBase = {}, 1, {}
for _, b in pairs(bins) do
b.x, b.y = b.sx / b.n, b.sy / b.n
list[#list + 1] = b
perBase[b.base] = (perBase[b.base] or 0) + 1
if b.n > maxN then
maxN = b.n
end
end
for _, b in ipairs(list) do
b.shared = perBase[b.base] > 1
end
-- Reihenfolge: Kreaturen zuerst, Quest-Punkte zuletzt (oben)
table.sort(list, function(a, b)
if ORDER[a.cls] ~= ORDER[b.cls] then return ORDER[a.cls] < ORDER[b.cls] end
if a.y ~= b.y then return a.y < b.y end
return a.x < b.x
end)
return list, maxN
end
-- Gestrichelte Linie durch die Wegpunkte (Luftlinie von Station zu Station).
-- Jeder Abschnitt merkt sich seine Lage: bleibt er gleich, werden die Striche nur wieder eingeblendet.
local function HideDashes(from)
for i = from or 1, #segDash do
local s = segDash[i]
for k = 1, s.n do s.tex[k]:Hide() end
s.n, s.sig = 0, nil
end
end
local function DrawPath(f, canvas, path, width, height, ratio, layerScale)
local total = 0
local len = DASH_LEN / layerScale
local step = (DASH_LEN + DASH_GAP) / layerScale
for i = 2, #path do
local s = segDash[i - 1]
if not s then s = { tex = {}, n = 0 }; segDash[i - 1] = s end
local x1, y1 = path[i - 1].x * width * ratio, -path[i - 1].y * height * ratio
local x2, y2 = path[i].x * width * ratio, -path[i].y * height * ratio
local sig = string.format("%.2f,%.2f,%.2f,%.2f,%.4f", x1, y1, x2, y2, layerScale)
if s.sig == sig then
for k = 1, s.n do s.tex[k]:Show() end
else
local dx, dy = x2 - x1, y2 - y1
local dist = math.sqrt(dx * dx + dy * dy)
local count = 0
if dist > 0 then
local ux, uy = dx / dist, dy / dist
local angle = atan2(dy, dx)
local t = len / 2
while t <= dist and total + count < MAX_DASHES do
count = count + 1
local tex = s.tex[count]
if not tex then
tex = f:CreateTexture(nil, "OVERLAY", nil, 0)
tex:SetColorTexture(1, 0.85, 0.2, 0.95)
s.tex[count] = tex
end
tex:ClearAllPoints()
tex:SetSize(len, DASH_THICK / layerScale)
tex:SetPoint("CENTER", canvas, "TOPLEFT", x1 + ux * t, y1 + uy * t)
if tex.SetRotation then tex:SetRotation(angle) end
tex:Show()
t = t + step
end
end
for k = count + 1, s.n do s.tex[k]:Hide() end
s.n, s.sig = count, sig
end
total = total + s.n
end
HideDashes(#path) -- Abschnitte, die es nicht mehr gibt
return total
end
function Overlay:Refresh()
local f = EnsureLayer()
if not f then
return
end
for _, d in ipairs(dots) do
HideDot(d)
end
if not (self.path and #self.path > 1 and self.points) then
HideDashes()
end
f:Show()
local btn = EnsureInfoButton()
if not self.points or #self.points == 0 then
if btn then
btn:Hide()
end
return
end
if btn then
btn:SetLabel(string.format(FW.L.CODEX_MAP_LABEL, self.label or ""))
btn:SetWidth(math.max(200, (btn.label:GetStringWidth() or 200) + 24))
btn:Show()
end
local mapID = CurrentMapID()
if not mapID then
return
end
local canvas = Canvas() or f
local width, height = canvas:GetWidth() or 1, canvas:GetHeight() or 1
if width < 2 or height < 2 then
if _G.C_Timer and not self.retrying then
self.retrying = true
_G.C_Timer.After(0.2, function()
Overlay.retrying = false
if Overlay.points then
Overlay:Refresh()
end
end)
end
return
end
local bins, maxN = self:Bins(mapID)
self.bins = bins
local scale = self:CanvasScale()
local layerScale = LayerScale()
local ratio = scale / layerScale -- Kartenflaeche-Einheiten -> Einheiten der Punkte-Ebene
self.lastScale = scale
self.drawnMap = mapID
self.hoverR = (DOT_MAX / 2 + 3) / scale / width -- Trefferradius als Anteil der Kartenbreite
self.hoverRY = (DOT_MAX / 2 + 3) / scale / height
self.lastDraw = { mapID = mapID, dots = #bins }
if self.path and #self.path > 1 and self.pathMap == mapID then
self.lastDraw.dashes = DrawPath(f, canvas, self.path, width, height, ratio, layerScale)
else
HideDashes()
end
for i, b in ipairs(bins) do
if i > MAX_DOTS then
break
end
local dot = dots[i]
if not dot then
dot = NewDot(f)
dots[i] = dot
end
local px = DOT_MIN + (DOT_MAX - DOT_MIN) * math.min(1, b.n / maxN)
local d = px / layerScale
local rim = (px + RIM * 2) / layerScale
local off, offY = 0, 0
if b.shared then
-- Kreatur links, Objekt rechts, Quest-Punkt darueber: nebeneinander statt uebereinander
local step = (px * 0.7) / layerScale
if b.cls == "u" then off = -step elseif b.cls == "o" then off = step else offY = step end
end
local x, y = b.x * width * ratio + off, -b.y * height * ratio + offY
local c = COLORS[b.cls]
dot.rim:ClearAllPoints()
dot.rim:SetSize(rim, rim)
dot.rim:SetPoint("CENTER", canvas, "TOPLEFT", x, y)
dot.rim:SetVertexColor(1, 1, 1, 1)
dot.rim:Show()
dot.fill:ClearAllPoints()
dot.fill:SetSize(d, d)
dot.fill:SetPoint("CENTER", canvas, "TOPLEFT", x, y)
dot.fill:SetVertexColor(c[1], c[2], c[3], 1)
dot.fill:Show()
end
end
-- Mouseover: Namen der Fundorte in der Naehe des Mauszeigers
local function MouseOverMap()
if API.Classic() then
return API.CursorNorm()
end
local wm = _G.WorldMapFrame
local sc = wm and wm.ScrollContainer
if type(sc) ~= "table" or type(sc.GetNormalizedCursorPosition) ~= "function" then
return nil
end
local focus
if GetMouseFoci then
focus = GetMouseFoci()
focus = focus and focus[1]
elseif GetMouseFocus then
focus = GetMouseFocus()
end
local f, ok = focus, (focus == nil)
local depth = 0
while f and depth < 12 do
if f == sc then
ok = true
break
end
f = f.GetParent and f:GetParent() or nil
depth = depth + 1
end
if not ok then
return nil
end
local x, y = sc:GetNormalizedCursorPosition()
if not x or x < 0 or x > 1 or y < 0 or y > 1 then
return nil
end
return x, y
end
function Overlay:HideHover()
self.hoverKey = nil
if self.hoverShown then
self.hoverShown = nil
local tip = _G.GameTooltip
if tip:GetOwner() == _G.WorldMapFrame then
tip:Hide()
end
end
end
function Overlay:UpdateHover()
if not self.points or not self.bins or not self.drawnMap then
return self:HideHover()
end
local x, y = MouseOverMap()
if not x then
return self:HideHover()
end
local key = math.floor(x * GRID) * 1000 + math.floor(y * GRID)
if key == self.hoverKey and self.hoverShown ~= nil then
return
end
self.hoverKey = key
local r, ry = self.hoverR or 0.02, self.hoverRY or 0.03
local names = {}
local hit = false
for i, b in ipairs(self.bins) do
if i > MAX_DOTS then
break
end
if math.abs(b.x - x) <= r and math.abs(b.y - y) <= ry then
hit = true
for name, n in pairs(b.names) do
names[name] = (names[name] or 0) + n
end
end
end
if not hit then
return self:HideHover()
end
local list = {}
for name, n in pairs(names) do
list[#list + 1] = { name = name, n = n }
end
table.sort(list, function(a, b)
if a.n ~= b.n then return a.n > b.n end
return a.name < b.name
end)
local tip = _G.GameTooltip
tip:SetOwner(_G.WorldMapFrame, "ANCHOR_CURSOR")
tip:AddLine(FW.L.CODEX_MAP_HOVER, 0.94, 0.75, 0.36)
for i, e in ipairs(list) do
if i > 10 then
tip:AddLine(string.format(FW.L.CODEX_MAP_MORE, #list - 10), 0.8, 0.8, 0.8)
break
end
tip:AddLine(e.name, 0.85, 0.85, 0.85)
end
tip:Show()
self.hoverShown = true
end
local hoverElapsed = 0
StartHover = function(f)
f:SetScript("OnUpdate", function(_, elapsed)
hoverElapsed = hoverElapsed + elapsed
if hoverElapsed < 0.08 then
return
end
hoverElapsed = 0
if Overlay.points then
local sig = API.Signature()
if sig ~= Overlay.lastSig then
Overlay.lastSig = sig
Overlay:Refresh()
end
local sc = Overlay:CanvasScale()
if Overlay.lastScale and math.abs(sc - Overlay.lastScale) > 0.01 then
Overlay:Refresh()
end
Overlay:UpdateHover()
end
end)
end
function Overlay:Hook()
if self.hooked then
return true
end
local wm = _G.WorldMapFrame
if not wm or not Canvas() then
return false
end
self.hooked = true
if wm.OnMapChanged then
pcall(hooksecurefunc, wm, "OnMapChanged", function()
Overlay:Refresh()
end)
end
if wm.HookScript then
wm:HookScript("OnShow", function()
Overlay:Refresh()
end)
end
if API.Classic() and _G.hooksecurefunc and _G.SetMapZoom then
pcall(hooksecurefunc, "SetMapZoom", function() Overlay:Refresh() end)
end
return true
end
-- Karte mit dem meisten Gewicht, bevorzugt die aktuelle, wenn sie Punkte hat
function Overlay:BestMap()
local weights = {}
for _, p in ipairs(self.points or {}) do
weights[p.m] = (weights[p.m] or 0) + (p.n or 1)
end
local current = CurrentMapID()
if current and weights[current] then
return current
end
local best, bestN
for m, n in pairs(weights) do
if not bestN or n > bestN or (n == bestN and m < best) then
best, bestN = m, n
end
end
return best
end
-- Weltkarte oeffnen und auf das passende Gebiet stellen
function Overlay:Focus()
local wm = _G.WorldMapFrame
if not wm or not self.points then
return
end
local target = self:BestMap()
if not wm:IsShown() then
if type(ToggleWorldMap) == "function" then
pcall(ToggleWorldMap)
else
pcall(wm.Show, wm)
end
end
if target and target ~= CurrentMapID() then
API.SetMap(target)
end
self:Refresh()
return target
end
-- points: Liste { m, x, y, n, name, pin }, label: Text fuers Knoepfchen auf der Karte
function Overlay:Show(points, label, path, pathMap)
if not points or #points == 0 then
FW:Print(FW.L.CODEX_MAP_NONE)
return false
end
self.route = nil -- eine andere Anzeige beendet die laufende Quest-Route
self.points = points
self.label = label
self.path = path
self.pathMap = pathMap
self:Hook()
self:Refresh()
self:Focus()
return true
end
function Overlay:Clear()
self.route = nil
self:HideHover()
self.points = nil
self.label = nil
self.path = nil
self.pathMap = nil
self.bins = nil
self:Refresh()
end
------------------------------------------------------------------------
-- Laufende Quest-Route: folgt dir, solange die Karte offen ist
------------------------------------------------------------------------
local ROUTE_MOVE = 0.005 -- Bewegung (Anteil der Kartenbreite), ab der neu gerechnet wird
local ROUTE_ARRIVE = 0.015 -- so nah dran gilt die Station als erreicht
local ROUTE_TICK = 1 -- Sekunden zwischen zwei Pruefungen
local routeGen = 0
-- Status der Stationen der aktuellen Route; aendert er sich (Quest angenommen), wird neu gerechnet
local function RouteSignature(points)
local t = {}
for i, p in ipairs(points or {}) do
t[i] = Codex.QuestStatus(p.id) and "x" or "o"
end
return table.concat(t)
end
local function PlayerLoc()
local loc = FW.Util.GetPlayerLocation()
if loc and loc.mapID and loc.x and loc.y then return loc end
return nil
end
-- Reihenfolge und Linie neu rechnen (billig: die Positionen der Quests stehen schon in r.cands).
-- Rueckgabe: false, wenn keine Station mehr uebrig ist
function Overlay:RouteRecompute(first)
local r = self.route
if not r then return false end
local loc = PlayerLoc()
local ax, ay = Codex.PlayerPosOn(r.mapID, loc)
if ax then
for _, p in ipairs(r.points or {}) do
if p.m == r.mapID and (p.x - ax) ^ 2 + (p.y - ay) ^ 2 < ROUTE_ARRIVE ^ 2 then
r.skip[p.id] = true
end
end
end
-- nur Quests, die noch offen sind (nicht angenommen, nicht abgegeben) und nicht schon erreicht wurden
local live = {}
for _, c in ipairs(r.cands) do
if not r.skip[c.id] and Codex.QuestStatus(c.id) == nil then live[#live + 1] = c end
end
r.cands = live
local points, mapID, path = Codex.QuestRouteOrder(live, loc)
if #points == 0 then
-- nichts mehr uebrig: Ende; sonst nur nichts in der Naehe (anderes Gebiet): alte Anzeige behalten
if #live == 0 then self:Clear() end
r.last = loc and { m = loc.mapID, x = loc.x, y = loc.y } or nil
return false
end
local mapChanged = r.mapID ~= mapID
self.points, self.path, self.pathMap = points, path, mapID
self.label = string.format(FW.L.CODEX_ROUTE_LABEL, #points)
self.bins = nil
r.points, r.mapID, r.sig = points, mapID, RouteSignature(points)
r.last = loc and { m = loc.mapID, x = loc.x, y = loc.y } or nil
local wm = _G.WorldMapFrame
if mapChanged and not first and wm and wm.IsShown and wm:IsShown() then
API.SetMap(mapID)
end
self:Refresh()
return true
end
-- Eine Pruefung: neu rechnen, wenn du dich bewegt hast oder sich eine Quest geaendert hat
function Overlay:RouteStep()
local r = self.route
if not r then return false end
local wm = _G.WorldMapFrame
if wm and wm.IsShown and not wm:IsShown() then return false end
local loc = PlayerLoc()
local changed = RouteSignature(r.points) ~= r.sig
if not changed and loc then
local last = r.last
changed = not last or last.m ~= loc.mapID or (loc.x - last.x) ^ 2 + (loc.y - last.y) ^ 2 > ROUTE_MOVE ^ 2
end
if changed then return self:RouteRecompute(false) end
return false
end
local function RouteTick(gen)
local r = Overlay.route
if not r or r.gen ~= gen then return end
Overlay:RouteStep()
if Overlay.route and Overlay.route.gen == gen and _G.C_Timer then
_G.C_Timer.After(ROUTE_TICK, function() RouteTick(gen) end)
end
end
-- ids: Quest-IDs der Liste. Zeigt die Route und haelt sie danach aktuell.
function Overlay:StartRoute(ids, opts)
local loc = PlayerLoc()
local cands = Codex.QuestRouteCandidates(ids, opts)
local points, mapID, path = Codex.QuestRouteOrder(cands, loc)
if #points == 0 then
FW:Print(FW.L.CODEX_ROUTE_NONE)
return false
end
self:Show(points, string.format(FW.L.CODEX_ROUTE_LABEL, #points), path, mapID)
routeGen = routeGen + 1
self.route = { cands = cands, skip = {}, points = points, mapID = mapID, gen = routeGen,
sig = RouteSignature(points), last = loc and { m = loc.mapID, x = loc.x, y = loc.y } or nil }
if _G.C_Timer then
_G.C_Timer.After(ROUTE_TICK, function() RouteTick(routeGen) end)
end
return true
end
FW:RegisterEvent("ADDON_LOADED", function(_, name)
if name == "Blizzard_WorldMap" then
Overlay:Hook()
Overlay:Refresh()
end
end, Overlay)
-- Diagnose: /fwcodex map
function Overlay:Debug()
local canvas = Canvas()
local d = self.lastDraw
FW:Print("Karte: " .. tostring(CurrentMapID()) .. ", Flaeche " .. tostring(canvas and canvas:GetWidth()) .. " x "
.. tostring(canvas and canvas:GetHeight()) .. ", Ebene " .. tostring(canvas and canvas:GetFrameLevel())
.. ", Overlay-Ebene " .. tostring(layer and layer:GetFrameLevel()))
FW:Print("Punkte geladen: " .. tostring(self.points and #self.points or 0) .. ", gezeichnet: " .. tostring(d and d.dots)
.. ", Maske: " .. tostring(MaskAvailable()) .. ", Skalierung: " .. string.format("%.2f", self:CanvasScale()))
local wm = _G.WorldMapFrame
FW:Print("Kartenfenster sichtbar: " .. tostring(wm and wm:IsShown()) .. ", Kartenflaeche sichtbar: " .. tostring(canvas and canvas:IsVisible())
.. ", Punkte-Ebene sichtbar: " .. tostring(layer and layer:IsVisible()) .. ", Ebenen-Skalierung: " .. string.format("%.2f", LayerScale()))
API.Debug()
local seen = {}
for _, p in ipairs(self.points or {}) do
if p.m and not seen[p.m] then
seen[p.m] = true
local c, z = API.Resolve(p.m)
FW:Print("Punkte-Karte " .. p.m .. " (" .. tostring(_G.C_Map and _G.C_Map.GetMapInfo and (_G.C_Map.GetMapInfo(p.m) or {}).name) .. ") -> Kontinent "
.. tostring(c) .. ", Zone " .. tostring(z))
end
end
local first = dots and dots[1]
if first then
FW:Print("Erster Punkt: sichtbar " .. tostring(first.fill:IsVisible()) .. ", Grösse " .. tostring(first.fill:GetWidth())
.. ", Ebene der Ebene sichtbar " .. tostring(layer and layer:IsVisible()))
end
end
-- Test: grosser roter Kasten in der Kartenmitte, zeigt ob ueberhaupt gezeichnet wird
function Overlay:Test()
local f = EnsureLayer()
if not f then
FW:Print("Weltkarte nicht gefunden.")
return
end
if not f.testTex then
f.testTex = f:CreateTexture(nil, "OVERLAY")
f.testTex:SetColorTexture(1, 0.1, 0.1, 0.6)
f.testTex:SetSize(160, 160)
f.testTex:SetPoint("CENTER", f, "CENTER", 0, 0)
end
if f.testTex:IsShown() then
f.testTex:Hide()
else
f.testTex:Show()
end
end
-- /fwcodex map square: Punkte als Quadrate (falls die runde Maske Probleme macht)
function Overlay:ToggleSquare()
local st = Codex.db.settings
st.squareDots = not st.squareDots
for _, d in ipairs(dots or {}) do
HideDot(d)
end
dots = {}
FW:Print("Codex-Karte: " .. (st.squareDots and "Quadrate" or "runde Punkte"))
self:Refresh()
end
+225
View File
@@ -0,0 +1,225 @@
-- Freewood-OctoWoW-Codex: Kartenschnittstelle fuer die Weltkarte des 1.12-Clients (OctoWoW).
-- Die alte Karte kennt weder SetMapID noch GetMapID noch eine Kartenflaeche mit Zoom. Stattdessen:
-- Kartenflaeche = WorldMapDetailFrame (Anteile 0..1 von links oben), Zone umschalten per SetMapZoom(Kontinent, Zone).
-- Die Zuordnung Karten-ID (AreaTable) -> Kontinent/Zone laeuft ueber den Zonennamen (C_Map.GetMapInfo gegen GetMapZones)
-- und wird zusaetzlich beim Spielen gelernt (stehst du in einer Zone, merkt sich Codex ihre Nummer).
-- Auf Clients mit Retail-Karte (SetMapID/GetMapID) werden diese Funktionen direkt benutzt.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local API = {}
Codex.MapAPI = API
local resolved = {} -- [mapID] = "c:z" oder false (nicht gefunden)
local zoneToID = {} -- ["c:z"] = mapID
local function Learned()
local st = Codex.db and Codex.db.settings
if not st then return {} end
st.mapZones = st.mapZones or {}
return st.mapZones
end
local function Norm(s)
if type(s) ~= "string" then return nil end
s = string.gsub(string.lower(s), "[^%w]", "")
if s == "" then return nil end
return s
end
function API.Classic()
local wm = _G.WorldMapFrame
return not (wm and wm.GetMapID and wm.SetMapID)
end
function API.Canvas()
local wm = _G.WorldMapFrame
if not wm then return nil end
if not API.Classic() then
if wm.GetCanvas then
local ok, c = pcall(wm.GetCanvas, wm)
if ok and c then return c end
end
return wm.ScrollContainer and wm.ScrollContainer.Child or nil
end
return _G.WorldMapDetailFrame or _G.WorldMapButton
end
-- Kontinent/Zone der angezeigten Karte
function API.Current()
if not (_G.GetCurrentMapContinent and _G.GetCurrentMapZone) then return nil end
local c, z = _G.GetCurrentMapContinent(), _G.GetCurrentMapZone()
if type(c) ~= "number" or type(z) ~= "number" then return nil end
return c, z
end
function API.Signature()
local c, z = API.Current()
return c and (c .. ":" .. z) or "?"
end
local function MapName(id)
local CM = _G.C_Map
if CM and CM.GetMapInfo then
local ok, info = pcall(CM.GetMapInfo, id)
if ok and type(info) == "table" and type(info.name) == "string" then return info.name end
end
if _G.C_Map and _G.C_Map.GetAreaInfo then
local ok, name = pcall(_G.C_Map.GetAreaInfo, id)
if ok and type(name) == "string" then return name end
end
return nil
end
-- Karten-ID -> Kontinent, Zone (Zone 0 = Kontinentkarte)
function API.Resolve(id)
if type(id) ~= "number" then return nil end
local key = Learned()[id] or resolved[id]
if key == nil then
key = false
local want = Norm(MapName(id))
if want and _G.GetMapContinents and _G.GetMapZones then
local conts = { _G.GetMapContinents() }
for c = 1, #conts do
if Norm(conts[c]) == want then key = c .. ":0" break end
local zones = { _G.GetMapZones(c) }
for z = 1, #zones do
if Norm(zones[z]) == want then key = c .. ":" .. z break end
end
if key then break end
end
end
resolved[id] = key
end
if not key then return nil end
local _, _, c, z = string.find(key, "^(%d+):(%d+)$")
return tonumber(c), tonumber(z)
end
-- Karten-ID der angezeigten Karte. cands: Liste oder Funktion, die Karten-IDs liefert, unter denen gesucht wird.
function API.CurrentMapID(cands)
local wm = _G.WorldMapFrame
if not API.Classic() then
if wm and wm.GetMapID then
local ok, id = pcall(wm.GetMapID, wm)
if ok then return id end
end
return nil
end
local c, z = API.Current()
if not c then return nil end
local key = c .. ":" .. z
if zoneToID[key] then return zoneToID[key] end
for id, k in pairs(Learned()) do
if k == key then zoneToID[key] = id return id end
end
if type(cands) == "function" then cands = cands() end
local list = cands or {}
local bm = _G.C_Map and _G.C_Map.GetBestMapForUnit and select(2, pcall(_G.C_Map.GetBestMapForUnit, "player"))
if type(bm) == "number" then list[#list + 1] = bm end
for i = 1, #list do
local id = list[i]
local rc, rz = API.Resolve(id)
if rc == c and rz == z then
zoneToID[key] = id
return id
end
end
return nil
end
-- Blizzards Kartenfenster (Titel, Auswahlfelder) nachziehen, falls es die Umschaltung nicht selbst tut
function API.RefreshWindow(c, z)
local names = { "WorldMapFrame_Update", "WorldMapContinentDropDown_Update", "WorldMapZoneDropDown_Update", "WorldMapZoomOutButton_Update" }
for i = 1, #names do
local f = _G[names[i]]
if type(f) == "function" then pcall(f) end
end
local title = _G.WorldMapFrameTitle or _G.WorldMapFrameTitleText
if title and title.SetText and _G.GetMapZones and _G.GetMapContinents then
local name
if z == 0 then
local conts = { _G.GetMapContinents() }
name = conts[c]
else
local zones = { _G.GetMapZones(c) }
name = zones[z]
end
if name then pcall(title.SetText, title, name) end
end
end
function API.SetMap(id)
local wm = _G.WorldMapFrame
if not API.Classic() then
if wm and wm.SetMapID then pcall(wm.SetMapID, wm, id) return true end
return false
end
local c, z = API.Resolve(id)
if not c or not _G.SetMapZoom then return false end
if z == 0 then pcall(_G.SetMapZoom, c) else pcall(_G.SetMapZoom, c, z) end
API.RefreshWindow(c, z)
if _G.C_Timer and _G.C_Timer.After then
_G.C_Timer.After(0.2, function() API.RefreshWindow(c, z) end)
end
return true
end
-- Mauszeiger auf der Kartenflaeche als Anteil 0..1 (links oben), sonst nil
function API.CursorNorm()
local canvas = API.Canvas()
if not (canvas and canvas.IsVisible and canvas:IsVisible()) then return nil end
local l, t, w, h = canvas:GetLeft(), canvas:GetTop(), canvas:GetWidth(), canvas:GetHeight()
if not (l and t and w and h and w > 1 and h > 1) then return nil end
local cx, cy = _G.GetCursorPosition()
local s = canvas:GetEffectiveScale()
if not (cx and s and s > 0) then return nil end
local x, y = (cx / s - l) / w, (t - cy / s) / h
if x < 0 or x > 1 or y < 0 or y > 1 then return nil end
return x, y
end
-- Zonen-Nummern lernen: Karte kurz auf die aktuelle Zone stellen, solange das Kartenfenster zu ist
local function LearnHere()
local wm = _G.WorldMapFrame
if not API.Classic() or (wm and wm:IsShown()) then return end
if not (_G.SetMapToCurrentZone and _G.C_Map and _G.C_Map.GetBestMapForUnit) then return end
local ok, id = pcall(_G.C_Map.GetBestMapForUnit, "player")
if not ok or type(id) ~= "number" then return end
pcall(_G.SetMapToCurrentZone)
local c, z = API.Current()
if c and z and z > 0 then
local L = Learned()
L[id] = c .. ":" .. z
zoneToID[c .. ":" .. z] = id
end
end
local function LearnLater()
if _G.C_Timer and _G.C_Timer.After then
_G.C_Timer.After(1.5, LearnHere)
else
LearnHere()
end
end
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("ZONE_CHANGED_NEW_AREA")
f:SetScript("OnEvent", LearnLater)
-- /fwcodex mapzones: zeigt die Zuordnung fuer die angezeigte Karte
function API.Debug()
local c, z = API.Current()
FW:Print("Karte: Kontinent " .. tostring(c) .. ", Zone " .. tostring(z) .. ", Flaeche "
.. tostring(API.Canvas() and API.Canvas():GetName()) .. ", klassisch: " .. tostring(API.Classic()))
local n = 0
for id, k in pairs(Learned()) do n = n + 1 end
FW:Print("Gelernte Zonen: " .. n)
end
+304
View File
@@ -0,0 +1,304 @@
-- Freewood-OctoWoW-Codex: Karten-Scan (ClassicAPI C_Map, C_TaxiMap)
-- Liest lokal (ohne Serveranfragen) alle Karten, Gebiete, Gebiets-Trigger und Flugpunkte aus dem Client.
-- Ablage in db.cat: m = Karten, a = Gebiete, ma = Gebiete je Karte, t = Trigger je Karte, x = Flugpunkte, shape = Aufbau der Antworten (zur Pruefung)
-- m[id] = "Feld=Wert;...|Name" a[id] = "Name" (oder "Feld=Wert;...|Name") t[mapID] = { "Feld=Wert;...", ... } x[nodeID] = "Feld=Wert;...|Name"
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Scan = { }
Codex.MapScan = Scan
NS.MapScan = Scan
local MAX_MAP = 2500
local MAX_AREA = 6000
local SLICE = 150 -- Eintraege je Schritt
local TICK = 0.05
local function Say(text) FW:Print("Karten-Scan: " .. text) 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.m = c.m or {}
c.a = c.a or {}
c.ma = c.ma or {}
c.t = c.t or {}
c.x = c.x or {}
c.shape = c.shape or {}
return c
end
local function Clean(t)
t = tostring(t):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|", "/"):gsub(";", ",")
return t
end
-- Aufbau einer Antwort als Text (nur die erste Antwort je Funktion), damit man die Felder pruefen kann
local function Describe(v, depth)
depth = depth or 0
local tv = type(v)
if tv ~= "table" then return tv .. (tv == "number" and ("=" .. tostring(v)) or (tv == "string" and ("=" .. v:sub(1, 20)) or "")) end
if depth >= 2 then return "table" end
local keys = {}
for k in pairs(v) do keys[#keys + 1] = k end
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
local parts = {}
for i, k in ipairs(keys) do
if i > 24 then parts[#parts + 1] = "..." break end
parts[#parts + 1] = tostring(k) .. ":" .. Describe(v[k], depth + 1)
end
return "{" .. table.concat(parts, ", ") .. "}"
end
local function Shape(name, v)
local c = Store()
if c and c.shape[name] == nil then c.shape[name] = Describe(v):sub(1, 700) end
end
-- einfache Felder als "Feld=Wert;..." (Zahlen, Wahrheitswerte, Zahlenlisten, x/y-Paare)
local function Pack(info, skip)
local keys = {}
for k, v in pairs(info) do
local tv = type(v)
if type(k) == "string" and k ~= skip 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("%.4f", v))
elseif type(v) == "boolean" then
parts[#parts + 1] = k .. "=" .. (v and "1" or "0")
else
if type(v.x) == "number" and type(v.y) == "number" then
parts[#parts + 1] = k .. "=" .. string.format("%.4f:%.4f", v.x, v.y)
else
local list = {}
for i = 1, 40 do
if type(v[i]) == "number" then list[#list + 1] = tostring(v[i]) else break end
end
if #list > 0 then parts[#parts + 1] = k .. "=" .. table.concat(list, ",") end
end
end
end
return table.concat(parts, ";")
end
local function Name(info)
if type(info) ~= "table" then return nil end
local n = info.name or info.areaName or info.mapName
return type(n) == "string" and n ~= "" and n or nil
end
local function Count(t) local n = 0 for _ in pairs(t or {}) do n = n + 1 end return n end
-- Phasen: jede holt Eintraege in Scheiben
local phases = {}
phases[1] = { label = "Karten", total = function() return MAX_MAP end, step = function(c, id)
local CM = _G.C_Map
if not (CM and CM.GetMapInfo) then return end
local ok, info = pcall(CM.GetMapInfo, id)
if ok and type(info) == "table" then
local nm = Name(info)
if nm then
Shape("GetMapInfo", info)
c.m[id] = Pack(info, "name") .. "|" .. Clean(nm)
end
end
end }
phases[2] = { label = "Gebiete", total = function() return MAX_AREA end, step = function(c, id)
local CM = _G.C_Map
if not (CM and CM.GetAreaInfo) then return end
local ok, r = pcall(CM.GetAreaInfo, id)
if not ok then return end
if type(r) == "string" and r ~= "" then
Shape("GetAreaInfo", r)
c.a[id] = Clean(r)
elseif type(r) == "table" and Name(r) then
Shape("GetAreaInfo", r)
c.a[id] = Pack(r, "name") .. "|" .. Clean(Name(r))
end
end }
-- Karten-IDs fuer die Phasen 3 und 4
local mapIDs
local function MapIDs(c)
if not mapIDs then
-- Karten aus der Kartenliste plus die Welt-/Instanzkarten (0 bis 700): Trigger und Flugpunkte haengen an der Welt-Karten-ID
local seen = {}
mapIDs = {}
for id = 0, 700 do seen[id] = true mapIDs[#mapIDs + 1] = id end
for id in pairs(c.m) do
if type(id) == "number" and not seen[id] then seen[id] = true mapIDs[#mapIDs + 1] = id end
end
table.sort(mapIDs)
end
return mapIDs
end
phases[3] = { label = "Trigger", total = function() local c = Store() return #MapIDs(c) end, step = function(c, i)
local CM = _G.C_Map
local id = MapIDs(c)[i]
if not (CM and id) then return end
if CM.GetAreaTriggers then
local ok, list = pcall(CM.GetAreaTriggers, id)
if ok and type(list) == "table" and next(list) then
Shape("GetAreaTriggers", list)
local rows = {}
for _, t in pairs(list) do
if type(t) == "table" then
rows[#rows + 1] = Pack(t)
elseif type(t) == "number" then
rows[#rows + 1] = "id=" .. t
end
end
if #rows > 0 then c.t[id] = rows end
end
end
-- Gebiete je Karte (zwei moegliche Funktionen)
for _, fname in ipairs({ "GetMapAreaIDs", "GetAreas" }) do
local f = CM[fname]
if f and not Scan.maDone then
local ok, list = pcall(f, id)
if ok and type(list) == "table" and next(list) then
Shape(fname, list)
local ids = {}
for k, v in pairs(list) do
if type(v) == "number" then ids[#ids + 1] = v
elseif type(v) == "table" then
local a = v.areaID or v.id
if type(a) == "number" then ids[#ids + 1] = a end
elseif type(k) == "number" and type(v) == "string" then ids[#ids + 1] = k end
end
table.sort(ids)
if #ids > 0 then c.ma[0] = table.concat(ids, ",") Scan.maDone = true end
end
end
end
end }
local function TakeNodes(c, list)
if type(list) ~= "table" then return end
for _, n in pairs(list) do
if type(n) == "table" and type(n.nodeID) == "number" then
Shape("TaxiNode", n)
c.x[n.nodeID] = Pack(n, "name") .. "|" .. Clean(Name(n) or "")
end
end
end
phases[4] = { label = "Flugpunkte", total = function() local c = Store() return #MapIDs(c) + 1 end, step = function(c, i)
local T = _G.C_TaxiMap
if not T then return end
local ids = MapIDs(c)
if i == 1 and T.GetAllTaxiNodes then
local ok, list = pcall(T.GetAllTaxiNodes)
if ok then TakeNodes(c, list) end
end
local id = ids[i - 1]
if id and T.GetAllTaxiNodes then
local ok, list = pcall(T.GetAllTaxiNodes, id)
if ok then TakeNodes(c, list) end
end
if id and T.GetTaxiNodesForMap then
local ok, list = pcall(T.GetTaxiNodesForMap, id)
if ok then TakeNodes(c, list) end
end
end }
Scan.state = nil
function Scan.Counts()
local c = Codex.db and Codex.db.cat
if not c then return 0, 0, 0, 0 end
return Count(c.m), Count(c.a), Count(c.t), Count(c.x)
end
function Scan.Running() return Scan.state ~= nil and not Scan.state.stop end
local function Tick(st)
if Scan.state ~= st then return end
if st.stop then Scan.state = nil return end
local c = Store()
local ph = phases[st.phase]
if not (c and ph) then Scan.state = nil return end
local total = ph.total()
local last = math.min(st.i + SLICE - 1, total)
for i = st.i, last do
local ok = pcall(ph.step, c, i)
if not ok then st.errors = (st.errors or 0) + 1 end
end
st.i = last + 1
st.total = total
if st.i > total then
Say(string.format("%s fertig.", ph.label))
if st.phase == 1 then mapIDs = nil end
st.phase = st.phase + 1
st.i = 1
if not phases[st.phase] then
local m, a, t, x = Scan.Counts()
Say(string.format("fertig: %d Karten, %d Gebiete, Trigger auf %d Karten, %d Flugpunkte%s.", m, a, t, x,
st.errors and (" (" .. st.errors .. " Fehler)") or ""))
Scan.state = nil
if FW.Fire then FW:Fire("CODEX_CHANGED", "mapscan") end
return
end
end
_G.C_Timer.After(TICK, function() Tick(st) end)
end
-- Rueckgabe: true | false, Grund
function Scan.Start()
if Scan.state then return false, "busy" end
local CM = _G.C_Map
if not (CM and CM.GetMapInfo) or not _G.C_Timer then return false, "api" end
if not Store() then return false, "db" end
mapIDs = nil
Scan.maDone = false
local st = { phase = 1, i = 1, total = MAX_MAP }
Scan.state = st
Tick(st)
return true
end
function Scan.Stop()
if Scan.state then Scan.state.stop = true end
end
function Scan.Label()
local st = Scan.state
if not st then return nil end
return phases[st.phase] and phases[st.phase].label or "", math.min(st.i, st.total or 1), st.total or 1
end
SLASH_FREEWOODMAPSCAN1 = "/fwkarte"
SlashCmdList["FREEWOODMAPSCAN"] = function(msg)
msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
if msg == "stop" then
if Scan.Running() then Scan.Stop() Say("wird angehalten.") else Say("es laeuft nichts.") end
return
elseif msg == "status" then
local m, a, t, x = Scan.Counts()
Say(string.format("%d Karten, %d Gebiete, Trigger auf %d Karten, %d Flugpunkte.", m, a, t, x))
return
end
local ok, why = Scan.Start()
if ok then
Say("gestartet (Karten, Gebiete, Trigger, Flugpunkte; lokal, ohne Serveranfragen). Anhalten: /fwkarte stop.")
elseif why == "busy" then
Say("es laeuft schon ein Scan.")
else
Say("nicht moeglich: " .. tostring(why))
end
end
+805
View File
@@ -0,0 +1,805 @@
-- Freewood-OctoWoW-Codex: Namen von Hand eintragen (Deutsch / Englisch)
-- Zwei Fenster im Tab "Datenbank":
-- "Name bearbeiten" fuer den gewaehlten Eintrag (Kreatur, Objekt, Quest, Item), mit Vorschlaegen aus dem Kampflog
-- "Namen einfuegen" mehrere Zeilen "ID Name" auf einmal fuer die aktuelle Liste
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex or not FW.UI then
return
end
local UI = FW.UI
local Util = FW.Util
local NameUI = {}
NS.NameUI = NameUI
Codex.NameUI = NameUI
local EDITABLE = { units = true, objects = true, quests = true, items = true }
local KIND_LABEL = { units = "CODEX_KIND_UNITS", objects = "CODEX_KIND_OBJECTS", quests = "CODEX_KIND_QUESTS", items = "CODEX_KIND_ITEMS" }
local MAX_SUGGEST = 6
local function MakeInput(parent, width)
local box = UI.CreatePanel(parent, "bgDeep", "bronze")
box:SetSize(width, 26)
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(100)
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)
box.edit = edit
return box
end
-- Wowhead-Adresse fuer Nachschlagen (an einer Stelle, falls sich die Domain aendert)
local WOWHEAD_BASE = "https://www.wowhead.com/forever/"
local WOWHEAD_PATH = { units = "npc", objects = "object", quests = "quest", items = "item" }
function NameUI.LookupURL(kind, id, lang)
local path = WOWHEAD_PATH[kind]
if not path or not id then return nil end
return WOWHEAD_BASE .. (lang == "de" and "de/" or "") .. path .. "=" .. tostring(id)
end
-- Schreibgeschuetztes Feld: Klick markiert alles, dann Strg+C
local function MakeLink(parent, width)
local box = MakeInput(parent, width)
box:SetHeight(22)
local edit = box.edit
edit:SetMaxLetters(0)
edit:SetTextColor(UI.Color("goldLight"))
local function Select(self) self:HighlightText() end
edit:SetScript("OnEditFocusGained", Select)
edit:SetScript("OnMouseUp", function(self) self:SetFocus(); self:HighlightText() end)
edit:SetScript("OnTextChanged", function(self, user)
if user and box.fwUrl and self:GetText() ~= box.fwUrl then
self:SetText(box.fwUrl)
self:HighlightText()
end
end)
function box.SetURL(url)
box.fwUrl = url or ""
edit:SetText(url or "")
edit:SetCursorPosition(0)
end
return box
end
local function MakeWindow(parent, width, height)
local w = UI.CreatePanel(parent, "bg", "gold")
w:SetSize(width, height)
w:SetPoint("CENTER", parent, "CENTER", 0, 0)
w:SetFrameStrata("DIALOG")
w:EnableMouse(true)
w:Hide()
return w
end
local function GameName(kind, id)
local v = Codex.V()
if kind == "units" then
local e = v.units[id]
return e and e.n
elseif kind == "objects" then
local e = v.objects[id]
return e and e.n
elseif kind == "quests" then
local q = v.quests[id]
return q and q.t
end
local info = Util.GetItem and Util.GetItem(id)
return (info and info.name) or Codex.db.names[id]
end
------------------------------------------------------------------------
-- Fenster "Name bearbeiten"
------------------------------------------------------------------------
local function BuildEdit(parent, onChanged)
local L = FW.L
local win = MakeWindow(parent, 480, 520)
local state = {}
local title = UI.CreateLabel(win, "", { font = "heading", size = 14, color = "goldLight" })
title:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -14)
local game = UI.CreateLabel(win, "", { size = 11, color = "textDim" })
game:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -40)
game:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -40)
game:SetWordWrap(false)
local deLabel = UI.CreateLabel(win, L.CODEX_NAME_DE, { size = 12 })
deLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -68)
local deBox = MakeInput(win, 448)
deBox:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -86)
local deLink = MakeLink(win, 448)
deLink:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -114)
local enLabel = UI.CreateLabel(win, L.CODEX_NAME_EN, { size = 12 })
enLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -146)
local enBox = MakeInput(win, 448)
enBox:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -164)
local enLink = MakeLink(win, 448)
enLink:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -192)
local hint = UI.CreateLabel(win, "", { size = 11, color = "textDim", justifyV = "TOP" })
hint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -222)
hint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -222)
hint:SetHeight(32)
hint:SetWordWrap(true)
local sugTitle = UI.CreateLabel(win, L.CODEX_NAME_KILLS, { size = 12, color = "goldLight" })
sugTitle:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -260)
local sugNone = UI.CreateLabel(win, L.CODEX_NAME_NOKILLS, { size = 11, color = "textDim", justifyV = "TOP" })
sugNone:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -284)
sugNone:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -284)
sugNone:SetHeight(32)
sugNone:SetWordWrap(true)
local function Fill(text)
local box = Codex.Lang() == "en" and enBox or deBox
box.edit:SetText(text or "")
box.edit:SetFocus()
end
local sugButtons = {}
for i = 1, MAX_SUGGEST do
local b = UI.CreateButton(win, "", 448, 24, function(self)
if self.fwText then Fill(self.fwText) end
end, "default")
b:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -284 - (i - 1) * 28)
b:Hide()
sugButtons[i] = b
end
local function Save()
if state.kind and state.id then
Codex:SetCustom(state.kind, state.id, deBox.edit:GetText(), enBox.edit:GetText())
if onChanged then onChanged() end
end
win:Hide()
end
local save = UI.CreateButton(win, L.CODEX_NAME_SAVE, 140, 28, Save, "primary")
save:SetPoint("BOTTOMLEFT", win, "BOTTOMLEFT", 16, 14)
local clear = UI.CreateButton(win, L.CODEX_NAME_CLEAR, 140, 28, function()
deBox.edit:SetText("")
enBox.edit:SetText("")
Save()
end, "default")
clear:SetPoint("LEFT", save, "RIGHT", 8, 0)
local cancel = UI.CreateButton(win, L.CODEX_NAME_CANCEL, 140, 28, function() win:Hide() end, "default")
cancel:SetPoint("LEFT", clear, "RIGHT", 8, 0)
function win.Open(kind, id)
state.kind, state.id = kind, id
local label = L[KIND_LABEL[kind]] or kind
title:SetText(string.format("%s #%s", label, tostring(id)))
local gn = GameName(kind, id)
game:SetText(gn and string.format(L.CODEX_NAME_GAME, gn) or "")
local de, en = Codex:GetCustom(kind, id)
deBox.edit:SetText(de or "")
enBox.edit:SetText(en or "")
hint:SetText((kind == "items" and L.CODEX_NAME_HINT_LOCAL or L.CODEX_NAME_HINT) .. "\n" .. (L.CODEX_NAME_LINK or ""))
deLink.SetURL(NameUI.LookupURL(kind, id, "de"))
enLink.SetURL(NameUI.LookupURL(kind, id, "en"))
local names = {}
if kind == "units" and Codex.Names and Codex.Names.RecentKills then
for _, k in ipairs(Codex.Names:RecentKills(900, MAX_SUGGEST)) do
names[#names + 1] = k.name
if k.alt then names[#names + 1] = k.alt end
end
end
local showSug = kind == "units"
if showSug then sugTitle:Show() else sugTitle:Hide() end
if showSug and #names == 0 then sugNone:Show() else sugNone:Hide() end
for i, b in ipairs(sugButtons) do
local text = names[i]
if text then
b.fwText = text
b:SetLabel(text)
b:Show()
else
b.fwText = nil
b:Hide()
end
end
win.fwKey = kind .. ":" .. tostring(id)
win:Show()
win:Raise()
end
return win
end
------------------------------------------------------------------------
-- Fenster "Eigenschaften bearbeiten": Zone(n), Dungeon, Klasse, Stufen
-- (Blizzard liefert aus Instanzen keine Orte und oft keine Klasse; hier koennen Berechtigte sie nachtragen)
------------------------------------------------------------------------
local mapIndex
-- Alle Karten des Spiels (Name -> ID), einmal gelesen
local function MapIndex()
if mapIndex then return mapIndex end
mapIndex = {}
if C_Map and C_Map.GetMapInfo then
for id = 1, 3000 do
local ok, info = pcall(C_Map.GetMapInfo, id)
if ok and type(info) == "table" and type(info.name) == "string" and info.name ~= "" then
mapIndex[#mapIndex + 1] = { id = id, name = info.name, lower = info.name:lower() }
end
end
end
return mapIndex
end
-- Karten, deren Name den Text enthaelt (oder deren ID passt). Rueckgabe: Liste { id, name }
function NameUI.FindMaps(text, limit)
local out = {}
text = type(text) == "string" and text:lower():gsub("^%s+", ""):gsub("%s+$", "") or ""
if text == "" then return out end
local num = tonumber(text)
if num and num >= 1 and num == math.floor(num) then
out[1] = { id = num, name = Codex.MapName(num) }
end
-- eingebaute Instanzen (Dungeons, Schlachtfelder): negative ID
local ids = {}
for id in pairs(Codex.INSTANCES) do ids[#ids + 1] = id end
table.sort(ids)
for _, id in ipairs(ids) do
if #out >= (limit or 6) then break end
local b = Codex.INSTANCES[id]
if b[1]:lower():find(text, 1, true) or b[2]:lower():find(text, 1, true) then
out[#out + 1] = { id = -id, name = Codex.InstanceName(id) }
end
end
for _, m in ipairs(MapIndex()) do
if #out >= (limit or 6) then break end
if m.lower:find(text, 1, true) and m.id ~= num then
out[#out + 1] = { id = m.id, name = m.name }
end
end
return out
end
local CLASS_CYCLE = { false, "normal", "elite", "rare", "rareelite", "worldboss" }
local function ClassLabel(cl)
local L = FW.L
if not cl then return L.CODEX_PROPS_AUTO end
return L["CODEX_PROPS_CLASS_" .. cl] or cl
end
local function BuildProps(parent, onChanged)
local L = FW.L
local win = MakeWindow(parent, 520, 650)
local state = { z = {} }
local MAXZ = Codex.MAX_ED_ZONES
local title = UI.CreateLabel(win, "", { font = "heading", size = 14, color = "goldLight" })
title:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -14)
local game = UI.CreateLabel(win, "", { size = 11, color = "textDim" })
game:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -40)
game:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -40)
game:SetWordWrap(false)
-- Zonen
local zoneTitle = UI.CreateLabel(win, L.CODEX_PROPS_ZONES, { size = 12, color = "goldLight" })
zoneTitle:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -68)
local zoneRows = {}
for i = 1, MAXZ do local i = i
local lbl = UI.CreateLabel(win, "", { size = 12 })
lbl:SetPoint("TOPLEFT", win, "TOPLEFT", 24, -92 - (i - 1) * 24)
lbl:SetWidth(400)
lbl:SetWordWrap(false)
local rm = UI.CreateButton(win, "x", 24, 20, function()
table.remove(state.z, i)
win.Refresh()
end, "default")
rm:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -90 - (i - 1) * 24)
zoneRows[i] = { lbl = lbl, rm = rm }
end
local zoneNone = UI.CreateLabel(win, L.CODEX_PROPS_ZONES_NONE, { size = 11, color = "textDim" })
zoneNone:SetPoint("TOPLEFT", win, "TOPLEFT", 24, -92)
local zoneInput = MakeInput(win, 300)
zoneInput:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -92 - MAXZ * 24 - 8)
local sugBtns = {}
local function AddZone(id)
if id and #state.z < MAXZ then
for _, z in ipairs(state.z) do if z == id then return end end
state.z[#state.z + 1] = id
end
zoneInput.edit:SetText("")
win.Refresh()
end
local function Suggest()
local found = NameUI.FindMaps(zoneInput.edit:GetText(), #sugBtns)
for i, b in ipairs(sugBtns) do
local m = found[i]
if m then
b.fwId = m.id
b:SetLabel(string.format("%s (#%d)", m.name, m.id < 0 and -m.id or m.id))
b:Show()
else
b.fwId = nil
b:Hide()
end
end
end
zoneInput.edit:SetScript("OnTextChanged", function(_, user) if user then Suggest() end end)
local here = UI.CreateButton(win, L.CODEX_PROPS_HERE, 170, 26, function()
local inst = Codex.CurrentInstance and Codex.CurrentInstance()
if inst then
AddZone(-inst)
return
end
local m = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
if m then AddZone(m) end
end, "default")
here:SetPoint("LEFT", zoneInput, "RIGHT", 8, 0)
for i = 1, 5 do
local b = UI.CreateButton(win, "", 488, 22, function(self)
if self.fwId then AddZone(self.fwId) end
end, "default")
b:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -92 - MAXZ * 24 - 40 - (i - 1) * 24)
b:Hide()
sugBtns[i] = b
end
local zoneHint = UI.CreateLabel(win, L.CODEX_PROPS_ZONE_HINT, { size = 11, color = "textDim", justifyV = "TOP" })
zoneHint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -92 - MAXZ * 24 - 40 - 5 * 24 - 2)
zoneHint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -92 - MAXZ * 24 - 40 - 5 * 24 - 2)
zoneHint:SetHeight(30)
zoneHint:SetWordWrap(true)
-- Dungeon / Klasse / Stufen
local baseY = 92 + MAXZ * 24 + 40 + 5 * 24 + 36
local dgLabel = UI.CreateLabel(win, L.CODEX_PROPS_DUNGEON, { size = 12 })
dgLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 4)
local dgBtn = UI.CreateButton(win, "", 200, 24, function()
if state.dg == nil then state.dg = true elseif state.dg == true then state.dg = false else state.dg = nil end
win.Refresh()
end, "default")
dgBtn:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY)
local clLabel = UI.CreateLabel(win, L.CODEX_PROPS_CLASS, { size = 12 })
clLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 36)
local clBtn = UI.CreateButton(win, "", 200, 24, function()
local idx = 1
for i, c in ipairs(CLASS_CYCLE) do if c == (state.cl or false) then idx = i end end
state.cl = CLASS_CYCLE[idx % #CLASS_CYCLE + 1] or nil
if state.cl == false then state.cl = nil end
win.Refresh()
end, "default")
clBtn:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - 32)
local lvLabel = UI.CreateLabel(win, L.CODEX_PROPS_LEVELS, { size = 12 })
lvLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 72)
local l1Box = MakeInput(win, 70)
l1Box:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - 68)
l1Box.edit:SetMaxLetters(3)
local lvDash = UI.CreateLabel(win, "-", { size = 12 })
lvDash:SetPoint("LEFT", l1Box, "RIGHT", 6, 0)
local l2Box = MakeInput(win, 70)
l2Box:SetPoint("LEFT", l1Box, "RIGHT", 20, 0)
l2Box.edit:SetMaxLetters(3)
-- Quests: Vorquests (an der Stelle der Stufen)
local preLabel = UI.CreateLabel(win, L.CODEX_PROPS_PRE, { size = 12 })
preLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 72)
local preBox = MakeInput(win, 300)
preBox:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - 68)
preBox.edit:SetMaxLetters(60)
-- Quests: Folgequests, davor, danach, Stufe, Questtext (Dev)
local QROW = { nx = 36, bf = 108, af = 140, ql = 172, tx = 204, si = 236 }
local qLabels, qBoxes = {}, {}
local function QField(key, label, width, letters, y)
local lbl = UI.CreateLabel(win, label, { size = 12 })
lbl:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - y - 4)
local box = MakeInput(win, width)
box:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - y)
box.edit:SetMaxLetters(letters)
qLabels[key], qBoxes[key] = lbl, box
end
QField("nx", L.CODEX_PROPS_NEXT, 300, 60, QROW.nx)
QField("bf", L.CODEX_PROPS_BEFORE, 300, 60, QROW.bf)
QField("af", L.CODEX_PROPS_AFTER, 300, 60, QROW.af)
QField("ql", L.CODEX_PROPS_QLEVEL, 70, 3, QROW.ql)
QField("tx", L.CODEX_PROPS_TEXT, 300, 600, QROW.tx)
QField("si", L.CODEX_PROPS_STARTITEM, 120, 9, QROW.si)
-- Kreaturen und Objekte: Quest-Zuordnung von Hand (Ziel von Quests, laesst Quest-Items fallen)
local uLabels, uBoxes = {}, {}
local function UField(key, label, width, letters, y)
local lbl = UI.CreateLabel(win, label, { size = 12 })
lbl:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - y - 4)
local box = MakeInput(win, width)
box:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - y)
box.edit:SetMaxLetters(letters)
uLabels[key], uBoxes[key] = lbl, box
end
UField("qk", L.CODEX_PROPS_QK, 300, 80, 108)
UField("qd", L.CODEX_PROPS_QD, 300, 120, 140)
UField("qb", L.CODEX_PROPS_QB, 300, 80, 172)
QField("qr", L.CODEX_PROPS_QR, 300, 200, 268)
local hint = UI.CreateLabel(win, L.CODEX_PROPS_HINT, { size = 11, color = "textDim", justifyV = "TOP" })
hint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 104)
hint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -baseY - 104)
hint:SetHeight(58)
hint:SetWordWrap(true)
function win.Refresh()
for i = 1, MAXZ do
local row = zoneRows[i]
local id = state.z[i]
if id then
row.lbl:SetText(string.format("%s |cff9a978c#%d|r", Codex.MapName(id), id < 0 and -id or id))
row.lbl:Show()
row.rm:Show()
else
row.lbl:Hide()
row.rm:Hide()
end
end
if #state.z == 0 then zoneNone:Show() else zoneNone:Hide() end
dgBtn:SetLabel(state.dg == nil and L.CODEX_PROPS_UNKNOWN or (state.dg and L.CODEX_PROPS_YES or L.CODEX_PROPS_NO))
local isUnit = state.kind == "units"
local hasDungeon = state.kind ~= nil
if hasDungeon then dgLabel:Show(); dgBtn:Show() end
if isUnit then
clLabel:Show(); clBtn:Show(); lvLabel:Show(); l1Box:Show(); l2Box:Show(); lvDash:Show()
clBtn:SetLabel(ClassLabel(state.cl))
else
clLabel:Hide(); clBtn:Hide(); lvLabel:Hide(); l1Box:Hide(); l2Box:Hide(); lvDash:Hide()
end
if state.kind == "quests" or state.kind == "items" then
preLabel:SetText(state.kind == "items" and L.CODEX_PROPS_KEY or L.CODEX_PROPS_PRE)
preLabel:Show(); preBox:Show()
else
preLabel:Hide(); preBox:Hide()
end
local isQuest = state.kind == "quests"
for key, lbl in pairs(qLabels) do
if isQuest then lbl:Show(); qBoxes[key]:Show() else lbl:Hide(); qBoxes[key]:Hide() end
end
local isEnt = state.kind == "units" or state.kind == "objects"
local isItem = state.kind == "items"
for key, lbl in pairs(uLabels) do
local show = isEnt or (isItem and key == "qb")
if show then lbl:Show(); uBoxes[key]:Show() else lbl:Hide(); uBoxes[key]:Hide() end
end
-- bei Items steht "Gehoert zu Quest" direkt unter dem Schluessel-Feld
local qbY = isItem and 108 or 172
uLabels.qb:ClearAllPoints()
uLabels.qb:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - qbY - 4)
uBoxes.qb:ClearAllPoints()
uBoxes.qb:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - qbY)
-- fuer Quests (und Kreaturen/Objekte mit Quest-Zeilen) ist das Fenster hoeher, der Hinweis rutscht nach unten
win:SetHeight(isQuest and (baseY + 430) or (isEnt and (baseY + 340) or (isItem and 700 or 650)))
hint:ClearAllPoints()
local hy = isQuest and (268 + 40) or (isEnt and 212 or (isItem and 148 or 104))
hint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - hy)
hint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -baseY - hy)
end
local function Collect()
local ed = { z = {}, dg = state.dg, cl = state.cl }
for i, z in ipairs(state.z) do ed.z[i] = z end
if state.kind == "units" then
ed.l1, ed.l2 = tonumber(l1Box.edit:GetText()), tonumber(l2Box.edit:GetText())
elseif state.kind == "quests" then
local pre = {}
for n in (preBox.edit:GetText() or ""):gmatch("%d+") do pre[#pre + 1] = tonumber(n) end
if #pre > 0 then ed.pre = pre end
for _, key in ipairs({ "nx", "bf", "af" }) do
local list = {}
for n in (qBoxes[key].edit:GetText() or ""):gmatch("%d+") do list[#list + 1] = tonumber(n) end
if #list > 0 then ed[key] = list end
end
ed.ql = tonumber(qBoxes.ql.edit:GetText())
local tx = qBoxes.tx.edit:GetText()
if tx and tx:find("%S") then ed.tx = tx end
ed.si = tonumber((qBoxes.si.edit:GetText() or ""):match("%d+"))
elseif state.kind == "items" then
ed.key = tonumber((preBox.edit:GetText() or ""):match("%d+"))
end
if state.kind == "units" or state.kind == "objects" then
local qk, qd = {}, {}
for n in (uBoxes.qk.edit:GetText() or ""):gmatch("%d+") do qk[#qk + 1] = tonumber(n) end
for q, it in (uBoxes.qd.edit:GetText() or ""):gmatch("(%d+)%s*[:/]%s*(%d+)") do qd[#qd + 1] = q .. ":" .. it end
if #qk > 0 then ed.qk = qk end
if #qd > 0 then ed.qd = qd end
end
if state.kind == "units" or state.kind == "objects" or state.kind == "items" then
local qb = {}
for n in (uBoxes.qb.edit:GetText() or ""):gmatch("%d+") do qb[#qb + 1] = tonumber(n) end
if #qb > 0 then ed.qb = qb end
end
if state.kind == "quests" then
local qr = {}
for k, n in (qBoxes.qr.edit:GetText() or ""):gmatch("([iuoIUO])%s*[:#]?%s*(%d+)") do qr[#qr + 1] = k:lower() .. ":" .. n end
if #qr > 0 then ed.qr = qr end
end
return ed
end
local function Save()
if state.kind and state.id then
Codex:SetEd(state.kind, state.id, Collect())
if onChanged then onChanged() end
end
win:Hide()
end
local save = UI.CreateButton(win, L.CODEX_NAME_SAVE, 140, 28, Save, "primary")
save:SetPoint("BOTTOMLEFT", win, "BOTTOMLEFT", 16, 14)
local clear = UI.CreateButton(win, L.CODEX_NAME_CLEAR, 140, 28, function()
state.z, state.dg, state.cl = {}, nil, nil
l1Box.edit:SetText("")
l2Box.edit:SetText("")
preBox.edit:SetText("")
for _, b in pairs(qBoxes) do b.edit:SetText("") end
for _, b in pairs(uBoxes) do b.edit:SetText("") end
Save()
end, "default")
clear:SetPoint("LEFT", save, "RIGHT", 8, 0)
local cancel = UI.CreateButton(win, L.CODEX_NAME_CANCEL, 140, 28, function() win:Hide() end, "default")
cancel:SetPoint("LEFT", clear, "RIGHT", 8, 0)
function win.Open(kind, id)
state.kind, state.id = kind, id
local ed = Codex:GetEd(kind, id) or {}
state.z = {}
for i, z in ipairs(ed.z or {}) do state.z[i] = z end
state.dg, state.cl = ed.dg, ed.cl
l1Box.edit:SetText(ed.l1 and tostring(ed.l1) or "")
l2Box.edit:SetText(ed.l2 and tostring(ed.l2) or "")
preBox.edit:SetText(kind == "items" and (ed.key and tostring(ed.key) or "") or (ed.pre and table.concat(ed.pre, ", ") or ""))
hint:SetText(kind == "quests" and (L.CODEX_PROPS_PRE_HINT .. "\n" .. L.CODEX_PROPS_HINT)
or (kind == "items" and (L.CODEX_PROPS_KEY_HINT .. "\n" .. L.CODEX_PROPS_HINT))
or ((kind == "units" or kind == "objects") and (L.CODEX_PROPS_Q_HINT .. "\n" .. L.CODEX_PROPS_HINT)) or L.CODEX_PROPS_HINT)
if kind == "quests" then hint:SetText(L.CODEX_PROPS_QR_HINT .. "\n" .. L.CODEX_PROPS_PRE_HINT .. "\n" .. L.CODEX_PROPS_HINT) end
for _, key in ipairs({ "nx", "bf", "af" }) do
qBoxes[key].edit:SetText(ed[key] and table.concat(ed[key], ", ") or "")
end
qBoxes.ql.edit:SetText(ed.ql and tostring(ed.ql) or "")
qBoxes.tx.edit:SetText(ed.tx or "")
qBoxes.si.edit:SetText(ed.si and tostring(ed.si) or "")
uBoxes.qk.edit:SetText(ed.qk and table.concat(ed.qk, ", ") or "")
uBoxes.qd.edit:SetText(ed.qd and table.concat(ed.qd, ", ") or "")
uBoxes.qb.edit:SetText(ed.qb and table.concat(ed.qb, ", ") or "")
qBoxes.qr.edit:SetText(ed.qr and table.concat(ed.qr, ", ") or "")
zoneInput.edit:SetText("")
Suggest()
title:SetText(string.format("%s #%s", L[KIND_LABEL[kind]] or kind, tostring(id)))
local gn = GameName(kind, id)
game:SetText(gn and string.format(L.CODEX_NAME_GAME, gn) or "")
win.fwKey = kind .. ":" .. tostring(id)
win.Refresh()
win:Show()
win:Raise()
end
return win
end
-- Stufenspanne eines Gebiets von Hand setzen (nur Berechtigte): geht wie alle Eigenschaften ueber die Vorschau im Dev-Reiter
local function BuildZoneProps(parent, onChanged)
local L = FW.L
local win = MakeWindow(parent, 420, 210)
local state = {}
local title = UI.CreateLabel(win, "", { font = "heading", size = 14, color = "goldLight" })
title:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -14)
local lvLabel = UI.CreateLabel(win, L.CODEX_PROPS_LEVELS, { size = 12 })
lvLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -52)
local l1Box = MakeInput(win, 70)
l1Box:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -48)
l1Box.edit:SetMaxLetters(3)
local dash = UI.CreateLabel(win, "-", { size = 12 })
dash:SetPoint("LEFT", l1Box, "RIGHT", 6, 0)
local l2Box = MakeInput(win, 70)
l2Box:SetPoint("LEFT", l1Box, "RIGHT", 20, 0)
l2Box.edit:SetMaxLetters(3)
local hint = UI.CreateLabel(win, L.CODEX_ZONE_EDIT_HINT, { size = 11, color = "textDim", justifyV = "TOP" })
hint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -86)
hint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -86)
hint:SetHeight(60)
hint:SetWordWrap(true)
local function Save(clear)
if state.key then
local ed
if not clear then ed = { l1 = tonumber(l1Box.edit:GetText()), l2 = tonumber(l2Box.edit:GetText()) } end
Codex:SetEd("zones", Codex.ZoneEdID(state.key), ed)
if onChanged then onChanged() end
end
win:Hide()
end
local save = UI.CreateButton(win, L.CODEX_NAME_SAVE, 120, 28, function() Save(false) end, "primary")
save:SetPoint("BOTTOMLEFT", win, "BOTTOMLEFT", 16, 14)
local clear = UI.CreateButton(win, L.CODEX_NAME_CLEAR, 120, 28, function() Save(true) end, "default")
clear:SetPoint("LEFT", save, "RIGHT", 8, 0)
local cancel = UI.CreateButton(win, L.CODEX_NAME_CANCEL, 120, 28, function() win:Hide() end, "default")
cancel:SetPoint("LEFT", clear, "RIGHT", 8, 0)
function win.Open(key)
state.key = key
local ed = Codex:GetEd("zones", Codex.ZoneEdID(key)) or {}
l1Box.edit:SetText(ed.l1 and tostring(ed.l1) or "")
l2Box.edit:SetText(ed.l2 and tostring(ed.l2) or "")
title:SetText(string.format("%s: %s", L.CODEX_KIND_ZONES, Codex.MapName(key)))
win.fwKey = "zones:" .. tostring(key)
win:Show()
win:Raise()
end
return win
end
------------------------------------------------------------------------
-- Fenster "Namen einfuegen"
------------------------------------------------------------------------
local function BuildImport(parent, onChanged)
local L = FW.L
local win = MakeWindow(parent, 560, 440)
local state = { lang = Codex.Lang() }
local title = UI.CreateLabel(win, L.CODEX_NAME_IMPORT, { font = "heading", size = 14, color = "goldLight" })
title:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -14)
local help = UI.CreateLabel(win, "", { size = 11, color = "textDim", justifyV = "TOP" })
help:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -40)
help:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -40)
help:SetHeight(32)
help:SetWordWrap(true)
local deButton, enButton
local function Sync()
deButton:SetStyle(state.lang == "de" and "primary" or "default")
enButton:SetStyle(state.lang == "en" and "primary" or "default")
end
deButton = UI.CreateButton(win, L.CODEX_NAME_DE, 140, 26, function() state.lang = "de"; Sync() end, "default")
deButton:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -78)
enButton = UI.CreateButton(win, L.CODEX_NAME_EN, 140, 26, function() state.lang = "en"; Sync() end, "default")
enButton:SetPoint("LEFT", deButton, "RIGHT", 8, 0)
local area = UI.CreateTextArea(win, 528, 220)
area:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -112)
local result = UI.CreateLabel(win, "", { size = 12, color = "goldLight" })
result:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -344)
result:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -344)
local go = UI.CreateButton(win, L.CODEX_NAME_IMPORT_GO, 140, 28, function()
local done, skipped = Codex:ImportNames(state.kind, state.lang, area:GetText())
result:SetText(string.format(L.CODEX_NAME_IMPORT_RESULT, done, skipped))
if onChanged then onChanged() end
end, "primary")
go:SetPoint("BOTTOMLEFT", win, "BOTTOMLEFT", 16, 14)
local close = UI.CreateButton(win, L.CODEX_NAME_CLOSE, 140, 28, function() win:Hide() end, "default")
close:SetPoint("LEFT", go, "RIGHT", 8, 0)
function win.Open(kind)
state.kind = kind
state.lang = Codex.Lang()
Sync()
help:SetText(string.format(L.CODEX_NAME_IMPORT_HELP, L[KIND_LABEL[kind]] or kind))
result:SetText("")
area:SetText("")
win:Show()
win:Raise()
end
return win
end
------------------------------------------------------------------------
-- Knoepfe im Tab
-- getSel() gibt art, auswahl zurueck; onChanged() aktualisiert Liste und Detail
------------------------------------------------------------------------
function NameUI.Attach(f, getSel, onChanged, origin)
local L = FW.L
local ctl = { height = 0 }
origin = origin or { x = 0, y = 0 }
local editWin, importWin, propsWin, zoneWin
function NameUI.OpenZone(key)
if not (Codex.Trust and Codex.Trust.CanEdit()) then return end
zoneWin = zoneWin or BuildZoneProps(f, onChanged)
zoneWin.Open(key)
end
local editButton = UI.CreateButton(f, L.CODEX_NAME_EDIT, 150, 26, function()
local kind, id = getSel()
if EDITABLE[kind] and id then
editWin = editWin or BuildEdit(f, onChanged)
editWin.Open(kind, id)
end
end, "default")
editButton:Hide()
local importButton = UI.CreateButton(f, L.CODEX_NAME_IMPORT, 150, 26, function()
local kind = getSel()
if EDITABLE[kind] then
importWin = importWin or BuildImport(f, onChanged)
importWin.Open(kind)
end
end, "default")
importButton:Hide()
local propsButton = UI.CreateButton(f, L.CODEX_PROPS_BTN, 150, 26, function()
local kind, id = getSel()
if EDITABLE[kind] and id then
propsWin = propsWin or BuildProps(f, onChanged)
propsWin.Open(kind, id)
end
end, "default")
propsButton:Hide()
-- "Fehlerhaft markieren" (nur Berechtigte): blendet den Datensatz bei allen Spielern aus
local function BadNow(kind, id)
local hook = Codex.DevHook
local staged = hook and hook.StagedBad and hook.StagedBad(kind, id)
if staged ~= nil then return staged end
return Codex:IsBad(kind, id)
end
local badButton = UI.CreateButton(f, L.CODEX_BAD_MARK, 170, 26, function()
local kind, id = getSel()
if kind and id and kind ~= "items" then
Codex:SetBad(kind, id, not BadNow(kind, id))
if onChanged then onChanged() end
ctl.Update()
end
end, "default")
badButton:Hide()
-- Die Knoepfe stehen in einer eigenen Reihe unter "Zurueck/Favorit", von links nach rechts;
-- ist der Platz zu knapp, laufen sie in die naechste Reihe. ctl.height ist die belegte Hoehe.
local strip = { propsButton, badButton, importButton, editButton }
function ctl.Layout()
local avail = (f:GetWidth() or 0) - origin.x
if avail < 240 then avail = 640 end
local x, y, any = 0, 0, false
for _, b in ipairs(strip) do
if b:IsShown() then
local w = b:GetWidth() or 150
if x > 0 and x + w > avail then
x = 0
y = y + 32
end
b:ClearAllPoints()
b:SetPoint("TOPLEFT", f, "TOPLEFT", origin.x + x, -(origin.y + y))
x = x + w + 8
any = true
end
end
local h = any and (y + 32) or 0
if h ~= ctl.height then
ctl.height = h
if origin.relayout then origin.relayout() end
end
end
ctl.strip = strip
function ctl.Update()
local kind, id = getSel()
if EDITABLE[kind] and kind ~= "items" and id and Codex.Trust then
local bad = BadNow(kind, id)
if Codex.Trust.Can(bad and "unbad" or "bad") then
badButton:SetLabel(bad and L.CODEX_BAD_UNMARK or L.CODEX_BAD_MARK)
badButton:SetStyle(bad and "primary" or "default")
badButton:Show()
else
badButton:Hide()
end
else
badButton:Hide()
end
-- Namen bearbeiten: nur berechtigte Charaktere mit dem Modul Freewood-OctoWoW-Dev
local can = Codex.Trust ~= nil and Codex.Trust.CanEdit()
if can and EDITABLE[kind] and id then editButton:Show() else editButton:Hide() end
if can and EDITABLE[kind] then importButton:Show() else importButton:Hide() end
if can and EDITABLE[kind] and id then propsButton:Show() else propsButton:Hide() end
if propsWin and propsWin:IsShown() and propsWin.fwKey ~= (tostring(kind) .. ":" .. tostring(id)) then propsWin:Hide() end
if editWin and editWin:IsShown() and editWin.fwKey ~= (tostring(kind) .. ":" .. tostring(id)) then editWin:Hide() end
if importWin and importWin:IsShown() and not EDITABLE[kind] then importWin:Hide() end
ctl.Layout()
end
ctl.editButton, ctl.importButton, ctl.propsButton = editButton, importButton, propsButton
return ctl
end
+494
View File
@@ -0,0 +1,494 @@
-- Freewood-OctoWoW-Codex: Namen von Kreaturen lernen
--
-- Forever gibt Kreaturennamen im Kampf als "geheime Werte" zurueck. In Instanzen ist fast alles Kampf,
-- darum kannte der Codex dort nur die IDs. Hier werden Namen auf mehreren Wegen gesammelt:
-- target / mouse Ziel und Mouseover (auch Leichen und noch nicht bekannte Kreaturen)
-- scan ausserhalb des Kampfes alle paar Sekunden: Ziel, Fokus, Gruppenziele, Namensplaketten
-- plate Ereignis "Namensplakette erscheint" (falls das Spiel es erlaubt)
-- xp XP-Meldung "X stirbt ..." mit passendem Ziel
-- pend XP-Meldung ohne lesbares Ziel: Name wird dem naechsten frisch gelooteten Unbekannten zugeordnet,
-- aber nur, wenn eindeutig (genau ein Name in den letzten 90 Sekunden)
-- quest Kill-Ziele von Quests (eigene und fremde), die genau eine Kreatur-ID kennen
-- cache Name war schon gesehen, bevor die Kreatur einen Datensatz hatte
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Util = FW.Util
local Names = {
stats = { target = 0, mouse = 0, scan = 0, plate = 0, xp = 0, pend = 0, quest = 0, cache = 0, tip = 0 },
tipFail = { na = 0, err = 0, none = 0, secret = 0 },
xpSeen = 0,
kills = {}, evCount = {}, evSecret = 0, samples = {}, killEvents = {},
reads = 0, secret = 0, sweeps = 0, plateEvents = 0,
fail = { existSecret = 0, playerSecret = 0, guidSecret = 0, notNpc = 0, xpSecret = 0 },
idName = {}, idCount = 0, pending = {}, running = false, plateOk = nil,
}
NS.Names = Names
Codex.Names = Names
local MAX_CACHE = 800
local PENDING_SECS = 90
local SCAN_SECS = 2.5
local QUEST_EVERY = 24 -- Durchlaeufe zwischen zwei Quest-Auswertungen (ca. eine Minute)
local TOKENS = { "target", "focus", "mouseover", "targettarget", "focustarget", "softenemy" }
local function ParseGUID(guid)
local parse = Codex.ParseGUID
if parse then
return parse(guid)
end
end
local function CleanName(v)
if Util.IsSecret(v) or type(v) ~= "string" then
return nil
end
v = v:gsub("[%c|]", ""):gsub("^%s+", ""):gsub("%s+$", "")
if v == "" then
return nil
end
return v:sub(1, 60)
end
-- Kreatur kennt der Codex schon (eigene oder fremde Daten)?
local function Known(id)
if Codex.db.units[id] then
return true
end
local v = Codex.V and Codex.V()
return v and v.units and v.units[id] ~= nil
end
-- Name zu einer Kreatur-ID eintragen. Gibt true zurueck, wenn ein neuer Name gespeichert wurde.
function Names:Learn(id, name, src)
name = CleanName(name)
if name and Codex.StripKillVerb then name = Codex.StripKillVerb(name) end
if type(id) ~= "number" or not name then
return false
end
local u = Codex.db.units[id]
if u and u.n then
return false
end
if not u and self.idName[id] then
return false
end
if u or Known(id) then
u = u or Codex:Unit(id, true)
u.n = name
self.idName[id] = nil
if src and self.stats[src] then
self.stats[src] = self.stats[src] + 1
end
FW:Fire("CODEX_CHANGED", "unit", id)
return true
end
-- noch kein Datensatz: Name merken, bis die Kreatur gelootet wird
if not self.idName[id] and self.idCount < MAX_CACHE then
self.idName[id] = name
self.idCount = self.idCount + 1
end
return false
end
-- Eine Einheit lesen: nur Kreaturen, nur wenn der Name lesbar ist.
-- fail[...] zaehlt, woran es scheitert (nur zur Diagnose): exist, player, guid, npc, name
function Names:Read(unit, src)
local f = self.fail
local existsRaw = UnitExists and UnitExists(unit)
if Util.IsSecret(existsRaw) then
f.existSecret = f.existSecret + 1
return
end
if not existsRaw then
return
end
local playerRaw = UnitIsPlayer and UnitIsPlayer(unit)
if Util.IsSecret(playerRaw) then
f.playerSecret = f.playerSecret + 1
return
end
if playerRaw then
return
end
local guidRaw = UnitGUID and UnitGUID(unit)
if Util.IsSecret(guidRaw) then
f.guidSecret = f.guidSecret + 1
return
end
local kind, id = ParseGUID(guidRaw)
if kind ~= "npc" then
f.notNpc = f.notNpc + 1
return
end
self.reads = self.reads + 1
local raw = UnitName and UnitName(unit)
if Util.IsSecret(raw) then
self.secret = self.secret + 1
return
end
self:Learn(id, raw, src)
end
local function GroupTokens()
local list = {}
local n = GetNumGroupMembers and GetNumGroupMembers() or 0
if n > 1 then
local raid = IsInRaid and IsInRaid()
local prefix, count = raid and "raid" or "party", raid and math.min(n, 40) or math.min(n - 1, 4)
for i = 1, count do
list[#list + 1] = prefix .. i .. "target"
end
end
return list
end
-- Alle bekannten Quellen einmal lesen. Im Kampf sind die Namen geheim, dann lohnt es nicht.
function Names:Scan(src)
src = src or "scan"
if UnitAffectingCombat and Util.Clean(UnitAffectingCombat("player")) then
return
end
self.sweeps = self.sweeps + 1
for _, unit in ipairs(TOKENS) do
self:Read(unit, src)
end
for _, unit in ipairs(GroupTokens()) do
self:Read(unit, src)
end
for i = 1, 40 do
local unit = "nameplate" .. i
if UnitExists and Util.Clean(UnitExists(unit)) then
self:Read(unit, "plate")
end
end
end
-- Kill-Ziele von Quests mit genau einer bekannten Kreatur-ID
function Names:FromQuests()
local v = Codex.V and Codex.V()
if not v or not v.quests then
return 0
end
local learned = 0
for _, q in pairs(v.quests) do
for _, ob in ipairs(q.obj or {}) do
if ob.ty == "monster" and ob.name and type(ob.ids) == "table" then
local only, count
count = 0
for id in pairs(ob.ids) do
only = id
count = count + 1
if count > 1 then
break
end
end
if count == 1 and self:Learn(only, ob.name, "quest") then
learned = learned + 1
end
end
end
end
return learned
end
-- XP-Meldung: Name einer getoeteten Kreatur ohne lesbares Ziel merken
function Names:NoteKill(name)
name = CleanName(name)
if not name then
return
end
local list = self.pending
list[#list + 1] = { name = name, t = time() }
while #list > 20 do
table.remove(list, 1)
end
end
-- Versuch: Name ueber den Tooltip-Link der Beute-GUID ("unit:<GUID>"). Die GUID aus dem Beutefenster ist lesbar.
function Names:FromTooltip(guid)
local api = _G.C_TooltipInfo and _G.C_TooltipInfo.GetHyperlink
if type(api) ~= "function" or type(guid) ~= "string" then
self.tipFail.na = self.tipFail.na + 1
return nil
end
local ok, data = pcall(api, "unit:" .. guid)
if not ok then
self.tipFail.err = self.tipFail.err + 1
return nil
end
if Util.IsSecret(data) then
self.tipFail.secret = self.tipFail.secret + 1
return nil
end
local line = type(data) == "table" and type(data.lines) == "table" and data.lines[1]
local text = type(line) == "table" and line.leftText
if Util.IsSecret(text) then
self.tipFail.secret = self.tipFail.secret + 1
return nil
end
local name = CleanName(text)
if not name then
self.tipFail.none = self.tipFail.none + 1
end
return name
end
-- Eine gelootete Kreatur mit bekanntem Namen hat ihre XP-Meldung "verbraucht"
function Names:Consume(name)
local list = self.pending
for i = #list, 1, -1 do
if list[i].name == name then
table.remove(list, i)
return
end
end
end
-- Frisch gelootete Kreatur ohne Namen: aus Merkzettel oder XP-Meldungen auffuellen
function Names:Resolve(id, ent, guid)
if type(id) ~= "number" or type(ent) ~= "table" or ent.n then
return
end
local cached = self.idName[id]
if cached then
ent.n = cached
self.idName[id] = nil
self.stats.cache = self.stats.cache + 1
return
end
local tip = guid and self:FromTooltip(guid)
if tip then
ent.n = tip
self.stats.tip = self.stats.tip + 1
return
end
if (ent.k or 0) > 1 then
return
end
local now, list = time(), self.pending
for i = #list, 1, -1 do
if now - list[i].t > PENDING_SECS then
table.remove(list, i)
end
end
-- nur zuordnen, wenn in den letzten 90 Sekunden genau ein Name gemeldet wurde
local only
for _, p in ipairs(list) do
if only and only ~= p.name then
return
end
only = p.name
end
if not only then
return
end
for i = #list, 1, -1 do
if list[i].name == only then
table.remove(list, i)
break
end
end
ent.n = only
self.stats.pend = self.stats.pend + 1
end
-- Kampflog-Zeilen "Euer Charakter besiegt X." / "X stirbt." als Vorschlaege fuer Namen (nie automatisch zugeordnet)
local KILL_EVENTS = { "CHAT_MSG_COMBAT_HOSTILE_DEATH", "CHAT_MSG_COMBAT_SELF_HITS", "CHAT_MSG_COMBAT_PARTY_HITS",
"CHAT_MSG_COMBAT_MISC_INFO", "CHAT_MSG_COMBAT_PET_HITS" }
local KILL_PATTERNS = {
"^Euer Charakter besiegt (.+)%.$", "besiegt (.+)%.$", "^Ihr habt (.+) get\195\182tet%.$", "^(.+) stirbt%.$",
"^You have slain (.+)!$", "^You defeat (.+)%.$", "defeats (.+)%.$", "^(.+) dies%.$",
}
-- Zeile pruefen. Rueckgabe: Name oder nil
function Names.ParseKill(text)
if type(text) ~= "string" then
return nil
end
text = text:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|H.-|h(.-)|h", "%1")
for _, pattern in ipairs(KILL_PATTERNS) do
local name = text:match(pattern)
if name then
name = name:gsub("^%s+", ""):gsub("%s+$", "")
if name ~= "" and #name <= 80 then
return name
end
end
end
return nil
end
function Names:OnKillLine(event, text)
self.evCount[event] = (self.evCount[event] or 0) + 1
if Util.IsSecret(text) then
self.evSecret = self.evSecret + 1
return
end
if type(text) ~= "string" then
return
end
local sample = self.samples[event]
if not sample then
sample = {}
self.samples[event] = sample
end
if #sample < 2 then
sample[#sample + 1] = text:sub(1, 90)
end
local name = Names.ParseKill(text)
if not name then
return
end
local list, now = self.kills, time()
local last = list[#list]
if last and last.name == name and now - last.t < 2 then
return
end
list[#list + 1] = { name = name, t = now }
while #list > 40 do
table.remove(list, 1)
end
end
-- Zuletzt besiegte Gegner (neueste zuerst, ohne Doppelte). Pro Eintrag: name, alt (ohne letztes grossgeschriebenes Wort)
function Names:RecentKills(maxAge, limit)
maxAge, limit = maxAge or 900, limit or 8
local now, seen, out = time(), {}, {}
for i = #self.kills, 1, -1 do
local k = self.kills[i]
if now - k.t <= maxAge and not seen[k.name] then
seen[k.name] = true
local alt
local head, tail = k.name:match("^(.+)%s+(%S+)$")
if head and tail:match("^%u") and #head >= 3 then
alt = head
end
out[#out + 1] = { name = k.name, alt = alt, t = k.t }
if #out >= limit then
break
end
end
end
return out
end
local function Tick()
if not Names.running then
return
end
pcall(Names.Scan, Names, "scan")
Names.tickCount = (Names.tickCount or 0) + 1
if Names.tickCount % QUEST_EVERY == 0 then
pcall(Names.FromQuests, Names)
end
C_Timer.After(SCAN_SECS, Tick)
end
function Names:OnRegen()
C_Timer.After(0.5, function() pcall(Names.Scan, Names, "scan") end)
C_Timer.After(2.5, function() pcall(Names.Scan, Names, "scan") end)
end
function Names:OnPlate(unit)
self.plateEvents = self.plateEvents + 1
if type(unit) == "string" then
self:Read(unit, "plate")
end
end
-- Alte Eintraege mit anhaengendem Kill-Verb ("... beseitigt", "getötet") bereinigen (einmal beim Start)
function Names.Cleanup()
local strip = Codex.StripKillVerb
if not strip or not Codex.db then return end
local n = 0
for _, u in pairs(Codex.db.units or {}) do
if type(u) == "table" and type(u.n) == "string" then
local c = strip(u.n)
if c ~= u.n then u.n = c n = n + 1 end
end
end
for _, q in pairs(Codex.db.quests or {}) do
for _, ob in ipairs(type(q) == "table" and q.obj or {}) do
if type(ob.name) == "string" then
local c = strip(ob.name)
if c ~= ob.name then ob.name = c n = n + 1 end
end
end
end
Names.cleaned = n
end
function Names:Start()
if self.running then
return
end
pcall(Names.Cleanup)
self.running = true
FW:RegisterEvent("PLAYER_REGEN_ENABLED", self.OnRegen, self)
-- das Ereignis kann in gesperrten Situationen verboten sein: Fehler abfangen
self.plateOk = pcall(FW.RegisterEvent, FW, "NAME_PLATE_UNIT_ADDED", self.OnPlate, self)
for _, loopEvent in ipairs(KILL_EVENTS) do
local event = loopEvent -- eigene Variable je Durchlauf: Lua 5.0 teilt die Schleifenvariable zwischen Closures
self.killEvents[event] = pcall(FW.RegisterEvent, FW, event, function(owner, text)
Names:OnKillLine(event, text)
end, Names)
end
C_Timer.After(12, function()
pcall(Names.FromQuests, Names)
end)
C_Timer.After(SCAN_SECS, Tick)
end
function Names:Stop()
self.running = false
FW:UnregisterEvent("PLAYER_REGEN_ENABLED", self)
if self.plateOk then
FW:UnregisterEvent("NAME_PLATE_UNIT_ADDED", self)
end
for event, ok in pairs(self.killEvents) do
if ok then
FW:UnregisterEvent(event, self)
end
end
end
function Names:Debug()
local total, named = 0, 0
local v = Codex.V and Codex.V() or Codex.db
for _, u in pairs(v.units or {}) do
total = total + 1
if u.n then
named = named + 1
end
end
local st = self.stats
FW:Print(string.format("Codex-Namen: %d Kreaturen, %d mit Namen, %d ohne", total, named, total - named))
FW:Print(string.format("Neu gelernt ueber: Ziel %d, Mouseover %d, Suche %d, Namensplakette %d, XP-Meldung %d, XP-Zuordnung %d, Quest %d, Merkzettel %d, Tooltip %d",
st.target, st.mouse, st.scan, st.plate, st.xp, st.pend, st.quest, st.cache, st.tip))
local tf = self.tipFail
FW:Print(string.format("XP-Meldungen gesehen %d (davon geheim %d); Tooltip-Weg: nicht vorhanden %d, Fehler %d, geheim %d, ohne Namen %d",
self.xpSeen, self.fail.xpSecret, tf.na, tf.err, tf.secret, tf.none))
local evs = {}
for event, n in pairs(self.evCount) do
evs[#evs + 1] = event:gsub("^CHAT_MSG_COMBAT_", "") .. "=" .. n
end
table.sort(evs)
FW:Print(string.format("Kampflog-Zeilen: %s (geheim %d); gemerkte Kills %d", #evs > 0 and table.concat(evs, " ") or "keine", self.evSecret, #self.kills))
for event, list in pairs(self.samples) do
for _, text in ipairs(list) do
FW:Print(" Beispiel " .. event:gsub("^CHAT_MSG_COMBAT_", "") .. ": " .. text:gsub("|", "||"))
end
end
local f = self.fail
FW:Print(string.format("Abbrueche vor dem Namen: Einheit geheim %d, Spieler-Angabe geheim %d, GUID geheim %d, keine Kreatur %d; Instanz: %s",
f.existSecret, f.playerSecret, f.guidSecret, f.notNpc, tostring(IsInInstance and select(1, IsInInstance()))))
FW:Print(string.format("Gelesen %d, davon im Kampf geheim %d; Suchdurchlaeufe %d; Namensplaketten-Ereignis %s (%d); Merkzettel %d, offene XP-Namen %d",
self.reads, self.secret, self.sweeps, self.plateOk and "ok" or "nicht verfuegbar", self.plateEvents, self.idCount, #self.pending))
end
+23
View File
@@ -0,0 +1,23 @@
-- Freewood-OctoWoW-Codex: Eigentuemer der Datenbank
--
-- Hier stehen die Charaktere (Name-Realm), die DEV-Rechte vergeben duerfen. Nur Listen, die von einem dieser
-- Charaktere kommen, werden von anderen Spielern uebernommen. Der Absender einer Addon-Nachricht wird vom
-- Spiel gesetzt und kann nicht gefaelscht werden.
--
-- Eintrag zum Beispiel: "Lacustat-Everlook",
-- (genau so geschrieben, wie der Charakter im Spiel heisst. Charaktere haben Vor- und Nachnamen: "Aroki Steel".
-- Ein Bindestrich gilt nur als Realm-Trenner, wenn dahinter ClassicBetaPvP2 steht.)
--
-- Ist die Liste leer, gibt es niemanden mit DEV-Rechten: Namen koennen dann nicht bearbeitet werden.
local ADDON, NS = ...
if not NS then
return
end
NS.Owners = {
"Yuto-ClassicBetaPvP2",
"Aroki Steel",
"Lujinja Steel",
"Lujinja-ClassicBetaPvE",
}
+1200
View File
File diff suppressed because it is too large Load Diff
+556
View File
@@ -0,0 +1,556 @@
-- Freewood-OctoWoW-Codex: Wegenetz auf der Weltkarte anzeigen (Dev-Reiter, /fwwege).
-- Ebenen: Wege (gruen), Faelle (rot), Gebietsuebergaenge (je Art eine Farbe), Luecken (gelb: bekannte Quest-Geber/-Abgaben und
-- Dienst-NPCs, bei denen noch kein Weg aufgezeichnet ist). Die Welt-Koordinaten des Wegenetzes werden mit drei Messpunkten
-- aus der Kartenschnittstelle in Kartenanteile umgerechnet (die Abbildung ist je Karte linear).
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex or not Codex.Path then
return
end
local Path = Codex.Path
local PM = { on = false }
Codex.PathMap = PM
local GRID = Codex.GRID or 200
local CELL = Path.CELL or 10
local BIN = 1 / 300 -- Karten-Anteil je Punkt (Raster der Anzeige)
local MAX_DOTS = 5000
local GAP_RADIUS = 0.012 -- so nah muss ein Weg an einem bekannten Ort liegen, damit er nicht als Luecke zaehlt
local MAX_GAPS = 250
local LAYER_LEVEL = 1400
local PROBE = 100 -- Yards Abstand der Messpunkte fuer die Umrechnung
local COLORS = {
w = { 0.30, 0.90, 0.40 }, -- Weg
f = { 0.75, 0.45, 1.00 }, -- Flugroute
r = { 1.00, 0.60, 0.15 }, -- fremder Weg (empfangen oder aus Startdaten)
d = { 1.00, 0.25, 0.20 }, -- Fall
g = { 1.00, 0.85, 0.10 }, -- Luecke
lw = { 0.20, 0.80, 1.00 }, -- Grenzuebergang
lt = { 0.90, 0.30, 0.90 }, -- Sprung (Zeppelin, Portal)
lf = { 0.60, 0.60, 1.00 }, -- Flugpunkt
li = { 1.00, 0.60, 0.10 }, -- Instanz
s = { 1.00, 1.00, 1.00 }, -- Ortspunkt (nur fuer Berechtigte)
}
local function View()
local db = Codex.db
db.settings.pathView = db.settings.pathView or {}
return db.settings.pathView
end
function PM.LayerOn(k)
local v = View()[k]
return v ~= false
end
function PM.SetLayer(k, on)
View()[k] = on and true or false
PM.Refresh()
end
------------------------------------------------------------------------
-- Umrechnung Welt -> Karte
------------------------------------------------------------------------
local function Vec(a, b)
return _G.CreateVector2D and _G.CreateVector2D(a, b) or { x = a, y = b, GetXY = function(self) return self.x, self.y end }
end
-- Kartenanteil zu einer Weltposition; first/second wie UnitPosition (erster, zweiter Rueckgabewert)
local function MapPosOf(world, first, second, mapID, swap)
local CM = _G.C_Map
if not (CM and CM.GetMapPosFromWorldPos) then return nil end
local vec = swap and Vec(second, first) or Vec(first, second)
local ok, id, pos = pcall(CM.GetMapPosFromWorldPos, world, vec, mapID)
if not ok or not pos or not pos.GetXY then return nil end
local ok2, x, y = pcall(pos.GetXY, pos)
if not ok2 or type(x) ~= "number" or type(y) ~= "number" then return nil end
return x, y
end
local function WorldOfMap(mapID)
local CM = _G.C_Map
if not (CM and CM.GetWorldPosFromMapPos) then return nil end
local ok, cont = pcall(CM.GetWorldPosFromMapPos, mapID, Vec(0.5, 0.5))
if ok and type(cont) == "number" then return cont end
return nil
end
local function CellCentre(cx, cy)
-- Zellen wurden aus x = zweiter, y = erster Rueckgabewert von UnitPosition gebildet
return (cy + 0.5) * CELL, (cx + 0.5) * CELL -- first, second
end
local function InMap(x, y)
return x and y and x >= 0 and x <= 1 and y >= 0 and y <= 1
end
-- Erste Zellen einer Welt (fuer die Messpunkte)
local function SampleCells(cells, n)
local out = {}
for key in pairs(cells) do
out[#out + 1] = key
if #out >= n then break end
end
return out
end
-- Orientierung bestimmen: erst am Spieler (falls er auf der Karte steht), sonst nach der Zahl der Zellen, die auf die Karte fallen
local function DetectSwap(world, cells, mapID)
local db = Codex.db
local saved = db.settings.pathSwap
if type(saved) == "boolean" then return saved end
local CM = _G.C_Map
local pos = Path.Read and Path.Read()
if pos and pos.world == world and CM and CM.GetPlayerMapPosition then
local ok, mp = pcall(CM.GetPlayerMapPosition, mapID, "player")
if ok and mp and mp.GetXY then
local px, py = mp:GetXY()
local best, bestErr
for _, swap in ipairs({ false, true }) do
local x, y = MapPosOf(world, pos.y, pos.x, mapID, swap)
if x then
local err = math.abs(x - px) + math.abs(y - py)
if not bestErr or err < bestErr then best, bestErr = swap, err end
end
end
if best ~= nil and bestErr < 0.05 then
db.settings.pathSwap = best
return best
end
end
end
local score = { [false] = 0, [true] = 0 }
for _, key in ipairs(SampleCells(cells, 40)) do
local cx, cy = Path.KeyToCell(key)
local f, s = CellCentre(cx, cy)
for _, swap in ipairs({ false, true }) do
local x, y = MapPosOf(world, f, s, mapID, swap)
if InMap(x, y) then score[swap] = score[swap] + 1 end
end
end
return score[true] > score[false]
end
-- Lineare Abbildung (first, second) -> (x, y) der Karte, aus drei Messpunkten
local function Fit(world, cells, mapID, swap)
for _, key in ipairs(SampleCells(cells, 60)) do
local cx, cy = Path.KeyToCell(key)
local f0, s0 = CellCentre(cx, cy)
local x0, y0 = MapPosOf(world, f0, s0, mapID, swap)
if InMap(x0, y0) then
local x1, y1 = MapPosOf(world, f0 + PROBE, s0, mapID, swap)
local x2, y2 = MapPosOf(world, f0, s0 + PROBE, mapID, swap)
if x1 and x2 then
local a, b, c, d = (x1 - x0) / PROBE, (x2 - x0) / PROBE, (y1 - y0) / PROBE, (y2 - y0) / PROBE
if a ~= 0 or b ~= 0 or c ~= 0 or d ~= 0 then
return function(f, s)
return x0 + (f - f0) * a + (s - s0) * b, y0 + (f - f0) * c + (s - s0) * d
end
end
end
end
end
return nil
end
-- Ersatz, wenn keine der Stichprobenzellen auf der Karte liegt (kleine Karten wie Staedte): Mittelpunkt der Karte
-- in Weltkoordinaten erfragen und die Abbildung dort messen
local function FitFromMap(world, mapID, swap)
local CM = _G.C_Map
if not (CM and CM.GetWorldPosFromMapPos) then return nil end
local ok, cont, wp = pcall(CM.GetWorldPosFromMapPos, mapID, Vec(0.5, 0.5))
if not ok or cont ~= world or not wp or not wp.GetXY then return nil end
local ok2, vx, vy = pcall(wp.GetXY, wp)
if not ok2 or type(vx) ~= "number" or type(vy) ~= "number" then return nil end
local f0, s0
if swap then f0, s0 = vy, vx else f0, s0 = vx, vy end
local x0, y0 = MapPosOf(world, f0, s0, mapID, swap)
local x1, y1 = MapPosOf(world, f0 + PROBE, s0, mapID, swap)
local x2, y2 = MapPosOf(world, f0, s0 + PROBE, mapID, swap)
if not (x0 and x1 and x2) then return nil end
local a, b, c, d = (x1 - x0) / PROBE, (x2 - x0) / PROBE, (y1 - y0) / PROBE, (y2 - y0) / PROBE
if a == 0 and b == 0 and c == 0 and d == 0 then return nil end
return function(f, s)
return x0 + (f - f0) * a + (s - s0) * b, y0 + (f - f0) * c + (s - s0) * d
end
end
------------------------------------------------------------------------
-- Daten einer Karte
------------------------------------------------------------------------
-- Rueckgabe: { dots = { { x, y, kind, w } }, counts = { w, d, x, g }, swap }, oder nil, wenn nichts umgerechnet werden kann
function PM.Compute(mapID)
local db = Codex.db
if not (db and mapID) then return nil end
local res = { dots = {}, counts = { w = 0, d = 0, x = 0, g = 0 }, mapID = mapID }
local world = WorldOfMap(mapID)
local cells = world and Path.Cells(world)
local map
if cells and next(cells) then
res.swap = DetectSwap(world, cells, mapID)
map = Fit(world, cells, mapID, res.swap) or FitFromMap(world, mapID, res.swap)
end
local bins = {}
local function Bin(x, y, kind, w)
local bx, by = math.floor(x / BIN), math.floor(y / BIN)
local key = by * 100000 + bx
local b = bins[key]
if not b then b = { x = x, y = y, kind = kind, w = w }; bins[key] = b
elseif kind == "d" then b.kind = "d" end
return b
end
local pathHash = {}
local own = (db.path and db.path[world]) or {}
if map then
for key, mask in pairs(cells) do
local cx, cy = Path.KeyToCell(key)
local f, s = CellCentre(cx, cy)
local x, y = map(f, s)
if InMap(x, y) then
local isDrop = math.floor(mask / 256) % 256 > 0
local mine = own[key] ~= nil
local b = Bin(x, y, isDrop and "d" or (mine and "w" or "r"), 1)
if b.kind == "r" and mine then b.kind = "w" end -- eigener Weg geht vor
if isDrop then res.counts.d = res.counts.d + 1
elseif mine then res.counts.w = res.counts.w + 1
else res.counts.r = (res.counts.r or 0) + 1 end
local hx, hy = math.floor(x / GAP_RADIUS), math.floor(y / GAP_RADIUS)
pathHash[hy * 100000 + hx] = true
end
end
end
-- Flugrouten dieser Karte: Strecke als Punktreihe, Beschriftung in der Mitte
local flights, tags = {}, {}
local TX = Codex.Taxi
if map and TX and TX.Routes then
for _, r in pairs(TX.Routes()) do
local na, nb = TX.Node(r.a), TX.Node(r.b)
if na and nb and na.w == world and r.p and #r.p >= 4 then
local pts = {}
if na.a then pts[#pts + 1] = { na.a, na.b } end
for i = 1, #r.p - 1, 2 do pts[#pts + 1] = { r.p[i], r.p[i + 1] } end
if nb.a then pts[#pts + 1] = { nb.a, nb.b } end
local xs, ys = {}, {}
for i, q in ipairs(pts) do xs[i], ys[i] = map(q[1], q[2]) end
local any = false
for i = 1, #pts - 1 do
local x1, y1, x2, y2 = xs[i], ys[i], xs[i + 1], ys[i + 1]
if x1 and x2 and (InMap(x1, y1) or InMap(x2, y2)) then
any = true
local steps = math.max(1, math.min(40, math.ceil(math.max(math.abs(x2 - x1), math.abs(y2 - y1)) / 0.004)))
for k = 0, steps - 1 do
local fx, fy = x1 + (x2 - x1) * k / steps, y1 + (y2 - y1) * k / steps
if InMap(fx, fy) then flights[#flights + 1] = { x = fx, y = fy, kind = "f", w = 4 } end
end
end
end
if any then
local mid = math.max(1, math.floor(#pts / 2))
if xs[mid] and InMap(xs[mid], ys[mid]) then
tags[#tags + 1] = { x = xs[mid], y = ys[mid],
text = string.format("%s > %s %s %s", na.n or "?", nb.n or "?", TX.Duration(r.t), TX.Money(r.c)) }
end
end
end
end
end
res.counts.f = #tags
res.tags = tags
-- Luecken: bekannte Orte ohne Weg in der Naehe
local function NearPath(x, y)
local hx, hy = math.floor(x / GAP_RADIUS), math.floor(y / GAP_RADIUS)
for dy = -1, 1 do
for dx = -1, 1 do
if pathHash[(hy + dy) * 100000 + hx + dx] then return true end
end
end
return false
end
local gaps, seen = {}, {}
local function Gap(x, y)
if not x or #gaps >= MAX_GAPS or not InMap(x, y) or NearPath(x, y) then return end
local k = math.floor(x / GAP_RADIUS) * 100000 + math.floor(y / GAP_RADIUS)
if seen[k] then return end
seen[k] = true
gaps[#gaps + 1] = { x = x, y = y, kind = "g", w = 2 }
end
local V = Codex.V and Codex.V()
for _, q in pairs(V and V.quests or {}) do
for _, p in ipairs({ q.give, q.turn }) do
if type(p) == "table" and p.m == mapID then Gap(p.x, p.y) end
end
end
for _, u in pairs(V and V.units or {}) do
if u.ro and next(u.ro) and u.cells then
for key in pairs(u.cells) do
local m = math.floor(key / (GRID * GRID))
if m == mapID then
local idx = key % (GRID * GRID)
Gap(((idx % GRID) + 0.5) / GRID, (math.floor(idx / GRID) + 0.5) / GRID)
break
end
end
end
end
res.counts.g = #gaps
-- Gebietsuebergaenge dieser Karte
local crossings = {}
local function AddLink(a, b, kinds)
for kind, e in pairs(kinds) do
for _, p in ipairs(e.p or {}) do
local key, w
if a == mapID then key, w = p.fk, p.fw elseif b == mapID then key, w = p.tk, p.tw end
if key and w == world and map then
local cx, cy = Path.KeyToCell(key)
local f, s = CellCentre(cx, cy)
local x, y = map(f, s)
if InMap(x, y) then crossings[#crossings + 1] = { x = x, y = y, kind = "l" .. kind, w = 3, to = a == mapID and b or a } end
end
end
end
end
for a, row in pairs(Path.Links()) do
for b, kinds in pairs(row) do
if a == mapID or b == mapID then AddLink(a, b, kinds) end
end
end
res.counts.x = #crossings
-- Ortspunkte dieser Karte (Instanz-Eingaenge, Aufstiege): nur fuer Berechtigte
local spotDots = {}
local SP = Codex.Spots
if SP and Codex.Trust and Codex.Trust.CanEdit() then
for _, r in ipairs(SP.All()) do
if r.p.m == mapID and InMap(r.p.x, r.p.y) then
spotDots[#spotDots + 1] = { x = r.p.x, y = r.p.y, kind = "s", w = 3, name = r.n, sk = r.k }
end
end
end
res.counts.s = #spotDots
-- Reihenfolge: Wege unten, dann Faelle, Luecken, Uebergaenge oben
local list = {}
for _, b in pairs(bins) do list[#list + 1] = b end
table.sort(list, function(p, q)
if p.kind ~= q.kind then
local rank = { r = 0, w = 1 }
local a, b2 = rank[p.kind] or 2, rank[q.kind] or 2
return a < b2
end
if p.y ~= q.y then return p.y < q.y end
return p.x < q.x
end)
for _, b in ipairs(list) do res.dots[#res.dots + 1] = b end
for _, g in ipairs(flights) do res.dots[#res.dots + 1] = g end
for _, g in ipairs(gaps) do res.dots[#res.dots + 1] = g end
for _, c in ipairs(crossings) do res.dots[#res.dots + 1] = c end
for _, c in ipairs(spotDots) do res.dots[#res.dots + 1] = c end
return res
end
-- Karten, die Daten haben (fuer die Auswahl): { { map, n } } nach Zahl der Uebergaenge
function PM.Maps()
local db = Codex.db
local count = {}
for a, row in pairs(Path.Links()) do
for b, kinds in pairs(row) do
local n = 0
for _, e in pairs(kinds) do n = n + e.n end
if a > 0 then count[a] = (count[a] or 0) + n end
if b > 0 then count[b] = (count[b] or 0) + n end
end
end
local cur = _G.C_Map and _G.C_Map.GetBestMapForUnit and select(2, pcall(_G.C_Map.GetBestMapForUnit, "player"))
if type(cur) == "number" then count[cur] = count[cur] or 0 end
local out = {}
for m, n in pairs(count) do out[#out + 1] = { map = m, n = n } end
table.sort(out, function(x, y)
if x.n ~= y.n then return x.n > y.n end
return x.map < y.map
end)
return out
end
------------------------------------------------------------------------
-- Anzeige auf der Weltkarte
------------------------------------------------------------------------
local layer, pool, label
local API = Codex.MapAPI
local function Canvas()
return API.Canvas()
end
local function CurrentMapID()
return API.CurrentMapID(function()
local list = {}
for _, e in ipairs(PM.Maps()) do list[#list + 1] = e.map end
return list
end)
end
local function Scale(frame)
local s = 1
if frame and frame.GetEffectiveScale and _G.UIParent then
local a, b = frame:GetEffectiveScale(), _G.UIParent:GetEffectiveScale()
if type(a) == "number" and type(b) == "number" and a > 0 and b > 0 then s = a / b end
end
return s
end
local function EnsureLayer()
if layer then return layer end
local wm = _G.WorldMapFrame
if not (wm and Canvas()) then return nil end
if API.Classic() then
local canvas = Canvas()
layer = CreateFrame("Frame", nil, canvas)
layer:SetAllPoints(canvas)
layer:SetFrameLevel((canvas:GetFrameLevel() or 1) + 15)
else
layer = CreateFrame("Frame", nil, wm)
layer:SetAllPoints(wm.ScrollContainer or wm)
layer:SetFrameLevel((wm:GetFrameLevel() or 1) + LAYER_LEVEL)
end
if wm.GetFrameStrata then layer:SetFrameStrata(wm:GetFrameStrata()) end
if layer.SetClipsChildren then layer:SetClipsChildren(true) end
pool = {}
label = layer:CreateFontString(nil, "OVERLAY", "GameFontNormal")
label:SetPoint("TOPLEFT", layer, "TOPLEFT", 12, API.Classic() and -42 or -12)
label:SetJustifyH("LEFT")
local acc, lastScale, lastMap = 0, nil, nil
layer:SetScript("OnUpdate", function(_, elapsed)
acc = acc + (elapsed or 0)
if acc < 0.3 then return end
acc = 0
if not PM.on then return end
local sc, m = Scale(Canvas()), API.Signature()
if sc ~= lastScale or m ~= lastMap then
lastScale, lastMap = sc, m
PM.Refresh()
end
end)
return layer
end
local function Dot(i)
local t = pool[i]
if not t then
t = layer:CreateTexture(nil, "OVERLAY")
pool[i] = t
end
return t
end
function PM.Refresh()
if not layer then
if not PM.on then return end
EnsureLayer()
end
if not layer then return end
for _, t in ipairs(pool) do t:Hide() end
for _, t in ipairs(PM.tagPool or {}) do t:Hide() end
if not PM.on then
label:SetText("")
layer:Hide()
return
end
layer:Show()
local mapID = CurrentMapID()
local canvas = Canvas()
if not (mapID and canvas) then return end
local width, height = canvas:GetWidth() or 1, canvas:GetHeight() or 1
if width < 2 or height < 2 then return end
local res = PM.Compute(mapID)
PM.last = res
local L = FW.L
if not res or (res.counts.w + (res.counts.f or 0) + (res.counts.r or 0) + res.counts.d + res.counts.x + res.counts.g + (res.counts.s or 0)) == 0 then
label:SetText(L.CODEX_PATHMAP_NONE)
return
end
label:SetText(string.format(L.CODEX_PATHMAP_LABEL, res.counts.w, res.counts.d, res.counts.x, res.counts.g))
local scale, layerScale = Scale(canvas), Scale(layer)
local ratio = scale / layerScale
local n = 0
for _, d in ipairs(res.dots) do
local show
if d.kind == "w" then show = PM.LayerOn("w")
elseif d.kind == "r" then show = PM.LayerOn("r")
elseif d.kind == "f" then show = PM.LayerOn("f")
elseif d.kind == "d" then show = PM.LayerOn("d")
elseif d.kind == "g" then show = PM.LayerOn("g")
elseif d.kind == "s" then show = PM.LayerOn("s")
else show = PM.LayerOn("x") end
if show then
n = n + 1
if n > MAX_DOTS then break end
local t = Dot(n)
local px = (d.w == 1 and 3 or (d.w == 4 and 4 or d.w == 2 and 8 or (d.kind == "s" and 14 or 10))) / layerScale
local c = COLORS[d.kind] or COLORS.w
t:ClearAllPoints()
t:SetSize(px, px)
t:SetPoint("CENTER", canvas, "TOPLEFT", d.x * width * ratio, -d.y * height * ratio)
t:SetColorTexture(c[1], c[2], c[3], (d.kind == "w" or d.kind == "r") and 0.85 or 1)
t:Show()
end
end
-- Beschriftung der Flugrouten
if PM.LayerOn("f") and res.tags then
PM.tagPool = PM.tagPool or {}
for i, tg in ipairs(res.tags) do
if i > 20 then break end
local fs = PM.tagPool[i]
if not fs then
fs = layer:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
PM.tagPool[i] = fs
end
fs:ClearAllPoints()
fs:SetPoint("BOTTOM", canvas, "TOPLEFT", tg.x * width * ratio, -tg.y * height * ratio + 4)
fs:SetText(tg.text)
fs:SetTextColor(COLORS.f[1], COLORS.f[2], COLORS.f[3])
fs:Show()
end
end
end
-- Weltkarte oeffnen, auf eine Karte stellen (optional) und die Ebenen einblenden
function PM.Show(mapID)
PM.on = true
local wm = _G.WorldMapFrame
if wm then
if not wm:IsShown() then
if type(_G.ToggleWorldMap) == "function" then pcall(_G.ToggleWorldMap) elseif wm.Show then pcall(wm.Show, wm) end
end
if mapID then API.SetMap(mapID) end
if not PM.hooked then
PM.hooked = true
if wm.OnMapChanged then pcall(_G.hooksecurefunc, wm, "OnMapChanged", function() PM.Refresh() end) end
if wm.HookScript then wm:HookScript("OnShow", function() PM.Refresh() end) end
if API.Classic() and _G.hooksecurefunc and _G.SetMapZoom then
pcall(_G.hooksecurefunc, "SetMapZoom", function() PM.Refresh() end)
end
end
end
PM.Refresh()
FW:Fire("CODEX_PATHMAP_CHANGED")
end
function PM.Hide()
PM.on = false
PM.Refresh()
FW:Fire("CODEX_PATHMAP_CHANGED")
end
function PM.Toggle(mapID)
if PM.on then PM.Hide() else PM.Show(mapID) end
end
SLASH_FREEWOODPATHMAP1 = "/fwwege"
SlashCmdList["FREEWOODPATHMAP"] = function() PM.Toggle() end
+215
View File
@@ -0,0 +1,215 @@
-- Freewood-OctoWoW-Codex: Probe /fwquest
-- Prueft auf Octo, was der Server zu einer Quest-ID liefert (C_QuestLog.GetQuestDetails) und ob sich die darin
-- genannten Kreaturen, Objekte und Items ueber ihre ID benennen lassen. Nur Diagnose: schreibt in den Chat und nach
-- Codex.db.questProbe (SavedVariables, erst bei /reload oder Abmelden), veraendert keine Daten der Datenbank.
--
-- /fwquest prueft ein paar bekannte Quests (7, 6, 3904, 1467)
-- /fwquest <ID> [...] prueft diese Quest-IDs
-- /fwquest go <ID> prueft ein Spielobjekt (C_GameObjectInfo)
-- /fwquest npc <ID> prueft eine Kreatur (C_CreatureInfo)
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Probe = {}
Codex.QuestProbe = Probe
local WAIT = 3
local function Store()
local db = Codex.db
if not db then return nil end
db.questProbe = db.questProbe or {}
return db.questProbe
end
local function Short(v)
local s = tostring(v)
s = s:gsub("|", "/"):gsub("\n", " ")
if #s > 120 then s = s:sub(1, 120) .. "..." end
return s
end
-- Tabelle als Zeilen "pfad = wert"
local function Flatten(t, path, out, depth)
if depth > 3 then out[#out + 1] = path .. " = {...}" return end
local keys = {}
for k in pairs(t) do keys[#keys + 1] = k end
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
for _, k in ipairs(keys) do
local v = t[k]
local p = path == "" and tostring(k) or (path .. "." .. tostring(k))
if type(v) == "table" then
Flatten(v, p, out, depth + 1)
else
out[#out + 1] = p .. " = " .. Short(v)
end
end
end
local function Say(text) FW:Print("Quest-Probe: " .. text) end
local function Log(rec, line)
rec[#rec + 1] = line
Say(line)
end
local function Call(fn, a)
if type(fn) ~= "function" then return false, "fehlt" end
return pcall(fn, a)
end
local function ItemName(id)
local CI = _G.C_Item
local get = (CI and CI.GetItemInfo) or _G.GetItemInfo
if not get then return nil end
local ok, name = pcall(get, id)
if ok and type(name) == "string" and name ~= "" then return name end
return nil
end
local function CreatureName(id)
local CC = _G.C_CreatureInfo
if not (CC and CC.GetCreatureInfoByID) then return nil end
local ok, info = pcall(CC.GetCreatureInfoByID, id)
if ok and type(info) == "table" then
return tostring(info.name) .. " (Typ " .. tostring(info.type) .. ", Rang " .. tostring(info.rank) .. (info.subName and info.subName ~= "" and (", " .. info.subName) or "") .. ")"
end
return nil
end
local function ObjectName(id)
local CG = _G.C_GameObjectInfo
if not (CG and CG.GetGameObjectInfoByID) then return nil end
local ok, info = pcall(CG.GetGameObjectInfoByID, id)
if ok and type(info) == "table" then
return tostring(info.name) .. " (Typ " .. tostring(info.type) .. ", Anzeige " .. tostring(info.displayID) .. ")"
end
return nil
end
local function RequestNames(details)
local CC, CG, CI = _G.C_CreatureInfo, _G.C_GameObjectInfo, _G.C_Item
local function Req(fn, id) if fn then pcall(fn, id) end end
for _, r in ipairs(details.requirements or {}) do
if r.kind == "monster" then Req(CC and CC.RequestLoadCreatureByID, r.id)
elseif r.kind == "object" then Req(CG and CG.RequestLoadGameObjectByID, r.id)
elseif r.kind == "item" then Req(CI and CI.RequestLoadItemDataByID, r.id) end
end
for _, list in ipairs({ details.rewardItems or {}, details.choiceItems or {} }) do
for _, it in ipairs(list) do Req(CI and CI.RequestLoadItemDataByID, it.id) end
end
if (details.srcItemID or 0) > 0 then Req(CI and CI.RequestLoadItemDataByID, details.srcItemID) end
end
local function ReportDetails(id, rec, d)
local lines = {}
Flatten(d, "", lines, 0)
Log(rec, "Quest " .. id .. ": " .. #lines .. " Felder")
for _, l in ipairs(lines) do rec[#rec + 1] = " " .. l end
Say(string.format("%s | Stufe %s | Typ %s | Geld %s | Ziele %d | feste Items %d | Wahl-Items %d | Ort %s",
Short(d.title), tostring(d.level), tostring(d.questType or "-"), tostring(d.rewardMoney),
#(d.requirements or {}), #(d.rewardItems or {}), #(d.choiceItems or {}),
d.poi and (tostring(d.poi.mapID) .. ":" .. tostring(d.poi.x) .. "," .. tostring(d.poi.y)) or "-"))
end
local function ReportNames(id, rec, d)
for _, r in ipairs(d.requirements or {}) do
local name
if r.kind == "monster" then name = CreatureName(r.id)
elseif r.kind == "object" then name = ObjectName(r.id)
else name = ItemName(r.id) end
Log(rec, string.format(" Ziel %s %d x%s -> %s", r.kind, r.id, tostring(r.count), name or "kein Name (noch nicht geladen)"))
end
for _, def in ipairs({ { "feste Belohnung", d.rewardItems }, { "Wahl-Belohnung", d.choiceItems } }) do
for _, it in ipairs(def[2] or {}) do
Log(rec, string.format(" %s Item %d x%s -> %s", def[1], it.id, tostring(it.count), ItemName(it.id) or "kein Name"))
end
end
if (d.srcItemID or 0) > 0 then
Log(rec, string.format(" Start-Item %d -> %s", d.srcItemID, ItemName(d.srcItemID) or "kein Name"))
end
end
function Probe.Quest(id)
local QL = _G.C_QuestLog
if not (QL and QL.GetQuestDetails and QL.RequestLoadQuestByID) then
Say("C_QuestLog.GetQuestDetails gibt es in dieser ClassicAPI-Version nicht.")
return
end
local st = Store()
if not st then return end
local rec = {}
st[id] = rec
local okC, cached = Call(QL.IsQuestDataCachedByID, id)
rec[#rec + 1] = "vorher im Zwischenspeicher: " .. tostring(okC and cached)
pcall(QL.RequestLoadQuestByID, id)
Say("Quest " .. id .. ": Antwort in " .. WAIT .. " Sekunden ...")
C_Timer.After(WAIT, function()
local ok, d = pcall(QL.GetQuestDetails, id)
if not (ok and type(d) == "table") then
Log(rec, "Quest " .. id .. ": keine Antwort (unbekannt oder noch nicht geladen)" .. ((not ok) and (" - Fehler: " .. Short(d)) or ""))
return
end
ReportDetails(id, rec, d)
local okN, n = Call(QL.GetNumQuestObjectives, id)
rec[#rec + 1] = "GetNumQuestObjectives: " .. tostring(okN and n or "-")
RequestNames(d)
C_Timer.After(WAIT, function() ReportNames(id, rec, d) end)
end)
end
function Probe.Object(id)
local CG = _G.C_GameObjectInfo
if not (CG and CG.GetGameObjectInfoByID) then Say("C_GameObjectInfo gibt es nicht.") return end
local st = Store()
if not st then return end
local rec = {}
st["go" .. id] = rec
rec[#rec + 1] = "vorher: " .. tostring(ObjectName(id))
if CG.RequestLoadGameObjectByID then pcall(CG.RequestLoadGameObjectByID, id) end
Say("Objekt " .. id .. ": Antwort in " .. WAIT .. " Sekunden ...")
C_Timer.After(WAIT, function()
Log(rec, "Objekt " .. id .. ": " .. tostring(ObjectName(id) or "keine Antwort"))
end)
end
function Probe.Npc(id)
local CC = _G.C_CreatureInfo
if not (CC and CC.GetCreatureInfoByID) then Say("C_CreatureInfo gibt es nicht.") return end
local st = Store()
if not st then return end
local rec = {}
st["npc" .. id] = rec
rec[#rec + 1] = "vorher: " .. tostring(CreatureName(id))
if CC.RequestLoadCreatureByID then pcall(CC.RequestLoadCreatureByID, id) end
Say("Kreatur " .. id .. ": Antwort in " .. WAIT .. " Sekunden ...")
C_Timer.After(WAIT, function()
Log(rec, "Kreatur " .. id .. ": " .. tostring(CreatureName(id) or "keine Antwort"))
end)
end
function Probe.Command(msg)
msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
local go = msg:match("^go%s+(%d+)$")
if go then Probe.Object(tonumber(go)) return end
local npc = msg:match("^npc%s+(%d+)$")
if npc then Probe.Npc(tonumber(npc)) return end
local ids = {}
for n in msg:gmatch("%d+") do ids[#ids + 1] = tonumber(n) end
if #ids == 0 then ids = { 7, 6, 3904, 1467 } end
for i, id in ipairs(ids) do
if i > 8 then break end
local qid = id
C_Timer.After((i - 1) * (WAIT * 2 + 1), function() Probe.Quest(qid) end)
end
Say("Ergebnis steht auch in Codex.db.questProbe (SavedVariables nach /reload).")
end
_G.SLASH_FWQUESTPROBE1 = "/fwquest"
SlashCmdList["FWQUESTPROBE"] = function(msg) Probe.Command(msg) end
+333
View File
@@ -0,0 +1,333 @@
-- Freewood-OctoWoW-Codex: Rezepte der Berufe
-- Lernt beim Oeffnen eines Berufsfensters jedes Rezept: Ergebnis-Item, Material mit Mengen und die Schwierigkeit
-- (orange/gelb/gruen/grau) bei deinem damaligen Skill. Zusammen mit den Lehrer-Daten (Skill-Anforderung, Preis)
-- weiss der Plan so, was ein Rezept braucht und was es bringt, auch bevor du es gelernt hast.
-- db.recipes[Beruf][Rezeptname] = { it = Ergebnis-Item, rg = { [Materialitem] = Anzahl },
-- ob = { optimal|medium|easy|trivial = { kleinster, groesster Skill } }, t = Zeit }
-- Nur lokal; das Teilen mit anderen Spielern kommt spaeter.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local R = {}
NS.Recipes = R
Codex.Recipes = R
local DIFFS = { optimal = true, medium = true, easy = true, trivial = true }
local MAX_RECIPES = 600
local function Store()
local db = Codex.db
if not db then return nil end
db.recipes = db.recipes or {}
return db.recipes
end
local function ItemIDOf(link)
return type(link) == "string" and tonumber(link:match("item:(%d+)")) or nil
end
-- Schwierigkeit aus Zahl (Enum: 0 optimal, 1 mittel, 2 leicht, 3 trivial) oder Text
local DIFF_NUM = { [0] = "optimal", [1] = "medium", [2] = "easy", [3] = "trivial" }
local function DiffOf(v)
if type(v) == "number" then return DIFF_NUM[v] end
if type(v) == "string" then
v = v:lower()
if DIFFS[v] then return v end
end
return nil
end
local function Try(fn, ...)
if type(fn) ~= "function" then return nil end
local ok, a, b, c, d = pcall(fn, ...)
if ok then return a, b, c, d end
return nil
end
-- Neue Berufsoberflaeche (C_TradeSkillUI): { profession, skill, max, list = { { name, item, diff, reag, learned } } }
local function SnapshotNew()
local T = _G.C_TradeSkillUI
if type(T) ~= "table" then return nil end
local base = Try(T.GetBaseProfessionInfo)
local name = type(base) == "table" and (base.professionName or base.parentProfessionName) or nil
local skill = type(base) == "table" and tonumber(base.skillLevel) or nil
local max = type(base) == "table" and tonumber(base.maxSkillLevel) or nil
if not name or name == "" then return nil end
local ids = Try(T.GetAllRecipeIDs)
if type(ids) ~= "table" or #ids == 0 then return nil end
local list = {}
for _, id in ipairs(ids) do
local info = Try(T.GetRecipeInfo, id)
if type(info) == "table" and type(info.name) == "string" then
local diff = DiffOf(info.relativeDifficulty) or DiffOf(info.difficulty)
local item
local link = Try(T.GetRecipeItemLink, id)
item = ItemIDOf(link)
local reag = {}
local schem = Try(T.GetRecipeSchematic, id, false)
if type(schem) == "table" then
item = item or tonumber(schem.outputItemID)
for _, slot in ipairs(schem.reagentSlotSchematics or {}) do
local first = slot.reagents and slot.reagents[1]
local rid = first and tonumber(first.itemID)
if rid and tonumber(slot.quantityRequired) then reag[rid] = tonumber(slot.quantityRequired) end
end
end
if not next(reag) then
local n = Try(T.GetRecipeNumReagents, id) or 0
for j = 1, n do
local _, _, need = Try(T.GetRecipeReagentInfo, id, j)
local rid = ItemIDOf(Try(T.GetRecipeReagentItemLink, id, j))
if rid and tonumber(need) then reag[rid] = tonumber(need) end
end
end
list[#list + 1] = { name = info.name, item = item, diff = diff or "optimal", reag = reag,
learned = info.learned ~= false, hasDiff = diff ~= nil, trivial = tonumber(info.maxTrivialLevel) }
end
end
return { profession = name, skill = skill or 0, max = max or 0, list = list }
end
-- Alte Handwerks-Schnittstelle (GetTradeSkill...)
local function SnapshotOld()
if not (GetTradeSkillLine and GetNumTradeSkills and GetTradeSkillInfo) then return nil end
local ok, name, rank, maxRank = pcall(GetTradeSkillLine)
if not ok or type(name) ~= "string" or name == "" or name == "UNKNOWN" then return nil end
local list = {}
for i = 1, math.min(GetNumTradeSkills() or 0, MAX_RECIPES) do
local rname, rtype = GetTradeSkillInfo(i)
if type(rname) == "string" and DIFFS[rtype] then
local item = GetTradeSkillItemLink and ItemIDOf(GetTradeSkillItemLink(i)) or nil
local reag = {}
local nr = GetTradeSkillNumReagents and GetTradeSkillNumReagents(i) or 0
for j = 1, nr do
local _, _, need = GetTradeSkillReagentInfo(i, j)
local rid = GetTradeSkillReagentItemLink and ItemIDOf(GetTradeSkillReagentItemLink(i, j)) or nil
if rid and tonumber(need) then reag[rid] = tonumber(need) end
end
list[#list + 1] = { name = rname, item = item, diff = rtype, reag = reag, learned = true, hasDiff = true }
end
end
return { profession = name, skill = tonumber(rank) or 0, max = tonumber(maxRank) or 0, list = list }
end
-- Offenes Berufsfenster lesen: neue Oberflaeche zuerst, sonst die alte
function R.Snapshot()
return SnapshotNew() or SnapshotOld()
end
function R.ReadOpenWindow()
local store = Store()
if not store then return false end
local snap = R.Snapshot()
if not snap then return false end
R.last = snap
local prof = store[snap.profession]
if not prof then prof = {}; store[snap.profession] = prof end
local n = 0
for _ in pairs(prof) do n = n + 1 end
for _, r in ipairs(snap.list) do
local rec = prof[r.name]
if not rec then
if n >= MAX_RECIPES then break end
rec = { rg = {}, ob = {} }
prof[r.name] = rec
n = n + 1
end
if r.item then rec.it = r.item end
if r.trivial and r.trivial > 0 then rec.tv = r.trivial end -- ab diesem Skill grau
for rid, need in pairs(r.reag) do rec.rg[rid] = need end
if r.learned and r.hasDiff then
local range = rec.ob[r.diff]
if range then
range[1], range[2] = math.min(range[1], snap.skill), math.max(range[2], snap.skill)
else
rec.ob[r.diff] = { snap.skill, snap.skill }
end
end
rec.t = time and time() or 0
end
FW:Fire("CODEX_RECIPES_CHANGED", snap.profession)
return true, #snap.list
end
local reading = false
local function OnTradeSkill()
if reading then return end
reading = true
local function Run()
reading = false
Codex.Safe(R.ReadOpenWindow)
end
if C_Timer and C_Timer.After then
C_Timer.After(0.5, Run)
else
Run()
end
end
-- Diagnose fuer /fwberufdump: zeigt, was das Spiel zurueckgibt
function R.Dump()
local function Say(msg) print("|cffe0b84dFreewood Beruf:|r " .. tostring(msg)) end
local T = _G.C_TradeSkillUI
Say("C_TradeSkillUI: " .. tostring(type(T) == "table") .. ", alte Schnittstelle (GetTradeSkillLine): " .. tostring(type(GetTradeSkillLine) == "function"))
if type(T) == "table" then
local base = Try(T.GetBaseProfessionInfo)
if type(base) == "table" then
local keys = {}
for k, v in pairs(base) do keys[#keys + 1] = k .. "=" .. tostring(v) end
table.sort(keys)
Say("GetBaseProfessionInfo: " .. table.concat(keys, ", "))
else
Say("GetBaseProfessionInfo: nichts (Berufsfenster offen?)")
end
local ids = Try(T.GetAllRecipeIDs)
Say("GetAllRecipeIDs: " .. (type(ids) == "table" and (#ids .. " Rezepte") or "nichts"))
local id = type(ids) == "table" and ids[1]
if id then
local info = Try(T.GetRecipeInfo, id)
if type(info) == "table" then
local keys = {}
for k, v in pairs(info) do
if type(v) ~= "table" then keys[#keys + 1] = k .. "=" .. tostring(v) end
end
table.sort(keys)
Say("GetRecipeInfo(" .. id .. "): " .. table.concat(keys, ", "))
else
Say("GetRecipeInfo(" .. id .. "): nichts")
end
local schem = Try(T.GetRecipeSchematic, id, false)
if type(schem) == "table" then
local keys = {}
for k, v in pairs(schem) do keys[#keys + 1] = k .. "=" .. (type(v) == "table" and ("#" .. #v) or tostring(v)) end
table.sort(keys)
Say("GetRecipeSchematic: " .. table.concat(keys, ", "))
else
Say("GetRecipeSchematic: nichts; NumReagents=" .. tostring(Try(T.GetRecipeNumReagents, id)) .. ", ItemLink=" .. tostring(Try(T.GetRecipeItemLink, id)))
end
end
end
local snap = R.Snapshot()
if snap then
local learned = 0
for _, r in ipairs(snap.list) do if r.learned then learned = learned + 1 end end
Say(string.format("Auslesen: %s %d/%d, %d Rezepte (%d gelernt)", snap.profession, snap.skill, snap.max, #snap.list, learned))
local r = snap.list[1]
if r then
local mats = {}
for rid, n in pairs(r.reag) do mats[#mats + 1] = rid .. "x" .. n end
Say(string.format("Beispiel: %s, Ergebnis-Item %s, Farbe %s, Material %s", r.name, tostring(r.item), tostring(r.diff), table.concat(mats, " ")))
end
else
Say("Auslesen: nichts. Bitte das Berufsfenster oeffnen und den Befehl erneut eingeben.")
end
local pn, rn = R.Count()
Say("In der Datenbank: " .. pn .. " Berufe, " .. rn .. " Rezepte")
end
SLASH_FREEWOODRECIPEDUMP1 = "/fwberufdump"
SlashCmdList["FREEWOODRECIPEDUMP"] = function() R.Dump() end
function R:Start()
if self.started then return end
self.started = true
for _, event in ipairs({ "TRADE_SKILL_SHOW", "TRADE_SKILL_UPDATE", "TRADE_SKILL_LIST_UPDATE", "TRADE_SKILL_DATA_SOURCE_CHANGED" }) do
FW:RegisterEvent(event, OnTradeSkill, R)
end
end
function R:Stop()
for _, event in ipairs({ "TRADE_SKILL_SHOW", "TRADE_SKILL_UPDATE", "TRADE_SKILL_LIST_UPDATE", "TRADE_SKILL_DATA_SOURCE_CHANGED" }) do
FW:UnregisterEvent(event, R)
end
self.started = false
end
function R.Get(profession)
local store = Store()
return store and store[profession] or {}
end
function R.Count()
local store = Store()
local professions, recipes = 0, 0
for _, prof in pairs(store or {}) do
professions = professions + 1
for _ in pairs(prof) do recipes = recipes + 1 end
end
return professions, recipes
end
-- Skill-Anforderung eines Rezepts: vom Lehrer (kleinster Wert), sonst kleinster Skill, bei dem es orange war
function R.ReqOf(profession, name)
local best
for _, u in pairs(Codex.V().units) do
if u.tr and u.ro and (u.ro.proftrainer or 0) > 0 then
for _, r in pairs(u.tr) do
if r.nm == name and r.sn == profession and tonumber(r.sr) and (not best or r.sr < best) then best = r.sr end
end
end
end
if best then return best, true end
local rec = R.Get(profession)[name]
local o = rec and rec.ob and rec.ob.optimal
if o then return o[1], false end
return nil
end
-- Schwierigkeit bei gegebenem Skill: aus Beobachtungen, sonst geschaetzt (je 10 Skill pro Farbe).
-- Rueckgabe: "optimal" | "medium" | "easy" | "trivial" | "locked", und ob geschaetzt (true) oder beobachtet (false)
function R.DiffAt(profession, name, skill)
local rec = R.Get(profession)[name]
local ob = rec and rec.ob or {}
for diff, range in pairs(ob) do
if skill >= range[1] and skill <= range[2] then return diff, false end
end
local req = R.ReqOf(profession, name)
if not req and not next(ob) and not (rec and rec.tv) then return nil end
-- Schwellen: gelb, gruen, grau beginnen bei
local base = req or (ob.optimal and ob.optimal[1]) or 0
local tv = rec and rec.tv
-- Ist die graue Schwelle (maxTrivialLevel) bekannt, liegen gruen und gelb je 10 Skill darunter
local gray = ob.trivial and ob.trivial[1] or tv or math.max(base + 30, ob.easy and ob.easy[2] + 1 or 0)
local green = ob.easy and ob.easy[1] or (tv and math.max(gray - 10, ob.medium and ob.medium[2] + 1 or 0)) or math.max(gray - 10, ob.medium and ob.medium[2] + 1 or 0)
local yellow = ob.medium and ob.medium[1] or math.max(green - 10, ob.optimal and ob.optimal[2] + 1 or 0)
if not req and not ob.optimal and tv then base = math.max(0, yellow - 10) end
local first = req or (ob.optimal and ob.optimal[1]) or 0
if skill < first then return "locked", true end
if skill < yellow then return "optimal", true end
if skill < green then return "medium", true end
if skill < gray then return "easy", true end
return "trivial", true
end
-- Rezepte beim Lehrer: Liste { name, profession, req, price, trainerID, rec = Rezept oder nil }
function R.TrainerRows()
local out, seen = {}, {}
for uid, u in pairs(Codex.V().units) do
if u.tr and u.ro and (u.ro.proftrainer or 0) > 0 then
for _, r in pairs(u.tr) do
if r.nm and r.sn then
local key = r.sn .. "\1" .. r.nm
local e = seen[key]
local price = Codex.ModePrice and Codex.ModePrice(r.pr) or nil
if not e then
e = { name = r.nm, profession = r.sn, req = tonumber(r.sr), price = price, trainerID = uid,
rec = R.Get(r.sn)[r.nm] }
seen[key] = e
out[#out + 1] = e
elseif price and (not e.price or price < e.price) then
e.price, e.trainerID = price, uid
end
end
end
end
end
return out
end
+1289
View File
File diff suppressed because it is too large Load Diff
+708
View File
@@ -0,0 +1,708 @@
-- Freewood-OctoWoW-Codex: Vorlage (Startdaten aus Freewood-OctoWoW-CodexData, Vanilla)
-- Das Datenaddon ist "LoadOnDemand" und wird erst beim ersten Zugriff geladen. Alles hier ist nur eine Vorlage:
-- eigene und geteilte Live-Daten gehen immer vor. Die Daten liegen als kurze Zeichenketten vor und werden erst
-- beim Zugriff entpackt (und gemerkt).
--
-- Formate (siehe tools/pf2seed.lua):
-- Quest "Stufe|Mindeststufe|Rassen|Klassen|Beruf|Vorquests|Start|Abgabe|Ziele" (Refs: "U123.O55.I6497")
-- Kreatur "Stufe|Fraktion|Rang|Gebiet:Zelle,Zelle;Gebiet:Zelle" (Zelle = x*100+y in 1 %)
-- Item "u<id>=<Chance %>,...;o<id>=<Chance>,...;v<haendlerID>,..."
-- Rollen meta[rolle] = "id:wert,id:wert" (negative IDs sind Objekte)
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS.Codex
if not FW or not Codex then
return
end
local Util = FW.Util
local GRID = Codex.GRID
local Seed = {}
Codex.Seed = Seed
local DATA_ADDON = "Freewood-OctoWoW-CodexData"
local S -- FreewoodCodexSeed, sobald geladen
local failed = false
local floor = math.floor
-- Rollen: Reihenfolge der Anzeige
Seed.ROLES = {
units = { "vendor", "repair", "flight", "innkeeper", "stablemaster", "spirithealer", "auctioneer", "battlemaster", "banker", "rares" },
objects = { "mailbox", "meetingstone", "chests", "herbs", "mines", "fish" },
}
------------------------------------------------------------------------
-- Laden
------------------------------------------------------------------------
function Seed.Load()
if S then
return true
end
if failed then
return false
end
local g = _G.FreewoodCodexSeed
if not (g and g.ready) then
local load = (C_AddOns and C_AddOns.LoadAddOn) or LoadAddOn
if type(load) == "function" then
pcall(load, DATA_ADDON)
end
g = _G.FreewoodCodexSeed
end
if g and g.ready then
S = g
return true
end
failed = true
return false
end
-- Laedt bei Bedarf (nur die Oberflaeche ruft das auf, damit der Ladevorgang beim Oeffnen des Tabs geschieht)
function Seed.Disabled()
local db = Codex.db
return db and db.settings and db.settings.noSeed == true or false
end
function Seed.Available()
if Seed.Disabled() then return false end
return Seed.Load()
end
-- Nur pruefen, ob schon geladen ist, ohne zu laden (fuer Code, der waehrend des Spielens laeuft)
function Seed.Ready()
return S ~= nil and not Seed.Disabled()
end
-- fuer Tests: Zustand zuruecksetzen
local caches
function Seed.Reset()
S, failed = nil, false
caches = { quest = {}, unit = {}, object = {}, item = {}, lists = {}, hay = {} }
Seed.nextIdx, Seed.rev, Seed.roleIdx, Seed.areaMap = nil, nil, nil, nil
end
Seed.Reset()
local function Lang()
return Codex.Lang()
end
------------------------------------------------------------------------
-- Kleine Helfer
------------------------------------------------------------------------
local function Fields(s, n)
local out, pos = {}, 1
for i = 1, n do
local e = s:find("|", pos, true)
if e then
out[i] = s:sub(pos, e - 1)
pos = e + 1
else
out[i] = s:sub(pos)
pos = #s + 2
end
end
return out
end
local function Nums(s)
local list = {}
for n in (s or ""):gmatch("%d+") do
list[#list + 1] = tonumber(n)
end
return list
end
local KIND_OF = { U = "units", O = "objects", I = "items" }
local function Refs(s)
local list = {}
for k, id in (s or ""):gmatch("(%a)(%d+)") do
list[#list + 1] = { kind = KIND_OF[k], id = tonumber(id) }
end
return list
end
local function AreaMap()
if Seed.areaMap then
return Seed.areaMap
end
local map = {}
for a, m in (S.areas or ""):gmatch("(%d+)=(%d+)") do
map[tonumber(a)] = tonumber(m)
end
Seed.areaMap = map
return map
end
local function Pick(de, en)
if Lang() == "en" then
return en or de
end
return de or en
end
------------------------------------------------------------------------
-- Quests
------------------------------------------------------------------------
function Seed.Quest(id)
local c = caches.quest[id]
if c ~= nil then
return c or nil
end
if not Seed.Load() then
return nil
end
local s = S.q[id]
if not s then
caches.quest[id] = false
return nil
end
local f = Fields(s, 9)
local q = {
id = id, lvl = tonumber(f[1]), min = tonumber(f[2]), race = tonumber(f[3]), class = tonumber(f[4]),
skill = tonumber(f[5]), pre = Nums(f[6]), start = Refs(f[7]), stop = Refs(f[8]), obj = Refs(f[9]),
}
caches.quest[id] = q
return q
end
function Seed.QuestTitle(id)
if not Seed.Load() then
return nil
end
return Pick(S.qd[id], S.qe[id])
end
-- Folgequests: alle Quests, die diese als Vorquest haben
function Seed.NextQuests(id)
if not Seed.Load() then
return {}
end
if not Seed.nextIdx then
local idx = {}
for qid, s in pairs(S.q) do
local pre = Fields(s, 6)[6]
for p in pre:gmatch("%d+") do
local n = tonumber(p)
local l = idx[n]
if not l then
l = {}
idx[n] = l
end
l[#l + 1] = qid
end
end
for _, l in pairs(idx) do
table.sort(l)
end
Seed.nextIdx = idx
end
return Seed.nextIdx[id] or {}
end
-- Position in der Questkette: pos, total (zum Beispiel 2, 4), nil ausserhalb einer Kette.
-- Bei Verzweigungen zaehlt der laengste Weg vor und nach der Quest.
local chainMemo = {}
local function ChainLen(id, dir, depth)
local key = dir .. id
local c = chainMemo[key]
if c then
return c
end
if depth > 40 then
return 1
end
local best = 0
local list
if dir == "u" then
local q = Seed.Quest(id)
list = q and q.pre or {}
else
list = Seed.NextQuests(id)
end
for _, other in ipairs(list) do
if other ~= id then
best = math.max(best, ChainLen(other, dir, depth + 1))
end
end
chainMemo[key] = best + 1
return best + 1
end
function Seed.ChainPos(id)
if not Seed.Quest(id) then
return nil
end
local up, down = ChainLen(id, "u", 0), ChainLen(id, "d", 0)
if up + down <= 2 then
return nil
end
return up, up + down - 1
end
-- Stellung in einer Questkette: "start" (hat Folgequests, keine Vorquest), "mid", "end" (nur Vorquests)
-- oder nil (gehoert zu keiner Kette)
function Seed.ChainRole(id)
local q = Seed.Quest(id)
if not q then
return nil
end
-- manche Quests nennen sich selbst als Vorquest: das zaehlt nicht als Kette
local hasPre, hasNext = false, false
for _, p in ipairs(q.pre or {}) do
if p ~= id then hasPre = true break end
end
for _, n in ipairs(Seed.NextQuests(id)) do
if n ~= id then hasNext = true break end
end
if hasPre and hasNext then return "mid" end
if hasNext then return "start" end
if hasPre then return "end" end
return nil
end
------------------------------------------------------------------------
-- Kreaturen und Objekte
------------------------------------------------------------------------
local function EntityData(kind)
return kind == "units" and S.u or S.o
end
-- Rueckgabe: { id, l1, l2, fac, rank, spots, cells }
function Seed.Entity(kind, id)
local cache = kind == "units" and caches.unit or caches.object
local c = cache[id]
if c ~= nil then
return c or nil
end
if not Seed.Load() then
return nil
end
local s = EntityData(kind)[id]
if not s then
cache[id] = false
return nil
end
local f = Fields(s, 4)
local l1, l2 = f[1]:match("^(%d+)%-?(%d*)$")
if l1 and l2 == "" then
l2 = l1
end
local e = {
id = id, l1 = tonumber(l1), l2 = tonumber(l2), fac = f[2] ~= "" and f[2] or nil,
rank = tonumber(f[3]), spots = f[4],
}
cache[id] = e
return e
end
function Seed.Name(kind, id)
if not Seed.Load() then
return nil
end
if kind == "units" then
return Pick(S.ud[id], S.ue[id])
elseif kind == "objects" then
return Pick(S.od[id], S.oe[id])
elseif kind == "items" then
return Pick(S.id[id], S.ie[id])
end
return nil
end
-- Zellen wie im Codex: { [Karte*GRID^2 + y*GRID + x] = 1 }
function Seed.Cells(kind, id)
local e = Seed.Entity(kind, id)
if not e then
return {}
end
if e.cells then
return e.cells
end
local cells = {}
local areas = AreaMap()
for area, list in e.spots:gmatch("(%d+):([%d,]+)") do
local m = areas[tonumber(area)]
if m then
for c in list:gmatch("%d+") do
c = tonumber(c)
local cx = math.min(GRID - 1, floor(c / 100) * 2 + 1)
local cy = math.min(GRID - 1, (c % 100) * 2 + 1)
cells[m * GRID * GRID + cy * GRID + cx] = 1
end
end
end
e.cells = cells
return cells
end
-- Erste Fundstelle: Karte, x, y (Anteile 0..1)
function Seed.FirstSpot(kind, id)
local e = Seed.Entity(kind, id)
if not e then
return nil
end
local area, list = e.spots:match("(%d+):([%d,]+)")
local m = area and AreaMap()[tonumber(area)]
local c = list and tonumber(list:match("%d+"))
if not (m and c) then
return nil
end
return m, (floor(c / 100) + 0.5) / 100, ((c % 100) + 0.5) / 100
end
-- Gebiet einer Kreatur / eines Objekts (Karten-ID der ersten Fundstelle)
function Seed.MapOf(kind, id)
local m = Seed.FirstSpot(kind, id)
return m
end
-- Kleinster Abstand (Kartenanteil) einer Zellenmenge zum Spieler auf der aktuellen Karte, sonst nil
function Seed.Distance(cells, loc)
loc = loc or (Util.GetPlayerLocation and Util.GetPlayerLocation())
if not (loc and loc.mapID and loc.x and loc.y) then
return nil
end
local best
local g2 = GRID * GRID
for key in pairs(cells or {}) do
if floor(key / g2) == loc.mapID then
local idx = key % g2
local dx = ((idx % GRID) + 0.5) / GRID - loc.x
local dy = (floor(idx / GRID) + 0.5) / GRID - loc.y
local d = dx * dx + dy * dy
if not best or d < best then
best = d
end
end
end
return best and math.sqrt(best) or nil
end
------------------------------------------------------------------------
-- Items: Quellen
------------------------------------------------------------------------
-- Rueckgabe: { u = { { id, chance } }, o = { { id, chance } }, v = { haendlerID } }
function Seed.Item(id)
local c = caches.item[id]
if c ~= nil then
return c or nil
end
if not Seed.Load() then
return nil
end
local s = S.i[id]
if not s then
caches.item[id] = false
return nil
end
local it = { u = {}, o = {}, v = {} }
for part in s:gmatch("[^;]+") do
local tag, body = part:sub(1, 1), part:sub(2)
if tag == "u" or tag == "o" then
for k, v in body:gmatch("(%d+)=([%d%.]+)") do
local t = it[tag]
t[#t + 1] = { id = tonumber(k), chance = tonumber(v) }
end
elseif tag == "v" then
it.v = Nums(body)
end
end
caches.item[id] = it
return it
end
function Seed.ItemHasVendor(id)
if not Seed.Load() then
return false
end
local s = S.i[id]
return s ~= nil and (s:find(";v", 1, true) ~= nil or s:sub(1, 1) == "v")
end
local function Reverse()
if Seed.rev then
return Seed.rev
end
-- flache Listen { Item, Chance, Item, Chance ... } sparen Speicher
local rev = { u = {}, o = {}, v = {} }
for itemID, s in pairs(S.i) do
for part in s:gmatch("[^;]+") do
local tag, body = part:sub(1, 1), part:sub(2)
if tag == "u" or tag == "o" then
local t = rev[tag]
for k, v in body:gmatch("(%d+)=([%d%.]+)") do
local key = tonumber(k)
local l = t[key]
if not l then
l = {}
t[key] = l
end
l[#l + 1] = itemID
l[#l + 1] = tonumber(v)
end
elseif tag == "v" then
for n in body:gmatch("%d+") do
local key = tonumber(n)
local l = rev.v[key]
if not l then
l = {}
rev.v[key] = l
end
l[#l + 1] = itemID
end
end
end
end
Seed.rev = rev
return rev
end
-- Beute einer Kreatur / eines Objekts laut Vorlage: { { item, chance (Prozent) } }, nach Chance sortiert
function Seed.Drops(kind, id)
if not Seed.Load() then
return {}
end
local flat = Reverse()[kind == "units" and "u" or "o"][id]
local list = {}
for i = 1, #(flat or {}), 2 do
list[#list + 1] = { item = flat[i], chance = flat[i + 1] }
end
table.sort(list, function(a, b)
if a.chance ~= b.chance then return a.chance > b.chance end
return a.item < b.item
end)
return list
end
-- Waren eines Haendlers: { itemID }
function Seed.VendorItems(id)
if not Seed.Load() then
return {}
end
local list = {}
for i, itemID in ipairs(Reverse().v[id] or {}) do
list[i] = itemID
end
table.sort(list)
return list
end
------------------------------------------------------------------------
-- Quests je Kreatur / Objekt (Geber, Abgabe, Ziel)
------------------------------------------------------------------------
function Seed.QuestsOf(kind, id)
if not Seed.Load() then
return {}, {}, {}
end
if not Seed.questRev then
local rev = { give = {}, turn = {}, target = {} }
local function Add(t, key, qid)
local l = t[key]
if not l then
l = {}
t[key] = l
end
if l[#l] ~= qid then
l[#l + 1] = qid
end
end
for qid in pairs(S.q) do
local q = Seed.Quest(qid)
for _, r in ipairs(q.start) do
if r.kind ~= "items" then Add(rev.give, r.kind .. r.id, qid) end
end
for _, r in ipairs(q.stop) do
if r.kind ~= "items" then Add(rev.turn, r.kind .. r.id, qid) end
end
for _, r in ipairs(q.obj) do
if r.kind ~= "items" then Add(rev.target, r.kind .. r.id, qid) end
end
end
Seed.questRev = rev
end
local key = kind .. id
local rev = Seed.questRev
return rev.give[key] or {}, rev.turn[key] or {}, rev.target[key] or {}
end
------------------------------------------------------------------------
-- Rollen (Haendler, Flugmeister ...)
------------------------------------------------------------------------
local function RoleIndex()
if Seed.roleIdx then
return Seed.roleIdx
end
local idx = { units = {}, objects = {}, ids = {} }
for _, group in ipairs({ Seed.ROLES.units, Seed.ROLES.objects }) do
for _, role in ipairs(group) do
local list = {}
for k, v in (S.meta[role] or ""):gmatch("(%-?%d+):([^,]*)") do
local n = tonumber(k)
local kind = n > 0 and "units" or "objects"
n = math.abs(n)
local t = idx[kind][n]
if not t then
t = {}
idx[kind][n] = t
end
t[role] = v ~= "" and v or true
list[#list + 1] = { kind = kind, id = n }
end
idx.ids[role] = list
end
end
Seed.roleIdx = idx
return idx
end
-- Rollen einer Kreatur / eines Objekts: { vendor = "H", herbs = 280, ... } oder nil
function Seed.Roles(kind, id)
if not Seed.Load() then
return nil
end
return RoleIndex()[kind][id]
end
-- Rollen als Liste in fester Reihenfolge
function Seed.RoleList(kind, id)
local roles = Seed.Roles(kind, id)
if not roles then
return {}
end
local list = {}
for _, role in ipairs(Seed.ROLES[kind]) do
if roles[role] then
list[#list + 1] = role
end
end
return list
end
function Seed.RoleIDs(role)
if not Seed.Load() then
return {}
end
return RoleIndex().ids[role] or {}
end
-- Naechste Kreaturen / Objekte mit einer Rolle auf der aktuellen Karte: { { kind, id, dist, m, x, y } }
function Seed.Nearest(role, limit, faction)
local loc = Util.GetPlayerLocation and Util.GetPlayerLocation()
local list = {}
if not (loc and loc.mapID and loc.x and loc.y) or not Seed.Load() then
return list
end
local side = faction or (UnitFactionGroup and select(1, UnitFactionGroup("player"))) or nil
local letter = side == "Alliance" and "A" or side == "Horde" and "H" or nil
for _, ref in ipairs(Seed.RoleIDs(role)) do
local e = Seed.Entity(ref.kind, ref.id)
local roles = Seed.Roles(ref.kind, ref.id)
local value = roles and roles[role]
local fac = (type(value) == "string" and value:match("^[AH]+$")) or (e and e.fac)
if e and (not letter or not fac or fac:find(letter, 1, true)) then
local d = Seed.Distance(Seed.Cells(ref.kind, ref.id))
if d then
local best, bm, bx, by
local g2 = GRID * GRID
for key in pairs(Seed.Cells(ref.kind, ref.id)) do
if floor(key / g2) == loc.mapID then
local idx = key % g2
local x, y = ((idx % GRID) + 0.5) / GRID, (floor(idx / GRID) + 0.5) / GRID
local dd = (x - loc.x) ^ 2 + (y - loc.y) ^ 2
if not best or dd < best then
best, bm, bx, by = dd, loc.mapID, x, y
end
end
end
list[#list + 1] = { kind = ref.kind, id = ref.id, dist = d, m = bm, x = bx, y = by }
end
end
end
table.sort(list, function(a, b)
if a.dist ~= b.dist then return a.dist < b.dist end
return a.id < b.id
end)
if limit then
for i = #list, limit + 1, -1 do
list[i] = nil
end
end
return list
end
------------------------------------------------------------------------
-- Listen fuer die Oberflaeche
------------------------------------------------------------------------
-- Alle IDs einer Art (sortiert)
function Seed.IDs(kind)
if not Seed.Load() then
return {}
end
local c = caches.lists[kind]
if c then
return c
end
local src
if kind == "quests" then src = S.q
elseif kind == "units" then src = S.u
elseif kind == "objects" then src = S.o
elseif kind == "items" then src = S.ie end
local list = {}
for id in pairs(src or {}) do
list[#list + 1] = id
end
if kind == "items" then
for id in pairs(S.i) do
if not S.ie[id] then list[#list + 1] = id end
end
end
table.sort(list)
caches.lists[kind] = list
return list
end
-- Kleingeschriebene Namen (deutsch und englisch) zur Suche
function Seed.Haystack(kind, id)
local c = caches.hay[kind]
if not c then
c = {}
caches.hay[kind] = c
end
local h = c[id]
if h then
return h
end
local de, en
if kind == "quests" then de, en = S.qd[id], S.qe[id]
elseif kind == "units" then de, en = S.ud[id], S.ue[id]
elseif kind == "objects" then de, en = S.od[id], S.oe[id]
else de, en = S.id[id], S.ie[id] end
h = ((de or "") .. "\n" .. (en or "")):lower()
c[id] = h
return h
end
function Seed.Count()
if not Seed.Load() then
return 0, 0, 0, 0
end
local function n(t)
local c = 0
for _ in pairs(t) do c = c + 1 end
return c
end
return n(S.q), n(S.u), n(S.o), n(S.i)
end
+2314
View File
File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
-- Freewood-OctoWoW-Codex: Abschnitt "Datenbank teilen" in den Einstellungen
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
local Sync = NS and NS.Sync
local Share = NS and NS.Share
if not FW or not Codex or not Sync or not Share or not FW.UI or not FW.UI.RegisterSettingsSection then
return
end
local UI = FW.UI
local function Build(parent)
local L = FW.L
local f, stack = UI.CreateSection(parent, L.CODEX_SHARE_HEADER)
local note = UI.CreateLabel(f, L.CODEX_SHARE_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
stack:Add(note, { fit = true, gap = 8 })
local box = UI.CreateCheckbox(f, L.CODEX_SHARE_SWITCH, function()
return Sync:IsEnabled()
end, function(on)
Sync:SetEnabled(on)
end)
stack:Add(box, { fit = true, gap = 10 })
UI.AttachTooltip(box, function() return L.CODEX_SHARE_SWITCH end, function() return L.CODEX_SHARE_SWITCH_TIP end)
local callButton = UI.CreateButton(f, L.CODEX_SHARE_NOW, 200, 26, function()
if Sync:IsEnabled() then
Sync:JoinGlobal()
Share.Scan(true)
Sync:SendHello()
end
end, "default")
stack:Add(callButton, { height = 26, gap = 12 })
local clearButton
local armed = false
clearButton = UI.CreateButton(f, L.CODEX_SHARE_CLEAR, 240, 26, function()
if not armed then
armed = true
clearButton:SetLabel(L.CODEX_SHARE_CLEAR_SURE)
C_Timer.After(5, function()
armed = false
clearButton:SetLabel(L.CODEX_SHARE_CLEAR)
end)
return
end
armed = false
clearButton:SetLabel(L.CODEX_SHARE_CLEAR)
local n = Share.ClearRemote()
FW:Print(string.format(L.CODEX_SHARE_CLEARED, n))
FW:Fire("CODEX_CHANGED", "import")
FW:Fire("CODEX_SYNC_CHANGED")
end, "default")
stack:Add(clearButton, { height = 26, gap = 8 })
local status = UI.CreateLabel(f, "", { size = 11, color = "textDim", justifyV = "TOP" })
stack:Add(status, { fit = true, gap = 10 })
function f.Refresh()
box:Refresh()
local st = Sync:Stats()
local text = string.format(L.CODEX_SHARE_STATS, st.sent, st.received)
text = text .. "\n" .. string.format(L.CODEX_SHARE_SOURCES, st.senders, st.records)
text = text .. "\n" .. string.format(L.CODEX_SHARE_HEARD, st.heard or 0)
if Sync:IsEnabled() then
local key = "CODEX_SHARE_GLOBAL_" .. string.upper(st.global or "off")
text = text .. "\n" .. (L[key] or L.CODEX_SHARE_GLOBAL_OFF)
end
status:SetText(text)
stack:Layout()
end
FW:On("CODEX_SYNC_CHANGED", function()
if f:IsVisible() then
f.Refresh()
end
end, f)
stack:Layout()
return f
end
UI:RegisterSettingsSection("codex-share", {
order = 105,
height = 360,
build = Build,
refresh = function(frame)
frame.Refresh()
end,
})
+322
View File
@@ -0,0 +1,322 @@
-- Freewood-OctoWoW-Codex: Ortspunkte (Eingang zu Instanz oder Hoehle, Berg-Aufstieg, Engstelle, Zielpunkt)
--
-- Berechtigte (Modul Freewood-OctoWoW-Dev, Rolle Bearbeiter oder hoeher) erfassen einzelne Punkte, an denen der Levelplan vorbeimuss:
-- zum Beispiel der Eingang einer Instanz fuer eine Dungeon-Quest. Ein Punkt gehoert zu einer oder mehreren Quests (q) und/oder
-- zu einem ganzen Gebiet (am = Karte; negativ = Instanz). Gespeichert wird in db.spots[id]; das normale Teilen verschickt sie als
-- Datensatz "h<ID>", angenommen nur von Absendern, die selbst berechtigt sind.
--
-- db.spots[id] = { k = Art, f = "A" | "H" | nil, n = Name, p = { m, x, y }, q = { Quest-IDs }, am = Gebiet, del, upd }
-- Die ID ist ein Pruefwert aus Fraktion und Ort (auf etwa 0,5 % gerundet): wer an derselben Stelle erneut speichert, aendert den Punkt.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Spots = { draft = { q = {} }, rev = 0 }
Codex.Spots = Spots
NS.Spots = Spots
Spots.KINDS = { "instance", "cave", "mountain", "pass", "point", "other" }
local KIND_OK = {}
for _, k in ipairs(Spots.KINDS) do KIND_OK[k] = true end
Spots.MAX_QUESTS = 20
local function Now() return _G.time and _G.time() or 0 end
local function DB()
local db = Codex.db
if not db then return nil end
db.spots = db.spots or {}
return db
end
local function CanEdit()
return Codex.Trust ~= nil and Codex.Trust.CanEdit()
end
local function Round(v, f) return math.floor(v * f + 0.5) end
local function Hash(text)
local h = 5381
for i = 1, #text do h = (h * 33 + text:byte(i)) % 99999989 end
return h
end
function Spots.Id(rec)
local p = rec.p or {}
return Hash(table.concat({ "s", tostring(rec.f or "-"), tostring(p.m), Round(p.x or 0, 200), Round(p.y or 0, 200) }, "|"))
end
-- Aenderung melden (auch fuer empfangene Datensaetze: ruft CodexShare auf)
function Spots.Bump(id)
Spots.rev = Spots.rev + 1
Spots.index = nil
FW:Fire("CODEX_SPOTS_CHANGED", id)
end
------------------------------------------------------------------------
-- Entwurf
------------------------------------------------------------------------
function Spots.Draft()
return Spots.draft
end
function Spots.Reset()
local d = Spots.draft
d.p, d.am, d.kind, d.faction, d.name, d.editing = nil, nil, nil, nil, nil, nil
d.q = {}
FW:Fire("CODEX_SPOTS_DRAFT")
end
-- Ort des Spielers { m, x, y } oder nil
function Spots.Here()
local T = Codex.Transport
if T and T.Here then return T.Here() end
return nil
end
function Spots.Capture()
local here = Spots.Here()
if not here then return false end
Spots.draft.p = here
FW:Fire("CODEX_SPOTS_DRAFT")
return true
end
-- Gebiet, in dem der Spieler gerade steht: Instanz (negativ) oder Karte
function Spots.AreaHere()
local inst = Codex.CurrentInstance and Codex.CurrentInstance()
if inst then return -inst end
local here = Spots.Here()
return here and here.m or nil
end
function Spots.SetArea(am)
Spots.draft.am = am
FW:Fire("CODEX_SPOTS_DRAFT")
end
function Spots.AddQuest(id)
id = tonumber(id)
local d = Spots.draft
if not id or id < 1 or id ~= math.floor(id) or #d.q >= Spots.MAX_QUESTS then return false end
for _, v in ipairs(d.q) do if v == id then return true end end
d.q[#d.q + 1] = id
FW:Fire("CODEX_SPOTS_DRAFT")
return true
end
function Spots.RemoveQuest(id)
local d = Spots.draft
for i, v in ipairs(d.q) do
if v == id then table.remove(d.q, i) FW:Fire("CODEX_SPOTS_DRAFT") return true end
end
return false
end
-- Quests zu einem Suchtext (Nummer oder Teil des Namens): { { id, title } }, hoechstens limit
function Spots.FindQuests(text, limit)
limit = limit or 8
local out = {}
text = tostring(text or ""):gsub("^%s+", ""):gsub("%s+$", "")
if text == "" then return out end
local num = tonumber(text)
local seed = Codex.Seed and Codex.Seed.Ready and Codex.Seed.Ready() and Codex.Seed or nil
local V = Codex.V and Codex.V()
local function Title(id)
return Codex.QuestTitle(V and V.quests[id], id)
end
if num then
out[1] = { id = num, title = Title(num) }
return out
end
local needle = text:lower()
local seen = {}
for id, q in pairs(V and V.quests or {}) do
local t = Title(id)
if t and t:lower():find(needle, 1, true) then
seen[id] = true
out[#out + 1] = { id = id, title = t }
end
end
if seed and seed.IDs then
for _, id in ipairs(seed.IDs("quests")) do
if not seen[id] and seed.Haystack("quests", id):find(needle, 1, true) then
out[#out + 1] = { id = id, title = Title(id) }
end
end
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)
while #out > limit do out[#out] = nil end
return out
end
-- Entwurf pruefen. Rueckgabe: true oder false, Grund
function Spots.CheckDraft()
local d = Spots.draft
if not d.p then return false, "pos" end
if not KIND_OK[d.kind or "instance"] then return false, "kind" end
if not d.am and #d.q == 0 then return false, "link" end
return true
end
------------------------------------------------------------------------
-- Speichern, Bearbeiten, Loeschen
------------------------------------------------------------------------
local function Touch(id)
if Codex.Share and Codex.Share.InvalidateScan then Codex.Share.InvalidateScan() end
Spots.Bump(id)
FW:Fire("CODEX_CHANGED", "spots", id)
end
local function Describe(rec)
local M = Codex.MapName
return string.format("%s: %s (%.1f / %.1f)%s", tostring(rec.k), M and M(rec.p.m) or rec.p.m, rec.p.x * 100, rec.p.y * 100,
rec.am and (" > " .. (M and M(rec.am) or rec.am)) or "")
end
local function Log(id, field, old, new)
if Codex.DevLog then
pcall(Codex.DevLog, { who = Codex.Trust and Codex.Trust.SelfKey() or "?", kind = "h", id = id, field = field, old = old, new = new })
end
end
-- Rueckgabe: id oder nil, Grund
function Spots.Save()
if not CanEdit() then return nil, "rights" end
local ok, why = Spots.CheckDraft()
if not ok then return nil, why end
local d = Spots.draft
local q = {}
for _, v in ipairs(d.q) do q[#q + 1] = v end
table.sort(q)
local name = type(d.name) == "string" and d.name:sub(1, 40) or ""
local rec = {
k = d.kind or "instance", f = (d.faction == "A" or d.faction == "H") and d.faction or nil,
n = name ~= "" and name or nil, p = { m = d.p.m, x = d.p.x, y = d.p.y }, q = q, am = d.am, upd = Now(),
}
local db = DB()
local id = Spots.Id(rec)
local old = db.spots[id]
rec.upd = math.max(rec.upd, (old and old.upd or 0) + 1)
db.spots[id] = rec
Log(id, "add", old and not old.del and Describe(old) or nil, Describe(rec))
Touch(id)
Spots.Reset()
return id
end
-- Liste { id, k, f, n, p, q, am, upd, del }; je ID der neueste Stand; geloeschte nur mit withDeleted
function Spots.All(withDeleted)
local best = {}
local db = DB()
if not db then return {} end
for id, rec in pairs(db.spots) do
if type(rec) == "table" and rec.p then best[id] = rec end
end
if Codex.Share and Codex.Share.RemoteRecords then
for _, rec in ipairs(Codex.Share.RemoteRecords("h")) do
local cur = best[rec.id]
if not cur or (rec.upd or 0) > (cur.upd or 0) then best[rec.id] = rec end
end
end
local out = {}
for id, rec in pairs(best) do
if withDeleted or not rec.del then
out[#out + 1] = { id = id, k = rec.k, f = rec.f, n = rec.n, p = rec.p, q = rec.q or {}, am = rec.am, upd = rec.upd, del = rec.del }
end
end
table.sort(out, function(x, y)
if x.p.m ~= y.p.m then return x.p.m < y.p.m end
return x.id < y.id
end)
return out
end
function Spots.Get(id)
for _, r in ipairs(Spots.All(true)) do
if r.id == id then return r end
end
return nil
end
-- Punkt in den Entwurf laden (zum Aendern); Speichern an derselben Stelle ersetzt ihn
function Spots.Edit(id)
local r = Spots.Get(id)
if not r or r.del then return false end
local d = Spots.draft
d.p = { m = r.p.m, x = r.p.x, y = r.p.y }
d.kind, d.faction, d.name, d.am, d.editing = r.k, r.f, r.n, r.am, id
d.q = {}
for _, v in ipairs(r.q) do d.q[#d.q + 1] = v end
FW:Fire("CODEX_SPOTS_DRAFT")
return true
end
function Spots.Delete(id)
if not CanEdit() then return false end
local rec = Spots.Get(id)
if not rec then return false end
local db = DB()
db.spots[id] = { k = rec.k, f = rec.f, n = rec.n, p = rec.p, q = rec.q, am = rec.am, del = true,
upd = math.max(Now(), (rec.upd or 0) + 1) }
Log(id, "del", Describe(rec), nil)
Touch(id)
return true
end
------------------------------------------------------------------------
-- Lesen fuer den Levelplan
------------------------------------------------------------------------
local EMPTY = {}
local function BuildIndex()
local idx = { quest = {}, area = {} }
for _, r in ipairs(Spots.All()) do
local s = { id = r.id, k = r.k, f = r.f, n = r.n, m = r.p.m, x = r.p.x, y = r.p.y }
for _, qid in ipairs(r.q) do
idx.quest[qid] = idx.quest[qid] or {}
table.insert(idx.quest[qid], s)
end
if r.am then
idx.area[r.am] = idx.area[r.am] or {}
table.insert(idx.area[r.am], s)
end
end
idx.empty = next(idx.quest) == nil and next(idx.area) == nil
return idx
end
-- Punkte, die fuer ein Ziel gelten: Quest-ID (oder nil) und Karte des Ziels (Instanzen negativ); faction = "A" | "H" | nil.
-- Rueckgabe: Liste { id, k, n, m, x, y } (leer, wenn nichts passt)
function Spots.For(questID, m, faction)
if not Spots.index then Spots.index = BuildIndex() end
local idx = Spots.index
if idx.empty then return EMPTY end
local out, seen = {}, {}
local function Add(list)
for _, s in ipairs(list or {}) do
if not seen[s.id] and not (faction and s.f and s.f ~= faction) then
seen[s.id] = true
out[#out + 1] = s
end
end
end
if questID then Add(idx.quest[questID]) end
if m then Add(idx.area[m]) end
return out
end
function Spots.Count()
return #Spots.All()
end
+1247
View File
File diff suppressed because it is too large Load Diff
+505
View File
@@ -0,0 +1,505 @@
-- Freewood-OctoWoW-Codex: Flugrouten (Taxi)
--
-- Erfasst beim Flug: Start- und Zielpunkt, die Strecke (Punkte alle paar Sekunden), die Flugdauer und den bezahlten Preis.
-- Je Charakter: welche Flugpunkte er kennt, wie viel Gold er fuer Fluege ausgegeben hat und wie oft er geflogen ist.
--
-- db.taxi.nodes[id] = { n = Name, w = Welt, tx, ty = Position auf der Flugkarte, a, b = Weltposition (erster, zweiter Wert von UnitPosition) }
-- db.taxi.routes[id] = { a = Start-ID, b = Ziel-ID, t = Sekunden, c = Preis in Kupfer, n = Anzahl, p = { a1, b1, a2, b2, ... }, upd = Stand }
-- db.taxiChar[Charakter] = { known = { [id] = true }, spent = Kupfer, trips = Anzahl, log = { { t, a, b, c, d }, ... } }
-- Die Flugpunkt-ID kommt aus der Position auf der Flugkarte (sprachunabhaengig), sonst aus dem Namen.
-- Zwischenlandungen: aufgezeichnet wird die Gesamtstrecke bis zum gewaehlten Ziel.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Taxi = { state = {} }
Codex.Taxi = Taxi
NS.Taxi = Taxi
local state = Taxi.state
local SAMPLE = 3 -- Sekunden zwischen zwei Streckenpunkten
local MAX_POINTS = 120 -- Punkte je Route (je 2 Zahlen)
local MAX_ROUTES = 600
local MAX_NODES = 300
local MAX_LOG = 30
local PRICE_WINDOW = 6 -- Sekunden nach dem Start, in denen der abgezogene Betrag nachgemessen wird
local function Clock() return _G.GetTime and _G.GetTime() or 0 end
local function Now() return _G.time and _G.time() or 0 end
local function Round(v) return math.floor((v or 0) + 0.5) end
local function DB()
local db = Codex.db
if not db then return nil end
local t = db.taxi
if type(t) ~= "table" then t = {}; db.taxi = t end
t.nodes = t.nodes or {}
t.routes = t.routes or {}
return t
end
local function CharRec(create)
local db = Codex.db
local key = Codex.Trust and Codex.Trust.SelfKey and Codex.Trust.SelfKey() or nil
if not db or not key then return nil end
db.taxiChar = db.taxiChar or {}
local r = db.taxiChar[key]
if not r and create ~= false then
r = { known = {}, spent = 0, trips = 0, log = {} }
db.taxiChar[key] = r
end
return r
end
function Taxi.Money(c)
c = math.floor(tonumber(c) or 0)
local g, s, k = math.floor(c / 10000), math.floor(c / 100) % 100, c % 100
local out = {}
if g > 0 then out[#out + 1] = g .. "g" end
if s > 0 then out[#out + 1] = s .. "s" end
if k > 0 or #out == 0 then out[#out + 1] = k .. "c" end
return table.concat(out, " ")
end
function Taxi.Duration(sec)
sec = math.floor(tonumber(sec) or 0)
return string.format("%d:%02d", math.floor(sec / 60), sec % 60)
end
------------------------------------------------------------------------
-- Abfragen (fuer Karte und Planer)
------------------------------------------------------------------------
Taxi.rev = 0
local merged
-- Eigene und empfangene Routen zusammen (eigene haben Vorrang)
function Taxi.Routes()
local t = DB()
if not t then return {} end
local rem = Codex.db.taxir
if not rem or next(rem) == nil then return t.routes end
if merged and merged.rev == Taxi.rev then return merged.routes end
local out = {}
for k, r in pairs(rem) do out[k] = r end
for k, r in pairs(t.routes) do out[k] = r end
merged = { rev = Taxi.rev, routes = out }
return out
end
function Taxi.Node(id)
local t = DB()
if not t then return nil end
local n = t.nodes[id]
if n then return n end
local rn = Codex.db.taxin
return rn and rn[id] or nil
end
-- Kennt dieser Charakter den Flugpunkt?
function Taxi.Known(id)
local c = CharRec(false)
return c and c.known and c.known[id] and true or false
end
function Taxi.CharStats()
local c = CharRec(false)
if not c then return nil end
local n = 0
for _ in pairs(c.known or {}) do n = n + 1 end
return { known = n, spent = c.spent or 0, trips = c.trips or 0, secs = c.secs or 0, log = c.log, routes = c.rt }
end
-- Wie oft dieser Charakter die Route geflogen ist und wie lange insgesamt: n, Sekunden
function Taxi.RouteStat(a, b)
local c = CharRec(false)
local r = c and c.rt and c.rt[a .. ">" .. b]
if not r then return 0, 0 end
return r.n or 0, r.sec or 0
end
------------------------------------------------------------------------
-- Flugkarte lesen
------------------------------------------------------------------------
local function NodeId(i, world)
if _G.TaxiNodePosition then
local ok, x, y = pcall(_G.TaxiNodePosition, i)
if ok and type(x) == "number" and type(y) == "number" then
return string.format("%d:%d:%d", world or 0, Round(x * 1000), Round(y * 1000)), x, y
end
end
local name = _G.TaxiNodeName and _G.TaxiNodeName(i)
if type(name) == "string" and name ~= "" then return "n:" .. name, nil, nil end
return nil
end
-- Kartenposition des Flugpunkts (Karte, x, y), solange der Spieler dort steht; fuer den Levelplaner
local function SetMapPos(n)
local loc = FW.Util and FW.Util.GetPlayerLocation and FW.Util.GetPlayerLocation()
if loc and loc.mapID and loc.x and loc.y and loc.x > 0 and loc.y > 0 then
if n.m ~= loc.mapID or not n.x then
n.m, n.x, n.y = loc.mapID, loc.x, loc.y
return true
end
end
return false
end
function Taxi.Scan()
local count = _G.NumTaxiNodes and _G.NumTaxiNodes() or 0
if type(count) ~= "number" or count <= 0 then return end
local T, C = DB(), CharRec(true)
if not T or not C then return end
local P = Codex.Path and Codex.Path.Read and Codex.Path.Read()
local world = P and P.world or 0
local nodes = 0
for _ in pairs(T.nodes) do nodes = nodes + 1 end
state.slot, state.cur = {}, nil
local changed = false
for i = 1, math.min(count, 120) do
local kind = _G.TaxiNodeGetType and _G.TaxiNodeGetType(i)
local name = _G.TaxiNodeName and _G.TaxiNodeName(i)
local id, tx, ty = NodeId(i, world)
if id then
local n = T.nodes[id]
if not n and nodes < MAX_NODES then
n = {}
T.nodes[id] = n
nodes = nodes + 1
end
if n then
if type(name) == "string" and name ~= "" then n.n = name:sub(1, 60) end
n.w, n.tx, n.ty = world, tx, ty
end
if kind ~= "NONE" and not C.known[id] then C.known[id] = true; changed = true end
if kind == "CURRENT" then
state.cur = id
if n and P then n.a, n.b = Round(P.y), Round(P.x) end
if n and SetMapPos(n) then changed = true end
end
local cost = _G.TaxiNodeCost and tonumber(_G.TaxiNodeCost(i)) or nil
state.slot[i] = { id = id, cost = cost and math.floor(cost) or nil, kind = kind }
end
end
if changed then
Taxi.rev = Taxi.rev + 1
FW:Fire("CODEX_TAXI_CHANGED")
end
if Taxi.OnOpen then pcall(Taxi.OnOpen) end
end
------------------------------------------------------------------------
-- Teilen (Share/Sync, Datensatzart "f", je Route ein Datensatz)
------------------------------------------------------------------------
-- Zahl zur Route: Flugpunkt-Zahl (Welt, x, y) * 10^7 + Flugpunkt-Zahl des Ziels. Nur Flugpunkte mit Positions-ID sind teilbar.
local NODE_W = 1002001
local SHARE_POINTS = 40
local function NodeNum(id)
local w, x, y = tostring(id):match("^(%d+):(%d+):(%d+)$")
w, x, y = tonumber(w), tonumber(x), tonumber(y)
if not w or w > 8 or x > 1000 or y > 1000 then return nil end
return w * NODE_W + x * 1001 + y
end
local function NodeFromNum(n)
if type(n) ~= "number" or n < 0 or n >= 9 * NODE_W then return nil end
local w, r = math.floor(n / NODE_W), n % NODE_W
return string.format("%d:%d:%d", w, math.floor(r / 1001), r % 1001)
end
local function RouteNum(a, b)
local na, nb = NodeNum(a), NodeNum(b)
if not na or not nb then return nil end
return na * 10000000 + nb
end
local function RouteIds(num)
local a, b = NodeFromNum(math.floor(num / 10000000)), NodeFromNum(num % 10000000)
if not a or not b then return nil end
return a, b
end
function Taxi.SyncList()
local out = {}
local t = DB()
if not t then return out end
for _, r in pairs(t.routes) do
local num = r.upd and RouteNum(r.a, r.b)
if num then out[#out + 1] = { key = "f" .. string.format("%.0f", num), upd = r.upd } end
end
return out
end
local function NodeShare(id)
local n = Taxi.Node(id) or {}
return { n = n.n, a = n.a, b = n.b, m = n.m, x = n.x, y = n.y }
end
-- Eigene Route als Datensatz (Strecke auf hoechstens SHARE_POINTS Punkte verkleinert)
function Taxi.RouteRecord(num)
local a, b = RouteIds(num)
local t = DB()
local r = a and t and t.routes[a .. ">" .. b]
if not r or not r.upd or type(r.p) ~= "table" then return nil end
local pts, m = {}, math.floor(#r.p / 2)
if m < 2 then return nil end
local take = math.min(m, SHARE_POINTS)
for i = 1, take do
local j = (take == 1) and 1 or (1 + math.floor((i - 1) * (m - 1) / (take - 1) + 0.5))
pts[#pts + 1] = r.p[j * 2 - 1]
pts[#pts + 1] = r.p[j * 2]
end
return { t = r.t, c = r.c, n = r.n or 1, p = pts, A = NodeShare(a), B = NodeShare(b) }, r.upd
end
local function NodeFromRecord(id, rn)
local w, x, y = tostring(id):match("^(%d+):(%d+):(%d+)$")
return { n = rn.n, w = tonumber(w), tx = tonumber(x) / 1000, ty = tonumber(y) / 1000, a = rn.a, b = rn.b, m = rn.m, x = rn.x, y = rn.y }
end
-- Empfangene Route einmischen. Rueckgabe: true, wenn gespeichert
function Taxi.StoreRemote(rec)
local db = Codex.db
if not db or type(rec) ~= "table" then return false end
local a, b = RouteIds(rec.id)
if not a then return false end
db.taxir = db.taxir or {}
db.taxin = db.taxin or {}
local rid = a .. ">" .. b
local old = db.taxir[rid]
if old and (old.upd or 0) >= rec.upd then return false end
if not old then
local count = 0
for _ in pairs(db.taxir) do count = count + 1 end
if count >= MAX_ROUTES then return false end
end
db.taxir[rid] = { a = a, b = b, t = rec.t, c = rec.c, n = rec.n, p = rec.p, upd = rec.upd }
local own = DB()
for id, rn in pairs({ [a] = rec.A, [b] = rec.B }) do
if not (own and own.nodes[id]) and not db.taxin[id] then
local nodes = 0
for _ in pairs(db.taxin) do nodes = nodes + 1 end
if nodes < MAX_NODES then db.taxin[id] = NodeFromRecord(id, rn) end
end
end
Taxi.rev = Taxi.rev + 1
return true
end
-- Levelplaner: Fluege als Kanten { id, k, t, wait, from = { m, x, y }, to = { m, x, y } }, nur zwischen Flugpunkten,
-- die dieser Charakter kennt und deren Kartenposition bekannt ist
function Taxi.PlannerOn()
local db = Codex.db
return not (db and db.settings and db.settings.taxiPlan == false)
end
local edgeCache
function Taxi.Edges()
if not Taxi.PlannerOn() then return {} end
local key = Codex.Trust and Codex.Trust.SelfKey and Codex.Trust.SelfKey() or "?"
if edgeCache and edgeCache.rev == Taxi.rev and edgeCache.key == key then return edgeCache.list end
local list = {}
for rid, r in pairs(Taxi.Routes()) do
local na, nb = Taxi.Node(r.a), Taxi.Node(r.b)
if na and nb and na.m and nb.m and r.t and Taxi.Known(r.a) and Taxi.Known(r.b) then
list[#list + 1] = { id = "f" .. rid, k = "flight", t = r.t, wait = 20, name = nb.n, fa = r.a, fb = r.b,
from = { m = na.m, x = na.x, y = na.y }, to = { m = nb.m, x = nb.x, y = nb.y } }
end
end
edgeCache = { rev = Taxi.rev, key = key, list = list }
return list
end
function Taxi.ClearRemote()
local db = Codex.db
if db then db.taxir, db.taxin = nil, nil end
Taxi.rev = Taxi.rev + 1
end
------------------------------------------------------------------------
-- Flug erfassen
------------------------------------------------------------------------
local function OnTake(slot)
local s = state.slot and state.slot[slot]
if not s or not state.cur then return end
state.pending = { from = state.cur, to = s.id, quote = s.cost or 0, money0 = _G.GetMoney and _G.GetMoney() or 0,
asked = Clock(), started = false }
end
local function Finish(p, now)
state.pending = nil
Taxi.flight = nil
local T, C = DB(), CharRec(true)
if not T or not C then return end
local dur = now - p.t0
if dur < 5 then return end
-- Endpunkt: Position der Landung
local P = Codex.Path and Codex.Path.Read and Codex.Path.Read()
if P then
p.pts[#p.pts + 1] = Round(P.y)
p.pts[#p.pts + 1] = Round(P.x)
local n = T.nodes[p.to]
if n and not n.a then n.a, n.b = Round(P.y), Round(P.x) end
if n and not n.m then SetMapPos(n) end
end
local paid = p.paid or 0
if paid <= 0 or paid > p.quote * 3 + 100 then paid = p.quote or 0 end
local rid = p.from .. ">" .. p.to
local r = T.routes[rid]
local isNew = false
if not r then
local count = 0
for _ in pairs(T.routes) do count = count + 1 end
if count >= MAX_ROUTES then return end
r = { a = p.from, b = p.to, t = Round(dur), c = paid, n = 1, p = p.pts, upd = Now() }
T.routes[rid] = r
isNew = true
else
r.n = (r.n or 1) + 1
r.t = Round((r.t * (r.n - 1) + dur) / r.n)
r.c = paid
if #p.pts > #(r.p or {}) then r.p = p.pts end
r.upd = Now()
end
C.known[p.to] = true
C.spent = (C.spent or 0) + paid
C.trips = (C.trips or 0) + 1
C.secs = (C.secs or 0) + Round(dur)
C.rt = C.rt or {}
local rs = C.rt[rid]
if not rs then rs = { n = 0, sec = 0, gold = 0 }; C.rt[rid] = rs end
rs.n, rs.sec, rs.gold = rs.n + 1, rs.sec + Round(dur), (rs.gold or 0) + paid
C.log = C.log or {}
C.log[#C.log + 1] = { t = Now(), a = p.from, b = p.to, c = paid, d = Round(dur) }
while #C.log > MAX_LOG do table.remove(C.log, 1) end
if isNew then
local na, nb = T.nodes[p.from], T.nodes[p.to]
FW:Print(string.format("Codex: Flugroute %s -> %s gespeichert (%s, %s, %d Punkte)",
na and na.n or p.from, nb and nb.n or p.to, Taxi.Duration(dur), Taxi.Money(paid), math.floor(#p.pts / 2)))
end
Taxi.rev = Taxi.rev + 1
FW:Fire("CODEX_TAXI_CHANGED")
FW:Fire("CODEX_CHANGED", "taxi")
end
local function Tick()
local p = state.pending
if not p then return end
local now = Clock()
local on = _G.UnitOnTaxi and _G.UnitOnTaxi("player") and true or false
if on and not p.started then
p.started, p.t0, p.nextAt, p.pts = true, now, now, {}
local r = Taxi.Routes()[p.from .. ">" .. p.to]
local nd = Taxi.Node(p.to)
Taxi.flight = { t0 = now, exp = r and r.t or nil, name = nd and nd.n or "?" }
end
if p.started then
if on then
if now - p.t0 <= PRICE_WINDOW then
local d = p.money0 - (_G.GetMoney and _G.GetMoney() or 0)
if d > (p.paid or 0) then p.paid = d end
end
if now >= p.nextAt and #p.pts < MAX_POINTS * 2 then
local P = Codex.Path and Codex.Path.Read and Codex.Path.Read()
if P then
p.pts[#p.pts + 1] = Round(P.y)
p.pts[#p.pts + 1] = Round(P.x)
end
p.nextAt = now + SAMPLE
end
else
Finish(p, now)
end
elseif now - p.asked > 15 then
state.pending = nil -- Flug nicht angetreten
Taxi.flight = nil
end
end
local acc = 0
local frame = _G.CreateFrame and _G.CreateFrame("Frame")
if frame then
frame:SetScript("OnUpdate", function(_, elapsed)
acc = acc + (elapsed or 0)
if acc < 1 then return end
acc = 0
pcall(Tick)
end)
end
-- Tooltip am Flugziel auf der Flugkarte: Reisedauer (und wie oft schon geflogen)
function Taxi.ButtonTip(button)
if not (button and button.GetID and state.cur and state.slot) then return end
local s = state.slot[button:GetID()]
local tt = _G.GameTooltip
if not s or not tt or s.id == state.cur then return end
local r = Taxi.Routes()[state.cur .. ">" .. s.id]
local L = FW.L
if r and r.t then
tt:AddLine(string.format(L.CODEX_TAXI_TIP_TIME, Taxi.Duration(r.t)), 0.94, 0.75, 0.36)
else
tt:AddLine(L.CODEX_TAXI_TIP_NONE, 0.6, 0.59, 0.55)
end
local n, sec = Taxi.RouteStat(state.cur, s.id)
if n > 0 then
tt:AddLine(string.format(L.CODEX_TAXI_TIP_COUNT, n, Taxi.Duration(sec)), 0.6, 0.59, 0.55)
end
tt:Show()
end
local function Install()
if Taxi.hooked then return end
Taxi.hooked = true
if _G.hooksecurefunc and _G.TaxiNodeOnButtonEnter then
pcall(_G.hooksecurefunc, "TaxiNodeOnButtonEnter", function(button) pcall(Taxi.ButtonTip, button) end)
end
if _G.hooksecurefunc and _G.TakeTaxiNode then
pcall(_G.hooksecurefunc, "TakeTaxiNode", function(slot) pcall(OnTake, slot) end)
end
FW:RegisterEvent("TAXIMAP_OPENED", function() pcall(Taxi.Scan) end, Taxi)
end
Install()
------------------------------------------------------------------------
-- /fwtaxi
------------------------------------------------------------------------
SLASH_FREEWOODTAXI1 = "/fwtaxi"
SlashCmdList["FREEWOODTAXI"] = function(arg)
local T = DB()
if not T then return end
local nodes, routes = 0, 0
for _ in pairs(T.nodes) do nodes = nodes + 1 end
for _ in pairs(T.routes) do routes = routes + 1 end
if arg == "diag" then
FW:Print(string.format("Taxi: TakeTaxiNode=%s TaxiNodePosition=%s TaxiNodeCost=%s TaxiNodeGetType=%s UnitOnTaxi=%s Hook=%s",
tostring(_G.TakeTaxiNode ~= nil), tostring(_G.TaxiNodePosition ~= nil), tostring(_G.TaxiNodeCost ~= nil),
tostring(_G.TaxiNodeGetType ~= nil), tostring(_G.UnitOnTaxi ~= nil), tostring(Taxi.hooked)))
FW:Print(string.format("Taxi: aktueller Punkt=%s, Plaetze=%d, Flug laeuft=%s", tostring(state.cur), state.slot and #state.slot or 0,
tostring(state.pending and state.pending.started or false)))
return
end
local c = Taxi.CharStats()
FW:Print(string.format("Taxi: %d Flugpunkte, %d Routen gespeichert.", nodes, routes))
if c then
FW:Print(string.format("Taxi: dieser Charakter kennt %d Flugpunkte, %d Fluege, %s ausgegeben, %s in der Luft.", c.known, c.trips, Taxi.Money(c.spent), Taxi.Duration(c.secs)))
end
local list = {}
for _, r in pairs(T.routes) do list[#list + 1] = r end
table.sort(list, function(x, y) return (x.upd or 0) > (y.upd or 0) end)
for i = 1, math.min(#list, 5) do
local r = list[i]
local na, nb = T.nodes[r.a], T.nodes[r.b]
FW:Print(string.format(" %s -> %s: %s, %s, %d Punkte, %dx", na and na.n or r.a, nb and nb.n or r.b, Taxi.Duration(r.t), Taxi.Money(r.c),
math.floor(#(r.p or {}) / 2), r.n or 1))
end
end
+149
View File
@@ -0,0 +1,149 @@
-- Freewood-OctoWoW-Codex: Flugfenster
--
-- Waehrend eines Fluges mit dem Flugmeister: kleines Fenster mit Zielname, verbleibender Zeit und einem ablaufenden Balken.
-- Ist die Flugdauer der Route noch unbekannt (noch nie geflogen oder empfangen), zaehlt es die vergangene Zeit hoch.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
local Taxi = NS and NS.Taxi
if not FW or not Codex or not Taxi or not FW.UI then
return
end
local UI = FW.UI
local WIDTH, HEIGHT, BAR_H = 250, 50, 8
local win
local function Settings()
local db = Codex.db
if not db then return {} end
db.settings = db.settings or {}
return db.settings
end
function Taxi.WindowOn()
return Settings().taxiWindow ~= false
end
local function SavePos()
if not win then return end
local point, _, rel, x, y = win:GetPoint(1)
if point then Settings().taxiWinPos = { point = point, rel = rel, x = x, y = y } end
end
local function ApplyPos()
win:ClearAllPoints()
local p = Settings().taxiWinPos
if type(p) == "table" and p.point and p.x and p.y then
win:SetPoint(p.point, UIParent, p.rel or p.point, p.x, p.y)
else
win:SetPoint("TOP", UIParent, "TOP", 0, -150)
end
end
local function Build()
local f = UI.CreatePanel(UIParent, "bg", "gold")
f:SetSize(WIDTH, HEIGHT)
f:SetFrameStrata("MEDIUM")
f:SetClampedToScreen(true)
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function(self) self:StartMoving() end)
f:SetScript("OnDragStop", function(self) self:StopMovingOrSizing(); SavePos() end)
win = f
ApplyPos()
f.title = UI.CreateLabel(f, "", { size = 12, color = "goldLight" })
f.title:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -7)
f.title:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -7)
f.title:SetHeight(14)
f.time = UI.CreateLabel(f, "", { size = 11, color = "text" })
f.time:SetPoint("TOPLEFT", f.title, "BOTTOMLEFT", 0, -2)
f.time:SetPoint("TOPRIGHT", f.title, "BOTTOMRIGHT", 0, -2)
f.time:SetHeight(13)
f.barBg = f:CreateTexture(nil, "ARTWORK")
f.barBg:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 10, 8)
f.barBg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 8)
f.barBg:SetHeight(BAR_H)
f.barBg:SetColorTexture(UI.Color("card"))
f.fill = f:CreateTexture(nil, "OVERLAY")
f.fill:SetPoint("BOTTOMLEFT", f.barBg, "BOTTOMLEFT", 0, 0)
f.fill:SetHeight(BAR_H)
f.fill:SetColorTexture(UI.Color("gold"))
f.acc = 0
f:SetScript("OnUpdate", function(self, elapsed)
self.acc = self.acc + (elapsed or 0)
if self.acc < 0.1 then return end
self.acc = 0
local fl = Taxi.flight
if not fl or not Taxi.WindowOn() then self:Hide() return end
local L = FW.L
local el = (GetTime and GetTime() or 0) - fl.t0
if self.shownName ~= fl.name then
self.shownName = fl.name
self.title:SetText(string.format(L.CODEX_TAXI_TITLE, fl.name))
end
local full = self.barBg:GetWidth() or (WIDTH - 20)
if fl.exp and fl.exp > 0 then
local left = fl.exp - el
if left > 0 then
self.time:SetText(string.format(L.CODEX_TAXI_LEFT, Taxi.Duration(math.ceil(left))))
self.fill:SetColorTexture(UI.Color("gold"))
self.fill:SetWidth(math.max(1, full * left / fl.exp))
else
self.time:SetText(L.CODEX_TAXI_ARRIVING)
self.fill:SetColorTexture(UI.Color("success"))
self.fill:SetWidth(1)
end
else
self.time:SetText(string.format(L.CODEX_TAXI_UNKNOWN, Taxi.Duration(el)))
self.fill:SetColorTexture(UI.Color("bronze"))
self.fill:SetWidth(math.max(1, full * ((el % 4) / 4)))
end
end)
f:Hide()
return f
end
-- Das Fenster zeigt sich, sobald ein Flug laeuft
local watch = CreateFrame("Frame")
local acc = 0
watch:SetScript("OnUpdate", function(_, elapsed)
acc = acc + (elapsed or 0)
if acc < 0.25 then return end
acc = 0
if Taxi.flight and Taxi.WindowOn() then
if not win then win = Build() end
if not win:IsShown() then win.shownName = nil; win:Show() end
end
end)
if UI.RegisterSettingsSection then
UI:RegisterSettingsSection("codex-taxi", {
order = 108,
height = 140,
build = function(parent)
local L = FW.L
local f, stack = UI.CreateSection(parent, L.CODEX_TAXI_HEADER)
local note = UI.CreateLabel(f, L.CODEX_TAXI_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
stack:Add(note, { fit = true, gap = 8 })
local cb = UI.CreateCheckbox(f, L.CODEX_TAXI_SWITCH, function() return Taxi.WindowOn() end,
function(on) Settings().taxiWindow = on and true or false end)
stack:Add(cb, { fit = true, gap = 6 })
local cp = UI.CreateCheckbox(f, L.CODEX_TAXI_PLAN, function() return Taxi.PlannerOn() end,
function(on) Settings().taxiPlan = on and true or false end)
stack:Add(cp, { fit = true, gap = 6 })
function f.Refresh() cb:Refresh(); cp:Refresh(); stack:Layout() end
stack:Layout()
return f
end,
refresh = function(frame) frame.Refresh() end,
})
end
+496
View File
@@ -0,0 +1,496 @@
-- Freewood-OctoWoW-Codex: Zeitmessung
-- Kampfdauer (Time to kill): wie lange dauert ein Solo-Kampf gegen einen einzelnen Gegner,
-- je Klasse, Spielerstufe und Stufenabstand (Gegner minus Spieler, -5 bis +5).
-- Quest-Zeit: aktive Spielzeit vom ersten Fortschritt bis zur Abgabebereitschaft, bei mehreren
-- gleichzeitig laufenden Quests gleichmaessig aufgeteilt.
-- Zeiten werden als ganze Zehntelsekunden gespeichert.
--
-- Gespeichert:
-- db.ttk[Klasse][Spielerstufe][Abstand] = { n, sum, min, max } (Klasse zum Beispiel "WARRIOR")
-- units[npcID].tt[Klasse][Spielerstufe] = { n, sum, min, max } (nur lokal)
-- quests[id].tm aufgeteilte aktive Zeit bis fertig, .tr aktive Zeit bis fertig ohne Aufteilen,
-- quests[id].ta aktive Zeit bis zur Abgabe
-- db.qtimer[questID] = laufende Zeitnehmer (ueberleben Neustart des Spiels)
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Util = FW.Util
local Time = {}
NS.Time = Time
Codex.Time = Time
local MAX_DELTA = 5
local MIN_KILL, MAX_KILL = 0.5, 120 -- Sekunden
local KILL_END_WINDOW = 8 -- Kampf muss kurz nach dem Kill enden
local MAX_QUEST_SECONDS = 4 * 3600
local TICK = 1
Time.stats = { fights = 0, ok = 0, skip = {}, quests = 0 }
local function Now()
return GetTime and GetTime() or time()
end
local function Safe(fn, ...)
local ok, err = pcall(fn, ...)
if not ok and FW.Debug then
FW:Debug("Codex.Time: " .. tostring(err))
end
return ok
end
local function Skip(reason)
Time.stats.skip[reason] = (Time.stats.skip[reason] or 0) + 1
end
local function Sample(tbl, key, value)
local s = tbl[key]
if not s then
s = { n = 0, sum = 0 }
tbl[key] = s
end
s.n = s.n + 1
s.sum = s.sum + value
s.min = s.min and math.min(s.min, value) or value
s.max = s.max and math.max(s.max, value) or value
return s
end
function Time.PlayerToken()
local token
if UnitClass then
local _, t = UnitClass("player")
token = t
end
if type(token) == "string" and token:match("^%u+$") then
return token
end
return nil
end
-- Klassenname des Spielers in der Spielsprache (fuer Anzeigen)
function Time.ClassName()
if UnitClass then
local name = UnitClass("player")
if type(name) == "string" and name ~= "" then
return name
end
end
return Time.PlayerToken() or "?"
end
------------------------------------------------------------------------
-- Kampfdauer
------------------------------------------------------------------------
local fight = nil
local function InGroup()
return (IsInGroup and IsInGroup()) or (GetNumGroupMembers and GetNumGroupMembers() > 1) or false
end
local function InInstance()
if IsInInstance then
local inside = IsInInstance()
return inside and true or false
end
return false
end
function Time:NoteTarget()
if not fight or not fight.ok then
return
end
if not (UnitExists and UnitExists("target")) then
return
end
if UnitCanAttack and not UnitCanAttack("player", "target") then
return
end
local guid = UnitGUID and UnitGUID("target")
if guid == nil or Util.IsSecret(guid) then
fight.ok, fight.why = false, "secret"
return
end
if not fight.targets[guid] then
fight.targets[guid] = true
fight.nTargets = fight.nTargets + 1
if fight.nTargets > 1 then
fight.ok, fight.why = false, "multi"
return
end
local classification = UnitClassification and UnitClassification("target")
if classification and classification ~= "normal" then
fight.ok, fight.why = false, "elite"
end
local level = UnitLevel and tonumber(UnitLevel("target"))
if level and level > 0 then
fight.tl = level -- Stufe des Gegners, falls der Erfahrungs-Hook sie nicht kennt
end
end
end
function Time:OnRegenDisabled()
Safe(function()
fight = { start = Now(), targets = {}, nTargets = 0, kills = 0, ok = true }
if InGroup() then
fight.ok, fight.why = false, "group"
elseif InInstance() then
fight.ok, fight.why = false, "instance"
end
Time:NoteTarget()
end)
end
function Time:OnTargetChanged()
Safe(Time.NoteTarget, Time)
end
-- vom Erfahrungs-Hook aufgerufen: ein Gegner ist gestorben (pl, ml: Stufen, id: NPC-ID oder nil)
function Time:OnKill(pl, ml, id, classification)
if not fight then
return
end
fight.kills = fight.kills + 1
fight.killAt = Now()
fight.pl, fight.ml, fight.id = pl, ml or fight.ml, id or fight.id
if classification and classification ~= "normal" and fight.ok then
fight.ok, fight.why = false, "elite"
end
end
function Time:Record(token, pl, delta, seconds, id)
local ds = math.floor(seconds * 10 + 0.5)
local db = Codex.db
db.ttk = db.ttk or {}
db.ttk[token] = db.ttk[token] or {}
db.ttk[token][pl] = db.ttk[token][pl] or {}
Sample(db.ttk[token][pl], delta, ds)
if id then
local u = Codex:Unit(id, true)
u.tt = u.tt or {}
u.tt[token] = u.tt[token] or {}
Sample(u.tt[token], pl, ds)
end
FW:Fire("CODEX_CHANGED", "ttk", pl)
end
function Time:OnRegenEnabled()
Safe(function()
local f = fight
fight = nil
if not f then
return
end
local st = Time.stats
st.fights = st.fights + 1
if not f.ok then
return Skip(f.why or "other")
end
if f.kills ~= 1 then
return Skip(f.kills == 0 and "nokill" or "kills")
end
local seconds = f.killAt - f.start
if seconds < MIN_KILL then
return Skip("short")
end
if seconds > MAX_KILL then
return Skip("long")
end
if Now() - f.killAt > KILL_END_WINDOW then
return Skip("late")
end
local pl = f.pl or (UnitLevel and tonumber(UnitLevel("player")))
local ml = f.ml or f.tl
local delta = ml and pl and (ml - pl)
if not delta or math.abs(delta) > MAX_DELTA then
return Skip("range")
end
local token = Time.PlayerToken()
if not token then
return Skip("class")
end
st.ok = st.ok + 1
Time:Record(token, pl, delta, seconds, f.id)
end)
end
-- Klassenname zu einem Klassenschluessel ("WARRIOR" -> "Krieger"), sonst der Schluessel selbst
function Time.ClassLabel(token)
local names = _G.LOCALIZED_CLASS_NAMES_MALE
if type(names) == "table" and type(names[token]) == "string" then
return names[token]
end
return tostring(token)
end
-- Kampfdauer einer Kreatur je Klasse (eigene und geteilte Messungen):
-- Liste { cls, secs, n, own } nach Dauer sortiert, Durchschnitt ueber alle Spielerstufen
function Time.UnitClasses(u)
local list = {}
local mine = Time.PlayerToken()
for cls, byPl in pairs(u and u.tt or {}) do
local n, sum = 0, 0
for _, s in pairs(byPl) do
n = n + (s.n or 0)
sum = sum + (s.sum or 0)
end
if n > 0 then
list[#list + 1] = { cls = cls, secs = sum / n / 10, n = n, own = cls == mine }
end
end
table.sort(list, function(a, b)
if a.secs ~= b.secs then return a.secs < b.secs end
return a.cls < b.cls
end)
return list
end
-- Durchschnittliche Kampfdauer (Sekunden, Anzahl) einer Kreatur fuer die eigene Klasse
function Time.UnitAverage(u)
local token = Time.PlayerToken()
local byPl = u and u.tt and token and u.tt[token]
if not byPl then
return nil
end
local n, sum = 0, 0
for _, s in pairs(byPl) do
n = n + (s.n or 0)
sum = sum + (s.sum or 0)
end
if n < 1 then
return nil
end
return sum / n / 10, n
end
------------------------------------------------------------------------
-- Quest-Zeit
------------------------------------------------------------------------
local lastTick = nil
local ticker = nil
local function QTimers()
Codex.db.qtimer = Codex.db.qtimer or {}
return Codex.db.qtimer
end
local function LogEntries()
local list = {}
local count
if C_QuestLog and C_QuestLog.GetNumQuestLogEntries then
count = C_QuestLog.GetNumQuestLogEntries()
elseif GetNumQuestLogEntries then
count = GetNumQuestLogEntries()
end
for i = 1, tonumber(count) or 0 do
local questID, isHeader
if C_QuestLog and C_QuestLog.GetInfo then
local info = C_QuestLog.GetInfo(i)
if type(info) == "table" then
questID, isHeader = info.questID, info.isHeader
end
elseif GetQuestLogTitle then
local _, _, _, h, _, _, _, id = GetQuestLogTitle(i)
questID, isHeader = id, h
end
if type(questID) == "number" and questID > 0 and not isHeader then
list[#list + 1] = questID
end
end
return list
end
-- Fortschritt (Summe der erfuellten Ziele) und ob alles erledigt ist
local function Progress(questID)
local have, finished, count = 0, true, 0
if C_QuestLog and C_QuestLog.GetQuestObjectives then
local objs = C_QuestLog.GetQuestObjectives(questID)
if type(objs) == "table" then
for _, o in ipairs(objs) do
if type(o) == "table" then
count = count + 1
have = have + (tonumber(o.numFulfilled) or 0)
if not o.finished then finished = false end
end
end
end
end
if count == 0 then
finished = false
end
if not finished and C_QuestLog and C_QuestLog.IsComplete then
local ok, done = pcall(C_QuestLog.IsComplete, questID)
if ok and done and count > 0 then finished = true end
end
return have, finished
end
function Time:ScanQuests()
Safe(function()
local timers = QTimers()
local seen = {}
for _, questID in ipairs(LogEntries()) do
seen[questID] = true
local have, finished = Progress(questID)
local t = timers[questID]
if not t then
-- neu im Log: nur ohne vorhandenen Fortschritt kennen wir den Beginn
t = { eff = 0, raw = 0, all = 0, have = have, started = false, skip = have > 0 or finished }
timers[questID] = t
end
if not t.skip then
if have > (t.have or 0) and not t.started then
t.started = true
end
if finished and t.started and not t.done then
t.done = true
end
end
t.have = have
end
for questID, t in pairs(timers) do
if not seen[questID] then
if t.done then
-- fertige Quests kurz behalten: die Abgabe kann nach dem Verlassen des Logs eintreffen
t.goneAt = t.goneAt or Now()
if Now() - t.goneAt > 60 then timers[questID] = nil end
else
timers[questID] = nil
end
end
end
end)
end
function Time:Tick()
Safe(function()
local now = Now()
local dt = lastTick and (now - lastTick) or 0
lastTick = now
if dt <= 0 or dt > 3 then
return
end
if UnitIsAFK and UnitIsAFK("player") then
return
end
local timers = QTimers()
local active = 0
for _, t in pairs(timers) do
if t.started and not t.done and not t.skip then
active = active + 1
end
end
for _, t in pairs(timers) do
if t.started and not t.skip then
t.all = (t.all or 0) + dt
if not t.done then
t.eff = t.eff + dt / math.max(1, active)
t.raw = t.raw + dt
end
end
end
end)
end
-- Quest abgegeben: Zeiten in die Quest schreiben
function Time:OnTurnedIn(questID)
Safe(function()
questID = tonumber(questID)
local t = questID and QTimers()[questID]
if not t or t.skip or not t.started then
if questID then QTimers()[questID] = nil end
return
end
QTimers()[questID] = nil
if not t.done or t.raw < 1 or t.all > MAX_QUEST_SECONDS then
return
end
local q = Codex:Quest(questID, true)
local function Add(field, seconds)
local ds = math.floor(seconds * 10 + 0.5)
q[field] = q[field] or { n = 0, sum = 0 }
local s = q[field]
s.n = s.n + 1
s.sum = s.sum + ds
s.min = s.min and math.min(s.min, ds) or ds
s.max = s.max and math.max(s.max, ds) or ds
end
Add("tm", t.eff)
Add("tr", t.raw)
Add("ta", t.all)
Time.stats.quests = Time.stats.quests + 1
FW:Fire("CODEX_CHANGED", "quest", questID)
end)
end
function Time:OnQuestLog()
if self.scanPending then
return
end
self.scanPending = true
C_Timer.After(0.5, function()
Time.scanPending = nil
Time:ScanQuests()
end)
end
------------------------------------------------------------------------
-- Start / Stop
------------------------------------------------------------------------
function Time:Start()
FW:RegisterEvent("PLAYER_REGEN_DISABLED", self.OnRegenDisabled, self)
FW:RegisterEvent("PLAYER_REGEN_ENABLED", self.OnRegenEnabled, self)
FW:RegisterEvent("PLAYER_TARGET_CHANGED", self.OnTargetChanged, self)
FW:RegisterEvent("QUEST_LOG_UPDATE", self.OnQuestLog, self)
FW:RegisterEvent("UNIT_QUEST_LOG_CHANGED", self.OnQuestLog, self)
FW:RegisterEvent("QUEST_ACCEPTED", self.OnQuestLog, self)
FW:RegisterEvent("QUEST_TURNED_IN", self.OnTurnedIn, self)
if not ticker and C_Timer and C_Timer.NewTicker then
lastTick = Now()
ticker = C_Timer.NewTicker(TICK, function() Time:Tick() end)
end
C_Timer.After(4, function() Time:ScanQuests() end)
end
function Time:Stop()
for _, event in ipairs({ "PLAYER_REGEN_DISABLED", "PLAYER_REGEN_ENABLED", "PLAYER_TARGET_CHANGED",
"QUEST_LOG_UPDATE", "UNIT_QUEST_LOG_CHANGED", "QUEST_ACCEPTED", "QUEST_TURNED_IN" }) do
FW:UnregisterEvent(event, self)
end
if ticker and ticker.Cancel then
ticker:Cancel()
end
ticker = nil
fight = nil
end
function Time:Debug()
local st = self.stats
local n = 0
for _, byPl in pairs(Codex.db.ttk or {}) do
for _, byDelta in pairs(byPl) do
for _ in pairs(byDelta) do n = n + 1 end
end
end
local reasons = {}
for k, v in pairs(st.skip) do reasons[#reasons + 1] = k .. "=" .. v end
table.sort(reasons)
local active, running = 0, 0
for _, t in pairs(QTimers()) do
running = running + 1
if t.started and not t.done and not t.skip then active = active + 1 end
end
FW:Print(string.format("Zeit: Kaempfe %d, gewertet %d, Messreihen %d, uebersprungen: %s", st.fights, st.ok, n,
#reasons > 0 and table.concat(reasons, " ") or "-"))
FW:Print(string.format("Quest-Zeit: Zeitnehmer %d, davon aktiv %d, gespeicherte Abgaben %d", running, active, st.quests))
end
+230
View File
@@ -0,0 +1,230 @@
-- Freewood-OctoWoW-Codex: Verkehrsverbindungen (Schiff, Zeppelin, U-Bahn, Portal)
--
-- Berechtigte (Modul Freewood-OctoWoW-Dev, Rolle Bearbeiter oder hoeher) erfassen Verbindungen: Start hier, Fahrt, Ziel hier,
-- die Fahrzeit wird gemessen. Gespeichert wird in db.transport[id] und ueber das normale Teilen als Datensatz "g<ID>"
-- verschickt; angenommen werden sie nur von Absendern, die selbst berechtigt sind.
--
-- db.transport[id] = { k = Art, f = "A" | "H" | nil, n = Name, t = Sekunden, a = { m, x, y }, b = { m, x, y },
-- two = Rueckfahrt moeglich, del = geloescht, upd = Stand }
-- Die ID ist ein Pruefwert aus Art, Fraktion und beiden Orten: derselbe Weg, von zwei Berechtigten erfasst, ergibt denselben Datensatz.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Transport = { draft = {} }
Codex.Transport = Transport
NS.Transport = Transport
Transport.KINDS = { "boat", "zeppelin", "subway", "portal" }
local KIND_OK = {}
for _, k in ipairs(Transport.KINDS) do KIND_OK[k] = true end
local function Now() return _G.time and _G.time() or 0 end
local function DB()
local db = Codex.db
if not db then return nil end
db.transport = db.transport or {}
return db
end
local function CanEdit()
return Codex.Trust ~= nil and Codex.Trust.CanEdit()
end
local function Round(v, f) return math.floor(v * f + 0.5) end
local function Hash(text)
local h = 5381
for i = 1, #text do h = (h * 33 + text:byte(i)) % 99999989 end
return h
end
-- Pruefwert (0..99999988) aus Art, Fraktion und den auf etwa 0,5 % gerundeten Orten
function Transport.Id(rec)
local a, b = rec.a or {}, rec.b or {}
return Hash(table.concat({ tostring(rec.k), tostring(rec.f or "-"), tostring(a.m), Round(a.x or 0, 200), Round(a.y or 0, 200),
tostring(b.m), Round(b.x or 0, 200), Round(b.y or 0, 200) }, "|"))
end
-- Aktueller Ort { m, x, y } oder nil
function Transport.Here()
local loc = FW.Util and FW.Util.GetPlayerLocation and FW.Util.GetPlayerLocation()
if loc and loc.mapID and loc.x and loc.y then
return { m = loc.mapID, x = math.floor(loc.x * 10000 + 0.5) / 10000, y = math.floor(loc.y * 10000 + 0.5) / 10000 }
end
return nil
end
local function Clock() return _G.GetTime and _G.GetTime() or 0 end
------------------------------------------------------------------------
-- Erfassen (Entwurf)
------------------------------------------------------------------------
function Transport.Draft()
return Transport.draft
end
function Transport.Reset()
local d = Transport.draft
d.a, d.b, d.t0, d.t = nil, nil, nil, nil
FW:Fire("CODEX_TRANSPORT_DRAFT")
end
function Transport.Start()
local here = Transport.Here()
if not here then return false end
local d = Transport.draft
d.a, d.b, d.t = here, nil, nil
d.t0 = Clock()
FW:Fire("CODEX_TRANSPORT_DRAFT")
return true
end
function Transport.Finish()
local d = Transport.draft
if not d.a or not d.t0 then return false end
local here = Transport.Here()
if not here then return false end
d.b = here
d.t = math.max(1, math.floor(Clock() - d.t0 + 0.5))
FW:Fire("CODEX_TRANSPORT_DRAFT")
return true
end
-- Laeuft gerade eine Aufnahme (Start gesetzt, Ziel noch nicht)? Dann zeichnet das Wegenetz nicht mit.
function Transport.Recording()
local d = Transport.draft
return d.a ~= nil and d.b == nil and d.t0 ~= nil
end
function Transport.SetDraft(key, value)
Transport.draft[key] = value
end
-- Entwurf pruefen. Rueckgabe: true oder false, Grund
function Transport.CheckDraft()
local d = Transport.draft
if not d.a then return false, "start" end
if not d.b or not d.t then return false, "finish" end
if not KIND_OK[d.kind or "boat"] then return false, "kind" end
if d.a.m == d.b.m and math.abs(d.a.x - d.b.x) < 0.005 and math.abs(d.a.y - d.b.y) < 0.005 then return false, "same" end
return true
end
------------------------------------------------------------------------
-- Speichern und Loeschen
------------------------------------------------------------------------
local function Touch(id)
if Codex.Share and Codex.Share.InvalidateScan then Codex.Share.InvalidateScan() end
Transport.rev = (Transport.rev or 0) + 1
FW:Fire("CODEX_TRANSPORT_CHANGED", id)
FW:Fire("CODEX_CHANGED", "transport", id)
end
local function Log(id, field, old, new)
if Codex.DevLog then
pcall(Codex.DevLog, { who = Codex.Trust and Codex.Trust.SelfKey() or "?", kind = "g", id = id, field = field, old = old, new = new })
end
end
local function Describe(rec)
local M = Codex.MapName
return string.format("%s: %s -> %s (%ds)", tostring(rec.k), M and M(rec.a.m) or rec.a.m, M and M(rec.b.m) or rec.b.m, rec.t or 0)
end
-- Speichert den Entwurf. Rueckgabe: id oder nil, Grund
function Transport.Save()
if not CanEdit() then return nil, "rights" end
local ok, why = Transport.CheckDraft()
if not ok then return nil, why end
local d = Transport.draft
local rec = {
k = d.kind or "boat", f = (d.faction == "A" or d.faction == "H") and d.faction or nil,
n = type(d.name) == "string" and d.name:sub(1, 40) ~= "" and d.name:sub(1, 40) or nil,
t = d.t, a = { m = d.a.m, x = d.a.x, y = d.a.y }, b = { m = d.b.m, x = d.b.x, y = d.b.y },
two = d.two ~= false, upd = Now(),
}
local db = DB()
local id = Transport.Id(rec)
local old = db.transport[id]
rec.upd = math.max(rec.upd, (old and old.upd or 0) + 1)
db.transport[id] = rec
Log(id, "add", old and not old.del and Describe(old) or nil, Describe(rec))
Touch(id)
Transport.Reset()
return id
end
function Transport.Delete(id)
if not CanEdit() then return false end
local db = DB()
local own = db.transport[id]
local rec = own
if not rec then
for _, r in ipairs(Transport.All(true)) do
if r.id == id then rec = r break end
end
end
if not rec then return false end
local del = { k = rec.k, f = rec.f, n = rec.n, t = rec.t, a = rec.a, b = rec.b, two = rec.two, del = true,
upd = math.max(Now(), (rec.upd or 0) + 1) }
db.transport[id] = del
Log(id, "del", Describe(rec), nil)
Touch(id)
return true
end
------------------------------------------------------------------------
-- Lesen: eigene und empfangene Verbindungen
------------------------------------------------------------------------
-- Liste { id, k, f, n, t, a, b, two, upd }, je ID der neueste Stand; geloeschte nur mit withDeleted
function Transport.All(withDeleted)
local best = {}
local db = DB()
if not db then return {} end
for id, rec in pairs(db.transport) do
if type(rec) == "table" and rec.a and rec.b then best[id] = rec end
end
if Codex.Share and Codex.Share.RemoteRecords then
for _, rec in ipairs(Codex.Share.RemoteRecords("g")) do
local cur = best[rec.id]
if not cur or (rec.upd or 0) > (cur.upd or 0) then best[rec.id] = rec end
end
end
local out = {}
for id, rec in pairs(best) do
if withDeleted or not rec.del then
out[#out + 1] = { id = id, k = rec.k, f = rec.f, n = rec.n, t = rec.t, a = rec.a, b = rec.b, two = rec.two ~= false, upd = rec.upd, del = rec.del }
end
end
table.sort(out, function(x, y)
if x.k ~= y.k then return x.k < y.k end
return x.id < y.id
end)
return out
end
-- Fahrten als gerichtete Kanten { id, k, t, from = { m, x, y }, to = { m, x, y } }; faction = "A" | "H" | nil (alle)
function Transport.Edges(faction)
local out = {}
for _, r in ipairs(Transport.All()) do
if not (faction and r.f and r.f ~= faction) then
out[#out + 1] = { id = r.id, k = r.k, t = r.t, from = r.a, to = r.b, name = r.n }
if r.two then out[#out + 1] = { id = r.id, k = r.k, t = r.t, from = r.b, to = r.a, name = r.n } end
end
end
return out
end
function Transport.Count()
return #Transport.All()
end
+308
View File
@@ -0,0 +1,308 @@
-- Freewood-OctoWoW-Codex: Berechtigungen (wer darf Daten fuer alle bearbeiten?)
--
-- Rollen:
-- owner steht in CodexOwners.lua, darf die Liste veroeffentlichen
-- moderator darf Namen aendern, Datensaetze als fehlerhaft markieren und fremde Markierungen aufheben
-- editor darf Namen aendern und Datensaetze als fehlerhaft markieren
-- blocked gesperrt: nichts wird mehr von diesem Charakter uebernommen
--
-- Die Liste (Charakter -> Rolle) kommt von einem Eigentuemer ueber den Kanal (Nachricht "T"). Der Absender einer
-- Addon-Nachricht wird vom Spiel gesetzt; Listen anderer Absender werden ignoriert.
-- Battle.net-Namen und Notizen bleiben beim Eigentuemer (Modul Freewood-OctoWoW-Rechte) und werden nie verschickt.
-- Bearbeiten (Namen, Markierungen) geht nur, wenn zusaetzlich das Modul Freewood-OctoWoW-Dev vorhanden ist.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Trust = { buffers = {} }
NS.Trust = Trust
Codex.Trust = Trust
local MAX_ENTRIES = 150
local ROLE_CODE = { editor = "e", moderator = "m", blocked = "b" }
local CODE_ROLE = { e = "editor", m = "moderator", b = "blocked" }
Trust.ROLES = { "editor", "moderator", "blocked" }
local ACTIONS = {
editor = { name = true, bad = true, export = true },
moderator = { name = true, bad = true, unbad = true, export = true },
}
------------------------------------------------------------------------
-- Namen
------------------------------------------------------------------------
local function Core(name)
if type(name) ~= "string" then return "" end
local n = name:gsub("\194\160", " "):match("^([^%-]*)") or ""
return (n:gsub("%s", ""):lower())
end
local function RealmPart(name)
local r = type(name) == "string" and name:match("%-(.+)$")
if r then
r = r:gsub("[%s%-]", ""):lower()
if r ~= "" then return r end
end
return nil
end
local function OwnRealm()
local r = (GetNormalizedRealmName and GetNormalizedRealmName()) or (GetRealmName and GetRealmName()) or ""
return (tostring(r):gsub("[%s%-]", ""):lower())
end
-- Der eigene Realm hat im Spiel mehrere Schreibweisen (z.B. GetNormalizedRealmName = "ClassicBetaPvP2",
-- UnitFullName liefert "Steel"). Alle gelten als derselbe Realm und werden auf OwnRealm() vereinheitlicht.
local function IsOwnRealmVariant(r)
if r == OwnRealm() then return true end
local variants = {}
if GetRealmName then variants[#variants + 1] = GetRealmName() end
if UnitFullName then
local ok, _, ur = pcall(UnitFullName, "player")
if ok then variants[#variants + 1] = ur end
end
for _, v in ipairs(variants) do
if type(v) == "string" and v:gsub("[%s%-]", ""):lower() == r then return true end
end
return false
end
-- "name-realm" in Kleinschreibung; ohne Realm gilt der eigene
function Trust.Key(name)
local core = Core(name)
if core == "" then return nil end
local realm = RealmPart(name)
if not realm or IsOwnRealmVariant(realm) then realm = OwnRealm() end
return core .. "-" .. realm
end
-- Eintrag aus einer Liste oder von Hand getippt ("Speedtest-Timerun", "Aroki Steel", "Yuto-ClassicBetaPvP2").
-- In Forever haben Charaktere Vor- und Nachnamen ("Aroki Steel"); ein Bindestrich gilt nur dann als Realm-Trenner,
-- wenn dahinter der eigene Realm steht. Sonst ist es der Nachname (Bindestrich = Leerzeichen).
function Trust.EntryKey(name)
if type(name) ~= "string" then return nil end
local clean = name:gsub("\194\160", " "):gsub("^%s+", ""):gsub("%s+$", "")
local base, realm = clean:match("^(.*)%-([^%-]+)$")
if base and realm:gsub("[%s%-]", ""):lower() == OwnRealm() then
return Trust.Key(clean)
end
return Trust.Key((clean:gsub("%-", " ")))
end
-- Eigener voller Name. UnitFullName liefert in Forever Vorname und Nachname ("Aroki", "Steel"); die anderen
-- sehen uns als "Aroki Steel-ClassicBetaPvP2". Weicht der zweite Teil vom eigenen Realm ab, ist es der Nachname.
function Trust.SelfKey()
local name, second
if UnitFullName then
local ok, n, r = pcall(UnitFullName, "player")
if ok then name, second = n, r end
end
if not name and UnitName then name = (UnitName("player")) end
if not name then return nil end
if second and second ~= "" and second:gsub("[%s%-]", ""):lower() ~= OwnRealm() then
name = name .. " " .. second
end
return Trust.Key(name)
end
local function Data()
local db = Codex.db
if not db then return { rev = 0, list = {} } end
db.trust = db.trust or { rev = 0, list = {} }
db.trust.list = db.trust.list or {}
return db.trust
end
local function OwnerKeys()
local set = {}
for _, n in ipairs(NS.Owners or {}) do
local k = Trust.EntryKey(n)
if k then set[k] = true end
end
return set
end
------------------------------------------------------------------------
-- Abfragen
------------------------------------------------------------------------
function Trust.IsOwner(name)
local k = Trust.Key(name)
return k ~= nil and OwnerKeys()[k] == true
end
function Trust.IsSelfOwner()
return Trust.IsOwner(Trust.SelfKey())
end
-- Rolle: "owner", "moderator", "editor", "blocked" oder nil
function Trust.RoleOf(name)
local k = Trust.Key(name)
if not k then return nil end
local entry = Data().list[k]
if entry and entry.role == "blocked" then return "blocked" end
if OwnerKeys()[k] then return "owner" end
return entry and entry.role or nil
end
function Trust.IsDev(name)
local r = Trust.RoleOf(name)
return r == "owner" or r == "moderator" or r == "editor"
end
function Trust.SelfRole()
-- OctoWoW-Test: Bypass der Berechtigungsliste (FW.OCTO.testBypass in Freewood-OctoWoW-Core/Core/Init.lua)
if FW.OCTO and FW.OCTO.testBypass then return "owner" end
return Trust.RoleOf(Trust.SelfKey())
end
-- Darf dieser Charakter eine Aktion ausfuehren? Aktion: name, bad, unbad, export. Braucht das Modul Freewood-OctoWoW-Dev.
function Trust.Can(action)
if not Codex.DevModule then return false end
if FW.OCTO and FW.OCTO.testBypass then return true end -- OctoWoW-Test
local role = Trust.SelfRole()
if role == "owner" then return true end
local a = role and ACTIONS[role]
return a ~= nil and a[action] == true
end
function Trust.CanEdit()
return Trust.Can("name")
end
function Trust.Revision()
return Data().rev or 0
end
function Trust.Entries()
local out = {}
for key, e in pairs(Data().list) do
out[#out + 1] = { key = key, role = e.role }
end
table.sort(out, function(a, b) return a.key < b.key end)
return out
end
------------------------------------------------------------------------
-- Liste setzen und verschicken
------------------------------------------------------------------------
local function Changed()
if Codex.Share and Codex.Share.TrustChanged then Codex.Share.TrustChanged() end
FW:Fire("CODEX_TRUST_CHANGED")
FW:Fire("CODEX_CHANGED", "trust")
end
-- entries: Liste { key, role }
function Trust.SetList(entries, rev)
local d = Data()
local list = {}
local n = 0
for _, e in ipairs(entries or {}) do
local key = Trust.Key(e.key)
if key and ROLE_CODE[e.role] and n < MAX_ENTRIES then
list[key] = { role = e.role }
n = n + 1
end
end
d.list = list
d.rev = rev or math.max((d.rev or 0) + 1, time and time() or 1) -- Zeitstempel: bleibt auch nach einem Zuruecksetzen steigend
d.stamp = time and time() or 0
Changed()
return d.rev
end
-- Nachrichten "T|rev|i/n|text" (Eintraege "name-realm,e;name-realm,m;...")
function Trust.Chunks()
local d = Data()
local parts = {}
for _, e in ipairs(Trust.Entries()) do
local key = e.key:gsub("[;,|]", "")
parts[#parts + 1] = key .. "," .. ROLE_CODE[e.role]
end
local chunks, cur = {}, ""
for _, p in ipairs(parts) do
if #cur + #p + 1 > 190 then
chunks[#chunks + 1] = cur
cur = ""
end
cur = cur .. (cur ~= "" and ";" or "") .. p
end
chunks[#chunks + 1] = cur
local out = {}
for i, c in ipairs(chunks) do
out[i] = string.format("T|%d|%d/%d|%s", d.rev or 0, i, #chunks, c)
end
return out
end
local function Parse(payload)
local list = {}
for item in (payload .. ";"):gmatch("([^;]*);") do
local key, code = item:match("^(.-),([emb])$")
if key and key ~= "" then
list[#list + 1] = { key = key, role = CODE_ROLE[code] }
end
end
return list
end
-- Empfang: nur von Eigentuemern. body = "rev|i/n|text". Rueckgabe: true, wenn eine neue Liste uebernommen wurde
function Trust.Receive(sender, body)
if not Trust.IsOwner(sender) then return false end
local rev, i, n, payload = tostring(body):match("^(%d+)|(%d+)/(%d+)|(.*)$")
rev, i, n = tonumber(rev), tonumber(i), tonumber(n)
if not rev or not i or not n or n < 1 or n > 12 or i < 1 or i > n then return false end
local d = Data()
if rev <= (d.rev or 0) then return false end -- aelter oder schon vorhanden
local key = Trust.Key(sender)
local buf = Trust.buffers[key]
if not buf or buf.rev ~= rev or buf.n ~= n then
buf = { rev = rev, n = n, parts = {}, got = 0 }
Trust.buffers[key] = buf
end
if not buf.parts[i] then
buf.parts[i] = payload
buf.got = buf.got + 1
end
if buf.got < n then return false end
Trust.buffers[key] = nil
local all = {}
for idx = 1, n do all[#all + 1] = buf.parts[idx] end
Trust.SetList(Parse(table.concat(all, ";")), rev)
return true
end
------------------------------------------------------------------------
-- Diagnose: /fwrolle
------------------------------------------------------------------------
SLASH_FREEWOODROLE1 = "/fwrolle"
SlashCmdList["FREEWOODROLE"] = function()
local d = Data()
local n = 0
for _ in pairs(d.list) do n = n + 1 end
local L = FW.L
FW:Print(string.format(L.CODEX_ROLE_INFO, tostring(Trust.SelfKey()), tostring(Trust.SelfRole() or "-"),
Codex.DevModule and L.CODEX_YES or L.CODEX_NO, n, d.rev or 0))
-- Ausfuehrliche Diagnose: was liefert das Spiel, und welche Schluessel stehen in der Eigentuemer-Liste?
local ok, un, ur = false, nil, nil
if UnitFullName then ok, un, ur = pcall(UnitFullName, "player") end
FW:Print(string.format("Rolle: UnitFullName = %s | %s, UnitName = %s, GetRealmName = %s, GetNormalizedRealmName = %s, eigener Realm = %s",
tostring(un), tostring(ur), tostring(UnitName and UnitName("player")), tostring(GetRealmName and GetRealmName()),
tostring(GetNormalizedRealmName and GetNormalizedRealmName()), OwnRealm()))
local self = Trust.SelfKey()
local found = false
for _, entry in ipairs(NS.Owners or {}) do
local k = Trust.EntryKey(entry)
if k == self then found = true end
FW:Print(string.format("Rolle: Eigentuemer \"%s\" -> %s%s", tostring(entry), tostring(k), k == self and " <== du" or ""))
end
FW:Print(string.format("Rolle: dein Schluessel %s steht %s in der Eigentuemer-Liste", tostring(self), found and "" or "NICHT"))
end
+4415
View File
File diff suppressed because it is too large Load Diff
+1406
View File
File diff suppressed because it is too large Load Diff
+237
View File
@@ -0,0 +1,237 @@
-- Freewood-OctoWoW-Codex: Gebiete mit Stufenspanne, berechnet aus den gesammelten Kreaturen.
--
-- Jede Kreatur zaehlt mit ihren Sichtungen (Zellen) in den Karten, in denen sie gesehen wurde, Instanzen mit ihrer
-- Instanz-ID (negative Zone). Pro Gebiet:
-- lo, hi typische Spanne: alle Stufen, die mindestens 5 % des Gewichts normaler Kreaturen haben
-- avg gewichteter Durchschnitt innerhalb der Spanne
-- out Ausreisser: normale Kreaturen ausserhalb der Spanne
-- special Elite / Selten / Boss usw. (fliessen nicht in die Spanne ein)
-- few zu wenig Daten (weniger als MIN_UNITS normale Kreaturen): Spanne nur grob
-- manual Spanne von Berechtigten von Hand gesetzt (db.ied, siehe Codex.ZoneEdID)
-- Wird mit den geteilten Kreaturdaten berechnet, daher sehen alle dasselbe.
local ADDON, NS = ...
local FW = _G.Freewood
if not FW or not FW:RequireAPI(1, ADDON) then
return
end
local Codex = NS and NS.Codex
if not Codex then
return
end
local Zones = {}
Codex.Zones = Zones
local GRID = Codex.GRID or 200
local MIN_UNITS = 3 -- weniger normale Kreaturen: "zu wenig Daten"
local LEVEL_SHARE = 0.05 -- Mindestanteil einer Stufe am Gewicht, damit sie zur typischen Spanne gehoert
local WEIGHT_CAP = 30 -- eine haeufig gesehene Kreatur zaehlt hoechstens so viel
local MAP_SHARE = 0.10 -- Karte zaehlt fuer eine Kreatur erst ab diesem Anteil ihrer Sichtungen
local CACHE_SECS = 5
local cache, cacheAt, dirty = nil, 0, true
function Zones.Invalidate()
dirty = true
end
FW:On("CODEX_CHANGED", function() dirty = true end, Zones)
local function Now()
return GetTime and GetTime() or 0
end
-- Gewichte einer Kreatur je Gebiet: { [zoneKey] = Gewicht }
local function Spread(u)
local out = {}
if type(u.ez) == "table" and #u.ez > 0 then
local each = 1 / #u.ez
local total = 0
for _, n in pairs(u.cells or {}) do total = total + n end
total = math.max(1, total)
for _, m in ipairs(u.ez) do out[m] = total * each end
return out
end
if type(u.iz) == "table" and next(u.iz) and u.dg ~= false then
for id, n in pairs(u.iz) do out[-id] = n end
return out
end
local per, total = {}, 0
for key, n in pairs(u.cells or {}) do
local m = math.floor(key / (GRID * GRID))
per[m] = (per[m] or 0) + n
total = total + n
end
for m, n in pairs(per) do
if total > 0 and n / total >= MAP_SHARE then out[m] = n end
end
return out
end
local function ManualRange(ied, key)
local rec = ied and ied[Codex.ZoneEdID(key)]
if not rec then return nil end
local l1 = rec.l1 or (rec.ed and rec.ed.l1)
local l2 = rec.l2 or (rec.ed and rec.ed.l2)
if l1 then return l1, l2 or l1 end
return nil
end
local function Compute()
local V = Codex.V()
local zones = {}
local function Zone(key)
local z = zones[key]
if not z then
z = { id = key, normal = {}, special = {}, out = {} }
zones[key] = z
end
return z
end
for id, u in pairs(V.units or {}) do
if u.l1 and u.bad ~= true then
local special = u.c ~= nil and u.c ~= false
local l2 = u.l2 or u.l1
for key, n in pairs(Spread(u)) do
local w = math.max(1, math.min(WEIGHT_CAP, n))
local entry = { id = id, l1 = u.l1, l2 = l2, w = w, c = special and u.c or nil, mid = (u.l1 + l2) / 2 }
local z = Zone(key)
if special then
z.special[#z.special + 1] = entry
else
z.normal[#z.normal + 1] = entry
end
end
end
end
for key, z in pairs(zones) do
local byLevel, total = {}, 0
local minL, maxL
for _, e in ipairs(z.normal) do
-- eine Kreatur mit Spanne (Stufe 3-4) zaehlt je zur Haelfte fuer beide Stufen
local a, b = e.l1, e.l2
local half = e.w / ((b > a) and 2 or 1)
byLevel[a] = (byLevel[a] or 0) + half
if b > a then byLevel[b] = (byLevel[b] or 0) + half end
total = total + e.w
minL = minL and math.min(minL, a) or a
maxL = maxL and math.max(maxL, b) or b
end
z.units = #z.normal
z.few = z.units < MIN_UNITS
local lo, hi
if total > 0 then
for lv, w in pairs(byLevel) do
if w / total >= LEVEL_SHARE then
lo = lo and math.min(lo, lv) or lv
hi = hi and math.max(hi, lv) or lv
end
end
lo, hi = lo or minL, hi or maxL
elseif #z.special > 0 then
for _, e in ipairs(z.special) do
minL = minL and math.min(minL, e.l1) or e.l1
maxL = maxL and math.max(maxL, e.l2) or e.l2
end
lo, hi = minL, maxL
z.few = true
end
local ml, mh = ManualRange(V.ied, key)
if ml then
lo, hi = ml, mh
z.manual = true
z.few = false
end
z.lo, z.hi = lo, hi
local sum, wsum = 0, 0
for _, e in ipairs(z.normal) do
if lo and e.mid >= lo and e.mid <= hi then
sum, wsum = sum + e.mid * e.w, wsum + e.w
end
end
z.avg = wsum > 0 and sum / wsum or ((lo and hi) and (lo + hi) / 2 or nil)
for _, e in ipairs(z.normal) do
if lo and (e.l2 < lo or e.l1 > hi) then z.out[#z.out + 1] = e end
end
z.dungeon = key < 0
z.minL, z.maxL = minL, maxL
end
-- von Hand gesetzte Spannen auch fuer Gebiete ohne Kreaturdaten
for edID, rec in pairs(V.ied or {}) do
local key = Codex.ZoneFromEdID(edID)
if key and not zones[key] then
local l1, l2 = ManualRange(V.ied, key)
if l1 then
zones[key] = { id = key, normal = {}, special = {}, out = {}, units = 0, lo = l1, hi = l2, avg = (l1 + l2) / 2,
manual = true, dungeon = key < 0 }
end
end
end
return zones
end
-- Alle Gebiete: { [zoneKey] = Daten }. Kurz zwischengespeichert.
function Zones.All()
local t = Now()
if not cache or (dirty and t - cacheAt >= CACHE_SECS) or t < cacheAt then
local ok, res = pcall(Compute)
cache = ok and res or cache or {}
cacheAt, dirty = t, false
end
return cache
end
-- Sofort neu berechnen (zum Beispiel nach einer Aenderung im Fenster oder in Tests)
function Zones.Refresh()
dirty = true
cacheAt = -1e9
return Zones.All()
end
function Zones.Get(key)
return Zones.All()[key]
end
-- Eine Kreatur gehoert zu einem Gebiet: Liste fuer die Detailansicht, nach Stufe sortiert
function Zones.Members(key)
local z = Zones.Get(key)
if not z then return {} end
local list = {}
local outSet = {}
for _, e in ipairs(z.out) do outSet[e] = true end
for _, e in ipairs(z.normal) do
list[#list + 1] = { id = e.id, l1 = e.l1, l2 = e.l2, w = e.w, out = outSet[e] or nil }
end
for _, e in ipairs(z.special) do
list[#list + 1] = { id = e.id, l1 = e.l1, l2 = e.l2, w = e.w, c = e.c }
end
table.sort(list, function(a, b)
if a.l1 ~= b.l1 then return a.l1 < b.l1 end
if a.l2 ~= b.l2 then return a.l2 < b.l2 end
return a.id < b.id
end)
return list
end
-- Passt die Stufe zum Gebiet? Rueckgabe: "in" (Stufe liegt in der Spanne), "below" (Spanne ist niedriger),
-- "above" (Spanne ist hoeher), nil (keine Daten). tol = Toleranz in Stufen.
function Zones.Compare(key, level, tol)
local z = Zones.Get(key)
if not z or not z.lo or z.few and not z.manual then return nil end
tol = tol or 0
if level < z.lo - tol then return "above" end
if level > z.hi + tol then return "below" end
return "in"
end
-- Gebiete, deren typische Spanne die Stufe enthaelt (mit Toleranz), mit genug Daten
function Zones.ForLevel(level, tol)
local out = {}
for key, z in pairs(Zones.All()) do
if Zones.Compare(key, level, tol) == "in" then out[#out + 1] = z end
end
return out
end
+41
View File
@@ -0,0 +1,41 @@
## Interface: 11200
## Title: Freewood |cffc89b3cCodex|r
## Notes: Learns quests, creatures and drops while you play: who gives and takes a quest, rewards, objectives, drop chances.
## Notes-deDE: Lernt beim Spielen Quests, Kreaturen und Beute: wer eine Quest vergibt und annimmt, Belohnungen, Ziele, Drop-Chancen.
## Author: Lacustat
## Version: 0.1.136
## Dependencies: Freewood-OctoWoW-Core
## OptionalDeps: Freewood-OctoWoW-CodexData, Freewood-OctoWoW-ForeverData
Locales.lua
Codex.lua
CodexOwners.lua
CodexTrust.lua
CodexNames.lua
CodexSeed.lua
CodexRoles.lua
CodexRecipes.lua
CodexTime.lua
CodexShare.lua
CodexZones.lua
CodexPath.lua
CodexMapAPI.lua
CodexPathMap.lua
CodexTaxi.lua
CodexTaxiUI.lua
CodexTransport.lua
CodexSpots.lua
CodexForever.lua
CodexAvail.lua
CodexCatalog.lua
CodexQuestProbe.lua
CodexWdbChain.lua
CodexMapScan.lua
CodexSync.lua
CodexLookup.lua
CodexMap.lua
CodexUI.lua
CodexHints.lua
CodexFavUI.lua
CodexNameUI.lua
CodexShareUI.lua
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+1095
View File
File diff suppressed because it is too large Load Diff