Start improve OLC menu docs and help texts

This commit is contained in:
Griatch 2018-07-21 19:06:15 +02:00
parent a954f9c723
commit e4016e435e
5 changed files with 203 additions and 29 deletions

View file

@ -19,6 +19,15 @@
- The spawn command got the /save switch to save the defined prototype and its key.
- The command spawn/menu will now start an OLC (OnLine Creation) menu to load/save/edit/spawn prototypes.
### EvMenu
- Added `EvMenu.helptext_formatter(helptext)` to allow custom formatting of per-node help.
- Added `evennia.utils.evmenu.list_node` decorator for turning an EvMenu node into a multi-page listing.
- A `goto` option callable returning None (rather than the name of the next node) will now rerun the
current node instead of failing.
- Better error handling of in-node syntax errors.
# Overviews

View file

@ -8,6 +8,7 @@ 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.prototypes import prototypes as protlib
@ -78,7 +79,9 @@ def _format_option_value(prop, required=False, prototype=None, cropper=None):
out = ", ".join(str(pr) for pr in prop)
if not out and required:
out = "|rrequired"
return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH))
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):
@ -214,31 +217,75 @@ def _validate_prototype(prototype):
return err, text
# Menu nodes
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)
# Menu nodes ------------------------------
# main index (start page) node
def node_index(caller):
prototype = _get_menu_prototype(caller)
text = (
"|c --- Prototype wizard --- |n\n\n"
"Define the |yproperties|n of the prototype. All prototype values can be "
"over-ridden at the time of spawning an instance of the prototype, but some are "
"required.\n\n'|wprototype-'-properties|n are not used in the prototype itself but are used "
"to organize and list prototypes. The 'prototype-key' uniquely identifies the prototype "
"and allows you to edit an existing prototype or save a new one for use by you or "
"others later.\n\n(make choice; q to abort. If unsure, start from 1.)")
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',
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
required = ("prototype_parent" not in prototype) and ("typeclass" not in prototype)
if key == 'Typeclass':
cropper = _path_cropper
options.append(
@ -256,16 +303,18 @@ def node_index(caller):
options.extend((
{"key": ("|wV|Walidate prototype", "validate", "v"),
"goto": "node_validate_prototype"},
{"key": ("|wS|Wave prototype", "save", "s"),
{"key": ("|wSA|Wve prototype", "save", "sa"),
"goto": "node_prototype_save"},
{"key": ("|wSP|Wawn prototype", "spawn", "sp"),
"goto": "node_prototype_spawn"},
{"key": ("|wL|Woad prototype", "load", "l"),
{"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)
@ -273,11 +322,22 @@ def node_validate_prototype(caller, raw_string, **kwargs):
_, text = _validate_prototype(prototype)
helptext = """
The validator checks if the prototype's various values are on the expected form. It also test
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)
@ -303,22 +363,36 @@ def _check_prototype_key(caller, key):
def node_prototype_key(caller):
prototype = _get_menu_prototype(caller)
text = ["The prototype name, or |wMeta-Key|n, uniquely identifies the prototype. "
"It is used to find and use the prototype to spawn new entities. "
"It is not case sensitive."]
text = """
The |cPrototype-Key|n uniquely identifies the prototype. It must be specified. 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 to spawn 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.append("Current key is '|w{key}|n'".format(key=old_key))
text = text.format(current="Currently set to '|w{key}|n'".format(key=old_key))
else:
text.append("The key is currently unset.")
text.append("Enter text or make a choice (q for quit)")
text = "\n\n".join(text)
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"]
@ -368,6 +442,8 @@ def node_prototype_parent(caller):
return text, options
# typeclasses node
def _all_typeclasses(caller):
"""Get name of available typeclasses."""
return list(name for name in
@ -423,6 +499,9 @@ def node_typeclass(caller):
return text, options
# key node
def node_key(caller):
prototype = _get_menu_prototype(caller)
key = prototype.get("key")
@ -442,6 +521,9 @@ def node_key(caller):
return text, options
# aliases node
def node_aliases(caller):
prototype = _get_menu_prototype(caller)
aliases = prototype.get("aliases")
@ -462,6 +544,9 @@ def node_aliases(caller):
return text, options
# attributes node
def _caller_attrs(caller):
prototype = _get_menu_prototype(caller)
attrs = prototype.get("attrs", [])
@ -572,6 +657,9 @@ def node_attrs(caller):
return text, options
# tags node
def _caller_tags(caller):
prototype = _get_menu_prototype(caller)
tags = prototype.get("tags", [])
@ -659,6 +747,9 @@ def node_tags(caller):
return text, options
# locks node
def node_locks(caller):
prototype = _get_menu_prototype(caller)
locks = prototype.get("locks")
@ -679,6 +770,9 @@ def node_locks(caller):
return text, options
# permissions node
def node_permissions(caller):
prototype = _get_menu_prototype(caller)
permissions = prototype.get("permissions")
@ -699,6 +793,9 @@ def node_permissions(caller):
return text, options
# location node
def node_location(caller):
prototype = _get_menu_prototype(caller)
location = prototype.get("location")
@ -718,6 +815,9 @@ def node_location(caller):
return text, options
# home node
def node_home(caller):
prototype = _get_menu_prototype(caller)
home = prototype.get("home")
@ -737,6 +837,9 @@ def node_home(caller):
return text, options
# destination node
def node_destination(caller):
prototype = _get_menu_prototype(caller)
dest = prototype.get("dest")
@ -756,6 +859,9 @@ def node_destination(caller):
return text, options
# prototype_desc node
def node_prototype_desc(caller):
prototype = _get_menu_prototype(caller)
@ -778,6 +884,9 @@ def node_prototype_desc(caller):
return text, options
# prototype_tags node
def node_prototype_tags(caller):
prototype = _get_menu_prototype(caller)
text = ["|wPrototype-Tags|n can be used to classify and find prototypes. "
@ -800,6 +909,9 @@ def node_prototype_tags(caller):
return text, options
# prototype_locks node
def node_prototype_locks(caller):
prototype = _get_menu_prototype(caller)
text = ["Set |wPrototype-Locks|n on the prototype. There are two valid lock types: "
@ -821,6 +933,9 @@ def node_prototype_locks(caller):
return text, options
# update existing objects node
def _update_spawned(caller, **kwargs):
"""update existing objects"""
prototype = kwargs['prototype']
@ -904,6 +1019,9 @@ def node_update_objects(caller, **kwargs):
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
@ -972,6 +1090,9 @@ def node_prototype_save(caller, **kwargs):
return "\n".join(text), options
# spawning node
def _spawn(caller, **kwargs):
"""Spawn prototype"""
prototype = kwargs["prototype"].copy()
@ -1037,6 +1158,9 @@ def node_prototype_spawn(caller, **kwargs):
return text, options
# prototype load node
def _prototype_load_select(caller, prototype_key):
matches = protlib.search_prototype(key=prototype_key)
if matches:
@ -1052,12 +1176,15 @@ def _prototype_load_select(caller, prototype_key):
@list_node(_all_prototype_parents, _prototype_load_select)
def node_prototype_load(caller, **kwargs):
text = ["Select a prototype to load. This will replace any currently edited prototype."]
options = _wizard_options("load", "save", "index")
options = _wizard_options("prototype_load", "prototype_save", "index")
options.append({"key": "_default",
"goto": _prototype_parent_examine})
return "\n".join(text), options
# EvMenu definition, formatting and access functions
class OLCMenu(EvMenu):
"""
A custom EvMenu with a different formatting for the options.
@ -1086,6 +1213,15 @@ class OLCMenu(EvMenu):
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):
"""

View file

@ -13,7 +13,7 @@ from evennia.objects.models import ObjectDB
from evennia.utils.create import create_script
from evennia.utils.utils import (
all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module,
get_all_typeclasses, to_str, dbref)
get_all_typeclasses, to_str, dbref, justify)
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
from evennia.utils import logger
from evennia.utils import inlinefuncs
@ -29,7 +29,7 @@ _PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + (
"permissions", "locks", "exec", "tags", "attrs")
_PROTOTYPE_TAG_CATEGORY = "from_prototype"
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
_PROT_FUNCS = {}
PROT_FUNCS = {}
_RE_DBREF = re.compile(r"(?<!\$obj\()(#[0-9]+)")
@ -51,7 +51,7 @@ class ValidationError(RuntimeError):
for mod in settings.PROT_FUNC_MODULES:
try:
callables = callables_from_module(mod)
_PROT_FUNCS.update(callables)
PROT_FUNCS.update(callables)
except ImportError:
logger.log_trace()
raise
@ -97,7 +97,7 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F
pass
value = to_str(value, force_string=True)
available_functions = _PROT_FUNCS if available_functions is None else available_functions
available_functions = PROT_FUNCS if available_functions is None else available_functions
# insert $obj(#dbref) for #dbref
value = _RE_DBREF.sub("$obj(\\1)", value)
@ -118,6 +118,20 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F
return result
def format_available_protfuncs():
"""
Get all protfuncs in a pretty-formatted form.
Args:
clr (str, optional): What coloration tag to use.
"""
out = []
for protfunc_name, protfunc in PROT_FUNCS.items():
out.append("- |c${name}|n - |W{docs}".format(
name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "")))
return justify("\n".join(out), indent=8)
# helper functions
def value_to_obj(value, force=True):

View file

@ -796,7 +796,7 @@ class EvMenu(object):
# handle the helptext
if helptext:
self.helptext = helptext
self.helptext = self.helptext_formatter(helptext)
elif options:
self.helptext = _HELP_FULL if self.auto_quit else _HELP_NO_QUIT
else:
@ -898,6 +898,19 @@ class EvMenu(object):
"""
return dedent(nodetext).strip()
def helptext_formatter(self, helptext):
"""
Format the node's help text
Args:
helptext (str): The unformatted help text for the node.
Returns:
helptext (str): The formatted help text.
"""
return dedent(helptext).strip()
def options_formatter(self, optionlist):
"""
Formats the option block.

View file

@ -321,6 +321,7 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False
# process string on stack
ncallable = 0
nlparens = 0
nvalid = 0
if stacktrace:
out = "STRING: {} =>".format(string)
@ -367,6 +368,7 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False
try:
# try to fetch the matching inlinefunc from storage
stack.append(available_funcs[funcname])
nvalid += 1
except KeyError:
stack.append(available_funcs["nomatch"])
stack.append(funcname)
@ -393,9 +395,9 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False
# this means not all inlinefuncs were complete
return string
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack):
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid:
# if stack is larger than limit, throw away parsing
return string + gdict["stackfull"](*args, **kwargs)
return string + available_funcs["stackfull"](*args, **kwargs)
elif usecache:
# cache the stack - we do this also if we don't check the cache above
_PARSING_CACHE[string] = stack