Skip to content

Fix stale subsystem model_num references across mission boundaries#7370

Draft
Goober5000 wants to merge 1 commit intoscp-fs2open:masterfrom
Goober5000:fix/repulse
Draft

Fix stale subsystem model_num references across mission boundaries#7370
Goober5000 wants to merge 1 commit intoscp-fs2open:masterfrom
Goober5000:fix/repulse

Conversation

@Goober5000
Copy link
Copy Markdown
Contributor

Fixes #3147, the long-standing intermittent bug where the GTD Orion#Repulse in Silent Threat: Reborn's "Forced Hand" mission spawns with all subsystems unlinked, leaving the ship untargetable and undisableable. The same root cause has been reported since at least 2015 (HLP topic 89403) and was provisionally closed in #3147 as fixed by #3858, but #3858 was an unrelated parse_ship_values memory leak fix and never actually addressed this behavior. Recently the underlying check at ship.cpp ~8043 was promoted from Warning to Error (#5591), which is why Release players are now hitting a hard error dialog instead of just seeing missing subsystems.

Root cause

model_unload() walks Ship_info and resets si.model_num = -1 on every ship_info that referenced the model, but does not touch si.subsystems[k].model_num. Subsystem entries are left pointing at the freed model id. When a subsequent mission reloads the same pof under a new id, ship variants that share the model rely on ship_copy_subsystem_fixup() to re-link their subsystem arrays, but the fixup matches by name via model_copy_subsystems() and any unmatched destination subsystem stays at -1. The mismatch case in model_copy_subsystems() was guarded only by Int3(), which is a no-op in Release builds, so the failure was completely silent until subsys_set() errored out much later with no useful context.

The bug is intermittent because it depends on three independent ordering factors: which prior mission was last played, which Orion variant that mission used, and the iteration order of Ship_info during the next mission's ship_page_in().

Changes

  • model_unload(): when clearing si.model_num for an unloaded model, also clear every si.subsystems[k].model_num that pointed at the same id. This is the structural fix and eliminates the entire class of stale subsystem pointers surviving across mission boundaries.

  • model_copy_subsystems(): replace the silent Int3() in the name-mismatch branch with Error(LOCATION, ...) naming the unmatched subsystem. This surfaces a failure mode that has been invisible in Release builds for over a decade and gives modders an actionable diagnostic when sibling ship classes that share a model have inconsistent subsystem names.

  • ship.cpp: introduce verify_ship_subsystems_linked() helper that checks every model_subsystem on a ship_info is linked into the same model the ship is using, and fails with Error(LOCATION, ...) naming both polymodels if not. This is the canonical post-condition for both ship_copy_subsystem_fixup() and model_load() (which is supposed to link subsystems via do_new_subsystem()).

  • ship_create(): call verify_ship_subsystems_linked() after ship_copy_subsystem_fixup() so that any failure to link a subsystem is surfaced with full context (ship name, subsystem name, both model filenames) instead of propagating to a generic error in subsys_set() much later. This closes a long-standing asymmetry where ship_page_in() had a guard (added by zookeeper in 2015) but ship_create() did not.

  • ship_page_in(): three post-fixup verifications were gated by #ifndef NDEBUG with Warning() or Assertion() calls, both of which are no-ops in Release builds. Replace each with a verify_ship_subsystems_linked() call so they fire in Release as well.

…cp-fs2open#3147)

Fixes the long-standing intermittent bug where the GTD Orion#Repulse in
Silent Threat: Reborn's "Forced Hand" mission spawns with all subsystems
unlinked, leaving the ship untargetable and undisableable.  The same root
cause has been reported since at least 2015 (HLP topic 89403) and was
provisionally closed in scp-fs2open#3147 as fixed by scp-fs2open#3858, but scp-fs2open#3858 was an unrelated
parse_ship_values memory leak fix and never actually addressed this
behavior.  Recently the underlying check at ship.cpp ~8043 was promoted
from Warning to Error (scp-fs2open#5591), which is why Release players are now
hitting a hard error dialog instead of just seeing missing subsystems.

Root cause
----------
model_unload() walks Ship_info and resets si.model_num = -1 on every
ship_info that referenced the model, but does not touch
si.subsystems[k].model_num.  Subsystem entries are left pointing at the
freed model id.  When a subsequent mission reloads the same pof under a
new id, ship variants that share the model rely on
ship_copy_subsystem_fixup() to re-link their subsystem arrays, but the
fixup matches by name via model_copy_subsystems() and any unmatched
destination subsystem stays at -1.  The mismatch case in
model_copy_subsystems() was guarded only by Int3(), which is a no-op in
Release builds, so the failure was completely silent until subsys_set()
errored out much later with no useful context.

The bug is intermittent because it depends on three independent ordering
factors: which prior mission was last played, which Orion variant that
mission used, and the iteration order of Ship_info during the next
mission's ship_page_in().

Changes
-------
* model_unload(): when clearing si.model_num for an unloaded model, also
  clear every si.subsystems[k].model_num that pointed at the same id.
  This is the structural fix and eliminates the entire class of stale
  subsystem pointers surviving across mission boundaries.

* model_copy_subsystems(): replace the silent Int3() in the name-mismatch
  branch with Error(LOCATION, ...) naming the unmatched subsystem.  This
  surfaces a failure mode that has been invisible in Release builds for
  over a decade and gives modders an actionable diagnostic when sibling
  ship classes that share a model have inconsistent subsystem names.

* ship.cpp: introduce verify_ship_subsystems_linked() helper that checks
  every model_subsystem on a ship_info is linked into the same model the
  ship is using, and fails with Error(LOCATION, ...) naming both
  polymodels if not.  This is the canonical post-condition for both
  ship_copy_subsystem_fixup() and model_load() (which is supposed to
  link subsystems via do_new_subsystem()).

* ship_create(): call verify_ship_subsystems_linked() after
  ship_copy_subsystem_fixup() so that any failure to link a subsystem
  is surfaced with full context (ship name, subsystem name, both model
  filenames) instead of propagating to a generic error in subsys_set()
  much later.  This closes a long-standing asymmetry where ship_page_in()
  had a guard (added by zookeeper in 2015) but ship_create() did not.

* ship_page_in(): three post-fixup verifications were gated by
  #ifndef NDEBUG with Warning() or Assertion() calls, both of which are
  no-ops in Release builds.  Replace each with a verify_ship_subsystems_linked()
  call so they fire in Release as well.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Goober5000 Goober5000 added fix A fix for bugs, not-a-bugs, and/or regressions. models Issues or features having to do with model data (like animations or geometry) labels Apr 11, 2026
@Goober5000 Goober5000 requested a review from Baezon April 11, 2026 06:12
Copy link
Copy Markdown
Member

@Baezon Baezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model_unload changes are the real smoking gun here, but the others should help any similar issues from happening as well.

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

Labels

fix A fix for bugs, not-a-bugs, and/or regressions. models Issues or features having to do with model data (like animations or geometry)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mysterious missing subsystems on the Repulse in ST:R's Forced Hand

2 participants