evennia/evennia/prototypes/menus.py

1122 lines
41 KiB
Python

"""
OLC Prototype menu nodes
"""
import json
from random import choice
from django.conf import settings
from evennia.utils.evmenu import EvMenu, list_node
from evennia.utils.ansi import strip_ansi
from evennia.utils import utils
from evennia.prototypes import prototypes as protlib
from evennia.prototypes import spawner
# ------------------------------------------------------------
#
# 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 _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 = "|rrequired"
return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH))
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.
Kwargs:
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", "node_index")
propname_low = prop.strip().lower()
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)
# typeclass and prototype_parent can't co-exist
if propname_low == "typeclass":
prototype.pop("prototype_parent", None)
if propname_low == "prototype_parent":
prototype.pop("typeclass", None)
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 ...")
err, parsed_value = protlib.protfunc_parser(value, testing=True)
if err:
out.append(" |yPython `literal_eval` warning: {}|n".format(err))
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"):
"""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)})
if "index" not in (prev_node, 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})})
return options
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 err:
errors = "\n\n|r{}|n".format(err)
err = True
except RuntimeWarning as err:
errors = "\n\n|y{}|n".format(err)
err = True
text = (txt + errors)
return err, text
# Menu nodes
def node_index(caller):
prototype = _get_menu_prototype(caller)
text = (
"|c --- Prototype wizard --- |n\n\n"
"Define the |yproperties|n of the prototype. All prototype values can be "
"over-ridden at the time of spawning an instance of the prototype, but some are "
"required.\n\n'|wprototype-'-properties|n are not used in the prototype itself but are used "
"to organize and list prototypes. The 'prototype-key' uniquely identifies the prototype "
"and allows you to edit an existing prototype or save a new one for use by you or "
"others later.\n\n(make choice; q to abort. If unsure, start from 1.)")
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 ('Typeclass', 'Prototype_parent', '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": "|w{}|n{}".format(
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": ("|wS|Wave prototype", "save", "s"),
"goto": "node_prototype_save"},
{"key": ("|wSP|Wawn prototype", "spawn", "sp"),
"goto": "node_prototype_spawn"},
{"key": ("|wL|Woad prototype", "load", "l"),
"goto": "node_prototype_load"}))
return text, options
def node_validate_prototype(caller, raw_string, **kwargs):
"""General node to view and validate a protototype"""
prototype = _get_menu_prototype(caller)
prev_node = kwargs.get("back", "index")
_, text = _validate_prototype(prototype)
options = _wizard_options(None, prev_node, None)
return text, options
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', next_node="node_prototype_parent")
def node_prototype_key(caller):
prototype = _get_menu_prototype(caller)
text = ["The prototype name, or |wMeta-Key|n, uniquely identifies the prototype. "
"It is used to find and use the prototype to spawn new entities. "
"It is not case sensitive."]
old_key = prototype.get('prototype_key', None)
if old_key:
text.append("Current key is '|w{key}|n'".format(key=old_key))
else:
text.append("The key is currently unset.")
text.append("Enter text or make a choice (q for quit)")
text = "\n\n".join(text)
options = _wizard_options("prototype_key", "index", "prototype_parent")
options.append({"key": "_default",
"goto": _check_prototype_key})
return text, options
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_examine(caller, prototype_name):
"""Convert prototype to a string representation for closer inspection"""
prototypes = protlib.search_prototype(key=prototype_name)
if prototypes:
ret = protlib.prototype_to_str(prototypes[0])
caller.msg(ret)
return ret
else:
caller.msg("Prototype not registered.")
def _prototype_parent_select(caller, prototype):
ret = _set_property(caller, "",
prop="prototype_parent", processor=str, next_node="node_key")
caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype))
return ret
@list_node(_all_prototype_parents, _prototype_parent_select)
def node_prototype_parent(caller):
prototype = _get_menu_prototype(caller)
prot_parent_key = prototype.get('prototype')
text = ["Set the prototype's |yParent Prototype|n. If this is unset, Typeclass will be used."]
if prot_parent_key:
prot_parent = protlib.search_prototype(prot_parent_key)
if prot_parent:
text.append(
"Current parent prototype is {}:\n{}".format(protlib.prototype_to_str(prot_parent)))
else:
text.append("Current parent prototype |r{prototype}|n "
"does not appear to exist.".format(prot_parent_key))
else:
text.append("Parent prototype is not set")
text = "\n\n".join(text)
options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W")
options.append({"key": "_default",
"goto": _prototype_parent_examine})
return text, options
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_examine(caller, typeclass_path):
"""Show info (docstring) about given typeclass."""
if typeclass_path is None:
# this means we are exiting the listing
return "node_key"
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 |y{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)
caller.msg(txt)
return txt
def _typeclass_select(caller, typeclass):
"""Select typeclass from list and add it to prototype. Return next node to go to."""
ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key")
caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass))
return ret
@list_node(_all_typeclasses, _typeclass_select)
def node_typeclass(caller):
prototype = _get_menu_prototype(caller)
typeclass = prototype.get("typeclass")
text = ["Set the typeclass's parent |yTypeclass|n."]
if typeclass:
text.append("Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass))
else:
text.append("Using default typeclass {typeclass}.".format(
typeclass=settings.BASE_OBJECT_TYPECLASS))
text = "\n\n".join(text)
options = _wizard_options("typeclass", "prototype_parent", "key", color="|W")
options.append({"key": "_default",
"goto": _typeclass_examine})
return text, options
def node_key(caller):
prototype = _get_menu_prototype(caller)
key = prototype.get("key")
text = ["Set the prototype's name (|yKey|n.) This will retain case sensitivity."]
if key:
text.append("Current key value is '|y{key}|n'.".format(key=key))
else:
text.append("Key is currently unset.")
text = "\n\n".join(text)
options = _wizard_options("key", "typeclass", "aliases")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="key",
processor=lambda s: s.strip(),
next_node="node_aliases"))})
return text, options
def node_aliases(caller):
prototype = _get_menu_prototype(caller)
aliases = prototype.get("aliases")
text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. "
"they'll retain case sensitivity."]
if aliases:
text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases))
else:
text.append("No aliases are set.")
text = "\n\n".join(text)
options = _wizard_options("aliases", "key", "attrs")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="aliases",
processor=lambda s: [part.strip() for part in s.split(",")],
next_node="node_attrs"))})
return text, options
def _caller_attrs(caller):
prototype = _get_menu_prototype(caller)
attrs = prototype.get("attrs", [])
return attrs
def _display_attribute(attr_tuple):
"""Pretty-print attribute tuple"""
attrkey, value, category, locks = attr_tuple
value = protlib.protfunc_parser(value)
typ = type(value)
out = ("Attribute key: '{attrkey}' (category: {category}, "
"locks: {locks})\n"
"Value (parsed to {typ}): {value}").format(
attrkey=attrkey,
category=category, locks=locks,
typ=typ, value=value)
return out
def _add_attr(caller, attr_string, **kwargs):
"""
Add new attrubute, parsing input.
attr is entered on these forms
attr = value
attr;category = value
attr;category;lockstring = value
"""
attrname = ''
category = None
locks = ''
if '=' 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, locks)
if attrname:
prot = _get_menu_prototype(caller)
attrs = prot.get('attrs', [])
try:
# replace existing attribute with the same name in the prototype
ind = [tup[0] for tup in attrs].index(attrname)
attrs[ind] = attr_tuple
except ValueError:
attrs.append(attr_tuple)
_set_prototype_value(caller, "attrs", attrs)
text = kwargs.get('text')
if not text:
if 'edit' in kwargs:
text = "Edited " + _display_attribute(attr_tuple)
else:
text = "Added " + _display_attribute(attr_tuple)
else:
text = "Attribute must be given as 'attrname[;category;locks] = <value>'."
options = {"key": "_default",
"goto": lambda caller: None}
return text, options
def _edit_attr(caller, attrname, new_value, **kwargs):
attr_string = "{}={}".format(attrname, new_value)
return _add_attr(caller, attr_string, edit=True)
def _examine_attr(caller, selection):
prot = _get_menu_prototype(caller)
ind = [part[0] for part in prot['attrs']].index(selection)
attr_tuple = prot['attrs'][ind]
return _display_attribute(attr_tuple)
@list_node(_caller_attrs)
def node_attrs(caller):
prot = _get_menu_prototype(caller)
attrs = prot.get("attrs")
text = ["Set the prototype's |yAttributes|n. Enter attributes on one of these forms:\n"
" attrname=value\n attrname;category=value\n attrname;category;lockstring=value\n"
"To give an attribute without a category but with a lockstring, leave that spot empty "
"(attrname;;lockstring=value)."
"Separate multiple attrs with commas. Use quotes to escape inputs with commas and "
"semi-colon."]
if attrs:
text.append("Current attrs are '|y{attrs}|n'.".format(attrs=attrs))
else:
text.append("No attrs are set.")
text = "\n\n".join(text)
options = _wizard_options("attrs", "aliases", "tags")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="attrs",
processor=lambda s: [part.strip() for part in s.split(",")],
next_node="node_tags"))})
return text, options
def _caller_tags(caller):
prototype = _get_menu_prototype(caller)
tags = prototype.get("tags", [])
return tags
def _display_tag(tag_tuple):
"""Pretty-print attribute 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, **kwargs):
"""
Add tags to the system, parsing this syntax:
tagname
tagname;category
tagname;category;data
"""
tag = tag.strip().lower()
category = None
data = ""
tagtuple = tag.split(";", 2)
ntuple = len(tagtuple)
if ntuple == 2:
tag, category = tagtuple
elif ntuple > 2:
tag, category, data = tagtuple
tag_tuple = (tag, category, data)
if tag:
prot = _get_menu_prototype(caller)
tags = prot.get('tags', [])
old_tag = kwargs.get("edit", None)
if not old_tag:
# a fresh, new tag
tags.append(tag_tuple)
else:
# old tag exists; editing a tag means removing the old and replacing with new
try:
ind = [tup[0] for tup in tags].index(old_tag)
del tags[ind]
if tags:
tags.insert(ind, tag_tuple)
else:
tags = [tag_tuple]
except IndexError:
pass
_set_prototype_value(caller, "tags", tags)
text = kwargs.get('text')
if not text:
if 'edit' in kwargs:
text = "Edited " + _display_tag(tag_tuple)
else:
text = "Added " + _display_tag(tag_tuple)
else:
text = "Tag must be given as 'tag[;category;data]."
options = {"key": "_default",
"goto": lambda caller: None}
return text, options
def _edit_tag(caller, old_tag, new_tag, **kwargs):
return _add_tag(caller, new_tag, edit=old_tag)
@list_node(_caller_tags)
def node_tags(caller):
text = ("Set the prototype's |yTags|n. Enter tags on one of the following forms:\n"
" tag\n tag;category\n tag;category;data\n"
"Note that 'data' is not commonly used.")
options = _wizard_options("tags", "attrs", "locks")
return text, options
def node_locks(caller):
prototype = _get_menu_prototype(caller)
locks = prototype.get("locks")
text = ["Set the prototype's |yLock string|n. Separate multiple locks with semi-colons. "
"Will retain case sensitivity."]
if locks:
text.append("Current locks are '|y{locks}|n'.".format(locks=locks))
else:
text.append("No locks are set.")
text = "\n\n".join(text)
options = _wizard_options("locks", "tags", "permissions")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="locks",
processor=lambda s: s.strip(),
next_node="node_permissions"))})
return text, options
def node_permissions(caller):
prototype = _get_menu_prototype(caller)
permissions = prototype.get("permissions")
text = ["Set the prototype's |yPermissions|n. Separate multiple permissions with commas. "
"Will retain case sensitivity."]
if permissions:
text.append("Current permissions are '|y{permissions}|n'.".format(permissions=permissions))
else:
text.append("No permissions are set.")
text = "\n\n".join(text)
options = _wizard_options("permissions", "destination", "location")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="permissions",
processor=lambda s: [part.strip() for part in s.split(",")],
next_node="node_location"))})
return text, options
def node_location(caller):
prototype = _get_menu_prototype(caller)
location = prototype.get("location")
text = ["Set the prototype's |yLocation|n"]
if location:
text.append("Current location is |y{location}|n.".format(location=location))
else:
text.append("Default location is {}'s inventory.".format(caller))
text = "\n\n".join(text)
options = _wizard_options("location", "permissions", "home")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="location",
processor=lambda s: s.strip(),
next_node="node_home"))})
return text, options
def node_home(caller):
prototype = _get_menu_prototype(caller)
home = prototype.get("home")
text = ["Set the prototype's |yHome location|n"]
if home:
text.append("Current home location is |y{home}|n.".format(home=home))
else:
text.append("Default home location (|y{home}|n) used.".format(home=settings.DEFAULT_HOME))
text = "\n\n".join(text)
options = _wizard_options("home", "aliases", "destination")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="home",
processor=lambda s: s.strip(),
next_node="node_destination"))})
return text, options
def node_destination(caller):
prototype = _get_menu_prototype(caller)
dest = prototype.get("dest")
text = ["Set the prototype's |yDestination|n. This is usually only used for Exits."]
if dest:
text.append("Current destination is |y{dest}|n.".format(dest=dest))
else:
text.append("No destination is set (default).")
text = "\n\n".join(text)
options = _wizard_options("destination", "home", "prototype_desc")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="dest",
processor=lambda s: s.strip(),
next_node="node_prototype_desc"))})
return text, options
def node_prototype_desc(caller):
prototype = _get_menu_prototype(caller)
text = ["The |wPrototype-Description|n briefly describes the prototype for "
"viewing in listings."]
desc = prototype.get("prototype_desc", None)
if desc:
text.append("The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc))
else:
text.append("Description is currently unset.")
text = "\n\n".join(text)
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_tags"))})
return text, options
def node_prototype_tags(caller):
prototype = _get_menu_prototype(caller)
text = ["|wPrototype-Tags|n can be used to classify and find prototypes. "
"Tags are case-insensitive. "
"Separate multiple by tags by commas."]
tags = prototype.get('prototype_tags', [])
if tags:
text.append("The current tags are:\n|w{tags}|n".format(tags=tags))
else:
text.append("No tags are currently set.")
text = "\n\n".join(text)
options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="prototype_tags",
processor=lambda s: [
str(part.strip().lower()) for part in s.split(",")],
next_node="node_prototype_locks"))})
return text, options
def node_prototype_locks(caller):
prototype = _get_menu_prototype(caller)
text = ["Set |wPrototype-Locks|n on the prototype. There are two valid lock types: "
"'edit' (who can edit the prototype) and 'spawn' (who can spawn new objects with this "
"prototype)\n(If you are unsure, leave as default.)"]
locks = prototype.get('prototype_locks', '')
if locks:
text.append("Current lock is |w'{lockstring}'|n".format(lockstring=locks))
else:
text.append("Lock unset - if not changed the default lockstring will be set as\n"
" |w'spawn:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id))
text = "\n\n".join(text)
options = _wizard_options("prototype_locks", "prototype_tags", "index")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="prototype_locks",
processor=lambda s: s.strip().lower(),
next_node="node_index"))})
return text, options
def _update_spawned(caller, **kwargs):
"""update existing objects"""
prototype = kwargs['prototype']
objects = kwargs['objects']
back_node = kwargs['back_key']
num_changed = spawner.batch_update_objects_with_prototype(prototype, objects=objects)
caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed))
return back_key
def _keep_diff(caller, **kwargs):
key = kwargs['key']
diff = kwargs['diff']
diff[key] = "KEEP"
def node_update_objects(caller, **kwargs):
"""Offer options for updating objects"""
def _keep_option(keyname, prototype, 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,
"obj": 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)
diff = kwargs.get("diff", 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
obj = choice(update_objects)
diff, obj_prototype = spawner.prototype_diff_from_object(prototype, obj)
text = ["Suggested changes to {} objects".format(len(update_objects)),
"Showing random example obj to change: {name} (#{dbref}))\n".format(obj.key, obj.dbref)]
options = []
io = 0
for (key, inst) in sorted(((key, val) for key, val in diff.items()), key=lambda tup: tup[0]):
line = "{iopt} |w{key}|n: {old}{sep}{new} {change}"
old_val = utils.crop(str(obj_prototype[key]), width=20)
if inst == "KEEP":
text.append(line.format(iopt='', key=key, old=old_val, sep=" ", new='', change=inst))
continue
new_val = utils.crop(str(spawner.init_spawn_value(prototype[key])), width=20)
io += 1
if inst in ("UPDATE", "REPLACE"):
text.append(line.format(iopt=io, key=key, old=old_val,
sep=" |y->|n ", new=new_val, change=inst))
options.append(_keep_option(key, prototype,
obj, obj_prototype, diff, objects, back_node))
elif inst == "REMOVE":
text.append(line.format(iopt=io, key=key, old=old_val,
sep=" |r->|n ", new='', change=inst))
options.append(_keep_option(key, prototype,
obj, obj_prototype, diff, objects, back_node))
options.extend(
[{"key": ("|wu|r update {} objects".format(len(update_objects)), "update", "u"),
"goto": (_update_spawned, {"prototype": prototype, "objects": objects,
"back_node": back_node, "diff": diff})},
{"key": ("|wr|neset changes", "reset", "r"),
"goto": ("node_update_objects", {"prototype": prototype, "back_node": back_node,
"objects": update_objects})},
{"key": "|wb|rack ({})".format(back_node[5:], 'b'),
"goto": back_node}])
return text, options
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)
accept_save = kwargs.get("accept_save", False)
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")
protlib.save_prototype(**prototype)
spawned_objects = protlib.search_objects_with_prototype(prototype_key)
nspawned = spawned_objects.count()
if nspawned:
text = ("Do you want to update {} object(s) "
"already using this prototype?".format(nspawned))
options = (
{"key": ("|wY|Wes|n", "yes", "y"),
"goto": ("node_update_objects",
{"accept_update": True, "objects": spawned_objects,
"prototype": prototype, "back_node": "node_prototype_save"})},
{"key": ("[|wN|Wo|n]", "n"),
"goto": "node_spawn"},
{"key": "_default",
"goto": "node_spawn"})
else:
text = "|gPrototype saved.|n"
options = {"key": "_default",
"goto": "node_spawn"}
return text, options
# not validated yet
prototype = _get_menu_prototype(caller)
error, text = _validate_prototype(prototype)
text = [text]
if error:
# abort save
text.append(
"Validation errors were found. They need to be corrected before this prototype "
"can be saved (or used to spawn).")
options = _wizard_options("prototype_save", "prototype_locks", "index")
return "\n".join(text), options
prototype_key = prototype['prototype_key']
if protlib.search_prototype(prototype_key):
text.append("Do you want to save/overwrite the existing prototype '{name}'?".format(
name=prototype_key))
else:
text.append("Do you want to save the prototype as '{name}'?".format(prototype_key))
options = (
{"key": ("[|wY|Wes|n]", "yes", "y"),
"goto": ("node_prototype_save",
{"accept": True, "prototype": prototype})},
{"key": ("|wN|Wo|n", "n"),
"goto": "node_spawn"},
{"key": "_default",
"goto": ("node_prototype_save",
{"accept": True, "prototype": prototype})})
return "\n".join(text), options
def _spawn(caller, **kwargs):
"""Spawn prototype"""
prototype = kwargs["prototype"].copy()
new_location = kwargs.get('location', None)
if new_location:
prototype['location'] = new_location
obj = spawner.spawn(prototype)
if obj:
obj = obj[0]
caller.msg("|gNew instance|n {key} ({dbref}) |gspawned.|n".format(
key=obj.key, dbref=obj.dbref))
else:
caller.msg("|rError: Spawner did not return a new instance.|n")
return obj
def node_prototype_spawn(caller, **kwargs):
"""Submenu for spawning the prototype"""
prototype = _get_menu_prototype(caller)
error, text = _validate_prototype(prototype)
text = [text]
if error:
text.append("|rPrototype validation failed. Correct the errors before spawning.|n")
options = _wizard_options("prototype_spawn", "prototype_locks", "index")
return "\n".join(text), options
# 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))})
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_update_objects",
dict(prototype=prototype, opjects=spawned_objects,
back_node="node_prototype_spawn"))})
options.extend(_wizard_options("prototype_spawn", "prototype_save", "index"))
return text, options
def _prototype_load_select(caller, prototype_key):
matches = protlib.search_prototype(key=prototype_key)
if matches:
prototype = matches[0]
_set_menu_prototype(caller, prototype)
caller.msg("|gLoaded prototype '{}'.".format(prototype_key))
return "node_index"
else:
caller.msg("|rFailed to load prototype '{}'.".format(prototype_key))
return None
@list_node(_all_prototype_parents, _prototype_load_select)
def node_prototype_load(caller, **kwargs):
text = ["Select a prototype to load. This will replace any currently edited prototype."]
options = _wizard_options("load", "save", "index")
options.append({"key": "_default",
"goto": _prototype_parent_examine})
return "\n".join(text), options
class OLCMenu(EvMenu):
"""
A custom EvMenu with a different formatting for the options.
"""
def options_formatter(self, optionlist):
"""
Split the options into two blocks - olc options and normal options
"""
olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype",
"save prototype", "load prototype", "spawn prototype")
olc_options = []
other_options = []
for key, desc in optionlist:
raw_key = strip_ansi(key).lower()
if raw_key in olc_keys:
desc = " {}".format(desc) if desc else ""
olc_options.append("|lc{}|lt{}|le{}".format(raw_key, key, desc))
else:
other_options.append((key, desc))
olc_options = " | ".join(olc_options) + " | " + "|wQ|Wuit" if olc_options else ""
other_options = super(OLCMenu, self).options_formatter(other_options)
sep = "\n\n" if olc_options and other_options else ""
return "{}{}{}".format(olc_options, sep, other_options)
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_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_update_objects": node_o
"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)