mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc7b96c963 | |||
| aea38240b2 | |||
| 0142468125 | |||
| e7d898625d | |||
| 5ee4a3cd35 | |||
| 4794ddcfdf | |||
| 6503e982db | |||
| 72ce1fe33e | |||
| 493fca7297 | |||
| 7d5856f93b | |||
| ef43814efc | |||
| bed1062a5f | |||
| 190c03cfca | |||
| cec264ad81 | |||
| 041051e154 | |||
| 0d14980205 |
@@ -82,7 +82,8 @@ game data:
|
|||||||
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
|
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
|
||||||
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
|
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
|
||||||
- Silver: `49b163f7e57702bc939d642a18f591de55d92dae`
|
- Silver: `49b163f7e57702bc939d642a18f591de55d92dae`
|
||||||
- Crystal: `f4cd194bdee0d04ca4eac29e09b8e4e9d818c133`
|
- Crystal (1.0): `f4cd194bdee0d04ca4eac29e09b8e4e9d818c133`
|
||||||
|
- Crystal (1.1): `f2f52230b536214ef7c9924f483392993e226cfb`
|
||||||
|
|
||||||
The packaged app contains neither a ROM nor pre-extracted game data. Music,
|
The packaged app contains neither a ROM nor pre-extracted game data. Music,
|
||||||
sound effects, and cries are synthesized while the game runs from compact
|
sound effects, and cries are synthesized while the game runs from compact
|
||||||
|
|||||||
@@ -12,6 +12,34 @@
|
|||||||
"tintColor": "3b5ca8",
|
"tintColor": "3b5ca8",
|
||||||
"category": "games",
|
"category": "games",
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "0.2.24",
|
||||||
|
"date": "2026-08-24",
|
||||||
|
"size": 14060528,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.24/gen1recomp++-0.2.24-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.2.23",
|
||||||
|
"date": "2026-08-24",
|
||||||
|
"size": 14060437,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.23/gen1recomp++-0.2.23-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.2.22",
|
||||||
|
"date": "2026-08-24",
|
||||||
|
"size": 14060365,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.22/gen1recomp++-0.2.22-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi\n- @ShaneMcGovernIE\n- @thibautbus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.2.21",
|
||||||
|
"date": "2026-08-24",
|
||||||
|
"size": 14058979,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.21/gen1recomp++-0.2.21-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1483 (Pokémon Gold) Missing HM/TM fanfare when receiving them\n- #1610 Wrong text when choosing Pokemon to trade\n- #1615 Fishing Guru dialogue issue\n- #1640 Sync doesnt work on XBOX build\n- #1646 Cashier dialogue + sound effects\n- #1649 Pokemon cries missing in Vermilion City\n- #1651 Ticket taker asks for ticket too much\n- #1653 Wrap animation is too fast and has no screen shake\n- #1656 Map visible in between end of fight & evolution\n- #1683 Music not changing when talking to trainer\n- #1685 Cancel option missing in item menu\n- #1686 Issues with teaching TMs & HMs\n- #1687 Pokemon cries missing on S.S. Anne\n- #1688 S.S. Anne rival dialogue\n- #1689 S.S. Anne departing cutscene broken\n- #1690 Wrong dialogue when taking back entrance to museum\n- #1693 (Gold) Issues not properly fixed\n- #1694 (Gold) Arrow when seeing items in the bag should be red, not black\n- #1695 (Gold) Wrong layout in TMs/HMs pocket\n- #1696 (Gold) Caught pokémon don't get healed when automatically sent to the PC\n- #1697 Standardize identification of mod-created options/settings screens\n- #1702 Lt. Surge's door opening isn't synced with the sound effect\n- #1704 Ledge jumping on the bike doesn't slow you down\n- #1705 The bike shouldn't have Use/Toss options\n- #1706 (Gold) Cursor wraps around the edge in the battle interface\n- #1707 (Gold) Pokémon sometimes wake up in the same turn they were put to sleep\n- #1708 (Gold) Missing rod when fishing\n- #1710 (Gold) You shouldn't be able to change boxes by pressing LEFT or RIGHT\n- #1711 (Gold) Missing animation for FLY in the overworld\n- #1712 (Gold) Wrong cursor in the map when using FLY\n- #1713 (Gold) Low framerate when jumping off ledges\n- #1716 (Gold) Player can go through a whirlpool even without having the HM\n- #1717 (Gold) Missing animation and SFX when using whirlpool in the overworld\n- #1718 (Gold) Can't pause in the Cycling Road while moving down\n- #1719 (Gold) Pokémon given by NPCs not being registered in the pokédex\n- #1720 Critical hit on multi-hit moves repeats the message\n- #1721 Rival not visible while talking if you lose first fight\n- #1724 Centralize ROM cache readiness and publication in an engine-owned contract\n- #1725 (Gold) Text not spaced nor centered correctly for secondary actions in the bag\n- #1732 Cursor doesn't stay on last used item\n- #1745 Viewing items makes ingame menu disappear\n- #1748 (Gold) Overworld pokémon sprite are stactic\n- #1749 (Gold) You shouldn't be able to surf on Cycling Road\n- #1751 (Gold) Synthesis and Morning Sun always recover 50% of max HP\n- #1754 Leaving building with bike speed\n- #1756 No cursor or gamepad controls in skin menu, locked out on Xbox\n\n## Contributors\n\n- @1Jamie\n- @BoLayerDev\n- @bryanthaboi\n- @campavao\n- @caorthann-celt\n- @dlloa\n- @syybott\n- @thibautbus\n- MaxTomahawk"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.2.20",
|
"version": "0.2.20",
|
||||||
"date": "2026-08-22",
|
"date": "2026-08-22",
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ GameVersion.VERSIONS = {
|
|||||||
saveSuffix = "_crystal", -- save_crystal.lua / .bak / .tmp
|
saveSuffix = "_crystal", -- save_crystal.lua / .bak / .tmp
|
||||||
generation = 2,
|
generation = 2,
|
||||||
engine = "crystal",
|
engine = "crystal",
|
||||||
|
revisions = {
|
||||||
|
{ sha1 = "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133", label = "1.0" },
|
||||||
|
{ sha1 = "f2f52230b536214ef7c9924f483392993e226cfb", label = "1.1" },
|
||||||
|
},
|
||||||
fixes = {
|
fixes = {
|
||||||
-- pokegold/docs/bugs_and_glitches.md:61
|
-- pokegold/docs/bugs_and_glitches.md:61
|
||||||
luckyNumberBoxes = true,
|
luckyNumberBoxes = true,
|
||||||
@@ -168,10 +172,29 @@ function GameVersion.cachePrefix(id)
|
|||||||
return GameVersion.info(id).cachePrefix
|
return GameVersion.info(id).cachePrefix
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function GameVersion.revisions(id)
|
||||||
|
local info = GameVersion.info(id)
|
||||||
|
return info.revisions or { { sha1 = info.sha1 } }
|
||||||
|
end
|
||||||
|
|
||||||
|
function GameVersion.acceptsSha1(id, sha1)
|
||||||
|
for _, revision in ipairs(GameVersion.revisions(id)) do
|
||||||
|
if revision.sha1 == sha1 then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function GameVersion.revisionLabel(id, sha1)
|
||||||
|
for _, revision in ipairs(GameVersion.revisions(id)) do
|
||||||
|
if revision.sha1 == sha1 then return revision.label end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
-- The version a ROM belongs to, by its SHA-1, or nil for an unknown ROM.
|
-- The version a ROM belongs to, by its SHA-1, or nil for an unknown ROM.
|
||||||
function GameVersion.forSha1(sha1)
|
function GameVersion.forSha1(sha1)
|
||||||
for id, info in pairs(GameVersion.VERSIONS) do
|
for id in pairs(GameVersion.VERSIONS) do
|
||||||
if info.sha1 == sha1 then return id end
|
if GameVersion.acceptsSha1(id, sha1) then return id end
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ local GameVersion = require("src.core.GameVersion")
|
|||||||
local CacheContract = {}
|
local CacheContract = {}
|
||||||
|
|
||||||
CacheContract.FORMAT = "rom-cache-v10:"
|
CacheContract.FORMAT = "rom-cache-v10:"
|
||||||
|
CacheContract.VERSION_FORMAT = {
|
||||||
|
crystal = "rom-cache-v10-crystal2:",
|
||||||
|
}
|
||||||
CacheContract.MARKER_PATH = "rom-cache.complete"
|
CacheContract.MARKER_PATH = "rom-cache.complete"
|
||||||
|
|
||||||
CacheContract.REQUIRED_FILES = {
|
CacheContract.REQUIRED_FILES = {
|
||||||
@@ -136,8 +139,20 @@ function CacheContract.requiredFilesFor(version)
|
|||||||
return CacheContract.REQUIRED_FILES, false
|
return CacheContract.REQUIRED_FILES, false
|
||||||
end
|
end
|
||||||
|
|
||||||
function CacheContract.markerFor(version)
|
function CacheContract.formatFor(version)
|
||||||
return CacheContract.FORMAT .. GameVersion.info(version).sha1
|
return CacheContract.VERSION_FORMAT[version] or CacheContract.FORMAT
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.markerFor(version, sha1)
|
||||||
|
return CacheContract.formatFor(version)
|
||||||
|
.. (sha1 or GameVersion.info(version).sha1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.markerMatches(version, marker)
|
||||||
|
for _, revision in ipairs(GameVersion.revisions(version)) do
|
||||||
|
if marker == CacheContract.markerFor(version, revision.sha1) then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Keep the process-global CacheFs prefix isolated even when a filesystem
|
-- Keep the process-global CacheFs prefix isolated even when a filesystem
|
||||||
@@ -189,11 +204,11 @@ function CacheContract.isReady(version, fs)
|
|||||||
fs = fs or require("src.import.CacheFs")
|
fs = fs or require("src.import.CacheFs")
|
||||||
if CacheContract.sourceTreeHasData(version) then return true end
|
if CacheContract.sourceTreeHasData(version) then return true end
|
||||||
local marker, readError = CacheContract.readMarker(version, fs)
|
local marker, readError = CacheContract.readMarker(version, fs)
|
||||||
if readError or marker ~= CacheContract.markerFor(version) then return false end
|
if readError or not CacheContract.markerMatches(version, marker) then return false end
|
||||||
return CacheContract.allRequiredFilesExist(version, fs)
|
return CacheContract.allRequiredFilesExist(version, fs)
|
||||||
end
|
end
|
||||||
|
|
||||||
function CacheContract.publish(version, fs)
|
function CacheContract.publish(version, fs, sha1)
|
||||||
fs = fs or require("src.import.CacheFs")
|
fs = fs or require("src.import.CacheFs")
|
||||||
local complete, missing = CacheContract.allRequiredFilesExist(version, fs)
|
local complete, missing = CacheContract.allRequiredFilesExist(version, fs)
|
||||||
if not complete then
|
if not complete then
|
||||||
@@ -212,7 +227,7 @@ function CacheContract.publish(version, fs)
|
|||||||
return false, "cache is incomplete; missing " .. tostring(missing)
|
return false, "cache is incomplete; missing " .. tostring(missing)
|
||||||
end
|
end
|
||||||
local changed, ok, err = withVersionPrefix(version, fs, function()
|
local changed, ok, err = withVersionPrefix(version, fs, function()
|
||||||
return fs.write(CacheContract.MARKER_PATH, CacheContract.markerFor(version))
|
return fs.write(CacheContract.MARKER_PATH, CacheContract.markerFor(version, sha1))
|
||||||
end)
|
end)
|
||||||
if not changed then return false, tostring(ok) end
|
if not changed then return false, tostring(ok) end
|
||||||
return ok, err
|
return ok, err
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ require("love.math")
|
|||||||
require("love.system")
|
require("love.system")
|
||||||
require("love.timer")
|
require("love.timer")
|
||||||
|
|
||||||
local version, prefix, romData, progressName, resultName = ...
|
local version, prefix, romData, progressName, resultName, romSha1 = ...
|
||||||
|
|
||||||
local progressChannel = love.thread.getChannel(progressName)
|
local progressChannel = love.thread.getChannel(progressName)
|
||||||
local resultChannel = love.thread.getChannel(resultName)
|
local resultChannel = love.thread.getChannel(resultName)
|
||||||
@@ -44,7 +44,7 @@ local ok, err = pcall(function()
|
|||||||
current = current, stageTotal = stageTotal,
|
current = current, stageTotal = stageTotal,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end)
|
end, romSha1)
|
||||||
extractor:run()
|
extractor:run()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -3056,8 +3056,8 @@ local function buildFindKindRow(imp, x, y, w, m)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function buildFindPanel(imp, x, y, w, availH, m)
|
local function buildFindPanel(imp, x, y, w, availH, m)
|
||||||
|
imp._findVisibleEntries = nil
|
||||||
imp:_ensureFind()
|
imp:_ensureFind()
|
||||||
imp:_ensureMods()
|
|
||||||
local ModIndex = require("src.mods.ModIndex")
|
local ModIndex = require("src.mods.ModIndex")
|
||||||
local ModUpdate = require("src.mods.ModUpdate")
|
local ModUpdate = require("src.mods.ModUpdate")
|
||||||
local sources = imp.findSources or {}
|
local sources = imp.findSources or {}
|
||||||
@@ -3126,7 +3126,9 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
|||||||
|
|
||||||
if #rows == 0 then
|
if #rows == 0 then
|
||||||
local empty
|
local empty
|
||||||
if carts then
|
if imp._findFetch then
|
||||||
|
empty = Strings("Loading mod index...")
|
||||||
|
elseif carts then
|
||||||
empty = (total == 0) and Strings("This index lists no carts yet.")
|
empty = (total == 0) and Strings("This index lists no carts yet.")
|
||||||
or Strings("No carts match that search.")
|
or Strings("No carts match that search.")
|
||||||
else
|
else
|
||||||
@@ -3191,6 +3193,11 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
|||||||
local listTop = cy
|
local listTop = cy
|
||||||
setPage(imp, "find", Kit.wheelPage(x, listTop, w, listH, cur, #rows, perPage))
|
setPage(imp, "find", Kit.wheelPage(x, listTop, w, listH, cur, #rows, perPage))
|
||||||
|
|
||||||
|
local visible = imp._findVisibleEntries or {}
|
||||||
|
for i = #visible, 1, -1 do visible[i] = nil end
|
||||||
|
for i = first, last do visible[#visible + 1] = rows[i] end
|
||||||
|
imp._findVisibleEntries = visible
|
||||||
|
|
||||||
for i = first, last do
|
for i = first, last do
|
||||||
local entry = rows[i]
|
local entry = rows[i]
|
||||||
local ry = listTop + (i - first) * (rowH + gap)
|
local ry = listTop + (i - first) * (rowH + gap)
|
||||||
@@ -5764,12 +5771,6 @@ local function loaderSpec(imp)
|
|||||||
return { title = b.title, detail = b.detail, progress = b.progress,
|
return { title = b.title, detail = b.detail, progress = b.progress,
|
||||||
onCancel = b.cancel }
|
onCancel = b.cancel }
|
||||||
end
|
end
|
||||||
-- The boot prewarm runs without an overlay (the user did not ask for it and
|
|
||||||
-- must be able to use the launcher meanwhile), but if they reach the Find
|
|
||||||
-- Mods tab before it lands, THEN they are waiting on it and it earns one.
|
|
||||||
if imp.tab == "find" and imp._findFetch and not imp.findLoaded then
|
|
||||||
return { title = Strings("Loading mod index") }
|
|
||||||
end
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,6 @@ local MAP_GROUP_COUNT = 26 -- constants/map_constants.asm NUM_MAP_GROUPS
|
|||||||
-- "bank13" (../pokecrystal/main.asm:192-195).
|
-- "bank13" (../pokecrystal/main.asm:192-195).
|
||||||
local PAL_MAP_BANK = 0x02
|
local PAL_MAP_BANK = 0x02
|
||||||
local PAL_MAP_BANK_CRYSTAL = 0x13
|
local PAL_MAP_BANK_CRYSTAL = 0x13
|
||||||
-- LoadBallIconGFX.gfx (engine/battle/trainer_huds.asm:225-232); bank $0b
|
|
||||||
-- carries no manifest symbol to resolve it through.
|
|
||||||
local BALL_ICON_GFX = { 0x0b, 0x41a4 }
|
|
||||||
-- A tileset sheet is 96 tiles (128x48 at 8x8), and its PalMap packs two
|
-- A tileset sheet is 96 tiles (128x48 at 8x8), and its PalMap packs two
|
||||||
-- tiles per byte: low nibble first tile, high nibble second (`dn` in the
|
-- tiles per byte: low nibble first tile, high nibble second (`dn` in the
|
||||||
-- tilepal macro). The high bit of each nibble is the VRAM bank, not colour.
|
-- tilepal macro). The high bit of each nibble is the VRAM bank, not colour.
|
||||||
@@ -201,14 +198,23 @@ local function copy(value)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
function RomExtractorGen2.new(romData, manifest, progress)
|
function RomExtractorGen2.new(romData, manifest, progress, romSha1)
|
||||||
-- _GOLD / _SILVER: the labels are shared, the data behind a handful of
|
-- _GOLD / _SILVER: the labels are shared, the data behind a handful of
|
||||||
-- them is not (gfx/misc.asm:9-20 vs :46-57).
|
-- them is not (gfx/misc.asm:9-20 vs :46-57).
|
||||||
local edition = GameVersion.forSha1(manifest.romSha1) or "gold"
|
local edition = GameVersion.forSha1(manifest.romSha1) or "gold"
|
||||||
|
local symbols = manifest.symbols
|
||||||
|
local revision = romSha1 and manifest.symbolRevisions
|
||||||
|
and manifest.symbolRevisions[romSha1]
|
||||||
|
if revision then
|
||||||
|
local merged = {}
|
||||||
|
for name, location in pairs(manifest.symbols) do merged[name] = location end
|
||||||
|
for name, location in pairs(revision) do merged[name] = location end
|
||||||
|
symbols = merged
|
||||||
|
end
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
rom = Rom.new(romData),
|
rom = Rom.new(romData),
|
||||||
manifest = manifest,
|
manifest = manifest,
|
||||||
symbols = manifest.symbols,
|
symbols = symbols,
|
||||||
progress = progress,
|
progress = progress,
|
||||||
stage = 0,
|
stage = 0,
|
||||||
edition = edition,
|
edition = edition,
|
||||||
@@ -2843,7 +2849,10 @@ function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers)
|
|||||||
out[#out + 1] = "\f"
|
out[#out + 1] = "\f"
|
||||||
elseif b == 0x55 then
|
elseif b == 0x55 then
|
||||||
out[#out + 1] = "\v"
|
out[#out + 1] = "\v"
|
||||||
elseif b == 0x52 then
|
elseif b == 0x52
|
||||||
|
or (b == 0x14 and inString and self.edition == "crystal") then
|
||||||
|
-- ../pokecrystal/constants/charmap.asm:6 <PLAY_G>,
|
||||||
|
-- ../pokecrystal/home/text.asm:243,380 PlaceGenderedPlayerName
|
||||||
out[#out + 1] = "{PLAYER}"
|
out[#out + 1] = "{PLAYER}"
|
||||||
elseif b == 0x53 then
|
elseif b == 0x53 then
|
||||||
out[#out + 1] = "{RIVAL}"
|
out[#out + 1] = "{RIVAL}"
|
||||||
@@ -5707,8 +5716,8 @@ function RomExtractorGen2:extractMenuGfx()
|
|||||||
|
|
||||||
-- Four OAM tiles at $31 -- normal, statused, fainted, empty -- and OBJ
|
-- Four OAM tiles at $31 -- normal, statused, fainted, empty -- and OBJ
|
||||||
-- colour 0 is transparent (engine/battle/trainer_huds.asm:47-99, :225-232).
|
-- colour 0 is transparent (engine/battle/trainer_huds.asm:47-99, :225-232).
|
||||||
local balls = self.symbols["LoadBallIconGFX.gfx"] or BALL_ICON_GFX
|
local balls = self:symbol("LoadBallIconGFX.gfx")
|
||||||
self:write2bpp(self.rom:bytes(balls[1], balls[2], 4 * 16), 32, 8,
|
self:write2bpp(self.rom:bytes(balls.bank, balls.address, 4 * 16), 32, 8,
|
||||||
"battle/hud/balls.png", true)
|
"battle/hud/balls.png", true)
|
||||||
hud.balls = "assets/generated/battle/hud/balls.png"
|
hud.balls = "assets/generated/battle/hud/balls.png"
|
||||||
hud.ballsFirstTile = 0x31
|
hud.ballsFirstTile = 0x31
|
||||||
|
|||||||
+81
-19
@@ -1390,8 +1390,9 @@ function RomImporter.new(onComplete, opts)
|
|||||||
-- player's index list from options; findIndex is the merged listing;
|
-- player's index list from options; findIndex is the merged listing;
|
||||||
-- _findThumbs caches one image per mod id (false = fetched and failed).
|
-- _findThumbs caches one image per mod id (false = fetched and failed).
|
||||||
findLoaded = false, findSources = nil, findIndex = nil,
|
findLoaded = false, findSources = nil, findIndex = nil,
|
||||||
findScroll = 0, findNotice = nil, findQuery = "", findCategory = nil,
|
findInstalled = nil, findScroll = 0, findNotice = nil, findQuery = "",
|
||||||
_findSearchFocus = false, _findThumbs = nil,
|
findCategory = nil, _findSearchFocus = false, _findThumbs = nil,
|
||||||
|
_findVisibleEntries = nil,
|
||||||
-- Which half of the feed the panel is browsing. Mods by default: carts
|
-- Which half of the feed the panel is browsing. Mods by default: carts
|
||||||
-- are the newer, much shorter list, and a feed may carry none at all.
|
-- are the newer, much shorter list, and a feed may carry none at all.
|
||||||
findKind = "mods", findBase = nil,
|
findKind = "mods", findBase = nil,
|
||||||
@@ -1432,7 +1433,7 @@ function RomImporter.new(onComplete, opts)
|
|||||||
-- "update required" (re-import) rather than a clean first-run choose
|
-- "update required" (re-import) rather than a clean first-run choose
|
||||||
local marker = CacheContract.readMarker(version, CacheFs)
|
local marker = CacheContract.readMarker(version, CacheFs)
|
||||||
self.returning[version] =
|
self.returning[version] =
|
||||||
(not ready) and marker ~= nil and marker ~= CacheContract.markerFor(version)
|
(not ready) and marker ~= nil and not CacheContract.markerMatches(version, marker)
|
||||||
self.romName[version] = "pokemon_" .. info.id
|
self.romName[version] = "pokemon_" .. info.id
|
||||||
.. ((info.id == "yellow" or GameVersion.generation(version) == 2)
|
.. ((info.id == "yellow" or GameVersion.generation(version) == 2)
|
||||||
and ".gbc" or ".gb")
|
and ".gbc" or ".gb")
|
||||||
@@ -1721,6 +1722,7 @@ function RomImporter:startData(data, displayName)
|
|||||||
.. "(tagged [b] or [BF]) never verify."):format(actualHash, cartsProse()))
|
.. "(tagged [b] or [BF]) never verify."):format(actualHash, cartsProse()))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
self.romSha1 = actualHash
|
||||||
local info = GameVersion.info(version)
|
local info = GameVersion.info(version)
|
||||||
|
|
||||||
-- Bring the launcher to this version's tab so its progress bar is on screen
|
-- Bring the launcher to this version's tab so its progress bar is on screen
|
||||||
@@ -1775,7 +1777,7 @@ function RomImporter:_startExtractThread(version, prefix, data, displayName)
|
|||||||
love.thread.getChannel(progressName):clear()
|
love.thread.getChannel(progressName):clear()
|
||||||
love.thread.getChannel(resultName):clear()
|
love.thread.getChannel(resultName):clear()
|
||||||
local started = pcall(thread.start, thread, version, prefix, data,
|
local started = pcall(thread.start, thread, version, prefix, data,
|
||||||
progressName, resultName)
|
progressName, resultName, self.romSha1)
|
||||||
if not started then return false end
|
if not started then return false end
|
||||||
self._extract = {
|
self._extract = {
|
||||||
thread = thread, version = version, prefix = prefix,
|
thread = thread, version = version, prefix = prefix,
|
||||||
@@ -1804,7 +1806,7 @@ function RomImporter:_startExtractCoroutine(version, info, prefix, displayName)
|
|||||||
self.stageCurrent = current
|
self.stageCurrent = current
|
||||||
self.stageTotal = stageTotal
|
self.stageTotal = stageTotal
|
||||||
coroutine.yield()
|
coroutine.yield()
|
||||||
end)
|
end, self.romSha1)
|
||||||
extractor:run()
|
extractor:run()
|
||||||
CacheFs.prefix = "" -- restore the default so later writes stay at the root
|
CacheFs.prefix = "" -- restore the default so later writes stay at the root
|
||||||
self.romData = nil
|
self.romData = nil
|
||||||
@@ -1822,7 +1824,7 @@ function RomImporter:_completeImport(version, prefix, displayName)
|
|||||||
-- appear once every required file is in place.
|
-- appear once every required file is in place.
|
||||||
local savedPrefix = CacheFs.prefix
|
local savedPrefix = CacheFs.prefix
|
||||||
CacheFs.prefix = prefix
|
CacheFs.prefix = prefix
|
||||||
local ok, writeError = CacheContract.publish(version, CacheFs)
|
local ok, writeError = CacheContract.publish(version, CacheFs, self.romSha1)
|
||||||
CacheFs.prefix = savedPrefix
|
CacheFs.prefix = savedPrefix
|
||||||
if not ok then
|
if not ok then
|
||||||
error("could not finish the private cache: " .. tostring(writeError))
|
error("could not finish the private cache: " .. tostring(writeError))
|
||||||
@@ -2589,6 +2591,7 @@ function RomImporter:update(dt)
|
|||||||
-- before a tab switch still completes.
|
-- before a tab switch still completes.
|
||||||
self:_pumpFindFetch()
|
self:_pumpFindFetch()
|
||||||
self:_pumpModInfoFetch()
|
self:_pumpModInfoFetch()
|
||||||
|
self:_queueFindEnrichment()
|
||||||
self:_pumpFindStats()
|
self:_pumpFindStats()
|
||||||
self:_pumpFindThumbs()
|
self:_pumpFindThumbs()
|
||||||
self:_pumpSkinFetch()
|
self:_pumpSkinFetch()
|
||||||
@@ -3226,6 +3229,7 @@ end
|
|||||||
-- offset persists inside the view's per-tab scroll container.
|
-- offset persists inside the view's per-tab scroll container.
|
||||||
function RomImporter:_switchTab(id)
|
function RomImporter:_switchTab(id)
|
||||||
self.tab = id
|
self.tab = id
|
||||||
|
if id ~= "find" then self._findVisibleEntries = nil end
|
||||||
self._findSearchFocus = false
|
self._findSearchFocus = false
|
||||||
self._skinUrlFocus = false
|
self._skinUrlFocus = false
|
||||||
self:_disarmTextInput()
|
self:_disarmTextInput()
|
||||||
@@ -3823,6 +3827,7 @@ function RomImporter:_toggleSafeMode()
|
|||||||
SaveData.saveOptions(options)
|
SaveData.saveOptions(options)
|
||||||
self.safeMode = enabled
|
self.safeMode = enabled
|
||||||
self.mods = nil
|
self.mods = nil
|
||||||
|
self.findInstalled = nil
|
||||||
self._modSortCache = nil
|
self._modSortCache = nil
|
||||||
self._modInfoFetch = nil
|
self._modInfoFetch = nil
|
||||||
self.modNotice = nil
|
self.modNotice = nil
|
||||||
@@ -4798,6 +4803,7 @@ function RomImporter:_refreshMods()
|
|||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
self._cartPlan = nil
|
self._cartPlan = nil
|
||||||
|
self.findInstalled = nil
|
||||||
self.safeMode = SaveData.isSafeMode(SaveData.loadOptions())
|
self.safeMode = SaveData.isSafeMode(SaveData.loadOptions())
|
||||||
-- Once per session, ahead of the first listing: pull in any mod the player
|
-- Once per session, ahead of the first listing: pull in any mod the player
|
||||||
-- unzipped beside the executable, which an ordinary (non-portable) install
|
-- unzipped beside the executable, which an ordinary (non-portable) install
|
||||||
@@ -5718,7 +5724,8 @@ end
|
|||||||
-- as long as the slowest index took -- measured at over two minutes on a
|
-- as long as the slowest index took -- measured at over two minutes on a
|
||||||
-- cold open, with no spinner, because the frame that would have drawn one
|
-- cold open, with no spinner, because the frame that would have drawn one
|
||||||
-- never ran. The fetch now starts here and completes across later frames in
|
-- never ran. The fetch now starts here and completes across later frames in
|
||||||
-- _pumpFindFetch; the loader overlay is up for the whole flight.
|
-- _pumpFindFetch. Only an explicit Refresh is blocking; boot prewarm and the
|
||||||
|
-- first visit keep the launcher interactive while the listing arrives.
|
||||||
function RomImporter:_refreshFind(force)
|
function RomImporter:_refreshFind(force)
|
||||||
-- The notice is the fix, not the gate (#876). This branch used to return an
|
-- The notice is the fix, not the gate (#876). This branch used to return an
|
||||||
-- empty listing silently, and because the player had by then added a source,
|
-- empty listing silently, and because the player had by then added a source,
|
||||||
@@ -5756,9 +5763,11 @@ function RomImporter:_refreshFind(force)
|
|||||||
carts = {}, cartSeen = {}, bases = {}, baseSeen = {},
|
carts = {}, cartSeen = {}, bases = {}, baseSeen = {},
|
||||||
stale = false, oldest = nil, at = 1,
|
stale = false, oldest = nil, at = 1,
|
||||||
}
|
}
|
||||||
self:_setBusy(Strings("Fetching mod index"),
|
if force == true then
|
||||||
#sources == 1 and (sources[1].label or sources[1].feed)
|
self:_setBusy(Strings("Fetching mod index"),
|
||||||
or Strings("%d indexes", #sources))
|
#sources == 1 and (sources[1].label or sources[1].feed)
|
||||||
|
or Strings("%d indexes", #sources))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Drive the in-flight index fetch one frame at a time. Called from update().
|
-- Drive the in-flight index fetch one frame at a time. Called from update().
|
||||||
@@ -5958,9 +5967,17 @@ end
|
|||||||
|
|
||||||
function RomImporter:_findInstalledMap()
|
function RomImporter:_findInstalledMap()
|
||||||
if self:findingCarts() then return self:_findInstalledCarts() end
|
if self:findingCarts() then return self:_findInstalledCarts() end
|
||||||
local map = {}
|
if self.findInstalled then return self.findInstalled end
|
||||||
for _, m in ipairs(self.mods or {}) do map[m.id] = m.version or true end
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
return map
|
if self.mods then
|
||||||
|
self.findInstalled = {}
|
||||||
|
for _, m in ipairs(self.mods) do
|
||||||
|
self.findInstalled[m.id] = m.version or true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.findInstalled = LauncherMods.installedVersions() or {}
|
||||||
|
end
|
||||||
|
return self.findInstalled
|
||||||
end
|
end
|
||||||
|
|
||||||
-- id -> installed version for every cart on disk, whatever game it plays as.
|
-- id -> installed version for every cart on disk, whatever game it plays as.
|
||||||
@@ -5978,10 +5995,9 @@ function RomImporter:_findInstalledCarts()
|
|||||||
return map
|
return map
|
||||||
end
|
end
|
||||||
|
|
||||||
-- One thumbnail per frame, and only for a card actually on screen: the fetch
|
-- Read the cached thumbnail for a card. Starting a download is deliberately
|
||||||
-- is a blocking curl, so downloading a whole listing's worth on open would
|
-- separate: immediate-mode draw may call this for every visible row, but it
|
||||||
-- stall the launcher for as many seconds as there are mods. A failure is
|
-- must not mutate the fetch queue or perform network work.
|
||||||
-- remembered as `false` so a broken URL is tried once, not every frame.
|
|
||||||
function RomImporter:_findThumb(entry)
|
function RomImporter:_findThumb(entry)
|
||||||
self._findThumbs = self._findThumbs or {}
|
self._findThumbs = self._findThumbs or {}
|
||||||
local cached = self._findThumbs[entry.id]
|
local cached = self._findThumbs[entry.id]
|
||||||
@@ -5992,6 +6008,20 @@ function RomImporter:_findThumb(entry)
|
|||||||
self._findThumbs[entry.id] = false
|
self._findThumbs[entry.id] = false
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Queue one thumbnail after draw has recorded the visible rows. The fetch
|
||||||
|
-- pool runs off-thread; only the finished image decode stays in update().
|
||||||
|
function RomImporter:_startFindThumb(entry)
|
||||||
|
self._findThumbs = self._findThumbs or {}
|
||||||
|
if self._findThumbs[entry.id] ~= nil then return end
|
||||||
|
local ModIndex = require("src.mods.ModIndex")
|
||||||
|
local url = ModIndex.joinUrl(entry._base, entry.thumbnail)
|
||||||
|
if not url then
|
||||||
|
self._findThumbs[entry.id] = false
|
||||||
|
return
|
||||||
|
end
|
||||||
-- ASYNC (was one blocking download per frame). Only rows on the current
|
-- ASYNC (was one blocking download per frame). Only rows on the current
|
||||||
-- page ever ask, so pagination already bounds this to a page's worth of
|
-- page ever ask, so pagination already bounds this to a page's worth of
|
||||||
-- requests; the fetch pool runs them off-thread and the card shows its
|
-- requests; the fetch pool runs them off-thread and the card shows its
|
||||||
@@ -6112,9 +6142,9 @@ function RomImporter:_requestFindStats(entry)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Request-and-read, for a row that is being drawn and for the detail modal.
|
-- Read-only accessor for a row being drawn or shown in the detail modal.
|
||||||
|
-- Network work is scheduled by _queueFindEnrichment from update().
|
||||||
function RomImporter:_findStats(entry)
|
function RomImporter:_findStats(entry)
|
||||||
self:_requestFindStats(entry)
|
|
||||||
return self:_findStatsCached(entry)
|
return self:_findStatsCached(entry)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -6134,6 +6164,38 @@ function RomImporter:_findThumbPending(id)
|
|||||||
return (self._findThumbFetch and self._findThumbFetch[id]) ~= nil
|
return (self._findThumbFetch and self._findThumbFetch[id]) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Draw records the visible page in _findVisibleEntries. Queue only a small
|
||||||
|
-- batch from that snapshot during update(), keeping network scheduling out of
|
||||||
|
-- the immediate-mode render path and preventing a large index from creating a
|
||||||
|
-- burst of thumbnail/GitHub work in one frame.
|
||||||
|
local FIND_ENRICH_PER_FRAME = 2
|
||||||
|
|
||||||
|
function RomImporter:_queueFindEnrichment()
|
||||||
|
if self.tab ~= "find" or not self.findLoaded then return end
|
||||||
|
local visible = self._findVisibleEntries
|
||||||
|
if not visible then return end
|
||||||
|
local thumbnails, stats = 0, 0
|
||||||
|
for _, entry in ipairs(visible) do
|
||||||
|
if thumbnails < FIND_ENRICH_PER_FRAME
|
||||||
|
and self:_findThumb(entry) == nil
|
||||||
|
and not self:_findThumbPending(entry.id) then
|
||||||
|
self:_startFindThumb(entry)
|
||||||
|
thumbnails = thumbnails + 1
|
||||||
|
end
|
||||||
|
if stats < FIND_ENRICH_PER_FRAME
|
||||||
|
and self:_findStatsCached(entry) == nil
|
||||||
|
and entry.github and entry.github ~= ""
|
||||||
|
and not self:_findStatsPendingFor(entry.id) then
|
||||||
|
self:_requestFindStats(entry)
|
||||||
|
stats = stats + 1
|
||||||
|
end
|
||||||
|
if thumbnails >= FIND_ENRICH_PER_FRAME
|
||||||
|
and stats >= FIND_ENRICH_PER_FRAME then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Drive in-flight FIND MODS stats lookups. Called from update().
|
-- Drive in-flight FIND MODS stats lookups. Called from update().
|
||||||
function RomImporter:_pumpFindStats()
|
function RomImporter:_pumpFindStats()
|
||||||
local pending = self._findStatsPending
|
local pending = self._findStatsPending
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ function RomManifest.decode(version)
|
|||||||
if not manifest then
|
if not manifest then
|
||||||
error("ROM import metadata is invalid: " .. tostring(decodeError))
|
error("ROM import metadata is invalid: " .. tostring(decodeError))
|
||||||
end
|
end
|
||||||
assert(manifest.romSha1 == info.sha1, "ROM import metadata version mismatch")
|
assert(GameVersion.acceptsSha1(version, manifest.romSha1),
|
||||||
|
"ROM import metadata version mismatch")
|
||||||
return manifest
|
return manifest
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -471,6 +471,20 @@ discover = function()
|
|||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- installedVersions() -> id -> installed version. MOD INDEX only needs to
|
||||||
|
-- know whether a listing is already present; it does not need enablement,
|
||||||
|
-- dependency/conflict status, required-import validation, or migration. Keep
|
||||||
|
-- that cheap read separate from list(), whose richer work belongs to MODS.
|
||||||
|
function LauncherMods.installedVersions()
|
||||||
|
local out = {}
|
||||||
|
local ok, manifests = pcall(discover)
|
||||||
|
if not ok then return out end
|
||||||
|
for _, manifest in ipairs(manifests or {}) do
|
||||||
|
out[manifest.id] = manifest.version or true
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
-- list([version]) -> the mods-panel rows for the current install. Reads the
|
-- list([version]) -> the mods-panel rows for the current install. Reads the
|
||||||
-- same enable-state the loader persists, so a toggle here is what the game
|
-- same enable-state the loader persists, so a toggle here is what the game
|
||||||
-- sees on its next boot; `version` narrows that to one game's answers.
|
-- sees on its next boot; `version` narrows that to one game's answers.
|
||||||
|
|||||||
+13
-3
@@ -371,9 +371,13 @@ end
|
|||||||
-- `es`: true for GLSL ES 1.00 (mobile), false for GLSL 1.20 (desktop). Only
|
-- `es`: true for GLSL ES 1.00 (mobile), false for GLSL 1.20 (desktop). Only
|
||||||
-- ever called from ShaderFX.convert().
|
-- ever called from ShaderFX.convert().
|
||||||
function ShaderFX.translate(fullPath, es)
|
function ShaderFX.translate(fullPath, es)
|
||||||
local ffi = require("ffi")
|
local okFfi, ffi = pcall(require, "ffi")
|
||||||
local ok, l = pcall(ensureLib)
|
if not okFfi or type(ffi) ~= "table" then
|
||||||
|
return nil, "this build has no ffi, so presets cannot be converted here"
|
||||||
|
end
|
||||||
|
local ok, l, lerr = pcall(ensureLib)
|
||||||
if not ok then return nil, "ffi.load failed: " .. tostring(l) end
|
if not ok then return nil, "ffi.load failed: " .. tostring(l) end
|
||||||
|
if not l then return nil, tostring(lerr or libError or "librashader bridge not available") end
|
||||||
local ptr = l.librashader_translate_preset(fullPath, es and 1 or 0)
|
local ptr = l.librashader_translate_preset(fullPath, es and 1 or 0)
|
||||||
if ptr == nil then return nil, "librashader_translate_preset returned NULL" end
|
if ptr == nil then return nil, "librashader_translate_preset returned NULL" end
|
||||||
local json = ffi.string(ptr)
|
local json = ffi.string(ptr)
|
||||||
@@ -780,7 +784,7 @@ end
|
|||||||
|
|
||||||
-- Translates `entry` via the bridge and writes ShaderFX.artifactPath(entry).
|
-- Translates `entry` via the bridge and writes ShaderFX.artifactPath(entry).
|
||||||
-- Sets entry.converted on success; an existing artifact survives a failure.
|
-- Sets entry.converted on success; an existing artifact survives a failure.
|
||||||
function ShaderFX.convert(entry, es)
|
local function doConvert(entry, es)
|
||||||
ShaderSourcePatches.apply(entry)
|
ShaderSourcePatches.apply(entry)
|
||||||
local preset, err = ShaderFX.translate(entry.fullPath, es == nil and defaultEs() or es)
|
local preset, err = ShaderFX.translate(entry.fullPath, es == nil and defaultEs() or es)
|
||||||
if not preset then return false, err end
|
if not preset then return false, err end
|
||||||
@@ -797,6 +801,12 @@ function ShaderFX.convert(entry, es)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ShaderFX.convert(entry, es)
|
||||||
|
local ok, res, err = pcall(doConvert, entry, es)
|
||||||
|
if not ok then return false, tostring(res) end
|
||||||
|
return res, err
|
||||||
|
end
|
||||||
|
|
||||||
-- Two independent slots. "shaderfx" stays main's option key, so existing
|
-- Two independent slots. "shaderfx" stays main's option key, so existing
|
||||||
-- saves keep meaning what they already meant.
|
-- saves keep meaning what they already meant.
|
||||||
ShaderFX.SLOTS = { "main", "secondary" }
|
ShaderFX.SLOTS = { "main", "secondary" }
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ function ShaderFXScreen.new(game, slot)
|
|||||||
item.entry.name, tostring(err))
|
item.entry.name, tostring(err))
|
||||||
end
|
end
|
||||||
applyRowState(item)
|
applyRowState(item)
|
||||||
|
if not ok then item.right = Strings("FAILED") end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+17
-10
@@ -38,9 +38,12 @@ local MON_HOLDING_MAIL = {
|
|||||||
Strings.source("Please remove the\nMAIL."),
|
Strings.source("Please remove the\nMAIL."),
|
||||||
}
|
}
|
||||||
|
|
||||||
-- _ChangeBoxSaveText (data/text/common_2.asm:1306) is three lines whose `cont`
|
-- _ChangeBoxSaveText (data/text/common_2.asm:1306) is three lines whose first
|
||||||
-- has already scrolled by the time YesNoBox goes up over its last two.
|
-- `cont` ("When you change a") has already scrolled by the time YesNoBox goes
|
||||||
local CHANGE_BOX_SAVE = { "#MON BOX, data", "will be saved. OK?" }
|
-- up over its last two -- confirmed against poke-corpus GoldSilver
|
||||||
|
-- en_msg.txt:4897. One \n-joined translatable key, same pattern as
|
||||||
|
-- SaveMenu.lua's OVERWRITE_PROMPT_SOURCE/SAVING_PROMPT_SOURCE.
|
||||||
|
local CHANGE_BOX_SAVE_SOURCE = Strings.source("#MON BOX, data\nwill be saved. OK?")
|
||||||
|
|
||||||
-- YesNoBox's own `lb bc, SCREEN_WIDTH - 6, 7` (home/menu.asm:382-383).
|
-- YesNoBox's own `lb bc, SCREEN_WIDTH - 6, 7` (home/menu.asm:382-383).
|
||||||
local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 14, 7, 6, 5
|
local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 14, 7, 6, 5
|
||||||
@@ -212,16 +215,20 @@ function PcMenu:writeChangeBox()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function PcMenu:savePrompt()
|
function PcMenu:savePrompt()
|
||||||
if self.savePhase == "overwrite" then return SaveMenu.OVERWRITE_PROMPT end
|
if self.savePhase == "overwrite" then
|
||||||
if self.savePhase == "saving" then return SaveMenu.SAVING_PROMPT end
|
return SaveMenu.twoLines(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE))
|
||||||
|
end
|
||||||
|
if self.savePhase == "saving" then
|
||||||
|
return SaveMenu.twoLines(Strings(SaveMenu.SAVING_PROMPT_SOURCE))
|
||||||
|
end
|
||||||
if self.savePhase == "done" then
|
if self.savePhase == "done" then
|
||||||
if self.saved then
|
if self.saved then
|
||||||
local name = (self.save.player and self.save.player.name) or "GOLD"
|
local name = (self.save.player and self.save.player.name) or "GOLD"
|
||||||
return { name .. " saved", "the game." }
|
return SaveMenu.twoLines(Strings("%s saved\nthe game.", name))
|
||||||
end
|
end
|
||||||
return { "Could not save.", "" }
|
return SaveMenu.twoLines(Strings("Could not save."))
|
||||||
end
|
end
|
||||||
return CHANGE_BOX_SAVE
|
return SaveMenu.twoLines(Strings(CHANGE_BOX_SAVE_SOURCE))
|
||||||
end
|
end
|
||||||
|
|
||||||
function PcMenu:updateChangeBox()
|
function PcMenu:updateChangeBox()
|
||||||
@@ -404,8 +411,8 @@ function PcMenu:drawPanel()
|
|||||||
Chrome.print(lines[2] or "", 1, 16)
|
Chrome.print(lines[2] or "", 1, 16)
|
||||||
if self.savePhase == "confirm" or self.savePhase == "overwrite" then
|
if self.savePhase == "confirm" or self.savePhase == "overwrite" then
|
||||||
Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H)
|
Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H)
|
||||||
Chrome.print("YES", YESNO_X + 2, YESNO_Y + 1)
|
Chrome.print(Strings("YES"), YESNO_X + 2, YESNO_Y + 1)
|
||||||
Chrome.print("NO", YESNO_X + 2, YESNO_Y + 3)
|
Chrome.print(Strings("NO"), YESNO_X + 2, YESNO_Y + 3)
|
||||||
Chrome.cursor(YESNO_X + 1,
|
Chrome.cursor(YESNO_X + 1,
|
||||||
YESNO_Y + (self.saveChoice == 1 and 1 or 3))
|
YESNO_Y + (self.saveChoice == 1 and 1 or 3))
|
||||||
end
|
end
|
||||||
|
|||||||
+17
-22
@@ -57,27 +57,21 @@ local TIME_X, TIME_Y = 13, 8
|
|||||||
local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5
|
local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5
|
||||||
|
|
||||||
-- AlreadyASaveFileText (AskOverwriteSaveFile, engine/menus/save.asm:47) and
|
-- AlreadyASaveFileText (AskOverwriteSaveFile, engine/menus/save.asm:47) and
|
||||||
-- SavingDontTurnOffThePower's own line, shared with the PC's CHANGE BOX save
|
-- SavingDontTurnOffThePower's own line -- one \n-joined translatable key
|
||||||
-- (src/ui/gen2/PcMenu.lua:savePrompt() reads these two tables' lines[1]/
|
-- each, used both by this screen's own prompt() below and, through the
|
||||||
-- lines[2] directly, so their shape is a cross-file contract: keep them
|
-- SOURCE/twoLines() exports at the bottom of this file, by the PC's CHANGE
|
||||||
-- plain, untranslated tables).
|
-- BOX save (src/ui/gen2/PcMenu.lua:savePrompt()), which shares these exact
|
||||||
local OVERWRITE_PROMPT = { "There is already a", "save file. Is it" }
|
-- same two cart messages. One key per prompt lets a translation write one
|
||||||
local SAVING_PROMPT = { "SAVING… DON'T TURN", "OFF THE POWER." }
|
-- whole, freely reordered sentence instead of two fragments translated in
|
||||||
|
-- isolation, and lets a cart whose own text is a single line (German's
|
||||||
-- Translatable copies of the two prompts above, one \n-joined key each, used
|
-- SAVING prompt) say so directly by simply omitting the "\n" -- the
|
||||||
-- only by this screen's own prompt() below. One key per prompt lets a
|
-- per-line override style used elsewhere requires a non-empty value for
|
||||||
-- translation write one whole, freely reordered sentence instead of two
|
-- every line, so it can't express "this line is blank".
|
||||||
-- fragments translated in isolation, and lets a cart whose own text is a
|
|
||||||
-- single line (German's SAVING prompt) say so directly by simply omitting
|
|
||||||
-- the "\n" -- the per-line override style used elsewhere requires a
|
|
||||||
-- non-empty value for every line, so it can't express "this line is blank".
|
|
||||||
--
|
--
|
||||||
-- Written as literals, not `table.concat(OVERWRITE_PROMPT, "\n")`: the
|
-- Written as a literal, not built from a table: the translation tooling's
|
||||||
-- translation tooling's string harvester only recognizes a literal inside
|
-- string harvester only recognizes a literal inside Strings.source(...),
|
||||||
-- Strings.source(...), not a computed expression, so a concat call here
|
-- not a computed expression, so a concat call here would quietly never
|
||||||
-- would quietly never reach a translator. Keep byte-for-byte in sync with
|
-- reach a translator.
|
||||||
-- OVERWRITE_PROMPT/SAVING_PROMPT above (checked by
|
|
||||||
-- tests/engine/gen2_save_menu_translation_test.lua).
|
|
||||||
local OVERWRITE_PROMPT_SOURCE = Strings.source("There is already a\nsave file. Is it")
|
local OVERWRITE_PROMPT_SOURCE = Strings.source("There is already a\nsave file. Is it")
|
||||||
local SAVING_PROMPT_SOURCE = Strings.source("SAVING… DON'T TURN\nOFF THE POWER.")
|
local SAVING_PROMPT_SOURCE = Strings.source("SAVING… DON'T TURN\nOFF THE POWER.")
|
||||||
|
|
||||||
@@ -279,7 +273,8 @@ end
|
|||||||
SaveMenu.SFX_SAVE = SFX_SAVE
|
SaveMenu.SFX_SAVE = SFX_SAVE
|
||||||
SaveMenu.SAVING_FRAMES = SAVING_FRAMES
|
SaveMenu.SAVING_FRAMES = SAVING_FRAMES
|
||||||
SaveMenu.SAVED_FRAMES = SAVED_FRAMES
|
SaveMenu.SAVED_FRAMES = SAVED_FRAMES
|
||||||
SaveMenu.OVERWRITE_PROMPT = OVERWRITE_PROMPT
|
SaveMenu.OVERWRITE_PROMPT_SOURCE = OVERWRITE_PROMPT_SOURCE
|
||||||
SaveMenu.SAVING_PROMPT = SAVING_PROMPT
|
SaveMenu.SAVING_PROMPT_SOURCE = SAVING_PROMPT_SOURCE
|
||||||
|
SaveMenu.twoLines = twoLines
|
||||||
|
|
||||||
return SaveMenu
|
return SaveMenu
|
||||||
|
|||||||
@@ -37,6 +37,36 @@ eq(crystal.romSha1, GameVersion.VERSIONS.crystal.sha1,
|
|||||||
eq(crystal.generation, 2, "generation 2")
|
eq(crystal.generation, 2, "generation 2")
|
||||||
eq(crystal.format, gold.format, "same manifest format as Gold")
|
eq(crystal.format, gold.format, "same manifest format as Gold")
|
||||||
|
|
||||||
|
-- ------- 1b. the 1.1 revision's symbol delta
|
||||||
|
|
||||||
|
local rev11
|
||||||
|
for _, revision in ipairs(GameVersion.revisions("crystal")) do
|
||||||
|
if revision.label == "1.1" then rev11 = revision end
|
||||||
|
end
|
||||||
|
check(rev11 ~= nil, "the crystal row names a 1.1 revision")
|
||||||
|
|
||||||
|
if rev11 then
|
||||||
|
check(GameVersion.acceptsSha1("crystal", GameVersion.VERSIONS.crystal.sha1),
|
||||||
|
"the crystal row accepts the canonical 1.0 sha1")
|
||||||
|
check(GameVersion.acceptsSha1("crystal", rev11.sha1),
|
||||||
|
"and the 1.1 sha1 too")
|
||||||
|
|
||||||
|
local overlay = (crystal.symbolRevisions or {})[rev11.sha1]
|
||||||
|
check(type(overlay) == "table",
|
||||||
|
"the manifest carries a symbolRevisions overlay for the 1.1 sha1")
|
||||||
|
if overlay then
|
||||||
|
eq(size(overlay), 1, "with exactly one symbol moved")
|
||||||
|
local moved = overlay.Stadium2N64Attrmap
|
||||||
|
check(type(moved) == "table", "and it names Stadium2N64Attrmap")
|
||||||
|
local base = crystal.symbols.Stadium2N64Attrmap
|
||||||
|
check(base ~= nil, "the base manifest still carries Stadium2N64Attrmap")
|
||||||
|
if moved and base then
|
||||||
|
check(not (moved[1] == base[1] and moved[2] == base[2]),
|
||||||
|
"at a location different from the base symbols entry")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- ------- 2. content counts
|
-- ------- 2. content counts
|
||||||
|
|
||||||
eq(size(crystal.maps), 388, "388 maps")
|
eq(size(crystal.maps), 388, "388 maps")
|
||||||
|
|||||||
@@ -68,11 +68,56 @@ eq(GameVersion.ORDER[5], "silver", "silver keeps slot 5")
|
|||||||
-- ------- 4. sha1 routing
|
-- ------- 4. sha1 routing
|
||||||
|
|
||||||
eq(GameVersion.forSha1("f4cd194bdee0d04ca4eac29e09b8e4e9d818c133"), "crystal",
|
eq(GameVersion.forSha1("f4cd194bdee0d04ca4eac29e09b8e4e9d818c133"), "crystal",
|
||||||
"the retail Crystal sha1 resolves to crystal")
|
"the retail Crystal 1.0 sha1 resolves to crystal")
|
||||||
|
eq(GameVersion.forSha1("f2f52230b536214ef7c9924f483392993e226cfb"), "crystal",
|
||||||
|
"the retail Crystal 1.1 sha1 also resolves to crystal")
|
||||||
eq(GameVersion.forSha1("d8b8a3600a465308c9953dfa04f0081c05bdcb94"), "gold",
|
eq(GameVersion.forSha1("d8b8a3600a465308c9953dfa04f0081c05bdcb94"), "gold",
|
||||||
"Gold's sha1 still resolves to gold")
|
"Gold's sha1 still resolves to gold")
|
||||||
eq(GameVersion.forSha1("deadbeef"), nil, "an unknown ROM resolves to nothing")
|
eq(GameVersion.forSha1("deadbeef"), nil, "an unknown ROM resolves to nothing")
|
||||||
|
|
||||||
|
-- ------- 4b. revisions / acceptsSha1 / revisionLabel
|
||||||
|
|
||||||
|
local crystalRevisions = GameVersion.revisions("crystal")
|
||||||
|
eq(#crystalRevisions, 2, "crystal lists both accepted revisions")
|
||||||
|
eq(crystalRevisions[1].sha1, "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133",
|
||||||
|
"revision 1 is the 1.0 sha1")
|
||||||
|
eq(crystalRevisions[1].label, "1.0", "revision 1 is labeled 1.0")
|
||||||
|
eq(crystalRevisions[2].sha1, "f2f52230b536214ef7c9924f483392993e226cfb",
|
||||||
|
"revision 2 is the 1.1 sha1")
|
||||||
|
eq(crystalRevisions[2].label, "1.1", "revision 2 is labeled 1.1")
|
||||||
|
|
||||||
|
check(GameVersion.acceptsSha1("crystal", "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133"),
|
||||||
|
"crystal accepts the 1.0 sha1")
|
||||||
|
check(GameVersion.acceptsSha1("crystal", "f2f52230b536214ef7c9924f483392993e226cfb"),
|
||||||
|
"crystal accepts the 1.1 sha1")
|
||||||
|
check(not GameVersion.acceptsSha1("crystal", "deadbeef"),
|
||||||
|
"crystal rejects an unknown sha1")
|
||||||
|
|
||||||
|
eq(GameVersion.revisionLabel("crystal", "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133"),
|
||||||
|
"1.0", "revisionLabel resolves the 1.0 hash")
|
||||||
|
eq(GameVersion.revisionLabel("crystal", "f2f52230b536214ef7c9924f483392993e226cfb"),
|
||||||
|
"1.1", "revisionLabel resolves the 1.1 hash")
|
||||||
|
eq(GameVersion.revisionLabel("crystal", "deadbeef"), nil,
|
||||||
|
"revisionLabel is nil for an unrecognized hash")
|
||||||
|
|
||||||
|
local goldRevisions = GameVersion.revisions("gold")
|
||||||
|
eq(#goldRevisions, 1, "gold synthesizes a single revision entry")
|
||||||
|
eq(goldRevisions[1].sha1, GameVersion.info("gold").sha1,
|
||||||
|
"the synthesized entry carries gold's canonical sha1")
|
||||||
|
check(GameVersion.acceptsSha1("gold", GameVersion.info("gold").sha1),
|
||||||
|
"gold accepts its own canonical sha1")
|
||||||
|
check(not GameVersion.acceptsSha1("gold", "deadbeef"),
|
||||||
|
"gold rejects an unknown sha1")
|
||||||
|
eq(GameVersion.revisionLabel("gold", GameVersion.info("gold").sha1), nil,
|
||||||
|
"gold's synthesized entry carries no label")
|
||||||
|
|
||||||
|
local redRevisions = GameVersion.revisions("red")
|
||||||
|
eq(#redRevisions, 1, "red synthesizes a single revision entry")
|
||||||
|
check(GameVersion.acceptsSha1("red", GameVersion.info("red").sha1),
|
||||||
|
"red accepts its own canonical sha1")
|
||||||
|
eq(GameVersion.forSha1(GameVersion.info("red").sha1), "red",
|
||||||
|
"and forSha1 still resolves red through the synthesized entry")
|
||||||
|
|
||||||
-- ------- 5. set / get round trip
|
-- ------- 5. set / get round trip
|
||||||
|
|
||||||
local savedCurrent = GameVersion.get()
|
local savedCurrent = GameVersion.get()
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
-- The PC's CHANGE BOX save flow (src/ui/gen2/PcMenu.lua:savePrompt()) used to
|
||||||
|
-- draw its overwrite/saving/done prompts and its YES/NO choice as bare
|
||||||
|
-- literals, invisible to a translation mod's `strings` registry, even though
|
||||||
|
-- the overwrite/saving prompts are the exact same two cart messages Gold's
|
||||||
|
-- SAVE screen (src/ui/gen2/SaveMenu.lua) already routes through Strings().
|
||||||
|
-- Same technique as tests/engine/gen2_save_menu_translation_test.lua: drives
|
||||||
|
-- PcMenu:drawPanel() directly at each save phase with a mod-loaded Strings
|
||||||
|
-- catalog and checks the translated text reaches Font.draw.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
|
||||||
|
love = require("tests.love_stub")
|
||||||
|
|
||||||
|
require("src.core.Logger").warn = function() end
|
||||||
|
|
||||||
|
local drawn
|
||||||
|
package.loaded["src.render.Font"] = {
|
||||||
|
draw = function(text, x, y)
|
||||||
|
drawn[#drawn + 1] = { text = text, x = x, y = y }
|
||||||
|
end,
|
||||||
|
drawCode = function() end,
|
||||||
|
drawBox = function() end,
|
||||||
|
width = function() return 0 end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local PcMenu = require("src.ui.gen2.PcMenu")
|
||||||
|
local Strings = require("src.core.Strings")
|
||||||
|
|
||||||
|
local function drawnAt(x, y)
|
||||||
|
for _, d in ipairs(drawn) do
|
||||||
|
if d.x == x and d.y == y then return d.text end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Chrome.print multiplies tile coordinates by 8 (src/ui/gen2/Chrome.lua).
|
||||||
|
-- The save-prompt box sits at the same (0,12) origin SaveMenu.lua's does, so
|
||||||
|
-- its two lines print at the same (1,14)/(1,16); PcMenu's own YESNO_X/Y
|
||||||
|
-- (14,7) differ from SaveMenu's (0,7), so YES/NO print at (16,8)/(16,10).
|
||||||
|
local PROMPT1_X, PROMPT1_Y = 1 * 8, 14 * 8
|
||||||
|
local PROMPT2_X, PROMPT2_Y = 1 * 8, 16 * 8
|
||||||
|
local YES_X, YES_Y = 16 * 8, 8 * 8
|
||||||
|
local NO_X, NO_Y = 16 * 8, 10 * 8
|
||||||
|
|
||||||
|
-- One party mon so Boxes.canUsePc doesn't refuse to open the PC at all.
|
||||||
|
local SAVE = { player = { name = "GOLD" }, party = { {} } }
|
||||||
|
|
||||||
|
local function newMenu()
|
||||||
|
return PcMenu.new({}, {
|
||||||
|
save = SAVE,
|
||||||
|
saveExists = false,
|
||||||
|
writer = function() return true end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------- vanilla: no mod catalog
|
||||||
|
do
|
||||||
|
local menu = newMenu()
|
||||||
|
menu.picking = true
|
||||||
|
menu.pickIndex = 1
|
||||||
|
menu.savePhase = "confirm"
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "#MON BOX, data", "the confirm prompt draws in English with no mod loaded")
|
||||||
|
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "will be saved. OK?", "and its second line")
|
||||||
|
T.eq(drawnAt(YES_X, YES_Y), "YES", "and YES")
|
||||||
|
T.eq(drawnAt(NO_X, NO_Y), "NO", "and NO")
|
||||||
|
|
||||||
|
menu.savePhase = "overwrite"
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "There is already a",
|
||||||
|
"the overwrite prompt, the same cart message SaveMenu.lua's SAVE screen shares")
|
||||||
|
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "save file. Is it", "its second line")
|
||||||
|
|
||||||
|
menu.savePhase = "saving"
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "SAVING… DON'T TURN", "the saving message")
|
||||||
|
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "OFF THE POWER.", "its second line")
|
||||||
|
|
||||||
|
menu.savePhase, menu.saved = "done", true
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "GOLD saved", "the saved message")
|
||||||
|
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "the game.", "its second line")
|
||||||
|
|
||||||
|
menu.savePhase, menu.saved = "done", false
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Could not save.", "the failed-save message")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ------------------------------------------------- a translation mod's turn
|
||||||
|
--
|
||||||
|
-- Same catalog values as gen2_save_menu_translation_test.lua's own
|
||||||
|
-- translated block: the overwrite/saving prompts, the saved/failed messages,
|
||||||
|
-- and YES/NO are the exact same keys both screens read, so one translation
|
||||||
|
-- covers both without a PcMenu-specific fork. Only the CHANGE BOX confirm
|
||||||
|
-- prompt's key is new here.
|
||||||
|
do
|
||||||
|
Strings.load({
|
||||||
|
strings = {
|
||||||
|
["YES"] = "OUI",
|
||||||
|
["NO"] = "NON",
|
||||||
|
["#MON BOX, data\nwill be saved. OK?"] = "Les donnees de la\nBOITE seront sauv.",
|
||||||
|
["There is already a\nsave file. Is it"] = "Un fichier existe\ndeja. Est-ce",
|
||||||
|
["SAVING… DON'T TURN\nOFF THE POWER."] = "SAUVEGARDE...\nN'ETEIGNEZ PAS.",
|
||||||
|
["%s saved\nthe game."] = "%s a sauvegarde\nla partie.",
|
||||||
|
["Could not save."] = "Echec de sauvegarde.",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
local menu = newMenu()
|
||||||
|
menu.picking = true
|
||||||
|
menu.pickIndex = 1
|
||||||
|
menu.savePhase = "confirm"
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Les donnees de la", "the confirm prompt")
|
||||||
|
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "BOITE seront sauv.", "its second line")
|
||||||
|
T.eq(drawnAt(YES_X, YES_Y), "OUI", "and YES")
|
||||||
|
T.eq(drawnAt(NO_X, NO_Y), "NON", "and NO")
|
||||||
|
|
||||||
|
menu.savePhase = "overwrite"
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Un fichier existe",
|
||||||
|
"the overwrite prompt, translated with no PcMenu-specific key")
|
||||||
|
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "deja. Est-ce", "its second line")
|
||||||
|
|
||||||
|
menu.savePhase = "saving"
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "SAUVEGARDE...", "the saving message")
|
||||||
|
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "N'ETEIGNEZ PAS.", "its second line")
|
||||||
|
|
||||||
|
menu.savePhase, menu.saved = "done", true
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "GOLD a sauvegarde",
|
||||||
|
"the saved message folds the player name into the mod's own word order")
|
||||||
|
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "la partie.", "its second line")
|
||||||
|
|
||||||
|
menu.savePhase, menu.saved = "done", false
|
||||||
|
drawn = {}
|
||||||
|
menu:drawPanel()
|
||||||
|
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Echec de sauvegarde.", "the failed-save message")
|
||||||
|
|
||||||
|
-- Module state is process-global (see tests/gen2_clock_test.lua's own
|
||||||
|
-- note); this suite gets its own process from tests/tier_runner.lua, but
|
||||||
|
-- leaving the catalog loaded past this point would still mistranslate
|
||||||
|
-- every check below it in this file.
|
||||||
|
Strings.load({})
|
||||||
|
T.check(not Strings.active(), "the catalog is unloaded for the checks after this one")
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("gen2_pcmenu_changebox_save_translation_test")
|
||||||
@@ -147,21 +147,26 @@ do
|
|||||||
T.check(not Strings.active(), "the catalog is unloaded for the checks after this one")
|
T.check(not Strings.active(), "the catalog is unloaded for the checks after this one")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- src/ui/gen2/PcMenu.lua:savePrompt() returns SaveMenu.OVERWRITE_PROMPT/
|
-- src/ui/gen2/PcMenu.lua:savePrompt() shares SaveMenu's overwrite/saving
|
||||||
-- SAVING_PROMPT straight through to its own `lines[1]`/`lines[2]`
|
-- prompts through SaveMenu.OVERWRITE_PROMPT_SOURCE/SAVING_PROMPT_SOURCE and
|
||||||
-- Chrome.print calls (the PC's CHANGE BOX save uses the same two prompts).
|
-- SaveMenu.twoLines(), rather than duplicating them -- both exported below,
|
||||||
-- Indexing a plain string with [1]/[2] returns nil, not characters, so this
|
-- both used by PcMenu's own translation test
|
||||||
-- shape is a cross-file contract: it caught a real regression during review,
|
-- (tests/engine/gen2_pcmenu_changebox_save_translation_test.lua). Checked
|
||||||
-- where routing these through a single Strings.source()-wrapped string (to
|
-- here that they stay callable the shape twoLines() expects: a table in,
|
||||||
-- translate SaveMenu's own screen) silently turned them into non-table
|
-- one \n-joined string out with the split back on load.
|
||||||
-- values and left PcMenu's overwrite/saving prompt blank.
|
|
||||||
do
|
do
|
||||||
T.eq(type(SaveMenu.OVERWRITE_PROMPT), "table", "OVERWRITE_PROMPT stays a table for PcMenu.lua")
|
T.eq(SaveMenu.OVERWRITE_PROMPT_SOURCE, "There is already a\nsave file. Is it",
|
||||||
T.eq(SaveMenu.OVERWRITE_PROMPT[1], "There is already a", "and its first line stays indexable")
|
"OVERWRITE_PROMPT_SOURCE stays the cart's own \\n-joined text")
|
||||||
T.eq(SaveMenu.OVERWRITE_PROMPT[2], "save file. Is it", "and its second line")
|
T.eq(SaveMenu.twoLines(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE))[1], "There is already a",
|
||||||
T.eq(type(SaveMenu.SAVING_PROMPT), "table", "SAVING_PROMPT stays a table for PcMenu.lua")
|
"and twoLines() splits its untranslated fallback back to the first line")
|
||||||
T.eq(SaveMenu.SAVING_PROMPT[1], "SAVING… DON'T TURN", "and its first line stays indexable")
|
T.eq(SaveMenu.twoLines(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE))[2], "save file. Is it",
|
||||||
T.eq(SaveMenu.SAVING_PROMPT[2], "OFF THE POWER.", "and its second line")
|
"and its second line")
|
||||||
|
T.eq(SaveMenu.SAVING_PROMPT_SOURCE, "SAVING… DON'T TURN\nOFF THE POWER.",
|
||||||
|
"SAVING_PROMPT_SOURCE stays the cart's own \\n-joined text")
|
||||||
|
T.eq(SaveMenu.twoLines(Strings(SaveMenu.SAVING_PROMPT_SOURCE))[1], "SAVING… DON'T TURN",
|
||||||
|
"and twoLines() splits its untranslated fallback back to the first line")
|
||||||
|
T.eq(SaveMenu.twoLines(Strings(SaveMenu.SAVING_PROMPT_SOURCE))[2], "OFF THE POWER.",
|
||||||
|
"and its second line")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A translation with a THIRD line (a second embedded "\n") has nowhere on
|
-- A translation with a THIRD line (a second embedded "\n") has nowhere on
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ end
|
|||||||
do
|
do
|
||||||
local ri = launcher()
|
local ri = launcher()
|
||||||
check(ri:_findStats(entry("nulled", nil, "someone/nulled")) == nil,
|
check(ri:_findStats(entry("nulled", nil, "someone/nulled")) == nil,
|
||||||
"a null count is not an answer; the repo is still consulted")
|
"a null count is not an answer")
|
||||||
|
ri:_requestFindStats(entry("nulled", nil, "someone/nulled"))
|
||||||
eq(#fetched, 1, "which is the fetch the panel already made for dates")
|
eq(#fetched, 1, "which is the fetch the panel already made for dates")
|
||||||
|
|
||||||
local bare = launcher()
|
local bare = launcher()
|
||||||
@@ -83,7 +84,7 @@ do
|
|||||||
check(stats ~= nil and stats.total == nil,
|
check(stats ~= nil and stats.total == nil,
|
||||||
"a listing with neither counts nor a repo is resolved-but-unknown")
|
"a listing with neither counts nor a repo is resolved-but-unknown")
|
||||||
check(stats.recent == nil, "and has nothing to trend on")
|
check(stats.recent == nil, "and has nothing to trend on")
|
||||||
eq(#fetched, 1, "and queues nothing of its own")
|
eq(#fetched, 1, "and the explicit scheduler queues nothing without a repo")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A real zero is not unknown: the index has seen the releases and counted
|
-- A real zero is not unknown: the index has seen the releases and counted
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- Launcher navigation performance seams. MOD INDEX must not pay the full
|
||||||
|
-- MODS validation pass, background index prefetch must not create a blocking
|
||||||
|
-- overlay, and visible-row enrichment must be scheduled from update state
|
||||||
|
-- rather than from immediate-mode draw calls.
|
||||||
|
-- luajit tests/engine/launcher_navigation_perf.lua
|
||||||
|
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
local check, eq = T.check, T.eq
|
||||||
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
|
local ModIndex = require("src.mods.ModIndex")
|
||||||
|
local RomImporter = require("src.import.RomImporter")
|
||||||
|
|
||||||
|
check(type(LauncherMods.installedVersions) == "function",
|
||||||
|
"LauncherMods exposes a lightweight installed-version scan")
|
||||||
|
|
||||||
|
do
|
||||||
|
local old = LauncherMods.installedVersions
|
||||||
|
local called = 0
|
||||||
|
LauncherMods.installedVersions = function()
|
||||||
|
called = called + 1
|
||||||
|
return { alpha = "1.2.3" }
|
||||||
|
end
|
||||||
|
local imp = setmetatable({}, RomImporter)
|
||||||
|
local installed = imp:_findInstalledMap()
|
||||||
|
eq(called, 1, "MOD INDEX asks for the lightweight scan when MODS is cold")
|
||||||
|
eq(installed.alpha, "1.2.3",
|
||||||
|
"the lightweight scan supplies installed versions")
|
||||||
|
LauncherMods.installedVersions = old
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local imp = setmetatable({ tab = "find", findLoaded = false }, RomImporter)
|
||||||
|
imp._refreshFindSources = function(self)
|
||||||
|
self.findSources = { { feed = "https://example.invalid/index.json" } }
|
||||||
|
end
|
||||||
|
local oldBegin = ModIndex.beginFetch
|
||||||
|
ModIndex.beginFetch = function() return { test = true } end
|
||||||
|
imp:_refreshFind(false)
|
||||||
|
check(imp._findFetch ~= nil, "background index refresh starts asynchronously")
|
||||||
|
eq(imp._busy, nil,
|
||||||
|
"background index refresh does not block navigation with a loader")
|
||||||
|
imp:_clearBusy()
|
||||||
|
imp._findFetch = nil
|
||||||
|
ModIndex.beginFetch = oldBegin
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local requests = 0
|
||||||
|
local imp = setmetatable({ tab = "find", findLoaded = true,
|
||||||
|
_findVisibleEntries = {
|
||||||
|
{ id = "one", thumbnail = "one.png", github = "a/one" },
|
||||||
|
{ id = "two", thumbnail = "two.png", github = "a/two" },
|
||||||
|
{ id = "three", thumbnail = "three.png", github = "a/three" },
|
||||||
|
} }, RomImporter)
|
||||||
|
imp._findThumb = function() return nil end
|
||||||
|
imp._findThumbPending = function() return false end
|
||||||
|
imp._findStatsCached = function() return nil end
|
||||||
|
imp._startFindThumb = function() requests = requests + 1 end
|
||||||
|
imp._requestFindStats = function() requests = requests + 1 end
|
||||||
|
imp:_queueFindEnrichment()
|
||||||
|
eq(requests, 4,
|
||||||
|
"update schedules a bounded thumbnail and stats batch for visible rows")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local requests = 0
|
||||||
|
local imp = setmetatable({}, RomImporter)
|
||||||
|
imp._findStatsCached = function() return nil end
|
||||||
|
imp._requestFindStats = function() requests = requests + 1 end
|
||||||
|
imp:_findStats({ id = "draw-only", github = "a/draw-only" })
|
||||||
|
eq(requests, 0, "reading row stats during draw never starts a request")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local f = assert(io.open("src/import/LauncherView.lua", "rb"))
|
||||||
|
local src = f:read("*a")
|
||||||
|
f:close()
|
||||||
|
local start = assert(src:find("local function buildFindPanel", 1, true))
|
||||||
|
local finish = assert(src:find("\nlocal function ", start + 1, true))
|
||||||
|
local panel = src:sub(start, finish - 1)
|
||||||
|
check(not panel:find("imp:_ensureMods()", 1, true),
|
||||||
|
"MOD INDEX panel does not force the full MODS list")
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("launcher_navigation_perf")
|
||||||
@@ -110,6 +110,61 @@ check(not silverSet["assets/generated/trade/game_boy.png"],
|
|||||||
check(CacheContract.VERSION_REQUIRED_FILES.yellow ~= nil,
|
check(CacheContract.VERSION_REQUIRED_FILES.yellow ~= nil,
|
||||||
"Yellow has version-specific required outputs")
|
"Yellow has version-specific required outputs")
|
||||||
|
|
||||||
|
-- Revisioned cache markers: Crystal accepts either the 1.0 or the 1.1 cart.
|
||||||
|
local CRYSTAL_1_0 = "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133"
|
||||||
|
local CRYSTAL_1_1 = "f2f52230b536214ef7c9924f483392993e226cfb"
|
||||||
|
|
||||||
|
local CRYSTAL_FORMAT = CacheContract.formatFor("crystal")
|
||||||
|
|
||||||
|
eq(CacheContract.markerFor("crystal", CRYSTAL_1_1),
|
||||||
|
CRYSTAL_FORMAT .. CRYSTAL_1_1,
|
||||||
|
"markerFor with an explicit sha1 uses that sha1, not the canonical one")
|
||||||
|
eq(CacheContract.markerFor("crystal"), CRYSTAL_FORMAT .. CRYSTAL_1_0,
|
||||||
|
"markerFor with no sha1 still defaults to the canonical (1.0) sha1")
|
||||||
|
|
||||||
|
check(CacheContract.markerMatches("crystal", CRYSTAL_FORMAT .. CRYSTAL_1_0),
|
||||||
|
"a marker written from the 1.0 hash matches crystal")
|
||||||
|
check(CacheContract.markerMatches("crystal", CRYSTAL_FORMAT .. CRYSTAL_1_1),
|
||||||
|
"a marker written from the 1.1 hash also matches crystal")
|
||||||
|
check(not CacheContract.markerMatches("crystal",
|
||||||
|
CRYSTAL_FORMAT .. "ea9bcae617fdf159b045185467ae58b2e4a48b9a"),
|
||||||
|
"a marker written from Red's hash does not match crystal")
|
||||||
|
check(not CacheContract.markerMatches("red", CRYSTAL_FORMAT .. CRYSTAL_1_1),
|
||||||
|
"a marker written from Crystal's 1.1 hash does not match red")
|
||||||
|
check(not CacheContract.markerMatches("crystal",
|
||||||
|
CacheContract.FORMAT .. CRYSTAL_1_0),
|
||||||
|
"a crystal marker written on the shared format no longer matches")
|
||||||
|
|
||||||
|
local crystalFs = { prefix = "crystal-marker-test/", files = {} }
|
||||||
|
function crystalFs.exists(path) return crystalFs.files[crystalFs.prefix .. path] ~= nil end
|
||||||
|
function crystalFs.read(path) return crystalFs.files[crystalFs.prefix .. path] end
|
||||||
|
function crystalFs.write(path, value)
|
||||||
|
crystalFs.files[crystalFs.prefix .. path] = value
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
function crystalFs.remove(path) crystalFs.files[crystalFs.prefix .. path] = nil end
|
||||||
|
|
||||||
|
local crystalRequired = CacheContract.requiredFilesFor("crystal")
|
||||||
|
for _, path in ipairs(crystalRequired) do
|
||||||
|
crystalFs.files["crystal/" .. path] = true
|
||||||
|
end
|
||||||
|
local published11 = CacheContract.publish("crystal", crystalFs, CRYSTAL_1_1)
|
||||||
|
check(published11, "publishing with the 1.1 sha1 succeeds")
|
||||||
|
eq(crystalFs.files["crystal/" .. CacheContract.MARKER_PATH],
|
||||||
|
CRYSTAL_FORMAT .. CRYSTAL_1_1, "the marker records the 1.1 sha1")
|
||||||
|
check(CacheContract.isReady("crystal", crystalFs),
|
||||||
|
"a cache published with the 1.1 sha1 still reads ready for crystal")
|
||||||
|
|
||||||
|
crystalFs.files["crystal/" .. CacheContract.MARKER_PATH] =
|
||||||
|
CacheContract.FORMAT .. "ea9bcae617fdf159b045185467ae58b2e4a48b9a"
|
||||||
|
check(not CacheContract.isReady("crystal", crystalFs),
|
||||||
|
"a marker from another version's hash does not read ready for crystal")
|
||||||
|
|
||||||
|
check(CacheContract.markerMatches("blue", CacheContract.markerFor("blue")),
|
||||||
|
"blue's own marker still matches blue")
|
||||||
|
check(not CacheContract.markerMatches("blue", CacheContract.markerFor("red")),
|
||||||
|
"red's marker still does not match blue")
|
||||||
|
|
||||||
-- A throwing adapter must not strand the process in its temporary prefix.
|
-- A throwing adapter must not strand the process in its temporary prefix.
|
||||||
local throwingFs = { prefix = "before/" }
|
local throwingFs = { prefix = "before/" }
|
||||||
function throwingFs.exists() error("probe failed") end
|
function throwingFs.exists() error("probe failed") end
|
||||||
|
|||||||
@@ -194,6 +194,21 @@ if not cache then
|
|||||||
cache = home .. "/Library/Application Support/LOVE/crystal-dev/crystal"
|
cache = home .. "/Library/Application Support/LOVE/crystal-dev/crystal"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
local CacheContract = require("src.import.CacheContract")
|
||||||
|
|
||||||
|
local function crystalCacheRevision()
|
||||||
|
local fs = { prefix = "", read = function(rel) return readFile(cache .. "/" .. rel) end }
|
||||||
|
local marker = CacheContract.readMarker("crystal", fs)
|
||||||
|
if not marker then return "1.0" end
|
||||||
|
for _, revision in ipairs(GameVersion.revisions("crystal")) do
|
||||||
|
if marker == CacheContract.markerFor("crystal", revision.sha1) then
|
||||||
|
return revision.label or "1.0"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return "1.0"
|
||||||
|
end
|
||||||
|
|
||||||
local function loadCache(rel)
|
local function loadCache(rel)
|
||||||
local chunk = loadfile(cache .. "/data/generated/" .. rel .. ".lua")
|
local chunk = loadfile(cache .. "/data/generated/" .. rel .. ".lua")
|
||||||
if not chunk then return nil end
|
if not chunk then return nil end
|
||||||
@@ -338,7 +353,7 @@ local MAPS = {
|
|||||||
{ "centerAttrmap", "mobile_center_attrmap", 360,
|
{ "centerAttrmap", "mobile_center_attrmap", 360,
|
||||||
"gfx/mobile/mobile_center.attrmap" },
|
"gfx/mobile/mobile_center.attrmap" },
|
||||||
{ "stadium2N64Tilemap", "stadium2_n64_tilemap", 360,
|
{ "stadium2N64Tilemap", "stadium2_n64_tilemap", 360,
|
||||||
"gfx/mobile/stadium2_n64.tilemap" },
|
"revision" },
|
||||||
{ "stadium2N64Attrmap", "stadium2_n64_attrmap", 360,
|
{ "stadium2N64Attrmap", "stadium2_n64_attrmap", 360,
|
||||||
"gfx/mobile/stadium2_n64.attrmap" },
|
"gfx/mobile/stadium2_n64.attrmap" },
|
||||||
{ "dialpadTilemap", "dialpad_tilemap", 360, "gfx/mobile/dialpad.tilemap" },
|
{ "dialpadTilemap", "dialpad_tilemap", 360, "gfx/mobile/dialpad.tilemap" },
|
||||||
@@ -408,6 +423,14 @@ else
|
|||||||
|
|
||||||
for _, row in ipairs(MAPS) do
|
for _, row in ipairs(MAPS) do
|
||||||
local key, file, bytes, source = row[1], row[2], row[3], row[4]
|
local key, file, bytes, source = row[1], row[2], row[3], row[4]
|
||||||
|
local revisionLabel
|
||||||
|
if key == "stadium2N64Tilemap" then
|
||||||
|
-- ../pokecrystal/mobile/mobile_5c.asm:869
|
||||||
|
revisionLabel = crystalCacheRevision()
|
||||||
|
source = revisionLabel == "1.1"
|
||||||
|
and "gfx/mobile/stadium2_n64_corrupt.tilemap"
|
||||||
|
or "gfx/mobile/stadium2_n64.tilemap"
|
||||||
|
end
|
||||||
local entry = maps[key]
|
local entry = maps[key]
|
||||||
local rel = "assets/generated/mobile/" .. file .. ".bin"
|
local rel = "assets/generated/mobile/" .. file .. ".bin"
|
||||||
if type(entry) ~= "table" then
|
if type(entry) ~= "table" then
|
||||||
@@ -425,7 +448,10 @@ else
|
|||||||
check(true, "no ../pokecrystal: " .. source .. " not diffed (SKIP)")
|
check(true, "no ../pokecrystal: " .. source .. " not diffed (SKIP)")
|
||||||
else
|
else
|
||||||
check(blob == want:sub(1, bytes),
|
check(blob == want:sub(1, bytes),
|
||||||
"it is ../pokecrystal/" .. source .. " byte for byte")
|
"it is ../pokecrystal/" .. source .. " byte for byte"
|
||||||
|
.. (revisionLabel
|
||||||
|
and (" (cache built from Crystal " .. revisionLabel .. ")")
|
||||||
|
or ""))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -91,6 +91,35 @@ do
|
|||||||
:format(extra))
|
:format(extra))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ---- the gendered player name ---------------------------------------------
|
||||||
|
-- ../pokecrystal/constants/charmap.asm:6 <PLAY_G> ($14),
|
||||||
|
-- ../pokecrystal/home/text.asm:243,380 PlaceGenderedPlayerName
|
||||||
|
do
|
||||||
|
local Extractor = require("src.import.RomExtractorGen2")
|
||||||
|
local charmap = manifest("tools/rom_manifest_crystal.json").charmap
|
||||||
|
|
||||||
|
local function decode(edition, bytes)
|
||||||
|
local rom = {}
|
||||||
|
function rom:byte(_, address) return bytes[address] or 0x50 end
|
||||||
|
function rom:word(_, address)
|
||||||
|
return (bytes[address] or 0) + (bytes[address + 1] or 0) * 0x100
|
||||||
|
end
|
||||||
|
local extractor = setmetatable({ rom = rom, edition = edition }, Extractor)
|
||||||
|
return extractor:decodeGen2Text(0, 0, charmap)
|
||||||
|
end
|
||||||
|
|
||||||
|
local H, i, comma, space, bang = 0x87, 0xa8, 0xf4, 0x7f, 0xe7
|
||||||
|
local inString = { [0] = 0x00, H, i, comma, space, 0x14, bang, 0x57 }
|
||||||
|
eq(decode("crystal", inString), "Hi, {PLAYER}!",
|
||||||
|
"an in-string $14 decodes as the player's name")
|
||||||
|
eq(decode("gold", inString), "Hi, !",
|
||||||
|
"and only on Crystal, which is the only edition that writes one")
|
||||||
|
|
||||||
|
local command = { [0] = 0x14, 0x03, 0x00, H, i, 0x57 }
|
||||||
|
eq(decode("crystal", command), "{STRBUF}Hi",
|
||||||
|
"a $14 outside a string is still TX_STRINGBUFFER and eats its buffer id")
|
||||||
|
end
|
||||||
|
|
||||||
-- ---- the slots RomText fills ----------------------------------------------
|
-- ---- the slots RomText fills ----------------------------------------------
|
||||||
-- decodeGen2Text emits {USER}, {TARGET} and {ENEMY} for the three names
|
-- decodeGen2Text emits {USER}, {TARGET} and {ENEMY} for the three names
|
||||||
-- PlaceMoveUsersName / PlaceMoveTargetsName / PlaceEnemysName write at
|
-- PlaceMoveUsersName / PlaceMoveTargetsName / PlaceEnemysName write at
|
||||||
|
|||||||
+17
-4
@@ -1582,9 +1582,17 @@ def t_png():
|
|||||||
assert label_art("#123456") == art
|
assert label_art("#123456") == art
|
||||||
|
|
||||||
|
|
||||||
|
class SelftestSkip(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def t_roundtrip():
|
def t_roundtrip():
|
||||||
root = tempfile.mkdtemp(prefix="cartkit-selftest-")
|
root = tempfile.mkdtemp(prefix="cartkit-selftest-")
|
||||||
repo = find_repo(os.path.dirname(os.path.abspath(__file__)))
|
repo = find_repo(os.getcwd()) or find_repo(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if not repo:
|
||||||
|
raise SelftestSkip("scaffold needs an engine checkout; none found "
|
||||||
|
"from the cwd or the script")
|
||||||
try:
|
try:
|
||||||
with contextlib.redirect_stdout(io.StringIO()):
|
with contextlib.redirect_stdout(io.StringIO()):
|
||||||
_roundtrip(root, repo)
|
_roundtrip(root, repo)
|
||||||
@@ -1650,21 +1658,26 @@ CHECKS = [
|
|||||||
|
|
||||||
def cmd_selftest(args, repo):
|
def cmd_selftest(args, repo):
|
||||||
failures = []
|
failures = []
|
||||||
|
skipped = []
|
||||||
for name, check in CHECKS:
|
for name, check in CHECKS:
|
||||||
try:
|
try:
|
||||||
check()
|
check()
|
||||||
|
except SelftestSkip as why:
|
||||||
|
skipped.append((name, why))
|
||||||
|
print(f"skip {name}: {why}")
|
||||||
except Exception as problem:
|
except Exception as problem:
|
||||||
failures.append((name, problem))
|
failures.append((name, problem))
|
||||||
if not args.quiet:
|
print(f"FAIL {name}: {problem!r}")
|
||||||
print(f"FAIL {name}: {problem!r}")
|
|
||||||
else:
|
else:
|
||||||
if not args.quiet:
|
if not args.quiet:
|
||||||
print(f"ok {name}")
|
print(f"ok {name}")
|
||||||
if failures:
|
if failures:
|
||||||
print(f"FAIL {len(failures)} of {len(CHECKS)} checks")
|
print(f"FAIL {len(failures)} of {len(CHECKS)} checks")
|
||||||
return 1
|
return 1
|
||||||
|
ran = len(CHECKS) - len(skipped)
|
||||||
|
tail = f" ({len(skipped)} skipped)" if skipped else ""
|
||||||
if not args.quiet:
|
if not args.quiet:
|
||||||
print(f"ok {len(CHECKS)} checks")
|
print(f"ok {ran} checks{tail}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,19 @@ where `charmap` and `fontCharmap` already live, so a consumer picks the map it
|
|||||||
wants by name and no reader of the main table can see these bytes. The `ascii`
|
wants by name and no reader of the main table can see these bytes. The `ascii`
|
||||||
map is not emitted, because nothing outside the Mobile System reads it.
|
map is not emitted, because nothing outside the Mobile System reads it.
|
||||||
|
|
||||||
|
The manifest also carries `symbolRevisions`, a map of ROM sha1 -> symbol name
|
||||||
|
-> [bank, address] for the retail revisions this port accepts besides the 1.0
|
||||||
|
hash in `romSha1`. Crystal has one, v1.1, where `Stadium2N64Attrmap` sits 13
|
||||||
|
bytes later than on 1.0 because the Stadium 2 tilemap in front of it is longer
|
||||||
|
(../pokecrystal/mobile/mobile_5c.asm:869). `crystal11_symbol_revisions` diffs
|
||||||
|
the v1.1 symbol table against the resolved 1.0 `symbols` and keeps only the
|
||||||
|
names that appear in both and moved.
|
||||||
|
|
||||||
Usage: python3 tools/make_crystal_manifest.py
|
Usage: python3 tools/make_crystal_manifest.py
|
||||||
Default paths: pokecrystal at ../pokecrystal (relative to the repo) or
|
Default paths: pokecrystal at ../pokecrystal (relative to the repo) or
|
||||||
/Users/bryanbassett/Documents/development/pokecrystal; symbols at
|
/Users/bryanbassett/Documents/development/pokecrystal; symbols at
|
||||||
/Users/bryanbassett/Documents/development/pokecrystal-symbols/pokecrystal.sym.
|
/Users/bryanbassett/Documents/development/pokecrystal-symbols/pokecrystal.sym
|
||||||
|
and /Users/bryanbassett/Documents/development/pokecrystal-symbols/pokecrystal11.sym.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -49,7 +58,9 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||||||
|
|
||||||
import make_gold_manifest as gold # noqa: E402
|
import make_gold_manifest as gold # noqa: E402
|
||||||
from crystal_symbol_deltas import crystal_required # noqa: E402
|
from crystal_symbol_deltas import crystal_required # noqa: E402
|
||||||
from rom_data import CANONICAL_CRYSTAL_SHA1 # noqa: E402
|
from rom_data import ( # noqa: E402
|
||||||
|
CANONICAL_CRYSTAL_SHA1, CANONICAL_CRYSTAL11_SHA1, SymbolTable,
|
||||||
|
)
|
||||||
|
|
||||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
DEV = "/Users/bryanbassett/Documents/development"
|
DEV = "/Users/bryanbassett/Documents/development"
|
||||||
@@ -58,6 +69,8 @@ DEFAULT_POKECRYSTAL_CANDIDATES = [
|
|||||||
os.path.join(DEV, "pokecrystal"),
|
os.path.join(DEV, "pokecrystal"),
|
||||||
]
|
]
|
||||||
DEFAULT_SYMBOLS = os.path.join(DEV, "pokecrystal-symbols/pokecrystal.sym")
|
DEFAULT_SYMBOLS = os.path.join(DEV, "pokecrystal-symbols/pokecrystal.sym")
|
||||||
|
DEFAULT_SYMBOLS11 = os.path.join(
|
||||||
|
DEV, "pokecrystal-symbols/pokecrystal11.sym")
|
||||||
DEFAULT_OUT = os.path.join(
|
DEFAULT_OUT = os.path.join(
|
||||||
os.path.dirname(__file__), "rom_manifest_crystal.json")
|
os.path.dirname(__file__), "rom_manifest_crystal.json")
|
||||||
|
|
||||||
@@ -126,7 +139,31 @@ def unown_charmap(pokecrystal, defines=None):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def generate(pokecrystal, symbols_path):
|
def crystal11_symbol_revisions(symbols11_path, base_symbols):
|
||||||
|
"""Diff the v1.1 symbol table against the manifest's resolved 1.0 symbols.
|
||||||
|
|
||||||
|
Returns only the entries whose [bank, address] differs, restricted to
|
||||||
|
names the 1.0 manifest actually carries -- a v1.1-only symbol with no
|
||||||
|
1.0 counterpart is nothing an extractor built against `symbols` could
|
||||||
|
ever look up, so it is not this table's business to report.
|
||||||
|
"""
|
||||||
|
if not os.path.isfile(symbols11_path):
|
||||||
|
raise SystemExit(
|
||||||
|
f"Crystal v1.1 symbol file not found: {symbols11_path} "
|
||||||
|
"(pass --symbols11 or install it at the default path)")
|
||||||
|
symbols11 = SymbolTable(symbols11_path)
|
||||||
|
revisions = {}
|
||||||
|
for name, location in base_symbols.items():
|
||||||
|
symbol11 = symbols11.by_name.get(name)
|
||||||
|
if symbol11 is None:
|
||||||
|
continue
|
||||||
|
location11 = [symbol11.bank, symbol11.address]
|
||||||
|
if location11 != location:
|
||||||
|
revisions[name] = location11
|
||||||
|
return revisions
|
||||||
|
|
||||||
|
|
||||||
|
def generate(pokecrystal, symbols_path, symbols11_path=DEFAULT_SYMBOLS11):
|
||||||
data = gold.generate(
|
data = gold.generate(
|
||||||
pokecrystal, symbols_path,
|
pokecrystal, symbols_path,
|
||||||
defines=CRYSTAL_ASM_DEFINES,
|
defines=CRYSTAL_ASM_DEFINES,
|
||||||
@@ -139,6 +176,10 @@ def generate(pokecrystal, symbols_path):
|
|||||||
os.path.join(pokecrystal, "constants", "engine_flags.asm"),
|
os.path.join(pokecrystal, "constants", "engine_flags.asm"),
|
||||||
defines=CRYSTAL_ASM_DEFINES)
|
defines=CRYSTAL_ASM_DEFINES)
|
||||||
data["unownCharmap"] = unown_charmap(pokecrystal, CRYSTAL_ASM_DEFINES)
|
data["unownCharmap"] = unown_charmap(pokecrystal, CRYSTAL_ASM_DEFINES)
|
||||||
|
data["symbolRevisions"] = {
|
||||||
|
CANONICAL_CRYSTAL11_SHA1: crystal11_symbol_revisions(
|
||||||
|
symbols11_path, data["symbols"]),
|
||||||
|
}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -153,13 +194,16 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--pokecrystal", default=find_pokecrystal())
|
parser.add_argument("--pokecrystal", default=find_pokecrystal())
|
||||||
parser.add_argument("--symbols", default=DEFAULT_SYMBOLS)
|
parser.add_argument("--symbols", default=DEFAULT_SYMBOLS)
|
||||||
|
parser.add_argument("--symbols11", default=DEFAULT_SYMBOLS11)
|
||||||
parser.add_argument("--out", default=DEFAULT_OUT)
|
parser.add_argument("--out", default=DEFAULT_OUT)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
pokecrystal = os.path.abspath(args.pokecrystal)
|
pokecrystal = os.path.abspath(args.pokecrystal)
|
||||||
if not os.path.isfile(os.path.join(pokecrystal, "main.asm")):
|
if not os.path.isfile(os.path.join(pokecrystal, "main.asm")):
|
||||||
raise SystemExit(f"{pokecrystal} is not a pokecrystal checkout")
|
raise SystemExit(f"{pokecrystal} is not a pokecrystal checkout")
|
||||||
data = generate(pokecrystal, os.path.abspath(args.symbols))
|
data = generate(
|
||||||
|
pokecrystal, os.path.abspath(args.symbols),
|
||||||
|
os.path.abspath(args.symbols11))
|
||||||
with open(args.out, "w", encoding="utf-8", newline="\n") as f:
|
with open(args.out, "w", encoding="utf-8", newline="\n") as f:
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2, sort_keys=True)
|
json.dump(data, f, ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|||||||
@@ -778,6 +778,9 @@ REQUIRED_SYMBOLS = {
|
|||||||
# nine fill cells. "HP:" and the ten HP-bar cells come from
|
# nine fill cells. "HP:" and the ten HP-bar cells come from
|
||||||
# FontBattleExtra, which is already extracted.
|
# FontBattleExtra, which is already extracted.
|
||||||
"EnemyHPBarBorderGFX", "HPExpBarBorderGFX", "ExpBarGFX",
|
"EnemyHPBarBorderGFX", "HPExpBarBorderGFX", "ExpBarGFX",
|
||||||
|
# gfx/battle/balls.2bpp, the four party-ball OAM tiles
|
||||||
|
# (engine/battle/trainer_huds.asm LoadBallIconGFX).
|
||||||
|
"LoadBallIconGFX.gfx",
|
||||||
# gfx/stats/stats_tiles.png + gfx/stats/pages.pal, StatsScreen_LoadFont
|
# gfx/stats/stats_tiles.png + gfx/stats/pages.pal, StatsScreen_LoadFont
|
||||||
# and _CGB_StatsScreenHPPals (#1558)
|
# and _CGB_StatsScreenHPPals (#1558)
|
||||||
"StatsScreenPageTilesGFX", "StatsScreenPagePals",
|
"StatsScreenPageTilesGFX", "StatsScreenPagePals",
|
||||||
|
|||||||
+11
-4
@@ -14,9 +14,9 @@ CANONICAL_YELLOW_SHA1 = "cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1"
|
|||||||
# Gold and Silver are Gen 2: 2 MiB carts, twice the size of the Gen 1 ROMs above.
|
# Gold and Silver are Gen 2: 2 MiB carts, twice the size of the Gen 1 ROMs above.
|
||||||
CANONICAL_GOLD_SHA1 = "d8b8a3600a465308c9953dfa04f0081c05bdcb94"
|
CANONICAL_GOLD_SHA1 = "d8b8a3600a465308c9953dfa04f0081c05bdcb94"
|
||||||
CANONICAL_SILVER_SHA1 = "49b163f7e57702bc939d642a18f591de55d92dae"
|
CANONICAL_SILVER_SHA1 = "49b163f7e57702bc939d642a18f591de55d92dae"
|
||||||
# Crystal is the retail international v1.0 build (pret/pokecrystal's default
|
# Crystal is the retail international v1.0 cart, also 2 MiB.
|
||||||
# `make` target), also 2 MiB.
|
|
||||||
CANONICAL_CRYSTAL_SHA1 = "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133"
|
CANONICAL_CRYSTAL_SHA1 = "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133"
|
||||||
|
CANONICAL_CRYSTAL11_SHA1 = "f2f52230b536214ef7c9924f483392993e226cfb"
|
||||||
ROM_BANK_SIZE = 0x4000
|
ROM_BANK_SIZE = 0x4000
|
||||||
|
|
||||||
|
|
||||||
@@ -74,9 +74,16 @@ class RomImage:
|
|||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
self.data = f.read()
|
self.data = f.read()
|
||||||
self.sha1 = hashlib.sha1(self.data).hexdigest()
|
self.sha1 = hashlib.sha1(self.data).hexdigest()
|
||||||
if expected_sha1 and self.sha1 != expected_sha1:
|
if isinstance(expected_sha1, (tuple, set, frozenset, list)):
|
||||||
|
allowed = expected_sha1
|
||||||
|
elif expected_sha1:
|
||||||
|
allowed = (expected_sha1,)
|
||||||
|
else:
|
||||||
|
allowed = None
|
||||||
|
if allowed and self.sha1 not in allowed:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"unsupported ROM SHA-1 {self.sha1}; expected {expected_sha1}")
|
f"unsupported ROM SHA-1 {self.sha1}; "
|
||||||
|
f"expected {' or '.join(allowed)}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def offset(bank, address):
|
def offset(bank, address):
|
||||||
|
|||||||
@@ -14279,6 +14279,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"romSha1": "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133",
|
"romSha1": "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133",
|
||||||
|
"symbolRevisions": {
|
||||||
|
"f2f52230b536214ef7c9924f483392993e226cfb": {
|
||||||
|
"Stadium2N64Attrmap": [
|
||||||
|
92,
|
||||||
|
29988
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"symbols": {
|
"symbols": {
|
||||||
"AbraBackpic": [
|
"AbraBackpic": [
|
||||||
86,
|
86,
|
||||||
@@ -17016,6 +17024,10 @@
|
|||||||
32,
|
32,
|
||||||
21684
|
21684
|
||||||
],
|
],
|
||||||
|
"LoadBallIconGFX.gfx": [
|
||||||
|
11,
|
||||||
|
16754
|
||||||
|
],
|
||||||
"LoadOpponentTrainerAndPokemon.resample": [
|
"LoadOpponentTrainerAndPokemon.resample": [
|
||||||
126,
|
126,
|
||||||
16418
|
16418
|
||||||
|
|||||||
@@ -15084,6 +15084,10 @@
|
|||||||
64,
|
64,
|
||||||
24169
|
24169
|
||||||
],
|
],
|
||||||
|
"LoadBallIconGFX.gfx": [
|
||||||
|
11,
|
||||||
|
16804
|
||||||
|
],
|
||||||
"LoadOrientedFrontpic": [
|
"LoadOrientedFrontpic": [
|
||||||
20,
|
20,
|
||||||
22948
|
22948
|
||||||
|
|||||||
@@ -15084,6 +15084,10 @@
|
|||||||
64,
|
64,
|
||||||
24169
|
24169
|
||||||
],
|
],
|
||||||
|
"LoadBallIconGFX.gfx": [
|
||||||
|
11,
|
||||||
|
16804
|
||||||
|
],
|
||||||
"LoadOrientedFrontpic": [
|
"LoadOrientedFrontpic": [
|
||||||
20,
|
20,
|
||||||
22948
|
22948
|
||||||
|
|||||||
Reference in New Issue
Block a user