1531 lines
53 KiB
Python
1531 lines
53 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 import evmore
|
|
from evennia.utils.ansi import strip_ansi
|
|
from evennia.utils import utils
|
|
from evennia.locks.lockhandler import get_all_lockfuncs
|
|
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"
|
|
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.
|
|
|
|
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")
|
|
|
|
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 ...")
|
|
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
|
|
|
|
|
|
def _format_protfuncs():
|
|
out = []
|
|
sorted_funcs = [(key, func) for key, func in
|
|
sorted(protlib.PROT_FUNCS.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(), key=lambda tup: tup[0])]
|
|
for lockfunc_name, lockfunc in sorted_funcs:
|
|
out.append("- |c${name}|n - |W{docs}".format(
|
|
name=lockfunc_name,
|
|
docs=utils.justify(lockfunc.__doc__.strip(), align='l', indent=10).strip()))
|
|
|
|
|
|
# Menu nodes ------------------------------
|
|
|
|
|
|
# main index (start page) node
|
|
|
|
|
|
def node_index(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
|
|
text = """
|
|
|c --- Prototype wizard --- |n
|
|
|
|
A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype
|
|
can be hard-coded or scripted using |w$protfuncs|n - for example to randomize the value
|
|
every time the prototype is used to spawn a new entity.
|
|
|
|
The prototype fields named 'prototype_*' are not used to create the entity itself but for
|
|
organizing the template when saving it for you (and maybe others) to use later.
|
|
|
|
Select prototype field to edit. If you are unsure, start from [|w1|n]. 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 or [|wLO|n]oad an existing prototype 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.
|
|
"""
|
|
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.
|
|
|
|
|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())
|
|
|
|
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 ('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": ("|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"}))
|
|
|
|
return text, options
|
|
|
|
|
|
# validate prototype (available as option from all nodes)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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', next_node="node_prototype_parent")
|
|
|
|
|
|
def node_prototype_key(caller):
|
|
prototype = _get_menu_prototype(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.
|
|
|
|
{current}"""
|
|
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.
|
|
"""
|
|
|
|
old_key = prototype.get('prototype_key', None)
|
|
if old_key:
|
|
text = text.format(current="Currently set to '|w{key}|n'".format(key=old_key))
|
|
else:
|
|
text = text.format(current="Currently |runset|n (required).")
|
|
|
|
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_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 = """
|
|
The |cPrototype Parent|n allows you to |winherit|n prototype values from another named
|
|
prototype (given as that prototype's |wprototype_key|). 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.
|
|
"""
|
|
|
|
if prot_parent_key:
|
|
prot_parent = protlib.search_prototype(prot_parent_key)
|
|
if prot_parent:
|
|
text.format(
|
|
current="Current parent prototype is {}:\n{}".format(
|
|
protlib.prototype_to_str(prot_parent)))
|
|
else:
|
|
text.format(
|
|
current="Current parent prototype |r{prototype}|n "
|
|
"does not appear to exist.".format(prot_parent_key))
|
|
else:
|
|
text.format(current="Parent prototype is not set")
|
|
text = (text, helptext)
|
|
|
|
options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W")
|
|
options.append({"key": "_default",
|
|
"goto": _prototype_parent_examine})
|
|
|
|
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_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.".format(typeclass))
|
|
return ret
|
|
|
|
|
|
@list_node(_all_typeclasses, _typeclass_select)
|
|
def node_typeclass(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
typeclass = prototype.get("typeclass")
|
|
|
|
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}
|
|
"""
|
|
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.
|
|
"""
|
|
|
|
if typeclass:
|
|
text.format(
|
|
current="Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass))
|
|
else:
|
|
text.format(
|
|
current="Using default typeclass {typeclass}.".format(
|
|
typeclass=settings.BASE_OBJECT_TYPECLASS))
|
|
|
|
text = (text, helptext)
|
|
|
|
options = _wizard_options("typeclass", "prototype_parent", "key", color="|W")
|
|
options.append({"key": "_default",
|
|
"goto": _typeclass_examine})
|
|
return text, options
|
|
|
|
|
|
# key node
|
|
|
|
|
|
def node_key(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
key = prototype.get("key")
|
|
|
|
text = """
|
|
The |cKey|n is the given name of the object to spawn. This will retain the given case.
|
|
|
|
{current}
|
|
"""
|
|
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())
|
|
|
|
if key:
|
|
text.format(current="Current key is '{key}'.".format(key=key))
|
|
else:
|
|
text.format(current="The key is currently unset.")
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# aliases node
|
|
|
|
|
|
def node_aliases(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
aliases = prototype.get("aliases")
|
|
|
|
text = """
|
|
|cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not
|
|
case sensitive.
|
|
|
|
Add multiple aliases separating with commas.
|
|
|
|
{current}
|
|
"""
|
|
helptext = """
|
|
Aliases are fixed alternative identifiers and are stored with the new object.
|
|
|
|
|c$protfuncs|n
|
|
|
|
{pfuncs}
|
|
""".format(pfuncs=_format_protfuncs())
|
|
|
|
if aliases:
|
|
text.format(current="Current aliases are '|c{aliases}|n'.".format(aliases=aliases))
|
|
else:
|
|
text.format(current="No aliases are set.")
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# attributes node
|
|
|
|
|
|
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 = """
|
|
|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}
|
|
"""
|
|
helptext = """
|
|
Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types
|
|
'attredit', 'attrread' are used to limiting editing and viewing of the Attribute. Putting
|
|
the lock-type `attrcreate` in the |clocks|n prototype key can be used to restrict builders
|
|
to add new Attributes.
|
|
|
|
|c$protfuncs
|
|
|
|
{pfuncs}
|
|
""".format(pfuncs=_format_protfuncs())
|
|
|
|
if attrs:
|
|
text.format(current="Current attrs {attrs}.".format(
|
|
attrs=attrs))
|
|
else:
|
|
text.format(current="No attrs are set.")
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# tags node
|
|
|
|
|
|
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 = """
|
|
|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
|
|
"""
|
|
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(protlib._PROTOTYPE_TAG_CATEGORY)
|
|
|
|
text = (text, helptext)
|
|
options = _wizard_options("tags", "attrs", "locks")
|
|
return text, options
|
|
|
|
|
|
# locks node
|
|
|
|
|
|
def node_locks(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
locks = prototype.get("locks")
|
|
|
|
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] ...
|
|
|
|
Separate multiple lockstrings by semicolons (;).
|
|
|
|
{current}
|
|
"""
|
|
helptext = """
|
|
Here is an example of a lock string constisting of two locks:
|
|
|
|
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'.
|
|
|
|
|c$lockfuncs|n
|
|
|
|
{lfuncs}
|
|
""".format(lfuncs=_format_lockfuncs())
|
|
|
|
if locks:
|
|
text.format(current="Current locks are '|y{locks}|n'.".format(locks=locks))
|
|
else:
|
|
text.format(current="No locks are set.")
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# permissions node
|
|
|
|
|
|
def node_permissions(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
permissions = prototype.get("permissions")
|
|
|
|
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.
|
|
|
|
{current}
|
|
"""
|
|
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(settings.PERMISSION_HIERARCHY)
|
|
|
|
if permissions:
|
|
text.format(current="Current permissions are {permissions}.".format(
|
|
permissions=permissions))
|
|
else:
|
|
text.format(current="No permissions are set.")
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# location node
|
|
|
|
|
|
def node_location(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
location = prototype.get("location")
|
|
|
|
text = """
|
|
The |cLocation|n of this object in the world. If not given, the object will spawn
|
|
in the inventory of |c{caller}|n instead.
|
|
|
|
{current}
|
|
""".format(caller=caller.key)
|
|
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)
|
|
|
|
if location:
|
|
text.format(current="Current location is {location}.".format(location=location))
|
|
else:
|
|
text.format(current="Default location is {}'s inventory.".format(caller))
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# home node
|
|
|
|
|
|
def node_home(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
home = prototype.get("home")
|
|
|
|
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 will be used.
|
|
|
|
{current}
|
|
"""
|
|
helptext = """
|
|
The location can be specified as as #dbref but can also be explicitly searched for using
|
|
$obj(name).
|
|
|
|
The home location is often not used except as a backup. It should never be unset.
|
|
"""
|
|
|
|
if home:
|
|
text.format(current="Current home location is {home}.".format(home=home))
|
|
else:
|
|
text.format(
|
|
current="Default home location ({home}) used.".format(home=settings.DEFAULT_HOME))
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# destination node
|
|
|
|
|
|
def node_destination(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
dest = prototype.get("dest")
|
|
|
|
text = """
|
|
The object's |cDestination|n is usually only set for Exit-like objects and designates where
|
|
the exit 'leads to'. It's usually unset for all other types of objects.
|
|
|
|
{current}
|
|
"""
|
|
helptext = """
|
|
The destination can be given as a #dbref but can also be explicitly searched for using
|
|
$obj(name).
|
|
"""
|
|
|
|
if dest:
|
|
text.format(current="Current destination is {dest}.".format(dest=dest))
|
|
else:
|
|
text.format("No destination is set (default).")
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# prototype_desc node
|
|
|
|
|
|
def node_prototype_desc(caller):
|
|
|
|
prototype = _get_menu_prototype(caller)
|
|
desc = prototype.get("prototype_desc", None)
|
|
|
|
text = """
|
|
The |cPrototype-Description|n optionally briefly describes the prototype when it's viewed in
|
|
listings.
|
|
|
|
{current}
|
|
"""
|
|
helptext = """
|
|
Giving a brief description helps you and others to locate the prototype for use later.
|
|
"""
|
|
|
|
if desc:
|
|
text.format(current="The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc))
|
|
else:
|
|
text.format(current="Prototype-Description is currently unset.")
|
|
|
|
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_tags"))})
|
|
|
|
return text, options
|
|
|
|
|
|
# prototype_tags node
|
|
|
|
|
|
def node_prototype_tags(caller):
|
|
prototype = _get_menu_prototype(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. Separate multiple tags by commas.
|
|
"""
|
|
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)
|
|
|
|
tags = prototype.get('prototype_tags', [])
|
|
|
|
if tags:
|
|
text.format(current="The current tags are:\n|w{tags}|n".format(tags=tags))
|
|
else:
|
|
text.format(current="No tags are currently set.")
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# prototype_locks node
|
|
|
|
|
|
def node_prototype_locks(caller):
|
|
prototype = _get_menu_prototype(caller)
|
|
locks = prototype.get('prototype_locks', '')
|
|
|
|
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, leave as default.
|
|
|
|
{current}
|
|
"""
|
|
helptext = """
|
|
Prototype locks can be used when there are different tiers of builders or for developers to
|
|
produce 'base prototypes' only meant for builders to inherit and expand on rather than
|
|
change.
|
|
"""
|
|
|
|
if locks:
|
|
text.format(current="Current lock is |w'{lockstring}'|n".format(lockstring=locks))
|
|
else:
|
|
text.format(
|
|
current="Default lock set: |w'spawn:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id))
|
|
|
|
text = (text, helptext)
|
|
|
|
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
|
|
|
|
|
|
# update existing objects node
|
|
|
|
|
|
def _update_spawned(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.msg("|g{num} objects were updated successfully.|n".format(num=num_changed))
|
|
return back_node
|
|
|
|
|
|
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, update_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, update_objects, back_node))
|
|
options.extend(
|
|
[{"key": ("|wu|r update {} objects".format(len(update_objects)), "update", "u"),
|
|
"goto": (_update_spawned, {"prototype": prototype, "objects": update_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}])
|
|
|
|
helptext = """
|
|
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) or even do the update manually if you are
|
|
unsure that the results will be acceptable. """
|
|
|
|
text = (text, helptext)
|
|
|
|
return text, options
|
|
|
|
|
|
# prototype save node
|
|
|
|
|
|
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})})
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
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"))
|
|
|
|
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)
|
|
|
|
return text, options
|
|
|
|
|
|
# prototype load node
|
|
|
|
|
|
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):
|
|
"""Load prototype"""
|
|
|
|
text = """
|
|
Select a prototype to load. This will replace any prototype currently being edited!
|
|
"""
|
|
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", "prototype_save", "index")
|
|
options.append({"key": "_default",
|
|
"goto": _prototype_parent_examine})
|
|
return text, options
|
|
|
|
|
|
# EvMenu definition, formatting and access functions
|
|
|
|
|
|
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 helptext_formatter(self, helptext):
|
|
"""
|
|
Show help text
|
|
"""
|
|
return "|c --- Help ---|n\n" + helptext
|
|
|
|
def display_helptext(self):
|
|
evmore.msg(self.caller, self.helptext, session=self._session)
|
|
|
|
|
|
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_update_objects,
|
|
"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)
|