CLOSES #2649, CLOSES #2650, CLOSES #2652, CLOSES #2653, CLOSES #2654, CLOSES #2657, CLOSES #2658, CLOSES #2659, CLOSES #2660, CLOSES #2662, CLOSES #2664, CLOSES #2665, CLOSES #2666, CLOSES #2669, CLOSES #2671, CLOSES #2672, CLOSES #2674, CLOSES #2676, CLOSES #2682, CLOSES #2683, CLOSES #2684, CLOSES #2685, CLOSES #2686, CLOSES #2687, CLOSES #2688, CLOSES #2689

CLOSES #2649, CLOSES #2650, CLOSES #2652, CLOSES #2653, CLOSES #2654, CLOSES #2657, CLOSES #2658, CLOSES #2659, CLOSES #2660, CLOSES #2662, CLOSES #2664, CLOSES #2665, CLOSES #2666, CLOSES #2669, CLOSES #2671, CLOSES #2672, CLOSES #2674, CLOSES #2676, CLOSES #2682, CLOSES #2683, CLOSES #2684, CLOSES #2685, CLOSES #2686, CLOSES #2687, CLOSES #2688, CLOSES #2689
This commit is contained in:
bryanthaboi
2026-10-05 11:06:01 -04:00
parent 160895c6bb
commit 72f2294d8d
110 changed files with 8042 additions and 694 deletions
@@ -0,0 +1,249 @@
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua"
.. ";./tools/save-editor/panels/?.lua"
love = require("tests.love_stub")
local passed, failed = 0, 0
local function check(cond, msg)
if cond then
passed = passed + 1
else
failed = failed + 1
print("FAIL: " .. msg)
end
end
local function eq(a, b, msg)
check(a == b, msg .. string.format(" (got %s, want %s)", tostring(a), tostring(b)))
end
print("== save editor / .sav split stack tests (#2671) ==")
require("src.core.GameVersion").set("red")
local Ops = require("Ops")
local State = require("State")
local Catalog = require("Catalog")
local Bag = require("src.inventory.Bag")
local SaveData = require("src.core.SaveData")
local GenSave = require("src.save_convert.GenSave")
local Data = require("src.core.Data")
Data:load()
GenSave.setCharmap(loadfile("src/save_convert/data/charmap.lua")())
local function red()
local S = State.new()
S.data = Data
S.cat = Catalog.build(Data)
S.save = SaveData.newGame()
S.save.inventory, S.save.bagOrder, S.save.pcItems, S.save.pcOrder = {}, nil, {}, nil
return S
end
local function split(S)
Bag.add(S.save, "X_ACCURACY", 99, S.data)
Bag.add(S.save, "POTION", 3, S.data)
Bag.add(S.save, "X_ACCURACY", 2, S.data)
end
local function counts(rows)
local out = {}
for _, r in ipairs(rows) do out[#out + 1] = tostring(r.id) .. "x" .. tostring(r.count) end
return table.concat(out, ",")
end
do
local S = red()
split(S)
eq(S.save.inventory.X_ACCURACY, 101, "fixture: 99 + 2 X ACCURACY totals 101")
eq(table.concat(S.save.bagOrder, ","), "X_ACCURACY,POTION,X_ACCURACY", "fixture: two cart slots")
eq(counts(Ops.stackRows(S, false)), "X_ACCURACYx99,POTIONx3,X_ACCURACYx2", "editor rows carry each slot's own count")
local legal = true
for _, r in ipairs(Ops.stackRows(S, false)) do
if not require("Legality").integer(r.count, 1, Ops.itemMax(S, r.id, false)) then legal = false end
end
check(legal, "a split stack is legal per row")
end
do
local S = red()
split(S)
check(Ops.bagCanMax(S, "X_ACCURACY"), "the part-filled overflow slot can be maxed")
check(Ops.bagMax(S, "X_ACCURACY") == true, "Max fills the overflow slot")
eq(S.save.inventory.X_ACCURACY, 198, "Max fills both slots to 99")
eq(counts(Ops.stackRows(S, false)), "X_ACCURACYx99,POTIONx3,X_ACCURACYx99", "both rows read x99")
end
do
local S = red()
split(S)
S.dirty = false
check(Ops.bagDrop(S, "X_ACCURACY", 2) == true, "Drop on the overflow row succeeds")
eq(S.save.inventory.X_ACCURACY, 99, "Drop on the overflow row removes only that slot")
eq(table.concat(S.save.bagOrder, ","), "X_ACCURACY,POTION", "the overflow slot leaves bagOrder")
end
do
local S = red()
Bag.add(S.save, "X_ACCURACY", 99, S.data)
check(Ops.bagAdjust(S, "X_ACCURACY", 1) == true, "+1 on a full slot opens a new slot")
eq(S.save.inventory.X_ACCURACY, 100, "the bag now holds 100")
eq(counts(Ops.stackRows(S, false)), "X_ACCURACYx99,X_ACCURACYx1", "the new slot reads x1")
check(Ops.bagAdjust(S, "X_ACCURACY", -1, 2) == true, "-1 on the new slot")
eq(table.concat(S.save.bagOrder, ","), "X_ACCURACY", "emptying the new slot drops it")
end
do
local S = red()
Bag.add(S.save, "X_ACCURACY", 99, S.data)
local n = 1
for _, id in ipairs(S.cat.items) do
if n >= Bag.capacity(S.data) then break end
if not Ops.isBadgeId(id) and id ~= "X_ACCURACY" and Bag.add(S.save, id, 1, S.data) then n = n + 1 end
end
eq(Bag.slots(S.save, S.data), 20, "fixture: a 20-slot bag")
S.dirty = false
check(Ops.bagAdjust(S, "X_ACCURACY", 1) == false, "a full bag refuses the overflow slot")
eq(S.save.inventory.X_ACCURACY, 99, "the refusal changes nothing")
check(S.dirty == false, "the refusal does not dirty")
end
do
local S = red()
S.save.pcItems.POTION = 150
eq(table.concat(Ops.pcOrder(S), ","), "POTION,POTION", "a 150 PC stack lists two box slots")
eq(counts(Ops.stackRows(S, true)), "POTIONx99,POTIONx51", "PC rows carry each slot's own count")
check(Ops.pcDrop(S, "POTION", 2) == true, "Drop on the PC overflow row")
eq(S.save.pcItems.POTION, 99, "only the overflow slot leaves the PC")
check(Ops.pcAdjust(S, "POTION", 1) == true, "+1 on a full PC slot opens a new slot")
eq(S.save.pcItems.POTION, 100, "the PC now holds 100")
end
do
local S = red()
split(S)
check(Ops.bagAdjust(S, "X_ACCURACY", -5, 1) == true, "-5 on the first bag slot")
eq(counts(Ops.stackRows(S, false)), "X_ACCURACYx94,POTIONx3,X_ACCURACYx2",
"editor rows keep 94 and 2 after a first-slot decrease")
eq(table.concat(S.save.bagStacks.X_ACCURACY, ","), "94,2", "the per-slot counts ride save.bagStacks")
S.save.pcItems.POTION = 150
check(Ops.pcAdjust(S, "POTION", -1, 1) == true, "-1 on the first PC slot")
eq(counts(Ops.stackRows(S, true)), "POTIONx98,POTIONx51", "PC rows keep 98 and 51")
end
do
local Kit = require("Kit")
local App = require("App")
local oldDim = love.graphics.getDimensions
love.graphics.getDimensions = function() return 1280, 720 end
local path = os.tmpname() .. "-bug2671.lua"
local seed = SaveData.newGame()
seed.inventory = { X_ACCURACY = 101, POTION = 3 }
seed.bagOrder = { "X_ACCURACY", "POTION", "X_ACCURACY" }
local f = assert(io.open(path, "wb")); f:write(SaveData.encode(seed)); f:close()
App.load(path, { version = "red" })
local S = App.getState()
S.tab = "items"
local labels, invalid = {}, {}
local real = Kit.button
Kit.button = function(x, y, w, h, label, opts)
if type(label) == "string" and label:find("×", 1, true) then
labels[#labels + 1] = label
if opts and opts.invalid then invalid[#invalid + 1] = label end
end
return real(x, y, w, h, label, opts)
end
App.draw()
Kit.button = real
eq(table.concat(labels, "|"), "X ACCURACY ×99|POTION ×3|X ACCURACY ×2", "panel: one row per cart slot with its own count")
eq(#invalid, 0, "panel: no row is flagged as an illegal stack")
App.unload()
os.remove(path)
love.graphics.getDimensions = oldDim
end
do
local O = GenSave.OFFSETS
local idx = GenSave.crosswalks(Data).itemsIndex
local save = SaveData.newGame()
save.inventory = { X_ACCURACY = 101, POTION = 3 }
save.bagOrder = { "X_ACCURACY", "POTION", "X_ACCURACY" }
save.cartBag = nil
save.pcItems = { POTION = 150, ANTIDOTE = 1 }
save.pcOrder = { "POTION", "ANTIDOTE", "POTION" }
save.cartPc = nil
local ok, bytes = pcall(GenSave.encode, save, Data)
check(ok and type(bytes) == "string", ".sav encode runs: " .. tostring(bytes))
if ok and type(bytes) == "string" then
eq(bytes:byte(O.numBagItems + 1), 3, ".sav: wNumBagItems counts both X ACCURACY slots")
local function slots(off, n)
local out = {}
for i = 0, n - 1 do
out[#out + 1] = bytes:byte(off + i * 2 + 1) .. ":" .. bytes:byte(off + i * 2 + 2)
end
out[#out + 1] = tostring(bytes:byte(off + n * 2 + 1))
return table.concat(out, ",")
end
eq(slots(O.bagItems, 3), ("%d:99,%d:3,%d:2,255"):format(idx.X_ACCURACY, idx.POTION, idx.X_ACCURACY),
".sav: wBagItems keeps each slot where bagOrder put it")
eq(bytes:byte(O.numPcItems + 1), 3, ".sav: wNumBoxItems counts both POTION slots")
eq(slots(O.pcItems, 3), ("%d:99,%d:1,%d:51,255"):format(idx.POTION, idx.ANTIDOTE, idx.POTION),
".sav: wBoxItems keeps each slot where pcOrder put it")
local okD, dec = pcall(GenSave.decode, bytes, Data)
check(okD and type(dec) == "table", ".sav decode runs: " .. tostring(dec))
if okD and type(dec) == "table" then
local back = dec.save or dec
eq(back.inventory.X_ACCURACY, 101, ".sav round trip: total 101")
eq(table.concat(back.bagOrder or {}, ","), "X_ACCURACY,POTION,X_ACCURACY", ".sav round trip: two bagOrder occurrences")
eq(back.cartBag, nil, ".sav round trip: the modelled rows need no raw carrier")
eq(back.pcItems.POTION, 150, ".sav round trip: PC total 150")
eq(table.concat(back.pcOrder or {}, ","), "POTION,ANTIDOTE,POTION", ".sav round trip: two pcOrder occurrences")
end
end
end
do
local O = GenSave.OFFSETS
local idx = GenSave.crosswalks(Data).itemsIndex
local save = SaveData.newGame()
save.inventory = { X_ACCURACY = 96, POTION = 3 }
save.bagOrder = { "X_ACCURACY", "POTION", "X_ACCURACY" }
save.bagStacks = { X_ACCURACY = { 94, 2 } }
save.cartBag = nil
save.pcItems = { POTION = 149, ANTIDOTE = 1 }
save.pcOrder = { "POTION", "ANTIDOTE", "POTION" }
save.pcStacks = { POTION = { 98, 51 } }
save.cartPc = nil
local ok, bytes = pcall(GenSave.encode, save, Data)
check(ok and type(bytes) == "string", ".sav encode of per-slot counts runs: " .. tostring(bytes))
if ok and type(bytes) == "string" then
local function slots(off, n)
local out = {}
for i = 0, n - 1 do
out[#out + 1] = bytes:byte(off + i * 2 + 1) .. ":" .. bytes:byte(off + i * 2 + 2)
end
out[#out + 1] = tostring(bytes:byte(off + n * 2 + 1))
return table.concat(out, ",")
end
eq(slots(O.bagItems, 3), ("%d:94,%d:3,%d:2,255"):format(idx.X_ACCURACY, idx.POTION, idx.X_ACCURACY),
".sav: wBagItems writes each slot's own count")
eq(slots(O.pcItems, 3), ("%d:98,%d:1,%d:51,255"):format(idx.POTION, idx.ANTIDOTE, idx.POTION),
".sav: wBoxItems writes each slot's own count")
local okD, dec = pcall(GenSave.decode, bytes, Data)
check(okD and type(dec) == "table", ".sav decode of per-slot counts runs: " .. tostring(dec))
if okD and type(dec) == "table" then
local back = dec.save or dec
eq(back.inventory.X_ACCURACY, 96, ".sav round trip: total 96")
eq(table.concat(back.bagStacks and back.bagStacks.X_ACCURACY or {}, ","), "94,2",
".sav round trip: 94/2 comes back as per-slot counts")
eq(back.cartBag, nil, ".sav round trip: per-slot counts need no raw carrier")
eq(table.concat(back.pcStacks and back.pcStacks.POTION or {}, ","), "98,51",
".sav round trip: PC 98/51 comes back as per-slot counts")
eq(back.cartPc, nil, ".sav round trip: PC per-slot counts need no raw carrier")
end
end
end
print(string.format("save editor split stack tests: %d passed, %d failed", passed, failed))
if failed > 0 then os.exit(1) end