Source code for evennia.contrib.base_systems.building_menu.building_menu
"""
Module containing the building menu system.
Evennia contributor: vincent-lg 2018
Building menus are in-game menus, not unlike `EvMenu` though using a
different approach. Building menus have been specifically designed to edit
information as a builder. Creating a building menu in a command allows
builders quick-editing of a given object, like a room. If you follow the
steps below to add the contrib, you will have access to an `@edit` command
that will edit any default object offering to change its key and description.
1. Import the `GenericBuildingCmd` class from this contrib in your
`mygame/commands/default_cmdset.py` file:
```python
from evennia.contrib.base_systems.building_menu import GenericBuildingCmd
```
2. Below, add the command in the `CharacterCmdSet`:
```python
# ... These lines should exist in the file
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
# ... add the line below
self.add(GenericBuildingCmd())
```
The `@edit` command will allow you to edit any object. You will need to
specify the object name or ID as an argument. For instance: `@edit here`
will edit the current room. However, building menus can perform much more
than this very simple example, read on for more details.
Building menus can be set to edit about anything. Here is an example of
output you could obtain when editing the room:
```
Editing the room: Limbo(#2)
[T]itle: the limbo room
[D]escription
This is the limbo room. You can easily change this default description,
either by using the |y@desc/edit|n command, or simply by entering this
menu (enter |yd|n).
[E]xits:
north to A parking(#4)
[Q]uit this menu
```
From there, you can open the title choice by pressing t. You can then
change the room title by simply entering text, and go back to the
main menu entering @ (all this is customizable). Press q to quit this menu.
The first thing to do is to create a new module and place a class
inheriting from `BuildingMenu` in it.
```python
from evennia.contrib.base_systems.building_menu.building_menu import BuildingMenu
class RoomBuildingMenu(BuildingMenu):
# ...
```
Next, override the `init` method. You can add choices (like the title,
description, and exits choices as seen above) by using the `add_choice`
method.
```
class RoomBuildingMenu(BuildingMenu):
def init(self, room):
self.add_choice("title", "t", attr="key")
```
That will create the first choice, the title choice. If one opens your menu
and enter t, she will be in the title choice. She can change the title
(it will write in the room's `key` attribute) and then go back to the
main menu using `@`.
`add_choice` has a lot of arguments and offers a great deal of
flexibility. The most useful ones is probably the usage of callbacks,
as you can set almost any argument in `add_choice` to be a callback, a
function that you have defined above in your module. This function will be
called when the menu element is triggered.
Notice that in order to edit a description, the best method to call isn't
`add_choice`, but `add_choice_edit`. This is a convenient shortcut
which is available to quickly open an `EvEditor` when entering this choice
and going back to the menu when the editor closes.
```
class RoomBuildingMenu(BuildingMenu):
def init(self, room):
self.add_choice("title", "t", attr="key")
self.add_choice_edit("description", key="d", attr="db.desc")
```
When you wish to create a building menu, you just need to import your
class, create it specifying your intended caller and object to edit,
then call `open`:
```python
from <wherever> import RoomBuildingMenu
class CmdEdit(Command):
key = "redit"
def func(self):
menu = RoomBuildingMenu(self.caller, self.caller.location)
menu.open()
```
This is a very short introduction. For more details, see the online tutorial
(https://github.com/evennia/evennia/wiki/Building-menus) or read the
heavily-documented code below.
"""
from inspect import getfullargspec
from textwrap import dedent
from django.conf import settings
from evennia import CmdSet, Command
from evennia.commands import cmdhandler
from evennia.utils.ansi import strip_ansi
from evennia.utils.eveditor import EvEditor
from evennia.utils.logger import log_err, log_trace
from evennia.utils.utils import class_from_module
# Constants
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_CMD_NOMATCH = cmdhandler.CMD_NOMATCH
_CMD_NOINPUT = cmdhandler.CMD_NOINPUT
# Private functions
def _menu_loadfunc(caller):
obj, attr = caller.attributes.get("_building_menu_to_edit", [None, None])
if obj and attr:
for part in attr.split(".")[:-1]:
obj = getattr(obj, part)
return getattr(obj, attr.split(".")[-1]) if obj is not None else ""
def _menu_savefunc(caller, buf):
obj, attr = caller.attributes.get("_building_menu_to_edit", [None, None])
if obj and attr:
for part in attr.split(".")[:-1]:
obj = getattr(obj, part)
setattr(obj, attr.split(".")[-1], buf)
caller.attributes.remove("_building_menu_to_edit")
return True
def _menu_quitfunc(caller):
caller.cmdset.add(
BuildingMenuCmdSet,
persistent=caller.ndb._building_menu and caller.ndb._building_menu.persistent or False,
)
if caller.ndb._building_menu:
caller.ndb._building_menu.move(back=True)
def _call_or_get(value, menu=None, choice=None, string=None, obj=None, caller=None):
"""
Call the value, if appropriate, or just return it.
Args:
value (any): the value to obtain. It might be a callable (see note).
Keyword Args:
menu (BuildingMenu, optional): the building menu to pass to value
if it is a callable.
choice (Choice, optional): the choice to pass to value if a callable.
string (str, optional): the raw string to pass to value if a callback.
obj (Object): the object to pass to value if a callable.
caller (Account or Object, optional): the caller to pass to value
if a callable.
Returns:
The value itself. If the argument is a function, call it with
specific arguments (see note).
Note:
If `value` is a function, call it with varying arguments. The
list of arguments will depend on the argument names in your callable.
- An argument named `menu` will contain the building menu or None.
- The `choice` argument will contain the choice or None.
- The `string` argument will contain the raw string or None.
- The `obj` argument will contain the object or None.
- The `caller` argument will contain the caller or None.
- Any other argument will contain the object (`obj`).
Thus, you could define callbacks like this:
def on_enter(menu, caller, obj):
def on_nomatch(string, choice, menu):
def on_leave(caller, room): # note that room will contain `obj`
"""
if callable(value):
# Check the function arguments
kwargs = {}
spec = getfullargspec(value)
args = spec.args
if spec.varkw:
kwargs.update(dict(menu=menu, choice=choice, string=string, obj=obj, caller=caller))
else:
if "menu" in args:
kwargs["menu"] = menu
if "choice" in args:
kwargs["choice"] = choice
if "string" in args:
kwargs["string"] = string
if "obj" in args:
kwargs["obj"] = obj
if "caller" in args:
kwargs["caller"] = caller
# Fill missing arguments
for arg in args:
if arg not in kwargs:
kwargs[arg] = obj
# Call the function and return its return value
return value(**kwargs)
return value
# Helper functions, to be used in menu choices
# Building menu commands and CmdSet
[docs]class CmdNoInput(Command):
"""No input has been found."""
key = _CMD_NOINPUT
locks = "cmd:all()"
[docs] def __init__(self, **kwargs):
self.menu = kwargs.pop("building_menu", None)
super().__init__(**kwargs)
[docs] def func(self):
"""Display the menu or choice text."""
if self.menu:
self.menu.display()
else:
log_err("When CMDNOINPUT was called, the building menu couldn't be found")
self.caller.msg("|rThe building menu couldn't be found, remove the CmdSet.|n")
self.caller.cmdset.delete(BuildingMenuCmdSet)
[docs]class CmdNoMatch(Command):
"""No input has been found."""
key = _CMD_NOMATCH
locks = "cmd:all()"
[docs] def __init__(self, **kwargs):
self.menu = kwargs.pop("building_menu", None)
super().__init__(**kwargs)
[docs] def func(self):
"""Call the proper menu or redirect to nomatch."""
raw_string = self.args.rstrip()
if self.menu is None:
log_err("When CMDNOMATCH was called, the building menu couldn't be found")
self.caller.msg("|rThe building menu couldn't be found, remove the CmdSet.|n")
self.caller.cmdset.delete(BuildingMenuCmdSet)
return
choice = self.menu.current_choice
if raw_string in self.menu.keys_go_back:
if self.menu.keys:
self.menu.move(back=True)
elif self.menu.parents:
self.menu.open_parent_menu()
else:
self.menu.display()
elif choice:
if choice.nomatch(raw_string):
self.caller.msg(choice.format_text())
else:
for choice in self.menu.relevant_choices:
if choice.key.lower() == raw_string.lower() or any(
raw_string.lower() == alias for alias in choice.aliases
):
self.menu.move(choice.key)
return
self.msg("|rUnknown command: {}|n.".format(raw_string))
[docs]class BuildingMenuCmdSet(CmdSet):
"""Building menu CmdSet."""
key = "building_menu"
priority = 5
mergetype = "Replace"
[docs] def at_cmdset_creation(self):
"""Populates the cmdset with commands."""
caller = self.cmdsetobj
# The caller could recall the menu
menu = caller.ndb._building_menu
if menu is None:
menu = caller.db._building_menu
if menu:
menu = BuildingMenu.restore(caller)
cmds = [CmdNoInput, CmdNoMatch]
for cmd in cmds:
self.add(cmd(building_menu=menu))
# Menu classes
[docs]class Choice:
"""A choice object, created by `add_choice`."""
[docs] def __init__(
self,
title,
key=None,
aliases=None,
attr=None,
text=None,
glance=None,
on_enter=None,
on_nomatch=None,
on_leave=None,
menu=None,
caller=None,
obj=None,
):
"""Constructor.
Args:
title (str): the choice's title.
key (str, optional): the key of the letters to type to access
the choice. If not set, try to guess it based on the title.
aliases (list of str, optional): the allowed aliases for this choice.
attr (str, optional): the name of the attribute of 'obj' to set.
text (str or callable, optional): a text to be displayed for this
choice. It can be a callable.
glance (str or callable, optional): an at-a-glance summary of the
sub-menu shown in the main menu. It can be set to
display the current value of the attribute in the
main menu itself.
menu (BuildingMenu, optional): the parent building menu.
on_enter (callable, optional): a callable to call when the
caller enters into the choice.
on_nomatch (callable, optional): a callable to call when no
match is entered in the choice.
on_leave (callable, optional): a callable to call when the caller
leaves the choice.
caller (Account or Object, optional): the caller.
obj (Object, optional): the object to edit.
"""
self.title = title
self.key = key
self.aliases = aliases
self.attr = attr
self.text = text
self.glance = glance
self.on_enter = on_enter
self.on_nomatch = on_nomatch
self.on_leave = on_leave
self.menu = menu
self.caller = caller
self.obj = obj
def __repr__(self):
return "<Choice (title={}, key={})>".format(self.title, self.key)
@property
def keys(self):
"""Return a tuple of keys separated by `sep_keys`."""
return tuple(self.key.split(self.menu.sep_keys))
[docs] def format_text(self):
"""Format the choice text and return it, or an empty string."""
text = ""
if self.text:
text = _call_or_get(
self.text, menu=self.menu, choice=self, string="", caller=self.caller, obj=self.obj
)
text = dedent(text.strip("\n"))
text = text.format(obj=self.obj, caller=self.caller)
return text
[docs] def enter(self, string):
"""Called when the user opens the choice.
Args:
string (str): the entered string.
"""
if self.on_enter:
_call_or_get(
self.on_enter,
menu=self.menu,
choice=self,
string=string,
caller=self.caller,
obj=self.obj,
)
[docs] def nomatch(self, string):
"""Called when the user entered something in the choice.
Args:
string (str): the entered string.
Returns:
to_display (bool): The return value of `nomatch` if set or
`True`. The rule is that if `no_match` returns `True`,
then the choice or menu is displayed.
"""
if self.on_nomatch:
return _call_or_get(
self.on_nomatch,
menu=self.menu,
choice=self,
string=string,
caller=self.caller,
obj=self.obj,
)
return True
[docs] def leave(self, string):
"""Called when the user closes the choice.
Args:
string (str): the entered string.
"""
if self.on_leave:
_call_or_get(
self.on_leave,
menu=self.menu,
choice=self,
string=string,
caller=self.caller,
obj=self.obj,
)
[docs]class BuildingMenu:
"""
Class allowing to create and set building menus to edit specific objects.
A building menu is somewhat similar to `EvMenu`, but designed to edit
objects by builders, although it can be used for players in some contexts.
You could, for instance, create a building menu to edit a room with a
sub-menu for the room's key, another for the room's description,
another for the room's exits, and so on.
To add choices (simple sub-menus), you should call `add_choice` (see the
full documentation of this method). With most arguments, you can
specify either a plain string or a callback. This callback will be
called when the operation is to be performed.
Some methods are provided for frequent needs (see the `add_choice_*`
methods). Some helper functions are defined at the top of this
module in order to be used as arguments to `add_choice`
in frequent cases.
"""
keys_go_back = ["@"] # The keys allowing to go back in the menu tree
sep_keys = "." # The key separator for menus with more than 2 levels
joker_key = "*" # The special key meaning "anything" in a choice key
min_shortcut = 1 # The minimum length of shortcuts when `key` is not set
[docs] def __init__(
self,
caller=None,
obj=None,
title="Building menu: {obj}",
keys=None,
parents=None,
persistent=False,
):
"""Constructor, you shouldn't override. See `init` instead.
Args:
caller (Account or Object): the caller.
obj (Object): the object to be edited, like a room.
title (str, optional): the menu title.
keys (list of str, optional): the starting menu keys (None
to start from the first level).
parents (tuple, optional): information for parent menus,
automatically supplied.
persistent (bool, optional): should this building menu
survive a reload/restart?
Note:
If some of these options have to be changed, it is
preferable to do so in the `init` method and not to
override `__init__`. For instance:
class RoomBuildingMenu(BuildingMenu):
def init(self, room):
self.title = "Menu for room: {obj.key}(#{obj.id})"
# ...
"""
self.caller = caller
self.obj = obj
self.title = title
self.keys = keys or []
self.parents = parents or ()
self.persistent = persistent
self.choices = []
self.cmds = {}
self.can_quit = False
if obj:
self.init(obj)
if not parents and not self.can_quit:
# Automatically add the menu to quit
self.add_choice_quit(key=None)
self._add_keys_choice()
@property
def current_choice(self):
"""Return the current choice or None.
Returns:
choice (Choice): the current choice or None.
Note:
We use the menu keys to identify the current position of
the caller in the menu. The menu `keys` hold a list of
keys that should match a choice to be usable.
"""
menu_keys = self.keys
if not menu_keys:
return None
for choice in self.choices:
choice_keys = choice.keys
if len(menu_keys) == len(choice_keys):
# Check all the intermediate keys
common = True
for menu_key, choice_key in zip(menu_keys, choice_keys):
if choice_key == self.joker_key:
continue
if not isinstance(menu_key, str) or menu_key != choice_key:
common = False
break
if common:
return choice
return None
@property
def relevant_choices(self):
"""Only return the relevant choices according to the current meny key.
Returns:
relevant (list of Choice object): the relevant choices.
Note:
We use the menu keys to identify the current position of
the caller in the menu. The menu `keys` hold a list of
keys that should match a choice to be usable.
"""
menu_keys = self.keys
relevant = []
for choice in self.choices:
choice_keys = choice.keys
if not menu_keys and len(choice_keys) == 1:
# First level choice with the menu key empty, that's relevant
relevant.append(choice)
elif len(menu_keys) == len(choice_keys) - 1:
# Check all the intermediate keys
common = True
for menu_key, choice_key in zip(menu_keys, choice_keys):
if choice_key == self.joker_key:
continue
if not isinstance(menu_key, str) or menu_key != choice_key:
common = False
break
if common:
relevant.append(choice)
return relevant
def _save(self):
"""Save the menu in a attributes on the caller.
If `persistent` is set to `True`, also save in a persistent attribute.
"""
self.caller.ndb._building_menu = self
if self.persistent:
self.caller.db._building_menu = {
"class": type(self).__module__ + "." + type(self).__name__,
"obj": self.obj,
"title": self.title,
"keys": self.keys,
"parents": self.parents,
"persistent": self.persistent,
}
def _add_keys_choice(self):
"""Add the choices' keys if some choices don't have valid keys."""
# If choices have been added without keys, try to guess them
for choice in self.choices:
if not choice.key:
title = strip_ansi(choice.title.strip()).lower()
length = self.min_shortcut
while length <= len(title):
i = 0
while i < len(title) - length + 1:
guess = title[i : i + length]
if guess not in self.cmds:
choice.key = guess
break
i += 1
if choice.key:
break
length += 1
if choice.key:
self.cmds[choice.key] = choice
else:
raise ValueError("Cannot guess the key for {}".format(choice))
[docs] def init(self, obj):
"""Create the sub-menu to edit the specified object.
Args:
obj (Object): the object to edit.
Note:
This method is probably to be overridden in your subclasses.
Use `add_choice` and its variants to create menu choices.
"""
pass
[docs] def add_choice(
self,
title,
key=None,
aliases=None,
attr=None,
text=None,
glance=None,
on_enter=None,
on_nomatch=None,
on_leave=None,
):
"""
Add a choice, a valid sub-menu, in the current builder menu.
Args:
title (str): the choice's title.
key (str, optional): the key of the letters to type to access
the sub-neu. If not set, try to guess it based on the
choice title.
aliases (list of str, optional): the aliases for this choice.
attr (str, optional): the name of the attribute of 'obj' to set.
This is really useful if you want to edit an
attribute of the object (that's a frequent need). If
you don't want to do so, just use the `on_*` arguments.
text (str or callable, optional): a text to be displayed when
the menu is opened It can be a callable.
glance (str or callable, optional): an at-a-glance summary of the
sub-menu shown in the main menu. It can be set to
display the current value of the attribute in the
main menu itself.
on_enter (callable, optional): a callable to call when the
caller enters into this choice.
on_nomatch (callable, optional): a callable to call when
the caller enters something in this choice. If you
don't set this argument but you have specified
`attr`, then `obj`.`attr` will be set with the value
entered by the user.
on_leave (callable, optional): a callable to call when the
caller leaves the choice.
Returns:
choice (Choice): the newly-created choice.
Raises:
ValueError if the choice cannot be added.
Note:
Most arguments can be callables, like functions. This has the
advantage of allowing great flexibility. If you specify
a callable in most of the arguments, the callable should return
the value expected by the argument (a str more often than
not). For instance, you could set a function to be called
to get the menu text, which allows for some filtering:
def text_exits(menu):
return "Some text to display"
class RoomBuildingMenu(BuildingMenu):
def init(self):
self.add_choice("exits", key="x", text=text_exits)
The allowed arguments in a callable are specific to the
argument names (they are not sensitive to orders, not all
arguments have to be present). For more information, see
`_call_or_get`.
"""
key = key or ""
key = key.lower()
aliases = aliases or []
aliases = [a.lower() for a in aliases]
if attr and on_nomatch is None:
on_nomatch = menu_setattr
if key and key in self.cmds:
raise ValueError(
"A conflict exists between {} and {}, both use key or alias {}".format(
self.cmds[key], title, repr(key)
)
)
if attr:
if glance is None:
glance = "{obj." + attr + "}"
if text is None:
text = """
-------------------------------------------------------------------------------
{attr} for {{obj}}(#{{obj.id}})
You can change this value simply by entering it.
Use |y{back}|n to go back to the main menu.
Current value: |c{{{obj_attr}}}|n
""".format(
attr=attr, obj_attr="obj." + attr, back="|n or |y".join(self.keys_go_back)
)
choice = Choice(
title,
key=key,
aliases=aliases,
attr=attr,
text=text,
glance=glance,
on_enter=on_enter,
on_nomatch=on_nomatch,
on_leave=on_leave,
menu=self,
caller=self.caller,
obj=self.obj,
)
self.choices.append(choice)
if key:
self.cmds[key] = choice
for alias in aliases:
self.cmds[alias] = choice
return choice
[docs] def add_choice_edit(
self,
title="description",
key="d",
aliases=None,
attr="db.desc",
glance="\n {obj.db.desc}",
on_enter=None,
):
"""
Add a simple choice to edit a given attribute in the EvEditor.
Args:
title (str, optional): the choice's title.
key (str, optional): the choice's key.
aliases (list of str, optional): the choice's aliases.
glance (str or callable, optional): the at-a-glance description.
on_enter (callable, optional): a different callable to edit
the attribute.
Returns:
choice (Choice): the newly-created choice.
Note:
This is just a shortcut method, calling `add_choice`.
If `on_enter` is not set, use `menu_edit` which opens
an EvEditor to edit the specified attribute.
When the caller closes the editor (with :q), the menu
will be re-opened.
"""
on_enter = on_enter or menu_edit
return self.add_choice(
title, key=key, aliases=aliases, attr=attr, glance=glance, on_enter=on_enter, text=""
)
[docs] def add_choice_quit(self, title="quit the menu", key="q", aliases=None, on_enter=None):
"""
Add a simple choice just to quit the building menu.
Args:
title (str, optional): the choice's title.
key (str, optional): the choice's key.
aliases (list of str, optional): the choice's aliases.
on_enter (callable, optional): a different callable
to quit the building menu.
Note:
This is just a shortcut method, calling `add_choice`.
If `on_enter` is not set, use `menu_quit` which simply
closes the menu and displays a message. It also
removes the CmdSet from the caller. If you supply
another callable instead, make sure to do the same.
"""
on_enter = on_enter or menu_quit
self.can_quit = True
return self.add_choice(title, key=key, aliases=aliases, on_enter=on_enter)
[docs] def open(self):
"""Open the building menu for the caller.
Note:
This method should be called once when the building menu
has been instanciated. From there, the building menu will
be re-created automatically when the server
reloads/restarts, assuming `persistent` is set to `True`.
"""
caller = self.caller
self._save()
# Remove the same-key cmdset if exists
if caller.cmdset.has(BuildingMenuCmdSet):
caller.cmdset.remove(BuildingMenuCmdSet)
self.caller.cmdset.add(BuildingMenuCmdSet, persistent=self.persistent)
self.display()
[docs] def move(self, key=None, back=False, quiet=False, string=""):
"""
Move inside the menu.
Args:
key (any): the portion of the key to add to the current
menu keys. If you wish to go back in the menu
tree, don't provide a `key`, just set `back` to `True`.
back (bool, optional): go back in the menu (`False` by default).
quiet (bool, optional): should the menu or choice be
displayed afterward?
string (str, optional): the string sent by the caller to move.
Note:
This method will need to be called directly should you
use more than two levels in your menu. For instance,
in your room menu, if you want to have an "exits"
option, and then be able to enter "north" in this
choice to edit an exit. The specific exit choice
could be a different menu (with a different class), but
it could also be an additional level in your original menu.
If that's the case, you will need to use this method.
"""
choice = self.current_choice
if choice:
choice.leave("")
if not back: # Move forward
if not key:
raise ValueError("you are asking to move forward, you should specify a key.")
self.keys.append(key)
else: # Move backward
if not self.keys:
raise ValueError(
"you already are at the top of the tree, you cannot move backward."
)
del self.keys[-1]
self._save()
choice = self.current_choice
if choice:
choice.enter(string)
if not quiet:
self.display()
[docs] def close(self):
"""Close the building menu, removing the CmdSet."""
if self.caller.cmdset.has(BuildingMenuCmdSet):
self.caller.cmdset.delete(BuildingMenuCmdSet)
if self.caller.attributes.has("_building_menu"):
self.caller.attributes.remove("_building_menu")
if self.caller.nattributes.has("_building_menu"):
self.caller.nattributes.remove("_building_menu")
# Display methods. Override for customization
[docs] def display_title(self):
"""Return the menu title to be displayed."""
return _call_or_get(self.title, menu=self, obj=self.obj, caller=self.caller).format(
obj=self.obj
)
[docs] def display_choice(self, choice):
"""Display the specified choice.
Args:
choice (Choice): the menu choice.
"""
title = _call_or_get(
choice.title, menu=self, choice=choice, obj=self.obj, caller=self.caller
)
clear_title = title.lower()
pos = clear_title.find(choice.key.lower())
ret = " "
if pos >= 0:
ret += title[:pos] + "[|y" + choice.key.title() + "|n]" + title[pos + len(choice.key) :]
else:
ret += "[|y" + choice.key.title() + "|n] " + title
if choice.glance:
glance = _call_or_get(
choice.glance, menu=self, choice=choice, caller=self.caller, string="", obj=self.obj
)
glance = glance.format(obj=self.obj, caller=self.caller)
ret += ": " + glance
return ret
[docs] def display(self):
"""Display the entire menu or a single choice, depending on the keys."""
choice = self.current_choice
if self.keys and choice:
text = choice.format_text()
else:
text = self.display_title() + "\n"
for choice in self.relevant_choices:
text += "\n" + self.display_choice(choice)
self.caller.msg(text)
[docs] @staticmethod
def restore(caller):
"""Restore the building menu for the caller.
Args:
caller (Account or Object): the caller.
Note:
This method should be automatically called if a menu is
saved in the caller, but the object itself cannot be found.
"""
menu = caller.db._building_menu
if menu:
class_name = menu.get("class")
if not class_name:
log_err(
"BuildingMenu: on caller {}, a persistent attribute holds building menu "
"data, but no class could be found to restore the menu".format(caller)
)
return
try:
menu_class = class_from_module(class_name)
except Exception:
log_trace(
"BuildingMenu: attempting to load class {} failed".format(repr(class_name))
)
return
# Create the menu
obj = menu.get("obj")
keys = menu.get("keys")
title = menu.get("title", "")
parents = menu.get("parents")
persistent = menu.get("persistent", False)
try:
building_menu = menu_class(
caller, obj, title=title, keys=keys, parents=parents, persistent=persistent
)
except Exception:
log_trace(
"An error occurred while creating building menu {}".format(repr(class_name))
)
return
return building_menu
# Generic building menu and command
[docs]class GenericBuildingMenu(BuildingMenu):
"""A generic building menu, allowing to edit any object.
This is more a demonstration menu. By default, it allows to edit the
object key and description. Nevertheless, it will be useful to demonstrate
how building menus are meant to be used.
"""
[docs] def init(self, obj):
"""Build the meny, adding the 'key' and 'description' choices.
Args:
obj (Object): any object to be edited, like a character or room.
Note:
The 'quit' choice will be automatically added, though you can
call `add_choice_quit` to add this choice with different options.
"""
self.add_choice(
"key",
key="k",
attr="key",
glance="{obj.key}",
text="""
-------------------------------------------------------------------------------
Editing the key of {{obj.key}}(#{{obj.id}})
You can change the simply by entering it.
Use |y{back}|n to go back to the main menu.
Current key: |c{{obj.key}}|n
""".format(
back="|n or |y".join(self.keys_go_back)
),
)
self.add_choice_edit("description", key="d", attr="db.desc")
[docs]class GenericBuildingCmd(Command):
"""
Generic building command.
Syntax:
@edit [object]
Open a building menu to edit the specified object. This menu allows to
change the object's key and description.
Examples:
@edit here
@edit self
@edit #142
"""
key = "@edit"
[docs] def func(self):
if not self.args.strip():
self.msg("You should provide an argument to this function: the object to edit.")
return
obj = self.caller.search(self.args.strip(), global_search=True)
if not obj:
return
menu = GenericBuildingMenu(self.caller, obj)
menu.open()