Files
psxrecomp/docs/MOD_PACKAGES.md
T
2026-07-25 21:14:28 -07:00

7.9 KiB

PSXRecomp mod packages and features

A .psxmod is a versioned installation, provenance, and trust boundary. A package may contribute any number of independently configurable features. The launcher presents those features as the primary Mods list; package installation, version selection, and removal are a secondary management view.

Feature identity is always (package_id, feature_id). Enabling one feature never enables, disables, or reconfigures another feature.

The player selects a verified stock BIN/CUE. Resolution produces guarded native operations and sparse disc overlays without rewriting or replacing that stock image.

Feature manifest

format_version = 1
id = "example.localization"
version = "1.2.0"
name = "Example Localization Pack"
author = "Example Author"
description = "Independent title and script features."
license = "MIT"
resolver = "declarative"

[[target]]
game_id = "SLUS-00000"
# Required for disc overlays. Use the digest of the supported stock image.
disc_sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

[[feature]]
id = "title-screen"
name = "Title Screen"
description = "Selects the title-screen artwork."
group = "Localization"
default_enabled = false

[[feature]]
id = "retranslation"
name = "Retranslation"
description = "Uses the revised English script."
group = "Localization"

[[option]]
feature = "title-screen"
id = "variant"
label = "Title artwork"
type = "choice"
default = "rockman"

[[option.choice]]
value = "mega-man"
label = "Mega Man X6 (USA)"

[[option.choice]]
value = "rockman"
label = "Rockman X6 (Japan)"

[[patch]]
feature = "title-screen"
target = "main_exe"
address = 0x80041234
expected = "2a 00 02 24"
replace = "0e 00 02 24"
when = { variant = "rockman" }

[[overlay]]
feature = "retranslation"
target = "disc_raw"
offset = 123456
file = "assets/retranslated-script.bin"
sha256 = "..."
# Optional additional guard over the same range in the stock image.
expected_sha256 = "..."

Every [[option]], [[patch]], and [[overlay]] in a feature-style manifest must name its owning feature. Ambiguous operations are rejected.

Option types are boolean, choice, and bounded integer. Conditions are feature-local: when = { option = "value", ... } requires every listed option to match. The legacy when_option/when_value pair remains accepted for a single condition.

Bounded integer patches

Package format 2 can encode a bounded integer option directly into a guarded write:

format_version = 2

[[feature]]
id = "starting-lives"
name = "Starting Lives"

[[option]]
feature = "starting-lives"
id = "count"
label = "Lives"
type = "integer"
min = 0
max = 99
step = 1
default = 2

[[patch]]
feature = "starting-lives"
target = "main_exe"
address = 0x8001DE64
expected = "02 00 02 24"
replace_from = { option = "count", encoding = "u16le", offset = 0 }

[[patch]]
feature = "starting-lives"
target = "main_exe"
address = 0x8001DE70
expected = "03 00 02 24"
replace_from = { option = "count", encoding = "u16le", offset = 0, addend = 1 }

replace_from and literal replace are mutually exclusive. The referenced option must be a bounded integer owned by the same feature. The initial encodings are u8, u16le, and u32le. offset selects a byte field inside the expected guard and defaults to zero. Generated replacement bytes begin as an exact copy of the expected bytes, then the encoded value replaces only that field. This lets a MIPS immediate, for example, retain a guard and collision claim over its complete instruction. addend is the only supported transform, and the complete declared option range after that addend must fit the unsigned encoding.

There is deliberately no host-endian encoding, signed inference, mask, shift, scale, expression language, or partial-field merge. A package uses multiple guarded [[patch]] entries when the same value has multiple destinations. Generated bytes enter the ordinary pre-boot write plan, collision checks, and fingerprint. A generated value identical to the stock guard is omitted as a no-op, so an enabled stock-valued option does not claim or conflict on bytes it does not change.

Integer values use canonical decimal text. Leading plus signs, redundant leading zeroes, values outside the bounds, and values not aligned to step are rejected.

Native operations

main_exe writes use PSX guest virtual addresses. Expected bytes are checked after the BIOS loads the executable, then the complete write plan is applied before its entry point. Changed executable ranges use the existing dirty-RAM interpreter/native-overlay machinery; untouched functions remain on the static native path.

Small disc_raw and disc_user patches are equal-length guarded writes and may not cross a sector boundary:

  • disc_raw offsets use lba * 2352 + byte_in_sector.
  • disc_user offsets use lba * 2048 + byte_in_sector.

File-backed [[overlay]] operations are intended for large assets and may span any number of sectors. Their paths must remain inside the archive. Payload size and SHA-256 are verified while scanning, but disabled payloads are not retained in memory. Enabled payloads are loaded and reverified during resolution, then indexed by target and LBA before boot. A CD read performs a direct indexed lookup rather than scanning every installed mod.

Feature disc overlays require an exact disc_sha256 on every target entry. expected_sha256 can additionally guard the replaced stock range.

State and migration

mods/state.toml format 2 stores selected package versions separately from per-feature enabled states and values:

format_version = 2

[[package]]
id = "example.localization"
version = "1.2.0"

[[feature]]
package_id = "example.localization"
id = "title-screen"
enabled = true

[feature.values]
variant = "rockman"

State format 1 and package-only manifests remain readable as a migration aid. They appear through one synthetic legacy feature. New packages should use explicit features.

The old derived_disc VCDIFF mechanism is legacy conversion scaffolding only. Feature-style manifests reject it. It is not a product mod primitive, fallback, or image-selection workflow; patched discs may be used offline as parity oracles while converting known mods to native operations.

Resolution and diagnostics

Before boot, the manager:

  1. verifies the selected stock game and revision;
  2. expands only enabled features and their selected options;
  3. orders active packages deterministically by dependencies;
  4. verifies enabled payloads and operation bounds;
  5. collision-checks the complete byte-range plan;
  6. coalesces only truly identical target/range/expected/replacement writes or identical overlays; and
  7. produces a canonical SHA-256 plan fingerprint.

Incompatible overlaps fail before launch. Structured diagnostics identify both (package, feature) owners and the exact contested target range. The launcher marks both feature rows and lets the user decide what to disable. It never silently chooses a winner.

Operation boundaries are not semantic boundaries. Partially overlapping writes compose when both their expected and replacement bytes agree throughout the intersection; partially overlapping overlays compose when their replacement payload bytes agree. A differing byte produces a diagnostic at that exact location. Exact duplicate operations may be coalesced.

Package-level dependencies and conflicts are reserved for actual implementation relationships. Mutually exclusive choices such as US versus Japanese artwork belong inside one feature as option values.

Trusted adapters and archive safety

resolver = "builtin:<id>" selects a resolver statically registered by the game. Packages cannot load arbitrary native code or select arbitrary symbols.

The installer accepts stored or DEFLATE-compressed ZIP entries, validates CRCs, rejects encrypted entries and unsafe or absolute paths, limits archives to 4096 files and 256 MiB expanded size, stages extraction, validates the manifest, and publishes the version atomically.