From 280ffacc2d4b2eaa9834184fbe30024f76069d98 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 10 Aug 2017 23:36:56 +0200 Subject: [PATCH 001/217] First, non-working version of a get_by_tag manager method that accepts multiple tags --- evennia/typeclasses/managers.py | 80 ++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/evennia/typeclasses/managers.py b/evennia/typeclasses/managers.py index b6c486b166..7fff4f2a43 100644 --- a/evennia/typeclasses/managers.py +++ b/evennia/typeclasses/managers.py @@ -181,6 +181,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): # Tag manager methods + def get_tag(self, key=None, category=None, obj=None, tagtype=None, global_search=False): """ Return Tag objects by key, by category, by object (it is @@ -256,28 +257,83 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): """ return self.get_tag(key=key, category=category, obj=obj, tagtype="alias") +# @returns_typeclass_list +# def get_by_tag(self, key=None, category=None, tagtype=None): +# """ +# Return objects having tags with a given key or category or +# combination of the two. +# +# Args: +# key (str, optional): Tag key. Not case sensitive. +# category (str, optional): Tag category. Not case sensitive. +# tagtype (str or None, optional): 'type' of Tag, by default +# this is either `None` (a normal Tag), `alias` or +# `permission`. +# Returns: +# objects (list): Objects with matching tag. +# """ +# dbmodel = self.model.__dbclass__.__name__.lower() +# query = [("db_tags__db_tagtype", tagtype), ("db_tags__db_model", dbmodel)] +# if key: +# query.append(("db_tags__db_key", key.lower())) +# if category: +# query.append(("db_tags__db_category", category.lower())) +# return self.filter(**dict(query)) + @returns_typeclass_list def get_by_tag(self, key=None, category=None, tagtype=None): """ - Return objects having tags with a given key or category or - combination of the two. + Return objects having tags with a given key or category or combination of the two. + Also accepts multiple tags/category/tagtype Args: - key (str, optional): Tag key. Not case sensitive. - category (str, optional): Tag category. Not case sensitive. - tagtype (str or None, optional): 'type' of Tag, by default + key (str or list, optional): Tag key or list of keys. Not case sensitive. + category (str or list, optional): Tag category. Not case sensitive. If `key` is + a list, a single category can either apply to all keys in that list or this + must be a list matching the `key` list element by element. + tagtype (str, optional): 'type' of Tag, by default this is either `None` (a normal Tag), `alias` or - `permission`. + `permission`. This always apply to all queried tags. + Returns: objects (list): Objects with matching tag. + + Raises: + IndexError: If `key` and `category` are both lists and `category` is shorter + than `key`. + """ + keys = make_iter(key) + categories = make_iter(category) + n_keys = len(keys) + n_categories = len(categories) + dbmodel = self.model.__dbclass__.__name__.lower() - query = [("db_tags__db_tagtype", tagtype), ("db_tags__db_model", dbmodel)] - if key: - query.append(("db_tags__db_key", key.lower())) - if category: - query.append(("db_tags__db_category", category.lower())) - return self.filter(**dict(query)) + if n_keys > 1: + if n_categories == 1: + category = categories[0] + query = Q(db_tags__db_tagtype=tagtype.lower() if tagtype else tagtype, + db_tags__db_category=category.lower() if category else category, + db_tags__db_model=dbmodel) + for key in keys: + query = query & Q(db_tags__db_key=key.lower()) + print "Query:", query + else: + query = Q(db_tags__db_tagtype=tagtype.lower(), + db_tags__db_model=dbmodel) + for ikey, key in keys: + category = categories[ikey] + category = category.lower() if category else category + query = query & Q(db_tags__db_key=key.lower(), + db_tags__db_category=category) + return self.filter(query) + else: + query = [("db_tags__db_tagtype", tagtype), ("db_tags__db_model", dbmodel)] + if key: + query.append(("db_tags__db_key", keys[0].lower())) + if category: + query.append(("db_tags__db_category", categories[0].lower())) + return self.filter(**dict(query)) def get_by_permission(self, key=None, category=None): """ From 583ce9b71ec1d24e62fd4f2aec4d369f63873c35 Mon Sep 17 00:00:00 2001 From: Nicholas Matlaga Date: Wed, 13 Dec 2017 21:08:02 -0500 Subject: [PATCH 002/217] Step through help and option popup when closing --- evennia/web/webclient/static/webclient/js/webclient_gui.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index ba657858d9..57d9b0b7c0 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -175,8 +175,11 @@ function onKeydown (event) { } if (code === 27) { // Escape key - closePopup("#optionsdialog"); - closePopup("#helpdialog"); + if ($('#helpdialog').is(':visible')) { + closePopup("#helpdialog"); + } else { + closePopup("#optionsdialog"); + } } if (history_entry !== null) { From 56d9887825a1a113ea82e987679f3a4b05065698 Mon Sep 17 00:00:00 2001 From: Tehom Date: Sun, 17 Dec 2017 18:53:41 -0500 Subject: [PATCH 003/217] Allow other typeclasses to have their Attributes set via command. --- evennia/commands/default/building.py | 42 +++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index f4a001e6d2..1e544748ff 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1455,6 +1455,11 @@ class CmdSetAttribute(ObjManipCommand): Switch: edit: Open the line editor (string values only) + script: If we're trying to set an attribute on a script + channel: If we're trying to set an attribute on a channel + account: If we're trying to set an attribute on an account + (room, exit, char/character may all be used as well for global + searches) Sets attributes on objects. The second form clears a previously set attribute while the last form @@ -1555,6 +1560,38 @@ class CmdSetAttribute(ObjManipCommand): # start the editor EvEditor(self.caller, load, save, key="%s/%s" % (obj, attr)) + def search_for_obj(self, objname): + """ + Searches for an object matching objname. The object may be of different typeclasses. + Args: + objname: Name of the object we're looking for + + Returns: + A typeclassed object, or None if nothing is found. + """ + from evennia.utils.utils import variable_from_module + _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) + caller = self.caller + if objname.startswith('*') or "account" in self.switches: + found_obj = caller.search_account(objname.lstrip('*')) + elif "script" in self.switches: + found_obj = _AT_SEARCH_RESULT(search.search_script(objname), caller) + elif "channel" in self.switches: + found_obj = _AT_SEARCH_RESULT(search.search_channel(objname), caller) + else: + global_search = True + if "char" in self.switches or "character" in self.switches: + typeclass = settings.BASE_CHARACTER_TYPECLASS + elif "room" in self.switches: + typeclass = settings.BASE_ROOM_TYPECLASS + elif "exit" in self.switches: + typeclass = settings.BASE_EXIT_TYPECLASS + else: + global_search = False + typeclass = None + found_obj = caller.search(objname, global_search=global_search, typeclass=typeclass) + return found_obj + def func(self): """Implement the set attribute - a limited form of @py.""" @@ -1568,10 +1605,7 @@ class CmdSetAttribute(ObjManipCommand): objname = self.lhs_objattr[0]['name'] attrs = self.lhs_objattr[0]['attrs'] - if objname.startswith('*'): - obj = caller.search_account(objname.lstrip('*')) - else: - obj = caller.search(objname) + obj = self.search_for_obj(objname) if not obj: return From 08365a5ef5fe5af66cb7de1c6fea1079a4f539d2 Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 19 Dec 2017 00:28:48 -0500 Subject: [PATCH 004/217] Attempt to handle any errors in logging. --- evennia/utils/logger.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/evennia/utils/logger.py b/evennia/utils/logger.py index ce3bfe9a15..b248278ce1 100644 --- a/evennia/utils/logger.py +++ b/evennia/utils/logger.py @@ -59,6 +59,22 @@ def timeformat(when=None): tz_sign, tz_hour, tz_mins) +def log_msg(msg): + """ + Wrapper around log.msg call to catch any exceptions that might + occur in logging. If an exception is raised, we'll print to + stdout instead. + + Args: + msg: The message that was passed to log.msg + + """ + try: + log.msg(msg) + except Exception: + print("Exception raised while writing message to log. Original message: %s" % msg) + + def log_trace(errmsg=None): """ Log a traceback to the log. This should be called from within an @@ -80,9 +96,9 @@ def log_trace(errmsg=None): except Exception as e: errmsg = str(e) for line in errmsg.splitlines(): - log.msg('[EE] %s' % line) + log_msg('[EE] %s' % line) except Exception: - log.msg('[EE] %s' % errmsg) + log_msg('[EE] %s' % errmsg) log_tracemsg = log_trace @@ -101,7 +117,7 @@ def log_err(errmsg): except Exception as e: errmsg = str(e) for line in errmsg.splitlines(): - log.msg('[EE] %s' % line) + log_msg('[EE] %s' % line) # log.err('ERROR: %s' % (errmsg,)) @@ -121,7 +137,7 @@ def log_warn(warnmsg): except Exception as e: warnmsg = str(e) for line in warnmsg.splitlines(): - log.msg('[WW] %s' % line) + log_msg('[WW] %s' % line) # log.msg('WARNING: %s' % (warnmsg,)) @@ -139,7 +155,7 @@ def log_info(infomsg): except Exception as e: infomsg = str(e) for line in infomsg.splitlines(): - log.msg('[..] %s' % line) + log_msg('[..] %s' % line) log_infomsg = log_info @@ -157,7 +173,7 @@ def log_dep(depmsg): except Exception as e: depmsg = str(e) for line in depmsg.splitlines(): - log.msg('[DP] %s' % line) + log_msg('[DP] %s' % line) log_depmsg = log_dep From 33645df6dda6990f5bdd78c3ab7aea87a99ef5e7 Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 19 Dec 2017 00:42:23 -0500 Subject: [PATCH 005/217] Try to clarify help file. --- evennia/commands/default/building.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 1e544748ff..5076170957 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1458,8 +1458,10 @@ class CmdSetAttribute(ObjManipCommand): script: If we're trying to set an attribute on a script channel: If we're trying to set an attribute on a channel account: If we're trying to set an attribute on an account - (room, exit, char/character may all be used as well for global - searches) + room: Setting an attribute on a room (global search) + exit: Setting an attribute on an exit (global search) + char: Setting an attribute on a character (global search) + character: Alias for char, as above. Sets attributes on objects. The second form clears a previously set attribute while the last form From 181adeaa9074cf166bd4435d56086e578eec2179 Mon Sep 17 00:00:00 2001 From: Tehom Date: Mon, 25 Dec 2017 07:15:26 -0500 Subject: [PATCH 006/217] Fix errors in django admin for Attributes --- evennia/typeclasses/admin.py | 20 +++++++++++++++++--- evennia/utils/picklefield.py | 4 +++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/evennia/typeclasses/admin.py b/evennia/typeclasses/admin.py index c5dd481f90..c4047449f6 100644 --- a/evennia/typeclasses/admin.py +++ b/evennia/typeclasses/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from evennia.typeclasses.models import Tag from django import forms from evennia.utils.picklefield import PickledFormField -from evennia.utils.dbserialize import from_pickle +from evennia.utils.dbserialize import from_pickle, _SaverSet import traceback @@ -164,12 +164,12 @@ class AttributeForm(forms.ModelForm): attr_category = forms.CharField(label="Category", help_text="type of attribute, for sorting", required=False, - max_length=4) + max_length=128) attr_value = PickledFormField(label="Value", help_text="Value to pickle/save", required=False) attr_type = forms.CharField(label="Type", help_text="Internal use. Either unset (normal Attribute) or \"nick\"", required=False, - max_length=4) + max_length=16) attr_strvalue = forms.CharField(label="String Value", help_text="Only set when using the Attribute as a string-only store", required=False, @@ -213,6 +213,9 @@ class AttributeForm(forms.ModelForm): self.instance.attr_key = attr_key self.instance.attr_category = attr_category self.instance.attr_value = attr_value + # prevent set from being transformed to unicode + if isinstance(attr_value, set) or isinstance(attr_value, _SaverSet): + self.fields['attr_value'].disabled = True self.instance.deserialized_value = from_pickle(attr_value) self.instance.attr_strvalue = attr_strvalue self.instance.attr_type = attr_type @@ -237,6 +240,17 @@ class AttributeForm(forms.ModelForm): instance.attr_lockstring = self.cleaned_data['attr_lockstring'] return instance + def clean_attr_value(self): + """ + Prevent Sets from being cleaned due to literal_eval failing on them. Otherwise they will be turned into + unicode. + """ + data = self.cleaned_data['attr_value'] + initial = self.instance.attr_value + if isinstance(initial, set) or isinstance(initial, _SaverSet): + return initial + return data + class AttributeFormSet(forms.BaseInlineFormSet): """ diff --git a/evennia/utils/picklefield.py b/evennia/utils/picklefield.py index bd0a9b1643..9e683d96e6 100644 --- a/evennia/utils/picklefield.py +++ b/evennia/utils/picklefield.py @@ -120,9 +120,11 @@ def dbsafe_decode(value, compress_object=False): class PickledWidget(Textarea): def render(self, name, value, attrs=None): + """Display of the PickledField in django admin""" value = repr(value) try: - literal_eval(value) + # necessary to convert it back after repr(), otherwise validation errors will mutate it + value = literal_eval(value) except ValueError: return value From 7de45c429bdd297a91ec4a5239d9fbd30eb61b1d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 31 Dec 2017 13:40:16 +0100 Subject: [PATCH 007/217] Fix wrong call of _SEARCH_AT_RESULT from tutorial version of look, as reported in #1544. --- evennia/contrib/tutorial_world/rooms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/tutorial_world/rooms.py b/evennia/contrib/tutorial_world/rooms.py index c16baccff1..15bddefb01 100644 --- a/evennia/contrib/tutorial_world/rooms.py +++ b/evennia/contrib/tutorial_world/rooms.py @@ -168,7 +168,7 @@ class CmdTutorialLook(default_cmds.CmdLook): else: # no detail found, delegate our result to the normal # error message handler. - _SEARCH_AT_RESULT(None, caller, args, looking_at_obj) + _SEARCH_AT_RESULT(looking_at_obj, caller, args) return else: # we found a match, extract it from the list and carry on From 0133636b9588a4f631ac05372b2595669198c143 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 31 Dec 2017 13:47:19 +0100 Subject: [PATCH 008/217] Fix typos in doc strings --- evennia/contrib/extended_room.py | 2 +- evennia/contrib/rpsystem.py | 2 +- evennia/objects/objects.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/extended_room.py b/evennia/contrib/extended_room.py index 6823ede50e..0530bd796c 100644 --- a/evennia/contrib/extended_room.py +++ b/evennia/contrib/extended_room.py @@ -189,7 +189,7 @@ class ExtendedRoom(DefaultRoom): key (str): A detail identifier. Returns: - detail (str or None): A detail mathing the given key. + detail (str or None): A detail matching the given key. Notes: A detail is a way to offer more things to look at in a room diff --git a/evennia/contrib/rpsystem.py b/evennia/contrib/rpsystem.py index a396a18abc..d469aff3d9 100644 --- a/evennia/contrib/rpsystem.py +++ b/evennia/contrib/rpsystem.py @@ -1200,7 +1200,7 @@ class ContribRPObject(DefaultObject): below. exact (bool): if unset (default) - prefers to match to beginning of string rather than not matching at all. If set, requires - exact mathing of entire string. + exact matching of entire string. candidates (list of objects): this is an optional custom list of objects to search (filter) between. It is ignored if `global_search` is given. If not set, this list will automatically be defined diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index aca4ec0924..1509e7ce08 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -335,7 +335,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): below. exact (bool): if unset (default) - prefers to match to beginning of string rather than not matching at all. If set, requires - exact mathing of entire string. + exact matching of entire string. candidates (list of objects): this is an optional custom list of objects to search (filter) between. It is ignored if `global_search` is given. If not set, this list will automatically be defined From d9a609d735a4f3f162c6f568514decfc52836dd2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 31 Dec 2017 23:16:16 +0100 Subject: [PATCH 009/217] Allow options to partially update portal session, correctly relay late handshakes. --- evennia/server/inputfuncs.py | 13 ++++++++----- evennia/server/portal/telnet.py | 11 ++++++----- evennia/server/portal/ttype.py | 2 +- evennia/server/session.py | 8 +++++++- evennia/server/sessionhandler.py | 16 ++++++++++++++++ 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/evennia/server/inputfuncs.py b/evennia/server/inputfuncs.py index 3715bb53fe..28546c2064 100644 --- a/evennia/server/inputfuncs.py +++ b/evennia/server/inputfuncs.py @@ -160,10 +160,10 @@ def client_options(session, *args, **kwargs): raw (bool): Turn off parsing """ - flags = session.protocol_flags + old_flags = session.protocol_flags if not kwargs or kwargs.get("get", False): # return current settings - options = dict((key, flags[key]) for key in flags + options = dict((key, old_flags[key]) for key in old_flags if key.upper() in ("ANSI", "XTERM256", "MXP", "UTF-8", "SCREENREADER", "ENCODING", "MCCP", "SCREENHEIGHT", @@ -189,6 +189,7 @@ def client_options(session, *args, **kwargs): return True if val.lower() in ("true", "on", "1") else False return bool(val) + flags = {} for key, value in kwargs.iteritems(): key = key.lower() if key == "client": @@ -230,9 +231,11 @@ def client_options(session, *args, **kwargs): err = _ERROR_INPUT.format( name="client_settings", session=session, inp=key) session.msg(text=err) - session.protocol_flags = flags - # we must update the portal as well - session.sessionhandler.session_portal_sync(session) + + session.protocol_flags.update(flags) + # we must update the protocol flags on the portal session copy as well + session.sessionhandler.session_portal_partial_sync( + {session.sessid: {"protocol_flags": flags}}) # GMCP alias diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 4112d85e2a..afd0a9094f 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -72,7 +72,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # timeout the handshakes in case the client doesn't reply at all from evennia.utils.utils import delay - delay(2, callback=self.handshake_done, force=True) + delay(2, callback=self.handshake_done, timeout=True) # TCP/IP keepalive watches for dead links self.transport.setTcpKeepAlive(1) @@ -100,17 +100,18 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.nop_keep_alive = LoopingCall(self._send_nop_keepalive) self.nop_keep_alive.start(30, now=False) - def handshake_done(self, force=False): + def handshake_done(self, timeout=False): """ This is called by all telnet extensions once they are finished. When all have reported, a sync with the server is performed. The system will force-call this sync after a small time to handle clients that don't reply to handshakes at all. """ - if self.handshakes > 0: - if force: + if timeout: + if self.handshakes > 0: + self.handshakes = 0 self.sessionhandler.sync(self) - return + else: self.handshakes -= 1 if self.handshakes <= 0: # do the sync diff --git a/evennia/server/portal/ttype.py b/evennia/server/portal/ttype.py index 96ae1c1100..b9c3e8b239 100644 --- a/evennia/server/portal/ttype.py +++ b/evennia/server/portal/ttype.py @@ -66,7 +66,7 @@ class Ttype(object): option (Option): Not used. """ - self.protocol.protocol_flags['TTYPE'] = True + self.protocol.protocol_flags['TTYPE'] = False self.protocol.handshake_done() def will_ttype(self, option): diff --git a/evennia/server/session.py b/evennia/server/session.py index 70be0708d7..cf29430185 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -118,7 +118,13 @@ class Session(object): """ for propname, value in sessdata.items(): - setattr(self, propname, value) + if (propname == "prototocol_flags" and isinstance(value, dict) and + hasattr(self, "protocol_flags") and + isinstance(self.protocol_flags.propname, dict)): + # special handling to allow partial update of protocol flags + self.protocol_flags.update(value) + else: + setattr(self, propname, value) def at_sync(self): """ diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 825cf6da30..c8c0fafd64 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -538,6 +538,22 @@ class ServerSessionHandler(SessionHandler): sessiondata=sessdata, clean=False) + def session_portal_partial_sync(self, session_data): + """ + Call to make a partial update of the session, such as only a particular property. + + Args: + session_data (dict): Store `{sessid: {property:value}, ...}` defining one or + more sessions in detail. + + """ + return self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, + operation=SSYNC, + sessiondata=session_data, + clean=False) + + + def disconnect_all_sessions(self, reason="You have been disconnected."): """ Cleanly disconnect all of the connected sessions. From 8eb384cfdb11fd3c82a9e3321a76a27bd717c117 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 31 Dec 2017 23:36:58 +0100 Subject: [PATCH 010/217] bugfix of protocol_flag update mechanism --- evennia/server/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/server/session.py b/evennia/server/session.py index cf29430185..9853bcd366 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -118,9 +118,9 @@ class Session(object): """ for propname, value in sessdata.items(): - if (propname == "prototocol_flags" and isinstance(value, dict) and + if (propname == "protocol_flags" and isinstance(value, dict) and hasattr(self, "protocol_flags") and - isinstance(self.protocol_flags.propname, dict)): + isinstance(self.protocol_flags, dict)): # special handling to allow partial update of protocol flags self.protocol_flags.update(value) else: From 7ac3b2d228b88390f898c46e085a9146e7419dc8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Jan 2018 13:19:59 +0100 Subject: [PATCH 011/217] Fix issue with messaging at session-level --- evennia/server/serversession.py | 1 + 1 file changed, 1 insertion(+) diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index aaaeaaaedd..2a427d591f 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -400,6 +400,7 @@ class ServerSession(Session): # this can happen if this is triggered e.g. a command.msg # that auto-adds the session, we'd get a kwarg collision. kwargs.pop("session", None) + kwargs.pop("from_obj", None) if text is not None: self.data_out(text=text, **kwargs) else: From be02d058e10d4da7c7f45fcf9cab2111898a8183 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Jan 2018 15:27:21 +0100 Subject: [PATCH 012/217] Minor refactoring and stabilizing --- evennia/server/portal/telnet.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index afd0a9094f..bdb937bde5 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -50,6 +50,9 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # when it reaches 0 the portal/server syncs their data self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp self.init_session(self.protocol_name, client_address, self.factory.sessionhandler) + # add this new connection to sessionhandler so + # the Server becomes aware of it. + self.sessionhandler.connect(self) # suppress go-ahead self.sga = suppress_ga.SuppressGA(self) @@ -66,12 +69,9 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.oob = telnet_oob.TelnetOOB(self) # mxp support self.mxp = Mxp(self) - # add this new connection to sessionhandler so - # the Server becomes aware of it. - self.sessionhandler.connect(self) - # timeout the handshakes in case the client doesn't reply at all from evennia.utils.utils import delay + # timeout the handshakes in case the client doesn't reply at all delay(2, callback=self.handshake_done, timeout=True) # TCP/IP keepalive watches for dead links @@ -306,8 +306,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # handle arguments options = kwargs.get("options", {}) flags = self.protocol_flags - xterm256 = options.get("xterm256", flags.get('XTERM256', False) if flags["TTYPE"] else True) - useansi = options.get("ansi", flags.get('ANSI', False) if flags["TTYPE"] else True) + xterm256 = options.get("xterm256", flags.get('XTERM256', False) if flags.get("TTYPE", False) else True) + useansi = options.get("ansi", flags.get('ANSI', False) if flags.get("TTYPE", False) else True) raw = options.get("raw", flags.get("RAW", False)) nocolor = options.get("nocolor", flags.get("NOCOLOR") or not (xterm256 or useansi)) echo = options.get("echo", None) From 3235d6dcc75525dd9217fc4df87f196a825eb24a Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Jan 2018 20:58:48 +0100 Subject: [PATCH 013/217] Add a slight delay to telnet handshake to give mudlet a chance to catch up --- evennia/commands/default/tests.py | 8 +++++--- evennia/server/sessionhandler.py | 8 +++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index ffb63ef723..65fb0fa639 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -16,7 +16,7 @@ import re import types from django.conf import settings -from mock import Mock +from mock import Mock, mock from evennia.commands.default.cmdset_character import CharacterCmdSet from evennia.utils.test_resources import EvenniaTest @@ -37,12 +37,14 @@ _RE = re.compile(r"^\+|-+\+|\+-+|--*|\|(?:\s|$)", re.MULTILINE) # Command testing # ------------------------------------------------------------ + +@mock.patch("evennia.utils.utils.delay") class CommandTest(EvenniaTest): """ Tests a command """ - - def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None, receiver=None, cmdstring=None, obj=None): + def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None, + receiver=None, cmdstring=None, obj=None): """ Test a command by assigning all the needed properties to cmdobj and running diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index c8c0fafd64..5fb15930c9 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -310,7 +310,13 @@ class ServerSessionHandler(SessionHandler): sess.uid = None # show the first login command - self.data_in(sess, text=[[CMD_LOGINSTART], {}]) + + # this delay is necessary notably for Mudlet, which will fail on the connection screen + # unless the MXP protocol has been negotiated. Unfortunately this may be too short for some + # networks, the symptom is that < and > are not parsed by mudlet on first connection. + from evennia.utils.utils import delay + delay(0.3, self.data_in, sess, text=[[CMD_LOGINSTART], {}]) + # self.data_in(sess, text=[[CMD_LOGINSTART], {}]) def portal_session_sync(self, portalsessiondata): """ From 980d94a4c729872546176f73a44c44faf630746d Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Jan 2018 21:03:11 +0100 Subject: [PATCH 014/217] Add TIME_IGNORE_DOWNTIMES to allow for true 1:1 time ratios. Implements #1545 --- evennia/settings_default.py | 7 ++++++- evennia/utils/gametime.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index b1e2a7a79e..7948868ed9 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -490,8 +490,13 @@ TIME_FACTOR = 2.0 # The starting point of your game time (the epoch), in seconds. # In Python a value of 0 means Jan 1 1970 (use negatives for earlier # start date). This will affect the returns from the utils.gametime -# module. +# module. If None, the server's first start-time is used as the epoch. TIME_GAME_EPOCH = None +# Normally, game time will only increase when the server runs. If this is True, +# game time will not pause when the server reloads or goes offline. This setting +# together with a time factor of 1 should keep the game in sync with +# the real time (add a different epoch to shift time) +TIME_IGNORE_DOWNTIMES = False ###################################################################### # Inlinefunc diff --git a/evennia/utils/gametime.py b/evennia/utils/gametime.py index 793e348143..3736128819 100644 --- a/evennia/utils/gametime.py +++ b/evennia/utils/gametime.py @@ -19,6 +19,8 @@ from evennia.utils.create import create_script # to real time. TIMEFACTOR = settings.TIME_FACTOR +IGNORE_DOWNTIMES = settings.TIME_IGNORE_DOWNTIMES + # Only set if gametime_reset was called at some point. GAME_TIME_OFFSET = ServerConfig.objects.conf("gametime_offset", default=0) @@ -133,7 +135,10 @@ def gametime(absolute=False): """ epoch = game_epoch() if absolute else 0 - gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR + if IGNORE_DOWNTIMES: + gtime = epoch + (time.time() - server_epoch()) * TIMEFACTOR + else: + gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR return gtime From 19a7637cfc5e0ff0bcc4527fdfad95c2b56f0904 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 2 Jan 2018 21:21:57 +0100 Subject: [PATCH 015/217] Work towards resolving unittests with deferreds --- evennia/commands/default/tests.py | 1 - evennia/commands/tests.py | 7 +++++ evennia/contrib/tests.py | 36 +++++++++++++++++------ evennia/contrib/tutorial_world/objects.py | 7 ++--- evennia/scripts/taskhandler.py | 7 +++-- evennia/server/sessionhandler.py | 3 +- evennia/utils/utils.py | 2 +- 7 files changed, 43 insertions(+), 20 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 65fb0fa639..f437779662 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -38,7 +38,6 @@ _RE = re.compile(r"^\+|-+\+|\+-+|--*|\|(?:\s|$)", re.MULTILINE) # ------------------------------------------------------------ -@mock.patch("evennia.utils.utils.delay") class CommandTest(EvenniaTest): """ Tests a command diff --git a/evennia/commands/tests.py b/evennia/commands/tests.py index 0e465e377b..ef8f9d24a5 100644 --- a/evennia/commands/tests.py +++ b/evennia/commands/tests.py @@ -264,14 +264,20 @@ class TestCmdSetMergers(TestCase): # test cmdhandler functions +import sys from evennia.commands import cmdhandler from twisted.trial.unittest import TestCase as TwistedTestCase +def _mockdelay(time, func, *args, **kwargs): + return func(*args, **kwargs) + + class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest): "Test the cmdhandler.get_and_merge_cmdsets function." def setUp(self): + self.patch(sys.modules['evennia.server.sessionhandler'], 'delay', _mockdelay) super(TestGetAndMergeCmdSets, self).setUp() self.cmdset_a = _CmdSetA() self.cmdset_b = _CmdSetB() @@ -325,6 +331,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest): a.no_exits = True a.no_channels = True self.set_cmdsets(self.obj1, a, b, c, d) + deferred = cmdhandler.get_and_merge_cmdsets(self.obj1, None, None, self.obj1, "object", "") def _callback(cmdset): diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 23e4662ec3..ff6b01d5cf 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -815,9 +815,30 @@ class TestTutorialWorldMob(EvenniaTest): from evennia.contrib.tutorial_world import objects as tutobjects +from mock.mock import MagicMock +from twisted.trial.unittest import TestCase as TwistedTestCase + +from twisted.internet.base import DelayedCall +DelayedCall.debug = True -class TestTutorialWorldObjects(CommandTest): +def _mockdelay(tim, func, *args, **kwargs): + func(*args, **kwargs) + return MagicMock() + + +def _mockdeferLater(reactor, timedelay, callback, *args, **kwargs): + callback(*args, **kwargs) + return MagicMock() + + +class TestTutorialWorldObjects(TwistedTestCase, CommandTest): + + def setUp(self): + self.patch(sys.modules['evennia.contrib.tutorial_world.objects'], 'delay', _mockdelay) + self.patch(sys.modules['evennia.scripts.taskhandler'], 'deferLater', _mockdeferLater) + super(TestTutorialWorldObjects, self).setUp() + def test_tutorialobj(self): obj1 = create_object(tutobjects.TutorialObject, key="tutobj") obj1.reset() @@ -839,10 +860,7 @@ class TestTutorialWorldObjects(CommandTest): def test_lightsource(self): light = create_object(tutobjects.LightSource, key="torch", location=self.room1) - self.call(tutobjects.CmdLight(), "", "You light torch.", obj=light) - light._burnout() - if hasattr(light, "deferred"): - light.deferred.cancel() + self.call(tutobjects.CmdLight(), "", "A torch on the floor flickers and dies.|You light torch.", obj=light) self.assertFalse(light.pk) def test_crumblingwall(self): @@ -860,12 +878,12 @@ class TestTutorialWorldObjects(CommandTest): "You shift the weedy green root upwards.|Holding aside the root you think you notice something behind it ...", obj=wall) self.call(tutobjects.CmdPressButton(), "", "You move your fingers over the suspicious depression, then gives it a decisive push. First", obj=wall) - self.assertTrue(wall.db.button_exposed) - self.assertTrue(wall.db.exit_open) + # we patch out the delay, so these are closed immediately + self.assertFalse(wall.db.button_exposed) + self.assertFalse(wall.db.exit_open) wall.reset() - if hasattr(wall, "deferred"): - wall.deferred.cancel() wall.delete() + return wall.deferred def test_weapon(self): weapon = create_object(tutobjects.Weapon, key="sword", location=self.char1) diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index 3859de7d2d..b260770577 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -23,8 +23,7 @@ from future.utils import listvalues import random from evennia import DefaultObject, DefaultExit, Command, CmdSet -from evennia import utils -from evennia.utils import search +from evennia.utils import search, delay from evennia.utils.spawner import spawn # ------------------------------------------------------------- @@ -373,7 +372,7 @@ class LightSource(TutorialObject): # start the burn timer. When it runs out, self._burnout # will be called. We store the deferred so it can be # killed in unittesting. - self.deferred = utils.delay(60 * 3, self._burnout) + self.deferred = delay(60 * 3, self._burnout) return True @@ -645,7 +644,7 @@ class CrumblingWall(TutorialObject, DefaultExit): self.db.exit_open = True # start a 45 second timer before closing again. We store the deferred so it can be # killed in unittesting. - self.deferred = utils.delay(45, self.reset) + self.deferred = delay(45, self.reset) def _translate_position(self, root, ipos): """Translates the position into words""" diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index f4819ba076..a61dd6fb4a 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -4,7 +4,8 @@ Module containing the task handler for Evennia deferred tasks, persistent or not from datetime import datetime, timedelta -from twisted.internet import reactor, task +from twisted.internet import reactor +from twisted.internet.task import deferLater from evennia.server.models import ServerConfig from evennia.utils.logger import log_err from evennia.utils.dbserialize import dbserialize, dbunserialize @@ -143,7 +144,7 @@ class TaskHandler(object): args = [task_id] kwargs = {} - return task.deferLater(reactor, timedelay, callback, *args, **kwargs) + return deferLater(reactor, timedelay, callback, *args, **kwargs) def remove(self, task_id): """Remove a persistent task without executing it. @@ -189,7 +190,7 @@ class TaskHandler(object): now = datetime.now() for task_id, (date, callbac, args, kwargs) in self.tasks.items(): seconds = max(0, (date - now).total_seconds()) - task.deferLater(reactor, seconds, self.do_task, task_id) + deferLater(reactor, seconds, self.do_task, task_id) # Create the soft singleton diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 5fb15930c9..39637bb00b 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -21,7 +21,7 @@ from evennia.commands.cmdhandler import CMD_LOGINSTART from evennia.utils.logger import log_trace from evennia.utils.utils import (variable_from_module, is_iter, to_str, to_unicode, - make_iter, + make_iter, delay, callables_from_module) from evennia.utils.inlinefuncs import parse_inlinefunc @@ -314,7 +314,6 @@ class ServerSessionHandler(SessionHandler): # this delay is necessary notably for Mudlet, which will fail on the connection screen # unless the MXP protocol has been negotiated. Unfortunately this may be too short for some # networks, the symptom is that < and > are not parsed by mudlet on first connection. - from evennia.utils.utils import delay delay(0.3, self.data_in, sess, text=[[CMD_LOGINSTART], {}]) # self.data_in(sess, text=[[CMD_LOGINSTART], {}]) diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 1a159991ba..550b11eac3 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -948,7 +948,7 @@ def delay(timedelay, callback, *args, **kwargs): specified here. Note: - The task handler (`evennia.scripts.taskhandler.TASK_HANDLEr`) will + The task handler (`evennia.scripts.taskhandler.TASK_HANDLER`) will be called for persistent or non-persistent tasks. If persistent is set to True, the callback, its arguments and other keyword arguments will be saved in the database, From d038cf0f634344e90fa884219b154c2b75482246 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 3 Jan 2018 22:37:52 +0100 Subject: [PATCH 016/217] Don't send delayed CMD_LOGINSTART if session already logged in (to handle autologins) --- evennia/server/sessionhandler.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 39637bb00b..49d9b46498 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -274,6 +274,16 @@ class ServerSessionHandler(SessionHandler): self.server = None self.server_data = {"servername": _SERVERNAME} + def _run_cmd_login(self, session): + """ + Launch the CMD_LOGINSTART command. This is wrapped + for delays. + + """ + if not session.logged_in: + self.data_in(session, text=[[CMD_LOGINSTART], {}]) + + def portal_connect(self, portalsessiondata): """ Called by Portal when a new session has connected. @@ -314,8 +324,7 @@ class ServerSessionHandler(SessionHandler): # this delay is necessary notably for Mudlet, which will fail on the connection screen # unless the MXP protocol has been negotiated. Unfortunately this may be too short for some # networks, the symptom is that < and > are not parsed by mudlet on first connection. - delay(0.3, self.data_in, sess, text=[[CMD_LOGINSTART], {}]) - # self.data_in(sess, text=[[CMD_LOGINSTART], {}]) + delay(0.3, self._run_cmd_login, sess) def portal_session_sync(self, portalsessiondata): """ From 55a29f00a12c50311dd7b277795ec7cfa606f443 Mon Sep 17 00:00:00 2001 From: sorressean Date: Fri, 5 Jan 2018 02:33:29 -0500 Subject: [PATCH 017/217] Fixed typo in help message that shows syntax. --- evennia/commands/default/building.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index f4a001e6d2..9f3e2303a9 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2686,7 +2686,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): def _show_prototypes(prototypes): """Helper to show a list of available prototypes""" prots = ", ".join(sorted(prototypes.keys())) - return "\nAvailable prototypes (case sensistive): %s" % ( + return "\nAvailable prototypes (case sensative): %s" % ( "\n" + utils.fill(prots) if prots else "None") prototypes = spawn(return_prototypes=True) From 78bdffa076d4802a3bf7749291d9f610bcf99a23 Mon Sep 17 00:00:00 2001 From: sorressean Date: Fri, 5 Jan 2018 02:38:35 -0500 Subject: [PATCH 018/217] spelled correctly this time. --- evennia/commands/default/building.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 9f3e2303a9..78aa7d7553 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2686,7 +2686,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): def _show_prototypes(prototypes): """Helper to show a list of available prototypes""" prots = ", ".join(sorted(prototypes.keys())) - return "\nAvailable prototypes (case sensative): %s" % ( + return "\nAvailable prototypes (case sensitive): %s" % ( "\n" + utils.fill(prots) if prots else "None") prototypes = spawn(return_prototypes=True) From 3c2b359f559efdb5a11abeb21763a42ded28c553 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Sat, 6 Jan 2018 13:32:30 +0100 Subject: [PATCH 019/217] [IGPS] Fix mistakes in the say event --- evennia/contrib/ingame_python/typeclasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/ingame_python/typeclasses.py b/evennia/contrib/ingame_python/typeclasses.py index 33729bef66..3f52b982bf 100644 --- a/evennia/contrib/ingame_python/typeclasses.py +++ b/evennia/contrib/ingame_python/typeclasses.py @@ -430,7 +430,7 @@ class EventCharacter(DefaultCharacter): # Browse all the room's other characters for obj in location.contents: - if obj is self or not inherits_from(obj, "objects.objects.DefaultCharacter"): + if obj is self or not inherits_from(obj, "evennia.objects.objects.DefaultCharacter"): continue allow = obj.callbacks.call("can_say", self, obj, message, parameters=message) @@ -491,7 +491,7 @@ class EventCharacter(DefaultCharacter): parameters=message) # Call the other characters' "say" event - presents = [obj for obj in location.contents if obj is not self and inherits_from(obj, "objects.objects.DefaultCharacter")] + presents = [obj for obj in location.contents if obj is not self and inherits_from(obj, "evennia.objects.objects.DefaultCharacter")] for present in presents: present.callbacks.call("say", self, present, message, parameters=message) From 0bbaf9ea1788106fb5268db10c2d8b2690adf18d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Jan 2018 19:19:20 +0100 Subject: [PATCH 020/217] Make DELAY_CMD_LOGINSTART configurable in settings --- evennia/server/sessionhandler.py | 10 ++++------ evennia/settings_default.py | 8 ++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 49d9b46498..423bc87f1c 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -65,6 +65,7 @@ from django.utils.translation import ugettext as _ _SERVERNAME = settings.SERVERNAME _MULTISESSION_MODE = settings.MULTISESSION_MODE _IDLE_TIMEOUT = settings.IDLE_TIMEOUT +_DELAY_CMD_LOGINSTART = settings.DELAY_CMD_LOGINSTART _MAX_SERVER_COMMANDS_PER_SECOND = 100.0 _MAX_SESSION_COMMANDS_PER_SECOND = 5.0 _MODEL_MAP = None @@ -319,12 +320,9 @@ class ServerSessionHandler(SessionHandler): sess.logged_in = False sess.uid = None - # show the first login command - - # this delay is necessary notably for Mudlet, which will fail on the connection screen - # unless the MXP protocol has been negotiated. Unfortunately this may be too short for some - # networks, the symptom is that < and > are not parsed by mudlet on first connection. - delay(0.3, self._run_cmd_login, sess) + # show the first login command, may delay slightly to allow + # the handshakes to finish. + delay(_DELAY_CMD_LOGINSTART, self._run_cmd_login, sess) def portal_session_sync(self, portalsessiondata): """ diff --git a/evennia/settings_default.py b/evennia/settings_default.py index b1e2a7a79e..7ae45c4be2 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -307,6 +307,14 @@ CMD_IGNORE_PREFIXES = "@&/+" # This module should contain one or more variables # with strings defining the look of the screen. CONNECTION_SCREEN_MODULE = "server.conf.connection_screens" +# Delay to use before sending the evennia.syscmdkeys.CMD_LOGINSTART Command +# when a new session connects (this defaults the unloggedin-look for showing +# the connection screen). The delay is useful mainly for telnet, to allow +# client/server to establish client capabilities like color/mxp etc before +# sending any text. A value of 0.3 should be enough. While a good idea, it may +# cause issues with menu-logins and autoconnects since the menu will not have +# started when the autoconnects starts sending menu commands. +DELAY_CMD_LOGINSTART = 0.3 # An optional module that, if existing, must hold a function # named at_initial_setup(). This hook method can be used to customize # the server's initial setup sequence (the very first startup of the system). From 5fabb57c26910d8af55e7d618d6d8c1551196f81 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Jan 2018 19:22:34 +0100 Subject: [PATCH 021/217] Edit version info --- evennia/VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/VERSION.txt b/evennia/VERSION.txt index faef31a435..f8d71478f5 100644 --- a/evennia/VERSION.txt +++ b/evennia/VERSION.txt @@ -1 +1 @@ -0.7.0 +0.8.0-dev From 9ae8206d8af5003408aa84d4ccb36257ea675029 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Jan 2018 19:19:20 +0100 Subject: [PATCH 022/217] Make DELAY_CMD_LOGINSTART configurable in settings --- evennia/server/sessionhandler.py | 10 ++++------ evennia/settings_default.py | 8 ++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 49d9b46498..423bc87f1c 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -65,6 +65,7 @@ from django.utils.translation import ugettext as _ _SERVERNAME = settings.SERVERNAME _MULTISESSION_MODE = settings.MULTISESSION_MODE _IDLE_TIMEOUT = settings.IDLE_TIMEOUT +_DELAY_CMD_LOGINSTART = settings.DELAY_CMD_LOGINSTART _MAX_SERVER_COMMANDS_PER_SECOND = 100.0 _MAX_SESSION_COMMANDS_PER_SECOND = 5.0 _MODEL_MAP = None @@ -319,12 +320,9 @@ class ServerSessionHandler(SessionHandler): sess.logged_in = False sess.uid = None - # show the first login command - - # this delay is necessary notably for Mudlet, which will fail on the connection screen - # unless the MXP protocol has been negotiated. Unfortunately this may be too short for some - # networks, the symptom is that < and > are not parsed by mudlet on first connection. - delay(0.3, self._run_cmd_login, sess) + # show the first login command, may delay slightly to allow + # the handshakes to finish. + delay(_DELAY_CMD_LOGINSTART, self._run_cmd_login, sess) def portal_session_sync(self, portalsessiondata): """ diff --git a/evennia/settings_default.py b/evennia/settings_default.py index b1e2a7a79e..7ae45c4be2 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -307,6 +307,14 @@ CMD_IGNORE_PREFIXES = "@&/+" # This module should contain one or more variables # with strings defining the look of the screen. CONNECTION_SCREEN_MODULE = "server.conf.connection_screens" +# Delay to use before sending the evennia.syscmdkeys.CMD_LOGINSTART Command +# when a new session connects (this defaults the unloggedin-look for showing +# the connection screen). The delay is useful mainly for telnet, to allow +# client/server to establish client capabilities like color/mxp etc before +# sending any text. A value of 0.3 should be enough. While a good idea, it may +# cause issues with menu-logins and autoconnects since the menu will not have +# started when the autoconnects starts sending menu commands. +DELAY_CMD_LOGINSTART = 0.3 # An optional module that, if existing, must hold a function # named at_initial_setup(). This hook method can be used to customize # the server's initial setup sequence (the very first startup of the system). From 1c6b74dc89f276194752bb5ae7253c0bd11fb7ae Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Jan 2018 20:12:51 +0100 Subject: [PATCH 023/217] Update/refactor search_channel with aliases and proper query. Resolves #1534. --- evennia/comms/managers.py | 53 +++++++++++++++++--------------------- evennia/objects/manager.py | 10 ++++--- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/evennia/comms/managers.py b/evennia/comms/managers.py index f50d813123..dc541528c7 100644 --- a/evennia/comms/managers.py +++ b/evennia/comms/managers.py @@ -355,15 +355,16 @@ class ChannelDBManager(TypedObjectManager): channel (Channel or None): A channel match. """ - # first check the channel key - channels = self.filter(db_key__iexact=channelkey) - if not channels: - # also check aliases - channels = [channel for channel in self.all() - if channelkey in channel.aliases.all()] - if channels: - return channels[0] - return None + dbref = self.dbref(channelkey) + if dbref: + try: + return self.get(id=dbref) + except self.model.DoesNotExist: + pass + results = self.filter(Q(db_key__iexact=channelkey) | + Q(db_tags__db_tagtype__iexact="alias", + db_tags__db_key__iexact=channelkey)).distinct() + return results[0] if results else None def get_subscriptions(self, subscriber): """ @@ -393,26 +394,20 @@ class ChannelDBManager(TypedObjectManager): case sensitive) match. """ - channels = [] - if not ostring: - return channels - try: - # try an id match first - dbref = int(ostring.strip('#')) - channels = self.filter(id=dbref) - except Exception: - # Usually because we couldn't convert to int - not a dbref - pass - if not channels: - # no id match. Search on the key. - if exact: - channels = self.filter(db_key__iexact=ostring) - else: - channels = self.filter(db_key__icontains=ostring) - if not channels: - # still no match. Search by alias. - channels = [channel for channel in self.all() - if ostring.lower() in [a.lower for a in channel.aliases.all()]] + dbref = self.dbref(ostring) + if dbref: + try: + return self.get(id=dbref) + except self.model.DoesNotExist: + pass + if exact: + channels = self.filter(Q(db_key__iexact=ostring) | + Q(db_tags__db_tagtype__iexact="alias", + db_tags__db_key__iexact=ostring)).distinct() + else: + channels = self.filter(Q(db_key__icontains=ostring) | + Q(db_tags__db_tagtype__iexact="alias", + db_tags__db_key__icontains=ostring)).distinct() return channels # back-compatibility alias channel_search = search_channel diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index 3d29768e5a..4c81ea186a 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -76,10 +76,14 @@ class ObjectDBManager(TypedObjectManager): # simplest case - search by dbref dbref = self.dbref(ostring) if dbref: - return dbref + try: + return self.get(id=dbref) + except self.model.DoesNotExist: + pass + # not a dbref. Search by name. - cand_restriction = candidates is not None and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) - if obj]) or Q() + cand_restriction = candidates is not None and Q( + pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q() if exact: return self.filter(cand_restriction & Q(db_account__username__iexact=ostring)) else: # fuzzy matching From 1835f75107fe64e49e06d46abd4712313656b812 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Jan 2018 16:20:56 +0100 Subject: [PATCH 024/217] Make alias command handle categories, remove 'alias' alias from nick cmd --- evennia/commands/default/building.py | 36 +++++++++++++++++++++------- evennia/commands/default/general.py | 2 +- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 10bfd0f099..445ec082a9 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -106,9 +106,15 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): Usage: @alias [= [alias[,alias,alias,...]]] @alias = + @alias/category = [alias[,alias,...]: + + Switches: + category - requires ending input with :category, to store the + given aliases with the given category. Assigns aliases to an object so it can be referenced by more - than one name. Assign empty to remove all aliases from object. + than one name. Assign empty to remove all aliases from object. If + assigning a category, all aliases given will be using this category. Observe that this is not the same thing as personal aliases created with the 'nick' command! Aliases set with @alias are @@ -138,9 +144,12 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): return if self.rhs is None: # no =, so we just list aliases on object. - aliases = obj.aliases.all() + aliases = obj.aliases.all(return_key_and_category=True) if aliases: - caller.msg("Aliases for '%s': %s" % (obj.get_display_name(caller), ", ".join(aliases))) + caller.msg("Aliases for %s: %s" % ( + obj.get_display_name(caller), + ", ".join("'%s'%s" % (alias, "" if category is None else "[category:'%s']" % category) + for (alias, category) in aliases))) else: caller.msg("No aliases exist for '%s'." % obj.get_display_name(caller)) return @@ -159,17 +168,27 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): caller.msg("No aliases to clear.") return + category = None + if "category" in self.switches: + if ":" in self.rhs: + rhs, category = self.rhs.rsplit(':', 1) + category = category.strip() + else: + caller.msg("If specifying the /category switch, the category must be given " + "as :category at the end.") + else: + rhs = self.rhs + # merge the old and new aliases (if any) - old_aliases = obj.aliases.all() - new_aliases = [alias.strip().lower() for alias in self.rhs.split(',') - if alias.strip()] + old_aliases = obj.aliases.get(category=category, return_list=True) + new_aliases = [alias.strip().lower() for alias in rhs.split(',') if alias.strip()] # make the aliases only appear once old_aliases.extend(new_aliases) aliases = list(set(old_aliases)) # save back to object. - obj.aliases.add(aliases) + obj.aliases.add(aliases, category=category) # we need to trigger this here, since this will force # (default) Exits to rebuild their Exit commands with the new @@ -177,7 +196,8 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): obj.at_cmdset_get(force_init=True) # report all aliases on the object - caller.msg("Alias(es) for '%s' set to %s." % (obj.get_display_name(caller), str(obj.aliases))) + caller.msg("Alias(es) for '%s' set to '%s'%s." % (obj.get_display_name(caller), + str(obj.aliases), " (category: '%s')" % category if category else "")) class CmdCopy(ObjManipCommand): diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 2f880f3f7f..612a798167 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -113,7 +113,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS): """ key = "nick" - aliases = ["nickname", "nicks", "alias"] + aliases = ["nickname", "nicks"] locks = "cmd:all()" def func(self): From 0b7c937903966258a46e95ff13fe5311fb299d2a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Jan 2018 16:21:59 +0100 Subject: [PATCH 025/217] Clarify taghandler.get docstring --- evennia/typeclasses/tags.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/evennia/typeclasses/tags.py b/evennia/typeclasses/tags.py index e7c3bfdbb1..488dce0f85 100644 --- a/evennia/typeclasses/tags.py +++ b/evennia/typeclasses/tags.py @@ -269,14 +269,15 @@ class TagHandler(object): def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False): """ - Get the tag for the given key or list of tags. + Get the tag for the given key, category or combination of the two. Args: - key (str or list): The tag or tags to retrieve. + key (str or list, optional): The tag or tags to retrieve. default (any, optional): The value to return in case of no match. category (str, optional): The Tag category to limit the request to. Note that `None` is the valid, default - category. + category. If no `key` is given, all tags of this category will be + returned. return_tagobj (bool, optional): Return the Tag object itself instead of a string representation of the Tag. return_list (bool, optional): Always return a list, regardless From dfd2d3897ab5f8118732907835be389bfd2da0cb Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Jan 2018 19:31:01 +0100 Subject: [PATCH 026/217] Add inflect dependency. Add get_plural_name and mechanisms discussed in #1385. --- evennia/objects/objects.py | 45 +++++++++++++++++++++++++++++++++----- requirements.txt | 1 + win_requirements.txt | 1 + 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index af7f520817..7c230a8c47 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -6,8 +6,10 @@ entities. """ import time +import inflect from builtins import object from future.utils import with_metaclass +from collections import Counter from django.conf import settings @@ -22,9 +24,10 @@ from evennia.commands import cmdhandler from evennia.utils import search from evennia.utils import logger from evennia.utils.utils import (variable_from_module, lazy_property, - make_iter, to_unicode, is_iter) + make_iter, to_unicode, is_iter, list_to_string) from django.utils.translation import ugettext as _ +_INFLECT = inflect.engine() _MULTISESSION_MODE = settings.MULTISESSION_MODE _ScriptDB = None @@ -281,9 +284,34 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): return "{}(#{})".format(self.name, self.id) return self.name + def get_plural_name(self, looker, **kwargs): + """ + Return the plural form of this object's key. This is used for grouping multiple same-named + versions of this object. + + Args: + looker (Object): Onlooker. Not used by default. + Kwargs: + key (str): Optional key to pluralize, use this instead of the object's key. + count (int): How many entities of this type are being counted (not used by default). + Returns: + plural (str): The determined plural form of the key. + + """ + key = kwargs.get("key", self.key) + plural = _INFLECT.plural(key, 2) + if not self.aliases.get(plural, category="plural_key"): + # we need to wipe any old plurals/an/a in case key changed in the interrim + self.aliases.clear(category="plural_key") + self.aliases.add(plural, category="plural_key") + # save the singular form as an alias here too so we can display "an egg" and also + # look at 'an egg'. + self.aliases.add(_INFLECT.an(key), category="plural_key") + return plural + def search(self, searchdata, global_search=False, - use_nicks=True, # should this default to off? + use_nicks=True, typeclass=None, location=None, attribute_name=None, @@ -1441,16 +1469,23 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): elif con.has_account: users.append("|c%s|n" % key) else: - things.append(key) + # things can be pluralized + things.append((key, con.get_plural_name(looker))) # get description, build string string = "|c%s|n\n" % self.get_display_name(looker) desc = self.db.desc if desc: string += "%s" % desc if exits: - string += "\n|wExits:|n " + ", ".join(exits) + string += "\n|wExits:|n " + list_to_string(exits) if users or things: - string += "\n|wYou see:|n " + ", ".join(users + things) + # handle pluralization + things = [("%s %s" % (_INFLECT.number_to_words(count, one=_INFLECT.an(key), threshold=12), + plural if count > 1 else "")).strip() + for ikey, ((key, plural), count) in enumerate(Counter(things).iteritems())] + + string += "\n|wYou see:|n " + list_to_string(users + things) + return string def at_look(self, target, **kwargs): diff --git a/requirements.txt b/requirements.txt index be3cf558e5..7f4b94726f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ pillow == 2.9.0 pytz future >= 0.15.2 django-sekizai +inflect diff --git a/win_requirements.txt b/win_requirements.txt index 7012643657..8a130b3268 100644 --- a/win_requirements.txt +++ b/win_requirements.txt @@ -10,3 +10,4 @@ pillow == 2.9.0 pytz future >= 0.15.2 django-sekizai +inflect From cc1bc92c3dc65a7a5cd7258350feb0d8937f3bdd Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Jan 2018 20:34:03 +0100 Subject: [PATCH 027/217] Rename to get_numbered_name. Handle 'two boxes' through aliasing. Fix unittests --- evennia/commands/default/tests.py | 3 ++- evennia/objects/objects.py | 42 +++++++++++++++++++------------ 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index f437779662..013baec4cc 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -239,7 +239,8 @@ class TestBuilding(CommandTest): self.call(building.CmdExamine(), "Obj", "Name/key: Obj") def test_set_obj_alias(self): - self.call(building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj(#4)' set to testobj1b.") + self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj(#4)") + self.call(building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj(#4)' set to 'testobj1b'.") def test_copy(self): self.call(building.CmdCopy(), "Obj = TestObj2;TestObj2b, TestObj3;TestObj3b", "Copied Obj to 'TestObj3' (aliases: ['TestObj3b']") diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 7c230a8c47..de4e887b32 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -9,7 +9,7 @@ import time import inflect from builtins import object from future.utils import with_metaclass -from collections import Counter +from collections import defaultdict from django.conf import settings @@ -284,30 +284,35 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): return "{}(#{})".format(self.name, self.id) return self.name - def get_plural_name(self, looker, **kwargs): + def get_numbered_name(self, count, looker, **kwargs): """ - Return the plural form of this object's key. This is used for grouping multiple same-named - versions of this object. + Return the numbered (singular, plural) forms of this object's key. This is by default called + by return_appearance and is used for grouping multiple same-named of this object. Note that + this will be called on *every* member of a group even though the plural name will be only + shown once. Also the singular display version, such as 'an apple', 'a tree' is determined + from this method. Args: + count (int): Number of objects of this type looker (Object): Onlooker. Not used by default. Kwargs: key (str): Optional key to pluralize, use this instead of the object's key. - count (int): How many entities of this type are being counted (not used by default). Returns: - plural (str): The determined plural form of the key. - + singular (str): The singular form to display. + plural (str): The determined plural form of the key, including the count. """ key = kwargs.get("key", self.key) plural = _INFLECT.plural(key, 2) + plural = "%s %s" % (_INFLECT.number_to_words(count, threshold=12), plural) + singular = _INFLECT.an(key) if not self.aliases.get(plural, category="plural_key"): # we need to wipe any old plurals/an/a in case key changed in the interrim self.aliases.clear(category="plural_key") self.aliases.add(plural, category="plural_key") # save the singular form as an alias here too so we can display "an egg" and also # look at 'an egg'. - self.aliases.add(_INFLECT.an(key), category="plural_key") - return plural + self.aliases.add(singular, category="plural_key") + return singular, plural def search(self, searchdata, global_search=False, @@ -1461,7 +1466,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): # get and identify all objects visible = (con for con in self.contents if con != looker and con.access(looker, "view")) - exits, users, things = [], [], [] + exits, users, things = [], [], defaultdict(list) for con in visible: key = con.get_display_name(looker) if con.destination: @@ -1470,7 +1475,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): users.append("|c%s|n" % key) else: # things can be pluralized - things.append((key, con.get_plural_name(looker))) + things[key].append(con) # get description, build string string = "|c%s|n\n" % self.get_display_name(looker) desc = self.db.desc @@ -1479,12 +1484,17 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): if exits: string += "\n|wExits:|n " + list_to_string(exits) if users or things: - # handle pluralization - things = [("%s %s" % (_INFLECT.number_to_words(count, one=_INFLECT.an(key), threshold=12), - plural if count > 1 else "")).strip() - for ikey, ((key, plural), count) in enumerate(Counter(things).iteritems())] + # handle pluralization of things (never pluralize users) + thing_strings = [] + for key, itemlist in sorted(things.iteritems()): + nitem = len(itemlist) + if nitem == 1: + key, _ = itemlist[0].get_numbered_name(nitem, looker, key=key) + else: + key = [item.get_numbered_name(nitem, looker, key=key)[1] for item in itemlist][0] + thing_strings.append(key) - string += "\n|wYou see:|n " + list_to_string(users + things) + string += "\n|wYou see:|n " + list_to_string(users + thing_strings) return string From b273641984bcf6cac2efdea4926b77f8896cbefa Mon Sep 17 00:00:00 2001 From: Tehom Date: Mon, 8 Jan 2018 05:31:20 -0500 Subject: [PATCH 028/217] Refactor return_appearance to extract desc update into its own method. --- evennia/contrib/extended_room.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/evennia/contrib/extended_room.py b/evennia/contrib/extended_room.py index 0530bd796c..755c2ac22e 100644 --- a/evennia/contrib/extended_room.py +++ b/evennia/contrib/extended_room.py @@ -213,37 +213,39 @@ class ExtendedRoom(DefaultRoom): return detail return None - def return_appearance(self, looker): + def return_appearance(self, looker, **kwargs): """ This is called when e.g. the look command wants to retrieve the description of this object. Args: looker (Object): The object looking at us. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). Returns: description (str): Our description. """ - update = False + # ensures that our description is current based on time/season + self.update_current_description() + # run the normal return_appearance method, now that desc is updated. + return super(ExtendedRoom, self).return_appearance(looker, **kwargs) + def update_current_description(self): + """ + This will update the description of the room if the time or season + has changed since last checked. + """ + update = False # get current time and season curr_season, curr_timeslot = self.get_time_and_season() - # compare with previously stored slots last_season = self.ndb.last_season last_timeslot = self.ndb.last_timeslot - if curr_season != last_season: # season changed. Load new desc, or a fallback. - if curr_season == 'spring': - new_raw_desc = self.db.spring_desc - elif curr_season == 'summer': - new_raw_desc = self.db.summer_desc - elif curr_season == 'autumn': - new_raw_desc = self.db.autumn_desc - else: - new_raw_desc = self.db.winter_desc + new_raw_desc = self.attributes.get("%s_desc" % curr_season) if new_raw_desc: raw_desc = new_raw_desc else: @@ -252,19 +254,15 @@ class ExtendedRoom(DefaultRoom): self.db.raw_desc = raw_desc self.ndb.last_season = curr_season update = True - if curr_timeslot != last_timeslot: # timeslot changed. Set update flag. self.ndb.last_timeslot = curr_timeslot update = True - if update: # if anything changed we have to re-parse # the raw_desc for time markers # and re-save the description again. self.db.desc = self.replace_timeslots(self.db.raw_desc, curr_timeslot) - # run the normal return_appearance method, now that desc is updated. - return super(ExtendedRoom, self).return_appearance(looker) # Custom Look command supporting Room details. Add this to From a634f156540c03a90f48bd887d1df0c7ba322158 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 9 Jan 2018 18:09:56 +0100 Subject: [PATCH 029/217] Add escaping = as \= in nicks, add colors. Resolves #1551. --- evennia/commands/default/general.py | 51 +++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 6a30e3813c..97c9e49a8d 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -1,6 +1,7 @@ """ General Character commands usually available to all characters """ +import re from django.conf import settings from evennia.utils import utils, evtable from evennia.typeclasses.attributes import NickTemplateInvalid @@ -75,13 +76,14 @@ class CmdLook(COMMAND_DEFAULT_CLASS): class CmdNick(COMMAND_DEFAULT_CLASS): """ - define a personal alias/nick + define a personal alias/nick by defining a string to + match and replace it with another on the fly Usage: nick[/switches] [= [replacement_string]] nick[/switches]