Start expanding spawn command for prot-storage

This commit is contained in:
Griatch 2018-03-03 18:29:37 +01:00
parent 8c4ceea4cd
commit 1dbbec0eba
3 changed files with 310 additions and 16 deletions

View file

@ -12,7 +12,7 @@ from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.utils import create, utils, search
from evennia.utils.utils import inherits_from, class_from_module
from evennia.utils.eveditor import EvEditor
from evennia.utils.spawner import spawn
from evennia.utils.spawner import spawn, search_prototype, list_prototypes
from evennia.utils.ansi import raw
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -2731,17 +2731,29 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
spawn objects from prototype
Usage:
@spawn
@spawn[/switch] <prototype_name>
@spawn[/switch] {prototype dictionary}
@spawn[/noloc] <prototype_name>
@spawn[/noloc] <prototype_dict>
Switch:
@spawn/search [query]
@spawn/list [tag, tag]
@spawn/show <key>
@spawn/save <prototype_dict> [;desc[;tag,tag,..[;lockstring]]]
@spawn/menu
Switches:
noloc - allow location to be None if not specified explicitly. Otherwise,
location will default to caller's current location.
search - search prototype by name or tags.
list - list available prototypes, optionally limit by tags.
show - inspect prototype by key.
save - save a prototype to the database. It will be listable by /list.
menu - manipulate prototype in a menu interface.
Example:
@spawn GOBLIN
@spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"}
@spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all()
Dictionary keys:
|wprototype |n - name of parent prototype to use. Can be a list for
@ -2760,12 +2772,16 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
The available prototypes are defined globally in modules set in
settings.PROTOTYPE_MODULES. If @spawn is used without arguments it
displays a list of available prototypes.
"""
key = "@spawn"
locks = "cmd:perm(spawn) or perm(Builder)"
help_category = "Building"
def parser(self):
super(CmdSpawn, self).parser()
def func(self):
"""Implements the spawner"""
@ -2774,6 +2790,13 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
prots = ", ".join(sorted(prototypes.keys()))
return "\nAvailable prototypes (case sensitive): %s" % (
"\n" + utils.fill(prots) if prots else "None")
caller = self.caller
if not self.args:
ncount = len(search_prototype())
caller.msg("Usage: @spawn <prototype-key> or {key: value, ...}"
"\n ({} existing prototypes. Use /list to inspect)".format(ncount))
return
prototypes = spawn(return_prototypes=True)
if not self.args:

View file

@ -228,11 +228,12 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non
# get use-permissions of readonly attributes (edit is always False)
readonly_prototypes = [
(tup.key,
tup.desc,
(metaproto.key,
metaproto.desc,
("{}/N".format('Y'
if caller.locks.check_lockstring(caller, tup.locks, access_type='use') else 'N')),
",".join(tup.tags)) for tup in readonly_prototypes]
if caller.locks.check_lockstring(caller, metaproto.locks, access_type='use') else 'N')),
",".join(metaproto.tags))
for metaproto in sorted(readonly_prototypes, key=lambda o: o.key)]
# next, handle db-stored prototypes
prototypes = search_persistent_prototype(key, tags)
@ -242,7 +243,7 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non
"{}/{}".format('Y' if prototype.access(caller, "use") else 'N',
'Y' if prototype.access(caller, "edit") else 'N'),
",".join(prototype.tags.get(category="persistent_prototype")))
for prototype in prototypes]
for prototype in sorted(prototypes, key=lambda o: o.key)]
prototypes = prototypes + readonly_prototypes
@ -250,16 +251,16 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non
return None
if not show_non_use:
prototypes = [tup for tup in sorted(prototypes, key=lambda o: o[0]) if tup[2]]
prototypes = [metaproto for metaproto in prototypes if metaproto[2].split("/", 1)[0] == 'Y']
if not show_non_edit:
prototypes = [tup for tup in sorted(prototypes, key=lambda o: o[0]) if tup[3]]
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(tup[i]) for tup in prototypes])
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)

View file

@ -86,6 +86,21 @@ 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
@ -96,7 +111,35 @@ import evennia
from evennia.objects.models import ObjectDB
from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj
from collections import namedtuple
from evennia.scripts.scripts import DefaultScript
from evennia.utils.create import create_script
from evennia.utils.evtable import EvTable
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
_READONLY_PROTOTYPES = {}
_READONLY_PROTOTYPE_MODULES = {}
# 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)]
_READONLY_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})
_READONLY_PROTOTYPE_MODULES.update({tup[0]: mod for tup in prots})
def _handle_dbref(inp):
@ -119,7 +162,8 @@ def _validate_prototype(key, prototype, protparents, visited):
raise RuntimeError("%s tries to prototype itself." % key or prototype)
protparent = protparents.get(protstring)
if not protparent:
raise RuntimeError("%s's prototype '%s' was not found." % (key or prototype, protstring))
raise RuntimeError(
"%s's prototype '%s' was not found." % (key or prototype, protstring))
_validate_prototype(protstring, protparent, protparents, visited)
@ -303,9 +347,235 @@ def spawn(*prototypes, **kwargs):
return _batch_create_object(*objsparams)
if __name__ == "__main__":
# testing
# Prototype storage mechanisms
class PersistentPrototype(DefaultScript):
"""
This stores a single prototype
"""
def at_script_creation(self):
self.key = "empty prototype"
self.desc = "A prototype"
def store_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 'persistent_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 edit:perm(Admin)".format(caller.id)
tags = [(tag, "persistent_prototype") for tag in make_iter(tags)]
if key in _READONLY_PROTOTYPES:
mod = _READONLY_PROTOTYPE_MODULES.get(key, "N/A")
raise PermissionError("{} is a read-only prototype "
"(defined as code in {}).".format(key_orig, mod))
stored_prototype = PersistentPrototype.objects.filter(db_key=key)
if stored_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:
stored_prototype.delete()
return
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)
else:
stored_prototype = create_script(
PersistentPrototype, key=key, desc=desc, persistent=True,
locks=locks, tags=tags, attributes=[("prototype", prototype)])
return stored_prototype
def search_persistent_prototype(key=None, tags=None):
"""
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 'persistent_protototype'
tag category.
Return:
matches (queryset): All found PersistentPrototypes
Note:
This will not include read-only prototypes defined in modules.
"""
if tags:
# exact match on tag(s)
tags = make_iter(tags)
tag_categories = ["persistent_prototype" for _ in tags]
matches = PersistentPrototype.objects.get_by_tag(tags, tag_categories)
else:
matches = PersistentPrototype.objects.all()
if key:
# partial match on key
matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key)
return matches
def search_readonly_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 _READONLY_PROTOTYPES.items()
if tagset.intersection(metaproto.tags)}
else:
matches = _READONLY_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):
"""
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 'persistent_protototype'
tag category.
Return:
matches (list): All found prototype dicts.
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.
"""
matches = []
if key and key in _READONLY_PROTOTYPES:
matches.append(_READONLY_PROTOTYPES[key][3])
else:
matches.extend([prot.attributes.get("prototype")
for prot in search_persistent_prototype(key, tags)])
return matches
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.
"""
# handle read-only prototypes separately
readonly_prototypes = search_readonly_prototype(key, tags)
# get use-permissions of readonly attributes (edit is always False)
readonly_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(readonly_prototypes, key=lambda o: o.key)]
# next, handle db-stored prototypes
prototypes = search_persistent_prototype(key, tags)
# gather access permissions as (key, desc, tags, can_use, can_edit)
prototypes = [(prototype.key, prototype.desc,
"{}/{}".format('Y' if prototype.access(caller, "use") else 'N',
'Y' if prototype.access(caller, "edit") else 'N'),
",".join(prototype.tags.get(category="persistent_prototype")))
for prototype in sorted(prototypes, key=lambda o: o.key)]
prototypes = prototypes + readonly_prototypes
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
# Testing
if __name__ == "__main__":
protparents = {
"NOBODY": {},
# "INFINITE" : {