mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 03:02:39 +02:00
initial commit
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
local ImageWriter = {}
|
||||
|
||||
local SHADES = {
|
||||
{ 1, 1, 1, 1 },
|
||||
{ 2 / 3, 2 / 3, 2 / 3, 1 },
|
||||
{ 1 / 3, 1 / 3, 1 / 3, 1 },
|
||||
{ 0, 0, 0, 1 },
|
||||
}
|
||||
|
||||
local function assertDimensions(raw, width, height, bits)
|
||||
assert(width % 8 == 0 and height % 8 == 0,
|
||||
("%dbpp dimensions must be tile-aligned: %dx%d")
|
||||
:format(bits, width, height))
|
||||
local expected = width * height * bits / 8
|
||||
assert(#raw == expected,
|
||||
("%dbpp payload is %d bytes, expected %d")
|
||||
:format(bits, #raw, expected))
|
||||
end
|
||||
|
||||
function ImageWriter.decode2bpp(raw, width, height, transparent)
|
||||
assertDimensions(raw, width, height, 2)
|
||||
local image = love.image.newImageData(width, height)
|
||||
local tilesPerRow = width / 8
|
||||
for tile = 0, #raw / 16 - 1 do
|
||||
local tileX = tile % tilesPerRow * 8
|
||||
local tileY = math.floor(tile / tilesPerRow) * 8
|
||||
for y = 0, 7 do
|
||||
local low = raw[tile * 16 + y * 2 + 1]
|
||||
local high = raw[tile * 16 + y * 2 + 2]
|
||||
for x = 0, 7 do
|
||||
local divisor = 2 ^ (7 - x)
|
||||
local shade = math.floor(high / divisor) % 2 * 2
|
||||
+ math.floor(low / divisor) % 2
|
||||
local color = SHADES[shade + 1]
|
||||
local alpha = color[4]
|
||||
if transparent and shade == 0 then alpha = 0 end
|
||||
image:setPixel(tileX + x, tileY + y,
|
||||
color[1], color[2], color[3], alpha)
|
||||
end
|
||||
end
|
||||
end
|
||||
return image
|
||||
end
|
||||
|
||||
function ImageWriter.decode1bpp(raw, width, height, transparent)
|
||||
assertDimensions(raw, width, height, 1)
|
||||
local image = love.image.newImageData(width, height)
|
||||
local tilesPerRow = width / 8
|
||||
for tile = 0, #raw / 8 - 1 do
|
||||
local tileX = tile % tilesPerRow * 8
|
||||
local tileY = math.floor(tile / tilesPerRow) * 8
|
||||
for y = 0, 7 do
|
||||
local row = raw[tile * 8 + y + 1]
|
||||
for x = 0, 7 do
|
||||
local filled = math.floor(row / 2 ^ (7 - x)) % 2 ~= 0
|
||||
local value = filled and 0 or 1
|
||||
local alpha = 1
|
||||
if transparent and not filled then alpha = 0 end
|
||||
image:setPixel(tileX + x, tileY + y,
|
||||
value, value, value, alpha)
|
||||
end
|
||||
end
|
||||
end
|
||||
return image
|
||||
end
|
||||
|
||||
function ImageWriter.blank(width, height, r, g, b, a)
|
||||
local image = love.image.newImageData(width, height)
|
||||
image:mapPixel(function() return r or 0, g or 0, b or 0, a or 0 end)
|
||||
return image
|
||||
end
|
||||
|
||||
function ImageWriter.blit(target, source, targetX, targetY,
|
||||
sourceX, sourceY, width, height, flipX)
|
||||
sourceX, sourceY = sourceX or 0, sourceY or 0
|
||||
width, height = width or source:getWidth(), height or source:getHeight()
|
||||
for y = 0, height - 1 do
|
||||
for x = 0, width - 1 do
|
||||
local sampleX = flipX and sourceX + width - 1 - x or sourceX + x
|
||||
target:setPixel(targetX + x, targetY + y,
|
||||
source:getPixel(sampleX, sourceY + y))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ImageWriter.matteColor0(image)
|
||||
local width, height = image:getDimensions()
|
||||
local queueX, queueY, head = {}, {}, 1
|
||||
local seen = {}
|
||||
local function add(x, y)
|
||||
local key = y * width + x
|
||||
if seen[key] then return end
|
||||
local r, g, b, a = image:getPixel(x, y)
|
||||
if r == 1 and g == 1 and b == 1 and a == 1 then
|
||||
seen[key] = true
|
||||
queueX[#queueX + 1], queueY[#queueY + 1] = x, y
|
||||
end
|
||||
end
|
||||
for x = 0, width - 1 do add(x, 0); add(x, height - 1) end
|
||||
for y = 0, height - 1 do add(0, y); add(width - 1, y) end
|
||||
while head <= #queueX do
|
||||
local x, y = queueX[head], queueY[head]
|
||||
head = head + 1
|
||||
image:setPixel(x, y, 1, 1, 1, 0)
|
||||
if x > 0 then add(x - 1, y) end
|
||||
if x + 1 < width then add(x + 1, y) end
|
||||
if y > 0 then add(x, y - 1) end
|
||||
if y + 1 < height then add(x, y + 1) end
|
||||
end
|
||||
return image
|
||||
end
|
||||
|
||||
function ImageWriter.columnsToRows(raw, tilesWide, tilesHigh, bytesPerTile)
|
||||
bytesPerTile = bytesPerTile or 16
|
||||
local out = {}
|
||||
for y = 0, tilesHigh - 1 do
|
||||
for x = 0, tilesWide - 1 do
|
||||
local source = (x * tilesHigh + y) * bytesPerTile
|
||||
local target = (y * tilesWide + x) * bytesPerTile
|
||||
for offset = 1, bytesPerTile do
|
||||
out[target + offset] = raw[source + offset]
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function ImageWriter.save(image, path)
|
||||
local parent = path:match("^(.*)/[^/]+$")
|
||||
if parent then
|
||||
local ok, err = love.filesystem.createDirectory(parent)
|
||||
if not ok then error("could not create " .. parent .. ": " .. tostring(err)) end
|
||||
end
|
||||
local ok, fileData = pcall(image.encode, image, "png")
|
||||
if not ok then error("could not encode " .. path .. ": " .. tostring(fileData)) end
|
||||
local written, writeError = love.filesystem.write(path, fileData)
|
||||
if not written then
|
||||
error("could not write " .. path .. ": " .. tostring(writeError))
|
||||
end
|
||||
return fileData
|
||||
end
|
||||
|
||||
return ImageWriter
|
||||
@@ -0,0 +1,99 @@
|
||||
local LuaWriter = {}
|
||||
|
||||
local KEYWORDS = {
|
||||
["and"] = true, ["break"] = true, ["do"] = true, ["else"] = true,
|
||||
["elseif"] = true, ["end"] = true, ["false"] = true, ["for"] = true,
|
||||
["function"] = true, ["goto"] = true, ["if"] = true, ["in"] = true,
|
||||
["local"] = true, ["nil"] = true, ["not"] = true, ["or"] = true,
|
||||
["repeat"] = true, ["return"] = true, ["then"] = true, ["true"] = true,
|
||||
["until"] = true, ["while"] = true,
|
||||
}
|
||||
|
||||
local function quote(value)
|
||||
local escaped = value:gsub('[%z\1-\31\\"]', function(character)
|
||||
if character == "\\" then return "\\\\" end
|
||||
if character == '"' then return '\\"' end
|
||||
if character == "\n" then return "\\n" end
|
||||
if character == "\r" then return "\\r" end
|
||||
if character == "\t" then return "\\t" end
|
||||
return ("\\%03d"):format(character:byte())
|
||||
end)
|
||||
return '"' .. escaped .. '"'
|
||||
end
|
||||
|
||||
local function keyText(key)
|
||||
if type(key) == "string"
|
||||
and key:match("^[A-Za-z_][A-Za-z0-9_]*$")
|
||||
and not KEYWORDS[key] then
|
||||
return key
|
||||
end
|
||||
return "[" .. (type(key) == "string" and quote(key) or tostring(key)) .. "]"
|
||||
end
|
||||
|
||||
local function isArray(value)
|
||||
local count, maximum = 0, 0
|
||||
for key in pairs(value) do
|
||||
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
|
||||
return false, 0
|
||||
end
|
||||
count = count + 1
|
||||
maximum = math.max(maximum, key)
|
||||
end
|
||||
return count == maximum, maximum
|
||||
end
|
||||
|
||||
local function sortedKeys(value)
|
||||
local keys = {}
|
||||
for key in pairs(value) do keys[#keys + 1] = key end
|
||||
table.sort(keys, function(a, b)
|
||||
if type(a) == type(b) then return a < b end
|
||||
return type(a) == "number"
|
||||
end)
|
||||
return keys
|
||||
end
|
||||
|
||||
local function encode(value, indent, seen)
|
||||
local kind = type(value)
|
||||
if value == nil then return "nil" end
|
||||
if kind == "boolean" or kind == "number" then return tostring(value) end
|
||||
if kind == "string" then return quote(value) end
|
||||
if kind ~= "table" then
|
||||
error("cannot serialize " .. kind)
|
||||
end
|
||||
if seen[value] then error("cannot serialize a cyclic table") end
|
||||
seen[value] = true
|
||||
|
||||
local pad = string.rep(" ", indent)
|
||||
local childPad = string.rep(" ", indent + 1)
|
||||
local out = {}
|
||||
local array, length = isArray(value)
|
||||
if array then
|
||||
for index = 1, length do
|
||||
out[#out + 1] = childPad .. encode(value[index], indent + 1, seen) .. ","
|
||||
end
|
||||
else
|
||||
for _, key in ipairs(sortedKeys(value)) do
|
||||
out[#out + 1] = childPad .. keyText(key) .. " = "
|
||||
.. encode(value[key], indent + 1, seen) .. ","
|
||||
end
|
||||
end
|
||||
seen[value] = nil
|
||||
if #out == 0 then return "{}" end
|
||||
return "{\n" .. table.concat(out, "\n") .. "\n" .. pad .. "}"
|
||||
end
|
||||
|
||||
function LuaWriter.encode(value)
|
||||
return "return " .. encode(value, 0, {}) .. "\n"
|
||||
end
|
||||
|
||||
function LuaWriter.write(path, value)
|
||||
local parent = path:match("^(.*)/[^/]+$")
|
||||
if parent then
|
||||
local ok, err = love.filesystem.createDirectory(parent)
|
||||
if not ok then error("could not create " .. parent .. ": " .. tostring(err)) end
|
||||
end
|
||||
local ok, err = love.filesystem.write(path, LuaWriter.encode(value))
|
||||
if not ok then error("could not write " .. path .. ": " .. tostring(err)) end
|
||||
end
|
||||
|
||||
return LuaWriter
|
||||
@@ -0,0 +1,211 @@
|
||||
local Rom = {}
|
||||
Rom.__index = Rom
|
||||
|
||||
local BANK_SIZE = 0x4000
|
||||
|
||||
function Rom.new(data)
|
||||
assert(type(data) == "string", "ROM data must be a string")
|
||||
return setmetatable({ data = data }, Rom)
|
||||
end
|
||||
|
||||
function Rom.offset(bank, address)
|
||||
if bank == 0 then
|
||||
assert(address >= 0 and address < BANK_SIZE,
|
||||
("ROM0 address out of range: $%04X"):format(address))
|
||||
return address
|
||||
end
|
||||
assert(address >= BANK_SIZE and address < BANK_SIZE * 2,
|
||||
("bank %02X address out of range: $%04X"):format(bank, address))
|
||||
return bank * BANK_SIZE + address - BANK_SIZE
|
||||
end
|
||||
|
||||
function Rom:byte(bank, address)
|
||||
local value = self.data:byte(Rom.offset(bank, address) + 1)
|
||||
assert(value, ("ROM read past end at %02X:%04X"):format(bank, address))
|
||||
return value
|
||||
end
|
||||
|
||||
function Rom:word(bank, address)
|
||||
return self:byte(bank, address) + self:byte(bank, address + 1) * 0x100
|
||||
end
|
||||
|
||||
function Rom:bytes(bank, address, length)
|
||||
local first = Rom.offset(bank, address) + 1
|
||||
local last = first + length - 1
|
||||
assert(last <= #self.data,
|
||||
("ROM read past end at %02X:%04X + %d"):format(bank, address, length))
|
||||
local out = {}
|
||||
for index = 1, length do
|
||||
out[index] = self.data:byte(first + index - 1)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function Rom:decodeText(raw, charmap, stop)
|
||||
local out = {}
|
||||
stop = stop or 0x50
|
||||
for _, value in ipairs(raw) do
|
||||
if value == stop then break end
|
||||
out[#out + 1] = charmap[tostring(value)]
|
||||
or ("{BYTE:%02X}"):format(value)
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
function Rom:readString(bank, address, charmap, stop, maxLength)
|
||||
local out = {}
|
||||
stop = stop or 0x50
|
||||
maxLength = maxLength or 4096
|
||||
for offset = 0, maxLength - 1 do
|
||||
local value = self:byte(bank, address + offset)
|
||||
if value == stop then return table.concat(out), offset + 1 end
|
||||
out[#out + 1] = charmap[tostring(value)]
|
||||
or ("{BYTE:%02X}"):format(value)
|
||||
end
|
||||
error(("unterminated string at %02X:%04X"):format(bank, address))
|
||||
end
|
||||
|
||||
function Rom.bcd(raw)
|
||||
local value = 0
|
||||
for _, byte in ipairs(raw) do
|
||||
value = value * 100 + math.floor(byte / 16) * 10 + byte % 16
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local BitReader = {}
|
||||
BitReader.__index = BitReader
|
||||
|
||||
function BitReader.new(data)
|
||||
return setmetatable({ data = data, byte = 1, bit = 7 }, BitReader)
|
||||
end
|
||||
|
||||
function BitReader:read(count)
|
||||
local value = 0
|
||||
for _ = 1, count or 1 do
|
||||
local byte = self.data[self.byte]
|
||||
if not byte then error("compressed picture ended unexpectedly") end
|
||||
value = value * 2 + math.floor(byte / 2 ^ self.bit) % 2
|
||||
self.bit = self.bit - 1
|
||||
if self.bit < 0 then
|
||||
self.byte = self.byte + 1
|
||||
self.bit = 7
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function fillPicPlane(reader, width)
|
||||
local mode = reader:read()
|
||||
local groupCount = width * width * 0x20
|
||||
local groups = {}
|
||||
while #groups < groupCount do
|
||||
if mode ~= 0 then
|
||||
while #groups < groupCount do
|
||||
local group = reader:read(2)
|
||||
if group == 0 then break end
|
||||
groups[#groups + 1] = group
|
||||
end
|
||||
else
|
||||
local prefix = 0
|
||||
while reader:read() ~= 0 do
|
||||
prefix = prefix + 1
|
||||
if prefix >= 16 then error("invalid compressed picture zero run") end
|
||||
end
|
||||
local zeroCount = 2 ^ (prefix + 1) - 1 + reader:read(prefix + 1)
|
||||
for _ = 1, math.min(zeroCount, groupCount - #groups) do
|
||||
groups[#groups + 1] = 0
|
||||
end
|
||||
end
|
||||
mode = 1 - mode
|
||||
end
|
||||
|
||||
local reordered = {}
|
||||
for y = 0, width - 1 do
|
||||
for x = 0, width * 8 - 1 do
|
||||
for group = 0, 3 do
|
||||
local source = (y * 4 + group) * width * 8 + x
|
||||
reordered[#reordered + 1] = groups[source + 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
local packed = {}
|
||||
for index = 0, width * width * 8 - 1 do
|
||||
local start = index * 4
|
||||
packed[index + 1] = reordered[start + 1] * 0x40
|
||||
+ reordered[start + 2] * 0x10
|
||||
+ reordered[start + 3] * 4
|
||||
+ reordered[start + 4]
|
||||
end
|
||||
return packed
|
||||
end
|
||||
|
||||
local PIC_CODES = {
|
||||
{ 0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5,
|
||||
0xF, 0xE, 0xC, 0xD, 0x8, 0x9, 0xB, 0xA },
|
||||
{ 0xF, 0xE, 0xC, 0xD, 0x8, 0x9, 0xB, 0xA,
|
||||
0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5 },
|
||||
}
|
||||
|
||||
local function unfilterPicPlane(plane, width)
|
||||
for x = 0, width * 8 - 1 do
|
||||
local bit = 0
|
||||
for y = 0, width - 1 do
|
||||
local index = y * width * 8 + x + 1
|
||||
local high = PIC_CODES[bit + 1][math.floor(plane[index] / 16) + 1]
|
||||
bit = high % 2
|
||||
local low = PIC_CODES[bit + 1][plane[index] % 16 + 1]
|
||||
bit = low % 2
|
||||
plane[index] = high * 16 + low
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function transposePicTiles(data, width)
|
||||
local tileCount = width * width
|
||||
for index = 0, tileCount - 1 do
|
||||
local other = (index * width + math.floor(index / width)) % tileCount
|
||||
if index < other then
|
||||
for offset = 1, 16 do
|
||||
local left = index * 16 + offset
|
||||
local right = other * 16 + offset
|
||||
data[left], data[right] = data[right], data[left]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Rom.decompressPic(data)
|
||||
local reader = BitReader.new(data)
|
||||
local width, height = reader:read(4), reader:read(4)
|
||||
if width == 0 or width ~= height then
|
||||
error(("compressed picture is not a non-empty square (%dx%d)")
|
||||
:format(width, height))
|
||||
end
|
||||
|
||||
local order = reader:read()
|
||||
local planes = {}
|
||||
planes[order + 1] = fillPicPlane(reader, width)
|
||||
local mode = reader:read()
|
||||
if mode ~= 0 then mode = mode + reader:read() end
|
||||
planes[(1 - order) + 1] = fillPicPlane(reader, width)
|
||||
|
||||
unfilterPicPlane(planes[order + 1], width)
|
||||
if mode ~= 1 then unfilterPicPlane(planes[(1 - order) + 1], width) end
|
||||
if mode ~= 0 then
|
||||
for index = 1, width * width * 8 do
|
||||
planes[(1 - order) + 1][index] =
|
||||
bit.bxor(planes[(1 - order) + 1][index], planes[order + 1][index])
|
||||
end
|
||||
end
|
||||
|
||||
local output = {}
|
||||
for index = 1, width * width * 8 do
|
||||
output[#output + 1] = planes[1][index]
|
||||
output[#output + 1] = planes[2][index]
|
||||
end
|
||||
transposePicTiles(output, width)
|
||||
return output, width
|
||||
end
|
||||
|
||||
return Rom
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,406 @@
|
||||
local RomImporter = {}
|
||||
RomImporter.__index = RomImporter
|
||||
|
||||
local ROM_SHA1 = "ea9bcae617fdf159b045185467ae58b2e4a48b9a"
|
||||
local CACHE_MARKER = "rom-cache-v5:" .. ROM_SHA1
|
||||
local MARKER_PATH = "rom-cache.complete"
|
||||
local COMMUNITY_URL = "https://bois.icu"
|
||||
local TRUST_WARNING = "if you did not get this from bryanthaboi's github " ..
|
||||
"or a link from the discord that bryanthaboi himself posted, just know " ..
|
||||
"it might have been tampered with. go to the discord to verify " ..
|
||||
COMMUNITY_URL .. " (or click the logo above)"
|
||||
local REQUIRED_FILES = {
|
||||
"data/generated/constants.lua",
|
||||
"data/generated/maps.lua",
|
||||
"data/generated/text.lua",
|
||||
"data/generated/field.lua",
|
||||
"data/generated/battle_anims.lua",
|
||||
"assets/generated/title/pokemon_logo.png",
|
||||
"assets/generated/fonts/font.png",
|
||||
"assets/generated/battle/front/pikachu.png",
|
||||
"assets/generated/battle/anims/move_anim_0.png",
|
||||
"assets/generated/battle/anims/move_anim_1.png",
|
||||
"assets/generated/audio/programs.bin",
|
||||
}
|
||||
|
||||
local function allRequiredFilesExist()
|
||||
for _, path in ipairs(REQUIRED_FILES) do
|
||||
if not love.filesystem.getInfo(path, "file") then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function sourceTreeHasData()
|
||||
if not allRequiredFilesExist() or not love.filesystem.getRealDirectory then
|
||||
return false
|
||||
end
|
||||
local real = love.filesystem.getRealDirectory(REQUIRED_FILES[1])
|
||||
return real == love.filesystem.getSource()
|
||||
end
|
||||
|
||||
function RomImporter.isReady()
|
||||
if sourceTreeHasData() then return true end
|
||||
return love.filesystem.read(MARKER_PATH) == CACHE_MARKER
|
||||
and allRequiredFilesExist()
|
||||
end
|
||||
|
||||
local function removeTree(path)
|
||||
local info = love.filesystem.getInfo(path)
|
||||
if not info then return end
|
||||
if info.type == "directory" then
|
||||
for _, child in ipairs(love.filesystem.getDirectoryItems(path)) do
|
||||
removeTree(path .. "/" .. child)
|
||||
end
|
||||
end
|
||||
if love.filesystem.getRealDirectory
|
||||
and love.filesystem.getRealDirectory(path)
|
||||
~= love.filesystem.getSaveDirectory() then
|
||||
return
|
||||
end
|
||||
local ok, err = love.filesystem.remove(path)
|
||||
if ok == false then
|
||||
error("could not remove stale cache: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
|
||||
local function decodeManifest()
|
||||
local raw, readError = love.filesystem.read("tools/rom_manifest.json")
|
||||
if not raw then error("ROM import metadata is missing: " .. tostring(readError)) end
|
||||
local Json = require("src.link.Json")
|
||||
local manifest, decodeError = Json.decode(raw)
|
||||
if not manifest then error("ROM import metadata is invalid: " .. tostring(decodeError)) end
|
||||
assert(manifest.romSha1 == ROM_SHA1, "ROM import metadata version mismatch")
|
||||
return manifest
|
||||
end
|
||||
|
||||
local function sha1(data)
|
||||
local digest = love.data.hash("sha1", data)
|
||||
if type(digest) == "userdata" and digest.getString then
|
||||
digest = digest:getString()
|
||||
end
|
||||
return love.data.encode("string", "hex", digest)
|
||||
end
|
||||
|
||||
local function readExternalPath(path)
|
||||
local file, openError = io.open(path, "rb")
|
||||
if not file then return nil, openError end
|
||||
local data = file:read("*a")
|
||||
file:close()
|
||||
return data
|
||||
end
|
||||
|
||||
local function readDroppedFile(file)
|
||||
local ok, openError = file:open("r")
|
||||
if not ok then return nil, openError end
|
||||
local data, readError = file:read(file:getSize())
|
||||
file:close()
|
||||
return data, readError
|
||||
end
|
||||
|
||||
local function trim(value)
|
||||
return value and value:gsub("^%s+", ""):gsub("%s+$", "") or ""
|
||||
end
|
||||
|
||||
local function commandOutput(command)
|
||||
local pipe = io.popen(command, "r")
|
||||
if not pipe then return nil end
|
||||
local result = pipe:read("*a")
|
||||
pipe:close()
|
||||
result = trim(result)
|
||||
return result ~= "" and result or nil
|
||||
end
|
||||
|
||||
local function chooseRom()
|
||||
local platform = love.system.getOS()
|
||||
if platform == "OS X" then
|
||||
return commandOutput(
|
||||
[[osascript -e 'POSIX path of (choose file with prompt "Choose your Pokemon Red ROM" of type {"gb"})' 2>/dev/null]])
|
||||
elseif platform == "Windows" then
|
||||
local script = table.concat({
|
||||
"Add-Type -AssemblyName System.Windows.Forms;",
|
||||
"$d=New-Object System.Windows.Forms.OpenFileDialog;",
|
||||
"$d.Title='Choose your Pokemon Red ROM';",
|
||||
"$d.Filter='Game Boy ROM (*.gb)|*.gb|All files (*.*)|*.*';",
|
||||
"if($d.ShowDialog() -eq 'OK'){[Console]::Write($d.FileName)}",
|
||||
})
|
||||
return commandOutput(
|
||||
'powershell -NoProfile -STA -Command "' .. script .. '"')
|
||||
elseif platform == "Linux" then
|
||||
local path = commandOutput(
|
||||
[[zenity --file-selection --title="Choose your Pokemon Red ROM" --file-filter="Game Boy ROM | *.gb" 2>/dev/null]])
|
||||
if path then return path end
|
||||
return commandOutput(
|
||||
[[kdialog --getopenfilename "$HOME" "*.gb|Game Boy ROM" 2>/dev/null]])
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function RomImporter.new(onComplete)
|
||||
return setmetatable({
|
||||
onComplete = onComplete,
|
||||
logo = love.graphics.newImage("assets/logo/logo.png"),
|
||||
bcg = love.graphics.newImage("assets/logo/bcg.png"),
|
||||
state = "waiting",
|
||||
status = "Choose or drop a Pokemon Red ROM",
|
||||
detail = "The ROM is verified before any files are created.",
|
||||
progress = 0,
|
||||
stageCurrent = 0,
|
||||
stageTotal = 1,
|
||||
pulse = 0,
|
||||
button = {},
|
||||
}, RomImporter)
|
||||
end
|
||||
|
||||
function RomImporter:setError(message)
|
||||
self.state = "error"
|
||||
self.status = "That ROM could not be imported"
|
||||
self.detail = tostring(message)
|
||||
self.progress = 0
|
||||
self.worker = nil
|
||||
self.romData = nil
|
||||
end
|
||||
|
||||
function RomImporter:startData(data, displayName)
|
||||
if self.state == "working" then return end
|
||||
if type(data) ~= "string" then
|
||||
self:setError("The selected file could not be read.")
|
||||
return
|
||||
end
|
||||
if #data ~= 1024 * 1024 then
|
||||
self:setError(("Expected a 1 MiB Pokemon Red ROM; this file is %.2f MiB.")
|
||||
:format(#data / 1024 / 1024))
|
||||
return
|
||||
end
|
||||
|
||||
self.state = "working"
|
||||
self.status = "Verifying ROM"
|
||||
self.detail = displayName or "Pokemon Red"
|
||||
self.progress = 0
|
||||
self.romData = data
|
||||
self.worker = coroutine.create(function()
|
||||
local actualHash = sha1(self.romData)
|
||||
if actualHash ~= ROM_SHA1 then
|
||||
error(("Unsupported ROM (SHA-1 %s). Use an unmodified US Pokemon Red ROM.")
|
||||
:format(actualHash))
|
||||
end
|
||||
self.status = "Preparing private game data"
|
||||
coroutine.yield()
|
||||
removeTree("data/generated")
|
||||
removeTree("assets/generated")
|
||||
love.filesystem.remove(MARKER_PATH)
|
||||
|
||||
local manifest = decodeManifest()
|
||||
local RomExtractor = require("src.import.RomExtractor")
|
||||
local extractor = RomExtractor.new(self.romData, manifest,
|
||||
function(progress, total, stage, current, stageTotal)
|
||||
self.status = stage
|
||||
self.progress = progress / total
|
||||
self.stageCurrent = current
|
||||
self.stageTotal = stageTotal
|
||||
coroutine.yield()
|
||||
end)
|
||||
extractor:run()
|
||||
self.romData = nil
|
||||
collectgarbage("collect")
|
||||
local ok, writeError = love.filesystem.write(MARKER_PATH, CACHE_MARKER)
|
||||
if not ok then error("could not finish the private cache: " .. tostring(writeError)) end
|
||||
self.state = "complete"
|
||||
self.status = "Ready"
|
||||
self.detail = "Starting Pokemon Red..."
|
||||
self.progress = 1
|
||||
if self.onComplete then self.onComplete() end
|
||||
end)
|
||||
end
|
||||
|
||||
function RomImporter:startPath(path)
|
||||
if not path then return end
|
||||
local data, readError = readExternalPath(path)
|
||||
if not data then
|
||||
self:setError("Could not read the selected file: " .. tostring(readError))
|
||||
return
|
||||
end
|
||||
self:startData(data, path:match("[^/\\]+$") or path)
|
||||
end
|
||||
|
||||
function RomImporter:filedropped(file)
|
||||
if self.state == "working" then return end
|
||||
local data, readError = readDroppedFile(file)
|
||||
if not data then
|
||||
self:setError("Could not read the dropped file: " .. tostring(readError))
|
||||
return
|
||||
end
|
||||
self:startData(data, file:getFilename())
|
||||
end
|
||||
|
||||
function RomImporter:choose()
|
||||
if self.state == "working" then return end
|
||||
local path = chooseRom()
|
||||
if path then
|
||||
self:startPath(path)
|
||||
elseif love.system.getOS() ~= "OS X"
|
||||
and love.system.getOS() ~= "Windows"
|
||||
and love.system.getOS() ~= "Linux" then
|
||||
self:setError("File selection is unavailable here. Drop the .gb file onto the window.")
|
||||
end
|
||||
end
|
||||
|
||||
function RomImporter:update(dt)
|
||||
self.pulse = self.pulse + dt
|
||||
if self.state ~= "working" or not self.worker then return end
|
||||
local started = love.timer.getTime()
|
||||
repeat
|
||||
local ok, workerError = coroutine.resume(self.worker)
|
||||
if not ok then
|
||||
print(debug.traceback(self.worker, tostring(workerError)))
|
||||
self:setError(tostring(workerError))
|
||||
return
|
||||
end
|
||||
if coroutine.status(self.worker) == "dead" then
|
||||
self.worker = nil
|
||||
return
|
||||
end
|
||||
until love.timer.getTime() - started >= 0.008
|
||||
end
|
||||
|
||||
local function setColor255(r, g, b, a)
|
||||
love.graphics.setColor(r / 255, g / 255, b / 255, (a or 255) / 255)
|
||||
end
|
||||
|
||||
local function printCentered(text, y, font, width)
|
||||
love.graphics.setFont(font)
|
||||
love.graphics.printf(text, 0, y, width, "center")
|
||||
end
|
||||
|
||||
function RomImporter:draw()
|
||||
local width, height = love.graphics.getDimensions()
|
||||
setColor255(241, 243, 232)
|
||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
||||
setColor255(181, 35, 42)
|
||||
love.graphics.rectangle("fill", 0, 0, width, math.max(8, height * 0.025))
|
||||
|
||||
local fontKey = ("%dx%d"):format(width, height)
|
||||
if self.fontKey ~= fontKey then
|
||||
self.fontKey = fontKey
|
||||
self.bodyFont = love.graphics.newFont(
|
||||
math.max(16, math.min(22, height * 0.038)))
|
||||
self.smallFont = love.graphics.newFont(
|
||||
math.max(13, math.min(17, height * 0.029)))
|
||||
self.warningFont = love.graphics.newFont(
|
||||
math.max(10, math.min(12, height * 0.022)))
|
||||
end
|
||||
local bodyFont, smallFont, warningFont =
|
||||
self.bodyFont, self.smallFont, self.warningFont
|
||||
local contentWidth = math.min(width - 40, 520)
|
||||
local left = (width - contentWidth) / 2
|
||||
|
||||
local logoWidth, logoHeight = self.logo:getDimensions()
|
||||
local logoScale = math.min(
|
||||
math.min(width - 48, 420) / logoWidth,
|
||||
height * 0.15 / logoHeight)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(
|
||||
self.logo,
|
||||
(width - logoWidth * logoScale) / 2,
|
||||
height * 0.075,
|
||||
0, logoScale, logoScale)
|
||||
setColor255(74, 88, 72)
|
||||
printCentered("FIRST RUN", height * 0.205, smallFont, width)
|
||||
|
||||
local zoneY, zoneH = height * 0.29, math.min(180, height * 0.31)
|
||||
setColor255(215, 220, 202)
|
||||
love.graphics.rectangle("fill", left, zoneY, contentWidth, zoneH)
|
||||
setColor255(74, 88, 72)
|
||||
love.graphics.setLineWidth(2)
|
||||
love.graphics.rectangle("line", left, zoneY, contentWidth, zoneH)
|
||||
|
||||
setColor255(25, 31, 28)
|
||||
printCentered(self.status, zoneY + zoneH * 0.25, bodyFont, width)
|
||||
setColor255(74, 88, 72)
|
||||
love.graphics.setFont(smallFont)
|
||||
local _, wrapped = smallFont:getWrap(self.detail, contentWidth - 48)
|
||||
local visible = {}
|
||||
for index = 1, math.min(#wrapped, 3) do visible[index] = wrapped[index] end
|
||||
love.graphics.printf(table.concat(visible, "\n"),
|
||||
left + 24, zoneY + zoneH * 0.52, contentWidth - 48, "center")
|
||||
|
||||
if self.state == "working" or self.state == "complete" then
|
||||
local barY = zoneY + zoneH - 24
|
||||
setColor255(164, 172, 151)
|
||||
love.graphics.rectangle("fill", left + 24, barY, contentWidth - 48, 8)
|
||||
setColor255(181, 35, 42)
|
||||
love.graphics.rectangle("fill", left + 24, barY,
|
||||
(contentWidth - 48) * self.progress, 8)
|
||||
else
|
||||
local buttonWidth = math.min(260, contentWidth - 80)
|
||||
local buttonHeight = math.max(46, math.min(56, height * 0.09))
|
||||
local buttonX = (width - buttonWidth) / 2
|
||||
local buttonY = math.min(height - buttonHeight - 34, zoneY + zoneH + 36)
|
||||
self.button = {
|
||||
x = buttonX, y = buttonY, width = buttonWidth, height = buttonHeight,
|
||||
}
|
||||
setColor255(25, 31, 28)
|
||||
love.graphics.rectangle("fill", buttonX, buttonY, buttonWidth, buttonHeight)
|
||||
setColor255(255, 255, 255)
|
||||
love.graphics.setFont(bodyFont)
|
||||
love.graphics.printf("Choose ROM", buttonX,
|
||||
buttonY + (buttonHeight - bodyFont:getHeight()) / 2,
|
||||
buttonWidth, "center")
|
||||
setColor255(74, 88, 72)
|
||||
love.graphics.setFont(smallFont)
|
||||
love.graphics.printf("or drop the .gb file here",
|
||||
0, buttonY + buttonHeight + 12, width, "center")
|
||||
end
|
||||
|
||||
local bcgWidth, bcgHeight = self.bcg:getDimensions()
|
||||
love.graphics.setFont(warningFont)
|
||||
local warningWidth = math.min(width - 32, 600)
|
||||
local _, warningLines = warningFont:getWrap(TRUST_WARNING, warningWidth)
|
||||
local warningHeight = #warningLines * warningFont:getHeight()
|
||||
local warningY = height - warningHeight - 8
|
||||
local bcgScale = math.min(
|
||||
math.min(width - 48, 220) / bcgWidth,
|
||||
height * 0.08 / bcgHeight)
|
||||
local bcgDrawWidth = bcgWidth * bcgScale
|
||||
local bcgDrawHeight = bcgHeight * bcgScale
|
||||
local bcgX = (width - bcgDrawWidth) / 2
|
||||
local bcgY = warningY - bcgDrawHeight - 8
|
||||
self.bcgButton = {
|
||||
x = bcgX, y = bcgY,
|
||||
width = bcgDrawWidth, height = bcgDrawHeight,
|
||||
}
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(
|
||||
self.bcg,
|
||||
bcgX, bcgY,
|
||||
0, bcgScale, bcgScale)
|
||||
setColor255(74, 88, 72)
|
||||
love.graphics.printf(
|
||||
TRUST_WARNING,
|
||||
(width - warningWidth) / 2, warningY,
|
||||
warningWidth, "center")
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function RomImporter:mousepressed(x, y, button)
|
||||
if button ~= 1 then return end
|
||||
local logo = self.bcgButton or {}
|
||||
if x >= (logo.x or 0) and x <= (logo.x or 0) + (logo.width or 0)
|
||||
and y >= (logo.y or 0) and y <= (logo.y or 0) + (logo.height or 0) then
|
||||
love.system.openURL(COMMUNITY_URL)
|
||||
return
|
||||
end
|
||||
if self.state == "working" then return end
|
||||
local rect = self.button
|
||||
if x >= (rect.x or 0) and x <= (rect.x or 0) + (rect.width or 0)
|
||||
and y >= (rect.y or 0) and y <= (rect.y or 0) + (rect.height or 0) then
|
||||
self:choose()
|
||||
end
|
||||
end
|
||||
|
||||
function RomImporter:keypressed(key)
|
||||
if (key == "return" or key == "space") and self.state ~= "working" then
|
||||
self:choose()
|
||||
end
|
||||
end
|
||||
|
||||
return RomImporter
|
||||
Reference in New Issue
Block a user