Fixed conflicts against master.

This commit is contained in:
Griatch 2015-10-18 12:43:43 +02:00
commit b952a290b5
18 changed files with 307 additions and 223 deletions

View file

@ -95,7 +95,7 @@ _ERROR_NOCMDSETS = "No command sets found! This is a sign of a critical bug." \
"\nsome other means for assistance." "\nsome other means for assistance."
_ERROR_CMDHANDLER = "{traceback}\n"\ _ERROR_CMDHANDLER = "{traceback}\n"\
"Above traceback is from a Command handler bug." \ "Above traceback is from a Command handler bug. " \
"Please file a bug report with the Evennia project." "Please file a bug report with the Evennia project."
_ERROR_RECURSION_LIMIT = "Command recursion limit ({recursion_limit}) " \ _ERROR_RECURSION_LIMIT = "Command recursion limit ({recursion_limit}) " \

View file

@ -1,6 +1,6 @@
# #
# This is Evennia's default connection screen. It is imported # This is Evennia's default connection screen. It is imported
# and run from world/connection_screens.py. # and run from server/conf/connection_screens.py.
# #
from django.conf import settings from django.conf import settings

View file

@ -1093,7 +1093,8 @@ class CmdName(ObjManipCommand):
Usage: Usage:
@name obj = name;alias1;alias2 @name obj = name;alias1;alias2
Rename an object to something new. Rename an object to something new. Use *obj to
rename a player.
""" """
@ -1107,12 +1108,30 @@ class CmdName(ObjManipCommand):
caller = self.caller caller = self.caller
if not self.args: if not self.args:
string = "Usage: @name <obj> = <newname>[;alias;alias;...]" caller.msg("Usage: @name <obj> = <newname>[;alias;alias;...]")
caller.msg(string)
return return
if self.lhs_objs: if self.lhs_objs:
objname = self.lhs_objs[0]['name'] objname = self.lhs_objs[0]['name']
if objname.startswith("*"):
# player mode
obj = caller.player.search(objname.lstrip("*"))
if obj:
if self.rhs_objs[0]['aliases']:
caller.msg("Players can't have aliases.")
return
newname = self.rhs
if not newname:
caller.msg("No name defined!")
return
if not obj.access(caller, "edit"):
caller.mgs("You don't have right to edit this player %s." % obj)
return
obj.username = newname
obj.save()
caller.msg("Player's name changed to '%s'." % newname)
return
# object search, also with *
obj = caller.search(objname) obj = caller.search(objname)
if not obj: if not obj:
return return
@ -1125,6 +1144,9 @@ class CmdName(ObjManipCommand):
if not newname and not aliases: if not newname and not aliases:
caller.msg("No names or aliases defined!") caller.msg("No names or aliases defined!")
return return
if not obj.access(caller, "edit"):
caller.msg("You don't have the right to edit %s." % obj)
return
# change the name and set aliases: # change the name and set aliases:
if newname: if newname:
obj.name = newname obj.name = newname

View file

@ -519,17 +519,15 @@ class CmdChannelCreate(MuxPlayerCommand):
channame = lhs channame = lhs
aliases = None aliases = None
if ';' in lhs: if ';' in lhs:
channame, aliases = [part.strip().lower() channame, aliases = lhs.split(';', 1)
for part in lhs.split(';', 1) if part.strip()] aliases = [alias.strip().lower() for alias in aliases.split(';')]
aliases = [alias.strip().lower()
for alias in aliases.split(';') if alias.strip()]
channel = ChannelDB.objects.channel_search(channame) channel = ChannelDB.objects.channel_search(channame)
if channel: if channel:
self.msg("A channel with that name already exists.") self.msg("A channel with that name already exists.")
return return
# Create and set the channel up # Create and set the channel up
lockstring = "send:all();listen:all();control:id(%s)" % caller.id lockstring = "send:all();listen:all();control:id(%s)" % caller.id
new_chan = create.create_channel(channame, new_chan = create.create_channel(channame.strip(),
aliases, aliases,
description, description,
locks=lockstring) locks=lockstring)

View file

@ -74,6 +74,7 @@ class CmdHelp(Command):
""" """
key = "help" key = "help"
locks = "cmd:all()" locks = "cmd:all()"
arg_regex = r"\s|$"
# this is a special cmdhandler flag that makes the cmdhandler also pack # this is a special cmdhandler flag that makes the cmdhandler also pack
# the current cmdset with the call to self.func(). # the current cmdset with the call to self.func().

View file

@ -194,23 +194,23 @@ class CmdPy(MuxCommand):
t0 = timemeasure() t0 = timemeasure()
ret = eval(pycode_compiled, {}, available_vars) ret = eval(pycode_compiled, {}, available_vars)
t1 = timemeasure() t1 = timemeasure()
duration = " (%.4f ms)" % ((t1 - t0) * 1000) duration = " (runtime ~ %.4f ms)" % ((t1 - t0) * 1000)
else: else:
ret = eval(pycode_compiled, {}, available_vars) ret = eval(pycode_compiled, {}, available_vars)
if mode == "eval": if mode == "eval":
ret = "{n<<< %s%s" % (str(ret), duration) ret = "<<< %s%s" % (str(ret), duration)
else: else:
ret = "{n<<< Done.%s" % duration ret = "<<< Done (use self.msg() if you want to catch output)%s" % duration
except Exception: except Exception:
errlist = traceback.format_exc().split('\n') errlist = traceback.format_exc().split('\n')
if len(errlist) > 4: if len(errlist) > 4:
errlist = errlist[4:] errlist = errlist[4:]
ret = "\n".join("{n<<< %s" % line for line in errlist if line) ret = "\n".join("<<< %s" % line for line in errlist if line)
try: try:
self.msg(ret, sessid=self.sessid) self.msg(ret, sessid=self.sessid, raw=True)
except TypeError: except TypeError:
self.msg(ret) self.msg(ret, raw=True)
# helper function. Kept outside so it can be imported and run # helper function. Kept outside so it can be imported and run

View file

@ -15,14 +15,14 @@ Install is simple:
To your settings file, add/edit the line: To your settings file, add/edit the line:
CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedInCmdSet" CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedinCmdSet"
That's it. Reload the server and try to log in to see it. That's it. Reload the server and try to log in to see it.
You will want to change the login "graphic", which defaults to give You will want to change the login "graphic", which defaults to give
information about commands which are not used in this version of the information about commands which are not used in this version of the
login. You can change the screen used by editing login. You can change the screen used by editing
`mygame/server/conf/connection_screens.py`. `$GAME_DIR/server/conf/connection_screens.py`.
""" """
@ -323,13 +323,13 @@ node3 = MenuNode("node3", text=LOGIN_SCREEN_HELP,
# access commands # access commands
class UnloggedInCmdSet(CmdSet): class UnloggedinCmdSet(CmdSet):
"Cmdset for the unloggedin state" "Cmdset for the unloggedin state"
key = "DefaultUnloggedin" key = "DefaultUnloggedin"
priority = 0 priority = 0
def at_cmdset_creation(self): def at_cmdset_creation(self):
"Called when cmdset is first created" "Called when cmdset is first created."
self.add(CmdUnloggedinLook()) self.add(CmdUnloggedinLook())
@ -337,7 +337,7 @@ class CmdUnloggedinLook(Command):
""" """
An unloggedin version of the look command. This is called by the server An unloggedin version of the look command. This is called by the server
when the player first connects. It sets up the menu before handing off when the player first connects. It sets up the menu before handing off
to the menu's own look command.. to the menu's own look command.
""" """
key = CMD_LOGINSTART key = CMD_LOGINSTART
# obs, this should NOT have aliases for look or l, this will clash with the menu version! # obs, this should NOT have aliases for look or l, this will clash with the menu version!

View file

@ -281,7 +281,7 @@ class MenuNode(object):
text (str, optional): The text that will be displayed at text (str, optional): The text that will be displayed at
top when viewing this node. top when viewing this node.
Kwargs: Kwargs:
links (list): A liist of keys for unique menunodes this is connected to. links (list): A list of keys for unique menunodes this is connected to.
The actual keys will not printed - keywords will be The actual keys will not printed - keywords will be
used (or a number) used (or a number)
linktexts (list)- A list of texts to describe the links. Must linktexts (list)- A list of texts to describe the links. Must

View file

@ -62,6 +62,8 @@ DATABASES = {{
###################################################################### ######################################################################
# Django web features # Django web features
# (don't remove these entries, they are needed to override the default
# locations with your actual GAME_DIR locations at run-time)
###################################################################### ######################################################################
# Absolute path to the directory that holds file uploads from web apps. # Absolute path to the directory that holds file uploads from web apps.

View file

@ -370,7 +370,7 @@ class DefaultObject(ObjectDB):
if quiet: if quiet:
return results return results
return _AT_SEARCH_RESULT(self, searchdata, results, global_search=True) return _AT_SEARCH_RESULT(results, self, query=searchdata)
def execute_cmd(self, raw_string, sessid=None, **kwargs): def execute_cmd(self, raw_string, sessid=None, **kwargs):
""" """

View file

@ -250,7 +250,7 @@ class MsgPortal2Server(amp.Command):
""" """
key = "MsgPortal2Server" key = "MsgPortal2Server"
arguments = [('data', Compressed())] arguments = [('packed_data', Compressed())]
errors = [(Exception, 'EXCEPTION')] errors = [(Exception, 'EXCEPTION')]
response = [] response = []
@ -261,7 +261,7 @@ class MsgServer2Portal(amp.Command):
""" """
key = "MsgServer2Portal" key = "MsgServer2Portal"
arguments = [('data', Compressed())] arguments = [('packed_data', Compressed())]
errors = [(Exception, 'EXCEPTION')] errors = [(Exception, 'EXCEPTION')]
response = [] response = []
@ -275,7 +275,7 @@ class AdminPortal2Server(amp.Command):
""" """
key = "AdminPortal2Server" key = "AdminPortal2Server"
arguments = [('data', Compressed())] arguments = [('packed_data', Compressed())]
errors = [(Exception, 'EXCEPTION')] errors = [(Exception, 'EXCEPTION')]
response = [] response = []
@ -289,7 +289,7 @@ class AdminServer2Portal(amp.Command):
""" """
key = "AdminServer2Portal" key = "AdminServer2Portal"
arguments = [('data', Compressed())] arguments = [('packed_data', Compressed())]
errors = [(Exception, 'EXCEPTION')] errors = [(Exception, 'EXCEPTION')]
response = [] response = []
@ -362,7 +362,7 @@ class AMPProtocol(amp.AMP):
sessdata = self.factory.portal.sessions.get_all_sync_data() sessdata = self.factory.portal.sessions.get_all_sync_data()
self.send_AdminPortal2Server(0, self.send_AdminPortal2Server(0,
PSYNC, PSYNC,
data=sessdata) sessiondata=sessdata)
self.factory.portal.sessions.at_server_connection() self.factory.portal.sessions.at_server_connection()
if hasattr(self.factory, "server_restart_mode"): if hasattr(self.factory, "server_restart_mode"):
del self.factory.server_restart_mode del self.factory.server_restart_mode
@ -399,53 +399,49 @@ class AMPProtocol(amp.AMP):
(sessid, kwargs). (sessid, kwargs).
""" """
batch = dumps((sessid, kwargs))
return self.callRemote(command, return self.callRemote(command,
data=batch).addErrback(self.errback, command.key) packed_data=dumps((sessid, kwargs))
).addErrback(self.errback, command.key)
# Message definition + helper methods to call/create each message type # Message definition + helper methods to call/create each message type
# Portal -> Server Msg # Portal -> Server Msg
@MsgPortal2Server.responder @MsgPortal2Server.responder
def server_receive_msgportal2server(self, data): def server_receive_msgportal2server(self, packed_data):
""" """
Receives message arriving to server. This method is executed Receives message arriving to server. This method is executed
on the Server. on the Server.
Args: Args:
data (str): Data to receive (a pickled tuple (sessid,kwargs)) packed_data (str): Data to receive (a pickled tuple (sessid,kwargs))
""" """
sessid, kwargs = loads(data) sessid, kwargs = loads(packed_data)
#print "msg portal -> server (server side):", sessid, msg, loads(ret["data"]) #print "msg portal -> server (server side):", sessid, msg, loads(ret["data"])
self.factory.server.sessions.data_in(sessid, self.factory.server.sessions.data_in(sessid, **kwargs)
text=kwargs["msg"],
data=kwargs["data"])
return {} return {}
def send_MsgPortal2Server(self, sessid, msg="", data=""): def send_MsgPortal2Server(self, sessid, text="", **kwargs):
""" """
Access method called by the Portal and executed on the Portal. Access method called by the Portal and executed on the Portal.
Args: Args:
sessid (int): Unique Session id. sessid (int): Unique Session id.
msg (str): Message to send over the wire. msg (str): Message to send over the wire.
data (str, optional): Optional data. kwargs (any, optional): Optional data.
Returns: Returns:
deferred (Deferred): Asynchronous return. deferred (Deferred): Asynchronous return.
""" """
#print "msg portal->server (portal side):", sessid, msg, data #print "msg portal->server (portal side):", sessid, msg, data
return self.send_data(MsgPortal2Server, sessid, return self.send_data(MsgPortal2Server, sessid, text=text, **kwargs)
msg=msg,
data=data)
# Server -> Portal message # Server -> Portal message
@MsgServer2Portal.responder @MsgServer2Portal.responder
def portal_receive_server2portal(self, data): def portal_receive_server2portal(self, packed_data):
""" """
Receives message arriving to Portal from Server. Receives message arriving to Portal from Server.
This method is executed on the Portal. This method is executed on the Portal.
@ -456,17 +452,15 @@ class AMPProtocol(amp.AMP):
before continuing. before continuing.
Args: Args:
data (str): Pickled data (sessid, kwargs) coming over the wire. packed_data (str): Pickled data (sessid, kwargs) coming over the wire.
""" """
sessid, kwargs = loads(data) sessid, kwargs = loads(packed_data)
#print "msg server->portal (portal side):", sessid, ret["text"], loads(ret["data"]) #print "msg server->portal (portal side):", sessid, ret["text"], loads(ret["data"])
self.factory.portal.sessions.data_out(sessid, self.factory.portal.sessions.data_out(sessid, **kwargs)
text=kwargs["msg"],
data=kwargs["data"])
return {} return {}
def send_MsgServer2Portal(self, sessid, msg="", data=""): def send_MsgServer2Portal(self, sessid, text="", **kwargs):
""" """
Access method - executed on the Server for sending data Access method - executed on the Server for sending data
to Portal. to Portal.
@ -474,39 +468,37 @@ class AMPProtocol(amp.AMP):
Args: Args:
sessid (int): Unique Session id. sessid (int): Unique Session id.
msg (str, optional): Message to send over the wire. msg (str, optional): Message to send over the wire.
data (str, optional): Extra data. kwargs (any, optiona): Extra data.
""" """
#print "msg server->portal (server side):", sessid, msg, data #print "msg server->portal (server side):", sessid, msg, data
return self.send_data(MsgServer2Portal, sessid, msg=msg, data=data) return self.send_data(MsgServer2Portal, sessid, text=text, **kwargs)
# Server administration from the Portal side # Server administration from the Portal side
@AdminPortal2Server.responder @AdminPortal2Server.responder
def server_receive_adminportal2server(self, data): def server_receive_adminportal2server(self, packed_data):
""" """
Receives admin data from the Portal (allows the portal to Receives admin data from the Portal (allows the portal to
perform admin operations on the server). This is executed on perform admin operations on the server). This is executed on
the Server. the Server.
Args: Args:
data (str): Data to send (often a part of a batch) packed_data (str): Incoming, pickled data.
""" """
#print "serveradmin (server side):", hashid, ipart, nparts #print "serveradmin (server side):", hashid, ipart, nparts
sessid, kwargs = loads(data) sessid, kwargs = loads(packed_data)
operation = kwargs.pop("operation", "")
operation = kwargs["operation"]
data = kwargs["data"]
server_sessionhandler = self.factory.server.sessions server_sessionhandler = self.factory.server.sessions
#print "serveradmin (server side):", sessid, ord(operation), data #print "serveradmin (server side):", sessid, ord(operation), data
if operation == PCONN: # portal_session_connect if operation == PCONN: # portal_session_connect
# create a new session and sync it # create a new session and sync it
server_sessionhandler.portal_connect(data) server_sessionhandler.portal_connect(kwargs.get("sessiondata"))
elif operation == PCONNSYNC: #portal_session_sync elif operation == PCONNSYNC: #portal_session_sync
server_sessionhandler.portal_session_sync(data) server_sessionhandler.portal_session_sync(kwargs.get("sessiondata"))
elif operation == PDISCONN: # portal_session_disconnect elif operation == PDISCONN: # portal_session_disconnect
# session closed from portal side # session closed from portal side
@ -518,12 +510,12 @@ class AMPProtocol(amp.AMP):
# contains a dict {sessid: {arg1:val1,...}} # contains a dict {sessid: {arg1:val1,...}}
# representing the attributes to sync for each # representing the attributes to sync for each
# session. # session.
server_sessionhandler.portal_sessions_sync(data) server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata"))
else: else:
raise Exception("operation %(op)s not recognized." % {'op': operation}) raise Exception("operation %(op)s not recognized." % {'op': operation})
return {} return {}
def send_AdminPortal2Server(self, sessid, operation="", data=""): def send_AdminPortal2Server(self, sessid, operation="", **kwargs):
""" """
Send Admin instructions from the Portal to the Server. Send Admin instructions from the Portal to the Server.
Executed Executed
@ -533,42 +525,41 @@ class AMPProtocol(amp.AMP):
sessid (int): Session id. sessid (int): Session id.
operation (char, optional): Identifier for the server operation, as defined by the operation (char, optional): Identifier for the server operation, as defined by the
global variables in `evennia/server/amp.py`. global variables in `evennia/server/amp.py`.
data (str, optional): Data going into the adminstrative operation. data (str or dict, optional): Data used in the administrative operation.
""" """
#print "serveradmin (portal side):", sessid, ord(operation), data #print "serveradmin (portal side):", sessid, ord(operation), data
return self.send_data(AdminPortal2Server, sessid, operation=operation, data=data) return self.send_data(AdminPortal2Server, sessid, operation=operation, **kwargs)
# Portal administraton from the Server side # Portal administraton from the Server side
@AdminServer2Portal.responder @AdminServer2Portal.responder
def portal_receive_adminserver2portal(self, data): def portal_receive_adminserver2portal(self, packed_data):
""" """
Receives and handles admin operations sent to the Portal Receives and handles admin operations sent to the Portal
This is executed on the Portal. This is executed on the Portal.
Args: Args:
data (str): Data received, a pickled tuple (sessid, kwargs). packed_data (str): Data received, a pickled tuple (sessid, kwargs).
""" """
#print "portaladmin (portal side):", sessid, ord(operation), data #print "portaladmin (portal side):", sessid, ord(operation), data
sessid, kwargs = loads(data) sessid, kwargs = loads(packed_data)
operation = kwargs["operation"] operation = kwargs.pop("operation")
data = kwargs["data"]
portal_sessionhandler = self.factory.portal.sessions portal_sessionhandler = self.factory.portal.sessions
if operation == SLOGIN: # server_session_login if operation == SLOGIN: # server_session_login
# a session has authenticated; sync it. # a session has authenticated; sync it.
portal_sessionhandler.server_logged_in(sessid, data) portal_sessionhandler.server_logged_in(sessid, kwargs.get("sessiondata"))
elif operation == SDISCONN: # server_session_disconnect elif operation == SDISCONN: # server_session_disconnect
# the server is ordering to disconnect the session # the server is ordering to disconnect the session
portal_sessionhandler.server_disconnect(sessid, reason=data) portal_sessionhandler.server_disconnect(sessid, reason=kwargs.get("reason"))
elif operation == SDISCONNALL: # server_session_disconnect_all elif operation == SDISCONNALL: # server_session_disconnect_all
# server orders all sessions to disconnect # server orders all sessions to disconnect
portal_sessionhandler.server_disconnect_all(reason=data) portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))
elif operation == SSHUTD: # server_shutdown elif operation == SSHUTD: # server_shutdown
# the server orders the portal to shut down # the server orders the portal to shut down
@ -577,18 +568,18 @@ class AMPProtocol(amp.AMP):
elif operation == SSYNC: # server_session_sync elif operation == SSYNC: # server_session_sync
# server wants to save session data to the portal, # server wants to save session data to the portal,
# maybe because it's about to shut down. # maybe because it's about to shut down.
portal_sessionhandler.server_session_sync(data) portal_sessionhandler.server_session_sync(kwargs.get("sessiondata"))
# set a flag in case we are about to shut down soon # set a flag in case we are about to shut down soon
self.factory.server_restart_mode = True self.factory.server_restart_mode = True
elif operation == SCONN: # server_force_connection (for irc/imc2 etc) elif operation == SCONN: # server_force_connection (for irc/imc2 etc)
portal_sessionhandler.server_connect(**data) portal_sessionhandler.server_connect(**kwargs)
else: else:
raise Exception("operation %(op)s not recognized." % {'op': operation}) raise Exception("operation %(op)s not recognized." % {'op': operation})
return {} return {}
def send_AdminServer2Portal(self, sessid, operation="", data=""): def send_AdminServer2Portal(self, sessid, operation="", **kwargs):
""" """
Administrative access method called by the Server to send an Administrative access method called by the Server to send an
instruction to the Portal. instruction to the Portal.
@ -598,16 +589,15 @@ class AMPProtocol(amp.AMP):
operation (char, optional): Identifier for the server operation (char, optional): Identifier for the server
operation, as defined by the global variables in operation, as defined by the global variables in
`evennia/server/amp.py`. `evennia/server/amp.py`.
data (str, optional): Data going into the adminstrative data (str or dict, optional): Data going into the adminstrative.
operation.
""" """
return self.send_data(AdminServer2Portal, sessid, operation=operation, data=data) return self.send_data(AdminServer2Portal, sessid, operation=operation, **kwargs)
# Extra functions # Extra functions
@FunctionCall.responder @FunctionCall.responder
def receive_functioncall(self, module, function, args, **kwargs): def receive_functioncall(self, module, function, func_args, func_kwargs):
""" """
This allows Portal- and Server-process to call an arbitrary This allows Portal- and Server-process to call an arbitrary
function in the other process. It is intended for use by function in the other process. It is intended for use by
@ -618,12 +608,12 @@ class AMPProtocol(amp.AMP):
`function` to call. `function` to call.
function (str): The name of the function to call in function (str): The name of the function to call in
`module`. `module`.
args, kwargs (any): These will be used as args/kwargs to func_args (str): Pickled args tuple for use in `function` call.
`function`. func_kwargs (str): Pickled kwargs dict for use in `function` call.
""" """
args = loads(args) args = loads(func_args)
kwargs = loads(kwargs) kwargs = loads(func_kwargs)
# call the function (don't catch tracebacks here) # call the function (don't catch tracebacks here)
result = variable_from_module(module, function)(*args, **kwargs) result = variable_from_module(module, function)(*args, **kwargs)

View file

@ -114,7 +114,7 @@ class PortalSessionHandler(SessionHandler):
#print "connecting", session.sessid, " number:", len(self.sessions) #print "connecting", session.sessid, " number:", len(self.sessions)
self.portal.amp_protocol.send_AdminPortal2Server(session.sessid, self.portal.amp_protocol.send_AdminPortal2Server(session.sessid,
operation=PCONN, operation=PCONN,
data=sessdata) sessiondata=sessdata)
def sync(self, session): def sync(self, session):
""" """
@ -143,7 +143,7 @@ class PortalSessionHandler(SessionHandler):
"server_data",)) "server_data",))
self.portal.amp_protocol.send_AdminPortal2Server(session.sessid, self.portal.amp_protocol.send_AdminPortal2Server(session.sessid,
operation=PCONNSYNC, operation=PCONNSYNC,
data=sessdata) sessiondata=sessdata)
def disconnect(self, session): def disconnect(self, session):
""" """
@ -390,7 +390,6 @@ class PortalSessionHandler(SessionHandler):
# data throttle (anti DoS measure) # data throttle (anti DoS measure)
now = time() now = time()
dT = now - self.command_counter_reset dT = now - self.command_counter_reset
print(" command rate:", _MAX_COMMAND_RATE / dT, dT, self.command_counter)
self.command_counter = 0 self.command_counter = 0
self.command_counter_reset = now self.command_counter_reset = now
self.command_overflow = dT < 1.0 self.command_overflow = dT < 1.0
@ -402,8 +401,8 @@ class PortalSessionHandler(SessionHandler):
# relay data to Server # relay data to Server
self.command_counter += 1 self.command_counter += 1
self.portal.amp_protocol.send_MsgPortal2Server(session.sessid, self.portal.amp_protocol.send_MsgPortal2Server(session.sessid,
msg=text, text=text,
data=kwargs) **kwargs)
else: else:
# called by the callLater callback # called by the callLater callback
if self.command_overflow: if self.command_overflow:

View file

@ -208,12 +208,15 @@ class ServerSession(Session):
idle timers and command counters. idle timers and command counters.
""" """
# Idle time used for timeout calcs.
self.cmd_last = time()
# Store the timestamp of the user's last command. # Store the timestamp of the user's last command.
if not idle: if not idle:
# Increment the user's command counter. # Increment the user's command counter.
self.cmd_total += 1 self.cmd_total += 1
# Player-visible idle time, not used in idle timeout calcs. # Player-visible idle time, not used in idle timeout calcs.
self.cmd_last_visible = time() self.cmd_last_visible = self.cmd_last
def data_in(self, text=None, **kwargs): def data_in(self, text=None, **kwargs):
""" """

View file

@ -282,11 +282,8 @@ class ServerSessionHandler(SessionHandler):
the Server. the Server.
""" """
data = {"protocol_path":protocol_path, self.server.amp_protocol.send_AdminServer2Portal(0, operation=SCONN,
"config":configdict} protocol_path=protocol_path, config=configdict)
self.server.amp_protocol.send_AdminServer2Portal(0,
operation=SCONN,
data=data)
def portal_shutdown(self): def portal_shutdown(self):
""" """
@ -294,8 +291,7 @@ class ServerSessionHandler(SessionHandler):
""" """
self.server.amp_protocol.send_AdminServer2Portal(0, self.server.amp_protocol.send_AdminServer2Portal(0,
operation=SSHUTD, operation=SSHUTD)
data="")
def login(self, session, player, testmode=False): def login(self, session, player, testmode=False):
""" """
@ -338,14 +334,12 @@ class ServerSessionHandler(SessionHandler):
string = "Logged in: {player} {address} ({nsessions} session(s) total)" string = "Logged in: {player} {address} ({nsessions} session(s) total)"
string = string.format(player=player,address=session.address, nsessions=nsess) string = string.format(player=player,address=session.address, nsessions=nsess)
session.log(string) session.log(string)
session.logged_in = True session.logged_in = True
# sync the portal to the session # sync the portal to the session
sessdata = {"logged_in": True}
if not testmode: if not testmode:
self.server.amp_protocol.send_AdminServer2Portal(session.sessid, self.server.amp_protocol.send_AdminServer2Portal(session.sessid,
operation=SLOGIN, operation=SLOGIN,
data=sessdata) sessiondata={"logged_in": True})
player.at_post_login(sessid=session.sessid) player.at_post_login(sessid=session.sessid)
def disconnect(self, session, reason=""): def disconnect(self, session, reason=""):
@ -375,7 +369,7 @@ class ServerSessionHandler(SessionHandler):
# inform portal that session should be closed. # inform portal that session should be closed.
self.server.amp_protocol.send_AdminServer2Portal(sessid, self.server.amp_protocol.send_AdminServer2Portal(sessid,
operation=SDISCONN, operation=SDISCONN,
data=reason) reason=reason)
def all_sessions_portal_sync(self): def all_sessions_portal_sync(self):
""" """
@ -386,7 +380,7 @@ class ServerSessionHandler(SessionHandler):
sessdata = self.get_all_sync_data() sessdata = self.get_all_sync_data()
return self.server.amp_protocol.send_AdminServer2Portal(0, return self.server.amp_protocol.send_AdminServer2Portal(0,
operation=SSYNC, operation=SSYNC,
data=sessdata) sessiondata=sessdata)
def disconnect_all_sessions(self, reason="You have been disconnected."): def disconnect_all_sessions(self, reason="You have been disconnected."):
""" """
@ -402,7 +396,7 @@ class ServerSessionHandler(SessionHandler):
# tell portal to disconnect all sessions # tell portal to disconnect all sessions
self.server.amp_protocol.send_AdminServer2Portal(0, self.server.amp_protocol.send_AdminServer2Portal(0,
operation=SDISCONNALL, operation=SDISCONNALL,
data=reason) reason=reason)
def disconnect_duplicate_sessions(self, curr_session, def disconnect_duplicate_sessions(self, curr_session,
reason=_("Logged in from elsewhere. Disconnecting.")): reason=_("Logged in from elsewhere. Disconnecting.")):
@ -586,8 +580,8 @@ class ServerSessionHandler(SessionHandler):
# send to all found sessions # send to all found sessions
for session in sessions: for session in sessions:
self.server.amp_protocol.send_MsgServer2Portal(sessid=session.sessid, self.server.amp_protocol.send_MsgServer2Portal(sessid=session.sessid,
msg=text, text=text,
data=kwargs) **kwargs)
def data_in(self, sessid, text="", **kwargs): def data_in(self, sessid, text="", **kwargs):
""" """

View file

@ -45,22 +45,28 @@ entered to get to this node). The node function code will only be
executed once per node-visit and the system will accept nodes with executed once per node-visit and the system will accept nodes with
both one or two arguments interchangeably. both one or two arguments interchangeably.
The menu tree itself is available on the caller as
`caller.ndb._menutree`. This makes it a convenient place to store
temporary state variables between nodes, since this NAttribute is
deleted when the menu is exited.
The return values must be given in the above order, but each can be The return values must be given in the above order, but each can be
returned as None as well. If the options are returned as None, the returned as None as well. If the options are returned as None, the
menu is immediately exited and the default "look" command is called. menu is immediately exited and the default "look" command is called.
text (str, tuple or None): Text shown at this node. If a tuple, the second text (str, tuple or None): Text shown at this node. If a tuple, the
element in the tuple is a help text to display at this node when second element in the tuple is a help text to display at this
the user enters the menu help command there. node when the user enters the menu help command there.
options (tuple, dict or None): ( {'key': name, # can also be a list of aliases. A special key is "_default", which options (tuple, dict or None): (
# marks this option as the default fallback when no other {'key': name, # can also be a list of aliases. A special key is
# option matches the user input. # "_default", which marks this option as the default
'desc': description, # option description # fallback when no other option matches the user input.
'desc': description, # optional description
'goto': nodekey, # node to go to when chosen 'goto': nodekey, # node to go to when chosen
'exec': nodekey, # node or callback to trigger as callback when chosen. If a node 'exec': nodekey}, # node or callback to trigger as callback when chosen.
# key is given the node will be executed once but its return u # If a node key is given, the node will be executed once
# values are ignored. If a callable is given, it must accept # but its return values are ignored. If a callable is
# one or two args, like any node. # given, it must accept one or two args, like any node.
{...}, ...) {...}, ...)
If key is not given, the option will automatically be identified by If key is not given, the option will automatically be identified by
@ -122,9 +128,9 @@ The menu tree is exited either by using the in-menu quit command or by
reaching a node without any options. reaching a node without any options.
For a menu demo, import CmdTestDemo from this module and add it to For a menu demo, import CmdTestMenu from this module and add it to
your default cmdset. Run it with this module, like `testdemo your default cmdset. Run it with this module, like `testmenu
evennia.utils.evdemo`. evennia.utils.evmenu`.
""" """
from __future__ import print_function from __future__ import print_function
@ -176,65 +182,25 @@ class EvMenuError(RuntimeError):
class CmdEvMenuNode(Command): class CmdEvMenuNode(Command):
""" """
Menu options. Menu options.
""" """
key = "look" key = _CMD_NOINPUT
aliases = ["l", _CMD_NOMATCH, _CMD_NOINPUT] aliases = [_CMD_NOMATCH]
locks = "cmd:all()" locks = "cmd:all()"
help_category = "Menu" help_category = "Menu"
def func(self): def func(self):
""" """
Implement all menu commands. Implement all menu commands.
""" """
caller = self.caller caller = self.caller
menu = caller.ndb._menutree menu = caller.ndb._menutree
if not menu: if not menu:
err = "Menu object not found as %s.ndb._menutree!" % (caller) err = "Menu object not found as %s.ndb._menutree!" % (caller)
self.caller.msg(err) caller.msg(err)
raise EvMenuError(err) raise EvMenuError(err)
# flags and data menu.parse_input(self.raw_string)
raw_string = self.raw_string
cmd = raw_string.strip().lower()
options = menu.options
allow_quit = menu.allow_quit
cmd_on_quit = menu.cmd_on_quit
default = menu.default
print("cmd, options:", cmd, options)
if cmd in options:
# this will overload the other commands
# if it has the same name!
goto, callback = options[cmd]
if callback:
menu.callback(callback, raw_string)
if goto:
menu.goto(goto, raw_string)
elif cmd in ("look", "l"):
caller.msg(menu.nodetext)
elif cmd in ("help", "h"):
caller.msg(menu.helptext)
elif allow_quit and cmd in ("quit", "q", "exit"):
menu.close_menu()
if cmd_on_quit is not None:
caller.execute_cmd(cmd_on_quit)
elif default:
goto, callback = default
if callback:
menu.callback(callback, raw_string)
if goto:
menu.goto(goto, raw_string)
else:
caller.msg(_HELP_NO_OPTION_MATCH)
if not (options or default):
# no options - we are at the end of the menu.
menu.close_menu()
if cmd_on_quit is not None:
caller.execute_cmd(cmd_on_quit)
class EvMenuCmdSet(CmdSet): class EvMenuCmdSet(CmdSet):
@ -269,7 +235,9 @@ class EvMenu(object):
""" """
def __init__(self, caller, menudata, startnode="start", def __init__(self, caller, menudata, startnode="start",
cmdset_mergetype="Replace", cmdset_priority=1, cmdset_mergetype="Replace", cmdset_priority=1,
allow_quit=True, cmd_on_quit="look"): allow_quit=True, cmd_on_quit="look",
nodetext_formatter=None, options_formatter=None,
node_formatter=None):
""" """
Initialize the menu tree and start the caller onto the first node. Initialize the menu tree and start the caller onto the first node.
@ -299,11 +267,35 @@ class EvMenu(object):
allow_quit (bool, optional): Allow user to use quit or allow_quit (bool, optional): Allow user to use quit or
exit to leave the menu at any point. Recommended during exit to leave the menu at any point. Recommended during
development! development!
cmd_on_quit (str or None, optional): When exiting the menu cmd_on_quit (callable, str or None, optional): When exiting the menu
(either by reaching a node with no options or by using the (either by reaching a node with no options or by using the
in-built quit command (activated with `allow_quit`), this in-built quit command (activated with `allow_quit`), this
command string will be executed. Set to None to not call callback function or command string will be executed.
any command. The callback function takes two parameters, the caller then the
EvMenu object. This is called after cleanup is complete.
Set to None to not call any command.
nodetext_formatter (callable, optional): This callable should be on
the form `function(nodetext, has_options)`, where `nodetext` is the
node text string and `has_options` a boolean specifying if there
are options associated with this node. It must return a formatted
string.
options_formatter (callable, optional): This callable should be on
the form `function(optionlist)`, where ` optionlist is a list
of option dictionaries, like
[{"key":..., "desc",..., "goto": ..., "exec",...}, ...]
Each dictionary describes each possible option. Note that this
will also be called if there are no options, and so should be
able to handle an empty list. This should
be formatted into an options list and returned as a string,
including the required separator to use between the node text
and the options. If not given the default EvMenu style will be used.
node_formatter (callable, optional): This callable should be on the
form `func(nodetext, optionstext)` where the arguments are strings
representing the node text and options respectively (possibly prepared
by `nodetext_formatter`/`options_formatter` or by the default styles).
It should return a string representing the final look of the node. This
can e.g. be used to create line separators that take into account the
dynamic width of the parts.
Raises: Raises:
EvMenuError: If the start/end node is not found in menu tree. EvMenuError: If the start/end node is not found in menu tree.
@ -313,17 +305,27 @@ class EvMenu(object):
self._startnode = startnode self._startnode = startnode
self._menutree = self._parse_menudata(menudata) self._menutree = self._parse_menudata(menudata)
self._nodetext_formatter = nodetext_formatter
self._options_formatter = nodetext_formatter
self._node_formatter = node_formatter
if startnode not in self._menutree: if startnode not in self._menutree:
raise EvMenuError("Start node '%s' not in menu tree!" % startnode) raise EvMenuError("Start node '%s' not in menu tree!" % startnode)
# variables made available to the command # variables made available to the command
self.allow_quit = allow_quit self.allow_quit = allow_quit
if isinstance(cmd_on_quit, str):
self.cmd_on_quit = lambda caller, menu: caller.execute_cmd(cmd_on_quit)
elif callable(cmd_on_quit):
self.cmd_on_quit = cmd_on_quit self.cmd_on_quit = cmd_on_quit
else:
self.cmd_on_quit = None
self.default = None self.default = None
self.nodetext = None self.nodetext = None
self.helptext = None self.helptext = None
self.options = None self.options = None
# store ourself on the object # store ourself on the object
self._caller.ndb._menutree = self self._caller.ndb._menutree = self
@ -382,20 +384,22 @@ class EvMenu(object):
# handle the node text # handle the node text
# #
if self._nodetext_formatter:
# use custom formatter
nodetext = self._nodetext_formatter(nodetext, len(optionlist))
else:
nodetext = dedent(nodetext).strip() nodetext = dedent(nodetext).strip()
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n")) nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
if not optionlist:
# return the node text "naked".
separator1 = "_" * nodetext_width_max + "\n\n" if nodetext_width_max else ""
separator2 = "\n" if nodetext_width_max else "" + "_" * nodetext_width_max
return separator1 + nodetext + separator2
# #
# handle the options # handle the options
# #
if self._options_formatter:
# use custom formatter
optionstext = self._options_formatter(optionlist)
elif optionlist:
# column separation distance # column separation distance
colsep = 4 colsep = 4
@ -436,20 +440,29 @@ class EvMenu(object):
table = [table[icol*nrows:(icol*nrows) + nrows] for icol in xrange(0, ncols)] table = [table[icol*nrows:(icol*nrows) + nrows] for icol in xrange(0, ncols)]
# adjust the width of each column # adjust the width of each column
total_width = 0
for icol in xrange(len(table)): for icol in xrange(len(table)):
col_width = max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep col_width = max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep
table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]] table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]]
total_width += col_width
# format the table into columns # format the table into columns
table = EvTable(table=table, border="none") optionstext = unicode(EvTable(table=table, border="none"))
else:
optionstext = ""
options_width_max = max(m_len(line) for line in optionstext.split("\n"))
#
# format the entire node
#
if self._node_formatter:
# use custom formatter
return self._node_formatter(nodetext, optionstext)
else:
# build the page # build the page
total_width = max(total_width, nodetext_width_max) total_width = max(options_width_max, nodetext_width_max)
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
return separator1 + nodetext + separator2 + unicode(table) return separator1 + nodetext + separator2 + optionstext
def _execute_node(self, nodename, raw_string): def _execute_node(self, nodename, raw_string):
""" """
@ -488,6 +501,56 @@ class EvMenu(object):
return nodetext, options return nodetext, options
def _display_nodetext(self):
self._caller.msg(self.nodetext)
def _display_helptext(self):
self._caller.msg(self.helptext)
def _callback_goto(self, callback, goto, raw_string):
if callback:
self.callback(callback, raw_string)
if goto:
self.goto(goto, raw_string)
def parse_input(self, raw_string):
"""
Processes the user' node inputs.
Args:
raw_string (str): The incoming raw_string from the menu
command.
"""
caller = self._caller
cmd = raw_string.strip().lower()
allow_quit = self.allow_quit
if cmd in self.options:
# this will take precedence over the default commands
# below
goto, callback = self.options[cmd]
self._callback_goto(callback, goto, raw_string)
elif cmd in ("look", "l"):
self._display_nodetext()
elif cmd in ("help", "h"):
self._display_helptext()
elif allow_quit and cmd in ("quit", "q", "exit"):
self.close_menu()
elif self.default:
goto, callback = self.default
self._callback_goto(callback, goto, raw_string)
else:
caller.msg(_HELP_NO_OPTION_MATCH)
if not (self.options or self.default):
# no options - we are at the end of the menu.
self.close_menu()
def callback(self, nodename, raw_string): def callback(self, nodename, raw_string):
""" """
Run a node as a callback. This makes no use of the return Run a node as a callback. This makes no use of the return
@ -582,7 +645,7 @@ class EvMenu(object):
else: else:
self.helptext = _HELP_NO_OPTIONS if self.allow_quit else _HELP_NO_OPTIONS_NO_QUIT self.helptext = _HELP_NO_OPTIONS if self.allow_quit else _HELP_NO_OPTIONS_NO_QUIT
self._caller.execute_cmd("look") self._display_nodetext()
def close_menu(self): def close_menu(self):
""" """
@ -590,6 +653,8 @@ class EvMenu(object):
""" """
self._caller.cmdset.remove(EvMenuCmdSet) self._caller.cmdset.remove(EvMenuCmdSet)
del self._caller.ndb._menutree del self._caller.ndb._menutree
if self.cmd_on_quit is not None:
self.cmd_on_quit(self._caller, self)
# ------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------
@ -689,6 +754,9 @@ def test_start_node(caller):
"desc": "Set an attribute on yourself.", "desc": "Set an attribute on yourself.",
"exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"), "exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"),
"goto": "test_set_node"}, "goto": "test_set_node"},
{"key": ("{yL{nook", "l"),
"desc": "Look and see a custom message.",
"goto": "test_look_node"},
{"key": ("{yV{niew", "v"), {"key": ("{yV{niew", "v"),
"desc": "View your own name", "desc": "View your own name",
"goto": "test_view_node"}, "goto": "test_view_node"},
@ -700,6 +768,13 @@ def test_start_node(caller):
return text, options return text, options
def test_look_node(caller):
text = "Looking again will take you back to the previous message."
options = {"key": ("{yL{nook", "l"),
"desc": "Go back to the previous menu.",
"goto": "test_start_node"}
return text, options
def test_set_node(caller): def test_set_node(caller):
text = (""" text = ("""
The attribute 'menuattrtest' was set to The attribute 'menuattrtest' was set to

View file

@ -1537,7 +1537,7 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
error = "" error = ""
if not matches: if not matches:
# no results. # no results.
error = kwargs.get("nofound_string", _("Could not find '%s'." % query)) error = kwargs.get("nofound_string") or _("Could not find '%s'." % query)
matches = None matches = None
elif len(matches) > 1: elif len(matches) > 1:
error = kwargs.get("multimatch_string", None) error = kwargs.get("multimatch_string", None)

View file

@ -3,5 +3,5 @@
django >= 1.8, < 1.9 django >= 1.8, < 1.9
twisted >= 15.2.1 twisted >= 15.2.1
mock >= 1.0.1 mock >= 1.0.1
pillow pillow == 2.9.0
pytz pytz

View file

@ -6,5 +6,5 @@ pypiwin32
django >= 1.8, < 1.9 django >= 1.8, < 1.9
twisted >= 15.2.1 twisted >= 15.2.1
mock >= 1.0.1 mock >= 1.0.1
pillow pillow == 2.9.0
pytz pytz