mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
feat: reconstruct battle checkpoints from data
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
-- facade in Loader.
|
||||
|
||||
local BattleCheckpoint = {}
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
|
||||
local BATTLER_FIELDS = {
|
||||
"shownHP", "shownStatus", "stages", "curStats", "curTypes", "curMoves",
|
||||
@@ -42,13 +43,193 @@ local function indexSet(set, party)
|
||||
end
|
||||
|
||||
local function captureBattler(battler, index, copy)
|
||||
local out = { index = index }
|
||||
local out = {
|
||||
index = index,
|
||||
curStatsFromMon = battler.curStats == battler.mon.stats,
|
||||
curTypesFromDefinition = battler.curTypes == battler.def.types,
|
||||
curMovesFromMon = battler.curMoves == battler.mon.moves,
|
||||
}
|
||||
for _, field in ipairs(BATTLER_FIELDS) do
|
||||
if battler[field] ~= nil then out[field] = battler[field] end
|
||||
end
|
||||
return copy(out)
|
||||
end
|
||||
|
||||
local function integer(value, min, max)
|
||||
return type(value) == "number" and value % 1 == 0
|
||||
and value >= (min or -math.huge) and value <= (max or math.huge)
|
||||
end
|
||||
|
||||
local function validateMoveList(data, moves)
|
||||
if type(moves) ~= "table" then return false end
|
||||
for _, move in ipairs(moves) do
|
||||
if type(move) ~= "table" or type(move.id) ~= "string"
|
||||
or type(data.moves[move.id]) ~= "table" or type(move.pp) ~= "number" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function validateMon(data, mon)
|
||||
return type(mon) == "table" and type(mon.species) == "string"
|
||||
and type(data.pokemon[mon.species]) == "table" and integer(mon.level, 1, 100)
|
||||
and type(mon.hp) == "number" and type(mon.stats) == "table"
|
||||
and validateMoveList(data, mon.moves)
|
||||
end
|
||||
|
||||
local function validateBattler(data, battler, maxIndex)
|
||||
if type(battler) ~= "table" or not integer(battler.index, 1, maxIndex) then
|
||||
return false
|
||||
end
|
||||
if battler.stages ~= nil then
|
||||
if type(battler.stages) ~= "table" then return false end
|
||||
for _, stage in pairs(battler.stages) do
|
||||
if not integer(stage, -6, 6) then return false end
|
||||
end
|
||||
end
|
||||
return validateMoveList(data, battler.curMoves)
|
||||
and type(battler.curStats) == "table" and type(battler.curTypes) == "table"
|
||||
end
|
||||
|
||||
local function clone(value, copy)
|
||||
if type(value) ~= "table" then return value end
|
||||
return assert(copy(value))
|
||||
end
|
||||
|
||||
function BattleCheckpoint.validate(game, checkpoint)
|
||||
local model = checkpoint.runtime and checkpoint.runtime.battle
|
||||
local rngState = checkpoint.rng and checkpoint.rng.love
|
||||
if type(model) ~= "table" or type(model.origin) ~= "table"
|
||||
or type(rngState) ~= "string" or rngState == "" then
|
||||
return nil, "invalid_checkpoint", "Battle checkpoint data or RNG is missing."
|
||||
end
|
||||
local expectedOrigin = model.kind == "wild" and "wild_encounter"
|
||||
or model.kind == "trainer" and "trainer_encounter" or nil
|
||||
if not expectedOrigin or model.origin.kind ~= expectedOrigin
|
||||
or model.origin.map ~= checkpoint.runtime.overworld.map then
|
||||
return nil, "battle_origin_unsupported",
|
||||
"Battle continuation data is unsupported or inconsistent."
|
||||
end
|
||||
local party = checkpoint.save.party
|
||||
if type(party) ~= "table" or not validateBattler(game.data, model.player, #party) then
|
||||
return nil, "invalid_content", "Player battle state is invalid."
|
||||
end
|
||||
if model.kind == "wild" then
|
||||
if not validateMon(game.data, model.enemyMon)
|
||||
or not validateBattler(game.data, model.enemy, 1) then
|
||||
return nil, "invalid_content", "Wild opponent state is invalid."
|
||||
end
|
||||
else
|
||||
local trainer = game.data.trainers and game.data.trainers[model.oppClass]
|
||||
if type(trainer) ~= "table" or not integer(model.partyIndex, 1)
|
||||
or type(model.enemyParty) ~= "table" or #model.enemyParty == 0
|
||||
or not integer(model.enemyIndex, 1, #model.enemyParty)
|
||||
or not validateBattler(game.data, model.enemy, #model.enemyParty) then
|
||||
return nil, "invalid_content", "Trainer battle identity or roster is invalid."
|
||||
end
|
||||
for _, mon in ipairs(model.enemyParty) do
|
||||
if not validateMon(game.data, mon) then
|
||||
return nil, "invalid_content", "Trainer opponent state is invalid."
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, indices in ipairs({ model.participants, model.leveledUp }) do
|
||||
if type(indices) ~= "table" then
|
||||
return nil, "invalid_checkpoint", "Battle party reference set is missing."
|
||||
end
|
||||
for _, index in ipairs(indices) do
|
||||
if not integer(index, 1, #party) then
|
||||
return nil, "invalid_checkpoint", "Battle party reference is invalid."
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function applyBattler(target, captured, copy)
|
||||
for _, field in ipairs(BATTLER_FIELDS) do
|
||||
if field ~= "curStats" and field ~= "curTypes" and field ~= "curMoves" then
|
||||
target[field] = captured[field] ~= nil and clone(captured[field], copy) or nil
|
||||
end
|
||||
end
|
||||
target.curStats = captured.curStatsFromMon and target.mon.stats
|
||||
or assert(copy(captured.curStats))
|
||||
target.curTypes = captured.curTypesFromDefinition and target.def.types
|
||||
or assert(copy(captured.curTypes))
|
||||
target.curMoves = captured.curMovesFromMon and target.mon.moves
|
||||
or assert(copy(captured.curMoves))
|
||||
return target
|
||||
end
|
||||
|
||||
local function restoreIndexSet(indices, party)
|
||||
local out = {}
|
||||
for _, index in ipairs(indices or {}) do out[party[index]] = true end
|
||||
return next(out) and out or nil
|
||||
end
|
||||
|
||||
function BattleCheckpoint.restore(game, checkpoint, copy)
|
||||
local model = checkpoint.runtime.battle
|
||||
local battle
|
||||
if model.kind == "trainer" then
|
||||
battle = BattleState.newTrainer(game, model.oppClass, model.partyIndex)
|
||||
battle.enemyParty = assert(copy(model.enemyParty))
|
||||
battle.enemyIndex = model.enemyIndex
|
||||
else
|
||||
battle = BattleState.newWild(game, model.enemyMon.species, model.enemyMon.level)
|
||||
end
|
||||
|
||||
battle.player = BattleState.makeBattler(game.data,
|
||||
game.save.party[model.player.index], true, game.save)
|
||||
applyBattler(battle.player, model.player, copy)
|
||||
local enemyMon
|
||||
if model.kind == "trainer" then
|
||||
enemyMon = battle.enemyParty[model.enemy.index]
|
||||
else
|
||||
enemyMon = assert(copy(model.enemyMon))
|
||||
end
|
||||
battle.enemy = BattleState.makeBattler(game.data, enemyMon, false)
|
||||
applyBattler(battle.enemy, model.enemy, copy)
|
||||
|
||||
for _, field in ipairs(BATTLE_FIELDS) do
|
||||
battle[field] = model[field] ~= nil and clone(model[field], copy) or nil
|
||||
end
|
||||
battle.kind = model.kind
|
||||
battle.checkpointOrigin = assert(copy(model.origin))
|
||||
battle.participants = restoreIndexSet(model.participants, game.save.party)
|
||||
battle.leveledUp = restoreIndexSet(model.leveledUp, game.save.party)
|
||||
battle.sides = assert(copy(model.sides))
|
||||
battle.sides[1].battlers = { battle.player }
|
||||
battle.sides[2].battlers = { battle.enemy }
|
||||
battle.field = assert(copy(model.field))
|
||||
battle.field.sides = battle.sides
|
||||
battle.phase, battle.queue = "menu", {}
|
||||
battle.frame = 0
|
||||
battle.current, battle.afterQueue, battle.nextInsert = nil, nil, nil
|
||||
battle.pendingHit, battle.waitingUI, battle.waitingSound = nil, nil, nil
|
||||
battle.waitFrames, battle.draining, battle.animPlaying = nil, nil, nil
|
||||
battle.introText, battle.introBalls, battle.introSlide = nil, nil, nil
|
||||
battle.showPlayerBack, battle.showEnemyTrainer, battle.showEnemyBalls = nil, nil, nil
|
||||
battle.player.shownHP, battle.player.shownStatus =
|
||||
battle.player.mon.hp, battle.player.mon.status
|
||||
battle.enemy.shownHP, battle.enemy.shownStatus =
|
||||
battle.enemy.mon.hp, battle.enemy.mon.status
|
||||
|
||||
local ow = game.overworld
|
||||
if not ow or type(ow.restoreBattleContinuation) ~= "function"
|
||||
or ow:restoreBattleContinuation(battle, battle.checkpointOrigin) ~= true then
|
||||
error("battle continuation reconstruction is unavailable", 0)
|
||||
end
|
||||
if type(game.restoreCheckpointBattle) ~= "function" then
|
||||
error("game has no battle checkpoint reconstruction path", 0)
|
||||
end
|
||||
game:restoreCheckpointBattle(battle)
|
||||
local setState = love and love.math and love.math.setRandomState
|
||||
if type(setState) ~= "function" then error("battle RNG restore is unavailable", 0) end
|
||||
setState(checkpoint.rng.love)
|
||||
return battle
|
||||
end
|
||||
|
||||
local function captureExtensions(battle, copy)
|
||||
local sides = {}
|
||||
for i = 1, 2 do
|
||||
|
||||
+46
-3
@@ -81,6 +81,15 @@ local function inspectBattle(ow, battle)
|
||||
return refusal("battle", "battle_phase_busy",
|
||||
"Wait for an ordinary player decision before creating a checkpoint.")
|
||||
end
|
||||
for _, battler in ipairs({ battle.player, battle.enemy }) do
|
||||
if battler.shownHP ~= battler.mon.hp
|
||||
or battler.shownStatus ~= battler.mon.status
|
||||
or battler.drainFloor ~= nil or battler.drainHold ~= nil
|
||||
or battler.faintQueued then
|
||||
return refusal("battle", "battle_phase_busy",
|
||||
"Wait for battle status and HP presentation to settle.")
|
||||
end
|
||||
end
|
||||
return { canCapture = true, canRestore = true, kind = "battle" }
|
||||
end
|
||||
|
||||
@@ -221,8 +230,8 @@ local function validate(game, checkpoint)
|
||||
if checkpoint.format ~= Checkpoint.FORMAT then
|
||||
return nil, "unsupported_format", "This checkpoint format is not supported."
|
||||
end
|
||||
if checkpoint.kind ~= "overworld" then
|
||||
return nil, "unsupported_runtime_kind", "Only overworld checkpoints are supported."
|
||||
if checkpoint.kind ~= "overworld" and checkpoint.kind ~= "battle" then
|
||||
return nil, "unsupported_runtime_kind", "This checkpoint runtime kind is not supported."
|
||||
end
|
||||
|
||||
local copy, copyErr = dataCopy(checkpoint)
|
||||
@@ -288,6 +297,13 @@ local function validate(game, checkpoint)
|
||||
return nil, "invalid_content",
|
||||
"Checkpoint references unavailable or invalid game content."
|
||||
end
|
||||
if copy.kind == "battle" then
|
||||
local battleOk, battleCode, battleMessage = BattleCheckpoint.validate(game, copy)
|
||||
if not battleOk then return nil, battleCode, battleMessage end
|
||||
elseif copy.runtime.battle ~= nil or copy.rng ~= nil then
|
||||
return nil, "invalid_checkpoint",
|
||||
"Overworld checkpoint contains unexpected battle state."
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
@@ -305,6 +321,9 @@ local function apply(game, checkpoint, options)
|
||||
error("game has no checkpoint reconstruction path", 0)
|
||||
end
|
||||
game:restoreCheckpointSave(save)
|
||||
if checkpoint.kind == "battle" then
|
||||
BattleCheckpoint.restore(game, checkpoint, dataCopy)
|
||||
end
|
||||
end
|
||||
|
||||
local function equalData(a, b)
|
||||
@@ -313,6 +332,28 @@ local function equalData(a, b)
|
||||
return okA and okB and encodedA == encodedB
|
||||
end
|
||||
|
||||
local function firstDifference(a, b, path)
|
||||
path = path or "$"
|
||||
if type(a) ~= type(b) then return path .. " (type)" end
|
||||
if type(a) ~= "table" then
|
||||
if a ~= b then return path end
|
||||
return nil
|
||||
end
|
||||
for key, value in pairs(a) do
|
||||
if b[key] == nil and value ~= nil then
|
||||
return path .. "." .. tostring(key) .. " (missing)"
|
||||
end
|
||||
local found = firstDifference(value, b[key], path .. "." .. tostring(key))
|
||||
if found then return found end
|
||||
end
|
||||
for key, value in pairs(b) do
|
||||
if a[key] == nil and value ~= nil then
|
||||
return path .. "." .. tostring(key) .. " (unexpected)"
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Checkpoint.restore(game, checkpoint)
|
||||
local capability = Checkpoint.inspect(game)
|
||||
if not capability.canRestore then
|
||||
@@ -329,7 +370,9 @@ function Checkpoint.restore(game, checkpoint)
|
||||
if ok then
|
||||
local restored, verifyCode = Checkpoint.capture(game)
|
||||
if restored and equalData(restored, validated) then return true end
|
||||
err = "restored state did not match checkpoint: " .. tostring(verifyCode)
|
||||
err = restored and ("restored state differed at "
|
||||
.. tostring(firstDifference(validated, restored) or "canonical encoding"))
|
||||
or ("restored state could not be captured: " .. tostring(verifyCode))
|
||||
end
|
||||
|
||||
local rolledBack, rollbackErr = pcall(apply, game, rollback, options)
|
||||
|
||||
Reference in New Issue
Block a user