Link compatibility
How two connected games decide whether they can battle and trade, and
how to keep a mod link-safe. Implementation: src/link/Fingerprint.lua,
src/link/Handshake.lua.
The fingerprint
On entering link play, each side digests its link surface: the
merged pokemon, moves, type chart, statuses, and move_effects
records (over stable allowlisted fields), the link-relevant constants,
any registered link_fields, and the { id, version } of every mod
whose affects_link is true. Identical surfaces produce identical
digests — regardless of how mods produced them.
The digest is generation-aware. A Gold dataset hashes a Gen 2 surface over
Gold's own records: species carry baseStats with the split specials,
genderRatio and evolutions; moves add effectChance; the type chart
brings its FORESIGHT matchups; and the Gen 2 registries statuses,
move_effects, held_items and growth_rates are hashed in place of their
Gen 1 counterparts. The two surfaces can never collide, and the
link.fingerprint hook keeps its (data, mods) shape on both.
The verdict
| Verdict | When | Battle | Trade |
|---|---|---|---|
full |
fingerprints match | yes | yes |
vanilla_peer |
old (v1) build on the far side, this side unmodified | yes | yes |
engine_skew |
both v2 on the same major, different engine releases | no | yes, negotiated |
subset |
both v2, fingerprints differ | no | strict-vanilla subset, negotiated |
refused |
v1 peer that our mods would corrupt, an engine mismatch, or a peer running the other generation | no | no |
engine_skew exists because the fingerprint only covers the data and mod
surface: battle logic moves between releases, so two honest vanilla installs
one release apart would pair as full and then drift mid-battle. They trade;
they do not battle until both sides run the same release.
The subset trade path admits only mons both sides rebuild identically
— per-record digests agree species by species — so a modded game and a
lightly-modded friend can still trade the vanilla 151 without risk. A
mid-battle divergence (the last line of defense) raises link.desync
with the failing component and ends the battle as a draw instead of
letting the two screens drift apart. Never desync silently is the rule.
The manifest switch
affects_link defaults by profile: false for content, true for
overhaul and total_conversion. Set it explicitly when the default is
wrong:
- A "content" mod that rebalances
movesor adds species does move battle outcomes — declare"affects_link": true. - An overhaul that only touches UI, music, and text can declare
"affects_link": falseto keep vanilla-compatible link play.
Declare false while writing into a link-relevant registry (pokemon,
moves, type_chart, statuses, move_effects —
src/mods/Manifest.lua LINK_REGISTRIES) and the loader logs an
attributed warning: your claim and your writes disagree.
Keeping a content mod link-safe
- Prefer additions over changes: a new species you never trade does not block the vanilla subset.
- Keep battle-relevant records untouched if you want
vanilla_peercompat with unmodified builds. - Extra per-mon data that must survive a trade belongs in the
link_fieldsregistry —{ rev, pack?, unpack? }per field, where onlyreventers the digest; bump it when the codec's meaning changes. - Two peers must run the same ordering rules:
battle.turn_orderwraps inside link battles too, so a mod that changes it must be installed on both sides (the fingerprint enforces this whenaffects_linkis true). - On Gold, held-item attributes and growth curves are link surface too:
what LEFTOVERS heals rides on a traded mon, and a curve decides what
level a traded mon's experience buys. A write into
held_itemsorgrowth_ratesmarks the install link-modified the way the five registries above always have, even though the loader's claim-vs-writes warning currently checks only those five.
Gold
Gold does not link yet: a Gold boot offers no LINK menu, and the trade and
battle sessions are not built. What is built is the fence around them. Each
hello carries a generation field (absent means 1, which every pre-Gold
build is), and a cross-generation pairing is refused as
generation_mismatch before any digest is compared. The cart's own answer
to that cable was the Time Capsule's party conversion and its validators;
until those exist, refusal is the honest verdict, named as the wrong game
rather than an older version. The link_fields registry is likewise Gen 1
only: on a Gold boot it has no home and a write is a reported drop.
Observing link play
Events: link.connected ({ role, remote = { name, mode, mods, fingerprint } }), link.ended, link.desync, trade.completed,
pokemon.received. The link.fingerprint hook wraps digest computation
itself — for tooling, not for lying to your peer.
Player-facing link setup: Link Play.
Start here
Concepts
Tutorials
- The ladder
- 01 Sprite and Text Tweak
- 02 Balance Patch
- 03 New Species
- 04 New Item and Ball
- 05 New Move
- 06 NPC and Dialogue
- 07 New Map
- 08 Quest
- 09 Custom Music
- 10 New Mechanic
- 11 Custom UI Screen
- 12 Mini Total Conversion
Reference
Guides
- Art Pipeline
- Audio Authoring
- Preparing Your Mod for Gen 2
- Translations
- Total Conversions
- Link Compatibility
- Publishing a Mod
- Custom Carts
- Docs Style Guide
Playing
Engine 1.0.0 · mod API 2 · repository · Discord
This project ships no ROM data. All game content is decoded on the player's machine from their own verified Pokemon Red ROM; mods ship recipes and original assets, never extracted content.