diff options
| author | troido <troido@protonmail.com> | 2020-03-06 11:26:59 +0100 |
|---|---|---|
| committer | troido <troido@protonmail.com> | 2020-03-06 11:26:59 +0100 |
| commit | dd07ff4d686f07cdc9736627dd0ef099ef5e4e4f (patch) | |
| tree | f43310b3779a95efd74a40ba8139cf146ad49051 /asciifarmclient/common | |
| parent | c9366616079240cd7ee3d243c9f6897d40b4267d (diff) | |
new directory structure for the separate client repo
Diffstat (limited to 'asciifarmclient/common')
| -rw-r--r-- | asciifarmclient/common/__init__.py | 0 | ||||
| -rw-r--r-- | asciifarmclient/common/messages.py | 157 | ||||
| -rw-r--r-- | asciifarmclient/common/tcommunicate.py | 32 | ||||
| -rw-r--r-- | asciifarmclient/common/utils.py | 47 |
4 files changed, 236 insertions, 0 deletions
diff --git a/asciifarmclient/common/__init__.py b/asciifarmclient/common/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/asciifarmclient/common/__init__.py diff --git a/asciifarmclient/common/messages.py b/asciifarmclient/common/messages.py new file mode 100644 index 0000000..d3aed9a --- /dev/null +++ b/asciifarmclient/common/messages.py @@ -0,0 +1,157 @@ + +import re +import unicodedata +import json + +class InvalidMessageError(Exception): + errType = "invalidmessage" + description = "" + + def __init__(self, description="", errType=None): + self.description = description + if errType is not None: + self.errType = errType + + def toMessage(self): + return ErrorMessage(self.errType, self.description) + +class InvalidNameError(InvalidMessageError): + errType = "invalidname" + +class Message: + + @classmethod + def msgType(cls): + return cls.typename + + def to_json(self): + raise NotImplementedError + + def to_json_bytes(self): + return bytes(json.dumps(self.to_json()), "utf-8") + + @classmethod + def from_json(cls, jsonobj): + raise NotImplementedError + +class ClientToServerMessage(Message): + + def body(self): + raise NotImplementedError + + def to_json(self): + return [self.typename, self.body()] + + @classmethod + def from_json(cls, jsonlist): + assert len(jsonlist) == 2, InvalidMessageError + typename, body = jsonlist + assert typename == cls.msgType(), InvalidMessageError + return cls(body) + + +class NameMessage(ClientToServerMessage): + + typename = "name" + categories = {"Lu", "Ll", "Lt", "Lm", "Lo", "Nd", "Nl", "No", "Pc"} + + + def __init__(self, name): + assert isinstance(name, str), InvalidNameError("name must be a string") + assert (len(name) > 0), InvalidNameError("name needs at least one character") + assert (len(bytes(name, "utf-8")) <= 256), InvalidNameError("name may not be longer than 256 utf8 bytes") + if name[0] != "~": + for char in name: + category = unicodedata.category(char) + assert category in self.categories, InvalidNameError("all name caracters must be in these unicode categories: " + "|".join(self.categories) + " (except for tildenames)") + self.name = name + + def body(self): + return self.name + + +class InputMessage(ClientToServerMessage): + + typename = "input" + + def __init__(self, inp): + self.inp = inp + + def body(self): + return self.inp + +class ChatMessage(ClientToServerMessage): + + typename = "chat" + + def __init__(self, text): + assert isinstance(text, str), InvalidMessageError("chat message must be a string") + assert text.isprintable(), InvalidMessageError("chat messages may only contain printable unicode characters") + self.text = text + + def body(self): + return self.text + + + +class ServerToClientMessage(Message): + msglen = 0 + + + @classmethod + def from_json(cls, jsonlist): + assert len(jsonlist) == cls.msglen, InvalidMessageError + assert jsonlist[0] == cls.msgType(), InvalidMessageError + return cls(*jsonlist[1:]) + + +class MessageMessage(ServerToClientMessage): # this name feels stupid + """ A message to inform the client. This is meant to be read by the user""" + + typename = "message" + msglen = 3 + + def __init__(self, text, type=""): + self.text = text + self.type = type + + def to_json(self): + return [self.typename, self.text, self.type] + + +class WorldMessage(ServerToClientMessage): + """ A message about the world state """ + + typename = "world" + msglen = 2 + + def __init__(self, updates): + assert isinstance(updates, list), InvalidMessageError + self.updates = updates + + def to_json(self): + return [self.typename, self.updates] + +class ErrorMessage(ServerToClientMessage): + + typename = "error" + msglen = 3 + + def __init__(self, errType, description=""): + self.errType = errType + self.description = description + + def to_json(self): + return [self.typename, self.errType, self.description] + + + +messages = {message.msgType(): message for message in [ + NameMessage, + InputMessage, + ChatMessage, + WorldMessage, + ErrorMessage, + MessageMessage +]} + diff --git a/asciifarmclient/common/tcommunicate.py b/asciifarmclient/common/tcommunicate.py new file mode 100644 index 0000000..b1fc1b0 --- /dev/null +++ b/asciifarmclient/common/tcommunicate.py @@ -0,0 +1,32 @@ + +HEADER_SIZE = 4 + + +# this module is for sending discree messages over TCP +# this is achieved by prefixing all messages with their length +# calls to send and recv will also keep attempting to send all data unless this proves impossible + + +def send(sock, msg): + length = len(msg) + header = length.to_bytes(4, byteorder="big") + totalmsg = header + msg + sock.sendall(totalmsg) + +def receive(sock): + header = recvall(sock, 4) #sock.recv(4) + length = int.from_bytes(header, byteorder="big") + return recvall(sock, length) + +def recvall(sock, length): + chunks = [] + bytes_recd = 0 + while bytes_recd < length: + chunk = sock.recv(min(length - bytes_recd, 4096)) + if chunk == b'': + break + #raise RuntimeError("socket connection broken") + chunks.append(chunk) + bytes_recd = bytes_recd + len(chunk) + return b''.join(chunks) + diff --git a/asciifarmclient/common/utils.py b/asciifarmclient/common/utils.py new file mode 100644 index 0000000..95ac32b --- /dev/null +++ b/asciifarmclient/common/utils.py @@ -0,0 +1,47 @@ + +import os + +def clamp(val, lower, upper): + """ val if it's between lower and upper, else the closest of the two""" + return max(min(val, upper), lower) + + +def concat(arr): + """Takes a list of sequences, returns the concatenation of the sequences """ + if isinstance(arr[0], str): + return "".join(arr) + if isinstance(arr[0], bytes): + return b"".join(arr) + if isinstance(arr[0], list): + l = [] + for s in arr: + l += s + return l + if isinstance(arr[0], tuple): + l = [] + for s in arr: + l += s + return tuple(l) + else: + raise ValueError("type {} can't be concatenated".format(type(arr[0]))) + + +def writeFileSafe(filename, data, tempname=None): + if tempname is None: + tempname = filename + ".tempfile" + with open(tempname, 'w') as f: + f.write(data) + os.rename(tempname, filename) + + +def readFile(filepath): + with open(filepath, "r") as f: + text = f.read() + return text + + +def get(collection, i, default=None): + """ Get an element in an indexed collection, or the default in case the index is out of bounds """ + if i < 0 or i >= len(collection): + return default + return collection[i] |
