diff --git a/sampy/client/__init__.py b/sampy/client/__init__.py index 88a7dae..10f3c35 100644 --- a/sampy/client/__init__.py +++ b/sampy/client/__init__.py @@ -1,52 +1,6 @@ -import struct -import socket -import asyncio -from time import time - from . import base from . import query from . import player +from . import client -STATE_UNKNOWN = (0, base.BaseClient) -STATE_QUERY = (1, query.QueryClient) -STATE_PLAYER = (2, player.PlayerClient) - -TIMEOUT = 10 # assume connection is closed after 10 seconds of inactivity (change this to a higher value so you dont timeout while debugging might be a good idea) - -class Client: - def __init__(self, server: "__ServerInstance__", ip: str, port: int): - self.server = server - self.ip = ip - self.port = port - - self.set_state(STATE_UNKNOWN) - - self.last_active = time() - self.keep_alive_task = asyncio.create_task( self.keep_alive() ) - self.connected = True # keep_alive will set this to False if connection has not been interacted with for a while (allowing server loop to remove their reference) - - def set_state(self, state: tuple): - #self.keep_alive_task.cancel() - self.state = state - self.client = self.state[1](self.server, self.ip, self.port) - - async def on_packet(self, packet: bytes): - self.last_active = time() - - if self.state == STATE_UNKNOWN: - # We are currently unaware if this is a player client or query client, but we got a packet that will be our check to know - if packet.startswith(b"SAMP"): - self.set_state(STATE_QUERY) - else: - self.set_state(STATE_PLAYER) - - await self.client.on_packet(packet) - - async def keep_alive(self): # Maybe bad name for this method as it rather checks if connection is dropped - while True: - timestamp = time() - if self.last_active + TIMEOUT - timestamp < 0: - self.connected = False - return - - await asyncio.sleep(self.last_active + TIMEOUT - timestamp) \ No newline at end of file +from .client import Client diff --git a/sampy/client/base.py b/sampy/client/base.py index 59bda42..76fedae 100644 --- a/sampy/client/base.py +++ b/sampy/client/base.py @@ -4,6 +4,7 @@ import struct import logging logger = logging.getLogger(__name__) + class BaseClient: def __init__(self, server: "__ServerInstance__", ip: str, port: int): self.server = server diff --git a/sampy/client/client.py b/sampy/client/client.py new file mode 100644 index 0000000..0b5fe97 --- /dev/null +++ b/sampy/client/client.py @@ -0,0 +1,53 @@ +import struct +import socket +import asyncio +from time import time + +from . import base +from . import query +from . import player + +STATE_UNKNOWN = (0, base.BaseClient) +STATE_QUERY = (1, query.QueryClient) +STATE_PLAYER = (2, player.PlayerClient) + +TIMEOUT = 10 # assume connection is closed after 10 seconds of inactivity (change this to a higher value so you dont timeout while debugging might be a good idea) + + +class Client: + def __init__(self, server: "__ServerInstance__", ip: str, port: int): + self.server = server + self.ip = ip + self.port = port + + self.set_state(STATE_UNKNOWN) + + self.last_active = time() + self.keep_alive_task = asyncio.create_task(self.keep_alive()) + self.connected = True # keep_alive will set this to False if connection has not been interacted with for a while (allowing server loop to remove their reference) + + def set_state(self, state: tuple): + # self.keep_alive_task.cancel() + self.state = state + self.client = self.state[1](self.server, self.ip, self.port) + + async def on_packet(self, packet: bytes): + self.last_active = time() + + if self.state == STATE_UNKNOWN: + # We are currently unaware if this is a player client or query client, but we got a packet that will be our check to know + if packet.startswith(b"SAMP"): + self.set_state(STATE_QUERY) + else: + self.set_state(STATE_PLAYER) + + await self.client.on_packet(packet) + + async def keep_alive(self): # Maybe bad name for this method as it rather checks if connection is dropped + while True: + timestamp = time() + if self.last_active + TIMEOUT - timestamp < 0: + self.connected = False + return + + await asyncio.sleep(self.last_active + TIMEOUT - timestamp) \ No newline at end of file diff --git a/sampy/client/player.py b/sampy/client/player.py index b5d45ef..2bd6018 100644 --- a/sampy/client/player.py +++ b/sampy/client/player.py @@ -10,6 +10,7 @@ logger = logging.getLogger(__name__) STATE_CONNECTING = 0 STATE_CONNECTED = 1 + class PlayerClient(BaseClient): def __init__(self, server: "__ServerInstance__", ip: str, port: int): super().__init__(server, ip, port) diff --git a/sampy/client/query.py b/sampy/client/query.py index 3bb6522..8d8b1ae 100644 --- a/sampy/client/query.py +++ b/sampy/client/query.py @@ -7,6 +7,7 @@ from ..shared import glob import logging logger = logging.getLogger(__name__) + class QueryClient(BaseClient): def __init__(self, server: "__ServerInstance__", ip: str, port: int): super().__init__(server, ip, port) diff --git a/sampy/env.py b/sampy/env.py index 5ae7a12..ec89327 100644 --- a/sampy/env.py +++ b/sampy/env.py @@ -4,6 +4,7 @@ from threading import Thread from .struct.server import ServerConfig from .server import Server + class Environment(Thread): def __init__(self, config: ServerConfig): super().__init__() diff --git a/sampy/raknet/__init__.py b/sampy/raknet/__init__.py new file mode 100644 index 0000000..b3deb28 --- /dev/null +++ b/sampy/raknet/__init__.py @@ -0,0 +1 @@ +from . import bitstream \ No newline at end of file diff --git a/sampy/raknet/bitstream.py b/sampy/raknet/bitstream.py index c7a75e2..4c6c948 100644 --- a/sampy/raknet/bitstream.py +++ b/sampy/raknet/bitstream.py @@ -7,6 +7,7 @@ from typing import * import logging logger = logging.getLogger(__name__) + class Bitstream: def __init__(self, data: bytes = b""): self._offset = 0 diff --git a/sampy/server/__init__.py b/sampy/server/__init__.py index c0504b5..7dd740a 100644 --- a/sampy/server/__init__.py +++ b/sampy/server/__init__.py @@ -1,64 +1,4 @@ -import socket -import asyncio -from select import select # This is straight up magic +from . import compression +from . import server -from ..struct.server import ServerConfig -from ..client import Client - -from .compression import Compressor - -import logging -logger = logging.getLogger(__name__) - -class Server: - def __init__(self, config: ServerConfig): - self.config = config - self.clients = {} - self.rcon_clients = {} - - self.compressor = Compressor(self.config) - - async def create_socket(self): - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.bind((self.config.host, self.config.port)) - self.socket.setblocking(0) - logger.debug("Socket created") - - async def on_command(self, cmd: str): - logger.debug("on_command(%s)" % cmd) - # TODO: When commands return a reponse we also want to forward this to potential rcon clients - - async def get_online_players(self): # TODO: Get data from server's client objects - return [ - {"nick": b"Sunpy", "score": 64, "ping": 8, "id": 1} # replace id with function to get player's id - ] - - async def get_rules(self): # TODO - return {b"Rule name sample": b"Rule value", b"weburl": b"https://git.osufx.com/Sunpy/sampy"} - - async def get_players_scores(self): # TODO - return {b"Sunpy": 64, b"username": 123} - - async def main(self): - await self.create_socket() - - while True: - (incomming, _, _) = select([self.socket], [], [], 0) # How this works is beyond me, but this sets `incomming` to be true~y if socket has awaiting data - - if incomming: - data, addr = self.socket.recvfrom(0xFFFF) - - if addr not in self.clients: - ip, port = addr - self.clients[addr] = Client(self, ip, port) - await self.clients[addr].on_packet(data) - - disconnected = [c for c in self.clients.values() if c.connected == False] - for c in disconnected: # Remove dead connections - addr = (c.ip, c.port) - if addr in self.clients: - del self.clients[addr] - logger.debug("free(%s)" % c) - - await asyncio.sleep(0) +from .server import Server diff --git a/sampy/server/compression.py b/sampy/server/compression.py index cbea019..06a81e1 100644 --- a/sampy/server/compression.py +++ b/sampy/server/compression.py @@ -7,6 +7,7 @@ logger = logging.getLogger(__name__) # Found @ addr 0x004C88E0 LOOKUP_TABLE = b"\xb4b\x07\xe5\x9d\xafc\xdd\xe3\xd0\xcc\xfe\xdc\xdbk.j@\xabG\xc9\xd1S\xd5 \x91\xa5\x0eJ\xdf\x18\x89\xfdo%\x12\xb7\x13w\x00e6mI\xecW*\xa9\x11_\xfax\x95\xa4\xbd\x1e\xd9yD\xcd\xde\x81\xeb\t>\xf6\xee\xda\x7f\xa3\x1a\xa7-\xa6\xad\xc1F\x93\xd2\x1b\x9c\xaa\xd7NKML\xf3\xb84\xc0\xca\x88\xf4\x94\xcb\x0490\x82\xd6s\xb0\xbf\"\x01AnH,\xa8u\xb1\n\xae\x9f\'\x80\x10\xce\xf0)(\x85\r\x05\xf75\xbb\xbc\x15\x06\xf5`q\x03\x1f\xeaZ3\x92\x8d\xe7\x90[\xe9\xcf\x9e\xd3]\xed1\x1c\x0bR\x16Q\x0f\x86\xc5h\x9b!\x0c\x8bB\x87\xffO\xbe\xc8\xe8\xc7\xd4z\xe0U/\x8a\x8e\xba\x987\xe4\xb28\xa1\xb62\x83:{\x84 int: return (self.config.port ^ 0xCCCC) & 0xFF + class StringCompressor: def __init__(self): pass diff --git a/sampy/server/server.py b/sampy/server/server.py new file mode 100644 index 0000000..f53b7bf --- /dev/null +++ b/sampy/server/server.py @@ -0,0 +1,65 @@ +import socket +import asyncio +from select import select # This is straight up magic + +from ..struct.server import ServerConfig +from ..client import Client + +from .compression import Compressor + +import logging +logger = logging.getLogger(__name__) + + +class Server: + def __init__(self, config: ServerConfig): + self.config = config + self.clients = {} + self.rcon_clients = {} + + self.compressor = Compressor(self.config) + + async def create_socket(self): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind((self.config.host, self.config.port)) + self.socket.setblocking(0) + logger.debug("Socket created") + + async def on_command(self, cmd: str): + logger.debug("on_command(%s)" % cmd) + # TODO: When commands return a reponse we also want to forward this to potential rcon clients + + async def get_online_players(self): # TODO: Get data from server's client objects + return [ + {"nick": b"Sunpy", "score": 64, "ping": 8, "id": 1} # replace id with function to get player's id + ] + + async def get_rules(self): # TODO + return {b"Rule name sample": b"Rule value", b"weburl": b"https://git.osufx.com/Sunpy/sampy"} + + async def get_players_scores(self): # TODO + return {b"Sunpy": 64, b"username": 123} + + async def main(self): + await self.create_socket() + + while True: + (incomming, _, _) = select([self.socket], [], [], 0) # How this works is beyond me, but this sets `incomming` to be true~y if socket has awaiting data + + if incomming: + data, addr = self.socket.recvfrom(0xFFFF) + + if addr not in self.clients: + ip, port = addr + self.clients[addr] = Client(self, ip, port) + await self.clients[addr].on_packet(data) + + disconnected = [c for c in self.clients.values() if c.connected == False] + for c in disconnected: # Remove dead connections + addr = (c.ip, c.port) + if addr in self.clients: + del self.clients[addr] + logger.debug("free(%s)" % c) + + await asyncio.sleep(0)