mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
379 lines
14 KiB
Lua
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
|