diff --git a/README.md b/README.md
index cbddf267..1a22202e 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,11 @@
# Killing Floor 2 Magicked Administrator
-Scripted management, statistics, and bot for ranked Killing Floor 2 servers.
+Scripted management, statistics, and bot for ranked Killing Floor 2 servers. Provides in-game commands, player stat tracking and ranking, live MOTD scoreboard and stats, greeter, and admin functions. Running entirely through the web administrator, it does not affect a server's ranked/custom status. It can be ran either directly on the server or remotely, and manage multiple servers at once.
### Player commands
+* !me - display a summary of your stats
+* !stats _player_ - display a summary of _player_'s stats
* !help - displays the help text in chat
+* !info - displays information about this project
* !dosh - display the players recorded dosh and rank by dosh
* !top\_dosh - displays the players with the highest recorded dosh
* !kills - display the players recorded kills and rank by kills
@@ -11,34 +14,41 @@ Scripted management, statistics, and bot for ranked Killing Floor 2 servers.
* !server\_dosh - displays total dosh earned on the server
### Admin commands
-* !difficulty {normal|hard|suicidal|hell} - sets difficulty of next game Example: !difficulty hard
-* !length {short|medium|long} - sets length of next game Example: !length medium
-* !start\_tc n command - repeat command every n seconds Example: !start\_tc 5 say test
+* !difficulty {normal|hard|suicidal|hell} - sets difficulty of next game
+ - Example: !difficulty hard
+* !length {short|medium|long} - sets length of next game
+ - Example: !length medium
+* !start\_tc _n_ _command_ - repeat _command_ every _n_ seconds
+ - Example: !start\_tc 5 say test
* !stop\_tc - stop all timed commands
-* !start\_wc n command - run command when wave n is reached. Example: !start\_wc say Wave Started. - This posts a message at EVERY wave start. Example: !start\_wc 4 say Wave 4 Started. - This posts a message when wave 4 starts.
+* !start\_wc _n command_ - run _command_ when wave _n_ is reached.
+ - Example: !start\_wc say Wave Started. - posts a message EVERY wave.
+ - Example: !start\_wc 4 say Wave 4 Started. - This posts a message when wave 4 starts.
* !stop\_wc - stop all wave commands
-* !start\_trc command - run command every time the trader opens Example: !start\_trc say Traders open.
+* !start\_trc _command_ - run _command_ every time the trader opens
+ - Example: !start\_trc say Traders open.
* !stop\_trc - stop trader commands
-* !say mesg - display mesg, for use in conjuction with other admin commands Example: !say This is an example.
+* !say _mesg_ - display _mesg_ in the chat, generally for use in conjuction with other commands
+ - Example: !say This is an example.
* !silent - toggles output in chat
-* !restart - immidiately restarts the current map
-* !toggle\_pass - enables or disables the configured game password (the password you entered in your config)
+* !restart - immediately restarts the current map
+* !load_map _map_name_ - immediately loads _map_name_
+* !toggle\_pass - toggles the configured game password (specified in `magicked_admin.conf`)
### Other features
-* Writing a server_name.motd file with pairs of %PLR and %SCR and enabling the motd_scoreboard option will put a live scoreboard in the motd.
-* Writting a server_name.init with a list of commands will run the commands when the bot starts on server_name
+* Writing a `server_name.motd` file with pairs of `%PLR` and `%SCR` and enabling the motd_scoreboard option will put a live scoreboard in the motd.
+ - `%SRV_D` and `%SRV_K` will be replaced by the total dosh and kills on the server respectively.
+* Writting a `server_name.init` with a list of commands will run the commands when the bot starts on server_name
## Dependancies/building
* Python 3.4+
* cx_freeze
* requests
* lxml
-* configparser
-* sqlite3
* colorama
* termcolor
-build by running the provided scripts `build.bat` or `build.sh` after installing dependancies via pip. Alternatively get a build from the releases page.
+build by running the provided scripts `build.bat` or `build.sh` after installing dependancies via pip.
## Minimal configuration
If running from source you will need to copy the example configs from the `config` folder to the root folder, the build scripts do this automatically.
diff --git a/magicked_admin/chatbot/chatbot.py b/magicked_admin/chatbot/chatbot.py
index 00767eb6..915bab3c 100644
--- a/magicked_admin/chatbot/chatbot.py
+++ b/magicked_admin/chatbot/chatbot.py
@@ -1,19 +1,24 @@
from server.chat.listener import Listener
from chatbot.commands.command_map import CommandMap
+from chatbot.commands.event_commands import CommandGreeter
+
+import logging
+import sys
-import time
-import threading
-import server
from os import path
#from FuzzyWuzzy import Fuzz
#from FuzzyWuzzy import process
-from utils.text import trim_string, millify
+logger = logging.getLogger(__name__)
+if __debug__ and not hasattr(sys, 'frozen'):
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
class Chatbot(Listener):
- def __init__(self, server):
+ def __init__(self, server, greeter_enabled=True):
self.server = server
self.chat = server.chat
# The in-game chat can fit 21 Ws horizontally
@@ -22,11 +27,12 @@ def __init__(self, server):
self.commands = CommandMap(server, self)
self.silent = False
+ self.greeter_enabled = True
if path.exists(server.name + ".init"):
self.execute_script(server.name + ".init")
- print("INFO: Bot on server " + server.name + " initialised")
+ logger.debug("Bot on server " + server.name + " initialised")
def receive_message(self, username, message, admin=False):
if message[0] == '!':
@@ -38,22 +44,17 @@ def command_handler(self, username, args, admin=False):
if args is None or len(args) == 0:
return
- ''' Put FuzzyWuzzy Here? You said that it might be handy elsewhere, not sure what you want to do with it.
- choices = ['restart','toggle_pass','silent','length','difficulty','players','game','help','info','kills',
- 'dosh','top_kills','total_kills','top_dosh','me','stats']
- match = process.extractOne(args, choices, scorer= fuzz.ratio, scorecutoff= 90)'''
-
if args[0].lower() in self.commands.command_map:
command = self.commands.command_map[args[0].lower()]
+ if not self.greeter_enabled and isinstance(command, CommandGreeter):
+ return
response = command.execute(username, args, admin)
if not self.silent:
self.chat.submit_message(response)
- # What would be the best way of handling CD commands?
- elif username != "server" and not self.silent:
- self.chat.submit_message("Sorry, I didn't understand that request.")
def execute_script(self, file_name):
- print("INFO: Executing script: " + file_name)
+ logger.debug("Executing script: " + file_name)
+ print("Executing script: " + file_name)
with open(file_name) as script:
for line in script:
print("\t\t" + line.strip())
diff --git a/magicked_admin/chatbot/commands/command_map.py b/magicked_admin/chatbot/commands/command_map.py
index 8aa4f1ef..edacde56 100644
--- a/magicked_admin/chatbot/commands/command_map.py
+++ b/magicked_admin/chatbot/commands/command_map.py
@@ -14,8 +14,11 @@ def generate_map(self):
wave_event_manager = CommandOnWaveManager(self.server, self.chatbot)
trader_event_manager = CommandOnTraderManager(self.server, self.chatbot)
time_event_manager = CommandOnTimeManager(self.server, self.chatbot)
+ greeter = CommandGreeter(self.server)
command_map = {
+ 'new_game': greeter,
+ 'player_join': greeter,
'stop_wc': wave_event_manager,
'start_wc': wave_event_manager,
'new_wave': wave_event_manager,
@@ -27,6 +30,7 @@ def generate_map(self):
't_open': trader_event_manager,
'say': CommandSay(self.server),
'restart': CommandRestart(self.server),
+ 'load_map': CommandLoadMap(self.server),
'toggle_pass': CommandTogglePassword(self.server),
'silent': CommandSilent(self.server, self.chatbot),
'length': CommandLength(self.server),
diff --git a/magicked_admin/chatbot/commands/event_commands.py b/magicked_admin/chatbot/commands/event_commands.py
index 20d9997f..42208485 100644
--- a/magicked_admin/chatbot/commands/event_commands.py
+++ b/magicked_admin/chatbot/commands/event_commands.py
@@ -1,9 +1,74 @@
from chatbot.commands.command import Command
+from utils.text import millify
+from utils.time import seconds_to_hhmmss
-import time
import threading
+import logging
+import sys
+import datetime
-ALL_WAVES = 99
+ALL_WAVES = 999
+
+logger = logging.getLogger(__name__)
+if __debug__ and not hasattr(sys, 'frozen'):
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
+
+
+class CommandGreeter(Command):
+ def __init__(self, server, admin_only=True):
+ Command.__init__(self, server, admin_only)
+
+ self.new_game_grace = 35
+ self.new_game_time = datetime.datetime.now()
+
+ def execute(self, username, args, admin):
+ if not self.authorise(admin):
+ return self.not_auth_message
+
+ if args[0] == "new_game":
+ logger.debug("Greeter received new game event")
+ self.new_game_time = datetime.datetime.now()
+ return None
+ now = datetime.datetime.now()
+ elapsed_time = now - self.new_game_time
+ seconds = elapsed_time.total_seconds()
+
+ if seconds < self.new_game_grace:
+ logger.debug("Skipping welcome {}, new_game happened recently ({})"
+ " [{}/{}]"
+ .format(username, self.server.name, seconds,
+ self.new_game_grace))
+ return None
+
+ if len(args) < 2:
+ return "Missing argument (username)"
+
+ requested_username = " ".join(args[1:])
+
+ player = self.server.get_player(requested_username)
+ if not player:
+ logger.debug("DEBUG: Bad player join command (not found) [{}]"
+ .format(requested_username))
+ return "Couldn't greet player {}.".format(requested_username)
+
+ if player.total_logins > 1:
+ pos_kills = self.server.database.rank_kills(requested_username)
+ pos_dosh = self.server.database.rank_dosh(requested_username)
+ return "\nWelcome back {}.\n" \
+ "You've killed {} zeds (#{}) and \n" \
+ "earned £{} (#{}) \nover {} sessions " \
+ "({}).".format(player.username,
+ millify(player.total_kills),
+ pos_kills,
+ millify(player.total_dosh),
+ pos_dosh,
+ player.total_logins,
+ seconds_to_hhmmss(player.total_time))\
+ .encode("iso-8859-1", "ignore")
+ else:
+ return None
class CommandOnWave:
diff --git a/magicked_admin/chatbot/commands/info_commands.py b/magicked_admin/chatbot/commands/info_commands.py
index 986fec87..08cbc274 100644
--- a/magicked_admin/chatbot/commands/info_commands.py
+++ b/magicked_admin/chatbot/commands/info_commands.py
@@ -1,6 +1,7 @@
from chatbot.commands.command import Command
from server.player import Player
from utils.time import seconds_to_hhmmss
+from utils.text import millify
import datetime
@@ -91,16 +92,13 @@ def execute(self, username, args, admin):
time = seconds_to_hhmmss(
player.total_time + session_time
)
- message = "Stats for " + player.username + ":\n" + \
- "Sessions:\t\t\t" + str(player.total_logins) + "\n" + \
- "Play time:\t\t" + time +"\n" + \
- "Deaths:\t\t\t" + str(player.total_deaths) + "\n" + \
- "Kills:\t\t\t\t" + str(player.total_kills) + "\n" + \
- "Dosh earned:\t\t" + str(player.total_dosh) + "\n" + \
- "Dosh spent:\t\t" + str(player.total_dosh_spent) + "\n" + \
- "Health lost:\t\t" + str(player.total_health_lost) + "\n" + \
- "Dosh this game:\t" + str(player.game_dosh) + "\n" + \
- "Kills this wave:\t\t" + str(player.wave_kills) + "\n" + \
- "Dosh this wave:\t" + str(player.wave_dosh)
+ message = "Stats for {}:\n".format(player.username) +\
+ "Total play time: {} ({} sessions)\n"\
+ .format(time, player.total_logins) +\
+ "Total deaths: {}\n".format(player.total_deaths) +\
+ "Total kills: {}\n".format(millify(player.total_kills)) +\
+ "Total dosh earned: {}\n"\
+ .format(millify(player.total_dosh)) +\
+ "Dosh this game: {}".format(millify(player.game_dosh))
return message
diff --git a/magicked_admin/chatbot/commands/player_commands.py b/magicked_admin/chatbot/commands/player_commands.py
index 67cee6e1..eade70fd 100644
--- a/magicked_admin/chatbot/commands/player_commands.py
+++ b/magicked_admin/chatbot/commands/player_commands.py
@@ -69,12 +69,18 @@ def execute(self, username, args, admin):
if not self.authorise(admin):
return self.not_auth_message
+ if len(args) > 1 and args[1] == '-w' and len(self.server.players) > 0:
+ self.server.players.sort(key=lambda player: player.wave_kills, reverse=True)
+ top_killer = self.server.players[0]
+ return "Player {} killed the most zeds this wave: {} zeds"\
+ .format(top_killer.username, top_killer.wave_kills)
+
self.server.write_all_players()
killers = self.server.database.top_kills()
if len(killers) < 5:
return "Not enough data."
# [row][col]
- return "\n\nTop 5 players by kills:\n"+ \
+ return "\n\nTop 5 players by total kills:\n" + \
"\t"+str(millify(killers[0][1])) + "\t-\t" + trim_string(killers[0][0],20) + "\n" + \
"\t"+str(millify(killers[1][1])) + "\t-\t" + trim_string(killers[1][0],20) + "\n" + \
"\t"+str(millify(killers[2][1])) + "\t-\t" + trim_string(killers[2][0],20) + "\n" + \
@@ -90,12 +96,19 @@ def execute(self, username, args, admin):
if not self.authorise(admin):
return self.not_auth_message
+ if len(args) > 1 and args[1] == '-w' and len(self.server.players) > 0:
+ self.server.players.sort(key=lambda player: player.wave_dosh, reverse=True)
+ top_dosh = self.server.players[0]
+ return "Player {} earned the most this wave: £{}"\
+ .format(top_dosh.username, millify(top_dosh.wave_dosh))\
+ .encode("iso-8859-1", "ignore")
+
self.server.write_all_players()
doshers = self.server.database.top_dosh()
if len(doshers) < 5:
return "Not enough data."
- message = "\n\nTop 5 players by earnings:\n"+ \
+ message = "\n\nTop 5 players by earnings:\n" + \
"\t£"+str(millify(doshers[0][1])) + "\t-\t" + trim_string(doshers[0][0],20) + "\n" + \
"\t£"+str(millify(doshers[1][1])) + "\t-\t" + trim_string(doshers[1][0],20) + "\n" + \
"\t£"+str(millify(doshers[2][1])) + "\t-\t" + trim_string(doshers[2][0],20) + "\n" + \
diff --git a/magicked_admin/chatbot/commands/server_commands.py b/magicked_admin/chatbot/commands/server_commands.py
index 015f55b8..476dd6db 100644
--- a/magicked_admin/chatbot/commands/server_commands.py
+++ b/magicked_admin/chatbot/commands/server_commands.py
@@ -31,6 +31,21 @@ def execute(self, username, args, admin):
return "Restarting map."
+class CommandLoadMap(Command):
+ def __init__(self, server, admin_only=True):
+ Command.__init__(self, server, admin_only)
+
+ def execute(self, username, args, admin):
+ if not self.authorise(admin):
+ return self.not_auth_message
+
+ if len(args) < 2:
+ return "Missing argument (map name)"
+
+ self.server.change_map(args[1])
+ return "Changing map."
+
+
class CommandTogglePassword(Command):
def __init__(self, server, admin_only=True):
Command.__init__(self, server, admin_only)
@@ -57,7 +72,7 @@ def execute(self, username, args, admin):
if self.chatbot.silent:
self.chatbot.silent = False
- return "Silent mode disabled."
+ return None
else:
self.chatbot.command_handler("server", "say Silent mode enabled.",
admin=True)
@@ -74,11 +89,11 @@ def execute(self, username, args, admin):
if len(args) < 2:
return "Length not recognised. Options are short, medium, or long."
- if args[1] == "short":
+ if args[1] in ["short", "0"]:
length = server.LEN_SHORT
- elif args[1] == "medium":
+ elif args[1] in ["medium", "med", "normal", "1"]:
length = server.LEN_NORM
- elif args[1] == "long":
+ elif args[1] in ["long", "2"]:
length = server.LEN_LONG
else:
return "Length not recognised. Options are short, medium, or long."
@@ -98,13 +113,13 @@ def execute(self, username, args, admin):
return "Difficulty not recognised. " + \
"Options are normal, hard, suicidal, or hell."
- if args[1] == "normal":
+ if args[1] in ["normal", "0"]:
difficulty = server.DIFF_NORM
- elif args[1] == "hard":
+ elif args[1] in ["hard", "1"]:
difficulty = server.DIFF_HARD
- elif args[1] == "suicidal":
+ elif args[1] in ["suicidal", "sui", "2"]:
difficulty = server.DIFF_SUI
- elif args[1] == "hell":
+ elif args[1] in ["hell", "hoe", "hellonearth", "3"]:
difficulty = server.DIFF_HOE
else:
return "Difficulty not recognised. " + \
diff --git a/magicked_admin/config/magicked_admin.conf.example b/magicked_admin/config/magicked_admin.conf.example
index 5d9b3819..bd0f1c3a 100644
--- a/magicked_admin/config/magicked_admin.conf.example
+++ b/magicked_admin/config/magicked_admin.conf.example
@@ -5,4 +5,5 @@ password = 123
game_password = game_password
motd_scoreboard = False
scoreboard_type = kills
+enable_greeter = True
diff --git a/magicked_admin/config/server_one.init.example b/magicked_admin/config/server_one.init.example
index d04160f2..091bc861 100644
--- a/magicked_admin/config/server_one.init.example
+++ b/magicked_admin/config/server_one.init.example
@@ -1 +1,4 @@
+silent
start_wc 1 say I'm a bot, type !help for usage
+start_trc top_dosh -w
+silent
diff --git a/magicked_admin/config/server_one.motd.example b/magicked_admin/config/server_one.motd.example
index 8625637c..895c14a3 100644
--- a/magicked_admin/config/server_one.motd.example
+++ b/magicked_admin/config/server_one.motd.example
@@ -1,5 +1,7 @@
Welcome to our server.
+%SRV_K Zeds killed on this server.
+
Top Players (total dosh):
1. %PLR [%SCR] 2. %PLR [%SCR] 3. %PLR [%SCR]
4. %PLR [%SCR] 5. %PLR [%SCR] 6. %PLR [%SCR]
diff --git a/magicked_admin/database/database.py b/magicked_admin/database/database.py
index 24009352..3071f486 100644
--- a/magicked_admin/database/database.py
+++ b/magicked_admin/database/database.py
@@ -1,6 +1,14 @@
import sqlite3
import datetime
+import logging
from os import path
+import sys
+
+logger = logging.getLogger(__name__)
+if __debug__ and not hasattr(sys, 'frozen'):
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
class ServerDatabase:
@@ -13,10 +21,11 @@ def __init__(self, name):
check_same_thread=False)
self.cur = self.conn.cursor()
- print("INFO: Database for " + name + " initialised")
+ logger.debug("Database for " + name + " initialised")
def build_schema(self):
- print("INFO: Building fresh schema...")
+ print("Building new database...")
+ logger.debug("Building new database...")
conn = sqlite3.connect(self.sqlite_db_file)
cur = conn.cursor()
@@ -27,14 +36,33 @@ def build_schema(self):
conn.commit()
conn.close()
- def start_session(self, player):
- pass
+ def rank_kills(self, username):
+ query = "select p1.*"\
+ ",("" \
+ ""select count(*)" \
+ "from players as p2"" \
+ ""where p2.kills > p1.kills"\
+ ") as kill_rank"" \
+ ""from players as p1"" \
+ ""where p1.username = ?"
+ self.cur.execute(query, (username,))
+ all_rows = self.cur.fetchall()
- def end_session(self, player):
- pass
+ return all_rows[0][-1] + 1
+
+ def rank_dosh(self, username):
+ query = "select p1.*"\
+ ",("" \
+ ""select count(*)" \
+ "from players as p2"" \
+ ""where p2.dosh > p1.dosh"\
+ ") as kill_rank"" \
+ ""from players as p1"" \
+ ""where p1.username = ?"
+ self.cur.execute(query, (username,))
+ all_rows = self.cur.fetchall()
- def end_game(self, game):
- pass
+ return all_rows[0][-1] + 1
# SUM(dosh_spent) Add in later.
def server_dosh(self):
diff --git a/magicked_admin/main.py b/magicked_admin/main.py
index fe117305..309e6fae 100644
--- a/magicked_admin/main.py
+++ b/magicked_admin/main.py
@@ -4,23 +4,36 @@
from utils.text import str_to_bool
import configparser
+import logging
import sys
import signal
import os
-DEBUG = True
+from colorama import init
+init()
+
+logging.basicConfig(stream=sys.stdout)
+
+logger = logging.getLogger(__name__)
+if __debug__ and not hasattr(sys, 'frozen'):
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
if not os.path.exists("./magicked_admin.conf"):
- sys.exit("Configuration file not found.")
+ logger.error("Configuration file not found")
+ input("Press enter to exit...")
+ sys.exit()
+
config = configparser.ConfigParser()
config.read("./magicked_admin.conf")
-class MagickedAdministrator():
+
+class MagickedAdministrator:
def __init__(self):
self.servers = []
self.chatbots = []
- self.watchdogs = []
self.motd_updaters = []
signal.signal(signal.SIGINT, self.terminate)
@@ -36,6 +49,9 @@ def run(self):
config[server_name]["motd_scoreboard"]
)
scoreboard_type = config[server_name]["scoreboard_type"]
+ enable_greeter = str_to_bool(
+ config[server_name]["enable_greeter"]
+ )
server = Server(server_name, address, user, password,
game_password)
@@ -46,21 +62,18 @@ def run(self):
motd_updater.start()
self.motd_updaters.append(motd_updater)
- cb = Chatbot(server)
+ cb = Chatbot(server, greeter_enabled=enable_greeter)
server.chat.add_listener(cb)
self.chatbots.append(cb)
- print("INFO: Initialisation complete\n")
+ print("Initialisation complete")
def terminate(self, signal, frame):
- print("\nINFO: Terminating...")
+ print("Terminating, saving data...")
for server in self.servers:
- server.terminate()
+ server.write_all_players(final=True)
- for wd in self.watchdogs:
- wd.terminate()
- for motd_updater in self.motd_updaters:
- motd_updater.terminate()
+ sys.exit()
if __name__ == "__main__":
diff --git a/magicked_admin/server/chat/chat.py b/magicked_admin/server/chat/chat.py
index 5e80d5ef..32f8c012 100644
--- a/magicked_admin/server/chat/chat.py
+++ b/magicked_admin/server/chat/chat.py
@@ -1,11 +1,16 @@
import threading
import requests
+import time
+import logging
from lxml import html
-from colorama import init
from termcolor import colored
+import sys
-# what the fuck does this do
-init()
+logger = logging.getLogger(__name__)
+if __debug__ and not hasattr(sys, 'frozen'):
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
class ChatLogger(threading.Thread):
@@ -23,23 +28,23 @@ def __init__(self, server):
self.poll_session = server.new_session()
- self.exit_flag = threading.Event()
-
self.print_messages = True
self.silent = False
threading.Thread.__init__(self)
def run(self):
- while not self.exit_flag.wait(self.time_interval):
+ while True:
try:
response = self.poll_session.post(
self.chat_request_url,
self.chat_request_payload,
timeout=2
)
- except requests.exceptions.RequestException as e:
- print("INFO: Couldn't retrieve chat (RequestException)")
+ except requests.exceptions.RequestException:
+ logger.debug("Couldn't retrieve chat (RequestException) ({})"
+ .format(self.server.name))
+ time.sleep(self.time_interval + 3)
continue
if response.text:
@@ -48,22 +53,27 @@ def run(self):
for message_html in messages_html:
message_tree = html.fromstring(message_html)
- try:
- # xpath returns a list but theres only ever one of each because i split earlier
- username = message_tree.xpath('//span[starts-with(@class,\'username\')]/text()')[0]
- user_type = message_tree.xpath('//span[starts-with(@class,\'username\')]/@class')[0]
- message = message_tree.xpath('//span[@class="message"]/text()')[0]
- admin = True if "admin" in user_type else False
-
- self.handle_message(username, message, admin)
- except IndexError:
- # Messages without usernames are not handled correctly. In particular, Controlled Difficulty
- # This is basic support for Controlled Difficulty messages. It may need to be expanded to support
- # other mutators and mods however.
- username = "Controlled Difficulty"
- admin = False
- message = message_tree.xpath('//span[@class="message"]/text()')[0]
- self.handle_message(username, message, admin)
+ # xpath returns a list but theres only ever one of each because i split earlier
+ username_arr = message_tree.xpath('//span[starts-with(@class,\'username\')]/text()')
+ if len(username_arr) < 1:
+ logger.debug("Message without username '{}' ({})"
+ .format(message, self.server.name))
+ continue
+ username = username_arr[0]
+
+ user_type_arr = message_tree.xpath('//span[starts-with(@class,\'username\')]/@class')
+ if len(user_type_arr) < 1:
+ logger.debug("Message without user type '{}' ({})"
+ .format(message, self.server.name))
+ continue
+ user_type = user_type_arr[0]
+
+ message = message_tree.xpath('//span[@class="message"]/text()')[0]
+ admin = True if "admin" in user_type else False
+
+ self.handle_message(username, message, admin)
+
+ time.sleep(self.time_interval)
def handle_message(self, username, message, admin):
@@ -74,8 +84,6 @@ def handle_message(self, username, message, admin):
if command:
print_line = colored(print_line, 'green')
- elif username == "Controlled Difficulty":
- print_line = colored(print_line, 'cyan')
else:
print_line = colored(print_line, 'yellow')
print(print_line)
@@ -102,8 +110,5 @@ def submit_message(self, message):
try:
self.server.session.post(chat_submit_url, message_payload)
- except requests.exceptions.RequestException as e:
- print("INFO: Couldn't submit message (RequestException)")
-
- def terminate(self):
- self.exit_flag.set()
+ except requests.exceptions.RequestException:
+ logger.debug("Couldn't submit message (RequestException)")
diff --git a/magicked_admin/server/managers/motd_updater.py b/magicked_admin/server/managers/motd_updater.py
index f7963851..16603d15 100644
--- a/magicked_admin/server/managers/motd_updater.py
+++ b/magicked_admin/server/managers/motd_updater.py
@@ -1,11 +1,20 @@
from os import path
import threading
import requests
+import time
+import logging
+import sys
from lxml import html
from utils.text import millify
from utils.text import trim_string
+logger = logging.getLogger(__name__)
+if __debug__ and not hasattr(sys, 'frozen'):
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
+
class MotdUpdater(threading.Thread):
@@ -16,12 +25,10 @@ def __init__(self, server, scoreboard_type):
self.time_interval = 5 * 60
self.motd = self.load_motd()
- self.exit_flag = threading.Event()
-
threading.Thread.__init__(self)
def run(self):
- while not self.exit_flag.wait(self.time_interval):
+ while True:
self.server.write_all_players()
try:
motd_payload = self.get_configuration()
@@ -36,21 +43,24 @@ def run(self):
except requests.exceptions.RequestException:
continue
+ time.sleep(self.time_interval)
+
def submit_motd(self, payload):
motd_url = "http://" + self.server.address + \
"/ServerAdmin/settings/welcome"
- print("INFO: Updating MOTD")
+ logger.debug("Updating MOTD ({})".format(self.server.name))
try:
self.server.session.post(motd_url, data=payload)
self.server.save_settings()
except requests.exceptions.RequestException:
- print("INFO: Couldn't submit motd (RequestException)")
+ logger.warning("Couldn't submit motd (RequestException) to {}"
+ .format(self.server.name))
raise
def load_motd(self):
if not path.exists(self.server.name + ".motd"):
- print("WARNING: No motd file for " + self.server.name)
+ logger.warning("No motd file for " + self.server.name)
return ""
motd_f = open(self.server.name + ".motd")
@@ -64,8 +74,9 @@ def render_motd(self, src_motd):
elif self.scoreboard_type in ['Dosh','dosh']:
scores = self.server.database.top_dosh()
else:
- print("ERROR: Bad configuration, scoreboard_type. " +
- "Options are: dosh, kills")
+ logger.error("Bad configuration, scoreboard_type. "
+ "Options are: dosh, kills ({})"
+ .format(self.server.name))
return
for player in scores:
@@ -93,7 +104,7 @@ def get_configuration(self):
try:
motd_response = self.server.session.get(motd_url, timeout=2)
except requests.exceptions.RequestException as e:
- print("INFO: Couldn't get motd config(RequestException)")
+ logger.debug("Couldn't get motd config(RequestException)")
raise
motd_tree = html.fromstring(motd_response.content)
@@ -112,5 +123,3 @@ def get_configuration(self):
'action': 'save'
}
- def terminate(self):
- self.exit_flag.set()
diff --git a/magicked_admin/server/managers/server_mapper.py b/magicked_admin/server/managers/server_mapper.py
index d1a4654f..cedd95cc 100644
--- a/magicked_admin/server/managers/server_mapper.py
+++ b/magicked_admin/server/managers/server_mapper.py
@@ -1,12 +1,21 @@
import threading
import requests
import time
+import logging
+import sys
from lxml import html
from lxml.html.clean import Cleaner
+from termcolor import colored
from server.player import Player
+logger = logging.getLogger(__name__)
+if __debug__ and not hasattr(sys, 'frozen'):
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
+
class ServerMapper(threading.Thread):
def __init__(self, server):
@@ -14,156 +23,172 @@ def __init__(self, server):
self.time_interval = 6
self.last_wave = 0
- self.exit_flag = threading.Event()
threading.Thread.__init__(self)
- print("INFO: Mapper for " + server.name + " initialised")
+ logger.debug("Mapper for " + server.name + " initialised")
- def run(self):
+ def poll(self):
info_url = "http://" + self.server.address + \
"/ServerAdmin/current/info"
-
- while not self.exit_flag.wait(self.time_interval):
- try:
- info_page_response = self.server.session.post(info_url,
- timeout=2)
- except requests.exceptions.RequestException:
- print("INFO: Couldn't get info page (RequestException), "
- "sleeping for 5 seconds")
- time.sleep(5)
- continue
-
- # Look into this encoding, pages are encoded in Windows 1252.
- info_tree = html.fromstring(info_page_response.content
- .decode('cp1252'))
- dds = info_tree.xpath('//dd/text()')
-
- z, zr = info_tree.xpath('//dd[@class="gs_wave"]/text()')[0]\
- .split("/")
- z, zr = int(z), int(zr)
- if z == zr and z > 1:
- # The if ensures
- if not self.server.trader_time:
- self.server.trader_open()
+ try:
+ info_page_response = self.server.session.post(info_url,
+ timeout=2)
+ except requests.exceptions.RequestException:
+ logger.debug("Couldn't get info page (RequestException)"
+ " on {} sleeping for 5 seconds"
+ .format(self.server.name))
+ time.sleep(5)
+ return
+
+ info_tree = html.fromstring(info_page_response.content)
+
+ headings, players_table = self.get_current_players(info_tree)
+ self.update_players(headings, players_table)
+
+ self.update_game(info_tree)
+
+ def get_current_players(self, info_tree):
+ table_head_pat = '//table[@id="players"]//thead//tr//th'
+ # Some but not all headers have an for sorting columns
+ # that needs to be removed
+ cleaner = Cleaner()
+ cleaner.remove_tags = ['a']
+
+ headings = []
+ required_headings = {'Name', 'Perk', 'Dosh', 'Health',
+ 'Kills', 'Ping', 'Admin'}
+ for heading in info_tree.xpath(table_head_pat):
+ heading = cleaner.clean_html(heading)
+ headings += heading.xpath('//th/text()')
+
+ if not required_headings.issubset(set(headings)):
+ logger.error("Player is missing columns ({}) on {}"
+ .format(required_headings - set(headings),
+ self.server.name))
+
+ player_rows_pat = '//table[@id="players"]//tbody//tr'
+ player_rows_tree = info_tree.xpath(player_rows_pat)
+
+ players_table = []
+
+ for player_row in player_rows_tree:
+ values = []
+ for value in player_row:
+ if not value.text_content():
+ values += [None]
+ else:
+ values += [value.text_content()]
+
+ if values[0] == "There are no players":
+ logger.debug("No players on server {}"
+ .format(self.server.name))
+ elif len(values) != len(headings):
+ logger.warning("Player row ({}) length did not "
+ "match the table length on {}"
+ .format(player_row[headings.index("Name")],
+ self.server.name))
else:
- if self.server.trader_time:
- self.server.trader_close()
- self.server.zeds_killed = z
- self.server.zeds_wave = zr
-
- map_title = info_tree.xpath('//dl//dd/@title')[1]
- map_name = dds[0]
+ players_table += [values]
+
+ return (headings, players_table)
+
+ def update_players(self, headings, players_table):
+ # Remove players that have quit
+ for player in self.server.players:
+ if player.username not in \
+ [player_row[headings.index("Name")]
+ for player_row in players_table]:
+ self.server.player_quit(player)
+
+ for player_row in players_table:
+ username = player_row[headings.index("Name")]
+ new_perk = player_row[headings.index("Perk")]
+ if not new_perk:
+ new_perk = "N/A"
try:
- wave, length = dds[7].split("/")
- difficulty = dds[8]
- except ValueError:
- wave, length = dds[8].split("/")
- difficulty = dds[9]
-
- self.server.game['map_title'] = map_title
- self.server.game['map_name'] = map_name
- self.server.game['wave'] = wave
- self.server.game['length'] = length
- self.server.game['difficulty'] = difficulty
-
- if int(wave) < self.last_wave:
- self.server.new_game()
- elif int(wave) > self.last_wave:
- self.server.new_wave()
- self.last_wave = int(wave)
-
- table_head_pat = '//table[@id="players"]//thead//tr//th'
- # Some but not all headers have an for sorting columns
- # that needs to be removed
- cleaner = Cleaner()
- cleaner.remove_tags = ['a']
-
- headings = []
- required_headings = {'Name', 'Perk', 'Dosh', 'Health',
- 'Kills', 'Ping', 'Admin'}
- for heading in info_tree.xpath(table_head_pat):
- heading = cleaner.clean_html(heading)
- headings += heading.xpath('//th/text()')
-
- if not required_headings.issubset(set(headings)):
- print("ERROR: Missing player columns {}"
- .format(required_headings - set(headings)))
-
- player_rows_pat = '//table[@id="players"]//tbody//tr'
- player_rows_tree = info_tree.xpath(player_rows_pat)
-
- players_table = []
-
- for player_row in player_rows_tree:
- values = []
- for value in player_row:
- if not value.text_content():
- values += [None]
- else:
- values += [value.text_content()]
-
- if values[0] == "There are no players":
- print("DEBUG: No players")
- elif len(values) != len(headings):
- print("ERROR: A player row length did not" +
- "match the table length")
- else:
- players_table += [values]
-
- # Remove players that have quit
- for player in self.server.players:
- if player.username not in \
- [player_row[headings.index("Name")]
- for player_row in players_table]:
- self.server.player_quit(player)
-
- for player_row in players_table:
- username = player_row[headings.index("Name")]
- new_perk = player_row[headings.index("Perk")]
- if not new_perk:
- print("DEBUG: Null perk on: " + player_row[headings.index("Name")])
- new_perk = "N/A"
- try:
- new_health = int(player_row[headings.index("Health")])
- except TypeError:
- print("DEBUG: Null health on: " + player_row[headings.index("Name")])
- new_health = 0
- new_kills = int(player_row[headings.index("Kills")])
- new_ping = int(player_row[headings.index("Ping")])
- new_dosh = int(player_row[headings.index("Dosh")])
-
- player = self.server.get_player(username)
- # New players
- if player is None:
- player = Player(username, new_perk)
- player.kills = new_kills
- player.health = new_health
- player.dosh = new_dosh
- self.server.player_join(player)
- continue
-
- if new_health == 0 and \
- new_health < player.health and \
- new_kills > 0:
- print("INFO: Player " + player.username + " died")
- player.total_deaths += 1
-
- player.perk = new_perk
- player.total_kills += new_kills - player.kills
- player.wave_kills += new_kills - player.kills
- player.wave_dosh += new_dosh - player.dosh
+ new_health = int(player_row[headings.index("Health")])
+ except TypeError:
+ new_health = 0
+ new_kills = int(player_row[headings.index("Kills")])
+ new_ping = int(player_row[headings.index("Ping")])
+ new_dosh = int(player_row[headings.index("Dosh")])
+
+ player = self.server.get_player(username)
+ # If the player has just joined, inform the server and skip maths
+ if player is None:
+ player = Player(username, new_perk)
player.kills = new_kills
- if new_health < player.health:
- player.total_health_lost += player.health - new_health
player.health = new_health
- player.ping = new_ping
- if new_dosh > player.dosh:
- player.game_dosh += new_dosh - player.dosh
- player.total_dosh += new_dosh - player.dosh
-
- else:
- player.total_dosh_spent += player.dosh - new_dosh
player.dosh = new_dosh
+ self.server.player_join(player)
+ continue
+
+ # Players can also have 0 HP while in lobby, do additional checks
+ if new_health == 0 and \
+ new_health < player.health and \
+ new_kills > 0:
+ message = "Player {} died on {}"\
+ .format(player.username, self.server.name)
+ print(colored(message, 'red'))
+ player.total_deaths += 1
+
+ player.perk = new_perk
+ player.ping = new_ping
- def terminate(self):
- self.exit_flag.set()
+ player.total_kills += new_kills - player.kills
+ player.wave_kills += new_kills - player.kills
+ player.kills = new_kills
+
+ if new_dosh > player.dosh:
+ player.wave_dosh += new_dosh - player.dosh
+ player.game_dosh += new_dosh - player.dosh
+ player.total_dosh += new_dosh - player.dosh
+ else:
+ player.total_dosh_spent += player.dosh - new_dosh
+ player.dosh = new_dosh
+
+ if new_health < player.health:
+ player.total_health_lost += player.health - new_health
+ player.health = new_health
+
+ def update_game(self, info_tree):
+ dds = info_tree.xpath('//dd/text()')
+
+ z, zr = info_tree.xpath('//dd[@class="gs_wave"]/text()')[0] \
+ .split("/")
+ z, zr = int(z), int(zr)
+ if z == zr and z > 1:
+ # The if ensures trader_open is only sent once
+ if not self.server.trader_time:
+ self.server.trader_open()
+ else:
+ if self.server.trader_time:
+ self.server.trader_close()
+ self.server.zeds_killed = z
+ self.server.zeds_wave = zr
+
+ map_title = info_tree.xpath('//dl//dd/@title')[1]
+ map_name = dds[0]
+ try:
+ wave, length = dds[7].split("/")
+ difficulty = dds[8]
+ except ValueError:
+ wave, length = dds[8].split("/")
+ difficulty = dds[9]
+
+ self.server.game['map_title'] = map_title
+ self.server.game['map_name'] = map_name
+ self.server.game['wave'] = wave
+ self.server.game['length'] = length
+ self.server.game['difficulty'] = difficulty
+
+ if int(wave) < self.last_wave:
+ self.server.new_game()
+ elif int(wave) > self.last_wave:
+ self.server.new_wave()
+ self.last_wave = int(wave)
+
+ def run(self):
+ while True:
+ self.poll()
+ time.sleep(self.time_interval)
diff --git a/magicked_admin/server/server.py b/magicked_admin/server/server.py
index 04eea783..b89c7ce8 100644
--- a/magicked_admin/server/server.py
+++ b/magicked_admin/server/server.py
@@ -1,9 +1,11 @@
-from os import path
+import requests
+import sys
+import logging
-import requests, sys
from hashlib import sha1
from lxml import html
from time import sleep
+from termcolor import colored
from server.chat.chat import ChatLogger
from server.managers.server_mapper import ServerMapper
@@ -12,12 +14,18 @@
DIFF_NORM = "0.0000"
DIFF_HARD = "1.0000"
DIFF_SUI = "2.0000"
-DIFF_HOE = "4.0000"
+DIFF_HOE = "3.0000"
LEN_SHORT = "0"
LEN_NORM = "1"
LEN_LONG = "2"
+logger = logging.getLogger(__name__)
+if __debug__ and not hasattr(sys, 'frozen'):
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
+
class Server:
def __init__(self, name, address, username, password, game_password):
@@ -32,7 +40,7 @@ def __init__(self, name, address, username, password, game_password):
self.game_password = game_password
self.database = ServerDatabase(name)
- print("INFO: Connecting to: " + self.address + " (" + self.name + ")")
+ print("Connecting to: {} ({})".format(self.name, self.address))
self.session = self.new_session()
self.general_settings = self.load_general_settings()
@@ -54,7 +62,7 @@ def __init__(self, name, address, username, password, game_password):
self.mapper = ServerMapper(self)
self.mapper.start()
- print("INFO: Server " + name + " initialised")
+ logger.debug("Server " + name + " initialised")
def new_session(self):
login_url = "http://" + self.address + "/ServerAdmin/"
@@ -81,14 +89,14 @@ def new_session(self):
login_response = s.post(login_url, data=login_payload)
if "Invalid credentials" in login_response.text:
- print("ERROR: Bad credentials for server: " + self.name)
+ logger.error("Bad credentials for server: " + self.name)
input("Press enter to exit...")
sys.exit()
# Add in something to retry for X times.
except requests.exceptions.RequestException:
- print("ERROR: Network error on: " + self.address +
- " (" + self.name + "), bad address?")
+ logger.error("Network error on: " + self.address +
+ " (" + self.name + "), bad address?")
input("Press enter to exit...")
sys.exit()
@@ -103,8 +111,8 @@ def load_general_settings(self):
try:
general_settings_response = self.session.get(general_settings_url)
except requests.exceptions.RequestException as e:
- print("INFO: Couldn't get settings " + self.name +
- " (RequestException)")
+ logger.debug("Couldn't get settings " + self.name +
+ " (RequestException), sleeping for 3s")
sleep(3)
general_settings_tree = html.fromstring(
general_settings_response.content
@@ -152,6 +160,9 @@ def trader_close(self):
self.chat.handle_message("server", "!t_close", admin=True)
def new_game(self):
+ message = "New game on {}, map: {}"\
+ .format(self.name, self.game['map_title'])
+ print(colored(message, 'magenta'))
self.chat.handle_message("server", "!new_game", admin=True)
def get_player(self, username):
@@ -164,15 +175,18 @@ def player_join(self, player):
self.database.load_player(player)
player.total_logins += 1
self.players.append(player)
+ message = "Player {} joined {}".format(player.username, self.name)
+ print(colored(message, 'cyan'))
self.chat.handle_message("server",
- "!p_join " + player.username,
+ "!player_join " + player.username,
admin=True)
- print("INFO: Player " + player.username + " joined")
def player_quit(self, quit_player):
for player in self.players:
if player.username == quit_player.username:
- print("INFO: Player " + player.username + " quit")
+ message = "Player {} quit {}"\
+ .format(quit_player.username, self.name)
+ print(colored(message, 'cyan'))
self.chat.handle_message("server",
"!p_quit " + player.username,
admin=True)
@@ -180,7 +194,7 @@ def player_quit(self, quit_player):
self.players.remove(player)
def write_all_players(self, final=False):
- print("INFO: Writing players")
+ logger.debug("Flushing database ({})".format(self.name))
for player in self.players:
self.database.save_player(player, final)
@@ -192,9 +206,9 @@ def set_difficulty(self, difficulty):
self.general_settings['settings_GameDifficulty_raw'] = difficulty
try:
self.session.post(general_settings_url, self.general_settings)
- except requests.exceptions.RequestException as e:
- print("INFO: Couldn't set difficulty " + self.name +
- " (RequestException)")
+ except requests.exceptions.RequestException:
+ logger.warning("Couldn't set difficulty on {} (RequestException)"
+ .format(self.name))
sleep(3)
def set_length(self, length):
@@ -206,8 +220,8 @@ def set_length(self, length):
try:
self.session.post(general_settings_url, self.general_settings)
except requests.exceptions.RequestException:
- print("INFO: Couldn't set length " + self.name +
- " (RequestException)")
+ logger.warning("Couldn't set length on {} (RequestException)"
+ .format(self.name))
sleep(3)
def save_settings(self):
@@ -217,9 +231,9 @@ def save_settings(self):
"/ServerAdmin/settings/general"
try:
self.session.post(general_settings_url, self.general_settings)
- except requests.exceptions.RequestException as e:
- print("INFO: Couldn't set general settings " + self.name +
- " (RequestException), retrying")
+ except requests.exceptions.RequestException:
+ logger.warning("Couldn't set general settings on {} "
+ "(RequestException)".format(self.name))
sleep(3)
def toggle_game_password(self):
@@ -231,10 +245,10 @@ def toggle_game_password(self):
try:
passwords_response = self.session.get(passwords_url)
- except requests.exceptions.RequestException as e:
- print("INFO: Couldn't get password state " + self.name +
- " (RequestException)")
- sleep(3)
+ except requests.exceptions.RequestException:
+ logger.warning("Couldn't get password state on {} "
+ "(RequestException), returning".format(self.name))
+ return
passwords_tree = html.fromstring(passwords_response.content)
password_state = passwords_tree.xpath(
@@ -250,8 +264,8 @@ def toggle_game_password(self):
try:
self.session.post(passwords_url, payload)
except requests.exceptions.RequestException:
- print("INFO: Couldn't set password " + self.name +
- " (RequestException)")
+ logger.warning("Couldn't set password on {} (RequestException)"
+ .format(self.name))
sleep(3)
if password_state == 'False':
return True
@@ -271,18 +285,9 @@ def change_map(self, new_map):
try:
self.session.post(map_url, payload)
except requests.exceptions.RequestException:
- print("INFO: Couldn't set map " + self.name +
- " (RequestException)")
+ logger.warning("Couldn't set map on {} (RequestException)"
+ .format(self.name))
sleep(3)
def restart_map(self):
self.change_map(self.game['map_title'])
-
- def terminate(self):
- self.mapper.terminate()
- self.mapper.join()
-
- self.chat.terminate()
- self.chat.join()
-
- self.write_all_players(final=True)