From 21b164f6996440af88a27d7101cecae63220766f Mon Sep 17 00:00:00 2001 From: homeofpoe Date: Thu, 21 Mar 2024 09:12:16 -0700 Subject: [PATCH 01/68] Update Beginner-Tutorial-Adding-Commands.md Resolves #3448 --- .../Part1/Beginner-Tutorial-Adding-Commands.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md index 78c3fa29bb..5d9d3f6e2c 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md @@ -238,7 +238,7 @@ Tweak this file as follows: ```python # in mygame/commands/default_cmdsets.py -# ,.. +# ... from . import mycommands # <------- @@ -299,9 +299,6 @@ Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and ```{code-block} python # in mygame/commands/mycommands.py -:linenos: -:emphasize-lines: 3,4,11,14,15,17,18,19,21 - # ... class CmdHit(Command): @@ -439,4 +436,4 @@ You won't see the second string. Only Smaug sees that (and is not amused). In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves. We also upset a dragon. In the next lesson we'll learn how to hit Smaug with different weapons. We'll also -get into how we replace and extend Evennia's default Commands. \ No newline at end of file +get into how we replace and extend Evennia's default Commands. From 2ec97f102d21fe4552055882fab56ca55e2345ed Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Fri, 29 Mar 2024 20:40:46 +1100 Subject: [PATCH 02/68] Fix aliases not being shown for disambiguation in utils.at_search_result() Also correct misplaced 'if' used to avoid '[]' in the case of no aliases and update unit tests. --- evennia/contrib/rpg/rpsystem/tests.py | 16 +++++++++++++--- evennia/utils/utils.py | 6 +++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/tests.py b/evennia/contrib/rpg/rpsystem/tests.py index 155af26672..04d4ef49de 100644 --- a/evennia/contrib/rpg/rpsystem/tests.py +++ b/evennia/contrib/rpg/rpsystem/tests.py @@ -6,6 +6,7 @@ import time from anything import Anything from evennia import DefaultObject, create_object, default_cmds +from evennia.commands.default import building from evennia.commands.default.tests import BaseEvenniaCommandTest from evennia.utils.test_resources import BaseEvenniaTest @@ -413,10 +414,9 @@ class TestRPSystemCommands(BaseEvenniaCommandTest): expected_first_call = [ "More than one match for 'Mushroom' (please narrow target):", - f" Mushroom-1 []", - f" Mushroom-2 []", + f" Mushroom-1", + f" Mushroom-2", ] - self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_first_call)) # PASSES expected_second_call = f"Mushroom(#{mushroom1.id})\nThe first mushroom is brown." @@ -424,3 +424,13 @@ class TestRPSystemCommands(BaseEvenniaCommandTest): expected_third_call = f"Mushroom(#{mushroom2.id})\nThe second mushroom is red." self.call(default_cmds.CmdLook(), "Mushroom-2", expected_third_call) # FAILS + + expected_fourth_call = "Alias(es) for 'Mushroom' set to 'fungus'." + self.call(building.CmdSetObjAlias(), "Mushroom-1 = fungus", expected_fourth_call) #PASSES + + expected_fifth_call = [ + "More than one match for 'Mushroom' (please narrow target):", + f" Mushroom-1 [fungus]", + f" Mushroom-2", + ] + self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_fifth_call)) # PASSES diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index a3e3633a94..6748d606f7 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -2403,9 +2403,9 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs): aliases = result.aliases.all(return_objs=True) # remove pluralization aliases aliases = [ - alias + alias.db_key for alias in aliases - if hasattr(alias, "category") and alias.category not in ("plural_key",) + if alias.db_category != "plural_key" ] else: # result is likely a Command, where `.aliases` is a list of strings. @@ -2416,7 +2416,7 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs): name=result.get_display_name(caller) if hasattr(result, "get_display_name") else query, - aliases=" [{alias}]".format(alias=";".join(aliases) if aliases else ""), + aliases=" [{alias}]".format(alias=";".join(aliases)) if aliases else "", info=result.get_extra_info(caller), ) matches = None From 334bfaefadd8c96dcf42965791c8d1fddfafbebb Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:49:19 -0600 Subject: [PATCH 03/68] fix crafting msg traceback --- evennia/contrib/game_systems/crafting/crafting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/game_systems/crafting/crafting.py b/evennia/contrib/game_systems/crafting/crafting.py index 593f1a04b9..875e843d5f 100644 --- a/evennia/contrib/game_systems/crafting/crafting.py +++ b/evennia/contrib/game_systems/crafting/crafting.py @@ -237,7 +237,7 @@ class CraftingRecipeBase: **kwargs: Any optional properties relevant to this send. """ - self.crafter.msg(message, {"type": "crafting"}) + self.crafter.msg(text=(message, {"type": "crafting"})) def pre_craft(self, **kwargs): """ From 8f67f3934c114d051fb963e631ea20a9bc55ef30 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sat, 30 Mar 2024 12:48:38 +1100 Subject: [PATCH 04/68] Fix traceback when using sethelp/edit to create a new topic --- evennia/commands/default/help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 9956a23c08..75860e976c 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -953,7 +953,7 @@ class CmdSetHelp(CmdHelp): else: helpentry = create.create_help_entry( topicstr, - self.rhs, + self.rhs if self.rhs is not None else "", category=category, locks=lockstring, aliases=aliases, From 03e6f6811cf5e31649ce3fc63d2fac420f00a8e8 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sat, 30 Mar 2024 13:29:40 +1100 Subject: [PATCH 05/68] Fix weighted_rows() sometimes 'losing' items --- evennia/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index a3e3633a94..ce21cc1d9d 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -2070,7 +2070,7 @@ def format_grid(elements, width=78, sep=" ", verbatim_elements=None, line_prefi else: row += " " * max(0, width - lrow) rows.append(row) - row = "" + row = element ic = 0 else: # add a new slot From 8357232dc0877b544f40be094cd08bf4df6069f7 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sat, 30 Mar 2024 17:34:55 +1100 Subject: [PATCH 06/68] Fix file help topic category casing differences appearing as duplicated categories --- evennia/help/filehelp.py | 2 +- evennia/help/tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/help/filehelp.py b/evennia/help/filehelp.py index 3f8a598249..4c9cf3d9f1 100644 --- a/evennia/help/filehelp.py +++ b/evennia/help/filehelp.py @@ -227,7 +227,7 @@ class FileHelpStorageHandler: for dct in loaded_help_dicts: key = dct.get("key").lower().strip() - category = dct.get("category", _DEFAULT_HELP_CATEGORY).strip() + category = dct.get("category", _DEFAULT_HELP_CATEGORY).lower().strip() aliases = list(dct.get("aliases", [])) entrytext = dct.get("text", "") locks = dct.get("locks", "") diff --git a/evennia/help/tests.py b/evennia/help/tests.py index 6989fddfb3..0f7f31f242 100644 --- a/evennia/help/tests.py +++ b/evennia/help/tests.py @@ -138,5 +138,5 @@ class TestFileHelp(TestCase): for inum, helpentry in enumerate(result): self.assertEqual(HELP_ENTRY_DICTS[inum]["key"], helpentry.key) self.assertEqual(HELP_ENTRY_DICTS[inum].get("aliases", []), helpentry.aliases) - self.assertEqual(HELP_ENTRY_DICTS[inum]["category"], helpentry.help_category) + self.assertEqual(HELP_ENTRY_DICTS[inum]["category"].lower(), helpentry.help_category) self.assertEqual(HELP_ENTRY_DICTS[inum]["text"], helpentry.entrytext) From f1220e7b0219782e3cb30490ff7694ffecc06232 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sat, 30 Mar 2024 18:30:41 +1100 Subject: [PATCH 07/68] Add sethelp/category switch for changing database help topic's category --- evennia/commands/default/help.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 9956a23c08..90a67602f2 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -780,13 +780,14 @@ class CmdSetHelp(CmdHelp): Edit the help database. Usage: - sethelp[/switches] [[;alias;alias][,category[,locks]] [= ] - + sethelp[/switches] [[;alias;alias][,category[,locks]] + [= ] Switches: edit - open a line editor to edit the topic's help text. replace - overwrite existing help topic. append - add text to the end of existing topic with a newline between. extend - as append, but don't add a newline. + category - change category of existing help topic. delete - remove help topic. Examples: @@ -794,6 +795,7 @@ class CmdSetHelp(CmdHelp): sethelp/append pickpocketing,Thievery = This steals ... sethelp/replace pickpocketing, ,attr(is_thief) = This steals ... sethelp/edit thievery + sethelp/category thievery = classes If not assigning a category, the `settings.DEFAULT_HELP_CATEGORY` category will be used. If no lockstring is specified, everyone will be able to read @@ -840,7 +842,7 @@ class CmdSetHelp(CmdHelp): key = "sethelp" aliases = [] - switch_options = ("edit", "replace", "append", "extend", "delete") + switch_options = ("edit", "replace", "append", "extend", "category", "delete") locks = "cmd:perm(Helper)" help_category = "Building" arg_regex = None @@ -857,7 +859,7 @@ class CmdSetHelp(CmdHelp): if not self.args: self.msg( - "Usage: sethelp[/switches] [;alias;alias][,category[,locks,..] = " + "Usage: sethelp[/switches] [[;alias;alias][,category[,locks]] [= ]" ) return @@ -986,6 +988,19 @@ class CmdSetHelp(CmdHelp): self.msg(f"Entry updated:\n{old_entry.entrytext}{aliastxt}") return + if "category" in switches: + # set the category + if not old_entry: + self.msg(f"Could not find topic '{topicstr}'{aliastxt}.") + return + if not self.rhs: + self.msg("You must supply a category.") + return + category = self.rhs.lower() + old_entry.help_category = category + self.msg(f"Category for entry '{topicstr}'{aliastxt} changed to '{category}'.") + return + if "delete" in switches or "del" in switches: # delete the help entry if not old_entry: From c05714d0d4a91a97bcd3eb11fddb9a0d1a4cac02 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 30 Mar 2024 09:47:07 +0100 Subject: [PATCH 08/68] Continue with quest beginner tutorial --- .../Part3/Beginner-Tutorial-Quests.md | 10 +- .../contrib/tutorials/evadventure/enums.py | 12 ++ .../contrib/tutorials/evadventure/quests.py | 179 ++++++++++++++---- .../evadventure/tests/test_quests.py | 2 +- 4 files changed, 165 insertions(+), 38 deletions(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md index e74f1578ee..2398ff20fe 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md @@ -1,5 +1,9 @@ # Game Quests -```{warning} -This part of the Beginner tutorial is still being developed. -``` \ No newline at end of file +A _quest_ is a common feature of games. From classic fetch-quests like retrieving 10 flowers to complex quest chains involving drama and intrigue, quests need to be properly tracked in our game. + +A quest follows a specific development: + +1. The quest is _started_. This normally involves the player accepting the quest, from a quest-giver, job board or other source. But the quest could also be thrust on the player ("save the family from the burning house before it collapses!") +2. A quest may consist of one or more 'steps'. Each step has its own set of finish conditions. +3. At suitable times the quest is _checked_. This could happen on a timer or when trying to 'hand in' the quest. When checking, the current 'step' is checked against its finish conditions. If ok, that step is closed and the next step is checked until it either hits a step that is not yet complete, or there are no more steps, in which case the entire quest is complete. \ No newline at end of file diff --git a/evennia/contrib/tutorials/evadventure/enums.py b/evennia/contrib/tutorials/evadventure/enums.py index 6ba77d82f9..4ec4405e9f 100644 --- a/evennia/contrib/tutorials/evadventure/enums.py +++ b/evennia/contrib/tutorials/evadventure/enums.py @@ -84,3 +84,15 @@ class ObjType(Enum): MAGIC = "magic" QUEST = "quest" TREASURE = "treasure" + + +class QuestStatus(Enum): + """ + Quest status + + """ + + STARTED = "started" + COMPLETED = "completed" + ABANDONED = "abandoned" + FAILED = "failed" diff --git a/evennia/contrib/tutorials/evadventure/quests.py b/evennia/contrib/tutorials/evadventure/quests.py index be8d0a98cd..1e1f2780da 100644 --- a/evennia/contrib/tutorials/evadventure/quests.py +++ b/evennia/contrib/tutorials/evadventure/quests.py @@ -14,9 +14,7 @@ another quest. """ -from copy import copy, deepcopy - -from evennia.utils import dbserialize +from .enums import QuestStatus class EvAdventureQuest: @@ -40,11 +38,11 @@ class EvAdventureQuest: start_step = "A" - help_A = "You need a '_quest_A_flag' on yourself to finish this step!" + help_A = "You need a 'A_flag' attribute on yourself to finish this step!" help_B = "Finally, you need more than 4 items in your inventory!" def step_A(self, *args, **kwargs): - if self.quester.db._quest_A_flag == True: + if self.get_data("A_flag") == True: self.quester.msg("Completed the first step of the quest.") self.current_step = "end" self.progress() @@ -67,21 +65,16 @@ class EvAdventureQuest: help_start = "You need to start first" help_end = "You need to end the quest" - def __init__(self, quester, start_step=None): + def __init__(self, quester, data=None): if " " in self.key: raise TypeError("The Quest name must not have spaces in it.") self.quester = quester - self._current_step = start_step or self.start_step - self.is_completed = False - self.is_abandoned = False + self.data = data or dict() + self._current_step = self.get_data("current_step") - def __serialize_dbobjs__(self): - self.quester = dbserialize.dbserialize(self.quester) - - def __deserialize_dbobjs__(self): - if isinstance(self.quester, bytes): - self.quester = dbserialize.dbunserialize(self.quester) + if not self.current_step: + self.current_step = self.start_step @property def questhandler(self): @@ -94,14 +87,73 @@ class EvAdventureQuest: @current_step.setter def current_step(self, step_name): self._current_step = step_name + self.add_data("current_step", step_name) self.questhandler.do_save = True + @property + def status(self): + return self.get_data("status", QuestStatus.STARTED) + + @status.setter + def status(self, value): + self.add_data("status", value) + + @property + def is_completed(self): + return self.status == QuestStatus.COMPLETED + + @property + def is_abandoned(self): + return self.status == QuestStatus.ABANDONED + + @property + def is_failed(self): + return self.status == QuestStatus.FAILED + + def add_data(self, key, value): + """ + Add data to the quest. This saves it permanently. + + Args: + key (str): The key to store the data under. + value (any): The data to store. + + """ + self.data[key] = value + self.questhandler.save_quest_data(self.key, self.data) + + def remove_data(self, key): + """ + Remove data from the quest permanently. + + Args: + key (str): The key to remove. + + """ + self.data.pop(key, None) + self.questhandler.save_quest_data(self.key, self.data) + + def get_data(self, key, default=None): + """ + Get data from the quest. + + Args: + key (str): The key to get data for. + default (any, optional): The default value to return if key is not found. + + Returns: + any: The data stored under the key. + + """ + return self.data.get(key, default) + def abandon(self): """ Call when quest is abandoned. """ - self.is_abandoned = True + self.add_data("status", QuestStatus.ABANDONED) + self.questhandler.clean_quest_data(self.key) self.cleanup() def complete(self): @@ -109,7 +161,8 @@ class EvAdventureQuest: Call this to end the quest. """ - self.is_completed = True + self.add_data("status", QuestStatus.COMPLETED) + self.questhandler.clean_quest_data(self.key) self.cleanup() def progress(self, *args, **kwargs): @@ -122,8 +175,7 @@ class EvAdventureQuest: *args, **kwargs: Will be passed into the step method. """ - if not (self.is_completed or self.is_abandoned): - getattr(self, f"step_{self.current_step}")(*args, **kwargs) + return getattr(self, f"step_{self.current_step}")(*args, **kwargs) def help(self): """ @@ -162,8 +214,9 @@ class EvAdventureQuest: def cleanup(self): """ This is called both when completing the quest, or when it is abandoned prematurely. - Make sure to cleanup any quest-related data stored when following the quest. + This is for cleaning up any extra state that were set during the quest (stuff in self.data + is automatically cleaned up) """ pass @@ -185,27 +238,84 @@ class EvAdventureQuestHandler: quest_storage_attribute_key = "_quests" quest_storage_attribute_category = "evadventure" + quest_data_attribute_template = "_quest_data_{quest_key}" + quest_data_attribute_category = "evadventure" + def __init__(self, obj): self.obj = obj self.do_save = False + self.quests = {} + self.quest_classes = {} self._load() def _load(self): - self.storage = self.obj.attributes.get( + self.quest_classes = self.obj.attributes.get( self.quest_storage_attribute_key, category=self.quest_storage_attribute_category, default={}, ) + # instantiate all quests + for quest_key, quest_class in self.quest_classes.items(): + self.quests[quest_key] = quest_class(self.obj, self.load_quest_data(quest_key)) def _save(self): self.obj.attributes.add( self.quest_storage_attribute_key, - self.storage, + self.quest_classes, category=self.quest_storage_attribute_category, ) self._load() # important self.do_save = False + def save_quest_data(self, quest_key, data): + """ + Save data for a quest. We store this on the quester as well as updating the quest itself. + + Args: + data (dict): The data to store. This is commonly flags or other data needed to track the + quest. + + """ + quest = self.get(quest_key) + if quest: + quest.data = data + self.obj.attributes.add( + self.quest_data_attribute_template.format(quest_key=quest_key), + data, + category=self.quest_data_attribute_category, + ) + + def load_quest_data(self, quest_key): + """ + Load data for a quest. + + Args: + quest_key (str): The quest to load data for. + + Returns: + dict: The data stored for the quest. + + """ + return self.obj.attributes.get( + self.quest_data_attribute_template.format(quest_key=quest_key), + category=self.quest_data_attribute_category, + default={}, + ) + + def clean_quest_data(self, quest_key): + """ + Remove data for a quest. + + Args: + quest_key (str): The quest to remove data for. + + """ + self.obj.attributes.remove( + self.quest_data_attribute_template.format(quest_key=quest_key), + category=self.quest_data_attribute_category, + ) + + def has(self, quest_key): """ Check if a given quest is registered with the Character. @@ -218,7 +328,7 @@ class EvAdventureQuestHandler: bool: If the character is following this quest or not. """ - return bool(self.storage.get(quest_key)) + return bool(self.quests.get(quest_key)) def get(self, quest_key): """ @@ -232,17 +342,17 @@ class EvAdventureQuestHandler: Character is not on this quest. """ - return self.storage.get(quest_key) + return self.quests.get(quest_key) - def add(self, quest): + def add(self, quest_class): """ Add a new quest Args: - quest (EvAdventureQuest): The quest class to start. + quest_class (EvAdventureQuest): The quest class to start. """ - self.storage[quest.key] = quest(self.obj) + self.quest_classes[quest_class.key] = quest_class self._save() def remove(self, quest_key): @@ -253,10 +363,11 @@ class EvAdventureQuestHandler: quest_key (str): The quest to remove. """ - quest = self.storage.pop(quest_key, None) + quest = self.quests.pop(quest_key, None) if not quest.is_completed: # make sure to cleanup quest.abandon() + self.quest_classes.pop(quest_key, None) self._save() def get_help(self, quest_key=None): @@ -274,10 +385,10 @@ class EvAdventureQuestHandler: """ help_texts = [] - if quest_key in self.storage: - quests = [self.storage[quest_key]] + if quest_key in self.quests: + quests = [self.quests[quest_key]] else: - quests = self.storage.values() + quests = self.quests.values() for quest in quests: help_texts.append(f"|c{quest.key}|n\n {quest.desc}\n\n - {quest.help()}") @@ -293,10 +404,10 @@ class EvAdventureQuestHandler: *args, **kwargs: Will be passed into each quest's `progress` call. """ - if quest_key in self.storage: - quests = [self.storage[quest_key]] + if quest_key in self.quests: + quests = [self.quests[quest_key]] else: - quests = self.storage.values() + quests = self.quests.values() for quest in quests: quest.progress(*args, **kwargs) diff --git a/evennia/contrib/tutorials/evadventure/tests/test_quests.py b/evennia/contrib/tutorials/evadventure/tests/test_quests.py index 5a65725044..2a6fe61021 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_quests.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_quests.py @@ -108,7 +108,7 @@ class EvAdventureQuestTest(EvAdventureMixin, BaseEvenniaTest): self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - You need to do A first."]) # help for finished quest - self._get_quest().is_completed = True + self._get_quest().complete() help_txt = self.character.quests.get_help() self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - This quest is completed!"]) From aed4c70583800ce01cb8a1ab29cc0e1604f70369 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sun, 31 Mar 2024 01:25:50 +1100 Subject: [PATCH 09/68] Fix editor echo not displaying correctly for lines with whitespace --- evennia/utils/eveditor.py | 2 +- evennia/utils/tests/test_eveditor.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 2df8574368..32faade5bb 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -414,7 +414,7 @@ class CmdLineInput(CmdEditorBase): self.caller.msg("|b%02i|||n (|g%s|n) %s" % (cline, indent, raw(line))) else: - self.caller.msg("|b%02i|||n %s" % (cline, raw(self.args))) + self.caller.msg("|b%02i|||n %s" % (cline, raw(line))) class CmdEditorGroup(CmdEditorBase): diff --git a/evennia/utils/tests/test_eveditor.py b/evennia/utils/tests/test_eveditor.py index b7333d6810..9e433f6bff 100644 --- a/evennia/utils/tests/test_eveditor.py +++ b/evennia/utils/tests/test_eveditor.py @@ -168,13 +168,13 @@ class TestEvEditor(BaseEvenniaCommandTest): eveditor.CmdLineInput(), 'First test "line".', raw_string='First test "line".', - msg='01First test "line" .', + msg='01First test "line".', ) self.call( eveditor.CmdLineInput(), "Second 'line'.", raw_string="Second 'line'.", - msg="02Second 'line' .", + msg="02Second 'line'.", ) self.assertEqual( self.char1.ndb._eveditor.get_buffer(), "First test \"line\".\nSecond 'line'." From f8b9ef231140ca1e78cef85efb681b9a681698d1 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 30 Mar 2024 15:56:36 +0100 Subject: [PATCH 10/68] Update CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b1034a9e..caf9268ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,23 @@ adding line breaks in hook returns. (InspectorCaracal) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) +- [Fix][pull3453]: Object aliases not showing in search multi-match + disambiguation display (chiizujin) +- [Fix][pull3455]: `sethelp/edit ` without a `= text` created a `None` + entry that would lose the edit. (chiiziujin) +- [Fix][pull3456]: `format_grid` utility used for `help` command caused commands + to disappear for wider client widths (chiizujin) +- [Fix][pull3457]: Help topic categories with different case would appear as + duplicates (chiizujin) - Doc: Added Beginner Tutorial lessons for AI and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) [pull3421]: https://github.com/evennia/evennia/pull/3421 [pull3446]: https://github.com/evennia/evennia/pull/3446 +[pull3453]: https://github.com/evennia/evennia/pull/3453 +[pull3455]: https://github.com/evennia/evennia/pull/3455 +[pull3456]: https://github.com/evennia/evennia/pull/3456 +[pull3457]: https://github.com/evennia/evennia/pull/3457 ## Evennia 4.0.0 From 9f9d58bedc5dc150a0957491970d5aa6770caa96 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sun, 31 Mar 2024 02:27:30 +1100 Subject: [PATCH 11/68] Add unit test for sethelp/category --- evennia/commands/default/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 8977c890d3..3a272e04f7 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -197,6 +197,12 @@ class TestHelp(BaseEvenniaCommandTest): cmdset=CharacterCmdSet(), ) self.call(help_module.CmdHelp(), "testhelp", "Help for testhelp", cmdset=CharacterCmdSet()) + self.call( + help_module.CmdSetHelp(), + "/category testhelp = misc", + "Category for entry 'testhelp' changed to 'misc'.", + cmdset=CharacterCmdSet(), + ) @parameterized.expand( [ From 5fcccca7d1c93bc7ac0a64b3d4ee46fad1367960 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 30 Mar 2024 16:43:10 +0100 Subject: [PATCH 12/68] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index caf9268ce7..f08c63febf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - [Feature][pull3421]: New `utils.compress_whitespace` utility used with default object's `.format_appearance` to make it easier to overload without adding line breaks in hook returns. (InspectorCaracal) +- [Feature][pull3458]: New `sethelp/category` switch to change a help topic's + category after it was created (chiizujin) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) - [Fix][pull3453]: Object aliases not showing in search multi-match @@ -24,6 +26,7 @@ [pull3455]: https://github.com/evennia/evennia/pull/3455 [pull3456]: https://github.com/evennia/evennia/pull/3456 [pull3457]: https://github.com/evennia/evennia/pull/3457 +[pull3458]: https://github.com/evennia/evennia/pull/3458 ## Evennia 4.0.0 From 540729e2b43683a00b3e0e881b1f57f0d3e023a6 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:38:13 -0600 Subject: [PATCH 13/68] update crafting contrib tests --- .../contrib/game_systems/crafting/tests.py | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/evennia/contrib/game_systems/crafting/tests.py b/evennia/contrib/game_systems/crafting/tests.py index 41ae4412bf..9e42695d1d 100644 --- a/evennia/contrib/game_systems/crafting/tests.py +++ b/evennia/contrib/game_systems/crafting/tests.py @@ -78,7 +78,7 @@ class TestCraftingRecipeBase(BaseEvenniaTestCase): """Test messaging to crafter""" self.recipe.msg("message") - self.crafter.msg.assert_called_with("message", {"type": "crafting"}) + self.crafter.msg.assert_called_with(text="message", {"type": "crafting"})) def test_pre_craft(self): """Test validating hook""" @@ -206,7 +206,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase): self.assertEqual(result[0].key, "Result1") self.assertEqual(result[0].tags.all(), ["result1", "resultprot"]) self.crafter.msg.assert_called_with( - recipe.success_message.format(outputs="Result1"), {"type": "crafting"} + text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"}) ) # make sure consumables are gone @@ -251,8 +251,10 @@ class TestCraftingRecipe(BaseEvenniaTestCase): result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"), - {"type": "crafting"}, + text=( + recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"), + {"type": "crafting"}, + ) ) # make sure consumables are still there @@ -269,8 +271,10 @@ class TestCraftingRecipe(BaseEvenniaTestCase): result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"), - {"type": "crafting"}, + text=( + recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"), + {"type": "crafting"}, + ) ) # make sure consumables are still there @@ -293,8 +297,10 @@ class TestCraftingRecipe(BaseEvenniaTestCase): self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"), - {"type": "crafting"}, + text=( + recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"), + {"type": "crafting"}, + ) ) # make sure consumables are deleted even though we failed @@ -317,10 +323,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase): result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_tool_excess_message.format( - outputs="Result1", excess=wrong.get_display_name(looker=self.crafter) - ), - {"type": "crafting"}, + text=( + recipe.error_tool_excess_message.format( + outputs="Result1", excess=wrong.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) @@ -342,10 +350,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase): result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_tool_excess_message.format( - outputs="Result1", excess=tool3.get_display_name(looker=self.crafter) - ), - {"type": "crafting"}, + text=( + recipe.error_tool_excess_message.format( + outputs="Result1", excess=tool3.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) ) # make sure consumables are still there @@ -369,10 +379,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase): result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_consumable_excess_message.format( - outputs="Result1", excess=cons4.get_display_name(looker=self.crafter) - ), - {"type": "crafting"}, + text=( + recipe.error_consumable_excess_message.format( + outputs="Result1", excess=cons4.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) ) # make sure consumables are still there @@ -396,7 +408,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase): result = recipe.craft() self.assertTrue(result) self.crafter.msg.assert_called_with( - recipe.success_message.format(outputs="Result1"), {"type": "crafting"} + text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"}) ) # make sure consumables are gone @@ -419,7 +431,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase): result = recipe.craft() self.assertTrue(result) self.crafter.msg.assert_called_with( - recipe.success_message.format(outputs="Result1"), {"type": "crafting"} + text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"}) ) # make sure consumables are gone @@ -439,10 +451,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase): result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_tool_order_message.format( - outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter) - ), - {"type": "crafting"}, + text=( + recipe.error_tool_order_message.format( + outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) ) # make sure consumables are still there @@ -462,10 +476,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase): result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_consumable_order_message.format( - outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter) - ), - {"type": "crafting"}, + text=( + recipe.error_consumable_order_message.format( + outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) ) # make sure consumables are still there From a0754d9f1a1ba0131c809e3872e3c2e1ad87c117 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Sat, 30 Mar 2024 14:14:30 -0600 Subject: [PATCH 14/68] add missing paren --- evennia/contrib/game_systems/crafting/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/game_systems/crafting/tests.py b/evennia/contrib/game_systems/crafting/tests.py index 9e42695d1d..7dff337035 100644 --- a/evennia/contrib/game_systems/crafting/tests.py +++ b/evennia/contrib/game_systems/crafting/tests.py @@ -78,7 +78,7 @@ class TestCraftingRecipeBase(BaseEvenniaTestCase): """Test messaging to crafter""" self.recipe.msg("message") - self.crafter.msg.assert_called_with(text="message", {"type": "crafting"})) + self.crafter.msg.assert_called_with(text=("message", {"type": "crafting"})) def test_pre_craft(self): """Test validating hook""" From 8f14cef6e60efdbf51426531a27cdb20b0eb96f9 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Sat, 30 Mar 2024 14:36:01 -0600 Subject: [PATCH 15/68] missed another msg test --- evennia/contrib/game_systems/crafting/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/game_systems/crafting/tests.py b/evennia/contrib/game_systems/crafting/tests.py index 7dff337035..201fa487b6 100644 --- a/evennia/contrib/game_systems/crafting/tests.py +++ b/evennia/contrib/game_systems/crafting/tests.py @@ -235,7 +235,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase): self.assertEqual(result[0].key, "Result1") self.assertEqual(result[0].tags.all(), ["result1", "resultprot"]) self.crafter.msg.assert_called_with( - recipe.success_message.format(outputs="Result1"), {"type": "crafting"} + text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"}) ) # make sure consumables are gone From f1f1f6219a806e9a31d65cea7ef86b0a334f6fe2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 30 Mar 2024 23:42:16 +0100 Subject: [PATCH 16/68] Implemented EvAdventure quest code+tutorial lesson --- CHANGELOG.md | 2 +- docs/source/Coding/Changelog.md | 16 + .../Part3/Beginner-Tutorial-AI.md | 2 + .../Part3/Beginner-Tutorial-Combat-Base.md | 4 +- .../Beginner-Tutorial-Combat-Turnbased.md | 2 +- .../Part3/Beginner-Tutorial-Quests.md | 399 +++++++++++++++++- .../grid/extended_room/extended_room.py | 13 +- .../contrib/tutorials/evadventure/enums.py | 12 - .../contrib/tutorials/evadventure/quests.py | 329 +++++++-------- .../evadventure/tests/test_quests.py | 48 +-- 10 files changed, 600 insertions(+), 227 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f08c63febf..a671619315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ to disappear for wider client widths (chiizujin) - [Fix][pull3457]: Help topic categories with different case would appear as duplicates (chiizujin) -- Doc: Added Beginner Tutorial lessons for AI and Procedural dungeon (Griatch) +- Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) [pull3421]: https://github.com/evennia/evennia/pull/3421 diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 3740618239..a671619315 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -5,12 +5,28 @@ - [Feature][pull3421]: New `utils.compress_whitespace` utility used with default object's `.format_appearance` to make it easier to overload without adding line breaks in hook returns. (InspectorCaracal) +- [Feature][pull3458]: New `sethelp/category` switch to change a help topic's + category after it was created (chiizujin) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) +- [Fix][pull3453]: Object aliases not showing in search multi-match + disambiguation display (chiizujin) +- [Fix][pull3455]: `sethelp/edit ` without a `= text` created a `None` + entry that would lose the edit. (chiiziujin) +- [Fix][pull3456]: `format_grid` utility used for `help` command caused commands + to disappear for wider client widths (chiizujin) +- [Fix][pull3457]: Help topic categories with different case would appear as + duplicates (chiizujin) +- Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) [pull3421]: https://github.com/evennia/evennia/pull/3421 [pull3446]: https://github.com/evennia/evennia/pull/3446 +[pull3453]: https://github.com/evennia/evennia/pull/3453 +[pull3455]: https://github.com/evennia/evennia/pull/3455 +[pull3456]: https://github.com/evennia/evennia/pull/3456 +[pull3457]: https://github.com/evennia/evennia/pull/3457 +[pull3458]: https://github.com/evennia/evennia/pull/3458 ## Evennia 4.0.0 diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.md index 115c61faf4..4e35e2b2c4 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.md @@ -40,6 +40,8 @@ You can find an AIHandler implemented in `evennia/contrib/tutorials`, in [evadve ``` This is the core logic for managing AI states. Create a new file `evadventure/ai.py`. +> Create a new file `evadventure/ai.py`. + ```{code-block} python :linenos: :emphasize-lines: 10,11-13,16,23 diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Combat-Base.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Combat-Base.md index 12d15ff8c9..b5611979af 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Combat-Base.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Combat-Base.md @@ -16,7 +16,7 @@ We will design a base combat system that supports both styles. > Create a new module `evadventure/combat_base.py` ```{sidebar} -In [evennia/contrib/tutorials/evadventure/combat_base.py](evennia.contrib.tutorials.evadventure.combat_base) you'll find a complete implementation of the base combat module. +Under `evennia/contrib/tutorials/evadventure/`, in [combat_base.py](evennia.contrib.tutorials.evadventure.combat_base) you'll find a complete implementation of the base combat module. ``` Our "Combat Handler" will handle the administration around combat. It needs to be _persistent_ (even is we reload the server your combat should keep going). @@ -718,7 +718,7 @@ We rely on the [Equipment handler](./Beginner-Tutorial-Equipment.md) we created > Create a module `evadventure/tests/test_combat.py`. ```{sidebar} -See [evennia/contrib/tutorials/evadventure/tests/test_combat.py](evennia.contrib.tutorials.evadventure.tests.test_combat) for ready-made combat unit tests. +Look under `evennia/contrib/tutorials/evadventure/`, in [tests/test_combat.py](evennia.contrib.tutorials.evadventure.tests.test_combat) for ready-made combat unit tests. ``` Unit testing the combat base classes can seem impossible because we have not yet implemented most of it. We can however get very far by the use of [Mocks](https://docs.python.org/3/library/unittest.mock.html). The idea of a Mock is that you _replace_ a piece of code with a dummy object (a 'mock') that can be called to return some specific value. diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Combat-Turnbased.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Combat-Turnbased.md index c456cd51d2..9932a5a5a1 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Combat-Turnbased.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Combat-Turnbased.md @@ -80,7 +80,7 @@ The advantage of using a menu is that you have all possible actions directly ava ## General Principle ```{sidebar} -An example of an implemented Turnbased combat system can be found in [evennia/contrib/tutorials/evadventure/combat_turnbased.py](evennia.contrib.tutorials.evadventure.combat_turnbased). +An example of an implemented Turnbased combat system can be found under `evennia/contrib/tutorials/evadventure/`, in [combat_turnbased.py](evennia.contrib.tutorials.evadventure.combat_turnbased). ``` Here is the general principle of the Turnbased combat handler: diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md index 2398ff20fe..e4aee29fe6 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md @@ -2,8 +2,401 @@ A _quest_ is a common feature of games. From classic fetch-quests like retrieving 10 flowers to complex quest chains involving drama and intrigue, quests need to be properly tracked in our game. -A quest follows a specific development: +A quest follows a specific development: 1. The quest is _started_. This normally involves the player accepting the quest, from a quest-giver, job board or other source. But the quest could also be thrust on the player ("save the family from the burning house before it collapses!") -2. A quest may consist of one or more 'steps'. Each step has its own set of finish conditions. -3. At suitable times the quest is _checked_. This could happen on a timer or when trying to 'hand in' the quest. When checking, the current 'step' is checked against its finish conditions. If ok, that step is closed and the next step is checked until it either hits a step that is not yet complete, or there are no more steps, in which case the entire quest is complete. \ No newline at end of file +2. Once a quest has been accepted and assigned to a character, it is either either `Started` (that is, 'in progress'), `Abandoned`, `Failed` or `Complete`. +3. A quest may consist of one or more 'steps'. Each step has its own set of finish conditions. +4. At suitable times the quest's _progress_ is checked. This could happen on a timer or when trying to 'hand in' the quest. When checking, the current 'step' is checked against its finish conditions. If ok, that step is closed and the next step is checked until it either hits a step that is not yet complete, or there are no more steps, in which case the entire quest is complete. + +```{sidebar} +An example implementation of quests is found under `evennia/contrib/tutorials`, in [evadvanture/quests.py](evennia.contrib.tutorials.evadventure.quests). +``` +To represent quests in code, we need +- A convenient flexible way to code how we check the status and current steps of the quest. We want this scripting to be as flexible as possible. Ideally we want to be able to code the quests's logic in full Python. +- Persistence. The fact that we accepted the quest, as well as its status and other flags must be saved in the database and survive a server reboot. + +We'll accomplish this using two pieces of Python code: +- `EvAdventureQuest`: A Python class with helper methods that we can call to check current quest status, figure if a given quest-step is complete or not. We will create and script new quests by simply inheriting from this base class and implement new methods on it in a standardized way. +- `EvAdventureQuestHandler` will sit 'on' each Character as `character.quests`. It will hold all `EvAdventureQuest`s that the character is or has been involved in. It is also responsible for storing quest state using [Attributes](../../../Components/Attributes.md) on the Character. + +## The Quest Handler + +> Create a new module `evadventure/quests.py`. + +We saw the implementation of an on-object handler back in the [lesson about NPC and monster AI](./Beginner-Tutorial-AI.md#the-aihandler) (the `AIHandler`). + +```{code-block} python +:linenos: +:emphasize-lines: 9,10,11,14-18,21,24-28 +# in evadventure/quests.py + +class EvAdventureQuestHandler: + quest_storage_attribute_key = "_quests" + quest_storage_attribute_category = "evadventure" + + def __init__(self, obj): + self.obj = obj + self.quest_classes = {} + self.quests = {} + self._load() + + def _load(self): + self.quest_classes = self.obj.attributes.get( + self.quest_storage_attribute_key, + category=self.quest_storage_attribute_category, + default={}, + ) + # instantiate all quests + for quest_key, quest_class in self.quest_classes.items(): + self.quests[quest_key] = quest_class(self.obj) + + def _save(self): + self.obj.attributes.add( + self.quest_storage_attribute_key, + self.quest_classes, + category=self.quest_storage_attribute_category, + ) + + def get(self, quest_key): + return self.quests.get(quest_key) + + def all(self): + return list(self.quests.values()) + + def add(self, quest_class): + self.quest_classes[quest_class.key] = quest_class + self.quests[quest_class.key] = quest_class(self.obj) + self._save() + + def remove(self, quest_key): + quest = self.quests.pop(quest_key, None) + self.quest_classes.pop(quest_key, None) + self.quests.pop(quest_key, None) + self._save() + +``` + +```{sidebar} Persistent handler pattern +Persistent handlers are commonly used throughout Evennia. You can read more about them in the [Making a Persistent object Handler](../../Tutorial-Persistent-Handler.md) tutorial. +``` +- **Line 9**: We know that the quests themselves will be Python classes inheriting from `EvAdventureQuest` (which we haven't created yet). We will store those classes in `self.quest_classes` on the handler. Note that there is a difference between a class and an _instance_ of a class! The class cannot hold any _state_ on its own, such as the status of that quest is for this particular character. The class only holds python code. +- **Line 10**: We set aside another property on the handler - `self.quest` This is dictionary that will hold `EvAdventureQuest` _instances_. +- **Line 11**: Note that we call the `self._load()` method here, this loads up data from the database whenever this handler is accessed. +- **Lines 14-18**: We use `self.obj.attributes.get` to fetch an [Attribute](../../../Components/Attributes.md) on the Character named `_quests` and with a category of `evadventure`. If it doesn't exist yet (because we never started any quests), we just return an empty dict. +- **Line 21**: Here we loop over all the classes and instantiate them. We haven't defined how these quest-classes look yet, but by instantiating them with `self.obj` (the Character) we should be covered - from the Character class the quest will be able to get to everything else (this handler itself will be accessible as `obj.quests` from that quest instance after all). +- **Line 24**: Here we do the corresponding save operation. + +The rest of the handler are just access methods for getting, adding and removing quests from the handler. We make one assumption in those code, namely that the quest class has a property `.key` being the unique quest-name. + +This is how it would be used in practice: + +```python +# in some questing code + +from evennia import search_object +from evadventure import quests + +class EvAdventureSuperQuest(quests.EvAdventureQuest): + key = "superquest" + # quest implementation here + +def start_super_quest(character): + character.quests.add(EvAdventureSuperQuest) + +``` +```{sidebar} What can be saved in Attributes? +For more details, see [the Attributes documentation](../../../Components/Attributes.md#what-types-of-data-can-i-save-in-an-attribute) on the matter. +``` +We chose to store classes and not instances of classes above. The reason for this has to do with what can be stored in a database `Attribute` - one limitation of an Attribute is that we can't save a class instance _with other database entities baked inside it_. If we saved quest instances as-is, it's highly likely they'd contain database entities 'hidden' inside them - a reference to the Character, maybe to objects required for the quest to be complete etc. Evennia would fail trying to save that data. +Instead we store only the classes, instantiate those classes with the Character, and let the quest store its state flags separately, like this: + +```python +# in evadventure/quests.py + +class EvAdventureQuestHandler: + + # ... + quest_data_attribute_template = "_quest_data_{quest_key}" + quest_data_attribute_category = "evadventure" + + # ... + + def save_quest_data(self, quest_key): + quest = self.get(quest_key) + if quest: + self.obj.attributes.add( + self.quest_data_attribute_template.format(quest_key=quest_key), + quest.data, + category=self.quest_data_attribute_category, + ) + + def load_quest_data(self, quest_key): + return self.obj.attributes.get( + self.quest_data_attribute_template.format(quest_key=quest_key), + category=self.quest_data_attribute_category, + default={}, + ) + +``` + +This works the same as the `_load` and `_save` methods, except it fetches a property `.data` (this will be a `dict`) on the quest instance and save it. As long as we make sure to call these methods from the quest the quest whenever that `.data` property is changed, all will be well - this is because Attributes know how to properly analyze a `dict` to find and safely serialize any database entities found within. + +Our handler is ready. We created the `EvAdventureCharacter` class back in the [Character lesson](./Beginner-Tutorial-Characters.md) - let's add quest-support to it. + +```python +# in evadventure/characters.py + +# ... + +from evennia.utils import lazy_property +from evadventure.quests import EvAdventureQuestHandler + +class EvAdventureCharacter(LivingMixin, DefaultCharacter): + # ... + + @lazy_property + def quests(self): + return EvAdventureQuestHandler(self) + + # ... + +``` + +We also need a way to represent the quests themselves though! +## The Quest class + + +```{code-block} python +:linenos: +:emphasize-lines: 7,12,13,34-36 +# in evadventure/quests.py + +# ... + +class EvAdventureQuest: + + key = "base-quest" + desc = "Base quest" + start_step = "start" + + def __init__(self, quester): + self.quester = quester + self.data = self.questhandler.load_quest_data(self.key) + self._current_step = self.get_data("current_step") + + if not self.current_step: + self.current_step = self.start_step + + def add_data(self, key, value): + self.data[key] = value + self.questhandler.save_quest_data(self.key) + + def get_data(self, key, default=None): + return self.data.get(key, default) + + def remove_data(self, key): + self.data.pop(key, None) + self.questhandler.save_quest_data(self.key) + + @property + def questhandler(self): + return self.quester.quests + + @property + def current_step(self): + return self._current_step + + @current_step.setter + def current_step(self, step_name): + self._current_step = step_name + self.add_data("current_step", step_name) + +``` + +- **Line 7**: Each class must have a `.key` property unquely identifying the quest. We depend on this in the quest-handler. +- **Line 12**: `quester` (the Character) is passed into this class when it is initiated inside `EvAdventureQuestHandler._load()`. +- **Line 13**: We load the quest data into `self.data` directly using the `questhandler.load_quest-data` method (which in turn loads it from an Attribute on the Character). Note that the `.questhandler` property is defined on **lines 34-36** as a shortcut to get to the handler. + +The `add/get/remove_data` methods are convenient wrappers for getting data in and out of the database using the matching methods on the handler. When we implement a quest we should prefer to use `.get_data`, `add_data` and `remove_data` over manipulating `.data` directly, since the former will make sure to save said that to the database automatically. + +The `current_step` tracks the current quest 'step' we are in; what this means is up to each Quest. We set up convenient properties for setting the `current_state` and also make sure to save it in the data dict as "current_step". + +The quest can have a few possible statuses: "started", "completed", "abandoned" and "failed". We create a few properties and methods for easily control that, while saving everything under the hood: + +```python +# in evadventure/quests.py + +# ... + +class EvAdventureQuest: + + # ... + + @property + def status(self): + return self.get_data("status", "started") + + @status.setter + def status(self, value): + self.add_data("status", value) + + @property + def is_completed(self): + return self.status == "completed" + + @property + def is_abandoned(self): + return self.status == "abandoned" + + @property + def is_failed(self): + return self.status == "failed" + + def complete(self): + self.status = "completed" + + def abandon(self): + self.status = "abandoned" + + def fail(self): + self.status = "failed" + + +``` + +So far we have only added convenience functions for checking statuses. How will the actual "quest" aspect of this work? + +What will happen when the system wants to check the progress of the quest, is that it will call a method `.progress()` on this class. Similarly, to get help for the current step, it will call a method `.help()` + +```python + + start_step = "start" + + # help entries for quests (could also be methods) + help_start = "You need to start first" + help_end = "You need to end the quest" + + def progress(self, *args, **kwargs): + getattr(self, f"step_{self.current_step}")(*args, **kwargs) + + def help(self, *args, **kwargs): + if self.status in ("abandoned", "completed", "failed"): + help_resource = getattr(self, f"help_{self.status}", + f"You have {self.status} this quest.") + else: + help_resource = getattr(self, f"help_{self.current_step}", "No help available.") + + if callable(help_resource): + # the help_* methods can be used to dynamically generate help + return help_resource(*args, **kwargs) + else: + # normally it's just a string + return str(help_resource) + +``` + +```{sidebar} What's with the *args, **kwargs? +These are optional, but allow you to pass extra information into your quest-check. This could be very powerful if you want to add extra context to determine if a quest-step is currently complete or not. +``` +Calling the `.progress(*args, **kwargs)` method will call a method named `step_(*args, **kwargs)` on this class. That is, if we are on the _start_ step, the method called will be `self.step_start(*args, **kwargs)`. Where is this method? It has not been implemented yet! In fact, it's up to us to implement methods like this for each quest. By just adding a correctly added method, we will easily be able to add more steps to a quest. + +Similarly, calling `.help(*args, **kwargs)` will try to find a property `help_`. If this is a callable, it will be called as for example `self.help_start(*args, **kwargs)`. If it is given as a string, then the string will be returned as-is and the `*args, **kwargs` will be ignored. + +### Example quest + +```python +# in some quest module, like world/myquests.py + +from evadventure.quests import EvAdventureQuest + +class ShortQuest(EvAdventureQuest): + + key = "simple-quest" + desc = "A very simple quest." + + def step_start(self, *args, **kwargs): + """Example step!""" + self.quester.msg("Quest started!") + self.current_step = "end" + + def step_end(self, *args, **kwargs): + if not self.is_completed: + self.quester.msg("Quest ended!") + self.complete() + +``` + +This is a very simple quest that will resolve on its own after two `.progress()` checks. Here's the full life cycle of this quest: + +```python +# in some module somewhere, using evennia shell or in-game using py + +from evennia import search_object +from world.myquests import ShortQuest + +character = search_object("MyCharacterName")[0] +character.quests.add(ShortQuest) + +# this will echo "Quest started!" to character +character.quests.get("short-quest").progress() +# this will echo "Quest ended!" to character +character.quests.get("short-quest").progress() + +``` + +### A useful Command + +The player must know which quests they have and be able to inspect them. Here's a simple `quests` command to handle this: + +```python +# in evadventure/quests.py + +class CmdQuests(Command): + """ + List all quests and their statuses as well as get info about the status of + a specific quest. + + Usage: + quests + quest + + """ + key = "quests" + aliases = ["quest"] + + def parse(self): + self.quest_name = self.args.strip() + + def func(self): + if self.quest_name: + quest = self.caller.quests.get(self.quest_name) + if not quest: + self.msg(f"Quest {self.quest_name} not found.") + return + self.msg(f"Quest {quest.key}: {quest.status}\n{quest.help()}") + return + + quests = self.caller.quests.all() + if not quests: + self.msg("No quests.") + return + + for quest in quests: + self.msg(f"Quest {quest.key}: {quest.status}") +``` + +Add this to the `CharacterCmdSet` in `mygame/commands/default_cmdsets.py`. Follow the [Adding a command lesson](../Part1/Beginner-Tutorial-Adding-Commands.md#add-the-echo-command-to-the-default-cmdset) if you are unsure how to do this. Reload and if you are playing as an `EvAdventureCharacter` you should be able to use `quests` to view your quests. + +## Testing + +> Create a new folder `evadventure/tests/test_quests.py`. + +```{sidebar} +An example test suite for quests is found in `evennia/contrib/tutorials/evadventure`, as [tests/test_quests.py](evennia.contrib.tutorials.evadventure.tests.test_quests). +``` +Testing of the quests means creating a test character, making a dummy quest, add it to the character's quest handler and making sure all methods work correcly. Create the testing quest so that it will automatically step forward when calling `.progress()`, so you can make sure it works as intended. + +## Conclusions + +What we created here is just the framework for questing. The actual complexity will come when creating the quests themselves (that is, implementing the `step_(*args, **kwargs)` methods), which is something we'll get to later, in [Part 4](../Part4/Beginner-Tutorial-Part4-Overview.md) of this tutorial. \ No newline at end of file diff --git a/evennia/contrib/grid/extended_room/extended_room.py b/evennia/contrib/grid/extended_room/extended_room.py index b23338b709..97dec6056e 100644 --- a/evennia/contrib/grid/extended_room/extended_room.py +++ b/evennia/contrib/grid/extended_room/extended_room.py @@ -47,17 +47,8 @@ from collections import deque from django.conf import settings from django.db.models import Q - -from evennia import ( - CmdSet, - DefaultRoom, - EvEditor, - FuncParser, - InterruptCommand, - default_cmds, - gametime, - utils, -) +from evennia import (CmdSet, DefaultRoom, EvEditor, FuncParser, + InterruptCommand, default_cmds, gametime, utils) from evennia.typeclasses.attributes import AttributeProperty from evennia.utils.utils import list_to_string, repeat diff --git a/evennia/contrib/tutorials/evadventure/enums.py b/evennia/contrib/tutorials/evadventure/enums.py index 4ec4405e9f..6ba77d82f9 100644 --- a/evennia/contrib/tutorials/evadventure/enums.py +++ b/evennia/contrib/tutorials/evadventure/enums.py @@ -84,15 +84,3 @@ class ObjType(Enum): MAGIC = "magic" QUEST = "quest" TREASURE = "treasure" - - -class QuestStatus(Enum): - """ - Quest status - - """ - - STARTED = "started" - COMPLETED = "completed" - ABANDONED = "abandoned" - FAILED = "failed" diff --git a/evennia/contrib/tutorials/evadventure/quests.py b/evennia/contrib/tutorials/evadventure/quests.py index 1e1f2780da..38476a058e 100644 --- a/evennia/contrib/tutorials/evadventure/quests.py +++ b/evennia/contrib/tutorials/evadventure/quests.py @@ -2,19 +2,17 @@ A simple quest system for EvAdventure. A quest is represented by a quest-handler sitting as -.quest on a Character. Individual Quests are objects -that track the state and can have multiple steps, each -of which are checked off during the quest's progress. - -The player can use the quest handler to track the -progress of their quests. +`.quests` on a Character. Individual Quests are child classes of `EvAdventureQuest` with +methods for each step of the quest. The quest handler can add, remove, and track the progress +by calling the `progress` method on the quest. Persistent changes are stored on the quester +using the `add_data` and `get_data` methods with an Attribute as storage backend. A quest ending can mean a reward or the start of another quest. """ -from .enums import QuestStatus +from evennia import Command class EvAdventureQuest: @@ -47,6 +45,9 @@ class EvAdventureQuest: self.current_step = "end" self.progress() + def step_B(self, *args, **kwargs): + + def step_end(self, *args, **kwargs): if len(self.quester.contents) > 4: self.quester.msg("Quest complete!") @@ -54,62 +55,23 @@ class EvAdventureQuest: ``` """ - key = "basequest" + key = "base quest" desc = "This is the base quest class" start_step = "start" - completed_text = "This quest is completed!" - abandoned_text = "This quest is abandoned." # help entries for quests (could also be methods) help_start = "You need to start first" help_end = "You need to end the quest" - def __init__(self, quester, data=None): - if " " in self.key: - raise TypeError("The Quest name must not have spaces in it.") - + def __init__(self, quester): self.quester = quester - self.data = data or dict() + self.data = self.questhandler.load_quest_data(self.key) self._current_step = self.get_data("current_step") if not self.current_step: self.current_step = self.start_step - @property - def questhandler(self): - return self.quester.quests - - @property - def current_step(self): - return self._current_step - - @current_step.setter - def current_step(self, step_name): - self._current_step = step_name - self.add_data("current_step", step_name) - self.questhandler.do_save = True - - @property - def status(self): - return self.get_data("status", QuestStatus.STARTED) - - @status.setter - def status(self, value): - self.add_data("status", value) - - @property - def is_completed(self): - return self.status == QuestStatus.COMPLETED - - @property - def is_abandoned(self): - return self.status == QuestStatus.ABANDONED - - @property - def is_failed(self): - return self.status == QuestStatus.FAILED - def add_data(self, key, value): """ Add data to the quest. This saves it permanently. @@ -120,18 +82,7 @@ class EvAdventureQuest: """ self.data[key] = value - self.questhandler.save_quest_data(self.key, self.data) - - def remove_data(self, key): - """ - Remove data from the quest permanently. - - Args: - key (str): The key to remove. - - """ - self.data.pop(key, None) - self.questhandler.save_quest_data(self.key, self.data) + self.questhandler.save_quest_data(self.key) def get_data(self, key, default=None): """ @@ -147,23 +98,70 @@ class EvAdventureQuest: """ return self.data.get(key, default) - def abandon(self): + def remove_data(self, key): """ - Call when quest is abandoned. + Remove data from the quest permanently. + + Args: + key (str): The key to remove. """ - self.add_data("status", QuestStatus.ABANDONED) - self.questhandler.clean_quest_data(self.key) - self.cleanup() + self.data.pop(key, None) + self.questhandler.save_quest_data(self.key) + + @property + def questhandler(self): + return self.quester.quests + + @property + def current_step(self): + return self._current_step + + @current_step.setter + def current_step(self, step_name): + self._current_step = step_name + self.add_data("current_step", step_name) + + @property + def status(self): + return self.get_data("status", "started") + + @status.setter + def status(self, value): + self.add_data("status", value) + + @property + def is_completed(self): + return self.status == "completed" + + @property + def is_abandoned(self): + return self.status == "abandoned" + + @property + def is_failed(self): + return self.status == "failed" def complete(self): """ - Call this to end the quest. + Complete the quest. """ - self.add_data("status", QuestStatus.COMPLETED) - self.questhandler.clean_quest_data(self.key) - self.cleanup() + self.status = "completed" + + def abandon(self): + """ + Abandon the quest. + + """ + self.status = "abandoned" + + def fail(self): + """ + Fail the quest. + + """ + self.status = "failed" def progress(self, *args, **kwargs): """ @@ -174,34 +172,38 @@ class EvAdventureQuest: Args: *args, **kwargs: Will be passed into the step method. - """ - return getattr(self, f"step_{self.current_step}")(*args, **kwargs) + Notes: + `self.quester` is available as the character following the quest. - def help(self): + """ + getattr(self, f"step_{self.current_step}")(*args, **kwargs) + + def help(self, *args, **kwargs): """ This is used to get help (or a reminder) of what needs to be done to complete the current - quest-step. + quest-step. It will look for a `help_` method or string attribute on the quest. + + Args: + *args, **kwargs: Will be passed into any help_* method. Returns: str: The help text for the current step. """ - if self.is_completed: - return self.completed_text - if self.is_abandoned: - return self.abandoned_text + if self.status in ("abandoned", "completed", "failed"): + help_resource = getattr(self, f"help_{self.status}", + f"You have {self.status} this quest.") + else: + help_resource = getattr(self, f"help_{self.current_step}", "No help available.") - help_resource = ( - getattr(self, f"help_{self.current_step}", None) - or "You need to {self.current_step} ..." - ) if callable(help_resource): - # the help_ can be a method to call - return help_resource() + # the help_* methods can be used to dynamically generate help + return help_resource(*args, **kwargs) else: # normally it's just a string return str(help_resource) + # step methods and hooks def step_start(self, *args, **kwargs): @@ -243,7 +245,6 @@ class EvAdventureQuestHandler: def __init__(self, obj): self.obj = obj - self.do_save = False self.quests = {} self.quest_classes = {} self._load() @@ -256,7 +257,7 @@ class EvAdventureQuestHandler: ) # instantiate all quests for quest_key, quest_class in self.quest_classes.items(): - self.quests[quest_key] = quest_class(self.obj, self.load_quest_data(quest_key)) + self.quests[quest_key] = quest_class(self.obj) def _save(self): self.obj.attributes.add( @@ -264,57 +265,6 @@ class EvAdventureQuestHandler: self.quest_classes, category=self.quest_storage_attribute_category, ) - self._load() # important - self.do_save = False - - def save_quest_data(self, quest_key, data): - """ - Save data for a quest. We store this on the quester as well as updating the quest itself. - - Args: - data (dict): The data to store. This is commonly flags or other data needed to track the - quest. - - """ - quest = self.get(quest_key) - if quest: - quest.data = data - self.obj.attributes.add( - self.quest_data_attribute_template.format(quest_key=quest_key), - data, - category=self.quest_data_attribute_category, - ) - - def load_quest_data(self, quest_key): - """ - Load data for a quest. - - Args: - quest_key (str): The quest to load data for. - - Returns: - dict: The data stored for the quest. - - """ - return self.obj.attributes.get( - self.quest_data_attribute_template.format(quest_key=quest_key), - category=self.quest_data_attribute_category, - default={}, - ) - - def clean_quest_data(self, quest_key): - """ - Remove data for a quest. - - Args: - quest_key (str): The quest to remove data for. - - """ - self.obj.attributes.remove( - self.quest_data_attribute_template.format(quest_key=quest_key), - category=self.quest_data_attribute_category, - ) - def has(self, quest_key): """ @@ -344,6 +294,16 @@ class EvAdventureQuestHandler: """ return self.quests.get(quest_key) + def all(self): + """ + Get all quests stored on character. + + Returns: + list: All quests stored on character. + + """ + return list(self.quests.values()) + def add(self, quest_class): """ Add a new quest @@ -353,6 +313,7 @@ class EvAdventureQuestHandler: """ self.quest_classes[quest_class.key] = quest_class + self.quests[quest_class.key] = quest_class(self.obj) self._save() def remove(self, quest_key): @@ -368,50 +329,74 @@ class EvAdventureQuestHandler: # make sure to cleanup quest.abandon() self.quest_classes.pop(quest_key, None) + self.quests.pop(quest_key, None) self._save() - def get_help(self, quest_key=None): + def save_quest_data(self, quest_key): """ - Get help text for a quest or for all quests. The help text is - a combination of the description of the quest and the help-text - of the current step. + Save data for a quest. We store this on the quester as well as updating the quest itself. Args: - quest_key (str, optional): The quest-key. If not given, get help for all - quests in handler. + quest_key (str): The quest to save data for. The data is assumed to be stored on the + quest as `.data` (a dict). + + """ + quest = self.get(quest_key) + if quest: + self.obj.attributes.add( + self.quest_data_attribute_template.format(quest_key=quest_key), + quest.data, + category=self.quest_data_attribute_category, + ) + + def load_quest_data(self, quest_key): + """ + Load data for a quest. + + Args: + quest_key (str): The quest to load data for. Returns: - list: Help texts, one for each quest, or only one if `quest_key` is given. + dict: The data stored for the quest. """ - help_texts = [] - if quest_key in self.quests: - quests = [self.quests[quest_key]] - else: - quests = self.quests.values() + return self.obj.attributes.get( + self.quest_data_attribute_template.format(quest_key=quest_key), + category=self.quest_data_attribute_category, + default={}, + ) + + +class CmdQuests(Command): + """ + List all quests and their statuses as well as get info about the status of + a specific quest. + + Usage: + quests + quest + + """ + key = "quests" + aliases = ["quest"] + + def parse(self): + self.quest_name = self.args.strip() + + def func(self): + if self.quest_name: + quest = self.caller.quests.get(self.quest_name) + if not quest: + self.msg(f"Quest {self.quest_name} not found.") + return + self.msg(f"Quest {quest.key}: {quest.status}\n{quest.help()}") + return + + quests = self.caller.quests.all() + if not quests: + self.msg("No quests.") + return for quest in quests: - help_texts.append(f"|c{quest.key}|n\n {quest.desc}\n\n - {quest.help()}") - return help_texts + self.msg(f"Quest {quest.key}: {quest.status}") - def progress(self, quest_key=None, *args, **kwargs): - """ - Check progress of a given quest or all quests. - - Args: - quest_key (str, optional): If given, check the progress of this quest (if we have it), - otherwise check progress on all quests. - *args, **kwargs: Will be passed into each quest's `progress` call. - - """ - if quest_key in self.quests: - quests = [self.quests[quest_key]] - else: - quests = self.quests.values() - - for quest in quests: - quest.progress(*args, **kwargs) - - if self.do_save: - # do_save is set by the quest - self._save() diff --git a/evennia/contrib/tutorials/evadventure/tests/test_quests.py b/evennia/contrib/tutorials/evadventure/tests/test_quests.py index 2a6fe61021..675cf3d498 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_quests.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_quests.py @@ -99,52 +99,50 @@ class EvAdventureQuestTest(EvAdventureMixin, BaseEvenniaTest): def test_help(self): """Get help""" - # get help for all quests - help_txt = self.character.quests.get_help() - self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - You need to do A first."]) - - # get help for one specific quest - help_txt = self.character.quests.get_help(_TestQuest.key) - self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - You need to do A first."]) + quest = self._get_quest() + # get help for a specific quest + help_txt = quest.help() + self.assertEqual(help_txt, "You need to do A first.") # help for finished quest - self._get_quest().complete() - help_txt = self.character.quests.get_help() - self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - This quest is completed!"]) + quest.complete() + help_txt = quest.help() + self.assertEqual(help_txt, "You have completed this quest.") def test_progress__fail(self): """ Check progress without having any. """ - # progress all quests - self.character.quests.progress() - # progress one quest - self.character.quests.progress(_TestQuest.key) + quest = self._get_quest() + # progress quest + quest.progress() # still on step A - self.assertEqual(self._get_quest().current_step, "A") + self.assertEqual(quest.current_step, "A") def test_progress(self): """ - Fulfill the quest steps in sequess + Fulfill the quest steps in sequence. """ + quest = self._get_quest() + # A requires a certain object in inventory self._fulfillA() - self.character.quests.progress() - self.assertEqual(self._get_quest().current_step, "B") + quest.progress() + self.assertEqual(quest.current_step, "B") # B requires progress be called with specific kwarg # should not step (no kwarg) - self.character.quests.progress() - self.assertEqual(self._get_quest().current_step, "B") + quest.progress() + self.assertEqual(quest.current_step, "B") # should step (kwarg sent) - self.character.quests.progress(complete_quest_B=True) - self.assertEqual(self._get_quest().current_step, "C") + quest.progress(complete_quest_B=True) + self.assertEqual(quest.current_step, "C") # C requires a counter Attribute on char be high enough self._fulfillC() - self.character.quests.progress() - self.assertEqual(self._get_quest().current_step, "C") # still on last step - self.assertEqual(self._get_quest().is_completed, True) + quest.progress() + self.assertEqual(quest.current_step, "C") # still on last step + self.assertEqual(quest.is_completed, True) From a3066c9b59a038958bf17f0a0c9d4a6ece3e72b8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 30 Mar 2024 23:46:47 +0100 Subject: [PATCH 17/68] Update Changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a671619315..3349e8a9d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ to disappear for wider client widths (chiizujin) - [Fix][pull3457]: Help topic categories with different case would appear as duplicates (chiizujin) +- [Fix][pull3454]: Traceback in crafting contrib's `recipe.msg` + (InspectorCaracal) - Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) @@ -27,6 +29,7 @@ [pull3456]: https://github.com/evennia/evennia/pull/3456 [pull3457]: https://github.com/evennia/evennia/pull/3457 [pull3458]: https://github.com/evennia/evennia/pull/3458 +[pull3454]: https://github.com/evennia/evennia/pull/3454 ## Evennia 4.0.0 From 22ea87b805dee331c167c2e94266e50c21746cd6 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sun, 31 Mar 2024 12:04:04 +1100 Subject: [PATCH 18/68] Add unit test for editor echo with lines containing whitespace --- evennia/utils/tests/test_eveditor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/evennia/utils/tests/test_eveditor.py b/evennia/utils/tests/test_eveditor.py index 9e433f6bff..73bae1ea39 100644 --- a/evennia/utils/tests/test_eveditor.py +++ b/evennia/utils/tests/test_eveditor.py @@ -156,6 +156,13 @@ class TestEvEditor(BaseEvenniaCommandTest): "First test line\nInserted-New Replaced Second line-End\n test line\n:", ) + self.call( + eveditor.CmdLineInput(), + " Whitespace echo test line.", + raw_string=" Whitespace echo test line.", + msg="05 Whitespace echo test line.", + ) + def test_eveditor_COLON_UU(self): eveditor.EvEditor(self.char1) self.call( From 505310cf25cb535eaed0bd3cf32a0242c05d6d93 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sun, 31 Mar 2024 15:42:37 +1100 Subject: [PATCH 19/68] Correct help for editor's paste command --- docs/source/Components/EvEditor.md | 2 +- evennia/utils/eveditor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/Components/EvEditor.md b/docs/source/Components/EvEditor.md index edc554aab1..d4b6f5fbbb 100644 --- a/docs/source/Components/EvEditor.md +++ b/docs/source/Components/EvEditor.md @@ -134,7 +134,7 @@ the in-editor help command (`:h`). :y - yank (copy) line to the copy buffer :x - cut line and store it in the copy buffer - :p - put (paste) previously copied line directly after + :p - put (paste) previously copied line directly before :i - insert new text at line . Old line will move down :r - replace line with text :I - insert text at the beginning of line diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 2df8574368..471533d8f5 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -88,7 +88,7 @@ _HELP_TEXT = _( :y - yank (copy) line(s) to the copy buffer :x - cut line(s) and store it in the copy buffer - :p - put (paste) previously copied line(s) directly after + :p - put (paste) previously copied line(s) directly before :i - insert new text at line . Old line will move down :r - replace line with text :I - insert text at the beginning of line From b7cbdd9433b052c65ee1b04c289b2fdbbd570646 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sun, 31 Mar 2024 17:12:59 +1100 Subject: [PATCH 20/68] Fix editor's range specification --- evennia/utils/eveditor.py | 9 ++-- evennia/utils/tests/test_eveditor.py | 63 +++++++++++++++++++++++++++- evennia/utils/utils.py | 18 ++++++++ 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 2df8574368..824ab23691 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -305,12 +305,9 @@ class CmdEditorBase(_COMMAND_DEFAULT_CLASS): linerange = False if arglist and arglist[0].count(":") == 1: part1, part2 = arglist[0].split(":") - if part1 and part1.isdigit(): - lstart = min(max(0, int(part1)) - 1, nlines) - linerange = True - if part2 and part2.isdigit(): - lend = min(lstart + 1, int(part2)) + 1 - linerange = True + lstart = min(max(1, int(part1)), nlines) - 1 if utils.value_is_integer(part1) else 0 + lend = min(max(lstart + 1, int(part2)), nlines) if utils.value_is_integer(part2) else nlines + linerange = True elif arglist and arglist[0].isdigit(): lstart = min(max(0, int(arglist[0]) - 1), nlines) lend = lstart + 1 diff --git a/evennia/utils/tests/test_eveditor.py b/evennia/utils/tests/test_eveditor.py index b7333d6810..fcdb4141c8 100644 --- a/evennia/utils/tests/test_eveditor.py +++ b/evennia/utils/tests/test_eveditor.py @@ -8,6 +8,67 @@ from evennia.utils import eveditor class TestEvEditor(BaseEvenniaCommandTest): + def test_eveditor_ranges(self): + eveditor.EvEditor(self.char1) + self.call( + eveditor.CmdEditorGroup(), + "", + cmdstring=":", + msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", + ) + self.call(eveditor.CmdLineInput(), "line 1", raw_string="line 1", msg="01line 1") + self.call(eveditor.CmdLineInput(), "line 2", raw_string="line 2", msg="02line 2") + self.call(eveditor.CmdLineInput(), "line 3", raw_string="line 3", msg="03line 3") + self.call(eveditor.CmdLineInput(), "line 4", raw_string="line 4", msg="04line 4") + self.call(eveditor.CmdLineInput(), "line 5", raw_string="line 5", msg="05line 5") + self.call( + eveditor.CmdEditorGroup(), + "", # list whole buffer + cmdstring=":", + msg="Line Editor []\n01line 1\n02line 2\n" + "03line 3\n04line 4\n05line 5\n" + "[l:05 w:010 c:0034](:h for help)", + ) + self.call( + eveditor.CmdEditorGroup(), + ":", # list empty range + cmdstring=":", + msg="Line Editor []\n01line 1\n02line 2\n" + "03line 3\n04line 4\n05line 5\n" + "[l:05 w:010 c:0034](:h for help)", + ) + self.call( + eveditor.CmdEditorGroup(), + ":4", # list from start to line 4 + cmdstring=":", + msg="Line Editor []\n01line 1\n02line 2\n" + "03line 3\n04line 4\n" + "[l:04 w:008 c:0027](:h for help)", + ) + self.call( + eveditor.CmdEditorGroup(), + "2:", # list from line 2 to end + cmdstring=":", + msg="Line Editor []\n02line 2\n03line 3\n" + "04line 4\n05line 5\n" + "[l:04 w:008 c:0027](:h for help)", + ) + self.call( + eveditor.CmdEditorGroup(), + "-10:10", # try to list invalid range (too large) + cmdstring=":", + msg="Line Editor []\n01line 1\n02line 2\n" + "03line 3\n04line 4\n05line 5\n" + "[l:05 w:010 c:0034](:h for help)", + ) + self.call( + eveditor.CmdEditorGroup(), + "3:1", # try to list invalid range (reversed) + cmdstring=":", + msg="Line Editor []\n03line 3\n" + "[l:01 w:002 c:0006](:h for help)", + ) + def test_eveditor_view_cmd(self): eveditor.EvEditor(self.char1) self.call( @@ -244,7 +305,7 @@ class TestEvEditor(BaseEvenniaCommandTest): msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", ) self.call(eveditor.CmdLineInput(), "line 1", raw_string="line 1", msg="01line 1") - self.call(eveditor.CmdEditorGroup(), "1:2", cmdstring=":f", msg="Flood filled lines 1-2.") + self.call(eveditor.CmdEditorGroup(), "1:2", cmdstring=":f", msg="Flood filled line 1.") self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "line 1") def test_eveditor_COLON_J(self): diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 877cb7ac8f..99e5170651 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -3077,3 +3077,21 @@ def ip_from_request(request, exclude=None) -> str: logger.log_warn("ip_from_request: No valid IP address found in request. Using remote_addr.") return remote_addr + + +def value_is_integer(value): + """ + Determines if a value can be type-cast to an integer. + + Args: + value (any): The value to check. + + Returns: + result (bool): Whether it can be type-cast to an integer or not. + """ + try: + int(value) + except ValueError: + return False + + return True From 818ab25b278d07785ac6de565ffc0739829f855a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 31 Mar 2024 11:54:04 +0200 Subject: [PATCH 21/68] Fix error in searching tutorial --- docs/source/Coding/Changelog.md | 3 + .../Beginner-Tutorial-Searching-Things.md | 110 +++++++++--------- 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index a671619315..3349e8a9d9 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -17,6 +17,8 @@ to disappear for wider client widths (chiizujin) - [Fix][pull3457]: Help topic categories with different case would appear as duplicates (chiizujin) +- [Fix][pull3454]: Traceback in crafting contrib's `recipe.msg` + (InspectorCaracal) - Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) @@ -27,6 +29,7 @@ [pull3456]: https://github.com/evennia/evennia/pull/3456 [pull3457]: https://github.com/evennia/evennia/pull/3457 [pull3458]: https://github.com/evennia/evennia/pull/3458 +[pull3454]: https://github.com/evennia/evennia/pull/3454 ## Evennia 4.0.0 diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md index e0b5dce20f..14e1ac6db7 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md @@ -149,6 +149,54 @@ If you you really want all matches to the search parameters you specify. In othe There are equivalent search functions for all the main resources. You can find a listing of them [in the Search functions section](../../../Evennia-API.md) of the API front page. +## Understanding object relationships + +It's important to understand how objects relate to one another when searching. + +Let's consider a `chest` with a `coin` inside it. The chest stands in a `dungeon` room. In the dungeon is also a `door` (an exit leading outside). + +``` +┌───────────────────────┐ +│dungeon │ +│ ┌─────────┐ │ +│ │chest │ ┌────┐ │ +│ │ ┌────┐ │ │door│ │ +│ │ │coin│ │ └────┘ │ +│ │ └────┘ │ │ +│ │ │ │ +│ └─────────┘ │ +│ │ +└───────────────────────┘ +``` + +If you have access to any in-game Object, you can find related objects by use if its `.location` and `.contents` properties. + +- `coin.location` is `chest`. +- `chest.location` is `dungeon`. +- `door.location` is `dungeon`. +- `room.location` is `None` since it's not inside something else. + +One can use this to find what is inside what. For example, `coin.location.location` is the `dungeon`. + +- `room.contents` is `[chest, door]` +- `chest.contents` is `[coin]` +- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin. +- `door.contents` is `[]` too. + +A convenient helper is `.contents_get` - this allows to restrict what is returned: + +- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?) + +There is a special property for finding exits: + +- `room.exits` is `[door]` +- `coin.exits` is `[]` since it has no exits (same for all the other objects) + +There is a property `.destination` which is only used by exits: + +- `door.destination` is `outside` (or wherever the door leads) +- `room.destination` is `None` (same for all the other non-exit objects) + ## What can be searched for These are the main database entities one can search for: @@ -206,9 +254,9 @@ However, using `search_object` will find the rose wherever it's located: > py evennia.search_object("rose") -However, if you demand that the room is in the current room, it won't be found: +The `evennia.search_object` method doesn't have a `location` argument. What you do instead is to limit the search by setting its `candidates` keyword to the `.contents` of the current location. This is the same as a location search, since it will only accept matches among those in the room. In this example we'll (correctly) find the rose is not in the room. - > py evennia.search_object("rose", location=here) + > py evennia.search_object("rose", candidate=here.contents) In general, the `Object.search` is a shortcut for doing the very common searches of things in the same location, whereas the `search_object` finds objects anywhere. @@ -317,53 +365,11 @@ In legacy code bases you may be used to relying a lot on #dbrefs to find and tra ``` -## Finding objects relative each other +## Summary -It's important to understand how objects relate to one another when searching. -Let's consider a `chest` with a `coin` inside it. The chest stands in a room `dungeon`. In the dungeon is also a `door`. This is an exit leading outside. +Knowing how to find things is important and the tools from this section will serve you well. These tools will cover most of your regular needs. -``` -┌───────────────────────┐ -│dungeon │ -│ ┌─────────┐ │ -│ │chest │ ┌────┐ │ -│ │ ┌────┐ │ │door│ │ -│ │ │coin│ │ └────┘ │ -│ │ └────┘ │ │ -│ │ │ │ -│ └─────────┘ │ -│ │ -└───────────────────────┘ -``` - -- `coin.location` is `chest`. -- `chest.location` is `dungeon`. -- `door.location` is `dungeon`. -- `room.location` is `None` since it's not inside something else. - -One can use this to find what is inside what. For example, `coin.location.location` is the `dungeon`. -We can also find what is inside each object. This is a list of things. - -- `room.contents` is `[chest, door]` -- `chest.contents` is `[coin]` -- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin. -- `door.contents` is `[]` too. - -A convenient helper is `.contents_get` - this allows to restrict what is returned: - -- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?) - -There is a special property for finding exits: - -- `room.exits` is `[door]` -- `coin.exits` is `[]` (same for all the other objects) - -There is a property `.destination` which is only used by exits: - -- `door.destination` is `outside` (or wherever the door leads) -- `room.destination` is `None` (same for all the other non-exit objects) - -You can also include this information in searches: +Not always though. If we go back to the example of a coin in a chest from before, you _could_ use the following to dynamically figure out if there are any chests in the room with coins inside: ```python from evennia import search_object @@ -372,13 +378,9 @@ from evennia import search_object dungeons = search_object("dungeon", typeclass="typeclasses.rooms.Room") chests = search_object("chest", location=dungeons[0]) # find if there are any skulls in the chest -skulls = search_object("Skull", candidates=chests[0].contents) +coins = search_object("coin", candidates=chests[0].contents) ``` -More advanced, nested queries like this can however often be made more efficient by using the hints in the next lesson. +This would work but is both quite inefficient, fragile and a lot to type. This kind of thing is better done by directly querying the database. -## Summary - -Knowing how to find things is important and the tools from this section will serve you well. These tools will cover most of your needs ... - -... but not always. In the next lesson we will dive further into more complex searching when we look at Django queries and querysets in earnest. \ No newline at end of file +In the next lesson we will dive further into more complex searching when we look at Django queries and querysets in earnest. \ No newline at end of file From a481cb4b2b10ed2da2110b4771f19754ef0c906c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 31 Mar 2024 11:58:31 +0200 Subject: [PATCH 22/68] Update security.md and release process --- .release.sh | 13 +++++++------ SECURITY.md | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.release.sh b/.release.sh index b9e131d7e8..55da41b193 100755 --- a/.release.sh +++ b/.release.sh @@ -9,13 +9,14 @@ echo " 2. On main branch, update CHANGELOG.md." echo " 3. Make sure pyproject.toml is set to the same major.minor.patch version as evennia/VERSION.txt ($VERSION)." echo " 4. If major release:" echo " a. Update docs/sources/conf.py, Add '[MAJOR_VERSION].x' to 'legacy_versions' and 'v$VERSION' to 'legacy_branches'." -echo " b. Make sure all changes are committed, e.g. as 'Evennia $VERSION major/minor/patch release'." -echo " c. Check out a new branch v$VERSION." -echo " d. Push the v$VERSION branch to github." -echo " e. On the v$VERSION branch, temporarily set 'current_is_legacy=True' in source/conf.py, then (re)build " +echo " b. Update 'SECURITY.md' with latest new version." +echo " c. Make sure all changes are committed, e.g. as 'Evennia $VERSION major/minor/patch release'." +echo " d. Check out a new branch v$VERSION." +echo " e. Push the v$VERSION branch to github." +echo " f. On the v$VERSION branch, temporarily set 'current_is_legacy=True' in source/conf.py, then (re)build " echo " the docs for this release with 'make local' and old-version warning headers. Throw away git changes after." -echo " f. Rename the created build/html folder to '[MAJOR_VERSION].x'. Manually copy it to the gh-pages branch's build/ folder." -echo " g. Add the folder, commit and push to the gh-pages branch. Then checkout main branch again." +echo " g. Rename the created build/html folder to '[MAJOR_VERSION].x'. Manually copy it to the gh-pages branch's build/ folder." +echo " h. Add the folder, commit and push to the gh-pages branch. Then checkout main branch again." echo " 5. Run 'make local' in docs/ to update dynamic docs (like Changelog.md) and autodocstrings (may have to run twice)." echo " 6. Make sure all changes are committed (if not already), e.g. as 'Evennia $VERSION major/minor/patch release' (un-staged files will be wiped)." echo " 7. Make sure all unit tests pass!" diff --git a/SECURITY.md b/SECURITY.md index 6ddcce0bee..99517cd032 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,7 +7,8 @@ to use the latest minor/patch version within each major version. | Version | Supported | | ------- | ------------------ | -| 3.x | :white_check_mark: | +| 4.x | :white_check_mark: | +| 3.x | :x: | | 2.x | :x: | | 1.x | :x: | | < 1.0 | :x: | From a9e8042bbe8cc17600390704f2f2c254a0a0499d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 31 Mar 2024 12:06:29 +0200 Subject: [PATCH 23/68] Update search tutorial --- .../Part1/Beginner-Tutorial-Searching-Things.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md index 14e1ac6db7..8f1a58dfe7 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md @@ -210,6 +210,8 @@ These are the main database entities one can search for: Most of the time you'll likely spend your time searching for Objects and the occasional Accounts. +Most search methods are available directly from `evennia`. But there are also a lot of useful search helpers found via `evennia.search`. + So to find an entity, what can be searched for? ### Search by key @@ -315,7 +317,7 @@ For example, let's say our plants have a 'growth state' that updates as it grows Now we can find the things that have a given growth state: - > py evennia.search_object_attribute("growth_state", "withering") + > py evennia.search_object("withering", attribute_name="growth_state") > Searching by Attribute can be very practical. But if you want to group entities or search very often, using Tags and search by Tags is faster and more resource-efficient. From f4cb272e22a72613ce6cc7e4c0e0ad52aa5e671e Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Mon, 1 Apr 2024 15:51:39 +1100 Subject: [PATCH 24/68] Add support to get_numbered_name() for omitting the article for single items --- evennia/objects/objects.py | 6 ++++++ evennia/objects/tests.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 97b5849224..829b8df6d8 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1476,6 +1476,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): method is used. return_string (bool): If `True`, return only the singular form if count is 0,1 or the plural form otherwise. If `False` (default), return both forms as a tuple. + no_article (bool): If `True`, do not return an article if `count` is 1. Returns: tuple: This is a tuple `(str, str)` with the singular and plural forms of the key @@ -1505,6 +1506,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # look at 'an egg'. self.aliases.add(singular, category=plural_category) + if kwargs.get("no_article") and count == 1: + if kwargs.get("return_string"): + return key + return key, key + if kwargs.get("return_string"): return singular if count==1 else plural diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index a77b348d02..abc2adecdb 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -185,6 +185,11 @@ class DefaultObjectTest(BaseEvenniaTest): pattern, ) + def test_get_name_without_article(self): + self.assertEqual(self.obj1.get_numbered_name(1, self.char1, return_string=True), "an Obj") + self.assertEqual( + self.obj1.get_numbered_name(1, self.char1, return_string=True, no_article=True), "Obj" + ) class TestObjectManager(BaseEvenniaTest): "Test object manager methods" From 54278171124b845cae0c459ccb87f0da21a28050 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Mon, 1 Apr 2024 18:20:10 +1100 Subject: [PATCH 25/68] Add alias/delete switch --- evennia/commands/default/building.py | 28 +++++++++++++++++++++++----- evennia/commands/default/tests.py | 17 +++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index c503990345..e99455d0f9 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -218,10 +218,13 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): alias [= [alias[,alias,alias,...]]] alias = alias/category = [alias[,alias,...]: + alias/delete = Switches: category - requires ending input with :category, to store the given aliases with the given category. + delete - deletes all occurrences of the given alias, regardless + of category Assigns aliases to an object so it can be referenced by more than one name. Assign empty to remove all aliases from object. If @@ -235,7 +238,7 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): key = "@alias" aliases = "setobjalias" - switch_options = ("category",) + switch_options = ("category", "delete") locks = "cmd:perm(setobjalias) or perm(Builder)" help_category = "Building" @@ -252,12 +255,12 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): return objname = self.lhs - # Find the object to receive aliases + # Find the object to receive/delete aliases obj = caller.search(objname) if not obj: return - if self.rhs is None: - # no =, so we just list aliases on object. + if self.rhs is None and 'delete' not in self.switches: + # no =, and not deleting, so we just list aliases on object. aliases = obj.aliases.all(return_key_and_category=True) if aliases: caller.msg( @@ -280,7 +283,9 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): return if not self.rhs: - # we have given an empty =, so delete aliases + # we have given an empty =, so delete aliases. + # as a side-effect, 'alias/delete obj' and 'alias/delete obj=' + # will also be caught here, which is fine old_aliases = obj.aliases.all() if old_aliases: caller.msg( @@ -292,6 +297,19 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): caller.msg("No aliases to clear.") return + if "delete" in self.switches: + # delete all matching keys, regardless of category + existed = False + for key, category in obj.aliases.all(return_key_and_category=True): + if key == self.rhs: + obj.aliases.remove(key=self.rhs, category=category) + existed = True + if existed: + caller.msg("Alias '%s' deleted from %s." % (self.rhs, obj.get_display_name(caller))) + else: + caller.msg("%s has no alias '%s'." % (obj.get_display_name(caller), self.rhs)) + return + category = None if "category" in self.switches: if ":" in self.rhs: diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 3a272e04f7..bf52a8be81 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -790,6 +790,23 @@ class TestBuilding(BaseEvenniaCommandTest): self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2") self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.") + self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj: testobj1b") + self.call(building.CmdSetObjAlias(), + "/category Obj = testobj1b:category1", + "Alias(es) for 'Obj' set to 'testobj1b' (category: 'category1')." + ) + self.call( + building.CmdSetObjAlias(), + "/category Obj = testobj1b:category2", + "Alias(es) for 'Obj' set to 'testobj1b,testobj1b' (category: 'category2')." + ) + self.call( + building.CmdSetObjAlias(), # delete both occurences of alias 'testobj1b' + "/delete Obj = testobj1b", + "Alias 'testobj1b' deleted from Obj." + ) + self.call(building.CmdSetObjAlias(), "Obj =", "No aliases to clear.") + def test_copy(self): self.call( building.CmdCopy(), From 7cce317da0f28b615fa6a15ff176e7e3d3169695 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 11:09:08 +0200 Subject: [PATCH 26/68] Update Changelog. Outline which doc files are auto-generated --- CHANGELOG.md | 11 +++++++++++ docs/source/Coding/Changelog.md | 11 +++++++++++ docs/source/Contributing-Docs.md | 15 ++++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3349e8a9d9..980dcc8c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ adding line breaks in hook returns. (InspectorCaracal) - [Feature][pull3458]: New `sethelp/category` switch to change a help topic's category after it was created (chiizujin) +- [Feature][pull3467]: Add `alias/delete` switch for removing object aliases + from in-game with default command (chiizujin) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) - [Fix][pull3453]: Object aliases not showing in search multi-match @@ -19,6 +21,11 @@ duplicates (chiizujin) - [Fix][pull3454]: Traceback in crafting contrib's `recipe.msg` (InspectorCaracal) +- [Fix][pull3459]: EvEditor line-echo compacted whitespace erroneously (chiizujin) +- [Fix][pull3463]: EvEditor :help described the :paste operation in the wrong + way (chiizujin) +- [Fix][pull3464]: EvEditor range:range specification didn't return correct + range (chiizujin) - Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) @@ -30,6 +37,10 @@ [pull3457]: https://github.com/evennia/evennia/pull/3457 [pull3458]: https://github.com/evennia/evennia/pull/3458 [pull3454]: https://github.com/evennia/evennia/pull/3454 +[pull3459]: https://github.com/evennia/evennia/pull/3459 +[pull3463]: https://github.com/evennia/evennia/pull/3463 +[pull3464]: https://github.com/evennia/evennia/pull/3464 +[pull3467]: https://github.com/evennia/evennia/pull/3467 ## Evennia 4.0.0 diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 3349e8a9d9..980dcc8c8a 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -7,6 +7,8 @@ adding line breaks in hook returns. (InspectorCaracal) - [Feature][pull3458]: New `sethelp/category` switch to change a help topic's category after it was created (chiizujin) +- [Feature][pull3467]: Add `alias/delete` switch for removing object aliases + from in-game with default command (chiizujin) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) - [Fix][pull3453]: Object aliases not showing in search multi-match @@ -19,6 +21,11 @@ duplicates (chiizujin) - [Fix][pull3454]: Traceback in crafting contrib's `recipe.msg` (InspectorCaracal) +- [Fix][pull3459]: EvEditor line-echo compacted whitespace erroneously (chiizujin) +- [Fix][pull3463]: EvEditor :help described the :paste operation in the wrong + way (chiizujin) +- [Fix][pull3464]: EvEditor range:range specification didn't return correct + range (chiizujin) - Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) @@ -30,6 +37,10 @@ [pull3457]: https://github.com/evennia/evennia/pull/3457 [pull3458]: https://github.com/evennia/evennia/pull/3458 [pull3454]: https://github.com/evennia/evennia/pull/3454 +[pull3459]: https://github.com/evennia/evennia/pull/3459 +[pull3463]: https://github.com/evennia/evennia/pull/3463 +[pull3464]: https://github.com/evennia/evennia/pull/3464 +[pull3467]: https://github.com/evennia/evennia/pull/3467 ## Evennia 4.0.0 diff --git a/docs/source/Contributing-Docs.md b/docs/source/Contributing-Docs.md index d595fd398a..0308cfdceb 100644 --- a/docs/source/Contributing-Docs.md +++ b/docs/source/Contributing-Docs.md @@ -23,12 +23,25 @@ at the root of `evennia/docs/source/`. result in Evennia. This is often on a tutorial or FAQ form and will refer to the rest of the documentation for further reading. - `source/Howtos/Beginner-Tutorial/` holds all documents part of the initial tutorial sequence. - Other files and folders: - `source/api/` contains the auto-generated API documentation as `.html` files. Don't edit these files manually, they are auto-generated from sources. - `source/_templates` and `source/_static` hold files for the doc itself. They should only be modified if wanting to change the look and structure of the documentation generation itself. - `conf.py` holds the Sphinx configuration. It should usually not be modified except to update the Evennia version on a new branch. +## Automatically generated doc pages + +Some doc pages are automatically generated. Changes to their generated markdown file will be overwritten. Instead they must be modified at the point the automation reads the text from. + +- All API docs under `source/api` are built from the doc strings of Evennia core code. Documentation fixes for these needs to be done in the doc strings of the relevant module, function, class or method. +- [Contribs/Contribs-Overview.md](Contribs/Contribs-Overview.md) is completely generated from scratch when building the docs, by the script `evennia/docs/pylib/contrib_readmes2docs.py`. + - All contrib blurbs on the above page are taken from the first paragraph of each contrib's `README.md`, found under `evennia/contrib/*/*/README.md`. + - Similarly, all contrib documentation linked from the above page is generated from each contrib's `README.md` file. +- [Components/Default-Commands.md](Components/Default-Commands.md) is generated from the command classes found under `evennia/commands/default/`. +- [Coding/Evennia-Code-Style.md](Coding/Evennia-Code-Style.md) is generated from `evennia/CODING_STYLE.md`. +- [Coding/Changelog.md](Coding/Changelog.md) is generated from `evennia/CHANGELOG.md` +- [Setup/Settings-Default.md](Setup/Settings-Default.md) is generated from the default settings file `evennia/default_settings.py` + +Most auto-generated pages have a warning in the header indicating that it's auto-generated. ## Editing syntax From 6f00c8cfc0e34597e8644821c7d492b08e4be26b Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 11:16:19 +0200 Subject: [PATCH 27/68] Named the rpsystem cmdset, trying to address #3452 --- evennia/contrib/rpg/rpsystem/rpsystem.py | 2 ++ .../tutorials/evadventure/combat_turnbased.py | 14 ++++------- .../tutorials/evadventure/combat_twitch.py | 25 +++++++------------ 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index 22a3bffa3b..bf808ff307 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -1277,6 +1277,8 @@ class RPSystemCmdSet(CmdSet): Mix-in for adding rp-commands to default cmdset. """ + key = "rpsystem_cmdset" + def at_cmdset_creation(self): self.add(CmdEmote()) self.add(CmdSay()) diff --git a/evennia/contrib/tutorials/evadventure/combat_turnbased.py b/evennia/contrib/tutorials/evadventure/combat_turnbased.py index 14b7f0bb26..fdea4a0038 100644 --- a/evennia/contrib/tutorials/evadventure/combat_turnbased.py +++ b/evennia/contrib/tutorials/evadventure/combat_turnbased.py @@ -26,15 +26,9 @@ from evennia import AttributeProperty, CmdSet, Command, EvMenu from evennia.utils import inherits_from, list_to_string from .characters import EvAdventureCharacter -from .combat_base import ( - CombatAction, - CombatActionAttack, - CombatActionHold, - CombatActionStunt, - CombatActionUseItem, - CombatActionWield, - EvAdventureCombatBaseHandler, -) +from .combat_base import (CombatAction, CombatActionAttack, CombatActionHold, + CombatActionStunt, CombatActionUseItem, + CombatActionWield, EvAdventureCombatBaseHandler) from .enums import Ability @@ -843,5 +837,7 @@ class TurnCombatCmdSet(CmdSet): CmdSet for the turn-based combat. """ + key = "turncombat_cmdset" + def at_cmdset_creation(self): self.add(CmdTurnAttack()) diff --git a/evennia/contrib/tutorials/evadventure/combat_twitch.py b/evennia/contrib/tutorials/evadventure/combat_twitch.py index 787081fc28..9e860ffff8 100644 --- a/evennia/contrib/tutorials/evadventure/combat_twitch.py +++ b/evennia/contrib/tutorials/evadventure/combat_twitch.py @@ -8,24 +8,13 @@ This implements a 'twitch' (aka DIKU or other traditional muds) style of MUD com """ from evennia import AttributeProperty, CmdSet, default_cmds from evennia.commands.command import Command, InterruptCommand -from evennia.utils.utils import ( - display_len, - inherits_from, - list_to_string, - pad, - repeat, - unrepeat, -) +from evennia.utils.utils import (display_len, inherits_from, list_to_string, + pad, repeat, unrepeat) from .characters import EvAdventureCharacter -from .combat_base import ( - CombatActionAttack, - CombatActionHold, - CombatActionStunt, - CombatActionUseItem, - CombatActionWield, - EvAdventureCombatBaseHandler, -) +from .combat_base import (CombatActionAttack, CombatActionHold, + CombatActionStunt, CombatActionUseItem, + CombatActionWield, EvAdventureCombatBaseHandler) from .enums import ABILITY_REVERSE_MAP @@ -560,6 +549,8 @@ class TwitchCombatCmdSet(CmdSet): Add to character, to be able to attack others in a twitch-style way. """ + key = "twitch_combat_cmdset" + def at_cmdset_creation(self): self.add(CmdAttack()) self.add(CmdHold()) @@ -573,5 +564,7 @@ class TwitchLookCmdSet(CmdSet): This will be added/removed dynamically when in combat. """ + key = "twitch_look_cmdset" + def at_cmdset_creation(self): self.add(CmdLook()) From b51f3b5a5e4ceb6ca1040db10f23bd367306540d Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 12:23:11 +0200 Subject: [PATCH 28/68] Tag pages with page, category comms, add lock:read check (backwards compatible) --- evennia/accounts/accounts.py | 25 +++++++++---------------- evennia/accounts/manager.py | 1 - evennia/commands/default/comms.py | 27 +++++++++++++++++++++++++-- evennia/commands/default/tests.py | 20 ++++++++++---------- 4 files changed, 44 insertions(+), 29 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 3ac6ca513a..e683aa8dd1 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -15,14 +15,13 @@ import time import typing from random import getrandbits +import evennia from django.conf import settings from django.contrib.auth import authenticate, password_validation from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ - -import evennia from evennia.accounts.manager import AccountManager from evennia.accounts.models import AccountDB from evennia.commands.cmdsethandler import CmdSetHandler @@ -30,24 +29,17 @@ from evennia.comms.models import ChannelDB from evennia.objects.models import ObjectDB from evennia.scripts.scripthandler import ScriptHandler from evennia.server.models import ServerConfig -from evennia.server.signals import ( - SIGNAL_ACCOUNT_POST_CREATE, - SIGNAL_ACCOUNT_POST_LOGIN_FAIL, - SIGNAL_OBJECT_POST_PUPPET, - SIGNAL_OBJECT_POST_UNPUPPET, -) +from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE, + SIGNAL_ACCOUNT_POST_LOGIN_FAIL, + SIGNAL_OBJECT_POST_PUPPET, + SIGNAL_OBJECT_POST_UNPUPPET) from evennia.server.throttle import Throttle from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import class_from_module, create, logger from evennia.utils.optionhandler import OptionHandler -from evennia.utils.utils import ( - is_iter, - lazy_property, - make_iter, - to_str, - variable_from_module, -) +from evennia.utils.utils import (is_iter, lazy_property, make_iter, to_str, + variable_from_module) __all__ = ("DefaultAccount", "DefaultGuest") @@ -1334,7 +1326,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if isinstance(searchdata, str): # handle wrapping of common terms if searchdata.lower() in ("me", "*me", "self", "*self"): - return self + return [self] if quiet else self + searchdata = self.nicks.nickreplace( searchdata, categories=("account",), include_account=False ) diff --git a/evennia/accounts/manager.py b/evennia/accounts/manager.py index f5d91e5fcf..83478654b8 100644 --- a/evennia/accounts/manager.py +++ b/evennia/accounts/manager.py @@ -7,7 +7,6 @@ import datetime from django.conf import settings from django.contrib.auth.models import UserManager from django.utils import timezone - from evennia.server import signals from evennia.typeclasses.managers import TypeclassManager, TypedObjectManager from evennia.utils.utils import class_from_module, dbid_to_obj, make_iter diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index bf9a90b0e3..2cec5ddf08 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -8,7 +8,7 @@ Communication commands: """ from django.conf import settings - +from django.db.models import Q from evennia.accounts import bots from evennia.accounts.models import AccountDB from evennia.comms.comms import DefaultChannel @@ -1338,8 +1338,24 @@ class CmdPage(COMMAND_DEFAULT_CLASS): # get the messages we've sent (not to channels) pages_we_sent = Msg.objects.get_messages_by_sender(caller).order_by("-db_date_created") + # get only messages tagged as pages or not tagged at all (legacy pages) + pages_we_sent = pages_we_sent.filter( + Q(db_tags__db_key__iexact="page", db_tags__db_category__iexact="comms") + | Q(db_tags__isnull=True) + ) + # we need to default to True to allow for legacy pages + pages_we_sent = [msg for msg in pages_we_sent if msg.access(caller, "read", default=True)] + # get last messages we've got pages_we_got = Msg.objects.get_messages_by_receiver(caller).order_by("-db_date_created") + pages_we_got = pages_we_got.filter( + Q(db_tags__db_key__iexact="page", db_tags__db_category__iexact="comms") + | Q(db_tags__isnull=True) + ) + # we need to default to True to allow for legacy pages + pages_we_got = [msg for msg in pages_we_got if msg.access(caller, "read", default=True)] + + # get only messages tagged as pages or not tagged at all (legacy pages) targets, message, number = [], None, None if "last" in self.switches: @@ -1360,6 +1376,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS): targets.append(target_obj) message = self.rhs.strip() else: + # no = sign, handler this as well target, *message = self.args.split(" ", 1) if target and target.isnumeric(): # a number to specify a historic page @@ -1395,7 +1412,13 @@ class CmdPage(COMMAND_DEFAULT_CLASS): message = f"{caller.key} {message.strip(':').strip()}" # create the persistent message object - create.create_message(caller, message, receivers=targets) + create.create_message(caller, message, receivers=targets, + locks=(f"read:id({caller.id}) or perm(Admin);" + f"delete:id({caller.id}) or perm(Admin);" + f"edit:id({caller.id}) or perm(Admin)" + ), + tags = [("page", "comms")], + ) # tell the accounts they got a message. received = [] diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index bf52a8be81..e60ee3ddc2 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -18,18 +18,13 @@ import evennia from anything import Anything from django.conf import settings from django.test import override_settings -from evennia import ( - DefaultCharacter, - DefaultExit, - DefaultObject, - DefaultRoom, - ObjectDB, - search_object, -) +from evennia import (DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom, + ObjectDB, search_object) from evennia.commands import cmdparser from evennia.commands.cmdset import CmdSet from evennia.commands.command import Command, InterruptCommand -from evennia.commands.default import account, admin, batchprocess, building, comms, general +from evennia.commands.default import (account, admin, batchprocess, building, + comms, general) from evennia.commands.default import help as help_module from evennia.commands.default import syscommands, system, unloggedin from evennia.commands.default.cmdset_character import CharacterCmdSet @@ -373,7 +368,8 @@ class TestCmdTasks(BaseEvenniaCommandTest): self.timedelay = 5 global _TASK_HANDLER if _TASK_HANDLER is None: - from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER + from evennia.scripts.taskhandler import \ + TASK_HANDLER as _TASK_HANDLER _TASK_HANDLER.clock = task.Clock() self.task_handler = _TASK_HANDLER self.task_handler.clear() @@ -2071,6 +2067,10 @@ class TestComms(BaseEvenniaCommandTest): ), receiver=self.account, ) + from evennia.comms.models import Msg + msgs = Msg.objects.filter(db_tags__db_key="page", db_tags__db_category="comms") + self.assertEqual(msgs[0].senders, [self.account]) + self.assertEqual(msgs[0].receivers, [self.account2]) @override_settings(DISCORD_BOT_TOKEN="notarealtoken", DISCORD_ENABLED=True) From 0c072dab02e0f87b8e382b205d063b81ab887b85 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 12:26:10 +0200 Subject: [PATCH 29/68] Update changelog. Resolve #3450 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 980dcc8c8a..41e72dc57e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ category after it was created (chiizujin) - [Feature][pull3467]: Add `alias/delete` switch for removing object aliases from in-game with default command (chiizujin) +- [Feature][issue3450]: The default `page` command now tags its `Msg` objects + with tag 'page' (category 'comms') and also checks the `Msg`' 'read' lock. + made backwards compatible for old pages. - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) - [Fix][pull3453]: Object aliases not showing in search multi-match @@ -41,6 +44,7 @@ [pull3463]: https://github.com/evennia/evennia/pull/3463 [pull3464]: https://github.com/evennia/evennia/pull/3464 [pull3467]: https://github.com/evennia/evennia/pull/3467 +[issue3450]: https://github.com/evennia/evennia/issues/3450 ## Evennia 4.0.0 From 8da1e3790fc58d6059db11dc32e74e7cd20c7dea Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Mon, 1 Apr 2024 23:05:25 +1100 Subject: [PATCH 30/68] Add examples to doc string of get_numbered_name() --- evennia/objects/objects.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 829b8df6d8..4b4fed4ad1 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1483,8 +1483,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): including the count. Examples: - :: - obj.get_numbered_name(3, looker, key="foo") -> ("a foo", "three foos") + :: + - obj.get_numbered_name(3, looker, key="foo") -> ("a foo", "three foos") + - obj.get_numbered_name(1, looker, key="Foobert", return_string=True) + -> "a Foobert" + - obj.get_numbered_name(1, looker, key="Foobert", return_string=True, no_article=True) + -> "Foobert" """ plural_category = "plural_key" From 763699ea14eaa4197edd8de62307bfbad802f743 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 14:05:45 +0200 Subject: [PATCH 31/68] Fix :UU and :UU usage in EvEditor. Resolve #3262 --- CHANGELOG.md | 7 +- evennia/commands/cmdhandler.py | 7 +- evennia/commands/command.py | 48 ++++++------- evennia/utils/eveditor.py | 4 +- evennia/utils/tests/test_eveditor.py | 100 +++++++++++++-------------- 5 files changed, 84 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41e72dc57e..cd0f28466b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ from in-game with default command (chiizujin) - [Feature][issue3450]: The default `page` command now tags its `Msg` objects with tag 'page' (category 'comms') and also checks the `Msg`' 'read' lock. - made backwards compatible for old pages. + made backwards compatible for old pages (Griatch) +- Feature: Clean up the default Command variable list shown when a command has + no `func()` defined (Griatch) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) - [Fix][pull3453]: Object aliases not showing in search multi-match @@ -29,6 +31,8 @@ way (chiizujin) - [Fix][pull3464]: EvEditor range:range specification didn't return correct range (chiizujin) +- [Fix][issue3462]: EvEditor :UU and :DD etc commands were not properly + differentiating from their lower-case alternatives (Griatch) - Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) @@ -45,6 +49,7 @@ [pull3464]: https://github.com/evennia/evennia/pull/3464 [pull3467]: https://github.com/evennia/evennia/pull/3467 [issue3450]: https://github.com/evennia/evennia/issues/3450 +[issue3462]: https://github.com/evennia/evennia/issues/3462 ## Evennia 4.0.0 diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 45605f758b..0da3039aaa 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -36,14 +36,13 @@ from weakref import WeakValueDictionary from django.conf import settings from django.utils.translation import gettext as _ -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks, returnValue -from twisted.internet.task import deferLater - from evennia.commands.cmdset import CmdSet from evennia.commands.command import InterruptCommand from evennia.utils import logger, utils from evennia.utils.utils import string_suggestions +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.task import deferLater _IN_GAME_ERRORS = settings.IN_GAME_ERRORS diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 1bc0a49764..3d0d93dbff 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -11,7 +11,6 @@ import re from django.conf import settings from django.urls import reverse from django.utils.text import slugify - from evennia.locks.lockhandler import LockHandler from evennia.utils.ansi import ANSIString from evennia.utils.evtable import EvTable @@ -20,6 +19,7 @@ from evennia.utils.utils import fill, is_iter, lazy_property, make_iter CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES + class InterruptCommand(Exception): """Cleanly interrupt a command.""" @@ -487,31 +487,29 @@ class Command(metaclass=CommandMeta): purposes when making commands. """ - variables = "\n".join( - " |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items() - ) - string = f""" -Command {self} has no defined `func()` - showing on-command variables: -{variables} - """ - # a simple test command to show the available properties - string += "-" * 50 - string += "\n|w%s|n - Command variables from evennia:\n" % self.key - string += "-" * 50 - string += "\nname of cmd (self.key): |w%s|n\n" % self.key - string += "cmd aliases (self.aliases): |w%s|n\n" % self.aliases - string += "cmd locks (self.locks): |w%s|n\n" % self.locks - string += "help category (self.help_category): |w%s|n\n" % self.help_category.capitalize() - string += "object calling (self.caller): |w%s|n\n" % self.caller - string += "object storing cmdset (self.obj): |w%s|n\n" % self.obj - string += "command string given (self.cmdstring): |w%s|n\n" % self.cmdstring - # show cmdset.key instead of cmdset to shorten output - string += fill( - "current cmdset (self.cmdset): |w%s|n\n" - % (self.cmdset.key if self.cmdset.key else self.cmdset.__class__) - ) + output_string = """ +Command \"{cmdname}\" has no defined `func()` method. Available properties on this command are: - self.msg(string) + {variables}""" + variables = [" |w{}|n ({}): {}".format( + key, type(val), f'"{val}"' if isinstance(val, str) else val + ) + for key, val in + (("self.key", self.key), + ("self.cmdname", self.cmdstring), + ("self.raw_cmdname", self.raw_cmdname), + ("self.raw_string", self.raw_string), + ("self.aliases", self.aliases), + ("self.args", self.args), + ("self.caller", self.caller), + ("self.obj", self.obj), + ("self.session", self.session), + ("self.locks", self.locks), + ("self.help_category", self.help_category), + ("self.cmdset", self.cmdset)) + ] + output = output_string.format(cmdname=self.key, variables="\n ".join(variables)) + self.msg(output) def func(self): """ diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 0def632759..f0f2168fca 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -43,7 +43,6 @@ import re from django.conf import settings from django.utils.translation import gettext as _ - from evennia import CmdSet from evennia.commands import cmdhandler from evennia.utils import dedent, fill, is_iter, justify, logger, to_str, utils @@ -468,7 +467,8 @@ class CmdEditorGroup(CmdEditorBase): linebuffer = self.linebuffer lstart, lend = self.lstart, self.lend - cmd = self.cmdstring + # preserve the cmdname including case (otherwise uu and UU would be the same) + cmd = self.raw_string[:len(self.cmdstring)] echo_mode = self.editor._echo_mode if cmd == ":": diff --git a/evennia/utils/tests/test_eveditor.py b/evennia/utils/tests/test_eveditor.py index 7e538e39c4..97bda62630 100644 --- a/evennia/utils/tests/test_eveditor.py +++ b/evennia/utils/tests/test_eveditor.py @@ -13,7 +13,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", ) self.call(eveditor.CmdLineInput(), "line 1", raw_string="line 1", msg="01line 1") @@ -24,7 +24,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", # list whole buffer - cmdstring=":", + raw_string=":", msg="Line Editor []\n01line 1\n02line 2\n" "03line 3\n04line 4\n05line 5\n" "[l:05 w:010 c:0034](:h for help)", @@ -32,7 +32,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), ":", # list empty range - cmdstring=":", + raw_string=":", msg="Line Editor []\n01line 1\n02line 2\n" "03line 3\n04line 4\n05line 5\n" "[l:05 w:010 c:0034](:h for help)", @@ -40,7 +40,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), ":4", # list from start to line 4 - cmdstring=":", + raw_string=":", msg="Line Editor []\n01line 1\n02line 2\n" "03line 3\n04line 4\n" "[l:04 w:008 c:0027](:h for help)", @@ -48,7 +48,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "2:", # list from line 2 to end - cmdstring=":", + raw_string=":", msg="Line Editor []\n02line 2\n03line 3\n" "04line 4\n05line 5\n" "[l:04 w:008 c:0027](:h for help)", @@ -56,7 +56,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "-10:10", # try to list invalid range (too large) - cmdstring=":", + raw_string=":", msg="Line Editor []\n01line 1\n02line 2\n" "03line 3\n04line 4\n05line 5\n" "[l:05 w:010 c:0034](:h for help)", @@ -64,7 +64,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "3:1", # try to list invalid range (reversed) - cmdstring=":", + raw_string=":", msg="Line Editor []\n03line 3\n" "[l:01 w:002 c:0006](:h for help)", ) @@ -74,14 +74,14 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":h", + raw_string=":h", msg=" - any non-command is appended to the end of the buffer.", ) # empty buffer self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", ) # input a string @@ -102,49 +102,49 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", # view buffer + raw_string=":", # view buffer msg="Line Editor []\n01First test line\n" "02Second test line\n[l:02 w:006 c:0032](:h for help)", ) self.call( eveditor.CmdEditorGroup(), "", - cmdstring="::", # view buffer, no linenums + raw_string="::", # view buffer, no linenums msg="Line Editor []\nFirst test line\n" "Second test line\n[l:02 w:006 c:0032](:h for help)", ) self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":::", # add single : alone on row + raw_string=":::", # add single : alone on row msg="Single ':' added to buffer.", ) self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01First test line\n" "02Second test line\n03:\n[l:03 w:007 c:0034](:h for help)", ) self.call( - eveditor.CmdEditorGroup(), "", cmdstring=":dd", msg="Deleted line 3." # delete line + eveditor.CmdEditorGroup(), "", raw_string=":dd", msg="Deleted line 3." # delete line ) self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "First test line\nSecond test line") - self.call(eveditor.CmdEditorGroup(), "", cmdstring=":u", msg="Undid one step.") # undo + self.call(eveditor.CmdEditorGroup(), "", raw_string=":u", msg="Undid one step.") # undo self.assertEqual( self.char1.ndb._eveditor.get_buffer(), "First test line\nSecond test line\n:" ) - self.call(eveditor.CmdEditorGroup(), "", cmdstring=":uu", msg="Redid one step.") # redo + self.call(eveditor.CmdEditorGroup(), "", raw_string=":uu", msg="Redid one step.") # redo self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "First test line\nSecond test line") - self.call(eveditor.CmdEditorGroup(), "", cmdstring=":u", msg="Undid one step.") # undo + self.call(eveditor.CmdEditorGroup(), "", raw_string=":u", msg="Undid one step.") # undo self.assertEqual( self.char1.ndb._eveditor.get_buffer(), "First test line\nSecond test line\n:" ) self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01First test line\n" "02Second test line\n03:\n[l:03 w:007 c:0034](:h for help)", ) @@ -152,31 +152,31 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "Second", - cmdstring=":dw", # delete by word + raw_string=":dw", # delete by word msg="Removed Second for lines 1-4.", ) - self.call(eveditor.CmdEditorGroup(), "", cmdstring=":u", msg="Undid one step.") # undo + self.call(eveditor.CmdEditorGroup(), "", raw_string=":u", msg="Undid one step.") # undo self.call( eveditor.CmdEditorGroup(), "2 Second", - cmdstring=":dw", # delete by word/line + raw_string=":dw", # delete by word/line msg="Removed Second for line 2.", ) self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "First test line\n test line\n:") self.call( - eveditor.CmdEditorGroup(), "2", cmdstring=":p", msg="Copy buffer is empty." # paste + eveditor.CmdEditorGroup(), "2", raw_string=":p", msg="Copy buffer is empty." # paste ) self.call( eveditor.CmdEditorGroup(), "2", - cmdstring=":y", # yank + raw_string=":y", # yank msg="Line 2, [' test line'] yanked.", ) self.call( eveditor.CmdEditorGroup(), "2", - cmdstring=":p", # paste + raw_string=":p", # paste msg="Pasted buffer [' test line'] to line 2.", ) self.assertEqual( @@ -184,31 +184,31 @@ class TestEvEditor(BaseEvenniaCommandTest): ) self.call( - eveditor.CmdEditorGroup(), "3", cmdstring=":x", msg="Line 3, [' test line'] cut." # cut + eveditor.CmdEditorGroup(), "3", raw_string=":x", msg="Line 3, [' test line'] cut." # cut ) self.call( eveditor.CmdEditorGroup(), "2 New Second line", - cmdstring=":i", # insert + raw_string=":i", # insert msg="Inserted 1 new line(s) at line 2.", ) self.call( eveditor.CmdEditorGroup(), "2 New Replaced Second line", # replace - cmdstring=":r", + raw_string=":r", msg="Replaced 1 line(s) at line 2.", ) self.call( eveditor.CmdEditorGroup(), "2 Inserted-", # insert beginning line - cmdstring=":I", + raw_string=":I", msg="Inserted text at beginning of line 2.", ) self.call( eveditor.CmdEditorGroup(), "2 -End", # append end line - cmdstring=":A", + raw_string=":A", msg="Appended text to end of line 2.", ) @@ -229,7 +229,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", ) self.call( @@ -250,7 +250,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":UU", + raw_string=":UU", msg="Reverted all changes to the buffer back to original state.", ) self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "") @@ -260,7 +260,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", ) self.call(eveditor.CmdLineInput(), "line 1.", raw_string="line 1.", msg="01line 1.") @@ -269,20 +269,20 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "2:3", - cmdstring=":", + raw_string=":", msg="Line Editor []\n02line 2.\n03line 3.\n[l:02 w:004 c:0015](:h for help)", ) self.call( eveditor.CmdEditorGroup(), "1:2 line LINE", - cmdstring=":s", + raw_string=":s", msg="Search-replaced line -> LINE for lines 1-2.", ) self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "LINE 1.\nLINE 2.\nline 3.") self.call( eveditor.CmdEditorGroup(), "line MINE", - cmdstring=":s", + raw_string=":s", msg="Search-replaced line -> MINE for lines 1-3.", ) self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "LINE 1.\nLINE 2.\nMINE 3.") @@ -292,14 +292,14 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", ) self.call(eveditor.CmdLineInput(), "line 1.", raw_string="line 1.", msg="01line 1.") self.call(eveditor.CmdLineInput(), "line 2.", raw_string="line 2.", msg="02line 2.") self.call(eveditor.CmdLineInput(), "line 3.", raw_string="line 3.", msg="03line 3.") self.call( - eveditor.CmdEditorGroup(), "", cmdstring=":DD", msg="Cleared 3 lines from buffer." + eveditor.CmdEditorGroup(), "", raw_string=":DD", msg="Cleared 3 lines from buffer." ) self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "") @@ -308,11 +308,11 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", ) self.call(eveditor.CmdLineInput(), "line 1", raw_string="line 1", msg="01line 1") - self.call(eveditor.CmdEditorGroup(), "1:2", cmdstring=":f", msg="Flood filled line 1.") + self.call(eveditor.CmdEditorGroup(), "1:2", raw_string=":f", msg="Flood filled line 1.") self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "line 1") def test_eveditor_COLON_J(self): @@ -320,16 +320,16 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", ) self.call(eveditor.CmdLineInput(), "line 1", raw_string="line 1", msg="01line 1") self.call(eveditor.CmdLineInput(), "l 2", raw_string="l 2", msg="02l 2") self.call(eveditor.CmdLineInput(), "l 3", raw_string="l 3", msg="03l 3") self.call(eveditor.CmdLineInput(), "l 4", raw_string="l 4", msg="04l 4") - self.call(eveditor.CmdEditorGroup(), "2 r", cmdstring=":j", msg="Right-justified line 2.") - self.call(eveditor.CmdEditorGroup(), "3 c", cmdstring=":j", msg="Center-justified line 3.") - self.call(eveditor.CmdEditorGroup(), "4 f", cmdstring=":j", msg="Full-justified line 4.") + self.call(eveditor.CmdEditorGroup(), "2 r", raw_string=":j", msg="Right-justified line 2.") + self.call(eveditor.CmdEditorGroup(), "3 c", raw_string=":j", msg="Center-justified line 3.") + self.call(eveditor.CmdEditorGroup(), "4 f", raw_string=":j", msg="Full-justified line 4.") l1, l2, l3, l4 = tuple(self.char1.ndb._eveditor.get_buffer().split("\n")) self.assertEqual(l1, "line 1") self.assertEqual(l2, " " * 75 + "l 2") @@ -341,44 +341,44 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":", + raw_string=":", msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", ) self.call(eveditor.CmdLineInput(), "line 1.", raw_string="line 1.", msg="01line 1.") self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":dw", + raw_string=":dw", msg="You must give a search word to delete.", ) # self.call( # eveditor.CmdEditorGroup(), # raw_string="", - # cmdstring=":i", + # raw_string=":i", # msg="You need to enter a new line and where to insert it.", # ) # self.call( # eveditor.CmdEditorGroup(), # "", - # cmdstring=":I", + # raw_string=":I", # msg="You need to enter text to insert.", # ) # self.call( # eveditor.CmdEditorGroup(), # "", - # cmdstring=":r", + # raw_string=":r", # msg="You need to enter a replacement string.", # ) self.call( eveditor.CmdEditorGroup(), "", - cmdstring=":s", + raw_string=":s", msg="You must give a search word and something to replace it with.", ) # self.call( # eveditor.CmdEditorGroup(), # "", - # cmdstring=":f", + # raw_string=":f", # msg="Valid justifications are [f]ull (default), [c]enter, [r]right or [l]eft" # ) self.assertEqual(self.char1.ndb._eveditor.get_buffer(), "line 1.") From 86e7b57c0739051940b1cf4b9b02b9e71662b3eb Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 14:14:32 +0200 Subject: [PATCH 32/68] Fix regression in menu_login contrib. Resolve #3460 --- CHANGELOG.md | 3 +++ evennia/contrib/base_systems/menu_login/menu_login.py | 10 +++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0f28466b..a40a4871a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ range (chiizujin) - [Fix][issue3462]: EvEditor :UU and :DD etc commands were not properly differentiating from their lower-case alternatives (Griatch) +- [Fix][issue3460]: The `menu_login` contrib regression caused it to error out + when creating a new character (Griatch) - Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) @@ -50,6 +52,7 @@ [pull3467]: https://github.com/evennia/evennia/pull/3467 [issue3450]: https://github.com/evennia/evennia/issues/3450 [issue3462]: https://github.com/evennia/evennia/issues/3462 +[issue3460]: https://github.com/evennia/evennia/issues/3460 ## Evennia 4.0.0 diff --git a/evennia/contrib/base_systems/menu_login/menu_login.py b/evennia/contrib/base_systems/menu_login/menu_login.py index e2e57a5359..b00dacfb67 100644 --- a/evennia/contrib/base_systems/menu_login/menu_login.py +++ b/evennia/contrib/base_systems/menu_login/menu_login.py @@ -21,14 +21,10 @@ called automatically when a new user connects. """ from django.conf import settings - from evennia import CmdSet, Command, syscmdkeys from evennia.utils.evmenu import EvMenu -from evennia.utils.utils import ( - callables_from_module, - class_from_module, - random_string_from_module, -) +from evennia.utils.utils import (callables_from_module, class_from_module, + random_string_from_module) _CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE _GUEST_ENABLED = settings.GUEST_ENABLED @@ -90,7 +86,7 @@ def node_enter_username(caller, raw_text, **kwargs): else: new_user = False - if new_user and not settings.ACCOUNT_REGISTRATION_ENABLED: + if new_user and not settings.NEW_ACCOUNT_REGISTRATION_ENABLED: caller.msg("Registration is currently disabled.") return None From 22067c32dc76cef8a423c261d77ea9c1752cc309 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 14:50:43 +0200 Subject: [PATCH 33/68] Fix the adding-command tutorial with correct example --- docs/source/Coding/Changelog.md | 12 +++ .../Beginner-Tutorial-Adding-Commands.md | 98 ++++++++----------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 980dcc8c8a..a40a4871a0 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -9,6 +9,11 @@ category after it was created (chiizujin) - [Feature][pull3467]: Add `alias/delete` switch for removing object aliases from in-game with default command (chiizujin) +- [Feature][issue3450]: The default `page` command now tags its `Msg` objects + with tag 'page' (category 'comms') and also checks the `Msg`' 'read' lock. + made backwards compatible for old pages (Griatch) +- Feature: Clean up the default Command variable list shown when a command has + no `func()` defined (Griatch) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) - [Fix][pull3453]: Object aliases not showing in search multi-match @@ -26,6 +31,10 @@ way (chiizujin) - [Fix][pull3464]: EvEditor range:range specification didn't return correct range (chiizujin) +- [Fix][issue3462]: EvEditor :UU and :DD etc commands were not properly + differentiating from their lower-case alternatives (Griatch) +- [Fix][issue3460]: The `menu_login` contrib regression caused it to error out + when creating a new character (Griatch) - Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) - Doc fixes (Griatch, InspectorCaracal) @@ -41,6 +50,9 @@ [pull3463]: https://github.com/evennia/evennia/pull/3463 [pull3464]: https://github.com/evennia/evennia/pull/3464 [pull3467]: https://github.com/evennia/evennia/pull/3467 +[issue3450]: https://github.com/evennia/evennia/issues/3450 +[issue3462]: https://github.com/evennia/evennia/issues/3462 +[issue3460]: https://github.com/evennia/evennia/issues/3460 ## Evennia 4.0.0 diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md index 5d9d3f6e2c..fba93441f7 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md @@ -91,47 +91,34 @@ The `me.cmdset` is the store of all cmdsets stored on us. By giving the path to Now try > echo - Command echo has no defined `func()` - showing on-command variables: - ... - ... + Command "echo" has no defined `func()`. Available properties ... + ...(lots of stuff)... -`echo` works! You should be getting a long list of outputs. The reason for this is that your `echo` function is not really "doing" anything yet and the default function is then to show all useful resources available to you when you use your Command. Let's look at some of those listed: +`echo` works! You should be getting a long list of outputs. Your `echo` function is not really "doing" anything yet and the default function is then to show all useful resources available to you when you use your Command. Let's look at some of those listed: - Command echo has no defined `func()` - showing on-command variables: - obj (): YourName - lockhandler (): cmd:all() - caller (): YourName - cmdname (): echo - raw_cmdname (): echo - cmdstring (): echo - args (): - cmdset (): @mail, about, access, accounts, addcom, alias, allcom, ban, batchcode, batchcommands, boot, cboot, ccreate, - cdesc, cdestroy, cemit, channels, charcreate, chardelete, checklockstring, clientwidth, clock, cmdbare, cmdsets, color, copy, cpattr, create, cwho, delcom, - desc, destroy, dig, dolphin, drop, echo, emit, examine, find, force, get, give, grapevine2chan, help, home, ic, inventory, irc2chan, ircstatus, link, lock, - look, menutest, mudinfo, mvattr, name, nick, objects, ooc, open, option, page, password, perm, pose, public, py, quell, quit, reload, reset, rss2chan, say, - script, scripts, server, service, sessions, set, setdesc, sethelp, sethome, shutdown, spawn, style, tag, tel, test2010, test2028, testrename, testtable, - tickers, time, tunnel, typeclass, unban, unlink, up, up, userpassword, wall, whisper, who, wipe - session (): Griatch(#1)@1:2:7:.:0:.:0:.:1 - account (): Griatch(account 1) - raw_string (): echo - - -------------------------------------------------- - echo - Command variables from evennia: - -------------------------------------------------- - name of cmd (self.key): echo - cmd aliases (self.aliases): [] - cmd locks (self.locks): cmd:all(); - help category (self.help_category): General - object calling (self.caller): Griatch - object storing cmdset (self.obj): Griatch - command string given (self.cmdstring): echo - current cmdset (self.cmdset): ChannelCmdSet +``` +Command "echo" has no defined `func()` method. Available properties on this command are: + self.key (): "echo" + self.cmdname (): "echo" + self.raw_cmdname (): "echo" + self.raw_string (): "echo +" + self.aliases (): [] + self.args (): "" + self.caller (): YourName + self.obj (): YourName + self.session (): YourName(#1)@1:2:7:.:0:.:0:.:1 + self.locks (): "cmd:all();" + self.help_category (): "general" + self.cmdset (... a long list of commands ...) +``` These are all properties you can access with `.` on the Command instance, such as `.key`, `.args` and so on. Evennia makes these available to you and they will be different every time a command is run. The most important ones we will make use of now are: - `caller` - this is 'you', the person calling the command. - - `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find that this would be `" foo bar"`. + - `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find that this would be `" foo bar"` (including the extra space between `echo` and `foo` that you may want to strip away). - `obj` - this is object on which this Command (and CmdSet) "sits". So you, in this case. + - `raw_string` is not commonly used, but it's the completely unmodified input from the user. It even includes the line break used to send to the command to the server (that's why the end-quotes appear on the next line). The reason our command doesn't do anything yet is because it's missing a `func` method. This is what Evennia looks for to figure out what a Command actually does. Modify your `CmdEcho` class: @@ -297,6 +284,8 @@ And Bob would see Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`. ```{code-block} python +:linenos: +:emphasize-lines: 5,6,13,16,19,20,21,23 # in mygame/commands/mycommands.py # ... @@ -321,18 +310,17 @@ class CmdHit(Command): return self.caller.msg(f"You hit {target.key} with full force!") target.msg(f"You got hit by {self.caller.key} with full force!") -# ... +# ... ``` A lot of things to dissect here: -- **Line 3**: The normal `class` header. We inherit from `Command` which we imported at the top of this file. -- **Lines 4-10**: The docstring and help-entry for the command. You could expand on this as much as you wanted. -- **Line 11**: We want to write `hit` to use this command. -- **Line 14**: We strip the whitespace from the argument like before. Since we don't want to have to do `self.args.strip()` over and over, we store the stripped version in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still have the whitespace and is not the same as `args` in this example. +- **Line 5**: The normal `class` header. We inherit from `Command` which we imported at the top of this file. +- **Lines 6-12**: The docstring and help-entry for the command. You could expand on this as much as you wanted. +- **Line 13**: We want to write `hit` to use this command. +- **Line 16**: We strip the whitespace from the argument like before. Since we don't want to have to do `self.args.strip()` over and over, we store the stripped version in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still have the whitespace and is not the same as `args` in this example. ```{sidebar} if-statements - The full form of the if statement is if condition: @@ -343,9 +331,9 @@ The full form of the if statement is ... There can be any number of `elifs` to mark when different branches of the code should run. If `else` is provided, it will run if none of the other conditions were truthy. - ``` -- **Line 15** has our first _conditional_, an `if` statement. This is written on the form `if :` and only if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in Python it's usually easier to learn what is "falsy": + +- **Line 17** has our first _conditional_, an `if` statement. This is written on the form `if :` and only if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in Python it's usually easier to learn what is "falsy": - `False` - this is a reserved boolean word in Python. The opposite is `True`. - `None` - another reserved word. This represents nothing, a null-result or value. - `0` or `0.0` @@ -353,12 +341,20 @@ There can be any number of `elifs` to mark when different branches of the code s - Empty _iterables_ we haven't used yet, like empty lists `[]`, empty tuples `()` and empty dicts `{}`. - Everything else is "truthy". -- **Line 16**'s condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the whole conditional becomes truthy. Let's continue in the code: + The conditional on **Line 16**'s condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the whole conditional becomes truthy. Let's continue in the code: +```{sidebar} Errors in your code + +With longer code snippets to try, it gets more and more likely you'll +make an error and get a `traceback` when you reload. This will either appear +directly in-game or in your log (view it with `evennia -l` in a terminal). + +Don't panic - tracebacks are your friends! They are to be read bottom-up and usually describe exactly where your problem is. Refer to [The Python introduction lesson](./Beginner-Tutorial-Python-basic-introduction.md) for more hints. If you get stuck, reach out to the Evennia community for help. +``` - **Lines 16-17**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string. -- **Line 17**: `return` is a reserved Python word that exits `func` immediately. -- **Line 18**: We use `self.caller.search` to look for the target in the current location. -- **Lines 19-20**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target. In that case, `target` will be `None` and we should just directly `return`. -- **Lines 21-22**: At this point we have a suitable target and can send our punching strings to each. +- **Line 19**: `return` is a reserved Python word that exits `func` immediately. +- **Line 20**: We use `self.caller.search` to look for the target in the current location. +- **Lines 21-22**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target. In that case, `target` will be `None` and we should just directly `return`. +- **Lines 23-24**: At this point we have a suitable target and can send our punching strings to each. Finally we must also add this to a CmdSet. Let's add it to `MyCmdSet`. @@ -374,14 +370,6 @@ class MyCmdSet(CmdSet): ``` -```{sidebar} Errors in your code - -With longer code snippets to try, it gets more and more likely you'll -make an error and get a `traceback` when you reload. This will either appear -directly in-game or in your log (view it with `evennia -l` in a terminal). -Don't panic; tracebacks are your friends - they are to be read bottom-up and usually describe exactly where your problem is. Refer to [The Python introduction lesson](./Beginner-Tutorial-Python-basic-introduction.md) for more hints. If you get stuck, reach out to the Evennia community for help. -``` - Note that since we did `py self.cmdset.remove("commands.mycommands.MyCmdSet")` earlier, this cmdset is no longer available on our Character. Instead we will add these commands directly to our default cmdset. ```python From 8bad4cc9a34eda7ffd9dbaddec3cd462ed44cde5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 14:55:22 +0200 Subject: [PATCH 34/68] Update Changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a40a4871a0..57881c5925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ - [Feature][issue3450]: The default `page` command now tags its `Msg` objects with tag 'page' (category 'comms') and also checks the `Msg`' 'read' lock. made backwards compatible for old pages (Griatch) +- [Feature][pull3466]: Add optional `no_article` kwarg to + `DefaultObject.get_numbered_name` for the system to skip adding automatic + articles. (chiizujin) - Feature: Clean up the default Command variable list shown when a command has no `func()` defined (Griatch) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in @@ -36,7 +39,7 @@ - [Fix][issue3460]: The `menu_login` contrib regression caused it to error out when creating a new character (Griatch) - Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) -- Doc fixes (Griatch, InspectorCaracal) +- Doc fixes (Griatch, InspectorCaracal, homeofpoe) [pull3421]: https://github.com/evennia/evennia/pull/3421 [pull3446]: https://github.com/evennia/evennia/pull/3446 @@ -49,6 +52,7 @@ [pull3459]: https://github.com/evennia/evennia/pull/3459 [pull3463]: https://github.com/evennia/evennia/pull/3463 [pull3464]: https://github.com/evennia/evennia/pull/3464 +[pull3466]: https://github.com/evennia/evennia/pull/3466 [pull3467]: https://github.com/evennia/evennia/pull/3467 [issue3450]: https://github.com/evennia/evennia/issues/3450 [issue3462]: https://github.com/evennia/evennia/issues/3462 From 3d89a1608a60fd051c0cfcc5f59c6cf0c86f6ba9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 15:20:07 +0200 Subject: [PATCH 35/68] Add `DefaultObject.filter_visible` method. Deprecate old visible generating methods. Resolve #3461 --- CHANGELOG.md | 8 +++++ evennia/objects/objects.py | 61 +++++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57881c5925..6c7be3c330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Main branch +- [Deprecation]: `DefaultObject.get_visible_contents` - unused in core, will be + removed. Use the new `.filter_visible` together with the `.get_display_*` methods instead.. +- [Deprecation]: `DefaultObject.get_content_names` - unused in core, will be + removed. Use the `DefaultObject.get_display_*` methods instead. + - [Feature][pull3421]: New `utils.compress_whitespace` utility used with default object's `.format_appearance` to make it easier to overload without adding line breaks in hook returns. (InspectorCaracal) @@ -17,6 +22,8 @@ articles. (chiizujin) - Feature: Clean up the default Command variable list shown when a command has no `func()` defined (Griatch) +- [Feature][issue3461]: Add `DefaultObject.filter_display_visible` helper method + to make it easier to customize object visibility rules. (Griatch) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) - [Fix][pull3453]: Object aliases not showing in search multi-match @@ -57,6 +64,7 @@ [issue3450]: https://github.com/evennia/evennia/issues/3450 [issue3462]: https://github.com/evennia/evennia/issues/3462 [issue3460]: https://github.com/evennia/evennia/issues/3460 +[issue3461]: https://github.com/evennia/evennia/issues/3461 ## Evennia 4.0.0 diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 4b4fed4ad1..353a46b3ba 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1419,6 +1419,27 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): self.at_access(result, accessing_obj, access_type, **kwargs) return result + def filter_visible(self, obj_list, looker, **kwargs): + """ + Filter a list of objects to only include those that are visible to the looker. + + Args: + obj_list (list): List of objects to filter. + looker (Object): Object doing the looking. + **kwargs: Arbitrary data for use when overriding. + Returns: + list: The filtered list of visible objects. + Notes: + By default this simply checks the 'view' and 'search' locks on each object in the list. + Override this + method to implement custom visibility mechanics. + + """ + return [obj for obj in obj_list + if (obj.access(looker, "view") + and obj.access(looker, "search", default=True)) + ] + # name and return_appearance hooks def get_display_name(self, looker=None, **kwargs): @@ -1557,11 +1578,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): str: The exits display data. """ - - def _filter_visible(obj_list): - return (obj for obj in obj_list if obj != looker and obj.access(looker, "view")) - - exits = _filter_visible(self.contents_get(content_type="exit")) + exits = self.filter_visible(self.contents_get(content_type="exit"), looker, **kwargs) exit_names = iter_to_str(exi.get_display_name(looker, **kwargs) for exi in exits) return f"|wExits:|n {exit_names}" if exit_names else "" @@ -1577,11 +1594,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): str: The character display data. """ - - def _filter_visible(obj_list): - return (obj for obj in obj_list if obj != looker and obj.access(looker, "view")) - - characters = _filter_visible(self.contents_get(content_type="character")) + characters = self.filter_visible(self.contents_get(content_type="character"), looker, + **kwargs) character_names = iter_to_str( char.get_display_name(looker, **kwargs) for char in characters ) @@ -1599,12 +1613,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): str: The things display data. """ - - def _filter_visible(obj_list): - return (obj for obj in obj_list if obj != looker and obj.access(looker, "view")) - # sort and handle same-named things - things = _filter_visible(self.contents_get(content_type="object")) + things = self.filter_visible(self.contents_get(content_type="object"), looker, **kwargs) grouped_things = defaultdict(list) for thing in things: @@ -2328,8 +2338,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): def get_visible_contents(self, looker, **kwargs): """ + DEPRECATED Get all contents of this object that a looker can see (whatever that means, by default it - checks the 'view' and 'search' locks), grouped by type. Helper method to return_appearance. + checks the 'view' and 'search' locks and excludes the looker themselves), grouped by type. + Helper method to return_appearance. Args: looker (Object): The entity looking. @@ -2342,23 +2354,18 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ - def filter_visible(obj_list): - return [ - obj - for obj in obj_list - if obj != looker - and obj.access(looker, "view") - and obj.access(looker, "search", default=True) - ] + def _filter_visible(obj_list): + return [obj for obj in self.filter_visible(obj_list, looker, **kwargs) if obj != looker] return { - "exits": filter_visible(self.contents_get(content_type="exit")), - "characters": filter_visible(self.contents_get(content_type="character")), - "things": filter_visible(self.contents_get(content_type="object")), + "exits": _filter_visible(self.contents_get(content_type="exit")), + "characters": _filter_visible(self.contents_get(content_type="character")), + "things": _filter_visible(self.contents_get(content_type="object")), } def get_content_names(self, looker, **kwargs): """ + DEPRECATED Get the proper names for all contents of this object. Helper method for return_appearance. From cf5123ad568c9df832980d48bcaa1a9e4b65bb75 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 15:25:37 +0200 Subject: [PATCH 36/68] Update changelog --- docs/source/Coding/Changelog.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index a40a4871a0..6c7be3c330 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -2,6 +2,11 @@ ## Main branch +- [Deprecation]: `DefaultObject.get_visible_contents` - unused in core, will be + removed. Use the new `.filter_visible` together with the `.get_display_*` methods instead.. +- [Deprecation]: `DefaultObject.get_content_names` - unused in core, will be + removed. Use the `DefaultObject.get_display_*` methods instead. + - [Feature][pull3421]: New `utils.compress_whitespace` utility used with default object's `.format_appearance` to make it easier to overload without adding line breaks in hook returns. (InspectorCaracal) @@ -12,8 +17,13 @@ - [Feature][issue3450]: The default `page` command now tags its `Msg` objects with tag 'page' (category 'comms') and also checks the `Msg`' 'read' lock. made backwards compatible for old pages (Griatch) +- [Feature][pull3466]: Add optional `no_article` kwarg to + `DefaultObject.get_numbered_name` for the system to skip adding automatic + articles. (chiizujin) - Feature: Clean up the default Command variable list shown when a command has no `func()` defined (Griatch) +- [Feature][issue3461]: Add `DefaultObject.filter_display_visible` helper method + to make it easier to customize object visibility rules. (Griatch) - [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in `get_numbered_name` for better grammatical form (InspectorCaracal) - [Fix][pull3453]: Object aliases not showing in search multi-match @@ -36,7 +46,7 @@ - [Fix][issue3460]: The `menu_login` contrib regression caused it to error out when creating a new character (Griatch) - Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) -- Doc fixes (Griatch, InspectorCaracal) +- Doc fixes (Griatch, InspectorCaracal, homeofpoe) [pull3421]: https://github.com/evennia/evennia/pull/3421 [pull3446]: https://github.com/evennia/evennia/pull/3446 @@ -49,10 +59,12 @@ [pull3459]: https://github.com/evennia/evennia/pull/3459 [pull3463]: https://github.com/evennia/evennia/pull/3463 [pull3464]: https://github.com/evennia/evennia/pull/3464 +[pull3466]: https://github.com/evennia/evennia/pull/3466 [pull3467]: https://github.com/evennia/evennia/pull/3467 [issue3450]: https://github.com/evennia/evennia/issues/3450 [issue3462]: https://github.com/evennia/evennia/issues/3462 [issue3460]: https://github.com/evennia/evennia/issues/3460 +[issue3461]: https://github.com/evennia/evennia/issues/3461 ## Evennia 4.0.0 From d145b5b96cb73602a6f33bc3f21d408bd3970a3a Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 15:34:48 +0200 Subject: [PATCH 37/68] Fix example indent in docstring for get_numbered_name --- evennia/objects/objects.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 353a46b3ba..4d765e6500 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1505,12 +1505,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Examples: :: - - obj.get_numbered_name(3, looker, key="foo") -> ("a foo", "three foos") - - obj.get_numbered_name(1, looker, key="Foobert", return_string=True) - -> "a Foobert" - - obj.get_numbered_name(1, looker, key="Foobert", return_string=True, no_article=True) - -> "Foobert" + obj.get_numbered_name(3, looker, key="foo") + -> ("a foo", "three foos") + obj.get_numbered_name(1, looker, key="Foobert", return_string=True) + -> "a Foobert" + obj.get_numbered_name(1, looker, key="Foobert", return_string=True, no_article=True) + -> "Foobert" """ plural_category = "plural_key" key = kwargs.get("key", self.get_display_name(looker)) From 870c0f5f75e15c2269a4bb9f5eb537e1668699f3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 17:47:24 +0200 Subject: [PATCH 38/68] Clean up objects/objects.py docstrings --- evennia/objects/objects.py | 541 +++++++++++++++++++------------------ 1 file changed, 272 insertions(+), 269 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 4d765e6500..57d29dd722 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -4,8 +4,6 @@ This module defines the basic `DefaultObject` and its children These are the (default) starting points for all in-game visible entities. -This is the v1.0 develop version (for ref in doc building). - """ import time import typing @@ -54,7 +52,7 @@ class ObjectSessionHandler: Initializes the handler. Args: - obj (Object): The object on which the handler is defined. + obj (DefaultObject): The object on which the handler is defined. """ self.obj = obj @@ -81,7 +79,7 @@ class ObjectSessionHandler: sessid (int, optional): A specific session id. Returns: - sessions (list): The sessions connected to this object. If `sessid` is given, + list: The sessions connected to this object. If `sessid` is given, this is a list of one (or zero) elements. Notes: @@ -111,7 +109,7 @@ class ObjectSessionHandler: Alias to get(), returning all sessions. Returns: - sessions (list): All sessions. + list: All sessions. """ return self.get() @@ -146,7 +144,7 @@ class ObjectSessionHandler: Remove session from handler. Args: - session (Session or int): Session or session id to remove. + Session or int: Session or session id to remove. """ try: @@ -174,7 +172,7 @@ class ObjectSessionHandler: Get amount of sessions connected. Returns: - sesslen (int): Number of sessions handled. + int: Number of sessions handled. """ return len(self._sessid_cache) @@ -186,7 +184,7 @@ class ObjectSessionHandler: class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ - This is the root typeclass object, representing all entities that + This is the root Object typeclass, representing all entities that have an actual presence in-game. DefaultObjects generally have a location. They can also be manipulated and looked at. Game entities you define should inherit from DefaultObject at some distance. @@ -223,23 +221,27 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): @lazy_property def cmdset(self): + """CmdSetHandler""" return CmdSetHandler(self, True) @lazy_property def scripts(self): + """ScriptHandler""" return ScriptHandler(self) @lazy_property def nicks(self): + """NickHandler""" return NickHandler(self, ModelAttributeBackend) @lazy_property def sessions(self): + """SessionHandler""" return ObjectSessionHandler(self) @property def is_connected(self): - # we get an error for objects subscribed to channels without this + """True if this object is associated with an Account with any connected sessions.""" if self.account: # seems sane to pass on the account return self.account.is_connected else: @@ -247,11 +249,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): @property def has_account(self): - """ - Convenience property for checking if an active account is - currently connected to this object. - - """ + """True is this object has an associated account.""" return self.sessions.count() def get_cmdset_providers(self) -> dict[str, "CmdSetProvider"]: @@ -272,10 +270,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): @property def is_superuser(self): - """ - Check if user has an account, and if so, if it is a superuser. - - """ + """True if this object has an account and that account is a superuser.""" return ( self.db_account and self.db_account.is_superuser @@ -289,23 +284,23 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): This should be publically available. Args: - exclude (Object): Object to exclude from returned + exclude (DefaultObject): Object to exclude from returned contents list content_type (str): A content_type to filter by. None for no filtering. Returns: - contents (list): List of contents of this Object. + list: List of contents of this Object. Notes: - Also available as the `contents` property, minus exclusion - and filtering. + Also available as the `.contents` property, but that doesn't allow for exclusion and + filtering on content-types. """ return self.contents_cache.get(exclude=exclude, content_type=content_type) def contents_set(self, *args): - "You cannot replace this property" + "Makes sure `.contents` is read-only. Raises `AttributeError` if trying to set it." raise AttributeError( "{}.contents is read-only. Use obj.move_to or " "obj.location to move an object here.".format(self.__class__) @@ -317,7 +312,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): def exits(self): """ Returns all exits from this object, i.e. all objects at this - location having the property destination != `None`. + location having the property .destination != `None`. """ return [exi for exi in self.contents if exi.destination] @@ -353,8 +348,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): **kwargs (any): These are the same as passed to the `search` method. Returns: - tuple: `(should_return, str or Obj)`, where `should_return` is a boolean indicating - the `.search` method should return the result immediately without further + tuple: A tuple `(should_return, str or Obj)`, where `should_return` is a boolean + indicating the `.search` method should return the result immediately without further processing. If `should_return` is `True`, the second element of the tuple is the result that is returned. @@ -369,15 +364,15 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): def get_search_candidates(self, searchdata, **kwargs): """ - Get the candidates for a search. Also the `candidates` provided to the - search function is included, and could be modified in-place here. + Helper for the `.search` method. Get the candidates for a search. Also the `candidates` + provided to the search function is included, and could be modified in-place here. Args: searchdata (str): The search criterion (could be modified by `get_search_query_replacement`). **kwargs (any): These are the same as passed to the `search` method. Returns: - list: A list of objects to search between. + list: A list of objects possibly relevant for the search. Notes: If `searchdata` is a #dbref, this method should always return `None`. This is because @@ -429,8 +424,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): **kwargs, ): """ - This is a wrapper for actually searching for objects, used by the `search` method. - This is broken out into a separate method to allow for easier overriding in child classes. + Helper for the `.search` method. This is a wrapper for actually searching for objects, used + by the `search` method. This is broken out into a separate method to allow for easier + overriding in child classes. Args: searchdata (str): The search criterion. @@ -441,6 +437,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): use_dbref (bool): Allow dbref search. tags (list): Tags to search for. + Returns: + queryset or iterable: The result of the search. + """ return ObjectDB.objects.search_object( @@ -462,10 +461,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): results (list): The list of results from the search. Returns: - tuple: `(stacked, results)`, where `stacked` is a boolean indicating if the - result is stacked and `results` is the list of results to return. If `stacked` - is True, the ".search" method will return `results` immediately without further - processing (it will not result in a multimatch-error). + tuple: A tuple `(stacked, results)`, where `stacked` is a boolean indicating if the + result is stacked and `results` is the list of results to return. If `stacked` + is True, the ".search" method will return `results` immediately without further + processing (it will not result in a multimatch-error). Notes: The `stacked` keyword argument is an integer that controls the max size of each stack @@ -699,16 +698,17 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): echo eventual standard error messages. Default `False`. Returns: - result (Account, None or list): Just what is returned depends on - the `quiet` setting: - - `quiet=True`: No match or multumatch auto-echoes errors - to self.msg, then returns `None`. The esults are passed - through `settings.SEARCH_AT_RESULT` and - `settings.SEARCH_AT_MULTIMATCH_INPUT`. If there is a - unique match, this will be returned. - - `quiet=True`: No automatic error messaging is done, and - what is returned is always a list with 0, 1 or more - matching Accounts. + DefaultAccount, None or list: What is returned depends on + the `quiet` setting: + + - `quiet=False`: No match or multumatch auto-echoes errors + to self.msg, then returns `None`. The esults are passed + through `settings.SEARCH_AT_RESULT` and + `settings.SEARCH_AT_MULTIMATCH_INPUT`. If there is a + unique match, this will be returned. + - `quiet=True`: No automatic error messaging is done, and + what is returned is always a list with 0, 1 or more + matching Accounts. """ if isinstance(searchdata, str): @@ -741,15 +741,15 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): change operating paramaters for commands at run-time. Returns: - defer (Deferred): This is an asynchronous Twisted object that - will not fire until the command has actually finished - executing. To overload this one needs to attach - callback functions to it, with addCallback(function). - This function will be called with an eventual return - value from the command execution. This return is not - used at all by Evennia by default, but might be useful - for coders intending to implement some sort of nested - command structure. + Deferred: This is an asynchronous Twisted object that + will not fire until the command has actually finished + executing. To overload this one needs to attach + callback functions to it, with addCallback(function). + This function will be called with an eventual return + value from the command execution. This return is not + used at all by Evennia by default, but might be useful + for coders intending to implement some sort of nested + command structure. """ # break circular import issues @@ -768,28 +768,27 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ Emits something to a session attached to the object. - Args: - text (str or tuple, optional): The message to send. This + Keyword Args: + text (str or tuple): The message to send. This is treated internally like any send-command, so its value can be a tuple if sending multiple arguments to the `text` oob command. - from_obj (obj or list, optional): object that is sending. If + from_obj (DefaultObject, DefaultAccount, Session, or list): object that is sending. If given, at_msg_send will be called. This value will be passed on to the protocol. If iterable, will execute hook on all entities in it. - session (Session or list, optional): Session or list of + session (Session or list): Session or list of Sessions to relay data to, if any. If set, will force send to these sessions. If unset, who receives the message depends on the MULTISESSION_MODE. - options (dict, optional): Message-specific option-value + options (dict): Message-specific option-value pairs. These will be applied at the protocol level. - Keyword Args: - any (string or tuples): All kwarg keys not listed above + **kwargs (string or tuples): All kwarg keys not listed above will be treated as send-command names and their arguments (which can be a string or a tuple). Notes: - `at_msg_receive` will be called on this Object. + The `at_msg_receive` method will be called on this Object. All extra kwargs will be passed on to the protocol. """ @@ -822,16 +821,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): for session in sessions: session.data_out(**kwargs) - def get_contents_unique(self, caller=None): - """ - Get a mapping of contents that are visually unique to the caller, along with - how many of each there are. - - Args: - caller (Object, optional): The object to check visibility from. If not given, - the current object will be used. - """ - def for_contents(self, func, exclude=None, **kwargs): """ Runs a function on every object contained within this one. @@ -908,11 +897,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Examples: - Let's assume - - `player1.key -> "Player1"`, - `player1.get_display_name(looker=player2) -> "The First girl"` - - `player2.key -> "Player2"`, - `player2.get_display_name(looker=player1) -> "The Second girl"` + Let's assume: + + - `player1.key` -> "Player1", + - `player1.get_display_name(looker=player2)` -> "The First girl" + - `player2.key` -> "Player2", + - `player2.get_display_name(looker=player1)` -> "The Second girl" Actor-stance: :: @@ -989,12 +979,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Moves this object to a new location. Args: - destination (Object): Reference to the object to move to. This + destination (DefaultObject): Reference to the object to move to. This can also be an exit object, in which case the destination property is used as destination. quiet (bool): If true, turn off the calling of the emit hooks (announce_move_to/from etc) - emit_to_obj (Object): object to receive error messages + emit_to_obj (DefaultObject): object to receive error messages use_destination (bool): Default is for objects to use the "destination" property of destinations as the target to move to. Turning off this keyword allows objects to move "inside" exit objects. @@ -1009,31 +999,28 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): the text message generated by announce_move_to and announce_move_from by defining their {"type": move_type} for outgoing text. This can be used for altering messages and/or overloaded hook behaviors. - - Keyword Args: - Passed on to announce_move_to and announce_move_from hooks. - Exits will set the "exit_obj" kwarg to themselves. + **kwargs: Passed on to announce_move_to and announce_move_from hooks. Exits will set + the "exit_obj" kwarg to themselves. Returns: - result (bool): True/False depending on if there were problems with the move. - This method may also return various error messages to the - `emit_to_obj`. + bool: True/False depending on if there were problems with the move. This method may also + return various error messages to the `emit_to_obj`. Notes: No access checks are done in this method, these should be handled before calling `move_to`. - The `DefaultObject` hooks called (if `move_hooks=True`) are, in order: + The `DefaultObject` hooks called (if `move_hooks=True`) are, in order: - 1. `self.at_pre_move(destination)` (abort if return False) - 2. `source_location.at_pre_object_leave(self, destination)` (abort if return False) - 3. `destination.at_pre_object_receive(self, source_location)` (abort if return False) - 4. `source_location.at_object_leave(self, destination)` - 5. `self.announce_move_from(destination)` - 6. (move happens here) - 7. `self.announce_move_to(source_location)` - 8. `destination.at_object_receive(self, source_location)` - 9. `self.at_post_move(source_location)` + 1. `self.at_pre_move(destination)` (abort if return False) + 2. `source_location.at_pre_object_leave(self, destination)` (abort if return False) + 3. `destination.at_pre_object_receive(self, source_location)` (abort if return False) + 4. `source_location.at_object_leave(self, destination)` + 5. `self.announce_move_from(destination)` + 6. (move happens here) + 7. `self.announce_move_to(source_location)` + 8. `destination.at_object_receive(self, source_location)` + 9. `self.at_post_move(source_location)` """ @@ -1206,12 +1193,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Classmethod called during .create() to determine default locks for the object. Args: - account (Account): Account to attribute this object to. + account (DefaultAccount): Account to attribute this object to. caller (DefaultObject): The object which is creating this one. **kwargs: Arbitrary input. Returns: - lockstring (str): A lockstring to use for this object. + str: A lockstring to use for this object. """ pid = f"pid({account.id})" if account else None cid = f"id({caller.id})" if caller else None @@ -1239,15 +1226,17 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Keyword Args: - account (Account): Account to attribute this object to. + account (DefaultAccount): Account to attribute this object to. caller (DefaultObject): The object which is creating this one. description (str): Brief description for this object. ip (str): IP address of creator (for object auditing). method (str): The method of creation. Defaults to "create". Returns: - object (Object): A newly created object of the given typeclass. - errors (list): A list of errors in string form, if any. + tuple: A tuple (Object, errors): A newly created object of the given typeclass. This + will be `None` if there are errors. The second element is then a list of errors that + occurred during creation. If this is empty, it's safe to assume the object was created + successfully. """ errors = [] @@ -1303,7 +1292,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): new_key (string): New key/name of copied object. If new_key is not specified, the copy will be named _copy by default. Returns: - copy (Object): A copy of this object. + Object: A copy of this object. """ @@ -1333,10 +1322,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): covered by .copy(), this can be used to deal with it. Args: - new_obj (Object): The new Copy of this object. + new_obj (DefaultObject): The new Copy of this object. - Returns: - None """ pass @@ -1347,8 +1334,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): locations, as well as clean up all exits to/from the object. Returns: - noerror (bool): Returns whether or not the delete completed - successfully or not. + bool: Whether or not the delete completed successfully or not. """ global _ScriptDB @@ -1400,14 +1386,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): in whatever way. Args: - accessing_obj (Object): Object trying to access this one. + accessing_obj (DefaultObject): Object trying to access this one. access_type (str, optional): Type of access sought. default (bool, optional): What to return if no lock of access_type was found. no_superuser_bypass (bool, optional): If `True`, don't skip lock check for superuser (be careful with this one). - - Keyword Args: - Passed on to the at_access hook along with the result of the access check. + **kwargs: Passed on to the at_access hook along with the result of the access check. """ result = super().access( @@ -1425,7 +1409,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: obj_list (list): List of objects to filter. - looker (Object): Object doing the looking. + looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. Returns: list: The filtered list of visible objects. @@ -1447,7 +1431,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Displays the name of the object in a viewer-aware manner. Args: - looker (TypedObject): The object or account that is looking at or getting information + looker (DefaultObject): The object or account that is looking at or getting information for this object. Returns: @@ -1466,11 +1450,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): object's dbref in parentheses, if the looker has permission to see it. Args: - looker (Object): The object looking at this object. + looker (DefaultObject): The object looking at this object. Returns: str: The dbref of this object, if the looker has permission to see it. Otherwise, an - empty string is returned. + empty string is returned. Notes: By default, this becomes a string (#dbref) attached to the object's name. @@ -1490,7 +1474,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: count (int): Number of objects of this type - looker (Object): Onlooker. Not used by default. + looker (DefaultObject): Onlooker. Not used by default. Keyword Args: key (str): Optional key to pluralize. If not given, the object's `.get_display_name()` @@ -1501,7 +1485,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Returns: tuple: This is a tuple `(str, str)` with the singular and plural forms of the key - including the count. + including the count. Examples: :: @@ -1547,7 +1531,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Get the 'header' component of the object description. Called by `return_appearance`. Args: - looker (Object): Object doing the looking. + looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. Returns: str: The header display string. @@ -1560,7 +1544,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Get the 'desc' component of the object description. Called by `return_appearance`. Args: - looker (Object): Object doing the looking. + looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. Returns: str: The desc display string. @@ -1573,7 +1557,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Get the 'exits' component of the object description. Called by `return_appearance`. Args: - looker (Object): Object doing the looking. + looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. Returns: str: The exits display data. @@ -1589,7 +1573,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Get the 'characters' component of the object description. Called by `return_appearance`. Args: - looker (Object): Object doing the looking. + looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. Returns: str: The character display data. @@ -1608,7 +1592,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Get the 'things' component of the object description. Called by `return_appearance`. Args: - looker (Object): Object doing the looking. + looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. Returns: str: The things display data. @@ -1635,7 +1619,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Get the 'footer' component of the object description. Called by `return_appearance`. Args: - looker (Object): Object doing the looking. + looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. Returns: str: The footer display string. @@ -1649,7 +1633,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: appearance (str): The compiled appearance string. - looker (Object): Object doing the looking. + looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. Returns: str: The final formatted output. @@ -1662,19 +1646,19 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Main callback used by 'look' for the object to describe itself. This formats a description. By default, this looks for the `appearance_template` string set on this class and populates it with formatting keys - 'name', 'desc', 'exits', 'characters', 'things' as well as - (currently empty) 'header'/'footer'. Each of these values are - retrieved by a matching method `.get_display_*`, such as `get_display_name`, - `get_display_footer` etc. + 'name', 'desc', 'exits', 'characters', 'things' as well as + (currently empty) 'header'/'footer'. Each of these values are + retrieved by a matching method `.get_display_*`, such as `get_display_name`, + `get_display_footer` etc. Args: - looker (Object): Object doing the looking. Passed into all helper methods. + looker (DefaultObject): Object doing the looking. Passed into all helper methods. **kwargs (dict): Arbitrary, optional arguments for users overriding the call. This is passed into all helper methods. Returns: str: The description of this entity. By default this includes - the entity's name, description and any contents inside it. + the entity's name, description and any contents inside it. Notes: To simply change the layout of how the object displays itself (like @@ -1855,7 +1839,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): have no cmdsets. Keyword Args: - caller (Object, Account or Session): The object requesting the cmdsets. + caller (DefaultObject, DefaultAccount or Session): The object requesting the cmdsets. current (CmdSet): The current merged cmdset. force_init (bool): If `True`, force a re-build of the cmdset. (seems unused) **kwargs: Arbitrary input for overloads. @@ -1868,7 +1852,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Called by the CommandHandler to get a list of cmdsets to merge. Args: - caller (obj): The object requesting the cmdsets. + caller (DefaultObject): The object requesting the cmdsets. current (cmdset): The current merged cmdset. **kwargs: Arbitrary input for overloads. @@ -1883,9 +1867,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): it. Args: - account (Account): This is the connecting account. + account (DefaultAccount): This is the connecting account. session (Session): Session controlling the connection. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). """ @@ -1897,12 +1881,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Account<->Object links have been established. Args: - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). - Note: - You can use `self.account` and `self.sessions.get()` to get - account and sessions at this point; the last entry in the - list from `self.sessions.get()` is the latest Session + Notes: + You can use `self.account` and `self.sessions.get()` to get account and sessions at this + point; the last entry in the list from `self.sessions.get()` is the latest Session puppeting this Object. """ @@ -1915,9 +1898,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): this Account. Args: - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). - Note: + Notes: You can use `self.account` and `self.sessions.get()` to get account and sessions at this point; the last entry in the list from `self.sessions.get()` is the latest Session @@ -1932,12 +1915,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): this object, severing all connections. Args: - account (Account): The account object that just disconnected + account (DefaultAccount): The account object that just disconnected from this object. This can be `None` if this is called automatically (such as after a cleanup operation). session (Session): Session id controlling the connection that just disconnected. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). """ @@ -1972,9 +1955,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): result (bool): The outcome of the access call. accessing_obj (Object or Account): The entity trying to gain access. access_type (str): The type of access that was requested. - - Keyword Args: - Unused by default, added for possible expandability in a game. + **kwargs: Arbitrary, optional arguments. Unused by default. """ pass @@ -1987,19 +1968,19 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): destination. Return False to abort move. Args: - destination (Object): The object we are moving to + destination (DefaultObject): The object we are moving to move_type (str): The type of move. "give", "traverse", etc. This is an arbitrary string provided to obj.move_to(). Useful for altering messages or altering logic depending on the kind of movement. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Returns: bool: If we should move or not. Notes: - If this method returns False/None, the move is cancelled + If this method returns `False` or `None`, the move is cancelled before it is even started. """ @@ -2011,14 +1992,16 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): previously 'inside' it. Return False to abort move. Args: - leaving_object (Object): The object that is about to leave. - destination (Object): Where object is going to. - **kwargs (dict): Arbitrary, optional arguments for users + leaving_object (DefaultObject): The object that is about to leave. + destination (DefaultObject): Where object is going to. + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Returns: bool: If `leaving_object` should be allowed to leave or not. - Notes: If this method returns False, None, the move is canceled before + Notes: + + If this method returns `False` or `None`, the move is canceled before it even started. """ @@ -2031,16 +2014,18 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): remains where it was. Args: - arriving_object (Object): The object moved into this one - source_location (Object): Where `moved_object` came from. + arriving_object (DefaultObject): The object moved into this one + source_location (DefaultObject): Where `moved_object` came from. Note that this could be `None`. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Returns: bool: If False, abort move and `moved_obj` remains where it was. - Notes: If this method returns False, None, the move is canceled before + Notes: + + If this method returns `False` or `None`, the move is canceled before it even started. """ @@ -2056,23 +2041,26 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): location. Args: - destination (Object): The place we are going to. + destination (DefaultObject): The place we are going to. msg (str, optional): a replacement message. mapping (dict, optional): additional mapping objects. move_type (str): The type of move. "give", "traverse", etc. This is an arbitrary string provided to obj.move_to(). Useful for altering messages or altering logic depending on the kind of movement. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). - You can override this method and call its parent with a - message to simply change the default message. In the string, - you can use the following as mappings (between braces): - object: the object which is moving. - exit: the exit from which the object is moving (if found). - origin: the location of the object before the move. - destination: the location of the object after moving. + Notes: + + You can override this method and call its parent with a + message to simply change the default message. In the string, + you can use the following as mappings: + + - `{object}`: the object which is moving. + - `{exit}`: the exit from which the object is moving (if found). + - `{origin}`: the location of the object before the move. + - `{destination}`: the location of the object after moving. """ if not self.location: @@ -2108,24 +2096,27 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): we are standing in the new location. Args: - source_location (Object): The place we came from + source_location (DefaultObject): The place we came from msg (str, optional): the replacement message if location. mapping (dict, optional): additional mapping objects. move_type (str): The type of move. "give", "traverse", etc. This is an arbitrary string provided to obj.move_to(). Useful for altering messages or altering logic depending on the kind of movement. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Notes: + You can override this method and call its parent with a message to simply change the default message. In the string, you can use the following as mappings (between braces): - object: the object which is moving. - exit: the exit from which the object is moving (if found). - origin: the location of the object before the move. - destination: the location of the object after moving. + + + - `{object}`: the object which is moving. + - `{exit}`: the exit from which the object is moving (if found). + - `{origin}`: the location of the object before the move. + - `{destination}`: the location of the object after moving. """ @@ -2179,12 +2170,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): now in. Args: - source_location (Object): Where we came from. This may be `None`. + source_location (DefaultObject): Where we came from. This may be `None`. move_type (str): The type of move. "give", "traverse", etc. This is an arbitrary string provided to obj.move_to(). Useful for altering messages or altering logic depending on the kind of movement. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). """ @@ -2198,13 +2189,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Called just before an object leaves from inside this object Args: - moved_obj (Object): The object leaving - target_location (Object): Where `moved_obj` is going. + moved_obj (DefaultObject): The object leaving + target_location (DefaultObject): Where `moved_obj` is going. move_type (str): The type of move. "give", "traverse", etc. This is an arbitrary string provided to obj.move_to(). Useful for altering messages or altering logic depending on the kind of movement. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). """ @@ -2215,14 +2206,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Called after an object has been moved into this object. Args: - moved_obj (Object): The object moved into this one - source_location (Object): Where `moved_object` came from. + moved_obj (DefaultObject): The object moved into this one + source_location (DefaultObject): Where `moved_object` came from. Note that this could be `None`. move_type (str): The type of move. "give", "traverse", etc. This is an arbitrary string provided to obj.move_to(). Useful for altering messages or altering logic depending on the kind of movement. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). """ @@ -2239,9 +2230,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): called. Args: - traversing_object (Object): Object traversing us. - target_location (Object): Where target is going. - **kwargs (dict): Arbitrary, optional arguments for users + traversing_object (DefaultObject): Object traversing us. + target_location (DefaultObject): Where target is going. + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). """ @@ -2254,8 +2245,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Exit) Args: - traversing_object (Object): The object traversing us. - source_location (Object): Where `traversing_object` came from. + traversing_object (DefaultObject): The object traversing us. + source_location (DefaultObject): Where `traversing_object` came from. **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). @@ -2273,8 +2264,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): some reason. Args: - traversing_object (Object): The object that failed traversing us. - **kwargs (dict): Arbitrary, optional arguments for users + traversing_object (DefaultObject): The object that failed traversing us. + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Notes: @@ -2301,9 +2292,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: text (str, optional): The message received. from_obj (any, optional): The object sending the message. - - Keyword Args: - This includes any keywords sent to the `msg` method. + **kwargs: This includes any keywords sent to the `msg` method. Returns: receive (bool): If this message should be received. @@ -2323,9 +2312,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: text (str, optional): Text to send. to_obj (any, optional): The object to send to. - - Keyword Args: - Keywords passed from msg() + **kwargs: Keywords passed from msg(). Notes: Since this method is executed by `from_obj`, if no `from_obj` @@ -2345,13 +2332,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Helper method to return_appearance. Args: - looker (Object): The entity looking. - **kwargs (any): Passed from `return_appearance`. Unused by default. + looker (DefaultObject): The entity looking. + **kwargs: Passed from `return_appearance`. Unused by default. Returns: dict: A dict of lists categorized by type. Byt default this - contains 'exits', 'characters' and 'things'. The elements of these - lists are the actual objects. + contains 'exits', 'characters' and 'things'. The elements of these + lists are the actual objects. """ @@ -2371,16 +2358,16 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): for return_appearance. Args: - looker (Object): The entity looking. - **kwargs (any): Passed from `return_appearance`. Passed into + looker (DefaultObject): The entity looking. + **kwargs: Passed from `return_appearance`. Passed into `get_display_name` for each found entity. Returns: dict: A dict of lists categorized by type. Byt default this - contains 'exits', 'characters' and 'things'. The elements - of these lists are strings - names of the objects that - can depend on the looker and also be grouped in the case - of multiple same-named things etc. + contains 'exits', 'characters' and 'things'. The elements + of these lists are strings - names of the objects that + can depend on the looker and also be grouped in the case + of multiple same-named things etc. Notes: This method shouldn't add extra coloring to the names beyond what is @@ -2418,17 +2405,16 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): send any data. Args: - target (Object): The target being looked at. This is + target (DefaultObject): The target being looked at. This is commonly an object or the current location. It will be checked for the "view" type access. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call. This will be passed into return_appearance, get_display_name and at_desc but is not used by default. Returns: - lookstring (str): A ready-processed look string - potentially ready to return to the looker. + str: A ready-processed look string potentially ready to return to the looker. """ if not target.access(self, "view"): @@ -2451,7 +2437,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: looker (Object, optional): The object requesting the description. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). """ @@ -2463,12 +2449,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): picked up. Args: - getter (Object): The object about to get this object. - **kwargs (dict): Arbitrary, optional arguments for users + getter (DefaultObject): The object about to get this object. + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Returns: - shouldget (bool): If the object should be gotten or not. + bool: If the object should be gotten or not. Notes: If this method returns False/None, the getting is cancelled @@ -2485,8 +2471,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): picked up. Args: - getter (Object): The object getting this object. - **kwargs (dict): Arbitrary, optional arguments for users + getter (DefaultObject): The object getting this object. + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Notes: @@ -2502,16 +2488,16 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): given. Args: - giver (Object): The object about to give this object. - getter (Object): The object about to get this object. - **kwargs (dict): Arbitrary, optional arguments for users + giver (DefaultObject): The object about to give this object. + getter (DefaultObject): The object about to get this object. + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Returns: shouldgive (bool): If the object should be given or not. Notes: - If this method returns False/None, the giving is cancelled + If this method returns `False` or `None`, the giving is cancelled before it is even started. """ @@ -2526,9 +2512,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): given. Args: - giver (Object): The object giving this object. - getter (Object): The object getting this object. - **kwargs (dict): Arbitrary, optional arguments for users + giver (DefaultObject): The object giving this object. + getter (DefaultObject): The object getting this object. + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Notes: @@ -2544,15 +2530,15 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): dropped. Args: - dropper (Object): The object which will drop this object. - **kwargs (dict): Arbitrary, optional arguments for users + dropper (DefaultObject): The object which will drop this object. + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Returns: - shoulddrop (bool): If the object should be dropped or not. + bool: If the object should be dropped or not. Notes: - If this method returns False/None, the dropping is cancelled + If this method returns `False` or `None`, the dropping is cancelled before it is even started. """ @@ -2573,8 +2559,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): dropped. Args: - dropper (Object): The object which just dropped this object. - **kwargs (dict): Arbitrary, optional arguments for users + dropper (DefaultObject): The object which just dropped this object. + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). Notes: @@ -2600,11 +2586,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): a say. This is sent by the whisper command by default. Other verbal commands could use this hook in similar ways. - receivers (Object or iterable): If set, this is the target or targets for the + receivers (DefaultObject or iterable): If set, this is the target or targets for the say/whisper. Returns: - message (str): The (possibly modified) text to be spoken. + str: The (possibly modified) text to be spoken. """ return message @@ -2635,7 +2621,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): msg_self (bool or str, optional): If boolean True, echo `message` to self. If a string, return that message. If False or unset, don't echo to self. msg_location (str, optional): The message to echo to self's location. - receivers (Object or iterable, optional): An eventual receiver or receivers of the + receivers (DefaultObject or iterable, optional): An eventual receiver or receivers of the message (by default only used by whispers). msg_receivers(str): Specific message to pass to the receiver(s). This will parsed with the {receiver} placeholder replaced with the given receiver. @@ -2649,20 +2635,22 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Messages can contain {} markers. These are substituted against the values passed in the `mapping` argument. + :: msg_self = 'You say: "{speech}"' msg_location = '{object} says: "{speech}"' msg_receivers = '{object} whispers: "{speech}"' Supported markers by default: - {self}: text to self-reference with (default 'You') - {speech}: the text spoken/whispered by self. - {object}: the object speaking. - {receiver}: replaced with a single receiver only for strings meant for a specific - receiver (otherwise 'None'). - {all_receivers}: comma-separated list of all receivers, - if more than one, otherwise same as receiver - {location}: the location where object is. + + - {self}: text to self-reference with (default 'You') + - {speech}: the text spoken/whispered by self. + - {object}: the object speaking. + - {receiver}: replaced with a single receiver only for strings meant for a specific + receiver (otherwise 'None'). + - {all_receivers}: comma-separated list of all receivers, + if more than one, otherwise same as receiver + - {location}: the location where object is. """ msg_type = "say" @@ -2778,12 +2766,13 @@ class DefaultCharacter(DefaultObject): Classmethod called during .create() to determine default locks for the object. Args: - account (Account): Account to attribute this object to. + account (DefaultAccount): Account to attribute this object to. caller (DefaultObject): The object which is creating this one. **kwargs: Arbitrary input. Returns: - lockstring (str): A lockstring to use for this object. + str: A lockstring to use for this object. + """ pid = f"pid({account.id})" if account else None character = kwargs.get("character", None) @@ -2895,16 +2884,24 @@ class DefaultCharacter(DefaultObject): @classmethod def normalize_name(cls, name): """ - Normalize the character name prior to creating. Note that this should be refactored to - support i18n for non-latin scripts, but as we (currently) have no bug reports requesting - better support of non-latin character sets, requiring character names to be latinified is an - acceptable option. + Normalize the character name prior to creating. Args: name (str) : The name of the character Returns: - latin_name (str) : A valid name. + str : A valid, latinized name. + + Notes: + + The main purpose of this is to make sure that character names are not created with + special unicode characters that look visually identical to latin charaters. This could + be used to impersonate other characters. + + This method should be refactored to support i18n for non-latin names, but as we + (currently) have no bug reports requesting better support of non-latin character sets, + requiring character names to be latinified is an acceptable default option. + """ from evennia.utils.utils import latinify @@ -2919,10 +2916,10 @@ class DefaultCharacter(DefaultObject): Args: name (str) : The name of the character - Kwargs: + Keyword Args: account (DefaultAccount, optional) : The account creating the character. Returns: - error (str, optional) : A non-empty error message if there is a problem, otherwise False. + str or None: A non-empty error message if there is a problem, otherwise `None`. """ if account and cls.objects.filter_family(db_key__iexact=name): @@ -2967,7 +2964,7 @@ class DefaultCharacter(DefaultObject): """ Return the character from storage in None location in `at_post_unpuppet`. Args: - account (Account): This is the connecting account. + account (DefaultAccount): This is the connecting account. session (Session): Session controlling the connection. """ @@ -2994,7 +2991,8 @@ class DefaultCharacter(DefaultObject): Args: **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). - Note: + Notes: + You can use `self.account` and `self.sessions.get()` to get account and sessions at this point; the last entry in the list from `self.sessions.get()` is the latest Session @@ -3019,15 +3017,16 @@ class DefaultCharacter(DefaultObject): after the account logged off ("headless", so to say). Args: - account (Account): The account object that just disconnected + account (DefaultAccount): The account object that just disconnected from this object. session (Session): Session controlling the connection that just disconnected. Keyword Args: reason (str): If given, adds a reason for the unpuppet. This is set when the user is auto-unpuppeted due to being link-dead. - **kwargs (dict): Arbitrary, optional arguments for users + **kwargs: Arbitrary, optional arguments for users overriding the call (unused by default). + """ if not self.sessions.count(): # only remove this char from grid if no sessions control it anymore. @@ -3113,8 +3112,9 @@ class DefaultRoom(DefaultObject): method (str): The method used to create the room. Defaults to "create". Returns: - room (Object): A newly created Room of the given typeclass. - errors (list): A list of errors in string form, if any. + tuple: A tuple `(Object, error)` with the newly created Room of the given typeclass, + or `None` if there was an error. If there was an error, `error` will be a list of + error strings. """ errors = [] @@ -3166,8 +3166,7 @@ class DefaultRoom(DefaultObject): def basetype_setup(self): """ - Simple room setup setting locks to make sure the room - cannot be picked up. + Simple room setup setting locks to make sure the room cannot be picked up. """ @@ -3215,13 +3214,13 @@ class ExitCommand(_COMMAND_DEFAULT_CLASS): Shows a bit of information on where the exit leads. Args: - caller (Object): The object (usually a character) that entered an ambiguous command. + caller (DefaultObject): The object (usually a character) that entered an ambiguous command. **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). Returns: - A string with identifying information to disambiguate the command, conventionally with a - preceding space. + str: A string with identifying information to disambiguate the command, conventionally + with a preceding space. """ if self.obj.destination: @@ -3263,7 +3262,7 @@ class DefaultExit(DefaultObject): exit's name, triggering the movement between rooms. Args: - exidbobj (Object): The DefaultExit object to base the command on. + exidbobj (DefaultObject): The DefaultExit object to base the command on. """ @@ -3313,15 +3312,16 @@ class DefaultExit(DefaultObject): location (Room): The room to create this exit in. Keyword Args: - account (AccountDB): Account to associate this Exit with. - caller (ObjectDB): The Object creating this Object. + account (DefaultAccountDB): Account to associate this Exit with. + caller (DefaultObject): The Object creating this Object. description (str): Brief description for this object. ip (str): IP address of creator (for object auditing). destination (Room): The room to which this exit should go. Returns: - exit (Object): A newly created Room of the given typeclass. - errors (list): A list of errors in string form, if any. + tuple: A tuple `(Object, errors)`, where the object is the newly + created exit of the given typeclass, or `None` if there was an error. + If there was an error, `errors` will be a list of error strings. """ errors = [] @@ -3375,7 +3375,7 @@ class DefaultExit(DefaultObject): def basetype_setup(self): """ - Setup exit-security + Setup exit-security. You should normally not need to overload this - if you do make sure you include all the functionality in this method. @@ -3409,7 +3409,7 @@ class DefaultExit(DefaultObject): has no cmdsets. Keyword Args: - caller (Object, Account or Session): The object requesting the cmdsets. + caller (DefaultObject, DefaultAccount or Session): The object requesting the cmdsets. current (CmdSet): The current merged cmdset. force_init (bool): If `True`, force a re-build of the cmdset (for example to update aliases). @@ -3425,6 +3425,7 @@ class DefaultExit(DefaultObject): This is called when this objects is re-loaded from cache. When that happens, we make sure to remove any old ExitCmdSet cmdset (this most commonly occurs when renaming an existing exit) + """ self.cmdset.remove_default() @@ -3434,8 +3435,8 @@ class DefaultExit(DefaultObject): already been checked (in the Exit command) at this point. Args: - traversing_object (Object): Object traversing us. - target_location (Object): Where target is going. + traversing_object (DefaultObject): Object traversing us. + target_location (DefaultObject): Where target is going. **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). @@ -3456,7 +3457,7 @@ class DefaultExit(DefaultObject): Overloads the default hook to implement a simple default error message. Args: - traversing_object (Object): The object that failed traversing us. + traversing_object (DefaultObject): The object that failed traversing us. **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). @@ -3475,10 +3476,12 @@ class DefaultExit(DefaultObject): Args: return_all (bool): Whether to return available results as a - list or single matching exit. + queryset or single matching exit. Returns: - queryset or exit (Exit): The matching exit(s). + Exit or queryset: The matching exit(s). If `return_all` is `True`, this + will be a queryset of all matching exits. Otherwise, it will be the first Exit matched. + """ query = ObjectDB.objects.filter(db_location=self.destination, db_destination=self.location) if return_all: From c5a4a34bac955ded5355cd2b1cdfc86f55df739f Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 17:51:05 +0200 Subject: [PATCH 39/68] Apply black to codes --- docs/source/conf.py | 8 +- evennia/__init__.py | 1 + evennia/accounts/accounts.py | 23 +++-- evennia/accounts/manager.py | 1 + evennia/accounts/models.py | 1 + evennia/commands/cmdhandler.py | 7 +- evennia/commands/cmdparser.py | 1 - evennia/commands/cmdset.py | 1 + evennia/commands/cmdsethandler.py | 1 + evennia/commands/command.py | 34 +++---- evennia/commands/default/account.py | 1 + evennia/commands/default/batchprocess.py | 1 + evennia/commands/default/building.py | 41 +++++---- evennia/commands/default/cmdset_character.py | 1 + evennia/commands/default/cmdset_session.py | 1 + evennia/commands/default/cmdset_unloggedin.py | 1 + evennia/commands/default/comms.py | 19 ++-- evennia/commands/default/general.py | 4 +- evennia/commands/default/system.py | 3 - evennia/commands/default/tests.py | 48 ++++++---- evennia/commands/default/unloggedin.py | 1 + evennia/comms/comms.py | 1 + evennia/comms/managers.py | 1 - evennia/comms/models.py | 1 + .../base_systems/awsstorage/aws_s3_cdn.py | 1 - .../base_systems/building_menu/__init__.py | 1 + .../building_menu/building_menu.py | 7 -- .../base_systems/components/__init__.py | 1 + .../base_systems/components/dbfield.py | 1 + .../custom_gametime/custom_gametime.py | 1 - .../base_systems/godotwebsocket/__init__.py | 1 + .../godotwebsocket/text2bbcode.py | 1 + .../base_systems/godotwebsocket/webclient.py | 1 + .../ingame_python/callbackhandler.py | 1 - .../base_systems/ingame_python/commands.py | 1 - .../base_systems/ingame_python/scripts.py | 2 - .../base_systems/ingame_python/tests.py | 3 - .../base_systems/ingame_python/typeclasses.py | 4 - .../base_systems/ingame_python/utils.py | 1 - .../base_systems/menu_login/menu_login.py | 8 +- .../mux_comms_cmds/mux_comms_cmds.py | 1 + .../contrib/base_systems/unixcommand/tests.py | 1 - .../base_systems/unixcommand/unixcommand.py | 3 - .../contrib/full_systems/evscaperoom/menu.py | 1 + .../full_systems/evscaperoom/objects.py | 1 + .../contrib/full_systems/evscaperoom/tests.py | 1 + .../contrib/game_systems/clothing/clothing.py | 2 + .../game_systems/containers/containers.py | 1 + .../contrib/game_systems/crafting/crafting.py | 8 +- .../contrib/game_systems/gendersub/tests.py | 1 - .../game_systems/multidescer/multidescer.py | 1 + .../grid/extended_room/extended_room.py | 14 ++- evennia/contrib/grid/extended_room/tests.py | 5 +- .../ingame_map_display/ingame_map_display.py | 1 + .../contrib/grid/ingame_map_display/tests.py | 1 - evennia/contrib/grid/simpledoor/tests.py | 1 - evennia/contrib/grid/xyzgrid/__init__.py | 1 + evennia/contrib/grid/xyzgrid/prototypes.py | 1 + evennia/contrib/grid/xyzgrid/tests.py | 5 +- evennia/contrib/grid/xyzgrid/xymap.py | 14 +-- evennia/contrib/grid/xyzgrid/xymap_legend.py | 5 +- evennia/contrib/grid/xyzgrid/xyzgrid.py | 1 + evennia/contrib/rpg/buffs/__init__.py | 7 +- evennia/contrib/rpg/buffs/buff.py | 1 + evennia/contrib/rpg/buffs/tests.py | 1 + .../character_creator/character_creator.py | 1 + evennia/contrib/rpg/dice/dice.py | 1 + evennia/contrib/rpg/rpsystem/rplanguage.py | 1 + evennia/contrib/rpg/rpsystem/rpsystem.py | 10 ++- evennia/contrib/rpg/rpsystem/tests.py | 6 +- evennia/contrib/rpg/traits/traits.py | 1 - .../tutorials/bodyfunctions/bodyfunctions.py | 1 + .../contrib/tutorials/bodyfunctions/tests.py | 1 + evennia/contrib/tutorials/evadventure/ai.py | 7 +- .../contrib/tutorials/evadventure/chargen.py | 1 + .../tutorials/evadventure/combat_turnbased.py | 13 ++- .../tutorials/evadventure/combat_twitch.py | 22 +++-- .../contrib/tutorials/evadventure/dungeon.py | 6 +- .../contrib/tutorials/evadventure/enums.py | 1 + evennia/contrib/tutorials/evadventure/npcs.py | 2 + .../contrib/tutorials/evadventure/quests.py | 9 +- .../contrib/tutorials/evadventure/rules.py | 1 + .../tutorials/evadventure/tests/test_ai.py | 1 + .../evadventure/tests/test_equipment.py | 1 - .../contrib/tutorials/evadventure/utils.py | 1 - .../tutorials/red_button/red_button.py | 1 + .../contrib/tutorials/talking_npc/tests.py | 1 + .../contrib/tutorials/tutorial_world/rooms.py | 1 - evennia/contrib/utils/auditing/outputs.py | 1 + evennia/contrib/utils/auditing/server.py | 1 + evennia/contrib/utils/fieldfill/fieldfill.py | 1 + .../random_string_generator.py | 4 - .../game_template/typeclasses/characters.py | 1 + evennia/game_template/typeclasses/exits.py | 1 + evennia/game_template/typeclasses/objects.py | 1 + evennia/game_template/web/admin/urls.py | 1 - evennia/game_template/web/urls.py | 1 + evennia/help/manager.py | 1 + evennia/help/models.py | 1 + evennia/help/utils.py | 1 + evennia/locks/lockfuncs.py | 1 - evennia/objects/manager.py | 2 + evennia/objects/models.py | 2 + evennia/objects/objects.py | 58 +++++++----- evennia/objects/tests.py | 1 + evennia/prototypes/spawner.py | 1 - evennia/scripts/models.py | 1 + evennia/scripts/monitorhandler.py | 1 + evennia/scripts/scripthandler.py | 1 + evennia/scripts/taskhandler.py | 1 - evennia/scripts/tests.py | 3 +- evennia/scripts/tickerhandler.py | 1 + evennia/server/connection_wizard.py | 1 + evennia/server/deprecations.py | 1 + evennia/server/game_index_client/client.py | 1 + evennia/server/game_index_client/service.py | 1 + evennia/server/initial_setup.py | 1 - evennia/server/manager.py | 1 + evennia/server/models.py | 1 + evennia/server/portal/amp_server.py | 2 +- evennia/server/portal/discord.py | 1 + evennia/server/portal/mccp.py | 1 + evennia/server/portal/mssp.py | 1 + evennia/server/portal/mxp.py | 1 + evennia/server/portal/naws.py | 1 + evennia/server/portal/portal.py | 1 + evennia/server/portal/portalsessionhandler.py | 1 - evennia/server/portal/rss.py | 1 + evennia/server/portal/ssh.py | 7 +- evennia/server/portal/ssl.py | 1 + evennia/server/portal/suppress_ga.py | 6 +- evennia/server/portal/telnet_oob.py | 1 + evennia/server/portal/telnet_ssl.py | 1 + evennia/server/portal/webclient.py | 1 + evennia/server/portal/webclient_ajax.py | 1 + evennia/server/profiling/dummyrunner.py | 1 - .../server/profiling/dummyrunner_settings.py | 1 + evennia/server/server.py | 1 + evennia/server/serversession.py | 1 + evennia/server/service.py | 14 +-- evennia/server/session.py | 1 + evennia/server/sessionhandler.py | 1 + evennia/server/signals.py | 1 + evennia/server/tests/test_server.py | 89 ++++++++++--------- evennia/server/tests/testrunner.py | 1 + evennia/server/webserver.py | 1 + evennia/settings_default.py | 1 + evennia/typeclasses/attributes.py | 1 + evennia/typeclasses/managers.py | 1 + evennia/typeclasses/models.py | 1 + evennia/typeclasses/tags.py | 1 + evennia/utils/ansi.py | 1 + evennia/utils/batchprocessors.py | 1 + evennia/utils/containers.py | 1 - evennia/utils/dbserialize.py | 1 + evennia/utils/eveditor.py | 10 ++- evennia/utils/funcparser.py | 1 + evennia/utils/idmapper/manager.py | 1 + evennia/utils/logger.py | 1 - evennia/utils/test_resources.py | 15 +++- evennia/utils/tests/test_eveditor.py | 20 +++-- evennia/utils/tests/test_evform.py | 1 + evennia/utils/tests/test_tagparsing.py | 1 + evennia/utils/tests/test_utils.py | 25 ++++-- evennia/utils/utils.py | 24 ++--- evennia/utils/verb_conjugation/pronouns.py | 1 + evennia/web/admin/help.py | 1 + evennia/web/admin/tags.py | 1 - evennia/web/admin/urls.py | 1 + evennia/web/api/filters.py | 2 +- evennia/web/api/permissions.py | 1 - evennia/web/api/tests.py | 1 + evennia/web/api/views.py | 1 + evennia/web/utils/general_context.py | 2 +- evennia/web/webclient/urls.py | 1 + evennia/web/website/urls.py | 1 + evennia/web/website/views/accounts.py | 1 - evennia/web/website/views/characters.py | 18 ++-- evennia/web/website/views/help.py | 1 + evennia/web/website/views/mixins.py | 1 + 180 files changed, 495 insertions(+), 288 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 193eafcb25..e63efefcd1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -374,8 +374,12 @@ def setup(app): # build toctree file sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) - from docs.pylib import (auto_link_remapper, contrib_readmes2docs, - update_default_cmd_index, update_dynamic_pages) + from docs.pylib import ( + auto_link_remapper, + contrib_readmes2docs, + update_default_cmd_index, + update_dynamic_pages, + ) _no_autodoc = os.environ.get("NOAUTODOC") update_default_cmd_index.run_update(no_autodoc=_no_autodoc) diff --git a/evennia/__init__.py b/evennia/__init__.py index bf5eafa8c6..5e8a53f776 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -16,6 +16,7 @@ to launch such a shell (using python or ipython depending on your install). See www.evennia.com for full documentation. """ + import evennia # docstring header diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index e683aa8dd1..b01592ddfa 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -10,18 +10,20 @@ character object, so you should customize that instead for most things). """ + import re import time import typing from random import getrandbits -import evennia from django.conf import settings from django.contrib.auth import authenticate, password_validation from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ + +import evennia from evennia.accounts.manager import AccountManager from evennia.accounts.models import AccountDB from evennia.commands.cmdsethandler import CmdSetHandler @@ -29,17 +31,24 @@ from evennia.comms.models import ChannelDB from evennia.objects.models import ObjectDB from evennia.scripts.scripthandler import ScriptHandler from evennia.server.models import ServerConfig -from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE, - SIGNAL_ACCOUNT_POST_LOGIN_FAIL, - SIGNAL_OBJECT_POST_PUPPET, - SIGNAL_OBJECT_POST_UNPUPPET) +from evennia.server.signals import ( + SIGNAL_ACCOUNT_POST_CREATE, + SIGNAL_ACCOUNT_POST_LOGIN_FAIL, + SIGNAL_OBJECT_POST_PUPPET, + SIGNAL_OBJECT_POST_UNPUPPET, +) from evennia.server.throttle import Throttle from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import class_from_module, create, logger from evennia.utils.optionhandler import OptionHandler -from evennia.utils.utils import (is_iter, lazy_property, make_iter, to_str, - variable_from_module) +from evennia.utils.utils import ( + is_iter, + lazy_property, + make_iter, + to_str, + variable_from_module, +) __all__ = ("DefaultAccount", "DefaultGuest") diff --git a/evennia/accounts/manager.py b/evennia/accounts/manager.py index 83478654b8..f5d91e5fcf 100644 --- a/evennia/accounts/manager.py +++ b/evennia/accounts/manager.py @@ -7,6 +7,7 @@ import datetime from django.conf import settings from django.contrib.auth.models import UserManager from django.utils import timezone + from evennia.server import signals from evennia.typeclasses.managers import TypeclassManager, TypedObjectManager from evennia.utils.utils import class_from_module, dbid_to_obj, make_iter diff --git a/evennia/accounts/models.py b/evennia/accounts/models.py index ea00e24d9d..e8ecabea45 100644 --- a/evennia/accounts/models.py +++ b/evennia/accounts/models.py @@ -15,6 +15,7 @@ persistently store attributes of its own. This is ideal for extra account info and OOC account configuration variables etc. """ + from django.conf import settings from django.contrib.auth.models import AbstractUser from django.db import models diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 0da3039aaa..45605f758b 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -36,13 +36,14 @@ from weakref import WeakValueDictionary from django.conf import settings from django.utils.translation import gettext as _ +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.task import deferLater + from evennia.commands.cmdset import CmdSet from evennia.commands.command import InterruptCommand from evennia.utils import logger, utils from evennia.utils.utils import string_suggestions -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks, returnValue -from twisted.internet.task import deferLater _IN_GAME_ERRORS = settings.IN_GAME_ERRORS diff --git a/evennia/commands/cmdparser.py b/evennia/commands/cmdparser.py index 6ae209c258..98b94638ea 100644 --- a/evennia/commands/cmdparser.py +++ b/evennia/commands/cmdparser.py @@ -6,7 +6,6 @@ same inputs as the default one. """ - import re from django.conf import settings diff --git a/evennia/commands/cmdset.py b/evennia/commands/cmdset.py index 618d78cfad..cac54816cb 100644 --- a/evennia/commands/cmdset.py +++ b/evennia/commands/cmdset.py @@ -26,6 +26,7 @@ Set theory. to affect the low-priority cmdset. Ex: A1,A3 + B1,B2,B4,B5 = B2,B4,B5 """ + from weakref import WeakKeyDictionary from django.utils.translation import gettext as _ diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 9f7b89e674..19b2f27391 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -64,6 +64,7 @@ example, you can have a 'On a boat' set, onto which you then tack on the 'Fishing' set. Fishing from a boat? No problem! """ + import sys from importlib import import_module from inspect import trace diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 3d0d93dbff..8dbac1c3a7 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -4,6 +4,7 @@ The base Command class. All commands in Evennia inherit from the 'Command' class in this module. """ + import inspect import math import re @@ -11,6 +12,7 @@ import re from django.conf import settings from django.urls import reverse from django.utils.text import slugify + from evennia.locks.lockhandler import LockHandler from evennia.utils.ansi import ANSIString from evennia.utils.evtable import EvTable @@ -19,9 +21,7 @@ from evennia.utils.utils import fill, is_iter, lazy_property, make_iter CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES - class InterruptCommand(Exception): - """Cleanly interrupt a command.""" pass @@ -491,22 +491,22 @@ class Command(metaclass=CommandMeta): Command \"{cmdname}\" has no defined `func()` method. Available properties on this command are: {variables}""" - variables = [" |w{}|n ({}): {}".format( - key, type(val), f'"{val}"' if isinstance(val, str) else val + variables = [ + " |w{}|n ({}): {}".format(key, type(val), f'"{val}"' if isinstance(val, str) else val) + for key, val in ( + ("self.key", self.key), + ("self.cmdname", self.cmdstring), + ("self.raw_cmdname", self.raw_cmdname), + ("self.raw_string", self.raw_string), + ("self.aliases", self.aliases), + ("self.args", self.args), + ("self.caller", self.caller), + ("self.obj", self.obj), + ("self.session", self.session), + ("self.locks", self.locks), + ("self.help_category", self.help_category), + ("self.cmdset", self.cmdset), ) - for key, val in - (("self.key", self.key), - ("self.cmdname", self.cmdstring), - ("self.raw_cmdname", self.raw_cmdname), - ("self.raw_string", self.raw_string), - ("self.aliases", self.aliases), - ("self.args", self.args), - ("self.caller", self.caller), - ("self.obj", self.obj), - ("self.session", self.session), - ("self.locks", self.locks), - ("self.help_category", self.help_category), - ("self.cmdset", self.cmdset)) ] output = output_string.format(cmdname=self.key, variables="\n ".join(variables)) self.msg(output) diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index 7b966eb7cf..f51b079e17 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -18,6 +18,7 @@ self.msg() and similar methods to reroute returns to the correct method. Otherwise all text will be returned to all connected sessions. """ + import time from codecs import lookup as codecs_lookup diff --git a/evennia/commands/default/batchprocess.py b/evennia/commands/default/batchprocess.py index 2d1ef758e4..694fd20972 100644 --- a/evennia/commands/default/batchprocess.py +++ b/evennia/commands/default/batchprocess.py @@ -17,6 +17,7 @@ the Evennia API. It is also a severe security risk and should therefore always be limited to superusers only. """ + import re from django.conf import settings diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index e99455d0f9..ff4111643e 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1,16 +1,17 @@ """ Building and world design commands """ + import re import typing -import evennia from django.conf import settings from django.core.paginator import Paginator from django.db.models import Max, Min, Q + +import evennia from evennia import InterruptCommand -from evennia.commands.cmdhandler import (generate_cmdset_providers, - get_and_merge_cmdsets) +from evennia.commands.cmdhandler import generate_cmdset_providers, get_and_merge_cmdsets from evennia.locks.lockhandler import LockException from evennia.objects.models import ObjectDB from evennia.prototypes import menus as olc_menus @@ -23,10 +24,18 @@ from evennia.utils.dbserialize import deserialize from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.evtable import EvTable -from evennia.utils.utils import (class_from_module, crop, dbref, display_len, - format_grid, get_all_typeclasses, - inherits_from, interactive, list_to_string, - variable_from_module) +from evennia.utils.utils import ( + class_from_module, + crop, + dbref, + display_len, + format_grid, + get_all_typeclasses, + inherits_from, + interactive, + list_to_string, + variable_from_module, +) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -259,7 +268,7 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): obj = caller.search(objname) if not obj: return - if self.rhs is None and 'delete' not in self.switches: + if self.rhs is None and "delete" not in self.switches: # no =, and not deleting, so we just list aliases on object. aliases = obj.aliases.all(return_key_and_category=True) if aliases: @@ -300,7 +309,7 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): if "delete" in self.switches: # delete all matching keys, regardless of category existed = False - for key, category in obj.aliases.all(return_key_and_category=True): + for key, category in obj.aliases.all(return_key_and_category=True): if key == self.rhs: obj.aliases.remove(key=self.rhs, category=category) existed = True @@ -2956,9 +2965,9 @@ class CmdExamine(ObjManipCommand): ): objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj) objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset) - objdata[ - f"Commands available to {obj.key} (result of Merged Cmdset(s))" - ] = self.format_current_cmds(obj, current_cmdset) + objdata[f"Commands available to {obj.key} (result of Merged Cmdset(s))"] = ( + self.format_current_cmds(obj, current_cmdset) + ) if self.object_type == "script": objdata["Description"] = self.format_script_desc(obj) objdata["Persistent"] = self.format_script_is_persistent(obj) @@ -3415,9 +3424,11 @@ class ScriptEvMore(EvMore): table.add_row( f"#{script.id}", - f"{script.obj.key}({script.obj.dbref})" - if (hasattr(script, "obj") and script.obj) - else "", + ( + f"{script.obj.key}({script.obj.dbref})" + if (hasattr(script, "obj") and script.obj) + else "" + ), script.key, script.interval if script.interval > 0 else "--", nextrep, diff --git a/evennia/commands/default/cmdset_character.py b/evennia/commands/default/cmdset_character.py index f8294dd6b7..df06f3f893 100644 --- a/evennia/commands/default/cmdset_character.py +++ b/evennia/commands/default/cmdset_character.py @@ -4,6 +4,7 @@ available (i.e. IC commands). Note that some commands, such as communication-commands are instead put on the account level, in the Account cmdset. Account commands remain available also to Characters. """ + from evennia.commands.cmdset import CmdSet from evennia.commands.default import ( admin, diff --git a/evennia/commands/default/cmdset_session.py b/evennia/commands/default/cmdset_session.py index f81e8b9636..eca352c44e 100644 --- a/evennia/commands/default/cmdset_session.py +++ b/evennia/commands/default/cmdset_session.py @@ -1,6 +1,7 @@ """ This module stores session-level commands. """ + from evennia.commands.cmdset import CmdSet from evennia.commands.default import account diff --git a/evennia/commands/default/cmdset_unloggedin.py b/evennia/commands/default/cmdset_unloggedin.py index b668325bd8..1ce4e7749d 100644 --- a/evennia/commands/default/cmdset_unloggedin.py +++ b/evennia/commands/default/cmdset_unloggedin.py @@ -3,6 +3,7 @@ This module describes the unlogged state of the default game. The setting STATE_UNLOGGED should be set to the python path of the state instance in this module. """ + from evennia.commands.cmdset import CmdSet from evennia.commands.default import unloggedin diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 2cec5ddf08..cac9905a5a 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -9,6 +9,7 @@ Communication commands: from django.conf import settings from django.db.models import Q + from evennia.accounts import bots from evennia.accounts.models import AccountDB from evennia.comms.comms import DefaultChannel @@ -1412,13 +1413,17 @@ class CmdPage(COMMAND_DEFAULT_CLASS): message = f"{caller.key} {message.strip(':').strip()}" # create the persistent message object - create.create_message(caller, message, receivers=targets, - locks=(f"read:id({caller.id}) or perm(Admin);" - f"delete:id({caller.id}) or perm(Admin);" - f"edit:id({caller.id}) or perm(Admin)" - ), - tags = [("page", "comms")], - ) + create.create_message( + caller, + message, + receivers=targets, + locks=( + f"read:id({caller.id}) or perm(Admin);" + f"delete:id({caller.id}) or perm(Admin);" + f"edit:id({caller.id}) or perm(Admin)" + ), + tags=[("page", "comms")], + ) # tell the accounts they got a message. received = [] diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 4a65b63b2d..e46fbe2cf5 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -1,10 +1,12 @@ """ General Character commands usually available to all characters """ + import re -import evennia from django.conf import settings + +import evennia from evennia.typeclasses.attributes import NickTemplateInvalid from evennia.utils import utils diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index 546945367e..d6e2466dc5 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -4,7 +4,6 @@ System commands """ - import code import datetime import os @@ -112,7 +111,6 @@ class CmdReset(COMMAND_DEFAULT_CLASS): class CmdShutdown(COMMAND_DEFAULT_CLASS): - """ stop the server completely @@ -277,7 +275,6 @@ def evennia_local_vars(caller): class EvenniaPythonConsole(code.InteractiveConsole): - """Evennia wrapper around a Python interactive console.""" def __init__(self, caller): diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index e60ee3ddc2..b3fe5b5fd7 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -14,17 +14,32 @@ main test suite started with import datetime from unittest.mock import MagicMock, Mock, patch -import evennia from anything import Anything from django.conf import settings from django.test import override_settings -from evennia import (DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom, - ObjectDB, search_object) +from parameterized import parameterized +from twisted.internet import task + +import evennia +from evennia import ( + DefaultCharacter, + DefaultExit, + DefaultObject, + DefaultRoom, + ObjectDB, + search_object, +) from evennia.commands import cmdparser from evennia.commands.cmdset import CmdSet from evennia.commands.command import Command, InterruptCommand -from evennia.commands.default import (account, admin, batchprocess, building, - comms, general) +from evennia.commands.default import ( + account, + admin, + batchprocess, + building, + comms, + general, +) from evennia.commands.default import help as help_module from evennia.commands.default import syscommands, system, unloggedin from evennia.commands.default.cmdset_character import CharacterCmdSet @@ -33,8 +48,6 @@ from evennia.prototypes import prototypes as protlib from evennia.utils import create, gametime, utils from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa from evennia.utils.test_resources import BaseEvenniaTest, EvenniaCommandTest -from parameterized import parameterized -from twisted.internet import task # ------------------------------------------------------------ # Command testing @@ -368,8 +381,7 @@ class TestCmdTasks(BaseEvenniaCommandTest): self.timedelay = 5 global _TASK_HANDLER if _TASK_HANDLER is None: - from evennia.scripts.taskhandler import \ - TASK_HANDLER as _TASK_HANDLER + from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER _TASK_HANDLER.clock = task.Clock() self.task_handler = _TASK_HANDLER self.task_handler.clear() @@ -787,19 +799,20 @@ class TestBuilding(BaseEvenniaCommandTest): self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.") self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj: testobj1b") - self.call(building.CmdSetObjAlias(), + self.call( + building.CmdSetObjAlias(), "/category Obj = testobj1b:category1", - "Alias(es) for 'Obj' set to 'testobj1b' (category: 'category1')." + "Alias(es) for 'Obj' set to 'testobj1b' (category: 'category1').", ) self.call( building.CmdSetObjAlias(), "/category Obj = testobj1b:category2", - "Alias(es) for 'Obj' set to 'testobj1b,testobj1b' (category: 'category2')." + "Alias(es) for 'Obj' set to 'testobj1b,testobj1b' (category: 'category2').", ) self.call( - building.CmdSetObjAlias(), # delete both occurences of alias 'testobj1b' + building.CmdSetObjAlias(), # delete both occurences of alias 'testobj1b' "/delete Obj = testobj1b", - "Alias 'testobj1b' deleted from Obj." + "Alias 'testobj1b' deleted from Obj.", ) self.call(building.CmdSetObjAlias(), "Obj =", "No aliases to clear.") @@ -1773,8 +1786,7 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdSpawn(), "{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', " - "'key':'goblin', 'location':'%s'}" - % spawnLoc.dbref, + "'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "Spawned goblin", ) goblin = get_object(self, "goblin") @@ -1822,8 +1834,7 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdSpawn(), "/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo'," - " 'location':'%s'}" - % spawnLoc.dbref, + " 'location':'%s'}" % spawnLoc.dbref, "Spawned Ball", ) ball = get_object(self, "Ball") @@ -2068,6 +2079,7 @@ class TestComms(BaseEvenniaCommandTest): receiver=self.account, ) from evennia.comms.models import Msg + msgs = Msg.objects.filter(db_tags__db_key="page", db_tags__db_category="comms") self.assertEqual(msgs[0].senders, [self.account]) self.assertEqual(msgs[0].receivers, [self.account2]) diff --git a/evennia/commands/default/unloggedin.py b/evennia/commands/default/unloggedin.py index 75fff10ccd..a69e20068e 100644 --- a/evennia/commands/default/unloggedin.py +++ b/evennia/commands/default/unloggedin.py @@ -2,6 +2,7 @@ Commands that are available from the connect screen. """ + import datetime import re from codecs import lookup as codecs_lookup diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index ea2a512b5c..a32b425df8 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -2,6 +2,7 @@ Base typeclass for in-game Channels. """ + import re from django.contrib.contenttypes.models import ContentType diff --git a/evennia/comms/managers.py b/evennia/comms/managers.py index b55b814009..7cd720a8dd 100644 --- a/evennia/comms/managers.py +++ b/evennia/comms/managers.py @@ -4,7 +4,6 @@ Comm system components. """ - from django.conf import settings from django.db.models import Q diff --git a/evennia/comms/models.py b/evennia/comms/models.py index 0869eaac09..a37cb733bc 100644 --- a/evennia/comms/models.py +++ b/evennia/comms/models.py @@ -18,6 +18,7 @@ connect to channels by use of a ChannelConnect object (this object is necessary to easily be able to delete connections on the fly). """ + from django.conf import settings from django.db import models from django.utils import timezone diff --git a/evennia/contrib/base_systems/awsstorage/aws_s3_cdn.py b/evennia/contrib/base_systems/awsstorage/aws_s3_cdn.py index f8633933ca..1123c66f1a 100644 --- a/evennia/contrib/base_systems/awsstorage/aws_s3_cdn.py +++ b/evennia/contrib/base_systems/awsstorage/aws_s3_cdn.py @@ -221,7 +221,6 @@ def get_available_overwrite_name(name, max_length): @deconstructible class S3Boto3StorageFile(File): - """ The default file object used by the S3Boto3Storage backend. This file implements file streaming using boto's multipart diff --git a/evennia/contrib/base_systems/building_menu/__init__.py b/evennia/contrib/base_systems/building_menu/__init__.py index 5ab8549d2d..0b7e8cbf05 100644 --- a/evennia/contrib/base_systems/building_menu/__init__.py +++ b/evennia/contrib/base_systems/building_menu/__init__.py @@ -2,5 +2,6 @@ Build-menu contrib - vincent-lg 2018 """ + from .building_menu import BuildingMenu # noqa from .building_menu import GenericBuildingCmd # noqa diff --git a/evennia/contrib/base_systems/building_menu/building_menu.py b/evennia/contrib/base_systems/building_menu/building_menu.py index e8bfd291e6..63d52c3211 100644 --- a/evennia/contrib/base_systems/building_menu/building_menu.py +++ b/evennia/contrib/base_systems/building_menu/building_menu.py @@ -331,7 +331,6 @@ def menu_edit(caller, choice, obj): class CmdNoInput(Command): - """No input has been found.""" key = _CMD_NOINPUT @@ -352,7 +351,6 @@ class CmdNoInput(Command): class CmdNoMatch(Command): - """No input has been found.""" key = _CMD_NOMATCH @@ -394,7 +392,6 @@ class CmdNoMatch(Command): class BuildingMenuCmdSet(CmdSet): - """Building menu CmdSet.""" key = "building_menu" @@ -421,7 +418,6 @@ class BuildingMenuCmdSet(CmdSet): class Choice: - """A choice object, created by `add_choice`.""" def __init__( @@ -557,7 +553,6 @@ class Choice: class BuildingMenu: - """ Class allowing to create and set building menus to edit specific objects. @@ -1200,7 +1195,6 @@ class BuildingMenu: # Generic building menu and command class GenericBuildingMenu(BuildingMenu): - """A generic building menu, allowing to edit any object. This is more a demonstration menu. By default, it allows to edit the @@ -1241,7 +1235,6 @@ class GenericBuildingMenu(BuildingMenu): class GenericBuildingCmd(Command): - """ Generic building command. diff --git a/evennia/contrib/base_systems/components/__init__.py b/evennia/contrib/base_systems/components/__init__.py index 21867e7528..931b156f88 100644 --- a/evennia/contrib/base_systems/components/__init__.py +++ b/evennia/contrib/base_systems/components/__init__.py @@ -7,6 +7,7 @@ This helps writing isolated code and reusing it over multiple objects. See the docs for more information. """ + from . import exceptions # noqa from .component import Component # noqa from .dbfield import DBField, NDBField, TagField # noqa diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index c5e0eb2eae..4b9c6d4fa8 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -3,6 +3,7 @@ Components - ChrisLR 2022 This file contains the Descriptors used to set Fields in Components """ + import typing from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty diff --git a/evennia/contrib/base_systems/custom_gametime/custom_gametime.py b/evennia/contrib/base_systems/custom_gametime/custom_gametime.py index ca6b26363e..f3f9ad3596 100644 --- a/evennia/contrib/base_systems/custom_gametime/custom_gametime.py +++ b/evennia/contrib/base_systems/custom_gametime/custom_gametime.py @@ -309,7 +309,6 @@ def schedule(callback, repeat=False, **kwargs): class GametimeScript(DefaultScript): - """Gametime-sensitive script.""" def at_script_creation(self): diff --git a/evennia/contrib/base_systems/godotwebsocket/__init__.py b/evennia/contrib/base_systems/godotwebsocket/__init__.py index 8311d5d7da..cb618d85d0 100644 --- a/evennia/contrib/base_systems/godotwebsocket/__init__.py +++ b/evennia/contrib/base_systems/godotwebsocket/__init__.py @@ -10,6 +10,7 @@ You could also pass extra data to this client for advanced functionality. See the docs for more information. """ + from evennia.contrib.base_systems.godotwebsocket.text2bbcode import ( BBCODE_PARSER, parse_to_bbcode, diff --git a/evennia/contrib/base_systems/godotwebsocket/text2bbcode.py b/evennia/contrib/base_systems/godotwebsocket/text2bbcode.py index ac16fb2743..5b94243941 100644 --- a/evennia/contrib/base_systems/godotwebsocket/text2bbcode.py +++ b/evennia/contrib/base_systems/godotwebsocket/text2bbcode.py @@ -3,6 +3,7 @@ Godot Websocket - ChrisLR 2022 This file contains the necessary code and data to convert text with color tags to bbcode (For godot) """ + from evennia.utils.ansi import * from evennia.utils.text2html import TextToHTMLparser diff --git a/evennia/contrib/base_systems/godotwebsocket/webclient.py b/evennia/contrib/base_systems/godotwebsocket/webclient.py index 3e77800abd..bd8576ff17 100644 --- a/evennia/contrib/base_systems/godotwebsocket/webclient.py +++ b/evennia/contrib/base_systems/godotwebsocket/webclient.py @@ -4,6 +4,7 @@ Godot Websocket - ChrisLR 2022 This file contains the code necessary to dedicate a port to communicate with Godot via Websockets. It uses the plugin system and should be plugged via settings as detailed in the readme. """ + import json from autobahn.twisted import WebSocketServerFactory diff --git a/evennia/contrib/base_systems/ingame_python/callbackhandler.py b/evennia/contrib/base_systems/ingame_python/callbackhandler.py index adb44395a0..e0bac0c3b3 100644 --- a/evennia/contrib/base_systems/ingame_python/callbackhandler.py +++ b/evennia/contrib/base_systems/ingame_python/callbackhandler.py @@ -6,7 +6,6 @@ from collections import namedtuple class CallbackHandler(object): - """ The callback handler for a specific object. diff --git a/evennia/contrib/base_systems/ingame_python/commands.py b/evennia/contrib/base_systems/ingame_python/commands.py index a3331648be..1d109aa888 100644 --- a/evennia/contrib/base_systems/ingame_python/commands.py +++ b/evennia/contrib/base_systems/ingame_python/commands.py @@ -70,7 +70,6 @@ Use the /del switch to remove callbacks that should not be connected. class CmdCallback(COMMAND_DEFAULT_CLASS): - """ Command to edit callbacks. """ diff --git a/evennia/contrib/base_systems/ingame_python/scripts.py b/evennia/contrib/base_systems/ingame_python/scripts.py index 63c0477008..047dbb10b7 100644 --- a/evennia/contrib/base_systems/ingame_python/scripts.py +++ b/evennia/contrib/base_systems/ingame_python/scripts.py @@ -27,7 +27,6 @@ RE_LINE_ERROR = re.compile(r'^ File "\", line (\d+)') class EventHandler(DefaultScript): - """ The event handler that contains all events in a global script. @@ -600,7 +599,6 @@ class EventHandler(DefaultScript): # Script to call time-related events class TimeEventScript(DefaultScript): - """Gametime-sensitive script.""" def at_script_creation(self): diff --git a/evennia/contrib/base_systems/ingame_python/tests.py b/evennia/contrib/base_systems/ingame_python/tests.py index b3f7d14a32..a0e420cb67 100644 --- a/evennia/contrib/base_systems/ingame_python/tests.py +++ b/evennia/contrib/base_systems/ingame_python/tests.py @@ -25,7 +25,6 @@ OLD_EVENTS = {} class TestEventHandler(BaseEvenniaTest): - """ Test cases of the event handler to add, edit or delete events. """ @@ -259,7 +258,6 @@ class TestEventHandler(BaseEvenniaTest): class TestCmdCallback(BaseEvenniaCommandTest): - """Test the @callback command.""" def setUp(self): @@ -448,7 +446,6 @@ class TestCmdCallback(BaseEvenniaCommandTest): class TestDefaultCallbacks(BaseEvenniaCommandTest): - """Test the default callbacks.""" def setUp(self): diff --git a/evennia/contrib/base_systems/ingame_python/typeclasses.py b/evennia/contrib/base_systems/ingame_python/typeclasses.py index 3922ffd11d..5f14ee8c6e 100644 --- a/evennia/contrib/base_systems/ingame_python/typeclasses.py +++ b/evennia/contrib/base_systems/ingame_python/typeclasses.py @@ -166,7 +166,6 @@ Variables you can use in this event: @register_events class EventCharacter(DefaultCharacter): - """Typeclass to represent a character and call event types.""" _events = { @@ -625,7 +624,6 @@ Variables you can use in this event: @register_events class EventExit(DefaultExit): - """Modified exit including management of events.""" _events = { @@ -721,7 +719,6 @@ Variables you can use in this event: @register_events class EventObject(DefaultObject): - """Default object with management of events.""" _events = { @@ -892,7 +889,6 @@ Variables you can use in this event: @register_events class EventRoom(DefaultRoom): - """Default room with management of events.""" _events = { diff --git a/evennia/contrib/base_systems/ingame_python/utils.py b/evennia/contrib/base_systems/ingame_python/utils.py index c99041e68c..8635792964 100644 --- a/evennia/contrib/base_systems/ingame_python/utils.py +++ b/evennia/contrib/base_systems/ingame_python/utils.py @@ -251,7 +251,6 @@ def phrase_event(callbacks, parameters): class InterruptEvent(RuntimeError): - """ Interrupt the current event. diff --git a/evennia/contrib/base_systems/menu_login/menu_login.py b/evennia/contrib/base_systems/menu_login/menu_login.py index b00dacfb67..bf9d1609d9 100644 --- a/evennia/contrib/base_systems/menu_login/menu_login.py +++ b/evennia/contrib/base_systems/menu_login/menu_login.py @@ -21,10 +21,14 @@ called automatically when a new user connects. """ from django.conf import settings + from evennia import CmdSet, Command, syscmdkeys from evennia.utils.evmenu import EvMenu -from evennia.utils.utils import (callables_from_module, class_from_module, - random_string_from_module) +from evennia.utils.utils import ( + callables_from_module, + class_from_module, + random_string_from_module, +) _CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE _GUEST_ENABLED = settings.GUEST_ENABLED diff --git a/evennia/contrib/base_systems/mux_comms_cmds/mux_comms_cmds.py b/evennia/contrib/base_systems/mux_comms_cmds/mux_comms_cmds.py index 5fadf4117b..ef1a3e0a5a 100644 --- a/evennia/contrib/base_systems/mux_comms_cmds/mux_comms_cmds.py +++ b/evennia/contrib/base_systems/mux_comms_cmds/mux_comms_cmds.py @@ -41,6 +41,7 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet): ``` """ + from django.conf import settings from evennia.commands.cmdset import CmdSet diff --git a/evennia/contrib/base_systems/unixcommand/tests.py b/evennia/contrib/base_systems/unixcommand/tests.py index adb7421352..6a044bdeac 100644 --- a/evennia/contrib/base_systems/unixcommand/tests.py +++ b/evennia/contrib/base_systems/unixcommand/tests.py @@ -9,7 +9,6 @@ from .unixcommand import UnixCommand class CmdDummy(UnixCommand): - """A dummy UnixCommand.""" key = "dummy" diff --git a/evennia/contrib/base_systems/unixcommand/unixcommand.py b/evennia/contrib/base_systems/unixcommand/unixcommand.py index de1c73ab8f..1ad5cbaefe 100644 --- a/evennia/contrib/base_systems/unixcommand/unixcommand.py +++ b/evennia/contrib/base_systems/unixcommand/unixcommand.py @@ -72,14 +72,12 @@ from evennia.utils.ansi import raw class ParseError(Exception): - """An error occurred during parsing.""" pass class UnixCommandParser(argparse.ArgumentParser): - """A modifier command parser for unix commands. This parser is used to replace `argparse.ArgumentParser`. It @@ -183,7 +181,6 @@ class UnixCommandParser(argparse.ArgumentParser): class HelpAction(argparse.Action): - """Override the -h/--help action in the default parser. Using the default -h/--help will call the exit function in different diff --git a/evennia/contrib/full_systems/evscaperoom/menu.py b/evennia/contrib/full_systems/evscaperoom/menu.py index 8e76fb67fe..68f2d910e2 100644 --- a/evennia/contrib/full_systems/evscaperoom/menu.py +++ b/evennia/contrib/full_systems/evscaperoom/menu.py @@ -7,6 +7,7 @@ Here player user can set their own description as well as select to create a new room (to start from scratch) or join an existing room (with other players). """ + from evennia import EvMenu from evennia.utils import create, justify, list_to_string, logger from evennia.utils.evmenu import list_node diff --git a/evennia/contrib/full_systems/evscaperoom/objects.py b/evennia/contrib/full_systems/evscaperoom/objects.py index 627c931472..fceade2bf0 100644 --- a/evennia/contrib/full_systems/evscaperoom/objects.py +++ b/evennia/contrib/full_systems/evscaperoom/objects.py @@ -43,6 +43,7 @@ Available parents: - Positionable (supports sit/lie/knee/climb at once) """ + import inspect import re diff --git a/evennia/contrib/full_systems/evscaperoom/tests.py b/evennia/contrib/full_systems/evscaperoom/tests.py index 1b5cd75e0a..ed599a27d6 100644 --- a/evennia/contrib/full_systems/evscaperoom/tests.py +++ b/evennia/contrib/full_systems/evscaperoom/tests.py @@ -2,6 +2,7 @@ Unit tests for the Evscaperoom """ + import inspect import pkgutil from os import path diff --git a/evennia/contrib/game_systems/clothing/clothing.py b/evennia/contrib/game_systems/clothing/clothing.py index 2c98b17f9f..6fe957eb72 100644 --- a/evennia/contrib/game_systems/clothing/clothing.py +++ b/evennia/contrib/game_systems/clothing/clothing.py @@ -72,9 +72,11 @@ with which to test the system: wear shirt """ + from collections import defaultdict from django.conf import settings + from evennia import DefaultCharacter, DefaultObject, default_cmds from evennia.commands.default.muxcommand import MuxCommand from evennia.utils import ( diff --git a/evennia/contrib/game_systems/containers/containers.py b/evennia/contrib/game_systems/containers/containers.py index 66c31b7175..be718d526e 100644 --- a/evennia/contrib/game_systems/containers/containers.py +++ b/evennia/contrib/game_systems/containers/containers.py @@ -30,6 +30,7 @@ or implement the same locks/hooks in your own typeclasses. at_pre_get_from(getter, target, **kwargs) - called with the pre-get hooks at_pre_put_in(putter, target, **kwargs) - called with the pre-put hooks """ + from django.conf import settings from evennia import AttributeProperty, CmdSet, DefaultObject diff --git a/evennia/contrib/game_systems/crafting/crafting.py b/evennia/contrib/game_systems/crafting/crafting.py index 875e843d5f..254d8aa458 100644 --- a/evennia/contrib/game_systems/crafting/crafting.py +++ b/evennia/contrib/game_systems/crafting/crafting.py @@ -615,9 +615,11 @@ class CraftingRecipe(CraftingRecipeBase): ) else: self.output_names = [ - prot.get("key", prot.get("typeclass", "unnamed")) - if isinstance(prot, dict) - else str(prot) + ( + prot.get("key", prot.get("typeclass", "unnamed")) + if isinstance(prot, dict) + else str(prot) + ) for prot in self.output_prototypes ] diff --git a/evennia/contrib/game_systems/gendersub/tests.py b/evennia/contrib/game_systems/gendersub/tests.py index c53a65db36..842f1d1c20 100644 --- a/evennia/contrib/game_systems/gendersub/tests.py +++ b/evennia/contrib/game_systems/gendersub/tests.py @@ -3,7 +3,6 @@ Test gendersub contrib. """ - from mock import patch from evennia.commands.default.tests import BaseEvenniaCommandTest diff --git a/evennia/contrib/game_systems/multidescer/multidescer.py b/evennia/contrib/game_systems/multidescer/multidescer.py index c32e92eddc..177437206a 100644 --- a/evennia/contrib/game_systems/multidescer/multidescer.py +++ b/evennia/contrib/game_systems/multidescer/multidescer.py @@ -25,6 +25,7 @@ Reload the server and you should have the +desc command available (it will replace the default `desc` command). """ + import re from evennia import default_cmds diff --git a/evennia/contrib/grid/extended_room/extended_room.py b/evennia/contrib/grid/extended_room/extended_room.py index 97dec6056e..882b235576 100644 --- a/evennia/contrib/grid/extended_room/extended_room.py +++ b/evennia/contrib/grid/extended_room/extended_room.py @@ -47,8 +47,17 @@ from collections import deque from django.conf import settings from django.db.models import Q -from evennia import (CmdSet, DefaultRoom, EvEditor, FuncParser, - InterruptCommand, default_cmds, gametime, utils) + +from evennia import ( + CmdSet, + DefaultRoom, + EvEditor, + FuncParser, + InterruptCommand, + default_cmds, + gametime, + utils, +) from evennia.typeclasses.attributes import AttributeProperty from evennia.utils.utils import list_to_string, repeat @@ -814,7 +823,6 @@ class CmdExtendedRoomDesc(default_cmds.CmdDesc): class CmdExtendedRoomDetail(default_cmds.MuxCommand): - """ sets a detail on a room diff --git a/evennia/contrib/grid/extended_room/tests.py b/evennia/contrib/grid/extended_room/tests.py index 2a8ed62256..a5026a42a6 100644 --- a/evennia/contrib/grid/extended_room/tests.py +++ b/evennia/contrib/grid/extended_room/tests.py @@ -6,11 +6,12 @@ Testing of ExtendedRoom contrib import datetime from django.conf import settings -from evennia import create_object -from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase from mock import Mock, patch from parameterized import parameterized +from evennia import create_object +from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase + from . import extended_room diff --git a/evennia/contrib/grid/ingame_map_display/ingame_map_display.py b/evennia/contrib/grid/ingame_map_display/ingame_map_display.py index f6369c7c31..3dec9a1d26 100644 --- a/evennia/contrib/grid/ingame_map_display/ingame_map_display.py +++ b/evennia/contrib/grid/ingame_map_display/ingame_map_display.py @@ -56,6 +56,7 @@ This changes the default map width/height. 2-5 for most clients is sensible. If you don't want the player to be able to specify the size of the map, ignore any arguments passed into the Map command. """ + import time from django.conf import settings diff --git a/evennia/contrib/grid/ingame_map_display/tests.py b/evennia/contrib/grid/ingame_map_display/tests.py index 4af1cdc41c..d047957c1e 100644 --- a/evennia/contrib/grid/ingame_map_display/tests.py +++ b/evennia/contrib/grid/ingame_map_display/tests.py @@ -3,7 +3,6 @@ Tests of ingame_map_display. """ - from typeclasses import exits, rooms from evennia.commands.default.tests import BaseEvenniaCommandTest diff --git a/evennia/contrib/grid/simpledoor/tests.py b/evennia/contrib/grid/simpledoor/tests.py index 9c0d3353c1..a97d31da92 100644 --- a/evennia/contrib/grid/simpledoor/tests.py +++ b/evennia/contrib/grid/simpledoor/tests.py @@ -3,7 +3,6 @@ Tests of simpledoor. """ - from evennia.commands.default.tests import BaseEvenniaCommandTest from . import simpledoor diff --git a/evennia/contrib/grid/xyzgrid/__init__.py b/evennia/contrib/grid/xyzgrid/__init__.py index cc2ad276e1..f086fdaa2b 100644 --- a/evennia/contrib/grid/xyzgrid/__init__.py +++ b/evennia/contrib/grid/xyzgrid/__init__.py @@ -2,6 +2,7 @@ XYZGrid - Griatch 2021 """ + from . import ( example, launchcmd, diff --git a/evennia/contrib/grid/xyzgrid/prototypes.py b/evennia/contrib/grid/xyzgrid/prototypes.py index 2c0341dc24..8f7804dc6f 100644 --- a/evennia/contrib/grid/xyzgrid/prototypes.py +++ b/evennia/contrib/grid/xyzgrid/prototypes.py @@ -14,6 +14,7 @@ and/or {'prototype_parent': 'xyz_exit', ...} """ + from django.conf import settings try: diff --git a/evennia/contrib/grid/xyzgrid/tests.py b/evennia/contrib/grid/xyzgrid/tests.py index a3cd7441d1..602f1bfed0 100644 --- a/evennia/contrib/grid/xyzgrid/tests.py +++ b/evennia/contrib/grid/xyzgrid/tests.py @@ -3,14 +3,15 @@ Tests for the XYZgrid system. """ + from random import randint from unittest import mock from django.test import TestCase -from evennia.utils.test_resources import (BaseEvenniaCommandTest, - BaseEvenniaTest) from parameterized import parameterized +from evennia.utils.test_resources import BaseEvenniaCommandTest, BaseEvenniaTest + from . import commands, xymap, xymap_legend, xyzgrid, xyzroom MAP1 = """ diff --git a/evennia/contrib/grid/xyzgrid/xymap.py b/evennia/contrib/grid/xyzgrid/xymap.py index 41135b52ba..9eb494dac0 100644 --- a/evennia/contrib/grid/xyzgrid/xymap.py +++ b/evennia/contrib/grid/xyzgrid/xymap.py @@ -92,6 +92,7 @@ See `./example.py` for a full grid example. ---- """ + import pickle from collections import defaultdict from os import mkdir @@ -108,6 +109,7 @@ except ImportError as err: "the SciPy package. Install with `pip install scipy'." ) from django.conf import settings + from evennia.prototypes import prototypes as protlib from evennia.prototypes.spawner import flatten_prototype from evennia.utils import logger @@ -172,6 +174,7 @@ class XYMap: but recommended for readability! """ + mapcorner_symbol = "+" max_pathfinding_length = 500 empty_symbol = " " @@ -475,10 +478,10 @@ class XYMap: max_X, max_Y = max(max_X, iX), max(max_Y, iY) node_index += 1 - xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[ - node_index - ] = mapnode_or_link_class( - x=ix, y=iy, Z=self.Z, node_index=node_index, symbol=char, xymap=self + xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[node_index] = ( + mapnode_or_link_class( + x=ix, y=iy, Z=self.Z, node_index=node_index, symbol=char, xymap=self + ) ) else: @@ -668,8 +671,7 @@ class XYMap: """ global _XYZROOMCLASS if not _XYZROOMCLASS: - from evennia.contrib.grid.xyzgrid.xyzroom import \ - XYZRoom as _XYZROOMCLASS + from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS x, y = xy wildcard = "*" spawned = [] diff --git a/evennia/contrib/grid/xyzgrid/xymap_legend.py b/evennia/contrib/grid/xyzgrid/xymap_legend.py index 1a42fc3eb4..8a205c48d2 100644 --- a/evennia/contrib/grid/xyzgrid/xymap_legend.py +++ b/evennia/contrib/grid/xyzgrid/xymap_legend.py @@ -20,11 +20,11 @@ import uuid from collections import defaultdict from django.core import exceptions as django_exceptions + from evennia.prototypes import spawner from evennia.utils.utils import class_from_module -from .utils import (BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, - MapParserError) +from .utils import BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, MapParserError NodeTypeclass = None ExitTypeclass = None @@ -844,6 +844,7 @@ class SmartRerouterMapLink(MapLink): /| """ + multilink = True def get_direction(self, start_direction): diff --git a/evennia/contrib/grid/xyzgrid/xyzgrid.py b/evennia/contrib/grid/xyzgrid/xyzgrid.py index 2065eeea12..56626c8cee 100644 --- a/evennia/contrib/grid/xyzgrid/xyzgrid.py +++ b/evennia/contrib/grid/xyzgrid/xyzgrid.py @@ -16,6 +16,7 @@ The grid has three main functions: """ + from evennia.scripts.scripts import DefaultScript from evennia.utils import logger from evennia.utils.utils import variable_from_module diff --git a/evennia/contrib/rpg/buffs/__init__.py b/evennia/contrib/rpg/buffs/__init__.py index d3e73893db..c08e1d8ceb 100644 --- a/evennia/contrib/rpg/buffs/__init__.py +++ b/evennia/contrib/rpg/buffs/__init__.py @@ -1,4 +1,3 @@ -from .buff import (BaseBuff, BuffableProperty, BuffHandler, CmdBuff, # noqa - Mod, cleanup_buffs, tick_buff) -from .samplebuffs import (Exploit, Exploited, Leeching, Poison, Sated, # noqa - StatBuff) +from .buff import CmdBuff # noqa +from .buff import BaseBuff, BuffableProperty, BuffHandler, Mod, cleanup_buffs, tick_buff +from .samplebuffs import Exploit, Exploited, Leeching, Poison, Sated, StatBuff # noqa diff --git a/evennia/contrib/rpg/buffs/buff.py b/evennia/contrib/rpg/buffs/buff.py index e09a070125..d77d47dc9f 100644 --- a/evennia/contrib/rpg/buffs/buff.py +++ b/evennia/contrib/rpg/buffs/buff.py @@ -98,6 +98,7 @@ You can see all the features of the `BaseBuff` class below, or browse `samplebuf many attributes and hook methods you can overload to create complex, interrelated buffs. """ + import time from random import random diff --git a/evennia/contrib/rpg/buffs/tests.py b/evennia/contrib/rpg/buffs/tests.py index 091db9acb6..50dea4da23 100644 --- a/evennia/contrib/rpg/buffs/tests.py +++ b/evennia/contrib/rpg/buffs/tests.py @@ -1,6 +1,7 @@ """ Tests for the buff system contrib """ + from unittest.mock import Mock, call, patch from evennia import DefaultObject, create_object diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index 8d1db4b6cc..994093b427 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -15,6 +15,7 @@ and examples, including how to allow players to choose and confirm character names from within the menu. """ + import string from random import choices diff --git a/evennia/contrib/rpg/dice/dice.py b/evennia/contrib/rpg/dice/dice.py index 5551ca7f64..5eec8df025 100644 --- a/evennia/contrib/rpg/dice/dice.py +++ b/evennia/contrib/rpg/dice/dice.py @@ -57,6 +57,7 @@ of the roll separately: """ + import re from ast import literal_eval from random import randint diff --git a/evennia/contrib/rpg/rpsystem/rplanguage.py b/evennia/contrib/rpg/rpsystem/rplanguage.py index 4be5782acc..a9a5cddade 100644 --- a/evennia/contrib/rpg/rpsystem/rplanguage.py +++ b/evennia/contrib/rpg/rpsystem/rplanguage.py @@ -137,6 +137,7 @@ This allows to quickly build a large corpus of translated words that never change (if this is desired). """ + import re from collections import defaultdict from random import choice, randint diff --git a/evennia/contrib/rpg/rpsystem/rpsystem.py b/evennia/contrib/rpg/rpsystem/rpsystem.py index bf808ff307..7cb9de81ab 100644 --- a/evennia/contrib/rpg/rpsystem/rpsystem.py +++ b/evennia/contrib/rpg/rpsystem/rpsystem.py @@ -148,19 +148,25 @@ Extra Installation Instructions: `type/reset/force me = typeclasses.characters.Character` """ + import re from collections import defaultdict from string import punctuation import inflect from django.conf import settings + from evennia.commands.cmdset import CmdSet from evennia.commands.command import Command from evennia.objects.models import ObjectDB from evennia.objects.objects import DefaultCharacter, DefaultObject from evennia.utils import ansi, logger -from evennia.utils.utils import (iter_to_str, lazy_property, make_iter, - variable_from_module) +from evennia.utils.utils import ( + iter_to_str, + lazy_property, + make_iter, + variable_from_module, +) _INFLECT = inflect.engine() diff --git a/evennia/contrib/rpg/rpsystem/tests.py b/evennia/contrib/rpg/rpsystem/tests.py index 04d4ef49de..df40eb367b 100644 --- a/evennia/contrib/rpg/rpsystem/tests.py +++ b/evennia/contrib/rpg/rpsystem/tests.py @@ -2,9 +2,11 @@ Tests for RP system """ + import time from anything import Anything + from evennia import DefaultObject, create_object, default_cmds from evennia.commands.default import building from evennia.commands.default.tests import BaseEvenniaCommandTest @@ -426,11 +428,11 @@ class TestRPSystemCommands(BaseEvenniaCommandTest): self.call(default_cmds.CmdLook(), "Mushroom-2", expected_third_call) # FAILS expected_fourth_call = "Alias(es) for 'Mushroom' set to 'fungus'." - self.call(building.CmdSetObjAlias(), "Mushroom-1 = fungus", expected_fourth_call) #PASSES + self.call(building.CmdSetObjAlias(), "Mushroom-1 = fungus", expected_fourth_call) # PASSES expected_fifth_call = [ "More than one match for 'Mushroom' (please narrow target):", f" Mushroom-1 [fungus]", f" Mushroom-2", ] - self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_fifth_call)) # PASSES + self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_fifth_call)) # PASSES diff --git a/evennia/contrib/rpg/traits/traits.py b/evennia/contrib/rpg/traits/traits.py index 1a2b8b99b7..fbcbeda34a 100644 --- a/evennia/contrib/rpg/traits/traits.py +++ b/evennia/contrib/rpg/traits/traits.py @@ -452,7 +452,6 @@ class Character(DefaultCharacter): """ - from functools import total_ordering from time import time diff --git a/evennia/contrib/tutorials/bodyfunctions/bodyfunctions.py b/evennia/contrib/tutorials/bodyfunctions/bodyfunctions.py index 009928da1f..90afb46f76 100644 --- a/evennia/contrib/tutorials/bodyfunctions/bodyfunctions.py +++ b/evennia/contrib/tutorials/bodyfunctions/bodyfunctions.py @@ -13,6 +13,7 @@ The script will only send messages to the object it is stored on, so make sure to put it on yourself or you won't see any messages! """ + import random from evennia import DefaultScript diff --git a/evennia/contrib/tutorials/bodyfunctions/tests.py b/evennia/contrib/tutorials/bodyfunctions/tests.py index 2c7d6c46e6..564f7ab6e4 100644 --- a/evennia/contrib/tutorials/bodyfunctions/tests.py +++ b/evennia/contrib/tutorials/bodyfunctions/tests.py @@ -2,6 +2,7 @@ Tests for the bodyfunctions. """ + from mock import Mock, patch from evennia.utils.test_resources import BaseEvenniaTest diff --git a/evennia/contrib/tutorials/evadventure/ai.py b/evennia/contrib/tutorials/evadventure/ai.py index 90338e968c..6a65943a38 100644 --- a/evennia/contrib/tutorials/evadventure/ai.py +++ b/evennia/contrib/tutorials/evadventure/ai.py @@ -49,9 +49,9 @@ class AIHandler: def __init__(self, obj): self.obj = obj - self.ai_state = obj.attributes.get(self.attribute_name, - category=self.attribute_category, - default="idle") + self.ai_state = obj.attributes.get( + self.attribute_name, category=self.attribute_category, default="idle" + ) def set_state(self, state): self.ai_state = state @@ -122,6 +122,7 @@ class AIMixin: of multiple inheritance. In a real game, you would probably want to use a mixin like this. """ + @lazy_property def ai(self): return AIHandler(self) diff --git a/evennia/contrib/tutorials/evadventure/chargen.py b/evennia/contrib/tutorials/evadventure/chargen.py index f27012e4df..a1b429e3d1 100644 --- a/evennia/contrib/tutorials/evadventure/chargen.py +++ b/evennia/contrib/tutorials/evadventure/chargen.py @@ -2,6 +2,7 @@ EvAdventure character generation. """ + from django.conf import settings from evennia.objects.models import ObjectDB diff --git a/evennia/contrib/tutorials/evadventure/combat_turnbased.py b/evennia/contrib/tutorials/evadventure/combat_turnbased.py index fdea4a0038..b67c0db028 100644 --- a/evennia/contrib/tutorials/evadventure/combat_turnbased.py +++ b/evennia/contrib/tutorials/evadventure/combat_turnbased.py @@ -18,7 +18,6 @@ action that takes several vulnerable turns to complete. """ - import random from collections import defaultdict @@ -26,9 +25,15 @@ from evennia import AttributeProperty, CmdSet, Command, EvMenu from evennia.utils import inherits_from, list_to_string from .characters import EvAdventureCharacter -from .combat_base import (CombatAction, CombatActionAttack, CombatActionHold, - CombatActionStunt, CombatActionUseItem, - CombatActionWield, EvAdventureCombatBaseHandler) +from .combat_base import ( + CombatAction, + CombatActionAttack, + CombatActionHold, + CombatActionStunt, + CombatActionUseItem, + CombatActionWield, + EvAdventureCombatBaseHandler, +) from .enums import Ability diff --git a/evennia/contrib/tutorials/evadventure/combat_twitch.py b/evennia/contrib/tutorials/evadventure/combat_twitch.py index 9e860ffff8..d8a8d38b3f 100644 --- a/evennia/contrib/tutorials/evadventure/combat_twitch.py +++ b/evennia/contrib/tutorials/evadventure/combat_twitch.py @@ -6,15 +6,27 @@ This implements a 'twitch' (aka DIKU or other traditional muds) style of MUD com ---- """ + from evennia import AttributeProperty, CmdSet, default_cmds from evennia.commands.command import Command, InterruptCommand -from evennia.utils.utils import (display_len, inherits_from, list_to_string, - pad, repeat, unrepeat) +from evennia.utils.utils import ( + display_len, + inherits_from, + list_to_string, + pad, + repeat, + unrepeat, +) from .characters import EvAdventureCharacter -from .combat_base import (CombatActionAttack, CombatActionHold, - CombatActionStunt, CombatActionUseItem, - CombatActionWield, EvAdventureCombatBaseHandler) +from .combat_base import ( + CombatActionAttack, + CombatActionHold, + CombatActionStunt, + CombatActionUseItem, + CombatActionWield, + EvAdventureCombatBaseHandler, +) from .enums import ABILITY_REVERSE_MAP diff --git a/evennia/contrib/tutorials/evadventure/dungeon.py b/evennia/contrib/tutorials/evadventure/dungeon.py index d03da74576..6e56bf0562 100644 --- a/evennia/contrib/tutorials/evadventure/dungeon.py +++ b/evennia/contrib/tutorials/evadventure/dungeon.py @@ -105,10 +105,10 @@ class EvAdventureDungeonRoom(EvAdventureRoom): """ self.tags.add("not_clear", category="dungeon_room") + def clear_room(self): self.tags.remove("not_clear", category="dungeon_room") - @property def is_room_clear(self): return not bool(self.tags.get("not_clear", category="dungeon_room")) @@ -146,9 +146,7 @@ class EvAdventureDungeonExit(DefaultExit): dungeon_branch = self.location.db.dungeon_branch if target_location == self.location: # destination points back to us - create a new room - self.destination = target_location = dungeon_branch.new_room( - self - ) + self.destination = target_location = dungeon_branch.new_room(self) dungeon_branch.register_exit_traversed(self) super().at_traverse(traversing_object, target_location, **kwargs) diff --git a/evennia/contrib/tutorials/evadventure/enums.py b/evennia/contrib/tutorials/evadventure/enums.py index 6ba77d82f9..cb7d4636e5 100644 --- a/evennia/contrib/tutorials/evadventure/enums.py +++ b/evennia/contrib/tutorials/evadventure/enums.py @@ -17,6 +17,7 @@ To get the `value` of an enum (must always be hashable, useful for Attribute loo ---- """ + from enum import Enum diff --git a/evennia/contrib/tutorials/evadventure/npcs.py b/evennia/contrib/tutorials/evadventure/npcs.py index 755ce70940..2afd9f73a9 100644 --- a/evennia/contrib/tutorials/evadventure/npcs.py +++ b/evennia/contrib/tutorials/evadventure/npcs.py @@ -2,6 +2,7 @@ EvAdventure NPCs. This includes both friends and enemies, only separated by their AI. """ + from random import choice from evennia import DefaultCharacter @@ -253,6 +254,7 @@ class EvAdventureMob(EvAdventureNPC): Mob (mobile) NPC; this is usually an enemy. """ + # change this to make the mob more or less likely to perform different actions combat_probabilities = { "hold": 0.0, diff --git a/evennia/contrib/tutorials/evadventure/quests.py b/evennia/contrib/tutorials/evadventure/quests.py index 38476a058e..5e862ede29 100644 --- a/evennia/contrib/tutorials/evadventure/quests.py +++ b/evennia/contrib/tutorials/evadventure/quests.py @@ -59,7 +59,6 @@ class EvAdventureQuest: desc = "This is the base quest class" start_step = "start" - # help entries for quests (could also be methods) help_start = "You need to start first" help_end = "You need to end the quest" @@ -191,8 +190,9 @@ class EvAdventureQuest: """ if self.status in ("abandoned", "completed", "failed"): - help_resource = getattr(self, f"help_{self.status}", - f"You have {self.status} this quest.") + help_resource = getattr( + self, f"help_{self.status}", f"You have {self.status} this quest." + ) else: help_resource = getattr(self, f"help_{self.current_step}", "No help available.") @@ -203,7 +203,6 @@ class EvAdventureQuest: # normally it's just a string return str(help_resource) - # step methods and hooks def step_start(self, *args, **kwargs): @@ -377,6 +376,7 @@ class CmdQuests(Command): quest """ + key = "quests" aliases = ["quest"] @@ -399,4 +399,3 @@ class CmdQuests(Command): for quest in quests: self.msg(f"Quest {quest.key}: {quest.status}") - diff --git a/evennia/contrib/tutorials/evadventure/rules.py b/evennia/contrib/tutorials/evadventure/rules.py index b0075b8e1f..9ad38f4f93 100644 --- a/evennia/contrib/tutorials/evadventure/rules.py +++ b/evennia/contrib/tutorials/evadventure/rules.py @@ -7,6 +7,7 @@ and determining what the outcome is. ---- """ + from random import randint from .enums import Ability diff --git a/evennia/contrib/tutorials/evadventure/tests/test_ai.py b/evennia/contrib/tutorials/evadventure/tests/test_ai.py index f96b4fb478..16fff27c98 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_ai.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_ai.py @@ -2,6 +2,7 @@ Test the ai module. """ + from unittest.mock import Mock, patch from evennia import create_object diff --git a/evennia/contrib/tutorials/evadventure/tests/test_equipment.py b/evennia/contrib/tutorials/evadventure/tests/test_equipment.py index 1dbe647887..58c53101e9 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_equipment.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_equipment.py @@ -3,7 +3,6 @@ Test the EvAdventure equipment handler. """ - from unittest.mock import MagicMock, patch from parameterized import parameterized diff --git a/evennia/contrib/tutorials/evadventure/utils.py b/evennia/contrib/tutorials/evadventure/utils.py index 6fb12de138..25152c236f 100644 --- a/evennia/contrib/tutorials/evadventure/utils.py +++ b/evennia/contrib/tutorials/evadventure/utils.py @@ -17,7 +17,6 @@ Attacks using |w{attack_type_name}|n against |w{defense_type_name}|n Damage roll: |w{damage_roll}|n""".strip() - def get_obj_stats(obj, owner=None): """ Get a string of stats about the object. diff --git a/evennia/contrib/tutorials/red_button/red_button.py b/evennia/contrib/tutorials/red_button/red_button.py index 51ee69060f..b4c9f74f3a 100644 --- a/evennia/contrib/tutorials/red_button/red_button.py +++ b/evennia/contrib/tutorials/red_button/red_button.py @@ -31,6 +31,7 @@ Timers are handled by persistent delays on the button. These are examples of such as when closing the lid and un-blinding a character. """ + import random from evennia import CmdSet, Command, DefaultObject diff --git a/evennia/contrib/tutorials/talking_npc/tests.py b/evennia/contrib/tutorials/talking_npc/tests.py index 9a33eccad0..92bbb630b0 100644 --- a/evennia/contrib/tutorials/talking_npc/tests.py +++ b/evennia/contrib/tutorials/talking_npc/tests.py @@ -2,6 +2,7 @@ Tutorial - talking NPC tests. """ + from evennia.commands.default.tests import BaseEvenniaCommandTest from evennia.utils.create import create_object diff --git a/evennia/contrib/tutorials/tutorial_world/rooms.py b/evennia/contrib/tutorials/tutorial_world/rooms.py index 6d27118812..42c4f6fc2d 100644 --- a/evennia/contrib/tutorials/tutorial_world/rooms.py +++ b/evennia/contrib/tutorials/tutorial_world/rooms.py @@ -9,7 +9,6 @@ in a separate module (e.g. if they could have been re-used elsewhere.) """ - import random # the system error-handling module is defined in the settings. We load the diff --git a/evennia/contrib/utils/auditing/outputs.py b/evennia/contrib/utils/auditing/outputs.py index d6d4f504ef..cafae2b118 100644 --- a/evennia/contrib/utils/auditing/outputs.py +++ b/evennia/contrib/utils/auditing/outputs.py @@ -13,6 +13,7 @@ the easiest place to do it. Write a method and invoke it via Evennia contribution - Johnny 2017 """ + import json import syslog diff --git a/evennia/contrib/utils/auditing/server.py b/evennia/contrib/utils/auditing/server.py index 6646d07f32..2337b457c9 100644 --- a/evennia/contrib/utils/auditing/server.py +++ b/evennia/contrib/utils/auditing/server.py @@ -5,6 +5,7 @@ user inputs and system outputs. Evennia contribution - Johnny 2017 """ + import os import re import socket diff --git a/evennia/contrib/utils/fieldfill/fieldfill.py b/evennia/contrib/utils/fieldfill/fieldfill.py index 625002611d..b94c8e5499 100644 --- a/evennia/contrib/utils/fieldfill/fieldfill.py +++ b/evennia/contrib/utils/fieldfill/fieldfill.py @@ -138,6 +138,7 @@ Optional: object dbrefs). For boolean fields, return '0' or '1' to set the field to False or True. """ + import evennia from evennia import Command from evennia.utils import delay, evmenu, evtable, list_to_string, logger diff --git a/evennia/contrib/utils/random_string_generator/random_string_generator.py b/evennia/contrib/utils/random_string_generator/random_string_generator.py index 21fb68fe60..708064caf8 100644 --- a/evennia/contrib/utils/random_string_generator/random_string_generator.py +++ b/evennia/contrib/utils/random_string_generator/random_string_generator.py @@ -59,7 +59,6 @@ from evennia.utils.create import create_script class RejectedRegex(RuntimeError): - """The provided regular expression has been rejected. More details regarding why this error occurred will be provided in @@ -72,14 +71,12 @@ class RejectedRegex(RuntimeError): class ExhaustedGenerator(RuntimeError): - """The generator hasn't any available strings to generate anymore.""" pass class RandomStringGeneratorScript(DefaultScript): - """ The global script to hold all generators. @@ -99,7 +96,6 @@ class RandomStringGeneratorScript(DefaultScript): class RandomStringGenerator: - """ A generator class to generate pseudo-random strings with a rule. diff --git a/evennia/game_template/typeclasses/characters.py b/evennia/game_template/typeclasses/characters.py index c0cdc30f08..b022c1f293 100644 --- a/evennia/game_template/typeclasses/characters.py +++ b/evennia/game_template/typeclasses/characters.py @@ -7,6 +7,7 @@ is setup to be the "default" character type created by the default creation commands. """ + from evennia.objects.objects import DefaultCharacter from .objects import ObjectParent diff --git a/evennia/game_template/typeclasses/exits.py b/evennia/game_template/typeclasses/exits.py index 8ccd996b8b..3a53753c2e 100644 --- a/evennia/game_template/typeclasses/exits.py +++ b/evennia/game_template/typeclasses/exits.py @@ -6,6 +6,7 @@ set and has a single command defined on itself with the same name as its key, for allowing Characters to traverse the exit to its destination. """ + from evennia.objects.objects import DefaultExit from .objects import ObjectParent diff --git a/evennia/game_template/typeclasses/objects.py b/evennia/game_template/typeclasses/objects.py index b8aab3eeb6..11b7363505 100644 --- a/evennia/game_template/typeclasses/objects.py +++ b/evennia/game_template/typeclasses/objects.py @@ -10,6 +10,7 @@ the other types, you can do so by adding this as a multiple inheritance. """ + from evennia.objects.objects import DefaultObject diff --git a/evennia/game_template/web/admin/urls.py b/evennia/game_template/web/admin/urls.py index 196d574160..c42f8dc16d 100644 --- a/evennia/game_template/web/admin/urls.py +++ b/evennia/game_template/web/admin/urls.py @@ -6,7 +6,6 @@ The main web/urls.py includes these routes for all urls starting with `admin/` """ - from django.urls import path from evennia.web.admin.urls import urlpatterns as evennia_admin_urlpatterns diff --git a/evennia/game_template/web/urls.py b/evennia/game_template/web/urls.py index 673a10bf08..3f306400cd 100644 --- a/evennia/game_template/web/urls.py +++ b/evennia/game_template/web/urls.py @@ -12,6 +12,7 @@ should modify urls.py in those sub directories. Search the Django documentation for "URL dispatcher" for more help. """ + from django.urls import include, path # default evennia patterns diff --git a/evennia/help/manager.py b/evennia/help/manager.py index ff8af1e7c9..58773a5472 100644 --- a/evennia/help/manager.py +++ b/evennia/help/manager.py @@ -1,6 +1,7 @@ """ Custom manager for HelpEntry objects. """ + from django.db import IntegrityError from evennia.server import signals diff --git a/evennia/help/models.py b/evennia/help/models.py index c86cab61a8..e9bdcf9d33 100644 --- a/evennia/help/models.py +++ b/evennia/help/models.py @@ -9,6 +9,7 @@ forms of help that do not concern commands, like information about the game world, policy info, rules and similar. """ + from django.contrib.contenttypes.models import ContentType from django.db import models from django.urls import reverse diff --git a/evennia/help/utils.py b/evennia/help/utils.py index 5ef3157ca9..716122accf 100644 --- a/evennia/help/utils.py +++ b/evennia/help/utils.py @@ -5,6 +5,7 @@ sub-categories. This is used primarily by the default `help` command. """ + import re from django.conf import settings diff --git a/evennia/locks/lockfuncs.py b/evennia/locks/lockfuncs.py index d477b5a1ca..4121107b22 100644 --- a/evennia/locks/lockfuncs.py +++ b/evennia/locks/lockfuncs.py @@ -13,7 +13,6 @@ a certain object type. """ - from ast import literal_eval from django.conf import settings diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index 7b5f51cb75..f962d7e55a 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -1,11 +1,13 @@ """ Custom manager for Objects. """ + import re from django.conf import settings from django.db.models import Q from django.db.models.fields import exceptions + from evennia.server import signals from evennia.typeclasses.managers import TypeclassManager, TypedObjectManager from evennia.utils.utils import ( diff --git a/evennia/objects/models.py b/evennia/objects/models.py index a282bafe0f..5d4bfd4acf 100644 --- a/evennia/objects/models.py +++ b/evennia/objects/models.py @@ -13,12 +13,14 @@ Attributes are separate objects that store values persistently onto the database object. Like everything else, they can be accessed transparently through the decorating TypeClass. """ + from collections import defaultdict from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.core.validators import validate_comma_separated_integer_list from django.db import models + from evennia.objects.manager import ObjectDBManager from evennia.typeclasses.models import TypedObject from evennia.utils import logger diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 57d29dd722..f11b43bc51 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -5,14 +5,16 @@ These are the (default) starting points for all in-game visible entities. """ + import time import typing from collections import defaultdict -import evennia import inflect from django.conf import settings from django.utils.translation import gettext as _ + +import evennia from evennia.commands import cmdset from evennia.commands.cmdsethandler import CmdSetHandler from evennia.objects.manager import ObjectManager @@ -22,9 +24,17 @@ from evennia.server.signals import SIGNAL_EXIT_TRAVERSED from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import ansi, create, funcparser, logger, search -from evennia.utils.utils import (class_from_module, compress_whitespace, dbref, - is_iter, iter_to_str, lazy_property, - make_iter, to_str, variable_from_module) +from evennia.utils.utils import ( + class_from_module, + compress_whitespace, + dbref, + is_iter, + iter_to_str, + lazy_property, + make_iter, + to_str, + variable_from_module, +) _INFLECT = inflect.engine() _MULTISESSION_MODE = settings.MULTISESSION_MODE @@ -955,9 +965,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # director-stance replacements outmessage = outmessage.format_map( { - key: obj.get_display_name(looker=receiver) - if hasattr(obj, "get_display_name") - else str(obj) + key: ( + obj.get_display_name(looker=receiver) + if hasattr(obj, "get_display_name") + else str(obj) + ) for key, obj in mapping.items() } ) @@ -1419,10 +1431,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): method to implement custom visibility mechanics. """ - return [obj for obj in obj_list - if (obj.access(looker, "view") - and obj.access(looker, "search", default=True)) - ] + return [ + obj + for obj in obj_list + if (obj.access(looker, "view") and obj.access(looker, "search", default=True)) + ] # name and return_appearance hooks @@ -1522,7 +1535,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): return key, key if kwargs.get("return_string"): - return singular if count==1 else plural + return singular if count == 1 else plural return singular, plural @@ -1579,8 +1592,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): str: The character display data. """ - characters = self.filter_visible(self.contents_get(content_type="character"), looker, - **kwargs) + characters = self.filter_visible( + self.contents_get(content_type="character"), looker, **kwargs + ) character_names = iter_to_str( char.get_display_name(looker, **kwargs) for char in characters ) @@ -2679,9 +2693,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): "object": self.get_display_name(self), "location": location.get_display_name(self) if location else None, "receiver": None, - "all_receivers": ", ".join(recv.get_display_name(self) for recv in receivers) - if receivers - else None, + "all_receivers": ( + ", ".join(recv.get_display_name(self) for recv in receivers) + if receivers + else None + ), "speech": message, } self_mapping.update(custom_mapping) @@ -2701,9 +2717,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): "object": self.get_display_name(receiver), "location": location.get_display_name(receiver), "receiver": receiver.get_display_name(receiver), - "all_receivers": ", ".join(recv.get_display_name(recv) for recv in receivers) - if receivers - else None, + "all_receivers": ( + ", ".join(recv.get_display_name(recv) for recv in receivers) + if receivers + else None + ), } receiver_mapping.update(individual_mapping) receiver_mapping.update(custom_mapping) diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index abc2adecdb..de23269cdf 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -191,6 +191,7 @@ class DefaultObjectTest(BaseEvenniaTest): self.obj1.get_numbered_name(1, self.char1, return_string=True, no_article=True), "Obj" ) + class TestObjectManager(BaseEvenniaTest): "Test object manager methods" diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 20a39bf4ac..b77651a6db 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -132,7 +132,6 @@ prototype, override its name with an empty dict. """ - import copy import hashlib import time diff --git a/evennia/scripts/models.py b/evennia/scripts/models.py index 6bff24c571..e2f98dea4c 100644 --- a/evennia/scripts/models.py +++ b/evennia/scripts/models.py @@ -24,6 +24,7 @@ Common examples of uses of Scripts: - Give the account/object a time-limited bonus/effect """ + from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db import models diff --git a/evennia/scripts/monitorhandler.py b/evennia/scripts/monitorhandler.py index 78ad71e77d..6789d3b55f 100644 --- a/evennia/scripts/monitorhandler.py +++ b/evennia/scripts/monitorhandler.py @@ -10,6 +10,7 @@ functionality: an action whenever that Attribute *changes* for whatever reason. """ + import inspect from collections import defaultdict diff --git a/evennia/scripts/scripthandler.py b/evennia/scripts/scripthandler.py index 8b8eb4bb45..720dd6e3a6 100644 --- a/evennia/scripts/scripthandler.py +++ b/evennia/scripts/scripthandler.py @@ -5,6 +5,7 @@ added to all game objects. You access it through the property `scripts` on the game object. """ + from django.utils.translation import gettext as _ from evennia.scripts.models import ScriptDB diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index b47a7f8166..a6539fb1ad 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -205,7 +205,6 @@ class TaskHandlerTask: class TaskHandler: - """A light singleton wrapper allowing to access permanent tasks. When `utils.delay` is called, the task handler is used to create diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 6d251eed54..8bf6983e21 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -6,6 +6,8 @@ Unit tests for the scripts package from collections import defaultdict from unittest import TestCase, mock +from parameterized import parameterized + from evennia import DefaultScript from evennia.objects.objects import DefaultObject from evennia.scripts.manager import ScriptDBManager @@ -17,7 +19,6 @@ from evennia.scripts.tickerhandler import TickerHandler from evennia.utils.create import create_script from evennia.utils.dbserialize import dbserialize from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTest -from parameterized import parameterized class TestScript(BaseEvenniaTest): diff --git a/evennia/scripts/tickerhandler.py b/evennia/scripts/tickerhandler.py index 7dd702cca3..3968cee69a 100644 --- a/evennia/scripts/tickerhandler.py +++ b/evennia/scripts/tickerhandler.py @@ -65,6 +65,7 @@ a custom handler one can make a custom `AT_STARTSTOP_MODULE` entry to call the handler's `save()` and `restore()` methods when the server reboots. """ + import inspect from django.core.exceptions import ObjectDoesNotExist diff --git a/evennia/server/connection_wizard.py b/evennia/server/connection_wizard.py index 2b17c9786b..440a884153 100644 --- a/evennia/server/connection_wizard.py +++ b/evennia/server/connection_wizard.py @@ -2,6 +2,7 @@ Link Evennia to external resources (wizard plugin for evennia_launcher) """ + import pprint import sys from os import path diff --git a/evennia/server/deprecations.py b/evennia/server/deprecations.py index 643f02afdc..a93f075cd9 100644 --- a/evennia/server/deprecations.py +++ b/evennia/server/deprecations.py @@ -4,6 +4,7 @@ checks for. These all print to the terminal. """ + import os diff --git a/evennia/server/game_index_client/client.py b/evennia/server/game_index_client/client.py index 02b9241379..c39c336f70 100644 --- a/evennia/server/game_index_client/client.py +++ b/evennia/server/game_index_client/client.py @@ -2,6 +2,7 @@ The client for sending data to the Evennia Game Index """ + import platform import urllib.error import urllib.parse diff --git a/evennia/server/game_index_client/service.py b/evennia/server/game_index_client/service.py index f4a9899c28..ff5edc1bbd 100644 --- a/evennia/server/game_index_client/service.py +++ b/evennia/server/game_index_client/service.py @@ -2,6 +2,7 @@ Service for integrating the Evennia Game Index client into Evennia. """ + from twisted.application.service import Service from twisted.internet import reactor from twisted.internet.task import LoopingCall diff --git a/evennia/server/initial_setup.py b/evennia/server/initial_setup.py index e886718a5e..4785e98f12 100644 --- a/evennia/server/initial_setup.py +++ b/evennia/server/initial_setup.py @@ -6,7 +6,6 @@ Limbo room). It will also hooks, and then perform an initial restart. Everything starts at handle_setup() """ - import time from django.conf import settings diff --git a/evennia/server/manager.py b/evennia/server/manager.py index 237c40df55..7424fa71b1 100644 --- a/evennia/server/manager.py +++ b/evennia/server/manager.py @@ -1,6 +1,7 @@ """ Custom manager for ServerConfig objects. """ + from django.db import models diff --git a/evennia/server/models.py b/evennia/server/models.py index 8a1b8e58b6..f945409ada 100644 --- a/evennia/server/models.py +++ b/evennia/server/models.py @@ -8,6 +8,7 @@ Config values should usually be set through the manager's conf() method. """ + from django.db import models from evennia.server.manager import ServerConfigManager diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index c9953346a1..67fbd1d1e7 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -4,6 +4,7 @@ communication to the AMP clients connecting to it (by default these are the Evennia Server and the evennia launcher). """ + import os import sys from subprocess import STDOUT, Popen @@ -36,7 +37,6 @@ def getenv(): class AMPServerFactory(protocol.ServerFactory): - """ This factory creates AMP Server connection. This acts as the 'Portal'-side communication to the 'Server' process. diff --git a/evennia/server/portal/discord.py b/evennia/server/portal/discord.py index e38c08aea9..0e03ac6481 100644 --- a/evennia/server/portal/discord.py +++ b/evennia/server/portal/discord.py @@ -8,6 +8,7 @@ discord bot set up via https://discord.com/developers/applications with the MESSAGE CONTENT toggle switched on, and your bot token added to `server/conf/secret_settings.py` as your DISCORD_BOT_TOKEN """ + import json import os from io import BytesIO diff --git a/evennia/server/portal/mccp.py b/evennia/server/portal/mccp.py index 43390b19bc..ba748e0010 100644 --- a/evennia/server/portal/mccp.py +++ b/evennia/server/portal/mccp.py @@ -14,6 +14,7 @@ terribly slow connection. This protocol is implemented by the telnet protocol importing mccp_compress and calling it from its write methods. """ + import zlib # negotiations for v1 and v2 of the protocol diff --git a/evennia/server/portal/mssp.py b/evennia/server/portal/mssp.py index 3f57566945..03714cf9da 100644 --- a/evennia/server/portal/mssp.py +++ b/evennia/server/portal/mssp.py @@ -10,6 +10,7 @@ active players and so on. """ + from django.conf import settings from evennia.utils import utils diff --git a/evennia/server/portal/mxp.py b/evennia/server/portal/mxp.py index af3e5d1e0d..a445168c97 100644 --- a/evennia/server/portal/mxp.py +++ b/evennia/server/portal/mxp.py @@ -13,6 +13,7 @@ http://www.mushclient.com/mushclient/mxp.htm http://www.gammon.com.au/mushclient/addingservermxp.htm """ + import re from django.conf import settings diff --git a/evennia/server/portal/naws.py b/evennia/server/portal/naws.py index ab7e892712..b4498e71ea 100644 --- a/evennia/server/portal/naws.py +++ b/evennia/server/portal/naws.py @@ -9,6 +9,7 @@ NAWS allows telnet clients to report their current window size to the client and update it when the size changes """ + from codecs import encode as codecs_encode from django.conf import settings diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index b1bf532cf1..99a9e904ff 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -7,6 +7,7 @@ sets up all the networking features. (this is done automatically by game/evennia.py). """ + import os import sys diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 8a5ed53ce2..cb118893c1 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -3,7 +3,6 @@ Sessionhandler for portal sessions. """ - import time from collections import deque, namedtuple diff --git a/evennia/server/portal/rss.py b/evennia/server/portal/rss.py index fdd5b32d2b..1499a89f17 100644 --- a/evennia/server/portal/rss.py +++ b/evennia/server/portal/rss.py @@ -5,6 +5,7 @@ This connects an RSS feed to an in-game Evennia channel, sending messages to the channel whenever the feed updates. """ + from django.conf import settings from twisted.internet import task, threads diff --git a/evennia/server/portal/ssh.py b/evennia/server/portal/ssh.py index 0a8e9637e8..294275f198 100644 --- a/evennia/server/portal/ssh.py +++ b/evennia/server/portal/ssh.py @@ -34,9 +34,6 @@ except ImportError: raise ImportError(_SSH_IMPORT_ERROR) from django.conf import settings -from evennia.accounts.models import AccountDB -from evennia.utils import ansi -from evennia.utils.utils import class_from_module, to_str from twisted.conch import interfaces as iconch from twisted.conch.insults import insults from twisted.conch.manhole import Manhole, recvline @@ -46,6 +43,10 @@ from twisted.conch.ssh.userauth import SSHUserAuthServer from twisted.internet import defer, protocol from twisted.python import components +from evennia.accounts.models import AccountDB +from evennia.utils import ansi +from evennia.utils.utils import class_from_module, to_str + _RE_N = re.compile(r"\|n$") _RE_SCREENREADER_REGEX = re.compile( r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE diff --git a/evennia/server/portal/ssl.py b/evennia/server/portal/ssl.py index 032f48d401..0fca77cb69 100644 --- a/evennia/server/portal/ssl.py +++ b/evennia/server/portal/ssl.py @@ -3,6 +3,7 @@ This is a simple context factory for auto-creating SSL keys and certificates. """ + import os import sys diff --git a/evennia/server/portal/suppress_ga.py b/evennia/server/portal/suppress_ga.py index 694f09b73a..cadf007fa9 100644 --- a/evennia/server/portal/suppress_ga.py +++ b/evennia/server/portal/suppress_ga.py @@ -39,9 +39,9 @@ class SuppressGA: self.protocol = protocol self.protocol.protocol_flags["NOGOAHEAD"] = True - self.protocol.protocol_flags[ - "NOPROMPTGOAHEAD" - ] = True # Used to send a GA after a prompt line only, set in TTYPE (per client) + self.protocol.protocol_flags["NOPROMPTGOAHEAD"] = ( + True # Used to send a GA after a prompt line only, set in TTYPE (per client) + ) # tell the client that we prefer to suppress GA ... self.protocol.will(SUPPRESS_GA).addCallbacks(self.will_suppress_ga, self.wont_suppress_ga) diff --git a/evennia/server/portal/telnet_oob.py b/evennia/server/portal/telnet_oob.py index 38012e9170..6fe68ff7a1 100644 --- a/evennia/server/portal/telnet_oob.py +++ b/evennia/server/portal/telnet_oob.py @@ -23,6 +23,7 @@ This implements the following telnet OOB communication protocols: ---- """ + import json import re diff --git a/evennia/server/portal/telnet_ssl.py b/evennia/server/portal/telnet_ssl.py index 46717c0a61..6bf6a0597d 100644 --- a/evennia/server/portal/telnet_ssl.py +++ b/evennia/server/portal/telnet_ssl.py @@ -7,6 +7,7 @@ when starting and will warn if this was not possible. These will appear as files ssl.cert in mygame/server/. """ + import os try: diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py index b74c3eb361..1f35d473ee 100644 --- a/evennia/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -14,6 +14,7 @@ The most common inputfunc is "text", which takes just the text input from the command line and interprets it as an Evennia Command: `["text", ["look"], {}]` """ + import html import json import re diff --git a/evennia/server/portal/webclient_ajax.py b/evennia/server/portal/webclient_ajax.py index b3fd54357f..2c43017c57 100644 --- a/evennia/server/portal/webclient_ajax.py +++ b/evennia/server/portal/webclient_ajax.py @@ -17,6 +17,7 @@ http://localhost:4001/webclient.) to sessions connected over the webclient. """ + import html import json import re diff --git a/evennia/server/profiling/dummyrunner.py b/evennia/server/profiling/dummyrunner.py index da7c9f43b2..00d3ea9391 100644 --- a/evennia/server/profiling/dummyrunner.py +++ b/evennia/server/profiling/dummyrunner.py @@ -31,7 +31,6 @@ for instructions on how to define this module. """ - import random import sys import time diff --git a/evennia/server/profiling/dummyrunner_settings.py b/evennia/server/profiling/dummyrunner_settings.py index 1dc63b0410..aa70874f50 100644 --- a/evennia/server/profiling/dummyrunner_settings.py +++ b/evennia/server/profiling/dummyrunner_settings.py @@ -57,6 +57,7 @@ commands (such as creating an account and logging in). ---- """ + import random import string diff --git a/evennia/server/server.py b/evennia/server/server.py index 88adad8a6d..3d22808266 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -7,6 +7,7 @@ the networking features. (this is done automatically by evennia/server/server_runner.py). """ + import os import sys diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 5177f30f5d..e1573b37c0 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -6,6 +6,7 @@ connection actually happens (so it's the same for telnet, web, ssh etc). It is stored on the Server side (as opposed to protocol-specific sessions which are stored on the Portal side) """ + import time from django.conf import settings diff --git a/evennia/server/service.py b/evennia/server/service.py index bf4a14d598..c1c57e5d56 100644 --- a/evennia/server/service.py +++ b/evennia/server/service.py @@ -2,6 +2,7 @@ This module contains the main EvenniaService class, which is the very core of the Evennia server. It is instantiated by the evennia/server/server.py module. """ + import importlib import time import traceback @@ -330,12 +331,13 @@ class EvenniaServerService(MultiService): (i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches ): # update the database - self.info_dict[ - "info" - ] = " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % ( - settings_names[i], - prev, - curr, + self.info_dict["info"] = ( + " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." + % ( + settings_names[i], + prev, + curr, + ) ) if i == 0: evennia.ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update( diff --git a/evennia/server/session.py b/evennia/server/session.py index c70c75234d..8c1a518796 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -3,6 +3,7 @@ This module defines a generic session class. All connection instances (both on Portal and Server side) should inherit from this class. """ + import time from django.conf import settings diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 697e1aeb56..3d3f1c6e6f 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -12,6 +12,7 @@ There are two similar but separate stores of sessions: handle network communication but holds no game info. """ + import time from codecs import decode as codecs_decode diff --git a/evennia/server/signals.py b/evennia/server/signals.py index 7c3f89221c..170d93b9a1 100644 --- a/evennia/server/signals.py +++ b/evennia/server/signals.py @@ -20,6 +20,7 @@ This is used on top of hooks to make certain features easier to add to contribs without necessitating a full takeover of hooks that may be in high demand. """ + from collections import defaultdict from django.dispatch import Signal diff --git a/evennia/server/tests/test_server.py b/evennia/server/tests/test_server.py index f1625f7d9e..c6518dd438 100644 --- a/evennia/server/tests/test_server.py +++ b/evennia/server/tests/test_server.py @@ -2,6 +2,7 @@ Test the main server component """ + from unittest import TestCase from django.test import override_settings @@ -25,13 +26,15 @@ class TestServer(TestCase): @override_settings(IDMAPPER_CACHE_MAXSIZE=1000) def test__server_maintenance_reset(self): - with patch.object(self.server, "_flush_cache", new=MagicMock()) as mockflush, patch.object( - evennia, "ServerConfig", new=MagicMock() - ) as mockconf, patch.multiple( - "evennia.server.service", - LoopingCall=DEFAULT, - connection=DEFAULT, - ) as mocks: + with ( + patch.object(self.server, "_flush_cache", new=MagicMock()) as mockflush, + patch.object(evennia, "ServerConfig", new=MagicMock()) as mockconf, + patch.multiple( + "evennia.server.service", + LoopingCall=DEFAULT, + connection=DEFAULT, + ) as mocks, + ): self.server.maintenance_count = 0 mocks["connection"].close = MagicMock() @@ -43,15 +46,15 @@ class TestServer(TestCase): @override_settings(IDMAPPER_CACHE_MAXSIZE=1000) def test__server_maintenance_flush(self): - with patch.multiple( - "evennia.server.service", - LoopingCall=DEFAULT, - connection=DEFAULT, - ) as mocks, patch.object( - evennia, "ServerConfig", new=MagicMock() - ) as mockconf, patch.object( - self.server, "_flush_cache", new=MagicMock() - ) as mockflush: + with ( + patch.multiple( + "evennia.server.service", + LoopingCall=DEFAULT, + connection=DEFAULT, + ) as mocks, + patch.object(evennia, "ServerConfig", new=MagicMock()) as mockconf, + patch.object(self.server, "_flush_cache", new=MagicMock()) as mockflush, + ): mocks["connection"].close = MagicMock() mockconf.objects.conf = MagicMock(return_value=100) self.server.maintenance_count = 5 - 1 @@ -61,11 +64,14 @@ class TestServer(TestCase): @override_settings(IDMAPPER_CACHE_MAXSIZE=1000) def test__server_maintenance_close_connection(self): - with patch.multiple( - "evennia.server.service", - LoopingCall=DEFAULT, - connection=DEFAULT, - ) as mocks, patch.object(evennia, "ServerConfig", new=MagicMock()) as mockconf: + with ( + patch.multiple( + "evennia.server.service", + LoopingCall=DEFAULT, + connection=DEFAULT, + ) as mocks, + patch.object(evennia, "ServerConfig", new=MagicMock()) as mockconf, + ): self.server._flush_cache = MagicMock() self.server.maintenance_count = (60 * 7) - 1 self.server._last_server_time_snapshot = 0 @@ -76,16 +82,16 @@ class TestServer(TestCase): @override_settings(IDLE_TIMEOUT=10) def test__server_maintenance_idle_time(self): - with patch.multiple( - "evennia.server.service", - LoopingCall=DEFAULT, - connection=DEFAULT, - time=DEFAULT, - ) as mocks, patch.object( - evennia, "ServerConfig", new=MagicMock() - ) as mockconf, patch.object( - evennia, "SESSION_HANDLER", new=MagicMock() - ) as mocksess: + with ( + patch.multiple( + "evennia.server.service", + LoopingCall=DEFAULT, + connection=DEFAULT, + time=DEFAULT, + ) as mocks, + patch.object(evennia, "ServerConfig", new=MagicMock()) as mockconf, + patch.object(evennia, "SESSION_HANDLER", new=MagicMock()) as mocksess, + ): self.server.maintenance_count = (3600 * 7) - 1 self.server._last_server_time_snapshot = 0 sess1 = MagicMock() @@ -114,13 +120,13 @@ class TestServer(TestCase): mocksess.disconnect.assert_has_calls(calls, any_order=True) def test_update_defaults(self): - with patch.object(evennia, "ObjectDB", new=MagicMock()) as mockobj, patch.object( - evennia, "AccountDB", new=MagicMock() - ) as mockacc, patch.object(evennia, "ScriptDB", new=MagicMock()) as mockscr, patch.object( - evennia, "ChannelDB", new=MagicMock() - ) as mockchan, patch.object( - evennia, "ServerConfig", new=MagicMock() - ) as mockconf: + with ( + patch.object(evennia, "ObjectDB", new=MagicMock()) as mockobj, + patch.object(evennia, "AccountDB", new=MagicMock()) as mockacc, + patch.object(evennia, "ScriptDB", new=MagicMock()) as mockscr, + patch.object(evennia, "ChannelDB", new=MagicMock()) as mockchan, + patch.object(evennia, "ServerConfig", new=MagicMock()) as mockconf, + ): for m in (mockscr, mockobj, mockacc, mockchan): m.objects.filter = MagicMock() @@ -220,9 +226,10 @@ class TestInitHooks(TestCase): @override_settings(TEST_ENVIRONMENT=True) def test_run_init_hooks(self): - with patch.object( - self.server, "at_server_reload_start", new=MagicMock() - ) as reload, patch.object(self.server, "at_server_cold_start", new=MagicMock()) as cold: + with ( + patch.object(self.server, "at_server_reload_start", new=MagicMock()) as reload, + patch.object(self.server, "at_server_cold_start", new=MagicMock()) as cold, + ): self.server.run_init_hooks("reload") self.server.run_init_hooks("reset") self.server.run_init_hooks("shutdown") diff --git a/evennia/server/tests/testrunner.py b/evennia/server/tests/testrunner.py index 9652c5a9d0..31aefb7666 100644 --- a/evennia/server/tests/testrunner.py +++ b/evennia/server/tests/testrunner.py @@ -5,6 +5,7 @@ all over the code base and runs them. Runs as part of the Evennia's test suite with 'evennia test evennia" """ + from django.test.runner import DiscoverRunner diff --git a/evennia/server/webserver.py b/evennia/server/webserver.py index 31e05bd0a9..db48e459d4 100644 --- a/evennia/server/webserver.py +++ b/evennia/server/webserver.py @@ -12,6 +12,7 @@ a great example/aid on how to do this.) """ + import urllib.parse from urllib.parse import quote as urlquote diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 2d53d524cc..50a27ce1b5 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -12,6 +12,7 @@ value - which may change as Evennia is developed. This way you can always be sure of what you have changed and what is default behaviour. """ + import os import sys diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index ac274614b0..5aedc03262 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -8,6 +8,7 @@ which is a non-db version of Attributes. """ + import fnmatch import re from collections import defaultdict diff --git a/evennia/typeclasses/managers.py b/evennia/typeclasses/managers.py index fee012eaed..7a48e94866 100644 --- a/evennia/typeclasses/managers.py +++ b/evennia/typeclasses/managers.py @@ -4,6 +4,7 @@ abstract models in dbobjects.py (and which are thus shared by all Attributes and TypedObjects). """ + import shlex from django.db.models import Count, ExpressionWrapper, F, FloatField, Q diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index 9758b1f8ce..8f39c66227 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -25,6 +25,7 @@ This module also contains the Managers for the respective models; inherit from these to create custom managers. """ + from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist diff --git a/evennia/typeclasses/tags.py b/evennia/typeclasses/tags.py index a44ed99332..b396419a4b 100644 --- a/evennia/typeclasses/tags.py +++ b/evennia/typeclasses/tags.py @@ -9,6 +9,7 @@ used for storing Aliases and Permissions. This module contains the respective handlers. """ + from collections import defaultdict from django.conf import settings diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 2cc03f4aec..60c88c4be8 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -61,6 +61,7 @@ Xterm256 greyscale: ---- """ + import functools import re from collections import OrderedDict diff --git a/evennia/utils/batchprocessors.py b/evennia/utils/batchprocessors.py index b8c0289bdb..d59ce339a3 100644 --- a/evennia/utils/batchprocessors.py +++ b/evennia/utils/batchprocessors.py @@ -167,6 +167,7 @@ made available in the script namespace. script = create.create_script() """ + import codecs import re import sys diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py index 70b7b43f7d..26e4f0ca69 100644 --- a/evennia/utils/containers.py +++ b/evennia/utils/containers.py @@ -10,7 +10,6 @@ evennia.OPTION_CLASSES """ - from pickle import dumps from django.conf import settings diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index 2d59f9b803..cc79eed89e 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -18,6 +18,7 @@ in-situ, e.g `obj.db.mynestedlist[3][5] = 3` would never be saved and be out of sync with the database. """ + from collections import OrderedDict, defaultdict, deque from collections.abc import MutableMapping, MutableSequence, MutableSet from functools import update_wrapper diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index f0f2168fca..04578c8be8 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -39,10 +39,12 @@ The editor can also be used to format Python code and be made to survive a reload. See the `EvEditor` class for more details. """ + import re from django.conf import settings from django.utils.translation import gettext as _ + from evennia import CmdSet from evennia.commands import cmdhandler from evennia.utils import dedent, fill, is_iter, justify, logger, to_str, utils @@ -305,7 +307,11 @@ class CmdEditorBase(_COMMAND_DEFAULT_CLASS): if arglist and arglist[0].count(":") == 1: part1, part2 = arglist[0].split(":") lstart = min(max(1, int(part1)), nlines) - 1 if utils.value_is_integer(part1) else 0 - lend = min(max(lstart + 1, int(part2)), nlines) if utils.value_is_integer(part2) else nlines + lend = ( + min(max(lstart + 1, int(part2)), nlines) + if utils.value_is_integer(part2) + else nlines + ) linerange = True elif arglist and arglist[0].isdigit(): lstart = min(max(0, int(arglist[0]) - 1), nlines) @@ -468,7 +474,7 @@ class CmdEditorGroup(CmdEditorBase): linebuffer = self.linebuffer lstart, lend = self.lstart, self.lend # preserve the cmdname including case (otherwise uu and UU would be the same) - cmd = self.raw_string[:len(self.cmdstring)] + cmd = self.raw_string[: len(self.cmdstring)] echo_mode = self.editor._echo_mode if cmd == ":": diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index 9417767467..2c434badcf 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -43,6 +43,7 @@ The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}` --- """ + import dataclasses import inspect import random diff --git a/evennia/utils/idmapper/manager.py b/evennia/utils/idmapper/manager.py index c60c6d89a7..7e6f0fa31c 100644 --- a/evennia/utils/idmapper/manager.py +++ b/evennia/utils/idmapper/manager.py @@ -1,6 +1,7 @@ """ IDmapper extension to the default manager. """ + from django.db.models.manager import Manager diff --git a/evennia/utils/logger.py b/evennia/utils/logger.py index 19b5e1214d..b0b4ab9604 100644 --- a/evennia/utils/logger.py +++ b/evennia/utils/logger.py @@ -13,7 +13,6 @@ log_typemsg(). This is for historical, back-compatible reasons. """ - import os import time from datetime import datetime diff --git a/evennia/utils/test_resources.py b/evennia/utils/test_resources.py index 44802a8325..9aee64842a 100644 --- a/evennia/utils/test_resources.py +++ b/evennia/utils/test_resources.py @@ -22,25 +22,32 @@ Other: helper. Used by the command-test classes, but can be used for making a customt test class. """ + import re import sys import types -import evennia from django.conf import settings from django.test import TestCase, override_settings +from mock import MagicMock, Mock, patch +from twisted.internet.defer import Deferred + +import evennia from evennia import settings_default from evennia.accounts.accounts import DefaultAccount from evennia.commands.command import InterruptCommand from evennia.commands.default.muxcommand import MuxCommand -from evennia.objects.objects import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom +from evennia.objects.objects import ( + DefaultCharacter, + DefaultExit, + DefaultObject, + DefaultRoom, +) from evennia.scripts.scripts import DefaultScript from evennia.server.serversession import ServerSession from evennia.utils import ansi, create from evennia.utils.idmapper.models import flush_cache from evennia.utils.utils import all_from_module, to_str -from mock import MagicMock, Mock, patch -from twisted.internet.defer import Deferred _RE_STRIP_EVMENU = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE) diff --git a/evennia/utils/tests/test_eveditor.py b/evennia/utils/tests/test_eveditor.py index 97bda62630..7fc50861c5 100644 --- a/evennia/utils/tests/test_eveditor.py +++ b/evennia/utils/tests/test_eveditor.py @@ -23,7 +23,7 @@ class TestEvEditor(BaseEvenniaCommandTest): self.call(eveditor.CmdLineInput(), "line 5", raw_string="line 5", msg="05line 5") self.call( eveditor.CmdEditorGroup(), - "", # list whole buffer + "", # list whole buffer raw_string=":", msg="Line Editor []\n01line 1\n02line 2\n" "03line 3\n04line 4\n05line 5\n" @@ -31,7 +31,7 @@ class TestEvEditor(BaseEvenniaCommandTest): ) self.call( eveditor.CmdEditorGroup(), - ":", # list empty range + ":", # list empty range raw_string=":", msg="Line Editor []\n01line 1\n02line 2\n" "03line 3\n04line 4\n05line 5\n" @@ -39,7 +39,7 @@ class TestEvEditor(BaseEvenniaCommandTest): ) self.call( eveditor.CmdEditorGroup(), - ":4", # list from start to line 4 + ":4", # list from start to line 4 raw_string=":", msg="Line Editor []\n01line 1\n02line 2\n" "03line 3\n04line 4\n" @@ -47,7 +47,7 @@ class TestEvEditor(BaseEvenniaCommandTest): ) self.call( eveditor.CmdEditorGroup(), - "2:", # list from line 2 to end + "2:", # list from line 2 to end raw_string=":", msg="Line Editor []\n02line 2\n03line 3\n" "04line 4\n05line 5\n" @@ -55,7 +55,7 @@ class TestEvEditor(BaseEvenniaCommandTest): ) self.call( eveditor.CmdEditorGroup(), - "-10:10", # try to list invalid range (too large) + "-10:10", # try to list invalid range (too large) raw_string=":", msg="Line Editor []\n01line 1\n02line 2\n" "03line 3\n04line 4\n05line 5\n" @@ -63,10 +63,9 @@ class TestEvEditor(BaseEvenniaCommandTest): ) self.call( eveditor.CmdEditorGroup(), - "3:1", # try to list invalid range (reversed) + "3:1", # try to list invalid range (reversed) raw_string=":", - msg="Line Editor []\n03line 3\n" - "[l:01 w:002 c:0006](:h for help)", + msg="Line Editor []\n03line 3\n" "[l:01 w:002 c:0006](:h for help)", ) def test_eveditor_view_cmd(self): @@ -184,7 +183,10 @@ class TestEvEditor(BaseEvenniaCommandTest): ) self.call( - eveditor.CmdEditorGroup(), "3", raw_string=":x", msg="Line 3, [' test line'] cut." # cut + eveditor.CmdEditorGroup(), + "3", + raw_string=":x", + msg="Line 3, [' test line'] cut.", # cut ) self.call( diff --git a/evennia/utils/tests/test_evform.py b/evennia/utils/tests/test_evform.py index 4d879cfafb..1d60508eb6 100644 --- a/evennia/utils/tests/test_evform.py +++ b/evennia/utils/tests/test_evform.py @@ -2,6 +2,7 @@ Unit tests for the EvForm text form generator """ + from unittest import skip from django.test import TestCase diff --git a/evennia/utils/tests/test_tagparsing.py b/evennia/utils/tests/test_tagparsing.py index ac1fa3069a..fc062b1c43 100644 --- a/evennia/utils/tests/test_tagparsing.py +++ b/evennia/utils/tests/test_tagparsing.py @@ -2,6 +2,7 @@ Unit tests for all sorts of inline text-tag parsing, like ANSI, html conversion, inlinefuncs etc """ + import re from django.test import TestCase, override_settings diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 17f6c4144a..9852802e72 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -11,11 +11,12 @@ from datetime import datetime, timedelta import mock from django.test import TestCase +from parameterized import parameterized +from twisted.internet import task + from evennia.utils import utils from evennia.utils.ansi import ANSIString from evennia.utils.test_resources import BaseEvenniaTest -from parameterized import parameterized -from twisted.internet import task class TestIsIter(TestCase): @@ -58,15 +59,26 @@ class TestCompressWhitespace(TestCase): # No text, return no text self.assertEqual("", utils.compress_whitespace("")) # If no whitespace is exceeded, should return the same - self.assertEqual("One line\nTwo spaces", utils.compress_whitespace("One line\nTwo spaces")) + self.assertEqual( + "One line\nTwo spaces", utils.compress_whitespace("One line\nTwo spaces") + ) # Extra newlines are removed - self.assertEqual("First line\nSecond line", utils.compress_whitespace("First line\n\nSecond line")) + self.assertEqual( + "First line\nSecond line", utils.compress_whitespace("First line\n\nSecond line") + ) # Extra spaces are removed self.assertEqual("Too many spaces", utils.compress_whitespace("Too many spaces")) # "Invisible" extra lines with whitespace are removed - self.assertEqual("First line\nSecond line", utils.compress_whitespace("First line\n \n \nSecond line")) + self.assertEqual( + "First line\nSecond line", utils.compress_whitespace("First line\n \n \nSecond line") + ) # Max kwargs are respected - self.assertEqual("First line\n\nSecond line", utils.compress_whitespace("First line\n\nSecond line", max_spacing=1, max_linebreaks=2)) + self.assertEqual( + "First line\n\nSecond line", + utils.compress_whitespace( + "First line\n\nSecond line", max_spacing=1, max_linebreaks=2 + ), + ) def test_preserve_indents(self): """Ensure that indentation spacing is preserved.""" @@ -78,6 +90,7 @@ Hanging Indents # since there is no doubled-up spacing besides indents, input should equal output self.assertEqual(indented, utils.compress_whitespace(indented)) + class TestListToString(TestCase): """ Default function header from time.py: diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 99e5170651..377d31958d 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -28,7 +28,6 @@ from os.path import join as osjoin from string import punctuation from unicodedata import east_asian_width -import evennia from django.apps import apps from django.conf import settings from django.core.exceptions import ValidationError as DjangoValidationError @@ -36,12 +35,14 @@ from django.core.validators import validate_email as django_validate_email from django.utils import timezone from django.utils.html import strip_tags from django.utils.translation import gettext as _ -from evennia.utils import logger from simpleeval import simple_eval from twisted.internet import reactor, threads from twisted.internet.defer import returnValue # noqa - used as import target from twisted.internet.task import deferLater +import evennia +from evennia.utils import logger + _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE _EVENNIA_DIR = settings.EVENNIA_DIR _GAME_DIR = settings.GAME_DIR @@ -474,6 +475,7 @@ iter_to_string = iter_to_str re_empty = re.compile("\n\s*\n") + def compress_whitespace(text, max_linebreaks=1, max_spacing=2): """ Removes extra sequential whitespace in a block of text. This will also remove any trailing @@ -492,9 +494,9 @@ def compress_whitespace(text, max_linebreaks=1, max_spacing=2): # this allows the blank-line compression to eliminate them if needed text = re_empty.sub("\n\n", text) # replace groups of extra spaces with the maximum number of spaces - text = re.sub(f"(?<=\S) {{{max_spacing},}}", " "*max_spacing, text) + text = re.sub(f"(?<=\S) {{{max_spacing},}}", " " * max_spacing, text) # replace groups of extra newlines with the maximum number of newlines - text = re.sub(f"\n{{{max_linebreaks},}}", "\n"*max_linebreaks, text) + text = re.sub(f"\n{{{max_linebreaks},}}", "\n" * max_linebreaks, text) return text @@ -2402,20 +2404,18 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs): # result is a typeclassed entity where `.aliases` is an AliasHandler. aliases = result.aliases.all(return_objs=True) # remove pluralization aliases - aliases = [ - alias.db_key - for alias in aliases - if alias.db_category != "plural_key" - ] + aliases = [alias.db_key for alias in aliases if alias.db_category != "plural_key"] else: # result is likely a Command, where `.aliases` is a list of strings. aliases = result.aliases error += _MULTIMATCH_TEMPLATE.format( number=num + 1, - name=result.get_display_name(caller) - if hasattr(result, "get_display_name") - else query, + name=( + result.get_display_name(caller) + if hasattr(result, "get_display_name") + else query + ), aliases=" [{alias}]".format(alias=";".join(aliases)) if aliases else "", info=result.get_extra_info(caller), ) diff --git a/evennia/utils/verb_conjugation/pronouns.py b/evennia/utils/verb_conjugation/pronouns.py index f7a293fda1..c3316e15a9 100644 --- a/evennia/utils/verb_conjugation/pronouns.py +++ b/evennia/utils/verb_conjugation/pronouns.py @@ -25,6 +25,7 @@ viewpoint/pronouns Subject Object Possessive Possessive Reflexive 3rd person plural they them their theirs themselves ==================== ======= ======== ========== ========== =========== """ + from evennia.utils.utils import copy_word_case, is_iter DEFAULT_PRONOUN_TYPE = "subject pronoun" diff --git a/evennia/web/admin/help.py b/evennia/web/admin/help.py index 4417142107..9c64630fca 100644 --- a/evennia/web/admin/help.py +++ b/evennia/web/admin/help.py @@ -1,6 +1,7 @@ """ This defines how to edit help entries in Admin. """ + from django import forms from django.contrib import admin diff --git a/evennia/web/admin/tags.py b/evennia/web/admin/tags.py index ebfe2cff56..fd7395f686 100644 --- a/evennia/web/admin/tags.py +++ b/evennia/web/admin/tags.py @@ -3,7 +3,6 @@ Tag admin """ - import traceback from datetime import datetime diff --git a/evennia/web/admin/urls.py b/evennia/web/admin/urls.py index 0a59fcf752..a6dfcf64f6 100644 --- a/evennia/web/admin/urls.py +++ b/evennia/web/admin/urls.py @@ -4,6 +4,7 @@ Rerouting admin frontpage to evennia version. These patterns are all under the admin/* namespace. """ + from django.conf import settings from django.contrib import admin from django.urls import include, path diff --git a/evennia/web/api/filters.py b/evennia/web/api/filters.py index 29ce027dc0..dedb874ad1 100644 --- a/evennia/web/api/filters.py +++ b/evennia/web/api/filters.py @@ -7,6 +7,7 @@ documentation specifically regarding DRF integration. https://django-filter.readthedocs.io/en/latest/guide/rest_framework.html """ + from typing import Union from django.db.models import Q @@ -138,7 +139,6 @@ class ScriptDBFilterSet(BaseTypeclassFilterSet): class HelpFilterSet(FilterSet): - """ Filter for help entries diff --git a/evennia/web/api/permissions.py b/evennia/web/api/permissions.py index 536cb9a552..901f485c55 100644 --- a/evennia/web/api/permissions.py +++ b/evennia/web/api/permissions.py @@ -3,7 +3,6 @@ Sets up an api-access permission check using the in-game permission hierarchy. """ - from django.conf import settings from rest_framework import permissions diff --git a/evennia/web/api/tests.py b/evennia/web/api/tests.py index 4af37b2603..94006dd641 100644 --- a/evennia/web/api/tests.py +++ b/evennia/web/api/tests.py @@ -2,6 +2,7 @@ Tests for the REST API. """ + from collections import namedtuple from django.core.exceptions import ObjectDoesNotExist diff --git a/evennia/web/api/views.py b/evennia/web/api/views.py index eb28073d97..51de2f0321 100644 --- a/evennia/web/api/views.py +++ b/evennia/web/api/views.py @@ -4,6 +4,7 @@ Rest Framework provides collections called 'ViewSets', which can generate a number of views for the common CRUD operations. """ + from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status from rest_framework.decorators import action diff --git a/evennia/web/utils/general_context.py b/evennia/web/utils/general_context.py index f8731a633d..57abb1843f 100644 --- a/evennia/web/utils/general_context.py +++ b/evennia/web/utils/general_context.py @@ -7,10 +7,10 @@ TEMPLATES["OPTIONS"]["context_processors"] list. """ - import os from django.conf import settings + from evennia.utils.utils import get_evennia_version # Setup lists of the most relevant apps so diff --git a/evennia/web/webclient/urls.py b/evennia/web/webclient/urls.py index 364a72feb5..39a7edfdbf 100644 --- a/evennia/web/webclient/urls.py +++ b/evennia/web/webclient/urls.py @@ -2,6 +2,7 @@ This structures the (simple) structure of the webpage 'application'. """ + from django.urls import path from . import views diff --git a/evennia/web/website/urls.py b/evennia/web/website/urls.py index 2777424eeb..92c01e2474 100644 --- a/evennia/web/website/urls.py +++ b/evennia/web/website/urls.py @@ -2,6 +2,7 @@ This redirects to website sub-pages. """ + from django.conf import settings from django.contrib import admin from django.urls import include, path diff --git a/evennia/web/website/views/accounts.py b/evennia/web/website/views/accounts.py index 5ecb5128ee..8cfd33a550 100644 --- a/evennia/web/website/views/accounts.py +++ b/evennia/web/website/views/accounts.py @@ -3,7 +3,6 @@ Views for managing accounts. """ - from django.conf import settings from django.contrib import messages from django.http import HttpResponseRedirect diff --git a/evennia/web/website/views/characters.py b/evennia/web/website/views/characters.py index f9e4142b1d..113710f2a5 100644 --- a/evennia/web/website/views/characters.py +++ b/evennia/web/website/views/characters.py @@ -14,12 +14,17 @@ from django.utils.encoding import iri_to_uri from django.utils.http import url_has_allowed_host_and_scheme from django.views.generic import ListView from django.views.generic.base import RedirectView + from evennia.utils import class_from_module from evennia.web.website import forms from .mixins import TypeclassMixin -from .objects import (ObjectCreateView, ObjectDeleteView, ObjectDetailView, - ObjectUpdateView) +from .objects import ( + ObjectCreateView, + ObjectDeleteView, + ObjectDetailView, + ObjectUpdateView, +) class CharacterMixin(TypeclassMixin): @@ -124,9 +129,11 @@ class CharacterPuppetView(LoginRequiredMixin, CharacterMixin, RedirectView, Obje # since next_page is untrusted input from the user, we need to check it's safe to next_page = iri_to_uri(next_page) - if not url_has_allowed_host_and_scheme(url=next_page, - allowed_hosts={self.request.get_host()}, - require_https=self.request.is_secure()): + if not url_has_allowed_host_and_scheme( + url=next_page, + allowed_hosts={self.request.get_host()}, + require_https=self.request.is_secure(), + ): next_page = self.success_url if char: @@ -140,7 +147,6 @@ class CharacterPuppetView(LoginRequiredMixin, CharacterMixin, RedirectView, Obje self.request.session["puppet"] = None messages.error(self.request, "You cannot become '%s'." % char) - return next_page diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index 98ef5cbcb3..c84feb20b1 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -4,6 +4,7 @@ Views to manipulate help entries. Multi entry object type supported added by DaveWithTheNiceHat 2021 Pull Request #2429 """ + from django.conf import settings from django.http import HttpResponseBadRequest from django.utils.text import slugify diff --git a/evennia/web/website/views/mixins.py b/evennia/web/website/views/mixins.py index 8cecd5b241..496ebab64e 100644 --- a/evennia/web/website/views/mixins.py +++ b/evennia/web/website/views/mixins.py @@ -2,6 +2,7 @@ These are mixins for class-based views, granting functionality. """ + from django.views.generic import DetailView from django.views.generic.edit import CreateView, DeleteView, UpdateView From b5239e18c1ab71cb6889cc888f7bfe2076f36174 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 19 Feb 2024 11:44:47 -0700 Subject: [PATCH 40/68] fix cmdget help lock --- evennia/commands/default/general.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index e46fbe2cf5..66c5afc4fd 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -387,13 +387,12 @@ class CmdGet(COMMAND_DEFAULT_CLASS): Usage: get - Picks up an object from your location and puts it in - your inventory. + Picks up an object from your location and puts it in your inventory. """ key = "get" aliases = "grab" - locks = "cmd:all();view:perm(Developer);read:perm(Developer)" + locks = "cmd:all()" arg_regex = r"\s|$" def func(self): From 829f32f5736bca5fdcda666b99e97a7a6b878245 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 19 Feb 2024 13:20:05 -0700 Subject: [PATCH 41/68] add object stacking to get/drop/give --- evennia/commands/default/general.py | 176 ++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 50 deletions(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 66c5afc4fd..8c031ac605 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -395,39 +395,64 @@ class CmdGet(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" + def parse(self): + super().parse() + self.number = 0 + if self.args: + # check for numbering + count, *args = self.args.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.args = args[0] + def func(self): """implements the command.""" caller = self.caller if not self.args: - caller.msg("Get what?") + self.msg("Get what?") return - obj = caller.search(self.args, location=caller.location) - if not obj: + objs = caller.search(self.args, location=caller.location, stacked=self.number) + if not objs: return - if caller == obj: - caller.msg("You can't get yourself.") - return - if not obj.access(caller, "get"): - if obj.db.get_err_msg: - caller.msg(obj.db.get_err_msg) - else: - caller.msg("You can't get that.") + # the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list + # NOTE: this behavior may be a bug, see issue #3432 + objs = utils.make_iter(objs) + + if len(objs) == 1 and caller == objs[0]: + self.msg("You can't get yourself.") return - # calling at_pre_get hook method - if not obj.at_pre_get(caller): - return + # if we aren't allowed to get any of the objects, cancel the get + for obj in objs: + # check the locks + if not obj.access(caller, "get"): + if obj.db.get_err_msg: + self.msg(obj.db.get_err_msg) + else: + self.msg("You can't get that.") + return + # calling at_pre_get hook method + if not obj.at_pre_get(caller): + return - success = obj.move_to(caller, quiet=True, move_type="get") - if not success: - caller.msg("This can't be picked up.") + moved = [] + # attempt to move all of the objects + for obj in objs: + if obj.move_to(caller, quiet=True, move_type="get"): + moved.append(obj) + # calling at_get hook method + obj.at_get(caller) + + if not moved: + # none of the objects were successfully moved + self.msg("That can't be picked up.") else: - singular, _ = obj.get_numbered_name(1, caller) - caller.location.msg_contents(f"$You() $conj(pick) up {singular}.", from_obj=caller) - # calling at_get hook method - obj.at_get(caller) + singular, plural = moved[0].get_numbered_name(len(moved), caller) + caller.location.msg_contents(f"$You() $conj(pick) up {plural if len(moved) > 1 else singular}.", from_obj=caller) class CmdDrop(COMMAND_DEFAULT_CLASS): @@ -445,6 +470,18 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" + def parse(self): + super().parse() + self.number = 0 + if self.args: + # check for numbering + count, *args = self.args.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.args = args[0] + def func(self): """Implement command""" @@ -455,27 +492,39 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): # Because the DROP command by definition looks for items # in inventory, call the search function using location = caller - obj = caller.search( + objs = caller.search( self.args, location=caller, nofound_string=f"You aren't carrying {self.args}.", multimatch_string=f"You carry more than one {self.args}:", + stacked=self.number, ) - if not obj: + if not objs: return + # the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list + # NOTE: this behavior may be a bug, see issue #3432 + objs = utils.make_iter(objs) - # Call the object script's at_pre_drop() method. - if not obj.at_pre_drop(caller): - return + # if any objects fail the drop permission check, cancel the drop + for obj in objs: + # Call the object's at_pre_drop() method. + if not obj.at_pre_drop(caller): + return - success = obj.move_to(caller.location, quiet=True, move_type="drop") - if not success: - caller.msg("This couldn't be dropped.") + # do the actual dropping + moved = [] + for obj in objs: + if obj.move_to(caller.location, quiet=True, move_type="drop"): + moved.append(obj) + # Call the object's at_drop() method. + obj.at_drop(caller) + + if not moved: + # none of the objects were successfully moved + self.msg("That can't be dropped.") else: - singular, _ = obj.get_numbered_name(1, caller) - caller.location.msg_contents(f"$You() $conj(drop) {singular}.", from_obj=caller) - # Call the object script's at_drop() method. - obj.at_drop(caller) + singular, plural = obj.get_numbered_name(len(moved), caller) + caller.location.msg_contents(f"$You() $conj(drop) {plural if len(moved) > 1 else singular}.", from_obj=caller) class CmdGive(COMMAND_DEFAULT_CLASS): @@ -494,6 +543,18 @@ class CmdGive(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" + def parse(self): + super().parse() + self.number = 0 + if self.lhs: + # check for numbering + count, *args = self.lhs.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.lhs = args[0] + def func(self): """Implement give""" @@ -501,37 +562,52 @@ class CmdGive(COMMAND_DEFAULT_CLASS): if not self.args or not self.rhs: caller.msg("Usage: give = ") return + # find the thing(s) to give away to_give = caller.search( self.lhs, location=caller, nofound_string=f"You aren't carrying {self.lhs}.", multimatch_string=f"You carry more than one {self.lhs}:", + stacked=self.number, ) + if not to_give: + return + # find the target to give to target = caller.search(self.rhs) - if not (to_give and target): + if not target: return - singular, _ = to_give.get_numbered_name(1, caller) + # the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list + # NOTE: this behavior may be a bug, see issue #3432 + to_give = utils.make_iter(to_give) + + + singular, plural = to_give[0].get_numbered_name(len(to_give), caller) if target == caller: - caller.msg(f"You keep {singular} to yourself.") - return - if not to_give.location == caller: - caller.msg(f"You are not holding {singular}.") + caller.msg(f"You keep {plural if len(to_give) > 1 else singular} to yourself.") return - # calling at_pre_give hook method - if not to_give.at_pre_give(caller, target): - return + # if any of the objects aren't allowed to be given, cancel the give + for obj in to_give: + # calling at_pre_give hook method + if not obj.at_pre_give(caller, target): + return - # give object - success = to_give.move_to(target, quiet=True, move_type="give") - if not success: - caller.msg(f"You could not give {singular} to {target.key}.") + # do the actual moving + moved = [] + for obj in to_give: + if obj.move_to(target, quiet=True, move_type="give"): + moved.append(obj) + # Call the object's at_give() method. + obj.at_give(caller, target) + + if not moved: + caller.msg(f"You could not give that to {target.get_display_name(caller)}.") else: - caller.msg(f"You give {singular} to {target.key}.") - target.msg(f"{caller.key} gives you {singular}.") - # Call the object script's at_give() method. - to_give.at_give(caller, target) + singular, plural = to_give[0].get_numbered_name(len(moved), caller) + names = plural if len(moved) > 1 else singular + caller.msg(f"You give {names} to {target.get_display_name(caller)}.") + target.msg(f"{caller.get_display_name(target)} gives you {names}.") class CmdSetDesc(COMMAND_DEFAULT_CLASS): From 86701f5d2c7115925a696655fa7bf9bb13d76e8d Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 19 Feb 2024 13:52:27 -0700 Subject: [PATCH 42/68] add stack tests --- evennia/commands/default/tests.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index b3fe5b5fd7..6bc69f602a 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -116,8 +116,12 @@ class TestGeneral(BaseEvenniaCommandTest): self.call(general.CmdNick(), "/list", "Defined Nicks:") def test_get_and_drop(self): - self.call(general.CmdGet(), "Obj", "You pick up an Obj") - self.call(general.CmdDrop(), "Obj", "You drop an Obj") + self.call(general.CmdGet(), "Obj", "You pick up an Obj.") + self.call(general.CmdDrop(), "Obj", "You drop an Obj.") + # test stacking + self.obj2.key = "Obj" + self.call(general.CmdGet(), "2 Obj", "You pick up two Objs.") + self.call(general.CmdDrop(), "2 Obj", "You drop two Objs.") def test_give(self): self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.") @@ -125,6 +129,10 @@ class TestGeneral(BaseEvenniaCommandTest): self.call(general.CmdGet(), "Obj", "You pick up an Obj") self.call(general.CmdGive(), "Obj to Char2", "You give") self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2) + # test stacking + self.obj2.key = "Obj" + self.obj2.location = self.char1 + self.call(general.CmdGive(), "2 Obj = Char2", "You give two Objs") def test_mux_command(self): class CmdTest(MuxCommand): From f5552121cf80a7df7884dbfd29263ae3ec729273 Mon Sep 17 00:00:00 2001 From: Cal Date: Mon, 1 Apr 2024 11:24:31 -0600 Subject: [PATCH 43/68] add new NumberedTargetCommand --- evennia/commands/default/general.py | 105 ++++++++++++++++------------ 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 8c031ac605..ce45daadc2 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -379,8 +379,58 @@ class CmdInventory(COMMAND_DEFAULT_CLASS): string = f"|wYou are carrying:\n{table}" self.msg(text=(string, {"type": "inventory"})) +class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): + """ + A class that parses out an optional number component from the input string. This + class is intended to be inherited from to provide additional functionality, rather + than used on its own. -class CmdGet(COMMAND_DEFAULT_CLASS): + Note that the class's __doc__ string (this text) is used by Evennia to create the + automatic help entry for the command, so make sure to document consistently here. + """ + def parse(self): + """ + This method is called by the cmdhandler once the command name + has been identified. It creates a new set of member variables + that can be later accessed from self.func() (see below) + + The following variables are available for our use when entering this + method (from the command definition, and assigned on the fly by the + cmdhandler): + self.key - the name of this command ('look') + self.aliases - the aliases of this cmd ('l') + self.permissions - permission string for this command + self.help_category - overall category of command + + self.caller - the object calling this command + self.cmdstring - the actual command name used to call this + (this allows you to know which alias was used, + for example) + self.args - the raw input; everything following self.cmdstring. + self.cmdset - the cmdset from which this command was picked. Not + often used (useful for commands like 'help' or to + list all available commands etc) + self.obj - the object on which this command was defined. It is often + the same as self.caller. + + This parser does additional parsing on self.args to identify a leading number, + storing the results in the following variables: + self.number = an integer representing the amount, or 0 if none was given + self.args = the re-defined input with the leading number removed + """ + super().parse() + self.number = 0 + if self.args: + # check for numbering + count, *args = self.args.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.args = args[0] + + +class CmdGet(NumberedTargetCommand): """ pick up something @@ -395,17 +445,6 @@ class CmdGet(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" - def parse(self): - super().parse() - self.number = 0 - if self.args: - # check for numbering - count, *args = self.args.split(maxsplit=1) - # we only use the first word as a count if it's a number and - # there is more text afterwards - if args and count.isdecimal(): - self.number = int(count) - self.args = args[0] def func(self): """implements the command.""" @@ -451,11 +490,11 @@ class CmdGet(COMMAND_DEFAULT_CLASS): # none of the objects were successfully moved self.msg("That can't be picked up.") else: - singular, plural = moved[0].get_numbered_name(len(moved), caller) - caller.location.msg_contents(f"$You() $conj(pick) up {plural if len(moved) > 1 else singular}.", from_obj=caller) + obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True) + caller.location.msg_contents(f"$You() $conj(pick) up {obj_name}.", from_obj=caller) -class CmdDrop(COMMAND_DEFAULT_CLASS): +class CmdDrop(NumberedTargetCommand): """ drop something @@ -470,18 +509,6 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" - def parse(self): - super().parse() - self.number = 0 - if self.args: - # check for numbering - count, *args = self.args.split(maxsplit=1) - # we only use the first word as a count if it's a number and - # there is more text afterwards - if args and count.isdecimal(): - self.number = int(count) - self.args = args[0] - def func(self): """Implement command""" @@ -523,11 +550,11 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): # none of the objects were successfully moved self.msg("That can't be dropped.") else: - singular, plural = obj.get_numbered_name(len(moved), caller) - caller.location.msg_contents(f"$You() $conj(drop) {plural if len(moved) > 1 else singular}.", from_obj=caller) + obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True) + caller.location.msg_contents(f"$You() $conj(drop) {obj_name}.", from_obj=caller) -class CmdGive(COMMAND_DEFAULT_CLASS): +class CmdGive(NumberedTargetCommand): """ give away something to someone @@ -543,17 +570,6 @@ class CmdGive(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" - def parse(self): - super().parse() - self.number = 0 - if self.lhs: - # check for numbering - count, *args = self.lhs.split(maxsplit=1) - # we only use the first word as a count if it's a number and - # there is more text afterwards - if args and count.isdecimal(): - self.number = int(count) - self.lhs = args[0] def func(self): """Implement give""" @@ -604,10 +620,9 @@ class CmdGive(COMMAND_DEFAULT_CLASS): if not moved: caller.msg(f"You could not give that to {target.get_display_name(caller)}.") else: - singular, plural = to_give[0].get_numbered_name(len(moved), caller) - names = plural if len(moved) > 1 else singular - caller.msg(f"You give {names} to {target.get_display_name(caller)}.") - target.msg(f"{caller.get_display_name(target)} gives you {names}.") + obj_name = to_give[0].get_numbered_name(len(moved), caller, return_string=True) + caller.msg(f"You give {obj_name} to {target.get_display_name(caller)}.") + target.msg(f"{caller.get_display_name(target)} gives you {obj_name}.") class CmdSetDesc(COMMAND_DEFAULT_CLASS): From 2b27214b52c0323a672847c9665bc6f2e9c8c965 Mon Sep 17 00:00:00 2001 From: Cal Date: Mon, 1 Apr 2024 11:38:52 -0600 Subject: [PATCH 44/68] accommodate MuxCommand splitting --- evennia/commands/default/general.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index ce45daadc2..d91b6b6b83 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -417,17 +417,30 @@ class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): storing the results in the following variables: self.number = an integer representing the amount, or 0 if none was given self.args = the re-defined input with the leading number removed + + Optionally, if COMMAND_DEFAULT_CLASS is a MuxCommand, it applies the same + parsing to self.lhs """ super().parse() self.number = 0 + if hasattr(self, 'lhs'): + # handle self.lhs but don't require it + count, *args = self.lhs.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.lhs = args[0] if self.args: # check for numbering count, *args = self.args.split(maxsplit=1) # we only use the first word as a count if it's a number and # there is more text afterwards if args and count.isdecimal(): - self.number = int(count) self.args = args[0] + # we only re-assign self.number if it wasn't already taken from self.lhs + if not self.number: + self.number = int(count) class CmdGet(NumberedTargetCommand): From 798d5b3059e272412ded2de0e785497b927819b9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 20:16:40 +0200 Subject: [PATCH 45/68] Update Changelog --- CHANGELOG.md | 3 +++ evennia/commands/default/general.py | 17 ++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c7be3c330..12d9de3e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ - [Feature][pull3466]: Add optional `no_article` kwarg to `DefaultObject.get_numbered_name` for the system to skip adding automatic articles. (chiizujin) +- [Feature][pull3433]: Add ability to default get/drop to affect stacks of + items, such as `get/drop 3 rock` by a custom class parent (InspectorCaracal) - Feature: Clean up the default Command variable list shown when a command has no `func()` defined (Griatch) - [Feature][issue3461]: Add `DefaultObject.filter_display_visible` helper method @@ -61,6 +63,7 @@ [pull3464]: https://github.com/evennia/evennia/pull/3464 [pull3466]: https://github.com/evennia/evennia/pull/3466 [pull3467]: https://github.com/evennia/evennia/pull/3467 +[pull3433]: https://github.com/evennia/evennia/pull/3433 [issue3450]: https://github.com/evennia/evennia/issues/3450 [issue3462]: https://github.com/evennia/evennia/issues/3462 [issue3460]: https://github.com/evennia/evennia/issues/3460 diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index d91b6b6b83..55099423f9 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -379,6 +379,7 @@ class CmdInventory(COMMAND_DEFAULT_CLASS): string = f"|wYou are carrying:\n{table}" self.msg(text=(string, {"type": "inventory"})) + class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): """ A class that parses out an optional number component from the input string. This @@ -386,8 +387,9 @@ class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): than used on its own. Note that the class's __doc__ string (this text) is used by Evennia to create the - automatic help entry for the command, so make sure to document consistently here. + automatic help entry for the command, so make sure to document consistently here. """ + def parse(self): """ This method is called by the cmdhandler once the command name @@ -417,13 +419,13 @@ class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): storing the results in the following variables: self.number = an integer representing the amount, or 0 if none was given self.args = the re-defined input with the leading number removed - + Optionally, if COMMAND_DEFAULT_CLASS is a MuxCommand, it applies the same parsing to self.lhs """ super().parse() self.number = 0 - if hasattr(self, 'lhs'): + if hasattr(self, "lhs"): # handle self.lhs but don't require it count, *args = self.lhs.split(maxsplit=1) # we only use the first word as a count if it's a number and @@ -458,7 +460,6 @@ class CmdGet(NumberedTargetCommand): locks = "cmd:all()" arg_regex = r"\s|$" - def func(self): """implements the command.""" @@ -498,7 +499,7 @@ class CmdGet(NumberedTargetCommand): moved.append(obj) # calling at_get hook method obj.at_get(caller) - + if not moved: # none of the objects were successfully moved self.msg("That can't be picked up.") @@ -558,7 +559,7 @@ class CmdDrop(NumberedTargetCommand): moved.append(obj) # Call the object's at_drop() method. obj.at_drop(caller) - + if not moved: # none of the objects were successfully moved self.msg("That can't be dropped.") @@ -583,7 +584,6 @@ class CmdGive(NumberedTargetCommand): locks = "cmd:all()" arg_regex = r"\s|$" - def func(self): """Implement give""" @@ -610,7 +610,6 @@ class CmdGive(NumberedTargetCommand): # NOTE: this behavior may be a bug, see issue #3432 to_give = utils.make_iter(to_give) - singular, plural = to_give[0].get_numbered_name(len(to_give), caller) if target == caller: caller.msg(f"You keep {plural if len(to_give) > 1 else singular} to yourself.") @@ -629,7 +628,7 @@ class CmdGive(NumberedTargetCommand): moved.append(obj) # Call the object's at_give() method. obj.at_give(caller, target) - + if not moved: caller.msg(f"You could not give that to {target.get_display_name(caller)}.") else: From c4552d69538253f7ce6425b88efb47735bc4f4b1 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 20:24:32 +0200 Subject: [PATCH 46/68] Minor cleanup for docstring to more succinct --- docs/source/Coding/Changelog.md | 3 +++ docs/source/Setup/Settings-Default.md | 1 + evennia/commands/default/general.py | 38 ++++----------------------- 3 files changed, 9 insertions(+), 33 deletions(-) diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 6c7be3c330..12d9de3e21 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -20,6 +20,8 @@ - [Feature][pull3466]: Add optional `no_article` kwarg to `DefaultObject.get_numbered_name` for the system to skip adding automatic articles. (chiizujin) +- [Feature][pull3433]: Add ability to default get/drop to affect stacks of + items, such as `get/drop 3 rock` by a custom class parent (InspectorCaracal) - Feature: Clean up the default Command variable list shown when a command has no `func()` defined (Griatch) - [Feature][issue3461]: Add `DefaultObject.filter_display_visible` helper method @@ -61,6 +63,7 @@ [pull3464]: https://github.com/evennia/evennia/pull/3464 [pull3466]: https://github.com/evennia/evennia/pull/3466 [pull3467]: https://github.com/evennia/evennia/pull/3467 +[pull3433]: https://github.com/evennia/evennia/pull/3433 [issue3450]: https://github.com/evennia/evennia/issues/3450 [issue3462]: https://github.com/evennia/evennia/issues/3462 [issue3460]: https://github.com/evennia/evennia/issues/3460 diff --git a/docs/source/Setup/Settings-Default.md b/docs/source/Setup/Settings-Default.md index 73f3a6d45b..a49fb8d786 100644 --- a/docs/source/Setup/Settings-Default.md +++ b/docs/source/Setup/Settings-Default.md @@ -31,6 +31,7 @@ value - which may change as Evennia is developed. This way you can always be sure of what you have changed and what is default behaviour. """ + import os import sys diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 55099423f9..88c15f74f4 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -4,9 +4,8 @@ General Character commands usually available to all characters import re -from django.conf import settings - import evennia +from django.conf import settings from evennia.typeclasses.attributes import NickTemplateInvalid from evennia.utils import utils @@ -385,43 +384,16 @@ class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): A class that parses out an optional number component from the input string. This class is intended to be inherited from to provide additional functionality, rather than used on its own. - - Note that the class's __doc__ string (this text) is used by Evennia to create the - automatic help entry for the command, so make sure to document consistently here. """ def parse(self): """ - This method is called by the cmdhandler once the command name - has been identified. It creates a new set of member variables - that can be later accessed from self.func() (see below) + Parser that extracts a `.number` property from the beginning of the input string. - The following variables are available for our use when entering this - method (from the command definition, and assigned on the fly by the - cmdhandler): - self.key - the name of this command ('look') - self.aliases - the aliases of this cmd ('l') - self.permissions - permission string for this command - self.help_category - overall category of command + For example, if the input string is "3 apples", this parser will set `self.number = 3` and + `self.args = "apples"`. If the input string is "apples", this parser will set + `self.number = 0` and `self.args = "apples"`. - self.caller - the object calling this command - self.cmdstring - the actual command name used to call this - (this allows you to know which alias was used, - for example) - self.args - the raw input; everything following self.cmdstring. - self.cmdset - the cmdset from which this command was picked. Not - often used (useful for commands like 'help' or to - list all available commands etc) - self.obj - the object on which this command was defined. It is often - the same as self.caller. - - This parser does additional parsing on self.args to identify a leading number, - storing the results in the following variables: - self.number = an integer representing the amount, or 0 if none was given - self.args = the re-defined input with the leading number removed - - Optionally, if COMMAND_DEFAULT_CLASS is a MuxCommand, it applies the same - parsing to self.lhs """ super().parse() self.number = 0 From c01ebb5915af2feb74d33062a21d2fbc64dfe614 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 20:36:26 +0200 Subject: [PATCH 47/68] Evennia 4.1.0 minor release --- CHANGELOG.md | 4 +++- docs/source/Coding/Changelog.md | 4 +++- evennia/VERSION.txt | 2 +- pyproject.toml | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12d9de3e21..7e0137fef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog -## Main branch +## Evennia 4.1.0 + +April 1, 2024 - [Deprecation]: `DefaultObject.get_visible_contents` - unused in core, will be removed. Use the new `.filter_visible` together with the `.get_display_*` methods instead.. diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 12d9de3e21..7e0137fef9 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -1,6 +1,8 @@ # Changelog -## Main branch +## Evennia 4.1.0 + +April 1, 2024 - [Deprecation]: `DefaultObject.get_visible_contents` - unused in core, will be removed. Use the new `.filter_visible` together with the `.get_display_*` methods instead.. diff --git a/evennia/VERSION.txt b/evennia/VERSION.txt index fcdb2e109f..ee74734aa2 100644 --- a/evennia/VERSION.txt +++ b/evennia/VERSION.txt @@ -1 +1 @@ -4.0.0 +4.1.0 diff --git a/pyproject.toml b/pyproject.toml index 74b950053a..17a9871c10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evennia" -version = "4.0.0" +version = "4.1.0" maintainers = [{ name = "Griatch", email = "griatch@gmail.com" }] description = "A full-featured toolkit and server for text-based multiplayer games (MUDs, MU*, etc)." requires-python = ">=3.10" From a5201551d4dd82b0cd90ad19e4b555644ff0a003 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 1 Apr 2024 21:39:40 +0200 Subject: [PATCH 48/68] Reformat Changelog a bit --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e0137fef9..ccbe296151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,8 @@ April 1, 2024 differentiating from their lower-case alternatives (Griatch) - [Fix][issue3460]: The `menu_login` contrib regression caused it to error out when creating a new character (Griatch) -- Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) +- Doc: Added Beginner Tutorial lessons for [Monster and NPC AI][docAI], + [Quests][docQuests] and [Making a Procedural dungeon][docDungeon] (Griatch) - Doc fixes (Griatch, InspectorCaracal, homeofpoe) [pull3421]: https://github.com/evennia/evennia/pull/3421 @@ -70,6 +71,9 @@ April 1, 2024 [issue3462]: https://github.com/evennia/evennia/issues/3462 [issue3460]: https://github.com/evennia/evennia/issues/3460 [issue3461]: https://github.com/evennia/evennia/issues/3461 +[docAI]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html +[docQuests]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.html +[docDungeon]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Dungeon.html ## Evennia 4.0.0 From ac5e186e5a2eed3fecbe06acae34aa3ad2616b3e Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Tue, 2 Apr 2024 15:23:27 +1100 Subject: [PATCH 49/68] Add exit_order kwarg to get_display_exits to sort exit names --- evennia/objects/objects.py | 24 +++++++++++++++++++++++- evennia/objects/tests.py | 16 ++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index f11b43bc51..b17ba11938 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1572,12 +1572,34 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. + + Keyword Args: + exit_order (iterable of str): The order in which exits should be listed, with + unspecified exits appearing at the end, alphabetically. + Returns: str: The exits display data. + Examples: + :: + + For a room with exits in the order 'portal', 'south', 'north', and 'out': + obj.get_display_name(looker, exit_order=('north', 'south')) + -> "Exits: north, south, out, and portal." (ANSI codes not shown here) """ + def _sort_exit_names(names): + exit_order = kwargs.get("exit_order") + if not exit_order: + return names + sort_index = {name: key for key, name in enumerate(exit_order)} + names = sorted(names) + end_pos = len(names) + 1 + names.sort(key=lambda name:sort_index.get(name, end_pos)) + return names + exits = self.filter_visible(self.contents_get(content_type="exit"), looker, **kwargs) - exit_names = iter_to_str(exi.get_display_name(looker, **kwargs) for exi in exits) + exit_names = (exi.get_display_name(looker, **kwargs) for exi in exits) + exit_names = iter_to_str(_sort_exit_names(exit_names)) return f"|wExits:|n {exit_names}" if exit_names else "" diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index de23269cdf..e7587b36e0 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -10,6 +10,7 @@ from evennia.typeclasses.tags import ( TagProperty, ) from evennia.utils import create, search +from evennia.utils.ansi import strip_ansi from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase @@ -94,6 +95,21 @@ class DefaultObjectTest(BaseEvenniaTest): all_return_exit = ex1.get_return_exit(return_all=True) self.assertEqual(len(all_return_exit), 2) + def test_exit_order(self): + DefaultExit.create("south", self.room1, self.room2, account=self.account) + DefaultExit.create("portal", self.room1, self.room2, account=self.account) + DefaultExit.create("north", self.room1, self.room2, account=self.account) + DefaultExit.create("aperture", self.room1, self.room2, account=self.account) + + # in creation order + exits = strip_ansi(self.room1.get_display_exits(self.char1)) + self.assertEqual(exits, "Exits: out, south, portal, north, and aperture") + + # in specified order with unspecified exits alpbabetically on the end + exit_order = ('north', 'south', 'out') + exits = strip_ansi(self.room1.get_display_exits(self.char1, exit_order=exit_order)) + self.assertEqual(exits, "Exits: north, south, out, aperture, and portal") + def test_urls(self): "Make sure objects are returning URLs" self.assertTrue(self.char1.get_absolute_url()) From d6983e4be35d020d66b8dd782a108fa22d313224 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:52:19 -0600 Subject: [PATCH 50/68] Update objects.py --- evennia/objects/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index f11b43bc51..c73e3c8342 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1434,7 +1434,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): return [ obj for obj in obj_list - if (obj.access(looker, "view") and obj.access(looker, "search", default=True)) + if obj != looker and (obj.access(looker, "view") and obj.access(looker, "search", default=True)) ] # name and return_appearance hooks From 9a825c7b45b326f18a7f5576c09071a697dff300 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Wed, 3 Apr 2024 17:32:14 +1100 Subject: [PATCH 51/68] Change "ANSI codes" to "markup" in get_display_exits usage example --- evennia/objects/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index b17ba11938..8bfa8aa2cf 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1585,7 +1585,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): For a room with exits in the order 'portal', 'south', 'north', and 'out': obj.get_display_name(looker, exit_order=('north', 'south')) - -> "Exits: north, south, out, and portal." (ANSI codes not shown here) + -> "Exits: north, south, out, and portal." (markup not shown here) """ def _sort_exit_names(names): exit_order = kwargs.get("exit_order") From 02a1ef006464a94de190a579da85b9650096818f Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:42:18 -0600 Subject: [PATCH 52/68] fix typo --- evennia/objects/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index f11b43bc51..21ddce342d 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -944,7 +944,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): you = from_obj or self if "you" not in mapping: - mapping[you] = you + mapping["you"] = you contents = self.contents if exclude: From 3537365e3c047efcf9aef67669ee6819d3adf643 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 3 Apr 2024 21:51:03 +0200 Subject: [PATCH 53/68] Update changelog --- CHANGELOG.md | 10 ++++++++++ evennia/contrib/tutorials/evadventure/chargen.py | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccbe296151..c37ac4979a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Main branch + +- [Fix][pull3438]: Error with 'you' mapping in third-person style of + `msg_contents` (InspectorCaracal) +- [Fix][pull3472]: The new `filter_visible` didn't exclude oneself by default + (InspectorCaracal) + +[pull3438]: https://github.com/evennia/evennia/pull/3446 + + ## Evennia 4.1.0 April 1, 2024 diff --git a/evennia/contrib/tutorials/evadventure/chargen.py b/evennia/contrib/tutorials/evadventure/chargen.py index a1b429e3d1..6c773a0116 100644 --- a/evennia/contrib/tutorials/evadventure/chargen.py +++ b/evennia/contrib/tutorials/evadventure/chargen.py @@ -4,7 +4,6 @@ EvAdventure character generation. """ from django.conf import settings - from evennia.objects.models import ObjectDB from evennia.prototypes.spawner import spawn from evennia.utils.create import create_object From 201ffc0da57815d600cc61a2cd6f45dd026c96c3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 3 Apr 2024 22:07:56 +0200 Subject: [PATCH 54/68] Some tweaks to xyzgrid error handling, as part of debugging #3475 --- evennia/contrib/grid/xyzgrid/xymap_legend.py | 10 ++++++++-- evennia/contrib/grid/xyzgrid/xyzroom.py | 7 ++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/evennia/contrib/grid/xyzgrid/xymap_legend.py b/evennia/contrib/grid/xyzgrid/xymap_legend.py index 8a205c48d2..29be2cca66 100644 --- a/evennia/contrib/grid/xyzgrid/xymap_legend.py +++ b/evennia/contrib/grid/xyzgrid/xymap_legend.py @@ -20,11 +20,11 @@ import uuid from collections import defaultdict from django.core import exceptions as django_exceptions - from evennia.prototypes import spawner from evennia.utils.utils import class_from_module -from .utils import BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, MapParserError +from .utils import (BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, + MapParserError) NodeTypeclass = None ExitTypeclass = None @@ -327,6 +327,12 @@ class MapNode: nodeobj, err = Typeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz) if err: raise RuntimeError(err) + except django_exceptions.MultipleObjectsReturned: + raise MapError( + f"Multiple objects found: {NodeTypeclass.objects.filter_xyz(xyz=xyz)}. " + "This may be due to manual creation of XYZRooms at this position. " + "Delete duplicates.", self + ) else: self.log(f" updating existing room (if changed) at xyz={xyz}") diff --git a/evennia/contrib/grid/xyzgrid/xyzroom.py b/evennia/contrib/grid/xyzgrid/xyzroom.py index 1d63a85a6f..6bf4161620 100644 --- a/evennia/contrib/grid/xyzgrid/xyzroom.py +++ b/evennia/contrib/grid/xyzgrid/xyzroom.py @@ -9,7 +9,6 @@ used as stand-alone XYZ-coordinate-aware rooms. from django.conf import settings from django.db.models import Q - from evennia.objects.manager import ObjectManager from evennia.objects.objects import DefaultExit, DefaultRoom @@ -308,7 +307,8 @@ class XYZRoom(DefaultRoom): def xyzgrid(self): global GET_XYZGRID if not GET_XYZGRID: - from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID + from evennia.contrib.grid.xyzgrid.xyzgrid import \ + get_xyzgrid as GET_XYZGRID return GET_XYZGRID() @property @@ -532,7 +532,8 @@ class XYZExit(DefaultExit): def xyzgrid(self): global GET_XYZGRID if not GET_XYZGRID: - from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID + from evennia.contrib.grid.xyzgrid.xyzgrid import \ + get_xyzgrid as GET_XYZGRID return GET_XYZGRID() @property From 425ec69928a74cb620d615eb623c315b45b02e5a Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 4 Apr 2024 19:33:23 +0200 Subject: [PATCH 55/68] Fix `func dbref` command to include optional dbref info --- evennia/commands/default/building.py | 30 +++++++++++----------------- evennia/objects/objects.py | 17 ++++------------ 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index ff4111643e..d8059c9a50 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -5,13 +5,13 @@ Building and world design commands import re import typing +import evennia from django.conf import settings from django.core.paginator import Paginator from django.db.models import Max, Min, Q - -import evennia from evennia import InterruptCommand -from evennia.commands.cmdhandler import generate_cmdset_providers, get_and_merge_cmdsets +from evennia.commands.cmdhandler import (generate_cmdset_providers, + get_and_merge_cmdsets) from evennia.locks.lockhandler import LockException from evennia.objects.models import ObjectDB from evennia.prototypes import menus as olc_menus @@ -24,18 +24,10 @@ from evennia.utils.dbserialize import deserialize from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.evtable import EvTable -from evennia.utils.utils import ( - class_from_module, - crop, - dbref, - display_len, - format_grid, - get_all_typeclasses, - inherits_from, - interactive, - list_to_string, - variable_from_module, -) +from evennia.utils.utils import (class_from_module, crop, dbref, display_len, + format_grid, get_all_typeclasses, + inherits_from, interactive, list_to_string, + variable_from_module) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -77,7 +69,7 @@ __all__ = ( ) # used by set -from ast import literal_eval as _LITERAL_EVAL +from ast import literal_eval as _LITERAL_EVAL # noqa LIST_APPEND_CHAR = "+" @@ -3282,9 +3274,11 @@ class CmdFind(COMMAND_DEFAULT_CLASS): string += f"\n |RNo match found for '{searchstring}' in #dbref interval.|n" else: result = result[0] - string += f"\n|g {result.get_display_name(caller)} - {result.path}|n" + string += (f"\n|g {result.get_display_name(caller)}" + f"{result.get_extra_display_name_info(caller)} - {result.path}|n") if "loc" in self.switches and not is_account and result.location: - string += f" (|wlocation|n: |g{result.location.get_display_name(caller)}|n)" + string += (f" (|wlocation|n: |g{result.location.get_display_name(caller)}" + f"{result.get_extra_display_name_info(caller)}|n)") else: # Not an account/dbref search but a wider search; build a queryset. # Searches for key and aliases diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index e63aee46a2..0e613e8482 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -10,11 +10,10 @@ import time import typing from collections import defaultdict +import evennia import inflect from django.conf import settings from django.utils.translation import gettext as _ - -import evennia from evennia.commands import cmdset from evennia.commands.cmdsethandler import CmdSetHandler from evennia.objects.manager import ObjectManager @@ -24,17 +23,9 @@ from evennia.server.signals import SIGNAL_EXIT_TRAVERSED from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import ansi, create, funcparser, logger, search -from evennia.utils.utils import ( - class_from_module, - compress_whitespace, - dbref, - is_iter, - iter_to_str, - lazy_property, - make_iter, - to_str, - variable_from_module, -) +from evennia.utils.utils import (class_from_module, compress_whitespace, dbref, + is_iter, iter_to_str, lazy_property, + make_iter, to_str, variable_from_module) _INFLECT = inflect.engine() _MULTISESSION_MODE = settings.MULTISESSION_MODE From d1447b75a054cf2583aa72e5d279144a13bde86f Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 4 Apr 2024 19:35:55 +0200 Subject: [PATCH 56/68] Update Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c37ac4979a..8da538d8f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ `msg_contents` (InspectorCaracal) - [Fix][pull3472]: The new `filter_visible` didn't exclude oneself by default (InspectorCaracal) +- Fix: `find #dbref` results didn't include the results of + `.get_extra_display_name_info` (the #dbref display by default) (Griatch) [pull3438]: https://github.com/evennia/evennia/pull/3446 From f88b68dac60e2a4cb08867240ae8d8d1f4dc057b Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 5 Apr 2024 09:32:35 +0200 Subject: [PATCH 57/68] Add DefaultAccount.get_extra_display_name_info method --- CHANGELOG.md | 2 ++ evennia/accounts/accounts.py | 34 +++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8da538d8f6..669b4ea2e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ (InspectorCaracal) - Fix: `find #dbref` results didn't include the results of `.get_extra_display_name_info` (the #dbref display by default) (Griatch) +- Fix: Add `DefaultAccount.get_extra_display_name_info` method for API + compliance with `DefaultObject` in commands. (Griatch) [pull3438]: https://github.com/evennia/evennia/pull/3446 diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index b01592ddfa..594424f269 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -16,14 +16,13 @@ import time import typing from random import getrandbits +import evennia from django.conf import settings from django.contrib.auth import authenticate, password_validation from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ - -import evennia from evennia.accounts.manager import AccountManager from evennia.accounts.models import AccountDB from evennia.commands.cmdsethandler import CmdSetHandler @@ -31,24 +30,17 @@ from evennia.comms.models import ChannelDB from evennia.objects.models import ObjectDB from evennia.scripts.scripthandler import ScriptHandler from evennia.server.models import ServerConfig -from evennia.server.signals import ( - SIGNAL_ACCOUNT_POST_CREATE, - SIGNAL_ACCOUNT_POST_LOGIN_FAIL, - SIGNAL_OBJECT_POST_PUPPET, - SIGNAL_OBJECT_POST_UNPUPPET, -) +from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE, + SIGNAL_ACCOUNT_POST_LOGIN_FAIL, + SIGNAL_OBJECT_POST_PUPPET, + SIGNAL_OBJECT_POST_UNPUPPET) from evennia.server.throttle import Throttle from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import class_from_module, create, logger from evennia.utils.optionhandler import OptionHandler -from evennia.utils.utils import ( - is_iter, - lazy_property, - make_iter, - to_str, - variable_from_module, -) +from evennia.utils.utils import (is_iter, lazy_property, make_iter, to_str, + variable_from_module) __all__ = ("DefaultAccount", "DefaultGuest") @@ -373,6 +365,18 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): session.protocol_flags.get("SCREENREADER") for session in self.sessions.all() ) + def get_extra_display_name_info(self, looker, **kwargs): + """ + Used in .get_display_name() to provide extra information to the looker. We split this + to be consistent with the Object version of this method. + + This is used e.g. by the `find` command by default. + + """ + if looker and self.locks.check_lockstring(looker, "perm(Admin)"): + return f"(#{self.id})" + return "" + def get_display_name(self, looker, **kwargs): """ This is used by channels and other OOC communications methods to give a From c578dfb1b54e6b84685144c344be4b0c2e5701b1 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Fri, 5 Apr 2024 18:55:00 +1100 Subject: [PATCH 58/68] Add missing bracket in sethome message --- 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 d8059c9a50..871cb5272c 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1397,7 +1397,7 @@ class CmdSetHome(CmdLink): obj.home = new_home if old_home: string = ( - f"Home location of {obj} was changed from {old_home}({old_home.dbref} to" + f"Home location of {obj} was changed from {old_home}({old_home.dbref}) to" f" {new_home}({new_home.dbref})." ) else: From a7f85296100b3dfd6c7f9d39789b46c48c7db2d7 Mon Sep 17 00:00:00 2001 From: Cal Date: Fri, 5 Apr 2024 11:31:58 -0600 Subject: [PATCH 59/68] fix numbered target cmd parse --- evennia/commands/default/general.py | 2 +- evennia/commands/default/tests.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 88c15f74f4..5b27318a03 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -397,7 +397,7 @@ class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): """ super().parse() self.number = 0 - if hasattr(self, "lhs"): + if getattr(self, "lhs", None): # handle self.lhs but don't require it count, *args = self.lhs.split(maxsplit=1) # we only use the first word as a count if it's a number and diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 6bc69f602a..f7fc7f1fd2 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -134,6 +134,17 @@ class TestGeneral(BaseEvenniaCommandTest): self.obj2.location = self.char1 self.call(general.CmdGive(), "2 Obj = Char2", "You give two Objs") + def test_numbered_target_command(self): + class CmdTest(general.NumberedTargetCommand): + key = "test" + + def func(self): + self.msg(f"Number: {self.number} Args: {self.args}") + + self.call(CmdTest(), "", "Number: 0 Args: ") + self.call(CmdTest(), "obj", "Number: 0 Args: obj") + self.call(CmdTest(), "1 obj", "Number: 1 Args: obj") + def test_mux_command(self): class CmdTest(MuxCommand): key = "test" From 9cec25419659c3c1713e93b8dc0b7245d6ab5685 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 5 Apr 2024 22:39:42 +0200 Subject: [PATCH 60/68] Show XYZRoom typeclass in repr() return --- CHANGELOG.md | 3 +++ evennia/contrib/grid/xyzgrid/xyzroom.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 669b4ea2e5..2d6da2d322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,11 @@ `.get_extra_display_name_info` (the #dbref display by default) (Griatch) - Fix: Add `DefaultAccount.get_extra_display_name_info` method for API compliance with `DefaultObject` in commands. (Griatch) +- Fix: Show `XYZRoom` subclass when repr() it. (Griatch) +- [Fix][pull3485]: Typo in `sethome` message (chiizujin) [pull3438]: https://github.com/evennia/evennia/pull/3446 +[pull3485]: https://github.com/evennia/evennia/pull/3485 ## Evennia 4.1.0 diff --git a/evennia/contrib/grid/xyzgrid/xyzroom.py b/evennia/contrib/grid/xyzgrid/xyzroom.py index 6bf4161620..3577d98b67 100644 --- a/evennia/contrib/grid/xyzgrid/xyzroom.py +++ b/evennia/contrib/grid/xyzgrid/xyzroom.py @@ -282,7 +282,7 @@ class XYZRoom(DefaultRoom): def __repr__(self): x, y, z = self.xyz - return f"" + return f"<{self.__class__.__name__} '{self.db_key}', XYZ=({x},{y},{z})>" @property def xyz(self): From 50d8ae2f546be8eda412b57bb48c0a0da7ce8dc9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 5 Apr 2024 23:01:09 +0200 Subject: [PATCH 61/68] Do not skip wrongly capitalized EvEditor commands. Resolve #3476 --- CHANGELOG.md | 6 ++++ evennia/utils/eveditor.py | 59 ++++++++++++++++++++++----------------- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6da2d322..83688edb82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,15 @@ compliance with `DefaultObject` in commands. (Griatch) - Fix: Show `XYZRoom` subclass when repr() it. (Griatch) - [Fix][pull3485]: Typo in `sethome` message (chiizujin) +- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no + arguments (InspectorCaracal) +- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization + (Griatch) [pull3438]: https://github.com/evennia/evennia/pull/3446 [pull3485]: https://github.com/evennia/evennia/pull/3485 +[pull3487]: https://github.com/evennia/evennia/pull/3487 +[issue3476]: https://github.com/evennia/evennia/issues/3476 ## Evennia 4.1.0 diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 04578c8be8..6175ada54f 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -44,7 +44,6 @@ import re from django.conf import settings from django.utils.translation import gettext as _ - from evennia import CmdSet from evennia.commands import cmdhandler from evennia.utils import dedent, fill, is_iter, justify, logger, to_str, utils @@ -98,7 +97,7 @@ _HELP_TEXT = _( :s - search/replace word or regex in buffer or on line :j - justify buffer or line . is f, c, l or r. Default f (full) - :f - flood-fill entire buffer or line : Equivalent to :j left + :f - flood-fill entire buffer or line . Equivalent to :j l :fi - indent entire buffer or line :fd - de-indent entire buffer or line @@ -351,6 +350,35 @@ class CmdEditorBase(_COMMAND_DEFAULT_CLASS): self.arg1 = arg1 self.arg2 = arg2 + def insert_raw_string_into_buffer(self): + """ + Insert a line into the buffer. Used by both CmdLineInput and CmdEditorGroup. + + """ + caller = self.caller + editor = caller.ndb._eveditor + buf = editor.get_buffer() + + # add a line of text to buffer + line = self.raw_string.strip("\r\n") + if editor._codefunc and editor._indent >= 0: + # if automatic indentation is active, add spaces + line = editor.deduce_indent(line, buf) + buf = line if not buf else buf + "\n%s" % line + self.editor.update_buffer(buf) + if self.editor._echo_mode: + # need to do it here or we will be off one line + cline = len(self.editor.get_buffer().split("\n")) + if editor._codefunc: + # display the current level of identation + indent = editor._indent + if indent < 0: + indent = "off" + + self.caller.msg("|b%02i|||n (|g%s|n) %s" % (cline, indent, raw(line))) + else: + self.caller.msg("|b%02i|||n %s" % (cline, raw(line))) + def _load_editor(caller): """ @@ -394,29 +422,7 @@ class CmdLineInput(CmdEditorBase): If the editor handles code, it might add automatic indentation. """ - caller = self.caller - editor = caller.ndb._eveditor - buf = editor.get_buffer() - - # add a line of text to buffer - line = self.raw_string.strip("\r\n") - if editor._codefunc and editor._indent >= 0: - # if automatic indentation is active, add spaces - line = editor.deduce_indent(line, buf) - buf = line if not buf else buf + "\n%s" % line - self.editor.update_buffer(buf) - if self.editor._echo_mode: - # need to do it here or we will be off one line - cline = len(self.editor.get_buffer().split("\n")) - if editor._codefunc: - # display the current level of identation - indent = editor._indent - if indent < 0: - indent = "off" - - self.caller.msg("|b%02i|||n (|g%s|n) %s" % (cline, indent, raw(line))) - else: - self.caller.msg("|b%02i|||n %s" % (cline, raw(line))) + self.insert_raw_string_into_buffer() class CmdEditorGroup(CmdEditorBase): @@ -801,6 +807,9 @@ class CmdEditorGroup(CmdEditorBase): caller.msg(_("Auto-indentation turned off.")) else: caller.msg(_("This command is only available in code editor mode.")) + else: + # no match - insert as line in buffer + self.insert_raw_string_into_buffer() class EvEditorCmdSet(CmdSet): From 387533d1f0c1f6e4c42bf6ba99a5634fccacc29d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Apr 2024 14:08:36 +0200 Subject: [PATCH 62/68] Fix at_server_reload_start not firing. Resolve #3477. Add Server-Lifecycle.md docpage --- CHANGELOG.md | 6 ++ docs/source/Coding/Changelog.md | 35 +++++++- docs/source/Concepts/Concepts-Overview.md | 1 + docs/source/Concepts/Server-Lifecycle.md | 81 +++++++++++++++++++ docs/source/Setup/Settings.md | 52 +++--------- evennia/accounts/accounts.py | 22 +++-- evennia/commands/default/building.py | 34 +++++--- evennia/commands/default/general.py | 3 +- evennia/contrib/grid/xyzgrid/xymap_legend.py | 7 +- evennia/contrib/grid/xyzgrid/xyzroom.py | 7 +- .../contrib/tutorials/evadventure/chargen.py | 1 + evennia/objects/objects.py | 20 +++-- evennia/server/evennia_launcher.py | 2 + evennia/server/portal/amp_server.py | 30 ++++--- evennia/utils/eveditor.py | 1 + 15 files changed, 222 insertions(+), 80 deletions(-) create mode 100644 docs/source/Concepts/Server-Lifecycle.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 83688edb82..3f2dc126cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,17 @@ arguments (InspectorCaracal) - [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch) +- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on + a reload (regression). +- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe + the hooks called on server start/stop/reload (Griatch) [pull3438]: https://github.com/evennia/evennia/pull/3446 [pull3485]: https://github.com/evennia/evennia/pull/3485 [pull3487]: https://github.com/evennia/evennia/pull/3487 [issue3476]: https://github.com/evennia/evennia/issues/3476 +[issue3477]: https://github.com/evennia/evennia/issues/3477 +[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html ## Evennia 4.1.0 diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 7e0137fef9..3f2dc126cf 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -1,5 +1,34 @@ # Changelog +## Main branch + +- [Fix][pull3438]: Error with 'you' mapping in third-person style of + `msg_contents` (InspectorCaracal) +- [Fix][pull3472]: The new `filter_visible` didn't exclude oneself by default + (InspectorCaracal) +- Fix: `find #dbref` results didn't include the results of + `.get_extra_display_name_info` (the #dbref display by default) (Griatch) +- Fix: Add `DefaultAccount.get_extra_display_name_info` method for API + compliance with `DefaultObject` in commands. (Griatch) +- Fix: Show `XYZRoom` subclass when repr() it. (Griatch) +- [Fix][pull3485]: Typo in `sethome` message (chiizujin) +- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no + arguments (InspectorCaracal) +- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization + (Griatch) +- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on + a reload (regression). +- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe + the hooks called on server start/stop/reload (Griatch) + +[pull3438]: https://github.com/evennia/evennia/pull/3446 +[pull3485]: https://github.com/evennia/evennia/pull/3485 +[pull3487]: https://github.com/evennia/evennia/pull/3487 +[issue3476]: https://github.com/evennia/evennia/issues/3476 +[issue3477]: https://github.com/evennia/evennia/issues/3477 +[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html + + ## Evennia 4.1.0 April 1, 2024 @@ -49,7 +78,8 @@ April 1, 2024 differentiating from their lower-case alternatives (Griatch) - [Fix][issue3460]: The `menu_login` contrib regression caused it to error out when creating a new character (Griatch) -- Doc: Added Beginner Tutorial lessons for AI, Quests and Procedural dungeon (Griatch) +- Doc: Added Beginner Tutorial lessons for [Monster and NPC AI][docAI], + [Quests][docQuests] and [Making a Procedural dungeon][docDungeon] (Griatch) - Doc fixes (Griatch, InspectorCaracal, homeofpoe) [pull3421]: https://github.com/evennia/evennia/pull/3421 @@ -70,6 +100,9 @@ April 1, 2024 [issue3462]: https://github.com/evennia/evennia/issues/3462 [issue3460]: https://github.com/evennia/evennia/issues/3460 [issue3461]: https://github.com/evennia/evennia/issues/3461 +[docAI]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html +[docQuests]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.html +[docDungeon]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Dungeon.html ## Evennia 4.0.0 diff --git a/docs/source/Concepts/Concepts-Overview.md b/docs/source/Concepts/Concepts-Overview.md index 84d0aa5ca4..60cb066e7f 100644 --- a/docs/source/Concepts/Concepts-Overview.md +++ b/docs/source/Concepts/Concepts-Overview.md @@ -37,6 +37,7 @@ Banning.md ```{toctree} :maxdepth: 2 +Server-Lifecycle Protocols.md Models.md Zones.md diff --git a/docs/source/Concepts/Server-Lifecycle.md b/docs/source/Concepts/Server-Lifecycle.md new file mode 100644 index 0000000000..2cc8a5a017 --- /dev/null +++ b/docs/source/Concepts/Server-Lifecycle.md @@ -0,0 +1,81 @@ + +# Evennia Server Lifecycle + +As part of your game design you may want to change how Evennia behaves when starting or stopping. A common use case would be to start up some piece of custom code you want to always have available once the server is up. + +Evennia has three main life cycles, all of which you can add custom behavior for: + +- **Database life cycle**: Evennia runs on top of one several [supported databases](../Setup/Choosing-a-Database.md). It's possible to wipe this database to 'start over' without needing to separately download Evennia anew. +- **Reboot life cycle**: From When Evennia starts from zero, to it being fully shut down, which means both Portal and Server are stopped. This kicks all players. +- **Reload life cycle:** This is the main runtime, until a "reload" event. Reloads do not kick any players. + +## When Evennia first starts + +This is the beginning of the **Database life cycle**, just after the database is created and migrated for the first time. After that it won't run again unless you wipe the database and initialize a new one (see [Choosing a Database](../Setup/Choosing-a-Database.md)). + +Hooks called, in sequence: + +1. `evennia.server.initial_setup.handle_setup(last_step=None)`: Evennia's core initialization function. This is what creates the #1 Character (tied to the superuser) and Limbo. It calls the next hook below and also understands to restart at the last failed step if there was some issue. You should normally not override this function unless you _really_ know what you are doing, but in that case you'd do so by setting `settings.INITIAL_SETUP_MODULE` to your own module with a `handle_setup` function in it. +2. `mygame/server/conf/at_initial_setup.py` contains a single function, `at_initial_setup()`, which will be called without arguments. It's called last in the setup sequence by the above function. Use this to add your own custom behavior or to tweak the initialization. If you for example wanted to change the auto-generated Limbo room, you should do it from here. If you want to change where this function is found, you can do so by changing `settings.AT_INITIAL_SETUP_HOOK_MODULE`. + + +## When Evennia starts and shutdowns + +This is the **Reboot life cycle**. Evennia consists of two main processes, the [Portal and the Server](../Components/Portal-And-Server.md). On a reboot or shutdown, both Portal and Server shuts down, which means all players are disconnected. + +Each process call a series of hooks located in `mygame/server/conf/at_server_startstop.py`. You can customize the module used with `settings.AT_SERVER_STARTSTOP_MODULE` - this can even be a list of modules, if so, the appropriately-named functions will be called from each module, in sequence. + +All hooks are called without arguments. + +> The use of the term 'server' in the hook-names indicate the whole of Evennia, not just the `Server` component. + +### Server cold start + +Starting the server from zero, after a full stop. This is done with `evennia start` from the terminal. + +1. `at_server_init()` - Always called first in the startup sequence. +2. `at_server_cold_start()` - Only called on cold starts. +3. `at_server_start()` - Always called last in the startup sequece. + +### Server cold shutdown + +Shutting everything down. Done with `shutdown` in-game or `evennia stop` from the terminal. + +1. `at_server_cold_stop()` - Only called on cold stops. +2. `at_server_stop()` - Always called last in the stopping sequence. + +### Server reboots + +This is done with `evennia reboot` and effectively constitutes an automatic cold shutdown followed by a cold start controlled from the `evennia` launcher. There are no special `reboot` hooks for this, instead it looks like you'd expect: + +1. `at_server_cold_stop()` +2. `at_server_stop()` (after this, both `Server` + `Portal` have both shut down) +3. `at_server_init()` (like a cold start) +4. `at_server_cold_start()` +5. `at_server_start()` + +## When Evennia reloads and resets + +This is the **Reload life cycle**. As mentioned above, Evennia consists of two components, the [Portal and Server](../Components/Portal-And-Server.md). During a reload, only the `Server` component is shut down and restarted. Since the Portal stays up, players are not disconnected. + +All hooks are called without arguments. + +### Server reload + +Reloads are initiated with the `reload` command in-game, or with `evennia reload` from the terminal. + +1. `at_server_reload_stop()` - Only called on reload stops. +2. `at_server_stop` - Always called last in the stopping sequence. +3. `at_server_init()` - Always called first in startup sequence. +4. `at_server_reload_start()` - Only called on a reload (re)start. +5. `at_server_start()` - Always called last in the startup sequence. + +### Server reset + +A 'reset' is a hybrid reload state, where the reload is treated as a cold shutdown only for the sake of running hooks (players are not disconnected). It's run with `reset` in-game or with `evennia reset` from the terminal. + +1. `at_server_cold_stop()` +2. `at_server_stop()` (after this, only `Server` has shut down) +3. `at_server_init()` (`Server` coming back up) +4. `at_server_cold_start()` +5. `at_server_start()` diff --git a/docs/source/Setup/Settings.md b/docs/source/Setup/Settings.md index 4d549f7549..766d24281e 100644 --- a/docs/source/Setup/Settings.md +++ b/docs/source/Setup/Settings.md @@ -1,7 +1,6 @@ # Changing Game Settings -Evennia runs out of the box without any changes to its settings. But there are several important -ways to customize the server and expand it with your own plugins. +Evennia runs out of the box without any changes to its settings. But there are several important ways to customize the server and expand it with your own plugins. All game-specific settings are located in the `mygame/server/conf/` directory. @@ -17,13 +16,9 @@ heavily documented and up-to-date, so you should refer to this file directly for Since `mygame/server/conf/settings.py` is a normal Python module, it simply imports `evennia/settings_default.py` into itself at the top. -This means that if any setting you want to change were to depend on some *other* default setting, -you might need to copy & paste both in order to change them and get the effect you want (for most -commonly changed settings, this is not something you need to worry about). +This means that if any setting you want to change were to depend on some *other* default setting, you might need to copy & paste both in order to change them and get the effect you want (for most commonly changed settings, this is not something you need to worry about). -You should never edit `evennia/settings_default.py`. Rather you should copy&paste the select -variables you want to change into your `settings.py` and edit them there. This will overload the -previously imported defaults. +You should never edit `evennia/settings_default.py`. Rather you should copy&paste the select variables you want to change into your `settings.py` and edit them there. This will overload the previously imported defaults. ```{warning} Don't copy everything! It may be tempting to copy *everything* from `settings_default.py` into your own settings file just to have it all in one place. Don't do this. By copying only what you need, you can easier track what you changed. @@ -41,45 +36,24 @@ In code, the settings is accessed through Each setting appears as a property on the imported `settings` object. You can also explore all possible options with `evennia.settings_full` (this also includes advanced Django defaults that are not touched in default Evennia). -> When importing `settings` into your code like this, it will be *read -only*. You *cannot* edit your settings from your code! The only way to change an Evennia setting is -to edit `mygame/server/conf/settings.py` directly. You will also need to restart the server -(possibly also the Portal) before a changed setting becomes available. +> When importing `settings` into your code like this, it will be *read only*. You *cannot* edit your settings from your code! The only way to change an Evennia setting is to edit `mygame/server/conf/settings.py` directly. You will also need to restart the server (possibly also the Portal) before a changed setting becomes available. ## Other files in the `server/conf` directory Apart from the main `settings.py` file, -- `at_initial_setup.py` - this allows you to add a custom startup method to be called (only) the -very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to -start your own global scripts or set up other system/world-related things your game needs to have -running from the start. -- `at_server_startstop.py` - this module contains two functions that Evennia will call every time -the Server starts and stops respectively - this includes stopping due to reloading and resetting as -well as shutting down completely. It's a useful place to put custom startup code for handlers and -other things that must run in your game but which has no database persistence. -- `connection_screens.py` - all global string variables in this module are interpreted by Evennia as -a greeting screen to show when an Account first connects. If more than one string variable is -present in the module a random one will be picked. +- `at_initial_setup.py` - this allows you to add a custom startup method to be called (only) the very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to start your own global scripts or set up other system/world-related things your game needs to have running from the start. +- `at_server_startstop.py` - this module contains functions that Evennia will call every time the Server starts and stops respectively - this includes stopping due to reloading and resetting as well as shutting down completely. It's a useful place to put custom startup code for handlers and other things that must run in your game but which has no database persistence. +- `connection_screens.py` - all global string variables in this module are interpreted by Evennia as a greeting screen to show when an Account first connects. If more than one string variable is present in the module a random one will be picked. - `inlinefuncs.py` - this is where you can define custom [FuncParser functions](../Components/FuncParser.md). -- `inputfuncs.py` - this is where you define custom [Input functions](../Components/Inputfuncs.md) to handle data -from the client. -- `lockfuncs.py` - this is one of many possible modules to hold your own "safe" *lock functions* to -make available to Evennia's [Locks](../Components/Locks.md). -- `mssp.py` - this holds meta information about your game. It is used by MUD search engines (which -you often have to register with) in order to display what kind of game you are running along with - statistics such as number of online accounts and online status. +- `inputfuncs.py` - this is where you define custom [Input functions](../Components/Inputfuncs.md) to handle data from the client. +- `lockfuncs.py` - this is one of many possible modules to hold your own "safe" *lock functions* to make available to Evennia's [Locks](../Components/Locks.md). +- `mssp.py` - this holds meta information about your game. It is used by MUD search engines (which you often have to register with) in order to display what kind of game you are running along with statistics such as number of online accounts and online status. - `oobfuncs.py` - in here you can define custom [OOB functions](../Concepts/OOB.md). -- `portal_services_plugin.py` - this allows for adding your own custom services/protocols to the -Portal. It must define one particular function that will be called by Evennia at startup. There can -be any number of service plugin modules, all will be imported and used if defined. More info can be -found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols). -- `server_services_plugin.py` - this is equivalent to the previous one, but used for adding new -services to the Server instead. More info can be found -[here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols). +- `portal_services_plugin.py` - this allows for adding your own custom services/protocols to the Portal. It must define one particular function that will be called by Evennia at startup. There can be any number of service plugin modules, all will be imported and used if defined. More info can be found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols). +- `server_services_plugin.py` - this is equivalent to the previous one, but used for adding new services to the Server instead. More info can be found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols). -Some other Evennia systems can be customized by plugin modules but has no explicit template in -`conf/`: +Some other Evennia systems can be customized by plugin modules but has no explicit template in `conf/`: - *cmdparser.py* - a custom module can be used to totally replace Evennia's default command parser. All this does is to split the incoming string into "command name" and "the rest". It also handles things like error messages for no-matches and multiple-matches among other things that makes this more complex than it sounds. The default parser is *very* generic, so you are most often best served by modifying things further down the line (on the command parse level) than here. - *at_search.py* - this allows for replacing the way Evennia handles search results. It allows to change how errors are echoed and how multi-matches are resolved and reported (like how the default understands that "2-ball" should match the second "ball" object if there are two of them in the room). \ No newline at end of file diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 594424f269..89137ff672 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -16,13 +16,14 @@ import time import typing from random import getrandbits -import evennia from django.conf import settings from django.contrib.auth import authenticate, password_validation from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ + +import evennia from evennia.accounts.manager import AccountManager from evennia.accounts.models import AccountDB from evennia.commands.cmdsethandler import CmdSetHandler @@ -30,17 +31,24 @@ from evennia.comms.models import ChannelDB from evennia.objects.models import ObjectDB from evennia.scripts.scripthandler import ScriptHandler from evennia.server.models import ServerConfig -from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE, - SIGNAL_ACCOUNT_POST_LOGIN_FAIL, - SIGNAL_OBJECT_POST_PUPPET, - SIGNAL_OBJECT_POST_UNPUPPET) +from evennia.server.signals import ( + SIGNAL_ACCOUNT_POST_CREATE, + SIGNAL_ACCOUNT_POST_LOGIN_FAIL, + SIGNAL_OBJECT_POST_PUPPET, + SIGNAL_OBJECT_POST_UNPUPPET, +) from evennia.server.throttle import Throttle from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import class_from_module, create, logger from evennia.utils.optionhandler import OptionHandler -from evennia.utils.utils import (is_iter, lazy_property, make_iter, to_str, - variable_from_module) +from evennia.utils.utils import ( + is_iter, + lazy_property, + make_iter, + to_str, + variable_from_module, +) __all__ = ("DefaultAccount", "DefaultGuest") diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 871cb5272c..558d96b3b5 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -5,13 +5,13 @@ Building and world design commands import re import typing -import evennia from django.conf import settings from django.core.paginator import Paginator from django.db.models import Max, Min, Q + +import evennia from evennia import InterruptCommand -from evennia.commands.cmdhandler import (generate_cmdset_providers, - get_and_merge_cmdsets) +from evennia.commands.cmdhandler import generate_cmdset_providers, get_and_merge_cmdsets from evennia.locks.lockhandler import LockException from evennia.objects.models import ObjectDB from evennia.prototypes import menus as olc_menus @@ -24,10 +24,18 @@ from evennia.utils.dbserialize import deserialize from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.evtable import EvTable -from evennia.utils.utils import (class_from_module, crop, dbref, display_len, - format_grid, get_all_typeclasses, - inherits_from, interactive, list_to_string, - variable_from_module) +from evennia.utils.utils import ( + class_from_module, + crop, + dbref, + display_len, + format_grid, + get_all_typeclasses, + inherits_from, + interactive, + list_to_string, + variable_from_module, +) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -3274,11 +3282,15 @@ class CmdFind(COMMAND_DEFAULT_CLASS): string += f"\n |RNo match found for '{searchstring}' in #dbref interval.|n" else: result = result[0] - string += (f"\n|g {result.get_display_name(caller)}" - f"{result.get_extra_display_name_info(caller)} - {result.path}|n") + string += ( + f"\n|g {result.get_display_name(caller)}" + f"{result.get_extra_display_name_info(caller)} - {result.path}|n" + ) if "loc" in self.switches and not is_account and result.location: - string += (f" (|wlocation|n: |g{result.location.get_display_name(caller)}" - f"{result.get_extra_display_name_info(caller)}|n)") + string += ( + f" (|wlocation|n: |g{result.location.get_display_name(caller)}" + f"{result.get_extra_display_name_info(caller)}|n)" + ) else: # Not an account/dbref search but a wider search; build a queryset. # Searches for key and aliases diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 5b27318a03..06aae08f75 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -4,8 +4,9 @@ General Character commands usually available to all characters import re -import evennia from django.conf import settings + +import evennia from evennia.typeclasses.attributes import NickTemplateInvalid from evennia.utils import utils diff --git a/evennia/contrib/grid/xyzgrid/xymap_legend.py b/evennia/contrib/grid/xyzgrid/xymap_legend.py index 29be2cca66..973f96a1d5 100644 --- a/evennia/contrib/grid/xyzgrid/xymap_legend.py +++ b/evennia/contrib/grid/xyzgrid/xymap_legend.py @@ -20,11 +20,11 @@ import uuid from collections import defaultdict from django.core import exceptions as django_exceptions + from evennia.prototypes import spawner from evennia.utils.utils import class_from_module -from .utils import (BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, - MapParserError) +from .utils import BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, MapParserError NodeTypeclass = None ExitTypeclass = None @@ -331,7 +331,8 @@ class MapNode: raise MapError( f"Multiple objects found: {NodeTypeclass.objects.filter_xyz(xyz=xyz)}. " "This may be due to manual creation of XYZRooms at this position. " - "Delete duplicates.", self + "Delete duplicates.", + self, ) else: self.log(f" updating existing room (if changed) at xyz={xyz}") diff --git a/evennia/contrib/grid/xyzgrid/xyzroom.py b/evennia/contrib/grid/xyzgrid/xyzroom.py index 3577d98b67..21de4aa874 100644 --- a/evennia/contrib/grid/xyzgrid/xyzroom.py +++ b/evennia/contrib/grid/xyzgrid/xyzroom.py @@ -9,6 +9,7 @@ used as stand-alone XYZ-coordinate-aware rooms. from django.conf import settings from django.db.models import Q + from evennia.objects.manager import ObjectManager from evennia.objects.objects import DefaultExit, DefaultRoom @@ -307,8 +308,7 @@ class XYZRoom(DefaultRoom): def xyzgrid(self): global GET_XYZGRID if not GET_XYZGRID: - from evennia.contrib.grid.xyzgrid.xyzgrid import \ - get_xyzgrid as GET_XYZGRID + from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID return GET_XYZGRID() @property @@ -532,8 +532,7 @@ class XYZExit(DefaultExit): def xyzgrid(self): global GET_XYZGRID if not GET_XYZGRID: - from evennia.contrib.grid.xyzgrid.xyzgrid import \ - get_xyzgrid as GET_XYZGRID + from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID return GET_XYZGRID() @property diff --git a/evennia/contrib/tutorials/evadventure/chargen.py b/evennia/contrib/tutorials/evadventure/chargen.py index 6c773a0116..a1b429e3d1 100644 --- a/evennia/contrib/tutorials/evadventure/chargen.py +++ b/evennia/contrib/tutorials/evadventure/chargen.py @@ -4,6 +4,7 @@ EvAdventure character generation. """ from django.conf import settings + from evennia.objects.models import ObjectDB from evennia.prototypes.spawner import spawn from evennia.utils.create import create_object diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 0e613e8482..8d04e3849c 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -10,10 +10,11 @@ import time import typing from collections import defaultdict -import evennia import inflect from django.conf import settings from django.utils.translation import gettext as _ + +import evennia from evennia.commands import cmdset from evennia.commands.cmdsethandler import CmdSetHandler from evennia.objects.manager import ObjectManager @@ -23,9 +24,17 @@ from evennia.server.signals import SIGNAL_EXIT_TRAVERSED from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import ansi, create, funcparser, logger, search -from evennia.utils.utils import (class_from_module, compress_whitespace, dbref, - is_iter, iter_to_str, lazy_property, - make_iter, to_str, variable_from_module) +from evennia.utils.utils import ( + class_from_module, + compress_whitespace, + dbref, + is_iter, + iter_to_str, + lazy_property, + make_iter, + to_str, + variable_from_module, +) _INFLECT = inflect.engine() _MULTISESSION_MODE = settings.MULTISESSION_MODE @@ -1425,7 +1434,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): return [ obj for obj in obj_list - if obj != looker and (obj.access(looker, "view") and obj.access(looker, "search", default=True)) + if obj != looker + and (obj.access(looker, "view") and obj.access(looker, "search", default=True)) ] # name and return_appearance hooks diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 60ed76ab03..7578da9e52 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -642,6 +642,8 @@ def send_instruction(operation, arguments, callback=None, errback=None): """ global AMP_CONNECTION, REACTOR_RUN + # print("launcher: Sending to portal: {} + {}".format(ord(operation), arguments)) + if None in (AMP_HOST, AMP_PORT, AMP_INTERFACE): print(ERROR_AMP_UNCONFIGURED) sys.exit() diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index 67fbd1d1e7..007bddd7cb 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -197,8 +197,6 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): if process and not _is_windows(): # avoid zombie-process on Unix/BSD process.wait() - # unset the reset-mode flag on the portal - self.factory.portal.server_restart_mode = None return def wait_for_disconnect(self, callback, *args, **kwargs): @@ -232,11 +230,18 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): """ if mode == "reload": - self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRELOAD) + self.send_AdminPortal2Server( + amp.DUMMYSESSION, operation=amp.SRELOAD, server_restart_mode=mode + ) elif mode == "reset": - self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET) + self.send_AdminPortal2Server( + amp.DUMMYSESSION, operation=amp.SRESET, server_restart_mode=mode + ) elif mode == "shutdown": - self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD) + self.send_AdminPortal2Server( + amp.DUMMYSESSION, operation=amp.SSHUTD, server_restart_mode=mode + ) + # store the mode for use once server comes back up again self.factory.portal.server_restart_mode = mode # sending amp data @@ -326,7 +331,6 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): _, server_connected, _, _, _, _ = self.get_status() # logger.log_msg("Evennia Launcher->Portal operation %s:%s received" % (ord(operation), arguments)) - # logger.log_msg("operation == amp.SSTART: {}: {}".format(operation == amp.SSTART, amp.loads(arguments))) if operation == amp.SSTART: # portal start #15 @@ -405,11 +409,11 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): sessid, kwargs = self.data_in(packed_data) - # logger.log_msg("Evennia Server->Portal admin data %s:%s received" % (sessid, kwargs)) - operation = kwargs.pop("operation") portal_sessionhandler = evennia.PORTAL_SESSION_HANDLER + # logger.log_msg(f"Evennia Server->Portal admin data operation {ord(operation)}") + if operation == amp.SLOGIN: # server_session_login # a session has authenticated; sync it. session = portal_sessionhandler.get(sessid) @@ -427,22 +431,28 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason")) elif operation == amp.SRELOAD: # server reload + # set up callback to restart server once it has disconnected self.factory.server_connection.wait_for_disconnect( self.start_server, self.factory.portal.server_twistd_cmd ) + # tell server to reload self.stop_server(mode="reload") elif operation == amp.SRESET: # server reset + # set up callback to restart server once it has disconnected self.factory.server_connection.wait_for_disconnect( self.start_server, self.factory.portal.server_twistd_cmd ) + # tell server to reset self.stop_server(mode="reset") elif operation == amp.SSHUTD: # server-only shutdown self.stop_server(mode="shutdown") elif operation == amp.PSHUTD: # full server+server shutdown + # set up callback to shut down portal once server has disconnected self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown) + # tell server to shut down self.stop_server(mode="shutdown") elif operation == amp.PSYNC: # portal sync @@ -451,6 +461,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): self.factory.portal.server_process_id = kwargs.get("spid", None) # this defaults to 'shutdown' or whatever value set in server_stop server_restart_mode = self.factory.portal.server_restart_mode + # print("Server has connected. Sending session data to Server ... mode: {}".format(server_restart_mode)) sessdata = evennia.PORTAL_SESSION_HANDLER.get_all_sync_data() self.send_AdminPortal2Server( @@ -461,6 +472,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): portal_start_time=self.factory.portal.start_time, ) evennia.PORTAL_SESSION_HANDLER.at_server_connection() + self.factory.portal.server_restart_mode = None if self.factory.server_connection: # this is an indication the server has successfully connected, so @@ -480,7 +492,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): ) # set a flag in case we are about to shut down soon - self.factory.server_restart_mode = True + self.factory.server_restart_mode = "shutdown" elif operation == amp.SCONN: # server_force_connection (for irc/etc) portal_sessionhandler.server_connect(**kwargs) diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 6175ada54f..9101fdcac6 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -44,6 +44,7 @@ import re from django.conf import settings from django.utils.translation import gettext as _ + from evennia import CmdSet from evennia.commands import cmdhandler from evennia.utils import dedent, fill, is_iter, justify, logger, to_str, utils From c8d75665d271e79f21ad9998a788c81cfea357c8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Apr 2024 14:24:21 +0200 Subject: [PATCH 63/68] Fix doc issues. Resolve #3479, #3480, #3481, #3482 --- CHANGELOG.md | 1 + .../Part3/Beginner-Tutorial-Characters.md | 68 ++++++------------- .../Part3/Beginner-Tutorial-Objects.md | 6 +- 3 files changed, 24 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2dc126cf..615450ee26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ a reload (regression). - [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe the hooks called on server start/stop/reload (Griatch) +- [Docs] Doc typo fixes (Griatch, chiizujin) [pull3438]: https://github.com/evennia/evennia/pull/3446 [pull3485]: https://github.com/evennia/evennia/pull/3485 diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md index eb1576452e..e9ebe2826a 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md @@ -1,13 +1,11 @@ # Player Characters -In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some -assumptions about the "Player Character" entity: +In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some assumptions about the "Player Character" entity: - It should store Abilities on itself as `character.strength`, `character.constitution` etc. - It should have a `.heal(amount)` method. -So we have some guidelines of how it should look! A Character is a database entity with values that -should be able to be changed over time. It makes sense to base it off Evennia's +So we have some guidelines of how it should look! A Character is a database entity with values that should be able to be changed over time. It makes sense to base it off Evennia's [DefaultCharacter Typeclass](../../../Components/Typeclasses.md). The Character class is like a 'character sheet' in a tabletop RPG, it will hold everything relevant to that PC. @@ -16,8 +14,7 @@ RPG, it will hold everything relevant to that PC. Player Characters (PCs) are not the only "living" things in our world. We also have _NPCs_ (like shopkeepers and other friendlies) as well as _monsters_ (mobs) that can attack us. -In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs, -we could use a class inheritance like this: +In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs, we could use a class inheritance like this: ```python from evennia import DefaultCharacter @@ -34,9 +31,7 @@ class EvAdventureMob(EvAdventureNPC): All code we put on the `Character` class would now be inherited to `NPC` and `Mob` automatically. -However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are -simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from -PCs like this: +However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from PCs like this: ```python from evennia import DefaultCharacter @@ -60,8 +55,7 @@ Nevertheless, there are some things that _should_ be common for all 'living thin - All can loot their fallen foes. - All can get looted when defeated. -We don't want to code this separately for every class but we no longer have a common parent -class to put it on. So instead we'll use the concept of a _mixin_ class: +We don't want to code this separately for every class but we no longer have a common parent class to put it on. So instead we'll use the concept of a _mixin_ class: ```python from evennia import DefaultCharacter @@ -83,10 +77,7 @@ class EvAdventureMob(LivingMixin, EvadventureNPC): In [evennia/contrib/tutorials/evadventure/characters.py](../../../api/evennia.contrib.tutorials.evadventure.characters.md) is an example of a character class structure. ``` -Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some -extra functionality all living things should be able to do. This is an example of -_multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance -since it can also get confusing to follow the code. +Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some extra functionality all living things should be able to do. This is an example of _multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance since it can also get confusing to follow the code. ## Living mixin class @@ -178,7 +169,6 @@ Most of these are empty since they will behave differently for characters and np Once we create more of our game, we will need to remember to actually call these hook methods so they serve a purpose. For example, once we implement combat, we must remember to call `at_attacked` as well as the other methods involving taking damage, getting defeated or dying. - ## Character class We will now start making the basic Character class, based on what we need from _Knave_. @@ -234,8 +224,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): We make an assumption about our rooms here - that they have a property `.allow_death`. We need to make a note to actually add such a property to rooms later! -In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset. -The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible on every character in several ways: +In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset. The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible on every character in several ways: - As `character.strength` - As `character.db.strength` @@ -249,7 +238,7 @@ We implement the Player Character versions of `at_defeat` and `at_death`. We als ### Funcparser inlines -This piece of code is worth some more explanation: +This piece of code in the `at_defeat` method above is worth some more extra explanation: ```python self.location.msg_contents( @@ -259,8 +248,7 @@ self.location.msg_contents( Remember that `self` is the Character instance here. So `self.location.msg_contents` means "send a message to everything inside my current location". In other words, send a message to everyone in the same place as the character. -The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that -execute in the string. The resulting string may look different for different audiences. The `$You()` inline function will use `from_obj` to figure out who 'you' are and either show your name or 'You'. The `$conj()` (verb conjugator) will tweak the (English) verb to match. +The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that execute in the string. The resulting string may look different for different audiences. The `$You()` inline function will use `from_obj` to figure out who 'you' are and either show your name or 'You'. The `$conj()` (verb conjugator) will tweak the (English) verb to match. - You will see: `"You collapse in a heap, alive but beaten."` - Others in the room will see: `"Thomas collapses in a heap, alive but beaten."` @@ -303,10 +291,7 @@ You can easily make yourself an `EvAdventureCharacter` in-game by using the You can now do `examine self` to check your type updated. -If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia -uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating -Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is, -the `Character` class in `mygame/typeclasses/characters.py`). +If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is, the `Character` class in `mygame/typeclasses/characters.py`). There are thus two ways to weave your new Character class into Evennia: @@ -327,8 +312,7 @@ instead. > Create a new module `mygame/evadventure/tests/test_characters.py` -For testing, we just need to create a new EvAdventure character and check -that calling the methods on it doesn't error out. +For testing, we just need to create a new EvAdventure character and check that calling the methods on it doesn't error out. ```python # mygame/evadventure/tests/test_characters.py @@ -368,22 +352,18 @@ class TestCharacters(BaseEvenniaTest): # tests for other methods ... ``` -If you followed the previous lessons, these tests should look familiar. Consider adding -tests for other methods as practice. Refer to previous lessons for details. +If you followed the previous lessons, these tests should look familiar. Consider adding tests for other methods as practice. Refer to previous lessons for details. For running the tests you do: - evennia test --settings settings.py .evadventure.tests.test_character + evennia test --settings settings.py .evadventure.tests.test_characters ## About races and classes -_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with -_races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd -add these functions. +_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with _races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd add these functions. -In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as -an Attribute on your Character: +In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as an Attribute on your Character: ```python # mygame/evadventure/characters.py @@ -399,8 +379,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): charrace = AttributeProperty("Human") ``` -We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming -`race` as `charrace` thus matches in style. +We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming `race` as `charrace` thus matches in style. We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later [character generation](./Beginner-Tutorial-Chargen.md) to check and include what these classes mean. @@ -409,23 +388,16 @@ We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and l ## Summary -With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look -like under _Knave_. +With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look like under _Knave_. -For now, we only have bits and pieces and haven't been testing this code in-game. But if you want -you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run -the command +For now, we only have bits and pieces and haven't been testing this code in-game. But if you want you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run the command type self = evadventure.characters.EvAdventureCharacter -If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`. -Check out your strength with +If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`. Check out your strength with py self.strength = 3 ```{important} -When doing `ex self` you will _not_ see all your Abilities listed yet. That's because -Attributes added with `AttributeProperty` are not available until they have been accessed at -least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from -then on. +When doing `ex self` you will _not_ see all your Abilities listed yet. That's because Attributes added with `AttributeProperty` are not available until they have been accessed at least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from then on. ``` diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md index a1ffb51526..449367eb0f 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md @@ -92,7 +92,7 @@ class EvAdventureObject(DefaultObject): """The top of the description""" return "" - def get_display_desc(self, looker, **kwargs) + def get_display_desc(self, looker, **kwargs): """The main display - show object stats""" return get_obj_stats(self, owner=looker) @@ -216,7 +216,7 @@ class EvAdventureConsumable(EvAdventureObject): """Called when using the item""" pass - def at_post_use(self. user, *args, **kwargs): + def at_post_use(self, user, *args, **kwargs): """Called after using the item""" # detract a usage, deleting the item if used up. self.uses -= 1 @@ -452,7 +452,7 @@ _BARE_HANDS = None # ... -class WeaponBareHands(EvAdventureWeapon) +class WeaponBareHands(EvAdventureWeapon): obj_type = ObjType.WEAPON inventory_use_slot = WieldLocation.WEAPON_HAND attack_type = Ability.STR From b8e37f9cf27a012dafbed1fa8a01f3ab7fef1077 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Apr 2024 22:13:06 +0200 Subject: [PATCH 64/68] Fix AttributeProperty access with mutable default. Resolve #3488 --- CHANGELOG.md | 6 +- evennia/objects/tests.py | 108 ++++++++++++++++++++++++++++-- evennia/typeclasses/attributes.py | 42 ++++++------ 3 files changed, 129 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 615450ee26..37beca050e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,11 @@ - [Fix][pull3485]: Typo in `sethome` message (chiizujin) - [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no arguments (InspectorCaracal) -- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization - (Griatch) +- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch) - [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on a reload (regression). +- [Fix][issue3488]: `AttributeProperty(, autocreate=False)`, where + `` was mutable would not update/save properly in-place (Griatch) - [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe the hooks called on server start/stop/reload (Griatch) - [Docs] Doc typo fixes (Griatch, chiizujin) @@ -27,6 +28,7 @@ [pull3487]: https://github.com/evennia/evennia/pull/3487 [issue3476]: https://github.com/evennia/evennia/issues/3476 [issue3477]: https://github.com/evennia/evennia/issues/3477 +[issue3488]: https://github.com/evennia/evennia/issues/3488 [doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index de23269cdf..2d54f51012 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -3,12 +3,8 @@ from unittest import skip from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom from evennia.objects.models import ObjectDB from evennia.typeclasses.attributes import AttributeProperty -from evennia.typeclasses.tags import ( - AliasProperty, - PermissionProperty, - TagCategoryProperty, - TagProperty, -) +from evennia.typeclasses.tags import (AliasProperty, PermissionProperty, + TagCategoryProperty, TagProperty) from evennia.utils import create, search from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase @@ -356,6 +352,10 @@ class TestObjectPropertiesClass(DefaultObject): attr2 = AttributeProperty(default="attr2", category="attrcategory") attr3 = AttributeProperty(default="attr3", autocreate=False) attr4 = SubAttributeProperty(default="attr4") + attr5 = AttributeProperty(default=list, autocreate=False) + attr6 = AttributeProperty(default=[None], autocreate=False) + attr7 = AttributeProperty(default=list) + attr8 = AttributeProperty(default=[None]) cusattr = CustomizedProperty(default=5) tag1 = TagProperty() tag2 = TagProperty(category="tagcategory") @@ -541,3 +541,99 @@ class TestProperties(EvenniaTestCase): obj1.delete() obj2.delete() + + def test_not_create_attribute_with_autocreate_false(self): + """ + Test that AttributeProperty with autocreate=False does not create an attribute in the database. + + """ + obj = create.create_object(TestObjectPropertiesClass, key="obj1") + + self.assertEqual(obj.attr3, "attr3") + self.assertEqual(obj.attributes.get("attr3"), None) + + self.assertEqual(obj.attr5, []) + self.assertEqual(obj.attributes.get("attr5"), None) + + obj.delete() + + def test_callable_defaults__autocreate_false(self): + """ + Test https://github.com/evennia/evennia/issues/3488, where a callable default value like `list` + would produce an infinitely empty result even when appended to. + + """ + obj1 = create.create_object(TestObjectPropertiesClass, key="obj1") + obj2 = create.create_object(TestObjectPropertiesClass, key="obj2") + + self.assertEqual(obj1.attr5, []) + obj1.attr5.append(1) + self.assertEqual(obj1.attr5, [1]) + + # check cross-instance sharing + self.assertEqual(obj2.attr5, [], "cross-instance sharing detected") + + + def test_mutable_defaults__autocreate_false(self): + """ + Test https://github.com/evennia/evennia/issues/3488, where a mutable default value (like a + list `[]` or `[None]`) would not be updated in the database when appended to. + + Note that using a mutable default value is not recommended, as the mutable will share the + same memory space across all instances of the class. This means that if one instance modifiesA + the mutable, all instances will be affected. + + """ + obj1 = create.create_object(TestObjectPropertiesClass, key="obj1") + obj2 = create.create_object(TestObjectPropertiesClass, key="obj2") + + self.assertEqual(obj1.attr6, [None]) + obj1.attr6.append(1) + self.assertEqual(obj1.attr6, [None, 1]) + + obj1.attr6[1] = 2 + self.assertEqual(obj1.attr6, [None, 2]) + + # check cross-instance sharing + self.assertEqual(obj2.attr6, [None], "cross-instance sharing detected") + + obj1.delete() + obj2.delete() + + def test_callable_defaults__autocreate_true(self): + """ + Test callables with autocreate=True. + + """ + obj1 = create.create_object(TestObjectPropertiesClass, key="obj1") + obj2 = create.create_object(TestObjectPropertiesClass, key="obj1") + + self.assertEqual(obj1.attr7, []) + obj1.attr7.append(1) + self.assertEqual(obj1.attr7, [1]) + + # check cross-instance sharing + self.assertEqual(obj2.attr7, []) + + + def test_mutable_defaults__autocreate_true(self): + """ + Test mutable defaults with autocreate=True. + + """ + obj1 = create.create_object(TestObjectPropertiesClass, key="obj1") + obj2 = create.create_object(TestObjectPropertiesClass, key="obj2") + + self.assertEqual(obj1.attr8, [None]) + obj1.attr8.append(1) + self.assertEqual(obj1.attr8, [None, 1]) + + obj1.attr8[1] = 2 + self.assertEqual(obj1.attr8, [None, 2]) + + # check cross-instance sharing + self.assertEqual(obj2.attr8, [None]) + + obj1.delete() + obj2.delete() + diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 5aedc03262..bdef3bdc33 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -12,11 +12,11 @@ which is a non-db version of Attributes. import fnmatch import re from collections import defaultdict +from copy import copy from django.conf import settings from django.db import models from django.utils.encoding import smart_str - from evennia.locks.lockhandler import LockHandler from evennia.utils.dbserialize import from_pickle, to_pickle from evennia.utils.idmapper.models import SharedMemoryModel @@ -166,6 +166,7 @@ class AttributeProperty: """ attrhandler_name = "attributes" + cached_default_name_template = "_property_attribute_default_{key}" def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=True): """ @@ -207,21 +208,6 @@ class AttributeProperty: self._autocreate = autocreate self._key = "" - @property - def _default(self): - """ - Tries returning a new instance of default if callable. - - """ - if callable(self.__default): - return self.__default() - - return self.__default - - @_default.setter - def _default(self, value): - self.__default = value - def __set_name__(self, cls, name): """ Called when descriptor is first assigned to the class. It is called with @@ -230,17 +216,35 @@ class AttributeProperty: """ self._key = name + def _get_and_cache_default(self, instance): + """ + Get and cache the default value for this attribute. We make sure to convert any mutables + into _Saver* equivalent classes here and cache the result on the instance's AttributeHandler. + + """ + attrhandler = getattr(instance, self.attrhandler_name) + value = getattr(attrhandler, self.cached_default_name_template.format(key=self._key), None) + if not value: + if callable(self._default): + value = self._default() + else: + value = copy(self._default) + value = from_pickle(value, db_obj=instance) + setattr(attrhandler, self.cached_default_name_template.format(key=self._key), value) + return value + def __get__(self, instance, owner): """ Called when the attrkey is retrieved from the instance. """ - value = self._default + value = self._get_and_cache_default(instance) + try: value = self.at_get( getattr(instance, self.attrhandler_name).get( key=self._key, - default=self._default, + default=value, category=self._category, strattr=self._strattr, raise_exception=self._autocreate, @@ -250,7 +254,7 @@ class AttributeProperty: except AttributeError: if self._autocreate: # attribute didn't exist and autocreate is set - self.__set__(instance, self._default) + self.__set__(instance, value) else: raise return value From e3ddbf08cf1558fe9a354fa647dd4dfc240bc929 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Apr 2024 22:42:01 +0200 Subject: [PATCH 65/68] Fix Component contrib issue with corrected AttributeProperty --- .../contrib/base_systems/components/component.py | 16 ++++++++++++++++ .../contrib/base_systems/components/dbfield.py | 3 ++- evennia/contrib/base_systems/components/tests.py | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index 48c7dfa738..6397b77a07 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -155,6 +155,22 @@ class Component(metaclass=BaseComponent): """ return self.host.attributes + @property + def pk(self): + """ + Shortcut property returning the host's primary key. + + Returns: + int: The Host's primary key. + + Notes: + This is requried to allow AttributeProperties to correctly update `_SaverMutable` data + (like lists) in-place (since the DBField sits on the Component which doesn't itself + have a primary key, this save operation would otherwise fail). + + """ + return self.host.pk + @property def nattributes(self): """ diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 4b9c6d4fa8..67f812b484 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -6,7 +6,8 @@ This file contains the Descriptors used to set Fields in Components import typing -from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty +from evennia.typeclasses.attributes import (AttributeProperty, + NAttributeProperty) if typing.TYPE_CHECKING: from .components import Component diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index b11ce68937..75e169b825 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -268,7 +268,7 @@ class TestComponents(EvenniaTest): def test_mutables_are_not_shared_when_autocreate(self): self.char1.test_a.my_list.append(1) - self.assertNotEqual(self.char1.test_a.my_list, self.char2.test_a.my_list) + self.assertNotEqual(id(self.char1.test_a.my_list), id(self.char2.test_a.my_list)) def test_replacing_class_component_slot_with_runtime_component(self): self.char1.components.add_default("replacement_inherited_test_a") From 92d964476cd51eea9c92558510c85867ccdbe6fa Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Apr 2024 23:00:49 +0200 Subject: [PATCH 66/68] Typos in Server-Lifecycle doc --- docs/source/Concepts/Server-Lifecycle.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/source/Concepts/Server-Lifecycle.md b/docs/source/Concepts/Server-Lifecycle.md index 2cc8a5a017..274d0e1d63 100644 --- a/docs/source/Concepts/Server-Lifecycle.md +++ b/docs/source/Concepts/Server-Lifecycle.md @@ -5,23 +5,22 @@ As part of your game design you may want to change how Evennia behaves when star Evennia has three main life cycles, all of which you can add custom behavior for: -- **Database life cycle**: Evennia runs on top of one several [supported databases](../Setup/Choosing-a-Database.md). It's possible to wipe this database to 'start over' without needing to separately download Evennia anew. -- **Reboot life cycle**: From When Evennia starts from zero, to it being fully shut down, which means both Portal and Server are stopped. This kicks all players. -- **Reload life cycle:** This is the main runtime, until a "reload" event. Reloads do not kick any players. +- **Database life cycle**: Evennia uses a database. This exists in parallel to the code changes you do. The database exists until you choose to reset or delete it. Doing so doesn't require re-downloading Evennia. +- **Reboot life cycle**: From When Evennia starts to it being fully shut down, which means both Portal and Server are stopped. At the end of this cycle, all players are disconnected. +- **Reload life cycle:** This is the main runtime, until a "reload" event. Reloads refreshes game code but do not kick any players. -## When Evennia first starts +## When Evennia starts for the first time -This is the beginning of the **Database life cycle**, just after the database is created and migrated for the first time. After that it won't run again unless you wipe the database and initialize a new one (see [Choosing a Database](../Setup/Choosing-a-Database.md)). +This is the beginning of the **Database life cycle**, just after the database is created and migrated for the first time (or after it was deleted and re-built). See [Choosing a Database](../Setup/Choosing-a-Database.md) for instructions on how to reset a database, should you want to re-run this sequence after the first time. Hooks called, in sequence: -1. `evennia.server.initial_setup.handle_setup(last_step=None)`: Evennia's core initialization function. This is what creates the #1 Character (tied to the superuser) and Limbo. It calls the next hook below and also understands to restart at the last failed step if there was some issue. You should normally not override this function unless you _really_ know what you are doing, but in that case you'd do so by setting `settings.INITIAL_SETUP_MODULE` to your own module with a `handle_setup` function in it. +1. `evennia.server.initial_setup.handle_setup(last_step=None)`: Evennia's core initialization function. This is what creates the #1 Character (tied to the superuser account) and `Limbo` room. It calls the next hook below and also understands to restart at the last failed step if there was some issue. You should normally not override this function unless you _really_ know what you are doing. To override, change `settings.INITIAL_SETUP_MODULE` to your own module with a `handle_setup` function in it. 2. `mygame/server/conf/at_initial_setup.py` contains a single function, `at_initial_setup()`, which will be called without arguments. It's called last in the setup sequence by the above function. Use this to add your own custom behavior or to tweak the initialization. If you for example wanted to change the auto-generated Limbo room, you should do it from here. If you want to change where this function is found, you can do so by changing `settings.AT_INITIAL_SETUP_HOOK_MODULE`. - ## When Evennia starts and shutdowns -This is the **Reboot life cycle**. Evennia consists of two main processes, the [Portal and the Server](../Components/Portal-And-Server.md). On a reboot or shutdown, both Portal and Server shuts down, which means all players are disconnected. +This is part of the **Reboot life cycle**. Evennia consists of two main processes, the [Portal and the Server](../Components/Portal-And-Server.md). On a reboot or shutdown, both Portal and Server shuts down, which means all players are disconnected. Each process call a series of hooks located in `mygame/server/conf/at_server_startstop.py`. You can customize the module used with `settings.AT_SERVER_STARTSTOP_MODULE` - this can even be a list of modules, if so, the appropriately-named functions will be called from each module, in sequence. From f823e367f0d71a1d39e8f146919b5447bca61526 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Apr 2024 23:07:10 +0200 Subject: [PATCH 67/68] Evennia 4.1.1 patch release --- CHANGELOG.md | 4 +++- docs/source/Coding/Changelog.md | 11 ++++++++--- evennia/VERSION.txt | 2 +- pyproject.toml | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37beca050e..b603ec36ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog -## Main branch +## Evennia 4.1.1 + +April 6, 2024 - [Fix][pull3438]: Error with 'you' mapping in third-person style of `msg_contents` (InspectorCaracal) diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 3f2dc126cf..b603ec36ca 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -1,6 +1,8 @@ # Changelog -## Main branch +## Evennia 4.1.1 + +April 6, 2024 - [Fix][pull3438]: Error with 'you' mapping in third-person style of `msg_contents` (InspectorCaracal) @@ -14,18 +16,21 @@ - [Fix][pull3485]: Typo in `sethome` message (chiizujin) - [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no arguments (InspectorCaracal) -- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization - (Griatch) +- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch) - [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on a reload (regression). +- [Fix][issue3488]: `AttributeProperty(, autocreate=False)`, where + `` was mutable would not update/save properly in-place (Griatch) - [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe the hooks called on server start/stop/reload (Griatch) +- [Docs] Doc typo fixes (Griatch, chiizujin) [pull3438]: https://github.com/evennia/evennia/pull/3446 [pull3485]: https://github.com/evennia/evennia/pull/3485 [pull3487]: https://github.com/evennia/evennia/pull/3487 [issue3476]: https://github.com/evennia/evennia/issues/3476 [issue3477]: https://github.com/evennia/evennia/issues/3477 +[issue3488]: https://github.com/evennia/evennia/issues/3488 [doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html diff --git a/evennia/VERSION.txt b/evennia/VERSION.txt index ee74734aa2..627a3f43a6 100644 --- a/evennia/VERSION.txt +++ b/evennia/VERSION.txt @@ -1 +1 @@ -4.1.0 +4.1.1 diff --git a/pyproject.toml b/pyproject.toml index 17a9871c10..7c507aada5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evennia" -version = "4.1.0" +version = "4.1.1" maintainers = [{ name = "Griatch", email = "griatch@gmail.com" }] description = "A full-featured toolkit and server for text-based multiplayer games (MUDs, MU*, etc)." requires-python = ">=3.10" From 6dd9442cef2fe59eb5ac2f53b4a5b18729ba6b4d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Apr 2024 23:16:41 +0200 Subject: [PATCH 68/68] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b603ec36ca..f92cdab5ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Main branch + +- [Feature][pull3470]: New `exit_order` kwarg to + `DefaultObject.get_display_exits` to easier customize the order in which + standard exits are displayed in a room (chiizujin) + +[pull3470]: https://github.com/evennia/evennia/pull/3470 + ## Evennia 4.1.1 April 6, 2024