Scope mod option controls to offline or online launcher flows

This commit is contained in:
Matthew Stanley
2026-10-24 13:04:54 -07:00
parent aaf4fb1ba2
commit cd4250438f
7 changed files with 82 additions and 5 deletions
+7
View File
@@ -252,6 +252,13 @@ feature-local: `when = { option = "value", ... }` requires every listed option
to match. The legacy `when_option`/`when_value` pair remains accepted for a
single condition.
Options may declare `display_context = "online"` or `"offline"` to present
their control only in that launcher flow. The default `"any"` preserves
existing visibility. This is presentation metadata: hiding a control does not
clear its value, change feature activation or remove it from the committed
session identity. Entering Netplay reveals online controls; returning to the
offline dashboard restores offline controls.
Format10 also permits these conditions on `[[resource]]`. Only matching
resources appear in the feature's file pickers, preparation inputs/outputs,
and resolved plan. A required resource is required only while its condition
+10
View File
@@ -49,6 +49,8 @@ enum class ModOptionType {
Integer,
};
enum class ModOptionDisplayContext { Any, Offline, Online };
struct ModChoice {
std::string value;
std::string label;
@@ -71,6 +73,14 @@ struct ModOption {
* 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;
/* Presentation only: selections remain part of the committed plan even
* when their control is hidden outside its launcher context. */
ModOptionDisplayContext display_context = ModOptionDisplayContext::Any;
bool visible_in(bool online) const {
return display_context == ModOptionDisplayContext::Any ||
(online ? display_context == ModOptionDisplayContext::Online
: display_context == ModOptionDisplayContext::Offline);
}
};
struct ModFeature {
+2
View File
@@ -53,6 +53,8 @@ bool mod_runtime_netplay_settings_fingerprint(const PsxLobbyMatchCaps& caps,
std::string* error = nullptr);
/* Profile-specific lobby capacity, including an optional last roster seat. */
int mod_runtime_netplay_player_limit(int fallback);
/* Launcher presentation only; never changes selection or simulation policy. */
void mod_runtime_set_launcher_netplay_context(bool online);
/* [netplay] content_negotiation from game.toml. Off (default): clear package
* simulation mods; an executable-owned profile takes precedence when present. */
void mod_runtime_set_netplay_content_negotiation(bool enabled);
+6
View File
@@ -16977,6 +16977,12 @@ namespace {
#endif
gi->memcard_inspect = ae_memcard_inspect;
gi->mods = PSXRecompV4::mod_runtime_launcher_provider();
#if defined(RECOMP_LAUNCHER_HAS_NETPLAY_MODE_POLICY)
PSXRecompV4::mod_runtime_set_launcher_netplay_context(false);
gi->netplay_mode_changed = +[](int online) {
PSXRecompV4::mod_runtime_set_launcher_netplay_context(online != 0);
};
#endif
gi->bios_verify = ae_bios_verify;
/* Launcher window icon: the SAME file psx_apply_window_icon() puts on
* the game window (assets/psxrecomp.png beside the exe, which
+8
View File
@@ -1916,6 +1916,14 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
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 display_context = v.contains("display_context")
? toml::find<std::string>(v, "display_context") : "any";
if (display_context == "online")
option.display_context = ModOptionDisplayContext::Online;
else if (display_context == "offline")
option.display_context = ModOptionDisplayContext::Offline;
else if (display_context != "any")
throw std::runtime_error("option display_context must be any, offline or online");
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");
+20 -5
View File
@@ -148,6 +148,7 @@ struct RuntimeMods {
/* The plan is a netplay match's own-view set: its hooks run only inside
* psx_mod_render_local_view, its vblank/savestate callbacks not at all. */
bool netplay_view_plan = false;
bool launcher_netplay_context = false;
const ModResolution::Plugin* current_plugin = nullptr;
CPUState* current_function_cpu = nullptr;
bool current_function_finished = false;
@@ -960,6 +961,14 @@ int feature_set_option_impl(const char* package_id, const char* feature_id,
}
#if defined(RECOMP_LAUNCHER)
std::vector<const ModOption*> provider_options(const ModPackage& package) {
std::vector<const ModOption*> out;
const bool online = state().launcher_netplay_context || psx_mod_netplay_is_active();
for (const ModOption& option : package.options)
if (option.visible_in(online)) out.push_back(&option);
return out;
}
void copy_text(char* out, size_t capacity, const std::string& value) {
if (!out || capacity == 0) return;
std::snprintf(out, capacity, "%s", value.c_str());
@@ -995,7 +1004,7 @@ int provider_package_get(void*, int index, RecompLauncherCModPackage* out) {
copy_text(out->source_name, sizeof(out->source_name), package->source_name);
copy_text(out->source_url, sizeof(out->source_url), package->source_url);
out->enabled = package_has_enabled_feature(*package);
out->option_count = (int)package->options.size();
out->option_count = (int)provider_options(*package).size();
/* A bundled package is build output. Offering to remove it would succeed
* and then be silently undone by the next build. */
out->removable = !package_in_use(*package) &&
@@ -1007,8 +1016,10 @@ int provider_option_get(void*, const char* package_id, int index,
RecompLauncherCModOption* out) {
if (!package_id || !out || index < 0) return 0;
const ModPackage* package = selected_package(package_id);
if (!package || (size_t)index >= package->options.size()) return 0;
const ModOption& option = package->options[(size_t)index];
if (!package) return 0;
const auto options = provider_options(*package);
if ((size_t)index >= options.size()) return 0;
const ModOption& option = *options[(size_t)index];
std::memset(out, 0, sizeof(*out));
copy_text(out->id, sizeof(out->id), option.id);
copy_text(out->label, sizeof(out->label), option.label);
@@ -1065,8 +1076,8 @@ bool provider_feature_at(int index, const ModPackage*& package,
std::vector<const ModOption*> provider_feature_options(
const ModPackage& package, const std::string& feature_id) {
std::vector<const ModOption*> out;
for (const ModOption& option : package.options)
if (option.feature_id == feature_id) out.push_back(&option);
for (const ModOption* option : provider_options(package))
if (option->feature_id == feature_id) out.push_back(option);
return out;
}
@@ -1483,6 +1494,10 @@ int mod_runtime_netplay_player_limit(int fallback) {
return count?(int)count:fallback;
}
void mod_runtime_set_launcher_netplay_context(bool online) {
state().launcher_netplay_context = online;
}
static std::optional<ModPackageManager> offline_manager_before_netplay;
static bool netplay_content_negotiation_enabled = false;
+29
View File
@@ -2569,6 +2569,35 @@ int main() {
check(!ModPackageManager::read_manifest(a,malformed,&error),"duplicate default roster rejected");
}
{
const fs::path path = root / "option-display-context.toml";
for (const char* context : {"any", "offline", "online", "invalid"}) {
write_text(path, manifest("context.mod", "1.0.0",
"[[option]]\nid = \"view\"\nlabel = \"View\"\n"
"type = \"boolean\"\ndefault = \"true\"\n"
"display_context = \"" + std::string(context) + "\"\n"));
ModPackage package;
const bool valid = ModPackageManager::read_manifest(path, package, &error);
if (std::string(context) == "invalid") {
check(!valid, "unknown option display context must be rejected");
continue;
}
check(valid && package.options.size() == 1,
"option display context must parse");
if (!valid || package.options.empty()) continue;
const ModOption& option = package.options.front();
check(option.visible_in(false) == (std::string(context) != "online"),
"offline option visibility must respect display context");
check(option.visible_in(true) == (std::string(context) != "offline"),
"online option visibility must respect display context");
check(option.default_value == "true",
"presentation context must not discard an option selection");
}
ModOption legacy;
check(legacy.visible_in(false) && legacy.visible_in(true),
"unspecified display context must preserve legacy visibility");
}
fs::remove_all(root, ec);
if (failures) {
std::cerr << failures << " mod package test(s) failed\n";