Repository navigation
Expand file tree
/
Copy pathSelftest.lua
More file actions
1295 lines (1193 loc) · 49 KB
/
Copy pathSelftest.lua
File metadata and controls
1295 lines (1193 loc) · 49 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 (and /manners check): Manners held up against
-- the game it is running in, in a few seconds, with a report to paste.
--
-- Every bug that reached a player in the 1.6 releases was one the mock client
-- in tests/ could not show: a scroll's imbue that only C_Item reports, a
-- scroll that wants an item target, a window fade whose mouse release went
-- missing, surnames, a range nothing answers without nameplates. This asks the
-- real client the same questions, out of combat, and says what it saw.
--
-- It touches nothing: no cast, no chat, no setting, no protected call, no
-- attribute on the prompt. A reading of your own buffs writes the memory of
-- the one you had up last (Core.lua, ReadOwnFamily), so that memory is put
-- back as it was. Every check runs under its own pcall, since this is a
-- command typed once and not a busy path, and one that throws is a FAIL with
-- the error while the rest run on.
--
-- The report is English, for whoever reads the bug report; the labels go
-- through L. Each line is PASS, WARN or FAIL, the label, and what was seen.
--
-- /manners selftest launch adds the launch checks (SelftestLaunch.lua): the
-- parts of Manners that have only run on the mock, asked of the real client.
-- They are the one place this file's report is joined by a section that does
-- touch things -- briefly, and every one put back.
local _, ns = ...
local L = ns.L
local addon = ns.addon
local issecretvalue = _G.issecretvalue
local InCombatLockdown = _G.InCombatLockdown
local unpack = _G.unpack or table.unpack
local Selftest = {}
ns.Selftest = Selftest
local PASS, WARN, FAIL = "PASS", "WARN", "FAIL"
-- Bindings.xml's command and Clicks.lua's macro.
local COMMAND = "CLICK MannersPrompt:LeftButton"
-- The global cooldown's spell, asked of C_Spell.GetSpellCooldown.
local GCD_SPELL = 61304
-- A Hearthstone: an item every client knows, for the item calls' shape.
local ANY_ITEM = 6948
local MISSING = {}
---------------------------------------------------------------------------
-- reading values without tripping on them
---------------------------------------------------------------------------
local function Secret(v)
return issecretvalue ~= nil and issecretvalue(v) == true
end
-- A value as the report shows it: SECRET for one the client withholds,
-- strings quoted and cut short, a table as its first fields.
local function Show(v)
if Secret(v) then return "SECRET" end
local kind = type(v)
if kind == "nil" then return "nil" end
if kind == "string" then
v = v:gsub("[\r\n]", " | ")
if #v > 90 then v = v:sub(1, 87) .. "..." end
return '"' .. v .. '"'
end
if kind ~= "table" then return tostring(v) end
local keys = {}
for k in pairs(v) do
if type(k) == "string" or type(k) == "number" then keys[#keys + 1] = k end
end
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
local parts = {}
for i = 1, math.min(#keys, 8) do
local x = v[keys[i]]
local shown
if Secret(x) then
shown = "SECRET"
elseif type(x) == "table" then
shown = "table"
else
shown = tostring(x)
end
parts[#parts + 1] = tostring(keys[i]) .. "=" .. shown
end
if #keys > 8 then parts[#parts + 1] = "..." end
return "{" .. table.concat(parts, " ") .. "}"
end
-- nil for a secret, so it can be compared.
local function Plain(v)
if Secret(v) then return nil end
return v
end
-- A client call: MISSING when there is no such function, else pcall's answer.
local function Ask(fn, ...)
if type(fn) ~= "function" then return MISSING end
return pcall(fn, ...)
end
-- Colour codes off, for a line that came from chat text.
local function Strip(text)
return (tostring(text):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""))
end
local function Field(tbl, key)
if type(tbl) ~= "table" or Secret(tbl) then return nil end
return tbl[key]
end
local function Class()
return ns.caps and ns.caps.class
end
local function OwnFamily(test)
for _, family in ipairs(ns.GetOwnFamilies(Class()) or {}) do
if test(family) then return family end
end
return nil
end
-- The mage's scrolls, Forever's own: not the weapon enhancements (a poison, a
-- stone, an oil, a lure), which every client has as items of its own and which
-- hold no more of this report's checks than a count of what the bags hold.
local function HasScrolls() return OwnFamily(function(f) return f.scroll and not f.enhance end) ~= nil end
local function ImbueFamily() return OwnFamily(function(f) return f.imbue end) end
local function FamiliarFamily() return OwnFamily(function(f) return f.scroll and not f.imbue and not f.enhance end) end
-- Why a call about the main hand's enchants does not matter to this character,
-- or nil when it does. Only a family with an imbue (Forever's mage scrolls)
-- reads them, and Enum.ItemEnchantType and C_Item.GetWeaponEnchantInfo exist
-- on Forever alone (Ketho's BlizzardInterfaceResources), so on the other four
-- clients their absence is the ordinary state, not a fault.
local function NoImbueWhy()
if ImbueFamily() then return nil end
return tostring(Class()) .. " has no weapon imbue on this client"
end
-- A spell id to ask the spell calls about: your best rank of a buff you know,
-- or of any buff of your class, or Arcane Intellect.
local function SomeSpell()
local buffs = ns.GetClassBuffs(Class()) or {}
for _, buff in ipairs(buffs) do
local info = ns.BuffInfo(buff)
if info and info.known and info.topRank then return info.topRank end
end
for _, family in ipairs(ns.GetOwnFamilies(Class()) or {}) do
for _, spell in ipairs(family.spells) do
local info = ns.BuffInfo(spell)
if not spell.item and info and info.known and info.topRank then return info.topRank end
end
end
return buffs[1] and buffs[1].ranks[1] or 1459
end
-- What SelftestLaunch.lua reads and shows values with.
Selftest.tools = { Show = Show, Secret = Secret, Plain = Plain, Ask = Ask, Field = Field, Strip = Strip,
SomeSpell = SomeSpell, MISSING = MISSING, ANY_ITEM = ANY_ITEM }
-- A reading of your own buffs remembers the one up (Core.lua, Remember):
-- put back as it was, so the test leaves no trace in the saved file.
local function KeepingMemory(fn, ...)
local char = ns.db and ns.db.char
local had = type(char) == "table" and char.ownLast or nil
local before
if type(had) == "table" then
before = {}
for k, v in pairs(had) do before[k] = v end
end
local out = { pcall(fn, ...) }
if type(char) == "table" then
if before then
if char.ownLast ~= had then char.ownLast = had end
for k in pairs(had) do had[k] = nil end
for k, v in pairs(before) do had[k] = v end
else
char.ownLast = had
end
end
if not out[1] then error(out[2], 0) end
return unpack(out, 2, table.maxn(out))
end
---------------------------------------------------------------------------
-- the checks, in the order the report gives them
--
-- Each is { id, group, label, run }: run(add) reports one line or several
-- with add(status, value[, label]). The ids are for the scenarios.
---------------------------------------------------------------------------
local GROUPS = {
{ key = "client", label = L["Client"] },
{ key = "api", label = L["Client API"] },
{ key = "beliefs", label = L["What Manners believes"] },
{ key = "button", label = L["Prompt and secure button"] },
{ key = "range", label = L["Range"] },
{ key = "speech", label = L["Speech"] },
{ key = "window", label = L["Options window"] },
{ key = "errors", label = L["Errors"] },
}
Selftest.GROUPS = GROUPS
local CHECKS = {}
Selftest.CHECKS = CHECKS
local function Check(id, group, label, run)
CHECKS[#CHECKS + 1] = { id = id, group = group, label = label, run = run }
end
-- One client function's existence and the shape of its answer. `missing` is
-- the status when there is no such function (FAIL or WARN), with why; `judge`
-- turns the answer into a status and a line, or nil for PASS with the values.
-- `unused`, when given, says why the call does not matter to this character
-- (or answers nil when it does): a call that is missing then passes with that.
local function ApiCheck(id, path, find, args, missing, why, judge, unused)
Check(id, "api", path, function(add)
local fn = find()
if type(fn) ~= "function" then
local idle = unused and unused()
if idle then
add(PASS, "missing -- not used: " .. idle)
else
add(type(missing) == "function" and missing() or missing, "missing -- " .. why)
end
return
end
local out = { pcall(fn, unpack(args())) }
if not out[1] then
add(FAIL, "threw: " .. tostring(out[2]))
return
end
local status, line
if judge then status, line = judge(unpack(out, 2, table.maxn(out))) end
if not line then
local shown = {}
for i = 2, math.max(2, table.maxn(out)) do shown[#shown + 1] = Show(out[i]) end
line = "returned " .. table.concat(shown, ", ")
end
add(status or PASS, line)
end)
end
local function Table(...)
local t = { ... }
return function() return t end
end
-- An aura read answers a table, nothing, or a secret; anything else is a shape
-- this addon does not read.
local function AuraShape(aura)
if aura == nil or Secret(aura) then return PASS end
if type(aura) ~= "table" then return FAIL, "returned " .. Show(aura) .. ", not an aura table" end
if not Secret(aura.spellId) and type(aura.spellId) ~= "number" then
return WARN, "returned " .. Show(aura) .. " with no spellId"
end
return PASS
end
---------------------------------------------------------------------------
-- client
---------------------------------------------------------------------------
Check("client.build", "client", L["build"], function(add)
local ok, version, build, _, toc = Ask(_G.GetBuildInfo)
if ok == MISSING or not ok then
add(FAIL, "GetBuildInfo " .. (ok == MISSING and "missing" or "threw: " .. tostring(version)))
return
end
add(type(Plain(toc)) == "number" and PASS or WARN,
("%s (%s), interface %s, Manners %s"):format(Show(version), Show(build), Show(toc), tostring(ns.BUILD)))
end)
-- The languages Locales/ translates; English needs none.
local TRANSLATED = { enUS = true, enGB = true, deDE = true, esES = true, esMX = true, frFR = true,
itIT = true, koKR = true, ptBR = true, zhCN = true, zhTW = true }
Check("client.locale", "client", L["locale"], function(add)
local ok, locale = Ask(_G.GetLocale)
if ok ~= true then
add(WARN, "GetLocale " .. (ok == MISSING and "missing" or "threw") .. "; Manners read " .. tostring(ns.LOCALE))
return
end
locale = Plain(locale)
if locale ~= ns.LOCALE then
add(WARN, ("the client says %s, Manners loaded %s"):format(Show(locale), tostring(ns.LOCALE)))
elseif not TRANSLATED[locale] then
add(WARN, tostring(locale) .. " -- no translation, so Manners speaks English")
else
add(PASS, tostring(locale))
end
end)
Check("client.flavour", "client", L["flavour"], function(add)
local f = ns.Flavour
if type(f) ~= "table" then
add(FAIL, "Flavour.lua did not load")
return
end
local summary = Strip(ns.FlavourSummary and ns.FlavourSummary() or "")
if f.err then
add(FAIL, summary)
elseif not f.recognised or f.agrees == false then
add(WARN, summary)
else
add(PASS, summary)
end
end)
Check("client.secrets", "client", L["secret values"], function(add)
local secrets = _G.C_Secrets
local restrictions = Field(secrets, "HasSecretRestrictions")
local aurasNow = Field(secrets, "ShouldAurasBeSecret")
local okR, r = Ask(restrictions)
local okA, a = Ask(aurasNow)
local line = ("issecretvalue=%s C_Secrets=%s restrictions=%s aurasSecretNow=%s"):format(
type(_G.issecretvalue), type(secrets),
okR == true and Show(r) or "n/a", okA == true and Show(a) or "n/a")
-- Values that may be secret with nothing to test them by: every compare
-- could throw.
if type(secrets) == "table" and type(_G.issecretvalue) ~= "function" then
add(FAIL, line .. " -- secrets exist and nothing can test for one")
else
add(PASS, line)
end
end)
---------------------------------------------------------------------------
-- the client calls Manners depends on: there, and answering in the shape
-- read, with no side effects
---------------------------------------------------------------------------
local function Auras() return _G.C_UnitAuras end
local function Spells() return _G.C_Spell end
local function Items() return _G.C_Item end
ApiCheck("api.auraByIndex", "C_UnitAuras.GetAuraDataByIndex",
function() return Field(Auras(), "GetAuraDataByIndex") end,
Table("player", 1, "HELPFUL"), FAIL, "buffs others put on you cannot be noticed",
AuraShape)
ApiCheck("api.auraBySpellID", "C_UnitAuras.GetUnitAuraBySpellID",
function() return Field(Auras(), "GetUnitAuraBySpellID") end,
function() return { "player", SomeSpell() } end, FAIL,
"nobody's buffs can be read, so everybody is offered everything",
AuraShape)
ApiCheck("api.auraBySpellName", "C_UnitAuras.GetAuraDataBySpellName",
function() return Field(Auras(), "GetAuraDataBySpellName") end,
function() return { "player", ns.SpellNameFor(SomeSpell()) or "", "HELPFUL" } end, WARN,
"your own buffs are found by walking your auras instead",
AuraShape)
ApiCheck("api.spellName", "C_Spell.GetSpellName",
function() return Field(Spells(), "GetSpellName") end,
function() return { SomeSpell() } end,
function() return type(_G.GetSpellInfo) == "function" and WARN or FAIL end,
"spell names come from GetSpellInfo, if at all",
function(name)
if type(Plain(name)) ~= "string" then return FAIL, "returned " .. Show(name) .. " for " .. SomeSpell() end
return PASS, ("%d is %s"):format(SomeSpell(), Show(name))
end)
ApiCheck("api.spellTexture", "C_Spell.GetSpellTexture",
function() return Field(Spells(), "GetSpellTexture") end,
function() return { SomeSpell() } end, WARN, "the prompt shows no spell icons")
ApiCheck("api.spellRange", "C_Spell.IsSpellInRange",
function() return Field(Spells(), "IsSpellInRange") end,
function() return { SomeSpell(), "player" } end,
function() return type(_G.IsSpellInRange) == "function" and WARN or FAIL end,
"range is asked of the old IsSpellInRange, if at all",
function(r)
r = Plain(r)
if r ~= nil and type(r) ~= "boolean" and type(r) ~= "number" then return WARN, "returned " .. Show(r) end
return PASS
end)
ApiCheck("api.spellUsable", "C_Spell.IsSpellUsable",
function() return Field(Spells(), "IsSpellUsable") end,
function() return { SomeSpell() } end, WARN, "usable is asked of the old IsUsableSpell, if at all")
ApiCheck("api.spellCooldown", "C_Spell.GetSpellCooldown",
function() return Field(Spells(), "GetSpellCooldown") end,
Table(GCD_SPELL), WARN, "the global cooldown is guessed at a second and a half",
function(cd)
if cd ~= nil and not Secret(cd) and type(cd) ~= "table" then return WARN, "returned " .. Show(cd) end
return PASS
end)
ApiCheck("api.weaponEnchants", "C_Item.GetWeaponEnchantInfo",
function() return Field(Items(), "GetWeaponEnchantInfo") end,
function()
local slots = _G.Enum and _G.Enum.WeaponSlot
return { slots and slots.MainHand or 0 }
end,
FAIL, "a scroll's imbue on your weapon cannot be seen (the 1.6.3 bug)",
function(list)
if Secret(list) then return WARN, "returned SECRET for the main hand" end
if type(list) ~= "table" then return FAIL, "returned " .. Show(list) .. ", not a list" end
local shown = {}
for i, entry in ipairs(list) do shown[i] = Show(entry) end
return PASS, ("%d on the main hand%s"):format(#list,
#shown > 0 and (": " .. table.concat(shown, ", ")) or "")
end, NoImbueWhy)
Check("api.enums", "api", "Enum.WeaponSlot / Enum.ItemEnchantType", function(add)
local enum = _G.Enum
local slots = Field(enum, "WeaponSlot")
local kinds = Field(enum, "ItemEnchantType")
local line = ("MainHand=%s Permanent=%s Temporary=%s Imbue=%s"):format(
Show(Field(slots, "MainHand")), Show(Field(kinds, "Permanent")),
Show(Field(kinds, "Temporary")), Show(Field(kinds, "Imbue")))
-- Core.lua falls back on 0, 2 and 3.
if type(Field(slots, "MainHand")) ~= "number" or type(Field(kinds, "Temporary")) ~= "number"
or type(Field(kinds, "Imbue")) ~= "number" then
local idle = NoImbueWhy()
if idle then
add(PASS, line .. " -- not used: " .. idle)
else
add(WARN, line .. " -- Manners assumes MainHand=0 Temporary=2 Imbue=3")
end
elseif kinds.Imbue == kinds.Temporary then
add(FAIL, line .. " -- Imbue and Temporary are the same number")
else
add(PASS, line)
end
end)
ApiCheck("api.tempEnchant", "C_PaperDollInfo.GetTemporaryEnchantmentInfo",
function() return Field(_G.C_PaperDollInfo, "GetTemporaryEnchantmentInfo") end,
Table(16), WARN, "only matters where C_Item.GetWeaponEnchantInfo is missing", nil, NoImbueWhy)
local function ScrollItem()
local family = OwnFamily(function(f) return f.scroll end)
return family and family.spells[1] and family.spells[1].item or ANY_ITEM
end
ApiCheck("api.itemCount", "C_Item.GetItemCount",
function() return Field(Items(), "GetItemCount") end,
function() return { ScrollItem() } end,
function() return HasScrolls() and FAIL or WARN end,
"scrolls in your bags cannot be counted",
function(n)
if type(Plain(n)) ~= "number" then return FAIL, "returned " .. Show(n) .. " for item " .. ScrollItem() end
return PASS, ("item %d: %d"):format(ScrollItem(), n)
end)
local function MainHandItem()
local ok, id = Ask(_G.GetInventoryItemID, "player", 16)
if ok == true and type(Plain(id)) == "number" then return id end
return ANY_ITEM
end
ApiCheck("api.itemInstant", "C_Item.GetItemInfoInstant",
function() return Field(Items(), "GetItemInfoInstant") end,
function() return { MainHandItem() } end,
function() return HasScrolls() and FAIL or WARN end,
"the weapon in your main hand cannot be told apart",
function(id, _, _, _, _, class, subclass)
if type(Plain(class)) ~= "number" then
return WARN, ("item %d: class %s, subclass %s"):format(MainHandItem(), Show(class), Show(subclass))
end
return PASS, ("item %s: class %s, subclass %s"):format(Show(id), Show(class), Show(subclass))
end)
Check("api.tracking", "api", "C_Minimap tracking", function(add)
local api = _G.C_Minimap
if type(Field(api, "GetNumTrackingTypes")) ~= "function" or type(Field(api, "GetTrackingInfo")) ~= "function" then
add(WARN, "missing -- no tracking reminders")
return
end
local okN, n = Ask(api.GetNumTrackingTypes)
local okI, info = true, nil
if okN and type(Plain(n)) == "number" and n > 0 then okI, info = Ask(api.GetTrackingInfo, 1) end
if not okN or not okI then
add(FAIL, "threw: " .. tostring(not okN and n or info))
elseif type(Plain(n)) ~= "number" then
add(WARN, "GetNumTrackingTypes returned " .. Show(n))
elseif n > 0 and (type(info) ~= "table" or Field(info, "spellID") == nil) then
add(WARN, ("%d types; the first is %s, with no spellID"):format(n, Show(info)))
else
add(PASS, ("%d types; the first %s"):format(n, Show(info)))
end
end)
Check("api.menu", "api", "MenuUtil.CreateContextMenu", function(add)
if type(Field(_G.MenuUtil, "CreateContextMenu")) == "function" then
add(PASS, "present")
else
add(FAIL, "missing -- the options window's dropdowns cannot open")
end
end)
Check("api.colorPicker", "api", "ColorPickerFrame:SetupColorPickerAndShow", function(add)
if type(Field(_G.ColorPickerFrame, "SetupColorPickerAndShow")) == "function" then
add(PASS, "present")
else
add(WARN, "missing -- the colour swatches on Look cannot open a picker")
end
end)
Check("api.settings", "api", "Settings.RegisterCanvasLayoutCategory", function(add)
local settings = _G.Settings
if type(Field(settings, "RegisterCanvasLayoutCategory")) == "function"
and type(Field(settings, "RegisterAddOnCategory")) == "function" then
add(PASS, "present")
else
add(WARN, "missing -- no Manners entry under Options > AddOns")
end
end)
-- Never called: it would emote.
Check("api.emote", "api", "C_ChatInfo.PerformEmote", function(add)
if type(Field(_G.C_ChatInfo, "PerformEmote")) == "function" then
add(PASS, "present (not called)")
elseif type(_G.DoEmote) == "function" then
add(WARN, "missing -- /thank goes through the old DoEmote")
else
add(FAIL, "missing, and DoEmote too -- /thank cannot go out")
end
end)
-- Every rank of every buff of your class, asked both ways: C_SpellBook, which
-- the probe asks first, and the shims Forever keeps only while the
-- deprecation fallbacks are on.
Check("api.spellbook", "api", "IsSpellKnown / IsPlayerSpell / C_SpellBook", function(add)
local book = _G.C_SpellBook
local inBook, known = Field(book, "IsSpellInSpellBook"), Field(book, "IsSpellKnown")
local enum = _G.Enum and _G.Enum.SpellBookSpellBank
local bank = enum and enum.Player or 0
local hasBook = type(inBook) == "function" or type(known) == "function"
local hasShim = type(_G.IsSpellKnown) == "function" or type(_G.IsPlayerSpell) == "function"
if not hasBook and not hasShim then
add(FAIL, "neither is there -- every spell reads as not learned")
return
end
local ids = {}
for _, buff in ipairs(ns.GetClassBuffs(Class()) or {}) do
for _, id in ipairs(buff.ranks) do ids[#ids + 1] = id end
end
for _, family in ipairs(ns.GetOwnFamilies(Class()) or {}) do
for _, spell in ipairs(family.spells) do
if not spell.item then
for _, id in ipairs(spell.ranks) do ids[#ids + 1] = id end
end
end
end
local bookYes, shimYes, differ = 0, 0, {}
for _, id in ipairs(ids) do
local b, s
if hasBook then
local ok1, a1 = Ask(inBook, id, bank, false)
local ok2, a2 = Ask(known, id, bank)
b = (ok1 == true and Plain(a1) == true) or (ok2 == true and Plain(a2) == true)
end
if hasShim then
local ok1, a1 = Ask(_G.IsSpellKnown, id)
local ok2, a2 = Ask(_G.IsPlayerSpell, id)
s = (ok1 == true and Plain(a1) == true) or (ok2 == true and Plain(a2) == true)
end
if b then bookYes = bookYes + 1 end
if s then shimYes = shimYes + 1 end
if hasBook and hasShim and b ~= s and #differ < 6 then differ[#differ + 1] = tostring(id) end
end
local line = ("%d ids: C_SpellBook knows %s, IsSpellKnown/IsPlayerSpell %s"):format(#ids,
hasBook and tostring(bookYes) or "missing", hasShim and tostring(shimYes) or "missing")
if #differ > 0 then
add(WARN, line .. " -- they disagree on " .. table.concat(differ, ", "))
elseif not hasBook then
add(WARN, line .. " -- only the deprecated shims answer")
else
add(PASS, line)
end
end)
Check("api.unitName", "api", "UnitName (surname)", function(add)
local ok, first, second = Ask(_G.UnitName, "player")
if ok ~= true then
add(FAIL, "UnitName " .. (ok == MISSING and "missing" or "threw"))
return
end
local surname = ns.caps and ns.caps.unitNameIsSurname
local line = ("%s, second %s, read as %s, full name %s"):format(Show(first), Show(second),
surname and "a surname" or "a realm", Show(ns.UnitFullName("player")))
second = Plain(second)
if surname and (type(second) ~= "string" or second == "") then
add(WARN, line .. " -- this client's names have surnames and none came back")
elseif not surname and type(second) == "string" and second ~= "" then
add(WARN, line .. " -- a second name on your own character, read as a realm")
else
add(PASS, line)
end
end)
-- What tells the options window a slider was let go of when its OnMouseUp
-- went missing (1.6.1: the window stayed faded).
Check("api.mouseButton", "api", "IsMouseButtonDown", function(add)
local ok, down = Ask(_G.IsMouseButtonDown, "LeftButton")
if ok == MISSING then
add(FAIL, "missing -- a fade whose mouse release goes missing never ends")
elseif not ok then
add(FAIL, "threw: " .. tostring(down))
elseif Plain(down) then
add(WARN, "the left button reads as held -- " .. Show(down))
else
add(PASS, "the left button reads as up -- " .. Show(down))
end
end)
local function CVar(name)
local get = Field(_G.C_CVar, "GetCVar")
if type(get) ~= "function" then get = _G.GetCVar end
local ok, value = Ask(get, name)
if ok ~= true then return nil, ok == MISSING and "missing" or "threw" end
return Plain(value)
end
Check("api.nameplateCVar", "api", "friendly nameplates setting", function(add)
local name, value = ns.FriendlyPlatesCVar()
if not name then
add(WARN, "GetCVar answered for none of " .. table.concat(ns.FRIENDLY_PLATES_CVARS, ", "))
elseif tostring(value) == "1" then
add(PASS, name .. " = 1 -- friendly nameplates are on")
else
add(WARN, name .. " = " .. Show(value) .. " -- friendly nameplates are off, so a passer-by's range is read only"
.. " while targeted or under the cursor")
end
end)
---------------------------------------------------------------------------
-- what Manners believes about this character
---------------------------------------------------------------------------
Check("beliefs.buffs", "beliefs", L["known buffs"], function(add)
local buffs = ns.GetClassBuffs(Class())
if not buffs then
add(PASS, tostring(Class()) .. " has no buffs for others")
return
end
local usable = Field(Spells(), "IsSpellUsable")
if type(usable) ~= "function" then usable = _G.IsUsableSpell end
for _, buff in ipairs(buffs) do
local info = ns.BuffInfo(buff)
local label = L["known buffs"] .. " " .. buff.key
if not info then
add(FAIL, "never probed", label)
elseif info.unresolved and #info.unresolved > 0 then
add(FAIL, ("the client has never heard of %s"):format(table.concat(info.unresolved, ", ")), label)
elseif not info.known then
add(PASS, ("%s: not learned"):format(Show(info.name)), label)
else
local ok, can = Ask(usable, info.topRank)
local castable
if ok == true then castable = Plain(can) end
local line = ("%s rank %s, readable=%s, castable=%s%s"):format(Show(info.name),
tostring(info.topRank), tostring(info.readable), Show(castable),
info.groupRank and (", group " .. Show(info.groupName)) or "")
add(castable == false and WARN or PASS, line, label)
end
end
end)
Check("beliefs.own", "beliefs", L["own buffs"], function(add)
local families = ns.KnownOwnFamilies()
if #families == 0 then
add(PASS, "none learned")
return
end
for _, family in ipairs(families) do
local up, spell, left, charges = KeepingMemory(ns.ReadOwnFamily, family)
local label = L["own buffs"] .. " " .. family.key
local pick = ns.OwnPick(family)
local line
if up == nil then
line = "the client would not say whether it is up"
elseif up then
line = ("up: %s%s%s"):format(spell and spell.key or "an enchant no scroll makes",
left and (", " .. math.floor(left) .. "s left") or "",
charges and (", " .. tostring(charges) .. " charges") or "")
else
line = "not up"
end
add(up == nil and WARN or PASS, ("pick %s, %s"):format(tostring(pick), line), label)
end
end)
Check("beliefs.scrolls", "beliefs", L["scrolls in bags"], function(add)
local any, held, unread = false, false, false
local count = Field(Items(), "GetItemCount")
for _, family in ipairs(ns.GetOwnFamilies(Class()) or {}) do
for _, spell in ipairs(family.scroll and family.spells or {}) do
any = true
local ok, n = Ask(count, spell.item)
n = ok == true and Plain(n) or nil
if type(n) ~= "number" then unread = true end
if type(n) == "number" and n > 0 then
held = true
local ready, why = ns.ScrollReady(spell)
local label = L["scrolls in bags"] .. " " .. spell.key
if why == "bags" then
add(WARN, ("%d in the bags, and Manners counts none yet"):format(n), label)
else
add(PASS, ("%d, %s"):format(n, ready and "ready" or ("held: " .. tostring(why))), label)
end
end
end
end
if not any then
add(PASS, tostring(Class()) .. " has no scrolls")
elseif type(count) ~= "function" then
add(FAIL, "C_Item.GetItemCount is missing, so none can be counted")
elseif not held then
-- Said once, when nothing above was: a mage with no scrolls on them is
-- the ordinary case, not "said nothing".
add(unread and WARN or PASS, unread and "the client would not say how many are in the bags"
or "none in the bags")
end
end)
-- The main hand's enchants read through every call the client has, and what
-- Manners makes of them. C_Item.GetWeaponEnchantInfo is the only one that
-- lists a scroll's imbue (Enum.ItemEnchantType.Imbue); the other two never
-- do. A Manners reading of "nothing on" over a list with an imbue in it is
-- the 1.6.3 bug.
Check("beliefs.mainHand", "beliefs", L["main hand enchants"], function(add)
local enum = _G.Enum
local slots, kinds = Field(enum, "WeaponSlot"), Field(enum, "ItemEnchantType")
local temporary, imbue = Field(kinds, "Temporary") or 2, Field(kinds, "Imbue") or 3
local label = L["main hand enchants"]
local okW, weapon = Ask(_G.GetInventoryItemID, "player", 16)
add(PASS, "weapon " .. (okW == true and Show(weapon) or "unreadable"), label)
local listed, listedOn, listedImbue
local okL, list = Ask(Field(Items(), "GetWeaponEnchantInfo"), Field(slots, "MainHand") or 0)
if okL == true and type(list) == "table" and not Secret(list) then
listed, listedOn, listedImbue = {}, false, false
for _, entry in ipairs(list) do
local on, kind = Plain(Field(entry, "hasEnchant")), Plain(Field(entry, "enchantType"))
if on == true and (kind == temporary or kind == imbue) then listedOn = true end
if on == true and kind == imbue then listedImbue = true end
listed[#listed + 1] = ("type=%s id=%s left=%s"):format(Show(kind),
Show(Field(entry, "enchantID")), Show(Field(entry, "timeLeft")))
end
add(PASS, "C_Item.GetWeaponEnchantInfo: " .. (#listed > 0 and table.concat(listed, "; ") or "nothing"), label)
else
-- Missing is a warning only where the character reads it (Forever's
-- scrolls); a class or client with no imbue has nothing to see.
local idle = okL == MISSING and NoImbueWhy()
add(okL == MISSING and (idle and PASS or WARN) or FAIL, "C_Item.GetWeaponEnchantInfo: "
.. (okL == MISSING and ("missing" .. (idle and (" -- not used: " .. idle) or ""))
or okL and ("returned " .. Show(list)) or ("threw: " .. tostring(list))), label)
end
local okP, paper = Ask(Field(_G.C_PaperDollInfo, "GetTemporaryEnchantmentInfo"), 16)
local paperOn = okP == true and paper ~= nil
add(PASS, "C_PaperDollInfo.GetTemporaryEnchantmentInfo: "
.. (okP == MISSING and "missing" or okP and Show(paper) or ("threw: " .. tostring(paper))), label)
local okO, has, expires, charges, id = Ask(_G.GetWeaponEnchantInfo)
local oldOn = okO == true and Plain(has) and true or false
add(PASS, "GetWeaponEnchantInfo: " .. (okO == MISSING and "missing"
or okO and ("%s, %s, %s, %s"):format(Show(has), Show(expires), Show(charges), Show(id))
or ("threw: " .. tostring(has))), label)
local family = ImbueFamily()
if not family then
if listed and not listedOn and (paperOn or oldOn) then
add(WARN, "the old calls see an enchant C_Item does not list", label)
else
add(PASS, tostring(Class()) .. " has no imbue reminder", label)
end
return
end
local up, spell = KeepingMemory(ns.ReadOwnFamily, family)
local reads = ("Manners reads %s%s"):format(up == nil and "cannot tell" or up and "on" or "nothing on",
spell and (" (" .. spell.key .. ")") or "")
if not listed then
add(FAIL, reads .. ", and the one call that lists a scroll's imbue gave nothing to read", label)
elseif listedImbue and up == false then
add(FAIL, reads .. ", but C_Item lists an imbue on", label)
elseif not listedOn and up == true then
add(FAIL, reads .. ", but C_Item lists nothing on", label)
elseif up == nil then
add(WARN, reads, label)
elseif listedImbue and not paperOn then
add(PASS, reads .. "; the imbue is seen only by C_Item, as expected", label)
elseif listedOn and not listedImbue and up == false then
-- A wizard oil on the staff: a Temporary enchant, which stays beside an
-- imbue, so the imbue is still to be put on.
add(PASS, reads .. "; C_Item lists a temporary enchant, which is no imbue", label)
else
add(PASS, reads, label)
end
end)
-- The familiar's aura read directly, beside what Manners reads.
Check("beliefs.familiar", "beliefs", L["familiar"], function(add)
local family = FamiliarFamily()
if not family then
add(PASS, tostring(Class()) .. " has no familiar")
return
end
local byId = Field(Auras(), "GetUnitAuraBySpellID")
local direct, refused
for _, spell in ipairs(family.spells) do
for _, id in ipairs(spell.auraIds or spell.ranks) do
local ok, aura = Ask(byId, "player", id)
if ok ~= true or Secret(aura) then
refused = true
elseif type(aura) == "table" then
direct = direct or spell
end
end
end
local up, spell = KeepingMemory(ns.ReadOwnFamily, family)
local line = ("the aura read by id: %s; Manners reads %s"):format(
direct and direct.key or refused and "refused" or "none",
up == nil and "cannot tell" or up and ("up (" .. (spell and spell.key or "?") .. ")") or "not up")
if direct and up == false then
add(FAIL, line, L["familiar"])
elseif up == true and not direct and not refused then
add(FAIL, line, L["familiar"])
elseif up == nil then
add(WARN, line, L["familiar"])
else
add(PASS, line, L["familiar"])
end
end)
---------------------------------------------------------------------------
-- the prompt and its secure button
---------------------------------------------------------------------------
local function Button()
local prompt = ns.Prompt
return prompt and prompt.regions and prompt.regions.button
end
Check("button.exists", "button", L["secure button"], function(add)
local b = Button()
if not b then
add(FAIL, "there is no prompt button")
return
end
local okName, name = Ask(b.GetName, b)
local okProt, protected = Ask(b.IsProtected, b)
local line = ("%s, global %s, protected %s"):format(okName == true and Show(name) or "?",
_G.MannersPrompt == b and "MannersPrompt" or "not MannersPrompt",
okProt == true and Show(protected) or "n/a")
if _G.MannersPrompt ~= b then
add(FAIL, line .. " -- the key binding and the /click macro name MannersPrompt")
elseif okProt == true and Plain(protected) == false then
add(FAIL, line .. " -- not a secure button")
else
add(PASS, line)
end
end)
Check("button.clicks", "button", L["clicks"], function(add)
local b = Button()
if not b then
add(FAIL, "there is no prompt button")
return
end
local clicks = ns.Prompt.regions.clicks
local hold = b:GetAttribute("pressAndHoldAction")
local keyDown = CVar("ActionButtonUseKeyDown")
local line = ("registered for %s, pressAndHoldAction=%s, ActionButtonUseKeyDown=%s"):format(
Show(clicks), Show(hold), Show(keyDown))
-- Registering down without pressAndHoldAction delivers the click and
-- casts nothing (Prompt/Panel.lua).
if clicks ~= "AnyDown" or not hold then
add(FAIL, line)
else
add(PASS, line)
end
end)
Check("button.macro", "button", L["armed macro"], function(add)
local b = Button()
if not b then
add(FAIL, "there is no prompt button")
return
end
local kind, text = b:GetAttribute("type1"), b:GetAttribute("macrotext1")
if type(text) ~= "string" or text == "" then
add(PASS, "nothing armed (type1=" .. Show(kind) .. ")")
return
end
local line = ("%d of %d characters, type1=%s: %s"):format(#text, ns.MACRO_LIMIT or 255, Show(kind),
(text:gsub("\n", " | ")))
if #text > (ns.MACRO_LIMIT or 255) then
add(FAIL, line .. " -- the client cuts it short")
elseif kind ~= "macro" then
add(WARN, line .. " -- armed text, but the button is not a macro button")
else
add(PASS, line)
end
end)
Check("button.binding", "button", L["binding"], function(add)
local asked = { Ask(_G.GetBindingKey, COMMAND) }
local keys = {}
if asked[1] == true then
for i = 2, table.maxn(asked) do
if type(Plain(asked[i])) == "string" then keys[#keys + 1] = asked[i] end
end
end
local okM, index = Ask(_G.GetMacroIndexByName, ns.CLICK_MACRO_NAME or "Manners")
local macro = okM == true and type(Plain(index)) == "number" and index > 0
local line = ("key %s, /click macro %s"):format(#keys > 0 and table.concat(keys, ", ") or "none",
macro and "made" or "none")
if #keys == 0 and not macro then
add(WARN, line .. " -- the prompt can only be clicked")
else
add(PASS, line)
end
end)
---------------------------------------------------------------------------
-- range: can it be read here, for whom
---------------------------------------------------------------------------
local function Exists(unit)
local ok, exists = Ask(_G.UnitExists, unit)
return ok == true and Plain(exists) and true or false
end
-- One unit measured as the scan measures it, and as raw as the client gives it.
local function Measure(add, unit, label, absent)
if not Exists(unit) then
add(WARN, absent, label)
return
end
local buff = ns.ResolveBuff(true)
if not buff then
add(WARN, "nothing to measure with: no buff learned or switched on", label)
return
end
local info = ns.BuffInfo(buff)
local okA, assist = Ask(_G.UnitCanAssist, "player", unit)
local okR, raw = Ask(Field(Spells(), "IsSpellInRange"), info and info.topRank, unit)
local reach = ns.ReachNow(unit, buff)
local near = ns.NearEnough(unit, true)
local line = ("%s: in reach of %s %s (C_Spell.IsSpellInRange %s), near enough %s, can assist %s"):format(
Show(ns.UnitFullName(unit)), Show(ns.BuffName(buff)), Show(reach),
okR == true and Show(raw) or "n/a", Show(near), okA == true and Show(assist) or "n/a")
add(reach == nil and WARN or PASS, reach == nil and (line .. " -- cannot tell") or line, label)
end
Check("range.target", "range", L["target"], function(add)
Measure(add, "target", L["target"], "no target -- target somebody friendly and run it again")
end)
Check("range.mouseover", "range", L["mouseover"], function(add)
Measure(add, "mouseover", L["mouseover"], "nobody under the cursor")
end)
Check("range.nameplates", "range", L["nameplates"], function(add)
local tracked, first = 0, nil
local units = {}
for unit in pairs(ns.nameplateUnits or {}) do units[#units + 1] = unit end
table.sort(units)
for _, unit in ipairs(units) do
tracked = tracked + 1
local ok, assist = Ask(_G.UnitCanAssist, "player", unit)
if not first and ok == true and Plain(assist) then first = unit end
end
local okP, plates = Ask(Field(_G.C_NamePlate, "GetNamePlates"))
local cvar, shown = ns.FriendlyPlatesCVar()
local line = ("%d tracked, C_NamePlate.GetNamePlates %s, %s %s"):format(tracked,
okP == true and type(plates) == "table" and tostring(#plates) or okP == MISSING and "missing" or "n/a",
cvar or "friendly nameplates", Show(shown))
if not first then
add(WARN, line .. " -- no friendly nameplate to measure", L["nameplates"])
return
end
add(PASS, line, L["nameplates"])
Measure(add, first, L["nameplates"], "gone")
end)
Check("range.proximity", "range", L["proximity"], function(add)
-- Graded by what Range.lua says it measures with, not by the words, which
-- are translated: "keine Messung", "aucun signal".
local summary, unmeasured = ns.ProximitySummary()
add(unmeasured == true and WARN or PASS, Strip(summary))
end)
-- What the press would do with the thank-you line (Prompt/Press.lua,
-- HoldLine) if your target were the one on the prompt.
Check("range.lineRule", "range", L["line rule"], function(add)
if type(ns.HoldLine) ~= "function" then
add(FAIL, "Prompt/Press.lua did not hand over HoldLine")
return
end
if not Exists("target") then
add(WARN, "no target -- target somebody friendly and run it again")
return
end
local buff = ns.ResolveBuff(true)
if not buff then