mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
186 lines
7.5 KiB
Lua
186 lines
7.5 KiB
Lua
-- Parity test, gift atomicity: a mon handed over by give_pokemon and the
|
|
-- event that closes its offer must land in the same script step, so a
|
|
-- script torn down between the two cannot hand the gift out twice (#426).
|
|
--
|
|
-- asm sources:
|
|
-- pokeyellow scripts/Route24.asm (Route24CooltrainerM4Text: CheckEvent
|
|
-- EVENT_54F -> YesNoChoice -> GivePokemon -> `jp nc, TextScriptEnd`
|
|
-- (party + box full leaves the event clear so the offer repeats) ->
|
|
-- PrintText Route24Text_515e3 -> SetEvent EVENT_54F)
|
|
-- pokeyellow scripts/CeruleanMelaniesHouse.asm (same shape plus predef
|
|
-- HideObject TOGGLE_CERULEAN_BULBASAUR, then SetEvent
|
|
-- EVENT_GOT_BULBASAUR_IN_CERULEAN)
|
|
-- pokeyellow scripts/VermilionCity_2.asm (CheckEvent / SetEvent
|
|
-- EVENT_GOT_SQUIRTLE_FROM_OFFICER_JENNY)
|
|
-- scripts/CeladonMansionRoofHouse.asm (Eevee ball: GivePokemon with no
|
|
-- confirm, HideObject on success)
|
|
-- On hardware the event write trails the received text because no step in
|
|
-- between can abort. The port yields there (AskName, NamingScreen, the
|
|
-- text box) and wraps every row in the script.command mod hook, so the
|
|
-- write is hoisted ahead of the text: the event is only read at script
|
|
-- entry, and the failed-give path still leaves it clear.
|
|
--
|
|
-- Self-contained: run via `luajit tests/parity_gift_atomicity.lua`; also
|
|
-- dofile'd by tests/run_tests.lua's aggregator.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
if not _G.love then _G.love = require("tests.love_stub") end
|
|
local Data = require("src.core.Data")
|
|
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
|
|
|
local S = require("tests.harness").suite("parity gift atomicity")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local Commands = require("src.script.Commands")
|
|
local Events = require("src.mods.Events")
|
|
local Flags = require("src.script.Flags")
|
|
local Game = require("src.core.Game")
|
|
local Hooks = require("src.mods.Hooks")
|
|
local Input = require("src.core.Input")
|
|
local Logger = require("src.core.Logger")
|
|
local Runtime = require("src.mods.Runtime")
|
|
local SaveData = require("src.core.SaveData")
|
|
local ScriptRunner = require("src.script.ScriptRunner")
|
|
local StateStack = require("src.core.StateStack")
|
|
|
|
Game.data = Data
|
|
Game.input = Input; Input:init()
|
|
Game.stack = StateStack; StateStack:init()
|
|
Game.save = SaveData.newGame()
|
|
require("src.render.Font").load(Data)
|
|
|
|
local gifts = require("data.scripts.yellow_gifts")
|
|
local eevee = require("data.scripts.celadon_eevee")
|
|
|
|
-- === 1) row-order audit: on every gift site the carry guard follows
|
|
-- give_pokemon immediately and the bookkeeping (event, and the
|
|
-- HideObject that clears a ball or a pen mon) comes before any
|
|
-- received text ===
|
|
local function audit(label, rows)
|
|
local give
|
|
for i, row in ipairs(rows) do
|
|
if row[1] == "give_pokemon" then give = i break end
|
|
end
|
|
if not give then
|
|
check(false, label .. ": has a give_pokemon row")
|
|
return
|
|
end
|
|
eq(rows[give + 1] and rows[give + 1][1], "jump_if_false",
|
|
label .. ": carry guard sits right after give_pokemon")
|
|
local flag, text, hide
|
|
for i = give + 2, #rows do
|
|
local name = rows[i][1]
|
|
if name == "set_flag" and not flag then flag = i end
|
|
if name == "hide_object" and not hide then hide = i end
|
|
if (name == "show_text" or name == "ask") and not text then text = i end
|
|
if name == "jump" and rows[i][2] ~= nil and text then break end
|
|
end
|
|
eq(flag, give + 2, label .. ": event write is the first row past the guard")
|
|
check(text and flag < text,
|
|
label .. ": event write precedes the received text")
|
|
if hide then
|
|
check(hide < text, label .. ": HideObject precedes the received text")
|
|
end
|
|
end
|
|
|
|
-- the two function-form scripts build their rows per talk; run them with
|
|
-- the gift branch's preconditions and keep what they hand the runner
|
|
local function capture(fn, save)
|
|
local rows
|
|
local ow = { runner = { run = function(_, r) rows = r end } }
|
|
fn({ save = save }, ow, { def = {}, facePlayer = function() end },
|
|
function() end)
|
|
return rows or {}
|
|
end
|
|
|
|
audit("Route 24 Damian",
|
|
gifts.ROUTE_24.talk.TEXT_ROUTE24_COOLTRAINER_M4)
|
|
audit("Melanie's BULBASAUR",
|
|
capture(gifts.CERULEAN_MELANIES_HOUSE.talk
|
|
.TEXT_CERULEANMELANIESHOUSE_MELANIE,
|
|
{ flags = {}, pikachuHappiness = 200 }))
|
|
audit("Officer Jenny's SQUIRTLE",
|
|
capture(gifts.VERMILION_CITY.talk.TEXT_VERMILIONCITY_OFFICER_JENNY,
|
|
{ flags = {}, inventory = { THUNDERBADGE = 1 } }))
|
|
audit("Celadon EEVEE ball",
|
|
eevee.talk.TEXT_CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL)
|
|
|
|
-- === harness: run a row list headless, A-mashing through the yes/no,
|
|
-- the nickname prompt and every text box, recording show_text ids
|
|
-- (Yellow's gift text is not in a Red cache, so show_text takes
|
|
-- its literal-id fallback: the ids are still what we assert on) ===
|
|
local shown = {}
|
|
local origShow = Commands.show_text
|
|
Commands.show_text = function(ctx, textId, subs)
|
|
shown[#shown + 1] = textId
|
|
return origShow(ctx, textId, subs)
|
|
end
|
|
|
|
local function runRows(rows)
|
|
shown = {}
|
|
StateStack:init()
|
|
local ow = { map = { id = "ROUTE_24", def = { label = "ROUTE_24" } },
|
|
npcs = {}, entities = {} }
|
|
local r = ScriptRunner.new(Game, ow)
|
|
r:run(rows, { npc = { def = {}, facePlayer = function() end },
|
|
overworld = ow })
|
|
local guard = 0
|
|
while r:isRunning() and guard < 3000 do
|
|
guard = guard + 1
|
|
Input.pressed = { a = true }
|
|
StateStack:update(1 / 60)
|
|
r:update()
|
|
end
|
|
Input.pressed = {}
|
|
return not r:isRunning()
|
|
end
|
|
|
|
local DAMIAN = gifts.ROUTE_24.talk.TEXT_ROUTE24_COOLTRAINER_M4
|
|
|
|
-- === 2) plain accept: one CHARMANDER, EVENT_54F set, and the next talk
|
|
-- is Damian's after-text only ===
|
|
Game.save = SaveData.newGame()
|
|
check(runRows(DAMIAN), "Damian gift script completes")
|
|
eq(#Game.save.party, 1, "CHARMANDER joins the party")
|
|
eq(Game.save.party[1].species, "CHARMANDER", "gift species is CHARMANDER")
|
|
check(Flags.get(Game.save, "EVENT_54F"), "accepting sets EVENT_54F")
|
|
check(runRows(DAMIAN), "post-gift talk completes")
|
|
eq(table.concat(shown, ","), "_Route24DamianText4",
|
|
"a closed offer shows only the after-text")
|
|
eq(#Game.save.party, 1, "no second CHARMANDER")
|
|
|
|
-- === 3) the regression itself: every row runs inside the script.command
|
|
-- hook, and a mod that mishandles the row after the give (the
|
|
-- reporter was running a third-party UI mod) tears the coroutine
|
|
-- down mid-gift -- here by sending the pc at a label that is not
|
|
-- there. The mon is already in the party, so EVENT_54F has to be
|
|
-- set by then or the next talk re-runs the whole offer ===
|
|
local savedEvents, savedHooks, savedErrors =
|
|
Runtime.events, Runtime.hooks, Runtime.errors
|
|
local hooks = Hooks.new()
|
|
Runtime.install(Events.new(), hooks, {})
|
|
local remove = hooks:wrap("script.command", function(nextFn, _, name, args)
|
|
if name == "show_text" and args[1] == "_Route24DamianText2" then
|
|
return "no_such_label"
|
|
end
|
|
return nextFn()
|
|
end, 0, "t")
|
|
|
|
Game.save = SaveData.newGame()
|
|
local origError = Logger.error -- the tear-down logs; the test expects it
|
|
Logger.error = function() end
|
|
runRows(DAMIAN)
|
|
Logger.error = origError
|
|
eq(#Game.save.party, 1, "the killed script still handed the CHARMANDER over")
|
|
check(Flags.get(Game.save, "EVENT_54F"),
|
|
"EVENT_54F survives a tear-down after the give")
|
|
|
|
remove()
|
|
Runtime.install(savedEvents, savedHooks, savedErrors)
|
|
|
|
check(runRows(DAMIAN), "talk after the tear-down completes")
|
|
eq(table.concat(shown, ","), "_Route24DamianText4",
|
|
"the interrupted gift is not offered again")
|
|
eq(#Game.save.party, 1, "still exactly one CHARMANDER")
|
|
|
|
S.finish()
|