This commit is contained in:
bryanthaboi
2026-08-21 14:03:27 -04:00
19 changed files with 1593 additions and 15 deletions
+3 -2
View File
@@ -24,7 +24,7 @@ The short version, for an author deciding what to write:
merged.** The write is taken, dropped, and named once per mod in the same
error feed the mod manager shows -- in both directions, so a Red boot writing
to `decorations` is told exactly as a Gold boot writing to `map_scripts` is.
- **40 event names and 43 hook names have a call site in both generations**, so
- **40 event names and 44 hook names have a call site in both generations**, so
one subscription serves both games. `tests/engine/gate_gen2_mod_api.lua`
reads those names back out of the source and fails if a site is renamed or
deleted on either side, and fails again if a new shared site appears without
@@ -539,7 +539,8 @@ gains a field instead of the name gaining a prefix.
`battle.damage_dealt`, `battle.fainted`, `battle.status_inflicted`,
`battle.battler_switched`, `battle.ball_thrown`, `battle.exp_gained`,
`pokemon.level_up`, `pokemon.move_learned`; hooks `battle.damage`,
`battle.crit`, `battle.accuracy`, `battle.turn_order`,
`battle.crit`, `battle.accuracy`, `battle.charge_required`,
`battle.turn_order`,
`battle.enemy_action`, `battle.run`, `battle.exp_award`, `exp.gain`,
`catch.rate`, `trainer.party`, `battle.overlay`, `battle.low_health_alarm`,
`battle.catch_exp`, `battle.bottom_ui_visible`,
+57 -5
View File
@@ -122,21 +122,40 @@ Each object requires a stable `id`, a display `name`, a destination `file`
digests. `format` is either `"raw"` (the default) or `"n64"`. An optional
`description` gives players dump or region guidance in the import panel.
`size` declares the exact canonical byte length; `max_size` declares a smaller
per-import ceiling when an exact size is not appropriate. Every import also
has an engine-enforced 128 MiB ceiling and is rejected before hashing when its
filesystem reports an invalid size.
per-import ceiling when an exact size is not appropriate. The engine hard limit
is 2 GiB. Imports above 128 MiB receive an explicit free-space confirmation and
use the launcher's streaming large-file path rather than being materialized as
one Lua string.
For `"n64"`, the launcher recognizes `.z64`, `.v64`, and `.n64` byte orders,
strips a recognized 512-byte copier header, converts the bytes to canonical
big-endian `.z64` order, and then checks MD5. The canonical bytes are written
to `mods/<mod-id>/baseroms/<file>`. Each selection is a private grant to that
mod: the launcher never scans or copies another mod's imported files merely
because its manifest names the same digest. Mods read the result with their existing scoped `mod:read` API, for
example `mod:read("baseroms/stadium2.z64")`; no host path or new filesystem
because its manifest names the same digest. Small sources can still be read
with the existing scoped `mod:read` API, for example
`mod:read("baseroms/stadium2.z64")`. For large sources, prefer the bounded
`mod.imports` facade described below; no host path or new general filesystem
permission is exposed. Missing `required_imports` block the mod before its
entry chunk runs; missing `optional_imports` remain visible in the same
launcher panel but do not block loading.
#### Bounded access to validated imports
A loaded mod can address only ids declared by its own `required_imports` or
`optional_imports` arrays:
```lua
local info, err = mod.imports:info("stadium2")
local header, err = mod.imports:read("stadium2", 0, 4096)
```
`read` uses zero-based offsets and is capped at 8 MiB per call. The engine
rechecks the stored import before exposing it, seeks into the engine-owned
copy, and never gives the mod a host path or file handle. This is intended for
large source formats whose table/index can be parsed with small reads before
selectively reading the payloads a transform actually needs.
MD5 here identifies a known dump because ROM databases commonly publish it;
it is not a security or authenticity guarantee. Do not paste the SHA-1 used by
Gen1Recomp's own game-ROM importer into an import's `md5` field. Mod archives
@@ -441,6 +460,28 @@ default** (1x front, 2x back).
ball-to-pic grow multiplies your scale through each stage, so a rescaled
mon still grows into place from the ball, grounded the whole way.
## Installation-scoped generated cache
Generated data derived from a validated user source often belongs to the mod
installation rather than to one Pokémon save. `mod.cache` is that namespace:
```lua
local ok, err = mod.cache:write("extract/v1/arena.bin", encodedArena)
local bytes, err = mod.cache:read("extract/v1/arena.bin")
local info = mod.cache:info("extract/v1/arena.bin")
mod.cache:delete("extract/v1/arena.bin")
```
The physical root is engine-owned (`mod_cache/<mod-id>/`) and never exposed to
the mod. Keys are safe relative paths and a single write is capped at 64 MiB.
The cache does not rewind with checkpoints and is not scoped to game version,
slot, or playthrough. The mod owns its generated format, fingerprints, rebuild
policy, and completion marker; the engine treats the bytes as opaque data.
Use `mod.storage` instead when the data belongs to one playthrough. Use
`mod.cache` when it is a reproducible installation artifact that can be rebuilt
from a declared user source.
## Durable tool storage and runtime checkpoints
`mod.save` remains the right place for state that should travel with the next
@@ -633,6 +674,17 @@ the selected indices. Mods remain responsible for selection policy and should
use only public `mod.ui`, hook, and save APIs. See RFC 0010 for the exact
contract and compatibility guarantees.
Both battle engines expose the guarded `battle.charge_required` hook when a
charge-capable move is selected for its initial turn and the active ruleset
would otherwise charge it. The wrapper receives `(next, ctx)`, where `ctx` is
`{ battle, user, target, move, charge = true, isCalled }`. Return `false` to
skip only that initial charge and continue through the ordinary move pipeline;
call `next(ctx)` to keep it. The hook does not run for the release turn or when
the active ruleset already skips charging (for example, Gold Solarbeam in
sun). PP use, accuracy, damage, animation, and secondary effects remain owned
by the engine. With no subscriber, the vanilla decision runs without building
the hook context.
## Developer console
Boot with developer mode on to unlock the in-game console and hot-reload
@@ -0,0 +1,141 @@
# RFC 0008 — Streamed mod imports and installation-scoped generated cache
## Motivation
`required_imports`/`optional_imports` can now describe files up to 2 GiB, but
the existing launcher and public mod API still assume imported bytes are small:
* the Windows desktop picker stages a selected required import through a fixed
`%TEMP%/pokeport_required_import.bin` path before validation;
* the fallback import path materializes the selected file as one Lua string;
* after validation a mod can only use `mod:read("baseroms/...")`, which also
materializes the whole file;
* `mod.storage` is intentionally scoped to one Pokémon playthrough, so it is
not an appropriate home for a one-time generated asset cache shared by every
save using the same installed mod.
This makes optical-disc-sized user sources impractical even though the manifest
schema already accepts them. A failed temporary staging copy can also turn a
valid large source into a smaller temporary file and produce a misleading
"wrong file size" rejection.
A mod should be able to consume its own already-validated source incrementally
and compile derived runtime data once, without receiving a host path or general
filesystem access.
## Decision being extended
This extends the same legal/sandbox direction as **D11 asset transforms**
(`src/mods/AssetTransform.lua`): mods distribute recipes and derive bytes from
user-owned sources rather than shipping ROM-derived data. It also follows the
**D14 parity-gate** contract referenced by `tests/harness.lua` and
`tests/engine/gate_meta_coverage.lua` (the `21-testing-and-ci` plan): additive
extension points ship public-API coverage, no-mod parity coverage, and docs in
the same change.
The historical D11 plan document is referenced by source comments but is not
present in the current repository tree; this RFC is the checked-in design
record for the new surface.
## Exact API delta
No manifest field changes. Existing `required_imports` and `optional_imports`
remain the declaration/validation authority.
Two additive facades are added to the `mod` object.
### `mod.imports`
```lua
local info, err = mod.imports:info("source_id")
local bytes, err = mod.imports:read("source_id", offset, length)
```
* `source_id` must name an import declared by the calling mod.
* the import is rechecked through `RequiredImports.validateStored` before it is
exposed, so missing, replaced, or invalid optional imports are not readable;
* `offset` and `length` are zero-based byte coordinates;
* one read is capped at 8 MiB;
* no host path or file handle is returned;
* production reads seek into the engine-owned stored copy instead of reading
the whole source.
`info()` returns declaration metadata plus stored size. It does not expose a
host path.
### `mod.cache`
```lua
mod.cache:write("extract/v1/model.bin", bytes)
local bytes = mod.cache:read("extract/v1/model.bin")
local info = mod.cache:info("extract/v1/model.bin")
mod.cache:delete("extract/v1/model.bin")
```
The cache is rooted at `mod_cache/<mod-id>/`, follows the engine persistence
backend, and is independent of game version, launcher slot, and playthrough.
Paths are checked with `SafePath`; `..`, absolute paths, drive paths, and other
escapes remain unavailable. A single cache write is capped at 64 MiB so large
generated datasets are naturally split into independently replaceable files.
The engine does not interpret cache bytes. Mods own generated-format versioning,
fingerprints, transactional completion markers, and rebuild policy.
## Launcher/import transport delta
For large raw required imports:
1. desktop pickers return the original selected path instead of staging it
through a fixed temporary file;
2. the engine opens that source itself;
3. bytes are copied directly to the existing engine-owned
`mods/<id>/baseroms/<file>` destination in 4 MiB chunks;
4. MD5 is updated incrementally during the copy;
5. the normal size/MD5 validation receipt is written only after the complete
destination passes validation;
6. partial destinations are removed on short reads, write failure, size
mismatch, or digest mismatch.
N64 imports stay on the existing canonicalization path because byte-order and
copier-header normalization require transformation rather than a raw copy.
If a validation receipt for an already-stored large raw import is missing, the
engine rebuilds it with streaming MD5 rather than a whole-file read.
## Backward compatibility / migration
**Existing mods do nothing.** This is additive.
* manifest v1/v2 fields are unchanged;
* `mod:read`, `mod.storage`, registries, events, hooks, and legacy compatibility
retain their existing behavior;
* small required imports retain the existing in-memory validation path;
* N64 imports retain canonicalization and existing accepted byte orders;
* a mod that never touches `mod.imports` or `mod.cache` creates no new cache
files and observes no new behavior.
The mod API integer is not bumped because no existing member changes meaning or
shape.
## Security and legal posture
The launcher remains the authority that validates user-supplied bytes. The new
facade narrows access rather than widening it: a mod can read only ids declared
in its own manifest, only after validation, and only in bounded ranges. It does
not receive host paths, `io`, or a raw filesystem handle.
`mod.cache` is writable only beneath the calling mod's generated-cache root.
Nothing in this RFC permits packaged ROM-derived bytes; `modkit lint/pack`
continue to enforce the existing legal posture.
## Parity guarantee
The change ships with:
* a no-mod/API-v1 parity test proving an empty load and an existing v1-style
`mod:read` load do not create cache data or change the old surface;
* a public mod-API test that reaches `mod.imports` and `mod.cache` through a
real `Loader` load, including bounded reads, undeclared/missing imports,
cache isolation, and traversal rejection;
* incremental MD5 vectors and a large-import streaming regression test;
* the existing engine suite, required-import suite, and mod lint gates.
+92
View File
@@ -0,0 +1,92 @@
# RFC 0011: Charge-required battle hook
## Status
Proposed.
## Motivation
A battle-mechanics mod can change damage through `battle.damage` and register
move effects, but it cannot conditionally skip the first turn of an existing
charge move. In Gen 1, the engine decides and stores the charge continuation
before any public effect callback can run. Reaching into `user.charging`,
`user.chargeReady`, or generation-specific volatile state is private,
checkpoint-fragile, and would require a mod to duplicate move-pipeline policy.
Weather is the immediate example: a portable sun rule needs Solarbeam to
resolve on selection while leaving Fly, Dig, PP use, hit resolution, animation,
and secondary effects to the engine. The capability is generic and useful to
other ruleset and move-mechanics mods.
## Decision and plan extended
This implements **D-AT-002: charge-stage policy remains mod authority through a
generic guarded engine decision seam**. The consuming design is tracked in the
Adaptive Trainers implementation plan,
[`docs/superpowers/plans/2026-08-14-adaptive-trainers.md`](https://github.com/MaxTomahawk/gen1recomp-adaptive-trainers/blob/main/docs/superpowers/plans/2026-08-14-adaptive-trainers.md),
Task 8. The delta follows the additive, guarded hook convention documented by
Route B in `CONTRIBUTING-mods.md`; it contains no weather, move-id, trainer, or
Adaptive Trainers policy.
## Exact API delta
Both the Gen 1 and Gen 2 battle engines add this guarded hook:
```lua
mod.hooks:wrap("battle.charge_required", function(next, ctx)
-- ctx = {
-- battle = live battle controller,
-- user = attacking battler,
-- target = defending battler,
-- move = merged move record,
-- charge = true,
-- isCalled = false,
-- }
if should_resolve_now(ctx) then return false end
return next(ctx)
end)
```
The call site is the initial-use charge decision, after announcement and PP
handling but before charge state, invulnerability, charge animation, or charge
text is created. It runs only when the active engine rules would otherwise
require a charge. It does not run on the release turn. Returning exactly
`false` skips that initial charge and continues through the engine-owned move
pipeline. Any other downstream return preserves the charge. `isCalled` is true
when Metronome or Mirror Move selected the move.
Gold keeps its native sun decision first, so Solarbeam in native sun already
requires no charge and does not invoke the hook. Gen 1 link battles use the
shared Gen 1 move pipeline and therefore receive the same seam; normal link
mod-compatibility rules continue to govern deterministic peers.
The hot path first calls `Runtime.wantsHook("battle.charge_required")`. With no
subscriber, no hook payload table is allocated and the existing branch runs
unchanged.
## Migration and compatibility
Existing mods change nothing. The hook name and payload are additive. With no
wrapper installed, Red, Blue, Yellow, Gold, and Silver retain their previous
charge state, PP use, text, animation, accuracy, damage, and native weather
behavior. Existing charge-move data and effect records require no migration.
A mod adopting the seam should call `next(ctx)` unless it deliberately wants to
skip this charge. It should not mutate private charge fields or re-run the move.
## Verification
- `tests/engine/battle_charge_required.lua` exercises the real Gen 1 and Gen 2
engines through a sandboxed public mod, including false-to-skip, next-to-keep,
release-turn behavior, called-move PP semantics, shared payload shape, and
native Gold sun behavior.
- The same test proves no-mod charge/release parity and replaces
`Runtime.call` with a sentinel behind a false `Runtime.wantsHook` guard.
- `tests/engine/gate_hooks.lua` discovers the new catalog name and proves empty
chains preserve vanilla values and allocation behavior.
- `tests/engine/gate_gen2_mod_api.lua` requires a guarded site in both
generations and keeps the compatibility reference list complete.
## Deprecation etiquette
Nothing is removed, renamed, superseded, or deprecated.