forked from skennedysocal/WoW_Hardcore
-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathDungeons.lua
More file actions
1839 lines (1549 loc) · 61.5 KB
/
Dungeons.lua
File metadata and controls
1839 lines (1549 loc) · 61.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
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
-- Dungeons.lua
-- Dungeon tracking functionality for WOW Classic Hardcore Addon
-- Written by Frank de Jong
-- Definitions
local DT_WARN_INTERVAL = 10 -- Warn every 10 seconds about repeated run (while in dungeon)
local DT_INSIDE_MAX_TIME = 60 -- Maximum time inside a dungeon without it being logged
local DT_INSIDE_MAX_TIME_NO_KILLS = 450 -- Maximum time inside a dungeon without it being logged if there are no kills
local DT_OUTSIDE_MAX_REAL_TIME = 1800 -- If seen outside, how many seconds since last seen inside before finalization (1800 = 30m)
local DT_OUTSIDE_MAX_RUN_TIME = 21600 -- If seen outside, how many seconds since start of run before finalization (21600 = 6 hrs)
local DT_TIME_STEP = 1 -- Dungeon code called every 1 second
local DT_GROUP_PULSE = 30 -- Send group pulse every 30 seconds
local DT_VERSION = 4 -- Increasing this will trigger a full rebuild of the dungeon tracker info
-- Some local variables defined in Hardcore.lua -- Make sure these are the same as in Hardcore.lua!!
local CTL = _G.ChatThrottleLib
local COMM_NAME = "HardcoreAddon" -- Overwritten in DungeonTrackerInitiate()
local COMM_COMMAND_DELIM = "$" -- Overwritten in DungeonTrackerInitiate()
local COMM_FIELD_DELIM = "|" -- Overwritten in DungeonTrackerInitiate()
local DT_PULSE_COMMAND = "DTPULSE" -- Overwritten in DungeonTrackerInitiate()
local combat_log_frame = nil
local dt_checked_for_missing_runs = false -- Did we check for missing runs in this session already?
local dt_party_member_addon_version = {}
local dt_party_member_verif_status = {}
local dt_player_level = 0
local dt_db_id_to_name = nil
local dt_db_max_levels = nil
local dt_db_name_to_index = nil -- Gives index in the database of the dungeon with given name
-- DungeonTrackerInitializeHashes()
--
-- Compiles the lookup tables dt_db_* for faster lookup of specific dungeons
local function DungeonTrackerInitializeHashes()
-- Hash from MapID to dungeon name
if dt_db_id_to_name == nil then
dt_db_id_to_name = {}
for i, v in ipairs(dt_db) do
dt_db_id_to_name[v[1]] = v[3]
end
end
-- Hash from name to max level
if dt_db_max_levels == nil then
dt_db_max_levels = {}
for i, v in ipairs(dt_db) do
if v[7] ~= nil then
dt_db_max_levels[v[3]] = v[7]
else
dt_db_max_levels[v[3]] = { 1000, 1000 }
end
end
end
-- Hash from name to index in the database
if dt_db_name_to_index == nil then
dt_db_name_to_index = {}
for i, v in ipairs(dt_db) do
dt_db_name_to_index[v[3]] = i
end
end
end
-- DungeonTrackerGetDungeonName( id )
--
-- Needed to get around regionalised names. We want everything in English, yo!
local function DungeonTrackerGetDungeonName(id)
if dt_db_id_to_name == nil or dt_db_id_to_name[id] == nil then
return "Unknown"
end
return dt_db_id_to_name[id]
end
-- DungeonTrackerGetDungeonMaxLevel
--
-- Returns the max level for a dungeon from the database above, or 1000 if not known
local function DungeonTrackerGetDungeonMaxLevel(name)
local max_level = 1000 -- Default: if we can't find it, or game version not set: it doesn't have a max level
if dt_db_max_levels ~= nil and dt_db_max_levels[name] ~= nil then
if Hardcore_Character.game_version ~= nil then
if Hardcore_Character.game_version == "Era" or Hardcore_Character.game_version == "SoM" then
max_level = dt_db_max_levels[name][1]
elseif Hardcore_Character.game_version == "WotLK" or Hardcore_Character.game_version == "Cata" then
max_level = dt_db_max_levels[name][2]
end
end
end
return max_level
end
-- DungeonTrackerGetDungeonMaxLevelAtRunTime
--
-- Returns the max level for a dungeon from the database at the specified run time
-- This is used to see if previous runs may have been under another version of the
-- game where the max levels were different
local function DungeonTrackerGetDungeonMaxLevelAtRunTime(run)
-- If we are in cata now, but the run started before pre-patch day, we use the WotLK max level (if it was differen than Cata)
if _G["HardcoreBuildLabel"] == "Cata" and run.start ~= nil and _dt_db_wotlk_max_levels[ run.name ] ~= nil then
if run.start < 1714482000 then -- Wed 30 April 2024, 15:00 PDT
return _dt_db_wotlk_max_levels[ run.name ]
end
end
-- Default to the current version's max level if not Cata
return DungeonTrackerGetDungeonMaxLevel(run.name)
end
-- DungeonTrackerGetAllDungeonMaxLevels()
--
-- Returns a table of dungeons and associated max levels
-- (only dungeons, not raids, not battle grounds)
-- Mostly for use in the Rules tab (so not local), called from Mainmenu.lua
function DungeonTrackerGetAllDungeonMaxLevels()
local the_table = {}
for i, v in pairs(dt_db) do
if v[4] == "D" then
local max_era_level = v[7][1]
if max_era_level == 1000 then
table.insert(the_table, { v[3], "--", v[7][2] })
else
table.insert(the_table, { v[3], max_era_level, v[7][2] })
end
end
end
return the_table
end
-- DungeonTrackerHasRun( name )
--
-- Returns true if a dungeon with the given name was found in any of .runs[], .pending[] or .current,
-- or false otherwise
local function DungeonTrackerHasRun( name )
if Hardcore_Character.dt.runs ~= nil then
for i, v in ipairs(Hardcore_Character.dt.runs) do
if v.name == name then
return true
end
end
end
if Hardcore_Character.dt.pending ~= nil then
for i, v in ipairs(Hardcore_Character.dt.pending) do
if v.name == name then
return true
end
end
end
if Hardcore_Character.dt.current ~= nil then
if Hardcore_Character.dt.current.name == name then
return true
end
end
return false
end
-- DungeonTrackerFindMissingRunsFromQuests()
--
-- Finds any dungeons that have the flagging quests, but do not have an associated run
-- (for whatever reason, such as weird update problems).
local function DungeonTrackerFindMissingRunsFromQuests()
local game_version_index = 1
local game_version_max_level = 60
-- Double check inputs
if Hardcore_Character.dt == nil or Hardcore_Character.dt.runs == nil then
return
end
-- For Era/SoM, we only look at the quests for dungeons with a max level of 60
if Hardcore_Character.game_version == "Era" or Hardcore_Character.game_version == "SoM" then
game_version_index = 1
game_version_max_level = 60
elseif Hardcore_Character.game_version == "WotLK" or Hardcore_Character.game_version == "Cata" then
game_version_index = 2
game_version_max_level = 80
else
return
end
-- Go through the list and log a run for each dungeon for which one or more quests are flagged as completed
for i, v in pairs(dt_db) do
local dungeon_done = false
local quests = v[8]
local name = v[3]
local map_id = v[1]
local max_levels = v[7]
if (quests ~= nil) and (max_levels ~= nil) and (max_levels[game_version_index] <= game_version_max_level) and (DungeonTrackerHasRun( name ) == false) then
local j, quest_num
for j = 1, #quests do
if C_QuestLog.IsQuestFlaggedCompleted(quests[j]) then
Hardcore:Debug("Found legacy quest " .. quests[j])
dungeon_done = true
quest_num = quests[j]
break
end
end
if dungeon_done == true then
DUNGEON_RUN = {}
DUNGEON_RUN.name = name
DUNGEON_RUN.id = map_id
DUNGEON_RUN.date = "(legacy)"
DUNGEON_RUN.time_inside = 0
DUNGEON_RUN.level = 0
DUNGEON_RUN.quest_id = quest_num
Hardcore:Debug("Logging missing run in " .. DUNGEON_RUN.name)
table.insert(Hardcore_Character.dt.runs, DUNGEON_RUN)
end
end
end
end
local function DungeonTrackerIsRepeatedRun(run1, run2)
-- If one of the runs is for an unknown SM wing, we don't count this as repeated
if run1.name == "Scarlet Monastery" or run2.name == "Scarlet Monastery" then
return false
end
-- Most common case is where everything is in English; then the names should be the same
if run1.name == run2.name then
return true
end
-- Handle exceptional case for Scarlet Monastery -- there, the instanceMapID will be the same for different wings,
-- but there is no repeated run if you do them both. The "true" must have come from the run name comparison above.
if run1.id ~= nil and run1.id == 189 then
return false
end
-- Handle more exotic cases where some of the names of the logged runs are in another language (backward compatibility)
-- or there was somehow an update in the dungeon database which caused a small change in the name
if run1.id ~= nil and run2.id ~= nil and run1.id == run2.id then
return true
end
-- Player-friendly: we can't figure it out, so we assume it's good
return false
end
-- DungeonTrackerAnyRunNamesRepeated()
--
-- Efficiency function for checking all logged runs to each other.
-- For N runs, N(N-1)/2 checks are necessary if you want to compare all to all.
-- This function simply checks if any of the names occurs more than once; this is Order(N).
local function DungeonTrackerAnyRunNamesRepeated()
if Hardcore_Character.dt.runs == nil then
return false
end
local names_table = {}
for i, v in ipairs(Hardcore_Character.dt.runs) do
if v.name ~= nil and v.name ~= "" then
if names_table[ v.name ] ~= nil then
return true
end
names_table[ v.name ] = 1
end
end
return false
end
-- DungeonTrackerUpdateInfractions()
--
-- Updates the dt.overleveled_runs and dt.repeated_runs variables
-- from the list of finalized runs. This can be called after a Mod command to
-- recalculate the infraction statistics
local function DungeonTrackerUpdateInfractions()
local repeated = 0
local over_leveled = 0
for i = 1, #Hardcore_Character.dt.runs do
-- Check overleveled run
if Hardcore_Character.dt.runs[i].level > DungeonTrackerGetDungeonMaxLevelAtRunTime(Hardcore_Character.dt.runs[i]) then
over_leveled = over_leveled + 1
end
-- Check if the run is repeated further down in the array (this prevents counting runs twice when i ends up at j)
for j = i + 1, #Hardcore_Character.dt.runs do
if DungeonTrackerIsRepeatedRun(Hardcore_Character.dt.runs[i], Hardcore_Character.dt.runs[j]) then
repeated = repeated + 1
end
end
end
Hardcore_Character.dt.overleveled_runs = over_leveled
Hardcore_Character.dt.repeated_runs = repeated
end
local function DungeonTrackerWarnInfraction()
local message
local chat_color = "\124cffFF0000"
-- We only warn if there is still chance to get out in time
local time_left = DT_INSIDE_MAX_TIME_NO_KILLS - Hardcore_Character.dt.current.time_inside
if time_left <= 0 then
return
end
-- Don't warn too frequently
if (Hardcore_Character.dt.current.last_warn ~= nil) and (Hardcore_Character.dt.current.time_inside - Hardcore_Character.dt.current.last_warn < DT_WARN_INTERVAL) then
return
end
-- Don't warn in the first few seconds of an unidentified SM wing. The time_left will be shorter after
-- a reconnect to an existing wing run, so then that first warning of 60s would be confusing
if (Hardcore_Character.dt.current.name == "Scarlet Monastery") and (time_left > 50) then
return
end
-- Don't warn at max level (they can do whatever dungeon then) or when the user turned warnings off
-- /run Hardcore_Character.dt.warn_infractions=false
if Hardcore_Character.dt.warn_infractions == false then
return
end
-- Get max level to know if we should even warn
if Hardcore_Character.game_version ~= nil then
local max_level
if Hardcore_Character.game_version == "Era" or Hardcore_Character.game_version == "SoM" then
max_level = 60
elseif Hardcore_Character.game_version == "WotLK" then
max_level = 80
else -- Cataclysm or anything else
max_level = 85
end
if UnitLevel("player") >= max_level then
Hardcore_Character.dt.warn_infractions = false
return
end
end
-- See if the player's level is allowed in this dungeon
local max_level = DungeonTrackerGetDungeonMaxLevel(Hardcore_Character.dt.current.name)
if Hardcore_Character.dt.current.level > max_level then
Hardcore_Character.dt.current.last_warn = Hardcore_Character.dt.current.time_inside
message = "You are overleveled for " .. Hardcore_Character.dt.current.name .. ". Leave dungeon now!"
Hardcore:Print(chat_color .. message)
Hardcore:ShowRedAlertFrame( message )
end
-- See if this dungeon was already in the list of completed runs, and warn every so many seconds if that is so
for i, v in ipairs(Hardcore_Character.dt.runs) do
if DungeonTrackerIsRepeatedRun(v, Hardcore_Character.dt.current) then
Hardcore_Character.dt.current.last_warn = Hardcore_Character.dt.current.time_inside
local instance_info1 = ""
local instance_info2 = ""
if v.iid ~= nil and Hardcore_Character.dt.current.iid ~= nil and v.iid ~= Hardcore_Character.dt.current.iid then
instance_info1 = " (ID:" .. Hardcore_Character.dt.current.iid .. ")"
instance_info2 = " (ID:" .. v.iid .. ")"
end
message = "You entered " .. v.name .. instance_info1 .. " already on " .. v.date .. instance_info2
.. ". Leave dungeon now!"
Hardcore:Print( chat_color .. message)
Hardcore:ShowRedAlertFrame( message )
break -- No need to warn about 3rd and higher entries
end
end
-- The following code probably can't ever be called, since pending runs with different instance IDs are automatically
-- logged upon entry. But let's keep it here for now. Better warned twice, than never.
-- See if this dungeon was already in the list of pending runs (but with a different instanceID), and warn every so many seconds if that is so
for i, v in ipairs(Hardcore_Character.dt.pending) do
-- We never warn about pending runs without an instanceID, they may or may not be the same as the current
-- (However, such pending runs should not exist, as they are deleted immediately when you exit the dungeon)
-- It is not possible for the IIDs to be the same at this point, as they would have been merged already
if v.iid ~= nil and Hardcore_Character.dt.current.iid ~= nil then
if DungeonTrackerIsRepeatedRun(v, Hardcore_Character.dt.current) then
Hardcore_Character.dt.current.last_warn = Hardcore_Character.dt.current.time_inside
message = "Your idle run of " .. v.name .. " of date ".. v.date .. " has another instance ID"
.. ". Leave dungeon now!"
Hardcore:Print( chat_color .. message )
Hardcore:ShowRedAlertFrame( message )
break -- No need to warn about 3rd and higher entries
end
end
end
end
local function DungeonTrackerLogRun(run)
-- We don't log this run if the inside time is too small
if run.time_inside < DT_INSIDE_MAX_TIME then
Hardcore:Debug("Not logging short run in " .. run.name)
return
end
-- We don't log this run if no instance ID was found (indicating that no mobs were attacked)
if run.iid == nil then
Hardcore:Debug("Not logging run without instanceID in " .. run.name)
return
end
-- We don't log this run if it's relatively short, there is an instance ID (checked above), but no kills
if run.num_kills ~= nil and run.num_kills == 0 and run.time_inside < (DT_INSIDE_MAX_TIME_NO_KILLS) then
Hardcore:Debug("Not logging short run without kills in " .. run.name)
return
end
-- Warn if this is a repeated run and log
for i, v in ipairs(Hardcore_Character.dt.runs) do
if DungeonTrackerIsRepeatedRun(v, run) then
local instance_info = ""
if v.iid ~= nil then
instance_info = " (ID:" .. v.iid .. ")"
end
if Hardcore_Character.dt.warn_infractions == true then
Hardcore:Print(
"\124cffFF0000You entered "
.. run.name .. " (ID:" .. run.iid .. ")"
.. " already on "
.. v.date .. instance_info
.. " -- logging repeated run"
)
end
break
end
end
-- Warn if this is an overleveled run and log
local max_level = DungeonTrackerGetDungeonMaxLevel(run.name)
if run.level > max_level then
if Hardcore_Character.dt.warn_infractions == true then
Hardcore:Print("\124cffFF0000You were overleveled for " .. run.name .. " -- logging overleveled run")
end
end
-- Now actually log the run
Hardcore:Debug("Logging run in " .. run.name)
table.insert(Hardcore_Character.dt.runs, run)
-- Update infraction statistics (involves a re-count)
DungeonTrackerUpdateInfractions()
end
-- DungeonTrackerUpdateEntryCount()
--
-- Keeps count of how often each dungeon has been entered
-- (= 1 + number of reconnects)
local function DungeonTrackerUpdateEntryCount()
local name = Hardcore_Character.dt.current.name
if name == nil then
return
end
-- Create the count if we don't have it already
if Hardcore_Character.dt.current.num_entries == nil then
Hardcore_Character.dt.current.num_entries = 0
end
-- Update or create the entry for this dungeon
Hardcore_Character.dt.current.num_entries = Hardcore_Character.dt.current.num_entries + 1
end
-- DungeonTrackerIdentifyScarletMonasteryWing( map_id, mob_type_id )
--
-- Finds the SM wing in which a certain mob_type_id is found. Only works for unique mob_ids,
-- so not for mobs that appear in more than one wing.
local function DungeonTrackerIdentifyScarletMonasteryWing( map_id, mob_type_id )
local SM = "Scarlet Monastery"
-- If this is SM (=189), and we don't know the wing yet, we try to find it
if map_id == 189 and Hardcore_Character.dt.current.name == SM then
local wing_spawns = {
{4293, "Scarlet Scryer", "GY"},
{4306, "Scarlet Torturer", "GY"},
{4287, "Scarlet Gallant", "Lib"},
{4296, "Scarlet Adept", "Lib"},
{4286, "Scarlet Soldier", "Arm"},
{4297, "Scarlet Conjuror", "Arm"},
{4298, "Scarlet Defender", "Cath"},
-- One more round of deeper-in mobs
{6427, "Haunting Phantasm", "GY"},
{4288, "Scarlet Beastmaster", "Lib"},
{4291, "Scarlet Diviner", "Lib"},
{4289, "Scarlet Evoker", "Arm"},
{4294, "Scarlet Sorceror", "Cath"},
-- Bosses as a last resort
{3983, "Interrogator Vishas", "GY"},
{6490, "Azshir the Sleepless", "GY"},
{6488, "Fallen Champion", "GY"},
{6489, "Ironspine", "GY"},
{3974, "Houndmaster Loksey", "Lib"},
{6487, "Arcanist Doan", "Lib"},
{3975, "Herod", "Arm"},
{3976, "Scarlet Commander Mograine", "Cath"},
{3977, "High Inquisitor Whitemane", "Cath"},
{4542, "High Inquisitor Fairbanks", "Cath"},
}
-- See if any of the listed mobs is recognised
for i, v in ipairs( wing_spawns ) do
if mob_type_id == v[1] then
Hardcore_Character.dt.current.name = SM .. " (" .. v[3] .. ")"
Hardcore:Debug( "Identified SM wing " .. v[3] .. " from " .. v[2] )
return
end
end
end
-- If not SM, or wing already known, or wing not found, we do nothing
end
-- DungeonTrackerCheckChanged(name)
--
-- Handles changes of dungeon (to do "emergency" logging or current run)
-- Also adapts the SM dungeon name to include the wing, if we know it
local function DungeonTrackerCheckChanged(name)
-- If there is no current, there is no change
if not next(Hardcore_Character.dt.current) then
return name
end
local SM = "Scarlet Monastery"
-- If this is Scarlet Monastery (any wing), we need to check if the wing changed
if name == SM then
-- If we don't know which wing we are in, try to identify any of the dungeon wing's mobs
if Hardcore_Character.dt.current.name ~= SM then
-- We already know our wing -- just copy over what we already had
name = Hardcore_Character.dt.current.name
end
-- At this point, either dt.current.name is "SM", or it is "SM (Wing)".
-- If it's "SM", the name can only be "SM", too. This happens if no door spawn was found yet.
-- If it's "SM (Wing)", then "name" has a wing too, which is either the same or different
end
-- Now check if the name changed (whether it's SM or RFC or whatever)
-- This should normally not happen, as once we're outside, the current dungeon is queued
-- But it could happen if people disable the addon inside a dungeon, and re-enable it in another
if Hardcore_Character.dt.current.name ~= name then
-- Change to the new dungeon, but we store only if we spent enough time
Hardcore:Print("Left dungeon " .. Hardcore_Character.dt.current.name .. " for dungeon " .. name)
DungeonTrackerLogRun(Hardcore_Character.dt.current)
Hardcore_Character.dt.current = {}
end
return name
end
-- DungeonTrackerAddToParty( run, name )
--
-- Adds the name to the party, if it's not already there
local function DungeonTrackerAddToParty( run, name )
-- Easy if there is no party yet
if run.party == nil or run.party == "" then
run.party = name
return
end
-- If there is at least one name already, let's make sure the new name is not there already
-- Can't use string.find, because that will bork if you have "Leet" and "Leethc"
local found = false
local names = { string.split(",", run.party ) }
for i, v in ipairs( names ) do
if v == name then
return -- Already there, done!
end
end
run.party = run.party .. "," .. name
end
-- DungeonTrackerReceivePulse( data, sender )
--
-- Receives a group pulse, storing the time in the message and the sender in the associated pending run
-- Not a local function, called from Hardcore.lua
function DungeonTrackerReceivePulse(data, sender)
local short_name
local version
local ping_time
local dungeon_name
local dungeon_id
local iid
local status
short_name, version, ping_time, dungeon_name, dungeon_id, iid, status = string.split(COMM_FIELD_DELIM, data)
-- Handle malformed pulse that breaks the script
if short_name == nil or version == nil or ping_time == nil or dungeon_name == nil or dungeon_id == nil then
return
end
dungeon_id = tonumber(dungeon_id)
-- Old version of the pulse does not have instance ID, so set it to 0
if iid == nil then
iid = 0
else
iid = tonumber(iid)
end
ping_time = tonumber(ping_time)
-- Versions <= 0.11.22 do not have status
if status == nil then
status = "?"
end
Hardcore:Debug(
"Received dungeon group pulse from "
.. sender
.. ", data = "
.. short_name
.. ", "
.. version
.. ", "
.. ping_time
.. ", "
.. dungeon_name
.. ", "
.. iid
.. ", "
.. status
)
-- Save the addon version and verification status for this player
dt_party_member_addon_version[ short_name ] = version
dt_party_member_verif_status[ short_name ] = status
-- Check for errors, dt might not be set right now (if it just got reset for some weird reason)
if (Hardcore_Character.dt == nil) or (not next(Hardcore_Character.dt)) or (not next(Hardcore_Character.dt.pending)) then
return
end
-- Update the latest ping time in the idle runs only (no need to do it in current run)
for i, v in pairs(Hardcore_Character.dt.pending) do
-- We only update the pulse time if the instanceIDs from pending and party member aren't known, or when they are the same
if v.iid == nil or iid == 0 or v.iid == iid then
local run_name_SM
-- If we receive a pulse from "Scarlet Monastery" (with or without wing), then we store that
-- pulse in all idle SM runs because they all share a single instance ID that is still alive
run_name_SM = string.sub(dungeon_name, 1, 17)
-- If this is the run from which the ping originated, and the ping time is later than we already have, store it
if v.name == dungeon_name or run_name_SM == "Scarlet Monastery" then
if ping_time > v.last_seen then
v.last_seen = ping_time
end
-- Add the ping sender to the party members, if not already there
DungeonTrackerAddToParty(v, short_name)
end
end
end
end
-- DungeonTrackerSendPulse( now )
--
-- Sends a group pulse, if the time out is expired
local function DungeonTrackerSendPulse(now)
local my_verif_status
-- Don't send too many pulses, one every 30 seconds is enough
if (Hardcore_Character.dt.sent_pulse ~= nil) and (now - Hardcore_Character.dt.sent_pulse < DT_GROUP_PULSE) then
return
end
Hardcore_Character.dt.sent_pulse = now
-- Generate PASS/FAIL information
my_verif_status = Hardcore:GetCleanVerificationStatus()
-- Send my own info to the party (=name + server time + dungeon)
if CTL then
local name = UnitName("player")
local iid = 0
if Hardcore_Character.dt.current.iid ~= nil then
iid = Hardcore_Character.dt.current.iid
end
local data = name
.. COMM_FIELD_DELIM
.. GetAddOnMetadata("Hardcore", "Version")
.. COMM_FIELD_DELIM
.. now
.. COMM_FIELD_DELIM
.. Hardcore_Character.dt.current.name
.. COMM_FIELD_DELIM
.. Hardcore_Character.dt.current.id
.. COMM_FIELD_DELIM
.. iid
.. COMM_FIELD_DELIM
.. my_verif_status
local comm_msg = DT_PULSE_COMMAND .. COMM_COMMAND_DELIM .. data
CTL:SendAddonMessage("NORMAL", COMM_NAME, comm_msg, "PARTY")
-- Hardcore:Debug("Sending dungeon group pulse: " .. string.gsub( comm_msg, COMM_FIELD_DELIM, "/" ))
-- Make sure we get our own ping if we are soloing the dungeon -- this can keep other SM wings alive
if Hardcore_Character.dt.current.party == UnitName("player") then
DungeonTrackerReceivePulse(data, Hardcore_Character.dt.current.party .. "-SelfPing")
end
-- For debug purposes, set this to true to simulate a send from group members
if false then
DungeonTrackerReceivePulse(data, UnitName("player") .. "-SelfPing")
DungeonTrackerReceivePulse("John|0.11.13|1234324|Ragefire Chasm|189", "John-TestServer")
DungeonTrackerReceivePulse("Jack|0.11.16|1244334|Ragefire Chasm|189|1113", "Jack-TestServer")
DungeonTrackerReceivePulse("Peter|0.11.23|1244334|Scarlet Monastery|189|1111|FAIL", "Peter-TestServer")
DungeonTrackerReceivePulse("Jane|0.11.23|1234324|Ragefire Chasm|189|1111|PASS", "Jane-TestServer")
Hardcore_Character.dt.current.party = UnitName("player") .. ",John,Jack,Peter,Jane"
end
end
end
-- DungeonTrackerDatabaseHasBossInfo( name )
--
-- Queries the dungeon database to see if the boss info is set for this dungeon,
-- basically to cover the period in which we gather the boss info
-- returning true or false
local function DungeonTrackerDatabaseHasBossInfo( name )
if dt_db_name_to_index ~= nil and dt_db_name_to_index[ name ] ~= nil then
local index = dt_db_name_to_index[ name ]
local record = dt_db[ index ]
if record[9] ~= nil then
return true
end
end
-- Error or not found
return false
end
-- DungeonTrackerIsBoss( name, mob_id )
--
-- Queries the dungeon database to see if the mob_id was a boss,
-- returning true or false
local function DungeonTrackerIsBoss( name, mob_id )
if dt_db_name_to_index ~= nil and dt_db_name_to_index[ name ] ~= nil then
local index = dt_db_name_to_index[ name ]
local record = dt_db[ index ]
if record[9] ~= nil then
local boss_list = record[9]
for i, v in ipairs( boss_list ) do
if v[2] ~= nil and v[2] == mob_id then
return true
end
end
end
end
-- Error or not found
return false
end
-- DungeonTrackerLogKill( dst_guid, dst_name )
--
-- Logs the killing of specific units
local function DungeonTrackerLogKill( mob_type_id )
-- Check if it's a boss
if DungeonTrackerIsBoss( Hardcore_Character.dt.current.name, mob_type_id ) then
-- Add it to the list of bosses we've killed
if Hardcore_Character.dt.current.bosses == nil then
Hardcore_Character.dt.current.bosses = {}
end
if Hardcore_Character.dt.current.bosses[ mob_type_id ] == nil then
Hardcore_Character.dt.current.bosses[ mob_type_id ] = GetServerTime()
else
-- This should not happen, so we log it here
Hardcore:Debug( "Warning -- repeated boss kill ignored" )
Hardcore_Character.dt.current.repeated_boss_kill = true
end
else
-- Add it to the list of NPCs we've killed
if Hardcore_Character.dt.current.kills == nil then
Hardcore_Character.dt.current.kills = {}
end
if Hardcore_Character.dt.current.kills[ mob_type_id ] == nil then
Hardcore_Character.dt.current.kills[ mob_type_id ] = 1
else
Hardcore_Character.dt.current.kills[ mob_type_id ] = Hardcore_Character.dt.current.kills[ mob_type_id ] + 1
end
end
-- Add it to the sum total of NPCs we've killed
if Hardcore_Character.dt.current.num_kills == nil then
Hardcore_Character.dt.current.num_kills = 0
end
Hardcore_Character.dt.current.num_kills = Hardcore_Character.dt.current.num_kills + 1
end
-- DungeonTrackerStoreInstanceID
--
-- Stores the instance ID, but only switches to it from another IID when there is more
-- evidence for that new instance ID than for the old. This is to work around a bug where
-- someone zones out of an instance, but gets a combat log message from the surrounding
-- zone in the <1s before the combatlog callback is unregistered.
local function DungeonTrackerStoreInstanceID( instance_id, event )
local instance_id_changed = false
-- Count the number of times we saw this instance ID
if Hardcore_Character.dt.current.iid_count == nil then
Hardcore_Character.dt.current.iid_count = {}
end
if Hardcore_Character.dt.current.iid_count[instance_id] == nil then
Hardcore_Character.dt.current.iid_count[instance_id] = 0
end
Hardcore_Character.dt.current.iid_count[instance_id] = Hardcore_Character.dt.current.iid_count[instance_id] + 1
-- If there is no instance ID yet, we store it
if Hardcore_Character.dt.current.iid == nil then
Hardcore_Character.dt.current.iid = instance_id
instance_id_changed = true
elseif Hardcore_Character.dt.current.iid ~= instance_id then
-- We already have a different instance ID
-- Catch exceptional case where an addon update was done during a dungeon run, and the
-- .iid_count is not set for the current IID. We then start at 1
if Hardcore_Character.dt.current.iid_count[Hardcore_Character.dt.current.iid] == nil then
Hardcore_Character.dt.current.iid_count[Hardcore_Character.dt.current.iid] = 1
end
-- Only swith to the new one if we saw it more often than the old one
if Hardcore_Character.dt.current.iid_count[instance_id] > Hardcore_Character.dt.current.iid_count[Hardcore_Character.dt.current.iid] then
Hardcore_Character.dt.current.iid = instance_id
instance_id_changed = true
end
end
-- Show a debug message if the stored IID changed
if instance_id_changed == true then
Hardcore:Debug( "Found instanceID " .. instance_id .. " from " .. event )
end
end
-- DungeonTrackerPlayerTargetChangedEventHandler
--
-- Handler for PLAYER_TARGET_CHANGED event, fired when player's target changes
-- This is used inside dungeons to get the instance ID without actually attacking
-- anything
local function DungeonTrackerPlayerTargetChangedEventHandler( self, event )
-- Make sure we only respond to the combat_log event
if event ~= "PLAYER_TARGET_CHANGED" then
return
end
-- Bail out right away if we don't have an active run (shouldn't happen, but could)
if not next( Hardcore_Character.dt.current ) then
return
end
-- Get our data once, so it can't change halfway this function, either.
local target_guid = UnitGUID("target")
local target_name = UnitName("target")
-- Bail out if the target was unset
if target_guid == nil or target_name == nil then
return
end
-- Hardcore:Debug("Changed to " .. target_name .. ", GUID=" .. target_guid )
-- Split the GUID
local target_type, _, server, map_id, instance_id, target_type_id = string.split("-", target_guid)
if target_type ~= "Creature" then
return
end
map_id = tonumber( map_id )
instance_id = tonumber( instance_id )
target_type_id = tonumber( target_type_id )
-- Do some checks, to eliminate unexpected results
if map_id ~= Hardcore_Character.dt.current.id then
Hardcore:Debug( "Warning: Got a target change to an NPC " .. target_name .. " (" .. target_type_id .. ") in wrong dungeon map " .. map_id .. " -- ignoring" )
return
end
-- Store the instanceID (the dynamic one)
-- To be thread-safe and fast, the reconnecting happens in the main timer routine
DungeonTrackerStoreInstanceID( instance_id, event )
-- Pass this mob to the SM wing identifier. This will update dt.current.name if possible.
DungeonTrackerIdentifyScarletMonasteryWing( map_id, target_type_id )
end
-- DungeonTrackerCombatLogEventHandler
--
-- Handler for combat events inside the dungeon
-- Retrieves the mapID (fixed for each dungeon), instanceID (dynamic) and NPCID (fixed) from combat events
-- and updates the dungeon log accordingly.
local function DungeonTrackerCombatLogEventHandler( self, event )
-- Make sure we only respond to the combat_log event
if event ~= "COMBAT_LOG_EVENT_UNFILTERED" then
return
end
-- Bail out right away if we don't have an active run (shouldn't happen, but could)
if not next( Hardcore_Character.dt.current ) then
return
end
-- Get the combat log data
local time_stamp, subevent, _, src_guid, src_name, _, _, dst_guid, dst_name = CombatLogGetCurrentEventInfo()
-- We don't want to accidentally identify anything from previous runs (SM wings), like dots and debuffs timing out
if subevent == "SPELL_AURA_REMOVED" or subevent == "SPELL_AURA_REMOVED_DOSE" then
return
end
-- Combat events have a source and a destination (doing and getting the damage). We don't care which one is the NPC.
-- If it's an NPC, we'll take it.
local mob_guid = nil
local mob_name = nil
if src_guid ~= nil then
local mob_type = string.split("-", src_guid)
if mob_type == "Creature" then
mob_guid = src_guid
mob_name = src_name
end
end
if mob_guid == nil and dst_guid ~= nil then
local mob_type = string.split("-", dst_guid)
if mob_type == "Creature" then
mob_guid = dst_guid
mob_name = dst_name
end
end
-- Return immediately if no NPC guid was found
if mob_guid == nil then
return
end
-- Split the GUID
local mob_type, _, server, map_id, instance_id, mob_type_id = string.split("-", mob_guid)
map_id = tonumber( map_id )
instance_id = tonumber( instance_id )
mob_type_id = tonumber( mob_type_id )
-- Do some checks, to eliminate unexpected results
if map_id ~= Hardcore_Character.dt.current.id then
Hardcore:Debug( "Error: Got a combat log message witn an NPC " .. mob_name .. " (" .. mob_type_id .. ") in wrong dungeon map " .. map_id .. " -- bailing out!" )
return
end
-- Store the instanceID (the dynamic one)
-- To be thread-safe and fast, the reconnecting happens in the main timer routine
DungeonTrackerStoreInstanceID( instance_id, subevent )
-- Pass this mob to the SM wing identifier. This will update dt.current.name if possible.
DungeonTrackerIdentifyScarletMonasteryWing( map_id, mob_type_id )