Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ compile_commands.json
*~
*.log
memcards/

# Android/Gradle
.gradle/
local.properties
**/.cxx/
*.apk
*.aar
captures/
*.keystore
53 changes: 42 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ cmake_minimum_required(VERSION 3.20)
project(CTR-Native C)
include(CheckCCompilerFlag)

set(CTR_NATIVE_VERSION "0.1.0-beta.7.1")
file(STRINGS "${CMAKE_SOURCE_DIR}/VERSION" CTR_NATIVE_VERSION LIMIT_COUNT 1)
string(STRIP "${CTR_NATIVE_VERSION}" CTR_NATIVE_VERSION)
if(CTR_NATIVE_VERSION STREQUAL "")
message(FATAL_ERROR "VERSION must contain the CTR Native release version.")
endif()

if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
message(FATAL_ERROR
Expand All @@ -18,9 +22,11 @@ if(CMAKE_C_COMPILER_ID STREQUAL "MSVC" OR CMAKE_C_SIMULATE_ID STREQUAL "MSVC")
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
if(NOT ANDROID)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
endif()

execute_process(
COMMAND git rev-parse --short=12 HEAD
Expand Down Expand Up @@ -60,8 +66,13 @@ function(ctr_native_add_label_warning_if_supported target warning flag_check)
endfunction()

# SDL3
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
if(ANDROID)
set(SDL_SHARED ON CACHE BOOL "" FORCE)
set(SDL_STATIC OFF CACHE BOOL "" FORCE)
else()
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
endif()
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
set(SDL_TESTS OFF CACHE BOOL "" FORCE)
set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE)
Expand All @@ -74,7 +85,7 @@ set(SDL_DIALOG OFF CACHE BOOL "" FORCE)
set(SDL_TRAY OFF CACHE BOOL "" FORCE)
set(SDL_POWER OFF CACHE BOOL "" FORCE)
set(SDL_VULKAN OFF CACHE BOOL "" FORCE)
set(SDL_OPENGLES OFF CACHE BOOL "" FORCE)
set(SDL_OPENGLES ${ANDROID} CACHE BOOL "" FORCE)
set(SDL_OFFSCREEN OFF CACHE BOOL "" FORCE)
set(SDL_SNDIO OFF CACHE BOOL "" FORCE)
set(SDL_SNDIO_SHARED OFF CACHE BOOL "" FORCE)
Expand All @@ -83,13 +94,19 @@ set(SDL_X11_XTEST OFF CACHE BOOL "" FORCE)
add_subdirectory(externals/SDL)

# CTR Native
add_executable(ctr_native main.c)
if(ANDROID)
add_library(ctr_native SHARED main.c)
else()
add_executable(ctr_native main.c)
endif()
set_target_properties(ctr_native PROPERTIES
C_STANDARD 17
C_STANDARD_REQUIRED ON
C_EXTENSIONS OFF
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
if(NOT ANDROID)
set_target_properties(ctr_native PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
endif()
target_include_directories(ctr_native PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_compile_definitions(ctr_native PRIVATE
CTR_NATIVE
Expand All @@ -111,7 +128,6 @@ if(CTR_NATIVE_MSVC_FRONTEND)
)
else()
target_compile_options(ctr_native PRIVATE
-msse
-Wenum-conversion
-Wenum-compare
-Wswitch-enum
Expand All @@ -121,6 +137,18 @@ else()
$<$<CONFIG:Release>:-g>
)

if(ANDROID)
# ARM defaults to unsigned plain char. Retail MIPS and the supported PC
# targets use signed char in game-owned fields and comparisons.
target_compile_options(ctr_native PRIVATE -fsigned-char)

# Two ASM-verified retail calls use static nonliteral format strings.
# Keep those game call sites exact while satisfying the NDK defaults.
target_compile_options(ctr_native PRIVATE -Wno-format-security)
else()
target_compile_options(ctr_native PRIVATE -msse)
endif()

if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
target_compile_options(ctr_native PRIVATE -Wall -Wextra -Wstrict-aliasing=2)
ctr_native_add_label_warning_if_supported(ctr_native -Wfree-labels CTR_HAS_WFREE_LABELS)
Expand All @@ -129,6 +157,9 @@ else()
endif()
endif()
target_link_libraries(ctr_native SDL3::SDL3)
if(ANDROID)
target_link_libraries(ctr_native log)
endif()
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
target_link_options(ctr_native PRIVATE -static-libgcc)
endif()
Expand All @@ -137,7 +168,7 @@ if(MINGW)
endif()

include(CTest)
if(BUILD_TESTING)
if(BUILD_TESTING AND NOT ANDROID)
add_test(NAME ctr_native_version COMMAND $<TARGET_FILE:ctr_native> --version)
set_tests_properties(ctr_native_version PROPERTIES PASS_REGULAR_EXPRESSION "^CTR Native ")
endif()
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
# CTR Native

A native PC port of Crash Team Racing (PS1, 1999), built on top of the [CTR-ModSDK](https://github.com/CTR-tools/CTR-ModSDK) decompilation project.

## Philosophy

- **No byte budget.** Game source lives in `game/` as our own copies. Edit freely.
- **No PSX toolchain.** Targets Windows and Linux with SDL3. No MIPS compiler needed.
- **Clean platform layer.** `main.c` owns process startup; host details stay in `platform/native_*`.
- **No build system nonsense.** Just `build.bat` / `build.sh`.
- **Fully static build.** Single executable, zero dependencies. SDL3 is compiled from vendored source and linked statically.
A native port of Crash Team Racing (PS1, 1999), built on top of the [CTR-ModSDK](https://github.com/CTR-tools/CTR-ModSDK) decompilation project.

## Directory Layout

Expand All @@ -19,8 +11,10 @@ ctr_native/
build-msvc.bat Windows build (MSVC x86)
build.bat Windows build (MinGW i686)
build.sh Linux build
android/ Android Gradle project and launcher
CMakePresets.json Shared CLion/command-line CMake configurations
README.md This file
README_ANDROID.md Android build and setup guide
game/ Our copies of all decompiled game source (943 files)
game_unity.h Ordered unity include chain for all game source files
include/ Project headers (structs, globals, declarations, platform facade)
Expand Down Expand Up @@ -60,6 +54,11 @@ sudo apt install gcc-multilib
sudo apt install libx11-dev libxext-dev libgl1-mesa-dev libasound2-dev libudev-dev libdbus-1-dev
```

### Android

See [README_ANDROID.md](README_ANDROID.md). Android currently builds 32-bit
`armeabi-v7a` and `x86` APKs and requires an OpenGL ES 3 capable device.

## Building

```
Expand All @@ -69,6 +68,13 @@ chmod +x build.sh
./build.sh # Linux
```

For Android:

```
cd android
./gradlew assembleDebug
```

The shared CMake presets can also be used directly or selected as CLion CMake profiles:

```
Expand All @@ -84,6 +90,7 @@ Output:
- MSVC: `build-msvc-x86/Release/ctr_native.exe`
- MinGW: `build/ctr_native.exe`
- Linux: `build/ctr_native`
- Android: `android/app/build/outputs/apk/debug/app-debug.apk`

### Clean build

Expand All @@ -100,6 +107,9 @@ rm -rf build/ # Linux: delete cached libraries

## Running

Android users select their own raw NTSC-U BIN through the in-app setup screen;
the app copies it into app-owned storage. See [README_ANDROID.md](README_ANDROID.md).

### Normal Setup

If you downloaded a release build, you only need two things for normal play:
Expand Down Expand Up @@ -197,4 +207,5 @@ main.c (entrypoint)
- [CTR-ModSDK](https://github.com/CTR-tools/CTR-ModSDK) — the decompilation project this is built on
- [PsyCross](https://github.com/OpenDriver2/PsyCross) — original PS1 compatibility code from which parts of CTR Native's owned platform layer and PsyQ facade headers are derived
- [SDL3](https://github.com/libsdl-org/SDL) — cross-platform multimedia
- [Simon Butt](https://github.com/Simon358) — initial Android port contribution
- Crash Team Racing is a trademark of Sony Computer Entertainment / Naughty Dog
78 changes: 78 additions & 0 deletions README_ANDROID.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# CTR Native on Android

CTR Native builds as a sideloadable Android APK. The Android target preserves
the same game code and PS1 VRAM model as the desktop builds while using SDL3 and
OpenGL ES 3 at the host boundary.

## Requirements

- JDK 17
- Android SDK Platform 35
- Android NDK `27.0.12077973`
- CMake 3.22.1 from the Android SDK
- An OpenGL ES 3 capable Android device
- An Android-recognized gamepad; there is currently no touch-control overlay
- Your own NTSC-U retail CTR disc image in raw MODE2/2352 BIN format

The current native memory model is 32-bit, so the APK contains `armeabi-v7a`
and `x86` libraries only. It is intended for direct sideloading, not Play Store
distribution.

## Build

Install the required SDK, NDK, and CMake versions through Android Studio's SDK
Manager, then run:

```sh
cd android
./gradlew assembleDebug
```

The Gradle wrapper downloads the pinned Gradle version automatically. From the
repository root, the APK is written to:

```text
android/app/build/outputs/apk/debug/app-debug.apk
```

While still in the `android/` directory, install or update it with:

```sh
adb install -r app/build/outputs/apk/debug/app-debug.apk
```

You can also open the `android/` directory as a project in Android Studio.

## Disc Setup

On first launch, select your own NTSC-U retail CTR BIN through the setup screen.
The image must be a single-track raw MODE2/2352 BIN whose data track begins at
byte zero. Cooked 2048-byte ISO images do not contain the XA/STR sector data the
game needs.

The launcher validates the raw sector layout, streams the selected image into
app-owned storage, and starts the game. It does not modify the selected file and
does not request broad storage or network permission. Android removes the
imported copy when the app is uninstalled, so it must be selected again after a
fresh install.

## Controllers

CTR Native uses SDL's Gamepad API and supports up to four recognized gamepads.
Bluetooth controllers and USB controllers connected through USB host/OTG can
both work when Android exposes them as standard gamepad devices. Pair or connect
the controller through Android; the app does not scan for Bluetooth devices and
does not require Bluetooth or USB permission.

SDL includes mappings for many common controllers. A device exposed only as an
unknown joystick, without an SDL gamepad mapping, will not be opened by the
current input path. Controllers can be connected or removed while the game is
running.

## Logs

Native output is mirrored to Logcat with the `CTR-Native` tag:

```sh
adb logcat -s CTR-Native
```
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0-beta.7.1
66 changes: 66 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
plugins {
id 'com.android.application'
}

def ctrNativeVersion = rootProject.file('../VERSION').getText('UTF-8').trim()

android {
namespace 'com.ctrnative'
compileSdk 35
ndkVersion '27.0.12077973'

defaultConfig {
applicationId 'com.ctrnative'
minSdk 21
targetSdk 35
versionCode 701
versionName ctrNativeVersion

ndk {
abiFilters 'armeabi-v7a', 'x86'
}

externalNativeBuild {
cmake {
arguments '-DANDROID_STL=c++_static'
}
}
}

buildTypes {
release {
minifyEnabled false
}
}

externalNativeBuild {
cmake {
path file('../../CMakeLists.txt')
version '3.22.1'
}
}

sourceSets {
main {
java.srcDirs = [
'src/main/java',
'../../externals/SDL/android-project/app/src/main/java'
]
res.srcDirs = [
'src/main/res',
'../../externals/SDL/android-project/app/src/main/res'
]
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

lint {
// AGP 8.7 intentionally caps this project at API 35.
disable 'OldTargetApi'
lintConfig file('lint.xml')
}
}
19 changes: 19 additions & 0 deletions android/app/lint.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
<!-- SDL's activity sources perform the API and permission guards at runtime. -->
<issue id="MissingPermission">
<ignore regexp=".*externals/SDL/android-project/app/src/main/.*" />
</issue>
<issue id="NewApi">
<ignore regexp=".*externals/SDL/android-project/app/src/main/.*" />
</issue>
<issue id="StaticFieldLeak">
<ignore regexp=".*externals/SDL/android-project/app/src/main/.*" />
</issue>
<issue id="UnspecifiedRegisterReceiverFlag">
<ignore regexp=".*externals/SDL/android-project/app/src/main/.*" />
</issue>
<issue id="UnusedResources">
<ignore regexp=".*externals/SDL/android-project/app/src/main/.*" />
</issue>
</lint>
Loading