-- `callasm` / `memcall` / `memcallasm` / `memjump`: the four script commands -- whose operand is not bytecode. -- -- Script_callasm (engine/overworld/scripting.asm) reads a three-byte far -- pointer out of the script and `rst FarCall`s it as raw Game Boy code. -- Script_memcallasm does the same with a pointer read out of WRAM instead; -- Script_memcall calls the WRAM pointer as bytecode, and Script_memjump jumps -- to it and never comes back. No interpreter can run any of them, so each -- target routine needs its own hand port, and this file is where they live. -- -- Dispatch is BY ADDRESS, the way src/script/gen2/Specials.lua dispatches by -- name. SITES maps the `bank:addr` pair the command carries -- resolved -- against pokegold-symbols/pokegold.sym -- onto a routine name, and HANDLERS -- maps that name onto the port. Keying on the pair rather than on a counted -- index means a routine that moved between revisions cannot silently call the -- wrong port; keying the port on the NAME means the table can be asserted -- against the symbol file by a test. -- -- WHAT wScriptVar DOES AND DOES NOT GET WRITTEN. Script_callasm itself never -- touches wScriptVar: it is the TARGET routine that does, and only eleven of -- them do. A handler therefore returns a byte only when its asm ends in -- `ld [wScriptVar], a`, and nil otherwise -- nil meaning "leave it alone". -- Inventing a 0 or a 1 for the rest would pick a branch at random for the -- `callasm` / `iffalse` pairs (FindItemInBallScript and FruitTreeScript are -- both one), which is exactly the bug this contract exists to prevent. -- -- WHAT IS ACTUALLY IN THE CACHE: nothing. Thirty-eight rows in -- data/generated/scripts.lua used to carry one of these four opcodes and not -- one of them was real -- every one sat inside a key the extractor had made -- out of a three-byte `hiddenitem` bg_event operand (45:697a is -- IlexForestHiddenEther, 47:40a5 is WhirlIslandB1FHiddenRareCandy) and then -- disassembled as if it were code. A cache built since the extractor stopped -- walking those operands has none at all, and a stale one still misses SITES -- because its bank/address pairs are noise; tests/gen2_callasm_test.lua -- asserts that neither resolves. -- -- The real sites are the fifty-seven in engine/*.asm listed below. None of -- those engine scripts is reachable from a map script pointer, so none of them -- is extracted either, and the port reaches their routines through the -- hand-ported flows in src/world/gen2/ -- which is why CallAsm.run is a public -- entry point beside CallAsm.dispatch. Both roads end at the same table, -- which is the point of having one. -- -- love-free: a handler takes the World (or anything shaped like it) and calls -- back into the module that already owns the behaviour. Nothing here -- re-transcribes a table that has a home elsewhere. local Phone = require("src.core.gen2.Phone") local CallAsm = {} -- ---- the site table -------------------------------------------------------- -- -- Every `callasm` / `memcall` / `memcallasm` / `memjump` operand in the cart, -- resolved against pokegold-symbols/pokegold.sym. The comment on each group -- is the file the site is IN; the name is the routine the pointer reaches. CallAsm.SITES = { -- engine/overworld/scripting.asm: GiveItemScript's opening no-op. ["25:6e71"] = "GiveItemScript_DummyFunction", -- engine/overworld/events.asm: the START and SELECT presses, the queued -- script the overworld hands itself, the egg hatch and the facing change. ["04:6994"] = "StartMenu", ["04:76e9"] = "SelectMenu", ["05:7312"] = "OverworldHatchEgg", ["25:664f"] = "EnableWildEncounters", -- engine/phone/phone.asm and engine/events/mom_phone.asm. ["24:4277"] = "RingTwice_StartCall", ["24:42df"] = "HangUp", ["04:5800"] = "InitCallReceiveDelay", ["24:4264"] = "LoadBillScript", ["24:4272"] = "LoadElmScript", ["3f:4db2"] = "MomTriesToBuySomething_ASMFunction", -- engine/items/itemfinder.asm. ["04:6960"] = "ItemfinderSound", -- engine/events/fruit_trees.asm. ["11:4041"] = "GetCurTreeFruit", ["11:404c"] = "TryResetFruitTrees", ["11:4055"] = "CheckFruitTree", ["11:405f"] = "PickedFruitTree", -- engine/events/sweet_scent.asm. ["14:4725"] = "SweetScentEncounter", -- engine/events/trainer_scripts.asm. ["02:490e"] = "TrainerWalkToPlayer", -- engine/events/squirtbottle.asm. ["14:4786"] = "CheckCanUseSquirtbottle", -- engine/events/hidden_item.asm. ["04:7a11"] = "SetMemEvent", -- engine/events/poisonstep.asm. ["14:468e"] = "PlayPoisonSFX", ["14:46b1"] = "CheckWhitedOut", -- engine/events/whiteout.asm. ["04:68c7"] = "OverworldBGMap", ["04:68d7"] = "BattleBGMap", ["04:68e0"] = "HalveMoney", ["04:68ee"] = "GetWhiteoutSpawn", -- engine/events/overworld.asm: the field moves. ["03:474b"] = "GetPartyNickname", ["03:4855"] = "CutDownTreeOrGrass", ["23:4a6b"] = "BlindingFlash", ["00:310a"] = "HideSprites", ["23:4d65"] = "FlyFromAnim", ["05:560c"] = "SkipUpdateMapSprites", ["23:4dab"] = "FlyToAnim", ["05:415c"] = "LoadWalkingSpritesGFX", ["03:4b49"] = "CheckContinueWaterfall", ["03:4d15"] = "SetStrengthFlag", ["03:4d7b"] = "TryStrengthOW", ["03:4e20"] = "DisappearWhirlpool", ["23:4a8e"] = "ShakeHeadbuttTree", ["03:4f7f"] = "HasRockSmash", ["03:5096"] = "PutTheRodAway", ["03:506d"] = "Fishing_CheckFacingUp", ["14:454f"] = "LoadFishingGFX", ["03:51c7"] = "AskCutScript_CheckMap", -- engine/events/treemons.asm, reached from the HEADBUTT and ROCK SMASH arms -- of engine/events/overworld.asm. ["2e:6378"] = "TreeMonEncounter", ["2e:63a1"] = "RockMonEncounter", -- engine/events/misc_scripts.asm. ["04:66d1"] = "TryReceiveItem", } -- The three WRAM addresses `memcall` / `memcallasm` / `memjump` take instead of -- a routine. Not sites: the pointer AT the address is written at run time -- (LoadMemScript for the queued script, the phone engine for the other two), -- so there is nothing static to resolve and nothing to port. Kept so a test -- can tell "a WRAM operand" apart from "a routine nobody has named yet". CallAsm.MEM_OPERANDS = { ["00:cfd8"] = "wQueuedScriptBank", ["00:ceed"] = "wPhoneScriptBank", ["00:cf2a"] = "wCallerContact", } -- ---- constants ------------------------------------------------------------- -- constants/map_object_constants.asm. The port already keeps -- SPRITEMOVEDATA_STRENGTH_BOULDER ($19) in World; this is its neighbour. local SPRITEMOVEDATA_SUDOWOODO = 0x17 -- constants/sfx_constants.asm, resolved by LABEL at call time the way -- src/world/gen2/HiddenItems.lua does it; the numbers are only the fallback -- for a cache whose sfx table sits somewhere else. local SFX_POISON = { "Sfx_Poison", 11 } local SFX_SECOND_PART_OF_ITEMFINDER = { "Sfx_SecondPartOfItemfinder", 18 } local SFX_TRANSACTION = { "Sfx_Transaction", 34 } local SFX_CALL = { "Sfx_Call", 106 } -- ItemFinder.ItemfinderSound's `ld c, 4`. local ITEMFINDER_SFX_LOOPS = 4 -- data/maps/spawn_points.asm. SPAWN_HOME is the first row, and it is what -- GetWhiteoutSpawn falls back to when IsSpawnPoint says no. local SPAWN_HOME = "SPAWN_HOME" -- ---- ctx plumbing ---------------------------------------------------------- -- A handler talks to the World through its own methods, and every one of them -- is optional: a headless test hands in a table with two of them and the rest -- of the file still runs. That is the same bargain Specials strikes with its -- hook table, and it is what makes this dispatch testable without a map. local function call(ctx, name, ...) local fn = ctx and ctx[name] if type(fn) ~= "function" then return nil end return fn(ctx, ...) end local function saveOf(ctx) return ctx and ctx.game and ctx.game.save or nil end -- ---- the phone's caller-ID box --------------------------------------------- -- -- Phone_TextboxWithName is the one routine in this file that DRAWS, so it is -- the one place a handler reaches past the World's method table: the box is a -- state on src/core/StateStack.lua, pushed under the call's text pages the way -- the cart's box sits under the speech box. The require is lazy and the whole -- pair is a no-op without a stack, so the "love-free" bargain at the top of -- this file still holds for every headless caller. -- -- The state is tagged rather than tracked in a local: a handler is dispatched -- from wherever the script happens to be running, and a module-level handle -- would outlive a save reload or a second world. local CALLER_BOX = "gen2CallerBox" local function stackOf(ctx) return ctx and ctx.game and ctx.game.stack or nil end -- GetCallerClassAndName reads wCurCaller (engine/phone/phone.asm:471-475), and -- vm.curPhoneCaller is this port's wCurCaller: World:receivePhoneCall and -- World:momTriesToBuy both park the PHONE_* contact there before the rows run. -- Contact 0 is the wrong-number script, which NonTrainerCallerNames answers -- for -- Phone.contactName already makes that distinction. local function showCallerBox(ctx) local stack = stackOf(ctx) if not stack then return end for _, state in ipairs(stack.states or {}) do if state[CALLER_BOX] then return end end local contact = (ctx.vm and ctx.vm.curPhoneCaller) or 0 local data = ctx.game and ctx.game.data local name, className = Phone.contactName(contact, data and data.trainers) local box = require("src.ui.gen2.CallerBox").new(name, className) box[CALLER_BOX] = true stack:push(box) end -- The box is the top of the stack when this runs, and StateStack:pop is what -- takes it off -- so `exit` and screen.popped fire the way they do for every -- other Gold screen. It CAN only be the top: src/core/Game2.lua's fixed step -- hands the tick to the top state and returns whenever that state has an -- `update`, so the VM this handler is running inside is not advancing at all -- while a text page is over the box. The by-identity search below is the -- guard for the day that stops being true (a mod screen pushed from a hook, -- say): the box comes off either way, because a caller box left on the stack -- would sit over the overworld forever with nothing to take it down. local function hideCallerBox(ctx) local stack = stackOf(ctx) local states = stack and stack.states if not states then return end for index = #states, 1, -1 do if states[index][CALLER_BOX] then if index == #states then stack:pop() else table.remove(states, index) end return end end end local H = {} -- ---- engine/overworld/scripting.asm --------------------------------------- -- GiveItemScript_DummyFunction is one `ret`. It exists so GiveItemScript -- opens on a command rather than on `writetext`, and the port keeps it named -- so the site is accounted for instead of falling through as unknown. function H.GiveItemScript_DummyFunction() return nil end -- ---- engine/overworld/events.asm ------------------------------------------ -- SelectMenuScript's `callasm SelectMenu` reaches UseRegisteredItem -- (engine/overworld/select_menu.asm), which is exactly World:useSelectItem: -- the four ITEMMENU_* arms plus CantUseItem's two "nothing registered" and -- "no field handler" cases. StartMenu, its neighbour in the same table, is a -- stub instead -- the START menu is a pushed screen, not a blocking call. function H.SelectMenu(ctx) call(ctx, "useSelectItem") return nil end -- HatchEggScript is `callasm OverworldHatchEgg / end`: the whole player event -- is this one routine. World:hatchEggs is HatchEggs -- (engine/pokemon/breeding.asm) with its four text beats. function H.OverworldHatchEgg(ctx) call(ctx, "hatchEggs") return nil end -- ---- engine/phone/phone.asm and engine/overworld/time.asm ----------------- -- RingTwice_StartCall: Phone_StartRinging's SFX_CALL and the flashing -- caller-ID box, twice over on a twenty-frame cadence. -- -- Both halves are here. The SOUND is the ring the player hears. The BOX is -- .CallerTextboxWithName -> Phone_TextboxWithName (engine/phone/phone.asm:466, -- :474, :582), the four-row strip across the top of the screen naming who is -- calling; src/ui/gen2/CallerBox.lua is its port and carries the layout. This -- port's textbox holds for A where the cart's PrintText returns, so the six -- twenty-frame flashes have nowhere to live -- the box simply goes up with the -- first ring and stays, which is the state the cart leaves it in anyway: it is -- written straight into wTilemap, CloseText restores it with everything else -- under the speech box, and only the overworld's own tilemap redraw at the end -- of the call takes it down (which is what InitCallReceiveDelay does below). -- -- Idempotent, because Script_ReceivePhoneCall rings TWICE and a second box -- would bury the first. ctx.game is absent in the pure-module tests and in -- any headless harness, and then the ring is all there is to do. function H.RingTwice_StartCall(ctx) call(ctx, "playSfxNamed", SFX_CALL[1], SFX_CALL[2]) showCallerBox(ctx) return nil end -- InitCallReceiveDelay (engine/overworld/time.asm): zero -- wTimeCyclesSinceLastCall and restart the receive countdown at its twenty -- in-game minutes. StartMap runs it on every map load (World:setMap -> -- Phone.onMapLoad) and Script_ReceivePhoneCall's tail runs it here, so a -- hung-up call arms the same timer a warp does. function H.InitCallReceiveDelay(ctx) -- The caller-ID box comes down here, and only here. Script_ReceivePhoneCall -- runs this as its LAST row (engine/phone/phone.asm:431-438), which is the -- moment the cart's overworld redraws its tilemap over the strip -- there is -- no routine that erases the box, so there is no row to hang the teardown on -- but this one. Ahead of the countdown, so a save-less harness still tidies -- the screen. hideCallerBox(ctx) local save = saveOf(ctx) if not save then return nil end local step = call(ctx, "stepContext") Phone.initReceiveDelay(save, step and step.phone) return nil end -- ---- engine/items/itemfinder.asm ------------------------------------------ -- .ItemfinderSound: four times round SFX_SECOND_PART_OF_ITEMFINDER then -- SFX_TRANSACTION, each through WaitPlaySFX so the pair does not overlap. -- The port plays them back to back; there is no channel to wait on. function H.ItemfinderSound(ctx) for _ = 1, ITEMFINDER_SFX_LOOPS do call(ctx, "playSfxNamed", SFX_SECOND_PART_OF_ITEMFINDER[1], SFX_SECOND_PART_OF_ITEMFINDER[2]) call(ctx, "playSfxNamed", SFX_TRANSACTION[1], SFX_TRANSACTION[2]) end return nil end -- ---- engine/events/fruit_trees.asm ---------------------------------------- -- -- All four read wCurFruitTree, which `fruittree tree_id` sets before it jumps -- to FruitTreeScript; `ctx.curFruitTree` is that byte. The item table itself -- lives in src/core/gen2/Apricorns.lua and is reached through World. -- GetCurTreeFruit: FruitTreeItems[wCurFruitTree - 1] -> wCurFruit. The `dec a` -- is the cart turning a 1-based FRUITTREE_* into a 0-based offset, which -- World:fruitTreeItem already undoes, so nothing is shifted here. function H.GetCurTreeFruit(ctx) local tree = (ctx and ctx.curFruitTree) or 0 ctx.curFruit = call(ctx, "fruitTreeItem", tree) or 0 return nil end -- TryResetFruitTrees: gated on DAILYFLAGS1_ALL_FRUIT_TREES, so the FIRST tree -- examined after the daily rollover refills every tree in the game. function H.TryResetFruitTrees(ctx) call(ctx, "fruitTreeReset") return nil end -- CheckFruitTree is `ld b, 2 / GetFruitTreeFlag`, a CHECK_FLAG over -- wFruitTreeFlags, and it puts the flag itself in wScriptVar. The flag means -- "already picked", so a CLEAR flag is the arm with fruit on it -- which is -- why FruitTreeScript reads it with `iffalse .fruit`. function H.CheckFruitTree(ctx) local tree = (ctx and ctx.curFruitTree) or 0 return call(ctx, "fruitTreePicked", tree) and 1 or 0 end -- PickedFruitTree is `ld b, 1 / GetFruitTreeFlag`: SET_FLAG, no wScriptVar. function H.PickedFruitTree(ctx) local tree = (ctx and ctx.curFruitTree) or 0 call(ctx, "fruitTreePick", tree) return nil end -- ---- engine/events/overworld.asm ------------------------------------------ -- GetPartyNickname copies wCurPartyMon's nickname into wStringBuffer1-3, which -- is what {STRBUF} reads back for " used CUT!". wCurPartyMon is -- whatever CheckPartyMove last left there, and `ctx.curPartyMon` is the port's -- name for the same slot. Five separate `callasm` sites reach it. function H.GetPartyNickname(ctx) local mon = ctx and ctx.curPartyMon if not mon then local save = saveOf(ctx) mon = save and save.party and save.party[1] or nil end call(ctx, "setNickname", mon) return nil end -- CutDownTreeOrGrass writes wCutWhirlpoolReplacementBlock over the block at -- wCutWhirlpoolOverworldBlockAddr, redraws, then plays OWCutAnimation. The -- port owns the pair as World:replaceBlock, which is also what drops the map's -- baked canvases -- a bake is keyed by map and daytime and knows nothing about -- the blocks it came from, so a changed block that skips it stays on screen. -- `ctx.cutWhirlpoolBlockIndex` / `ctx.cutWhirlpoolReplacement` are the port's -- wCutWhirlpoolOverworldBlockAddr and wCutWhirlpoolReplacementBlock: an index -- into the loaded map's block buffer rather than a pointer into it. function H.CutDownTreeOrGrass(ctx) local index = ctx and ctx.cutWhirlpoolBlockIndex local blockId = ctx and ctx.cutWhirlpoolReplacement if index and blockId then call(ctx, "replaceBlock", index, blockId) end return nil end -- DisappearWhirlpool is CutDownTreeOrGrass with PlayWhirlpoolSound in place of -- OWCutAnimation: the same block write, the same redraw. function H.DisappearWhirlpool(ctx) return H.CutDownTreeOrGrass(ctx) end -- BlindingFlash sets STATUSFLAGS_FLASH_F and reloads the palettes. Setting -- the flag is all there is to it here: Palettes.daytimeFor already turns a -- flashed PALETTE_DARK map into a NITE one, which is the cart's .UsedFlash -- arm, and the reload is the port's own baked-canvas drop. function H.BlindingFlash(ctx) if not ctx then return nil end ctx.flashUsed = true if call(ctx, "applyPalettes") then call(ctx, "refreshMapImages") end return nil end -- .CheckContinueWaterfall, the bottom of Script_UsedWaterfall's loop: TRUE -- when the tile the player has just climbed onto is ANOTHER waterfall tile, -- and the `iffalse .loop` reads it inverted -- 0 keeps climbing. function H.CheckContinueWaterfall(ctx) local coll = call(ctx, "playerCollision") local FieldMoves = require("src.world.gen2.FieldMoves") return FieldMoves.waterfallContinues(coll) and 1 or 0 end -- SetStrengthFlag: BIKEFLAGS_STRENGTH_ACTIVE, wStrengthSpecies from the mon -- CheckPartyMove picked, and then a tail call into GetPartyNickname. It runs -- FIRST, before the text, which is why the line that follows can name the mon. function H.SetStrengthFlag(ctx) ctx.strengthActive = true local mon = ctx and ctx.curPartyMon if mon then ctx.strengthSpecies = mon.species end return H.GetPartyNickname(ctx) end -- TryStrengthOW's three answers, and note the inversion in the cart: `bit -- BIKEFLAGS_STRENGTH_ACTIVE_F` jumps to .already_using when the bit is CLEAR, -- so 2 is the not-yet case and 0 the already-on one. -- 0 STRENGTH is already active -- 1 no mon knows it, or no PLAINBADGE -- 2 it may be switched on right now function H.TryStrengthOW(ctx) local FieldMoves = require("src.world.gen2.FieldMoves") local fieldCtx = call(ctx, "fieldContext") if not fieldCtx then return 1 end local result = FieldMoves.tryStrengthOW(fieldCtx) if result.ok then return 2 end if fieldCtx.strengthActive then return 0 end return 1 end -- .CheckMap, inside AskCutScript and reached only after the YES: it is -- CheckMapForSomethingToCut with the answer inverted into wScriptVar, so the -- `iftrue Script_Cut` that follows fires on 1. function H.AskCutScript_CheckMap(ctx) local FieldMoves = require("src.world.gen2.FieldMoves") local fieldCtx = call(ctx, "fieldContext") if not fieldCtx then return 0 end return FieldMoves.somethingToCut(fieldCtx) and 1 or 0 end -- HasRockSmash is INVERTED: `call CheckPartyMove / jr nc, .yes` puts 1 in -- wScriptVar when the party does NOT know ROCK SMASH, which is why -- AskRockSmashScript reads it with `ifequal 1, .no`. Transcribing it the -- obvious way round refuses the move for every party that has it. function H.HasRockSmash(ctx) local mon = call(ctx, "partyMoveUser", "ROCK_SMASH") if mon then -- engine/events/overworld.asm:1339 ctx.curPartyMon = mon return 0 end return 1 end -- PutTheRodAway: ClearBox over the text window, then wPlayerAction back to -- PLAYER_NORMAL so the fishing sprite drops. Two of the fishing scripts end -- on it and one has it in the middle, and in this port the whole rod pose is -- World's `fishing` state -- dropping it IS wPlayerAction going back to -- normal. It has to be gone before a battle is pushed, or World:busy would -- still be holding the world when the battle returns. function H.PutTheRodAway(ctx) if ctx then ctx.fishing = nil end return nil end -- Fishing_CheckFacingUp: `and $c / cp OW_UP`. The rod is only cast upward -- from a shore tile, and a 1 here is what lets the bite script run. function H.Fishing_CheckFacingUp(ctx) local player = ctx and ctx.player return (player and player.facing == "up") and 1 or 0 end -- ---- engine/events/treemons.asm ------------------------------------------- -- TreeMonEncounter: GetTreeMonSet / GetTreeMons / GetTreeMon, and -- BATTLETYPE_TREE plus wScriptVar = 1 only if all three came back with carry. -- The roll needs the tree's own cell, which HeadbuttScript left in the object -- coordinates; `ctx.curHeadbuttCell` is the port's name for the same pair. -- -- World:tryHeadbutt fuses the roll with the `startbattle` that follows it in -- the script, so a "battle" answer here is the 1 and everything else the 0. -- That fusing is why the port's own headbutt path calls tryHeadbutt directly -- rather than coming through this routine. function H.TreeMonEncounter(ctx) local cell = ctx and ctx.curHeadbuttCell if not cell then return 0 end return call(ctx, "tryHeadbutt", cell[1], cell[2]) == "battle" and 1 or 0 end -- RockMonEncounter is that same twin over RockMonMaps with a flat 40 percent -- (`ld a, 10 / RandomRange / cp 4`) where the tree has its coordinate score. -- It writes NO wScriptVar: RockSmashScript reads the answer back one row later -- with `readmem wTempWildMonSpecies / iffalse`, so returning a number here -- would decide a branch the cart decides some other way. World's own routine -- leaves the pair where that readmem can see it, and where the `randomwildmon` -- two rows further down takes it into the battle. function H.RockMonEncounter(ctx) call(ctx, "rockMonEncounter") return nil end -- ---- engine/events/sweet_scent.asm ---------------------------------------- -- SweetScentEncounter: the same CanEncounterWildMon gate a step takes, but -- every percentage roll downstream is skipped -- GetMapEncounterRate only has -- to come back nonzero. World:sweetScentEncounter is that whole routine. function H.SweetScentEncounter(ctx) return call(ctx, "sweetScentEncounter") and 1 or 0 end -- ---- engine/events/trainer_scripts.asm ------------------------------------ -- TrainerWalkToPlayer builds the movement that closes the gap and hands it to -- the applymovement that follows. The World owns the path, so this is the -- same seam the VM's `trainerapproach` branch uses. No wScriptVar. function H.TrainerWalkToPlayer(ctx) call(ctx, "trainerApproach") return nil end -- ---- engine/events/squirtbottle.asm --------------------------------------- -- .CheckCanUseSquirtbottle: Route 36, and the object the player is facing has -- to carry SPRITEMOVEDATA_SUDOWOODO. Anything else is a 0 and the item says -- nothing happened. GetFacingObject's own carry (nobody there) is the same 0. function H.CheckCanUseSquirtbottle(ctx) local map = ctx and ctx.map if not (map and map.id == "ROUTE_36") then return 0 end local player = ctx.player if not player then return 0 end local Map = require("src.world.gen2.Map") local delta = Map.DELTA[player.facing or "down"] or Map.DELTA.down local npc = call(ctx, "npcAt", player.cellX + delta[1], player.cellY + delta[2]) local def = npc and npc.def if def and def.movement == SPRITEMOVEDATA_SUDOWOODO then return 1 end return 0 end -- ---- engine/events/hidden_item.asm ---------------------------------------- -- SetMemEvent sets the flag whose number is sitting in wHiddenItemEvent, the -- word the `hiddenitem` bg_event operand carries beside the item. It runs -- AFTER the giveitem, so a full bag leaves the item on the map. -- -- wEventFlags is keyed by NUMBER: the operand is already one, and passing a -- name here would throw. function H.SetMemEvent(ctx) local flag = ctx and ctx.hiddenItemEvent if flag and ctx.events then ctx.events:set(flag, true) end return nil end -- ---- engine/events/poisonstep.asm ----------------------------------------- -- .PlayPoisonSFX: SFX_POISON, then LoadPoisonBGPals for two frames. The port -- plays the sound; the two-frame red flash is the renderer's business. function H.PlayPoisonSFX(ctx) call(ctx, "playSfxNamed", SFX_POISON[1], SFX_POISON[2]) return nil end -- .CheckWhitedOut walks wPoisonStepPartyFlags, applies HAPPINESS_POISONFAINT -- and prints one line per mon that actually dropped, and ends on -- CheckPlayerPartyForFitMon -- whose `d` is 1 when something can still fight. -- The `iffalse .whiteout` that follows is therefore reading "no fit mon". -- -- The happiness hit and the naming are World:poisonFaintScript's, because they -- have to be spread over as many text boxes as there were faints; what is left -- for this routine is the answer. function H.CheckWhitedOut(ctx) local StepEvents = require("src.world.gen2.StepEvents") local save = saveOf(ctx) local party = (save and save.party) or {} return StepEvents.whitedOut(party) and 0 or 1 end -- ---- engine/events/whiteout.asm ------------------------------------------- -- HalveMoney shifts wMoney right one bit as a 24-bit big-endian value: -- `srl a` on the high byte then `rra` twice, so the carry walks down and the -- whole three bytes end up halved with the remainder dropped. That is floor -- division for every value wMoney can hold, and the wallet ALONE -- Mom's -- savings are a separate three bytes the routine never reaches. function H.HalveMoney(ctx) local save = saveOf(ctx) local player = save and save.player if not player then return nil end player.money = math.floor((player.money or 0) / 2) return nil end -- GetWhiteoutSpawn validates wLastSpawnMapGroup / wLastSpawnMapNumber (the -- pair `blackoutmod` writes) against the SpawnPoints table and falls back to -- SPAWN_HOME when it is not one of them, which is what stops a whiteout at sea -- from respawning the player somewhere they cannot leave. -- -- Registered and ported, but NOT yet consumed: World:warpToSpawn prefers the -- blackout map itself over the SPAWN_* row it matches, which is a deliberate -- divergence tests/gen2_world_test.lua pins. The spawn id is left on the ctx -- for the day that changes. function H.GetWhiteoutSpawn(ctx) local save = saveOf(ctx) local override = save and save.blackoutMap local spawns = ctx and ctx.landmarks and ctx.landmarks.spawns local answer = SPAWN_HOME if override and type(spawns) == "table" then for id, row in pairs(spawns) do if type(row) == "table" and row.map == override then answer = id break end end end ctx.defaultSpawnpoint = answer return nil end -- ---- stubs ----------------------------------------------------------------- -- -- { name, wScriptVar or nil, reason }. A nil second field is the whole point -- for most of these: the asm writes no wScriptVar, so neither does the stub. local STUB_ROWS = { -- Graphics and VRAM. Every one of these is a tilemap, palette or sprite -- reload around an animation the port's renderer draws its own way; there is -- no VRAM here to load anything into. { "OverworldBGMap", nil, "ClearPalettes / ClearScreen / RotateThreePalettesLeft: the fade to white is the renderer's" }, { "BattleBGMap", nil, "SCGB_BATTLE_GRAYSCALE through GetSGBLayout: no SGB layout in this port" }, { "HideSprites", nil, "clears OAM for the FLY animation; the port hides the party sprite itself" }, { "FlyFromAnim", nil, "the bird's own frames; World:flyTo lifts the player under the setup script's fade instead" }, { "FlyToAnim", nil, "the landing frames; the same lift read backwards under the fade in" }, { "SkipUpdateMapSprites", nil, "suppresses one UpdateMapSprites while FLY is mid-air" }, { "LoadWalkingSpritesGFX", nil, "reloads the walking sprite bank after FLY lands" }, { "LoadFishingGFX", nil, "the rod and bobber tiles, which the extractor does not carry; World:updateFishing bobs the player instead" }, { "ShakeHeadbuttTree", nil, "the tree wobble frames; World:runHeadbutt owns the shake and the outcome" }, -- The START menu is a pushed screen with its own lifetime, not something a -- script coroutine can block on the way the cart's StartMenu does. { "StartMenu", nil, "the START menu is a Screens.push state, not a blocking call" }, -- The phone. These four write into wCallerContact / wPhoneScriptBank so -- that the `memcall` two lines later has somewhere to jump. The port's -- call descriptors carry their scripts.lua key instead (bank $41 is -- extracted through PhoneContacts now), so the pointer plumbing has nothing -- to store: src/core/gen2/PhoneRing.lua hands the VM the caller script -- directly, and RingTwice_StartCall / InitCallReceiveDelay above are the -- two halves of Script_ReceivePhoneCall that still had asm to port. { "HangUp", nil, "the VM's own `hangup` op carries the Click! and SFX_HANG_UP; PhoneRing.script uses it in this row's place" }, { "LoadBillScript", nil, "writes wCallerContact for Script_SpecialBillCall; the call descriptor names the script key instead" }, { "LoadElmScript", nil, "writes wCallerContact for Script_SpecialElmCall; the call descriptor names the script key instead" }, { "MomTriesToBuySomething_ASMFunction", nil, "queues Mom's pages into wCallerContact; World:momTriesToBuy hands the same rows to PhoneRing.script" }, -- wEnabledPlayerEvents has no home in this port: nothing ever CLEARS -- PLAYEREVENTS_WILD_ENCOUNTERS here, so re-enabling it is already the -- standing state and a write would be a write nobody reads. Not the same -- byte as STATUSFLAGS_NO_WILD_ENCOUNTERS_F, which `wildoff` owns. { "EnableWildEncounters", nil, "the port has no wEnabledPlayerEvents; the bit is never cleared" }, -- FindItemInBallScript's opener. The port reaches Poke Ball items through -- the object's own itemball row rather than through this script, so the -- ReceiveItem here has no bag to write to and no wItemBallItemID to read. -- A 0 or a 1 would pick the "no room" arm at random, so it stays nil. { "TryReceiveItem", nil, "wItemBallItemID / wItemBallQuantity are set by a script path this port does not run" }, } CallAsm.HANDLERS = H CallAsm.STUBS = {} CallAsm.STUB_REASONS = {} for _, row in ipairs(STUB_ROWS) do local name, value, reason = row[1], row[2], row[3] CallAsm.STUB_REASONS[name] = reason CallAsm.STUBS[name] = function() return value end end -- The merged table, built rather than written out so the two sets cannot -- drift and so a name in both is a hard error here instead of a silent shadow. CallAsm.ALL = {} for name, fn in pairs(CallAsm.HANDLERS) do CallAsm.ALL[name] = fn end for name, fn in pairs(CallAsm.STUBS) do if CallAsm.ALL[name] then error("gen2 callasm routine '" .. name .. "' is both implemented and stubbed", 0) end CallAsm.ALL[name] = fn end -- ---- dispatch -------------------------------------------------------------- -- The `bank:addr` key, lower case hex, two digits and four -- the same shape -- data/generated/scripts.lua uses for its own script keys, so the two can be -- compared by eye. function CallAsm.key(bank, addr) return string.format("%02x:%04x", (bank or 0) % 0x100, (addr or 0) % 0x10000) end -- The routine name for a site, or nil. `label` wins when the extractor has -- one (it does not today: nothing resolves bank:addr against the symbol file -- at import time), and the address pair is the fallback that always works. function CallAsm.nameFor(label, bank, addr) if label and CallAsm.ALL[label] then return label end return CallAsm.SITES[CallAsm.key(bank, addr)] end -- Run a routine by name. Returns the byte the asm leaves in wScriptVar, or -- nil when it writes none -- and an unknown name is nil too, because a routine -- nobody has ported must not decide a branch. function CallAsm.run(ctx, name) local fn = CallAsm.ALL[name] if not fn then return nil end local ok, value = pcall(fn, ctx) -- A handler that throws is a bug, but it must not take the script down with -- it: the cart's own callasm cannot fail, so the branch after it still has -- to see the "left alone" answer rather than an aborted coroutine. if not ok then return nil end if type(value) ~= "number" then return nil end return value % 0x100 end -- The VM seam: src/script/gen2/Vm.lua's `callasm` / `memcallasm` branch, via -- World:callAsm. Nil for anything not in the table, which is every row in -- this cache -- all thirty-eight of them are mis-decoded `hiddenitem` data. function CallAsm.dispatch(ctx, label, bank, addr) local name = CallAsm.nameFor(label, bank, addr) if not name then return nil end return CallAsm.run(ctx, name) end return CallAsm