Source code for evennia.prototypes.menus

"""

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): 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]class OLCMenu(EvMenu): """ A custom EvMenu with a different formatting for the options. """
[docs] def nodetext_formatter(self, nodetext): """ Format the node text itself. """ return super().nodetext_formatter(nodetext)
[docs] 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", "search objects", ) actioninfo = self.actioninfo + "\n" if hasattr(self, "actioninfo") else "" self.actioninfo = "" # important, or this could bleed over to other nodes 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 = ( actioninfo + " |W|||n ".join(olc_options) + " |W|||n " + "|wQ|Wuit" if olc_options else "" ) other_options = super().options_formatter(other_options) sep = "\n\n" if olc_options and other_options else "" return "{}{}{}".format(olc_options, sep, other_options)
[docs] def helptext_formatter(self, helptext): """ Show help text """ return "|c --- Help ---|n\n" + utils.dedent(helptext)
[docs] def display_helptext(self): evmore.msg(self.caller, self.helptext, session=self._session, exit_cmd="look")
[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, )