mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-28 18:06:09 +02:00
big online energy
This commit is contained in:
@@ -61,6 +61,48 @@ the same core data and graphics into the source tree for verification.
|
||||
| ui | `src/ui/*` | start menu, generic menu, yes/no box, party/bag lists |
|
||||
| | `tools/save-editor/` | Save editor: shipped in every build, opened from the launcher's Edit button or standalone with `love . --editor` |
|
||||
|
||||
## Online play
|
||||
|
||||
Online play is owned by the launcher, not by a running game. `src/online/`
|
||||
holds one persistent relay connection for the life of the process
|
||||
(`main.lua` pumps it every frame, whether the launcher or a game is on
|
||||
screen), and a battle is run by **arena booting** the game: no splash, no
|
||||
title, no overworld, just the lockstep battle, then straight back to the
|
||||
launcher with the room still selected. In-game link (`src/link/LinkState.lua`)
|
||||
stays as it was and is LAN only. The relay lives in its own repo,
|
||||
`../pokeserver`; `docs/link-security.md` describes protocol v2 and what it
|
||||
does and does not guarantee.
|
||||
|
||||
- `src/online/Client.lua` - process singleton: connection, heartbeat,
|
||||
reconnect with session resume, the inbox, and the local model (presence,
|
||||
room, match, tournament). `Client.roomSession()` hands `LinkBattle` the
|
||||
same shape a LAN `Session` does.
|
||||
- `src/online/Protocol2.lua` - relay protocol v2 message builders; the
|
||||
matching schemas live with the v1 ones in `src/link/Wire.lua`.
|
||||
- `src/online/ArenaData.lua` - computes an ArenaProfile (engine, version,
|
||||
engine/api version, fingerprint, ruleset, vanilla or sealed cart) headless
|
||||
by mounting a version's cache, and compares two profiles
|
||||
(`equal`, `describeMismatch`).
|
||||
- `src/online/ArenaBoot.lua` - the ArenaSpec: profile, role, slot, team,
|
||||
seed, parties, session, `onDone`; plus the battle options a spec turns
|
||||
into.
|
||||
- `src/online/TeamPick.lua` - headless slot read, rule validation and party
|
||||
packing. `src/online/Convert.lua` - Gen 1 <-> Gen 2 mon conversion with
|
||||
Time Capsule refusals. `src/online/Trade.lua` - launcher-side trade with a
|
||||
two-file commit. `src/online/OnlineSprites.lua` - the cached party icons
|
||||
and front sprites those pickers draw.
|
||||
- Boot path: `main.lua` `bootGame(version, cartId, { arena = spec })` ->
|
||||
`Game:load` / `Game2:load` skip the intro and push
|
||||
`src/ui/ArenaState.lua` or `src/ui/gen2/ArenaState.lua`, which build
|
||||
`src/link/LinkBattle.lua` (Gen 1) or `src/link/LinkBattle2.lua` (Gen 2),
|
||||
host, guest or spectator, and return the result.
|
||||
- Mods in an arena: `Loader:load(data, { mode = ... })` runs `disableAll`
|
||||
(verified translations only) for a vanilla arena and `cartOnly` for a
|
||||
sealed-cart one, without touching the player's saved enable state.
|
||||
- `src/import/OnlinePanel.lua` plus `src/import/online/` are the launcher's
|
||||
ONLINE tab: a small stack of screens (home, play, setup, room, watch,
|
||||
tournaments, trade) drawn from `Client`, `ArenaData` and `TeamPick`.
|
||||
|
||||
## Map scripts
|
||||
|
||||
Map-specific behavior lives in `data/scripts/<map>.lua`, keyed by the
|
||||
|
||||
+249
-45
@@ -4,6 +4,106 @@ Link play is the only part of this game that reads bytes written by
|
||||
somebody else. This is what it defends against, what it does not, and
|
||||
where each guarantee lives.
|
||||
|
||||
## Where link play lives now
|
||||
|
||||
Two separate things, one wire stack:
|
||||
|
||||
- **In-game link is LAN only.** `src/link/LinkState.lua` is the START menu /
|
||||
Cable Club path over ENet on the local network. It has no relay, no room
|
||||
codes and no bracket: the in-game ONLINE and TOURNAMENT rows are gone and
|
||||
`src/link/Tournament.lua` no longer exists.
|
||||
- **Online play lives in the launcher**, over relay protocol v2 against
|
||||
`../pokeserver`. `src/online/Client.lua` holds one persistent TCP
|
||||
connection for the life of the process (`main.lua` pumps it every frame,
|
||||
whether the launcher or a booted game is on screen). Presence, rooms,
|
||||
spectators and tournaments are objects addressed inside that one
|
||||
connection. A tournament is a room of rooms: the relay creates each
|
||||
bracket match as an ordinary v2 room and attaches every non-playing
|
||||
entrant to it as a spectator.
|
||||
- A battle never runs in the launcher. The launcher **arena boots** the
|
||||
game (`src/online/ArenaBoot.lua` -> `Game:load` / `Game2:load` ->
|
||||
`src/ui/ArenaState.lua` or `src/ui/gen2/ArenaState.lua`), which skips
|
||||
splash, title and overworld, runs `LinkBattle` / `LinkBattle2` on the room
|
||||
session, and returns to the launcher with the room still selected.
|
||||
|
||||
## The arena profile
|
||||
|
||||
The compatibility contract is one table, computed in the launcher without
|
||||
booting a game (`src/online/ArenaData.lua`), attached to every lobby entry
|
||||
and room, and checked twice:
|
||||
|
||||
| Field | What it pins |
|
||||
| --- | --- |
|
||||
| `engine` | 1 or 2: which battle engine the room runs |
|
||||
| `version` | which generated cache boots (`red` ... `crystal`) |
|
||||
| `engineVersion`, `apiVersion` | exact string match, no skew |
|
||||
| `fingerprint` | `Fingerprint.compute` over the *arena* dataset |
|
||||
| `rulesetId` | the host-dealt rulebook (see below) |
|
||||
| `kind` | `vanilla` (all mods off) or `cart` (a sealed cart) |
|
||||
| `cart` | `{ id, version, hash }`, the `CartManifest.hash` of that cart |
|
||||
| `rule` | party size and level bounds, not part of the identity |
|
||||
|
||||
**The relay enforces it on join.** `room_join` walks
|
||||
`PROFILE_MATCH_FIELDS` (`../pokeserver/relay.js:66`) in order -- `engine`,
|
||||
`version`, `engineVersion`, `fingerprint`, `rulesetId`, `kind`, `cart.hash`
|
||||
-- and refuses the first field that differs with
|
||||
`join_error { reason: "profile_mismatch", field, detail }`. `room_ready` runs
|
||||
the party against `rule` the same way and answers `party_ineligible`.
|
||||
Spectators are held to the profile too: a spectator runs the same engine to
|
||||
render the battle.
|
||||
|
||||
**The client checks it before it will join or boot.** The ONLINE tab runs
|
||||
`ArenaData.equal` (everything except `rule`) against its own profile for
|
||||
every lobby row and greys out the ones it cannot play, with
|
||||
`ArenaData.describeMismatch` naming the first differing field so the reason
|
||||
is *what* differs rather than "incompatible"
|
||||
(`src/import/OnlinePanel.lua`). The battle it then boots is constructed
|
||||
with `strict = true` (`ArenaBoot.battleOpts`), so a mon that cannot be
|
||||
rebuilt from real species data refuses the battle instead of being
|
||||
approximated into a desync.
|
||||
|
||||
A `vanilla` arena boots the mod loader in `disableAll` mode and a `cart`
|
||||
arena in `cartOnly` (`Loader.ARENA_MODES`, `src/mods/Loader.lua:618`).
|
||||
Neither reads or writes `options.safeMode` or `options.modsByVersion`, so an
|
||||
arena never disturbs the player's own enable state. `disableAll` keeps
|
||||
verified translations and only those: `_arenaDisableAll` keeps a mod that
|
||||
declares `language = true`, `affects_link = false` and no permissions, and
|
||||
`_arenaVerifyTranslations` then re-checks the survivors through
|
||||
`Handshake.onlineBlockers` and rolls back anything that does not qualify.
|
||||
`cartOnly` refuses to boot when the cart plan is not `enforced` or the
|
||||
slot's seal is broken; `sealed+` and `open` carts are refused outright,
|
||||
because a player-togglable pin set is not a fixed identity.
|
||||
|
||||
## Desync: the ruleset and the RNG counter
|
||||
|
||||
Nearly every reported desync was turn 1 or 2 with **identical engine
|
||||
versions on both sides**. The cause was local: `BattleState` picked its
|
||||
ruleset from `game.save.options.ruleset`, an ordinary OPTIONS row, and
|
||||
`gen1_faithful` spends one RNG draw on the 1/256 miss that `modern_clean`
|
||||
does not. Both machines share one Park-Miller stream, so a single skipped
|
||||
draw offsets it permanently and the very next number is the damage factor.
|
||||
|
||||
The fix is three parts, all of them in the wire:
|
||||
|
||||
- The **host deals the ruleset**. `LinkBattle` takes `opts.ruleset` and
|
||||
overwrites `self.ruleset` from it the way it already overwrote `rng`
|
||||
(`src/link/LinkBattle.lua:275`, spectator at `:824`). In an arena that id
|
||||
comes from the profile.
|
||||
- The hello **carries** it (`Handshake.ruleset`, `src/link/Handshake.lua:180`)
|
||||
and `checkCompat` refuses a mismatch as `ruleset_skew`, so a LAN pairing
|
||||
cannot start split either.
|
||||
- The rulesets registry is part of the **Gen 1 fingerprint surface**
|
||||
(`src/link/Fingerprint.lua:319`), so a retuned rulebook is caught even when
|
||||
both sides name the same id.
|
||||
|
||||
The other half of the diagnosis was that the state signature could not see a
|
||||
draw-count split, so a desync was blamed on the wrong turn. The RNG draw
|
||||
counter is now hashed into the `actives` component
|
||||
(`src/link/LinkBattle.lua:196`; Gen 2 stamps it on the battle at
|
||||
`src/link/LinkBattle2.lua:256` and hashes it in
|
||||
`src/battle/gen2/Battle.lua:5173`), so the two sides disagree on the exact
|
||||
turn the streams part.
|
||||
|
||||
## The boundary
|
||||
|
||||
Everything a peer or the relay sends arrives as one JSON object per line.
|
||||
@@ -21,6 +121,11 @@ right by construction. A message with no schema (a mod's, or a future
|
||||
build's) keeps a bounded, scalar-only copy of its payload instead of
|
||||
being dropped.
|
||||
|
||||
The v2 types go through the same door: `SCHEMAS.lobby_*`, `room_*` and
|
||||
`tour_*` live beside the v1 ones in `Wire.lua`, and `room_msg` sanitizes its
|
||||
inner lockstep message as well as its envelope, so `Client.roomSession()`
|
||||
hands `LinkBattle` the same shape a LAN `Session` does.
|
||||
|
||||
A message that fails its schema is **dropped and logged**, never fatal.
|
||||
Latching a terminal failure would hand a hostile peer a cheaper
|
||||
disconnect than sending nothing at all.
|
||||
@@ -38,17 +143,22 @@ it without a Session (the mod API, `tests/`).
|
||||
|
||||
Assume something still gets through:
|
||||
|
||||
- `Game:step` pcalls the link pump, and pcalls `stack:update` **only
|
||||
while a link session is active**. On a throw, `Game:breakLink` closes
|
||||
the connection, unwinds to the overworld and says "The link was
|
||||
broken." Outside link play the stack is unguarded on purpose: a blanket
|
||||
pcall would swallow real engine bugs and leave the game silently wrong
|
||||
instead of loudly broken.
|
||||
- `Net` caps `rxBuf` at 256KB and its per-frame read at 512KB, so a peer
|
||||
that never sends a newline ends as a clean disconnect.
|
||||
- `Json.decode` refuses documents nested past 64 levels, and takes an
|
||||
optional length cap that the link path passes and the mod-manifest path
|
||||
does not.
|
||||
- `Game:step` pcalls the link pump, and xpcalls `stack:update` **only
|
||||
while a link session is active** (`src/core/Game.lua:329`). A throw
|
||||
reaches `Game:breakLink` (`:276`), which closes the connection, unwinds
|
||||
to the overworld and says so. Transport failure and a caught Lua error are
|
||||
distinguished on purpose: the player is told "The link was broken." for
|
||||
one and "Something broke during the link." for the other, and the log
|
||||
line differs, so an engine bug inside a link battle no longer reads as a
|
||||
network fault. Outside link play the stack is unguarded on purpose: a
|
||||
blanket pcall would swallow real engine bugs and leave the game silently
|
||||
wrong instead of loudly broken.
|
||||
- `Net` caps a line at 256KB (`Net.MAX_LINE`) and its per-frame read at
|
||||
512KB (`Net.MAX_RX_PER_FRAME`), so a peer that never sends a newline ends
|
||||
as a clean disconnect.
|
||||
- `Json.decode` refuses documents nested past 64 levels (`Json.MAX_DEPTH`),
|
||||
and takes an optional length cap that the link path passes and the
|
||||
mod-manifest path does not.
|
||||
|
||||
## The relay (`../pokeserver`)
|
||||
|
||||
@@ -57,48 +167,106 @@ Assume something still gets through:
|
||||
`server.js` installs `uncaughtException`/`unhandledRejection` handlers:
|
||||
one bad packet must never take every live match down with the process.
|
||||
- Line buffers are capped, lines per second are capped, connections per
|
||||
IP and in total are capped, and an unbound connection that never hosts
|
||||
or joins is swept after 30s.
|
||||
- `SERVER_ONLY` is the set of message types the server is the only
|
||||
legitimate author of (`peer_gone`, `bracket_update`, `match_start`,
|
||||
`tournament_over`, `spectate`, ...). A peer that sends one has them
|
||||
dropped rather than forwarded, so a bracket opponent cannot forge a
|
||||
tournament result or fake "your opponent left".
|
||||
- Trainer names are reduced to a printable subset and capped at the same
|
||||
10 characters the game enforces, on the way in, because they are
|
||||
IP and in total are capped, and an unbound connection that never hosts,
|
||||
joins or binds as a lobby is swept after 30s.
|
||||
- `SERVER_ONLY` (`relay.js:76`) is the set of message types the server is
|
||||
the only legitimate author of. It covers the v1 names (`peer_gone`,
|
||||
`bracket_update`, `match_start`, `tournament_over`, `spectate`, ...) and
|
||||
every v2 one (`lobby_welcome`, `lobby_list`, `lobby_delta`, `room_state`,
|
||||
`room_replay`, `room_deadline`, `room_result`, `room_closed`, `tour_state`,
|
||||
`tour_match`, `tour_match_spectate`, `tour_bye`, `tour_deadline`,
|
||||
`tour_over`, `tour_closed`). A peer that sends one has it dropped rather
|
||||
than forwarded, so a bracket opponent cannot forge a result or fake "your
|
||||
opponent left".
|
||||
- `room_msg` only carries an inner `msg.type` from a fixed set (`hello`,
|
||||
`party`, `action`, `hash`, `replace`, `bye`, `forfeit`), only from a
|
||||
seated player, and only while the room is `battling`. Its `seq` must
|
||||
rise, so a resumed client's replayed tail is idempotent.
|
||||
- Names, notes, room codes and profile strings are reduced to a printable
|
||||
subset and capped on the way in (10 characters for a trainer name, 16 for
|
||||
a lobby display name, 40 for an advertisement note), because they are
|
||||
rendered by the dashboard and broadcast to every participant.
|
||||
|
||||
`pokeserver/test/hostile.js` is the regression net for all of that.
|
||||
|
||||
### Identity: tickets
|
||||
|
||||
Going online verified takes a ticket, not a credential.
|
||||
`POST /lobby/ticket` on the HTTPS port (`lobby.js`) authenticates with the
|
||||
usual sync headers, mints 32 hex characters bound to
|
||||
`{ account, displayName }`, valid for 60 seconds and **single use**, and the
|
||||
client presents that on the plaintext relay in `lobby_hello`. The relay
|
||||
redeems it in process (both modules live in `server.js`) and never sees the
|
||||
device token: nothing on port 7778 can be replayed into an account. The
|
||||
ticket store is memory only, so a restart invalidates every outstanding
|
||||
ticket and clients simply mint another. A client with no ticket still
|
||||
connects and still plays, as a guest, listed `verified: false`.
|
||||
|
||||
### Heartbeat, resume and the replay bound
|
||||
|
||||
- `ping`/`pong` runs both ways. The relay pings a connection idle for 20s
|
||||
and drops one idle for 60s, on top of TCP keepalive; the client's own
|
||||
heartbeat interval arrives in `lobby_welcome`.
|
||||
- A dropped socket keeps its seat for **2 minutes**. A new connection sends
|
||||
`resume { session, ack }` and gets its room, its match and the traffic it
|
||||
missed back. `room_replay` also names `yourSeq`, the highest `seq` the
|
||||
relay logged from that client, so the client re-sends only its own lost
|
||||
tail.
|
||||
- The room's message log is bounded at **512 messages or 256 KiB**,
|
||||
whichever comes first, since the last `room_ready` pair. A spectator that
|
||||
arrives while the log is intact gets `room_replay` and then live traffic;
|
||||
one that arrives after the bound already dropped messages is refused with
|
||||
`spectate_late` rather than being fed a battle it cannot reconstruct.
|
||||
|
||||
## What is NOT defended
|
||||
|
||||
**Party legality is trust-the-client.** Online play meets strangers, and
|
||||
**Party legality is trust-the-client.** Online rooms, spectating and
|
||||
tournaments live in the launcher, where play meets strangers, and
|
||||
`Handshake.onlineAllowed` is a Lua function in the same VM the mods load
|
||||
into. It cannot be made tamper-proof in-process, and pretending otherwise
|
||||
would only cost honest mod authors. What lockstep and
|
||||
`Protocol.unpackMon`'s recompute-from-species-data *do* guarantee is that
|
||||
a cheater cannot invent stats, moves, or a shiny: every derived value is
|
||||
rebuilt locally from real species data. They can send a legal party they
|
||||
farmed or edited. That is the honest boundary.
|
||||
rebuilt locally from real species data. The relay checks party size and
|
||||
level bounds against the room's `rule`, and that is the only rule it
|
||||
enforces. A player can still send a legal party they farmed or edited.
|
||||
That is the honest boundary.
|
||||
|
||||
What the relay does instead is **observe and record**. It already sees
|
||||
every `hello`, so it keeps each connection's self-reported
|
||||
`engineVersion`, `fingerprint` and `linkModified`, compares the two sides
|
||||
of a room or a live tournament match, and logs and surfaces a
|
||||
`modded` / `fingerprint_mismatch` / `version_skew` flag on the dashboard.
|
||||
A patched client can still lie; what it cannot do is lie without the
|
||||
tournament organizer having a record of it.
|
||||
**Match results are trust-the-client too**, and this is where the relay
|
||||
stopped being silent. Both sides report; agreement resolves the match, a
|
||||
lone report waits out a grace window, a lone forfeit resolves at once, and
|
||||
a stalled match resolves on its deadline. Two **disagreeing** reports are
|
||||
counted as a dispute (`totals.disputes` and the room's or tournament's own
|
||||
counter, both in the dashboard snapshot), logged with both sides' reports
|
||||
verbatim, and settled **deterministically**, never by coin flip: a forfeit
|
||||
or a gone player decides it, otherwise the earliest report wins
|
||||
(`roomTiebreak`, `relay.js:1761`). The outcome carries `how` --
|
||||
`reported`, `agreed`, `forfeit`, `timeout`, `disputed`, `disconnect` or
|
||||
`closed` -- so a tournament organizer can see which matches were argued.
|
||||
|
||||
The same is true of the arena profile. A patched client can lie about its
|
||||
fingerprint or its cart hash; what it cannot do is lie without the relay
|
||||
having a record of it, since the relay sees every profile and compares the
|
||||
two sides of every room. A sealed cart's hash proves the manifest, the
|
||||
pins' sha256 prove the archives, and the fingerprint proves the merged
|
||||
data -- against an honest client.
|
||||
|
||||
Client-side attestation is deliberately not built. This is an
|
||||
open-source Lua game: it would be theater, and it would break honest
|
||||
mods.
|
||||
|
||||
**The relay has no TLS.** Port 7778 is plaintext, so party contents,
|
||||
trades and trainer names are visible to anyone on the network path. There
|
||||
is nothing secret in a Pokemon party, but it is a real property of the
|
||||
system and not an oversight. Fixing it means a TLS terminator in front of
|
||||
the relay and a client that speaks it, which is a version break for every
|
||||
shipped build.
|
||||
trades and trainer names are visible to anyone on the network path. Tickets
|
||||
keep account credentials off it (above), and there is nothing secret in a
|
||||
Pokemon party, but it is a real property of the system and not an
|
||||
oversight. Fixing it means a TLS terminator in front of the relay and a
|
||||
client that speaks it, which is a version break for every shipped build.
|
||||
|
||||
**Presence is not durable.** The lobby, every room and every outstanding
|
||||
ticket live in the relay process. A restart clears them and clients
|
||||
reconnect and re-advertise within one heartbeat. Nothing about a match is
|
||||
persisted, so nothing about a match can be audited after the fact beyond
|
||||
the log lines.
|
||||
|
||||
**The dashboard has no default password.** `DASHBOARD_PASSWORD` is
|
||||
required; with it unset the relay runs and the dashboard simply does not
|
||||
@@ -107,14 +275,50 @@ IP restriction or an SSH tunnel (`pokeserver/DEPLOY.md`).
|
||||
|
||||
## Tests
|
||||
|
||||
luajit tests/link_hostile.lua every message type x every wrong type
|
||||
luajit tests/link_desync_fuzz.lua lockstep fuzz, plus a mutation mode
|
||||
luajit tests/run_link_tests.lua both of the above, plus the rest
|
||||
cd ../pokeserver && npm test relay smoke, 16-player bracket, hostile
|
||||
luajit tests/link_hostile.lua v1 message types x every wrong type
|
||||
luajit tests/link_desync_fuzz.lua fuzz: baseline, mutation, ruleset split
|
||||
luajit tests/run_link_tests.lua the above, plus online_client and link2_*
|
||||
cd ../pokeserver && npm test relay smoke, brackets, lobby, tour
|
||||
|
||||
`tests/link_hostile.lua` builds its corpus from a template per message
|
||||
type, replaces each field (and several nested ones) with every wrong Lua
|
||||
type, and drives the survivors through the real trade session, a real
|
||||
lockstep battle, a real spectator battle, and the tournament screen
|
||||
**including its draw** -- because the two nastiest payloads are
|
||||
delayed-fuse ones that crash on render rather than on receipt.
|
||||
What guards which claim:
|
||||
|
||||
- **Ruleset desync**: `run_link_tests.lua` asserts the draw-count difference
|
||||
between `gen1_faithful` and `modern_clean` directly, then runs a loopback
|
||||
battle where the guest's own `modern_clean` OPTIONS row is overridden by
|
||||
the host's dealt ruleset. `link_desync_fuzz.lua`'s ruleset-split mode
|
||||
deliberately configures the two sides differently and fails if the two
|
||||
`LinkBattle`s ever end up holding different rulebooks.
|
||||
- **Wire hostility**: `link_hostile.lua` builds a corpus from a template per
|
||||
v1 message type, replaces each field (and several nested ones) with every
|
||||
wrong Lua type, and drives the survivors through the real trade session, a
|
||||
real lockstep battle and a real spectator battle, **including their draws**
|
||||
-- because the two nastiest payloads are delayed-fuse ones that crash on
|
||||
render rather than on receipt. The v2 envelopes are covered on the client
|
||||
side by `tests/online_client.lua`, which sanitizes oversized `lobby_list`,
|
||||
`room_replay`, `room_state` and `tour_state` payloads and asserts a stream
|
||||
of malformed server messages is counted and dropped without throwing or
|
||||
changing connection state.
|
||||
- **Client, rooms, resume, tournaments**: `tests/online_client.lua` (run from
|
||||
`run_link_tests.lua`) covers the handshake, lobby, advertising, rooms,
|
||||
`match_start`, the room session, reconnect and resume replay, spectating,
|
||||
results, a full tournament run and the lockstep path, against a fake relay
|
||||
and, when a socket library is present, a real one.
|
||||
- **Gen 2 lockstep**: `tests/link2_lockstep.lua` and
|
||||
`tests/link2_desync_fuzz.lua`.
|
||||
- **Relay**: `../pokeserver/test/lobby.js` (bind, presence, rooms,
|
||||
spectating, the lockstep stream, deadlines, resume, a real HTTP ticket
|
||||
from a real sync account, and a v1 room in the same process),
|
||||
`test/tour.js` (a 5-player v2 bracket with byes, child rooms, spectator
|
||||
fan-out, the server-side shot clock, disconnects and creator handoff),
|
||||
`test/hostile.js`, plus `test/smoke.js`, `test/keepalive.js`,
|
||||
`test/tournament16.js` and `test/tournament5.js` for the v1 path.
|
||||
- **End to end**: the LÖVE drivers. `tests/drivers/online_relay_smoke.lua`
|
||||
runs two `Client`s through a real pokeserver over TCP: room create, join,
|
||||
ready, a full Gen 1 `LinkBattle` over `roomSession()`, both reports and the
|
||||
relay's `room_result`. `tests/drivers/online_tour_smoke.lua` runs five
|
||||
clients through a v2 tournament including a bye, an outside spectator, a
|
||||
mid-battle socket kill with resume and replay, and a forfeit.
|
||||
`tests/drivers/arena_boot_loopback.lua` and
|
||||
`arena_boot_gen2_loopback.lua` (with their `*_spec.lua` payloads) boot
|
||||
straight into an arena battle, play it against a headless guest and
|
||||
screenshot the battle and the launcher it returns to.
|
||||
|
||||
@@ -676,6 +676,17 @@ the same letter raises a second event.
|
||||
| `phone.contact_list` | `Phone`'s `wPhoneList` read | called `(save, list)`, the shape the other list hooks use | the same list |
|
||||
| `shiny.roll` | `Mon` | `dvs`, `species`, `def`, `level` | the DV-derived boolean |
|
||||
| `gender.roll` | `Mon` | `def`, `dvs`, `ratio`, `species`, `level` | the DV-derived gender |
|
||||
| `battle.enemy_switch_or_item` | `Battle:enemyTrySwitchOrItem` | called `(battle)` | `true` when the foe spent the turn rotating or drinking |
|
||||
|
||||
`battle.enemy_switch_or_item` is the companion to the shared
|
||||
`battle.enemy_action`: that one rewrites which MOVE the foe picks, this one
|
||||
decides whether the foe spends the whole turn on a rotation or an item instead
|
||||
of moving at all. Red has no such branch, which is why the name is new. Return
|
||||
a boolean to answer "the turn was spent" the way vanilla does, or an action
|
||||
table -- `{ kind = "switch", index = n }` or `{ kind = "item", item = id }` --
|
||||
to have the engine perform it. A link battle supplies both sides' actions
|
||||
directly (`Battle:takeLinkTurn`) and consults neither this hook nor
|
||||
`battle.enemy_action`, so a mod cannot desync a lockstep match through either.
|
||||
|
||||
`held_item.trigger` is one hook over eight call sites, because on the cart
|
||||
those eight *are* one routine (`GetUserItem` / `GetOpponentItem` loading b and
|
||||
@@ -737,7 +748,9 @@ change in the Gen 2 module first and a routing row second:
|
||||
- `field`: the Gen 1 overworld's data grab bag. Gold's equivalents live in
|
||||
`data.gen2Maps` and the VM's own tables.
|
||||
- `text_pointers`: Gen 1's `TEXT_*` indirection. Gold's text *is* pointers.
|
||||
- `link_fields`: link play is Gen 1 only.
|
||||
- `link_fields`: gated until the Gen 2 mon wire format carries mod fields;
|
||||
Gen 2 link battles exist (launcher arenas over `src/link/LinkBattle2.lua`)
|
||||
but ship no extra mon fields yet.
|
||||
- `map_scripts`: `data.gen2Scripts` is the cart's bytecode pool keyed by ROM
|
||||
pointer, and a Lua row list merged into it is not something
|
||||
`src/script/gen2/Vm.lua` can run. Routing it needs a Gen 2 side dispatcher in
|
||||
@@ -796,10 +809,10 @@ The list is much shorter than it was. What is outstanding, in descending value:
|
||||
when that selection is required.
|
||||
- `pokemon.before_give` / `pokemon.received`: Gold has no give-mon seam of its
|
||||
own yet.
|
||||
- `link.*` and `trade.completed`: a Gold boot offers no link menu at all. The
|
||||
Gen 2 fingerprint and handshake exist (`src/link/Fingerprint.lua` hashes a
|
||||
Gen 2 surface and a cross-generation pairing is refused by name), but nothing
|
||||
in `src/ui/gen2/` opens onto the protocol, so these raise nowhere.
|
||||
- `link.*` and `trade.completed`: a Gold boot offers no in-game link menu.
|
||||
Gen 2 battles run as launcher arenas (`src/ui/gen2/ArenaState.lua` over
|
||||
`src/link/LinkBattle2.lua`), which raise `link.battle_ended`; trades happen
|
||||
in the launcher, so `trade.completed` still raises nowhere in Gold.
|
||||
|
||||
Four groups that used to sit here have since landed and moved to the shared
|
||||
table above: the frame seams (`render.compose` / `render.hud` /
|
||||
|
||||
@@ -7,7 +7,8 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
|
||||
* **Multiple color modes**, including original, SGB, advanced GBC, monochrome, and classic green
|
||||
* **Optional GBC screen effects**, including pixel grids, shadows, glare, and transparency
|
||||
* **Performance presets** and configurable FPS limits
|
||||
* **Peer-to-peer link play** for trades and battles between Red, Blue, and Yellow
|
||||
* **Peer-to-peer LAN link play** for trades and battles between Red, Blue, and Yellow
|
||||
* **Online lobby** in the launcher for battles, spectating and tournaments
|
||||
* **Persistent custom options** stored separately from game saves
|
||||
* **Optional widescreen battle layout**
|
||||
* **Mobile touch controls** with editable layouts, vibration, and orientation settings
|
||||
|
||||
Reference in New Issue
Block a user