mirror of
https://github.com/spicyjpeg/573in1.git
synced 2025-03-01 07:20:42 +01:00
Make launchers standalone, remove xorriso dependency
This commit is contained in:
parent
176eb07915
commit
b921d6e7b4
114
CMakeLists.txt
114
CMakeLists.txt
@ -4,28 +4,37 @@ cmake_minimum_required(VERSION 3.25)
|
||||
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/cmake/toolchain.cmake")
|
||||
|
||||
project(
|
||||
cart_tool_private
|
||||
cart_tool
|
||||
LANGUAGES C CXX ASM
|
||||
VERSION 0.4.2
|
||||
DESCRIPTION "Konami System 573 security cartridge tool"
|
||||
)
|
||||
|
||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||
find_program(
|
||||
XORRISO_PATH xorriso
|
||||
PATHS "C:/msys64/usr/bin"
|
||||
DOC "Path to xorriso tool (optional)"
|
||||
)
|
||||
find_program(
|
||||
CHDMAN_PATH chdman
|
||||
PATHS "C:/Program Files/MAME" "C:/Program Files (x86)/MAME"
|
||||
PATHS
|
||||
"C:/Program Files/MAME"
|
||||
"C:/Program Files (x86)/MAME"
|
||||
"/opt/mame"
|
||||
DOC "Path to MAME chdman tool (optional)"
|
||||
)
|
||||
|
||||
string(TOUPPER "${PROJECT_NAME}" _name)
|
||||
string(REPLACE "." "_" _version "${PROJECT_VERSION}")
|
||||
set(releaseInfo "cart_tool build ${PROJECT_VERSION} - (C) 2022-2024 spicyjpeg")
|
||||
set(releaseName "cart_tool_${PROJECT_VERSION}")
|
||||
set(cdVolumeName "CART_TOOL_${_version}")
|
||||
|
||||
set(
|
||||
RELEASE_INFO "${PROJECT_NAME} ${PROJECT_VERSION} - (C) 2022-2024 spicyjpeg"
|
||||
CACHE STRING "Executable description and version string (optional)"
|
||||
)
|
||||
set(
|
||||
RELEASE_NAME "${PROJECT_NAME}_${PROJECT_VERSION}"
|
||||
CACHE STRING "CD-ROM image and release package file name"
|
||||
)
|
||||
set(
|
||||
CD_VOLUME_NAME "${_name}_${_version}"
|
||||
CACHE STRING "CD-ROM image volume label"
|
||||
)
|
||||
|
||||
## Files common to all executables
|
||||
|
||||
@ -61,7 +70,7 @@ function(addExecutable name address stackTop)
|
||||
COMMAND
|
||||
"${Python3_EXECUTABLE}"
|
||||
"${PROJECT_SOURCE_DIR}/tools/convertExecutable.py"
|
||||
-r "${releaseInfo}"
|
||||
-r "${RELEASE_INFO}"
|
||||
-s 0x${stackTop}
|
||||
"$<TARGET_FILE:${name}>"
|
||||
"${name}.psexe"
|
||||
@ -157,8 +166,6 @@ addExecutable(
|
||||
)
|
||||
target_link_libraries(boot PRIVATE bootFlags)
|
||||
|
||||
list(APPEND packageContents boot.psexe)
|
||||
|
||||
function(addLauncher address stackTop)
|
||||
addExecutable(
|
||||
launcher${address} ${address} ${stackTop}
|
||||
@ -236,9 +243,9 @@ add_custom_command(
|
||||
resources.json
|
||||
assets/app.palette.json
|
||||
assets/app.strings.json
|
||||
main
|
||||
launcher801f8000
|
||||
launcher803f8000
|
||||
main.psexe
|
||||
launcher801f8000.psexe
|
||||
launcher803f8000.psexe
|
||||
COMMENT "Building resource archive"
|
||||
VERBATIM
|
||||
)
|
||||
@ -249,80 +256,57 @@ addBinaryFile(
|
||||
|
||||
## CD-ROM image
|
||||
|
||||
configure_file(cd.json cd.json ESCAPE_QUOTES)
|
||||
configure_file(assets/cdreadme.txt readme.txt NEWLINE_STYLE CRLF)
|
||||
|
||||
if(EXISTS "${XORRISO_PATH}")
|
||||
add_custom_command(
|
||||
COMMAND
|
||||
"${XORRISO_PATH}"
|
||||
-outdev "${releaseName}.iso"
|
||||
-blank all
|
||||
-rockridge off
|
||||
-joliet off
|
||||
-padding 2M
|
||||
-volid "${cdVolumeName}"
|
||||
-volset_id "${cdVolumeName}"
|
||||
-publisher "SPICYJPEG"
|
||||
-application_id "PLAYSTATION"
|
||||
-system_id "PLAYSTATION"
|
||||
-preparer_id "CART_TOOL BUILD SCRIPT"
|
||||
-map readme.txt README.TXT
|
||||
-map boot.psexe PSX.EXE
|
||||
-clone PSX.EXE GSE.NXX
|
||||
-clone PSX.EXE NSE.GXX
|
||||
-clone PSX.EXE OSE.FXX
|
||||
-clone PSX.EXE QSU.DXH
|
||||
-clone PSX.EXE QSX.DXE
|
||||
-clone PSX.EXE QSY.DXD
|
||||
-clone PSX.EXE QSZ.DXC
|
||||
-clone PSX.EXE RSU.CXH
|
||||
-clone PSX.EXE RSV.CXG
|
||||
-clone PSX.EXE RSW.CXF
|
||||
-clone PSX.EXE RSZ.CXC
|
||||
-clone PSX.EXE SSW.BXF
|
||||
-clone PSX.EXE SSX.BXE
|
||||
-clone PSX.EXE SSY.BXD
|
||||
-clone PSX.EXE TSV.AXG
|
||||
-clone PSX.EXE TSW.AXF
|
||||
-clone PSX.EXE TSX.AXE
|
||||
-clone PSX.EXE TSY.AXD
|
||||
-clone PSX.EXE TSZ.AXC
|
||||
OUTPUT "${releaseName}.iso"
|
||||
DEPENDS boot
|
||||
"${Python3_EXECUTABLE}"
|
||||
"${PROJECT_SOURCE_DIR}/tools/buildCDImage.py"
|
||||
cd.json
|
||||
"${RELEASE_NAME}.iso"
|
||||
OUTPUT "${RELEASE_NAME}.iso"
|
||||
DEPENDS
|
||||
cd.json
|
||||
boot.psexe
|
||||
COMMENT "Building CD-ROM image"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
list(APPEND packageContents "${releaseName}.iso")
|
||||
|
||||
if(EXISTS "${CHDMAN_PATH}")
|
||||
add_custom_command(
|
||||
COMMAND
|
||||
"${CHDMAN_PATH}" createcd -f
|
||||
-i "${releaseName}.iso"
|
||||
-o "${releaseName}.chd"
|
||||
OUTPUT "${releaseName}.chd"
|
||||
DEPENDS "${releaseName}.iso"
|
||||
-i "${RELEASE_NAME}.iso"
|
||||
-o "${RELEASE_NAME}.chd"
|
||||
OUTPUT "${RELEASE_NAME}.chd"
|
||||
DEPENDS "${RELEASE_NAME}.iso"
|
||||
COMMENT "Building MAME CHD image"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
list(APPEND packageContents "${releaseName}.chd")
|
||||
endif()
|
||||
list(APPEND packageFiles "${RELEASE_NAME}.chd")
|
||||
endif()
|
||||
|
||||
## Release package
|
||||
|
||||
list(
|
||||
APPEND packageFiles
|
||||
readme.txt
|
||||
boot.psexe
|
||||
"${RELEASE_NAME}.iso"
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
COMMAND
|
||||
"${CMAKE_COMMAND}" -E tar cf
|
||||
"${releaseName}.zip"
|
||||
"${RELEASE_NAME}.zip"
|
||||
--format=zip
|
||||
${packageContents} readme.txt
|
||||
OUTPUT "${releaseName}.zip"
|
||||
DEPENDS ${packageContents}
|
||||
${packageFiles}
|
||||
OUTPUT "${RELEASE_NAME}.zip"
|
||||
DEPENDS ${packageFiles}
|
||||
COMMENT "Packaging built files"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(package ALL DEPENDS "${releaseName}.zip")
|
||||
add_custom_target(package ALL DEPENDS "${RELEASE_NAME}.zip")
|
||||
|
126
cd.json
Normal file
126
cd.json
Normal file
@ -0,0 +1,126 @@
|
||||
{
|
||||
"identifiers": {
|
||||
"system": "PLAYSTATION",
|
||||
"volume": "${CD_VOLUME_NAME}",
|
||||
"volumeSet": "${CD_VOLUME_NAME}",
|
||||
"publisher": "SPICYJPEG",
|
||||
"dataPreparer": "GCC ${CMAKE_CXX_COMPILER_VERSION}, CMAKE ${CMAKE_VERSION}, PYTHON ${Python3_VERSION}",
|
||||
"application": "PLAYSTATION",
|
||||
"copyright": "README.TXT;1"
|
||||
},
|
||||
|
||||
"entries": [
|
||||
{
|
||||
"type": "file",
|
||||
"name": "README.TXT",
|
||||
"source": "${PROJECT_BINARY_DIR}/readme.txt"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "file",
|
||||
"name": "PSX.EXE",
|
||||
"source": "${PROJECT_BINARY_DIR}/boot.psexe"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "GSE.NXX",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "NSE.GXX",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "OSE.FXX",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "QSU.DXH",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "QSX.DXE",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "QSY.DXD",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "QSZ.DXC",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "RSU.CXH",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "RSV.CXG",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "RSW.CXF",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "RSZ.CXC",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "SSW.BXF",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "SSX.BXE",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "SSY.BXD",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "TSV.AXG",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "TSW.AXF",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "TSX.AXE",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "TSY.AXD",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
{
|
||||
"type": "fileAlias",
|
||||
"name": "TSZ.AXC",
|
||||
"source": "PSX.EXE"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "padding",
|
||||
"name": "PADDING.BIN",
|
||||
"size": 1048576
|
||||
}
|
||||
]
|
||||
}
|
@ -2,18 +2,18 @@
|
||||
{
|
||||
"type": "binary",
|
||||
"name": "binaries/main.psexe.lz4",
|
||||
"source": "main.psexe",
|
||||
"source": "${PROJECT_BINARY_DIR}/main.psexe",
|
||||
"compression": "lz4"
|
||||
},
|
||||
{
|
||||
"type": "binary",
|
||||
"name": "binaries/launcher801f8000.psexe",
|
||||
"source": "launcher801f8000.psexe"
|
||||
"source": "${PROJECT_BINARY_DIR}/launcher801f8000.psexe"
|
||||
},
|
||||
{
|
||||
"type": "binary",
|
||||
"name": "binaries/launcher803f8000.psexe",
|
||||
"source": "launcher803f8000.psexe"
|
||||
"source": "${PROJECT_BINARY_DIR}/launcher803f8000.psexe"
|
||||
},
|
||||
|
||||
{
|
||||
|
@ -87,7 +87,12 @@ bool ExecutableLauncherArgs::parseArgument(const char *arg) {
|
||||
return true;
|
||||
|
||||
default:
|
||||
#if 0
|
||||
return CommonArgs::parseArgument(arg);
|
||||
#else
|
||||
// Avoid pulling in strtol().
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,7 +144,7 @@ static inline void setMiscOutput(MiscOutputPin pin, bool value) {
|
||||
static inline bool isDigitalIOPresent(void) {
|
||||
return (
|
||||
(SYS573D_CPLD_STAT & (SYS573D_CPLD_STAT_ID1 | SYS573D_CPLD_STAT_ID2)) ==
|
||||
SYS573D_CPLD_STAT_ID1
|
||||
SYS573D_CPLD_STAT_ID2
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -7,10 +7,17 @@
|
||||
#include "vendor/ff.h"
|
||||
|
||||
LauncherError ExecutableLauncher::openFile(void) {
|
||||
#if 0
|
||||
if (!args.drive || !args.path) {
|
||||
LOG("required arguments missing");
|
||||
return INVALID_ARGS;
|
||||
}
|
||||
#else
|
||||
if (!args.drive)
|
||||
args.drive = "1:";
|
||||
if (!args.path)
|
||||
args.path = "psx.exe";
|
||||
#endif
|
||||
|
||||
// The drive index is always a single digit, so there is no need to pull in
|
||||
// strtol() here.
|
||||
|
@ -302,7 +302,7 @@ void StorageActionsScreen::update(ui::Context &ctx) {
|
||||
ctx.show(APP->_storageInfoScreen, true, true);
|
||||
} else {
|
||||
if (action.region.isPresent()) {
|
||||
this->selectedRegion = &(action.region);
|
||||
selectedRegion = &(action.region);
|
||||
|
||||
if (action.region.regionLength > 0x1000000) {
|
||||
APP->_cardSizeScreen.callback = action.target;
|
||||
|
280
tools/buildCDImage.py
Executable file
280
tools/buildCDImage.py
Executable file
@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""ISO9660 CD-ROM image generator
|
||||
|
||||
A simple tool for building ISO9660 CD-ROM images with 2048-byte sectors, as used
|
||||
by the PlayStation 1 and other machines, from a JSON configuration file
|
||||
describing their contents. Unlike most other ISO9660 authoring tools, files and
|
||||
directories are placed in the image in the same order they are specified in the
|
||||
JSON file, allowing for customization of the disc's layout. Requires pycdlib to
|
||||
be installed.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "spicyjpeg"
|
||||
|
||||
import json, re, sys
|
||||
from argparse import ArgumentParser, FileType, Namespace
|
||||
from pathlib import Path
|
||||
from typing import Any, BinaryIO, Iterable
|
||||
|
||||
from pycdlib.dr import DirectoryRecord
|
||||
from pycdlib.inode import Inode
|
||||
from pycdlib.pycdlib import PyCdlib
|
||||
|
||||
## Utilities
|
||||
|
||||
PATH_IGNORE_REGEX: re.Pattern = re.compile(r";[0-9]+$")
|
||||
PATH_INVALID_REGEX: re.Pattern = re.compile(r"[^0-9A-Z_./]")
|
||||
|
||||
def normalizePath(path: str, isDirectory: bool = False) -> str:
|
||||
path = PATH_IGNORE_REGEX.sub("", path.upper())
|
||||
path = PATH_INVALID_REGEX.sub("_", path)
|
||||
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
|
||||
if isDirectory:
|
||||
path = path.replace(".", "_")
|
||||
else:
|
||||
# The ISO9660 specification requires file names to always contain
|
||||
# exactly one period (even if no extension is present) and be terminated
|
||||
# with a version number.
|
||||
numPeriods: int = path.count(".")
|
||||
|
||||
if numPeriods:
|
||||
path = path.replace(".", "_", numPeriods - 1)
|
||||
else:
|
||||
path += "."
|
||||
|
||||
path += ";1"
|
||||
|
||||
return path
|
||||
|
||||
def restoreISOFileOrder(
|
||||
iso: PyCdlib, isoEntries: Iterable[DirectoryRecord | Inode],
|
||||
printLayout: bool = False
|
||||
):
|
||||
# By default, when calling force_consistency(), pycdlib allocates files in
|
||||
# the same order as their respective directory records, which are in turn
|
||||
# sorted alphabetically as required by the ISO9660 specification. We're
|
||||
# going to "undo" this sorting by iterating through the provided array and
|
||||
# reassigning an LBA to each file and directory manually to restore the
|
||||
# intended order.
|
||||
iso.force_consistency()
|
||||
|
||||
sectorLength: int = iso.logical_block_size
|
||||
sectorOffset: int = \
|
||||
iso.pvd.path_table_location_be + iso.pvd.path_table_num_extents
|
||||
|
||||
if printLayout:
|
||||
sys.stderr.write("CD-ROM image layout:\n")
|
||||
|
||||
# Allocate the indices (arrays of directory records) for the root directory
|
||||
# and each subfolder. Note that these will always be placed before any file
|
||||
# data, regardless of where they are listed in the configuration file.
|
||||
for entry in isoEntries:
|
||||
names: list[str] = []
|
||||
length: int = \
|
||||
(entry.data_length + sectorLength - 1) // sectorLength
|
||||
|
||||
if isinstance(entry, DirectoryRecord):
|
||||
entry.set_data_location(sectorOffset, 0)
|
||||
names.append(entry.file_identifier().decode("ascii"))
|
||||
else:
|
||||
entry.set_extent_location(sectorOffset)
|
||||
|
||||
for record, _ in entry.linked_records:
|
||||
if not isinstance(record, DirectoryRecord):
|
||||
continue
|
||||
|
||||
record.set_data_location(sectorOffset, 0)
|
||||
names.append(record.file_identifier().decode("ascii"))
|
||||
|
||||
if printLayout:
|
||||
nameList: str = ", ".join(names)
|
||||
|
||||
sys.stderr.write(
|
||||
f" [{sectorOffset:6d}-{sectorOffset + length - 1:6d}] "
|
||||
f"{nameList}\n"
|
||||
)
|
||||
|
||||
sectorOffset += length
|
||||
|
||||
if printLayout:
|
||||
sys.stderr.write(f"Total image size: {sectorOffset} sectors\n")
|
||||
|
||||
def showProgress(part: int, total: int, _: Any):
|
||||
if part >= total:
|
||||
sys.stderr.write("\rPacking finished.\n")
|
||||
else:
|
||||
sys.stderr.write(f"\rPacking: {part / total * 100.0:5.1f}%")
|
||||
|
||||
class PaddingFile(BinaryIO):
|
||||
mode: str = "rb"
|
||||
|
||||
def read(self, length: int) -> bytes:
|
||||
return bytes(length)
|
||||
|
||||
## Main
|
||||
|
||||
def createParser() -> ArgumentParser:
|
||||
parser = ArgumentParser(
|
||||
description = \
|
||||
"Parses a JSON file containing a list of ISO9660 identifiers, file "
|
||||
"and directory entries and generates a CD-ROM image.",
|
||||
add_help = False
|
||||
)
|
||||
|
||||
group = parser.add_argument_group("Tool options")
|
||||
group.add_argument(
|
||||
"-h", "--help",
|
||||
action = "help",
|
||||
help = "Show this help message and exit"
|
||||
)
|
||||
group.add_argument(
|
||||
"-q", "--quiet",
|
||||
action = "store_true",
|
||||
help = "Suppress all non-error output"
|
||||
)
|
||||
|
||||
group = parser.add_argument_group("CD-ROM image options")
|
||||
group.add_argument(
|
||||
"-l", "--level",
|
||||
type = lambda value: int(value, 0),
|
||||
default = 1,
|
||||
help = \
|
||||
"Set ISO9660 interchange level and maximum file name length "
|
||||
"(default 1)",
|
||||
metavar = "1-4"
|
||||
)
|
||||
group.add_argument(
|
||||
"-x", "--xa",
|
||||
action = "store_true",
|
||||
help = "Add CD-XA header and metadata tags"
|
||||
)
|
||||
group.add_argument(
|
||||
"-S", "--system-area",
|
||||
type = FileType("rb"),
|
||||
help = \
|
||||
"Insert specified file (in 2048-bytes-per-sector format, up to 32 "
|
||||
"KB) into the image's system area",
|
||||
metavar = "file"
|
||||
)
|
||||
|
||||
group = parser.add_argument_group("File paths")
|
||||
group.add_argument(
|
||||
"-s", "--source-dir",
|
||||
type = Path,
|
||||
help = \
|
||||
"Set path to directory containing source files (same directory as "
|
||||
"configuration file by default)",
|
||||
metavar = "dir"
|
||||
)
|
||||
group.add_argument(
|
||||
"configFile",
|
||||
type = FileType("rt"),
|
||||
help = "Path to JSON configuration file",
|
||||
)
|
||||
group.add_argument(
|
||||
"output",
|
||||
type = FileType("wb"),
|
||||
help = "Path to ISO9660 CD-ROM image to generate"
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def main():
|
||||
parser: ArgumentParser = createParser()
|
||||
args: Namespace = parser.parse_args()
|
||||
|
||||
with args.configFile as _file:
|
||||
configFile: dict[str, Any] = json.load(_file)
|
||||
sourceDir: Path = \
|
||||
args.source_dir or Path(_file.name).parent
|
||||
|
||||
iso: PyCdlib = PyCdlib()
|
||||
paddingFile: PaddingFile = PaddingFile()
|
||||
|
||||
identifiers: dict[str, str] = configFile.get("identifiers", {})
|
||||
|
||||
iso.new(
|
||||
interchange_level = args.level,
|
||||
sys_ident = identifiers.get("system", ""),
|
||||
vol_ident = identifiers.get("volume", ""),
|
||||
vol_set_ident = identifiers.get("volumeSet", ""),
|
||||
pub_ident_str = identifiers.get("publisher", ""),
|
||||
preparer_ident_str = identifiers.get("dataPreparer", ""),
|
||||
app_ident_str = identifiers.get("application", ""),
|
||||
copyright_file = identifiers.get("copyright", ""),
|
||||
abstract_file = identifiers.get("abstract", ""),
|
||||
bibli_file = identifiers.get("bibliographic", ""),
|
||||
joliet = None,
|
||||
rock_ridge = None,
|
||||
xa = args.xa,
|
||||
udf = None
|
||||
)
|
||||
|
||||
entryList: list[dict[str, Any]] = configFile["entries"]
|
||||
isoEntries: list[DirectoryRecord | Inode] = [
|
||||
iso.pvd.root_directory_record()
|
||||
]
|
||||
|
||||
for entry in entryList:
|
||||
match entry.get("type", "file").strip():
|
||||
case "padding":
|
||||
name: str = normalizePath(entry["name"])
|
||||
|
||||
iso.add_fp(
|
||||
fp = paddingFile,
|
||||
length = int(entry["size"]),
|
||||
iso_path = name
|
||||
)
|
||||
iso.set_hidden(iso_path = name)
|
||||
isoEntries.append(iso.inodes[-1])
|
||||
|
||||
case "file":
|
||||
iso.add_file(
|
||||
filename = sourceDir / entry["source"],
|
||||
iso_path = normalizePath(entry["name"])
|
||||
)
|
||||
isoEntries.append(iso.inodes[-1])
|
||||
|
||||
case "fileAlias":
|
||||
iso.add_hard_link(
|
||||
iso_old_path = normalizePath(entry["source"]),
|
||||
iso_new_path = normalizePath(entry["name"])
|
||||
)
|
||||
|
||||
case "directoryAlias":
|
||||
iso.add_hard_link(
|
||||
iso_old_path = normalizePath(entry["source"], True),
|
||||
iso_new_path = normalizePath(entry["name"], True)
|
||||
)
|
||||
|
||||
case "directory":
|
||||
name: str = normalizePath(entry["name"], True)
|
||||
|
||||
iso.add_directory(iso_path = name)
|
||||
isoEntries.append(iso.get_record(iso_path = name))
|
||||
|
||||
case _type:
|
||||
raise KeyError(f"unsupported entry type '{_type}'")
|
||||
|
||||
restoreISOFileOrder(iso, isoEntries, not args.quiet)
|
||||
|
||||
with args.output as _file:
|
||||
iso.write_fp(
|
||||
outfp = _file,
|
||||
progress_cb = None if args.quiet else showProgress
|
||||
)
|
||||
iso.close()
|
||||
|
||||
if args.system_area:
|
||||
with args.system_area as inputFile:
|
||||
_file.seek(0)
|
||||
_file.write(inputFile.read(iso.logical_block_size * 16))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = "0.4.1"
|
||||
__version__ = "0.4.2"
|
||||
__author__ = "spicyjpeg"
|
||||
|
||||
import json, logging, os, re
|
||||
@ -14,6 +14,7 @@ from typing import Any, Mapping, Sequence, TextIO
|
||||
from common.cart import CartDump, DumpFlag
|
||||
from common.cartdata import *
|
||||
from common.games import GameDB, GameDBEntry
|
||||
from common.util import setupLogger
|
||||
|
||||
## MAME NVRAM file parser
|
||||
|
||||
@ -200,17 +201,6 @@ def createParser() -> ArgumentParser:
|
||||
|
||||
return parser
|
||||
|
||||
def setupLogger(level: int | None):
|
||||
logging.basicConfig(
|
||||
format = "[{levelname:8s}] {message}",
|
||||
style = "{",
|
||||
level = (
|
||||
logging.WARNING,
|
||||
logging.INFO,
|
||||
logging.DEBUG
|
||||
)[min(level or 0, 2)]
|
||||
)
|
||||
|
||||
def main():
|
||||
parser: ArgumentParser = createParser()
|
||||
args: Namespace = parser.parse_args()
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = "0.4.1"
|
||||
__version__ = "0.4.2"
|
||||
__author__ = "spicyjpeg"
|
||||
|
||||
import json, logging, os, re
|
||||
@ -12,7 +12,7 @@ from typing import ByteString, Mapping, TextIO
|
||||
from common.cart import DumpFlag, ROMHeaderDump
|
||||
from common.cartdata import *
|
||||
from common.games import GameDB, GameDBEntry
|
||||
from common.util import InterleavedFile
|
||||
from common.util import InterleavedFile, setupLogger
|
||||
|
||||
## Flash dump "parser"
|
||||
|
||||
@ -143,17 +143,6 @@ def createParser() -> ArgumentParser:
|
||||
|
||||
return parser
|
||||
|
||||
def setupLogger(level: int | None):
|
||||
logging.basicConfig(
|
||||
format = "[{levelname:8s}] {message}",
|
||||
style = "{",
|
||||
level = (
|
||||
logging.WARNING,
|
||||
logging.INFO,
|
||||
logging.DEBUG
|
||||
)[min(level or 0, 2)]
|
||||
)
|
||||
|
||||
def main():
|
||||
parser: ArgumentParser = createParser()
|
||||
args: Namespace = parser.parse_args()
|
||||
|
@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = "0.4.1"
|
||||
__version__ = "0.4.2"
|
||||
__author__ = "spicyjpeg"
|
||||
|
||||
import json
|
||||
from argparse import ArgumentParser, FileType, Namespace
|
||||
from pathlib import Path
|
||||
from typing import ByteString
|
||||
from typing import Any, ByteString
|
||||
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
|
||||
|
||||
import lz4.block
|
||||
@ -53,7 +53,8 @@ def createParser() -> ArgumentParser:
|
||||
type = Path,
|
||||
help = \
|
||||
"Set path to directory containing source files (same directory as "
|
||||
"resource list by default)"
|
||||
"resource list by default)",
|
||||
metavar = "dir"
|
||||
)
|
||||
group.add_argument(
|
||||
"resourceList",
|
||||
@ -73,8 +74,9 @@ def main():
|
||||
args: Namespace = parser.parse_args()
|
||||
|
||||
with args.resourceList as _file:
|
||||
assetList: list = json.load(_file)
|
||||
sourceDir: Path = args.source_dir or Path(_file.name).parent
|
||||
assetList: list[dict[str, Any]] = json.load(_file)
|
||||
sourceDir: Path = \
|
||||
args.source_dir or Path(_file.name).parent
|
||||
|
||||
with ZipFile(args.output, "w", allowZip64 = False) as _zip:
|
||||
for asset in assetList:
|
||||
|
@ -9,7 +9,7 @@ from typing import Any, Generator, Mapping, Sequence
|
||||
import numpy
|
||||
from numpy import ndarray
|
||||
from PIL import Image
|
||||
from .util import hashData
|
||||
from .util import colorFromString, hashData
|
||||
|
||||
## .TIM image converter
|
||||
|
||||
@ -155,8 +155,9 @@ def generateFontMetrics(metrics: Mapping[str, Any]) -> bytearray:
|
||||
|
||||
## Color palette generator
|
||||
|
||||
_PALETTE_COLOR_REGEX: re.Pattern = re.compile(r"^#?([0-9A-Fa-f]{6})$")
|
||||
_PALETTE_COLORS: Sequence[str] = (
|
||||
_PALETTE_ENTRY_STRUCT: Struct = Struct("< 3B x")
|
||||
|
||||
_PALETTE_ENTRIES: Sequence[str] = (
|
||||
"default",
|
||||
"shadow",
|
||||
"backdrop",
|
||||
@ -177,25 +178,22 @@ _PALETTE_COLORS: Sequence[str] = (
|
||||
"subtitle"
|
||||
)
|
||||
|
||||
_PALETTE_ENTRY_STRUCT: Struct = Struct("< 3s x")
|
||||
|
||||
def generateColorPalette(palette: Mapping[str, str]) -> bytearray:
|
||||
def generateColorPalette(
|
||||
palette: Mapping[str, str | Sequence[int]]
|
||||
) -> bytearray:
|
||||
data: bytearray = bytearray()
|
||||
|
||||
for entry in _PALETTE_COLORS:
|
||||
color: str | None = palette.get(entry, None)
|
||||
for entry in _PALETTE_ENTRIES:
|
||||
color: str | Sequence[int] | None = palette.get(entry, None)
|
||||
|
||||
if color is None:
|
||||
raise ValueError(f"no entry found for {entry}")
|
||||
if isinstance(color, str):
|
||||
r, g, b = colorFromString(color)
|
||||
else:
|
||||
r, g, b = color
|
||||
|
||||
matched: re.Match | None = _PALETTE_COLOR_REGEX.match(color)
|
||||
|
||||
if matched is None:
|
||||
raise ValueError(f"invalid color value: {color}")
|
||||
|
||||
colorValue: bytes = bytes.fromhex(matched.group(1))
|
||||
|
||||
data.extend(_PALETTE_ENTRY_STRUCT.pack(colorValue))
|
||||
data.extend(_PALETTE_ENTRY_STRUCT.pack(r, g, b))
|
||||
|
||||
return data
|
||||
|
||||
@ -241,7 +239,7 @@ def _walkStringTree(
|
||||
|
||||
if value is None:
|
||||
yield hashData(fullKey.encode("ascii")), None
|
||||
elif type(value) is str:
|
||||
elif isinstance(value, str):
|
||||
yield hashData(fullKey.encode("ascii")), _convertString(value)
|
||||
else:
|
||||
yield from _walkStringTree(value, f"{fullKey}.")
|
||||
|
@ -1,10 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging, re
|
||||
from collections import deque
|
||||
from hashlib import md5
|
||||
from io import SEEK_SET, SEEK_END
|
||||
from typing import BinaryIO, ByteString, Iterable, Iterator, Sequence, TextIO
|
||||
from io import SEEK_END, SEEK_SET
|
||||
from typing import \
|
||||
BinaryIO, ByteString, Iterable, Iterator, Mapping, MutableSequence, \
|
||||
Sequence, TextIO
|
||||
|
||||
## Misc. utilities
|
||||
import numpy
|
||||
|
||||
## Value and array manipulation
|
||||
|
||||
def signExtend(value: int, bitLength: int) -> int:
|
||||
signMask: int = 1 << (bitLength - 1)
|
||||
@ -12,12 +18,31 @@ def signExtend(value: int, bitLength: int) -> int:
|
||||
|
||||
return (value & valueMask) - (value & signMask)
|
||||
|
||||
def blitArray(
|
||||
source: numpy.ndarray, dest: numpy.ndarray, position: Sequence[int]
|
||||
):
|
||||
pos: map[int | None] = map(lambda x: x if x >= 0 else None, position)
|
||||
neg: map[int | None] = map(lambda x: -x if x < 0 else None, position)
|
||||
|
||||
destView: numpy.ndarray = dest[tuple(
|
||||
slice(start, None) for start in pos
|
||||
)]
|
||||
sourceView: numpy.ndarray = source[tuple(
|
||||
slice(start, end) for start, end in zip(neg, destView.shape)
|
||||
)]
|
||||
|
||||
destView[tuple(
|
||||
slice(None, end) for end in source.shape
|
||||
)] = sourceView
|
||||
|
||||
## String manipulation
|
||||
|
||||
# This encoding is similar to standard base45, but with some problematic
|
||||
# characters (' ', '$', '%', '*') excluded.
|
||||
_BASE41_CHARSET: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./:"
|
||||
|
||||
_COLOR_REGEX: re.Pattern = re.compile(r"^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")
|
||||
|
||||
def toPrintableChar(value: int) -> str:
|
||||
if (value < 0x20) or (value > 0x7e):
|
||||
return "."
|
||||
@ -54,6 +79,27 @@ def decodeBase41(data: str) -> bytearray:
|
||||
|
||||
return output
|
||||
|
||||
def colorFromString(value: str) -> tuple[int, int, int]:
|
||||
matched: re.Match | None = _COLOR_REGEX.match(value)
|
||||
|
||||
if matched is None:
|
||||
raise ValueError(f"invalid color value '{value}'")
|
||||
|
||||
digits: str = matched.group(1)
|
||||
|
||||
if len(digits) == 3:
|
||||
return (
|
||||
int(digits[0], 16) * 0x11,
|
||||
int(digits[1], 16) * 0x11,
|
||||
int(digits[2], 16) * 0x11
|
||||
)
|
||||
else:
|
||||
return (
|
||||
int(digits[0:2], 16),
|
||||
int(digits[2:4], 16),
|
||||
int(digits[4:6], 16)
|
||||
)
|
||||
|
||||
## Hashes and checksums
|
||||
|
||||
def hashData(data: Iterable[int]) -> int:
|
||||
@ -87,6 +133,19 @@ def shortenedMD5(data: ByteString) -> bytearray:
|
||||
|
||||
return output
|
||||
|
||||
## Logging
|
||||
|
||||
def setupLogger(level: int | None):
|
||||
logging.basicConfig(
|
||||
format = "[{levelname:8s}] {message}",
|
||||
style = "{",
|
||||
level = (
|
||||
logging.WARNING,
|
||||
logging.INFO,
|
||||
logging.DEBUG
|
||||
)[min(level or 0, 2)]
|
||||
)
|
||||
|
||||
## Odd/even interleaved file reader
|
||||
|
||||
class InterleavedFile(BinaryIO):
|
||||
@ -139,3 +198,158 @@ class InterleavedFile(BinaryIO):
|
||||
|
||||
self._offset += _length
|
||||
return output
|
||||
|
||||
## Boolean algebra expression parser
|
||||
|
||||
class BooleanOperator:
|
||||
precedence: int = 1
|
||||
operands: int = 2
|
||||
|
||||
@staticmethod
|
||||
def execute(stack: MutableSequence[bool]):
|
||||
pass
|
||||
|
||||
class AndOperator(BooleanOperator):
|
||||
precedence: int = 2
|
||||
|
||||
@staticmethod
|
||||
def execute(stack: MutableSequence[bool]):
|
||||
a: bool = stack.pop()
|
||||
b: bool = stack.pop()
|
||||
|
||||
stack.append(a and b)
|
||||
|
||||
class OrOperator(BooleanOperator):
|
||||
@staticmethod
|
||||
def execute(stack: MutableSequence[bool]):
|
||||
a: bool = stack.pop()
|
||||
b: bool = stack.pop()
|
||||
|
||||
stack.append(a or b)
|
||||
|
||||
class XorOperator(BooleanOperator):
|
||||
@staticmethod
|
||||
def execute(stack: MutableSequence[bool]):
|
||||
a: bool = stack.pop()
|
||||
b: bool = stack.pop()
|
||||
|
||||
stack.append(a != b)
|
||||
|
||||
class NotOperator(BooleanOperator):
|
||||
precedence: int = 3
|
||||
operands: int = 1
|
||||
|
||||
@staticmethod
|
||||
def execute(stack: MutableSequence):
|
||||
stack.append(not stack.pop())
|
||||
|
||||
_OPERATORS: Mapping[str, type[BooleanOperator]] = {
|
||||
"*": AndOperator,
|
||||
"+": OrOperator,
|
||||
"@": XorOperator,
|
||||
"~": NotOperator
|
||||
}
|
||||
|
||||
class BooleanFunction:
|
||||
def __init__(self, expression: str):
|
||||
# "Compile" the expression to its respective RPN representation using
|
||||
# the shunting yard algorithm.
|
||||
self.expression: list[str | type[BooleanOperator]] = []
|
||||
|
||||
operators: deque[str] = deque()
|
||||
tokenBuffer: str = ""
|
||||
|
||||
for char in expression:
|
||||
if char not in "*+@~()":
|
||||
tokenBuffer += char
|
||||
continue
|
||||
|
||||
# Flush the non-operator token buffer when an operator is
|
||||
# encountered.
|
||||
if tokenBuffer:
|
||||
self.expression.append(tokenBuffer)
|
||||
tokenBuffer = ""
|
||||
|
||||
match char:
|
||||
case "(":
|
||||
operators.append(char)
|
||||
case ")":
|
||||
if "(" not in operators:
|
||||
raise RuntimeError("mismatched parentheses in expression")
|
||||
while (op := operators.pop()) != "(":
|
||||
self.expression.append(_OPERATORS[op])
|
||||
case _:
|
||||
precedence: int = _OPERATORS[char].precedence
|
||||
|
||||
while operators:
|
||||
op: str = operators[-1]
|
||||
|
||||
if op == "(":
|
||||
break
|
||||
if _OPERATORS[op].precedence < precedence:
|
||||
break
|
||||
|
||||
self.expression.append(_OPERATORS[op])
|
||||
operators.pop()
|
||||
|
||||
operators.append(char)
|
||||
|
||||
if tokenBuffer:
|
||||
self.expression.append(tokenBuffer)
|
||||
tokenBuffer = ""
|
||||
|
||||
if "(" in operators:
|
||||
raise RuntimeError("mismatched parentheses in expression")
|
||||
while operators:
|
||||
self.expression.append(_OPERATORS[operators.pop()])
|
||||
|
||||
def evaluate(self, variables: Mapping[str, bool]) -> bool:
|
||||
values: dict[str, bool] = { "0": False, "1": True, **variables }
|
||||
stack: deque[bool] = deque()
|
||||
|
||||
for token in self.expression:
|
||||
if isinstance(token, str):
|
||||
value: bool | None = values.get(token)
|
||||
|
||||
if value is None:
|
||||
raise RuntimeError(f"unknown variable '{token}'")
|
||||
|
||||
stack.append(value)
|
||||
else:
|
||||
token.execute(stack)
|
||||
|
||||
if len(stack) != 1:
|
||||
raise RuntimeError("invalid or malformed expression")
|
||||
|
||||
return stack[0]
|
||||
|
||||
## Logic lookup table conversion
|
||||
|
||||
def generateLUTFromExpression(expression: str, inputs: Sequence[str]) -> int:
|
||||
lut: int = 0
|
||||
|
||||
function: BooleanFunction = BooleanFunction(expression)
|
||||
variables: dict[str, bool] = {}
|
||||
|
||||
for index in range(1 << len(inputs)):
|
||||
for bit, name in enumerate(inputs):
|
||||
variables[name] = bool((index >> bit) & 1)
|
||||
|
||||
if function.evaluate(variables):
|
||||
lut |= 1 << index # LSB-first
|
||||
|
||||
return lut
|
||||
|
||||
def generateExpressionFromLUT(lut: int, inputs: Sequence[str]) -> str:
|
||||
products: list[str] = []
|
||||
|
||||
for index in range(1 << len(inputs)):
|
||||
values: str = "*".join(
|
||||
(value if (index >> bit) & 1 else f"~{value}")
|
||||
for bit, value in enumerate(inputs)
|
||||
)
|
||||
|
||||
if (lut >> index) & 1:
|
||||
products.append(f"({values})")
|
||||
|
||||
return "+".join(products) or "0"
|
||||
|
@ -5,3 +5,4 @@
|
||||
lz4 >= 4.3.2
|
||||
numpy >= 1.19.4
|
||||
Pillow >= 8.2.0
|
||||
pycdlib >= 1.14.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user