"""
advanced_telopt.py
Adds advanced TELOPT negotiations to the MUD, including support for:
* TELOPT_NAWS - Negotiate About Window Size
Determine the size of the client window in columns and rows.
* TELOPT_TTYPE - Terminal Type
Determine the connected client to estimate its capabilities.
* TELOPT_EOR - End-of-Record
Mark the end of text transmission from the server with IAC EOR to let a
client know the exact point. If enabled, this is automatically appended
to the end of the prompt.
* TELOPT_MSP - MUD Sound Protocol
Trigger the playback of sounds and music on the client. Only basic support
is included... negotiation and the triggering of hooks when it's enabled
or disabled. The rest of the implementation is up to the MUD creator, though
it consists merely of sending your own MSP commands to the client.
* TELOPT_MSDP - MUD Server Data Protocol
MSDP allows you to easilly send variables to the client.
* TELOPT_MXP - MUD eXtension Protocol
MXP is, for those who don't know, an application of HTML-like tags to the
MUD itself, allowing for richer interaction with the user. It is in no way
critical to playing and enjoying the game, but it is a nice touch.
Note that this implementation of MXP isn't complete, as it doesn't handle
asking the client which tags it supports. I'd recommend keeping yourself
limited to simple tags like <SEND> if you don't add <SUPPORTS> support.
For more information about MXP, visit http://www.zuggsoft.com/zmud/mxp.htm
* Window Title Text
Set a custom string to the window's title bar. This also works together
with TELOPT_TTYPE to avoid sending the code to clients that wouldn't
properly interpret it.
If the bell() function of a socket is called, the title will be flashed
with two prepended asterisks until the next command is recieved. This can
be used to notify a distracted player when the MUD needs their input.
* XTERM 256 and ANSI Colors
This file also handles color code parsing, taking that functionality away
from colour.py as to combine color formatting and MXP conversion into one
post-processing function. I haven't tested it, but I can only assume that
one less function call makes things easier on the server.
"""
import mud
import mudsys, mudsock, storage, auxiliary, hooks, telnetlib, re, event, random
################################################################################
# variables
################################################################################
# Various Regex Expressions
coloreg = re.compile('{([ndrgybpcw{]|[0-5][0-5][0-5]|6[0-1]\d|62[0-3]|7)', re.IGNORECASE)
mxpreg = re.compile('[<>&]')
linkreg = re.compile("""(?<!=['"])((?:https?|telnet)://(?:\w+(?:\.-\w*)+|(?:[a-z0-9\-]*(?:[a-z0-9\-])\.)+(?:com|edu|biz|gov|in(?:t|fo)|mil|net|org|[a-zA-Z]\.[a-z][a-z]))(?::\d{1,4})?(?:/[^.!,?;"\'<>()\[\]\{\}\s\x1b\x7F-\xFF]*(?:[.!,?]+[^.!,?;"\'<>()\[\]\{\}\s\x1b\x7F-\xFF]+)*)?)""",re.IGNORECASE)
# List of all the letter colors.
colorletters = 'drgybpcwDRGYBPCW'
# ANSI Color Replacements
extcolors = ['0;30', '0;31', '0;32', '0;33', '0;34', '0;35', '0;36', '0;37',
'1;30', '1;31', '1;32', '1;33', '1;34', '1;35', '1;36', '1;37', '0;30', '0;34',
'0;34', '0;34', '1;34', '1;34', '0;32', '0;36', '0;36', '0;36', '1;34', '1;34',
'0;32', '0;36', '0;36', '0;36', '1;36', '1;36', '0;32', '0;36', '0;36', '0;36',
'1;36', '1;36', '1;32', '1;32', '1;36', '1;36', '1;36', '1;36', '1;32', '1;32',
'1;36', '1;36', '1;36', '1;36', '0;31', '0;35', '0;35', '0;35', '1;34', '1;34',
'0;33', '1;30', '0;34', '0;34', '1;34', '1;34', '0;33', '0;32', '0;36', '0;36',
'1;34', '1;34', '0;33', '0;32', '0;36', '0;36', '1;36', '1;36', '1;32', '1;32',
'1;32', '1;36', '1;36', '1;36', '1;32', '1;32', '1;32', '1;36', '1;36', '1;36',
'0;31', '0;35', '0;35', '0;35', '1;35', '1;35', '0;33', '0;31', '0;35', '0;35',
'1;34', '1;34', '0;33', '0;33', '0;37', '0;37', '1;34', '1;34', '0;33', '0;33',
'0;37', '0;37', '1;36', '1;34', '1;33', '1;32', '1;32', '1;36', '1;36', '1;36',
'1;33', '1;32', '1;32', '1;32', '1;36', '1;36', '0;31', '0;35', '0;35', '0;35',
'1;35', '1;35', '0;33', '0;31', '0;35', '0;35', '1;35', '1;35', '0;33', '0;33',
'0;37', '0;37', '1;35', '1;34', '0;33', '0;33', '0;37', '0;37', '1;37', '1;34',
'1;33', '1;33', '1;33', '1;37', '1;37', '1;36', '1;33', '1;33', '1;32', '1;32',
'1;36', '1;36', '1;31', '1;31', '1;35', '1;35', '1;35', '1;35', '1;31', '1;31',
'1;31', '1;35', '1;35', '1;35', '1;33', '1;31', '1;31', '1;35', '1;35', '1;35',
'1;33', '1;33', '1;33', '1;37', '1;37', '1;35', '1;33', '1;33', '1;33', '1;37',
'1;37', '1;37', '1;33', '1;33', '1;33', '1;33', '1;37', '1;37', '1;31', '1;31',
'1;35', '1;35', '1;35', '1;35', '1;31', '1;31', '1;31', '1;35', '1;35', '1;35',
'1;33', '1;31', '1;31', '1;31', '1;35', '1;35', '1;33', '1;33', '1;31', '1;31',
'1;35', '1;35', '1;33', '1;33', '1;33', '1;33', '1;37', '1;37', '1;33', '1;33',
'1;33', '1;33', '1;37', '1;37', '0;30', '0;30', '1;30', '1;30', '1;30', '1;30',
'1;30', '1;30', '1;30', '1;30', '1;30', '1;30', '1;30', '0;37', '0;37', '0;37',
'0;37', '0;37', '0;37', '1;37', '1;37', '1;37', '1;37', '1;37']
htmlcolors = ['#000000', '#800000', '#008000', '#808000', '#000080', '#800080',
'#008080', '#C0C0C0', '#808080', '#FF0000', '#00FF00', '#FFFF00', '#0000FF',
'#FF00FF', '#00FFFF', '#FFFFFF', '#000000', '#00005f', '#000087', '#0000af',
'#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7',
'#005fff', '#008700', '#00875f', '#008787', '#0087af', '#0087d7', '#0087ff',
'#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700',
'#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f',
'#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087',
'#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf',
'#5f5fd7', '#5f5fff', '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7',
'#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff',
'#5fd700', '#5fd75f', '#5fd787', '#5fd7af', '#5fd7d7', '#5fd7ff', '#5fff00',
'#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f',
'#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f', '#875f87',
'#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af',
'#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7',
'#87afff', '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff',
'#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000',
'#af005f', '#af0087', '#af00af', '#af00d7', '#af00ff', '#af5f00', '#af5f5f',
'#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787',
'#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f', '#afaf87', '#afafaf',
'#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7',
'#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff',
'#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00',
'#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f',
'#d78787', '#d787af', '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87',
'#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af',
'#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f', '#d7ff87', '#d7ffaf', '#d7ffd7',
'#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff',
'#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff', '#ff8700',
'#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f',
'#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787',
'#ffd7af', '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf',
'#ffffd7', '#ffffff', '#191919', '#232323', '#2d2d2d', '#373737', '#414141',
'#4b4b4b', '#555555', '#5f5f5f', '#696969', '#737373', '#7d7d7d', '#878787',
'#919191', '#9b9b9b', '#a5a5a5', '#afafaf', '#b9b9b9', '#c3c3c3', '#cdcdcd',
'#d7d7d7', '#e1e1e1', '#ebebeb', '#f5f5f5', '#ffffff']
# Values of the 6x6x6 Cube
colorvalues = [0, 95, 135, 175, 215, 255]
# ANSI Color Codes
colors = {'n' : '0',
'd' : '30',
'r' : '31',
'g' : '32',
'y' : '33',
'b' : '34',
'p' : '35',
'c' : '36',
'w' : '37'}
# The file for copyover data copying
copyover_file = "../lib/misc/copyover"
# TELOPT Codes
MSDP = 'E' # "%c" % 69
MSP = 'Z' # "%c" % 90
MXP = '[' # "%c" % 91
# TELOPT_MSP: Handshake Strings
server_will_msp = telnetlib.IAC + telnetlib.WILL + MSP
server_wont_msp = telnetlib.IAC + telnetlib.WONT + MSP
client_do_msp = telnetlib.IAC + telnetlib.DO + MSP
client_dont_msp = telnetlib.IAC + telnetlib.DONT + MSP
# TELOPT_MXP: Handshake Strings
server_will_mxp = telnetlib.IAC + telnetlib.WILL + MXP
server_wont_mxp = telnetlib.IAC + telnetlib.WONT + MXP
client_do_mxp = telnetlib.IAC + telnetlib.DO + MXP
client_dont_mxp = telnetlib.IAC + telnetlib.DONT + MXP
server_start_mxp = telnetlib.IAC + telnetlib.SB + MXP + telnetlib.IAC + telnetlib.SE
# TELOPT_NAWS: Handshake Strings
server_do_naws = telnetlib.IAC + telnetlib.DO + telnetlib.NAWS
client_will_naws = telnetlib.IAC + telnetlib.WILL + telnetlib.NAWS
client_naws = telnetlib.IAC + telnetlib.SB + telnetlib.NAWS
# TELOPT_TTYPE: Handshake Strings
server_do_ttype = telnetlib.IAC + telnetlib.DO + telnetlib.TTYPE
client_will_ttype = telnetlib.IAC + telnetlib.WILL + telnetlib.TTYPE
server_send_ttype = telnetlib.IAC + telnetlib.SB + telnetlib.TTYPE + telnetlib.ECHO + telnetlib.IAC + telnetlib.SE # ECHO = SEND
client_ttype_is = telnetlib.IAC + telnetlib.SB + telnetlib.TTYPE
# TELOPT_EOR: Handshake Strings
server_will_eor = telnetlib.IAC + telnetlib.WILL + telnetlib.EOR
server_wont_eor = telnetlib.IAC + telnetlib.WONT + telnetlib.EOR
client_do_eor = telnetlib.IAC + telnetlib.DO + telnetlib.EOR
client_dont_eor = telnetlib.IAC + telnetlib.DONT + telnetlib.EOR
server_eor_code = telnetlib.IAC + '\xEF'
# TELOPT_MSDP: Handshake Strings
server_will_msdp = telnetlib.IAC + telnetlib.WILL + MSDP
client_do_msdp = telnetlib.IAC + telnetlib.DO + MSDP
client_dont_msdp = telnetlib.IAC + telnetlib.DONT + MSDP
msdp_start = telnetlib.IAC + telnetlib.SB + MSDP
msdp_end = telnetlib.IAC + telnetlib.SE
# MSDP Strings
MSDP_VAR = '\x01' # "%c" % 1
MSDP_VAL = '\x02' # "%c" % 2
# MXP Line Tags
mxp_escape = '\x1b['
mxp_open_line = mxp_escape + '0z'
mxp_secure_line = mxp_escape + '1z'
mxp_locked_line = mxp_escape + '2z'
mxp_reset = mxp_escape + '3z'
mxp_temp_secure = mxp_escape + '4z'
mxp_lock_open = mxp_escape + '5z'
mxp_lock_secure = mxp_escape + '6z'
mxp_lock_locked = mxp_escape + '7z'
# MXP Tag Markers
mxp_tag_start = '\x01MXP\x03'
mxp_tag_end = '\x02MXP\x03'
mxp_and = '\x04MXP\x03'
# MXP Secure Tags
mxp_secure_tags = ['SEND','A','VAR','NOBR','P','BR','SBR','EXPIRE','VERSION','SUPPORT','GAUGE','STAT','SOUND','MUSIC','FRAME','DEST','RELOCATE','IMAGE','FILTER']
# MXP Tags
mxp_tags = []
# Known clients.
clients = ['jmud','xterm','fmud','zmud','mushclient','tintin++','ansi','unknown']
# Known clients without proper title support
no_title = ['fmud','zmud','mushclient','ansi','unknown']
# Known clients with proper xterm 256 colors support
clients256 = ['jmud','zmud','mushclient','tintin++','xterm']
################################################################################
# Auxiliary Data for the Socket
################################################################################
class VariableData:
def __init__(self):
self.pairs = {}
def __getitem__(self, key):
return self.pairs[key]
def __setitem__(self, key, val):
self.pairs[key] = val
def __delitem__(self, key):
del self.pairs[key]
def has_key(self, key):
return self.pairs.has_key(key)
def copy(self):
return self.pairs.copy()
def __str__(self):
return str(self.pairs)
class TeloptAux:
def __init__(self, set = None):
self.nMXP = False
self.MXP = 0
self.mxp_sec = False
self.nMSP = False
self.MSP = 0
self.nMSDP = False
self.MSDP = 0
self.vMSDP = {}
self.title = 0
self.ntitle = False
self.title_s = ''
self.bell = False
self.bellon = False
self.cols = 80
self.rows = 24
self.client = 'unknown'
self.eor = False
self.n256 = False
self.c256 = 0
if set != None:
self.load(set)
def copy(self):
newdata = TeloptAux()
self.copyTo(newdata)
return newdata
def copyTo(self, to):
to.nMXP = self.nMXP
to.MXP = self.MXP
to.mxp_sec = self.mxp_sec
to.nMSP = self.nMSP
to.MSP = self.MSP
to.nMSDP = self.nMSDP
to.MSDP = self.MSDP
to.vMSDP = self.vMSDP.copy()
to.title = self.title
to.ntitle = self.ntitle
to.title_s = self.title_s
to.bell = self.bell
to.bellon = self.bellon
to.cols = self.cols
to.rows = self.rows
to.client = self.client
to.eor = self.eor
to.n256 = self.n256
to.c256 = self.c256
def load(self, set = None):
if set is not None:
self.nMXP = set.readBool("nMXP")
self.MXP = set.readInt("MXP")
self.mxp_sec = set.readBool("mxp_sec")
self.nMSP = set.readBool("nMSP")
self.MSP = set.readInt("MSP")
self.nMSDP = set.readBool("nMSDP")
self.MSDP = set.readInt("MSDP")
self.title = set.readInt("title")
self.ntitle = set.readBool("ntitle")
self.title_s = set.readString("title_s")
self.bell = set.readBool("bell")
self.bellon = set.readBool("bellon")
self.cols = set.readInt("cols")
self.rows = set.readInt("rows")
self.client = set.readString("client")
self.eor = set.readBool("eor")
self.n256 = set.readBool("n256")
self.c256 = set.readInt("c256")
def store(self):
set = storage.StorageSet()
set.storeBool("nMXP", self.nMXP)
set.storeInt( "MXP", self.MXP)
set.storeBool("mxp_sec", self.mxp_sec)
set.storeBool("nMSP", self.nMSP)
set.storeInt( "MSP", self.MSP)
set.storeBool("nMSDP", self.nMSDP)
set.storeInt( "MSDP", self.MSDP)
set.storeInt( "title", self.title)
set.storeBool("ntitle", self.ntitle)
set.storeString("title_s", self.title_s)
set.storeBool("bell", self.bell)
set.storeBool("bellon", self.bellon)
set.storeInt( "cols", self.cols)
set.storeInt( "rows", self.rows)
set.storeString("client", self.client)
set.storeBool("eor", self.eor)
set.storeBool("n256", self.n256)
set.storeInt( "c256", self.c256)
return set
################################################################################
# Functions
################################################################################
def register_mxp_flag(name, flag):
'''Add a new MXP flag to the list of tags. This is good for easilly setting a list of
variables for things like the prompt, or adding custom tags for flagging room
names and descriptions and the like.'''
register_mxp_tag(name, 'FLAG="'+flag+'"', True)
def register_mxp_tag(name, args = None, secure = False):
'''Add a new MXP tag to the list of tags. The name must be an alphanumeric string, and
args should be a string of the element's arguments, though it could be None. If secure
is true, the tag will be added to a list of flags that are sent via an MXP secure line.'''
if type(name) != str or not name.isalnum(): raise TypeError("Name must be an alphanumeric string.")
args = mxpreg.sub(lambda m: [[mxp_tag_start,mxp_tag_end][m.group() == '>'],'&'][m.group() == '&'], args)
mxp_tags.append("%s!ELEMENT %s%s%s" % (mxp_tag_start, name, ['',' '+str(args)][len(str(args)) > 0], mxp_tag_end))
if secure:
mxp_secure_tags.append(name)
def mxp_tag(sock, text = '', tag = '', args = '', closing = False):
'''Output a MXP tag. If it's a secure tag, flag the socket so it's next line is sent secure.
If closing is true and text is not supplied, only a closing tag will be returned. Be sure that args is None if you're making a closing tag.
If text is None, only a single tag will be created. This can be useful for sending things like the <IMAGE> tag.'''
if not sock: return text
if text is not None: text = str(text)
if tag and tag in mxp_secure_tags: sock.telopt.mxp_sec = True
if not sock.MXP or not tag: return text
elif text and args: return '%s%s %s%s%s%s/%s%s' % (mxp_tag_start,tag,args,mxp_tag_end,text,mxp_tag_start,tag,mxp_tag_end)
elif text and not args: return '%s%s%s%s%s/%s%s' % (mxp_tag_start,tag,mxp_tag_end,text,mxp_tag_start,tag,mxp_tag_end)
elif not text and args: return '%s%s%s %s%s' % (mxp_tag_start,closing and '/' or '', tag, args, mxp_tag_end)
elif not text and not args: return '%s%s%s%s' % (mxp_tag_start, closing and '/' or '', tag, mxp_tag_end)
else: return text
def copyover():
'''Saves the telopt auxiliary data of all active connections with set players
to be restored after the copyover is complete.'''
set = storage.StorageSet()
list = storage.StorageList()
# Iterate through the sockets and save the ones with connected users. Ignore
# the others since they're simply disconnected.
for sk in mudsock.socket_list():
if sk.ch:
s = storage.StorageSet()
s.storeString("k", sk.ch.name)
s.storeSet("v", sk.telopt.store())
list.add(s)
# Store and close the set
set.storeList("list", list)
set.write(copyover_file)
set.close()
def send_title(sock):
'''Sends the current title string to the socket.'''
sock.trans_raw('\33]2;' + (sock.telopt.bellon and '**' or '') + sock.telopt.title_s + '\33\\')
################################################################################
# Colour Conversion Functions
# Since colors are hard-coded now, these functions are just included to show
# you how it was done in case you want to play with it.
################################################################################
def rgb2hsv(r, g, b):
'''Convert an RGB color to HSV.'''
r,g,b = r / 255.0, g / 255.0, b / 255.0
mn = min(r,g,b) # Minimum Value
mx = max(r,g,b) # Maximum Value
dm = mx - mn # Delta Value
v = mx
if dm == 0: return 0,0,v # This is a gray, no chroma.
else: s = dm / mx
dr = ((( mx - r ) / 6) + (dm / 2) ) / dm
dg = ((( mx - g ) / 6) + (dm / 2) ) / dm
db = ((( mx - b ) / 6) + (dm / 2) ) / dm
if r == mx: h = db - dg
elif g == mx: h = (1/3.0) + dr - db
elif b == mx: h = (2/3.0) + dg - dr
if h < 0: h += 1
if h > 1: h -= 1
return h,s,v
def simplecolor(c, html = False):
'''Simplify an xterm 256 color code down to ANSI 16. It isn't perfect,
but it works a whole lot better than stripping all color, or letting
people with poor client choices from seeing a screen of garbled mess.
Will return an HTML hex color code if html is true'''
if c < 16:
# This is one of the 16 ANSI colors
cs = 'drgybpcw'
if html:
return '#'+['000000','800000','008000','808000','000080','800080','008080','C0C0C0','808080','FF0000','00FF00','FFFF00','0000FF','FF00FF','00FFFF','FFFFFF'][c]
else:
if c > 7: return '1;'+colors[cs[c-8]]
else: return '0;'+colors[cs[c]]
elif c > 231:
# Grayscale
if html:
v = min(255,15 + ((c - 231) * 10))
return '#%02x%02x%02x' % (v,v,v)
else:
if c > 250: return '1;'+colors['w']
elif c > 244: return '0;'+colors['w']
elif c > 233: return '1;'+colors['d']
else: return '0;'+colors['d']
else:
# From the 6x6x6
if html:
return '#%02x%02x%02x' % (colorvalues[((c-16)/36)%6], colorvalues[((c-16)/6)%6], colorvalues[(c-16)%6])
h,s,v = rgb2hsv(colorvalues[((c-16)/36)%6], colorvalues[((c-16)/6)%6], colorvalues[(c-16)%6])
# If the saturation is very low, treat it as a gray.
if s < 0.25:
if v > 0.75: return '1;'+colors['w']
elif v > 0.50: return '0;'+colors['w']
elif v > 0.25: return '1;'+colors['d']
else: return '0;'+colors['d']
if h < 3/36.0 or h > 33/36.0:
# Red
if v > 0.75: return '1;'+colors['r']
else: return '0;'+colors['r']
elif h > 3/36.0 and h < 9/36.0:
# Yellow
if v > 0.75: return '1;'+colors['y']
else: return '0;'+colors['y']
elif h > 9/36.0 and h < 15/36.0:
# Green
if v > 0.75: return '1;'+colors['g']
else: return '0;'+colors['g']
elif h > 15/36.0 and h < 21/36.0:
# Cyan
if v > 0.75: return '1;'+colors['c']
else: return '0;'+colors['c']
elif h > 21/36.0 and h < 27/36.0:
# Blue
if v > 0.75: return '1;'+colors['b']
else: return '0;'+colors['b']
elif h > 27/36.0 and h < 33/36.0:
# Purple
if v > 0.75: return '1;'+colors['p']
else: return '0;'+colors['p']
else:
# Wha...?
return '0;'+colors['d']
################################################################################
# Events and Hooks
################################################################################
def flash_title(owner, data, arg):
'''Scheduled event that, once per second, toggles the titlebar notification flag and sends the updated title to the client.'''
if owner.sock.telopt.bell:
owner.sock.telopt.bellon = not owner.sock.telopt.bellon
send_title(owner.sock)
event.start_event(owner, 1, flash_title)
def post_command(info):
'''Hook that will stop flashing the titlebar once a command is recieved.'''
ch, cmd, arg = hooks.parse_info(info)
if ch.sock is not None and ch.sock.telopt.bell:
ch.sock.telopt.bell = False
ch.sock.telopt.bellon = False
send_title(ch.sock)
def begin_negotiations(info):
'''Sends the initial negotiation strings to new sockets.'''
sock, = hooks.parse_info(info)
sock.trans_raw(server_do_ttype + server_do_naws + server_will_eor + server_will_msp + server_will_mxp + server_will_msdp)
def process_iac_sequence(info):
'''Process the IAC sequence to see if it was one of the options supported.'''
sock, cmd = hooks.parse_info(info)
# TELOPT_TTYPE
if cmd == client_will_ttype:
# The client has TTYPE. Request a TTYPE.
sock.trans_raw(server_send_ttype)
elif cmd.startswith(client_ttype_is):
# TTYPE Response
client = cmd[3:-3].lower()
sock.telopt.client = client
# 256-colors support
sock.telopt.n256 = client in clients256
# Window Title Setting
sock.telopt.ntitle = not client in no_title
if sock.telopt.ntitle and sock.telopt.title == 0: send_title(sock)
# TELOPT_NAWS
# This handler is a bit messed up since any '\0' characters are removed from the string.
elif cmd.startswith(client_naws):
if len(cmd) == 8: # Two numbers. Cols x Rows
cols, rows = ord(cmd[3]), ord(cmd[4])
elif len(cmd) == 9: # Three Numbers. Assume Cols Cols x Rows
cols, rows = ord(cmd[3]) * 256 + ord(cmd[4]), ord(cmd[5])
elif len(cmd) == 10: # Four numbers. Cols Cols x Rows Rows
cols, rows = ord(cmd[3]) * 256 + ord(cmd[4]), ord(cmd[5]) * 256 + ord(cmd[6])
else: # What? Just use the default then
cols, rows = 80, 23
# If cols is less than five, assume it was supposed to be the larger byte.
if cols < 5: cols *= 256
#sock.telopt.cols,sock.telopt.rows = cols, rows
#sock.page_cols,sock.page_rows = cols, rows
sock.cols,sock.rows = cols, rows
# TELOPT_EOR
elif cmd == client_do_eor:
sock.telopt.eor = True
elif cmd == client_dont_eor:
sock.telopt.eor = False
# TELOPT_MSDP
elif cmd == client_do_msdp:
# Enables the nMSDP tag. If MSDP is enabled because of this, run a hook.
old = sock.nMSDP
sock.telopt.nMSDP = True
if not old and sock.telopt.MSDP == 0: hooks.run("msdp_enabled", hooks.build_info("sk",(sock,)))
elif cmd == client_dont_msdp:
# Disables the nMSDP tag. If MSDP is disabled after this, run a hook.
old = sock.nMSDP
sock.telopt.nMSDP = False
if old and sock.telopt.MSDP == 0: hooks.run("msdp_disabled", hooks.build_info("sk",(sock,)))
# TELOPT_MSP
elif cmd == client_do_msp:
# Enables the nMSP tag. If MSP is enabled because of this, run a hook.
old = sock.nMSP
sock.telopt.nMSP = True
if not old and sock.telopt.MSP == 0: hooks.run("msp_enabled", hooks.build_info("sk",(sock,)))
elif cmd == client_dont_msp:
# Disables the nMSP tag. If MSP is disabled after this, run a hook.
old = sock.nMSP
sock.telopt.nMSP = False
if old and sock.telopt.MSP == 0: hooks.run("msp_disabled", hooks.build_info("sk",(sock,)))
# TELOPT_MXP
elif cmd == client_do_mxp:
# Enables the nMXP tag. If MXP is enabled because of this, run a hook.
old = sock.nMXP
sock.telopt.nMXP = True
if not old and sock.telopt.MXP == 0: hooks.run("mxp_enabled", hooks.build_info("sk",(sock,)))
elif cmd == client_dont_mxp:
# Disables the nMXP tag. If MXP is disabled because of this, run a hook.
old = sock.nMXP
sock.telopt.nMXP = False
if old and sock.telopt.MXP == 0: hooks.run("mxp_disabled", hooks.build_info("sk",(sock,)))
def mxp_stop_hook(info):
'''Send a wont MXP string.'''
sock, = hooks.parse_info(info)
sock.trans_raw(server_wont_mxp)
def mxp_start_hook(info):
'''Send out our notice that MXP is starting, along with the MXP tags.'''
sock, = hooks.parse_info(info)
sock.trans_raw(server_start_mxp + mxp_lock_secure + ''.join(mxp_tags))
def msdp_start_hook(info):
'''Send out all the current MSDP variables.'''
sock, = hooks.parse_info(info)
# If there's no variables, return.
if len(sock.telopt.vMSDP) == 0: return
# Build a string.
buf = msdp_start
for k,v in sock.telopt.vMSDP.iteritems():
buf += MSDP_VAR + k + __outmsdpv(v)
# Send the string
sock.trans_raw(buf + msdp_end)
def copyover_complete(info):
'''Loads socket auxiliary data for the active connections from a file.'''
set = storage.StorageSet(copyover_file)
# Read the data into a dict.
data = { }
for s in set.readList("list").sets():
data[s.readString("k")] = s.readSet("v")
# Iterate through sockets, and if a player is connected, restore the aux data
# To their socket.
for sk in mudsock.socket_list():
if sk.ch is not None and data.has_key(sk.ch.name):
sk.telopt.load(data[sk.ch.name])
# If MXP is enabled, resend tags in case they were updated.
if sk.MXP: mxp_start_hook(hooks.build_info("sk",(sk,)))
# Close the set
set.close()
def process_outbound_prompt(info):
'''Process outbound prompt to add END-OF-RECORD on the end if necessary.'''
sock, = hooks.parse_info(info)
# First, process the text normally.
process_outbound_text(info)
if sock.endofrecord:
sock.outbound_text += server_eor_code
def process_outbound_text(info):
'''Process outbound text by replacing color codes and escaping MXP tags.'''
sock, = hooks.parse_info(info)
buf = sock.outbound_text
# Process color sequences
if sock.c256:
if sock.client == 'zmud':
# MXP for zMUD
buf = buf.replace("\n",mxp_tag_start+'/C'+mxp_tag_end+'\n'+mxp_tag_start+'C "#C0C0C0"'+mxp_tag_end)
buf = mxp_tag_start+'C "#C0C0C0"'+mxp_tag_end+coloreg.sub(mxp_color, buf)+mxp_tag_start+'/C'+mxp_tag_end
else:
buf = coloreg.sub(c256_color, buf)
else:
buf = coloreg.sub(ansi_color, buf)
# Is MXP enabled?
if sock.MXP:
# It is.
buf = mxpreg.sub(lambda m: ['>','<','&'][int(m.group() != '>') + int(m.group() == '&')], buf)
buf = buf.replace(mxp_tag_start, '<').replace(mxp_tag_end, '>').replace(mxp_and, '&')
buf,c = linkreg.subn(lambda m: '<A href="'+m.group(1)+'">'+m.group(1)+'</A>', buf)
if c > 0: sock.telopt.mxp_sec = True
elif sock.client == 'zmud':
# If it's zmud, we have to escape tags regardless. It doesn't negotiate well with others.
buf = mxpreg.sub(lambda m: ['>','<'][m.group() == '<'], buf)
# Is it a secure line? If so, prepend our secure line tag.
if sock.telopt.mxp_sec:
buf = mxp_lock_secure + buf + mxp_lock_open
sock.telopt.mxp_sec = False
# Return our processed text.
sock.outbound_text = buf
def mxp_color(match):
'''Replace a matching color code with an MXP tag. This is for zMUD.'''
m = match.group(1)
if m == '{': return '{'
if m == 'n' or m == 'N': return mxp_tag_start+'/C'+mxp_tag_end+mxp_tag_start+'C "#C0C0C0"'+mxp_tag_end
if m in colorletters: return mxp_tag_start+'/C'+mxp_tag_end+mxp_tag_start+'C "'+htmlcolors[colorletters.find(m)]+'"'+mxp_tag_end
# Random
if m[0] == '7': return mxp_tag_start+'/C'+mxp_tag_end+mxp_tag_start+'C "'+htmlcolors[random.randrange(0,255)]+'"'+mxp_tag_end
# Still here? One of the extended codes then. First, check for grayscale.
if m[0] == '6': return mxp_tag_start+'/C'+mxp_tag_end+mxp_tag_start+'C "'+htmlcolors[232 + (int(m[1:]) % 24)]+'"'+mxp_tag_end
# One of the 6x6x6
return mxp_tag_start+'/C'+mxp_tag_end+mxp_tag_start+'C "'+htmlcolors[16 + int(m[0])*36 + int(m[1])*6 + int(m[2])]+'"'+mxp_tag_end
def c256_color(match):
'''Replace a matching color code with the proper 256-color escape sequence.'''
m = match.group(1)
if m == '{': return '{'
if m == 'n' or m == 'N': return '\x1b[0;'+colors['n']+'m'
if m in colorletters: return '\x1b[38;5;%dm' % colorletters.find(m)
# Random
if m[0] == '7': return '\x1b[38;5;%dm' % random.randrange(0,255)
# Still here? One of the extended codes then. First, check for grayscale.
if m[0] == '6': return '\x1b[38;5;%dm' % (232 + int(m[1:]) % 24)
# One of the 6x6x6.
return '\x1b[38;5;%dm' % (16 + int(m[0])*36 + int(m[1])*6 + int(m[2]))
def ansi_color(match):
'''Replace a matching color code with the simplified ANSI escape sequence.'''
m = match.group(1)
if m == '{': return '{'
if m == 'n' or m == 'N': return '\x1b[0;'+colors['n']+'m'
if m in colorletters: return '\x1b['+extcolors[colorletters.find(m)]+'m'
# Random
if m[0] == '7': return '\x1b['+extcolors[random.randrange(0,255)]+'m'
# Still here? One of the extended codes then. First, check for grayscale.
if m[0] == '6': return '\x1b['+extcolors[232 + int(m[1:]) % 24]+'m'
# One of the 6x6x6
return '\x1b['+extcolors[16 + int(m[0])*36 + int(m[1]) * 6 + int(m[2])]+'m'
################################################################################
# Properties
################################################################################
# The Aux Data
def __settelopt(s, data):
if not isinstance(data, TeloptAux): raise TypeError("TeloptAux object not supplied")
data.copyTo(s.aux("telopt"))
def __gettelopt(s):
return s.aux("telopt")
# Settable Properties
def __delmxp(s): s.MXP = 0
def __getmxp(s): return [False, s.telopt.nMXP, True][s.telopt.MXP + 1]
def __setmxp(s, v):
old = s.MXP # Run the appropriate hook if MXP is enabled or disabled here.
if type(v) == int and -2 < v < 2: s.telopt.MXP = v
else: s.telopt.MXP = bool(v) and 1 or -1
if old and not s.MXP: hooks.run("mxp_disabled", hooks.build_info("sk",(s,)))
elif not old and s.MXP: hooks.run("mxp_enabled", hooks.build_info("sk",(s,)))
def __delmsp(s): s.MSP = 0
def __getmsp(s): return [False, s.telopt.nMSP, True][s.telopt.MSP + 1]
def __setmsp(s, v):
old = s.MSP # Run the appropriate hook if MSP is enabled or disabled here.
if type(v) == int and -2 < v < 2: s.telopt.MSP = v
else: s.telopt.MSP = bool(v) and 1 or -1
if old and not s.MSP: hooks.run("msp_disabled", hooks.build_info("sk",(s,)))
elif not old and s.MSP: hooks.run("msp_enabled", hooks.build_info("sk",(s,)))
def __deltitle(s): s.title = 0
def __gettitle(s): return [False, s.telopt.ntitle, True][s.telopt.title + 1]
def __settitle(s,v):
old,olds = s.title, s.telopt.title_s
# Depending on the variable type, either store the flag or the string.
if type(v) == int and -2 < v < 2: s.telopt.title = v
elif type(v) == str: s.telopt.title_s = v
else: s.telopt.title = bool(v) and 1 or -1
# If we've updated, send the fresh title.
if s.title and (not old or olds is not s.telopt.title_s): send_title(s)
def __delc256(s): s.telopt.c256 = 0
def __getc256(s): return [False, s.telopt.n256, True][s.telopt.c256 + 1]
def __setc256(s,v):
if type(v) == int and -2 < v < 2: s.telopt.c256 = v
else: s.telopt.c256 = bool(v) and 1 or -1
def __delmsdp(s): s.MSDP = 0
def __getmsdp(s): return [False, s.telopt.nMSDP, True][s.telopt.MSDP + 1]
def __setmsdp(s, v):
# If a dict was assigned, hand things over to the variable sending.
if type(v) == dict:
s.sendMSDP(v)
return
old = s.MSDP # Run the appropriate hook if MSDP is enabled or disabled here.
if type(v) == int and -2 < v < 2: s.telopt.MSDP = v
else: s.telopt.MSDP = bool(v) and 1 or -1
if old and not s.MSDP: hooks.run("msdp_disabled", hooks.build_info("sk",(s,)))
elif not old and s.MSDP: hooks.run("msdp_enabled", hooks.build_info("sk",(s,)))
# Read Only Properties / Methods
def __getnmxp(s): return s.telopt.nMXP
def __getnmsp(s): return s.telopt.nMSP
def __getnmsdp(s): return s.telopt.nMSDP
def __getclient(s): return s.telopt.client
def __getcols(s): return s.telopt.cols
def __getrows(s): return s.telopt.rows
def __geteor(s): return s.telopt.eor
def __getn256(s): return s.telopt.n256
def __getntitle(s): return s.telopt.ntitle
def __sendMSDP(s, key, val = None):
'''Send a new MSDP variable to the client if MSDP is enabled. Store the
variable to be sure not to send repeated information.'''
if type(key) != dict: key = { key : val }
# Loop through all the variables, building a string
buf = msdp_start
count = 0
for k,v in key.iteritems():
# If the variable hasn't updated, just continue.
if s.telopt.vMSDP.has_key(k) and s.telopt.vMSDP[k] == v: continue
s.telopt.vMSDP[k] = v
count += 1
buf += MSDP_VAR + k + __outmsdpv(v)
# If MSDP is enabled and we have at least one updated variable then
# send the string to the client.
if s.MSDP and count > 0:
s.trans_raw(buf + msdp_end)
def __outmsdpv(v):
'''Output a value for MSDP. Handle lists and tuples too.'''
if type(v) in [list,tuple]:
buf = ''
for k in v:
buf += __outmsdpv(k)
return buf
else:
return MSDP_VAL + str(v)
def __bell(s):
'''Set a flashing marker in the title to let someone know the MUD wants their attention.'''
# Set the flashing title marker. Until the next recieved command.
if not s.telopt.bell:
s.trans_raw('\007\007')
if s.title:
s.telopt.bell = True
s.telopt.bellon = True
send_title(s)
event.start_event(s.ch, 1, flash_title)
################################################################################
# Player Commands
################################################################################
def cmd_colors(ch, cmd, arg):
'''Usage: {Wcolors{n
Displays a table of avaliable colors and the code used to select them.'''
# Display 256-colors status
if ch.sock.telopt.c256 == 0:
if ch.sock.c256:
if ch.sock.client == 'zmud':
ch.send("256-colors mode is currently automatically detected as {genabled{n - {gMXP{n.")
else:
ch.send("256-colors mode is currently automatically detected as " + mxp_tag(ch.sock,'{genabled{n','B') + ".") #{genabled{n.")
else:
ch.send("256-colors mode is currently automatically detected as {rdisabled{n.")
else:
if ch.sock.c256: ch.send("256-colors mode is currently set to {genabled{n.")
else: ch.send("256-colors mode is currenlty set to {rdisabled{n.")
# Output the table of colors
ch.send("\r\nThe following colors are avaliable:")
# If the screen is wide enough, display the full wide table, else minimize it.
if ch.sock.cols > 147: im = 35
else: im = 17
buf = ''
i = 0
for k in range(0,16):
buf += ' {%s%s ' % (colorletters[k],colorletters[k])
i += 1
if i % 6 == 0: buf += ' '
ch.send(buf)
i = 0
buf = ''
for r in range(0,6):
for g in range(0, 6):
for b in range(0, 6):
buf += '{%d%d%d%d%d%d ' % (r,g,b,r,g,b)
i += 1
if i % 6 == 0: buf += ' '
if i > im:
ch.send(buf.rstrip())
buf = ''
i = 0
for c in range(0,24):
buf += '{6%02d6%02d ' % (c,c)
i += 1
if i % 6 == 0: buf += ' '
if i > im:
ch.send(buf)
buf = ''
i = 0
if i > 0:
ch.send(buf)
################################################################################
# Initialization
################################################################################
auxiliary.install("telopt", TeloptAux, "socket")
# Fill the extcolors table.
# Disregard that! I put cached values at the start of the file so we don't waste
# time recalculating something easilly predictable.
'''for c in range(0,256):
extcolors.append(simplecolor(c))
htmlcolors.append(simplecolor(c,True))'''
# Set our hooks.
hooks.add("receive_connection", begin_negotiations)
hooks.add("receive_iac", process_iac_sequence)
hooks.add("process_outbound_text", process_outbound_text)
hooks.add("process_outbound_prompt", process_outbound_prompt)
hooks.add("command", post_command)
hooks.add("mxp_enabled", mxp_start_hook)
hooks.add("msdp_enabled", msdp_start_hook)
hooks.add("mxp_disabled", mxp_stop_hook)
hooks.add("copyover_complete", copyover_complete)
# Add our commands.
mudsys.add_cmd("colors", None, cmd_colors, "player", False)
# Add our properties to the socket.
mudsys.add_sock_method("bell", __bell)
mudsys.add_sock_method("sendMSDP", __sendMSDP)
mudsys.add_sock_method("telopt", property(__gettelopt,__settelopt,doc="The auxiliary data of the advanced_telopt module."))
mudsys.add_sock_method("MXP", property(__getmxp,__setmxp,__delmxp,
"True if MXP output to the client is enabled, else false. Setting a boolean to this property will forcibly enable or disable MXP. Delete this to resume automatic detection of MXP support."))
mudsys.add_sock_method("MSP", property(__getmsp,__setmsp,__delmsp,
"True if MSP output to the client is enabled, else false. Setting a boolean to this property will forcibly enable or disable MSP. Delete this to resume automatic detection of MSP support."))
mudsys.add_sock_method("MSDP", property(__getmsdp,__setmsdp,__delmsdp,
"True if MSDP output to the client is enabled, else false. Setting a boolean to this property will forcibly enable or disable MSDP. Delete this to resume automatic detection of MSDP support."))
mudsys.add_sock_method("title",property(__gettitle,__settitle,__deltitle,
"True if title string output to the client is enabled, else false. Setting a string to this property will change the window title message. Setting a boolean to this property will forcibly enable or disable title output. Delete this to determine title support based on the report client."))
mudsys.add_sock_method("c256", property(__getc256,__setc256,__delc256,
"Gets or sets the state of 256-colors mode on the socket. Set to True to enable, False to disable, or delete to use the automatically detect state based on client identification."))
mudsys.add_sock_method("nMXP", property(__getnmxp, doc="True if the client has requested MXP, else False."))
mudsys.add_sock_method("nMSP", property(__getnmsp, doc="True if the client has requested MSP, else False."))
mudsys.add_sock_method("nMSDP", property(__getnmsp, doc="True if the client has requested MSDP, else False."))
mudsys.add_sock_method("nTitle", property(__getntitle, doc="The automatically detected state of window title support, based on client identification."))
#mudsys.add_sock_method("cols", property(__getcols, doc="The width of the client's output in columns."))
#mudsys.add_sock_method("rows", property(__getrows, doc="The height of the client's output in rows."))
mudsys.add_sock_method("client", property(__getclient, doc="The client software, as given by TELOPT_TTYPE."))
mudsys.add_sock_method("endofrecord",property(__geteor, doc="True if the client has requested END-OF-RECORD codes, else False. See RFC 885 for more on END-OF-RECORD."))
mudsys.add_sock_method("n256", property(__getn256, doc="The automatically detected state of 256-colors mode, based on client identification."))
# Build our list of MXP tags
# These are just some examples, mind you.
'''for k,v in {'RNum':'RoomNum', 'RName':'RoomName', 'RDesc':'RoomDesc', 'RExits':'RoomExit', 'Prompt':'Prompt'}.iteritems():
register_mxp_flag(k,v)
register_mxp_tag('Ex', """'<SEND "&text;|look &text;" hint="Right-click to see menu.|Go &text;.|Look &text;." EXPIRE="Exits">'""", True)
register_mxp_tag('REdit', """'<SEND "goto &text;|redit &text;" hint="Right-click to see menu.|Go to &text;|Edit &text;">'""", True)
register_mxp_tag('Help', """'<SEND "help &text;" hint="Get help for &text;">'""", True)
register_mxp_tag('HEdit', """'<SEND "help &text;|hedit &text;" hint="Right-click to see menu.|Get help for &text;|Edit help for &text;">'""", True)'''
def __unload__():
''' Detatch our hooks from the game. '''
hooks.remove("receive_connection", begin_negotiations)
hooks.remove("receive_iac", process_iac_sequence)
hooks.remove("process_outbound_text", process_outbound_text)
hooks.remove("process_outbound_prompt", process_outbound_prompt)
hooks.remove("command", post_command)
hooks.remove("mxp_enabled", mxp_start_hook)
hooks.remove("mxp_disabled", mxp_stop_hook)
hooks.remove("msdp_enabled", msdp_start_hook)
hooks.remove("copyover_complete", copyover_complete)