mirror of
https://github.com/evennia/evennia.git
synced 2026-03-29 20:17:16 +02:00
1207 lines
44 KiB
Python
1207 lines
44 KiB
Python
"""
|
|
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')]
|
|
"args": [("weapon", "sword")]
|
|
}
|
|
```
|
|
|
|
Possible keywords are:
|
|
prototype - string parent prototype
|
|
key - string, the main object identifier
|
|
typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`
|
|
location - this should be a valid object or #dbref
|
|
home - valid object or #dbref
|
|
destination - only valid for exits (object or dbref)
|
|
|
|
permissions - string or list of permission strings
|
|
locks - a lock-string
|
|
aliases - string or list of strings
|
|
exec - 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'
|
|
tags - 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 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> - value of a nattribute (ndb_ is stripped)
|
|
other - 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
|
|
GOBLIN_WIZARD = {
|
|
"prototype": GOBLIN,
|
|
"key": "goblin wizard",
|
|
"spells": ["fire ball", "lighting bolt"]
|
|
}
|
|
|
|
GOBLIN_ARCHER = {
|
|
"prototype": GOBLIN,
|
|
"key": "goblin archer",
|
|
"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"
|
|
"prototype": (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
|
|
from django.conf import settings
|
|
from random import randint
|
|
import evennia
|
|
from evennia.objects.models import ObjectDB
|
|
from evennia.utils.utils import (
|
|
make_iter, all_from_module, dbid_to_obj, is_iter, crop, get_all_typeclasses)
|
|
|
|
from collections import namedtuple
|
|
from evennia.scripts.scripts import DefaultScript
|
|
from evennia.utils.create import create_script
|
|
from evennia.utils.evtable import EvTable
|
|
from evennia.utils.evmenu import EvMenu, list_node
|
|
|
|
|
|
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
|
|
_MODULE_PROTOTYPES = {}
|
|
_MODULE_PROTOTYPE_MODULES = {}
|
|
_MENU_CROP_WIDTH = 15
|
|
|
|
|
|
class PermissionError(RuntimeError):
|
|
pass
|
|
|
|
# storage of meta info about the prototype
|
|
MetaProto = namedtuple('MetaProto', ['key', 'desc', 'locks', 'tags', 'prototype'])
|
|
|
|
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 = [(key, prot) for key, prot in all_from_module(mod).items()
|
|
if prot and isinstance(prot, dict)]
|
|
_MODULE_PROTOTYPES.update(
|
|
{key.lower(): MetaProto(
|
|
key.lower(),
|
|
prot['prototype_desc'] if 'prototype_desc' in prot else mod,
|
|
prot['prototype_lock'] if 'prototype_lock' in prot else "use:all()",
|
|
set(make_iter(
|
|
prot['prototype_tags']) if 'prototype_tags' in prot else ["base-prototype"]),
|
|
prot)
|
|
for key, prot in prots})
|
|
_MODULE_PROTOTYPE_MODULES.update({tup[0]: mod for tup in prots})
|
|
|
|
# Prototype storage mechanisms
|
|
|
|
|
|
class DbPrototype(DefaultScript):
|
|
"""
|
|
This stores a single prototype
|
|
"""
|
|
def at_script_creation(self):
|
|
self.key = "empty prototype"
|
|
self.desc = "A prototype"
|
|
|
|
|
|
def build_metaproto(key='', desc='', locks='', tags=None, prototype=None):
|
|
"""
|
|
Create a metaproto from combinant parts.
|
|
|
|
"""
|
|
if locks:
|
|
locks = (";".join(locks) if is_iter(locks) else locks)
|
|
else:
|
|
locks = []
|
|
prototype = dict(prototype) if prototype else {}
|
|
return MetaProto(key, desc, locks, tags, dict(prototype))
|
|
|
|
|
|
def save_db_prototype(caller, key, prototype, desc="", tags=None, locks="", delete=False):
|
|
"""
|
|
Store a prototype persistently.
|
|
|
|
Args:
|
|
caller (Account or Object): Caller aiming to store prototype. At this point
|
|
the caller should have permission to 'add' new prototypes, but to edit
|
|
an existing prototype, the 'edit' lock must be passed on that prototype.
|
|
key (str): Name of prototype to store.
|
|
prototype (dict): Prototype dict.
|
|
desc (str, optional): Description of prototype, to use in listing.
|
|
tags (list, optional): Tag-strings to apply to prototype. These are always
|
|
applied with the 'db_prototype' category.
|
|
locks (str, optional): Locks to apply to this prototype. Used locks
|
|
are 'use' and 'edit'
|
|
delete (bool, optional): Delete an existing prototype identified by 'key'.
|
|
This requires `caller` to pass the 'edit' lock of the prototype.
|
|
Returns:
|
|
stored (StoredPrototype or None): The resulting prototype (new or edited),
|
|
or None if deleting.
|
|
Raises:
|
|
PermissionError: If edit lock was not passed by caller.
|
|
|
|
|
|
"""
|
|
key_orig = key
|
|
key = key.lower()
|
|
locks = locks if locks else "use:all();edit:id({}) or perm(Admin)".format(caller.id)
|
|
|
|
is_valid, err = caller.locks.validate(locks)
|
|
if not is_valid:
|
|
caller.msg("Lock error: {}".format(err))
|
|
return False
|
|
|
|
tags = [(tag, "db_prototype") for tag in make_iter(tags)]
|
|
|
|
if key in _MODULE_PROTOTYPES:
|
|
mod = _MODULE_PROTOTYPE_MODULES.get(key, "N/A")
|
|
raise PermissionError("{} is a read-only prototype "
|
|
"(defined as code in {}).".format(key_orig, mod))
|
|
|
|
stored_prototype = DbPrototype.objects.filter(db_key=key)
|
|
|
|
if stored_prototype:
|
|
# edit existing prototype
|
|
stored_prototype = stored_prototype[0]
|
|
if not stored_prototype.access(caller, 'edit'):
|
|
raise PermissionError("{} does not have permission to "
|
|
"edit prototype {}.".format(caller, key))
|
|
|
|
if delete:
|
|
# delete prototype
|
|
stored_prototype.delete()
|
|
return True
|
|
|
|
if desc:
|
|
stored_prototype.desc = desc
|
|
if tags:
|
|
stored_prototype.tags.batch_add(*tags)
|
|
if locks:
|
|
stored_prototype.locks.add(locks)
|
|
if prototype:
|
|
stored_prototype.attributes.add("prototype", prototype)
|
|
elif delete:
|
|
# didn't find what to delete
|
|
return False
|
|
else:
|
|
# create a new prototype
|
|
stored_prototype = create_script(
|
|
DbPrototype, key=key, desc=desc, persistent=True,
|
|
locks=locks, tags=tags, attributes=[("prototype", prototype)])
|
|
return stored_prototype
|
|
|
|
|
|
def delete_db_prototype(caller, key):
|
|
"""
|
|
Delete a stored prototype
|
|
|
|
Args:
|
|
caller (Account or Object): Caller aiming to delete a prototype.
|
|
key (str): The persistent prototype to delete.
|
|
Returns:
|
|
success (bool): If deletion worked or not.
|
|
Raises:
|
|
PermissionError: If 'edit' lock was not passed.
|
|
|
|
"""
|
|
return save_db_prototype(caller, key, None, delete=True)
|
|
|
|
|
|
def search_db_prototype(key=None, tags=None, return_metaprotos=False):
|
|
"""
|
|
Find persistent (database-stored) prototypes based on key and/or tags.
|
|
|
|
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_metaproto (bool): Return results as metaprotos.
|
|
Return:
|
|
matches (queryset or list): All found DbPrototypes. If `return_metaprotos`
|
|
is set, return a list of MetaProtos.
|
|
|
|
Note:
|
|
This will not include read-only prototypes defined in modules.
|
|
|
|
"""
|
|
if tags:
|
|
# exact match on tag(s)
|
|
tags = make_iter(tags)
|
|
tag_categories = ["db_prototype" for _ in tags]
|
|
matches = DbPrototype.objects.get_by_tag(tags, tag_categories)
|
|
else:
|
|
matches = DbPrototype.objects.all()
|
|
if key:
|
|
# exact or partial match on key
|
|
matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key)
|
|
if return_metaprotos:
|
|
return [build_metaproto(match.key, match.desc, match.locks.all(),
|
|
match.tags.get(category="db_prototype", return_list=True),
|
|
match.attributes.get("prototype"))
|
|
for match in matches]
|
|
return matches
|
|
|
|
|
|
def search_module_prototype(key=None, tags=None):
|
|
"""
|
|
Find read-only prototypes, defined in modules.
|
|
|
|
Kwargs:
|
|
key (str): An exact or partial key to query for.
|
|
tags (str or list): Tag key to query for.
|
|
|
|
Return:
|
|
matches (list): List of MetaProto tuples that includes
|
|
prototype metadata,
|
|
|
|
"""
|
|
matches = {}
|
|
if tags:
|
|
# use tags to limit selection
|
|
tagset = set(tags)
|
|
matches = {key: metaproto for key, metaproto in _MODULE_PROTOTYPES.items()
|
|
if tagset.intersection(metaproto.tags)}
|
|
else:
|
|
matches = _MODULE_PROTOTYPES
|
|
|
|
if key:
|
|
if key in matches:
|
|
# exact match
|
|
return [matches[key]]
|
|
else:
|
|
# fuzzy matching
|
|
return [metaproto for pkey, metaproto in matches.items() if key in pkey]
|
|
else:
|
|
return [match for match in matches.values()]
|
|
|
|
|
|
def search_prototype(key=None, tags=None, return_meta=True):
|
|
"""
|
|
Find prototypes based on key and/or tags.
|
|
|
|
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_meta (bool): If False, only return prototype dicts, if True
|
|
return MetaProto namedtuples including prototype meta info
|
|
|
|
Return:
|
|
matches (list): All found prototype dicts or MetaProtos
|
|
|
|
Note:
|
|
The available prototypes is a combination of those supplied in
|
|
PROTOTYPE_MODULES and those stored from in-game. For the latter,
|
|
this will use the tags to make a subselection before attempting
|
|
to match on the key. So if key/tags don't match up nothing will
|
|
be found.
|
|
|
|
"""
|
|
module_prototypes = search_module_prototype(key, tags)
|
|
db_prototypes = search_db_prototype(key, tags, return_metaprotos=True)
|
|
|
|
matches = db_prototypes + module_prototypes
|
|
if len(matches) > 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.key == key]
|
|
if filter_matches and len(filter_matches) < len(matches):
|
|
matches = filter_matches
|
|
|
|
if not return_meta:
|
|
matches = [mta.prototype for mta in matches]
|
|
|
|
return matches
|
|
|
|
|
|
def get_protparents():
|
|
"""
|
|
Get prototype parents. These are a combination of meta-key and prototype-dict and are used when
|
|
a prototype refers to another parent-prototype.
|
|
|
|
"""
|
|
# get all prototypes
|
|
metaprotos = search_prototype(return_meta=True)
|
|
# organize by key
|
|
return {metaproto.key: metaproto.prototype for metaproto in metaprotos}
|
|
|
|
|
|
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 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 metaprotos for readonly and db-based prototypes
|
|
metaprotos = search_module_prototype(key, tags)
|
|
metaprotos += search_db_prototype(key, tags, return_metaprotos=True)
|
|
|
|
# get use-permissions of readonly attributes (edit is always False)
|
|
prototypes = [
|
|
(metaproto.key,
|
|
metaproto.desc,
|
|
("{}/N".format('Y'
|
|
if caller.locks.check_lockstring(
|
|
caller,
|
|
metaproto.locks,
|
|
access_type='use') else 'N')),
|
|
",".join(metaproto.tags))
|
|
for metaproto in sorted(metaprotos, key=lambda o: o.key)]
|
|
|
|
if not prototypes:
|
|
return None
|
|
|
|
if not show_non_use:
|
|
prototypes = [metaproto for metaproto in prototypes if metaproto[2].split("/", 1)[0] == 'Y']
|
|
if not show_non_edit:
|
|
prototypes = [metaproto for metaproto in prototypes if metaproto[2].split("/", 1)[1] == 'Y']
|
|
|
|
if not prototypes:
|
|
return None
|
|
|
|
table = []
|
|
for i in range(len(prototypes[0])):
|
|
table.append([str(metaproto[i]) for metaproto in prototypes])
|
|
table = EvTable("Key", "Desc", "Use/Edit", "Tags", table=table, crop=True, width=78)
|
|
table.reformat_column(0, width=28)
|
|
table.reformat_column(1, width=40)
|
|
table.reformat_column(2, width=11, align='r')
|
|
table.reformat_column(3, width=20)
|
|
return table
|
|
|
|
# Spawner mechanism
|
|
|
|
|
|
def _handle_dbref(inp):
|
|
return dbid_to_obj(inp, ObjectDB)
|
|
|
|
|
|
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 any.
|
|
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_protparents()
|
|
if _visited is None:
|
|
_visited = []
|
|
protkey = protkey.lower() if protkey is not None else 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 _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): Parameters for the respective creation/add
|
|
handlers 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.
|
|
for the respective creation/add handlers in the following
|
|
order: (create_kwargs, permissions, locks, aliases, nattributes,
|
|
attributes, tags, execs)
|
|
|
|
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)
|
|
|
|
"""
|
|
# get available protparents
|
|
protparents = get_protparents()
|
|
|
|
# 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 = {}
|
|
keyval = prot.pop("key", "Spawned Object %06i" % randint(1, 100000))
|
|
create_kwargs["db_key"] = keyval() if callable(keyval) else keyval
|
|
|
|
locval = prot.pop("location", None)
|
|
create_kwargs["db_location"] = locval() if callable(locval) else _handle_dbref(locval)
|
|
|
|
homval = prot.pop("home", settings.DEFAULT_HOME)
|
|
create_kwargs["db_home"] = homval() if callable(homval) else _handle_dbref(homval)
|
|
|
|
destval = prot.pop("destination", None)
|
|
create_kwargs["db_destination"] = destval() if callable(destval) else _handle_dbref(destval)
|
|
|
|
typval = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
|
|
create_kwargs["db_typeclass_path"] = typval() if callable(typval) else typval
|
|
|
|
# extract calls to handlers
|
|
permval = prot.pop("permissions", [])
|
|
permission_string = permval() if callable(permval) else permval
|
|
lockval = prot.pop("locks", "")
|
|
lock_string = lockval() if callable(lockval) else lockval
|
|
aliasval = prot.pop("aliases", "")
|
|
alias_string = aliasval() if callable(aliasval) else aliasval
|
|
tagval = prot.pop("tags", [])
|
|
tags = tagval() if callable(tagval) else tagval
|
|
attrval = prot.pop("attrs", [])
|
|
attributes = attrval() if callable(tagval) else attrval
|
|
|
|
exval = prot.pop("exec", "")
|
|
execs = make_iter(exval() if callable(exval) else exval)
|
|
|
|
# extract ndb assignments
|
|
nattributes = dict((key.split("_", 1)[1], value() if callable(value) else value)
|
|
for key, value in prot.items() if key.startswith("ndb_"))
|
|
|
|
# the rest are attributes
|
|
simple_attributes = [(key, value()) if callable(value) else (key, value)
|
|
for key, value in prot.items() if not key.startswith("ndb_")]
|
|
attributes = attributes + simple_attributes
|
|
attributes = [tup for tup in attributes if not tup[0] in _CREATE_OBJECT_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)
|
|
|
|
|
|
# prototype design menu nodes
|
|
|
|
def _get_menu_metaprot(caller):
|
|
if hasattr(caller.ndb._menutree, "olc_metaprot"):
|
|
return caller.ndb._menutree.olc_metaprot
|
|
else:
|
|
metaproto = build_metaproto(None, '', [], [], None)
|
|
caller.ndb._menutree.olc_metaprot = metaproto
|
|
caller.ndb._menutree.olc_new = True
|
|
return metaproto
|
|
|
|
|
|
def _is_new_prototype(caller):
|
|
return hasattr(caller.ndb._menutree, "olc_new")
|
|
|
|
|
|
def _set_menu_metaprot(caller, field, value):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
kwargs = dict(metaprot.__dict__)
|
|
kwargs[field] = value
|
|
caller.ndb._menutree.olc_metaprot = build_metaproto(**kwargs)
|
|
|
|
|
|
def _format_property(key, required=False, metaprot=None, prototype=None):
|
|
key = key.lower()
|
|
if metaprot is not None:
|
|
prop = getattr(metaprot, key) or ''
|
|
elif prototype is not None:
|
|
prop = prototype.get(key, '')
|
|
|
|
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(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", "meta_key")
|
|
processor = kwargs.get("processor", None)
|
|
next_node = kwargs.get("next_node", "node_index")
|
|
|
|
propname_low = prop.strip().lower()
|
|
meta = propname_low.startswith("meta_")
|
|
if meta:
|
|
propname_low = propname_low[5:]
|
|
|
|
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 meta:
|
|
_set_menu_metaprot(caller, propname_low, value)
|
|
else:
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prototype = metaprot.prototype
|
|
prototype[propname_low] = value
|
|
_set_menu_metaprot(caller, "prototype", prototype)
|
|
|
|
caller.msg("Set {prop} to {value}.".format(
|
|
prop=prop.replace("_", "-").capitalize(), value=str(value)))
|
|
|
|
return next_node
|
|
|
|
|
|
def _wizard_options(prev_node, next_node):
|
|
options = [{"desc": "forward ({})".format(next_node.replace("_", "-")),
|
|
"goto": "node_{}".format(next_node)},
|
|
{"desc": "back ({})".format(prev_node.replace("_", "-")),
|
|
"goto": "node_{}".format(prev_node)}]
|
|
if "index" not in (prev_node, next_node):
|
|
options.append({"desc": "index",
|
|
"goto": "node_index"})
|
|
return options
|
|
|
|
|
|
# menu nodes
|
|
|
|
def node_index(caller):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prototype = metaprot.prototype
|
|
|
|
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 = []
|
|
# The meta-key goes first
|
|
options.append(
|
|
{"desc": "|WMeta-Key|n|n{}".format(_format_property("Key", True, metaprot, None)),
|
|
"goto": "node_meta_key"})
|
|
for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks',
|
|
'Permissions', 'Location', 'Home', 'Destination'):
|
|
req = False
|
|
if key in ("Prototype", "Typeclass"):
|
|
req = "prototype" not in prototype and "typeclass" not in prototype
|
|
options.append(
|
|
{"desc": "|w{}|n{}".format(key, _format_property(key, req, None, prototype)),
|
|
"goto": "node_{}".format(key.lower())})
|
|
for key in ('Desc', 'Tags', 'Locks'):
|
|
options.append(
|
|
{"desc": "|WMeta-{}|n|n{}".format(key, _format_property(key, req, metaprot, None)),
|
|
"goto": "node_meta_{}".format(key.lower())})
|
|
|
|
return text, options
|
|
|
|
|
|
def _check_meta_key(caller, key):
|
|
old_metaprot = search_prototype(key)
|
|
olc_new = caller.ndb._menutree.olc_new
|
|
key = key.strip().lower()
|
|
if old_metaprot:
|
|
# we are starting a new prototype that matches an existing
|
|
if not caller.locks.check_lockstring(caller, old_metaprot.locks, access_type='edit'):
|
|
# return to the node_meta_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_meta_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_metaprot = old_metaprot
|
|
caller.msg("Prototype already exists. Reloading.")
|
|
return "node_index"
|
|
|
|
return _set_property(caller, key, prop='meta_key', next_node="node_prototype")
|
|
|
|
|
|
def node_meta_key(caller):
|
|
metaprot = _get_menu_metaprot(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 = metaprot.key
|
|
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("index", "prototype")
|
|
options.append({"key": "_default",
|
|
"goto": _check_meta_key})
|
|
return text, options
|
|
|
|
|
|
def node_prototype(caller):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
prototype = prot.get("prototype")
|
|
|
|
text = ["Set the prototype's parent |yPrototype|n. If this is unset, Typeclass will be used."]
|
|
if prototype:
|
|
text.append("Current prototype is |y{prototype}|n.".format(prototype=prototype))
|
|
else:
|
|
text.append("Parent prototype is not set")
|
|
text = "\n\n".join(text)
|
|
options = _wizard_options("meta_key", "typeclass")
|
|
options.append({"key": "_default",
|
|
"goto": (_set_property,
|
|
dict(prop="prototype",
|
|
processor=lambda s: s.strip(),
|
|
next_node="node_typeclass"))})
|
|
return text, options
|
|
|
|
|
|
def _typeclass_examine(caller, typeclass):
|
|
return "This is typeclass |y{}|n.".format(typeclass)
|
|
|
|
|
|
def _typeclass_select(caller, typeclass):
|
|
caller.msg("Selected typeclass |y{}|n.".format(typeclass))
|
|
return None
|
|
|
|
|
|
@list_node(list(sorted(get_all_typeclasses().keys())), _typeclass_examine, _typeclass_select)
|
|
def node_typeclass(caller):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
typeclass = prot.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("prototype", "key")
|
|
options.append({"key": "_default",
|
|
"goto": (_set_property,
|
|
dict(prop="typeclass",
|
|
processor=lambda s: s.strip(),
|
|
next_node="node_key"))})
|
|
return text, options
|
|
|
|
|
|
def node_key(caller):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
key = prot.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("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):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
aliases = prot.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("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 node_attrs(caller):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
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("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 node_tags(caller):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
tags = prot.get("tags")
|
|
|
|
text = ["Set the prototype's |yTags|n. Separate multiple tags with commas. "
|
|
"Will retain case sensitivity."]
|
|
if tags:
|
|
text.append("Current tags are '|y{tags}|n'.".format(tags=tags))
|
|
else:
|
|
text.append("No tags are set.")
|
|
text = "\n\n".join(text)
|
|
options = _wizard_options("attrs", "locks")
|
|
options.append({"key": "_default",
|
|
"goto": (_set_property,
|
|
dict(prop="tags",
|
|
processor=lambda s: [part.strip() for part in s.split(",")],
|
|
next_node="node_locks"))})
|
|
return text, options
|
|
|
|
|
|
def node_locks(caller):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
locks = prot.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("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):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
permissions = prot.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("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):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
location = prot.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("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):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
home = prot.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("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):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
prot = metaprot.prototype
|
|
dest = prot.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("home", "meta_desc")
|
|
options.append({"key": "_default",
|
|
"goto": (_set_property,
|
|
dict(prop="dest",
|
|
processor=lambda s: s.strip(),
|
|
next_node="node_meta_desc"))})
|
|
return text, options
|
|
|
|
|
|
def node_meta_desc(caller):
|
|
|
|
metaprot = _get_menu_metaprot(caller)
|
|
text = ["The |wMeta-Description|n briefly describes the prototype for viewing in listings."]
|
|
desc = metaprot.desc
|
|
|
|
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("meta_key", "meta_tags")
|
|
options.append({"key": "_default",
|
|
"goto": (_set_property,
|
|
dict(prop='meta_desc',
|
|
processor=lambda s: s.strip(),
|
|
next_node="node_meta_tags"))})
|
|
|
|
return text, options
|
|
|
|
|
|
def node_meta_tags(caller):
|
|
metaprot = _get_menu_metaprot(caller)
|
|
text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. "
|
|
"Separate multiple by tags by commas."]
|
|
tags = metaprot.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("meta_desc", "meta_locks")
|
|
options.append({"key": "_default",
|
|
"goto": (_set_property,
|
|
dict(prop="meta_tags",
|
|
processor=lambda s: [
|
|
str(part.strip().lower()) for part in s.split(",")],
|
|
next_node="node_meta_locks"))})
|
|
return text, options
|
|
|
|
|
|
def node_meta_locks(caller):
|
|
metaprot = _get_menu_metaprot(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 = metaprot.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("meta_tags", "index")
|
|
options.append({"key": "_default",
|
|
"goto": (_set_property,
|
|
dict(prop="meta_locks",
|
|
processor=lambda s: s.strip().lower(),
|
|
next_node="node_index"))})
|
|
return text, options
|
|
|
|
|
|
|
|
def start_olc(caller, session=None, metaproto=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.
|
|
metaproto (MetaProto, optional): Given when editing an existing
|
|
prototype rather than creating a new one.
|
|
|
|
"""
|
|
menudata = {"node_index": node_index,
|
|
"node_meta_key": node_meta_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_meta_desc": node_meta_desc,
|
|
"node_meta_tags": node_meta_tags,
|
|
"node_meta_locks": node_meta_locks,
|
|
}
|
|
EvMenu(caller, menudata, startnode='node_index', session=session, olc_metaproto=metaproto)
|
|
|
|
|
|
# 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)])
|