Files
gen1recomp/tests/save_editor_property_legality_test.lua
T
bryanthaboi 20ab97abf8 save editor
2026-10-01 21:31:40 -04:00

263 lines
9.3 KiB
Lua

package.path = package.path
.. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
_G.love = require("tests.love_stub")
require("tests.game3_cache").mountOrSkip("save_editor_property_legality_test", "pokemon/meta.lua")
local App = require("tools.save-editor.App")
local Ops = require("Ops")
local Gen = require("Gen")
local L = require("Legality")
local P = require("Properties")
local History = require("History")
local SD = require("src.core.SaveData")
local Copy = require("src.mods.Merge").deepCopy
local path = os.tmpname() .. "-properties.lua"
local file = assert(io.open(path, "wb"))
file:write(SD.encode(Gen.newGame("firered")))
file:close()
App.load(path, { version = "firered", embedded = true })
local S = App.getState()
assert(not S.loadError, S.status)
Ops.partyAdd(S)
Ops.selectParty(S, 1)
local function unique(list, field, records)
local seen = {}
for _, key in ipairs(list) do
local id = tonumber(records[key][field])
if id then
assert(not seen[id], "duplicate picker alias " .. key)
seen[id] = true
end
end
end
unique(S.cat.items, "itemId", S.data.items)
unique(S.cat.species, "speciesId", S.data.pokemon)
unique(S.cat.moves, "moveId", S.data.moves)
local potions = Ops.itemSearch(S, "potion")
assert(#potions >= 4, "Gen 3 search resolves display names")
local exact = Ops.itemSearch(S, "13")
local found = false
for _, id in ipairs(exact) do
if tonumber(S.data.items[id].itemId) == 13 then
found = true
end
end
assert(found, "Gen 3 search resolves numeric item IDs")
local mon = S.editingMon
local count = 0
local function check(ok, message)
count = count + 1
assert(ok, message)
end
local function invalid(key, value)
local bad = Copy(mon)
bad[key] = value
local ok, report = pcall(L.mon, S, bad)
check(ok and report.errors > 0, "invalid " .. key .. " is reported without crashing")
end
check(L.mon(S, mon).errors == 0, "created Gen 3 properties pass")
check(L.mon(S, mon).status == "unchecked", "property success does not certify encounter legality")
for _, pair in ipairs({
{ "otId", 4321 },
{ "otSecretId", 1234 },
{ "otName", "ASH" },
{ "otGender", 1 },
{ "language", 7 },
{ "metLevel", 3 },
{ "metLocation", 88 },
{ "metGame", 2 },
{ "pokeball", 3 },
{ "markings", 9 },
{ "pokerus", 17 },
{ "contest.cool", 100 },
{ "ribbon.Cool", 4 },
{ "ribbon.Champion", true },
{ "modernFatefulEncounter", true },
}) do
check(Ops.setMonProperty(S, mon, pair[1], pair[2]), "writes " .. pair[1])
check(
P.get(mon, P.find(S, pair[1])) == pair[2]
or (pair[2] == true and P.get(mon, P.find(S, pair[1])) == 1),
"reads " .. pair[1]
)
end
check(require("src.core.game3.rse.ribbons").get(mon, "fateful") == 1, "native fateful bit agrees")
check(Ops.setMonProperty(S, mon, "modernFatefulEncounter", false), "fateful bit can clear")
check(require("src.core.game3.rse.ribbons").get(mon, "fateful") == 0, "native fateful bit clears")
check(not Ops.setMonProperty(S, mon, "language", 6), "unused language refused")
check(not Ops.setMonProperty(S, mon, "personality", 4294967296), "PID overflow refused")
check(not Ops.setMonProperty(S, mon, "ribbon.Cool", 5), "contest rank overflow refused")
local summary = require("src.core.game3.summary_data")
local pokemon = require("src.core.game3.pokemon")
local exp = summary.expForLevel(pokemon.growthRate(mon.species), 15) + 1
check(Ops.setExperience(S, mon, exp), "exact experience edited")
check(
mon.level == 15 and Gen.exp(mon) == exp,
"experience derives level and preserves exact remainder"
)
check(not Ops.setExperience(S, mon, 99999999), "experience overflow refused")
check(Ops.setCurrentHp(S, mon, 0) and mon.hp == 0, "fainted HP allowed")
check(not Ops.setCurrentHp(S, mon, mon.maxHp + 1), "HP above calculated maximum refused")
check(Ops.setMonStatus(S, mon, "SLP") and mon.sleep == 1, "sleep sets a valid duration")
check(not Ops.setMonStatus(S, mon, "unknown"), "unknown status refused")
check(Ops.setMonStatus(S, mon, nil), "status can clear")
check(Ops.setPpUps(S, mon, 1, 2), "table-form Gen 3 move PP Ups edited")
check(Ops.setPp(S, mon, 1, 3), "table-form Gen 3 move PP edited")
check(mon.moves[1].pp == 3 and mon.pp[1] == 3, "Gen 3 table and parallel PP agree")
require("MonOps").recalc(S.data, mon, 3)
check(mon.moves[1].pp == 3 and mon.pp[1] == 3, "recalculation preserves edited PP")
check(not Ops.itemHoldable(S, "364"), "TM Case cannot be held")
check(not Ops.itemHoldable(S, "339"), "HM cannot be held")
check(Ops.itemHoldable(S, "13"), "Potion can be held")
for _, pair in ipairs({
{ "ivs", "broken" },
{ "moves", false },
{ "abilityNum", "broken" },
{ "personality", -1 },
{ "ribbons", -1 },
{ "language", 6 },
{ "level", 101 },
{ "pokerus", 31 },
{ "metLevel", 99 },
{ "otSecretId", 65536 },
{ "status", "unknown" },
{ "friendship", 256 },
{ "happiness", -1 },
{ "ppBonusesPacked", -1 },
{ "ppBonuses", "broken" },
}) do
invalid(pair[1], pair[2])
end
local bad = Copy(mon)
bad.evs = { hp = 255, atk = 255, def = 1, spe = 0, spa = 0, spd = 0 }
check(L.mon(S, bad).errors > 0, "EV total overflow detected")
bad = Copy(mon)
bad.moves[2] = Copy(bad.moves[1])
check(L.mon(S, bad).errors > 0, "duplicate moves detected")
bad = Copy(mon)
bad.egg = true
bad.isEgg = true
check(L.mon(S, bad).errors >= 3, "egg level, ball and language conflicts detected")
bad = Copy(mon)
bad.moves[2], bad.moves[3], bad.moves[4] = 0, 0, 0
bad.pp = { bad.moves[1].pp, 0, 0, 0 }
bad.ppBonusesPacked = 0
require("MonOps").maxAllPpUps(S.data, bad, 3)
check(
(bad.pp[2] or 0) == 0 and require("MonOps").getPpUps(bad, 2) == 0,
"max PP leaves empty native slots empty"
)
bad.egg, bad.isEgg = true, true
require("MonOps").maxAllPpUps(S.data, bad, 3)
check(require("MonOps").getPpUps(bad, 1) == 0, "egg max PP clears PP Ups")
bad.moves[1].pp = 0
local eggReport = L.mon(S, bad)
local ppError = false
for _, entry in ipairs(eggReport.checks) do
if entry.message == "Egg moves must have base PP and no PP Ups" then
ppError = true
end
end
check(ppError, "egg depleted PP is reported")
check(L.mon(S, mon).errors == 0, "valid changes still pass property checks")
check(App.save(), "native edited save writes")
local output = assert(require("SaveIO").load(path))
for _, key in ipairs({
"otId",
"otSecretId",
"otName",
"otGender",
"language",
"metLevel",
"metLocation",
"metGame",
"pokeball",
"markings",
"pokerus",
"ribbons",
"contest",
"exp",
"hp",
"moves",
"pp",
"ppBonusesPacked",
"maxPp",
}) do
local function eq(a, b)
if type(a) == "table" then
for k, v in pairs(a) do
eq(v, b[k])
end
else
check(a == b, "saved " .. key .. " preserved")
end
end
eq(mon[key], output.party[1][key])
end
local level = mon.level
check(History.undo(S), "undo after saving succeeds")
check(S.dirty, "undo away from saved state marks dirty")
check(History.redo(S), "redo after saving succeeds")
check(not S.dirty and S.save.party[1].level == level, "redo to saved state clears dirty")
-- The imported Gen 3 schema has nature, origin and ribbon controls that the
-- Gen 1 layout sweep cannot exercise. Include open named-choice grids and
-- the bottom of each form at phone, landscape and desktop sizes.
local Kit = require("Kit")
S.editingMon, S.mobileInspector, S.tab = S.save.party[1], true, "party"
for _, size in ipairs({ { 320, 568 }, { 390, 844 }, { 640, 360 }, { 1280, 720 } }) do
local W, H = size[1], size[2]
love.graphics.getDimensions = function()
return W, H
end
love.window.getSafeArea = function()
return 0, 0, W, H
end
for _, form in ipairs({
{ "main", "nature" },
{ "origin", "language" },
{ "origin", "pokeball" },
{ "origin", "metGame" },
{ "extras" },
{ "checks" },
}) do
for _, offset in ipairs({ 0, 400, 10000 }) do
S.monSection, S.propertyChoice, S.inspectorScroll = form[1], form[2], offset
Kit.audit = {}
App.draw()
for _, rect in ipairs(Kit.audit) do
local clip = rect.clip or { x = 0, y = 0, w = W, h = H }
local x1, y1 = math.max(rect.x, clip.x), math.max(rect.y, clip.y)
local x2, y2 =
math.min(rect.x + rect.w, clip.x + clip.w), math.min(rect.y + rect.h, clip.y + clip.h)
if x1 < x2 and y1 < y2 then
if rect.class == "control" or rect.class == "row" then
check(rect.w >= 43.9 and rect.h >= 43.9, "native form touch target: " .. rect.label)
end
check(
x1 >= -0.5 and y1 >= -0.5 and x2 <= W + 0.5 and y2 <= H + 0.5,
"native form bounds: " .. rect.label
)
if rect.class == "scrollbar" then
for _, control in ipairs(Kit.audit) do
if control.class == "control" then
local c = control.clip or { x = 0, y = 0, w = W, h = H }
local cx1, cy1 = math.max(control.x, c.x), math.max(control.y, c.y)
local cx2, cy2 =
math.min(control.x + control.w, c.x + c.w),
math.min(control.y + control.h, c.y + c.h)
check(
math.min(x2, cx2) <= math.max(x1, cx1) or math.min(y2, cy2) <= math.max(y1, cy1),
"native form scrollbar covers " .. control.label
)
end
end
end
end
end
Kit.audit = nil
end
end
end
App.unload()
os.remove(path)
print("save editor property legality: " .. count .. " checks passed")