Add Doom plugin
This commit is contained in:
parent
079ae88dba
commit
bbfeab6962
|
|
@ -0,0 +1 @@
|
|||
Zandronum Server Query
|
||||
|
|
@ -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:
|
||||
|
|
@ -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:
|
||||
|
|
@ -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))
|
||||
"""
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Stub so local is a module, used for third-party modules
|
||||
|
|
@ -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:
|
||||
|
|
@ -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:
|
||||
Loading…
Reference in New Issue