Skip to content

Unit scripts: no double scheduling, completions sent once, COB UNIT_TEAM, failed LUS Killed, COB woken threads in saves - #3479

Open
moss-orbit-9f339e wants to merge 2 commits into
beyond-all-reason:masterfrom
moss-orbit-9f339e:fix/unit-script-scheduling
Open

moss-orbit-9f339e wants to merge 2 commits into
beyond-all-reason:masterfrom
moss-orbit-9f339e:fix/unit-script-scheduling

Conversation

@moss-orbit-9f339e

@moss-orbit-9f339e moss-orbit-9f339e commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Five fixes in the unit script engine. They change synced behaviour only in the cases that they describe.

  • A script could be in animating two times. CUnitScriptEngine::AddInstance called VectorInsertUnique without checkIfUnique (the true was commented out). In the single-threaded part of Tick, a completion callback of script A can start an animation on script B after B's last animation ended in this tick (in TickAllAnims) but before B's turn in the loop removed it. HaveAnimations() is then false, so B was added a second time. In the next tick for_mt ran TickAllAnims for B on two threads at the same time.
  • Old completions were sent again. TickAnimFinished sends the completions in doneAnims, which is cleared only in TickAllAnims. A script that is added again in the same tick (see above) sent its old completions a second time, which could wake a thread that waits on a new animation of the same piece and axis. A completion is now sent once (hasWaiting is cleared); the records stay for DumpState.
  • COB UNIT_TEAM: GetUnitVal(UNIT_TEAM, id) returned the team of the calling unit, not the team of unit id.
  • LUS Killed error: when the callin raised an error, inKilled stayed true and KilledScriptFinished was never called, so the dead unit was never removed. It now finishes with the default wreck level, as the code already does for a bad return value.
  • COB threads lost on load: waitingThreadIDs was CR_IGNORED ("always empty when saving"), but a COB thread that is woken after TickRunningThreads (for example when its animation ends in CUnitScriptEngine::Tick) waits there for the next frame. After a load it was in no list and never ran again. It is now saved.

Sync: changes only in the cases above. The replay below gives the same checksums as master, so it does not reach them.

Testing (Windows, RTX 3080; master 01efdf4312 and this branch, each with the springcontent.sdz of its own build; a Zero-K replay on Nuclear Winter, 29 players and about 1,000 units, many LUS units):

  • The checksums (270 frames) and unit counts are the same as master. No Lua errors.
  • I did not make a test for the double entry, the repeated completion, a COB UNIT_TEAM query, a Killed error or a save with a woken COB thread. These are checked by reading the code.

AI disclosure: OpenAI Codex (GPT-6 Astra and GPT-6.1 Sol) found these bugs in code audits and drafted the fixes; a second Codex review checked the branch. Claude Code checked each finding against the code, reviewed and edited every change, and ran the tests.

🤖 Generated with Claude Code

moss-orbit-9f339e and others added 2 commits October 13, 2026 01:21
…_TEAM, failed Killed, COB waiting threads in saves

- CUnitScriptEngine::AddInstance did not check that the script was
  already in animating. In the single-threaded part of Tick, a
  completion callback of script A can start an animation on script B
  after B's last animation ended in this tick but before B was removed
  from animating; B then had two entries, and the next tick ran
  TickAllAnims for B twice, on two worker threads at the same time.
  Pass checkIfUnique = true.
- TickAnimFinished sent the completions in doneAnims again each time it
  ran. doneAnims is cleared only in TickAllAnims, so a script that was
  added again in the same tick (see above) sent its old completions a
  second time and could wake a thread that waits on a new animation of
  the same piece and axis. A completion is now sent once (hasWaiting is
  cleared); the records stay for DumpState.
- COB GetUnitVal(UNIT_TEAM, id) returned the team of the calling unit,
  not of unit id.
- LUS Killed: when the callin raised an error, inKilled stayed true and
  KilledScriptFinished was never called, so the dead unit was never
  removed. Finish with the default wreck level, as for a bad return
  value.
- CCobEngine: waitingThreadIDs was not saved (CR_IGNORED, "always empty
  when saving"). It is not empty at the end of a frame: a COB thread
  that is woken after TickRunningThreads (for example when its animation
  ends in CUnitScriptEngine::Tick) waits there for the next frame. After
  a load, such a thread was in no list and never ran again.

Found by a Codex audit and checked by a second review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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