mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -32,6 +32,11 @@ struct ModOption {
|
||||
int64_t max_value = 0;
|
||||
int64_t step = 1;
|
||||
std::vector<ModChoice> choices;
|
||||
/* Optional id of a BOOLEAN option in the same feature that overrides this
|
||||
* one. While that option is true this control is inert: the launcher greys
|
||||
* it out and plugins ignore its value. Models "tick Instant and the speed
|
||||
* box stops mattering" without inventing a second widget type. */
|
||||
std::string disabled_by;
|
||||
};
|
||||
|
||||
struct ModFeature {
|
||||
|
||||
@@ -123,10 +123,20 @@ enum {
|
||||
int psx_mod_set_frame_interpolation_blend(uint32_t blend_mode);
|
||||
int psx_mod_set_auto_skip_fmv(int enabled);
|
||||
|
||||
/*
|
||||
* Upper bounds for the two loading-speed knobs below. Both are generous on
|
||||
* purpose: games surface them to players as free-form integers, and neither
|
||||
* can corrupt guest state (see the notes on each setter). They exist to reject
|
||||
* nonsense, not to curate a list of "blessed" speeds.
|
||||
*/
|
||||
#define PSX_MOD_LOAD_ACCEL_MAX 1024u
|
||||
#define PSX_MOD_DISC_SPEED_MAX 1024u
|
||||
|
||||
/*
|
||||
* Accelerate only the wall-clock pacing of sustained non-XA data loads while
|
||||
* preserving every guest VBlank, CD deadline, interrupt, and callback.
|
||||
* wall_clock_multiplier accepts 2..16, or zero for uncapped host speed.
|
||||
* wall_clock_multiplier accepts 1..PSX_MOD_LOAD_ACCEL_MAX, or zero for
|
||||
* uncapped host speed (1 is a no-op, i.e. authentic pacing).
|
||||
* release_frames controls how many guest frames acceleration may remain active
|
||||
* after the load predicate clears; zero is the precise/speedrun-safe policy.
|
||||
*/
|
||||
@@ -134,10 +144,15 @@ int psx_mod_set_load_acceleration(uint32_t wall_clock_multiplier,
|
||||
uint32_t release_frames);
|
||||
|
||||
/*
|
||||
* Select guest-visible CD timing for a game-owned loading feature. divisor is
|
||||
* 2 or 4, or zero for the bounded "instant" scheduler; instant_max_per_frame
|
||||
* is used only with divisor zero. Unlike host load acceleration, this changes
|
||||
* when the emulated game receives CD interrupts and can expose timing bugs.
|
||||
* Select guest-visible CD timing for a game-owned loading feature. divisor
|
||||
* divides the emulated sector delay, so it IS the speed multiplier: 1 is
|
||||
* authentic timing, higher is faster, up to PSX_MOD_DISC_SPEED_MAX. Zero
|
||||
* selects the bounded "instant" scheduler, and instant_max_per_frame (1..256)
|
||||
* applies only in that case. cdrom.c floors the divided delay at
|
||||
* CDROM_MIN_DELAY and leaves XA streaming at authentic timing, so no value
|
||||
* here can produce a zero-delay storm or speed up FMV audio. Unlike host load
|
||||
* acceleration this changes WHEN the guest receives CD interrupts and can
|
||||
* expose game timing bugs, which is why it is a separate, opt-in knob.
|
||||
*/
|
||||
int psx_mod_set_disc_speed(uint32_t divisor,
|
||||
uint32_t instant_max_per_frame);
|
||||
|
||||
+14
-4
@@ -801,9 +801,12 @@ extern "C" int psx_mod_set_auto_skip_fmv(int enabled) {
|
||||
|
||||
extern "C" int psx_mod_set_load_acceleration(
|
||||
uint32_t wall_clock_multiplier, uint32_t release_frames) {
|
||||
if ((wall_clock_multiplier != 0 &&
|
||||
(wall_clock_multiplier < 2 || wall_clock_multiplier > 16)) ||
|
||||
release_frames > 60) {
|
||||
/* Host pacing only changes how fast wall-clock time is fed to a load; every
|
||||
* guest frame, CD deadline, interrupt and callback still happens, so a high
|
||||
* multiplier cannot desync the guest -- it just approaches "as fast as the
|
||||
* host can". Ceiling raised from 16 for the same reason as disc speed:
|
||||
* players set this as an integer and want to find their own limit. */
|
||||
if (wall_clock_multiplier > PSX_MOD_LOAD_ACCEL_MAX || release_frames > 60) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: mod rejected load acceleration "
|
||||
"(multiplier %u, release frames %u)\n",
|
||||
@@ -817,7 +820,14 @@ extern "C" int psx_mod_set_load_acceleration(
|
||||
|
||||
extern "C" int psx_mod_set_disc_speed(
|
||||
uint32_t divisor, uint32_t instant_max_per_frame) {
|
||||
if ((divisor != 0 && divisor != 2 && divisor != 4) ||
|
||||
/* Any positive divisor is arithmetically safe: cdrom.c divides the sector
|
||||
* delay by it and floors the result at CDROM_MIN_DELAY, and XA streaming
|
||||
* keeps authentic timing regardless. The old 2-or-4 allowlist was policy,
|
||||
* not a hardware constraint, and it blocked players from finding the
|
||||
* highest speed their game tolerates (GH TombaRecomp#5). Games expose the
|
||||
* value as a player-set integer, so accept the full sane range and let the
|
||||
* mod's own description carry the risk warning. */
|
||||
if (divisor > PSX_MOD_DISC_SPEED_MAX ||
|
||||
(divisor == 0 &&
|
||||
(instant_max_per_frame < 1 || instant_max_per_frame > 256))) {
|
||||
std::fprintf(stderr,
|
||||
|
||||
@@ -1514,6 +1514,8 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
option.description =
|
||||
v.contains("description") ? toml::find<std::string>(v, "description") : "";
|
||||
option.group = v.contains("group") ? toml::find<std::string>(v, "group") : "General";
|
||||
option.disabled_by =
|
||||
v.contains("disabled_by") ? toml::find<std::string>(v, "disabled_by") : "";
|
||||
const std::string type = toml::find<std::string>(v, "type");
|
||||
if (!find_feature(out, option.feature_id))
|
||||
throw std::runtime_error("option references unknown feature");
|
||||
@@ -1555,6 +1557,26 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
}
|
||||
out.options.push_back(std::move(option));
|
||||
}
|
||||
/* Resolve disabled_by AFTER the whole list is read, so it may name
|
||||
* an option declared later. A dangling or non-boolean reference is
|
||||
* a manifest bug that would silently leave the control always
|
||||
* enabled, so reject it here rather than at render time. */
|
||||
for (const ModOption& option : out.options) {
|
||||
if (option.disabled_by.empty()) continue;
|
||||
if (option.disabled_by == option.id)
|
||||
throw std::runtime_error("option disabled_by references itself");
|
||||
const auto owner = std::find_if(
|
||||
out.options.begin(), out.options.end(),
|
||||
[&](const ModOption& o) {
|
||||
return o.feature_id == option.feature_id &&
|
||||
o.id == option.disabled_by;
|
||||
});
|
||||
if (owner == out.options.end() ||
|
||||
owner->type != ModOptionType::Boolean)
|
||||
throw std::runtime_error(
|
||||
"option disabled_by must name a boolean option "
|
||||
"in the same feature");
|
||||
}
|
||||
}
|
||||
if (cfg.contains("constraint")) {
|
||||
if (out.format_version < 3)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user