Spider Car Unleashed

CLOSES #1483, CLOSES #1610, CLOSES #1615, CLOSES #1646, CLOSES #1649, CLOSES #1651, CLOSES #1653, CLOSES #1656, CLOSES #1683, CLOSES #1685, CLOSES #1686, CLOSES #1687, CLOSES #1688, CLOSES #1689, CLOSES #1690, CLOSES #1693, CLOSES #1694, CLOSES #1695, CLOSES #1696, CLOSES #1702, CLOSES #1704, CLOSES #1705, CLOSES #1706, CLOSES #1707, CLOSES #1708, CLOSES #1710, CLOSES #1711, CLOSES #1712, CLOSES #1713, CLOSES #1716, CLOSES #1717, CLOSES #1718, CLOSES #1719, CLOSES #1720, CLOSES #1721, CLOSES #1725, CLOSES #1732, CLOSES #1745, CLOSES #1748, CLOSES #1749, CLOSES #1751, CLOSES #1754
This commit is contained in:
bryanthaboi
2026-08-24 07:52:05 -04:00
parent f895293217
commit 1905261c5b
180 changed files with 19567 additions and 1556 deletions
+863 -7
View File
@@ -96,6 +96,38 @@ eq(#imp:_ensureCarts("blue"), 1, "blue lists only its own cart")
eq(imp:_ensureCarts("blue")[1].id, "johto_lite", "and that one is Johto Lite")
eq(#imp:_ensureCarts("yellow"), 0, "a game with no carts lists none")
-- A .g1rcart dropped into the carts folder by hand, with no registry entry:
-- the launcher must adopt it, not read past it. _refreshCarts used to call
-- CartStore.index (registry only) and never saw one.
do
local stray = CartManifest.parse(cartTable({
id = "dropped_in", title = "Dropped In", version = "2.0.0" }))
check(stray ~= nil, "the stray fixture parses")
local opts = SaveData.loadOptions()
opts[CartStore.OPTIONS_KEY] = nil
SaveData.saveOptions(opts)
love.filesystem.createDirectory(CartStore.DIR)
love.filesystem.write(CartStore.fileFor("dropped_in"),
CartManifest.encode(stray))
local fresh = freshLauncher()
local found
for _, row in ipairs(fresh:_ensureCarts("red")) do
if row.id == "dropped_in" then found = row end
end
check(found ~= nil, "the launcher lists a cart dropped into the folder")
if found then
eq(found.title, "Dropped In", "and reads its title from the file")
eq(found.base, "red", "and its base game")
end
eq(CartStore.index()[1] ~= nil, true,
"listing healed the registry so the cheap index sees it too")
-- Put the fixture set back: later blocks assert on the picker's own layout,
-- and a third red cart changes its height.
CartStore.uninstall("dropped_in")
end
imp.tab = "red"
imp.ready.red = true
imp._cartPopup = "red"
@@ -109,8 +141,9 @@ check(picker:find("Johto Lite", 1, true) == nil,
check(picker:find("v1.2.0", 1, true) ~= nil, "a cart row carries its version")
check(picker:find("sealed", 1, true) ~= nil, "a cart row carries its seal state")
check(picker:find("open", 1, true) ~= nil, "including an open one")
check(picker:find("Get more carts", 1, true) ~= nil,
"the last row is the browse placeholder")
check(picker:find("Import a cart", 1, true) ~= nil
or picker:find("Get more carts", 1, true) ~= nil,
"the last row imports a cart, by picker or by folder")
imp._cartPopup = nil
local vanillaColors = drawColors(imp)
@@ -305,14 +338,14 @@ local maker = freshLauncher()
maker.tab = "red"
maker.ready.red = true
maker:_setModScope("red")
eq(maker:_cartCaptureCount("red"), 2,
"the control counts only the mods enabled for this game")
eq(maker:_cartCaptureCount("red"), 3,
"the control counts every mod the capture would pin, on or off")
maker:_beginCartSave("red")
check(maker._cartSave ~= nil, "Save as cart opens a form")
eq(maker._cartSave.count, 2, "the form reports the captured mod count")
eq(maker._cartSave.count, 3, "the form reports the captured mod count")
eq(maker._cartSave.version, "red", "scoped to the game the panel is showing")
eq(#maker._cartSave.unresolved, 1, "capture reports one pin it could not resolve")
eq(#maker._cartSave.unresolved, 2, "capture reports the pins it could not resolve")
eq(maker._cartSave.unresolved[1].id, "wide_gym", "naming the mod it belongs to")
check(tostring(maker._cartSave.unresolved[1].reason):find("semantic", 1, true) ~= nil,
"and why it could only be pinned locally")
@@ -345,7 +378,13 @@ eq(made.base, "red", "based on the game the panel was showing")
eq(made.version, "1.0.0", "at the default cart version")
eq(made.seal, "sealed", "sealed by default")
eq(made.shell, "#ff3c48", "wearing the base game's rail colour")
eq(#made.mods, 2, "pinning exactly the enabled mods")
eq(#made.mods, 3, "pinning every installed mod, on or off")
local madeOff
for _, entry in ipairs(made.mods) do
if entry.id == "off_mod" then madeOff = entry end
end
check(madeOff ~= nil, "including the one the player has switched off")
eq(madeOff.enabled, false, "which is pinned switched off rather than dropped")
local listedNow = false
for _, row in ipairs(maker:_ensureCarts("red")) do
@@ -472,9 +511,513 @@ eq(SaveData.slotSealBroken("kanto_plus", freshSlot), false,
"starts sealed again")
eq(gapCart:cartPlan("red").refused, true, "so the cart refuses that one")
install({ id = "plus_cart", title = "Plus Cart", seal = "sealed+",
shell = "#445566" })
LauncherMods.list = function() return FULL_MODS end
local plusCart = freshLauncher()
plusCart.tab = "red"
plusCart.ready.red = true
plusCart._cartPopup = "red"
local plusPicker = drawAndCapture(plusCart)
check(plusPicker:find("sealed+", 1, true) ~= nil,
"the picker spells a sealed+ cart's seal out")
plusCart._cartPopup = nil
plusCart:_selectCart("red", "plus_cart")
local plusPlan = plusCart:cartPlan("red")
eq(plusPlan.seal, "sealed+", "the plan carries the sealed+ seal")
eq(plusPlan.sealed, true, "sealed+ is a sealed cart")
eq(plusPlan.enforced, true, "and enforces its mod set")
local plusText = drawAndCapture(plusCart)
check(plusText:find("Sealed", 1, true) ~= nil, "the page says it is sealed")
check(plusText:find("switch any of them on or off", 1, true) ~= nil,
"and that the player may switch the mods it pins")
check(plusText:find("Break the seal", 1, true) ~= nil,
"while still offering the escape hatch")
eq(SaveData.slotSealBroken("plus_cart",
plusCart.activeSlot[plusCart:slotScope("red")] or "slot1"), false,
"and reading the page breaks nothing")
-- ------- installing the mods a cart pins, instead of defeating its seal
local ModUpdate = require("src.mods.ModUpdate")
local realFetchBegin, realFetchPump =
ModUpdate.beginFetchReleases, ModUpdate.pumpFetchReleases
local realDlBegin, realDlPump =
ModUpdate.beginDownloadZip, ModUpdate.pumpDownloadZip
local realInstallDownloaded = LauncherMods.installDownloadedZip
love.data = love.data or {}
local savedData = { hash = love.data.hash, encode = love.data.encode }
-- The archive's digest IS its bytes here, so a fixture writes the hash it
-- wants the downloaded zip to have.
love.data.hash = function(_, data) return tostring(data) end
love.data.encode = function(_, _, digest) return tostring(digest) end
local SHA_ONE = ("0123abcd"):rep(8)
local SHA_TWO = ("dead9876"):rep(8)
local net = { versions = {}, bytes = {}, err = nil, repos = {} }
ModUpdate.beginFetchReleases = function(repo, modId)
net.repos[modId] = repo
return { modId = modId, repo = repo }
end
ModUpdate.pumpFetchReleases = function(h)
if net.err then return true, nil, net.err end
local out = {}
for _, v in ipairs(net.versions[h.modId] or {}) do
out[#out + 1] = { version = v, tag = "v" .. v,
zip = { url = h.modId .. "@" .. v, name = h.modId .. ".zip" } }
end
return true, out
end
ModUpdate.beginDownloadZip = function(url, destName)
love.filesystem.write(destName, net.bytes[url] or "not-the-pinned-archive")
return { path = destName }
end
ModUpdate.pumpDownloadZip = function(h) return true, h.path end
local haveMods = {}
LauncherMods.installDownloadedZip = function(modId, localPath, version)
haveMods[modId] = version or "?"
pcall(love.filesystem.remove, localPath)
return true, haveMods[modId]
end
LauncherMods.list = function()
local rows = {}
for id, v in pairs(haveMods) do
rows[#rows + 1] = fakeRow({ id = id, name = id, version = v })
end
return rows
end
install({ id = "fill_cart", title = "Fill Cart", shell = "#227744",
mods = {
{ id = "fill_one", source = "github", repo = "ren/fill-one",
version = "1.0.0", sha256 = SHA_ONE },
{ id = "fill_two", source = "github", repo = "ren/fill-two",
version = "2.1.0", sha256 = SHA_TWO },
},
load_order = { "fill_one", "fill_two" } })
install({ id = "odd_cart", title = "Odd Cart", shell = "#772244",
mods = {
{ id = "banana_mod", source = "gamebanana", mod = 4821, file = 99123,
md5 = ("ab"):rep(16) },
{ id = "here_mod", source = "local", version = "0.0.0" },
},
load_order = { "banana_mod", "here_mod" } })
local function fillPage(cartId)
local page = freshLauncher()
page.tab = "red"
page.ready.red = true
page:_selectCart("red", cartId)
return page
end
local function runFill(page)
page:pressInstallCartMods("red")
local guard = 0
while page._cartFill and guard < 500 do
page:_pumpModInstall()
page:_pumpCartFill()
guard = guard + 1
end
check(page._cartFill == nil, "the install run finishes")
return page.cartFillNotice or {}
end
local function failureText(notice)
return table.concat(notice.failures or {}, " | ")
end
net.versions.fill_one = { "1.0.0", "0.9.0" }
net.versions.fill_two = { "2.1.0" }
net.bytes["fill_one@1.0.0"] = SHA_ONE
net.bytes["fill_two@2.1.0"] = SHA_TWO
-- A cart whose pins are all installed offers nothing to install.
haveMods = { fill_one = "1.0.0", fill_two = "2.1.0" }
local readyFill = fillPage("fill_cart")
eq(readyFill:cartPlan("red").refused, false, "every pin installed, so it plays")
eq(#readyFill:cartFillRows("red"), 0, "with nothing left to install")
local readyText = drawAndCapture(readyFill)
check(readyText:find("Install required mods", 1, true) == nil
and readyText:find("required mods", 1, true) == nil,
"so a ready cart never offers the install button")
check(readyText:find("Break the seal", 1, true) ~= nil,
"while the seal control is where it always was")
-- Nothing installed: the button appears, named for the count, beside the seal.
haveMods = {}
local gapFill = fillPage("fill_cart")
eq(#gapFill:cartFillRows("red"), 2, "both uninstalled pins queue up")
local gapFillText = drawAndCapture(gapFill)
check(gapFillText:find("Install 2 required mods", 1, true) ~= nil,
"a refused cart offers to install what it pins, counted")
check(gapFillText:find("the way its author built it", 1, true) ~= nil,
"saying plainly that this is the way to play it as built")
check(gapFillText:find("Break the seal", 1, true) ~= nil,
"with breaking the seal still there as the fallback")
-- A pin installed at the WRONG version is a gap too: the cart wants its own.
haveMods = { fill_one = "0.9.0", fill_two = "2.1.0" }
local skewFill = fillPage("fill_cart")
eq(skewFill:cartPlan("red").mismatched[1].id, "fill_one",
"a pin at another version is a mismatch")
eq(#skewFill:cartFillRows("red"), 1, "which queues for install like a gap")
eq(skewFill:cartFillRows("red")[1].version, "1.0.0",
"at the version the cart pins, not the one on disk")
local skewText = drawAndCapture(skewFill)
check(skewText:find("Install required mods", 1, true) ~= nil,
"a single gap drops the count from the label")
-- THE HASH GATE. An archive that is not the one the cart recorded installs
-- nothing, however well the id and version line up.
haveMods = {}
net.bytes["fill_one@1.0.0"] = "some other build entirely"
local badHash = fillPage("fill_cart")
local badNotice = runFill(badHash)
eq(haveMods.fill_one, nil, "a hash mismatch installs nothing at all")
eq(badNotice.ok, false, "and the run reports as failed")
check(failureText(badNotice):find("fill_one", 1, true) ~= nil,
"naming the mod whose archive did not match")
check(failureText(badNotice):find("sha256", 1, true) ~= nil,
"and saying it was the hash: " .. failureText(badNotice))
eq(badHash:cartPlan("red").refused, true, "so the cart still refuses")
net.bytes["fill_one@1.0.0"] = SHA_ONE
-- Partial: one archive verifies, the other does not.
haveMods = {}
net.bytes["fill_two@2.1.0"] = "wrong bytes for the second mod"
local partial = fillPage("fill_cart")
local partialNotice = runFill(partial)
eq(haveMods.fill_one, "1.0.0", "the pin that verified is installed")
eq(haveMods.fill_two, nil, "the pin that did not is not")
eq(partialNotice.ok, false, "a partial run reads as a failure")
check(tostring(partialNotice.text):find("1 of 2", 1, true) ~= nil,
"counting what landed: " .. tostring(partialNotice.text))
check(failureText(partialNotice):find("fill_two", 1, true) ~= nil,
"and naming only the mod that failed")
check(failureText(partialNotice):find("fill_one", 1, true) == nil,
"not the one that worked")
local partialText = drawAndCapture(partial)
check(partialText:find("fill_two", 1, true) ~= nil,
"the card reports the failure where the player pressed the button")
net.bytes["fill_two@2.1.0"] = SHA_TWO
-- The whole run, verified, with the seal untouched at the end of it.
haveMods = {}
local good = fillPage("fill_cart")
local goodScope = good:slotScope("red")
good:_ensureSlots(goodScope)
local goodSlot = good.activeSlot[goodScope]
eq(good:cartPlan("red").refused, true, "the cart refuses before the run")
local goodNotice = runFill(good)
eq(goodNotice.ok, true, "the run reports success: " .. tostring(goodNotice.text))
eq(haveMods.fill_one, "1.0.0", "the first pin lands at its pinned version")
eq(haveMods.fill_two, "2.1.0", "and the second at its own")
eq(net.repos.fill_one, "ren/fill-one", "each pin was looked up in its own repo")
eq(net.repos.fill_two, "ren/fill-two", "including the second")
eq(good:cartPlan("red").refused, false,
"and the cart is ready to play with no further presses")
eq(SaveData.slotSealBroken("fill_cart", goodSlot or "slot1"), false,
"installing never breaks the cart's seal")
eq(SaveData.isSealBroken(), false, "nor the session's")
local goodText = drawAndCapture(good)
check(goodText:find("Sealed", 1, true) ~= nil,
"the card flips to ready without another click")
check(goodText:find("required mods", 1, true) == nil,
"and stops offering the install button")
-- No release carrying that version.
haveMods = {}
net.versions.fill_one = { "0.9.0" }
local noRel = fillPage("fill_cart")
local noRelNotice = runFill(noRel)
eq(haveMods.fill_one, nil, "a version the repo does not publish installs nothing")
check(failureText(noRelNotice):find("v1.0.0", 1, true) ~= nil,
"and the failure names the version it wanted: " .. failureText(noRelNotice))
net.versions.fill_one = { "1.0.0", "0.9.0" }
-- No .zip on the matching release.
haveMods = {}
local savedPump = ModUpdate.pumpFetchReleases
ModUpdate.pumpFetchReleases = function(h)
return true, { { version = "1.0.0", tag = "v1.0.0" },
{ version = "2.1.0", tag = "v2.1.0" } }
end
local noZip = fillPage("fill_cart")
local noZipNotice = runFill(noZip)
eq(haveMods.fill_one, nil, "a release with no archive installs nothing")
check(failureText(noZipNotice):find(".zip", 1, true) ~= nil,
"and says so: " .. failureText(noZipNotice))
ModUpdate.pumpFetchReleases = savedPump
-- Network down.
haveMods = {}
net.err = "no network transport on this platform"
local offline = fillPage("fill_cart")
local offlineNotice = runFill(offline)
eq(haveMods.fill_one, nil, "an offline run installs nothing")
eq(offlineNotice.ok, false, "and reports as failed")
check(failureText(offlineNotice):find("no network transport", 1, true) ~= nil,
"carrying the transport's own words: " .. failureText(offlineNotice))
net.err = nil
-- Sources the launcher cannot fetch are refused per mod, not as one silence.
haveMods = {}
local odd = fillPage("odd_cart")
eq(#odd:cartFillRows("red"), 2, "both odd pins are gaps")
local oddText = drawAndCapture(odd)
check(oddText:find("Install 2 required mods", 1, true) ~= nil,
"the button is offered even when a pin may not be fetchable")
local oddNotice = runFill(odd)
eq(oddNotice.ok, false, "neither can be installed")
eq(#(oddNotice.failures or {}), 2, "and each is reported on its own line")
check(failureText(oddNotice):find("banana_mod", 1, true) ~= nil
and failureText(oddNotice):find("GameBanana", 1, true) ~= nil,
"the gamebanana pin says what it is pinned to: " .. failureText(oddNotice))
check(failureText(oddNotice):find("here_mod", 1, true) ~= nil
and failureText(oddNotice):find("nothing to download", 1, true) ~= nil,
"and the local pin says there is nothing to fetch")
eq(next(haveMods), nil, "with nothing installed either way")
local oddAfter = drawAndCapture(odd)
check(oddAfter:find("Break the seal", 1, true) ~= nil,
"and the seal is still the fallback it was")
-- Breaking the seal still works exactly as it did, install button or not.
local sealStill = fillPage("odd_cart")
local oddScope = sealStill:slotScope("red")
sealStill:_newSlot(oddScope)
local oddSlot = sealStill.activeSlot[oddScope]
check(type(oddSlot) == "string", "the cart page has a loaded save slot")
eq(sealStill:pressBreakSeal("red"), false, "the first press still only arms")
eq(SaveData.slotSealBroken("odd_cart", oddSlot), false, "breaking nothing")
eq(sealStill:pressBreakSeal("red"), true, "and the second still breaks it")
eq(SaveData.slotSealBroken("odd_cart", oddSlot), true, "on that slot")
eq(sealStill:cartPlan("red").refused, false, "so the cart plays")
love.data.hash, love.data.encode = savedData.hash, savedData.encode
ModUpdate.beginFetchReleases, ModUpdate.pumpFetchReleases =
realFetchBegin, realFetchPump
ModUpdate.beginDownloadZip, ModUpdate.pumpDownloadZip =
realDlBegin, realDlPump
LauncherMods.installDownloadedZip = realInstallDownloaded
-- Later blocks audit the picker's layout, which counts the carts on red.
CartStore.uninstall("fill_cart")
CartStore.uninstall("odd_cart")
LauncherMods.list = realModList
window(1280, 720)
-- ------- the MODS tab under an active cart
-- the rows the base game's own list would hand back, so the panel with no
-- cart active has something real to disagree with the carts about
local PIN_MODS = {
fakeRow({ id = "pin_on", name = "Pin On", version = "1.0.0" }),
fakeRow({ id = "pin_off", name = "Pin Off", version = "1.0.0",
enabled = false,
enabledByVersion = { red = false, blue = false, yellow = false,
gold = false, silver = false } }),
}
local function localPin(id, off)
local entry = { id = id, source = "local", version = "1.0.0" }
if off then entry.enabled = false end
return entry
end
local PIN_SET = { localPin("pin_on"), localPin("pin_off", true) }
local PIN_ORDER = { "pin_on", "pin_off" }
install({ id = "plus_mods", title = "Plus Mods", seal = "sealed+",
shell = "#2b8a3e", mods = PIN_SET, load_order = PIN_ORDER })
install({ id = "hard_mods", title = "Hard Mods", seal = "sealed",
shell = "#8a2b3e", mods = PIN_SET, load_order = PIN_ORDER })
install({ id = "gap_mods", title = "Gap Mods", seal = "sealed+",
shell = "#3e2b8a",
mods = { localPin("pin_on"), localPin("ghost_mod") },
load_order = { "pin_on", "ghost_mod" } })
local realSetEnabled, realSetAllEnabled = LauncherMods.setEnabled, LauncherMods.setAllEnabled
local perGameWrites = 0
LauncherMods.setEnabled = function(...)
perGameWrites = perGameWrites + 1
return realSetEnabled(...)
end
LauncherMods.setAllEnabled = function(...)
perGameWrites = perGameWrites + 1
return realSetAllEnabled(...)
end
LauncherMods.list = function() return PIN_MODS end
-- the base game's own answer for the same mod, which nothing below may move
local seeded = SaveData.loadOptions()
SaveData.setModEnabled(seeded, "pin_off", false, "red")
SaveData.setModEnabled(seeded, "pin_on", true, "red")
SaveData.saveOptions(seeded)
local function rowsById(imp)
local byId = {}
for _, row in ipairs(imp.mods or {}) do byId[row.id] = row end
return byId
end
local plain = freshLauncher()
plain.tab = "mods"
plain.ready.red = true
plain:_selectCart("red", nil)
plain:_setModScope("red")
eq(#plain.mods, 2, "with no cart the panel lists the installed mods")
eq(plain.mods[1].cartPin, nil, "which are not marked as any cart's")
check(type(plain.mods[1].enabledByVersion) == "table",
"and still carry their per-game answers")
perGameWrites = 0
plain:_toggleMod("pin_on", nil, "red")
eq(perGameWrites, 1, "a toggle with no cart still writes the per-game flag")
eq(SaveData.modEnabled(SaveData.loadOptions(), "pin_on", "red"), false,
"which is what changed")
eq(SaveData.cartModEnabled(SaveData.loadOptions(), "plus_mods", "pin_on"), nil,
"and no cart scope was touched")
SaveData.setModEnabled(seeded, "pin_on", true, "red")
SaveData.saveOptions(seeded)
local pins = freshLauncher()
pins.tab = "mods"
pins.ready.red = true
pins:_selectCart("red", "plus_mods")
pins:_setModScope("red")
eq(#pins.mods, 2, "an active cart makes the panel list its pins")
local pinRows = rowsById(pins)
eq(pinRows.pin_on.cartPin, true, "each row is marked as the cart's")
eq(pinRows.pin_on.cartId, "plus_mods", "naming the cart it came from")
eq(pinRows.pin_on.enabled, true, "a pin shipped switched on shows on")
eq(pinRows.pin_off.enabled, false, "a pin shipped switched off shows off")
eq(pinRows.pin_off.cartTogglable, true, "sealed+ hands every pin's switch over")
eq(pinRows.pin_on.enabledByVersion, nil,
"and a cart row carries no per-game answer, because a cart is one game")
local pinText = drawAndCapture(pins)
check(pinText:find("PINNED", 1, true) ~= nil,
"the list says on every row that these are the cart's mods")
check(pinText:find("Plus Mods", 1, true) ~= nil, "and names the cart above them")
check(pinText:find("In this cart:", 1, true) ~= nil,
"with a switch that answers the cart, not the game")
perGameWrites = 0
pins:_toggleMod("pin_off", nil, "red")
local afterOn = SaveData.loadOptions()
eq(SaveData.cartModEnabled(afterOn, "plus_mods", "pin_off"), true,
"a sealed+ toggle writes the player's answer into the cart's scope")
eq(perGameWrites, 0, "and never through the per-game path")
eq(SaveData.modEnabled(afterOn, "pin_off", "red"), false,
"so the base game's flag for that mod is untouched")
eq(SaveData.modEnabled(afterOn, "pin_on", "red"), true, "as is every other")
eq(rowsById(pins).pin_off.enabled, true, "the row follows the new answer")
local pinScope = pins:slotScope("red")
pins:_ensureSlots(pinScope)
local pinSlot = pins.activeSlot[pinScope]
eq(SaveData.slotSealBroken("plus_mods", pinSlot or "slot1"), false,
"switching a sealed+ pin does not break the cart's seal")
eq(SaveData.isSealBroken(), false, "nor the session's")
eq(pins:cartPlan("red").broken, false, "and the plan still reads it as intact")
pins:_toggleMod("pin_off", nil, "red")
eq(SaveData.cartModEnabled(SaveData.loadOptions(), "plus_mods", "pin_off"), false,
"pressing again switches it back off, still in the cart's scope")
perGameWrites = 0
pins:_setAllMods(true)
eq(perGameWrites, 0, "Enable all cannot reach a cart's mod set")
eq(SaveData.cartModEnabled(SaveData.loadOptions(), "plus_mods", "pin_off"), false,
"so no pin moved")
check(tostring(pins.modNotice.text):find("Plus Mods", 1, true) ~= nil,
"and the panel says which cart is deciding")
eq(pins.modNotice.ok, false, "as a refusal")
pins:_setAllMods(false)
eq(perGameWrites, 0, "Disable all cannot either")
eq(SaveData.modEnabled(SaveData.loadOptions(), "pin_on", "red"), true,
"and the base game's list is where it was")
pins:_toggleMod("wide_gym", nil, "red")
check(tostring(pins.modNotice.text):find("cannot be added to", 1, true) ~= nil,
"a mod the cart does not pin cannot be switched on from here")
eq(SaveData.cartModEnabled(SaveData.loadOptions(), "plus_mods", "wide_gym"), nil,
"and nothing is written for it")
pins.safeMode = true
pins:_toggleMod("pin_off", nil, "red")
check(tostring(pins.modNotice.text):find("Safe mode", 1, true) ~= nil,
"safe mode still refuses a cart toggle first")
eq(SaveData.cartModEnabled(SaveData.loadOptions(), "plus_mods", "pin_off"), false,
"and writes nothing")
pins.safeMode = false
local sealedPins = freshLauncher()
sealedPins.tab = "mods"
sealedPins.ready.red = true
sealedPins:_selectCart("red", "hard_mods")
sealedPins:_setModScope("red")
local hardRows = rowsById(sealedPins)
eq(hardRows.pin_off.enabled, false, "a sealed cart's off pin shows off")
eq(hardRows.pin_off.cartTogglable, false, "and hands no switch over")
eq(hardRows.pin_on.cartTogglable, false, "nor does the pin it ships on")
perGameWrites = 0
sealedPins:_toggleMod("pin_off", nil, "red")
eq(perGameWrites, 0, "a sealed pin never reaches the per-game path either")
eq(SaveData.cartModEnabled(SaveData.loadOptions(), "hard_mods", "pin_off"), nil,
"and a press under a sealed cart writes nothing")
eq(sealedPins.modNotice.ok, false, "the press is refused")
check(tostring(sealedPins.modNotice.text):find("sealed", 1, true) ~= nil,
"with the panel saying why rather than doing nothing")
check(tostring(sealedPins.modNotice.text):find("Break the seal", 1, true) ~= nil,
"and pointing at the one way to change it")
local hardScope = sealedPins:slotScope("red")
sealedPins:_ensureSlots(hardScope)
eq(SaveData.slotSealBroken("hard_mods",
sealedPins.activeSlot[hardScope] or "slot1"), false,
"a refused press breaks no seal of its own")
local hardText = drawAndCapture(sealedPins)
check(hardText:find("Pinned, sealed:", 1, true) ~= nil,
"and the row itself reads as locked")
local gapPins = freshLauncher()
gapPins.tab = "mods"
gapPins.ready.red = true
gapPins:_selectCart("red", "gap_mods")
gapPins:_setModScope("red")
local gapRows = rowsById(gapPins)
eq(#gapPins.mods, 2, "a pin that is not installed is still listed")
eq(gapRows.ghost_mod.status, "missing", "as missing")
eq(gapRows.ghost_mod.enabled, false, "and switched off")
gapPins:_toggleMod("ghost_mod", nil, "red")
check(tostring(gapPins.modNotice.text):find("not installed", 1, true) ~= nil,
"switching it says so instead of writing an answer for a mod that is absent")
eq(SaveData.cartModEnabled(SaveData.loadOptions(), "gap_mods", "ghost_mod"), nil,
"and writes nothing")
pins:_selectCart("red", nil)
pins:_setModScope("red")
local backRows = rowsById(pins)
eq(#pins.mods, 2, "choosing the base game lists the player's own mods again")
eq(backRows.pin_on.cartPin, nil, "with no cart marks left on them")
check(type(backRows.pin_on.enabledByVersion) == "table",
"and their per-game answers back")
eq(backRows.pin_off.enabled, false,
"reading exactly what the base game's flags say")
perGameWrites = 0
pins:_toggleMod("pin_off", nil, "red")
eq(perGameWrites, 1, "and a toggle is a per-game write once more")
eq(SaveData.modEnabled(SaveData.loadOptions(), "pin_off", "red"), true,
"which lands in the game's own flags")
eq(SaveData.cartModEnabled(SaveData.loadOptions(), "plus_mods", "pin_off"), false,
"leaving the cart's answer where the player left it")
LauncherMods.setEnabled, LauncherMods.setAllEnabled = realSetEnabled, realSetAllEnabled
LauncherMods.list = realModList
local function clipped(r)
local x1, y1, x2, y2 = r.x, r.y, r.x + r.w, r.y + r.h
if r.clip then
@@ -540,6 +1083,22 @@ for _, size in ipairs(SIZES) do
if ok then
auditFrame(("%dx%d %s"):format(W, H, cart and "cart" or "vanilla"),
"Custom Carts")
-- The refused card carries two chips now, so both have to stay on it.
if cart then
local sawFill = false
for _, r in ipairs(Kit.audit or {}) do
local label = tostring(r.label)
if label == "Install required mods" or label == "Break the seal" then
sawFill = sawFill or label == "Install required mods"
check(r.x >= -0.5 and r.x + r.w <= W + 0.5
and r.y >= -0.5 and r.y + r.h <= H + 0.5,
("%dx%d cart card: %q stays inside the window")
:format(W, H, label))
end
end
check(sawFill,
("%dx%d cart card: drew Install required mods"):format(W, H))
end
end
Kit.audit = nil
@@ -595,6 +1154,303 @@ for _, size in ipairs(SIZES) do
end
Kit.audit = nil
end
LauncherMods.list = function() return PIN_MODS end
for _, size in ipairs(SIZES) do
local W, H = size[1], size[2]
window(W, H)
for _, cart in ipairs({ "plus_mods", "hard_mods" }) do
local panel = freshLauncher()
panel.tab = "mods"
panel.ready.red = true
panel:_selectCart("red", cart)
panel:_setModScope("red")
LauncherView.draw(panel)
Kit.audit = {}
local ok, err = pcall(LauncherView.draw, panel)
Kit.audit = ok and Kit.audit or nil
check(ok, ("%dx%d %s mods draws: %s"):format(W, H, cart, tostring(err)))
if ok then auditFrame(("%dx%d %s mods"):format(W, H, cart)) end
Kit.audit = nil
end
end
LauncherMods.list = realModList
-- ------- FIND tab: browsing the index's carts instead of its mods
--
-- The feed carries carts beside mods at the same schema_version, so the panel
-- has a Mods / Carts switch. Installing a cart from a listing goes through
-- CartStore.install with the downloaded bytes, never the mod installer.
local ModIndex = require("src.mods.ModIndex")
local ModUpdate = require("src.mods.ModUpdate")
local Json = require("src.link.Json")
local INDEX_CART = {
id = "indexed_cart", title = "Indexed Cart", author = "Ren",
version = "1.4.0", base = "silver", seal = "sealed",
repo = "https://github.com/ren/indexed-cart",
github = "ren/indexed-cart",
mods = { { id = "rare_soda", source = "github", repo = "ren/rare-soda",
version = "0.4.1", sha256 = SHA } },
update_check = "ok",
latest = { version = "1.4.0", tag = "v1.4.0",
zip = { name = "indexed_cart-1.4.0.zip",
url = "https://example.test/indexed_cart-1.4.0.zip" } },
}
local INDEX_FEED = ModIndex.parse(Json.encode({
schema_version = 1,
categories = { "GAMEPLAY" },
base_games = { "red", "blue", "yellow", "gold", "silver" },
mods = {
{ id = "rare_soda", title = "Rare Soda", author = "Ren", version = "0.4.1",
categories = { "GAMEPLAY" }, update_check = "off" },
{ id = "true_colour", title = "True Colour", author = "Sam",
version = "1.0.0", categories = { "ART" }, update_check = "off" },
},
carts = {
INDEX_CART,
{ id = "gold_rush", title = "Gold Rush", author = "Sam", version = "3.0.0",
base = "gold", seal = "open", repo = "https://github.com/sam/gold-rush",
mods = { { id = "steps", source = "github", repo = "sam/steps",
version = "1.0.0", sha256 = SHA } },
update_check = "off" },
},
}))
check(INDEX_FEED ~= nil, "the find-tab fixture feed parses")
window(1280, 720)
local find = freshLauncher()
eq(find.findKind, "mods", "the Find tab browses mods by default")
check(find.findBase == nil, "with no base-game filter armed")
find.tab = "find"
find.modScope = nil
find.findLoaded = true
find.findSources = { { feed = "https://example.test/data/index.json",
base = "https://example.test/",
label = "example/index" } }
find.findIndex = { mods = INDEX_FEED.mods, carts = INDEX_FEED.carts,
categories = { "GAMEPLAY", "ART" },
baseGames = ModIndex.baseGamesIn(INDEX_FEED) }
eq(#find:_findRows(), 2, "the default rows are the feed's mods")
eq(find:_findRows()[1].id, "rare_soda", "and they are the mod entries")
local modsFind = drawAndCapture(find)
check(modsFind:find("Mods (2)", 1, true) ~= nil,
"the switch says how many mods the feed lists")
check(modsFind:find("Carts (2)", 1, true) ~= nil, "and how many carts")
check(modsFind:find("Rare Soda", 1, true) ~= nil, "mods are what is listed")
find:_setFindKind("carts")
eq(find.findKind, "carts", "the switch flips to carts")
eq(#find:_findRows(), 2, "and the rows become the feed's carts")
check(ModIndex.isCart(find:_findRows()[1]), "which are cart entries")
local cartsFind = drawAndCapture(find)
check(cartsFind:find("Indexed Cart", 1, true) ~= nil,
"a cart listing is drawn on the Carts half")
check(cartsFind:find("Rare Soda", 1, true) == nil,
"and the mod listings are gone")
check(cartsFind:find("Search carts", 1, true) ~= nil,
"the search field asks for carts")
-- search spans the same fields it does for a mod
find.findQuery = "gold"
eq(#find:_findRows(), 1, "searching a cart title narrows the list")
eq(find:_findRows()[1].id, "gold_rush", "to that cart")
find.findQuery = "Ren"
eq(find:_findRows()[1].id, "indexed_cart", "searching by author works too")
find.findQuery = ""
-- the cart-side filter is by base game, and the popup offers only the base
-- games the feed's carts actually play as
find.findBase = "gold"
eq(#find:_findRows(), 1, "filtering by base game keeps that game's carts")
eq(find:_findRows()[1].id, "gold_rush", "and only those")
local filterText
do
find._filterPopup = true
filterText = drawAndCapture(find)
find._filterPopup = nil
end
check(filterText:find("Filter by base game", 1, true) ~= nil,
"the filter popup filters carts by base game")
check(filterText:find("Filter by category", 1, true) == nil,
"not by a category no cart has")
find.findBase = nil
-- MODS-tab scope still applies: a cart plays as exactly one game
find.modScope = "silver"
eq(#find:_findRows(), 1, "a scoped launcher lists only that game's carts")
eq(find:_findRows()[1].id, "indexed_cart", "the one based on silver")
find.modScope = nil
find:_setFindKind("mods")
eq(#find:_findRows(), 2, "switching back restores the mod rows")
find:_setFindKind("carts")
-- ------- installing a cart from a listing
local INDEX_CART_BYTES = CartManifest.encode(CartManifest.parse(cartTable({
id = "indexed_cart", title = "Indexed Cart", version = "1.4.0",
base = "silver", shell = "#d8c24a" })))
eq(#find:_ensureCarts("silver"), 0, "silver has no carts before the install")
local entry
for _, row in ipairs(find:_findRows()) do
if row.id == "indexed_cart" then entry = row end
end
check(entry ~= nil, "the cart listing is on screen to install")
local realBegin, realPump = ModUpdate.beginDownloadZip, ModUpdate.pumpDownloadZip
local asked = {}
ModUpdate.beginDownloadZip = function(url, name)
asked[#asked + 1] = { url = url, name = name }
return { name = name }
end
ModUpdate.pumpDownloadZip = function(h)
love.filesystem.write(h.name, INDEX_CART_BYTES)
return true, h.name
end
find:_findConfirmInstall(entry)
check(find._modConfirm ~= nil, "installing a cart arms a confirm first")
eq(find._modConfirm.title, "Install cart", "and it says it is a cart")
eq(find._modConfirm.indexEntry, entry, "carrying the listing it will install")
do
local lines = table.concat(find._modConfirm.lines or {}, "\n")
check(lines:find("Pins 1 mod", 1, true) ~= nil,
"the confirm says how many mods the cart pins")
check(lines:find("installed separately", 1, true) ~= nil,
"and that installing the cart does not install them")
end
find._modConfirm = nil
find:_findInstall(entry)
check(find._cartInstall ~= nil, "the cart download is in flight")
check(find._modInstall == nil, "and it is not a mod install")
eq(asked[1].url, "https://example.test/indexed_cart-1.4.0.zip",
"the release the listing resolves is what gets downloaded")
check(asked[1].name:find(".g1rcart", 1, true) ~= nil,
"into a cart file, not a mod zip")
-- single in flight: a mod install cannot start on top of a cart download
find:_beginModInstall({ modId = "rare_soda", name = "Rare Soda", notice = "find",
release = { version = "0.4.1",
zip = { url = "https://example.test/rare_soda.zip" } } })
check(find._modInstall == nil, "a mod install cannot race a cart install")
eq(#asked, 1, "and nothing else was downloaded")
find:_pumpCartInstall()
check(find._cartInstall == nil, "the cart install completes")
check(find.findNotice ~= nil and find.findNotice.ok,
"and reports success on the Find tab")
local installedCart = CartStore.get("indexed_cart")
check(installedCart ~= nil, "the cart is installed through CartStore")
if installedCart then
eq(installedCart.title, "Indexed Cart", "with its own title")
eq(installedCart.base, "silver", "and the game it plays as")
end
-- the per-version cache is the whole point: without invalidating it the new
-- cart would not show in Custom Carts until a relaunch
eq(#find.carts["silver"], 1,
"the cached cart list for that game is refreshed in place")
eq(find.carts["silver"][1].id, "indexed_cart", "with the new cart in it")
eq(find:_findInstalledCarts()["indexed_cart"], "1.4.0",
"and the Find rows now read it as installed")
-- the Custom Carts picker draws the new cart in the same session, with no
-- refresh call from here and no relaunch
do
local picker = freshLauncher()
picker.tab = "silver"
picker.ready.silver = true
picker._cartPopup = "silver"
local pickerText = drawAndCapture(picker)
check(pickerText:find("Indexed Cart", 1, true) ~= nil,
"the Custom Carts picker lists the freshly installed cart")
end
do
local same = drawAndCapture(find)
check(same ~= nil, "the Find tab still draws after an install")
end
-- ------- the pins prompt
--
-- Installing a cart never installs its mods behind the player's back, but a
-- cart whose pins are missing will not start, so it asks once and routes a
-- yes at the existing hash-verified fill queue.
check(find._modConfirm ~= nil, "a cart with missing pins prompts after install")
eq(find._modConfirm.kind, "cartPins", "through the launcher's confirm modal")
eq(find._modConfirm.version, "silver", "for the game the cart plays as")
eq(find._modConfirm.id, "indexed_cart", "naming the cart just installed")
do
local lines = table.concat(find._modConfirm.lines or {}, "\n")
check(lines:find("Indexed Cart pins 1 mod", 1, true) ~= nil,
"the prompt names the cart and how many pins are missing")
end
check(find.activeCart["silver"] == nil,
"and asking does not select the cart on its own")
-- yes routes at pressInstallCartMods, which owns the hash check
do
local realFetchRel = ModUpdate.beginFetchReleases
local realPumpRel = ModUpdate.pumpFetchReleases
local asked_repos = {}
ModUpdate.beginFetchReleases = function(repo)
asked_repos[#asked_repos + 1] = repo
return { repo = repo }
end
ModUpdate.pumpFetchReleases = function() return true, nil, "no releases" end
find._modConfirm = nil
find:_installCartPins("silver", "indexed_cart")
eq(find.activeCart["silver"], "indexed_cart",
"saying yes selects the cart the pins belong to")
for _ = 1, 8 do find:_pumpCartFill() end
eq(asked_repos[1], "ren/rare-soda",
"and the fill queue resolves the cart's own pin")
check(find.cartFillNotice ~= nil and not find.cartFillNotice.ok,
"reporting per-mod failures through the existing notice")
find:_selectCart("silver", nil)
find.cartFillNotice = nil
ModUpdate.beginFetchReleases = realFetchRel
ModUpdate.pumpFetchReleases = realPumpRel
end
-- a cart whose pins are all installed must not prompt at all
do
local realList = LauncherMods.list
LauncherMods.list = function()
return { { id = "rare_soda", version = "0.4.1",
manifest = { id = "rare_soda", version = "0.4.1" } } }
end
local satisfied = freshLauncher()
eq(#satisfied:_cartPinsMissing("silver", "indexed_cart"), 0,
"a cart with every pin installed is missing none")
satisfied:_offerCartPins({ id = "indexed_cart", title = "Indexed Cart",
base = "silver" })
check(satisfied._modConfirm == nil, "so it does not prompt")
LauncherMods.list = realList
end
-- a download that is not a cart at all fails loudly instead of installing
ModUpdate.pumpDownloadZip = function(h)
love.filesystem.write(h.name, "not a cart at all")
return true, h.name
end
find.findNotice = nil
find:_findInstall(entry)
find:_pumpCartInstall()
check(find._cartInstall == nil, "a junk download ends the job")
check(find.findNotice ~= nil and not find.findNotice.ok,
"and says so rather than installing anything")
ModUpdate.beginDownloadZip, ModUpdate.pumpDownloadZip = realBegin, realPump
T.finish("cart launcher")