Skip to content
85 changes: 85 additions & 0 deletions .agents/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# FSO Project Skills

Vendor-agnostic Agent Skills for the FreeSpace 2 Open engine. Each skill is
a directory containing a `SKILL.md` that encodes an FSO-specific workflow
(conventions, the exact files/functions to touch, and how to verify). Skills
auto-trigger from natural requests; you can also invoke one explicitly by name.

This directory (`.agents/skills/`) is the single source of truth. Vendor skill
directories contain thin pointer stubs back to it (see "Cross-agent compatibility"
below).

## Available skills

| Skill | Use it when you want to… | Key code | Module guide |
| --- | --- | --- | --- |
| `fso-add-table-field` | Add/modify a parsed option in a `.tbl`/`.tbm` (ships, weapons, ai_profiles, game_settings, hud_gauges) and wire it into an `*_info` struct | `code/parse/parselo.*` + the owning module's parser | `documentation/modules/parse.md` |
| `fso-add-sexp` | Add a new SEXP operator (mission-scripting function): `OP_*`, `Operators` table, arg typing, `eval_sexp`, help text | `code/parse/sexp.{h,cpp}` | `documentation/modules/parse.md` |
| `fso-add-lua-api` | Expose engine data/functions to Lua via ADE bindings, or add a `scripting::Hook` mods can subscribe to | `code/scripting/` (`api/`, `global_hooks.*`) | `documentation/modules/scripting.md` |
| `fso-add-object-type` | **Last resort** — introduce a new in-world `OBJ_*` entity with create/move/delete + collision + render; only when no existing object type (ship/weapon/debris/asteroid/etc.) can represent it | `code/object/object.{h,cpp}` | `documentation/modules/object.md` |
| `fso-add-hud-gauge` | Add a new built-in HUD gauge (`HudGauge` subclass + `HUD_OBJECT_*` + `hud_gauges.tbl` parsing) | `code/hud/hud.h`, `code/hud/hudparse.*` | `documentation/modules/hud.md` |
| `fso-build-and-test` | Configure/build with CMake+Ninja, run `unittests`, and check clang-format/clang-tidy compliance, mirroring CI | `CMakeLists.txt`, `ci/linux/*` | root `AGENTS.md` |

## Review skills

| Skill | Use it when you want to… | Invocation |
| --- | --- | --- |
| `thermo-nuclear-code-quality-review` | Run an extremely strict maintainability/abstraction audit of the current branch's changes (file-size growth, spaghetti conditionals, missed "code-judo" simplifications) | Explicit only (`disable-model-invocation`); ask for a "thermo-nuclear review" |

## Conventions shared by all skills

- Builds use `FSO_FATAL_WARNINGS=ON` (CI default) — keep changes warning-clean.
- Code must compile on GCC, Clang, and MSVC; isolate platform code under
`code/osapi/`, `code/windows_stub/`, or platform guards.
- New parsed options use `optional_string` (never `required_string`) to preserve

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"unless gated as suboptions only passed if a new optional_string is hit"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

backward compatibility with existing tables — unless nested inside a block
that's itself only entered after a new `optional_string` was matched (e.g.
sub-fields of a new optional block), where `required_string` is fine since
the whole block is already optional.
- Author-facing ship/weapon/SEXP changes may also need FRED updates (`fred2/`, `qtfred/`).
- After any change, validate with the `fso-build-and-test` skill.

## How skills load

- **Automatic:** the agent matches a request to a skill's `description` and reads
its `SKILL.md` before acting.
- **Explicit:** reference the skill by name (e.g. "use `fso-add-sexp`").

## Cross-agent compatibility

The skills here live in a vendor-agnostic folder. Each vendor reads its own skills
directory, so the per-vendor directories carry thin pointer stubs (plain files, not
symlinks — symlinks are avoided for Windows checkout compatibility).

| Path | Agent | Kind |
| --- | --- | --- |
| `.agents/skills/` | opencode (native), shared canonical | full playbooks |
| `.cursor/skills/` | Cursor | pointer stubs → `.agents/skills` |
| `.claude/skills/` | Claude Code, opencode | pointer stubs → `.agents/skills` |

Project guidance (build/style/test conventions) is shared via the root `AGENTS.md`,
read natively by Cursor and opencode; Claude Code reads it through the root
`CLAUDE.md`, which imports `AGENTS.md`.

Each stub at `.cursor/skills/<name>/SKILL.md` and `.claude/skills/<name>/SKILL.md`
carries the discovery frontmatter (`name` + `description`) and a body that points
to the canonical playbook here. Edit the playbook here in `.agents/skills/`; if you
change a skill's `name`/`description`, mirror that line into the matching stubs so
triggering stays in sync. The Cursor stub for `thermo-nuclear-code-quality-review`
also keeps `disable-model-invocation: true` (a Cursor-only field) to preserve its
explicit-only behavior.

## Authoring more skills

Use the built-in `create-skill` workflow. Place new project skills here
(`.agents/skills/<name>/SKILL.md`); keep descriptions third-person with clear
WHAT + WHEN trigger terms, and link one level deep to the relevant
`documentation/modules/*.md`. Then add a matching pointer stub under
`.cursor/skills/<name>/SKILL.md` and `.claude/skills/<name>/SKILL.md`.

## Related documentation

- Engine architecture overview: `documentation/ARCHITECTURE.md`
- Per-module entry-point guides: `documentation/modules/`
- Build/style/test conventions: root `AGENTS.md`
- Table-option reference (wiki): https://wiki.hard-light.net/index.php/Tables
68 changes: 68 additions & 0 deletions .agents/skills/fso-add-hud-gauge/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: fso-add-hud-gauge
description: >-
Add a new HUD gauge to FSO. Use when creating a HUD element/gauge, subclassing
HudGauge, adding a HUD_OBJECT_ type, or wiring a gauge into hud_gauges.tbl
parsing and the per-frame HUD render path.
---

# FSO: Add a HUD Gauge

HUD gauges live in `code/hud/`. Each gauge is a subclass of `HudGauge`
(`code/hud/hud.h`, ~line 214) with a `render()` method; gauges are instantiated
from `hud_gauges.tbl` via `code/hud/hudparse.cpp`. Decide first which path you need.

## Choose an approach

- **Mod-authored custom gauge** → usually no engine change: mission/mod authors use
the `+Scripted Gauge:` table entry (`HUD_OBJECT_SCRIPTING`, backed by
`HudGaugeScripting` in `code/hud/hudscripting.{h,cpp}`), whose `render()` calls
straight into a Lua-registered render function — the fully Lua-rendered path.
The older `HUD_OBJECT_CUSTOM` (`code/scripting/api/objs/hudgauge.cpp`) only
exposes Name/Text and general Lua HUD-draw hooks. Point the user to the
`fso-add-lua-api` skill + `hud_gauges.tbl` if either of these suffices.
- **New built-in gauge type** → follow the steps below.

## Steps (new built-in gauge)

1. **Subclass `HudGauge`.** Create `hudmygauge.h`/`.cpp` (or add to a related
existing gauge file). Implement the constructor (gauge type, default position,
colors) and override `render(float frametime)` using `gr_*` / `renderString` /
`renderBitmap` helpers from the base class.

2. **Add a gauge type id.** Add `#define HUD_OBJECT_MY_GAUGE NN` in
`code/hud/hudparse.h` (the `HUD_OBJECT_*` list). If the gauge belongs in the
legacy default set, also add it to `Legacy_HUD_gauges[]` and bump
`NUM_HUD_GAUGES` (`code/hud/hudgauges.h`); newer gauges (e.g. the scripting
gauge) are explicitly excluded from that legacy list and don't need it.

3. **Parse + load.** In `code/hud/hudparse.cpp`:
- Add a `load_gauge_my_gauge(...)` that reads gauge settings (coords, font,
color, frames) and constructs your gauge, registering it on the ship/HUD.
- Add a `case HUD_OBJECT_MY_GAUGE:` to the load dispatch (the switch around
line 1184) calling `load_gauge_my_gauge(settings)`.
- Map the gauge's table name string to the new id where gauge names are parsed.

4. **Render path.** Gauges added to the HUD list are rendered automatically each
frame; confirm your gauge is added to the per-ship gauge list so it draws.

5. **Defaults.** Provide retail-style default coordinates for 640 and 1024 layouts
like the existing gauges, so it works without an explicit `hud_gauges.tbl` entry.

## Conventions

- Reuse base-class draw helpers; don't call backend `gr_*` for text/bitmaps if a
`HudGauge::render*` helper exists.
- Respect HUD config (gauge can be toggled); check the gauge's active/visible state.
- Keep warning-clean (`FSO_FATAL_WARNINGS=ON`).

## Verify

- Build (see `fso-build-and-test`), start a mission, confirm the gauge renders at
the right place, scales across resolutions, and obeys HUD config show/hide.
- Test a `hud_gauges.tbl` entry that positions/recolors it.

## Reference

- `code/hud/hud.h` (`HudGauge`), `code/hud/hudparse.{h,cpp}`, `documentation/modules/hud.md`.
- hud_gauges.tbl docs: https://wiki.hard-light.net/index.php/Hud_gauges.tbl
75 changes: 75 additions & 0 deletions .agents/skills/fso-add-lua-api/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
name: fso-add-lua-api
description: >-
Expose engine functionality to Lua scripting in FSO via ADE bindings or add a
new scripting hook. Use when adding a Lua API object/library/function/property,
writing ADE_OBJ/ADE_FUNC/ADE_VIRTVAR/ADE_LIB bindings, or creating a
scripting::Hook/OverridableHook that mods can subscribe to.
---

# FSO: Add a Lua/ADE Binding or Hook

FSO's Lua layer (ADE) lives in `code/scripting/`. There are two common tasks:
**A)** expose data/functions to Lua, and **B)** fire an engine event mods can hook.
(For mission-designer scripting use the `fso-add-sexp` skill instead.)

## A) Add a Lua binding (ADE)

Bound **objects** live in `code/scripting/api/objs/`, **libraries** in
`code/scripting/api/libs/`. Copy an existing file (e.g. `objs/wing.cpp`) as a model.

- **Define an object type:**
```cpp
ADE_OBJ(l_MyThing, my_thing_h, "mything", "My thing handle");
```
- **Add a method:**
```cpp
ADE_FUNC(doStuff, l_MyThing, "number amount",
"Does stuff to the thing.", "boolean", "true on success")
{ /* parse args with ade_get_args, act, return with ade_set_args */ }
```
- **Add a gettable/settable property:**
```cpp
ADE_VIRTVAR(Name, l_MyThing, "string", "The name.", "string", "name or empty")
{ /* ... */ }
```
- **Add to a library:** use `ADE_LIB` / `ADE_LIB_DERIV` and attach functions.

Argument marshalling uses `ade_get_args(L, "...", ...)` and
`ade_set_args(L, "...", ...)`; format strings are documented in
`code/scripting/ade_args.h`. Match the documented type string in the macro to the
actual returned type, since the Lua API docs are generated from these.

## B) Add a scripting hook

1. Declare the hook in `code/scripting/global_hooks.h` and define it in
`global_hooks.cpp`, choosing `scripting::Hook<...>` (non-overridable) or
`scripting::OverridableHook<...>` (script can replace default behaviour).
2. Define any condition/parameter types via the hook's template args (see
`hook_conditions.h` / `hook_api.h`).
3. **Fire it** from the relevant subsystem at the right point:
```cpp
if (scripting::hooks::OnMyEvent->isActive()) {
scripting::hooks::OnMyEvent->run(scripting::hook_param_list(/* params */));
}
```
For overridable hooks, check `isOverride(...)` to let scripts suppress default logic.

## Conventions

- Keep binding docs accurate — they are the generated Lua API reference (`doc_*`).
- Register script files/tables via `scripting.tbl` (`script_parse_table`).
- Keep warning-clean (`FSO_FATAL_WARNINGS=ON`).

## Verify

- Build (see `fso-build-and-test`).
- Test from a Lua script (a `scripting.tbl` `$GameInit:` block — the "On Game
Init" hook's table tag — or another hook) that calls the new API / responds
to the new hook.

## Reference

- `code/scripting/ade_api.h`, `code/scripting/ade_args.h`, `code/scripting/hook_api.h`.
- `documentation/modules/scripting.md`.
- Lua API wiki: https://wiki.hard-light.net/index.php/Scripting
77 changes: 77 additions & 0 deletions .agents/skills/fso-add-object-type/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: fso-add-object-type
description: >-
Add a new in-world object type to the FSO object system (alongside ships,
weapons, debris, asteroids, etc.). Use when introducing a new OBJ_ type, a new
kind of simulated entity, or wiring a new entity into obj_create/obj_move_all
and the collision system.
---

# FSO: Add a New Object Type

Every world entity is an `object` (`code/object/object.h`) whose `type` is an
`OBJ_*` constant and whose `instance` indexes a type-specific array. Adding a new
type means defining that array + the create/move/delete trio and registering it
with the object system.

This is a last resort: adding a new `OBJ_*` type touches `MAX_OBJECT_TYPES`,
every switch over object type, and the collision matrix. Prefer extending an
existing type (ship subsystem, weapon subtype, etc.) if it can represent the
new entity at all.

## Checklist

```
- [ ] 1. Define OBJ_MY_TYPE + bump MAX_OBJECT_TYPES (object.h)
- [ ] 2. Add the name to Object_type_names[] (object.cpp)
- [ ] 3. Create the instance storage + *_info if data-driven (info-vs-instance)
- [ ] 4. Implement my_create() (wraps obj_create), my_move(), my_delete()
- [ ] 5. Hook my_move into obj_move_all_pre/post dispatch
- [ ] 6. Hook deletion into obj_delete_all_that_should_be_dead path
- [ ] 7. Add collision handling if it collides (object/objcollide + collide*.cpp)
- [ ] 8. Add rendering in the object render dispatch
```

## Steps

1. **Type constant** — add `#define OBJ_MY_TYPE NN` in `object.h` and increase
`MAX_OBJECT_TYPES`. Add the matching string to `Object_type_names[]` in
`object.cpp` (order must match).

2. **Storage** — follow the **info-vs-instance** pattern used by ships/weapons:
a `my_type_info` class table (parsed from a `.tbl`, see `fso-add-table-field`)
if the type is data-driven, plus a `my_type` instance array or `SCP_vector`.

3. **Create** — write `int my_create(...)` that calls
`obj_create(OBJ_MY_TYPE, parent, instance, &orient, &pos, radius, flags)` and
initializes the instance. Set `Collides` flag only if it participates in collisions.

4. **Move** — write `my_move(object *objp, float frametime)` and call it from the
per-type dispatch inside `obj_move_all_pre()` / `obj_move_all_post()`
(`object.cpp`). Physics runs via `obj_move_call_physics()` if `phys_info` is used.

5. **Delete** — to destroy, set the `Should_be_dead` flag. Add a `case OBJ_MY_TYPE:`
in `obj_delete_all_that_should_be_dead()` (or the delete dispatch) to free
instance data, then `obj_delete()`.

6. **Collisions** (optional) — register pairs in `code/object/objcollide.cpp` and
add a `collide_my_type_*` handler (model the existing `collide*.cpp` files).

7. **Rendering** — add a `case OBJ_MY_TYPE:` to the object render dispatch
(`obj_render` / `obj_queue_render` in `object.cpp`) that queues your draw call.

## Conventions

- Use `object_h`/signatures for cross-frame references, not raw objnums.
- Change flags via `obj_set_flags()` so collision-pair state stays consistent.
- Respect `MAX_OBJECTS` (must stay < 2^16-1 for collision-pair caching).
- Keep warning-clean (`FSO_FATAL_WARNINGS=ON`).

## Verify

- Build (see `fso-build-and-test`), spawn the entity (via code, SEXP, or Lab),
confirm it moves, renders, collides, and cleans up without asserts.

## Reference

- `code/object/object.h` / `object.cpp`, `documentation/modules/object.md`.
86 changes: 86 additions & 0 deletions .agents/skills/fso-add-sexp/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
name: fso-add-sexp
description: >-
Add a new SEXP operator (mission-scripting function) to FSO. Use when creating
a new SEXP, adding a mission event/goal operator, registering an OP_ identifier,
or wiring a sexp into eval_sexp. Covers sexp.h enum, the Operators table,
argument typing, evaluation, and help text in code/parse/sexp.cpp.
---

# FSO: Add a SEXP Operator

SEXPs are the mission designer's scripting language. Adding one touches a fixed
set of locations in `code/parse/sexp.h` and `code/parse/sexp.cpp`. Search an
existing similar operator name across both files to find every spot to edit.

## Checklist

```
- [ ] 1. Declare OP_* identifier (sexp.h)
- [ ] 2. Register in the Operators table (sexp.cpp ~line 146)
- [ ] 3. Add argument type-checking in query_operator_argument_type() (sexp.cpp ~line 32126)
- [ ] 4. Implement the handler + dispatch in eval_sexp() (sexp.cpp ~line 28169)
- [ ] 5. Add help text to Sexp_help (sexp.cpp ~line 38004)
- [ ] 6. Add the FRED submenu category in get_category()/get_subcategory() (sexp.cpp ~line 36656)
- [ ] 7. (If multiplayer-relevant) handle packing in network/multi_sexp.cpp
```

## Steps

1. **OP_ identifier** — add to the operator enum in `code/parse/sexp.h`
(the block starting `OP_PLUS = FIRST_OP, ...`). Do not reuse a value.

2. **Register the operator** in the `Operators` vector (`sexp.cpp`, ~line 146).
Entry format is `{ text, OP_id, min_args, max_args, type }`:

```cpp
{ "my-operator", OP_MY_OPERATOR, 1, 2, SEXP_ACTION_OPERATOR, },
```
The 5th field (`sexp_oper_type`, the `SEXP_*_OPERATOR` macros defined near
`sexp.cpp` ~line 126) classifies the operator's *kind*
(action/conditional/arithmetic/boolean/…) — it does **not** control the FRED
submenu; that's step 6.

3. **Argument typing** — add a `case OP_MY_OPERATOR:` to the big switch in
`query_operator_argument_type()` (`sexp.cpp`, ~line 32126), declaring what
each argument slot expects using the `OPF_*` enums (`OPF_NUMBER`, `OPF_SHIP`, …).

4. **Implement + dispatch** — write a handler function, then add a `case OP_MY_OPERATOR:`
in `eval_sexp()` (`sexp.cpp`, ~line 28169) that calls it and returns a SEXP
result (`SEXP_TRUE`/`SEXP_FALSE`/`SEXP_KNOWN_*`, or a number for arithmetic ops).
Read arguments via the `CDR`/`CADR` node walk like neighbouring cases.

5. **Help text** — add an entry to the `Sexp_help` vector (`sexp.cpp`, ~line 38004):

```cpp
{ OP_MY_OPERATOR, "my-operator\r\n"
"\tWhat it does.\r\n\r\n"
"Takes 1 or 2 arguments...\r\n"
"\t1:\tFirst argument.\r\n"
"\t2:\t(optional) Second argument." },
```

6. **FRED submenu category** — add a `case OP_MY_OPERATOR:` to `get_category()`
(`sexp.cpp`, ~line 36656) so the operator appears under the right FRED
category; add it to `get_subcategory()` (~line 37380) too if the category
has subcategories.

7. **Multiplayer** — if the operator changes game state on the server, ensure it
packs/sends correctly via `code/network/multi_sexp.*`.

## Conventions

- Operator names are lowercase-hyphenated and must be ≤ `OPERATOR_LENGTH` (30).
- Prefer adding self-contained logic; large new systems may live under `code/parse/sexp/`.
- Keep warning-clean (`FSO_FATAL_WARNINGS=ON`).

## Verify

- Build (see `fso-build-and-test`), open FRED/qtFRED, confirm the operator appears
in the right category with correct arg constraints, and test it in a mission.

## Reference

- `code/parse/sexp.h` — `OP_*` enum, `OPF_*` arg types, `OP_CATEGORY_*`, return codes.
- `documentation/modules/parse.md`.
- SEXP list: https://wiki.hard-light.net/index.php/SEXP
Loading
Loading