Fix evadventure QuestHandler initialization. Resolve #3560

This commit is contained in:
Griatch 2026-02-15 11:40:54 +01:00
parent 36ee67ef16
commit cc25051e8f
4 changed files with 40 additions and 15 deletions

View file

@ -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

View file

@ -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_<current_step>(*args, **kwargs)` methods), which is something we'll get to later, in [Part 4](../Part4/Beginner-Tutorial-Part4-Overview.md) of this tutorial.
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_<current_step>(*args, **kwargs)` methods), which is something we'll get to later, in [Part 4](../Part4/Beginner-Tutorial-Part4-Overview.md) of this tutorial.

View file

@ -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):

View file

@ -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()