-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOptions.lua
More file actions
3607 lines (3474 loc) · 158 KB
/
Copy pathOptions.lua
File metadata and controls
3607 lines (3474 loc) · 158 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 -- options table, Blizzard settings panel, minimap button.
local ADDON, ns = ...
-- Player-facing text, in the client's language: see Locales/Init.lua.
local L = ns.L
local AceConfig = LibStub("AceConfig-3.0")
local AceConfigDialog = LibStub("AceConfigDialog-3.0")
-- Asked for optionally. It ships inside AceConfig-3.0 and will be there, but
-- the only thing that depends on it is the page repainting itself when a fight
-- ends -- and a missing library must not take the options screen with it.
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0", true)
local AceDBOptions = LibStub("AceDBOptions-3.0")
local LSM = LibStub("LibSharedMedia-3.0")
local LDB = LibStub("LibDataBroker-1.1", true)
local LDBIcon = LibStub("LibDBIcon-1.0", true)
-- The logo tools/make-icon.py draws, and the same file the toc's IconTexture
-- names, so the minimap button and the addon list show one picture. It was a
-- Blizzard spell icon while the TGA shipped in every zip with nothing pointing
-- at it. No extension: the client finds the .tga itself.
local ICON = "Interface\\AddOns\\Manners\\Textures\\Manners64"
-- Whether there is a minimap button at all.
--
-- It takes both libraries and SetupOptions only registers one when it has both:
-- LibDataBroker makes the data object, LibDBIcon is what puts it on the
-- minimap. Either missing and there is nothing on the minimap to show or hide.
local function HasMinimapButton()
return LDB ~= nil and LDBIcon ~= nil
end
-- The launcher, once it exists. Kept at file scope so its text can be put back
-- in step from outside SetupOptions, which runs once and then never again.
local broker
-- Asked with a net under it: the tooltip and the launcher text are read by
-- other addons' display frames, on their own schedule, and one of them asking
-- before AceDB has handed us a profile must not throw inside somebody else's
-- layout pass.
local function Enabled()
return ns.db ~= nil and ns.db.profile ~= nil and ns.db.profile.enabled == true
end
-- Whether an unlocked prompt is up to be dragged. Prompt:RefreshPanel takes the
-- panel down for a character with nothing it can cast, then for /manners off,
-- and only after both reads the lock -- so the lock alone put "up to be
-- dragged" into the tooltip, Who's next and the snooze note of a rogue, of a
-- mage who has not learned Arcane Intellect, and of an addon switched off, none
-- of which has anything on screen.
local function DragPanelUp()
return Enabled() and ns.db.profile.prompt.locked == false
and ns.caps ~= nil and ns.caps.anyKnown == true
end
-- How many people who buffed you are still waiting for one back.
--
-- The favours, and not the whole queue. A crowd in a city puts a dozen
-- strangers missing a buff into the queue on every scan, and a bar reading
-- "12 waiting" all evening is a number nobody reads twice; somebody who buffed
-- you is the one kind of person actually waiting for something. Counted by the
-- debt's live expiry, the one every other reader of the debts asks.
--
-- And none at all while "People who buffed me" is switched off. The debts
-- filed before it was turned off stay until they expire, but the queue ignores
-- every one of them, so counting them would have the bar promise a return the
-- addon is never going to offer.
local function WaitingCount()
local profile = ns.db and ns.db.profile
if not (profile and profile.sources and profile.sources.owed) then return 0 end
local owed, expiry = ns.owed, ns.DebtExpiry
if type(owed) ~= "table" or type(expiry) ~= "function" then return 0 end
local now, n = GetTime(), 0
for _, debt in pairs(owed) do
if type(debt) == "table" then
local ok, ends = pcall(expiry, debt)
if ok and type(ends) == "number" and ends > now then n = n + 1 end
end
end
return n
end
-- What the launcher says it is.
--
-- It used to say "Manners" and nothing else, which on a broker display is the
-- addon's name written next to the addon's icon -- so the only way to find out
-- whether it was switched on was to right-click it and read chat, and that
-- changes the answer. Off is the state worth carrying: the prompt simply never
-- appears, and from the outside that is exactly what a broken addon looks like.
--
-- Each state is one whole phrase with the addon's name passed in, so a
-- translation sees what the word describes and can put it, and its colour,
-- where its own grammar wants them. The name itself is never translated.
local function BrokerText()
-- A snooze is the other state in which no prompt appears on purpose, and
-- the end of it is the part worth reading off a bar. Only while on: off
-- outranks it, since a snooze ending brings nothing back while off.
local ends = Enabled() and ns.SnoozeEndsAt and ns.SnoozeEndsAt()
if ends then return L["%s |cffffd100snoozed until %s|r"]:format("Manners", ends) end
if not Enabled() then return L["%s |cffff8080off|r"]:format("Manners") end
-- Then the favours still to return, which is the number worth glancing at
-- a bar for. Nothing at all when there are none, so a quiet evening reads
-- as the name and nothing else, as it always has.
local waiting = WaitingCount()
if waiting == 1 then return L["%s |cff80e0801 waiting|r"]:format("Manners") end
if waiting > 1 then return L["%s |cff80e080%d waiting|r"]:format("Manners", waiting) end
return "Manners"
end
-- The colour the launcher's icon is drawn in: dimmed while snoozed, darker
-- still while switched off, as it is otherwise. The icon is the only part of
-- the launcher on the minimap that is always in view -- the text is only on a
-- broker bar, and the tooltip only on a hover -- so it is the one place a
-- glance can tell a resting addon from a working one.
--
-- Brightness only, never a hue. The icon is a gold arrow over a blue one, and
-- the amber the snooze used to multiply it by left the gold as it was and
-- turned the blue arrow olive: at minimap size it read as a different icon
-- rather than a resting one. Scaled evenly, both arrows keep their colours and
-- the three states read in order -- working, resting, off -- with the text and
-- the tooltip saying which.
local function IconTint()
if not Enabled() then return 0.4, 0.4, 0.4 end
if ns.SnoozeLeft and ns.SnoozeLeft() then return 0.7, 0.7, 0.7 end
return 1, 1, 1
end
---------------------------------------------------------------------------
-- get/set helpers
--
-- Each group binds to one table in the profile and uses the option's own key,
-- so adding an option is a one-liner rather than a pair of closures.
--
-- Where a control has moved between tabs its key has deliberately not changed,
-- and where it had to -- the three sound controls, which now sit beside the
-- flash setting on a tab that has its own `enabled` -- the get and set name the
-- profile field outright instead of reading it off the option's key. Moving a
-- setting must never be a setting reset.
---------------------------------------------------------------------------
local function bind(pathFn, after)
local get = function(info) return pathFn()[info[#info]] end
local set = function(info, value)
pathFn()[info[#info]] = value
if after then after() end
end
local getColor = function(info)
local c = pathFn()[info[#info]] or { 1, 1, 1, 1 }
return c[1], c[2], c[3], c[4] == nil and 1 or c[4]
end
local setColor = function(info, r, g, b, a)
pathFn()[info[#info]] = { r, g, b, a }
if after then after() end
end
return get, set, getColor, setColor
end
local function restyle() ns.Prompt:ApplyStyle() end
local function rescan() ns.addon:StartScanner() end
local function remacro() ns.Prompt:InvalidateMacro() end
local function restyleAndMacro()
ns.Prompt:InvalidateMacro()
ns.Prompt:ApplyStyle()
end
-- Redraw the page once a run of slider ticks has stopped.
--
-- For the Width and Height sliders, which shrink the icon to fit and so change
-- what another slider on the page should be showing. The dialog redraws a
-- slider when a drag is let go, but a mouse wheel never lets go of anything, so
-- without this a wheel left the Icon size slider showing a value the prompt was
-- no longer using. Held back rather than immediate because a redraw rebuilds
-- the slider being dragged under the pointer; each call replaces the one
-- before, so a wheel or a drag ends with a single redraw. C_Timer.After cannot
-- be cancelled, hence the token.
local repaintToken = 0
local function RepaintSoon()
repaintToken = repaintToken + 1
local mine = repaintToken
C_Timer.After(0.3, function()
if mine == repaintToken and ns.RefreshOptionsDisplay then
ns.Guard("icon repaint", ns.RefreshOptionsDisplay)
end
end)
end
local function P() return ns.db.profile.prompt end
local function S() return ns.db.profile.sources end
local function F() return ns.db.profile.filters end
local function T() return ns.db.profile.timing end
local function SND() return ns.db.profile.sound end
local function SP() return ns.db.profile.speech end
local function B() return ns.db.profile.buff end
local function PR() return ns.db.profile.priority end
local pGet, pSet, pGetColor, pSetColor = bind(P, restyle)
-- The launcher's "N waiting" counts favours only while "People who buffed me"
-- is on, so throwing that switch changes the number on the bar.
local sGet, sSet = bind(S, function()
if ns.RefreshBrokerText then ns.Guard("broker text", ns.RefreshBrokerText) end
end)
local fGet, fSet = bind(F)
-- The armed macro is only rebuilt when the candidate changes, so a filter that
-- alters what the macro says -- rather than who is on the prompt -- has to say
-- so. restoreTarget is the only one.
local fGetMacro, fSetMacro = bind(F, remacro)
local tGet, tSet = bind(T, rescan)
local spGet, spSet = bind(SP, remacro)
local bGet, bSet = bind(B, restyleAndMacro)
local prGet, prSet = bind(PR)
---------------------------------------------------------------------------
-- dynamic values
---------------------------------------------------------------------------
local function HasClassBuffs()
return ns.caps.hasClassBuffs == true
end
local function BuffChoices()
local values = { auto = L["Automatic"] }
for _, buff in ipairs(ns.GetClassBuffs(ns.caps.class) or {}) do
local info = ns.BuffInfo(buff)
local label = (info and info.name) or buff.key
if not (info and info.known) then label = L["%s |cff808080(not learned)|r"]:format(label) end
values[buff.key] = label
end
return values
end
-- The named distances, read off the list Core.lua measures with.
--
-- Both built from the one table rather than written out here as well: a
-- dropdown that has its own copy of the choices is a dropdown that can offer a
-- setting nothing implements, and an ordering with its own copy is one that
-- silently drops a new entry off the end.
local function ProximityChoices()
local values = {}
for _, tier in ipairs(ns.PROXIMITY) do
values[tier.key] = tier.name
end
return values
end
-- AceConfig sorts a select's values by their labels unless it is given an
-- order, and alphabetically these read "Anywhere I can cast", "Nearby", "Right
-- beside me" -- which is loosest to tightest by luck rather than by design. One
-- rename would scramble them.
local function ProximityOrder()
local keys = {}
for _, tier in ipairs(ns.PROXIMITY) do
keys[#keys + 1] = tier.key
end
return keys
end
-- The spell's name, with the one thing about it that changes who it is offered
-- to. A priest reading "Divine Spirit" has no way to know from the page that a
-- warrior will never see it.
--
-- Unless "Skip players the buff does nothing for" is off, which is the only
-- thing that holds a mana-only spell back from a warrior. With it off the
-- warrior is offered Divine Spirit, and the qualifier was a promise about a
-- filter that was not running.
local function BuffLabel(buff)
local label = ns.BuffName(buff)
if buff.manaOnly and F().relevantOnly then
label = L["%s |cff808080(mana users only)|r"]:format(label)
end
if buff.partyOnly then label = L["%s |cff808080(your group only)|r"]:format(label) end
return label
end
-- What "Automatic" will actually do, for this character, as it is configured
-- right now.
--
-- It used to name Wisdom and Might and nothing else -- the single class whose
-- auto pick depends on who is standing there -- so every other class read an
-- explanation of somebody else's spells. Automatic is a walk down the class
-- list now rather than one resolved choice, so the list in the order it is
-- walked is the answer, and it is taken from the same function the scan uses so
-- the two cannot drift.
local function AutoExplanation()
local castable = ns.CastableBuffs()
if #castable == 0 then
-- Three ways to have nothing to offer, and they are three different
-- problems with three different answers. One line saying "nothing is
-- switched on" would be wrong for two of them, and wrong in the
-- direction that sends somebody looking at the switches.
if not HasClassBuffs() then
return L["This character has nothing it can cast on another player."]
end
local anyKnown = false
for _, buff in ipairs(ns.GetClassBuffs(ns.caps.class) or {}) do
if ns.IsBuffKnown(buff) then anyKnown = true end
end
if not anyKnown then
return "|cffff8080"
.. L["You have not learned any of these yet, so nobody will be offered anything."]
.. "|r"
end
-- A fourth way, which arrived with the per-flavour tables: everything
-- learned and switched on, and the only thing learned is one Automatic
-- deliberately never reaches for -- a Mists warlock with Unending Breath
-- and no Dark Intent yet. Both answers above would be false, and the one
-- below would send somebody hunting for a switch that is already on.
for _, buff in ipairs(ns.GetClassBuffs(ns.caps.class) or {}) do
if buff.neverAuto and ns.IsBuffKnown(buff) and not B().skip[buff.key] then
return "|cffff8080"
.. L["Automatic never offers %s -- nobody standing in a city wants it -- so nobody will be offered anything."]
:format(ns.BuffName(buff))
.. "|r\n\n"
.. L["Pin it in the dropdown above if you want it given out anyway."]
end
end
-- Everything learned is switched off -- but the spells not learned yet
-- are still ticked below, and learning one of them brings the prompt
-- back without anybody touching a switch. "Every spell" and "never" are
-- only both true once those are unticked as well. A neverAuto spell is
-- left out: learning it would bring nothing back.
for _, buff in ipairs(ns.GetClassBuffs(ns.caps.class) or {}) do
if not buff.neverAuto and not ns.IsBuffKnown(buff) and not B().skip[buff.key] then
return "|cffff8080"
.. L["Every spell you have learned is switched off, so nothing is offered until you switch one back on or learn one of the others."]
.. "|r"
end
end
return "|cffff8080" .. L["Every spell below is switched off, so the prompt will never appear."]
.. "|r"
end
local names = {}
for _, buff in ipairs(castable) do
names[#names + 1] = "|cffffffff" .. BuffLabel(buff) .. "|r"
end
local list = table.concat(names, ", ")
-- Blessings overwrite one another, so for these classes Automatic is not a
-- walk at all: it gives one and stops. Saying "the first of these they are
-- missing" here would promise a rotation that would take away what the last
-- click gave.
--
-- "Left alone" rests on reading what they carry, and two things stop the
-- reading: "Always offer" chooses not to look, and a client that hides a
-- blessing's aura cannot. Either way the walk hands out the first blessing
-- that suits them -- deliberately, see PickBuffFor -- and for a mana user
-- wearing your Might that is Wisdom, which takes the Might away. The note
-- said it could not happen.
--
-- Three whole sentences rather than one with a clause bolted on, so that a
-- translation can put the exception wherever its own grammar wants it.
if ns.EXCLUSIVE_BUFFS[ns.caps.class] then
local hidden = false
for _, buff in ipairs(castable) do
local info = ns.BuffInfo(buff)
if not (info and info.readable) then hidden = true end
end
if F().whenBuffed == "always" then
return L["Your blessings replace one another, so Automatic gives one and stops: the first of %s that suits them. Anybody already carrying one of yours is left alone rather than handed a different one -- except with |cffffd100Always offer|r chosen, which does not look: then the first that suits them is offered, and it can replace one of yours."]
:format(list)
elseif hidden then
return L["Your blessings replace one another, so Automatic gives one and stops: the first of %s that suits them. Anybody already carrying one of yours is left alone rather than handed a different one -- except where the game won't show which blessing they carry: then the first that suits them is offered, and it can replace one of yours."]
:format(list)
end
return L["Your blessings replace one another, so Automatic gives one and stops: the first of %s that suits them. Anybody already carrying one of yours is left alone rather than handed a different one."]
:format(list)
end
local text = L["Automatic offers the first of these they are missing, in this order: %s."]
:format(list)
if ns.RotatesBuffs() then
text = text .. "\n|cff888888"
.. L["Where the game will not say what somebody is carrying, it moves down the list each time instead of offering the same one over and over."]
.. "|r"
end
return text
end
-- What pinning one spell means, and the one case where pinning is a silent
-- switch-off.
--
-- A pinned buff you have not learned is not a fallback: the scan resolves the
-- pin, finds it unlearned and offers that person nothing, and it does that for
-- everybody -- so the addon goes quiet with nothing anywhere saying why. The
-- pin is deliberately not reset for you (a failed spell probe must not rewrite
-- a setting), which is exactly why it has to be said out loud here.
local function PinExplanation()
local choice = B().choice
local buff = ns.FindBuff(ns.caps.class, choice)
local name = buff and ns.BuffName(buff) or tostring(choice)
if buff and ns.IsBuffKnown(buff) then
return L["Only |cffffffff%s|r is ever offered, to everybody, whatever else they are missing. The per-spell switches above apply to Automatic and are left alone while one spell is pinned."]
:format(name)
end
return "|cffff8080"
.. L["You have pinned %s, which you have not learned."]:format(name)
.. "|r\n\n"
.. L["Nothing will be offered to anybody until you learn it or switch back to Automatic -- a pinned spell is the only one considered, so there is nothing to fall back to."]
end
-- One toggle per spell the class can put on somebody else.
--
-- Built once, with the page: which spells a class has never changes during a
-- session -- only whether each is learned, which the label asks for live.
-- Sparse on the way in as well as out: switched on is the *absence* of a key,
-- so a profile nobody has touched stores nothing at all and every existing one
-- arrives with the whole list on.
local function AddBuffToggles(args)
local buffs = ns.GetClassBuffs(ns.caps.class) or {}
-- One spell is not a choice. The walk has nothing to walk, and a lone
-- toggle under "Automatic" reads as a second way to switch the addon off.
if #buffs < 2 then return end
for index, buff in ipairs(buffs) do
args["offer_" .. buff.key] = {
type = "toggle",
name = function()
local label = BuffLabel(buff)
if not ns.IsBuffKnown(buff) then
label = L["%s |cff808080(not learned)|r"]:format(label)
end
return label
end,
desc = L["Switched off, this one is never offered to anybody and Automatic walks straight past it. Everything else carries on as before."],
-- Sub-one steps so the whole block sits between the buff dropdown
-- and the Sources header whatever the class has, and in the order
-- the walk visits them.
order = 4 + index / 10,
width = "full",
-- A pinned spell is the only one considered, so these would be
-- switches over something that is not consulted. Only a pin of this
-- class's counts: another class's is Automatic here.
hidden = function() return ns.PinnedBuff() ~= nil end,
disabled = function() return not ns.IsBuffKnown(buff) end,
get = function() return not B().skip[buff.key] end,
set = function(_, value)
-- nil rather than false: AceDB stores the difference from the
-- defaults, and an empty table is stored as nothing.
B().skip[buff.key] = (not value) or nil
restyleAndMacro()
end,
}
end
end
-- Whether everything this character could offer reaches its party and nobody
-- else -- a warrior's Battle Shout. For these classes the strangers toggle is a
-- switch with nothing behind it. Core's answer, which the greeting and the
-- favour line read as well; a copy here is how the three would come to
-- disagree.
local function OnlyReachesGroup()
return ns.OnlyReachesGroup()
end
-- Whether nothing this character can offer takes a target at all -- a warrior,
-- whose Battle Shout is cast on himself and heard by the party.
--
-- CastLines builds no /target line for a selfCast buff and returns restore =
-- false with it, so for these classes the whole Targeting section is about a
-- line the macro will never contain: a toggle that does nothing and a note
-- explaining a /target that is not there. Computed the same way
-- OnlyReachesGroup is, and for the same reason -- it follows the per-spell
-- switches and a pin, so a warrior who learns something targetable gets the
-- control back on its own.
local function NeverTargets()
local castable = ns.CastableBuffs()
if #castable == 0 then return false end
for _, buff in ipairs(castable) do
if not buff.selfCast then return false end
end
return true
end
-- Which of the two things that can carry the reason colour is actually on
-- screen, given every setting that silently takes one away.
--
-- Both are switched off somewhere other than the dropdown that asks for them,
-- and neither says so: ApplyStyle refuses the stripe on the framed look, and
-- the ring is a texture *behind* the icon, so hiding the icon takes it -- and so
-- does rounding the icon off, which swaps that texture for a mask. Pick the
-- ring, round the icon, and the setting above reads "Ring around the icon" over
-- a prompt with no reason colour anywhere on it.
--
-- Returns two booleans rather than one, because the interesting answer is which
-- one is left, not merely whether any is.
local function AccentCarriers()
local p = P()
local mode = p.accentMode or "icon"
local ring = (mode == "icon" or mode == "both") and p.showIcon and not p.roundIcon
local stripe = (mode == "stripe" or mode == "both") and p.style ~= "framed"
return ring == true, stripe == true
end
-- Whether the copy-for-a-bug-report box is open.
--
-- A file local rather than a setting: it is a state of the window rather than
-- of the profile, and one that has no business surviving the window being shut.
-- Put back in OpenOptions and when the Settings page hides; see there.
local reportOpen = false
-- And the box holding these settings as text, for the same reasons.
local shareOpen = false
-- Everything somebody would otherwise be asked for twice, in one block that can
-- be selected and pasted. No colour codes: this is written to be quoted
-- somewhere that is not a chat frame.
local function BugReport()
local lines = { ("Manners %s"):format(tostring(ns.BUILD)) }
-- Guarded, not assumed. This is read from a `get`, which nothing wraps, and
-- a client without GetBuildInfo would otherwise take the whole page down at
-- the moment somebody is trying to report that something is broken.
local ok, version, build, _, toc = pcall(GetBuildInfo)
if ok and version then
lines[#lines + 1] = ("client %s (%s), interface %s")
:format(tostring(version), tostring(build), tostring(toc))
end
local caps = ns.caps
lines[#lines + 1] = ("class %s | secrets %s | auras secret now %s | nameplates %s")
:format(tostring(caps.class), tostring(caps.hasSecrets),
tostring(caps.aurasSecretNow), tostring(caps.namePlates))
-- Which spell tables this client was handed. Without it, a report about a
-- spell that is never offered cannot be told from a report about a spell
-- that no longer exists on the reporter's client.
lines[#lines + 1] = ("buff data %s%s"):format(tostring(ns.BUFFS_SOURCE),
ns.BUFFS_MISSING and (" -- " .. tostring(ns.BUFFS_MISSING)) or "")
for _, buff in ipairs(ns.GetClassBuffs(caps.class) or {}) do
local info = ns.BuffInfo(buff)
lines[#lines + 1] = (" %-12s known=%s readable=%s off=%s"):format(
buff.key,
tostring(info and info.known),
tostring(info and info.readable),
tostring(B().skip[buff.key] == true))
if info and info.unresolved and #info.unresolved > 0 then
lines[#lines + 1] = (" no such spell on this client: %s"):format(
table.concat(info.unresolved, ", "))
end
end
-- The settings that change what it does, rather than how it looks. A report
-- that leaves these out is a report about the defaults.
local db = ns.db.profile
lines[#lines + 1] = ("enabled=%s buff=%s sources owed/group/strangers=%s/%s/%s"
.. " whenBuffed=%s targetFirst=%s keepDebts=%s"):format(
tostring(db.enabled), tostring(db.buff.choice),
tostring(db.sources.owed), tostring(db.sources.group), tostring(db.sources.strangers),
tostring(db.filters.whenBuffed), tostring(db.priority.target),
tostring(db.timing.keepDebts))
-- Who is ordered and who is held back, as opposed to who is on the list at
-- all. A report of "my friend is never offered" is answered by the last
-- number here more often than by anything else.
lines[#lines + 1] = ("friendsFirst=%s restingOnly=%s neverOffered=%d"):format(
tostring(db.priority.friends), tostring(db.filters.restingOnly), #ns.NeverList())
local scan = ns.auraScan
lines[#lines + 1] = ("own buffs: %s read, baseline %s, primed=%s, doubt=%s"):format(
tostring(scan.read), tostring(scan.held), tostring(scan.primed), tostring(scan.doubt))
-- The second favour source, where the client has one. Left out entirely
-- rather than reported as zeroes on a client with no combat log: a line
-- about a source that cannot exist there is a question the person reading
-- the report has to go and answer before they can ignore it.
if caps.combatLog then
local log = ns.logScan
lines[#lines + 1] = ("combat log: armed=%s, %s seen, %s filed"):format(
tostring(log.armed), tostring(log.applied), tostring(log.noted))
end
if #ns.errors == 0 then
lines[#lines + 1] = "errors: none this session"
else
-- How many have happened, then how many are still here to read. The ring
-- holds thirty, so its length was never the count this line claimed to
-- print: "errors: 30 this session" is what a handler throwing on every
-- frame looks like and what three unrelated bugs look like, and the
-- person receiving this report cannot ask which.
lines[#lines + 1] = ("errors: %d this session (%d kept), last five:")
:format(ns.errorCount or #ns.errors, #ns.errors)
for i = math.max(1, #ns.errors - 4), #ns.errors do
local e = ns.errors[i]
lines[#lines + 1] = (" %s %s -- %s"):format(
tostring(e.at), tostring(e.where), tostring(e.err))
end
end
return table.concat(lines, "\n")
end
-- Who is picked in the never-offer dropdown, waiting for Take them off. A file
-- local for the reason reportOpen is one: it is a state of the window, not of
-- the profile.
local neverPicked
-- The never-offer list as dropdown choices, built fresh each time the page asks,
-- because a shift-right-click on the prompt or /manners never can add to it
-- while the page is open.
local function NeverChoices()
local values = {}
for _, name in ipairs(ns.NeverList()) do values[name] = name end
return values
end
---------------------------------------------------------------------------
-- options table
---------------------------------------------------------------------------
local function BuildOptions()
local who = {
type = "group",
name = L["Who to buff"],
order = 2,
hidden = function() return not HasClassBuffs() end,
args = {
buffsHeader = { type = "header", name = L["Buffs"], order = 1 },
choice = {
type = "select",
name = L["Buff to cast"],
order = 2,
values = BuffChoices,
-- What the walk is honouring, rather than what is stored. The
-- profile is shared, so a pin can be another class's, and read
-- raw the dropdown was blank over a walk that was Automatic.
get = function()
local pin = ns.PinnedBuff()
return pin and pin.key or "auto"
end,
set = bSet,
},
autoNote = {
type = "description",
order = 3,
hidden = function() return ns.PinnedBuff() ~= nil end,
name = function() return AutoExplanation() end,
},
-- Below the per-spell switches, because when it is red the thing it
-- is about is the dropdown two controls up rather than the switches.
pinNote = {
type = "description",
order = 5,
fontSize = "medium",
hidden = function() return ns.PinnedBuff() == nil end,
name = function() return PinExplanation() end,
},
sourcesHeader = { type = "header", name = L["Sources"], order = 10 },
-- Three toggles, all off, and the only symptom is a prompt that
-- never appears -- which is what a broken addon looks like.
emptyWarning = {
type = "description",
order = 10.5,
hidden = function()
local s = S()
-- A source this class cannot use does not count as switched
-- on. A warrior's Battle Shout reaches the group and nobody
-- else, so the page hides "passers-by" -- and this used to
-- read the hidden toggle's leftover true and stay silent,
-- in exactly the case where the prompt really was dead and
-- no visible control could explain it.
return s.owed or s.group or s.asked or (s.strangers and not OnlyReachesGroup())
end,
name = "|cffff8080"
.. L["Nothing below is switched on, so the prompt will never appear."] .. "|r",
},
owed = {
type = "toggle",
name = L["People who buffed me"],
-- A function, because the second sentence is not true of every
-- class. A warrior's shout reaches the group and nobody else, so
-- a stranger who buffed him is turned down until they join --
-- which is what the favour line in chat says, and what the
-- strangers note a few lines down says too.
--
-- In a raid on the older flavours that group is the warrior's
-- own subgroup, and saying "group" there told somebody already in
-- the raid to join it.
desc = function()
if OnlyReachesGroup() then
if ns.PARTY_IS_SUBGROUP then
return L["Watch for buffs cast on you and offer to return them. What you cast reaches only your own party -- in a raid, your own subgroup -- so somebody outside it is offered once they join."]
end
return L["Watch for buffs cast on you and offer to return them. What you cast reaches your group only, so somebody outside it is offered once they join."]
end
return L["Watch for buffs cast on you and offer to return them. Works on strangers who are not in your group."]
end,
order = 11,
width = "full",
get = sGet,
set = sSet,
},
owedClassBuffsOnly = {
type = "toggle",
name = L["Only count real class buffs"],
desc = L["A shield, a heal-over-time or a trinket proc is not a favour owed. Leave this on unless you want every incoming aura to count."],
order = 12,
width = "full",
disabled = function() return not S().owed end,
get = sGet,
set = sSet,
},
group = {
type = "toggle",
name = L["My party and raid"],
order = 13,
width = "full",
get = sGet,
set = sSet,
},
strangers = {
type = "toggle",
name = L["Nearby players not in my group"],
-- Four tokens are walked, not three: IterateUnits asks target,
-- mouseover and focus before it touches a single nameplate.
-- Leaving focus out made a genuine way of reaching somebody
-- look like it was not one.
desc = L["Offer passers-by who are missing the buff. Seen through nameplates, your target, your focus and your mouseover."],
order = 14,
width = "full",
-- Hidden, not disabled: a disabled control is one you could
-- have if something else were different, and there is nothing
-- on this page that would ever make a shout reach a stranger.
hidden = OnlyReachesGroup,
get = sGet,
set = sSet,
},
strangersNote = {
type = "description",
order = 14.5,
hidden = function() return not OnlyReachesGroup() end,
name = "|cff888888"
.. L["Everything you can offer is cast on yourself and heard by your party, so there is nothing to give a passer-by."]
.. "|r",
},
-- The source that reads chat. The rule is spelled out here in full,
-- because it is the whole of what decides whether somebody is put on
-- the prompt, and "why did it offer them" has no other answer the
-- player can see. The section in Core.lua is the same rule in code.
asked = {
type = "toggle",
name = L["People who ask me for it"],
desc = L["Somebody who asks for your buff in /say, /yell, your group's chat or a whisper is offered it for the next minute, if the game can see them in that time and they do not have it yet -- \"int pls\", \"fort?\", \"can I get motw\", \"buffs please\", or the spell's own name in your language. They come after people who buffed you and before your group. Nothing is said back to them, and nobody of your own class is taken for asking."]
.. "\n\n"
.. L["Only short messages that ask count: eight words at most, the buff named as a whole word, nothing saying no or not, and a please, a question mark, an opening like \"can I\" or \"anyone\", or nothing but the buff's name. Beside a nickname like int or fort, or beside \"buff\", only small words like \"me\", \"get\" or \"pls\" may stand, so \"int the healer\" and \"need int ring\" ask for nothing. Words English uses for other things -- might, mark, wisdom, spirit, shadow -- need a please or to stand alone, and never count in your group's chat."]
.. "\n\n|cff888888"
.. L["Off at first, because reading chat is guesswork: now and then somebody only talking about a buff will be offered one. What is said in a fight is taken for tactics and ignored, except a whisper; a request still waiting when a fight starts waits until it ends."]
.. "|r",
order = 14.6,
width = "full",
get = sGet,
set = sSet,
},
reasonAsked = {
type = "input",
name = L["Wording: asked for it"],
desc = L["The prompt's second line for somebody who asked for the buff in chat."],
order = 14.7,
disabled = function() return not S().asked end,
get = pGet,
set = pSet,
},
-- Not a source: everybody here is already on the list by one of the
-- three above. This decides who reaches the top of it, which is its
-- own question and used to have no answer on the page at all.
firstHeader = { type = "header", name = L["Who comes first"], order = 15 },
target = {
type = "toggle",
name = L["Whoever I have targeted comes first"],
-- The second condition is the same one as the first, arriving
-- from the When tab: Always offer means nobody's buffs are read,
-- so there is never a reading to promote a target on. The
-- switch stayed ticked and did nothing, and nothing said why.
desc = L["Targeting somebody is the plainest way of saying you mean them, so they outrank a favour owed -- but only when the game lets us read that they are genuinely missing the buff. Switched off, a target is ranked by why they are on the list like anybody else."]
.. "\n\n"
.. L["Not while |cffffd100If they already have the buff|r is set to Always offer, under When: nothing is read then, so your target is ranked by why they are on the list like anybody else."]
.. "\n\n|cff888888"
.. L["Mouseover is deliberately left out: at a scan every four tenths of a second the prompt would flicker as the cursor crossed the screen."]
.. "|r",
order = 16,
width = "full",
get = prGet,
set = prSet,
},
friends = {
type = "toggle",
name = L["My friends and guildmates come before the others"],
-- Inside a kind of offer and never across one, which is what the
-- sort does; see BuildQueue. Saying "ahead of strangers" alone
-- would promise a friend passing by a place above your group.
-- Your target is named only where it is true: a target is put
-- first by the switch above, which needs their buffs readable,
-- and a stranger's often are not. Otherwise a targeted stranger
-- is a passer-by like any other, and a friend goes ahead of them.
desc = L["A friend or guildmate passing by comes ahead of the other passers-by, and one in your group ahead of the rest of your group. People who buffed you still come first, and so does your target whenever |cffffd100Whoever I have targeted comes first|r puts them there. Nobody is added or left out by this -- it only changes the order."]
.. "\n\n|cff888888"
.. L["Friends include Battle.net friends. When the game will not say whether somebody is a friend, they are ranked like anybody else."]
.. "|r",
order = 17,
width = "full",
get = prGet,
set = prSet,
},
skipHeader = { type = "header", name = L["Who to skip"], order = 20 },
relevantOnly = {
type = "toggle",
name = L["Skip players the buff does nothing for"],
desc = L["Mana-only buffs such as Arcane Intellect, Wisdom and Divine Spirit are wasted on warriors and rogues."],
order = 21,
width = "full",
get = fGet,
set = fSet,
},
requireInRange = {
-- It was called "Only players in range", which is what people
-- read and is not what it does -- its own description said so
-- one line below. The label has to be the promise.
type = "toggle",
name = L["Hide players known to be out of range"],
desc = L["When the game will not tell us the range -- common on this client -- they are still offered."],
order = 22,
width = "full",
get = fGet,
set = fSet,
},
proximity = {
type = "select",
name = L["How near a passer-by has to be"],
-- The yardage is here rather than in the choices themselves:
-- what somebody picks is a feeling, and nobody can judge ten
-- yards from inside the game -- but they will want to know
-- roughly what they just asked for.
desc = L["Being in range is not the same as being near. Arcane Intellect and its like reach about thirty yards, which in a city is everybody on the screen."]
.. "\n\n" .. L["|cffffd100Anywhere I can cast|r -- about thirty yards, as it was."]
.. "\n" .. L["|cffffd100Nearby|r -- about ten yards."]
.. "\n" .. L["|cffffd100Right beside me|r -- about five yards."]
.. "\n\n"
.. L["This only applies to passers-by. Somebody who buffed you was close enough a moment ago, your group is your group, and whoever you have targeted or focused you picked on purpose -- none of them are measured."]
.. "\n\n|cff888888"
.. L["The game will not say how far away somebody is, so this is measured with whatever this client offers and lands on the nearest step it has. When it cannot measure at all, everybody in casting range is offered, as before."]
.. "|r",
order = 22.5,
width = "full",
values = ProximityChoices,
sorting = ProximityOrder,
-- The setting is about passers-by and nothing else, so it is
-- hidden exactly where the passer-by toggle is and switched off
-- exactly when that toggle is.
hidden = OnlyReachesGroup,
disabled = function() return not S().strangers end,
get = fGet,
set = fSet,
},
proximityNote = {
type = "description",
order = 22.6,
hidden = function()
return OnlyReachesGroup() or F().proximity == "cast"
end,
-- Where the promise is kept. A distance filter that has quietly
-- stopped measuring offers the same crowded queue it always
-- did, and a user who has just turned it on has no way to tell
-- that from nobody being nearby -- so the page says which
-- signal is doing the work and how often it answers, in the
-- one place they are already looking.
name = function()
return "|cff888888" .. tostring(ns.ProximitySummary()) .. "|r"
end,
},
restingOnly = {
type = "toggle",
name = L["Only offer passers-by in cities and inns"],
-- Hidden and disabled exactly where the distance setting above
-- is, and for the same reasons: it is about passers-by and
-- nothing else.
desc = L["Out in the world, passers-by are left alone; they are offered only where the game shows you as resting, which is in a city or an inn."]
.. "\n\n"
.. L["Somebody who buffed you, your group, and whoever you have targeted or focused are offered anywhere."]
.. "\n\n|cff888888"
.. L["If the game will not say whether you are resting, passers-by are offered as usual."]
.. "|r",
order = 22.7,
width = "full",
hidden = OnlyReachesGroup,
disabled = function() return not S().strangers end,
get = fGet,
set = fSet,
},
reachableOnly = {
type = "toggle",
name = L["Drop people who are probably gone"],
desc = L["Somebody who buffed you is rarely your target or showing a nameplate, so there is usually no way to range-check them. What we do know is that they were within casting range the moment they buffed you. With this on, that counts for a short while and then they are let go."],
order = 23,
width = "full",
get = fGet,
set = fSet,
},
graceSeconds = {
-- Named so it stands on its own. It used to read "...after this
-- long", which only makes sense directly under the toggle above
-- -- and directly under it is exactly where a duplicate order
-- number stopped putting it.
type = "range",
name = L["Let them go after (seconds)"],
-- It said "once we can no longer see the player", which is not the
-- clock this runs on. BuildQueue measures from the moment they
-- buffed you -- that moment is the whole of the evidence, because
-- it is the one instant they were provably in casting range -- and
-- nothing anywhere notices a player walking off. Somebody who
-- buffed you two minutes ago and has not moved is let go on exactly
-- the same schedule as somebody who left at once.
desc = L["How long after somebody buffs you that counts as proof they were in range. It runs from their buff, not from the moment they walk off: nothing here can see them go."],
order = 23.5,
min = 10,
max = 180,
step = 5,
disabled = function() return not F().reachableOnly end,
get = tGet,
set = tSet,
},
minLevel = {
type = "range",
name = L["Minimum level"],
desc = L["Players below this are never offered. The level is read off the unit, so somebody we only know by name -- the usual case for a passer-by who buffed you -- cannot be level-checked at all and is offered anyway."],
order = 24,
min = 1,
max = 60,
step = 1,
get = fGet,
set = fSet,
},
neverHeader = { type = "header", name = L["Never offer"], order = 30 },
neverNote = {
type = "description",
order = 31,
fontSize = "medium",
name = function()
local count = #ns.NeverList()
if count == 0 then
return L["Nobody is on the list. Shift-right-click the prompt to put whoever it is showing on it, or add a name below."]
end
-- The exception is the decision this section rests on, so it
-- is said every time the list is, rather than once in a
-- tooltip nobody hovers.
local text = count == 1
and L["One person is on the list. They are never offered anything as a passer-by or as a member of your group."]
or L["%d people are on the list. They are never offered anything as passers-by or as members of your group."]:format(count)
return text .. "\n\n"
.. L["Somebody on it who buffs you is still offered the favour back: returning a favour is what Manners is for. Shift-right-click them on the prompt to let that favour go."]
end,
},
neverAdd = {
type = "input",
name = L["Add somebody by name"],
desc = L["Spelled the way the prompt shows them. Capitals do not matter."],
order = 32,
width = "full",
-- Always empty: it is a box to type into, not a setting with a
-- value to show back.
get = function() return "" end,
set = function(_, value) ns.PutOnNeverList(value) end,
},
neverPick = {
type = "select",
name = L["On the list"],
order = 33,
values = NeverChoices,
disabled = function() return #ns.NeverList() == 0 end,
-- Only somebody still on the list: the pick outlives a removal
-- made from chat, and a dropdown showing a name that is no
-- longer there offers a Remove that does nothing.
get = function()
if neverPicked and ns.IsNeverOffered(neverPicked) then return neverPicked end
return nil
end,
set = function(_, value) neverPicked = value end,
},
neverRemove = {
type = "execute",
name = L["Take them off"],
order = 34,
disabled = function()
return not (neverPicked and ns.IsNeverOffered(neverPicked))
end,
func = function()
local name = neverPicked and ns.AllowAgain(neverPicked)
neverPicked = nil
if name then
ns.addon:Print(L["|cffffffff%s|r can be offered again."]:format(name))
end
end,
},
neverClear = {
type = "execute",
name = L["Clear the list"],
order = 35,
disabled = function() return #ns.NeverList() == 0 end,
confirm = true,