feat(mods): let a trusted plugin read its own option values

The manifest schema already carries typed, validated, launcher-rendered,
persisted options ([[option]] boolean/choice/integer), and the launcher
provider already reads them. A trusted activation plugin could not: the
callback takes no arguments, so a plugin was only ever an on/off switch.

That forced a parameterised feature to be modelled as one feature per
value, and `constraint` only expresses ordered_integer WITHIN a feature, so
those pseudo-features could not even be made mutually exclusive. A game
wanting "Widescreen: 16:9 / 21:9" or "Interpolation: 90/120/144/165/240"
had no way to express it as one entry.

psx_mod_option_value(package, feature, option, out, out_size) closes the
gap against the value the player actually committed. It returns 0 with an
empty string when the plan is not committed or the ids do not resolve, so a
caller applies its own default rather than reading "" as a selection. Ids
are explicit because registration is by plugin id alone and the callback
carries no package/feature context.

Also in this commit, from the same investigation:

* Sustained overlay-autocompile failure is now LOUD. After 3 consecutive
  failed runs the runtime prints once to stdout: what failed, the exit
  code, that overlays are staying interpreted and frame times will be far
  worse than the build is capable of, what to check, and the compiler's own
  output tail. Previously the only trace was a `fails` counter reachable
  over TCP, so a wholly-interpreted title presented as "why is this slow"
  rather than as a message. Threshold is 3, not 1, because isolated
  failures (a shard racing a capture rewrite, a transient lock) are
  ordinary and self-heal. Measured cost of the silent case on Tomba!:
  p50 frame period 105ms, vs 3.1ms once shards existed.

* The autocompile child's working directory is printed at startup beside
  cache and captures. Every relative path in overlay_autocompile_cmd
  resolves against it and it comes from game.toml's detected project root,
  so a game.toml staged outside the real root silently repoints the entire
  compile command with no other symptom.

* A build tree whose cache still carries recomp-ui's OFF for
  RECOMP_UI_ENABLE_MODS now says so at configure time. 610234a's seed only
  fires on a fresh cache, and a stale OFF is indistinguishable from a
  deliberate one, so such trees kept producing Mods-less builds across
  reconfigures - the same "reads as a stale build" failure one level up,
  and invisible. The explicit-OFF contract is deliberate so this does not
  override it; it makes the state audible.

Runtime ctest 16/16. Warning verified to fire on a tree configured with
-DRECOMP_UI_ENABLE_MODS=OFF and to stay silent on a fresh configure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-07-27 17:33:44 -07:00
parent 5ec343ab4f
commit eb262c4b23
5 changed files with 129 additions and 2 deletions
+22
View File
@@ -979,6 +979,28 @@ extern "C" int psx_mod_game_started(void) {
return fntrace_is_game_started();
}
extern "C" int psx_mod_option_value(const char* package_id,
const char* feature_id,
const char* option_id,
char* out, uint32_t out_size) {
using namespace PSXRecompV4;
if (out && out_size) out[0] = '\0';
if (!package_id || !feature_id || !option_id || !out || out_size == 0)
return 0;
RuntimeMods& s = state();
/* Activation runs after the final plan commit, so a committed plan is the
* precondition for a meaningful answer. Without one there is no selection
* to read and the caller must fall back to its own default rather than
* treat an empty string as a value. */
if (!s.initialized || !s.plan.ok) return 0;
const std::string value = s.manager.feature_option_value(
package_id, feature_id, option_id);
if (value.empty()) return 0;
if (value.size() + 1 > (size_t)out_size) return 0;
std::memcpy(out, value.c_str(), value.size() + 1);
return 1;
}
extern "C" uint8_t psx_mod_read_byte(uint32_t address) {
return psx_read_byte(address);
}