From 1689d54ff3631a8ba224ac72a86c97f3e888b522 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 31 Mar 2018 21:10:20 +0200 Subject: [PATCH] New list_node decorator for evmenu. Tested with olc menu --- evennia/commands/default/building.py | 16 +-- evennia/utils/evmenu.py | 52 ++++++---- evennia/utils/spawner.py | 141 +++++++++++++++++++++------ evennia/utils/utils.py | 9 +- 4 files changed, 152 insertions(+), 66 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index dc70e52cea..759247acc7 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -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 diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 6c8729aee1..cccda6798f 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -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 ' to examine an option closer)" return text, options diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 3b2d6d4353..8caa9ae0c2 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -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 "" + 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 diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index a8d2171f75..22d59a165f 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -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