Add Doom plugin

This commit is contained in:
Pedro de Oliveira 2019-11-25 11:41:17 +00:00
parent 079ae88dba
commit bbfeab6962
8 changed files with 825 additions and 0 deletions

1
Doom/README.md Normal file
View File

@ -0,0 +1 @@
Zandronum Server Query

72
Doom/__init__.py Normal file
View File

@ -0,0 +1,72 @@
###
# Copyright (c) 2019, Pedro de Oliveira
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Doom: Zandronum Server Query
"""
import sys
import supybot
from supybot import world
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = ""
# XXX Replace this with an appropriate author or supybot.Author instance.
__author__ = supybot.authors.unknown
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
# This is a url where the most recent plugin package can be downloaded.
__url__ = ''
from . import config
from . import plugin
if sys.version_info >= (3, 4):
from importlib import reload
else:
from imp import reload
# In case we're being reloaded.
reload(config)
reload(plugin)
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
if world.testing:
from . import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

56
Doom/config.py Normal file
View File

@ -0,0 +1,56 @@
###
# Copyright (c) 2019, Pedro de Oliveira
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot import conf, registry
try:
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('Doom')
except:
# Placeholder that allows to run the plugin on a bot
# without the i18n module
_ = lambda x: x
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified themself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Doom', True)
Doom = conf.registerPlugin('Doom')
# This is where your configuration variables (if any) should go. For example:
# conf.registerGlobalValue(Doom, 'someConfigVariableName',
# registry.Boolean(False, _("""Help for someConfigVariableName.""")))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

393
Doom/doomserver.py Normal file
View File

@ -0,0 +1,393 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import socket
import struct
import sys
import time
import concurrent.futures
import threading
#from huffman import HuffmanObject, SKULLTAG_FREQS
class Tools:
def __init__(self):
self.position = None
self.buffer = None
def __find_nul_size(self):
nul = None
nul_idx = self.position
while nul is None:
if self.buffer[nul_idx] == 0:
nul = True
break
nul_idx += 1
return nul_idx - self.position
def __get_value(self, format_type):
size = struct.Struct("<" + format_type).size
#print(size, self.position, self.position + size)
value = struct.unpack("<" + format_type,
self.buffer[self.position:self.position + size])[0]
self.position += size
return (value, size)
def get_long(self):
return self.__get_value("l")[0]
def get_short(self):
return self.__get_value("H")[0]
def get_byte(self):
return ord(self.__get_value("c")[0])
def get_string(self):
str_size = str(self.__find_nul_size())
value = self.__get_value(str_size + "s")
self.position += 1
return value[0].decode('iso-8859-1')
class MasterServer(Tools):
LAUNCHER_MASTER_CHALLENGE = 5660028
MASTER_SERVER_VERSION = 2
HOST = "zandronum.com"
PORT = 15300
SERVERS = []
def __init__(self, huffman):
self.__huffman = huffman
self.__client = None
self.__status = None
self.__packet = None
def __connect(self):
self.__client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def __query(self):
MAGIC_NUMBER = struct.pack('<lh',
self.LAUNCHER_MASTER_CHALLENGE, self.MASTER_SERVER_VERSION)
ENCODED_MAGIC_NUMBER = self.__huffman.encode(MAGIC_NUMBER)
self.__client.sendto(ENCODED_MAGIC_NUMBER, (self.HOST, self.PORT))
def __receive_data(self):
self.buffer = self.__huffman.decode(self.__client.recv(1024))
self.position = 0
newFile = open ("mainserver.bin", "wb")
newFile.write(self.buffer)
newFile.close()
def __get_status(self):
self.__status = self.get_long()
if self.__status is not 6:
sys.exit("Oh no! Status is {}".format(self.__status))
def __get_packet(self):
self.__packet = self.get_byte()
def __get_server_block(self):
server_block = self.get_byte()
if server_block is not 8:
sys.exit("Oh no! Server block is {}".format(server_block))
while True:
# Get number of servers, 0 if finished
number_of_servers = self.get_byte()
if number_of_servers is 0:
return self.get_byte()
# Get IP
ip = []
for _ in range(0, 4):
ip.append(self.get_byte())
# Get ports
for _ in range(0, number_of_servers):
port = self.get_short()
"""
self.SERVERS.append(
{
'host': "{}.{}.{}.{}:{}".format(
ip[0],
ip[1],
ip[2],
ip[3],
port
),
}
)
"""
self.SERVERS.append(
"{}.{}.{}.{}:{}".format(
ip[0],
ip[1],
ip[2],
ip[3],
port
)
)
def get_list(self):
self.__connect()
self.__query()
self.__receive_data()
while True:
self.__get_status()
self.__get_packet()
status = self.__get_server_block()
# Got the full list
if status is 2:
self.__client.close()
return self.SERVERS
else:
self.__receive_data()
class IndividualServer(Tools):
QUERY_FLAGS = [
{ 'name': 'SQF_NAME', 'value': 0x00000001 }, # The name of the server
{ 'name': 'SQF_URL', 'value': 0x00000002 }, # The associated website
{ 'name': 'SQF_EMAIL', 'value': 0x00000004 }, # Contact address
{ 'name': 'SQF_MAPNAME', 'value': 0x00000008 }, # Current map being played
{ 'name': 'SQF_MAXCLIENTS', 'value': 0x00000010 }, # Maximum amount of clients who can connect to the server
{ 'name': 'SQF_MAXPLAYERS', 'value': 0x00000020 }, # Maximum amount of players who can join the game (the rest must spectate)
{ 'name': 'SQF_PWADS', 'value': 0x00000040 }, # PWADs loaded by the server
{ 'name': 'SQF_GAMETYPE', 'value': 0x00000080 }, # Game type code
{ 'name': 'SQF_GAMENAME', 'value': 0x00000100 }, # Game mode name
{ 'name': 'SQF_IWAD', 'value': 0x00000200 }, # The IWAD used by the server
{ 'name': 'SQF_FORCEPASSWORD', 'value': 0x00000400 }, # Whether or not the server enforces a password
{ 'name': 'SQF_FORCEJOINPASSWORD', 'value': 0x00000800 }, # Whether or not the server enforces a join password
{ 'name': 'SQF_GAMESKILL', 'value': 0x00001000 }, # The skill level on the server
{ 'name': 'SQF_BOTSKILL', 'value': 0x00002000 }, # The skill level of any bots on the server
{ 'name': 'SQF_DMFLAGS', 'value': 0x00004000 }, # (Deprecated) The values of dmflags, dmflags2 and compatflags. Use SQF_ALL_DMFLAGS instead.
{ 'name': 'SQF_LIMITS', 'value': 0x00010000 }, # Timelimit, fraglimit, etc.
{ 'name': 'SQF_TEAMDAMAGE', 'value': 0x00020000 }, # Team damage factor.
{ 'name': 'SQF_TEAMSCORES', 'value': 0x00040000 }, # (Deprecated) The scores of the red and blue teams. Use SQF_TEAMINFO_* instead.
{ 'name': 'SQF_NUMPLAYERS', 'value': 0x00080000 }, # Amount of players currently on the server.
{ 'name': 'SQF_PLAYERDATA', 'value': 0x00100000 }, # Information of each player in the server.
{ 'name': 'SQF_TEAMINFO_NUMBER', 'value': 0x00200000 }, # Amount of teams available.
{ 'name': 'SQF_TEAMINFO_NAME', 'value': 0x00400000 }, # Names of teams.
{ 'name': 'SQF_TEAMINFO_COLOR', 'value': 0x00800000 }, # RGB colors of teams.
{ 'name': 'SQF_TEAMINFO_SCORE', 'value': 0x01000000 }, # Scores of teams.
{ 'name': 'SQF_TESTING_SERVER', 'value': 0x02000000 }, # Whether or not the server is a testing server, also the name of the testing binary.
{ 'name': 'SQF_DATA_MD5SUM', 'value': 0x04000000 }, # (Deprecated) Used to retrieve the MD5 checksum of skulltag_data.pk3, now obsolete and returns an empty string instead.
{ 'name': 'SQF_ALL_DMFLAGS', 'value': 0x08000000 }, # Values of various dmflags used by the server.
{ 'name': 'SQF_SECURITY_SETTINGS', 'value': 0x10000000 }, # Security setting values (for now only whether the server enforces the master banlist)
{ 'name': 'SQF_OPTIONAL_WADS', 'value': 0x20000000 }, # Which PWADs are optional
{ 'name': 'SQF_DEH', 'value': 0x40000000 }, # List of DEHACKED patches loaded by the server.
{ 'name': 'SQF_EXTENDED_INFO', 'value': 0x80000000 }, # (development version 3.1-alpha and above only) Additional server information, see the table below for more information.
]
# Extended Flags
SQF2_PWAD_HASHES = 0x00000001 # The MD5 hashes of the server's loaded PWADs
# Query
LAUNCHER_CHALLENGE = 199
FLAGS = [
{ 'name': 'SQF_NAME', 'type': 's' }, # The server's name (sv_hostname)
{ 'name': 'SQF_URL', 'type': 's' }, # The server's WAD URL (sv_website)
{ 'name': 'SQF_EMAIL', 'type': 's' }, # The server host's e-mail (sv_hostemail)
{ 'name': 'SQF_MAPNAME', 'type': 's' }, # The current map's name
{ 'name': 'SQF_MAXCLIENTS', 'type': 'c' }, # The max number of clients (sv_maxclients)
{ 'name': 'SQF_MAXPLAYERS', 'type': 'c' }, # The max number of players (sv_maxplayers)
{ 'name': 'SQF_PWADS', 'type': 'c' }, # The number of PWADs loaded
{ 'name': 'SQF_PWADS', 'type': 's' }, # The PWAD's name (Sent for each PWAD)
{ 'name': 'SQF_GAMETYPE', 'type': 'c' }, # The current game mode. See below.
{ 'name': 'SQF_GAMETYPE', 'type': 'c' }, # Instagib - true (1) / false (0)
{ 'name': 'SQF_GAMETYPE', 'type': 'c' }, # Buckshot - true (1) / false (0)
{ 'name': 'SQF_GAMENAME', 'type': 's' }, # The base game's name ("DOOM", "DOOM II", "HERETIC", "HEXEN", "ERROR!")
{ 'name': 'SQF_IWAD', 'type': 's' }, # The IWAD's name
{ 'name': 'SQF_FORCEPASSWORD', 'type': 'c' }, # Whether a password is required to join the server (sv_forcepassword)
{ 'name': 'SQF_FORCEJOINPASSWORD', 'type': 'c' }, # Whether a password is required to join the game (sv_forcejoinpassword)
{ 'name': 'SQF_GAMESKILL', 'type': 'c' }, # The game's difficulty (skill)
{ 'name': 'SQF_BOTSKILL', 'type': 'c' }, # The bot difficulty (botskill)
{ 'name': 'SQF_DMFLAGS', 'type': 'l' }, # [Deprecated] Value of dmflags
{ 'name': 'SQF_DMFLAGS', 'type': 'l' }, # [Deprecated] Value of dmflags2
{ 'name': 'SQF_DMFLAGS', 'type': 'l' }, # [Deprecated] Value of compatflags
{ 'name': 'SQF_LIMITS', 'type': 'h' }, # Value of fraglimit
{ 'name': 'SQF_LIMITS', 'type': 'h' }, # Value of timelimit
{ 'name': 'SQF_LIMITS', 'type': 'h' }, # time left in minutes (only sent if timelimit > 0)
{ 'name': 'SQF_LIMITS', 'type': 'h' }, # duellimit
{ 'name': 'SQF_LIMITS', 'type': 'h' }, # pointlimit
{ 'name': 'SQF_LIMITS', 'type': 'h' }, # winlimit
{ 'name': 'SQF_TEAMDAMAGE', 'type': 'f' }, # The team damage scalar (teamdamage)
{ 'name': 'SQF_TEAMSCORES', 'type': 'h' }, # [Deprecated] Blue team's fragcount/wincount/score
{ 'name': 'SQF_TEAMSCORES', 'type': 'h' }, # [Deprecated] Red team's fragcount/wincount/score
{ 'name': 'SQF_NUMPLAYERS', 'type': 'c' }, # The number of players in the server
{ 'name': 'SQF_PLAYERDATA', 'type': 's' }, # Player's name
{ 'name': 'SQF_PLAYERDATA', 'type': 'h' }, # Player's pointcount/fragcount/killcount
{ 'name': 'SQF_PLAYERDATA', 'type': 'h' }, # Player's ping
{ 'name': 'SQF_PLAYERDATA', 'type': 'c' }, # Player is spectating - true (1) / false (0)
{ 'name': 'SQF_PLAYERDATA', 'type': 'c' }, # Player is a bot - true (1) / false (0)
{ 'name': 'SQF_PLAYERDATA', 'type': 'c' }, # Player's team (returned on team games, 255 is no team)
{ 'name': 'SQF_PLAYERDATA', 'type': 'c' }, # Player's time on the server, in minutes. Note: SQF_PLAYERDATA information is sent once for each player on the server.
{ 'name': 'SQF_TEAMINFO_NUMBER', 'type': 'c' }, # The number of teams used.
{ 'name': 'SQF_TEAMINFO_NAME', 'type': 's' }, # The team's name. (Sent for each team.)
{ 'name': 'SQF_TEAMINFO_COLOR', 'type': 'l' }, # The team's color. (Sent for each team.)
{ 'name': 'SQF_TEAMINFO_SCORE', 'type': 'h' }, # The team's score. (Sent for each team.)
{ 'name': 'SQF_TESTING_SERVER', 'type': 'c' }, # Whether this server is running a testing binary - true (1) / false (0)
{ 'name': 'SQF_TESTING_SERVER', 'type': 's' }, # An empty string in case the server is running a stable binary, otherwise name of the testing binary archive found in http://www.skulltag.com/testing/files/
{ 'name': 'SQF_DATA_MD5SUM', 'type': 's' }, # [Deprecated] Returns an empty string.
{ 'name': 'SQF_ALL_DMFLAGS', 'type': 'c' }, # The number of flags that will be sent.
{ 'name': 'SQF_ALL_DMFLAGS', 'type': 'l' }, # The value of the flags (Sent for each flag in the order dmflags, dmflags2, zadmflags, compatflags, zacompatflags, compatflags2)
{ 'name': 'SQF_SECURITY_SETTINGS', 'type': 'c' }, # Whether the server is enforcing the master ban list - true (1) / false (0) The other bits of this byte may be used to transfer other security related settings in the future.
{ 'name': 'SQF_OPTIONAL_WADS', 'type': 'c' }, # Amount of optional wad indices that follow
{ 'name': 'SQF_OPTIONAL_WADS', 'type': 'c' }, # Index number int the list sent with SQF_PWADS - this wad is optional (sent for each optional Wad)
{ 'name': 'SQF_DEH', 'type': 'c' }, # Amount of deh patches loaded
{ 'name': 'SQF_DEH', 'type': 's' }, # Deh patch name (one string for each deh patch)
{ 'name': 'SQF_EXTENDED_INFO', 'type': 'l' }, # (development version 3.1-alpha and above only) The flags specifying extended server information you will receive. Check all SQF2 values against this field.
{ 'name': 'SQF2_PWAD_HASHES', 'type': 'c' }, # (development version 3.1-alpha and above only) The number of hashes sent.
{ 'name': 'SQF2_PWAD_HASHES', 'type': 's' }, # (development version 3.1-alpha and above only) The hash of the PWAD, sent for each PWAD. The indices are the same as sent in SQF_PWADS
]
def __init__(self, host, port, huffman):
#print(host, port)
self.__host = host
self.__port = abs(port)
self.__huffman = huffman
self.__client = None
def __get_query_flag(self, flag):
for item in self.QUERY_FLAGS:
if item['name'] == flag:
return item['value']
def __get_flag_type(self, flag):
for item in self.QUERY_FLAGS:
if item['name'] == flag:
return item['type']
def __parse_flags(self, value):
flags = []
for item in self.QUERY_FLAGS:
if value & item['value']:
flags.append(item['name'])
return flags
def __connect(self):
self.__client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
MAGIC_NUMBER = struct.pack('<llll',
self.LAUNCHER_CHALLENGE,
self.__get_query_flag('SQF_NAME') | self.__get_query_flag('SQF_MAPNAME') |
self.__get_query_flag('SQF_NUMPLAYERS') | self.__get_query_flag('SQF_PLAYERDATA'),
int(time.time()),
0)
ENCODED_MAGIC_NUMBER = self.__huffman.encode(MAGIC_NUMBER)
self.__client.sendto(ENCODED_MAGIC_NUMBER, (self.__host, self.__port))
def __receive_data(self):
self.__client.settimeout(1)
self.buffer = self.__huffman.decode(self.__client.recv(1024))
self.position = 0
#newFile = open ("server.bin", "wb")
#newFile.write(self.__buffer)
#newFile.close()
def get_info(self):
self.__connect()
try:
self.__receive_data()
except socket.timeout:
self.__client.close()
return []
info = {'players': []}
# Get response
response = self.get_long()
self.get_long() # unused
if response != 5660023:
return []
#sys.exit("Oh no! Response is {}".format(response))
# Get version
self.get_string()
#version = self.get_string()
#print(version)
# Get Flags
flags = self.get_long()
#print(self.__parse_flags(flags))
"""
for flag in flags:
flag_type = self.__get_flag_type(flag)
if flag_type == 's':
value = self.get_string()
elif flag_type == 'c':
value = self.get_byte()
elif flag_type == 'l':
value = self.get_long()
elif flag_type == 'h':
value = self.get_short()
if value:
info[flag] = value
"""
SQF_NAME = self.get_string()
SQF_MAPNAME = self.get_string()
SQF_NUMPLAYERS = self.get_byte()
info['name'] = SQF_NAME
info['map_name'] = SQF_MAPNAME
info['num_players'] = SQF_NUMPLAYERS
#if (SQF_NAME.find('MOP') == -1):
# return []
for _ in range(0, SQF_NUMPLAYERS):
# Get Player Info
nick = self.get_string()
kills = self.get_short()
ping = self.get_short()
spectator = self.get_byte()
bot = self.get_byte()
time = self.get_byte()
info['players'].append({
'nick': nick,
'kills': kills,
'ping': ping,
'spectator': spectator,
'bot': bot,
'time': time,
})
info['host'] = "{}:{}".format(self.__host, self.__port)
self.__client.close()
return info
"""
huffman = HuffmanObject(SKULLTAG_FREQS)
def get_server_info(host):
zbr = host.split(':')
ds = IndividualServer(zbr[0], int(zbr[1]), huffman)
return ds.get_info()
doom = MasterServer(huffman)
servers = doom.get_list()
with_info = []
with concurrent.futures.ThreadPoolExecutor(max_workers=25) as executor:
future_server_info = {executor.submit(get_server_info, server): server for server in servers}
for future in concurrent.futures.as_completed(future_server_info):
server = future_server_info[future]
try:
info = future.result()
except Exception as exc:
print('{} generated an exception: {}'.format(server, exc))
else:
if len(info) > 0:
with_info.append(info)
print(json.dumps(with_info, indent=4, sort_keys=True))
"""

185
Doom/huffman.py Normal file
View File

@ -0,0 +1,185 @@
SKULLTAG_FREQS = [
0.14473691, 0.01147017, 0.00167522, 0.03831121, 0.00356579, 0.03811315,
0.00178254, 0.00199644, 0.00183511, 0.00225716, 0.00211240, 0.00308829,
0.00172852, 0.00186608, 0.00215921, 0.00168891, 0.00168603, 0.00218586,
0.00284414, 0.00161833, 0.00196043, 0.00151029, 0.00173932, 0.00218370,
0.00934121, 0.00220530, 0.00381211, 0.00185456, 0.00194675, 0.00161977,
0.00186680, 0.00182071, 0.06421956, 0.00537786, 0.00514019, 0.00487155,
0.00493925, 0.00503143, 0.00514019, 0.00453520, 0.00454241, 0.00485642,
0.00422407, 0.00593387, 0.00458130, 0.00343687, 0.00342823, 0.00531592,
0.00324890, 0.00333388, 0.00308613, 0.00293776, 0.00258918, 0.00259278,
0.00377105, 0.00267488, 0.00227516, 0.00415997, 0.00248763, 0.00301555,
0.00220962, 0.00206990, 0.00270369, 0.00231694, 0.00273826, 0.00450928,
0.00384380, 0.00504728, 0.00221251, 0.00376961, 0.00232990, 0.00312574,
0.00291688, 0.00280236, 0.00252436, 0.00229461, 0.00294353, 0.00241201,
0.00366590, 0.00199860, 0.00257838, 0.00225860, 0.00260646, 0.00187256,
0.00266552, 0.00242641, 0.00219450, 0.00192082, 0.00182071, 0.02185930,
0.00157439, 0.00164353, 0.00161401, 0.00187544, 0.00186248, 0.03338637,
0.00186968, 0.00172132, 0.00148509, 0.00177749, 0.00144620, 0.00192442,
0.00169683, 0.00209439, 0.00209439, 0.00259062, 0.00194531, 0.00182359,
0.00159096, 0.00145196, 0.00128199, 0.00158376, 0.00171412, 0.00243433,
0.00345704, 0.00156359, 0.00145700, 0.00157007, 0.00232342, 0.00154198,
0.00140730, 0.00288807, 0.00152830, 0.00151246, 0.00250203, 0.00224420,
0.00161761, 0.00714383, 0.08188576, 0.00802537, 0.00119484, 0.00123805,
0.05632671, 0.00305156, 0.00105584, 0.00105368, 0.00099246, 0.00090459,
0.00109473, 0.00115379, 0.00261223, 0.00105656, 0.00124381, 0.00100326,
0.00127550, 0.00089739, 0.00162481, 0.00100830, 0.00097229, 0.00078864,
0.00107240, 0.00084409, 0.00265760, 0.00116891, 0.00073102, 0.00075695,
0.00093916, 0.00106880, 0.00086786, 0.00185600, 0.00608367, 0.00133600,
0.00075695, 0.00122077, 0.00566955, 0.00108249, 0.00259638, 0.00077063,
0.00166586, 0.00090387, 0.00087074, 0.00084914, 0.00130935, 0.00162409,
0.00085922, 0.00093340, 0.00093844, 0.00087722, 0.00108249, 0.00098598,
0.00095933, 0.00427593, 0.00496661, 0.00102775, 0.00159312, 0.00118404,
0.00114947, 0.00104936, 0.00154342, 0.00140082, 0.00115883, 0.00110769,
0.00161112, 0.00169107, 0.00107816, 0.00142747, 0.00279804, 0.00085922,
0.00116315, 0.00119484, 0.00128559, 0.00146204, 0.00130215, 0.00101551,
0.00091756, 0.00161184, 0.00236375, 0.00131872, 0.00214120, 0.00088875,
0.00138570, 0.00211960, 0.00094060, 0.00088083, 0.00094564, 0.00090243,
0.00106160, 0.00088659, 0.00114514, 0.00095861, 0.00108753, 0.00124165,
0.00427016, 0.00159384, 0.00170547, 0.00104431, 0.00091395, 0.00095789,
0.00134681, 0.00095213, 0.00105944, 0.00094132, 0.00141883, 0.00102127,
0.00101911, 0.00082105, 0.00158448, 0.00102631, 0.00087938, 0.00139290,
0.00114658, 0.00095501, 0.00161329, 0.00126542, 0.00113218, 0.00123661,
0.00101695, 0.00112930, 0.00317976, 0.00085346, 0.00101190, 0.00189849,
0.00105728, 0.00186824, 0.00092908, 0.00160896,
]
class HuffmanObject(object):
def __init__(self, freqs):
self.huffman_freqs = freqs
self.huffman_tree = []
self.huffman_table = [None] * 256
self.__build_binary_tree()
self.__binary_tree_to_lookup_table(self.huffman_tree)
def __build_binary_tree(self):
"""
Create the huffman tree from frequency list found in the
current object.
"""
# Create starting leaves
for i in range(256):
self.huffman_tree.append({
'frq': self.huffman_freqs[i],
'asc': i,
})
# Pair leaves and branches based on frequency until there is a
# single root
for i in range(255):
lowest_key1 = -1
lowest_key2 = -1
lowest_frq1 = 1e30
lowest_frq2 = 1e30
# Find two lowest frequencies
for j in range(256):
if not self.huffman_tree[j]:
continue
if self.huffman_tree[j]['frq'] < lowest_frq1:
lowest_key2 = lowest_key1
lowest_frq2 = lowest_frq1
lowest_key1 = j
lowest_frq1 = self.huffman_tree[j]['frq']
elif self.huffman_tree[j]['frq'] < lowest_frq2:
lowest_key2 = j
lowest_frq2 = self.huffman_tree[j]['frq']
# Join the two together under a new branch
self.huffman_tree[lowest_key1] = {
'frq': lowest_frq1 + lowest_frq2,
'0': self.huffman_tree[lowest_key2],
'1': self.huffman_tree[lowest_key1],
}
self.huffman_tree[lowest_key2] = None
# Make the root the list
self.huffman_tree = self.huffman_tree[lowest_key1]
def __binary_tree_to_lookup_table(self, branch, binary_path = ''):
"""
Recursively create the lookup table used to encode a message.
"""
# Go through a branch finding leaves while tracking the path taken
if '0' in branch:
self.__binary_tree_to_lookup_table(branch['0'], binary_path + '0')
self.__binary_tree_to_lookup_table(branch['1'], binary_path + '1')
else:
self.huffman_table[branch['asc']] = binary_path
def encode(self, data_string):
"""
Encode a string into a huffman-coded string.
"""
if type(data_string) is not bytes:
raise ValueError('Must pass bytes to encode')
binary_string = ''
# Match ASCII to entries in the lookup table
for byte in data_string:
binary_string += self.huffman_table[byte]
# Convert binary string into ASCII
encoded_string = b'';
for i in range(0, len(binary_string), 8):
binary = binary_string[i:i+8]
encoded_string += bytes([int(binary[::-1], 2)])
# If the huffman-coded string is longer than the original
# string, return the original string instead. Putting an
# ASCII value 0xff where the padding bit should be signals to
# the decoder that the message is not encoded.
#print(len(data_string), len(encoded_string))
#if len(data_string) <= len(encoded_string):
# return b'\xff' + data_string
# In the first byte, store the number of padding bits
padding_value = (8 - (len(binary_string) % 8)) % 8
encoded_string = bytes([padding_value]) + encoded_string
return encoded_string
def decode(self, data_string):
"""
Decode a huffman-coded string into a string.
"""
if type(data_string) is not bytes:
raise ValueError('Must pass bytes to decode')
# Obtain and remove the number of padding bits stored in the
# first byte.
padding_length = data_string[0]
data_string = data_string[1:]
# If the padding bit is set to 0xff the message is not encoded.
if padding_length == 0xff:
return data_string
# Convert ascii string into binary string
binary_string = ''
for byte in data_string:
binary_string += '{0:08b}'.format(byte)[::-1]
# Remove padding bits from the end
binary_string = binary_string[:len(binary_string) - padding_length]
# Match binary to entries in the huffman tree
decoded_string = b'';
tree_node = self.huffman_tree
for bit in binary_string:
if bit in tree_node:
tree_node = tree_node[bit]
else:
decoded_string += bytes([tree_node['asc']])
tree_node = self.huffman_tree[bit]
decoded_string += bytes([tree_node['asc']])
return decoded_string

1
Doom/local/__init__.py Normal file
View File

@ -0,0 +1 @@
# Stub so local is a module, used for third-party modules

79
Doom/plugin.py Normal file
View File

@ -0,0 +1,79 @@
###
# Copyright (c) 2019, Pedro de Oliveira
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot import utils, plugins, ircutils, callbacks
from supybot.commands import *
try:
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('Doom')
except ImportError:
# Placeholder that allows to run the plugin on a bot
# without the i18n module
_ = lambda x: x
from .doomserver import IndividualServer
from .huffman import HuffmanObject, SKULLTAG_FREQS
class Doom(callbacks.Plugin):
"""Zandronum Server Query"""
threaded = True
def __format_message(self, info):
message = "{} @ {} [ {} ] - Players:4 {} ".format(
info['name'],
info['host'],
info['map_name'],
info['num_players']
)
if info['num_players'] > 0:
for player in info['players']:
message += "{} [Kills: {}] ".format(player['nick'], player['kills'])
return message[:-1]
def status(self, irc, msg, args, server):
"""<server>
Returns informations about server.
"""
huffman = HuffmanObject(SKULLTAG_FREQS)
zbr = server.split(':')
ds = IndividualServer(zbr[0], int(zbr[1]), huffman)
info = ds.get_info()
if len(info) == 0:
irc.reply("It's dead Jim", prefixNick=False)
else:
irc.reply(self.__format_message(info), prefixNick=False)
status = wrap(status, ['anything'])
Class = Doom
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

38
Doom/test.py Normal file
View File

@ -0,0 +1,38 @@
###
# Copyright (c) 2019, Pedro de Oliveira
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot.test import *
class DoomTestCase(PluginTestCase):
plugins = ('Doom',)
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: