Only display menu options under specific conditions?

Ask about ACS, DECORATE, ZScript, or any other scripting questions here!

Moderator: GZDoom Developers

Forum rules
Before asking on how to use a ZDoom feature, read the ZDoom wiki first. If you still don't understand how to use a feature, then ask here.

Please bear in mind that the people helping you do not automatically know how much you know. You may be asked to upload your project file to look at. Don't be afraid to ask questions about what things mean, but also please be patient with the people trying to help you. (And helpers, please be patient with the person you're trying to help!)
milkFiend
Posts: 27
Joined: Tue Nov 21, 2023 10:30 pm
Preferred Pronouns: No Preference
Operating System Version (Optional): Windows 11
Graphics Processor: Intel (Modern GZDoom)

Only display menu options under specific conditions?

Post by milkFiend »

I was trying to make my own Main Menu and Options Menus with a MENUDEF, but I wanted to hide some options while the player is not in-game

I was hoping to have a "Resume Game" option visible in the menu only while in-game, as well as changing the "Save Game" option to follow the same rules

Is that possible to do via MENUDEF or do I need to write some ZScript?
User avatar
Player701
 
 
Posts: 1694
Joined: Wed May 13, 2009 3:15 am
Graphics Processor: nVidia with Vulkan support

Re: Only display menu options under specific conditions?

Post by Player701 »

ZScript is required here because MENUDEF's capabilities are not that advanced.

To better understand what needs to be done, consider this simple main menu that I've adapted from the built-it MENUDEF from gzdoom.pk3, which exactly replicates the Doom II main menu:

Code: Select all

ListMenu "MainMenu"
{
    LineSpacing 16
    StaticPatch 94, 2, "M_DOOM"
    Position 97, 72
    PatchItem "M_NGAME", "n", "PlayerclassMenu"
    PatchItem "M_LOADG", "l", "LoadGameMenu", 0
    PatchItem "M_SAVEG", "s", "SaveGameMenu",0
    PatchItem "M_OPTION","o", "OptionsMenu", 0
    PatchItem "M_QUITG", "q", "QuitMenu", 0
}
First of all, to use ZScript with menus, a Class directive needs to be added to the ListMenu block:

Code: Select all

Class "MyMainMenu"
This tells GZDoom to use a custom menu class called MyMainMenu instead of the built-in ListMenu for this menu. Let's write such a class:

Code: Select all

class MyMainMenu : ListMenu
{
    override void Init(Menu parent, ListMenuDescriptor desc)
    {
        Super.Init(parent, desc);
    }
}
Right now it doesn't do anything special - we've overridden Init but it only calls the base implementation. Before we add more code, here's one thing to remember: The instance of our menu class is re-created each time the menu is brought up. Everything that needs to persist while the menu is hidden is contained within its descriptor, which is passed here to Init as the second argument, desc. Among other stuff, it stores the menu items, as well as the index of the item currently selected.

Now, suppose we want to remove the "save game" menu item if the player is currently not in-game. Let's try a simple approach - if gamestate tells us we're not in a level, remove the menu item from the array. For that, we need to count the items in our MENUDEF:

Code: Select all

ListMenu "MainMenu"
{
    /* not an item */ Class "MyMainMenu"
    /* not an item */ LineSpacing 16
    /* #0 */ StaticPatch 94, 2, "M_DOOM"
    /* not an item */ Position 97, 72
    /* #1 */ PatchItem "M_NGAME", "n", "PlayerclassMenu"
    /* #2 */ PatchItem "M_LOADG", "l", "LoadGameMenu", 0
    /* #3 */ PatchItem "M_SAVEG", "s", "SaveGameMenu",0
    /* #4 */ PatchItem "M_OPTION","o", "OptionsMenu", 0
    /* #5 */ PatchItem "M_QUITG", "q", "QuitMenu", 0
}
The "save game" item has index #3. All right, let's add some code now:

Code: Select all

version "4.13"

class MyMainMenu : ListMenu
{
    override void Init(Menu parent, ListMenuDescriptor desc)
    {
        if (gamestate != GS_LEVEL)
        {
            desc.mItems.Delete(3);
        }

        Super.Init(parent, desc);
    }
}
Was it that simple? Actually, not quite. First, on the title screen you will notice that there is now an empty space where the "Save Game" option was:
Spoiler:
And now try closing and opening the menu again. Bah - suddenly "Options" are gone!
Spoiler:
If you try this yet again, "Quit Game" is now gone too:
Spoiler:
By now it should become clear what has been happening here: since the items array is persistent, it's not being reset each time the menu gets re-initialized. So by opening the menu multiple times, we keep removing the item at index #3 from this array - which initially points to "Save Game", then to "Options", and then to "Quit Game". (By this point, there are only 3 items left, so the contents are no longer affected.)

How do we solve this? We can't store a copy of the items array in the menu itself because we always get a new instance when the menu is brought up, which means our array will be reset to an empty one. The correct solution is to create the menu items from ZScript. In our MENUDEF, we will only leave the header because it's the only part that is guaranteed to never change:

Code: Select all

ListMenu "MainMenu"
{
    Class "MyMainMenu"
    LineSpacing 16
    StaticPatch 94, 2, "M_DOOM"
}
And the rest has to be emulated in our script code. I've tried to comment it as thoroughly as possible, so I hope you can understand what's going on:

Code: Select all

version "4.13"

class MyMainMenu : ListMenu
{
    override void Init (Menu parent, ListMenuDescriptor desc)
    {
        // Remove all existing items, except the header.
        // Otherwise, we will keep adding more and more items each time the menu is brought up.
        desc.mItems.Resize(1);

        // Set the current X and Y-offsets to add items at the proper position.
        // This emulates the "Position" directive in the MENUDEF.
        desc.mXpos = 97;
        desc.mYpos = 72;

        // Add the "New Game", "Options", and "Load Game" menu items.
        // Each call to AddPatchItem emulates the "PatchItem" MENUDEF directive.
        AddPatchItem(desc, "M_NGAME", "n", "PlayerclassMenu");
        AddPatchItem(desc, "M_OPTION", "o", "OptionsMenu", 0);
        AddPatchItem(desc, "M_LOADG", "l", "LoadGameMenu", 0);

        // If we are currently in a level, also add the "Save Game" menu item.
        if (gamestate == GS_LEVEL)
        {
            AddPatchItem(desc, "M_SAVEG", "s", "SaveGameMenu", 0);
        }

        // Add the "Quit Game" menu item.
        AddPatchItem(desc, "M_QUITG", "q", "QuitMenu", 0);            

        // Ensure the currently selected item is within the allowed range.
        // Otherwise, a VM abort might happen if this index points to a non-existent item when the menu is brought up.
        desc.mSelectedItem = Clamp(desc.mSelectedItem, 1, desc.mItems.Size() - 1);

        // Perform the rest of menu initialization.
        Super.Init(parent, desc);
    }

    private static void AddPatchItem(ListMenuDescriptor desc, string patch, String hotkey, Name child, int param = 0)
    {    
        // Find the texture for the patch.
        let tex = TexMan.CheckForTexture(patch, TexMan.TYPE_MiscPatch);

        // Create a new menu item instance.
        let item = new('ListMenuItemPatchItem');

        // Initialize the menu item.
        item.Init(desc, tex, hotkey, child, param);

        // Add the menu item.
        AddItem(desc, item);
    }

    private static void AddItem(ListMenuDescriptor desc, ListMenuItem item)
    {
        // Add the menu item to the items array.
        desc.mItems.Push(item);

        // Advance the current Y-offset to add the next item at the proper position.
        desc.mYpos += desc.mLinespacing;
    }
}
As for a "Resume Game" option, this can be done by implementing a custom menu item class inherited from ListMenuItemTextItem:

Code: Select all

class ResumeGameMenuItem : ListMenuItemTextItem
{
    void Init(ListMenuDescriptor desc)
    {
        Super.Init(desc, "Resume Game", "r", 'None');        
    }

    override bool Activate()
    {
        Menu.GetCurrentMenu().Close();
        return true;
    }
}
Suppose you want to add it to the beginning of your menu, then just add the following code to MyMainMenu::Init before the first bunch of calls to AddPatchItem:

Code: Select all

if (gamestate == GS_LEVEL)
{
    let resume = new('ResumeGameMenuItem');
    resume.Init(desc);
    AddItem(desc, resume);
}
And that's it! Well, mostly - there is still the issue of localization, but if you're willing to forgo it, you can use the provided code as-is.
milkFiend
Posts: 27
Joined: Tue Nov 21, 2023 10:30 pm
Preferred Pronouns: No Preference
Operating System Version (Optional): Windows 11
Graphics Processor: Intel (Modern GZDoom)

Re: Only display menu options under specific conditions?

Post by milkFiend »

Thank you! <3 I cannot properly put into words how much I appreciate this! Very well put together! :D And thank you for the comments in the script, I definitely needed them lol.
I'll prioritize getting the menus together then see what I can do about localization later, for now, I'll be getting busy knee-deep in menus lmao
Again, can't thank you enough! All the best to you!

Return to “Scripting”