-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathDebug.lua
More file actions
734 lines (666 loc) · 30.5 KB
/
Copy pathDebug.lua
File metadata and controls
734 lines (666 loc) · 30.5 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
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
-- ============================================================
-- Debug.lua — developer diagnostics: the debug-state setters (shared by
-- the /cbdebug subcommands and the Settings → Debug tab), the KnownBots
-- popup window, the /cbdebug chat dump, the /cbtiming whisper-latency
-- measurer, and the /cbinspect NotifyInspect trace.
-- ============================================================
local NS = CleanBotNS
-- ============================================================
-- KnownBots popup window (created lazily, reused)
-- ============================================================
local debugKnownBotsFrame = nil
--- Builds the multi-line diagnostic text dump: handshake state, the last raw
--- STATES payload, and per-bot parsed combat strategy flags.
---@return string The formatted report, or a placeholder when no bots are known.
local function CB_FormatKnownBots()
local lines = {
"=== Handshake ===",
"bridgeReady: " .. tostring(NS.bridgeReady),
"Last HELLO_ACK: " .. (NS.lastHelloAck or "(none received yet)"),
"",
"=== Debug Overrides ===",
"Bridge override : " .. (NS.debugBridgeOverride or "none (auto)"),
"Simulate mode : " .. (NS.debugSimulate and "ON" or "OFF"),
"",
"=== Last raw STATES payload ===",
NS.lastRawStates or "(none received yet)",
"",
"=== Parsed KnownBots ===",
"",
}
local count = 0
for key, bot in pairs(CleanBot_PartyBots) do
count = count + 1
lines[#lines + 1] = string.format("[%d] %s (%s)", count, bot.name or key, bot.class or "?")
if bot.combat then
local active, inactive, unknown = {}, {}, {}
for field, val in pairs(bot.combat) do
if val == true then
active[#active + 1] = field
elseif val == false then
inactive[#inactive + 1] = field
else
unknown[#unknown + 1] = field
end
end
table.sort(active); table.sort(inactive); table.sort(unknown)
if #active > 0 then lines[#lines + 1] = " |cff00ff00ON |r " .. table.concat(active, " ") end
if #inactive > 0 then lines[#lines + 1] = " |cffff4444OFF|r " .. table.concat(inactive, " ") end
if #unknown > 0 then lines[#lines + 1] = " |cffaaaaaa? |r " .. table.concat(unknown, " ") end
else
lines[#lines + 1] = " (no combat data)"
end
lines[#lines + 1] = ""
end
if count == 0 then return "(CleanBot_PartyBots is empty)" end
return table.concat(lines, "\n")
end
--- Opens (creating once, then reusing) the KnownBots diagnostic popup window
--- and fills it with the CB_FormatKnownBots report.
NS.CB_ShowDebugKnownBots = function()
local screenH = UIParent:GetHeight()
local winW = 520
local winH = math.floor(screenH / 2)
local titleH = 24
local footerH = 32
local padH = 8
if not debugKnownBotsFrame then
local f = CreateFrame("Frame", "CleanBotDebugKnownBots", UIParent)
f:SetSize(winW, winH)
f:SetPoint("CENTER")
f:SetMovable(true)
f:SetToplevel(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", f.StartMoving)
f:SetScript("OnDragStop", f.StopMovingOrSizing)
f:SetFrameStrata("DIALOG")
NS.CB_ApplyFrameSkin(f, 1)
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOP", f, "TOP", 0, -8)
title:SetText("CleanBot — KnownBots")
local closeBtn = NS.CB_CreateButton(f, nil, "Close", 80, 22, function() f:Hide() end)
closeBtn:SetPoint("BOTTOM", f, "BOTTOM", 0, 6)
local sf = CreateFrame("ScrollFrame", "CleanBotDebugScroll", f,
"UIPanelScrollFrameTemplate")
sf:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -(titleH + padH))
sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -28, footerH + padH)
local child = CreateFrame("Frame", nil, sf)
child:SetWidth(sf:GetWidth() or (winW - 36))
sf:SetScrollChild(child)
local txt = child:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
txt:SetPoint("TOPLEFT", child, "TOPLEFT", 4, -4)
txt:SetWidth((sf:GetWidth() or (winW - 36)) - 8)
txt:SetJustifyH("LEFT")
txt:SetNonSpaceWrap(false)
f.scrollFrame = sf
f.scrollChild = child
f.textObj = txt
debugKnownBotsFrame = f
end
local f = debugKnownBotsFrame
local txt = f.textObj
txt:SetText(CB_FormatKnownBots())
f.scrollChild:SetHeight(math.max(txt:GetStringHeight() + 8, 1))
f:SetHeight(winH)
f:Show()
f.scrollFrame:SetVerticalScroll(0)
end
-- ============================================================
-- /cbframes — hover over a stuck widget then run this to see
-- which frame is capturing the mouse and why.
-- ============================================================
SLASH_CBFRAMES1 = "/cbframes"
SlashCmdList["CBFRAMES"] = function()
local focus = GetMouseFocus and GetMouseFocus()
if focus then
print(string.format("MouseFocus: %s strata=%s level=%d mouseEnabled=%s",
tostring(focus:GetName() or "(unnamed)"),
tostring(focus:GetFrameStrata()),
focus:GetFrameLevel(),
tostring(focus:IsMouseEnabled())))
else
print("MouseFocus: nil (mouse not over any mouse-enabled frame)")
end
-- Report key manage-tab frame states.
local function fr(label, f)
if not f then print(label .. ": nil"); return end
print(string.format(" %s: strata=%s level=%d mouseEnabled=%s visible=%s",
label, tostring(f:GetFrameStrata()), f:GetFrameLevel(),
tostring(f:IsMouseEnabled()), tostring(f:IsVisible())))
end
print("-- Manage tab frames --")
fr("managePanel", NS.managePanel)
fr("managePanel.iborder", NS.managePanel and NS.managePanel.iborder)
fr("managePanel.oborder", NS.managePanel and NS.managePanel.oborder)
fr("manageScrollFrame", NS.manageScrollFrame)
fr("manageScrollChild", NS.manageScrollChild)
local sf = NS.manageScrollFrame
if sf then
local bar = _G["CleanBotManageScrollFrameScrollBar"]
fr("ScrollBar", bar)
end
end
-- ============================================================
-- Debug state setters — the single source of truth for changing debug
-- options. Both the /cbdebug subcommands and the Settings → Debug tab route
-- through these, so chat and UI always agree. Each setter persists to
-- SavedVars, prints the change, and refreshes the Debug tab if it's built
-- (NS.CB_RefreshDebugTab is defined by SettingsTab.lua at build time).
-- ============================================================
local function CB_RefreshDebugTabIfBuilt()
if NS.CB_RefreshDebugTab then NS.CB_RefreshDebugTab() end
end
-- Sets/clears the bridge override and re-syncs so the new effective state
-- applies immediately (roster/inventory/quests re-fetch via the right path)
-- instead of waiting for the next window open.
---@param value string|nil "present" | "absent" | nil (= auto, follow handshake).
NS.CB_SetBridgeOverride = function(value)
NS.debugBridgeOverride = value
if CleanBot_SavedVars then CleanBot_SavedVars.debugBridgeOverride = value end
if value == "absent" then
NS.CB_Print("Bridge override set to |cffff4444absent|r (whisper fallback).")
elseif value == "present" then
NS.CB_Print("Bridge override set to |cff00ff00present|r (bridge path).")
else
NS.CB_Print("Bridge override cleared — following real handshake (" .. NS.bridgeState .. ").")
end
NS.CB_RequestSync()
CB_RefreshDebugTabIfBuilt()
end
---@param on boolean Whether CB_SendBotCommand prints instead of sending.
NS.CB_SetDebugSimulate = function(on)
NS.debugSimulate = on and true or false
if CleanBot_SavedVars then CleanBot_SavedVars.debugSimulate = NS.debugSimulate end
NS.CB_Print("Simulate mode: " .. (NS.debugSimulate and "|cff00ff00ON|r" or "|cffff4444OFF|r") .. ".")
CB_RefreshDebugTabIfBuilt()
end
---@param on boolean Whether strategy toggles log optimistic-vs-actual mismatches.
NS.CB_SetDebugVerify = function(on)
NS.debugVerify = on and true or false
if CleanBot_SavedVars then CleanBot_SavedVars.debugVerify = NS.debugVerify end
NS.CB_Print("Strategy verify logging: " .. (NS.debugVerify and "|cff00ff00ON|r" or "|cffff4444OFF|r") .. ".")
CB_RefreshDebugTabIfBuilt()
end
-- Shows/hides the Settings → Debug sub-tab (persisted). The tab widget itself
-- is managed by SettingsTab.lua via NS.CB_SetDebugTabVisible.
---@param on boolean Whether the Debug sub-tab is available in Settings.
NS.CB_SetDebugTabEnabled = function(on)
NS.debugTabEnabled = on and true or false
if CleanBot_SavedVars then CleanBot_SavedVars.debugTabEnabled = NS.debugTabEnabled end
if NS.CB_SetDebugTabVisible then NS.CB_SetDebugTabVisible(NS.debugTabEnabled) end
NS.CB_Print("Debug tab " .. (NS.debugTabEnabled
and "|cff00ff00enabled|r — see Settings."
or "|cffff4444disabled|r."))
end
-- ============================================================
-- /cbdebug — quick party/cache dump + debug option subcommands
--
-- /cbdebug enable — show the Debug sub-tab in Settings (persisted)
-- /cbdebug disable — hide the Debug sub-tab
-- /cbdebug bridge off — force bridge absent (uses whisper fallback)
-- /cbdebug bridge on — force bridge present (uses bridge path)
-- /cbdebug bridge reset — clear override; follow real handshake result
-- /cbdebug simulate — toggle simulate mode (print commands instead of sending)
-- /cbdebug verify — toggle strategy-toggle mismatch logging
-- ============================================================
SLASH_CBDEBUG1 = "/cbdebug"
SlashCmdList["CBDEBUG"] = function(msg)
msg = (msg or ""):lower():match("^%s*(.-)%s*$")
if msg == "enable" then
NS.CB_SetDebugTabEnabled(true)
return
elseif msg == "disable" then
NS.CB_SetDebugTabEnabled(false)
return
elseif msg == "bridge off" then
NS.CB_SetBridgeOverride("absent")
return
elseif msg == "bridge on" then
NS.CB_SetBridgeOverride("present")
return
elseif msg == "bridge reset" then
NS.CB_SetBridgeOverride(nil)
return
elseif msg == "simulate" then
NS.CB_SetDebugSimulate(not NS.debugSimulate)
return
elseif msg == "verify" then
NS.CB_SetDebugVerify(not NS.debugVerify)
return
end
-- Default: quick group/cache/state dump.
print(string.format("Bridge: real=%s override=%s simulate=%s verify=%s tabEnabled=%s loginPhase=%s",
tostring(NS.bridgeState),
tostring(NS.debugBridgeOverride or "none"),
tostring(NS.debugSimulate),
tostring(NS.debugVerify),
tostring(NS.debugTabEnabled),
tostring(NS.loginPhaseActive)))
local prefix, n = NS.CB_GroupInfo()
print(string.format("Group: type=%s count=%d (party=%d raid=%d)",
prefix, n, GetNumPartyMembers(), GetNumRaidMembers()))
-- Walk the resolved group (skips the player), reporting per-member state.
NS.CB_ForEachGroupMember(function(unit, name)
local _, class = UnitClass(unit)
local key = name and strlower(name)
print(string.format(" %s name=%s exists=%s isPlayer=%s class=%s inCache=%s probed=%s awaiting=%s",
unit,
tostring(name),
tostring(UnitExists(unit)),
tostring(UnitIsPlayer(unit)),
tostring(class),
tostring(key and CleanBot_PartyBots[key] ~= nil),
tostring(key and NS.probed[key] == true),
tostring(key and NS.awaitingProbe[key] == true)))
end)
print("KnownBots cache:")
local count = 0
for k, v in pairs(CleanBot_PartyBots) do
print(" " .. k .. " = " .. tostring(v.class))
count = count + 1
end
if count == 0 then print(" (empty)") end
end
-- ============================================================
-- /cbtiming — measures how fast bots whisper replies, to tune
-- NS.WHISPER_SILENCE (the collection silence timeout in Bridge.lua).
--
-- /cbtiming [runs] [botName] — default 3 runs against the selected bot
--
-- Each run whispers "items" and records:
-- first = time from send to the FIRST reply line
-- gaps = time between consecutive reply lines within the burst
-- The silence timeout must cover max(first, maxGap) — it resets on every
-- line, so total reply length is irrelevant. Reports per-run and aggregate
-- stats plus a suggested timeout (2x the worst observation, for headroom).
-- ============================================================
---@class CB_TimingSession
---@field key string
---@field name string
---@field runsLeft number
---@field results table
---@field run table? Per-query stats for the in-flight 'items' request.
local timing = nil ---@type CB_TimingSession? -- active session, or nil
local timingEvents = CreateFrame("Frame")
timingEvents:RegisterEvent("CHAT_MSG_WHISPER")
timingEvents:SetScript("OnEvent", function(_, _, msg, sender)
if not timing then return end
local run = timing.run
if not run then return end
if strlower(sender or "") ~= timing.key then return end
local now = GetTime()
if not run.firstAt then
run.firstAt = now
run.first = now - run.sendTime
else
local gap = now - run.lastAt
run.gapSum = run.gapSum + gap
run.gapN = run.gapN + 1
if gap > run.maxGap then run.maxGap = gap end
end
run.lastAt = now
run.lines = run.lines + 1
end)
local timingTicker = CreateFrame("Frame")
timingTicker:Hide()
local function timingStartRun()
if not timing then return end
timing.run = {
sendTime = GetTime(),
lines = 0,
gapSum = 0,
gapN = 0,
maxGap = 0,
}
NS.CB_SendBotCommand(timing.name, "items")
end
local function timingReport()
timingTicker:Hide()
if not timing then return end
local rs = timing.results
timing = nil
if #rs == 0 then
NS.CB_Print("[timing] No replies received — is the bot online and whispering?")
return
end
local firstSum, firstMax, gapSum, gapN, gapMax = 0, 0, 0, 0, 0
for i, r in ipairs(rs) do
local avgGap = r.gapN > 0 and (r.gapSum / r.gapN) or 0
print(string.format(" run %d: first %.0f ms, %d lines, avg gap %.0f ms, max gap %.0f ms",
i, r.first * 1000, r.lines, avgGap * 1000, r.maxGap * 1000))
firstSum = firstSum + r.first
if r.first > firstMax then firstMax = r.first end
gapSum = gapSum + r.gapSum
gapN = gapN + r.gapN
if r.maxGap > gapMax then gapMax = r.maxGap end
end
local avgFirst = firstSum / #rs
local avgGap = gapN > 0 and (gapSum / gapN) or 0
local worst = math.max(firstMax, gapMax)
NS.CB_Print(string.format(
"[timing] avg first reply %.0f ms (max %.0f) | avg line gap %.0f ms (max %.0f) | suggested NS.WHISPER_SILENCE >= %.2f s (2x worst; currently %.2f s)",
avgFirst * 1000, firstMax * 1000, avgGap * 1000, gapMax * 1000,
worst * 2, NS.WHISPER_SILENCE or 0))
end
timingTicker:SetScript("OnUpdate", function()
if not timing then timingTicker:Hide(); return end
local run = timing.run
if not run then timingTicker:Hide(); return end
local now = GetTime()
if run.firstAt then
-- Run is complete after a generous fixed measurement window of silence
-- (independent of NS.WHISPER_SILENCE, so the tool stays valid while tuning).
if now - run.lastAt > 1.5 then
timing.results[#timing.results + 1] = run
timing.run = nil
timing.runsLeft = timing.runsLeft - 1
if timing.runsLeft > 0 then timingStartRun() else timingReport() end
end
elseif now - run.sendTime > 5 then
-- No reply at all: count the run as failed and move on.
NS.CB_Print("[timing] run got no reply within 5 s — skipping.")
timing.run = nil
timing.runsLeft = timing.runsLeft - 1
if timing.runsLeft > 0 then timingStartRun() else timingReport() end
end
end)
-- Starts a timing measurement session. Shared by /cbtiming and the Settings →
-- Debug tab's "Measure Reply Timing" button.
---@param runs number? How many "items" queries to run (default 3).
---@param botName string? Target bot name; defaults to the selected bot, then any known bot.
NS.CB_RunTimingMeasure = function(runs, botName)
if timing then NS.CB_Print("[timing] already measuring — wait for the report.") return end
if NS.debugSimulate then
NS.CB_Print("[timing] simulate mode is ON — commands aren't actually sent. Turn simulate off first.")
return
end
local key, entry
if botName and botName ~= "" then
key = strlower(botName)
entry = CleanBot_PartyBots[key]
else
key = NS.selectedBotKey
entry = key and CleanBot_PartyBots[key]
if not entry then
for k, e in pairs(CleanBot_PartyBots) do key = k; entry = e; break end
end
end
if not entry then
NS.CB_Print("[timing] no known bot to measure (open the Individual tab or pass a name).")
return
end
timing = {
key = key,
name = entry.name,
runsLeft = runs or 3,
results = {},
}
NS.CB_Print(string.format("[timing] measuring %s with %d 'items' queries...", entry.name, timing.runsLeft))
timingStartRun()
timingTicker:Show()
end
SLASH_CBTIMING1 = "/cbtiming"
SlashCmdList["CBTIMING"] = function(msg)
local runsArg, nameArg = msg:match("^%s*(%d*)%s*(%S*)")
NS.CB_RunTimingMeasure(tonumber(runsArg), nameArg ~= "" and nameArg or nil)
end
-- ============================================================
-- /cbinspect — traces every NotifyInspect call (from ANY addon), to diagnose
-- something other than CleanBot inspecting in the background and evicting the
-- single-unit equipment-inspect cache (rich tooltips reverting to generic).
-- source=CleanBot → our own inspect (Equip.lua / SelectBot / OnEnter reclaim)
-- source=EXTERNAL → another addon is inspecting.
-- The printed stack line identifies the caller's file.
-- ============================================================
NS.debugInspectTrace = false -- session-only; deliberately NOT persisted
local cbInspectHooked = false
-- Enables/disables the NotifyInspect trace. Shared by /cbinspect and the
-- Settings → Debug tab checkbox. The hooksecurefunc is installed once on
-- first enable (hooks can't be removed; the flag gates the output).
---@param on boolean Whether to print every NotifyInspect call.
NS.CB_SetInspectTrace = function(on)
NS.debugInspectTrace = on and true or false
if NS.debugInspectTrace and not cbInspectHooked then
cbInspectHooked = true
hooksecurefunc("NotifyInspect", function(unit)
if not NS.debugInspectTrace then return end
local stack = debugstack(2, 8, 0) or ""
-- CleanBot's files all live under the ...\CleanBot\ folder, so a stack
-- containing "CleanBot" is our own call; anything else is external.
local mine = stack:find("CleanBot", 1, true) ~= nil
local name = (unit and UnitName(unit)) or "?"
print(string.format("|cff%s[CBInspect]|r NotifyInspect(%s = %s) source=%s",
mine and "00ff00" or "ff4444",
tostring(unit), tostring(name),
mine and "CleanBot" or "EXTERNAL"))
-- Print the first caller frame (skip the hook frame itself in Debug.lua)
-- so an external addon's file/line is visible.
for line in stack:gmatch("[^\r\n]+") do
if not line:find("Debug.lua", 1, true) then
print(" " .. line)
break
end
end
end)
end
if NS.debugInspectTrace then
NS.CB_Print("NotifyInspect trace |cff00ff00ON|r — watch for |cffff4444source=EXTERNAL|r lines.")
else
NS.CB_Print("NotifyInspect trace |cffff4444OFF|r.")
end
if NS.CB_RefreshDebugTab then NS.CB_RefreshDebugTab() end
end
SLASH_CBINSPECT1 = "/cbinspect"
SlashCmdList["CBINSPECT"] = function()
NS.CB_SetInspectTrace(not NS.debugInspectTrace)
end
-- ── /cbicons — scrollable icon browser ───────────────────────────────────
-- Browse every usable icon (the macro-icon database via GetMacroIconInfo /
-- GetMacroItemIconInfo) in a scrollable grid. Type to filter by name; hover for
-- the full path; click to print a paste-ready "Interface\Icons\..." path to chat.
local PAD, CELL, ICON, COLS, ROWS = 14, 40, 36, 12, 10
local iconBrowser ---@type table|nil
local allIcons ---@type string[]|nil Every macro icon texture, built once.
-- Normalize a macro-icon texture to a paste-ready full path. GetMacroIconInfo
-- returns either a bare name ("INV_Misc_Coin_01") or a full path; SetTexture
-- accepts both, but our code wants the "Interface\Icons\..." form.
---@param tex string Raw texture as returned by the macro-icon APIs.
---@return string Full texture path.
local function CB_IconFullPath(tex)
if tex:find("\\", 1, true) then return tex end
return "Interface\\Icons\\" .. tex
end
-- Builds the flat list of every macro icon once, deduped.
local function CB_BuildIconList()
if allIcons then return end
allIcons = {}
local seen = {}
local function add(tex)
if not tex or type(tex) ~= "string" or seen[tex] then return end
seen[tex] = true
allIcons[#allIcons + 1] = tex
end
for i = 1, GetNumMacroIcons() do add(GetMacroIconInfo(i)) end
for i = 1, GetNumMacroItemIcons() do add(GetMacroItemIconInfo(i)) end
end
local function CB_BuildIconBrowser()
CB_BuildIconList()
local f = CreateFrame("Frame", "CleanBotIconBrowser", UIParent)
f:SetFrameStrata("DIALOG")
f:SetWidth(PAD * 2 + COLS * CELL + 24) -- +24 leaves room for the scrollbar
f:SetHeight(PAD * 2 + 56 + ROWS * CELL)
f:SetPoint("CENTER")
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", f.StartMoving)
f:SetScript("OnDragStop", f.StopMovingOrSizing)
NS.CB_ApplyFrameSkin(f, 0)
local closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -2)
closeBtn:SetScript("OnClick", function() f:Hide() end)
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOPLEFT", f, "TOPLEFT", PAD, -PAD)
title:SetText("Icon Browser — click to print path")
f.title = title
local search = NS.CB_CreateEditBox(f, "CleanBotIconBrowserSearch", 200, 20)
search:SetPoint("TOPLEFT", f, "TOPLEFT", PAD + 4, -(PAD + 22))
search:SetAutoFocus(false)
local scroll = CreateFrame("ScrollFrame", "CleanBotIconBrowserScroll", f, "FauxScrollFrameTemplate")
scroll:SetPoint("TOPLEFT", f, "TOPLEFT", PAD, -(PAD + 50))
scroll:SetWidth(COLS * CELL)
scroll:SetHeight(ROWS * CELL)
local filtered = allIcons or {}
local pool = {}
local function RefreshGrid()
local total = #filtered
local numLines = math.ceil(total / COLS)
-- Surface the database size (and current filter count) so an empty macro-icon DB is obvious.
f.title:SetText(string.format("Icon Browser — %d icons%s", #(allIcons or {}),
(#filtered ~= #(allIcons or {})) and (" (" .. total .. " shown)") or ""))
FauxScrollFrame_Update(scroll, numLines, ROWS, CELL)
local offset = FauxScrollFrame_GetOffset(scroll)
for idx = 1, COLS * ROWS do
local btn = pool[idx]
local tex = filtered[offset * COLS + idx]
if tex then
btn.tex:SetTexture(tex)
btn.rawTex = tex
btn:Show()
else
btn:Hide()
end
end
end
for idx = 1, COLS * ROWS do
local col, row = (idx - 1) % COLS, math.floor((idx - 1) / COLS)
-- Parent to f, NOT scroll: FauxScrollFrame_Update hides the scroll frame when the results fit
-- without scrolling, which would hide buttons parented to it. Anchor cross-frame to scroll so
-- they still position correctly. (Same trap CB_CreateSelectList documents.)
local btn = CreateFrame("Button", nil, f)
btn:SetSize(ICON, ICON)
btn:SetPoint("TOPLEFT", scroll, "TOPLEFT",
col * CELL + (CELL - ICON) / 2, -(row * CELL + (CELL - ICON) / 2))
btn.tex = btn:CreateTexture(nil, "ARTWORK")
btn.tex:SetAllPoints()
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
hl:SetAllPoints()
hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
hl:SetBlendMode("ADD")
NS.CB_AttachTooltip(btn, function(tt, self)
if not self.rawTex then return false end
tt:AddLine(CB_IconFullPath(self.rawTex), 1, 1, 1)
end)
btn:SetScript("OnClick", function(self)
if self.rawTex then print("|cff66ccffCleanBot icon:|r " .. CB_IconFullPath(self.rawTex)) end
end)
pool[idx] = btn
end
scroll:SetScript("OnVerticalScroll", function(self, offset)
FauxScrollFrame_OnVerticalScroll(self, offset, CELL, RefreshGrid)
end)
-- Filter on type: substring match against the raw texture name, reset to top.
search:SetScript("OnTextChanged", function(self)
local q = strlower(strtrim(self:GetText() or ""))
if q == "" then
filtered = allIcons or {}
else
filtered = {}
for _, tex in ipairs(allIcons) do
if strfind(strlower(tex), q, 1, true) then filtered[#filtered + 1] = tex end
end
end
local bar = _G["CleanBotIconBrowserScrollScrollBar"]
if bar then bar:SetValue(0) end
RefreshGrid()
end)
search:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
RefreshGrid()
f:Hide() -- start hidden so the first toggle shows it
return f
end
NS.CB_ToggleIconBrowser = function()
if not iconBrowser then iconBrowser = CB_BuildIconBrowser() end
if iconBrowser:IsShown() then iconBrowser:Hide() else iconBrowser:Show() end
end
SLASH_CBICONS1 = "/cbicons"
SlashCmdList["CBICONS"] = function() NS.CB_ToggleIconBrowser() end
-- ============================================================
-- Layout test buttons — dev tool for spam-testing live margin/padding re-flow.
--
-- Two centered buttons: "Randomize Layout" sets every NS.MARGIN/NS.PADDING side to a
-- random value; "Reset Layout" restores the defaults. Both persist to SavedVars and
-- apply immediately via the live system (CB_EmitDisplaySettings), exactly like
-- Settings → Apply. Visibility is the "Layout Test Buttons" checkbox in Settings →
-- Debug (persisted in CleanBot_SavedVars.layoutTestButtons; hidden by default).
-- ============================================================
local layoutTestFrame -- centered button container (built at PLAYER_ENTERING_WORLD)
NS.layoutTestButtons = false -- hidden by default; enable via Settings → Debug (persisted)
-- Mirrors the Settings Apply persistence, then emits the live re-flow.
local function CB_LayoutTestPersistApply()
if not CleanBot_SavedVars then return end
CleanBot_SavedVars.margins = CleanBot_SavedVars.margins or {}
CleanBot_SavedVars.padding = CleanBot_SavedVars.padding or {}
for k, v in pairs(NS.MARGIN) do
CleanBot_SavedVars.margins[k] = { top = v.top, bottom = v.bottom, left = v.left, right = v.right }
end
for k, v in pairs(NS.PADDING) do
CleanBot_SavedVars.padding[k] = { top = v.top, bottom = v.bottom, left = v.left, right = v.right }
end
if NS.CB_EmitDisplaySettings then NS.CB_EmitDisplaySettings(true) end
end
local function CB_LayoutTestRandomize()
for _, v in pairs(NS.MARGIN) do
v.top, v.bottom, v.left, v.right =
math.random(0, 16), math.random(0, 16), math.random(0, 16), math.random(0, 16)
end
for _, v in pairs(NS.PADDING) do
v.top, v.bottom, v.left, v.right =
math.random(0, 30), math.random(0, 30), math.random(0, 30), math.random(0, 30)
end
CB_LayoutTestPersistApply()
NS.CB_Print("Layout randomized.")
end
local function CB_LayoutTestReset()
for k, v in pairs(NS.MARGIN) do
local d = NS.MARGIN_DEFAULTS[k]
if d then v.top, v.bottom, v.left, v.right = d.top, d.bottom, d.left, d.right end
end
for k, v in pairs(NS.PADDING) do
local d = NS.PADDING_DEFAULTS[k]
if d then v.top, v.bottom, v.left, v.right = d.top, d.bottom, d.left, d.right end
end
CB_LayoutTestPersistApply()
NS.CB_Print("Layout reset to defaults.")
end
--- Shows/hides the centered test buttons and persists the choice. Toggled from Settings → Debug.
---@param on boolean
NS.CB_SetLayoutTestButtons = function(on)
NS.layoutTestButtons = on and true or false
if CleanBot_SavedVars then CleanBot_SavedVars.layoutTestButtons = NS.layoutTestButtons end
if layoutTestFrame then
if NS.layoutTestButtons then layoutTestFrame:Show() else layoutTestFrame:Hide() end
end
if NS.CB_RefreshDebugTab then NS.CB_RefreshDebugTab() end
end
-- Built at PLAYER_ENTERING_WORLD (ElvUI + widget factories fully initialized), like the action bar.
local layoutTestLoader = CreateFrame("Frame")
layoutTestLoader:RegisterEvent("PLAYER_ENTERING_WORLD")
layoutTestLoader:SetScript("OnEvent", function(self)
self:UnregisterEvent("PLAYER_ENTERING_WORLD")
layoutTestFrame = CreateFrame("Frame", "CleanBotLayoutTestFrame", UIParent)
layoutTestFrame:SetFrameStrata("FULLSCREEN_DIALOG")
layoutTestFrame:SetSize(252, 28)
layoutTestFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
local rnd = NS.CB_CreateButton(layoutTestFrame, "CleanBotLayoutTestRandom", "Randomize Layout", 140, 24, CB_LayoutTestRandomize)
rnd:SetPoint("LEFT", layoutTestFrame, "LEFT", 0, 0)
local rst = NS.CB_CreateButton(layoutTestFrame, "CleanBotLayoutTestReset", "Reset Layout", 104, 24, CB_LayoutTestReset)
rst:SetPoint("LEFT", rnd, "RIGHT", 8, 0)
-- Restore saved visibility (hidden unless explicitly turned on) and sync the Debug checkbox.
if CleanBot_SavedVars and CleanBot_SavedVars.layoutTestButtons == true then
NS.layoutTestButtons = true
end
if not NS.layoutTestButtons then layoutTestFrame:Hide() end
if NS.CB_RefreshDebugTab then NS.CB_RefreshDebugTab() end
end)