Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
f91737c
Add native in-game Mods options tab scaffold with MiscSettingsScreen …
NikkelM Jul 8, 2026
1ad0b7a
Add native in-game Mods options tab for editing mod .cfg settings
NikkelM Jul 10, 2026
af2a326
Add native rom.mod_settings.load config helper (drop-in Chalk replace…
NikkelM Jul 10, 2026
152d794
Port config helper to native C++ (mod_config_proxy usertype); native …
NikkelM Jul 11, 2026
dd506a6
Add hover descriptions, metadata registry, native row colour and valu…
NikkelM Jul 11, 2026
5f3dabd
Add native number-box stepper rows for bounded numeric settings in th…
NikkelM Jul 11, 2026
cfa8c2c
Add native enum cycler rows (num-box string mode) for the Mods tab
NikkelM Jul 11, 2026
fcc1701
Escape markup, add display_name override, and truncate long value dis…
NikkelM Jul 12, 2026
d0c6b94
Add freetext caret navigation and fit Mods-tab value displays by glyp…
NikkelM Jul 12, 2026
d131072
Split camelCase setting keys when prettifying Mods-tab labels
NikkelM Jul 12, 2026
615d20f
Add freetext override for bounded number settings with step snapping
NikkelM Jul 12, 2026
6dbca70
Refresh freetext value in place on commit to preserve scroll position
NikkelM Jul 12, 2026
870af80
Version-guard the Mods options tab so it skips cleanly on symbol/offs…
NikkelM Jul 12, 2026
1d42f01
Add per-mod Reset, context prompts, and vanilla row spacing to the Mo…
NikkelM Jul 12, 2026
d0a7642
Remove redundant Back row and clean up mod-settings menu comments
NikkelM Jul 12, 2026
50f3e3c
Show mod descriptions and add drill-down config groups to the Mods op…
NikkelM Jul 12, 2026
1982c8b
Match native option-row fade ease for Mods-tab scroll and view transi…
NikkelM Jul 13, 2026
fc9d0ea
Split CamelCase in Mods-tab mod names using the setting-key friendly-…
NikkelM Jul 13, 2026
7cdc39a
Hide Hell2Modding-General from the Mods-tab list
NikkelM Jul 13, 2026
e74b7f9
Render bounded-number settings as native sliders with percentage disp…
NikkelM Jul 13, 2026
551fd31
Add Mods-tab controller support: initial focus, press-A slider/enum e…
NikkelM Jul 13, 2026
29f5f35
Improve Mods-tab restart prompt: display_name, trigger on leaving a m…
NikkelM Jul 13, 2026
2adeb9a
Capitalize the first letter in the Mods-tab friendly-name reformat
NikkelM Jul 13, 2026
933d7d8
Fix Mods tab: proper localization id, restart-prompt display_name + l…
NikkelM Jul 13, 2026
0e5ee8e
Persist native settings before forced restart
NikkelM Jul 14, 2026
c09cdaa
Disable OS fallback restart confirmation window
NikkelM Jul 14, 2026
ee17a29
Add rom.mod_settings.opt_out(): grey out opted-out mods in the menu w…
NikkelM Jul 14, 2026
f0b5a62
Preserve mod-list scroll position and re-focus the opened mod when ba…
NikkelM Jul 14, 2026
dcce1a9
Block disabling a mod that enabled mods depend on, with an info popup…
NikkelM Jul 14, 2026
326a09c
Fix heap corruption from cross-CRT dialog free (allocate MessageDialo…
NikkelM Jul 14, 2026
b5cd9d3
Mods menu review fixes: free rows on close, harden restart-confirm gu…
NikkelM Jul 14, 2026
d402567
Re-select the drilled-through row (mod or group) when backing out, at…
NikkelM Jul 14, 2026
f666df0
Reset config settings for Chalk mods too via the entry's own stored d…
NikkelM Jul 14, 2026
4c7b5e7
Support localization tables for display_name, description, and enum l…
NikkelM Jul 14, 2026
705d2e1
Add LuaCATS config schema + authoring guide and document rom.mod_sett…
NikkelM Jul 14, 2026
0053242
Add per-setting editable_context (main_menu/in_save) with read-only g…
NikkelM Jul 14, 2026
e0968a0
Keep the partial final page's offset when backing out of a mod
NikkelM Jul 14, 2026
8442127
Dynamic settings, action buttons
NikkelM Jul 18, 2026
cc72391
Fix action-button hitbox width and prompt refresh on drag release
NikkelM Jul 18, 2026
197657c
Fix keyboard/controller slider adjustment not being discrete
NikkelM Jul 19, 2026
a03680f
Fix one-frame prompt/description/highlight blink on setting rebuilds
NikkelM Jul 19, 2026
810d501
Add keyboard/controller cross-page scrolling in the Mods tab
NikkelM Jul 20, 2026
1e609c4
Switch Mods-tab cross-page scrolling to the native scroll arrows
NikkelM Jul 20, 2026
27cea66
Fix slider row staying highlighted after a settings rebuild
NikkelM Jul 20, 2026
e1dadbd
Skip disabled rows in Mods-tab keyboard/controller navigation
NikkelM Jul 21, 2026
95b0108
Merge branch 'master' into nikkelm/mod-menu
NikkelM Jul 22, 2026
f4d11eb
Only adjust the entered slider with left/right in Mods settings
NikkelM Jul 22, 2026
647b2b1
Play the vanilla toggle click sound for boolean setting rows
NikkelM Jul 22, 2026
24664a3
Disable and grey slider and enum rows when a mod is disabled
NikkelM Jul 23, 2026
928e97b
Update naming scheme
NikkelM Jul 23, 2026
43e1d93
Hide mod config keys that have no configDesc entry
NikkelM Jul 24, 2026
799cdb7
Keep the current scroll page when resetting a mod's settings
NikkelM Jul 24, 2026
0d03ad1
Add inHub editable context for hub-only mod settings
NikkelM Jul 24, 2026
7599361
Add disabledDescription and show editableContext note in hint texts
NikkelM Jul 25, 2026
e8183e1
Make the mod settings menu resilient to game updates
NikkelM Jul 29, 2026
2b6ce6f
Add virtual configDesc rows and a native table-based config proxy
NikkelM Jul 30, 2026
c88c0ba
Add type and default fields to virtual mod-settings rows
NikkelM Jul 31, 2026
95710fa
Fixed typo
NikkelM Jul 31, 2026
3be22f8
Fixed disabled action buttons being clickable
NikkelM Jul 31, 2026
1a0a284
Suppress press feedback on greyed mod-settings action buttons and bou…
NikkelM Jul 31, 2026
2a8f3c3
Fix disabled widget rendering
NikkelM Aug 1, 2026
0f1812a
Restore action-button vertical spacing drift-free via SetLocation
NikkelM Aug 1, 2026
0a20ff8
Make active mod-settings action buttons keyboard/controller navigable
NikkelM Aug 2, 2026
5d973b8
Add optional custom description to rom.mod_settings.opt_out
NikkelM Aug 2, 2026
2a917d1
Allow custom re-nesting using groups
NikkelM Aug 2, 2026
0d6e627
Small bug fixes
NikkelM Aug 2, 2026
110492e
Updated docs
NikkelM Aug 2, 2026
9159871
Rename onChange to onChanged, updated default ordering
NikkelM Aug 3, 2026
acc09cb
Only reset settings within current tree view
NikkelM Aug 3, 2026
c13f780
Quiet redundant ERROR and traceback for failed mod callbacks
NikkelM Aug 3, 2026
bda7a3d
Added regions for better discoverability
NikkelM Aug 4, 2026
2b1fdaa
Update GUID
NikkelM Aug 6, 2026
8277b78
Derive mod-settings row flags from the widget actually built
NikkelM Aug 6, 2026
c93acaa
Allocate and free native GUI objects through the game's CRT
NikkelM Aug 6, 2026
16e15e3
Split build_mod_settings into item collection and row construction
NikkelM Aug 7, 2026
69f1263
Condense comments across the mod-settings files
NikkelM Aug 7, 2026
ca18282
clang-format
NikkelM Aug 7, 2026
e21ce84
Update default ordering to alphabetical
NikkelM Aug 8, 2026
41b47df
Condensed comments
NikkelM Aug 8, 2026
56bf071
Updated Readme
NikkelM Aug 8, 2026
6428a75
Align concepts
NikkelM Aug 8, 2026
515a8c8
Allow groups to be disabled dynamically
NikkelM Aug 9, 2026
501aa55
Merge branch 'master' into nikkelm/mod-menu
NikkelM Aug 10, 2026
2d048f0
Merge branch 'master' into nikkelm/mod-menu
NikkelM Aug 11, 2026
86e1b2c
Added hold-to-move for sliders
NikkelM Aug 11, 2026
c7cc52e
Updated comments
NikkelM Aug 11, 2026
b49c140
Added note
NikkelM Aug 13, 2026
5ca1142
Fixed hot reload
NikkelM Aug 14, 2026
9edd8c7
Additional Chalk parity fixes
NikkelM Aug 14, 2026
11c4a89
Only show declared keys in the menu
NikkelM Aug 16, 2026
3e5be86
Don't resolve internal IDs in the mod menu
NikkelM Aug 16, 2026
1d3a390
Fixed array support
NikkelM Aug 16, 2026
375a3df
Support config arrays
NikkelM Aug 16, 2026
bc7755a
Show mod rows without any config options as greyed out
NikkelM Aug 17, 2026
d98871b
Prevent crash if configDesc is present but empty
NikkelM Aug 17, 2026
1754d60
Updated description
NikkelM Aug 17, 2026
e3289c8
Added centered mod title on main options page per mod
NikkelM Aug 17, 2026
a2d82ba
Render config values verbatim in the mod menu
NikkelM Aug 17, 2026
f2fdccf
Updated schema
NikkelM Aug 17, 2026
bf57356
Use Enter instead of Spacebar icon for submit button hint
NikkelM Aug 17, 2026
06c34ae
Ensure mouse pointer doesn't disappear when typing wasd in freetext f…
NikkelM Aug 17, 2026
2fbd66c
Discard stale enum commits from the settings menu
NikkelM Aug 17, 2026
5eaf987
Refresh the panel after onChanged edits other settings
NikkelM Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/lua/tables/definitions/rom.mod_settings.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---@meta mod_settings

---@class (exact) rom.mod_settings

-- Loads a mod's `config.lua` and registers its settings under the Mods tab of the in-game Options
-- menu, returning a live read/write proxy over the config. Also manages the mod's `.cfg` file,
-- setting default values for new options and loading values saved to it by users. When using this,
-- your mod does not need to depend on or use `Chalk`.
---@param configFilePath string Path, relative to the mod's folder, of the `config.lua` that returns `config` and `configDesc`.
---@return table # A live read/write proxy over the mod's config. Index it to read a setting and assign to write one.
function mod_settings.load(configFilePath) end

-- Excludes the calling mod from the in-game mod settings menu: it stays listed but will be greyed out and
-- cannot be opened. Use it when the mod should not be edited in-game. Works with Chalk or rom.mod_settings.load.
---@param description? string A plain string or a localization table `{ en = "...", de = "..." }` shown in place of the generic note when the mod's disabled row is hovered.
function mod_settings.opt_out(description) end
33 changes: 33 additions & 0 deletions docs/lua/tables/rom.mod_settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Table: rom.mod_settings

## Functions (2)

### `load(configFilePath)`

Loads a mod's `config.lua` and registers its settings under the Mods tab of the in-game Options menu, returning a
live read/write proxy over the config. Also manages the mod's `.cfg` file, setting default values for new options
and loading values saved to it by users. When using this, your mod does not need to depend on or use `Chalk`.

- **Parameters:**
- `configFilePath` (string): Path, relative to the mod's folder, of the `config.lua` that returns `config` and `configDesc`.

- **Returns:**
- `table`: A live read/write proxy over the mod's config. Index it to read a setting and assign to write one.

**Example Usage:**
```lua
config = rom.mod_settings.load("config.lua")
```

### `opt_out(description)`

Excludes the calling mod from the in-game mod settings menu: it stays listed but will be greyed out and
cannot be opened. Use it when the mod should not be edited in-game. Works with Chalk or rom.mod_settings.load.

- **Parameters:**
- `description` (string): Optional. A plain string or a localization table `{ en = "...", de = "..." }` shown in place of the generic note when the mod's disabled row is hovered.

**Example Usage:**
```lua
rom.mod_settings.opt_out("Please use the imgui menu to configure this mod (opens with \"Insert\" by default).")
```
193 changes: 193 additions & 0 deletions docs/mod_settings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# In-game mod settings - IDE schema & hints

> Note: Load your config using `config = rom.mod_settings.load("config.lua")` in your `main.lua` to benefit from the advanced features below.

Hell2Modding renders each mod's config file as a tab in the game's Options screen. Mods declare how
their settings look and read/write their values through a `config.lua` that returns two tables:

- `config` - the config keys and their default values.
- `configDesc` - the description/metadata for each setting (labels, help text, ranges, enums, ...).

Loading the `config.lua` also writes the mod's `.cfg`: it is created from the declared defaults on first run, and
rewritten on later runs to pick up newly added keys and descriptions, keeping any values already saved there.

> **Only keys with a `configDesc` entry are shown.** A key present in `config` but absent from `configDesc`
> is treated as internal state and is not displayed in the menu.

## VS Code type hints

To get schema validation and type hints to show in VS Code when you edit your `config.lua`, follow these steps:

1. Install the [Lua extension](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) for VS Code.
2. In the extension's settings, add the folder containing the `config_schema.lua` from this repository to the `workspace.library` array.
3. Annotate the `configDesc` table with `---@type mod_settings.config_desc`.

## Field reference

Hover any field in the editor for its documentation. The available fields on a **setting** description are
below. Two other kinds of `configDesc` entry have their own fields and sections: **action buttons** (an
`action` function - see [Action buttons](#action-buttons)) and **virtual rows** (`virtual = true` with a
`text` or `get`/`set` callback and no config value - see [Virtual rows](#virtual-rows)).

| Field | Type | Purpose |
| --- | --- | --- |
| `displayName` | string \| localization table \| callback | Row label (defaults to a prettified key). Keep it to ~35 characters so it leaves room for the value shown to its right. |
| `description` | string \| localization table \| callback | Help text shown in the description box at the bottom of the screen while the row is highlighted. Keep it to ~450 characters. |
| `min`/`max` | number \| callback | Numeric bounds. If both are present the input will turn into a slider. |
| `step` | number \| callback | Slider/number step size (default 1). Will clamp user input automatically. |
| `values` | array \| callback | Enum: the values stored in the `.cfg` file. If present, the input will turn into a selector. |
| `labels` | array of (string \| localization table) \| callback | Display labels to show instead of the underlying `values` in the mod menu. Keep each to ~20 characters. |
| `order` | number \| callback | Sort key for custom ordering config entries in the menu, lowest first. Rows carrying an `order` are listed above those without one. When omitted, rows are sorted alphabetically by their `displayName`. |
| `hidden` | boolean | Hide the setting from the menu entirely. Static only - use `disabled` for rows that change state while the menu is open. |
| `disabled` | boolean \| callback | Grey the setting out (read-only) while true. Updates live while the menu is open. |
| `disabledDescription` | string \| localization table \| callback | Description shown in place of `description` while the setting is greyed by its own `disabled` field, to explain why. Falls back to `description` when omitted. |
| `restartRequired` | boolean | Force the user to restart the game after exiting the mod menu if this setting was changed. |
| `editableContext` | `"any"` \| `"mainMenu"` \| `"inSave"` \| `"inHub"` | Restrict where the row can be edited: `"any"` (default), `"mainMenu"` (only from the main menu), `"inSave"` (only while a save is loaded), or `"inHub"` (only in the Crossroads). Outside of the allowed context the row shows as disabled. Restrict this if the mod or game would break if the setting is edited in the wrong context. Can also be set on a whole menu category, which restricts everything inside it. The "enabled" setting and any `restartRequired` settings are always treated as `"mainMenu"`. |
| `showAsPercentage` | boolean | Append "%" to the value. Usually used for min/max restricted number fields. |
| `isPercentage` | boolean | Show a 0..x value as 0..x00 *and* append "%". You don't need `showAsPercentage` when using this. |
| `onChanged` | `fun(key, new_value)` | Called after the setting is changed through the menu. |

Try to avoid naming your config keys after any of the reserved fields above.

## Menu grouping (`group` and `groups`)

The in-game menu layout can be **decoupled** from your config file structure. `configDesc` must still mirror the config
(`config.debugging.logLevel` is described at `configDesc.debugging.logLevel`), but where each row *appears* in the menu
can be independent:

- By default a row appears under its **config section** - so a nested config nests in the menu automatically.
- Add a **`group`** property to any entry (setting, action, or virtual row) to move it into a different menu
category. It is a string for a single level, or an array for a nested path. This works for flat *and* nested config
keys, and doesn't change where the value is stored in the .cfg file.
- Declare menu categories that do **not** exist as config sections in a top-level **`groups`** table (keyed by the id
used in a `group`), each with an optional `displayName`, `description`, `order`, `disabled`,
`disabledDescription`, `editableContext`, and further nested `groups`.

This lets you keep a flat config but present any grouping you like, or re-nest an already-nested config another way.

## Dynamic fields (functions)

Most fields can also be dynamically resolved through a function call, which is evaluated when the menu
is opened and refreshed (after any other setting is changed). This lets a setting react to the live game
state or to other settings. The function runs in your mod's environment, so it can read your `config`,
and call functions in your `mod` or the `game` namespace.

One thing to watch out for in callbacks: Guard any calls to functions in your mod namespace: Write `mod and mod.Thing`
rather than `mod.Thing`. This is needed as these callbacks are registered independently of the mod's enabled state,
so if your mod was disabled on startup, and the user then enables it in the mod menu, any callbacks would error as
these functions are not yet registered.

Examples:

```lua
revive_count = {
displayName = "Allowed Revives",
min = 2,
-- Max could be dependent on internal mod state, which is unset if the mod is disabled on startup
max = function() return (mod and mod.CalcNumAllowedRevives()) or 2 end,
-- Perhaps mod.CalcNumAllowedRevives() accesses the game's GameState, in which case it would error when called in the Main Menu
editableContext = "inSave",
},
revive_chance = {
displayName = "Chance to automatically revive",
description = "After dying without any Death Defiance left, you have a chance to automatically respawn at the start of the encounter."
min = 0,
max = 100,
-- Row is greyed/disabled unless another config value is toggled on
disabled = function() return not config.easy_mode end,
disabledDescription = "Enable \"Easy Mode\" above to change this.",
},
```

## Action buttons

A `configDesc` entry with an `action` function (and a key that has NO config value) renders as a button
that runs the callback when pressed, instead of editing a setting. It supports `displayName`, `description`,
`disabledDescription`, `order`, `editableContext`, and `disabled`.

```lua
apply_scaling = {
action = function() mod.ApplyEasyModeScaling() end,
displayName = "Apply Easy Mode Scaling",
description = "Apply the scaling values above to the current save file.",
editableContext = "inSave",
disabled = function() return not mod.HasUnappliedEasyModeScaling() end,
disabledDescription = "Change a scaling value above to enable this.",
},
```

## Virtual rows

A **virtual row** is a menu row that is not backed by a `config` value - its value comes from Lua callbacks.
Declare it as a `configDesc` entry whose key has no matching `config` value, marked `virtual = true`.

A virtual row is either **read-only** or **interactive**:

- **Read-only:** give it a `text` field - a string, or a function returning a string/number/boolean - for
the value to show.
- **Interactive:** give it `get` (reads the current value) and `set` (writes the edited value). The widget is
inferred from `get()`'s value and the metadata, exactly like a config setting is inferred from its config
value: a **boolean** is a toggle, a **number** with `min`+`max` is a slider (otherwise a freetext field),
and any type with a `values` list is an **enum selector**.

Interactive rows also support `disabled`, `disabledDescription`, `editableContext`, `showAsPercentage`/
`isPercentage`, and (for enums) `labels` - the same as config settings. `get`/`set`/`text` and the metadata
fields may be functions, re-evaluated live.

Two extra fields help interactive rows that have no `.cfg` backing:

- **`type`** - force the widget kind (`"boolean"`, `"number"`, `"string"`, or `"enum"`) when `get()` can
return `nil` at build time and so cannot be inferred.
- **`default`** - the value the menu's **Reset** button restores the row to, applied through its `set()` callback.
A virtual row without a `default` is left untouched by Reset.

```lua
-- Not chosen yet, so get() returns nil until the player picks one
mod.EasyModePreset = nil
local configDesc = {
preset = {
virtual = true,
displayName = "Difficulty Preset",
type = "enum",
values = { "off", "balanced", "max" },
default = "balanced",
get = function() return mod.EasyModePreset end,
set = function(v) mod.EasyModePreset = v end,
},
}
```

## Reacting to changes (`onChanged`)

Give a setting an `onChanged` function to react when the player changes it through the options menu. Use it to
apply the new value to the live game, and/or to update **other rows'** dynamic fields.
It receives the setting's key and the new value:

```lua
local configDesc = {
run_difficulty = {
displayName = "Run difficulty",
min = 0, max = 100,
onChanged = function(key, newValue)
if game.CurrentRun then
mod.ApplyNewRunDifficulty(newValue)
end
end,
},
}
```

The callback fires AFTER the new value is stored and the `.cfg` is saved, so reading the setting back
(directly or via your `config` proxy) returns the new value. Note:

- It is **not called for other config writes** (e.g. from imgui or the config file) - only for edits made through
this menu.
- Re-writing the same value is a no-op and does not fire, so an `onChanged` that writes another setting
cannot loop.
- Errors thrown in the callback are logged and do not propagate into the game.

## Localization tables

Any `displayName`, `description`, or `labels` entry may be a table keyed by the game's language
codes (`en`, `de`, `el`, `es`, `fr`, `it`, `ja`, `ko`, `pl`, `pt-BR`, `ru`, `tr`, `uk`, `zh-CN`,
`zh-TW`). The menu resolves it to the currently set language, falling back to English.
Loading