Repository navigation
Expand file tree
/
Copy pathCore.lua
More file actions
379 lines (337 loc) · 14.3 KB
/
Copy pathCore.lua
File metadata and controls
379 lines (337 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
-- Alts Forever core: saved data, character identity, event dispatch, slash commands.
local ADDON, ns = ...
local L = ns.L
-- Only one copy of Alts Forever may run (e.g. the CurseForge copy and a dev copy both
-- enabled). Addons load one at a time, so a copy that finds another already running stays
-- off: every file stops here, before touching any global, event or hook, and it says so at
-- login. Both copies keep their data in the same global, AltsForeverDB, so the running copy
-- also puts its own data back when the other copy's saved data replaces it (below).
if AltsForeverRunning then
ns.disabled = true
local running = AltsForeverRunning
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGIN")
f:SetScript("OnEvent", function()
print("|cff66ccffAlts Forever|r: " .. L["two copies are enabled (%s and %s). Only %s is running; disable one of them in the AddOns list."]
:format(running, ADDON, running))
end)
return
end
AltsForeverRunning = ADDON
local DB_VERSION = 2
local pcall, type, pairs, ipairs, print, time = pcall, type, pairs, ipairs, print, time
local issecretvalue = issecretvalue or function() return false end
local UnitName, UnitClass, UnitFactionGroup = UnitName, UnitClass, UnitFactionGroup
-- Bumped whenever the current character's data changes; the tooltip uses it to
-- know when its cached line for the current character is stale.
ns.version = 0
-- Bumped when anyone's craftable items may have changed, including /af delete.
ns.craftVersion = 0
---------------------------------------------------------------------------
-- Events: one frame dispatches to every handler registered for an event
---------------------------------------------------------------------------
local frame = CreateFrame("Frame")
local handlers = {}
-- Registering an event this client doesn't know throws on Forever, so guard it.
-- Several handlers can listen to one event; they run in the order added.
function ns.On(event, fn)
if not pcall(frame.RegisterEvent, frame, event) then return false end
local prev = handlers[event]
handlers[event] = prev and function(...)
prev(...)
fn(...)
end or fn
return true
end
-- Removes every handler for the event.
function ns.Off(event)
handlers[event] = nil
frame:UnregisterEvent(event)
end
frame:SetScript("OnEvent", function(_, event, ...)
local fn = handlers[event]
if fn then fn(...) end
end)
---------------------------------------------------------------------------
-- Saved data
---------------------------------------------------------------------------
-- Version 1 keyed characters "Name-Realm". Forever has no realms (a full name is
-- unique in its region), so version 2 keys them by full name alone. If two entries
-- share a name, the most recently updated one is kept.
local function Upgrade(saved)
if saved.v ~= 1 or type(saved.chars) ~= "table" then return end
local chars = {}
for key, c in pairs(saved.chars) do
if type(c) == "table" then
local name = c.name or key:match("^(.*)%-[^%-]*$") or key
c.name, c.realm = name, nil
local old = chars[name]
if not old or (c.updated or c.seen or 0) > (old.updated or old.seen or 0) then chars[name] = c end
end
end
saved.chars, saved.realmOnly, saved.v = chars, nil, 2
end
-- Since build 70009, UnitName returns the first name and the surname as two values, and
-- versions 0.2.0 and 0.2.1 saved characters under the first name alone ("Mira" for "Mira
-- Dawnfield"). Forever names are always two words, so a one-word entry with exactly one
-- "First Last" entry of the same class is that character.
local function FullNameMatch(chars, first, class)
local match
for key, c in pairs(chars) do
if key:sub(1, #first + 1) == first .. " " and type(c) == "table" and c.class == class then
if match then return nil end -- two candidates: don't guess
match = key
end
end
return match
end
local function Newer(a, b)
return (a.updated or a.seen or 0) >= (b.updated or b.seen or 0)
end
-- Keeps everything in `keep` and fills in what it lacks (bank, mail, recipes...) from `other`.
local function Merge(keep, other)
for k, v in pairs(other) do
if keep[k] == nil then keep[k] = v end
end
end
-- Folds entries saved under a first name only back into the full name, newer data first.
local function FoldFirstNames(chars)
local short = {}
for key in pairs(chars) do
if not key:find(" ", 1, true) then short[#short + 1] = key end
end
for _, key in ipairs(short) do
local c = chars[key]
local full = type(c) == "table" and FullNameMatch(chars, key, c.class)
if full then
local keep, old = c, chars[full]
if not Newer(keep, old) then keep, old = old, keep end
Merge(keep, old)
keep.name = full
chars[full], chars[key] = keep, nil
end
end
end
function ns.InitDB(saved)
if type(saved) == "table" then Upgrade(saved) end
if type(saved) ~= "table" or saved.v ~= DB_VERSION or type(saved.chars) ~= "table" then
saved = { v = DB_VERSION, chars = {} }
end
FoldFirstNames(saved.chars)
return saved
end
-- The logged-in character's full name. Build 70009 returns it as UnitName's two values
-- ("Mira", "Dawnfield"); earlier builds returned "Mira Dawnfield" as the first value.
function ns.PlayerName()
local first, surname = UnitName("player")
if surname and not issecretvalue(surname) and surname ~= "" then
return first .. " " .. surname
end
return first
end
-- An entry saved under this character's first name alone (by 0.2.0 or 0.2.1) takes the
-- full name. Where both exist, InitDB has already folded them together.
function ns.AdoptFirstName(chars, name, class)
local first = name:match("^(%S+) ")
local old = first and chars[first]
if type(old) == "table" and old.class == class and chars[name] == nil then
chars[name], chars[first] = old, nil
old.name = name
end
end
-- Bank and mail stay nil until first seen, so "never scanned" differs from "empty".
-- Characters are keyed by full name ("First Last"), which is unique in a region.
function ns.InitChar(db, name, class, faction)
local key = name
local c = db.chars[key]
if type(c) ~= "table" then
c = {}
db.chars[key] = c
end
c.name, c.class, c.faction = name, class, faction
c.bags = c.bags or {}
c.equip = c.equip or {}
return key, c
end
ns.On("ADDON_LOADED", function(name)
if name ~= ADDON then
-- Another copy's saved data just loaded into the shared global: keep ours.
if ns.db and AltsForeverDB ~= ns.db then AltsForeverDB = ns.db end
return
end
-- A fresh install: no saved data, or no characters yet (then the welcome is shown).
local saved = AltsForeverDB
ns.freshInstall = type(saved) ~= "table" or type(saved.chars) ~= "table" or next(saved.chars) == nil
AltsForeverDB = ns.InitDB(AltsForeverDB)
ns.db = AltsForeverDB
-- Left over from the rested XP rate check, which has been removed.
ns.db.restedChecks, ns.db.restCheckOff = nil, nil
-- Before login, so EllesmereUI's minimap finds it when it collects addon buttons.
ns.CreateMinimapButton()
end)
ns.On("PLAYER_LOGIN", function()
ns.Off("PLAYER_LOGIN")
local _, class = UnitClass("player")
local name = ns.PlayerName()
ns.AdoptFirstName(ns.db.chars, name, class)
local newChar = ns.db.chars[name] == nil
ns.charKey, ns.char = ns.InitChar(ns.db, name, class, UnitFactionGroup("player"))
ns.StartScanner()
ns.StartMail()
ns.StartMoney()
ns.StartProfessions()
ns.StartCharacter()
ns.StartTalents()
ns.StartOverview()
ns.StartBars()
ns.StartGear()
ns.StartBags()
ns.StartTooltip()
ns.StartUpgrades()
ns.StartReputation()
ns.StartOptions()
ns.StartGamepad()
ns.Welcome(newChar)
end)
-- A few seconds after login (after the game's own login messages): on a fresh install,
-- how to use Alts Forever, once; on a character seen for the first time, that it's now
-- tracked. Existing players never see the welcome (db.welcomed is set quietly).
function ns.Welcome(newChar)
local db = ns.db
local lines
if not db.welcomed and ns.freshInstall then
lines = {
L["Welcome! Hover any item to see how many your characters have, and where."],
L["Type /af or click the minimap button for all your characters at a glance. Settings are under Options > AddOns > Alts Forever."],
L["Log in on each character once, and open their bank, mailbox and profession windows once, so Alts Forever knows what they have."],
}
elseif newChar then
lines = { L["Now tracking %s. Open the bank and mailbox once on this character so they're included too."]:format(ns.charKey) }
end
db.welcomed = true
if not lines then return end
local function Show()
for i = 1, #lines do ns.Print(lines[i]) end
end
if C_Timer and C_Timer.After then C_Timer.After(5, Show) else Show() end
end
ns.On("PLAYER_LOGOUT", function()
if ns.char then ns.char.seen = time() end
end)
---------------------------------------------------------------------------
-- Slash commands
---------------------------------------------------------------------------
local function Print(msg)
print("|cff66ccffAlts Forever|r: " .. msg)
end
ns.Print = Print
-- Finds a stored character by full name, ignoring case.
function ns.FindChar(input)
input = input:lower()
for key in pairs(ns.db.chars) do
if key:lower() == input then return key end
end
end
local FindChar = ns.FindChar
local commands = {}
function commands.list()
for key, c in pairs(ns.db.chars) do
Print(key .. (c.bank and "" or " " .. L["(bank not scanned)"]))
end
end
function commands.delete(arg)
local key = arg ~= "" and FindChar(arg)
if not key then return Print(L["No character named '%s'. Use /af list."]:format(arg)) end
if key == ns.charKey then return Print(L["You can't delete the character you're logged in on."]) end
ns.ForgetCharacter(key)
Print(L["Deleted %s."]:format(key))
end
function commands.mail()
-- Everyone with mail on record, however far off it expires.
if not ns.PrintMailWarnings(math.huge) then Print(L["No mail with items or gold on record."]) end
end
function commands.skillups()
ns.SetSkillups(not ns.SkillupsOn())
Print(ns.db.skillupsOff and L["Skill-up details in tooltips off."] or L["Skill-up details in tooltips on."])
end
function commands.upgrades()
ns.SetUpgrades(not ns.UpgradesOn())
Print(ns.UpgradesOn() and L["Upgrades in item tooltips on."] or L["Upgrades in item tooltips off."])
end
-- /af role First Last melee|ranged|caster|healer|tank|feral|auto: the stats a character's
-- upgrades are judged by; auto follows their talents.
function commands.role(arg)
local name, word = arg:match("^(.-)%s+(%S+)$")
local key = name and FindChar(name)
if not key then return Print(L["Type /af role, a character's name and a role: melee, ranged, caster, healer, tank, feral, or auto to follow their talents."]) end
local c = ns.db.chars[key]
local choices = ns.RoleChoices(c)
if not choices then return Print(L["%s has only one role, so there's nothing to choose."]:format(key)) end
word = word:lower()
if word == "auto" then
ns.SetRole(key, nil)
return Print(L["Upgrades for %s now follow their talents (%s)."]:format(key, ns.RoleName(ns.AutoRole(c))))
end
local role = ns.RoleFromWord(c, word)
if not role then
local words = {}
for i, r in ipairs(choices) do words[i] = ns.RoleWord(r) end
return Print(L["%s has no role '%s'. Choose from: %s, or auto."]:format(key, word, table.concat(words, ", ")))
end
ns.SetRole(key, role)
Print(L["Upgrades for %s are now judged as %s."]:format(key, ns.RoleName(role)))
end
function commands.minimap()
ns.SetMinimapButton(not ns.MinimapButtonOn())
Print(ns.MinimapButtonOn() and L["Minimap button shown."] or L["Minimap button hidden. /af minimap brings it back."])
end
function commands.sendmail()
ns.SetSendToAlt(not ns.SendToAltOn())
Print(ns.SendToAltOn() and L["Send to alt arrow at the mailbox on."] or L["Send to alt arrow at the mailbox off."])
end
function commands.stats()
ns.SetStats(not ns.StatsOn())
Print(ns.StatsOn() and L["Session stats on: XP this session on the XP bar, gold over time on your bag gold."]
or L["Session stats off."])
end
function commands.icons()
if not ns.OpenSettings() then ns.OpenIconPicker("bags") end
end
function commands.find(arg)
ns.FindItems(arg)
end
-- /af bags [name], /af bank [name]: a character's bags or bank, slot by slot.
local function ShowBags(arg, which)
local key = arg == "" and ns.charKey or FindChar(arg)
if not key then return Print(L["No character named '%s'. Use /af list."]:format(arg)) end
ns.ShowBags(key, which)
end
function commands.bags(arg) ShowBags(arg, "bags") end
function commands.bank(arg) ShowBags(arg, "bank") end
function commands.rep()
ns.ToggleReputation()
end
function commands.mem()
UpdateAddOnMemoryUsage()
Print(L["Memory: %.1f KB"]:format(GetAddOnMemoryUsage(ADDON)))
end
function commands.help()
Print(L["by Kadmai. /af opens the overview. Also: /af rep | mail | list | delete Name | skillups | sendmail | minimap | stats | mem"])
Print(L["/af find <text> searches every character's items by name."])
Print(L["/af bags or /af bank, with a name for another character, shows their bags or bank slot by slot."])
Print(L["/af icons picks the icons item tooltips use for bags, bank, mail and worn items."])
Print(L["/af upgrades turns upgrade hints in item tooltips on or off. /af role Name role sets what a character's upgrades are judged by (melee, ranged, caster, healer, tank, feral or auto)."])
Print(L["Or use the minimap button (right-click for options)."])
end
ns.ShowHelp = commands.help
-- For the menus: runs a slash command by name.
function ns.RunCommand(name, arg)
commands[name](arg or "")
end
commands[""] = function() ns.ToggleOverview() end
SLASH_ALTSFOREVER1, SLASH_ALTSFOREVER2 = "/af", "/altsforever"
SlashCmdList.ALTSFOREVER = function(msg)
local cmd, arg = msg:match("^%s*(%S*)%s*(.-)%s*$")
local fn = commands[cmd:lower()] or commands.help
fn(arg)
end