mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
chris 1.1 hotfix
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -136,8 +136,15 @@ function CacheContract.requiredFilesFor(version)
|
|||||||
return CacheContract.REQUIRED_FILES, false
|
return CacheContract.REQUIRED_FILES, false
|
||||||
end
|
end
|
||||||
|
|
||||||
function CacheContract.markerFor(version)
|
function CacheContract.markerFor(version, sha1)
|
||||||
return CacheContract.FORMAT .. GameVersion.info(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
|
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 +196,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 +219,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)
|
||||||
|
|
||||||
|
|||||||
@@ -201,14 +201,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,
|
||||||
|
|||||||
@@ -1432,7 +1432,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 +1721,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 +1776,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 +1805,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 +1823,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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -110,6 +110,56 @@ 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"
|
||||||
|
|
||||||
|
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.
|
-- 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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
+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,
|
||||||
|
|||||||
Reference in New Issue
Block a user