-- 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 not (AA.db and AA.db.chat) 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