Building menu

Contrib by 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 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.

Install

  1. Import the GenericBuildingCmd class from this contrib in your mygame/commands/default_cmdset.py file:

    from evennia.contrib.base_systems.building_menu import GenericBuildingCmd
    
  2. Below, add the command in the CharacterCmdSet:

    # ... 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())
    

Basic Usage

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.

from evennia.contrib.base_systems.building_menu import BuildingMenu

class RoomBuildingMenu(BuildingMenu):
    # ...

Next, override the init method (not __init__!). 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:

from <wherever> import RoomBuildingMenu

class CmdEdit(Command):

    key = "redit"

    def func(self):
        menu = RoomBuildingMenu(self.caller, self.caller.location)
        menu.open()

A simple menu example

Before diving in, there are some things to point out:

  • Building menus work on an object. This object will be edited by manipulations in the menu. So you can create a menu to add/edit a room, an exit, a character and so on.

  • Building menus are arranged in layers of choices. A choice gives access to an option or to a sub- menu. Choices are linked to commands (usually very short). For instance, in the example shown below, to edit the room key, after opening the building menu, you can type k. That will lead you to the key choice where you can enter a new key for the room. Then you can enter @ to leave this choice and go back to the entire menu. (All of this can be changed).

  • To open the menu, you will need something like a command. This contrib offers a basic command for demonstration, but we will override it in this example, using the same code with more flexibility.

So let’s add a very basic example to begin with.

A generic editing command

Let’s begin by adding a new command. You could add or edit the following file (there’s no trick here, feel free to organize the code differently):

# file: commands/building.py
from evennia.contrib.building_menu import BuildingMenu
from commands.command import Command

class EditCmd(Command):

    """
    Editing command.

    Usage:
      @edit [object]

    Open a building menu to edit the specified object.  This menu allows to
    specific information about this object.

    Examples:
      @edit here
      @edit self
      @edit #142

    """

    key = "@edit"
    locks = "cmd:id(1) or perm(Builders)"
    help_category = "Building"

    def func(self):
        if not self.args.strip():
            self.msg("|rYou should provide an argument to this function: the object to edit.|n")
            return

        obj = self.caller.search(self.args.strip(), global_search=True)
        if not obj:
            return

        if obj.typename == "Room":
            Menu = RoomBuildingMenu
        else:
            obj_name = obj.get_display_name(self.caller)
            self.msg(f"|rThe object {obj_name} cannot be edited.|n")
            return

        menu = Menu(self.caller, obj)
        menu.open()

This command is rather simple in itself:

  1. It has a key @edit and a lock to only allow builders to use it.

  2. In its func method, it begins by checking the arguments, returning an error if no argument is specified.

  3. It then searches for the given argument. We search globally. The search method used in this way will return the found object or None. It will also send the error message to the caller if necessary.

  4. Assuming we have found an object, we check the object typename. This will be used later when we want to display several building menus. For the time being, we only handle Room. If the caller specified something else, we’ll display an error.

  5. Assuming this object is a Room, we have defined a Menu object containing the class of our building menu. We build this class (creating an instance), giving it the caller and the object to edit.

  6. We then open the building menu, using the open method.

The end might sound a bit surprising at first glance. But the process is still very simple: we create an instance of our building menu and call its open method. Nothing more.

Where is our building menu?

If you go ahead and add this command and test it, you’ll get an error. We haven’t defined RoomBuildingMenu yet.

To add this command, edit commands/default_cmdsets.py. Import our command, adding an import line at the top of the file:

"""
...
"""

from evennia import default_cmds

# The following line is to be added
from commands.building import EditCmd

And in the class below (CharacterCmdSet), add the last line of this code:

class CharacterCmdSet(default_cmds.CharacterCmdSet):
    """
    The `CharacterCmdSet` contains general in-game commands like `look`,
    `get`, etc available on in-game Character objects. It is merged with
    the `AccountCmdSet` when an Account puppets a Character.
    """
    key = "DefaultCharacter"

    def at_cmdset_creation(self):
        """
        Populates the cmdset
        """
        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones.
        #
        self.add(EditCmd())

Our first menu

So far, we can’t use our building menu. Our @edit command will throw an error. We have to define the RoomBuildingMenu class. Open the commands/building.py file and add to the end of the file:

# ... at the end of commands/building.py
# Our building menu

class RoomBuildingMenu(BuildingMenu):

    """
    Building menu to edit a room.

    For the time being, we have only one choice: key, to edit the room key.

    """

    def init(self, room):
        self.add_choice("key", "k", attr="key")

Save these changes, reload your game. You can now use the @edit command. Here’s what we get (notice that the commands we enter into the game are prefixed with > , though this prefix will probably not appear in your MUD client):

> look
Limbo(#2)
Welcome to your new Evennia-based game! Visit https://www.evennia.com if you need
help, want to contribute, report issues or just join the community.
As Account #1 you can create a demo/tutorial area with @batchcommand tutorial_world.build.

> @edit here
Building menu: Limbo

 [K]ey: Limbo
 [Q]uit the menu

> q
Closing the building menu.

> @edit here
Building menu: Limbo

 [K]ey: Limbo
 [Q]uit the menu

> k
-------------------------------------------------------------------------------
key for Limbo(#2)

You can change this value simply by entering it.

Use @ to go back to the main menu.

Current value: Limbo

> A beautiful meadow
-------------------------------------------------------------------------------

key for A beautiful meadow(#2)

You can change this value simply by entering it.

Use @ to go back to the main menu.

Current value: A beautiful meadow

> @
Building menu: A beautiful meadow

 [K]ey: A beautiful meadow
 [Q]uit the menu

> q

Closing the building menu.

> look
A beautiful meadow(#2)
Welcome to your new Evennia-based game! Visit https://www.evennia.com if you need
help, want to contribute, report issues or just join the community.
As Account #1 you can create a demo/tutorial area with @batchcommand tutorial_world.build.

Before diving into the code, let’s examine what we have:

  • When we use the @edit here command, a building menu for this room appears.

  • This menu has two choices:

    • Enter k to edit the room key. You will go into a choice where you can simply type the key room key (the way we have done here). You can use @ to go back to the menu.

    • You can use q to quit the menu.

We then check, with the look command, that the menu has modified this room key. So by adding a class, with a method and a single line of code within, we’ve added a menu with two choices.

Code explanation

Let’s examine our code again:

class RoomBuildingMenu(BuildingMenu):

    """
    Building menu to edit a room.

    For the time being, we have only one choice: key, to edit the room key.

    """

    def init(self, room):
        self.add_choice("key", "k", attr="key")
  • We first create a class inheriting from BuildingMenu. This is usually the case when we want to create a building menu with this contrib.

  • In this class, we override the init method, which is called when the menu opens.

  • In this init method, we call add_choice. This takes several arguments, but we’ve defined only three here:

    • The choice name. This is mandatory and will be used by the building menu to know how to display this choice.

    • The command key to access this choice. We’ve given a simple "k". Menu commands usually are pretty short (that’s part of the reason building menus are appreciated by builders). You can also specify additional aliases, but we’ll see that later.

    • We’ve added a keyword argument, attr. This tells the building menu that when we are in this choice, the text we enter goes into this attribute name. It’s called attr, but it could be a room attribute or a typeclass persistent or non-persistent attribute (we’ll see other examples as well).

We’ve added the menu choice for key here, why is another menu choice defined for quit?

Our building menu creates a choice at the end of our choice list if it’s a top-level menu (sub-menus don’t have this feature). You can, however, override it to provide a different “quit” message or to perform some actions.

I encourage you to play with this code. As simple as it is, it offers some functionalities already.

Customizing building menus

This somewhat long section explains how to customize building menus. There are different ways depending on what you would like to achieve. We’ll go from specific to more advanced here.

Generic choices

In the previous example, we’ve used add_choice. This is one of three methods you can use to add choices. The other two are to handle more generic actions:

  • add_choice_edit: this is called to add a choice which points to the EvEditor. It is used to edit a description in most cases, although you could edit other things. We’ll see an example shortly. add_choice_edit uses most of the add_choice keyword arguments we’ll see, but usually we specify only two (sometimes three):

    • The choice title as usual.

    • The choice key (command key) as usual.

    • Optionally, the attribute of the object to edit, with the attr keyword argument. By default, attr contains db.desc. It means that this persistent data attribute will be edited by the EvEditor. You can change that to whatever you want though.

  • add_choice_quit: this allows to add a choice to quit the editor. Most advisable! If you don’t do it, the building menu will do it automatically, except if you really tell it not to. Again, you can specify the title and key of this menu. You can also call a function when this menu closes.

So here’s a more complete example (you can replace your RoomBuildingMenu class in commands/building.py to see it):

class RoomBuildingMenu(BuildingMenu):

    """
    Building menu to edit a room.
    """

    def init(self, room):
        self.add_choice("key", "k", attr="key")
        self.add_choice_edit("description", "d")
        self.add_choice_quit("quit this editor", "q")

So far, our building menu class is still thin… and yet we already have some interesting feature. See for yourself the following MUD client output (again, the commands are prefixed with > to distinguish them):

> @reload

> @edit here
Building menu: A beautiful meadow

 [K]ey: A beautiful meadow
 [D]escription:
   Welcome to your new Evennia-based game! Visit https://www.evennia.com if you need
help, want to contribute, report issues or just join the community.
As Account #1 you can create a demo/tutorial area with @batchcommand tutorial_world.build.
 [Q]uit this editor

> d

----------Line Editor [editor]----------------------------------------------------
01| Welcome to your new |wEvennia|n-based game! Visit https://www.evennia.com if you need
02| help, want to contribute, report issues or just join the community.
03| As Account #1 you can create a demo/tutorial area with |w@batchcommand tutorial_world.build|n.

> :DD

----------[l:03 w:034 c:0247]------------(:h for help)----------------------------
Cleared 3 lines from buffer.

> This is a beautiful meadow. But so beautiful I can't describe it.

01| This is a beautiful meadow. But so beautiful I can't describe it.

> :wq
Building menu: A beautiful meadow

 [K]ey: A beautiful meadow
 [D]escription:
   This is a beautiful meadow.  But so beautiful I can't describe it.
 [Q]uit this editor

> q
Closing the building menu.

> look
A beautiful meadow(#2)
This is a beautiful meadow.  But so beautiful I can't describe it.

So by using the d shortcut in our building menu, an EvEditor opens. You can use the EvEditor commands (like we did here, :DD to remove all, :wq to save and quit). When you quit the editor, the description is saved (here, in room.db.desc) and you go back to the building menu.

Notice that the choice to quit has changed too, which is due to our adding add_choice_quit. In most cases, you will probably not use this method, since the quit menu is added automatically.

add_choice options

add_choice and the two methods add_choice_edit and add_choice_quit take a lot of optional arguments to make customization easier. Some of these options might not apply to add_choice_edit or add_choice_quit however.

Below are the options of add_choice, specify them as arguments:

  • The first positional, mandatory argument is the choice title, as we have seen. This will influence how the choice appears in the menu.

  • The second positional, mandatory argument is the command key to access to this menu. It is best to use keyword arguments for the other arguments.

  • The aliases keyword argument can contain a list of aliases that can be used to access to this menu. For instance: add_choice(..., aliases=['t'])

  • The attr keyword argument contains the attribute to edit when this choice is selected. It’s a string, it has to be the name, from the object (specified in the menu constructor) to reach this attribute. For instance, a attr of "key" will try to find obj.key to read and write the attribute. You can specify more complex attribute names, for instance, attr="db.desc" to set the desc persistent attribute, or attr="ndb.something" so use a non-persistent data attribute on the object.

  • The text keyword argument is used to change the text that will be displayed when the menu choice is selected. Menu choices provide a default text that you can change. Since this is a long text, it’s useful to use multi-line strings (see an example below).

  • The glance keyword argument is used to specify how to display the current information while in the menu, when the choice hasn’t been opened. If you examine the previous examples, you will see that the current (key or db.desc) was shown in the menu, next to the command key. This is useful for seeing at a glance the current value (hence the name). Again, menu choices will provide a default glance if you don’t specify one.

  • The on_enter keyword argument allows to add a callback to use when the menu choice is opened. This is more advanced, but sometimes useful.

  • The on_nomatch keyword argument is called when, once in the menu, the caller enters some text that doesn’t match any command (including the @ command). By default, this will edit the specified attr.

  • The on_leave keyword argument allows to specify a callback used when the caller leaves the menu choice. This can be useful for cleanup as well.

These are a lot of possibilities, and most of the time you won’t need them all. Here is a short example using some of these arguments (again, replace the RoomBuildingMenu class in commands/building.py with the following code to see it working):

class RoomBuildingMenu(BuildingMenu):

    """
    Building menu to edit a room.

    For the time being, we have only one choice: key, to edit the room key.

    """

    def init(self, room):
        self.add_choice("title", key="t", attr="key", glance="{obj.key}", text="""
                -------------------------------------------------------------------------------
                Editing the title of {{obj.key}}(#{{obj.id}})

                You can change the title simply by entering it.
                Use |y{back}|n to go back to the main menu.

                Current title: |c{{obj.key}}|n
        """.format(back="|n or |y".join(self.keys_go_back)))
        self.add_choice_edit("description", "d")

Reload your game and see it in action:

> @edit here
Building menu: A beautiful meadow

 [T]itle: A beautiful meadow
 [D]escription:
   This is a beautiful meadow.  But so beautiful I can't describe it.
 [Q]uit the menu

> t
-------------------------------------------------------------------------------

Editing the title of A beautiful meadow(#2)

You can change the title simply by entering it.
Use @ to go back to the main menu.

Current title: A beautiful meadow

> @

Building menu: A beautiful meadow

 [T]itle: A beautiful meadow
 [D]escription:
   This is a beautiful meadow.  But so beautiful I can't describe it.
 [Q]uit the menu

> q
Closing the building menu.

The most surprising part is no doubt the text. We use the multi-line syntax (with """). Excessive spaces will be removed from the left for each line automatically. We specify some information between braces… sometimes using double braces. What might be a bit odd:

  • {back} is a direct format argument we’ll use (see the .format specifiers).

  • {{obj...}} refers to the object being edited. We use two braces, because .format will remove them.

In glance, we also use {obj.key} to indicate we want to show the room’s key.

Everything can be a function

The keyword arguments of add_choice are often strings (type str). But each of these arguments can also be a function. This allows for a lot of customization, since we define the callbacks that will be executed to achieve such and such an operation.

To demonstrate, we will try to add a new feature. Our building menu for rooms isn’t that bad, but it would be great to be able to edit exits too. So we can add a new menu choice below description… but how to actually edit exits? Exits are not just an attribute to set: exits are objects (of type Exit by default) which stands between two rooms (object of type Room). So how can we show that?

First let’s add a couple of exits in limbo, so we have something to work with:

@tunnel n
@tunnel s

This should create two new rooms, exits leading to them from limbo and back to limbo.

> look
A beautiful meadow(#2)
This is a beautiful meadow.  But so beautiful I can't describe it.
Exits: north(#4) and south(#7)

We can access room exits with the exits property:

> @py here.exits
[<Exit: north>, <Exit: south>]

So what we need is to display this list in our building menu… and to allow to edit it would be great. Perhaps even add new exits?

First of all, let’s write a function to display the glance on existing exits. Here’s the code, it’s explained below:

class RoomBuildingMenu(BuildingMenu):

    """
    Building menu to edit a room.

    """

    def init(self, room):
        self.add_choice("title", key="t", attr="key", glance="{obj.key}", text="""
                -------------------------------------------------------------------------------
                Editing the title of {{obj.key}}(#{{obj.id}})

                You can change the title simply by entering it.
                Use |y{back}|n to go back to the main menu.

                Current title: |c{{obj.key}}|n
        """.format(back="|n or |y".join(self.keys_go_back)))
        self.add_choice_edit("description", "d")
        self.add_choice("exits", "e", glance=glance_exits, attr="exits")


# Menu functions
def glance_exits(room):
    """Show the room exits."""
    if room.exits:
        glance = ""
        for exit in room.exits:
            glance += f"\n  |y{exit.key}|n"

        return glance

    return "\n  |gNo exit yet|n"

When the building menu opens, it displays each choice to the caller. A choice is displayed with its title (rendered a bit nicely to show the key as well) and the glance. In the case of the exits choice, the glance is a function, so the building menu calls this function giving it the object being edited (the room here). The function should return the text to see.

> @edit here
Building menu: A beautiful meadow

 [T]itle: A beautiful meadow
 [D]escription:
   This is a beautiful meadow.  But so beautiful I can't describe it.
 [E]xits:
  north
  south
 [Q]uit the menu

> q
Closing the editor.

How do I know the parameters of the function to give?

The function you give can accept a lot of different parameters. This allows for a flexible approach but might seem complicated at first. Basically, your function can accept any parameter, and the building menu will send only the parameter based on their names. If your function defines an argument named caller for instance (like def func(caller): ), then the building menu knows that the first argument should contain the caller of the building menu. Here are the arguments, you don’t have to specify them (if you do, they need to have the same name):

  • menu: if your function defines an argument named menu, it will contain the building menu itself.

  • choice: if your function defines an argument named choice, it will contain the Choice object representing this menu choice.

  • string: if your function defines an argument named string, it will contain the user input to reach this menu choice. This is not very useful, except on nomatch callbacks which we’ll see later.

  • obj: if your function defines an argument named obj, it will contain the building menu edited object.

  • caller: if your function defines an argument named caller, it will contain the caller of the building menu.

  • Anything else: any other argument will contain the object being edited by the building menu.

So in our case:

def glance_exits(room):

The only argument we need is room. It’s not present in the list of possible arguments, so the editing object of the building menu (the room, here) is given.

Why is it useful to get the menu or choice object?

Most of the time, you will not need these arguments. In very rare cases, you will use them to get specific data (like the default attribute that was set). This tutorial will not elaborate on these possibilities. Just know that they exist.

We should also define a text callback, so that we can enter our menu to see the room exits. We’ll see how to edit them in the next section but this is a good opportunity to show a more complete callback. To see it in action, as usual, replace the class and functions in commands/building.py:

# Our building menu

class RoomBuildingMenu(BuildingMenu):

    """
    Building menu to edit a room.

    """

    def init(self, room):
        self.add_choice("title", key="t", attr="key", glance="{obj.key}", text="""
                -------------------------------------------------------------------------------
                Editing the title of {{obj.key}}(#{{obj.id}})

                You can change the title simply by entering it.
                Use |y{back}|n to go back to the main menu.

                Current title: |c{{obj.key}}|n
        """.format(back="|n or |y".join(self.keys_go_back)))
        self.add_choice_edit("description", "d")
        self.add_choice("exits", "e", glance=glance_exits, attr="exits", text=text_exits)


# Menu functions
def glance_exits(room):
    """Show the room exits."""
    if room.exits:
        glance = ""
        for exit in room.exits:
            glance += f"\n  |y{exit.key}|n"

        return glance

    return "\n  |gNo exit yet|n"

def text_exits(caller, room):
    """Show the room exits in the choice itself."""
    text = "-" * 79
    text += "\n\nRoom exits:"
    text += "\n Use |y@c|n to create a new exit."
    text += "\n\nExisting exits:"
    if room.exits:
        for exit in room.exits:
            text += f"\n  |y@e {exit.key}|n"
            if exit.aliases.all():
                text += " (|y{aliases}|n)".format(aliases="|n, |y".join(
                    alias for alias in exit.aliases.all()
                ))
            if exit.destination:
                text += f" toward {exit.get_display_name(caller)}"
    else:
        text += "\n\n |gNo exit has yet been defined.|n"

    return text

Look at the second callback in particular. It takes an additional argument, the caller (remember, the argument names are important, their order is not relevant). This is useful for displaying destination of exits accurately. Here is a demonstration of this menu:

> @edit here
Building menu: A beautiful meadow

 [T]itle: A beautiful meadow
 [D]escription:
   This is a beautiful meadow.  But so beautiful I can't describe it.
 [E]xits:
  north
  south
 [Q]uit the menu

> e
-------------------------------------------------------------------------------

Room exits:
 Use @c to create a new exit.

Existing exits:
  @e north (n) toward north(#4)
  @e south (s) toward south(#7)

> @
Building menu: A beautiful meadow

 [T]itle: A beautiful meadow
 [D]escription:
   This is a beautiful meadow.  But so beautiful I can't describe it.
 [E]xits:
  north
  south
 [Q]uit the menu

> q
Closing the building menu.

Using callbacks allows a great flexibility. We’ll now see how to handle sub-menus.

Full sub-menu as separate classes

The best way to handle individual exits is to create two separate classes:

  • One for the room menu.

  • One for the individual exit menu.

The first one will have to redirect on the second. This might be more intuitive and flexible, depending on what you want to achieve. So let’s build two menus:

# Still in commands/building.py, replace the menu class and functions by...
# Our building menus

class RoomBuildingMenu(BuildingMenu):

    """
    Building menu to edit a room.
    """

    def init(self, room):
        self.add_choice("title", key="t", attr="key", glance="{obj.key}", text="""
                -------------------------------------------------------------------------------
                Editing the title of {{obj.key}}(#{{obj.id}})

                You can change the title simply by entering it.
                Use |y{back}|n to go back to the main menu.

                Current title: |c{{obj.key}}|n
        """.format(back="|n or |y".join(self.keys_go_back)))
        self.add_choice_edit("description", "d")
        self.add_choice("exits", "e", glance=glance_exits, text=text_exits,
on_nomatch=nomatch_exits)


# Menu functions
def glance_exits(room):
    """Show the room exits."""
    if room.exits:
        glance = ""
        for exit in room.exits:
            glance += f"\n  |y{exit.key}|n"

        return glance

    return "\n  |gNo exit yet|n"

def text_exits(caller, room):
    """Show the room exits in the choice itself."""
    text = "-" * 79
    text += "\n\nRoom exits:"
    text += "\n Use |y@c|n to create a new exit."
    text += "\n\nExisting exits:"
    if room.exits:
        for exit in room.exits:
            text += f"\n  |y@e {exit.key}|n"
            if exit.aliases.all():
                text += " (|y{aliases}|n)".format(aliases="|n, |y".join(
                    alias for alias in exit.aliases.all()
                ))
            if exit.destination:
                text += f" toward {exit.get_display_name(caller)}"
    else:
        text += "\n\n |gNo exit has yet been defined.|n"

    return text

def nomatch_exits(menu, caller, room, string):
    """
    The user typed something in the list of exits.  Maybe an exit name?
    """
    string = string[3:]
    exit = caller.search(string, candidates=room.exits)
    if exit is None:
        return

    # Open a sub-menu, using nested keys
    caller.msg(f"Editing: {exit.key}")
    menu.open_submenu("commands.building.ExitBuildingMenu", exit, parent_keys=["e"])
    return False

class ExitBuildingMenu(BuildingMenu):

    """
    Building menu to edit an exit.

    """

    def init(self, exit):
        self.add_choice("key", key="k", attr="key", glance="{obj.key}")
        self.add_choice_edit("description", "d")

The code might be much easier to read. But before detailing it, let’s see how it behaves in the game:

> @edit here
Building menu: A beautiful meadow

 [T]itle: A beautiful meadow
 [D]escription:
   This is a beautiful meadow.  But so beautiful I can't describe it.
 [E]xits:
  door
  south
 [Q]uit the menu

> e
-------------------------------------------------------------------------------

Room exits:
 Use @c to create a new exit.

Existing exits:
  @e door (n) toward door(#4)
  @e south (s) toward south(#7)

Editing: door

> @e door
Building menu: door

 [K]ey: door
 [D]escription:
   None

> k
-------------------------------------------------------------------------------
key for door(#4)

You can change this value simply by entering it.

Use @ to go back to the main menu.

Current value: door

> north

-------------------------------------------------------------------------------
key for north(#4)

You can change this value simply by entering it.

Use @ to go back to the main menu.

Current value: north

> @
Building menu: north

 [K]ey: north
 [D]escription:
   None

> d
----------Line Editor [editor]----------------------------------------------------
01| None
----------[l:01 w:001 c:0004]------------(:h for help)----------------------------

> :DD
Cleared 1 lines from buffer.

> This is the northern exit. Cool huh?
01| This is the northern exit. Cool huh?

> :wq
Building menu: north
 [K]ey: north
 [D]escription:
   This is the northern exit.  Cool huh?

> @
-------------------------------------------------------------------------------
Room exits:
 Use @c to create a new exit.

Existing exits:
  @e north (n) toward north(#4)
  @e south (s) toward south(#7)

> @
Building menu: A beautiful meadow

 [T]itle: A beautiful meadow
 [D]escription:
   This is a beautiful meadow.  But so beautiful I can't describe it.
 [E]xits:
  north
  south
 [Q]uit the menu

> q
Closing the building menu.

> look
A beautiful meadow(#2)
This is a beautiful meadow.  But so beautiful I can't describe it.
Exits: north(#4) and south(#7)
> @py here.exits[0]
>>> here.exits[0]
north
> @py here.exits[0].db.desc
>>> here.exits[0].db.desc
This is the northern exit.  Cool huh?

Very simply, we created two menus and bridged them together. This needs much less callbacks. There is only one line in the nomatch_exits to add:

    menu.open_submenu("commands.building.ExitBuildingMenu", exit, parent_keys=["e"])

We have to call open_submenu on the menu object (which opens, as its name implies, a sub menu) with three arguments:

  • The path of the menu class to create. It’s the Python class leading to the menu (notice the dots).

  • The object that will be edited by the menu. Here, it’s our exit, so we give it to the sub-menu.

  • The keys of the parent to open when the sub-menu closes. Basically, when we’re in the root of the sub-menu and press @, we’ll open the parent menu, with the parent keys. So we specify ["e"], since the parent menus is the “exits” choice.

And that’s it. The new class will be automatically created. As you can see, we have to create a on_nomatch callback to open the sub-menu, but once opened, it automatically close whenever needed.

Generic menu options

There are some options that can be set on any menu class. These options allow for greater customization. They are class attributes (see the example below), so just set them in the class body:

  • keys_go_back (default to ["@"]): the keys to use to go back in the menu hierarchy, from choice to root menu, from sub-menu to parent-menu. By default, only a @ is used. You can change this key for one menu or all of them. You can define multiple return commands if you want.

  • sep_keys (default "."): this is the separator for nested keys. There is no real need to redefine it except if you really need the dot as a key, and need nested keys in your menu.

  • joker_key (default to "*"): used for nested keys to indicate “any key”. Again, you shouldn’t need to change it unless you want to be able to use the @*@ in a command key, and also need nested keys in your menu.

  • min_shortcut (default to 1): although we didn’t see it here, one can create a menu choice without giving it a key. If so, the menu system will try to “guess” the key. This option allows to change the minimum length of any key for security reasons.

To set one of them just do so in your menu class(es):

class RoomBuildingMenu(BuildingMenu):
    keys_go_back = ["/"]
    min_shortcut = 2

Conclusion

Building menus mean to save you time and create a rich yet simple interface. But they can be complicated to learn and require reading the source code to find out how to do such and such a thing. This documentation, however long, is an attempt at describing this system, but chances are you’ll still have questions about it after reading it, especially if you try to push this system to a great extent. Do not hesitate to read the documentation of this contrib, it’s meant to be exhaustive but user-friendly.


This document page is generated from evennia/contrib/base_systems/building_menu/README.md. Changes to this file will be overwritten, so edit that file rather than this one.