mirror of
https://github.com/evennia/evennia.git
synced 2026-03-28 02:36:32 +01:00
Refactor prototype-functionality into its own package
This commit is contained in:
parent
01acdccd6d
commit
054cba42bf
9 changed files with 1978 additions and 1752 deletions
|
|
@ -647,6 +647,22 @@ def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False,
|
|||
default=default, access_type=access_type)
|
||||
|
||||
|
||||
def validate_lockstring(lockstring):
|
||||
"""
|
||||
Validate so lockstring is on a valid form.
|
||||
|
||||
Args:
|
||||
lockstring (str): Lockstring to validate.
|
||||
|
||||
Returns:
|
||||
is_valid (bool): If the lockstring is valid or not.
|
||||
error (str or None): A string describing the error, or None
|
||||
if no error was found.
|
||||
|
||||
"""
|
||||
return _LOCK_HANDLER.valdate(lockstring)
|
||||
|
||||
|
||||
def _test():
|
||||
# testing
|
||||
|
||||
|
|
|
|||
145
evennia/prototypes/README.md
Normal file
145
evennia/prototypes/README.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# Prototypes
|
||||
|
||||
A 'Prototype' is a normal Python dictionary describing unique features of individual instance of a
|
||||
Typeclass. The prototype is used to 'spawn' a new instance with custom features detailed by said
|
||||
prototype. This allows for creating variations without having to create a large number of actual
|
||||
Typeclasses. It is a good way to allow Builders more freedom of creation without giving them full
|
||||
Python access to create Typeclasses.
|
||||
|
||||
For example, if a Typeclass 'Cat' describes all the coded differences between a Cat and
|
||||
other types of animals, then prototypes could be used to quickly create unique individual cats with
|
||||
different Attributes/properties (like different colors, stats, names etc) without having to make a new
|
||||
Typeclass for each. Prototypes have inheritance and can be scripted when they are applied to create
|
||||
a new instance of a typeclass - a common example would be to randomize stats and name.
|
||||
|
||||
The prototype is a normal dictionary with specific keys. Almost all values can be callables
|
||||
triggered when the prototype is used to spawn a new instance. Below is an example:
|
||||
|
||||
```
|
||||
{
|
||||
# meta-keys - these are used only when listing prototypes in-game. Only prototype_key is mandatory,
|
||||
# but it must be globally unique.
|
||||
|
||||
"prototype_key": "base_goblin",
|
||||
"prototype_desc": "A basic goblin",
|
||||
"prototype_locks": "edit:all();spawn:all()",
|
||||
"prototype_tags": "mobs",
|
||||
|
||||
# fixed-meaning keys, modifying the spawned instance. 'typeclass' may be
|
||||
# replaced by 'parent', referring to the prototype_key of an existing prototype
|
||||
# to inherit from.
|
||||
|
||||
"typeclass": "types.objects.Monster",
|
||||
"key": "goblin grunt",
|
||||
"tags": ["mob", "evil", ('greenskin','mob')] # tags as well as tags with category etc
|
||||
"attrs": [("weapon", "sword")] # this allows to set Attributes with categories etc
|
||||
|
||||
# non-fixed keys are interpreted as Attributes and their
|
||||
|
||||
"health": lambda: randint(20,30),
|
||||
"resists": ["cold", "poison"],
|
||||
"attacks": ["fists"],
|
||||
"weaknesses": ["fire", "light"]
|
||||
}
|
||||
|
||||
```
|
||||
## Using prototypes
|
||||
|
||||
Prototypes are generally used as inputs to the `spawn` command:
|
||||
|
||||
@spawn prototype_key
|
||||
|
||||
This will spawn a new instance of the prototype in the caller's current location unless the
|
||||
`location` key of the prototype was set (see below). The caller must pass the prototype's 'spawn'
|
||||
lock to be able to use it.
|
||||
|
||||
@spawn/list [prototype_key]
|
||||
|
||||
will show all available prototypes along with meta info, or look at a specific prototype in detail.
|
||||
|
||||
|
||||
## Creating prototypes
|
||||
|
||||
The `spawn` command can also be used to directly create/update prototypes from in-game.
|
||||
|
||||
spawn/save {"prototype_key: "goblin", ... }
|
||||
|
||||
but it is probably more convenient to use the menu-driven prototype wizard:
|
||||
|
||||
spawn/menu goblin
|
||||
|
||||
In code:
|
||||
|
||||
```python
|
||||
|
||||
from evennia import prototypes
|
||||
|
||||
goblin = {"prototype_key": "goblin:, ... }
|
||||
|
||||
prototype = prototypes.save_prototype(caller, **goblin)
|
||||
|
||||
```
|
||||
|
||||
Prototypes will normally be stored in the database (internally this is done using a Script, holding
|
||||
the meta-info and the prototype). One can also define prototypes outside of the game by assigning
|
||||
the prototype dictionary to a global variable in a module defined by `settings.PROTOTYPE_MODULES`:
|
||||
|
||||
```python
|
||||
# in e.g. mygame/world/prototypes.py
|
||||
|
||||
GOBLIN = {
|
||||
"prototype_key": "goblin",
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Such prototypes cannot be modified from inside the game no matter what `edit` lock they are given
|
||||
(we refer to them as 'readonly') but can be a fast and efficient way to give builders a starting
|
||||
library of prototypes to inherit from.
|
||||
|
||||
## Valid Prototype keys
|
||||
|
||||
Every prototype key also accepts a callable (taking no arguments) for producing its value or a
|
||||
string with an $protfunc definition. That callable/protfunc must then return a value on a form the
|
||||
prototype key expects.
|
||||
|
||||
- `prototype_key` (str): name of this prototype. This is used when storing prototypes and should
|
||||
be unique. This should always be defined but for prototypes defined in modules, the
|
||||
variable holding the prototype dict will become the prototype_key if it's not explicitly
|
||||
given.
|
||||
- `prototype_desc` (str, optional): describes prototype in listings
|
||||
- `prototype_locks` (str, optional): locks for restricting access to this prototype. Locktypes
|
||||
supported are 'edit' and 'use'.
|
||||
- `prototype_tags` (list, optional): List of tags or tuples (tag, category) used to group prototype
|
||||
in listings
|
||||
|
||||
- `parent` (str or tuple, optional): name (`prototype_key`) of eventual parent prototype, or a
|
||||
list of parents for multiple left-to-right inheritance.
|
||||
- `prototype`: Deprecated. Same meaning as 'parent'.
|
||||
- `typeclass` (str, optional): if not set, will use typeclass of parent prototype or use
|
||||
`settings.BASE_OBJECT_TYPECLASS`
|
||||
- `key` (str, optional): the name of the spawned object. If not given this will set to a
|
||||
random hash
|
||||
- `location` (obj, optional): location of the object - a valid object or #dbref
|
||||
- `home` (obj or str, optional): valid object or #dbref
|
||||
- `destination` (obj or str, optional): only valid for exits (object or #dbref)
|
||||
|
||||
- `permissions` (str or list, optional): which permissions for spawned object to have
|
||||
- `locks` (str, optional): lock-string for the spawned object
|
||||
- `aliases` (str or list, optional): Aliases for the spawned object.
|
||||
- `exec` (str, optional): this is a string of python code to execute or a list of such
|
||||
codes. This can be used e.g. to trigger custom handlers on the object. The execution
|
||||
namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit
|
||||
this functionality to Developer/superusers. Usually it's better to use callables or
|
||||
prototypefuncs instead of this.
|
||||
- `tags` (str, tuple or list, optional): string or list of strings or tuples
|
||||
`(tagstr, category)`. Plain strings will be result in tags with no category (default tags).
|
||||
- `attrs` (tuple or list, optional): tuple or list of tuples of Attributes to add. This
|
||||
form allows more complex Attributes to be set. Tuples at least specify `(key, value)`
|
||||
but can also specify up to `(key, value, category, lockstring)`. If you want to specify a
|
||||
lockstring but not a category, set the category to `None`.
|
||||
- `ndb_<name>` (any): value of a nattribute (`ndb_` is stripped). This is usually not useful to
|
||||
put in a prototype unless the NAttribute is used immediately upon spawning.
|
||||
- `other` (any): any other name is interpreted as the key of an Attribute with
|
||||
its value. Such Attributes have no categories.
|
||||
0
evennia/prototypes/__init__.py
Normal file
0
evennia/prototypes/__init__.py
Normal file
709
evennia/prototypes/menus.py
Normal file
709
evennia/prototypes/menus.py
Normal file
|
|
@ -0,0 +1,709 @@
|
|||
"""
|
||||
|
||||
OLC Prototype menu nodes
|
||||
|
||||
"""
|
||||
|
||||
from evennia.utils.evmenu import EvMenu, list_node
|
||||
from evennia.utils.ansi import strip_ansi
|
||||
|
||||
# ------------------------------------------------------------
|
||||
#
|
||||
# OLC Prototype design menu
|
||||
#
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Helper functions
|
||||
|
||||
|
||||
def _get_menu_prototype(caller):
|
||||
|
||||
prototype = None
|
||||
if hasattr(caller.ndb._menutree, "olc_prototype"):
|
||||
prototype = caller.ndb._menutree.olc_prototype
|
||||
if not prototype:
|
||||
caller.ndb._menutree.olc_prototype = prototype = {}
|
||||
caller.ndb._menutree.olc_new = True
|
||||
return prototype
|
||||
|
||||
|
||||
def _is_new_prototype(caller):
|
||||
return hasattr(caller.ndb._menutree, "olc_new")
|
||||
|
||||
|
||||
def _set_menu_prototype(caller, field, value):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
prototype[field] = value
|
||||
caller.ndb._menutree.olc_prototype = prototype
|
||||
|
||||
|
||||
def _format_property(prop, required=False, prototype=None, cropper=None):
|
||||
|
||||
if prototype is not None:
|
||||
prop = prototype.get(prop, '')
|
||||
|
||||
out = prop
|
||||
if callable(prop):
|
||||
if hasattr(prop, '__name__'):
|
||||
out = "<{}>".format(prop.__name__)
|
||||
else:
|
||||
out = repr(prop)
|
||||
if is_iter(prop):
|
||||
out = ", ".join(str(pr) for pr in prop)
|
||||
if not out and required:
|
||||
out = "|rrequired"
|
||||
return " ({}|n)".format(cropper(out) if cropper else crop(out, _MENU_CROP_WIDTH))
|
||||
|
||||
|
||||
def _set_property(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Update a property. To be called by the 'goto' option variable.
|
||||
|
||||
Args:
|
||||
caller (Object, Account): The user of the wizard.
|
||||
raw_string (str): Input from user on given node - the new value to set.
|
||||
Kwargs:
|
||||
prop (str): Property name to edit with `raw_string`.
|
||||
processor (callable): Converts `raw_string` to a form suitable for saving.
|
||||
next_node (str): Where to redirect to after this has run.
|
||||
Returns:
|
||||
next_node (str): Next node to go to.
|
||||
|
||||
"""
|
||||
prop = kwargs.get("prop", "prototype_key")
|
||||
processor = kwargs.get("processor", None)
|
||||
next_node = kwargs.get("next_node", "node_index")
|
||||
|
||||
propname_low = prop.strip().lower()
|
||||
|
||||
if callable(processor):
|
||||
try:
|
||||
value = processor(raw_string)
|
||||
except Exception as err:
|
||||
caller.msg("Could not set {prop} to {value} ({err})".format(
|
||||
prop=prop.replace("_", "-").capitalize(), value=raw_string, err=str(err)))
|
||||
# this means we'll re-run the current node.
|
||||
return None
|
||||
else:
|
||||
value = raw_string
|
||||
|
||||
if not value:
|
||||
return next_node
|
||||
|
||||
prototype = _get_menu_prototype(caller)
|
||||
|
||||
# typeclass and prototype can't co-exist
|
||||
if propname_low == "typeclass":
|
||||
prototype.pop("prototype", None)
|
||||
if propname_low == "prototype":
|
||||
prototype.pop("typeclass", None)
|
||||
|
||||
caller.ndb._menutree.olc_prototype = prototype
|
||||
|
||||
caller.msg("Set {prop} to '{value}'.".format(prop, value=str(value)))
|
||||
|
||||
return next_node
|
||||
|
||||
|
||||
def _wizard_options(curr_node, prev_node, next_node, color="|W"):
|
||||
options = []
|
||||
if prev_node:
|
||||
options.append({"key": ("|wb|Wack", "b"),
|
||||
"desc": "{color}({node})|n".format(
|
||||
color=color, node=prev_node.replace("_", "-")),
|
||||
"goto": "node_{}".format(prev_node)})
|
||||
if next_node:
|
||||
options.append({"key": ("|wf|Worward", "f"),
|
||||
"desc": "{color}({node})|n".format(
|
||||
color=color, node=next_node.replace("_", "-")),
|
||||
"goto": "node_{}".format(next_node)})
|
||||
|
||||
if "index" not in (prev_node, next_node):
|
||||
options.append({"key": ("|wi|Wndex", "i"),
|
||||
"goto": "node_index"})
|
||||
|
||||
if curr_node:
|
||||
options.append({"key": ("|wv|Walidate prototype", "v"),
|
||||
"goto": ("node_validate_prototype", {"back": curr_node})})
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def _path_cropper(pythonpath):
|
||||
"Crop path to only the last component"
|
||||
return pythonpath.split('.')[-1]
|
||||
|
||||
|
||||
# Menu nodes
|
||||
|
||||
def node_index(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
|
||||
text = ("|c --- Prototype wizard --- |n\n\n"
|
||||
"Define the |yproperties|n of the prototype. All prototype values can be "
|
||||
"over-ridden at the time of spawning an instance of the prototype, but some are "
|
||||
"required.\n\n'|wMeta'-properties|n are not used in the prototype itself but are used "
|
||||
"to organize and list prototypes. The 'Meta-Key' uniquely identifies the prototype "
|
||||
"and allows you to edit an existing prototype or save a new one for use by you or "
|
||||
"others later.\n\n(make choice; q to abort. If unsure, start from 1.)")
|
||||
|
||||
options = []
|
||||
options.append(
|
||||
{"desc": "|WPrototype-Key|n|n{}".format(_format_property("Key", True, prototype, None)),
|
||||
"goto": "node_prototype_key"})
|
||||
for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks',
|
||||
'Permissions', 'Location', 'Home', 'Destination'):
|
||||
required = False
|
||||
cropper = None
|
||||
if key in ("Prototype", "Typeclass"):
|
||||
required = "prototype" not in prototype and "typeclass" not in prototype
|
||||
if key == 'Typeclass':
|
||||
cropper = _path_cropper
|
||||
options.append(
|
||||
{"desc": "|w{}|n{}".format(
|
||||
key, _format_property(key, required, prototype, cropper=cropper)),
|
||||
"goto": "node_{}".format(key.lower())})
|
||||
required = False
|
||||
for key in ('Desc', 'Tags', 'Locks'):
|
||||
options.append(
|
||||
{"desc": "|WPrototype-{}|n|n{}".format(key, _format_property(key, required, prototype, None)),
|
||||
"goto": "node_prototype_{}".format(key.lower())})
|
||||
|
||||
return text, options
|
||||
|
||||
|
||||
def node_validate_prototype(caller, raw_string, **kwargs):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
|
||||
txt = prototype_to_str(prototype)
|
||||
errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)"
|
||||
try:
|
||||
# validate, don't spawn
|
||||
spawn(prototype, return_prototypes=True)
|
||||
except RuntimeError as err:
|
||||
errors = "\n\n|rError: {}|n".format(err)
|
||||
text = (txt + errors)
|
||||
|
||||
options = _wizard_options(None, kwargs.get("back"), None)
|
||||
|
||||
return text, options
|
||||
|
||||
|
||||
def _check_prototype_key(caller, key):
|
||||
old_prototype = search_prototype(key)
|
||||
olc_new = _is_new_prototype(caller)
|
||||
key = key.strip().lower()
|
||||
if old_prototype:
|
||||
# we are starting a new prototype that matches an existing
|
||||
if not caller.locks.check_lockstring(
|
||||
caller, old_prototype['prototype_locks'], access_type='edit'):
|
||||
# return to the node_prototype_key to try another key
|
||||
caller.msg("Prototype '{key}' already exists and you don't "
|
||||
"have permission to edit it.".format(key=key))
|
||||
return "node_prototype_key"
|
||||
elif olc_new:
|
||||
# we are selecting an existing prototype to edit. Reset to index.
|
||||
del caller.ndb._menutree.olc_new
|
||||
caller.ndb._menutree.olc_prototype = old_prototype
|
||||
caller.msg("Prototype already exists. Reloading.")
|
||||
return "node_index"
|
||||
|
||||
return _set_property(caller, key, prop='prototype_key', next_node="node_prototype")
|
||||
|
||||
|
||||
def node_prototype_key(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
text = ["The prototype name, or |wMeta-Key|n, uniquely identifies the prototype. "
|
||||
"It is used to find and use the prototype to spawn new entities. "
|
||||
"It is not case sensitive."]
|
||||
old_key = prototype.get('prototype_key', None)
|
||||
if old_key:
|
||||
text.append("Current key is '|w{key}|n'".format(key=old_key))
|
||||
else:
|
||||
text.append("The key is currently unset.")
|
||||
text.append("Enter text or make a choice (q for quit)")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("prototype_key", "index", "prototype")
|
||||
options.append({"key": "_default",
|
||||
"goto": _check_prototype_key})
|
||||
return text, options
|
||||
|
||||
|
||||
def _all_prototypes(caller):
|
||||
return [prototype["prototype_key"]
|
||||
for prototype in search_prototype() if "prototype_key" in prototype]
|
||||
|
||||
|
||||
def _prototype_examine(caller, prototype_name):
|
||||
prototypes = search_prototype(key=prototype_name)
|
||||
if prototypes:
|
||||
caller.msg(prototype_to_str(prototypes[0]))
|
||||
caller.msg("Prototype not registered.")
|
||||
return None
|
||||
|
||||
|
||||
def _prototype_select(caller, prototype):
|
||||
ret = _set_property(caller, prototype, prop="prototype", processor=str, next_node="node_key")
|
||||
caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype))
|
||||
return ret
|
||||
|
||||
|
||||
@list_node(_all_prototypes, _prototype_select)
|
||||
def node_prototype(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
|
||||
prot_parent_key = prototype.get('prototype')
|
||||
|
||||
text = ["Set the prototype's |yParent Prototype|n. If this is unset, Typeclass will be used."]
|
||||
if prot_parent_key:
|
||||
prot_parent = search_prototype(prot_parent_key)
|
||||
if prot_parent:
|
||||
text.append("Current parent prototype is {}:\n{}".format(prototype_to_str(prot_parent)))
|
||||
else:
|
||||
text.append("Current parent prototype |r{prototype}|n "
|
||||
"does not appear to exist.".format(prot_parent_key))
|
||||
else:
|
||||
text.append("Parent prototype is not set")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("prototype", "prototype_key", "typeclass", color="|W")
|
||||
options.append({"key": "_default",
|
||||
"goto": _prototype_examine})
|
||||
|
||||
return text, options
|
||||
|
||||
|
||||
def _all_typeclasses(caller):
|
||||
return list(sorted(get_all_typeclasses().keys()))
|
||||
|
||||
|
||||
def _typeclass_examine(caller, typeclass_path):
|
||||
if typeclass_path is None:
|
||||
# this means we are exiting the listing
|
||||
return "node_key"
|
||||
|
||||
typeclass = get_all_typeclasses().get(typeclass_path)
|
||||
if typeclass:
|
||||
docstr = []
|
||||
for line in typeclass.__doc__.split("\n"):
|
||||
if line.strip():
|
||||
docstr.append(line)
|
||||
elif docstr:
|
||||
break
|
||||
docstr = '\n'.join(docstr) if docstr else "<empty>"
|
||||
txt = "Typeclass |y{typeclass_path}|n; First paragraph of docstring:\n\n{docstring}".format(
|
||||
typeclass_path=typeclass_path, docstring=docstr)
|
||||
else:
|
||||
txt = "This is typeclass |y{}|n.".format(typeclass)
|
||||
caller.msg(txt)
|
||||
return None
|
||||
|
||||
|
||||
def _typeclass_select(caller, typeclass):
|
||||
ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key")
|
||||
caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass))
|
||||
return ret
|
||||
|
||||
|
||||
@list_node(_all_typeclasses, _typeclass_select)
|
||||
def node_typeclass(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
typeclass = prototype.get("typeclass")
|
||||
|
||||
text = ["Set the typeclass's parent |yTypeclass|n."]
|
||||
if typeclass:
|
||||
text.append("Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass))
|
||||
else:
|
||||
text.append("Using default typeclass {typeclass}.".format(
|
||||
typeclass=settings.BASE_OBJECT_TYPECLASS))
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("typeclass", "prototype", "key", color="|W")
|
||||
options.append({"key": "_default",
|
||||
"goto": _typeclass_examine})
|
||||
return text, options
|
||||
|
||||
|
||||
def node_key(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
key = prototype.get("key")
|
||||
|
||||
text = ["Set the prototype's |yKey|n. This will retain case sensitivity."]
|
||||
if key:
|
||||
text.append("Current key value is '|y{key}|n'.".format(key=key))
|
||||
else:
|
||||
text.append("Key is currently unset.")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("key", "typeclass", "aliases")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="key",
|
||||
processor=lambda s: s.strip(),
|
||||
next_node="node_aliases"))})
|
||||
return text, options
|
||||
|
||||
|
||||
def node_aliases(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
aliases = prototype.get("aliases")
|
||||
|
||||
text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. "
|
||||
"ill retain case sensitivity."]
|
||||
if aliases:
|
||||
text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases))
|
||||
else:
|
||||
text.append("No aliases are set.")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("aliases", "key", "attrs")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="aliases",
|
||||
processor=lambda s: [part.strip() for part in s.split(",")],
|
||||
next_node="node_attrs"))})
|
||||
return text, options
|
||||
|
||||
|
||||
def _caller_attrs(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
attrs = prototype.get("attrs", [])
|
||||
return attrs
|
||||
|
||||
|
||||
def _attrparse(caller, attr_string):
|
||||
"attr is entering on the form 'attr = value'"
|
||||
|
||||
if '=' in attr_string:
|
||||
attrname, value = (part.strip() for part in attr_string.split('=', 1))
|
||||
attrname = attrname.lower()
|
||||
if attrname:
|
||||
try:
|
||||
value = literal_eval(value)
|
||||
except SyntaxError:
|
||||
caller.msg(_MENU_ATTR_LITERAL_EVAL_ERROR)
|
||||
else:
|
||||
return attrname, value
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
def _add_attr(caller, attr_string, **kwargs):
|
||||
attrname, value = _attrparse(caller, attr_string)
|
||||
if attrname:
|
||||
prot = _get_menu_prototype(caller)
|
||||
prot['attrs'][attrname] = value
|
||||
_set_menu_prototype(caller, "prototype", prot)
|
||||
text = "Added"
|
||||
else:
|
||||
text = "Attribute must be given as 'attrname = <value>' where <value> uses valid Python."
|
||||
options = {"key": "_default",
|
||||
"goto": lambda caller: None}
|
||||
return text, options
|
||||
|
||||
|
||||
def _edit_attr(caller, attrname, new_value, **kwargs):
|
||||
attrname, value = _attrparse("{}={}".format(caller, attrname, new_value))
|
||||
if attrname:
|
||||
prot = _get_menu_prototype(caller)
|
||||
prot['attrs'][attrname] = value
|
||||
text = "Edited Attribute {} = {}".format(attrname, value)
|
||||
else:
|
||||
text = "Attribute value must be valid Python."
|
||||
options = {"key": "_default",
|
||||
"goto": lambda caller: None}
|
||||
return text, options
|
||||
|
||||
|
||||
def _examine_attr(caller, selection):
|
||||
prot = _get_menu_prototype(caller)
|
||||
value = prot['attrs'][selection]
|
||||
return "Attribute {} = {}".format(selection, value)
|
||||
|
||||
|
||||
@list_node(_caller_attrs)
|
||||
def node_attrs(caller):
|
||||
prot = _get_menu_prototype(caller)
|
||||
attrs = prot.get("attrs")
|
||||
|
||||
text = ["Set the prototype's |yAttributes|n. Separate multiple attrs with commas. "
|
||||
"Will retain case sensitivity."]
|
||||
if attrs:
|
||||
text.append("Current attrs are '|y{attrs}|n'.".format(attrs=attrs))
|
||||
else:
|
||||
text.append("No attrs are set.")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("attrs", "aliases", "tags")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="attrs",
|
||||
processor=lambda s: [part.strip() for part in s.split(",")],
|
||||
next_node="node_tags"))})
|
||||
return text, options
|
||||
|
||||
|
||||
def _caller_tags(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
tags = prototype.get("tags")
|
||||
return tags
|
||||
|
||||
|
||||
def _add_tag(caller, tag, **kwargs):
|
||||
tag = tag.strip().lower()
|
||||
prototype = _get_menu_prototype(caller)
|
||||
tags = prototype.get('tags', [])
|
||||
if tags:
|
||||
if tag not in tags:
|
||||
tags.append(tag)
|
||||
else:
|
||||
tags = [tag]
|
||||
prot['tags'] = tags
|
||||
_set_menu_prototype(caller, "prototype", prot)
|
||||
text = kwargs.get("text")
|
||||
if not text:
|
||||
text = "Added tag {}. (return to continue)".format(tag)
|
||||
options = {"key": "_default",
|
||||
"goto": lambda caller: None}
|
||||
return text, options
|
||||
|
||||
|
||||
def _edit_tag(caller, old_tag, new_tag, **kwargs):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
tags = prototype.get('tags', [])
|
||||
|
||||
old_tag = old_tag.strip().lower()
|
||||
new_tag = new_tag.strip().lower()
|
||||
tags[tags.index(old_tag)] = new_tag
|
||||
prototype['tags'] = tags
|
||||
_set_menu_prototype(caller, 'prototype', prototype)
|
||||
|
||||
text = kwargs.get('text')
|
||||
if not text:
|
||||
text = "Changed tag {} to {}.".format(old_tag, new_tag)
|
||||
options = {"key": "_default",
|
||||
"goto": lambda caller: None}
|
||||
return text, options
|
||||
|
||||
|
||||
@list_node(_caller_tags)
|
||||
def node_tags(caller):
|
||||
text = "Set the prototype's |yTags|n."
|
||||
options = _wizard_options("tags", "attrs", "locks")
|
||||
return text, options
|
||||
|
||||
|
||||
def node_locks(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
locks = prototype.get("locks")
|
||||
|
||||
text = ["Set the prototype's |yLock string|n. Separate multiple locks with semi-colons. "
|
||||
"Will retain case sensitivity."]
|
||||
if locks:
|
||||
text.append("Current locks are '|y{locks}|n'.".format(locks=locks))
|
||||
else:
|
||||
text.append("No locks are set.")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("locks", "tags", "permissions")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="locks",
|
||||
processor=lambda s: s.strip(),
|
||||
next_node="node_permissions"))})
|
||||
return text, options
|
||||
|
||||
|
||||
def node_permissions(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
permissions = prototype.get("permissions")
|
||||
|
||||
text = ["Set the prototype's |yPermissions|n. Separate multiple permissions with commas. "
|
||||
"Will retain case sensitivity."]
|
||||
if permissions:
|
||||
text.append("Current permissions are '|y{permissions}|n'.".format(permissions=permissions))
|
||||
else:
|
||||
text.append("No permissions are set.")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("permissions", "destination", "location")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="permissions",
|
||||
processor=lambda s: [part.strip() for part in s.split(",")],
|
||||
next_node="node_location"))})
|
||||
return text, options
|
||||
|
||||
|
||||
def node_location(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
location = prototype.get("location")
|
||||
|
||||
text = ["Set the prototype's |yLocation|n"]
|
||||
if location:
|
||||
text.append("Current location is |y{location}|n.".format(location=location))
|
||||
else:
|
||||
text.append("Default location is {}'s inventory.".format(caller))
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("location", "permissions", "home")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="location",
|
||||
processor=lambda s: s.strip(),
|
||||
next_node="node_home"))})
|
||||
return text, options
|
||||
|
||||
|
||||
def node_home(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
home = prototype.get("home")
|
||||
|
||||
text = ["Set the prototype's |yHome location|n"]
|
||||
if home:
|
||||
text.append("Current home location is |y{home}|n.".format(home=home))
|
||||
else:
|
||||
text.append("Default home location (|y{home}|n) used.".format(home=settings.DEFAULT_HOME))
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("home", "aliases", "destination")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="home",
|
||||
processor=lambda s: s.strip(),
|
||||
next_node="node_destination"))})
|
||||
return text, options
|
||||
|
||||
|
||||
def node_destination(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
dest = prototype.get("dest")
|
||||
|
||||
text = ["Set the prototype's |yDestination|n. This is usually only used for Exits."]
|
||||
if dest:
|
||||
text.append("Current destination is |y{dest}|n.".format(dest=dest))
|
||||
else:
|
||||
text.append("No destination is set (default).")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("destination", "home", "prototype_desc")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="dest",
|
||||
processor=lambda s: s.strip(),
|
||||
next_node="node_prototype_desc"))})
|
||||
return text, options
|
||||
|
||||
|
||||
def node_prototype_desc(caller):
|
||||
|
||||
prototype = _get_menu_prototype(caller)
|
||||
text = ["The |wMeta-Description|n briefly describes the prototype for viewing in listings."]
|
||||
desc = prototype.get("prototype_desc", None)
|
||||
|
||||
if desc:
|
||||
text.append("The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc))
|
||||
else:
|
||||
text.append("Description is currently unset.")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("prototype_desc", "prototype_key", "prototype_tags")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop='prototype_desc',
|
||||
processor=lambda s: s.strip(),
|
||||
next_node="node_prototype_tags"))})
|
||||
|
||||
return text, options
|
||||
|
||||
|
||||
def node_prototype_tags(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. "
|
||||
"Separate multiple by tags by commas."]
|
||||
tags = prototype.get('prototype_tags', [])
|
||||
|
||||
if tags:
|
||||
text.append("The current tags are:\n|w{tags}|n".format(tags=tags))
|
||||
else:
|
||||
text.append("No tags are currently set.")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="prototype_tags",
|
||||
processor=lambda s: [
|
||||
str(part.strip().lower()) for part in s.split(",")],
|
||||
next_node="node_prototype_locks"))})
|
||||
return text, options
|
||||
|
||||
|
||||
def node_prototype_locks(caller):
|
||||
prototype = _get_menu_prototype(caller)
|
||||
text = ["Set |wMeta-Locks|n on the prototype. There are two valid lock types: "
|
||||
"'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n"
|
||||
"(If you are unsure, leave as default.)"]
|
||||
locks = prototype.get('prototype_locks', '')
|
||||
if locks:
|
||||
text.append("Current lock is |w'{lockstring}'|n".format(lockstring=locks))
|
||||
else:
|
||||
text.append("Lock unset - if not changed the default lockstring will be set as\n"
|
||||
" |w'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id))
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("prototype_locks", "prototype_tags", "index")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="prototype_locks",
|
||||
processor=lambda s: s.strip().lower(),
|
||||
next_node="node_index"))})
|
||||
return text, options
|
||||
|
||||
|
||||
class OLCMenu(EvMenu):
|
||||
"""
|
||||
A custom EvMenu with a different formatting for the options.
|
||||
|
||||
"""
|
||||
def options_formatter(self, optionlist):
|
||||
"""
|
||||
Split the options into two blocks - olc options and normal options
|
||||
|
||||
"""
|
||||
olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype")
|
||||
olc_options = []
|
||||
other_options = []
|
||||
for key, desc in optionlist:
|
||||
raw_key = strip_ansi(key)
|
||||
if raw_key in olc_keys:
|
||||
desc = " {}".format(desc) if desc else ""
|
||||
olc_options.append("|lc{}|lt{}|le{}".format(raw_key, key, desc))
|
||||
else:
|
||||
other_options.append((key, desc))
|
||||
|
||||
olc_options = " | ".join(olc_options) + " | " + "|wq|Wuit" if olc_options else ""
|
||||
other_options = super(OLCMenu, self).options_formatter(other_options)
|
||||
sep = "\n\n" if olc_options and other_options else ""
|
||||
|
||||
return "{}{}{}".format(olc_options, sep, other_options)
|
||||
|
||||
|
||||
def start_olc(caller, session=None, prototype=None):
|
||||
"""
|
||||
Start menu-driven olc system for prototypes.
|
||||
|
||||
Args:
|
||||
caller (Object or Account): The entity starting the menu.
|
||||
session (Session, optional): The individual session to get data.
|
||||
prototype (dict, optional): Given when editing an existing
|
||||
prototype rather than creating a new one.
|
||||
|
||||
"""
|
||||
menudata = {"node_index": node_index,
|
||||
"node_validate_prototype": node_validate_prototype,
|
||||
"node_prototype_key": node_prototype_key,
|
||||
"node_prototype": node_prototype,
|
||||
"node_typeclass": node_typeclass,
|
||||
"node_key": node_key,
|
||||
"node_aliases": node_aliases,
|
||||
"node_attrs": node_attrs,
|
||||
"node_tags": node_tags,
|
||||
"node_locks": node_locks,
|
||||
"node_permissions": node_permissions,
|
||||
"node_location": node_location,
|
||||
"node_home": node_home,
|
||||
"node_destination": node_destination,
|
||||
"node_prototype_desc": node_prototype_desc,
|
||||
"node_prototype_tags": node_prototype_tags,
|
||||
"node_prototype_locks": node_prototype_locks,
|
||||
}
|
||||
OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=prototype)
|
||||
|
||||
78
evennia/prototypes/protfuncs.py
Normal file
78
evennia/prototypes/protfuncs.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""
|
||||
Protfuncs are function-strings embedded in a prototype and allows for a builder to create a
|
||||
prototype with custom logics without having access to Python. The Protfunc is parsed using the
|
||||
inlinefunc parser but is fired at the moment the spawning happens, using the creating object's
|
||||
session as input.
|
||||
|
||||
In the prototype dict, the protfunc is specified as a string inside the prototype, e.g.:
|
||||
|
||||
{ ...
|
||||
|
||||
"key": "$funcname(arg1, arg2, ...)"
|
||||
|
||||
... }
|
||||
|
||||
and multiple functions can be nested (no keyword args are supported). The result will be used as the
|
||||
value for that prototype key for that individual spawn.
|
||||
|
||||
Available protfuncs are callables in one of the modules of `settings.PROTOTYPEFUNC_MODULES`. They
|
||||
are specified as functions
|
||||
|
||||
def funcname (*args, **kwargs)
|
||||
|
||||
where *args are the arguments given in the prototype, and **kwargs are inserted by Evennia:
|
||||
|
||||
- session (Session): The Session of the entity spawning using this prototype.
|
||||
- prototype_key (str): The currently spawning prototype-key.
|
||||
- prototype (dict): The dict this protfunc is a part of.
|
||||
|
||||
Any traceback raised by this function will be handled at the time of spawning and abort the spawn
|
||||
before any object is created/updated. It must otherwise return the value to store for the specified
|
||||
prototype key (this value must be possible to serialize in an Attribute).
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from evennia.utils import inlinefuncs
|
||||
from evennia.utils.utils import callables_from_module
|
||||
|
||||
|
||||
_PROTOTYPEFUNCS = {}
|
||||
|
||||
for mod in settings.PROTOTYPEFUNC_MODULES:
|
||||
try:
|
||||
callables = callables_from_module(mod)
|
||||
if mod == __name__:
|
||||
callables.pop("protfunc_parser")
|
||||
_PROTOTYPEFUNCS.update(callables)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def protfunc_parser(value, available_functions=None, **kwargs):
|
||||
"""
|
||||
Parse a prototype value string for a protfunc and process it.
|
||||
|
||||
Available protfuncs are specified as callables in one of the modules of
|
||||
`settings.PROTOTYPEFUNC_MODULES`, or specified on the command line.
|
||||
|
||||
Args:
|
||||
value (string): The value to test for a parseable protfunc.
|
||||
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing.
|
||||
|
||||
Kwargs:
|
||||
any (any): Passed on to the inlinefunc.
|
||||
|
||||
Returns:
|
||||
any (any): A structure to replace the string on the prototype level. If this is a
|
||||
callable or a (callable, (args,)) structure, it will be executed as if one had supplied
|
||||
it to the prototype directly.
|
||||
|
||||
"""
|
||||
if not isinstance(value, basestring):
|
||||
return value
|
||||
available_functions = _PROTOTYPEFUNCS if available_functions is None else available_functions
|
||||
return inlinefuncs.parse_inlinefunc(value, _available_funcs=available_functions, **kwargs)
|
||||
|
||||
|
||||
# default protfuncs
|
||||
280
evennia/prototypes/prototypes.py
Normal file
280
evennia/prototypes/prototypes.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
"""
|
||||
|
||||
Handling storage of prototypes, both database-based ones (DBPrototypes) and those defined in modules
|
||||
(Read-only prototypes).
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.utils.create import create_script
|
||||
from evennia.utils.utils import all_from_module, make_iter, callables_from_module, is_iter
|
||||
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
|
||||
from evennia.utils import logger
|
||||
|
||||
|
||||
_MODULE_PROTOTYPE_MODULES = {}
|
||||
_MODULE_PROTOTYPES = {}
|
||||
|
||||
|
||||
class ValidationError(RuntimeError):
|
||||
"""
|
||||
Raised on prototype validation errors
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# module-based prototypes
|
||||
|
||||
for mod in settings.PROTOTYPE_MODULES:
|
||||
# to remove a default prototype, override it with an empty dict.
|
||||
# internally we store as (key, desc, locks, tags, prototype_dict)
|
||||
prots = [(prototype_key, prot) for prototype_key, prot in all_from_module(mod).items()
|
||||
if prot and isinstance(prot, dict)]
|
||||
# assign module path to each prototype_key for easy reference
|
||||
_MODULE_PROTOTYPE_MODULES.update({prototype_key: mod for prototype_key, _ in prots})
|
||||
# make sure the prototype contains all meta info
|
||||
for prototype_key, prot in prots:
|
||||
actual_prot_key = prot.get('prototype_key', prototype_key).lower()
|
||||
prot.update({
|
||||
"prototype_key": actual_prot_key,
|
||||
"prototype_desc": prot['prototype_desc'] if 'prototype_desc' in prot else mod,
|
||||
"prototype_locks": (prot['prototype_locks']
|
||||
if 'prototype_locks' in prot else "use:all();edit:false()"),
|
||||
"prototype_tags": list(set(make_iter(prot.get('prototype_tags', [])) + ["module"]))})
|
||||
_MODULE_PROTOTYPES[actual_prot_key] = prot
|
||||
|
||||
|
||||
# Db-based prototypes
|
||||
|
||||
|
||||
class DbPrototype(DefaultScript):
|
||||
"""
|
||||
This stores a single prototype, in an Attribute `prototype`.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
self.key = "empty prototype" # prototype_key
|
||||
self.desc = "A prototype" # prototype_desc
|
||||
self.db.prototype = {} # actual prototype
|
||||
|
||||
|
||||
# General prototype functions
|
||||
|
||||
def check_permission(prototype_key, action, default=True):
|
||||
"""
|
||||
Helper function to check access to actions on given prototype.
|
||||
|
||||
Args:
|
||||
prototype_key (str): The prototype to affect.
|
||||
action (str): One of "spawn" or "edit".
|
||||
default (str): If action is unknown or prototype has no locks
|
||||
|
||||
Returns:
|
||||
passes (bool): If permission for action is granted or not.
|
||||
|
||||
"""
|
||||
if action == 'edit':
|
||||
if prototype_key in _MODULE_PROTOTYPES:
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
|
||||
logger.log_err("{} is a read-only prototype "
|
||||
"(defined as code in {}).".format(prototype_key, mod))
|
||||
return False
|
||||
|
||||
prototype = search_prototype(key=prototype_key)
|
||||
if not prototype:
|
||||
logger.log_err("Prototype {} not found.".format(prototype_key))
|
||||
return False
|
||||
|
||||
lockstring = prototype.get("prototype_locks")
|
||||
|
||||
if lockstring:
|
||||
return check_lockstring(None, lockstring, default=default, access_type=action)
|
||||
return default
|
||||
|
||||
|
||||
def create_prototype(**kwargs):
|
||||
"""
|
||||
Store a prototype persistently.
|
||||
|
||||
Kwargs:
|
||||
prototype_key (str): This is required for any storage.
|
||||
All other kwargs are considered part of the new prototype dict.
|
||||
|
||||
Returns:
|
||||
prototype (dict or None): The prototype stored using the given kwargs, None if deleting.
|
||||
|
||||
Raises:
|
||||
prototypes.ValidationError: If prototype does not validate.
|
||||
|
||||
Note:
|
||||
No edit/spawn locks will be checked here - if this function is called the caller
|
||||
is expected to have valid permissions.
|
||||
|
||||
"""
|
||||
|
||||
def _to_batchtuple(inp, *args):
|
||||
"build tuple suitable for batch-creation"
|
||||
if is_iter(inp):
|
||||
# already a tuple/list, use as-is
|
||||
return inp
|
||||
return (inp, ) + args
|
||||
|
||||
prototype_key = kwargs.get("prototype_key")
|
||||
if not prototype_key:
|
||||
raise ValidationError("Prototype requires a prototype_key")
|
||||
|
||||
prototype_key = str(prototype_key).lower()
|
||||
|
||||
# we can't edit a prototype defined in a module
|
||||
if prototype_key in _MODULE_PROTOTYPES:
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
|
||||
raise PermissionError("{} is a read-only prototype "
|
||||
"(defined as code in {}).".format(prototype_key, mod))
|
||||
|
||||
# want to create- or edit
|
||||
prototype = kwargs
|
||||
|
||||
# make sure meta properties are included with defaults
|
||||
prototype['prototype_desc'] = prototype.get('prototype_desc', '')
|
||||
locks = prototype.get('prototype_locks', "spawn:all();edit:perm(Admin)")
|
||||
is_valid, err = validate_lockstring(locks)
|
||||
if not is_valid:
|
||||
raise ValidationError("Lock error: {}".format(err))
|
||||
prototype["prototype_locks"] = locks
|
||||
prototype["prototype_tags"] = [
|
||||
_to_batchtuple(tag, "db_prototype")
|
||||
for tag in make_iter(prototype.get("prototype_tags", []))]
|
||||
|
||||
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
|
||||
|
||||
if stored_prototype:
|
||||
# edit existing prototype
|
||||
stored_prototype = stored_prototype[0]
|
||||
|
||||
stored_prototype.desc = prototype['prototype_desc']
|
||||
stored_prototype.tags.batch_add(*prototype['prototype_tags'])
|
||||
stored_prototype.locks.add(prototype['prototype_locks'])
|
||||
stored_prototype.attributes.add('prototype', prototype)
|
||||
else:
|
||||
# create a new prototype
|
||||
stored_prototype = create_script(
|
||||
DbPrototype, key=prototype_key, desc=prototype['prototype_desc'], persistent=True,
|
||||
locks=locks, tags=prototype['prototype_tags'], attributes=[("prototype", prototype)])
|
||||
return stored_prototype
|
||||
|
||||
|
||||
def delete_prototype(key, caller=None):
|
||||
"""
|
||||
Delete a stored prototype
|
||||
|
||||
Args:
|
||||
key (str): The persistent prototype to delete.
|
||||
caller (Account or Object, optionsl): Caller aiming to delete a prototype.
|
||||
Note that no locks will be checked if`caller` is not passed.
|
||||
Returns:
|
||||
success (bool): If deletion worked or not.
|
||||
Raises:
|
||||
PermissionError: If 'edit' lock was not passed or deletion failed for some other reason.
|
||||
|
||||
"""
|
||||
if prototype_key in _MODULE_PROTOTYPES:
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
|
||||
raise PermissionError("{} is a read-only prototype "
|
||||
"(defined as code in {}).".format(prototype_key, mod))
|
||||
|
||||
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
|
||||
|
||||
if not stored_prototype:
|
||||
raise PermissionError("Prototype {} was not found.".format(prototype_key))
|
||||
if caller:
|
||||
if not stored_prototype.access(caller, 'edit'):
|
||||
raise PermissionError("{} does not have permission to "
|
||||
"delete prototype {}.".format(caller, prototype_key))
|
||||
stored_prototype.delete()
|
||||
return True
|
||||
|
||||
|
||||
def search_prototype(key=None, tags=None):
|
||||
"""
|
||||
Find prototypes based on key and/or tags, or all prototypes.
|
||||
|
||||
Kwargs:
|
||||
key (str): An exact or partial key to query for.
|
||||
tags (str or list): Tag key or keys to query for. These
|
||||
will always be applied with the 'db_protototype'
|
||||
tag category.
|
||||
|
||||
Return:
|
||||
matches (list): All found prototype dicts. If no keys
|
||||
or tags are given, all available prototypes will be returned.
|
||||
|
||||
Note:
|
||||
The available prototypes is a combination of those supplied in
|
||||
PROTOTYPE_MODULES and those stored in the database. Note that if
|
||||
tags are given and the prototype has no tags defined, it will not
|
||||
be found as a match.
|
||||
|
||||
"""
|
||||
# search module prototypes
|
||||
|
||||
mod_matches = {}
|
||||
if tags:
|
||||
# use tags to limit selection
|
||||
tagset = set(tags)
|
||||
mod_matches = {prototype_key: prototype
|
||||
for prototype_key, prototype in _MODULE_PROTOTYPES.items()
|
||||
if tagset.intersection(prototype.get("prototype_tags", []))}
|
||||
else:
|
||||
mod_matches = _MODULE_PROTOTYPES
|
||||
if key:
|
||||
if key in mod_matches:
|
||||
# exact match
|
||||
module_prototypes = [mod_matches[key]]
|
||||
else:
|
||||
# fuzzy matching
|
||||
module_prototypes = [prototype for prototype_key, prototype in mod_matches.items()
|
||||
if key in prototype_key]
|
||||
else:
|
||||
module_prototypes = [match for match in mod_matches.values()]
|
||||
|
||||
# search db-stored prototypes
|
||||
|
||||
if tags:
|
||||
# exact match on tag(s)
|
||||
tags = make_iter(tags)
|
||||
tag_categories = ["db_prototype" for _ in tags]
|
||||
db_matches = DbPrototype.objects.get_by_tag(tags, tag_categories)
|
||||
else:
|
||||
db_matches = DbPrototype.objects.all()
|
||||
if key:
|
||||
# exact or partial match on key
|
||||
db_matches = db_matches.filter(db_key=key) or db_matches.filter(db_key__icontains=key)
|
||||
# return prototype
|
||||
db_prototypes = [dict(dbprot.attributes.get("prototype", {})) for dbprot in db_matches]
|
||||
|
||||
matches = db_prototypes + module_prototypes
|
||||
nmatches = len(matches)
|
||||
if nmatches > 1 and key:
|
||||
key = key.lower()
|
||||
# avoid duplicates if an exact match exist between the two types
|
||||
filter_matches = [mta for mta in matches
|
||||
if mta.get('prototype_key') and mta['prototype_key'] == key]
|
||||
if filter_matches and len(filter_matches) < nmatches:
|
||||
matches = filter_matches
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def search_objects_with_prototype(prototype_key):
|
||||
"""
|
||||
Retrieve all object instances created by a given prototype.
|
||||
|
||||
Args:
|
||||
prototype_key (str): The exact (and unique) prototype identifier to query for.
|
||||
|
||||
Returns:
|
||||
matches (Queryset): All matching objects spawned from this prototype.
|
||||
|
||||
"""
|
||||
return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
|
||||
600
evennia/prototypes/spawner.py
Normal file
600
evennia/prototypes/spawner.py
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
"""
|
||||
Spawner
|
||||
|
||||
The spawner takes input files containing object definitions in
|
||||
dictionary forms. These use a prototype architecture to define
|
||||
unique objects without having to make a Typeclass for each.
|
||||
|
||||
The main function is `spawn(*prototype)`, where the `prototype`
|
||||
is a dictionary like this:
|
||||
|
||||
```python
|
||||
GOBLIN = {
|
||||
"typeclass": "types.objects.Monster",
|
||||
"key": "goblin grunt",
|
||||
"health": lambda: randint(20,30),
|
||||
"resists": ["cold", "poison"],
|
||||
"attacks": ["fists"],
|
||||
"weaknesses": ["fire", "light"]
|
||||
"tags": ["mob", "evil", ('greenskin','mob')]
|
||||
"attrs": [("weapon", "sword")]
|
||||
}
|
||||
```
|
||||
|
||||
Possible keywords are:
|
||||
prototype_key (str): name of this prototype. This is used when storing prototypes and should
|
||||
be unique. This should always be defined but for prototypes defined in modules, the
|
||||
variable holding the prototype dict will become the prototype_key if it's not explicitly
|
||||
given.
|
||||
prototype_desc (str, optional): describes prototype in listings
|
||||
prototype_locks (str, optional): locks for restricting access to this prototype. Locktypes
|
||||
supported are 'edit' and 'use'.
|
||||
prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype
|
||||
in listings
|
||||
|
||||
parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or
|
||||
a list of parents, for multiple left-to-right inheritance.
|
||||
prototype: Deprecated. Same meaning as 'parent'.
|
||||
typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use
|
||||
`settings.BASE_OBJECT_TYPECLASS`
|
||||
key (str or callable, optional): the name of the spawned object. If not given this will set to a
|
||||
random hash
|
||||
location (obj, str or callable, optional): location of the object - a valid object or #dbref
|
||||
home (obj, str or callable, optional): valid object or #dbref
|
||||
destination (obj, str or callable, optional): only valid for exits (object or #dbref)
|
||||
|
||||
permissions (str, list or callable, optional): which permissions for spawned object to have
|
||||
locks (str or callable, optional): lock-string for the spawned object
|
||||
aliases (str, list or callable, optional): Aliases for the spawned object
|
||||
exec (str or callable, optional): this is a string of python code to execute or a list of such
|
||||
codes. This can be used e.g. to trigger custom handlers on the object. The execution
|
||||
namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit
|
||||
this functionality to Developer/superusers. Usually it's better to use callables or
|
||||
prototypefuncs instead of this.
|
||||
tags (str, tuple, list or callable, optional): string or list of strings or tuples
|
||||
`(tagstr, category)`. Plain strings will be result in tags with no category (default tags).
|
||||
attrs (tuple, list or callable, optional): tuple or list of tuples of Attributes to add. This
|
||||
form allows more complex Attributes to be set. Tuples at least specify `(key, value)`
|
||||
but can also specify up to `(key, value, category, lockstring)`. If you want to specify a
|
||||
lockstring but not a category, set the category to `None`.
|
||||
ndb_<name> (any): value of a nattribute (ndb_ is stripped)
|
||||
other (any): any other name is interpreted as the key of an Attribute with
|
||||
its value. Such Attributes have no categories.
|
||||
|
||||
Each value can also be a callable that takes no arguments. It should
|
||||
return the value to enter into the field and will be called every time
|
||||
the prototype is used to spawn an object. Note, if you want to store
|
||||
a callable in an Attribute, embed it in a tuple to the `args` keyword.
|
||||
|
||||
By specifying the "prototype" key, the prototype becomes a child of
|
||||
that prototype, inheritng all prototype slots it does not explicitly
|
||||
define itself, while overloading those that it does specify.
|
||||
|
||||
```python
|
||||
import random
|
||||
|
||||
|
||||
GOBLIN_WIZARD = {
|
||||
"parent": GOBLIN,
|
||||
"key": "goblin wizard",
|
||||
"spells": ["fire ball", "lighting bolt"]
|
||||
}
|
||||
|
||||
GOBLIN_ARCHER = {
|
||||
"parent": GOBLIN,
|
||||
"key": "goblin archer",
|
||||
"attack_skill": (random, (5, 10))"
|
||||
"attacks": ["short bow"]
|
||||
}
|
||||
```
|
||||
|
||||
One can also have multiple prototypes. These are inherited from the
|
||||
left, with the ones further to the right taking precedence.
|
||||
|
||||
```python
|
||||
ARCHWIZARD = {
|
||||
"attack": ["archwizard staff", "eye of doom"]
|
||||
|
||||
GOBLIN_ARCHWIZARD = {
|
||||
"key" : "goblin archwizard"
|
||||
"parent": (GOBLIN_WIZARD, ARCHWIZARD),
|
||||
}
|
||||
```
|
||||
|
||||
The *goblin archwizard* will have some different attacks, but will
|
||||
otherwise have the same spells as a *goblin wizard* who in turn shares
|
||||
many traits with a normal *goblin*.
|
||||
|
||||
|
||||
Storage mechanism:
|
||||
|
||||
This sets up a central storage for prototypes. The idea is to make these
|
||||
available in a repository for buildiers to use. Each prototype is stored
|
||||
in a Script so that it can be tagged for quick sorting/finding and locked for limiting
|
||||
access.
|
||||
|
||||
This system also takes into consideration prototypes defined and stored in modules.
|
||||
Such prototypes are considered 'read-only' to the system and can only be modified
|
||||
in code. To replace a default prototype, add the same-name prototype in a
|
||||
custom module read later in the settings.PROTOTYPE_MODULES list. To remove a default
|
||||
prototype, override its name with an empty dict.
|
||||
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import time
|
||||
from ast import literal_eval
|
||||
from django.conf import settings
|
||||
from random import randint
|
||||
import evennia
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.utils.utils import (
|
||||
make_iter, dbid_to_obj,
|
||||
is_iter, crop, get_all_typeclasses)
|
||||
|
||||
from evennia.utils.evtable import EvTable
|
||||
|
||||
|
||||
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
|
||||
_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks")
|
||||
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES
|
||||
_MENU_CROP_WIDTH = 15
|
||||
_PROTOTYPE_TAG_CATEGORY = "spawned_by_prototype"
|
||||
|
||||
_MENU_ATTR_LITERAL_EVAL_ERROR = (
|
||||
"|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n"
|
||||
"You also need to use correct Python syntax. Remember especially to put quotes around all "
|
||||
"strings inside lists and dicts.|n")
|
||||
|
||||
|
||||
# Helper functions
|
||||
|
||||
def _to_obj(value, force=True):
|
||||
return dbid_to_obj(value, ObjectDB)
|
||||
|
||||
|
||||
def _to_obj_or_any(value):
|
||||
obj = dbid_to_obj(value, ObjectDB)
|
||||
return obj if obj is not None else value
|
||||
|
||||
|
||||
def validate_spawn_value(value, validator=None):
|
||||
"""
|
||||
Analyze the value and produce a value for use at the point of spawning.
|
||||
|
||||
Args:
|
||||
value (any): This can be:
|
||||
callable - will be called as callable()
|
||||
(callable, (args,)) - will be called as callable(*args)
|
||||
other - will be assigned depending on the variable type
|
||||
validator (callable, optional): If given, this will be called with the value to
|
||||
check and guarantee the outcome is of a given type.
|
||||
|
||||
Returns:
|
||||
any (any): The (potentially pre-processed value to use for this prototype key)
|
||||
|
||||
"""
|
||||
value = protfunc_parser(value)
|
||||
validator = validator if validator else lambda o: o
|
||||
if callable(value):
|
||||
return validator(value())
|
||||
elif value and is_iter(value) and callable(value[0]):
|
||||
# a structure (callable, (args, ))
|
||||
args = value[1:]
|
||||
return validator(value[0](*make_iter(args)))
|
||||
else:
|
||||
return validator(value)
|
||||
|
||||
# Spawner mechanism
|
||||
|
||||
|
||||
def validate_prototype(prototype, protkey=None, protparents=None, _visited=None):
|
||||
"""
|
||||
Run validation on a prototype, checking for inifinite regress.
|
||||
|
||||
Args:
|
||||
prototype (dict): Prototype to validate.
|
||||
protkey (str, optional): The name of the prototype definition. If not given, the prototype
|
||||
dict needs to have the `prototype_key` field set.
|
||||
protpartents (dict, optional): The available prototype parent library. If
|
||||
note given this will be determined from settings/database.
|
||||
_visited (list, optional): This is an internal work array and should not be set manually.
|
||||
Raises:
|
||||
RuntimeError: If prototype has invalid structure.
|
||||
|
||||
"""
|
||||
if not protparents:
|
||||
protparents = get_protparent_dict()
|
||||
if _visited is None:
|
||||
_visited = []
|
||||
|
||||
protkey = protkey and protkey.lower() or prototype.get('prototype_key', None)
|
||||
|
||||
assert isinstance(prototype, dict)
|
||||
|
||||
if id(prototype) in _visited:
|
||||
raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype)
|
||||
|
||||
_visited.append(id(prototype))
|
||||
protstrings = prototype.get("prototype")
|
||||
|
||||
if protstrings:
|
||||
for protstring in make_iter(protstrings):
|
||||
protstring = protstring.lower()
|
||||
if protkey is not None and protstring == protkey:
|
||||
raise RuntimeError("%s tries to prototype itself." % protkey or prototype)
|
||||
protparent = protparents.get(protstring)
|
||||
if not protparent:
|
||||
raise RuntimeError(
|
||||
"%s's prototype '%s' was not found." % (protkey or prototype, protstring))
|
||||
validate_prototype(protparent, protstring, protparents, _visited)
|
||||
|
||||
|
||||
def _get_prototype(dic, prot, protparents):
|
||||
"""
|
||||
Recursively traverse a prototype dictionary, including multiple
|
||||
inheritance. Use validate_prototype before this, we don't check
|
||||
for infinite recursion here.
|
||||
|
||||
"""
|
||||
if "prototype" in dic:
|
||||
# move backwards through the inheritance
|
||||
for prototype in make_iter(dic["prototype"]):
|
||||
# Build the prot dictionary in reverse order, overloading
|
||||
new_prot = _get_prototype(protparents.get(prototype.lower(), {}), prot, protparents)
|
||||
prot.update(new_prot)
|
||||
prot.update(dic)
|
||||
prot.pop("prototype", None) # we don't need this anymore
|
||||
return prot
|
||||
|
||||
|
||||
def prototype_diff_from_object(prototype, obj):
|
||||
"""
|
||||
Get a simple diff for a prototype compared to an object which may or may not already have a
|
||||
prototype (or has one but changed locally). For more complex migratations a manual diff may be
|
||||
needed.
|
||||
|
||||
Args:
|
||||
prototype (dict): Prototype.
|
||||
obj (Object): Object to
|
||||
|
||||
Returns:
|
||||
diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
|
||||
|
||||
"""
|
||||
prot1 = prototype
|
||||
prot2 = prototype_from_object(obj)
|
||||
|
||||
diff = {}
|
||||
for key, value in prot1.items():
|
||||
diff[key] = "KEEP"
|
||||
if key in prot2:
|
||||
if callable(prot2[key]) or value != prot2[key]:
|
||||
diff[key] = "UPDATE"
|
||||
elif key not in prot2:
|
||||
diff[key] = "REMOVE"
|
||||
|
||||
return diff
|
||||
|
||||
|
||||
def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
||||
"""
|
||||
Update existing objects with the latest version of the prototype.
|
||||
|
||||
Args:
|
||||
prototype (str or dict): Either the `prototype_key` to use or the
|
||||
prototype dict itself.
|
||||
diff (dict, optional): This a diff structure that describes how to update the protototype.
|
||||
If not given this will be constructed from the first object found.
|
||||
objects (list, optional): List of objects to update. If not given, query for these
|
||||
objects using the prototype's `prototype_key`.
|
||||
Returns:
|
||||
changed (int): The number of objects that had changes applied to them.
|
||||
|
||||
"""
|
||||
prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key']
|
||||
prototype_obj = search_db_prototype(prototype_key, return_queryset=True)
|
||||
prototype_obj = prototype_obj[0] if prototype_obj else None
|
||||
new_prototype = prototype_obj.db.prototype
|
||||
objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
|
||||
|
||||
if not objs:
|
||||
return 0
|
||||
|
||||
if not diff:
|
||||
diff = prototype_diff_from_object(new_prototype, objs[0])
|
||||
|
||||
changed = 0
|
||||
for obj in objs:
|
||||
do_save = False
|
||||
for key, directive in diff.items():
|
||||
val = new_prototype[key]
|
||||
if directive in ('UPDATE', 'REPLACE'):
|
||||
do_save = True
|
||||
if key == 'key':
|
||||
obj.db_key = validate_spawn_value(val, str)
|
||||
elif key == 'typeclass':
|
||||
obj.db_typeclass_path = validate_spawn_value(val, str)
|
||||
elif key == 'location':
|
||||
obj.db_location = validate_spawn_value(val, _to_obj)
|
||||
elif key == 'home':
|
||||
obj.db_home = validate_spawn_value(val, _to_obj)
|
||||
elif key == 'destination':
|
||||
obj.db_destination = validate_spawn_value(val, _to_obj)
|
||||
elif key == 'locks':
|
||||
if directive == 'REPLACE':
|
||||
obj.locks.clear()
|
||||
obj.locks.add(validate_spawn_value(val, str))
|
||||
elif key == 'permissions':
|
||||
if directive == 'REPLACE':
|
||||
obj.permissions.clear()
|
||||
obj.permissions.batch_add(validate_spawn_value(val, make_iter))
|
||||
elif key == 'aliases':
|
||||
if directive == 'REPLACE':
|
||||
obj.aliases.clear()
|
||||
obj.aliases.batch_add(validate_spawn_value(val, make_iter))
|
||||
elif key == 'tags':
|
||||
if directive == 'REPLACE':
|
||||
obj.tags.clear()
|
||||
obj.tags.batch_add(validate_spawn_value(val, make_iter))
|
||||
elif key == 'attrs':
|
||||
if directive == 'REPLACE':
|
||||
obj.attributes.clear()
|
||||
obj.attributes.batch_add(validate_spawn_value(val, make_iter))
|
||||
elif key == 'exec':
|
||||
# we don't auto-rerun exec statements, it would be huge security risk!
|
||||
pass
|
||||
else:
|
||||
obj.attributes.add(key, validate_spawn_value(val, _to_obj))
|
||||
elif directive == 'REMOVE':
|
||||
do_save = True
|
||||
if key == 'key':
|
||||
obj.db_key = ''
|
||||
elif key == 'typeclass':
|
||||
# fall back to default
|
||||
obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS
|
||||
elif key == 'location':
|
||||
obj.db_location = None
|
||||
elif key == 'home':
|
||||
obj.db_home = None
|
||||
elif key == 'destination':
|
||||
obj.db_destination = None
|
||||
elif key == 'locks':
|
||||
obj.locks.clear()
|
||||
elif key == 'permissions':
|
||||
obj.permissions.clear()
|
||||
elif key == 'aliases':
|
||||
obj.aliases.clear()
|
||||
elif key == 'tags':
|
||||
obj.tags.clear()
|
||||
elif key == 'attrs':
|
||||
obj.attributes.clear()
|
||||
elif key == 'exec':
|
||||
# we don't auto-rerun exec statements, it would be huge security risk!
|
||||
pass
|
||||
else:
|
||||
obj.attributes.remove(key)
|
||||
if do_save:
|
||||
changed += 1
|
||||
obj.save()
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def _batch_create_object(*objparams):
|
||||
"""
|
||||
This is a cut-down version of the create_object() function,
|
||||
optimized for speed. It does NOT check and convert various input
|
||||
so make sure the spawned Typeclass works before using this!
|
||||
|
||||
Args:
|
||||
objsparams (tuple): Each paremter tuple will create one object instance using the parameters within.
|
||||
The parameters should be given in the following order:
|
||||
- `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`.
|
||||
- `permissions` (str): Permission string used with `new_obj.batch_add(permission)`.
|
||||
- `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`.
|
||||
- `aliases` (list): A list of alias strings for
|
||||
adding with `new_object.aliases.batch_add(*aliases)`.
|
||||
- `nattributes` (list): list of tuples `(key, value)` to be loop-added to
|
||||
add with `new_obj.nattributes.add(*tuple)`.
|
||||
- `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for
|
||||
adding with `new_obj.attributes.batch_add(*attributes)`.
|
||||
- `tags` (list): list of tuples `(key, category)` for adding
|
||||
with `new_obj.tags.batch_add(*tags)`.
|
||||
- `execs` (list): Code strings to execute together with the creation
|
||||
of each object. They will be executed with `evennia` and `obj`
|
||||
(the newly created object) available in the namespace. Execution
|
||||
will happend after all other properties have been assigned and
|
||||
is intended for calling custom handlers etc.
|
||||
|
||||
Returns:
|
||||
objects (list): A list of created objects
|
||||
|
||||
Notes:
|
||||
The `exec` list will execute arbitrary python code so don't allow this to be available to
|
||||
unprivileged users!
|
||||
|
||||
"""
|
||||
|
||||
# bulk create all objects in one go
|
||||
|
||||
# unfortunately this doesn't work since bulk_create doesn't creates pks;
|
||||
# the result would be duplicate objects at the next stage, so we comment
|
||||
# it out for now:
|
||||
# dbobjs = _ObjectDB.objects.bulk_create(dbobjs)
|
||||
|
||||
dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams]
|
||||
objs = []
|
||||
for iobj, obj in enumerate(dbobjs):
|
||||
# call all setup hooks on each object
|
||||
objparam = objparams[iobj]
|
||||
# setup
|
||||
obj._createdict = {"permissions": make_iter(objparam[1]),
|
||||
"locks": objparam[2],
|
||||
"aliases": make_iter(objparam[3]),
|
||||
"nattributes": objparam[4],
|
||||
"attributes": objparam[5],
|
||||
"tags": make_iter(objparam[6])}
|
||||
# this triggers all hooks
|
||||
obj.save()
|
||||
# run eventual extra code
|
||||
for code in objparam[7]:
|
||||
if code:
|
||||
exec(code, {}, {"evennia": evennia, "obj": obj})
|
||||
objs.append(obj)
|
||||
return objs
|
||||
|
||||
|
||||
def spawn(*prototypes, **kwargs):
|
||||
"""
|
||||
Spawn a number of prototyped objects.
|
||||
|
||||
Args:
|
||||
prototypes (dict): Each argument should be a prototype
|
||||
dictionary.
|
||||
Kwargs:
|
||||
prototype_modules (str or list): A python-path to a prototype
|
||||
module, or a list of such paths. These will be used to build
|
||||
the global protparents dictionary accessible by the input
|
||||
prototypes. If not given, it will instead look for modules
|
||||
defined by settings.PROTOTYPE_MODULES.
|
||||
prototype_parents (dict): A dictionary holding a custom
|
||||
prototype-parent dictionary. Will overload same-named
|
||||
prototypes from prototype_modules.
|
||||
return_prototypes (bool): Only return a list of the
|
||||
prototype-parents (no object creation happens)
|
||||
|
||||
Returns:
|
||||
object (Object): Spawned object.
|
||||
|
||||
"""
|
||||
# get available protparents
|
||||
protparents = get_protparent_dict()
|
||||
|
||||
# overload module's protparents with specifically given protparents
|
||||
protparents.update(kwargs.get("prototype_parents", {}))
|
||||
for key, prototype in protparents.items():
|
||||
validate_prototype(prototype, key.lower(), protparents)
|
||||
|
||||
if "return_prototypes" in kwargs:
|
||||
# only return the parents
|
||||
return copy.deepcopy(protparents)
|
||||
|
||||
objsparams = []
|
||||
for prototype in prototypes:
|
||||
|
||||
validate_prototype(prototype, None, protparents)
|
||||
prot = _get_prototype(prototype, {}, protparents)
|
||||
if not prot:
|
||||
continue
|
||||
|
||||
# extract the keyword args we need to create the object itself. If we get a callable,
|
||||
# call that to get the value (don't catch errors)
|
||||
create_kwargs = {}
|
||||
# we must always add a key, so if not given we use a shortened md5 hash. There is a (small)
|
||||
# chance this is not unique but it should usually not be a problem.
|
||||
val = prot.pop("key", "Spawned-{}".format(
|
||||
hashlib.md5(str(time.time())).hexdigest()[:6]))
|
||||
create_kwargs["db_key"] = validate_spawn_value(val, str)
|
||||
|
||||
val = prot.pop("location", None)
|
||||
create_kwargs["db_location"] = validate_spawn_value(val, _to_obj)
|
||||
|
||||
val = prot.pop("home", settings.DEFAULT_HOME)
|
||||
create_kwargs["db_home"] = validate_spawn_value(val, _to_obj)
|
||||
|
||||
val = prot.pop("destination", None)
|
||||
create_kwargs["db_destination"] = validate_spawn_value(val, _to_obj)
|
||||
|
||||
val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
|
||||
create_kwargs["db_typeclass_path"] = validate_spawn_value(val, str)
|
||||
|
||||
# extract calls to handlers
|
||||
val = prot.pop("permissions", [])
|
||||
permission_string = validate_spawn_value(val, make_iter)
|
||||
val = prot.pop("locks", "")
|
||||
lock_string = validate_spawn_value(val, str)
|
||||
val = prot.pop("aliases", [])
|
||||
alias_string = validate_spawn_value(val, make_iter)
|
||||
|
||||
val = prot.pop("tags", [])
|
||||
tags = validate_spawn_value(val, make_iter)
|
||||
|
||||
prototype_key = prototype.get('prototype_key', None)
|
||||
if prototype_key:
|
||||
# we make sure to add a tag identifying which prototype created this object
|
||||
tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY))
|
||||
|
||||
val = prot.pop("exec", "")
|
||||
execs = validate_spawn_value(val, make_iter)
|
||||
|
||||
# extract ndb assignments
|
||||
nattribute = dict((key.split("_", 1)[1], validate_spawn_value(val, _to_obj))
|
||||
for key, val in prot.items() if key.startswith("ndb_"))
|
||||
|
||||
# the rest are attributes
|
||||
val = prot.pop("attrs", [])
|
||||
attributes = validate_spawn_value(val, list)
|
||||
|
||||
simple_attributes = []
|
||||
for key, value in ((key, value) for key, value in prot.items()
|
||||
if not (key.startswith("ndb_"))):
|
||||
if is_iter(value) and len(value) > 1:
|
||||
# (value, category)
|
||||
simple_attributes.append((key,
|
||||
validate_spawn_value(value[0], _to_obj_or_any),
|
||||
validate_spawn_value(value[1], str)))
|
||||
else:
|
||||
simple_attributes.append((key,
|
||||
validate_spawn_value(value, _to_obj_or_any)))
|
||||
|
||||
attributes = attributes + simple_attributes
|
||||
attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS]
|
||||
|
||||
# pack for call into _batch_create_object
|
||||
objsparams.append((create_kwargs, permission_string, lock_string,
|
||||
alias_string, nattributes, attributes, tags, execs))
|
||||
|
||||
return _batch_create_object(*objsparams)
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
if __name__ == "__main__":
|
||||
protparents = {
|
||||
"NOBODY": {},
|
||||
# "INFINITE" : {
|
||||
# "prototype":"INFINITE"
|
||||
# },
|
||||
"GOBLIN": {
|
||||
"key": "goblin grunt",
|
||||
"health": lambda: randint(20, 30),
|
||||
"resists": ["cold", "poison"],
|
||||
"attacks": ["fists"],
|
||||
"weaknesses": ["fire", "light"]
|
||||
},
|
||||
"GOBLIN_WIZARD": {
|
||||
"prototype": "GOBLIN",
|
||||
"key": "goblin wizard",
|
||||
"spells": ["fire ball", "lighting bolt"]
|
||||
},
|
||||
"GOBLIN_ARCHER": {
|
||||
"prototype": "GOBLIN",
|
||||
"key": "goblin archer",
|
||||
"attacks": ["short bow"]
|
||||
},
|
||||
"ARCHWIZARD": {
|
||||
"attacks": ["archwizard staff"],
|
||||
},
|
||||
"GOBLIN_ARCHWIZARD": {
|
||||
"key": "goblin archwizard",
|
||||
"prototype": ("GOBLIN_WIZARD", "ARCHWIZARD")
|
||||
}
|
||||
}
|
||||
# test
|
||||
print([o.key for o in spawn(protparents["GOBLIN"],
|
||||
protparents["GOBLIN_ARCHWIZARD"],
|
||||
prototype_parents=protparents)])
|
||||
150
evennia/prototypes/utils.py
Normal file
150
evennia/prototypes/utils.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"""
|
||||
|
||||
Prototype utilities
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class PermissionError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_protparent_dict():
|
||||
"""
|
||||
Get prototype parents.
|
||||
|
||||
Returns:
|
||||
parent_dict (dict): A mapping {prototype_key: prototype} for all available prototypes.
|
||||
|
||||
"""
|
||||
return {prototype['prototype_key']: prototype for prototype in search_prototype()}
|
||||
|
||||
|
||||
def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True):
|
||||
"""
|
||||
Collate a list of found prototypes based on search criteria and access.
|
||||
|
||||
Args:
|
||||
caller (Account or Object): The object requesting the list.
|
||||
key (str, optional): Exact or partial prototype key to query for.
|
||||
tags (str or list, optional): Tag key or keys to query for.
|
||||
show_non_use (bool, optional): Show also prototypes the caller may not use.
|
||||
show_non_edit (bool, optional): Show also prototypes the caller may not edit.
|
||||
Returns:
|
||||
table (EvTable or None): An EvTable representation of the prototypes. None
|
||||
if no prototypes were found.
|
||||
|
||||
"""
|
||||
# this allows us to pass lists of empty strings
|
||||
tags = [tag for tag in make_iter(tags) if tag]
|
||||
|
||||
# get prototypes for readonly and db-based prototypes
|
||||
prototypes = search_prototype(key, tags)
|
||||
|
||||
# get use-permissions of readonly attributes (edit is always False)
|
||||
display_tuples = []
|
||||
for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')):
|
||||
lock_use = caller.locks.check_lockstring(
|
||||
caller, prototype.get('prototype_locks', ''), access_type='use')
|
||||
if not show_non_use and not lock_use:
|
||||
continue
|
||||
if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES:
|
||||
lock_edit = False
|
||||
else:
|
||||
lock_edit = caller.locks.check_lockstring(
|
||||
caller, prototype.get('prototype_locks', ''), access_type='edit')
|
||||
if not show_non_edit and not lock_edit:
|
||||
continue
|
||||
ptags = []
|
||||
for ptag in prototype.get('prototype_tags', []):
|
||||
if is_iter(ptag):
|
||||
if len(ptag) > 1:
|
||||
ptags.append("{} (category: {}".format(ptag[0], ptag[1]))
|
||||
else:
|
||||
ptags.append(ptag[0])
|
||||
else:
|
||||
ptags.append(str(ptag))
|
||||
|
||||
display_tuples.append(
|
||||
(prototype.get('prototype_key', '<unset>'),
|
||||
prototype.get('prototype_desc', '<unset>'),
|
||||
"{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'),
|
||||
",".join(ptags)))
|
||||
|
||||
if not display_tuples:
|
||||
return None
|
||||
|
||||
table = []
|
||||
width = 78
|
||||
for i in range(len(display_tuples[0])):
|
||||
table.append([str(display_tuple[i]) for display_tuple in display_tuples])
|
||||
table = EvTable("Key", "Desc", "Use/Edit", "Tags", table=table, crop=True, width=width)
|
||||
table.reformat_column(0, width=22)
|
||||
table.reformat_column(1, width=29)
|
||||
table.reformat_column(2, width=11, align='c')
|
||||
table.reformat_column(3, width=16)
|
||||
return table
|
||||
|
||||
|
||||
def prototype_to_str(prototype):
|
||||
"""
|
||||
Format a prototype to a nice string representation.
|
||||
|
||||
Args:
|
||||
prototype (dict): The prototype.
|
||||
"""
|
||||
|
||||
header = (
|
||||
"|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n"
|
||||
"|cdesc:|n {} \n|cprototype:|n ".format(
|
||||
prototype['prototype_key'],
|
||||
", ".join(prototype['prototype_tags']),
|
||||
prototype['prototype_locks'],
|
||||
prototype['prototype_desc']))
|
||||
proto = ("{{\n {} \n}}".format(
|
||||
"\n ".join(
|
||||
"{!r}: {!r},".format(key, value) for key, value in
|
||||
sorted(prototype.items()) if key not in _PROTOTYPE_META_NAMES)).rstrip(","))
|
||||
return header + proto
|
||||
|
||||
|
||||
def prototype_from_object(obj):
|
||||
"""
|
||||
Guess a minimal prototype from an existing object.
|
||||
|
||||
Args:
|
||||
obj (Object): An object to analyze.
|
||||
|
||||
Returns:
|
||||
prototype (dict): A prototype estimating the current state of the object.
|
||||
|
||||
"""
|
||||
# first, check if this object already has a prototype
|
||||
|
||||
prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True)
|
||||
prot = search_prototype(prot)
|
||||
if not prot or len(prot) > 1:
|
||||
# no unambiguous prototype found - build new prototype
|
||||
prot = {}
|
||||
prot['prototype_key'] = "From-Object-{}-{}".format(
|
||||
obj.key, hashlib.md5(str(time.time())).hexdigest()[:6])
|
||||
prot['prototype_desc'] = "Built from {}".format(str(obj))
|
||||
prot['prototype_locks'] = "use:all();edit:all()"
|
||||
|
||||
prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6]
|
||||
prot['location'] = obj.db_location
|
||||
prot['home'] = obj.db_home
|
||||
prot['destination'] = obj.db_destination
|
||||
prot['typeclass'] = obj.db_typeclass_path
|
||||
prot['locks'] = obj.locks.all()
|
||||
prot['permissions'] = obj.permissions.get()
|
||||
prot['aliases'] = obj.aliases.get()
|
||||
prot['tags'] = [(tag.key, tag.category, tag.data)
|
||||
for tag in obj.tags.get(return_tagobj=True, return_list=True)]
|
||||
prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks)
|
||||
for attr in obj.attributes.get(return_obj=True, return_list=True)]
|
||||
|
||||
return prot
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue