Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
52f3e0a
Add design document of existing chat functionality
Garanas Apr 18, 2026
08fbf05
Add initial CLAUDE.md to help code the new chat implementation
Garanas Apr 18, 2026
2a66bfa
Add idea of being able to launch dialogs from a hotkey to make testin…
Garanas Apr 18, 2026
d37e1d5
Experiment with generating chat config as MVC
Garanas Apr 18, 2026
01e2183
Directly call the controller
Garanas Apr 19, 2026
63ca384
Reduce magic values
Garanas Apr 19, 2026
fc61487
First draft of the chat dialog
Garanas Apr 22, 2026
9d8553b
Use standard chat dialog textures
Garanas Apr 22, 2026
b805a94
Add chat list interface to select recipient(s)
Garanas Apr 22, 2026
7d012a1
Determine chat list interface based on clients and not armies
Garanas Apr 22, 2026
0f31318
Add faction badge to chat list interface
Garanas Apr 22, 2026
8e1cb17
Add basic support for chat commands
Garanas Apr 22, 2026
cecbe01
Do not load commands as a side effect of importing a file
Garanas Apr 22, 2026
dac1888
Document functionality of chat lines
Garanas Apr 22, 2026
af5d753
Add support for text wrapping and scrolling
Garanas Apr 22, 2026
36a19fc
Add support for scroll bar
Garanas Apr 22, 2026
64d3b66
Add support to toggle chat configuration
Garanas Apr 22, 2026
503040a
Add basic support for command hint
Garanas Apr 22, 2026
33a7524
Reactive to font size, introduce use of trashbag for lazyvars
Garanas Apr 23, 2026
75f232d
Formatting
Garanas Apr 23, 2026
8761a8b
Refactor key/value pairs
Garanas Apr 23, 2026
e249ded
Refactor key/value pairs to use pascal case
Garanas Apr 23, 2026
726734f
Refactor use of OnFrame to event-driven functions of Edit
Garanas Apr 23, 2026
6e66b15
Add behavior on use of escape
Garanas Apr 23, 2026
2e23ebb
Add support for page up/down
Garanas Apr 23, 2026
101f6e2
Respect maximum number of characters per message
Garanas Apr 23, 2026
b2b802e
Set minimum width/height of chat dialog
Garanas Apr 23, 2026
a7c70f7
Fix snap van chat configuratie bij bewegen
Garanas Apr 23, 2026
52b7b22
Add drag handles and reset button
Garanas Apr 23, 2026
dfeecac
Refactor to make code easier to read
Garanas Apr 23, 2026
ea0a6dd
Properly implement on roll over
Garanas Apr 23, 2026
7de85b3
Implement sending chat messages
Garanas Apr 23, 2026
c6a79e8
Init the chat functionality when creating the UI
Garanas Apr 23, 2026
8bd3e4a
Update imports of chat
Garanas Apr 23, 2026
80d76e3
Rewire other existing references to the old chat
Garanas Apr 23, 2026
244fccd
Add analysis about missing features and changes
Garanas Apr 23, 2026
a16cb74
Take over the engine events of the chat
Garanas Apr 23, 2026
693324d
Remove reference to rework of ActiveChat
Garanas Apr 23, 2026
f2c8e6a
Add feature to click on player name to whisper
Garanas Apr 23, 2026
b1c7281
Add support for autocomplete
Garanas Apr 23, 2026
8e70c3b
Fix bug where shift + enter did not work
Garanas Apr 23, 2026
ed30748
Refactor commands to be in separate files
Garanas Apr 23, 2026
c1b55fb
Add other commands that may be interesting and put them into a builti…
Garanas Apr 23, 2026
2d7b1b8
Add ability to mute players
Garanas Apr 24, 2026
37a64fd
Add commands to quickly mute players
Garanas Apr 24, 2026
a23f853
Abandon focus upon losing visibility of chat window
Garanas Apr 24, 2026
3a8b8bb
Do not infer opening with all or allies twice
Garanas Apr 24, 2026
6f256cc
Add support for auto complete player names
Garanas Apr 24, 2026
a62790c
Fix issues with hot reload
Garanas Apr 24, 2026
84043f0
Implement focus grab on moving
Garanas Apr 24, 2026
1c56d7c
Send chat messages as callback to store it in the replay
Garanas Apr 24, 2026
f0ce46c
Re-introduce the taunt ability
Garanas Apr 24, 2026
e7b76d3
Add single player commands
Garanas Apr 24, 2026
baeca85
Robust loading of commands to prevent game failing to start when drunk
Garanas Apr 24, 2026
b83dab8
Add ability for AIs and/or other source to make chat messages
Garanas Apr 24, 2026
0b62bf0
Add a scrollbar for command hints
Garanas Apr 25, 2026
b9ab8a4
Always read from the configuration source
Garanas Apr 25, 2026
6a975ad
Introduce shorthand to quickly get options
Garanas Apr 25, 2026
3e023bd
Attempt to improve ui scaling
Garanas Apr 25, 2026
db23ec6
__post_init belongs close to __init
Garanas Apr 25, 2026
f4c1dcd
Add separate control for as a container for the chat lines
Garanas Apr 25, 2026
727848c
Add debug tooling to make it easier to reason about controls
Garanas Apr 25, 2026
1171961
Fix scaling for edit interface
Garanas Apr 25, 2026
1627eee
Fix scaling of chat lines container
Garanas Apr 25, 2026
789bd10
Rename fields to make it easier to understand what they are
Garanas Apr 25, 2026
c7903cb
Fix chat line overflow when chat window is reset
Garanas Apr 25, 2026
b7e6f32
Fix bug in window title group not scaling with ui scale
Garanas Apr 25, 2026
6d2c4be
Fix scaling issues
Garanas Apr 25, 2026
75da955
Do not fade out chat lines
Garanas Apr 25, 2026
b01432f
Implement window-wide fade on inactivity
Garanas Apr 25, 2026
c2143ff
Add debug hotkeys to make testing more convenient
Garanas Apr 25, 2026
edf003f
Implement a chat feed when dialog is not open
Garanas Apr 25, 2026
d5f98c6
Add filter for links
Garanas Apr 25, 2026
9a24745
Add guards for messages that we receive
Garanas Apr 25, 2026
7683d4a
Implement feed background
Garanas Apr 25, 2026
96755f9
Remove references to chat feed persist option as we will not support it
Garanas Apr 25, 2026
cdfb890
Validate incoming messages
Garanas Apr 25, 2026
720a263
Implement colors and the pin
Garanas Apr 26, 2026
7e33530
Always initialize chat window so that we can see the feed before open…
Garanas Apr 26, 2026
220d83c
Unable to click your own name to whisper
Garanas Apr 26, 2026
99f39eb
Add a color to messages with camera or location data
Garanas Apr 26, 2026
caf0675
Update missing functionality
Garanas Apr 26, 2026
a3412eb
Add backwards compatibility to original api
Garanas Apr 26, 2026
dba1260
Apply faction theme to chat window border and handlers
Garanas Apr 26, 2026
15ae910
Document the key events
Garanas Apr 26, 2026
435c64c
Add tooltips for various interactions
Garanas Apr 26, 2026
4dae368
Remove log statement
Garanas Apr 26, 2026
6f875b8
Add support for legacy notify chat commands
Garanas Apr 26, 2026
4c891de
Enable scrolling through chat feed
Garanas Apr 26, 2026
7c87ba3
Add support for going up/down previous messages
Garanas Apr 26, 2026
769a1a4
Add a skill to make it easier to add chat commands
Garanas Apr 26, 2026
f69e877
Refine the configuration window
Garanas Apr 26, 2026
77c0c42
Add missing functionality to the chat configuration window
Garanas Apr 26, 2026
e7be56c
Moving the chat window is considered activity so that it does not close
Garanas Apr 26, 2026
36c041c
Add more guard rails for Claude
Garanas Apr 26, 2026
21e2c13
Always snap windows to the available screen area
Garanas Apr 26, 2026
ef22a6f
Update claude.md for the chat window
Garanas Apr 26, 2026
faa9a42
Fix drag handlers being themed to the UEF initially
Garanas Apr 26, 2026
0d63273
Add chat messages about the amount of shared resources
Garanas Apr 26, 2026
119f895
Clean up specs for development
Garanas Apr 26, 2026
bbb8169
Add chat message when units are gifted
Garanas Apr 26, 2026
2f96e34
Update layout file
Garanas Apr 26, 2026
a8f8ddd
Add additional padding to camera area for chat message
Garanas Apr 26, 2026
933fd80
Fix chat lines not populating properly
Garanas Apr 26, 2026
fb255f6
Fix width of edit box
Garanas Apr 26, 2026
30d8a9b
Merge branch 'develop' into refactor/chat
Garanas Apr 26, 2026
3f63cc3
Always rewrap text
Garanas Apr 26, 2026
3af81fa
Remove local settings
Garanas Apr 26, 2026
a3501fe
Add gitignore to prevent local files of Claude to pollute the repository
Garanas Apr 26, 2026
02adfbb
Remove stray debug message
Garanas Apr 26, 2026
b3af5bb
Add current tick to message id
Garanas Apr 26, 2026
33c839d
Properly annotate the chat payload
Garanas Apr 26, 2026
260f37c
Construct chat message based on the actual units transferred
Garanas Apr 26, 2026
bb667e9
Localize the chat recipient field(s)
Garanas Apr 26, 2026
d04350f
Auto-reload commands and compact comments
Garanas Apr 26, 2026
005ef0a
Compact comments of chat config
Garanas Apr 26, 2026
5812483
Compact comments of debug functionality
Garanas Apr 26, 2026
e10e4b1
Compact remaining comments
Garanas Apr 26, 2026
f54c64f
Add support for ctrl + click to copy to clipboard
Garanas Apr 26, 2026
9002d91
Use square root for fade out
Garanas Apr 26, 2026
60bd41d
Rename the chat brain component
Garanas Apr 27, 2026
b65678e
Remove legacy hook that isn't used at this stage
Garanas Apr 27, 2026
af0c9ee
Compact comments of chat payload
Garanas Apr 27, 2026
d841287
Only share the resources that the recipient can receive
Garanas Apr 27, 2026
17bb872
Update description
Garanas Apr 27, 2026
08b8f55
De-duplicate messages in `OnReceive` to prevent race conditions
Garanas Apr 27, 2026
79071e9
Apply muting changes to both committed and pending config
Garanas Apr 27, 2026
1c17c6f
Clear out draft when closing chat config
Garanas Apr 27, 2026
b967a7a
Attempt to guardrail the use of comments
Garanas Apr 28, 2026
c7a8eaf
Improve comment about console output ending up as a chat message
Garanas Apr 28, 2026
87d3f5b
Remove the legacy chat
Garanas Apr 28, 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
5 changes: 5 additions & 0 deletions .claude/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
settings.local.json
.credentials.json
*.local.*
scratch/
*.draft.md
209 changes: 209 additions & 0 deletions .claude/skills/add-chat-command/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
---
name: add-chat-command
description: Add a new in-game chat slash command (e.g. `/foo <arg>`) to the FAF chat system. Use when the user asks to add, register, or scaffold a new chat slash-command, or extend the `/help` listing with a new entry. Creates the command file under `lua/ui/game/chat/commands/builtin/`, wires it into `ChatController.RegisterBuiltinCommands`, and follows the parser/Accept/Execute split documented in `lua/ui/game/chat/commands/design.md`.
---

# Adding a chat slash-command

Use this skill when the user wants a new `/something` command available in the in-game chat edit box.

## Before you start — clarify with the user

Ask only what you can't infer:

1. **Canonical name** — the slash word users type (e.g. `whisper`). Lowercase, no spaces. Aliases optional.
2. **What it does** — one sentence; this becomes `Description` and is shown by `/help`.
3. **Parameters** — name, type, and whether each is optional. Types are listed below. If unsure, propose a shape based on similar existing commands and confirm.
4. **Side effect target** — does it call `ctx.Controller.X`, a `SimCallback`, a global engine function, or just print a system line? Most commands route through `ctx.Controller` to preserve MVC discipline.
5. **Gating** — should the command only register in some sessions (observer-only, replay-only, single-player-only, host-only)? If yes, use `ShouldRegister`.

If the user gives a fully-specified ask ("add `/ping` that prints 'pong' to the local feed"), skip the Q&A and proceed.

## Files you will touch

| File | What changes |
|------|--------------|
| `lua/ui/game/chat/commands/builtin/<Name>.lua` | **New file**. Exports a single top-level `Command` table. |
| `lua/ui/game/chat/ChatController.lua` | Add one `Registry.RegisterFromPath(...)` line inside `RegisterBuiltinCommands` (around line 102). |

Do **not** touch:
- [ChatCommandRegistry.lua](lua/ui/game/chat/commands/ChatCommandRegistry.lua) — the dispatcher is generic.
- [ChatCommandTypes.lua](lua/ui/game/chat/commands/ChatCommandTypes.lua) — only edit if the command needs a brand-new parameter resolver type. Prefer reusing existing types.
- [ChatModel.lua](lua/ui/game/chat/ChatModel.lua) — commands never write to the model directly; they go through the controller.

## File naming

- Filename matches the command in PascalCase: `whisper` → `Whisper.lua`, `gift-resources` → `GiftResources.lua`.
- Place under `lua/ui/game/chat/commands/builtin/`.
- One command per file. Even if two commands share a helper, give them their own files and lift the helper to a sibling module if needed.

## Command descriptor template

```lua

-------------------------------------------------------------------------------
-- /<name> [<args>] — one-line summary mirroring the Description field, plus
-- any constraints worth flagging to a future maintainer (sim callbacks used,
-- gating rules, surprising defaults).

---@type UIChatCommand
Command = {
Name = '<name>',
Aliases = { '<short>', '<other>' }, -- optional; remove if none
Description = '<one-line summary shown by /help>',
Params = {
{ Name = '<arg1>', Type = 'String' },
{ Name = '<arg2>', Type = 'Int', Optional = true },
},
ShouldRegister = function() -- optional; remove if always-on
return GetFocusArmy() ~= -1 -- e.g. hide from observers
end,
Accept = function(args, ctx)
-- Semantic checks against runtime state. Return (false, "/<name>: reason.")
-- on rejection. The string is shown to the user as a local system line.
return true
end,
Execute = function(args, ctx)
-- Side effect. Prefer ctx.Controller.X over importing modules directly.
end,
}
```

Module file structure rules:

- The export is a **bare top-level `Command` global**, not `return { Command = ... }`. The registry reads `module.Command` after `import`. Match the existing files — see `Whisper.lua` or `Recall.lua`.
- Do **not** call `Registry.Register` inside the file. Registration happens once, centrally, in `ChatController.RegisterBuiltinCommands`.
- Importing the file must have **no side effects**. The command stays inert until the controller registers it.
- Keep `import(...)` calls at the top of the file (e.g. `local ChatModel = import("/lua/ui/game/chat/ChatModel.lua")` — see `GiftResources.lua`). Don't import inside `Execute`.

## Parameter types

Defined in [ChatCommandTypes.lua](lua/ui/game/chat/commands/ChatCommandTypes.lua):

| Type | Accepts | Resolved value |
|------|---------|----------------|
| `Recipient` | `"all"`, `"allies"`/`"team"`, nickname, army ID | `'all' \| 'allies' \| number` |
| `Player` | nickname or army ID (also accepts a leading `@`) | `number` (army ID) |
| `Int` | integer literal | `number` |
| `String` | one whitespace-delimited token | `string` |
| `Rest` | every remaining token, joined with single spaces | `string` |

Rules:
- Param order = token-consumption order.
- Only the **last** param may be `Rest`.
- A missing required param produces `"/<name>: missing argument <argname>."` automatically — don't re-check in `Accept`.

If the command needs a parameter shape none of these cover, add a new resolver to `Resolvers` in `ChatCommandTypes.lua` and the matching string to the `UIChatCommandParamType` alias. Prefer this over ad-hoc string parsing inside `Accept`.

## Accept vs. Execute

- **Parser** rejects *structural* errors (missing args, wrong types, unknown name) — you don't write code for these.
- **`Accept`** rejects *semantic* errors that depend on runtime state: target is yourself, observer trying a player-only action, target just disconnected. Return `(false, "/<name>: human reason.")`. The string surfaces as a local system feed line.
- **`Execute`** assumes inputs are valid and runs the side effect. Don't re-validate.

`Accept` may also normalize `args` in-place — e.g. `GiftResources.lua` rewrites `args.type` to `'mass'`/`'energy'` and back-fills `args.target` from the model when the user omitted it.

## Error-string convention

Every user-facing error from a command must start with `"/<name>: "` so failures are self-identifying in the chat feed. Lowercase the message, end with a period.

```lua
return false, "/whisper: can't whisper yourself."
```

## ShouldRegister gating

Use when the command should be invisible (and unparseable) outside specific sessions. The hook runs once at registration; the command is dropped from the registry **and from `/help`** if it returns false. Examples already in the tree:

- Observer-only: `return GetFocusArmy() == -1`
- Single-player-only: `return SessionIsGameOver() or SessionIsReplay() or ...` — see `EndMission.lua` for a real example.
- Replay-only: gate on `SessionIsReplay()`.

Don't use `ShouldRegister` for "is this a valid moment to run" — that's `Accept`'s job. `ShouldRegister` is for "does this command exist at all in this session."

## Wiring it up

After writing the file, add one line to [ChatController.lua](lua/ui/game/chat/ChatController.lua) inside `RegisterBuiltinCommands` (around line 102). The list is loosely grouped — keep yours next to thematically similar entries:

```lua
Registry.RegisterFromPath("/lua/ui/game/chat/commands/builtin/<Name>.lua")
```

`RegisterFromPath` is defensive: a missing file, a bad import, a malformed `Command` export, or a throw inside `Register` are all logged and swallowed. So a broken new command can't take down the entire chat system — but verify your file actually got registered (see Verification below).

## Calling back into the controller

The execution context passed to `Accept` / `Execute`:

```lua
---@class UIChatCommandContext
---@field Model UIChatModel
---@field Controller table -- ChatController module
---@field SourceText string -- raw "/whisper Jip hi" text
```

Common patterns:

| Goal | How |
|------|-----|
| Change recipient | `ctx.Controller.SetRecipient(args.target)` |
| Print a local system line (no network) | `ctx.Controller.AppendLocalSystemMessage("text")` |
| Send a chat message via the network | `ctx.Controller.Send("text")` |
| Read current chat state | `ctx.Model.Recipient()`, `ctx.Model.History()`, etc. (call the LazyVar — never store) |
| Sim callback | `SimCallback({ Func = "...", Args = { ... } })` directly |

Reading a LazyVar: always `ctx.Model.Recipient()` (call it). Never cache it in a local. See `CLAUDE.md` § Reactive State.

## Worked example: `/ping`

Adding a tiny `/ping` command that prints "pong" locally — useful as a smoke test.

**1.** Create `lua/ui/game/chat/commands/builtin/Ping.lua`:

```lua

-------------------------------------------------------------------------------
-- /ping — prints "pong" as a local system line. No network traffic. Useful
-- as a smoke test that the command pipeline is alive end-to-end.

---@type UIChatCommand
Command = {
Name = 'ping',
Description = 'Prints "pong" locally — smoke test for the chat command pipeline.',
Execute = function(_, ctx)
ctx.Controller.AppendLocalSystemMessage("pong")
end,
}
```

**2.** Register it in [ChatController.lua](lua/ui/game/chat/ChatController.lua):

```lua
Registry.RegisterFromPath("/lua/ui/game/chat/commands/builtin/Ping.lua")
```

**3.** Verify (see below).

## Verification

After making the changes:

1. **Static check** — re-read both files. Confirm:
- The file exports `Command = { ... }` at top level (not inside `return`).
- Required keys present: `Name` (non-empty string), `Execute` (function). Everything else is optional.
- Every error string returned from `Accept` starts with `"/<name>: "`.
- The `RegisterFromPath` line is inside `RegisterBuiltinCommands`, not at module scope.
2. **Runtime check (ask the user to do this)**:
- Launch a skirmish or replay, open chat, type `/help`. The new command should appear in the list with the right description and aliases.
- Type the new command. Hit each error path (`Accept` rejections) to confirm the error strings render as local system lines.
- Check the game log for `WARN Chat command skipped: …` lines — those mean `RegisterFromPath` rejected the file (path typo, malformed export, bad `Name`/`Execute`). Fix and reload.

If you can't run the game, say so explicitly rather than claiming the command works.

## Don'ts

- **Don't write to `ChatModel` from inside a command.** Go through `ctx.Controller`. Commands are MVC peers of views — read the model, mutate via the controller.
- **Don't call `Registry.Register` from the command file itself.** Central registration in `ChatController` is the only way; it makes hot-reload (the `Reload()` path) work and keeps the bootstrap order deterministic.
- **Don't add `<LOC ...>` localization tags to error strings yet.** This is listed as open work in `commands/design.md` § 10 — keep parity with the existing English-only commands so the eventual sweep is uniform.
- **Don't reach into `ChatCommandTypes.lua` to add a one-off resolver.** If a command needs special parsing of one argument, do it inside `Accept`. Only add a new resolver type when two or more commands would share it.
- **Don't import view files** (`Chat*Interface.lua`). Commands are headless — they should run in any context that has a model and controller.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"Lua.runtime.pathStrict": false,
"Lua.runtime.exportEnvDefault": true,
"Lua.completion.autoRequire": false,
"Lua.diagnostics.globals": ["ScenarioInfo"],
"Lua.diagnostics.globals": ["ScenarioInfo", "__moduleinfo"],
"Lua.format.defaultConfig": {
"max_line_length": "unset",
},
Expand Down
106 changes: 106 additions & 0 deletions annotation.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,65 @@ And a few specifics:
- A comment should not end with a dot (.), but it can be used between sentences
- Varargs `...` should be documented as: `@param ... <type> <description>`, it shouldn't use `@vararg`

## Comment style

Comments explain intent and engine behavior. Anything readable straight from the code does not need a comment.

### Rules

1. Every `@class`, `@type`, `@alias`, and other top-level annotation has a `---` description directly above it.
2. Every function has a `---` description. The description tells a consumer what the function does and anything they need to know before calling it. Implementation details and rationale go inside the function body, at the very top, before any logic.
3. Comments are succinct and declarative. Short sentences. State the fact, not the reasoning around the fact.
4. Do not use em-dashes (`—`) or similar joining punctuation. Split into separate sentences, or use a colon, a comma, or parentheses.

### Why the function-level vs. body split

The `---` block is what the language server shows on hover. It is the public face of the function and should read as a contract. Implementation rationale belongs with the implementation, where future maintainers will read it.

### Good

```lua
--- Returns the army index that authored the current command, or nil
--- outside a command-source context.
---@return integer?
function GetCurrentCommandSourceArmy()
-- The engine clears the source between command dispatches, so nil
-- is normal during idle ticks.
...
end
```

```lua
--- Wire-format chat payload. Travels through both the live engine
--- broadcast and the sim-routed sync stream.
---@class ChatPayload
```

```lua
--- A known channel constant, or an army index for a private whisper.
---@alias UIChatRecipient 'all' | 'allies' | number
```

### Not good

The rationale leaks into the consumer-facing doc, and an em-dash joins what should be two sentences:

```lua
--- Returns the army index that authored the current command — the engine
--- clears the source between command dispatches so a nil is normal during
--- idle ticks. Returns nil if there is no source.
---@return integer?
function GetCurrentCommandSourceArmy()
...
end
```

The class is annotated but has no description, so the hover tooltip is empty:

```lua
---@class ChatPayload
```

## Examples

In general, all annotated code in the repository is a good example. Some was written before these guidelines were written. Therefore we'll include some good examples for you to look at.
Expand Down Expand Up @@ -103,3 +162,50 @@ BaseManager = ClassSimple {
```

Two examples of annotating a class. Note that fields need to be added manually, specifically those that are populated in the instance of a class.

## Class fields

Every field assigned to `self` inside a constructor or initialization function (`__init`, `__post_init`, `Create`, `OnCreate`, etc.) must have a matching `---@field` annotation on the class definition. This gives the language server full type information across the whole file and makes the class self-documenting at a glance.

### Rule

Annotate the class immediately above the class declaration. List every field in the order it appears in the constructor. For fields whose type is an array of a named struct, define that struct as its own `---@class` above the main class.

### Example

```lua
---@class UIChatConfigColorRow
---@field label Text
---@field combo BitmapCombo
---@field key string

---@class UIChatConfigInterface : Window
---@field LabelColors Text
---@field ColorRows UIChatConfigColorRow[]
---@field LabelFontSize Text
---@field SliderFontSize IntegerSlider
---@field LabelBehavior Text
---@field Checkboxes Checkbox[]
---@field BtnApply Button
---@field BtnOk Button
local ChatConfigInterface = ClassUI(Window) {
__init = function(self, parent, ...)
self.LabelColors = UIUtil.CreateText(...)
self.ColorRows = {}
self.LabelFontSize = UIUtil.CreateText(...)
self.SliderFontSize = IntegerSlider(...)
self.LabelBehavior = UIUtil.CreateText(...)
self.Checkboxes = {}
self.BtnApply = UIUtil.CreateButtonStd(...)
self.BtnOk = UIUtil.CreateButtonStd(...)
end,
}
```

### What counts as a field

- Every `self.Foo` written in any constructor or init function (`__init`, `__post_init`, `Create`, `OnCreate`, etc.).
- Fields inherited from the parent class do **not** need repeating — the `: Parent` in the `---@class` declaration inherits them.
- Methods defined inside the class table (`Foo = function(self, ...) end` entries) do **not** need `---@field` — the language server picks them up from the function signature.
- Temporary locals inside a method are not fields and need no annotation.
- Optional fields (assigned only on some code paths) should be marked with `?`, e.g. `---@field DebugBG? Bitmap`.
Loading
Loading