-
-
Notifications
You must be signed in to change notification settings - Fork 30
/
ml_core.lua
1659 lines (1502 loc) · 65.7 KB
/
ml_core.lua
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
--[[--- ml_core.lua Contains core elements for the MasterLooter.
Assumes several functions in SessionFrame and VotingFrame.
@author Potdisc
]]
--[[ COMMS:
MAIN:
pI T - PlayerInfo sent from candidate.
MLdb_request T - Candidate request for "Mldb".
council_request T - Candidate requests council.
reconnect T - Candidate has reconnect - needs all data.
lootTable T - We've received the LootTable we sent out.
tradable T - Candidate has looted a tradeable item.
n_t T - Candidate received "non-tradeable" loot.
trade_complete T - Candidate completed a trade.
trade_WrongWinner T - Candidate traded an item to the wrong person.
bonus_roll T - Canidate performed bonus roll.
r_t T - Candidate "rejected_trade" of loot.
]]
--[[TODOs/NOTES:
]]
--- @type RCLootCouncil
local addon = select(2, ...)
--- @class RCLootCouncilML : AceModule, AceEvent-3.0, AceBucket-3.0, AceTimer-3.0, AceHook-3.0
_G.RCLootCouncilML = addon:NewModule("RCLootCouncilML", "AceEvent-3.0", "AceBucket-3.0", "AceTimer-3.0", "AceHook-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("RCLootCouncil")
-- Lua
local time, date, tonumber, unpack, select, wipe, pairs, ipairs, format, table, tinsert, tremove, bit, tostring, type
= time, date, tonumber, unpack, select, wipe, pairs, ipairs, format, table, tinsert, tremove, bit, tostring, type
local db;
local LOOT_TIMEOUT = 5 -- If we give loot to someone, but loot slot is not cleared after this time period, consider this loot distribute as failed.
-- The real time needed is the sum of two players'(ML and the awardee) latency, so 1 second timeout should be enough.
-- v2.17: There's reports of increased latency, especially in Classic - bump to 3 seconds.
local COUNCIL_COMMS_THROTTLE = 5
local Player = addon.Require "Data.Player"
local Council = addon.Require "Data.Council"
local Comms = addon.Require "Services.Comms"
local TempTable = addon.Require "Utils.TempTable"
local MLDB = addon.Require "Data.MLDB"
local ErrorHandler = addon.Require "Services.ErrorHandler"
local ItemUtils = addon.Require "Utils.Item"
local subscriptions
function RCLootCouncilML:OnInitialize()
self.Log = addon.Require "Utils.Log":New("ML")
self.Log("Init")
self.Send = Comms:GetSender(addon.PREFIXES.MAIN)
end
function RCLootCouncilML:OnDisable()
self.Log("Disabled")
self:UnregisterAllEvents()
self:UnregisterAllBuckets()
self:UnregisterAllMessages()
self:UnhookAll()
for _,v in ipairs(subscriptions) do v:unsubscribe() end
end
function RCLootCouncilML:OnEnable()
self.Log "Enabled"
db = addon:Getdb()
self.lootTable = {} -- The MLs operating lootTable, see ML:AddItem()
self.oldLootTable = {}
self.lootQueue = {} -- Items ML have attempted to give out that waiting for LOOT_SLOT_CLEARED
self.running = false -- true if we're handling a session
self:UpdateGroupCouncil()
self.combatQueue = {} -- The functions that will be executed when combat ends. format: [num] = {func, arg1, arg2, ...}
self.timers = {} -- Table to hold timer references. Each value is the name of a timer, whose value is the timer id.
self.groupSize = 0
self.printSessionHelp = false -- Print help message when session starts
self:RegisterEvent("CHAT_MSG_WHISPER", "OnEvent")
self:RegisterEvent("PLAYER_REGEN_ENABLED", "OnEvent")
self:RegisterEvent("ENCOUNTER_START", "OnEvent")
self:RegisterBucketEvent("GROUP_ROSTER_UPDATE", 5, "OnGroupRosterUpdate")
self:RegisterBucketMessage("RCConfigTableChanged", 5, "ConfigTableChanged") -- The messages can burst
self:RegisterMessage("RCCouncilChanged", "CouncilChanged")
self:RegisterComms()
-- Subscribe after comms, as that will override the table
tinsert(subscriptions, addon.Require "Utils.GroupLoot".OnLootRoll:subscribe(
function (...)
self:OnGroupLootRoll(...)
end))
end
function RCLootCouncilML:GetItemInfo(item)
local name, link, rarity, ilvl, iMinLevel, type, subType, iStackCount, equipLoc, texture, sellPrice, typeID, subTypeID, bindType, expansionID, itemSetID, isCrafting = C_Item.GetItemInfo(item) -- luacheck: ignore
local itemID = ItemUtils:GetItemIDFromLink(link)
if name then
-- Most of these are kept for use in SessionFrame
return {
string = ItemUtils:GetTransmittableItemString(link),
["link"] = link,
["ilvl"] = addon:GetTokenIlvl(link) or ilvl, -- if the item is a token, ilvl is the min ilvl of the item it creates.
["texture"] = texture,
["token"] = itemID and RCTokenTable[itemID],
["classes"] = addon:GetItemClassesAllowedFlag(link)
}
else
return nil
end
end
--- Add an item to the lootTable
--- You CAN sort or delete entries in the lootTable while an item is being added.
--- @paramsig item[, bagged, slotIndex, index]
--- @param item ItemID|itemString|itemLink
--- @param bagged Item? The Item as stored in ItemStorage. Item is bagged if not nil.
--- @param slotIndex integer? Index of the lootSlot, or nil if none - either this or 'bagged' needs to be supplied
--- @param owner? string The owner of the item (if any). Defaults to 'BossName'.
--- @param entry? table Used to set data in a specific lootTable entry.
--- @param boss? string Set to override boss name. Defaults to `RCLootCouncil.bossName`.
function RCLootCouncilML:AddItem(item, bagged, slotIndex, owner, entry, boss)
self.Log:d("AddItem", item, bagged, slotIndex, owner, entry, boss)
addon:LogItemGUID(item)
if type(item) == "string" and item:find("|Hcurrency") then return end -- Ignore "Currency" item links
-- Not having the lootTable is a sure sign that the module isn't enabled.
if not self.lootTable then
if not self:IsEnabled() and addon.isMasterLooter then
ErrorHandler:ThrowSilentError("ML module not enabled @AddItem")
addon:StartHandleLoot()
end
end
if not entry then
entry = {}
self.lootTable[#self.lootTable + 1] = entry
entry.attempts = 0
else
local attempts = entry.attempts -- Attempts should persist
wipe(entry) -- Clear the entry. Don't use 'entry = {}' here to preserve table pointer.
entry.attempts = attempts
end
entry.bagged = bagged
entry.lootSlot = slotIndex
entry.awarded = false
entry.owner = owner
entry.boss = boss or addon.bossName
entry.isSent = false
entry.typeCode = addon:GetTypeCodeForItem(item)
local itemInfo = self:GetItemInfo(item)
if itemInfo then
for k, v in pairs(itemInfo) do
entry[k] = v
end
end
-- Item isn't properly loaded, so update the data next frame (Should only happen with /rc test)
if not itemInfo then
entry.attempts = entry.attempts + 1
-- Give it 20 attempts to find the item (roughly 1 second)
if entry.attempts >= 20 then
tDeleteItem(self.lootTable, entry)
wipe(entry)
self.Log:D("Couldn't find item info for ", item)
addon:Print(format(L["ML_ADD_ITEM_MAX_ATTEMPTS"], tostring(item)))
self:ShowSessionFrame(self.lootTable)
return
end
self:ScheduleTimer("Timer", 0.05, "AddItem", item, bagged, slotIndex, owner, entry, boss)
self.Log:d("Started timer:", "AddItem", "for", item)
else
entry.attempts = nil
addon:SendMessage("RCMLAddItem", item, entry)
end
end
---Show the session frame with the given lootTable or `self.lootTable`.
---@param lootTable table Defaults to `self.lootTable`.
---@param disableAwardLater boolean Defaults to `false`. See [SessionFrame:Show()](lua://RCSessionFrame.Show)
function RCLootCouncilML:ShowSessionFrame(lootTable, disableAwardLater)
addon:CallModule("sessionframe")
addon:GetActiveModule("sessionframe"):Show(lootTable or self.lootTable, disableAwardLater)
end
--- Removes everything that doesn't need to be sent in the lootTable
---@param overrideIsSent boolean @Ignores .isSent status and adds the item anyway.
---@return table LootTable
function RCLootCouncilML:GetLootTableForTransmit(overrideIsSent)
local copy = CopyTable(self.lootTable)
for k, v in pairs(copy) do
if not overrideIsSent and v.isSent then -- Don't retransmit already sent items
copy[k] = nil
else
v.bagged = nil
v.lootSlot = nil
v.awarded = nil
v.classes = nil
v.isSent = nil
v.link = nil
v.ilvl = nil
v.texture = nil
v.token = nil
end
end
return copy
end
--- Removes a session from the lootTable
-- @param session The session (index) in lootTable to remove
function RCLootCouncilML:RemoveItem(session)
tremove(self.lootTable, session)
end
local function SendCouncil ()
local council = Council:GetForTransmit()
RCLootCouncilML:Send("group", "council", council)
TempTable:Release(council)
RCLootCouncilML.timers.council_send = nil
end
local function OnCouncilCooldown ()
RCLootCouncilML.timers.council_cooldown = nil
end
-- Quick solution for throtteling council comms.
-- Group Loot support expects the ML to always send council, which is doesn't
-- if changing from ML to GL (as the ML hasn't changed).
-- We will receive numurous `council_request`, but only need to reply once.
-- Same goes for a few detected edge cases in ML where council isn't properly sent (reason unknown).
function RCLootCouncilML:SendCouncil ()
if self.timers.council_cooldown then
if self.timers.council_send then
return -- do nothing, comm is queued.
else -- Cooldown, but nothing queued - queue the command for when cooldown is done.
local timeRemaining = self:TimeLeft(self.timers.council_cooldown)
self.timers.council_send = self:ScheduleTimer(SendCouncil, timeRemaining)
return
end
else -- No cooldown, send and start cooldown
self.timers.council_cooldown = self:ScheduleTimer(OnCouncilCooldown, COUNCIL_COMMS_THROTTLE)
SendCouncil()
end
end
function RCLootCouncilML:StartSession()
self.Log("StartSession")
-- Make sure we haven't started the session too fast
if Council:GetNum() == 0 then
addon:Print(L["Please wait a few seconds until all data has been synchronized."])
return self.Log:d("Data wasn't ready", Council:GetNum())
end
if db.sortItems and not self.running then
self:SortLootTable(self.lootTable)
end
if self.running then -- We're already running a sessions, so any new items needs to get added
-- REVIEW This is not optimal, but will be changed anyway with the planned comms changes for v3.0
--local count = 0
--for k,v in ipairs(self.lootTable) do if not v.isSent then count = count + 1 end end
self:Send("group", "lt_add", self:GetLootTableForTransmit())
else
self:Send("group", "lootTable", self:GetLootTableForTransmit())
end
for _, v in ipairs(self.lootTable) do
v.isSent = true
end
self.running = true
self:AnnounceItems(self.lootTable)
-- Print some help messages for not direct mode.
if not addon.testMode and self.printSessionHelp then
-- Use the first entry in lootTable to determinte mode
if not self.lootTable[1].lootSlot then
addon:ScheduleTimer("Print", 1, L["session_help_not_direct"]) -- Delay a bit, so annouceItems are printed first.
end
if self.lootTable[1].bagged then
addon:ScheduleTimer("Print", 1, L["session_help_from_bag"]) -- Delay a bit, so annouceItems are printed first.
end
end
end
function RCLootCouncilML:AddUserItem(item, username)
if type(tonumber(item)) == "number" or string.find(item, "item:") then -- Ensure we can handle it
self:AddItem(item, false, nil, username) -- The item is neither bagged nor in the loot slot.
self:ShowSessionFrame()
else
addon:Print(format(L["ML_ADD_INVALID_ITEM"], tostring(item)))
end
end
function RCLootCouncilML:SessionFromBags()
if self.running then return addon:Print(L["You're already running a session."]) end
local Items = addon.ItemStorage:GetAllItemsOfType("award_later")
if #Items == 0 then return addon:Print(L["No items to award later registered"]) end
for _, v in ipairs(Items) do
self:AddItem(v.link, v, nil, addon.playerName, nil, v.args.boss)
end
if db.autoStart then
self:StartSession()
else
self:ShowSessionFrame(self.lootTable, true)
end
end
function RCLootCouncilML:ClearOldItemsInBags()
local Items = addon.ItemStorage:GetAllItemsOfType("award_later")
for _,Item in ipairs(Items) do
Item:UpdateTime()
if (Item.args.bop and Item:TimeRemaining() < 0) or -- BoP item, 2 hrs
time() - Item.time_added > 3600 * 6 then -- Non BoP, timeout after 6 hrs
self.Log:d("Removed Item", Item.link, "due to timeout.")
addon.ItemStorage:RemoveItem(Item)
-- REVIEW Notify the user?
end
end
end
function RCLootCouncilML:ClearAllItemsInBags()
addon.ItemStorage:RemoveAllItemsOfType("award_later")
addon:Print(L["The award later list has been cleared."])
end
-- Print all items that should be awarded later
function RCLootCouncilML:PrintItemsInBags()
local Items = addon.ItemStorage:GetAllItemsOfType("award_later")
if #Items == 0 then
return addon:Print(L["The award later list is empty."])
end
addon:Print(L["Following items were registered in the award later list:"])
for i, Item in ipairs(Items) do
Item:UpdateTime()
addon:Print(i..". "..Item.link, format(GUILD_BANK_LOG_TIME, SecondsToTime(time() - Item.time_added, true)) )
-- GUILD_BANK_LOG_TIME == "( %s ago )", although the constant name does not make sense here, this constant expresses we intend to do.
-- SecondsToTime is defined in SharedXML/util.lua
end
end
-- @param ... indexes
-- Remove entries in the award later list with the those index
-- Accept number or strings that can be converted into number as input
function RCLootCouncilML:RemoveItemsInBags(...)
local indexes = {...}
table.sort(indexes, function(a, b)
if tonumber(a) and tonumber(b) then
return tonumber(a) < tonumber(b)
end
end)
local Items = addon.ItemStorage:GetAllItemsOfType("award_later")
local removedEntries = {}
for i=#indexes, 1, -1 do
local index = tonumber(indexes[i])
if index and Items[index] then
addon.ItemStorage:RemoveItem(Items[index])
tinsert(removedEntries, 1, Items[index])
end
end
if #removedEntries == 0 then
addon:Print(L["No entry in the award later list is removed."])
else
addon:Print(L["The following entries are removed from the award later list:"])
for k, v in ipairs(removedEntries) do
addon:Print(k..". "..v.link, "-->", v.args.recipient and addon:GetUnitClassColoredName(v.args.recipient) or L["Unawarded"],
format(GUILD_BANK_LOG_TIME, SecondsToTime(time()-v.time_added)) )
end
end
end
-- Check if there are any BOP item in the player's inventory that is in the award later list and has low trade time remaining.
-- If yes, print the items to remind the user.
local lastCheckItemsInBagsLowTradeTimeRemainingReminder = 0
function RCLootCouncilML:ItemsInBagsLowTradeTimeRemainingReminder()
if GetTime() - lastCheckItemsInBagsLowTradeTimeRemainingReminder < 120 then -- Dont spam
return
end
local entriesToRemind = TempTable:Acquire()
local remindThreshold = 1200 -- 20min
local Items = addon.ItemStorage:GetAllItemsOfType("award_later")
local remainingTime
for k, Item in ipairs(Items) do
remainingTime = Item:TimeRemaining()
if remainingTime > 0 and remainingTime < remindThreshold then
tinsert(entriesToRemind, { index = k, Item = Item, remainingTime = remainingTime})
end
end
if #entriesToRemind > 0 then
addon:Print(format(L["item_in_bags_low_trade_time_remaining_reminder"], "|cffff0000"..SecondsToTime(remindThreshold).."|r"))
for _, v in ipairs(entriesToRemind) do
addon:Print(v.index..". "..v.Item.link, "-->", v.args.recipient and addon:GetUnitClassColoredName(v.args.recipient) or L["Unawarded"],
"(", _G.CLOSES_IN..":", SecondsToTime(v.remainingTime), ")")
end
end
TempTable:Release(entriesToRemind)
lastCheckItemsInBagsLowTradeTimeRemainingReminder = GetTime()
end
function RCLootCouncilML:ConfigTableChanged(value)
-- The db was changed, so check if we should make a new mldb
-- We can do this by checking if the changed value is a key in mldb
if not addon.mldb then return self:UpdateMLdb() end -- mldb isn't made, so just make it
for val in pairs(value) do
if MLDB:IsKey(val) then return self:UpdateMLdb() end
end
end
function RCLootCouncilML:CouncilChanged()
-- The council was changed, so send out the council
self:UpdateGroupCouncil()
self:SendCouncil()
end
function RCLootCouncilML:OnGroupRosterUpdate()
-- Push MLDB if new people has joined.
local newGroupSize = GetNumGroupMembers() or 0
if newGroupSize > self.groupSize then
self.Log:d("Group size changed to "..newGroupSize)
MLDB:Send("group")
self:SendCouncil()
end
self.groupSize = newGroupSize
end
function RCLootCouncilML:UpdateMLdb()
-- The db has changed, so update the mldb and send the changes
self.Log:d("UpdateMLdb")
addon:OnMLDBReceived(self:BuildMLdb())
MLDB:Send("group")
end
function RCLootCouncilML:BuildMLdb()
local MLdb = MLDB:Update()
addon:SendMessage("RCMLBuildMLdb", MLdb)
return MLdb
end
--- Setup RCLootCouncilML for a new ML
---@param newML Player
function RCLootCouncilML:NewML(newML)
self.Log:d("NewML", newML)
if newML == addon.player then -- we are the the ML
self:Send("group", "playerInfoRequest")
self:UpdateMLdb() -- Will build and send mldb
-- Delay council a bit, as GetRaidRosterInfo may not be quite ready yet
self:ScheduleTimer(function()
self:UpdateGroupCouncil()
self:SendCouncil()
end, addon.testMode and 0 or 2)
self:ClearOldItemsInBags()
if #addon.ItemStorage:GetAllItemsOfType("award_later") > 0 then
addon:Print(L["new_ml_bagged_items_reminder"])
end
self:ScheduleTimer("ItemsInBagsLowTradeTimeRemainingReminder", 5, self) -- Delay a bit as it might not be initialized
else
self:Disable() -- We don't want to use this if we're not the ML
end
end
function RCLootCouncilML:Timer(type, ...)
if type == "AddItem" then
self:AddItem(...)
elseif type == "LootSend" then
self:Send("group", "offline_timer")
end
end
function RCLootCouncilML:OnBonusRoll (winner, type, link)
self:TrackAndLogLoot(winner, link, "BONUSROLL", addon.bossName)
end
function RCLootCouncilML:OnTradeComplete(link, recipient, trader)
if db.printCompletedTrades then
addon:Print(format(L["trade_complete_message"], addon:GetClassIconAndColoredName(trader), link, addon:GetClassIconAndColoredName(recipient)))
end
end
function RCLootCouncilML:HandleReceivedTradeable (sender, item)
if not (addon.handleLoot and item and item ~= "") then return self.Log:E("HandleReceivedTradeable", sender, item) end -- Auto fail criterias
if not C_Item.GetItemInfo(item) then
self.Log:d("Tradable item uncached: ", item, sender)
return self:ScheduleTimer("HandleReceivedTradeable", 1, item, sender)
end
sender = addon:UnitName(sender)
self.Log:d("ML:HandleReceivedTradeable", item, sender)
-- For ML loot method, ourselve must be excluded because it should be handled in self:LootOpen()
if not addon:UnitIsUnit(sender, "player") or addon.lootMethod ~= "master" then
local quality = select(3, C_Item.GetItemInfo(item))
local autoAward, mode, winner = self:ShouldAutoAward(item, quality)
if autoAward then
self:AutoAward(nil, item, quality, winner, mode, addon.bossName, sender)
return
end
local boe = addon:IsItemBoE(item)
if (not boe or (db.autolootBoE and boe)) and -- BoE
not self:IsItemIgnored(item) and -- Item mustn't be ignored
(quality and quality >= addon.Utils:GetLootThreshold()) then
if InCombatLockdown() and not db.skipCombatLockdown then
addon:Print(format(L["autoloot_others_item_combat"], addon:GetUnitClassColoredName(sender), item))
tinsert(self.combatQueue, {self.AddUserItem, self, item, sender}) -- Run the function when combat ends
else
self:AddUserItem(item, sender)
end
end
end
end
function RCLootCouncilML:HandleNonTradeable(link, owner, reason)
if not (addon.handleLoot and link and link ~= "") then return end -- Auto fail criterias
local responseID
if reason == "n_t" then
responseID = "PL"
-- We don't want to log it if the non-tradeable is on the ignore/black list
if self:IsItemIgnored(link) then return end
elseif reason == "r_t" then
responseID = "PL_REJECT"
else
return self.Log:W("Non handled reason in ML:HandleNonTradeable()",link,owner,reason)
end
self:TrackAndLogLoot(owner, link, responseID, addon.bossName)
end
--- Subscriber for `Utils.GroupLoot`.OnLootRoll
--- @param link Itemlink
--- @param rollID integer
--- @param rollType RollType
function RCLootCouncilML:OnGroupLootRoll(link, rollID, rollType)
-- Only add items we've needed/greeded
if rollType == 0 or rollType == 3 then return end
-- Since there's no difference between PL and GL once we have the loot, just treat it like PL:
self:HandleReceivedTradeable(addon.player:GetName(), link)
end
function RCLootCouncilML:OnEvent(event, ...)
self.Log:d("ML event", event, ...)
if event == "CHAT_MSG_WHISPER" and addon.isMasterLooter and db.acceptWhispers then
local msg, sender = ...
if msg == "rchelp" then
self:SendWhisperHelp(sender)
elseif self.running then
self:GetItemsFromMessage(msg, sender)
end
elseif event == "PLAYER_REGEN_ENABLED" then
self:ItemsInBagsLowTradeTimeRemainingReminder()
for _, entry in ipairs(self.combatQueue) do
entry[1](select(2, unpack(entry)))
end
wipe(self.combatQueue)
elseif event == "ENCOUNTER_START" then
-- FIXME: People joining after "StartHandleLoot" is sent naturally won't have it,
-- but they still need it for group loot auto pass to work. For now just send it everytime
-- we start an encounter.
if addon.handleLoot then
self:Send("group", "StartHandleLoot")
end
end
end
-- called in addon:OnEvent
function RCLootCouncilML:OnLootSlotCleared(slot, link)
-- REVIEW v2.19.2: Apperantly this is called sometimes without self.lootQueue being initialized - especially in Classic.
-- Not sure the exact cause - maybe due to looting between :GetML() registers player as ML and ML module being initialized.
-- For now silently log an error and stack trace and hopefully find the issue in some SV.
if not self.lootQueue then
return ErrorHandler:ThrowSilentError("ML.lootQueue nil")
end
for i = #self.lootQueue, 1, -1 do -- Check latest loot attempt first
local v = self.lootQueue[i]
if v.slot == slot then -- loot success
self:CancelTimer(v.timer)
tremove(self.lootQueue, i)
if (v.callback) then
v.callback(true, nil, unpack(v.args))
end
break
end
end
end
--- Awards all items in lootTable to the ML for award later
function RCLootCouncilML:DoAwardLater (lootTable)
local awardsDone = 0
for session in ipairs(lootTable) do
self:Award(session, nil, nil, nil, function()
-- Ensure all awards are done before ending the session.
awardsDone = awardsDone + 1
if awardsDone >= #lootTable then
RCLootCouncilML:EndSession()
end
end)
end
end
function RCLootCouncilML:CanWeLootItem(item, quality)
local ret = false
if item and db.autoLoot and (quality and quality >= addon.Utils:GetLootThreshold())
and not self:IsItemIgnored(item) then -- it's something we're allowed to loot
-- Let's check if it's BoE
ret = db.autolootBoE or not addon:IsItemBoE(item) -- Don't bother checking if we know we want to loot it
end
self.Log:d("CanWeLootItem", item, quality, ret)
return ret
end
-- Do we have free space in our bags to hold this item?
function RCLootCouncilML:HaveFreeSpaceForItem(item)
local itemFamily = C_Item.GetItemFamily(item)
-- If the item is a container, then the itemFamily should be 0
local equipSlot = select(4, C_Item.GetItemInfoInstant(item))
if equipSlot == "INVTYPE_BAG" then
itemFamily = 0
end
-- Get the bag's family
for bag = BACKPACK_CONTAINER, NUM_BAG_SLOTS do
local freeSlots, bagFamily = addon.C_Container.GetContainerNumFreeSlots(bag)
if freeSlots and freeSlots > 0 and (bagFamily == 0 or bit.band(itemFamily, bagFamily) > 0) then
return true
end
end
return false
end
-- Return can we give the loot to the winner
-- Note that it's still possible that GiveMasterLoot fails to be given after this check returns true.
-- Then the reason is most likely the winner's inventory is full.
--@return true we can, false and the cause if not
-- causes:
-- "loot_not_open": No loot windowed is open.
-- "loot_gone": No loot on the slot provided or loot on the slot is not the item provided.
-- "locked": The loot slot is locked for us. We are not eligible to loot this slot.
-- "ml_inventory_full": The winner is ourselves and our inventory is full.
-- "quality_below_threshold": The winner is not ourselve and the quality of the item is below loot threshold.
-- "not_in_group": The winner is not ourselve and not in our group.
-- "offline": The winner is offline.
-- "ml_not_in_instance": ML is not in an instance during a RC session.
-- "out_of_instance": Winner is not in the ML's instance.
-- "not_bop": The winner is not ourselves and the item is not a bop that cannot be looted by the winner.
-- "not_ml_candidate": The winner is not ML and not in ml candidate.
function RCLootCouncilML:CanGiveLoot(slot, item, winner)
if not addon.lootOpen then
return false, "loot_not_open"
elseif not addon.lootSlotInfo[slot] or (not addon:ItemIsItem(addon.lootSlotInfo[slot].link, item)) then
return false, "loot_gone"
elseif addon.lootSlotInfo[slot].locked then
return false, "locked" -- Side Note: When the loot method is master, but ML is ineligible to loot (didn't tag boss/did the boss earlier in the week), WoW gives loot as if it is group loot method.
elseif addon:UnitIsUnit(winner, "player") and not self:HaveFreeSpaceForItem(addon.lootSlotInfo[slot].link) then
return false, "ml_inventory_full"
elseif not addon:UnitIsUnit(winner, "player") then
if addon.lootSlotInfo[slot].quality < addon.Utils:GetLootThreshold() then
return false, "quality_below_threshold"
end
-- Actually, the unit who leaves our group can still receive loot, as long as he is in the instance group.
-- After left group, the unit doesn't leave the instance group until leave instance or gets booted out of instance after 60s grace period expires.
-- I just don't want to bother this issue, and it's practical bad to do so,
-- as CHAT_LOOT_MSG, which many ML uses to get the loot confirmation, is very likely to be missing after the loot is given to a person out of group.
-- I want to give the user more precise reason why the item cant be given.
local shortName = Ambiguate(winner, "short"):lower()
if (not UnitInParty(shortName)) and (not UnitInRaid(shortName)) then
return false, "not_in_group"
end
if not UnitIsConnected(shortName) then
return false, "offline"
end
local found = false
for i = 1, _G.MAX_RAID_MEMBERS do
if addon:UnitIsUnit(GetMasterLootCandidate(slot, i), winner) then
found = true
break
end
end
if not IsInInstance() then
return false, "ml_not_in_instance" -- ML leaves the instance during a RC session.
end
if select(4, UnitPosition(Ambiguate(winner, "short"))) ~= select(4, UnitPosition("player")) then
return false, "out_of_instance" -- Winner not in the same instance as ML
end
local bindType = select(14, C_Item.GetItemInfo(item))
if not found then
if bindType ~= Enum.ItemBind.OnAcquire then
return false, "not_bop"
else
return false, "not_ml_candidate"
end
end
end
return true
end
local function OnGiveLootTimeout(entryInQueue)
for k, v in pairs(RCLootCouncilML.lootQueue) do -- remove entry from the loot queue.
if v == entryInQueue then
tremove(RCLootCouncilML.lootQueue, k)
end
end
if entryInQueue.callback then
entryInQueue.callback(false, "timeout", unpack(entryInQueue.args)) -- loot attempt fails
end
end
-- Attempt to give loot to winner.
-- This function does not check loot eligibility. Use CanGiveLoot for that.
-- This function always call callback function, with the maximum delay of LOOT_TIMEOUT,
-- as callback(awarded, cause, ...), if callback is provided.
-- Currently, "cause" is always nil when award success (awarded == true) and "timeout" when awarded failed (awarded == false)
--@param slot the loot slot
--@param winner The name of the candidate who we want to give the item to
--@param callback The callback function that do stuff when this loot attempt success/fail
--@param ... The additional arguments provided to the callback.
--@return nil
function RCLootCouncilML:GiveLoot(slot, winner, callback, ...)
if addon.lootOpen then
local entryInQueue = {slot = slot, callback = callback, args = {...}, }
entryInQueue.timer = self:ScheduleTimer(OnGiveLootTimeout, LOOT_TIMEOUT, entryInQueue)
tinsert(self.lootQueue, entryInQueue)
for i = 1, _G.MAX_RAID_MEMBERS do
if addon:UnitIsUnit(GetMasterLootCandidate(slot, i), winner) then
self.Log("GiveMasterLoot", slot, i)
GiveMasterLoot(slot, i)
break
end
end
end
end
function RCLootCouncilML:UpdateLootSlots()
if not addon.lootOpen then return self.Log:d("ML:UpdateLootSlots() without loot window open!!") end
local updatedLootSlot = {}
for i = 1, GetNumLootItems() do
local item = GetLootSlotLink(i)
for session = 1, #self.lootTable do
-- Just skip if we've already awarded the item or found a fitting lootSlot
if not self.lootTable[session].awarded and not updatedLootSlot[session] then
if addon:ItemIsItem(item, self.lootTable[session].link) then
if i ~= self.lootTable[session].lootSlot then -- It has changed!
self.Log:d("lootSlot @session", session, "Was at:",self.lootTable[session].lootSlot, "is now at:", i)
end
self.lootTable[session].lootSlot = i -- update it
updatedLootSlot[session] = true
break
end
end
end
end
end
function RCLootCouncilML:PrintLootErrorMsg(cause, slot, item, winner)
self.Log:d("ML:PrintLootErrorMsg", cause, slot, item, winner)
if cause == "loot_not_open" then
addon:Print(L["Unable to give out loot without the loot window open."])
elseif cause == "timeout" then
addon:Print(format(L["Timeout when giving 'item' to 'player'"], item, addon:GetUnitClassColoredName(winner)), " - ", _G.ERR_INV_FULL) -- "Inventory is full."
elseif cause == "locked" then
addon:SessionError("No permission to loot the item at slot "..slot)
else
local prefix = format(L["Unable to give 'item' to 'player'"], item, addon:GetUnitClassColoredName(winner)).." - "
if cause == "loot_gone" then
addon:Print(prefix, _G.LOOT_GONE) -- "Item already looted."
elseif cause == "ml_inventory_full" then
addon:Print(prefix, _G.ERR_INV_FULL) -- "Inventory is full."
elseif cause == "quality_below_threshold" then
addon:Print(prefix, L["Item quality is below the loot threshold"])
elseif cause == "not_in_group" then
addon:Print(prefix, L["Player is not in the group"])
elseif cause == "offline" then
addon:Print(prefix, L["Player is offline"])
elseif cause == "ml_not_in_instance" then
addon:Print(prefix, L["You are not in an instance"])
elseif cause == "out_of_instance" then
addon:Print(prefix, L["Player is not in this instance"])
elseif cause == "not_ml_candidate" then
addon:Print(prefix, L["Player is ineligible for this item"])
elseif cause == "not_bop" then
addon:Print(prefix, L["The item can only be looted by you but it is not bind on pick up"])
else
addon:Print(prefix) -- should not happen if programming is correct
end
end
end
-- Status can be one of the following:
-- test_mode, normal, manually_added, indirect,
-- See :Award() for the different scenarios
local function awardSuccess(session, winner, status, callback, responseText, ...)
RCLootCouncilML.Log:d("ML:awardSuccess", session, winner, status, callback, responseText, ...)
addon:SendMessage("RCMLAwardSuccess", session, winner, status, RCLootCouncilML.lootTable[session].link, responseText)
if callback then
callback(true, session, winner, status, ...)
end
return true
end
-- Status can be one of the following:
-- award later success: bagged, manually_bagged,
-- normal error: bagging_awarded_item, loot_not_open, loot_gone, locked, ml_inventory_full, quality_below_threshold
-- , not_in_group, offline, not_ml_candidate, timeout, test_mode, bagging_bagged, ml_not_in_instance, out_of_instance
-- Status when the addon is bugged(should not happen): unlooted_in_bag
-- See :Award() and :CanGiveLoot() for the different scenarios and to get their meanings
local function awardFailed(session, winner, status, callback, responseText, ...)
RCLootCouncilML.Log:d("ML:awardFailed", session, winner, status, callback, responseText, ...)
addon:SendMessage("RCMLAwardFailed", session, winner, status, RCLootCouncilML.lootTable[session].link, responseText)
if callback then
callback(false, session, winner, status, ...)
end
return false
end
-- Run this after awardSuccess so callback can do stuff to announcement.
local function registerAndAnnounceAward(session, winner, response, reason)
local self = RCLootCouncilML
local changeAward = self.lootTable[session].awarded
self.lootTable[session].awarded = winner
if self.lootTable[session].bagged then
addon.ItemStorage:RemoveItem(self.lootTable[session].bagged)
end
self:Send("group", "awarded", session, winner, self.lootTable[session].owner)
self:AnnounceAward(winner, self.lootTable[session].link,
reason and reason.text or response, addon:GetActiveModule("votingframe"):GetCandidateData(session, winner, "roll"), session, changeAward)
if self:HasAllItemsBeenAwarded() then
addon:Print(L["All items have been awarded and the loot session concluded"])
self:ScheduleTimer("EndSession", 1) -- Delay a bit to ensure callback is handled before session ends.
end
return true
end
local function registerAndAnnounceBagged(session)
local self = RCLootCouncilML
local Item = addon.ItemStorage:New(self.lootTable[session].link, "award_later", {
bop = addon:IsItemBoP(self.lootTable[session].link),
boss = self.lootTable[session].boss
}):Store()
if not Item.inBags then -- It wasn't found!
-- We don't care about onFound, as all we need is to record the time_remaining
addon.ItemStorage:WatchForItemInBags(Item, nil, function(Item)
RCLootCouncilML.Log:E(format("Award Later item %s was never found in bags!", Item.link))
end, 5)
end
if self.lootTable[session].lootSlot or self.running then -- Item is looted by ML, announce it.
-- Also announce if the item is awarded later in voting frame.
self:AnnounceAward(L["The loot master"], self.lootTable[session].link, L["Store in bag and award later"], nil, session)
else
addon:Print(format(L["'Item' is added to the award later list."], self.lootTable[session].link))
end
self.lootTable[session].lootSlot = nil -- Now the item is bagged and no longer in the loot window.
self.lootTable[session].bagged = Item
if self.running then -- Award later can be done when actually loot session hasn't been started yet.
self.lootTable[session].baggedInSession = true -- Used in VotingFrame
self.lootTable[session].awarded = true
self:Send("group", "bagged", session, addon.playerName)
if self:HasAllItemsBeenAwarded() then self:ScheduleTimer("EndSession", 1) end
end
return false
end
---@param session integer The session to award.
---@param winner? string Name of player to award to. Nil/false if items should be stored in inventory and awarded later.
---@param response? string The candidate's response.
---@param reason? table Entry in db.awardReasons.
---@param callback? fun(awarded: boolean, session: integer, winner: string?, status: string, ...) Callback function that's called once the award fails or succeeds.
---@vararg any Additional arguments passed to the callback function.
---@returns boolean true if award is success. false if award is failed. nil if we don't know the result yet.
function RCLootCouncilML:Award(session, winner, response, reason, callback, ...) -- Note: MAKE SURE callbacks is always called eventually in any case, or there is bug.
self.Log("Award", session, winner, response, reason)
local args = {...} -- "..."(Three dots) cant be used in an inner function, use unpack(args) instead.
local responseText = reason and reason.text or response
if not self.lootTable or #self.lootTable == 0 then -- Our session probably ended, check the old loot table
if self.oldLootTable and #self.oldLootTable > 0 then
-- Restore it, and assume we want to reaward something
self.lootTable = self.oldLootTable
else
-- We have neither lootTable nor oldLootTable - that shouldn't happen!
self.Log:E("ML:Award - Neither lootTable nor oldLootTable!")
return false
end
end
if self.lootTable[session].lootSlot and self.lootTable[session].bagged then -- For debugging purpose, addon bug if this happens, such values never exist at any time.
awardFailed(session, winner, "unlooted_in_bag", callback, responseText, ...)
addon:SessionError("Session "..session.." has unlooted item in the bag!?")
return false
end
if self.lootTable[session].bagged and not winner then -- We should also check this in voting frame, but this check is needed due to comm delay between ML and voting frame.
awardFailed(session, nil, "bagging_bagged", callback, responseText, ...)
addon:Print(L["Items stored in the loot master's bag for award later cannot be awarded later."])
return false
end
if self.lootTable[session].awarded and not winner then -- We should also check this in voting frame, but this check is needed due to comm delay between ML and voting frame.
awardFailed(session, nil, "bagging_awarded_item", callback, responseText, ...)
addon:Print(L["Awarded item cannot be awarded later."])
return false
end
-- already awarded. Change award
if self.lootTable[session].awarded then
registerAndAnnounceAward(session, winner, response, reason)
if not self.lootTable[session].lootSlot and not self.lootTable[session].bagged then -- "/rc add" or test mode
awardSuccess(session, winner, addon.testMode and "test_mode" or "manually_added", callback, responseText, ...)
elseif self.lootTable[session].bagged then
awardSuccess(session, winner, "indirect", callback, responseText, ...)
else
awardSuccess(session, winner, "normal", callback, responseText, ...)
end
return true
end
-- For the rest, the item is not awarded.
if not self.lootTable[session].lootSlot and not self.lootTable[session].bagged then -- "/rc add" or test mode. Note that "/rc add" does't add the item to ItemStorage unless award later is checked.
if winner then
awardSuccess(session, winner, addon.testMode and "test_mode" or "manually_added", callback, responseText, ...)
registerAndAnnounceAward(session, winner, response, reason)
return true
else
if addon.testMode then
awardFailed(session, nil, "test_mode", callback, responseText, ...)
addon:Print(L["Award later isn't supported when testing."])
return false
else -- Award later optioned is checked in "/rc add", put in ML's bag.
registerAndAnnounceBagged(session)
awardFailed(session, nil, "manually_bagged", callback, responseText, ...)
return false
end
end
end
if self.lootTable[session].bagged then -- indirect mode (the item is in a bag)
-- Add to the list of awarded items in MLs bags,
registerAndAnnounceAward(session, winner, response, reason)
awardSuccess(session, winner, "indirect", callback, responseText, ...)
return true
end
-- The rest is direct mode (item is in WoW loot window)
-- v2.4.4+: Check if the item is still in the expected slot
if addon.lootOpen and not addon:ItemIsItem(self.lootTable[session].link, GetLootSlotLink(self.lootTable[session].lootSlot)) then
self.Log:d("LootSlot has changed before award!", session)
-- And update them if not
self:UpdateLootSlots()
end
local canGiveLoot, cause = self:CanGiveLoot(self.lootTable[session].lootSlot, self.lootTable[session].link, winner or addon.playerName) -- if winner is nil, give the loot to the ML for award later.
if not canGiveLoot then
if cause == "quality_below_threshold" or cause == "not_bop" then
self:PrintLootErrorMsg(cause, self.lootTable[session].lootSlot, self.lootTable[session].link, winner or addon.playerName)
addon:Print(L["Gave the item to you for distribution."])
return self:Award(session, nil, response, reason, callback, ...) -- cant give the item to other people, award later.
else
awardFailed(session, winner, cause, callback, responseText, ...)
self:PrintLootErrorMsg(cause, self.lootTable[session].lootSlot, self.lootTable[session].link, winner or addon.playerName)
return false
end
else
if winner then -- award the item now
-- Attempt to give loot
self:GiveLoot(self.lootTable[session].lootSlot, winner, function(awarded, cause)
if awarded then
registerAndAnnounceAward(session, winner, response, reason)
awardSuccess(session, winner, "normal", callback, responseText, unpack(args))
return true
else
awardFailed(session, winner, cause, callback, responseText, unpack(args))
self:PrintLootErrorMsg(cause, self.lootTable[session].lootSlot, self.lootTable[session].link, winner)
return false
end
end)
else -- Store in our bags and award later
self:GiveLoot(self.lootTable[session].lootSlot, addon.playerName, function(awarded, cause)
if awarded then
registerAndAnnounceBagged(session)
awardFailed(session, nil, "bagged", callback, responseText, unpack(args)) -- Item hasn't been awarded
else
awardFailed(session, nil, cause, callback, responseText, unpack(args))
self:PrintLootErrorMsg(cause, self.lootTable[session].lootSlot, self.lootTable[session].link, addon.playerName)
end
return false
end)
end
end
end
--- Substitution strings for AnnounceItems