Skip to content

missions: CM8 Ashfall under pressure - #8578

Closed
keithharvey wants to merge 2 commits into
cm8-ashfallfrom
cm8-ashfall-pressure
Closed

missions: CM8 Ashfall under pressure#8578
keithharvey wants to merge 2 commits into
cm8-ashfallfrom
cm8-ashfall-pressure

Conversation

@keithharvey

Copy link
Copy Markdown
Collaborator

The mission gains scavenger waves, and everything the mission needed in order to ask for them.

The pressure arc

Four beats in cm8_ashfall/triggers/waves.lua: open quiet half a minute in, climb when the outpost is relieved, spike when the enclave is found, stop when the commander dies. It names a pack — scavengers.skirmish — and the missions module resolves it through the scavengers module's mission vocabulary, so a mission gets the same wave director a multiplayer game runs, at a tenth of the intensity, with no bot on the field to activate it.

.After(seconds)

New, and what makes the opening beat possible. A trigger holds its effects until its conditions have gone on being true for that long, re-arming each time it fires so a repeating trigger reads as a rate limit. Seconds, never frames: MissionSeconds is a declared alias, so every editor deriving slots from the types can name the unit instead of showing a bare number.

Transfer.Give is repaired here, because CM8 is what proved it broken

Its contract is that no policy question is asked — the game moving a base, not a team choosing to share. But the move goes through Spring.TransferUnit, which fires AllowUnitTransfer, which the controller answers from the same sharing policy as any ordinary share. So the bypass bypassed nothing, and the policy gave the right answer to the wrong question: Gaia is nobody's ally, refuse.

The opening beat of this mission is "you inherit this failing base". What the player actually inherited was six structures that never moved and an enclave that shot at them. The give now announces itself for the duration of the move, the same way /take already did — a rules param rather than an upvalue, because VFS.Include is uncached and the controller and the caller hold different copies of api.lua.

The editor learns to watch

A trigger publishes that it has FIRED — not that its condition is true, which would flicker back off on a once-trigger — and both editors shade a card that has fired. That is the difference between a list of rules and a visible run through them. Wave progress is published the same way, so a director's clocks are readable from a panel.

One guard the combat module needed for the same reason waves did

The gadget handler defers its callin-list updates while iterating but nils the method field immediately, so unhooking from inside a callin takes the whole gadget down on the next frame.

Tests

CM8 needs an enemy team to arm at all, which the shared headless startscript does not have, so the scenario brings its own. test_waves.lua drives the whole arc bot-free; test_outpost.lua is the one that fails for thirty seconds without the Give fix. Both self-skip everywhere else.


Stacked on #8577 (scavengers) → #8576 (waves) → #8494 (cm8-ashfall).

Note for review: this commit bundles more than its title suggests — the Transfer.Give fix, .After/MissionSeconds, the fired-trigger shading and the combat callin guard are all separable if you would rather they landed as their own units.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Integration Test Results

23 tests  +2    8 ✅ ±0   16s ⏱️ ±0s
 1 suites ±0   11 💤 +2 
 1 files   ±0    4 ❌ ±0 

For more details on these failures, see this check.

Results for commit 52f7acc. ± Comparison against base commit 4484c4c.

♻️ This comment has been updated with latest results.

@keithharvey
keithharvey force-pushed the cm8-ashfall-pressure branch from 8ac453b to caf420a Compare August 1, 2026 18:28
The first mission built on the framework, and the reason the roster is a
DSL rather than a table: a unit is declared once, with the team it
belongs to and the name triggers refer to it by, so a trigger file says
Unit("outpost_command_hub") and a typo is a load error rather than a
condition that silently never fires.

Mission mode comes with it. Triggers own the verdict, so engine
elimination must not end the match — that is standing configuration, not
something a trigger can assert mid-game, and it belongs in the mode the
lobby picks.

The editor learns the two probes a roster makes possible: whether a
named unit is dead or has been spotted. Both read latches the loader
publishes, which is why they arrive with the roster rather than with the
editor.
The mission gains scavenger waves, and everything the mission needed in
order to ask for them.

The pressure arc is four beats in cm8_ashfall/triggers/waves.lua: open
quiet half a minute in, climb when the outpost is relieved, spike when the
enclave is found, and stop when the commander dies. It names a pack —
scavengers.skirmish — and the missions module resolves it through the
scavengers module's mission vocabulary, so a mission gets the same wave
director a multiplayer game runs, at a tenth of the intensity, with no bot
on the field to activate it.

.After(seconds) is new, and is what makes the opening beat possible. A
trigger holds its effects until its conditions have gone on being true for
that long, re-arming each time it fires so a repeating trigger reads as a
rate limit. Seconds, never frames: MissionSeconds is a declared alias, so
every editor deriving slots from the types can name the unit instead of
showing a bare number.

Transfer.Give is repaired here because CM8 is what proved it broken. Its
contract is that no policy question is asked — the game moving a base, not
a team choosing to share — but the move goes through Spring.TransferUnit,
which fires AllowUnitTransfer, which the controller answers from the same
sharing policy as any ordinary share. So the bypass bypassed nothing, and
the policy gave the right answer to the wrong question: Gaia is nobody's
ally, refuse. The opening beat of this mission is "you inherit this
failing base"; what the player actually inherited was six structures that
never moved and an enclave that shot at them. The give now announces
itself for the duration of the move, the same way /take already did, and a
rules param rather than an upvalue because VFS.Include is uncached and the
controller and the caller hold different copies of api.lua.

The editor learns to watch. A trigger publishes that it has FIRED — not
that its condition is true, which would flicker back off on a once-trigger
— and both editors shade a card that has fired, which is the difference
between a list of rules and a visible run through them. Wave progress is
published the same way, so a director's clocks are readable from a panel.

And a guard the combat module needed for the same reason waves did: the
gadget handler defers its callin-list updates while iterating but nils the
method field immediately, so unhooking from inside a callin takes the
whole gadget down on the next frame.

CM8 needs an enemy team to arm at all, which the shared headless
startscript does not have, so the scenario brings its own:
luaui/Tests/cm8_ashfall/test_waves.lua drives the whole arc bot-free, and
test_outpost.lua is the one that fails for thirty seconds without the
Give fix. Both self-skip everywhere else.
@keithharvey
keithharvey force-pushed the cm8-ashfall-pressure branch from caf420a to 52f7acc Compare August 2, 2026 14:16
@keithharvey
keithharvey changed the base branch from scavengers to cm8-ashfall August 2, 2026 14:16
@keithharvey

Copy link
Copy Markdown
Collaborator Author

Folded into #8494 — CM8 Ashfall is now one commit covering both the roster and the pressure arc, on the same cm8-ashfall branch. Closing this in favour of that PR; no content is lost (the resulting tree is unchanged).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant