From 6a11da1714c1ae85b5713832578a99652cce6436 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 8 Oct 2015 10:59:21 +0200 Subject: [PATCH 01/24] A lingering wrong call to at_search_result that was missed in the previous changes. Resolves #813. --- evennia/objects/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index ff3e8f628..0370fa0f2 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -370,7 +370,7 @@ class DefaultObject(ObjectDB): if quiet: 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): """ From 1348fae5a08599775746ef78142cf184352ac177 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 11 Oct 2015 18:49:38 +0200 Subject: [PATCH 02/24] Reworked the flow of the data property through AMP to fix inconsistencies and make some overall cleanup of the arguments used to send AMP messages. Resolves #815. --- evennia/server/amp.py | 106 ++++++++---------- evennia/server/portal/portalsessionhandler.py | 10 +- evennia/server/sessionhandler.py | 24 ++-- 3 files changed, 62 insertions(+), 78 deletions(-) diff --git a/evennia/server/amp.py b/evennia/server/amp.py index deb240b87..d7b62ef00 100644 --- a/evennia/server/amp.py +++ b/evennia/server/amp.py @@ -249,7 +249,7 @@ class MsgPortal2Server(amp.Command): """ key = "MsgPortal2Server" - arguments = [('data', Compressed())] + arguments = [('packed_data', Compressed())] errors = [(Exception, 'EXCEPTION')] response = [] @@ -260,7 +260,7 @@ class MsgServer2Portal(amp.Command): """ key = "MsgServer2Portal" - arguments = [('data', Compressed())] + arguments = [('packed_data', Compressed())] errors = [(Exception, 'EXCEPTION')] response = [] @@ -274,7 +274,7 @@ class AdminPortal2Server(amp.Command): """ key = "AdminPortal2Server" - arguments = [('data', Compressed())] + arguments = [('packed_data', Compressed())] errors = [(Exception, 'EXCEPTION')] response = [] @@ -288,7 +288,7 @@ class AdminServer2Portal(amp.Command): """ key = "AdminServer2Portal" - arguments = [('data', Compressed())] + arguments = [('packed_data', Compressed())] errors = [(Exception, 'EXCEPTION')] response = [] @@ -361,7 +361,7 @@ class AMPProtocol(amp.AMP): sessdata = self.factory.portal.sessions.get_all_sync_data() self.send_AdminPortal2Server(0, PSYNC, - data=sessdata) + sessiondata=sessdata) self.factory.portal.sessions.at_server_connection() if hasattr(self.factory, "server_restart_mode"): del self.factory.server_restart_mode @@ -398,53 +398,49 @@ class AMPProtocol(amp.AMP): (sessid, kwargs). """ - batch = dumps((sessid, kwargs)) 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 # Portal -> Server Msg @MsgPortal2Server.responder - def server_receive_msgportal2server(self, data): + def server_receive_msgportal2server(self, packed_data): """ Receives message arriving to server. This method is executed on the Server. 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"]) - self.factory.server.sessions.data_in(sessid, - text=kwargs["msg"], - data=kwargs["data"]) + self.factory.server.sessions.data_in(sessid, **kwargs) 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. Args: sessid (int): Unique Session id. msg (str): Message to send over the wire. - data (str, optional): Optional data. + kwargs (any, optional): Optional data. Returns: deferred (Deferred): Asynchronous return. """ #print "msg portal->server (portal side):", sessid, msg, data - return self.send_data(MsgPortal2Server, sessid, - msg=msg, - data=data) + return self.send_data(MsgPortal2Server, sessid, text=text, **kwargs) # Server -> Portal message @MsgServer2Portal.responder - def portal_receive_server2portal(self, data): + def portal_receive_server2portal(self, packed_data): """ Receives message arriving to Portal from Server. This method is executed on the Portal. @@ -455,17 +451,15 @@ class AMPProtocol(amp.AMP): before continuing. 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"]) - self.factory.portal.sessions.data_out(sessid, - text=kwargs["msg"], - data=kwargs["data"]) + self.factory.portal.sessions.data_out(sessid, **kwargs) return {} - def send_MsgServer2Portal(self, sessid, msg="", data=""): + def send_MsgServer2Portal(self, sessid, text="", **kwargs): """ Access method - executed on the Server for sending data to Portal. @@ -473,39 +467,37 @@ class AMPProtocol(amp.AMP): Args: sessid (int): Unique Session id. 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 - 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 @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 perform admin operations on the server). This is executed on the Server. 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 - sessid, kwargs = loads(data) - - operation = kwargs["operation"] - data = kwargs["data"] + sessid, kwargs = loads(packed_data) + operation = kwargs.pop("operation", "") server_sessionhandler = self.factory.server.sessions #print "serveradmin (server side):", sessid, ord(operation), data if operation == PCONN: # portal_session_connect # 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 - server_sessionhandler.portal_session_sync(data) + server_sessionhandler.portal_session_sync(kwargs.get("sessiondata")) elif operation == PDISCONN: # portal_session_disconnect # session closed from portal side @@ -517,12 +509,12 @@ class AMPProtocol(amp.AMP): # contains a dict {sessid: {arg1:val1,...}} # representing the attributes to sync for each # session. - server_sessionhandler.portal_sessions_sync(data) + server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata")) else: raise Exception("operation %(op)s not recognized." % {'op': operation}) return {} - def send_AdminPortal2Server(self, sessid, operation="", data=""): + def send_AdminPortal2Server(self, sessid, operation="", **kwargs): """ Send Admin instructions from the Portal to the Server. Executed @@ -532,42 +524,41 @@ class AMPProtocol(amp.AMP): sessid (int): Session id. operation (char, optional): Identifier for the server operation, as defined by the 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 - 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 @AdminServer2Portal.responder - def portal_receive_adminserver2portal(self, data): + def portal_receive_adminserver2portal(self, packed_data): """ Receives and handles admin operations sent to the Portal This is executed on the Portal. 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 - sessid, kwargs = loads(data) - operation = kwargs["operation"] - data = kwargs["data"] + sessid, kwargs = loads(packed_data) + operation = kwargs.pop("operation") portal_sessionhandler = self.factory.portal.sessions if operation == SLOGIN: # server_session_login # 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 # 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 # 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 # the server orders the portal to shut down @@ -576,18 +567,18 @@ class AMPProtocol(amp.AMP): elif operation == SSYNC: # server_session_sync # server wants to save session data to the portal, # 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 self.factory.server_restart_mode = True elif operation == SCONN: # server_force_connection (for irc/imc2 etc) - portal_sessionhandler.server_connect(**data) + portal_sessionhandler.server_connect(**kwargs) else: raise Exception("operation %(op)s not recognized." % {'op': operation}) 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 instruction to the Portal. @@ -597,16 +588,15 @@ class AMPProtocol(amp.AMP): operation (char, optional): Identifier for the server operation, as defined by the global variables in `evennia/server/amp.py`. - data (str, optional): Data going into the adminstrative - operation. + data (str or dict, optional): Data going into the adminstrative. """ - return self.send_data(AdminServer2Portal, sessid, operation=operation, data=data) + return self.send_data(AdminServer2Portal, sessid, operation=operation, **kwargs) # Extra functions @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 function in the other process. It is intended for use by @@ -617,12 +607,12 @@ class AMPProtocol(amp.AMP): `function` to call. function (str): The name of the function to call in `module`. - args, kwargs (any): These will be used as args/kwargs to - `function`. + func_args (str): Pickled args tuple for use in `function` call. + func_kwargs (str): Pickled kwargs dict for use in `function` call. """ - args = loads(args) - kwargs = loads(kwargs) + args = loads(func_args) + kwargs = loads(func_kwargs) # call the function (don't catch tracebacks here) result = variable_from_module(module, function)(*args, **kwargs) diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 00ede7adf..8dc6e0bbf 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -113,7 +113,7 @@ class PortalSessionHandler(SessionHandler): #print "connecting", session.sessid, " number:", len(self.sessions) self.portal.amp_protocol.send_AdminPortal2Server(session.sessid, operation=PCONN, - data=sessdata) + sessiondata=sessdata) def sync(self, session): """ @@ -142,7 +142,7 @@ class PortalSessionHandler(SessionHandler): "server_data",)) self.portal.amp_protocol.send_AdminPortal2Server(session.sessid, operation=PCONNSYNC, - data=sessdata) + sessiondata=sessdata) def disconnect(self, session): """ @@ -389,7 +389,7 @@ class PortalSessionHandler(SessionHandler): # data throttle (anti DoS measure) now = time() dT = now - self.command_counter_reset - print " command rate:", _MAX_COMMAND_RATE / dT, dT, self.command_counter + #print " command rate:", _MAX_COMMAND_RATE / dT, dT, self.command_counter self.command_counter = 0 self.command_counter_reset = now self.command_overflow = dT < 1.0 @@ -401,8 +401,8 @@ class PortalSessionHandler(SessionHandler): # relay data to Server self.command_counter += 1 self.portal.amp_protocol.send_MsgPortal2Server(session.sessid, - msg=text, - data=kwargs) + text=text, + **kwargs) else: # called by the callLater callback if self.command_overflow: diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index f86e33166..0d5163910 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -282,11 +282,8 @@ class ServerSessionHandler(SessionHandler): the Server. """ - data = {"protocol_path":protocol_path, - "config":configdict} - self.server.amp_protocol.send_AdminServer2Portal(0, - operation=SCONN, - data=data) + self.server.amp_protocol.send_AdminServer2Portal(0, operation=SCONN, + protocol_path=protocol_path, config=configdict) def portal_shutdown(self): """ @@ -294,8 +291,7 @@ class ServerSessionHandler(SessionHandler): """ self.server.amp_protocol.send_AdminServer2Portal(0, - operation=SSHUTD, - data="") + operation=SSHUTD) def login(self, session, player, testmode=False): """ @@ -338,14 +334,12 @@ class ServerSessionHandler(SessionHandler): string = "Logged in: {player} {address} ({nsessions} session(s) total)" string = string.format(player=player,address=session.address, nsessions=nsess) session.log(string) - session.logged_in = True # sync the portal to the session - sessdata = {"logged_in": True} if not testmode: self.server.amp_protocol.send_AdminServer2Portal(session.sessid, operation=SLOGIN, - data=sessdata) + sessiondata={"logged_in": True}) player.at_post_login(sessid=session.sessid) def disconnect(self, session, reason=""): @@ -375,7 +369,7 @@ class ServerSessionHandler(SessionHandler): # inform portal that session should be closed. self.server.amp_protocol.send_AdminServer2Portal(sessid, operation=SDISCONN, - data=reason) + reason=reason) def all_sessions_portal_sync(self): """ @@ -386,7 +380,7 @@ class ServerSessionHandler(SessionHandler): sessdata = self.get_all_sync_data() return self.server.amp_protocol.send_AdminServer2Portal(0, operation=SSYNC, - data=sessdata) + sessiondata=sessdata) def disconnect_all_sessions(self, reason="You have been disconnected."): """ @@ -402,7 +396,7 @@ class ServerSessionHandler(SessionHandler): # tell portal to disconnect all sessions self.server.amp_protocol.send_AdminServer2Portal(0, operation=SDISCONNALL, - data=reason) + reason=reason) def disconnect_duplicate_sessions(self, curr_session, reason=_("Logged in from elsewhere. Disconnecting.")): @@ -586,8 +580,8 @@ class ServerSessionHandler(SessionHandler): # send to all found sessions for session in sessions: self.server.amp_protocol.send_MsgServer2Portal(sessid=session.sessid, - msg=text, - data=kwargs) + text=text, + **kwargs) def data_in(self, sessid, text="", **kwargs): """ From 90c6ade56deee9ca35009accdd1d391fd37ed570 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 11 Oct 2015 19:08:32 +0200 Subject: [PATCH 03/24] Made sure the command sends raw evals as raw data. Resolves #816. This removes color completely from raw evals but they will still be present if using self.msg() in the evaluation (if doing so, this particular side effect still exists too though). --- evennia/commands/default/system.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index c22b38bd7..331d29409 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -194,23 +194,23 @@ class CmdPy(MuxCommand): t0 = timemeasure() ret = eval(pycode_compiled, {}, available_vars) t1 = timemeasure() - duration = " (%.4f ms)" % ((t1 - t0) * 1000) + duration = " (runtime ~ %.4f ms)" % ((t1 - t0) * 1000) else: ret = eval(pycode_compiled, {}, available_vars) if mode == "eval": - ret = "{n<<< %s%s" % (str(ret), duration) + ret = "<<< %s%s" % (str(ret), duration) else: - ret = "{n<<< Done.%s" % duration + ret = "<<< Done (use self.msg() if you want to catch output)%s" % duration except Exception: errlist = traceback.format_exc().split('\n') if len(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: - self.msg(ret, sessid=self.sessid) + self.msg(ret, sessid=self.sessid, raw=True) except TypeError: - self.msg(ret) + self.msg(ret, raw=True) # helper function. Kept outside so it can be imported and run From c191996975e2e498a4710ed70b2e6bf4af8d772b Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 11 Oct 2015 19:22:14 +0200 Subject: [PATCH 04/24] Changed so that channel names can use any case. Resolves #814. --- evennia/commands/default/comms.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index ab66000f4..d89f23f68 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -519,17 +519,15 @@ class CmdChannelCreate(MuxPlayerCommand): channame = lhs aliases = None if ';' in lhs: - channame, aliases = [part.strip().lower() - for part in lhs.split(';', 1) if part.strip()] - aliases = [alias.strip().lower() - for alias in aliases.split(';') if alias.strip()] + channame, aliases = lhs.split(';', 1) + aliases = [alias.strip().lower() for alias in aliases.split(';')] channel = ChannelDB.objects.channel_search(channame) if channel: self.msg("A channel with that name already exists.") return # Create and set the channel up lockstring = "send:all();listen:all();control:id(%s)" % caller.id - new_chan = create.create_channel(channame, + new_chan = create.create_channel(channame.strip(), aliases, description, locks=lockstring) From f111753e0ffc072e23edf869f74c178b282745c2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 13 Oct 2015 22:47:37 +0200 Subject: [PATCH 05/24] Forcing pillow 2.9.0 for now to avoid the new forced dependencies introduced in 3.0. --- requirements.txt | 2 +- win_requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7a552253f..889423270 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ django >= 1.8, < 1.9 twisted >= 15.2.1 mock >= 1.0.1 -pillow +pillow == 2.9.0 pytz diff --git a/win_requirements.txt b/win_requirements.txt index 82d1b2c23..787d64367 100644 --- a/win_requirements.txt +++ b/win_requirements.txt @@ -6,5 +6,5 @@ pypiwin32 django >= 1.8, < 1.9 twisted >= 15.2.1 mock >= 1.0.1 -pillow +pillow == 2.9.0 pytz From 767cb693f39a8b6c160f4abbe9113de50f7129d5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 15 Oct 2015 17:02:10 +0200 Subject: [PATCH 06/24] Update settings.py --- evennia/game_template/server/conf/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evennia/game_template/server/conf/settings.py b/evennia/game_template/server/conf/settings.py index 6ea4f4ad9..9a29edd53 100644 --- a/evennia/game_template/server/conf/settings.py +++ b/evennia/game_template/server/conf/settings.py @@ -62,6 +62,8 @@ DATABASES = {{ ###################################################################### # 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. From a745a47d5a8a699a876e6d0a18ec4a7539bf5daa Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Fri, 16 Oct 2015 09:47:55 -0700 Subject: [PATCH 07/24] Remove extra space in comment. --- evennia/contrib/menu_login.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/menu_login.py b/evennia/contrib/menu_login.py index c70972a9c..2eb95c8e8 100644 --- a/evennia/contrib/menu_login.py +++ b/evennia/contrib/menu_login.py @@ -329,7 +329,7 @@ class UnloggedInCmdSet(CmdSet): priority = 0 def at_cmdset_creation(self): - "Called when cmdset is first created" + "Called when cmdset is first created." self.add(CmdUnloggedinLook()) @@ -337,7 +337,7 @@ class CmdUnloggedinLook(Command): """ 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 - to the menu's own look command.. + to the menu's own look command. """ key = CMD_LOGINSTART # obs, this should NOT have aliases for look or l, this will clash with the menu version! From 3cfee6dc87ac6cb2855d8384ee2cae0fcbaf9bab Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Fri, 16 Oct 2015 17:17:32 +0000 Subject: [PATCH 08/24] Fix comments about connection_screens.py --- evennia/commands/connection_screen.py | 2 +- evennia/contrib/menu_login.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/commands/connection_screen.py b/evennia/commands/connection_screen.py index c164fe62f..f6160db90 100644 --- a/evennia/commands/connection_screen.py +++ b/evennia/commands/connection_screen.py @@ -1,6 +1,6 @@ # # 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 diff --git a/evennia/contrib/menu_login.py b/evennia/contrib/menu_login.py index 2eb95c8e8..f62ca8a00 100644 --- a/evennia/contrib/menu_login.py +++ b/evennia/contrib/menu_login.py @@ -22,7 +22,7 @@ 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 information about commands which are not used in this version of the login. You can change the screen used by editing -`mygame/server/conf/connection_screens.py`. +`$GAME_DIR/server/conf/connection_screens.py`. """ From 030e252f36fab0e7e8a75c7057d9570e248107e4 Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Fri, 16 Oct 2015 17:25:50 +0000 Subject: [PATCH 09/24] The rest of Evennia uses lowercase i in UnloggedinCmdSet. --- evennia/contrib/menu_login.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/menu_login.py b/evennia/contrib/menu_login.py index f62ca8a00..7868aa998 100644 --- a/evennia/contrib/menu_login.py +++ b/evennia/contrib/menu_login.py @@ -15,7 +15,7 @@ Install is simple: 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. @@ -323,7 +323,7 @@ node3 = MenuNode("node3", text=LOGIN_SCREEN_HELP, # access commands -class UnloggedInCmdSet(CmdSet): +class UnloggedinCmdSet(CmdSet): "Cmdset for the unloggedin state" key = "DefaultUnloggedin" priority = 0 From 7037ac2e5f4b1f8fdfd818579b49dd256a534d5b Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Fri, 16 Oct 2015 12:19:33 -0700 Subject: [PATCH 10/24] Fix typo in menusystem.py. --- evennia/contrib/menusystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/menusystem.py b/evennia/contrib/menusystem.py index f243e45ff..3e81c30cf 100644 --- a/evennia/contrib/menusystem.py +++ b/evennia/contrib/menusystem.py @@ -281,7 +281,7 @@ class MenuNode(object): text (str, optional): The text that will be displayed at top when viewing this node. 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 used (or a number) linktexts (list)- A list of texts to describe the links. Must From 0c7ee1bdfc7eea774d5708104641bedf1ae5159a Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 16 Oct 2015 22:34:40 +0200 Subject: [PATCH 11/24] Fixed a bug in search error reporting. Made accept to change a player's name. Closes PR: Resolves #820, Resolves #821, Resolves #822, Resolves #823. --- evennia/commands/default/building.py | 28 +++++++++++++++++++++++++--- evennia/utils/utils.py | 2 +- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 2d1d139d7..ea8168cbe 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1093,7 +1093,8 @@ class CmdName(ObjManipCommand): Usage: @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 if not self.args: - string = "Usage: @name = [;alias;alias;...]" - caller.msg(string) + caller.msg("Usage: @name = [;alias;alias;...]") return if self.lhs_objs: 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) if not obj: return @@ -1125,6 +1144,9 @@ class CmdName(ObjManipCommand): if not newname and not aliases: caller.msg("No names or aliases defined!") 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: if newname: obj.name = newname diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 883864c10..e92bc9d73 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1536,7 +1536,7 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs): error = "" if not matches: # 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 elif len(matches) > 1: error = kwargs.get("multimatch_string", None) From cd211d99010184db02f6826ba8c84564d76b4ad3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 17 Oct 2015 09:05:49 +0200 Subject: [PATCH 12/24] Added a mention of the use of caller.ndb._menutree in the docs to EvMenu. --- evennia/utils/evmenu.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 5fba5f680..4bb600a49 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -45,6 +45,11 @@ 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 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 returned as None as well. If the options are returned as None, the menu is immediately exited and the default "look" command is called. @@ -203,7 +208,6 @@ class CmdEvMenuNode(Command): 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! From cba12699fd7dc85ec3ac76a78cf70ed156878a5d Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Fri, 16 Oct 2015 15:26:42 -0700 Subject: [PATCH 13/24] Fix typos in documentation. --- evennia/utils/evmenu.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 4bb600a49..48ad4c27d 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -127,9 +127,9 @@ The menu tree is exited either by using the in-menu quit command or by reaching a node without any options. -For a menu demo, import CmdTestDemo from this module and add it to -your default cmdset. Run it with this module, like `testdemo -evennia.utils.evdemo`. +For a menu demo, import CmdTestMenu from this module and add it to +your default cmdset. Run it with this module, like `testmenu +evennia.utils.evmenu`. """ From b4cc24e65516e7d0ee5848a5ef22e5a144885f74 Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Fri, 16 Oct 2015 19:42:42 +0000 Subject: [PATCH 14/24] Improve help command. --- evennia/commands/default/help.py | 1 + 1 file changed, 1 insertion(+) diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index c5c9b431a..2d7dd225d 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -74,6 +74,7 @@ class CmdHelp(Command): """ key = "help" locks = "cmd:all()" + arg_regex = r"\s|$" # this is a special cmdhandler flag that makes the cmdhandler also pack # the current cmdset with the call to self.func(). From a7db340d82c905588426832871f69adbc4b68cc6 Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Sat, 17 Oct 2015 23:33:33 +0000 Subject: [PATCH 15/24] Fix idle timeouts. The fix for rate limiting removed the code which updated cmd_last, which is used to determine whether someone is idle. --- evennia/server/serversession.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index d56b68e56..492e58256 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -208,12 +208,15 @@ class ServerSession(Session): idle timers and command counters. """ + # Idle time used for timeout calcs. + self.cmd_last = time() + # Store the timestamp of the user's last command. if not idle: # Increment the user's command counter. self.cmd_total += 1 # 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): """ From 53ec675960f7925d271e1ac266477e3e941052b5 Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Sat, 17 Oct 2015 22:43:25 +0000 Subject: [PATCH 16/24] Add extra space between sentences. --- evennia/commands/cmdhandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index d40d8bfcc..9579d2c94 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -95,7 +95,7 @@ _ERROR_NOCMDSETS = "No command sets found! This is a sign of a critical bug." \ "\nsome other means for assistance." _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." _ERROR_RECURSION_LIMIT = "Command recursion limit ({recursion_limit}) " \ From 5fa9978e1aa78fb8f3160be50a7a73022a828f59 Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Sat, 17 Oct 2015 19:44:20 +0000 Subject: [PATCH 17/24] Make the EvMenu docs look a little cleaner. --- evennia/utils/evmenu.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 48ad4c27d..dd057d918 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -54,19 +54,20 @@ 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 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 - element in the tuple is a help text to display at this 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 - # marks this option as the default fallback when no other - # option matches the user input. - 'desc': description, # option description - 'goto': nodekey, # node to go to when chosen - 'exec': nodekey, # node or callback to trigger as callback when chosen. If a node - # key is given the node will be executed once but its return u - # values are ignored. If a callable is given, it must accept - # one or two args, like any node. - {...}, ...) + text (str, tuple or None): Text shown at this node. If a tuple, the + second element in the tuple is a help text to display at this + 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 marks this option as the default + # fallback when no other option matches the user input. + 'desc': description, # optional description + 'goto': nodekey, # node to go to when chosen + 'exec': nodekey}, # node or callback to trigger as callback when chosen. + # If a node key is given, the node will be executed once + # but its return values are ignored. If a callable is + # given, it must accept one or two args, like any node. + {...}, ...) If key is not given, the option will automatically be identified by its number 1..N. From 848f3295136755e96f50d6da459361130ea1cc77 Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Sat, 17 Oct 2015 20:54:40 +0000 Subject: [PATCH 18/24] Add a handle method to react to user input. --- evennia/utils/evmenu.py | 88 +++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index dd057d918..c3d7ea7fe 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -181,64 +181,25 @@ class EvMenuError(RuntimeError): class CmdEvMenuNode(Command): """ Menu options. - """ - key = "look" - aliases = ["l", _CMD_NOMATCH, _CMD_NOINPUT] + key = _CMD_NOINPUT + aliases = [_CMD_NOMATCH] locks = "cmd:all()" help_category = "Menu" def func(self): """ Implement all menu commands. - """ caller = self.caller menu = caller.ndb._menutree if not menu: err = "Menu object not found as %s.ndb._menutree!" % (caller) - self.caller.msg(err) + caller.msg(err) raise EvMenuError(err) - # flags and data - 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 - - 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) + menu.handle(self.raw_string) class EvMenuCmdSet(CmdSet): @@ -492,6 +453,47 @@ class EvMenu(object): return nodetext, options + def handle(self, raw_string): + # flags and data + caller = self._caller + cmd = raw_string.strip().lower() + options = self.options + allow_quit = self.allow_quit + cmd_on_quit = self.cmd_on_quit + default = self.default + + if cmd in options: + # this will overload the other commands + # if it has the same name! + goto, callback = options[cmd] + if callback: + self.callback(callback, raw_string) + if goto: + self.goto(goto, raw_string) + elif cmd in ("look", "l"): + caller.msg(self.nodetext) + elif cmd in ("help", "h"): + caller.msg(self.helptext) + elif allow_quit and cmd in ("quit", "q", "exit"): + self.close_menu() + if cmd_on_quit is not None: + caller.execute_cmd(cmd_on_quit) + elif default: + goto, callback = default + if callback: + self.callback(callback, raw_string) + if goto: + self.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. + self.close_menu() + if cmd_on_quit is not None: + caller.execute_cmd(cmd_on_quit) + + def callback(self, nodename, raw_string): """ Run a node as a callback. This makes no use of the return From bec6cb438f5ab71cbdce9aa48df6bb8c215589ea Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Sun, 18 Oct 2015 00:13:47 +0000 Subject: [PATCH 19/24] Make cmd_on_quit support function callbacks as well. --- evennia/utils/evmenu.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index c3d7ea7fe..c9882e80d 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -264,11 +264,13 @@ class EvMenu(object): allow_quit (bool, optional): Allow user to use quit or exit to leave the menu at any point. Recommended during development! - cmd_on_quit (str or None, optional): When exiting the menu + cmd_on_quit (callback, str or None, optional): When exiting the menu (either by reaching a node with no options or by using the in-built quit command (activated with `allow_quit`), this - command string will be executed. Set to None to not call - any command. + callback function or command string will be executed. + 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. Raises: EvMenuError: If the start/end node is not found in menu tree. @@ -283,7 +285,12 @@ class EvMenu(object): # variables made available to the command self.allow_quit = allow_quit - self.cmd_on_quit = cmd_on_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 + else: + self.cmd_on_quit = None self.default = None self.nodetext = None self.helptext = None @@ -459,7 +466,6 @@ class EvMenu(object): cmd = raw_string.strip().lower() options = self.options allow_quit = self.allow_quit - cmd_on_quit = self.cmd_on_quit default = self.default if cmd in options: @@ -476,8 +482,6 @@ class EvMenu(object): caller.msg(self.helptext) elif allow_quit and cmd in ("quit", "q", "exit"): self.close_menu() - if cmd_on_quit is not None: - caller.execute_cmd(cmd_on_quit) elif default: goto, callback = default if callback: @@ -490,8 +494,6 @@ class EvMenu(object): if not (options or default): # no options - we are at the end of the menu. self.close_menu() - if cmd_on_quit is not None: - caller.execute_cmd(cmd_on_quit) def callback(self, nodename, raw_string): @@ -596,6 +598,8 @@ class EvMenu(object): """ self._caller.cmdset.remove(EvMenuCmdSet) del self._caller.ndb._menutree + if self.cmd_on_quit is not None: + self.cmd_on_quit(self._caller, self) # ------------------------------------------------------------------------------------------------- From 6619949381aa547cf83036b6910218a48ded8583 Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Sun, 18 Oct 2015 00:14:28 +0000 Subject: [PATCH 20/24] Add helper functions to EvMenu. --- evennia/utils/evmenu.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index c9882e80d..8ec319c39 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -460,6 +460,21 @@ class EvMenu(object): 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 handle(self, raw_string): # flags and data caller = self._caller @@ -472,22 +487,16 @@ class EvMenu(object): # this will overload the other commands # if it has the same name! goto, callback = options[cmd] - if callback: - self.callback(callback, raw_string) - if goto: - self.goto(goto, raw_string) + self._callback_goto(callback, goto, raw_string) elif cmd in ("look", "l"): - caller.msg(self.nodetext) + self._display_nodetext() elif cmd in ("help", "h"): - caller.msg(self.helptext) + self._display_helptext() elif allow_quit and cmd in ("quit", "q", "exit"): self.close_menu() elif default: goto, callback = default - if callback: - self.callback(callback, raw_string) - if goto: - self.goto(goto, raw_string) + self._callback_goto(callback, goto, raw_string) else: caller.msg(_HELP_NO_OPTION_MATCH) From a9f2e333944b7b509e270e54b6f2254ed25ca64f Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Sun, 18 Oct 2015 00:30:18 +0000 Subject: [PATCH 21/24] Change running 'look' to just displaying the node text. --- evennia/utils/evmenu.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 8ec319c39..cb9615d91 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -479,14 +479,12 @@ class EvMenu(object): # flags and data caller = self._caller cmd = raw_string.strip().lower() - options = self.options allow_quit = self.allow_quit - default = self.default - if cmd in options: + if cmd in self.options: # this will overload the other commands # if it has the same name! - goto, callback = options[cmd] + goto, callback = self.options[cmd] self._callback_goto(callback, goto, raw_string) elif cmd in ("look", "l"): self._display_nodetext() @@ -494,13 +492,13 @@ class EvMenu(object): self._display_helptext() elif allow_quit and cmd in ("quit", "q", "exit"): self.close_menu() - elif default: - goto, callback = default + elif self.default: + goto, callback = self.default self._callback_goto(callback, goto, raw_string) else: caller.msg(_HELP_NO_OPTION_MATCH) - if not (options or default): + if not (self.options or self.default): # no options - we are at the end of the menu. self.close_menu() @@ -599,7 +597,7 @@ class EvMenu(object): else: 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): """ From 609b1777c74f96b0e8bad5d7b6ab839ae87a8c95 Mon Sep 17 00:00:00 2001 From: Ahmed Charles Date: Sun, 18 Oct 2015 01:15:17 +0000 Subject: [PATCH 22/24] Extend EvMenu test case. There is now a custom look command, which was not possible with the previous implementation which used execute_cmd('look'), because that would infinitely recurse. --- evennia/utils/evmenu.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index cb9615d91..489f23b8a 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -706,6 +706,9 @@ def test_start_node(caller): "desc": "Set an attribute on yourself.", "exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"), "goto": "test_set_node"}, + {"key": ("{yL{nook", "l"), + "desc": "Look and see a custom message.", + "goto": "test_look_node"}, {"key": ("{yV{niew", "v"), "desc": "View your own name", "goto": "test_view_node"}, @@ -717,6 +720,13 @@ def test_start_node(caller): 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): text = (""" The attribute 'menuattrtest' was set to From 36337f9853ba99b4904f3efe5d96055634d3ee66 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 18 Oct 2015 11:06:19 +0200 Subject: [PATCH 23/24] Extended evmenu with optional callbacks for node formatting. Replaces and Resolves #826. --- evennia/utils/evmenu.py | 149 +++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 56 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 489f23b8a..af97973d3 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -234,7 +234,9 @@ class EvMenu(object): """ def __init__(self, caller, menudata, startnode="start", 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. @@ -264,13 +266,35 @@ class EvMenu(object): allow_quit (bool, optional): Allow user to use quit or exit to leave the menu at any point. Recommended during development! - cmd_on_quit (callback, 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 in-built quit command (activated with `allow_quit`), this callback function or command string will be executed. 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: EvMenuError: If the start/end node is not found in menu tree. @@ -280,6 +304,10 @@ class EvMenu(object): self._startnode = startnode 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: raise EvMenuError("Start node '%s' not in menu tree!" % startnode) @@ -296,6 +324,7 @@ class EvMenu(object): self.helptext = None self.options = None + # store ourself on the object self._caller.ndb._menutree = self @@ -354,74 +383,82 @@ class EvMenu(object): # handle the node text # - nodetext = dedent(nodetext).strip() - - 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 + if self._nodetext_formatter: + # use custom formatter + nodetext = self._nodetext_formatter(nodetext, len(optionlist)) + else: + nodetext = dedent(nodetext).strip() + nodetext_width_max = max(m_len(line) for line in nodetext.split("\n")) # # handle the options # - # column separation distance - colsep = 4 + if self._options_formatter: + # use custom formatter + optionstext = self._options_formatter(optionlist) + else: + # column separation distance + colsep = 4 - nlist = len(optionlist) + nlist = len(optionlist) - # get the widest option line in the table. - table_width_max = -1 - table = [] - for key, desc in optionlist: - table_width_max = max(table_width_max, - max(m_len(p) for p in key.split("\n")) + - max(m_len(p) for p in desc.split("\n")) + colsep) - raw_key = strip_ansi(key) - if raw_key != key: - # already decorations in key definition - table.append(ANSIString(" {lc%s{lt%s{le: %s" % (raw_key, key, desc))) - else: - # add a default white color to key - table.append(ANSIString(" {lc%s{lt{w%s{n{le: %s" % (raw_key, raw_key, desc))) + # get the widest option line in the table. + table_width_max = -1 + table = [] + for key, desc in optionlist: + table_width_max = max(table_width_max, + max(m_len(p) for p in key.split("\n")) + + max(m_len(p) for p in desc.split("\n")) + colsep) + raw_key = strip_ansi(key) + if raw_key != key: + # already decorations in key definition + table.append(ANSIString(" {lc%s{lt%s{le: %s" % (raw_key, key, desc))) + else: + # add a default white color to key + table.append(ANSIString(" {lc%s{lt{w%s{n{le: %s" % (raw_key, raw_key, desc))) - ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols - nlastcol = nlist % ncols # number of elements left in last row + ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols + nlastcol = nlist % ncols # number of elements left in last row - # get the amount of rows needed (start with 4 rows) - nrows = 4 - while nrows * ncols < nlist: - nrows += 1 - ncols = nlist // nrows # number of full columns - nlastcol = nlist % nrows # number of elements in last column + # get the amount of rows needed (start with 4 rows) + nrows = 4 + while nrows * ncols < nlist: + nrows += 1 + ncols = nlist // nrows # number of full columns + nlastcol = nlist % nrows # number of elements in last column - # get the final column count - ncols = ncols + 1 if nlastcol > 0 else ncols - if ncols > 1: - # only extend if longer than one column - table.extend([" " for i in xrange(nrows-nlastcol)]) + # get the final column count + ncols = ncols + 1 if nlastcol > 0 else ncols + if ncols > 1: + # only extend if longer than one column + table.extend([" " for i in xrange(nrows-nlastcol)]) - # build the actual table grid - table = [table[icol*nrows:(icol*nrows) + nrows] for icol in xrange(0, ncols)] + # build the actual table grid + table = [table[icol*nrows:(icol*nrows) + nrows] for icol in xrange(0, ncols)] - # adjust the width of each column - total_width = 0 - 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 - table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]] - total_width += col_width + # adjust the width of each column + options_total_width = 0 + 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 + table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]] + options_total_width += col_width - # format the table into columns - table = EvTable(table=table, border="none") + # format the table into columns + optionstext = unicode(EvTable(table=table, border="none")) - # build the page - total_width = max(total_width, nodetext_width_max) - separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" - separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" - return separator1 + nodetext + separator2 + unicode(table) + # + # format the entire node + # + if self._node_formatter: + # use custom formatter + return self._node_formatter(nodetext, optionstext) + else: + # build the page + total_width = max(options_total_width, nodetext_width_max) + separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" + separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" + return separator1 + nodetext + separator2 + optionstext def _execute_node(self, nodename, raw_string): """ From 940eb020640e1c1640f0eb8e33d30dab9d42393d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 18 Oct 2015 11:52:16 +0200 Subject: [PATCH 24/24] Resolved an outstanding bug in _format_display. --- evennia/utils/evmenu.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index af97973d3..071672b5f 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -199,7 +199,7 @@ class CmdEvMenuNode(Command): caller.msg(err) raise EvMenuError(err) - menu.handle(self.raw_string) + menu.parse_input(self.raw_string) class EvMenuCmdSet(CmdSet): @@ -388,7 +388,8 @@ class EvMenu(object): nodetext = self._nodetext_formatter(nodetext, len(optionlist)) else: 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")) # # handle the options @@ -397,7 +398,7 @@ class EvMenu(object): if self._options_formatter: # use custom formatter optionstext = self._options_formatter(optionlist) - else: + elif optionlist: # column separation distance colsep = 4 @@ -438,14 +439,16 @@ class EvMenu(object): table = [table[icol*nrows:(icol*nrows) + nrows] for icol in xrange(0, ncols)] # adjust the width of each column - options_total_width = 0 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 table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]] - options_total_width += col_width # format the table into columns 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 @@ -455,7 +458,7 @@ class EvMenu(object): return self._node_formatter(nodetext, optionstext) else: # build the page - total_width = max(options_total_width, nodetext_width_max) + total_width = max(options_width_max, nodetext_width_max) separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" return separator1 + nodetext + separator2 + optionstext @@ -512,15 +515,22 @@ class EvMenu(object): self.goto(goto, raw_string) - def handle(self, raw_string): - # flags and data + 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 overload the other commands - # if it has the same name! + # 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"):