Repository navigation
Expand file tree
/
Copy pathSelftestLaunch.lua
More file actions
1432 lines (1366 loc) · 61.2 KB
/
Copy pathSelftestLaunch.lua
File metadata and controls
1432 lines (1366 loc) · 61.2 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
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- Manners -- /manners selftest launch: the parts of Manners that have never run
-- in a real client, asked of the one it is running in.
--
-- Several features were written against Blizzard's documentation and the mock
-- in tests/ and have not seen a game: the looks Ribbon, Letter and Leather
-- (Morpheus, masks, the unroll's clipping, tiled strips, animations), the
-- look picker's live card icons and text, the action bar slot of the macro
-- (SetMacroSpell, SetMacroItem, clearing with "", PickupMacro), the key
-- glyphs on the chip for a controller, the tooltip's settings (anchors,
-- Shift), the hand-back with nothing targeted, and the prompt laying itself
-- out again when the display changes. One command asks the client about each
-- and says what it answered, so that a report can be read for what the game
-- did, not for what Manners hoped.
--
-- PASS the client answered as Manners expects
-- CHECK it answered something else, Manners falls back, or only eyes can
-- tell (the line says which)
-- FAIL an error, or a refusal Manners has no way round
--
-- WHAT IT TOUCHES, AND HOW IT PUTS IT BACK. Out of combat only (the command
-- refuses in a fight, and nothing here is asked in one). It casts nothing,
-- says nothing and presses nothing. The prompt's style is switched through
-- every look in one go, inside one call, so no frame is drawn between them,
-- and put back before the call ends; the profile is never written to
-- otherwise. The macro's slot is set, read back and cleared on the player's
-- own Manners macro, and put back to what it showed (what it held is read
-- first); a player with no such macro gets a temporary one named
-- MannersSelfTest, made for this and deleted at the end, and the report says
-- so. The cursor is checked to be empty before the macro is picked up and
-- emptied after. The tooltip is a scratch one of its own: the game's is never
-- touched. Everything else is drawn on a hidden scratch frame made once.
--
-- It is quick: one pass in a single frame (a few milliseconds a look, the
-- first time a look is shown, which builds it) and half a second of listening
-- afterwards for UPDATE_MACROS, which tells a macro call that the client
-- answers once from one it answers forever.
local _, ns = ...
local L = ns.L
local addon = ns.addon
local Selftest = ns.Selftest
if not Selftest or not Selftest.tools then return end
local T = Selftest.tools
local Show, Plain, Ask, MISSING, Field, SomeSpell = T.Show, T.Plain, T.Ask, T.MISSING, T.Field, T.SomeSpell
local InCombatLockdown = _G.InCombatLockdown
local unpack = _G.unpack or table.unpack
local PASS, CHECK, FAIL = "PASS", "CHECK", "FAIL"
local RANK = { [PASS] = 1, [CHECK] = 2, [FAIL] = 3 }
-- Bindings.xml's command.
local COMMAND = "CLICK MannersPrompt:LeftButton"
local MORPHEUS = "Fonts\\MORPHEUS.TTF"
local ART = "Interface\\AddOns\\Manners\\Textures\\"
-- The client languages whose alphabet Morpheus draws (Ribbon, Letter, Leather).
local ROMAN = { enUS = true, enGB = true, deDE = true, esES = true, esMX = true, frFR = true, itIT = true, ptBR = true }
-- The macro made for the test when the player has none of their own.
local TEMP_MACRO = "MannersSelfTest"
-- How long the listener for UPDATE_MACROS stays up after the calls, and from
-- when in it an event counts as the client still answering.
local LISTEN, QUIET_AFTER = 0.5, 0.25
local QUESTION_MARK = 134400
local LAUNCH = {}
Selftest.LAUNCH = LAUNCH
-- run(add, ctx): add(status, value[, label[, id]]). A `late` check runs after
-- the half second of listening, with the rest of the report already made.
local function Launch(id, label, run, late)
LAUNCH[#LAUNCH + 1] = { id = id, label = label, run = run, late = late }
end
local function Clock()
local now = _G.debugprofilestop
if type(now) == "function" then return now() end
return nil
end
-- A method of a client object: MISSING when there is none, else pcall's answer.
local function Call(obj, method, ...)
if type(obj) ~= "table" and type(obj) ~= "userdata" then return MISSING end
local fn = obj[method]
if type(fn) ~= "function" then return MISSING end
return pcall(fn, obj, ...)
end
local function Lower(v) return type(v) == "string" and v:lower() or v end
---------------------------------------------------------------------------
-- the scratch objects, made once for the session and never shown
---------------------------------------------------------------------------
local function Scratch()
local s = Selftest.scratch
if s then return s end
s = {}
s.frame = CreateFrame("Frame", nil, UIParent)
s.frame:Hide()
s.tex = s.frame:CreateTexture(nil, "ARTWORK")
s.fs = s.frame:CreateFontString(nil, "OVERLAY")
s.clip = CreateFrame("Frame", nil, s.frame)
-- The picker's card, in little: an icon, a mask and two lines of text.
s.cardIcon = s.frame:CreateTexture(nil, "ARTWORK")
s.cardLine = s.frame:CreateFontString(nil, "OVERLAY")
-- Animations are tried on a frame that is up and draws nothing.
s.animFrame = CreateFrame("Frame", nil, UIParent)
s.animFrame:SetSize(1, 1)
s.animFrame:SetAlpha(0)
s.events = CreateFrame("Frame")
s.log = {}
s.events:SetScript("OnEvent", function(_, event)
ns.Guard("selftest event", function()
s.log[#s.log + 1] = { event = event, at = GetTime() }
end)
end)
Selftest.scratch = s
return s
end
-- For the scenarios, which swap what a scratch object answers.
Selftest.Scratch = Scratch
local function Registered(event)
local get = _G.LibStub
if type(get) ~= "table" and type(get) ~= "function" then return nil end
local ok, lib = pcall(get, "AceEvent-3.0", true)
local registry = ok and type(lib) == "table" and type(lib.events) == "table" and lib.events.events
if type(registry) ~= "table" then return nil end
local who = registry[event]
return type(who) == "table" and who[addon] ~= nil
end
---------------------------------------------------------------------------
-- events
---------------------------------------------------------------------------
-- The four events launch day cares about, each tried on a frame of our own:
-- a client that does not know one refuses the registration with an error.
-- `severity` is what a refusal costs: the three the addon registers through
-- a guard cost a feature, the one the tooltip registers on hovering costs an
-- error.
local EVENTS = {
{ "UPDATE_MACROS", CHECK, "the action bar slot is not set again when the macro list changes" },
{ "UI_SCALE_CHANGED", CHECK, "the prompt is not laid out again for a new UI scale" },
{ "DISPLAY_SIZE_CHANGED", CHECK, "the prompt is not laid out again for a new resolution" },
{ "MODIFIER_STATE_CHANGED", FAIL, "hovering the prompt with a tooltip setting that watches Shift would throw" },
}
Launch("launch.events", "events", function(add)
local frame = Scratch().events
Call(frame, "UnregisterEvent", "UPDATE_MACROS")
local parts, status = {}, PASS
for _, row in ipairs(EVENTS) do
local event, cost, why = row[1], row[2], row[3]
local ok, err = Call(frame, "RegisterEvent", event)
if ok == true then
local heard = Registered(event)
parts[#parts + 1] = event .. " exists" .. (heard == nil and "" or heard and ", Manners listens"
or ", Manners does NOT listen")
if heard == false and RANK[CHECK] > RANK[status] then status = CHECK end
-- UPDATE_MACROS stays up for the half second after the macro calls.
if event ~= "UPDATE_MACROS" then Call(frame, "UnregisterEvent", event) end
else
parts[#parts + 1] = event .. " REFUSED (" .. (ok == MISSING and "no RegisterEvent" or tostring(err)) .. ") -- " .. why
if RANK[cost] > RANK[status] then status = cost end
end
end
add(status, table.concat(parts, "; "))
end)
---------------------------------------------------------------------------
-- the macro's slot on an action bar
---------------------------------------------------------------------------
local function SpellNameOf(spell)
if type(spell) == "string" then return spell end
if type(spell) ~= "number" then return nil end
local fn = Field(_G.C_Spell, "GetSpellName")
if type(fn) == "function" then
local ok, name = pcall(fn, spell)
if ok and type(Plain(name)) == "string" then return name end
end
if type(_G.GetSpellInfo) == "function" then
local ok, name = pcall(_G.GetSpellInfo, spell)
if ok and type(Plain(name)) == "string" then return name end
end
return nil
end
-- What the slot shows now: the spell (an id) and the item (name, link), as the
-- client reads them back.
local function ReadSlot(index)
local slot = {}
local ok, spell = Ask(_G.GetMacroSpell, index)
slot.spellKnown = ok == true
slot.spell = ok == true and Plain(spell) or nil
local okI, name, link = Ask(_G.GetMacroItem, index)
slot.itemKnown = okI == true
slot.itemName = okI == true and Plain(name) or nil
slot.itemLink = okI == true and Plain(link) or nil
return slot
end
-- The slot as it was: cleared, then given back what it held. Answers whether
-- reading it now says what it said before.
local function RestoreSlot(index, before)
local setSpell, setItem = _G.SetMacroSpell, _G.SetMacroItem
if type(setSpell) == "function" then pcall(setSpell, index, "") end
if type(setItem) == "function" then pcall(setItem, index, "") end
if before.spell ~= nil and type(setSpell) == "function" then
pcall(setSpell, index, SpellNameOf(before.spell) or before.spell)
end
if (before.itemLink or before.itemName) and type(setItem) == "function" then
pcall(setItem, index, before.itemLink or before.itemName)
end
local now = ReadSlot(index)
return now.spell == before.spell and now.itemName == before.itemName, now
end
local function DeleteTemporary(ctx)
local macro = ctx.macro
if not (macro and macro.temp) or macro.deleted then return end
macro.deleted = true
local okI, index = Ask(_G.GetMacroIndexByName, TEMP_MACRO)
if okI == true and type(Plain(index)) == "number" and index > 0 then
local okN, name = Ask(_G.GetMacroInfo, index)
-- Only a macro that is still the one made here.
if okN ~= true or name == TEMP_MACRO then pcall(_G.DeleteMacro, index) end
end
local okAfter, left = Ask(_G.GetMacroIndexByName, TEMP_MACRO)
macro.left = okAfter == true and type(Plain(left)) == "number" and left > 0
end
-- The macro the checks work on: the player's own, else (when asked to) a
-- temporary one, else nothing and the reason. `ctx.macro` false: none.
local function MacroTarget(ctx)
if ctx.macro ~= nil then return ctx.macro end
local name = ns.CLICK_MACRO_NAME or "Manners"
local ok, index = Ask(_G.GetMacroIndexByName, name)
if ok == true and type(Plain(index)) == "number" and index > 0 then
ctx.macro = { index = index, name = name }
return ctx.macro
end
ctx.macro = false
if ok == MISSING then
ctx.macroWhy = "GetMacroIndexByName is missing, so the macro cannot be looked for"
return false
end
if not ctx.opts.createMacro then
ctx.macroWhy = "no macro named " .. name .. " (make it with /manners macro, or run /manners selftest launch, which tests on a temporary one)"
return false
end
if type(_G.CreateMacro) ~= "function" or type(_G.DeleteMacro) ~= "function" then
ctx.macroWhy = "no macro named " .. name .. " and CreateMacro or DeleteMacro is missing, so a temporary one cannot be made"
return false
end
-- Account-wide first, as Clicks.lua makes the real one, then this character's.
local made, made1 = pcall(_G.CreateMacro, TEMP_MACRO, "INV_MISC_NOTE_03", "#MannersSelfTest", false)
if not made then made, made1 = pcall(_G.CreateMacro, TEMP_MACRO, "INV_MISC_NOTE_03", "#MannersSelfTest", true) end
if not made or type(Plain(made1)) ~= "number" or made1 <= 0 then
ctx.macroWhy = "no macro named " .. name .. " and a temporary one could not be made"
.. (made and "" or (": " .. tostring(made1)))
return false
end
ctx.macro = { index = made1, name = TEMP_MACRO, temp = true }
ctx.cleanup[#ctx.cleanup + 1] = function() DeleteTemporary(ctx) end
return ctx.macro
end
-- Which macro the line is about, said every time.
local function MacroWhere(macro)
if macro.temp then
return ("a temporary macro %s (slot %d), made for this test and deleted at its end"):format(TEMP_MACRO, macro.index)
end
return ("your %s macro (slot %d), put back as it was"):format(macro.name, macro.index)
end
local function SlotText(slot)
return ("spell %s, item %s"):format(Show(slot.spell), Show(slot.itemName))
end
Launch("launch.macro.slot", "SetMacroSpell", function(add, ctx)
local macro = MacroTarget(ctx)
if not macro then
add(CHECK, "skipped: " .. tostring(ctx.macroWhy))
return
end
local setSpell = _G.SetMacroSpell
if type(setSpell) ~= "function" then
add(CHECK, "SetMacroSpell is missing -- the bar slot keeps the note icon instead of the spell")
return
end
if InCombatLockdown() then
add(CHECK, "in combat -- not tried")
return
end
local id = SomeSpell()
local name = SpellNameOf(id)
if not name then
add(CHECK, "no spell name to try with")
return
end
local before = ReadSlot(macro.index)
local okSet, errSet = pcall(setSpell, macro.index, name)
local afterSet = ReadSlot(macro.index)
local okClear, errClear = pcall(setSpell, macro.index, "")
local afterClear = ReadSlot(macro.index)
local restored, now = RestoreSlot(macro.index, before)
local where = MacroWhere(macro)
if not okSet or not okClear then
add(FAIL, ("SetMacroSpell(%d, %s) threw: %s (%s)"):format(macro.index, Show(name),
tostring(okSet and errClear or errSet), where))
return
end
local status, notes = PASS, {}
local function worse(s, note)
if RANK[s] > RANK[status] then status = s end
notes[#notes + 1] = note
end
local shown = afterSet.spell
if not afterSet.spellKnown then
worse(CHECK, "GetMacroSpell is missing, so what the slot holds cannot be read back")
elseif shown == nil then
worse(CHECK, "GetMacroSpell answered nothing after setting it")
elseif shown ~= id and shown ~= name and SpellNameOf(shown) ~= name then
worse(CHECK, "GetMacroSpell answered something other than the spell set")
end
if afterSet.spellKnown and afterClear.spell ~= nil then
worse(CHECK, "an empty string leaves the spell on the slot (with nobody to buff it would keep showing the last one)")
end
if not restored then
worse(CHECK, "it could not be put back exactly: it showed " .. SlotText(before) .. ", now " .. SlotText(now))
end
add(status, ("SetMacroSpell(%d, %s) then GetMacroSpell answered %s; SetMacroSpell(%d, \"\") leaves %s; on %s%s"):format(
macro.index, Show(name), Show(shown), macro.index, Show(afterClear.spell), where,
#notes > 0 and (" -- " .. table.concat(notes, "; ")) or ""))
end)
Launch("launch.macro.item", "SetMacroItem", function(add, ctx)
local macro = MacroTarget(ctx)
if not macro then
add(CHECK, "skipped: " .. tostring(ctx.macroWhy))
return
end
local setItem = _G.SetMacroItem
if type(setItem) ~= "function" then
add(CHECK, "SetMacroItem is missing -- the bar slot keeps the note icon instead of a scroll's or an oil's")
return
end
if InCombatLockdown() then
add(CHECK, "in combat -- not tried")
return
end
local item = "item:" .. tostring(T.ANY_ITEM)
local before = ReadSlot(macro.index)
local okSet, errSet = pcall(setItem, macro.index, item)
local afterSet = ReadSlot(macro.index)
local okClear, errClear = pcall(setItem, macro.index, "")
local afterClear = ReadSlot(macro.index)
local restored, now = RestoreSlot(macro.index, before)
if not okSet or not okClear then
add(FAIL, ("SetMacroItem(%d, %s) threw: %s"):format(macro.index, Show(item), tostring(okSet and errClear or errSet)))
return
end
local status, notes = PASS, {}
local function worse(note)
status = CHECK
notes[#notes + 1] = note
end
if not afterSet.itemKnown then
worse("GetMacroItem is missing, so what the slot holds cannot be read back")
elseif afterSet.itemName == nil then
worse("GetMacroItem answered nothing after setting it")
end
if afterSet.itemKnown and afterClear.itemName ~= nil then
worse("an empty string leaves the item on the slot")
end
if not restored then
worse("it could not be put back exactly: it showed " .. SlotText(before) .. ", now " .. SlotText(now))
end
add(status, ("SetMacroItem(%d, %s) then GetMacroItem answered %s, %s; SetMacroItem(%d, \"\") leaves %s%s"):format(
macro.index, Show(item), Show(afterSet.itemName), Show(afterSet.itemLink), macro.index,
Show(afterClear.itemName), #notes > 0 and (" -- " .. table.concat(notes, "; ")) or ""))
end)
Launch("launch.macro.pickup", "PickupMacro", function(add, ctx)
local macro = MacroTarget(ctx)
if not macro then
add(CHECK, "skipped: " .. tostring(ctx.macroWhy))
return
end
local pickup, info, clear = _G.PickupMacro, _G.GetCursorInfo, _G.ClearCursor
if type(pickup) ~= "function" then
add(CHECK, "PickupMacro is missing -- the options button can only say where the macro is")
return
end
if type(info) ~= "function" or type(clear) ~= "function" then
add(CHECK, "GetCursorInfo or ClearCursor is missing, so the cursor cannot be checked and emptied")
return
end
if InCombatLockdown() then
add(CHECK, "in combat -- not tried")
return
end
-- Something already carried is the player's: it is not touched.
local okHeld, heldKind = Ask(info)
if okHeld == true and heldKind ~= nil then
add(CHECK, "something is on the cursor (" .. Show(heldKind) .. ") -- put it down and run this again")
return
end
local okPick, errPick = pcall(pickup, macro.index)
local okInfo, kind, which = Ask(info)
pcall(clear)
local _, kindAfter = Ask(info)
if not okPick then
add(FAIL, ("PickupMacro(%d) threw: %s"):format(macro.index, tostring(errPick)))
elseif kindAfter ~= nil then
add(FAIL, ("the macro was picked up and the cursor still holds %s after ClearCursor"):format(Show(kindAfter)))
elseif okInfo ~= true or kind ~= "macro" then
add(CHECK, ("PickupMacro(%d) then GetCursorInfo answered %s, %s -- the macro would not be carried to a bar"):format(
macro.index, Show(kind), Show(which)))
else
add(PASS, ("PickupMacro(%d) then GetCursorInfo answered %s, %s; ClearCursor emptied it"):format(
macro.index, Show(kind), Show(which)))
end
end)
-- The bar macro carries the cast under its first line (Clicks.lua, click
-- macro): a body of two lines written and read back, on the temporary macro
-- only. The player's own Manners macro is never rewritten here (Manners writes
-- it as the prompt changes): for it, its first line and whose Manners counts
-- it.
Launch("launch.macro.body", "EditMacro", function(add, ctx)
local macro = MacroTarget(ctx)
if not macro then
add(CHECK, "skipped: " .. tostring(ctx.macroWhy))
return
end
local readBody = _G.GetMacroBody
if not macro.temp then
local okB, text = Ask(readBody, macro.index)
text = okB == true and Plain(text) or nil
local first = type(text) == "string" and text:match("^%s*([^\n]*)") or nil
local state = ns.BarMacroState and ns.BarMacroState() or "?"
local line = ("your %s macro (slot %d) starts %s, and Manners counts it %s; not rewritten here"):format(
macro.name, macro.index, Show(first), state)
if first == ns.BAR_LINE and state ~= "foreign" then
add(PASS, line)
else
add(CHECK, line .. " -- Put Manners on an action bar hands it back to Manners")
end
return
end
if type(_G.EditMacro) ~= "function" or type(readBody) ~= "function" then
add(CHECK, "EditMacro or GetMacroBody is missing -- the bar macro cannot carry the cast")
return
end
if InCombatLockdown() then
add(CHECK, "in combat -- not tried")
return
end
local want = tostring(ns.BAR_LINE) .. "\n/cleartarget"
-- Called as Clicks.lua calls it, with the macro's own name: a client that
-- refuses a nil name would otherwise read as a fault Manners never meets.
local okE, errE = pcall(_G.EditMacro, macro.index, macro.name, nil, want)
local okB, back = Ask(readBody, macro.index)
back = okB == true and Plain(back) or nil
local got = type(back) == "string" and back:match("^%s*(.-)%s*$") or nil
local where = MacroWhere(macro)
if not okE then
add(FAIL, ("EditMacro(%d) threw: %s (%s)"):format(macro.index, tostring(errE), where))
elseif got == want then
add(PASS, ("EditMacro wrote two lines and GetMacroBody read them back the same, on %s"):format(where))
else
add(CHECK, ("EditMacro wrote %s and GetMacroBody answered %s, on %s -- the bar macro would not carry the cast as written"):format(
Show(want), Show(back), where))
end
end)
Launch("launch.macro.cleanup", "temporary macro", function(add, ctx)
local macro = ctx.macro
if not macro then
add(PASS, "none made")
return
end
if not macro.temp then
add(PASS, "none made -- the test used your own " .. macro.name .. " macro and put it back")
return
end
DeleteTemporary(ctx)
if macro.left then
add(FAIL, TEMP_MACRO .. " is still there -- delete it in the macro window (/macro)")
else
add(PASS, TEMP_MACRO .. " was made for the test and has been deleted")
end
end)
-- After the half second: the client answers a macro call with UPDATE_MACROS,
-- and Manners answers that by setting the slot again only when the macro's
-- place changed. A client that went on sending it after our last call would
-- say the slot is being set in a loop.
Launch("launch.macro.events", "UPDATE_MACROS", function(add, ctx)
local log = Scratch().log
local total, late = 0, 0
for _, e in ipairs(log) do
if e.event == "UPDATE_MACROS" then
total = total + 1
if ctx.listenFrom and e.at and e.at - ctx.listenFrom > QUIET_AFTER then late = late + 1 end
end
end
if not ctx.macro then
add(CHECK, "no macro call was made, so there was nothing to listen for")
elseif late > 0 then
add(CHECK, ("%d UPDATE_MACROS in %.1f s after the macro calls, %d of them after the first %.2f s -- it may be answering in a loop"):format(
total, LISTEN, late, QUIET_AFTER))
else
add(PASS, ("%d UPDATE_MACROS in %.1f s after the macro calls, none after the first %.2f s"):format(
total, LISTEN, QUIET_AFTER))
end
end, true)
---------------------------------------------------------------------------
-- fonts, masks, clipping, tiling, animations
---------------------------------------------------------------------------
local function FaceTry(fs, path)
local ok, accepted = Call(fs, "SetFont", path, 14, "")
if ok ~= true then return { threw = ok == false and tostring(accepted) or "SetFont is missing" } end
local face = { accepted = accepted }
local okG, got, size = Call(fs, "GetFont")
face.got, face.size = okG == true and got or nil, okG == true and size or nil
local okT, errT = Call(fs, "SetText", "Hello, Manners")
if okT == true then
local okW, width = Call(fs, "GetStringWidth")
face.width = okW == true and Plain(width) or nil
else
face.textErr = okT == MISSING and "no SetText" or tostring(errT)
end
return face
end
local function FaceLine(path, face)
if face.threw then return ("%s: %s"):format(path, face.threw) end
return ("%s: SetFont answered %s, GetFont %s, text %s wide"):format(path, Show(face.accepted), Show(face.got),
face.textErr and ("refused (" .. face.textErr .. ")") or Show(face.width))
end
Launch("launch.font", "Morpheus font", function(add)
local fs = Scratch().fs
local standard = _G.STANDARD_TEXT_FONT
local face = FaceTry(fs, MORPHEUS)
local control = standard and FaceTry(fs, standard)
local locale = tostring(ns.LOCALE)
local used = ROMAN[ns.LOCALE or "enUS"] and ("language " .. locale .. ": the looks write in it")
or ("language " .. locale .. ": the looks keep the client's own face and never ask for it")
local line = FaceLine("Morpheus", face) .. "; " .. (control and FaceLine("the game's own", control) or "no STANDARD_TEXT_FONT") .. "; " .. used
if face.threw then
add(FAIL, line)
elseif control and (control.threw or control.accepted ~= true) then
add(FAIL, line .. " -- SetFont refuses even the game's own face")
elseif face.accepted ~= true or face.textErr or type(face.width) ~= "number" or face.width <= 0 then
add(CHECK, line .. " -- the client refused the file: the looks fall back to the game's face")
elseif face.got == nil or Lower(face.got) ~= Lower(MORPHEUS) then
add(CHECK, line .. " -- the client took it but reads another face back")
else
add(PASS, line)
end
end)
Launch("launch.mask", "icon masks", function(add)
local s = Scratch()
local mask, tex = nil, s.tex
-- One mask for the session: a frame keeps every one it is ever given.
local okC, made = true, s.mask
if made == nil then okC, made = Call(s.frame, "CreateMaskTexture") end
if okC == MISSING then
add(FAIL, "CreateMaskTexture is missing -- Ribbon, Letter, Leather, Toast and the picker cannot cut an icon round")
return
elseif okC ~= true then
add(FAIL, "CreateMaskTexture threw: " .. tostring(made))
return
elseif made == nil then
add(FAIL, "CreateMaskTexture answered nothing -- no mask can be made")
return
end
mask = made
s.mask = made
local file = ART .. "Ribbon\\Ribbon_Mask"
local okS, errS = Call(mask, "SetTexture", file, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
local okP = Call(mask, "SetAllPoints", tex)
local okA, errA = Call(tex, "AddMaskTexture", mask)
local okN, count = Call(tex, "GetNumMaskTextures")
local okR, errR = Call(tex, "RemoveMaskTexture", mask)
local okN2, count2 = Call(tex, "GetNumMaskTextures")
local line = ("mask created (%s), SetTexture %s, AddMaskTexture %s, attached count %s, after RemoveMaskTexture %s"):format(
Show(Call(mask, "GetObjectType") == true and select(2, Call(mask, "GetObjectType")) or "MaskTexture"),
okS == true and "accepted" or okS == MISSING and "MISSING" or ("threw " .. tostring(errS)),
okA == true and "accepted" or okA == MISSING and "MISSING" or ("threw " .. tostring(errA)),
okN == true and Show(count) or "unreadable", okN2 == true and Show(count2) or "unreadable")
if okA == MISSING or okS == MISSING then
add(FAIL, line .. " -- the looks call it without asking")
elseif okS ~= true or okA ~= true or okR == false then
add(FAIL, line)
elseif okN ~= true then
add(CHECK, line .. " -- the client has no call to count masks, so only a look at the prompt's round icon shows it")
elseif count ~= 1 or (okN2 == true and count2 ~= 0) then
add(CHECK, line .. " -- not one mask on, then none")
else
add(PASS, line)
end
-- okP is only told for the line above being a complete use of the calls.
local _ = okP
end)
Launch("launch.clip", "SetClipsChildren", function(add)
local clip = Scratch().clip
if type(clip.SetClipsChildren) ~= "function" then
add(CHECK, "SetClipsChildren is missing -- the Letter's unroll and the seal's shadow are drawn without being cut to the paper")
return
end
local okSet, errSet = Call(clip, "SetClipsChildren", true)
local okDoes, does = Call(clip, "DoesClipChildren")
local okOff = Call(clip, "SetClipsChildren", false)
local okOff2, doesOff = Call(clip, "DoesClipChildren")
local line = ("SetClipsChildren(true) %s, DoesClipChildren answered %s; after (false) %s"):format(
okSet == true and "accepted" or ("threw " .. tostring(errSet)), okDoes == true and Show(does) or "unreadable",
okOff2 == true and Show(doesOff) or "unreadable")
if okSet ~= true then
add(FAIL, line)
elseif okDoes ~= true then
add(CHECK, line .. " -- the client cannot say whether it clips, so only the Letter's unroll shows it")
elseif does ~= true or (okOff2 == true and doesOff ~= false) then
add(CHECK, line .. " -- it does not read back as set")
else
add(PASS, line)
end
local _ = okOff
end)
Launch("launch.tile", "REPEAT tiling", function(add)
local tex = Scratch().tex
local file = ART .. "Leather\\Leather_BodyM"
local okT, errT = Call(tex, "SetTexture", file, "REPEAT", "REPEAT")
local okC, errC = Call(tex, "SetTexCoord", 0, 6, 0, 1)
local okG, ulx, uly, llx, lly, urx = Call(tex, "GetTexCoord")
local okF, got = Call(tex, "GetTexture")
local okH, horiz = Call(tex, "GetHorizTile")
local okV, vert = Call(tex, "GetVertTile")
-- Put back for the next test of the same texture.
Call(tex, "SetTexture", nil)
Call(tex, "SetTexCoord", 0, 1, 0, 1)
local line = ("SetTexture(Leather_BodyM, \"REPEAT\", \"REPEAT\") %s, SetTexCoord(0, 6, 0, 1) %s, GetTexCoord reads %s, GetTexture %s, GetHorizTile %s, GetVertTile %s"):format(
okT == true and "accepted" or ("threw " .. tostring(errT)),
okC == true and "accepted" or ("threw " .. tostring(errC)),
okG == true and ("%s..%s"):format(Show(ulx), Show(urx)) or "unreadable",
okF == true and Show(got) or "unreadable",
okH == true and Show(horiz) or "n/a", okV == true and Show(vert) or "n/a")
if okT ~= true or okC ~= true then
add(FAIL, line)
elseif okG ~= true then
add(CHECK, line .. " -- the client cannot read the span back; only a look at the Leather's strips shows the tiling")
elseif type(Plain(urx)) ~= "number" or math.abs(urx - 6) > 0.01 then
add(CHECK, line .. " -- the span past 1 was not kept, so the strips would not repeat")
else
add(PASS, line .. " (how the leather looks tiled is for eyes)")
end
local _ = { uly, llx, lly }
end)
-- The animation kinds and settings the looks use, tried on a group of our own.
Launch("launch.anim", "animations", function(add)
local s = Scratch()
local frame = s.animFrame
-- One group, and one animation of each kind, for the session.
local okG, group = true, s.animGroup
if group == nil then okG, group = Call(frame, "CreateAnimationGroup") end
s.anims = s.anims or {}
if okG == MISSING then
add(FAIL, "CreateAnimationGroup is missing -- no look can animate")
return
elseif okG ~= true or group == nil then
add(FAIL, "CreateAnimationGroup " .. (okG == true and "answered nothing" or ("threw " .. tostring(group))))
return
end
local made, missing, threw = {}, {}, {}
local function try(obj, method, ...)
local ok, err = Call(obj, method, ...)
if ok == MISSING then missing[#missing + 1] = method
elseif ok ~= true then threw[#threw + 1] = method .. ": " .. tostring(err) end
return ok == true
end
-- `key` names the animation in the scratch's keep when one kind is tried
-- twice (the second Scale below); it reads as the kind otherwise.
local function make(kind, setup, key)
key = key or kind
local ok, anim = true, s.anims[key]
if anim == nil then ok, anim = Call(group, "CreateAnimation", kind) end
if ok == true and anim ~= nil then s.anims[key] = anim end
if ok ~= true or anim == nil then
threw[#threw + 1] = "CreateAnimation(" .. kind .. ") " .. (ok == true and "answered nothing" or tostring(anim))
return
end
made[#made + 1] = key
try(anim, "SetDuration", 0.1)
try(anim, "SetOrder", 1)
try(anim, "SetSmoothing", "OUT")
setup(anim)
end
make("Alpha", function(a) try(a, "SetFromAlpha", 0) try(a, "SetToAlpha", 1) end)
make("Scale", function(a) try(a, "SetScaleFrom", 1, 1) try(a, "SetScaleTo", 1.1, 1.1) try(a, "SetOrigin", "CENTER", 0, 0) end)
-- The entrance's rules grow along one axis only, about their left end
-- (Prompt/Effects.lua): the pair of unequal scales and the LEFT origin,
-- which the mock accepts and launch day has to confirm.
make("Scale", function(a) try(a, "SetScaleFrom", 0.04, 1) try(a, "SetScaleTo", 1, 1) try(a, "SetOrigin", "LEFT", 0, 0) end,
"Scale(x, y) about LEFT")
make("Translation", function(a) try(a, "SetOffset", 2, 0) end)
make("Rotation", function(a) try(a, "SetDegrees", 5) end)
try(group, "SetLooping", "BOUNCE")
try(group, "SetLooping", "REPEAT")
try(group, "SetLooping", "NONE")
local okP = Call(group, "Play")
local okI, playing = Call(group, "IsPlaying")
Call(group, "Stop")
local okI2, after = Call(group, "IsPlaying")
local line = ("made %s; Play %s, IsPlaying %s, after Stop %s"):format(table.concat(made, ", "),
okP == true and "accepted" or "refused", okI == true and Show(playing) or "unreadable",
okI2 == true and Show(after) or "unreadable")
if #threw > 0 then
add(FAIL, line .. " -- " .. table.concat(threw, "; "))
elseif #missing > 0 or #made < 5 then
add(CHECK, line .. " -- missing: " .. table.concat(missing, ", "))
elseif okI == true and playing ~= true then
add(CHECK, line .. " -- the group did not report playing")
else
add(PASS, line)
end
-- One group on the scratch frame for ever, not one more each run.
s.animGroup = group
end)
---------------------------------------------------------------------------
-- every look, on the real prompt
---------------------------------------------------------------------------
-- The prompt's tree as the client lists it: how many frames and regions are
-- built (every look that was ever shown keeps its own), and, of those, what is
-- switched on from the prompt down -- shown, and under frames that are shown,
-- the prompt's own state aside, which is hidden whenever nobody is on it. What
-- the looks lean on is counted among the live ones, so that a look's line
-- speaks of that look and not of the ones built before it.
local function Tree(root)
local c = { frames = 0, textures = 0, strings = 0, masks = 0, live = 0, liveFrames = 0, clips = 0,
tiled = 0, groups = 0 }
local seen, nodes = {}, 0
local function shown(obj)
local ok, on = Call(obj, "IsShown")
return ok ~= true or on == true
end
local function region(r, live)
local okK, kind = Call(r, "GetObjectType")
kind = okK == true and kind or "Texture"
if kind == "FontString" then c.strings = c.strings + 1
elseif kind == "MaskTexture" then c.masks = c.masks + 1
else c.textures = c.textures + 1 end
if not (live and shown(r)) then return end
c.live = c.live + 1
local okH, h = Call(r, "GetHorizTile")
local okV, v = Call(r, "GetVertTile")
if (okH == true and h == true) or (okV == true and v == true) then c.tiled = c.tiled + 1 end
end
local function walk(f, level, live)
if seen[f] or level > 12 or nodes > 4000 then return end
seen[f] = true
nodes = nodes + 1
c.frames = c.frames + 1
if live then
c.liveFrames = c.liveFrames + 1
local okC, clips = Call(f, "DoesClipChildren")
if okC == true and clips == true then c.clips = c.clips + 1 end
local okA, groups = pcall(function() return select("#", f:GetAnimationGroups()) end)
if okA then c.groups = c.groups + groups end
end
if type(f.GetRegions) == "function" then
local ok, regions = pcall(function() return { f:GetRegions() } end)
if ok then for _, r in ipairs(regions) do region(r, live) end end
end
if type(f.GetChildren) == "function" then
local ok, children = pcall(function() return { f:GetChildren() } end)
if ok then
for _, child in ipairs(children) do walk(child, level + 1, live and shown(child)) end
end
end
end
c.listed = type(root.GetRegions) == "function" and type(root.GetChildren) == "function"
walk(root, 0, true)
c.regions = c.textures + c.strings + c.masks
return c
end
-- What each look must show when the client does what the code relies on.
local EXPECT = {
ribbon = { face = true, mask = true },
letter = { face = true, mask = true, clip = true },
leather = { face = true, mask = true },
}
local function WantsMorpheus(p)
local d = ns.defaults and ns.defaults.profile.prompt.font
if type(p.font) == "string" and p.font ~= d then return false end
return ROMAN[ns.LOCALE or "enUS"] == true
end
local function Measure(Prompt, R, key, look)
local row = { key = key }
local S = Prompt.state
row.active = S and S.activeLook
row.matches = row.active == look
row.tree = Tree(R.button)
local okF, path, size = Call(R.nameText, "GetFont")
row.font, row.size = okF == true and path or nil, okF == true and size or nil
row.fontKnown = okF == true
local okN, masks = Call(R.icon, "GetNumMaskTextures")
row.masks = okN == true and masks or nil
-- How many of them are switched on: a mask that is hidden is the look's way
-- of taking its shape off the icon, and the game's word on that is the
-- thing to read.
if okN == true and type(masks) == "number" then
row.masksShown = 0
for i = 1, masks do
local okM, mask = Call(R.icon, "GetMaskTexture", i)
local okS, on = Call(okM == true and mask or nil, "IsShown")
if okM ~= true or okS ~= true then
row.masksShown = nil
break
end
if on then row.masksShown = row.masksShown + 1 end
end
end
local okS, shown = Call(R.icon, "IsShown")
row.iconShown = okS == true and shown == true
row.ownMask = look ~= nil and look.mask ~= nil
return row
end
-- Every look put on the prompt in turn, in one call, and the style put back.
-- Run once, by the first check that wants it.
local function Sweep(ctx)
if ctx.sweep then return ctx.sweep end
local sweep = { per = {}, order = {} }
ctx.sweep = sweep
local Prompt = ns.Prompt
local R = Prompt and Prompt.regions
local p = ns.db and ns.db.profile and ns.db.profile.prompt
if not (R and R.button and p and ns.Looks and type(Prompt.ApplyStyle) == "function") then
sweep.why = "the prompt is not built"
return sweep
end
if InCombatLockdown() then
sweep.why = "in combat"
return sweep
end
local _, keys = ns.Looks.Choices()
local saved = rawget(p, "style")
local t0 = Clock()
local before = Tree(R.button)
local seen = before.frames + before.regions
sweep.before = before
sweep.style = saved == nil and p.style or saved
for _, key in ipairs(keys) do
local row
local started = Clock()
p.style = key
local ok, err = pcall(Prompt.ApplyStyle, Prompt)
local ms = started and Clock() and (Clock() - started) or nil
if not ok then
row = { key = key, err = tostring(err) }
else
local okM, measured = pcall(Measure, Prompt, R, key, ns.Looks.Get(key))
row = okM and measured or { key = key, err = "measuring it threw: " .. tostring(measured) }
if row.tree then
row.built = row.tree.frames + row.tree.regions - seen
seen = row.tree.frames + row.tree.regions
end
end
row.ms = ms
sweep.per[key] = row
sweep.order[#sweep.order + 1] = key
end
-- Put back, and the prompt laid out for it again.
p.style = saved
local okBack, errBack = pcall(Prompt.ApplyStyle, Prompt)
sweep.restoreErr = not okBack and tostring(errBack) or nil
sweep.after = Tree(R.button)
sweep.ms = t0 and Clock() and (Clock() - t0) or nil
sweep.wore = rawget(p, "style") == saved
return sweep
end
Launch("launch.looks", "looks", function(add, ctx)
local sweep = Sweep(ctx)
if sweep.why then
add(CHECK, "not tried: " .. sweep.why)
return
end
local p = ns.db.profile.prompt
local status, notes = PASS, {}
local function worse(s, note)
if RANK[s] > RANK[status] then status = s end
notes[#notes + 1] = note
end
if sweep.restoreErr then worse(FAIL, "putting the style back threw: " .. sweep.restoreErr) end
if not sweep.wore then worse(FAIL, "the style was not put back") end
if not sweep.before.listed then
worse(CHECK, "the client cannot list a frame's regions and children here, so region counts are not read")
elseif sweep.after.live ~= sweep.before.live or sweep.after.liveFrames ~= sweep.before.liveFrames then
worse(CHECK, ("the prompt has %d regions in %d frames switched on now, it had %d in %d before"):format(
sweep.after.live, sweep.after.liveFrames, sweep.before.live, sweep.before.liveFrames))
end
add(status, ("switched through %d looks in one call%s and put the style back to %s: %d regions switched on, as before%s"):format(
#sweep.order, sweep.ms and (" (" .. math.floor(sweep.ms + 0.5) .. " ms)") or "", Show(sweep.style),
sweep.after.live, #notes > 0 and (" -- " .. table.concat(notes, "; ")) or ""))
local face = WantsMorpheus(p)
local clipApi = type(Scratch().clip.SetClipsChildren) == "function"
for _, key in ipairs(sweep.order) do
local row = sweep.per[key]
local id, label = "launch.look." .. key, "look " .. key
if row.err then
add(FAIL, "applying it threw: " .. row.err, label, id)
else
local tree = row.tree
local problems, st = {}, PASS
local function bad(note)
st = CHECK
problems[#problems + 1] = note
end
if not row.matches then
st = FAIL
problems[#problems + 1] = "the prompt did not end up wearing it"
end
local expect = EXPECT[key] or {}
if expect.face and face then
if not row.fontKnown then bad("the name's font cannot be read back")
elseif Lower(row.font) ~= Lower(MORPHEUS) then bad("the name is not in Morpheus (" .. Show(row.font) .. ")") end
end
if expect.mask and row.iconShown then
if row.masks == nil then bad("the icon's masks cannot be counted")
elseif row.masks < 1 or (row.masksShown or row.masks) < 1 then bad("the icon has no mask on it") end
end
if expect.clip and clipApi and tree.listed and tree.clips < 1 then bad("no frame clips its children") end
add(st, ("%s%s, name font %s, icon masks %s, clipping frames %s, tiled textures %s, animation groups %s%s"):format(
row.ms and (math.floor(row.ms + 0.5) .. " ms; ") or "",
tree.listed and ("%d regions in %d frames switched on%s"):format(tree.live, tree.liveFrames,
row.built and row.built > 0 and (" (" .. row.built .. " built now)") or "") or "regions not listable",
row.fontKnown and (Show(row.font) .. " " .. Show(row.size)) or "n/a",
row.masks == nil and "n/a" or (row.masksShown and row.masksShown ~= row.masks
and ("%d (%d shown)"):format(row.masks, row.masksShown) or tostring(row.masks)),
tree.listed and tostring(tree.clips) or "n/a",
tree.listed and tostring(tree.tiled) or "n/a", tree.listed and tostring(tree.groups) or "n/a",
#problems > 0 and (" -- " .. table.concat(problems, "; ")) or ""), label, id)
end
end
end)
---------------------------------------------------------------------------
-- the look picker's cards
---------------------------------------------------------------------------
local function BuffIcon()
for _, buff in ipairs(ns.CastableBuffs and ns.CastableBuffs() or {}) do
local info = ns.BuffInfo(buff)
local icon = info and info.icon
if type(icon) == "number" or type(icon) == "string" then return icon, true end
end
return QUESTION_MARK, false
end
Launch("launch.picker", "look picker cards", function(add, ctx)
local s = Scratch()
local icon, own = BuffIcon()
-- A card in little: the spell icon on the picture, cut round by the look's
-- mask, with the sample name in the look's face.
local tex, line = s.cardIcon, s.cardLine
local okT, errT = Call(tex, "SetTexture", icon)
local okC, errC = Call(tex, "SetTexCoord", 0.08, 0.92, 0.08, 0.92)
local okFile, file = Call(tex, "GetTexture")
local okM, mask = true, s.cardMask
if mask == nil then okM, mask = Call(s.frame, "CreateMaskTexture") end
local masked = false
if okM == true and mask ~= nil then
s.cardMask = mask
Call(mask, "SetTexture", ART .. "Letter\\Letter_Mask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
Call(mask, "SetAllPoints", tex)
masked = Call(tex, "AddMaskTexture", mask) == true
local okN, n = Call(tex, "GetNumMaskTextures")
masked = masked and (okN ~= true or n == 1)
Call(tex, "RemoveMaskTexture", mask)
end
local face = FaceTry(line, MORPHEUS)
Call(line, "SetShadowOffset", 1, -1)