client -> server messages are now objects; better name validation; fixed concurrency bug
This commit is contained in:
parent
aa9f1913c8
commit
54c7238f2d
|
@ -17,7 +17,6 @@ class CommandHandler:
|
|||
self.client = client
|
||||
|
||||
self.commands = {
|
||||
"send": self.send,
|
||||
"input": self.input,
|
||||
"move": self.move,
|
||||
"say": self.say,
|
||||
|
@ -65,11 +64,8 @@ class CommandHandler:
|
|||
|
||||
# Commands
|
||||
|
||||
def send(self, data):
|
||||
self.client.send(data)
|
||||
|
||||
def input(self, action):
|
||||
self.send(["input", action])
|
||||
self.client.sendInput(action)
|
||||
|
||||
def move(self, direction):
|
||||
self.input(["move", direction])
|
||||
|
@ -81,7 +77,7 @@ class CommandHandler:
|
|||
self.input(["pick", option])
|
||||
|
||||
def chat(self, text):
|
||||
self.send(["chat", text])
|
||||
self.client.sendChat( text)
|
||||
|
||||
|
||||
def log(self, text):
|
||||
|
|
|
@ -13,6 +13,7 @@ from queue import Queue
|
|||
import ratuil.inputs
|
||||
|
||||
from .inputhandler import InputHandler
|
||||
from asciifarm.common import messages
|
||||
|
||||
class Client:
|
||||
|
||||
|
@ -33,15 +34,24 @@ class Client:
|
|||
self.queue = Queue()
|
||||
|
||||
|
||||
def send(self, data):
|
||||
text = json.dumps(data)
|
||||
self.connection.send(text)
|
||||
def sendMessage(self, message):
|
||||
self.connection.send(json.dumps(message.to_json()))
|
||||
|
||||
def sendInput(self, inp):
|
||||
message = messages.InputMessage(inp)
|
||||
self.sendMessage(message)
|
||||
|
||||
def sendChat(self, text):
|
||||
try:
|
||||
self.sendMessage(messages.ChatMessage(text))
|
||||
except messages.InvalidMessageError as e:
|
||||
self.log(e.description)
|
||||
|
||||
def start(self):
|
||||
self.sendMessage(messages.NameMessage(self.name))
|
||||
threading.Thread(target=self.listen, daemon=True).start()
|
||||
threading.Thread(target=self.getInput, daemon=True).start()
|
||||
|
||||
self.connection.send(json.dumps(["name", self.name]))
|
||||
self.command_loop()
|
||||
|
||||
def listen(self):
|
||||
|
@ -79,48 +89,53 @@ class Client:
|
|||
self.close("error: name is already taken")
|
||||
return
|
||||
if error == "invalidname":
|
||||
self.close("Invalid name error: "+ msg[2:])
|
||||
self.close("Invalid name error: "+ str(msg[2:]))
|
||||
return
|
||||
self.log(" ".join(msg[1:]))
|
||||
if msgType == 'field':
|
||||
field = msg[1]
|
||||
fieldWidth = field['width']
|
||||
fieldHeight = field['height']
|
||||
self.display.resizeField((fieldWidth, fieldHeight))
|
||||
fieldCells = field['field']
|
||||
mapping = field['mapping']
|
||||
self.display.drawFieldCells(
|
||||
(
|
||||
tuple(reversed(divmod(i, fieldWidth))),
|
||||
mapping[spr]
|
||||
)
|
||||
for i, spr in enumerate(fieldCells))
|
||||
|
||||
if msgType == 'changecells' and len(msg[1]):
|
||||
self.display.drawFieldCells(msg[1])
|
||||
|
||||
if msgType == "playerpos":
|
||||
self.display.setFieldCenter(msg[1])
|
||||
|
||||
if msgType == "health":
|
||||
health, maxHealth = msg[1]
|
||||
self.display.setHealth(health, maxHealth)
|
||||
if maxHealth is None:
|
||||
self.log("You have died. Restart the client to respawn")
|
||||
if msgType == "inventory":
|
||||
self.display.setInventory(msg[1])
|
||||
if msgType == "equipment":
|
||||
self.display.setEquipment(msg[1])
|
||||
if msgType == "ground":
|
||||
self.display.setGround(msg[1])
|
||||
if msgType == "message":
|
||||
self.log(*msg[1:])
|
||||
if msgType == "options":
|
||||
if msg[1] != None:
|
||||
description, options = msg[1]
|
||||
self.log(description)
|
||||
for option in options:
|
||||
self.log(option)
|
||||
if msgType == "world":
|
||||
for msg in msg[1]:
|
||||
msgType = msg[0]
|
||||
if msgType == 'field':
|
||||
field = msg[1]
|
||||
fieldWidth = field['width']
|
||||
fieldHeight = field['height']
|
||||
self.display.resizeField((fieldWidth, fieldHeight))
|
||||
fieldCells = field['field']
|
||||
mapping = field['mapping']
|
||||
self.display.drawFieldCells(
|
||||
(
|
||||
tuple(reversed(divmod(i, fieldWidth))),
|
||||
mapping[spr]
|
||||
)
|
||||
for i, spr in enumerate(fieldCells))
|
||||
|
||||
if msgType == 'changecells' and len(msg[1]):
|
||||
self.display.drawFieldCells(msg[1])
|
||||
|
||||
if msgType == "playerpos":
|
||||
self.display.setFieldCenter(msg[1])
|
||||
|
||||
if msgType == "health":
|
||||
health, maxHealth = msg[1]
|
||||
self.display.setHealth(health, maxHealth)
|
||||
if maxHealth is None:
|
||||
self.log("You have died. Restart the client to respawn")
|
||||
if msgType == "inventory":
|
||||
self.display.setInventory(msg[1])
|
||||
if msgType == "equipment":
|
||||
self.display.setEquipment(msg[1])
|
||||
if msgType == "ground":
|
||||
self.display.setGround(msg[1])
|
||||
if msgType == "message":
|
||||
self.log(*msg[1:])
|
||||
if msgType == "options":
|
||||
if msg[1] != None:
|
||||
description, options = msg[1]
|
||||
self.log(description)
|
||||
for option in options:
|
||||
self.log(option)
|
||||
|
||||
|
||||
def log(self, text, type=None):
|
||||
|
|
|
@ -23,7 +23,7 @@ def parse_args(argv):
|
|||
Kill the goblins and plant the seeds.
|
||||
|
||||
~troido""", formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('-n', '--name', help='Your player name (must be unique!). Defaults to username on inet sockets and tildename on (unix socket (including abstract)', default=None)
|
||||
parser.add_argument('-n', '--name', help='Your player name (must be unique!). Defaults to username on inet sockets and tildename on unix socket (including abstract). Apart from the tilde in a tildename all characters must be unicode letters, numbers or connection puctuation. The maximum size of a name is 256 bytes when encoded as utf8', default=None)
|
||||
parser.add_argument("-a", "--address", help="The address of the socket. When the socket type is 'abstract' this is just a name. When it is 'unix' this is a filename. When it is 'inet' is should be in the format 'address:port', eg 'localhost:8080'. Defaults depends on the socket type")
|
||||
parser.add_argument("-s", "--socket", help="the socket type. 'unix' is unix domain sockets, 'abstract' is abstract unix domain sockets and 'inet' is inet sockets. ", choices=["abstract", "unix", "inet"], default="abstract")
|
||||
parser.add_argument('-k', '--keybindings', help='The file with the keybinding configuration. This file is a JSON file.', default="default")
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
class InvalidMessageError(Exception):
|
||||
errType = "invalidmessage"
|
||||
description = ""
|
||||
|
||||
def __init__(self, description="", errType=None):
|
||||
self.description = description
|
||||
if errType is not None:
|
||||
self.errType = errType
|
||||
|
||||
class InvalidNameError(InvalidMessageError):
|
||||
errType = "invalidname"
|
||||
|
||||
class Message:
|
||||
|
||||
@classmethod
|
||||
def msgType(cls):
|
||||
return cls.typename
|
||||
|
||||
def to_json(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@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"
|
||||
nameRegex = re.compile("(~|\w)\w*")
|
||||
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")
|
||||
for char in name if name[0] != "~" else name[1:]:
|
||||
category = unicodedata.category(char)
|
||||
assert category in self.categories, InvalidNameError("all name caracters must be in these unicode categories: " + "|".join(self.categories) + " (except the tilde in a tildename)")
|
||||
|
||||
#assert (name.rfind("~") < 1), InvalidNameError("tilde character may only occur at start of name")
|
||||
#assert (self.nameRegex.match(name) is not None), InvalidNameError("name must match the following regex: {}".format(self.nameRegex.pattern))
|
||||
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
|
||||
|
||||
|
||||
|
||||
messages = {message.msgType(): message for message in [
|
||||
NameMessage,
|
||||
InputMessage,
|
||||
ChatMessage
|
||||
]}
|
||||
|
|
@ -11,6 +11,8 @@ from . import view
|
|||
from . import socketserver as server
|
||||
from . import player
|
||||
|
||||
from asciifarm.common import messages
|
||||
|
||||
import re
|
||||
|
||||
nameRegex = re.compile("(~|\w)\w*")
|
||||
|
@ -37,7 +39,7 @@ class GameServer:
|
|||
data = view.playerView(name)
|
||||
if data is None:
|
||||
continue
|
||||
databytes = bytes(json.dumps(data), 'utf-8')
|
||||
databytes = bytes(json.dumps(["world", data]), 'utf-8')
|
||||
|
||||
self.serv.send(connection, databytes)
|
||||
|
||||
|
@ -56,48 +58,65 @@ class GameServer:
|
|||
return
|
||||
msg = data
|
||||
msgType = msg[0]
|
||||
if msgType == "name":
|
||||
name = msg[1]
|
||||
|
||||
if len(name) < 1:
|
||||
self.error(n, "invalidname", "name needs at least one character")
|
||||
return
|
||||
if len(bytes(name, "utf-8")) > 256:
|
||||
self.error(n, "invalidname", "name may not be longer than 256 utf8 bytes")
|
||||
return
|
||||
if nameRegex.match(name) is None:
|
||||
self.error(n, "invalidname", "Name must match the following regex: {}".format(nameRegex.pattern))
|
||||
return
|
||||
if name[0] == "~" and name[1:] != self.serv.getUsername(n):
|
||||
self.error(n, "invalidname", "tildenames are only available on unix sockets and when the rest of the name equals the username")
|
||||
return
|
||||
if name in self.players:
|
||||
self.error(n, "nametaken", "another connection to this player already exists")
|
||||
return
|
||||
self.connections[n] = name
|
||||
self.players[name] = n
|
||||
self.messages.put(("join", name))
|
||||
print("new player: "+name)
|
||||
self.broadcast("{} has connected".format(name), "connect")
|
||||
elif msgType == "input":
|
||||
if n in self.connections:
|
||||
self.messages.put(("input", self.connections[n], msg[1]))
|
||||
elif msgType == "chat":
|
||||
if n in self.connections:
|
||||
name = self.connections[n]
|
||||
if not msg[1].isprintable():
|
||||
self.error("invalidmessage", "Chat message may only contain printable unicode characters")
|
||||
message = name + ": " + msg[1]
|
||||
print(message)
|
||||
self.broadcast(message, "chat")
|
||||
if not isinstance(msgType, str):
|
||||
self.error(n, "invalidmessage", "Message type must be a string")
|
||||
return
|
||||
msgClass = messages.messages.get(msgType)
|
||||
if msgClass is None:
|
||||
self.error(n, "invalidmessage", "Unknown message type '{}'".format(msgType))
|
||||
return
|
||||
try:
|
||||
message = msgClass.from_json(msg)
|
||||
except messages.InvalidMessageError as e:
|
||||
self.error(n, e.errType, e.description)
|
||||
return
|
||||
|
||||
self.handleMessage(n, message)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.error(n, "invalidmessage", "An unknown error occured in handling the message")
|
||||
|
||||
def handleMessage(self, n, message):
|
||||
# I wish I had type overloading
|
||||
if isinstance(message, messages.NameMessage):
|
||||
self.handleNameMessage(n, message)
|
||||
elif isinstance(message, messages.InputMessage):
|
||||
self.handleInputMessage(n, message)
|
||||
elif isinstance(message, messages.ChatMessage):
|
||||
self.handleChatMessage(n, message)
|
||||
else:
|
||||
self.error(n, "invalidmessage", "unknown message '{}'".format(message.__class__))
|
||||
|
||||
def error(self, n, errtype, *data):
|
||||
self.serv.send(n, bytes(json.dumps(["error", errtype]+list(data)), "utf-8"))
|
||||
|
||||
def handleNameMessage(self, n, message):
|
||||
name = message.name
|
||||
if name[0] == "~" and name[1:] != self.serv.getUsername(n):
|
||||
self.error(n, "invalidname", "tildenames are only available on unix sockets and when the rest of the name equals the username")
|
||||
return
|
||||
if name in self.players:
|
||||
self.error(n, "nametaken", "another connection to this player already exists")
|
||||
return
|
||||
self.connections[n] = name
|
||||
self.players[name] = n
|
||||
self.messages.put(("join", name))
|
||||
print("new player: "+name)
|
||||
self.broadcast("{} has connected".format(name), "connect")
|
||||
|
||||
def handleInputMessage(self, n, message):
|
||||
if n in self.connections:
|
||||
self.messages.put(("input", self.connections[n], message.inp))
|
||||
|
||||
def handleChatMessage(self, n, msg):#if n in self.connections:
|
||||
name = self.connections[n]
|
||||
message = name + ": " + msg.text
|
||||
print(message)
|
||||
self.broadcast(message, "chat")
|
||||
|
||||
|
||||
def error(self, n, errtype, description=""):
|
||||
self.serv.send(n, bytes(json.dumps(["error", errtype, description]), "utf-8"))
|
||||
|
||||
def close(self, connection):
|
||||
if connection in self.connections:
|
||||
|
|
|
@ -133,7 +133,7 @@ class Server:
|
|||
header = length.to_bytes(4, byteorder="big")
|
||||
try:
|
||||
connection.sendall(header + msg)
|
||||
except BrokenPipeError:
|
||||
except (BrokenPipeError, OSError):
|
||||
self.closeConnection(connection)
|
||||
|
||||
def broadcast(self, msg):
|
||||
|
|
Loading…
Reference in New Issue