ISO build (partial)
This commit is contained in:
@@ -110,6 +110,28 @@ jobs:
|
|||||||
path: ./git-artifcats/Dusk
|
path: ./git-artifcats/Dusk
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-gamecube-iso:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Set up Docker
|
||||||
|
uses: docker/setup-docker-action@v5
|
||||||
|
- name: Build GameCube ISO
|
||||||
|
run: ./scripts/build-gamecube-iso-docker.sh
|
||||||
|
- name: Copy output files.
|
||||||
|
run: |
|
||||||
|
mkdir -p ./git-artifcats/Dusk
|
||||||
|
cp build-gamecube-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso
|
||||||
|
cp build-gamecube-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso
|
||||||
|
cp build-gamecube-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso
|
||||||
|
- name: Upload GameCube ISO
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: dusk-gamecube-iso
|
||||||
|
path: ./git-artifcats/Dusk
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
build-wii:
|
build-wii:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -124,10 +146,32 @@ jobs:
|
|||||||
mkdir -p ./git-artifcats/Dusk/apps/Dusk
|
mkdir -p ./git-artifcats/Dusk/apps/Dusk
|
||||||
cp build-wii/Dusk.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
|
cp build-wii/Dusk.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
|
||||||
cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk
|
cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk
|
||||||
cp docker/dolphin/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
|
cp build-wii/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
|
||||||
- name: Upload Wii binary
|
- name: Upload Wii binary
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: dusk-wii
|
name: dusk-wii
|
||||||
path: ./git-artifcats/Dusk
|
path: ./git-artifcats/Dusk
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-wii-iso:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
- name: Set up Docker
|
||||||
|
uses: docker/setup-docker-action@v5
|
||||||
|
- name: Build Wii ISO
|
||||||
|
run: ./scripts/build-wii-iso-docker.sh
|
||||||
|
- name: Copy output files.
|
||||||
|
run: |
|
||||||
|
mkdir -p ./git-artifcats/Dusk
|
||||||
|
cp build-wii-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso
|
||||||
|
cp build-wii-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso
|
||||||
|
cp build-wii-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso
|
||||||
|
- name: Upload Wii ISO
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: dusk-wii-iso
|
||||||
|
path: ./git-artifcats/Dusk
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -14,6 +14,12 @@ cmake_policy(SET CMP0079 NEW)
|
|||||||
|
|
||||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||||
|
|
||||||
|
# Game identity — override these per-project
|
||||||
|
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||||
|
set(DUSK_GAME_AUTHOR "YouWish" CACHE STRING "Game author / coder")
|
||||||
|
set(DUSK_GAME_SHORT_DESCRIPTION "Dusk game" CACHE STRING "One-line description")
|
||||||
|
set(DUSK_GAME_LONG_DESCRIPTION "No description yet." CACHE STRING "Full description")
|
||||||
|
|
||||||
# Prep cache
|
# Prep cache
|
||||||
set(DUSK_CACHE_TARGET "dusk-target")
|
set(DUSK_CACHE_TARGET "dusk-target")
|
||||||
|
|
||||||
@@ -74,6 +80,10 @@ endif()
|
|||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
DUSK_TARGET_SYSTEM="${DUSK_TARGET_SYSTEM}"
|
DUSK_TARGET_SYSTEM="${DUSK_TARGET_SYSTEM}"
|
||||||
|
DUSK_GAME_NAME="${DUSK_GAME_NAME}"
|
||||||
|
DUSK_GAME_AUTHOR="${DUSK_GAME_AUTHOR}"
|
||||||
|
DUSK_GAME_SHORT_DESCRIPTION="${DUSK_GAME_SHORT_DESCRIPTION}"
|
||||||
|
DUSK_GAME_LONG_DESCRIPTION="${DUSK_GAME_LONG_DESCRIPTION}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Toolchains
|
# Toolchains
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# Build type: FAT (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver)
|
||||||
|
set(DUSK_DOLPHIN_BUILD_TYPE "FAT" CACHE STRING "Dolphin asset source: FAT (SD/USB) or ISO (DVD disc)")
|
||||||
|
set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "FAT" "ISO")
|
||||||
|
|
||||||
# Target definitions
|
# Target definitions
|
||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
DUSK_DOLPHIN
|
DUSK_DOLPHIN
|
||||||
@@ -5,8 +9,10 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_DISPLAY_WIDTH=640
|
DUSK_DISPLAY_WIDTH=640
|
||||||
DUSK_DISPLAY_HEIGHT=480
|
DUSK_DISPLAY_HEIGHT=480
|
||||||
DUSK_THREAD_PTHREAD
|
DUSK_THREAD_PTHREAD
|
||||||
|
DUSK_DOLPHIN_BUILD_TYPE="${DUSK_DOLPHIN_BUILD_TYPE}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Custom compiler flags
|
# Custom compiler flags
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions")
|
||||||
|
|
||||||
@@ -26,11 +32,16 @@ find_package(cglm REQUIRED)
|
|||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
||||||
cglm
|
cglm
|
||||||
m
|
m
|
||||||
fat
|
|
||||||
PkgConfig::zip
|
PkgConfig::zip
|
||||||
)
|
)
|
||||||
|
|
||||||
# Postbuild
|
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
||||||
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_DOLPHIN_BUILD_ISO)
|
||||||
|
else()
|
||||||
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE fat)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Postbuild: ELF -> DOL
|
||||||
set(DUSK_BINARY_TARGET_NAME_DOL "${DUSK_BUILD_DIR}/Dusk.dol")
|
set(DUSK_BINARY_TARGET_NAME_DOL "${DUSK_BUILD_DIR}/Dusk.dol")
|
||||||
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
|
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
|
||||||
COMMAND elf2dol
|
COMMAND elf2dol
|
||||||
|
|||||||
@@ -8,3 +8,17 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
||||||
# bba
|
# bba
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
|
||||||
|
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
||||||
|
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
|
||||||
|
COMMAND ${Python3_EXECUTABLE}
|
||||||
|
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
|
||||||
|
"GCN"
|
||||||
|
"${DUSK_BINARY_TARGET_NAME_DOL}"
|
||||||
|
"${DUSK_ASSETS_ZIP}"
|
||||||
|
"${DUSK_GAME_NAME}"
|
||||||
|
"${DUSK_BUILD_DIR}"
|
||||||
|
COMMENT "Building GameCube ISO images (NTSC-J, NTSC-U, PAL)"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|||||||
+23
-1
@@ -2,4 +2,26 @@ include(cmake/targets/dolphin.cmake)
|
|||||||
|
|
||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
DUSK_WII
|
DUSK_WII
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generate Homebrew Channel meta.xml from project identity variables
|
||||||
|
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
||||||
|
configure_file(
|
||||||
|
"${CMAKE_SOURCE_DIR}/docker/dolphin/meta.xml.in"
|
||||||
|
"${DUSK_BUILD_DIR}/meta.xml"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
|
||||||
|
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
|
||||||
|
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
||||||
|
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
|
||||||
|
COMMAND ${Python3_EXECUTABLE}
|
||||||
|
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
|
||||||
|
"WII"
|
||||||
|
"${DUSK_BINARY_TARGET_NAME_DOL}"
|
||||||
|
"${DUSK_ASSETS_ZIP}"
|
||||||
|
"${DUSK_GAME_NAME}"
|
||||||
|
"${DUSK_BUILD_DIR}"
|
||||||
|
COMMENT "Building Wii ISO images (NTSC-J, NTSC-U, PAL)"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<app version="1">
|
|
||||||
<name>Dusk</name>
|
|
||||||
<version>1.00</version>
|
|
||||||
<release_date></release_date>
|
|
||||||
<coder>YouWish</coder>
|
|
||||||
<short_description>Dusk game</short_description>
|
|
||||||
<long_description>No description yet.</long_description>
|
|
||||||
<ahb_access/>
|
|
||||||
</app>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<app version="1">
|
||||||
|
<name>@DUSK_GAME_NAME@</name>
|
||||||
|
<version>@PROJECT_VERSION@</version>
|
||||||
|
<release_date>@DUSK_BUILD_DATE@</release_date>
|
||||||
|
<coder>@DUSK_GAME_AUTHOR@</coder>
|
||||||
|
<short_description>@DUSK_GAME_SHORT_DESCRIPTION@</short_description>
|
||||||
|
<long_description>@DUSK_GAME_LONG_DESCRIPTION@</long_description>
|
||||||
|
<ahb_access/>
|
||||||
|
</app>
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
||||||
|
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ -z "$DEVKITPRO" ]; then
|
||||||
|
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p build-gamecube-iso
|
||||||
|
cmake -S. -Bbuild-gamecube-iso \
|
||||||
|
-DDUSK_TARGET_SYSTEM=gamecube \
|
||||||
|
-DDUSK_DOLPHIN_BUILD_TYPE=ISO \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake"
|
||||||
|
cd build-gamecube-iso
|
||||||
|
make -j$(nproc) VERBOSE=1
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
|
||||||
|
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ -z "$DEVKITPRO" ]; then
|
||||||
|
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p build-wii-iso
|
||||||
|
cmake -S. -Bbuild-wii-iso \
|
||||||
|
-DDUSK_TARGET_SYSTEM=wii \
|
||||||
|
-DDUSK_DOLPHIN_BUILD_TYPE=ISO \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake"
|
||||||
|
cd build-wii-iso
|
||||||
|
make -j$(nproc) VERBOSE=1
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
# Copyright (c) 2026 Dominic Masters
|
# Copyright (c) 2026 Dominic Masters
|
||||||
#
|
#
|
||||||
# This software is released under the MIT License.
|
# This software is released under the MIT License.
|
||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
# Sources
|
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
PUBLIC
|
||||||
PUBLIC
|
assetdolphindvd.c
|
||||||
assetdolphin.c
|
)
|
||||||
)
|
else()
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
assetdolphinfat.c
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "assetdolphindvd.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include <ogc/dvd.h>
|
||||||
|
#include <ogc/cache.h>
|
||||||
|
#include <malloc.h>
|
||||||
|
|
||||||
|
#define DUSK_DVD_ALIGN 32u
|
||||||
|
#define DUSK_DVD_ALIGN_UP(n) \
|
||||||
|
(((u32)(n) + DUSK_DVD_ALIGN - 1u) & ~(DUSK_DVD_ALIGN - 1u))
|
||||||
|
|
||||||
|
static u32 dvdBe32(const u8 *p) {
|
||||||
|
return ((u32)p[0] << 24) | ((u32)p[1] << 16) | ((u32)p[2] << 8) | (u32)p[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *dvdRead(s64 offset, u32 size) {
|
||||||
|
u32 padded = DUSK_DVD_ALIGN_UP(size);
|
||||||
|
void *buf = memalign(DUSK_DVD_ALIGN, padded);
|
||||||
|
if(!buf) return NULL;
|
||||||
|
DCInvalidateRange(buf, padded);
|
||||||
|
dvdcmdblk block;
|
||||||
|
if(DVD_ReadPrio(&block, buf, padded, offset, 0) <= 0) {
|
||||||
|
free(buf);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetInitDolphin(void) {
|
||||||
|
DVD_Init();
|
||||||
|
DVD_Mount();
|
||||||
|
|
||||||
|
// Read disc header to find FST location
|
||||||
|
u8 *hdr = (u8 *)dvdRead(0, 0x440);
|
||||||
|
if(!hdr) errorThrow("Failed to read DVD disc header.");
|
||||||
|
u32 fstOff = dvdBe32(hdr + 0x424);
|
||||||
|
u32 fstSize = dvdBe32(hdr + 0x428);
|
||||||
|
free(hdr);
|
||||||
|
|
||||||
|
// Read the FST
|
||||||
|
u8 *fst = (u8 *)dvdRead((s64)fstOff, fstSize);
|
||||||
|
if(!fst) errorThrow("Failed to read DVD FST.");
|
||||||
|
|
||||||
|
// Root entry (index 0) bytes 8-11 = total entry count
|
||||||
|
u32 numEntries = dvdBe32(fst + 8);
|
||||||
|
u8 *strTable = fst + numEntries * 12u;
|
||||||
|
|
||||||
|
u32 fileOff = 0, fileLen = 0;
|
||||||
|
for(u32 i = 1; i < numEntries; i++) {
|
||||||
|
u8 *e = fst + i * 12u;
|
||||||
|
if(e[0] != 0) continue;
|
||||||
|
|
||||||
|
u32 nameOff = ((u32)e[1] << 16) | ((u32)e[2] << 8) | (u32)e[3];
|
||||||
|
const char_t *name = (const char_t *)(strTable + nameOff);
|
||||||
|
if(stringCompareInsensitive(name, ASSET_FILE_NAME) == 0) {
|
||||||
|
fileOff = dvdBe32(e + 4);
|
||||||
|
fileLen = dvdBe32(e + 8);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(fst);
|
||||||
|
|
||||||
|
if(!fileOff) errorThrow("Failed to find asset file on DVD.");
|
||||||
|
|
||||||
|
u8 *data = (u8 *)dvdRead((s64)fileOff, fileLen);
|
||||||
|
if(!data) errorThrow("Failed to read asset file from DVD.");
|
||||||
|
|
||||||
|
zip_error_t zerr;
|
||||||
|
zip_source_t *src = zip_source_buffer_create(data, fileLen, 1, &zerr);
|
||||||
|
if(!src) {
|
||||||
|
free(data);
|
||||||
|
errorThrow("Failed to create zip source from DVD buffer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSET.zip = zip_open_from_source(src, ZIP_RDONLY, &zerr);
|
||||||
|
if(!ASSET.zip) {
|
||||||
|
zip_source_free(src);
|
||||||
|
errorThrow("Failed to open asset zip from DVD.");
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetDisposeDolphin(void) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t nothing;
|
||||||
|
} assetdolphin_t;
|
||||||
|
|
||||||
|
errorret_t assetInitDolphin(void);
|
||||||
|
errorret_t assetDisposeDolphin(void);
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2026 Dominic Masters
|
* Copyright (c) 2026 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "assetdolphinfat.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include <fat.h>
|
#include <fat.h>
|
||||||
@@ -17,27 +18,21 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
errorret_t assetInitDolphin(void) {
|
errorret_t assetInitDolphin(void) {
|
||||||
// Init FAT driver.
|
|
||||||
if(!fatInitDefault()) errorThrow("Failed to initialize FAT filesystem.");
|
if(!fatInitDefault()) errorThrow("Failed to initialize FAT filesystem.");
|
||||||
|
|
||||||
char_t **dolphinSearchPath = (char_t **)ASSET_DOLPHIN_PATHS;
|
char_t **dolphinSearchPath = (char_t **)ASSET_DOLPHIN_PATHS;
|
||||||
char_t foundPath[ASSET_FILE_PATH_MAX];
|
char_t foundPath[ASSET_FILE_PATH_MAX];
|
||||||
foundPath[0] = '\0';
|
foundPath[0] = '\0';
|
||||||
do {
|
do {
|
||||||
// Try open dir
|
|
||||||
DIR *pdir = opendir(*dolphinSearchPath);
|
DIR *pdir = opendir(*dolphinSearchPath);
|
||||||
if(pdir == NULL) continue;
|
if(pdir == NULL) continue;
|
||||||
|
|
||||||
// Scan if file is present
|
|
||||||
while(true) {
|
while(true) {
|
||||||
struct dirent* pent = readdir(pdir);
|
struct dirent* pent = readdir(pdir);
|
||||||
if(pent == NULL) break;
|
if(pent == NULL) break;
|
||||||
|
|
||||||
if(stringCompareInsensitive(pent->d_name, ASSET_FILE_NAME) != 0) {
|
if(stringCompareInsensitive(pent->d_name, ASSET_FILE_NAME) != 0) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy out filename
|
|
||||||
snprintf(
|
snprintf(
|
||||||
foundPath,
|
foundPath,
|
||||||
ASSET_FILE_PATH_MAX,
|
ASSET_FILE_PATH_MAX,
|
||||||
@@ -47,27 +42,19 @@ errorret_t assetInitDolphin(void) {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close dir.
|
|
||||||
closedir(pdir);
|
|
||||||
|
|
||||||
// Did we find the file here?
|
closedir(pdir);
|
||||||
if(foundPath[0] != '\0') break;
|
if(foundPath[0] != '\0') break;
|
||||||
} while(*(++dolphinSearchPath) != NULL);
|
} while(*(++dolphinSearchPath) != NULL);
|
||||||
|
|
||||||
// Did we find the asset file?
|
if(foundPath[0] == '\0') errorThrow("Failed to find asset file on FAT filesystem.");
|
||||||
if(foundPath[0] == '\0') {
|
|
||||||
errorThrow("Failed to find asset file on FAT filesystem.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ASSET.zip = zip_open(foundPath, ZIP_RDONLY, NULL);
|
ASSET.zip = zip_open(foundPath, ZIP_RDONLY, NULL);
|
||||||
if(ASSET.zip == NULL) {
|
if(ASSET.zip == NULL) errorThrow("Failed to open asset file on FAT filesystem.");
|
||||||
errorThrow("Failed to open asset file on FAT filesystem.");
|
|
||||||
}
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t assetDisposeDolphin() {
|
errorret_t assetDisposeDolphin(void) {
|
||||||
// Nothing doing.
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2026 Dominic Masters
|
* Copyright (c) 2026 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "asset/assetfile.h"
|
|
||||||
|
|
||||||
static const char_t *ASSET_DOLPHIN_PATHS[] = {
|
static const char_t *ASSET_DOLPHIN_PATHS[] = {
|
||||||
"/",
|
"/",
|
||||||
@@ -34,16 +33,5 @@ typedef struct {
|
|||||||
uint8_t nothing;
|
uint8_t nothing;
|
||||||
} assetdolphin_t;
|
} assetdolphin_t;
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the Dolphin asset system.
|
|
||||||
*
|
|
||||||
* @return An error code indicating success or failure.
|
|
||||||
*/
|
|
||||||
errorret_t assetInitDolphin(void);
|
errorret_t assetInitDolphin(void);
|
||||||
|
errorret_t assetDisposeDolphin(void);
|
||||||
/**
|
|
||||||
* Disposes of the Dolphin asset system, freeing any allocated resources.
|
|
||||||
*
|
|
||||||
* @return An error code indicating success or failure.
|
|
||||||
*/
|
|
||||||
errorret_t assetDisposeDolphin(void);
|
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2026 Dominic Masters
|
* Copyright (c) 2026 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "assetdolphin.h"
|
|
||||||
|
#ifdef DUSK_DOLPHIN_BUILD_ISO
|
||||||
|
#include "assetdolphindvd.h"
|
||||||
|
#else
|
||||||
|
#include "assetdolphinfat.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#define assetInitPlatform assetInitDolphin
|
#define assetInitPlatform assetInitDolphin
|
||||||
#define assetDisposePlatform assetDisposeDolphin
|
#define assetDisposePlatform assetDisposeDolphin
|
||||||
|
|
||||||
typedef assetdolphin_t assetplatform_t;
|
typedef assetdolphin_t assetplatform_t;
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Assembles minimal GameCube / Wii disc images (NTSC-J, NTSC-U, PAL) from a
|
||||||
|
.dol executable and an asset .dsk file.
|
||||||
|
|
||||||
|
Disc layout
|
||||||
|
0x000000 boot.bin (disc header, 0x440 bytes)
|
||||||
|
0x000440 bi2.bin (region info, 0x2000 bytes)
|
||||||
|
0x002440 apploader (stub, padded to 0x8000-byte boundary)
|
||||||
|
0x008000 DOL (padded to 0x8000-byte boundary)
|
||||||
|
... FST (file system table, padded to 0x8000-byte boundary)
|
||||||
|
... dusk.dsk (asset archive)
|
||||||
|
|
||||||
|
The apploader is a functional stub suitable for ODE devices (GCLoader etc.)
|
||||||
|
and disc backup systems. It is NOT suitable for booting on bare modchip
|
||||||
|
hardware — for that you need a real apploader extracted from a retail disc.
|
||||||
|
|
||||||
|
Wii discs use the same basic structure but with the Wii magic number. The
|
||||||
|
full Wii partition/encryption layer is not implemented here; the produced
|
||||||
|
images work in Dolphin emulator and on Wii ODE devices with GCN-compat mode.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
GCN_MAGIC = 0xC2339F3D
|
||||||
|
WII_MAGIC = 0x5D1C9EA3
|
||||||
|
SECTOR = 0x8000 # 32 KB — standard disc layout alignment
|
||||||
|
|
||||||
|
REGIONS = [
|
||||||
|
('J', 'NTSC-J', 0),
|
||||||
|
('E', 'NTSC-U', 1),
|
||||||
|
('P', 'PAL', 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def align_up(value, boundary):
|
||||||
|
return (value + boundary - 1) & ~(boundary - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def be32(value):
|
||||||
|
return struct.pack('>I', value & 0xFFFFFFFF)
|
||||||
|
|
||||||
|
|
||||||
|
def make_bi2(region_code):
|
||||||
|
bi2 = bytearray(0x2000)
|
||||||
|
struct.pack_into('>I', bi2, 0x18, region_code)
|
||||||
|
return bytes(bi2)
|
||||||
|
|
||||||
|
|
||||||
|
def make_apploader():
|
||||||
|
"""
|
||||||
|
Minimal apploader stub. The IPL calls three entry points in order:
|
||||||
|
init(report_fn) -> void
|
||||||
|
main(&dst, &size, &offset) -> bool (0 = no more sections)
|
||||||
|
close() -> entry_point address
|
||||||
|
|
||||||
|
PPC code (big-endian):
|
||||||
|
blr 4E 80 00 20 - init: return immediately
|
||||||
|
li r3, 0 38 60 00 00 - main: return 0 (done)
|
||||||
|
blr 4E 80 00 20
|
||||||
|
lis r3, 0x8130 - close: return stub entry point
|
||||||
|
blr 4E 80 00 20
|
||||||
|
"""
|
||||||
|
entry_addr = 0x81300000 # stub entry point for close()
|
||||||
|
code = bytes([
|
||||||
|
0x4E, 0x80, 0x00, 0x20, # init: blr
|
||||||
|
0x38, 0x60, 0x00, 0x00, # main: li r3, 0
|
||||||
|
0x4E, 0x80, 0x00, 0x20, # blr
|
||||||
|
0x3C, 0x60, 0x81, 0x30, # close: lis r3, 0x8130
|
||||||
|
0x4E, 0x80, 0x00, 0x20, # blr
|
||||||
|
])
|
||||||
|
hdr = bytearray(0x20)
|
||||||
|
hdr[0:16] = b'2000/01/01 00:00'
|
||||||
|
struct.pack_into('>I', hdr, 0x10, entry_addr)
|
||||||
|
struct.pack_into('>I', hdr, 0x14, len(code))
|
||||||
|
# trailer_size and pad remain zero
|
||||||
|
return bytes(hdr) + code
|
||||||
|
|
||||||
|
|
||||||
|
def make_fst(files):
|
||||||
|
"""
|
||||||
|
Build a flat GCN FST for a list of (name, disc_offset, size) tuples
|
||||||
|
all sitting in the root directory.
|
||||||
|
|
||||||
|
FST entry layout (12 bytes, big-endian):
|
||||||
|
byte 0 : type (0 = file, 1 = directory)
|
||||||
|
bytes 1-3 : name_offset into string table
|
||||||
|
bytes 4-7 : file_offset (files) / parent_dir_index (dirs)
|
||||||
|
bytes 8-11 : file_size (files) / next_index (dirs)
|
||||||
|
"""
|
||||||
|
num_entries = 1 + len(files) # root + files
|
||||||
|
entry_data = bytearray(num_entries * 12)
|
||||||
|
string_table = bytearray(b'\x00') # offset 0 = empty string (root name)
|
||||||
|
|
||||||
|
# Root directory entry
|
||||||
|
struct.pack_into('>I', entry_data, 0, 0x01000000) # type=1, name_off=0
|
||||||
|
struct.pack_into('>I', entry_data, 4, 0) # parent = 0
|
||||||
|
struct.pack_into('>I', entry_data, 8, num_entries) # total entries
|
||||||
|
|
||||||
|
for i, (name, disc_off, size) in enumerate(files, start=1):
|
||||||
|
name_off = len(string_table)
|
||||||
|
string_table += name.encode('ascii') + b'\x00'
|
||||||
|
base = i * 12
|
||||||
|
struct.pack_into('>I', entry_data, base, (0 << 24) | (name_off & 0xFFFFFF))
|
||||||
|
struct.pack_into('>I', entry_data, base + 4, disc_off)
|
||||||
|
struct.pack_into('>I', entry_data, base + 8, size)
|
||||||
|
|
||||||
|
return bytes(entry_data) + bytes(string_table)
|
||||||
|
|
||||||
|
|
||||||
|
def build_disc(platform, game_id, title, region_code, dol_data, dsk_data, dsk_name):
|
||||||
|
magic = GCN_MAGIC if platform == 'GCN' else WII_MAGIC
|
||||||
|
apploader = make_apploader()
|
||||||
|
bi2 = make_bi2(region_code)
|
||||||
|
dsk_filename = os.path.basename(dsk_name)
|
||||||
|
|
||||||
|
# compute disc layout offsets
|
||||||
|
apploader_off = 0x2440
|
||||||
|
dol_off = align_up(apploader_off + len(apploader), SECTOR)
|
||||||
|
fst_off = align_up(dol_off + len(dol_data), SECTOR)
|
||||||
|
|
||||||
|
# Pre-compute FST size so we can place the data file after it.
|
||||||
|
# String table: "\x00" (root) + "<filename>\x00"
|
||||||
|
fst_str_size = 1 + len(dsk_filename) + 1
|
||||||
|
fst_size = 2 * 12 + fst_str_size # 2 entries * 12 bytes + strings
|
||||||
|
|
||||||
|
dsk_off = align_up(fst_off + fst_size, SECTOR)
|
||||||
|
|
||||||
|
# build FST with final offsets
|
||||||
|
fst_data = make_fst([(dsk_filename, dsk_off, len(dsk_data))])
|
||||||
|
assert len(fst_data) == fst_size, \
|
||||||
|
f"FST size mismatch: expected {fst_size}, got {len(fst_data)}"
|
||||||
|
|
||||||
|
# build disc header (boot.bin, 0x440 bytes)
|
||||||
|
boot = bytearray(0x440)
|
||||||
|
boot[0:6] = game_id.encode('ascii')
|
||||||
|
# bytes 6-7: disc number / version = 0
|
||||||
|
# bytes 8-9: audio streaming / buffer size = 0
|
||||||
|
struct.pack_into('>I', boot, 0x01C, magic)
|
||||||
|
title_bytes = title.encode('ascii')[:0x3E0]
|
||||||
|
boot[0x020:0x020 + len(title_bytes)] = title_bytes
|
||||||
|
struct.pack_into('>I', boot, 0x420, dol_off)
|
||||||
|
struct.pack_into('>I', boot, 0x424, fst_off)
|
||||||
|
struct.pack_into('>I', boot, 0x428, fst_size)
|
||||||
|
struct.pack_into('>I', boot, 0x42C, fst_size) # max FST size = actual size
|
||||||
|
|
||||||
|
# assemble disc image
|
||||||
|
disc_size = dsk_off + len(dsk_data)
|
||||||
|
disc = bytearray(disc_size)
|
||||||
|
|
||||||
|
disc[0x000:0x440] = boot
|
||||||
|
disc[0x440:0x2440] = bi2
|
||||||
|
disc[apploader_off:apploader_off + len(apploader)] = apploader
|
||||||
|
disc[dol_off:dol_off + len(dol_data)] = dol_data
|
||||||
|
disc[fst_off:fst_off + fst_size] = fst_data
|
||||||
|
disc[dsk_off:dsk_off + len(dsk_data)] = dsk_data
|
||||||
|
|
||||||
|
return bytes(disc)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(
|
||||||
|
description='Build GCN/Wii disc images from a DOL and asset file.')
|
||||||
|
ap.add_argument('platform', choices=['GCN', 'WII'],
|
||||||
|
help='Target platform')
|
||||||
|
ap.add_argument('dol', help='Path to Dusk.dol')
|
||||||
|
ap.add_argument('dsk', help='Path to dusk.dsk asset archive')
|
||||||
|
ap.add_argument('title', help='Game title string')
|
||||||
|
ap.add_argument('build_dir', help='Output directory')
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
with open(args.dol, 'rb') as f:
|
||||||
|
dol_data = f.read()
|
||||||
|
with open(args.dsk, 'rb') as f:
|
||||||
|
dsk_data = f.read()
|
||||||
|
|
||||||
|
id_prefix = 'G' if args.platform == 'GCN' else 'R'
|
||||||
|
|
||||||
|
for region_char, region_name, region_code in REGIONS:
|
||||||
|
game_id = f'{id_prefix}DK{region_char}01'
|
||||||
|
out_path = os.path.join(args.build_dir, f'Dusk-{region_name}.iso')
|
||||||
|
|
||||||
|
disc = build_disc(
|
||||||
|
args.platform, game_id, args.title,
|
||||||
|
region_code, dol_data, dsk_data, args.dsk,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(out_path, 'wb') as f:
|
||||||
|
f.write(disc)
|
||||||
|
|
||||||
|
print(f'Created {out_path} ({len(disc):,} bytes)')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user