Merge branch 'fix-protoype-spawn'
This commit is contained in:
commit
c7005cf5d6
8 changed files with 689 additions and 375 deletions
|
|
@ -54,6 +54,8 @@ without arguments starts a full interactive Python console.
|
||||||
bugfixes.
|
bugfixes.
|
||||||
- Remove `dummy@example.com` as a default account email when unset, a string is no longer
|
- Remove `dummy@example.com` as a default account email when unset, a string is no longer
|
||||||
required by Django.
|
required by Django.
|
||||||
|
- Fixes to `spawn`, make updating an existing prototype/object work better. Add `/raw` switch
|
||||||
|
to `spawn` command to extract the raw prototype dict for manual editing.
|
||||||
|
|
||||||
|
|
||||||
## Evennia 0.9 (2018-2019)
|
## Evennia 0.9 (2018-2019)
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,19 @@ from django.db.models import Q, Min, Max
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.locks.lockhandler import LockException
|
from evennia.locks.lockhandler import LockException
|
||||||
from evennia.commands.cmdhandler import get_and_merge_cmdsets
|
from evennia.commands.cmdhandler import get_and_merge_cmdsets
|
||||||
from evennia.utils import create, utils, search
|
from evennia.utils import create, utils, search, logger
|
||||||
from evennia.utils.utils import (
|
from evennia.utils.utils import (
|
||||||
inherits_from,
|
inherits_from,
|
||||||
class_from_module,
|
class_from_module,
|
||||||
get_all_typeclasses,
|
get_all_typeclasses,
|
||||||
variable_from_module,
|
variable_from_module,
|
||||||
dbref,
|
dbref, interactive,
|
||||||
|
list_to_string
|
||||||
)
|
)
|
||||||
from evennia.utils.eveditor import EvEditor
|
from evennia.utils.eveditor import EvEditor
|
||||||
from evennia.utils.evmore import EvMore
|
from evennia.utils.evmore import EvMore
|
||||||
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
|
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
|
||||||
from evennia.utils.ansi import raw
|
from evennia.utils.ansi import raw
|
||||||
from evennia.prototypes.menus import _format_diff_text_and_options
|
|
||||||
|
|
||||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||||
|
|
||||||
|
|
@ -2099,10 +2099,10 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
||||||
# to confirm changes.
|
# to confirm changes.
|
||||||
if "prototype" in self.switches:
|
if "prototype" in self.switches:
|
||||||
diff, _ = spawner.prototype_diff_from_object(prototype, obj)
|
diff, _ = spawner.prototype_diff_from_object(prototype, obj)
|
||||||
txt, options = _format_diff_text_and_options(diff, objects=[obj])
|
txt = spawner.format_diff(diff)
|
||||||
prompt = (
|
prompt = (
|
||||||
"Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n"
|
"Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n"
|
||||||
% (prototype["key"], obj.name, "\n".join(txt))
|
% (prototype["key"], obj.name, txt)
|
||||||
)
|
)
|
||||||
if not reset:
|
if not reset:
|
||||||
prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state."
|
prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state."
|
||||||
|
|
@ -3227,6 +3227,10 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
||||||
self.caller.msg(string)
|
self.caller.msg(string)
|
||||||
|
|
||||||
|
|
||||||
|
# helper functions for spawn
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
spawn objects from prototype
|
spawn objects from prototype
|
||||||
|
|
@ -3250,13 +3254,14 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
search - search prototype by name or tags.
|
search - search prototype by name or tags.
|
||||||
list - list available prototypes, optionally limit by tags.
|
list - list available prototypes, optionally limit by tags.
|
||||||
show, examine - inspect prototype by key. If not given, acts like list.
|
show, examine - inspect prototype by key. If not given, acts like list.
|
||||||
|
raw - show the raw dict of the prototype as a one-line string for manual editing.
|
||||||
save - save a prototype to the database. It will be listable by /list.
|
save - save a prototype to the database. It will be listable by /list.
|
||||||
delete - remove a prototype from database, if allowed to.
|
delete - remove a prototype from database, if allowed to.
|
||||||
update - find existing objects with the same prototype_key and update
|
update - find existing objects with the same prototype_key and update
|
||||||
them with latest version of given prototype. If given with /save,
|
them with latest version of given prototype. If given with /save,
|
||||||
will auto-update all objects with the old version of the prototype
|
will auto-update all objects with the old version of the prototype
|
||||||
without asking first.
|
without asking first.
|
||||||
edit, olc - create/manipulate prototype in a menu interface.
|
edit, menu, olc - create/manipulate prototype in a menu interface.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
spawn GOBLIN
|
spawn GOBLIN
|
||||||
|
|
@ -3298,6 +3303,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
"search",
|
"search",
|
||||||
"list",
|
"list",
|
||||||
"show",
|
"show",
|
||||||
|
"raw",
|
||||||
"examine",
|
"examine",
|
||||||
"save",
|
"save",
|
||||||
"delete",
|
"delete",
|
||||||
|
|
@ -3309,56 +3315,209 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
locks = "cmd:perm(spawn) or perm(Builder)"
|
locks = "cmd:perm(spawn) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
def _search_prototype(self, prototype_key, quiet=False):
|
||||||
|
"""
|
||||||
|
Search for prototype and handle no/multi-match and access.
|
||||||
|
|
||||||
|
Returns a single found prototype or None - in the
|
||||||
|
case, the caller has already been informed of the
|
||||||
|
search error we need not do any further action.
|
||||||
|
|
||||||
|
"""
|
||||||
|
prototypes = protlib.search_prototype(prototype_key)
|
||||||
|
nprots = len(prototypes)
|
||||||
|
|
||||||
|
# handle the search result
|
||||||
|
err = None
|
||||||
|
if not prototypes:
|
||||||
|
err = f"No prototype named '{prototype_key}' was found."
|
||||||
|
elif nprots > 1:
|
||||||
|
err = "Found {} prototypes matching '{}':\n {}".format(
|
||||||
|
nprots,
|
||||||
|
prototype_key,
|
||||||
|
", ".join(proto.get("prototype_key", "") for proto in prototypes),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# we have a single prototype, check access
|
||||||
|
prototype = prototypes[0]
|
||||||
|
if not self.caller.locks.check_lockstring(
|
||||||
|
self.caller, prototype.get("prototype_locks", ""),
|
||||||
|
access_type="spawn", default=True):
|
||||||
|
err = "You don't have access to use this prototype."
|
||||||
|
|
||||||
|
if err:
|
||||||
|
# return None on any error
|
||||||
|
if not quiet:
|
||||||
|
self.caller.msg(err)
|
||||||
|
return
|
||||||
|
return prototype
|
||||||
|
|
||||||
|
def _parse_prototype(self, inp, expect=dict):
|
||||||
|
"""
|
||||||
|
Parse a prototype dict or key from the input and convert it safely
|
||||||
|
into a dict if appropriate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
inp (str): The input from user.
|
||||||
|
expect (type, optional):
|
||||||
|
Returns:
|
||||||
|
prototype (dict, str or None): The parsed prototype. If None, the error
|
||||||
|
was already reported.
|
||||||
|
|
||||||
|
"""
|
||||||
|
eval_err = None
|
||||||
|
try:
|
||||||
|
prototype = _LITERAL_EVAL(inp)
|
||||||
|
except (SyntaxError, ValueError) as err:
|
||||||
|
# treat as string
|
||||||
|
eval_err = err
|
||||||
|
prototype = utils.to_str(inp)
|
||||||
|
finally:
|
||||||
|
# it's possible that the input was a prototype-key, in which case
|
||||||
|
# it's okay for the LITERAL_EVAL to fail. Only if the result does not
|
||||||
|
# match the expected type do we have a problem.
|
||||||
|
if not isinstance(prototype, expect):
|
||||||
|
if eval_err:
|
||||||
|
string = (
|
||||||
|
f"{inp}\n{eval_err}\n|RCritical Python syntax error in argument. Only primitive "
|
||||||
|
"Python structures are allowed. \nMake sure to use correct "
|
||||||
|
"Python syntax. Remember especially to put quotes around all "
|
||||||
|
"strings inside lists and dicts.|n For more advanced uses, embed "
|
||||||
|
"inlinefuncs in the strings."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
string = "Expected {}, got {}.".format(expect, type(prototype))
|
||||||
|
self.caller.msg(string)
|
||||||
|
return
|
||||||
|
|
||||||
|
if expect == dict:
|
||||||
|
# an actual prototype. We need to make sure it's safe,
|
||||||
|
# so don't allow exec.
|
||||||
|
# TODO: Exec support is deprecated. Remove completely for 1.0.
|
||||||
|
if "exec" in prototype and not self.caller.check_permstring("Developer"):
|
||||||
|
self.caller.msg(
|
||||||
|
"Spawn aborted: You are not allowed to " "use the 'exec' prototype key."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
# we homogenize the protoype first, to be more lenient with free-form
|
||||||
|
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
|
||||||
|
except RuntimeError as err:
|
||||||
|
self.caller.msg(str(err))
|
||||||
|
return
|
||||||
|
return prototype
|
||||||
|
|
||||||
|
def _get_prototype_detail(self, query=None, prototypes=None):
|
||||||
|
"""
|
||||||
|
Display the detailed specs of one or more prototypes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str, optional): If this is given and `prototypes` is not, search for
|
||||||
|
the prototype(s) by this query. This may be a partial query which
|
||||||
|
may lead to multiple matches, all being displayed.
|
||||||
|
prototypes (list, optional): If given, ignore `query` and only show these
|
||||||
|
prototype-details.
|
||||||
|
Returns:
|
||||||
|
display (str, None): A formatted string of one or more prototype details.
|
||||||
|
If None, the caller was already informed of the error.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not prototypes:
|
||||||
|
# we need to query. Note that if query is None, all prototypes will
|
||||||
|
# be returned.
|
||||||
|
prototypes = protlib.search_prototype(key=query)
|
||||||
|
if prototypes:
|
||||||
|
return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes)
|
||||||
|
elif query:
|
||||||
|
self.caller.msg(f"No prototype named '{query}' was found.")
|
||||||
|
else:
|
||||||
|
self.caller.msg(f"No prototypes found.")
|
||||||
|
|
||||||
|
def _list_prototypes(self, key=None, tags=None):
|
||||||
|
"""Display prototypes as a list, optionally limited by key/tags. """
|
||||||
|
table = protlib.list_prototypes(self.caller, key=key, tags=tags)
|
||||||
|
if not table:
|
||||||
|
return True
|
||||||
|
EvMore(
|
||||||
|
self.caller,
|
||||||
|
str(table),
|
||||||
|
exit_on_lastpage=True,
|
||||||
|
justify_kwargs=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@interactive
|
||||||
|
def _update_existing_objects(self, caller, prototype_key, quiet=False):
|
||||||
|
"""
|
||||||
|
Update existing objects (if any) with this prototype-key to the latest
|
||||||
|
prototype version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
caller (Object): This is necessary for @interactive to work.
|
||||||
|
prototype_key (str): The prototype to update.
|
||||||
|
quiet (bool, optional): If set, don't report to user if no
|
||||||
|
old objects were found to update.
|
||||||
|
Returns:
|
||||||
|
n_updated (int): Number of updated objects.
|
||||||
|
|
||||||
|
"""
|
||||||
|
prototype = self._search_prototype(prototype_key)
|
||||||
|
if not prototype:
|
||||||
|
return
|
||||||
|
|
||||||
|
existing_objects = protlib.search_objects_with_prototype(prototype_key)
|
||||||
|
if not existing_objects:
|
||||||
|
if not quiet:
|
||||||
|
caller.msg("No existing objects found with an older version of this prototype.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if existing_objects:
|
||||||
|
n_existing = len(existing_objects)
|
||||||
|
slow = " (note that this may be slow)" if n_existing > 10 else ""
|
||||||
|
string = (
|
||||||
|
f"There are {n_existing} existing object(s) with an older version "
|
||||||
|
f"of prototype '{prototype_key}'. Should it be re-applied to them{slow}? [Y]/N"
|
||||||
|
)
|
||||||
|
answer = yield (string)
|
||||||
|
if answer.lower() in ["n", "no"]:
|
||||||
|
caller.msg(
|
||||||
|
"|rNo update was done of existing objects. "
|
||||||
|
"Use spawn/update <key> to apply later as needed.|n"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
n_updated = spawner.batch_update_objects_with_prototype(
|
||||||
|
prototype, objects=existing_objects)
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
caller.msg(f"{n_updated} objects were updated.")
|
||||||
|
return
|
||||||
|
|
||||||
|
def _parse_key_desc_tags(self, argstring, desc=True):
|
||||||
|
"""
|
||||||
|
Parse ;-separated input list.
|
||||||
|
"""
|
||||||
|
key, desc, tags = "", "", []
|
||||||
|
if ";" in argstring:
|
||||||
|
parts = [part.strip().lower() for part in argstring.split(";")]
|
||||||
|
if len(parts) > 1 and desc:
|
||||||
|
key = parts[0]
|
||||||
|
desc = parts[1]
|
||||||
|
tags = parts[2:]
|
||||||
|
else:
|
||||||
|
key = parts[0]
|
||||||
|
tags = parts[1:]
|
||||||
|
else:
|
||||||
|
key = argstring.strip().lower()
|
||||||
|
return key, desc, tags
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""Implements the spawner"""
|
"""Implements the spawner"""
|
||||||
|
|
||||||
def _parse_prototype(inp, expect=dict):
|
|
||||||
err = None
|
|
||||||
try:
|
|
||||||
prototype = _LITERAL_EVAL(inp)
|
|
||||||
except (SyntaxError, ValueError) as err:
|
|
||||||
# treat as string
|
|
||||||
prototype = utils.to_str(inp)
|
|
||||||
finally:
|
|
||||||
if not isinstance(prototype, expect):
|
|
||||||
if err:
|
|
||||||
string = (
|
|
||||||
"{}\n|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 For more advanced uses, embed "
|
|
||||||
"inline functions in the strings.".format(err)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
string = "Expected {}, got {}.".format(expect, type(prototype))
|
|
||||||
self.caller.msg(string)
|
|
||||||
return None
|
|
||||||
if expect == 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 are not allowed to " "use the 'exec' prototype key."
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
# we homogenize first, to be more lenient
|
|
||||||
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
|
|
||||||
except RuntimeError as err:
|
|
||||||
self.caller.msg(str(err))
|
|
||||||
return
|
|
||||||
return prototype
|
|
||||||
|
|
||||||
def _search_show_prototype(query, prototypes=None):
|
|
||||||
# prototype detail
|
|
||||||
if not prototypes:
|
|
||||||
prototypes = protlib.search_prototype(key=query)
|
|
||||||
if prototypes:
|
|
||||||
return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
caller = self.caller
|
caller = self.caller
|
||||||
|
noloc = "noloc" in self.switches
|
||||||
|
|
||||||
|
# run the menu/olc
|
||||||
if (
|
if (
|
||||||
self.cmdstring == "olc"
|
self.cmdstring == "olc"
|
||||||
or "menu" in self.switches
|
or "menu" in self.switches
|
||||||
|
|
@ -3368,94 +3527,122 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
# OLC menu mode
|
# OLC menu mode
|
||||||
prototype = None
|
prototype = None
|
||||||
if self.lhs:
|
if self.lhs:
|
||||||
key = self.lhs
|
prototype_key = self.lhs
|
||||||
prototype = protlib.search_prototype(key=key)
|
prototype = self._search_prototype(prototype_key)
|
||||||
if len(prototype) > 1:
|
if not prototype:
|
||||||
caller.msg(
|
|
||||||
"More than one match for {}:\n{}".format(
|
|
||||||
key, "\n".join(proto.get("prototype_key", "") for proto in prototype)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
elif prototype:
|
|
||||||
# one match
|
|
||||||
prototype = prototype[0]
|
|
||||||
else:
|
|
||||||
# no match
|
|
||||||
caller.msg("No prototype '{}' was found.".format(key))
|
|
||||||
return
|
return
|
||||||
olc_menus.start_olc(caller, session=self.session, prototype=prototype)
|
olc_menus.start_olc(caller, session=self.session, prototype=prototype)
|
||||||
return
|
return
|
||||||
|
|
||||||
if "search" in self.switches:
|
if "search" in self.switches:
|
||||||
# query for a key match
|
# query for a key match. The arg is a search query or nothing.
|
||||||
|
|
||||||
if not self.args:
|
if not self.args:
|
||||||
self.switches.append("list")
|
# an empty search returns the full list
|
||||||
else:
|
self._list_prototypes()
|
||||||
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,
|
|
||||||
str(protlib.list_prototypes(caller, key=key, tags=tags)),
|
|
||||||
exit_on_lastpage=True,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# search for key;tag combinations
|
||||||
|
key, _, tags = self._parse_key_desc_tags(self.args, desc=False)
|
||||||
|
self._list_prototypes(key, tags)
|
||||||
|
return
|
||||||
|
|
||||||
|
if "raw" in self.switches:
|
||||||
|
# query for key match and return the prototype as a safe one-liner string.
|
||||||
|
if not self.args:
|
||||||
|
caller.msg("You need to specify a prototype-key to get the raw data for.")
|
||||||
|
prototype = self._search_prototype(self.args)
|
||||||
|
if not prototype:
|
||||||
|
return
|
||||||
|
caller.msg(str(prototype))
|
||||||
|
return
|
||||||
|
|
||||||
if "show" in self.switches or "examine" in self.switches:
|
if "show" in self.switches or "examine" in self.switches:
|
||||||
# the argument is a key in this case (may be a partial key)
|
# show a specific prot detail. The argument is a search query or empty.
|
||||||
if not self.args:
|
if not self.args:
|
||||||
self.switches.append("list")
|
# we don't show the list of all details, that's too spammy.
|
||||||
else:
|
caller.msg("You need to specify a prototype-key to show.")
|
||||||
matchstring = _search_show_prototype(self.args)
|
|
||||||
if matchstring:
|
|
||||||
caller.msg(matchstring)
|
|
||||||
else:
|
|
||||||
caller.msg("No prototype '{}' was found.".format(self.args))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if "list" in self.switches:
|
detail_string = self._get_prototype_detail(self.args)
|
||||||
# for list, all optional arguments are tags
|
if not detail_string:
|
||||||
# import pudb; pudb.set_trace()
|
return
|
||||||
|
caller.msg(detail_string)
|
||||||
|
return
|
||||||
|
|
||||||
EvMore(
|
if "list" in self.switches:
|
||||||
caller,
|
# for list, all optional arguments are tags.
|
||||||
str(protlib.list_prototypes(caller, tags=self.lhslist)),
|
tags = self.lhslist
|
||||||
exit_on_lastpage=True,
|
err = self._list_prototypes(tags=tags)
|
||||||
justify_kwargs=False,
|
if err:
|
||||||
)
|
caller.msg("No prototypes found with prototype-tag(s): {}".format(
|
||||||
|
list_to_string(tags, "or")))
|
||||||
return
|
return
|
||||||
|
|
||||||
if "save" in self.switches:
|
if "save" in self.switches:
|
||||||
# store a prototype to the database store
|
# store a prototype to the database store
|
||||||
if not self.args:
|
if not self.args:
|
||||||
caller.msg(
|
caller.msg(
|
||||||
"Usage: spawn/save <key>[;desc[;tag,tag[,...][;lockstring]]] = <prototype_dict>"
|
"Usage: spawn/save [<key>[;desc[;tag,tag[,...][;lockstring]]]] = <prototype_dict>"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
if self.rhs:
|
||||||
|
# input on the form key = prototype
|
||||||
|
prototype_key, prototype_desc, prototype_tags = self._parse_key_desc_tags(self.lhs)
|
||||||
|
prototype_key = None if not prototype_key else prototype_key
|
||||||
|
prototype_desc = None if not prototype_desc else prototype_desc
|
||||||
|
prototype_tags = None if not prototype_tags else prototype_tags
|
||||||
|
prototype_input = self.rhs.strip()
|
||||||
|
else:
|
||||||
|
prototype_key = prototype_desc = None
|
||||||
|
prototype_tags = None
|
||||||
|
prototype_input = self.lhs.strip()
|
||||||
|
|
||||||
# handle rhs:
|
# handle parsing
|
||||||
prototype = _parse_prototype(self.lhs.strip())
|
prototype = self._parse_prototype(prototype_input)
|
||||||
if not prototype:
|
if not prototype:
|
||||||
return
|
return
|
||||||
|
|
||||||
# present prototype to save
|
prot_prototype_key = prototype.get("prototype_key")
|
||||||
new_matchstring = _search_show_prototype("", prototypes=[prototype])
|
|
||||||
string = "|yCreating new prototype:|n\n{}".format(new_matchstring)
|
|
||||||
question = "\nDo you want to continue saving? [Y]/N"
|
|
||||||
|
|
||||||
prototype_key = prototype.get("prototype_key")
|
if not (prototype_key or prot_prototype_key):
|
||||||
if not prototype_key:
|
caller.msg("A prototype_key must be given, either as `prototype_key = <prototype>` "
|
||||||
caller.msg("\n|yTo save a prototype it must have the 'prototype_key' set.")
|
"or as a key 'prototype_key' inside the prototype structure.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# check for existing prototype,
|
if prototype_key is None:
|
||||||
old_matchstring = _search_show_prototype(prototype_key)
|
prototype_key = prot_prototype_key
|
||||||
|
|
||||||
if old_matchstring:
|
if prot_prototype_key != prototype_key:
|
||||||
string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring)
|
caller.msg("(Replacing `prototype_key` in prototype with given key.)")
|
||||||
question = "\n|yDo you want to replace the existing prototype?|n [Y]/N"
|
prototype['prototype_key'] = prototype_key
|
||||||
|
|
||||||
|
if prototype_desc is not None and prot_prototype_key != prototype_desc:
|
||||||
|
caller.msg("(Replacing `prototype_desc` in prototype with given desc.)")
|
||||||
|
prototype['prototype_desc'] = prototype_desc
|
||||||
|
if prototype_tags is not None and prototype.get("prototype_tags") != prototype_tags:
|
||||||
|
caller.msg("(Replacing `prototype_tags` in prototype with given tag(s))" )
|
||||||
|
prototype['prototype_tags'] = prototype_tags
|
||||||
|
|
||||||
|
string = ""
|
||||||
|
# check for existing prototype (exact match)
|
||||||
|
old_prototype = self._search_prototype(prototype_key, quiet=True)
|
||||||
|
|
||||||
|
diff = spawner.prototype_diff(old_prototype, prototype, homogenize=True)
|
||||||
|
diffstr = spawner.format_diff(diff)
|
||||||
|
new_prototype_detail = self._get_prototype_detail(prototypes=[prototype])
|
||||||
|
|
||||||
|
if old_prototype:
|
||||||
|
if not diffstr:
|
||||||
|
string = f"|yAlready existing Prototype:|n\n{new_prototype_detail}\n"
|
||||||
|
question = "\nThere seems to be no changes. Do you still want to (re)save? [Y]/N"
|
||||||
|
else:
|
||||||
|
string = (f"|yExisting prototype \"{prototype_key}\" found. Change:|n\n{diffstr}\n"
|
||||||
|
f"|yNew changed prototype:|n\n{new_prototype_detail}")
|
||||||
|
question = "\n|yDo you want to apply the change to the existing prototype?|n [Y]/N"
|
||||||
|
else:
|
||||||
|
string = f"|yCreating new prototype:|n\n{new_prototype_detail}"
|
||||||
|
question = "\nDo you want to continue saving? [Y]/N"
|
||||||
|
|
||||||
answer = yield (string + question)
|
answer = yield (string + question)
|
||||||
if answer.lower() in ["n", "no"]:
|
if answer.lower() in ["n", "no"]:
|
||||||
|
|
@ -3474,82 +3661,52 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
caller.msg("|gSaved prototype:|n {}".format(prototype_key))
|
caller.msg("|gSaved prototype:|n {}".format(prototype_key))
|
||||||
|
|
||||||
# check if we want to update existing objects
|
# check if we want to update existing objects
|
||||||
existing_objects = protlib.search_objects_with_prototype(prototype_key)
|
|
||||||
if existing_objects:
|
self._update_existing_objects(self.caller, prototype_key, quiet=True)
|
||||||
if "update" not in self.switches:
|
|
||||||
n_existing = len(existing_objects)
|
|
||||||
slow = " (note that this may be slow)" if n_existing > 10 else ""
|
|
||||||
string = (
|
|
||||||
"There are {} objects already created with an older version "
|
|
||||||
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
|
|
||||||
n_existing, prototype_key, slow
|
|
||||||
)
|
|
||||||
)
|
|
||||||
answer = yield (string)
|
|
||||||
if answer.lower() in ["n", "no"]:
|
|
||||||
caller.msg(
|
|
||||||
"|rNo update was done of existing objects. "
|
|
||||||
"Use spawn/update <key> to apply later as needed.|n"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
|
|
||||||
caller.msg("{} objects were updated.".format(n_updated))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.args:
|
if not self.args:
|
||||||
|
# all switches beyond this point gets a common non-arg return
|
||||||
ncount = len(protlib.search_prototype())
|
ncount = len(protlib.search_prototype())
|
||||||
caller.msg(
|
caller.msg(
|
||||||
"Usage: spawn <prototype-key> or {{key: value, ...}}"
|
"Usage: spawn <prototype-key> or {{key: value, ...}}"
|
||||||
"\n ({} existing prototypes. Use /list to inspect)".format(ncount)
|
f"\n ({ncount} existing prototypes. Use /list to inspect)"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if "delete" in self.switches:
|
if "delete" in self.switches:
|
||||||
# remove db-based prototype
|
# remove db-based prototype
|
||||||
matchstring = _search_show_prototype(self.args)
|
prototype_detail = self._get_prototype_detail(self.args)
|
||||||
if matchstring:
|
if not prototype_detail:
|
||||||
string = "|rDeleting prototype:|n\n{}".format(matchstring)
|
|
||||||
question = "\nDo you want to continue deleting? [Y]/N"
|
|
||||||
answer = yield (string + question)
|
|
||||||
if answer.lower() in ["n", "no"]:
|
|
||||||
caller.msg("|rDeletion cancelled.|n")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
success = protlib.delete_prototype(self.args)
|
|
||||||
except protlib.PermissionError as err:
|
|
||||||
caller.msg("|rError deleting:|R {}|n".format(err))
|
|
||||||
caller.msg(
|
|
||||||
"Deletion {}.".format(
|
|
||||||
"successful" if success else "failed (does the prototype exist?)"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
string = f"|rDeleting prototype:|n\n{prototype_detail}"
|
||||||
|
question = "\nDo you want to continue deleting? [Y]/N"
|
||||||
|
answer = yield (string + question)
|
||||||
|
if answer.lower() in ["n", "no"]:
|
||||||
|
caller.msg("|rDeletion cancelled.|n")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = protlib.delete_prototype(self.args)
|
||||||
|
except protlib.PermissionError as err:
|
||||||
|
retmsg = f"|rError deleting:|R {err}|n"
|
||||||
else:
|
else:
|
||||||
caller.msg("Could not find prototype '{}'".format(key))
|
retmsg = ("Deletion successful" if success else
|
||||||
|
"Deletion failed (does the prototype exist?)")
|
||||||
|
caller.msg(retmsg)
|
||||||
|
return
|
||||||
|
|
||||||
if "update" in self.switches:
|
if "update" in self.switches:
|
||||||
# update existing prototypes
|
# update existing prototypes
|
||||||
key = self.args.strip().lower()
|
prototype_key = self.args.strip().lower()
|
||||||
existing_objects = protlib.search_objects_with_prototype(key)
|
self._update_existing_objects(self.caller, prototype_key)
|
||||||
if existing_objects:
|
return
|
||||||
n_existing = len(existing_objects)
|
|
||||||
slow = " (note that this may be slow)" if n_existing > 10 else ""
|
|
||||||
string = (
|
|
||||||
"There are {} objects already created with an older version "
|
|
||||||
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
|
|
||||||
n_existing, key, slow
|
|
||||||
)
|
|
||||||
)
|
|
||||||
answer = yield (string)
|
|
||||||
if answer.lower() in ["n", "no"]:
|
|
||||||
caller.msg("|rUpdate cancelled.")
|
|
||||||
return
|
|
||||||
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
|
|
||||||
caller.msg("{} objects were updated.".format(n_updated))
|
|
||||||
|
|
||||||
# A direct creation of an object from a given prototype
|
# If we get to this point, we use not switches but are trying a
|
||||||
|
# direct creation of an object from a given prototype or -key
|
||||||
|
|
||||||
prototype = _parse_prototype(
|
prototype = self._parse_prototype(
|
||||||
self.args, expect=dict if self.args.strip().startswith("{") else str
|
self.args, expect=dict if self.args.strip().startswith("{") else str
|
||||||
)
|
)
|
||||||
if not prototype:
|
if not prototype:
|
||||||
|
|
@ -3559,35 +3716,20 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
key = "<unnamed>"
|
key = "<unnamed>"
|
||||||
if isinstance(prototype, str):
|
if isinstance(prototype, str):
|
||||||
# A prototype key we are looking to apply
|
# A prototype key we are looking to apply
|
||||||
key = prototype
|
prototype_key = prototype
|
||||||
prototypes = protlib.search_prototype(prototype)
|
prototype = self._search_prototype(prototype_key)
|
||||||
nprots = len(prototypes)
|
|
||||||
if not prototypes:
|
|
||||||
caller.msg("No prototype named '%s'." % prototype)
|
|
||||||
return
|
|
||||||
elif nprots > 1:
|
|
||||||
caller.msg(
|
|
||||||
"Found {} prototypes matching '{}':\n {}".format(
|
|
||||||
nprots,
|
|
||||||
prototype,
|
|
||||||
", ".join(proto.get("prototype_key", "") for proto in prototypes),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
# we have a prototype, check access
|
|
||||||
prototype = prototypes[0]
|
|
||||||
if not caller.locks.check_lockstring(
|
|
||||||
caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
|
|
||||||
):
|
|
||||||
caller.msg("You don't have access to use this prototype.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if "noloc" not in self.switches and "location" not in prototype:
|
if not prototype:
|
||||||
prototype["location"] = self.caller.location
|
return
|
||||||
|
|
||||||
# proceed to spawning
|
# proceed to spawning
|
||||||
try:
|
try:
|
||||||
for obj in spawner.spawn(prototype):
|
for obj in spawner.spawn(prototype):
|
||||||
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
||||||
|
if not prototype.get('location') and not noloc:
|
||||||
|
# we don't hardcode the location in the prototype (unless the user
|
||||||
|
# did so manually) - that would lead to it having to be 'removed' every
|
||||||
|
# time we try to update objects with this prototype in the future.
|
||||||
|
obj.location = caller.location
|
||||||
except RuntimeError as err:
|
except RuntimeError as err:
|
||||||
caller.msg(err)
|
caller.msg(err)
|
||||||
|
|
|
||||||
|
|
@ -487,6 +487,8 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||||
locks = "cmd:perm(listscripts) or perm(Admin)"
|
locks = "cmd:perm(listscripts) or perm(Admin)"
|
||||||
help_category = "System"
|
help_category = "System"
|
||||||
|
|
||||||
|
excluded_typeclass_paths = ["evennia.prototypes.prototypes.DbPrototype"]
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""implement method"""
|
"""implement method"""
|
||||||
|
|
||||||
|
|
@ -519,6 +521,8 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||||
if not scripts:
|
if not scripts:
|
||||||
caller.msg("No scripts are running.")
|
caller.msg("No scripts are running.")
|
||||||
return
|
return
|
||||||
|
# filter any found scripts by tag category.
|
||||||
|
scripts = scripts.exclude(db_typeclass_path__in=self.excluded_typeclass_paths)
|
||||||
|
|
||||||
if not scripts:
|
if not scripts:
|
||||||
string = "No scripts found with a key '%s', or on an object named '%s'." % (args, args)
|
string = "No scripts found with a key '%s', or on an object named '%s'." % (args, args)
|
||||||
|
|
|
||||||
|
|
@ -1228,13 +1228,22 @@ class TestBuilding(CommandTest):
|
||||||
inputs=["y"],
|
inputs=["y"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.call(
|
||||||
|
building.CmdSpawn(),
|
||||||
|
"/save testprot2 = {'key':'Test Char', "
|
||||||
|
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||||
|
"(Replacing `prototype_key` in prototype with given key.)|Saved prototype: testprot2",
|
||||||
|
inputs=["y"],
|
||||||
|
)
|
||||||
|
|
||||||
self.call(building.CmdSpawn(), "/search ", "Key ")
|
self.call(building.CmdSpawn(), "/search ", "Key ")
|
||||||
self.call(building.CmdSpawn(), "/search test;test2", "")
|
self.call(building.CmdSpawn(), "/search test;test2", "")
|
||||||
|
|
||||||
self.call(
|
self.call(
|
||||||
building.CmdSpawn(),
|
building.CmdSpawn(),
|
||||||
"/save {'key':'Test Char', " "'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
"/save {'key':'Test Char', " "'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||||
"To save a prototype it must have the 'prototype_key' set.",
|
"A prototype_key must be given, either as `prototype_key = <prototype>` or as "
|
||||||
|
"a key 'prototype_key' inside the prototype structure.",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||||
|
|
@ -1312,7 +1321,7 @@ class TestBuilding(CommandTest):
|
||||||
ball.delete()
|
ball.delete()
|
||||||
|
|
||||||
# test calling spawn with an invalid prototype.
|
# test calling spawn with an invalid prototype.
|
||||||
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'")
|
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST' was found.")
|
||||||
|
|
||||||
# Test listing commands
|
# Test listing commands
|
||||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||||
|
|
@ -1343,13 +1352,12 @@ class TestBuilding(CommandTest):
|
||||||
|
|
||||||
# spawn/edit with invalid prototype
|
# spawn/edit with invalid prototype
|
||||||
msg = self.call(
|
msg = self.call(
|
||||||
building.CmdSpawn(), "/edit NO_EXISTS", "No prototype 'NO_EXISTS' was found."
|
building.CmdSpawn(), "/edit NO_EXISTS", "No prototype named 'NO_EXISTS' was found."
|
||||||
)
|
)
|
||||||
|
|
||||||
# spawn/examine (missing prototype)
|
# spawn/examine (missing prototype)
|
||||||
# lists all prototypes that exist
|
# lists all prototypes that exist
|
||||||
msg = self.call(building.CmdSpawn(), "/examine")
|
self.call(building.CmdSpawn(), "/examine", "You need to specify a prototype-key to show.")
|
||||||
assert "testball" in msg and "testprot" in msg
|
|
||||||
|
|
||||||
# spawn/examine with valid prototype
|
# spawn/examine with valid prototype
|
||||||
# prints the prototype
|
# prints the prototype
|
||||||
|
|
@ -1358,7 +1366,7 @@ class TestBuilding(CommandTest):
|
||||||
|
|
||||||
# spawn/examine with invalid prototype
|
# spawn/examine with invalid prototype
|
||||||
# shows error
|
# shows error
|
||||||
self.call(building.CmdSpawn(), "/examine NO_EXISTS", "No prototype 'NO_EXISTS' was found.")
|
self.call(building.CmdSpawn(), "/examine NO_EXISTS", "No prototype named 'NO_EXISTS' was found.")
|
||||||
|
|
||||||
|
|
||||||
class TestComms(CommandTest):
|
class TestComms(CommandTest):
|
||||||
|
|
|
||||||
|
|
@ -1488,7 +1488,7 @@ def node_tags(caller):
|
||||||
as the |cprototype_key|n and with a category "{tag_category}". This allows the spawner to
|
as the |cprototype_key|n and with a category "{tag_category}". This allows the spawner to
|
||||||
optionally update previously spawned objects when their prototype changes.
|
optionally update previously spawned objects when their prototype changes.
|
||||||
""".format(
|
""".format(
|
||||||
tag_category=protlib._PROTOTYPE_TAG_CATEGORY
|
tag_category=protlib.PROTOTYPE_TAG_CATEGORY
|
||||||
)
|
)
|
||||||
|
|
||||||
text = (text, helptext)
|
text = (text, helptext)
|
||||||
|
|
@ -2131,12 +2131,13 @@ def _keep_diff(caller, **kwargs):
|
||||||
tmp[path[-1]] = tuple(list(tmp[path[-1]][:-1]) + ["KEEP"])
|
tmp[path[-1]] = tuple(list(tmp[path[-1]][:-1]) + ["KEEP"])
|
||||||
|
|
||||||
|
|
||||||
def _format_diff_text_and_options(diff, **kwargs):
|
def _format_diff_text_and_options(diff, minimal=True, **kwargs):
|
||||||
"""
|
"""
|
||||||
Reformat the diff in a way suitable for the olc menu.
|
Reformat the diff in a way suitable for the olc menu.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
diff (dict): A diff as produced by `prototype_diff`.
|
diff (dict): A diff as produced by `prototype_diff`.
|
||||||
|
minimal (bool, optional): Don't show KEEPs.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
any (any): Forwarded into the generated options as arguments to the callable.
|
any (any): Forwarded into the generated options as arguments to the callable.
|
||||||
|
|
@ -2150,12 +2151,15 @@ def _format_diff_text_and_options(diff, **kwargs):
|
||||||
|
|
||||||
def _visualize(obj, rootname, get_name=False):
|
def _visualize(obj, rootname, get_name=False):
|
||||||
if utils.is_iter(obj):
|
if utils.is_iter(obj):
|
||||||
|
if not obj:
|
||||||
|
return str(obj)
|
||||||
if get_name:
|
if get_name:
|
||||||
return obj[0] if obj[0] else "<unset>"
|
return obj[0] if obj[0] else "<unset>"
|
||||||
if rootname == "attrs":
|
if rootname == "attrs":
|
||||||
return "{} |W=|n {} |W(category:|n {}|W, locks:|n {}|W)|n".format(*obj)
|
return "{} |W=|n {} |W(category:|n {}|W, locks:|n {}|W)|n".format(*obj)
|
||||||
elif rootname == "tags":
|
elif rootname == "tags":
|
||||||
return "{} |W(category:|n {}|W)|n".format(obj[0], obj[1])
|
return "{} |W(category:|n {}|W)|n".format(obj[0], obj[1])
|
||||||
|
|
||||||
return "{}".format(obj)
|
return "{}".format(obj)
|
||||||
|
|
||||||
def _parse_diffpart(diffpart, optnum, *args):
|
def _parse_diffpart(diffpart, optnum, *args):
|
||||||
|
|
@ -2166,17 +2170,33 @@ def _format_diff_text_and_options(diff, **kwargs):
|
||||||
rootname = args[0]
|
rootname = args[0]
|
||||||
old, new, instruction = diffpart
|
old, new, instruction = diffpart
|
||||||
if instruction == "KEEP":
|
if instruction == "KEEP":
|
||||||
texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old, rootname)))
|
if not minimal:
|
||||||
|
texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old, rootname)))
|
||||||
else:
|
else:
|
||||||
|
# instructions we should be able to revert by a menu choice
|
||||||
vold = _visualize(old, rootname)
|
vold = _visualize(old, rootname)
|
||||||
vnew = _visualize(new, rootname)
|
vnew = _visualize(new, rootname)
|
||||||
vsep = "" if len(vold) < 78 else "\n"
|
vsep = "" if len(vold) < 78 else "\n"
|
||||||
vinst = "|rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction)
|
|
||||||
texts.append(
|
if instruction == "ADD":
|
||||||
" |c[{num}] {inst}|W:|n {old} |W->|n{sep} {new}".format(
|
texts.append(" |c[{optnum}] |yADD|n: {new}".format(
|
||||||
inst=vinst, num=optnum, old=vold, sep=vsep, new=vnew
|
optnum=optnum, new=_visualize(new, rootname)))
|
||||||
|
elif instruction == "REMOVE" and not new:
|
||||||
|
if rootname == "tags" and old[1] == protlib.PROTOTYPE_TAG_CATEGORY:
|
||||||
|
# special exception for the prototype-tag mechanism
|
||||||
|
# this is added post-spawn automatically and should
|
||||||
|
# not be listed as REMOVE.
|
||||||
|
return texts, options, optnum
|
||||||
|
|
||||||
|
texts.append(" |c[{optnum}] |rREMOVE|n: {old}".format(
|
||||||
|
optnum=optnum, old=_visualize(old, rootname)))
|
||||||
|
else:
|
||||||
|
vinst = "|y{}|n".format(instruction)
|
||||||
|
texts.append(
|
||||||
|
" |c[{num}] {inst}|W:|n {old} |W->|n{sep} {new}".format(
|
||||||
|
inst=vinst, num=optnum, old=vold, sep=vsep, new=vnew
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
options.append(
|
options.append(
|
||||||
{
|
{
|
||||||
"key": str(optnum),
|
"key": str(optnum),
|
||||||
|
|
@ -2203,11 +2223,8 @@ def _format_diff_text_and_options(diff, **kwargs):
|
||||||
for root_key in sorted(diff):
|
for root_key in sorted(diff):
|
||||||
diffpart = diff[root_key]
|
diffpart = diff[root_key]
|
||||||
text, option, optnum = _parse_diffpart(diffpart, optnum, root_key)
|
text, option, optnum = _parse_diffpart(diffpart, optnum, root_key)
|
||||||
|
|
||||||
heading = "- |w{}:|n ".format(root_key)
|
heading = "- |w{}:|n ".format(root_key)
|
||||||
if root_key in ("attrs", "tags", "permissions"):
|
if text:
|
||||||
texts.append(heading)
|
|
||||||
elif text:
|
|
||||||
text = [heading + text[0]] + text[1:]
|
text = [heading + text[0]] + text[1:]
|
||||||
else:
|
else:
|
||||||
text = [heading]
|
text = [heading]
|
||||||
|
|
@ -2277,7 +2294,8 @@ def node_apply_diff(caller, **kwargs):
|
||||||
if not custom_location:
|
if not custom_location:
|
||||||
diff.pop("location", None)
|
diff.pop("location", None)
|
||||||
|
|
||||||
txt, options = _format_diff_text_and_options(diff, objects=update_objects, base_obj=base_obj)
|
txt, options = _format_diff_text_and_options(diff, objects=update_objects,
|
||||||
|
base_obj=base_obj, prototype=prototype)
|
||||||
|
|
||||||
if options:
|
if options:
|
||||||
text = [
|
text = [
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,12 @@ _PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + (
|
||||||
"tags",
|
"tags",
|
||||||
"attrs",
|
"attrs",
|
||||||
)
|
)
|
||||||
_PROTOTYPE_TAG_CATEGORY = "from_prototype"
|
PROTOTYPE_TAG_CATEGORY = "from_prototype"
|
||||||
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
|
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
|
||||||
PROT_FUNCS = {}
|
PROT_FUNCS = {}
|
||||||
|
|
||||||
|
_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:all()"
|
||||||
|
|
||||||
|
|
||||||
class PermissionError(RuntimeError):
|
class PermissionError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
@ -84,8 +86,19 @@ def homogenize_prototype(prototype, custom_keys=None):
|
||||||
homogenizations like adding missing prototype_keys and setting a default typeclass.
|
homogenizations like adding missing prototype_keys and setting a default typeclass.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not prototype or not isinstance(prototype, dict):
|
||||||
|
return {}
|
||||||
|
|
||||||
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
|
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
|
||||||
|
|
||||||
|
# correct cases of setting None for certain values
|
||||||
|
for protkey in prototype:
|
||||||
|
if prototype[protkey] is None:
|
||||||
|
if protkey in ("attrs", "tags", "prototype_tags"):
|
||||||
|
prototype[protkey] = []
|
||||||
|
elif protkey in ("prototype_key", "prototype_desc"):
|
||||||
|
prototype[protkey] = ""
|
||||||
|
|
||||||
attrs = list(prototype.get("attrs", [])) # break reference
|
attrs = list(prototype.get("attrs", [])) # break reference
|
||||||
tags = make_iter(prototype.get("tags", []))
|
tags = make_iter(prototype.get("tags", []))
|
||||||
homogenized_tags = []
|
homogenized_tags = []
|
||||||
|
|
@ -111,12 +124,14 @@ def homogenize_prototype(prototype, custom_keys=None):
|
||||||
|
|
||||||
# add required missing parts that had defaults before
|
# add required missing parts that had defaults before
|
||||||
|
|
||||||
if "prototype_key" not in prototype:
|
homogenized["prototype_key"] = homogenized.get("prototype_key",
|
||||||
# assign a random hash as key
|
# assign a random hash as key
|
||||||
homogenized["prototype_key"] = "prototype-{}".format(
|
"prototype-{}".format(
|
||||||
hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7]
|
hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7]))
|
||||||
)
|
homogenized["prototype_tags"] = homogenized.get("prototype_tags", [])
|
||||||
|
homogenized["prototype_locks"] = homogenized.get(
|
||||||
|
"prototype_lock", _PROTOTYPE_FALLBACK_LOCK)
|
||||||
|
homogenized["prototype_desc"] = homogenized.get("prototype_desc", "")
|
||||||
if "typeclass" not in prototype and "prototype_parent" not in prototype:
|
if "typeclass" not in prototype and "prototype_parent" not in prototype:
|
||||||
homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS
|
homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS
|
||||||
|
|
||||||
|
|
@ -223,14 +238,11 @@ def save_prototype(prototype):
|
||||||
)
|
)
|
||||||
|
|
||||||
# make sure meta properties are included with defaults
|
# make sure meta properties are included with defaults
|
||||||
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
|
|
||||||
prototype = stored_prototype[0].prototype if stored_prototype else {}
|
|
||||||
|
|
||||||
in_prototype["prototype_desc"] = in_prototype.get(
|
in_prototype["prototype_desc"] = in_prototype.get(
|
||||||
"prototype_desc", prototype.get("prototype_desc", "")
|
"prototype_desc", prototype.get("prototype_desc", "")
|
||||||
)
|
)
|
||||||
prototype_locks = in_prototype.get(
|
prototype_locks = in_prototype.get(
|
||||||
"prototype_locks", prototype.get("prototype_locks", "spawn:all();edit:perm(Admin)")
|
"prototype_locks", prototype.get("prototype_locks", _PROTOTYPE_FALLBACK_LOCK)
|
||||||
)
|
)
|
||||||
is_valid, err = validate_lockstring(prototype_locks)
|
is_valid, err = validate_lockstring(prototype_locks)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
|
|
@ -245,27 +257,26 @@ def save_prototype(prototype):
|
||||||
]
|
]
|
||||||
in_prototype["prototype_tags"] = prototype_tags
|
in_prototype["prototype_tags"] = prototype_tags
|
||||||
|
|
||||||
prototype.update(in_prototype)
|
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
|
||||||
|
|
||||||
if stored_prototype:
|
if stored_prototype:
|
||||||
# edit existing prototype
|
# edit existing prototype
|
||||||
stored_prototype = stored_prototype[0]
|
stored_prototype = stored_prototype[0]
|
||||||
stored_prototype.desc = prototype["prototype_desc"]
|
stored_prototype.desc = in_prototype["prototype_desc"]
|
||||||
if prototype_tags:
|
if prototype_tags:
|
||||||
stored_prototype.tags.clear(category=_PROTOTYPE_TAG_CATEGORY)
|
stored_prototype.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
|
||||||
stored_prototype.tags.batch_add(*prototype["prototype_tags"])
|
stored_prototype.tags.batch_add(*in_prototype["prototype_tags"])
|
||||||
stored_prototype.locks.add(prototype["prototype_locks"])
|
stored_prototype.locks.add(in_prototype["prototype_locks"])
|
||||||
stored_prototype.attributes.add("prototype", prototype)
|
stored_prototype.attributes.add("prototype", in_prototype)
|
||||||
else:
|
else:
|
||||||
# create a new prototype
|
# create a new prototype
|
||||||
stored_prototype = create_script(
|
stored_prototype = create_script(
|
||||||
DbPrototype,
|
DbPrototype,
|
||||||
key=prototype_key,
|
key=prototype_key,
|
||||||
desc=prototype["prototype_desc"],
|
desc=in_prototype["prototype_desc"],
|
||||||
persistent=True,
|
persistent=True,
|
||||||
locks=prototype_locks,
|
locks=prototype_locks,
|
||||||
tags=prototype["prototype_tags"],
|
tags=in_prototype["prototype_tags"],
|
||||||
attributes=[("prototype", prototype)],
|
attributes=[("prototype", in_prototype)],
|
||||||
)
|
)
|
||||||
return stored_prototype.prototype
|
return stored_prototype.prototype
|
||||||
|
|
||||||
|
|
@ -410,7 +421,7 @@ def search_objects_with_prototype(prototype_key):
|
||||||
matches (Queryset): All matching objects spawned from this prototype.
|
matches (Queryset): All matching objects spawned from this prototype.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
|
return ObjectDB.objects.get_by_tag(key=prototype_key, category=PROTOTYPE_TAG_CATEGORY)
|
||||||
|
|
||||||
|
|
||||||
def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True):
|
def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True):
|
||||||
|
|
@ -713,15 +724,15 @@ def prototype_to_str(prototype):
|
||||||
prototype_desc=prototype.get("prototype_desc", "|wNone|n"),
|
prototype_desc=prototype.get("prototype_desc", "|wNone|n"),
|
||||||
prototype_parent=prototype.get("prototype_parent", "|wNone|n"),
|
prototype_parent=prototype.get("prototype_parent", "|wNone|n"),
|
||||||
)
|
)
|
||||||
|
key = aliases = attrs = tags = locks = permissions = location = home = destination = ""
|
||||||
key = prototype.get("key", "")
|
if "key" in prototype:
|
||||||
if key:
|
key = prototype["key"]
|
||||||
key = "|ckey:|n {key}".format(key=key)
|
key = "|ckey:|n {key}".format(key=key)
|
||||||
aliases = prototype.get("aliases", "")
|
if "aliases" in prototype:
|
||||||
if aliases:
|
aliases = prototype["aliases"]
|
||||||
aliases = "|caliases:|n {aliases}".format(aliases=", ".join(aliases))
|
aliases = "|caliases:|n {aliases}".format(aliases=", ".join(aliases))
|
||||||
attrs = prototype.get("attrs", "")
|
if "attrs" in prototype:
|
||||||
if attrs:
|
attrs = prototype["attrs"]
|
||||||
out = []
|
out = []
|
||||||
for (attrkey, value, category, locks) in attrs:
|
for (attrkey, value, category, locks) in attrs:
|
||||||
locks = ", ".join(lock for lock in locks if lock)
|
locks = ", ".join(lock for lock in locks if lock)
|
||||||
|
|
@ -740,8 +751,8 @@ def prototype_to_str(prototype):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
|
attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
|
||||||
tags = prototype.get("tags", "")
|
if "tags" in prototype:
|
||||||
if tags:
|
tags = prototype['tags']
|
||||||
out = []
|
out = []
|
||||||
for (tagkey, category, data) in tags:
|
for (tagkey, category, data) in tags:
|
||||||
out.append(
|
out.append(
|
||||||
|
|
@ -750,20 +761,20 @@ def prototype_to_str(prototype):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
|
tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
|
||||||
locks = prototype.get("locks", "")
|
if "locks" in prototype:
|
||||||
if locks:
|
locks = prototype["locks"]
|
||||||
locks = "|clocks:|n\n {locks}".format(locks=locks)
|
locks = "|clocks:|n\n {locks}".format(locks=locks)
|
||||||
permissions = prototype.get("permissions", "")
|
if "permissions" in prototype:
|
||||||
if permissions:
|
permissions = prototype["permissions"]
|
||||||
permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))
|
permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))
|
||||||
location = prototype.get("location", "")
|
if "location" in prototype:
|
||||||
if location:
|
location = prototype["location"]
|
||||||
location = "|clocation:|n {location}".format(location=location)
|
location = "|clocation:|n {location}".format(location=location)
|
||||||
home = prototype.get("home", "")
|
if "home" in prototype:
|
||||||
if home:
|
home = prototype["home"]
|
||||||
home = "|chome:|n {home}".format(home=home)
|
home = "|chome:|n {home}".format(home=home)
|
||||||
destination = prototype.get("destination", "")
|
if "destination" in prototype:
|
||||||
if destination:
|
destination = prototype["destination"]
|
||||||
destination = "|cdestination:|n {destination}".format(destination=destination)
|
destination = "|cdestination:|n {destination}".format(destination=destination)
|
||||||
|
|
||||||
body = "\n".join(
|
body = "\n".join(
|
||||||
|
|
|
||||||
|
|
@ -138,13 +138,14 @@ from django.conf import settings
|
||||||
|
|
||||||
import evennia
|
import evennia
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
|
from evennia.utils import logger
|
||||||
from evennia.utils.utils import make_iter, is_iter
|
from evennia.utils.utils import make_iter, is_iter
|
||||||
from evennia.prototypes import prototypes as protlib
|
from evennia.prototypes import prototypes as protlib
|
||||||
from evennia.prototypes.prototypes import (
|
from evennia.prototypes.prototypes import (
|
||||||
value_to_obj,
|
value_to_obj,
|
||||||
value_to_obj_or_any,
|
value_to_obj_or_any,
|
||||||
init_spawn_value,
|
init_spawn_value,
|
||||||
_PROTOTYPE_TAG_CATEGORY,
|
PROTOTYPE_TAG_CATEGORY,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -165,6 +166,18 @@ _PROTOTYPE_ROOT_NAMES = (
|
||||||
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES
|
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES
|
||||||
|
|
||||||
|
|
||||||
|
class Unset:
|
||||||
|
"""
|
||||||
|
Helper class representing a non-set diff element.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __bool__(self):
|
||||||
|
return False
|
||||||
|
def __str__(self):
|
||||||
|
return "<Unset>"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Helper
|
# Helper
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -268,7 +281,7 @@ def prototype_from_object(obj):
|
||||||
"""
|
"""
|
||||||
# first, check if this object already has a prototype
|
# first, check if this object already has a prototype
|
||||||
|
|
||||||
prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True)
|
prot = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True)
|
||||||
if prot:
|
if prot:
|
||||||
prot = protlib.search_prototype(prot[0])
|
prot = protlib.search_prototype(prot[0])
|
||||||
|
|
||||||
|
|
@ -322,9 +335,9 @@ def prototype_from_object(obj):
|
||||||
return prot
|
return prot
|
||||||
|
|
||||||
|
|
||||||
def prototype_diff(prototype1, prototype2, maxdepth=2):
|
def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implicit_keep=False):
|
||||||
"""
|
"""
|
||||||
A 'detailed' diff specifies differences down to individual sub-sectiions
|
A 'detailed' diff specifies differences down to individual sub-sections
|
||||||
of the prototype, like individual attributes, permissions etc. It is used
|
of the prototype, like individual attributes, permissions etc. It is used
|
||||||
by the menu to allow a user to customize what should be kept.
|
by the menu to allow a user to customize what should be kept.
|
||||||
|
|
||||||
|
|
@ -334,6 +347,12 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
||||||
maxdepth (int, optional): The maximum depth into the diff we go before treating the elements
|
maxdepth (int, optional): The maximum depth into the diff we go before treating the elements
|
||||||
of iterables as individual entities to compare. This is important since a single
|
of iterables as individual entities to compare. This is important since a single
|
||||||
attr/tag (for example) are represented by a tuple.
|
attr/tag (for example) are represented by a tuple.
|
||||||
|
homogenize (bool, optional): Auto-homogenize both prototypes for the best comparison.
|
||||||
|
This is most useful for displaying.
|
||||||
|
implicit_keep (bool, optional): If set, the resulting diff will assume KEEP unless the new
|
||||||
|
prototype explicitly change them. That is, if a key exists in `prototype1` and
|
||||||
|
not in `prototype2`, it will not be REMOVEd but set to KEEP instead. This is particularly
|
||||||
|
useful for auto-generated prototypes when updating objects.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
diff (dict): A structure detailing how to convert prototype1 to prototype2. All
|
diff (dict): A structure detailing how to convert prototype1 to prototype2. All
|
||||||
|
|
@ -344,12 +363,16 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
||||||
instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP".
|
instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP".
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
_unset = Unset()
|
||||||
|
|
||||||
def _recursive_diff(old, new, depth=0):
|
def _recursive_diff(old, new, depth=0):
|
||||||
|
|
||||||
old_type = type(old)
|
old_type = type(old)
|
||||||
new_type = type(new)
|
new_type = type(new)
|
||||||
|
|
||||||
|
if old_type == new_type and not (old or new):
|
||||||
|
# both old and new are unset, like [] or None
|
||||||
|
return (None, None, "KEEP")
|
||||||
if old_type != new_type:
|
if old_type != new_type:
|
||||||
if old and not new:
|
if old and not new:
|
||||||
if depth < maxdepth and old_type == dict:
|
if depth < maxdepth and old_type == dict:
|
||||||
|
|
@ -358,6 +381,9 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
||||||
return {
|
return {
|
||||||
part[0] if is_iter(part) else part: (part, None, "REMOVE") for part in old
|
part[0] if is_iter(part) else part: (part, None, "REMOVE") for part in old
|
||||||
}
|
}
|
||||||
|
if isinstance(new, Unset) and implicit_keep:
|
||||||
|
# the new does not define any change, use implicit-keep
|
||||||
|
return (old, None, "KEEP")
|
||||||
return (old, new, "REMOVE")
|
return (old, new, "REMOVE")
|
||||||
elif not old and new:
|
elif not old and new:
|
||||||
if depth < maxdepth and new_type == dict:
|
if depth < maxdepth and new_type == dict:
|
||||||
|
|
@ -371,7 +397,7 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
||||||
elif depth < maxdepth and new_type == dict:
|
elif depth < maxdepth and new_type == dict:
|
||||||
all_keys = set(list(old.keys()) + list(new.keys()))
|
all_keys = set(list(old.keys()) + list(new.keys()))
|
||||||
return {
|
return {
|
||||||
key: _recursive_diff(old.get(key), new.get(key), depth=depth + 1)
|
key: _recursive_diff(old.get(key, _unset), new.get(key, _unset), depth=depth + 1)
|
||||||
for key in all_keys
|
for key in all_keys
|
||||||
}
|
}
|
||||||
elif depth < maxdepth and is_iter(new):
|
elif depth < maxdepth and is_iter(new):
|
||||||
|
|
@ -379,7 +405,7 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
||||||
new_map = {part[0] if is_iter(part) else part: part for part in new}
|
new_map = {part[0] if is_iter(part) else part: part for part in new}
|
||||||
all_keys = set(list(old_map.keys()) + list(new_map.keys()))
|
all_keys = set(list(old_map.keys()) + list(new_map.keys()))
|
||||||
return {
|
return {
|
||||||
key: _recursive_diff(old_map.get(key), new_map.get(key), depth=depth + 1)
|
key: _recursive_diff(old_map.get(key, _unset), new_map.get(key, _unset), depth=depth + 1)
|
||||||
for key in all_keys
|
for key in all_keys
|
||||||
}
|
}
|
||||||
elif old != new:
|
elif old != new:
|
||||||
|
|
@ -387,7 +413,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
||||||
else:
|
else:
|
||||||
return (old, new, "KEEP")
|
return (old, new, "KEEP")
|
||||||
|
|
||||||
diff = _recursive_diff(prototype1, prototype2)
|
prot1 = protlib.homogenize_prototype(prototype1) if homogenize else prototype1
|
||||||
|
prot2 = protlib.homogenize_prototype(prototype2) if homogenize else prototype2
|
||||||
|
|
||||||
|
diff = _recursive_diff(prot1, prot2)
|
||||||
|
|
||||||
return diff
|
return diff
|
||||||
|
|
||||||
|
|
@ -460,7 +489,7 @@ def flatten_diff(diff):
|
||||||
return flat_diff
|
return flat_diff
|
||||||
|
|
||||||
|
|
||||||
def prototype_diff_from_object(prototype, obj):
|
def prototype_diff_from_object(prototype, obj, implicit_keep=True):
|
||||||
"""
|
"""
|
||||||
Get a simple diff for a prototype compared to an object which may or may not already have a
|
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
|
prototype (or has one but changed locally). For more complex migratations a manual diff may be
|
||||||
|
|
@ -474,6 +503,11 @@ def prototype_diff_from_object(prototype, obj):
|
||||||
diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
|
diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
|
||||||
obj_prototype (dict): The prototype calculated for the given object. The diff is how to
|
obj_prototype (dict): The prototype calculated for the given object. The diff is how to
|
||||||
convert this prototype into the new prototype.
|
convert this prototype into the new prototype.
|
||||||
|
implicit_keep (bool, optional): This is usually what one wants for object updating. When
|
||||||
|
set, this means the prototype diff will assume KEEP on differences
|
||||||
|
between the object-generated prototype and that which is not explicitly set in the
|
||||||
|
new prototype. This means e.g. that even though the object has a location, and the
|
||||||
|
prototype does not specify the location, it will not be unset.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
The `diff` is on the following form:
|
The `diff` is on the following form:
|
||||||
|
|
@ -486,11 +520,87 @@ def prototype_diff_from_object(prototype, obj):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
obj_prototype = prototype_from_object(obj)
|
obj_prototype = prototype_from_object(obj)
|
||||||
diff = prototype_diff(obj_prototype, protlib.homogenize_prototype(prototype))
|
diff = prototype_diff(obj_prototype, protlib.homogenize_prototype(prototype),
|
||||||
|
implicit_keep=implicit_keep)
|
||||||
return diff, obj_prototype
|
return diff, obj_prototype
|
||||||
|
|
||||||
|
|
||||||
def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
def format_diff(diff, minimal=True):
|
||||||
|
"""
|
||||||
|
Reformat a diff for presentation. This is a shortened version
|
||||||
|
of the olc _format_diff_text_and_options without the options.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diff (dict): A diff as produced by `prototype_diff`.
|
||||||
|
minimal (bool, optional): Only show changes (remove KEEPs)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
texts (str): The formatted text.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
valid_instructions = ("KEEP", "REMOVE", "ADD", "UPDATE")
|
||||||
|
|
||||||
|
def _visualize(obj, rootname, get_name=False):
|
||||||
|
if is_iter(obj):
|
||||||
|
if not obj:
|
||||||
|
return str(obj)
|
||||||
|
if get_name:
|
||||||
|
return obj[0] if obj[0] else "<unset>"
|
||||||
|
if rootname == "attrs":
|
||||||
|
return "{} |w=|n {} |w(category:|n |n{}|w, locks:|n {}|w)|n".format(*obj)
|
||||||
|
elif rootname == "tags":
|
||||||
|
return "{} |w(category:|n {}|w)|n".format(obj[0], obj[1])
|
||||||
|
return "{}".format(obj)
|
||||||
|
|
||||||
|
def _parse_diffpart(diffpart, rootname):
|
||||||
|
typ = type(diffpart)
|
||||||
|
texts = []
|
||||||
|
if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
|
||||||
|
old, new, instruction = diffpart
|
||||||
|
if instruction == "KEEP":
|
||||||
|
if not minimal:
|
||||||
|
texts.append(" |gKEEP|n: {old}".format(old=_visualize(old, rootname)))
|
||||||
|
elif instruction == "ADD":
|
||||||
|
texts.append(" |yADD|n: {new}".format(new=_visualize(new, rootname)))
|
||||||
|
elif instruction == "REMOVE" and not new:
|
||||||
|
texts.append(" |rREMOVE|n: {old}".format(old=_visualize(old, rootname)))
|
||||||
|
else:
|
||||||
|
vold = _visualize(old, rootname)
|
||||||
|
vnew = _visualize(new, rootname)
|
||||||
|
vsep = "" if len(vold) < 78 else "\n"
|
||||||
|
vinst = " |rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction)
|
||||||
|
varrow = "|r->|n" if instruction == "REMOVE" else "|y->|n"
|
||||||
|
texts.append(
|
||||||
|
" {inst}|W:|n {old} |W{varrow}|n{sep} {new}".format(
|
||||||
|
inst=vinst, old=vold, varrow=varrow, sep=vsep, new=vnew
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for key in sorted(list(diffpart.keys())):
|
||||||
|
subdiffpart = diffpart[key]
|
||||||
|
text = _parse_diffpart(subdiffpart, rootname)
|
||||||
|
texts.extend(text)
|
||||||
|
return texts
|
||||||
|
|
||||||
|
texts = []
|
||||||
|
|
||||||
|
for root_key in sorted(diff):
|
||||||
|
diffpart = diff[root_key]
|
||||||
|
text = _parse_diffpart(diffpart, root_key)
|
||||||
|
if text or not minimal:
|
||||||
|
heading = "- |w{}:|n\n".format(root_key)
|
||||||
|
if text:
|
||||||
|
text = [heading + text[0]] + text[1:]
|
||||||
|
else:
|
||||||
|
text = [heading]
|
||||||
|
|
||||||
|
texts.extend(text)
|
||||||
|
|
||||||
|
return "\n ".join(line for line in texts if line)
|
||||||
|
|
||||||
|
|
||||||
|
def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exact=False):
|
||||||
"""
|
"""
|
||||||
Update existing objects with the latest version of the prototype.
|
Update existing objects with the latest version of the prototype.
|
||||||
|
|
||||||
|
|
@ -501,6 +611,12 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
||||||
If not given this will be constructed from the first object found.
|
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 (list, optional): List of objects to update. If not given, query for these
|
||||||
objects using the prototype's `prototype_key`.
|
objects using the prototype's `prototype_key`.
|
||||||
|
exact (bool, optional): By default (`False`), keys not explicitly in the prototype will
|
||||||
|
not be applied to the object, but will be retained as-is. This is usually what is
|
||||||
|
expected - for example, one usually do not want to remove the object's location even
|
||||||
|
if it's not set in the prototype. With `exact=True`, all un-specified properties of the
|
||||||
|
objects will be removed if they exist. This will lead to a more accurate 1:1 correlation
|
||||||
|
between the object and the prototype but is usually impractical.
|
||||||
Returns:
|
Returns:
|
||||||
changed (int): The number of objects that had changes applied to them.
|
changed (int): The number of objects that had changes applied to them.
|
||||||
|
|
||||||
|
|
@ -515,7 +631,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
||||||
prototype_key = new_prototype["prototype_key"]
|
prototype_key = new_prototype["prototype_key"]
|
||||||
|
|
||||||
if not objects:
|
if not objects:
|
||||||
objects = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
|
objects = ObjectDB.objects.get_by_tag(prototype_key, category=PROTOTYPE_TAG_CATEGORY)
|
||||||
|
|
||||||
if not objects:
|
if not objects:
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -525,104 +641,117 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
||||||
|
|
||||||
# make sure the diff is flattened
|
# make sure the diff is flattened
|
||||||
diff = flatten_diff(diff)
|
diff = flatten_diff(diff)
|
||||||
|
|
||||||
changed = 0
|
changed = 0
|
||||||
for obj in objects:
|
for obj in objects:
|
||||||
do_save = False
|
do_save = False
|
||||||
|
|
||||||
old_prot_key = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True)
|
old_prot_key = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True)
|
||||||
old_prot_key = old_prot_key[0] if old_prot_key else None
|
old_prot_key = old_prot_key[0] if old_prot_key else None
|
||||||
if prototype_key != old_prot_key:
|
|
||||||
obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY)
|
|
||||||
obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
|
|
||||||
|
|
||||||
for key, directive in diff.items():
|
try:
|
||||||
if directive in ("UPDATE", "REPLACE"):
|
for key, directive in diff.items():
|
||||||
|
|
||||||
if key in _PROTOTYPE_META_NAMES:
|
if key not in new_prototype and not exact:
|
||||||
# prototype meta keys are not stored on-object
|
# we don't update the object if the prototype does not actually
|
||||||
|
# contain the key (the diff will report REMOVE but we ignore it
|
||||||
|
# since exact=False)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
val = new_prototype[key]
|
if directive in ("UPDATE", "REPLACE"):
|
||||||
do_save = True
|
|
||||||
|
|
||||||
if key == "key":
|
if key in _PROTOTYPE_META_NAMES:
|
||||||
obj.db_key = init_spawn_value(val, str)
|
# prototype meta keys are not stored on-object
|
||||||
elif key == "typeclass":
|
continue
|
||||||
obj.db_typeclass_path = init_spawn_value(val, str)
|
|
||||||
elif key == "location":
|
val = new_prototype[key]
|
||||||
obj.db_location = init_spawn_value(val, value_to_obj)
|
do_save = True
|
||||||
elif key == "home":
|
|
||||||
obj.db_home = init_spawn_value(val, value_to_obj)
|
if key == "key":
|
||||||
elif key == "destination":
|
obj.db_key = init_spawn_value(val, str)
|
||||||
obj.db_destination = init_spawn_value(val, value_to_obj)
|
elif key == "typeclass":
|
||||||
elif key == "locks":
|
obj.db_typeclass_path = init_spawn_value(val, str)
|
||||||
if directive == "REPLACE":
|
elif key == "location":
|
||||||
obj.locks.clear()
|
obj.db_location = init_spawn_value(val, value_to_obj)
|
||||||
obj.locks.add(init_spawn_value(val, str))
|
elif key == "home":
|
||||||
elif key == "permissions":
|
obj.db_home = init_spawn_value(val, value_to_obj)
|
||||||
if directive == "REPLACE":
|
elif key == "destination":
|
||||||
obj.permissions.clear()
|
obj.db_destination = init_spawn_value(val, value_to_obj)
|
||||||
obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val))
|
elif key == "locks":
|
||||||
elif key == "aliases":
|
if directive == "REPLACE":
|
||||||
if directive == "REPLACE":
|
obj.locks.clear()
|
||||||
obj.aliases.clear()
|
obj.locks.add(init_spawn_value(val, str))
|
||||||
obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val))
|
elif key == "permissions":
|
||||||
elif key == "tags":
|
if directive == "REPLACE":
|
||||||
if directive == "REPLACE":
|
obj.permissions.clear()
|
||||||
obj.tags.clear()
|
obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val))
|
||||||
obj.tags.batch_add(
|
elif key == "aliases":
|
||||||
*(
|
if directive == "REPLACE":
|
||||||
(init_spawn_value(ttag, str), tcategory, tdata)
|
obj.aliases.clear()
|
||||||
for ttag, tcategory, tdata in val
|
obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val))
|
||||||
)
|
elif key == "tags":
|
||||||
)
|
if directive == "REPLACE":
|
||||||
elif key == "attrs":
|
obj.tags.clear()
|
||||||
if directive == "REPLACE":
|
obj.tags.batch_add(
|
||||||
obj.attributes.clear()
|
*(
|
||||||
obj.attributes.batch_add(
|
(init_spawn_value(ttag, str), tcategory, tdata)
|
||||||
*(
|
for ttag, tcategory, tdata in val
|
||||||
(
|
|
||||||
init_spawn_value(akey, str),
|
|
||||||
init_spawn_value(aval, value_to_obj),
|
|
||||||
acategory,
|
|
||||||
alocks,
|
|
||||||
)
|
)
|
||||||
for akey, aval, acategory, alocks in val
|
|
||||||
)
|
)
|
||||||
)
|
elif key == "attrs":
|
||||||
elif key == "exec":
|
if directive == "REPLACE":
|
||||||
# we don't auto-rerun exec statements, it would be huge security risk!
|
obj.attributes.clear()
|
||||||
pass
|
obj.attributes.batch_add(
|
||||||
else:
|
*(
|
||||||
obj.attributes.add(key, init_spawn_value(val, value_to_obj))
|
(
|
||||||
elif directive == "REMOVE":
|
init_spawn_value(akey, str),
|
||||||
do_save = True
|
init_spawn_value(aval, value_to_obj),
|
||||||
if key == "key":
|
acategory,
|
||||||
obj.db_key = ""
|
alocks,
|
||||||
elif key == "typeclass":
|
)
|
||||||
# fall back to default
|
for akey, aval, acategory, alocks in val
|
||||||
obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS
|
)
|
||||||
elif key == "location":
|
)
|
||||||
obj.db_location = None
|
elif key == "exec":
|
||||||
elif key == "home":
|
# we don't auto-rerun exec statements, it would be huge security risk!
|
||||||
obj.db_home = None
|
pass
|
||||||
elif key == "destination":
|
else:
|
||||||
obj.db_destination = None
|
obj.attributes.add(key, init_spawn_value(val, value_to_obj))
|
||||||
elif key == "locks":
|
elif directive == "REMOVE":
|
||||||
obj.locks.clear()
|
do_save = True
|
||||||
elif key == "permissions":
|
if key == "key":
|
||||||
obj.permissions.clear()
|
obj.db_key = ""
|
||||||
elif key == "aliases":
|
elif key == "typeclass":
|
||||||
obj.aliases.clear()
|
# fall back to default
|
||||||
elif key == "tags":
|
obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS
|
||||||
obj.tags.clear()
|
elif key == "location":
|
||||||
elif key == "attrs":
|
obj.db_location = None
|
||||||
obj.attributes.clear()
|
elif key == "home":
|
||||||
elif key == "exec":
|
obj.db_home = None
|
||||||
# we don't auto-rerun exec statements, it would be huge security risk!
|
elif key == "destination":
|
||||||
pass
|
obj.db_destination = None
|
||||||
else:
|
elif key == "locks":
|
||||||
obj.attributes.remove(key)
|
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)
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace(f"Failed to apply prototype '{prototype_key}' to {obj}.")
|
||||||
|
finally:
|
||||||
|
# we must always make sure to re-add the prototype tag
|
||||||
|
obj.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
|
||||||
|
obj.tags.add(prototype_key, category=PROTOTYPE_TAG_CATEGORY)
|
||||||
|
|
||||||
if do_save:
|
if do_save:
|
||||||
changed += 1
|
changed += 1
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
@ -707,7 +836,7 @@ def spawn(*prototypes, **kwargs):
|
||||||
Args:
|
Args:
|
||||||
prototypes (str or dict): Each argument should either be a
|
prototypes (str or dict): Each argument should either be a
|
||||||
prototype_key (will be used to find the prototype) or a full prototype
|
prototype_key (will be used to find the prototype) or a full prototype
|
||||||
dictionary. These will be batched-spawned as one object each.
|
dictionary. These will be batched-spawned as one object each.
|
||||||
Kwargs:
|
Kwargs:
|
||||||
prototype_modules (str or list): A python-path to a prototype
|
prototype_modules (str or list): A python-path to a prototype
|
||||||
module, or a list of such paths. These will be used to build
|
module, or a list of such paths. These will be used to build
|
||||||
|
|
@ -804,7 +933,7 @@ def spawn(*prototypes, **kwargs):
|
||||||
prototype_key = prototype.get("prototype_key", None)
|
prototype_key = prototype.get("prototype_key", None)
|
||||||
if prototype_key:
|
if prototype_key:
|
||||||
# we make sure to add a tag identifying which prototype created this object
|
# we make sure to add a tag identifying which prototype created this object
|
||||||
tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY))
|
tags.append((prototype_key, PROTOTYPE_TAG_CATEGORY))
|
||||||
|
|
||||||
val = prot.pop("exec", "")
|
val = prot.pop("exec", "")
|
||||||
execs = init_spawn_value(val, make_iter)
|
execs = init_spawn_value(val, make_iter)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from evennia.utils.test_resources import EvenniaTest
|
||||||
from evennia.utils.tests.test_evmenu import TestEvMenu
|
from evennia.utils.tests.test_evmenu import TestEvMenu
|
||||||
from evennia.prototypes import spawner, prototypes as protlib
|
from evennia.prototypes import spawner, prototypes as protlib
|
||||||
from evennia.prototypes import menus as olc_menus
|
from evennia.prototypes import menus as olc_menus
|
||||||
from evennia.prototypes import protfuncs as protofuncs
|
from evennia.prototypes import protfuncs as protofuncs, spawner
|
||||||
|
|
||||||
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
|
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
|
||||||
|
|
||||||
|
|
@ -212,22 +212,21 @@ class TestUtils(EvenniaTest):
|
||||||
"puppet:pperm(Developer);tell:perm(Admin);view:all()",
|
"puppet:pperm(Developer);tell:perm(Admin);view:all()",
|
||||||
"KEEP",
|
"KEEP",
|
||||||
),
|
),
|
||||||
"prototype_tags": {},
|
"prototype_tags": (None, None, 'KEEP'),
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"oldtest": (
|
"oldtest": (
|
||||||
("oldtest", "to_keep", None, ""),
|
("oldtest", "to_keep", None, ""),
|
||||||
("oldtest", "to_keep", None, ""),
|
("oldtest", "to_keep", None, ""),
|
||||||
"KEEP",
|
"KEEP",
|
||||||
),
|
),
|
||||||
"test": (("test", "testval", None, ""), None, "REMOVE"),
|
"desc": (("desc", "changed desc", None, ""), None, "KEEP"),
|
||||||
"desc": (("desc", "changed desc", None, ""), None, "REMOVE"),
|
"fooattr": (Something, ("fooattr", "fooattrval", None, ""), "ADD"),
|
||||||
"fooattr": (None, ("fooattr", "fooattrval", None, ""), "ADD"),
|
|
||||||
"test": (
|
"test": (
|
||||||
("test", "testval", None, ""),
|
("test", "testval", None, ""),
|
||||||
("test", "testval_changed", None, ""),
|
("test", "testval_changed", None, ""),
|
||||||
"UPDATE",
|
"UPDATE",
|
||||||
),
|
),
|
||||||
"new": (None, ("new", "new_val", None, ""), "ADD"),
|
"new": (Something, ("new", "new_val", None, ""), "ADD"),
|
||||||
},
|
},
|
||||||
"key": ("Obj", "Obj", "KEEP"),
|
"key": ("Obj", "Obj", "KEEP"),
|
||||||
"typeclass": (
|
"typeclass": (
|
||||||
|
|
@ -246,7 +245,7 @@ class TestUtils(EvenniaTest):
|
||||||
spawner.flatten_diff(pdiff),
|
spawner.flatten_diff(pdiff),
|
||||||
{
|
{
|
||||||
"aliases": "REMOVE",
|
"aliases": "REMOVE",
|
||||||
"attrs": "REPLACE",
|
"attrs": "UPDATE",
|
||||||
"home": "KEEP",
|
"home": "KEEP",
|
||||||
"key": "KEEP",
|
"key": "KEEP",
|
||||||
"location": "KEEP",
|
"location": "KEEP",
|
||||||
|
|
@ -270,7 +269,9 @@ class TestUtils(EvenniaTest):
|
||||||
new_prot = spawner.prototype_from_object(self.obj1)
|
new_prot = spawner.prototype_from_object(self.obj1)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{
|
{
|
||||||
|
"aliases": ['foo'],
|
||||||
"attrs": [
|
"attrs": [
|
||||||
|
("desc", "changed desc", None, ""),
|
||||||
("fooattr", "fooattrval", None, ""),
|
("fooattr", "fooattrval", None, ""),
|
||||||
("new", "new_val", None, ""),
|
("new", "new_val", None, ""),
|
||||||
("oldtest", "to_keep", None, ""),
|
("oldtest", "to_keep", None, ""),
|
||||||
|
|
@ -293,6 +294,9 @@ class TestUtils(EvenniaTest):
|
||||||
"view:all()",
|
"view:all()",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
'tags': [
|
||||||
|
('footag', 'foocategory', None),
|
||||||
|
(Something, 'from_prototype', None)],
|
||||||
"permissions": ["builder"],
|
"permissions": ["builder"],
|
||||||
"prototype_desc": "Built from Obj",
|
"prototype_desc": "Built from Obj",
|
||||||
"prototype_key": Something,
|
"prototype_key": Something,
|
||||||
|
|
@ -851,7 +855,7 @@ class TestMenuModule(EvenniaTest):
|
||||||
|
|
||||||
self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject")
|
self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot["prototype_key"]
|
obj.tags.get(category=spawner.PROTOTYPE_TAG_CATEGORY), self.test_prot["prototype_key"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# update helpers
|
# update helpers
|
||||||
|
|
@ -912,24 +916,20 @@ class TestMenuModule(EvenniaTest):
|
||||||
|
|
||||||
texts, options = olc_menus._format_diff_text_and_options(obj_diff)
|
texts, options = olc_menus._format_diff_text_and_options(obj_diff)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"\n".join(texts),
|
"\n".join(txt.strip() for txt in texts),
|
||||||
"- |wattrs:|n \n"
|
"- |wattrs:|n |c[1] |yADD|n: foo |W=|n bar |W(category:|n None|W, locks:|n |W)|n"
|
||||||
" |gKEEP|W:|n desc |W=|n This is User #1. |W(category:|n None|W, locks:|n |W)|n\n"
|
"\n- |whome:|n"
|
||||||
" |c[1] |yADD|n|W:|n None |W->|n foo |W=|n bar |W(category:|n None|W, locks:|n |W)|n\n"
|
"\n- |wkey:|n"
|
||||||
" |gKEEP|W:|n prelogout_location |W=|n #2 |W(category:|n None|W, locks:|n |W)|n\n"
|
"\n- |wlocks:|n"
|
||||||
"- |whome:|n |gKEEP|W:|n #2\n"
|
"\n- |wpermissions:|n"
|
||||||
"- |wkey:|n |gKEEP|W:|n TestChar\n"
|
"\n- |wprototype_desc:|n |c[2] |rREMOVE|n: Testobject build"
|
||||||
"- |wlocks:|n |gKEEP|W:|n boot:false();call:false();control:perm(Developer);delete:false();edit:false();examine:perm(Developer);get:false();msg:all();puppet:false();tell:perm(Admin);view:all()\n"
|
"\n- |wprototype_key:|n"
|
||||||
"- |wpermissions:|n \n"
|
"\n- |wprototype_locks:|n"
|
||||||
" |gKEEP|W:|n developer\n"
|
"\n- |wprototype_tags:|n"
|
||||||
"- |wprototype_desc:|n |c[2] |rREMOVE|n|W:|n Testobject build |W->|n None\n"
|
"\n- |wtags:|n |c[3] |yADD|n: foo |W(category:|n None|W)|n"
|
||||||
"- |wprototype_key:|n |gKEEP|W:|n TestDiffKey\n"
|
"\n- |wtypeclass:|n"
|
||||||
"- |wprototype_locks:|n |gKEEP|W:|n spawn:all();edit:all()\n"
|
|
||||||
"- |wprototype_tags:|n \n"
|
|
||||||
"- |wtags:|n \n"
|
|
||||||
" |c[3] |yADD|n|W:|n None |W->|n foo |W(category:|n None|W)|n\n"
|
|
||||||
"- |wtypeclass:|n |gKEEP|W:|n typeclasses.characters.Character",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
options,
|
options,
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue