summaryrefslogtreecommitdiff
path: root/asciifarmclient/common
diff options
context:
space:
mode:
Diffstat (limited to 'asciifarmclient/common')
-rw-r--r--asciifarmclient/common/__init__.py0
-rw-r--r--asciifarmclient/common/messages.py157
-rw-r--r--asciifarmclient/common/tcommunicate.py32
-rw-r--r--asciifarmclient/common/utils.py47
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]