mirror of
https://github.com/evennia/evennia.git
synced 2026-03-21 23:36:30 +01:00
Merge pull request #2692 from ChrisLR/components-tagfield
Add TagField to Contrib Components
This commit is contained in:
commit
2aea48c850
6 changed files with 169 additions and 10 deletions
|
|
@ -50,7 +50,29 @@ class Health(Component):
|
|||
health = DBField(default=1)
|
||||
```
|
||||
|
||||
Note that default is optional and will default to None
|
||||
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
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ See the docs for more information.
|
|||
"""
|
||||
|
||||
from evennia.contrib.base_systems.components.component import Component
|
||||
from evennia.contrib.base_systems.components.dbfield import DBField, NDBField
|
||||
from evennia.contrib.base_systems.components.dbfield import DBField, NDBField, TagField
|
||||
from evennia.contrib.base_systems.components.holder import ComponentHolderMixin, ComponentProperty
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,11 @@ class Component:
|
|||
ndb_fields = getattr(self, "_ndb_fields", {})
|
||||
return ndb_fields.keys()
|
||||
|
||||
@property
|
||||
def tag_field_names(self):
|
||||
tag_fields = getattr(self, "_tag_fields", {})
|
||||
return tag_fields.keys()
|
||||
|
||||
|
||||
class ComponentRegisterError(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -52,3 +52,64 @@ class NDBField(NAttributeProperty):
|
|||
ndb_fields = {}
|
||||
setattr(owner, '_ndb_fields', ndb_fields)
|
||||
ndb_fields[name] = self
|
||||
|
||||
|
||||
class TagField:
|
||||
"""
|
||||
Component Tags Descriptor.
|
||||
Allows you to set Tags related to a component on the class.
|
||||
The tags are set with a prefixed category, so it can support
|
||||
multiple tags or enforce a single one.
|
||||
|
||||
Default value of a tag is added when the component is registered.
|
||||
Tags are removed if the component itself is removed.
|
||||
"""
|
||||
def __init__(self, default=None, enforce_single=False):
|
||||
self._category_key = None
|
||||
self._default = default
|
||||
self._enforce_single = enforce_single
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
"""
|
||||
Called when TagField is first assigned to the class.
|
||||
It is called with the component class and the name of the field.
|
||||
"""
|
||||
self._category_key = f"{owner.name}::{name}"
|
||||
tag_fields = getattr(owner, "_tag_fields", None)
|
||||
if tag_fields is None:
|
||||
tag_fields = {}
|
||||
setattr(owner, '_tag_fields', tag_fields)
|
||||
tag_fields[name] = self
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""
|
||||
Called when retrieving the value of the TagField.
|
||||
It is called with the component instance and the class.
|
||||
"""
|
||||
tag_value = instance.host.tags.get(
|
||||
default=self._default,
|
||||
category=self._category_key,
|
||||
)
|
||||
return tag_value
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""
|
||||
Called when setting a value on the TagField.
|
||||
It is called with the component instance and the value.
|
||||
"""
|
||||
|
||||
tag_handler = instance.host.tags
|
||||
if self._enforce_single:
|
||||
tag_handler.clear(category=self._category_key)
|
||||
|
||||
tag_handler.add(
|
||||
key=value,
|
||||
category=self._category_key,
|
||||
)
|
||||
|
||||
def __delete__(self, instance):
|
||||
"""
|
||||
Used when 'del' is called on the TagField.
|
||||
It is called with the component instance.
|
||||
"""
|
||||
instance.host.tags.clear(category=self._category_key)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ComponentHandler:
|
|||
"""
|
||||
self._set_component(component)
|
||||
self.db_names.append(component.name)
|
||||
self.host.tags.add(component.name, category="components")
|
||||
self._add_component_tags(component)
|
||||
component.at_added(self.host)
|
||||
|
||||
def add_default(self, name):
|
||||
|
|
@ -85,9 +85,24 @@ class ComponentHandler:
|
|||
new_component = component.default_create(self.host)
|
||||
self._set_component(new_component)
|
||||
self.db_names.append(name)
|
||||
self.host.tags.add(name, category="components")
|
||||
self._add_component_tags(new_component)
|
||||
new_component.at_added(self.host)
|
||||
|
||||
def _add_component_tags(self, component):
|
||||
"""
|
||||
Private method that adds the Tags set on a Component via TagFields
|
||||
It will also add the name of the component so objects can be filtered
|
||||
by the components the implement.
|
||||
|
||||
Args:
|
||||
component (object): The component instance that is added.
|
||||
"""
|
||||
self.host.tags.add(component.name, category="components")
|
||||
for tag_field_name in component.tag_field_names:
|
||||
default_tag = type(component).__dict__[tag_field_name]._default
|
||||
if default_tag:
|
||||
setattr(component, tag_field_name, default_tag)
|
||||
|
||||
def remove(self, component):
|
||||
"""
|
||||
Method to remove a component instance from a host.
|
||||
|
|
@ -100,9 +115,9 @@ class ComponentHandler:
|
|||
"""
|
||||
component_name = component.name
|
||||
if component_name in self._loaded_components:
|
||||
self._remove_component_tags(component)
|
||||
component.at_removed(self.host)
|
||||
self.db_names.remove(component_name)
|
||||
self.host.tags.remove(component_name, category="components")
|
||||
del self._loaded_components[component_name]
|
||||
else:
|
||||
message = f"Cannot remove {component_name} from {self.host.name} as it is not registered."
|
||||
|
|
@ -123,11 +138,24 @@ class ComponentHandler:
|
|||
message = f"Cannot remove {name} from {self.host.name} as it is not registered."
|
||||
raise ComponentIsNotRegistered(message)
|
||||
|
||||
self._remove_component_tags(instance)
|
||||
instance.at_removed(self.host)
|
||||
self.db_names.remove(name)
|
||||
self.host.tags.remove(name, category="components")
|
||||
|
||||
del self._loaded_components[name]
|
||||
|
||||
def _remove_component_tags(self, component):
|
||||
"""
|
||||
Private method that will remove the Tags set on a Component via TagFields
|
||||
It will also remove the component name tag.
|
||||
|
||||
Args:
|
||||
component (object): The component instance that is removed.
|
||||
"""
|
||||
self.host.tags.remove(component.name, category="components")
|
||||
for tag_field_name in component.tag_field_names:
|
||||
delattr(component, tag_field_name)
|
||||
|
||||
def get(self, name):
|
||||
"""
|
||||
Method to retrieve a cached Component instance by its name.
|
||||
|
|
@ -225,8 +253,8 @@ class ComponentHolderMixin(object):
|
|||
Method that add component related tags that were set using ComponentProperty.
|
||||
"""
|
||||
super().basetype_posthook_setup()
|
||||
for component_name in self.db.component_names:
|
||||
self.tags.add(component_name, category="components")
|
||||
for component in self.components._loaded_components.values():
|
||||
self.components._add_component_tags(component)
|
||||
|
||||
@property
|
||||
def components(self) -> ComponentHandler:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from evennia.contrib.base_systems.components import Component, DBField
|
||||
from evennia.contrib.base_systems.components import Component, DBField, TagField
|
||||
from evennia.contrib.base_systems.components.holder import ComponentProperty, ComponentHolderMixin
|
||||
from evennia.objects.objects import DefaultCharacter
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
|
@ -14,12 +14,17 @@ class ComponentTestB(Component):
|
|||
name = "test_b"
|
||||
my_int = DBField(default=1)
|
||||
my_list = DBField(default=[])
|
||||
default_tag = TagField(default="initial_value")
|
||||
single_tag = TagField(enforce_single=True)
|
||||
multiple_tags = TagField()
|
||||
default_single_tag = TagField(default="initial_value", enforce_single=True)
|
||||
|
||||
|
||||
class RuntimeComponentTestC(Component):
|
||||
name = "test_c"
|
||||
my_int = DBField(default=6)
|
||||
my_dict = DBField(default={})
|
||||
added_tag = TagField(default="added_value")
|
||||
|
||||
|
||||
class CharacterWithComponents(ComponentHolderMixin, DefaultCharacter):
|
||||
|
|
@ -111,7 +116,11 @@ class TestComponents(EvenniaTest):
|
|||
def test_host_has_class_component_tags(self):
|
||||
assert self.char1.tags.has(key="test_a", category="components")
|
||||
assert self.char1.tags.has(key="test_b", category="components")
|
||||
assert self.char1.tags.has(key="initial_value", category="test_b::default_tag")
|
||||
assert self.char1.test_b.default_tag == "initial_value"
|
||||
assert not self.char1.tags.has(key="test_c", category="components")
|
||||
assert not self.char1.tags.has(category="test_b::single_tag")
|
||||
assert not self.char1.tags.has(category="test_b::multiple_tags")
|
||||
|
||||
def test_host_has_added_component_tags(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
|
|
@ -119,12 +128,16 @@ class TestComponents(EvenniaTest):
|
|||
test_c = self.char1.components.get('test_c')
|
||||
|
||||
assert self.char1.tags.has(key="test_c", category="components")
|
||||
assert self.char1.tags.has(key="added_value", category="test_c::added_tag")
|
||||
assert test_c.added_tag == "added_value"
|
||||
|
||||
def test_host_has_added_default_component_tags(self):
|
||||
self.char1.components.add_default("test_c")
|
||||
test_c = self.char1.components.get("test_c")
|
||||
|
||||
assert self.char1.tags.has(key="test_c", category="components")
|
||||
assert self.char1.tags.has(key="added_value", category="test_c::added_tag")
|
||||
assert test_c.added_tag == "added_value"
|
||||
|
||||
def test_host_remove_component_tags(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
|
|
@ -134,6 +147,7 @@ class TestComponents(EvenniaTest):
|
|||
handler.remove(rct)
|
||||
|
||||
assert not self.char1.tags.has(key="test_c", category="components")
|
||||
assert not self.char1.tags.has(key="added_value", category="test_c::added_tag")
|
||||
|
||||
def test_host_remove_by_name_component_tags(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
|
|
@ -142,4 +156,33 @@ class TestComponents(EvenniaTest):
|
|||
assert self.char1.tags.has(key="test_c", category="components")
|
||||
handler.remove_by_name("test_c")
|
||||
|
||||
assert not self.char1.tags.has(key="test_c", category="components")
|
||||
assert not self.char1.tags.has(key="test_c", category="components")
|
||||
assert not self.char1.tags.has(key="added_value", category="test_c::added_tag")
|
||||
|
||||
def test_component_tags_only_hold_one_value_when_enforce_single(self):
|
||||
test_b = self.char1.components.get('test_b')
|
||||
test_b.single_tag = "first_value"
|
||||
test_b.single_tag = "second value"
|
||||
|
||||
assert self.char1.tags.has(key="second value", category="test_b::single_tag")
|
||||
assert test_b.single_tag == "second value"
|
||||
assert not self.char1.tags.has(key="first_value", category="test_b::single_tag")
|
||||
|
||||
def test_component_tags_default_value_is_overridden_when_enforce_single(self):
|
||||
test_b = self.char1.components.get('test_b')
|
||||
test_b.default_single_tag = "second value"
|
||||
|
||||
assert self.char1.tags.has(key="second value", category="test_b::default_single_tag")
|
||||
assert test_b.default_single_tag == "second value"
|
||||
assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag")
|
||||
|
||||
def test_component_tags_support_multiple_values_by_default(self):
|
||||
test_b = self.char1.components.get('test_b')
|
||||
test_b.multiple_tags = "first value"
|
||||
test_b.multiple_tags = "second value"
|
||||
test_b.multiple_tags = "third value"
|
||||
|
||||
assert all(val in test_b.multiple_tags for val in ("first value", "second value", "third value"))
|
||||
assert self.char1.tags.has(key="first value", category="test_b::multiple_tags")
|
||||
assert self.char1.tags.has(key="second value", category="test_b::multiple_tags")
|
||||
assert self.char1.tags.has(key="third value", category="test_b::multiple_tags")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue