Skip to content

Commit 2dcf851

Browse files
Goober5000claude
andcommitted
Fix stale subsystem model_num references across mission boundaries (#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 #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. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 219f692 commit 2dcf851

2 files changed

Lines changed: 44 additions & 25 deletions

File tree

code/model/modelread.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,15 @@ void model_unload(int modelnum, int force)
277277
for (auto& si : Ship_info) {
278278
if (pm->id == si.model_num) {
279279
si.model_num = -1;
280+
281+
// also reset any subsystem model_num references that pointed to this model,
282+
// otherwise stale ids can survive across missions and cause subsystems to fail
283+
// to re-link when the model is reloaded with a different id.
284+
for (int k = 0; k < si.n_subsystems; k++) {
285+
if (si.subsystems[k].model_num == pm->id) {
286+
si.subsystems[k].model_num = -1;
287+
}
288+
}
280289
}
281290

282291
if (pm->id == si.cockpit_model_num) {
@@ -592,7 +601,11 @@ void model_copy_subsystems( int n_subsystems, model_subsystem *d_sp, model_subsy
592601
}
593602
}
594603
if ( j == n_subsystems )
595-
Int3(); // get allender -- something is amiss with models
604+
Error(LOCATION, "Subsystem '%s' could not be matched between two ship classes that share a model. "
605+
"The destination ship will be missing this subsystem at runtime. "
606+
"Check that subsystem names are spelled identically on both ship classes, "
607+
"and that any modular table extensions to one ship are mirrored on the other.",
608+
source->subobj_name);
596609

597610
}
598611
}

code/ship/ship.cpp

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7917,6 +7917,30 @@ static void ship_copy_subsystem_fixup(ship_info *sip)
79177917

79187918
}
79197919

7920+
// Verify that every model_subsystem on this ship_info is linked into the same
7921+
// model the ship is using. If any subsystem still points at a different
7922+
// model_num, fail with a diagnostic naming both polymodels. This is the
7923+
// canonical post-condition check for both ship_copy_subsystem_fixup() and
7924+
// model_load() (which is supposed to link subsystems via do_new_subsystem()).
7925+
// context_label disambiguates the call site in the resulting Error text.
7926+
static void verify_ship_subsystems_linked(const ship_info *sip, const char *context_label)
7927+
{
7928+
for (int i = 0; i < sip->n_subsystems; i++) {
7929+
if (sip->subsystems[i].model_num == sip->model_num)
7930+
continue;
7931+
7932+
polymodel *sip_pm = (sip->model_num >= 0) ? model_get(sip->model_num) : nullptr;
7933+
polymodel *subsys_pm = (sip->subsystems[i].model_num >= 0) ? model_get(sip->subsystems[i].model_num) : nullptr;
7934+
Error(LOCATION, "%s, ship '%s' does not have subsystem '%s' linked into the model file, '%s'.\n\n(Ship_info model is '%s' and subsystem model is '%s'.)",
7935+
context_label,
7936+
sip->name,
7937+
sip->subsystems[i].subobj_name,
7938+
sip->pof_file,
7939+
(sip_pm != nullptr) ? sip_pm->filename : "NULL",
7940+
(subsys_pm != nullptr) ? subsys_pm->filename : "NULL");
7941+
}
7942+
}
7943+
79207944
// as with object, don't set next and prev to NULL because they keep the object on the free and used lists
79217945
void ship_subsys::clear()
79227946
{
@@ -11504,6 +11528,8 @@ int ship_create(matrix* orient, vec3d* pos, int ship_type, const char* ship_name
1150411528
polymodel *pm = model_get(sip->model_num);
1150511529

1150611530
ship_copy_subsystem_fixup(sip);
11531+
verify_ship_subsystems_linked(sip, "After ship_copy_subsystem_fixup in ship_create");
11532+
1150711533
show_ship_subsys_count();
1150811534

1150911535
if ( sip->num_detail_levels != pm->n_detail_levels )
@@ -19139,42 +19165,22 @@ void ship_page_in()
1913919165
sip->subsystems[j].model_num = -1;
1914019166

1914119167
ship_copy_subsystem_fixup(&(*sip));
19142-
19143-
#ifndef NDEBUG
19144-
for (j = 0; j < sip->n_subsystems; j++) {
19145-
if (sip->subsystems[j].model_num != sip->model_num) {
19146-
polymodel *sip_pm = (sip->model_num >= 0) ? model_get(sip->model_num) : NULL;
19147-
polymodel *subsys_pm = (sip->subsystems[j].model_num >= 0) ? model_get(sip->subsystems[j].model_num) : NULL;
19148-
Warning(LOCATION, "After ship_copy_subsystem_fixup, ship '%s' does not have subsystem '%s' linked into the model file, '%s'.\n\n(Ship_info model is '%s' and subsystem model is '%s'.)", sip->name, sip->subsystems[j].subobj_name, sip->pof_file, (sip_pm != NULL) ? sip_pm->filename : "NULL", (subsys_pm != NULL) ? subsys_pm->filename : "NULL");
19149-
}
19150-
}
19151-
#endif
19168+
verify_ship_subsystems_linked(&(*sip), "After ship_copy_subsystem_fixup in ship_page_in");
1915219169
} else {
1915319170
// Just to be safe (I mean to check that my code works...)
1915419171
Assert( sip->model_num >= 0 );
1915519172
Assert( sip->model_num == model_previously_loaded );
1915619173

19157-
#ifndef NDEBUG
19158-
for (j = 0; j < sip->n_subsystems; j++) {
19159-
if (sip->subsystems[j].model_num != sip->model_num) {
19160-
polymodel *sip_pm = (sip->model_num >= 0) ? model_get(sip->model_num) : NULL;
19161-
polymodel *subsys_pm = (sip->subsystems[j].model_num >= 0) ? model_get(sip->subsystems[j].model_num) : NULL;
19162-
Warning(LOCATION, "Without ship_copy_subsystem_fixup, ship '%s' does not have subsystem '%s' linked into the model file, '%s'.\n\n(Ship_info model is '%s' and subsystem model is '%s'.)", sip->name, sip->subsystems[j].subobj_name, sip->pof_file, (sip_pm != NULL) ? sip_pm->filename : "NULL", (subsys_pm != NULL) ? subsys_pm->filename : "NULL");
19163-
}
19164-
}
19165-
#endif
19174+
verify_ship_subsystems_linked(&(*sip), "Without ship_copy_subsystem_fixup in ship_page_in");
1916619175
}
1916719176
} else {
1916819177
// Model not loaded, so load it
1916919178
sip->model_num = model_load(sip->pof_file, &*sip);
1917019179

1917119180
Assert( sip->model_num >= 0 );
1917219181

19173-
#ifndef NDEBUG
19174-
// Verify that all the subsystem model numbers are updated
19175-
for (j = 0; j < sip->n_subsystems; j++)
19176-
Assertion( sip->subsystems[j].model_num == sip->model_num, "Model reference for subsystem %s (model num: %d) on model %s (model num: %d) is invalid.\n", sip->subsystems[j].name, sip->subsystems[j].model_num, sip->pof_file, sip->model_num ); // JAS
19177-
#endif
19182+
// Verify that all the subsystem model numbers were updated by model_load
19183+
verify_ship_subsystems_linked(&(*sip), "After model_load in ship_page_in");
1917819184
}
1917919185

1918019186
// more weapon marking, the weapon info in Ship_info[] is the default

0 commit comments

Comments
 (0)