chris 1.1 hotfix

This commit is contained in:
bryanthaboi
2026-08-24 11:32:42 -04:00
parent 041051e154
commit bed1062a5f
14 changed files with 280 additions and 28 deletions
+2 -1
View File
@@ -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
+25 -2
View File
@@ -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
+12 -5
View File
@@ -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
+2 -2
View File
@@ -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)
+11 -2
View File
@@ -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,
+5 -4
View File
@@ -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))
+2 -1
View File
@@ -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
+30
View File
@@ -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")
+46 -1
View File
@@ -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()
+50
View File
@@ -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
+28 -2
View File
@@ -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
+48 -4
View File
@@ -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")
+11 -4
View File
@@ -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):
+8
View File
@@ -14279,6 +14279,14 @@
}
},
"romSha1": "f4cd194bdee0d04ca4eac29e09b8e4e9d818c133",
"symbolRevisions": {
"f2f52230b536214ef7c9924f483392993e226cfb": {
"Stadium2N64Attrmap": [
92,
29988
]
}
},
"symbols": {
"AbraBackpic": [
86,