Skip to content

Commit

Permalink
Merge pull request #48 from th3-z/master-wave-stats
Browse files Browse the repository at this point in the history
Per wave top players, greeter, logging, load_map
  • Loading branch information
th3-z committed Apr 14, 2018
2 parents 95f888b + b26d0c5 commit 5862693
Show file tree
Hide file tree
Showing 16 changed files with 492 additions and 295 deletions.
38 changes: 24 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand Down
31 changes: 16 additions & 15 deletions magicked_admin/chatbot/chatbot.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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] == '!':
Expand All @@ -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())
Expand Down
4 changes: 4 additions & 0 deletions magicked_admin/chatbot/commands/command_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
Expand Down
69 changes: 67 additions & 2 deletions magicked_admin/chatbot/commands/event_commands.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
20 changes: 9 additions & 11 deletions magicked_admin/chatbot/commands/info_commands.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
17 changes: 15 additions & 2 deletions magicked_admin/chatbot/commands/player_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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" + \
Expand All @@ -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" + \
Expand Down
31 changes: 23 additions & 8 deletions magicked_admin/chatbot/commands/server_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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."
Expand All @@ -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. " + \
Expand Down
1 change: 1 addition & 0 deletions magicked_admin/config/magicked_admin.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ password = 123
game_password = game_password
motd_scoreboard = False
scoreboard_type = kills
enable_greeter = True

3 changes: 3 additions & 0 deletions magicked_admin/config/server_one.init.example
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
silent
start_wc 1 say I'm a bot, type !help for usage
start_trc top_dosh -w
silent
2 changes: 2 additions & 0 deletions magicked_admin/config/server_one.motd.example
Original file line number Diff line number Diff line change
@@ -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]
Expand Down

0 comments on commit 5862693

Please sign in to comment.