mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-04 08:21:40 -04:00
263 lines
9.3 KiB
Lua
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")
|