This commit is contained in:
David Chavez 2024-11-04 14:52:23 +00:00 committed by GitHub
commit 6dd650cc75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1230 additions and 137 deletions

33
.github/macos/Info.plist.in vendored Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleExecutable</key>
<string>Zelda64Recompiled</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSMinimumSystemVersion</key>
<string>11</string>
<key>LSEnvironment</key>
<dict>
<key>MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS</key>
<string>1</string>
<key>MVK_CONFIG_USE_METAL_PRIVATE_API</key>
<string>1</string>
<key>MVK_CONFIG_RESUME_LOST_DEVICE</key>
<string>1</string>
</dict>
</dict>
</plist>

8
.github/macos/MoltenVK_icd.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"file_format_version": "1.0.0",
"ICD": {
"library_path": "../../../Frameworks/libMoltenVK.dylib",
"api_version": "1.2.0",
"is_portability_driver": true
}
}

12
.github/macos/entitlements.plist vendored Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

11
.github/macos/fixup_bundle.cmake vendored Normal file
View File

@ -0,0 +1,11 @@
include(BundleUtilities)
# Use generator expressions to get the absolute path to the bundle and frameworks
set(APPS "Zelda64Recompiled.app/Contents/MacOS/Zelda64Recompiled")
set(DIRS "Zelda64Recompiled.app/Contents/Frameworks")
# The fixup_bundle command needs an absolute path
file(REAL_PATH ${APPS} APPS)
file(REAL_PATH ${DIRS} DIRS)
fixup_bundle("${APPS}" "" "${DIRS}")

14
.github/macos/macports.yaml vendored Normal file
View File

@ -0,0 +1,14 @@
version: '2.9.3'
prefix: '/opt/local'
variants:
select:
- aqua
- metal
deselect: x11
ports:
- name: clang-18
- name: llvm-18
- name: libsdl2
select: universal
- name: freetype
select: universal

View File

@ -9,11 +9,19 @@ on:
N64RECOMP_COMMIT:
type: string
required: false
default: '5b17bf8bb556d2544c6161487232a455eae8f188'
default: 'd33d38161798167929b114c2b0fd445f9670e10a'
DXC_CHECKSUM:
type: string
required: false
default: '4e6f4e52989aca69739880b40b9f988357f15d10ca03284377b81f1502463ff5'
VULKAN_SDK_VERSION:
type: string
required: false
default: '1.3.296.0'
MOLTENVK_COMMIT:
type: string
required: false
default: ''
secrets:
ZRE_REPO_WITH_PAT:
required: true
@ -22,7 +30,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 +103,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 +111,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 +207,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
type: [ Debug, Release ]
type: [ Debug, RelWithDebInfo ]
name: windows (${{ matrix.type }})
steps:
- name: Checkout
@ -264,6 +272,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 +285,125 @@ jobs:
SDL2.dll
assets/
gamecontrollerdb.txt
- name: Archive Debug Files
uses: actions/upload-artifact@v4
with:
name: Zelda64Recompiled-PDB-${{ matrix.type }}
path: |
Zelda64Recompiled.pdb
build-macos:
runs-on: blaze/macos-14
strategy:
matrix:
type: [ Debug, Release ]
name: macos (x64, arm64, ${{ matrix.type }})
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
submodules: recursive
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ${{ runner.os }}-z64re-ccache-${{ matrix.type }}
- name: Homebrew Setup
run: |
brew install ninja
brew uninstall --ignore-dependencies libpng freetype
- name: MacPorts Setup
uses: melusina-org/setup-macports@v1
id: 'macports'
with:
parameters: '.github/macos/macports.yaml'
- name: Prepare Build
run: |-
git clone ${{ secrets.ZRE_REPO_WITH_PAT }}
./zre/process.sh
cp ./zre/mm_shader_cache.bin ./shadercache/
- name: Build N64Recomp & RSPRecomp
run: |
git clone https://github.com/Mr-Wiseguy/N64Recomp.git --recurse-submodules N64RecompSource
cd N64RecompSource
git checkout ${{ inputs.N64RECOMP_COMMIT }}
git submodule update --init --recursive
# enable ccache
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
# Build N64Recomp & RSPRecomp
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build
cmake --build cmake-build --config Release --target N64Recomp -j $(sysctl -n hw.ncpu)
cmake --build cmake-build --config Release --target RSPRecomp -j $(sysctl -n hw.ncpu)
# Copy N64Recomp & RSPRecomp to root directory
cp cmake-build/N64Recomp ..
cp cmake-build/RSPRecomp ..
- name: Run N64Recomp & RSPRecomp
run: |
./N64Recomp us.rev1.toml
./RSPRecomp aspMain.us.rev1.toml
./RSPRecomp njpgdspMain.us.rev1.toml
- name: Cache Vulkan SDK
id: cache-vulkan-sdk
uses: actions/cache@v3
with:
path: /Users/runner/VulkanSDK/${{ inputs.VULKAN_SDK_VERSION }}
key: ${{ runner.os }}-${{ matrix.type }}-vulkan-sdk-${{ inputs.VULKAN_SDK_VERSION }}
- name: Install Vulkan SDK
if: steps.cache-vulkan-sdk.outputs.cache-hit != 'true'
run: |
wget https://sdk.lunarg.com/sdk/download/${{ inputs.VULKAN_SDK_VERSION }}/mac/vulkansdk-macos-${{ inputs.VULKAN_SDK_VERSION }}.zip
unzip vulkansdk-macos-${{ inputs.VULKAN_SDK_VERSION }}.zip
sudo InstallVulkan.app/Contents/MacOS/InstallVulkan --root ~/VulkanSDK/${{ inputs.VULKAN_SDK_VERSION }} --accept-licenses --default-answer --confirm-command install
- name: Checkout MoltenVK
if: inputs.MOLTENVK_COMMIT != ''
run: |
git clone https://github.com/KhronosGroup/MoltenVK.git
cd MoltenVK
git checkout ${{ inputs.MOLTENVK_COMMIT }}
- name: Cache MoltenVK Dependencies
if: inputs.MOLTENVK_COMMIT != ''
id: cache-mvk-dependencies
uses: actions/cache@v3
with:
path: |
MoltenVK/External/build
!MoltenVK/External/build/Intermediates
key: ${{ runner.os }}-${{ matrix.type }}-${{ hashFiles('MoltenVK/fetchDependencies','MoltenVK/ExternalRevisions/**','MoltenVK/ExternalDependencies.xcodeproj/**','MoltenVK/Scripts/**') }}
- name: Fetch MVK Dependencies (Use Built Cache)
if: inputs.MOLTENVK_COMMIT != '' && steps.cache-mvk-dependencies.outputs.cache-hit == 'true'
run: |
cd MoltenVK
./fetchDependencies -v --none
- name: Fetch MVK Dependencies
if: inputs.MOLTENVK_COMMIT != '' && steps.cache-mvk-dependencies.outputs.cache-hit != 'true'
run: |
cd MoltenVK
./fetchDependencies -v --macos
- name: Build MoltenVK
if: inputs.MOLTENVK_COMMIT != ''
run: |
cd MoltenVK
make macos
sudo cp Package/Latest/MoltenVK/dylib/macOS/libMoltenVK.dylib ~/VulkanSDK/${{ inputs.VULKAN_SDK_VERSION }}/macOS/lib/libMoltenVK.dylib
- name: Build ZeldaRecomp
run: |-
# enable ccache
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build \
-DPATCHES_LD=/opt/local/bin/ld.lld-mp-18 -DPATCHES_OBJCOPY=/opt/local/bin/llvm-objcopy-mp-18 -DCMAKE_AR=/opt/local/bin/llvm-ar-mp-18 -DPATCHES_C_COMPILER=/opt/local/bin/clang-mp-18 \
-DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"
cmake --build cmake-build --config ${{ matrix.type }} --target Zelda64Recompiled -j $(sysctl -n hw.ncpu)
env:
VULKAN_SDK: /Users/runner/VulkanSDK/${{ inputs.VULKAN_SDK_VERSION }}/macOS
- name: Prepare Archive
run: |
mv cmake-build/Zelda64Recompiled.app Zelda64Recompiled.app
zip -r -y Zelda64Recompiled.zip Zelda64Recompiled.app
- name: Archive Zelda64Recomp
uses: actions/upload-artifact@v4
with:
name: Zelda64Recompiled-${{ runner.os }}-${{ matrix.type }}
path: Zelda64Recompiled.zip

View File

@ -1,5 +1,10 @@
cmake_minimum_required(VERSION 3.20)
project(Zelda64Recompiled)
if (APPLE) # has to be set before the first project() or enable_language()
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version")
endif()
project(Zelda64Recompiled LANGUAGES C CXX)
set(CMAKE_C_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -15,6 +20,30 @@ if (WIN32)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR")
endif()
if (APPLE)
enable_language(OBJC OBJCXX)
# Check if VULKAN_SDK environment variable is set
if(NOT DEFINED ENV{VULKAN_SDK})
message(FATAL_ERROR "VULKAN_SDK environment variable is not set.")
else()
set(VULKAN_SDK $ENV{VULKAN_SDK})
endif()
# Define paths to Vulkan loader and MoltenVK libraries
set(VULKAN_LOADER_PATH "${VULKAN_SDK}/lib/libvulkan.dylib")
set(MOLTENVK_PATH "${VULKAN_SDK}/lib/libMoltenVK.dylib")
# Ensure the Vulkan loader and MoltenVK libraries exist
if(NOT EXISTS "${VULKAN_LOADER_PATH}")
message(FATAL_ERROR "Vulkan loader not found at ${VULKAN_LOADER_PATH}")
endif()
if(NOT EXISTS "${MOLTENVK_PATH}")
message(FATAL_ERROR "MoltenVK not found at ${MOLTENVK_PATH}")
endif()
endif()
# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24:
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
cmake_policy(SET CMP0135 NEW)
@ -135,21 +164,12 @@ add_custom_target(DownloadGameControllerDB
DEPENDS ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt)
# Main executable
add_executable(Zelda64Recompiled)
add_executable(Zelda64Recompiled MACOSX_BUNDLE)
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/support.cpp
${CMAKE_SOURCE_DIR}/src/main/register_overlays.cpp
${CMAKE_SOURCE_DIR}/src/main/register_patches.cpp
${CMAKE_SOURCE_DIR}/src/main/rt64_render_context.cpp
@ -174,8 +194,8 @@ 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)
if (APPLE)
list(APPEND SOURCES ${CMAKE_SOURCE_DIR}/src/main/support_apple.mm)
endif()
target_include_directories(Zelda64Recompiled PRIVATE
@ -210,6 +230,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)
@ -250,6 +275,114 @@ if (WIN32)
)
target_sources(Zelda64Recompiled PRIVATE ${CMAKE_SOURCE_DIR}/icons/app.rc)
target_link_libraries(Zelda64Recompiled PRIVATE SDL2)
endif()
if (APPLE)
find_package(SDL2 REQUIRED)
target_include_directories(Zelda64Recompiled PRIVATE ${SDL2_INCLUDE_DIRS})
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads REQUIRED)
target_link_libraries(Zelda64Recompiled PRIVATE ${CMAKE_DL_LIBS} Threads::Threads SDL2::SDL2)
# Set bundle properties
set_target_properties(Zelda64Recompiled PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "Zelda64Recompiled"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.github.zelda64recompiled"
MACOSX_BUNDLE_BUNDLE_VERSION "1.0"
MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0"
MACOSX_BUNDLE_ICON_FILE "AppIcon.icns"
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_BINARY_DIR}/Info.plist
)
set(ICON_SOURCE ${CMAKE_SOURCE_DIR}/icons/512.png)
set(ICONSET_DIR ${CMAKE_BINARY_DIR}/AppIcon.iconset)
set(ICNS_FILE ${CMAKE_BINARY_DIR}/resources/AppIcon.icns)
# Create iconset directory and add PNG file
add_custom_command(
OUTPUT ${ICONSET_DIR}
COMMAND ${CMAKE_COMMAND} -E make_directory ${ICONSET_DIR}
COMMAND ${CMAKE_COMMAND} -E copy ${ICON_SOURCE} ${ICONSET_DIR}/icon_512x512.png
COMMAND ${CMAKE_COMMAND} -E copy ${ICON_SOURCE} ${ICONSET_DIR}/icon_512x512@2x.png
COMMAND touch ${ICONSET_DIR}
COMMENT "Creating iconset directory and copying PNG file"
)
# Convert iconset to icns
add_custom_command(
OUTPUT ${ICNS_FILE}
DEPENDS ${ICONSET_DIR}
COMMAND iconutil -c icns ${ICONSET_DIR} -o ${ICNS_FILE}
COMMENT "Converting iconset to icns"
)
# Custom target to ensure icns creation
add_custom_target(create_icns ALL DEPENDS ${ICNS_FILE})
# Set source file properties for the resulting icns file
set_source_files_properties(${ICNS_FILE} PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources"
)
# Add the icns file to the executable target
target_sources(Zelda64Recompiled PRIVATE ${ICNS_FILE})
# Ensure Zelda64Recompiled depends on create_icns
add_dependencies(Zelda64Recompiled create_icns)
# Configure Info.plist
configure_file(${CMAKE_SOURCE_DIR}/.github/macos/Info.plist.in ${CMAKE_BINARY_DIR}/Info.plist @ONLY)
# Install the app bundle
install(TARGETS Zelda64Recompiled BUNDLE DESTINATION .)
# Copy required frameworks to bundle
target_link_libraries(Zelda64Recompiled PRIVATE ${MOLTENVK_PATH} ${VULKAN_LOADER_PATH})
# Define the path to the entitlements file
set(ENTITLEMENTS_FILE ${CMAKE_SOURCE_DIR}/.github/macos/entitlements.plist)
# Ensure the entitlements file exists
if(NOT EXISTS ${ENTITLEMENTS_FILE})
message(FATAL_ERROR "Entitlements file not found at ${ENTITLEMENTS_FILE}")
endif()
# Post-build steps for macOS bundle
add_custom_command(TARGET Zelda64Recompiled POST_BUILD
# Copy and fix frameworks first
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/.github/macos/fixup_bundle.cmake
# Copy all resources
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/assets ${CMAKE_BINARY_DIR}/temp_assets
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/temp_assets/scss
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/temp_assets $<TARGET_BUNDLE_DIR:Zelda64Recompiled>/Contents/Resources/assets
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/temp_assets
# Copy Vulkan ICD files
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_BUNDLE_DIR:Zelda64Recompiled>/Contents/Resources/vulkan/icd.d
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/.github/macOS/MoltenVK_icd.json $<TARGET_BUNDLE_DIR:Zelda64Recompiled>/Contents/Resources/vulkan/icd.d/
# Copy controller database
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt $<TARGET_BUNDLE_DIR:Zelda64Recompiled>/Contents/Resources/
# Set RPATH
COMMAND install_name_tool -add_rpath "@executable_path/../Frameworks/" $<TARGET_BUNDLE_DIR:Zelda64Recompiled>/Contents/MacOS/Zelda64Recompiled
# Apply JIT workaround
COMMAND ${CMAKE_COMMAND} -E echo "Applying JIT compilation workaround"
COMMAND /bin/bash -c "printf '\\x07' | dd of=$<TARGET_FILE:Zelda64Recompiled> bs=1 seek=160 count=1 conv=notrunc"
# Finally sign the whole bundle with runtime option and entitlements
COMMAND codesign --deep --force --sign - --entitlements ${ENTITLEMENTS_FILE} $<TARGET_BUNDLE_DIR:Zelda64Recompiled>
COMMENT "Performing post-build steps for macOS bundle"
VERBATIM
)
endif()
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
@ -283,7 +416,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux")
message(STATUS "FREETYPE_LIBRARIES = ${FREETYPE_LIBRARIES}")
include_directories(${FREETYPE_LIBRARIES})
target_link_libraries(Zelda64Recompiled PRIVATE ${FREETYPE_LIBRARIES})
target_link_libraries(Zelda64Recompiled PRIVATE ${FREETYPE_LIBRARIES} SDL2::SDL2)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
@ -295,7 +428,6 @@ endif()
target_link_libraries(Zelda64Recompiled PRIVATE
PatchesLib
RecompiledFuncs
SDL2
librecomp
ultramodern
rt64

View File

@ -15,7 +15,7 @@ Zelda 64: Recompiled is a project that uses [N64: Recompiled](https://github.com
<br style="display: none;"/>
_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._
</div>

View File

@ -58,7 +58,7 @@
</div>
</div>
<div class="bottom-left">
<label>{{version_number}}</label>
<label>v{{version_number}}</label>
</div>
</div>
</div>

View File

@ -120,7 +120,8 @@ namespace recomp {
std::vector<InputField> apply_menu;
};
constexpr const std::vector<InputField>& get_default_mapping_for_input(const DefaultN64Mappings& defaults, const GameInput input) {
inline const std::vector<InputField>& get_default_mapping_for_input(const DefaultN64Mappings& defaults, const GameInput input) {
static const std::vector<InputField> 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<InputField>();
default: return empty_input_field;
}
}

View File

@ -1,8 +1,12 @@
#ifndef __ZELDA_RENDER_H__
#define __ZELDA_RENDER_H__
#include <unordered_set>
#include <filesystem>
#include "common/rt64_user_configuration.h"
#include "ultramodern/renderer_context.hpp"
#include "librecomp/mods.hpp"
namespace RT64 {
struct Application;
@ -29,6 +33,7 @@ namespace zelda64 {
protected:
std::unique_ptr<RT64::Application> app;
std::unordered_set<std::filesystem::path> enabled_texture_packs;
};
std::unique_ptr<ultramodern::renderer::RendererContext> create_render_context(uint8_t *rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode);
@ -36,6 +41,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);
}
}

14
include/zelda_support.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef __ZELDA_SUPPORT_H__
#define __ZELDA_SUPPORT_H__
#include <functional>
namespace zelda64 {
void dispatch_on_main_thread(std::function<void()> func);
#ifdef __APPLE__
const char* get_bundle_resource_directory();
#endif
}
#endif

@ -1 +1 @@
Subproject commit 0a538553330ea5fdb1673708704bb92a854241b9
Subproject commit 3e39c2ec34340e245a5b7e353560b5a793b9990b

@ -1 +1 @@
Subproject commit 88c618c1f8d8089f94e78537e49e0d77798fc0be
Subproject commit c648adae38ed8d0342c02ccf3a6063e99d746752

View File

@ -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

View File

@ -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,15 +668,34 @@ 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;
// @recomp_event recomp_on_moon_crash(SramContext* sramCtx): A moon crash has just been triggered.
recomp_on_moon_crash(sramCtx);
if (moon_crash_resets_save)
{
bzero(sramCtx->saveBuf, SAVE_BUFFER_SIZE);
if (SysFlashrom_ReadData(sramCtx->saveBuf, gFlashSaveStartPages[gSaveContext.fileNum * 2],
@ -676,6 +710,7 @@ RECOMP_PATCH void Sram_ResetSaveFromMoonCrash(SramContext* sramCtx) {
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();
// @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;
}

16
patches/custom_syms.toml Normal file
View File

@ -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 },
]

View File

@ -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,16 +623,33 @@ RECOMP_PATCH void Interface_UpdateButtonsPart1(PlayState* play) {
BUTTON_STATUS(EQUIP_SLOT_C_RIGHT) = BTN_DISABLED;
set_extra_item_slot_status(BTN_DISABLED);
} else {
// @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) {
// @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);
}
// @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;
@ -620,6 +657,11 @@ RECOMP_PATCH void Interface_UpdateButtonsPart1(PlayState* play) {
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);
@ -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 {
// @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) {
} 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) {
// @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;
restoreHudVisibility = true;
}
// @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;
}
}
// @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) {

View File

@ -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;

View File

@ -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

View File

@ -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) }

View File

@ -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);
}

View File

@ -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);

View File

@ -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;

View File

@ -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);
}

View File

@ -13,13 +13,14 @@
#elif defined(__linux__)
#include <unistd.h>
#include <pwd.h>
#elif defined(__APPLE__)
#include "common/rt64_apple.h"
#endif
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;
@ -146,8 +147,8 @@ std::filesystem::path zelda64::get_app_folder_path() {
}
CoTaskMemFree(known_path);
#elif defined(__linux__)
// check for APP_FOLDER_PATH env var used by AppImage
#elif defined(__linux__) || defined(__APPLE__)
// check for APP_FOLDER_PATH env var
if (getenv("APP_FOLDER_PATH") != nullptr) {
return std::filesystem::path{getenv("APP_FOLDER_PATH")};
}
@ -155,7 +156,11 @@ std::filesystem::path zelda64::get_app_folder_path() {
const char *homedir;
if ((homedir = getenv("HOME")) == nullptr) {
#if defined(__linux__)
homedir = getpwuid(getuid())->pw_dir;
#elif defined(__APPLE__)
homedir = GetHomeDirectory();
#endif
}
if (homedir != nullptr) {

View File

@ -25,8 +25,10 @@
#include "zelda_config.h"
#include "zelda_sound.h"
#include "zelda_render.h"
#include "zelda_support.h"
#include "ovl_patches.hpp"
#include "librecomp/game.hpp"
#include "librecomp/mods.hpp"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
@ -36,12 +38,14 @@
#include "../../lib/rt64/src/contrib/stb/stb_image.h"
const std::string version_string = "1.2.0-dev";
template<typename... Ts>
void exit_error(const char* str, Ts ...args) {
// TODO pop up an error
((void)fprintf(stderr, str, args), ...);
assert(false);
std::quick_exit(EXIT_FAILURE);
ULTRAMODERN_QUICK_EXIT();
}
ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
@ -115,7 +119,12 @@ 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 window_flags = SDL_WINDOW_RESIZABLE;
#ifdef __APPLE__
window_flags |= SDL_WINDOW_METAL;
#endif
window = SDL_CreateWindow("Zelda 64: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, window_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
@ -143,6 +152,9 @@ ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::
}
return ultramodern::renderer::WindowHandle{ wmInfo.info.x11.display, wmInfo.info.x11.window };
#elif defined(__APPLE__)
SDL_MetalView view = SDL_Metal_CreateView(window);
return ultramodern::renderer::WindowHandle{ wmInfo.info.cocoa.window, SDL_Metal_GetLayer(view) };
#else
static_assert(false && "Unimplemented");
#endif
@ -327,6 +339,7 @@ std::vector<recomp::GameEntry> supported_games = {
.rom_hash = 0xEF18B4A9E2386169ULL,
.internal_name = "ZELDA MAJORA'S MASK",
.game_id = u8"mm.n64.us.1.0",
.mod_game_id = "mm",
.is_enabled = true,
.entrypoint_address = get_entrypoint_address(),
.entrypoint = recomp_entrypoint,
@ -524,7 +537,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;
@ -563,11 +592,18 @@ int main(int argc, char** argv) {
SDL_InitSubSystem(SDL_INIT_AUDIO);
reset_audio(48000);
// Source controller mappings file
if (SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt") < 0) {
#if defined(__APPLE__)
const char* resource_directory = zelda64::get_bundle_resource_directory();
std::string mapping_file_path = std::string(resource_directory) + "gamecontrollerdb.txt";
#else
std::string mapping_file_path = "gamecontrollerdb.txt";
#endif
if (SDL_GameControllerAddMappingsFromFile(mapping_file_path.c_str()) < 0) {
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 +611,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 +653,48 @@ 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(
64 * 1024 * 1024, // 64MB to have plenty of room for loading mods
project_version,
{},
rsp_callbacks,
renderer_callbacks,

View File

@ -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);
}

View File

@ -1,5 +1,6 @@
#include <memory>
#include <cstring>
#include <variant>
#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<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
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<TexturePackEnableAction, TexturePackDisableAction>;
static moodycamel::ConcurrentQueue<TexturePackAction> texture_pack_action_queue;
unsigned int MI_INTR_REG = 0;
unsigned int DPC_START_REG = 0;
@ -187,7 +207,7 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
appCore.window.window = window_handle.window;
#elif defined(__APPLE__)
appCore.window.window = window_handle.window;
appCore.window.view = window_handle.view;
appCore.window.layer = window_handle.view;
#endif
appCore.checkInterrupts = dummy_check_interrupts;
@ -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);
@ -376,3 +422,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});
}

10
src/main/support.cpp Normal file
View File

@ -0,0 +1,10 @@
#ifndef __APPLE__
#include "zelda_support.h"
#include <functional>
void zelda64::dispatch_on_main_thread(std::function<void()> func) {
func();
}
#endif

13
src/main/support_apple.mm Normal file
View File

@ -0,0 +1,13 @@
#include "zelda_support.h"
#import <Foundation/Foundation.h>
void zelda64::dispatch_on_main_thread(std::function<void()> func) {
dispatch_async(dispatch_get_main_queue(), ^{
func();
});
}
const char* zelda64::get_bundle_resource_directory() {
NSString *bundlePath = [[NSBundle mainBundle] resourcePath];
return strdup([bundlePath UTF8String]);
}

View File

@ -2,6 +2,7 @@
#include "recomp_input.h"
#include "zelda_sound.h"
#include "zelda_config.h"
#include "zelda_support.h"
#include "zelda_debug.h"
#include "zelda_render.h"
#include "promptfont.h"
@ -519,6 +520,11 @@ public:
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
#if defined(__APPLE__)
const Rml::String asset = "/assets/config_menu.rml";
return context->LoadDocument(zelda64::get_bundle_resource_directory() + asset);
#endif
return context->LoadDocument("assets/config_menu.rml");
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
@ -725,7 +731,7 @@ public:
throw std::runtime_error("Failed to make RmlUi data model for the controls config menu");
}
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = recomp::get_num_inputs(); } );
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = static_cast<uint64_t>(recomp::get_num_inputs()); } );
constructor.BindFunc("input_device_is_keyboard", [](Rml::Variant& out) { out = cur_device == recomp::InputDevice::Keyboard; } );
constructor.RegisterTransformFunc("get_input_name", [](const Rml::VariantList& inputs) {

View File

@ -1,12 +1,13 @@
#include "recomp_ui.h"
#include "zelda_config.h"
#include "zelda_support.h"
#include "librecomp/game.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
#include "nfd.h"
#include <filesystem>
std::string version_number = "v1.1.1";
static std::string version_string;
Rml::DataModelHandle model_handle;
bool mm_rom_valid = false;
@ -15,6 +16,7 @@ extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
zelda64::dispatch_on_main_thread([&native_path] {
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
if (result == NFD_OKAY) {
@ -50,6 +52,7 @@ void select_rom() {
break;
}
}
});
}
class LauncherMenu : public recompui::MenuController {
@ -61,6 +64,11 @@ public:
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
#if defined(__APPLE__)
const Rml::String asset = "/assets/launcher.rml";
return context->LoadDocument(zelda64::get_bundle_resource_directory() + asset);
#endif
return context->LoadDocument("assets/launcher.rml");
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
@ -103,7 +111,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();
}

View File

@ -16,6 +16,7 @@
#include "librecomp/game.hpp"
#include "zelda_config.h"
#include "ui_rml_hacks.hpp"
#include "zelda_support.h"
#include "concurrentqueue.h"
@ -1144,8 +1145,6 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
Rml::Debugger::Initialise(ui_context->rml.context);
{
const Rml::String directory = "assets/";
struct FontFace {
const char* filename;
bool fallback_face;
@ -1162,7 +1161,13 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
};
for (const FontFace& face : font_faces) {
#if defined(__APPLE__)
const Rml::String directory = "/assets/";
Rml::LoadFontFace(zelda64::get_bundle_resource_directory() + directory + face.filename, face.fallback_face);
#else
const Rml::String directory = "assets/";
Rml::LoadFontFace(directory + face.filename, face.fallback_face);
#endif
}
}
@ -1287,6 +1292,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;
@ -1500,6 +1511,9 @@ recompui::Menu recompui::get_current_menu() {
}
void recompui::message_box(const char* msg) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, zelda64::program_name.data(), msg, nullptr);
std::string message(msg);
zelda64::dispatch_on_main_thread([message] {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, zelda64::program_name.data(), message.c_str(), nullptr);
});
printf("[ERROR] %s\n", msg);
}

View File

@ -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"