mirror of
https://github.com/evennia/evennia.git
synced 2026-04-04 06:57:16 +02:00
New list_node decorator for evmenu. Tested with olc menu
This commit is contained in:
parent
058a3085e4
commit
1689d54ff3
4 changed files with 152 additions and 66 deletions
|
|
@ -15,7 +15,8 @@ from evennia.utils.eveditor import EvEditor
|
|||
from evennia.utils.evmore import EvMore
|
||||
from evennia.utils.spawner import (spawn, search_prototype, list_prototypes,
|
||||
save_db_prototype, build_metaproto, validate_prototype,
|
||||
delete_db_prototype, PermissionError, start_olc)
|
||||
delete_db_prototype, PermissionError, start_olc,
|
||||
metaproto_to_str)
|
||||
from evennia.utils.ansi import raw
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
|
@ -2886,21 +2887,10 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
def _search_show_prototype(query, metaprots=None):
|
||||
# prototype detail
|
||||
strings = []
|
||||
if not metaprots:
|
||||
metaprots = search_prototype(key=query, return_meta=True)
|
||||
if metaprots:
|
||||
for metaprot in metaprots:
|
||||
header = (
|
||||
"|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n"
|
||||
"|cdesc:|n {} \n|cprototype:|n ".format(
|
||||
metaprot.key, ", ".join(metaprot.tags),
|
||||
metaprot.locks, metaprot.desc))
|
||||
prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value)
|
||||
for key, value in
|
||||
sorted(metaprot.prototype.items())).rstrip(",")))
|
||||
strings.append(header + prototype)
|
||||
return "\n".join(strings)
|
||||
return "\n".join(metaproto_to_str(metaprot) for metaprot in metaprots)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -979,18 +979,20 @@ class EvMenu(object):
|
|||
#
|
||||
# -----------------------------------------------------------
|
||||
|
||||
def list_node(option_list, examine_processor, goto_processor, pagesize=10):
|
||||
def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
|
||||
"""
|
||||
Decorator for making an EvMenu node into a multi-page list node. Will add new options,
|
||||
prepending those options added in the node.
|
||||
|
||||
Args:
|
||||
option_list (list): List of strings indicating the options.
|
||||
examine_processor (callable): Will be called with the caller and the chosen option when
|
||||
examining said option. Should return a text string to display in the node.
|
||||
goto_processor (callable): Will be called with caller and
|
||||
the chosen option from the optionlist. Should return the target node to goto after the
|
||||
selection.
|
||||
option_generator (callable or list): A list of strings indicating the options, or a callable
|
||||
that is called without any arguments to produce such a list.
|
||||
examine_processor (callable, optional): Will be called with the caller and the chosen option
|
||||
when examining said option. Should return a text string to display in the node.
|
||||
goto_processor (callable, optional): Will be called as goto_processor(caller, menuchoice)
|
||||
where menuchoice is the chosen option as a string. Should return the target node to
|
||||
goto after this selection.
|
||||
|
||||
pagesize (int): How many options to show per page.
|
||||
|
||||
Example:
|
||||
|
|
@ -1009,6 +1011,7 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
|
|||
|
||||
available_choices = kwargs.get("available_choices", [])
|
||||
processor = kwargs.get("selection_processor")
|
||||
|
||||
try:
|
||||
match_ind = int(re.search(r"[0-9]+$", raw_string).group()) - 1
|
||||
selection = available_choices[match_ind]
|
||||
|
|
@ -1022,12 +1025,14 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
|
|||
logger.log_trace()
|
||||
return selection
|
||||
|
||||
nall_options = len(option_list)
|
||||
pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)]
|
||||
npages = len(pages)
|
||||
|
||||
def _list_node(caller, raw_string, **kwargs):
|
||||
|
||||
option_list = option_generator() if callable(option_generator) else option_generator
|
||||
|
||||
nall_options = len(option_list)
|
||||
pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)]
|
||||
npages = len(pages)
|
||||
|
||||
page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
|
||||
page = pages[page_index]
|
||||
|
||||
|
|
@ -1042,19 +1047,21 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
|
|||
# if the goto callable returns None, the same node is rerun, and
|
||||
# kwargs not used by the callable are passed on to the node. This
|
||||
# allows us to call ourselves over and over, using different kwargs.
|
||||
if page_index > 0:
|
||||
options.append({"key": ("|wb|Wack|n", "b"),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index - 1})})
|
||||
if page_index < npages - 1:
|
||||
options.append({"key": ("|wn|Wext|n", "n"),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index + 1})})
|
||||
|
||||
options.append({"key": ("|Wcurrent|n", "c"),
|
||||
"desc": "|W({}/{})|n".format(page_index + 1, npages),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index})})
|
||||
if page_index > 0:
|
||||
options.append({"key": ("|wp|Wrevious page|n", "p"),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index - 1})})
|
||||
if page_index < npages - 1:
|
||||
options.append({"key": ("|wn|Wext page|n", "n"),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index + 1})})
|
||||
|
||||
|
||||
# this catches arbitrary input, notably to examine entries ('look 4' or 'l4' etc)
|
||||
options.append({"key": "_default",
|
||||
"goto": (lambda caller: None,
|
||||
{"show_detail": True, "optionpage_index": page_index})})
|
||||
|
|
@ -1071,6 +1078,7 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
|
|||
# add data from the decorated node
|
||||
|
||||
text = ''
|
||||
extra_options = []
|
||||
try:
|
||||
text, extra_options = func(caller, raw_string)
|
||||
except TypeError:
|
||||
|
|
@ -1080,14 +1088,16 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
|
|||
raise
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
print("extra_options:", extra_options)
|
||||
else:
|
||||
if isinstance(extra_options, {}):
|
||||
extra_options = [extra_options]
|
||||
else:
|
||||
extra_options = make_iter(extra_options)
|
||||
options.append(extra_options)
|
||||
|
||||
options.extend(extra_options)
|
||||
text = text + "\n\n" + text_detail if text_detail else text
|
||||
text += "\n\n(Make a choice or enter 'look <num>' to examine an option closer)"
|
||||
|
||||
return text, options
|
||||
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ def search_module_prototype(key=None, tags=None):
|
|||
|
||||
def search_prototype(key=None, tags=None, return_meta=True):
|
||||
"""
|
||||
Find prototypes based on key and/or tags.
|
||||
Find prototypes based on key and/or tags, or all prototypes.
|
||||
|
||||
Kwargs:
|
||||
key (str): An exact or partial key to query for.
|
||||
|
|
@ -344,7 +344,8 @@ def search_prototype(key=None, tags=None, return_meta=True):
|
|||
return MetaProto namedtuples including prototype meta info
|
||||
|
||||
Return:
|
||||
matches (list): All found prototype dicts or MetaProtos
|
||||
matches (list): All found prototype dicts or MetaProtos. If no keys
|
||||
or tags are given, all available prototypes/MetaProtos will be returned.
|
||||
|
||||
Note:
|
||||
The available prototypes is a combination of those supplied in
|
||||
|
|
@ -438,6 +439,25 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed
|
|||
table.reformat_column(3, width=20)
|
||||
return table
|
||||
|
||||
|
||||
def metaproto_to_str(metaproto):
|
||||
"""
|
||||
Format a metaproto to a nice string representation.
|
||||
|
||||
Args:
|
||||
metaproto (NamedTuple): Represents the prototype.
|
||||
"""
|
||||
header = (
|
||||
"|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n"
|
||||
"|cdesc:|n {} \n|cprototype:|n ".format(
|
||||
metaproto.key, ", ".join(metaproto.tags),
|
||||
metaproto.locks, metaproto.desc))
|
||||
prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value)
|
||||
for key, value in
|
||||
sorted(metaproto.prototype.items())).rstrip(",")))
|
||||
return header + prototype
|
||||
|
||||
|
||||
# Spawner mechanism
|
||||
|
||||
|
||||
|
|
@ -660,7 +680,13 @@ def spawn(*prototypes, **kwargs):
|
|||
return _batch_create_object(*objsparams)
|
||||
|
||||
|
||||
# prototype design menu nodes
|
||||
# ------------------------------------------------------------
|
||||
#
|
||||
# OLC Prototype design menu
|
||||
#
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Helper functions
|
||||
|
||||
def _get_menu_metaprot(caller):
|
||||
if hasattr(caller.ndb._menutree, "olc_metaprot"):
|
||||
|
|
@ -683,7 +709,7 @@ def _set_menu_metaprot(caller, field, value):
|
|||
caller.ndb._menutree.olc_metaprot = build_metaproto(**kwargs)
|
||||
|
||||
|
||||
def _format_property(key, required=False, metaprot=None, prototype=None):
|
||||
def _format_property(key, required=False, metaprot=None, prototype=None, cropper=None):
|
||||
key = key.lower()
|
||||
if metaprot is not None:
|
||||
prop = getattr(metaprot, key) or ''
|
||||
|
|
@ -700,7 +726,7 @@ def _format_property(key, required=False, metaprot=None, prototype=None):
|
|||
out = ", ".join(str(pr) for pr in prop)
|
||||
if not out and required:
|
||||
out = "|rrequired"
|
||||
return " ({}|n)".format(crop(out, _MENU_CROP_WIDTH))
|
||||
return " ({}|n)".format(cropper(out) if cropper else crop(out, _MENU_CROP_WIDTH))
|
||||
|
||||
|
||||
def _set_property(caller, raw_string, **kwargs):
|
||||
|
|
@ -744,26 +770,43 @@ def _set_property(caller, raw_string, **kwargs):
|
|||
metaprot = _get_menu_metaprot(caller)
|
||||
prototype = metaprot.prototype
|
||||
prototype[propname_low] = value
|
||||
|
||||
# typeclass and prototype can't co-exist
|
||||
if propname_low == "typeclass":
|
||||
prototype.pop("prototype", None)
|
||||
if propname_low == "prototype":
|
||||
prototype.pop("typeclass", None)
|
||||
|
||||
_set_menu_metaprot(caller, "prototype", prototype)
|
||||
|
||||
|
||||
caller.msg("Set {prop} to {value}.".format(
|
||||
prop=prop.replace("_", "-").capitalize(), value=str(value)))
|
||||
|
||||
return next_node
|
||||
|
||||
|
||||
def _wizard_options(prev_node, next_node):
|
||||
options = [{"desc": "forward ({})".format(next_node.replace("_", "-")),
|
||||
def _wizard_options(prev_node, next_node, color="|W"):
|
||||
options = [{"key": ("|wf|Worward", "f"),
|
||||
"desc": "{color}({node})|n".format(
|
||||
color=color, node=next_node.replace("_", "-")),
|
||||
"goto": "node_{}".format(next_node)},
|
||||
{"desc": "back ({})".format(prev_node.replace("_", "-")),
|
||||
{"key": ("|wb|Wack", "b"),
|
||||
"desc": "{color}({node})|n".format(
|
||||
color=color, node=prev_node.replace("_", "-")),
|
||||
"goto": "node_{}".format(prev_node)}]
|
||||
if "index" not in (prev_node, next_node):
|
||||
options.append({"desc": "index",
|
||||
options.append({"key": ("|wi|Wndex", "i"),
|
||||
"goto": "node_index"})
|
||||
return options
|
||||
|
||||
|
||||
# menu nodes
|
||||
def _path_cropper(pythonpath):
|
||||
"Crop path to only the last component"
|
||||
return pythonpath.split('.')[-1]
|
||||
|
||||
|
||||
# Menu nodes
|
||||
|
||||
def node_index(caller):
|
||||
metaprot = _get_menu_metaprot(caller)
|
||||
|
|
@ -784,15 +827,20 @@ def node_index(caller):
|
|||
"goto": "node_meta_key"})
|
||||
for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks',
|
||||
'Permissions', 'Location', 'Home', 'Destination'):
|
||||
req = False
|
||||
required = False
|
||||
cropper = None
|
||||
if key in ("Prototype", "Typeclass"):
|
||||
req = "prototype" not in prototype and "typeclass" not in prototype
|
||||
required = "prototype" not in prototype and "typeclass" not in prototype
|
||||
if key == 'Typeclass':
|
||||
cropper = _path_cropper
|
||||
options.append(
|
||||
{"desc": "|w{}|n{}".format(key, _format_property(key, req, None, prototype)),
|
||||
{"desc": "|w{}|n{}".format(
|
||||
key, _format_property(key, required, None, prototype, cropper=cropper)),
|
||||
"goto": "node_{}".format(key.lower())})
|
||||
required = False
|
||||
for key in ('Desc', 'Tags', 'Locks'):
|
||||
options.append(
|
||||
{"desc": "|WMeta-{}|n|n{}".format(key, _format_property(key, req, metaprot, None)),
|
||||
{"desc": "|WMeta-{}|n|n{}".format(key, _format_property(key, required, metaprot, None)),
|
||||
"goto": "node_meta_{}".format(key.lower())})
|
||||
|
||||
return text, options
|
||||
|
|
@ -837,6 +885,24 @@ def node_meta_key(caller):
|
|||
return text, options
|
||||
|
||||
|
||||
def _all_prototypes():
|
||||
return [mproto.key for mproto in search_prototype()]
|
||||
|
||||
|
||||
def _prototype_examine(caller, prototype_name):
|
||||
metaprot = search_prototype(key=prototype_name)
|
||||
if metaprot:
|
||||
return metaproto_to_str(metaprot[0])
|
||||
return "Prototype not registered."
|
||||
|
||||
|
||||
def _prototype_select(caller, prototype):
|
||||
ret = _set_property(caller, prototype, prop="prototype", processor=str, next_node="node_key")
|
||||
caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype))
|
||||
return ret
|
||||
|
||||
|
||||
@list_node(_all_prototypes, _prototype_examine, _prototype_select)
|
||||
def node_prototype(caller):
|
||||
metaprot = _get_menu_metaprot(caller)
|
||||
prot = metaprot.prototype
|
||||
|
|
@ -848,25 +914,43 @@ def node_prototype(caller):
|
|||
else:
|
||||
text.append("Parent prototype is not set")
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("meta_key", "typeclass")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="prototype",
|
||||
processor=lambda s: s.strip(),
|
||||
next_node="node_typeclass"))})
|
||||
options = _wizard_options("meta_key", "typeclass", color="|W")
|
||||
return text, options
|
||||
|
||||
|
||||
def _typeclass_examine(caller, typeclass):
|
||||
return "This is typeclass |y{}|n.".format(typeclass)
|
||||
def _all_typeclasses():
|
||||
return list(sorted(get_all_typeclasses().keys()))
|
||||
# return list(sorted(get_all_typeclasses(parent="evennia.objects.objects.DefaultObject").keys()))
|
||||
|
||||
|
||||
def _typeclass_examine(caller, typeclass_path):
|
||||
if typeclass_path is None:
|
||||
# this means we are exiting the listing
|
||||
return "node_key"
|
||||
|
||||
typeclass = get_all_typeclasses().get(typeclass_path)
|
||||
if typeclass:
|
||||
docstr = []
|
||||
for line in typeclass.__doc__.split("\n"):
|
||||
if line.strip():
|
||||
docstr.append(line)
|
||||
elif docstr:
|
||||
break
|
||||
docstr = '\n'.join(docstr) if docstr else "<empty>"
|
||||
txt = "Typeclass |y{typeclass_path}|n; First paragraph of docstring:\n\n{docstring}".format(
|
||||
typeclass_path=typeclass_path, docstring=docstr)
|
||||
else:
|
||||
txt = "This is typeclass |y{}|n.".format(typeclass)
|
||||
return txt
|
||||
|
||||
|
||||
def _typeclass_select(caller, typeclass):
|
||||
caller.msg("Selected typeclass |y{}|n.".format(typeclass))
|
||||
return None
|
||||
ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key")
|
||||
caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass))
|
||||
return ret
|
||||
|
||||
|
||||
@list_node(list(sorted(get_all_typeclasses().keys())), _typeclass_examine, _typeclass_select)
|
||||
@list_node(_all_typeclasses, _typeclass_examine, _typeclass_select)
|
||||
def node_typeclass(caller):
|
||||
metaprot = _get_menu_metaprot(caller)
|
||||
prot = metaprot.prototype
|
||||
|
|
@ -879,12 +963,7 @@ def node_typeclass(caller):
|
|||
text.append("Using default typeclass {typeclass}.".format(
|
||||
typeclass=settings.BASE_OBJECT_TYPECLASS))
|
||||
text = "\n\n".join(text)
|
||||
options = _wizard_options("prototype", "key")
|
||||
options.append({"key": "_default",
|
||||
"goto": (_set_property,
|
||||
dict(prop="typeclass",
|
||||
processor=lambda s: s.strip(),
|
||||
next_node="node_key"))})
|
||||
options = _wizard_options("prototype", "key", color="|W")
|
||||
return text, options
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1882,10 +1882,14 @@ def get_game_dir_path():
|
|||
raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.")
|
||||
|
||||
|
||||
def get_all_typeclasses():
|
||||
def get_all_typeclasses(parent=None):
|
||||
"""
|
||||
List available typeclasses from all available modules.
|
||||
|
||||
Args:
|
||||
parent (str, optional): If given, only return typeclasses inheriting (at any distance)
|
||||
from this parent.
|
||||
|
||||
Returns:
|
||||
typeclasses (dict): On the form {"typeclass.path": typeclass, ...}
|
||||
|
||||
|
|
@ -1898,4 +1902,7 @@ def get_all_typeclasses():
|
|||
from evennia.typeclasses.models import TypedObject
|
||||
typeclasses = {"{}.{}".format(model.__module__, model.__name__): model
|
||||
for model in apps.get_models() if TypedObject in getmro(model)}
|
||||
if parent:
|
||||
typeclasses = {name: typeclass for name, typeclass in typeclasses.items()
|
||||
if inherits_from(typeclass, parent)}
|
||||
return typeclasses
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue