Merge pull request #2667 from ChrisLR/components-contrib-sq
Components Contrib (Pt1)
This commit is contained in:
commit
841f6dac35
6 changed files with 749 additions and 0 deletions
170
evennia/contrib/base_systems/components/README.md
Normal file
170
evennia/contrib/base_systems/components/README.md
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
24
evennia/contrib/base_systems/components/__init__.py
Normal file
24
evennia/contrib/base_systems/components/__init__.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""
|
||||||
|
Components - ChrisLR 2022
|
||||||
|
|
||||||
|
This is a basic Component System.
|
||||||
|
It allows you to use components on typeclasses using a simple syntax.
|
||||||
|
This helps writing isolated code and reusing it over multiple objects.
|
||||||
|
|
||||||
|
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.holder import ComponentHolderMixin, ComponentProperty
|
||||||
|
|
||||||
|
|
||||||
|
def get_component_class(component_name):
|
||||||
|
subclasses = Component.__subclasses__()
|
||||||
|
component_class = next((sc for sc in subclasses if sc.name == component_name), None)
|
||||||
|
if component_class is None:
|
||||||
|
message = f"Component named {component_name} has not been found. " \
|
||||||
|
f"Make sure it has been imported before being used."
|
||||||
|
raise Exception(message)
|
||||||
|
|
||||||
|
return component_class
|
||||||
149
evennia/contrib/base_systems/components/component.py
Normal file
149
evennia/contrib/base_systems/components/component.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
"""
|
||||||
|
Components - ChrisLR 2022
|
||||||
|
|
||||||
|
This file contains the base class to inherit for creating new components.
|
||||||
|
"""
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
|
class Component:
|
||||||
|
"""
|
||||||
|
This is the base class for components.
|
||||||
|
Any component must inherit from this class to be considered for usage.
|
||||||
|
|
||||||
|
Each Component must supply the name, it is used as a slot name but also part of the attribute key.
|
||||||
|
"""
|
||||||
|
name = ""
|
||||||
|
|
||||||
|
def __init__(self, host=None):
|
||||||
|
assert self.name, "All Components must have a Name"
|
||||||
|
self.host = host
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_create(cls, host):
|
||||||
|
"""
|
||||||
|
This is called when the host is created
|
||||||
|
and should return the base initialized state of a component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host (object): The host typeclass instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Component: The created instance of the component
|
||||||
|
|
||||||
|
"""
|
||||||
|
new = cls(host)
|
||||||
|
return new
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, host, **kwargs):
|
||||||
|
"""
|
||||||
|
This is the method to call when supplying kwargs to initialize a component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host (object): The host typeclass instance
|
||||||
|
**kwargs: Key-Value of default values to replace.
|
||||||
|
To persist the value, the key must correspond to a DBField.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Component: The created instance of the component
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
new = cls.default_create(host)
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
setattr(new, key, value)
|
||||||
|
|
||||||
|
return new
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
This deletes all component attributes from the host's db
|
||||||
|
"""
|
||||||
|
for attribute in self._all_db_field_names:
|
||||||
|
delattr(self, attribute)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, host):
|
||||||
|
"""
|
||||||
|
Loads a component instance
|
||||||
|
This is called whenever a component is loaded (ex: Server Restart)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host (object): The host typeclass instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Component: The loaded instance of the component
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return cls(host)
|
||||||
|
|
||||||
|
def at_added(self, host):
|
||||||
|
"""
|
||||||
|
This is the method called when a component is registered on a host.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host (object): The host typeclass instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.host:
|
||||||
|
if self.host == host:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise ComponentRegisterError("Components must not register twice!")
|
||||||
|
|
||||||
|
self.host = host
|
||||||
|
|
||||||
|
def at_removed(self, host):
|
||||||
|
"""
|
||||||
|
This is the method called when a component is removed from a host.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host (object): The host typeclass instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
if host != self.host:
|
||||||
|
raise ComponentRegisterError("Component attempted to remove from the wrong host.")
|
||||||
|
self.host = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attributes(self):
|
||||||
|
"""
|
||||||
|
Shortcut property returning the host's AttributeHandler.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AttributeHandler: The Host's AttributeHandler
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.host.attributes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nattributes(self):
|
||||||
|
"""
|
||||||
|
Shortcut property returning the host's In-Memory AttributeHandler (Non Persisted).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AttributeHandler: The Host's In-Memory AttributeHandler
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.host.nattributes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _all_db_field_names(self):
|
||||||
|
return itertools.chain(self.db_field_names, self.ndb_field_names)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_field_names(self):
|
||||||
|
db_fields = getattr(self, "_db_fields", {})
|
||||||
|
return db_fields.keys()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ndb_field_names(self):
|
||||||
|
ndb_fields = getattr(self, "_ndb_fields", {})
|
||||||
|
return ndb_fields.keys()
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentRegisterError(Exception):
|
||||||
|
pass
|
||||||
54
evennia/contrib/base_systems/components/dbfield.py
Normal file
54
evennia/contrib/base_systems/components/dbfield.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""
|
||||||
|
Components - ChrisLR 2022
|
||||||
|
|
||||||
|
This file contains the Descriptors used to set Fields in Components
|
||||||
|
"""
|
||||||
|
from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty
|
||||||
|
|
||||||
|
|
||||||
|
class DBField(AttributeProperty):
|
||||||
|
"""
|
||||||
|
Component Attribute Descriptor.
|
||||||
|
Allows you to set attributes related to a component on the class.
|
||||||
|
It uses AttributeProperty under the hood but prefixes the key with the component name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
"""
|
||||||
|
Called when descriptor is first assigned to the class.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner (object): The component classF on which this is set
|
||||||
|
name (str): The name that was used to set the DBField.
|
||||||
|
"""
|
||||||
|
key = f"{owner.name}__{name}"
|
||||||
|
self._key = key
|
||||||
|
db_fields = getattr(owner, "_db_fields", None)
|
||||||
|
if db_fields is None:
|
||||||
|
db_fields = {}
|
||||||
|
setattr(owner, '_db_fields', db_fields)
|
||||||
|
db_fields[name] = self
|
||||||
|
|
||||||
|
|
||||||
|
class NDBField(NAttributeProperty):
|
||||||
|
"""
|
||||||
|
Component In-Memory Attribute Descriptor.
|
||||||
|
Allows you to set in-memory attributes related to a component on the class.
|
||||||
|
It uses NAttributeProperty under the hood but prefixes the key with the component name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
"""
|
||||||
|
Called when descriptor is first assigned to the class.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner (object): The component class on which this is set
|
||||||
|
name (str): The name that was used to set the DBField.
|
||||||
|
"""
|
||||||
|
key = f"{owner.name}__{name}"
|
||||||
|
self._key = key
|
||||||
|
ndb_fields = getattr(owner, "_ndb_fields", None)
|
||||||
|
if ndb_fields is None:
|
||||||
|
ndb_fields = {}
|
||||||
|
setattr(owner, '_ndb_fields', ndb_fields)
|
||||||
|
ndb_fields[name] = self
|
||||||
243
evennia/contrib/base_systems/components/holder.py
Normal file
243
evennia/contrib/base_systems/components/holder.py
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
"""
|
||||||
|
Components - ChrisLR 2022
|
||||||
|
|
||||||
|
This file contains the classes that allow a typeclass to use components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from evennia.contrib.base_systems import components
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentProperty:
|
||||||
|
"""
|
||||||
|
This allows you to register a component on a typeclass.
|
||||||
|
Components registered with this property are automatically added
|
||||||
|
to any instance of this typeclass.
|
||||||
|
|
||||||
|
Defaults can be overridden for this typeclass by passing kwargs
|
||||||
|
"""
|
||||||
|
def __init__(self, component_name, **kwargs):
|
||||||
|
"""
|
||||||
|
Initializes the descriptor
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component_name (str): The name of the component
|
||||||
|
**kwargs (any): Key=Values overriding default values of the component
|
||||||
|
"""
|
||||||
|
self.component_name = component_name
|
||||||
|
self.values = kwargs
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
component = instance.components.get(self.component_name)
|
||||||
|
return component
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
raise Exception("Cannot set a class property")
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
class_components = getattr(owner, "_class_components", None)
|
||||||
|
if not class_components:
|
||||||
|
class_components = []
|
||||||
|
setattr(owner, "_class_components", class_components)
|
||||||
|
|
||||||
|
class_components.append((self.component_name, self.values))
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentHandler:
|
||||||
|
"""
|
||||||
|
This is the handler that will be added to any typeclass that inherits from ComponentHolder.
|
||||||
|
It lets you add or remove components and will load components as needed.
|
||||||
|
It stores the list of registered components on the host .db with component_names as key.
|
||||||
|
"""
|
||||||
|
def __init__(self, host):
|
||||||
|
self.host = host
|
||||||
|
self._loaded_components = {}
|
||||||
|
|
||||||
|
def add(self, component):
|
||||||
|
"""
|
||||||
|
Method to add a Component to a host.
|
||||||
|
It caches the loaded component and appends its name to the host's component name list.
|
||||||
|
It will also call the component's 'at_added' method, passing its host.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component (object): The 'loaded' component instance to add.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._set_component(component)
|
||||||
|
self.db_names.append(component.name)
|
||||||
|
component.at_added(self.host)
|
||||||
|
|
||||||
|
def add_default(self, name):
|
||||||
|
"""
|
||||||
|
Method to add a Component initialized to default values on a host.
|
||||||
|
It will retrieve the proper component and instanciate it with 'default_create'.
|
||||||
|
It will cache this new component and add it to its list.
|
||||||
|
It will also call the component's 'at_added' method, passing its host.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component class to add.
|
||||||
|
|
||||||
|
"""
|
||||||
|
component = components.get_component_class(name)
|
||||||
|
if not component:
|
||||||
|
raise ComponentDoesNotExist(f"Component {name} does not exist.")
|
||||||
|
|
||||||
|
new_component = component.default_create(self.host)
|
||||||
|
self._set_component(new_component)
|
||||||
|
self.db_names.append(name)
|
||||||
|
new_component.at_added(self.host)
|
||||||
|
|
||||||
|
def remove(self, component):
|
||||||
|
"""
|
||||||
|
Method to remove a component instance from a host.
|
||||||
|
It removes the component from the cache and listing.
|
||||||
|
It will call the component's 'at_removed' method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component (object): The component instance to remove.
|
||||||
|
|
||||||
|
"""
|
||||||
|
component_name = component.name
|
||||||
|
if component_name in self._loaded_components:
|
||||||
|
component.at_removed(self.host)
|
||||||
|
self.db_names.remove(component_name)
|
||||||
|
del self._loaded_components[component_name]
|
||||||
|
else:
|
||||||
|
message = f"Cannot remove {component_name} from {self.host.name} as it is not registered."
|
||||||
|
raise ComponentIsNotRegistered(message)
|
||||||
|
|
||||||
|
def remove_by_name(self, name):
|
||||||
|
"""
|
||||||
|
Method to remove a component instance from a host.
|
||||||
|
It removes the component from the cache and listing.
|
||||||
|
It will call the component's 'at_removed' method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component to remove.
|
||||||
|
|
||||||
|
"""
|
||||||
|
instance = self.get(name)
|
||||||
|
if not instance:
|
||||||
|
message = f"Cannot remove {name} from {self.host.name} as it is not registered."
|
||||||
|
raise ComponentIsNotRegistered(message)
|
||||||
|
|
||||||
|
instance.at_removed(self.host)
|
||||||
|
self.db_names.remove(name)
|
||||||
|
del self._loaded_components[name]
|
||||||
|
|
||||||
|
def get(self, name):
|
||||||
|
"""
|
||||||
|
Method to retrieve a cached Component instance by its name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component to retrieve.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._loaded_components.get(name)
|
||||||
|
|
||||||
|
def has(self, name):
|
||||||
|
"""
|
||||||
|
Method to check if a component is registered and ready.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return name in self._loaded_components
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""
|
||||||
|
Method that loads and caches each component currently registered on the host.
|
||||||
|
It retrieves the names from the registered listing and calls 'load' on each
|
||||||
|
prototype class that can be found from this listing.
|
||||||
|
|
||||||
|
"""
|
||||||
|
component_names = self.db_names
|
||||||
|
if not component_names:
|
||||||
|
return
|
||||||
|
|
||||||
|
for component_name in component_names:
|
||||||
|
component = components.get_component_class(component_name)
|
||||||
|
if component:
|
||||||
|
component_instance = component.load(self.host)
|
||||||
|
self._set_component(component_instance)
|
||||||
|
else:
|
||||||
|
message = f"Could not initialize runtime component {component_name} of {self.host.name}"
|
||||||
|
raise ComponentDoesNotExist(message)
|
||||||
|
|
||||||
|
def _set_component(self, component):
|
||||||
|
self._loaded_components[component.name] = component
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_names(self):
|
||||||
|
"""
|
||||||
|
Property shortcut to retrieve the registered component names
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
component_names (iterable): The name of each component that is registered
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.host.attributes.get("component_names")
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return self.get(name)
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentHolderMixin(object):
|
||||||
|
"""
|
||||||
|
Mixin to add component support to a typeclass
|
||||||
|
|
||||||
|
Components are set on objects using the component.name as an object attribute.
|
||||||
|
All registered components are initialized on the typeclass.
|
||||||
|
They will be of None value if not present in the class components or runtime components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def at_init(self):
|
||||||
|
"""
|
||||||
|
Method that initializes the ComponentHandler.
|
||||||
|
"""
|
||||||
|
super(ComponentHolderMixin, self).at_init()
|
||||||
|
setattr(self, "_component_handler", ComponentHandler(self))
|
||||||
|
self.components.initialize()
|
||||||
|
|
||||||
|
def basetype_setup(self):
|
||||||
|
"""
|
||||||
|
Method that initializes the ComponentHandler, creates and registers all
|
||||||
|
components that were set on the typeclass using ComponentProperty.
|
||||||
|
"""
|
||||||
|
super().basetype_setup()
|
||||||
|
component_names = []
|
||||||
|
setattr(self, "_component_handler", ComponentHandler(self))
|
||||||
|
class_components = getattr(self, "_class_components", ())
|
||||||
|
for component_name, values in class_components:
|
||||||
|
component_class = components.get_component_class(component_name)
|
||||||
|
component = component_class.create(self, **values)
|
||||||
|
component_names.append(component_name)
|
||||||
|
self.components._loaded_components[component_name] = component
|
||||||
|
|
||||||
|
self.db.component_names = component_names
|
||||||
|
|
||||||
|
@property
|
||||||
|
def components(self) -> ComponentHandler:
|
||||||
|
"""
|
||||||
|
Property getter to retrieve the component_handler.
|
||||||
|
Returns:
|
||||||
|
ComponentHandler: This Host's ComponentHandler
|
||||||
|
"""
|
||||||
|
return getattr(self, "_component_handler", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cmp(self) -> ComponentHandler:
|
||||||
|
"""
|
||||||
|
Shortcut Property getter to retrieve the component_handler.
|
||||||
|
Returns:
|
||||||
|
ComponentHandler: This Host's ComponentHandler
|
||||||
|
"""
|
||||||
|
return self.components
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentDoesNotExist(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentIsNotRegistered(Exception):
|
||||||
|
pass
|
||||||
109
evennia/contrib/base_systems/components/tests.py
Normal file
109
evennia/contrib/base_systems/components/tests.py
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
from evennia.contrib.base_systems.components import Component, DBField
|
||||||
|
from evennia.contrib.base_systems.components.holder import ComponentProperty, ComponentHolderMixin
|
||||||
|
from evennia.objects.objects import DefaultCharacter
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentTestA(Component):
|
||||||
|
name = "test_a"
|
||||||
|
my_int = DBField(default=1)
|
||||||
|
my_list = DBField(default=[])
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentTestB(Component):
|
||||||
|
name = "test_b"
|
||||||
|
my_int = DBField(default=1)
|
||||||
|
my_list = DBField(default=[])
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeComponentTestC(Component):
|
||||||
|
name = "test_c"
|
||||||
|
my_int = DBField(default=6)
|
||||||
|
my_dict = DBField(default={})
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterWithComponents(ComponentHolderMixin, DefaultCharacter):
|
||||||
|
test_a = ComponentProperty("test_a")
|
||||||
|
test_b = ComponentProperty("test_b", my_int=3, my_list=[1, 2, 3])
|
||||||
|
|
||||||
|
|
||||||
|
class TestComponents(EvenniaTest):
|
||||||
|
character_typeclass = CharacterWithComponents
|
||||||
|
|
||||||
|
def test_character_has_class_components(self):
|
||||||
|
assert self.char1.test_a
|
||||||
|
assert self.char1.test_b
|
||||||
|
|
||||||
|
def test_character_instances_components_properly(self):
|
||||||
|
assert isinstance(self.char1.test_a, ComponentTestA)
|
||||||
|
assert isinstance(self.char1.test_b, ComponentTestB)
|
||||||
|
|
||||||
|
def test_character_assigns_default_value(self):
|
||||||
|
assert self.char1.test_a.my_int == 1
|
||||||
|
assert self.char1.test_a.my_list == []
|
||||||
|
|
||||||
|
def test_character_assigns_default_provided_values(self):
|
||||||
|
assert self.char1.test_b.my_int == 3
|
||||||
|
assert self.char1.test_b.my_list == [1, 2, 3]
|
||||||
|
|
||||||
|
def test_character_can_register_runtime_component(self):
|
||||||
|
rct = RuntimeComponentTestC.create(self.char1)
|
||||||
|
self.char1.components.add(rct)
|
||||||
|
test_c = self.char1.components.get('test_c')
|
||||||
|
|
||||||
|
assert test_c
|
||||||
|
assert test_c.my_int == 6
|
||||||
|
assert test_c.my_dict == {}
|
||||||
|
|
||||||
|
def test_handler_can_add_default_component(self):
|
||||||
|
self.char1.components.add_default("test_c")
|
||||||
|
test_c = self.char1.components.get("test_c")
|
||||||
|
|
||||||
|
assert test_c
|
||||||
|
assert test_c.my_int == 6
|
||||||
|
|
||||||
|
def test_handler_has_returns_true_for_any_components(self):
|
||||||
|
rct = RuntimeComponentTestC.create(self.char1)
|
||||||
|
handler = self.char1.components
|
||||||
|
handler.add(rct)
|
||||||
|
|
||||||
|
assert handler.has("test_a")
|
||||||
|
assert handler.has("test_b")
|
||||||
|
assert handler.has("test_c")
|
||||||
|
|
||||||
|
def test_can_remove_component(self):
|
||||||
|
rct = RuntimeComponentTestC.create(self.char1)
|
||||||
|
handler = self.char1.components
|
||||||
|
handler.add(rct)
|
||||||
|
handler.remove(rct)
|
||||||
|
|
||||||
|
assert handler.has("test_a")
|
||||||
|
assert handler.has("test_b")
|
||||||
|
assert not handler.has("test_c")
|
||||||
|
|
||||||
|
def test_can_remove_component_by_name(self):
|
||||||
|
rct = RuntimeComponentTestC.create(self.char1)
|
||||||
|
handler = self.char1.components
|
||||||
|
handler.add(rct)
|
||||||
|
handler.remove_by_name("test_c")
|
||||||
|
|
||||||
|
assert handler.has("test_a")
|
||||||
|
assert handler.has("test_b")
|
||||||
|
assert not handler.has("test_c")
|
||||||
|
|
||||||
|
def test_cannot_replace_component(self):
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
self.char1.test_a = None
|
||||||
|
|
||||||
|
def test_can_get_component(self):
|
||||||
|
rct = RuntimeComponentTestC.create(self.char1)
|
||||||
|
handler = self.char1.components
|
||||||
|
handler.add(rct)
|
||||||
|
|
||||||
|
assert handler.get("test_c") is rct
|
||||||
|
|
||||||
|
def test_can_access_component_regular_get(self):
|
||||||
|
assert self.char1.cmp.test_a is self.char1.components.get('test_a')
|
||||||
|
|
||||||
|
def test_returns_none_with_regular_get_when_no_attribute(self):
|
||||||
|
assert self.char1.cmp.does_not_exist is None
|
||||||
Loading…
Add table
Add a link
Reference in a new issue