diff --git a/BUILDING.md b/BUILDING.md index e1dfea4..8f88785 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -7,6 +7,12 @@ These steps cover: decompressing the ROM, running the recompiler and finally bui ## 1. Building the Bomberman 64 decompilation You will need a decompressed ROM. Build the project at https://github.com/Bomberhackers/bm64 which will generate a decompressed ROM from an existing locally dumped BM64 US ROM. +Place the decompressed ROM in the repository root as: + +```bash +bm64.decomp.us.z64 +``` + ## 2. Clone the Bomberman 64 Recompiled Repository This project makes use of submodules so you will need to clone the repository with the `--recurse-submodules` flag. @@ -26,6 +32,16 @@ For Linux the instructions for Ubuntu are provided, but you can find the equival sudo apt-get install cmake ninja libsdl2-dev libgtk-3-dev lld llvm clang-15 ``` +### macOS +For macOS, install dependencies with Homebrew: + +```bash +brew install cmake ninja sdl2 freetype llvm lld +python3 -m pip install --user macholib +``` + +`macholib` is required by the macOS linker wrapper used by this project. + ### Windows You will need to install [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/). In the setup process you'll need to select the following options and tools for installation: @@ -33,7 +49,7 @@ In the setup process you'll need to select the following options and tools for i - C++ Clang Compiler for Windows - C++ CMake tools for Windows -The other tool necessary will be `make` which can be installe via [Chocolatey](https://chocolatey.org/): +The other tool necessary will be `make` which can be installed via [Chocolatey](https://chocolatey.org/): ```bash choco install make ``` @@ -58,12 +74,14 @@ If you prefer the command line or you're on a Unix platform you can build the pr ```bash cmake -S . -B build-cmake -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -G Ninja -DCMAKE_BUILD_TYPE=Release # or Debug if you want to debug -cmake --build build-cmake --target BM64Recompiled -j$(nproc) --config Release # or Debug +cmake --build build-cmake --target BM64Recompiled --config Release --parallel # or Debug ``` ## 6. Success -VoilĂ ! You should now have a `BM64Recompiled` executable in the build directory! If you used Visual Studio this will be `out/build/x64-[Configuration]` and if you used the provided CMake commands then this will be `build-cmake`. You will need to run the executable out of the root folder of this project or copy the assets folder to the build folder to run it. +VoilĂ ! You should now have a BM64Recompiled executable in the build directory! If you used Visual Studio this will be `out/build/x64-[Configuration]` and if you used the provided CMake commands then this will be `build-cmake`. + +On macOS, the output is `build-cmake/BM64Recompiled.app`. -> [!IMPORTANT] +> [!IMPORTANT] > In the game itself, you should be using a standard ROM, not the decompressed one. diff --git a/CMakeLists.txt b/CMakeLists.txt index c104f86..188c37d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,20 @@ endif() if (APPLE) enable_language(OBJC OBJCXX) + + execute_process( + COMMAND /usr/bin/python3 -c "import macholib" + RESULT_VARIABLE MACHOLIB_IMPORT_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + if (NOT MACHOLIB_IMPORT_RESULT EQUAL 0) + message(WARNING + "Python package 'macholib' was not found for /usr/bin/python3. " + "The custom macOS linker wrapper at .github/macos/ld64 will fail at link time without it. " + "Install it with: python3 -m pip install --user macholib" + ) + endif() endif() if (CMAKE_SYSTEM_NAME MATCHES "Linux") @@ -61,6 +75,29 @@ SET(RMLUI_TESTS_ENABLED OFF CACHE BOOL "" FORCE) add_subdirectory(${CMAKE_SOURCE_DIR}/lib/RmlUi) target_compile_definitions(rmlui_core PRIVATE LUNASVG_BUILD_STATIC) +# Apply controller pak patch to N64ModernRuntime if not already applied. +# Source: https://github.com/gcsmith/N64ModernRuntime/tree/controller_pak +execute_process( + COMMAND git apply --check ${CMAKE_SOURCE_DIR}/patches/n64modernruntime-controller-pak.patch + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime + RESULT_VARIABLE PATCH_CHECK_RESULT + OUTPUT_QUIET + ERROR_QUIET +) +if (PATCH_CHECK_RESULT EQUAL 0) + execute_process( + COMMAND git apply ${CMAKE_SOURCE_DIR}/patches/n64modernruntime-controller-pak.patch + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime + RESULT_VARIABLE PATCH_APPLY_RESULT + ) + if (NOT PATCH_APPLY_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to apply N64ModernRuntime controller pak patch") + endif() + message(STATUS "Applied N64ModernRuntime controller pak patch") +else() + message(STATUS "N64ModernRuntime controller pak patch already applied (or conflicts exist)") +endif() + add_subdirectory(${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime) target_include_directories(rt64 PRIVATE ${CMAKE_BINARY_DIR}/rt64/src) @@ -113,13 +150,44 @@ set_source_files_properties(${CMAKE_SOURCE_DIR}/RecompiledPatches/patches.c PROP # Build patches elf if(NOT DEFINED PATCHES_C_COMPILER) - set(PATCHES_C_COMPILER clang) + if (APPLE) + if (EXISTS "/opt/local/bin/clang-mp-18") + set(PATCHES_C_COMPILER "/opt/local/bin/clang-mp-18") + elseif (EXISTS "/opt/homebrew/opt/llvm/bin/clang") + set(PATCHES_C_COMPILER "/opt/homebrew/opt/llvm/bin/clang") + elseif (EXISTS "/usr/local/opt/llvm/bin/clang") + set(PATCHES_C_COMPILER "/usr/local/opt/llvm/bin/clang") + else() + set(PATCHES_C_COMPILER clang) + endif() + else() + set(PATCHES_C_COMPILER clang) + endif() endif() if(NOT DEFINED PATCHES_LD) - set(PATCHES_LD ld.lld) + if (APPLE) + if (EXISTS "/opt/local/bin/ld.lld-mp-18") + set(PATCHES_LD "/opt/local/bin/ld.lld-mp-18") + elseif (EXISTS "/opt/homebrew/opt/lld/bin/ld.lld") + set(PATCHES_LD "/opt/homebrew/opt/lld/bin/ld.lld") + elseif (EXISTS "/opt/homebrew/opt/llvm/bin/ld.lld") + set(PATCHES_LD "/opt/homebrew/opt/llvm/bin/ld.lld") + elseif (EXISTS "/usr/local/opt/lld/bin/ld.lld") + set(PATCHES_LD "/usr/local/opt/lld/bin/ld.lld") + elseif (EXISTS "/usr/local/opt/llvm/bin/ld.lld") + set(PATCHES_LD "/usr/local/opt/llvm/bin/ld.lld") + else() + set(PATCHES_LD ld.lld) + endif() + else() + set(PATCHES_LD ld.lld) + endif() endif() +message(STATUS "PATCHES_C_COMPILER = ${PATCHES_C_COMPILER}") +message(STATUS "PATCHES_LD = ${PATCHES_LD}") + add_custom_target(PatchesBin COMMAND ${CMAKE_COMMAND} -E env CC=${PATCHES_C_COMPILER} LD=${PATCHES_LD} make WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/patches diff --git a/assets/config_menu/controls.rml b/assets/config_menu/controls.rml index 862579d..d7ae5f3 100644 --- a/assets/config_menu/controls.rml +++ b/assets/config_menu/controls.rml @@ -81,6 +81,26 @@
Reset to defaults
+ +
+ + + +
diff --git a/include/recomp_input.h b/include/recomp_input.h index 2abda38..5f7b294 100644 --- a/include/recomp_input.h +++ b/include/recomp_input.h @@ -102,12 +102,26 @@ namespace recomp { {recomp::ControllerPortMode::Controller, "Controller"} }); + enum class PakType { + ControllerPak, + RumblePak, + OptionCount + }; + + NLOHMANN_JSON_SERIALIZE_ENUM(recomp::PakType, { + {recomp::PakType::ControllerPak, "ControllerPak"}, + {recomp::PakType::RumblePak, "RumblePak"} + }); + constexpr int max_ports = 4; ControllerPortMode get_port_mode(int port); void set_port_mode(int port, ControllerPortMode mode); int get_port_count(); + PakType get_port_pak_type(int port); + void set_port_pak_type(int port, PakType type); + void start_scanning_input(InputDevice device); void stop_scanning_input(); void finish_scanning_input(InputField scanned_field); diff --git a/patches/Makefile b/patches/Makefile index 13f0e4a..e6f58d7 100644 --- a/patches/Makefile +++ b/patches/Makefile @@ -5,7 +5,7 @@ LD ?= ld.lld CFLAGS := -target mips -mips2 -mabi=32 -O0 -G0 -mno-abicalls -mno-odd-spreg -mno-check-zero-division \ -fomit-frame-pointer -ffast-math -fno-unsafe-math-optimizations -fno-builtin-memset \ - -Wall -Wextra -Wno-incompatible-library-redeclaration -Wno-unused-parameter -Wno-unknown-pragmas -Wno-unused-variable -Wno-unused-but-set-variable -Wno-missing-braces -Wno-unsupported-floating-point-opt -Wno-switch -D_MIPS_SZLONG=32 + -Wall -Wextra -Wno-incompatible-library-redeclaration -Wno-unused-parameter -Wno-unknown-pragmas -Wno-unused-variable -Wno-unused-but-set-variable -Wno-missing-braces -Wno-unsupported-floating-point-opt -Wno-switch -Wno-incompatible-pointer-types -Wno-int-conversion -D_MIPS_SZLONG=32 CPPFLAGS := -nostdinc -DF3DEX_GBI -D_LANGUAGE_C -DMIPS -I dummy_headers -I ../lib/sf64/include -I ../lib/sf64/include/libultra -I ../lib/sf64/src -I ../lib/rt64/include -I ../lib/N64ModernRuntime/ultramodern/include -I ../lib/N64ModernRuntime/ultramodern/include/ultramodern LDFLAGS := -nostdlib -T patches.ld -T syms.ld -Map patches.map --unresolved-symbols=ignore-all --emit-relocs diff --git a/patches/n64modernruntime-controller-pak.patch b/patches/n64modernruntime-controller-pak.patch new file mode 100644 index 0000000..02a3d78 --- /dev/null +++ b/patches/n64modernruntime-controller-pak.patch @@ -0,0 +1,2043 @@ +diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt +index fcc3337..5e000ed 100644 +--- a/librecomp/CMakeLists.txt ++++ b/librecomp/CMakeLists.txt +@@ -13,7 +13,6 @@ add_library(librecomp STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/src/eep.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/euc-jp.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/extensions.cpp" +- "${CMAKE_CURRENT_SOURCE_DIR}/src/files.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/flash.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/heap.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/math_routines.cpp" +@@ -23,8 +22,8 @@ add_library(librecomp STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_manifest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_config_api.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/overlays.cpp" +- "${CMAKE_CURRENT_SOURCE_DIR}/src/pak.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/patcher.cpp" ++ "${CMAKE_CURRENT_SOURCE_DIR}/src/pfs.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/pi.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/print.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/recomp.cpp" +diff --git a/librecomp/include/librecomp/game.hpp b/librecomp/include/librecomp/game.hpp +index 6df55ee..1695765 100644 +--- a/librecomp/include/librecomp/game.hpp ++++ b/librecomp/include/librecomp/game.hpp +@@ -7,17 +7,10 @@ + #include "recomp.h" + #include "rsp.hpp" + #include ++#include + + namespace recomp { +- enum class SaveType { +- None, +- Eep4k, +- Eep16k, +- Sram, +- Flashram, +- AllowAll, // Allows all save types to work and reports eeprom size as 16kbit. +- }; +- ++ using SaveType = ultramodern::SaveType; + struct GameEntry { + uint64_t rom_hash; + std::string internal_name; +@@ -109,11 +102,6 @@ namespace recomp { + /// + void start(const Configuration& cfg); + +- SaveType get_save_type(); +- bool eeprom_allowed(); +- bool sram_allowed(); +- bool flashram_allowed(); +- + void start_game(const std::u8string& game_id); + std::u8string current_game_id(); + std::string current_mod_game_id(); +diff --git a/librecomp/include/librecomp/helpers.hpp b/librecomp/include/librecomp/helpers.hpp +index d8f5afd..eb9f86f 100644 +--- a/librecomp/include/librecomp/helpers.hpp ++++ b/librecomp/include/librecomp/helpers.hpp +@@ -7,8 +7,7 @@ + #include + + template +-T _arg(uint8_t* rdram, recomp_context* ctx) { +- static_assert(index < 4, "Only args 0 through 3 supported"); ++T _arg(uint8_t* rdram, recomp_context* ctx) requires(index < 4) { + gpr raw_arg = (&ctx->r4)[index]; + if constexpr (std::is_same_v) { + if constexpr (index < 2) { +@@ -38,6 +37,25 @@ T _arg(uint8_t* rdram, recomp_context* ctx) { + } + } + ++template ++T _arg(uint8_t* rdram, recomp_context* ctx) requires(index >= 4) { ++ const auto raw_arg = MEM_W(index * 4, ctx->r29); ++ if constexpr (std::is_pointer_v) { ++ static_assert (!std::is_pointer_v>, "Double pointers not supported"); ++ return TO_PTR(std::remove_pointer_t, raw_arg); ++ } ++ else if constexpr (std::is_integral_v) { ++ static_assert(sizeof(T) <= 4, "64-bit args not supported"); ++ return static_cast(raw_arg); ++ } ++ else { ++ // static_assert in else workaround ++ [] () { ++ static_assert(flag, "Unsupported type"); ++ }(); ++ } ++} ++ + inline float _arg_float_a1(uint8_t* rdram, recomp_context* ctx) { + (void)rdram; + union { +diff --git a/librecomp/src/eep.cpp b/librecomp/src/eep.cpp +index 048246e..94fedf6 100644 +--- a/librecomp/src/eep.cpp ++++ b/librecomp/src/eep.cpp +@@ -1,20 +1,19 @@ + #include "recomp.h" + #include "librecomp/game.hpp" + +-#include "ultramodern/ultra64.h" +- +-void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); +-void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); ++#include ++#include + + constexpr int eeprom_block_size = 8; ++static std::vector save_buffer; + + extern "C" void osEepromProbe_recomp(uint8_t* rdram, recomp_context* ctx) { +- switch (recomp::get_save_type()) { +- case recomp::SaveType::AllowAll: +- case recomp::SaveType::Eep16k: ++ switch (ultramodern::get_save_type()) { ++ case ultramodern::SaveType::AllowAll: ++ case ultramodern::SaveType::Eep16k: + ctx->r2 = 0x02; // EEPROM_TYPE_16K + break; +- case recomp::SaveType::Eep4k: ++ case ultramodern::SaveType::Eep4k: + ctx->r2 = 0x01; // EEPROM_TYPE_4K + break; + default: +@@ -24,7 +23,7 @@ extern "C" void osEepromProbe_recomp(uint8_t* rdram, recomp_context* ctx) { + } + + extern "C" void osEepromWrite_recomp(uint8_t* rdram, recomp_context* ctx) { +- if (!recomp::eeprom_allowed()) { ++ if (!ultramodern::eeprom_allowed()) { + ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -33,13 +32,17 @@ extern "C" void osEepromWrite_recomp(uint8_t* rdram, recomp_context* ctx) { + gpr buffer = ctx->r6; + int32_t nbytes = eeprom_block_size; + +- save_write(rdram, buffer, eep_address * eeprom_block_size, nbytes); ++ save_buffer.resize(nbytes); ++ for (uint32_t i = 0; i < nbytes; i++) { ++ save_buffer[i] = MEM_B(i, buffer); ++ } ++ ultramodern::save_write_ptr(save_buffer.data(), eep_address * eeprom_block_size, nbytes); + + ctx->r2 = 0; + } + + extern "C" void osEepromLongWrite_recomp(uint8_t* rdram, recomp_context* ctx) { +- if (!recomp::eeprom_allowed()) { ++ if (!ultramodern::eeprom_allowed()) { + ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -50,13 +53,17 @@ extern "C" void osEepromLongWrite_recomp(uint8_t* rdram, recomp_context* ctx) { + + assert((nbytes % eeprom_block_size) == 0); + +- save_write(rdram, buffer, eep_address * eeprom_block_size, nbytes); ++ save_buffer.resize(nbytes); ++ for (uint32_t i = 0; i < nbytes; i++) { ++ save_buffer[i] = MEM_B(i, buffer); ++ } ++ ultramodern::save_write_ptr(save_buffer.data(), eep_address * eeprom_block_size, nbytes); + + ctx->r2 = 0; + } + + extern "C" void osEepromRead_recomp(uint8_t* rdram, recomp_context* ctx) { +- if (!recomp::eeprom_allowed()) { ++ if (!ultramodern::eeprom_allowed()) { + ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -65,13 +72,17 @@ extern "C" void osEepromRead_recomp(uint8_t* rdram, recomp_context* ctx) { + gpr buffer = ctx->r6; + int32_t nbytes = eeprom_block_size; + +- save_read(rdram, buffer, eep_address * eeprom_block_size, nbytes); ++ save_buffer.resize(nbytes); ++ ultramodern::save_read_ptr(save_buffer.data(), eep_address * eeprom_block_size, nbytes); ++ for (uint32_t i = 0; i < nbytes; i++) { ++ MEM_B(i, buffer) = save_buffer[i]; ++ } + + ctx->r2 = 0; + } + + extern "C" void osEepromLongRead_recomp(uint8_t* rdram, recomp_context* ctx) { +- if (!recomp::eeprom_allowed()) { ++ if (!ultramodern::eeprom_allowed()) { + ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -82,7 +93,11 @@ extern "C" void osEepromLongRead_recomp(uint8_t* rdram, recomp_context* ctx) { + + assert((nbytes % eeprom_block_size) == 0); + +- save_read(rdram, buffer, eep_address * eeprom_block_size, nbytes); ++ save_buffer.resize(nbytes); ++ ultramodern::save_read_ptr(save_buffer.data(), eep_address * eeprom_block_size, nbytes); ++ for (uint32_t i = 0; i < nbytes; i++) { ++ MEM_B(i, buffer) = save_buffer[i]; ++ } + + ctx->r2 = 0; + } +diff --git a/librecomp/src/flash.cpp b/librecomp/src/flash.cpp +index f46261c..9a877aa 100644 +--- a/librecomp/src/flash.cpp ++++ b/librecomp/src/flash.cpp +@@ -1,5 +1,6 @@ + #include + #include ++#include + #include + #include + #include "recomp.h" +@@ -15,15 +16,11 @@ constexpr uint32_t page_count = flash_size / page_size; + constexpr uint32_t sector_size = page_size * pages_per_sector; + constexpr uint32_t sector_count = flash_size / sector_size; + +-void save_write_ptr(const void* in, uint32_t offset, uint32_t count); +-void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); +-void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); +-void save_clear(uint32_t start, uint32_t size, char value); +- + std::array write_buffer; ++std::vector save_buffer; + + extern "C" void osFlashInit_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -32,7 +29,7 @@ extern "C" void osFlashInit_recomp(uint8_t * rdram, recomp_context * ctx) { + } + + extern "C" void osFlashReadStatus_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -43,7 +40,7 @@ extern "C" void osFlashReadStatus_recomp(uint8_t * rdram, recomp_context * ctx) + } + + extern "C" void osFlashReadId_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -57,7 +54,7 @@ extern "C" void osFlashReadId_recomp(uint8_t * rdram, recomp_context * ctx) { + } + + extern "C" void osFlashClearStatus_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -66,30 +63,30 @@ extern "C" void osFlashClearStatus_recomp(uint8_t * rdram, recomp_context * ctx) + } + + extern "C" void osFlashAllErase_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } + +- save_clear(0, ultramodern::save_size, 0xFF); ++ ultramodern::save_clear(0, ultramodern::save_size, 0xFF); + + ctx->r2 = 0; + } + + extern "C" void osFlashAllEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } + +- save_clear(0, ultramodern::save_size, 0xFF); ++ ultramodern::save_clear(0, ultramodern::save_size, 0xFF); + + ctx->r2 = 0; + } + + // This function is named sector but really means page. + extern "C" void osFlashSectorErase_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -102,14 +99,14 @@ extern "C" void osFlashSectorErase_recomp(uint8_t * rdram, recomp_context * ctx) + return; + } + +- save_clear(page_num * page_size, page_size, 0xFF); ++ ultramodern::save_clear(page_num * page_size, page_size, 0xFF); + + ctx->r2 = 0; + } + + // Same naming issue as above. + extern "C" void osFlashSectorEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -122,13 +119,13 @@ extern "C" void osFlashSectorEraseThrough_recomp(uint8_t * rdram, recomp_context + return; + } + +- save_clear(page_num * page_size, page_size, 0xFF); ++ ultramodern::save_clear(page_num * page_size, page_size, 0xFF); + + ctx->r2 = 0; + } + + extern "C" void osFlashCheckEraseEnd_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -138,7 +135,7 @@ extern "C" void osFlashCheckEraseEnd_recomp(uint8_t * rdram, recomp_context * ct + } + + extern "C" void osFlashWriteBuffer_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -160,7 +157,7 @@ extern "C" void osFlashWriteBuffer_recomp(uint8_t * rdram, recomp_context * ctx) + } + + extern "C" void osFlashWriteArray_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -168,13 +165,13 @@ extern "C" void osFlashWriteArray_recomp(uint8_t * rdram, recomp_context * ctx) + uint32_t page_num = ctx->r4; + + // Copy the write buffer into the save file +- save_write_ptr(write_buffer.data(), page_num * page_size, page_size); ++ ultramodern::save_write_ptr(write_buffer.data(), page_num * page_size, page_size); + + ctx->r2 = 0; + } + + extern "C" void osFlashReadArray_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -190,7 +187,11 @@ extern "C" void osFlashReadArray_recomp(uint8_t * rdram, recomp_context * ctx) { + uint32_t count = n_pages * page_size; + + // Read from the save file into the provided buffer +- save_read(PASS_RDRAM dramAddr, offset, count); ++ save_buffer.resize(count); ++ ultramodern::save_read_ptr(save_buffer.data(), offset, count); ++ for (uint32_t i = 0; i < count; i++) { ++ MEM_B(i, dramAddr) = save_buffer[i]; ++ } + + // Send the message indicating read completion + ultramodern::enqueue_external_message_src(mq, 0, false, ultramodern::EventMessageSource::Pi); +@@ -199,7 +200,7 @@ extern "C" void osFlashReadArray_recomp(uint8_t * rdram, recomp_context * ctx) { + } + + extern "C" void osFlashChange_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp +index a75d253..b9b5134 100644 +--- a/librecomp/src/mod_manifest.cpp ++++ b/librecomp/src/mod_manifest.cpp +@@ -3,8 +3,8 @@ + #include "json/json.hpp" + + #include "recompiler/context.h" +-#include "librecomp/files.hpp" + #include "librecomp/mods.hpp" ++#include + + static bool read_json(std::ifstream input_file, nlohmann::json &json_out) { + if (!input_file.good()) { +@@ -27,7 +27,7 @@ static bool read_json_with_backups(const std::filesystem::path &path, nlohmann:: + } + + // Try reading and parsing the backup file. +- if (read_json(recomp::open_input_backup_file(path), json_out)) { ++ if (read_json(ultramodern::open_input_backup_file(path), json_out)) { + return true; + } + +diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp +index ff40212..e4ef3c8 100644 +--- a/librecomp/src/mods.cpp ++++ b/librecomp/src/mods.cpp +@@ -3,7 +3,7 @@ + #include + #include + +-#include "librecomp/files.hpp" ++#include + #include "librecomp/mods.hpp" + #include "librecomp/overlays.hpp" + #include "librecomp/game.hpp" +@@ -32,7 +32,7 @@ static bool read_json_with_backups(const std::filesystem::path &path, nlohmann:: + } + + // Try reading and parsing the backup file. +- if (read_json(recomp::open_input_backup_file(path), json_out)) { ++ if (read_json(ultramodern::open_input_backup_file(path), json_out)) { + return true; + } + +@@ -679,7 +679,7 @@ bool save_mod_config_storage(const std::filesystem::path &path, const std::strin + } + } + +- std::ofstream output_file = recomp::open_output_file_with_backup(path); ++ std::ofstream output_file = ultramodern::open_output_file_with_backup(path); + if (!output_file.good()) { + return false; + } +@@ -687,7 +687,7 @@ bool save_mod_config_storage(const std::filesystem::path &path, const std::strin + output_file << std::setw(4) << config_json; + output_file.close(); + +- return recomp::finalize_output_file_with_backup(path); ++ return ultramodern::finalize_output_file_with_backup(path); + } + + bool parse_mods_config(const std::filesystem::path &path, std::unordered_set &enabled_mods, std::vector &mod_order) { +@@ -720,7 +720,7 @@ bool save_mods_config(const std::filesystem::path &path, const std::unordered_se + config_json["enabled_mods"] = enabled_mods; + config_json["mod_order"] = mod_order; + +- std::ofstream output_file = recomp::open_output_file_with_backup(path); ++ std::ofstream output_file = ultramodern::open_output_file_with_backup(path); + if (!output_file.good()) { + return false; + } +@@ -728,7 +728,7 @@ bool save_mods_config(const std::filesystem::path &path, const std::unordered_se + output_file << std::setw(4) << config_json; + output_file.close(); + +- return recomp::finalize_output_file_with_backup(path); ++ return ultramodern::finalize_output_file_with_backup(path); + } + + void recomp::mods::ModContext::dirty_mod_configuration_thread_process() { +diff --git a/librecomp/src/pak.cpp b/librecomp/src/pak.cpp +deleted file mode 100644 +index 0be1513..0000000 +--- a/librecomp/src/pak.cpp ++++ /dev/null +@@ -1,51 +0,0 @@ +-#include "ultramodern/ultra64.h" +-#include "ultramodern/ultramodern.hpp" +- +-#include "recomp.h" +-#include "helpers.hpp" +- +-extern "C" void osPfsInitPak_recomp(uint8_t * rdram, recomp_context* ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsFreeBlocks_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsAllocateFile_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsDeleteFile_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsFileState_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsFindFile_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsReadWriteFile_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsChecker_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsNumFiles_recomp(uint8_t * rdram, recomp_context * ctx) { +- s32* max_files = _arg<1, s32*>(rdram, ctx); +- s32* files_used = _arg<2, s32*>(rdram, ctx); +- +- *max_files = 0; +- *files_used = 0; +- +- _return(ctx, 1); // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsRepairId_recomp(uint8_t * rdram, recomp_context * ctx) { +- _return(ctx, 1); // PFS_ERR_NOPACK +-} +diff --git a/librecomp/src/pfs.cpp b/librecomp/src/pfs.cpp +new file mode 100644 +index 0000000..2145056 +--- /dev/null ++++ b/librecomp/src/pfs.cpp +@@ -0,0 +1,189 @@ ++#include "ultramodern/ultramodern.hpp" ++ ++#include "helpers.hpp" ++ ++extern "C" void osPfsInitPak_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); ++ PTR(OSPfs) pfs = _arg<1, PTR(OSPfs)>(rdram, ctx); ++ int channel = _arg<2, int>(rdram, ctx); ++ ++ s32 ret = osPfsInitPak(PASS_RDRAM mq, pfs, channel); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsRepairId_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ ++ s32 ret = osPfsRepairId(PASS_RDRAM pfs); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsInit_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); ++ PTR(OSPfs) pfs = _arg<1, PTR(OSPfs)>(rdram, ctx); ++ int channel = _arg<2, int>(rdram, ctx); ++ ++ s32 ret = osPfsInit(PASS_RDRAM mq, pfs, channel); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsReFormat_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(OSMesgQueue) mq = _arg<1, PTR(OSMesgQueue)>(rdram, ctx); ++ int channel = _arg<2, int>(rdram, ctx); ++ ++ s32 ret = osPfsReFormat(PASS_RDRAM pfs, mq, channel); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsChecker_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ ++ s32 ret = osPfsChecker(PASS_RDRAM pfs); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsAllocateFile_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ u16 company_code = _arg<1, u16>(rdram, ctx); ++ u32 game_code = _arg<2, u32>(rdram, ctx); ++ PTR(u8) game_name = _arg<3, PTR(u8)>(rdram, ctx); ++ PTR(u8) ext_name = _arg<4, PTR(u8)>(rdram, ctx); ++ int nbytes = _arg<5, int>(rdram, ctx); ++ PTR(s32) file_no = _arg<6, PTR(s32)>(rdram, ctx); ++ u8 game_name_proxy[PFS_FILE_NAME_LEN]; ++ u8 ext_name_proxy[PFS_FILE_EXT_LEN]; ++ ++ for (uint32_t i = 0; i < PFS_FILE_NAME_LEN; i++) { ++ game_name_proxy[i] = MEM_B(i, (gpr)game_name); ++ } ++ for (uint32_t i = 0; i < PFS_FILE_EXT_LEN; i++) { ++ ext_name_proxy[i] = MEM_B(i, (gpr)ext_name); ++ } ++ s32 ret = osPfsAllocateFile(PASS_RDRAM pfs, company_code, game_code, game_name_proxy, ext_name_proxy, nbytes, file_no); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsFindFile_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ u16 company_code = _arg<1, u16>(rdram, ctx); ++ u32 game_code = _arg<2, u32>(rdram, ctx); ++ PTR(u8) game_name = _arg<3, PTR(u8)>(rdram, ctx); ++ PTR(u8) ext_name = _arg<4, PTR(u8)>(rdram, ctx); ++ PTR(s32) file_no = _arg<5, PTR(s32)>(rdram, ctx); ++ u8 game_name_proxy[PFS_FILE_NAME_LEN]; ++ u8 ext_name_proxy[PFS_FILE_EXT_LEN]; ++ ++ for (uint32_t i = 0; i < PFS_FILE_NAME_LEN; i++) { ++ game_name_proxy[i] = MEM_B(i, (gpr)game_name); ++ } ++ for (uint32_t i = 0; i < PFS_FILE_EXT_LEN; i++) { ++ ext_name_proxy[i] = MEM_B(i, (gpr)ext_name); ++ } ++ s32 ret = osPfsFindFile(PASS_RDRAM pfs, company_code, game_code, game_name_proxy, ext_name_proxy, file_no); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsDeleteFile_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ u16 company_code = _arg<1, u16>(rdram, ctx); ++ u32 game_code = _arg<2, u32>(rdram, ctx); ++ PTR(u8) game_name = _arg<3, PTR(u8)>(rdram, ctx); ++ PTR(u8) ext_name = _arg<4, PTR(u8)>(rdram, ctx); ++ u8 game_name_proxy[PFS_FILE_NAME_LEN]; ++ u8 ext_name_proxy[PFS_FILE_EXT_LEN]; ++ ++ for (uint32_t i = 0; i < PFS_FILE_NAME_LEN; i++) { ++ game_name_proxy[i] = MEM_B(i, (gpr)game_name); ++ } ++ for (uint32_t i = 0; i < PFS_FILE_EXT_LEN; i++) { ++ ext_name_proxy[i] = MEM_B(i, (gpr)ext_name); ++ } ++ s32 ret = osPfsDeleteFile(PASS_RDRAM pfs, company_code, game_code, game_name_proxy, ext_name_proxy); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsReadWriteFile_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ s32 file_no = _arg<1, s32>(rdram, ctx); ++ u8 flag = _arg<2, u8>(rdram, ctx); ++ int offset = _arg<3, int>(rdram, ctx); ++ int nbytes = _arg<4, int>(rdram, ctx); ++ PTR(u8) data_buffer = _arg<5, PTR(u8)>(rdram, ctx); ++ std::vector data_buffer_proxy(nbytes); ++ ++ if (flag == PFS_WRITE) { ++ for (uint32_t i = 0; i < nbytes; i++) { ++ data_buffer_proxy[i] = MEM_B(i, (gpr)data_buffer); ++ } ++ } ++ s32 ret = osPfsReadWriteFile(PASS_RDRAM pfs, file_no, flag, offset, nbytes, data_buffer_proxy.data()); ++ if (flag == PFS_READ) { ++ for (uint32_t i = 0; i < nbytes; i++) { ++ MEM_B(i, (gpr)data_buffer) = data_buffer_proxy[i]; ++ } ++ } ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsFileState_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ s32 file_no = _arg<1, s32>(rdram, ctx); ++ PTR(OSPfsState) state = _arg<2, PTR(OSPfsState)>(rdram, ctx); ++ ++ s32 ret = osPfsFileState(PASS_RDRAM pfs, file_no, state); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsGetLabel_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(u8) label = _arg<1, PTR(u8)>(rdram, ctx); ++ PTR(int) len = _arg<2, PTR(int)>(rdram, ctx); ++ u8 label_proxy[32]; ++ ++ s32 ret = osPfsGetLabel(PASS_RDRAM pfs, label_proxy, len); ++ for (uint32_t i = 0; i < 32; i++) { ++ MEM_B(i, (gpr)label) = label_proxy[i]; ++ } ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsSetLabel_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(u8) label = _arg<1, PTR(u8)>(rdram, ctx); ++ u8 label_proxy[32]; ++ ++ for (uint32_t i = 0; i < 32; i++) { ++ label_proxy[i] = MEM_B(i, (gpr)label); ++ } ++ s32 ret = osPfsSetLabel(PASS_RDRAM pfs, label_proxy); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsIsPlug_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); ++ PTR(u8) pattern = _arg<1, PTR(u8)>(rdram, ctx); ++ u8 pattern_proxy = 0; ++ ++ s32 ret = osPfsIsPlug(PASS_RDRAM mq, &pattern_proxy); ++ MEM_B(0, (gpr)pattern) = pattern_proxy; ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsFreeBlocks_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(s32) bytes_not_used = _arg<1, PTR(s32)>(rdram, ctx); ++ ++ s32 ret = osPfsFreeBlocks(PASS_RDRAM pfs, bytes_not_used); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsNumFiles_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(s32) max_files = _arg<1, PTR(s32)>(rdram, ctx); ++ PTR(s32) files_used = _arg<2, PTR(s32)>(rdram, ctx); ++ ++ s32 ret = osPfsNumFiles(PASS_RDRAM pfs, max_files, files_used); ++ _return(ctx, ret); ++} ++ +diff --git a/librecomp/src/pi.cpp b/librecomp/src/pi.cpp +index 43509ba..67cd86d 100644 +--- a/librecomp/src/pi.cpp ++++ b/librecomp/src/pi.cpp +@@ -7,11 +7,12 @@ + #include "recomp.h" + #include "librecomp/addresses.hpp" + #include "librecomp/game.hpp" +-#include "librecomp/files.hpp" ++#include + #include + #include + + static std::vector rom; ++static std::vector save_buffer; + + bool recomp::is_rom_loaded() { + return !rom.empty(); +@@ -84,188 +85,6 @@ void recomp::do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr) + MEM_B(3, ram_address) = *rom_addr++; + } + +-struct { +- std::vector save_buffer; +- std::thread saving_thread; +- std::filesystem::path save_file_path; +- moodycamel::LightweightSemaphore write_sempahore; +- // Used to tell the saving thread that a file swap is pending. +- moodycamel::LightweightSemaphore swap_file_pending_sempahore; +- // Used to tell the consumer thread that the saving thread is ready for a file swap. +- moodycamel::LightweightSemaphore swap_file_ready_sempahore; +- std::mutex save_buffer_mutex; +-} save_context; +- +-const std::u8string save_folder = u8"saves"; +- +-extern std::filesystem::path config_path; +- +-std::filesystem::path ultramodern::get_save_file_path() { +- return save_context.save_file_path; +-} +- +-void set_save_file_path(const std::u8string& subfolder, const std::u8string& name) { +- std::filesystem::path save_folder_path = config_path / save_folder; +- if (!subfolder.empty()) { +- save_folder_path = save_folder_path / subfolder; +- } +- save_context.save_file_path = save_folder_path / (name + u8".bin"); +-} +- +-void update_save_file() { +- bool saving_failed = false; +- { +- std::ofstream save_file = recomp::open_output_file_with_backup(ultramodern::get_save_file_path(), std::ios_base::binary); +- +- if (save_file.good()) { +- std::lock_guard lock{ save_context.save_buffer_mutex }; +- save_file.write(save_context.save_buffer.data(), save_context.save_buffer.size()); +- } +- else { +- saving_failed = true; +- } +- } +- if (!saving_failed) { +- saving_failed = !recomp::finalize_output_file_with_backup(ultramodern::get_save_file_path()); +- } +- if (saving_failed) { +- ultramodern::error_handling::message_box("Failed to write to the save file. Check your file permissions and whether the save folder has been moved to Dropbox or similar, as this can cause issues."); +- } +-} +- +-extern std::atomic_bool exited; +- +-void saving_thread_func(RDRAM_ARG1) { +- while (!exited) { +- bool save_buffer_updated = false; +- // Repeatedly wait for a new action to be sent. +- constexpr int64_t wait_time_microseconds = 10000; +- constexpr int max_actions = 128; +- int num_actions = 0; +- +- // Wait up to the given timeout for a write to come in. Allow multiple writes to coalesce together into a single save. +- // Cap the number of coalesced writes to guarantee that the save buffer eventually gets written out to the file even if the game +- // is constantly sending writes. +- while (save_context.write_sempahore.wait(wait_time_microseconds) && num_actions < max_actions) { +- save_buffer_updated = true; +- num_actions++; +- } +- +- // If an action came through that affected the save file, save the updated contents. +- if (save_buffer_updated) { +- update_save_file(); +- } +- +- if (save_context.swap_file_pending_sempahore.tryWait()) { +- save_context.swap_file_ready_sempahore.signal(); +- } +- } +-} +- +-void save_write_ptr(const void* in, uint32_t offset, uint32_t count) { +- assert(offset + count <= save_context.save_buffer.size()); +- +- { +- std::lock_guard lock { save_context.save_buffer_mutex }; +- memcpy(&save_context.save_buffer[offset], in, count); +- } +- +- save_context.write_sempahore.signal(); +-} +- +-void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) { +- assert(offset + count <= save_context.save_buffer.size()); +- +- { +- std::lock_guard lock { save_context.save_buffer_mutex }; +- for (gpr i = 0; i < count; i++) { +- save_context.save_buffer[offset + i] = MEM_B(i, rdram_address); +- } +- } +- +- save_context.write_sempahore.signal(); +-} +- +-void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) { +- assert(offset + count <= save_context.save_buffer.size()); +- +- std::lock_guard lock { save_context.save_buffer_mutex }; +- for (gpr i = 0; i < count; i++) { +- MEM_B(i, rdram_address) = save_context.save_buffer[offset + i]; +- } +-} +- +-void save_clear(uint32_t start, uint32_t size, char value) { +- assert(start + size < save_context.save_buffer.size()); +- +- { +- std::lock_guard lock { save_context.save_buffer_mutex }; +- std::fill_n(save_context.save_buffer.begin() + start, size, value); +- } +- +- save_context.write_sempahore.signal(); +-} +- +-size_t get_save_size(recomp::SaveType save_type) { +- switch (save_type) { +- case recomp::SaveType::AllowAll: +- case recomp::SaveType::Flashram: +- return 0x20000; +- case recomp::SaveType::Sram: +- return 0x8000; +- case recomp::SaveType::Eep16k: +- return 0x800; +- case recomp::SaveType::Eep4k: +- return 0x200; +- case recomp::SaveType::None: +- return 0; +- } +- return 0; +-} +- +-void read_save_file() { +- std::filesystem::path save_file_path = ultramodern::get_save_file_path(); +- +- // Ensure the save file directory exists. +- std::filesystem::create_directories(save_file_path.parent_path()); +- +- // Read the save file if it exists. +- std::ifstream save_file = recomp::open_input_file_with_backup(save_file_path, std::ios_base::binary); +- if (save_file.good()) { +- save_file.read(save_context.save_buffer.data(), save_context.save_buffer.size()); +- } +- else { +- // Otherwise clear the save file to all zeroes. +- std::fill(save_context.save_buffer.begin(), save_context.save_buffer.end(), 0); +- } +-} +- +-void ultramodern::init_saving(RDRAM_ARG1) { +- set_save_file_path(u8"", recomp::current_game_id()); +- +- save_context.save_buffer.resize(get_save_size(recomp::get_save_type())); +- +- read_save_file(); +- +- save_context.saving_thread = std::thread{saving_thread_func, PASS_RDRAM}; +-} +- +-void ultramodern::change_save_file(const std::u8string& subfolder, const std::u8string& name) { +- // Tell the saving thread that a file swap is pending. +- save_context.swap_file_pending_sempahore.signal(); +- // Wait until the saving thread indicates it's ready to swap files. +- save_context.swap_file_ready_sempahore.wait(); +- // Perform the save file swap. +- set_save_file_path(subfolder, name); +- read_save_file(); +-} +- +-void ultramodern::join_saving_thread() { +- if (save_context.saving_thread.joinable()) { +- save_context.saving_thread.join(); +- } +-} +- + void do_dma(RDRAM_ARG PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t physical_addr, uint32_t size, uint32_t direction) { + // TODO asynchronous transfer + // TODO implement unaligned DMA correctly +@@ -277,12 +96,16 @@ void do_dma(RDRAM_ARG PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t physical_ + // Send a message to the mq to indicate that the transfer completed + ultramodern::enqueue_external_message_src(mq, 0, false, ultramodern::EventMessageSource::Pi); + } else if (physical_addr >= recomp::sram_base) { +- if (!recomp::sram_allowed()) { ++ if (!ultramodern::sram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use SRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } + // read sram +- save_read(rdram, rdram_address, physical_addr - recomp::sram_base, size); ++ save_buffer.resize(size); ++ ultramodern::save_read_ptr(save_buffer.data(), physical_addr - recomp::sram_base, size); ++ for (uint32_t i = 0; i < size; i++) { ++ MEM_B(i, rdram_address) = save_buffer[i]; ++ } + + // Send a message to the mq to indicate that the transfer completed + ultramodern::enqueue_external_message_src(mq, 0, false, ultramodern::EventMessageSource::Pi); +@@ -294,12 +117,16 @@ void do_dma(RDRAM_ARG PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t physical_ + // write cart rom + throw std::runtime_error("ROM DMA write unimplemented"); + } else if (physical_addr >= recomp::sram_base) { +- if (!recomp::sram_allowed()) { ++ if (!ultramodern::sram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use SRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } + // write sram +- save_write(rdram, rdram_address, physical_addr - recomp::sram_base, size); ++ save_buffer.resize(size); ++ for (uint32_t i = 0; i < size; i++) { ++ save_buffer[i] = MEM_B(i, rdram_address); ++ } ++ ultramodern::save_write_ptr(save_buffer.data(), physical_addr - recomp::sram_base, size); + + // Send a message to the mq to indicate that the transfer completed + ultramodern::enqueue_external_message_src(mq, 0, false, ultramodern::EventMessageSource::Pi); +diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp +index ea17675..a166c85 100644 +--- a/librecomp/src/recomp.cpp ++++ b/librecomp/src/recomp.cpp +@@ -21,6 +21,7 @@ + #include "xxHash/xxh3.h" + #include "ultramodern/ultramodern.hpp" + #include "ultramodern/error_handling.hpp" ++#include "ultramodern/save.hpp" + #include "librecomp/addresses.hpp" + #include "librecomp/mods.hpp" + #include "recompiler/live_recompiler.h" +@@ -57,8 +58,6 @@ std::unordered_map game_roms {}; + std::unique_ptr mod_context = std::make_unique(); + // The project's version. + recomp::Version project_version; +-// The current game's save type. +-recomp::SaveType save_type = recomp::SaveType::None; + + std::u8string recomp::GameEntry::stored_filename() const { + return game_id + u8".z64"; +@@ -687,8 +686,8 @@ bool wait_for_game_started(uint8_t* rdram, recomp_context* context) { + + recomp::init_heap(rdram, recomp::mod_rdram_start + mod_ram_used); + +- save_type = game_entry.save_type; +- ultramodern::init_saving(rdram); ++ ultramodern::set_save_type(game_entry.save_type); ++ ultramodern::init_saving(rdram, recomp::current_game_id()); + + try { + game_entry.entrypoint(rdram, context); +@@ -706,29 +705,6 @@ bool wait_for_game_started(uint8_t* rdram, recomp_context* context) { + } + } + +-recomp::SaveType recomp::get_save_type() { +- return save_type; +-} +- +-bool recomp::eeprom_allowed() { +- return +- save_type == SaveType::Eep4k || +- save_type == SaveType::Eep16k || +- save_type == SaveType::AllowAll; +-} +- +-bool recomp::sram_allowed() { +- return +- save_type == SaveType::Sram || +- save_type == SaveType::AllowAll; +-} +- +-bool recomp::flashram_allowed() { +- return +- save_type == SaveType::Flashram || +- save_type == SaveType::AllowAll; +-} +- + void recomp::start(const recomp::Configuration& cfg) { + project_version = cfg.project_version; + recomp::check_all_stored_roms(); +diff --git a/ultramodern/CMakeLists.txt b/ultramodern/CMakeLists.txt +index 5593172..54cdd9f 100644 +--- a/ultramodern/CMakeLists.txt ++++ b/ultramodern/CMakeLists.txt +@@ -10,11 +10,14 @@ add_library(ultramodern STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/src/error_handling.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/events.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/extensions.cpp" ++ "${CMAKE_CURRENT_SOURCE_DIR}/src/files.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/input.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/mesgqueue.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/misc_ultra.cpp" ++ "${CMAKE_CURRENT_SOURCE_DIR}/src/pfs.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/renderer_context.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/rsp.cpp" ++ "${CMAKE_CURRENT_SOURCE_DIR}/src/save.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/scheduling.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/task_win32.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/threadqueue.cpp" +diff --git a/librecomp/include/librecomp/files.hpp b/ultramodern/include/ultramodern/files.hpp +similarity index 80% +rename from librecomp/include/librecomp/files.hpp +rename to ultramodern/include/ultramodern/files.hpp +index 63e3e9d..497d6d9 100644 +--- a/librecomp/include/librecomp/files.hpp ++++ b/ultramodern/include/ultramodern/files.hpp +@@ -1,14 +1,15 @@ +-#ifndef __RECOMP_FILES_H__ +-#define __RECOMP_FILES_H__ ++#ifndef __ULTRAMODERN_FILES_HPP__ ++#define __ULTRAMODERN_FILES_HPP__ + + #include + #include + +-namespace recomp { ++namespace ultramodern { + std::ifstream open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in); + std::ifstream open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in); + std::ofstream open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::out); + bool finalize_output_file_with_backup(const std::filesystem::path& filepath); +-}; ++} ++ ++#endif // __ULTRAMODERN_FILES_HPP__ + +-#endif +diff --git a/ultramodern/include/ultramodern/input.hpp b/ultramodern/include/ultramodern/input.hpp +index 1dab1b8..abb8e76 100644 +--- a/ultramodern/include/ultramodern/input.hpp ++++ b/ultramodern/include/ultramodern/input.hpp +@@ -15,7 +15,7 @@ namespace ultramodern { + enum class Pak { + None, + RumblePak, +- // ControllerPak, ++ ControllerPak, + // TransferPak + }; + +@@ -58,6 +58,11 @@ namespace ultramodern { + + void set_callbacks(const callbacks_t& callbacks); + } ++ ++ input::connected_device_info_t get_connected_device_info(int channel); ++ ++ int get_max_controllers(); + } + + #endif ++ +diff --git a/ultramodern/include/ultramodern/save.hpp b/ultramodern/include/ultramodern/save.hpp +new file mode 100644 +index 0000000..576c17c +--- /dev/null ++++ b/ultramodern/include/ultramodern/save.hpp +@@ -0,0 +1,49 @@ ++#ifndef __ULTRAMODERN_SAVE_HPP__ ++#define __ULTRAMODERN_SAVE_HPP__ ++ ++#include ++#include ++ ++namespace ultramodern { ++ enum class SaveType { ++ None, ++ Eep4k, ++ Eep16k, ++ Sram, ++ Flashram, ++ AllowAll, // Allows all save types to work and reports eeprom size as 16kbit. ++ }; ++ ++ void set_save_type(SaveType type); ++ ++ void set_save_file_path(const std::u8string& subfolder, const std::u8string& name); ++ ++ void init_saving(RDRAM_ARG const std::u8string& name); ++ ++ void change_save_file(const std::u8string& subfolder, const std::u8string& name); ++ ++ void join_saving_thread(); ++ ++ void save_write_ptr(const void* in, uint32_t offset, uint32_t count); ++ ++ void save_read_ptr(void *out, uint32_t offset, uint32_t count); ++ ++ void save_clear(uint32_t start, uint32_t size, char value); ++ ++ SaveType get_save_type(); ++ ++ size_t get_save_size(SaveType save_type); ++ ++ std::filesystem::path get_save_base_path(); ++ ++ std::filesystem::path get_save_file_path(); ++ ++ bool eeprom_allowed(); ++ ++ bool sram_allowed(); ++ ++ bool flashram_allowed(); ++} ++ ++#endif // __ULTRAMODERN_SAVE_HPP__ ++ +diff --git a/ultramodern/include/ultramodern/ultra64.h b/ultramodern/include/ultramodern/ultra64.h +index 31eb106..7ca579e 100644 +--- a/ultramodern/include/ultramodern/ultra64.h ++++ b/ultramodern/include/ultramodern/ultra64.h +@@ -74,6 +74,59 @@ typedef u64 OSTime; + #define OS_EVENT_THREADSTATUS 13 /* CPU thread status: used by rmon */ + #define OS_EVENT_PRENMI 14 /* Pre NMI interrupt */ + ++/* Controller errors */ ++ ++#define CONT_NO_RESPONSE_ERROR 0x8 ++#define CONT_OVERRUN_ERROR 0x4 ++#define CONT_RANGE_ERROR -1 ++#define CONT_FRAME_ERROR 0x2 ++#define CONT_COLLISION_ERROR 0x1 ++ ++/* Controller type */ ++ ++#define CONT_TYPE_NORMAL 0x0005 ++#define CONT_TYPE_MOUSE 0x0002 ++#define CONT_TYPE_VOICE 0x0100 ++ ++/* File System error number */ ++ ++#define PFS_ERR_NOPACK 1 /* no memory card is plugged or */ ++#define PFS_ERR_NEW_PACK 2 /* ram pack has been changed to a different one */ ++#define PFS_ERR_INCONSISTENT 3 /* need to run Pfschecker*/ ++#define PFS_ERR_CONTRFAIL CONT_OVERRUN_ERROR ++#define PFS_ERR_INVALID 5 /* invalid parameter or file not exist*/ ++#define PFS_ERR_BAD_DATA 6 /* the data read from pack are bad*/ ++#define PFS_DATA_FULL 7 /* no free pages on ram pack*/ ++#define PFS_DIR_FULL 8 /* no free directories on ram pack*/ ++#define PFS_ERR_EXIST 9 /* file exists*/ ++#define PFS_ERR_ID_FATAL 10 /* dead ram pack */ ++#define PFS_ERR_DEVICE 11 /* wrong device type*/ ++#define PFS_ERR_NO_GBCART 12 /* no gb cartridge (64GB-PAK) */ ++#define PFS_ERR_NEW_GBCART 13 /* gb cartridge may be changed */ ++ ++/* File System size */ ++ ++#define PFS_INODE_SIZE_PER_PAGE 128 ++#define PFS_FILE_NAME_LEN 16 ++#define PFS_FILE_EXT_LEN 4 ++#define PFS_BLOCKSIZE 32 /* bytes */ ++#define PFS_ONE_PAGE 8 /* blocks */ ++#define PFS_MAX_BANKS 62 ++ ++/* File System flag */ ++ ++#define PFS_READ 0 ++#define PFS_WRITE 1 ++#define PFS_CREATE 2 ++ ++/* File System status */ ++ ++#define PFS_INITIALIZED 0x1 ++#define PFS_CORRUPTED 0x2 ++#define PFS_ID_BROKEN 0x4 ++#define PFS_MOTOR_INITIALIZED 0x8 ++#define PFS_GBPAK_INITIALIZED 0x10 ++ + #define M_GFXTASK 1 + #define M_AUDTASK 2 + #define M_VIDTASK 3 +@@ -235,6 +288,17 @@ typedef struct { + u8 banks; + } OSPfs; + ++typedef struct { ++ u32 file_size; ++ u32 game_code; ++ char ext_name_0[2]; // insane layout due to ext_name starting on halfword boundary ++ u16 company_code; ++ char game_name_0[2]; ++ char ext_name_1[2]; ++ char game_name_1[12]; ++ char padding[2]; ++ char game_name_2[2]; ++} OSPfsState; // size = 0x20 + + // Controller + +@@ -314,6 +378,24 @@ s32 osMotorStop(RDRAM_ARG PTR(OSPfs)); + s32 osMotorStart(RDRAM_ARG PTR(OSPfs)); + s32 __osMotorAccess(RDRAM_ARG PTR(OSPfs), s32); + ++/* Controller PAK interface */ ++ ++s32 osPfsInitPak(RDRAM_ARG PTR(OSMesgQueue) queue, PTR(OSPfs) pfs, int channel); ++s32 osPfsRepairId(RDRAM_ARG PTR(OSPfs) pfs); ++s32 osPfsInit(RDRAM_ARG PTR(OSMesgQueue) queue, PTR(OSPfs) pfs, int channel); ++s32 osPfsReFormat(RDRAM_ARG PTR(OSPfs) pfs, PTR(OSMesgQueue) queue, int channel); ++s32 osPfsChecker(RDRAM_ARG PTR(OSPfs) pfs); ++s32 osPfsAllocateFile(RDRAM_ARG PTR(OSPfs) pfs, u16 company_code, u32 game_code, u8* game_name, u8* ext_name, int nbytes, PTR(s32) file_no); ++s32 osPfsFindFile(RDRAM_ARG PTR(OSPfs) pfs, u16 company_code, u32 game_code, u8* game_name, u8* ext_name, PTR(s32) file_no); ++s32 osPfsDeleteFile(RDRAM_ARG PTR(OSPfs) pfs, u16 company_code, u32 game_code, u8* game_name, u8* ext_name); ++s32 osPfsReadWriteFile(RDRAM_ARG PTR(OSPfs) pfs, s32 file_no, u8 flag, int offset, int nbytes, u8* data_buffer); ++s32 osPfsFileState(RDRAM_ARG PTR(OSPfs) pfs, s32 file_no, PTR(OSPfsState) state); ++s32 osPfsGetLabel(RDRAM_ARG PTR(OSPfs) pfs, u8* label, PTR(int) len); ++s32 osPfsSetLabel(RDRAM_ARG PTR(OSPfs) pfs, u8* label); ++s32 osPfsIsPlug(RDRAM_ARG PTR(OSMesgQueue) mq, u8* pattern); ++s32 osPfsFreeBlocks(RDRAM_ARG PTR(OSPfs) pfs, PTR(s32) bytes_not_used); ++s32 osPfsNumFiles(RDRAM_ARG PTR(OSPfs) pfs, PTR(s32) max_files, PTR(s32) files_used); ++ + #ifdef __cplusplus + } // extern "C" + #endif +diff --git a/librecomp/src/files.cpp b/ultramodern/src/files.cpp +similarity index 74% +rename from librecomp/src/files.cpp +rename to ultramodern/src/files.cpp +index af6f18d..0dd1a2f 100644 +--- a/librecomp/src/files.cpp ++++ b/ultramodern/src/files.cpp +@@ -1,15 +1,15 @@ +-#include "files.hpp" ++#include + + constexpr std::u8string_view backup_suffix = u8".bak"; + constexpr std::u8string_view temp_suffix = u8".temp"; + +-std::ifstream recomp::open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode) { ++std::ifstream ultramodern::open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode) { + std::filesystem::path backup_path{filepath}; + backup_path += backup_suffix; + return std::ifstream{backup_path, mode}; + } + +-std::ifstream recomp::open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { ++std::ifstream ultramodern::open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { + std::ifstream ret{filepath, mode}; + + // Check if the file failed to open and open the corresponding backup file instead if so. +@@ -20,7 +20,7 @@ std::ifstream recomp::open_input_file_with_backup(const std::filesystem::path& f + return ret; + } + +-std::ofstream recomp::open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { ++std::ofstream ultramodern::open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { + std::filesystem::path temp_path{filepath}; + temp_path += temp_suffix; + std::ofstream temp_file_out{ temp_path, mode }; +@@ -28,7 +28,7 @@ std::ofstream recomp::open_output_file_with_backup(const std::filesystem::path& + return temp_file_out; + } + +-bool recomp::finalize_output_file_with_backup(const std::filesystem::path& filepath) { ++bool ultramodern::finalize_output_file_with_backup(const std::filesystem::path& filepath) { + std::filesystem::path backup_path{filepath}; + backup_path += backup_suffix; + +diff --git a/ultramodern/src/input.cpp b/ultramodern/src/input.cpp +index 8e86cb1..5d723c5 100644 +--- a/ultramodern/src/input.cpp ++++ b/ultramodern/src/input.cpp +@@ -4,23 +4,28 @@ + #include "ultramodern/ultra64.h" + #include "ultramodern/ultramodern.hpp" + +-#define PFS_ERR_NOPACK 1 // no device inserted +-#define PFS_ERR_CONTRFAIL 4 // data transmission failure +-#define PFS_ERR_INVALID 5 // invalid parameter or invalid file +-#define PFS_ERR_DEVICE 11 // different type of device inserted ++#define MAXCONTROLLERS 4 + +-#define PFS_INITIALIZED 1 +-#define PFS_CORRUPTED 2 +-#define PFS_ID_BROKEN 4 +-#define PFS_MOTOR_INITIALIZED 8 +-#define PFS_GBPAK_INITIALIZED 16 ++static int max_controllers = 0; + + static ultramodern::input::callbacks_t input_callbacks {}; + ++int ultramodern::get_max_controllers() { ++ return max_controllers; ++} ++ + void ultramodern::input::set_callbacks(const callbacks_t& callbacks) { + input_callbacks = callbacks; + } + ++ultramodern::input::connected_device_info_t ultramodern::get_connected_device_info(int channel) { ++ ultramodern::input::connected_device_info_t device_info{}; ++ if (input_callbacks.get_connected_device_info != nullptr) { ++ device_info = input_callbacks.get_connected_device_info(channel); ++ } ++ return device_info; ++} ++ + static std::chrono::high_resolution_clock::time_point input_poll_time; + + static void update_poll_time() { +@@ -33,16 +38,6 @@ void ultramodern::measure_input_latency() { + #endif + } + +-#define MAXCONTROLLERS 4 +- +-#define CONT_NO_RESPONSE_ERROR 0x8 +- +-#define CONT_TYPE_NORMAL 0x0005 +-#define CONT_TYPE_MOUSE 0x0002 +-#define CONT_TYPE_VOICE 0x0100 +- +-static int max_controllers = 0; +- + /* Plain controller */ + + static u16 get_controller_type(ultramodern::input::Device device_type) { +@@ -69,12 +64,7 @@ static void __osContGetInitData(u8* pattern, OSContStatus *data) { + *pattern = 0x00; + + for (int controller = 0; controller < max_controllers; controller++) { +- ultramodern::input::connected_device_info_t device_info{}; +- +- if (input_callbacks.get_connected_device_info != nullptr) { +- device_info = input_callbacks.get_connected_device_info(controller); +- } +- ++ const auto device_info = ultramodern::get_connected_device_info(controller); + if (device_info.connected_device != ultramodern::input::Device::None) { + // Mark controller as present + +@@ -160,22 +150,18 @@ extern "C" void osContGetReadData(OSContPad *data) { + } + } + +-/* Rumble */ ++/* RumblePak */ + +-s32 osMotorInit(RDRAM_ARG PTR(OSMesgQueue) mq, PTR(OSPfs) pfs_, int channel) { ++extern "C" s32 osMotorInit(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSPfs) pfs_, int channel) { + OSPfs *pfs = TO_PTR(OSPfs, pfs_); + + // basic initialization performed regardless of connected/disconnected status +- pfs->queue = mq; ++ pfs->queue = mq_; + pfs->channel = channel; + pfs->activebank = 0xFF; + pfs->status = 0; + +- ultramodern::input::connected_device_info_t device_info{}; +- if (input_callbacks.get_connected_device_info != nullptr) { +- device_info = input_callbacks.get_connected_device_info(channel); +- } +- ++ const auto device_info = ultramodern::get_connected_device_info(channel); + if (device_info.connected_device != ultramodern::input::Device::Controller) { + return PFS_ERR_CONTRFAIL; + } +@@ -190,15 +176,15 @@ s32 osMotorInit(RDRAM_ARG PTR(OSMesgQueue) mq, PTR(OSPfs) pfs_, int channel) { + return 0; + } + +-s32 osMotorStop(RDRAM_ARG PTR(OSPfs) pfs) { ++extern "C" s32 osMotorStop(RDRAM_ARG PTR(OSPfs) pfs) { + return __osMotorAccess(PASS_RDRAM pfs, false); + } + +-s32 osMotorStart(RDRAM_ARG PTR(OSPfs) pfs) { ++extern "C" s32 osMotorStart(RDRAM_ARG PTR(OSPfs) pfs) { + return __osMotorAccess(PASS_RDRAM pfs, true); + } + +-s32 __osMotorAccess(RDRAM_ARG PTR(OSPfs) pfs_, s32 flag) { ++extern "C" s32 __osMotorAccess(RDRAM_ARG PTR(OSPfs) pfs_, s32 flag) { + OSPfs *pfs = TO_PTR(OSPfs, pfs_); + + if (!(pfs->status & PFS_MOTOR_INITIALIZED)) { +diff --git a/ultramodern/src/pfs.cpp b/ultramodern/src/pfs.cpp +new file mode 100644 +index 0000000..36ba29c +--- /dev/null ++++ b/ultramodern/src/pfs.cpp +@@ -0,0 +1,389 @@ ++#include ++#include ++#include ++#include ++#include ++ ++#define ALIGN_UP(x, align) (((x) + ((align) - 1)) & ~((align) - 1)) ++#define ARRLEN(x) (sizeof(x) / sizeof((x)[0])) ++#define DEF_DIR_PAGES 2 ++#define MAX_FILES 16 ++#define MAX_PAGES 123 // 128 total, 5 reserved for filesystem ++ ++/* PFS Context */ ++ ++struct pfs_header_t { // same layout as OSPfsState, but non-byteswapped ++ uint32_t file_size; ++ uint32_t game_code; ++ uint16_t company_code; ++ std::array ext_name; ++ std::array game_name; ++ uint16_t padding; ++ ++ pfs_header_t() = default; ++ pfs_header_t(uint32_t fs, uint32_t gc, uint16_t cc, const char* en, const char* gn) ++ : file_size{fs}, game_code{gc}, company_code{cc} { ++ std::memcpy(ext_name.data(), en, sizeof(ext_name)); ++ std::memcpy(game_name.data(), gn, sizeof(game_name)); ++ } ++ inline bool valid() const { ++ return game_code != 0 && company_code != 0; ++ } ++ inline bool compare(uint32_t gcode, uint16_t ccode, const char* ename, const char* gname) const { ++ return game_code == gcode && company_code == ccode && ++ std::memcmp(ext_name.data(), ename, sizeof(ext_name)) == 0 && ++ std::memcmp(game_name.data(), gname, sizeof(game_name)) == 0; ++ } ++}; ++ ++inline std::filesystem::path pfs_header_path() { ++ const auto filename = "controllerpak_header.bin"; ++ return ultramodern::get_save_base_path() / filename; ++} ++ ++inline std::filesystem::path pfs_file_path(size_t file_no) { ++ const auto filename = "controllerpak_file_" + std::to_string(file_no) + ".bin"; ++ return ultramodern::get_save_base_path() / filename; ++} ++ ++inline bool pfs_header_alloc() { ++ if (!std::filesystem::exists(pfs_header_path())) { ++ std::vector zero_block(MAX_FILES * sizeof(pfs_header_t)); ++ std::ofstream out(pfs_header_path(), std::ios::binary | std::ios::out | std::ios::trunc); ++ out.write(zero_block.data(), zero_block.size()); ++ return out.good(); ++ } ++ return true; ++} ++ ++inline bool pfs_header_write(int file_no, const pfs_header_t& hdr) { ++ std::fstream out(pfs_header_path(), std::ios::binary | std::ios::out | std::ios::in); ++ if (out.is_open() && out.good()) { ++ out.seekp(file_no * sizeof(pfs_header_t), std::ios::beg); ++ out.write((const char*)&hdr.file_size, sizeof(hdr.file_size)); ++ out.write((const char*)&hdr.game_code, sizeof(hdr.game_code)); ++ out.write((const char*)&hdr.company_code, sizeof(hdr.company_code)); ++ out.write((const char*)&hdr.ext_name[0], hdr.ext_name.size()); ++ out.write((const char*)&hdr.game_name[0], hdr.game_name.size()); ++ out.write((const char*)&hdr.padding, sizeof(hdr.padding)); ++ } ++ return out.good(); ++} ++ ++inline bool pfs_header_read(int file_no, pfs_header_t& hdr) { ++ hdr = {}; // reset ++ std::ifstream in(pfs_header_path(), std::ios::binary | std::ios::in); ++ if (in.is_open() && in.good()) { ++ in.seekg(file_no * sizeof(pfs_header_t), std::ios::beg); ++ in.read((char*)&hdr.file_size, sizeof(hdr.file_size)); ++ in.read((char*)&hdr.game_code, sizeof(hdr.game_code)); ++ in.read((char*)&hdr.company_code, sizeof(hdr.company_code)); ++ in.read((char*)&hdr.ext_name[0], hdr.ext_name.size()); ++ in.read((char*)&hdr.game_name[0], hdr.game_name.size()); ++ in.read((char*)&hdr.padding, sizeof(hdr.padding)); ++ } ++ return in.good(); ++} ++ ++inline bool pfs_file_alloc(int file_no, int nbytes) { ++ std::vector zero_block(ALIGN_UP(nbytes, PFS_ONE_PAGE * PFS_BLOCKSIZE)); ++ std::ofstream out(pfs_file_path(file_no), std::ios::binary | std::ios::out | std::ios::trunc); ++ if (out.is_open() && out.good()) { ++ out.write(zero_block.data(), zero_block.size()); ++ } ++ return out.good(); ++} ++ ++inline bool pfs_file_write(int file_no, int offset, const char* data_buffer, int nbytes) { ++ std::fstream out(pfs_file_path(file_no), std::ios::binary | std::ios::out | std::ios::in); ++ if (out.is_open() && out.good()) { ++ out.seekp(offset, std::ios::beg); ++ out.write((const char*)data_buffer, nbytes); ++ } ++ return out.good(); ++} ++ ++inline bool pfs_file_read(int file_no, int offset, char* data_buffer, int nbytes) { ++ std::ifstream in(pfs_file_path(file_no), std::ios::binary | std::ios::in); ++ if (in.is_open() && in.good()) { ++ in.seekg(offset, std::ios::beg); ++ in.read((char*)data_buffer, nbytes); ++ } ++ return in.good(); ++} ++ ++/* ControllerPak */ ++ ++static s32 __osPfsGetStatus(RDRAM_ARG PTR(OSMesgQueue) queue, int channel) { ++ const auto device_info = ultramodern::get_connected_device_info(channel); ++ if (device_info.connected_device != ultramodern::input::Device::Controller) { ++ return PFS_ERR_CONTRFAIL; ++ } ++ if (device_info.connected_pak == ultramodern::input::Pak::None) { ++ return PFS_ERR_NOPACK; ++ } ++ if (device_info.connected_pak != ultramodern::input::Pak::ControllerPak) { ++ return PFS_ERR_DEVICE; ++ } ++ ++ pfs_header_alloc(); ++ return 0; ++} ++ ++static s32 __osGetId(RDRAM_ARG PTR(OSPfs) pfs_) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ ++ // we don't implement the real filesystem, so just mimic initialization ++ pfs->version = 0; ++ pfs->banks = 1; ++ pfs->activebank = 0; ++ pfs->inode_start_page = 1 + DEF_DIR_PAGES + (2 * pfs->banks); ++ pfs->dir_size = DEF_DIR_PAGES * PFS_ONE_PAGE; ++ pfs->inode_table = 1 * PFS_ONE_PAGE; ++ pfs->minode_table = (1 + pfs->banks) * PFS_ONE_PAGE; ++ pfs->dir_table = pfs->minode_table + (pfs->banks * PFS_ONE_PAGE); ++ ++ std::memset(pfs->id, 0, ARRLEN(pfs->id)); ++ std::memset(pfs->label, 0, ARRLEN(pfs->label)); ++ return 0; ++} ++ ++extern "C" s32 osPfsInitPak(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSPfs) pfs_, int channel) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ ++ const auto status = __osPfsGetStatus(PASS_RDRAM mq_, channel); ++ if (status != 0) { ++ return status; ++ } ++ ++ pfs->queue = mq_; ++ pfs->channel = channel; ++ pfs->status = 0; ++ __osGetId(PASS_RDRAM pfs_); ++ ++ const s32 ret = osPfsChecker(PASS_RDRAM pfs_); ++ pfs->status |= PFS_INITIALIZED; ++ return ret; ++} ++ ++extern "C" s32 osPfsRepairId(RDRAM_ARG PTR(OSPfs) pfs) { ++ return 0; ++} ++ ++extern "C" s32 osPfsInit(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSPfs) pfs_, int channel) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ ++ const auto status = __osPfsGetStatus(PASS_RDRAM mq_, channel); ++ if (status != 0) { ++ return status; ++ } ++ ++ pfs->queue = mq_; ++ pfs->channel = channel; ++ pfs->status = 0; ++ pfs->activebank = -1; ++ __osGetId(PASS_RDRAM pfs_); ++ ++ const s32 ret = osPfsChecker(PASS_RDRAM pfs_); ++ pfs->status |= PFS_INITIALIZED; ++ return ret; ++} ++ ++extern "C" s32 osPfsReFormat(RDRAM_ARG PTR(OSPfs) pfs, PTR(OSMesgQueue) mq_, int channel) { ++ return 0; ++} ++ ++extern "C" s32 osPfsChecker(RDRAM_ARG PTR(OSPfs) pfs) { ++ return 0; ++} ++ ++extern "C" s32 osPfsAllocateFile(RDRAM_ARG PTR(OSPfs) pfs, u16 company_code, u32 game_code, u8* game_name, u8* ext_name, int nbytes, PTR(s32) file_no_) { ++ s32* file_no = TO_PTR(s32, file_no_); ++ ++ if (company_code == 0 || game_code == 0) { ++ return PFS_ERR_INVALID; ++ } ++ ++ pfs_header_t hdr{}; ++ u8 free_file_index = 0; ++ for (size_t i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (!hdr.valid()) { ++ free_file_index = i; ++ break; ++ } ++ } ++ ++ if (free_file_index == MAX_FILES) { ++ return PFS_DIR_FULL; ++ } ++ if (!pfs_header_write(free_file_index, pfs_header_t{(uint32_t)nbytes, game_code, company_code, (char*)ext_name, (char*)game_name})) { ++ return PFS_ERR_INVALID; ++ } ++ if (!pfs_file_alloc(free_file_index, nbytes)) { ++ return PFS_ERR_INVALID; ++ } ++ *file_no = free_file_index; ++ return 0; ++} ++ ++extern "C" s32 osPfsFindFile(RDRAM_ARG PTR(OSPfs) pfs_, u16 company_code, u32 game_code, u8* game_name, u8* ext_name, PTR(s32) file_no_) { ++ s32* file_no = TO_PTR(s32, file_no_); ++ ++ if (company_code == 0 || game_code == 0) { ++ return PFS_ERR_INVALID; ++ } ++ ++ pfs_header_t hdr{}; ++ for (size_t i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (hdr.compare(game_code, company_code, (char*)ext_name, (char*)game_name)) { ++ *file_no = i; ++ return 0; ++ } ++ } ++ return PFS_ERR_INVALID; ++} ++ ++extern "C" s32 osPfsDeleteFile(RDRAM_ARG PTR(OSPfs) pfs_, u16 company_code, u32 game_code, u8* game_name, u8* ext_name) { ++ if (company_code == 0 || game_code == 0) { ++ return PFS_ERR_INVALID; ++ } ++ ++ pfs_header_t hdr{}; ++ for (int i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (hdr.compare(game_code, company_code, (char*)ext_name, (char*)game_name)) { ++ pfs_header_write(i, pfs_header_t{}); ++ std::filesystem::remove(pfs_file_path(i)); ++ return 0; ++ } ++ } ++ return PFS_ERR_INVALID; ++} ++ ++extern "C" s32 osPfsReadWriteFile(RDRAM_ARG PTR(OSPfs) pfs_, s32 file_no, u8 flag, int offset, int nbytes, u8* data_buffer) { ++ if (!std::filesystem::exists(pfs_file_path(file_no))) { ++ return PFS_ERR_INVALID; ++ } ++ ++ const auto file_size = std::filesystem::file_size(pfs_file_path(file_no)); ++ if (offset % PFS_BLOCKSIZE || nbytes % PFS_BLOCKSIZE || (offset + nbytes) > file_size) { ++ return PFS_ERR_INVALID; ++ } ++ else if ((flag == PFS_READ) && !pfs_file_read(file_no, offset, (char*)data_buffer, nbytes)) { ++ return PFS_ERR_INVALID; ++ } ++ else if ((flag == PFS_WRITE) && !pfs_file_write(file_no, offset, (const char*)data_buffer, nbytes)) { ++ return PFS_ERR_INVALID; ++ } ++ return 0; ++} ++ ++inline void bswap_copy(char* dst, const char* src, int offset, int n) { ++ for (int i = 0; i < n; i++) { dst[(i + offset) ^ 3] = src[i + offset]; } ++} ++ ++extern "C" s32 osPfsFileState(RDRAM_ARG PTR(OSPfs) pfs_, s32 file_no, PTR(OSPfsState) state_) { ++ OSPfsState *state = TO_PTR(OSPfsState, state_); ++ ++ if (!std::filesystem::exists(pfs_file_path(file_no))) { ++ return PFS_ERR_INVALID; ++ } ++ ++ pfs_header_t hdr{}; ++ pfs_header_read(file_no, hdr); ++ ++ state->file_size = hdr.file_size; ++ state->company_code = hdr.company_code; ++ state->game_code = hdr.game_code; ++ ++ // FIXME OSPfsState layout is an absoute mess. giving up and byte swapping ++ bswap_copy((char*)state, (char*)&hdr, 10, 20); ++ return 0; ++} ++ ++extern "C" s32 osPfsGetLabel(RDRAM_ARG PTR(OSPfs) pfs_, u8* label, PTR(int) len_) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ int* len = TO_PTR(int, len_); ++ ++ if (label == NULL) { ++ return PFS_ERR_INVALID; ++ } ++// if (__osCheckId(pfs) == PFS_ERR_NEW_PACK) { ++// return PFS_ERR_NEW_PACK; ++// } ++ ++ int i; ++ for (i = 0; i < ARRLEN(pfs->label); i++) { ++ if (pfs->label[i] == 0) { ++ break; ++ } ++ *label++ = pfs->label[i]; ++ } ++ *len = i; ++ return 0; ++} ++ ++extern "C" s32 osPfsSetLabel(RDRAM_ARG PTR(OSPfs) pfs_, u8* label) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ ++ if (label != NULL) { ++ for (int i = 0; i < ARRLEN(pfs->label); i++) { ++ if (*label == 0) { ++ break; ++ } ++ pfs->label[i] = *label++; ++ } ++ } ++ return 0; ++} ++ ++extern "C" s32 osPfsIsPlug(RDRAM_ARG PTR(OSMesgQueue) mq_, u8* pattern) { ++ u8 bits = 0; ++ ++ for (int channel = 0; channel < ultramodern::get_max_controllers(); channel++) { ++ if (__osPfsGetStatus(PASS_RDRAM mq_, channel) == 0) { ++ bits |= (1 << channel); ++ } ++ } ++ *pattern = bits; ++ return 0; ++} ++ ++extern "C" s32 osPfsFreeBlocks(RDRAM_ARG PTR(OSPfs) pfs_, PTR(s32) bytes_not_used_) { ++ OSPfs *pfs = TO_PTR(OSPfs, pfs_); ++ s32 *bytes_not_used = TO_PTR(s32, bytes_not_used_); ++ ++ s32 pages_used = 0; ++ pfs_header_t hdr{}; ++ for (size_t i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (hdr.valid()) { ++ pages_used += hdr.file_size >> 8; ++ } ++ } ++ ++ *bytes_not_used = (MAX_PAGES - pages_used) << 8; ++ return 0; ++} ++ ++extern "C" s32 osPfsNumFiles(RDRAM_ARG PTR(OSPfs) pfs_, PTR(s32) max_files_, PTR(s32) files_used_) { ++ OSPfs *pfs = TO_PTR(OSPfs, pfs_); ++ s32 *max_files = TO_PTR(s32, max_files_); ++ s32 *files_used = TO_PTR(s32, files_used_); ++ ++ u8 num_files = 0; ++ pfs_header_t hdr{}; ++ for (size_t i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (hdr.valid()) { ++ num_files++; ++ } ++ } ++ ++ *max_files = MAX_FILES; ++ *files_used = num_files; ++ return 0; ++} ++ +diff --git a/ultramodern/src/save.cpp b/ultramodern/src/save.cpp +new file mode 100644 +index 0000000..9a216f4 +--- /dev/null ++++ b/ultramodern/src/save.cpp +@@ -0,0 +1,212 @@ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++struct { ++ std::vector save_buffer; ++ std::thread saving_thread; ++ std::filesystem::path save_base_path; ++ std::filesystem::path save_file_path; ++ moodycamel::LightweightSemaphore write_sempahore; ++ // Used to tell the saving thread that a file swap is pending. ++ moodycamel::LightweightSemaphore swap_file_pending_sempahore; ++ // Used to tell the consumer thread that the saving thread is ready for a file swap. ++ moodycamel::LightweightSemaphore swap_file_ready_sempahore; ++ std::mutex save_buffer_mutex; ++} save_context; ++ ++// The current game's save directory within the config path. ++const std::u8string save_folder = u8"saves"; ++ ++// The current game's config directory path. ++extern std::filesystem::path config_path; ++ ++// The current game's save type. ++ultramodern::SaveType save_type = ultramodern::SaveType::None; ++ ++void ultramodern::set_save_type(ultramodern::SaveType type) { ++ save_type = type; ++} ++ ++ultramodern::SaveType ultramodern::get_save_type() { ++ return save_type; ++} ++ ++bool ultramodern::eeprom_allowed() { ++ return ++ save_type == SaveType::Eep4k || ++ save_type == SaveType::Eep16k || ++ save_type == SaveType::AllowAll; ++} ++ ++bool ultramodern::sram_allowed() { ++ return ++ save_type == SaveType::Sram || ++ save_type == SaveType::AllowAll; ++} ++ ++bool ultramodern::flashram_allowed() { ++ return ++ save_type == SaveType::Flashram || ++ save_type == SaveType::AllowAll; ++} ++ ++std::filesystem::path ultramodern::get_save_base_path() { ++ return save_context.save_base_path; ++} ++ ++std::filesystem::path ultramodern::get_save_file_path() { ++ return save_context.save_file_path; ++} ++ ++void ultramodern::set_save_file_path(const std::u8string& subfolder, const std::u8string& name) { ++ save_context.save_base_path = config_path / save_folder; ++ if (!subfolder.empty()) { ++ save_context.save_base_path = save_context.save_base_path / subfolder; ++ } ++ save_context.save_file_path = save_context.save_base_path / (name + u8".bin"); ++} ++ ++void update_save_file() { ++ bool saving_failed = false; ++ { ++ std::ofstream save_file = ultramodern::open_output_file_with_backup(ultramodern::get_save_file_path(), std::ios_base::binary); ++ ++ if (save_file.good()) { ++ std::lock_guard lock{ save_context.save_buffer_mutex }; ++ save_file.write(save_context.save_buffer.data(), save_context.save_buffer.size()); ++ } ++ else { ++ saving_failed = true; ++ } ++ } ++ if (!saving_failed) { ++ saving_failed = !ultramodern::finalize_output_file_with_backup(ultramodern::get_save_file_path()); ++ } ++ if (saving_failed) { ++ ultramodern::error_handling::message_box("Failed to write to the save file. Check your file permissions and whether the save folder has been moved to Dropbox or similar, as this can cause issues."); ++ } ++} ++ ++extern std::atomic_bool exited; ++ ++void saving_thread_func(RDRAM_ARG1) { ++ while (!exited) { ++ bool save_buffer_updated = false; ++ // Repeatedly wait for a new action to be sent. ++ constexpr int64_t wait_time_microseconds = 10000; ++ constexpr int max_actions = 128; ++ int num_actions = 0; ++ ++ // Wait up to the given timeout for a write to come in. Allow multiple writes to coalesce together into a single save. ++ // Cap the number of coalesced writes to guarantee that the save buffer eventually gets written out to the file even if the game ++ // is constantly sending writes. ++ while (save_context.write_sempahore.wait(wait_time_microseconds) && num_actions < max_actions) { ++ save_buffer_updated = true; ++ num_actions++; ++ } ++ ++ // If an action came through that affected the save file, save the updated contents. ++ if (save_buffer_updated) { ++ update_save_file(); ++ } ++ ++ if (save_context.swap_file_pending_sempahore.tryWait()) { ++ save_context.swap_file_ready_sempahore.signal(); ++ } ++ } ++} ++ ++void ultramodern::save_write_ptr(const void* in, uint32_t offset, uint32_t count) { ++ assert(offset + count <= save_context.save_buffer.size()); ++ ++ { ++ std::lock_guard lock { save_context.save_buffer_mutex }; ++ memcpy(&save_context.save_buffer[offset], in, count); ++ } ++ ++ save_context.write_sempahore.signal(); ++} ++ ++void ultramodern::save_read_ptr(void *out, uint32_t offset, uint32_t count) { ++ assert(offset + count <= save_context.save_buffer.size()); ++ ++ std::lock_guard lock { save_context.save_buffer_mutex }; ++ std::memcpy(out, &save_context.save_buffer[offset], count); ++} ++ ++void ultramodern::save_clear(uint32_t start, uint32_t size, char value) { ++ assert(start + size < save_context.save_buffer.size()); ++ ++ { ++ std::lock_guard lock { save_context.save_buffer_mutex }; ++ std::fill_n(save_context.save_buffer.begin() + start, size, value); ++ } ++ ++ save_context.write_sempahore.signal(); ++} ++ ++size_t ultramodern::get_save_size(ultramodern::SaveType save_type) { ++ switch (save_type) { ++ case ultramodern::SaveType::AllowAll: ++ case ultramodern::SaveType::Flashram: ++ return 0x20000; ++ case ultramodern::SaveType::Sram: ++ return 0x8000; ++ case ultramodern::SaveType::Eep16k: ++ return 0x800; ++ case ultramodern::SaveType::Eep4k: ++ return 0x200; ++ case ultramodern::SaveType::None: ++ return 0; ++ } ++ return 0; ++} ++ ++void read_save_file() { ++ std::filesystem::path save_file_path = ultramodern::get_save_file_path(); ++ ++ // Ensure the save file directory exists. ++ std::filesystem::create_directories(save_file_path.parent_path()); ++ ++ // Read the save file if it exists. ++ std::ifstream save_file = ultramodern::open_input_file_with_backup(save_file_path, std::ios_base::binary); ++ if (save_file.good()) { ++ save_file.read(save_context.save_buffer.data(), save_context.save_buffer.size()); ++ } ++ else { ++ // Otherwise clear the save file to all zeroes. ++ std::fill(save_context.save_buffer.begin(), save_context.save_buffer.end(), 0); ++ } ++} ++ ++void ultramodern::init_saving(RDRAM_ARG const std::u8string& name) { ++ set_save_file_path(u8"", name); ++ ++ save_context.save_buffer.resize(get_save_size(ultramodern::get_save_type())); ++ ++ read_save_file(); ++ ++ save_context.saving_thread = std::thread{saving_thread_func, PASS_RDRAM}; ++} ++ ++void ultramodern::change_save_file(const std::u8string& subfolder, const std::u8string& name) { ++ // Tell the saving thread that a file swap is pending. ++ save_context.swap_file_pending_sempahore.signal(); ++ // Wait until the saving thread indicates it's ready to swap files. ++ save_context.swap_file_ready_sempahore.wait(); ++ // Perform the save file swap. ++ set_save_file_path(subfolder, name); ++ read_save_file(); ++} ++ ++void ultramodern::join_saving_thread() { ++ if (save_context.saving_thread.joinable()) { ++ save_context.saving_thread.join(); ++ } ++} ++ diff --git a/src/game/config.cpp b/src/game/config.cpp index c82d5fd..d4a01f7 100644 --- a/src/game/config.cpp +++ b/src/game/config.cpp @@ -4,7 +4,7 @@ #include "zelda_render.h" #include "zelda_support.h" #include "ultramodern/config.hpp" -#include "librecomp/files.hpp" +#include "ultramodern/files.hpp" #include #include #include @@ -208,7 +208,7 @@ bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& j } // Try reading and parsing the backup file. - if (read_json(recomp::open_input_backup_file(path), json_out)) { + if (read_json(ultramodern::open_input_backup_file(path), json_out)) { return true; } @@ -218,14 +218,14 @@ bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& j bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::json& json_data) { { - std::ofstream output_file = recomp::open_output_file_with_backup(path); + std::ofstream output_file = ultramodern::open_output_file_with_backup(path); if (!output_file.good()) { return false; } output_file << std::setw(4) << json_data; } - return recomp::finalize_output_file_with_backup(path); + return ultramodern::finalize_output_file_with_backup(path); } bool save_general_config(const std::filesystem::path& path) { @@ -407,8 +407,9 @@ bool save_controls_config(const std::filesystem::path& path) { for (int p = 0; p < recomp::max_ports; p++) { nlohmann::json port_json{}; - // Save mode + // Save mode and pak type recomp::to_json(port_json["mode"], recomp::get_port_mode(p)); + recomp::to_json(port_json["pak_type"], recomp::get_port_pak_type(p)); // Save bindings port_json["keyboard"] = {}; @@ -481,10 +482,12 @@ bool load_controls_config(const std::filesystem::path& path) { for (int p = 0; p < recomp::max_ports && p < (int)ports_array.size(); p++) { const nlohmann::json& port_json = ports_array[p]; - // Load mode + // Load mode and pak type recomp::ControllerPortMode mode = from_or_default(port_json, "mode", p == 0 ? recomp::ControllerPortMode::Keyboard : recomp::ControllerPortMode::Off); recomp::set_port_mode(p, mode); + recomp::PakType pak_type = from_or_default(port_json, "pak_type", recomp::PakType::ControllerPak); + recomp::set_port_pak_type(p, pak_type); // Load bindings if (!load_input_device_from_json(port_json, recomp::InputDevice::Keyboard, "keyboard", p)) { diff --git a/src/game/input.cpp b/src/game/input.cpp index 040d114..ef18e0a 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -31,6 +31,14 @@ static std::array, recomp::max_ports> po recomp::ControllerPortMode::Off, }; +// Per-port pak type state (default to ControllerPak) +static std::array, recomp::max_ports> port_pak_types = { + recomp::PakType::ControllerPak, + recomp::PakType::ControllerPak, + recomp::PakType::ControllerPak, + recomp::PakType::ControllerPak, +}; + recomp::ControllerPortMode recomp::get_port_mode(int port) { if (port < 0 || port >= recomp::max_ports) return recomp::ControllerPortMode::Off; return port_modes[port].load(); @@ -45,6 +53,16 @@ int recomp::get_port_count() { return recomp::max_ports; } +recomp::PakType recomp::get_port_pak_type(int port) { + if (port < 0 || port >= recomp::max_ports) return recomp::PakType::ControllerPak; + return port_pak_types[port].load(); +} + +void recomp::set_port_pak_type(int port, recomp::PakType type) { + if (port < 0 || port >= recomp::max_ports) return; + port_pak_types[port].store(type); +} + static struct { const Uint8* keys = nullptr; SDL_Keymod keymod = SDL_Keymod::KMOD_NONE; @@ -639,9 +657,20 @@ void recomp::set_rumble(int controller_num, bool on) { ultramodern::input::connected_device_info_t recomp::get_connected_device_info(int controller_num) { if (controller_num >= 0 && controller_num < recomp::max_ports) { if (port_modes[controller_num].load() != recomp::ControllerPortMode::Off) { + ultramodern::input::Pak pak = ultramodern::input::Pak::None; + switch (port_pak_types[controller_num].load()) { + case recomp::PakType::ControllerPak: + pak = ultramodern::input::Pak::ControllerPak; + break; + case recomp::PakType::RumblePak: + pak = ultramodern::input::Pak::RumblePak; + break; + default: + break; + } return ultramodern::input::connected_device_info_t { .connected_device = ultramodern::input::Device::Controller, - .connected_pak = ultramodern::input::Pak::RumblePak, + .connected_pak = pak, }; } } diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp index c52f184..32998c1 100644 --- a/src/ui/ui_config.cpp +++ b/src/ui/ui_config.cpp @@ -97,6 +97,7 @@ static int focused_input_index = -1; static int focused_config_option_index = -1; static int selected_port = 0; static int port_mode_index = 0; // 0=Off, 1=Keyboard, 2=Controller +static int port_pak_type_index = 0; // 0=ControllerPak, 1=RumblePak static int port_controller_index = -1; static std::vector connected_controller_names; @@ -788,6 +789,10 @@ class ConfigMenu : public recompui::MenuController { selected_port = 0; cur_device = recomp::InputDevice::Keyboard; port_mode_index = static_cast(recomp::ControllerPortMode::Keyboard); + port_pak_type_index = static_cast(recomp::PakType::ControllerPak); + for (int p = 0; p < recomp::max_ports; p++) { + recomp::set_port_pak_type(p, recomp::PakType::ControllerPak); + } refresh_controller_list(); zelda64::save_config(); model_handle.DirtyAllVariables(); @@ -820,6 +825,7 @@ class ConfigMenu : public recompui::MenuController { selected_port = inputs.at(0).Get(); recomp::ControllerPortMode mode = recomp::get_port_mode(selected_port); port_mode_index = static_cast(mode); + port_pak_type_index = static_cast(recomp::get_port_pak_type(selected_port)); // Sync cur_device with the selected port's mode if (mode == recomp::ControllerPortMode::Controller) { cur_device = recomp::InputDevice::Controller; @@ -960,6 +966,7 @@ class ConfigMenu : public recompui::MenuController { constructor.Bind("active_binding_slot", &scanned_binding_index); constructor.Bind("selected_port", &selected_port); constructor.Bind("port_mode_index", &port_mode_index); + constructor.Bind("port_pak_type_index", &port_pak_type_index); constructor.Bind("port_controller_index", &port_controller_index); constructor.RegisterArray>(); constructor.Bind("connected_controller_names", &connected_controller_names); @@ -974,6 +981,27 @@ class ConfigMenu : public recompui::MenuController { } }); + constructor.BindFunc("pak_type", [](Rml::Variant& out) { + recomp::PakType type = recomp::get_port_pak_type(selected_port); + switch (type) { + case recomp::PakType::ControllerPak: out = "Controller Pak"; break; + case recomp::PakType::RumblePak: out = "Rumble Pak"; break; + default: out = "Controller Pak"; break; + } + }); + + constructor.BindEventCallback("set_pak_type", + [](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) { + std::string pak_str = inputs.at(0).Get(); + recomp::PakType pak = recomp::PakType::ControllerPak; + if (pak_str == "RumblePak") pak = recomp::PakType::RumblePak; + recomp::set_port_pak_type(selected_port, pak); + port_pak_type_index = static_cast(pak); + model_handle.DirtyVariable("pak_type"); + model_handle.DirtyVariable("port_pak_type_index"); + zelda64::save_config(); + }); + constructor.BindFunc("assigned_controller_name", [](Rml::Variant& out) { out = recomp::get_port_controller_name(selected_port); });