From 91db87632c2bfb6995ef1554ec71b11977c621f8 Mon Sep 17 00:00:00 2001
From: Wiseguy <68165316+Mr-Wiseguy@users.noreply.github.com>
Date: Fri, 14 Feb 2025 18:38:10 -0500
Subject: [PATCH] Mod Support (#499)
Integrates the modding functionality in N64ModernRuntime and adds several exported functions for mods to use. Also adds a ROM decompressor so that the runtime has access to the uncompressed code in the ROM for hooking purposes.
---
.github/workflows/validate.yml | 19 +-
.gitmodules | 3 -
CMakeLists.txt | 33 ++-
README.md | 2 +-
Zelda64RecompSyms | 2 +-
assets/launcher.rml | 2 +-
include/recomp_input.h | 5 +-
include/zelda_game.h | 5 +
include/zelda_render.h | 11 +-
lib/N64ModernRuntime | 2 +-
lib/rt64 | 2 +-
lib/sse2neon | 1 -
patches.toml | 6 +-
patches/autosaving.c | 134 +++++++++---
patches/camera_patches.c | 122 ++++++++---
patches/custom_syms.toml | 16 ++
patches/effect_patches.c | 4 +-
patches/graphics.h | 3 +-
patches/input.c | 178 ++++++++++++---
patches/input.h | 9 +-
patches/input_latency.c | 5 +-
patches/misc_funcs.h | 2 +-
patches/options.c | 5 +-
patches/patch_helpers.h | 2 +-
patches/patches.h | 5 +
patches/patches.ld | 5 +-
patches/play_patches.c | 358 +++++++++++++++++++++++++++++++
patches/print.c | 2 +-
patches/required_patches.c | 5 +
patches/sky_transform_tagging.c | 2 +-
patches/syms.ld | 11 +-
patches/ui_patches.c | 3 +-
src/game/config.cpp | 1 -
src/game/input.cpp | 2 +-
src/game/recomp_api.cpp | 19 +-
src/game/rom_decompression.cpp | 168 +++++++++++++++
src/main/main.cpp | 118 ++++++++--
src/main/register_patches.cpp | 3 +
src/main/rt64_render_context.cpp | 71 ++++--
src/ui/ui_launcher.cpp | 6 +-
src/ui/ui_renderer.cpp | 6 +
us.rev1.toml | 1 +
42 files changed, 1172 insertions(+), 187 deletions(-)
delete mode 160000 lib/sse2neon
create mode 100644 patches/custom_syms.toml
create mode 100644 src/game/rom_decompression.cpp
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 4440cc2..5a79eae 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -9,7 +9,7 @@ on:
N64RECOMP_COMMIT:
type: string
required: false
- default: '5b17bf8bb556d2544c6161487232a455eae8f188'
+ default: '198de1b5cf6e58415948588584750c51562d58dd'
DXC_CHECKSUM:
type: string
required: false
@@ -22,7 +22,7 @@ concurrency:
cancel-in-progress: true
jobs:
build-linux:
- runs-on: ${{ matrix.os }}
+ runs-on: ${{ format('blaze/compute/{0}-amd64', matrix.os) }}
container:
image: dcvz/n64recomp:ubuntu-18.04
volumes:
@@ -95,7 +95,7 @@ jobs:
rm -rf assets/scss
tar -czf Zelda64Recompiled.tar.gz Zelda64Recompiled assets/ gamecontrollerdb.txt
- name: Archive Zelda64Recomp
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: Zelda64Recompiled-${{ runner.os }}-X64-${{ matrix.type }}
path: Zelda64Recompiled.tar.gz
@@ -103,12 +103,12 @@ jobs:
run: |-
./.github/linux/appimage.sh
- name: Zelda64Recomp AppImage
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: Zelda64Recompiled-AppImage-X64-${{ matrix.type }}
path: Zelda64Recompiled-*.AppImage
build-linux-arm64:
- runs-on: ${{ format('blaze/{0}', matrix.os) }}
+ runs-on: ${{ format('blaze/compute/{0}', matrix.os) }}
strategy:
matrix:
type: [ Debug, Release ]
@@ -199,7 +199,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
- type: [ Debug, Release ]
+ type: [ Debug, RelWithDebInfo ]
name: windows (${{ matrix.type }})
steps:
- name: Checkout
@@ -264,6 +264,7 @@ jobs:
Move-Item -Path "cmake-build/dxcompiler.dll" -Destination "dxcompiler.dll"
Move-Item -Path "cmake-build/dxil.dll" -Destination "dxil.dll"
Move-Item -Path "cmake-build/SDL2.dll" -Destination "SDL2.dll"
+ Move-Item -Path "cmake-build/Zelda64Recompiled.pdb" -Destination "Zelda64Recompiled.pdb"
Remove-Item -Path "assets/scss" -Recurse -Force
- name: Archive Zelda64Recomp
uses: actions/upload-artifact@v4
@@ -276,3 +277,9 @@ jobs:
SDL2.dll
assets/
gamecontrollerdb.txt
+ - name: Archive Debug Files
+ uses: actions/upload-artifact@v4
+ with:
+ name: Zelda64Recompiled-PDB-${{ matrix.type }}
+ path: |
+ Zelda64Recompiled.pdb
diff --git a/.gitmodules b/.gitmodules
index 72562af..55acbb5 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -13,9 +13,6 @@
[submodule "lib/lunasvg"]
path = lib/lunasvg
url = https://github.com/sammycage/lunasvg
-[submodule "lib/sse2neon"]
- path = lib/sse2neon
- url = https://github.com/DLTcollab/sse2neon.git
[submodule "lib/N64ModernRuntime"]
path = lib/N64ModernRuntime
url = https://github.com/N64Recomp/N64ModernRuntime.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 62bce6f..58e2f42 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,6 +28,7 @@ if (WIN32)
endif()
set(RT64_STATIC TRUE)
+set(RT64_SDL_WINDOW_VULKAN TRUE)
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/rt64 ${CMAKE_BINARY_DIR}/rt64)
# set(BUILD_SHARED_LIBS_SAVED "${BUILD_SHARED_LIBS}")
@@ -57,6 +58,7 @@ target_include_directories(RecompiledFuncs PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/ultramodern/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/librecomp/include
+ ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/N64Recomp/include
)
file(GLOB FUNC_C_SOURCES ${CMAKE_SOURCE_DIR}/RecompiledFuncs/*.c)
@@ -78,6 +80,7 @@ target_include_directories(PatchesLib PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/ultramodern/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/librecomp/include
+ ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/N64Recomp/include
)
target_sources(PatchesLib PRIVATE
@@ -96,12 +99,8 @@ if(NOT DEFINED PATCHES_LD)
set(PATCHES_LD ld.lld)
endif()
-if(NOT DEFINED PATCHES_OBJCOPY)
- set(PATCHES_OBJCOPY llvm-objcopy)
-endif()
-
add_custom_target(PatchesBin
- COMMAND ${CMAKE_COMMAND} -E env CC=${PATCHES_C_COMPILER} LD=${PATCHES_LD} OBJCOPY=${PATCHES_OBJCOPY} make
+ COMMAND ${CMAKE_COMMAND} -E env CC=${PATCHES_C_COMPILER} LD=${PATCHES_LD} make
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/patches
BYPRODUCTS ${CMAKE_SOURCE_DIR}/patches/patches.elf
)
@@ -138,16 +137,6 @@ add_custom_target(DownloadGameControllerDB
add_executable(Zelda64Recompiled)
add_dependencies(Zelda64Recompiled DownloadGameControllerDB)
-# Generate mm_shader_cache.c from the MM shader cache if it exists
-if (EXISTS ${CMAKE_SOURCE_DIR}/shadercache/mm_shader_cache.bin)
- set(HAS_MM_SHADER_CACHE TRUE)
- target_compile_definitions(Zelda64Recompiled PRIVATE HAS_MM_SHADER_CACHE)
- add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/mm_shader_cache.c ${CMAKE_CURRENT_BINARY_DIR}/mm_shader_cache.h
- COMMAND file_to_c ${CMAKE_SOURCE_DIR}/shadercache/mm_shader_cache.bin mm_shader_cache_bytes ${CMAKE_CURRENT_BINARY_DIR}/mm_shader_cache.c ${CMAKE_CURRENT_BINARY_DIR}/mm_shader_cache.h
- DEPENDS ${CMAKE_SOURCE_DIR}/shadercache/mm_shader_cache.bin
- )
-endif()
-
set (SOURCES
${CMAKE_SOURCE_DIR}/src/main/main.cpp
${CMAKE_SOURCE_DIR}/src/main/register_overlays.cpp
@@ -161,6 +150,7 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/game/debug.cpp
${CMAKE_SOURCE_DIR}/src/game/quicksaving.cpp
${CMAKE_SOURCE_DIR}/src/game/recomp_api.cpp
+ ${CMAKE_SOURCE_DIR}/src/game/rom_decompression.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_renderer.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_launcher.cpp
@@ -174,12 +164,9 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/lib/RmlUi/Backends/RmlUi_Platform_SDL.cpp
)
-if (HAS_MM_SHADER_CACHE)
- list(APPEND SOURCES ${CMAKE_CURRENT_BINARY_DIR}/mm_shader_cache.c)
-endif()
-
target_include_directories(Zelda64Recompiled PRIVATE
${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/N64Recomp/include
${CMAKE_SOURCE_DIR}/lib/concurrentqueue
${CMAKE_SOURCE_DIR}/lib/GamepadMotionHelpers
${CMAKE_SOURCE_DIR}/lib/RmlUi/Include
@@ -190,7 +177,6 @@ target_include_directories(Zelda64Recompiled PRIVATE
${CMAKE_SOURCE_DIR}/lib/rt64/src
${CMAKE_SOURCE_DIR}/lib/rt64/src/rhi
${CMAKE_SOURCE_DIR}/lib/rt64/src/render
- ${CMAKE_SOURCE_DIR}/lib/sse2neon
${CMAKE_SOURCE_DIR}/lib/freetype-windows-binaries/include
${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/nativefiledialog-extended/src/include
${CMAKE_BINARY_DIR}/shaders
@@ -210,6 +196,11 @@ else()
)
endif()
+if (MSVC)
+ # Disable identical code folding, since this breaks mod function patching as multiple functions can get merged into one.
+ target_link_options(Zelda64Recompiled PRIVATE /OPT:NOICF)
+endif()
+
if (WIN32)
include(FetchContent)
@@ -256,6 +247,8 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux")
find_package(SDL2 REQUIRED)
find_package(X11 REQUIRED)
+ add_compile_definitions("RT64_SDL_WINDOW_VULKAN")
+
# Generate icon_bytes.c from the app icon PNG.
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/icons/512.png icon_bytes ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
diff --git a/README.md b/README.md
index d840f9e..95f9aa3 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Zelda 64: Recompiled is a project that uses [N64: Recompiled](https://github.com
-_Thank you [Blaze](https://runblaze.dev) for supporting this project by providing Linux ARM64 and Apple Silicon macOS Github Action Runners!_
+_Special thanks to [Blaze](https://runblaze.dev) for their support of this project. They provide high-performance Linux (AMD64 & ARM64) and Apple Silicon macOS runners for GitHub Actions, greatly reducing our automated build times._
diff --git a/Zelda64RecompSyms b/Zelda64RecompSyms
index 50d63de..a325f8f 160000
--- a/Zelda64RecompSyms
+++ b/Zelda64RecompSyms
@@ -1 +1 @@
-Subproject commit 50d63debd58ce9f01957142ea91b67b6c7018c96
+Subproject commit a325f8fa5c48c925616bd43685ef14bbabc29537
diff --git a/assets/launcher.rml b/assets/launcher.rml
index d4a77d1..6496117 100644
--- a/assets/launcher.rml
+++ b/assets/launcher.rml
@@ -58,7 +58,7 @@
-
+
diff --git a/include/recomp_input.h b/include/recomp_input.h
index db9b160..8599628 100644
--- a/include/recomp_input.h
+++ b/include/recomp_input.h
@@ -120,7 +120,8 @@ namespace recomp {
std::vector apply_menu;
};
- constexpr const std::vector& get_default_mapping_for_input(const DefaultN64Mappings& defaults, const GameInput input) {
+ inline const std::vector& get_default_mapping_for_input(const DefaultN64Mappings& defaults, const GameInput input) {
+ static const std::vector empty_input_field{};
switch (input) {
case GameInput::A: return defaults.a;
case GameInput::B: return defaults.b;
@@ -143,7 +144,7 @@ namespace recomp {
case GameInput::TOGGLE_MENU: return defaults.toggle_menu;
case GameInput::ACCEPT_MENU: return defaults.accept_menu;
case GameInput::APPLY_MENU: return defaults.apply_menu;
- default: return std::vector();
+ default: return empty_input_field;
}
}
diff --git a/include/zelda_game.h b/include/zelda_game.h
index ac63979..739eaa0 100644
--- a/include/zelda_game.h
+++ b/include/zelda_game.h
@@ -1,9 +1,14 @@
#ifndef __ZELDA_GAME_H__
#define __ZELDA_GAME_H__
+#include
+#include
+#include
+
namespace zelda64 {
void quicksave_save();
void quicksave_load();
+ std::vector decompress_mm(std::span compressed_rom);
};
#endif
diff --git a/include/zelda_render.h b/include/zelda_render.h
index 684672b..0143471 100644
--- a/include/zelda_render.h
+++ b/include/zelda_render.h
@@ -1,8 +1,12 @@
#ifndef __ZELDA_RENDER_H__
#define __ZELDA_RENDER_H__
+#include
+#include
+
#include "common/rt64_user_configuration.h"
#include "ultramodern/renderer_context.hpp"
+#include "librecomp/mods.hpp"
namespace RT64 {
struct Application;
@@ -10,7 +14,7 @@ namespace RT64 {
namespace zelda64 {
namespace renderer {
- class RT64Context : public ultramodern::renderer::RendererContext {
+ class RT64Context final : public ultramodern::renderer::RendererContext {
public:
~RT64Context() override;
RT64Context(uint8_t *rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode);
@@ -25,10 +29,10 @@ namespace zelda64 {
void shutdown() override;
uint32_t get_display_framerate() const override;
float get_resolution_scale() const override;
- void load_shader_cache(std::span cache_binary) override;
protected:
std::unique_ptr app;
+ std::unordered_set enabled_texture_packs;
};
std::unique_ptr create_render_context(uint8_t *rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode);
@@ -36,6 +40,9 @@ namespace zelda64 {
RT64::UserConfiguration::Antialiasing RT64MaxMSAA();
bool RT64SamplePositionsSupported();
bool RT64HighPrecisionFBEnabled();
+
+ void enable_texture_pack(const recomp::mods::ModHandle& mod);
+ void disable_texture_pack(const recomp::mods::ModHandle& mod);
}
}
diff --git a/lib/N64ModernRuntime b/lib/N64ModernRuntime
index 0a53855..0afeb08 160000
--- a/lib/N64ModernRuntime
+++ b/lib/N64ModernRuntime
@@ -1 +1 @@
-Subproject commit 0a538553330ea5fdb1673708704bb92a854241b9
+Subproject commit 0afeb089a55cb391c24352f23b7683ab3c2ca854
diff --git a/lib/rt64 b/lib/rt64
index 88c618c..0ca92ee 160000
--- a/lib/rt64
+++ b/lib/rt64
@@ -1 +1 @@
-Subproject commit 88c618c1f8d8089f94e78537e49e0d77798fc0be
+Subproject commit 0ca92eeb6c2f58ce3581c65f87f7261b8ac0fea0
diff --git a/lib/sse2neon b/lib/sse2neon
deleted file mode 160000
index 42c7047..0000000
--- a/lib/sse2neon
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 42c704755d3ec218ed9126a122f0a667beeb630a
diff --git a/patches.toml b/patches.toml
index 6d928be..18a8856 100644
--- a/patches.toml
+++ b/patches.toml
@@ -9,8 +9,12 @@ single_file_output = true
use_absolute_symbols = true
# Point the recompiler at the symbol files so that it can resolve relocations during recompilation.
func_reference_syms_file = "Zelda64RecompSyms/mm.us.rev1.syms.toml"
-data_reference_syms_files = [ "Zelda64RecompSyms/mm.us.rev1.datasyms.toml", "Zelda64RecompSyms/mm.us.rev1.datasyms_static.toml" ]
+data_reference_syms_files = [ "Zelda64RecompSyms/mm.us.rev1.datasyms.toml", "Zelda64RecompSyms/mm.us.rev1.datasyms_static.toml", "patches/custom_syms.toml" ]
# Tell the recompiler to write the output binary. Doing this instead of using objcopy allows the recompiler to patch MIPS32 relocs.
output_binary_path = "patches/patches.bin"
# Do not emit warnings for unpaired LO16 values, as clang produces many of them.
unpaired_lo16_warnings = false
+# Allow exporting functions and events for mods to use.
+allow_exports = true
+# # Enable strict patch mode, validates that patched symbols exist and that non-patch functions aren't symbols.
+strict_patch_mode = true
diff --git a/patches/autosaving.c b/patches/autosaving.c
index 46f1e8a..da46a57 100644
--- a/patches/autosaving.c
+++ b/patches/autosaving.c
@@ -67,10 +67,10 @@ RECOMP_PATCH void KaleidoSetup_Update(PlayState* play) {
void Sram_SyncWriteToFlash(SramContext* sramCtx, s32 curPage, s32 numPages);
-void autosave_reset_timer();
-void autosave_reset_timer_slow();
+void recomp_reset_autosave_timer();
+void recomp_reset_autosave_timer_slow();
-void do_autosave(PlayState* play) {
+RECOMP_EXPORT void recomp_do_autosave(PlayState* play) {
// Transfer the scene flags into the cycle flags.
Play_SaveCycleSceneFlags(&play->state);
// Transfer the cycle flags into the save buffer. Logic copied from func_8014546C.
@@ -99,13 +99,22 @@ void do_autosave(PlayState* play) {
gSaveContext.save.isOwlSave = false;
}
-// @recomp Do not clear the save if the save was an autosave.
+bool loading_deletes_owl_save = true;
+
+// @recomp_export void recomp_set_loading_deletes_owl_save(bool new_val): Set whether loading an owl save should also delete it.
+RECOMP_EXPORT void recomp_set_loading_deletes_owl_save(bool new_val)
+{
+ loading_deletes_owl_save = new_val;
+}
+
+// @recomp Do not clear the save if the save was an autosave, or if mods have disabled save deletion.
RECOMP_PATCH void func_80147314(SramContext* sramCtx, s32 fileNum) {
s32 save_type = gSaveContext.save.isOwlSave;
gSaveContext.save.isOwlSave = false;
- // @recomp Prevent owl save/autosave deletion if autosaving is enabled.
- if (!recomp_autosave_enabled()) {
+ // @recomp Prevent owl save/autosave deletion if autosaving is enabled, and...
+ // @recomp_use_export_var loading_deletes_owl_save: Prevent owl save deletion if mods disable it.
+ if (!recomp_get_autosave_enabled() && loading_deletes_owl_save) {
gSaveContext.save.saveInfo.playerData.newf[0] = '\0';
gSaveContext.save.saveInfo.playerData.newf[1] = '\0';
gSaveContext.save.saveInfo.playerData.newf[2] = '\0';
@@ -176,7 +185,7 @@ RECOMP_PATCH void func_8014546C(SramContext* sramCtx) {
// @recomp Delete the owl save.
delete_owl_save(sramCtx, gSaveContext.fileNum);
// @recomp Reset the autosave timer.
- autosave_reset_timer();
+ recomp_reset_autosave_timer();
for (i = 0; i < ARRAY_COUNT(gSaveContext.cycleSceneFlags); i++) {
gSaveContext.save.saveInfo.permanentSceneFlags[i].chest = gSaveContext.cycleSceneFlags[i].chest;
gSaveContext.save.saveInfo.permanentSceneFlags[i].switch0 = gSaveContext.cycleSceneFlags[i].switch0;
@@ -344,11 +353,11 @@ void draw_autosave_icon(PlayState* play) {
CLOSE_DISPS(play->state.gfxCtx);
}
-void show_autosave_icon() {
+RECOMP_EXPORT void recomp_show_autosave_icon() {
autosave_icon_counter = AUTOSAVE_ICON_TOTAL_FRAMES;
}
-u32 recomp_autosave_interval() {
+RECOMP_EXPORT u32 recomp_autosave_interval() {
return 2 * 60 * 1000;
}
@@ -367,12 +376,12 @@ bool reached_final_three_hours() {
return false;
}
-void autosave_reset_timer() {
+RECOMP_EXPORT void recomp_reset_autosave_timer() {
last_autosave_time = osGetTime();
extra_autosave_delay_milliseconds = 0;
}
-void autosave_reset_timer_slow() {
+RECOMP_EXPORT void recomp_reset_autosave_timer_slow() {
// Set the most recent autosave time in the future to give extra time before an autosave triggers.
last_autosave_time = osGetTime();
extra_autosave_delay_milliseconds = 2 * 60 * 1000;
@@ -381,7 +390,7 @@ void autosave_reset_timer_slow() {
void autosave_post_play_update(PlayState* play) {
static int frames_since_save_changed = 0;
static int frames_since_autosave_ready = 0;
- if (recomp_autosave_enabled()) {
+ if (recomp_get_autosave_enabled()) {
if (autosave_compare_saves(&gSaveContext, &prev_save_ctx)) {
frames_since_save_changed = 0;
Lib_MemCpy(&prev_save_ctx, &gSaveContext, offsetof(SaveContext, fileNum));
@@ -425,20 +434,20 @@ void autosave_post_play_update(PlayState* play) {
frames_since_autosave_ready >= MIN_FRAMES_SINCE_READY &&
time_now - last_autosave_time > (OS_USEC_TO_CYCLES(1000 * (recomp_autosave_interval() + extra_autosave_delay_milliseconds)))
) {
- do_autosave(play);
- show_autosave_icon();
- autosave_reset_timer();
+ recomp_do_autosave(play);
+ recomp_show_autosave_icon();
+ recomp_reset_autosave_timer();
}
}
else {
// Update the last autosave time to the current time to prevent autosaving immediately if autosaves are turned back on.
- autosave_reset_timer();
+ recomp_reset_autosave_timer();
}
gCanPause = false;
}
void autosave_init() {
- autosave_reset_timer_slow();
+ recomp_reset_autosave_timer_slow();
Lib_MemCpy(&prev_save_ctx, &gSaveContext, offsetof(SaveContext, fileNum));
}
@@ -522,6 +531,9 @@ s32 spawn_entrance_from_autosave_entrance(s16 autosave_entrance) {
}
}
+RECOMP_DECLARE_EVENT(recomp_on_load_save(FileSelectState* fileSelect, SramContext* sramCtx));
+RECOMP_DECLARE_EVENT(recomp_after_load_save(FileSelectState* fileSelect, SramContext* sramCtx));
+
// @recomp Patched to change the entrance for autosaves and initialize autosaves.
RECOMP_PATCH void Sram_OpenSave(FileSelectState* fileSelect, SramContext* sramCtx) {
s32 i;
@@ -530,6 +542,9 @@ RECOMP_PATCH void Sram_OpenSave(FileSelectState* fileSelect, SramContext* sramCt
s32 pad1;
s32 fileNum;
+ // @recomp_event recomp_on_load_save(FileSelectState* fileSelect, SramContext* sramCtx): A save-file was just chosen.
+ recomp_on_load_save(fileSelect, sramCtx);
+
if (gSaveContext.flashSaveAvailable) {
bzero(sramCtx->saveBuf, SAVE_BUFFER_SIZE);
@@ -653,29 +668,49 @@ RECOMP_PATCH void Sram_OpenSave(FileSelectState* fileSelect, SramContext* sramCt
// @recomp Initialize the autosave state tracking.
autosave_init();
+
+ // @recomp_event recomp_after_load_save(FileSelectState* fileSelect, SramContext* sramCtx): The save has finished loading.
+ recomp_after_load_save(fileSelect, sramCtx);
+}
+
+bool moon_crash_resets_save = true;
+
+// @recomp_export void recomp_set_moon_crash_resets_save(bool new_val): Set whether a moon crash should revert the player's save data.
+RECOMP_EXPORT void recomp_set_moon_crash_resets_save(bool new_val)
+{
+ moon_crash_resets_save = new_val;
}
extern s32 Actor_ProcessTalkRequest(Actor* actor, GameState* gameState);
+RECOMP_DECLARE_EVENT(recomp_on_moon_crash(SramContext* sramCtx));
+RECOMP_DECLARE_EVENT(recomp_after_moon_crash(SramContext* sramCtx));
+
// @recomp Reset the autosave timer when the moon crashes.
RECOMP_PATCH void Sram_ResetSaveFromMoonCrash(SramContext* sramCtx) {
s32 i;
s32 cutsceneIndex = gSaveContext.save.cutsceneIndex;
- bzero(sramCtx->saveBuf, SAVE_BUFFER_SIZE);
+ // @recomp_event recomp_on_moon_crash(SramContext* sramCtx): A moon crash has just been triggered.
+ recomp_on_moon_crash(sramCtx);
- if (SysFlashrom_ReadData(sramCtx->saveBuf, gFlashSaveStartPages[gSaveContext.fileNum * 2],
- gFlashSaveNumPages[gSaveContext.fileNum * 2]) != 0) {
- SysFlashrom_ReadData(sramCtx->saveBuf, gFlashSaveStartPages[gSaveContext.fileNum * 2 + 1],
- gFlashSaveNumPages[gSaveContext.fileNum * 2 + 1]);
+ if (moon_crash_resets_save)
+ {
+ bzero(sramCtx->saveBuf, SAVE_BUFFER_SIZE);
+
+ if (SysFlashrom_ReadData(sramCtx->saveBuf, gFlashSaveStartPages[gSaveContext.fileNum * 2],
+ gFlashSaveNumPages[gSaveContext.fileNum * 2]) != 0) {
+ SysFlashrom_ReadData(sramCtx->saveBuf, gFlashSaveStartPages[gSaveContext.fileNum * 2 + 1],
+ gFlashSaveNumPages[gSaveContext.fileNum * 2 + 1]);
+ }
+ Lib_MemCpy(&gSaveContext.save, sramCtx->saveBuf, sizeof(Save));
+ if (CHECK_NEWF(gSaveContext.save.saveInfo.playerData.newf)) {
+ SysFlashrom_ReadData(sramCtx->saveBuf, gFlashSaveStartPages[gSaveContext.fileNum * 2 + 1],
+ gFlashSaveNumPages[gSaveContext.fileNum * 2 + 1]);
+ Lib_MemCpy(&gSaveContext, sramCtx->saveBuf, sizeof(Save));
+ }
+ gSaveContext.save.cutsceneIndex = cutsceneIndex;
}
- Lib_MemCpy(&gSaveContext.save, sramCtx->saveBuf, sizeof(Save));
- if (CHECK_NEWF(gSaveContext.save.saveInfo.playerData.newf)) {
- SysFlashrom_ReadData(sramCtx->saveBuf, gFlashSaveStartPages[gSaveContext.fileNum * 2 + 1],
- gFlashSaveNumPages[gSaveContext.fileNum * 2 + 1]);
- Lib_MemCpy(&gSaveContext, sramCtx->saveBuf, sizeof(Save));
- }
- gSaveContext.save.cutsceneIndex = cutsceneIndex;
for (i = 0; i < ARRAY_COUNT(gSaveContext.eventInf); i++) {
gSaveContext.eventInf[i] = 0;
@@ -705,25 +740,56 @@ RECOMP_PATCH void Sram_ResetSaveFromMoonCrash(SramContext* sramCtx) {
gSaveContext.jinxTimer = 0;
// @recomp Use the slow autosave timer to give the player extra time to respond to the moon crashing to decide if they want to reload their autosave.
- autosave_reset_timer_slow();
+ recomp_reset_autosave_timer_slow();
+
+ // @recomp_event recomp_after_moon_crash(SramContext* sramCtx): The effects of moon crash have been written.
+ recomp_after_moon_crash(sramCtx);
}
-// @recomp If autosave is enabled, skip the part of the owl statue dialog that talks about the file being deleted on load, since it's not true.
+bool owls_save_and_quit = true;
+
+// @recomp_export void recomp_set_owls_save_and_quit(bool new_val): Set if owls should use their code to save and quit. If false is passed, owl saves now do nothing.
+RECOMP_EXPORT void recomp_set_owls_save_and_quit(bool new_val)
+{
+ owls_save_and_quit = new_val;
+}
+
+RECOMP_DECLARE_EVENT(recomp_on_owl_update(ObjWarpstone* this, PlayState* play));
+RECOMP_DECLARE_EVENT(recomp_on_owl_save(ObjWarpstone* this, PlayState* play));
+RECOMP_DECLARE_EVENT(recomp_after_owl_save(ObjWarpstone* this, PlayState* play));
+
+// @recomp If autosave is enabled or owl save deletion is disabled, skip the part of the owl statue dialog that talks about the file being deleted on load, since it's not true.
RECOMP_PATCH void ObjWarpstone_Update(Actor* thisx, PlayState* play) {
ObjWarpstone* this = (ObjWarpstone*)thisx;
s32 pad;
+ // @recomp_event recomp_on_owl_update(ObjWarpstone* this, PlayState* play): Allow mods to handle owl update frames.
+ recomp_on_owl_update(this, play);
+
if (this->isTalking) {
if (Actor_TextboxIsClosing(&this->dyna.actor, play)) {
this->isTalking = false;
} else if ((Message_GetState(&play->msgCtx) == TEXT_STATE_CHOICE) && Message_ShouldAdvance(play)) {
if (play->msgCtx.choiceIndex != 0) {
+ // @recomp_event recomp_on_owl_save(ObjWarpstone* this, PlayState* play): The player chose to save from an owl statue.
+ recomp_on_owl_save(this, play);
+
Audio_PlaySfx_MessageDecide();
- play->msgCtx.msgMode = MSGMODE_OWL_SAVE_0;
+
+ // @recomp_use_export_var owls_save_and_quit: Only use normal owl save if quit flag is set.
+ if (owls_save_and_quit) {
+ play->msgCtx.msgMode = MSGMODE_OWL_SAVE_0;
+ } else {
+ Message_CloseTextbox(play);
+ }
+
play->msgCtx.unk120D6 = 0;
play->msgCtx.unk120D4 = 0;
gSaveContext.save.owlWarpId = OBJ_WARPSTONE_GET_OWL_WARP_ID(&this->dyna.actor);
+
+ // @recomp_event recomp_after_owl_save(ObjWarpstone* this, PlayState* play): Owl save is finished.
+ recomp_after_owl_save(this, play);
} else {
Message_CloseTextbox(play);
}
@@ -734,8 +800,8 @@ RECOMP_PATCH void ObjWarpstone_Update(Actor* thisx, PlayState* play) {
Actor_OfferTalkNearColChkInfoCylinder(&this->dyna.actor, play);
}
- // @recomp Skip the text talking about the save being deleted on load, if autosave is enabled.
- if (recomp_autosave_enabled()) {
+ // @recomp_use_export_var loading_deletes_owl_save: Skip the text talking about the save being deleted on load, if autosave is enabled or if owl save deletion is disabled.
+ if (recomp_get_autosave_enabled() || !loading_deletes_owl_save) {
if (this->isTalking && play->msgCtx.currentTextId == 0xC01 && play->msgCtx.msgBufPos == 269) {
play->msgCtx.msgBufPos = 530;
}
diff --git a/patches/camera_patches.c b/patches/camera_patches.c
index f0ae7e0..938b2b8 100644
--- a/patches/camera_patches.c
+++ b/patches/camera_patches.c
@@ -9,6 +9,8 @@
#include "z64shrink_window.h"
#include "z64player.h"
+static bool camera_fixes = false;
+
static bool prev_analog_cam_active = false;
static bool can_use_analog_cam = false;
static bool analog_cam_active = false;
@@ -26,6 +28,10 @@ float analog_camera_y_sensitivity = 500.0f;
static const float analog_cam_threshold = 0.1f;
+RECOMP_EXPORT void recomp_set_camera_fixes(bool new_val) {
+ camera_fixes = new_val;
+}
+
void update_analog_camera_params(Camera* camera) {
// recomp_printf("Camera at: %.2f %.2f %.2f\n"
// " eye: %.2f %.2f %.2f\n"
@@ -48,17 +54,17 @@ void update_analog_camera_params(Camera* camera) {
void update_analog_cam(Camera* c) {
can_use_analog_cam = true;
-
+
Player* player = GET_PLAYER(c->play);
// recomp_printf(" val: %d\n", func_80123434(player));
-
+
// Check if the player just started Z targeting and reset to auto cam if so.
static bool prev_targeting_held = false;
bool targeting_held = func_80123434(player) || player->lockOnActor != NULL;
if (targeting_held && !prev_targeting_held) {
analog_cam_active = false;
}
-
+
// Enable analog cam if the right stick is held.
float input_x, input_y;
recomp_get_camera_inputs(&input_x, &input_y);
@@ -66,12 +72,12 @@ void update_analog_cam(Camera* c) {
if (fabsf(input_x) >= analog_cam_threshold || fabsf(input_y) >= analog_cam_threshold) {
analog_cam_active = true;
}
-
+
if (analog_cam_skip_once) {
analog_cam_active = false;
analog_cam_skip_once = false;
}
-
+
// Record the Z targeting state.
prev_targeting_held = targeting_held;
@@ -92,7 +98,7 @@ void update_analog_cam(Camera* c) {
analog_camera_pos.pitch += analog_camera_pitch_vel;
analog_camera_pos.yaw += analog_camera_yaw_vel;
-
+
if (analog_camera_pos.pitch > 0x36B0) {
analog_camera_pos.pitch = 0x36B0;
}
@@ -782,9 +788,9 @@ RECOMP_PATCH s32 Camera_Normal1(Camera* camera) {
}
// @recomp Update the analog camera.
- if (recomp_analog_cam_enabled()) {
+ if (recomp_get_analog_cam_enabled()) {
update_analog_cam(camera);
-
+
if (analog_cam_active) {
spB4.pitch = analog_camera_pos.pitch;
// spB4.r = analog_camera_pos.r;
@@ -838,7 +844,7 @@ RECOMP_PATCH s32 Camera_Normal1(Camera* camera) {
phi_f2 = (gSaveContext.save.saveInfo.playerData.health <= 0x10) ? 0.8f : 1.0f;
// @recomp Don't zoom in on low health when dual analog is used
- if (recomp_analog_cam_enabled()) {
+ if (recomp_get_analog_cam_enabled()) {
phi_f2 = 1.0f;
}
@@ -1017,14 +1023,14 @@ RECOMP_PATCH s32 Camera_Jump2(Camera* camera) {
camera->pitchUpdateRateInv = 100.0f;
camera->rUpdateRateInv = 100.0f;
}
-
+
spB4.pitch = CLAMP_MAX(spB4.pitch, DEG_TO_BINANG(60.43f));
spB4.pitch = CLAMP_MIN(spB4.pitch, -DEG_TO_BINANG(60.43f));
// @recomp Update the analog camera.
- if (recomp_analog_cam_enabled()) {
+ if (recomp_get_analog_cam_enabled()) {
update_analog_cam(camera);
-
+
if (analog_cam_active) {
spB4.pitch = analog_camera_pos.pitch;
// spB4.r = analog_camera_pos.r;
@@ -1064,6 +1070,10 @@ RECOMP_PATCH s32 Camera_Jump2(Camera* camera) {
*/
// @recomp Patched for analog cam.
RECOMP_PATCH s32 Camera_Parallel1(Camera* camera) {
+ // @recomp
+ static bool prev_targeting_held = false;
+ bool isChargingDekuFlowerDive = !!(((Player*)camera->focalActor)->stateFlags3 & PLAYER_STATE3_100);
+
Vec3f* eye = &camera->eye;
Vec3f* at = &camera->at;
Vec3f* eyeNext = &camera->eyeNext;
@@ -1089,6 +1099,10 @@ RECOMP_PATCH s32 Camera_Parallel1(Camera* camera) {
CameraModeValue* values;
f32 yNormal;
+ // @recomp Read timer4 from timer2.
+ s16 timer4 = rwData->timer2 >> 5; // @recomp Used to check if z-target can be quit. Mirrors timer2 values during z-target in unmodified function.
+ rwData->timer2 &= 0x001F;
+
if (!RELOAD_PARAMS(camera)) {
} else {
values = sCameraSettings[camera->setting].cameraModes[camera->mode].values;
@@ -1151,6 +1165,8 @@ RECOMP_PATCH s32 Camera_Parallel1(Camera* camera) {
rwData->timer2 = 20;
} else {
rwData->timer2 = 6;
+ // @recomp Initiate timer4 for z-target.
+ timer4 = 6;
}
if ((camera->focalActor == &GET_PLAYER(camera->play)->actor) && (camera->mode == CAM_MODE_CHARGE)) {
@@ -1187,12 +1203,55 @@ RECOMP_PATCH s32 Camera_Parallel1(Camera* camera) {
rwData->unk_26 = 1;
camera->animState = 1;
sCameraInterfaceFlags = roData->interfaceFlags;
+
+ // @recomp Reset prev_targeting_held after transition.
+ prev_targeting_held = false;
+
break;
}
+ // @recomp Change behavior for z-target only.
+ if (camera_fixes) {
+ if ((roData->interfaceFlags & (PARALLEL1_FLAG_3 | PARALLEL1_FLAG_2 | PARALLEL1_FLAG_1)) == PARALLEL1_FLAG_1) {
+ Player* player = GET_PLAYER(camera->play);
+ bool targeting_held = func_80123434(player) || player->lockOnActor != NULL;
+
+ // @recomp Fix camera rotating with player if z-target gets released too fast after transition.
+ if ((targeting_held) && (!prev_targeting_held)) {
+ // @recomp Reset timer2 to avoid immediate rotation, if player presses, releases and presses z-target in a very short time-window.
+ rwData->timer2 = 6;
+ rwData->unk_1E = BINANG_ROT180(camera->focalActorPosRot.rot.y) + roData->unk_22;
+ }
+
+ // @recomp Maintain vanilla behavior for quitting z-target.
+ if ((timer4 == 0) && (!targeting_held)) {
+ rwData->timer2 = 0;
+ }
+
+ // @recomp Decrease timer4 only in cases where timer2 would be decreased.
+ if ((timer4 > 0)
+ && (rwData->timer3 <= 0)) {
+ timer4--;
+ }
+
+ prev_targeting_held = targeting_held;
+ }
+ }
+
if (rwData->timer2 != 0) {
switch (roData->interfaceFlags & (PARALLEL1_FLAG_3 | PARALLEL1_FLAG_2 | PARALLEL1_FLAG_1)) {
case PARALLEL1_FLAG_1:
+ if (camera_fixes) {
+ // @recomp Fix camera rotating with player if z-target gets released too fast after transition.
+ if (isChargingDekuFlowerDive) {
+ // @recomp Fix camera wiggle during dive into deku flower.
+ rwData->unk_1E = BINANG_ROT180(camera->focalActorPosRot.rot.y) + roData->unk_22;
+ }
+ rwData->unk_20 = roData->unk_20;
+ break;
+ }
+ // @recomp fallthrough
+
case (PARALLEL1_FLAG_3 | PARALLEL1_FLAG_2 | PARALLEL1_FLAG_1):
rwData->unk_1E = BINANG_ROT180(camera->focalActorPosRot.rot.y) + roData->unk_22;
rwData->unk_20 = roData->unk_20;
@@ -1372,9 +1431,9 @@ RECOMP_PATCH s32 Camera_Parallel1(Camera* camera) {
}
// @recomp Update the analog camera.
- if (recomp_analog_cam_enabled()) {
+ if (recomp_get_analog_cam_enabled()) {
update_analog_cam(camera);
-
+
if (analog_cam_active) {
sp90.pitch = analog_camera_pos.pitch;
// sp90.r = analog_camera_pos.r;
@@ -1400,10 +1459,20 @@ RECOMP_PATCH s32 Camera_Parallel1(Camera* camera) {
func_800CBFA4(camera, at, eye, 3);
}
- if (rwData->timer2 != 0) {
- sUpdateCameraDirection = true;
+ if (camera_fixes) {
+ // @recomp Fix camera not updating input dir for the first few frames after transition.
+ if ((isChargingDekuFlowerDive) && (rwData->timer2 != 0)) {
+ // @recomp Fix camera wiggle during dive into deku flower.
+ sUpdateCameraDirection = true;
+ } else {
+ sUpdateCameraDirection = false;
+ }
} else {
- sUpdateCameraDirection = false;
+ if (rwData->timer2 != 0) {
+ sUpdateCameraDirection = true;
+ } else {
+ sUpdateCameraDirection = false;
+ }
}
}
@@ -1412,6 +1481,9 @@ RECOMP_PATCH s32 Camera_Parallel1(Camera* camera) {
camera->atLerpStepScale = Camera_ClampLerpScale(camera, sp72 ? roData->unk_1C : roData->unk_18);
rwData->unk_26 &= ~1;
+ // @recomp Save timer4 in unused bits of timer2.
+ rwData->timer2 |= timer4 << 5;
+
return 1;
}
@@ -1580,11 +1652,11 @@ RECOMP_PATCH s32 Camera_Normal3(Camera* camera) {
Camera_UnsetStateFlag(camera, CAM_STATE_DISABLE_MODE_CHANGE);
}
}
-
+
// @recomp Update the analog camera.
- if (recomp_analog_cam_enabled()) {
+ if (recomp_get_analog_cam_enabled()) {
update_analog_cam(camera);
-
+
if (analog_cam_active) {
sp80.pitch = analog_camera_pos.pitch;
// sp80.r = analog_camera_pos.r;
@@ -1820,9 +1892,9 @@ RECOMP_PATCH s32 Camera_Jump3(Camera* camera) {
}
// @recomp Update the analog camera.
- if (recomp_analog_cam_enabled()) {
+ if (recomp_get_analog_cam_enabled()) {
update_analog_cam(camera);
-
+
if (analog_cam_active) {
spAC.pitch = analog_camera_pos.pitch;
// spAC.r = analog_camera_pos.r;
@@ -1871,7 +1943,7 @@ void analog_cam_post_play_update(PlayState* play) {
// recomp_printf("prev_analog_cam_active: %d can_use_analog_cam: %d\n", prev_analog_cam_active, can_use_analog_cam);
// recomp_printf("setting: %d mode: %d func: %d\n", active_cam->setting, active_cam->mode, sCameraSettings[active_cam->setting].cameraModes[active_cam->mode].funcId);
// recomp_printf("active cam yaw %d\n", Camera_GetInputDirYaw(GET_ACTIVE_CAM(play)));
-
+
// Update parameters for the analog cam if the game is unpaused.
if (play->pauseCtx.state == PAUSE_STATE_OFF && R_PAUSE_BG_PRERENDER_STATE == PAUSE_BG_PRERENDER_OFF) {
update_analog_camera_params(active_cam);
@@ -1934,7 +2006,7 @@ RECOMP_PATCH void func_809EC568(Boss04* this, PlayState* play) {
this->unk_2D0 = 2000.0f;
// @recomp do not require being in c-up mode if analog cam is enabled
// also, use the new variable instead of the vanilla value to check if the player is looking at the boss.
- if (((player->stateFlags1 & PLAYER_STATE1_100000) || (recomp_analog_cam_enabled())) && (this->actor.projectedPos.z > 0.0f) &&
+ if (((player->stateFlags1 & PLAYER_STATE1_100000) || (recomp_get_analog_cam_enabled())) && (this->actor.projectedPos.z > 0.0f) &&
(fabsf(this->actor.projectedPos.x) < maxProjectedPosToStartFight) && (fabsf(this->actor.projectedPos.y) < maxProjectedPosToStartFight)) {
if ((this->unk_704 >= 15) && (CutsceneManager_GetCurrentCsId() == CS_ID_NONE)) {
Actor* boss;
@@ -2056,7 +2128,7 @@ RECOMP_PATCH void func_809EC568(Boss04* this, PlayState* play) {
if (this->unk_704 == 70) {
this->unk_2C8 = 300;
this->unk_2D0 = 0.0f;
-
+
D_809EE4D0 = 1;
this->unk_2E2 = 60;
this->unk_2E0 = 93;
diff --git a/patches/custom_syms.toml b/patches/custom_syms.toml
new file mode 100644
index 0000000..9128278
--- /dev/null
+++ b/patches/custom_syms.toml
@@ -0,0 +1,16 @@
+# Custom symbols used for patches.
+[[section]]
+name = "ABSOLUTE_SYMS"
+vram = 0x00000000
+size = 0x0
+
+symbols = [
+ # Alternate references to symbols that would otherwise be relocated
+ { name = "Setup_Init_NORELOCATE", vram = 0x80173338 }, # Not relocatable, but here for uniformity with the other gamestate funcs
+ { name = "MapSelect_Init_NORELOCATE", vram = 0x80801B4C },
+ { name = "ConsoleLogo_Init_NORELOCATE", vram = 0x8080074C },
+ { name = "Play_Init_NORELOCATE", vram = 0x8016A2C8 },
+ { name = "TitleSetup_Init_NORELOCATE", vram = 0x80803F30 },
+ { name = "FileSelect_Init_NORELOCATE", vram = 0x80813C98 },
+ { name = "DayTelop_Init_NORELOCATE", vram = 0x80815820 },
+]
diff --git a/patches/effect_patches.c b/patches/effect_patches.c
index 1b34a12..f267ff8 100644
--- a/patches/effect_patches.c
+++ b/patches/effect_patches.c
@@ -17,7 +17,7 @@ RECOMP_PATCH void TransitionWipe3_Draw(void* thisx, Gfx** gfxP) {
// @recomp Modify the scale based on the aspect ratio to make sure the transition circle covers the whole screen
float original_aspect_ratio = ((float)SCREEN_WIDTH) / ((float)SCREEN_HEIGHT);
- scale *= recomp_get_aspect_ratio(original_aspect_ratio) / original_aspect_ratio;
+ scale *= recomp_get_target_aspect_ratio(original_aspect_ratio) / original_aspect_ratio;
THIS->frame ^= 1;
gDPPipeSync(gfx++);
@@ -129,7 +129,7 @@ RECOMP_PATCH void Play_DrawMotionBlur(PlayState* this) {
RECOMP_PATCH void Actor_DrawLensOverlay(Gfx** gfxP, s32 lensMaskSize) {
// @recomp Calculate the increase in aspect ratio.
f32 original_aspect_ratio = (float)SCREEN_WIDTH / SCREEN_HEIGHT;
- f32 aspect_ratio_scale = recomp_get_aspect_ratio(original_aspect_ratio) / original_aspect_ratio;
+ f32 aspect_ratio_scale = recomp_get_target_aspect_ratio(original_aspect_ratio) / original_aspect_ratio;
// @recomp Increase the circle's scale based on the aspect ratio scale. Also increase the base scaling
// from 0.003f to 0.004f to account for overscan removal.
diff --git a/patches/graphics.h b/patches/graphics.h
index 5d517d7..31837f8 100644
--- a/patches/graphics.h
+++ b/patches/graphics.h
@@ -3,7 +3,8 @@
#include "patch_helpers.h"
-DECLARE_FUNC(float, recomp_get_aspect_ratio, float);
+DECLARE_FUNC(void, recomp_get_window_resolution, u32*, u32*);
+DECLARE_FUNC(float, recomp_get_target_aspect_ratio, float);
DECLARE_FUNC(s32, recomp_get_target_framerate, s32);
DECLARE_FUNC(s32, recomp_high_precision_fb_enabled);
DECLARE_FUNC(float, recomp_get_resolution_scale);
diff --git a/patches/input.c b/patches/input.c
index 53f7eb3..3acf7a2 100644
--- a/patches/input.c
+++ b/patches/input.c
@@ -6,21 +6,35 @@
#include "z64voice.h"
#include "audiothread_cmd.h"
+RECOMP_DECLARE_EVENT(recomp_before_first_person_aiming_update_event(PlayState* play, Player* this, bool in_free_look, RecompAimingOverideMode* recomp_aiming_override_mode));
+RECOMP_DECLARE_EVENT(recomp_after_first_person_aiming_update_event(PlayState* play, Player* this, bool in_free_look));
+
s32 func_80847190(PlayState* play, Player* this, s32 arg2);
s16 func_80832754(Player* this, s32 arg1);
s32 func_8082EF20(Player* this);
+// This flag is reset every frame by 'poll_inputs()'.
+RecompAimingOverideMode recomp_aiming_override_mode = RECOMP_AIMING_OVERRIDE_OFF;
+
// @recomp Patched to add gyro and mouse aiming.
RECOMP_PATCH s32 func_80847190(PlayState* play, Player* this, s32 arg2) {
s32 pad;
s16 var_s0;
+
+ // Checks if we're in free look (C-Up look around mode).
+ bool in_free_look = (!func_800B7128(this) && !func_8082EF20(this) && !arg2);
+
+ // Checking if any mods have disabled aiming with the left stick.
+ recomp_before_first_person_aiming_update_event(play, this, in_free_look, &recomp_aiming_override_mode);
+
// @recomp Get the aiming camera inversion state.
s32 inverted_x, inverted_y;
recomp_get_inverted_axes(&inverted_x, &inverted_y);
- // @recomp Get the analog camera input values if analog cam is enabled.
+
+ // @recomp Get the analog camera input values if analog cam is enabled, or right-stick aiming is being forced.
s32 analog_x = 0;
s32 analog_y = 0;
- if (recomp_analog_cam_enabled()) {
+ if (recomp_get_analog_cam_enabled() || recomp_aiming_override_mode == RECOMP_AIMING_OVERRIDE_FORCE_RIGHT_STICK) {
float analog_x_float = 0.0f;
float analog_y_float = 0.0f;
recomp_get_camera_inputs(&analog_x_float, &analog_y_float);
@@ -35,9 +49,13 @@ RECOMP_PATCH s32 func_80847190(PlayState* play, Player* this, s32 arg2) {
// play->state.input[0].rel.stick_x, play->state.input[0].rel.stick_y,
// analog_x, analog_y);
- if (!func_800B7128(this) && !func_8082EF20(this) && !arg2) {
+ if (in_free_look) {
// @recomp Add in the analog camera Y input. Clamp to prevent moving the camera twice as fast if both sticks are held.
- var_s0 = CLAMP(play->state.input[0].rel.stick_y + analog_y, -61, 61) * 0xF0;
+ s32 cam_input_y = analog_y;
+ if (recomp_aiming_override_mode == RECOMP_AIMING_OVERRIDE_OFF) {
+ cam_input_y += play->state.input[0].rel.stick_y;
+ }
+ var_s0 = CLAMP(cam_input_y, -61, 61) * 0xF0;
// @recomp Invert the Y axis accordingly (default is inverted, so negate if not inverted).
if (!inverted_y) {
@@ -46,7 +64,11 @@ RECOMP_PATCH s32 func_80847190(PlayState* play, Player* this, s32 arg2) {
Math_SmoothStepToS(&this->actor.focus.rot.x, var_s0, 0xE, 0xFA0, 0x1E);
// @recomp Add in the analog camera X input. Clamp to prevent moving the camera twice as fast if both sticks are held.
- var_s0 = CLAMP(play->state.input[0].rel.stick_x + analog_x, -61, 61) * -0x10;
+ s32 cam_input_x = analog_x;
+ if (recomp_aiming_override_mode == RECOMP_AIMING_OVERRIDE_OFF) {
+ cam_input_x += play->state.input[0].rel.stick_x;
+ }
+ var_s0 = CLAMP(cam_input_x, -61, 61) * -0x10;
// @recomp Invert the X axis accordingly
if (inverted_x) {
@@ -97,7 +119,13 @@ RECOMP_PATCH s32 func_80847190(PlayState* play, Player* this, s32 arg2) {
// @recomp Invert the Y axis accordingly (default is inverted, so negate if not inverted).
// Also add in the analog camera Y input. Clamp to prevent moving the camera twice as fast if both sticks are held.
- s32 stick_y = CLAMP(play->state.input[0].rel.stick_y + analog_y, -61, 61);
+ s32 cam_input_y = analog_y;
+ if (recomp_aiming_override_mode == RECOMP_AIMING_OVERRIDE_OFF) {
+ cam_input_y += play->state.input[0].rel.stick_y;
+ }
+ s32 stick_y;
+ stick_y = CLAMP(cam_input_y, -61, 61);
+
if (!inverted_y) {
stick_y = -stick_y;
}
@@ -118,7 +146,13 @@ RECOMP_PATCH s32 func_80847190(PlayState* play, Player* this, s32 arg2) {
// @recomp Invert the X axis accordingly. Also add in the analog camera Y input.
// Clamp to prevent moving the camera twice as fast if both sticks are held.
- s32 stick_x = CLAMP(play->state.input[0].rel.stick_x + analog_x, -61, 61);
+ s32 cam_input_x = analog_x;
+ if (recomp_aiming_override_mode == RECOMP_AIMING_OVERRIDE_OFF) {
+ cam_input_x += play->state.input[0].rel.stick_x;
+ }
+ s32 stick_x;
+ stick_x = CLAMP(cam_input_x, -61, 61);
+
if (inverted_x) {
stick_x = -stick_x;
}
@@ -130,6 +164,8 @@ RECOMP_PATCH s32 func_80847190(PlayState* play, Player* this, s32 arg2) {
this->actor.focus.rot.y = CLAMP(var_s0, -0x4AAA, 0x4AAA) + this->actor.shape.rot.y;
}
+ recomp_after_first_person_aiming_update_event(play, this, in_free_look);
+
this->unk_AA6 |= 2;
return func_80832754(this, (play->unk_1887C != 0) || func_800B7128(this) || func_8082EF20(this));
@@ -162,7 +198,7 @@ RECOMP_PATCH void func_8083A98C(Actor* thisx, PlayState* play2) {
// @recomp Get the analog camera input values if analog cam is enabled.
s32 analog_x = 0;
s32 analog_y = 0;
- if (recomp_analog_cam_enabled()) {
+ if (recomp_get_analog_cam_enabled()) {
float analog_x_float = 0.0f;
float analog_y_float = 0.0f;
recomp_get_camera_inputs(&analog_x_float, &analog_y_float);
@@ -534,11 +570,31 @@ RECOMP_PATCH void Player_Action_86(Player *this, PlayState *play) {
func_808550D0(play, this, this->unk_B10[4], this->unk_B10[5], (this->transformation == PLAYER_FORM_HUMAN) ? 0 : 1);
}
+bool no_bow_epona_fix = false;
+
+// @recomp_export void recomp_set_no_bow_epona_fix(bool new_val): Set whether to enable the fix for getting on Epona without a bow.
+RECOMP_EXPORT void recomp_set_no_bow_epona_fix(bool new_val) {
+ no_bow_epona_fix = new_val;
+}
+
+bool h_and_d_no_sword_fix = false;
+
+// @recomp_export void recomp_set_h_and_d_no_sword_fix(bool new_val): Set whether to enable the fix for playing Honey and Darling without a sword.
+RECOMP_EXPORT void recomp_set_h_and_d_no_sword_fix(bool new_val) {
+ h_and_d_no_sword_fix = new_val;
+}
+
extern s16 sPictoState;
extern s16 sPictoPhotoBeingTaken;
extern void* gWorkBuffer;
u16 func_801A5100(void);
+#define ON_EPONA (player->stateFlags1 & PLAYER_STATE1_800000)
+#define EPONA_FIX_ACTIVE (no_bow_epona_fix && ON_EPONA)
+
+#define AT_H_AND_D (play->sceneId == SCENE_BOWLING)
+#define H_AND_D_FIX_ACTIVE (h_and_d_no_sword_fix && AT_H_AND_D)
+
// @recomp Patched to update status of extra buttons via set_extra_item_slot_status.
RECOMP_PATCH void Interface_UpdateButtonsPart1(PlayState* play) {
InterfaceContext* interfaceCtx = &play->interfaceCtx;
@@ -548,11 +604,11 @@ RECOMP_PATCH void Interface_UpdateButtonsPart1(PlayState* play) {
if (gSaveContext.save.cutsceneIndex < 0xFFF0) {
gSaveContext.hudVisibilityForceButtonAlphasByStatus = false;
- if ((player->stateFlags1 & PLAYER_STATE1_800000) || CHECK_WEEKEVENTREG(WEEKEVENTREG_08_01) ||
+ if (ON_EPONA || CHECK_WEEKEVENTREG(WEEKEVENTREG_08_01) ||
(!CHECK_EVENTINF(EVENTINF_41) && (play->unk_1887C >= 2))) {
// Riding Epona OR Honey & Darling minigame OR Horseback balloon minigame OR related to swamp boat
// (non-minigame?)
- if ((player->stateFlags1 & PLAYER_STATE1_800000) && (player->currentMask == PLAYER_MASK_BLAST) &&
+ if (ON_EPONA && (player->currentMask == PLAYER_MASK_BLAST) &&
(gSaveContext.bButtonStatus == BTN_DISABLED)) {
// Riding Epona with blast mask?
restoreHudVisibility = true;
@@ -603,24 +659,46 @@ RECOMP_PATCH void Interface_UpdateButtonsPart1(PlayState* play) {
BUTTON_STATUS(EQUIP_SLOT_C_RIGHT) = BTN_DISABLED;
set_extra_item_slot_status(BTN_DISABLED);
} else {
- BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_BOW;
+ // @recomp_use_export_var no_bow_epona_fix: Part of the no bow Epona fix.
+ if (EPONA_FIX_ACTIVE) {
+ if (gSaveContext.save.saveInfo.inventory.items[SLOT_BOW] == ITEM_BOW) {
+ BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_BOW;
+ BUTTON_STATUS(EQUIP_SLOT_B) = BTN_ENABLED;
+ }
+ } else {
+ BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_BOW;
+ }
if (play->unk_1887C >= 2) {
Interface_LoadItemIconImpl(play, EQUIP_SLOT_B);
} else if (gSaveContext.save.saveInfo.inventory.items[SLOT_BOW] == ITEM_NONE) {
- BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_NONE;
+ // @recomp_use_export_var no_bow_epona_fix: Part of the no bow Epona fix.
+ if (EPONA_FIX_ACTIVE) {
+ gSaveContext.buttonStatus[EQUIP_SLOT_B] = BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B);
+ BUTTON_STATUS(EQUIP_SLOT_B) = BTN_DISABLED;
+ } else {
+ BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_NONE;
+ }
} else {
Interface_LoadItemIconImpl(play, EQUIP_SLOT_B);
}
- BUTTON_STATUS(EQUIP_SLOT_C_LEFT) = BTN_DISABLED;
- BUTTON_STATUS(EQUIP_SLOT_C_DOWN) = BTN_DISABLED;
- BUTTON_STATUS(EQUIP_SLOT_C_RIGHT) = BTN_DISABLED;
- set_extra_item_slot_status(BTN_DISABLED);
- Interface_SetHudVisibility(HUD_VISIBILITY_A_HEARTS_MAGIC_MINIMAP_WITH_OVERWRITE);
+ // @recomp_use_export_var no_bow_epona_fix: If the B button does not contain a sword, don't disable the UI.
+ if (!EPONA_FIX_ACTIVE || BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) < ITEM_SWORD_KOKIRI ||
+ BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) > ITEM_SWORD_GILDED) {
+ BUTTON_STATUS(EQUIP_SLOT_C_LEFT) = BTN_DISABLED;
+ BUTTON_STATUS(EQUIP_SLOT_C_DOWN) = BTN_DISABLED;
+ BUTTON_STATUS(EQUIP_SLOT_C_RIGHT) = BTN_DISABLED;
+ set_extra_item_slot_status(BTN_DISABLED);
+ Interface_SetHudVisibility(HUD_VISIBILITY_A_HEARTS_MAGIC_MINIMAP_WITH_OVERWRITE);
+ }
}
}
+ if (BUTTON_STATUS(EQUIP_SLOT_B) == BTN_DISABLED && BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) == ITEM_BOW) {
+ BUTTON_STATUS(EQUIP_SLOT_B) = BTN_ENABLED;
+ }
+
if (play->transitionMode != TRANS_MODE_OFF) {
Interface_SetHudVisibility(HUD_VISIBILITY_NONE);
} else if ((gSaveContext.minigameStatus == MINIGAME_STATUS_ACTIVE) &&
@@ -640,12 +718,12 @@ RECOMP_PATCH void Interface_UpdateButtonsPart1(PlayState* play) {
BUTTON_STATUS(EQUIP_SLOT_C_RIGHT) = BTN_DISABLED;
set_extra_item_slot_status(BTN_DISABLED);
Interface_SetHudVisibility(HUD_VISIBILITY_A_B_MINIMAP);
- } else if (player->stateFlags1 & PLAYER_STATE1_800000) {
+ } else if (ON_EPONA) {
Interface_SetHudVisibility(HUD_VISIBILITY_A_B_MINIMAP);
}
}
} else {
- if (player->stateFlags1 & PLAYER_STATE1_800000) {
+ if (ON_EPONA) {
Interface_SetHudVisibility(HUD_VISIBILITY_A_B_MINIMAP);
}
@@ -657,32 +735,59 @@ RECOMP_PATCH void Interface_UpdateButtonsPart1(PlayState* play) {
} else {
BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_BOW;
}
+ if (h_and_d_no_sword_fix) {
+ BUTTON_STATUS(EQUIP_SLOT_B) = BTN_ENABLED;
+ }
BUTTON_STATUS(EQUIP_SLOT_C_LEFT) = BTN_DISABLED;
BUTTON_STATUS(EQUIP_SLOT_C_DOWN) = BTN_DISABLED;
BUTTON_STATUS(EQUIP_SLOT_C_RIGHT) = BTN_DISABLED;
set_extra_item_slot_status(BTN_DISABLED);
} else {
- BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_BOW;
+ // @recomp_use_export_var no_bow_epona_fix: Part of the no bow Epona fix.
+ if (EPONA_FIX_ACTIVE) {
+ if (gSaveContext.save.saveInfo.inventory.items[SLOT_BOW] == ITEM_BOW) {
+ BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_BOW;
+ BUTTON_STATUS(EQUIP_SLOT_B) = BTN_ENABLED;
+ }
+ } else {
+ BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_BOW;
+ }
}
if (play->unk_1887C >= 2) {
Interface_LoadItemIconImpl(play, EQUIP_SLOT_B);
- } else if (gSaveContext.save.saveInfo.inventory.items[SLOT_BOW] == ITEM_NONE) {
- BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_NONE;
+ } else if (gSaveContext.save.saveInfo.inventory.items[SLOT_BOW] == ITEM_NONE && !H_AND_D_FIX_ACTIVE) {
+ // @recomp_use_export_var no_bow_epona_fix: Part of the no bow Epona fix.
+ if (EPONA_FIX_ACTIVE) {
+ gSaveContext.buttonStatus[EQUIP_SLOT_B] = BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B);
+ BUTTON_STATUS(EQUIP_SLOT_B) = BTN_DISABLED;
+ } else {
+ BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) = ITEM_NONE;
+ }
} else {
Interface_LoadItemIconImpl(play, EQUIP_SLOT_B);
}
if (BUTTON_STATUS(EQUIP_SLOT_B) == BTN_DISABLED) {
- BUTTON_STATUS(EQUIP_SLOT_B) = BTN_ENABLED;
- restoreHudVisibility = true;
+ // @recomp_use_export_var no_bow_epona_fix: Don't enable the B button unless it is being used for the bow.
+ if (!no_bow_epona_fix || BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) == ITEM_BOW) {
+ BUTTON_STATUS(EQUIP_SLOT_B) = BTN_ENABLED;
+ }
+
+ // @recomp_use_export_var no_bow_epona_fix: Don't restore hud visibility from Epona without a sword.
+ if (!no_bow_epona_fix || (player->stateFlags1 & PLAYER_STATE1_800000) == 0) {
+ restoreHudVisibility = true;
+ }
}
- BUTTON_STATUS(EQUIP_SLOT_C_LEFT) = BTN_DISABLED;
- BUTTON_STATUS(EQUIP_SLOT_C_DOWN) = BTN_DISABLED;
- BUTTON_STATUS(EQUIP_SLOT_C_RIGHT) = BTN_DISABLED;
- set_extra_item_slot_status(BTN_DISABLED);
- Interface_SetHudVisibility(HUD_VISIBILITY_A_HEARTS_MAGIC_MINIMAP_WITH_OVERWRITE);
+ // @recomp_use_export_var no_bow_epona_fix: If the B button does not contain the bow, don't disable the UI.
+ if ((!no_bow_epona_fix || BUTTON_ITEM_EQUIP(CUR_FORM, EQUIP_SLOT_B) == ITEM_BOW) && !H_AND_D_FIX_ACTIVE) {
+ BUTTON_STATUS(EQUIP_SLOT_C_LEFT) = BTN_DISABLED;
+ BUTTON_STATUS(EQUIP_SLOT_C_DOWN) = BTN_DISABLED;
+ BUTTON_STATUS(EQUIP_SLOT_C_RIGHT) = BTN_DISABLED;
+ set_extra_item_slot_status(BTN_DISABLED);
+ Interface_SetHudVisibility(HUD_VISIBILITY_A_HEARTS_MAGIC_MINIMAP_WITH_OVERWRITE);
+ }
if (play->transitionMode != TRANS_MODE_OFF) {
Interface_SetHudVisibility(HUD_VISIBILITY_NONE);
@@ -700,8 +805,11 @@ RECOMP_PATCH void Interface_UpdateButtonsPart1(PlayState* play) {
BUTTON_STATUS(EQUIP_SLOT_C_RIGHT) = BTN_DISABLED;
set_extra_item_slot_status(BTN_DISABLED);
Interface_SetHudVisibility(HUD_VISIBILITY_A_B_MINIMAP);
- } else if (player->stateFlags1 & PLAYER_STATE1_800000) {
+ } else if (ON_EPONA) {
Interface_SetHudVisibility(HUD_VISIBILITY_A_B_MINIMAP);
+ } else if (H_AND_D_FIX_ACTIVE) {
+ Interface_LoadItemIconImpl(play, EQUIP_SLOT_B);
+ Interface_SetHudVisibility(HUD_VISIBILITY_B);
}
}
} else if (sPictoState != PICTO_BOX_STATE_OFF) {
@@ -802,6 +910,13 @@ RECOMP_PATCH void Interface_UpdateButtonsPart1(PlayState* play) {
}
}
+bool fd_anywhere = false;
+
+// @recomp_export void recomp_set_fd_anywhere(bool new_val): Set whether the Fierce Deity's Mask has scene restrictions.
+RECOMP_EXPORT void recomp_set_fd_anywhere(bool new_val) {
+ fd_anywhere = new_val;
+}
+
/**
* A continuation of the if-else chain from Interface_UpdateButtonsPart1
* Also used directly when opening the pause menu i.e. skips part 1
@@ -1181,7 +1296,8 @@ RECOMP_PATCH void Interface_UpdateButtonsPart2(PlayState* play) {
}
} else if (GET_CUR_FORM_BTN_ITEM(i) == ITEM_MASK_FIERCE_DEITY) {
// Fierce Deity's Mask is equipped
- if ((play->sceneId != SCENE_MITURIN_BS) && (play->sceneId != SCENE_HAKUGIN_BS) &&
+ // @recomp_use_export_var fd_anywhere: Allow the player to use the Fierce Deity's Mask anywhere if mods enable it.
+ if (!fd_anywhere && (play->sceneId != SCENE_MITURIN_BS) && (play->sceneId != SCENE_HAKUGIN_BS) &&
(play->sceneId != SCENE_SEA_BS) && (play->sceneId != SCENE_INISIE_BS) &&
(play->sceneId != SCENE_LAST_BS)) {
if (BUTTON_STATUS(i) != BTN_DISABLED) {
diff --git a/patches/input.h b/patches/input.h
index 0f00e36..e505bfc 100644
--- a/patches/input.h
+++ b/patches/input.h
@@ -8,13 +8,20 @@ typedef enum {
RECOMP_CAMERA_DUALANALOG,
} RecompCameraMode;
+typedef enum {
+ RECOMP_AIMING_OVERRIDE_OFF = 0,
+ RECOMP_AIMING_OVERRIDE_DISABLE_LEFT_STICK = 1,
+ RECOMP_AIMING_OVERRIDE_FORCE_RIGHT_STICK = 2
+} RecompAimingOverideMode;
+
extern RecompCameraMode recomp_camera_mode;
+extern RecompAimingOverideMode recomp_aiming_override_mode;
DECLARE_FUNC(void, recomp_get_gyro_deltas, float* x, float* y);
DECLARE_FUNC(void, recomp_get_mouse_deltas, float* x, float* y);
DECLARE_FUNC(s32, recomp_get_targeting_mode);
DECLARE_FUNC(void, recomp_get_inverted_axes, s32* x, s32* y);
-DECLARE_FUNC(s32, recomp_analog_cam_enabled);
+DECLARE_FUNC(s32, recomp_get_analog_cam_enabled);
DECLARE_FUNC(void, recomp_get_analog_inverted_axes, s32* x, s32* y);
DECLARE_FUNC(void, recomp_get_camera_inputs, float* x, float* y);
DECLARE_FUNC(void, recomp_set_right_analog_suppressed, s32 suppressed);
diff --git a/patches/input_latency.c b/patches/input_latency.c
index 8007c6a..10c32a5 100644
--- a/patches/input_latency.c
+++ b/patches/input_latency.c
@@ -70,8 +70,11 @@ void poll_inputs(void) {
// Begin reading controller data
osContStartReadData(serialEventQueue);
+ bool needs_right_stick = recomp_get_analog_cam_enabled() || recomp_aiming_override_mode == RECOMP_AIMING_OVERRIDE_FORCE_RIGHT_STICK;
// Suppress the right analog stick if analog camera is active unless the ocarina is in use.
- recomp_set_right_analog_suppressed(recomp_analog_cam_enabled() && sOcarinaInstrumentId == OCARINA_INSTRUMENT_OFF);
+ recomp_set_right_analog_suppressed(needs_right_stick && sOcarinaInstrumentId == OCARINA_INSTRUMENT_OFF);
+ // Resets this flag for the next frame;
+ recomp_aiming_override_mode = RECOMP_AIMING_OVERRIDE_OFF;
// Wait for controller data
osRecvMesg(serialEventQueue, NULL, OS_MESG_BLOCK);
diff --git a/patches/misc_funcs.h b/patches/misc_funcs.h
index 853877b..9eae368 100644
--- a/patches/misc_funcs.h
+++ b/patches/misc_funcs.h
@@ -10,6 +10,6 @@ DECLARE_FUNC(void, recomp_handle_quicksave_actions, OSMesgQueue* enter_mq, OSMes
DECLARE_FUNC(void, recomp_handle_quicksave_actions_main, OSMesgQueue* enter_mq, OSMesgQueue* exit_mq);
DECLARE_FUNC(u16, recomp_get_pending_warp);
DECLARE_FUNC(u32, recomp_get_pending_set_time);
-DECLARE_FUNC(s32, recomp_autosave_enabled);
+DECLARE_FUNC(s32, recomp_get_autosave_enabled);
#endif
diff --git a/patches/options.c b/patches/options.c
index c26133c..07c0a23 100644
--- a/patches/options.c
+++ b/patches/options.c
@@ -1036,6 +1036,9 @@ RECOMP_PATCH void FileSelect_ConfirmFile(GameState *thisx) {
}
}
+// Non-relocatable reference to the original address of Play_Init.
+void Play_Init_NORELOCATE(GameState*);
+
/**
* Load the save for the appropriate file and start the game.
* Update function for `SM_LOAD_GAME`
@@ -1060,7 +1063,7 @@ RECOMP_PATCH void FileSelect_LoadGame(GameState* thisx) {
gSaveContext.gameMode = GAMEMODE_NORMAL;
STOP_GAMESTATE(&this->state);
- SET_NEXT_GAMESTATE(&this->state, Play_Init, sizeof(PlayState));
+ SET_NEXT_GAMESTATE(&this->state, Play_Init_NORELOCATE, sizeof(PlayState));
gSaveContext.respawnFlag = 0;
gSaveContext.respawn[RESPAWN_MODE_DOWN].entrance = ENTR_LOAD_OPENING;
diff --git a/patches/patch_helpers.h b/patches/patch_helpers.h
index 371e8b4..f6b88d3 100644
--- a/patches/patch_helpers.h
+++ b/patches/patch_helpers.h
@@ -4,7 +4,7 @@
#ifdef MIPS
#include "ultra64.h"
#else
-#include "librecomp/recomp.h"
+#include "recomp.h"
#endif
#ifdef __cplusplus
diff --git a/patches/patches.h b/patches/patches.h
index f0f2b5e..a93a6fd 100644
--- a/patches/patches.h
+++ b/patches/patches.h
@@ -4,6 +4,11 @@
#define RECOMP_EXPORT __attribute__((section(".recomp_export")))
#define RECOMP_PATCH __attribute__((section(".recomp_patch")))
#define RECOMP_FORCE_PATCH __attribute__((section(".recomp_force_patch")))
+#define RECOMP_DECLARE_EVENT(func) \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wunused-parameter\"") \
+ __attribute__((noinline, weak, used, section(".recomp_event"))) void func {} \
+ _Pragma("GCC diagnostic pop")
// TODO fix renaming symbols in patch recompilation
#define osCreateMesgQueue osCreateMesgQueue_recomp
diff --git a/patches/patches.ld b/patches/patches.ld
index b0a3b7f..fad9148 100644
--- a/patches/patches.ld
+++ b/patches/patches.ld
@@ -1,5 +1,5 @@
RAMBASE = 0x80801000; /* Used to hold any new symbols */
-EXTRA_RAM_SIZE = 0x01000000; /* Amount of extra ram allocated by recomp */
+PATCH_RAM_END = 0x81000000; /* Amount of extra ram allocated by recomp */
MEMORY {
extram : ORIGIN = RAMBASE, LENGTH = 64M
@@ -12,10 +12,11 @@ SECTIONS {
.text : { *(.text*) } >extram AT >rom
.recomp_patch : { *(.recomp_patch*) *(.recomp_force_patch*) } >extram AT >rom
.recomp_export : { *(.recomp_export*) } >extram AT >rom
+ .recomp_event : { *(.recomp_event*) } >extram AT >rom
.rodata : { *(.rodata*) } >extram AT >rom
.data : { *(.data*) } >extram AT >rom
.bss (NOLOAD) : { *(.bss*) *(COMMON) } >extram
- ASSERT(. < RAMBASE + EXTRA_RAM_SIZE, "Maxed out recomp extra ram")
+ ASSERT(. <= PATCH_RAM_END, "Maxed out recomp extra ram")
.reloc 0 : { *(.reloc*) }
.symtab 0 : { *(.symtab) }
diff --git a/patches/play_patches.c b/patches/play_patches.c
index e3d7c84..04f7b88 100644
--- a/patches/play_patches.c
+++ b/patches/play_patches.c
@@ -1,9 +1,37 @@
#include "play_patches.h"
#include "z64debug_display.h"
#include "input.h"
+#include "prevent_bss_reordering.h"
+#include "z64.h"
+#include "regs.h"
+#include "functions.h"
+#include "z64vismono.h"
+#include "z64visfbuf.h"
+#include "buffers.h"
+
+#include "variables.h"
+#include "macros.h"
+#include "buffers.h"
+#include "idle.h"
+#include "sys_cfb.h"
+#include "z64bombers_notebook.h"
+#include "z64quake.h"
+#include "z64rumble.h"
+#include "z64shrink_window.h"
+#include "z64view.h"
+
+#include "overlays/gamestates/ovl_daytelop/z_daytelop.h"
+#include "overlays/gamestates/ovl_opening/z_opening.h"
+#include "overlays/gamestates/ovl_file_choose/z_file_select.h"
+#include "overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope.h"
+#include "debug.h"
extern Input D_801F6C18;
+RECOMP_DECLARE_EVENT(recomp_on_play_main(PlayState* play));
+RECOMP_DECLARE_EVENT(recomp_on_play_update(PlayState* play));
+RECOMP_DECLARE_EVENT(recomp_after_play_update(PlayState* play));
+
void controls_play_update(PlayState* play) {
gSaveContext.options.zTargetSetting = recomp_get_targeting_mode();
}
@@ -13,6 +41,9 @@ RECOMP_PATCH void Play_Main(GameState* thisx) {
static Input* prevInput = NULL;
PlayState* this = (PlayState*)thisx;
+ // @recomp_event recomp_on_play_main(PlayState* play): Allow mods to execute code every frame.
+ recomp_on_play_main(this);
+
// @recomp
debug_play_update(this);
controls_play_update(this);
@@ -32,7 +63,15 @@ RECOMP_PATCH void Play_Main(GameState* thisx) {
this->state.gfxCtx = NULL;
}
camera_pre_play_update(this);
+
+ // @recomp_event recomp_on_play_update(PlayState* play): Play_Update is about to be called.
+ recomp_on_play_update(this);
+
Play_Update(this);
+
+ // @recomp_event recomp_after_play_update(PlayState* play): Play_Update was called.
+ recomp_after_play_update(this);
+
camera_post_play_update(this);
analog_cam_post_play_update(this);
autosave_post_play_update(this);
@@ -83,3 +122,322 @@ RECOMP_PATCH s32 Room_HandleLoadCallbacks(PlayState* play, RoomContext* roomCtx)
return 1;
}
+
+void ZeldaArena_Init(void* start, size_t size);
+
+void Play_SpawnScene(PlayState* this, s32 sceneId, s32 spawn);
+void Play_InitMotionBlur(void);
+
+extern s16 sTransitionFillTimer;
+extern Input D_801F6C18;
+extern TransitionTile sTransitionTile;
+extern s32 gTransitionTileState;
+extern VisMono sPlayVisMono;
+extern Color_RGBA8_u32 gVisMonoColor;
+extern VisFbuf sPlayVisFbuf;
+extern VisFbuf* sPlayVisFbufInstance;
+extern BombersNotebook sBombersNotebook;
+extern u8 sBombersNotebookOpen;
+extern u8 sMotionBlurStatus;
+
+extern s32 gDbgCamEnabled;
+extern u8 D_801D0D54;
+
+extern u8 gPictoPhotoI8[];
+extern u8 D_80784600[];
+
+// Non-relocatable references to the original addresses of these game state functions.
+void DayTelop_Init_NORELOCATE(GameState*);
+void TitleSetup_Init_NORELOCATE(GameState*);
+
+RECOMP_DECLARE_EVENT(recomp_on_play_init(PlayState* this));
+RECOMP_DECLARE_EVENT(recomp_after_play_init(PlayState* this));
+
+bool allow_no_ocarina_tf = false;
+
+// @recomp_export void recomp_set_allow_no_ocarina_tf(bool new_val): Set whether to force Termina Field to load normally even if Link has no ocarina.
+RECOMP_EXPORT void recomp_set_allow_no_ocarina_tf(bool new_val) {
+ allow_no_ocarina_tf = new_val;
+}
+
+RECOMP_PATCH void Play_Init(GameState* thisx) {
+ PlayState* this = (PlayState*)thisx;
+ GraphicsContext* gfxCtx = this->state.gfxCtx;
+ s32 pad;
+ uintptr_t zAlloc;
+ s32 zAllocSize;
+ Player* player;
+ s32 i;
+ s32 spawn;
+ u8 sceneLayer;
+ s32 scene;
+
+ // @recomp_event recomp_on_play_init(PlayState* this): A new PlayState is being initialized.
+ recomp_on_play_init(this);
+
+ if ((gSaveContext.respawnFlag == -4) || (gSaveContext.respawnFlag == -0x63)) {
+ if (CHECK_EVENTINF(EVENTINF_TRIGGER_DAYTELOP)) {
+ CLEAR_EVENTINF(EVENTINF_TRIGGER_DAYTELOP);
+ STOP_GAMESTATE(&this->state);
+ // Use non-relocatable reference to DayTelop_Init instead.
+ SET_NEXT_GAMESTATE(&this->state, DayTelop_Init_NORELOCATE, sizeof(DayTelopState));
+ return;
+ }
+
+ gSaveContext.unk_3CA7 = 1;
+ if (gSaveContext.respawnFlag == -0x63) {
+ gSaveContext.respawnFlag = 2;
+ }
+ } else {
+ gSaveContext.unk_3CA7 = 0;
+ }
+
+ if (gSaveContext.save.entrance == -1) {
+ gSaveContext.save.entrance = 0;
+ STOP_GAMESTATE(&this->state);
+ // Use non-relocatable reference to TitleSetup_Init instead.
+ SET_NEXT_GAMESTATE(&this->state, TitleSetup_Init_NORELOCATE, sizeof(TitleSetupState));
+ return;
+ }
+
+ if ((gSaveContext.nextCutsceneIndex == 0xFFEF) || (gSaveContext.nextCutsceneIndex == 0xFFF0)) {
+ scene = ((void)0, gSaveContext.save.entrance) >> 9;
+ spawn = (((void)0, gSaveContext.save.entrance) >> 4) & 0x1F;
+
+ if (CHECK_WEEKEVENTREG(WEEKEVENTREG_CLEARED_SNOWHEAD_TEMPLE)) {
+ if (scene == ENTR_SCENE_MOUNTAIN_VILLAGE_WINTER) {
+ scene = ENTR_SCENE_MOUNTAIN_VILLAGE_SPRING;
+ } else if (scene == ENTR_SCENE_GORON_VILLAGE_WINTER) {
+ scene = ENTR_SCENE_GORON_VILLAGE_SPRING;
+ } else if (scene == ENTR_SCENE_PATH_TO_GORON_VILLAGE_WINTER) {
+ scene = ENTR_SCENE_PATH_TO_GORON_VILLAGE_SPRING;
+ } else if ((scene == ENTR_SCENE_SNOWHEAD) || (scene == ENTR_SCENE_PATH_TO_SNOWHEAD) ||
+ (scene == ENTR_SCENE_PATH_TO_MOUNTAIN_VILLAGE) || (scene == ENTR_SCENE_GORON_SHRINE) ||
+ (scene == ENTR_SCENE_GORON_RACETRACK)) {
+ gSaveContext.nextCutsceneIndex = 0xFFF0;
+ }
+ }
+
+ if (CHECK_WEEKEVENTREG(WEEKEVENTREG_CLEARED_WOODFALL_TEMPLE)) {
+ if (scene == ENTR_SCENE_SOUTHERN_SWAMP_POISONED) {
+ scene = ENTR_SCENE_SOUTHERN_SWAMP_CLEARED;
+ } else if (scene == ENTR_SCENE_WOODFALL) {
+ gSaveContext.nextCutsceneIndex = 0xFFF1;
+ }
+ }
+
+ if (CHECK_WEEKEVENTREG(WEEKEVENTREG_CLEARED_STONE_TOWER_TEMPLE) && (scene == ENTR_SCENE_IKANA_CANYON)) {
+ gSaveContext.nextCutsceneIndex = 0xFFF2;
+ }
+
+ if (CHECK_WEEKEVENTREG(WEEKEVENTREG_CLEARED_GREAT_BAY_TEMPLE) &&
+ ((scene == ENTR_SCENE_GREAT_BAY_COAST) || (scene == ENTR_SCENE_ZORA_CAPE))) {
+ gSaveContext.nextCutsceneIndex = 0xFFF0;
+ }
+
+ // "First cycle" Termina Field
+ // @recomp_use_export_var allow_no_ocarina_tf: Skip loading into "First cycle" Termina Field if mods enable it.
+ if (!allow_no_ocarina_tf && INV_CONTENT(ITEM_OCARINA_OF_TIME) != ITEM_OCARINA_OF_TIME) {
+ if ((scene == ENTR_SCENE_TERMINA_FIELD) &&
+ (((void)0, gSaveContext.save.entrance) != ENTRANCE(TERMINA_FIELD, 10))) {
+ gSaveContext.nextCutsceneIndex = 0xFFF4;
+ }
+ }
+ //! FAKE:
+ gSaveContext.save.entrance =
+ Entrance_Create(((void)0, scene), spawn, ((void)0, gSaveContext.save.entrance) & 0xF);
+ }
+
+ GameState_Realloc(&this->state, 0);
+ KaleidoManager_Init(this);
+ ShrinkWindow_Init();
+ View_Init(&this->view, gfxCtx);
+ Audio_SetExtraFilter(0);
+ Quake_Init();
+ Distortion_Init(this);
+
+ for (i = 0; i < ARRAY_COUNT(this->cameraPtrs); i++) {
+ this->cameraPtrs[i] = NULL;
+ }
+
+ Camera_Init(&this->mainCamera, &this->view, &this->colCtx, this);
+ Camera_ChangeStatus(&this->mainCamera, CAM_STATUS_ACTIVE);
+
+ for (i = 0; i < ARRAY_COUNT(this->subCameras); i++) {
+ Camera_Init(&this->subCameras[i], &this->view, &this->colCtx, this);
+ Camera_ChangeStatus(&this->subCameras[i], CAM_STATUS_INACTIVE);
+ }
+
+ this->cameraPtrs[CAM_ID_MAIN] = &this->mainCamera;
+ this->cameraPtrs[CAM_ID_MAIN]->uid = CAM_ID_MAIN;
+ this->activeCamId = CAM_ID_MAIN;
+
+ Camera_OverwriteStateFlags(&this->mainCamera, CAM_STATE_0 | CAM_STATE_CHECK_WATER | CAM_STATE_2 | CAM_STATE_3 |
+ CAM_STATE_4 | CAM_STATE_DISABLE_MODE_CHANGE | CAM_STATE_6);
+ Sram_Alloc(&this->state, &this->sramCtx);
+ Regs_InitData(this);
+ Message_Init(this);
+ GameOver_Init(this);
+ SoundSource_InitAll(this);
+ EffFootmark_Init(this);
+ Effect_Init(this);
+ EffectSS_Init(this, 100);
+ CollisionCheck_InitContext(this, &this->colChkCtx);
+ AnimationContext_Reset(&this->animationCtx);
+ Cutscene_InitContext(this, &this->csCtx);
+
+ if (gSaveContext.nextCutsceneIndex != 0xFFEF) {
+ gSaveContext.save.cutsceneIndex = gSaveContext.nextCutsceneIndex;
+ gSaveContext.nextCutsceneIndex = 0xFFEF;
+ }
+
+ if (gSaveContext.save.cutsceneIndex == 0xFFFD) {
+ gSaveContext.save.cutsceneIndex = 0;
+ }
+
+ if (gSaveContext.nextDayTime != NEXT_TIME_NONE) {
+ gSaveContext.save.time = gSaveContext.nextDayTime;
+ gSaveContext.skyboxTime = gSaveContext.nextDayTime;
+ }
+
+ if ((CURRENT_TIME >= CLOCK_TIME(18, 0)) || (CURRENT_TIME < CLOCK_TIME(6, 30))) {
+ gSaveContext.save.isNight = true;
+ } else {
+ gSaveContext.save.isNight = false;
+ }
+
+ func_800EDDB0(this);
+
+ if (((gSaveContext.gameMode != GAMEMODE_NORMAL) && (gSaveContext.gameMode != GAMEMODE_TITLE_SCREEN)) ||
+ (gSaveContext.save.cutsceneIndex >= 0xFFF0)) {
+ gSaveContext.nayrusLoveTimer = 0;
+ Magic_Reset(this);
+ gSaveContext.sceneLayer = (gSaveContext.save.cutsceneIndex & 0xF) + 1;
+
+ // Set saved cutscene to 0 so it doesn't immediately play, but instead let the `CutsceneManager` handle it.
+ gSaveContext.save.cutsceneIndex = 0;
+ } else {
+ gSaveContext.sceneLayer = 0;
+ }
+
+ sceneLayer = gSaveContext.sceneLayer;
+
+ Play_SpawnScene(
+ this, Entrance_GetSceneIdAbsolute(((void)0, gSaveContext.save.entrance) + ((void)0, gSaveContext.sceneLayer)),
+ Entrance_GetSpawnNum(((void)0, gSaveContext.save.entrance) + ((void)0, gSaveContext.sceneLayer)));
+ KaleidoScopeCall_Init(this);
+ Interface_Init(this);
+
+ if (gSaveContext.nextDayTime != NEXT_TIME_NONE) {
+ if (gSaveContext.nextDayTime == NEXT_TIME_DAY) {
+ gSaveContext.save.day++;
+ gSaveContext.save.eventDayCount++;
+ gSaveContext.dogIsLost = true;
+ gSaveContext.nextDayTime = NEXT_TIME_DAY_SET;
+ } else {
+ gSaveContext.nextDayTime = NEXT_TIME_NIGHT_SET;
+ }
+ }
+
+ Play_InitMotionBlur();
+
+ R_PAUSE_BG_PRERENDER_STATE = PAUSE_BG_PRERENDER_OFF;
+ R_PICTO_PHOTO_STATE = PICTO_PHOTO_STATE_OFF;
+
+ PreRender_Init(&this->pauseBgPreRender);
+ PreRender_SetValuesSave(&this->pauseBgPreRender, gCfbWidth, gCfbHeight, NULL, NULL, NULL);
+ PreRender_SetValues(&this->pauseBgPreRender, gCfbWidth, gCfbHeight, NULL, NULL);
+
+ this->unk_18E64 = gWorkBuffer;
+ this->pictoPhotoI8 = gPictoPhotoI8;
+ this->unk_18E68 = D_80784600;
+ this->unk_18E58 = D_80784600;
+ this->unk_18E60 = D_80784600;
+ gTransitionTileState = TRANS_TILE_OFF;
+ this->transitionMode = TRANS_MODE_OFF;
+ D_801D0D54 = false;
+
+ FrameAdvance_Init(&this->frameAdvCtx);
+ Rand_Seed(osGetTime());
+ Matrix_Init(&this->state);
+
+ this->state.main = Play_Main;
+ this->state.destroy = Play_Destroy;
+
+ this->transitionTrigger = TRANS_TRIGGER_END;
+ this->worldCoverAlpha = 0;
+ this->bgCoverAlpha = 0;
+ this->haltAllActors = false;
+ this->unk_18844 = false;
+
+ if (gSaveContext.gameMode != GAMEMODE_TITLE_SCREEN) {
+ if (gSaveContext.nextTransitionType == TRANS_NEXT_TYPE_DEFAULT) {
+ this->transitionType =
+ (Entrance_GetTransitionFlags(((void)0, gSaveContext.save.entrance) + sceneLayer) >> 7) & 0x7F;
+ } else {
+ this->transitionType = gSaveContext.nextTransitionType;
+ gSaveContext.nextTransitionType = TRANS_NEXT_TYPE_DEFAULT;
+ }
+ } else {
+ this->transitionType = TRANS_TYPE_FADE_BLACK;
+ }
+
+ TransitionFade_Init(&this->unk_18E48);
+ TransitionFade_SetType(&this->unk_18E48, 3);
+ TransitionFade_SetColor(&this->unk_18E48, RGBA8(160, 160, 160, 255));
+ TransitionFade_Start(&this->unk_18E48);
+ VisMono_Init(&sPlayVisMono);
+
+ gVisMonoColor.a = 0;
+ sPlayVisFbufInstance = &sPlayVisFbuf;
+ VisFbuf_Init(sPlayVisFbufInstance);
+ sPlayVisFbufInstance->lodProportion = 0.0f;
+ sPlayVisFbufInstance->mode = VIS_FBUF_MODE_GENERAL;
+ sPlayVisFbufInstance->primColor.r = 0;
+ sPlayVisFbufInstance->primColor.g = 0;
+ sPlayVisFbufInstance->primColor.b = 0;
+ sPlayVisFbufInstance->primColor.a = 0;
+ sPlayVisFbufInstance->envColor.r = 0;
+ sPlayVisFbufInstance->envColor.g = 0;
+ sPlayVisFbufInstance->envColor.b = 0;
+ sPlayVisFbufInstance->envColor.a = 0;
+ CutsceneFlags_UnsetAll(this);
+ THA_GetRemaining(&this->state.tha);
+ zAllocSize = THA_GetRemaining(&this->state.tha);
+ zAlloc = (uintptr_t)THA_AllocTailAlign16(&this->state.tha, zAllocSize);
+
+ //! @bug: Incorrect ALIGN16s
+ ZeldaArena_Init((void*)((zAlloc + 8) & ~0xF), (zAllocSize - ((zAlloc + 8) & ~0xF)) + zAlloc);
+
+ Actor_InitContext(this, &this->actorCtx, this->linkActorEntry);
+
+ while (!Room_HandleLoadCallbacks(this, &this->roomCtx)) {}
+
+ if ((CURRENT_DAY != 0) && ((this->roomCtx.curRoom.behaviorType1 == ROOM_BEHAVIOR_TYPE1_1) ||
+ (this->roomCtx.curRoom.behaviorType1 == ROOM_BEHAVIOR_TYPE1_5))) {
+ Actor_Spawn(&this->actorCtx, this, ACTOR_EN_TEST4, 0.0f, 0.0f, 0.0f, 0, 0, 0, 0);
+ }
+
+ player = GET_PLAYER(this);
+
+ Camera_InitFocalActorSettings(&this->mainCamera, &player->actor);
+ gDbgCamEnabled = false;
+
+ if (PLAYER_GET_BG_CAM_INDEX(&player->actor) != 0xFF) {
+ Camera_ChangeActorCsCamIndex(&this->mainCamera, PLAYER_GET_BG_CAM_INDEX(&player->actor));
+ }
+
+ CutsceneManager_StoreCamera(&this->mainCamera);
+ Interface_SetSceneRestrictions(this);
+ Environment_PlaySceneSequence(this);
+ gSaveContext.seqId = this->sequenceCtx.seqId;
+ gSaveContext.ambienceId = this->sequenceCtx.ambienceId;
+ AnimationContext_Update(this, &this->animationCtx);
+ Cutscene_HandleEntranceTriggers(this);
+ gSaveContext.respawnFlag = 0;
+ sBombersNotebookOpen = false;
+ BombersNotebook_Init(&sBombersNotebook);
+
+ // @recomp_event recomp_after_play_init(PlayState* this): The new PlayState has finished initializing.
+ recomp_after_play_init(this);
+}
diff --git a/patches/print.c b/patches/print.c
index 0fec414..d5216b3 100644
--- a/patches/print.c
+++ b/patches/print.c
@@ -6,7 +6,7 @@ void* proutPrintf(void* dst, const char* fmt, size_t size) {
return (void*)1;
}
-int recomp_printf(const char* fmt, ...) {
+RECOMP_EXPORT int recomp_printf(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
diff --git a/patches/required_patches.c b/patches/required_patches.c
index 721a41a..7aada58 100644
--- a/patches/required_patches.c
+++ b/patches/required_patches.c
@@ -7,6 +7,8 @@ void Main_InitMemory(void);
void Main_InitScreen(void);
+RECOMP_DECLARE_EVENT(recomp_on_init());
+
// @recomp Patched to load the code segment in the recomp runtime.
RECOMP_PATCH void Main_Init(void) {
DmaRequest dmaReq;
@@ -14,6 +16,9 @@ RECOMP_PATCH void Main_Init(void) {
OSMesg msg[1];
size_t prevSize;
+ // @recomp_event recomp_on_init(): Allow mods to initialize themselves once.
+ recomp_on_init();
+
osCreateMesgQueue(&mq, msg, ARRAY_COUNT(msg));
prevSize = gDmaMgrDmaBuffSize;
diff --git a/patches/sky_transform_tagging.c b/patches/sky_transform_tagging.c
index c776468..dff8ca8 100644
--- a/patches/sky_transform_tagging.c
+++ b/patches/sky_transform_tagging.c
@@ -150,7 +150,7 @@ RECOMP_PATCH void Environment_DrawSkyboxStarsImpl(PlayState* play, Gfx** gfxP) {
f32(*viewProjectionMtxF)[4];
// @recomp Get the original and actual aspect ratios.
f32 original_aspect_ratio = view_aspect_ratio(&play->view);
- f32 recomp_aspect_ratio = recomp_get_aspect_ratio(original_aspect_ratio);
+ f32 recomp_aspect_ratio = recomp_get_target_aspect_ratio(original_aspect_ratio);
f32 recomp_aspect_ratio_scale = recomp_aspect_ratio / original_aspect_ratio;
// @recomp Store the original billboard matrix.
diff --git a/patches/syms.ld b/patches/syms.ld
index a0e5d7d..cd67a44 100644
--- a/patches/syms.ld
+++ b/patches/syms.ld
@@ -3,12 +3,12 @@ __start = 0x80000000;
/* Dummy addresses that get recompiled into function calls */
recomp_puts = 0x8F000000;
recomp_exit = 0x8F000004;
-recomp_handle_quicksave_actions = 0x8F000008;
-recomp_handle_quicksave_actions_main = 0x8F00000C;
+/* recomp_handle_quicksave_actions = 0x8F000008;
+recomp_handle_quicksave_actions_main = 0x8F00000C; */
osRecvMesg_recomp = 0x8F000010;
osSendMesg_recomp = 0x8F000014;
recomp_get_gyro_deltas = 0x8F000018;
-recomp_get_aspect_ratio = 0x8F00001C;
+recomp_get_target_aspect_ratio = 0x8F00001C;
recomp_get_pending_warp = 0x8F000020;
recomp_powf = 0x8F000024;
recomp_get_target_framerate = 0x8F000028;
@@ -34,13 +34,14 @@ osContGetQuery_recomp = 0x8F00007C;
recomp_get_mouse_deltas = 0x8F000080;
bcmp_recomp = 0x8F000084;
osGetTime_recomp = 0x8F000088;
-recomp_autosave_enabled = 0x8F00008C;
+recomp_get_autosave_enabled = 0x8F00008C;
recomp_load_overlays = 0x8F000090;
osInvalICache_recomp = 0x8F000094;
-recomp_analog_cam_enabled = 0x8F000098;
+recomp_get_analog_cam_enabled = 0x8F000098;
recomp_get_camera_inputs = 0x8F00009C;
recomp_set_right_analog_suppressed = 0x8F0000A0;
recomp_get_inverted_axes = 0x8F0000A4;
recomp_high_precision_fb_enabled = 0x8F0000A8;
recomp_get_resolution_scale = 0x8F0000AC;
recomp_get_analog_inverted_axes = 0x8F0000B0;
+recomp_get_window_resolution = 0x8F0000B4;
diff --git a/patches/ui_patches.c b/patches/ui_patches.c
index a057655..0177611 100644
--- a/patches/ui_patches.c
+++ b/patches/ui_patches.c
@@ -56,9 +56,10 @@ RECOMP_PATCH void Graph_SetNextGfxPool(GraphicsContext* gfxCtx) {
gSPEndDisplayList(&gGfxMasterDL->disps[4]);
gSPBranchList(&gGfxMasterDL->debugDisp[0], bigger_pool->debugBuffer);
- // @recomp Enable RT64 extended GBI mode and set the current framerate
+ // @recomp Enable RT64 extended GBI mode and extended rdram.
OPEN_DISPS(gfxCtx);
gEXEnable(POLY_OPA_DISP++);
+ gEXSetRDRAMExtended(POLY_OPA_DISP++, 1);
CLOSE_DISPS(gfxCtx);
}
diff --git a/src/game/config.cpp b/src/game/config.cpp
index ac8ebff..ab24e0a 100644
--- a/src/game/config.cpp
+++ b/src/game/config.cpp
@@ -19,7 +19,6 @@ constexpr std::u8string_view general_filename = u8"general.json";
constexpr std::u8string_view graphics_filename = u8"graphics.json";
constexpr std::u8string_view controls_filename = u8"controls.json";
constexpr std::u8string_view sound_filename = u8"sound.json";
-constexpr std::u8string_view program_id = u8"Zelda64Recompiled";
constexpr auto res_default = ultramodern::renderer::Resolution::Auto;
constexpr auto hr_default = ultramodern::renderer::HUDRatioMode::Clamp16x9;
diff --git a/src/game/input.cpp b/src/game/input.cpp
index 4aff5e6..fc707ee 100644
--- a/src/game/input.cpp
+++ b/src/game/input.cpp
@@ -2,7 +2,7 @@
#include
#include "ultramodern/ultramodern.hpp"
-#include "librecomp/recomp.h"
+#include "recomp.h"
#include "recomp_input.h"
#include "zelda_config.h"
#include "recomp_ui.h"
diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp
index 28b4779..9ad08be 100644
--- a/src/game/recomp_api.cpp
+++ b/src/game/recomp_api.cpp
@@ -1,6 +1,6 @@
#include
-#include "librecomp/recomp.h"
+#include "recomp.h"
#include "librecomp/overlays.hpp"
#include "zelda_config.h"
#include "recomp_input.h"
@@ -58,7 +58,18 @@ extern "C" void recomp_get_target_framerate(uint8_t* rdram, recomp_context* ctx)
_return(ctx, ultramodern::get_target_framerate(60 / frame_divisor));
}
-extern "C" void recomp_get_aspect_ratio(uint8_t* rdram, recomp_context* ctx) {
+extern "C" void recomp_get_window_resolution(uint8_t* rdram, recomp_context* ctx) {
+ int width, height;
+ recompui::get_window_size(width, height);
+
+ gpr width_out = _arg<0, PTR(u32)>(rdram, ctx);
+ gpr height_out = _arg<1, PTR(u32)>(rdram, ctx);
+
+ MEM_W(0, width_out) = (u32)width;
+ MEM_W(0, height_out) = (u32)height;
+}
+
+extern "C" void recomp_get_target_aspect_ratio(uint8_t* rdram, recomp_context* ctx) {
ultramodern::renderer::GraphicsConfig graphics_config = ultramodern::renderer::get_graphics_config();
float original = _arg<0, float>(rdram, ctx);
int width, height;
@@ -91,7 +102,7 @@ extern "C" void recomp_time_us(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast(std::chrono::duration_cast(ultramodern::time_since_start()).count()));
}
-extern "C" void recomp_autosave_enabled(uint8_t* rdram, recomp_context* ctx) {
+extern "C" void recomp_get_autosave_enabled(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast(zelda64::get_autosave_mode() == zelda64::AutosaveMode::On));
}
@@ -131,7 +142,7 @@ extern "C" void recomp_get_analog_inverted_axes(uint8_t* rdram, recomp_context*
*y_out = (mode == zelda64::CameraInvertMode::InvertY || mode == zelda64::CameraInvertMode::InvertBoth);
}
-extern "C" void recomp_analog_cam_enabled(uint8_t* rdram, recomp_context* ctx) {
+extern "C" void recomp_get_analog_cam_enabled(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, zelda64::get_analog_cam_mode() == zelda64::AnalogCamMode::On);
}
diff --git a/src/game/rom_decompression.cpp b/src/game/rom_decompression.cpp
new file mode 100644
index 0000000..2576fb2
--- /dev/null
+++ b/src/game/rom_decompression.cpp
@@ -0,0 +1,168 @@
+#include
+#include
+#include
+
+#include "zelda_game.h"
+
+void naive_copy(std::span dst, std::span src) {
+ for (size_t i = 0; i < src.size(); i++) {
+ dst[i] = src[i];
+ }
+}
+
+void yaz0_decompress(std::span input, std::span output) {
+ int32_t layoutBitIndex;
+ uint8_t layoutBits;
+
+ size_t input_pos = 0;
+ size_t output_pos = 0;
+
+ size_t input_size = input.size();
+ size_t output_size = output.size();
+
+ while (input_pos < input_size) {
+ int32_t layoutBitIndex = 0;
+ uint8_t layoutBits = input[input_pos++];
+
+ while (layoutBitIndex < 8 && input_pos < input_size && output_pos < output_size) {
+ if (layoutBits & 0x80) {
+ output[output_pos++] = input[input_pos++];
+ } else {
+ int32_t firstByte = input[input_pos++];
+ int32_t secondByte = input[input_pos++];
+ uint32_t bytes = firstByte << 8 | secondByte;
+ uint32_t offset = (bytes & 0x0FFF) + 1;
+ uint32_t length;
+
+ // Check how the group length is encoded
+ if ((firstByte & 0xF0) == 0) {
+ // 3 byte encoding, 0RRRNN
+ int32_t thirdByte = input[input_pos++];
+ length = thirdByte + 0x12;
+ } else {
+ // 2 byte encoding, NRRR
+ length = ((bytes & 0xF000) >> 12) + 2;
+ }
+
+ naive_copy(output.subspan(output_pos, length), output.subspan(output_pos - offset, length));
+ output_pos += length;
+ }
+
+ layoutBitIndex++;
+ layoutBits <<= 1;
+ }
+ }
+}
+
+#ifdef _MSC_VER
+inline uint32_t byteswap(uint32_t val) {
+ return _byteswap_ulong(val);
+}
+#else
+constexpr uint32_t byteswap(uint32_t val) {
+ return __builtin_bswap32(val);
+}
+#endif
+
+// Produces a decompressed MM rom. This is only needed because the game has compressed code.
+// For other recomps using this repo as an example, you can omit the decompression routine and
+// set the corresponding fields in the GameEntry if the game doesn't have compressed code,
+// even if it does have compressed data.
+std::vector zelda64::decompress_mm(std::span compressed_rom) {
+ // Sanity check the rom size and header. These should already be correct from the runtime's check,
+ // but it should prevent this file from accidentally being copied to another recomp.
+ if (compressed_rom.size() != 0x2000000) {
+ assert(false);
+ return {};
+ }
+
+ if (compressed_rom[0x3B] != 'N' || compressed_rom[0x3C] != 'Z' || compressed_rom[0x3D] != 'S' || compressed_rom[0x3E] != 'E') {
+ assert(false);
+ return {};
+ }
+
+ struct DmaDataEntry {
+ uint32_t vrom_start;
+ uint32_t vrom_end;
+ uint32_t rom_start;
+ uint32_t rom_end;
+
+ void bswap() {
+ vrom_start = byteswap(vrom_start);
+ vrom_end = byteswap(vrom_end);
+ rom_start = byteswap(rom_start);
+ rom_end = byteswap(rom_end);
+ }
+ };
+
+ DmaDataEntry cur_entry{};
+ size_t cur_entry_index = 0;
+
+ constexpr size_t dma_data_rom_addr = 0x1A500;
+
+ std::vector ret{};
+ ret.resize(0x2F00000);
+
+ size_t content_end = 0;
+
+ do {
+ // Read the entry from the compressed rom.
+ size_t cur_entry_rom_address = dma_data_rom_addr + (cur_entry_index++) * sizeof(DmaDataEntry);
+ memcpy(&cur_entry, compressed_rom.data() + cur_entry_rom_address, sizeof(DmaDataEntry));
+ // Swap the entry to native endianness after reading from the big endian data.
+ cur_entry.bswap();
+
+ // Rom end being 0 means the data is already uncompressed, so copy it as-is to vrom start.
+ size_t entry_decompressed_size = cur_entry.vrom_end - cur_entry.vrom_start;
+ if (cur_entry.rom_end == 0) {
+ memcpy(ret.data() + cur_entry.vrom_start, compressed_rom.data() + cur_entry.rom_start, entry_decompressed_size);
+
+ // Edit the entry to account for it being in a new location now.
+ cur_entry.rom_start = cur_entry.vrom_start;
+ }
+ // Otherwise, decompress the input data into the output data.
+ else {
+ if (cur_entry.rom_end != cur_entry.rom_start) {
+ // Validate the presence of the yaz0 header.
+ if (compressed_rom[cur_entry.rom_start + 0] != 'Y' ||
+ compressed_rom[cur_entry.rom_start + 1] != 'a' ||
+ compressed_rom[cur_entry.rom_start + 2] != 'z' ||
+ compressed_rom[cur_entry.rom_start + 3] != '0')
+ {
+ assert(false);
+ return {};
+ }
+ // Skip the yaz0 header.
+ size_t compressed_data_rom_start = cur_entry.rom_start + 0x10;
+ size_t entry_compressed_size = cur_entry.rom_end - compressed_data_rom_start;
+
+ std::span input_span = std::span{ compressed_rom }.subspan(compressed_data_rom_start, entry_compressed_size);
+ std::span output_span = std::span{ ret }.subspan(cur_entry.vrom_start, entry_decompressed_size);
+ yaz0_decompress(input_span, output_span);
+
+ // Edit the entry to account for it being decompressed now.
+ cur_entry.rom_start = cur_entry.vrom_start;
+ cur_entry.rom_end = 0;
+ }
+ }
+
+ if (entry_decompressed_size != 0) {
+ if (cur_entry.vrom_end > content_end) {
+ content_end = cur_entry.vrom_end;
+ }
+ }
+
+ // Swap the entry back to big endian for writing.
+ cur_entry.bswap();
+ // Write the modified entry to the decompressed rom.
+ memcpy(ret.data() + cur_entry_rom_address, &cur_entry, sizeof(DmaDataEntry));
+ } while (cur_entry.vrom_end != 0);
+
+ // Align the start of padding to the closest 0x1000 (matches decomp rom decompression behavior).
+ content_end = (content_end + 0x1000 - 1) & -0x1000;
+
+ // Write 0xFF as the padding.
+ std::fill(ret.begin() + content_end, ret.end(), 0xFF);
+
+ return ret;
+}
diff --git a/src/main/main.cpp b/src/main/main.cpp
index d350b6d..69f130c 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -25,8 +25,16 @@
#include "zelda_config.h"
#include "zelda_sound.h"
#include "zelda_render.h"
+#include "zelda_game.h"
#include "ovl_patches.hpp"
#include "librecomp/game.hpp"
+#include "librecomp/mods.hpp"
+#include "librecomp/helpers.hpp"
+
+#include "../../patches/graphics.h"
+#include "../../patches/input.h"
+#include "../../patches/sound.h"
+#include "../../patches/misc_funcs.h"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
@@ -36,6 +44,8 @@
#include "../../lib/rt64/src/contrib/stb/stb_image.h"
+const std::string version_string = "1.2.0-dev";
+
template
void exit_error(const char* str, Ts ...args) {
// TODO pop up an error
@@ -52,14 +62,12 @@ ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
-#if defined(__linux__)
- SDL_SetHint(SDL_HINT_VIDEODRIVER, "x11");
-#endif
-
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) > 0) {
exit_error("Failed to initialize SDL2: %s\n", SDL_GetError());
}
+ fprintf(stdout, "SDL Video Driver: %s\n", SDL_GetCurrentVideoDriver());
+
return {};
}
@@ -115,7 +123,13 @@ bool SetImageAsIcon(const char* filename, SDL_Window* window)
SDL_Window* window;
ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
- window = SDL_CreateWindow("Zelda 64: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, SDL_WINDOW_RESIZABLE );
+ uint32_t flags = SDL_WINDOW_RESIZABLE;
+
+#if defined(RT64_SDL_WINDOW_VULKAN)
+ flags |= SDL_WINDOW_VULKAN;
+#endif
+
+ window = SDL_CreateWindow("Zelda 64: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, flags);
#if defined(__linux__)
SetImageAsIcon("icons/512.png",window);
if (ultramodern::renderer::get_graphics_config().wm_option == ultramodern::renderer::WindowMode::Fullscreen) { // TODO: Remove once RT64 gets native fullscreen support on Linux
@@ -135,14 +149,8 @@ ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::
#if defined(_WIN32)
return ultramodern::renderer::WindowHandle{ wmInfo.info.win.window, GetCurrentThreadId() };
-#elif defined(__ANDROID__)
- static_assert(false && "Unimplemented");
-#elif defined(__linux__)
- if (wmInfo.subsystem != SDL_SYSWM_X11) {
- exit_error("Unsupported SDL2 video driver \"%s\". Only X11 is supported on Linux.\n", SDL_GetCurrentVideoDriver());
- }
-
- return ultramodern::renderer::WindowHandle{ wmInfo.info.x11.display, wmInfo.info.x11.window };
+#elif defined(__linux__) || defined(__ANDROID__)
+ return ultramodern::renderer::WindowHandle{ window };
#else
static_assert(false && "Unimplemented");
#endif
@@ -327,7 +335,11 @@ std::vector supported_games = {
.rom_hash = 0xEF18B4A9E2386169ULL,
.internal_name = "ZELDA MAJORA'S MASK",
.game_id = u8"mm.n64.us.1.0",
- .is_enabled = true,
+ .mod_game_id = "mm",
+ .save_type = recomp::SaveType::Flashram,
+ .is_enabled = false,
+ .decompression_routine = zelda64::decompress_mm,
+ .has_compressed_code = true,
.entrypoint_address = get_entrypoint_address(),
.entrypoint = recomp_entrypoint,
},
@@ -524,7 +536,27 @@ void release_preload(PreloadContext& context) {
#endif
+void enable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
+ (void)context;
+ zelda64::renderer::enable_texture_pack(mod);
+}
+
+void disable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
+ (void)context;
+ zelda64::renderer::disable_texture_pack(mod);
+}
+
+#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
+
int main(int argc, char** argv) {
+ (void)argc;
+ (void)argv;
+ recomp::Version project_version{};
+ if (!recomp::Version::from_string(version_string, project_version)) {
+ ultramodern::error_handling::message_box(("Invalid version string: " + version_string).c_str());
+ return EXIT_FAILURE;
+ }
+
// Map this executable into memory and lock it, which should keep it in physical memory. This ensures
// that there are no stutters from the OS having to load new pages of the executable whenever a new code page is run.
PreloadContext preload_context;
@@ -568,15 +600,29 @@ int main(int argc, char** argv) {
fprintf(stderr, "Failed to load controller mappings: %s\n", SDL_GetError());
}
+ recomp::register_config_path(zelda64::get_app_folder_path());
+
// Register supported games and patches
for (const auto& game : supported_games) {
recomp::register_game(game);
}
+ REGISTER_FUNC(recomp_get_window_resolution);
+ REGISTER_FUNC(recomp_get_target_aspect_ratio);
+ REGISTER_FUNC(recomp_get_target_framerate);
+ REGISTER_FUNC(recomp_get_autosave_enabled);
+ REGISTER_FUNC(recomp_get_analog_cam_enabled);
+ REGISTER_FUNC(recomp_get_camera_inputs);
+ REGISTER_FUNC(recomp_get_targeting_mode);
+ REGISTER_FUNC(recomp_get_bgm_volume);
+ REGISTER_FUNC(recomp_get_low_health_beeps_enabled);
+ REGISTER_FUNC(recomp_get_gyro_deltas);
+ REGISTER_FUNC(recomp_get_mouse_deltas);
+ REGISTER_FUNC(recomp_get_inverted_axes);
+ REGISTER_FUNC(recomp_get_analog_inverted_axes);
+
zelda64::register_overlays();
zelda64::register_patches();
-
- recomp::register_config_path(zelda64::get_app_folder_path());
zelda64::load_config();
recomp::rsp::callbacks_t rsp_callbacks{
@@ -619,7 +665,47 @@ int main(int argc, char** argv) {
.get_game_thread_name = zelda64::get_game_thread_name,
};
+ // Register the texture pack content type with rt64.json as its content file.
+ recomp::mods::ModContentType texture_pack_content_type{
+ .content_filename = "rt64.json",
+ .allow_runtime_toggle = true,
+ .on_enabled = enable_texture_pack,
+ .on_disabled = disable_texture_pack,
+ };
+ auto texture_pack_content_type_id = recomp::mods::register_mod_content_type(texture_pack_content_type);
+
+ // Register the .rtz texture pack file format with the previous content type as its only allowed content type.
+ recomp::mods::register_mod_container_type("rtz", std::vector{ texture_pack_content_type_id }, false);
+
+ recomp::mods::scan_mods();
+
+ printf("Found mods:\n");
+ for (const auto& mod : recomp::mods::get_mod_details("mm")) {
+ printf(" %s(%s)\n", mod.mod_id.c_str(), mod.version.to_string().c_str());
+ if (!mod.authors.empty()) {
+ printf(" Authors: %s", mod.authors[0].c_str());
+ for (size_t author_index = 1; author_index < mod.authors.size(); author_index++) {
+ const std::string& author = mod.authors[author_index];
+ printf(", %s", author.c_str());
+ }
+ printf("\n");
+ printf(" Runtime toggleable: %d\n", mod.runtime_toggleable);
+ }
+ if (!mod.dependencies.empty()) {
+ printf(" Dependencies: %s:%s", mod.dependencies[0].mod_id.c_str(), mod.dependencies[0].version.to_string().c_str());
+ for (size_t dep_index = 1; dep_index < mod.dependencies.size(); dep_index++) {
+ const recomp::mods::Dependency& dep = mod.dependencies[dep_index];
+ printf(", %s:%s", dep.mod_id.c_str(), dep.version.to_string().c_str());
+ }
+ printf("\n");
+ }
+ // TODO load all mods as a temporary solution to not having a UI yet.
+ recomp::mods::enable_mod(mod.mod_id, true);
+ }
+ printf("\n");
+
recomp::start(
+ project_version,
{},
rsp_callbacks,
renderer_callbacks,
diff --git a/src/main/register_patches.cpp b/src/main/register_patches.cpp
index 12c5725..91bbe80 100644
--- a/src/main/register_patches.cpp
+++ b/src/main/register_patches.cpp
@@ -7,4 +7,7 @@
void zelda64::register_patches() {
recomp::overlays::register_patches(mm_patches_bin, sizeof(mm_patches_bin), section_table, ARRLEN(section_table));
+ recomp::overlays::register_base_exports(export_table);
+ recomp::overlays::register_base_events(event_names);
+ recomp::overlays::register_manual_patch_symbols(manual_patch_symbols);
}
diff --git a/src/main/rt64_render_context.cpp b/src/main/rt64_render_context.cpp
index 497e504..126ab70 100644
--- a/src/main/rt64_render_context.cpp
+++ b/src/main/rt64_render_context.cpp
@@ -1,5 +1,6 @@
#include
#include
+#include
#define HLSL_CPU
#include "hle/rt64_application.h"
@@ -10,6 +11,13 @@
#include "zelda_render.h"
#include "recomp_ui.h"
+#include "concurrentqueue.h"
+
+// Helper class for variant visiting.
+template
+struct overloaded : Ts... { using Ts::operator()...; };
+template
+overloaded(Ts...) -> overloaded;
static RT64::UserConfiguration::Antialiasing device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
static bool sample_positions_supported = false;
@@ -18,6 +26,18 @@ static bool high_precision_fb_enabled = false;
static uint8_t DMEM[0x1000];
static uint8_t IMEM[0x1000];
+struct TexturePackEnableAction {
+ std::filesystem::path path;
+};
+
+struct TexturePackDisableAction {
+ std::filesystem::path path;
+};
+
+using TexturePackAction = std::variant;
+
+static moodycamel::ConcurrentQueue texture_pack_action_queue;
+
unsigned int MI_INTR_REG = 0;
unsigned int DPC_START_REG = 0;
@@ -180,11 +200,8 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
RT64::Application::Core appCore{};
#if defined(_WIN32)
appCore.window = window_handle.window;
-#elif defined(__ANDROID__)
- assert(false && "Unimplemented");
-#elif defined(__linux__)
- appCore.window.display = window_handle.display;
- appCore.window.window = window_handle.window;
+#elif defined(__linux__) || defined(__ANDROID__)
+ appCore.window = window_handle;
#elif defined(__APPLE__)
appCore.window.window = window_handle.window;
appCore.window.view = window_handle.view;
@@ -286,6 +303,32 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
zelda64::renderer::RT64Context::~RT64Context() = default;
void zelda64::renderer::RT64Context::send_dl(const OSTask* task) {
+ bool packs_disabled = false;
+ TexturePackAction cur_action;
+ while (texture_pack_action_queue.try_dequeue(cur_action)) {
+ std::visit(overloaded{
+ [&](TexturePackDisableAction& to_disable) {
+ enabled_texture_packs.erase(to_disable.path);
+ packs_disabled = true;
+ },
+ [&](TexturePackEnableAction& to_enable) {
+ enabled_texture_packs.insert(to_enable.path);
+ // Load the pack now if no packs have been disabled.
+ if (!packs_disabled) {
+ app->textureCache->loadReplacementDirectory(to_enable.path);
+ }
+ }
+ }, cur_action);
+ }
+
+ // If any packs were disabled, unload all packs and load all the active ones.
+ if (packs_disabled) {
+ app->textureCache->clearReplacementDirectories();
+ for (const std::filesystem::path& cur_pack_path : enabled_texture_packs) {
+ app->textureCache->loadReplacementDirectory(cur_pack_path);
+ }
+ }
+
app->state->rsp->reset();
app->interpreter->loadUCodeGBI(task->t.ucode & 0x3FFFFFF, task->t.ucode_data & 0x3FFFFFF, true);
app->processDisplayLists(app->core.RDRAM, task->t.data_ptr & 0x3FFFFFF, 0, true);
@@ -351,16 +394,6 @@ float zelda64::renderer::RT64Context::get_resolution_scale() const {
}
}
-void zelda64::renderer::RT64Context::load_shader_cache(std::span cache_binary) {
- // TODO figure out how to avoid a copy here.
- std::istringstream cache_stream{std::string{cache_binary.data(), cache_binary.size()}};
-
- if (!app->rasterShaderCache->loadOfflineList(cache_stream)) {
- printf("Failed to preload shader cache!\n");
- assert(false);
- }
-}
-
RT64::UserConfiguration::Antialiasing zelda64::renderer::RT64MaxMSAA() {
return device_max_msaa;
}
@@ -376,3 +409,11 @@ bool zelda64::renderer::RT64SamplePositionsSupported() {
bool zelda64::renderer::RT64HighPrecisionFBEnabled() {
return high_precision_fb_enabled;
}
+
+void zelda64::renderer::enable_texture_pack(const recomp::mods::ModHandle& mod) {
+ texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_root_path});
+}
+
+void zelda64::renderer::disable_texture_pack(const recomp::mods::ModHandle& mod) {
+ texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_root_path});
+}
diff --git a/src/ui/ui_launcher.cpp b/src/ui/ui_launcher.cpp
index b4b6312..f699eb7 100644
--- a/src/ui/ui_launcher.cpp
+++ b/src/ui/ui_launcher.cpp
@@ -6,7 +6,7 @@
#include "nfd.h"
#include
-std::string version_number = "v1.1.1";
+static std::string version_string;
Rml::DataModelHandle model_handle;
bool mm_rom_valid = false;
@@ -103,7 +103,9 @@ public:
Rml::DataModelConstructor constructor = context->CreateDataModel("launcher_model");
constructor.Bind("mm_rom_valid", &mm_rom_valid);
- constructor.Bind("version_number", &version_number);
+
+ version_string = recomp::get_project_version().to_string();
+ constructor.Bind("version_number", &version_string);
model_handle = constructor.GetModelHandle();
}
diff --git a/src/ui/ui_renderer.cpp b/src/ui/ui_renderer.cpp
index 8fb93d4..a234d7c 100644
--- a/src/ui/ui_renderer.cpp
+++ b/src/ui/ui_renderer.cpp
@@ -1287,6 +1287,12 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
static recompui::Menu prev_menu = recompui::Menu::None;
recompui::Menu cur_menu = open_menu.load();
+ // Return to the launcher if no menu is open and the game isn't started.
+ if (cur_menu == recompui::Menu::None && !ultramodern::is_game_started()) {
+ cur_menu = recompui::Menu::Launcher;
+ recompui::set_current_menu(cur_menu);
+ }
+
if (reload_sheets) {
ui_context->rml.load_documents();
prev_menu = recompui::Menu::None;
diff --git a/us.rev1.toml b/us.rev1.toml
index 7e776f4..7257691 100644
--- a/us.rev1.toml
+++ b/us.rev1.toml
@@ -5,6 +5,7 @@ entrypoint = 0x80080000
# Paths are relative to the location of this config file.
output_func_path = "RecompiledFuncs"
relocatable_sections_path = "overlays.us.rev1.txt"
+# elf_path = "mm.us.rev1.rom_uncompressed.elf"
symbols_file_path = "Zelda64RecompSyms/mm.us.rev1.syms.toml"
rom_file_path = "mm.us.rev1.rom_uncompressed.z64"