Initial Release of IGNCore version 2.5
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
#
|
||||
# Base class of all modules...
|
||||
# makes accessing fields which exist in all modules easier
|
||||
#
|
||||
class BaseModule:
|
||||
module_name = ""
|
||||
module_dir = ""
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
def inject(self, registry):
|
||||
pass
|
||||
|
||||
def pre_start(self):
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
@@ -0,0 +1,117 @@
|
||||
import select
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from core.aochat.client_packets import LoginRequest, LoginSelect
|
||||
from core.aochat.crypt import generate_login_key
|
||||
from core.aochat.server_packets import ServerPacket, LoginOK, LoginError, LoginCharacterList
|
||||
from core.logger import Logger
|
||||
|
||||
|
||||
class Bot:
|
||||
def __init__(self):
|
||||
self.socket = None
|
||||
self.char_id = None
|
||||
self.char_name = None
|
||||
self.logger = Logger(__name__)
|
||||
|
||||
def connect(self, host, port):
|
||||
self.logger.info("Connecting to '%s:%d'" % (host, port))
|
||||
self.socket = socket.create_connection((host, port), 10)
|
||||
|
||||
def disconnect(self):
|
||||
if self.socket:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def login(self, username, password, character):
|
||||
character = character.capitalize()
|
||||
|
||||
# read seed packet
|
||||
self.logger.info("Logging in as '%s'" % character)
|
||||
seed_packet = self.read_packet(10)
|
||||
seed = seed_packet.seed
|
||||
|
||||
# send back challenge
|
||||
key = generate_login_key(seed, username, password)
|
||||
login_request_packet = LoginRequest(0, username, key)
|
||||
self.send_packet(login_request_packet)
|
||||
|
||||
# read character list
|
||||
character_list_packet: LoginCharacterList = self.read_packet()
|
||||
if isinstance(character_list_packet, LoginError):
|
||||
self.logger.error("Error logging in: %s" % character_list_packet.message)
|
||||
return False
|
||||
if character not in character_list_packet.names:
|
||||
self.logger.error("Character '%s' does not exist on this account" % character)
|
||||
return False
|
||||
index = character_list_packet.names.index(character)
|
||||
|
||||
# select character
|
||||
self.char_id = character_list_packet.char_ids[index]
|
||||
self.char_name = character_list_packet.names[index]
|
||||
login_select_packet = LoginSelect(self.char_id)
|
||||
self.send_packet(login_select_packet)
|
||||
|
||||
# wait for OK
|
||||
packet = self.read_packet()
|
||||
if packet.id == LoginOK.id:
|
||||
self.logger.info("Connected!")
|
||||
return packet
|
||||
else:
|
||||
self.logger.error("Error logging in: %s" % packet.message)
|
||||
return False
|
||||
|
||||
def read_packet(self, max_delay_time=1):
|
||||
"""
|
||||
Wait for packet from server.
|
||||
"""
|
||||
|
||||
read, write, error = select.select([self.socket], [], [], max_delay_time)
|
||||
if not read:
|
||||
return None
|
||||
else:
|
||||
# Read data from server
|
||||
head = self.read_bytes(4)
|
||||
packet_type, packet_length = struct.unpack(">2H", head)
|
||||
data = self.read_bytes(packet_length)
|
||||
|
||||
try:
|
||||
return ServerPacket.get_instance(packet_type, data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error parsing packet parameters for packet_type {packet_type} and payload: {data}",
|
||||
e)
|
||||
return None
|
||||
|
||||
def send_packet(self, packet):
|
||||
data = packet.to_bytes()
|
||||
data = struct.pack(">2H", packet.id, len(data)) + data
|
||||
|
||||
self.write_bytes(data)
|
||||
|
||||
def read_bytes(self, num_bytes):
|
||||
data = bytes()
|
||||
|
||||
while num_bytes > 0:
|
||||
chunk = self.socket.recv(num_bytes)
|
||||
|
||||
if len(chunk) == 0:
|
||||
raise EOFError
|
||||
|
||||
num_bytes -= len(chunk)
|
||||
data = data + chunk
|
||||
|
||||
return data
|
||||
|
||||
def write_bytes(self, data):
|
||||
num_bytes = len(data)
|
||||
|
||||
while num_bytes > 0:
|
||||
sent = self.socket.send(data)
|
||||
|
||||
if sent == 0:
|
||||
raise EOFError
|
||||
|
||||
data = data[sent:]
|
||||
num_bytes -= sent
|
||||
@@ -0,0 +1,266 @@
|
||||
from core.aochat.packets import *
|
||||
|
||||
|
||||
class ClientPacket(Packet):
|
||||
def __init__(self, packet_id, types, args):
|
||||
self.id = packet_id
|
||||
self.types = types
|
||||
self.args = args
|
||||
|
||||
def to_bytes(self):
|
||||
return encode_args(self.types, self.args)
|
||||
|
||||
def __str__(self):
|
||||
return "ClientPacket(%d): %s" % (self.id, self.args)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, packet_id, data):
|
||||
if packet_id == LoginRequest.id:
|
||||
LoginRequest.from_bytes(data)
|
||||
elif packet_id == LoginSelect.id:
|
||||
LoginSelect.from_bytes(data)
|
||||
elif packet_id == CharacterLookup.id:
|
||||
CharacterLookup.from_bytes(data)
|
||||
elif packet_id == PrivateMessage.id:
|
||||
PrivateMessage.from_bytes(data)
|
||||
elif packet_id == BuddyAdd.id:
|
||||
BuddyAdd.from_bytes(data)
|
||||
elif packet_id == BuddyRemove.id:
|
||||
BuddyRemove.from_bytes(data)
|
||||
elif packet_id == PrivateChannelInvite.id:
|
||||
PrivateChannelInvite.from_bytes(data)
|
||||
elif packet_id == PrivateChannelKick.id:
|
||||
PrivateChannelKick.from_bytes(data)
|
||||
elif packet_id == PrivateChannelJoin.id:
|
||||
PrivateChannelJoin.from_bytes(data)
|
||||
elif packet_id == PrivateChannelLeave.id:
|
||||
PrivateChannelLeave.from_bytes(data)
|
||||
elif packet_id == PrivateChannelKickAll.id:
|
||||
PrivateChannelKickAll.from_bytes(data)
|
||||
elif packet_id == PrivateChannelMessage.id:
|
||||
PrivateChannelMessage.from_bytes(data)
|
||||
elif packet_id == PublicChannelMessage.id:
|
||||
PublicChannelMessage.from_bytes(data)
|
||||
elif packet_id == Ping.id:
|
||||
Ping.from_bytes(data)
|
||||
elif packet_id == ChatCommand.id:
|
||||
ChatCommand.from_bytes(data)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class LoginRequest(ClientPacket):
|
||||
id = 2
|
||||
types = "ISS"
|
||||
|
||||
def __init__(self, unknown, username, key):
|
||||
self.unknown = unknown
|
||||
self.username = username
|
||||
self.key = key
|
||||
super().__init__(self.id, self.types, [self.unknown, self.username, self.key])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class LoginSelect(ClientPacket):
|
||||
id = 3
|
||||
types = "I"
|
||||
|
||||
def __init__(self, char_id):
|
||||
self.char_id = char_id
|
||||
super().__init__(self.id, self.types, [self.char_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class CharacterLookup(ClientPacket):
|
||||
id = 21
|
||||
types = "S"
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
super().__init__(self.id, self.types, [self.name])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateMessage(ClientPacket):
|
||||
id = 30
|
||||
types = "ISS"
|
||||
|
||||
def __init__(self, char_id, message, blob):
|
||||
self.char_id = char_id
|
||||
self.message = message
|
||||
self.blob = blob
|
||||
super().__init__(self.id, self.types, [self.char_id, self.message, self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class BuddyAdd(ClientPacket):
|
||||
id = 40
|
||||
types = "IS"
|
||||
|
||||
def __init__(self, char_id, status):
|
||||
self.char_id = char_id
|
||||
self.status = status
|
||||
super().__init__(self.id, self.types, [self.char_id, self.status])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class BuddyRemove(ClientPacket):
|
||||
id = 41
|
||||
types = "I"
|
||||
|
||||
def __init__(self, char_id):
|
||||
self.char_id = char_id
|
||||
super().__init__(self.id, self.types, [self.char_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelInvite(ClientPacket):
|
||||
id = 50
|
||||
types = "I"
|
||||
|
||||
def __init__(self, char_id):
|
||||
self.char_id = char_id
|
||||
super().__init__(self.id, self.types, [self.char_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelKick(ClientPacket):
|
||||
id = 51
|
||||
types = "I"
|
||||
|
||||
def __init__(self, char_id):
|
||||
self.char_id = char_id
|
||||
super().__init__(self.id, self.types, [self.char_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelJoin(ClientPacket):
|
||||
id = 52
|
||||
types = "I"
|
||||
|
||||
def __init__(self, private_channel_id):
|
||||
self.private_channel_id = private_channel_id
|
||||
super().__init__(self.id, self.types, [self.private_channel_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelLeave(ClientPacket):
|
||||
id = 53
|
||||
types = "I"
|
||||
|
||||
def __init__(self, private_channel_id):
|
||||
self.private_channel_id = private_channel_id
|
||||
super().__init__(self.id, self.types, [self.private_channel_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelKickAll(ClientPacket):
|
||||
id = 54
|
||||
types = ""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self.id, self.types, [])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
return cls()
|
||||
|
||||
|
||||
class PrivateChannelMessage(ClientPacket):
|
||||
id = 57
|
||||
types = "ISS"
|
||||
|
||||
def __init__(self, private_channel_id, message, blob):
|
||||
self.private_channel_id = private_channel_id
|
||||
self.message = message
|
||||
self.blob = blob
|
||||
super().__init__(self.id, self.types, [self.private_channel_id, self.message, self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PublicChannelMessage(ClientPacket):
|
||||
id = 65
|
||||
types = "GSS"
|
||||
|
||||
def __init__(self, channel_id, message, blob):
|
||||
self.channel_id = channel_id
|
||||
self.message = message
|
||||
self.blob = blob
|
||||
super().__init__(self.id, self.types, [self.channel_id, self.message, self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class Ping(ClientPacket):
|
||||
id = 100
|
||||
types = "S"
|
||||
|
||||
def __init__(self, blob):
|
||||
self.blob = blob
|
||||
super().__init__(self.id, self.types, [self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class ChatCommand(ClientPacket):
|
||||
id = 120
|
||||
types = "s"
|
||||
|
||||
def __init__(self, commands):
|
||||
self.commands = commands
|
||||
super().__init__(self.id, self.types, [self.commands])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
@@ -0,0 +1,110 @@
|
||||
# Relevant parts of original copyright notice of AOChat.php:
|
||||
|
||||
# Copyright (C) 2005 by Jürgen A. Erhard
|
||||
# Copyright (C) 2002-2004 Oskari Saarenmaa <auno@auno.org>.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
|
||||
# USA
|
||||
|
||||
|
||||
import random
|
||||
import socket
|
||||
import struct
|
||||
|
||||
|
||||
# This is 'half' Diffie-Hellman key exchange.
|
||||
# 'Half' as in we already have the server's key ($dhY)
|
||||
# $dhN is a prime and $dhG is generator for it.
|
||||
#
|
||||
# http://en.wikipedia.org/wiki/Diffie-Hellman_key_exchange
|
||||
|
||||
|
||||
# noinspection LongLine
|
||||
def generate_login_key(server_key, username, password):
|
||||
dhY = 0x9c32cc23d559ca90fc31be72df817d0e124769e809f936bc14360ff4bed758f260a0d596584eacbbc2b88bdd410416163e11dbf62173393fbc0c6fefb2d855f1a03dec8e9f105bbad91b3437d8eb73fe2f44159597aa4053cf788d2f9d7012fb8d7c4ce3876f7d6cd5d0c31754f4cd96166708641958de54a6def5657b9f2e92
|
||||
dhN = 0xeca2e8c85d863dcdc26a429a71a9815ad052f6139669dd659f98ae159d313d13c6bf2838e10a69b6478b64a24bd054ba8248e8fa778703b418408249440b2c1edd28853e240d8a7e49540b76d120d3b1ad2878b1b99490eb4a2a5e84caa8a91cecbdb1aa7c816e8be343246f80c637abc653b893fd91686cf8d32d6cfe5f2a6f
|
||||
dhG = 0x5
|
||||
dhx = random.randrange(0, 2 ** 256)
|
||||
|
||||
dhX = pow(dhG, dhx, dhN)
|
||||
dhK = pow(dhY, dhx, dhN)
|
||||
|
||||
dhK = "%x" % dhK
|
||||
if len(dhK) > 32:
|
||||
dhK = dhK[:32]
|
||||
|
||||
dhK = eval("0x" + dhK)
|
||||
|
||||
challenge = "%s|%s|%s" % (username, server_key, password)
|
||||
|
||||
# prefix is an 8 bytes of randomness
|
||||
prefix_bytes = random.randrange(0, 2 ** 64)
|
||||
prefix = struct.pack(">Q", prefix_bytes)
|
||||
|
||||
length = 8 + 4 + len(challenge) # prefix, int, ...
|
||||
pad = " " * ((8 - length % 8) % 8)
|
||||
challenge_len = struct.pack(">I", len(challenge))
|
||||
|
||||
plain = prefix + challenge_len + challenge.encode('ascii') + pad.encode('ascii')
|
||||
crypted = aochat_crypt(dhK, plain)
|
||||
|
||||
if not crypted:
|
||||
raise Exception("panic")
|
||||
|
||||
return ("%0x" % dhX) + "-" + crypted
|
||||
|
||||
|
||||
def aochat_crypt(key, data):
|
||||
if len(data) % 8 != 0:
|
||||
return None
|
||||
|
||||
cycle = [0, 0]
|
||||
result = [0, 0]
|
||||
ret = ""
|
||||
|
||||
key_arr = [socket.ntohl(int(s, 16)) for s in
|
||||
struct.unpack("8s" * (len("%s" % key) // 8), ("%x" % key).encode('ascii'))]
|
||||
data_arr = struct.unpack("I" * (len(data) // 4), data)
|
||||
|
||||
i = 0
|
||||
while i < len(data_arr):
|
||||
cycle[0] = data_arr[i] ^ result[0]
|
||||
cycle[1] = data_arr[i + 1] ^ result[1]
|
||||
result = aochat_tea_encrypt(cycle, key_arr)
|
||||
|
||||
p = "%08x%08x" % (socket.htonl(result[0]) & 0xffffffff, socket.htonl(result[1]) & 0xffffffff)
|
||||
|
||||
ret += p
|
||||
|
||||
i += 2
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def aochat_tea_encrypt(cycle, key):
|
||||
a, b = cycle
|
||||
total = 0
|
||||
delta = 0x9e3779b9
|
||||
i = 32
|
||||
|
||||
while i:
|
||||
total = (total + delta) & 0xffffffff
|
||||
a += (((b << 4 & 0xfffffff0) + key[0]) ^ (b + total) ^ ((b >> 5 & 0x7ffffff) + key[1])) & 0xffffffff
|
||||
a &= 0xffffffff
|
||||
b += (((a << 4 & 0xfffffff0) + key[2]) ^ (a + total) ^ ((a >> 5 & 0x7ffffff) + key[3])) & 0xffffffff
|
||||
b &= 0xffffffff
|
||||
i -= 1
|
||||
|
||||
return a, b
|
||||
@@ -0,0 +1,34 @@
|
||||
import time
|
||||
|
||||
|
||||
class DelayQueue:
|
||||
def __init__(self, recovery: int, burst=0):
|
||||
self.recovery = recovery
|
||||
self.burst = burst
|
||||
self.items = []
|
||||
self.next_packet = 0
|
||||
|
||||
def enqueue(self, item):
|
||||
self.items.insert(0, item)
|
||||
|
||||
def dequeue(self):
|
||||
if self.items:
|
||||
t = time.time()
|
||||
time_with_burst = t - (self.burst * self.recovery)
|
||||
if self.next_packet < time_with_burst:
|
||||
self.next_packet = time_with_burst
|
||||
|
||||
if t >= self.next_packet:
|
||||
self.next_packet += self.recovery
|
||||
return self.items.pop()
|
||||
else:
|
||||
return None
|
||||
|
||||
def __len__(self):
|
||||
return len(self.items)
|
||||
|
||||
def clear(self):
|
||||
self.items = []
|
||||
|
||||
def is_empty(self):
|
||||
return len(self.items) == 0
|
||||
@@ -0,0 +1,20 @@
|
||||
class ExtendedMessage:
|
||||
def __init__(self, category_id, instance_id, template, params):
|
||||
self.category_id = category_id
|
||||
self.instance_id = instance_id
|
||||
self.template = template
|
||||
self.params = params
|
||||
|
||||
def get_message(self):
|
||||
try:
|
||||
return self.template % tuple(self.params)
|
||||
except TypeError:
|
||||
# sometimes params are sent even tho the template does not include param placeholders
|
||||
# ex: ExtendedMessage:
|
||||
# [20000, 134870373,
|
||||
# 'Your ability to send private messages has been revoked temporarily with a GM gag.',
|
||||
# [1000]]
|
||||
return self.template
|
||||
|
||||
def __str__(self):
|
||||
return str([self.category_id, self.instance_id, self.template, self.params, self.get_message()])
|
||||
@@ -0,0 +1,137 @@
|
||||
import struct
|
||||
|
||||
from core.logger import Logger
|
||||
|
||||
|
||||
class MMDBParser:
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self.logger = Logger(__name__)
|
||||
|
||||
def get_message_string(self, category_id, instance_id):
|
||||
with open(self.filename, mode="rb") as file:
|
||||
categories = self.get_categories(file)
|
||||
|
||||
try:
|
||||
category = next(categories)
|
||||
while category["id"] != category_id:
|
||||
category = next(categories)
|
||||
next_category = next(categories)
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
instance = self.find_entry(file, instance_id, category["offset"], next_category["offset"])
|
||||
|
||||
if instance:
|
||||
file.seek(instance["offset"])
|
||||
return self.read_string(file)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_all_message_strings(self):
|
||||
with open(self.filename, mode="rb") as file:
|
||||
categories = iter(list(self.get_categories(file)))
|
||||
next_category = next(categories)
|
||||
|
||||
while True:
|
||||
try:
|
||||
category = next_category
|
||||
next_category = next(categories)
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
max_offset = next_category["offset"]
|
||||
file.seek(category["offset"])
|
||||
|
||||
instances = []
|
||||
while file.tell() < max_offset:
|
||||
entry = self.read_entry(file)
|
||||
instances.append(entry)
|
||||
|
||||
for instance in instances:
|
||||
file.seek(instance["offset"])
|
||||
message_string = self.read_string(file)
|
||||
print([category["id"], instance["id"], message_string])
|
||||
|
||||
def find_entry(self, file, entry_id, min_offset, max_offset):
|
||||
file.seek(min_offset)
|
||||
entry = self.read_entry(file)
|
||||
while file.tell() <= max_offset:
|
||||
if entry["id"] == entry_id:
|
||||
return entry
|
||||
entry = self.read_entry(file)
|
||||
|
||||
return None
|
||||
|
||||
def get_categories(self, file):
|
||||
file.seek(4)
|
||||
num_categories = self.read_int(file)
|
||||
for i in range(0, num_categories):
|
||||
yield self.read_entry(file)
|
||||
|
||||
def read_entry(self, file):
|
||||
return {"id": self.read_int(file), "offset": self.read_int(file)}
|
||||
|
||||
def read_int(self, file):
|
||||
return int.from_bytes(file.read(4), byteorder="little")
|
||||
|
||||
def read_string(self, file):
|
||||
message = bytearray()
|
||||
char = file.read(1)
|
||||
i = 0
|
||||
while char and char != b'\x00':
|
||||
i += 1
|
||||
message.append(ord(char))
|
||||
char = file.read(1)
|
||||
|
||||
return message.decode("utf-8")
|
||||
|
||||
def read_base_85(self, num_str):
|
||||
n = 0
|
||||
for i in range(0, 5):
|
||||
n = n * 85 + num_str[i] - 33
|
||||
return n
|
||||
|
||||
def parse_params(self, param_arr):
|
||||
args = []
|
||||
while len(param_arr) > 0:
|
||||
data_type = chr(param_arr[0])
|
||||
param_arr = param_arr[1:]
|
||||
if data_type == "S":
|
||||
size = param_arr[0] * 256 + param_arr[1]
|
||||
args.append(param_arr[2:2 + size].decode("utf-8"))
|
||||
param_arr = param_arr[2 + size:]
|
||||
elif data_type == "s":
|
||||
size = param_arr[0] - 1 # size is 1 less than indicated
|
||||
args.append(param_arr[1:1 + size].decode("utf-8"))
|
||||
param_arr = param_arr[1 + size:]
|
||||
elif data_type == "I":
|
||||
args.append(struct.unpack(">I", param_arr[:4])[0])
|
||||
param_arr = param_arr[4:]
|
||||
elif data_type == "i" or data_type == "u":
|
||||
args.append(self.read_base_85(param_arr[:5]))
|
||||
param_arr = param_arr[5:]
|
||||
elif data_type == "R":
|
||||
category_id = self.read_base_85(param_arr[:5])
|
||||
instance_id = self.read_base_85(param_arr[5:10])
|
||||
message = self.get_message_string(category_id, instance_id)
|
||||
if not message:
|
||||
raise Exception(f"Could not find message string for category "
|
||||
f"'{category_id}' and instance '{instance_id}'")
|
||||
args.append(message)
|
||||
param_arr = param_arr[10:]
|
||||
elif data_type == "l":
|
||||
category_id = 20000
|
||||
instance_id = struct.unpack(">I", param_arr[:4])[0]
|
||||
message = self.get_message_string(category_id, instance_id)
|
||||
if not message:
|
||||
raise Exception(f"Could not find message string for category "
|
||||
f"'{category_id}' and instance '{instance_id}'")
|
||||
args.append(message)
|
||||
param_arr = param_arr[4:]
|
||||
elif data_type == "~":
|
||||
break
|
||||
else:
|
||||
raise Exception("Unknown argument type '%s'" % data_type)
|
||||
|
||||
return args
|
||||
@@ -0,0 +1,95 @@
|
||||
import struct
|
||||
|
||||
|
||||
class UnknownArgumentType(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PacketMissingArgument(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def decode_args(types, data):
|
||||
args = []
|
||||
for argtype in types:
|
||||
if argtype == "I":
|
||||
elem, data = data[:4], data[4:]
|
||||
result = struct.unpack(">I", elem)[0]
|
||||
|
||||
elif argtype == "S":
|
||||
length = struct.unpack(">H", data[:2])[0]
|
||||
result = data[2:2 + length].decode("utf-8", "ignore")
|
||||
data = data[2 + length:]
|
||||
|
||||
elif argtype == "B":
|
||||
length = struct.unpack(">H", data[:2])[0]
|
||||
result = data[2:2 + length]
|
||||
data = data[2 + length:]
|
||||
|
||||
elif argtype == "G":
|
||||
result, data = data[:5], data[5:]
|
||||
# Convert result (5 bytes) to a long. Can't use
|
||||
# struct.unpack(">Q", "\x00"*3 + result), since we
|
||||
# can't rely on "long long" being available.
|
||||
high, low = struct.unpack(">BI", result)
|
||||
result = (high << 32) + low
|
||||
|
||||
elif argtype == "i":
|
||||
length = struct.unpack(">H", data[:2])[0]
|
||||
result = struct.unpack(">%sI" % length, data[2:2 + 4 * length])
|
||||
data = data[2 + 4 * length:]
|
||||
|
||||
elif argtype == "s":
|
||||
length = struct.unpack(">H", data[:2])[0]
|
||||
data = data[2:]
|
||||
result = []
|
||||
while length:
|
||||
slength = struct.unpack(">H", data[:2])[0]
|
||||
result.append(data[2:2 + slength].decode("utf-8"))
|
||||
data = data[2 + slength:]
|
||||
length -= 1
|
||||
|
||||
else:
|
||||
raise UnknownArgumentType(argtype)
|
||||
|
||||
args.append(result)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def encode_args(types, args):
|
||||
data = b""
|
||||
|
||||
for argtype in types:
|
||||
if not args:
|
||||
raise PacketMissingArgument
|
||||
|
||||
it = args[0]
|
||||
del args[0]
|
||||
|
||||
if argtype == "I":
|
||||
data += struct.pack(">I", it)
|
||||
|
||||
elif argtype == "S":
|
||||
encoded = it.encode("utf-8")
|
||||
data += struct.pack(">H", len(encoded))
|
||||
data += encoded
|
||||
|
||||
elif argtype == "G":
|
||||
data += struct.pack(">BI", it >> 32, it & 0xffffffff)
|
||||
|
||||
elif argtype == "s":
|
||||
data += struct.pack(">H", len(it))
|
||||
for it_elem in it:
|
||||
encoded = it_elem.encode("utf-8")
|
||||
data += struct.pack(">H", len(encoded))
|
||||
data += encoded
|
||||
|
||||
else:
|
||||
raise UnknownArgumentType(argtype)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class Packet:
|
||||
pass
|
||||
@@ -0,0 +1,456 @@
|
||||
from core.aochat.extended_message import ExtendedMessage
|
||||
from core.aochat.packets import *
|
||||
|
||||
|
||||
class ServerPacket(Packet):
|
||||
def __init__(self, packet_id, types, args):
|
||||
self.id = packet_id
|
||||
self.types = types
|
||||
self.args = args
|
||||
|
||||
def to_bytes(self):
|
||||
return encode_args(self.types, self.args)
|
||||
|
||||
def __str__(self):
|
||||
return "ServerPacket(%d): %s" % (self.id, self.args)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, packet_id, data):
|
||||
if packet_id == LoginSeed.id:
|
||||
return LoginSeed.from_bytes(data)
|
||||
elif packet_id == LoginOK.id:
|
||||
return LoginOK.from_bytes(data)
|
||||
elif packet_id == LoginError.id:
|
||||
return LoginError.from_bytes(data)
|
||||
elif packet_id == LoginCharacterList.id:
|
||||
return LoginCharacterList.from_bytes(data)
|
||||
elif packet_id == CharacterUnknown.id:
|
||||
return CharacterUnknown.from_bytes(data)
|
||||
elif packet_id == CharacterName.id:
|
||||
return CharacterName.from_bytes(data)
|
||||
elif packet_id == CharacterLookup.id:
|
||||
return CharacterLookup.from_bytes(data)
|
||||
elif packet_id == PrivateMessage.id:
|
||||
return PrivateMessage.from_bytes(data)
|
||||
elif packet_id == VicinityMessage.id:
|
||||
return VicinityMessage.from_bytes(data)
|
||||
elif packet_id == BroadcastMessage.id:
|
||||
return BroadcastMessage.from_bytes(data)
|
||||
elif packet_id == SimpleSystemMessage.id:
|
||||
return SimpleSystemMessage.from_bytes(data)
|
||||
elif packet_id == SystemMessage.id:
|
||||
return SystemMessage.from_bytes(data)
|
||||
elif packet_id == BuddyAdded.id:
|
||||
return BuddyAdded.from_bytes(data)
|
||||
elif packet_id == BuddyRemoved.id:
|
||||
return BuddyRemoved.from_bytes(data)
|
||||
elif packet_id == PrivateChannelInvited.id:
|
||||
return PrivateChannelInvited.from_bytes(data)
|
||||
elif packet_id == PrivateChannelKicked.id:
|
||||
return PrivateChannelKicked.from_bytes(data)
|
||||
elif packet_id == PrivateChannelLeft.id:
|
||||
return PrivateChannelLeft.from_bytes(data)
|
||||
elif packet_id == PrivateChannelClientJoined.id:
|
||||
return PrivateChannelClientJoined.from_bytes(data)
|
||||
elif packet_id == PrivateChannelClientLeft.id:
|
||||
return PrivateChannelClientLeft.from_bytes(data)
|
||||
elif packet_id == PrivateChannelMessage.id:
|
||||
return PrivateChannelMessage.from_bytes(data)
|
||||
elif packet_id == PrivateChannelInviteRefused.id:
|
||||
return PrivateChannelInviteRefused.from_bytes(data)
|
||||
elif packet_id == PublicChannelJoined.id:
|
||||
return PublicChannelJoined.from_bytes(data)
|
||||
elif packet_id == PublicChannelLeft.id:
|
||||
return PublicChannelLeft.from_bytes(data)
|
||||
elif packet_id == PublicChannelMessage.id:
|
||||
return PublicChannelMessage.from_bytes(data)
|
||||
elif packet_id == Pong.id:
|
||||
return Pong.from_bytes(data)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class LoginSeed(ServerPacket):
|
||||
id = 0
|
||||
types = "S"
|
||||
|
||||
def __init__(self, seed):
|
||||
self.seed = seed
|
||||
super().__init__(self.id, self.types, [self.seed])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class LoginOK(ServerPacket):
|
||||
id = 5
|
||||
types = ""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self.id, self.types, [])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
return cls()
|
||||
|
||||
|
||||
class LoginError(ServerPacket):
|
||||
id = 6
|
||||
types = "S"
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
super().__init__(self.id, self.types, [self.message])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class LoginCharacterList(ServerPacket):
|
||||
id = 7
|
||||
types = "isii"
|
||||
|
||||
def __init__(self, char_ids, names, levels, online_statuses):
|
||||
self.char_ids = char_ids
|
||||
self.names = names
|
||||
self.levels = levels
|
||||
self.online_statuses = online_statuses
|
||||
super().__init__(self.id, self.types, [self.char_ids, self.names, self.levels, self.online_statuses])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class CharacterUnknown(ServerPacket):
|
||||
id = 10
|
||||
types = "I"
|
||||
|
||||
def __init__(self, char_id):
|
||||
self.char_id = char_id
|
||||
super().__init__(self.id, self.types, [self.char_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class CharacterName(ServerPacket):
|
||||
id = 20
|
||||
types = "IS"
|
||||
|
||||
def __init__(self, char_id, name):
|
||||
self.char_id = char_id
|
||||
self.name = name
|
||||
super().__init__(self.id, self.types, [self.char_id, self.name])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class CharacterLookup(ServerPacket):
|
||||
id = 21
|
||||
types = "IS"
|
||||
|
||||
def __init__(self, char_id, name):
|
||||
self.char_id = char_id
|
||||
self.name = name
|
||||
super().__init__(self.id, self.types, [self.char_id, self.name])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateMessage(ServerPacket):
|
||||
id = 30
|
||||
types = "ISS"
|
||||
|
||||
def __init__(self, char_id, message, blob):
|
||||
self.char_id = char_id
|
||||
self.message = message
|
||||
self.blob = blob
|
||||
super().__init__(self.id, self.types, [self.char_id, self.message, self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class VicinityMessage(ServerPacket):
|
||||
id = 34
|
||||
types = "ISS"
|
||||
|
||||
def __init__(self, char_id, message, blob):
|
||||
self.char_id = char_id
|
||||
self.message = message
|
||||
self.blob = blob
|
||||
super().__init__(self.id, self.types, [self.char_id, self.message, self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class BroadcastMessage(ServerPacket):
|
||||
id = 35
|
||||
types = "SSS"
|
||||
|
||||
def __init__(self, text, message, blob):
|
||||
self.text = text
|
||||
self.message = message
|
||||
self.blob = blob
|
||||
super().__init__(self.id, self.types, [self.text, self.message, self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class SimpleSystemMessage(ServerPacket):
|
||||
id = 36
|
||||
types = "S"
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
super().__init__(self.id, self.types, [self.message])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class SystemMessage(ServerPacket):
|
||||
id = 37
|
||||
types = "IIIB"
|
||||
|
||||
def __init__(self, client_id, window_id, message_id, message_args):
|
||||
self.client_id = client_id
|
||||
self.window_id = window_id
|
||||
self.message_id = message_id
|
||||
self.message_args = message_args
|
||||
# noinspection PyTypeChecker
|
||||
self.extended_message: ExtendedMessage = None
|
||||
super().__init__(self.id, self.types, [self.client_id, self.window_id, self.message_id, self.message_args])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
def __str__(self):
|
||||
return super().__str__() + ", ExtendedMessage: %s" % self.extended_message
|
||||
|
||||
|
||||
class BuddyAdded(ServerPacket):
|
||||
id = 40
|
||||
types = "IIS"
|
||||
|
||||
def __init__(self, char_id, online, status):
|
||||
self.char_id = char_id
|
||||
self.online = online
|
||||
self.status = status
|
||||
super().__init__(self.id, self.types, [self.char_id, self.online, self.status])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class BuddyRemoved(ServerPacket):
|
||||
id = 41
|
||||
types = "I"
|
||||
|
||||
def __init__(self, char_id):
|
||||
self.char_id = char_id
|
||||
super().__init__(self.id, self.types, [self.char_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelInvited(ServerPacket):
|
||||
id = 50
|
||||
types = "I"
|
||||
|
||||
def __init__(self, private_channel_id):
|
||||
self.private_channel_id = private_channel_id
|
||||
super().__init__(self.id, self.types, [self.private_channel_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelKicked(ServerPacket):
|
||||
id = 51
|
||||
types = "I"
|
||||
|
||||
def __init__(self, private_channel_id):
|
||||
self.private_channel_id = private_channel_id
|
||||
super().__init__(self.id, self.types, [self.private_channel_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelLeft(ServerPacket):
|
||||
id = 53
|
||||
types = "I"
|
||||
|
||||
def __init__(self, private_channel_id):
|
||||
self.private_channel_id = private_channel_id
|
||||
super().__init__(self.id, self.types, [self.private_channel_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelClientJoined(ServerPacket):
|
||||
id = 55
|
||||
types = "II"
|
||||
|
||||
def __init__(self, private_channel_id, char_id):
|
||||
self.private_channel_id = private_channel_id
|
||||
self.char_id = char_id
|
||||
super().__init__(self.id, self.types, [self.private_channel_id, self.char_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelClientLeft(ServerPacket):
|
||||
id = 56
|
||||
types = "II"
|
||||
|
||||
def __init__(self, private_channel_id, char_id):
|
||||
self.private_channel_id = private_channel_id
|
||||
self.char_id = char_id
|
||||
super().__init__(self.id, self.types, [self.private_channel_id, self.char_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelMessage(ServerPacket):
|
||||
id = 57
|
||||
types = "IISS"
|
||||
|
||||
def __init__(self, private_channel_id, char_id, message, blob):
|
||||
self.private_channel_id = private_channel_id
|
||||
self.char_id = char_id
|
||||
self.message = message
|
||||
self.blob = blob
|
||||
super().__init__(self.id, self.types, [self.private_channel_id, self.char_id, self.message, self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PrivateChannelInviteRefused(ServerPacket):
|
||||
id = 58
|
||||
types = "II"
|
||||
|
||||
def __init__(self, private_channel_id, char_id):
|
||||
self.private_channel_id = private_channel_id
|
||||
self.char_id = char_id
|
||||
super().__init__(self.id, self.types, [self.private_channel_id, self.char_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PublicChannelJoined(ServerPacket):
|
||||
id = 60
|
||||
types = "GSIS"
|
||||
|
||||
def __init__(self, channel_id, name, unknown, flags):
|
||||
self.channel_id = channel_id
|
||||
self.name = name
|
||||
self.unknown = unknown
|
||||
self.flags = flags
|
||||
super().__init__(self.id, self.types, [self.channel_id, self.name, self.unknown, self.flags])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PublicChannelLeft(ServerPacket):
|
||||
id = 61
|
||||
types = "G"
|
||||
|
||||
def __init__(self, channel_id):
|
||||
self.channel_id = channel_id
|
||||
super().__init__(self.id, self.types, [self.channel_id])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class PublicChannelMessage(ServerPacket):
|
||||
id = 65
|
||||
types = "GISS"
|
||||
|
||||
def __init__(self, channel_id, char_id, message, blob):
|
||||
self.channel_id = channel_id
|
||||
self.char_id = char_id
|
||||
self.message = message
|
||||
self.blob = blob
|
||||
# noinspection PyTypeChecker
|
||||
self.extended_message: ExtendedMessage = None
|
||||
super().__init__(self.id, self.types, [self.channel_id, self.char_id, self.message, self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
|
||||
def __str__(self):
|
||||
return super().__str__() + ", ExtendedMessage: %s" % self.extended_message
|
||||
|
||||
|
||||
class Pong(ServerPacket):
|
||||
id = 100
|
||||
types = "S"
|
||||
|
||||
def __init__(self, blob):
|
||||
self.blob = blob
|
||||
super().__init__(self.id, self.types, [self.blob])
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
args = decode_args(cls.types, data)
|
||||
return cls(*args)
|
||||
Reference in New Issue
Block a user