docs(code-languages): add Lua and GSC skills
Lua covers dialect pinning (5.1/LuaJIT/5.2/5.3+/HavokScript), luac -p and luacheck as the only real gate for an embedded host, locals over globals, pcall at the boundary, the #t / pairs / pattern traps, and the sandboxed-host rules including Call of Duty LUI specifics. GSC covers the absence of any compiler, linter, or test harness and what to do instead, scope prefixing on level/self, isdefined on every external read, endon before the first wait, the per-frame instruction budget, load-phase precaching, and side effects on shared level state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b8505fc623
commit
6207de1a97
4 files changed
+473
No files matched your search
@@ -0,0 +1,237 @@
|
||||
---
|
||||
name: gsc
|
||||
description: >-
|
||||
Use this skill when writing, reviewing, or debugging GSC (Game Script / Call of Duty script)
|
||||
or CSC — including entity-vs-level scope, threads and notify/wait semantics, endon/waittill
|
||||
discipline, undefined-value guards, the no-OOP/no-exceptions reality, asset precaching,
|
||||
per-frame cost, and the fact that GSC has no compiler, linter, or test harness worth the name.
|
||||
---
|
||||
|
||||
# GSC — Rules & Preferences
|
||||
|
||||
Extends [`coding-dev.md`](skills-public/coding-dev.md). Everything there applies; this adds GSC specifics.
|
||||
|
||||
GSC is the Call of Duty engine's own scripting language (`.gsc` server-side, `.csc`
|
||||
client-side). It is **not Lua, not JavaScript, and not C** despite resembling all three. It has
|
||||
no type system, no exceptions, no classes, no closures in most dialects, and — in a modded
|
||||
install — **no diagnostics beyond the in-game console**. That last fact drives most of what
|
||||
follows: the language will not tell you that you are wrong, so the code has to be written so
|
||||
that it cannot be.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Establish the dialect before writing anything.** IW3/IW4 (MW/MW2), IW5 (MW3), T5/T6 (Black
|
||||
> Ops 1/2), IW6, s1/s2, H1/H2 (MWR / MW2CR), and the various mod loaders (h2-mod, iw4x, plutonium,
|
||||
> t6 GSC Studio) each differ in available builtins, in how scripts get loaded, and in whether
|
||||
> `foreach`, `switch` fallthrough, or function pointers exist. A builtin from the wrong title is
|
||||
> a silent `undefined` or a hard crash.
|
||||
|
||||
---
|
||||
|
||||
## 1. There Is No Gate — Build One
|
||||
|
||||
GSC has no `gsc -Wall`. What exists:
|
||||
|
||||
| Check | How |
|
||||
| :--- | :--- |
|
||||
| **Syntax** | The engine's own compile at load. Read the console; a parse error names the file and line and then stops the whole script loading. |
|
||||
| **Runtime errors** | In-game console / `developer 1`. Many mod loaders print a script stack trace. |
|
||||
| **Static lint** | Third-party only, and dialect-specific. Use one if it exists for the target title; otherwise there is none. |
|
||||
| **Tests** | None off-host. Behaviour is verified by playing the map. |
|
||||
|
||||
Therefore:
|
||||
|
||||
- **`developer 1` (or the loader's equivalent) is mandatory during development.** Without it the
|
||||
engine suppresses script warnings and an undefined-value error looks like "the feature did
|
||||
nothing".
|
||||
- **Load the script and read the console on every change**, before judging behaviour. "Nothing
|
||||
happened" is far more often a parse error at load than a logic bug.
|
||||
- **Instrument with `iprintln` / `iprintlnbold` / `println`** on the success *and* the failure
|
||||
path. A function that silently returns on a guard is indistinguishable from one that never ran.
|
||||
- **Say plainly in your report that GSC behaviour is unverified** unless you actually loaded the
|
||||
map and watched it. There is no test suite to hide behind.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope — `self`, `level`, `game`, and Locals
|
||||
|
||||
| Scope | Lifetime | Use for |
|
||||
| :--- | :--- | :--- |
|
||||
| local variable | the function call | everything it can hold |
|
||||
| `self` | the entity | per-entity state (a player's, an actor's) |
|
||||
| `level` | the current map/level | per-round state, map config, shared handles |
|
||||
| `game` | across map restarts | persistent state only |
|
||||
|
||||
- **Prefix your own fields.** `level.mymod_spawn_queue`, not `level.queue`. `level` and `self`
|
||||
are one flat namespace shared with the stock game scripts and every other mod; a collision
|
||||
overwrites engine state and the symptom appears somewhere unrelated.
|
||||
- **Never write to a stock field you did not create** unless that is the documented integration
|
||||
point. Read it, copy it, leave it.
|
||||
- **A function called *on* an entity gets it as `self`**: `player giveweapon("m4")`. Calling the
|
||||
same function without a prefix leaves `self` undefined and the first `self.x` access errors.
|
||||
- `self` is not inherited into a `thread`ed call's children automatically in the way callers
|
||||
expect — `ent thread f()` gives `f` `self == ent`, but a plain `f()` inside it does not.
|
||||
|
||||
---
|
||||
|
||||
## 3. Undefined Is The Default — Guard Everything External
|
||||
|
||||
GSC has no types and no nullability. **Reading a field that was never set yields `undefined`,
|
||||
and using it in arithmetic or a method call is a runtime error that aborts the thread** — often
|
||||
silently from the player's point of view.
|
||||
|
||||
```gsc
|
||||
// WRONG: any of these can be undefined
|
||||
ent = level.target;
|
||||
ent setorigin(level.player.origin + (0, 0, 64));
|
||||
|
||||
// RIGHT
|
||||
if (!isdefined(level.target) || !isdefined(level.player))
|
||||
return;
|
||||
```
|
||||
|
||||
- **`isdefined()` on every entity, every `level.*`/`self.*` read you did not write in the same
|
||||
function, and every return value from a lookup.**
|
||||
- **Entities die.** A handle held across a `wait` may be a dead entity by the time the thread
|
||||
resumes. Re-check `isdefined(ent)` *after* every `wait`, not only before.
|
||||
- `isalive(ent)` is the check for an actor you intend to act on; `isdefined` only proves the
|
||||
handle exists.
|
||||
- This is the one place where the general "don't over-guard" rule inverts: in GSC a value from
|
||||
outside the current function genuinely can be undefined, so the check is not dead code.
|
||||
- Arrays: `array.size` is safe on an empty array but errors on an undefined one. Guard the array,
|
||||
then the index.
|
||||
|
||||
---
|
||||
|
||||
## 4. Threads, `notify`, `waittill`, `endon`
|
||||
|
||||
```gsc
|
||||
monitor_thing()
|
||||
{
|
||||
level endon("game_ended");
|
||||
self endon("death");
|
||||
|
||||
for (;;)
|
||||
{
|
||||
self waittill("trigger_activated");
|
||||
do_work();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`thread` starts a coroutine and returns immediately.** It does not return the function's
|
||||
value — a `thread`ed function's `return` value is unreachable. Pass a handle or notify.
|
||||
- **Every long-lived thread needs an `endon`.** Without one, a loop keeps running after the
|
||||
entity dies or the level ends, and then errors on a dead `self` — or leaks, one copy per spawn.
|
||||
`level endon("game_ended")` plus the relevant entity `endon` is the baseline.
|
||||
- **`endon` must be registered before the first `wait`**, at the top of the function. Registering
|
||||
it later leaves a window.
|
||||
- **`waittill` is the event primitive — prefer it to polling.** `self waittill("death")` is
|
||||
correct; `while (isalive(self)) wait 0.05;` is the same thing, worse, and costs every frame
|
||||
forever. This is the general events-over-polling rule, and GSC gives you the event.
|
||||
- **A `notify` with no waiter is lost.** There is no queue. If a thread might not be waiting yet,
|
||||
set a flag as well as notifying, and have the waiter check the flag first.
|
||||
- `waittill("x", a, b)` receives the notify's extra parameters. The names are positional.
|
||||
- **`waittillframeend`** is the tool for "after everything else this frame" — use it instead of
|
||||
`wait 0.05` when the intent is ordering, not delay.
|
||||
- `wait 0` is not zero; it is one frame. `wait 0.05` is one server frame at 20Hz — write the
|
||||
intent, not a magic number, and name the constant.
|
||||
|
||||
---
|
||||
|
||||
## 5. Per-Frame Cost Is Your Budget
|
||||
|
||||
The script VM shares the server frame with everything else, and there is a **hard instruction
|
||||
budget per frame**. Exceeding it produces `script runtime error: exceeded maximum number of
|
||||
script variables` or a visible server hitch.
|
||||
|
||||
- **One polling loop per concern, never one per entity.** A `for(;;) { wait 0.05 }` loop on each
|
||||
of 60 actors is 60 wakeups a frame. Keep one manager loop over an array.
|
||||
- **Where an event exists, the polling loop should not.** See §4.
|
||||
- **Never loop without a `wait` inside.** A `for(;;)` with no `wait` hangs the server frame and
|
||||
takes the match down — this is the single most common fatal GSC mistake.
|
||||
- Keep the body of a per-frame loop trivial. Move anything expensive behind a condition or a
|
||||
longer interval, and say in a comment why that interval.
|
||||
- Spawning is expensive and entity counts are capped. Reuse and pool; delete what you spawned.
|
||||
|
||||
---
|
||||
|
||||
## 6. Assets Must Be Precached
|
||||
|
||||
```gsc
|
||||
init()
|
||||
{
|
||||
precachemodel("com_laptop_open");
|
||||
precacheshader("hud_icon_frag");
|
||||
precachestring(&"MYMOD_HINT");
|
||||
}
|
||||
```
|
||||
|
||||
- **Precaching only works during the level's load phase.** A `precachemodel` after the map is
|
||||
live either errors or silently fails, and the model then renders as a missing-asset
|
||||
placeholder or nothing at all.
|
||||
- Put every precache in the init path, unconditionally — not behind the feature flag that decides
|
||||
whether the asset gets used.
|
||||
- **A model or weapon name not present in the loaded level's zone will not spawn**, regardless of
|
||||
precaching. Asset availability is per-map; a name verified on one map proves nothing about
|
||||
another. State which map you verified on.
|
||||
- Localized strings use the `&"REFERENCE"` form and must exist in the string table; a missing one
|
||||
displays the raw reference.
|
||||
|
||||
---
|
||||
|
||||
## 7. Language Facts That Surprise People
|
||||
|
||||
- **No exceptions, no `try`.** An error aborts the current thread and nothing else. There is no
|
||||
recovery and no handler — which is why §3's guards are the error handling.
|
||||
- **No classes, no inheritance, no `new`.** "Objects" are entities with fields, or plain arrays
|
||||
used as structs. `spawnstruct()` is the idiom for a bag of fields with no world presence.
|
||||
- **Function pointers**: `::funcname` (and `[[ptr]]()` to call) exist in most dialects but not
|
||||
all. Verify before relying on them.
|
||||
- **No closures** in most dialects — an inner function cannot capture a local. Pass state
|
||||
explicitly or hang it on a struct.
|
||||
- `foreach (x in arr)` exists in IW4+ and T6 but not in the earliest dialects; `for (i = 0;
|
||||
i < arr.size; i++)` always works.
|
||||
- **Arrays are associative.** `array[]` with integer keys behaves like a list, but deleting an
|
||||
element leaves a hole and `.size` counts keys. Rebuild rather than delete-in-place.
|
||||
- String comparison is `==`; there is no `switch` on anything but constants, and fallthrough
|
||||
rules differ by dialect.
|
||||
- Vectors are `(x, y, z)` literals with `+`, `-`, and scalar `*`. Component access is `v[0]`,
|
||||
`v[1]`, `v[2]`. `anglestoforward` / `vectornormalize` / `distance` are builtins — do not
|
||||
hand-roll vector maths.
|
||||
|
||||
---
|
||||
|
||||
## 8. Style & Structure
|
||||
|
||||
- **Allman braces, 4 spaces** — the stock scripts' convention; match it, because GSC is almost
|
||||
always read next to engine scripts.
|
||||
- `main()` is the level entry point; `init()` is the convention for a module's setup called from
|
||||
it. Which one the loader calls is dialect- and loader-specific — verify, do not assume.
|
||||
- `#include maps\_utility;` (dialect-dependent path syntax) at the top; prefer the stock utility
|
||||
functions over reimplementing them. `maps\_utility` and `common_scripts\utility` already have
|
||||
flag, wait, and array helpers.
|
||||
- **`/* */` comments do not nest.** Commenting out a block that contains one breaks the file.
|
||||
- File and function size limits from the general rules apply, and matter more here than usual:
|
||||
there is no debugger worth the name, so a 300-line function is unanalysable.
|
||||
- **Delete dead code.** With no linter to flag it, commented-out GSC accumulates forever.
|
||||
|
||||
---
|
||||
|
||||
## 9. Side Effects on Level State
|
||||
|
||||
GSC runs inside someone else's running game. A change to shared state is not scoped to your
|
||||
feature and is not undone when your thread ends.
|
||||
|
||||
- **Mutating a stock spawner, trigger, or flag changes the mission.** Setting
|
||||
`spawner.count = 999` to get an extra AI makes that spawner infinite for the rest of the level.
|
||||
Save the old value and restore it, or clone rather than mutate.
|
||||
- **Anything you spawn, you delete.** Track handles in your own `level.mymod_*` array and provide
|
||||
a cleanup path; an orphaned entity survives until map change and counts against the cap.
|
||||
- **`setdvar` is global and persistent.** A dvar used as a command channel must be cleared
|
||||
immediately on read, and the clear must happen even on the failure path — otherwise the next
|
||||
poll re-fires the same command.
|
||||
- A dvar as an IPC channel is a **poll**, which §5 says to avoid; it is acceptable only because
|
||||
GSC cannot receive a host event, and the interval and the clear-on-read should both be
|
||||
explicit and commented.
|
||||
- Never leave the player in a modified state (god mode, `notarget`, a changed movement speed)
|
||||
when the feature that set it is turned off. Toggles need an off path that actually restores.
|
||||
Reference in new issue
Block a user