-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathl4d2_playstats_rl4d2l.sp
8293 lines (7101 loc) · 279 KB
/
l4d2_playstats_rl4d2l.sp
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
/*
todo
----
fix:
------
- the current CMT + forwards for teamswaps solution is kinda bad.
- would be nicer to fix CMT so the normal gamerules swapped
check is correct -- so: test whether "m_bAreTeamsFlipped"
can be unproblematically written to (yes, I was afraid to
just try this without doing some serious testing with it
first).
- end of round MVP chat prints: doesn't show your rank
- full game stats don't show before round is live
- full game stat: shows last round time, instead of full game time
build:
------
- skill
- clears / instaclears (show in stats)
- show average clear time (for all survivors?)
ideas
-----
- instead of hits/shots, display average multiplier for shotgun pellets
(can just do that per hitgroup, if we use what we know about the SI)
*/
#pragma semicolon 1
#pragma newdecls required
#include <sourcemod>
#include <sdktools>
#include <sdkhooks>
#include <left4dhooks>
#include <clientprefs>
#undef REQUIRE_PLUGIN
#include <readyup>
#include <confogl>
#include <system2>
#include <discord_scoreboard>
#define IS_VALID_CLIENT(%1) (%1 > 0 && %1 <= MaxClients)
#define IS_SURVIVOR(%1) (GetClientTeam(%1) == 2)
#define IS_INFECTED(%1) (GetClientTeam(%1) == 3)
#define IS_VALID_INGAME(%1) (IS_VALID_CLIENT(%1) && IsClientInGame(%1))
#define IS_VALID_SURVIVOR(%1) (IS_VALID_INGAME(%1) && IS_SURVIVOR(%1))
#define IS_VALID_INFECTED(%1) (IS_VALID_INGAME(%1) && IS_INFECTED(%1))
#define IS_SURVIVOR_ALIVE(%1) (IS_VALID_SURVIVOR(%1) && IsPlayerAlive(%1))
#define IS_INFECTED_ALIVE(%1) (IS_VALID_INFECTED(%1) && IsPlayerAlive(%1))
#define TEAM_SPECTATOR 1
#define TEAM_SURVIVOR 2
#define TEAM_INFECTED 3
#define ZC_SMOKER 1
#define ZC_BOOMER 2
#define ZC_HUNTER 3
#define ZC_SPITTER 4
#define ZC_JOCKEY 5
#define ZC_CHARGER 6
#define ZC_WITCH 7
#define ZC_TANK 8
#define ZC_NOTINFECTED 9
#define ZC_TOTAL 7
#define CONBUFSIZE (1 << 10) // 1k
#define CONBUFSIZELARGE (1 << 12) // 4k
#define MAXCHUNKS 10 // how many chunks of 4k max
#define CHARTHRESHOLD 160 // detecting unicode stuff
#define MAXLINESPERCHUNK 4 // how many lines in a chunk
#define DIVIDERINTERVAL 4 // add divider line every X lines
#define MAXTRACKED 64
#define MAXROUNDS 48 // ridiculously high, but just in case players do a marathon or something
#define MAXSHOWROUNDS 10 // how many rounds to show in the general stats table, max
#define MAXNAME 64
#define MAXNAME_TABLE 20 // name size max in console tables
#define MAXCHARACTERS 4
#define MAXMAP 32
#define MAXGAME 24
#define MAXWEAPNAME 24
#define STUMBLE_DMG_THRESH 3 // smaller than this is stumble damage (for chargers)
#define STATS_RESET_DELAY 5.0
#define ROUNDSTART_DELAY 5.5 // this should always be longer than CMT's roundstart scores check, so we know whether there's been a swap! hardcoded 5.0 in there
#define ROUNDEND_SCORE_DELAY 1.0
#define ROUNDEND_DELAY 3.0
#define ROUNDEND_DELAY_SCAV 2.0
#define PRINT_REPEAT_DELAY 15 // how many seconds to wait before re-doing automatic round end prints (opening/closing end door, etc)
#define PRINT_DELAY_INC 0.1 // print delay increments (pauses between tables)
#define FREQ_FLOWCHECK 1.0
#define MIN_TEAM_PRESENT_TIME 30 // how many seconds a player with 0-stats has to have been on a team to be listed as part of that team
#define WP_MELEE 19
#define WP_PISTOL 1
#define WP_PISTOL_MAGNUM 32
#define WP_SMG 2
#define WP_SMG_SILENCED 7
#define WP_HUNTING_RIFLE 6
#define WP_SNIPER_MILITARY 10
#define WP_PUMPSHOTGUN 3
#define WP_SHOTGUN_CHROME 8
#define WP_AUTOSHOTGUN 4
#define WP_SHOTGUN_SPAS 11
#define WP_RIFLE 5
#define WP_RIFLE_DESERT 9
#define WP_RIFLE_AK47 26
#define WP_SMG_MP5 33
#define WP_RIFLE_SG552 34
#define WP_SNIPER_AWP 35
#define WP_SNIPER_SCOUT 36
#define WP_RIFLE_M60 37
#define WP_MACHINEGUN 45
#define HITGROUP_HEAD 1
#define FIRST_NON_BOT 4 // first index that doesn't belong to a survivor bot
#define TOTAL_FFGIVEN 0
#define TOTAL_FFTAKEN 1
#define FFTYPE_TOTAL 0
#define FFTYPE_PELLET 1
#define FFTYPE_BULLET 2
#define FFTYPE_SNIPER 3
#define FFTYPE_MELEE 4
#define FFTYPE_FIRE 5
#define FFTYPE_INCAP 6
#define FFTYPE_OTHER 7
#define FFTYPE_SELF 8
#define FFTYPE_MAX 9
#define SORT_SI 0
#define SORT_CI 1
#define SORT_FF 2
#define SORT_INF 3
#define MAXSORTS 4
#define LTEAM_A 0
#define LTEAM_B 1
#define LTEAM_CURRENT 2
#define BREV_SI (1 << 0) // flags for MVP chat print appearance
#define BREV_CI (1 << 1)
#define BREV_FF (1 << 2)
#define BREV_RANK (1 << 3) // note: 16 reserved/removed
#define BREV_PERCENT (1 << 5)
#define BREV_ABSOLUTE (1 << 6)
#define AUTO_MVPCHAT_ROUND (1 << 0) // flags for what to print automatically at round end
#define AUTO_MVPCHAT_GAME (1 << 1)
#define AUTO_MVPCON_ROUND (1 << 2)
#define AUTO_MVPCON_GAME (1 << 3)
#define AUTO_MVPCON_TANK (1 << 4) // 16
#define AUTO_FFCON_ROUND (1 << 5)
#define AUTO_FFCON_GAME (1 << 6)
#define AUTO_SKILLCON_ROUND (1 << 7) // 128
#define AUTO_SKILLCON_GAME (1 << 8)
#define AUTO_ACCCON_ROUND (1 << 9)
#define AUTO_ACCCON_GAME (1 << 10) // 1024
#define AUTO_ACCCON_MORE_ROUND (1 << 11)
#define AUTO_ACCCON_MORE_GAME (1 << 12)
#define AUTO_FUNFACT_ROUND (1 << 13)
#define AUTO_FUNFACT_GAME (1 << 14) // 16384
#define AUTO_MVPCON_MORE_ROUND (1 << 15)
#define AUTO_MVPCON_MORE_GAME (1 << 16)
#define AUTO_INFCON_ROUND (1 << 17) // 131072
#define AUTO_INFCON_GAME (1 << 18) // 262144
// fun fact
#define FFACT_MAX_WEIGHT 10
#define FFACT_TYPE_CROWN 1
#define FFACT_TYPE_DRAWCROWN 2
#define FFACT_TYPE_SKEETS 3
#define FFACT_TYPE_MELEESKEETS 4
#define FFACT_TYPE_HUNTERDP 5
#define FFACT_TYPE_JOCKEYDP 6
#define FFACT_TYPE_M2 7
#define FFACT_TYPE_MELEETANK 8
#define FFACT_TYPE_CUT 9
#define FFACT_TYPE_POP 10
#define FFACT_TYPE_DEADSTOP 11
#define FFACT_TYPE_LEVELS 12
#define FFACT_TYPE_SCRATCH 13
#define FFACT_TYPE_DCHARGE 14
#define FFACT_TYPE_BOOMDMG 15
#define FFACT_TYPE_SPITDMG 16
#define FFACT_MAXTYPES 16
#define FFACT_MIN_CROWN 1
#define FFACT_MAX_CROWN 10
#define FFACT_MIN_DRAWCROWN 1
#define FFACT_MAX_DRAWCROWN 10
#define FFACT_MIN_SKEET 2
#define FFACT_MAX_SKEET 20
#define FFACT_MIN_MELEESKEET 1
#define FFACT_MAX_MELEESKEET 10
#define FFACT_MIN_HUNTERDP 2
#define FFACT_MAX_HUNTERDP 10
#define FFACT_MIN_JOCKEYDP 2
#define FFACT_MAX_JOCKEYDP 10
#define FFACT_MIN_M2 15
#define FFACT_MAX_M2 50
#define FFACT_MIN_MELEETANK 4
#define FFACT_MAX_MELEETANK 10
#define FFACT_MIN_CUT 4
#define FFACT_MAX_CUT 10
#define FFACT_MIN_POP 4
#define FFACT_MAX_POP 10
#define FFACT_MIN_DEADSTOP 7
#define FFACT_MAX_DEADSTOP 20
#define FFACT_MIN_LEVEL 3
#define FFACT_MAX_LEVEL 10
#define FFACT_MIN_SCRATCH 50
#define FFACT_MAX_SCRATCH 200
#define FFACT_MIN_DCHARGE 1
#define FFACT_MAX_DCHARGE 4
#define FFACT_MIN_BOOMDMG 40
#define FFACT_MAX_BOOMDMG 200
#define FFACT_MIN_SPITDMG 60
#define FFACT_MAX_SPITDMG 200
// writing
#define DIR_OUTPUT "logs/"
#define MAX_QUERY_SIZE 8192
#define FILETABLEFLAGS 164532 // AUTO_ flags for what to print to a file automatically
#define MAX(%0,%1) (((%0) > (%1)) ? (%0) : (%1))
#define MIN(%0,%1) (((%0) < (%1)) ? (%0) : (%1))
// types of statistic table(sets)
enum /*strStatType*/
{
typGeneral,
typMVP,
typFF,
typSkill,
typAcc,
typInf,
typFact
};
// information for entire game
enum /*strGameData*/
{
gmFailed, // survivors lost the mission * times
gmStartTime, // GetTime() value when starting
gmMaxSize
};
// information per round
enum /*strRoundData*/
{
rndRestarts, // how many times retried?
rndPillsUsed,
rndKitsUsed,
rndDefibsUsed,
rndCommon,
rndSIKilled,
rndSIDamage,
rndSISpawned,
rndWitchKilled,
rndTankKilled,
rndIncaps, // 10
rndDeaths,
rndFFDamageTotal,
rndStartTime, // GetTime() value when starting
rndEndTime, // GetTime() value when done
rndStartTimePause,
rndStopTimePause,
rndStartTimeTank,
rndStopTimeTank,
rndMaxSize
};
#define MAXRNDSTATS 18
// information per player
enum /*strPlayerData*/
{
plyShotsShotgun, // 0 pellets
plyShotsSmg, // all bullets from smg/rifle
plyShotsSniper, // all bullets from snipers
plyShotsPistol, // all bullets from pistol/magnum
plyHitsShotgun,
plyHitsSmg,
plyHitsSniper,
plyHitsPistol,
plyHeadshotsSmg, // headshots for everything but on tank
plyHeadshotsSniper,
plyHeadshotsPistol, // 10
plyHeadshotsSISmg, // headshots for SI only
plyHeadshotsSISniper,
plyHeadshotsSIPistol,
plyHitsSIShotgun, // all hits on special infected (not tank)
plyHitsSISmg,
plyHitsSISniper,
plyHitsSIPistol,
plyHitsTankShotgun, // all hits on tank
plyHitsTankSmg, // useful for getting real headshot count (leave tank out of it)
plyHitsTankSniper, // 20
plyHitsTankPistol,
plyCommon,
plyCommonTankUp,
plySIKilled,
plySIKilledTankUp,
plySIDamage,
plySIDamageTankUp,
plyIncaps,
plyDied,
plySkeets, // 30 skeets, full
plySkeetsHurt,
plySkeetsMelee,
plyLevels, // charger levels, full
plyLevelsHurt,
plyPops, // boomer pops (pre puke)
plyCrowns,
plyCrownsHurt, // non-full crowns
plyShoves, // count every shove
plyDeadStops,
plyTongueCuts, // 40 only real cuts
plySelfClears,
plyFallDamage,
plyDmgTaken,
plyDmgTakenBoom, // damage taken from common while boomed
plyDmgTakenCommon, // damage taken from common
plyDmgTakenTank, // damage taken from tank
plyBowls, // bowls from charger
plyCharges, // charges from charger
plyDeathCharges, // death charge count
plyFFGiven,
plyFFTaken, // 50
plyFFHits, // total amount of shotgun blasts / bullets / etc
plyTankDamage, // survivor damage to tank
plyWitchDamage,
plyMeleesOnTank,
plyRockSkeets,
plyRockEats,
plyFFGivenPellet,
plyFFGivenBullet,
plyFFGivenSniper, // 60
plyFFGivenMelee,
plyFFGivenFire,
plyFFGivenIncap,
plyFFGivenOther,
plyFFGivenSelf,
plyFFTakenPellet,
plyFFTakenBullet,
plyFFTakenSniper,
plyFFTakenMelee,
plyFFTakenFire, // 70
plyFFTakenIncap,
plyFFTakenOther,
plyFFGivenTotal,
plyFFTakenTotal,
plyCarsTriggered,
plyJockeyRideDuration,
plyJockeyRideTotal,
plyClears, // amount of clears (under a min)
plyAvgClearTime, // average time it takes to clear someone (* 1000 so it doesn't have to be a float)
plyTimeStartPresent, // 80 time present (on the team)
plyTimeStopPresent, // if stoptime is 0, then it's NOW, ongoing
plyTimeStartAlive,
plyTimeStopAlive, // time not capped
plyTimeStartUpright,
plyTimeStopUpright,
plyCurFlowDist,
plyFarFlowDist,
plyProtectAwards,
plyMaxSize
};
#define MAXPLYSTATS 88
// information per infected player (during other team's survivor round)
enum /*strInfData*/
{
infDmgTotal, // including on incapped, excluding all tank damage!
infDmgUpright, // 1
infDmgTank, // only upright
infDmgTankIncap, // only incapped
infDmgScratch, // only upright
infDmgScratchSmoker, // only upright
infDmgScratchBoomer, // only upright
infDmgScratchHunter, // only upright
infDmgScratchCharger, // only upright
infDmgScratchSpitter, // only upright
infDmgScratchJockey, // 10 only upright
infDmgSpit, // only upright
infDmgBoom, // only upright
infDmgTankUp, // only upright, excluding the tank itself
infHunterDPs, // damage pounce count
infHunterDPDmg, // damage pounce damage
infJockeyDPs,
infDeathCharges,
infCharges,
infMultiCharges,
infBoomsSingle, // 20
infBoomsDouble,
infBoomsTriple,
infBoomsQuad,
infBooms, // boomed survivors
infBoomerPops, // times popped as boomer
infLedged, // survivors ledged
infCommon, // common killed by SI
infSpawns,
infSpawnSmoker,
infSpawnBoomer, // 30
infSpawnHunter,
infSpawnCharger,
infSpawnSpitter,
infSpawnJockey,
infTankPasses,
infTankRockHits,
infCarsTriggered,
infJockeyRideDuration, // in milliseconds
infJockeyRideTotal,
infTimeStartPresent, // 40 time present (on the team)
infTimeStopPresent // if stoptime is 0, then it's NOW, ongoing
};
#define MAXINFSTATS 41
// trie values: weapon type (per accuracy-class)
enum /*strWeaponType*/
{
WPTYPE_NONE,
WPTYPE_SHOTGUN,
WPTYPE_SMG,
WPTYPE_SNIPER,
WPTYPE_PISTOL
};
// trie values: weapon type (per accuracy-class)
enum /*strMapType*/
{
MP_FINALE
};
// trie values: OnEntityCreated classname
enum /*strOEC*/
{
OEC_INFECTED,
OEC_WITCH
};
bool
g_bLateLoad = false,
g_bFirstLoadDone = false, // true after first onMapStart
g_bLoadSkipDone = false, // true after skipping the _resetnextmap for stats
g_bLGOAvailable = false, // whether confogl is loaded
g_bReadyUpAvailable = false,
g_bPauseAvailable = false,
g_bSkillDetectLoaded = false,
g_bSystem2Loaded = false,
g_bDiscordScoreboardAvailable = false,
g_bCMTActive = false, // whether custom map transitions is running a mapset
g_bCMTSwapped = false, // whether A/B teams have been swapped
g_bModeCampaign = false,
g_bModeScavenge = false,
g_bGameStarted = false,
g_bInRound = false,
g_bTeamChanged = false, // to only do a teamcheck if a check is not already pending
g_bTankInGame = false,
g_bPlayersLeftStart = false,
g_bSecondHalf = false, // second roundhalf in a versus round
g_bFailedPrevious = false, // whether the previous attempt was a failed campaign mode round
g_bPaused = false; // whether paused with pause.smx
Handle
g_hCookiePrint = null,
g_hCvarDatabaseConfig = null,
g_hCvarDebug = null,
g_hCvarMVPBrevityFlags = null,
g_hCvarAutoPrintVs = null,
g_hCvarAutoPrintCoop = null,
g_hCvarShowBots = null,
g_hCvarDetailPercent = null,
g_hCvarWriteStats = null,
g_hCvarSkipMap = null,
g_hCvarCustomConfig = null,
g_hTriePlayers = null, // trie for getting player index
g_hTrieWeapons = null, // trie for getting weapon type (from classname)
g_hTrieMaps = null, // trie for getting finale maps
g_hTrieEntityCreated = null, // trie for getting classname of entity created
hFileFinaleMaps = null,
g_hStatsFile; // handle for a statsfile that we write tables to
Database g_Database = null;
int
g_iRound = 0,
g_iCurTeam = LTEAM_A, // current logical team
g_iTeamSize = 4,
g_iLastRoundEndPrint = 0, // when the last automatic print was shown
g_iSurvived [2], // just for stats: how many survivors that round (0 = wipe)
g_iCookieValue[MAXPLAYERS + 1], // if a cookie is set for a client, this is its value
g_iPauseStart = 0, // time the current pause started
g_iScores[2], // scores for both teams, as currently known
g_iFirstScoresSet[3], // scores when first set for a new map (index 3 = 0 if not yet set)
g_iBoomedBy[MAXPLAYERS + 1], // if someone is boomed, by whom?
g_iPlayerIndexSorted[MAXSORTS][MAXTRACKED], // used to create a sorted list
g_iPlayerSortedUseTeam[MAXSORTS][MAXTRACKED], // after sorting: which team to use as the survivor team for player
g_iPlayerRoundTeam[3][MAXTRACKED], // which team is the player 0 = A, 1 = B, -1 = no team; [2] = current survivor round; [0]/[1] = team A / B (anyone who was ever on it)
g_iPlayerGameTeam[2][MAXTRACKED], // for entire game for team A / B if the player was ever on it
g_strGameData[gmMaxSize],
g_strAllRoundData[2][rndMaxSize], // rounddata for ALL rounds, per team
g_strRoundData[MAXROUNDS][2][rndMaxSize], // rounddata per game round, per team
g_strPlayerData[MAXTRACKED][plyMaxSize],
g_strRoundPlayerData[MAXTRACKED][2][plyMaxSize], // player data per team
g_strPlayerInfData[MAXTRACKED][plyMaxSize],
g_strRoundPlayerInfData[MAXTRACKED][2][plyMaxSize], // player data for infected action per team (team is survivor team! -- when infected player was on opposite team)
g_iPlayers = 0,
g_iConsoleBufChunks = 0,
g_iQueries = 0,
g_strRoundPvPFFData[MAXTRACKED][2][MAXTRACKED], // pvp ff data per team
g_strRoundPvPInfDmgData[MAXTRACKED][2][MAXTRACKED];
float
g_fHighestFlow[4]; // highest flow a survivor was seen to have in the round (per character 0-3)
char
g_sPlayerName [MAXTRACKED][MAXNAME],
g_sPlayerNameSafe[MAXTRACKED][MAXNAME], // version of name without unicode characters
g_sPlayerId[MAXTRACKED][32], // steam id
g_sMapName[MAXROUNDS][MAXMAP],
g_sConfigName[MAXMAP],
g_sConsoleBuf[MAXCHUNKS][CONBUFSIZELARGE],
errorBuffer[255],
g_sDatabaseConfig[64],
g_sStatsFile[MAXNAME]; // name for the statsfile we should write to
public Plugin myinfo =
{
name = "Player Statistics",
author = "Tabun, A1m`, devilesk",
description = "Tracks statistics, even when clients disconnect. MVP, Skills, Accuracy, etc. Modified for RL4D2L",
version = "1.1.1-rl4d2l-1.0.1",
url = "https://github.com/devilesk/rl4d2l-plugins"
};
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
g_bLateLoad = late;
CreateNative("PLAYSTATS_BroadcastRoundStats", Native_BroadcastRoundStats);
CreateNative("PLAYSTATS_BroadcastGameStats", Native_BroadcastGameStats);
RegPluginLibrary("l4d2_playstats");
return APLRes_Success;
}
// crox readyup usage
public void OnAllPluginsLoaded()
{
g_bLGOAvailable = LibraryExists("confogl");
g_bReadyUpAvailable = LibraryExists("readyup");
g_bPauseAvailable = LibraryExists("pause");
g_bSkillDetectLoaded = LibraryExists("skill_detect");
g_bSystem2Loaded = LibraryExists("system2");
g_bDiscordScoreboardAvailable = LibraryExists("discord_scoreboard");
}
public void OnLibraryRemoved(const char[] LibName)
{
CheckLib(LibName, false);
}
public void OnLibraryAdded(const char[] LibName)
{
CheckLib(LibName, true);
}
void CheckLib(const char[] LibName, bool state)
{
if (strcmp(LibName, "confogl") == 0) {
g_bLGOAvailable = state;
} else if (strcmp(LibName, "readyup") == 0) {
g_bReadyUpAvailable = state;
} else if (strcmp(LibName, "pause") == 0) {
g_bPauseAvailable = state;
} else if (strcmp(LibName, "skill_detect") == 0) {
g_bSkillDetectLoaded = state;
} else if (strcmp(LibName, "system2") == 0) {
g_bSystem2Loaded = state;
} else if (strcmp(LibName, "discord_scoreboard") == 0) {
g_bDiscordScoreboardAvailable = state;
}
}
public void OnPluginStart()
{
// events
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("scavenge_round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
HookEvent("mission_lost", Event_MissionLostCampaign, EventHookMode_Post);
HookEvent("map_transition", Event_MapTransition, EventHookMode_PostNoCopy);
HookEvent("finale_win", Event_FinaleWin, EventHookMode_PostNoCopy);
HookEvent("survivor_rescued", Event_SurvivorRescue, EventHookMode_Post);
HookEvent("player_team", Event_PlayerTeam, EventHookMode_Post);
HookEvent("player_spawn", Event_PlayerSpawn, EventHookMode_Post);
HookEvent("player_hurt", Event_PlayerHurt, EventHookMode_Post);
HookEvent("player_death", Event_PlayerDeath, EventHookMode_Post);
HookEvent("player_incapacitated", Event_PlayerIncapped, EventHookMode_Post);
HookEvent("player_ledge_grab", Event_PlayerLedged, EventHookMode_Post);
HookEvent("player_ledge_release", Event_PlayerLedgeRelease, EventHookMode_Post);
HookEvent("revive_success", Event_PlayerRevived, EventHookMode_Post);
HookEvent("player_falldamage", Event_PlayerFallDamage, EventHookMode_Post);
HookEvent("tank_spawn", Event_TankSpawned, EventHookMode_Post);
HookEvent("weapon_fire", Event_WeaponFire, EventHookMode_Post);
HookEvent("infected_hurt", Event_InfectedHurt, EventHookMode_Post);
HookEvent("witch_killed", Event_WitchKilled, EventHookMode_Post);
HookEvent("heal_success", Event_HealSuccess, EventHookMode_Post);
HookEvent("defibrillator_used", Event_DefibUsed, EventHookMode_Post);
HookEvent("pills_used", Event_PillsUsed, EventHookMode_Post);
HookEvent("adrenaline_used", Event_AdrenUsed, EventHookMode_Post);
HookEvent("player_now_it", Event_PlayerBoomed, EventHookMode_Post);
HookEvent("player_no_longer_it", Event_PlayerUnboomed, EventHookMode_Post);
HookEvent("charger_carry_start", Event_ChargerCarryStart, EventHookMode_Post);
HookEvent("charger_impact", Event_ChargerImpact, EventHookMode_Post);
HookEvent("jockey_ride", Event_JockeyRide, EventHookMode_Post);
HookEvent("jockey_ride_end", Event_JockeyRideEnd, EventHookMode_Post);
HookEvent("award_earned", Event_AwardEarned, EventHookMode_Post);
// Database config cvar
g_hCvarDatabaseConfig = CreateConVar(
"l4d2_playstats_database_cfg",
"l4d2_playstats",
"Name of database keyvalue entry to use in databases.cfg",
_, true, 0.0, false
);
g_hCvarCustomConfig = CreateConVar(
"l4d2_playstats_customcfg",
"",
"Name of custom cfg",
_, true, 0.0, false
);
// cvars
g_hCvarDebug = CreateConVar(
"sm_stats_debug",
"0",
"Debug mode",
_, true, 0.0, false
);
g_hCvarMVPBrevityFlags = CreateConVar(
"sm_survivor_mvp_brevity_latest",
"4",
"Flags for setting brevity of MVP chat report (hide 1:SI, 2:CI, 4:FF, 8:rank, 32:perc, 64:abs).",
_, true, 0.0, false
);
g_hCvarAutoPrintVs = CreateConVar(
"sm_stats_autoprint_vs_round",
"8325", // default = 1 (mvpchat) + 4 (mvpcon-round) + 128 (special round) = 133 + (funfact round) 8192 = 8325
"Flags for automatic print [versus round] (show 1,4:MVP-chat, 4,8,16:MVP-console, 32,64:FF, 128,256:special, 512,1024,2048,4096:accuracy).",
_, true, 0.0, false
);
g_hCvarAutoPrintCoop = CreateConVar(
"sm_stats_autoprint_coop_round",
"1289", // default = 1 (mvpchat) + 8 (mvpcon-all) + 256 (special all) + 1024 (acc all) = 1289
"Flags for automatic print [campaign round] (show 1,4:MVP-chat, 4,8,16:MVP-console, 32,64:FF, 128,256:special, 512,1024,2048,4096:accuracy).",
_, true, 0.0, false
);
g_hCvarShowBots = CreateConVar(
"sm_stats_showbots",
"1",
"Show bots in all tables (0 = show them in MVP and FF tables only)",
_, true, 0.0, false
);
g_hCvarDetailPercent = CreateConVar(
"sm_stats_percentdecimal",
"0",
"Show the first decimal for (most) MVP percent in console tables.",
_, true, 0.0, false
);
g_hCvarWriteStats = CreateConVar(
"sm_stats_writestats",
"0",
"Whether to store stats in logs/ dir (1 = write csv; 2 = write csv & pretty tables). Versus only.",
_, true, 0.0, false
);
g_hCvarSkipMap = CreateConVar(
"sm_stats_resetnextmap",
"0",
"First round is ignored (for use with confogl/matchvotes - this will be automatically unset after a new map is loaded).",
_, true, 0.0, false
);
g_iTeamSize = 4;
g_iFirstScoresSet[2] = 1; // don't save scores for first map
// commands:
RegConsoleCmd("sm_stats", Cmd_StatsDisplayGeneral, "Prints stats for survivors");
RegConsoleCmd("sm_mvp", Cmd_StatsDisplayGeneral, "Prints MVP stats for survivors");
RegConsoleCmd("sm_skill", Cmd_StatsDisplayGeneral, "Prints special skills stats for survivors");
RegConsoleCmd("sm_ff", Cmd_StatsDisplayGeneral, "Prints friendly fire stats stats");
RegConsoleCmd("sm_acc", Cmd_StatsDisplayGeneral, "Prints accuracy stats for survivors");
RegConsoleCmd("sm_stats_auto", Cmd_Cookie_SetPrintFlags, "Sets client-side preference for automatic stats-print at end of round");
RegAdminCmd("statsreset", Cmd_StatsReset, ADMFLAG_CHANGEMAP, "Resets the statistics. Admins only.");
RegConsoleCmd("say", Cmd_Say);
RegConsoleCmd("say_team", Cmd_Say);
// cookie
g_hCookiePrint = RegClientCookie("sm_stats_autoprintflags", "Stats Auto Print Flags", CookieAccess_Public);
// tries
InitTries();
// prepare team array
ClearPlayerTeam();
if (g_bLateLoad) {
int i;
int time = GetTime();
for (i = 1; i <= MaxClients; i++) {
if (IsClientInGame(i) && !IsFakeClient(i)) {
// store each player with a first check
int index = GetPlayerIndexForClient(i);
// set start time to now
if (IS_VALID_SURVIVOR(i)) {
g_strRoundPlayerData[index][0][plyTimeStartPresent] = time;
g_strRoundPlayerData[index][0][plyTimeStartAlive] = time;
g_strRoundPlayerData[index][0][plyTimeStartUpright] = time;
g_strRoundPlayerData[index][1][plyTimeStartPresent] = time;
g_strRoundPlayerData[index][1][plyTimeStartAlive] = time;
g_strRoundPlayerData[index][1][plyTimeStartUpright] = time;
} else {
g_strRoundPlayerInfData[index][0][infTimeStartPresent] = time;
}
}
}
// set time for bots aswell
for (i = 0; i < FIRST_NON_BOT; i++) {
g_strRoundPlayerData[i][0][plyTimeStartPresent] = time;
g_strRoundPlayerData[i][0][plyTimeStartAlive] = time;
g_strRoundPlayerData[i][0][plyTimeStartUpright] = time;
g_strRoundPlayerData[i][1][plyTimeStartPresent] = time;
g_strRoundPlayerData[i][1][plyTimeStartAlive] = time;
g_strRoundPlayerData[i][1][plyTimeStartUpright] = time;
}
// just assume this
g_bInRound = true;
g_bPlayersLeftStart = true;
g_strGameData[gmStartTime] = GetTime();
g_strRoundData[0][0][rndStartTime] = GetTime();
g_strRoundData[0][1][rndStartTime] = GetTime();
// team
g_iCurTeam = (g_bModeCampaign) ? 0 : GetCurrentTeamSurvivor();
UpdatePlayerCurrentTeam();
}
PrintDebug(1, "OnPluginStart g_bLateLoad %i %i %i", g_bLateLoad, g_strGameData[gmStartTime], g_strRoundData[0][0][rndStartTime]);
}
/*
Forwards from confogl
--------------------- */
public void LGO_OnMatchModeStart(const char[] sConfig)
{
// ignore this map, match will start on next reload.
g_bLoadSkipDone = false;
}
public void OnConfigsExecuted()
{
g_iTeamSize = GetConVarInt(FindConVar("survivor_limit"));
// currently loaded config?
g_sConfigName = "";
ConVar tmpHandle = FindConVar("l4d_ready_cfg_name");
if (tmpHandle != null) {
tmpHandle.GetString(g_sConfigName, MAXMAP);
}
PrintDebug(1, "OnConfigsExecuted %i", g_Database == null);
InitDatabase();
}
// find a player
public void OnClientPostAdminCheck(int client)
{
GetPlayerIndexForClient(client);
}
public void OnClientDisconnect(int client)
{
g_iCookieValue[client] = 0;
if (!g_bPlayersLeftStart) {
return;
}
int index = GetPlayerIndexForClient(client);
if (index == -1) {
return;
}
int time = GetTime();
// if paused, substract time so far from player's time in game
if (g_bPaused) {
time = g_iPauseStart;
}
// only note time for survivor team players
if (g_iPlayerRoundTeam[LTEAM_CURRENT][index] == g_iCurTeam) {
// survivor leaving
// store time they left
g_strRoundPlayerData[index][g_iCurTeam][plyTimeStopPresent] = time;
if (!g_strRoundPlayerData[index][g_iCurTeam][plyTimeStopAlive]) {
g_strRoundPlayerData[index][g_iCurTeam][plyTimeStopAlive] = time;
}
if (!g_strRoundPlayerData[index][g_iCurTeam][plyTimeStopUpright]) {
g_strRoundPlayerData[index][g_iCurTeam][plyTimeStopUpright] = time;
}
} else if (g_iPlayerRoundTeam[LTEAM_CURRENT][index] == (g_iCurTeam) ? 0 : 1) {
// infected leaving
g_strRoundPlayerInfData[index][g_iCurTeam][infTimeStopPresent] = time;
}
}
public void OnMapStart()
{
g_bSecondHalf = false;
CheckGameMode();
if (!g_bLoadSkipDone && (g_bLGOAvailable || GetConVarBool(g_hCvarSkipMap))) {
// reset stats and unset cvar
PrintDebug(2, "OnMapStart: Resetting all stats (resetnextmap setting)... ");
ResetStats(false, -1);
// this might not work (server might be resetting the resetnextmap var every time
// so also using the bool to make sure it only happens once
SetConVarInt(g_hCvarSkipMap, 0);
g_bLoadSkipDone = true;
g_iFirstScoresSet[0] = 0;
g_iFirstScoresSet[1] = 0;
g_iFirstScoresSet[2] = 1;
} else if (g_bFirstLoadDone) {
// reset stats for previous round
PrintDebug(2, "OnMapStart: Reset stats for round (Timer_ResetStats)");
CreateTimer(STATS_RESET_DELAY, Timer_ResetStats, 1, TIMER_FLAG_NO_MAPCHANGE);
}
g_bFirstLoadDone = true;
// start flow-check timer
CreateTimer(FREQ_FLOWCHECK, Timer_SaveFlows, _, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE);
// save map name (after onmapload resets, so it doesn't get deleted)
GetCurrentMapLower(g_sMapName[g_iRound], MAXMAP);
//PrintDebug(2, "MapStart (round %i): %s ", g_iRound, g_sMapName[g_iRound]);
}
public void OnMapEnd()
{
//PrintDebug(2, "MapEnd (round %i)", g_iRound);
g_bInRound = false;
g_iRound++;
// if this was a finale, (and CMT is not loaded), end of game
if (!g_bCMTActive && !g_bModeCampaign && IsMissionFinalMap()) {
HandleGameEnd();
}
}
public void Event_MissionLostCampaign(Event hEvent, const char[] eName, bool dontBroadcast)
{
//PrintDebug(2, "Event: MissionLost (times %i)", g_strGameData[gmFailed] + 1);
g_strGameData[gmFailed]++;
g_strRoundData[g_iRound][g_iCurTeam][rndRestarts]++;
HandleRoundEnd(true);
}
public void Event_RoundStart(Event hEvent, const char[] eName, bool dontBroadcast)
{
HandleRoundStart();
CreateTimer(ROUNDSTART_DELAY, Timer_RoundStart, _, TIMER_FLAG_NO_MAPCHANGE);
}
void HandleRoundStart(bool bLeftStart = false)
{
//PrintDebug(1, "HandleRoundStart (leftstart: %i): inround: %i", bLeftStart, g_bInRound);
if (g_bInRound) {
return;
}
g_bInRound = true;
g_bPlayersLeftStart = bLeftStart;
g_bTankInGame = false;
g_bPaused = false;
if (bLeftStart) {
g_iCurTeam = (g_bModeCampaign) ? 0 : GetCurrentTeamSurvivor();
ClearPlayerTeam(g_iCurTeam);
}
}
// delayed, so we can trust GetCurrentTeamSurvivor()
public Action Timer_RoundStart(Handle hTimer)
{
// easier to handle: store current survivor team
g_iCurTeam = (g_bModeCampaign) ? 0 : GetCurrentTeamSurvivor();
// clear team for stats
ClearPlayerTeam(g_iCurTeam);
//PrintDebug(2, "Event_RoundStart (roundhalf: %i: survivor team: %i (cur survivor: %i))", (g_bSecondHalf) ? 1 : 0, g_iCurTeam, GetCurrentTeamSurvivor());
return Plugin_Stop;
}
public void Event_RoundEnd(Event hEvent, const char[] eName, bool dontBroadcast)
{
// In coop, when we fail, the mission lost event has already handled this.
if (g_bModeCampaign && g_bFailedPrevious) {
return;
}
// called on versus round end
// and mission failed coop
HandleRoundEnd();
}
// do something when round ends (including for campaign mode)
void HandleRoundEnd(bool bFailed = false)
{
PrintDebug(1, "HandleRoundEnd (failed: %i): inround: %i, current round: %i", bFailed, g_bInRound, g_iRound);
// only do once
if (!g_bInRound && !g_bModeCampaign) {
return;
}
// count survivors
g_iSurvived[g_iCurTeam] = GetUprightSurvivors();
// note end of tankfight
if (g_bTankInGame) {
HandleTankTimeEnd();
}
// set all 0 times to present
SetRoundEndTimes();
g_bInRound = false;
if (!g_bModeCampaign || !bFailed) {
// write stats for this roundhalf to file
// do before addition, because these are round stats
if (GetConVarBool(g_hCvarWriteStats)) {
PrintDebug( 1, "[Stats] Writing stats to database started." );
if (g_bSecondHalf) {
CreateTimer(ROUNDEND_SCORE_DELAY, Timer_WriteStats, g_iCurTeam);
} else {
//WriteStatsToFile(g_iCurTeam, false);
WriteStatsToDB(g_iCurTeam, false);
}
} else {
PrintDebug(1, "[Stats] Writing stats to database disabled.");
}