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);Menu Builder API
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);Menu Configuration
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
);Menu Options
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);Submenu Option
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);Menu Option Properties
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 Option Events
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;
};Menu Management
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);Menu Events
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}");
};Menu Scroll Styles
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"
};
}
}