mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Scope mod option controls to offline or online launcher flows
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user