fb785eda by Barry

Huge update and added more protocol handling to the mudserver.py

1 parent 51c565a0
......@@ -7,7 +7,7 @@ def look(id, mud, players, tokens):
del tokens[0]
if len(tokens) == 0:
mud.send_message(id, room_data.get('title'), color=['bold', 'green'])
mud.send_message(id, room_data.get('description'), color='green')
mud.send_message(id, room_data.get('description'), line_ending='\r\n\r\n', color='green')
else:
subject = tokens[0]
items = room_data.get('look_items')
......@@ -46,12 +46,13 @@ def look(id, mud, players, tokens):
if len(playershere) > 0:
mud.send_message(id, "%boldPlayers here:%reset {}".format(", ".join(playershere)))
# send player a message containing the list of exits from this room
mud.send_message(id, "%boldObvious Exits:%reset {}".format(", ".join(room_data.get('exits'))))
mud.send_message(id, "%boldObvious Exits:%reset ", line_ending='')
mud.send_list(id, room_data.get('exits'), "go")
# send player a message containing the list of exits from this room
if len(room_data.get('inventory')) > 0:
mud.send_message(id, "%boldItems here:%reset {}".format(", ".join(room_data.get('inventory').keys())))
mud.send_message(id, "%boldItems here:%reset ", line_ending='')
mud.send_list(id, room_data.get('inventory').keys(), "look")
# send player a message containing the list of players in the room
for mon_name, monster in room_monsters.items():
......
def score(id, players, mud):
from math import floor
mud.send_message(id, " %green+----------------------------=Score=---------------------------+", nowrap=True)
hp = str("%d/%d" % (floor(players[id]['hp']), floor(players[id]['maxhp']))).center(12, ' ')
mp = str("%d/%d" % (floor(players[id]['mp']), floor(players[id]['maxmp']))).center(12, ' ')
sta = str("%d/%d" % (floor(players[id]['sta']), floor(players[id]['maxsta']))).center(12, ' ')
mud.send_message(id, " %%green|%%reset%%bold%%white%s%%reset%%green|" % (players[id]["name"].center(62),), nowrap=True)
mud.send_message(id, " %green+--------------------------------------------------------------+", nowrap=True)
mud.send_message(id, " %%green|%%reset %%cyanHP:%%reset %%white[%s]%%reset %%green|%%reset %%cyanMP:%%reset %%white[%s]%%reset %%green|%%reset %%cyanST:%%reset %%white[%s]%%reset %%green|%%reset" % (hp, mp, sta), nowrap=True)
# output = """
# --------------------------------------------
# HP: {hp} Max HP: {maxhp}
# MP: {mp} Max MP: {maxmp}
# Sta: {sta} Max Sta: {maxsta}
# Experience: {exp}
# Skills:""".format(hp=floor(players[id]['hp']),
# mp=floor(players[id]['mp']),
# maxhp=floor(players[id]['maxhp']),
# maxmp=floor(players[id]['maxmp']),
# sta=floor(players[id]['sta']),
# maxsta=floor(players[id]['maxsta']),
# exp=0)
# mud.send_message(id, output)
mud.send_message(id, " %green+---------------------------=Skills=---------------------------+", nowrap=True)
skills = ["skillasdfasdfasdfasdf", "skill 2 sdfg sdfg", "Skill 3 asdf ewrewwr"]
for skill in skills:
mud.send_message(id, " %%green|%%reset %s%%green|%%reset" % (skill.ljust(61),), nowrap=True)
mud.send_message(id, " %green+--------------------------------------------------------------+", nowrap=True)
score(id, players, mud)
del score
\ No newline at end of file
......@@ -2,6 +2,8 @@ def who(id, players, mud):
mud.send_message(id, "")
mud.send_message(id, "-------- {} Players Currently Online --------".format(len(players)))
for pid, player in players.items():
if player["name"] is None:
continue
mud.send_message(id, " " + player["name"])
mud.send_message(id, "---------------------------------------------".format(len(players)))
mud.send_message(id, "")
......
{
"name": null,
"password": null,
"room": null,
"inventory": {},
"prompt": "hp %hp mp %mp> ",
......
......@@ -11,6 +11,7 @@ from math import floor
from sys import platform
from mudserver import MudServer
from commandhandler import CommandHandler
from utils import load_object_from_file, save_object_to_file
if 'esp' in platform:
......@@ -47,6 +48,8 @@ def show_prompt(pid):
tick = 0.0
spawn = 0.0
cmd_handler = CommandHandler()
# main game loop. We loop forever (i.e. until the program is terminated)
while True:
if 'esp' in platform:
......@@ -104,6 +107,8 @@ while True:
for line in f:
mud.send_message(id, line, "\r")
# send the new player a prompt for their name
#bytes_to_send = bytearray([mud._TN_INTERPRET_AS_COMMAND, mud._TN_WONT, mud._ECHO])
#mud.raw_send(id, bytes_to_send)
mud.send_message(id, "What is your name?")
# go through any recently disconnected players
......@@ -148,9 +153,21 @@ while True:
already_logged_in = True
if already_logged_in:
continue
loaded_player = load_object_from_file("players/{}.json".format(command))
players[id]["name"] = command
mud.remote_echo(id, False)
mud.send_message(id, "Enter Password: ")
elif players[id]["password"] is None:
if command.strip() == '' or command is None:
mud.send_message(id, "Invalid Password")
continue
# TODO: Write password shadow file
loaded_player = load_object_from_file("players/{}.json".format(players[pid]["name"]))
if loaded_player is None:
players[id]["name"] = command
players[id]["password"] = True
players[id]["room"] = "Tavern"
else:
players[id] = loaded_player
......@@ -160,22 +177,21 @@ while True:
# send each player a message to tell them about the new player
mud.send_message(pid, "{} entered the game".format(players[id]["name"]))
mud.remote_echo(id, True)
# send the new player a welcome message
mud.send_message(id, "\r\n\r\nWelcome to the game, {}. ".format(players[id]["name"]) +
"\r\nType 'help' for a list of commands. Have fun!\r\n\r\n")
# send the new player the description of their current room
mud.send_message(id, load_object_from_file('rooms/' + players[id]["room"] + '.json')['description'])
cmd_handler.parse(id, 'look', '', mud, players)
show_prompt(id)
else:
from commandhandler import CommandHandler
if 'esp' in platform:
collect()
cmd_handler = CommandHandler()
handler_results = cmd_handler.parse(id, command, params, mud, players)
show_prompt(id)
cmd_handler.parse(id, command, params, mud, players)
show_prompt(id)
# Start WIFI Setup
if 'esp' in platform:
......
......@@ -3,7 +3,7 @@ def run_mobs(players, mud):
from utils import load_object_from_file, save_object_to_file, calc_att, get_att
for pid, player in players.items():
if not player['name']:
if not player['password']:
continue
if player['mp'] < player['maxmp']:
players[pid]['mp'] += player['mpr']
......
......@@ -12,6 +12,7 @@ import socket
import select
import time
import sys
import struct
from utils import get_color, get_color_list, multiple_replace
......@@ -58,17 +59,34 @@ class MudServer(object):
_READ_STATE_NORMAL = 1
_READ_STATE_COMMAND = 2
_READ_STATE_SUBNEG = 3
_READ_NAWS = 4
_READ_MSDP = 5
_ECHO = 1
_NAWS = 31
_MSDP = 69
_MSSP = 70
_MXP = 91
_MSSP_VAR = 1
_MSSP_VAL = 2
_MSDP_VAR = 1
_MSDP_VAL = 2
_MSDP_TABLE_OPEN = 3
_MSDP_TABLE_CLOSE = 4
_MSDP_ARRAY_OPEN = 5
_MSDP_ARRAY_CLOSE = 6
# Command codes used by Telnet protocol
# See _process_sent_data function
_TN_INTERPRET_AS_COMMAND = 255
_TN_ARE_YOU_THERE = 246
_TN_WILL = 251
_TN_WONT = 252
_TN_DO = 253
_TN_IAC = 255
_TN_DONT = 254
_TN_SUBNEGOTIATION_START = 250
_TN_SUBNEGOTIATION_END = 240
_TN_DO = 253
_TN_WONT = 252
_TN_WILL = 251
_TN_SUB_START = 250
_TN_SUB_END = 240
# socket used to listen for new clients
_listen_socket = None
......@@ -81,6 +99,9 @@ class MudServer(object):
# list of newly-added occurences
_new_events = []
start_time = time.time()
def __init__(self):
"""Constructs the MudServer object and starts listening for
new players.
......@@ -96,8 +117,7 @@ class MudServer(object):
# set a special option on the socket which allows the port to be
# immediately without having to wait
self._listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,
1)
self._listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# bind the socket to an ip address and port. Port 23 is the standard
# telnet port which telnet clients will use, however on some platforms
......@@ -183,7 +203,7 @@ class MudServer(object):
# return the info list
return retval
def send_message(self, to, message, line_ending='\r\n', color=None):
def send_message(self, to, message, line_ending='\r\n', color=None, nowrap=False):
"""Sends the text in the 'message' parameter to the player with
the id number given in the 'to' parameter. The text will be
printed out in the player's terminal.
......@@ -191,8 +211,11 @@ class MudServer(object):
# we make sure to put a newline on the end so the client receives the
# message on its own line
message = multiple_replace(message, get_color_list())
chunks, chunk_size = len(message), 80 #len(x)/4
lines = [message[i:i + chunk_size] for i in range(0, chunks, chunk_size)]
if nowrap:
lines = [message]
else:
chunks, chunk_size = len(message), 80 #len(x)/4
lines = [message[i:i + chunk_size] for i in range(0, chunks, chunk_size)]
if color:
if isinstance(color, list):
colors = ''.join([get_color(c) for c in color])
......@@ -214,6 +237,28 @@ class MudServer(object):
# stop listening for new clients
self._listen_socket.close()
def send_list(self, clid, list_items, command=''):
if self._clients[clid].MXP_ENABLED:
output = ""
if command != '':
command = command + ' '
for idx, item in enumerate(list_items):
if idx > 0:
output += ", "
output += "<send \"{}&text;\">{}</send>".format(command, item)
self.mxp_secure(clid, output)
else:
self.send_message(clid, ', '.join(list_items))
def mxp_secure(self, clid, message):
bytes_to_send = bytearray("\x1b[1z{}\x1b[6z\r\n".format(message), 'utf-8')
client_socket = self._clients[clid].socket
client_socket.sendall(bytes_to_send)
def raw_send(self, clid, bytes_to_send):
client_socket = self._clients[clid].socket
client_socket.sendall(bytes_to_send)
def _attempt_send(self, clid, data):
# python 2/3 compatability fix - convert non-unicode string to unicode
if sys.version < '3' and type(data) != unicode:
......@@ -229,8 +274,8 @@ class MudServer(object):
client_socket = self._clients[clid].socket
client_socket.sendall(bytes_to_send)
# KeyError will be raised if there is no client with the given id in
# the map
# KeyError will be raised if there is no client with the given id in
# the map
except KeyError:
pass
# If there is a connection problem with the client (e.g. they have
......@@ -266,11 +311,24 @@ class MudServer(object):
# 'recv' will return immediately without waiting
joined_socket.setblocking(False)
# construct a new _Client object to hold info about the newly connected
# client. Use 'nextid' as the new client's id number
self._clients[self._nextid] = MudServer._Client(joined_socket, addr[0],
"", time.time())
MSSP_REQUEST = bytearray([self._TN_IAC, self._TN_WILL, self._MSSP])
joined_socket.sendall(MSSP_REQUEST)
# MSDP_REQUEST = bytearray([self._TN_IAC, self._TN_WILL, self._MSDP])
# joined_socket.sendall(MSDP_REQUEST)
MXP_REQUEST = bytearray([self._TN_IAC, self._TN_WILL, self._MXP])
joined_socket.sendall(MXP_REQUEST)
NAWS_REQUEST = bytes([self._TN_IAC, self._TN_DO, self._NAWS])
joined_socket.sendall(NAWS_REQUEST)
# add a new player occurence to the new events list with the player's
# id number
self._new_events.append((self._EVENT_NEW_PLAYER, self._nextid))
......@@ -317,7 +375,7 @@ class MudServer(object):
try:
# read data from the socket, using a max length of 4096
data = cl.socket.recv(1024).decode("latin1")
data = cl.socket.recv(4096)
# process the data, stripping out any special Telnet commands
message = self._process_sent_data(cl, data)
......@@ -357,6 +415,52 @@ class MudServer(object):
# player's id number
self._new_events.append((self._EVENT_PLAYER_LEFT, clid))
def remote_echo(self, clid, enable=True):
if enable:
bytes_to_send = bytearray([self._TN_IAC, self._TN_WONT, self._ECHO])
else:
bytes_to_send = bytearray([self._TN_IAC, self._TN_WILL, self._ECHO])
client_socket = self._clients[clid].socket
client_socket.sendall(bytes_to_send)
def _send_mssp(self, client):
byte_data = bytearray([self._TN_IAC, self._TN_SUB_START, self._MSSP, self._MSSP_VAR])
byte_data.extend(b'PLAYERS')
byte_data.append(self._MSSP_VAL)
byte_data.extend(str(len(self._clients)).encode())
byte_data.append(self._MSSP_VAR)
byte_data.extend(b'UPTIME')
byte_data.append(self._MSSP_VAL)
byte_data.extend(str(time.time() - self.start_time).encode())
byte_data.append(self._MSSP_VAR)
byte_data.extend(b'NAME')
byte_data.append(self._MSSP_VAL)
byte_data.extend(b'WeeMud')
byte_data.extend([self._TN_IAC, self._TN_SUB_END])
print("SENDING MSSP")
print(byte_data)
client.socket.sendall(byte_data)
# def _send_msdp_array(self, client, var, vals):
# byte_data = bytearray([self._TN_IAC, self._TN_SUB_START, self._MSDP, self._MSDP_VAR])
# byte_data.extend(var)
# byte_data.extend([self._MSDP_VAL, self._MSDP_ARRAY_OPEN])
# for val in vals:
# byte_data.append(self._MSDP_VAL)
# byte_data.extend(val)
# byte_data.extend([self._MSDP_ARRAY_CLOSE, self._TN_IAC, self._TN_SUB_END])
# print("SENDING ARRAY")
# out = ""
# out2 = ""
# for b in byte_data:
# out += " " + str(b)
# out2 += " " + chr(b)
# print(out)
# print(out2)
# print(byte_data.hex())
# client.socket.sendall(byte_data)
def _process_sent_data(self, client, data):
# the Telnet protocol allows special command codes to be inserted into
......@@ -364,12 +468,22 @@ class MudServer(object):
# any of these codes, but we must at least detect and skip over them
# so that we don't interpret them as text data.
# More info on the Telnet protocol can be found here:
# http://pcmicro.com/netfoss/telnet.html
# http://pcmicro.com/netfoss/telnet.htm
# start with no message and in the normal state
message = None
state = self._READ_STATE_NORMAL
out = ""
out2 = ""
for b in data:
out += " " + str(b)
out2 += " " + chr(b)
print(out)
print(out2)
option_data = bytearray()
option_state = 0
option_support = 0
# go through the data a character at a time
for c in data:
......@@ -381,13 +495,13 @@ class MudServer(object):
# if we received the special 'interpret as command' code,
# switch to 'command' state so that we handle the next
# character as a command code and not as regular text data
if ord(c) == self._TN_INTERPRET_AS_COMMAND:
if c == self._TN_IAC:
state = self._READ_STATE_COMMAND
# if we get a newline character, this is the end of the
# message. Set 'message' to the contents of the buffer and
# clear the buffer
elif c == "\n":
elif chr(c) == "\n":
message = client.buffer
client.buffer = ""
......@@ -395,13 +509,13 @@ class MudServer(object):
# types them. So if we get a backspace character, this is where
# the user has deleted a character and we should delete the
# last character from the buffer.
elif c == "\x08":
elif chr(c) == "\x08":
client.buffer = client.buffer[:-1]
# otherwise it's just a regular character - add it to the
# buffer where we're building up the received message
else:
client.buffer += c
client.buffer += chr(c)
# command state
elif state == self._READ_STATE_COMMAND:
......@@ -410,16 +524,41 @@ class MudServer(object):
# that the following characters are a list of options until
# we're told otherwise. We switch into 'subnegotiation' state
# to handle this
if ord(c) == self._TN_SUBNEGOTIATION_START:
if c == self._TN_SUB_START:
state = self._READ_STATE_SUBNEG
# if the command code is one of the 'will', 'wont', 'do' or
# 'dont' commands, the following character will be an option
# code so we must remain in the 'command' state
elif ord(c) in (self._TN_WILL, self._TN_WONT, self._TN_DO,
elif c in (self._TN_WILL, self._TN_WONT, self._TN_DO,
self._TN_DONT):
option_support = c
state = self._READ_STATE_COMMAND
elif c == self._MXP:
if option_support == self._TN_DO:
print("MXP Enabled")
client.MXP_ENABLED = True
# Enable for mushclient "on command"
byte_data = bytearray([self._TN_IAC, self._TN_SUB_START, self._MXP, self._TN_IAC, self._TN_SUB_END])
client.socket.sendall(byte_data)
else:
print("MXP Disabled")
client.MXP_ENABLED = False
state = self._READ_STATE_NORMAL
elif c == self._MSSP:
if option_support == self._TN_DO:
self._send_mssp(client)
state = self._READ_STATE_NORMAL
# elif c == self._MSDP:
# if option_support == self._TN_DO:
# client.MSDP_ENABLED = True
# else:
# client.MSDP_ENABLED = False
# state = self._READ_STATE_NORMAL
# for all other command codes, there is no accompanying data so
# we can return to 'normal' state.
else:
......@@ -427,12 +566,59 @@ class MudServer(object):
# subnegotiation state
elif state == self._READ_STATE_SUBNEG:
# if we reach an 'end of subnegotiation' command, this ends the
# list of options and we can return to 'normal' state.
# Otherwise we must remain in this state
if ord(c) == self._TN_SUBNEGOTIATION_END:
if c == self._TN_SUB_END:
if option_state == self._READ_NAWS:
height = 30
width = 100
height, width = struct.unpack('>hh', option_data)
if height > 0:
client.height = height
if width > 0:
client.width = width
print("Got NAWS Width: %d Height: %d" % (client.width, client.height))
option_state = 0
option_data = bytearray()
# elif option_state == self._READ_MSDP:
# print("GOT MSDP")
# print(option_data)
# if option_data[0] == self._MSDP_VAR:
# var = ''
# val = ''
# val_started = False
# for v in option_data[1:]:
# print(v)
# if v == self._TN_IAC:
# break
# if v == self._MSDP_VAL:
# val_started = True
# continue
# if val_started:
# val += chr(v)
# else:
# var += chr(v)
# print("VAR: " + var)
# print("VAL: " + val)
# if var == "LIST" and val == "COMMANDS":
# vals = [b"LIST", b"REPORT", b"SEND"]
# self._send_msdp_array(client, b"COMMANDS", vals)
state = self._READ_STATE_NORMAL
# if c == self._MSDP:
# option_state = self._READ_MSDP
# option_data = bytearray()
if c == self._NAWS:
option_state = self._READ_NAWS
option_data = bytearray()
# if option_state in [self._READ_NAWS, self._READ_MSDP] and c not in [self._MSDP, self._NAWS] and c != self._TN_IAC:
if option_state in [self._READ_NAWS] and c not in [self._NAWS] and c != self._TN_IAC:
option_data.append(c)
# return the contents of 'message' which is either a string or None
return message
......
{"name": "test", "room": "Room001", "inventory": {"candle": 1}, "prompt": "h %hp m %mp s %st> ", "aliases": {}, "hp": 74, "mp": 5.0, "sta": 1.2000000000000361, "aa": "1d2", "mpr": 0.25, "star": 0.4, "maxhp": 953, "maxmp": 10, "maxsta": 10, "weapon": "sword", "sp": {"fireball": {"cost": 5, "dmg": "2d4", "desc": "A fireball blasts from your fingertips striking the target"}}, "at": {"kick": {"cost": 5, "dmg": "2d4", "desc": "You unleash a powerful kick"}}}
\ No newline at end of file
{"name": "test", "password": true, "room": "Tavern", "inventory": {"candle": 1}, "prompt": "h %hp m %mp s %st> ", "aliases": {}, "hp": 74, "mp": 10.0, "sta": 10.00000000000004, "aa": "1d2", "mpr": 0.25, "star": 0.4, "maxhp": 953, "maxmp": 10, "maxsta": 10, "weapon": "sword", "sp": {"fireball": {"cost": 5, "dmg": "2d4", "desc": "A fireball blasts from your fingertips striking the target"}}, "at": {"kick": {"cost": 5, "dmg": "2d4", "desc": "You unleash a powerful kick"}}}
\ No newline at end of file
......