docs(api): per-symbol pages, fuzzy search, deep token linking, hover cards
All checks were successful
docs / build-and-deploy (push) Successful in 2s
All checks were successful
docs / build-and-deploy (push) Successful in 2s
Rebuild the API Reference around one page per symbol and richer, verified content.
Pages & navigation
- One HTML page per symbol (kw-*, type-*, phase-*, screen-*, fn-*, annot-*, op-*)
instead of a single scrolling page; namespace overview pages (ns-screen …
ns-color) and a searchable index (api.html) with client-side fuzzy search.
- Sticky-header scroll offset (scroll-margin) so a jumped-to entry/param/color is
never hidden, plus a flash highlight on the scrolled-to target.
Deep linking in every snippet & example
- Namespace members split: `Screen`→namespace page, `fill_rectangle`→method page;
`Color`→palette page, `Charcoal`→its swatch — separately.
- Named arguments (`width:`) link to that parameter's anchor on the method page.
- Hover any token for a summary card built from the real API data (symbols.json).
Content & coverage
- Full authoritative surface documented from the compiler: every keyword, type,
the 6 phases (Start/Input/FixedUpdate/Update/LateUpdate/Render, each its own
page), all 22 annotations, namespace methods with parameter docs, builtins,
the world_* reflection ABI, networking, operators — 155 symbols.
- Longer, clearer explanations; "model"/"model instance" terminology, not "entity";
descriptive identifiers in every example (Position{column,row}, Velocity{delta_x,
delta_y}, Health{current,maximum}, Player/Enemy) — no Pos/Seg/x/dx.
- Accuracy fixes from compiler ground-truth: world_count() takes no arg,
world_query_next(property, cursor) arg order, event fields bind by name; dropped
`when` and `module` (not in the self-hosted parser).
Tooling
- inventory.json + check.py: coverage guard (every symbol has a page), duplicate-
token guard, and broken-link guard — fail CI so docs can't drift.
- validate.py: compiles every ```ludic example against bin/ludicc (158 compile).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
25f987e30d
commit
3c7ec9b016
172 changed files with 5240 additions and 895 deletions
|
|
@ -5,8 +5,30 @@ category: annotations
|
|||
kind: annotation
|
||||
tokens: @Computed
|
||||
sig: @Computed on a property field
|
||||
tip: A derived field: an expression expanded inline wherever it is read, never stored.
|
||||
tip: A derived field expanded inline wherever it is read, never stored.
|
||||
order: 1
|
||||
---
|
||||
|
||||
A derived field: an expression expanded inline wherever it is read, never stored.
|
||||
<code>@Computed</code> marks a property field as <b>derived</b>: instead of occupying a slot in the world, its expression is expanded inline at every site that reads it, so it is recomputed on demand and never takes storage. Use it for values that are a pure function of other fields — a total, a midpoint, a scaled amount — where storing them would risk going stale. Because a computed field has no backing storage it cannot be assigned to, and it is not something you replicate: it costs nothing on the wire and is simply recomputed on each peer. Reach for it when a value should always be consistent with its inputs and you would otherwise have to remember to recompute it by hand.
|
||||
|
||||
```ludic
|
||||
program DerivedStats {
|
||||
property Health {
|
||||
current: int = 100,
|
||||
maximum: int = 100,
|
||||
@Computed missing: int = maximum - current # never stored; expanded where read
|
||||
}
|
||||
model Player { Health }
|
||||
|
||||
handler ReportDamage phase Update {
|
||||
for (Health) in query [Health] {
|
||||
Health.current = 70
|
||||
print(Health.missing) # 30 — recomputed from current and maximum
|
||||
}
|
||||
}
|
||||
|
||||
handler SpawnPlayer phase Start {
|
||||
spawn Player { Health { current: 100, maximum: 100 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,24 @@ category: annotations
|
|||
kind: annotation
|
||||
tokens: @export
|
||||
sig: @export fn name(…) -> R
|
||||
tip: Expose a function as a C-ABI symbol from a module.
|
||||
tip: Expose a function as a C-ABI symbol so a host can call it.
|
||||
order: 3
|
||||
---
|
||||
|
||||
Expose a function as a C-ABI symbol from a <code>module</code>.
|
||||
<code>@export</code> makes a <code>fn</code> visible outside the module as a plain C-ABI symbol, so a host program or another linked object can call it by name. Without it, functions are internal to the compiled unit; with it, the emitted symbol is externally linkable, which is how Ludic hands entry points to a runtime seam or a foreign caller. It is the outbound counterpart to <code>extern fn</code>, which pulls a foreign symbol in. Keep exported signatures to POD scalars and pointers, since they cross a C boundary where Ludic's richer types do not apply.
|
||||
|
||||
```ludic
|
||||
program ScoreModule {
|
||||
var running_total: int = 0
|
||||
|
||||
@export fn add_points(amount: int) -> int { # callable from a C-ABI host
|
||||
running_total = running_total + amount
|
||||
return running_total
|
||||
}
|
||||
|
||||
entry {
|
||||
print(add_points(10)) # 10
|
||||
print(add_points(5)) # 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
33
docs/language/annotations/annot-handles.md
Normal file
33
docs/language/annotations/annot-handles.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
id: annot-handles
|
||||
name: @Handles
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Handles
|
||||
sig: @Handles(Event) handler Name { … }
|
||||
tip: Declarative hint naming the event or subsystem a handler is responsible for.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@Handles</code> is a declarative annotation that names the event or subsystem a handler (or a whole program) is responsible for. The parser accepts and records it as metadata for analysis and tooling — it does not by itself wire up dispatch, so it documents intent rather than driving execution. When you want a handler to actually run in response to an event, use <code>@On(Event)</code>, which registers the listener; <code>@Handles</code> is the companion label that makes ownership of an event legible across a modular codebase. Keep the named symbol a real event or system so the annotation stays meaningful.
|
||||
|
||||
```ludic
|
||||
program CombatModule {
|
||||
property Health { current: int = 100 }
|
||||
model Enemy { Health }
|
||||
|
||||
event DamageDealt { amount: int = 0 }
|
||||
|
||||
@Handles(DamageDealt) # documents this handler's responsibility
|
||||
@On(DamageDealt) handler ApplyDamage { # @On does the actual dispatch wiring
|
||||
for (Health) in query [Health, {Enemy}] {
|
||||
Health.current = Health.current - amount
|
||||
}
|
||||
}
|
||||
|
||||
entry {
|
||||
spawn Enemy { Health { current: 100 } }
|
||||
emit DamageDealt(amount: 25)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,12 +1,31 @@
|
|||
---
|
||||
id: annot-on
|
||||
name: @OnEnable / @OnDisable / @OnDespawn
|
||||
name: @On
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @OnEnable @OnDisable @OnDespawn
|
||||
sig: @OnDisable(Prop) handler … { … }
|
||||
tip: Lifecycle hooks that fire when a property/model is toggled or an entity is torn down.
|
||||
tokens: @On
|
||||
sig: @On(Event) handler Name { … }
|
||||
tip: Register a compile-time listener that runs whenever an event is emitted.
|
||||
order: 2
|
||||
---
|
||||
|
||||
Lifecycle hooks that fire when a property/model is toggled or an entity is torn down.
|
||||
<code>@On(Event)</code> turns a handler into a listener for a named <code>event</code>: whenever any code runs <code>emit Event(...)</code>, this handler's body runs with the event's fields bound by name. It is how systems talk to each other without referencing one another directly — the emitter never knows who is listening, so gameplay, UI and mods can all react to the same moment independently. Several handlers may listen to one event; they run in declaration order. On a <code>cancellable event</code>, a listener may <code>cancel</code> to veto it, and the emitter sees that outcome. Related lifecycle hooks such as <code>@OnSpawn</code>, <code>@OnEnable</code> and <code>@OnDespawn</code> are specialized event listeners the compiler wires up for you.
|
||||
|
||||
```ludic
|
||||
program Events {
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Player { Health }
|
||||
|
||||
event Damaged { amount: int = 0 }
|
||||
|
||||
handler SpawnPlayer phase Start {
|
||||
spawn Player { Health { current: 100, maximum: 100 } }
|
||||
emit Damaged(amount: 25)
|
||||
}
|
||||
|
||||
@On(Damaged)
|
||||
handler ReportDamage {
|
||||
print(amount)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
33
docs/language/annotations/annot-onattach.md
Normal file
33
docs/language/annotations/annot-onattach.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
id: annot-onattach
|
||||
name: @OnAttach
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @OnAttach
|
||||
sig: @OnAttach(Property) handler Name { … }
|
||||
tip: Run a handler when a property is structurally attached to a live instance.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@OnAttach(Property)</code> fires when the named property is <b>structurally added</b> to a live model instance with <code>attach P on e</code> — the property's fields are seeded first, then the hook runs in the context of that instance so it can read and adjust them. This is the structural birth of a property, the counterpart to <code>@OnDetach</code>; it differs from <code>@OnEnable</code>, which only un-pauses a property that already exists. Use it to react when a capability appears on an instance at runtime — a shield goes up, a status effect lands. The property name is a checked reference. Add <code>@Public</code> to also emit a <code>prop_<Property>_attach</code> event.
|
||||
|
||||
```ludic
|
||||
program AttachHook {
|
||||
property Tag { value: int = 0 }
|
||||
property Shield { amount: int = 0 }
|
||||
model Unit { Tag }
|
||||
|
||||
@OnAttach(Shield) handler RaiseShield { # fires when Shield is attached
|
||||
print(Shield.amount + 10) # 15 — reads the seeded amount
|
||||
}
|
||||
|
||||
handler Seed phase Start { spawn Unit { Tag { value: 1 } } }
|
||||
|
||||
handler AddShield phase Render {
|
||||
for (unit) in query [Unit] {
|
||||
attach Shield on self() { amount: 5 } # triggers @OnAttach
|
||||
}
|
||||
quit()
|
||||
}
|
||||
}
|
||||
```
|
||||
34
docs/language/annotations/annot-ondespawn.md
Normal file
34
docs/language/annotations/annot-ondespawn.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
id: annot-ondespawn
|
||||
name: @OnDespawn
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @OnDespawn
|
||||
sig: @OnDespawn(Model, reason: r) handler Name { … }
|
||||
tip: Run a handler when a model instance is torn down, optionally knowing why.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@OnDespawn(Model)</code> is the teardown counterpart to <code>@OnSpawn</code>: it fires once for each instance of the named model as it is removed, whether by an in-world <code>despawn</code> or at program shutdown, and its body still reads the instance's outgoing field values before they are gone. With the optional <code>reason: r</code> binding it becomes reason-carrying teardown — <code>r</code> is bound to an <code>EndReason</code> the compiler passes at each teardown site (an in-world despawn passes <code>EndReason.Despawned</code>, program exit passes <code>EndReason.Quit</code>) so one hook can branch on <b>why</b> the instance is ending, the way Unreal's <code>EndPlay(reason)</code> does. Every still-live instance's hook fires at quit, which makes "no silent deaths" real: drop loot on a real death but skip it when the app is simply closing. Add <code>@Public</code> to also emit a <code>model_<Model>_despawn</code> event.
|
||||
|
||||
```ludic
|
||||
program DespawnHook {
|
||||
property Health { current: int = 0 }
|
||||
property Loot { gold: int = 0 }
|
||||
model Enemy { Health, Loot }
|
||||
|
||||
@OnDespawn(Enemy, reason: teardown_reason) handler DropLoot {
|
||||
match teardown_reason {
|
||||
EndReason.Quit => {} # app closing — do not drop loot
|
||||
_ => { print(Loot.gold) } # died in-world — award the gold
|
||||
}
|
||||
}
|
||||
|
||||
handler SpawnWave phase Start { spawn Enemy { Health { current: 10 }, Loot { gold: 25 } } }
|
||||
|
||||
handler KillOne phase Render {
|
||||
for (Health) in query [Health, {Enemy}] { despawn self() } # prints 25
|
||||
quit()
|
||||
}
|
||||
}
|
||||
```
|
||||
36
docs/language/annotations/annot-ondetach.md
Normal file
36
docs/language/annotations/annot-ondetach.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
id: annot-ondetach
|
||||
name: @OnDetach
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @OnDetach
|
||||
sig: @OnDetach(Property) handler Name { … }
|
||||
tip: Run a handler when a property is structurally detached from a live instance.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@OnDetach(Property)</code> is the teardown counterpart to <code>@OnAttach</code>: it fires when the named property is structurally removed from a live instance with <code>detach P on e</code>. The hook runs <b>before</b> the property's presence flag clears, so its body can still read the outgoing field values one last time — useful for releasing whatever the property was tracking or logging its final state. It is distinct from <code>@OnDisable</code>, which pauses a property while keeping its data; detach actually destroys the property's presence on the instance. Add <code>@Public</code> to also emit a <code>prop_<Property>_detach</code> event.
|
||||
|
||||
```ludic
|
||||
program DetachHook {
|
||||
property Tag { value: int = 0 }
|
||||
property Shield { amount: int = 0 }
|
||||
model Unit { Tag }
|
||||
|
||||
@OnDetach(Shield) handler DropShield { # fires as Shield is removed
|
||||
print(Shield.amount + 20) # 25 — reads the outgoing amount
|
||||
}
|
||||
|
||||
handler Seed phase Start {
|
||||
spawn Unit { Tag { value: 1 } }
|
||||
for (unit) in query [Unit] { attach Shield on self() { amount: 5 } }
|
||||
}
|
||||
|
||||
handler RemoveShield phase Render {
|
||||
for (shield) in query [Shield] {
|
||||
detach Shield on self() # triggers @OnDetach
|
||||
}
|
||||
quit()
|
||||
}
|
||||
}
|
||||
```
|
||||
33
docs/language/annotations/annot-ondisable.md
Normal file
33
docs/language/annotations/annot-ondisable.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
id: annot-ondisable
|
||||
name: @OnDisable
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @OnDisable
|
||||
sig: @OnDisable(Property) handler Name { … }
|
||||
tip: Run a handler when a property is paused (disabled) on an instance.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@OnDisable(Property)</code> fires when a present property is paused with <code>disable P on e</code>. Disabling keeps the property's data intact and simply stops it from participating — the logical "off" switch — so this hook is the moment a capability goes dormant, not the moment it is destroyed (that is <code>@OnDetach</code>). The body runs in the instance's context and can read the fields before they go quiet, which is handy for cleaning up an effect the property was driving. It pairs with <code>@OnEnable</code> for capabilities that toggle on and off repeatedly. Add <code>@Public</code> to also emit a <code>prop_<Property>_disable</code> event.
|
||||
|
||||
```ludic
|
||||
program DisableHook {
|
||||
property Tag { value: int = 0 }
|
||||
property Shield { amount: int = 0 }
|
||||
model Unit { Tag, Shield }
|
||||
|
||||
@OnDisable(Shield) handler ShieldDown { # fires when Shield is disabled
|
||||
print(Shield.amount) # 5 — data is retained, just paused
|
||||
}
|
||||
|
||||
handler Seed phase Start { spawn Unit { Tag { value: 1 }, Shield { amount: 5 } } }
|
||||
|
||||
handler Deactivate phase Render {
|
||||
for (unit) in query [Unit] {
|
||||
disable Shield on self() # triggers @OnDisable
|
||||
}
|
||||
quit()
|
||||
}
|
||||
}
|
||||
```
|
||||
34
docs/language/annotations/annot-onenable.md
Normal file
34
docs/language/annotations/annot-onenable.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
id: annot-onenable
|
||||
name: @OnEnable
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @OnEnable
|
||||
sig: @OnEnable(Property) handler Name { … }
|
||||
tip: Run a handler when a paused property is re-enabled on an instance.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@OnEnable(Property)</code> fires when a property that is present but disabled is turned back on with <code>enable P on e</code>. Unlike <code>@OnAttach</code>, the property's data was never destroyed — enable/disable only pause and resume a property that already exists, keeping its fields intact — so this hook is about a capability becoming active again rather than being created. The body runs in the instance's context and can read the retained field values. Use the enable/disable pair for a capability that toggles repeatedly (a power-up, a stun) where re-seeding on every toggle would be wrong. Add <code>@Public</code> to also emit a <code>prop_<Property>_enable</code> event.
|
||||
|
||||
```ludic
|
||||
program EnableHook {
|
||||
property Tag { value: int = 0 }
|
||||
property Shield { amount: int = 0 }
|
||||
model Unit { Tag, Shield }
|
||||
|
||||
@OnEnable(Shield) handler ShieldBackOnline { # fires when Shield is re-enabled
|
||||
print(Shield.amount) # 5 — the retained amount, not re-seeded
|
||||
}
|
||||
|
||||
handler Seed phase Start {
|
||||
spawn Unit { Tag { value: 1 }, Shield { amount: 5 } }
|
||||
for (unit) in query [Unit] { disable Shield on self() }
|
||||
}
|
||||
|
||||
handler Reactivate phase Render {
|
||||
for (unit) in query [Unit] { enable Shield on self() } # triggers @OnEnable
|
||||
quit()
|
||||
}
|
||||
}
|
||||
```
|
||||
27
docs/language/annotations/annot-onquit.md
Normal file
27
docs/language/annotations/annot-onquit.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: annot-onquit
|
||||
name: @OnQuit
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @OnQuit
|
||||
sig: @OnQuit handler Name { … }
|
||||
tip: Run a handler once at shutdown, after the last frame.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@OnQuit</code> pins a handler to the program's shutdown moment: it runs exactly once as the program exits, after the final frame. Use it for teardown that must happen once — flushing a save, printing a final tally, releasing a host resource. In the lifecycle timeline it runs after every still-live instance's <code>@OnDespawn</code> hook has fired, so the world has already been torn down by the time <code>@OnQuit</code> sees it. It is the boot-time counterpart of <code>@OnStart</code>; add <code>@Public</code> to also emit a <code>program_quit</code> event other modules can hook.
|
||||
|
||||
```ludic
|
||||
program QuitHook {
|
||||
property Score { total: int = 0 }
|
||||
model Scoreboard { Score }
|
||||
|
||||
@OnQuit handler ReportFinalScore { # runs once at shutdown
|
||||
for (Score) in query [Score] { print(Score.total) }
|
||||
}
|
||||
|
||||
handler Seed phase Start { spawn Scoreboard { Score { total: 42 } } }
|
||||
|
||||
handler Finish phase Render { quit() } # triggers shutdown -> prints 42
|
||||
}
|
||||
```
|
||||
28
docs/language/annotations/annot-onspawn.md
Normal file
28
docs/language/annotations/annot-onspawn.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: annot-onspawn
|
||||
name: @OnSpawn
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @OnSpawn
|
||||
sig: @OnSpawn(Model) handler Name { … }
|
||||
tip: Run a handler once each time an instance of a model is spawned.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@OnSpawn(Model)</code> turns a handler into a birth hook: it fires once for each newly <code>spawn</code>ed instance of the named model, right after its properties are seeded. The body runs in the context of that fresh instance, so it can read and adjust the instance's fields by property name to finish initialization — seed derived state, register the instance, play a spawn effect. The model name is a checked reference, so a typo is a compile error rather than a silently dead hook. Its teardown counterpart is <code>@OnDespawn</code>; add <code>@Public</code> to also emit a <code>model_<Model>_spawn</code> event other modules can listen for.
|
||||
|
||||
```ludic
|
||||
program SpawnHook {
|
||||
property Health { current: int = 0, maximum: int = 100 }
|
||||
model Enemy { Health }
|
||||
|
||||
@OnSpawn(Enemy) handler InitEnemy { # fires per new Enemy instance
|
||||
Health.current = Health.maximum # start every enemy at full health
|
||||
print(Health.current) # 100
|
||||
}
|
||||
|
||||
handler SpawnWave phase Start {
|
||||
spawn Enemy { Health { current: 0, maximum: 100 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/annotations/annot-onstart.md
Normal file
28
docs/language/annotations/annot-onstart.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: annot-onstart
|
||||
name: @OnStart
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @OnStart
|
||||
sig: @OnStart handler Name { … }
|
||||
tip: Run a handler once at boot instead of assigning it a frame phase.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@OnStart</code> pins a handler to the program's boot moment: it runs exactly once when the program starts, before the per-frame phases begin, instead of being placed in a recurring phase. Use it for one-time setup — spawning the initial world, seeding program vars, loading a scene. It overrides the handler's phase, so you write <code>@OnStart handler Name { … }</code> rather than <code>handler Name phase Start</code> and get the same once-at-boot placement through the annotation channel. Its shutdown counterpart is <code>@OnQuit</code>; add <code>@Public</code> to also emit a <code>program_start</code> event other modules can hook.
|
||||
|
||||
```ludic
|
||||
program BootHook {
|
||||
property Health { current: int = 0, maximum: int = 100 }
|
||||
model Player { Health }
|
||||
|
||||
var elapsed_frames: int = 0
|
||||
|
||||
@OnStart handler CreateWorld { # runs once at boot
|
||||
spawn Player { Health { current: 100, maximum: 100 } }
|
||||
elapsed_frames = 0
|
||||
}
|
||||
|
||||
handler CountFrames phase Update { elapsed_frames = elapsed_frames + 1 }
|
||||
}
|
||||
```
|
||||
29
docs/language/annotations/annot-owned.md
Normal file
29
docs/language/annotations/annot-owned.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
id: annot-owned
|
||||
name: @Owned
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Owned
|
||||
sig: @Owned model Name { … }
|
||||
tip: Give a model a network owner slot so its instances can be assigned to a peer.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@Owned</code> declares that every instance of a model carries a network owner — it adds an owner slot to the model (the runtime's <code>@L_owner</code> array), which the builtins <code>owner</code>, <code>set_owner</code>, and <code>is_owner</code> read and assign. A fresh instance starts unowned (<code>-1</code>) until the authority assigns it. Ownership is what gates who may write <code>@Sync(to: owner)</code> fields and who runs <code>@Predicted</code> control handlers, and it is stored in the world snapshot so it round-trips through replication and rollback. Mark the models that represent a player's avatar or units; leave shared scenery unowned.
|
||||
|
||||
```ludic
|
||||
program OwnedModel {
|
||||
@Sync property Position { column: int = 0, row: int = 0 }
|
||||
@Owned model Player { @Sync Position } # each Player instance has an owner slot
|
||||
|
||||
entry {
|
||||
spawn Player { Position { column: 5, row: 6 } }
|
||||
for (Position) in query [Position, {Player}] {
|
||||
let player_id = self()
|
||||
print(owner(player_id)) # -1 — unowned until assigned
|
||||
set_owner(player_id, 0)
|
||||
print(is_owner(player_id)) # 1 — the local peer now owns it
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
30
docs/language/annotations/annot-predicted.md
Normal file
30
docs/language/annotations/annot-predicted.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
id: annot-predicted
|
||||
name: @Predicted
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Predicted
|
||||
sig: @Predicted handler Name { … }
|
||||
tip: Run a control handler on the owning client speculatively and on the server authoritatively.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@Predicted</code> marks a control handler that runs in two places: speculatively on the client that <b>owns</b> the affected instance, so local input feels instant, and authoritatively on the server, whose result reconciles the client if the two diverge. It is the responsive-control role from Quake-style client-prediction, named for the netcode behavior (owner-predicts plus server-authoritative plus reconcile) rather than the machine, and it matches Unity's <code>GhostMode.Predicted</code> so the concept transfers. Prediction is explicit — Ludic never silently predicts — and it only applies to instances of an <code>@Owned</code> model, since the dispatch reads <code>is_owner</code> to decide whether the local client should run it. Use it for the owning player's movement and actions; leave authority-only rules on <code>@Server</code>.
|
||||
|
||||
```ludic
|
||||
program PredictedMovement {
|
||||
@Sync property Position { column: int = 0, row: int = 0 }
|
||||
@Owned model Player { @Sync Position }
|
||||
|
||||
@Predicted handler MovePlayer phase Input { # owner predicts, server reconciles
|
||||
let pressed = Input.key()
|
||||
for (Position) in query [Position, {Player}] {
|
||||
if pressed == 'd' { Position.column = Position.column + 1 }
|
||||
}
|
||||
}
|
||||
|
||||
handler SpawnPlayer phase Start {
|
||||
spawn Player { Position { column: 0, row: 0 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
31
docs/language/annotations/annot-public.md
Normal file
31
docs/language/annotations/annot-public.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
id: annot-public
|
||||
name: @Public
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Public
|
||||
sig: @Public @OnSpawn(Model) handler … { … }
|
||||
tip: Promote a lifecycle hook to a public event other modules can listen for.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@Public</code> stacks in front of a lifecycle hook to also emit a well-named event at the same moment the hook fires, so other modules (or mods) can react without touching the original code. It turns a private hook into a public seam: <code>@Public @OnSpawn(Model)</code> emits <code>model_<Model>_spawn</code>, <code>@OnDespawn</code> emits <code>model_<Model>_despawn</code>, the property hooks emit <code>prop_<Property>_attach</code>/<code>_detach</code>/<code>_enable</code>/<code>_disable</code>, and <code>@OnStart</code>/<code>@OnQuit</code> emit <code>program_start</code>/<code>program_quit</code>. This is the same lowering the event system uses, so anyone can <code>@On</code> the generated event to extend behavior at that lifecycle point. Add it when a hook marks an extension point you want to keep open across a modular codebase.
|
||||
|
||||
```ludic
|
||||
program PublicSpawn {
|
||||
property Health { current: int = 0, maximum: int = 100 }
|
||||
model Enemy { Health }
|
||||
|
||||
@Public @OnSpawn(Enemy) handler InitEnemy { # also emits model_Enemy_spawn
|
||||
Health.current = Health.maximum
|
||||
}
|
||||
|
||||
@On(model_Enemy_spawn) handler AnnounceEnemy { # another module hooks the public event
|
||||
print(1) # 1 — reacts to every Enemy spawn
|
||||
}
|
||||
|
||||
handler SpawnWave phase Start {
|
||||
spawn Enemy { Health { current: 0, maximum: 100 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -4,9 +4,27 @@ name: @Queries
|
|||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Queries
|
||||
sig: @Queries(these: [Pos, Vel])
|
||||
tip: Declare the properties a handler touches, binding their fields by name in the body.
|
||||
sig: @Queries(these: [Position, Velocity])
|
||||
tip: Declare the properties a handler operates on, binding their fields by name in the body.
|
||||
order: 0
|
||||
---
|
||||
|
||||
Declare the properties a handler touches, binding their fields by name in the body.
|
||||
<code>@Queries</code> lets a handler declare the properties it operates on up front, and the compiler wraps the whole body in a query loop over the matching model instances. The body then runs once per match with each listed property bound by name — you write <code>Position.column</code> directly instead of opening a <code>for (…) in query […]</code> yourself — and <code>self()</code> gives the current instance. It takes the same shape as a manual query: <code>these: [ … ]</code> lists the required properties (each may carry a filter like <code>Health{current <= 0}</code>), and an optional <code>on: Model</code> restricts the match to instances of one model. Reach for it when a handler's entire job is "for every matching instance, do this"; drop to an explicit <code>query</code> when you need finer control inside the loop.
|
||||
|
||||
```ludic
|
||||
program AdvanceMovement {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
model Player { Position, Velocity }
|
||||
|
||||
@Queries(these: [Position, Velocity], on: Player) # body runs once per matching Player
|
||||
handler AdvancePositions phase FixedUpdate {
|
||||
Position.column = Position.column + Velocity.delta_x
|
||||
Position.row = Position.row + Velocity.delta_y
|
||||
}
|
||||
|
||||
handler SpawnPlayer phase Start {
|
||||
spawn Player { Position { column: 0, row: 0 }, Velocity { delta_x: 1, delta_y: 0 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
31
docs/language/annotations/annot-reads.md
Normal file
31
docs/language/annotations/annot-reads.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
id: annot-reads
|
||||
name: @Reads
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Reads
|
||||
sig: @Reads(Property)
|
||||
tip: Declare that a handler reads a property — an analysis and scheduling hint.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@Reads(Property)</code> declares that a handler reads the named property. It is an analysis hint: it documents a handler's data dependencies so tooling and scheduling can reason about which handlers touch which state — for example, to tell whether two handlers can run without conflict. It does not itself bind fields the way <code>@Queries</code> does, nor does it grant access; it annotates intent alongside the handler's actual query. Pair it with <code>@Writes</code> to spell out the full read/write footprint of a handler. Keep the named property real so the declared footprint matches what the body does.
|
||||
|
||||
```ludic
|
||||
program ReadFootprint {
|
||||
property Health { current: int = 100 }
|
||||
property Score { total: int = 0 }
|
||||
model Player { Health, Score }
|
||||
|
||||
@Reads(Health) # declares the read dependency
|
||||
handler ReportLowHealth phase Update {
|
||||
for (Health) in query [Health, {Player}] {
|
||||
if Health.current < 25 { print(Health.current) }
|
||||
}
|
||||
}
|
||||
|
||||
handler SpawnPlayer phase Start {
|
||||
spawn Player { Health { current: 20 }, Score { total: 0 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
31
docs/language/annotations/annot-server.md
Normal file
31
docs/language/annotations/annot-server.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
id: annot-server
|
||||
name: @Server
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Server
|
||||
sig: @Server handler Name { … }
|
||||
tip: Run a handler only on the authority; clients receive the result via @Sync.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@Server</code> marks a handler as server-authoritative: it runs only on the peer acting as the authority, and clients receive its effects through ordinary <code>@Sync</code> replication rather than by running the handler themselves. This is the declarative alternative to sprinkling <code>is_server()</code> branches through gameplay code — the role is a property of the handler, so every line's placement stays legible. Unmarked handlers are the shared, deterministic simulation that runs everywhere; use <code>@Server</code> for decisions that must have a single source of truth, like awarding score, resolving damage, or spawning authoritative entities. Offline the role register defaults to server, so the guard collapses to "run here" and a single-player build is unchanged.
|
||||
|
||||
```ludic
|
||||
program AuthoritativeScore {
|
||||
property Score { total: int = 0 }
|
||||
model Scoreboard { Score }
|
||||
|
||||
handler Tick phase Update { # unmarked -> runs on every peer
|
||||
for (Score) in query [Score] { Score.total = Score.total + 1 }
|
||||
}
|
||||
|
||||
@Server handler AwardBonus phase Update { # authority only
|
||||
for (Score) in query [Score] { Score.total = Score.total + 100 }
|
||||
}
|
||||
|
||||
handler SpawnScoreboard phase Start {
|
||||
spawn Scoreboard { Score { total: 0 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,12 +1,29 @@
|
|||
---
|
||||
id: annot-sync
|
||||
name: @Sync / @Owned
|
||||
name: @Sync
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Sync @Owned
|
||||
tokens: @Sync
|
||||
sig: @Sync property … / @Owned
|
||||
tip: Mark a property's fields as replicated, and entities as owned, for networking.
|
||||
tip: Mark fields as replicated and models as owned so the compiler generates networking.
|
||||
order: 4
|
||||
---
|
||||
|
||||
Mark a property's fields as replicated, and entities as owned, for networking.
|
||||
These are the two declarative switches at the top of Ludic's networking: <code>@Sync</code> says <b>what</b> replicates and <code>@Owned</code> says <b>who</b> owns an instance. Replication is opt-in at the field level and decided per model use-site — a field crosses the wire only when it is both replicable (marked <code>@Sync</code>, either on the field or via <code>@Sync property P</code> which marks every field of <code>P</code>) <b>and</b> participating (the model marks the component <code>@Sync</code>). So the same property can replicate in one model and not another, and there is no <code>@NoSync</code> because the surface is purely additive. From these marks the compiler generates the per-model <code>serialize</code>/<code>apply</code> codecs; <code>@Sync</code> on a non-POD-scalar field (like a <code>ptr</code>) is a compile error, since a machine-local pointer cannot cross the wire. <code>@Owned</code> adds the owner slot the ownership builtins and <code>@Predicted</code> read. See the dedicated <code>@Owned</code> page for ownership details.
|
||||
|
||||
```ludic
|
||||
program SyncedWorld {
|
||||
@Sync property Position { column: int = 0, row: int = 0 } # every field replicable
|
||||
property Health { @Sync current: int = 0, maximum: int = 0 } # only current replicable
|
||||
|
||||
@Owned model Player { @Sync Position, @Sync Health } # participates -> column,row,current
|
||||
model Scenery { Position } # not @Sync here -> never replicates
|
||||
|
||||
entry {
|
||||
spawn Player { Position { column: 3, row: 4 }, Health { current: 50, maximum: 100 } }
|
||||
for (Position, Health) in query [Position, Health, {Player}] {
|
||||
print(sync_size(self())) # 12 = Position(8) + Health.current(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
32
docs/language/annotations/annot-toclients.md
Normal file
32
docs/language/annotations/annot-toclients.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
id: annot-toclients
|
||||
name: @ToClients
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @ToClients
|
||||
sig: @ToClients event Name { … }
|
||||
tip: A remote event broadcast from the server to clients — a server→clients notification.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@ToClients</code> marks an <code>event</code> as a directional remote event that travels from the authority down to clients — a broadcast, again the ordinary event bus with a direction flag. When the server <code>emit</code>s it, the POD payload is serialized as an event id plus its packed fields and sent through the transport seam to the clients; each client's pump re-emits it into normal <code>@On</code> dispatch, so an <code>@On(Name)</code> handler on every client reacts. Use it for authoritative notifications a client should present but not decide — an explosion happened, a round ended, a pickup was granted. Its mirror is <code>@ToServer</code>, which carries client requests the other way.
|
||||
|
||||
```ludic
|
||||
program ExplosionBroadcast {
|
||||
property EffectLog { count: int = 0 }
|
||||
model Client { EffectLog }
|
||||
|
||||
@ToClients event Boom { column: int = 0, row: int = 0 } # server -> clients broadcast
|
||||
|
||||
@On(Boom) handler PlayExplosion { # every client reacts
|
||||
for (EffectLog) in query [EffectLog, {Client}] {
|
||||
EffectLog.count = EffectLog.count + 1
|
||||
}
|
||||
}
|
||||
|
||||
entry {
|
||||
spawn Client { EffectLog { count: 0 } }
|
||||
emit Boom(column: 8, row: 3) # serialized to clients, re-emitted on arrival
|
||||
}
|
||||
}
|
||||
```
|
||||
32
docs/language/annotations/annot-toserver.md
Normal file
32
docs/language/annotations/annot-toserver.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
id: annot-toserver
|
||||
name: @ToServer
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @ToServer
|
||||
sig: @ToServer event Name { … }
|
||||
tip: A remote event sent from a client to the server — a client→server request.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@ToServer</code> marks an <code>event</code> as a directional remote event that travels from a client up to the authority — an RPC, expressed as the ordinary event bus with a direction flag rather than a new concept. At an <code>emit</code> site the event's POD payload is serialized as an event id followed by its packed fields and sent through the transport seam toward the server; it does <b>not</b> run locally. On the far side, draining the transport (the runtime's pump) re-emits it into the normal <code>@On</code> dispatch, so a <code>@Server @On(Name)</code> handler picks it up. Use it for player intent — a move request, a fire command — that the authority must validate before it affects the world. Its mirror is <code>@ToClients</code>, which broadcasts the other way.
|
||||
|
||||
```ludic
|
||||
program FireRequest {
|
||||
property AmmoLog { shots_fired: int = 0 }
|
||||
model Turret { AmmoLog }
|
||||
|
||||
@ToServer event Fire { direction: int = 0 } # client -> server request
|
||||
|
||||
@Server @On(Fire) handler HandleFire { # authority validates and applies
|
||||
for (AmmoLog) in query [AmmoLog, {Turret}] {
|
||||
AmmoLog.shots_fired = AmmoLog.shots_fired + direction
|
||||
}
|
||||
}
|
||||
|
||||
entry {
|
||||
spawn Turret { AmmoLog { shots_fired: 0 } }
|
||||
emit Fire(direction: 1) # serialized onto the wire, not run locally
|
||||
}
|
||||
}
|
||||
```
|
||||
30
docs/language/annotations/annot-writes.md
Normal file
30
docs/language/annotations/annot-writes.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
id: annot-writes
|
||||
name: @Writes
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Writes
|
||||
sig: @Writes(Property)
|
||||
tip: Declare that a handler writes a property — an analysis and scheduling hint.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>@Writes(Property)</code> declares that a handler writes the named property. Like <code>@Reads</code> it is an analysis hint that documents a handler's data footprint so tooling and scheduling can reason about conflicts — two handlers that write the same property, or one that writes what another reads, cannot be reordered freely. It does not bind fields or grant access on its own; the handler still queries and mutates the property in its body as usual. Declaring reads and writes together makes a handler's effect on the world legible at a glance, which matters most in a large, modular codebase. Keep the named property accurate so the declared footprint matches the code.
|
||||
|
||||
```ludic
|
||||
program WriteFootprint {
|
||||
property Health { current: int = 100 }
|
||||
model Enemy { Health }
|
||||
|
||||
@Reads(Health) @Writes(Health) # full read/write footprint
|
||||
handler ApplyPoison phase Update {
|
||||
for (Health) in query [Health, {Enemy}] {
|
||||
Health.current = Health.current - 1
|
||||
}
|
||||
}
|
||||
|
||||
handler SpawnEnemy phase Start {
|
||||
spawn Enemy { Health { current: 100 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue