mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-09 10:01:50 -04:00
334 lines
15 KiB
Lua
334 lines
15 KiB
Lua
local Store = require("src.box.Store")
|
|
local Records = require("src.box.Records")
|
|
local Catalog = require("src.box.Catalog")
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local Organizer = {}
|
|
Organizer.SORTS = { "species", "name", "level", "type", "nature", "ability", "trainer", "shiny", "egg", "friendship" }
|
|
Organizer.LABELS = { species = "Pokédex number", name = "Nickname", level = "Level", type = "Type",
|
|
nature = "Nature", ability = "Ability", trainer = "Trainer", shiny = "Shiny", egg = "Egg", friendship = "Friendship" }
|
|
Organizer.FIELDS = { "shiny", "egg", "species", "type", "levelMin", "levelMax", "nature", "ability",
|
|
"gender", "trainer", "game", "mark", "tag", "name", "move", "item" }
|
|
Organizer.FIELD_LABELS = { shiny = "Shiny", egg = "Egg", species = "Species", type = "Type",
|
|
levelMin = "Level at least", levelMax = "Level at most", nature = "Nature", ability = "Ability",
|
|
gender = "Gender", trainer = "Trainer", game = "Game", mark = "Marked", tag = "Tag contains",
|
|
name = "Nickname", move = "Move", item = "Held item" }
|
|
function Organizer.available(field, version)
|
|
if not version then return true end
|
|
local generation = GameVersion.generation(version)
|
|
if field == "tag" then return false end
|
|
if generation < 3 and (field == "mark" or field == "nature" or field == "ability") then return false end
|
|
if generation == 1 and (field == "gender" or field == "egg" or field == "item") then return false end
|
|
return true
|
|
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 contains(list, value)
|
|
for _, item in ipairs(list) do if item == value then return true end end
|
|
return false
|
|
end
|
|
local function array(list, max)
|
|
if type(list) ~= "table" or #list > max then return false end
|
|
for key in pairs(list) do if not integer(key, 1, #list) then return false end end
|
|
return true
|
|
end
|
|
function Organizer.validate(config, count)
|
|
count = count or Store.BOXES
|
|
if type(config) ~= "table" or (config.mode ~= "sort" and config.mode ~= "rules")
|
|
or not contains(Organizer.SORTS, config.sort)
|
|
or type(config.descending) ~= "boolean"
|
|
or config.box ~= nil and not integer(config.box, 1, count) then
|
|
return nil, "Choose a valid sort and box."
|
|
end
|
|
if config.mode == "sort" then
|
|
if config.rules ~= nil then
|
|
local ruleConfig = Store.copy(config)
|
|
ruleConfig.mode, ruleConfig.box = "rules", nil
|
|
return Organizer.validate(ruleConfig)
|
|
end
|
|
return true
|
|
end
|
|
if config.box ~= nil or not array(config.rules, 24) or #config.rules == 0
|
|
or (config.fallback ~= "keep" and config.fallback ~= "sort") then
|
|
return nil, "Add a rule and choose what happens to the rest."
|
|
end
|
|
for index, rule in ipairs(config.rules) do
|
|
if type(rule) ~= "table" or not integer(rule.firstBox, 1, count)
|
|
or not integer(rule.lastBox, rule.firstBox, count) then
|
|
return nil, "Rule " .. index .. " needs a destination available in this collection."
|
|
end
|
|
if not array(rule.conditions, 8) or #rule.conditions == 0
|
|
or (rule.match ~= "all" and rule.match ~= "any") then
|
|
return nil, "Rule " .. index .. " needs a condition."
|
|
end
|
|
for _, condition in ipairs(rule.conditions) do
|
|
if type(condition) ~= "table" or not contains(Organizer.FIELDS, condition.field) then
|
|
return nil, "A rule condition is invalid."
|
|
end
|
|
local field, value = condition.field, condition.value
|
|
if field == "shiny" or field == "egg" then
|
|
if type(value) ~= "boolean" then return nil, "Choose yes or no for this condition." end
|
|
elseif field == "levelMin" or field == "levelMax" then
|
|
if not integer(value, 0, 255) then return nil, "Choose a level from 0 to 255." end
|
|
elseif type(value) ~= "string" or #value == 0 or #value > 128 or value:find("%c") then
|
|
return nil, "Choose a value for this condition."
|
|
elseif field == "game" and not GameVersion.VERSIONS[value]
|
|
or field == "mark" and not contains({ "circle", "square", "triangle", "heart" }, value) then
|
|
return nil, "Choose a valid game or marking."
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function normalized(value) return require("src.box.Search").normalize(value) end
|
|
local function matches(entry, condition)
|
|
local d, field, wanted = entry.display, condition.field, condition.value
|
|
if field == "shiny" or field == "egg" then return (d[field] == true) == wanted end
|
|
if field == "levelMin" then return tonumber(d.level) ~= nil and tonumber(d.level) >= wanted end
|
|
if field == "levelMax" then return tonumber(d.level) ~= nil and tonumber(d.level) <= wanted end
|
|
if field == "mark" then
|
|
local mask = ({ circle = 1, square = 2, triangle = 4, heart = 8 })[wanted]
|
|
local value = tonumber(entry.generation == 3 and entry.mon.markings or entry.markings) or 0
|
|
return math.floor(value / mask) % 2 == 1
|
|
end
|
|
if field == "tag" then return normalized(entry.tags):find(normalized(wanted), 1, true) ~= nil end
|
|
if field == "type" then
|
|
for item in tostring(d.types or ""):gmatch("[^/]+") do
|
|
if normalized(item:match("^%s*(.-)%s*$")) == normalized(wanted) then return true end
|
|
end
|
|
return false
|
|
end
|
|
if field == "move" then
|
|
for item in tostring(d.moves or ""):gmatch("[^,]+") do
|
|
if normalized(item:match("^%s*(.-)%s*$")) == normalized(wanted) then return true end
|
|
end
|
|
return false
|
|
end
|
|
local value = field == "game" and entry.version or d[field]
|
|
return value ~= nil and normalized(value) == normalized(wanted)
|
|
end
|
|
local function ruleMatches(entry, rule)
|
|
for _, condition in ipairs(rule.conditions) do
|
|
local hit = matches(entry, condition)
|
|
if rule.match == "any" and hit then return true end
|
|
if rule.match == "all" and not hit then return false end
|
|
end
|
|
return rule.match == "all"
|
|
end
|
|
local function sortValue(entry, field)
|
|
local d = entry.display
|
|
if field == "species" then
|
|
local national = tonumber(d.national)
|
|
return national and national > 0 and national or nil
|
|
end
|
|
if field == "type" then return d.types and d.types ~= "" and normalized(d.types) or nil end
|
|
if field == "shiny" or field == "egg" then return d[field] and 1 or 0 end
|
|
if field == "level" or field == "friendship" then return tonumber(d[field]) end
|
|
return d[field] and d[field] ~= "" and normalized(d[field]) or nil
|
|
end
|
|
local function compare(a, b, config)
|
|
local av, bv = sortValue(a.entry, config.sort), sortValue(b.entry, config.sort)
|
|
if av == nil or bv == nil then
|
|
if av ~= bv then return av ~= nil end
|
|
elseif av ~= bv then
|
|
if config.descending then return av > bv end
|
|
return av < bv
|
|
end
|
|
for _, key in ipairs({ "species", "name", "level" }) do
|
|
av, bv = sortValue(a.entry, key), sortValue(b.entry, key)
|
|
if av == nil or bv == nil then
|
|
if av ~= bv then return av ~= nil end
|
|
elseif av ~= bv then return av < bv end
|
|
end
|
|
return a.box < b.box or a.box == b.box and a.slot < b.slot
|
|
end
|
|
|
|
function Organizer.warehouse(state)
|
|
local layout = { count = Store.BOXES, capacity = Store.SLOTS, rows = {}, names = {} }
|
|
for b, box in ipairs(state.boxes) do
|
|
layout.names[b] = box.name
|
|
for slot = 1, Store.SLOTS do
|
|
if box.mons[slot] then layout.rows[#layout.rows + 1] = { box = b, slot = slot, entry = box.mons[slot] } end
|
|
end
|
|
end
|
|
return layout
|
|
end
|
|
function Organizer.game(save, version)
|
|
if not GameVersion.VERSIONS[version] then return nil, "Choose a supported game." end
|
|
local generation = GameVersion.generation(version)
|
|
if not Catalog.get(version).ready then return nil, "Import this game's ROM before organizing its PC." end
|
|
local layout = { count = generation == 1 and 12 or 14, capacity = generation == 3 and 30 or 20,
|
|
rows = {}, names = {}, compact = generation < 3 }
|
|
local boxes = Records.boxes(save, generation)
|
|
if type(boxes) ~= "table" then return nil, "This save's PC could not be read." end
|
|
for b, box in pairs(boxes) do
|
|
if not integer(b, 1, layout.count) or type(box) ~= "table" then
|
|
return nil, "This save has a PC box this tool cannot represent."
|
|
end
|
|
end
|
|
for b = 1, layout.count do
|
|
local list = Records.list(save, generation, b)
|
|
if type(list) ~= "table" then return nil, "A PC box could not be read." end
|
|
local seen = {}
|
|
for key, mon in pairs(list) do
|
|
local slot = tonumber(key)
|
|
if not integer(slot, 1, layout.capacity) or type(mon) ~= "table" or seen[slot] then
|
|
return nil, "A PC record is invalid or appears twice in one slot."
|
|
end
|
|
seen[slot] = true
|
|
end
|
|
local box = boxes[b]
|
|
layout.names[b] = generation == 3 and box and box.name or save.boxNames and save.boxNames[b] or "BOX " .. b
|
|
for slot = 1, layout.capacity do
|
|
local mon = list[slot] or list[tostring(slot)]
|
|
if mon then layout.rows[#layout.rows + 1] = { box = b, slot = slot,
|
|
entry = { version = version, generation = generation, mon = mon, display = Catalog.describe(version, mon) } } end
|
|
end
|
|
end
|
|
return layout
|
|
end
|
|
|
|
function Organizer.analyze(layout, config)
|
|
local valid, why = Organizer.validate(config, layout.count)
|
|
if not valid then return nil, why end
|
|
local counts, reserved, rest = {}, {}, 0
|
|
for i, rule in ipairs(config.mode == "rules" and config.rules or {}) do
|
|
counts[i] = 0
|
|
for b = rule.firstBox, rule.lastBox do reserved[b] = true end
|
|
end
|
|
for _, row in ipairs(layout.rows) do
|
|
local found
|
|
for i, rule in ipairs(config.mode == "rules" and config.rules or {}) do
|
|
if ruleMatches(row.entry, rule) then counts[i], found = counts[i] + 1, true; break end
|
|
end
|
|
if not found then rest = rest + 1 end
|
|
end
|
|
local free = 0
|
|
for b = 1, layout.count do if not reserved[b] then free = free + 1 end end
|
|
return { ruleCounts = counts, remaining = rest, reserved = reserved, freeBoxes = free, total = #layout.rows }
|
|
end
|
|
|
|
function Organizer.plan(layout, config)
|
|
local valid, why = Organizer.validate(config, layout.count)
|
|
if not valid then return nil, why end
|
|
local boxes, groups, rest, reserved = {}, {}, {}, {}
|
|
local report = { total = #layout.rows, moved = 0, ruleCounts = {}, boxes = {}, assignments = {} }
|
|
for b = 1, layout.count do
|
|
boxes[b] = {}
|
|
report.boxes[b] = { name = layout.names[b], before = 0, after = 0, capacity = layout.capacity, moved = 0 }
|
|
end
|
|
for i, rule in ipairs(config.mode == "rules" and config.rules or {}) do
|
|
groups[i], report.ruleCounts[i] = {}, 0
|
|
for b = rule.firstBox, rule.lastBox do reserved[b] = true end
|
|
end
|
|
for _, row in ipairs(layout.rows) do
|
|
report.boxes[row.box].before = report.boxes[row.box].before + 1
|
|
local chosen
|
|
if config.mode == "rules" then
|
|
for i, rule in ipairs(config.rules) do
|
|
if ruleMatches(row.entry, rule) then chosen = i; break end
|
|
end
|
|
end
|
|
if chosen then
|
|
groups[chosen][#groups[chosen] + 1] = row
|
|
report.ruleCounts[chosen] = report.ruleCounts[chosen] + 1
|
|
elseif config.mode == "rules" and config.fallback == "keep"
|
|
or config.mode == "sort" and config.box and row.box ~= config.box then
|
|
boxes[row.box][row.slot] = row
|
|
else rest[#rest + 1] = row end
|
|
end
|
|
local function place(rows, firstBox, lastBox, skipReserved)
|
|
table.sort(rows, function(a, b) return compare(a, b, config) end)
|
|
local index = 1
|
|
for b = firstBox, lastBox do
|
|
if not skipReserved or not reserved[b] then
|
|
for slot = 1, layout.capacity do
|
|
if not boxes[b][slot] and rows[index] then boxes[b][slot], index = rows[index], index + 1 end
|
|
end
|
|
end
|
|
end
|
|
return index > #rows
|
|
end
|
|
if config.mode == "rules" then
|
|
for i, rule in ipairs(config.rules) do
|
|
if not place(groups[i], rule.firstBox, rule.lastBox) then
|
|
return nil, "Rule " .. i .. " needs more room in its destination. Widen its box range."
|
|
end
|
|
end
|
|
end
|
|
if not place(rest, config.box or 1, config.box or layout.count, config.mode == "rules") then
|
|
return nil, "The remaining Pokémon need more room outside the rule boxes. Keep their boxes or free a destination."
|
|
end
|
|
for b = 1, layout.count do
|
|
if layout.compact and (config.mode == "rules" or not config.box or config.box == b) then
|
|
local dense = {}
|
|
for slot = 1, layout.capacity do if boxes[b][slot] then dense[#dense + 1] = boxes[b][slot] end end
|
|
boxes[b] = dense
|
|
end
|
|
for slot = 1, layout.capacity do
|
|
local row = boxes[b][slot]
|
|
if row then
|
|
report.boxes[b].after = report.boxes[b].after + 1
|
|
local moved = row.box ~= b or row.slot ~= slot
|
|
if moved then report.moved, report.boxes[b].moved = report.moved + 1, report.boxes[b].moved + 1 end
|
|
report.assignments[#report.assignments + 1] = { fromBox = row.box, fromSlot = row.slot,
|
|
box = b, slot = slot, entry = row.entry, moved = moved }
|
|
end
|
|
end
|
|
end
|
|
return report, boxes
|
|
end
|
|
|
|
function Organizer.applyWarehouse(state, config)
|
|
local report, boxes = Organizer.plan(Organizer.warehouse(state), config)
|
|
if not report then return nil, boxes end
|
|
local nextState = Store.copy(state)
|
|
for b = 1, Store.BOXES do
|
|
nextState.boxes[b].mons = {}
|
|
for slot, row in pairs(boxes[b]) do nextState.boxes[b].mons[slot] = Store.copy(row.entry) end
|
|
end
|
|
nextState.revision = state.revision + 1
|
|
return nextState, report
|
|
end
|
|
function Organizer.applyGame(save, version, config)
|
|
local layout, why = Organizer.game(save, version)
|
|
if not layout then return nil, why end
|
|
local valid, err = Organizer.validate(config, layout.count)
|
|
if not valid then return nil, err end
|
|
if config.mode == "rules" then
|
|
for _, rule in ipairs(config.rules) do
|
|
for _, condition in ipairs(rule.conditions) do
|
|
if not Organizer.available(condition.field, version) then
|
|
return nil, GameVersion.info(version).label .. "'s Game PC doesn't record "
|
|
.. Organizer.FIELD_LABELS[condition.field] .. ". Choose another condition."
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local report, boxes = Organizer.plan(layout, config)
|
|
if not report then return nil, boxes end
|
|
local nextSave, generation = Store.copy(save), GameVersion.generation(version)
|
|
if generation == 3 then
|
|
nextSave.storage = nextSave.storage or { currentBox = 1, boxes = {} }
|
|
nextSave.storage.boxes = nextSave.storage.boxes or {}
|
|
for b = 1, layout.count do
|
|
if config.mode == "rules" or not config.box or config.box == b then
|
|
local box = nextSave.storage.boxes[b] or { name = layout.names[b], wallpaper = 0 }
|
|
box.mons = {}; nextSave.storage.boxes[b] = box
|
|
for slot, row in pairs(boxes[b]) do box.mons[slot] = Store.copy(row.entry.mon) end
|
|
end
|
|
end
|
|
else
|
|
nextSave.boxes, nextSave.box = Records.boxes(nextSave, generation), nil
|
|
for b = 1, layout.count do
|
|
if config.mode == "rules" or not config.box or config.box == b then
|
|
nextSave.boxes[b] = {}
|
|
for slot, row in pairs(boxes[b]) do nextSave.boxes[b][slot] = Store.copy(row.entry.mon) end
|
|
end
|
|
end
|
|
end
|
|
return nextSave, report
|
|
end
|
|
return Organizer
|