From cc25051e8ff71ec53f306bebb91bbb4764fc73e4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 15 Feb 2026 11:40:54 +0100 Subject: [PATCH] Fix evadventure QuestHandler initialization. Resolve #3560 --- CHANGELOG.md | 2 ++ .../Part3/Beginner-Tutorial-Quests.md | 24 ++++++++++--------- .../contrib/tutorials/evadventure/quests.py | 9 +++---- .../evadventure/tests/test_quests.py | 20 ++++++++++++++++ 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5441d1b0a..7254e17853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - [Fix][issue3513]: Fixed issue where OnDemandHandler could traceback on an un-pickle-able object and cause an error at server shutdown (Griatch) - [Fix][issue3649]: The `:j` command in EvEditor would squash empty lines (Griatch) +- [Fix][issue3560]: Tutorial QuestHandler failed to load after server restart (Griatch) - [Doc][pull3801]: Move Evennia doc build system to latest Sphinx/myST (PowershellNinja, also honorary mention to electroglyph) - [Doc][pull3800]: Describe support for Telnet SSH in HAProxy documentation (holl0wstar) @@ -65,6 +66,7 @@ [issue3858]: https://github.com/evennia/evennia/issues/3858 [issue3813]: https://github.com/evennia/evennia/issues/3513 [issue3649]: https://github.com/evennia/evennia/issues/3649 +[issue3560]: https://github.com/evennia/evennia/issues/3560 ## Evennia 5.0.1 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 91ff51f7ce..a190899b86 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md @@ -14,7 +14,7 @@ A quest follows a specific development: 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). +An example implementation of quests is found under `evennia/contrib/tutorials`, in [evadventure/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. @@ -32,7 +32,7 @@ We saw the implementation of an on-object handler back in the [lesson about NPC ```{code-block} python :linenos: -:emphasize-lines: 9,10,11,14-18,21,24-28 +:emphasize-lines: 9,10,13-17,20,23-27 # in evadventure/quests.py class EvAdventureQuestHandler: @@ -53,7 +53,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.quests[quest_key] = quest_class(self.obj, questhandler=self) def _save(self): self.obj.attributes.add( @@ -70,7 +70,7 @@ class EvAdventureQuestHandler: def add(self, quest_class): self.quest_classes[quest_class.key] = quest_class - self.quests[quest_class.key] = quest_class(self.obj) + self.quests[quest_class.key] = quest_class(self.obj, questhandler=self) self._save() def remove(self, quest_key): @@ -173,7 +173,7 @@ We also need a way to represent the quests themselves though! ```{code-block} python :linenos: -:emphasize-lines: 7,12,13,34-36 +:emphasize-lines: 7,10-14,17,24,31 # in evadventure/quests.py # ... @@ -183,9 +183,10 @@ class EvAdventureQuest: key = "base-quest" desc = "Base quest" start_step = "start" - - def __init__(self, quester): + + def __init__(self, quester, questhandler=None): self.quester = quester + self._questhandler = questhandler self.data = self.questhandler.load_quest_data(self.key) self._current_step = self.get_data("current_step") @@ -205,7 +206,7 @@ class EvAdventureQuest: @property def questhandler(self): - return self.quester.quests + return self._questhandler if self._questhandler else self.quester.quests @property def current_step(self): @@ -220,9 +221,10 @@ class EvAdventureQuest: - **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. +- **Line 13**: The handler is also passed in during loading, so this quest instance can use it directly without triggering recursion during lazy loading. +- **Lines 17, 24 and 31**: `add_data` and `remove_data` call back to `questhandler.save_quest_data` so persistence happens in one place. -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 `add/get/remove_data` methods are convenient wrappers for getting data in and out of the database. 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". @@ -403,4 +405,4 @@ Testing of the quests means creating a test character, making a dummy quest, add ## 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 +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. diff --git a/evennia/contrib/tutorials/evadventure/quests.py b/evennia/contrib/tutorials/evadventure/quests.py index e4ec77372f..62257455d1 100644 --- a/evennia/contrib/tutorials/evadventure/quests.py +++ b/evennia/contrib/tutorials/evadventure/quests.py @@ -63,8 +63,9 @@ class EvAdventureQuest: help_start = "You need to start first" help_end = "You need to end the quest" - def __init__(self, quester): + def __init__(self, quester, questhandler=None): self.quester = quester + self._questhandler = questhandler self.data = self.questhandler.load_quest_data(self.key) self._current_step = self.get_data("current_step") @@ -110,7 +111,7 @@ class EvAdventureQuest: @property def questhandler(self): - return self.quester.quests + return self._questhandler if self._questhandler else self.quester.quests @property def current_step(self): @@ -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.quests[quest_key] = quest_class(self.obj, questhandler=self) def _save(self): self.obj.attributes.add( @@ -312,7 +313,7 @@ class EvAdventureQuestHandler: """ self.quest_classes[quest_class.key] = quest_class - self.quests[quest_class.key] = quest_class(self.obj) + self.quests[quest_class.key] = quest_class(self.obj, questhandler=self) self._save() def remove(self, quest_key): diff --git a/evennia/contrib/tutorials/evadventure/tests/test_quests.py b/evennia/contrib/tutorials/evadventure/tests/test_quests.py index 675cf3d498..d0496dc515 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_quests.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_quests.py @@ -5,6 +5,7 @@ Testing Quest functionality. from unittest.mock import MagicMock +from evennia.utils import create from evennia.utils.test_resources import BaseEvenniaTest from .. import quests @@ -146,3 +147,22 @@ class EvAdventureQuestTest(EvAdventureMixin, BaseEvenniaTest): quest.progress() self.assertEqual(quest.current_step, "C") # still on last step self.assertEqual(quest.is_completed, True) + + def test_questhandler_reload_after_restart(self): + """ + Test #3560: quest handler should reload and keep quest data after restart. + """ + quest = self._get_quest() + questgiver = create.create_object( + EvAdventureObject, key="questgiver", location=self.location + ) + quest.add_data("client", questgiver) + + # simulate server restart by clearing lazy-property cache + self.character.__dict__.pop("quests", None) + + reloaded_quest = self.character.quests.get(_TestQuest.key) + self.assertIsNotNone(reloaded_quest) + self.assertEqual(reloaded_quest.get_data("client"), questgiver) + + questgiver.delete()