diff --git a/README.md b/README.md index 897a2874..141f35d4 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,8 @@ game data: - Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1` - Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94` - 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, sound effects, and cries are synthesized while the game runs from compact diff --git a/src/core/GameVersion.lua b/src/core/GameVersion.lua index 3e39ced9..d254a46d 100644 --- a/src/core/GameVersion.lua +++ b/src/core/GameVersion.lua @@ -96,6 +96,10 @@ GameVersion.VERSIONS = { saveSuffix = "_crystal", -- save_crystal.lua / .bak / .tmp generation = 2, engine = "crystal", + revisions = { + { sha1 = "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133", label = "1.0" }, + { sha1 = "f2f52230b536214ef7c9924f483392993e226cfb", label = "1.1" }, + }, fixes = { -- pokegold/docs/bugs_and_glitches.md:61 luckyNumberBoxes = true, @@ -168,10 +172,29 @@ function GameVersion.cachePrefix(id) return GameVersion.info(id).cachePrefix 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. function GameVersion.forSha1(sha1) - for id, info in pairs(GameVersion.VERSIONS) do - if info.sha1 == sha1 then return id end + for id in pairs(GameVersion.VERSIONS) do + if GameVersion.acceptsSha1(id, sha1) then return id end end return nil end diff --git a/src/import/CacheContract.lua b/src/import/CacheContract.lua index dd4e8503..3ae48d21 100644 --- a/src/import/CacheContract.lua +++ b/src/import/CacheContract.lua @@ -136,8 +136,15 @@ function CacheContract.requiredFilesFor(version) return CacheContract.REQUIRED_FILES, false end -function CacheContract.markerFor(version) - return CacheContract.FORMAT .. GameVersion.info(version).sha1 +function CacheContract.markerFor(version, sha1) + return CacheContract.FORMAT .. (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 -- Keep the process-global CacheFs prefix isolated even when a filesystem @@ -189,11 +196,11 @@ function CacheContract.isReady(version, fs) fs = fs or require("src.import.CacheFs") if CacheContract.sourceTreeHasData(version) then return true end 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) end -function CacheContract.publish(version, fs) +function CacheContract.publish(version, fs, sha1) fs = fs or require("src.import.CacheFs") local complete, missing = CacheContract.allRequiredFilesExist(version, fs) if not complete then @@ -212,7 +219,7 @@ function CacheContract.publish(version, fs) return false, "cache is incomplete; missing " .. tostring(missing) end 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) if not changed then return false, tostring(ok) end return ok, err diff --git a/src/import/ExtractThread.lua b/src/import/ExtractThread.lua index b1bf99b2..e4631eb4 100644 --- a/src/import/ExtractThread.lua +++ b/src/import/ExtractThread.lua @@ -13,7 +13,7 @@ require("love.math") require("love.system") require("love.timer") -local version, prefix, romData, progressName, resultName = ... +local version, prefix, romData, progressName, resultName, romSha1 = ... local progressChannel = love.thread.getChannel(progressName) local resultChannel = love.thread.getChannel(resultName) @@ -44,7 +44,7 @@ local ok, err = pcall(function() current = current, stageTotal = stageTotal, }) end - end) + end, romSha1) extractor:run() end) diff --git a/src/import/RomExtractorGen2.lua b/src/import/RomExtractorGen2.lua index 9584a9b6..2e8aea2c 100644 --- a/src/import/RomExtractorGen2.lua +++ b/src/import/RomExtractorGen2.lua @@ -201,14 +201,23 @@ local function copy(value) return result 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 -- them is not (gfx/misc.asm:9-20 vs :46-57). 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({ rom = Rom.new(romData), manifest = manifest, - symbols = manifest.symbols, + symbols = symbols, progress = progress, stage = 0, edition = edition, diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index d0671ad7..f7c911e7 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -1432,7 +1432,7 @@ function RomImporter.new(onComplete, opts) -- "update required" (re-import) rather than a clean first-run choose local marker = CacheContract.readMarker(version, CacheFs) 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 .. ((info.id == "yellow" or GameVersion.generation(version) == 2) and ".gbc" or ".gb") @@ -1721,6 +1721,7 @@ function RomImporter:startData(data, displayName) .. "(tagged [b] or [BF]) never verify."):format(actualHash, cartsProse())) return end + self.romSha1 = actualHash local info = GameVersion.info(version) -- Bring the launcher to this version's tab so its progress bar is on screen @@ -1775,7 +1776,7 @@ function RomImporter:_startExtractThread(version, prefix, data, displayName) love.thread.getChannel(progressName):clear() love.thread.getChannel(resultName):clear() local started = pcall(thread.start, thread, version, prefix, data, - progressName, resultName) + progressName, resultName, self.romSha1) if not started then return false end self._extract = { thread = thread, version = version, prefix = prefix, @@ -1804,7 +1805,7 @@ function RomImporter:_startExtractCoroutine(version, info, prefix, displayName) self.stageCurrent = current self.stageTotal = stageTotal coroutine.yield() - end) + end, self.romSha1) extractor:run() CacheFs.prefix = "" -- restore the default so later writes stay at the root self.romData = nil @@ -1822,7 +1823,7 @@ function RomImporter:_completeImport(version, prefix, displayName) -- appear once every required file is in place. local savedPrefix = CacheFs.prefix CacheFs.prefix = prefix - local ok, writeError = CacheContract.publish(version, CacheFs) + local ok, writeError = CacheContract.publish(version, CacheFs, self.romSha1) CacheFs.prefix = savedPrefix if not ok then error("could not finish the private cache: " .. tostring(writeError)) diff --git a/src/import/RomManifest.lua b/src/import/RomManifest.lua index f1205635..c3d21b40 100644 --- a/src/import/RomManifest.lua +++ b/src/import/RomManifest.lua @@ -18,7 +18,8 @@ function RomManifest.decode(version) if not manifest then error("ROM import metadata is invalid: " .. tostring(decodeError)) end - assert(manifest.romSha1 == info.sha1, "ROM import metadata version mismatch") + assert(GameVersion.acceptsSha1(version, manifest.romSha1), + "ROM import metadata version mismatch") return manifest end diff --git a/tests/crystal_import_test.lua b/tests/crystal_import_test.lua index 00d82379..5d4dd3e7 100644 --- a/tests/crystal_import_test.lua +++ b/tests/crystal_import_test.lua @@ -37,6 +37,36 @@ eq(crystal.romSha1, GameVersion.VERSIONS.crystal.sha1, eq(crystal.generation, 2, "generation 2") 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 eq(size(crystal.maps), 388, "388 maps") diff --git a/tests/engine/crystal_version_test.lua b/tests/engine/crystal_version_test.lua index cddcacd2..8b2c8937 100644 --- a/tests/engine/crystal_version_test.lua +++ b/tests/engine/crystal_version_test.lua @@ -68,11 +68,56 @@ eq(GameVersion.ORDER[5], "silver", "silver keeps slot 5") -- ------- 4. sha1 routing 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", "Gold's sha1 still resolves to gold") 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 local savedCurrent = GameVersion.get() diff --git a/tests/engine/rom_cache_contract_test.lua b/tests/engine/rom_cache_contract_test.lua index f5f5102d..b8960b02 100644 --- a/tests/engine/rom_cache_contract_test.lua +++ b/tests/engine/rom_cache_contract_test.lua @@ -110,6 +110,56 @@ check(not silverSet["assets/generated/trade/game_boy.png"], check(CacheContract.VERSION_REQUIRED_FILES.yellow ~= nil, "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" + +eq(CacheContract.markerFor("crystal", CRYSTAL_1_1), + CacheContract.FORMAT .. CRYSTAL_1_1, + "markerFor with an explicit sha1 uses that sha1, not the canonical one") +eq(CacheContract.markerFor("crystal"), CacheContract.FORMAT .. CRYSTAL_1_0, + "markerFor with no sha1 still defaults to the canonical (1.0) sha1") + +check(CacheContract.markerMatches("crystal", CacheContract.FORMAT .. CRYSTAL_1_0), + "a marker written from the 1.0 hash matches crystal") +check(CacheContract.markerMatches("crystal", CacheContract.FORMAT .. CRYSTAL_1_1), + "a marker written from the 1.1 hash also matches crystal") +check(not CacheContract.markerMatches("crystal", + CacheContract.FORMAT .. "ea9bcae617fdf159b045185467ae58b2e4a48b9a"), + "a marker written from Red's hash does not match crystal") +check(not CacheContract.markerMatches("red", CacheContract.FORMAT .. CRYSTAL_1_1), + "a marker written from Crystal's 1.1 hash does not match red") + +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], + CacheContract.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. local throwingFs = { prefix = "before/" } function throwingFs.exists() error("probe failed") end diff --git a/tests/gen2_crystal_mobile_gfx_test.lua b/tests/gen2_crystal_mobile_gfx_test.lua index f4cf94d3..b0659512 100644 --- a/tests/gen2_crystal_mobile_gfx_test.lua +++ b/tests/gen2_crystal_mobile_gfx_test.lua @@ -194,6 +194,21 @@ if not cache then cache = home .. "/Library/Application Support/LOVE/crystal-dev/crystal" 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 chunk = loadfile(cache .. "/data/generated/" .. rel .. ".lua") if not chunk then return nil end @@ -338,7 +353,7 @@ local MAPS = { { "centerAttrmap", "mobile_center_attrmap", 360, "gfx/mobile/mobile_center.attrmap" }, { "stadium2N64Tilemap", "stadium2_n64_tilemap", 360, - "gfx/mobile/stadium2_n64.tilemap" }, + "revision" }, { "stadium2N64Attrmap", "stadium2_n64_attrmap", 360, "gfx/mobile/stadium2_n64.attrmap" }, { "dialpadTilemap", "dialpad_tilemap", 360, "gfx/mobile/dialpad.tilemap" }, @@ -408,6 +423,14 @@ else for _, row in ipairs(MAPS) do 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 rel = "assets/generated/mobile/" .. file .. ".bin" if type(entry) ~= "table" then @@ -425,7 +448,10 @@ else check(true, "no ../pokecrystal: " .. source .. " not diffed (SKIP)") else 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 diff --git a/tools/make_crystal_manifest.py b/tools/make_crystal_manifest.py index 033686a6..f79678cb 100644 --- a/tools/make_crystal_manifest.py +++ b/tools/make_crystal_manifest.py @@ -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` 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 Default paths: pokecrystal at ../pokecrystal (relative to the repo) or /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 @@ -49,7 +58,9 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import make_gold_manifest as gold # 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__))) DEV = "/Users/bryanbassett/Documents/development" @@ -58,6 +69,8 @@ DEFAULT_POKECRYSTAL_CANDIDATES = [ os.path.join(DEV, "pokecrystal"), ] 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( os.path.dirname(__file__), "rom_manifest_crystal.json") @@ -126,7 +139,31 @@ def unown_charmap(pokecrystal, defines=None): 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( pokecrystal, symbols_path, defines=CRYSTAL_ASM_DEFINES, @@ -139,6 +176,10 @@ def generate(pokecrystal, symbols_path): os.path.join(pokecrystal, "constants", "engine_flags.asm"), defines=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 @@ -153,13 +194,16 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--pokecrystal", default=find_pokecrystal()) parser.add_argument("--symbols", default=DEFAULT_SYMBOLS) + parser.add_argument("--symbols11", default=DEFAULT_SYMBOLS11) parser.add_argument("--out", default=DEFAULT_OUT) args = parser.parse_args() pokecrystal = os.path.abspath(args.pokecrystal) if not os.path.isfile(os.path.join(pokecrystal, "main.asm")): 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: json.dump(data, f, ensure_ascii=False, indent=2, sort_keys=True) f.write("\n") diff --git a/tools/rom_data.py b/tools/rom_data.py index bfb3d084..43d12e82 100644 --- a/tools/rom_data.py +++ b/tools/rom_data.py @@ -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. CANONICAL_GOLD_SHA1 = "d8b8a3600a465308c9953dfa04f0081c05bdcb94" CANONICAL_SILVER_SHA1 = "49b163f7e57702bc939d642a18f591de55d92dae" -# Crystal is the retail international v1.0 build (pret/pokecrystal's default -# `make` target), also 2 MiB. +# Crystal is the retail international v1.0 cart, also 2 MiB. CANONICAL_CRYSTAL_SHA1 = "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133" +CANONICAL_CRYSTAL11_SHA1 = "f2f52230b536214ef7c9924f483392993e226cfb" ROM_BANK_SIZE = 0x4000 @@ -74,9 +74,16 @@ class RomImage: with open(path, "rb") as f: self.data = f.read() 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( - f"unsupported ROM SHA-1 {self.sha1}; expected {expected_sha1}") + f"unsupported ROM SHA-1 {self.sha1}; " + f"expected {' or '.join(allowed)}") @staticmethod def offset(bank, address): diff --git a/tools/rom_manifest_crystal.json b/tools/rom_manifest_crystal.json index 260b93f7..b4e7546c 100644 --- a/tools/rom_manifest_crystal.json +++ b/tools/rom_manifest_crystal.json @@ -14279,6 +14279,14 @@ } }, "romSha1": "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133", + "symbolRevisions": { + "f2f52230b536214ef7c9924f483392993e226cfb": { + "Stadium2N64Attrmap": [ + 92, + 29988 + ] + } + }, "symbols": { "AbraBackpic": [ 86,