diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4440cc2..b9f7c96 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: 'd33d38161798167929b114c2b0fd445f9670e10a' 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: @@ -108,7 +108,7 @@ jobs: 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/CMakeLists.txt b/CMakeLists.txt index 62bce6f..0b900bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,16 +138,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 @@ -174,10 +164,6 @@ 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/concurrentqueue @@ -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) 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/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_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..d5c81d0 160000 --- a/lib/N64ModernRuntime +++ b/lib/N64ModernRuntime @@ -1 +1 @@ -Subproject commit 0a538553330ea5fdb1673708704bb92a854241b9 +Subproject commit d5c81d0a6bf2e5f36747a095a7a060d7623bbf58 diff --git a/lib/rt64 b/lib/rt64 index 88c618c..67422c3 160000 --- a/lib/rt64 +++ b/lib/rt64 @@ -1 +1 @@ -Subproject commit 88c618c1f8d8089f94e78537e49e0d77798fc0be +Subproject commit 67422c3b647058d3d38f2813a2abe79cf1638f13 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..bcf5c3c 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_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; @@ -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_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..46e018f 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; } @@ -784,7 +790,7 @@ RECOMP_PATCH s32 Camera_Normal1(Camera* camera) { // @recomp Update the analog camera. if (recomp_analog_cam_enabled()) { update_analog_cam(camera); - + if (analog_cam_active) { spB4.pitch = analog_camera_pos.pitch; // spB4.r = analog_camera_pos.r; @@ -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()) { 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; @@ -1374,7 +1433,7 @@ RECOMP_PATCH s32 Camera_Parallel1(Camera* camera) { // @recomp Update the analog camera. if (recomp_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()) { update_analog_cam(camera); - + if (analog_cam_active) { sp80.pitch = analog_camera_pos.pitch; // sp80.r = analog_camera_pos.r; @@ -1822,7 +1894,7 @@ RECOMP_PATCH s32 Camera_Jump3(Camera* camera) { // @recomp Update the analog camera. if (recomp_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); @@ -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/input.c b/patches/input.c index 53f7eb3..2962204 100644 --- a/patches/input.c +++ b/patches/input.c @@ -534,11 +534,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 +568,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 +623,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 +682,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 +699,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 +769,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 +874,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 +1260,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/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/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/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/main/main.cpp b/src/main/main.cpp index d350b6d..bf13a28 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -27,6 +27,7 @@ #include "zelda_render.h" #include "ovl_patches.hpp" #include "librecomp/game.hpp" +#include "librecomp/mods.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -36,6 +37,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 @@ -327,6 +330,8 @@ std::vector supported_games = { .rom_hash = 0xEF18B4A9E2386169ULL, .internal_name = "ZELDA MAJORA'S MASK", .game_id = u8"mm.n64.us.1.0", + .mod_game_id = "mm", + .save_type = recomp::SaveType::Flashram, .is_enabled = true, .entrypoint_address = get_entrypoint_address(), .entrypoint = recomp_entrypoint, @@ -524,7 +529,23 @@ 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); +} + int main(int argc, char** 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,6 +589,8 @@ 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); @@ -575,8 +598,6 @@ int main(int argc, char** argv) { 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 +640,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..ef800fd 100644 --- a/src/main/register_patches.cpp +++ b/src/main/register_patches.cpp @@ -7,4 +7,6 @@ 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); } diff --git a/src/main/rt64_render_context.cpp b/src/main/rt64_render_context.cpp index 497e504..e800d99 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; @@ -286,6 +306,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 +397,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 +412,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"