mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 00:02:23 +02:00
G2 support
This commit is contained in:
@@ -0,0 +1,734 @@
|
||||
-- `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 "<nickname> 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)
|
||||
return call(ctx, "partyMoveUser", "ROCK_SMASH") and 0 or 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
|
||||
@@ -0,0 +1,180 @@
|
||||
-- Gen 2 overworld movement-script helpers (macros/scripts/movement.asm).
|
||||
-- Import stores raw byte streams; the world steps them one command at a time.
|
||||
|
||||
local bit = require("bit")
|
||||
|
||||
local Movement = {}
|
||||
|
||||
local DIR = { [0] = "down", [1] = "up", [2] = "left", [3] = "right" }
|
||||
Movement.DIR = DIR
|
||||
Movement.FACING = DIR
|
||||
|
||||
-- High nibble / command family (low 2 bits = facing when directional).
|
||||
local STEP_END = 0x47
|
||||
local STEP_WAIT_END = 0x48
|
||||
-- The two warp-exit bytes. Each of StepFunction_TeleportFrom's two beats runs
|
||||
-- for 16 frames (`ld [hl], 16` into OBJECT_STEP_DURATION), and TeleportTo's
|
||||
-- three -- wait, descent, final spin -- for 16 each.
|
||||
local TELEPORT_FROM = 0x4c
|
||||
local TELEPORT_TO = 0x4d
|
||||
local TELEPORT_BEAT_FRAMES = 16
|
||||
local TELEPORT_FROM_FRAMES = 2 * TELEPORT_BEAT_FRAMES
|
||||
local TELEPORT_TO_FRAMES = 3 * TELEPORT_BEAT_FRAMES
|
||||
-- The four OBJECT_FLAGS1 control bytes (macros/scripts/movement.asm:78-96).
|
||||
-- None of them consume a frame: each Movement_* handler ends in
|
||||
-- `jp ContinueReadingMovement` (engine/overworld/movement.asm:353-375), so the
|
||||
-- next byte is read in the same pass.
|
||||
local REMOVE_SLIDING = 0x38
|
||||
local SET_SLIDING = 0x39
|
||||
local REMOVE_FIXED_FACING = 0x3a
|
||||
local FIX_FACING = 0x3b
|
||||
-- Movement_tree_shake (engine/overworld/movement.asm:334): 24 frames of
|
||||
-- STEP_TYPE_SLEEP with OBJECT_ACTION set to OBJECT_ACTION_WEIRD_TREE.
|
||||
local TREE_SHAKE = 0x56
|
||||
local TREE_SHAKE_FRAMES = 24
|
||||
|
||||
function Movement.dir(nibble)
|
||||
return DIR[nibble % 4] or "down"
|
||||
end
|
||||
|
||||
-- Decode one byte into an action table, or nil to ignore / end.
|
||||
-- Returns { kind="step"|"turn"|"sleep"|"end", dir=?, frames=? }.
|
||||
function Movement.decodeByte(b)
|
||||
if b == STEP_END or b == STEP_WAIT_END then
|
||||
return { kind = "end" }
|
||||
end
|
||||
local family = bit.band(b, 0xfc)
|
||||
local dir = Movement.dir(bit.band(b, 0x03))
|
||||
if family == 0x00 then -- turn_head
|
||||
return { kind = "turn", dir = dir }
|
||||
elseif family == 0x04 then -- turn_step (face then step)
|
||||
return { kind = "step", dir = dir }
|
||||
elseif family == 0x08 then -- slow_step
|
||||
return { kind = "step", dir = dir }
|
||||
elseif family == 0x0c then -- step
|
||||
return { kind = "step", dir = dir }
|
||||
elseif family == 0x10 then -- big_step
|
||||
return { kind = "step", dir = dir }
|
||||
elseif family == 0x14 or family == 0x18 or family == 0x1c then -- slides
|
||||
return { kind = "step", dir = dir }
|
||||
elseif family == 0x20 or family == 0x24 or family == 0x28 then -- turn_away/in/waterfall
|
||||
return { kind = "turn", dir = dir }
|
||||
elseif family == 0x2c or family == 0x30 or family == 0x34 then
|
||||
-- slow_jump_step / jump_step / fast_jump_step, all of which reach
|
||||
-- JumpStep (engine/overworld/movement.asm:741) and so run under
|
||||
-- StepFunction_NPCJump (engine/overworld/map_objects.asm:1129). That
|
||||
-- step type is TWO beats: `.Jump` walks a cell and then calls GetNextTile
|
||||
-- a second time (map_objects.asm:1143), which re-advances OBJECT_MAP_X/Y,
|
||||
-- and `.Land` walks that second cell. A jump therefore covers two cells,
|
||||
-- not one; folding it into a plain step left every scripted jump a cell
|
||||
-- short and carried the offset through the rest of the stream (the Ilex
|
||||
-- Forest Farfetch'd ended up walking its last six UP steps through the
|
||||
-- trees one column over).
|
||||
return { kind = "jump", dir = dir }
|
||||
elseif b == SET_SLIDING or b == REMOVE_SLIDING then
|
||||
-- Movement_set_sliding / _remove_sliding toggle SLIDING_F in
|
||||
-- OBJECT_FLAGS1 (engine/overworld/movement.asm:353-363). Both
|
||||
-- SetFacingStepAction and SetFacingBumpAction bail straight to
|
||||
-- SetFacingCurrent while it is set (engine/overworld/map_object_action.asm
|
||||
-- :48 and :74), so the object keeps BOTH its facing and its current step
|
||||
-- frame for the whole stream: it is a slide, not a walk. Dropping the
|
||||
-- byte is what turned the three beasts around to face the way they flee
|
||||
-- out of the Burned Tower and gave them a walk cycle on the way
|
||||
-- (maps/BurnedTowerB1F.asm:103-125 wrap every beast stream in it).
|
||||
return { kind = "sliding", on = (b == SET_SLIDING) }
|
||||
elseif b == FIX_FACING or b == REMOVE_FIXED_FACING then
|
||||
-- Movement_fix_facing / _remove_fixed_facing toggle FIXED_FACING_F in
|
||||
-- OBJECT_FLAGS1 (engine/overworld/movement.asm:365-375). InitStep tests
|
||||
-- it and jumps PAST the write to OBJECT_DIRECTION
|
||||
-- (engine/overworld/map_objects.asm:284-294), so a fixed-facing object
|
||||
-- steps without turning; ApplyObjectFacing refuses it too
|
||||
-- (engine/overworld/scripting.asm:856), which is why faceplayer cannot
|
||||
-- turn one either. Route 30's two Rattata lunge under it
|
||||
-- (maps/Route30.asm:183-193) and ended up facing away from the fight.
|
||||
return { kind = "fixfacing", fixed = (b == FIX_FACING) }
|
||||
elseif b == 0x3c or b == 0x3d then -- show/hide object
|
||||
return { kind = "nop" }
|
||||
elseif b >= 0x3e and b <= 0x46 then -- step_sleep N
|
||||
return { kind = "sleep", frames = (b - 0x3e + 1) * 16 }
|
||||
elseif b == TELEPORT_FROM or b == TELEPORT_TO then
|
||||
-- Movement_teleport_from / _to set STEP_TYPE_TELEPORT_FROM / _TO
|
||||
-- (engine/overworld/movement.asm:95), whose step functions are
|
||||
-- StepFunction_TeleportFrom / _TeleportTo in
|
||||
-- engine/overworld/map_objects.asm: a spin on the spot, then a spinning
|
||||
-- rise (or a wait, a spinning descent and a last spin) driven off
|
||||
-- OBJECT_JUMP_HEIGHT through Sine. Without a branch here the byte fell off
|
||||
-- the family ladder and read as `nop`, so Lance blinked out of the Lake of
|
||||
-- Rage and Blue off Cinnabar with no animation at all.
|
||||
return {
|
||||
kind = "teleport",
|
||||
mode = (b == TELEPORT_FROM) and "from" or "to",
|
||||
frames = (b == TELEPORT_FROM) and TELEPORT_FROM_FRAMES
|
||||
or TELEPORT_TO_FRAMES,
|
||||
}
|
||||
elseif b == TREE_SHAKE then
|
||||
-- Movement_tree_shake (engine/overworld/movement.asm:334) puts 24 in
|
||||
-- OBJECT_STEP_DURATION, sets STEP_TYPE_SLEEP and sets OBJECT_ACTION to
|
||||
-- OBJECT_ACTION_WEIRD_TREE, so SetFacingWeirdTree
|
||||
-- (engine/overworld/map_object_action.asm:204) rocks the tree through
|
||||
-- FACING_WEIRD_TREE_0..3 for those 24 frames. $56 sits above the
|
||||
-- step_sleep window and matches no family, so it used to reach the
|
||||
-- trailing nop: the world then ate it and the step_end after it in one
|
||||
-- pass and Sudowoodo's shake played in zero frames
|
||||
-- (maps/Route36.asm:260-262 SudowoodoShakeMovement is this byte alone).
|
||||
return { kind = "treeshake", frames = TREE_SHAKE_FRAMES }
|
||||
end
|
||||
return { kind = "nop" }
|
||||
end
|
||||
|
||||
function Movement.isEnd(b)
|
||||
return b == STEP_END or b == STEP_WAIT_END
|
||||
end
|
||||
|
||||
Movement.STEP_END = STEP_END
|
||||
Movement.STEP_WAIT_END = STEP_WAIT_END
|
||||
Movement.TELEPORT_FROM = TELEPORT_FROM
|
||||
Movement.TELEPORT_TO = TELEPORT_TO
|
||||
Movement.TELEPORT_BEAT_FRAMES = TELEPORT_BEAT_FRAMES
|
||||
Movement.REMOVE_SLIDING = REMOVE_SLIDING
|
||||
Movement.SET_SLIDING = SET_SLIDING
|
||||
Movement.REMOVE_FIXED_FACING = REMOVE_FIXED_FACING
|
||||
Movement.FIX_FACING = FIX_FACING
|
||||
Movement.TREE_SHAKE = TREE_SHAKE
|
||||
Movement.TREE_SHAKE_FRAMES = TREE_SHAKE_FRAMES
|
||||
|
||||
-- SetFacingWeirdTree's own index: it increments OBJECT_STEP_FRAME BEFORE
|
||||
-- masking (`inc a / maskbits NUM_DIRECTIONS, 2 / rrca / rrca`), so the count
|
||||
-- that reaches the facing table is frame+1 and the quarter changes every four
|
||||
-- frames (engine/overworld/map_object_action.asm:204). FacingWeirdTree0 and
|
||||
-- FacingWeirdTree2 are FacingStepDown0's tiles $00-$03, FacingWeirdTree1 is
|
||||
-- $04-$07 and FacingWeirdTree3 is those same four with the columns swapped and
|
||||
-- OAM_XFLIP on each, i.e. the mirror image (data/sprites/facings.asm:46-52,
|
||||
-- :185-190, :192-197).
|
||||
function Movement.treeShakeIndex(frame)
|
||||
return math.floor(((frame or 0) + 1) % 16 / 4) % 4
|
||||
end
|
||||
|
||||
-- Sine (home/sine.asm) with the amplitude StepFunction_TeleportFrom passes:
|
||||
-- `ld d, $60 / call Sine / ld a, h / sub $60`, i.e. the high byte of
|
||||
-- $60 * sin(height * 2pi / 64) minus $60. Both teleport beats walk
|
||||
-- OBJECT_JUMP_HEIGHT through this and write the answer to
|
||||
-- OBJECT_SPRITE_Y_OFFSET, so the sprite lifts a hundred-odd pixels off its
|
||||
-- tile over sixteen frames and comes back down the same curve.
|
||||
function Movement.teleportYOffset(height)
|
||||
return math.floor(0x60 * math.sin((height % 64) * math.pi / 32)) - 0x60
|
||||
end
|
||||
|
||||
-- OBJECT_JUMP_HEIGHT's own start values: $10 for the rise (.InitSpinRise) and
|
||||
-- 0 for the descent (.InitDescent).
|
||||
Movement.TELEPORT_RISE_HEIGHT = 0x10
|
||||
Movement.TELEPORT_FALL_HEIGHT = 0
|
||||
|
||||
local DIR_BYTE = { down = 0, up = 1, left = 2, right = 3 }
|
||||
|
||||
-- The `step <dir>` family ($0c-$0f), which is what InitMovementBuffer fills
|
||||
-- for a trainer walking up to the player.
|
||||
function Movement.stepByte(dir)
|
||||
return 0x0c + (DIR_BYTE[dir] or 0)
|
||||
end
|
||||
|
||||
return Movement
|
||||
@@ -0,0 +1,214 @@
|
||||
-- Gen 2 script opcodes from pokegold/macros/scripts/events.asm.
|
||||
-- `size` is operand bytes after the opcode (for import-time disassembly).
|
||||
|
||||
local Opcodes = {
|
||||
[0x00] = { name = "scall", size = 2 },
|
||||
[0x01] = { name = "farscall", size = 3 },
|
||||
[0x02] = { name = "memcall", size = 2 },
|
||||
[0x03] = { name = "sjump", size = 2 },
|
||||
[0x04] = { name = "farsjump", size = 3 },
|
||||
[0x05] = { name = "memjump", size = 2 },
|
||||
[0x06] = { name = "ifequal", size = 3 },
|
||||
[0x07] = { name = "ifnotequal", size = 3 },
|
||||
[0x08] = { name = "iffalse", size = 2 },
|
||||
[0x09] = { name = "iftrue", size = 2 },
|
||||
[0x0a] = { name = "ifgreater", size = 3 },
|
||||
[0x0b] = { name = "ifless", size = 3 },
|
||||
[0x0c] = { name = "jumpstd", size = 2 },
|
||||
[0x0d] = { name = "callstd", size = 2 },
|
||||
[0x0e] = { name = "callasm", size = 3 },
|
||||
[0x0f] = { name = "special", size = 2 },
|
||||
[0x10] = { name = "memcallasm", size = 2 },
|
||||
[0x11] = { name = "checkmapscene", size = 2 },
|
||||
[0x12] = { name = "setmapscene", size = 3 },
|
||||
[0x13] = { name = "checkscene", size = 0 },
|
||||
[0x14] = { name = "setscene", size = 1 },
|
||||
[0x15] = { name = "setval", size = 1 },
|
||||
[0x16] = { name = "addval", size = 1 },
|
||||
[0x17] = { name = "random", size = 1 },
|
||||
[0x18] = { name = "checkver", size = 0 },
|
||||
[0x19] = { name = "readmem", size = 2 },
|
||||
[0x1a] = { name = "writemem", size = 2 },
|
||||
[0x1b] = { name = "loadmem", size = 3 },
|
||||
[0x1c] = { name = "readvar", size = 1 },
|
||||
[0x1d] = { name = "writevar", size = 1 },
|
||||
[0x1e] = { name = "loadvar", size = 2 },
|
||||
[0x1f] = { name = "giveitem", size = 2 },
|
||||
[0x20] = { name = "takeitem", size = 2 },
|
||||
[0x21] = { name = "checkitem", size = 1 },
|
||||
[0x22] = { name = "givemoney", size = 4 }, -- account + 3-byte money (macro)
|
||||
[0x23] = { name = "takemoney", size = 4 },
|
||||
[0x24] = { name = "checkmoney", size = 4 },
|
||||
[0x25] = { name = "givecoins", size = 2 },
|
||||
[0x26] = { name = "takecoins", size = 2 },
|
||||
[0x27] = { name = "checkcoins", size = 2 },
|
||||
[0x28] = { name = "addcellnum", size = 1 },
|
||||
[0x29] = { name = "delcellnum", size = 1 },
|
||||
[0x2a] = { name = "checkcellnum", size = 1 },
|
||||
[0x2b] = { name = "checktime", size = 1 },
|
||||
[0x2c] = { name = "checkpoke", size = 1 },
|
||||
-- Variable length in ROM (4, or 8 when trainer≠0); extractor special-cases it.
|
||||
[0x2d] = { name = "givepoke", size = 4 },
|
||||
[0x2e] = { name = "giveegg", size = 2 },
|
||||
[0x2f] = { name = "givepokemail", size = 2 },
|
||||
[0x30] = { name = "checkpokemail", size = 2 },
|
||||
[0x31] = { name = "checkevent", size = 2 },
|
||||
[0x32] = { name = "clearevent", size = 2 },
|
||||
[0x33] = { name = "setevent", size = 2 },
|
||||
[0x34] = { name = "checkflag", size = 2 },
|
||||
[0x35] = { name = "clearflag", size = 2 },
|
||||
[0x36] = { name = "setflag", size = 2 },
|
||||
[0x37] = { name = "wildon", size = 0 },
|
||||
[0x38] = { name = "wildoff", size = 0 },
|
||||
[0x39] = { name = "xycompare", size = 2 },
|
||||
[0x3a] = { name = "warpmod", size = 3 },
|
||||
[0x3b] = { name = "blackoutmod", size = 2 },
|
||||
[0x3c] = { name = "warp", size = 4 },
|
||||
[0x3d] = { name = "getmoney", size = 2 },
|
||||
[0x3e] = { name = "getcoins", size = 1 },
|
||||
[0x3f] = { name = "getnum", size = 1 },
|
||||
[0x40] = { name = "getmonname", size = 2 },
|
||||
[0x41] = { name = "getitemname", size = 2 },
|
||||
[0x42] = { name = "getcurlandmarkname", size = 1 },
|
||||
[0x43] = { name = "gettrainername", size = 3 },
|
||||
[0x44] = { name = "getstring", size = 3 },
|
||||
[0x45] = { name = "itemnotify", size = 0 },
|
||||
[0x46] = { name = "pocketisfull", size = 0 },
|
||||
[0x47] = { name = "opentext", size = 0 },
|
||||
[0x48] = { name = "reanchormap", size = 1 },
|
||||
[0x49] = { name = "closetext", size = 0 },
|
||||
[0x4a] = { name = "writeunusedbyte", size = 1 },
|
||||
[0x4b] = { name = "farwritetext", size = 3 },
|
||||
[0x4c] = { name = "writetext", size = 2 },
|
||||
[0x4d] = { name = "repeattext", size = 2 },
|
||||
[0x4e] = { name = "yesorno", size = 0 },
|
||||
[0x4f] = { name = "loadmenu", size = 2 },
|
||||
[0x50] = { name = "closewindow", size = 0 },
|
||||
[0x51] = { name = "jumptextfaceplayer", size = 2 },
|
||||
[0x52] = { name = "jumptext", size = 2 },
|
||||
[0x53] = { name = "waitbutton", size = 0 },
|
||||
[0x54] = { name = "promptbutton", size = 0 },
|
||||
[0x55] = { name = "pokepic", size = 1 },
|
||||
[0x56] = { name = "closepokepic", size = 0 },
|
||||
[0x57] = { name = "_2dmenu", size = 0 },
|
||||
[0x58] = { name = "verticalmenu", size = 0 },
|
||||
[0x59] = { name = "loadpikachudata", size = 0 },
|
||||
[0x5a] = { name = "randomwildmon", size = 0 },
|
||||
[0x5b] = { name = "loadtemptrainer", size = 0 },
|
||||
[0x5c] = { name = "loadwildmon", size = 2 },
|
||||
[0x5d] = { name = "loadtrainer", size = 2 },
|
||||
[0x5e] = { name = "startbattle", size = 0 },
|
||||
[0x5f] = { name = "reloadmapafterbattle", size = 0 },
|
||||
[0x60] = { name = "catchtutorial", size = 1 },
|
||||
[0x61] = { name = "trainertext", size = 1 },
|
||||
[0x62] = { name = "trainerflagaction", size = 1 },
|
||||
[0x63] = { name = "winlosstext", size = 4 },
|
||||
[0x64] = { name = "scripttalkafter", size = 0 },
|
||||
[0x65] = { name = "endifjustbattled", size = 0 },
|
||||
[0x66] = { name = "checkjustbattled", size = 0 },
|
||||
[0x67] = { name = "setlasttalked", size = 1 },
|
||||
[0x68] = { name = "applymovement", size = 3 },
|
||||
[0x69] = { name = "applymovementlasttalked", size = 2 },
|
||||
[0x6a] = { name = "faceplayer", size = 0 },
|
||||
[0x6b] = { name = "faceobject", size = 2 },
|
||||
[0x6c] = { name = "variablesprite", size = 2 },
|
||||
[0x6d] = { name = "disappear", size = 1 },
|
||||
[0x6e] = { name = "appear", size = 1 },
|
||||
[0x6f] = { name = "follow", size = 2 },
|
||||
[0x70] = { name = "stopfollow", size = 0 },
|
||||
[0x71] = { name = "moveobject", size = 3 },
|
||||
[0x72] = { name = "writeobjectxy", size = 1 },
|
||||
[0x73] = { name = "loademote", size = 1 },
|
||||
[0x74] = { name = "showemote", size = 3 },
|
||||
[0x75] = { name = "turnobject", size = 2 },
|
||||
[0x76] = { name = "follownotexact", size = 2 },
|
||||
[0x77] = { name = "earthquake", size = 1 },
|
||||
[0x78] = { name = "changemapblocks", size = 3 },
|
||||
[0x79] = { name = "changeblock", size = 3 },
|
||||
[0x7a] = { name = "reloadmap", size = 0 },
|
||||
[0x7b] = { name = "refreshmap", size = 0 },
|
||||
[0x7c] = { name = "writecmdqueue", size = 2 },
|
||||
[0x7d] = { name = "delcmdqueue", size = 1 },
|
||||
[0x7e] = { name = "playmusic", size = 2 },
|
||||
[0x7f] = { name = "encountermusic", size = 0 },
|
||||
[0x80] = { name = "musicfadeout", size = 3 },
|
||||
[0x81] = { name = "playmapmusic", size = 0 },
|
||||
[0x82] = { name = "dontrestartmapmusic", size = 0 },
|
||||
[0x83] = { name = "cry", size = 2 },
|
||||
[0x84] = { name = "playsound", size = 2 },
|
||||
[0x85] = { name = "waitsfx", size = 0 },
|
||||
[0x86] = { name = "warpsound", size = 0 },
|
||||
[0x87] = { name = "specialsound", size = 0 },
|
||||
[0x88] = { name = "autoinput", size = 3 },
|
||||
[0x89] = { name = "newloadmap", size = 1 },
|
||||
[0x8a] = { name = "pause", size = 1 },
|
||||
[0x8b] = { name = "deactivatefacing", size = 1 },
|
||||
[0x8c] = { name = "sdefer", size = 2 },
|
||||
[0x8d] = { name = "warpcheck", size = 0 },
|
||||
[0x8e] = { name = "stopandsjump", size = 2 },
|
||||
[0x8f] = { name = "endcallback", size = 0 },
|
||||
[0x90] = { name = "end", size = 0 },
|
||||
[0x91] = { name = "reloadend", size = 1 },
|
||||
[0x92] = { name = "endall", size = 0 },
|
||||
[0x93] = { name = "pokemart", size = 3 },
|
||||
[0x94] = { name = "elevator", size = 2 },
|
||||
[0x95] = { name = "trade", size = 1 },
|
||||
[0x96] = { name = "askforphonenumber", size = 1 },
|
||||
[0x97] = { name = "phonecall", size = 2 },
|
||||
[0x98] = { name = "hangup", size = 0 },
|
||||
[0x99] = { name = "describedecoration", size = 1 },
|
||||
[0x9a] = { name = "fruittree", size = 1 },
|
||||
[0x9b] = { name = "specialphonecall", size = 2 },
|
||||
[0x9c] = { name = "checkphonecall", size = 0 },
|
||||
[0x9d] = { name = "verbosegiveitem", size = 2 },
|
||||
-- `swarm` is a bare `map_id` (macros/scripts/events.asm: `db swarm_command /
|
||||
-- map_id \1`), and map_id is two bytes; Script_swarm makes exactly two
|
||||
-- GetScriptByte calls. This row said 3, which ate the opcode byte after
|
||||
-- every swarm and shifted the rest of that script by one. The four users all
|
||||
-- live in engine/phone/scripts/trainers.asm, which the pointer walk does not
|
||||
-- reach yet, so nothing in today's cache is mis-decoded by it.
|
||||
[0x9e] = { name = "swarm", size = 2 },
|
||||
[0x9f] = { name = "halloffame", size = 0 },
|
||||
[0xa0] = { name = "credits", size = 0 },
|
||||
[0xa1] = { name = "warpfacing", size = 5 },
|
||||
}
|
||||
|
||||
-- Commands that end the current linear path (jumps transfer control).
|
||||
--
|
||||
-- `fruittree` and `describedecoration` are ScriptJumps (Script_fruittree does
|
||||
-- `jp ScriptJump` into FruitTreeScript, Script_describedecoration jumps to
|
||||
-- whatever DescribeDecoration hands back), and `halloffame` / `credits` both
|
||||
-- fall into ReturnFromCredits, which is Script_endall plus MAPSTATUS_DONE.
|
||||
-- Without them the disassembler kept walking the data that follows: all 42
|
||||
-- extracted fruittree scripts and all 13 describedecoration ones currently
|
||||
-- carry a tail of garbage commands, which is why Vm.lua returns on each of
|
||||
-- these four regardless. `catchtutorial` is deliberately NOT here: it ends on
|
||||
-- `jp Script_reloadmap` and the script really does continue afterwards, the
|
||||
-- same way `reloadmap` itself does.
|
||||
Opcodes.TERMINATORS = {
|
||||
sjump = true, farsjump = true, memjump = true, jumpstd = true,
|
||||
jumptext = true, jumptextfaceplayer = true,
|
||||
stopandsjump = true, ["end"] = true, endall = true, endcallback = true,
|
||||
reloadend = true,
|
||||
fruittree = true, describedecoration = true,
|
||||
halloffame = true, credits = true,
|
||||
}
|
||||
|
||||
-- The one op name this engine adds, and the reason it deliberately has NO byte
|
||||
-- behind it. Every row above is keyed by the opcode byte the cart carries, and
|
||||
-- src/import/RomExtractorGen2.lua resolves a command as Opcodes[byte]: a byte
|
||||
-- that is not in that table breaks the pointer walk into an `unknown` row. Give
|
||||
-- a mod verb one of the free bytes ($a2..$ff) and ROM data that happens to start
|
||||
-- with it would decode as a mod call instead of ending the walk, so the free
|
||||
-- space stays free and the extension op is reachable only by NAME -- which is to
|
||||
-- say, only from a row a mod wrote, never from one the extractor did.
|
||||
--
|
||||
-- src/script/gen2/Vm.lua:runModCommand is the only reader; the contract for the
|
||||
-- row shapes and the verb table is documented there.
|
||||
Opcodes.MOD_COMMAND = "modcommand"
|
||||
|
||||
function Opcodes.key(bank, address)
|
||||
return string.format("%02x:%04x", bank, address)
|
||||
end
|
||||
|
||||
return Opcodes
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user