Skip to content
Open
90 changes: 81 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
cmake_minimum_required(VERSION 3.16.0 FATAL_ERROR)

if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
endif()

# Set the project version and language
project(Spaghettify VERSION 1.0.0 LANGUAGES C CXX ASM)
include(FetchContent)
Expand Down Expand Up @@ -59,6 +63,7 @@ endif()
message("Spaghetti Kart version: ${PROJECT_VERSION} ${PROJECT_PATCH_WORD}")

if(APPLE)
enable_language(OBJC)
enable_language(OBJCXX)
endif()

Expand Down Expand Up @@ -187,6 +192,14 @@ set(SKIP_XCODE_VERSION_CHECK ON)
set(GFX_DEBUG_DISASSEMBLER OFF)

# Add compile definitions for the target
include(CheckCCompilerFlag)
check_c_compiler_flag("-Wno-error=int-conversion" HAS_WNO_ERROR_INT_CONVERSION)

if(HAS_WNO_ERROR_INT_CONVERSION)
add_compile_options("$<$<COMPILE_LANGUAGE:C>:-Wno-error=incompatible-pointer-types>" "$<$<COMPILE_LANGUAGE:C>:-Wno-error=int-conversion>" "$<$<COMPILE_LANGUAGE:CXX>:-Wno-error=changes-meaning>" "$<$<COMPILE_LANGUAGE:CXX>:-Wno-error=narrowing>")
else()
add_compile_options("$<$<COMPILE_LANGUAGE:C>:-Wno-error=incompatible-pointer-types>" "$<$<COMPILE_LANGUAGE:CXX>:-Wno-error=narrowing>")
endif()
add_compile_definitions(
VERSION_US=1
ENABLE_RUMBLE=1
Expand Down Expand Up @@ -263,6 +276,7 @@ file(GLOB_RECURSE ALL_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
"src/port/*.h"
"src/port/*.c"
"src/port/*.cpp"
"src/port/*.mm"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is .mm?

"src/mods/*.h"
"src/mods/*.c"
"src/mods/*.cpp"
Expand Down Expand Up @@ -301,10 +315,9 @@ if (CMAKE_SYSTEM_NAME STREQUAL "iOS")
set(IOS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libultraship/ios)

set(STORYBOARD_FILE ${IOS_DIR}/Launch.storyboard)
set(IMAGE_FILES ${IOS_DIR}/PoweredBy.png)
set(ICON_FILES ${IOS_DIR}/Icon.png)
set(ICON_FILES ${CMAKE_CURRENT_SOURCE_DIR}/icon.png)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why image files removed? Not used?


list(APPEND ALL_FILES ${STORYBOARD_FILE} ${IMAGE_FILES} ${ICON_FILES})
list(APPEND ALL_FILES ${STORYBOARD_FILE} ${ICON_FILES})

add_executable(${PROJECT_NAME} ${ALL_FILES})
set_xcode_property(${PROJECT_NAME} PRODUCT_BUNDLE_IDENTIFIER ${PROJECT_ID} All)
Expand All @@ -313,7 +326,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "iOS")
PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST ${IOS_DIR}/plist.in
RESOURCE "${IMAGE_FILES};${STORYBOARD_FILE};${ICON_FILES}"
RESOURCE "${STORYBOARD_FILE};${ICON_FILES}"
)
else()
add_executable(${PROJECT_NAME} ${ALL_FILES})
Expand Down Expand Up @@ -483,6 +496,52 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "CafeOS")
target_include_directories(${PROJECT_NAME} PRIVATE
${DEVKITPRO}/portlibs/wiiu/include/
)
elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS")
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(INSTALL_DOCS OFF CACHE BOOL "" FORCE)
set(INSTALL_PKG_CONFIG_MODULE OFF CACHE BOOL "" FORCE)

FetchContent_Declare(
libogg
GIT_REPOSITORY https://github.com/xiph/ogg.git
GIT_TAG v1.3.6
)
FetchContent_MakeAvailable(libogg)

set(OGG_ROOT "${libogg_BINARY_DIR}" CACHE PATH "" FORCE)
set(OGG_INCLUDE_DIR "${libogg_SOURCE_DIR}/include" CACHE PATH "" FORCE)
set(OGG_LIBRARY "${libogg_BINARY_DIR}/libogg.a" CACHE FILEPATH "" FORCE)

if(TARGET ogg AND NOT TARGET Ogg::ogg)
add_library(Ogg::ogg ALIAS ogg)
endif()

FetchContent_Declare(
libvorbis
GIT_REPOSITORY https://github.com/xiph/vorbis.git
GIT_TAG v1.3.7
)
FetchContent_MakeAvailable(libvorbis)

if(TARGET vorbis AND NOT TARGET Vorbis::vorbis)
add_library(Vorbis::vorbis ALIAS vorbis)
endif()

if(TARGET vorbisenc AND NOT TARGET Vorbis::vorbisenc)
add_library(Vorbis::vorbisenc ALIAS vorbisenc)
endif()

if(TARGET vorbisfile AND NOT TARGET Vorbis::vorbisfile)
add_library(Vorbis::vorbisfile ALIAS vorbisfile)
endif()

set(ADDITIONAL_LIBRARY_DEPENDENCIES
"Ogg::ogg"
"Vorbis::vorbis"
"Vorbis::vorbisenc"
"Vorbis::vorbisfile"
)
else()
find_package(Ogg REQUIRED)
find_package(Vorbis REQUIRED)
Expand Down Expand Up @@ -658,11 +717,20 @@ endif()
add_custom_command(
TARGET ${PROJECT_NAME} POST_BUILD
COMMENT "Copying asset yamls..."
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/config.yml" "$<TARGET_FILE_DIR:Spaghettify>/config.yml"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/yamls/" "$<TARGET_FILE_DIR:Spaghettify>/yamls/"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/meta/" "$<TARGET_FILE_DIR:Spaghettify>/meta/"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/config.yml" "$<TARGET_FILE_DIR:Spaghettify>/config.yml"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/yamls/" "$<TARGET_FILE_DIR:Spaghettify>/yamls/"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/meta/" "$<TARGET_FILE_DIR:Spaghettify>/meta/"
)

if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
add_custom_command(
TARGET ${PROJECT_NAME} POST_BUILD
COMMENT "Copying packaged O2R assets..."
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/mk64.o2r" "$<TARGET_FILE_DIR:Spaghettify>/mk64.o2r"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/spaghetti.o2r" "$<TARGET_FILE_DIR:Spaghettify>/spaghetti.o2r"
)
endif()

if(NOT CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch")
include(ExternalProject)
ExternalProject_Add(TorchExternal
Expand All @@ -687,15 +755,19 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch")
COMMAND ${TORCH_EXECUTABLE} pack assets spaghetti.o2r o2r
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/mk64.o2r" "${CMAKE_BINARY_DIR}/mk64.o2r"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/spaghetti.o2r" "${CMAKE_BINARY_DIR}/spaghetti.o2r"
)
)

add_custom_target(
GenerateO2R
DEPENDS TorchExternal
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${TORCH_EXECUTABLE} pack assets spaghetti.o2r o2r
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/spaghetti.o2r" "${CMAKE_BINARY_DIR}/spaghetti.o2r"
)
)

if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
add_dependencies(${PROJECT_NAME} ExtractAssets)
endif()

if("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")
install(FILES "${CMAKE_BINARY_DIR}/spaghetti.o2r" DESTINATION . COMPONENT ${PROJECT_NAME})
Expand Down
53 changes: 53 additions & 0 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,59 @@ cpack
cmake --build build-cmake --target clean
```

## iOS
Requires:
* macOS with Xcode installed
* CMake and Ninja in `PATH`
* Homebrew packages: `cmake ninja sdl2 libpng glew nlohmann-json libzip vorbis-tools sdl2_net tinyxml2 pkg-config git`
* Your own supported Mario Kart 64 ROM

Install the Homebrew dependencies:

```bash
brew install cmake ninja sdl2 libpng glew nlohmann-json libzip vorbis-tools sdl2_net tinyxml2 pkg-config git
```

Build steps:

```bash
# Clone the repo
git clone --recurse-submodules https://github.com/HarbourMasters/SpaghettiKart.git
cd SpaghettiKart

# If needed later
git submodule update --init --recursive

# Put your supported ROM at the repo root with this filename
ln -sf "/path/to/your/baserom.us.z64" baserom.us.z64

# Generate mk64.o2r and spaghetti.o2r from your own ROM
cmake -S . -B build-cmake -GNinja
cmake --build build-cmake --target ExtractAssets

# Configure the iOS build
cmake -S . -B build-ios-make -DCMAKE_BUILD_TYPE=Release -DIOS=ON -DSIGN_LIBRARY=OFF

# Build the app
cmake --build build-ios-make --config Release

# Package an unsigned IPA
# The IPA must contain Payload/ at the archive root.
rm -rf ipa-package
mkdir -p ipa-package/Payload
cp -R build-ios-make/Spaghettify.app ipa-package/Payload/Spaghettify.app
(cd ipa-package && zip -qry ../SpaghettiKart-unsigned.ipa Payload)

# Output
ls SpaghettiKart-unsigned.ipa
```

Notes:
* The IPA produced by these steps is unsigned.
* The build generates `mk64.o2r` and `spaghetti.o2r` from your own ROM and copies them into the iOS app bundle.
* Do not zip `build-ios-make/Payload` directly, or the archive will have the wrong root folder for signing tools.
* No additional mod pack is required for the current iOS build flow.

## Getting CI to work on your fork

The CI works via [Github Actions](https://github.com/features/actions) where we mostly make use of machines hosted by Github; except for the very first step of the CI process called "Extract assets". This steps extracts assets from the game file and generates an "assets" folder in `mm/`.
Expand Down
4 changes: 4 additions & 0 deletions include/mk64.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ extern "C" {
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240

#ifdef TARGET_N64
#define STACKSIZE 0x2000
#else
#define STACKSIZE 0x10000
#endif
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert, the stacks are unused


// Border Height Define for NTSC Versions
#define BORDER_HEIGHT 1
Expand Down
5 changes: 4 additions & 1 deletion src/engine/RaceManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ void RaceManager::BeginPlay() {
}
}
}
gEditor.AddLight("Sun", nullptr, D_800DC610[1].l->l.dir);

if (gEditor.IsEnabled()) {
gEditor.AddLight("Sun", nullptr, D_800DC610[1].l->l.dir);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good!


track->BeginPlay();
}
Expand Down
40 changes: 30 additions & 10 deletions src/engine/mods/ModManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
#include "port/Engine.h"
#include "semver.hpp"
#include "utils/StringHelper.h"
#include <filesystem>
#include <memory>
#include <optional>
#include <set>
#include <string>

#include "ModManager.h"
Expand All @@ -18,6 +20,27 @@ void DetectCyclicDependencies();
void DetectOutdatedDependencies();
void SortModsByDependencies();

namespace {
bool IsArchivePath(const std::filesystem::path& path) {
const auto ext = path.extension().string();
return StringHelper::IEquals(ext, ".zip") || StringHelper::IEquals(ext, ".o2r") ||
std::filesystem::is_directory(path);
}

void AddUniqueArchive(std::vector<std::string>& archiveFiles, std::set<std::string>& seenArchivePaths,
const std::filesystem::path& path) {
if (!IsArchivePath(path)) {
return;
}

const auto normalizedPath = path.lexically_normal().generic_string();
if (seenArchivePaths.insert(normalizedPath).second) {
archiveFiles.push_back(normalizedPath);
}
}

} // namespace

std::vector<std::tuple<ModMetadata, std::shared_ptr<Ship::Archive>>> Mods = {};

void InitModsSystem() {
Expand Down Expand Up @@ -63,19 +86,20 @@ void GenerateAssetsMods() {
}

std::vector<std::string> ListMods() {
const std::string main_path = Ship::Context::GetPathRelativeToAppDirectory(game_asset_file);
Copy link
Copy Markdown
Contributor

@coco875 coco875 Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about this change

Copy link
Copy Markdown
Contributor

@MegaMech MegaMech Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this file feel like feature changes to me.

Prefer if this sort of change was discussed in the PR description (here is fine now) as to why/what this improves. Separate PR best but not required.

const std::string main_path = Ship::Context::LocateFileAcrossAppDirs(game_asset_file);
const std::string assets_path = Ship::Context::LocateFileAcrossAppDirs(engine_asset_file);

std::vector<std::string> archiveFiles;
std::set<std::string> seenArchivePaths;
if (std::filesystem::exists(main_path)) {
archiveFiles.push_back(main_path);
AddUniqueArchive(archiveFiles, seenArchivePaths, main_path);
} else { // should not happen, but just in case
GenerateAssetsMods();
archiveFiles.push_back(main_path);
AddUniqueArchive(archiveFiles, seenArchivePaths, main_path);
}

if (std::filesystem::exists(assets_path)) {
archiveFiles.push_back(assets_path);
AddUniqueArchive(archiveFiles, seenArchivePaths, assets_path);
}

const std::string mods_path = Ship::Context::GetPathRelativeToAppDirectory("mods");
Expand All @@ -88,13 +112,9 @@ std::vector<std::string> ListMods() {

if (std::filesystem::exists(mods_path) && std::filesystem::is_directory(mods_path)) {
for (const auto& p : std::filesystem::directory_iterator(mods_path)) {
auto ext = p.path().extension().string();
if (StringHelper::IEquals(ext, ".zip") || StringHelper::IEquals(ext, ".o2r") || std::filesystem::is_directory(p.path())) {
archiveFiles.push_back(p.path().generic_string());
}
AddUniqueArchive(archiveFiles, seenArchivePaths, p.path());
}
}

return archiveFiles;
}

Expand Down Expand Up @@ -203,7 +223,7 @@ void AddCoreDependencies() {
}

void CheckMK64O2RExists() {
const std::string main_path = Ship::Context::GetPathRelativeToAppDirectory(game_asset_file);
const std::string main_path = Ship::Context::LocateFileAcrossAppDirs(game_asset_file);

if (!std::filesystem::exists(main_path)) {
GenerateAssetsMods();
Expand Down
2 changes: 1 addition & 1 deletion src/engine/mods/ModManager.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#pragma once

void InitModsSystem();
void UnloadMods();
void UnloadMods();
5 changes: 5 additions & 0 deletions src/port/Engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,12 @@ bool GameEngine::GenAssetFile() {
auto extractor = new GameExtractor();

if (!extractor->SelectGameFromUI()) {
#ifdef __IOS__
ShowMessage("ROM Required",
"Copy a supported Mario Kart 64 ROM to this app's Documents folder as baserom.us.z64, then relaunch the app.");
#else
ShowMessage("Error", "No ROM selected.\n\nExiting...");
#endif
exit(1);
}

Expand Down
Loading
Loading