diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 6268fa57ec..be0527668a 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -160,6 +160,14 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10 - Attribute storage support defaultdics (Hendher) - Add ObjectParent mixin to default game folder template as an easy, ready-made way to override features on all ObjectDB-inheriting objects easily. +- Add `TagProperty`, `AliasProperty` and `PermissionProperty` to assign these + data in a similar way to django fields. +- The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` + to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute. +- Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will + now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal) +- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal) +- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal) ## Evennia 0.9.5 diff --git a/docs/source/Components/Attributes.md b/docs/source/Components/Attributes.md index 978fcdf39d..3d0cdda131 100644 --- a/docs/source/Components/Attributes.md +++ b/docs/source/Components/Attributes.md @@ -367,8 +367,8 @@ obj.db.mydata = container # will raise error! By adding two methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` to the object you want to save, you can pre-serialize and post-deserialize all 'hidden' objects before Evennia's main serializer gets to work. Inside these methods, use Evennia's -[evennia.utils.dbserialize.dbserialize](api:evennia.utils.dbserialize.dbserialize) and -[dbunserialize](api:evennia.utils.dbserialize.dbunserialize) functions to safely +[evennia.utils.dbserialize.dbserialize](evennia.utils.dbserialize.dbserialize) and +[dbunserialize](evennia.utils.dbserialize.dbunserialize) functions to safely serialize the db-objects you want to store. ```{code-block} python diff --git a/docs/source/Components/Tags.md b/docs/source/Components/Tags.md index 488bab577f..c3f7ce3b30 100644 --- a/docs/source/Components/Tags.md +++ b/docs/source/Components/Tags.md @@ -22,7 +22,7 @@ class Sword(DefaultObject): ``` -_Tags_ are short text lables one can 'hang' on objects in order to organize, group and quickly find out their properties. An Evennia entity can be tagged by any number of tags. They are more efficient than [Attributes](Attributes) since on the database-side, Tags are _shared_ between all objects with that particular tag. A tag does not carry a value in itself; it either sits on the entity +_Tags_ are short text lables one can 'hang' on objects in order to organize, group and quickly find out their properties. An Evennia entity can be tagged by any number of tags. They are more efficient than [Attributes](./Attributes.md) since on the database-side, Tags are _shared_ between all objects with that particular tag. A tag does not carry a value in itself; it either sits on the entity Above, the tags inform us that the `Sword` is both sharp and can be wielded. If that's all they do, they could just be a normal Python flag. When tags become important is if there are a lot of objects with different combinations of tags. Maybe you have a magical spell that dulls _all_ sharp-edged objects in the castle - whether sword, dagger, spear or kitchen knife! You can then just grab all objects with the `has_sharp_edge` tag. Another example would be a weather script affecting all rooms tagged as `outdoors` or finding all characters tagged with `belongs_to_fighter_guild`. diff --git a/docs/source/Contribs/Contrib-Components.md b/docs/source/Contribs/Contrib-Components.md new file mode 100644 index 0000000000..63932e24bf --- /dev/null +++ b/docs/source/Contribs/Contrib-Components.md @@ -0,0 +1,198 @@ +# Components + +_Contrib by ChrisLR 2021_ + +# The Components Contrib + +This contrib introduces Components and Composition to Evennia. +Each 'Component' class represents a feature that will be 'enabled' on a typeclass instance. +You can register these components on an entire typeclass or a single object at runtime. +It supports both persisted attributes and in-memory attributes by using Evennia's AttributeHandler. + +# Pros +- You can reuse a feature across multiple typeclasses without inheritance +- You can cleanly organize each feature into a self-contained class. +- You can check if your object supports a feature without checking its instance. + +# Cons +- It introduces additional complexity. +- A host typeclass instance is required. + +# How to install + +To enable component support for a typeclass, +import and inherit the ComponentHolderMixin, similar to this +```python +from evennia.contrib.base_systems.components import ComponentHolderMixin +class Character(ComponentHolderMixin, DefaultCharacter): +# ... +``` + +Components need to inherit the Component class directly and require a name. +```python +from evennia.contrib.components import Component + +class Health(Component): + name = "health" +``` + +Components may define DBFields or NDBFields at the class level. +DBField will store its values in the host's DB with a prefixed key. +NDBField will store its values in the host's NDB and will not persist. +The key used will be 'component_name::field_name'. +They use AttributeProperty under the hood. + +Example: +```python +from evennia.contrib.base_systems.components import Component, DBField + +class Health(Component): + health = DBField(default=1) +``` + +Note that default is optional and will default to None. + +Adding a component to a host will also a similarly named tag with 'components' as category. +A Component named health will appear as key="health, category="components". +This allows you to retrieve objects with specific components by searching with the tag. + +It is also possible to add Component Tags the same way, using TagField. +TagField accepts a default value and can be used to store a single or multiple tags. +Default values are automatically added when the component is added. +Component Tags are cleared from the host if the component is removed. + +Example: +```python +from evennia.contrib.base_systems.components import Component, TagField + +class Health(Component): + resistances = TagField() + vulnerability = TagField(default="fire", enforce_single=True) +``` + +The 'resistances' field in this example can be set to multiple times and it will keep the added tags. +The 'vulnerability' field in this example will override the previous tag with the new one. + + + +Each typeclass using the ComponentHolderMixin can declare its components +in the class via the ComponentProperty. +These are components that will always be present in a typeclass. +You can also pass kwargs to override the default values +Example +```python +from evennia.contrib.base_systems.components import ComponentHolderMixin +class Character(ComponentHolderMixin, DefaultCharacter): + health = ComponentProperty("health", hp=10, max_hp=50) +``` + +You can then use character.components.health to access it. +The shorter form character.cmp.health also exists. +character.health would also be accessible but only for typeclasses that have +this component defined on the class. + +Alternatively you can add those components at runtime. +You will have to access those via the component handler. +Example +```python +character = self +vampirism = components.Vampirism.create(character) +character.components.add(vampirism) + +... + +vampirism_from_elsewhere = character.components.get("vampirism") +``` + +Keep in mind that all components must be imported to be visible in the listing. +As such, I recommend regrouping them in a package. +You can then import all your components in that package's __init__ + +Because of how Evennia import typeclasses and the behavior of python imports +I recommend placing the components package inside the typeclass package. +In other words, create a folder named components inside your typeclass folder. +Then, inside the 'typeclasses/__init__.py' file add the import to the folder, like +```python +from typeclasses import components +``` +This ensures that the components package will be imported when the typeclasses are imported. +You will also need to import each components inside the package's own 'typeclasses/components/__init__.py' file. +You only need to import each module/file from there but importing the right class is a good practice. +```python +from typeclasses.components.health import Health +``` +```python +from typeclasses.components import health +``` +Both of the above examples will work. + +# Full Example +```python +from evennia.contrib.base_systems import components + + +# This is the Component class +class Health(components.Component): + name = "health" + + # Stores the current and max values as Attributes on the host, defaulting to 100 + current = components.DBField(default=100) + max = components.DBField(default=100) + + def damage(self, value): + if self.current <= 0: + return + + self.current -= value + if self.current > 0: + return + + self.current = 0 + self.on_death() + + def heal(self, value): + hp = self.current + hp += value + if hp >= self.max_hp: + hp = self.max_hp + + self.current = hp + + @property + def is_dead(self): + return self.current <= 0 + + def on_death(self): + # Behavior is defined on the typeclass + self.host.on_death() + + +# This is how the Character inherits the mixin and registers the component 'health' +class Character(ComponentHolderMixin, DefaultCharacter): + health = ComponentProperty("health") + + +# This is an example of a command that checks for the component +class Attack(Command): + key = "attack" + aliases = ('melee', 'hit') + + def at_pre_cmd(self): + caller = self.caller + targets = self.caller.search(args, quiet=True) + valid_target = None + for target in targets: + # Attempt to retrieve the component, None is obtained if it does not exist. + if target.components.health: + valid_target = target + + if not valid_target: + caller.msg("You can't attack that!") + return True +``` + + +---- + +This document page is generated from `evennia/contrib/base_systems/components/README.md`. Changes to this +file will be overwritten, so edit that file rather than this one. diff --git a/docs/source/Contributing-Docs.md b/docs/source/Contributing-Docs.md index 49524a358f..a20ebb1074 100644 --- a/docs/source/Contributing-Docs.md +++ b/docs/source/Contributing-Docs.md @@ -728,4 +728,4 @@ available at https://evennia.github.io/evennia/latest/. [linkdemo]: #Links [retext]: https://github.com/retext-project/retext [grip]: https://github.com/joeyespo/grip -[pycharm]: https://www.jetbrains.com/pycharm/ \ No newline at end of file +[pycharm]: https://www.jetbrains.com/pycharm/ diff --git a/docs/source/api/evennia.contrib.base_systems.components.component.md b/docs/source/api/evennia.contrib.base_systems.components.component.md new file mode 100644 index 0000000000..c2666e69a3 --- /dev/null +++ b/docs/source/api/evennia.contrib.base_systems.components.component.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.base\_systems.components.component +========================================================= + +.. automodule:: evennia.contrib.base_systems.components.component + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.base_systems.components.dbfield.md b/docs/source/api/evennia.contrib.base_systems.components.dbfield.md new file mode 100644 index 0000000000..6069eb28b6 --- /dev/null +++ b/docs/source/api/evennia.contrib.base_systems.components.dbfield.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.base\_systems.components.dbfield +======================================================= + +.. automodule:: evennia.contrib.base_systems.components.dbfield + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.base_systems.components.holder.md b/docs/source/api/evennia.contrib.base_systems.components.holder.md new file mode 100644 index 0000000000..d5c5452932 --- /dev/null +++ b/docs/source/api/evennia.contrib.base_systems.components.holder.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.base\_systems.components.holder +====================================================== + +.. automodule:: evennia.contrib.base_systems.components.holder + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.base_systems.components.md b/docs/source/api/evennia.contrib.base_systems.components.md new file mode 100644 index 0000000000..c72406ab4d --- /dev/null +++ b/docs/source/api/evennia.contrib.base_systems.components.md @@ -0,0 +1,21 @@ +```{eval-rst} +evennia.contrib.base\_systems.components +================================================ + +.. automodule:: evennia.contrib.base_systems.components + :members: + :undoc-members: + :show-inheritance: + + + +.. toctree:: + :maxdepth: 6 + + evennia.contrib.base_systems.components.component + evennia.contrib.base_systems.components.dbfield + evennia.contrib.base_systems.components.holder + evennia.contrib.base_systems.components.signals + evennia.contrib.base_systems.components.tests + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.base_systems.components.signals.md b/docs/source/api/evennia.contrib.base_systems.components.signals.md new file mode 100644 index 0000000000..64260b780b --- /dev/null +++ b/docs/source/api/evennia.contrib.base_systems.components.signals.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.base\_systems.components.signals +======================================================= + +.. automodule:: evennia.contrib.base_systems.components.signals + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.base_systems.components.tests.md b/docs/source/api/evennia.contrib.base_systems.components.tests.md new file mode 100644 index 0000000000..699b208fb0 --- /dev/null +++ b/docs/source/api/evennia.contrib.base_systems.components.tests.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.base\_systems.components.tests +===================================================== + +.. automodule:: evennia.contrib.base_systems.components.tests + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file