-
Notifications
You must be signed in to change notification settings - Fork 9
/
mybalance.py
1419 lines (1162 loc) · 56.3 KB
/
mybalance.py
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
# This is a plugin created by iouonegirl(@gmail.com)
# Copyright (c) 2016 iouonegirl
# https://github.com/dsverdlo/minqlx-plugins
#
# You are free to modify this plugin to your custom,
# except for the version command related code.
#
# Thanks to Bus Station, Minkyn, Melodeiro, BarelyMiSSeD,
# TNT and Shin0 for their input on this plugin.
#
# Its purpose is to balance the games on a server out.
# Some features of this plugin include:
# - setting up an ELO* limit (minimum and maximum values)
# - kicking players outside this limit
# - - prevent players from getting kicked with !nokick
# - allowing players outside the limit to only spec
# - blocking players outside the limit from connecting
# - - block players, but normal kick those who are close_enough
# - Uneven teams action: spec, slay, ignore
# - Display ready-up interval reminders during a long warmup
# - Optional autoshuffle before a match (disables shuffle callvotes)
# - Freezes then specs players while teams are uneven in CTF and TDM
#
#
# Uses:
# - set qlx_elo_limit_min "0"
# - set qlx_elo_limit_max "1600"
# - set qlx_elo_games_needed "10"
#
# - set qlx_mybalance_perm_allowed "2"
# ^ (players with this perm-level will always be allowed)
#
# - set qlx_mybalance_autoshuffle "0"
# ^ (set "1" if you want an automatic shuffle before every match)
#
# - set qlx_mybalance_exclude "0"
# ^ (set "1" if you want to kick players without enough info/games)
#
# - set qlx_elo_kick "1"
# ^ (set "1" to kick spectators after they joined)
#
# - set qlx_elo_block_connecters "0"
# ^ (set "1" to block players from connecting)
#
# - set qlx_elo_close_enough "20"
# ^ (if blocking is on, and a player's glicko differs less than N from
# the limit, let them join for a normal kick (giving a chance to !nokick))
# (set this to 0 to disable this feature)
#
# - set qlx_mybalance_warmup_seconds "300"
# ^ (how many seconds of warmup before readyup messages come. Set to -1 to disable)
#
# - set qlx_mybalance_warmup_interval "60"
# ^ (interval in seconds for readyup messages)
#
# - set qlx_mybalance_uneven_time "10"
# ^ (for CTF and TDM, specify how many seconds to wait before balancing uneven teams)
#
# - set qlx_mybalance_elo_bump_regs "[]"
# ^ (Add a little bump to the boundary for regulars.
# This list must be in ordered lists of [games_needed, elo_bump] from small to big
# E.g. "[ [25,100], [50,200], [75,400], [100,800] ]"
# --> if a player has played 60 games on our server -> he reaches [50,200] and the upper elo limit adds 200)
#
import minqlx
import requests
import itertools
import threading
import random
import time
import os
import re
from minqlx.database import Redis
VERSION = "v0.56.5"
# This code makes sure the required superclass is loaded automatically
try:
from .iouonegirl import iouonegirlPlugin
except:
try:
abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py")
res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py")
if res.status_code != requests.codes.ok: raise
with open(abs_file_path,"a+") as f: f.write(res.text)
from .iouonegirl import iouonegirlPlugin
except Exception as e :
minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e))
raise
BOUNDARIES = []
# If this is True, a message will be printed on the screen of the person who should spec when teams are uneven
CP = True
CP_MESS = "\n\n\nTeams are uneven. You will be forced to spec."
# Default action to be performed when teams are uneven:
# Options: spec, slay, ignore
DEFAULT_LAST_ACTION = "spec"
# Database Keys
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
PLAYER_KEY = "minqlx:players:{}"
LAST_KEY = "minqlx:last"
COMPLETED_KEY = PLAYER_KEY + ":games_completed"
LEFT_KEY = PLAYER_KEY + ":games_left"
# Yep
EXCEPTIONS_FILE = "exceptions.txt"
# Elo retrieval vars
EXT_SUPPORTED_GAMETYPES = ("ca", "ctf", "dom", "ft", "tdm", "duel", "ffa")
RATING_KEY = "minqlx:players:{0}:ratings:{1}" # 0 == steam_id, 1 == short gametype.
MAX_ATTEMPTS = 3
CACHE_EXPIRE = 60*30 # 30 minutes TTL.
DEFAULT_RATING = 1500
SUPPORTED_GAMETYPES = ("ca", "ctf", "dom", "ft", "tdm")
class mybalance(iouonegirlPlugin):
def __init__(self):
super().__init__(self.__class__.__name__, VERSION)
# set cvars once. EDIT THESE IN SERVER.CFG!
self.set_cvar_once("qlx_elo_limit_min", "0")
self.set_cvar_once("qlx_elo_limit_max", "1600")
self.set_cvar_once("qlx_elo_games_needed", "10")
self.set_cvar_once("qlx_balanceApi", "elo")
self.set_cvar_once("qlx_elo_kick", "1")
self.set_cvar_once("qlx_elo_block_connecters", "0")
self.set_cvar_once("qlx_mybalance_warmup_seconds", "300")
self.set_cvar_once("qlx_mybalance_warmup_interval", "60")
self.set_cvar_once("qlx_mybalance_autoshuffle", "0")
self.set_cvar_once("qlx_mybalance_perm_allowed", "2")
self.set_cvar_once("qlx_mybalance_exclude", "0")
self.set_cvar_once("qlx_mybalance_uneven_time", "10")
self.set_cvar_once("qlx_mybalance_elo_bump_regs", "[]")
self.set_cvar_once("qlx_elo_close_enough", "20")
# get cvars
self.ELO_MIN = int(self.get_cvar("qlx_elo_limit_min"))
self.ELO_MAX = int(self.get_cvar("qlx_elo_limit_max"))
self.GAMES_NEEDED = int(self.get_cvar("qlx_elo_games_needed"))
try:
global BOUNDARIES
BOUNDARIES = eval(self.get_cvar("qlx_mybalance_elo_bump_regs"))
assert type(BOUNDARIES) is list
for _e, _b in BOUNDARIES:
assert type(_e) is int
assert type(_b) is int
except:
BOUNDARIES = []
self.prevent = False
self.last_action = DEFAULT_LAST_ACTION
self.jointimes = {}
self.game_active = self.game.state == "in_progress"
# Vars for CTF / TDM
self.ctfplayer = False
self.checking_balance = False
# steam_id : [name, elo]
self.kicked = {}
# collection of [steam_id, name, thread]
self.kickthreads = []
# Collection of threads looking up elo of players {steam_id: thread }
self.connectthreads = {}
# Keep broadcasting warmup reminders?
self.warmup_reminders = True
self.ratings_lock = threading.RLock()
# Keys: steam_id - Items: {"ffa": {"elo": 123, "games": 321, "local": False}, ...}
self.ratings = {}
self.exceptions = []
self.cmd_help_load_exceptions(None, None, None)
self.add_command("prevent", self.cmd_prevent_last, 2)
self.add_command("last", self.cmd_last_action, 2, usage="[SLAY|SPEC|IGNORE]")
self.add_command(("load_exceptions", "reload_exceptions", "list_exceptions", "listexceptions", "exceptions"), self.cmd_help_load_exceptions, 3)
self.add_command(("add_exception", "elo_exception"), self.cmd_add_exception, 3, usage="<name>|<steam_id> <name>")
self.add_command(("del_exception", "rem_exception"), self.cmd_del_exception, 3, usage="<name>|<id>|<steam_id>")
self.add_command("elokicked", self.cmd_elo_kicked)
self.add_command("remkicked", self.cmd_rem_kicked, 2, usage="<id>")
self.add_command(("nokick", "dontkick"), self.cmd_nokick, 2, usage="[<name>]")
self.add_command(("limit", "limits", "elolimit"), self.cmd_elo_limit)
self.add_command(("elomin", "minelo"), self.cmd_min_elo, 3, usage="[ELO]")
self.add_command(("elomax", "maxelo"), self.cmd_max_elo, 3, usage="[ELO]")
self.add_command(("rankings", "elotype"), self.cmd_elo_type, usage="[A|B]")
self.add_command("reminders", self.cmd_warmup_reminders, 2, usage="[ON|OFF]")
self.add_hook("team_switch", self.handle_team_switch)
self.add_hook("round_end", self.handle_round_end)
self.add_hook("round_countdown", self.handle_round_count)
self.add_hook("round_start", self.handle_round_start)
self.add_hook("game_start", self.handle_game_start)
self.add_hook("game_end", self.handle_game_end)
self.add_hook("map", self.handle_map)
self.add_hook("player_connect", self.handle_player_connect, priority=minqlx.PRI_HIGH)
self.add_hook("player_disconnect", self.handle_player_disconnect)
self.add_hook("game_countdown", self.handle_game_countdown)
self.add_hook("new_game", self.handle_new_game)
self.add_hook("vote_called", self.handle_vote_called)
self.add_command(("setrating", "setelo"), self.cmd_setrating, 3, priority=minqlx.PRI_HIGH, usage="<id>|<name> <rating>")
self.add_command(("getrating", "getelo", "elo"), self.cmd_getrating, priority=minqlx.PRI_HIGH, usage="<id>|<name> [gametype]")
self.add_command(("remrating", "remelo"), self.cmd_remrating, 3, priority=minqlx.PRI_HIGH, usage="<id>|<name>")
self.add_command("belo", self.cmd_getratings, usage="<id>|<name> [gametype]")
#self.unload_overlapping_commands()
self.handle_new_game() # start counting reminders if we are in warmup
if self.game_active and self.game.type_short in ['ctf', 'tdm']:
self.balance_before_start(self.game.type_short, True)
def cmd_elo_type(self, player, msg, channel):
if len(msg) < 2:
if self.get_cvar('qlx_balanceApi') == 'elo':
channel.reply("^7The server is retrieving A (normal) rankings.")
elif self.get_cvar('qlx_balanceApi') == 'elo_b':
channel.reply("^7The server is retrieving with B (fun server) rankings.")
return
elif len(msg) < 3:
# If the player doesnt have the permission to change it
if not self.db.has_permission(player, 3):
player.tell("^6You don't have the required permission (3) to perform this action. ")
return minqlx.RET_STOP_ALL
# If there was not a correct ranking type given
rankings = {'a':'elo', 'b': 'elo_b'}
if not (msg[1].lower() in rankings):
return minqlx.RET_USAGE
self.set_cvar('qlx_balanceApi', rankings[msg[1].lower()])
channel.reply("^7Switched to ^6{}^7 rankings.".format(msg[1].upper()))
return
def cmd_min_elo(self, player, msg, channel):
if len(msg) < 2:
channel.reply("^7The minimum skill rating required for this server is: ^6{}^7.".format(self.ELO_MIN))
elif len(msg) < 3:
try:
new_elo = int(msg[1])
assert new_elo >= 0
except:
return minqlx.RET_USAGE
self.ELO_MIN = new_elo
channel.reply("^7The server minimum skill rating has been temporarily set to: ^6{}^7.".format(new_elo))
else:
return minqlx.RET_USAGE
def cmd_max_elo(self, player, msg, channel):
if len(msg) < 2:
channel.reply("^7The maximum skill rating set for this server is: ^6{}^7.".format(self.ELO_MAX))
elif len(msg) < 3:
try:
new_elo = int(msg[1])
assert new_elo >= 0
except:
return minqlx.RET_USAGE
self.ELO_MAX = new_elo
channel.reply("^7The server maximum skill ratings has been temporarily set to: ^6{}^7.".format(new_elo))
else:
return minqlx.RET_USAGE
def cmd_elo_limit(self, player, msg, channel):
if int(self.get_cvar('qlx_elo_block_connecters')):
close_enough = self.get_cvar("qlx_elo_close_enough", int)
if close_enough:
close_enough = " (and normal kick when ^6{}^7 from limit)".format(close_enough)
else:
close_enough = ""
self.msg("^7Players will be blocked on connection outside limits: [^6{}^7-^6{}^7]{}.".format(self.ELO_MIN, self.ELO_MAX, close_enough))
elif int(self.get_cvar('qlx_elo_kick')):
self.msg("^7The server will kick players who fall outside [^6{}^7-^6{}^7].".format(self.ELO_MIN, self.ELO_MAX))
else:
self.msg("^7Players who don't have a skill rating between ^6{} ^7and ^6{} ^7are only allowed to spec.".format(self.ELO_MIN, self.ELO_MAX))
# View a list of kicked players with their ID and elo
@minqlx.thread
def cmd_elo_kicked(self, player, msg, channel):
@minqlx.next_frame
def reply(m):
if player: player.tell(m)
else: channel.reply(m)
n = 0
if not self.kicked:
reply("No players kicked since plugin (re)start.")
for sid in self.kicked:
name, elo = self.kicked[sid]
m = "^7{}: ^6{}^7 - ^6{}^7 - ^6{}".format(n, sid, elo, name)
reply(m)
n += 1
time.sleep(0.2)
return minqlx.RET_STOP_ALL
def cmd_rem_kicked(self, player, msg, channel):
if len(msg) < 2:
return minqlx.RET_USAGE
try:
n = int(msg[1])
assert 0 <= n < len(self.kicked)
except:
return minqlx.RET_USAGE
counter = 0
for sid in self.kicked.copy():
if counter == n:
name, elo = self.kicked[sid]
del self.kicked[sid]
break
counter += 1
channel.reply("^7Successfully removed ^6{}^7 (glicko {}) from the list.".format(name, elo))
def cmd_nokick(self, player, msg, channel):
def dontkick(kickthread):
sid, nam, thr = kickthread
thr.stop()
if sid in self.kicked:
del self.kicked[sid]
new_kickthreats = []
for kt in self.kickthreads:
if kt[0] != sid:
new_kickthreats.append(kt)
else:
kt[2].stop()
self.kickthreads = new_kickthreats
try:
self.find_player(nam)[0].unmute()
except:
pass
channel.reply("^7An admin has prevented {} from being kicked.".format(nam))
if not self.kickthreads:
player.tell("^6Psst^7: There are no people being kicked right now.")
return minqlx.RET_STOP_ALL
# if there is only one
if len(self.kickthreads) == 1:
dontkick(self.kickthreads[0])
return
# If no arguments given
if len(msg) < 2:
_names = map(lambda _el: _el[1], self.kickthreads)
player.tell("^6Psst^7: did you mean ^6{}^7?".format("^7 or ^6".join(_names)))
return minqlx.RET_STOP_ALL
# If a search term, name, was given
else:
match_threads = [] # Collect matching names
new_threads = [] # Collect non-matching threads
for kt in self.kickthreads:
if msg[1] in kt[1]:
match_threads.append(kt)
else:
new_threads.append(kt)
# If none of the threads had a name like that
if not match_threads:
player.tell("^6Psst^7: no players matched '^6{}^7'?".format(msg[1]))
return minqlx.RET_STOP_ALL
# If there was one result:
if len(match_threads) == 1:
self.kickthreads = new_threads
dontkick(match_threads.pop())
return
# If multiple results were found:
else:
_names = map(lambda el: el[1], match_threads)
player.tell("^6Psst^7: did you mean ^6{}^7?".format("^7 or ^6".join(_names)))
return minqlx.RET_STOP_ALL
def cmd_add_exception(self, player, msg, channel):
try:
# more than 2 arguments = NO NO
if len(msg) > 3:
return minqlx.RET_USAGE
# less than 2 arguments is NOT OKAY if it was with a steam id
if len(msg) < 3 and len(msg[1]) == 17:
return minqlx.RET_USAGE
# if steam_id given
match_id = re.search('[0-9]{17}', msg[1])
if match_id and match_id.group() == msg[1]:
add_sid = int(msg[1])
add_nam = msg[2]
# if name given
else:
target = self.find_by_name_or_id(player, msg[1])
if not target:
return minqlx.RET_STOP_ALL
add_sid = target.steam_id
add_nam = msg[2] if len(msg) == 3 else target.name
abs_file_path = os.path.join(self.get_cvar("fs_homepath"), EXCEPTIONS_FILE)
with open (abs_file_path, "r") as file:
for line in file:
if line.startswith("#"): continue
split = line.split()
sid = split.pop(0)
name = " ".join(split)
if int(sid) == add_sid:
player.tell("^6Psst: ^7This ID is already in the exception list under name ^6{}^7!".format(name))
return minqlx.RET_STOP_ALL
with open (abs_file_path, "a") as file:
file.write("{} {}\n".format(add_sid, add_nam))
if not add_sid in self.exceptions:
self.exceptions.append(add_sid)
if add_sid in self.kicked:
del self.kicked[add_sid]
player.tell("^6Psst: ^2Succesfully ^7added ^6{} ^7to the exception list.".format(add_nam))
return minqlx.RET_STOP_ALL
except IOError as e:
player.tell("^6Psst: IOError: ^7{}".format(e))
except ValueError as e:
return minqlx.RET_USAGE
except Exception as e:
player.tell("^6Psst: ^1Error: ^7{}".format(e))
return minqlx.RET_STOP_ALL
# Load a list of exceptions
def cmd_help_load_exceptions(self, player, msg, channel):
names = {}
for p in self.players():
names[p.steam_id] = p.name
try:
abs_file_path = os.path.join(self.get_cvar("fs_homepath"), EXCEPTIONS_FILE)
with open (abs_file_path, "r") as file:
excps = []
n = 0
if player: player.tell("^6Psst: ^7Glicko exceptions:\n")
for line in file:
if line.startswith("#"): continue # comment lines
split = line.split()
sid = split.pop(0)
name = " ".join(split)
try:
excps.append(int(sid))
if player:
_name = names[int(sid)] if int(sid) in names else name.strip('\n\r\t')
player.tell("^6Psst: ^7{} ({})".format(sid, _name))
n += 1
except:
continue
self.exceptions = excps
if player:
player.tell("^6Open your console to see {} exceptions.".format(n))
except IOError as e:
try:
abs_file_path = os.path.join(self.get_cvar("fs_homepath"), EXCEPTIONS_FILE)
with open(abs_file_path,"a+") as f:
f.write("# This is a commented line because it starts with a '#'\n")
f.write("# Every exception on a newline, format: STEAMID NAME\n")
f.write("# The NAME is for a mental reference and may contain spaces\n")
f.write("{} (owner)\n".format(self.get_cvar('qlx_owner')))
minqlx.CHAT_CHANNEL.reply("^6mybalance plugin^7: No exception list found, so I made one myself.")
except:
minqlx.CHAT_CHANNEL.reply("^1Error: ^7reading and creating exception list: {}".format(e))
except Exception as e:
minqlx.CHAT_CHANNEL.reply("^1Error: ^7reading exception list: {}".format(e))
def cmd_del_exception(self, player, msg, channel):
if len(msg) != 2:
return minqlx.RET_USAGE
try:
# if steam_id given
assert len(msg[1]) == 17
add_sid = int(msg[1])
except:
# if name given
target = self.find_by_name_or_id(player, msg[1])
if not target:
return minqlx.RET_STOP_ALL
add_sid = target.steam_id
try:
f = open(os.path.join(self.get_cvar("fs_homepath"), EXCEPTIONS_FILE),"r+")
d = f.readlines()
f.seek(0)
for i in d:
if not i.startswith(str(add_sid)):
f.write(i)
else:
player.tell("^6Player found and removed!")
if add_sid in self.exceptions:
self.exceptions.remove(add_sid)
msg = None
f.truncate()
f.close()
if msg: player.tell("^6{} was not found in the exception list...".format(msg[1]))
except:
player.tell("^1Error^7: cannot open exception list.")
return minqlx.RET_STOP_ALL
def handle_vote_called(self, caller, vote, args):
# If it is not shuffle, whatever
if vote.lower() != "shuffle": return
# Shuffle won't be called in ffa or duel
if self.game.type_short in ["ffa", "duel"]: return
# If it is shuffle and we have autoshuffle enabled...
if self.get_cvar("qlx_mybalance_autoshuffle", int):
self.msg("^7Callvote shuffle ^1DENIED ^7since the server will ^3autoshuffle ^7on match start.")
return minqlx.RET_STOP_ALL
def cmd_warmup_reminders(self, player, msg, channel):
if len(msg) < 2 and self.warmup_reminders:
s = self.get_cvar('qlx_mybalance_warmup_seconds')
i = self.get_cvar('qlx_mybalance_warmup_interval')
channel.reply("^7Warmup reminders will be displayed after {}s at {}s intervals.".format(s,i))
elif len(msg) < 2:
channel.reply("^7Warmup reminders have currently been turned ^6off^7.")
elif len(msg) < 3 and msg[1].lower() in ['on', 'off']:
if not self.warmup_reminders and (msg[1].lower() == 'on'):
self.warmup_reminders = True
self.check_warmup(time.time(), self.game.map)
self.warmup_reminders = msg[1].lower() == 'on'
channel.reply("^7Warmup reminders have been turned ^6{}^7.".format(msg[1].lower()))
else:
return minqlx.RET_USAGE
# Goes off when new maps are loaded, games are aborted, games ended but stay on same map and matchstart
@minqlx.delay(3)
def handle_new_game(self):
if self.game.state in ["in_progress", "countdown"]: return
self.game_active = False
self.checking_balance = False
self.check_warmup(time.time(), self.game.map)
@minqlx.thread
def check_warmup(self, warmup, mapname):
while self.is_game_in_warmup() and self.game_with_map_loaded(mapname) and self.warmup_reminders and \
self.is_plugin_still_loaded() and self.is_warmup_seconds_enabled() and \
self.is_there_more_than_one_player_joined():
diff = time.time() - warmup # difference in seconds
if diff >= int(self.get_cvar('qlx_mybalance_warmup_seconds')):
pgs = minqlx.Plugin._loaded_plugins
if 'maps' in pgs and pgs['maps'].plugin_active:
m = "^7Type ^2!s^7 to skip this map, or ^3ready up^7! "
if self.get_cvar("qlx_mybalance_autoshuffle", int):
m += "\nTeams will auto shuffle+balance!"
self.msg(m.replace('\n', ''))
self.center_print(m)
else:
m = "^7Time to ^3ready^7 up! "
if self.get_cvar("qlx_mybalance_autoshuffle", int):
m += "\nTeams will be auto shuffled and balanced!"
self.msg(m.replace('\n', ''))
self.center_print(m)
time.sleep(int(self.get_cvar('qlx_mybalance_warmup_interval')))
continue
time.sleep(1)
def is_game_in_warmup(self) -> bool:
if not self.game:
return False
return self.game.state == "warmup"
def game_with_map_loaded(self, mapname) -> bool:
if not self.game:
return False
return self.game.map == mapname
def is_plugin_still_loaded(self) -> bool:
return self.__class__.__name__ in minqlx.Plugin._loaded_plugins
def is_warmup_seconds_enabled(self) -> bool:
return self.get_cvar('qlx_mybalance_warmup_seconds', int) > -1
def is_there_more_than_one_player_joined(self) -> bool:
teams = self.teams()
return len(teams["red"] + teams["blue"]) > 1
@minqlx.delay(5)
def handle_game_countdown(self):
if self.game.type_short in ["ffa", "race"]: return
# Make sure teams have even amount of players
self.balance_before_start(0, True)
# If autoshuffle is off, return
if not int(self.get_cvar("qlx_mybalance_autoshuffle")): return
# Do the autoshuffle
self.center_print("*autoshuffle*")
self.msg("^7Autoshuffle...")
self.shuffle()
if 'balance' in minqlx.Plugin._loaded_plugins:
self.msg("^7Balancing on skill ratings...")
b = minqlx.Plugin._loaded_plugins['balance']
teams = self.teams()
players = dict([(p.steam_id, self.game.type_short) for p in teams["red"] + teams["blue"]])
b.add_request(players, b.callback_balance, minqlx.CHAT_CHANNEL)
else:
self.msg("^7Couldn't balance on skill, make sure ^6balance^7 is loaded.")
def handle_player_connect(self, player):
# If they joined very very very recently (like a short block from other plugins)
if player.steam_id in self.jointimes:
if (time.time() - self.jointimes[player.steam_id]) < 5: # dunno why 5s but should be enough
return
# Record their join times regardless
self.jointimes[player.steam_id] = time.time()
# If you are not an exception (or have high enough perm lvl);
# you must be checked for elo limit
if not (player.steam_id in self.exceptions or self.db.has_permission(player, self.get_cvar("qlx_mybalance_perm_allowed", int))):
# If we don't want to block, just look up his skill rating for a kick
if not int(self.get_cvar("qlx_elo_block_connecters")):
self.fetch(player, self.game.type_short, self.callback)
return
# If want to block, check for a lookup thread. Else create one
if not player.steam_id in self.connectthreads:
ct = ConnectThread(self, player)
self.connectthreads[player.steam_id] = ct
ct.start()
self.remove_thread(player.steam_id) # remove it after a while
# Check if thread is ready or not
ct = self.connectthreads[player.steam_id]
if ct.isAlive():
return "Fetching your skill rating..."
try:
res = ct._result
if not res: return "Fetching your skill rating..."
if res.status_code != requests.codes.ok: raise
js = res.json()
gt = self.game.type_short
if "players" not in js: raise
for p in js["players"]:
if int(p["steamid"]) == player.steam_id:
# Evaluate if their skill rating is not allowed on server
_elo, _games = [p[gt]['elo'], p[gt]['games']] if gt in p else [0,0]
eval_elo = self.evaluate_elo_games(player, _elo, _games )
# If it's too high, but it is close enough to the limit, start kickthread
if eval_elo and eval_elo[0] == "high" and (eval_elo[1] - self.ELO_MAX) <= self.get_cvar("qlx_elo_close_enough",int):
self.msg("^7Connecting player ({}^7)'s glicko ^6{}^7 is too high, but maybe close enough for a ^2!nokick ^7?".format(player.name, eval_elo[1]))
self.kicked[player.steam_id] = [player.name, eval_elo[1]]
self.help_start_kickthread(player, eval_elo[1], eval_elo[0])
# If it's too low, but close enough to the limit, start kickthread
elif eval_elo and eval_elo[0] == "low" and (self.ELO_MIN - eval_elo[1]) <= self.get_cvar("qlx_elo_close_enough",int):
self.kicked[player.steam_id] = [player.name, eval_elo[1]]
self.msg("^7Connecting player ({}^7)'s glicko ^6{}^7 is too low, but maybe close enough for a ^2!nokick ^7?".format(player.name, eval_elo[1]))
self.help_start_kickthread(player, eval_elo[1], eval_elo[0])
# If it's still not allowed, block connection
elif eval_elo:
return "^1Sorry, but your skill rating {} is too {}!".format(eval_elo[1], eval_elo[0])
# If the player was found, he will have been blocked or fetched
return
# If the player we want was not returned, and we are strict, block him
if self.get_cvar("qlx_mybalance_exclude",int):
return "This server requires a minimum of {} {} games".format(self.GAMES_NEEDED, self.game.type_short.upper())
except Exception as e:
minqlx.console_command("echo MybalanceError: {}".format(e))
def handle_player_disconnect(self, player, reason):
if player.steam_id in self.jointimes:
del self.jointimes[player.steam_id]
new_kickthreads = []
for kt in self.kickthreads:
if kt[0] != player.steam_id:
new_kickthreads.append(kt)
else:
try:
thread = kt[2]
thread.stop()
except:
pass
self.kickthreads = new_kickthreads
if self.game_active and player.team != "spectator" and self.game.type_short in ["ctf", "tdm"]:
self.balance_before_start(self.game.type_short, True)
def handle_team_switch(self, player, old, new):
if new in ['red', 'blue', 'free']:
if player.steam_id in self.kicked:
player.put("spectator")
if self.get_cvar("qlx_elo_kick") == "1":
kickmsg = "so you'll be kicked shortly..."
else:
kickmsg = "but you are free to keep watching."
player.tell("^6You do not meet the skill rating requirements to play on this server, {}".format(kickmsg))
player.center_print("^6You do not meet the skill rating requirements to play on this server, {}".format(kickmsg))
return
# If the game mode has no rounds, and a player joins, set a timer
if self.game_active and self.game.type_short in ["ctf", "tdm"]:
teams = self.teams()
# If someone joins, check if teams are even
if new in ['red', 'blue']:
if len(teams['red']) != len(teams['blue']):
self.msg("^7If teams will remain uneven for ^6{}^7 seconds, {} will be put to spec.".format(self.get_cvar("qlx_mybalance_uneven_time", int), player.name))
self.ctfplayer = player
self.evaluate_team_balance(player)
else:
# If teams are even now, it's all good.
self.ctfplayer = None
else:
# If someone goes to spec, check later if they are still uneven
self.ctfplayer = None # stop watching anyone
if len(teams['red']) != len(teams['blue']):
self.msg("^7Uneven teams detected! If teams are still uneven in {} seconds, I will spec someone.".format(self.get_cvar("qlx_mybalance_uneven_time")))
if not self.checking_balance:
self.checking_balance = True
self.evaluate_team_balance()
@minqlx.thread
def evaluate_team_balance(self, player=None):
@minqlx.next_frame
def setpos(_p, _x, _y, _z):
_p.position(x=_x, y=_y, z=_z)
_p.velocity(reset=True)
@minqlx.next_frame
def cprint(_p, _m):
if _p: _p.center_print(_m)
if not self.game_active: return
pos = None
if player: pos = list(player.position())
cvar = float(self.get_cvar("qlx_mybalance_uneven_time", int))
while (cvar > 0):
if not self.game_active: return
# If there was a player to watch given, see if he is still extra
if player:
if self.ctfplayer:
if(self.ctfplayer.steam_id != player.steam_id):
return # different guy? return without doing anything
else:
return # If there is a player but he is not tagged; return
setpos(player, pos[0], pos[1], pos[2])
if cvar.is_integer():
cprint(player, "^7Teams are uneven. ^6{}^7s until spec!".format(int(cvar)))
time.sleep(0.1)
cvar -= 0.1
# Time's up; time to check the teams
self.checking_balance = False
self.balance_before_start(self.game.type_short, True)
@minqlx.thread
def balance_before_start(self, roundnumber, direct=False):
@minqlx.next_frame # Game logic should never be done in a thread directly
def game_logic(func): func()
@minqlx.next_frame
def slay_player(p): p.health = 0 # assignment wasnt allowed in lambda
# Calculate the difference between teams (optional excluded teams argument)
def red_min_blue(t = False):
if not t: t = self.teams()
return len(t['red']) - len(t['blue'])
# Return a copy of the teams without the given player
def exclude_player(p):
t = self.teams().copy()
if p in t['red']: t['red'].remove(p)
if p in t['blue']: t['blue'].remove(p)
return t
# Wait until round almost starts
countdown = int(self.get_cvar('g_roundWarmupDelay'))
if self.game.type_short == "ft":
countdown = int(self.get_cvar('g_freezeRoundDelay'))
if not direct: time.sleep(max(countdown / 1000 - 0.8, 0))
# Grab the teams
teams = self.teams()
player_count = len(teams["red"] + teams["blue"])
# If it is the last player, don't do this and let the game finish normally
# OR if there is no match going on
if player_count == 1 or not self.game_active:
return
# Double check to not do anything you don't have to
if self.game.type_short == "ca":
if self.game.roundlimit in [self.game.blue_score, self.game.red_score]:
return
if self.game.type_short == "tdm":
if self.game.fraglimit in [self.game.blue_score, self.game.red_score]:
return
if self.game.type_short == "ctf":
if self.game.capturelimit in [self.game.blue_score, self.game.red_score]:
return
# If the last person is prevented or ignored to spec, we need to exclude him to balance the rest.
excluded_teams = False
# While there is a difference in teams of more than 1
while abs(red_min_blue(excluded_teams)) >= 1:
last = self.algo_get_last(excluded_teams)
diff = red_min_blue(excluded_teams)
if not last:
#self.msg("^1Mybalance couldn't retrieve the last player. Please consult error logs.")
minqlx.console_command("echo Error: Trying to balance before round {} start. Red({}) - Blue({}) players".format(roundnumber, len(teams['red']), len(teams['blue'])))
return
if self.is_even(diff): # one team has an even amount of people more than the other
to, fr = ['blue','red'] if diff > 0 else ['red', 'blue']
game_logic(lambda: last.put(to))
self.msg("^6Uneven teams action^7: Moved {} from {} to {}".format(last.name, fr, to))
else: # there is an odd number of players, then one will have to spec
if self.prevent or self.last_action == "ignore":
excluded_teams = exclude_player(last)
self.msg("^6Uneven teams^7: {} will not be moved to spec".format(last.name))
elif self.last_action == "slay":
if 'anti_rape' in minqlx.Plugin._loaded_plugins:
game_logic(lambda: last.put("spectator"))
self.msg("^6Uneven teams action^7: {} was moved to spec to even teams!".format(last.name))
minqlx.console_command("echo Not slayed because anti_rape plugin is loaded.")
else:
slay_player(last)
self.msg("{} ^7has been ^1slain ^7to even the teams!")
else:
self.msg("^6Uneven teams action^7: {} was moved to spec to even teams!".format(last.name))
game_logic(lambda: last.put("spectator"))
time.sleep(0.2)
def cmd_last_action(self, player, msg, channel):
if len(msg) < 2:
if self.last_action == 'slay' and 'anti_rape' in minqlx.Plugin._loaded_plugins:
return channel.reply("^7The current action is ^6slay^7, but will ^6spec^7 since ^6anti_rape^7 is active.")
return channel.reply("^7The current action when teams are uneven is: ^6{}^7.".format(self.last_action))
if msg[1] not in ["slay", "spec", "ignore"]:
return minqlx.RET_USAGE
self.last_action = msg[1]
if self.last_action == 'slay' and 'anti_rape' in minqlx.Plugin._loaded_plugins:
return channel.reply("^7Action has been set to ^6slay^7, but will ^6spec^7 because ^6anti_rape^7 is loaded.")
channel.reply("^7Action has been succesfully changed to: ^6{}^7.".format(msg[1]))
# At the end of a round, prevent is reset back to false.
# This gives us 10 seconds to prevent slaying before the
# next round starts
def handle_round_end(self, data):
self.prevent = False
def handle_round_count(self, round_number):
def red_min_blue():
t = self.teams()
return len(t['red']) - len(t['blue'])
# Grab the teams
teams = self.teams()
player_count = len(teams["red"] + teams["blue"])
# If it is the last player, don't do this and let the game finish normally
if player_count == 1:
return
# If there is a difference in teams of more than 1
diff = red_min_blue()
to, fr = ['blue', 'red'] if diff > 0 else ['red','blue']
n = int(abs(diff) / 2)
if abs(diff) >= 1:
last = self.algo_get_last()
if not last:
self.msg("^7No last person could be predicted in round countdown from teams:\nRed:{}\nBlue:{}".format(teams['red'], teams['blue']))
elif self.is_even(diff):
n = last.name if n == 1 else "{} players".format(n)
self.msg("^6Uneven teams detected!^7 At round start i'll move {} to {}".format(n, to))
else:
m = 'lowest player' if n == 1 else '{} lowest players'.format(n)
m = " and move the {} to {}".format(m, to) if n else ''
self.msg("^6Uneven teams detected!^7 Server will auto spec {}{}.".format(last.name, m))
self.balance_before_start(round_number)
# Normally the teams have already been balanced so players are switched in time,
# but check it again to make sure the round starts even
def handle_round_start(self, round_number):
self.balance_before_start(round_number, True)
# If there is no round delay, then round_count hasnt been called.
## if self.game.type_short == "ft":
## if not int(self.get_cvar('g_freezeRoundDelay')):
## self.balance_before_start(round_number, True)
## else:
## if not int(self.get_cvar('g_roundWarmupDelay')):
## self.balance_before_start(round_number, True)
def handle_game_start(self, data):
self.game_active = True
# There are no rounds?? Check it yourself then, pronto!
if self.game.type_short in ["ctf", "tdm"]:
self.balance_before_start(self.game.type_short, True)
def handle_game_end(self, data):
self.game_active = False
def handle_map(self, mapname, factory):
self.game_active = False
def cmd_prevent_last(self, player, msg, channel):
"""A command to prevent the last player on a team being kicked if
teams are magically balanced """
self.prevent = True
channel.reply("^7You will prevent the last player to be acted on at the start of next round.")
def cmd_setrating(self, player, msg, channel):
if len(msg) < 3:
return minqlx.RET_USAGE
try:
sid = int(msg[1])
assert len(msg[1]) == 17
name = sid