"""
OLC Prototype menu nodes
"""
import json
import re
from random import choice
from django.conf import settings
from django.db.models import Q
from evennia.locks.lockhandler import get_all_lockfuncs
from evennia.objects.models import ObjectDB
from evennia.prototypes import prototypes as protlib
from evennia.prototypes import spawner
from evennia.utils import evmore, utils
from evennia.utils.ansi import strip_ansi
from evennia.utils.evmenu import EvMenu, list_node
# ------------------------------------------------------------
#
# OLC Prototype design menu
#
# ------------------------------------------------------------
_MENU_CROP_WIDTH = 15
_MENU_ATTR_LITERAL_EVAL_ERROR = (
"|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n"
"You also need to use correct Python syntax. Remember especially to put quotes around all "
"strings inside lists and dicts.|n"
)
# Helper functions
def _get_menu_prototype(caller):
"""Return currently active menu prototype."""
prototype = None
if hasattr(caller.ndb._menutree, "olc_prototype"):
prototype = caller.ndb._menutree.olc_prototype
if not prototype:
caller.ndb._menutree.olc_prototype = prototype = {}
caller.ndb._menutree.olc_new = True
return prototype
def _get_flat_menu_prototype(caller, refresh=False, validate=False):
"""Return prototype where parent values are included"""
flat_prototype = None
if not refresh and hasattr(caller.ndb._menutree, "olc_flat_prototype"):
flat_prototype = caller.ndb._menutree.olc_flat_prototype
if not flat_prototype:
prot = _get_menu_prototype(caller)
caller.ndb._menutree.olc_flat_prototype = flat_prototype = spawner.flatten_prototype(
prot, validate=validate
)
return flat_prototype
def _get_unchanged_inherited(caller, protname):
"""Return prototype values inherited from parent(s), which are not replaced in child"""
prototype = _get_menu_prototype(caller)
if protname in prototype:
return protname[protname], False
else:
flattened = _get_flat_menu_prototype(caller)
if protname in flattened:
return protname[protname], True
return None, False
def _set_menu_prototype(caller, prototype):
"""Set the prototype with existing one"""
caller.ndb._menutree.olc_prototype = prototype
caller.ndb._menutree.olc_new = False
return prototype
def _is_new_prototype(caller):
"""Check if prototype is marked as new or was loaded from a saved one."""
return hasattr(caller.ndb._menutree, "olc_new")
def _format_option_value(prop, required=False, prototype=None, cropper=None):
"""
Format wizard option values.
Args:
prop (str): Name or value to format.
required (bool, optional): The option is required.
prototype (dict, optional): If given, `prop` will be considered a key in this prototype.
cropper (callable, optional): A function to crop the value to a certain width.
Returns:
value (str): The formatted value.
"""
if prototype is not None:
prop = prototype.get(prop, "")
out = prop
if callable(prop):
if hasattr(prop, "__name__"):
out = "<{}>".format(prop.__name__)
else:
out = repr(prop)
if utils.is_iter(prop):
out = ", ".join(str(pr) for pr in prop)
if not out and required:
out = "|runset"
if out:
return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH))
return ""
def _set_prototype_value(caller, field, value, parse=True):
"""Set prototype's field in a safe way."""
prototype = _get_menu_prototype(caller)
prototype[field] = value
caller.ndb._menutree.olc_prototype = prototype
return prototype
def _set_property(caller, raw_string, **kwargs):
"""
Add or update a property. To be called by the 'goto' option variable.
Args:
caller (Object, Account): The user of the wizard.
raw_string (str): Input from user on given node - the new value to set.
Keyword Args:
test_parse (bool): If set (default True), parse raw_string for protfuncs and obj-refs and
try to run result through literal_eval. The parser will be run in 'testing' mode and any
parsing errors will shown to the user. Note that this is just for testing, the original
given string will be what is inserted.
prop (str): Property name to edit with `raw_string`.
processor (callable): Converts `raw_string` to a form suitable for saving.
next_node (str): Where to redirect to after this has run.
Returns:
next_node (str): Next node to go to.
"""
prop = kwargs.get("prop", "prototype_key")
processor = kwargs.get("processor", None)
next_node = kwargs.get("next_node", None)
if callable(processor):
try:
value = processor(raw_string)
except Exception as err:
caller.msg(
"Could not set {prop} to {value} ({err})".format(
prop=prop.replace("_", "-").capitalize(), value=raw_string, err=str(err)
)
)
# this means we'll re-run the current node.
return None
else:
value = raw_string
if not value:
return next_node
prototype = _set_prototype_value(caller, prop, value)
caller.ndb._menutree.olc_prototype = prototype
try:
# TODO simple way to get rid of the u'' markers in list reprs, remove this when on py3.
repr_value = json.dumps(value)
except Exception:
repr_value = value
out = [" Set {prop} to {value} ({typ}).".format(prop=prop, value=repr_value, typ=type(value))]
if kwargs.get("test_parse", True):
out.append(" Simulating prototype-func parsing ...")
parsed_value = protlib.protfunc_parser(value, testing=True, prototype=prototype)
if parsed_value != value:
out.append(
" |g(Example-)value when parsed ({}):|n {}".format(type(parsed_value), parsed_value)
)
else:
out.append(" |gNo change when parsed.")
caller.msg("\n".join(out))
return next_node
def _wizard_options(curr_node, prev_node, next_node, color="|W", search=False):
"""Creates default navigation options available in the wizard."""
options = []
if prev_node:
options.append(
{
"key": ("|wB|Wack", "b"),
"desc": "{color}({node})|n".format(color=color, node=prev_node.replace("_", "-")),
"goto": "node_{}".format(prev_node),
}
)
if next_node:
options.append(
{
"key": ("|wF|Worward", "f"),
"desc": "{color}({node})|n".format(color=color, node=next_node.replace("_", "-")),
"goto": "node_{}".format(next_node),
}
)
options.append({"key": ("|wI|Wndex", "i"), "goto": "node_index"})
if curr_node:
options.append(
{
"key": ("|wV|Walidate prototype", "validate", "v"),
"goto": ("node_validate_prototype", {"back": curr_node}),
}
)
if search:
options.append(
{
"key": ("|wSE|Warch objects", "search object", "search", "se"),
"goto": ("node_search_object", {"back": curr_node}),
}
)
return options
def _set_actioninfo(caller, string):
caller.ndb._menutree.actioninfo = string
def _path_cropper(pythonpath):
"Crop path to only the last component"
return pythonpath.split(".")[-1]
def _validate_prototype(prototype):
"""Run validation on prototype"""
txt = protlib.prototype_to_str(prototype)
errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)"
err = False
try:
# validate, don't spawn
spawner.spawn(prototype, only_validate=True)
except RuntimeError as exc:
errors = "\n\n|r{}|n".format(exc)
err = True
except RuntimeWarning as exc:
errors = "\n\n|y{}|n".format(exc)
err = True
text = txt + errors
return err, text
def _format_protfuncs():
out = []
sorted_funcs = [
(key, func)
for key, func in sorted(protlib.FUNC_PARSER.callables.items(), key=lambda tup: tup[0])
]
for protfunc_name, protfunc in sorted_funcs:
out.append(
"- |c${name}|n - |W{docs}".format(
name=protfunc_name,
docs=utils.justify(protfunc.__doc__.strip(), align="l", indent=10).strip(),
)
)
return "\n ".join(out)
def _format_lockfuncs():
out = []
sorted_funcs = [
(key, func) for key, func in sorted(get_all_lockfuncs().items(), key=lambda tup: tup[0])
]
for lockfunc_name, lockfunc in sorted_funcs:
doc = (lockfunc.__doc__ or "").strip()
out.append(
"- |c${name}|n - |W{docs}".format(
name=lockfunc_name, docs=utils.justify(doc, align="l", indent=10).strip()
)
)
return "\n".join(out)
def _format_list_actions(*args, **kwargs):
"""Create footer text for nodes with extra list actions
Args:
actions (str): Available actions. The first letter of the action name will be assumed
to be a shortcut.
Keyword Args:
prefix (str): Default prefix to use.
Returns:
string (str): Formatted footer for adding to the node text.
"""
actions = []
prefix = kwargs.get("prefix", "|WSelect with |w<num>|W. Other actions:|n ")
for action in args:
actions.append("|w{}|n|W{} |w<num>|n".format(action[0], action[1:]))
return prefix + " |W|||n ".join(actions)
def _get_current_value(caller, keyname, comparer=None, formatter=str, only_inherit=False):
"""
Return current value, marking if value comes from parent or set in this prototype.
Args:
keyname (str): Name of prototoype key to get current value of.
comparer (callable, optional): This will be called as comparer(prototype_value,
flattened_value) and is expected to return the value to show as the current
or inherited one. If not given, a straight comparison is used and what is returned
depends on the only_inherit setting.
formatter (callable, optional)): This will be called with the result of comparer.
only_inherit (bool, optional): If a current value should only be shown if all
the values are inherited from the prototype parent (otherwise, show an empty string).
Returns:
current (str): The current value.
"""
def _default_comparer(protval, flatval):
if only_inherit:
return "" if protval else flatval
else:
return protval if protval else flatval
if not callable(comparer):
comparer = _default_comparer
prot = _get_menu_prototype(caller)
flat_prot = _get_flat_menu_prototype(caller)
out = ""
if keyname in prot:
if keyname in flat_prot:
out = formatter(comparer(prot[keyname], flat_prot[keyname]))
if only_inherit:
if str(out).strip():
return "|WCurrent|n {} |W(|binherited|W):|n {}".format(keyname, out)
return ""
else:
if out:
return "|WCurrent|n {}|W:|n {}".format(keyname, out)
return "|W[No {} set]|n".format(keyname)
elif only_inherit:
return ""
else:
out = formatter(prot[keyname])
return "|WCurrent|n {}|W:|n {}".format(keyname, out)
elif keyname in flat_prot:
out = formatter(flat_prot[keyname])
if out:
return "|WCurrent|n {} |W(|n|binherited|W):|n {}".format(keyname, out)
else:
return ""
elif only_inherit:
return ""
else:
return "|W[No {} set]|n".format(keyname)
def _default_parse(raw_inp, choices, *args):
"""
Helper to parse default input to a node decorated with the node_list decorator on
the form l1, l 2, look 1, etc. Spaces are ignored, as is case.
Args:
raw_inp (str): Input from the user.
choices (list): List of available options on the node listing (list of strings).
args (tuples): The available actions, each specifed as a tuple (name, alias, ...)
Returns:
choice (str): A choice among the choices, or None if no match was found.
action (str): The action operating on the choice, or None.
"""
raw_inp = raw_inp.lower().strip()
mapping = {t.lower(): tup[0] for tup in args for t in tup}
match = re.match(r"(%s)\s*?(\d+)$" % "|".join(mapping.keys()), raw_inp)
if match:
action = mapping.get(match.group(1), None)
num = int(match.group(2)) - 1
num = num if 0 <= num < len(choices) else None
if action is not None and num is not None:
return choices[num], action
return None, None
# Menu nodes ------------------------------
# helper nodes
# validate prototype (available as option from all nodes)
[docs]def node_validate_prototype(caller, raw_string, **kwargs):
"""General node to view and validate a protototype"""
prototype = _get_flat_menu_prototype(caller, refresh=True, validate=False)
prev_node = kwargs.get("back", "index")
_, text = _validate_prototype(prototype)
helptext = """
The validator checks if the prototype's various values are on the expected form. It also tests
any $protfuncs.
"""
text = (text, helptext)
options = _wizard_options(None, prev_node, None)
options.append({"key": "_default", "goto": "node_" + prev_node})
return text, options
# node examine_entity
[docs]def node_examine_entity(caller, raw_string, **kwargs):
"""
General node to view a text and then return to previous node. Kwargs should contain "text" for
the text to show and 'back" pointing to the node to return to.
"""
text = kwargs.get("text", "Nothing was found here.")
helptext = "Use |wback|n to return to the previous node."
prev_node = kwargs.get("back", "index")
text = (text, helptext)
options = _wizard_options(None, prev_node, None)
options.append({"key": "_default", "goto": "node_" + prev_node})
return text, options
# node object_search
def _search_object(caller):
"update search term based on query stored on menu; store match too"
try:
searchstring = caller.ndb._menutree.olc_search_object_term.strip()
caller.ndb._menutree.olc_search_object_matches = []
except AttributeError:
return []
if not searchstring:
caller.msg("Must specify a search criterion.")
return []
is_dbref = utils.dbref(searchstring)
is_account = searchstring.startswith("*")
if is_dbref or is_account:
if is_dbref:
# a dbref search
results = caller.search(searchstring, global_search=True, quiet=True)
else:
# an account search
searchstring = searchstring.lstrip("*")
results = caller.search_account(searchstring, quiet=True)
else:
keyquery = Q(db_key__istartswith=searchstring)
aliasquery = Q(
db_tags__db_key__istartswith=searchstring, db_tags__db_tagtype__iexact="alias"
)
results = ObjectDB.objects.filter(keyquery | aliasquery).distinct()
caller.msg("Searching for '{}' ...".format(searchstring))
caller.ndb._menutree.olc_search_object_matches = results
return ["{}(#{})".format(obj.key, obj.id) for obj in results]
def _object_search_select(caller, obj_entry, **kwargs):
choices = kwargs["available_choices"]
num = choices.index(obj_entry)
matches = caller.ndb._menutree.olc_search_object_matches
obj = matches[num]
if not obj.access(caller, "examine"):
caller.msg("|rYou don't have 'examine' access on this object.|n")
del caller.ndb._menutree.olc_search_object_term
return "node_search_object"
prot = spawner.prototype_from_object(obj)
txt = protlib.prototype_to_str(prot)
return "node_examine_entity", {"text": txt, "back": "search_object"}
def _object_search_actions(caller, raw_inp, **kwargs):
"All this does is to queue a search query"
choices = kwargs["available_choices"]
obj_entry, action = _default_parse(
raw_inp, choices, ("examine", "e"), ("create prototype from object", "create", "c")
)
raw_inp = raw_inp.strip()
if obj_entry:
num = choices.index(obj_entry)
matches = caller.ndb._menutree.olc_search_object_matches
obj = matches[num]
prot = spawner.prototype_from_object(obj)
if action == "examine":
if not obj.access(caller, "examine"):
caller.msg("\n|rYou don't have 'examine' access on this object.|n")
del caller.ndb._menutree.olc_search_object_term
return "node_search_object"
txt = protlib.prototype_to_str(prot)
return "node_examine_entity", {"text": txt, "back": "search_object"}
else:
# load prototype
if not obj.access(caller, "edit"):
caller.msg("|rYou don't have access to do this with this object.|n")
del caller.ndb._menutree.olc_search_object_term
return "node_search_object"
_set_menu_prototype(caller, prot)
caller.msg("Created prototype from object.")
return "node_index"
elif raw_inp:
caller.ndb._menutree.olc_search_object_term = raw_inp
return "node_search_object", kwargs
else:
# empty input - exit back to previous node
prev_node = "node_" + kwargs.get("back", "index")
return prev_node
@list_node(_search_object, _object_search_select)
def node_search_object(caller, raw_inp, **kwargs):
"""
Node for searching for an existing object.
"""
try:
matches = caller.ndb._menutree.olc_search_object_matches
except AttributeError:
matches = []
nmatches = len(matches)
prev_node = kwargs.get("back", "index")
if matches:
text = """
Found {num} match{post}.
(|RWarning: creating a prototype will |roverwrite|r |Rthe current prototype!)|n""".format(
num=nmatches, post="es" if nmatches > 1 else ""
)
_set_actioninfo(
caller,
_format_list_actions("examine", "create prototype from object", prefix="Actions: "),
)
else:
text = "Enter search criterion."
helptext = """
You can search objects by specifying partial key, alias or its exact #dbref. Use *query to
search for an Account instead.
Once having found any matches you can choose to examine it or use |ccreate prototype from
object|n. If doing the latter, a prototype will be calculated from the selected object and
loaded as the new 'current' prototype. This is useful for having a base to build from but be
careful you are not throwing away any existing, unsaved, prototype work!
"""
text = (text, helptext)
options = _wizard_options(None, prev_node, None)
options.append({"key": "_default", "goto": (_object_search_actions, {"back": prev_node})})
return text, options
# main index (start page) node
[docs]def node_index(caller):
prototype = _get_menu_prototype(caller)
text = """
|c --- Prototype wizard --- |n
%s
A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype
can either be hard-coded, left empty or scripted using |w$protfuncs|n - for example to
randomize the value every time a new entity is spawned. The fields whose names start with
'Prototype-' are not fields on the object itself but are used for prototype-inheritance, or
when saving and loading.
Select prototype field to edit. If you are unsure, start from [|w1|n]. Enter [|wh|n]elp at
any menu node for more info.
"""
helptxt = """
|c- prototypes |n
A prototype is really just a Python dictionary. When spawning, this dictionary is essentially
passed into `|wevennia.utils.create.create_object(**prototype)|n` to create a new object. By
using different prototypes you can customize instances of objects without having to do code
changes to their typeclass (something which requires code access). The classical example is
to spawn goblins with different names, looks, equipment and skill, each based on the same
`Goblin` typeclass.
At any time you can [|wV|n]alidate that the prototype works correctly and use it to
[|wSP|n]awn a new entity. You can also [|wSA|n]ve|n your work, [|wLO|n]oad an existing
prototype to [|wSE|n]arch for existing objects to use as a base. Use [|wL|n]ook to re-show a
menu node. [|wQ|n]uit will always exit the menu and [|wH|n]elp will show context-sensitive
help.
|c- $protfuncs |n
Prototype-functions (protfuncs) allow for limited scripting within a prototype. These are
entered as a string $funcname(arg, arg, ...) and are evaluated |wat the time of spawning|n
only. They can also be nested for combined effects.
{pfuncs}
""".format(
pfuncs=_format_protfuncs()
)
# If a prototype is being edited, show its key and
# prototype_key under the title
loaded_prototype = ""
if "prototype_key" in prototype or "key" in prototype:
loaded_prototype = " --- Editing: |y{}({})|n --- ".format(
prototype.get("key", ""), prototype.get("prototype_key", "")
)
text = text % (loaded_prototype)
text = (text, helptxt)
options = []
options.append(
{
"desc": "|WPrototype-Key|n|n{}".format(
_format_option_value("Key", "prototype_key" not in prototype, prototype, None)
),
"goto": "node_prototype_key",
}
)
for key in (
"Prototype_Parent",
"Typeclass",
"Key",
"Aliases",
"Attrs",
"Tags",
"Locks",
"Permissions",
"Location",
"Home",
"Destination",
):
required = False
cropper = None
if key in ("Prototype_Parent", "Typeclass"):
required = ("prototype_parent" not in prototype) and ("typeclass" not in prototype)
if key == "Typeclass":
cropper = _path_cropper
options.append(
{
"desc": "{}{}|n{}".format(
"|W" if key == "Prototype_Parent" else "|w",
key.replace("_", "-"),
_format_option_value(key, required, prototype, cropper=cropper),
),
"goto": "node_{}".format(key.lower()),
}
)
required = False
for key in ("Desc", "Tags", "Locks"):
options.append(
{
"desc": "|WPrototype-{}|n|n{}".format(
key, _format_option_value(key, required, prototype, None)
),
"goto": "node_prototype_{}".format(key.lower()),
}
)
options.extend(
(
{"key": ("|wV|Walidate prototype", "validate", "v"), "goto": "node_validate_prototype"},
{"key": ("|wSA|Wve prototype", "save", "sa"), "goto": "node_prototype_save"},
{"key": ("|wSP|Wawn prototype", "spawn", "sp"), "goto": "node_prototype_spawn"},
{"key": ("|wLO|Wad prototype", "load", "lo"), "goto": "node_prototype_load"},
{"key": ("|wSE|Warch objects|n", "search", "se"), "goto": "node_search_object"},
)
)
return text, options
# prototype_key node
def _check_prototype_key(caller, key):
old_prototype = protlib.search_prototype(key)
olc_new = _is_new_prototype(caller)
key = key.strip().lower()
if old_prototype:
old_prototype = old_prototype[0]
# we are starting a new prototype that matches an existing
if not caller.locks.check_lockstring(
caller, old_prototype["prototype_locks"], access_type="edit"
):
# return to the node_prototype_key to try another key
caller.msg(
"Prototype '{key}' already exists and you don't "
"have permission to edit it.".format(key=key)
)
return "node_prototype_key"
elif olc_new:
# we are selecting an existing prototype to edit. Reset to index.
del caller.ndb._menutree.olc_new
caller.ndb._menutree.olc_prototype = old_prototype
caller.msg("Prototype already exists. Reloading.")
return "node_index"
return _set_property(caller, key, prop="prototype_key")
[docs]def node_prototype_key(caller):
text = """
The |cPrototype-Key|n uniquely identifies the prototype and is |wmandatory|n. It is used to
find and use the prototype to spawn new entities. It is not case sensitive.
(To set a new value, just write it and press enter)
{current}""".format(
current=_get_current_value(caller, "prototype_key")
)
helptext = """
The prototype-key is not itself used when spawnng the new object, but is only used for
managing, storing and loading the prototype. It must be globally unique, so existing keys
will be checked before a new key is accepted. If an existing key is picked, the existing
prototype will be loaded.
"""
options = _wizard_options("prototype_key", "index", "prototype_parent")
options.append({"key": "_default", "goto": _check_prototype_key})
text = (text, helptext)
return text, options
# prototype_parents node
def _all_prototype_parents(caller):
"""Return prototype_key of all available prototypes for listing in menu"""
return [
prototype["prototype_key"]
for prototype in protlib.search_prototype()
if "prototype_key" in prototype
]
def _prototype_parent_actions(caller, raw_inp, **kwargs):
"""Parse the default Convert prototype to a string representation for closer inspection"""
choices = kwargs.get("available_choices", [])
prototype_parent, action = _default_parse(
raw_inp, choices, ("examine", "e", "l"), ("add", "a"), ("remove", "r", "delete", "d")
)
if prototype_parent:
# a selection of parent was made
prototype_parent = protlib.search_prototype(key=prototype_parent)[0]
prototype_parent_key = prototype_parent["prototype_key"]
# which action to apply on the selection
if action == "examine":
# examine the prototype
txt = protlib.prototype_to_str(prototype_parent)
kwargs["text"] = txt
kwargs["back"] = "prototype_parent"
return "node_examine_entity", kwargs
elif action == "add":
# add/append parent
prot = _get_menu_prototype(caller)
current_prot_parent = prot.get("prototype_parent", None)
if current_prot_parent:
current_prot_parent = utils.make_iter(current_prot_parent)
if prototype_parent_key in current_prot_parent:
caller.msg("Prototype_parent {} is already used.".format(prototype_parent_key))
return "node_prototype_parent"
else:
current_prot_parent.append(prototype_parent_key)
caller.msg("Add prototype parent for multi-inheritance.")
else:
current_prot_parent = prototype_parent_key
try:
if prototype_parent:
spawner.flatten_prototype(prototype_parent, validate=True)
else:
raise RuntimeError("Not found.")
except RuntimeError as err:
caller.msg(
"Selected prototype-parent {} "
"caused Error(s):\n|r{}|n".format(prototype_parent, err)
)
return "node_prototype_parent"
_set_prototype_value(caller, "prototype_parent", current_prot_parent)
_get_flat_menu_prototype(caller, refresh=True)
elif action == "remove":
# remove prototype parent
prot = _get_menu_prototype(caller)
current_prot_parent = prot.get("prototype_parent", None)
if current_prot_parent:
current_prot_parent = utils.make_iter(current_prot_parent)
try:
current_prot_parent.remove(prototype_parent_key)
_set_prototype_value(caller, "prototype_parent", current_prot_parent)
_get_flat_menu_prototype(caller, refresh=True)
caller.msg("Removed prototype parent {}.".format(prototype_parent_key))
except ValueError:
caller.msg(
"|rPrototype-parent {} could not be removed.".format(prototype_parent_key)
)
return "node_prototype_parent"
def _prototype_parent_select(caller, new_parent, **kwargs):
ret = None
prototype_parent = protlib.search_prototype(new_parent)
try:
if prototype_parent:
spawner.flatten_prototype(prototype_parent[0], validate=True)
else:
raise RuntimeError("Not found.")
except RuntimeError as err:
caller.msg(
"Selected prototype-parent {} " "caused Error(s):\n|r{}|n".format(new_parent, err)
)
else:
ret = _set_property(
caller,
new_parent,
prop="prototype_parent",
processor=str,
next_node="node_prototype_parent",
)
_get_flat_menu_prototype(caller, refresh=True)
caller.msg("Selected prototype parent |c{}|n.".format(new_parent))
return ret
@list_node(_all_prototype_parents, _prototype_parent_select)
def node_prototype_parent(caller):
prototype = _get_menu_prototype(caller)
prot_parent_keys = prototype.get("prototype_parent")
text = """
The |cPrototype Parent|n allows you to |winherit|n prototype values from another named
prototype (given as that prototype's |wprototype_key|n). If not changing these values in
the current prototype, the parent's value will be used. Pick the available prototypes below.
Note that somewhere in the prototype's parentage, a |ctypeclass|n must be specified. If no
parent is given, this prototype must define the typeclass (next menu node).
{current}
"""
helptext = """
Prototypes can inherit from one another. Changes in the child replace any values set in a
parent. The |wtypeclass|n key must exist |wsomewhere|n in the parent chain for the
prototype to be valid.
"""
_set_actioninfo(caller, _format_list_actions("examine", "add", "remove"))
ptexts = []
if prot_parent_keys:
for pkey in utils.make_iter(prot_parent_keys):
prot_parent = protlib.search_prototype(pkey)
if prot_parent:
prot_parent = prot_parent[0]
ptexts.append(
"|c -- {pkey} -- |n\n{prot}".format(
pkey=pkey, prot=protlib.prototype_to_str(prot_parent)
)
)
else:
ptexts.append("Prototype parent |r{pkey} was not found.".format(pkey=pkey))
if not ptexts:
ptexts.append("[No prototype_parent set]")
text = text.format(current="\n\n".join(ptexts))
text = (text, helptext)
options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W")
options.append({"key": "_default", "goto": _prototype_parent_actions})
return text, options
# typeclasses node
def _all_typeclasses(caller):
"""Get name of available typeclasses."""
return list(
name
for name in sorted(utils.get_all_typeclasses("evennia.objects.models.ObjectDB").keys())
if name != "evennia.objects.models.ObjectDB"
)
def _typeclass_actions(caller, raw_inp, **kwargs):
"""Parse actions for typeclass listing"""
choices = kwargs.get("available_choices", [])
typeclass_path, action = _default_parse(
raw_inp, choices, ("examine", "e", "l"), ("remove", "r", "delete", "d")
)
if typeclass_path:
if action == "examine":
typeclass = utils.get_all_typeclasses().get(typeclass_path)
if typeclass:
docstr = []
for line in typeclass.__doc__.split("\n"):
if line.strip():
docstr.append(line)
elif docstr:
break
docstr = "\n".join(docstr) if docstr else "<empty>"
txt = (
"Typeclass |c{typeclass_path}|n; "
"First paragraph of docstring:\n\n{docstring}".format(
typeclass_path=typeclass_path, docstring=docstr
)
)
else:
txt = "This is typeclass |y{}|n.".format(typeclass)
return "node_examine_entity", {"text": txt, "back": "typeclass"}
elif action == "remove":
prototype = _get_menu_prototype(caller)
old_typeclass = prototype.pop("typeclass", None)
if old_typeclass:
_set_menu_prototype(caller, prototype)
caller.msg("Cleared typeclass {}.".format(old_typeclass))
else:
caller.msg("No typeclass to remove.")
return "node_typeclass"
def _typeclass_select(caller, typeclass, **kwargs):
"""Select typeclass from list and add it to prototype. Return next node to go to."""
ret = _set_property(caller, typeclass, prop="typeclass", processor=str)
caller.msg("Selected typeclass |c{}|n.".format(typeclass))
return ret
@list_node(_all_typeclasses, _typeclass_select)
def node_typeclass(caller):
text = """
The |cTypeclass|n defines what 'type' of object this is - the actual working code to use.
All spawned objects must have a typeclass. If not given here, the typeclass must be set in
one of the prototype's |cparents|n.
{current}
""".format(
current=_get_current_value(caller, "typeclass"),
actions="|WSelect with |w<num>|W. Other actions: "
"|we|Wxamine |w<num>|W, |wr|Wemove selection",
)
helptext = """
A |nTypeclass|n is specified by the actual python-path to the class definition in the
Evennia code structure.
Which |cAttributes|n, |cLocks|n and other properties have special
effects or expects certain values depend greatly on the code in play.
"""
text = (text, helptext)
options = _wizard_options("typeclass", "prototype_parent", "key", color="|W")
options.append({"key": "_default", "goto": _typeclass_actions})
return text, options
# key node
[docs]def node_key(caller):
text = """
The |cKey|n is the given name of the object to spawn. This will retain the given case.
{current}
""".format(
current=_get_current_value(caller, "key")
)
helptext = """
The key should often not be identical for every spawned object. Using a randomising
$protfunc can be used, for example |c$choice(Alan, Tom, John)|n will give one of the three
names every time an object of this prototype is spawned.
|c$protfuncs|n
{pfuncs}
""".format(
pfuncs=_format_protfuncs()
)
text = (text, helptext)
options = _wizard_options("key", "typeclass", "aliases")
options.append(
{
"key": "_default",
"goto": (_set_property, dict(prop="key", processor=lambda s: s.strip())),
}
)
return text, options
# aliases node
def _all_aliases(caller):
"Get aliases in prototype"
prototype = _get_menu_prototype(caller)
return prototype.get("aliases", [])
def _aliases_select(caller, alias):
"Add numbers as aliases"
aliases = _all_aliases(caller)
try:
ind = str(aliases.index(alias) + 1)
if ind not in aliases:
aliases.append(ind)
_set_prototype_value(caller, "aliases", aliases)
caller.msg("Added alias '{}'.".format(ind))
except (IndexError, ValueError) as err:
caller.msg("Error: {}".format(err))
return "node_aliases"
def _aliases_actions(caller, raw_inp, **kwargs):
"""Parse actions for aliases listing"""
choices = kwargs.get("available_choices", [])
alias, action = _default_parse(raw_inp, choices, ("remove", "r", "delete", "d"))
aliases = _all_aliases(caller)
if alias and action == "remove":
try:
aliases.remove(alias)
_set_prototype_value(caller, "aliases", aliases)
caller.msg("Removed alias '{}'.".format(alias))
except ValueError:
caller.msg("No matching alias found to remove.")
else:
# if not a valid remove, add as a new alias
alias = raw_inp.lower().strip()
if alias and alias not in aliases:
aliases.append(alias)
_set_prototype_value(caller, "aliases", aliases)
caller.msg("Added alias '{}'.".format(alias))
else:
caller.msg("Alias '{}' was already set.".format(alias))
return "node_aliases"
@list_node(_all_aliases, _aliases_select)
def node_aliases(caller):
text = """
|cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not
case sensitive.
{current}
""".format(
current=_get_current_value(
caller,
"aliases",
comparer=lambda propval, flatval: [al for al in flatval if al not in propval],
formatter=lambda lst: "\n" + ", ".join(lst),
only_inherit=True,
)
)
_set_actioninfo(
caller, _format_list_actions("remove", prefix="|w<text>|W to add new alias. Other action: ")
)
helptext = """
Aliases are fixed alternative identifiers and are stored with the new object.
|c$protfuncs|n
{pfuncs}
""".format(
pfuncs=_format_protfuncs()
)
text = (text, helptext)
options = _wizard_options("aliases", "key", "attrs")
options.append({"key": "_default", "goto": _aliases_actions})
return text, options
# attributes node
def _caller_attrs(caller):
prototype = _get_menu_prototype(caller)
attrs = [
"{}={}".format(tup[0], utils.crop(utils.to_str(tup[1]), width=10))
for tup in prototype.get("attrs", [])
]
return attrs
def _get_tup_by_attrname(caller, attrname):
prototype = _get_menu_prototype(caller)
attrs = prototype.get("attrs", [])
try:
inp = [tup[0] for tup in attrs].index(attrname)
return attrs[inp]
except ValueError:
return None
def _display_attribute(attr_tuple):
"""Pretty-print attribute tuple"""
attrkey, value, category, locks = attr_tuple
value = protlib.protfunc_parser(value)
typ = type(value)
out = "{attrkey} |c=|n {value} |W({typ}{category}{locks})|n".format(
attrkey=attrkey,
value=value,
typ=typ,
category=", category={}".format(category) if category else "",
locks=", locks={}".format(";".join(locks)) if any(locks) else "",
)
return out
def _add_attr(caller, attr_string, **kwargs):
"""
Add new attribute, parsing input.
Args:
caller (Object): Caller of menu.
attr_string (str): Input from user
attr is entered on these forms
attr = value
attr;category = value
attr;category;lockstring = value
Keyword Args:
delete (str): If this is set, attr_string is
considered the name of the attribute to delete and
no further parsing happens.
Returns:
result (str): Result string of action.
"""
attrname = ""
value = ""
category = None
locks = ""
if "delete" in kwargs:
attrname = attr_string.lower().strip()
elif "=" in attr_string:
attrname, value = (part.strip() for part in attr_string.split("=", 1))
attrname = attrname.lower()
nameparts = attrname.split(";", 2)
nparts = len(nameparts)
if nparts == 2:
attrname, category = nameparts
elif nparts > 2:
attrname, category, locks = nameparts
attr_tuple = (attrname, value, category, str(locks))
if attrname:
prot = _get_menu_prototype(caller)
attrs = prot.get("attrs", [])
if "delete" in kwargs:
try:
ind = [tup[0] for tup in attrs].index(attrname)
del attrs[ind]
_set_prototype_value(caller, "attrs", attrs)
return "Removed Attribute '{}'".format(attrname)
except IndexError:
return "Attribute to delete not found."
try:
# replace existing attribute with the same name in the prototype
ind = [tup[0] for tup in attrs].index(attrname)
attrs[ind] = attr_tuple
text = "Edited Attribute '{}'.".format(attrname)
except ValueError:
attrs.append(attr_tuple)
text = "Added Attribute " + _display_attribute(attr_tuple)
_set_prototype_value(caller, "attrs", attrs)
else:
text = "Attribute must be given as 'attrname[;category;locks] = <value>'."
return text
def _attr_select(caller, attrstr):
attrname, _ = attrstr.split("=", 1)
attrname = attrname.strip()
attr_tup = _get_tup_by_attrname(caller, attrname)
if attr_tup:
return ("node_examine_entity", {"text": _display_attribute(attr_tup), "back": "attrs"})
else:
caller.msg("Attribute not found.")
return "node_attrs"
def _attrs_actions(caller, raw_inp, **kwargs):
"""Parse actions for attribute listing"""
choices = kwargs.get("available_choices", [])
attrstr, action = _default_parse(
raw_inp, choices, ("examine", "e"), ("remove", "r", "delete", "d")
)
if attrstr is None:
attrstr = raw_inp
try:
attrname, _ = attrstr.split("=", 1)
except ValueError:
caller.msg("|rNeed to enter the attribute on the form attrname=value.|n")
return "node_attrs"
attrname = attrname.strip()
attr_tup = _get_tup_by_attrname(caller, attrname)
if action and attr_tup:
if action == "examine":
return ("node_examine_entity", {"text": _display_attribute(attr_tup), "back": "attrs"})
elif action == "remove":
res = _add_attr(caller, attrname, delete=True)
caller.msg(res)
else:
res = _add_attr(caller, raw_inp)
caller.msg(res)
return "node_attrs"
@list_node(_caller_attrs, _attr_select)
def node_attrs(caller):
def _currentcmp(propval, flatval):
"match by key + category"
cmp1 = [(tup[0].lower(), tup[2].lower() if tup[2] else None) for tup in propval]
return [
tup
for tup in flatval
if (tup[0].lower(), tup[2].lower() if tup[2] else None) not in cmp1
]
text = """
|cAttributes|n are custom properties of the object. Enter attributes on one of these forms:
attrname=value
attrname;category=value
attrname;category;lockstring=value
To give an attribute without a category but with a lockstring, leave that spot empty
(attrname;;lockstring=value). Attribute values can have embedded $protfuncs.
{current}
""".format(
current=_get_current_value(
caller,
"attrs",
comparer=_currentcmp,
formatter=lambda lst: "\n" + "\n".join(_display_attribute(tup) for tup in lst),
only_inherit=True,
)
)
_set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: "))
helptext = """
Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types
'attredit' and 'attrread' are used to limit editing and viewing of the Attribute. Putting
the lock-type `attrcreate` in the |clocks|n prototype key can be used to restrict builders
from adding new Attributes.
|c$protfuncs
{pfuncs}
""".format(
pfuncs=_format_protfuncs()
)
text = (text, helptext)
options = _wizard_options("attrs", "aliases", "tags")
options.append({"key": "_default", "goto": _attrs_actions})
return text, options
# tags node
def _caller_tags(caller):
prototype = _get_menu_prototype(caller)
tags = [tup[0] for tup in prototype.get("tags", [])]
return tags
def _get_tup_by_tagname(caller, tagname):
prototype = _get_menu_prototype(caller)
tags = prototype.get("tags", [])
try:
inp = [tup[0] for tup in tags].index(tagname)
return tags[inp]
except ValueError:
return None
def _display_tag(tag_tuple):
"""Pretty-print tag tuple"""
tagkey, category, data = tag_tuple
out = "Tag: '{tagkey}' (category: {category}{dat})".format(
tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else ""
)
return out
def _add_tag(caller, tag_string, **kwargs):
"""
Add tags to the system, parsing input
Args:
caller (Object): Caller of menu.
tag_string (str): Input from user on one of these forms
tagname
tagname;category
tagname;category;data
Keyword Args:
delete (str): If this is set, tag_string is considered
the name of the tag to delete.
Returns:
result (str): Result string of action.
"""
tag = tag_string.strip().lower()
category = None
data = ""
if "delete" in kwargs:
tag = tag_string.lower().strip()
else:
nameparts = tag.split(";", 2)
ntuple = len(nameparts)
if ntuple == 2:
tag, category = nameparts
elif ntuple > 2:
tag, category, data = nameparts[:3]
tag_tuple = (tag.lower(), category.lower() if category else None, data)
if tag:
prot = _get_menu_prototype(caller)
tags = prot.get("tags", [])
old_tag = _get_tup_by_tagname(caller, tag)
if "delete" in kwargs:
if old_tag:
tags.pop(tags.index(old_tag))
text = "Removed Tag '{}'.".format(tag)
else:
text = "Found no Tag to remove."
elif not old_tag:
# a fresh, new tag
tags.append(tag_tuple)
text = "Added Tag '{}'".format(tag)
else:
# old tag exists; editing a tag means replacing old with new
ind = tags.index(old_tag)
tags[ind] = tag_tuple
text = "Edited Tag '{}'".format(tag)
_set_prototype_value(caller, "tags", tags)
else:
text = "Tag must be given as 'tag[;category;data]'."
return text
def _tag_select(caller, tagname):
tag_tup = _get_tup_by_tagname(caller, tagname)
if tag_tup:
return "node_examine_entity", {"text": _display_tag(tag_tup), "back": "attrs"}
else:
caller.msg("Tag not found.")
return "node_attrs"
def _tags_actions(caller, raw_inp, **kwargs):
"""Parse actions for tags listing"""
choices = kwargs.get("available_choices", [])
tagname, action = _default_parse(
raw_inp, choices, ("examine", "e"), ("remove", "r", "delete", "d")
)
if tagname is None:
tagname = raw_inp.lower().strip()
tag_tup = _get_tup_by_tagname(caller, tagname)
if tag_tup:
if action == "examine":
return ("node_examine_entity", {"text": _display_tag(tag_tup), "back": "tags"})
elif action == "remove":
res = _add_tag(caller, tagname, delete=True)
caller.msg(res)
else:
res = _add_tag(caller, raw_inp)
caller.msg(res)
return "node_tags"
@list_node(_caller_tags, _tag_select)
def node_tags(caller):
def _currentcmp(propval, flatval):
"match by key + category"
cmp1 = [(tup[0].lower(), tup[1].lower() if tup[2] else None) for tup in propval]
return [
tup
for tup in flatval
if (tup[0].lower(), tup[1].lower() if tup[1] else None) not in cmp1
]
text = """
|cTags|n are used to group objects so they can quickly be found later. Enter tags on one of
the following forms:
tagname
tagname;category
tagname;category;data
{current}
""".format(
current=_get_current_value(
caller,
"tags",
comparer=_currentcmp,
formatter=lambda lst: "\n" + "\n".join(_display_tag(tup) for tup in lst),
only_inherit=True,
)
)
_set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: "))
helptext = """
Tags are shared between all objects with that tag. So the 'data' field (which is not
commonly used) can only hold eventual info about the Tag itself, not about the individual
object on which it sits.
All objects created with this prototype will automatically get assigned a tag named the same
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.
""".format(
tag_category=protlib.PROTOTYPE_TAG_CATEGORY
)
text = (text, helptext)
options = _wizard_options("tags", "attrs", "locks")
options.append({"key": "_default", "goto": _tags_actions})
return text, options
# locks node
def _caller_locks(caller):
locks = _get_menu_prototype(caller).get("locks", "")
return [lck for lck in locks.split(";") if lck]
def _locks_display(caller, lock):
return lock
def _lock_select(caller, lockstr):
return ("node_examine_entity", {"text": _locks_display(caller, lockstr), "back": "locks"})
def _lock_add(caller, lock, **kwargs):
locks = _caller_locks(caller)
try:
locktype, lockdef = lock.split(":", 1)
except ValueError:
return "Lockstring lacks ':'."
locktype = locktype.strip().lower()
if "delete" in kwargs:
try:
ind = locks.index(lock)
locks.pop(ind)
_set_prototype_value(caller, "locks", ";".join(locks), parse=False)
ret = "Lock {} deleted.".format(lock)
except ValueError:
ret = "No lock found to delete."
return ret
try:
locktypes = [lck.split(":", 1)[0].strip().lower() for lck in locks]
ind = locktypes.index(locktype)
locks[ind] = lock
ret = "Lock with locktype '{}' updated.".format(locktype)
except ValueError:
locks.append(lock)
ret = "Added lock '{}'.".format(lock)
_set_prototype_value(caller, "locks", ";".join(locks))
return ret
def _locks_actions(caller, raw_inp, **kwargs):
choices = kwargs.get("available_choices", [])
lock, action = _default_parse(
raw_inp, choices, ("examine", "e"), ("remove", "r", "delete", "d")
)
if lock:
if action == "examine":
return ("node_examine_entity", {"text": _locks_display(caller, lock), "back": "locks"})
elif action == "remove":
ret = _lock_add(caller, lock, delete=True)
caller.msg(ret)
else:
ret = _lock_add(caller, raw_inp)
caller.msg(ret)
return "node_locks"
@list_node(_caller_locks, _lock_select)
def node_locks(caller):
def _currentcmp(propval, flatval):
"match by locktype"
cmp1 = [lck.split(":", 1)[0] for lck in propval.split(";")]
return ";".join(lstr for lstr in flatval.split(";") if lstr.split(":", 1)[0] not in cmp1)
text = """
The |cLock string|n defines limitations for accessing various properties of the object once
it's spawned. The string should be on one of the following forms:
locktype:[NOT] lockfunc(args)
locktype: [NOT] lockfunc(args) [AND|OR|NOT] lockfunc(args) [AND|OR|NOT] ...
{current}{action}
""".format(
current=_get_current_value(
caller,
"locks",
comparer=_currentcmp,
formatter=lambda lockstr: "\n".join(
_locks_display(caller, lstr) for lstr in lockstr.split(";")
),
only_inherit=True,
),
action=_format_list_actions("examine", "remove", prefix="Actions: "),
)
helptext = """
Here is an example of two lock strings:
edit:false()
call:tag(Foo) OR perm(Builder)
Above locks limit two things, 'edit' and 'call'. Which lock types are actually checked
depend on the typeclass of the object being spawned. Here 'edit' is never allowed by anyone
while 'call' is allowed to all accessors with a |ctag|n 'Foo' OR which has the
|cPermission|n 'Builder'.
|cAvailable lockfuncs:|n
{lfuncs}
""".format(
lfuncs=_format_lockfuncs()
)
text = (text, helptext)
options = _wizard_options("locks", "tags", "permissions")
options.append({"key": "_default", "goto": _locks_actions})
return text, options
# permissions node
def _caller_permissions(caller):
prototype = _get_menu_prototype(caller)
perms = prototype.get("permissions", [])
return perms
def _display_perm(caller, permission, only_hierarchy=False):
hierarchy = settings.PERMISSION_HIERARCHY
perm_low = permission.lower()
txt = ""
if perm_low in [prm.lower() for prm in hierarchy]:
txt = "Permission (in hieararchy): {}".format(
", ".join(
[
"|w[{}]|n".format(prm) if prm.lower() == perm_low else "|W{}|n".format(prm)
for prm in hierarchy
]
)
)
elif not only_hierarchy:
txt = "Permission: '{}'".format(permission)
return txt
def _permission_select(caller, permission, **kwargs):
return (
"node_examine_entity",
{"text": _display_perm(caller, permission), "back": "permissions"},
)
def _add_perm(caller, perm, **kwargs):
if perm:
perm_low = perm.lower()
perms = _caller_permissions(caller)
perms_low = [prm.lower() for prm in perms]
if "delete" in kwargs:
try:
ind = perms_low.index(perm_low)
del perms[ind]
text = "Removed Permission '{}'.".format(perm)
except ValueError:
text = "Found no Permission to remove."
else:
if perm_low in perms_low:
text = "Permission already set."
else:
perms.append(perm)
_set_prototype_value(caller, "permissions", perms)
text = "Added Permission '{}'".format(perm)
return text
def _permissions_actions(caller, raw_inp, **kwargs):
"""Parse actions for permission listing"""
choices = kwargs.get("available_choices", [])
perm, action = _default_parse(
raw_inp, choices, ("examine", "e"), ("remove", "r", "delete", "d")
)
if perm:
if action == "examine":
return (
"node_examine_entity",
{"text": _display_perm(caller, perm), "back": "permissions"},
)
elif action == "remove":
res = _add_perm(caller, perm, delete=True)
caller.msg(res)
else:
res = _add_perm(caller, raw_inp.strip())
caller.msg(res)
return "node_permissions"
@list_node(_caller_permissions, _permission_select)
def node_permissions(caller):
def _currentcmp(pval, fval):
cmp1 = [perm.lower() for perm in pval]
return [perm for perm in fval if perm.lower() not in cmp1]
text = """
|cPermissions|n are simple strings used to grant access to this object. A permission is used
when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions. Certain
permissions belong in the |cpermission hierarchy|n together with the |Wperm()|n lock
function.
{current}
""".format(
current=_get_current_value(
caller,
"permissions",
comparer=_currentcmp,
formatter=lambda lst: "\n" + "\n".join(prm for prm in lst),
only_inherit=True,
)
)
_set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: "))
helptext = """
Any string can act as a permission as long as a lock is set to look for it. Depending on the
lock, having a permission could even be negative (i.e. the lock is only passed if you
|wdon't|n have the 'permission'). The most common permissions are the hierarchical
permissions:
{permissions}.
For example, a |clock|n string like "edit:perm(Builder)" will grant access to accessors
having the |cpermission|n "Builder" or higher.
""".format(
permissions=", ".join(settings.PERMISSION_HIERARCHY)
)
text = (text, helptext)
options = _wizard_options("permissions", "locks", "location")
options.append({"key": "_default", "goto": _permissions_actions})
return text, options
# location node
[docs]def node_location(caller):
text = """
The |cLocation|n of this object in the world. If not given, the object will spawn in the
inventory of |c{caller}|n by default.
{current}
""".format(
caller=caller.key, current=_get_current_value(caller, "location")
)
helptext = """
You get the most control by not specifying the location - you can then teleport the spawned
objects as needed later. Setting the location may be useful for quickly populating a given
location. One could also consider randomizing the location using a $protfunc.
|c$protfuncs|n
{pfuncs}
""".format(
pfuncs=_format_protfuncs()
)
text = (text, helptext)
options = _wizard_options("location", "permissions", "home", search=True)
options.append(
{
"key": "_default",
"goto": (_set_property, dict(prop="location", processor=lambda s: s.strip())),
}
)
return text, options
# home node
[docs]def node_home(caller):
text = """
The |cHome|n location of an object is often only used as a backup - this is where the object
will be moved to if its location is deleted. The home location can also be used as an actual
home for characters to quickly move back to.
If unset, the global home default (|w{default}|n) will be used.
{current}
""".format(
default=settings.DEFAULT_HOME, current=_get_current_value(caller, "home")
)
helptext = """
The home can be given as a #dbref but can also be specified using the protfunc
'$obj(name)'. Use |wSE|nearch to find objects in the database.
The home location is commonly not used except as a backup; using the global default is often
enough.
|c$protfuncs|n
{pfuncs}
""".format(
pfuncs=_format_protfuncs()
)
text = (text, helptext)
options = _wizard_options("home", "location", "destination", search=True)
options.append(
{
"key": "_default",
"goto": (_set_property, dict(prop="home", processor=lambda s: s.strip())),
}
)
return text, options
# destination node
[docs]def node_destination(caller):
text = """
The object's |cDestination|n is generally only used by Exit-like objects to designate where
the exit 'leads to'. It's usually unset for all other types of objects.
{current}
""".format(
current=_get_current_value(caller, "destination")
)
helptext = """
The destination can be given as a #dbref but can also be specified using the protfunc
'$obj(name)'. Use |wSEearch to find objects in the database.
|c$protfuncs|n
{pfuncs}
""".format(
pfuncs=_format_protfuncs()
)
text = (text, helptext)
options = _wizard_options("destination", "home", "prototype_desc", search=True)
options.append(
{
"key": "_default",
"goto": (_set_property, dict(prop="destination", processor=lambda s: s.strip())),
}
)
return text, options
# prototype_desc node
[docs]def node_prototype_desc(caller):
text = """
The |cPrototype-Description|n briefly describes the prototype when it's viewed in listings.
{current}
""".format(
current=_get_current_value(caller, "prototype_desc")
)
helptext = """
Giving a brief description helps you and others to locate the prototype for use later.
"""
text = (text, helptext)
options = _wizard_options("prototype_desc", "prototype_key", "prototype_tags")
options.append(
{
"key": "_default",
"goto": (
_set_property,
dict(
prop="prototype_desc",
processor=lambda s: s.strip(),
next_node="node_prototype_desc",
),
),
}
)
return text, options
# prototype_tags node
def _caller_prototype_tags(caller):
prototype = _get_menu_prototype(caller)
tags = prototype.get("prototype_tags", [])
tags = [tag[0] if isinstance(tag, tuple) else tag for tag in tags]
return tags
def _add_prototype_tag(caller, tag_string, **kwargs):
"""
Add prototype_tags to the system. We only support straight tags, no
categories (category is assigned automatically).
Args:
caller (Object): Caller of menu.
tag_string (str): Input from user - only tagname
Keyword Args:
delete (str): If this is set, tag_string is considered
the name of the tag to delete.
Returns:
result (str): Result string of action.
"""
tag = tag_string.strip().lower()
if tag:
tags = _caller_prototype_tags(caller)
exists = tag in tags
if "delete" in kwargs:
if exists:
tags.pop(tags.index(tag))
text = "Removed Prototype-Tag '{}'.".format(tag)
else:
text = "Found no Prototype-Tag to remove."
elif not exists:
# a fresh, new tag
tags.append(tag)
text = "Added Prototype-Tag '{}'.".format(tag)
else:
text = "Prototype-Tag already added."
_set_prototype_value(caller, "prototype_tags", tags)
else:
text = "No Prototype-Tag specified."
return text
def _prototype_tag_select(caller, tagname):
caller.msg("Prototype-Tag: {}".format(tagname))
return "node_prototype_tags"
def _prototype_tags_actions(caller, raw_inp, **kwargs):
"""Parse actions for tags listing"""
choices = kwargs.get("available_choices", [])
tagname, action = _default_parse(raw_inp, choices, ("remove", "r", "delete", "d"))
if tagname:
if action == "remove":
res = _add_prototype_tag(caller, tagname, delete=True)
caller.msg(res)
else:
res = _add_prototype_tag(caller, raw_inp.lower().strip())
caller.msg(res)
return "node_prototype_tags"
@list_node(_caller_prototype_tags, _prototype_tag_select)
def node_prototype_tags(caller):
text = """
|cPrototype-Tags|n can be used to classify and find prototypes in listings Tag names are not
case-sensitive and can have not have a custom category.
{current}
""".format(
current=_get_current_value(
caller,
"prototype_tags",
formatter=lambda lst: ", ".join(tg for tg in lst),
only_inherit=True,
)
)
_set_actioninfo(
caller, _format_list_actions("remove", prefix="|w<text>|n|W to add Tag. Other Action:|n ")
)
helptext = """
Using prototype-tags is a good way to organize and group large numbers of prototypes by
genre, type etc. Under the hood, prototypes' tags will all be stored with the category
'{tagmetacategory}'.
""".format(
tagmetacategory=protlib._PROTOTYPE_TAG_META_CATEGORY
)
text = (text, helptext)
options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks")
options.append({"key": "_default", "goto": _prototype_tags_actions})
return text, options
# prototype_locks node
def _caller_prototype_locks(caller):
locks = _get_menu_prototype(caller).get("prototype_locks", "")
return [lck for lck in locks.split(";") if lck]
def _prototype_lock_select(caller, lockstr):
return (
"node_examine_entity",
{"text": _locks_display(caller, lockstr), "back": "prototype_locks"},
)
def _prototype_lock_add(caller, lock, **kwargs):
locks = _caller_prototype_locks(caller)
try:
locktype, lockdef = lock.split(":", 1)
except ValueError:
return "Lockstring lacks ':'."
locktype = locktype.strip().lower()
if "delete" in kwargs:
try:
ind = locks.index(lock)
locks.pop(ind)
_set_prototype_value(caller, "prototype_locks", ";".join(locks), parse=False)
ret = "Prototype-lock {} deleted.".format(lock)
except ValueError:
ret = "No Prototype-lock found to delete."
return ret
try:
locktypes = [lck.split(":", 1)[0].strip().lower() for lck in locks]
ind = locktypes.index(locktype)
locks[ind] = lock
ret = "Prototype-lock with locktype '{}' updated.".format(locktype)
except ValueError:
locks.append(lock)
ret = "Added Prototype-lock '{}'.".format(lock)
_set_prototype_value(caller, "prototype_locks", ";".join(locks))
return ret
def _prototype_locks_actions(caller, raw_inp, **kwargs):
choices = kwargs.get("available_choices", [])
lock, action = _default_parse(
raw_inp, choices, ("examine", "e"), ("remove", "r", "delete", "d")
)
if lock:
if action == "examine":
return ("node_examine_entity", {"text": _locks_display(caller, lock), "back": "locks"})
elif action == "remove":
ret = _prototype_lock_add(caller, lock.strip(), delete=True)
caller.msg(ret)
else:
ret = _prototype_lock_add(caller, raw_inp.strip())
caller.msg(ret)
return "node_prototype_locks"
@list_node(_caller_prototype_locks, _prototype_lock_select)
def node_prototype_locks(caller):
text = """
|cPrototype-Locks|n are used to limit access to this prototype when someone else is trying
to access it. By default any prototype can be edited only by the creator and by Admins while
they can be used by anyone with access to the spawn command. There are two valid lock types
the prototype access tools look for:
- 'edit': Who can edit the prototype.
- 'spawn': Who can spawn new objects with this prototype.
If unsure, keep the open defaults.
{current}
""".format(
current=_get_current_value(
caller,
"prototype_locks",
formatter=lambda lstring: "\n".join(
_locks_display(caller, lstr) for lstr in lstring.split(";")
),
only_inherit=True,
)
)
_set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: "))
helptext = """
Prototype locks can be used to vary access for different tiers of builders. It also allows
developers to produce 'base prototypes' only meant for builders to inherit and expand on
rather than tweak in-place.
"""
text = (text, helptext)
options = _wizard_options("prototype_locks", "prototype_tags", "index")
options.append({"key": "_default", "goto": _prototype_locks_actions})
return text, options
# update existing objects node
def _apply_diff(caller, **kwargs):
"""update existing objects"""
prototype = kwargs["prototype"]
objects = kwargs["objects"]
back_node = kwargs["back_node"]
diff = kwargs.get("diff", None)
num_changed = spawner.batch_update_objects_with_prototype(
prototype, diff=diff, objects=objects, caller=caller
)
caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed))
return back_node
def _keep_diff(caller, **kwargs):
"""Change to KEEP setting for a given section of a diff"""
# from evennia import set_trace;set_trace(term_size=(182, 50))
path = kwargs["path"]
diff = kwargs["diff"]
tmp = diff
for key in path[:-1]:
tmp = tmp[key]
tmp[path[-1]] = tuple(list(tmp[path[-1]][:-1]) + ["KEEP"])
def _format_diff_text_and_options(diff, minimal=True, **kwargs):
"""
Reformat the diff in a way suitable for the olc menu.
Args:
diff (dict): A diff as produced by `prototype_diff`.
minimal (bool, optional): Don't show KEEPs.
Keyword Args:
any (any): Forwarded into the generated options as arguments to the callable.
Returns:
texts (list): List of texts.
options (list): List of options dict.
"""
valid_instructions = ("KEEP", "REMOVE", "ADD", "UPDATE")
def _visualize(obj, rootname, get_name=False):
if utils.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 {}|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, optnum, *args):
typ = type(diffpart)
texts = []
options = []
if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
rootname = args[0]
old, new, instruction = diffpart
if instruction == "KEEP":
if not minimal:
texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old, rootname)))
else:
# instructions we should be able to revert by a menu choice
vold = _visualize(old, rootname)
vnew = _visualize(new, rootname)
vsep = "" if len(vold) < 78 else "\n"
if instruction == "ADD":
texts.append(
" |c[{optnum}] |yADD|n: {new}".format(
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(
{
"key": str(optnum),
"desc": "|gKEEP|n ({}) {}".format(
rootname, _visualize(old, args[-1], get_name=True)
),
"goto": (_keep_diff, dict((("path", args), ("diff", diff)), **kwargs)),
}
)
optnum += 1
else:
for key in sorted(list(diffpart.keys())):
subdiffpart = diffpart[key]
text, option, optnum = _parse_diffpart(subdiffpart, optnum, *(args + (key,)))
texts.extend(text)
options.extend(option)
return texts, options, optnum
texts = []
options = []
# we use this to allow for skipping full KEEP instructions
optnum = 1
for root_key in sorted(diff):
diffpart = diff[root_key]
text, option, optnum = _parse_diffpart(diffpart, optnum, root_key)
heading = "- |w{}:|n ".format(root_key)
if text:
text = [heading + text[0]] + text[1:]
else:
text = [heading]
texts.extend(text)
options.extend(option)
return texts, options
[docs]def node_apply_diff(caller, **kwargs):
"""Offer options for updating objects"""
def _keep_option(keyname, prototype, base_obj, obj_prototype, diff, objects, back_node):
"""helper returning an option dict"""
options = {
"desc": "Keep {} as-is".format(keyname),
"goto": (
_keep_diff,
{
"key": keyname,
"prototype": prototype,
"base_obj": base_obj,
"obj_prototype": obj_prototype,
"diff": diff,
"objects": objects,
"back_node": back_node,
},
),
}
return options
prototype = kwargs.get("prototype", None)
update_objects = kwargs.get("objects", None)
back_node = kwargs.get("back_node", "node_index")
obj_prototype = kwargs.get("obj_prototype", None)
base_obj = kwargs.get("base_obj", None)
diff = kwargs.get("diff", None)
custom_location = kwargs.get("custom_location", None)
if not update_objects:
text = "There are no existing objects to update."
options = {"key": "_default", "goto": back_node}
return text, options
if not diff:
# use one random object as a reference to calculate a diff
base_obj = choice(update_objects)
diff, obj_prototype = spawner.prototype_diff_from_object(prototype, base_obj)
helptext = """
This will go through all existing objects and apply the changes you accept.
Be careful with this operation! The upgrade mechanism will try to automatically estimate
what changes need to be applied. But the estimate is |wonly based on the analysis of one
randomly selected object|n among all objects spawned by this prototype. If that object
happens to be unusual in some way the estimate will be off and may lead to unexpected
results for other objects. Always test your objects carefully after an upgrade and consider
being conservative (switch to KEEP) for things you are unsure of. For complex upgrades it
may be better to get help from an administrator with access to the `@py` command for doing
this manually.
Note that the `location` will never be auto-adjusted because it's so rare to want to
homogenize the location of all object instances."""
if not custom_location:
diff.pop("location", None)
txt, options = _format_diff_text_and_options(
diff, objects=update_objects, base_obj=base_obj, prototype=prototype
)
if options:
text = [
"Suggested changes to {} objects. ".format(len(update_objects)),
"Showing random example obj to change: {name} ({dbref}))\n".format(
name=base_obj.key, dbref=base_obj.dbref
),
] + txt
options.extend(
[
{
"key": ("|wu|Wpdate {} objects".format(len(update_objects)), "update", "u"),
"desc": "Update {} objects".format(len(update_objects)),
"goto": (
_apply_diff,
{
"prototype": prototype,
"objects": update_objects,
"back_node": back_node,
"diff": diff,
"base_obj": base_obj,
},
),
},
{
"key": ("|wr|Weset changes", "reset", "r"),
"goto": (
"node_apply_diff",
{"prototype": prototype, "back_node": back_node, "objects": update_objects},
),
},
]
)
else:
text = [
"Analyzed a random sample object (out of {}) - "
"found no changes to apply.".format(len(update_objects))
]
options.extend(_wizard_options("update_objects", back_node[5:], None))
options.append({"key": "_default", "goto": back_node})
text = "\n".join(text)
text = (text, helptext)
return text, options
# prototype save node
[docs]def node_prototype_save(caller, **kwargs):
"""Save prototype to disk"""
# these are only set if we selected 'yes' to save on a previous pass
prototype = kwargs.get("prototype", None)
# set to True/False if answered, None if first pass
accept_save = kwargs.get("accept_save", None)
if accept_save and prototype:
# we already validated and accepted the save, so this node acts as a goto callback and
# should now only return the next node
prototype_key = prototype.get("prototype_key")
try:
protlib.save_prototype(prototype)
except Exception as exc:
text = "|rCould not save:|n {}\n(press Return to continue)".format(exc)
options = {"key": "_default", "goto": "node_index"}
return text, options
spawned_objects = protlib.search_objects_with_prototype(prototype_key)
nspawned = spawned_objects.count()
text = ["|gPrototype saved.|n"]
if nspawned:
text.append(
"\nDo you want to update {} object(s) "
"already using this prototype?".format(nspawned)
)
options = (
{
"key": ("|wY|Wes|n", "yes", "y"),
"desc": "Go to updating screen",
"goto": (
"node_apply_diff",
{
"accept_update": True,
"objects": spawned_objects,
"prototype": prototype,
"back_node": "node_prototype_save",
},
),
},
{"key": ("[|wN|Wo|n]", "n"), "desc": "Return to index", "goto": "node_index"},
{"key": "_default", "goto": "node_index"},
)
else:
text.append("(press Return to continue)")
options = {"key": "_default", "goto": "node_index"}
text = "\n".join(text)
helptext = """
Updating objects means that the spawner will find all objects previously created by this
prototype. You will be presented with a list of the changes the system will try to apply to
each of these objects and you can choose to customize that change if needed. If you have
done a lot of manual changes to your objects after spawning, you might want to update those
objects manually instead.
"""
text = (text, helptext)
return text, options
# not validated yet
prototype = _get_menu_prototype(caller)
error, text = _validate_prototype(prototype)
text = [text]
if error:
# abort save
text.append(
"\n|yValidation errors were found. They need to be corrected before this prototype "
"can be saved (or used to spawn).|n"
)
options = _wizard_options("prototype_save", "index", None)
options.append({"key": "_default", "goto": "node_index"})
return "\n".join(text), options
prototype_key = prototype["prototype_key"]
if protlib.search_prototype(prototype_key):
text.append(
"\nDo you want to save/overwrite the existing prototype '{name}'?".format(
name=prototype_key
)
)
else:
text.append("\nDo you want to save the prototype as '{name}'?".format(name=prototype_key))
text = "\n".join(text)
helptext = """
Saving the prototype makes it available for use later. It can also be used to inherit from,
by name. Depending on |cprototype-locks|n it also makes the prototype usable and/or
editable by others. Consider setting good |cPrototype-tags|n and to give a useful, brief
|cPrototype-desc|n to make the prototype easy to find later.
"""
text = (text, helptext)
options = (
{
"key": ("[|wY|Wes|n]", "yes", "y"),
"desc": "Save prototype",
"goto": ("node_prototype_save", {"accept_save": True, "prototype": prototype}),
},
{"key": ("|wN|Wo|n", "n"), "desc": "Abort and return to Index", "goto": "node_index"},
{
"key": "_default",
"goto": ("node_prototype_save", {"accept_save": True, "prototype": prototype}),
},
)
return text, options
# spawning node
def _spawn(caller, **kwargs):
"""Spawn prototype"""
prototype = kwargs["prototype"].copy()
new_location = kwargs.get("location", None)
if new_location:
prototype["location"] = new_location
if not prototype.get("location"):
prototype["location"] = caller
obj = spawner.spawn(prototype, caller=caller)
if obj:
obj = obj[0]
text = "|gNew instance|n {key} ({dbref}) |gspawned at location |n{loc}|n|g.|n".format(
key=obj.key, dbref=obj.dbref, loc=prototype["location"]
)
else:
text = "|rError: Spawner did not return a new instance.|n"
return "node_examine_entity", {"text": text, "back": "prototype_spawn"}
[docs]def node_prototype_spawn(caller, **kwargs):
"""Submenu for spawning the prototype"""
prototype = _get_menu_prototype(caller)
already_validated = kwargs.get("already_validated", False)
if already_validated:
error, text = None, []
else:
error, text = _validate_prototype(prototype)
text = [text]
if error:
text.append("\n|rPrototype validation failed. Correct the errors before spawning.|n")
options = _wizard_options("prototype_spawn", "index", None)
return "\n".join(text), options
text = "\n".join(text)
helptext = """
Spawning is the act of instantiating a prototype into an actual object. As a new object is
spawned, every $protfunc in the prototype is called anew. Since this is a common thing to
do, you may also temporarily change the |clocation|n of this prototype to bypass whatever
value is set in the prototype.
"""
text = (text, helptext)
# show spawn submenu options
options = []
prototype_key = prototype["prototype_key"]
location = prototype.get("location", None)
if location:
options.append(
{
"desc": "Spawn in prototype's defined location ({loc})".format(loc=location),
"goto": (
_spawn,
dict(prototype=prototype, location=location, custom_location=True),
),
}
)
caller_loc = caller.location
if location != caller_loc:
options.append(
{
"desc": "Spawn in {caller}'s location ({loc})".format(
caller=caller, loc=caller_loc
),
"goto": (_spawn, dict(prototype=prototype, location=caller_loc)),
}
)
if location != caller_loc != caller:
options.append(
{
"desc": "Spawn in {caller}'s inventory".format(caller=caller),
"goto": (_spawn, dict(prototype=prototype, location=caller)),
}
)
spawned_objects = protlib.search_objects_with_prototype(prototype_key)
nspawned = spawned_objects.count()
if spawned_objects:
options.append(
{
"desc": "Update {num} existing objects with this prototype".format(num=nspawned),
"goto": (
"node_apply_diff",
{
"objects": list(spawned_objects),
"prototype": prototype,
"back_node": "node_prototype_spawn",
},
),
}
)
options.extend(_wizard_options("prototype_spawn", "index", None))
options.append({"key": "_default", "goto": "node_index"})
return text, options
# prototype load node
def _prototype_load_select(caller, prototype_key, **kwargs):
matches = protlib.search_prototype(key=prototype_key)
if matches:
prototype = matches[0]
_set_menu_prototype(caller, prototype)
return (
"node_examine_entity",
{
"text": "|gLoaded prototype {}.|n".format(prototype["prototype_key"]),
"back": "index",
},
)
else:
caller.msg("|rFailed to load prototype '{}'.".format(prototype_key))
return None
def _prototype_load_actions(caller, raw_inp, **kwargs):
"""Parse the default Convert prototype to a string representation for closer inspection"""
choices = kwargs.get("available_choices", [])
prototype, action = _default_parse(
raw_inp, choices, ("examine", "e", "l"), ("delete", "del", "d")
)
if prototype:
# which action to apply on the selection
if action == "examine":
# examine the prototype
prototype = protlib.search_prototype(key=prototype)[0]
txt = protlib.prototype_to_str(prototype)
return "node_examine_entity", {"text": txt, "back": "prototype_load"}
elif action == "delete":
# delete prototype from disk
try:
protlib.delete_prototype(prototype, caller=caller)
except protlib.PermissionError as err:
txt = "|rDeletion error:|n {}".format(err)
else:
txt = "|gPrototype {} was deleted.|n".format(prototype)
return "node_examine_entity", {"text": txt, "back": "prototype_load"}
return "node_prototype_load"
@list_node(_all_prototype_parents, _prototype_load_select)
def node_prototype_load(caller, **kwargs):
"""Load prototype"""
text = """
Select a prototype to load. This will replace any prototype currently being edited!
"""
_set_actioninfo(caller, _format_list_actions("examine", "delete"))
helptext = """
Loading a prototype will load it and return you to the main index. It can be a good idea
to examine the prototype before loading it.
"""
text = (text, helptext)
options = _wizard_options("prototype_load", "index", None)
options.append({"key": "_default", "goto": _prototype_load_actions})
return text, options
# EvMenu definition, formatting and access functions
[docs]def start_olc(caller, session=None, prototype=None):
"""
Start menu-driven olc system for prototypes.
Args:
caller (Object or Account): The entity starting the menu.
session (Session, optional): The individual session to get data.
prototype (dict, optional): Given when editing an existing
prototype rather than creating a new one.
"""
menudata = {
"node_index": node_index,
"node_validate_prototype": node_validate_prototype,
"node_examine_entity": node_examine_entity,
"node_search_object": node_search_object,
"node_prototype_key": node_prototype_key,
"node_prototype_parent": node_prototype_parent,
"node_typeclass": node_typeclass,
"node_key": node_key,
"node_aliases": node_aliases,
"node_attrs": node_attrs,
"node_tags": node_tags,
"node_locks": node_locks,
"node_permissions": node_permissions,
"node_location": node_location,
"node_home": node_home,
"node_destination": node_destination,
"node_apply_diff": node_apply_diff,
"node_prototype_desc": node_prototype_desc,
"node_prototype_tags": node_prototype_tags,
"node_prototype_locks": node_prototype_locks,
"node_prototype_load": node_prototype_load,
"node_prototype_save": node_prototype_save,
"node_prototype_spawn": node_prototype_spawn,
}
OLCMenu(
caller,
menudata,
startnode="node_index",
session=session,
olc_prototype=prototype,
debug=True,
)