From 3d102740a985f97362b920d8aafac179a91fb004 Mon Sep 17 00:00:00 2001 From: RealKinetix Date: Mon, 29 Mar 2021 22:34:55 -0700 Subject: [PATCH 1/7] Maintenance time calculations should be done in minutes, not seconds. Should resolve #2336 --- evennia/server/portal/portal.py | 2 +- evennia/server/server.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index d1af41a143..f3e29fb9a0 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -119,7 +119,7 @@ def _portal_maintenance(): _MAINTENANCE_COUNT += 1 - if _MAINTENANCE_COUNT % (3600 * 7) == 0: + if _MAINTENANCE_COUNT % (60 * 7) == 0: # drop database connection every 7 hrs to avoid default timeouts on MySQL # (see https://github.com/evennia/evennia/issues/1376) connection.close() diff --git a/evennia/server/server.py b/evennia/server/server.py index 4d09d8fdb6..fb112fe5cb 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -140,16 +140,16 @@ def _server_maintenance(): _GAMETIME_MODULE.SERVER_RUNTIME_LAST_UPDATED = now ServerConfig.objects.conf("runtime", _GAMETIME_MODULE.SERVER_RUNTIME) - if _MAINTENANCE_COUNT % 300 == 0: + if _MAINTENANCE_COUNT % 5 == 0: # check cache size every 5 minutes _FLUSH_CACHE(_IDMAPPER_CACHE_MAXSIZE) - if _MAINTENANCE_COUNT % 3600 == 0: + if _MAINTENANCE_COUNT % 60 == 0: # validate scripts every hour evennia.ScriptDB.objects.validate() - if _MAINTENANCE_COUNT % 3700 == 0: + if _MAINTENANCE_COUNT % 61 == 0: # validate channels off-sync with scripts evennia.CHANNEL_HANDLER.update() - if _MAINTENANCE_COUNT % (3600 * 7) == 0: + if _MAINTENANCE_COUNT % (60 * 7) == 0: # drop database connection every 7 hrs to avoid default timeouts on MySQL # (see https://github.com/evennia/evennia/issues/1376) connection.close() From f088ba4ba21b130b472dd140a72b76900c0dfb8e Mon Sep 17 00:00:00 2001 From: RealKinetix Date: Tue, 6 Apr 2021 17:25:08 -0700 Subject: [PATCH 2/7] Fixed related server test in the test suite with appropriate timing trigger. --- evennia/server/tests/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/server/tests/test_server.py b/evennia/server/tests/test_server.py index 33a9341cae..0d4a217ed7 100644 --- a/evennia/server/tests/test_server.py +++ b/evennia/server/tests/test_server.py @@ -84,7 +84,7 @@ class TestServer(TestCase): _FLUSH_CACHE=DEFAULT, connection=DEFAULT, _IDMAPPER_CACHE_MAXSIZE=1000, - _MAINTENANCE_COUNT=3700 - 1, + _MAINTENANCE_COUNT=62 - 1, _LAST_SERVER_TIME_SNAPSHOT=0, ServerConfig=DEFAULT, ) as mocks: From 24ed366b326d1de0ac59d880e20b46dd0bea5901 Mon Sep 17 00:00:00 2001 From: RealKinetix Date: Tue, 6 Apr 2021 17:43:49 -0700 Subject: [PATCH 3/7] Unsure how the last commit had an old edit, but this should fix tests now. --- evennia/server/tests/test_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/server/tests/test_server.py b/evennia/server/tests/test_server.py index 0d4a217ed7..a7933cd6ff 100644 --- a/evennia/server/tests/test_server.py +++ b/evennia/server/tests/test_server.py @@ -47,7 +47,7 @@ class TestServer(TestCase): _FLUSH_CACHE=DEFAULT, connection=DEFAULT, _IDMAPPER_CACHE_MAXSIZE=1000, - _MAINTENANCE_COUNT=600 - 1, + _MAINTENANCE_COUNT=5 - 1, ServerConfig=DEFAULT, ) as mocks: mocks["connection"].close = MagicMock() @@ -65,7 +65,7 @@ class TestServer(TestCase): _FLUSH_CACHE=DEFAULT, connection=DEFAULT, _IDMAPPER_CACHE_MAXSIZE=1000, - _MAINTENANCE_COUNT=3600 - 1, + _MAINTENANCE_COUNT=60 - 1, _LAST_SERVER_TIME_SNAPSHOT=0, ServerConfig=DEFAULT, ) as mocks: @@ -84,7 +84,7 @@ class TestServer(TestCase): _FLUSH_CACHE=DEFAULT, connection=DEFAULT, _IDMAPPER_CACHE_MAXSIZE=1000, - _MAINTENANCE_COUNT=62 - 1, + _MAINTENANCE_COUNT=61 - 1, _LAST_SERVER_TIME_SNAPSHOT=0, ServerConfig=DEFAULT, ) as mocks: @@ -102,7 +102,7 @@ class TestServer(TestCase): _FLUSH_CACHE=DEFAULT, connection=DEFAULT, _IDMAPPER_CACHE_MAXSIZE=1000, - _MAINTENANCE_COUNT=(3600 * 7) - 1, + _MAINTENANCE_COUNT=(60 * 7) - 1, _LAST_SERVER_TIME_SNAPSHOT=0, ServerConfig=DEFAULT, ) as mocks: From 251a70275bbd98a3e157cbb4c025597a4bb24ac9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 13 Apr 2021 23:57:09 +0200 Subject: [PATCH 4/7] Update the evscaperoom README --- evennia/contrib/evscaperoom/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/evscaperoom/README.md b/evennia/contrib/evscaperoom/README.md index 8cf8930e74..113897b409 100644 --- a/evennia/contrib/evscaperoom/README.md +++ b/evennia/contrib/evscaperoom/README.md @@ -4,9 +4,15 @@ Evennia contrib - Griatch 2019 This 'Evennia escaperoom game engine' was created for the MUD Coders Guild game Jam, April 14-May 15 2019. The theme for the jam was "One Room". This contains the -utilities and base classes and an empty example room. The code for the full -in-production game 'Evscaperoom' is found at https://github.com/Griatch/evscaperoom -and you can play the full game (for now) at `http://experimental.evennia.com`. +utilities and base classes and an empty example room. + +The original code for the contest is found at https://github.com/Griatch/evscaperoom +but the version on the public Evennia demo is more updated, so if you really +want the latest bug fixes etc you should rather look at +https://github.com/evennia/evdemo/tree/master/evdemo/evscaperoom instead. +A copy of the full game can also be played on the Evennia demo server at +https://demo.evennia.com - just connect to the server and write `evscaperoom` +in the first room to start! # Introduction From 8a64ee9830a411bea660fd95ff3af971373283ff Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Tue, 20 Apr 2021 16:31:30 -0400 Subject: [PATCH 5/7] rpsystem.send_emote passes kwargs, uses sender as from_obj rpsystem.send_emote now passes kwargs to obj.msg. rpsystem.send_emote uses sender as from_obj when calling obj.msg All evennia unit tests pass. --- evennia/contrib/rpsystem.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/rpsystem.py b/evennia/contrib/rpsystem.py index 107d6c9c06..b5ebed2df7 100644 --- a/evennia/contrib/rpsystem.py +++ b/evennia/contrib/rpsystem.py @@ -480,7 +480,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False): return string, mapping -def send_emote(sender, receivers, emote, anonymous_add="first"): +def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs): """ Main access function for distribute an emote. @@ -508,7 +508,9 @@ def send_emote(sender, receivers, emote, anonymous_add="first"): # we escape the object mappings since we'll do the language ones first # (the text could have nested object mappings). emote = _RE_REF.sub(r"{{#\1}}", emote) - + # if anonymous_add is passed as a kwarg, collect and remove it from kwargs + if 'anonymous_add' in kwargs: + anonymous_add = kwargs.pop('anonymous_add') if anonymous_add and not "#%i" % sender.id in obj_mapping: # no self-reference in the emote - add to the end key = "#%i" % sender.id @@ -566,7 +568,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first"): receiver_sdesc_mapping[rkey] = process_sdesc(receiver.key, receiver) # do the template replacement of the sdesc/recog {#num} markers - receiver.msg(sendemote.format(**receiver_sdesc_mapping)) + receiver.msg(sendemote.format(**receiver_sdesc_mapping), from_obj=sender, **kwargs) # ------------------------------------------------------------ From 4480bd6130ea362ba6d93837daed1e7ac163fb2a Mon Sep 17 00:00:00 2001 From: Ben Longden Date: Tue, 20 Apr 2021 22:30:10 +0100 Subject: [PATCH 6/7] Don't allow fuzzy match on db if exact match on module prototype --- evennia/prototypes/prototypes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 185b9dfc5d..a304dd8684 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -379,10 +379,12 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators else: mod_matches = _MODULE_PROTOTYPES + allow_fuzzy = True if key: if key in mod_matches: # exact match module_prototypes = [mod_matches[key]] + allow_fuzzy = False else: # fuzzy matching module_prototypes = [ @@ -406,7 +408,7 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators if key: # exact or partial match on key exact_match = db_matches.filter(Q(db_key__iexact=key)).order_by("db_key") - if not exact_match: + if not exact_match and allow_fuzzy: # try with partial match instead db_matches = db_matches.filter(Q(db_key__icontains=key)).order_by("db_key") else: @@ -423,7 +425,7 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators nmodules = len(module_prototypes) ndbprots = db_matches.count() if nmodules + ndbprots != 1: - raise KeyError(f"Found {nmodules + ndbprots} matching prototypes.") + raise KeyError(f"Found {nmodules + ndbprots} matching prototypes {module_prototypes}.") if return_iterators: # trying to get the entire set of prototypes - we must paginate From 3290511d9d9f75e9ed0407ba778c8126c6d97e81 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 8 May 2021 10:09:04 +0200 Subject: [PATCH 7/7] Some docstring cleanup --- docs/Makefile | 8 + docs/source/Components/Components-Overview.md | 2 +- evennia/contrib/traits.py | 143 ++++++++++-------- evennia/utils/evmenu.py | 47 +++--- 4 files changed, 118 insertions(+), 82 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 0cc01b83ed..ff42f7fa68 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -120,6 +120,14 @@ local: @echo "" @echo "Documentation built (single version)." @echo "To see result, open evennia/docs/build/html/index.html in a browser." + +# build only that which updated since last run (no clean or index-creation) +localupdate: + make _check-env + make _html-build + @echo "" + @echo "Documentation built (single version, only updates, no auto-index)." + @echo "To see result, open evennia/docs/build/html/index.html in a browser." # note that this should be done for each relevant multiversion branch. mv-index: diff --git a/docs/source/Components/Components-Overview.md b/docs/source/Components/Components-Overview.md index 8d9bcea25a..d737b00040 100644 --- a/docs/source/Components/Components-Overview.md +++ b/docs/source/Components/Components-Overview.md @@ -38,7 +38,7 @@ than, the doc-strings of each component in the [API](../Evennia-API). - [MonitorHandler](./MonitorHandler) - [TickerHandler](./TickerHandler) - [Lock system](./Locks) -- [FuncParser](FuncParser) +- [FuncParser](./FuncParser) ## Server and network diff --git a/evennia/contrib/traits.py b/evennia/contrib/traits.py index 33636f2bdf..ce0d99bd21 100644 --- a/evennia/contrib/traits.py +++ b/evennia/contrib/traits.py @@ -14,31 +14,31 @@ a server reload/reboot). ## Adding Traits to a typeclass -To access and manipulate traits on an object, its Typeclass needs to have a +To access and manipulate traits on an entity, its Typeclass needs to have a `TraitHandler` assigned it. Usually, the handler is made available as `.traits` -(in the same way as `.tags` or `.attributes`). +(in the same way as `.tags` or `.attributes`). It's recommended to do this +using Evennia's `lazy_property` (which basically just means it's not +initialized until it's actually accessed). Here's an example for adding the TraitHandler to the base Object class: - ```python - # mygame/typeclasses/objects.py +```python +# mygame/typeclasses/objects.py - from evennia import DefaultObject - from evennia.utils import lazy_property - from evennia.contrib.traits import TraitHandler +from evennia import DefaultObject +from evennia.utils import lazy_property +from evennia.contrib.traits import TraitHandler - # ... +# ... - class Object(DefaultObject): - ... - @lazy_property - def traits(self): - # this adds the handler as .traits - return TraitHandler(self) +class Object(DefaultObject): + ... + @lazy_property + def traits(self): + # this adds the handler as .traits + return TraitHandler(self) - ``` - -After a reload you can now try adding some example traits: +``` ## Using traits @@ -48,6 +48,7 @@ in Evennia). ```python # this is an example using the "static" trait, described below + >>> obj.traits.add("hunting", "Hunting Skill", trait_type="static", base=4) >>> obj.traits.hunting.value 4 @@ -130,17 +131,19 @@ that varies slowly or not at all, and which may be modified in-place. ``` ### Counter +:: min/unset base base+mod max/unset - |--------------|--------|---------X--------X------------| - current value - = current - + mod + |--------------|--------|---------X--------X------------| + current value + = current + + mod -A counter describes a value that can move from a base. The `current` property -is the thing usually modified. It starts at the `base`. One can also add a modifier, -which will both be added to the base and to current (forming .value). -The min/max of the range are optional, a boundary set to None will remove it. +A counter describes a value that can move from a base. The `.current` property +is the thing usually modified. It starts at the `.base`. One can also add a +modifier, which will both be added to the base and to current (forming +`.value`). The min/max of the range are optional, a boundary set to None will +remove it. A suggested use for a Counter Trait would be to track skill values. ```python >>> obj.traits.add("hunting", "Hunting Skill", trait_type="counter", @@ -160,10 +163,15 @@ The min/max of the range are optional, a boundary set to None will remove it. Counters have some extra properties: -`descs` is a dict {upper_bound:text_description}. This allows for easily +#### .descs + +The `descs` property is a dict {upper_bound:text_description}. This allows for easily storing a more human-friendly description of the current value in the interval. Here is an example for skill values between 0 and 10: +:: + {0: "unskilled", 1: "neophyte", 5: "trained", 7: "expert", 9: "master"} + The keys must be supplied from smallest to largest. Any values below the lowest and above the highest description will be considered to be included in the closest description slot. By calling `.desc()` on the Counter, will you get the text matching the current `value` @@ -190,11 +198,11 @@ value. The `rate` property defaults to 0. If set to a value different from 0, it allows the trait to change value dynamically. This could be used for example for an attribute that was temporarily lowered but will gradually (or abruptly) -recover after a certain time. The rate is given as change of the `current` -per-second, and the .value will still be restrained by min/max boundaries, if -those are set. +recover after a certain time. The rate is given as change of the current +`.value` per-second, and this will still be restrained by min/max boundaries, +if those are set. -It is also possible to set a ".ratetarget", for the auto-change to stop at +It is also possible to set a `.ratetarget`, for the auto-change to stop at (rather than at the min/max boundaries). This allows the value to return to a previous value. @@ -220,35 +228,41 @@ a previous value. >>> obj.traits.hunting.rate = 0 # disable auto-change ``` -Note that if rate is a non-integer, the resulting .value (at least until it -reaches the boundary) will likely also come out a float. If you expect an -integer, you must run run int() on the result yourself. +Note that if `.rate` is a non-integer, the resulting `.value` (at least until it +reaches a boundary or rate-target) will also come out a float (so you can get a +very exact value at the current time). If you expect an integer, you must run +`int()` (or something like `round()`) on the result yourself. -#### .percentage() +#### .percent() -If both min and max are defined, the `.percentage()` method of the trait will +If both min and max are defined, the `.percent()` method of the trait will return the value as a percentage. ```python ->>> obj.traits.hunting.percentage() +>>> obj.traits.hunting.percent() "71.0%" +>>> obj.traits.hunting.percent(formatting=None) +71.0 ``` ### Gauge This emulates a [fuel-] gauge that empties from a base+mod value. +:: min/0 max=base+mod |-----------------------X---------------------------| value = current -The 'current' value will start from a full gauge. The .max property is -read-only and is set by .base + .mod. So contrary to a Counter, the modifier -only applies to the max value of the gauge and not the current value. The -minimum bound defaults to 0. This trait is useful for showing resources that -can deplete, like health, stamina and the like. +The `.current` value will start from a full gauge. The .max property is +read-only and is set by `.base` + `.mod`. So contrary to a `Counter`, the +`.mod` modifier only applies to the max value of the gauge and not the current +value. The minimum bound defaults to 0 if not set explicitly. + +This trait is useful for showing commonly depletable resources like health, +stamina and the like. ```python >>> obj.traits.add("hp", "Health", trait_type="gauge", base=100) @@ -263,20 +277,24 @@ can deplete, like health, stamina and the like. ``` -Same as Counters, Gauges can also have `descs` to describe the interval and can also -have `rate` and `ratetarget` to auto-update the value. The rate is particularly useful -for gauges, for everything from poison slowly draining your health, to resting gradually -increasing it. You can also use the `.percentage()` function to show the current value -as a percentage. +The Gauge trait is subclass of the Counter, so you have access to the same +methods and properties where they make sense. So gauges can also have a +`.descs` dict to describe the intervals in text, and can use `.percent()` to +get how filled it is as a percentage etc. + +The `.rate` is particularly relevant for gauges - useful for everything +from poison slowly draining your health, to resting gradually increasing it. ### Trait A single value of any type. -This is the 'base' Trait, meant to inherit from if you want to make your own -trait-types (see below). Its .value can be anything (that can be stored in an Attribute) -and if it's a integer/float you can do arithmetic with it, but otherwise it -acts just like a glorified Attribute. +This is the 'base' Trait, meant to inherit from if you want to invent +trait-types from scratch (most of the time you'll probably inherit from some of +the more advanced trait-type classes though). A `Trait`s `.value` can be +anything (that can be stored in an Attribute) and if it's a integer/float you +can do arithmetic with it, but otherwise it acts just like a glorified +Attribute. ```python @@ -291,38 +309,45 @@ acts just like a glorified Attribute. ## Expanding with your own Traits -A Trait is a class inhering from `evennia.contrib.traits.Trait` (or -from one of the existing Trait classes). +A Trait is a class inhering from `evennia.contrib.traits.Trait` (or from one of +the existing Trait classes). ```python # in a file, say, 'mygame/world/traits.py' -from evennia.contrib.traits import Trait +from evennia.contrib.traits import StaticTrait -class RageTrait(Trait): +class RageTrait(StaticTrait): trait_type = "rage" default_keys = { "rage": 0 } + def berserk(self): + self.mod = 100 + + def sedate(self): + self.mod = 0 + + ``` Above is an example custom-trait-class "rage" that stores a property "rage" on -itself, with a default value of 0. This has all the -functionality of a Trait - for example, if you do del on the `rage` property, it will be -set back to its default (0). If you wanted to customize what it does, you -just add `rage` property get/setters/deleters on the class. +itself, with a default value of 0. This has all the functionality of a Trait - +for example, if you do del on the `rage` property, it will be set back to its +default (0). Above we also added some helper methods. To add your custom RageTrait to Evennia, add the following to your settings file (assuming your class is in mygame/world/traits.py): +:: TRAIT_CLASS_PATHS = ["world.traits.RageTrait"] Reload the server and you should now be able to use your trait: ```python ->>> obj.traits.add("mood", "A dark mood", rage=30) +>>> obj.traits.add("mood", "A dark mood", rage=30, trait_type='rage') >>> obj.traits.mood.rage 30 diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 8d7e25dc58..af30789c74 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1710,36 +1710,39 @@ def ask_yes_no(caller, prompt, yes_action, no_action, default=None, Args: prompt (str): The yes/no question to ask. This takes an optional formatting - marker `{suffix}` which will be filled with 'Y/N', [Y]/N or Y/[N] - depending on the setting of `default`. If `allow_abort`, then the - `A(bort)` will also be available. + marker `{options}` which will be filled with 'Y/N', '[Y]/N' or + 'Y/[N]' depending on the setting of `default`. If `allow_abort` is set, + then the 'A(bort)' option will also be available. yes_action (callable or str): If a callable, this will be called - with `(caller, *args, **kwargs) when the yes-choice is made. + with `(caller, *args, **kwargs)` when the Yes-choice is made. If a string, this string will be echoed back to the caller. no_action (callable or str): If a callable, this will be called - with `(caller, *args, **kwargs)` when the no-choice is made. + with `(caller, *args, **kwargs)` when the No-choice is made. If a string, this string will be echoed back to the caller. - default (str optional): One of "N", "Y", "A" or None for no default. - If "A" is given, `allow_abort` is assumed set. The user can choose - the default option just by pressing return. - allow_abort (bool, optional): If set, the Q(uit) option is available, - which is neither yes or no. + default (str optional): This is what the user will get if they just press the + return key without giving any input. One of 'N', 'Y', 'A' or 'None' + for no default. If 'A' is given, `allow_abort` is auto-set. + allow_abort (bool, optional): If set, the 'A(bort)' option is available + (a third option meaning neither yes or no but just exits the prompt). session (Session, optional): This allows to specify the session to send the prompt to. It's usually only needed if `caller` is an Account in multisession modes greater than 2. The session is then updated by the command and is available (for example in callbacks) through `caller.ndb._yes_no_question.session`. - *args, **kwargs: These are passed into the callables, if any. + *args, **kwargs: These are passed into the callables. Raises: - RuntimeError: If default and allow_abort clashes. + RuntimeError, FooError: If default and allow_abort clashes. Example: + :: - ask_yes_no(caller, "Are you happy {suffix}?", - "you answered yes", "you answered no") - ask_yes_no(caller, "Are you sad {suffix}?", - _callable_yes, _callable_no, allow_abort=True) + # just returning strings + ask_yes_no(caller, "Are you happy {options}?", + "you answered yes", "you answered no") + # trigger callables + ask_yes_no(caller, "Are you sad {options}?", + _callable_yes, _callable_no, allow_abort=True) """ def _callable_yes_txt(caller, *args, **kwargs): @@ -1760,20 +1763,20 @@ def ask_yes_no(caller, prompt, yes_action, no_action, default=None, kwargs['no_txt'] = str(no_action) no_action = _callable_no_txt - # prepare the prompt with suffix - suffix = "Y/N" + # prepare the prompt with options + options = "Y/N" abort_txt = "/Abort" if allow_abort else "" if default: default = default.lower() if default == "y": - suffix = "[Y]/N" + options = "[Y]/N" elif default == "n": - suffix = "Y/[N]" + options = "Y/[N]" elif default == "a": allow_abort = True abort_txt = "/[A]bort" - suffix += abort_txt - prompt = prompt.format(suffix=suffix) + options += abort_txt + prompt = prompt.format(options=options) caller.ndb._yes_no_question = _Prompt() caller.ndb._yes_no_question.session = session