413 lines
13 KiB
Lua
413 lines
13 KiB
Lua
-- Ade Alts: sync
|
|
local AA = AdeAlts
|
|
|
|
----------------------------------------------------------------------
|
|
-- sync between accounts (guild / party / raid addon messages)
|
|
----------------------------------------------------------------------
|
|
local function Encode(c)
|
|
local head = "C^" .. c.name .. "^" .. c.realm .. "^" .. (c.class or "") .. "^" .. (c.updated or 0) .. "^"
|
|
local msgs, cur = {}, ""
|
|
for key, exp in pairs(c.cds) do
|
|
local pair = key .. "=" .. exp
|
|
if string.len(head) + string.len(cur) + string.len(pair) + 1 > 240 then
|
|
table.insert(msgs, head .. cur)
|
|
cur = ""
|
|
end
|
|
if cur ~= "" then cur = cur .. "~" end
|
|
cur = cur .. pair
|
|
end
|
|
table.insert(msgs, head .. cur)
|
|
return msgs
|
|
end
|
|
|
|
function AA.Send(msg)
|
|
if AA.db and AA.db.noChat then return end
|
|
if IsInGuild and IsInGuild() then SendAddonMessage(AA.PREFIX, msg, "GUILD") end
|
|
if GetNumRaidMembers() > 0 then
|
|
SendAddonMessage(AA.PREFIX, msg, "RAID")
|
|
elseif GetNumPartyMembers() > 0 then
|
|
SendAddonMessage(AA.PREFIX, msg, "PARTY")
|
|
end
|
|
end
|
|
|
|
function AA.Broadcast()
|
|
if not AA.me then return end
|
|
AA.lastBroadcast = GetTime()
|
|
for _, m in ipairs(Encode(AA.me)) do AA.Send(m) end
|
|
end
|
|
|
|
function AA.ReceiveRecord(text)
|
|
local _, _, name, realm, class, upd, body = string.find(text, "^C%^([^%^]*)%^([^%^]*)%^([^%^]*)%^(%d*)%^(.*)$")
|
|
if not name or name == "" then return false end
|
|
upd = tonumber(upd) or 0
|
|
local k = name .. "-" .. realm
|
|
if AA.db.ignored and AA.db.ignored[k] then return false end
|
|
local c = AA.db.chars[k]
|
|
if c and (c.updated or 0) > upd then return false end -- we already have newer data
|
|
if not c then
|
|
c = { name = name, realm = realm, class = class, cds = {}, remote = true }
|
|
AA.db.chars[k] = c
|
|
end
|
|
if class ~= "" then c.class = class end
|
|
c.cds = c.cds or {}
|
|
for key, exp in string.gfind(body, "([^~=]+)=(%d+)") do
|
|
c.cds[key] = tonumber(exp)
|
|
end
|
|
c.updated = upd
|
|
AA.ClampRecord(c)
|
|
return true
|
|
end
|
|
|
|
-- shared file between accounts on the same game folder (needs SuperWoW)
|
|
local function InvStr(list)
|
|
if not list then return "" end
|
|
local parts = {}
|
|
for _, e in ipairs(list) do table.insert(parts, e[1] .. ":" .. e[2]) end
|
|
return table.concat(parts, "~")
|
|
end
|
|
|
|
local function ParseInv(str)
|
|
local list = {}
|
|
for id, n in string.gfind(str or "", "(%d+):(%d+)") do
|
|
table.insert(list, { tonumber(id), tonumber(n) })
|
|
end
|
|
return list
|
|
end
|
|
|
|
local function EncodeAlt(c)
|
|
return "A^" .. c.name .. "^" .. c.realm .. "^" .. (c.class or "") .. "^" .. (c.level or 0) .. "^" .. (c.gold or 0)
|
|
.. "^" .. (c.dataTs or 0) .. "^" .. (c.invTs or 0) .. "^" .. (c.bankTs or 0)
|
|
.. "^" .. InvStr(c.inv) .. "^" .. InvStr(c.bank)
|
|
end
|
|
|
|
local function ReceiveAlt(text)
|
|
local _, _, name, realm, class, lvl, gold, dts, its, bts, inv, bank =
|
|
string.find(text, "^A%^([^%^]*)%^([^%^]*)%^([^%^]*)%^(%d*)%^(%d*)%^(%d*)%^(%d*)%^(%d*)%^([^%^]*)%^(.*)$")
|
|
if not name or name == "" then return false end
|
|
local k = name .. "-" .. realm
|
|
if AA.db.ignored and AA.db.ignored[k] then return false end
|
|
local c = AA.db.chars[k]
|
|
if not c then
|
|
c = { name = name, realm = realm, class = class, cds = {}, remote = true }
|
|
AA.db.chars[k] = c
|
|
end
|
|
dts, its, bts = tonumber(dts) or 0, tonumber(its) or 0, tonumber(bts) or 0
|
|
local ch = false
|
|
if dts > (c.dataTs or 0) then
|
|
c.dataTs = dts
|
|
c.level = tonumber(lvl)
|
|
c.gold = tonumber(gold)
|
|
if class ~= "" then c.class = class end
|
|
ch = true
|
|
end
|
|
if its > (c.invTs or 0) then
|
|
c.invTs = its
|
|
c.inv = ParseInv(inv)
|
|
ch = true
|
|
end
|
|
if bts > (c.bankTs or 0) then
|
|
c.bankTs = bts
|
|
c.bank = ParseInv(bank)
|
|
ch = true
|
|
end
|
|
return ch
|
|
end
|
|
|
|
local function ReceiveItem(text)
|
|
local _, _, id, name, tex, q = string.find(text, "^I%^(%d+)%^([^%^]*)%^([^%^]*)%^(%d*)$")
|
|
if not id then return end
|
|
id = tonumber(id)
|
|
AA.db.items = AA.db.items or {}
|
|
local it = AA.db.items[id]
|
|
if not it or not it.n or not it.t or it.t == "" then
|
|
AA.db.items[id] = { n = name, t = tex, q = tonumber(q) or 1 }
|
|
end
|
|
end
|
|
|
|
local function EncodeProf(c, pname, p)
|
|
return "P^" .. c.name .. "^" .. c.realm .. "^" .. pname .. "^" .. (p.r or 0) .. "^" .. (p.m or 0)
|
|
.. "^" .. (p.ts or 0) .. "^" .. (p.rts or 0) .. "^" .. table.concat(p.rec or {}, "~")
|
|
end
|
|
|
|
local function ReceiveProf(text)
|
|
local _, _, name, realm, pname, r, m, ts, rts, rec =
|
|
string.find(text, "^P%^([^%^]*)%^([^%^]*)%^([^%^]*)%^(%d*)%^(%d*)%^(%d*)%^(%d*)%^(.*)$")
|
|
if not name or name == "" or pname == "" then return false end
|
|
local gone = (tonumber(m) or 0) == 0 and (tonumber(r) or 0) == 0 -- tombstone: profession was dropped
|
|
if not gone and AA.BadProf(pname, tonumber(m)) then return false end
|
|
local k = name .. "-" .. realm
|
|
if AA.db.ignored and AA.db.ignored[k] then return false end
|
|
local c = AA.db.chars[k]
|
|
if not c then
|
|
c = { name = name, realm = realm, cds = {}, remote = true }
|
|
AA.db.chars[k] = c
|
|
end
|
|
c.prof = c.prof or {}
|
|
c.profGone = c.profGone or {}
|
|
ts, rts = tonumber(ts) or 0, tonumber(rts) or 0
|
|
if gone then
|
|
if ts > (c.profGone[pname] or 0) then
|
|
c.profGone[pname] = ts
|
|
local p0 = c.prof[pname]
|
|
if p0 and ts > (p0.ts or 0) then c.prof[pname] = nil end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
if ts <= (c.profGone[pname] or 0) then return false end -- dropped after this data was written
|
|
local p = c.prof[pname]
|
|
if not p then
|
|
p = {}
|
|
c.prof[pname] = p
|
|
end
|
|
local ch = false
|
|
if ts > (p.ts or 0) then
|
|
p.ts = ts
|
|
p.r = tonumber(r)
|
|
p.m = tonumber(m)
|
|
ch = true
|
|
end
|
|
if rts > (p.rts or 0) then
|
|
p.rts = rts
|
|
local list = {}
|
|
for rn in string.gfind(rec, "[^~]+") do table.insert(list, rn) end
|
|
p.rec = list
|
|
p.sig = nil
|
|
ch = true
|
|
end
|
|
return ch
|
|
end
|
|
|
|
local function EncodeEquip(c)
|
|
local parts = {}
|
|
for slot = 0, 19 do
|
|
if c.equip and c.equip[slot] then table.insert(parts, slot .. "=" .. c.equip[slot]) end
|
|
end
|
|
return "E^" .. c.name .. "^" .. c.realm .. "^" .. (c.eqTs or 0) .. "^" .. table.concat(parts, "~")
|
|
end
|
|
|
|
local function ReceiveEquip(text)
|
|
local _, _, name, realm, ts, body = string.find(text, "^E%^([^%^]*)%^([^%^]*)%^(%d*)%^(.*)$")
|
|
if not name or name == "" then return false end
|
|
local k = name .. "-" .. realm
|
|
if AA.db.ignored and AA.db.ignored[k] then return false end
|
|
local c = AA.db.chars[k]
|
|
if not c then
|
|
c = { name = name, realm = realm, cds = {}, remote = true }
|
|
AA.db.chars[k] = c
|
|
end
|
|
ts = tonumber(ts) or 0
|
|
if ts <= (c.eqTs or 0) then return false end
|
|
local eq = {}
|
|
for slot, ref in string.gfind(body, "(%d+)=(%-?%d+:%-?%d+:%-?%d+:%-?%d+)") do
|
|
eq[tonumber(slot)] = ref
|
|
end
|
|
c.equip = eq
|
|
c.eqTs = ts
|
|
return true
|
|
end
|
|
|
|
-- bag sizes / used slots: B^name^realm^invBagsTs^bankBagsTs^inv^bank
|
|
local function EncodeBags(c)
|
|
return "B^" .. c.name .. "^" .. c.realm .. "^" .. (c.invBagsTs or 0) .. "^" .. (c.bankBagsTs or 0)
|
|
.. "^" .. (c.invBags or "") .. "^" .. (c.bankBags or "")
|
|
end
|
|
|
|
local function ReceiveBags(text)
|
|
local _, _, name, realm, its, bts, inv, bank =
|
|
string.find(text, "^B%^([^%^]*)%^([^%^]*)%^(%d*)%^(%d*)%^([^%^]*)%^(.*)$")
|
|
if not name or name == "" then return false end
|
|
local k = name .. "-" .. realm
|
|
if AA.db.ignored and AA.db.ignored[k] then return false end
|
|
local c = AA.db.chars[k]
|
|
if not c then
|
|
c = { name = name, realm = realm, cds = {}, remote = true }
|
|
AA.db.chars[k] = c
|
|
end
|
|
its, bts = tonumber(its) or 0, tonumber(bts) or 0
|
|
local ch = false
|
|
if its > (c.invBagsTs or 0) then
|
|
c.invBagsTs = its
|
|
c.invBags = inv
|
|
ch = true
|
|
end
|
|
if bts > (c.bankBagsTs or 0) then
|
|
c.bankBagsTs = bts
|
|
c.bankBags = bank
|
|
ch = true
|
|
end
|
|
return ch
|
|
end
|
|
|
|
local function FileRead(name)
|
|
if not (ImportFile and ExportFile) then return nil end
|
|
local ok, txt = pcall(ImportFile, name)
|
|
if ok and txt and txt ~= "" then return txt end
|
|
ok, txt = pcall(ImportFile, name .. ".txt")
|
|
if ok and txt and txt ~= "" then return txt end
|
|
return nil
|
|
end
|
|
|
|
local function MergeText(txt)
|
|
local n = 0
|
|
for line in string.gfind(txt, "[^\r\n]+") do
|
|
local t = string.sub(line, 1, 2)
|
|
if t == "C^" then
|
|
if AA.ReceiveRecord(line) then n = n + 1 end
|
|
elseif t == "A^" then
|
|
if ReceiveAlt(line) then n = n + 1 end
|
|
elseif t == "P^" then
|
|
if ReceiveProf(line) then n = n + 1 end
|
|
elseif t == "E^" then
|
|
if ReceiveEquip(line) then n = n + 1 end
|
|
elseif t == "B^" then
|
|
if ReceiveBags(line) then n = n + 1 end
|
|
elseif t == "I^" then
|
|
ReceiveItem(line)
|
|
end
|
|
end
|
|
return n
|
|
end
|
|
|
|
local legacyDone = false
|
|
AA.FileMerge = function()
|
|
if not AA.db then return end
|
|
local n = 0
|
|
local txt = FileRead(AA.FILE)
|
|
if txt then n = n + MergeText(txt) end
|
|
if not legacyDone then
|
|
legacyDone = true
|
|
local old = FileRead(AA.LEGACY)
|
|
if old then n = n + MergeText(old) end
|
|
end
|
|
if n > 0 then AA.Refresh() end
|
|
end
|
|
|
|
local function FileWrite()
|
|
if not (ImportFile and ExportFile) or not AA.db then return end
|
|
local parts, seen = {}, {}
|
|
for k, c in pairs(AA.db.chars) do
|
|
for _, m in ipairs(Encode(c)) do table.insert(parts, m) end
|
|
if c.dataTs or c.invTs or c.bankTs then table.insert(parts, EncodeAlt(c)) end
|
|
local lists = { c.inv or {}, c.bank or {} }
|
|
for _, list in ipairs(lists) do
|
|
for _, e in ipairs(list) do seen[e[1]] = true end
|
|
end
|
|
if c.prof then
|
|
for pname, p in pairs(c.prof) do table.insert(parts, EncodeProf(c, pname, p)) end
|
|
end
|
|
for pname, gts in pairs(c.profGone or {}) do
|
|
if not (c.prof and c.prof[pname]) then
|
|
table.insert(parts, "P^" .. c.name .. "^" .. c.realm .. "^" .. pname .. "^0^0^" .. gts .. "^0^")
|
|
end
|
|
end
|
|
if c.invBags or c.bankBags then table.insert(parts, EncodeBags(c)) end
|
|
if c.equip then
|
|
table.insert(parts, EncodeEquip(c))
|
|
for slot, ref in pairs(c.equip) do
|
|
local _, _, sid = string.find(ref, "^(%d+)")
|
|
if sid then seen[tonumber(sid)] = true end
|
|
end
|
|
end
|
|
end
|
|
if AA.db.items then
|
|
for id in pairs(seen) do
|
|
local it = AA.db.items[id]
|
|
if it and it.n then
|
|
table.insert(parts, "I^" .. id .. "^" .. it.n .. "^" .. (it.t or "") .. "^" .. (it.q or 1))
|
|
end
|
|
end
|
|
end
|
|
pcall(ExportFile, AA.FILE, table.concat(parts, "\n"))
|
|
AA.fileDirty = false
|
|
AA.lastFileWrite = GetTime()
|
|
end
|
|
|
|
AA.FileSync = function()
|
|
AA.FileMerge()
|
|
FileWrite()
|
|
end
|
|
|
|
local function ExportString()
|
|
local parts = {}
|
|
for k, c in pairs(AA.db.chars) do
|
|
for _, m in ipairs(Encode(c)) do table.insert(parts, m) end
|
|
end
|
|
return table.concat(parts, "#")
|
|
end
|
|
|
|
local function ImportString(text)
|
|
local n = 0
|
|
for rec in string.gfind(text, "[^#]+") do
|
|
if AA.ReceiveRecord(rec) then n = n + 1 end
|
|
end
|
|
return n
|
|
end
|
|
|
|
-- export / import box
|
|
local io = CreateFrame("Frame", "AdeAltsIO", UIParent)
|
|
io:SetWidth(440)
|
|
io:SetHeight(90)
|
|
io:SetPoint("CENTER", UIParent, "CENTER", 0, 150)
|
|
io:SetFrameStrata("DIALOG")
|
|
io:SetBackdrop({
|
|
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = true, tileSize = 16, edgeSize = 16,
|
|
insets = { left = 4, right = 4, top = 4, bottom = 4 },
|
|
})
|
|
io:SetBackdropColor(0, 0, 0, 0.95)
|
|
io:EnableMouse(true)
|
|
io:Hide()
|
|
table.insert(UISpecialFrames, "AdeAltsIO")
|
|
|
|
local iotitle = io:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
iotitle:SetPoint("TOPLEFT", io, "TOPLEFT", 12, -10)
|
|
iotitle:SetText("Copy (Ctrl+C) to export, or paste (Ctrl+V) and press Import")
|
|
|
|
local eb = CreateFrame("EditBox", "AdeAltsIOEdit", io)
|
|
eb:SetWidth(410)
|
|
eb:SetHeight(22)
|
|
eb:SetPoint("TOPLEFT", io, "TOPLEFT", 15, -30)
|
|
eb:SetFontObject(ChatFontNormal)
|
|
eb:SetAutoFocus(false)
|
|
eb:SetMaxLetters(0)
|
|
eb:SetTextInsets(4, 4, 2, 2)
|
|
eb:SetBackdrop({
|
|
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = true, tileSize = 8, edgeSize = 8,
|
|
insets = { left = 2, right = 2, top = 2, bottom = 2 },
|
|
})
|
|
eb:SetBackdropColor(0.1, 0.1, 0.1, 1)
|
|
eb:SetScript("OnEscapePressed", function() this:ClearFocus() end)
|
|
|
|
local bimp = CreateFrame("Button", nil, io, "UIPanelButtonTemplate")
|
|
bimp:SetWidth(90); bimp:SetHeight(22)
|
|
bimp:SetPoint("BOTTOMLEFT", io, "BOTTOMLEFT", 15, 10)
|
|
bimp:SetText("Import")
|
|
bimp:SetScript("OnClick", function()
|
|
local n = ImportString(eb:GetText() or "")
|
|
AA.Print("imported " .. n .. " record(s).")
|
|
AA.Refresh()
|
|
end)
|
|
|
|
local bclose = CreateFrame("Button", nil, io, "UIPanelButtonTemplate")
|
|
bclose:SetWidth(90); bclose:SetHeight(22)
|
|
bclose:SetPoint("BOTTOMRIGHT", io, "BOTTOMRIGHT", -15, 10)
|
|
bclose:SetText("Close")
|
|
bclose:SetScript("OnClick", function() io:Hide() end)
|
|
|
|
function AA.ShowIO(exportMode)
|
|
io:Show()
|
|
if exportMode then
|
|
eb:SetText(ExportString())
|
|
eb:SetFocus()
|
|
eb:HighlightText()
|
|
else
|
|
eb:SetText("")
|
|
eb:SetFocus()
|
|
end
|
|
end
|
|
|