-- 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", 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