SwiftlyS2
Development

Menus

SwiftlyS2 provides a powerful menu system that allows you to create interactive menus for players. The system uses a fluent builder API for easy menu creation and customization.

Overview

The menu system is accessed through Core.Menus which implements IMenuManagerAPI. It provides a comprehensive set of tools for creating dynamic, interactive menus with support for various option types, custom styling, and event handling.

Creating a Menu

Use Core.Menus.CreateBuilder() to create a new menu builder, configure it with the fluent API, then call Build() to create the menu instance.

var menu = Core.Menus.CreateBuilder()
    .Design.SetMenuTitle("Settings Menu")
    .EnableSound()
    .AddOption(new ButtonMenuOption("Click me"))
    .Build();

// Open the menu for a player
Core.Menus.OpenMenuForPlayer(player, menu);

The menu builder provides a fluent API for configuring menus. All builder methods return the builder instance, allowing you to chain multiple calls.

Basic Configuration

var builder = Core.Menus.CreateBuilder();

builder
    .EnableSound()                    // Enable sound effects
    .SetPlayerFrozen(true)            // Freeze player while menu is open
    .SetAutoCloseDelay(10.0f);        // Auto-close after 10 seconds

// Design configuration
builder.Design
    .SetMenuTitle("Main Menu")
    .SetMaxVisibleItems(5)            // Set max visible items per page (1-5)
    .SetMenuTitleVisible(true)        // Show/hide title
    .SetMenuFooterVisible(true)       // Show/hide footer
    .EnableAutoAdjustVisibleItems()   // Auto-adjust visible items when hiding title/footer
    .SetGlobalScrollStyle(MenuOptionScrollStyle.CenterFixed);

When creating menus directly with CreateMenu, you can pass configuration objects:

var config = new MenuConfiguration
{
    Title = "Settings",
    HideTitle = false,
    HideFooter = false,
    PlaySound = true,
    MaxVisibleItems = 5,
    AutoIncreaseVisibleItems = true,
    FreezePlayer = false,
    AutoCloseAfter = 0f
};

var keybinds = new MenuKeybindOverrides
{
    Select = KeyBind.E,
    Move = KeyBind.W,
    MoveBack = KeyBind.S,
    Exit = KeyBind.Esc
};

var menu = Core.Menus.CreateMenu(
    configuration: config,
    keybindOverrides: keybinds,
    parent: null,
    optionScrollStyle: MenuOptionScrollStyle.CenterFixed,
    optionTextStyle: MenuOptionTextStyle.TruncateEnd
);

SwiftlyS2 provides several built-in menu option types that you can use or extend.

Button Option

A simple clickable button that executes a callback when selected.

var button = new ButtonMenuOption("Click me");
button.Click += async (sender, args) =>
{
    args.Player.PrintToChat("Button clicked!");
    return ValueTask.CompletedTask;
};

builder.AddOption(button);

Toggle Option

A boolean toggle that switches between on/off states, displaying ✔ or ✘.

var toggle = new ToggleMenuOption("Enable Feature");
toggle.Click += async (sender, args) =>
{
    var isToggled = toggle.GetDisplayText(args.Player, 0).Contains("✔");
    args.Player.PrintToChat($"Feature is now {(isToggled ? "enabled" : "disabled")}");
    return ValueTask.CompletedTask;
};

builder.AddOption(toggle);

Slider Option

A slider that allows selecting numeric values within a range with visual feedback.

var slider = new SliderMenuOption(
    text: "Volume",
    min: 0f,
    max: 100f,
    defaultValue: 50f,
    step: 5f,
    totalBars: 10
);

slider.ValueChanged += (sender, args) =>
{
    args.Player.PrintToChat($"Volume set to: {args.NewValue}");
};

builder.AddOption(slider);

// Get or set slider value
var currentVolume = slider.GetValue(player);
slider.SetValue(player, 75f);

Choice Option

A choice option that cycles through predefined string values.

string[] difficulties = { "Easy", "Medium", "Hard", "Expert" };

var choice = new ChoiceMenuOption(
    text: "Difficulty",
    choices: difficulties,
    defaultChoice: "Medium"
);

choice.ValueChanged += (sender, args) =>
{
    args.Player.PrintToChat($"Difficulty changed from {args.OldValue} to {args.NewValue}");
};

builder.AddOption(choice);

// Get or set choice
var currentDifficulty = choice.GetSelectedChoice(player);
choice.SetSelectedChoice(player, "Hard");

Text Option

Display non-interactive text in the menu.

var text = new TextMenuOption("Welcome to the settings menu!");
text.TextSize = MenuOptionTextSize.Large;
text.PlaySound = false;

builder.AddOption(text);

Input Option

Allows players to enter text input through chat.

var input = new InputMenuOption(
    text: "Enter your name",
    defaultValue: "",
    maxLength: 16,
    hintMessage: "Type your name in chat",
    validator: (value) => !string.IsNullOrWhiteSpace(value) && value.Length >= 3
);

input.ValueChanged += (sender, args) =>
{
    args.Player.PrintToChat($"Name changed to: {args.NewValue}");
};

builder.AddOption(input);

// Get or set input value
var currentName = input.GetValue(player);
input.SetValue(player, "NewName");

Progress Bar Option

Display a progress bar that updates dynamically.

float progress = 0.0f;

var progressBar = new ProgressBarMenuOption(
    text: "Loading",
    progressProvider: () => progress,
    multiLine: false,
    showPercentage: true,
    filledChar: "█",
    emptyChar: "░"
);

builder.AddOption(progressBar);

// Update progress externally
progress = 0.75f; // 75%

// Or set per-player progress provider
progressBar.SetProgressProvider(player, () => player.Health / 100f);

Opens another menu when clicked, with support for lazy loading.

// Pre-built submenu
var submenu = Core.Menus.CreateBuilder()
    .Design.SetMenuTitle("Submenu")
    .AddOption(new ButtonMenuOption("Submenu Option"))
    .Build();

var submenuOption = new SubmenuMenuOption("Open Submenu", submenu);
builder.AddOption(submenuOption);

// Lazy-loaded submenu (built when clicked)
var lazySubmenu = new SubmenuMenuOption("Lazy Submenu", () =>
{
    return Core.Menus.CreateBuilder()
        .Design.SetMenuTitle("Dynamically Created")
        .AddOption(new TextMenuOption("This menu was just created!"))
        .Build();
});
builder.AddOption(lazySubmenu);

// Async lazy-loaded submenu
var asyncSubmenu = new SubmenuMenuOption("Async Submenu", async () =>
{
    await Task.Delay(500); // Simulate loading
    return Core.Menus.CreateBuilder()
        .Design.SetMenuTitle("Loaded Menu")
        .Build();
});
builder.AddOption(asyncSubmenu);

All menu options inherit from IMenuOption and share common properties:

var option = new ButtonMenuOption("Example");

// Basic properties
option.Text = "Updated Text";
option.Visible = true;
option.Enabled = true;
option.PlaySound = true;
option.Tag = customData; // Store custom data

// Text styling
option.TextSize = MenuOptionTextSize.Large;
option.TextStyle = MenuOptionTextStyle.ScrollLeftFade;
option.MaxWidth = 80f;

// Readonly properties
int lineCount = option.LineCount; // Number of lines this option occupies
bool closeAfter = option.CloseAfterClick;

Text Size Options

public enum MenuOptionTextSize
{
    ExtraSmall,   // fontSize-xs
    Small,        // fontSize-s
    SmallMedium,  // fontSize-sm
    Medium,       // fontSize-m (default)
    MediumLarge,  // fontSize-ml
    Large,        // fontSize-l
    ExtraLarge    // fontSize-xl
}

Text Overflow Styles

public enum MenuOptionTextStyle
{
    TruncateEnd,        // "Very Long Text..." (truncate at end)
    TruncateBothEnds,   // "...Middle Text..." (keep middle)
    ScrollLeftFade,     // Scroll left with fade effect
    ScrollRightFade,    // Scroll right with fade effect
    ScrollLeftLoop,     // Continuous scroll left
    ScrollRightLoop     // Continuous scroll right
}

Menu options support various events for customization and tracking:

var option = new ButtonMenuOption("Example");

// Property change events
option.TextChanged += (sender, args) =>
{
    Console.WriteLine($"Text changed for option");
};

option.VisibilityChanged += (sender, args) =>
{
    Console.WriteLine($"Visibility changed to: {option.Visible}");
};

option.EnabledChanged += (sender, args) =>
{
    Console.WriteLine($"Enabled changed to: {option.Enabled}");
};

// Validation event (can cancel interaction)
option.Validating += (sender, args) =>
{
    if (!Core.Permission.PlayerHasPermission(args.Player.SteamID, "admin"))
    {
        args.Cancel = true;
        args.CancelReason = "Admin only!";
        args.Player.PrintToChat("You don't have permission!");
    }
};

// Click event
option.Click += async (sender, args) =>
{
    args.Player.PrintToChat("Option selected!");
    args.CloseMenu = false; // Set to true to close menu after handling
    await Task.CompletedTask;
};

// Formatting events (customize display)
option.BeforeFormat += (sender, args) =>
{
    // Customize text before HTML formatting
    args.CustomText = $"[CUSTOM] {args.Option.Text}";
};

option.AfterFormat += (sender, args) =>
{
    // Customize HTML after formatting
    args.CustomText = $"<font color='#FF0000'>{args.CustomText}</font>";
};

Hierarchical Menus

Create parent-child menu relationships for navigation:

var mainMenu = Core.Menus.CreateBuilder()
    .Design.SetMenuTitle("Main Menu")
    .Build();

var subMenu = Core.Menus.CreateBuilder()
    .Design.SetMenuTitle("Sub Menu")
    .BindToParent(mainMenu)  // Set parent relationship
    .AddOption(new ButtonMenuOption("Back to Main"))
    .Build();

// The back button can navigate to parent
var backButton = new ButtonMenuOption("Back");
backButton.Click += async (sender, args) =>
{
    if (subMenu.Parent != null)
    {
        Core.Menus.OpenMenuForPlayer(args.Player, subMenu.Parent);
    }
    await Task.CompletedTask;
};

Opening and Closing Menus

// Open a menu for a player
Core.Menus.OpenMenuForPlayer(player, menu);

// Open for all players
Core.Menus.OpenMenu(menu);

// Close a menu for a player
Core.Menus.CloseMenuForPlayer(player, menu);

// Close menu for all players who have it open
Core.Menus.CloseMenu(menu);

// Close all menus for all players
Core.Menus.CloseAllMenus();

// Check current menu
IMenuAPI? currentMenu = Core.Menus.GetCurrentMenu(player);

Subscribe to global menu manager events:

Core.Menus.MenuOpened += (sender, args) =>
{
    Console.WriteLine($"Menu '{args.Menu?.Configuration.Title}' opened for {args.Player?.Controller.PlayerName}");
};

Core.Menus.MenuClosed += (sender, args) =>
{
    Console.WriteLine($"Menu '{args.Menu?.Configuration.Title}' closed for {args.Player?.Controller.PlayerName}");
};

Subscribe to per-menu events:

menu.OptionHovering += (sender, args) =>
{
    // Player is hovering over an option
    var option = args.Options?.FirstOrDefault();
    Console.WriteLine($"Hovering: {option?.Text}");
};

Control how the menu scrolls when navigating:

public enum MenuOptionScrollStyle
{
    // Selection indicator moves, content stays fixed until edge
    LinearScroll,

    // Indicator stays centered, options scroll around it (circular)
    CenterFixed,

    // Indicator moves to center then stays, can move at edges
    WaitingCenter
}

// Set globally for menu
builder.Design.SetGlobalScrollStyle(MenuOptionScrollStyle.CenterFixed);

// Or when creating menu directly
var menu = Core.Menus.CreateMenu(
    configuration: config,
    keybindOverrides: keybinds,
    optionScrollStyle: MenuOptionScrollStyle.WaitingCenter
);

Custom Key Bindings

Override default menu controls:

builder
    .SetSelectButton(KeyBind.E | KeyBind.Mouse1)      // Multiple keys with |
    .SetMoveForwardButton(KeyBind.W)
    .SetMoveBackwardButton(KeyBind.S)
    .SetExitButton(KeyBind.Esc | KeyBind.Q);

Dynamic Menu Content

Add and remove options dynamically:

// Add option to existing menu
var newOption = new ButtonMenuOption("Dynamic Option");
menu.AddOption(newOption);

// Remove option
menu.RemoveOption(newOption);

// Move player selection
menu.MoveToOption(player, specificOption);
menu.MoveToOptionIndex(player, 2);

// Get current selection
var currentOption = menu.GetCurrentOption(player);
var currentIndex = menu.GetCurrentOptionIndex(player);

Advanced Option Features

Per-Player Visibility and Enabled State

var option = new ButtonMenuOption("Admin Only");

// Check per-player state
bool visibleToPlayer = option.GetVisible(player);
bool enabledForPlayer = option.GetEnabled(player);

// Control with events
option.Validating += (sender, args) =>
{
    // Disable for non-admins
    if (!IsAdmin(args.Player))
    {
        args.Cancel = true;
    }
};

Custom Option Display

// Customize how option appears to specific player
option.BeforeFormat += (sender, args) =>
{
    if (IsVIP(args.Player))
    {
        args.CustomText = $"[VIP] {args.Option.Text}";
    }
};

option.AfterFormat += (sender, args) =>
{
    // Add color for admins
    if (IsAdmin(args.Player))
    {
        args.CustomText = $"<font color='#FFD700'>{args.CustomText}</font>";
    }
};

Multi-Line Options

Options can span multiple lines:

public class CustomOption : MenuOptionBase
{
    public override int LineCount => 2; // This option uses 2 lines

    public override string GetDisplayText(IPlayer player, int displayLine = 0)
    {
        return displayLine switch
        {
            1 => "Line 1 content",
            2 => "Line 2 content",
            _ => "Line 1 content<br>Line 2 content"
        };
    }
}

On this page