|
CS2-Kit
C++23 library for CS2 Metamod:Source plugin development
|
Work in Progress — The menu API may change.
The menu system (CS2Kit::Menu) provides WASD-navigated center-HTML menus for CS2. Each row is a typed CS2Kit::Menu::MenuOption — buttons, toggles, choice cycles, sliders, progress bars, free-text inputs, and submenu links — built fluently with CS2Kit::Menu::MenuBuilder.
Players interact using:
| Key | Action |
|---|---|
| W | Navigate up |
| S | Navigate down |
| E | Activate the highlighted row (button click, toggle flip, choice commit, input prompt, …) |
| A | If the highlighted row is a value option (Toggle / Choice / Selector / Slider): adjust value left. Otherwise: previous page (paginated menus only). |
| D | Same as A, in the opposite direction. |
| R | Close menu (root) / go back to parent (submenu). Cancels an active chat-input capture. |
The [R] hint in the footer renders as Close on a root menu and Back on a submenu.
Use CS2Kit::Menu::MenuBuilder to construct menus with a fluent API. Pull MenuBuilder.hpp — it includes every concrete option type via the aggregate Options.hpp:
Every row is a CS2Kit::Menu::MenuOption subclass. The builder methods construct the right type for you; only reach for AddOption(std::shared_ptr<MenuOption>) if you need a custom subclass.
AddText(label) — heading or divider. Rendered in muted color, skipped by W/S.
AddButton(label, onActivate, enabled = true) — fires the callback on E. The dynamic-label variant AddDynamicButton(getLabel, onActivate, enabled = true) recomputes the label every frame, useful when the row reflects live state but isn't a toggle.
AddToggle(title, onLabel, offLabel, getState, onToggle, enabled = true) — renders "<title>: <onLabel|offLabel>". Both E and A/D flip. State lives wherever the caller keeps it (engine field, an EffectManager, a config struct); pass getter and toggle callbacks.
AddChoice<T>(title, choices, getIndex, setIndex, onCommit = nullptr, enabled = true) — A/D walks the list (wrapping), E commits the current value via onCommit. Each choice is {label, value}; T is whatever you want to pass to onCommit. State is index-based and external — the caller decides where the index lives (a captured std::shared_ptr<int> is fine for ephemeral menu state).
When onCommit is omitted, E advances to the next value (same as D) so the row stays interactive — useful for plain "pick a value, no separate apply" rows where the change is read live by another part of the menu.
AddSelector<T>(title, values, formatter, getIndex, setIndex, onCommit = nullptr, enabled = true) — like Choice, but you supply a std::function<std::string(const T&)> to derive the label. Use it when the value type doesn't carry its own pretty name (e.g. seconds → "5m", an enum → a translated label). Same E-advances-when-no-onCommit fallback as Choice.
AddSlider(title, min, max, step, getValue, setValue, enabled = true) — A/D adjusts in step units, clamped to [min, max]. Renders "<title>: [▮▮▮▯▯▯▯▯▯▯] 30/100". E does nothing by default.
AddProgressBar(title, getValue, max) — non-selectable. Renders the same bar shape as Slider but is skipped by the cursor.
AddInput(title, prompt, get, set, maxLength = 64, enabled = true) — pressing E pauses the menu, shows the prompt overlay, and routes the player's next chat line into the validator. Return false from set to re-prompt for invalid input; true accepts and resumes the menu. R during capture cancels.
This relies on CS2Kit::Sdk::ChatInputCapture — the plugin must call ChatInputCapture::Instance().TryConsume(slot, text) from its chat-message hook before its own command parsing, suppressing the chat broadcast when the call returns true. See the SDK guide for the integration snippet.
AddSubmenu(label, factory, enabled = true) — the factory is invoked lazily on E, and the returned menu is pushed onto the player's stack. R pops back to the parent.
AddOption(std::shared_ptr<MenuOption>) lets you append a custom subclass. Override GetLabel(slot), OnActivate(slot), and optionally OnHorizontal(slot, direction) (return true to consume A/D, false to fall through to page-jump).
Menus with more than CS2Kit::Menu::ItemsPerPage items (5 by default) automatically paginate. The page indicator (e.g. (2/3)) appears next to the title. The footer shows the [A/D] Page hint only when more than one page exists.
A/D is item-aware: when the highlighted row is a value option (Toggle / Choice / Selector / Slider), A/D adjusts its value and pagination is not triggered. Highlight a plain Button or Submenu row to page through.
Disabled rows and non-selectable rows (Text, ProgressBar) are skipped during W/S navigation, so the cursor never lands on a row you can't act on.
Override the default header / footer with your own HTML:
CS2Kit::Menu::MenuManager keeps a per-player menu stack and runs entirely on the game thread:
OpenMenu(slot, menu) pushes the menu and rendering begins.OnGameFrame() reads IN_FORWARD/IN_BACK/IN_USE/IN_RELOAD/IN_MOVELEFT/IN_MOVERIGHT each tick and dispatches via the CS2Kit::Menu::MenuOption virtuals.CloseAllMenus(slot) clears the entire stack.The plugin must call CS2Kit::OnGameFrame() every tick (which drives MenuManager::OnGameFrame() internally) and CS2Kit::OnPlayerDisconnect(slot) on disconnect.
Most consumers only need MenuBuilder.hpp and MenuManager.hpp. Pull Options.hpp (or an individual Options/*.hpp) only when you need to construct an option manually for AddOption, or when defining a custom MenuOption subclass.