mods: let games expose loading speed as a free integer, plus disabled_by

Players asked for CD speeds beyond the curated 2x/4x list and for the two
loading accelerators to stop being one control (GH TombaRecomp#5: a
speedrunner wants shorter loads but NOT the host speed-up, which runs past
the loading screen and breaks a skip).

Three changes, all general:

- psx_mod_set_disc_speed accepted only divisor 2 or 4. That was policy,
  not safety: cdrom.c divides the sector delay and floors the result at
  CDROM_MIN_DELAY, and apply_speed leaves XA streaming at authentic
  timing, so no positive divisor can produce a zero-delay storm or
  pitch-shift FMV audio. Accept 1..PSX_MOD_DISC_SPEED_MAX and let the
  mod's description carry the risk warning.

- psx_mod_set_load_acceleration accepted 2..16. Host pacing cannot desync
  the guest at all (every frame, CD deadline, interrupt and callback still
  happens), so the ceiling was arbitrary. Accept 1..PSX_MOD_LOAD_ACCEL_MAX,
  with 1 meaning authentic and 0 still meaning uncapped.

- New option key disabled_by: an option names a BOOLEAN sibling that
  overrides it, so "tick Instant and the speed box stops mattering" needs
  no second widget type. The parser rejects a dangling, self-referential
  or non-boolean reference rather than leaving a control silently always
  enabled, and the provider resolves the link against the live selection
  so the launcher and the plugins can never disagree about whether a
  control counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-08-04 23:55:25 -07:00
parent 59244b228c
commit f44f56bfdc
5 changed files with 79 additions and 9 deletions
+18
View File
@@ -107,6 +107,22 @@ std::string selected_value(const ModPackage& package, const ModOption& option) {
return option.default_value;
}
/* Resolve the manifest's disabled_by link against the CURRENT selection: the
* named boolean sibling being true makes this option inert. Both the launcher
* (greys the control) and psx_mod_option_value (returns the default instead of
* a stale value) go through this, so the UI and the plugins can never disagree
* about whether a control counts. */
bool option_is_disabled(const ModPackage& package, const ModOption& option) {
if (option.disabled_by.empty()) return false;
for (const ModOption& other : package.options) {
if (other.feature_id != option.feature_id ||
other.id != option.disabled_by)
continue;
return selected_value(package, other) == "true";
}
return false;
}
void build_disc_index(RuntimeMods& s) {
s.raw_disc_index.clear();
s.user_disc_index.clear();
@@ -562,6 +578,7 @@ int provider_option_get(void*, const char* package_id, int index,
out->max_value = option.max_value;
out->step = option.step;
out->choice_count = (int)option.choices.size();
out->disabled = option_is_disabled(*package, option) ? 1 : 0;
return 1;
}
@@ -682,6 +699,7 @@ int provider_feature_option_get(void*, const char* package_id,
out->max_value = option.max_value;
out->step = option.step;
out->choice_count = (int)option.choices.size();
out->disabled = option_is_disabled(*package, option) ? 1 : 0;
return 1;
}