diff --git a/.gitignore b/.gitignore index 802f88e9f..daffc5b48 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ compile_commands.json *~ *.log memcards/ + +ctr-native-beta-6_1-linux-x86/ + +ctr_native diff --git a/extract_assets.sh b/extract_assets.sh new file mode 100755 index 000000000..f74033875 --- /dev/null +++ b/extract_assets.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Extract CTR (NTSC-U) game assets from a retail disc image (.bin/.cue or .iso). +# XA audio files are extracted as raw 2352-byte sectors (required by ctr-native). +# All other files are extracted as standard 2048-byte ISO data. +# +# Usage: +# ./extract_assets.sh /path/to/CTR.bin (will look for matching .cue) +# ./extract_assets.sh /path/to/CTR.iso +# ./extract_assets.sh /path/to/CTR.bin ./assets (custom output dir) + +die() { echo "[ERROR] $*" >&2; exit 1; } +info() { echo "[INFO] $*"; } + +INPUT="${1:-}" +OUTPUT_DIR="${2:-assets}" + +[ -z "$INPUT" ] && die "Usage: $0 [output-dir]" +[ -f "$INPUT" ] || die "File not found: $INPUT" + +# --- Resolve ISO path --- + +ISO_PATH="" +CLEANUP_ISO=0 + +ext="${INPUT##*.}" +ext_lower="$(echo "$ext" | tr '[:upper:]' '[:lower:]')" + +if [ "$ext_lower" = "iso" ]; then + ISO_PATH="$INPUT" +elif [ "$ext_lower" = "bin" ]; then + # Need bchunk to convert bin/cue → iso + CUE_PATH="${INPUT%.*}.cue" + [ -f "$CUE_PATH" ] || CUE_PATH="${INPUT%.*}.CUE" + [ -f "$CUE_PATH" ] || die "Cannot find .cue file for: $INPUT (tried ${INPUT%.*}.cue)" + + if ! command -v bchunk &>/dev/null; then + info "bchunk not found, building from source..." + BCHUNK_DIR="$(mktemp -d)" + git clone --depth 1 https://github.com/hessu/bchunk.git "$BCHUNK_DIR/src" 2>/dev/null + make -C "$BCHUNK_DIR/src" -s + BCHUNK="$BCHUNK_DIR/src/bchunk" + else + BCHUNK="bchunk" + fi + + # bchunk takes an output PREFIX and appends "01.iso", "02.iso", etc. + ISO_PREFIX="$(mktemp -d)/ctr_track" + CLEANUP_ISO=1 + info "Converting BIN/CUE → ISO..." + "$BCHUNK" "$INPUT" "$CUE_PATH" "$ISO_PREFIX" >/dev/null 2>&1 || true + + ISO_PATH="${ISO_PREFIX}01.iso" + [ -f "$ISO_PATH" ] || die "bchunk conversion failed (expected ${ISO_PATH})" + info "ISO created: $ISO_PATH" +else + die "Unsupported format: .$ext (expected .bin or .iso)" +fi + +# --- Verify it's a valid CTR disc --- + +if ! command -v isoinfo &>/dev/null; then + die "isoinfo not found. Install genisoimage or cdrtools." +fi + +isoinfo -i "$ISO_PATH" -l >/dev/null 2>&1 || die "Not a valid ISO 9660 image: $ISO_PATH" +isoinfo -i "$ISO_PATH" -l 2>/dev/null | grep -q "BIGFILE.BIG" || die "BIGFILE.BIG not found — not a CTR disc image" + +info "Valid CTR disc image detected." + +# --- Extract standard files (2048-byte sectors) --- + +info "Extracting standard assets to $OUTPUT_DIR/ ..." +mkdir -p "$OUTPUT_DIR/SOUNDS" + +isoinfo -i "$ISO_PATH" -x "/BIGFILE.BIG;1" > "$OUTPUT_DIR/BIGFILE.BIG" +isoinfo -i "$ISO_PATH" -x "/SOUNDS/KART.HWL;1" > "$OUTPUT_DIR/SOUNDS/KART.HWL" +isoinfo -i "$ISO_PATH" -x "/TEST.STR;1" > "$OUTPUT_DIR/TEST.STR" + +# --- Extract XA files in raw 2352-byte sectors from the original BIN --- +# isoinfo strips CD subheaders, making XA audio unreadable. +# We must pull raw sectors directly from the disc image. + +info "Extracting XA audio files (raw 2352-byte sectors)..." + +python3 - "$INPUT" "$ISO_PATH" "$OUTPUT_DIR" << 'PYEOF' +import sys, os, subprocess, struct + +bin_path = sys.argv[1] +iso_path = sys.argv[2] +output_dir = sys.argv[3] + +RAW_SECTOR = 2352 +ISO_SECTOR = 2048 + +# If input is ISO (no raw sectors available), fall back to isoinfo extraction +# with a warning. Raw extraction only works from .bin files. +input_ext = os.path.splitext(bin_path)[1].lower() +use_raw = input_ext == ".bin" + +# Parse XA file locations from isoinfo +result = subprocess.run(['isoinfo', '-i', iso_path, '-l'], capture_output=True, text=True) +lines = result.stdout.splitlines() + +xa_files = [] +current_dir = "" +for line in lines: + if line.startswith("Directory listing of"): + current_dir = line.split("of ")[-1].strip() + elif ";1" in line and "/XA" in current_dir: + parts = line.split() + name = parts[-1].split(';')[0] + if not name.upper().endswith(".XA"): + continue + size = None + for p in parts: + if p.isdigit() and int(p) > 100: + size = int(p) + break + if size is None: + continue + bracket_start = line.index('[') + bracket_end = line.index(']') + extent = int(line[bracket_start+1:bracket_end].strip().split()[0]) + filepath = current_dir + name + xa_files.append((filepath, extent, size)) + +if not xa_files: + print("[WARN] No XA files found in disc image") + sys.exit(0) + +for filepath, lba, iso_size in xa_files: + rel_path = filepath.lstrip('/') + out_path = os.path.join(output_dir, rel_path) + os.makedirs(os.path.dirname(out_path), exist_ok=True) + + num_sectors = (iso_size + ISO_SECTOR - 1) // ISO_SECTOR + + if use_raw: + with open(bin_path, 'rb') as f: + raw_data = bytearray() + for s in range(num_sectors): + f.seek((lba + s) * RAW_SECTOR) + raw_data.extend(f.read(RAW_SECTOR)) + with open(out_path, 'wb') as out: + out.write(raw_data) + actual_size = len(raw_data) + else: + # Fallback: extract via isoinfo (no raw sectors — voices may not work) + iso_name = "/" + rel_path.replace("/", "/") + ";1" + result = subprocess.run(['isoinfo', '-i', iso_path, '-x', iso_name], + capture_output=True) + with open(out_path, 'wb') as out: + out.write(result.stdout) + actual_size = len(result.stdout) + + print(f" {rel_path} ({actual_size} bytes, {num_sectors} sectors" + + (" raw" if use_raw else " ISO-MODE WARNING: voices may not work") + ")") + +if not use_raw: + print("\n[WARN] Input was .iso — XA files extracted without raw CD headers.") + print(" Voices/music may not play. Use the original .bin for full audio.") +PYEOF + +# --- Also extract ENG.XNF (small index file, standard extraction is fine) --- + +mkdir -p "$OUTPUT_DIR/XA" +isoinfo -i "$ISO_PATH" -x "/XA/ENG.XNF;1" > "$OUTPUT_DIR/XA/ENG.XNF" + +# --- Cleanup --- + +[ "$CLEANUP_ISO" -eq 1 ] && rm -f "$ISO_PATH" + +# --- Verify --- + +info "Verifying extracted assets..." +MISSING=0 +for f in BIGFILE.BIG SOUNDS/KART.HWL TEST.STR XA/ENG.XNF; do + if [ ! -s "$OUTPUT_DIR/$f" ]; then + echo " MISSING: $f" + MISSING=1 + fi +done + +XA_COUNT=$(find "$OUTPUT_DIR/XA" -name "*.XA" | wc -l) +if [ "$XA_COUNT" -lt 28 ]; then + echo " WARNING: Only $XA_COUNT XA files found (expected 29)" + MISSING=1 +fi + +if [ "$MISSING" -eq 0 ]; then + info "All assets extracted successfully ($XA_COUNT XA files)." + info "Output directory: $OUTPUT_DIR/" + echo "" + echo "To use with ctr-native, ensure assets/ is next to the ctr_native binary:" + echo " ln -sf \$(realpath $OUTPUT_DIR) build/assets" +else + die "Some assets are missing — check disc image integrity." +fi diff --git a/game/230/D230.c b/game/230/D230.c index 22217e4ec..c727fbce4 100644 --- a/game/230/D230.c +++ b/game/230/D230.c @@ -380,7 +380,12 @@ struct OverlayDATA_230 D230 = { // Penta {0xE0, 0xCE, {5, 13, 12, 14}, 13, 0x6}, // Fake Crash - {0x120, 0xCE, {6, 14, 13, 14}, 14, 0xB}}, + {0x120, 0xCE, {6, 14, 13, 15}, 14, 0xB}, +#ifdef CTR_NATIVE + // Oxide + {0x160, 0xCE, {7, 15, 14, 15}, 15, 0xFFFF}, +#endif + }, .csm_1P2P = {// Crash @@ -406,13 +411,22 @@ struct OverlayDATA_230 D230 = { // Roo {0x40, 0x87, {8, 10, 10, 4}, 10, 0x7}, // Papu +#ifdef CTR_NATIVE + {0x180, 0x87, {9, 15, 7, 11}, 9, 0x8}, +#else {0x180, 0x87, {9, 11, 7, 11}, 9, 0x8}, +#endif // Komodo Joe {0xA0, 0xAE, {4, 12, 12, 13}, 11, 0x9}, // Penta {0xE0, 0xAE, {5, 13, 12, 14}, 13, 0x6}, // Fake Crash - {0x120, 0xAE, {6, 14, 13, 14}, 14, 0xB}}, + {0x120, 0xAE, {6, 14, 13, 15}, 14, 0xB}, +#ifdef CTR_NATIVE + // Oxide + {0x160, 0xAE, {11, 15, 14, 15}, 15, 0xFFFF}, +#endif + }, .csm_3P = {// Crash @@ -444,7 +458,12 @@ struct OverlayDATA_230 D230 = { // Penta {0x80, 0x20, {13, 2, 12, 14}, 13, 0x6}, // Fake Crash - {0xC0, 0x20, {14, 3, 13, 14}, 14, 0xB}}, + {0xC0, 0x20, {14, 3, 13, 15}, 14, 0xB}, +#ifdef CTR_NATIVE + // Oxide + {0x100, 0x20, {15, 3, 14, 15}, 15, 0xFFFF}, +#endif + }, .csm_4P = {// Crash @@ -476,7 +495,12 @@ struct OverlayDATA_230 D230 = { // Pinstripe {0x180, 0x6E, {11, 13, 7, 13}, 8, 0xA}, // Penta - {0xE0, 0x20, {14, 1, 14, 14}, 13, 0x6}}, + {0xE0, 0x20, {14, 1, 14, 15}, 13, 0x6}, +#ifdef CTR_NATIVE + // Oxide + {0x120, 0x20, {15, 2, 14, 15}, 15, 0xFFFF}, +#endif + }, .ptrCsmArr = {&D230.csm_1P2P[0], &D230.csm_1P2P[0], &D230.csm_3P[0], &D230.csm_4P[0], &D230.csm_1P2P_limited[0], &D230.csm_1P2P_limited[0]}, @@ -504,21 +528,33 @@ struct OverlayDATA_230 D230 = { { {0, 0xC8, 6, 0, 0}, {0, 0xC8, 5, 0, 0}, {0, 0xC8, 4, 0, 0}, {0, 0xC8, 3, 0, 0}, {0, 0xC8, 5, 0, 0}, {0, 0xC8, 4, 0, 0}, {0, 0xC8, 3, 0, 0}, {0, 0xC8, 2, 0, 0}, {0, 0xC8, 7, 0, 0}, {0, 0xC8, 1, 0, 0}, {0, 0xC8, 6, 0, 0}, {0, 0xC8, 0, 0, 0}, {0, 0xC8, 4, 0, 0}, {0, 0xC8, 3, 0, 0}, +#ifdef CTR_NATIVE + {0, 0xC8, 2, 0, 0}, {0, 0xC8, 1, 0, 0}, {-512, 0, 2, 0, 0}, {512, 0, 3, 0, 0}, {512, 0, 1, 0, 0}, {512, 0, 7, 0, 0}, {512, 0, 5, 0, 0}, {0, 0, -1, 0, 0}, +#else {0, 0xC8, 2, 0, 0}, {-512, 0, 2, 0, 0}, {512, 0, 3, 0, 0}, {512, 0, 1, 0, 0}, {512, 0, 7, 0, 0}, {512, 0, 5, 0, 0}, {0, 0, -1, 0, 0}, +#endif }, .transitionMeta_csm_3P = { {-512, 0, 2, 0, 0}, {-512, 0, 3, 0, 0}, {-512, 0, 4, 0, 0}, {-512, 0, 5, 0, 0}, {-512, 0, 1, 0, 0}, {-512, 0, 2, 0, 0}, {-512, 0, 3, 0, 0}, {-512, 0, 4, 0, 0}, {-512, 0, 0, 0, 0}, {-512, 0, 1, 0, 0}, {-512, 0, 2, 0, 0}, {-512, 0, 3, 0, 0}, {-512, 0, 2, 0, 0}, {-512, 0, 3, 0, 0}, +#ifdef CTR_NATIVE + {-512, 0, 4, 0, 0}, {-512, 0, 5, 0, 0}, {-512, 0, 0, 0, 0}, {512, 0, 5, 0, 0}, {512, 0, 3, 0, 0}, {512, 0, 1, 0, 0}, {512, 0, 5, 0, 0}, {0, 0, -1, 0, 0}, +#else {-512, 0, 4, 0, 0}, {-512, 0, 0, 0, 0}, {512, 0, 5, 0, 0}, {512, 0, 3, 0, 0}, {512, 0, 1, 0, 0}, {512, 0, 5, 0, 0}, {0, 0, -1, 0, 0}, +#endif }, .transitionMeta_csm_4P = { {-512, 0, 1, 0, 0}, {-512, 0, 2, 0, 0}, {-512, 0, 3, 0, 0}, {-512, 0, 4, 0, 0}, {-512, 0, 2, 0, 0}, {-512, 0, 3, 0, 0}, {-512, 0, 4, 0, 0}, {-512, 0, 5, 0, 0}, {-512, 0, 3, 0, 0}, {-512, 0, 4, 0, 0}, {-512, 0, 0, 0, 0}, {-512, 0, 5, 0, 0}, {-512, 0, 1, 0, 0}, {-512, 0, 6, 0, 0}, +#ifdef CTR_NATIVE + {-512, 0, 2, 0, 0}, {-512, 0, 3, 0, 0}, {-512, 0, 0, 0, 0}, {512, 0, 3, 0, 0}, {512, 0, 1, 0, 0}, {512, 0, 7, 0, 0}, {512, 0, 5, 0, 0}, {0, 0, -1, 0, 0}, +#else {-512, 0, 2, 0, 0}, {-512, 0, 0, 0, 0}, {512, 0, 3, 0, 0}, {512, 0, 1, 0, 0}, {512, 0, 7, 0, 0}, {512, 0, 5, 0, 0}, {0, 0, -1, 0, 0}, +#endif }, .ptr_transitionMeta_csm = {&D230.transitionMeta_csm_1P2P[0], &D230.transitionMeta_csm_1P2P[0], &D230.transitionMeta_csm_3P[0], diff --git a/game/230/MM_Characters.c b/game/230/MM_Characters.c index 5a7d5ed60..5047c40bd 100644 --- a/game/230/MM_Characters.c +++ b/game/230/MM_Characters.c @@ -135,6 +135,30 @@ struct Model *MM_Characters_GetModelByName(int *name) return model; } } + +#ifdef CTR_NATIVE + { + static struct Model *s_oxideModel = NULL; + static int s_oxideLoaded = 0; + + int *oxideName = (int *)rdata.s_oxide; + if ((name[0] == oxideName[0]) && (name[1] == oxideName[1]) && + (name[2] == oxideName[2]) && (name[3] == oxideName[3])) + { + if (!s_oxideLoaded) + { + s_oxideLoaded = 1; + int size; + char *buf = LOAD_DramFile(sdata->ptrBigfile1, + BI_RACERMODELHI + NITROS_OXIDE, NULL, &size, -1); + if (buf != NULL) + s_oxideModel = (struct Model *)(buf + 4); + } + return s_oxideModel; + } + } +#endif + return NULL; } @@ -355,7 +379,11 @@ void MM_Characters_SetMenuLayout(void) iVar3 = numPlyrNextGame - 1; // original game +#ifdef CTR_NATIVE +#define NUM_ICONS 0x10 +#else #define NUM_ICONS 0xF +#endif // Loop through bottom characters, // if any are unlocked, use expanded @@ -508,8 +536,6 @@ void MM_Characters_RestoreIDs(void) MM_Characters_SetMenuLayout(); -#define NUM_ICONS 0xF - for (i = 0; i < NUM_ICONS; i++) { // would not need this if CSM was sorted @@ -1025,8 +1051,6 @@ void MM_Characters_MenuProc(struct RectMenu *unused) csm_Active = D230.csm_Active; -#define NUM_ICONS 0xF - // loop through character icons for (i = 0; i < NUM_ICONS; i++) { diff --git a/game/MAIN/MainMain.c b/game/MAIN/MainMain.c index b55cf0b0d..64b751a04 100644 --- a/game/MAIN/MainMain.c +++ b/game/MAIN/MainMain.c @@ -610,6 +610,11 @@ void StateZero() // PAL SCES02105 calls it multiple times LOAD_LangFile((int)sdata->ptrBigfile1, 1); GAMEPROG_NewGame_OnBoot(); + +#ifdef CTR_NATIVE + sdata->gameProgress.unlockFlags |= UNLOCK_CHARACTERS; +#endif + gGT->overlayIndex_null_notUsed = 0; gGT->levelID = NAUGHTY_DOG_CRATE; diff --git a/include/ovr_230.h b/include/ovr_230.h index da4729611..2c5ca80e5 100644 --- a/include/ovr_230.h +++ b/include/ovr_230.h @@ -501,19 +501,35 @@ struct OverlayDATA_230 // 800b4dcc - UsaRetail // 800b55a8 - EurRetail +#ifdef CTR_NATIVE + struct CharacterSelectMeta csm_1P2P_limited[0x10]; +#else struct CharacterSelectMeta csm_1P2P_limited[0xF]; +#endif // 800b4e80 - UsaRetail // 800b565c - EurRetail +#ifdef CTR_NATIVE + struct CharacterSelectMeta csm_1P2P[0x10]; +#else struct CharacterSelectMeta csm_1P2P[0xF]; +#endif // 800b4f34 - UsaRetail // 800b5710 - EurRetail +#ifdef CTR_NATIVE + struct CharacterSelectMeta csm_3P[0x10]; +#else struct CharacterSelectMeta csm_3P[0xF]; +#endif // 800b4fe8 - UsaRetail // 800b57c4 - EurRetail +#ifdef CTR_NATIVE + struct CharacterSelectMeta csm_4P[0x10]; +#else struct CharacterSelectMeta csm_4P[0xF]; +#endif // 800b509C - UsaRetail // 800b5878 - EurRetail @@ -532,18 +548,22 @@ struct OverlayDATA_230 // 800b50D4 - UsaRetail // 800b58b0 - EurRetail // 1P/2P mode +#ifdef CTR_NATIVE + struct TransitionMeta transitionMeta_csm_1P2P[0x16]; +#else struct TransitionMeta transitionMeta_csm_1P2P[0x15]; - - // 0x2 byte padding s16 padding800b51A6; +#endif // 3P mode // 800b51A8 - UsaRetail // 800b5984 - EurRetail +#ifdef CTR_NATIVE + struct TransitionMeta transitionMeta_csm_3P[0x16]; +#else struct TransitionMeta transitionMeta_csm_3P[0x15]; - - // 0x2 byte padding s16 padding800B527A; +#endif // 4P mode // 800b527c - UsaRetail diff --git a/run.sh b/run.sh new file mode 100755 index 000000000..a46ae8157 --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cd "$(dirname "$0")/build" && ./ctr_native