mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
670 lines
27 KiB
C++
670 lines
27 KiB
C++
#pragma once
|
|
|
|
#include "mod_plugins.h"
|
|
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <vector>
|
|
|
|
namespace PSXRecompV4 {
|
|
|
|
/* How finished a feature is, and therefore who is allowed to see it.
|
|
*
|
|
* Stable ships and is offered to everyone. Experimental ships, badged, and is
|
|
* default-off: it works, but has not been validated on this title. Developer
|
|
* does not ship at all -- contributors reach those features by cloning the
|
|
* repo and building locally.
|
|
*
|
|
* This lives on the FEATURE, not the package. A package is the trust and
|
|
* installation boundary; how finished one of its features is has nothing to do
|
|
* with that, and tying the two forced a whole catalog down to the maturity of
|
|
* its least finished feature. A package may still declare a channel, which its
|
|
* features inherit unless they state their own. */
|
|
enum class ModChannel {
|
|
Stable = 0,
|
|
Experimental = 1,
|
|
Developer = 2,
|
|
};
|
|
|
|
/* Whether this build may see developer-channel features. A local build may; a
|
|
* CI/release build may not, which is what "developer does not ship" means in
|
|
* practice. runtime.cmake defines PSX_MOD_DEVELOPER_CHANNEL for local builds. */
|
|
#if defined(PSX_MOD_DEVELOPER_CHANNEL) && PSX_MOD_DEVELOPER_CHANNEL
|
|
inline constexpr bool kDeveloperChannelDefault = true;
|
|
#else
|
|
inline constexpr bool kDeveloperChannelDefault = false;
|
|
#endif
|
|
|
|
const char* mod_channel_name(ModChannel channel);
|
|
bool mod_channel_from_name(const std::string& name, ModChannel& out);
|
|
|
|
enum class ModOptionType {
|
|
Boolean,
|
|
Choice,
|
|
Integer,
|
|
};
|
|
|
|
struct ModChoice {
|
|
std::string value;
|
|
std::string label;
|
|
};
|
|
|
|
struct ModOption {
|
|
std::string feature_id;
|
|
std::string id;
|
|
std::string label;
|
|
std::string description;
|
|
std::string group;
|
|
ModOptionType type = ModOptionType::Boolean;
|
|
std::string default_value;
|
|
int64_t min_value = 0;
|
|
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 {
|
|
std::string id;
|
|
std::string name;
|
|
std::string author;
|
|
std::string description;
|
|
std::string group = "General";
|
|
ModChannel channel = ModChannel::Stable;
|
|
bool default_enabled = false;
|
|
bool hidden = false;
|
|
bool legacy = false;
|
|
};
|
|
|
|
enum class ModConstraintKind {
|
|
OrderedInteger,
|
|
RequiresFeature,
|
|
};
|
|
|
|
enum class ModConstraintDirection {
|
|
Nondecreasing,
|
|
Nonincreasing,
|
|
};
|
|
|
|
struct ModConstraint {
|
|
std::string feature_id;
|
|
ModConstraintKind kind = ModConstraintKind::OrderedInteger;
|
|
ModConstraintDirection direction =
|
|
ModConstraintDirection::Nondecreasing;
|
|
std::vector<std::string> options;
|
|
std::string required_feature_id;
|
|
std::string required_option_id;
|
|
std::string required_value;
|
|
};
|
|
|
|
struct ModRequirement {
|
|
std::string id;
|
|
std::string version;
|
|
};
|
|
|
|
/* A feature of this package that needs a feature of ANOTHER package while a
|
|
* condition on its own options holds (manifest [[requirement]], format 7).
|
|
*
|
|
* Package-level [[dependency]] is unconditional, and an in-package
|
|
* requires_feature constraint cannot name another package. This is the
|
|
* missing shape: "Extras = Full needs psx.enhancement.8mb-ram/8mb-ram". The
|
|
* required feature is DERIVED for the session -- resolve() activates it even
|
|
* when it is hidden or disabled in state.toml -- and is never written to
|
|
* state.toml, because the player did not choose it. A requirement that cannot
|
|
* be met (package absent, excluded, wrong version, feature missing) fails the
|
|
* plan loudly; it never runs the requiring selection without it. */
|
|
struct ModFeatureRequirement {
|
|
std::string feature_id; /* the requiring feature (owner) */
|
|
std::string package_id; /* the package that provides it */
|
|
std::string version = "*"; /* same range syntax as [[dependency]] */
|
|
std::string required_feature_id; /* feature of package_id to activate */
|
|
std::map<std::string, std::string> when; /* owner's option conditions */
|
|
};
|
|
|
|
struct ModTarget {
|
|
std::string game_id;
|
|
std::string exe_sha256;
|
|
std::string disc_sha256;
|
|
};
|
|
|
|
enum class ModPatchTarget {
|
|
MainExe,
|
|
DiscRaw,
|
|
DiscUser,
|
|
};
|
|
|
|
enum class ModValueEncoding {
|
|
U8,
|
|
U16LE,
|
|
U32LE,
|
|
MipsLuiOriU32,
|
|
};
|
|
|
|
enum class ModIntegerPredicateOp {
|
|
Equal,
|
|
NotEqual,
|
|
Less,
|
|
LessEqual,
|
|
Greater,
|
|
GreaterEqual,
|
|
};
|
|
|
|
struct ModIntegerPredicate {
|
|
bool present = false;
|
|
std::string option;
|
|
ModIntegerPredicateOp op = ModIntegerPredicateOp::Equal;
|
|
int64_t value = 0;
|
|
};
|
|
|
|
struct ModPatchField {
|
|
uint64_t offset = 0;
|
|
std::vector<uint8_t> replacement;
|
|
std::string replace_from_option;
|
|
ModValueEncoding replace_encoding = ModValueEncoding::U8;
|
|
int64_t replace_addend = 0;
|
|
};
|
|
|
|
struct ModPatch {
|
|
std::string feature_id;
|
|
ModPatchTarget target = ModPatchTarget::MainExe;
|
|
uint64_t location = 0; /* guest address or canonical disc-stream byte offset */
|
|
std::vector<uint8_t> expected;
|
|
std::vector<uint8_t> replacement;
|
|
std::string replace_from_option;
|
|
ModValueEncoding replace_encoding = ModValueEncoding::U8;
|
|
uint64_t replace_offset = 0;
|
|
int64_t replace_addend = 0;
|
|
bool replace_omit_when_default = false;
|
|
std::string when_option;
|
|
std::string when_value;
|
|
std::map<std::string, std::string> when;
|
|
ModIntegerPredicate when_integer;
|
|
std::vector<ModPatchField> fields;
|
|
int64_t order = 0;
|
|
};
|
|
|
|
struct ModOverlay {
|
|
std::string feature_id;
|
|
ModPatchTarget target = ModPatchTarget::DiscRaw;
|
|
uint64_t location = 0;
|
|
std::filesystem::path file;
|
|
// Format 10: verified same-feature derived resource instead of a package file.
|
|
std::string resource_id;
|
|
uint64_t resource_offset = 0;
|
|
std::string sha256;
|
|
std::string expected_sha256;
|
|
uint64_t size = 0;
|
|
std::map<std::string, std::string> when;
|
|
struct FeaturePredicate {
|
|
bool present = false;
|
|
std::string package_id;
|
|
std::string feature_id;
|
|
bool enabled = true;
|
|
} when_feature;
|
|
int64_t order = 0;
|
|
};
|
|
|
|
struct ModPlugin {
|
|
std::string feature_id;
|
|
std::string id;
|
|
std::map<std::string, std::string> when;
|
|
int64_t order = 0;
|
|
/* [[plugin]] netplay = "local_view": the plugin only changes what this
|
|
* player sees. In a netplay match it stays on, per player, but its hooks
|
|
* run only inside the sandboxed own-view render
|
|
* (psx_mod_render_local_view), never in the shared simulation. */
|
|
bool netplay_local_view = false;
|
|
/* netplay = "input": the plugin only transforms this player's own pad
|
|
* before it is staged (psx_mod_set_pad_transform). Online it stays on per
|
|
* player: the post-transform pad is what every peer simulates. */
|
|
bool netplay_input = false;
|
|
/* netplay = "host_output": the plugin only reads guest state and drives
|
|
* host output (rumble, haptics). Online it stays on per player and its
|
|
* hooks run in the shared simulation, but every guest write it attempts
|
|
* is refused there, so it can never diverge the peers. */
|
|
bool netplay_host_output = false;
|
|
};
|
|
|
|
struct ModResource {
|
|
std::string feature_id;
|
|
std::string id;
|
|
std::string label;
|
|
std::string description;
|
|
std::string file_patterns;
|
|
std::string file_description;
|
|
std::string format = "file";
|
|
bool required = false;
|
|
uint64_t size = 0;
|
|
std::string sha256;
|
|
// Source inputs are launcher configuration, not guest resources. A shared
|
|
// key stores one path across packages; derived resources never get pickers.
|
|
std::string shared_source;
|
|
bool input_only = false;
|
|
bool hidden = false;
|
|
// Package-relative path used when the player has not selected one (for
|
|
// example a pack folder the package ships). A selection always wins.
|
|
std::string default_path;
|
|
};
|
|
|
|
struct ModDerivedDisc {
|
|
std::string kind = "vcdiff";
|
|
std::filesystem::path patch;
|
|
std::string patch_sha256;
|
|
uint64_t output_size = 0;
|
|
std::string output_sha256;
|
|
std::string when_option;
|
|
std::string when_value;
|
|
std::map<std::string, std::string> when;
|
|
};
|
|
|
|
struct ModAuthorLink {
|
|
std::string name;
|
|
std::string url;
|
|
};
|
|
|
|
/* Where a package's files came from, and therefore who owns them on disk.
|
|
*
|
|
* Bundled packages are build output: the framework and the title stage them
|
|
* into <exe>/mods/bundled, which every build wipes and re-stages. Nothing the
|
|
* player owns may live there. Installed packages are written by the launcher
|
|
* into <exe>/mods/installed and are never touched by a build.
|
|
*
|
|
* Before this split there was one <exe>/mods/packages tree serving both roles,
|
|
* so a rebuild deleted every mod the player had installed. */
|
|
enum class ModPackageOrigin {
|
|
Bundled,
|
|
Installed,
|
|
};
|
|
|
|
struct ModPackage {
|
|
uint32_t format_version = 0;
|
|
std::string id;
|
|
std::string version;
|
|
std::string name;
|
|
std::string author;
|
|
std::vector<ModAuthorLink> author_links;
|
|
std::string description;
|
|
std::string license;
|
|
std::string source_name;
|
|
std::string source_url;
|
|
std::string resolver = "declarative";
|
|
std::string prepare;
|
|
std::string save_compatibility = "shared";
|
|
/* Default channel for features that do not declare their own. */
|
|
ModChannel channel = ModChannel::Stable;
|
|
ModPackageOrigin origin = ModPackageOrigin::Installed;
|
|
/* Set when an installed package of the same id shadows a bundled one. */
|
|
bool shadows_bundled = false;
|
|
std::filesystem::path root;
|
|
std::vector<ModTarget> targets;
|
|
std::vector<ModRequirement> dependencies;
|
|
std::vector<std::string> conflicts;
|
|
std::vector<ModFeature> features;
|
|
std::vector<ModOption> options;
|
|
std::vector<ModConstraint> constraints;
|
|
std::vector<ModFeatureRequirement> requirements;
|
|
std::vector<ModPatch> patches;
|
|
std::vector<ModOverlay> overlays;
|
|
std::vector<ModPlugin> plugins;
|
|
std::vector<ModResource> resources;
|
|
std::vector<ModDerivedDisc> derived_discs;
|
|
};
|
|
|
|
// False requests cancellation. Providers report between bounded work units
|
|
// and before publishing outputs; no UI code runs on the preparation worker.
|
|
using ModPrepareProgress = std::function<bool(float, const std::string&)>;
|
|
struct ModPrepareContext {
|
|
const ModPackage& package;
|
|
std::string feature_id;
|
|
std::filesystem::path disc_path;
|
|
std::filesystem::path cache_root;
|
|
std::filesystem::path application_dir;
|
|
std::map<std::string, std::filesystem::path> inputs;
|
|
ModPrepareProgress progress;
|
|
};
|
|
using ModMediaPreparer = std::function<bool(const ModPrepareContext&,
|
|
std::map<std::string, std::filesystem::path>&, std::string&)>;
|
|
// Implementations are linked trusted code. A manifest cannot execute a command.
|
|
// Normally called synchronously during launch preparation. A title enabling
|
|
// PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE also permits preboot offline preparation
|
|
// on a launcher-owned worker. Every registered callback/service used by that
|
|
// title must then support serialized worker calls without SDL/UI/GL or
|
|
// main-thread affinity. Registration must finish before opening the launcher;
|
|
// activation callbacks still run separately, before renderer initialization.
|
|
bool mod_register_media_preparer(const std::string& id, ModMediaPreparer callback,
|
|
ModMediaPreparer cache_probe = {});
|
|
|
|
struct ModFeatureSelection {
|
|
bool enabled = false;
|
|
bool has_enabled = false;
|
|
std::map<std::string, std::string> values;
|
|
std::map<std::string, std::string> resources;
|
|
};
|
|
|
|
struct ModSelection {
|
|
/* v1 migration state. Feature-style manifests do not use these fields. */
|
|
bool enabled = false;
|
|
std::string version;
|
|
std::map<std::string, std::string> values;
|
|
std::map<std::string, ModFeatureSelection> features;
|
|
};
|
|
|
|
struct ModResolution {
|
|
bool ok = false;
|
|
std::string fingerprint;
|
|
std::vector<const ModPackage*> ordered;
|
|
struct Write {
|
|
struct Field {
|
|
uint64_t offset = 0;
|
|
std::vector<uint8_t> replacement;
|
|
};
|
|
ModPatchTarget target = ModPatchTarget::MainExe;
|
|
uint64_t location = 0;
|
|
std::vector<uint8_t> expected;
|
|
std::vector<uint8_t> replacement;
|
|
std::vector<Field> fields;
|
|
std::string package_id;
|
|
std::string feature_id;
|
|
};
|
|
std::vector<Write> writes;
|
|
struct Overlay {
|
|
ModPatchTarget target = ModPatchTarget::DiscRaw;
|
|
uint64_t location = 0;
|
|
std::vector<uint8_t> payload;
|
|
std::string payload_sha256;
|
|
std::string expected_sha256;
|
|
std::string package_id;
|
|
std::string feature_id;
|
|
};
|
|
std::vector<Overlay> overlays;
|
|
struct DerivedDisc {
|
|
std::string kind;
|
|
std::filesystem::path patch;
|
|
std::string patch_sha256;
|
|
uint64_t output_size = 0;
|
|
std::string output_sha256;
|
|
std::string package_id;
|
|
};
|
|
std::vector<DerivedDisc> derived_discs;
|
|
struct Plugin {
|
|
std::string id;
|
|
std::string package_id;
|
|
std::string feature_id;
|
|
bool netplay_local_view = false;
|
|
bool netplay_input = false;
|
|
bool netplay_host_output = false;
|
|
};
|
|
std::vector<Plugin> plugins;
|
|
struct Resource {
|
|
std::string package_id;
|
|
std::string feature_id;
|
|
std::string id;
|
|
std::filesystem::path path;
|
|
std::string format;
|
|
std::string sha256;
|
|
std::shared_ptr<const std::vector<uint8_t>> bytes;
|
|
};
|
|
std::vector<Resource> resources;
|
|
struct Diagnostic {
|
|
std::string message;
|
|
std::string resource;
|
|
std::string package_id;
|
|
std::string feature_id;
|
|
std::string other_package_id;
|
|
std::string other_feature_id;
|
|
};
|
|
std::vector<Diagnostic> diagnostics;
|
|
/* Features the plan activates because an active [[requirement]] needs
|
|
* them, not because state.toml enables them. One entry per derived
|
|
* feature (the first requirement that activated it). */
|
|
struct ImplicitFeature {
|
|
std::string package_id;
|
|
std::string feature_id;
|
|
std::string required_by_package_id;
|
|
std::string required_by_feature_id;
|
|
};
|
|
std::vector<ImplicitFeature> implicit_features;
|
|
/* The selection the plan was built from: state.toml's selection plus the
|
|
* implicit activations above. Plugins read option values from this, so a
|
|
* committed plan and the values its plugins observe cannot disagree. */
|
|
std::map<std::string, ModSelection> selections;
|
|
std::vector<std::string> errors;
|
|
};
|
|
|
|
struct ModBuiltinResolverContext {
|
|
const std::map<std::string, const ModPackage*>* active_packages = nullptr;
|
|
const std::map<std::string, ModSelection>* selections = nullptr;
|
|
};
|
|
|
|
using ModBuiltinResolver = std::function<bool(
|
|
const ModPackage& package,
|
|
const ModSelection& selection,
|
|
const ModBuiltinResolverContext& context,
|
|
std::vector<ModResolution::Write>& writes,
|
|
std::vector<std::string>& errors)>;
|
|
|
|
class ModPackageManager {
|
|
public:
|
|
explicit ModPackageManager(std::filesystem::path mods_root = {});
|
|
|
|
void set_root(std::filesystem::path mods_root);
|
|
const std::filesystem::path& root() const { return root_; }
|
|
/* Developer-channel features are stripped at scan and install time unless
|
|
* this is set. Defaults to kDeveloperChannelDefault; tests set it
|
|
* explicitly so they exercise both a local and a release build. */
|
|
void set_developer_channel_visible(bool visible) {
|
|
developer_channel_ = visible;
|
|
}
|
|
bool developer_channel_visible() const { return developer_channel_; }
|
|
/* Trusted title policy: hide and deactivate these ids even when an old
|
|
* bundle or an installed copy survives an update. Saved choices stay
|
|
* dormant and files remain untouched. Set before scan(). */
|
|
void set_disabled_packages(std::vector<std::string> ids) { disabled_packages_ = std::move(ids); }
|
|
|
|
/* Build-owned catalog: wiped and re-staged by every build. */
|
|
std::filesystem::path bundled_root() const { return root_ / "bundled"; }
|
|
/* Launcher-owned catalog: a build never touches this tree. */
|
|
std::filesystem::path installed_root() const { return root_ / "installed"; }
|
|
|
|
bool scan(std::string* error = nullptr);
|
|
bool load_state(std::string* error = nullptr);
|
|
bool save_state(std::string* error = nullptr) const;
|
|
bool prepare_resources(const std::string& game_id,
|
|
const std::filesystem::path& disc_path,
|
|
const std::filesystem::path& cache_root,
|
|
std::string* error = nullptr, bool cached_only = false,
|
|
const ModPrepareProgress& progress = {});
|
|
|
|
bool install_archive(const std::filesystem::path& archive,
|
|
std::string* installed_id = nullptr,
|
|
std::string* installed_version = nullptr,
|
|
std::string* error = nullptr);
|
|
bool remove_version(const std::string& id, const std::string& version,
|
|
std::string* error = nullptr);
|
|
|
|
bool set_enabled(const std::string& id, bool enabled, std::string* error = nullptr);
|
|
bool select_version(const std::string& id, const std::string& version,
|
|
std::string* error = nullptr);
|
|
bool set_option(const std::string& id, const std::string& option,
|
|
const std::string& value, std::string* error = nullptr);
|
|
bool set_feature_enabled(const std::string& package_id,
|
|
const std::string& feature_id, bool enabled,
|
|
std::string* error = nullptr);
|
|
bool set_feature_option(const std::string& package_id,
|
|
const std::string& feature_id,
|
|
const std::string& option_id,
|
|
const std::string& value,
|
|
std::string* error = nullptr);
|
|
bool set_feature_resource_path(const std::string& package_id,
|
|
const std::string& feature_id,
|
|
const std::string& resource_id,
|
|
const std::filesystem::path& path,
|
|
std::string* error = nullptr);
|
|
|
|
const std::map<std::string, std::map<std::string, ModPackage>>& packages() const {
|
|
return packages_;
|
|
}
|
|
const std::map<std::string, ModSelection>& selections() const { return selections_; }
|
|
/* Swap the in-memory selection for `next` and return the previous one.
|
|
* Never saved by itself: a session that must not run the player's whole
|
|
* choice (a netplay match) swaps a reduced selection in, resolves, and
|
|
* swaps the player's back before anything can save_state(). */
|
|
std::map<std::string, ModSelection> exchange_selections(
|
|
std::map<std::string, ModSelection> next) {
|
|
selections_.swap(next);
|
|
return next;
|
|
}
|
|
const ModPackage* selected_package(const std::string& id) const;
|
|
const ModFeature* selected_feature(const std::string& package_id,
|
|
const std::string& feature_id) const;
|
|
bool feature_enabled(const std::string& package_id,
|
|
const std::string& feature_id) const;
|
|
std::string feature_option_value(const std::string& package_id,
|
|
const std::string& feature_id,
|
|
const std::string& option_id) const;
|
|
/* Runtime-derived defaults are not player selections and are never saved.
|
|
* relative_path is rooted at this manager's mods root, captured as an
|
|
* absolute path at registration so subsequent cwd changes cannot rebase it.
|
|
* Existing explicit selections (including legacy absolute paths) win. */
|
|
bool set_generated_feature_resource_default(const std::string& package_id,
|
|
const std::string& feature_id, const std::string& resource_id,
|
|
const std::filesystem::path& relative_path, std::string* error = nullptr);
|
|
|
|
std::filesystem::path feature_resource_path(
|
|
const std::string& package_id,
|
|
const std::string& feature_id,
|
|
const std::string& resource_id) const;
|
|
/* True when an active [[requirement]] activates this feature for the
|
|
* session although state.toml does not enable it. feature_enabled() keeps
|
|
* answering for the player's own choice, which is what the launcher shows
|
|
* and what save_state() persists. */
|
|
bool feature_implicitly_enabled(const std::string& package_id,
|
|
const std::string& feature_id) const;
|
|
/* An option value as a resolved plan sees it (plan.selections), for
|
|
* plugins running under that plan. */
|
|
std::string feature_option_value(const ModResolution& plan,
|
|
const std::string& package_id,
|
|
const std::string& feature_id,
|
|
const std::string& option_id) const;
|
|
|
|
// Defer prepared resources only for launcher diagnostics, never execution
|
|
// or fingerprints. A commit prepares first, then resolves without deferral.
|
|
ModResolution resolve(const std::string& game_id,
|
|
const std::string& exe_sha256 = {},
|
|
const std::string& disc_sha256 = {},
|
|
bool defer_prepared_resources = false) const;
|
|
|
|
static bool read_manifest(const std::filesystem::path& path, ModPackage& out,
|
|
std::string* error = nullptr);
|
|
|
|
/* Manifests that failed to parse during the last scan(), as
|
|
* "<path>: <reason>". A package that cannot be read must never vanish
|
|
* silently -- the launcher surfaces these so a mod author sees why their
|
|
* package is absent from the list. */
|
|
const std::vector<std::string>& scan_errors() const { return scan_errors_; }
|
|
|
|
/* Package ids that mods/state.toml carries a selection for but that no
|
|
* catalog root holds -- a package the player removed, a developer-only
|
|
* package a release build stripped, or a framework builtin the title
|
|
* excludes (EXCLUDE_BUILTIN_MODS). Such a selection is DORMANT: resolve()
|
|
* never visits it, so it contributes nothing to the plan, and save_state()
|
|
* keeps it verbatim so the choice returns if the package ever does. The
|
|
* runtime names these once at startup instead of dropping them silently. */
|
|
std::vector<std::string> dormant_selections() const;
|
|
|
|
private:
|
|
/* One-time move of a pre-split <exe>/mods/packages tree into the two
|
|
* owned roots. Anything the build also staged into bundled/ is dropped;
|
|
* everything else is the player's and moves to installed/. */
|
|
void migrate_legacy_root();
|
|
bool scan_root(const std::filesystem::path& packages_root,
|
|
ModPackageOrigin origin, std::string* error);
|
|
/* selections_ plus every feature an active [[requirement]] derives,
|
|
* iterated to a fixed point (a derived feature may carry requirements of
|
|
* its own). Unmet requirements are reported through errors/diagnostics.
|
|
* Never touches selections_: derived activations are not player choices. */
|
|
std::map<std::string, ModSelection> effective_selections(
|
|
std::vector<ModResolution::ImplicitFeature>* implicit,
|
|
std::vector<ModResolution::Diagnostic>* diagnostics,
|
|
std::vector<std::string>* errors) const;
|
|
|
|
std::filesystem::path generated_resource_default(const std::string& package_id,
|
|
const std::string& feature_id, const std::string& resource_id) const;
|
|
std::map<std::tuple<std::string, std::string, std::string>, std::filesystem::path>
|
|
generated_resource_defaults_;
|
|
std::filesystem::path root_;
|
|
bool developer_channel_ = kDeveloperChannelDefault;
|
|
std::vector<std::string> scan_errors_;
|
|
std::map<std::string, std::map<std::string, ModPackage>> packages_;
|
|
std::vector<std::string> disabled_packages_;
|
|
std::map<std::string, ModSelection> selections_;
|
|
std::map<std::string, std::filesystem::path> sources_;
|
|
};
|
|
|
|
bool mod_register_builtin_resolver(const std::string& id, ModBuiltinResolver resolver);
|
|
void mod_clear_builtin_resolvers_for_tests();
|
|
bool mod_register_activation_plugin(const std::string& id, void (*callback)(void));
|
|
bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void));
|
|
bool mod_register_savestate_plugin(const std::string& id, void (*callback)(void));
|
|
void mod_invoke_savestate_plugin(const std::string& id);
|
|
/* A function-entry hook is a trusted implementation like the others: manifests
|
|
* select it by id, and it runs only while a resolved plan activates that id. */
|
|
bool mod_register_function_entry_plugin(const std::string& id, uint32_t address,
|
|
PSXModFunctionEntryCallback callback);
|
|
bool mod_register_function_filter_plugin(const std::string& id, uint32_t address,
|
|
PSXModFunctionFilterCallback callback);
|
|
bool mod_register_guest_function_plugin(const std::string& id, uint32_t address,
|
|
PSXModFunctionEntryCallback callback);
|
|
bool mod_plugin_registered(const std::string& id);
|
|
void mod_invoke_activation_plugin(const std::string& id);
|
|
void mod_invoke_vblank_plugin(const std::string& id);
|
|
struct ModFunctionEntryHook {
|
|
uint32_t address = 0;
|
|
PSXModFunctionEntryCallback callback = nullptr;
|
|
PSXModFunctionFilterCallback filter = nullptr;
|
|
};
|
|
/* Hooks one implementation registered, in registration order. mod_runtime
|
|
* flattens these into an address table when the plan's plugins activate. */
|
|
std::vector<ModFunctionEntryHook> mod_function_entry_hooks(const std::string& id);
|
|
std::vector<ModFunctionEntryHook> mod_guest_functions(const std::string& id);
|
|
struct ModInstructionHook {
|
|
uint32_t address, expected;
|
|
PSXModFunctionEntryCallback callback;
|
|
};
|
|
bool mod_register_instruction_plugin(const std::string& id, uint32_t address,
|
|
uint32_t expected, PSXModFunctionEntryCallback callback);
|
|
std::vector<ModInstructionHook> mod_instruction_hooks(const std::string& id);
|
|
void mod_clear_plugins_for_tests();
|
|
|
|
/* Every id a trusted implementation registered (activation, vblank or
|
|
* function-entry), sorted. */
|
|
std::vector<std::string> mod_registered_plugin_ids();
|
|
|
|
/* Registered implementations that no manifest under `manifest_roots` declares
|
|
* as a [[plugin]] id. The resolved plan activates plugins by manifest id only,
|
|
* so such an implementation can never run: a hook registered under a sub-id
|
|
* ("x.widescreen.hud") is silently dead while its package ("x.widescreen")
|
|
* resolves cleanly. Roots are searched recursively for manifest.toml; pass the
|
|
* AUTHORED trees (title mods/preloaded, framework mods/builtin) so developer
|
|
* packages a release catalog strips still count as declarations. Manifests
|
|
* that fail to parse are reported through `errors`. */
|
|
struct ModPluginAudit {
|
|
std::vector<std::string> undeclared; /* registered, never declared */
|
|
std::vector<std::string> declared; /* every manifest [[plugin]] id */
|
|
std::vector<std::string> errors;
|
|
size_t manifests = 0;
|
|
};
|
|
ModPluginAudit mod_audit_registered_plugins(
|
|
const std::vector<std::filesystem::path>& manifest_roots);
|
|
|
|
} // namespace PSXRecompV4
|