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
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
local overlay = require("lib.overlay")
local rotation = require("lib.rotation")
local cfg = rb.ui.schema({
enabled = rb.ui.toggle(true, { label = "Turbo keypress on", group = "Turbo keypress" }),
keys = rb.ui.list(
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Minus", "Equal",
"R", "F", "G", "T", "Z", "X", "C", "V", "Mouse5" },
{
label = "Keys",
placeholder = "key, or key:ms for its own gap",
tooltip = "A key name, or key:ms to give that key its own gap between presses, such as 3:250",
group = "Turbo keypress",
}
),
start_delay = rb.ui.slider(90, { min = 0, max = 500, suffix = "ms", label = "Hold before repeat", group = "Turbo keypress" }),
hold = rb.ui.slider(40, { min = 10, max = 150, suffix = "ms", label = "Press length", group = "Turbo keypress" }),
gap = rb.ui.slider(40, { min = 10, max = 400, suffix = "ms", label = "Between presses", group = "Turbo keypress" }),
toggle_key = rb.ui.keybind("F8", { label = "Toggle turbo keypress", group = "Turbo keypress" }),
afk_enabled = rb.ui.toggle(false, { label = "Anti-AFK on", group = "Anti-AFK" }),
afk_hotkey = rb.ui.keybind("F9", { label = "Toggle anti-AFK", group = "Anti-AFK" }),
afk_interval_s = rb.ui.slider(180, { min = 30, max = 600, suffix = "s", label = "Nudge interval", group = "Anti-AFK" }),
afk_key = rb.ui.keybind("End", {
label = "Nudge key",
tooltip = "Pressed when idle. Use a key with no action in WoW's key bindings.",
group = "Anti-AFK",
}),
afk_overlay = rb.ui.toggle(true, { label = "Show indicator", group = "Anti-AFK" }),
rotation_play = rb.ui.keybind("F12", {
label = "Play rotation",
tooltip = "Plays the saved rotation once; pressing it again restarts it",
group = "Rotation",
}),
stop_keys = rb.ui.list({ "W", "A", "S", "D", "Q", "E", "Space" }, {
label = "Stop rotation keys",
placeholder = "key name",
tooltip = "Pressing any of these stops a rotation that is playing",
group = "Rotation",
}),
chat_detect = rb.ui.toggle(true, { label = "Chat detection", group = "Chat safety" }),
chat_overlay = rb.ui.toggle(false, { label = "Show chat state overlay", group = "Chat safety" }),
menu_key = rb.ui.keybind("F10", { label = "Open menu", group = "Menu" }),
})
local function bare(keybind)
return keybind:match("^[^@]+")
end
local keys = {
menu = bare(cfg.menu_key),
turbo = bare(cfg.toggle_key),
afk = bare(cfg.afk_hotkey),
afk_key = bare(cfg.afk_key),
play = bare(cfg.rotation_play),
}
local function square_shows(w, index, color, tolerance)
local x = w.x + index * 4 + 1
return rb.screen.search_color({ x, w.y + 1, x + 1, w.y + 2 }, color, tolerance) ~= nil
end
local function read_strip()
local w = rb.system.window()
if not w or w.width == 0 then
return nil
end
if not square_shows(w, 0, "ff00ff", 0.1) then
return nil
end
if square_shows(w, 6, "ffffff", 0.5) then
return true
end
if square_shows(w, 6, "000000", 0.5) then
return false
end
return nil
end
local strip_ok = false
local function typing()
if not strip_ok then
return false
end
local w = rb.system.window()
return w ~= nil and square_shows(w, 6, "ffffff", 0.5)
end
local function turbo_active()
return cfg.enabled and not (cfg.chat_detect and typing())
end
local turbo_keys = {}
local ability_keys = {}
for _, entry in cfg.keys do
local key, gap = entry:match("^([^:]+):(%d+)$")
key = key or entry
table.insert(turbo_keys, { key = key, gap = tonumber(gap) })
ability_keys[key] = { gap = tonumber(gap) }
end
local function turbo_taps(key, held_ms)
if not cfg.enabled then
return { { at = 0, length = held_ms } }
end
local taps = { { at = 0, length = 0 } }
local at = cfg.start_delay
while at < held_ms do
table.insert(taps, { at = at, length = cfg.hold })
at += cfg.hold + (ability_keys[key].gap or cfg.gap)
end
return taps
end
local stop_keys = {}
for _, key in cfg.stop_keys do
stop_keys[key] = true
end
local last_activity_at = 0
local function mark_activity()
last_activity_at = rb.system.time()
end
local playing = nil
rb.on("input_down", function(key)
mark_activity()
if ability_keys[key] and rotation.saw() then
overlay.recording(rotation.seen())
end
if playing and stop_keys[key] then
playing.cancelled = true
end
end)
rb.on("input_move", mark_activity)
rb.on("input_scroll", function()
mark_activity()
if rotation.saw() then
overlay.recording(rotation.seen())
end
end)
local tasks = {}
local function turbo(key, gap)
rb.bind(key, function()
rb.hid.combo(key)
local running = tasks[key]
if running and running:is_running() then
return
end
local task
task = rb.run(function()
rb.sleep(cfg.start_delay)
while turbo_active() and rb.input.is_down(key) do
rb.hid.down(key)
rb.sleep(cfg.hold)
rb.hid.up(key)
rb.sleep(gap or cfg.gap)
end
if tasks[key] == task then
tasks[key] = nil
end
end)
tasks[key] = task
end, { when = turbo_active })
end
for _, turbo_key in turbo_keys do
turbo(turbo_key.key, turbo_key.gap)
end
rb.bind(cfg.toggle_key, function()
cfg.enabled = not cfg.enabled
overlay.notice(if cfg.enabled then "chip_turbo_on" else "chip_turbo_off",
{ { key = keys.turbo }, if cfg.enabled then "d_turns_off" else "d_turns_on" })
end)
rb.bind(cfg.afk_hotkey, function()
cfg.afk_enabled = not cfg.afk_enabled
overlay.notice(if cfg.afk_enabled then "chip_afk_on" else "chip_afk_off",
{ { key = keys.afk }, if cfg.afk_enabled then "d_turns_off" else "d_turns_on" })
end)
local function seconds(ms)
return string.format("%.1f s", ms / 1000)
end
local function start_recording()
rotation.start()
overlay.recording(0)
end
local function finish_recording(save)
overlay.recording(nil)
if not save then
rotation.cancel()
overlay.notice("chip_cancelled")
return
end
local presses = rotation.stop(ability_keys, turbo_taps)
if presses == 0 then
overlay.notice("chip_nothing")
return
end
overlay.notice("chip_saved", {
{ text = tostring(presses), color = "#f0c060", size = 13 }, "d_presses",
{ text = seconds(rotation.length()), color = "#f0c060", size = 13 }, "d_dot",
{ key = keys.play }, "d_plays_it",
}, 4000)
end
rb.bind(cfg.rotation_play, function()
if rotation.recording() then
return
end
if not rotation.ready() then
overlay.notice("chip_empty")
return
end
local previous = playing
if previous then
previous.cancelled = true
end
local pass = { cancelled = false }
playing = pass
overlay.playing(true)
pass.task = rb.run(function()
while previous and previous.task:is_running() do
rb.sleep(5)
end
rotation.play(function()
return not pass.cancelled and not (cfg.chat_detect and typing())
end)
if playing == pass then
playing = nil
overlay.playing(false)
end
end)
end)
local function install_addon()
local handle = rb.window.get_foreground()
if not handle then
return
end
local pid = rb.window.get_pid(handle)
rb.run(function()
local result = rb.system.exec(
"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
.. " -NoProfile -ExecutionPolicy Bypass -File install.ps1 " .. pid,
{ cwd = rb.file.script_dir() .. "\\addon", timeout = 15000 }
)
if result.exit ~= 0 then
rb.log.error("addon install: " .. result.stderr)
overlay.toast(false, result.stderr:match("[^\r\n]+") or "exit " .. result.exit)
return
end
rb.log.info("addon installed to " .. result.stdout)
overlay.toast(true)
end)
end
overlay.init(cfg, keys, {
record = start_recording,
install = install_addon,
length = function()
return if rotation.ready() then seconds(rotation.length()) else nil
end,
})
rb.bind(cfg.menu_key, function()
if not rotation.recording() then
overlay.toggle_menu()
end
end)
for _, key in { "Up", "Down", "Left", "Right" } do
rb.bind(key, function()
overlay.menu_key(key)
end, { when = overlay.menu_open })
end
for _, key in { "Enter", "Escape" } do
rb.bind(key, function()
if rotation.recording() then
finish_recording(key == "Enter")
else
overlay.menu_key(key)
end
end, {
when = function()
return overlay.menu_open() or rotation.recording()
end,
})
end
rb.on("blur", function()
for _, turbo_key in turbo_keys do
rb.hid.up(turbo_key.key)
end
overlay.hide()
end)
local welcomed = false
rb.on("focus", function()
overlay.show()
if not welcomed then
welcomed = true
overlay.notice("chip_welcome", { { key = keys.menu }, "d_opens_menu" }, 4000)
end
end)
rb.timer.every(250, function()
if cfg.afk_enabled then
local now = rb.system.time()
if (now - last_activity_at) >= cfg.afk_interval_s * 1000 then
rb.hid.combo(bare(cfg.afk_key))
last_activity_at = now
end
end
local chat = read_strip()
strip_ok = chat ~= nil
overlay.sync(chat)
end)