Files
2026-09-24 05:17:32 -04:00

379 lines
14 KiB
Lua

local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_link_battle"
local CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
local COLOSSEUM = "FR_BATTLE_COLOSSEUM_2P"
-- pokefirered/data/specials.inc:5 SetCableClubWarp
local SET_CABLE_CLUB_WARP = 0x01
-- pokefirered/include/constants/vars.h:163
local VAR_CABLE_CLUB_STATE = 0x406F
local VAR_0x8004 = 0x8004
-- pokefirered/include/constants/flags.h:1375
local FLAG_SYS_POKEDEX_GET = 0x829
local failures = 0
local function result(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
if failures == 0 then
print("PASS link_battle")
love.event.quit(0)
else
print("FAIL link_battle failures=" .. failures)
love.event.quit(1)
end
end
return function(game)
print("PASS driver_started")
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "RED" })
U.wait(240)
local Runtime = require("src.core.game3.runtime")
local Map = require("src.core.game3.map")
local Space = require("src.core.game3.scripting.space")
local Flags = require("src.core.game3.scripting.flags")
local Player = require("src.core.game3.player")
local Party = require("src.core.game3.party")
local Natives = require("src.core.game3.scripting.natives")
local Link = require("src.core.game3.link")
local LB = require("src.core.game3.link.battle")
local Game3Link = require("src.link.Game3Link")
local Protocol = require("src.link.Protocol")
local Battle = require("src.core.game3.battle")
local BattleUi = require("src.core.game3.battle.ui")
local session = Runtime.getSession()
if not result(session ~= nil, "new game reached the game3 field") then return finish() end
local function ctx() return Space.vm and Space.vm.ctx end
local function adapters() return Space.vm and Space.vm.adapters end
local function getVar(id) return tonumber(Flags.getVar(Space.store, ctx(), id)) or 0 end
local function setVar(id, v) Flags.setVar(Space.store, ctx(), id, v) end
local function place(x, y, facing)
Player.cellX, Player.cellY = x, y
Player.px, Player.py = x * 16, y * 16
Player.targetX, Player.targetY = x, y
Player.facing = facing
if game.session then
game.session.x, game.session.y, game.session.facing = x, y, facing
end
end
-- pokefirered/src/pokemon.c:1796 the other GBA's party
local peerHolder = { name = "BLUE", trainerId = 0x2222, party = {} }
Party.giveMon(peerHolder, 129, 5)
Party.giveMon(peerHolder, 23, 5)
local karp, ekans = peerHolder.party[1], peerHolder.party[2]
karp.moves, karp.pp, karp.maxPp = { 150 }, { 40 }, { 40 }
ekans.moves, ekans.pp, ekans.maxPp = { 40 }, { 35 }, { 35 }
local peerParty = Protocol.packParty3(peerHolder.party)
local peer, turnsAnswered, peerSeat, peerSetup = nil, 0, false, false
local peerSwitches, peerSentSwitch = 0, false
local function pumpPeer()
if not peer then return end
peer:update(0)
local lu = peer:take(LB.MSG.LINKUP)
if lu then
peer:send({ type = LB.MSG.LINKUP, linkType = lu.linkType, players = lu.players })
end
if peer:take(LB.MSG.SEAT) then
peerSeat = true
peer:send({ type = LB.MSG.SEAT, seat = 1 })
end
local setup = peer:take(LB.MSG.SETUP)
if setup then
peerSetup = true
peer:send({
type = LB.MSG.SETUP, seed = setup.seed, mode = setup.mode,
name = "BLUE", trainerId = 0x2222, gender = 0, party = peerParty,
})
end
local act = peer:take(LB.MSG.ACTION)
if act then
turnsAnswered = turnsAnswered + 1
peer:send({ type = LB.MSG.ACTION, turn = act.turn, kind = "move", slot = 1 })
end
if peer:take(LB.MSG.SWITCH) then peerSwitches = peerSwitches + 1 end
-- pokefirered/data/battle_scripts_1.s:2837 the other machine picks its own replacement
if Battle._phase == "linkswitch" and not peerSentSwitch then
peerSentSwitch = true
peer:send({ type = LB.MSG.SWITCH, slot = 2 })
end
peer:take(LB.MSG.OUTCOME)
end
local function waitP(frames)
for _ = 1, frames do
U.wait(1)
pumpPeer()
end
end
local function printedStill(path)
local Message = require("src.ui.game3.message")
local n = 0
while n < 600 and not (Message.isOpen() and Message.isWaiting()) do
waitP(1)
n = n + 1
end
U.still(game, path)
end
local function mashP(frames)
local left = frames
while left > 0 do
U.tap(game, "a")
waitP(12)
left = left - 12
end
end
Flags.setFlag(Space.store, ctx(), FLAG_SYS_POKEDEX_GET, true)
session.party = {}
Party.giveMon(session, 6, 50)
local lead = session.party[1]
lead.moves, lead.pp, lead.maxPp = { 53 }, { 15 }, { 15 }
Party.giveMon(session, 1, 48)
result(#session.party == 2, "the player has a party to battle with")
-- pokefirered/data/scripts/cable_club.inc:284 CableClub_EventScript_EnterColosseum
Map.load(nil, game, CENTER_2F, { x = 9, y = 2, facing = "up" })
place(9, 2, "up")
U.wait(60)
Flags.setFlag(Space.store, ctx(), FLAG_SYS_POKEDEX_GET, true)
place(9, 1, "up")
U.wait(10)
Natives.special(ctx(), SET_CABLE_CLUB_WARP, adapters())
result(session.dynamicWarp ~= nil and session.dynamicWarp.map == CENTER_2F,
"SetCableClubWarp recorded the way back to the cable club counter")
local host
host, peer = Game3Link.loopback({ game = game })
host:update(0)
peer:update(0)
Link.attach(host)
result(host:isReady(), "the other GBA is on the cable")
-- pokefirered/data/scripts/cable_club.inc:262 CableClub_EventScript_SingleBattleMode
setVar(VAR_0x8004, Link.USING.SINGLE_BATTLE)
local NativesLink = require("src.core.game3.scripting.natives_link")
local VAR_RESULT = 0x800D
local yielded = Natives.special(ctx(), NativesLink.SPECIAL.TryBattleLinkup, adapters())
result(yielded, "TryBattleLinkup parked the script while the two machines agreed")
local linkup, guard = nil, 0
while guard < 300 and linkup == nil do
pumpPeer()
local poll = ctx().nativePoll
if poll and poll() then
linkup = getVar(VAR_RESULT)
ctx().nativePoll = nil
ctx().mode = "bytecode"
elseif LB.linkup ~= nil and LB.linkup ~= Link.LINKUP.ONGOING then
linkup = LB.linkup
end
U.wait(1)
guard = guard + 1
end
print("[driver] linkup=" .. tostring(linkup) .. " lb=" .. tostring(LB.state))
result(linkup == Link.LINKUP.SUCCESS,
"both machines picked SINGLE BATTLE, so the linkup reports LINKUP_SUCCESS")
result(LB.state == "seat", "and the session is waiting for a colosseum seat")
-- pokefirered/data/scripts/cable_club.inc:284 special SavePlayerParty
Natives.special(ctx(), 0x27, adapters())
Natives.special(ctx(), 0x14B, adapters())
setVar(VAR_CABLE_CLUB_STATE, Link.USING.SINGLE_BATTLE)
Map.load(nil, game, COLOSSEUM, { x = 6, y = 8, facing = "up" })
place(6, 8, "up")
waitP(120)
result(Space.mapId == COLOSSEUM, "the player is inside the 2P colosseum")
U.shot(game, DIR .. "/link_battle_01_colosseum.png")
-- pokefirered/data/scripts/cable_club.inc:575 BattleColosseum_2P_EventScript_PlayerSpot0
place(3, 6, "up")
waitP(20)
U.hold(game, "up", 24)
waitP(60)
guard = 0
while guard < 600 and not Battle.isActive() do
pumpPeer()
if Player.cellY > 5 and not Player.moving then U.hold(game, "up", 12) end
waitP(15)
guard = guard + 15
end
print("[driver] seat=" .. tostring(LB.seat) .. " lb=" .. tostring(LB.state)
.. " cell=" .. tostring(Player.cellX) .. "," .. tostring(Player.cellY))
result(peerSeat, "the other machine saw this player take a colosseum seat")
result(peerSetup, "and received this player's party over the cable")
if not result(Battle.isActive(), "the link battle started") then
U.shot(game, DIR .. "/link_battle_99_no_battle.png")
Link.reset()
return finish()
end
local st = Battle.getState()
result(st.link == true, "BATTLE_TYPE_LINK is set on the battle state")
result(st.peerName == "BLUE", "the opponent is the peer, not an NPC trainer")
waitP(180)
printedStill(DIR .. "/link_battle_02_battle_start.png")
guard = 0
while guard < 1800 and Battle._phase ~= "command" and Battle._phase ~= "linkwait" do
U.tap(game, "a")
waitP(12)
guard = guard + 12
end
print("[driver] first menu: phase=" .. tostring(Battle._phase))
result(Battle._phase == "command" or Battle._phase == "linkwait",
"the battle reached the first command menu")
U.shot(game, DIR .. "/link_battle_03_command_menu.png")
guard = 0
while guard < 1800 and turnsAnswered < 1 and Battle.isActive() do
U.tap(game, "a")
waitP(12)
guard = guard + 12
end
print("[driver] turn exchange: answered=" .. tostring(turnsAnswered)
.. " phase=" .. tostring(Battle._phase))
result(turnsAnswered >= 1, "this player's chosen action went out over the cable")
guard = 0
while guard < 600 and Battle._phase == "linkwait" do
waitP(10)
guard = guard + 10
end
result(Battle._phase ~= "linkwait",
"and the turn only resolved once the other machine answered")
result((Battle.getState() or st).turn >= 1, "turn 1 is resolved on both machines")
waitP(60)
U.shot(game, DIR .. "/link_battle_04_turn_one.png")
-- pokefirered/data/battle_scripts_1.s:2837 switchhandleorder BS_FAINTED
local function enemySlot()
local s2 = Battle.getState()
return s2 and s2.enemy and s2.enemy.partyIndex
end
guard = 0
while guard < 1800 and Battle.isActive() and enemySlot() ~= 2 do
U.tap(game, "a")
waitP(12)
guard = guard + 12
end
print("[driver] peer replacement: sent=" .. tostring(peerSentSwitch)
.. " slot=" .. tostring(enemySlot()) .. " phase=" .. tostring(Battle._phase))
result(peerSentSwitch, "the other machine had to name its own replacement over the cable")
result(enemySlot() == 2, "and the mon it named is the one that came out")
local SwitchSeq = require("src.core.game3.battle.switch_seq")
local function switchStep()
local steps = SwitchSeq._steps
local step = steps and steps[SwitchSeq._i]
return step and step.kind
end
guard = 0
while guard < 600 and switchStep() ~= "healthbox" do
waitP(1)
guard = guard + 1
end
waitP(19)
local foeMon = require("src.core.game3.battle.anim").present("enemy")
local M5 = require("src.ui.game3.message")
local page5 = (M5.isOpen() and tostring(M5.currentPage() or "")) or ""
result(foeMon ~= nil and foeMon.visible and page5:find("sent out", 1, true) ~= nil,
"EKANS is on the field under the send-out line with no A press (" .. page5:gsub("\n", " ") .. ")")
U.still(game, DIR .. "/link_battle_05_peer_replacement.png")
local PartyMenu = require("src.ui.game3.party_menu")
local SummaryMenu = require("src.ui.game3.summary_menu")
guard = 0
local lastReport = 0
local sentOutNext, endShot = false, false
while guard < 6000 and Battle.isActive() do
-- pokefirered/data/battle_scripts_1.s:2984 BattleScript_LinkBattleWonOrLost
if not endShot and Battle._phase == "ending" then
local M = require("src.ui.game3.message")
local page = (M.isOpen() and M.isWaiting() and tostring(M.currentPage() or "")) or ""
if page:find("defeated", 1, true) or page:find("lost", 1, true) or page:find("draw", 1, true) then
endShot = true
U.still(game, DIR .. "/link_battle_06_link_battle_end.png")
end
end
-- pokefirered/src/battle_controller_player.c:1486 ChooseNextMon
if PartyMenu.isOpen and PartyMenu.isOpen() then
if SummaryMenu.isOpen and SummaryMenu.isOpen() then
U.tap(game, "b")
elseif PartyMenu.mode == "message" then
U.tap(game, "a")
elseif PartyMenu.mode == "action" then
-- pokefirered/src/party_menu.c:3760 SEND OUT is the first row
U.tap(game, "a")
sentOutNext = true
elseif PartyMenu.cursor ~= 2 then
U.tap(game, "down")
else
U.tap(game, "a")
end
waitP(10)
guard = guard + 10
else
U.tap(game, "a")
waitP(12)
guard = guard + 12
end
if guard - lastReport >= 900 then
lastReport = guard
local s2 = Battle.getState()
print("[driver] still battling: phase=" .. tostring(Battle._phase)
.. " turn=" .. tostring(s2 and s2.turn) .. " answered=" .. tostring(turnsAnswered)
.. " party=" .. tostring(PartyMenu.isOpen and PartyMenu.isOpen())
.. " mode=" .. tostring(PartyMenu.mode) .. " cursor=" .. tostring(PartyMenu.cursor)
.. " mon=" .. tostring(s2 and s2.player and s2.player.mon and s2.player.mon.species)
.. " hp=" .. tostring(s2 and s2.player and s2.player.mon and s2.player.mon.hp)
.. "/" .. tostring(s2 and s2.enemy and s2.enemy.mon and s2.enemy.mon.hp))
end
end
print("[driver] battle over: sentOutNext=" .. tostring(sentOutNext)
.. " peerSwitches=" .. tostring(peerSwitches) .. " turns=" .. tostring(turnsAnswered)
.. " lead=" .. tostring(session.party[1] and session.party[1].species)
.. " hp=" .. tostring(session.party[1] and session.party[1].hp)
.. "/" .. tostring(session.party[1] and session.party[1].maxHp))
result(not Battle.isActive(), "the link battle ran to its end")
local log = BattleUi.log() or {}
local sawExp, sawEnd = false, false
for i, line in ipairs(log) do
if line:find("EXP. Points", 1, true) then sawExp = true end
if line:find("BLUE!", 1, true) then sawEnd = true end
if i > #log - 6 then print("[driver] log: " .. (line:gsub("\n", " "))) end
end
result(not sawExp, "no EXP was handed out, as in every link battle")
result(sawEnd, "the battle ended on the link battle string naming BLUE")
waitP(120)
-- pokefirered/src/cable_club.c:776 CB2_ReturnFromCableClubBattle
result(#(session.party or {}) == 2, "LoadPlayerParty handed the pre-link party back")
local records = session.linkBattleRecords or {}
result(records[1] ~= nil and records[1].name == "BLUE",
"the battle record screen now has a row for BLUE")
local stats = session.gameStats or {}
local total = (tonumber(stats.linkBattleWins) or 0) + (tonumber(stats.linkBattleLosses) or 0)
+ (tonumber(stats.linkBattleDraws) or 0)
result(total == 1, "and one GAME_STAT_LINK_BATTLE_* was counted")
waitP(60)
U.shot(game, DIR .. "/link_battle_07_back_at_the_seat.png")
Link.reset()
finish()
end