First version of expanded spawn command with storage

This commit is contained in:
Griatch 2018-03-04 11:39:55 +01:00
parent e95387a7a9
commit ddd56cdeb3
5 changed files with 181 additions and 328 deletions

View file

@ -13,7 +13,7 @@ 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.evmore import EvMore
from evennia.utils.spawner import spawn, search_prototype, list_prototypes
from evennia.utils.spawner import spawn, search_prototype, list_prototypes, store_prototype
from evennia.utils.ansi import raw
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -2735,11 +2735,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
@spawn[/noloc] <prototype_name>
@spawn[/noloc] <prototype_dict>
@spawn/search [query]
@spawn/search [key][;tag[,tag]]
@spawn/list [tag, tag]
@spawn/show [<key>]
@spawn/save <prototype_dict> [;desc[;tag,tag,..[;lockstring]]]
@spawn/save <key>[;desc[;tag,tag[,...][;lockstring]]] = <prototype_dict>
@spawn/menu
Switches:
@ -2786,73 +2786,164 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
def func(self):
"""Implements the spawner"""
def _show_prototypes(prototypes):
"""Helper to show a list of available prototypes"""
prots = ", ".join(sorted(prototypes.keys()))
return "\nAvailable prototypes (case sensitive): %s" % (
"\n" + utils.fill(prots) if prots else "None")
def _parse_prototype(inp, allow_key=False):
try:
# make use of _convert_from_string from the SetAttribute command
prototype = _convert_from_string(self, inp)
except SyntaxError:
# this means literal_eval tried to parse a faulty string
string = ("|RCritical Python syntax error in argument. Only primitive "
"Python structures are allowed. \nYou also need to use correct "
"Python syntax. Remember especially to put quotes around all "
"strings inside lists and dicts.|n")
self.caller.msg(string)
return None
if isinstance(prototype, dict):
# an actual prototype. We need to make sure it's safe. Don't allow exec
if "exec" in prototype and not self.caller.check_permstring("Developer"):
self.caller.msg("Spawn aborted: You don't have access to "
"use the 'exec' prototype key.")
return None
elif isinstance(prototype, basestring):
# a prototype key
if allow_key:
return prototype
else:
self.caller.msg("The prototype must be defined as a Python dictionary.")
else:
caller.msg("The prototype must be given either as a Python dictionary or a key")
return None
def _search_show_prototype(query):
# prototype detail
strings = []
metaprots = search_prototype(key=query, return_meta=True)
if metaprots:
for metaprot in metaprots:
header = (
"|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n"
"|cdesc:|n {} \n|cprototype:|n ".format(
metaprot.key, ", ".join(metaprot.tags),
metaprot.locks, metaprot.desc))
prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value)
for key, value in
sorted(metaprot.prototype.items())).rstrip(",")))
strings.append(header + prototype)
return "\n".join(strings)
else:
return False
caller = self.caller
if 'search' in self.switches:
# query for a key match
if not self.args:
self.switches.append("list")
else:
key, tags = self.args.strip(), None
if ';' in self.args:
key, tags = (part.strip().lower() for part in self.args.split(";", 1))
tags = [tag.strip() for tag in tags.split(",")] if tags else None
EvMore(caller, unicode(list_prototypes(caller, key=key, tags=tags)),
exit_on_lastpage=True)
return
if 'show' in self.switches:
# the argument is a key in this case (may be a partial key)
if not self.args:
self.switches.append('list')
else:
EvMore(caller, unicode(list_prototypes(key=self.args), exit_on_lastpage=True))
matchstring = _search_show_prototype(self.args)
if matchstring:
caller.msg(matchstring)
else:
caller.msg("No prototype '{}' was found.".format(self.args))
return
if 'list' in self.switches:
# for list, all optional arguments are tags
EvMore(caller, unicode(list_prototypes(tags=self.lhslist)), exit_on_lastpage=True)
EvMore(caller, unicode(list_prototypes(caller,
tags=self.lhslist)), exit_on_lastpage=True)
return
if 'save' in self.switches:
if not self.args or not self.rhs:
caller.msg("Usage: @spawn/save <key>[;desc[;tag,tag[,...][;lockstring]]] = <prototype_dict>")
return
# handle lhs
parts = self.rhs.split(";", 3)
key, desc, tags, lockstring = "", "", [], ""
nparts = len(parts)
if nparts == 1:
key = parts.strip()
elif nparts == 2:
key, desc = (part.strip() for part in parts)
elif nparts == 3:
key, desc, tags = (part.strip() for part in parts)
tags = [tag.strip().lower() for tag in tags.split(",")]
else:
# lockstrings can itself contain ;
key, desc, tags, lockstring = (part.strip() for part in parts)
tags = [tag.strip().lower() for tag in tags.split(",")]
# handle rhs:
prototype = _parse_prototype(caller, self.rhs)
if not prototype:
return
# check for existing prototype
matchstring = _search_show_prototype(key)
if matchstring:
caller.msg("|yExisting saved prototype found:|n\n{}".format(matchstring))
answer = ("Do you want to replace the existing prototype? Y/[N]")
if not answer.lower() not in ["y", "yes"]:
caller.msg("Save cancelled.")
# all seems ok. Try to save.
try:
store_prototype(caller, key, prototype, desc=desc, tags=tags, locks=lockstring)
except PermissionError as err:
caller.msg("|rError saving:|R {}|n".format(err))
return
caller.msg("Saved prototype:")
caller.execute_cmd("spawn/show {}".format(key))
return
if not self.args:
ncount = len(search_prototype())
caller.msg("Usage: @spawn <prototype-key> or {key: value, ...}"
caller.msg("Usage: @spawn <prototype-key> or {{key: value, ...}}"
"\n ({} existing prototypes. Use /list to inspect)".format(ncount))
return
# A direct creation of an object from a given prototype
prototypes = spawn(return_prototypes=True)
if not self.args:
string = "Usage: @spawn {key:value, key, value, ... }"
self.caller.msg(string + _show_prototypes(prototypes))
return
try:
# make use of _convert_from_string from the SetAttribute command
prototype = _convert_from_string(self, self.args)
except SyntaxError:
# this means literal_eval tried to parse a faulty string
string = "|RCritical Python syntax error in argument. "
string += "Only primitive Python structures are allowed. "
string += "\nYou also need to use correct Python syntax. "
string += "Remember especially to put quotes around all "
string += "strings inside lists and dicts.|n"
self.caller.msg(string)
prototype = _parse_prototype(self.args, allow_key=True)
if not prototype:
return
if isinstance(prototype, basestring):
# A prototype key
keystr = prototype
prototype = prototypes.get(prototype, None)
if not prototype:
string = "No prototype named '%s'." % keystr
self.caller.msg(string + _show_prototypes(prototypes))
# A prototype key we are looking to apply
metaprotos = search_prototype(prototype)
nprots = len(metaprotos)
if not metaprotos:
caller.msg("No prototype named '%s'." % prototype)
return
elif isinstance(prototype, dict):
# we got the prototype on the command line. We must make sure to not allow
# the 'exec' key unless we are developers or higher.
if "exec" in prototype and not self.caller.check_permstring("Developer"):
self.caller.msg("Spawn aborted: You don't have access to use the 'exec' prototype key.")
elif nprots > 1:
caller.msg("Found {} prototypes matching '{}':\n {}".format(
nprots, prototype, ", ".join(metaproto.key for metaproto in metaprotos)))
return
else:
self.caller.msg("The prototype must be a prototype key or a Python dictionary.")
return
# we have a metaprot, check access
metaproto = metaprotos[0]
if not caller.locks.check_lockstring(caller, metaproto.locks, access_type='use'):
caller.msg("You don't have access to use this prototype.")
return
prototype = metaproto.prototype
if "noloc" not in self.switches and "location" not in prototype:
prototype["location"] = self.caller.location
# proceed to spawning
for obj in spawn(prototype):
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))

View file

@ -113,7 +113,7 @@ class MuxCommand(Command):
# check for arg1, arg2, ... = argA, argB, ... constructs
lhs, rhs = args, None
lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
lhslist, rhslist = [arg.strip() for arg in args.split(',') if arg], []
if args and '=' in args:
lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
lhslist = [arg.strip() for arg in lhs.split(',')]

View file

@ -202,15 +202,18 @@ class EvMore(object):
# goto top of the text
self.page_top()
def display(self):
def display(self, show_footer=True):
"""
Pretty-print the page.
"""
pos = self._pos
text = self._pages[pos]
page = _DISPLAY.format(text=text,
pageno=pos + 1,
pagemax=self._npages)
if show_footer:
page = _DISPLAY.format(text=text,
pageno=pos + 1,
pagemax=self._npages)
else:
page = text
# check to make sure our session is still valid
sessions = self._caller.sessions.get()
if not sessions:
@ -245,9 +248,11 @@ class EvMore(object):
self.page_quit()
else:
self._pos += 1
self.display()
if self.exit_on_lastpage and self._pos >= self._npages - 1:
self.page_quit()
if self.exit_on_lastpage and self._pos >= (self._npages - 1):
self.display(show_footer=False)
self.page_quit(quiet=True)
else:
self.display()
def page_back(self):
"""
@ -256,16 +261,18 @@ class EvMore(object):
self._pos = max(0, self._pos - 1)
self.display()
def page_quit(self):
def page_quit(self, quiet=False):
"""
Quit the pager
"""
del self._caller.ndb._more
self._caller.msg(text=self._exit_msg, **self._kwargs)
if not quiet:
self._caller.msg(text=self._exit_msg, **self._kwargs)
self._caller.cmdset.remove(CmdSetMore)
def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, **kwargs):
def msg(caller, text="", always_page=False, session=None,
justify_kwargs=None, exit_on_lastpage=True, **kwargs):
"""
More-supported version of msg, mimicking the normal msg method.
@ -280,9 +287,10 @@ def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, *
justify_kwargs (dict, bool or None, optional): If given, this should
be valid keyword arguments to the utils.justify() function. If False,
no justification will be done.
exit_on_lastpage (bool, optional): Immediately exit pager when reaching the last page.
kwargs (any, optional): These will be passed on
to the `caller.msg` method.
"""
EvMore(caller, text, always_page=always_page, session=session,
justify_kwargs=justify_kwargs, **kwargs)
justify_kwargs=justify_kwargs, exit_on_lastpage=exit_on_lastpage, **kwargs)

View file

@ -1,269 +0,0 @@
"""
OLC storage and sharing 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 collections import namedtuple
from django.conf import settings
from evennia.scripts.scripts import DefaultScript
from evennia.utils.create import create_script
from evennia.utils.utils import make_iter, all_from_module
from evennia.utils.evtable import EvTable
# prepare the available prototypes defined in modules
_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})
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 get_prototype_list(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

View file

@ -483,7 +483,7 @@ def search_readonly_prototype(key=None, tags=None):
return [match for match in matches.values()]
def search_prototype(key=None, tags=None):
def search_prototype(key=None, tags=None, return_meta=True):
"""
Find prototypes based on key and/or tags.
@ -492,8 +492,11 @@ def search_prototype(key=None, tags=None):
tags (str or list): Tag key or keys to query for. These
will always be applied with the 'persistent_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.
matches (list): All found prototype dicts or MetaProtos
Note:
The available prototypes is a combination of those supplied in
@ -505,10 +508,30 @@ def search_prototype(key=None, tags=None):
"""
matches = []
if key and key in _READONLY_PROTOTYPES:
matches.append(_READONLY_PROTOTYPES[key][3])
if return_meta:
matches.append(_READONLY_PROTOTYPES[key])
else:
matches.append(_READONLY_PROTOTYPES[key][3])
elif tags:
if return_meta:
matches.extend(
[MetaProto(prot.key, prot.desc, prot.locks.all(),
prot.tags.all(), prot.attributes.get("prototype"))
for prot in search_persistent_prototype(key, tags)])
else:
matches.extend([prot.attributes.get("prototype")
for prot in search_persistent_prototype(key, tags)])
else:
matches.extend([prot.attributes.get("prototype")
for prot in search_persistent_prototype(key, tags)])
# neither key nor tags given. Return all.
if return_meta:
matches = [MetaProto(prot.key, prot.desc, prot.locks.all(),
prot.tags.all(), prot.attributes.get("prototype"))
for prot in search_persistent_prototype(key, tags)] + \
list(_READONLY_PROTOTYPES.values())
else:
matches = [prot.attributes.get("prototype")
for prot in search_persistent_prototype()] + \
[metaprot[3] for metaprot in _READONLY_PROTOTYPES.values()]
return matches