mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
143 lines
6.0 KiB
Markdown
143 lines
6.0 KiB
Markdown
# RFC 0013: Conditional map occupancy and active-block reads
|
|
|
|
## Status
|
|
|
|
Proposed.
|
|
|
|
## Motivation
|
|
|
|
The Gen 1 Vermilion Dock script correctly ejects a player who enters after the
|
|
S.S. Anne has departed. A content mod can add a city-side route back to that
|
|
empty harbor, but it cannot preserve the dock visit: replacing the complete
|
|
dock handler would discard vanilla behavior and peer handlers, while an added
|
|
handler cannot cancel the base handler's ejection.
|
|
|
|
A mod that changes one active map block also needs to prove that it is looking
|
|
at the expected Red, Blue, or Yellow layout before it acts. `mapOverview()` is
|
|
intentionally presentation-oriented and does not expose block identity.
|
|
Requiring internal `Map` state or generated ROM data would cross the public mod
|
|
boundary and make a wrong-version edit difficult to fail closed.
|
|
|
|
## Decision and plan extended
|
|
|
|
This extends Route B in `CONTRIBUTING-mods.md`: new behavior is additive,
|
|
ordinary hook composition remains the authority, an empty hook chain is a
|
|
provable no-op, and mods receive copied or scalar data rather than mutable
|
|
engine state. It supports the approved Mew-under-the-truck implementation plan
|
|
without adding any Mew-specific rule, asset, flag, or content to the engine.
|
|
|
|
## Exact API delta
|
|
|
|
### `map.occupancy_allowed`
|
|
|
|
The post-departure `VERMILION_DOCK` script calls this hook after it replaces the
|
|
ship blocks with water and immediately before it would display the departure
|
|
message and warp the player to Vermilion City.
|
|
|
|
A wrapper has this shape:
|
|
|
|
```lua
|
|
function(next, game, context) -> boolean
|
|
```
|
|
|
|
The context is a new table with these fields:
|
|
|
|
| Field | Meaning |
|
|
|---|---|
|
|
| `mapId` | `"VERMILION_DOCK"` at this call site |
|
|
| `reason` | Stable reason key `"ss_anne_departed"` |
|
|
| `gameVersion` | Active save version (`red`, `blue`, or `yellow`) when present |
|
|
| `x`, `y` | Current player cell coordinates when present |
|
|
|
|
Vanilla returns `false`. The player remains only when the final chain result is
|
|
exactly `true`. Absent, throwing, or malformed wrappers therefore preserve
|
|
ejection. A wrapper composes by calling `next(game, context)` and returning
|
|
true when either downstream or its own narrow rule permits occupancy. The hook
|
|
does not replace `MapScripts` registration, merging, or dispatch, and does not
|
|
change the departure flag or reconstruct the ship.
|
|
|
|
As with every wrapper hook, a callback that does not call `next` intentionally
|
|
owns the final answer and does not run lower-priority callbacks. Permission
|
|
wrappers must call downstream to compose. A false, non-forwarding wrapper
|
|
safely denies occupancy and can suppress downstream permission by this normal
|
|
rule. A malformed final result also fails closed and cannot permit occupancy.
|
|
|
|
The call is guarded by `Runtime.wantsHook`, so an empty chain allocates no
|
|
context and follows the prior branch exactly.
|
|
|
|
### `WorldAPI:activeBlockAt`
|
|
|
|
Gen 1's public `mod.world` facade adds:
|
|
|
|
```lua
|
|
activeBlockAt(mapId, blockX, blockY) -> blockId
|
|
| nil, reason
|
|
```
|
|
|
|
`mapId` must equal the active map ID. Coordinates are finite, integral,
|
|
zero-based block coordinates. A successful result is a numeric scalar copied
|
|
from the active runtime map. The method never returns the map's mutable block
|
|
array and never writes game or save state.
|
|
|
|
Failure reasons are stable:
|
|
|
|
| Condition | Reason |
|
|
|---|---|
|
|
| No active overworld map | `no overworld` |
|
|
| `mapId` differs from the active map | `map is not active` |
|
|
| Coordinate has the wrong type, is non-finite, or is fractional | `invalid block coordinates` |
|
|
| Coordinate is negative or outside the active map | `block coordinates out of bounds` |
|
|
| Active block data is absent or malformed | `block unavailable` |
|
|
|
|
The block slot and the active map accessor must both contain the same valid
|
|
nonnegative integer. Missing or sparse storage and inconsistent accessor data
|
|
return `block unavailable`.
|
|
|
|
Requiring the expected map ID and rejecting all ambiguous input lets a mod
|
|
compare every cell in its version-specific signature before it calls an
|
|
existing mutation API. Red, Blue, and Yellow each use their own loaded map
|
|
data. Gold does not gain this method in this RFC.
|
|
|
|
## Migration and compatibility
|
|
|
|
Existing mods change nothing. No hook, event, registry, manifest field, save
|
|
field, map handler, or WorldAPI method is removed or renamed. With no hook
|
|
subscriber the departed-dock behavior is unchanged. Existing callers cannot
|
|
invoke the new WorldAPI method accidentally.
|
|
|
|
The occupancy answer is not persisted by the engine. Disabling or uninstalling
|
|
a mod removes its wrapper through normal owner cleanup, so a later dock entry
|
|
uses vanilla ejection. The engine writes no new save state and neither API
|
|
returns or serializes ROM or save data.
|
|
|
|
## Verification requirements
|
|
|
|
The parity gate must prove:
|
|
|
|
- vanilla departed-dock message and warp remain with no subscriber;
|
|
- one permission wrapper can allow occupancy without removing the ship-erasure
|
|
work or any map handler;
|
|
- multiple cooperative wrappers preserve downstream permission;
|
|
- absent, throwing, nil, false, malformed, and non-forwarding hook answers fail
|
|
closed according to the normal wrapper-chain rule;
|
|
- Red, Blue, and Yellow contexts keep their version identity separate;
|
|
- disabling or removing the owner restores vanilla behavior without a save
|
|
migration;
|
|
- `activeBlockAt` accepts only the active map and valid in-range integral
|
|
coordinates, returns a scalar, and never mutates map or save state; and
|
|
- the test fixtures and resulting changes contain no ROM or save payload.
|
|
|
|
The hook must be driven through a real public `hooks:wrap` chain. The block API
|
|
must be exercised through a public `WorldAPI` instance. Tests must not replace
|
|
the production map handler with a test-only implementation.
|
|
|
|
## Docs with the change
|
|
|
|
`docs/modding.md` documents both contracts, their failure behavior, the
|
|
cooperative wrapper pattern, and the Gen 1-only block method. No registry or
|
|
schema changes occur, so generated registry documentation is unchanged.
|
|
|
|
## Deprecation etiquette
|
|
|
|
Nothing is removed, superseded, or deprecated.
|