mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 01:34:06 -04:00
334 lines
15 KiB
Lua
334 lines
15 KiB
Lua
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
|