diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 138f97cf13..ab66363c71 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -7,7 +7,9 @@ OLC Prototype menu nodes import json import re from random import choice +from django.db.models import Q from django.conf import settings +from evennia.objects.models import ObjectDB from evennia.utils.evmenu import EvMenu, list_node from evennia.utils import evmore from evennia.utils.ansi import strip_ansi @@ -273,7 +275,11 @@ def _get_current_value(caller, keyname, formatter=str): flat_prot = _get_flat_menu_prototype(caller) if keyname in flat_prot: # value in flattened prot - return "Current {} (|binherited|n): {}".format(keyname, formatter(flat_prot[keyname])) + if keyname == 'prototype_key': + # we don't inherit prototype_keys + return "[No prototype_key set] (|rnot inherited|n)" + else: + return "Current {} (|binherited|n): {}".format(keyname, formatter(flat_prot[keyname])) return "[No {} set]".format(keyname) @@ -305,6 +311,180 @@ def _default_parse(raw_inp, choices, *args): # Menu nodes ------------------------------ +# helper nodes + +# 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_flat_menu_prototype(caller, 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 + + +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 + + +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_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_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")) + + 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, 'control'): + 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" + else: + caller.ndb._menutree.olc_search_object_term = raw_inp + return "node_search_object", kwargs + + +@list_node(_search_object, _object_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}. + + {actions} + (|RWarning: creating a prototype will |roverwrite|r |Rthe current prototype!)|n""".format( + num=nmatches, post="es" if nmatches > 1 else "", + actions=_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_actions, {"back": prev_node})}) + + return text, options # main index (start page) node @@ -382,49 +562,9 @@ def node_index(caller): {"key": ("|wSP|Wawn prototype", "spawn", "sp"), "goto": "node_prototype_spawn"}, {"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) - 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 - - -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}) + "goto": "node_prototype_load"}, + {"key": ("|wSE|Warch objects|n", "search", "se"), + "goto": "node_search_object"})) return text, options @@ -811,7 +951,7 @@ def node_aliases(caller): def _caller_attrs(caller): prototype = _get_menu_prototype(caller) - attrs = ["{}={}".format(tup[0], utils.crop(utils.to_str(tup[1]), width=10)) + attrs = ["{}={}".format(tup[0], utils.crop(utils.to_str(tup[1], force_string=True), width=10)) for tup in prototype.get("attrs", [])] return attrs @@ -1837,7 +1977,7 @@ class OLCMenu(EvMenu): """ olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype", - "save prototype", "load prototype", "spawn prototype") + "save prototype", "load prototype", "spawn prototype", "search objects") olc_options = [] other_options = [] for key, desc in optionlist: @@ -1878,6 +2018,7 @@ def start_olc(caller, session=None, prototype=None): 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, diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 4c0a2d3186..8ce20d5311 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -150,7 +150,8 @@ def value_to_obj_or_any(value): stype = type(value) if is_iter(value): if stype == dict: - return {value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.items()} + return {value_to_obj_or_any(key): + value_to_obj_or_any(val) for key, val in value.items()} else: return stype([value_to_obj_or_any(val) for val in value]) obj = dbid_to_obj(value, ObjectDB) @@ -165,18 +166,70 @@ def prototype_to_str(prototype): prototype (dict): The prototype. """ - header = ( - "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" - "|cdesc:|n {} \n|cprototype:|n ".format( - prototype.get('prototype_key', None), - ", ".join(prototype.get('prototype_tags', ['None'])), - prototype.get('prototype_locks', None), - prototype.get('prototype_desc', None))) - proto = ("{{\n {} \n}}".format( - "\n ".join( - "{!r}: {!r},".format(key, value) for key, value in - sorted(prototype.items()) if key not in _PROTOTYPE_META_NAMES)).rstrip(",")) - return header + proto + header = """ +|cprototype-key:|n {prototype_key}, |c-tags:|n {prototype_tags}, |c-locks:|n {prototype_locks}|n +|c-desc|n: {prototype_desc} +|cprototype-parent:|n {prototype_parent} + \n""".format( + prototype_key=prototype.get('prototype_key', '|r[UNSET](required)|n'), + prototype_tags=prototype.get('prototype_tags', '|wNone|n'), + prototype_locks=prototype.get('prototype_locks', '|wNone|n'), + prototype_desc=prototype.get('prototype_desc', '|wNone|n'), + prototype_parent=prototype.get('prototype_parent', '|wNone|n')) + + key = prototype.get('key', '') + if key: + key = "|ckey:|n {key}".format(key=key) + aliases = prototype.get("aliases", '') + if aliases: + aliases = "|caliases:|n {aliases}".format( + aliases=", ".join(aliases)) + attrs = prototype.get("attrs", '') + if attrs: + out = [] + for (attrkey, value, category, locks) in attrs: + locks = ", ".join(lock for lock in locks if lock) + category = "|ccategory:|n {}".format(category) if category else '' + cat_locks = "" + if category or locks: + cat_locks = "(|ccategory:|n {category}, ".format( + category=category if category else "|wNone|n") + out.append( + "{attrkey} " + "{cat_locks}\n" + " |c=|n {value}".format( + attrkey=attrkey, + cat_locks=cat_locks, + locks=locks if locks else "|wNone|n", + value=value)) + attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out)) + tags = prototype.get('tags', '') + if tags: + out = [] + for (tagkey, category, data) in tags: + out.append("{tagkey} (category: {category}{dat})".format( + tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else "")) + tags = "|ctags:|n\n {tags}".format(tags="\n ".join(out)) + locks = prototype.get('locks', '') + if locks: + locks = "|clocks:|n\n {locks}".format(locks="\n ".join(locks.split(";"))) + permissions = prototype.get("permissions", '') + if permissions: + permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions)) + location = prototype.get("location", '') + if location: + location = "|clocation:|n {location}".format(location=location) + home = prototype.get("home", '') + if home: + home = "|chome:|n {home}".format(home=home) + destination = prototype.get("destination", '') + if destination: + destination = "|cdestination:|n {destination}".format(destination=destination) + + body = "\n".join(part for part in (key, aliases, attrs, tags, locks, permissions, + location, home, destination) if part) + + return header.lstrip() + body.strip() def check_permission(prototype_key, action, default=True): diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 3dd8e11d67..1bae219368 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -150,6 +150,8 @@ def _get_prototype(dic, prot, protparents): for infinite recursion here. """ + # we don't overload the prototype_key + prototype_key = prot.get('prototype_key', None) if "prototype_parent" in dic: # move backwards through the inheritance for prototype in make_iter(dic["prototype_parent"]): @@ -157,6 +159,7 @@ def _get_prototype(dic, prot, protparents): new_prot = _get_prototype(protparents.get(prototype.lower(), {}), prot, protparents) prot.update(new_prot) prot.update(dic) + prot['prototype_key'] = prototype_key prot.pop("prototype_parent", None) # we don't need this anymore return prot @@ -217,19 +220,19 @@ def prototype_from_object(obj): location = obj.db_location if location: - prot['location'] = location + prot['location'] = location.dbref home = obj.db_home if home: - prot['home'] = home + prot['home'] = home.dbref destination = obj.db_destination if destination: - prot['destination'] = destination + prot['destination'] = destination.dbref locks = obj.locks.all() if locks: - prot['locks'] = locks + prot['locks'] = ";".join(locks) perms = obj.permissions.get() if perms: - prot['permissions'] = perms + prot['permissions'] = make_iter(perms) aliases = obj.aliases.get() if aliases: prot['aliases'] = aliases diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index d21aec2c56..a2c7429d0d 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1055,7 +1055,10 @@ def list_node(option_generator, select=None, pagesize=10): else: if callable(select): try: - return select(caller, selection) + if bool(getargspec(select).keywords): + return select(caller, selection, available_choices=available_choices) + else: + return select(caller, selection) except Exception: logger.log_trace() elif select: @@ -1124,7 +1127,7 @@ def list_node(option_generator, select=None, pagesize=10): except Exception: logger.log_trace() else: - if isinstance(decorated_options, {}): + if isinstance(decorated_options, dict): decorated_options = [decorated_options] else: decorated_options = make_iter(decorated_options)