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