mirror of
https://github.com/evennia/evennia.git
synced 2026-03-19 06:16:31 +01:00
Fixing protoype spawning
This commit is contained in:
parent
f5917378d2
commit
cb989934ba
4 changed files with 411 additions and 212 deletions
|
|
@ -13,13 +13,12 @@ from evennia.utils.utils import (
|
|||
class_from_module,
|
||||
get_all_typeclasses,
|
||||
variable_from_module,
|
||||
dbref,
|
||||
dbref, interactive
|
||||
)
|
||||
from evennia.utils.eveditor import EvEditor
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
|
||||
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)
|
||||
|
||||
|
|
@ -2099,10 +2098,10 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
# to confirm changes.
|
||||
if "prototype" in self.switches:
|
||||
diff, _ = spawner.prototype_diff_from_object(prototype, obj)
|
||||
txt, options = _format_diff_text_and_options(diff, objects=[obj])
|
||||
txt = spawner.format_diff(diff)
|
||||
prompt = (
|
||||
"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:
|
||||
prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state."
|
||||
|
|
@ -3227,6 +3226,10 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
|||
self.caller.msg(string)
|
||||
|
||||
|
||||
# helper functions for spawn
|
||||
|
||||
|
||||
|
||||
class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
spawn objects from prototype
|
||||
|
|
@ -3256,7 +3259,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
them with latest version of given prototype. If given with /save,
|
||||
will auto-update all objects with the old version of the prototype
|
||||
without asking first.
|
||||
edit, olc - create/manipulate prototype in a menu interface.
|
||||
edit, menu, olc - create/manipulate prototype in a menu interface.
|
||||
|
||||
Example:
|
||||
spawn GOBLIN
|
||||
|
|
@ -3309,56 +3312,203 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
locks = "cmd:perm(spawn) or perm(Builder)"
|
||||
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}'."
|
||||
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 found to match the query '{query}'.")
|
||||
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. """
|
||||
EvMore(
|
||||
self.caller,
|
||||
str(protlib.list_prototypes(self.caller, key=key, tags=tags)),
|
||||
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
|
||||
n_updated = spawner.batch_update_objects_with_prototype(
|
||||
prototype, objects=existing_objects)
|
||||
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 self.args:
|
||||
parts = (part.strip().lower() for part in self.args.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):
|
||||
"""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
|
||||
noloc = "noloc" in self.switches
|
||||
|
||||
# run the menu/olc
|
||||
if (
|
||||
self.cmdstring == "olc"
|
||||
or "menu" in self.switches
|
||||
|
|
@ -3368,63 +3518,42 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
# OLC menu mode
|
||||
prototype = None
|
||||
if self.lhs:
|
||||
key = self.lhs
|
||||
prototype = protlib.search_prototype(key=key)
|
||||
if len(prototype) > 1:
|
||||
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))
|
||||
prototype_key = self.lhs
|
||||
prototype = self._search_prototype(prototype_key)
|
||||
if not prototype:
|
||||
return
|
||||
olc_menus.start_olc(caller, session=self.session, prototype=prototype)
|
||||
return
|
||||
|
||||
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:
|
||||
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,
|
||||
str(protlib.list_prototypes(caller, key=key, tags=tags)),
|
||||
exit_on_lastpage=True,
|
||||
)
|
||||
# an empty search returns the full list
|
||||
self._list_prototypes()
|
||||
return
|
||||
|
||||
# search for key;tag combinations
|
||||
key, _, tags = self._parse_key_desc_tags(self.args, desc=False)
|
||||
self._list_prototypes(key, tags)
|
||||
return
|
||||
|
||||
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:
|
||||
self.switches.append("list")
|
||||
else:
|
||||
matchstring = _search_show_prototype(self.args)
|
||||
if matchstring:
|
||||
caller.msg(matchstring)
|
||||
else:
|
||||
caller.msg("No prototype '{}' was found.".format(self.args))
|
||||
# we don't show the list of all details, that's too spammy.
|
||||
caller.msg("You need to specify a prototype-key to show.")
|
||||
return
|
||||
|
||||
if "list" in self.switches:
|
||||
# for list, all optional arguments are tags
|
||||
# import pudb; pudb.set_trace()
|
||||
detail_string = self._get_prototype_detail(self.args)
|
||||
if not detail_string:
|
||||
return
|
||||
caller.msg(detail_string)
|
||||
return
|
||||
|
||||
EvMore(
|
||||
caller,
|
||||
str(protlib.list_prototypes(caller, tags=self.lhslist)),
|
||||
exit_on_lastpage=True,
|
||||
justify_kwargs=False,
|
||||
)
|
||||
if "list" in self.switches:
|
||||
# for list, all optional arguments are tags.
|
||||
self._list_prototypes(tags=self.lhslist)
|
||||
return
|
||||
|
||||
if "save" in self.switches:
|
||||
|
|
@ -3435,27 +3564,41 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
return
|
||||
|
||||
prototype_key, prototype_desc, prototype_tags = self._parse_key_desc_tags(self.lhs)
|
||||
|
||||
# handle rhs:
|
||||
prototype = _parse_prototype(self.lhs.strip())
|
||||
prototype = self._parse_prototype(self.rhs.strip())
|
||||
if not prototype:
|
||||
return
|
||||
|
||||
# present prototype to save
|
||||
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"
|
||||
if prototype.get("prototype_key") != prototype_key:
|
||||
caller.msg("(Replacing `prototype_key` in prototype with given key.)")
|
||||
prototype['prototype_key'] = prototype_key
|
||||
if prototype_desc and prototype.get("prototype_desc") != prototype_desc:
|
||||
caller.msg("(Replacing `prototype_desc` in prototype with given desc.)")
|
||||
prototype['prototype_desc'] = prototype_desc
|
||||
if prototype_tags and prototype.get("prototype_tags") != prototype_tags:
|
||||
caller.msg("(Replacing `prototype_tags` in prototype with given tag(s))" )
|
||||
prototype['prototype_tags'] = prototype_tags
|
||||
|
||||
prototype_key = prototype.get("prototype_key")
|
||||
if not prototype_key:
|
||||
caller.msg("\n|yTo save a prototype it must have the 'prototype_key' set.")
|
||||
return
|
||||
string = ""
|
||||
# check for existing prototype (exact match)
|
||||
old_prototype = self._search_prototype(prototype_key, quiet=True)
|
||||
|
||||
# check for existing prototype,
|
||||
old_matchstring = _search_show_prototype(prototype_key)
|
||||
print("old_prototype", old_prototype)
|
||||
print("new_prototype", prototype)
|
||||
diff = spawner.prototype_diff(old_prototype, prototype, homogenize=True)
|
||||
print("diff", diff)
|
||||
diffstr = spawner.format_diff(diff)
|
||||
new_prototype_detail = self._get_prototype_detail(prototypes=[prototype])
|
||||
|
||||
if old_matchstring:
|
||||
string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring)
|
||||
question = "\n|yDo you want to replace the existing prototype?|n [Y]/N"
|
||||
if old_prototype:
|
||||
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)
|
||||
if answer.lower() in ["n", "no"]:
|
||||
|
|
@ -3474,82 +3617,52 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg("|gSaved prototype:|n {}".format(prototype_key))
|
||||
|
||||
# check if we want to update existing objects
|
||||
existing_objects = protlib.search_objects_with_prototype(prototype_key)
|
||||
if existing_objects:
|
||||
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))
|
||||
|
||||
self._update_existing_objects(self.caller, prototype_key, quiet=True)
|
||||
return
|
||||
|
||||
if not self.args:
|
||||
# all switches beyond this point gets a common non-arg return
|
||||
ncount = len(protlib.search_prototype())
|
||||
caller.msg(
|
||||
"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
|
||||
|
||||
if "delete" in self.switches:
|
||||
# remove db-based prototype
|
||||
matchstring = _search_show_prototype(self.args)
|
||||
if matchstring:
|
||||
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?)"
|
||||
)
|
||||
)
|
||||
prototype_detail = self._get_prototype_detail(self.args)
|
||||
if not prototype_detail:
|
||||
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:
|
||||
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:
|
||||
# update existing prototypes
|
||||
key = self.args.strip().lower()
|
||||
existing_objects = protlib.search_objects_with_prototype(key)
|
||||
if existing_objects:
|
||||
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))
|
||||
prototype_key = self.args.strip().lower()
|
||||
self._update_existing_objects(self.caller, prototype_key)
|
||||
return
|
||||
|
||||
# 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
|
||||
)
|
||||
if not prototype:
|
||||
|
|
@ -3559,35 +3672,20 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
key = "<unnamed>"
|
||||
if isinstance(prototype, str):
|
||||
# A prototype key we are looking to apply
|
||||
key = prototype
|
||||
prototypes = protlib.search_prototype(prototype)
|
||||
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
|
||||
prototype_key = prototype
|
||||
prototype = self._search_prototype(prototype_key)
|
||||
|
||||
if "noloc" not in self.switches and "location" not in prototype:
|
||||
prototype["location"] = self.caller.location
|
||||
if not prototype:
|
||||
return
|
||||
|
||||
# proceed to spawning
|
||||
try:
|
||||
for obj in spawner.spawn(prototype):
|
||||
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
||||
if not noloc and "location" not in prototype:
|
||||
# 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:
|
||||
caller.msg(err)
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ _PROTOTYPE_TAG_CATEGORY = "from_prototype"
|
|||
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
|
||||
PROT_FUNCS = {}
|
||||
|
||||
_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:perm(Admin)"
|
||||
|
||||
|
||||
class PermissionError(RuntimeError):
|
||||
pass
|
||||
|
|
@ -84,8 +86,19 @@ def homogenize_prototype(prototype, custom_keys=None):
|
|||
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 ())
|
||||
|
||||
# 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
|
||||
tags = make_iter(prototype.get("tags", []))
|
||||
homogenized_tags = []
|
||||
|
|
@ -111,12 +124,14 @@ def homogenize_prototype(prototype, custom_keys=None):
|
|||
|
||||
# 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
|
||||
homogenized["prototype_key"] = "prototype-{}".format(
|
||||
hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7]
|
||||
)
|
||||
|
||||
"prototype-{}".format(
|
||||
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:
|
||||
homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS
|
||||
|
||||
|
|
@ -230,7 +245,7 @@ def save_prototype(prototype):
|
|||
"prototype_desc", prototype.get("prototype_desc", "")
|
||||
)
|
||||
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)
|
||||
if not is_valid:
|
||||
|
|
|
|||
|
|
@ -322,9 +322,9 @@ def prototype_from_object(obj):
|
|||
return prot
|
||||
|
||||
|
||||
def prototype_diff(prototype1, prototype2, maxdepth=2):
|
||||
def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=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
|
||||
by the menu to allow a user to customize what should be kept.
|
||||
|
||||
|
|
@ -334,6 +334,8 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
|||
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
|
||||
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.
|
||||
|
||||
Returns:
|
||||
diff (dict): A structure detailing how to convert prototype1 to prototype2. All
|
||||
|
|
@ -350,7 +352,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
|||
old_type = type(old)
|
||||
new_type = type(new)
|
||||
|
||||
if old_type != new_type:
|
||||
if old_type == new_type and not (old and new):
|
||||
# both old and new are unset, like [] or None
|
||||
return (old, new, "KEEP")
|
||||
elif old_type != new_type:
|
||||
if old and not new:
|
||||
if depth < maxdepth and old_type == dict:
|
||||
return {key: (part, None, "REMOVE") for key, part in old.items()}
|
||||
|
|
@ -387,7 +392,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
|||
else:
|
||||
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
|
||||
|
||||
|
|
@ -490,6 +498,80 @@ def prototype_diff_from_object(prototype, obj):
|
|||
return diff, obj_prototype
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Update existing objects with the latest version of the prototype.
|
||||
|
|
@ -525,15 +607,13 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
|||
|
||||
# make sure the diff is flattened
|
||||
diff = flatten_diff(diff)
|
||||
|
||||
changed = 0
|
||||
for obj in objects:
|
||||
do_save = False
|
||||
|
||||
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
|
||||
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():
|
||||
if directive in ("UPDATE", "REPLACE"):
|
||||
|
|
@ -623,6 +703,11 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
|||
pass
|
||||
else:
|
||||
obj.attributes.remove(key)
|
||||
|
||||
# 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:
|
||||
changed += 1
|
||||
obj.save()
|
||||
|
|
@ -707,7 +792,7 @@ def spawn(*prototypes, **kwargs):
|
|||
Args:
|
||||
prototypes (str or dict): Each argument should either be a
|
||||
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:
|
||||
prototype_modules (str or list): A python-path to a prototype
|
||||
module, or a list of such paths. These will be used to build
|
||||
|
|
|
|||
|
|
@ -70,14 +70,15 @@ class HTTPChannelWithXForwardedFor(http.HTTPChannel):
|
|||
Check to see if this is a reverse proxied connection.
|
||||
|
||||
"""
|
||||
CLIENT = 0
|
||||
http.HTTPChannel.allHeadersReceived(self)
|
||||
req = self.requests[-1]
|
||||
client_ip, port = self.transport.client
|
||||
proxy_chain = req.getHeader("X-FORWARDED-FOR")
|
||||
if proxy_chain and client_ip in _UPSTREAM_IPS:
|
||||
forwarded = proxy_chain.split(", ", 1)[CLIENT]
|
||||
self.transport.client = (forwarded, port)
|
||||
if self.requests:
|
||||
CLIENT = 0
|
||||
http.HTTPChannel.allHeadersReceived(self)
|
||||
req = self.requests[-1]
|
||||
client_ip, port = self.transport.client
|
||||
proxy_chain = req.getHeader("X-FORWARDED-FOR")
|
||||
if proxy_chain and client_ip in _UPSTREAM_IPS:
|
||||
forwarded = proxy_chain.split(", ", 1)[CLIENT]
|
||||
self.transport.client = (forwarded, port)
|
||||
|
||||
|
||||
# Monkey-patch Twisted to handle X-Forwarded-For.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue