mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-03 08:07:07 -04:00
412 lines
14 KiB
Lua
412 lines
14 KiB
Lua
-- Read-only checks against the active game's data. A clean property report
|
|
-- deliberately stays "unchecked" until encounter/event/RNG provenance can
|
|
-- be proved. Never call a structurally valid record fully legal.
|
|
local Gen = require("Gen")
|
|
local Properties = require("Properties")
|
|
local MonOps = require("MonOps")
|
|
local L = {}
|
|
local IV_KEYS = { "hp", "atk", "def", "spe", "spa", "spd" }
|
|
local DV_KEYS = { "hp", "attack", "defense", "speed", "special" }
|
|
local function integer(v, lo, hi)
|
|
return type(v) == "number" and v == v and v == math.floor(v) and v >= lo and v <= hi
|
|
end
|
|
L.integer = integer
|
|
function L.mon(S, mon)
|
|
local r = { errors = 0, warnings = 0, checks = {}, status = "unchecked" }
|
|
local function add(kind, field, message)
|
|
r.checks[#r.checks + 1] = { kind = kind, field = field, message = message }
|
|
if kind == "error" then
|
|
r.errors = r.errors + 1
|
|
end
|
|
if kind == "warning" then
|
|
r.warnings = r.warnings + 1
|
|
end
|
|
end
|
|
local function range(field, v, lo, hi)
|
|
if not integer(v, lo, hi) then
|
|
add("error", field, field .. " must be a whole number from " .. lo .. " to " .. hi)
|
|
end
|
|
end
|
|
if type(mon) ~= "table" then
|
|
add("error", "record", "Missing Pokemon record")
|
|
r.status = "invalid"
|
|
return r
|
|
end
|
|
for _, key in ipairs({
|
|
"ivs",
|
|
"evs",
|
|
"dvs",
|
|
"statExp",
|
|
"moves",
|
|
"pp",
|
|
"ppBonuses",
|
|
"contest",
|
|
"stats",
|
|
}) do
|
|
if mon[key] ~= nil and type(mon[key]) ~= "table" then
|
|
add("error", key, key .. " must be a property table")
|
|
end
|
|
end
|
|
if r.errors > 0 then
|
|
r.status = "invalid"
|
|
return r
|
|
end
|
|
local g = Gen.ofState(S)
|
|
local species = mon.species or mon.speciesId
|
|
local def = S.data and S.data.pokemon and S.data.pokemon[species]
|
|
if not def then
|
|
add("error", "species", "Species is missing from the active game catalog")
|
|
end
|
|
range("level", mon.level, 1, 100)
|
|
for _, key in ipairs({ "happiness", "friendship" }) do
|
|
if mon[key] ~= nil then
|
|
range(key, mon[key], 0, 255)
|
|
end
|
|
end
|
|
if mon.ppBonusesPacked ~= nil then
|
|
range("packed PP Ups", mon.ppBonusesPacked, 0, 255)
|
|
end
|
|
local Ops = require("Ops")
|
|
if
|
|
mon.nickname
|
|
and (Ops.nicknameLength(mon.nickname) > 10 or not Ops.nicknameUsable(S, mon.nickname))
|
|
then
|
|
add(
|
|
"error",
|
|
"nickname",
|
|
"Nickname exceeds the game's name limit or uses unsupported characters"
|
|
)
|
|
end
|
|
for _, d in ipairs(Properties.all(S)) do
|
|
local v = Properties.get(mon, d)
|
|
if d.text then
|
|
if Ops.nicknameLength(v) > d.max or not Ops.nicknameUsable(S, v) then
|
|
add("error", d.key, d.label .. " contains an invalid name")
|
|
end
|
|
else
|
|
local parsed, err = Properties.parse(d, v)
|
|
if parsed == nil then
|
|
add("error", d.key, err)
|
|
end
|
|
end
|
|
end
|
|
if g == 3 then
|
|
local total = 0
|
|
for _, k in ipairs(IV_KEYS) do
|
|
range("IV " .. k, mon.ivs and mon.ivs[k], 0, 31)
|
|
local ev = mon.evs and mon.evs[k] or 0
|
|
range("EV " .. k, ev, 0, 255)
|
|
if integer(ev, 0, 255) then
|
|
total = total + ev
|
|
end
|
|
end
|
|
if total > 510 then
|
|
add("error", "evs", "Total EVs exceed 510 (" .. total .. ")")
|
|
end
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
local pid = mon.personality
|
|
if integer(pid, 0, 4294967295) then
|
|
if mon.nature ~= nil and mon.nature ~= pid % 25 then
|
|
add("error", "nature", "Nature disagrees with PID")
|
|
end
|
|
if mon.gender and mon.gender ~= Pokemon.gender(species, pid) then
|
|
add("error", "gender", "Gender disagrees with the species and PID")
|
|
end
|
|
local pair = Pokemon.abilities(species)
|
|
local slot = mon.abilityNum or (pair[2] and pair[2] ~= 0 and pid % 2 or 0)
|
|
range("ability slot", slot, 0, 1)
|
|
local expected = integer(slot, 0, 1) and pair[slot + 1] or nil
|
|
if expected == 0 or not expected or (mon.ability and mon.ability ~= expected) then
|
|
add("error", "ability", "Ability is unavailable in the selected species slot")
|
|
end
|
|
local bit = require("bit")
|
|
local shiny = bit.bxor(
|
|
tonumber(mon.otId) or 0,
|
|
tonumber(mon.otSecretId) or 0,
|
|
math.floor(pid / 65536),
|
|
pid % 65536
|
|
) < 8
|
|
if mon.isShiny ~= nil and mon.isShiny ~= shiny then
|
|
add("error", "shiny", "Shiny flag disagrees with PID and trainer IDs")
|
|
end
|
|
end
|
|
if
|
|
integer(mon.metLevel, 0, 100)
|
|
and integer(mon.level, 1, 100)
|
|
and mon.metLevel > mon.level
|
|
then
|
|
add("error", "metLevel", "Met level exceeds current level")
|
|
end
|
|
local ball = mon.pokeball or 4
|
|
if (mon.isEgg or mon.egg) and ball ~= 4 then
|
|
add("error", "pokeball", "Gen 3 eggs must use a Poke Ball")
|
|
end
|
|
if type(mon.ribbons) == "number" then
|
|
range("ribbon word", mon.ribbons, 0, 4294967295)
|
|
end
|
|
local virus = tonumber(mon.pokerus) or 0
|
|
if integer(virus, 0, 255) then
|
|
local strain, days = math.floor(virus / 16), virus % 16
|
|
if days > 4 or (strain == 0 and days > 0) or (strain > 0 and days > strain % 4 + 1) then
|
|
add("error", "pokerus", "Pokerus days are inconsistent with its strain")
|
|
end
|
|
end
|
|
if math.floor(Properties.ribbonWord(mon) / 2 ^ 27) % 16 ~= 0 then
|
|
add("error", "ribbons", "Unused ribbon bits must be zero")
|
|
end
|
|
if mon.modernFatefulEncounter or mon.ribbons or mon.championRibbon then
|
|
add(
|
|
"warning",
|
|
"ribbons",
|
|
"Ribbon and event eligibility require encounter and distribution history"
|
|
)
|
|
end
|
|
else
|
|
for _, k in ipairs(DV_KEYS) do
|
|
range("DV " .. k, mon.dvs and mon.dvs[k], 0, 15)
|
|
range("Stat experience " .. k, mon.statExp and mon.statExp[k] or 0, 0, 65535)
|
|
end
|
|
local d = mon.dvs or {}
|
|
if
|
|
integer(d.attack, 0, 15)
|
|
and integer(d.defense, 0, 15)
|
|
and integer(d.speed, 0, 15)
|
|
and integer(d.special, 0, 15)
|
|
then
|
|
local hp = d.attack % 2 * 8 + d.defense % 2 * 4 + d.speed % 2 * 2 + d.special % 2
|
|
if d.hp ~= hp then
|
|
add("error", "dvs.hp", "HP DV must be derived from the other four DVs")
|
|
end
|
|
if g == 2 then
|
|
local shiny = d.defense == 10
|
|
and d.speed == 10
|
|
and d.special == 10
|
|
and math.floor(d.attack / 2) % 2 == 1
|
|
if mon.shiny ~= nil and mon.shiny ~= shiny then
|
|
add("error", "shiny", "Shininess disagrees with DVs")
|
|
end
|
|
if
|
|
mon.gender ~= nil
|
|
and def
|
|
and mon.gender ~= require("src.battle.gen2.Mon").gender(def, d)
|
|
then
|
|
add("error", "gender", "Gender disagrees with species and DVs")
|
|
end
|
|
if
|
|
species == "UNOWN"
|
|
and mon.unownLetter ~= nil
|
|
and mon.unownLetter ~= require("src.core.gen2.Unown").letterFromDVs(d)
|
|
then
|
|
add("error", "form", "Unown form disagrees with DVs")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if g == 2 then
|
|
local virus = mon.pokerus or 0
|
|
range("pokerus", virus, 0, 255)
|
|
if integer(virus, 0, 255) then
|
|
local strain, days = math.floor(virus / 16), virus % 16
|
|
if strain > 8 or days > strain % 4 + 1 or (strain == 0 and days > 0) then
|
|
add("error", "pokerus", "Gen 2 Pokerus strain and days are inconsistent")
|
|
end
|
|
end
|
|
end
|
|
if g >= 2 and (mon.egg or mon.isEgg) and mon.level ~= 5 then
|
|
add("error", "level", "An unhatched egg must be level 5 in this generation")
|
|
end
|
|
local statuses = { SLP = true, PSN = true, BRN = true, FRZ = true, PAR = true, TOX = true }
|
|
if type(mon.status) == "number" and g == 3 then
|
|
local allowed = integer(mon.status, 0, 7)
|
|
or mon.status == 8
|
|
or mon.status == 16
|
|
or mon.status == 32
|
|
or mon.status == 64
|
|
or mon.status == 128
|
|
if not allowed then
|
|
add("error", "status", "Status bits contain an invalid combination")
|
|
end
|
|
elseif mon.status ~= nil and not statuses[mon.status] then
|
|
add("error", "status", "Unknown status condition")
|
|
end
|
|
if mon.status == "SLP" then
|
|
range("sleep", mon.sleep or 1, 1, 7)
|
|
end
|
|
local seen, occupied = {}, 0
|
|
for slot = 1, 4 do
|
|
local mv = mon.moves and mon.moves[slot]
|
|
local id = type(mv) == "table" and (mv.moveId or mv.id) or mv
|
|
if id and id ~= 0 then
|
|
occupied = occupied + 1
|
|
local md = S.data and S.data.moves and S.data.moves[id]
|
|
if not md then
|
|
add(
|
|
"error",
|
|
"move" .. slot,
|
|
"Move " .. tostring(id) .. " is missing from this game's catalog"
|
|
)
|
|
end
|
|
if seen[id] then
|
|
add("error", "move" .. slot, "Duplicate move in slot " .. slot)
|
|
end
|
|
seen[id] = true
|
|
local ups = MonOps.getPpUps(mon, slot)
|
|
range("PP Ups " .. slot, ups, 0, 3)
|
|
if type(mv) == "table" and mv.ppUps ~= nil then
|
|
range("stored PP Ups " .. slot, mv.ppUps, 0, 3)
|
|
end
|
|
local base = MonOps.getBasePp(S.data, mon, slot)
|
|
local max = MonOps.calcMaxPp(base, ups, g)
|
|
local pp = type(mv) == "table" and mv.pp or (mon.pp and mon.pp[slot])
|
|
range("PP " .. slot, pp, 0, max)
|
|
if (mon.egg or mon.isEgg) and (ups ~= 0 or pp ~= base) then
|
|
add("error", "move" .. slot, "Egg moves must have base PP and no PP Ups")
|
|
end
|
|
if md and (md.name == "SKETCH" or id == "SKETCH" or id == 166) and ups > 0 then
|
|
add("error", "move" .. slot, "Sketch cannot use PP Ups")
|
|
end
|
|
local found = false
|
|
local learnset = def and def.learnset or {}
|
|
if g == 3 then
|
|
learnset = require("src.core.game3.pokemon").learnset(species)
|
|
end
|
|
for _, e in ipairs(learnset) do
|
|
if
|
|
(e.move or e.id or e[2]) == id
|
|
and (tonumber(e.level or e[1]) or 1) <= (tonumber(mon.level) or 0)
|
|
then
|
|
found = true
|
|
end
|
|
end
|
|
if g == 3 and not found then
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
for i = 0, 57 do
|
|
if Pokemon.moveFromTmItem(289 + i) == id and Pokemon.canLearnTmIndex(species, i) then
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
add(
|
|
found and "pass" or "warning",
|
|
"move" .. slot,
|
|
found and ("Slot " .. slot .. ": current species learns this move")
|
|
or ("Slot " .. slot .. ": check breeding, pre-evolution, tutor, event or trade origin")
|
|
)
|
|
else
|
|
if mon.pp and (mon.pp[slot] or 0) ~= 0 then
|
|
add("error", "move" .. slot, "Empty move slot has nonzero PP")
|
|
end
|
|
if MonOps.getPpUps(mon, slot) ~= 0 then
|
|
add("error", "move" .. slot, "Empty move slot has PP Ups")
|
|
end
|
|
end
|
|
end
|
|
if occupied == 0 and not (mon.egg or mon.isEgg) then
|
|
add("error", "moves", "A non-egg Pokemon needs at least one move")
|
|
end
|
|
if mon.moves then
|
|
for slot in pairs(mon.moves) do
|
|
if not integer(slot, 1, 4) then
|
|
add("error", "moves", "Move slots must be numbered 1-4")
|
|
end
|
|
end
|
|
end
|
|
if integer(mon.level, 1, 100) and def then
|
|
local exp = Gen.exp(mon)
|
|
local lower, upper
|
|
if g == 3 then
|
|
local Summary = require("src.core.game3.summary_data")
|
|
local growth = require("src.core.game3.pokemon").growthRate(species)
|
|
lower = Summary.expForLevel(growth, mon.level)
|
|
upper = Summary.expForLevel(growth, math.min(100, mon.level + 1))
|
|
elseif g == 2 then
|
|
local Mon = require("src.battle.gen2.Mon")
|
|
local growth = Mon.growthFor(S.data, def.growthRate)
|
|
lower, upper =
|
|
Mon.experienceForLevel(growth, mon.level),
|
|
Mon.experienceForLevel(growth, math.min(100, mon.level + 1))
|
|
else
|
|
local Growth = require("src.pokemon.Growth")
|
|
lower = Growth.expForLevel(def.growthRate, mon.level, S.data.growth_rates)
|
|
upper = Growth.expForLevel(def.growthRate, math.min(100, mon.level + 1), S.data.growth_rates)
|
|
end
|
|
if
|
|
not integer(exp, lower, 16777215)
|
|
or (mon.level < 100 and exp >= upper)
|
|
or (mon.level == 100 and exp ~= lower)
|
|
then
|
|
add("error", "experience", "Experience does not match the level and growth curve")
|
|
end
|
|
end
|
|
local held = mon.heldItem or mon.item
|
|
if held and held ~= 0 and held ~= "NONE" then
|
|
local item = S.data and S.data.items and S.data.items[held]
|
|
if not item then
|
|
add("error", "heldItem", "Held item is missing from this game's catalog")
|
|
elseif g == 1 then
|
|
add("error", "heldItem", "Gen 1 does not store held items")
|
|
elseif not Ops.itemHoldable(S, held) then
|
|
add("error", "heldItem", "Key items and HMs cannot be held")
|
|
end
|
|
end
|
|
if r.errors == 0 then
|
|
local copy = require("src.mods.Merge").deepCopy(mon)
|
|
local ok = pcall(MonOps.recalc, S.data, copy, g)
|
|
if ok then
|
|
local max = copy.maxHp or (copy.stats and copy.stats.hp)
|
|
if max then
|
|
range("current HP", mon.hp, 0, max)
|
|
end
|
|
for key, value in pairs(copy.stats or {}) do
|
|
if mon.stats and mon.stats[key] ~= nil and mon.stats[key] ~= value then
|
|
add("error", "stats." .. key, "Calculated " .. key .. " stat disagrees with stored stat")
|
|
end
|
|
end
|
|
else
|
|
add("warning", "stats", "Stats could not be checked against the active game data")
|
|
end
|
|
end
|
|
if g == 3 and (mon.isEgg or mon.egg) then
|
|
if (mon.language or 2) ~= 1 then
|
|
add("error", "language", "Gen 3 eggs must use the Japanese language flag")
|
|
end
|
|
for _, v in pairs(mon.contest or {}) do
|
|
if v ~= 0 then
|
|
add("error", "contest", "Eggs cannot have contest conditions")
|
|
break
|
|
end
|
|
end
|
|
end
|
|
add(
|
|
"warning",
|
|
"encounter",
|
|
"Encounter tables, event distributions, transfer history and PID/IV RNG correlation are not fully verified"
|
|
)
|
|
if r.errors > 0 then
|
|
r.status = "invalid"
|
|
end
|
|
return r
|
|
end
|
|
function L.save(S)
|
|
local report = { entries = {}, errors = 0, warnings = 0 }
|
|
local function inspect(mon, label, box, slot)
|
|
local r = L.mon(S, mon)
|
|
report.entries[#report.entries + 1] =
|
|
{ mon = mon, label = label, report = r, box = box, slot = slot }
|
|
report.errors, report.warnings = report.errors + r.errors, report.warnings + r.warnings
|
|
end
|
|
for i, mon in ipairs(S.save.party or {}) do
|
|
inspect(mon, "Party " .. i, nil, i)
|
|
end
|
|
for b, box in ipairs(require("Ops").boxes(S)) do
|
|
for i = 1, Gen.boxCapacity(S.save) do
|
|
if box[i] then
|
|
inspect(box[i], "Box " .. b .. " / " .. i, b, i)
|
|
end
|
|
end
|
|
end
|
|
return report
|
|
end
|
|
return L
|