fixes to id sync and also box

This commit is contained in:
bryanthaboi
2026-10-06 11:00:16 -04:00
parent 581262bc2f
commit 927f3ecc6c
99 changed files with 13369 additions and 509 deletions
+333
View File
@@ -0,0 +1,333 @@
local GameVersion = require("src.core.GameVersion")
local Serializer = require("src.core.SaveSerializer")
local Store = { PATH = "box/storage.lua", FORMAT = 1, BOXES = 25, SLOTS = 60 }
Store.THEMES = { "Launcher", "Forest", "Sky", "Brick", "Sunset", "Ocean", "Showcase" }
function Store.copy(value, seen)
if type(value) ~= "table" then return value end
seen = seen or {}
if seen[value] then return seen[value] end
local out = {}; seen[value] = out
for k, v in pairs(value) do out[Store.copy(k, seen)] = Store.copy(v, seen) end
return out
end
local function integer(n, lo, hi)
return type(n) == "number" and n == math.floor(n) and n >= lo and n <= hi
end
local function archivesValid(entry)
if entry.archives == nil then return true end
if type(entry.archives) ~= "table" then return false end
for index, original in pairs(entry.archives) do
if not integer(index, 1, #entry.archives) or type(original) ~= "table"
or original.id ~= entry.id or not GameVersion.VERSIONS[original.version]
or original.generation ~= GameVersion.generation(original.version)
or type(original.mon) ~= "table" or type(original.display) ~= "table"
or original.archives ~= nil then return false end
end
return true
end
function Store.new()
local state = { format = Store.FORMAT, revision = 0, nextId = 1, boxes = {} }
for b = 1, Store.BOXES do state.boxes[b] = { name = "BOX " .. b, mons = {} } end
return state
end
function Store.validate(state)
if type(state) ~= "table" or state.format ~= Store.FORMAT
or not integer(state.revision, 0, 2^53 - 1)
or not integer(state.nextId, 1, 2^53 - 1) or type(state.boxes) ~= "table" then
return nil, "The Box storage format is invalid."
end
local ids = {}
for b, box in pairs(state.boxes) do
if not integer(b, 1, Store.BOXES) or type(box) ~= "table"
or type(box.name) ~= "string" or #box.name > 128 or type(box.mons) ~= "table" then
return nil, "A Box header is invalid."
end
if box.theme ~= nil then
local found = false
for _, theme in ipairs(Store.THEMES) do if box.theme == theme then found = true end end
if not found then return nil, "A Box theme is invalid." end
end
if box.wallpaper ~= nil and (type(box.wallpaper) ~= "string"
or not box.wallpaper:match("^box/showcase/%d+%.png$")) then
return nil, "A Box wallpaper is invalid."
end
if box.showcaseMusic ~= nil then
local found = false
for _, track in ipairs(require("src.box.Showcase").MUSIC) do if track == box.showcaseMusic then found = true end end
if not found or box.theme ~= "Showcase" then return nil, "A Box music track is invalid." end
end
for slot, entry in pairs(box.mons) do
if not integer(slot, 1, Store.SLOTS) or type(entry) ~= "table"
or not integer(entry.id, 1, state.nextId - 1) or ids[entry.id]
or not GameVersion.VERSIONS[entry.version] or type(entry.mon) ~= "table"
or entry.generation ~= GameVersion.generation(entry.version)
or type(entry.display) ~= "table" or not archivesValid(entry) then
return nil, "A stored Pokémon record is invalid."
end
ids[entry.id] = true
if entry.markings ~= nil and not integer(entry.markings, 0, 15)
or entry.depositorId ~= nil and not integer(entry.depositorId, 0, 4294967295)
or entry.tags ~= nil and (type(entry.tags) ~= "string" or #entry.tags > 128) then
return nil, "A Pokémon's local labels are invalid."
end
end
end
for b = 1, Store.BOXES do
if not state.boxes[b] then return nil, "A Box is missing." end
end
if state.progress ~= nil and type(state.progress) ~= "table" then return nil, "Gift progress is invalid." end
for key, value in pairs(state.progress or {}) do
if type(key) ~= "string" or #key > 1024 or not integer(value, 0, Store.BOXES * Store.SLOTS) then
return nil, "Gift progress is invalid."
end
end
if state.presets ~= nil and type(state.presets) ~= "table" then return nil, "Team presets are invalid." end
if state.organizerProfiles ~= nil then
if type(state.organizerProfiles) ~= "table" or #state.organizerProfiles > 20 then return nil, "Auto Box presets are invalid." end
for index, profile in pairs(state.organizerProfiles) do
if not integer(index, 1, #state.organizerProfiles) or type(profile) ~= "table"
or type(profile.name) ~= "string" or #profile.name == 0 or #profile.name > 64 or profile.name:find("%c")
or (profile.target ~= "box" and profile.target ~= "pc")
or not require("src.box.Organizer").validate(profile.config) then return nil, "An Auto Box preset is invalid." end
end
end
if state.stages ~= nil and type(state.stages) ~= "table" then return nil, "Showcase stages are invalid." end
if state.departures ~= nil and type(state.departures) ~= "table" then return nil, "Departed entries are invalid." end
if state.syncMembers ~= nil and type(state.syncMembers) ~= "table" then return nil, "Box sync links are invalid." end
for key, member in pairs(state.syncMembers or {}) do
if type(key) ~= "string" or type(member) ~= "table" or not GameVersion.VERSIONS[member.version]
or type(member.playthroughId) ~= "string" or not member.playthroughId:match("^[%w%._:%-]+$")
or #member.playthroughId > 64 or key ~= member.version .. "/" .. member.playthroughId
or type(member.path) ~= "string" or not member.path:match("^saves/[%w%._%-]+/slot%d+%.lua$") then
return nil, "A Box sync link is invalid."
end
local scope, slot = member.path:match("^saves/([%w%._%-]+)/(slot%d+)%.lua$")
if member.cartId ~= nil and (type(member.cartId) ~= "string" or #member.cartId > 64
or not member.cartId:match("^[%w%._%-]+$"))
or scope ~= (member.cartId and "cart_" .. member.cartId or member.version)
or member.slotId ~= nil and member.slotId ~= slot
or member.deleted ~= nil and type(member.deleted) ~= "boolean" then
return nil, "A Box sync link points to a different save."
end
end
for id, departure in pairs(state.departures or {}) do
local entry = type(departure) == "table" and departure.entry
if not integer(id, 1, state.nextId - 1) or ids[id] or type(entry) ~= "table" or entry.id ~= id
or not GameVersion.VERSIONS[entry.version] or entry.generation ~= GameVersion.generation(entry.version)
or type(entry.mon) ~= "table" or type(entry.display) ~= "table"
or type(departure.path) ~= "string" or not archivesValid(entry)
or departure.identity ~= nil and type(departure.identity) ~= "string" then return nil, "A departed Pokémon reference is invalid." end
end
for index, stage in pairs(state.stages or {}) do
if not integer(index, 1, 5) then return nil, "A showcase stage slot is invalid." end
local valid, why = require("src.box.Showcase").validate(stage, state.nextId)
if not valid then return nil, why end
end
for key, preset in pairs(state.presets or {}) do
if not integer(key, 1, 1000000) or type(preset) ~= "table"
or type(preset.name) ~= "string" or #preset.name > 64
or type(preset.ids) ~= "table" or #preset.ids < 1 or #preset.ids > 6 then
return nil, "A team preset is invalid."
end
local used = {}
for _, id in ipairs(preset.ids) do
if not integer(id, 1, state.nextId - 1) or used[id] then return nil, "A team reference is invalid." end
used[id] = true
end
end
return state
end
function Store.load(fs)
local body = fs.read(Store.PATH)
if not body then
if fs.getInfo(Store.PATH) then return nil, "Box storage could not be read." end
local backup = fs.read(Store.PATH .. ".bak")
if backup then
local state = Serializer.decode(backup)
if not Store.validate(state) then return nil, "Box storage is missing and its backup could not be read." end
local ok = fs.write(Store.PATH, backup)
if not ok or fs.read(Store.PATH) ~= backup then return nil, "Box storage could not be restored from its backup." end
return state, nil, backup
end
if fs.getInfo(Store.PATH .. ".bak") or fs.getInfo(Store.PATH .. ".box-bak") then
return nil, "Box storage is missing. Its backup was kept in the box folder."
end
return Store.new(), nil, nil
end
local state, err = Serializer.decode(body)
if not state then return nil, "Box storage could not be decoded: " .. tostring(err) end
local valid, why = Store.validate(state)
if not valid then return nil, why end
return valid, nil, body
end
function Store.at(state, box, slot)
if not integer(box, 1, Store.BOXES) or not integer(slot, 1, Store.SLOTS) then return nil end
return state.boxes[box].mons[slot]
end
function Store.free(state, box)
if not integer(box, 1, Store.BOXES) then return nil end
for slot = 1, Store.SLOTS do if not state.boxes[box].mons[slot] then return slot end end
end
function Store.count(state)
local n = 0
for b = 1, Store.BOXES do for _ in pairs(state.boxes[b].mons) do n = n + 1 end end
return n
end
function Store.move(state, fromBox, fromSlot, toBox, toSlot)
local entry = Store.at(state, fromBox, fromSlot)
if not entry or not integer(toBox, 1, Store.BOXES)
or not integer(toSlot, 1, Store.SLOTS) then return nil, "Choose valid Box slots." end
local nextState = Store.copy(state)
nextState.boxes[fromBox].mons[fromSlot], nextState.boxes[toBox].mons[toSlot] =
nextState.boxes[toBox].mons[toSlot], nextState.boxes[fromBox].mons[fromSlot]
nextState.revision = state.revision + 1
return nextState
end
function Store.rename(state, box, name)
if not integer(box, 1, Store.BOXES) or type(name) ~= "string"
or #name == 0 or #name > 128 or name:find("%c") then
return nil, "Enter a Box name of 1 to 128 bytes."
end
local nextState = Store.copy(state)
nextState.boxes[box].name = name
nextState.revision = state.revision + 1
return nextState
end
function Store.find(state, id)
for b = 1, Store.BOXES do
for slot, entry in pairs(state.boxes[b].mons) do
if entry.id == id then return { box = b, slot = slot, entry = entry } end
end
end
end
function Store.moveGroup(state, refs, box, slot)
if type(refs) ~= "table" or #refs == 0 or not integer(box, 1, Store.BOXES)
or not integer(slot, 1, Store.SLOTS) or slot + #refs - 1 > Store.SLOTS then
return nil, "Choose a destination with room for the entire group."
end
local nextState, group, origins, vacated, displaced = Store.copy(state), {}, {}, {}, {}
for _, ref in ipairs(refs) do
local entry = Store.at(state, ref.box, ref.slot)
local key = tostring(ref.box) .. ":" .. tostring(ref.slot)
if not entry or origins[key] then return nil, "A selected Box Pokémon is missing or repeated." end
origins[key], group[#group + 1] = true, Store.copy(entry)
nextState.boxes[ref.box].mons[ref.slot] = nil
end
local targets = {}
for i = 1, #refs do
local target = slot + i - 1
targets[box .. ":" .. target] = true
local entry = nextState.boxes[box].mons[target]
if entry then displaced[#displaced + 1] = entry end
nextState.boxes[box].mons[target] = group[i]
end
for _, ref in ipairs(refs) do
if not targets[ref.box .. ":" .. ref.slot] then vacated[#vacated + 1] = ref end
end
for i, entry in ipairs(displaced) do
local ref = vacated[i]
nextState.boxes[ref.box].mons[ref.slot] = entry
end
nextState.revision = state.revision + 1
return nextState
end
function Store.arrange(state, box, sort)
if not integer(box, 1, Store.BOXES) then return nil, "Choose a Box." end
local nextState, rows = Store.copy(state), Store.search(state, "", sort)
nextState.boxes[box].mons = {}
local index = 0
for _, row in ipairs(rows) do
if row.box == box then index = index + 1; nextState.boxes[box].mons[index] = Store.copy(row.entry) end
end
nextState.revision = state.revision + 1
return nextState
end
function Store.theme(state, box, theme, wallpaper, music)
if not integer(box, 1, Store.BOXES) then return nil, "Choose a Box." end
local nextState = Store.copy(state)
nextState.boxes[box].theme, nextState.boxes[box].wallpaper = theme, wallpaper
nextState.boxes[box].showcaseMusic = theme == "Showcase" and music or nil
nextState.revision = state.revision + 1
return Store.validate(nextState)
end
function Store.mark(state, refs, markings, tags)
if not integer(markings, 0, 15) or type(refs) ~= "table" or #refs == 0 then
return nil, "Choose Pokémon and valid markings."
end
if tags ~= nil and (type(tags) ~= "string" or #tags > 128 or tags:find("%c")) then
return nil, "Enter tags of at most 128 bytes."
end
local nextState, seen = Store.copy(state), {}
for _, ref in ipairs(refs) do
local entry = Store.at(nextState, ref.box, ref.slot)
if not entry or seen[entry.id] then return nil, "A selected Pokémon is missing or repeated." end
seen[entry.id] = true
if entry.generation == 3 then
entry.mon.markings, entry.display.markings = markings, markings
else entry.markings = markings end
if tags ~= nil then entry.tags = tags end
end
nextState.revision = state.revision + 1
return nextState
end
function Store.preset(state, name, refs)
if type(name) ~= "string" or #name == 0 or #name > 64 or name:find("%c")
or type(refs) ~= "table" or #refs == 0 or #refs > 6 then
return nil, "Name a team of one to six Pokémon."
end
local nextState, ids, seen = Store.copy(state), {}, {}
for _, ref in ipairs(refs) do
local entry = Store.at(state, ref.box, ref.slot)
if not entry or seen[entry.id] then return nil, "A team member is missing or repeated." end
seen[entry.id], ids[#ids + 1] = true, entry.id
end
nextState.presets = nextState.presets or {}
if #nextState.presets >= 100 then return nil, "All 100 team preset slots are full." end
nextState.presets[#nextState.presets + 1] = { name = name, ids = ids }
nextState.revision = state.revision + 1
return nextState
end
function Store.search(state, query, sort)
local out = {}
local matches = require("src.box.Search").compile(query)
for b = 1, Store.BOXES do
for slot, entry in pairs(state.boxes[b].mons) do
if matches(entry, state.boxes[b].name) then
out[#out + 1] = { box = b, slot = slot, entry = entry }
end
end
end
table.sort(out, function(a, b)
local x, y = a.entry.display, b.entry.display
local av, bv
if sort == "level" then av, bv = tonumber(x.level) or 0, tonumber(y.level) or 0
elseif sort == "species" then av, bv = tonumber(x.national) or 0, tonumber(y.national) or 0
elseif sort == "name" or sort == "nature" or sort == "ability" or sort == "trainer" or sort == "gender" then
av, bv = tostring(x[sort] or ""):lower(), tostring(y[sort] or ""):lower()
elseif sort ~= "slot" then av, bv = tonumber(x[sort]) or -1, tonumber(y[sort]) or -1 end
if av and av ~= bv then return av < bv end
return a.box < b.box or (a.box == b.box and a.slot < b.slot)
end)
return out
end
return Store