mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Start improve OLC menu docs and help texts
This commit is contained in:
parent
a954f9c723
commit
e4016e435e
5 changed files with 203 additions and 29 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue