323 lines
10 KiB
Lua
323 lines
10 KiB
Lua
-- 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
|