diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3bc1a33..dcb82360 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,6 +110,28 @@ jobs: path: ./git-artifcats/Dusk 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: runs-on: ubuntu-latest steps: @@ -124,10 +146,32 @@ jobs: mkdir -p ./git-artifcats/Dusk/apps/Dusk 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 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 uses: actions/upload-artifact@v6 with: name: dusk-wii 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 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a660a3e..ddd7eaae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,12 @@ cmake_policy(SET CMP0079 NEW) 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 set(DUSK_CACHE_TARGET "dusk-target") @@ -74,6 +80,10 @@ endif() target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC 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 diff --git a/cmake/targets/dolphin.cmake b/cmake/targets/dolphin.cmake index 0d31dcee..a8cd9ddf 100644 --- a/cmake/targets/dolphin.cmake +++ b/cmake/targets/dolphin.cmake @@ -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_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_DOLPHIN @@ -5,8 +9,10 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_DISPLAY_WIDTH=640 DUSK_DISPLAY_HEIGHT=480 DUSK_THREAD_PTHREAD + DUSK_DOLPHIN_BUILD_TYPE="${DUSK_DOLPHIN_BUILD_TYPE}" ) + # Custom compiler flags 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 cglm m - fat 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") add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD COMMAND elf2dol diff --git a/cmake/targets/gamecube.cmake b/cmake/targets/gamecube.cmake index 269b60d6..f8234be9 100644 --- a/cmake/targets/gamecube.cmake +++ b/cmake/targets/gamecube.cmake @@ -8,3 +8,17 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE # 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() diff --git a/cmake/targets/wii.cmake b/cmake/targets/wii.cmake index 83fedd4a..af29731b 100644 --- a/cmake/targets/wii.cmake +++ b/cmake/targets/wii.cmake @@ -2,4 +2,26 @@ include(cmake/targets/dolphin.cmake) target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_WII -) \ No newline at end of file +) + +# 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() diff --git a/docker/dolphin/meta.xml b/docker/dolphin/meta.xml deleted file mode 100644 index d9c45e05..00000000 --- a/docker/dolphin/meta.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - Dusk - 1.00 - - YouWish - Dusk game - No description yet. - - diff --git a/docker/dolphin/meta.xml.in b/docker/dolphin/meta.xml.in new file mode 100644 index 00000000..ee6a7e3c --- /dev/null +++ b/docker/dolphin/meta.xml.in @@ -0,0 +1,10 @@ + + + @DUSK_GAME_NAME@ + @PROJECT_VERSION@ + @DUSK_BUILD_DATE@ + @DUSK_GAME_AUTHOR@ + @DUSK_GAME_SHORT_DESCRIPTION@ + @DUSK_GAME_LONG_DESCRIPTION@ + + diff --git a/scripts/build-gamecube-iso-docker.sh b/scripts/build-gamecube-iso-docker.sh new file mode 100755 index 00000000..b5340f76 --- /dev/null +++ b/scripts/build-gamecube-iso-docker.sh @@ -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" diff --git a/scripts/build-gamecube-iso.sh b/scripts/build-gamecube-iso.sh new file mode 100755 index 00000000..b04180d6 --- /dev/null +++ b/scripts/build-gamecube-iso.sh @@ -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 diff --git a/scripts/build-wii-iso-docker.sh b/scripts/build-wii-iso-docker.sh new file mode 100755 index 00000000..043a4bb1 --- /dev/null +++ b/scripts/build-wii-iso-docker.sh @@ -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" diff --git a/scripts/build-wii-iso.sh b/scripts/build-wii-iso.sh new file mode 100755 index 00000000..04b084e3 --- /dev/null +++ b/scripts/build-wii-iso.sh @@ -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 diff --git a/src/duskdolphin/asset/CMakeLists.txt b/src/duskdolphin/asset/CMakeLists.txt index ac94ce41..32883cf8 100644 --- a/src/duskdolphin/asset/CMakeLists.txt +++ b/src/duskdolphin/asset/CMakeLists.txt @@ -1,11 +1,16 @@ # Copyright (c) 2026 Dominic Masters -# +# # This software is released under the MIT License. # https://opensource.org/licenses/MIT -# Sources - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - assetdolphin.c -) \ No newline at end of file +if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO") + target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + assetdolphindvd.c + ) +else() + target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + assetdolphinfat.c + ) +endif() diff --git a/src/duskdolphin/asset/assetdolphindvd.c b/src/duskdolphin/asset/assetdolphindvd.c new file mode 100644 index 00000000..ff5c3bff --- /dev/null +++ b/src/duskdolphin/asset/assetdolphindvd.c @@ -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 +#include +#include + +#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(); +} diff --git a/src/duskdolphin/asset/assetdolphindvd.h b/src/duskdolphin/asset/assetdolphindvd.h new file mode 100644 index 00000000..75b0fbaa --- /dev/null +++ b/src/duskdolphin/asset/assetdolphindvd.h @@ -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); diff --git a/src/duskdolphin/asset/assetdolphin.c b/src/duskdolphin/asset/assetdolphinfat.c similarity index 71% rename from src/duskdolphin/asset/assetdolphin.c rename to src/duskdolphin/asset/assetdolphinfat.c index f441ece3..5d64172c 100644 --- a/src/duskdolphin/asset/assetdolphin.c +++ b/src/duskdolphin/asset/assetdolphinfat.c @@ -1,10 +1,11 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +#include "assetdolphinfat.h" #include "asset/asset.h" #include "util/string.h" #include @@ -17,27 +18,21 @@ #include errorret_t assetInitDolphin(void) { - // Init FAT driver. if(!fatInitDefault()) errorThrow("Failed to initialize FAT filesystem."); char_t **dolphinSearchPath = (char_t **)ASSET_DOLPHIN_PATHS; char_t foundPath[ASSET_FILE_PATH_MAX]; foundPath[0] = '\0'; do { - // Try open dir DIR *pdir = opendir(*dolphinSearchPath); if(pdir == NULL) continue; - // Scan if file is present while(true) { struct dirent* pent = readdir(pdir); if(pent == NULL) break; - if(stringCompareInsensitive(pent->d_name, ASSET_FILE_NAME) != 0) { - continue; - } + if(stringCompareInsensitive(pent->d_name, ASSET_FILE_NAME) != 0) continue; - // Copy out filename snprintf( foundPath, ASSET_FILE_PATH_MAX, @@ -47,27 +42,19 @@ errorret_t assetInitDolphin(void) { ); break; } - - // Close dir. - closedir(pdir); - // Did we find the file here? + closedir(pdir); if(foundPath[0] != '\0') break; } 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); - if(ASSET.zip == NULL) { - errorThrow("Failed to open asset file on FAT filesystem."); - } + if(ASSET.zip == NULL) errorThrow("Failed to open asset file on FAT filesystem."); + errorOk(); } -errorret_t assetDisposeDolphin() { - // Nothing doing. +errorret_t assetDisposeDolphin(void) { errorOk(); -} \ No newline at end of file +} diff --git a/src/duskdolphin/asset/assetdolphin.h b/src/duskdolphin/asset/assetdolphinfat.h similarity index 62% rename from src/duskdolphin/asset/assetdolphin.h rename to src/duskdolphin/asset/assetdolphinfat.h index f55dc81f..6b418b47 100644 --- a/src/duskdolphin/asset/assetdolphin.h +++ b/src/duskdolphin/asset/assetdolphinfat.h @@ -1,13 +1,12 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #pragma once #include "error/error.h" -#include "asset/assetfile.h" static const char_t *ASSET_DOLPHIN_PATHS[] = { "/", @@ -34,16 +33,5 @@ typedef struct { uint8_t nothing; } assetdolphin_t; -/** - * Initializes the Dolphin asset system. - * - * @return An error code indicating success or failure. - */ errorret_t assetInitDolphin(void); - -/** - * Disposes of the Dolphin asset system, freeing any allocated resources. - * - * @return An error code indicating success or failure. - */ -errorret_t assetDisposeDolphin(void); \ No newline at end of file +errorret_t assetDisposeDolphin(void); diff --git a/src/duskdolphin/asset/assetplatform.h b/src/duskdolphin/asset/assetplatform.h index a49bd838..d728ebc9 100644 --- a/src/duskdolphin/asset/assetplatform.h +++ b/src/duskdolphin/asset/assetplatform.h @@ -1,14 +1,19 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #pragma once -#include "assetdolphin.h" + +#ifdef DUSK_DOLPHIN_BUILD_ISO + #include "assetdolphindvd.h" +#else + #include "assetdolphinfat.h" +#endif #define assetInitPlatform assetInitDolphin #define assetDisposePlatform assetDisposeDolphin -typedef assetdolphin_t assetplatform_t; \ No newline at end of file +typedef assetdolphin_t assetplatform_t; diff --git a/tools/makedolphiniso.py b/tools/makedolphiniso.py new file mode 100644 index 00000000..c7ae7258 --- /dev/null +++ b/tools/makedolphiniso.py @@ -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) + "\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()