Mystery Gift
FireRed/LeafGreen Mystery Gift over Friend accepts a Wonder Card, a delivery script, Wonder News, a visiting Battle Tower trainer, and a Pokemon straight into the party, with no Pokemon Center. The Mystery Event VM and native ARM code are on Code on the console.
The session
The console’s client boots with {CLI_RECV, MG_LINKID_CLIENT_SCRIPT}, {CLI_COPY_RECV} [mystery_gift_scripts.c:15] and executes any MysteryGiftClientCmd array it receives [mystery_gift_client.c:87].
console joins -> SEND_PLAYER_IDS -> LinkPlayer block exchange -> one standby barrier
server -> CLIENT_SCRIPT (sClientScript_SendGameData, 32 B)
client -> GAME_DATA (MysteryGiftLinkGameData, 96 B) -- validated, card flag compared
server -> CLIENT_SCRIPT (sClientScript_SaveCard, 48 B)
server -> CARD (struct WonderCard, 332 B)
server -> RAM_SCRIPT (1024 B: the delivery bytecode, zero-padded)
client -> READY_END (1024 B)
close-link handshake -> disconnect
Framing rules
- Size 0 means 1024 [mystery_gift_link.c:55];
SVR_COPY_SAVED_RAM_SCRIPTnever setsramScriptSize[mystery_gift_server.c:275], so the RAM script and READY_END are full 1024-byte messages. The CRC covers the padded buffer. - Block pacing has no acknowledgement:
SEND_BLOCK_INITis ignored unless the receiver’s slot isRECV_STATE_READY[link_rfu_2.c:1146].MysteryGiftTiming.inter_block_gap_framesis 36; a dropped block leaves the console waiting forever. - Stalls while the console sends follow the mirror rule.
Modules
Under pokeldn/frlg/gift/ unless stated:
| file | role |
|---|---|
mg_link.py | framing: 6-byte {ident, crc, size} header block + ≤252-byte chunks |
mg_script.py | client-script assembler, the decomp’s canned scripts, MysteryGiftLinkGameData reader |
mg_server.py | server-script interpreter (SVR_*) |
host_mystery_gift.py | leader engine: tick() → parent gSendCmd, feed_child_slot() ← child row |
host_mg_app.py | application hooks over the host runtime |
wonder_card.py | byte-exact Celebi and legendary-beast card/RAM-script builders |
stamp_rally.py | Stamp Rally card, stamps, activation wrappers, delivery script |
gift_composer.py | action definitions, cursor-state validation, RAM-script compiler |
gift_registry.py | the catalog |
gift_to_bin.py | .bin exporter for external Gen-3 Mystery Gift tools |
pokeldn/frlg/save/save_inject.py | save gift writing with card, RAM-script and sector checksums |
bin/frlg_mg_host.py | the CLI |
Running it
(them) Mystery Gift -> Wonder Cards (Recevoir) -> Friend (Ami), wait on the search screen
(you) POKELDN_RADIO=esp32:auto ./.venv/bin/python -u bin/frlg_mg_host.py --live --phy auto \
--keys PROD_KEYS --gift beast-cutscene --flag-id 1005
(them) join the host when it appears; YES on the replace-card prompt if one shows
Radio setup: The ESP32 radio. One console per run; the host stops once it has left LDN (Host implementation). tests/test_mystery_gift_flow.py and tests/test_mystery_gift_end_to_end.py model the console’s receive path.
--save-to-board stores the gift on the ESP32 board instead and exits; powered from any USB charger, the board then plays this session itself for every console that joins, one at a time, with each cartridge’s own bytes (A host image).
(you) POKELDN_RADIO=esp32:auto ./.venv/bin/python -u bin/frlg_mg_host.py --live \
--keys PROD_KEYS --gift celebi --save-to-board
What the link can carry
Three of the 22 client instructions [include/mystery_gift_client.h:18] execute something:
| instruction | what the console does |
|---|---|
CLI_SAVE_RAM_SCRIPT (17) | stores a field script; it runs on the next NPC interaction |
CLI_RUN_MEVENT_SCRIPT (15) | runs a Mystery Event bytecode script (17-opcode VM) |
CLI_RUN_BUFFER_SCRIPT (21) | func = (void *)gDecompressionBuffer; func(¶m, gSaveBlock2Ptr, gSaveBlock1Ptr); up to 1024 bytes of ARM |
The one RAM script slot
A console holds a Wonder Card or a bound RAM script, never both. ValidateSavedWonderCard ends in ValidateRamScript [mystery_gift.c:186], which requires RAM_SCRIPT_MAGIC, map MAP_UNDEFINED and objectId == 0xFF [script.c:538]; the field runs one only at the coordinates of the object talked to [field_control_avatar.c:458]. CLI_SAVE_RAM_SCRIPT writes MAP_UNDEFINED/0xFF [script.c:578]; the Mystery Event VM’s initramscript writes real coordinates. With a bound script the card stays in the save but the menu hides it and GameData reports flagId 0 [mystery_gift.c:349].
The next Wonder Card rebinds the slot and the card comes back; a buffer script leaves the slot alone. A card rewrites SaveBlock1 +0x32E0 (card) and +0x361C (RAM script), one sector, nothing in SaveBlock2.
The save itself can carry the same pair: pokeldn/frlg/save/save_inject.py writes a valid card and a RamScript into a raw save (inject_gift, build_ram_script_struct), so a save imported over a Wonder Card runs the script at its NPC with no live host. The script’s callnative opcode reaches guest ARM, and the resident-save gift uses it to bind MOM’s loader.
ramScript.checksum is CalcCRC16WithTable over 1000 bytes, the 999 declared and one zeroed padding byte [script.c:500] (CalculateRamScriptChecksum 0x0806D43C BPRE, 0x0806D5A0 BPRF). A slot whose CRC covers 999 bytes is wiped the first time its object is talked to [script.c:526].
The gift catalogue
--gift NAME; --help lists flag ids (1000..1019). Only the held card matters: the same id means “already has this card” (MysteryGift_CompareCardFlags). A console already holding the card takes a Mystery Event gift alone, with no prompt.
| gift | what it does |
|---|---|
beast-cutscene-share | the repeatable legendary-beast cutscene; the default demo card |
celebi | the composed level-50 Celebi card |
porygon-tm-gift | a Porygon card, a Clefairy scene, TM29 Psychic then TM46 Thief |
solrock-stamp / lunatone-stamp | the two halves of one Stamp Rally card |
altering-cave | the official Altering Cave event, ported |
wish-egg, pokepark-egg, pc-japan-egg | the official distribution eggs |
event-pokemon | a Gen 3 distribution Pokemon, straight into the party |
starter-egg | an egg of one of the nine first partners, drawn by random |
rare-berries | an Enigma, a Lansat and a Starf Berry, one stage each |
national-dex | EnableNationalPokedex (special 367) unless IsNationalPokedexEnabled (403) answers 1 |
nature-mint, pc-anywhere and 42 more | the GB-Link Team cards |
battle-count-card | the official Battle Count Card |
visiting-trainer | a Battle Tower trainer as ident 26 (FireRed only) |
mystery-event-probe | givenationaldex; setstatus 42; checksum, the VM’s self-test |
mystery-event-celebi | givepokemon: a Lv30 Celebi holding mail, straight into the party |
mystery-event-npc | initramscript: binds a field script to the Pallet Town fat man |
rng-seed-reader, rng-rate-probe, rng-shiny-hunt, rng-mon-hunt, rng-mon-hunt-both, rng-mon-hunt-log | see the RNG |
The legendary beast
A deliveryman gives the Lansat and Liechi Berries and a Master Ball, then starts a repeatable level-65 battle: Suicune for a Bulbasaur save, Entei for Squirtle, Raikou for Charmander.
Porygon TM gift
A Porygon-icon card and a Clefairy sprite three tiles right of the player give TM29 then TM46 at separate checkpoints. Default flag id 1007; the viewer shows flag_id % 100.
The Stamp Rally
Two events share one SUN AND MOON RALLY card (Claydol icon, two stamp slots, flag id 1006), in either order. Each stamp gives a level-30 Solrock or Lunatone; both give a level-50 Celebi.
| state | meaning |
|---|---|
VAR_MYSTERY_GIFT_1 | Celebi completion cursor |
VAR_MYSTERY_GIFT_2 = 0/1/2 | Solrock absent / active / received |
VAR_MYSTERY_GIFT_3 = 0/1/2 | Lunatone absent / active / received |
FLAG_MYSTERY_GIFT_DONE | rally completion |
matching = saved flag ID == distribution flag ID and max stamps == 2 and icon species == CLAYDOL
if no card: install shared card + delivery script + selected stamp, run activation
else if not matching: offer the toss prompt; on accept, install as above
else if stamp species or ID already exists: HAS_STAMP
else if neither slot empty: NO_ROOM_STAMPS
else: save the stamp, run activation, STAMP_RECEIVED
The activation is a Mystery Event wrapper. IsStampInMetadata [mystery_gift.c:272] rejects a colliding stamp; CLI_SAVE_STAMP writes only cardMetadata.stampData [mystery_gift.c:307].
Altering Cave
The official script [data/mystery_event_msg.s:325] adds 1 to VAR_ALTERING_CAVE_WILD_SET (0x4024, SaveBlock1 + 0x1048), wrapping at 10; values 9 and above read table 0 [wild_encounter.c:192].
| var | species | var | species | |
|---|---|---|---|---|
| 0 | Zubat | 5 | Aipom | |
| 1 | Mareep | 6 | Shuckle | |
| 2 | Pineco | 7 | Stantler | |
| 3 | Houndour | 8 | Smeargle | |
| 4 | Teddiursa |
Distribution eggs
Three Japanese distributions, from GB-Link-Switch-LDN’s web/js/gift/official.js. The card picks one egg with random [scrcmd.c:455]; it gets the distribution’s moves, the fateful-encounter bit and met location 0xFF. A full party refuses before the draw.
| card | eggs |
|---|---|
wish-egg (Pokemon Center New York) | Chansey, Drowzee, Exeggcute, Farfetch’d, Kangaskhan, Lickitung; each knows Wish |
pokepark-egg (PokePark Market Fantasia) | Cacnea, Corphish, Corsola, Igglybuff, Minun, Pichu, Plusle, Psyduck, Skitty, Spinda, Spoink, Surskit, Taillow, Whismur, Wynaut |
pc-japan-egg (Pokemon Center Japan) | Bellsprout (Teeter Dance), Meowth (Petal Dance), Oddish (Leech Seed), Poliwag (Sweet Kiss) |
Event Pokemon
--gift event-pokemon --event-pokemon NAME sends a Gen 3 distribution made by PKHeX.Core from EncounterGift3 (non-egg, non-Japanese) by its PID/IV method, legality-checked, through the Mystery Event givepokemon when the card is saved. A full party answers status 3 and the card can be received again. The default is the WISHMKR Jirachi.
NAME is trainer name and species: WISHMKR Jirachi, CHANNEL Jirachi, Aura Mew, MYSTRY Mew, DOEL Deoxys, SPACE C Deoxys, ROCKS Metang, every 10 ANIV species, and the European 10ANNIV, 10JAHRE, 10ANNI and 10ANIV releases; --language picks among languages.
The card is build-independent; tests/test_frlg_english_cartridges.py runs it through each cartridge’s RunMysteryEventScript.
GB-Link Team cards
The GB-Link Team’s custom cards (GB-Link-Switch-LDN cards/, GPL-3.0) are a Wonder Card plus a RAM script that calls THUMB code through callnative. Sources are in vendor/gblink-cards/; scripts/gen_team_cards.py builds pokeldn/frlg/data/team_cards.json, reproducing all 44 of their BPRE 1.10 payloads byte for byte, and team_cards.py registers each id without custom-. starter-egg, rare-berries (items 173-175) and national-dex (FLAG_SYS_NATIONAL_DEX 0x840) are rebuilt with the composer.
| group | cards |
|---|---|
| change a Pokemon | nature-mint, ability-capsule, poke-ball-changer, pokemon-gender, nickname, stat-judge, hidden-power, hidden-power-type, ev-training, friendship, pp-max, max-conditions, pokerus, unown-letters, trade-evolution, espeon-umbreon, move-tutor |
| per-frame hooks | speed-2, speed-3, speed-4, speed-0-75, speed-0-5, fast-text, travel-anywhere, pc-anywhere, hm-moves, reusable-tms, physical-special-split, exp-share, shiny-hunting, roamer |
| other | no-encounters, legendary-respawn, instant-eggs, gift-box, pocket-casino, gift-ribbons, trainer-ids, gender-swap, rival-name |
| event Pokemon | box-eggs, colosseum-pikachu, ageto-celebi, mattle-ho-oh |
Differences from their build:
- All twelve revision
0x0Acartridges:vendor/gblink-cards/symbols.jsonholds the 167 addresses per ROM,tests/test_team_cards.pychecks them, and each script checks the header’s game, language and revision (The cartridge maps). - The relocated script (996 bytes) and menu list (80 bytes) live at
0x0203F768and0x0203FB50, below the resident hooks at0x0203FC00(Where a payload can live). - Hook installers chain to
VBlankIntr, stored at0x0203FBFC, so a hook card and a game boost replace each other. Their state is at0x0203FF60. - Ids above 1019 have no
sReceivedGiftFlagsbit; the registry sends 1000 + id mod 20. hm-movesandphysical-special-splittexts are shortened to fit 995 bytes.
Every card runs bound to Mom under mGBA on all twelve cartridges. On the other game’s cartridge a payload answers “This gift doesn’t work with this version of the game.” (“Wrong game.” in Japanese). colosseum-pikachu and ageto-celebi carry Japanese trainer names, drawn as dots on a European cartridge.
The Battle Count Card
MysteryEventScript_BattleCard [data/mystery_event_msg.s:162] gives a POTION at exactly three GET_CARD_BATTLES_WON (special 390); the port gates on its own var, so it repeats. The counters advance only if the u16 after the 96-byte trainer card equals the held card’s flag id [union_room.c:1777] (frlg_trade_host.py --card-flag-id N).
| what increments | where | the rule |
|---|---|---|
numTrades | a completed trade [trade_scene.c:2609] | a trainer id the card has not counted |
battlesWon / battlesLost | a cable club battle [cable_club.c:792] | the same, 5 ids per stat |
Union Room battles count nothing; only the colosseum does. The counters sit at SaveBlock1 + 0x3434 with no CRC [mystery_gift.c:490]:
0x3434: 0000 0000 0100 2300 trades 1, icon 35 (CARD_TYPE_LINK_STAT)
A CARD_TYPE_GIFT card stays at zero even when the special runs.
The visiting trainer
CLI_RECV_EREADER_TRAINER (18, ident 26) copies the buffer into gSaveBlock2Ptr->battleTower.ereaderTrainer [mystery_gift_client.c:233]. The struct is 188 bytes [global.h:286]:
0x00 u8 unk0 0x10 u16 greeting[6] 0x34 BattleTowerPokemon party[3]
0x01 u8 trainerClass 0x1C u16 farewellPlayerLost[6] 0xB8 u32 checksum
0x02 u16 winStreak 0x28 u16 farewellPlayerWon[6]
0x04 u8 name[8]
0x0C u8 trainerId[4]
Valid when the first 46 words are not all zero and the u32 is their sum [battle_tower.c:1354, :1384]. The old woman in SevenIsland_House_Room1 then offers a repeatable 3v3 built by CreateBattleTowerMon [battle_tower.c:928]; no level rule or banlist applies. FRLG shows five name bytes [battle_tower.c:1343]. --gift visiting-trainer sends card, RAM script and trainer together; with the same card held, the trainer alone.
Wonder News
struct WonderNews [global.h:646] is 444 bytes:
| offset | size | field | notes |
|---|---|---|---|
| 0x000 | 2 | id | must not be 0, the only check [mystery_gift.c:113] |
| 0x002 | 1 | sendType | SEND_TYPE_DISALLOWED hides “Send” [mystery_gift.c:120] |
| 0x003 | 1 | bgType | >= NUM_WONDER_BGS reads 0 [mystery_gift_show_news.c:110] |
| 0x004 | 40 | titleText | centred in a 224 px window |
| 0x02C | 400 | bodyText[10][40] | eight lines on screen; a line past index 7 turns on the scroll indicator [:346] |
IsWonderNewsSameAsSaved compares all 444 bytes [mystery_gift.c:140] (--news-id N makes news new). News and cards never displace each other.
build_wonder_news_app_dataadvertises activity 22 [src/data/union_room.h:406].- The console answers
MG_LINKID_RESPONSE(ident 19):FALSEsaved,TRUEalready held [mystery_gift_client.c:210; mystery_gift_scripts.c:126]. - News from a Friend rolls a berry between
ITEM_RAZZ_BERRYandITEM_NOMEL_BERRY, given by the man inCeruleanCity_House4, up to five, then 500 steps [mystery_gift_menu.c:1367, wonder_news.c:21]. The four-berry reward needs the closed Wireless path.
--news (--news berry, --news-id N). Message order (about 18 s):
ident 16 sClientScript_SendGameData
ident 17 MysteryGiftLinkGameData
ident 16 sClientScript_SaveNews
ident 23 MG_LINKID_NEWS - 444 bytes in three blocks
ident 19 MG_LINKID_RESPONSE - FALSE: the console saved it
ident 16 sClientScript_NewsReceived
ident 20 READY_END -> SVR_MSG_NEWS_SENT
The questionnaire as a password gate
SVR_CHECK_QUESTIONNAIRE compares the four Poke Mart questionnaire words [mystery_gift.c:422]; mg_server.gate_on_questionnaire(script) splices it after the game-data prefix:
bin/frlg_mg_host.py --gift ... --questionnaire species:55,FEELINGS/60,move:177,why
A word is an English name, species:N, move:N, GROUP/INDEX or a raw id. A wrong phrase gets the host’s 64-byte message (CLIENT_SCRIPT_DYNAMIC_ERROR) and nothing is sent. Every session logs the console’s four words from GameData [mystery_gift.c:361]:
questionnaire: POKEMON/55 done [FEELINGS/60] MOVE_1/177 why [MISC/37]
EC_GROUP_POKEMON indexes by species, EC_GROUP_MOVE_1 by move id; see the French Easy Chat vocabulary.
What the console volunteers about itself
GameData carries the Easy Chat profile and the card stats [mystery_gift.c:361]. --game-data-log PATH (game_data_log.py) appends each session to a JSONL ledger and prints what moved since that console’s last one; tools/frlg/game_data_read.py PATH reads it. A counter is evidence only as a difference on the same card flag id.
Save backup and restore
A Wonder Cards, Friend session copies the console’s whole 128 KiB save chip to the host, or writes a .sav onto it and makes the game load and save it; no card is sent. asm/save-backup.s and asm/save-restore.s are ported from the GB-Link Team’s cards/savebackup.s and cards/saverestore.s (GPL-3.0); the host is pokeldn.frlg.gift.save_transfer (--save-backup FILE, --save-restore FILE), and the app’s Your save tab (Your saves). Both run on all twelve cartridges (Save backup and restore).
The token coding
A byte n < 0x80 is followed by n + 1 literal bytes; n >= 0x80 by one byte repeated n - 0x80 + 3 times (runs of 3 to 130, literals up to 128).
Backup
The client script repeats CLI_LOAD_TOSS_RESPONSE, CLI_RUN_BUFFER_SCRIPT, CLI_SEND_LOADED up to 32 times per script. Each pass sends up to 1 KiB of tokens for at most 8 KiB of the chip, never across the 64 KiB bank edge; the chip offset is client->param = 0x5A << 24 | offset [mystery_gift_client.c:276], and a param without 0x5A starts at first.
| payload word | offset | value |
|---|---|---|
first | 0x004 | the chip offset the first pass starts at |
send_queue | 0x008 | &gRfu.sendQueue.count |
A pass stages 0x800 bytes a frame into gDecompressionBuffer + 0x800, compresses into + 0x400, and sets link.sendBuffer (param + 0x3C) and link.sendSize (param + 0x34). It waits while gRfu.sendQueue.count is not zero. The session ends on CLI_MSG_BUFFER_FAILURE with a message, so the console does not save [mystery_gift_menu.c:1379]. A cut-short backup resumes by game code and trainer id.
Restore
- The first message is the 620-byte image;
installcopies it togDecompressionBuffer + 0x400and answers with the 12-byte footers (id, checksum, signature, counter at+0xFF4) of the 28 slot sectors. - The host finds the newest whole slot as
GetSaveValidStatuswould and writes the file’s copy into the other slot with counternewest + 1, then sectors 28 to 31 as the file has them. - It erases the target slot’s id-0 sector first and writes the new id 0 last, so the chip’s own copy loads until the new one is whole.
- Later messages start
b RESIDENT + 4(0xEA0000FF), then an op.OP_DATA(1) carries sector, write flag, u16 offset, u16 token length and tokens into a 4 KiB buffer; the sector’s last message writes it withswi 0x48and verifies it, setting a fail-mask bit on mismatch. OP_FINISH(2) callsLoadGameSave(SAVE_NORMAL)if nothing failed [save.c:803]. OnSAVE_STATUS_OKthe session ends onCLI_MSG_BUFFER_SUCCESSand the console saves the loaded game; otherwise onCLI_MSG_BUFFER_FAILUREand the old save stays.
| payload word | offset | value |
|---|---|---|
b install | 0x000 | the first message’s entry |
b entry | 0x004 | every later message’s entry, at gDecompressionBuffer + 0x404 |
load_game_save | 0x008 | LoadGameSave \| 1 |
The host refuses a save whose loaded copy is not whole or of the other layout: Japanese SaveBlock1 is 40 bytes shorter, read from the language byte (+0x12) of the trainer’s own Pokemon.
tests/test_save_transfer.py covers backup, resume, restore and an interrupted restore against the scripted console. On retail, backup works on French and English FireRed, and restore on English FireRed, a French save included. The console’s next save differs from the file in sectors 1 to 7, which carry the encryption key LoadGameSave re-rolls.
Authoring gifts
pokeldn.frlg.gift.gift_composer builds cards and delivery scripts from WonderGift definitions:
MEWTWO_GIFT = WonderGift(
slug="mewtwo-encounter",
card=WonderCardSpec(icon_species=150, title="MYSTERIOUS ENCOUNTER",
body=("Visit the deliveryman.",), default_flag_id=1008),
intro_message="A powerful presence is waiting!",
event=GiftSpec(shareable="once"), # or StampRallySpec(...)
delivery=DeliveryPlan(delivery=(
DeliveryStage(Message("Take this."), GiveItem(1)), # Master Ball
DeliveryStage(ShowSprite(0, RelativeToPlayer(dx=1)), BattleLegendary(150, level=70)),
)),
completed_message="That mysterious encounter is over.",
)
WonderGift.delivery uses delivery; StampSlot.delivery and StampRallySpec.completion use pre_stages and post_stages. The compiler resumes stages from VAR_MYSTERY_GIFT_1 and on success sets FLAG_MYSTERY_GIFT_DONE and the receipt flag; GiftSpec(repeatable=True) resets the cursor.
Stages and conditions
Each DeliveryStage is one checkpoint; never put two fallible rewards (GiveItem, GivePokemon, GiveEgg) in one stage. A move-bearing GiveEgg needs a party slot. GiveRandomEgg(eggs) picks one through a jump table (fifteen four-move eggs fit in 944 bytes). condition= (VarEquals, FlagSet, Not, AllOf, AnyOf) skips a stage but advances the cursor; RequireSpecialResult(...) stops on failure without advancing.
DeliveryStage(ShowSprite(142, RelativeToPlayer(dx=1)),
condition=VarEquals(0x4031, 0)) # VAR_STARTER_MON == Bulbasaur
DeliveryStage(RequireSpecialResult(SPECIAL_HAS_ALL_KANTO_MONS, 1, "Finish the KANTO POKEDEX first."),
GivePokemon(251, level=50))
SetVar (setvar 0x16) and AddVar (addvar 0x17) accept saved vars 0x4000..0x40FF and special vars 0x8000..0x8011.
Battles
BattleLegendary emits setwildbattle, special StartLegendaryBattle, end with no waitstate (a RAM script may not come back from a battle); BattlePokemon emits dowildbattle. Either ends its stage. Battles are refused in a stamp-slot path except as conditional terminal alternatives.
Sharing
GiftSpec.shareable sets the card’s sendType: "never", "once" (the receiver’s copy becomes unshareable) or "always".
Fateful-encounter marking
GivePokemon(..., fateful_encounter=True) (and GiveEgg) emits setmonmodernfatefulencounter (0xCD) and setmonmetlocation (0xD2, 0xFF) [data/mystery_event_msg.s:71]. The first command does not bounds-check its index [scrcmd.c:2239]; the index is the party count before the give, and a full party jumps to the failure label. The summary line comes from the met location alone [pokemon_summary_screen.c:2665, 2799]; modernFatefulEncounter is bit 31 of Misc+0x08 [include/pokemon.h:40-82].
initramscript in the composer
gift_composer.build_bound_script(actions) and build_mevent_npc_script(actions=...) compile actions into the field script initramscript binds. It has no stage cursor and reruns whole, so a once-only effect needs its own SetVar or condition.
Registration and validation
from pokeldn.frlg.gift.gift_registry import GIFT_REGISTRY
GIFT_REGISTRY.register_definition(MEWTWO_GIFT)
Registration validates and compiles: card text and flags, plan structure, action ranges, cursor bounds, unique stamps, battle placement, pointers and the 995-byte RAM-script limit, with errors naming the path (example-rally.event.slots[1].delivery.post_stages[0].actions[1]: ...).
Static tools
./.venv/bin/python -m pokeldn.frlg.gift.gift_to_bin --gift beast-cutscene --flag-id 1005 --out-dir exported-gift
./.venv/bin/python -m pokeldn.frlg.save.save_inject game.sav --gift beast-cutscene --flag-id 1005
gift_to_bin writes a 336-byte Wonder Card and a 1004-byte RAM script for pokemon-gen3-mysterygift-tool. save_inject writes both into the active slot, fixes the card, RAM-script and sector checksums, and saves <save>.gift.sav (--in-place overwrites; --make-artifact writes a .ram.lst listing under artifacts/).
Closed paths
Wireless Communication (JoySpot)
The Wireless path is blocked at the RFU serial-number gate:
| Friend | Wireless Communication | |
|---|---|---|
| listener | Task_ListenForCompatiblePartners [union_room.c:3757] | Task_ListenForWonderDistributor [union_room.c:3799] |
| accepts RFU serial | {0x0002, 0x7F7D} | == 0x7F7D only [link_rfu_3.c:920] |
| selection | the player picks from a list | auto-connects |
| reachable from a Switch | yes | no |
Task_CardOrNewsOverWireless [union_room.c:2415] keeps candidate 0 only if its serialNo is 0x7F7D, then checks the spawn state, the hasCard bit (SE_BOO on failure) and reconnects. The Switch bridge reports 0x0002, and the advertisement has no serial field; svc_47 [sloopsvc.c:34] takes 24 bytes with no serial. A native advertisement:
50 10 | c1 cc bf bf c8 ff 00 00 | 65 ac | 00 00 00 00 | 84 15 | 00 00 00 00 00 00
TID | uname | parent| unexplained | search| unexplained
The search word at record[16:18] is activity:7 | bit7 | version:3 | language:3 | hasCard?:1 | startedActivity:1.
No tried advertisement variant (scene id, app versions, 0x7F7D at every free offset, activity, hasCard, bit 7) drew an authentication from the Wireless path. Untested: local_communication_id, a scene brute force, combinations.
The e-Reader itself
Trainer Tower e-Reader sets use LINKTYPE_EREADER_FRLG over the GBA serial link (ereader_screen.c), not the wireless adapter.
The Aurora and Mystic Tickets
The Switch release grants both tickets and their FLAG_RECEIVED_* flags on the first Hall of Fame entry [post_battle_event_funcs.c:52, REVISION >= 0xA], so a ticket card gives nothing after it. A card’s iconSpecies other than SPECIES_NONE draws an icon; past SPECIES_UNOWN_B - 1 it draws the question mark [mystery_gift_show_card.c:466, pokemon_icon.c:1102]. The Old Sea Map is Emerald-only [mystery_gift.c:30].
Traps
charmap.encodedrops unknown characters, newline included. The line break is 0xFE;mg_serverrefuses a third line or one wider than 31 characters [strings.c:1291] (window 28 tiles by 4, [mystery_gift_menu.c:97,524]).- A new payload goes into
DUMP_SCRIPTSandDECODED_SCRIPTSinbuffer_script.pyand the launcher’s--dump-fileline.