mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-22 13:36:14 +02:00
Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev
This commit is contained in:
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user