Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
82 changes: 82 additions & 0 deletions .agents/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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` | Introduce a new in-world `OBJ_*` entity with create/move/delete + collision + render | `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 run clang-format/clang-tidy, 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
backward compatibility with existing tables.
- 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
61 changes: 61 additions & 0 deletions .agents/skills/fso-add-hud-gauge/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
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 existing `HUD_OBJECT_CUSTOM` gauge (and Lua HUD-draw hooks). Point the user to
the `fso-add-lua-api` skill + `hud_gauges.tbl` if that 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) and keep the count in sync.

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
74 changes: 74 additions & 0 deletions .agents/skills/fso-add-lua-api/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
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` `$On Game Init:` block or a 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
72 changes: 72 additions & 0 deletions .agents/skills/fso-add-object-type/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
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.

## 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`.
78 changes: 78 additions & 0 deletions .agents/skills/fso-add-sexp/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
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 (sexp.cpp, get/check argument-type switch)
- [ ] 4. Implement the handler + dispatch in eval_sexp() (sexp.cpp ~line 28166)
- [ ] 5. Add help text to Sexp_help (sexp.cpp ~line 37998)
- [ ] 6. (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, category }`:

```cpp
{ "my-operator", OP_MY_OPERATOR, 1, 2, SEXP_ACTION_OPERATOR, },
```
Pick the right category (`OP_CATEGORY_*` / the `SEXP_*_OPERATOR` kind) so it
shows in the correct FRED submenu.

3. **Argument typing** — in the argument-type switch (search `case OP_` near the
`get_argument_type`/`check_sexp_syntax` logic, ~line 4869), declare 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 28166) 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 37998):

```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. **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