From dd07ff4d686f07cdc9736627dd0ef099ef5e4e4f Mon Sep 17 00:00:00 2001 From: troido Date: Fri, 6 Mar 2020 11:26:59 +0100 Subject: new directory structure for the separate client repo --- asciifarm/charmaps/__init__.py | 0 asciifarm/charmaps/emoji.json | 10 -- asciifarm/charmaps/fullwidth.json | 52 ---------- asciifarm/charmaps/halfwidth.json | 64 ------------ asciifarm/client/__init__.py | 0 asciifarm/client/__main__.py | 16 --- asciifarm/client/commandhandler.py | 150 ---------------------------- asciifarm/client/connection.py | 30 ------ asciifarm/client/display.py | 174 --------------------------------- asciifarm/client/gameclient.py | 173 -------------------------------- asciifarm/client/inputhandler.py | 98 ------------------- asciifarm/client/layout.xml | 44 --------- asciifarm/client/listselector.py | 49 ---------- asciifarm/client/loaders.py | 81 --------------- asciifarm/client/main.py | 65 ------------ asciifarm/client/parseargs.py | 66 ------------- asciifarm/client/paths.py | 7 -- asciifarm/common/__init__.py | 0 asciifarm/common/messages.py | 157 ----------------------------- asciifarm/common/tcommunicate.py | 32 ------ asciifarm/common/utils.py | 47 --------- asciifarm/keybindings/azerty.json | 14 --- asciifarm/keybindings/default.json | 42 -------- asciifarmclient/__init__.py | 0 asciifarmclient/__main__.py | 16 +++ asciifarmclient/commandhandler.py | 150 ++++++++++++++++++++++++++++ asciifarmclient/common/__init__.py | 0 asciifarmclient/common/messages.py | 157 +++++++++++++++++++++++++++++ asciifarmclient/common/tcommunicate.py | 32 ++++++ asciifarmclient/common/utils.py | 47 +++++++++ asciifarmclient/connection.py | 30 ++++++ asciifarmclient/display.py | 174 +++++++++++++++++++++++++++++++++ asciifarmclient/gameclient.py | 173 ++++++++++++++++++++++++++++++++ asciifarmclient/inputhandler.py | 98 +++++++++++++++++++ asciifarmclient/layout.xml | 44 +++++++++ asciifarmclient/listselector.py | 49 ++++++++++ asciifarmclient/loaders.py | 81 +++++++++++++++ asciifarmclient/main.py | 65 ++++++++++++ asciifarmclient/parseargs.py | 66 +++++++++++++ asciifarmclient/paths.py | 7 ++ charmaps/__init__.py | 0 charmaps/emoji.json | 10 ++ charmaps/fullwidth.json | 52 ++++++++++ charmaps/halfwidth.json | 64 ++++++++++++ keybindings/azerty.json | 14 +++ keybindings/default.json | 42 ++++++++ 46 files changed, 1371 insertions(+), 1371 deletions(-) delete mode 100644 asciifarm/charmaps/__init__.py delete mode 100644 asciifarm/charmaps/emoji.json delete mode 100644 asciifarm/charmaps/fullwidth.json delete mode 100644 asciifarm/charmaps/halfwidth.json delete mode 100644 asciifarm/client/__init__.py delete mode 100644 asciifarm/client/__main__.py delete mode 100644 asciifarm/client/commandhandler.py delete mode 100644 asciifarm/client/connection.py delete mode 100644 asciifarm/client/display.py delete mode 100644 asciifarm/client/gameclient.py delete mode 100644 asciifarm/client/inputhandler.py delete mode 100644 asciifarm/client/layout.xml delete mode 100644 asciifarm/client/listselector.py delete mode 100644 asciifarm/client/loaders.py delete mode 100644 asciifarm/client/main.py delete mode 100644 asciifarm/client/parseargs.py delete mode 100644 asciifarm/client/paths.py delete mode 100644 asciifarm/common/__init__.py delete mode 100644 asciifarm/common/messages.py delete mode 100644 asciifarm/common/tcommunicate.py delete mode 100644 asciifarm/common/utils.py delete mode 100644 asciifarm/keybindings/azerty.json delete mode 100644 asciifarm/keybindings/default.json create mode 100644 asciifarmclient/__init__.py create mode 100644 asciifarmclient/__main__.py create mode 100644 asciifarmclient/commandhandler.py create mode 100644 asciifarmclient/common/__init__.py create mode 100644 asciifarmclient/common/messages.py create mode 100644 asciifarmclient/common/tcommunicate.py create mode 100644 asciifarmclient/common/utils.py create mode 100644 asciifarmclient/connection.py create mode 100644 asciifarmclient/display.py create mode 100644 asciifarmclient/gameclient.py create mode 100644 asciifarmclient/inputhandler.py create mode 100644 asciifarmclient/layout.xml create mode 100644 asciifarmclient/listselector.py create mode 100644 asciifarmclient/loaders.py create mode 100644 asciifarmclient/main.py create mode 100644 asciifarmclient/parseargs.py create mode 100644 asciifarmclient/paths.py create mode 100644 charmaps/__init__.py create mode 100644 charmaps/emoji.json create mode 100644 charmaps/fullwidth.json create mode 100644 charmaps/halfwidth.json create mode 100644 keybindings/azerty.json create mode 100644 keybindings/default.json diff --git a/asciifarm/charmaps/__init__.py b/asciifarm/charmaps/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/asciifarm/charmaps/emoji.json b/asciifarm/charmaps/emoji.json deleted file mode 100644 index 3049816..0000000 --- a/asciifarm/charmaps/emoji.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "templates": ["fullwidth"], - "mapping":{ - "tree": ["🌳",0,2], - "stairdown": ["↘",7], - "stairup": ["↖",7], - "player": ["🙂",15], - "sword": ["🗡️", 7] - } -} diff --git a/asciifarm/charmaps/fullwidth.json b/asciifarm/charmaps/fullwidth.json deleted file mode 100644 index 6d912fe..0000000 --- a/asciifarm/charmaps/fullwidth.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "templates": ["halfwidth"], - "mapping":{ - "tree": ["T",0,2], - "wall": ["#",7,8], - "builtwall": ["+",7,8], - "rock": ["#",8,8], - "stone": ["o",7], - "pebble": ["*",7], - "player": ["@",15], - "ground": [".",3], - "grass1": [",",2], - "grass2": ["'",2], - "grass3": ["`",10], - "rabbit": ["b",15], - "water": ["~",4,4], - "floor": [".",8], - "portal": ["$", 5], - "stairdown": [">",7], - "stairup": ["<",7], - "dummy": ["d",3], - "spikes": ["^",7], - "goblin": ["g",2], - "seed": [":",10], - "plantedseed": [".",10], - "seedling": [",",10], - "youngplant": ["v",10], - "smallplant": ["v",2], - "plant": ["Y",2], - "food": ["8",13], - "troll": ["T",1], - "sword": ["/", 7], - "club": ["!", 3], - "house": ["^", 3, 1], - "bridge": ["=", 9,3], - "smallbridge": ["=", 3,4], - "entry": ["%", 7], - "fence": ["#", 3], - "armour": ["[", 7], - "wound": [" ", 7, 1], - "rat": ["r",7], - "closeddoor": ["+",1 ,3], - "opendoor": [""",3], - "raindrop": [":",12], - "snowflake": ["*",15], - " ": [" ", 7] - }, - "default": ["?", 7], - "charwidth": 2, - "alphabet": - "!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~" -} diff --git a/asciifarm/charmaps/halfwidth.json b/asciifarm/charmaps/halfwidth.json deleted file mode 100644 index d3e16f8..0000000 --- a/asciifarm/charmaps/halfwidth.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "mapping":{ - "tree": ["T", 0, 2], - "wall": ["#", 7, 8], - "builtwall": ["+", 7, 8], - "rock": ["#", 8, 8], - "stone": ["o", 7], - "pebble": ["*", 7], - "player": ["@", 15], - "ground": [".", 3], - "grass1": [",", 2], - "grass2": ["'", 2], - "grass3": ["`", 10], - "water": ["~", 4, 4], - "rabbit": ["b", 15], - "floor": [".", 8], - "portal": ["$", 5], - "stairdown": [">",7], - "stairup": ["<",7], - "dummy": ["d", 3], - "spikes": ["^",7], - "goblin": ["g",2], - "seed": [":",10], - "plantedseed": [".",10], - "seedling": [",", 10], - "youngplant": ["v", 10], - "smallplant": ["v", 2], - "plant": ["Y",2], - "food": ["8",13], - "troll": ["T",1], - "sword": ["/",7], - "club": ["!", 3], - "house": ["^", 3, 1], - "bridge": ["=", 9,3], - "smallbridge": ["=", 3,4], - "entry": ["%", 7], - "fence": ["#", 3], - "armour": ["[", 7], - "wound": [" ", 7, 1], - "rat": ["r", 7], - "closeddoor": ["+",1, 3], - "opendoor": ["\"", 3], - "raindrop": [":", 12], - "snowflake": ["*", 15], - " ": [" ", 7] - }, - "writable": { - "engravedwall": [15, 8], - "emptyletter": [7, 0] - }, - "default": ["?", 7], - "charwidth": 1, - "healthfull": ["#", 7, 2], - "healthempty": ["_", 7, 1], - "msgcolours": { - "chat": [15,0], - "attack": [12,0], - "damage": [9,0], - "heal": [10,0], - "connect": [3,0], - "server": [11,0] - }, - "alphabet": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" -} diff --git a/asciifarm/client/__init__.py b/asciifarm/client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/asciifarm/client/__main__.py b/asciifarm/client/__main__.py deleted file mode 100644 index 2841ae9..0000000 --- a/asciifarm/client/__main__.py +++ /dev/null @@ -1,16 +0,0 @@ - - -import sys - -if sys.version_info[0] < 3: - print("This game is written in python 3.\nRun 'python3 "+sys.argv[0]+"' or './"+sys.argv[0]+"'") - sys.exit(-1) - -if __package__ == "asciifarm.client": - from . import main -else: - import os.path - sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..")) - from asciifarm.client import main - -main.main() diff --git a/asciifarm/client/commandhandler.py b/asciifarm/client/commandhandler.py deleted file mode 100644 index ec5baa3..0000000 --- a/asciifarm/client/commandhandler.py +++ /dev/null @@ -1,150 +0,0 @@ - -import json - -try: - import hy -except ImportError as e: - hy = None - hyErr = e - -class InvalidCommandException(Exception): - pass - - -class CommandHandler: - - def __init__(self, client): - self.client = client - - self.commands = { - "input": self.input, - "move": self.move, - "say": self.say, - "pick": self.pick, - "chat": self.chat, - "log": self.log, - "do": self.do, - "runinput": self.runInput, - "selectwidget": self.selectWidget, - "selectitem": self.selectItem, - "inputwithselected": self.actWithSelected, - "use": self.useSelected, - "drop": self.dropSelected, - "take": self.takeSelected, - "eval": self.eval, - "exec": self.exec, - "scrollchat": self.scrollChat, - "json": self.json, - "j": self.json, - "ijson": self.ijson, - "ij": self.ijson, - "hy": self.hy - } - - self.evalArgs = { - "self": self, - "client": self.client, - "connection": self.client.connection, - "display": self.client.display, - "print": self.log - } - - def execute(self, action): - if action is None: - return - if isinstance(action[0], str): - command = action[0] - if command in self.commands: - self.commands[command](*action[1:]) - else: - raise InvalidCommandException("Invalid command '{}'".format(command)) - else: - raise Exception("Command should be a string") - - - # Commands - - def input(self, action): - self.client.sendInput(action) - - def move(self, direction): - self.input(["move", direction]) - - def say(self, text): - self.input(["say", text]) - - def pick(self, option): - self.input(["interact", [None, "north", "south", "east", "west"], option]) - - def chat(self, text): - self.client.sendChat( text) - - - def log(self, text): - self.client.log(text) - - def do(self, actions): - for action in actions: - self.execute(action) - - def runInput(self, startText=""): - self.client.inputHandler.startTyping(startText) - - def selectWidget(self, value, relative=False, modular=False): - self.client.display.selectMenu(value, relative, modular) - - def selectItem(self, value, relative=False, modular=False): - self.client.display.selectItem(None, value, relative, modular) - - def actWithSelected(self, action, menu): - self.input([action, self.client.display.getSelectedItem(menu).getSelected()]) - - def useSelected(self): - menu = self.client.display.getSelectedMenu() - selected = self.client.display.getSelectedItem(menu) - if menu in ("inventory", "equipment"): - self.input(["use", menu, selected]) - elif menu == "ground": - self.input(["interact", selected]) - - def dropSelected(self): - menu = self.client.display.getSelectedMenu() - selected = self.client.display.getSelectedItem(menu) - if menu == "inventory": - action = "drop" - else: - return - self.input([action, selected]) - - def takeSelected(self): - menu = self.client.display.getSelectedMenu() - selected = self.client.display.getSelectedItem(menu) - if menu == "ground": - action = "take" - else: - return - self.input([action, selected]) - - def eval(self, text): - self.log(eval(text, self.evalArgs)) - - def exec(self, text): - exec(text, self.evalArgs) - - def hy(self, code): - if hy is None: - self.log(hyErr) - return - expr = hy.read_str(code) - self.log(hy.eval(expr, self.evalArgs)) - - def scrollChat(self, lines): - self.client.display.scrollBack(lines) - - def json(self, text): - self.execute(json.loads(text)) - - def ijson(self, text): - self.input(json.loads(text)) - - diff --git a/asciifarm/client/connection.py b/asciifarm/client/connection.py deleted file mode 100644 index 0a188c3..0000000 --- a/asciifarm/client/connection.py +++ /dev/null @@ -1,30 +0,0 @@ - -import socket - -from asciifarm.common.tcommunicate import send, receive - -class Connection: - - def __init__(self, socketType): - if socketType == "abstract" or socketType == "unix": - sockType = socket.AF_UNIX - elif socketType == "inet": - sockType = socket.AF_INET - else: - raise ValueError("Invalid socket type: %r" % (socketType,)) - self.sock = socket.socket(sockType, socket.SOCK_STREAM) - - def connect(self, address): - self.sock.connect(address) - - def listen(self, callback, onError): - while True: - try: - data = receive(self.sock) - except Exception as err: - onError(err) - else: - callback(data) - - def send(self, message): - send(self.sock, message) diff --git a/asciifarm/client/display.py b/asciifarm/client/display.py deleted file mode 100644 index edd5acb..0000000 --- a/asciifarm/client/display.py +++ /dev/null @@ -1,174 +0,0 @@ - - - -import os -from ratuil.layout import Layout -from ratuil.bufferedscreen import BufferedScreen as Screen -#from ratuil.screen import Screen -from ratuil.textstyle import TextStyle -from asciifarm.common.utils import get -from .listselector import ListSelector - - -SIDEWIDTH = 20 - -ALPHABET = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" - -class Display: - - def __init__(self, charMap): - - self.characters = {} - - def parseSprite(sprite): - if isinstance(sprite, str): - return (sprite, None, None) - char = get(sprite, 0, " ") - fg = get(sprite, 1) - bg = get(sprite, 2) - return (char, fg, bg) - for name, sprite in charMap["mapping"].items(): - vals = parseSprite(sprite) - if vals: - self.characters[name] = vals - - for name, colours in charMap.get("writable", {}).items(): - fg = get(colours, 0) - bg = get(colours, 1) - for i in range(min(len(ALPHABET), len(charMap.get("alphabet", [])))): - self.characters[name + '-' + ALPHABET[i]] = (charMap["alphabet"][i], fg, bg) - - self.defaultChar = parseSprite(charMap.get("default", "?")) - - self.messageColours = charMap.get("msgcolours", {}) - - fname = os.path.join(os.path.dirname(__file__), "layout.xml") - self.layout = Layout.from_xml_file(fname) - self.layout.get("field").set_char_size(charMap.get("charwidth", 1)) - - self.screen = Screen() - self.screen.clear() - - self.layout.set_target(self.screen) - self.layout.update() - - - - # temporary, until these have a better place - self.inventory = ListSelector(self.getWidget("inventory")) - self.inventory._debug_name = "inventory" - self.equipment = ListSelector(self.getWidget("equipment")) - self.equipment._debug_name = "equipment" - self.ground = ListSelector(self.getWidget("ground")) - self.ground._debug_name = "ground" - self.switch = ListSelector(self.getWidget("switchtitles")) - self.switch._debug_name = "switch" - - self.switch.setItems(["inventory", "equipment", "ground"]) - self.menus = { - "inventory": self.inventory, - "equipment": self.equipment, - "ground": self.ground - } - - self.layout.get("switch").select(0) - - - def getWidget(self, name): - return self.layout.get(name) - - def resizeField(self, size): - self.getWidget("field").set_size(*size) - self.getWidget("fieldbackground").change() - - def drawFieldCells(self, cells): - field = self.getWidget("field") - for cell in cells: - (x, y), spriteNames = cell - if not len(spriteNames): - char, fg, bg = self.getChar(' ') - else: - char, fg, bg = self.getChar(spriteNames[0]) - for spriteName in spriteNames[1:]: - if bg is not None: - break - _char, _fg, bg = self.getChar(spriteName) - field.change_cell(x, y, char, TextStyle(fg, bg)) - - - def setFieldCenter(self, pos): - self.getWidget("field").set_center(*pos) - - def setHealth(self, health, maxHealth): - if health is None: - health = 0 - if maxHealth is None: - maxHealth = 0 - self.getWidget("health").set_total(maxHealth) - self.getWidget("health").set_filled(health) - self.getWidget("healthtitle").format({"filled": health, "total":maxHealth}) - - - def showInfo(self, infostring): - self.getWidget("info").set_text(infostring) - - def selectMenu(self, *args, **kwargs): - self.switch.select(*args, **kwargs) - self.layout.get("switch").select(self.getSelectedMenu()) - - def getSelectedMenu(self): - return self.switch.getSelectedItem() - - def getSelectedItem(self, menu=None): - return self._getMenu(menu).getSelected() - - def selectItem(self, menu=None, *args, **kwargs): - self._getMenu(menu).select(*args, **kwargs) - - def _getMenu(self, name=None): - if name is None: - name = self.getSelectedMenu() - name = name.casefold() - return self.menus[name] - - def setInventory(self, items): - self.inventory.setItems(items) - - - def setEquipment(self, slots): - self.equipment.setItems([ - slot + ": " + (item if item else "") - for slot, item in slots - ]) - - def setGround(self, items): - self.ground.setItems(items) - - - def addMessage(self, message, msgtype=None): - if msgtype is not None: - style = TextStyle(*self.messageColours.get(msgtype, (7,0))) - else: - style = None - self.getWidget("msg").add_message(message, style) - - def log(self, message): - self.addMessage(str(message)) - - def scrollBack(self, amount, relative=True): - self.getWidget("msg").scroll(amount, relative) - - def setInputString(self, string, cursor): - self.getWidget("textinput").set_text(string, cursor) - - def update(self): - self.layout.update() - self.screen.update() - - def getChar(self, sprite): - """This returns the character belonging to some spritename. This does not read a character""" - return self.characters.get(sprite, self.defaultChar) - - def update_size(self): - self.screen.reset() - diff --git a/asciifarm/client/gameclient.py b/asciifarm/client/gameclient.py deleted file mode 100644 index c0f702c..0000000 --- a/asciifarm/client/gameclient.py +++ /dev/null @@ -1,173 +0,0 @@ - - -import os -import sys - -import threading -import json -import getpass -import argparse -import string -from queue import Queue - -import ratuil.inputs - -from .inputhandler import InputHandler -from asciifarm.common import messages - -class Client: - - def __init__(self, display, name, connection, keybindings, logFile=None): - - self.display = display - self.name = name - self.keepalive = True - self.connection = connection - self.logFile = logFile - self.closeMessage = None - - self.inputHandler = InputHandler(self, keybindings["actions"]) - - self.controlsString = keybindings.get("help", "") - - self.display.showInfo(self.controlsString) - self.queue = Queue() - - - def sendMessage(self, message): - self.connection.send(message.to_json_bytes()) - - 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.command_loop() - - def listen(self): - self.connection.listen(self.pushMessage, self.onConnectionError) - - def pushMessage(self, databytes): - self.queue.put(("message", databytes)) - - def onConnectionError(self, error): - self.queue.put(("error", error)) - - def getInput(self): - while True: - key = ratuil.inputs.get_key() - self.queue.put(("input", key)) - - def close(self, msg=None): - self.keepalive = False - self.closeMessage = msg - - - def update(self, databytes): - if len(databytes) == 0: - self.close("Connection closed by server") - return - datastr = databytes.decode('utf-8') - msg = json.loads(datastr) - message = messages.messages[msg[0]].from_json(msg) - if isinstance(message, messages.ErrorMessage): - error = message.errType - if error == "nametaken": - self.close("error: name is already taken") - return - if error == "invalidname": - self.close("Invalid name error: "+ str(message.description)) - return - self.log(message.errType + ": " + message.description) - elif isinstance(message, messages.MessageMessage): - self.log(message.text, message.type) - elif isinstance(message, messages.WorldMessage): - for msg in message.updates: - self.handleWorldUpdate(msg) - - def handleWorldUpdate(self, msg): - 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": - type, text = msg[1][:2] - self.log(text, type) - 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): - if not isinstance(text, str): - text = str(text) - self.display.addMessage(text, type) - if self.logFile: - with(open(self.logFile, 'a')) as f: - f.write("[{}] {}\n".format(type or "", text)) - - - def command_loop(self): - while self.keepalive: - self.display.update() - action = self.queue.get() - if action[0] == "message": - self.update(action[1]) - elif action[0] == "input": - if action[1] == "^C": - raise KeyboardInterrupt - self.inputHandler.onInput(action[1]) - elif action[0] == "error": - raise action[1] - elif action[0] == "sigwinch": - self.display.update_size() - else: - raise Exception("invalid action in queue") - - def onSigwinch(self, signum, frame): - self.queue.put(("sigwinch", (signum, frame))) - - - - diff --git a/asciifarm/client/inputhandler.py b/asciifarm/client/inputhandler.py deleted file mode 100644 index 4281c01..0000000 --- a/asciifarm/client/inputhandler.py +++ /dev/null @@ -1,98 +0,0 @@ - -import string - -from .commandhandler import CommandHandler, InvalidCommandException - -import ratuil.inputs as inp - - -class InputHandler: - - def __init__(self, client, keybindings): - self.client = client - self.keybindings = keybindings - self.commandHandler = CommandHandler(self.client) - - self.typing = False - self.string = "" - self.cursor = 0 - - - def onInput(self, key): - if not self.typing: - keyName = key - if keyName in self.keybindings: - self.commandHandler.execute(self.keybindings[keyName]) - else: - self.addKey(key) - - - def processString(self, message): - if message: - if message[0] == '/': - if len(message) == 1: - return - if message[1] == '/': - self.commandHandler.chat(message[1:]) - else: - try: - command, _sep, arg = message[1:].partition(' ') - try: - self.commandHandler.execute([command, arg]) - except Exception as e: - self.log(e) - except InvalidCommandException as e: - self.client.log(", ".join(e.args)) - else: - self.commandHandler.chat(message) - - def startTyping(self, startText=""): - self.typing = True - if startText and not self.string: - self.string = startText - self.cursor = len(self.string) - - self.showString() - - def showString(self): - self.client.display.setInputString(self.string, self.cursor if self.typing else None) - - def addKey(self, key): - if key == inp.BACKSPACE: - self.string = self.string[:self.cursor-1] + self.string[self.cursor:] - self.cursor = max(self.cursor - 1, 0) - elif key == inp.RIGHT: - self.cursor = min(self.cursor + 1, len(self.string)) - elif key == inp.LEFT: - self.cursor = max(self.cursor - 1, 0) - elif key == inp.DELETE: - self.string = self.string[:self.cursor] + self.string[self.cursor+1:] - elif key == inp.HOME: - self.cursor = 0 - elif key == inp.END: - self.cursor = len(self.string) - - elif key == inp.ESCAPE: - # throw away entered string and go back to game - self.typing = False - self.string = "" - self.cursor = 0 - elif key == inp.ENTER: - # process entered string and reset it - message = self.string - self.string = "" - self.cursor = 0 - self.typing = False - self.processString(message) - elif key == "^I": # tab - # return to game but keep entered string - self.typing = False - elif key.isprintable() and len(key) == 1: - self.string = self.string[:self.cursor] + key + self.string[self.cursor:] - self.cursor += len(key) - - self.showString() - - - - diff --git a/asciifarm/client/layout.xml b/asciifarm/client/layout.xml deleted file mode 100644 index d50ecba..0000000 --- a/asciifarm/client/layout.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - Health ({filled}/{total}) - - - - - Inventory: - - milk - eggs - bread - - - - Equipment: - - cotton underwear - cotton shirt - jeans - friendship bracelet - - - - Ground: - - concrete - - - - - - - hello - - Welcome to asciifarm - - - - - - - diff --git a/asciifarm/client/listselector.py b/asciifarm/client/listselector.py deleted file mode 100644 index b88b967..0000000 --- a/asciifarm/client/listselector.py +++ /dev/null @@ -1,49 +0,0 @@ - -from asciifarm.common import utils - - -class ListSelector: - - def __init__(self, widget): - self.widget = widget - self.items = [] - self.selector = 0 - - def getSelected(self): - return self.selector - - def select(self, value, relative=False, modular=False): - invLen = len(self.items) - if relative: - value += self.selector - if modular and invLen: - value %= invLen - if value < 0: - value = 0 - if value >= invLen: - value = invLen-1 - if value in range(invLen): - self.doSelect(value) - - def doSelect(self, value): - self.selector = value - self.widget.select(value) - - def setItems(self, items): - self.items = items - self.selector = utils.clamp(self.selector, 0, len(items)-1) - self.widget.set_items([self.itemName(item) for item in self.items]) - self.widget.select(self.selector) - - def getItem(self, num): - return self.items[num] - - def getSelectedItem(self): - return self.getItem(self.getSelected()) - - def getNumItems(self): - return len(self.items) - - def itemName(self, item): - return item - diff --git a/asciifarm/client/loaders.py b/asciifarm/client/loaders.py deleted file mode 100644 index efdd1c0..0000000 --- a/asciifarm/client/loaders.py +++ /dev/null @@ -1,81 +0,0 @@ - -import os - -from .paths import keybindingsPath, charmapPath -import json - - -standardKeyFiles = { - "default": os.path.join(keybindingsPath, "default.json"), - "azerty": os.path.join(keybindingsPath, "azerty.json") -} - -def loadKeybindings(name): - fname = None - if name in standardKeyFiles: - fname = standardKeyFiles[name] - else: - fname = name - with open(fname) as f: - data = json.load(f) - bindings = {} - help = "" - for ftemplate in data.get("templates", []): - if ftemplate.partition(os.sep)[0] in {".", ".."}: - ftemplate = os.path.relpath(ftemplate, fname) - template = loadKeybindings(ftemplate) - bindings.update(template.get("actions", {})) - help = template.get("help", help) - bindings.update(data.get("actions", {})) - help = data.get("help", help) - return {"actions": bindings, "help": help} - - -standardCharFiles = {name: os.path.join(charmapPath, file) for name, file in { - "default": "fullwidth.json", - "halfwidth": "halfwidth.json", - "hw": "halfwidth.json", - "fullwidth": "fullwidth.json", - "fw": "fullwidth.json", - "emoji": "emoji.json" -}.items()} - -def loadCharmap(name): - fname = None - if name in standardCharFiles: - fname = standardCharFiles[name] - else: - fname = name - with open(fname) as f: - data = json.load(f) - - templates = [] - for ftemplate in data.get("templates", []): - if ftemplate.partition(os.sep)[0] in {".", ".."}: - ftemplate = os.path.relpath(ftemplate, fname) - templates.append(loadCharmap(ftemplate)) - - templates.append(data) - - mapping = {} - writable = {} - default = None - charwidth = 1 - alphabet = "" - msgcolours = {} - - for template in templates: - mapping.update(template.get("mapping", {})) - writable.update(template.get("writable", {})) - default = template.get("default", default) - charwidth = template.get("charwidth", charwidth) - alphabet = template.get("alphabet", alphabet) - msgcolours.update(template.get("msgcolours", {})) - return { - "mapping": mapping, - "writable": writable, - "default": default, - "charwidth": charwidth, - "alphabet": alphabet, - "msgcolours": msgcolours - } diff --git a/asciifarm/client/main.py b/asciifarm/client/main.py deleted file mode 100644 index d720477..0000000 --- a/asciifarm/client/main.py +++ /dev/null @@ -1,65 +0,0 @@ -#! /usr/bin/python3 - -import json - -import sys -import termios -import tty -import signal -#import os - -from .connection import Connection -from .gameclient import Client -from .display import Display -from .parseargs import parse_args -from ratuil.screen import Screen - -def main(argv=None): - - (name, socketType, address, keybindings, characters, colours, logfile) = parse_args(argv) - - - connection = Connection(socketType) - try: - connection.connect(address) - except ConnectionRefusedError: - print("ERROR: Could not connect to server.\nAre you sure that the server is running and that you're connecting to the right address?", file=sys.stderr) - return - - error = None - closeMessage = None - - #os.environ.setdefault("ESCDELAY", "25") - - fd = sys.stdin.fileno() - oldterm = termios.tcgetattr(fd) - - try: - - tty.setraw(sys.stdin) - Screen.default.hide_cursor() - - display = Display(characters) - client = Client(display, name, connection, keybindings, logfile) - signal.signal(signal.SIGWINCH, client.onSigwinch) - try: - client.start() - except KeyboardInterrupt: - client.close("^C caught, goodbye") - except Exception as e: - # throw the execption outside ncurses - # so the cleanup can happen first - error = e - closeMessage = client.closeMessage - finally: - ## Set everything back to normal - termios.tcsetattr(fd, termios.TCSADRAIN, oldterm) - Screen.default.finalize() - - - if error is not None: - raise error - - if closeMessage: - print(closeMessage, file=sys.stderr) - diff --git a/asciifarm/client/parseargs.py b/asciifarm/client/parseargs.py deleted file mode 100644 index a8b393b..0000000 --- a/asciifarm/client/parseargs.py +++ /dev/null @@ -1,66 +0,0 @@ - - -import argparse -import getpass -import json -import os -import os.path - -from . import loaders - - -defaultAdresses = { - "abstract": "asciifarm", - "unix": "asciifarm.socket", - "inet": "localhost:9021", -} - -def parse_args(argv): - - parser = argparse.ArgumentParser(description="The client to AsciiFarm. Run this to connect to to the server.", epilog=""" - Gameplay information: - Walk around and explore the rooms. - 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). 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") - parser.add_argument('-c', '--characters', help='The file with the character mappings for the graphics. If it is either of these names: {} it will be loaded from the charmaps directory.'.format(list(loaders.standardCharFiles.keys())), default="default") - parser.add_argument('-o', '--logfile', help='All game messages will be written to this file.', default=None) - - colourGroup = parser.add_mutually_exclusive_group() - colourGroup.add_argument('-l', '--colours', '--colors', help='enable colours! :)', action="store_true") - colourGroup.add_argument('-b', '--nocolours', '--nocolors', help='disable colours! :)', action="store_true") - - args = parser.parse_args(argv) - - charmap = loaders.loadCharmap(args.characters) - - keybindings = loaders.loadKeybindings(args.keybindings) - - address = args.address - if address is None: - address = defaultAdresses[args.socket] - if args.socket == "abstract": - address = '\0' + address - elif args.socket == "inet": - hostname, sep, port = address.partition(':') - address = (hostname, int(port)) - - colours = True - if args.colours: - colours = True - elif args.nocolours: - colours = False - - name = args.name - if name is None: - username = getpass.getuser() - if args.socket == "unix" or args.socket == "abstract": - name = "~"+username - else: - name = username - - return (name, args.socket, address, keybindings, charmap, colours, args.logfile) diff --git a/asciifarm/client/paths.py b/asciifarm/client/paths.py deleted file mode 100644 index 407d54c..0000000 --- a/asciifarm/client/paths.py +++ /dev/null @@ -1,7 +0,0 @@ - -import os.path - -clientPath = os.path.dirname(__file__) -farmsPath = os.path.normpath(os.path.join(clientPath, "..")) -charmapPath = os.path.join(farmsPath, "charmaps") -keybindingsPath = os.path.join(farmsPath, "keybindings") diff --git a/asciifarm/common/__init__.py b/asciifarm/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/asciifarm/common/messages.py b/asciifarm/common/messages.py deleted file mode 100644 index d3aed9a..0000000 --- a/asciifarm/common/messages.py +++ /dev/null @@ -1,157 +0,0 @@ - -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/asciifarm/common/tcommunicate.py b/asciifarm/common/tcommunicate.py deleted file mode 100644 index b1fc1b0..0000000 --- a/asciifarm/common/tcommunicate.py +++ /dev/null @@ -1,32 +0,0 @@ - -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/asciifarm/common/utils.py b/asciifarm/common/utils.py deleted file mode 100644 index 95ac32b..0000000 --- a/asciifarm/common/utils.py +++ /dev/null @@ -1,47 +0,0 @@ - -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] diff --git a/asciifarm/keybindings/azerty.json b/asciifarm/keybindings/azerty.json deleted file mode 100644 index 3d69d07..0000000 --- a/asciifarm/keybindings/azerty.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "templates": ["default"], - "actions": { - "z": ["move", "north"], - "w": null, - "W": null, - "q": ["move", "west"], - "a": ["drop"], - "A": ["take"], - "Z": ["input", ["attack", ["north"]]], - "Q": ["input", ["attack", ["west"]]] - }, - "help": "Controls:\n aqsd or arrows: Move around\n e: Grab\n q: Drop selected\n r: Interact\n f: Attack\n t: Chat\n E: Use selected\n A: Take selected\n xc: select item\n vb: select menu\n ctrl-c: close client" -} diff --git a/asciifarm/keybindings/default.json b/asciifarm/keybindings/default.json deleted file mode 100644 index faf1b40..0000000 --- a/asciifarm/keybindings/default.json +++ /dev/null @@ -1,42 +0,0 @@ -{ -"actions": { -"w": ["move", "north"], -"s": ["move", "south"], -"d": ["move", "east"], -"a": ["move", "west"], -"up": ["move", "north"], -"down": ["move", "south"], -"right": ["move", "east"], -"left": ["move", "west"], -"k": ["move", "north"], -"j": ["move", "south"], -"l": ["move", "east"], -"h": ["move", "west"], -"e": ["input", ["take", null]], -"q": ["drop"], -"Q": ["take"], -"E": ["use"], -"R": ["input", ["interact", [null]]], -"r": ["input", ["interact", [null, "north", "south", "east", "west"]]], -"x": ["selectitem", -1, true, true], -"c": ["selectitem", 1, true, true], -"v": ["selectwidget", -1, true, true], -"b": ["selectwidget", 1, true, true], -"-": ["selectitem", -1, true, true], -"+": ["selectitem", 1, true, true], -"/": ["selectwidget", -1, true, true], -"*": ["selectwidget", 1, true, true], -"f": ["input", ["attack", [null, "north", "south", "east", "west"]]], -"F": ["input", ["attack", [null]]], -"W": ["input", ["attack", ["north"]]], -"S": ["input", ["attack", ["south"]]], -"D": ["input", ["attack", ["east"]]], -"A": ["input", ["attack", ["west"]]], -"t": ["runinput"], -"enter": ["runinput"], -"pageup": ["scrollchat", 1], -"pagedown": ["scrollchat", -1], -"/": ["runinput", "/"] -}, -"help": " Controls:\nwasd or arrows: Move around\ne: Grab\nq: Drop selected\nr: Interact\nf: Attack\nt: Chat\nE: Use selected\nxc: select item\nvb: select menu\nctrl-c: close client\nPgUp/PgDn: scroll chat" -} diff --git a/asciifarmclient/__init__.py b/asciifarmclient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/asciifarmclient/__main__.py b/asciifarmclient/__main__.py new file mode 100644 index 0000000..d086dfb --- /dev/null +++ b/asciifarmclient/__main__.py @@ -0,0 +1,16 @@ + + +import sys + +if sys.version_info[0] < 3: + print("This game is written in python 3.\nRun 'python3 "+sys.argv[0]+"' or './"+sys.argv[0]+"'") + sys.exit(-1) + +if __package__ == "asciifarmclient": + from . import main +else: + import os.path + sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + from asciifarmclient import main + +main.main() diff --git a/asciifarmclient/commandhandler.py b/asciifarmclient/commandhandler.py new file mode 100644 index 0000000..ec5baa3 --- /dev/null +++ b/asciifarmclient/commandhandler.py @@ -0,0 +1,150 @@ + +import json + +try: + import hy +except ImportError as e: + hy = None + hyErr = e + +class InvalidCommandException(Exception): + pass + + +class CommandHandler: + + def __init__(self, client): + self.client = client + + self.commands = { + "input": self.input, + "move": self.move, + "say": self.say, + "pick": self.pick, + "chat": self.chat, + "log": self.log, + "do": self.do, + "runinput": self.runInput, + "selectwidget": self.selectWidget, + "selectitem": self.selectItem, + "inputwithselected": self.actWithSelected, + "use": self.useSelected, + "drop": self.dropSelected, + "take": self.takeSelected, + "eval": self.eval, + "exec": self.exec, + "scrollchat": self.scrollChat, + "json": self.json, + "j": self.json, + "ijson": self.ijson, + "ij": self.ijson, + "hy": self.hy + } + + self.evalArgs = { + "self": self, + "client": self.client, + "connection": self.client.connection, + "display": self.client.display, + "print": self.log + } + + def execute(self, action): + if action is None: + return + if isinstance(action[0], str): + command = action[0] + if command in self.commands: + self.commands[command](*action[1:]) + else: + raise InvalidCommandException("Invalid command '{}'".format(command)) + else: + raise Exception("Command should be a string") + + + # Commands + + def input(self, action): + self.client.sendInput(action) + + def move(self, direction): + self.input(["move", direction]) + + def say(self, text): + self.input(["say", text]) + + def pick(self, option): + self.input(["interact", [None, "north", "south", "east", "west"], option]) + + def chat(self, text): + self.client.sendChat( text) + + + def log(self, text): + self.client.log(text) + + def do(self, actions): + for action in actions: + self.execute(action) + + def runInput(self, startText=""): + self.client.inputHandler.startTyping(startText) + + def selectWidget(self, value, relative=False, modular=False): + self.client.display.selectMenu(value, relative, modular) + + def selectItem(self, value, relative=False, modular=False): + self.client.display.selectItem(None, value, relative, modular) + + def actWithSelected(self, action, menu): + self.input([action, self.client.display.getSelectedItem(menu).getSelected()]) + + def useSelected(self): + menu = self.client.display.getSelectedMenu() + selected = self.client.display.getSelectedItem(menu) + if menu in ("inventory", "equipment"): + self.input(["use", menu, selected]) + elif menu == "ground": + self.input(["interact", selected]) + + def dropSelected(self): + menu = self.client.display.getSelectedMenu() + selected = self.client.display.getSelectedItem(menu) + if menu == "inventory": + action = "drop" + else: + return + self.input([action, selected]) + + def takeSelected(self): + menu = self.client.display.getSelectedMenu() + selected = self.client.display.getSelectedItem(menu) + if menu == "ground": + action = "take" + else: + return + self.input([action, selected]) + + def eval(self, text): + self.log(eval(text, self.evalArgs)) + + def exec(self, text): + exec(text, self.evalArgs) + + def hy(self, code): + if hy is None: + self.log(hyErr) + return + expr = hy.read_str(code) + self.log(hy.eval(expr, self.evalArgs)) + + def scrollChat(self, lines): + self.client.display.scrollBack(lines) + + def json(self, text): + self.execute(json.loads(text)) + + def ijson(self, text): + self.input(json.loads(text)) + + diff --git a/asciifarmclient/common/__init__.py b/asciifarmclient/common/__init__.py new file mode 100644 index 0000000..e69de29 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] diff --git a/asciifarmclient/connection.py b/asciifarmclient/connection.py new file mode 100644 index 0000000..370ef8a --- /dev/null +++ b/asciifarmclient/connection.py @@ -0,0 +1,30 @@ + +import socket + +from asciifarmclient.common.tcommunicate import send, receive + +class Connection: + + def __init__(self, socketType): + if socketType == "abstract" or socketType == "unix": + sockType = socket.AF_UNIX + elif socketType == "inet": + sockType = socket.AF_INET + else: + raise ValueError("Invalid socket type: %r" % (socketType,)) + self.sock = socket.socket(sockType, socket.SOCK_STREAM) + + def connect(self, address): + self.sock.connect(address) + + def listen(self, callback, onError): + while True: + try: + data = receive(self.sock) + except Exception as err: + onError(err) + else: + callback(data) + + def send(self, message): + send(self.sock, message) diff --git a/asciifarmclient/display.py b/asciifarmclient/display.py new file mode 100644 index 0000000..ae23b05 --- /dev/null +++ b/asciifarmclient/display.py @@ -0,0 +1,174 @@ + + + +import os +from ratuil.layout import Layout +from ratuil.bufferedscreen import BufferedScreen as Screen +#from ratuil.screen import Screen +from ratuil.textstyle import TextStyle +from asciifarmclient.common.utils import get +from .listselector import ListSelector + + +SIDEWIDTH = 20 + +ALPHABET = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + +class Display: + + def __init__(self, charMap): + + self.characters = {} + + def parseSprite(sprite): + if isinstance(sprite, str): + return (sprite, None, None) + char = get(sprite, 0, " ") + fg = get(sprite, 1) + bg = get(sprite, 2) + return (char, fg, bg) + for name, sprite in charMap["mapping"].items(): + vals = parseSprite(sprite) + if vals: + self.characters[name] = vals + + for name, colours in charMap.get("writable", {}).items(): + fg = get(colours, 0) + bg = get(colours, 1) + for i in range(min(len(ALPHABET), len(charMap.get("alphabet", [])))): + self.characters[name + '-' + ALPHABET[i]] = (charMap["alphabet"][i], fg, bg) + + self.defaultChar = parseSprite(charMap.get("default", "?")) + + self.messageColours = charMap.get("msgcolours", {}) + + fname = os.path.join(os.path.dirname(__file__), "layout.xml") + self.layout = Layout.from_xml_file(fname) + self.layout.get("field").set_char_size(charMap.get("charwidth", 1)) + + self.screen = Screen() + self.screen.clear() + + self.layout.set_target(self.screen) + self.layout.update() + + + + # temporary, until these have a better place + self.inventory = ListSelector(self.getWidget("inventory")) + self.inventory._debug_name = "inventory" + self.equipment = ListSelector(self.getWidget("equipment")) + self.equipment._debug_name = "equipment" + self.ground = ListSelector(self.getWidget("ground")) + self.ground._debug_name = "ground" + self.switch = ListSelector(self.getWidget("switchtitles")) + self.switch._debug_name = "switch" + + self.switch.setItems(["inventory", "equipment", "ground"]) + self.menus = { + "inventory": self.inventory, + "equipment": self.equipment, + "ground": self.ground + } + + self.layout.get("switch").select(0) + + + def getWidget(self, name): + return self.layout.get(name) + + def resizeField(self, size): + self.getWidget("field").set_size(*size) + self.getWidget("fieldbackground").change() + + def drawFieldCells(self, cells): + field = self.getWidget("field") + for cell in cells: + (x, y), spriteNames = cell + if not len(spriteNames): + char, fg, bg = self.getChar(' ') + else: + char, fg, bg = self.getChar(spriteNames[0]) + for spriteName in spriteNames[1:]: + if bg is not None: + break + _char, _fg, bg = self.getChar(spriteName) + field.change_cell(x, y, char, TextStyle(fg, bg)) + + + def setFieldCenter(self, pos): + self.getWidget("field").set_center(*pos) + + def setHealth(self, health, maxHealth): + if health is None: + health = 0 + if maxHealth is None: + maxHealth = 0 + self.getWidget("health").set_total(maxHealth) + self.getWidget("health").set_filled(health) + self.getWidget("healthtitle").format({"filled": health, "total":maxHealth}) + + + def showInfo(self, infostring): + self.getWidget("info").set_text(infostring) + + def selectMenu(self, *args, **kwargs): + self.switch.select(*args, **kwargs) + self.layout.get("switch").select(self.getSelectedMenu()) + + def getSelectedMenu(self): + return self.switch.getSelectedItem() + + def getSelectedItem(self, menu=None): + return self._getMenu(menu).getSelected() + + def selectItem(self, menu=None, *args, **kwargs): + self._getMenu(menu).select(*args, **kwargs) + + def _getMenu(self, name=None): + if name is None: + name = self.getSelectedMenu() + name = name.casefold() + return self.menus[name] + + def setInventory(self, items): + self.inventory.setItems(items) + + + def setEquipment(self, slots): + self.equipment.setItems([ + slot + ": " + (item if item else "") + for slot, item in slots + ]) + + def setGround(self, items): + self.ground.setItems(items) + + + def addMessage(self, message, msgtype=None): + if msgtype is not None: + style = TextStyle(*self.messageColours.get(msgtype, (7,0))) + else: + style = None + self.getWidget("msg").add_message(message, style) + + def log(self, message): + self.addMessage(str(message)) + + def scrollBack(self, amount, relative=True): + self.getWidget("msg").scroll(amount, relative) + + def setInputString(self, string, cursor): + self.getWidget("textinput").set_text(string, cursor) + + def update(self): + self.layout.update() + self.screen.update() + + def getChar(self, sprite): + """This returns the character belonging to some spritename. This does not read a character""" + return self.characters.get(sprite, self.defaultChar) + + def update_size(self): + self.screen.reset() + diff --git a/asciifarmclient/gameclient.py b/asciifarmclient/gameclient.py new file mode 100644 index 0000000..d677636 --- /dev/null +++ b/asciifarmclient/gameclient.py @@ -0,0 +1,173 @@ + + +import os +import sys + +import threading +import json +import getpass +import argparse +import string +from queue import Queue + +import ratuil.inputs + +from .inputhandler import InputHandler +from asciifarmclient.common import messages + +class Client: + + def __init__(self, display, name, connection, keybindings, logFile=None): + + self.display = display + self.name = name + self.keepalive = True + self.connection = connection + self.logFile = logFile + self.closeMessage = None + + self.inputHandler = InputHandler(self, keybindings["actions"]) + + self.controlsString = keybindings.get("help", "") + + self.display.showInfo(self.controlsString) + self.queue = Queue() + + + def sendMessage(self, message): + self.connection.send(message.to_json_bytes()) + + 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.command_loop() + + def listen(self): + self.connection.listen(self.pushMessage, self.onConnectionError) + + def pushMessage(self, databytes): + self.queue.put(("message", databytes)) + + def onConnectionError(self, error): + self.queue.put(("error", error)) + + def getInput(self): + while True: + key = ratuil.inputs.get_key() + self.queue.put(("input", key)) + + def close(self, msg=None): + self.keepalive = False + self.closeMessage = msg + + + def update(self, databytes): + if len(databytes) == 0: + self.close("Connection closed by server") + return + datastr = databytes.decode('utf-8') + msg = json.loads(datastr) + message = messages.messages[msg[0]].from_json(msg) + if isinstance(message, messages.ErrorMessage): + error = message.errType + if error == "nametaken": + self.close("error: name is already taken") + return + if error == "invalidname": + self.close("Invalid name error: "+ str(message.description)) + return + self.log(message.errType + ": " + message.description) + elif isinstance(message, messages.MessageMessage): + self.log(message.text, message.type) + elif isinstance(message, messages.WorldMessage): + for msg in message.updates: + self.handleWorldUpdate(msg) + + def handleWorldUpdate(self, msg): + 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": + type, text = msg[1][:2] + self.log(text, type) + 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): + if not isinstance(text, str): + text = str(text) + self.display.addMessage(text, type) + if self.logFile: + with(open(self.logFile, 'a')) as f: + f.write("[{}] {}\n".format(type or "", text)) + + + def command_loop(self): + while self.keepalive: + self.display.update() + action = self.queue.get() + if action[0] == "message": + self.update(action[1]) + elif action[0] == "input": + if action[1] == "^C": + raise KeyboardInterrupt + self.inputHandler.onInput(action[1]) + elif action[0] == "error": + raise action[1] + elif action[0] == "sigwinch": + self.display.update_size() + else: + raise Exception("invalid action in queue") + + def onSigwinch(self, signum, frame): + self.queue.put(("sigwinch", (signum, frame))) + + + + diff --git a/asciifarmclient/inputhandler.py b/asciifarmclient/inputhandler.py new file mode 100644 index 0000000..4281c01 --- /dev/null +++ b/asciifarmclient/inputhandler.py @@ -0,0 +1,98 @@ + +import string + +from .commandhandler import CommandHandler, InvalidCommandException + +import ratuil.inputs as inp + + +class InputHandler: + + def __init__(self, client, keybindings): + self.client = client + self.keybindings = keybindings + self.commandHandler = CommandHandler(self.client) + + self.typing = False + self.string = "" + self.cursor = 0 + + + def onInput(self, key): + if not self.typing: + keyName = key + if keyName in self.keybindings: + self.commandHandler.execute(self.keybindings[keyName]) + else: + self.addKey(key) + + + def processString(self, message): + if message: + if message[0] == '/': + if len(message) == 1: + return + if message[1] == '/': + self.commandHandler.chat(message[1:]) + else: + try: + command, _sep, arg = message[1:].partition(' ') + try: + self.commandHandler.execute([command, arg]) + except Exception as e: + self.log(e) + except InvalidCommandException as e: + self.client.log(", ".join(e.args)) + else: + self.commandHandler.chat(message) + + def startTyping(self, startText=""): + self.typing = True + if startText and not self.string: + self.string = startText + self.cursor = len(self.string) + + self.showString() + + def showString(self): + self.client.display.setInputString(self.string, self.cursor if self.typing else None) + + def addKey(self, key): + if key == inp.BACKSPACE: + self.string = self.string[:self.cursor-1] + self.string[self.cursor:] + self.cursor = max(self.cursor - 1, 0) + elif key == inp.RIGHT: + self.cursor = min(self.cursor + 1, len(self.string)) + elif key == inp.LEFT: + self.cursor = max(self.cursor - 1, 0) + elif key == inp.DELETE: + self.string = self.string[:self.cursor] + self.string[self.cursor+1:] + elif key == inp.HOME: + self.cursor = 0 + elif key == inp.END: + self.cursor = len(self.string) + + elif key == inp.ESCAPE: + # throw away entered string and go back to game + self.typing = False + self.string = "" + self.cursor = 0 + elif key == inp.ENTER: + # process entered string and reset it + message = self.string + self.string = "" + self.cursor = 0 + self.typing = False + self.processString(message) + elif key == "^I": # tab + # return to game but keep entered string + self.typing = False + elif key.isprintable() and len(key) == 1: + self.string = self.string[:self.cursor] + key + self.string[self.cursor:] + self.cursor += len(key) + + self.showString() + + + + diff --git a/asciifarmclient/layout.xml b/asciifarmclient/layout.xml new file mode 100644 index 0000000..d50ecba --- /dev/null +++ b/asciifarmclient/layout.xml @@ -0,0 +1,44 @@ + + + + Health ({filled}/{total}) + + + + + Inventory: + + milk + eggs + bread + + + + Equipment: + + cotton underwear + cotton shirt + jeans + friendship bracelet + + + + Ground: + + concrete + + + + + + + hello + + Welcome to asciifarm + + + + + + + diff --git a/asciifarmclient/listselector.py b/asciifarmclient/listselector.py new file mode 100644 index 0000000..65e6e8c --- /dev/null +++ b/asciifarmclient/listselector.py @@ -0,0 +1,49 @@ + +from asciifarmclient.common import utils + + +class ListSelector: + + def __init__(self, widget): + self.widget = widget + self.items = [] + self.selector = 0 + + def getSelected(self): + return self.selector + + def select(self, value, relative=False, modular=False): + invLen = len(self.items) + if relative: + value += self.selector + if modular and invLen: + value %= invLen + if value < 0: + value = 0 + if value >= invLen: + value = invLen-1 + if value in range(invLen): + self.doSelect(value) + + def doSelect(self, value): + self.selector = value + self.widget.select(value) + + def setItems(self, items): + self.items = items + self.selector = utils.clamp(self.selector, 0, len(items)-1) + self.widget.set_items([self.itemName(item) for item in self.items]) + self.widget.select(self.selector) + + def getItem(self, num): + return self.items[num] + + def getSelectedItem(self): + return self.getItem(self.getSelected()) + + def getNumItems(self): + return len(self.items) + + def itemName(self, item): + return item + diff --git a/asciifarmclient/loaders.py b/asciifarmclient/loaders.py new file mode 100644 index 0000000..efdd1c0 --- /dev/null +++ b/asciifarmclient/loaders.py @@ -0,0 +1,81 @@ + +import os + +from .paths import keybindingsPath, charmapPath +import json + + +standardKeyFiles = { + "default": os.path.join(keybindingsPath, "default.json"), + "azerty": os.path.join(keybindingsPath, "azerty.json") +} + +def loadKeybindings(name): + fname = None + if name in standardKeyFiles: + fname = standardKeyFiles[name] + else: + fname = name + with open(fname) as f: + data = json.load(f) + bindings = {} + help = "" + for ftemplate in data.get("templates", []): + if ftemplate.partition(os.sep)[0] in {".", ".."}: + ftemplate = os.path.relpath(ftemplate, fname) + template = loadKeybindings(ftemplate) + bindings.update(template.get("actions", {})) + help = template.get("help", help) + bindings.update(data.get("actions", {})) + help = data.get("help", help) + return {"actions": bindings, "help": help} + + +standardCharFiles = {name: os.path.join(charmapPath, file) for name, file in { + "default": "fullwidth.json", + "halfwidth": "halfwidth.json", + "hw": "halfwidth.json", + "fullwidth": "fullwidth.json", + "fw": "fullwidth.json", + "emoji": "emoji.json" +}.items()} + +def loadCharmap(name): + fname = None + if name in standardCharFiles: + fname = standardCharFiles[name] + else: + fname = name + with open(fname) as f: + data = json.load(f) + + templates = [] + for ftemplate in data.get("templates", []): + if ftemplate.partition(os.sep)[0] in {".", ".."}: + ftemplate = os.path.relpath(ftemplate, fname) + templates.append(loadCharmap(ftemplate)) + + templates.append(data) + + mapping = {} + writable = {} + default = None + charwidth = 1 + alphabet = "" + msgcolours = {} + + for template in templates: + mapping.update(template.get("mapping", {})) + writable.update(template.get("writable", {})) + default = template.get("default", default) + charwidth = template.get("charwidth", charwidth) + alphabet = template.get("alphabet", alphabet) + msgcolours.update(template.get("msgcolours", {})) + return { + "mapping": mapping, + "writable": writable, + "default": default, + "charwidth": charwidth, + "alphabet": alphabet, + "msgcolours": msgcolours + } diff --git a/asciifarmclient/main.py b/asciifarmclient/main.py new file mode 100644 index 0000000..d720477 --- /dev/null +++ b/asciifarmclient/main.py @@ -0,0 +1,65 @@ +#! /usr/bin/python3 + +import json + +import sys +import termios +import tty +import signal +#import os + +from .connection import Connection +from .gameclient import Client +from .display import Display +from .parseargs import parse_args +from ratuil.screen import Screen + +def main(argv=None): + + (name, socketType, address, keybindings, characters, colours, logfile) = parse_args(argv) + + + connection = Connection(socketType) + try: + connection.connect(address) + except ConnectionRefusedError: + print("ERROR: Could not connect to server.\nAre you sure that the server is running and that you're connecting to the right address?", file=sys.stderr) + return + + error = None + closeMessage = None + + #os.environ.setdefault("ESCDELAY", "25") + + fd = sys.stdin.fileno() + oldterm = termios.tcgetattr(fd) + + try: + + tty.setraw(sys.stdin) + Screen.default.hide_cursor() + + display = Display(characters) + client = Client(display, name, connection, keybindings, logfile) + signal.signal(signal.SIGWINCH, client.onSigwinch) + try: + client.start() + except KeyboardInterrupt: + client.close("^C caught, goodbye") + except Exception as e: + # throw the execption outside ncurses + # so the cleanup can happen first + error = e + closeMessage = client.closeMessage + finally: + ## Set everything back to normal + termios.tcsetattr(fd, termios.TCSADRAIN, oldterm) + Screen.default.finalize() + + + if error is not None: + raise error + + if closeMessage: + print(closeMessage, file=sys.stderr) + diff --git a/asciifarmclient/parseargs.py b/asciifarmclient/parseargs.py new file mode 100644 index 0000000..a8b393b --- /dev/null +++ b/asciifarmclient/parseargs.py @@ -0,0 +1,66 @@ + + +import argparse +import getpass +import json +import os +import os.path + +from . import loaders + + +defaultAdresses = { + "abstract": "asciifarm", + "unix": "asciifarm.socket", + "inet": "localhost:9021", +} + +def parse_args(argv): + + parser = argparse.ArgumentParser(description="The client to AsciiFarm. Run this to connect to to the server.", epilog=""" + Gameplay information: + Walk around and explore the rooms. + 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). 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") + parser.add_argument('-c', '--characters', help='The file with the character mappings for the graphics. If it is either of these names: {} it will be loaded from the charmaps directory.'.format(list(loaders.standardCharFiles.keys())), default="default") + parser.add_argument('-o', '--logfile', help='All game messages will be written to this file.', default=None) + + colourGroup = parser.add_mutually_exclusive_group() + colourGroup.add_argument('-l', '--colours', '--colors', help='enable colours! :)', action="store_true") + colourGroup.add_argument('-b', '--nocolours', '--nocolors', help='disable colours! :)', action="store_true") + + args = parser.parse_args(argv) + + charmap = loaders.loadCharmap(args.characters) + + keybindings = loaders.loadKeybindings(args.keybindings) + + address = args.address + if address is None: + address = defaultAdresses[args.socket] + if args.socket == "abstract": + address = '\0' + address + elif args.socket == "inet": + hostname, sep, port = address.partition(':') + address = (hostname, int(port)) + + colours = True + if args.colours: + colours = True + elif args.nocolours: + colours = False + + name = args.name + if name is None: + username = getpass.getuser() + if args.socket == "unix" or args.socket == "abstract": + name = "~"+username + else: + name = username + + return (name, args.socket, address, keybindings, charmap, colours, args.logfile) diff --git a/asciifarmclient/paths.py b/asciifarmclient/paths.py new file mode 100644 index 0000000..407d54c --- /dev/null +++ b/asciifarmclient/paths.py @@ -0,0 +1,7 @@ + +import os.path + +clientPath = os.path.dirname(__file__) +farmsPath = os.path.normpath(os.path.join(clientPath, "..")) +charmapPath = os.path.join(farmsPath, "charmaps") +keybindingsPath = os.path.join(farmsPath, "keybindings") diff --git a/charmaps/__init__.py b/charmaps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/charmaps/emoji.json b/charmaps/emoji.json new file mode 100644 index 0000000..3049816 --- /dev/null +++ b/charmaps/emoji.json @@ -0,0 +1,10 @@ +{ + "templates": ["fullwidth"], + "mapping":{ + "tree": ["🌳",0,2], + "stairdown": ["↘",7], + "stairup": ["↖",7], + "player": ["🙂",15], + "sword": ["🗡️", 7] + } +} diff --git a/charmaps/fullwidth.json b/charmaps/fullwidth.json new file mode 100644 index 0000000..6d912fe --- /dev/null +++ b/charmaps/fullwidth.json @@ -0,0 +1,52 @@ +{ + "templates": ["halfwidth"], + "mapping":{ + "tree": ["T",0,2], + "wall": ["#",7,8], + "builtwall": ["+",7,8], + "rock": ["#",8,8], + "stone": ["o",7], + "pebble": ["*",7], + "player": ["@",15], + "ground": [".",3], + "grass1": [",",2], + "grass2": ["'",2], + "grass3": ["`",10], + "rabbit": ["b",15], + "water": ["~",4,4], + "floor": [".",8], + "portal": ["$", 5], + "stairdown": [">",7], + "stairup": ["<",7], + "dummy": ["d",3], + "spikes": ["^",7], + "goblin": ["g",2], + "seed": [":",10], + "plantedseed": [".",10], + "seedling": [",",10], + "youngplant": ["v",10], + "smallplant": ["v",2], + "plant": ["Y",2], + "food": ["8",13], + "troll": ["T",1], + "sword": ["/", 7], + "club": ["!", 3], + "house": ["^", 3, 1], + "bridge": ["=", 9,3], + "smallbridge": ["=", 3,4], + "entry": ["%", 7], + "fence": ["#", 3], + "armour": ["[", 7], + "wound": [" ", 7, 1], + "rat": ["r",7], + "closeddoor": ["+",1 ,3], + "opendoor": [""",3], + "raindrop": [":",12], + "snowflake": ["*",15], + " ": [" ", 7] + }, + "default": ["?", 7], + "charwidth": 2, + "alphabet": + "!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +} diff --git a/charmaps/halfwidth.json b/charmaps/halfwidth.json new file mode 100644 index 0000000..d3e16f8 --- /dev/null +++ b/charmaps/halfwidth.json @@ -0,0 +1,64 @@ +{ + "mapping":{ + "tree": ["T", 0, 2], + "wall": ["#", 7, 8], + "builtwall": ["+", 7, 8], + "rock": ["#", 8, 8], + "stone": ["o", 7], + "pebble": ["*", 7], + "player": ["@", 15], + "ground": [".", 3], + "grass1": [",", 2], + "grass2": ["'", 2], + "grass3": ["`", 10], + "water": ["~", 4, 4], + "rabbit": ["b", 15], + "floor": [".", 8], + "portal": ["$", 5], + "stairdown": [">",7], + "stairup": ["<",7], + "dummy": ["d", 3], + "spikes": ["^",7], + "goblin": ["g",2], + "seed": [":",10], + "plantedseed": [".",10], + "seedling": [",", 10], + "youngplant": ["v", 10], + "smallplant": ["v", 2], + "plant": ["Y",2], + "food": ["8",13], + "troll": ["T",1], + "sword": ["/",7], + "club": ["!", 3], + "house": ["^", 3, 1], + "bridge": ["=", 9,3], + "smallbridge": ["=", 3,4], + "entry": ["%", 7], + "fence": ["#", 3], + "armour": ["[", 7], + "wound": [" ", 7, 1], + "rat": ["r", 7], + "closeddoor": ["+",1, 3], + "opendoor": ["\"", 3], + "raindrop": [":", 12], + "snowflake": ["*", 15], + " ": [" ", 7] + }, + "writable": { + "engravedwall": [15, 8], + "emptyletter": [7, 0] + }, + "default": ["?", 7], + "charwidth": 1, + "healthfull": ["#", 7, 2], + "healthempty": ["_", 7, 1], + "msgcolours": { + "chat": [15,0], + "attack": [12,0], + "damage": [9,0], + "heal": [10,0], + "connect": [3,0], + "server": [11,0] + }, + "alphabet": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" +} diff --git a/keybindings/azerty.json b/keybindings/azerty.json new file mode 100644 index 0000000..3d69d07 --- /dev/null +++ b/keybindings/azerty.json @@ -0,0 +1,14 @@ +{ + "templates": ["default"], + "actions": { + "z": ["move", "north"], + "w": null, + "W": null, + "q": ["move", "west"], + "a": ["drop"], + "A": ["take"], + "Z": ["input", ["attack", ["north"]]], + "Q": ["input", ["attack", ["west"]]] + }, + "help": "Controls:\n aqsd or arrows: Move around\n e: Grab\n q: Drop selected\n r: Interact\n f: Attack\n t: Chat\n E: Use selected\n A: Take selected\n xc: select item\n vb: select menu\n ctrl-c: close client" +} diff --git a/keybindings/default.json b/keybindings/default.json new file mode 100644 index 0000000..faf1b40 --- /dev/null +++ b/keybindings/default.json @@ -0,0 +1,42 @@ +{ +"actions": { +"w": ["move", "north"], +"s": ["move", "south"], +"d": ["move", "east"], +"a": ["move", "west"], +"up": ["move", "north"], +"down": ["move", "south"], +"right": ["move", "east"], +"left": ["move", "west"], +"k": ["move", "north"], +"j": ["move", "south"], +"l": ["move", "east"], +"h": ["move", "west"], +"e": ["input", ["take", null]], +"q": ["drop"], +"Q": ["take"], +"E": ["use"], +"R": ["input", ["interact", [null]]], +"r": ["input", ["interact", [null, "north", "south", "east", "west"]]], +"x": ["selectitem", -1, true, true], +"c": ["selectitem", 1, true, true], +"v": ["selectwidget", -1, true, true], +"b": ["selectwidget", 1, true, true], +"-": ["selectitem", -1, true, true], +"+": ["selectitem", 1, true, true], +"/": ["selectwidget", -1, true, true], +"*": ["selectwidget", 1, true, true], +"f": ["input", ["attack", [null, "north", "south", "east", "west"]]], +"F": ["input", ["attack", [null]]], +"W": ["input", ["attack", ["north"]]], +"S": ["input", ["attack", ["south"]]], +"D": ["input", ["attack", ["east"]]], +"A": ["input", ["attack", ["west"]]], +"t": ["runinput"], +"enter": ["runinput"], +"pageup": ["scrollchat", 1], +"pagedown": ["scrollchat", -1], +"/": ["runinput", "/"] +}, +"help": " Controls:\nwasd or arrows: Move around\ne: Grab\nq: Drop selected\nr: Interact\nf: Attack\nt: Chat\nE: Use selected\nxc: select item\nvb: select menu\nctrl-c: close client\nPgUp/PgDn: scroll chat" +} -- cgit