Repository navigation
Expand file tree
/
Copy pathCore.lua
More file actions
292 lines (256 loc) · 10.4 KB
/
Copy pathCore.lua
File metadata and controls
292 lines (256 loc) · 10.4 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
-- Keystance core: saved data, character identity, event dispatch, the combat queue and
-- slash commands.
local ADDON, ns = ...
local L = ns.L
-- Only one copy of Keystance 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, KeystanceDB, so the running copy
-- also puts its own data back when the other copy's saved data replaces it (below).
if KeystanceRunning then
ns.disabled = true
local running = KeystanceRunning
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGIN")
f:SetScript("OnEvent", function()
print("|cff66ccffKeystance|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
KeystanceRunning = ADDON
local DB_VERSION = 1
-- Builders for the window's tabs, by tab key: fn(page, window). Filled by the tab files.
ns.pageBuilders = {}
local pcall, type, pairs, print = pcall, type, pairs, print
local issecretvalue = issecretvalue or function() return false end
local UnitName, UnitClass, InCombatLockdown = UnitName, UnitClass, InCombatLockdown
---------------------------------------------------------------------------
-- 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, each on its own:
-- an error in one is reported (BugGrabber) and doesn't stop the others.
function ns.On(event, fn)
if not pcall(frame.RegisterEvent, frame, event) then return false end
local list = handlers[event]
if not list then
list = {}
handlers[event] = list
end
list[#list + 1] = 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 list = handlers[event]
if not list then return end
for i = 1, #list do xpcall(list[i], geterrorhandler(), ...) end
end)
---------------------------------------------------------------------------
-- Saved data
---------------------------------------------------------------------------
-- Settings every account starts with. Missing ones are filled in at load; a player's own
-- choices are never replaced.
local DEFAULTS = {
skin = "auto", -- "auto" (EllesmereUI, then ElvUI, then classic), "classic", "ellesmere", "elvui"
layout = "auto", -- keyboard drawn: "auto" (UK for an English (UK) client, else US), "ansi", "iso"
}
-- UPGRADES[v] turns version v data into version v + 1 in place. Saved data is repaired,
-- never reset: players' profiles and snapshots must survive every update.
local UPGRADES = {}
function ns.InitDB(saved)
if type(saved) ~= "table" then saved = {} end
saved.v = saved.v or DB_VERSION
while saved.v < DB_VERSION and UPGRADES[saved.v] do
UPGRADES[saved.v](saved)
saved.v = saved.v + 1
end
if type(saved.settings) ~= "table" then saved.settings = {} end
for k, v in pairs(DEFAULTS) do
if saved.settings[k] == nil then saved.settings[k] = v end
end
if type(saved.chars) ~= "table" then saved.chars = {} end
return saved
end
-- The logged-in character's full name. Build 70009 returns it as UnitName's two values
-- ("Vespera", "Ashward"); earlier builds returned the full name 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
-- Characters are keyed by full name ("First Last"), which is unique: Forever has no realms.
function ns.InitChar(db, name, class)
local c = db.chars[name]
if type(c) ~= "table" then
c = {}
db.chars[name] = c
end
c.class = class
if type(c.profiles) ~= "table" then c.profiles = {} end
if type(c.rules) ~= "table" then c.rules = {} end
return 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 KeystanceDB ~= ns.db then KeystanceDB = ns.db end
return
end
KeystanceDB = ns.InitDB(KeystanceDB)
ns.db = KeystanceDB
-- 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")
ns.charKey = ns.PlayerName()
ns.char = ns.InitChar(ns.db, ns.charKey, class)
end)
---------------------------------------------------------------------------
-- Combat: changing bars and keys is blocked in combat (it fires ADDON_ACTION_BLOCKED, which
-- players see as an error), so such work never runs then. It waits here and runs as soon
-- as combat ends, in the order it was asked for.
---------------------------------------------------------------------------
local queued, order = {}, {}
local combatStarted = false
function ns.InCombat()
return (combatStarted or InCombatLockdown()) and true or false
end
ns.On("PLAYER_REGEN_DISABLED", function() combatStarted = true end)
ns.On("PLAYER_REGEN_ENABLED", function() combatStarted = false end)
-- Runs fn now if out of combat and returns true; otherwise runs it after combat and returns
-- false. Work queued again under the same key replaces the earlier request (the latest
-- profile asked for wins) but keeps its place.
function ns.OutOfCombat(key, fn)
if not InCombatLockdown() then
fn()
return true
end
if not queued[key] then order[#order + 1] = key end
queued[key] = fn
return false
end
-- Drops the work waiting under this key (a switch the player changed their mind about).
function ns.CancelWaiting(key)
if not queued[key] then return false end
queued[key] = nil
for i = #order, 1, -1 do
if order[i] == key then table.remove(order, i) end
end
return true
end
-- True while work under this key (or any key, without one) waits for combat to end.
function ns.Waiting(key)
if key then return queued[key] ~= nil end
return #order > 0
end
-- Runs the waiting work in order, each job taken off the queue before it runs, so one that
-- fails (reported, not hidden) can't jam the rest. A job may queue more: if combat started
-- again, that waits for the next end.
ns.On("PLAYER_REGEN_ENABLED", function()
while order[1] and not InCombatLockdown() do
local key = table.remove(order, 1)
local fn = queued[key]
queued[key] = nil
if fn then xpcall(fn, geterrorhandler()) end
end
end)
---------------------------------------------------------------------------
-- Slash commands
---------------------------------------------------------------------------
local function Print(msg)
print("|cff66ccffKeystance|r: " .. msg)
end
ns.Print = Print
-- Adds one of Keystance's pop-ups to Blizzard's StaticPopupDialogs the first time it's
-- needed (never assigning the table itself: that taints it), with the settings they share.
function ns.Dialog(which, def)
if not StaticPopupDialogs[which] then
def.timeout, def.whileDead, def.hideOnEscape, def.preferredIndex = 0, true, true, 3
StaticPopupDialogs[which] = def
end
return StaticPopupDialogs[which]
end
-- Where everyday messages go ("Prot applied", "Gear: 3 items put on"): "screen" (the
-- default: briefly at the top of the screen, where the game's own notices fade), "chat" or
-- "quiet". Problems, and answers to /kst commands, always use ns.Print (chat).
function ns.MessagesMode()
return ns.db and ns.db.settings.messages or "screen"
end
function ns.Notify(msg)
local mode = ns.MessagesMode()
if mode == "quiet" then return end
if mode == "screen" and UIErrorsFrame and UIErrorsFrame.AddMessage then
UIErrorsFrame:AddMessage("Keystance: " .. msg, 1, 0.82, 0)
return
end
Print(msg)
end
-- Plays the first of the game's sounds (SOUNDKIT names) this client has, on the Sound
-- Effects channel, so the player's own volume and mute apply. Names, not numbers: the list
-- differs between clients, and a missing one just tries the next.
function ns.Sound(names)
local kit = SOUNDKIT
if type(kit) ~= "table" or not PlaySound then return end
for _, name in ipairs(names) do
local id = kit[name]
if id then
PlaySound(id, "SFX")
return
end
end
end
local commands = {}
function commands.minimap()
ns.SetMinimapButton(not ns.MinimapButtonOn())
Print(ns.MinimapButtonOn() and L["Minimap button shown."] or L["Minimap button hidden. /kst minimap brings it back."])
end
function commands.skin(arg)
arg = arg:lower()
if arg == "" then
return Print(L["Look: %s (in use: %s). Choose with /kst skin auto | classic | ellesmere | elvui"]
:format(ns.db.settings.skin, ns.SkinName()))
end
if not ns.ChooseSkin(arg) then
return Print(L["Unknown look '%s'. Choose auto, classic, ellesmere or elvui."]:format(arg))
end
end
function commands.options()
ns.OpenOptions()
end
function commands.mem()
UpdateAddOnMemoryUsage()
Print(L["Memory: %.1f KB"]:format(GetAddOnMemoryUsage(ADDON)))
end
function commands.help()
Print(L["by Kadmai. /kst opens Keystance. Profiles: /kst save Name | apply Name | undo | restore | profiles | ownkeys | ranks | auto on/off"])
Print(L["Also: /kst switcher on/off/lock | guide | options | minimap | skin | mem | help"])
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
-- Lets other files add a slash command (/kst name args).
function ns.AddCommand(name, fn)
commands[name] = fn
end
commands[""] = function() ns.ToggleWindow() end
SLASH_KEYSTANCE1, SLASH_KEYSTANCE2 = "/kst", "/keystance"
SlashCmdList.KEYSTANCE = function(msg)
local cmd, arg = msg:match("^%s*(%S*)%s*(.-)%s*$")
local fn = commands[cmd:lower()] or commands.help
fn(arg)
end