From ca6456b134fd930c724427968812aa4c3613c786 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 12 Sep 2022 20:15:00 +0200 Subject: [PATCH 1/4] Clarify the rplanguage docstring a bit --- evennia/contrib/rpg/rpsystem/rplanguage.py | 179 ++++++++++++--------- 1 file changed, 101 insertions(+), 78 deletions(-) diff --git a/evennia/contrib/rpg/rpsystem/rplanguage.py b/evennia/contrib/rpg/rpsystem/rplanguage.py index 4c47fc63b7..4be5782acc 100644 --- a/evennia/contrib/rpg/rpsystem/rplanguage.py +++ b/evennia/contrib/rpg/rpsystem/rplanguage.py @@ -17,110 +17,133 @@ in the game in various ways: overhear (for example "s" sounds tend to be audible even when no other meaning can be determined). -Usage: +## Usage - ```python - from evennia.contrib import rplanguage +```python +from evennia.contrib import rplanguage - # need to be done once, here we create the "default" lang - rplanguage.add_language() +# need to be done once, here we create the "default" lang +rplanguage.add_language() - say = "This is me talking." - whisper = "This is me whispering. +say = "This is me talking." +whisper = "This is me whispering. - print rplanguage.obfuscate_language(say, level=0.0) - <<< "This is me talking." - print rplanguage.obfuscate_language(say, level=0.5) - <<< "This is me byngyry." - print rplanguage.obfuscate_language(say, level=1.0) - <<< "Daly ly sy byngyry." +print rplanguage.obfuscate_language(say, level=0.0) +<<< "This is me talking." +print rplanguage.obfuscate_language(say, level=0.5) +<<< "This is me byngyry." +print rplanguage.obfuscate_language(say, level=1.0) +<<< "Daly ly sy byngyry." - result = rplanguage.obfuscate_whisper(whisper, level=0.0) - <<< "This is me whispering" - result = rplanguage.obfuscate_whisper(whisper, level=0.2) - <<< "This is m- whisp-ring" - result = rplanguage.obfuscate_whisper(whisper, level=0.5) - <<< "---s -s -- ---s------" - result = rplanguage.obfuscate_whisper(whisper, level=0.7) - <<< "---- -- -- ----------" - result = rplanguage.obfuscate_whisper(whisper, level=1.0) - <<< "..." +result = rplanguage.obfuscate_whisper(whisper, level=0.0) +<<< "This is me whispering" +result = rplanguage.obfuscate_whisper(whisper, level=0.2) +<<< "This is m- whisp-ring" +result = rplanguage.obfuscate_whisper(whisper, level=0.5) +<<< "---s -s -- ---s------" +result = rplanguage.obfuscate_whisper(whisper, level=0.7) +<<< "---- -- -- ----------" +result = rplanguage.obfuscate_whisper(whisper, level=1.0) +<<< "..." - ``` +``` - To set up new languages, import and use the `add_language()` - helper method in this module. This allows you to customize the - "feel" of the semi-random language you are creating. Especially - the `word_length_variance` helps vary the length of translated - words compared to the original and can help change the "feel" for - the language you are creating. You can also add your own - dictionary and "fix" random words for a list of input words. +## Custom languages - Below is an example of "elvish", using "rounder" vowels and sounds: +To set up new languages, you need to run `add_language()` +helper function in this module. The arguments of this function (see below) +are used to store the new language in the database (in the LanguageHandler, +which is a type of Script). - ```python - # vowel/consonant grammar possibilities - grammar = ("v vv vvc vcc vvcc cvvc vccv vvccv vcvccv vcvcvcc vvccvvcc " - "vcvvccvvc cvcvvcvvcc vcvcvvccvcvv") +If you want to remember the language definitions, you could put them all +in a module along with the `add_language` call as a quick way to +rebuild the language on a db reset: - # all not in this group is considered a consonant - vowels = "eaoiuy" +```python +# a stand-alone module somewhere under mygame. Just import this +# once to automatically add the language! - # you need a representative of all of the minimal grammars here, so if a - # grammar v exists, there must be atleast one phoneme available with only - # one vowel in it - phonemes = ("oi oh ee ae aa eh ah ao aw ay er ey ow ia ih iy " - "oy ua uh uw y p b t d f v t dh s z sh zh ch jh k " - "ng g m n l r w") +from evennia.contrib.rpg.rpsystem import rplanguage +grammar = (...) +vowels = "eaouy" +# etc - # how much the translation varies in length compared to the original. 0 is - # smallest, higher values give ever bigger randomness (including removing - # short words entirely) - word_length_variance = 1 +rplanguage.add_language(grammar=grammar, vowels=vowels, ...) +``` - # if a proper noun (word starting with capitalized letter) should be - # translated or not. If not (default) it means e.g. names will remain - # unchanged across languages. - noun_translate = False +The variables of `add_language` allows you to customize the "feel" of +the semi-random language you are creating. Especially +the `word_length_variance` helps vary the length of translated +words compared to the original. You can also add your own +dictionary and "fix" random words for a list of input words. - # all proper nouns (words starting with a capital letter not at the beginning - # of a sentence) can have either a postfix or -prefix added at all times - noun_postfix = "'la" +## Example - # words in dict will always be translated this way. The 'auto_translations' - # is instead a list or filename to file with words to use to help build a - # bigger dictionary by creating random translations of each word in the - # list *once* and saving the result for subsequent use. - manual_translations = {"the":"y'e", "we":"uyi", "she":"semi", "he":"emi", - "you": "do", 'me':'mi','i':'me', 'be':"hy'e", 'and':'y'} +Below is an example module creating "elvish", using "rounder" vowels and sounds: - rplanguage.add_language(key="elvish", phonemes=phonemes, grammar=grammar, - word_length_variance=word_length_variance, - noun_translate=noun_translate, - noun_postfix=noun_postfix, vowels=vowels, - manual_translations=manual_translations, - auto_translations="my_word_file.txt") +```python +# vowel/consonant grammar possibilities +grammar = ("v vv vvc vcc vvcc cvvc vccv vvccv vcvccv vcvcvcc vvccvvcc " + "vcvvccvvc cvcvvcvvcc vcvcvvccvcvv") - ``` +# all not in this group is considered a consonant +vowels = "eaoiuy" - This will produce a decicively more "rounded" and "soft" language - than the default one. The few manual_translations also make sure - to make it at least look superficially "reasonable". +# you need a representative of all of the minimal grammars here, so if a +# grammar v exists, there must be atleast one phoneme available with only +# one vowel in it +phonemes = ("oi oh ee ae aa eh ah ao aw ay er ey ow ia ih iy " + "oy ua uh uw y p b t d f v t dh s z sh zh ch jh k " + "ng g m n l r w") - The `auto_translations` keyword is useful, this accepts either a - list or a path to a file of words (one per line) to automatically - create fixed translations for according to the grammatical rules. - This allows to quickly build a large corpus of translated words - that never change (if this is desired). +# how much the translation varies in length compared to the original. 0 is +# smallest, higher values give ever bigger randomness (including removing +# short words entirely) +word_length_variance = 1 + +# if a proper noun (word starting with capitalized letter) should be +# translated or not. If not (default) it means e.g. names will remain +# unchanged across languages. +noun_translate = False + +# all proper nouns (words starting with a capital letter not at the beginning +# of a sentence) can have either a postfix or -prefix added at all times +noun_postfix = "'la" + +# words in dict will always be translated this way. The 'auto_translations' +# is instead a list or filename to file with words to use to help build a +# bigger dictionary by creating random translations of each word in the +# list *once* and saving the result for subsequent use. +manual_translations = {"the":"y'e", "we":"uyi", "she":"semi", "he":"emi", + "you": "do", 'me':'mi','i':'me', 'be':"hy'e", 'and':'y'} + +rplanguage.add_language(key="elvish", phonemes=phonemes, grammar=grammar, + word_length_variance=word_length_variance, + noun_translate=noun_translate, + noun_postfix=noun_postfix, vowels=vowels, + manual_translations=manual_translations, + auto_translations="my_word_file.txt") + +``` + +This will produce a decicively more "rounded" and "soft" language +than the default one. The few manual_translations also make sure +to make it at least look superficially "reasonable". + +The `auto_translations` keyword is useful, this accepts either a +list or a path to a file of words (one per line) to automatically +create fixed translations for according to the grammatical rules. +This allows to quickly build a large corpus of translated words +that never change (if this is desired). """ import re -from random import choice, randint from collections import defaultdict +from random import choice, randint + from evennia import DefaultScript from evennia.utils import logger - # ------------------------------------------------------------ # # Obfuscate language From f18d9d68b38778c1e315edb2bf76be161fd0f907 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 13 Sep 2022 12:38:51 +0200 Subject: [PATCH 2/4] Back-port handler-article from evadventure branch --- .../Howtos/Tutorial-Persistent-Handler.md | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 docs/source/Howtos/Tutorial-Persistent-Handler.md diff --git a/docs/source/Howtos/Tutorial-Persistent-Handler.md b/docs/source/Howtos/Tutorial-Persistent-Handler.md new file mode 100644 index 0000000000..cf9bd3cc6f --- /dev/null +++ b/docs/source/Howtos/Tutorial-Persistent-Handler.md @@ -0,0 +1,197 @@ +# Making a Persistent object Handler + +A _handler_ is a convenient way to group functionality on an object. This allows you to logically group all actions related to that thing in one place. This tutorial expemplifies how to make your own handlers and make sure data you store in them survives a reload. + +For example, when you do `obj.attributes.get("key")` or `obj.tags.add('tagname')` you are evoking handlers stored as `.attributes` and `tags` on the `obj`. On these handlers are methods (`get()` and `add()` in this example). + +## Base Handler example + +Here is a base way to set up an on-object handler: + +```python + +from evennia import DefaultObject, create_object +from evennia.utils.utils import lazy_property + +class NameChanger: + def __init__(self, obj): + self.obj = obj + + def add_to_key(self, suffix): + self.obj.key = f"self.obj.key_{suffix}" + +# make a test object +class MyObject(DefaultObject): + @lazy_property: + def namechange(self): + return MyHandler(self) + + +obj = create_object(MyObject, key="test") +print(obj.key) +>>> "test" +obj.namechange.add_to_key("extra") +print(obj.key) +>>> "test_extra" +``` + +What happens here is that we make a new class `MyHandler`. We use the `@lazy_property` decorator to set it up - this means the handler will not be actually created until someone really wants to use it, by accessing `obj.namechange` later. The decorated `namechange` method returns the handler and makes sure to initialize it with `self` - this becomes the `obj` inside the handler! + +We then make a silly method `add_to_key` that uses the handler to manipulate the key of the object. In this example, the handler is pretty pointless, but grouping functionality this way can both make for an easy-to-remember API and can also allow you cache data for easy access - this is how the `AttributeHandler` (`.attributes`) and `TagHandler` (`.tags`) works. + +## Persistent storage of data in handler + +Let's say we want to track 'quests' in our handler. A 'quest' is a regular class that represents the quest. Let's make it simple as an example: + +```python +# for example in mygame/world/quests.py + + +class Quest: + + key = "The quest for the red key" + + def __init__(self): + self.current_step = "start" + + def check_progress(self): + # uses self.current_step to check + # progress of this quest + getattr(self, f"step_{self.current_step}")() + + def step_start(self): + # check here if quest-step is complete + self.current_step = "find_the_red_key" + def step_find_the_red_key(self): + # check if step is complete + self.current_step = "hand_in_quest" + def step_hand_in_quest(self): + # check if handed in quest to quest giver + self.current_step = None # finished + +``` + + +We expect the dev to make subclasses of this to implement different quests. Exactly how this works doesn't matter, the key is that we want to track `self.current_step` - a property that _should survive a server reload_. But so far there is no way for `Quest` to accomplish this, it's just a normal Python class with no connection to the database. + +### Handler with save/load capability + +Let's make a `QuestHandler` that manages a character's quests. + +```python +# for example in the same mygame/world/quests.py + + +class QuestHandler: + def __init__(self, obj): + self.obj = obj + self.do_save = False + self._load() + + def _load(self): + self.storage = self.obj.attributes.get( + "quest_storage", default={}, category="quests") + + def _save(self): + self.obj.attributes.add( + "quest_storage", self.storage, category="quests") + self._load() # important + self.do_save = False + + def add(self, questclass): + self.storage[questclass.key] = questclass(self.obj) + self._save() + + def check_progress(self): + for quest in self.storage.values(): + quest.check_progress() + if self.do_save: + # .do_save is set on handler by Quest if it wants to save progress + self._save() + +``` + +The handler is just a normal Python class and has no database-storage on its own. But it has a link to `.obj`, which is assumed to be a full typeclased entity, on which we can create persistent [Attributes](../Components/Attributes.md) to store things however we like! + +We make two helper methods `_load` and +`_save` that handles local fetches and saves `storage` to an Attribute on the object. To avoid saving more than necessary, we have a property `do_save`. This we will set in `Quest` below. + +> Note that once we `_save` the data, we need to call `_load` again. This is to make sure the version we store on the handler is properly de-serialized. If you get an error about data being `bytes`, you probably missed this step. + + +### Make quests storable + +The handler will save all `Quest` objects as a `dict` in an Attribute on `obj`. We are not done yet though, the `Quest` object needs access to the `obj` too - not only will this is important to figure out if the quest is complete (the `Quest` must be able to check the quester's inventory to see if they have the red key, for example), it also allows the `Quest` to tell the handler when its state changed and it should be saved. + +We change the `Quest` such: + +```python +from evennia.utils import dbserialize + + +class Quest: + + def __init__(self, obj): + self.obj = obj + self._current_step = "start" + + def __serialize_dbobjs__(self): + self.obj = dbserialize.dbserialize(self.obj) + + def __deserialize_dbobjs__(self): + if isinstance(self.obj, bytes): + self.obj = dbserialize.dbunserialize(self.obj) + + @property + def questhandler(self): + return self.obj.quests + + @property + def current_step(self): + return self._current_step + + @current_step.setter + def current_step(self, value): + self._current_step = value + self.questhandler.do_save = True # this triggers save in handler! + + # [same as before] + +``` + +The `Quest.__init__` now takes `obj` as argument, to match what we pass to it in `QuestHandler.add`. We want to monitor the changing of `current_step`, so we make it into a `property`. When we edit that value, we set the `do_save` flag on the handler, which means it will save the status to database once it has checked progress on all its quests. + +The `__serialize__dbobjs__` and `__deserialize_dbobjs__` methods are needed because `Attributes` can't store 'hidden' database objects (the `Quest.obj` property. The methods help Evennia serialize/deserialize `Quest` propertly when the handler saves it. For more information, see [Storing Single objects](../Components/Attributes.md#storing-single-objects) in the Attributes documentation. + +### Tying it all together + +The final thing we need to do is to add the quest-handler to the character: + +```python +# in mygame/typeclasses/characters.py + +from evennia import DefaultCharacter +from evennia.utils.utils import lazy_property +from .world.quests import QuestHandler # as an example + + +class Character(DefaultCharacter): + # ... + @lazy_property + def quests(self): + return QuestHandler(self) + +``` + + +You can now make your Quest classes to describe your quests and add them to characters with + + character.quests.add(FindTheRedKey) + +and can later do + + character.quests.check_progress() + +and be sure that quest data is not lost between reloads. + +You can find a full-fledged quest-handler example as [EvAdventure quests](evennia.contribs.tutorials.evadventure.quests) contrib in the Evennia repository. \ No newline at end of file From 59bb10e76a835981545e88f9d1295c586dc21b3b Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 13 Sep 2022 12:59:05 +0200 Subject: [PATCH 3/4] Fix some bugs in handler tutorial --- docs/source/Howtos/Tutorial-Persistent-Handler.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/Howtos/Tutorial-Persistent-Handler.md b/docs/source/Howtos/Tutorial-Persistent-Handler.md index cf9bd3cc6f..0fb123e124 100644 --- a/docs/source/Howtos/Tutorial-Persistent-Handler.md +++ b/docs/source/Howtos/Tutorial-Persistent-Handler.md @@ -24,7 +24,7 @@ class NameChanger: class MyObject(DefaultObject): @lazy_property: def namechange(self): - return MyHandler(self) + return NameChanger(self) obj = create_object(MyObject, key="test") @@ -35,7 +35,7 @@ print(obj.key) >>> "test_extra" ``` -What happens here is that we make a new class `MyHandler`. We use the `@lazy_property` decorator to set it up - this means the handler will not be actually created until someone really wants to use it, by accessing `obj.namechange` later. The decorated `namechange` method returns the handler and makes sure to initialize it with `self` - this becomes the `obj` inside the handler! +What happens here is that we make a new class `NameChanger`. We use the `@lazy_property` decorator to set it up - this means the handler will not be actually created until someone really wants to use it, by accessing `obj.namechange` later. The decorated `namechange` method returns the handler and makes sure to initialize it with `self` - this becomes the `obj` inside the handler! We then make a silly method `add_to_key` that uses the handler to manipulate the key of the object. In this example, the handler is pretty pointless, but grouping functionality this way can both make for an easy-to-remember API and can also allow you cache data for easy access - this is how the `AttributeHandler` (`.attributes`) and `TagHandler` (`.tags`) works. @@ -92,8 +92,8 @@ class QuestHandler: self.storage = self.obj.attributes.get( "quest_storage", default={}, category="quests") - def _save(self): - self.obj.attributes.add( + def _save(self): + self.obj.attributes.add( "quest_storage", self.storage, category="quests") self._load() # important self.do_save = False From e9cce46d667829524b0b7187765f6ea52d0b75d7 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 13 Sep 2022 13:12:36 +0200 Subject: [PATCH 4/4] Fixing tabulation in handler tut --- .../Howtos/Tutorial-Persistent-Handler.md | 147 +++++++++++------- 1 file changed, 87 insertions(+), 60 deletions(-) diff --git a/docs/source/Howtos/Tutorial-Persistent-Handler.md b/docs/source/Howtos/Tutorial-Persistent-Handler.md index 0fb123e124..01d3663620 100644 --- a/docs/source/Howtos/Tutorial-Persistent-Handler.md +++ b/docs/source/Howtos/Tutorial-Persistent-Handler.md @@ -2,11 +2,11 @@ A _handler_ is a convenient way to group functionality on an object. This allows you to logically group all actions related to that thing in one place. This tutorial expemplifies how to make your own handlers and make sure data you store in them survives a reload. -For example, when you do `obj.attributes.get("key")` or `obj.tags.add('tagname')` you are evoking handlers stored as `.attributes` and `tags` on the `obj`. On these handlers are methods (`get()` and `add()` in this example). +For example, when you do `obj.attributes.get("key")` or `obj.tags.add('tagname')` you are evoking handlers stored as `.attributes` and `tags` on the `obj`. On these handlers are methods (`get()` and `add()` in this example). ## Base Handler example -Here is a base way to set up an on-object handler: +Here is a base way to set up an on-object handler: ```python @@ -20,9 +20,9 @@ class NameChanger: def add_to_key(self, suffix): self.obj.key = f"self.obj.key_{suffix}" -# make a test object -class MyObject(DefaultObject): - @lazy_property: +# make a test object +class MyObject(DefaultObject): + @lazy_property: def namechange(self): return NameChanger(self) @@ -35,15 +35,25 @@ print(obj.key) >>> "test_extra" ``` -What happens here is that we make a new class `NameChanger`. We use the `@lazy_property` decorator to set it up - this means the handler will not be actually created until someone really wants to use it, by accessing `obj.namechange` later. The decorated `namechange` method returns the handler and makes sure to initialize it with `self` - this becomes the `obj` inside the handler! +What happens here is that we make a new class `NameChanger`. We use the +`@lazy_property` decorator to set it up - this means the handler will not be +actually created until someone really wants to use it, by accessing +`obj.namechange` later. The decorated `namechange` method returns the handler +and makes sure to initialize it with `self` - this becomes the `obj` inside the +handler! -We then make a silly method `add_to_key` that uses the handler to manipulate the key of the object. In this example, the handler is pretty pointless, but grouping functionality this way can both make for an easy-to-remember API and can also allow you cache data for easy access - this is how the `AttributeHandler` (`.attributes`) and `TagHandler` (`.tags`) works. +We then make a silly method `add_to_key` that uses the handler to manipulate the +key of the object. In this example, the handler is pretty pointless, but +grouping functionality this way can both make for an easy-to-remember API and +can also allow you cache data for easy access - this is how the +`AttributeHandler` (`.attributes`) and `TagHandler` (`.tags`) works. -## Persistent storage of data in handler +## Persistent storage of data in handler -Let's say we want to track 'quests' in our handler. A 'quest' is a regular class that represents the quest. Let's make it simple as an example: +Let's say we want to track 'quests' in our handler. A 'quest' is a regular class +that represents the quest. Let's make it simple as an example: -```python +```python # for example in mygame/world/quests.py @@ -51,40 +61,40 @@ class Quest: key = "The quest for the red key" - def __init__(self): + def __init__(self): self.current_step = "start" - def check_progress(self): - # uses self.current_step to check - # progress of this quest + def check_progress(self): + # uses self.current_step to check + # progress of this quest getattr(self, f"step_{self.current_step}")() - def step_start(self): - # check here if quest-step is complete - self.current_step = "find_the_red_key" - def step_find_the_red_key(self): - # check if step is complete - self.current_step = "hand_in_quest" - def step_hand_in_quest(self): - # check if handed in quest to quest giver - self.current_step = None # finished - + def step_start(self): + # check here if quest-step is complete + self.current_step = "find_the_red_key" + def step_find_the_red_key(self): + # check if step is complete + self.current_step = "hand_in_quest" + def step_hand_in_quest(self): + # check if handed in quest to quest giver + self.current_step = None # finished + ``` -We expect the dev to make subclasses of this to implement different quests. Exactly how this works doesn't matter, the key is that we want to track `self.current_step` - a property that _should survive a server reload_. But so far there is no way for `Quest` to accomplish this, it's just a normal Python class with no connection to the database. +We expect the dev to make subclasses of this to implement different quests. Exactly how this works doesn't matter, the key is that we want to track `self.current_step` - a property that _should survive a server reload_. But so far there is no way for `Quest` to accomplish this, it's just a normal Python class with no connection to the database. ### Handler with save/load capability Let's make a `QuestHandler` that manages a character's quests. -```python +```python # for example in the same mygame/world/quests.py class QuestHandler: - def __init__(self, obj): - self.obj = obj + def __init__(self, obj): + self.obj = obj self.do_save = False self._load() @@ -92,106 +102,123 @@ class QuestHandler: self.storage = self.obj.attributes.get( "quest_storage", default={}, category="quests") - def _save(self): - self.obj.attributes.add( - "quest_storage", self.storage, category="quests") + def _save(self): + self.obj.attributes.add( + "quest_storage", self.storage, category="quests") self._load() # important self.do_save = False - def add(self, questclass): + def add(self, questclass): self.storage[questclass.key] = questclass(self.obj) self._save() - def check_progress(self): + def check_progress(self): for quest in self.storage.values(): quest.check_progress() if self.do_save: # .do_save is set on handler by Quest if it wants to save progress - self._save() + self._save() ``` The handler is just a normal Python class and has no database-storage on its own. But it has a link to `.obj`, which is assumed to be a full typeclased entity, on which we can create persistent [Attributes](../Components/Attributes.md) to store things however we like! -We make two helper methods `_load` and +We make two helper methods `_load` and `_save` that handles local fetches and saves `storage` to an Attribute on the object. To avoid saving more than necessary, we have a property `do_save`. This we will set in `Quest` below. -> Note that once we `_save` the data, we need to call `_load` again. This is to make sure the version we store on the handler is properly de-serialized. If you get an error about data being `bytes`, you probably missed this step. +> Note that once we `_save` the data, we need to call `_load` again. This is to make sure the version we store on the handler is properly de-serialized. If you get an error about data being `bytes`, you probably missed this step. ### Make quests storable -The handler will save all `Quest` objects as a `dict` in an Attribute on `obj`. We are not done yet though, the `Quest` object needs access to the `obj` too - not only will this is important to figure out if the quest is complete (the `Quest` must be able to check the quester's inventory to see if they have the red key, for example), it also allows the `Quest` to tell the handler when its state changed and it should be saved. +The handler will save all `Quest` objects as a `dict` in an Attribute on `obj`. We are not done yet though, the `Quest` object needs access to the `obj` too - not only will this is important to figure out if the quest is complete (the `Quest` must be able to check the quester's inventory to see if they have the red key, for example), it also allows the `Quest` to tell the handler when its state changed and it should be saved. We change the `Quest` such: -```python +```python from evennia.utils import dbserialize -class Quest: +class Quest: def __init__(self, obj): - self.obj = obj + self.obj = obj self._current_step = "start" def __serialize_dbobjs__(self): self.obj = dbserialize.dbserialize(self.obj) def __deserialize_dbobjs__(self): - if isinstance(self.obj, bytes): + if isinstance(self.obj, bytes): self.obj = dbserialize.dbunserialize(self.obj) - @property - def questhandler(self): + @property + def questhandler(self): return self.obj.quests @property - def current_step(self): + def current_step(self): return self._current_step - @current_step.setter - def current_step(self, value): - self._current_step = value + @current_step.setter + def current_step(self, value): + self._current_step = value self.questhandler.do_save = True # this triggers save in handler! # [same as before] ``` -The `Quest.__init__` now takes `obj` as argument, to match what we pass to it in `QuestHandler.add`. We want to monitor the changing of `current_step`, so we make it into a `property`. When we edit that value, we set the `do_save` flag on the handler, which means it will save the status to database once it has checked progress on all its quests. +The `Quest.__init__` now takes `obj` as argument, to match what we pass to it in +`QuestHandler.add`. We want to monitor the changing of `current_step`, so we +make it into a `property`. When we edit that value, we set the `do_save` flag on +the handler, which means it will save the status to database once it has checked +progress on all its quests. The `Quest.questhandler` property allows to easily +get back to the handler (and the object on which it sits). -The `__serialize__dbobjs__` and `__deserialize_dbobjs__` methods are needed because `Attributes` can't store 'hidden' database objects (the `Quest.obj` property. The methods help Evennia serialize/deserialize `Quest` propertly when the handler saves it. For more information, see [Storing Single objects](../Components/Attributes.md#storing-single-objects) in the Attributes documentation. +The `__serialize__dbobjs__` and `__deserialize_dbobjs__` methods are needed +because `Attributes` can't store 'hidden' database objects (the `Quest.obj` +property. The methods help Evennia serialize/deserialize `Quest` propertly when +the handler saves it. For more information, see [Storing Single +objects](../Components/Attributes.md#storing-single-objects) in the Attributes +documentation. ### Tying it all together The final thing we need to do is to add the quest-handler to the character: -```python +```python # in mygame/typeclasses/characters.py -from evennia import DefaultCharacter +from evennia import DefaultCharacter from evennia.utils.utils import lazy_property from .world.quests import QuestHandler # as an example -class Character(DefaultCharacter): - # ... - @lazy_property - def quests(self): +class Character(DefaultCharacter): + # ... + @lazy_property + def quests(self): return QuestHandler(self) ``` -You can now make your Quest classes to describe your quests and add them to characters with +You can now make your Quest classes to describe your quests and add them to +characters with - character.quests.add(FindTheRedKey) +```python +character.quests.add(FindTheRedKey) +``` -and can later do +and can later do - character.quests.check_progress() +```python +character.quests.check_progress() +``` and be sure that quest data is not lost between reloads. -You can find a full-fledged quest-handler example as [EvAdventure quests](evennia.contribs.tutorials.evadventure.quests) contrib in the Evennia repository. \ No newline at end of file +You can find a full-fledged quest-handler example as [EvAdventure +quests](evennia.contribs.tutorials.evadventure.quests) contrib in the Evennia +repository.