diff --git a/.clang-format b/.clang-format index 1428d3c71e..8ffd4ebe96 100644 --- a/.clang-format +++ b/.clang-format @@ -2,7 +2,7 @@ Language: Cpp Standard: C++03 AccessModifierOffset: -4 -AlignAfterOpenBracket: Align +AlignAfterOpenBracket: DontAlign AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignOperands: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 37679e3f56..7ccde425c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: pull_request: env: - # SCCACHE_GHA_ENABLED: "true" + SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" SENTRY_DSN: ${{ secrets.SENTRY_DSN }} @@ -22,7 +22,7 @@ jobs: matrix: include: - name: GCC x86_64 - runner: [self-hosted, Linux] + runner: ubuntu-latest preset: gcc artifact_arch: x86_64 # - name: GCC aarch64 @@ -41,7 +41,6 @@ jobs: submodules: recursive - name: Install dependencies - if: 'false' # disabled for self-hosted run: | sudo apt-get update sudo apt-get -y install ninja-build clang lld openssl libcurl4-openssl-dev \ @@ -51,8 +50,7 @@ jobs: libxss-dev libfuse2 libusb-1.0-0-dev libdecor-0-dev libpipewire-0.3-dev libunwind-dev - name: Setup sccache - if: 'false' # disabled for self-hosted - uses: mozilla-actions/sccache-action@v0.0.9 + uses: mozilla-actions/sccache-action@v0.0.10 - name: Print sccache stats run: sccache --show-stats @@ -67,7 +65,6 @@ jobs: run: ci/build-appimage.sh - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}} @@ -77,7 +74,7 @@ jobs: build-apple: name: Build Apple (${{matrix.name}}) - runs-on: [self-hosted, macOS] + runs-on: macos-latest strategy: fail-fast: false matrix: @@ -86,14 +83,14 @@ jobs: platform: macos preset: x-macos-ci-arm64 artifact_name: macos-appleclang-arm64 - # - name: AppleClang macOS x86_64 - # platform: macos - # preset: x-macos-ci-x86_64 - # artifact_name: macos-appleclang-x86_64 - # - name: AppleClang iOS arm64 - # platform: ios - # preset: x-ios-ci - # artifact_name: ios-appleclang-arm64 + - name: AppleClang macOS x86_64 + platform: macos + preset: x-macos-ci-x86_64 + artifact_name: macos-appleclang-x86_64 + - name: AppleClang iOS arm64 + platform: ios + preset: x-ios-ci + artifact_name: ios-appleclang-arm64 # - name: AppleClang tvOS arm64 # platform: tvos # preset: x-tvos-ci @@ -106,7 +103,6 @@ jobs: submodules: recursive - name: Install dependencies - if: 'false' run: brew install cmake ninja - name: Install Rust iOS target @@ -128,7 +124,7 @@ jobs: rustup target add x86_64-apple-darwin - name: Setup sccache - uses: mozilla-actions/sccache-action@v0.0.9 + uses: mozilla-actions/sccache-action@v0.0.10 - name: Configure CMake run: cmake --preset ${{matrix.preset}} @@ -137,7 +133,6 @@ jobs: run: cmake --build --preset ${{matrix.preset}} - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}} @@ -145,6 +140,73 @@ jobs: build/install/Dusk.app build/install/debug.tar.* + build-android: + name: Build Android (${{matrix.name}}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - name: Clang arm64-v8a + preset: x-android-ci-arm64 + abi: arm64-v8a + artifact_arch: arm64 + rust_target: aarch64-linux-android + + env: + ANDROID_NDK_VERSION: "29.0.14206865" + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get -y install ninja-build + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Setup Android SDK + uses: android-actions/setup-android@v4 + + - name: Install Android SDK packages + run: sdkmanager "platforms;android-36" "build-tools;36.1.0" "ndk;${ANDROID_NDK_VERSION}" + + - name: Install Rust Android target + run: | + rustup toolchain install stable + rustup target add ${{matrix.rust_target}} + + - name: Setup sccache + uses: mozilla-actions/sccache-action@v0.0.10 + + - name: Configure CMake + run: cmake --preset ${{matrix.preset}} + + - name: Build native library + run: cmake --build --preset ${{matrix.preset}} --target dusk + + - name: Stage stripped JNI library + run: ANDROID_STAGE_ABIS="${{matrix.abi}}" platforms/android/scripts/stage-jni-libs.sh + + - name: Build APK + working-directory: platforms/android + run: ./gradlew :app:assembleRelease --rerun-tasks + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: dusk-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}} + path: platforms/android/app/build/outputs/apk/release/app-${{matrix.abi}}-release-unsigned.apk + build-windows: name: Build Windows (${{matrix.name}}) runs-on: ${{matrix.runner}} @@ -154,7 +216,7 @@ jobs: matrix: include: - name: MSVC x86_64 - runner: [self-hosted, Windows] + runner: windows-latest preset: msvc msvc_arch: amd64 vcpkg_arch: x64 @@ -191,7 +253,6 @@ jobs: uses: mozilla-actions/sccache-action@v0.0.9 - name: Install dependencies - if: 'false' # disabled for self-hosted run: | choco install ninja vcpkg install freetype:${{matrix.vcpkg_arch}}-windows-static zstd:${{matrix.vcpkg_arch}}-windows-static @@ -203,7 +264,6 @@ jobs: run: cmake --build --preset x-windows-ci-${{matrix.preset}} - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}} diff --git a/CMakeLists.txt b/CMakeLists.txt index 9004ab346d..f620de430b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,13 +48,15 @@ else () message(STATUS "Unable to find git, commit information will not be available") endif () -if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+).*)?$") +if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$") set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") - if (CMAKE_MATCH_5) - set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${CMAKE_MATCH_5}") - else () - set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.0") + set(DUSK_VERSION_TWEAK "0") + if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$") + set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}") + elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$") + set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}") endif () + set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}") else () set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION") set(DUSK_VERSION_STRING "0.0.0.0") @@ -69,7 +71,7 @@ message(STATUS "Dusk version set to ${DUSK_WC_DESCRIBE}") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") project(dusk LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING}) if (APPLE) - enable_language(OBJC) + enable_language(OBJC OBJCXX) endif () if (APPLE AND NOT TVOS AND CMAKE_SYSTEM_NAME STREQUAL tvOS) # ios.toolchain.cmake hack for SDL @@ -100,26 +102,35 @@ if (CMAKE_SYSTEM_NAME STREQUAL Linux) endif () set(AURORA_ENABLE_DVD ON CACHE BOOL "Enable DVD API support" FORCE) set(AURORA_ENABLE_CARD ON CACHE BOOL "Enable CARD API support" FORCE) +set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE) add_subdirectory(extern/aurora EXCLUDE_FROM_ALL) +target_compile_definitions(aurora_mtx PRIVATE MTX_USE_PS=1) add_subdirectory(libs/freeverb) option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)") option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.") option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON) +option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON) if(ANDROID) set(DUSK_MOVIE_SUPPORT OFF) - set(NOD_COMPRESS_BZIP2 OFF CACHE BOOL "" FORCE) - set(NOD_COMPRESS_LZMA OFF CACHE BOOL "" FORCE) - set(NOD_COMPRESS_ZLIB OFF CACHE BOOL "" FORCE) - set(NOD_COMPRESS_ZSTD OFF CACHE BOOL "" FORCE) endif () option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF) set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN") set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment") +# Edit & Continue +if (MSVC) + if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue") + endif () + if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue") + add_link_options("/INCREMENTAL") + endif () +endif () + if (DUSK_MOVIE_SUPPORT) find_package(libjpeg-turbo 3.0 CONFIG QUIET) if (libjpeg-turbo_FOUND) @@ -149,6 +160,7 @@ if (DUSK_MOVIE_SUPPORT) CMAKE_C_COMPILER_LAUNCHER CMAKE_MAKE_PROGRAM CMAKE_MSVC_RUNTIME_LIBRARY + CMAKE_MSVC_DEBUG_INFORMATION_FORMAT CMAKE_OSX_ARCHITECTURES DEPLOYMENT_TARGET ENABLE_ARC @@ -309,22 +321,16 @@ set(DUSK_COMPANY_NAME "Twilit Realm") set(DUSK_FILE_DESCRIPTION "Dusk") set(DUSK_PRODUCT_NAME "Dusk") set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors") -set(DUSK_GAME_NAME "GZ2E") -set(DUSK_GAME_VERSION "01") -set(DUSK_TP_VERSION ${DUSK_GAME_NAME}${DUSK_GAME_VERSION}) - -message(STATUS "dusk: Game Version: ${DUSK_TP_VERSION}") source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES}) -source_group("dusk" FILES ${DUSK_FILES}) +source_group("dusk" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES}) -set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 - DUSK_TP_VERSION="${DUSK_TP_VERSION}" DUSK_GAME_NAME="${DUSK_GAME_NAME}" DUSK_GAME_VERSION="${DUSK_GAME_VERSION}") +set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1) set(GAME_INCLUDE_DIRS include src - assets/${DUSK_TP_VERSION} + assets/GZ2E01 # TODO: make this dynamic if needed? libs/JSystem/include libs extern/aurora/include/dolphin @@ -339,8 +345,10 @@ target_include_directories(dusk_game_headers INTERFACE ${GAME_INCLUDE_DIRS}) target_compile_definitions(dusk_game_headers INTERFACE TARGET_PC=1) target_link_libraries(dusk_game_headers INTERFACE TracyClient) +find_package(Threads REQUIRED) set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd - aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt funchook-static) + aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt funchook-static + Threads::Threads) list(APPEND GAME_LIBS libzstd_static) @@ -349,6 +357,45 @@ if (DUSK_ENABLE_SENTRY_NATIVE) list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_SENTRY_NATIVE=1 SENTRY_BUILD_STATIC=1) endif () +if (WIN32) + list(APPEND GAME_LIBS Ws2_32) +endif () + +set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/no_backend.cpp) +if (DUSK_ENABLE_UPDATE_CHECKER) + list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_UPDATE_CHECKER=1) + if (WIN32) + set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/winhttp.cpp) + list(APPEND GAME_LIBS winhttp) + list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_WINHTTP=1) + message(STATUS "dusk: Enabled update checker (WinHTTP)") + elseif (ANDROID) + set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/android.cpp) + list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_ANDROID=1) + message(STATUS "dusk: Enabled update checker (Android)") + elseif (APPLE) + find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED) + set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/url_session.mm) + set_source_files_properties(src/dusk/http/url_session.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) + list(APPEND GAME_LIBS ${FOUNDATION_FRAMEWORK}) + list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_URLSESSION=1) + message(STATUS "dusk: Enabled update checker (NSURLSession)") + elseif (CMAKE_SYSTEM_NAME STREQUAL Linux) + find_package(CURL QUIET OPTIONAL_COMPONENTS HTTPS SSL) + if (CURL_FOUND AND CURL_HTTPS_FOUND AND CURL_SSL_FOUND) + set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/curl.cpp) + list(APPEND GAME_LIBS CURL::libcurl) + list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_LIBCURL=1) + message(STATUS "dusk: Enabled update checker (libcurl)") + else () + message(STATUS "dusk: Disabled update checker (libcurl + HTTPS/SSL not found)") + endif () + else () + message(STATUS "dusk: Disabled update checker (unsupported platform)") + endif () +endif () +list(APPEND DUSK_FILES ${DUSK_HTTP_BACKEND_SOURCE}) + if (DUSK_MOVIE_SUPPORT) if (TARGET libjpeg-turbo::turbojpeg-static) list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static) @@ -358,56 +405,13 @@ if (DUSK_MOVIE_SUPPORT) list(APPEND GAME_COMPILE_DEFS MOVIE_SUPPORT=1) endif () -option(DUSK_ENABLE_DISCORD_RPC "Enable Discord Rich Presence support" ON) -if (DUSK_ENABLE_DISCORD_RPC AND NOT ANDROID AND NOT IOS AND NOT TVOS) - - FetchContent_Populate(discord_rpc - URL https://github.com/discord/discord-rpc/archive/refs/tags/v3.4.0.tar.gz - URL_HASH SHA256=e13427019027acd187352dacba6c65953af66fdf3c35fcf38fc40b454a9d7855 - DOWNLOAD_EXTRACT_TIMESTAMP TRUE - ) - # RapidJSON is a git submodule absent from the discord-rpc tarball; fetch separately. - FetchContent_Populate(rapidjson - URL https://github.com/Tencent/rapidjson/archive/refs/tags/v1.1.0.tar.gz - URL_HASH SHA256=bf7ced29704a1e696fbccf2a2b4ea068e7774fa37f6d7dd4039d0787f8bed98e - DOWNLOAD_EXTRACT_TIMESTAMP TRUE - ) - - if (NOT TARGET discord-rpc) - set(_drpc ${discord_rpc_SOURCE_DIR}/src) - set(_drpc_src - ${_drpc}/discord_rpc.cpp - ${_drpc}/rpc_connection.cpp - ${_drpc}/serialization.cpp - ) - if (WIN32) - list(APPEND _drpc_src ${_drpc}/connection_win.cpp ${_drpc}/discord_register_win.cpp) - elseif (APPLE) - list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_osx.m) - else () - list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_linux.cpp) - endif () - add_library(discord-rpc STATIC ${_drpc_src}) - target_include_directories(discord-rpc PUBLIC - ${discord_rpc_SOURCE_DIR}/include - ${rapidjson_SOURCE_DIR}/include - ) - if (UNIX) - target_link_libraries(discord-rpc PUBLIC pthread) - endif () - endif () - list(APPEND GAME_LIBS discord-rpc) - list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD_RPC=1) +set(DUSK_ENABLE_DISCORD_DEFAULT ON) +if (DEFINED DUSK_ENABLE_DISCORD_RPC AND NOT DEFINED DUSK_ENABLE_DISCORD) + set(DUSK_ENABLE_DISCORD_DEFAULT ${DUSK_ENABLE_DISCORD_RPC}) endif () - -# Edit & Continue -if (MSVC) - if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug") - set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue") - endif () - if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue") - add_link_options("/INCREMENTAL") - endif () +option(DUSK_ENABLE_DISCORD "Enable Discord Rich Presence support" ${DUSK_ENABLE_DISCORD_DEFAULT}) +if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS) + list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1) endif () if(ANDROID) diff --git a/CMakePresets.json b/CMakePresets.json index 92799249a9..629bbe9c18 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -352,6 +352,31 @@ "ANDROID_ABI": "x86_64" } }, + { + "name": "x-android-ci", + "hidden": true, + "inherits": [ + "android-base", + "ci" + ], + "cacheVariables": { + "DUSK_ENABLE_SENTRY_NATIVE": { + "type": "BOOL", + "value": false + } + } + }, + { + "name": "x-android-ci-arm64", + "binaryDir": "${sourceDir}/build/android-arm64", + "inherits": [ + "x-android-ci" + ], + "cacheVariables": { + "ANDROID_ABI": "arm64-v8a", + "Rust_CARGO_TARGET": "aarch64-linux-android" + } + }, { "name": "x-linux-ci", "hidden": true, @@ -412,6 +437,7 @@ "x-macos-ci" ], "cacheVariables": { + "AURORA_DAWN_PROVIDER": "vendor", "CMAKE_OSX_ARCHITECTURES": "x86_64", "Rust_CARGO_TARGET": "x86_64-apple-darwin" } @@ -555,6 +581,15 @@ "dusk" ] }, + { + "name": "x-android-ci-arm64", + "configurePreset": "x-android-ci-arm64", + "description": "(Internal) Android CI arm64-v8a", + "displayName": "(Internal) Android CI arm64-v8a", + "targets": [ + "dusk" + ] + }, { "name": "windows-msvc-debug", "configurePreset": "windows-msvc-debug", diff --git a/README.md b/README.md index c2fe53576d..652eae2538 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,69 @@ -![DuskLogo](res/logo-mascot.webp) +
+ Logo -- ### **[Official Website](https://twilitrealm.dev)** -- ### **[Discord](https://discord.gg/QACynxeyna)** +

+ Official Website + • + Discord +

+
+ +# Overview + +Dusk is a reverse-engineered reimplementation of Twilight Princess. + +It aims to be as accurate as possible to the original while also providing new options, enhancements, and tools to customize your experience. # Setup -**⚠️Dusk does NOT provide any copyrighted assets. You must provide your own copy of the game.** -### 1. Verify your ROM dump -First make sure your dump of the game is clean and supported by Dusk. You can do this by checking the sha1 hash of your dump against this list of supported versions. +> [!IMPORTANT] +> Dusk does *not* provide any copyrighted assets. You must provide your own copy of the original game. -| Version | sha1 hash | -|--------------| ---------------------------------------- | -| GameCube USA | 75edd3ddff41f125d1b4ce1a40378f1b565519e7 | -| GameCube PAL | 2601822a488eeb86fb89db16ca8f29c2c953e1ca | +> [!IMPORTANT] +> At a minimum, Dusk requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelyhood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them. + +### 1. Verify your dump + +First, make sure your dump of the game is clean and supported by Dusk. You can do this by checking the SHA-1 hash of your dump against this list of supported versions: + +| Version | SHA-1 hash | +|--------------| ------------------------------------------ | +| GameCube USA | `75edd3ddff41f125d1b4ce1a40378f1b565519e7` | +| GameCube EUR | `2601822a488eeb86fb89db16ca8f29c2c953e1ca` | + +*Support for other versions of the game is planned in the future. ### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases) ### 3. Setup the game -- Extract the zip folder +**Windows / macOS / Linux** +- Extract the .zip file - Launch Dusk -- Select Options, then set the ISO Path to your supported game dump -- Press Start Game to play! +- Press **Select Disc Image** and provide the path to your supported game dump +- Press **Play**! -![DuskOptions](assets/dusk_options.png) +**iOS** +- Follow the [iOS setup guide](docs/ios-install-altstore.md) + +**Android** +- Install the Dusk apk +- Launch Dusk +- Press **Select Disc Image** and provide the path to your supported game dump +- Press **Play**! # Building + If you'd like to build Dusk from source, please read the [build instructions](docs/building.md). -# Credits -- Taka -- encounter -- Antidote -- caseif -- CraftyBoss -- crowell -- dooplecks -- gymnast86 -- Irastris -- kipcode66 -- Lars -- LunarSoap -- Maddie -- MelonSpeedruns -- Pheenoh -- PJB -- Roeming -- YunataSavior +Pull requests are welcomed! Note that we do not accept contributions that are primarily AI-generated and will close your PR if we suspect as much. Please also see the [code conventions](docs/code-conventions.md). -Special thanks to the TP Decomp team, the GC/Wii Decomp community, the Aurora developers, and the TP speedrunning community. +# Credits + +Special thanks to the [TP decompilation](https://github.com/zeldaret/tp) team, the GC/Wii decompilation community, the [Aurora](https://github.com/encounter/aurora) developers, the [TP speedrunning community](https://zsrtp.link), and all [contributors](https://github.com/TwilitRealm/dusk/graphs/contributors). + +
+
+ + Powered by Aurora + +
diff --git a/assets/aurora-powered.png b/assets/aurora-powered.png new file mode 100644 index 0000000000..35ff017776 Binary files /dev/null and b/assets/aurora-powered.png differ diff --git a/assets/dusk_options.png b/assets/dusk_options.png deleted file mode 100644 index 4ed4ad7563..0000000000 Binary files a/assets/dusk_options.png and /dev/null differ diff --git a/assets/objdiff.png b/assets/objdiff.png deleted file mode 100644 index 94835fa1b3..0000000000 Binary files a/assets/objdiff.png and /dev/null differ diff --git a/assets/org-icon.svg b/assets/org-icon.svg new file mode 100644 index 0000000000..b1e82ead45 --- /dev/null +++ b/assets/org-icon.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ci/build-appimage.sh b/ci/build-appimage.sh index 078d6cabf0..cef4ddfaf0 100755 --- a/ci/build-appimage.sh +++ b/ci/build-appimage.sh @@ -1,18 +1,26 @@ #!/bin/bash -ex -shopt -s extglob + +if [[ -n "${GITHUB_WORKSPACE:-}" ]]; then + cd "$GITHUB_WORKSPACE" +fi + +build_dir="$PWD/build" +linuxdeploy="$build_dir/linuxdeploy-$(uname -m).AppImage" # Get linuxdeploy -cd "$RUNNER_WORKSPACE" -curl -fOL https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$(uname -m).AppImage -chmod +x linuxdeploy-$(uname -m).AppImage +mkdir -p "$build_dir" +curl -fL "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$(uname -m).AppImage" -o "$linuxdeploy" +chmod +x "$linuxdeploy" # Build AppImage -cd "$GITHUB_WORKSPACE" mkdir -p build/appdir/usr/{bin,share/{applications,icons/hicolor}} -cp -r build/install/!(*.*) build/appdir/usr/bin +for install_path in build/install/*; do + [[ "$(basename "$install_path")" == *.* ]] && continue + cp -r "$install_path" build/appdir/usr/bin +done cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,1024x1024} build/appdir/usr/share/icons/hicolor cp platforms/freedesktop/dusk.desktop build/appdir/usr/share/applications cd build/install -VERSION="$DUSK_VERSION" NO_STRIP=1 "$RUNNER_WORKSPACE"/linuxdeploy-$(uname -m).AppImage \ - -l /usr/lib/x86_64-linux-gnu/libusb-1.0.so --appdir "$GITHUB_WORKSPACE"/build/appdir --output appimage +VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \ + -l /usr/lib/x86_64-linux-gnu/libusb-1.0.so --appdir "$build_dir/appdir" --output appimage diff --git a/cmake-variants.yaml b/cmake-variants.yaml index 62d2642311..37edcede95 100644 --- a/cmake-variants.yaml +++ b/cmake-variants.yaml @@ -13,13 +13,3 @@ buildType: short: RelWithDebInfo long: Optimized, with debug symbols buildType: RelWithDebInfo - -tp_version: - default: GZ2E01 - description: TP Version - choices: - GZ2E01: - short: GZ2E01 - long: GZ2E01 - settings: - DUSK_TP_VERSION: GZ2E01 diff --git a/docs/code-conventions.md b/docs/code-conventions.md new file mode 100644 index 0000000000..9c2659ac02 --- /dev/null +++ b/docs/code-conventions.md @@ -0,0 +1,13 @@ +# Code conventions for Dusk + +## Upstream when appropriate + +Bug fixes, documentation improvements, code cleanup, etc that also apply to the [original decompilation project](https://github.com/zeldaret/tp) should preferably be PR'd there. + +## Properly indicate Dusk-modified code + +When modifying original game code (i.e. in decomp) for Dusk's purposes, please clearly delineate such code as being Dusk-specific. Generally, this can be done by using `#if TARGET_PC` and keeping the original code in place. Use `#if AVOID_UB` for Undefined Behavior fixes to the original codebase. + +## Miscellaneous things + +* The original codebase makes heavy use of global `operator new` and similar overloads to allocate into a strict tree of heaps. This would cause many linkage headaches for us, so effectively all uses of `new` or `delete` in the original game code have been replaced with `JKR_NEW`, `JKR_DELETE`, or similar macros. See `JKRHeap.h` for the full list. diff --git a/docs/ios-install-altstore.md b/docs/ios-install-altstore.md new file mode 100644 index 0000000000..fdfc7689d6 --- /dev/null +++ b/docs/ios-install-altstore.md @@ -0,0 +1,46 @@ +# Installing Dusk on iOS via AltStore + +## Prerequisites + +- Mac with Homebrew installed +- iPhone connected via USB +- Dusk IPA file (download the latest `Dusk-vX.X.X-ios-arm64.ipa` from the [releases page](https://github.com/TwilitRealm/dusk/releases)) +- Game disc - `GZ2E01` (Gamecube USA) or `GZ2PE01` (Gamecube PAL) + +## 1. Install AltServer + +```sh +brew install altserver +open -a AltServer +``` + +AltServer will appear in your menu bar. + +## 2. Enable Developer Mode (iOS 16+) + +- On your iPhone, go to **Settings > Privacy & Security > Developer Mode** +- Toggle it on and restart when prompted + +## 3. Install AltStore on Your iPhone + +- Click AltServer in the menu bar +- Click **Install AltStore > [Your iPhone]** +- Enter your Apple ID credentials when prompted +- On your iPhone, go to **Settings > General > VPN & Device Management** +- Tap your Apple ID under "Developer App" and tap **Trust** + +## 4. Copy Files to Your iPhone + +Transfer the IPA and game disc to your iPhone so they're accessible in the Files app. A few ways to do this: + +- **AirDrop** - Right-click the files on your Mac and choose Share > AirDrop +- **iCloud Drive** - Place files in iCloud Drive on your Mac and they'll sync to Files on your iPhone +- **USB transfer** - Connect your iPhone and drag files via Finder's sidebar +- **Cloud storage** - Upload to Google Drive, Dropbox, etc. and download on your iPhone + +## 5. Install via AltStore + +- Open **AltStore** on your iPhone +- Go to the **My Apps** tab +- Tap the **+** button (top left) +- Open the **Files** app and select the `.ipa` file \ No newline at end of file diff --git a/extern/aurora b/extern/aurora index be1395c134..aff6edf9de 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit be1395c1343a5587cceea25754daa5cacce34f76 +Subproject commit aff6edf9de39b3c211ccb252e76331abaa9e5e01 diff --git a/files.cmake b/files.cmake index 5d6850cfca..b141291e7b 100644 --- a/files.cmake +++ b/files.cmake @@ -1,7 +1,7 @@ set(DOLZEL_FILES src/m_Do/m_Do_main.cpp - src/m_Do/m_Do_printf.cpp + #src/m_Do/m_Do_printf.cpp src/m_Do/m_Do_audio.cpp src/m_Do/m_Do_controller_pad.cpp #src/m_Do/m_Re_controller_pad.cpp @@ -1429,11 +1429,15 @@ set(DUSK_FILES src/dusk/globals.cpp src/dusk/gyro.cpp src/dusk/gamepad_color.cpp + src/dusk/autosave.cpp + src/dusk/http/http.hpp src/dusk/io.cpp src/dusk/layout.cpp src/dusk/logging.cpp src/dusk/settings.cpp src/dusk/stubs.cpp + src/dusk/update_check.cpp + src/dusk/update_check.hpp #src/dusk/m_Do_ext_dusk.cpp src/dusk/imgui/ImGuiConfig.hpp src/dusk/imgui/ImGuiConsole.hpp @@ -1446,28 +1450,84 @@ set(DUSK_FILES src/dusk/imgui/ImGuiBloomWindow.hpp src/dusk/imgui/ImGuiMenuTools.cpp src/dusk/imgui/ImGuiMenuTools.hpp - src/dusk/imgui/ImGuiPreLaunchWindow.cpp - src/dusk/imgui/ImGuiPreLaunchWindow.hpp - src/dusk/imgui/ImGuiFirstRunPreset.hpp - src/dusk/imgui/ImGuiFirstRunPreset.cpp + src/dusk/imgui/ImGuiActorSpawner.cpp src/dusk/imgui/ImGuiProcessOverlay.cpp src/dusk/imgui/ImGuiCameraOverlay.cpp src/dusk/imgui/ImGuiHeapOverlay.cpp - src/dusk/imgui/ImGuiDebugPad.cpp src/dusk/imgui/ImGuiControllerOverlay.cpp src/dusk/imgui/ImGuiStubLog.cpp src/dusk/imgui/ImGuiMapLoader.cpp src/dusk/imgui/ImGuiSaveEditor.cpp src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStateShare.cpp + src/dusk/ui/achievements.cpp + src/dusk/ui/achievements.hpp + src/dusk/ui/bool_button.cpp + src/dusk/ui/bool_button.hpp + src/dusk/ui/button.cpp + src/dusk/ui/button.hpp + src/dusk/ui/component.cpp + src/dusk/ui/component.hpp + src/dusk/ui/controller_config.cpp + src/dusk/ui/controller_config.hpp + src/dusk/ui/document.cpp + src/dusk/ui/document.hpp + src/dusk/ui/editor.cpp + src/dusk/ui/editor.hpp + src/dusk/ui/event.cpp + src/dusk/ui/event.hpp + src/dusk/ui/graphics_tuner.cpp + src/dusk/ui/graphics_tuner.hpp + src/dusk/ui/input.cpp + src/dusk/ui/input.hpp + src/dusk/ui/modal.cpp + src/dusk/ui/modal.hpp + src/dusk/ui/nav_types.hpp + src/dusk/ui/number_button.cpp + src/dusk/ui/number_button.hpp + src/dusk/ui/overlay.cpp + src/dusk/ui/overlay.hpp + src/dusk/ui/pane.cpp + src/dusk/ui/pane.hpp + src/dusk/ui/menu_bar.cpp + src/dusk/ui/menu_bar.hpp + src/dusk/ui/prelaunch.cpp + src/dusk/ui/prelaunch.hpp + src/dusk/ui/preset.cpp + src/dusk/ui/preset.hpp + src/dusk/ui/select_button.cpp + src/dusk/ui/select_button.hpp + src/dusk/ui/settings.cpp + src/dusk/ui/settings.hpp + src/dusk/ui/string_button.cpp + src/dusk/ui/string_button.hpp + src/dusk/ui/tab_bar.cpp + src/dusk/ui/tab_bar.hpp + src/dusk/ui/ui.cpp + src/dusk/ui/ui.hpp + src/dusk/ui/window.cpp + src/dusk/ui/window.hpp + src/dusk/achievements.cpp src/dusk/iso_validate.cpp + src/dusk/livesplit.cpp src/dusk/offset_ptr.cpp src/dusk/OSContext.cpp + src/dusk/OSReport.cpp src/dusk/OSThread.cpp src/dusk/OSMutex.cpp src/dusk/hook_system.cpp src/dusk/mod_loader.cpp src/dusk/imgui/ImGuiMenuMods.cpp src/dusk/gx_helper.cpp + src/dusk/discord.cpp + src/dusk/discord.hpp src/dusk/discord_presence.cpp + src/dusk/version.cpp +) + +set(DUSK_HTTP_BACKEND_FILES + src/dusk/http/no_backend.cpp + src/dusk/http/curl.cpp + src/dusk/http/winhttp.cpp + src/dusk/http/url_session.mm ) diff --git a/flake.nix b/flake.nix index cd703bda64..c8e38a849b 100644 --- a/flake.nix +++ b/flake.nix @@ -5,9 +5,73 @@ outputs = { self, nixpkgs }: let pkgs = import nixpkgs { system = "x86_64-linux"; }; + + # Dependencies that are not packaged in nixpkgs: + aurora-src = pkgs.fetchFromGitHub { + owner = "encounter"; + repo = "aurora"; + rev = "63606a43265a3bc18dafd500ab4d7a2108f109e6"; + hash = "sha256-xBvnAwGwNzav67Ac6oUz7RqDUwqgL2bsME3OOMn8Tqw="; + }; + dawn-src = pkgs.fetchzip { + url = "https://github.com/encounter/dawn-build/releases/download/v20260423.175430/dawn-linux-x86_64.tar.gz"; + hash = "sha256-HXfKTLHtMPwupnFnaflCARtXVPuS/0PoCePXidjE5xs="; + stripRoot = false; + }; + nod-src = pkgs.fetchzip { + url = "https://github.com/encounter/nod/releases/download/v2.0.0-alpha.8/libnod-linux-x86_64.tar.gz"; + hash = "sha256-mUqvLsbsqaZ+HAjMmHYPYO+MgtanGRTw7Gzn5uXR5rE="; + stripRoot = false; + }; + # The version of imgui on nixpkgs does not map cleanly. + imgui-src = pkgs.fetchFromGitHub { + owner = "ocornut"; + repo = "imgui"; + rev = "v1.91.9b-docking"; + hash = "sha256-mQOJ6jCN+7VopgZ61yzaCnt4R1QLrW7+47xxMhFRHLQ="; + }; + sqlite-src = pkgs.fetchzip { + url = "https://sqlite.org/2026/sqlite-amalgamation-3510300.zip"; + hash = "sha256-pNMR8zxaaqfAzQ0AQBOXMct4usdjey1Q0Gnitg06UhM="; + }; + rmlui-src = pkgs.fetchzip { + url = "https://github.com/mikke89/RmlUi/archive/f9b8c9e2935d5df2c7dff2c190d3968e99b0c3dc.tar.gz"; + hash = "sha256-g4O/JZUrrcseOz8o2QJRt+2CeuiLnVeuDJc906xvuIg="; + }; + # Dusk Actual dusk = pkgs.stdenv.mkDerivation { name = "dusk"; src = ./.; + postUnpack = '' + mkdir -p $sourceRoot/extern/aurora + cp -r ${aurora-src}/. $sourceRoot/extern/aurora/ + chmod -R u+w $sourceRoot/extern/aurora + sed -i '/add_subdirectory(tests)/d' $sourceRoot/extern/aurora/CMakeLists.txt + ''; + # Remove last line to re-enable tests + cmakeFlags = [ + "-DFETCHCONTENT_FULLY_DISCONNECTED=ON" + "-DFETCHCONTENT_SOURCE_DIR_CXXOPTS=${pkgs.cxxopts.src}" + "-DFETCHCONTENT_SOURCE_DIR_JSON=${pkgs.nlohmann_json.src}" + "-DFETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT=${dawn-src}" + "-DFETCHCONTENT_SOURCE_DIR_XXHASH=${pkgs.xxHash.src}" + "-DFETCHCONTENT_SOURCE_DIR_FMT=${pkgs.fmt.src}" + "-DFETCHCONTENT_SOURCE_DIR_TRACY=${pkgs.tracy.src}" + "-DAURORA_SDL3_PROVIDER=system" + "-DFETCHCONTENT_SOURCE_DIR_NOD_PREBUILT=${nod-src}" + "-DAURORA_NOD_PROVIDER=package" + "-DFETCHCONTENT_SOURCE_DIR_FREETYPE=${pkgs.freetype.src}" + "-DFETCHCONTENT_SOURCE_DIR_ZSTD=${pkgs.zstd.src}" + "-DFETCHCONTENT_SOURCE_DIR_SQLITE3=${sqlite-src}" + "-DFETCHCONTENT_SOURCE_DIR_IMGUI=${imgui-src}" + "-DFETCHCONTENT_SOURCE_DIR_RMLUI=${rmlui-src}" + "-DCMAKE_CROSSCOMPILING=ON" # Tests are not working as I didn't want to work through getting google's test suite working as well. This is the only guard I could find to disable it. + ]; + installPhase = '' + mkdir -p $out/bin + cp dusk $out/bin/dusk + cp -r ./res $out/bin/res + ''; nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config @@ -25,6 +89,13 @@ pkgs.libjpeg8 pkgs.libxkbcommon pkgs.libglvnd + pkgs.cxxopts + pkgs.abseil-cpp + pkgs.sdl3 + pkgs.fmt + pkgs.tracy + pkgs.freetype + pkgs.zstd ]; }; in { diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 9980ab776f..107afc1ed3 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -4552,6 +4552,18 @@ public: void handleWolfHowl(); void handleQuickTransform(); bool checkGyroAimContext(); + + void onIronBallChainInterpCallback(); + + static const int IRON_BALL_CHAIN_COUNT = 102; + cXyz mIBChainInterpPrevPos[IRON_BALL_CHAIN_COUNT]; + cXyz mIBChainInterpCurrPos[IRON_BALL_CHAIN_COUNT]; + csXyz mIBChainInterpPrevAngle[IRON_BALL_CHAIN_COUNT]; + csXyz mIBChainInterpCurrAngle[IRON_BALL_CHAIN_COUNT]; + cXyz mIBChainInterpPrevHandRoot; + cXyz mIBChainInterpCurrHandRoot; + bool mIBChainInterpPrevValid; + bool mIBChainInterpCurrValid; #endif }; // Size: 0x385C diff --git a/include/d/actor/d_a_b_gnd.h b/include/d/actor/d_a_b_gnd.h index b9c8781ec6..0827102eec 100644 --- a/include/d/actor/d_a_b_gnd.h +++ b/include/d/actor/d_a_b_gnd.h @@ -188,6 +188,15 @@ public: /* 0x273C */ f32 mKankyoBlend; /* 0x2740 */ u8 field_0x2740; /* 0x2744 */ dMsgFlow_c mMsgFlow; +#if TARGET_PC + cXyz mReinsInterpPrev[2][16]; + cXyz mReinsInterpCurr[2][16]; + cXyz mReinsTexInterpPrev[2]; + cXyz mReinsTexInterpCurr[2]; + bool mReinsInterpPrevValid; + bool mReinsInterpCurrValid; + s8 mDemoCamSyncTicks; +#endif }; STATIC_ASSERT(sizeof(b_gnd_class) == 0x2790); diff --git a/include/d/actor/d_a_e_db.h b/include/d/actor/d_a_e_db.h index a95722a6d1..4f10715443 100644 --- a/include/d/actor/d_a_e_db.h +++ b/include/d/actor/d_a_e_db.h @@ -80,6 +80,12 @@ public: /* 0x125C */ u32 field_0x125c; /* 0x1260 */ u8 field_0x1260[0x126C - 0x1260]; /* 0x126C */ u8 HIOInit; +#if TARGET_PC + cXyz mStalkLineInterpPrev[12]; + cXyz mStalkLineInterpCurr[12]; + bool mStalkLineInterpPrevValid; + bool mStalkLineInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_db_class) == 0x1270); diff --git a/include/d/actor/d_a_e_hb.h b/include/d/actor/d_a_e_hb.h index a7e0241007..3069bcd325 100644 --- a/include/d/actor/d_a_e_hb.h +++ b/include/d/actor/d_a_e_hb.h @@ -73,6 +73,12 @@ public: /* 0x124C */ f32 field_0x124c; /* 0x1250 */ u8 field_0x1250[0x1264 - 0x1250]; /* 0x1264 */ u8 HIOInit; +#if TARGET_PC + cXyz mStalkLineInterpPrev[12]; + cXyz mStalkLineInterpCurr[12]; + bool mStalkLineInterpPrevValid; + bool mStalkLineInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_hb_class) == 0x1268); diff --git a/include/d/actor/d_a_e_mb.h b/include/d/actor/d_a_e_mb.h index f36c744879..527cf30eac 100644 --- a/include/d/actor/d_a_e_mb.h +++ b/include/d/actor/d_a_e_mb.h @@ -44,6 +44,12 @@ public: /* 0x88C */ u8 field_0x88C[0x8C8 - 0x88C]; /* 0x8C8 */ s8 field_0x8c8; /* 0x8C9 */ u8 mInitHIO; +#if TARGET_PC + cXyz mRopeInterpPrev[16]; + cXyz mRopeInterpCurr[16]; + bool mRopeInterpPrevValid; + bool mRopeInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_mb_class) == 0x8cc); diff --git a/include/d/actor/d_a_e_s1.h b/include/d/actor/d_a_e_s1.h index a631ba47ac..5cbae84696 100644 --- a/include/d/actor/d_a_e_s1.h +++ b/include/d/actor/d_a_e_s1.h @@ -81,6 +81,15 @@ public: /* 0x306D */ u8 field_0x306D[0x307C - 0x306D]; /* 0x307C */ u32 mBodyEffEmtrID; /* 0x3080 */ u8 mInitHIO; + +#if TARGET_PC + static const int HAIR_STRAND_COUNT = 22; + static const int HAIR_SEGMENT_COUNT = 16; + cXyz mHairInterpPrev[HAIR_STRAND_COUNT * HAIR_SEGMENT_COUNT]; + cXyz mHairInterpCurr[HAIR_STRAND_COUNT * HAIR_SEGMENT_COUNT]; + bool mHairInterpPrevValid; + bool mHairInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_s1_class) == 0x3084); diff --git a/include/d/actor/d_a_e_yd.h b/include/d/actor/d_a_e_yd.h index 188e435ba5..44d6036600 100644 --- a/include/d/actor/d_a_e_yd.h +++ b/include/d/actor/d_a_e_yd.h @@ -74,6 +74,12 @@ public: /* 0x1250 */ f32 field_0x1250; /* 0x1254 */ u8 field_0x1254[0x1268 - 0x1254]; /* 0x1268 */ u8 field_0x1268; +#if TARGET_PC + cXyz mLineMatInterpPrev[12]; + cXyz mLineMatInterpCurr[12]; + bool mLineMatInterpPrevValid; + bool mLineMatInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_yd_class) == 0x126c); diff --git a/include/d/actor/d_a_e_yg.h b/include/d/actor/d_a_e_yg.h index 69a85430a8..6be19933cb 100644 --- a/include/d/actor/d_a_e_yg.h +++ b/include/d/actor/d_a_e_yg.h @@ -63,6 +63,15 @@ public: /* 0x0BB4 */ yg_ke_s mYgKes[13]; /* 0x1880 */ mDoExt_3DlineMat0_c mLineMat; /* 0x189C */ u8 mIsFirstSpawn; + +#if TARGET_PC + static const int TENTACLE_STRAND_COUNT = 13; + static const int TENTACLE_SEGMENT_COUNT = 10; + cXyz mTentacleInterpPrev[TENTACLE_STRAND_COUNT * TENTACLE_SEGMENT_COUNT]; + cXyz mTentacleInterpCurr[TENTACLE_STRAND_COUNT * TENTACLE_SEGMENT_COUNT]; + bool mTentacleInterpPrevValid; + bool mTentacleInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_yg_class) == 0x18a0); diff --git a/include/d/actor/d_a_e_yh.h b/include/d/actor/d_a_e_yh.h index 519e5e2779..63e8ac1882 100644 --- a/include/d/actor/d_a_e_yh.h +++ b/include/d/actor/d_a_e_yh.h @@ -77,6 +77,12 @@ public: /* 0x1260 */ u32 field_0x1260; /* 0x1260 */ u8 field_0x1264[0x1270 - 0x1264]; /* 0x1270 */ bool mIsHIOOwner; +#if TARGET_PC + cXyz mLineInterpPrev[12]; + cXyz mLineInterpCurr[12]; + bool mLineInterpPrevValid; + bool mLineInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_yh_class) == 0x1274); diff --git a/include/d/actor/d_a_mirror.h b/include/d/actor/d_a_mirror.h index 06c5603899..e2f9a51e30 100644 --- a/include/d/actor/d_a_mirror.h +++ b/include/d/actor/d_a_mirror.h @@ -27,6 +27,7 @@ public: /* 0x17C */ cXyz mViewScale; #if TARGET_PC bool mbReset = false; + bool mbHadEntry = false; #endif }; diff --git a/include/d/actor/d_a_obj_fchain.h b/include/d/actor/d_a_obj_fchain.h index 1bd7b9a810..7c4ae74faf 100644 --- a/include/d/actor/d_a_obj_fchain.h +++ b/include/d/actor/d_a_obj_fchain.h @@ -31,6 +31,10 @@ public: csXyz* getAngle() { return field_0x8a4; } J3DModelData* getModelData() { return mModelData; } +#if TARGET_PC + void onInterpCallback(); +#endif + private: /* 0x568 */ request_of_phase_process_class mPhase; /* 0x570 */ J3DModelData* mModelData; @@ -42,6 +46,14 @@ private: /* 0x694 */ cXyz field_0x694[22]; /* 0x79C */ cXyz field_0x79c[22]; /* 0x8A4 */ csXyz field_0x8a4[22]; + +#if TARGET_PC + static const int CHAIN_COUNT = 22; + cXyz mChainInterpPrev[CHAIN_COUNT]; + cXyz mChainInterpCurr[CHAIN_COUNT]; + bool mChainInterpPrevValid; + bool mChainInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(daObjFchain_c) == 0x928); diff --git a/include/d/actor/d_a_obj_klift00.h b/include/d/actor/d_a_obj_klift00.h index 865fab8c47..ab5403d51b 100644 --- a/include/d/actor/d_a_obj_klift00.h +++ b/include/d/actor/d_a_obj_klift00.h @@ -25,6 +25,10 @@ public: int Draw(); int Delete(); +#if TARGET_PC + void onInterpCallback(); +#endif + enum Param_e { LOCK_e = (1 << 6), NO_BASE_DISP = (1 << 7) }; @@ -50,6 +54,13 @@ private: /* 0x1020 */ dCcD_Cyl mCylinderCollider; /* 0x115C */ s32 mStopSwingingFrames; +#if TARGET_PC + cXyz mChainInterpPrev[64]; + cXyz mChainInterpCurr[64]; + bool mChainInterpPrevValid; + bool mChainInterpCurrValid; +#endif + // Number of chain models u32 getArg0() { return fopAcM_GetParamBit(this, 0, 6); diff --git a/include/d/actor/d_a_obj_lv8Lift.h b/include/d/actor/d_a_obj_lv8Lift.h index c5a8ae9f27..c327f4cf3c 100644 --- a/include/d/actor/d_a_obj_lv8Lift.h +++ b/include/d/actor/d_a_obj_lv8Lift.h @@ -58,6 +58,9 @@ public: void setNextPoint(); int Draw(); int Delete(); +#if TARGET_PC + friend void daL8Lift_interp_callback(bool isSimFrame, void* pUserWork); +#endif u8 getPthID() { return fopAcM_GetParamBit(this, 0, 8); } u8 getMoveSpeed() { return fopAcM_GetParamBit(this, 8, 4); } diff --git a/include/d/d_cam_param.h b/include/d/d_cam_param.h index d6ff7b46d7..867c66e38d 100644 --- a/include/d/d_cam_param.h +++ b/include/d/d_cam_param.h @@ -143,6 +143,12 @@ public: /* 0x20 */ JORFile mFile; #endif +#if TARGET_PC + /* 0x24 */ u8 mManualMode; + /* 0x25 */ f32 freeXAngle; + /* 0x29 */ f32 freeYAngle; +#endif + u32 Id(s32 i_style) { return mCamStyleData[i_style].field_0x0; } int Algorythmn(s32 i_style) { return mCamStyleData[i_style].field_0x4; } int Algorythmn() { return mCurrentStyle->field_0x4; } diff --git a/include/d/d_camera.h b/include/d/d_camera.h index 85e930c395..c481262a46 100644 --- a/include/d/d_camera.h +++ b/include/d/d_camera.h @@ -118,6 +118,18 @@ class camera_class; class dCamera_c; typedef bool (dCamera_c::*engine_fn)(s32); +#if TARGET_PC +struct DebugFlyCam { + bool initialized; + f32 pitch; + f32 yaw; + cXyz savedCenter; + cXyz savedEye; + f32 savedFovy; + cSAngle savedBank; +}; +#endif + class dCamera_c { public: class dCamInfo_c { @@ -273,6 +285,8 @@ public: /* 0xA4 */ f32 field_0xa4; /* 0xA8 */ int field_0xa8; /* 0xAC */ f32 field_0xac; + f32 xAngle; + f32 yAngle; }; struct LockOnData { @@ -1024,6 +1038,11 @@ public: bool colosseumCamera(s32); bool test1Camera(s32); bool test2Camera(s32); + #if TARGET_PC + bool freeCamera(); + bool executeDebugFlyCam(); + void deactivateDebugFlyCam(); + #endif bool towerCamera(s32); bool hookshotCamera(s32); bool railCamera(s32); @@ -1371,6 +1390,10 @@ public: /* 0x970 */ dCamSetup_c mCamSetup; /* 0xAEC */ dCamParam_c mCamParam; /* 0xB0C */ u8 field_0xb0c; + +#if TARGET_PC + DebugFlyCam mDebugFlyCam; +#endif }; // Size: 0xB10 dCamera_c* dCam_getBody(); diff --git a/include/d/d_com_inf_game.h b/include/d/d_com_inf_game.h index fcd4e77ba2..e67406f3a3 100644 --- a/include/d/d_com_inf_game.h +++ b/include/d/d_com_inf_game.h @@ -1845,6 +1845,12 @@ inline void dComIfGs_addDeathCount() { g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().addDeathCount(); } +#if TARGET_PC +inline u16 dComIfGs_getDeathCount() { + return g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().getDeathCount(); +} +#endif + inline char* dComIfGs_getPlayerName() { return g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().getPlayerName(); } diff --git a/include/d/d_menu_dmap.h b/include/d/d_menu_dmap.h index e50c533654..81baea1446 100644 --- a/include/d/d_menu_dmap.h +++ b/include/d/d_menu_dmap.h @@ -91,6 +91,10 @@ public: void calcCursor(); void drawCursor(); + #if TARGET_PC + void dMapBgWide(); + #endif + void setDPDFloorSelCurPos(s8 i_pos) { field_0xdd6 = i_pos; } f32 getMapWidth() { return mMapWidth; } diff --git a/include/d/d_menu_fmap2D.h b/include/d/d_menu_fmap2D.h index 9b237f2771..7df78bb6d0 100644 --- a/include/d/d_menu_fmap2D.h +++ b/include/d/d_menu_fmap2D.h @@ -81,6 +81,10 @@ public: void calcDrawPriority(); void setArrowPosAxis(f32, f32); + #if TARGET_PC + void fMapBackWide(); + #endif + virtual void draw(); virtual ~dMenu_Fmap2DBack_c(); @@ -165,6 +169,12 @@ public: void mapBlink() {} + #if PLATFORM_WII || TARGET_PC + f32 getMirrorPosX(f32 param_0, f32 param_1) { + return (field_0x11dc * 2.0f - (param_0 + param_1)) - param_1; + } + #endif + // Unknown name struct RegionTexData { /* 0x00 */ float mMinX; @@ -330,6 +340,10 @@ public: void setHIO(bool); bool isWarpAccept(); + #if TARGET_PC + void fMapTopWide(); + #endif + virtual void draw(); virtual ~dMenu_Fmap2DTop_c(); diff --git a/include/d/d_menu_map_common.h b/include/d/d_menu_map_common.h index 989aee7d8b..de50d775ca 100644 --- a/include/d/d_menu_map_common.h +++ b/include/d/d_menu_map_common.h @@ -66,6 +66,16 @@ public: _c90 = param_2; } +#if PLATFORM_WII || TARGET_PC + f32 getMirrorCenterPosX(f32 param_0, f32 param_1) { + if (_c90) { + return (mCenterPosX * 2.0f - (param_0 + param_1)) - param_1; + } + + return param_0; + } +#endif + struct Stage_c { // Incomplete class diff --git a/include/d/d_meter_button.h b/include/d/d_meter_button.h index 3028abeab5..0f05d32440 100644 --- a/include/d/d_meter_button.h +++ b/include/d/d_meter_button.h @@ -343,6 +343,11 @@ public: /* 0x624 */ f32 mMidonaPosX; /* 0x628 */ f32 mMidonaPosY; /* 0x62C */ f32 mMidonaScale; + +#ifdef TARGET_PC + bool mWasListen[2]; + bool mWasRepeat[2]; +#endif }; #endif /* D_METER_D_METER_BUTTON_H */ diff --git a/include/d/d_msg_object.h b/include/d/d_msg_object.h index b55ea73904..d2ac4ecb2a 100644 --- a/include/d/d_msg_object.h +++ b/include/d/d_msg_object.h @@ -67,6 +67,9 @@ public: bool isStaffMessage(); bool isSaveMessage(); bool isTalkMessage(); +#if TARGET_PC + bool isShopItemMessage(); +#endif const char* getSmellName(); const char* getPortalName(); const char* getBombName(); diff --git a/include/d/d_name.h b/include/d/d_name.h index e685871738..3ac6592457 100644 --- a/include/d/d_name.h +++ b/include/d/d_name.h @@ -89,7 +89,7 @@ public: void MojiSelectAnm3(); int mojiChange(u8); void selectMojiSet(); - #if REGION_JPN + #if TARGET_PC || REGION_JPN int checkDakuon(int, u8); int setDakuon(int, u8); #endif diff --git a/include/d/d_s_logo.h b/include/d/d_s_logo.h index 56d10eef82..ab714a17ae 100644 --- a/include/d/d_s_logo.h +++ b/include/d/d_s_logo.h @@ -79,7 +79,7 @@ public: bool isProgressiveMode(); void setRenderMode(); - #if VERSION == VERSION_GCN_PAL || PLATFORM_WII || PLATFORM_SHIELD + #if TARGET_PC || VERSION == VERSION_GCN_PAL || PLATFORM_WII || PLATFORM_SHIELD u8 getPalLanguage(); #endif @@ -149,7 +149,7 @@ public: /* 0x200 */ dDlst_2D_c* mNvLogo; /* 0x204 */ dDlst_2D_c* mMocImg; #endif -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC || VERSION == VERSION_GCN_PAL /* 0x1FC */ mDoDvdThd_mountArchive_c* mpPalLogoResCommand; #endif /* 0x1FC */ request_of_phase_process_class* m_preLoad_dylPhase; diff --git a/include/d/d_save.h b/include/d/d_save.h index 0e9a4b37cd..e2719e0a98 100644 --- a/include/d/d_save.h +++ b/include/d/d_save.h @@ -486,6 +486,9 @@ public: mDeathCount++; } } +#if TARGET_PC + u16 getDeathCount() const { return mDeathCount; } +#endif char* getPlayerName() const { return const_cast(mPlayerName); } void setPlayerName(const char* i_name) { #if AVOID_UB diff --git a/include/dusk/achievements.h b/include/dusk/achievements.h new file mode 100644 index 0000000000..5c2758b6b7 --- /dev/null +++ b/include/dusk/achievements.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "nlohmann/json.hpp" + +namespace dusk { + +enum class AchievementCategory : uint8_t { + Challenge, + Collection, + Minigame, + Misc, + Glitched +}; + +struct Achievement { + const char* key; + const char* name; + const char* description; + AchievementCategory category; + bool isCounter; + int32_t goal; + int32_t progress; + bool unlocked; +}; + +// Responsible for updating a.progress. +// Use extra for any per-achievement state that must survive across frames or sessions, extra is saved +using AchievementCheckFn = std::function; + +class AchievementSystem { +public: + static AchievementSystem& get(); + + void load(); + void save(); + void tick(); + void clearAll(); + void clearOne(const char* key); + + // Signals are visible to all achievement checks within the same tick, then cleared. + void signal(const char* key); + bool hasSignal(const char* key) const; + + std::vector getAchievements() const; + +private: + struct Entry { + Achievement achievement; + AchievementCheckFn check; + nlohmann::json extra; + }; + + AchievementSystem(); + static std::vector makeEntries(); + void processEntry(Entry& e); + + std::vector m_entries; + std::unordered_set m_signals; + bool m_loaded = false; + bool m_dirty = false; +}; + +} // namespace dusk diff --git a/include/dusk/autosave.h b/include/dusk/autosave.h new file mode 100644 index 0000000000..248924fcb8 --- /dev/null +++ b/include/dusk/autosave.h @@ -0,0 +1,17 @@ +#pragma once + +#ifndef AUTOSAVE_H +#define AUTOSAVE_H + +#include +#include + +void noAutoSave(); +void triggerAutoSave(); +void updateAutoSave(); +void enterAutoSave(); +void autoSaving(); +void waitingForWrite(); +void endAutoSave(); + +#endif \ No newline at end of file diff --git a/include/dusk/discord_presence.hpp b/include/dusk/discord_presence.hpp index 899b2ad5f9..ebecc2d07b 100644 --- a/include/dusk/discord_presence.hpp +++ b/include/dusk/discord_presence.hpp @@ -1,18 +1,14 @@ #pragma once -#ifdef DUSK_DISCORD_RPC +#ifdef DUSK_DISCORD -namespace dusk { -namespace discord { +namespace dusk::discord { -void Initialize(); +void initialize(); +void run_callbacks(); +void update_presence(); +void shutdown(); -void RunCallbacks(); +} // namespace dusk::discord -void UpdatePresence(); - -void Shutdown(); -} -} - -#endif // DUSK_DISCORD_RPC +#endif // DUSK_DISCORD diff --git a/include/dusk/dvd_asset.hpp b/include/dusk/dvd_asset.hpp index 014c683d44..8594d4631c 100644 --- a/include/dusk/dvd_asset.hpp +++ b/include/dusk/dvd_asset.hpp @@ -1,22 +1,31 @@ #pragma once #include "dolphin/types.h" +#include "version.hpp" namespace dusk { +struct OffsetVersion { + version::GameVersion mGameVersion; + u32 mOffset; + + constexpr OffsetVersion(const version::GameVersion gameVersion, const u32 offset) + : mGameVersion(gameVersion), mOffset(offset) {} +}; + /** * Load bytes from the main DOL by GameCube virtual address */ -bool LoadDolAsset(void* dst, u32 virtualAddress, s32 size); +bool LoadDolAsset(void* dst, std::initializer_list virtualAddress, s32 size); /** * Load bytes from a REL file in the ISO filesystem, dst must be 32-byte aligned */ -bool LoadRelAsset(void* dst, const char* dvdPath, s32 offset, s32 size); +bool LoadRelAsset(void* dst, const char* dvdPath, std::initializer_list offset, s32 size); /** * Load bytes from a REL inside RELS.arc */ -bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, s32 offset, s32 size); +bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, std::initializer_list offset, s32 size); } // namespace dusk diff --git a/include/dusk/gyro.h b/include/dusk/gyro.h index 279aeae16e..a206100739 100644 --- a/include/dusk/gyro.h +++ b/include/dusk/gyro.h @@ -12,6 +12,7 @@ void rollgoalTableOffset(s16& out_ax, s16& out_az); extern bool s_sensor_keep_alive; bool get_sensor_keep_alive(); void set_sensor_keep_alive(bool value); +bool rollgoal_gyro_enabled(); } // namespace dusk::gyro #endif diff --git a/include/dusk/io.hpp b/include/dusk/io.hpp index fb71a77d68..2efc4a8d3b 100644 --- a/include/dusk/io.hpp +++ b/include/dusk/io.hpp @@ -1,6 +1,7 @@ #ifndef DUSK_IO_HPP #define DUSK_IO_HPP +#include #include // I can't believe it's 2026 and neither SDL (no error codes) nor @@ -15,7 +16,7 @@ namespace dusk::io { * Methods on this class throw appropriate C++ exceptions when an error occurs. */ class FileStream { - void* file; + FILE* file; public: FileStream() noexcept; @@ -23,7 +24,7 @@ public: /** * \brief Take ownership of a FILE* handle. */ - explicit FileStream(void* file); + explicit FileStream(FILE* file); FileStream(const FileStream& other) = delete; FileStream(FileStream&& other) noexcept; @@ -34,6 +35,11 @@ public: */ static FileStream OpenRead(const char* utf8Path); + /** + * \brief Open a file for reading at the given path. + */ + static FileStream OpenRead(const std::filesystem::path& path); + /** * \brief Create a file for writing. * @@ -41,16 +47,33 @@ public: */ static FileStream Create(const char* utf8Path); + /** + * \brief Create a file for writing. + * + * If there is an existing file, its contents are demolished. + */ + static FileStream Create(const std::filesystem::path& path); + /** * \brief Read the byte contents of a file directly into a vector. */ static std::vector ReadAllBytes(const char* utf8Path); + /** + * \brief Read the byte contents of a file directly into a vector. + */ + static std::vector ReadAllBytes(const std::filesystem::path& path); + /** * \brief Read the byte contents of a file directly into a vector. */ static void WriteAllText(const char* utf8Path, std::string_view text); + /** + * \brief Read the byte contents of a file directly into a vector. + */ + static void WriteAllText(const std::filesystem::path& path, std::string_view text); + /** * \brief Read the remaining contents of the file directly into a vector. */ @@ -59,17 +82,24 @@ public: /** * Get direct access to the underlying FILE* handle. */ - [[nodiscard]] void* GetFileHandle() const noexcept { - return file; - } + [[nodiscard]] void* GetFileHandle() const noexcept { return file; } /** * Write data to the file. */ void Write(const char* data, size_t dataLen); + + FILE* ToInner(); }; +/** + * Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows. + */ +inline std::string fs_path_to_string(const std::filesystem::path& path) { + const auto u8str = path.u8string(); + return {reinterpret_cast(u8str.c_str())}; } +} // namespace dusk::io #endif // DUSK_IO_HPP diff --git a/include/dusk/livesplit.h b/include/dusk/livesplit.h new file mode 100644 index 0000000000..b283a29af4 --- /dev/null +++ b/include/dusk/livesplit.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace dusk::speedrun { +void onGameFrame(); +uint64_t getFrameCount(); +void start(); +void reset(); +void connectLiveSplit(const char* host = "127.0.0.1", int port = 16834); +void disconnectLiveSplit(); +bool consumeConnectedEvent(); +bool consumeDisconnectedEvent(); +void updateLiveSplit(); +void shutdown(); +} diff --git a/include/dusk/main.h b/include/dusk/main.h index 065f507d36..c5c4fb27d1 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -1,14 +1,36 @@ #ifndef DUSK_MAIN_H #define DUSK_MAIN_H +#if defined(__APPLE__) +#include +#endif + #include +#if defined(_WIN32) || \ + (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \ + (defined(__linux__) && !defined(__ANDROID__)) +#define DUSK_CAN_OPEN_DATA_FOLDER 1 +#else +#define DUSK_CAN_OPEN_DATA_FOLDER 0 +#endif + namespace dusk { extern bool IsRunning; extern bool IsShuttingDown; extern bool IsGameLaunched; - extern bool IsFocusPaused; + extern bool RestartRequested; extern std::filesystem::path ConfigPath; + +#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ + (defined(TARGET_OS_TV) && TARGET_OS_TV) + inline constexpr bool SupportsProcessRestart = false; +#else + inline constexpr bool SupportsProcessRestart = true; +#endif + + void RequestRestart() noexcept; + bool OpenDataFolder(); } #endif // DUSK_MAIN_H diff --git a/include/dusk/map_loader_definitions.h b/include/dusk/map_loader_definitions.h index a0f7150d2d..0ffda772ba 100644 --- a/include/dusk/map_loader_definitions.h +++ b/include/dusk/map_loader_definitions.h @@ -1,5 +1,7 @@ #pragma once +#include + struct RoomEntry { u8 roomNo; std::vector roomPoints = {}; diff --git a/include/dusk/math.h b/include/dusk/math.h index fe27b3046b..04f1087670 100644 --- a/include/dusk/math.h +++ b/include/dusk/math.h @@ -2,9 +2,6 @@ #define _SRC_DUSK_MATH_H_ #include -#include -#include -#include #ifndef M_PI #define M_PI 3.14159265358979323846f @@ -19,139 +16,6 @@ inline float i_cosf(float x) { return cos(x); } inline float i_tanf(float x) { return tan(x); } inline float i_acosf(float x) { return acos(x); } - -// frsqrte matching courtesy of Geotale, with reference to https://achurch.org/cpu-tests/ppc750cl.s - -struct BaseAndDec32 { - uint32_t base; - int32_t dec; -}; - -struct BaseAndDec64 { - uint64_t base; - int64_t dec; -}; - -union c32 { - constexpr c32(const float p) { - f = p; - } - - constexpr c32(const uint32_t p) { - u = p; - } - - uint32_t u; - float f; -}; - -union c64 { - constexpr c64(const double p) { - f = p; - } - - constexpr c64(const uint64_t p) { - u = p; - } - - uint64_t u; - double f; -}; - -static constexpr uint64_t EXPONENT_SHIFT_F64 = 52; -static constexpr uint64_t MANTISSA_MASK_F64 = 0x000fffffffffffffULL; -static constexpr uint64_t EXPONENT_MASK_F64 = 0x7ff0000000000000ULL; -static constexpr uint64_t SIGN_MASK_F64 = 0x8000000000000000ULL; - -static constexpr std::array RSQRTE_TABLE = {{ - {0x69fa000000000ULL, -0x15a0000000LL}, - {0x5f2e000000000ULL, -0x13cc000000LL}, - {0x554a000000000ULL, -0x1234000000LL}, - {0x4c30000000000ULL, -0x10d4000000LL}, - {0x43c8000000000ULL, -0x0f9c000000LL}, - {0x3bfc000000000ULL, -0x0e88000000LL}, - {0x34b8000000000ULL, -0x0d94000000LL}, - {0x2df0000000000ULL, -0x0cb8000000LL}, - {0x2794000000000ULL, -0x0bf0000000LL}, - {0x219c000000000ULL, -0x0b40000000LL}, - {0x1bfc000000000ULL, -0x0aa0000000LL}, - {0x16ae000000000ULL, -0x0a0c000000LL}, - {0x11a8000000000ULL, -0x0984000000LL}, - {0x0ce6000000000ULL, -0x090c000000LL}, - {0x0862000000000ULL, -0x0898000000LL}, - {0x0416000000000ULL, -0x082c000000LL}, - {0xffe8000000000ULL, -0x1e90000000LL}, - {0xf0a4000000000ULL, -0x1c00000000LL}, - {0xe2a8000000000ULL, -0x19c0000000LL}, - {0xd5c8000000000ULL, -0x17c8000000LL}, - {0xc9e4000000000ULL, -0x1610000000LL}, - {0xbedc000000000ULL, -0x1490000000LL}, - {0xb498000000000ULL, -0x1330000000LL}, - {0xab00000000000ULL, -0x11f8000000LL}, - {0xa204000000000ULL, -0x10e8000000LL}, - {0x9994000000000ULL, -0x0fe8000000LL}, - {0x91a0000000000ULL, -0x0f08000000LL}, - {0x8a1c000000000ULL, -0x0e38000000LL}, - {0x8304000000000ULL, -0x0d78000000LL}, - {0x7c48000000000ULL, -0x0cc8000000LL}, - {0x75e4000000000ULL, -0x0c28000000LL}, - {0x6fd0000000000ULL, -0x0b98000000LL}, -}}; - -[[nodiscard]] static inline double frsqrte(const double val) { - c64 bits(val); - - uint64_t mantissa = bits.u & MANTISSA_MASK_F64; - int64_t exponent = bits.u & EXPONENT_MASK_F64; - bool sign = (bits.u & SIGN_MASK_F64) != 0; - - // Handle 0 case - if (mantissa == 0 && exponent == 0) { - return std::copysign(std::numeric_limits::infinity(), bits.f); - } - - // Handle NaN-like - if (exponent == EXPONENT_MASK_F64) { - if (mantissa == 0) { - return sign ? std::numeric_limits::quiet_NaN() : 0.0; - } - - return val; - } - - // Handle negative inputs - if (sign) { - return std::numeric_limits::quiet_NaN(); - } - - if (exponent == 0) { - // Shift so one bit goes to where the exponent would be, - // then clear that bit to mimic a not-subnormal number! - // Aka, if there are 12 leading zeroes, shift left once - uint32_t shift = std::countl_zero(mantissa) - static_cast(63 - EXPONENT_SHIFT_F64); - - mantissa <<= shift; - mantissa &= MANTISSA_MASK_F64; - // The shift is subtracted by 1 because denormals by default - // are offset by 1 (exponent 0 doesn't have implied 1 bit) - exponent -= static_cast(shift - 1) << EXPONENT_SHIFT_F64; - } - - // In reality this doesn't get the full exponent -- Only the least significant bit - // Only that's needed because square roots of higher exponent bits simply multiply the - // result by 2!! - uint32_t key = static_cast((static_cast(exponent) | mantissa) >> 37); - uint64_t new_exp = - (static_cast((0xbfcLL << EXPONENT_SHIFT_F64) - exponent) >> 1) & EXPONENT_MASK_F64; - - // Remove the bits relating to anything higher than the LSB of the exponent - const auto &entry = RSQRTE_TABLE[0x1f & (key >> 11)]; - - // The result is given by an estimate then an adjustment based on the original - // key that was computed - uint64_t new_mantissa = static_cast(entry.base + entry.dec * static_cast(key & 0x7ff)); - - return c64(new_exp | new_mantissa).f; -} +#include #endif // _SRC_DUSK_MATH_H_ diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 039e51af7d..f74a4bf61f 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -13,12 +13,49 @@ enum class BloomMode : int { Dusk = 2, }; +enum class GameLanguage : u8 { + English = OS_LANGUAGE_ENGLISH, + German = OS_LANGUAGE_GERMAN, + French = OS_LANGUAGE_FRENCH, + Spanish = OS_LANGUAGE_SPANISH, + Italian = OS_LANGUAGE_ITALIAN, +}; + +enum class DiscVerificationState : u8 { + Unknown = 0, + Success, + HashMismatch, +}; + +enum class GyroMode : u8 { + Sensor = 0, + Mouse = 1, +}; + namespace config { template <> struct ConfigEnumRange { static constexpr auto min = BloomMode::Off; static constexpr auto max = BloomMode::Dusk; }; + +template <> +struct ConfigEnumRange { + static constexpr auto min = GameLanguage::English; + static constexpr auto max = GameLanguage::Italian; +}; + +template <> +struct ConfigEnumRange { + static constexpr auto min = DiscVerificationState::Unknown; + static constexpr auto max = DiscVerificationState::HashMismatch; +}; + +template <> +struct ConfigEnumRange { + static constexpr auto min = GyroMode::Sensor; + static constexpr auto max = GyroMode::Mouse; +}; } // Persistent user settings @@ -31,6 +68,8 @@ struct UserSettings { ConfigVar enableFullscreen; ConfigVar enableVsync; ConfigVar lockAspectRatio; + ConfigVar enableFpsOverlay; + ConfigVar fpsOverlayCorner; } video; struct { @@ -41,15 +80,18 @@ struct UserSettings { ConfigVar soundEffectsVolume; ConfigVar fanfareVolume; ConfigVar enableReverb; + ConfigVar enableHrtf; + ConfigVar menuSounds; } audio; // Game settings struct { + ConfigVar language; + // QoL ConfigVar enableQuickTransform; ConfigVar hideTvSettingsScreen; - ConfigVar skipWarningScreen; ConfigVar biggerWallets; ConfigVar noReturnRupees; ConfigVar disableRupeeCutscenes; @@ -60,17 +102,19 @@ struct UserSettings { ConfigVar fastClimbing; ConfigVar noMissClimbing; ConfigVar fastTears; + ConfigVar no2ndFishForCat; ConfigVar instantSaves; ConfigVar instantText; ConfigVar sunsSong; + ConfigVar autoSave; // Preferences ConfigVar enableMirrorMode; - ConfigVar invertCameraXAxis; - ConfigVar disableMainHUD; + ConfigVar minimalHUD; ConfigVar pauseOnFocusLost; ConfigVar enableLinkDollRotation; - + ConfigVar enableAchievementToasts; + ConfigVar enableControllerToasts; // Graphics ConfigVar bloomMode; @@ -80,12 +124,14 @@ struct UserSettings { ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; ConfigVar enableDepthOfField; + ConfigVar enableMapBackground; // Audio ConfigVar noLowHpSound; ConfigVar midnasLamentNonStop; // Input + ConfigVar gyroMode; ConfigVar enableGyroAim; ConfigVar enableGyroRollgoal; ConfigVar gyroSensitivityX; @@ -95,6 +141,13 @@ struct UserSettings { ConfigVar gyroDeadband; ConfigVar gyroInvertPitch; ConfigVar gyroInvertYaw; + ConfigVar freeCamera; + ConfigVar invertCameraXAxis; + ConfigVar invertCameraYAxis; + ConfigVar freeCameraSensitivity; + ConfigVar debugFlyCam; + ConfigVar debugFlyCamLockEvents; + ConfigVar allowBackgroundInput; // Cheats ConfigVar infiniteHearts; @@ -117,17 +170,24 @@ struct UserSettings { // Controls ConfigVar enableTurboKeybind; + + // Tools + ConfigVar speedrunMode; + ConfigVar liveSplitEnabled; + ConfigVar recordingMode; } game; struct { ConfigVar isoPath; + ConfigVar isoVerification; ConfigVar graphicsBackend; ConfigVar skipPreLaunchUI; ConfigVar showPipelineCompilation; ConfigVar wasPresetChosen; ConfigVar enableCrashReporting; - ConfigVar duskMenuOpen; + ConfigVar checkForUpdates; ConfigVar cardFileType; + ConfigVar enableAdvancedSettings; } backend; }; diff --git a/include/dusk/version.hpp b/include/dusk/version.hpp new file mode 100644 index 0000000000..fe393b049b --- /dev/null +++ b/include/dusk/version.hpp @@ -0,0 +1,67 @@ +#ifndef DUSK_VERSION_HPP +#define DUSK_VERSION_HPP + +/** + * Functionality for switching game behavior based on the loaded game version (e.g. PAL/JPN, GC/Wii) + */ +namespace dusk::version { + enum class GameVersion : u8 { + GcnUsa = VERSION_GCN_USA, + GcnPal = VERSION_GCN_PAL, + GcnJpn = VERSION_GCN_JPN, + WiiUsaRev0 = VERSION_WII_USA_R0, + WiiUsa = VERSION_WII_USA_R2, + WiiPal = VERSION_WII_PAL, + WiiJpn = VERSION_WII_JPN, + WiiKor = VERSION_WII_KOR, + }; + + bool isGcn(); + bool isWii(); + bool isPalOrAtLeastWiiR2(); + + bool isRegionPal(); + bool isRegionJpn(); + bool isRegionUsa(); + + GameVersion getGameVersion(); + + const DVDDiskID& getDiskID(); + + void init(); + + template + struct VersionOption { + GameVersion mVersion; + T mValue; + + constexpr VersionOption(GameVersion version, T value) : mVersion(version), mValue(value) {} + }; + + template + const T& versionSelect(const std::initializer_list> options) { + const auto version = getGameVersion(); + for (const auto& opt : options) { + if (opt.mVersion == version) { + return opt.mValue; + } + } + + // Unable to find value. + abort(); + } + + template + const T& versionSelect(const std::initializer_list> options, const T& defaultValue) { + const auto version = getGameVersion(); + for (const auto& opt : options) { + if (opt.mVersion == version) { + return opt.mValue; + } + } + + return defaultValue; + } +} // namespace dusk::version + +#endif // DUSK_VERSION_HPP diff --git a/include/global.h b/include/global.h index 8cc1b55743..11415b0737 100644 --- a/include/global.h +++ b/include/global.h @@ -243,12 +243,16 @@ using std::isnan; // Some basic macros that are more convenient than putting down #if blocks for one-line changes. #if TARGET_PC #define IF_DUSK(statement) statement +#define IF_DUSK_BLOCK(cond) if (cond) { +#define IF_DUSK_BLOCK_END } #define IF_DUSK_ARG(expr) , expr #define IF_NOT_DUSK(statement) #define DUSK_IF_ELSE(dusk, orig) dusk #else #define IF_DUSK(statement) #define IF_DUSK_ARG(expr) +#define IF_DUSK_BLOCK(cond) +#define IF_DUSK_BLOCK_END #define IF_NOT_DUSK(statement) statement #define DUSK_IF_ELSE(dusk, orig) orig #endif diff --git a/include/m_Do/m_Do_audio.h b/include/m_Do/m_Do_audio.h index 1d58e8d8f2..4bc2f20c94 100644 --- a/include/m_Do/m_Do_audio.h +++ b/include/m_Do/m_Do_audio.h @@ -5,6 +5,7 @@ #include "Z2AudioLib/Z2EnvSeMgr.h" #include "Z2AudioLib/Z2LinkMgr.h" #include "dusk/audio.h" +#include "dusk/settings.h" class mDoAud_zelAudio_c : public Z2AudioMgr { public: @@ -132,6 +133,18 @@ inline void mDoAud_seStart(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_re -1.0f, -1.0f, 0); } +#if TARGET_PC +inline void mDoAud_seStartMenu(u32 i_sfxID) { + if (!mDoAud_zelAudio_c::isInitFlag()) { + return; + } + if (!dusk::getSettings().audio.menuSounds.getValue()) { + return; + } + mDoAud_seStart(i_sfxID, nullptr, 0, 0); +} +#endif + inline void mDoAud_seStartLevel(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_reverb) { DUSK_AUDIO_SKIP() Z2AudioMgr::getInterface()->seStartLevel(i_sfxID, i_sePos, param_2, i_reverb, 1.0f, 1.0f, diff --git a/libs/JSystem/include/JSystem/J3DGraphBase/J3DStruct.h b/libs/JSystem/include/JSystem/J3DGraphBase/J3DStruct.h index b565ad78f8..05663fd84e 100644 --- a/libs/JSystem/include/JSystem/J3DGraphBase/J3DStruct.h +++ b/libs/JSystem/include/JSystem/J3DGraphBase/J3DStruct.h @@ -1,9 +1,9 @@ #ifndef J3DSTRUCT_H #define J3DSTRUCT_H +#include #include #include -#include #include "global.h" #include "JSystem/JMath/JMath.h" @@ -11,7 +11,7 @@ /** * @ingroup jsystem-j3d - * + * */ struct J3DLightInfo { bool operator==(J3DLightInfo& other) const; @@ -28,7 +28,7 @@ struct J3DLightInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DTextureSRTInfo { // NOTE: Big endian when loaded from file! @@ -79,7 +79,7 @@ enum J3DTexMtxMode { /** * @ingroup jsystem-j3d - * + * */ struct J3DTexMtxInfo { bool operator==(J3DTexMtxInfo& other) const; @@ -97,7 +97,7 @@ struct J3DTexMtxInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DIndTexMtxInfo { J3DIndTexMtxInfo& operator=(J3DIndTexMtxInfo const&); @@ -107,7 +107,7 @@ struct J3DIndTexMtxInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DFogInfo { bool operator==(J3DFogInfo&) const; @@ -126,7 +126,7 @@ struct J3DFogInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DNBTScaleInfo { bool operator==(const J3DNBTScaleInfo& other) const; @@ -153,7 +153,7 @@ struct J3DIndTexOrderInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DTevSwapModeInfo { /* 0x0 */ u8 mRasSel; @@ -164,7 +164,7 @@ struct J3DTevSwapModeInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DTevSwapModeTableInfo { /* 0x0 */ u8 field_0x0; @@ -175,7 +175,7 @@ struct J3DTevSwapModeTableInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DTevStageInfo { /* 0x0 */ u8 field_0x0; @@ -202,7 +202,7 @@ struct J3DTevStageInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DIndTevStageInfo { /* 0x0 */ u8 mIndStage; @@ -219,7 +219,7 @@ struct J3DIndTevStageInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DTexCoordInfo { /* 0x0 */ u8 mTexGenType; @@ -265,7 +265,7 @@ struct J3DBlendInfo { /** * @ingroup jsystem-j3d - * + * */ struct J3DTevOrderInfo { void operator=(const J3DTevOrderInfo& other) { diff --git a/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h b/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h index 87c85f316f..6f6d29ca98 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h @@ -59,6 +59,9 @@ public: bool isActive() const { return mSeqList.getNumLinks() != 0; } int getNumActiveSeqs() const { return mSeqList.getNumLinks(); } void pause(bool paused) { mActivity.field_0x0.flags.flag2 = paused; } + #if TARGET_PC + JSUList* getSeqList() { return &mSeqList; } + #endif private: /* 0x08 */ JAIAudience* mAudience; diff --git a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h index d348859ebe..795a5a2628 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h +++ b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h @@ -207,4 +207,11 @@ void JPARegistAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*); void JPARegistPrmAlpha(JPAEmitterWorkData*, JPABaseParticle*); void JPARegistPrmAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*); +#if TARGET_PC +void JPAInterpBillboard(JPAEmitterWorkData*, JPABaseParticle*); +void JPAInterpRotBillboard(JPAEmitterWorkData*, JPABaseParticle*); +void JPAInterpDirection(JPAEmitterWorkData*, JPABaseParticle*); +void JPAInterpRotDirection(JPAEmitterWorkData*, JPABaseParticle*); +#endif + #endif /* JPABASESHAPE_H */ diff --git a/libs/JSystem/include/JSystem/JParticle/JPAParticle.h b/libs/JSystem/include/JSystem/JParticle/JPAParticle.h index 842f293286..c864a5cd4a 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPAParticle.h +++ b/libs/JSystem/include/JSystem/JParticle/JPAParticle.h @@ -24,6 +24,9 @@ public: void init_c(JPAEmitterWorkData*, JPABaseParticle*); bool calc_p(JPAEmitterWorkData*); bool calc_c(JPAEmitterWorkData*); +#if TARGET_PC + void interp(JPAEmitterWorkData*, void const* drawFunc); +#endif bool canCreateChild(JPAEmitterWorkData*); f32 getWidth(JPABaseEmitter const*) const; f32 getHeight(JPABaseEmitter const*) const; diff --git a/libs/JSystem/include/JSystem/JUtility/JUTPalette.h b/libs/JSystem/include/JSystem/JUtility/JUTPalette.h index 017d511d50..5d29005a92 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTPalette.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTPalette.h @@ -40,6 +40,9 @@ public: JUTTransparency getTransparency() const { return JUTTransparency(mTransparency); } u16 getNumColors() const { return mNumColors; } ResTLUT* getColorTable() const { return mColorTable; } +#if TARGET_PC + void dataUploaded(); +#endif private: /* 0x00 */ GXTlutObj mTlutObj; diff --git a/libs/JSystem/include/JSystem/JUtility/JUTTexture.h b/libs/JSystem/include/JSystem/JUtility/JUTTexture.h index e0f1e15ff0..6a7a8f9a0e 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTTexture.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTTexture.h @@ -75,6 +75,7 @@ public: s32 getTransparency() const { return mTexInfo->alphaEnabled; } s32 getWidth() const { return mTexInfo->width; } s32 getHeight() const { return mTexInfo->height; } + JUTPalette* getPalette() const { return mPalette; } void setCaptureFlag(bool flag) { mFlags &= 2 | flag; } bool getCaptureFlag() const { return mFlags & 1; } bool getEmbPaletteDelFlag() const { return mFlags & 2; } @@ -82,7 +83,7 @@ public: int getTlutName() const { return mTlutName; } bool operator==(const JUTTexture& other) { return mTexInfo == other.mTexInfo - && field_0x2c == other.field_0x2c + && mPalette == other.mPalette && mWrapS == other.mWrapS && mWrapT == other.mWrapT && mMinFilter == other.mMinFilter @@ -100,7 +101,7 @@ private: /* 0x20 */ const ResTIMG* mTexInfo; /* 0x24 */ void* mTexData; /* 0x28 */ JUTPalette* mEmbPalette; - /* 0x2C */ JUTPalette* field_0x2c; + /* 0x2C */ JUTPalette* mPalette; /* 0x30 */ u8 mWrapS; /* 0x31 */ u8 mWrapT; /* 0x32 */ u8 mMinFilter; diff --git a/libs/JSystem/src/J3DGraphLoader/J3DModelLoader.cpp b/libs/JSystem/src/J3DGraphLoader/J3DModelLoader.cpp index 32f0e4eed9..c3c72310a0 100644 --- a/libs/JSystem/src/J3DGraphLoader/J3DModelLoader.cpp +++ b/libs/JSystem/src/J3DGraphLoader/J3DModelLoader.cpp @@ -556,8 +556,8 @@ void J3DModelLoader::readVertexData(const J3DVertexBlock& block, J3DVertexData& if (attr == GX_VA_POS) { // can be a little off due to 0x20 alignment, account for that - u32 expect = ((data.mVtxNum * vertStride) + 0x1F) & ~0x1F; - JUT_ASSERT(1234, expect == addrDiff); + // u32 expect = ((data.mVtxNum * vertStride) + 0x1F) & ~0x1F; + // JUT_ASSERT(1234, expect == addrDiff); } else if (attr == GX_VA_NRM) { data.mNrmNum = num; } else if (attr == GX_VA_CLR0) { diff --git a/libs/JSystem/src/JAudio2/JASChannel.cpp b/libs/JSystem/src/JAudio2/JASChannel.cpp index 61fe80a098..758167cc5c 100644 --- a/libs/JSystem/src/JAudio2/JASChannel.cpp +++ b/libs/JSystem/src/JAudio2/JASChannel.cpp @@ -1,6 +1,9 @@ #include "JSystem/JSystem.h" // IWYU pragma: keep #include "JSystem/JAudio2/JASChannel.h" +#if TARGET_PC +#include "dusk/audio/DuskDsp.hpp" +#endif #include "JSystem/JAudio2/JASAiCtrl.h" #include "JSystem/JAudio2/JASCalc.h" #include "JSystem/JAudio2/JASDriverIF.h" @@ -170,7 +173,12 @@ void JASChannel::updateEffectorParam(JASDsp::TChannel* i_channel, u16* i_mixerVo f32 pan = 0.5f; f32 dolby = 0.0f; - switch (JASDriver::getOutputMode()) { +#if TARGET_PC + u32 effectiveOutputMode = dusk::audio::EnableHrtf ? JAS_OUTPUT_SURROUND : JASDriver::getOutputMode(); +#else + u32 effectiveOutputMode = JASDriver::getOutputMode(); +#endif + switch (effectiveOutputMode) { case JAS_OUTPUT_MONO: break; case JAS_OUTPUT_STEREO: diff --git a/libs/JSystem/src/JAudio2/JASHeapCtrl.cpp b/libs/JSystem/src/JAudio2/JASHeapCtrl.cpp index 45568cc29a..e3722e3726 100644 --- a/libs/JSystem/src/JAudio2/JASHeapCtrl.cpp +++ b/libs/JSystem/src/JAudio2/JASHeapCtrl.cpp @@ -302,7 +302,6 @@ void JASKernel::setupRootHeap(JKRSolidHeap* heap, u32 size) { JKRHEAP_NAME(sSystemHeap, "JASKernel::sSystemHeap"); JUT_ASSERT(787, sSystemHeap); sCommandHeap = JKR_NEW_ARGS (heap, 0) JASMemChunkPool<1024, JASThreadingModel::ObjectLevelLockable>; - JKRHEAP_NAME(sSystemHeap, "JASKernel::sCommandHeap"); JUT_ASSERT(790, sCommandHeap); JASDram = heap; } diff --git a/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp b/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp index e1f45de1ae..cc94097712 100644 --- a/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp +++ b/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp @@ -442,6 +442,7 @@ static JAUSectionHeap* JAUNewSectionHeap(JKRSolidHeap* heap, bool param_1) { JAUSectionHeap* JAUNewSectionHeap(bool param_0) { s32 freeSize = JASDram->getFreeSize(); JKRSolidHeap* sectionHeap = JKRCreateSolidHeap(freeSize, JASDram, true); + JKRHEAP_NAME(sectionHeap, "sectionHeap"); JUT_ASSERT(821, sectionHeap); return JAUNewSectionHeap(sectionHeap, param_0); } diff --git a/libs/JSystem/src/JKernel/JKRExpHeap.cpp b/libs/JSystem/src/JKernel/JKRExpHeap.cpp index 4a6712cb65..dc88090a47 100644 --- a/libs/JSystem/src/JKernel/JKRExpHeap.cpp +++ b/libs/JSystem/src/JKernel/JKRExpHeap.cpp @@ -222,16 +222,11 @@ void* JKRExpHeap::do_alloc(u32 size, int alignment) { OSReport_Error("Free block list as follows:\n"); OSReport_Error("Start | End | Size \n"); - int i = 0; for (const CMemBlock* block = mHeadFreeList; block; block = block->mNext) { if (block->mMagic) { // Allocated, ignore. continue; } - if (i++ > 10) { - OSReport_Error("\n"); - break; - } auto blockStart = (uintptr_t)block - (uintptr_t)mStart; auto blockEnd = (uintptr_t)block + block->size - (uintptr_t)mStart; @@ -239,6 +234,14 @@ void* JKRExpHeap::do_alloc(u32 size, int alignment) { OSReport_Error("%08X | %08X | %08X\n", (u32) blockStart, (u32) blockEnd, (u32) blockSize); } + OSReport_Error("Child heaps as follows:\n"); + OSReport_Error("Start | End | Name \n"); + + const JSUTree& tree = getHeapTree(); + for (JSUTreeIterator iter(tree.getFirstChild()); iter != tree.getEndChild(); ++iter) { + OSReport_Error("%08X | %08X | %s\n", iter->getStartAddr(), iter->getEndAddr(), iter->getName()); + } + CRASH("Aborting due to allocation failure!"); } #else diff --git a/libs/JSystem/src/JParticle/JPABaseShape.cpp b/libs/JSystem/src/JParticle/JPABaseShape.cpp index 178606a687..ff346984cd 100644 --- a/libs/JSystem/src/JParticle/JPABaseShape.cpp +++ b/libs/JSystem/src/JParticle/JPABaseShape.cpp @@ -9,6 +9,9 @@ #include #include +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif #include "tracy/Tracy.hpp" void JPASetPointSize(JPAEmitterWorkData* work) { @@ -418,50 +421,95 @@ static projectionFunc p_prj[3] = { loadPrjAnm, }; -void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { - if (param_1->checkStatus(JPAPtclStts_Invisible)) { +#if TARGET_PC +void JPAInterpBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + Mtx ptclPosMtx; + MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z); + dusk::frame_interp::record_final_mtx(ptclPosMtx, ptcl); +} + +void JPAInterpRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + Mtx ptclPosMtx; + f32 sinRot = JMASSin(ptcl->mRotateAngle); + f32 cosRot = JMASCos(ptcl->mRotateAngle); + MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z); + ptclPosMtx[0][0] = cosRot; + ptclPosMtx[0][1] = -sinRot; + ptclPosMtx[1][0] = sinRot; + ptclPosMtx[1][1] = cosRot; + dusk::frame_interp::record_final_mtx(ptclPosMtx, ptcl); +} +#endif + +void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } - JGeometry::TVec3 local_48; - MTXMultVec(work->mPosCamMtx, ¶m_1->mPosition, &local_48); - Mtx local_38; - local_38[0][0] = work->mGlobalPtclScl.x * param_1->mParticleScaleX; - local_38[0][3] = local_48.x; - local_38[1][1] = work->mGlobalPtclScl.y * param_1->mParticleScaleY; - local_38[1][3] = local_48.y; - local_38[2][2] = 1.0f; - local_38[2][3] = local_48.z; - local_38[0][1] = local_38[0][2] = local_38[1][0] = local_38[1][2] = local_38[2][0] = local_38[2][1] = 0.0f; - GXLoadPosMtxImm(local_38, 0); - p_prj[work->mPrjType](work, local_38); + JGeometry::TVec3 pos; +#if TARGET_PC + Mtx ptclPosMtx; + if (dusk::frame_interp::lookup_replacement(ptcl, ptclPosMtx)) { + pos.set(ptclPosMtx[0][3], ptclPosMtx[1][3], ptclPosMtx[2][3]); + MTXMultVec(work->mPosCamMtx, &pos, &pos); + } else +#endif + { + MTXMultVec(work->mPosCamMtx, &ptcl->mPosition, &pos); + } + Mtx posMtx; + posMtx[0][0] = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + posMtx[0][3] = pos.x; + posMtx[1][1] = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + posMtx[1][3] = pos.y; + posMtx[2][2] = 1.0f; + posMtx[2][3] = pos.z; + posMtx[0][1] = posMtx[0][2] = posMtx[1][0] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f; + GXLoadPosMtxImm(posMtx, GX_PNMTX0); + p_prj[work->mPrjType](work, posMtx); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); } -void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { - if (param_1->checkStatus(JPAPtclStts_Invisible)) { +void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } - JGeometry::TVec3 local_48; - MTXMultVec(work->mPosCamMtx, ¶m_1->mPosition, &local_48); - f32 sinRot = JMASSin(param_1->mRotateAngle); - f32 cosRot = JMASCos(param_1->mRotateAngle); - f32 particleX = work->mGlobalPtclScl.x * param_1->mParticleScaleX; - f32 particleY = work->mGlobalPtclScl.y * param_1->mParticleScaleY; + if (work->mpRes->getUsrIdx() == 0x89d7) { + int a = 0; + } - Mtx local_38; - local_38[0][0] = cosRot * particleX; - local_38[0][1] = -sinRot * particleY; - local_38[0][3] = local_48.x; - local_38[1][0] = sinRot * particleX; - local_38[1][1] = cosRot * particleY; - local_38[1][3] = local_48.y; - local_38[2][2] = 1.0f; - local_38[2][3] = local_48.z; - local_38[0][2] = local_38[1][2] = local_38[2][0] = local_38[2][1] = 0.0f; - GXLoadPosMtxImm(local_38, 0); - p_prj[work->mPrjType](work, local_38); + JGeometry::TVec3 pos; + f32 sinRot, cosRot; +#if TARGET_PC + Mtx ptclPosMtx; + MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z); + if (dusk::frame_interp::lookup_replacement(ptcl, ptclPosMtx)) { + pos.set(ptclPosMtx[0][3], ptclPosMtx[1][3], ptclPosMtx[2][3]); + sinRot = ptclPosMtx[1][0]; + cosRot = ptclPosMtx[0][0]; + MTXMultVec(work->mPosCamMtx, &pos, &pos); + } else +#endif + { + MTXMultVec(work->mPosCamMtx, &ptcl->mPosition, &pos); + sinRot = JMASSin(ptcl->mRotateAngle); + cosRot = JMASCos(ptcl->mRotateAngle); + } + f32 particleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + f32 particleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + Mtx posMtx; + posMtx[0][0] = cosRot * particleX; + posMtx[0][1] = -sinRot * particleY; + posMtx[0][3] = pos.x; + posMtx[1][0] = sinRot * particleX; + posMtx[1][1] = cosRot * particleY; + posMtx[1][3] = pos.y; + posMtx[2][2] = 1.0f; + posMtx[2][3] = pos.z; + posMtx[0][2] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f; + GXLoadPosMtxImm(posMtx, GX_PNMTX0); + p_prj[work->mPrjType](work, posMtx); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); } @@ -484,7 +532,7 @@ void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { local_38[2][2] = work->mYBBCamMtx[2][2]; local_38[2][3] = local_48.z; local_38[0][1] = local_38[0][2] = local_38[1][0] = local_38[2][0] = 0.0f; - GXLoadPosMtxImm(local_38, 0); + GXLoadPosMtxImm(local_38, GX_PNMTX0); p_prj[work->mPrjType](work, local_38); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); } @@ -517,7 +565,7 @@ void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { local_38[2][1] = local_94 * fVar1; local_38[2][2] = local_90; local_38[2][3] = local_48.z; - GXLoadPosMtxImm(local_38, 0); + GXLoadPosMtxImm(local_38, GX_PNMTX0); p_prj[work->mPrjType](work, local_38); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); } @@ -681,103 +729,197 @@ static u8* p_dl[2] = { jpa_dl_x, }; -void JPADrawDirection(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { - if (param_1->checkStatus(JPAPtclStts_Invisible)) { +#if TARGET_PC +void JPAInterpDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + JGeometry::TVec3 axisY; + JGeometry::TVec3 axisZ; + p_direction[work->mDirType](work, ptcl, &axisY); + + if (axisY.isZero()) { return; } - ZoneScoped; + axisY.normalize(); + axisZ.cross(ptcl->mBaseAxis, axisY); - JGeometry::TVec3 local_6c; - JGeometry::TVec3 local_78; - p_direction[param_0->mDirType](param_0, param_1, &local_6c); - - if (local_6c.isZero()) { + if (axisZ.isZero()) { return; } - local_6c.normalize(); - local_78.cross(param_1->mBaseAxis, local_6c); - - if (local_78.isZero()) { - return; - } - - local_78.normalize(); - param_1->mBaseAxis.cross(local_6c, local_78); - param_1->mBaseAxis.normalize(); - Mtx local_60; - f32 fVar1 = param_0->mGlobalPtclScl.x * param_1->mParticleScaleX; - f32 fVar2 = param_0->mGlobalPtclScl.y * param_1->mParticleScaleY; - local_60[0][0] = param_1->mBaseAxis.x; - local_60[0][1] = local_6c.x; - local_60[0][2] = local_78.x; - local_60[0][3] = param_1->mPosition.x; - local_60[1][0] = param_1->mBaseAxis.y; - local_60[1][1] = local_6c.y; - local_60[1][2] = local_78.y; - local_60[1][3] = param_1->mPosition.y; - local_60[2][0] = param_1->mBaseAxis.z; - local_60[2][1] = local_6c.z; - local_60[2][2] = local_78.z; - local_60[2][3] = param_1->mPosition.z; - p_plane[param_0->mPlaneType](local_60, fVar1, fVar2); - MTXConcat(param_0->mPosCamMtx, local_60, local_60); - GXLoadPosMtxImm(local_60, 0); - p_prj[param_0->mPrjType](param_0, local_60); - GXCallDisplayList(p_dl[param_0->mDLType], sizeof(jpa_dl)); + axisZ.normalize(); + ptcl->mBaseAxis.cross(axisY, axisZ); + ptcl->mBaseAxis.normalize(); + Mtx posMtx; + f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + posMtx[0][0] = ptcl->mBaseAxis.x; + posMtx[0][1] = axisY.x; + posMtx[0][2] = axisZ.x; + posMtx[0][3] = ptcl->mPosition.x; + posMtx[1][0] = ptcl->mBaseAxis.y; + posMtx[1][1] = axisY.y; + posMtx[1][2] = axisZ.y; + posMtx[1][3] = ptcl->mPosition.y; + posMtx[2][0] = ptcl->mBaseAxis.z; + posMtx[2][1] = axisY.z; + posMtx[2][2] = axisZ.z; + posMtx[2][3] = ptcl->mPosition.z; + p_plane[work->mPlaneType](posMtx, scaleX, scaleY); + dusk::frame_interp::record_final_mtx(posMtx, ptcl); } -void JPADrawRotDirection(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { - if (param_1->checkStatus(JPAPtclStts_Invisible)) { +void JPAInterpRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + f32 sinRot = JMASSin(ptcl->mRotateAngle); + f32 cosRot = JMASCos(ptcl->mRotateAngle); + JGeometry::TVec3 axisY; + JGeometry::TVec3 axisZ; + p_direction[work->mDirType](work, ptcl, &axisY); + + if (axisY.isZero()) { + return; + } + + axisY.normalize(); + axisZ.cross(ptcl->mBaseAxis, axisY); + + if (axisZ.isZero()) { + return; + } + + axisZ.normalize(); + ptcl->mBaseAxis.cross(axisY, axisZ); + ptcl->mBaseAxis.normalize(); + f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + Mtx mtx1; + Mtx mtx2; + p_rot[work->mRotType](sinRot, cosRot, mtx1); + p_plane[work->mPlaneType](mtx1, scaleX, scaleY); + mtx2[0][0] = ptcl->mBaseAxis.x; + mtx2[0][1] = axisY.x; + mtx2[0][2] = axisZ.x; + mtx2[0][3] = ptcl->mPosition.x; + mtx2[1][0] = ptcl->mBaseAxis.y; + mtx2[1][1] = axisY.y; + mtx2[1][2] = axisZ.y; + mtx2[1][3] = ptcl->mPosition.y; + mtx2[2][0] = ptcl->mBaseAxis.z; + mtx2[2][1] = axisY.z; + mtx2[2][2] = axisZ.z; + mtx2[2][3] = ptcl->mPosition.z; + MTXConcat(mtx2, mtx1, mtx1); + dusk::frame_interp::record_final_mtx(mtx1, ptcl); +} +#endif + +void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } ZoneScoped; - f32 sinRot = JMASSin(param_1->mRotateAngle); - f32 cosRot = JMASCos(param_1->mRotateAngle); - JGeometry::TVec3 local_6c; - JGeometry::TVec3 local_78; - p_direction[param_0->mDirType](param_0, param_1, &local_6c); + Mtx posMtx; +#if TARGET_PC + if (!dusk::frame_interp::lookup_replacement(ptcl, posMtx)) +#endif + { + JGeometry::TVec3 axisY; + JGeometry::TVec3 axisZ; + p_direction[work->mDirType](work, ptcl, &axisY); - if (local_6c.isZero()) { + if (axisY.isZero()) { + return; + } + + axisY.normalize(); + axisZ.cross(ptcl->mBaseAxis, axisY); + + if (axisZ.isZero()) { + return; + } + + axisZ.normalize(); + ptcl->mBaseAxis.cross(axisY, axisZ); + ptcl->mBaseAxis.normalize(); + f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + posMtx[0][0] = ptcl->mBaseAxis.x; + posMtx[0][1] = axisY.x; + posMtx[0][2] = axisZ.x; + posMtx[0][3] = ptcl->mPosition.x; + posMtx[1][0] = ptcl->mBaseAxis.y; + posMtx[1][1] = axisY.y; + posMtx[1][2] = axisZ.y; + posMtx[1][3] = ptcl->mPosition.y; + posMtx[2][0] = ptcl->mBaseAxis.z; + posMtx[2][1] = axisY.z; + posMtx[2][2] = axisZ.z; + posMtx[2][3] = ptcl->mPosition.z; + p_plane[work->mPlaneType](posMtx, scaleX, scaleY); + } + + MTXConcat(work->mPosCamMtx, posMtx, posMtx); + GXLoadPosMtxImm(posMtx, GX_PNMTX0); + p_prj[work->mPrjType](work, posMtx); + GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl)); +} + +void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } - local_6c.normalize(); - local_78.cross(param_1->mBaseAxis, local_6c); + ZoneScoped; - if (local_78.isZero()) { - return; + Mtx mtx1; + Mtx mtx2; +#if TARGET_PC + if (!dusk::frame_interp::lookup_replacement(ptcl, mtx1)) +#endif + { + f32 sinRot = JMASSin(ptcl->mRotateAngle); + f32 cosRot = JMASCos(ptcl->mRotateAngle); + JGeometry::TVec3 axisY; + JGeometry::TVec3 axisZ; + p_direction[work->mDirType](work, ptcl, &axisY); + + if (axisY.isZero()) { + return; + } + + axisY.normalize(); + axisZ.cross(ptcl->mBaseAxis, axisY); + + if (axisZ.isZero()) { + return; + } + + axisZ.normalize(); + ptcl->mBaseAxis.cross(axisY, axisZ); + ptcl->mBaseAxis.normalize(); + f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + p_rot[work->mRotType](sinRot, cosRot, mtx1); + p_plane[work->mPlaneType](mtx1, scaleX, scaleY); + mtx2[0][0] = ptcl->mBaseAxis.x; + mtx2[0][1] = axisY.x; + mtx2[0][2] = axisZ.x; + mtx2[0][3] = ptcl->mPosition.x; + mtx2[1][0] = ptcl->mBaseAxis.y; + mtx2[1][1] = axisY.y; + mtx2[1][2] = axisZ.y; + mtx2[1][3] = ptcl->mPosition.y; + mtx2[2][0] = ptcl->mBaseAxis.z; + mtx2[2][1] = axisY.z; + mtx2[2][2] = axisZ.z; + mtx2[2][3] = ptcl->mPosition.z; + MTXConcat(mtx2, mtx1, mtx1); } - - local_78.normalize(); - param_1->mBaseAxis.cross(local_6c, local_78); - param_1->mBaseAxis.normalize(); - f32 particleX = param_0->mGlobalPtclScl.x * param_1->mParticleScaleX; - f32 particleY = param_0->mGlobalPtclScl.y * param_1->mParticleScaleY; - Mtx auStack_80; - Mtx local_60; - p_rot[param_0->mRotType](sinRot, cosRot, auStack_80); - p_plane[param_0->mPlaneType](auStack_80, particleX, particleY); - local_60[0][0] = param_1->mBaseAxis.x; - local_60[0][1] = local_6c.x; - local_60[0][2] = local_78.x; - local_60[0][3] = param_1->mPosition.x; - local_60[1][0] = param_1->mBaseAxis.y; - local_60[1][1] = local_6c.y; - local_60[1][2] = local_78.y; - local_60[1][3] = param_1->mPosition.y; - local_60[2][0] = param_1->mBaseAxis.z; - local_60[2][1] = local_6c.z; - local_60[2][2] = local_78.z; - local_60[2][3] = param_1->mPosition.z; - MTXConcat(local_60, auStack_80, auStack_80); - MTXConcat(param_0->mPosCamMtx, auStack_80, local_60); - GXLoadPosMtxImm(local_60, 0); - p_prj[param_0->mPrjType](param_0, local_60); - GXCallDisplayList(p_dl[param_0->mDLType], sizeof(jpa_dl)); + MTXConcat(work->mPosCamMtx, mtx1, mtx2); + GXLoadPosMtxImm(mtx2, GX_PNMTX0); + p_prj[work->mPrjType](work, mtx2); + GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl)); } void JPADrawDBillboard(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { diff --git a/libs/JSystem/src/JParticle/JPAParticle.cpp b/libs/JSystem/src/JParticle/JPAParticle.cpp index a9294e0b4e..d3490ab294 100644 --- a/libs/JSystem/src/JParticle/JPAParticle.cpp +++ b/libs/JSystem/src/JParticle/JPAParticle.cpp @@ -204,6 +204,28 @@ void JPABaseParticle::init_c(JPAEmitterWorkData* work, JPABaseParticle* parent) mTexAnmIdx = 0; } +#if TARGET_PC +void JPABaseParticle::interp(JPAEmitterWorkData* work, void const* drawFunc) { + static bool enable = false; + if (!enable) + return; + + // don't interpolate the first frame + if (mAge == 0) + return; + + if (drawFunc == JPADrawBillboard) { + JPAInterpBillboard(work, this); + } else if (drawFunc == JPADrawRotBillboard) { + JPAInterpRotBillboard(work, this); + } else if (drawFunc == JPADrawDirection) { + JPAInterpDirection(work, this); + } else if (drawFunc == JPADrawRotDirection) { + JPAInterpRotDirection(work, this); + } +} +#endif + bool JPABaseParticle::calc_p(JPAEmitterWorkData* work) { if (++mAge >= mLifeTime) { return true; @@ -247,6 +269,17 @@ bool JPABaseParticle::calc_p(JPAEmitterWorkData* work) { mOffsetPosition.y + mLocalPosition.y * work->mPublicScale.y, mOffsetPosition.z + mLocalPosition.z * work->mPublicScale.z); +#if TARGET_PC + JPABaseShape* pBsp = work->mpRes->getBsp(); + work->mGlobalPtclScl.x = work->mpEmtr->mGlobalPScl.x * pBsp->getBaseSizeX(); + work->mGlobalPtclScl.y = work->mpEmtr->mGlobalPScl.y * pBsp->getBaseSizeY(); + work->mDirType = pBsp->getDirType(); + work->mRotType = pBsp->getRotType(); + work->mDLType = pBsp->getType() == 4 || pBsp->getType() == 8; + work->mPlaneType = work->mDLType ? 2 : pBsp->getBasePlaneType(); + interp(work, (void const*)work->mpRes->mpDrawParticleFuncList[0]); +#endif + return false; } @@ -289,6 +322,23 @@ bool JPABaseParticle::calc_c(JPAEmitterWorkData* work) { mOffsetPosition.y + mLocalPosition.y * work->mPublicScale.y, mOffsetPosition.z + mLocalPosition.z * work->mPublicScale.z); +#if TARGET_PC + JPABaseShape* pBsp = work->mpRes->getBsp(); + JPAChildShape* pCsp = work->mpRes->getCsp(); + if (pCsp->isScaleInherited()) { + work->mGlobalPtclScl.x = work->mpEmtr->mGlobalPScl.x * pBsp->getBaseSizeX(); + work->mGlobalPtclScl.y = work->mpEmtr->mGlobalPScl.y * pBsp->getBaseSizeY(); + } else { + work->mGlobalPtclScl.x = work->mpEmtr->mGlobalPScl.x * pCsp->getScaleX(); + work->mGlobalPtclScl.y = work->mpEmtr->mGlobalPScl.y * pCsp->getScaleY(); + } + work->mDirType = pCsp->getDirType(); + work->mRotType = pCsp->getRotType(); + work->mDLType = pCsp->getType() == 4 || pCsp->getType() == 8; + work->mPlaneType = work->mDLType ? 2 : pCsp->getBasePlaneType(); + interp(work, (void const*)work->mpRes->mpDrawParticleChildFuncList[0]); +#endif + return false; } diff --git a/libs/JSystem/src/JUtility/JUTFader.cpp b/libs/JSystem/src/JUtility/JUTFader.cpp index 599ae22720..e7d33454b7 100644 --- a/libs/JSystem/src/JUtility/JUTFader.cpp +++ b/libs/JSystem/src/JUtility/JUTFader.cpp @@ -8,6 +8,10 @@ #include "JSystem/JUtility/JUTFader.h" #include "JSystem/J2DGraph/J2DOrthoGraph.h" +#ifdef TARGET_PC +#include +#endif + JUTFader::JUTFader(int x, int y, int width, int height, JUtility::TColor pColor) : mColor(pColor), mBox(x, y, x + width, y + height) { mStatus = None; @@ -63,14 +67,24 @@ void JUTFader::advance() { void JUTFader::control() { advance(); -#ifndef TARGET_PC - // FRAME INTERP NOTE: Draw is called by JFWDisplay when interpolation is active draw(); -#endif } void JUTFader::draw() { if (mColor.a != 0) { +#ifdef TARGET_PC + if (dusk::frame_interp::is_enabled() && mDuration != 0) { + const auto step = dusk::frame_interp::get_interpolation_step(); + const auto progress = static_cast(mTimer) / static_cast(mDuration); + const auto timer = mTimer - 1 + step + progress; + auto alpha = timer / mDuration; + if (mStatus == FadeIn) { + alpha = 1.0f - alpha; + } + alpha = std::clamp(alpha, 0.0f, 1.0f); + mColor.a = static_cast(alpha * 255.0f); + } +#endif J2DOrthoGraph orthograph; orthograph.setColor(mColor); orthograph.fillBox(mBox); diff --git a/libs/JSystem/src/JUtility/JUTPalette.cpp b/libs/JSystem/src/JUtility/JUTPalette.cpp index d98bf98c2e..9c8a51432c 100644 --- a/libs/JSystem/src/JUtility/JUTPalette.cpp +++ b/libs/JSystem/src/JUtility/JUTPalette.cpp @@ -38,3 +38,9 @@ bool JUTPalette::load() { return check; } + +#if TARGET_PC +void JUTPalette::dataUploaded() { + GXInitTlutObjData(&mTlutObj, (void*)mColorTable); +} +#endif diff --git a/libs/JSystem/src/JUtility/JUTTexture.cpp b/libs/JSystem/src/JUtility/JUTTexture.cpp index 060c581e80..1d799c2820 100644 --- a/libs/JSystem/src/JUtility/JUTTexture.cpp +++ b/libs/JSystem/src/JUtility/JUTTexture.cpp @@ -27,7 +27,7 @@ void JUTTexture::storeTIMG(ResTIMG const* param_0, u8 param_1) { mTexData = (void*)((intptr_t)mTexInfo + 0x20); } - field_0x2c = NULL; + mPalette = NULL; mTlutName = 0; mWrapS = mTexInfo->wrapS; mWrapT = mTexInfo->wrapT; @@ -95,7 +95,7 @@ void JUTTexture::storeTIMG(ResTIMG const* param_0, JUTPalette* param_1, GXTlut p } mEmbPalette = param_1; setEmbPaletteDelFlag(false); - field_0x2c = NULL; + mPalette = NULL; if (param_1 != NULL) { mTlutName = param_2; if (param_2 != param_1->getTlutName()) { @@ -120,11 +120,11 @@ void JUTTexture::storeTIMG(ResTIMG const* param_0, JUTPalette* param_1, GXTlut p void JUTTexture::attachPalette(JUTPalette* param_0) { if (mTexInfo->indexTexture) { if (param_0 == NULL && mEmbPalette != NULL) { - field_0x2c = mEmbPalette; + mPalette = mEmbPalette; } else { - field_0x2c = param_0; + mPalette = param_0; } - initTexObj(field_0x2c->getTlutName()); + initTexObj(mPalette->getTlutName()); } } @@ -133,9 +133,9 @@ void JUTTexture::init() { initTexObj(); } else { if (mEmbPalette != NULL) { - field_0x2c = mEmbPalette; + mPalette = mEmbPalette; - initTexObj(field_0x2c->getTlutName()); + initTexObj(mPalette->getTlutName()); } else { OS_REPORT("This texture is CI-Format, but EmbPalette is NULL.\n"); } @@ -179,8 +179,8 @@ void JUTTexture::initTexObj(GXTlut param_0) { } void JUTTexture::load(GXTexMapID param_0) { - if (field_0x2c) { - field_0x2c->load(); + if (mPalette) { + mPalette->load(); } GXLoadTexObj(&mTexObj, param_0); } diff --git a/platforms/android/README.md b/platforms/android/README.md index c7ecd684c1..f51b37db73 100644 --- a/platforms/android/README.md +++ b/platforms/android/README.md @@ -66,12 +66,11 @@ Output APK: You can pass command-line args through the activity intent: ```bash -adb shell am start -n com.twilitrealm.dusk/.DuskActivity \ - --es dusk_args "'/sdcard/Download/The Legend of Zelda: Twilight Princess (USA).iso'" +adb shell am start -n dev.twilitrealm.dusk/.DuskActivity \ + --es dusk_args "--backend vulkan" ``` Supported extras: - `dusk_args`: single shell-like argument string - `dusk_argv`: string-array argv -- `dusk_disc`: compatibility shortcut (single ISO path) diff --git a/platforms/android/app/build.gradle b/platforms/android/app/build.gradle index 9375d06910..b7775df8e0 100644 --- a/platforms/android/app/build.gradle +++ b/platforms/android/app/build.gradle @@ -2,12 +2,22 @@ plugins { id 'com.android.application' } +def duskRepoDir = rootProject.projectDir.parentFile.parentFile +def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusk').get().asFile +def syncDuskAssets = tasks.register('syncDuskAssets', Sync) { + from(new File(duskRepoDir, 'res')) { + into 'res' + exclude '**/.DS_Store' + } + into duskGeneratedAssetsDir +} + android { - namespace 'com.twilitrealm.dusk' + namespace 'dev.twilitrealm.dusk' compileSdk 36 defaultConfig { - applicationId 'com.twilitrealm.dusk' + applicationId 'dev.twilitrealm.dusk' minSdk 26 targetSdk 36 versionCode 1 @@ -27,7 +37,7 @@ android { sourceSets { main { jniLibs.srcDirs = ['src/main/jniLibs'] - assets.srcDirs = ['../../assets'] + assets.srcDirs = [duskGeneratedAssetsDir] } } @@ -48,3 +58,10 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) } + +tasks.configureEach { task -> + if ((task.name.startsWith('merge') && task.name.endsWith('Assets')) || + task.name.toLowerCase().contains('lint')) { + task.dependsOn(syncDuskAssets) + } +} diff --git a/platforms/android/app/proguard-rules.pro b/platforms/android/app/proguard-rules.pro index 952161ca8b..72b7ca16fa 100644 --- a/platforms/android/app/proguard-rules.pro +++ b/platforms/android/app/proguard-rules.pro @@ -1,2 +1,4 @@ # Keep SDL activity and related JNI bridge methods. -keep class org.libsdl.app.** { *; } +-keep class dev.twilitrealm.dusk.DuskHttpClient { *; } +-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; } diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml index e8c2bb29a8..f5a0365856 100644 --- a/platforms/android/app/src/main/AndroidManifest.xml +++ b/platforms/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + + + + + + + out = new ArrayList<>(); StringBuilder current = new StringBuilder(); @@ -61,18 +71,46 @@ public class DuskActivity extends SDLActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + hideSystemBars(); + } + @Override + protected void onResume() { + super.onResume(); + hideSystemBars(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + hideSystemBars(); + } + } + + private void hideSystemBars() { + Window window = getWindow(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); - }else { - View decorView = getWindow().getDecorView(); - // Hide the status bar. - int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN; + window.setDecorFitsSystemWindows(false); + WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController(); + if (ctrl != null) { + ctrl.setSystemBarsBehavior( + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + ctrl.hide(WindowInsets.Type.systemBars()); + } + } else { + View decorView = window.getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE; decorView.setSystemUiVisibility(uiOptions); - // Remember that you should never show the action bar if the - // status bar is hidden, so hide that too if necessary. ActionBar actionBar = getActionBar(); - actionBar.hide(); + if (actionBar != null) { + actionBar.hide(); + } } } @@ -100,12 +138,96 @@ public class DuskActivity extends SDLActivity { return splitArgs(trimmed); } } - - String discPath = intent.getStringExtra("dusk_disc"); - if (discPath != null && !discPath.isEmpty()) { - return new String[] { discPath }; - } } return new String[0]; } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + persistUriPermissions(data); + } + super.onActivityResult(requestCode, resultCode, data); + } + + private void persistUriPermissions(Intent data) { + if (data == null) { + return; + } + + int permissionFlags = + data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (permissionFlags == 0) { + return; + } + + Uri uri = data.getData(); + if (uri != null) { + persistUriPermission(uri, permissionFlags); + } + + ClipData clipData = data.getClipData(); + if (clipData == null) { + return; + } + for (int i = 0; i < clipData.getItemCount(); ++i) { + Uri itemUri = clipData.getItemAt(i).getUri(); + if (itemUri != null) { + persistUriPermission(itemUri, permissionFlags); + } + } + } + + private void persistUriPermission(Uri uri, int permissionFlags) { + if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) { + persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read"); + } + if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { + persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write"); + } + } + + private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) { + try { + getContentResolver().takePersistableUriPermission(uri, permissionFlag); + } catch (SecurityException | IllegalArgumentException e) { + Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e); + } + } + + public String getDisplayNameForUri(String uriString) { + if (uriString == null || uriString.isEmpty()) { + return ""; + } + + Uri uri = Uri.parse(uriString); + if ("content".equals(uri.getScheme())) { + try (Cursor cursor = getContentResolver().query( + uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null)) + { + if (cursor != null && cursor.moveToFirst()) { + int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (displayNameColumn >= 0) { + String displayName = cursor.getString(displayNameColumn); + if (displayName != null && !displayName.isEmpty()) { + return displayName; + } + } + } + } catch (SecurityException | IllegalArgumentException e) { + Log.w(TAG, "Unable to query display name for " + uri, e); + } + } else if ("file".equals(uri.getScheme())) { + String path = uri.getPath(); + if (path != null && !path.isEmpty()) { + String name = new File(path).getName(); + if (!name.isEmpty()) { + return name; + } + } + } + + String lastSegment = uri.getLastPathSegment(); + return lastSegment != null ? lastSegment : ""; + } } diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java new file mode 100644 index 0000000000..79fa8894d0 --- /dev/null +++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java @@ -0,0 +1,413 @@ +package dev.twilitrealm.dusk; + +import android.content.ContentResolver; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; +import android.provider.DocumentsContract.Root; +import android.provider.DocumentsProvider; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +public class DuskDocumentsProvider extends DocumentsProvider { + public static final String AUTHORITY = "dev.twilitrealm.dusk.documents"; + + private static final String ROOT_ID = "dusk"; + private static final String ROOT_DOCUMENT_ID = "root"; + private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR; + + private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { + Root.COLUMN_ROOT_ID, + Root.COLUMN_FLAGS, + Root.COLUMN_TITLE, + Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_ICON, + Root.COLUMN_AVAILABLE_BYTES, + Root.COLUMN_SUMMARY + }; + + private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_FLAGS, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_SIZE + }; + + @Override + public boolean onCreate() { + ensureUserDirectories(); + return true; + } + + @Override + public Cursor queryRoots(String[] projection) throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); + final File root = getRootDirectory(); + final MatrixCursor.RowBuilder row = result.newRow(); + + row.add(Root.COLUMN_ROOT_ID, ROOT_ID); + row.add(Root.COLUMN_FLAGS, + Root.FLAG_LOCAL_ONLY | + Root.FLAG_SUPPORTS_CREATE | + Root.FLAG_SUPPORTS_IS_CHILD); + row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name)); + row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID); + row.add(Root.COLUMN_ICON, R.mipmap.icon); + row.add(Root.COLUMN_AVAILABLE_BYTES, root.getFreeSpace()); + row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.documents_provider_summary)); + + return result; + } + + @Override + public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); + includeDocument(result, documentId, getFileForDocumentId(documentId)); + return result; + } + + @Override + public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) + throws FileNotFoundException + { + return queryChildDocumentsInternal(parentDocumentId, projection); + } + + @Override + public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs) + throws FileNotFoundException + { + return queryChildDocumentsInternal(parentDocumentId, projection); + } + + private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection) + throws FileNotFoundException + { + final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); + final File parent = getFileForDocumentId(parentDocumentId); + final File[] files = parent.listFiles(); + result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId)); + + if (files == null) { + return result; + } + + for (File file : files) { + includeDocument(result, getDocumentIdForFile(file), file); + } + + return result; + } + + @Override + public boolean isChildDocument(String parentDocumentId, String documentId) { + try { + final File parent = getFileForDocumentId(parentDocumentId); + final File child = getFileForDocumentId(documentId); + return isInside(parent, child); + } catch (FileNotFoundException e) { + return false; + } + } + + @Override + public String createDocument(String parentDocumentId, String mimeType, String displayName) + throws FileNotFoundException + { + final File parent = getFileForDocumentId(parentDocumentId); + if (!parent.isDirectory()) { + throw new FileNotFoundException("Parent is not a directory: " + parentDocumentId); + } + + final String safeDisplayName = sanitizeDisplayName(displayName); + final File file = buildUniqueFile(parent, safeDisplayName); + final boolean created; + if (DIRECTORY_MIME_TYPE.equals(mimeType)) { + created = file.mkdir(); + } else { + try { + created = file.createNewFile(); + } catch (IOException e) { + throw asFileNotFound("Unable to create document", e); + } + } + + if (!created) { + throw new FileNotFoundException("Unable to create document: " + displayName); + } + + notifyChildrenChanged(parentDocumentId); + return getDocumentIdForFile(file); + } + + @Override + public String renameDocument(String documentId, String displayName) throws FileNotFoundException { + final File file = getFileForDocumentId(documentId); + if (ROOT_DOCUMENT_ID.equals(documentId)) { + throw new FileNotFoundException("Cannot rename root document"); + } + + final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName)); + final String parentDocumentId = getDocumentIdForFile(file.getParentFile()); + if (!file.renameTo(target)) { + throw new FileNotFoundException("Unable to rename document: " + documentId); + } + notifyDocumentChanged(documentId); + notifyDocumentChanged(getDocumentIdForFile(target)); + notifyChildrenChanged(parentDocumentId); + return getDocumentIdForFile(target); + } + + @Override + public void deleteDocument(String documentId) throws FileNotFoundException { + if (ROOT_DOCUMENT_ID.equals(documentId)) { + throw new FileNotFoundException("Cannot delete root document"); + } + + final File file = getFileForDocumentId(documentId); + final String parentDocumentId = getDocumentIdForFile(file.getParentFile()); + deleteRecursively(file); + notifyDocumentChanged(documentId); + notifyChildrenChanged(parentDocumentId); + } + + @Override + public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) + throws FileNotFoundException + { + return ParcelFileDescriptor.open(getFileForDocumentId(documentId), modeToParcelMode(mode)); + } + + @Override + public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint, + CancellationSignal signal) throws FileNotFoundException + { + throw new FileNotFoundException("Thumbnails are not supported"); + } + + private void includeDocument(MatrixCursor result, String documentId, File file) throws FileNotFoundException { + final MatrixCursor.RowBuilder row = result.newRow(); + final boolean isDirectory = file.isDirectory(); + final String displayName = ROOT_DOCUMENT_ID.equals(documentId) + ? getContext().getString(R.string.documents_provider_root_name) + : file.getName(); + + int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME; + if (isDirectory) { + flags |= Document.FLAG_DIR_SUPPORTS_CREATE; + } else if (file.canWrite()) { + flags |= Document.FLAG_SUPPORTS_WRITE; + } + if (ROOT_DOCUMENT_ID.equals(documentId)) { + flags &= ~(Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME); + } + + row.add(Document.COLUMN_DOCUMENT_ID, documentId); + row.add(Document.COLUMN_DISPLAY_NAME, displayName); + row.add(Document.COLUMN_FLAGS, flags); + row.add(Document.COLUMN_MIME_TYPE, isDirectory ? DIRECTORY_MIME_TYPE : getMimeType(file)); + row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified()); + row.add(Document.COLUMN_SIZE, isDirectory ? null : file.length()); + } + + private File getRootDirectory() throws FileNotFoundException { + final File root = getContext().getFilesDir(); + if (root == null) { + throw new FileNotFoundException("Dusk files directory is unavailable"); + } + return root; + } + + private File getFileForDocumentId(String documentId) throws FileNotFoundException { + final File root = getRootDirectory(); + if (ROOT_DOCUMENT_ID.equals(documentId)) { + return root; + } + if (!documentId.startsWith(ROOT_DOCUMENT_ID + "/")) { + throw new FileNotFoundException("Invalid document id: " + documentId); + } + + final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1); + final File file = new File(root, relativePath); + if (!isInside(root, file)) { + throw new FileNotFoundException("Document escapes Dusk files directory: " + documentId); + } + if (!file.exists()) { + throw new FileNotFoundException("Document does not exist: " + documentId); + } + return file; + } + + private String getDocumentIdForFile(File file) throws FileNotFoundException { + final File root = getRootDirectory(); + if (sameFile(root, file)) { + return ROOT_DOCUMENT_ID; + } + if (!isInside(root, file)) { + throw new FileNotFoundException("File escapes Dusk files directory: " + file); + } + + final String rootPath = canonicalPath(root); + final String filePath = canonicalPath(file); + return ROOT_DOCUMENT_ID + "/" + filePath.substring(rootPath.length() + 1); + } + + private void ensureUserDirectories() { + final File root = getContext().getFilesDir(); + if (root == null) { + return; + } + new File(root, "texture_replacements").mkdirs(); + new File(root, "USA/Card A").mkdirs(); + new File(root, "EUR/Card A").mkdirs(); + } + + private static String[] resolveRootProjection(String[] projection) { + return projection != null ? projection : DEFAULT_ROOT_PROJECTION; + } + + private static String[] resolveDocumentProjection(String[] projection) { + return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; + } + + private static String sanitizeDisplayName(String displayName) throws FileNotFoundException { + if (displayName == null) { + throw new FileNotFoundException("Document name is empty"); + } + + final String sanitized = displayName.trim(); + if (sanitized.isEmpty() || ".".equals(sanitized) || "..".equals(sanitized) || + sanitized.contains("/") || sanitized.contains("\\")) + { + throw new FileNotFoundException("Invalid document name: " + displayName); + } + return sanitized; + } + + private static File buildUniqueFile(File parent, String displayName) { + File file = new File(parent, displayName); + if (!file.exists()) { + return file; + } + + final int dot = displayName.lastIndexOf('.'); + final String baseName = dot > 0 ? displayName.substring(0, dot) : displayName; + final String extension = dot > 0 ? displayName.substring(dot) : ""; + for (int i = 1; i < 100; ++i) { + file = new File(parent, baseName + " (" + i + ")" + extension); + if (!file.exists()) { + return file; + } + } + return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension); + } + + private static int modeToParcelMode(String mode) { + if ("r".equals(mode)) { + return ParcelFileDescriptor.MODE_READ_ONLY; + } + if ("w".equals(mode) || "wt".equals(mode)) { + return ParcelFileDescriptor.MODE_WRITE_ONLY | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE; + } + if ("wa".equals(mode)) { + return ParcelFileDescriptor.MODE_WRITE_ONLY | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_APPEND; + } + if ("rw".equals(mode)) { + return ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE; + } + if ("rwt".equals(mode)) { + return ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE; + } + return ParcelFileDescriptor.MODE_READ_ONLY; + } + + private static String getMimeType(File file) { + final int dot = file.getName().lastIndexOf('.'); + if (dot >= 0) { + final String extension = file.getName().substring(dot + 1).toLowerCase(); + final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mimeType != null) { + return mimeType; + } + } + return "application/octet-stream"; + } + + private Uri getChildDocumentsUri(String parentDocumentId) { + return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId); + } + + private void notifyChildrenChanged(String parentDocumentId) { + final ContentResolver resolver = getContext().getContentResolver(); + resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false); + } + + private void notifyDocumentChanged(String documentId) { + final ContentResolver resolver = getContext().getContentResolver(); + resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false); + } + + private static void deleteRecursively(File file) throws FileNotFoundException { + if (file.isDirectory()) { + final File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + } + if (!file.delete()) { + throw new FileNotFoundException("Unable to delete document: " + file); + } + } + + private static boolean isInside(File parent, File child) { + try { + final String parentPath = canonicalPath(parent); + final String childPath = canonicalPath(child); + return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator); + } catch (FileNotFoundException e) { + return false; + } + } + + private static boolean sameFile(File a, File b) { + try { + return canonicalPath(a).equals(canonicalPath(b)); + } catch (FileNotFoundException e) { + return false; + } + } + + private static String canonicalPath(File file) throws FileNotFoundException { + try { + return file.getCanonicalPath(); + } catch (IOException e) { + throw asFileNotFound("Unable to resolve path", e); + } + } + + private static FileNotFoundException asFileNotFound(String message, IOException cause) { + final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage()); + exception.initCause(cause); + return exception; + } +} diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java new file mode 100644 index 0000000000..be160d6a2d --- /dev/null +++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java @@ -0,0 +1,237 @@ +package dev.twilitrealm.dusk; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; + +public final class DuskHttpClient { + public static final int ERROR_NONE = 0; + public static final int ERROR_INVALID_URL = 1; + public static final int ERROR_UNSUPPORTED_SCHEME = 2; + public static final int ERROR_TIMEOUT = 3; + public static final int ERROR_TOO_LARGE = 4; + public static final int ERROR_NETWORK = 5; + + private static final int MAX_REDIRECTS = 5; + + public static final class Response { + public int error; + public String message; + public int statusCode; + public String[] headerNames; + public String[] headerValues; + public byte[] body; + + Response(int error, String message, int statusCode, String[] headerNames, + String[] headerValues, byte[] body) { + this.error = error; + this.message = message; + this.statusCode = statusCode; + this.headerNames = headerNames != null ? headerNames : new String[0]; + this.headerValues = headerValues != null ? headerValues : new String[0]; + this.body = body != null ? body : new byte[0]; + } + } + + private DuskHttpClient() { + } + + public static Response get(String url, String[] headerNames, String[] headerValues, + int timeoutMs, long maxBodyBytes) { + if (url == null || url.isEmpty()) { + return fail(ERROR_INVALID_URL, "URL is empty"); + } + + try { + URL currentUrl = new URL(url); + if (!isHttps(currentUrl)) { + return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported"); + } + + for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) { + HttpsURLConnection connection = + (HttpsURLConnection) currentUrl.openConnection(); + try { + connection.setRequestMethod("GET"); + connection.setConnectTimeout(timeoutMs); + connection.setReadTimeout(timeoutMs); + connection.setUseCaches(false); + connection.setInstanceFollowRedirects(false); + applyHeaders(connection, headerNames, headerValues); + + int statusCode = connection.getResponseCode(); + if (isRedirect(statusCode)) { + String location = connection.getHeaderField("Location"); + if (location == null || location.isEmpty()) { + return fail(ERROR_NETWORK, "Redirect response did not include Location", + statusCode, connection, new byte[0]); + } + + URL nextUrl = new URL(currentUrl, location); + if (!isHttps(nextUrl)) { + return fail(ERROR_UNSUPPORTED_SCHEME, + "Only https:// redirects are supported", statusCode, + connection, new byte[0]); + } + currentUrl = nextUrl; + continue; + } + + byte[] body = readBody(connection, statusCode, maxBodyBytes); + return success(statusCode, connection, body); + } catch (ResponseTooLargeException e) { + return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit", + safeStatusCode(connection), connection, e.partialBody); + } finally { + connection.disconnect(); + } + } + + return fail(ERROR_NETWORK, "Too many redirects"); + } catch (MalformedURLException e) { + return fail(ERROR_INVALID_URL, "Failed to parse URL"); + } catch (SocketTimeoutException e) { + return fail(ERROR_TIMEOUT, "Request timed out"); + } catch (IOException e) { + String message = e.getMessage(); + return fail(ERROR_NETWORK, message != null ? message : e.toString()); + } catch (ClassCastException e) { + return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported"); + } + } + + private static void applyHeaders(HttpsURLConnection connection, String[] names, + String[] values) { + if (names == null || values == null) { + return; + } + + int count = Math.min(names.length, values.length); + for (int i = 0; i < count; ++i) { + if (names[i] != null && values[i] != null) { + connection.setRequestProperty(names[i], values[i]); + } + } + } + + private static boolean isHttps(URL url) { + return "https".equalsIgnoreCase(url.getProtocol()); + } + + private static boolean isRedirect(int statusCode) { + return statusCode == HttpURLConnection.HTTP_MOVED_PERM || + statusCode == HttpURLConnection.HTTP_MOVED_TEMP || + statusCode == HttpURLConnection.HTTP_SEE_OTHER || + statusCode == 307 || + statusCode == 308; + } + + private static byte[] readBody(HttpsURLConnection connection, int statusCode, + long maxBodyBytes) throws IOException, + ResponseTooLargeException { + InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ? + connection.getErrorStream() : connection.getInputStream(); + if (stream == null) { + return new byte[0]; + } + + try (InputStream bodyStream = stream; + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + byte[] buffer = new byte[8192]; + long total = 0; + while (true) { + int read = bodyStream.read(buffer); + if (read < 0) { + return out.toByteArray(); + } + if (read == 0) { + continue; + } + if (read > maxBodyBytes || total > maxBodyBytes - read) { + throw new ResponseTooLargeException(out.toByteArray()); + } + out.write(buffer, 0, read); + total += read; + } + } + } + + private static int safeStatusCode(HttpsURLConnection connection) { + try { + return connection.getResponseCode(); + } catch (IOException e) { + return 0; + } + } + + private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) { + HeaderLists headers = readHeaders(connection); + return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body); + } + + private static Response fail(int error, String message) { + return new Response(error, message, 0, null, null, null); + } + + private static Response fail(int error, String message, int statusCode, + HttpsURLConnection connection, byte[] body) { + HeaderLists headers = readHeaders(connection); + return new Response(error, message, statusCode, headers.names, headers.values, body); + } + + private static HeaderLists readHeaders(HttpsURLConnection connection) { + List names = new ArrayList<>(); + List values = new ArrayList<>(); + + Map> headerFields = connection.getHeaderFields(); + if (headerFields == null) { + return new HeaderLists(new String[0], new String[0]); + } + + for (Map.Entry> entry : headerFields.entrySet()) { + String name = entry.getKey(); + if (name == null) { + continue; + } + List entryValues = entry.getValue(); + if (entryValues == null || entryValues.isEmpty()) { + names.add(name); + values.add(""); + continue; + } + for (String value : entryValues) { + names.add(name); + values.add(value != null ? value : ""); + } + } + + return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0])); + } + + private static final class HeaderLists { + final String[] names; + final String[] values; + + HeaderLists(String[] names, String[] values) { + this.names = names; + this.values = values; + } + } + + private static final class ResponseTooLargeException extends Exception { + final byte[] partialBody; + + ResponseTooLargeException(byte[] partialBody) { + this.partialBody = partialBody; + } + } +} diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java index b91a8211b1..1fb2bfb4a7 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -256,6 +256,7 @@ public class HIDDeviceManager { 0x24c6, // PowerA 0x2c22, // Qanba 0x2dc8, // 8BitDo + 0x37d7, // Flydigi 0x9886, // ASTRO Gaming }; diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java index 9548c529c0..42f5a911f7 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh private static final String TAG = "SDL"; private static final int SDL_MAJOR_VERSION = 3; private static final int SDL_MINOR_VERSION = 4; - private static final int SDL_MICRO_VERSION = 2; + private static final int SDL_MICRO_VERSION = 4; /* // Display InputType.SOURCE/CLASS of events and devices // @@ -2032,7 +2032,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh try { ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode); return pfd != null ? pfd.detachFd() : -1; - } catch (FileNotFoundException e) { + } catch (FileNotFoundException | SecurityException e) { e.printStackTrace(); return -1; } @@ -2227,4 +2227,3 @@ class SDLClipboardHandler implements SDLActivity.onNativeClipboardChanged(); } } - diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java index 027b8fd3e5..fdc2994c15 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java @@ -65,17 +65,15 @@ class SDLInputConnection extends BaseInputConnection @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { - if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) { - // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection - // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 - if (beforeLength > 0 && afterLength == 0) { - // backspace(s) - while (beforeLength-- > 0) { - nativeGenerateScancodeForUnichar('\b'); - } - return true; - } - } + // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection + // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 + if (beforeLength > 0 && afterLength == 0) { + // backspace(s) + while (beforeLength-- > 0) { + nativeGenerateScancodeForUnichar('\b'); + } + return true; + } if (!super.deleteSurroundingText(beforeLength, afterLength)) { return false; diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java index 1579b73345..5ed335ac39 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java @@ -35,6 +35,8 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener, SensorEventListener, ScaleGestureDetector.OnScaleGestureListener { + private static native void auroraNativeSetSurfaceReady(boolean ready); + // Sensors protected SensorManager mSensorManager; protected Display mDisplay; @@ -96,6 +98,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, @Override public void surfaceCreated(SurfaceHolder holder) { Log.v("SDL", "surfaceCreated()"); + auroraNativeSetSurfaceReady(false); SDLActivity.onNativeSurfaceCreated(); } @@ -103,6 +106,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.v("SDL", "surfaceDestroyed()"); + auroraNativeSetSurfaceReady(false); // Transition to pause, if needed SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED; @@ -192,6 +196,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, /* Surface is ready */ mIsSurfaceReady = true; + auroraNativeSetSurfaceReady(true); SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED; SDLActivity.handleNativeState(); diff --git a/platforms/android/app/src/main/res/values/strings.xml b/platforms/android/app/src/main/res/values/strings.xml index 79d8812b82..752dae76bf 100644 --- a/platforms/android/app/src/main/res/values/strings.xml +++ b/platforms/android/app/src/main/res/values/strings.xml @@ -1,4 +1,6 @@ Dusk + Dusk Data + Saves, texture packs, settings, and logs diff --git a/platforms/android/gradlew b/platforms/android/gradlew old mode 100644 new mode 100755 diff --git a/platforms/android/scripts/stage-jni-libs.sh b/platforms/android/scripts/stage-jni-libs.sh old mode 100644 new mode 100755 index ae25522e4d..42b08c13e6 --- a/platforms/android/scripts/stage-jni-libs.sh +++ b/platforms/android/scripts/stage-jni-libs.sh @@ -14,9 +14,22 @@ if [[ -z "$ANDROID_NDK_VER" ]] && [[ -d "$ANDROID_HOME_DIR/ndk" ]]; then fi if [[ -n "$ANDROID_NDK_VER" ]]; then - TOOLCHAIN_BIN="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/bin" - if [[ -x "$TOOLCHAIN_BIN/llvm-strip" ]]; then - STRIP_TOOL="$TOOLCHAIN_BIN/llvm-strip" + case "$(uname -s)" in + Darwin) HOST_TAG="darwin-x86_64" ;; + Linux) HOST_TAG="linux-x86_64" ;; + *) HOST_TAG="" ;; + esac + + PREBUILT_DIR="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt" + if [[ -n "$HOST_TAG" && -x "$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" ]]; then + STRIP_TOOL="$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" + else + for candidate in "$PREBUILT_DIR"/*/bin/llvm-strip; do + if [[ -x "$candidate" ]]; then + STRIP_TOOL="$candidate" + break + fi + done fi fi @@ -25,29 +38,35 @@ copy_lib() { local src="$2" local dst_dir="$APP_DIR/$abi" local dst="$dst_dir/libmain.so" + local tmp="$dst_dir/.libmain.so.$$" + if [[ ! -f "$src" ]]; then + echo "Missing native library for $abi: $src" >&2 + exit 1 + fi + mkdir -p "$dst_dir" - cp -f "$src" "$dst" + cp -f "$src" "$tmp" if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then - "$STRIP_TOOL" --strip-debug "$dst" - echo "Staged and stripped $src -> $dst" + "$STRIP_TOOL" --strip-unneeded "$tmp" + mv -f "$tmp" "$dst" + echo "Stripped and staged $src -> $dst" else + mv -f "$tmp" "$dst" echo "Staged $src -> $dst (strip disabled or strip tool unavailable)" fi } -declare -A ABI_TO_LIB=( - ["arm64-v8a"]="$ROOT_DIR/build/android-arm64/libmain.so" - ["x86_64"]="$ROOT_DIR/build/android-x86_64/libmain.so" -) - # Drop any previously staged ABI directories to avoid stale APK contents. rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64" for abi in $ANDROID_STAGE_ABIS; do - src="${ABI_TO_LIB[$abi]:-}" - if [[ -z "$src" ]]; then - echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2 - exit 1 - fi + case "$abi" in + arm64-v8a) src="$ROOT_DIR/build/android-arm64/libmain.so" ;; + x86_64) src="$ROOT_DIR/build/android-x86_64/libmain.so" ;; + *) + echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2 + exit 1 + ;; + esac copy_lib "$abi" "$src" done diff --git a/platforms/freedesktop/1024x1024/apps/dusk.png b/platforms/freedesktop/1024x1024/apps/dusk.png index 862cbed0d8..33eceb7c37 100644 Binary files a/platforms/freedesktop/1024x1024/apps/dusk.png and b/platforms/freedesktop/1024x1024/apps/dusk.png differ diff --git a/platforms/freedesktop/128x128/apps/dusk.png b/platforms/freedesktop/128x128/apps/dusk.png index aaf38689d2..7d73dff804 100644 Binary files a/platforms/freedesktop/128x128/apps/dusk.png and b/platforms/freedesktop/128x128/apps/dusk.png differ diff --git a/platforms/freedesktop/16x16/apps/dusk.png b/platforms/freedesktop/16x16/apps/dusk.png index 21b653c63d..3680f3bf6a 100644 Binary files a/platforms/freedesktop/16x16/apps/dusk.png and b/platforms/freedesktop/16x16/apps/dusk.png differ diff --git a/platforms/freedesktop/256x256/apps/dusk.png b/platforms/freedesktop/256x256/apps/dusk.png index 1f4677878a..4fa9995913 100644 Binary files a/platforms/freedesktop/256x256/apps/dusk.png and b/platforms/freedesktop/256x256/apps/dusk.png differ diff --git a/platforms/freedesktop/32x32/apps/dusk.png b/platforms/freedesktop/32x32/apps/dusk.png index b879a01602..3d966b7cbf 100644 Binary files a/platforms/freedesktop/32x32/apps/dusk.png and b/platforms/freedesktop/32x32/apps/dusk.png differ diff --git a/platforms/freedesktop/48x48/apps/dusk.png b/platforms/freedesktop/48x48/apps/dusk.png index 4f3744ab89..e12bca0df9 100644 Binary files a/platforms/freedesktop/48x48/apps/dusk.png and b/platforms/freedesktop/48x48/apps/dusk.png differ diff --git a/platforms/freedesktop/512x512/apps/dusk.png b/platforms/freedesktop/512x512/apps/dusk.png index 6334f80e81..53afadf4a5 100644 Binary files a/platforms/freedesktop/512x512/apps/dusk.png and b/platforms/freedesktop/512x512/apps/dusk.png differ diff --git a/platforms/freedesktop/64x64/apps/dusk.png b/platforms/freedesktop/64x64/apps/dusk.png index 067a83da90..c0dd5502a5 100644 Binary files a/platforms/freedesktop/64x64/apps/dusk.png and b/platforms/freedesktop/64x64/apps/dusk.png differ diff --git a/platforms/freedesktop/dusk.desktop b/platforms/freedesktop/dusk.desktop index 2e19d7ea26..b052eac6ef 100644 --- a/platforms/freedesktop/dusk.desktop +++ b/platforms/freedesktop/dusk.desktop @@ -6,4 +6,4 @@ Exec=dusk Icon=dusk Terminal=false Type=Application -Categories=Graphics;3DGraphics;Game +Categories=Game; diff --git a/platforms/ios/Info.plist.in b/platforms/ios/Info.plist.in index 881d5aff35..395b29b7d7 100644 --- a/platforms/ios/Info.plist.in +++ b/platforms/ios/Info.plist.in @@ -79,5 +79,11 @@ CADisableMinimumFrameDurationOnPhone + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + + LSSupportsGameMode + diff --git a/platforms/macos/Info.plist.in b/platforms/macos/Info.plist.in index b7bc7b706c..aee7393ee8 100644 --- a/platforms/macos/Info.plist.in +++ b/platforms/macos/Info.plist.in @@ -28,5 +28,7 @@ ${MACOSX_BUNDLE_SHORT_VERSION_STRING} NSHighResolutionCapable + LSSupportsGameMode + diff --git a/platforms/tvos/Info.plist.in b/platforms/tvos/Info.plist.in index 841d120cc2..49ed85edd7 100644 --- a/platforms/tvos/Info.plist.in +++ b/platforms/tvos/Info.plist.in @@ -45,5 +45,7 @@ LaunchScreen UIUserInterfaceStyle Automatic + LSSupportsGameMode + diff --git a/res/AlegreyaSC-Bold.ttf b/res/AlegreyaSC-Bold.ttf new file mode 100644 index 0000000000..dc73beb916 Binary files /dev/null and b/res/AlegreyaSC-Bold.ttf differ diff --git a/res/AlegreyaSC-Regular.ttf b/res/AlegreyaSC-Regular.ttf new file mode 100644 index 0000000000..31cae6c8ad Binary files /dev/null and b/res/AlegreyaSC-Regular.ttf differ diff --git a/res/FiraSans-Bold.ttf b/res/FiraSans-Bold.ttf new file mode 100644 index 0000000000..e3593fb0f3 Binary files /dev/null and b/res/FiraSans-Bold.ttf differ diff --git a/res/FiraSans-Regular.ttf b/res/FiraSans-Regular.ttf new file mode 100644 index 0000000000..6f80647494 Binary files /dev/null and b/res/FiraSans-Regular.ttf differ diff --git a/res/FiraSansCondensed-Bold.ttf b/res/FiraSansCondensed-Bold.ttf new file mode 100644 index 0000000000..ec7e841549 Binary files /dev/null and b/res/FiraSansCondensed-Bold.ttf differ diff --git a/res/FiraSansCondensed-Regular.ttf b/res/FiraSansCondensed-Regular.ttf new file mode 100644 index 0000000000..6e1a192127 Binary files /dev/null and b/res/FiraSansCondensed-Regular.ttf differ diff --git a/res/MaterialSymbolsRounded-Regular.ttf b/res/MaterialSymbolsRounded-Regular.ttf new file mode 100644 index 0000000000..782fc0a406 Binary files /dev/null and b/res/MaterialSymbolsRounded-Regular.ttf differ diff --git a/res/logo-mascot.png b/res/logo-mascot.png new file mode 100644 index 0000000000..9f9a5d1ace Binary files /dev/null and b/res/logo-mascot.png differ diff --git a/res/logo-mascot.webp b/res/logo-mascot.webp deleted file mode 100644 index c22f3bc90f..0000000000 Binary files a/res/logo-mascot.webp and /dev/null differ diff --git a/res/org-icon-center.png b/res/org-icon-center.png new file mode 100644 index 0000000000..b3ec7e1f6b Binary files /dev/null and b/res/org-icon-center.png differ diff --git a/res/org-icon-inner.png b/res/org-icon-inner.png new file mode 100644 index 0000000000..4b3c864c74 Binary files /dev/null and b/res/org-icon-inner.png differ diff --git a/res/org-icon-outer.png b/res/org-icon-outer.png new file mode 100644 index 0000000000..6cbcf084d2 Binary files /dev/null and b/res/org-icon-outer.png differ diff --git a/res/org-icon.png b/res/org-icon.png index b7a0614b9b..288dc8768f 100644 Binary files a/res/org-icon.png and b/res/org-icon.png differ diff --git a/res/prelaunch-bg.png b/res/prelaunch-bg.png new file mode 100644 index 0000000000..045dcbe655 Binary files /dev/null and b/res/prelaunch-bg.png differ diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss new file mode 100644 index 0000000000..3ce4d51068 --- /dev/null +++ b/res/rml/overlay.rcss @@ -0,0 +1,291 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + overflow: visible; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: "Fira Sans"; + font-weight: normal; + font-size: 20dp; + color: #E0DBC8; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: stretch; + z-index: 1; + pointer-events: none; +} + +fps, +toast { + position: absolute; + border: 1dp #92875B; + background-color: rgba(21, 22, 16, 80%); +} + +toast { + top: 40dp; + right: 40dp; + display: flex; + flex-flow: column; + border-radius: 14dp; + overflow: hidden; + backdrop-filter: blur(5dp); + box-shadow: 0 0 15dp 3dp; + filter: opacity(0); + transform: scale(0.9); + transform-origin: center; + transition: filter transform 0.2s cubic-in-out; + padding: 18dp 24dp; + gap: 8dp; +} + +toast[open] { + filter: opacity(1); + transform: scale(1); +} + +/*toast:hover { + cursor: pointer; + background-color: rgba(61, 59, 36, 80%); +} + +toast:active { + background-color: rgba(45, 43, 26, 80%); +}*/ + +toast heading { + display: flex; + gap: 18dp; + align-items: center; + font-family: "Fira Sans Condensed"; + font-size: 18dp; + font-weight: bold; + text-transform: uppercase; + color: #92875B; +} + +toast heading > span { + flex: 1 0 auto; +} + +toast heading > row { + flex: 1 0 auto; + display: flex; + align-items: center; + gap: 4dp; +} + +toast message { + display: flex; + flex-flow: column; + gap: 8dp; +} + +toast message row { + display: flex; +} + +toast message row.muted { + opacity: 0.5; +} + +toast progress { + height: 4dp; + position: absolute; + left: 0; + bottom: 0; + width: 100%; +} + +toast progress fill { + background-color: rgba(194, 164, 45, 80%); +} + +toast.achievement { + border: 1dp #C2A42D; +} + +toast.achievement heading { + color: #C2A42D; +} + +toast.controller-warning { + top: auto; + right: auto; + bottom: 40dp; + left: 50%; + width: 440dp; + max-width: 90%; + transform: translateX(-50%) scale(0.9); +} + +toast.controller-warning[open] { + transform: translateX(-50%) scale(1); +} + +toast.controller-warning heading { + color: #C2A42D; +} + +toast.menu-notification { + top: 40dp; + right: auto; + bottom: auto; + left: 50%; + max-width: 90%; + transform: translateX(-50%) scale(0.9); +} + +toast.menu-notification[open] { + transform: translateX(-50%) scale(1); +} + +toast.menu-notification message { + align-items: center; + text-align: center; +} + +toast.menu-notification message row { + align-items: center; + gap: 6dp; +} + +icon { + font-family: "Material Symbols Rounded"; + font-weight: normal; + display: inline-block; + vertical-align: middle; +} + +icon.arrow-forward { + width: 1.2em; + height: 1.2em; + font-size: 1.2em; + decorator: text("" center center); +} + +icon.trophy { + width: 1.2em; + height: 1.2em; + font-size: 1.2em; + decorator: text("" center center); +} + +icon.controller { + width: 1.2em; + height: 1.2em; + font-size: 1.2em; + decorator: text("" center center); +} + +icon.warning { + width: 1.2em; + height: 1.2em; + font-size: 1.2em; + decorator: text("" center center); +} + +fps { + display: none; + z-index: 99; + font-size: 18dp; + font-weight: bold; + padding: 9dp 12dp; + border-radius: 7dp; + pointer-events: none; + white-space: nowrap; +} + +fps[open] { + display: block; +} + +fps[corner=tl] { + top: 12dp; + left: 12dp; +} + +fps[corner=tr] { + top: 12dp; + right: 12dp; +} + +fps[corner=bl] { + bottom: 12dp; + left: 12dp; +} + +fps[corner=br] { + bottom: 12dp; + right: 12dp; +} + +logo { + position: absolute; + width: 100dp; + height: 100dp; + bottom: 40dp; + left: 40dp; + opacity: 0; + transition: opacity 0.5s linear-in-out; +} + +logo[open] { + opacity: 0.65; +} + +logo img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + filter: drop-shadow(#0008 0 0 14dp); + transform-origin: center; +} + +logo img.inner { + animation: 24s linear-in-out infinite logo-inner-spin; +} + +logo img.outer { + animation: 8s linear-in-out infinite logo-outer-spin; +} + +@keyframes logo-inner-spin { + from { + transform: rotate(360deg); + } + to { + transform: rotate(0deg); + } +} + +@keyframes logo-outer-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (max-height: 640dp) { + toast { + top: 20dp; + right: 20dp; + } + + toast.controller-warning { + bottom: 20dp; + } + + toast.menu-notification { + top: 20dp; + } +} diff --git a/res/rml/popup.rcss b/res/rml/popup.rcss new file mode 100644 index 0000000000..effc80344d --- /dev/null +++ b/res/rml/popup.rcss @@ -0,0 +1,45 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + overflow: visible; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 18dp; + color: #E0DBC8; +} + +button { + cursor: pointer; + focus: auto; +} + +popup { + width: 100%; + display: flex; + align-items: stretch; + height: 64dp; + background-color: rgba(21, 22, 16, 80%); + border-bottom: 2dp #92875B; + backdrop-filter: blur(5dp); + transform: translateY(-64dp); + transition: transform 0.2s cubic-in-out; +} + +popup[open] { + transform: translateY(0); +} + +popup tab-bar { + flex: 1 1 0; +} + +popup tab-bar tab { + opacity: 0.35; + color: #E0DBC8; +} diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss new file mode 100644 index 0000000000..707d8b8234 --- /dev/null +++ b/res/rml/prelaunch.rcss @@ -0,0 +1,497 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + width: 100%; + height: 100%; + font-family: "Fira Sans"; + font-weight: normal; + font-size: 20dp; + color: #FFFFFF; + filter: opacity(0); + transition: filter 1s 0.2s linear-in-out; + z-index: -1; +} + +.gradient { + position: absolute; + width: 100%; + height: 100%; + /* The color gradient from the Figma bands really badly. A fully black gradient does as well, but not as badly. */ + decorator: horizontal-gradient(#000000FF #00000000); +} + +body.mirrored .gradient { + decorator: horizontal-gradient(#00000000 #000000FF); +} + +.background { + position: absolute; + width: 100%; + height: 100%; + decorator: image(../prelaunch-bg.png cover left center); + opacity: 0; + transition: opacity 1s linear-in-out; +} + +body[open] { + filter: opacity(1); +} + +body[open] .background { + opacity: 1; +} + +body.disc-ready .background { + opacity: 0; +} + +content { + display: block; + width: 100%; + height: 100%; + filter: opacity(0); + transition: filter 0.2s linear-in-out; +} + +content[open] { + filter: opacity(1); +} + +menu { + position: absolute; + left: 96dp; + right: auto; + top: 50%; + transform: translateY(-50%); + /* Scale based on a reference screen width, 428/1216 */ + width: 35.230264vw; + min-width: 428dp; + max-width: 856dp; + height: auto; + display: flex; + flex-direction: column; + gap: 48dp; +} + +body.mirrored menu { + left: auto; + right: 96dp; +} + +hero { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 8dp; +} + +body.mirrored hero { + align-items: flex-end; +} + +hero img { + width: 100%; +} + +.eyebrow { + font-family: "Alegreya SC"; + font-size: 32dp; +} + +@media (min-width: 1216dp) { + .eyebrow { + /* Same logic as .menu, 32/1216 */ + font-size: 2.631579vw; + } +} + +.eyebrow span { + font-weight: bold; +} + +#menu-list { + display: flex; + flex-direction: column; + gap: 12dp; + align-items: flex-start; +} + +#menu-list button { + width: 428dp; + height: 54dp; + padding: 8dp 16dp; + border-radius: 8dp; + text-align: left; + text-transform: uppercase; + font-family: "Fira Sans Condensed"; + font-size: 32dp; + font-weight: normal; + cursor: pointer; + /* Define a fully transparent gradient as the default state, otherwise a white flash occurs */ + decorator: horizontal-gradient(#00000000 #00000000); +} + +#menu-list button:disabled { + opacity: 0.75; + cursor: default; + decorator: horizontal-gradient(#00000000 #00000000); +} + +#menu-list button.anim-done { + transition: decorator color opacity 0.1s linear-in-out; +} + +#menu-list button:hover, +#menu-list button:focus-visible { + color: black; + decorator: horizontal-gradient(#FEE685FF #FEE68500); +} + +body.mirrored #menu-list { + align-items: flex-end; +} + +body.mirrored #menu-list button { + text-align: right; +} + +body.mirrored #menu-list button:hover, +body.mirrored #menu-list button:focus-visible { + decorator: horizontal-gradient(#FEE68500 #FEE685FF); +} + +disc-info { + position: absolute; + left: 96dp; + right: auto; + bottom: 72dp; + display: flex; + flex-direction: column; + gap: 12dp; + font-size: 24dp; + font-effect: glow(0dp 4dp 0dp 4dp black); + text-align: left; +} + +body.mirrored disc-info { + left: auto; + right: 96dp; + text-align: right; +} + +version-info { + position: absolute; + right: 96dp; + left: auto; + bottom: 72dp; + display: flex; + flex-direction: column; + gap: 12dp; + text-align: right; + font-size: 24dp; + font-effect: glow(0dp 4dp 0dp 4dp black); + text-align: right; +} + +body.mirrored version-info { + right: auto; + left: 96dp; + text-align: left; +} + +#disc-status { + display: flex; + align-items: center; + gap: 8dp; +} + +#disc-status[status=good] { + color: #D8F999; +} + +#disc-status[status=bad] { + color: #FFC9C9; +} + +#disc-status[status=verifying] { + color: #FFFFFF; +} + +#disc-status[status=mismatch] { + color: #FFD6A7; +} + +#disc-status[status=unknown] { + color: rgba(224, 219, 200, 65%); +} + +#disc-status[status=pending] { + color: #FEE685; +} + +#disc-status icon { + display: none; + width: 24dp; + height: 24dp; + font-family: "Material Symbols Rounded"; + font-weight: normal; + font-size: 24dp; +} + +#disc-status[status] icon { + display: block; +} + +#disc-status[status=good] icon { + decorator: text("" center center); +} + +#disc-status[status=bad] icon { + decorator: text("" center center); +} + +#disc-status[status=verifying] icon { + decorator: text("" center center); +} + +#disc-status[status=mismatch] icon { + decorator: text("" center center); +} + +#disc-status[status=unknown] icon { + decorator: text("" center center); +} + +#disc-status[status=pending] icon { + decorator: text("" center center); +} + +#disc-version { + font-size: 20dp; +} + +.update { + display: none; + color: #A6A09B; + align-items: center; + justify-content: flex-end; + gap: 8dp; + font-size: 20dp; +} + +.update[state=checking], +.update[state=failed] { + display: block; +} + +.update[state=available] { + display: flex; +} + +#update-download { + display: none; + margin: 0dp; + padding: 0dp; + border-width: 0dp; + background-color: transparent; + color: #D8F999; + cursor: pointer; + text-transform: uppercase; + font-weight: bold; + decorator: horizontal-gradient(#00000000 #00000000); +} + +.update[state=available] #update-download { + display: flex; + align-items: center; + gap: 2dp; +} + +#update-download icon { + display: block; + width: 18dp; + height: 18dp; + font-family: "Material Symbols Rounded"; + font-weight: normal; + decorator: text("" center center); +} + +.detail { + color: #A6A09B; +} + +body.mirrored .update { + justify-content: flex-start; +} + +/* Startup animation */ +.intro-item { + opacity: 0; + transform: translateY(10dp); + transition: opacity transform 0.3s 0.1s cubic-in-out; +} + +body.animate-in .intro-item { + opacity: 1; + transform: translateY(0dp); +} + +.delay-0 { + transition: opacity transform 0.3s 0.1s cubic-in-out; +} + +.delay-1 { + transition: opacity transform 0.3s 0.2s cubic-in-out; +} + +.delay-2 { + transition: opacity transform 0.3s 0.3s cubic-in-out; +} + +.delay-3 { + transition: opacity transform 0.3s 0.4s cubic-in-out; +} + +.delay-4 { + transition: opacity transform 0.3s 0.5s cubic-in-out; +} + +.delay-5 { + transition: opacity transform 0.3s 0.6s cubic-in-out; +} + +/* Mobile layout */ +@media (max-height: 640dp) { + .gradient { + decorator: horizontal-gradient(#00000000 #000000FF); + } + + body.mirrored .gradient { + decorator: horizontal-gradient(#000000FF #00000000); + } + + menu { + left: 32dp; + right: 32dp; + width: auto; + min-width: 0; + max-width: none; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 16dp; + } + + body.mirrored menu { + left: 32dp; + right: 32dp; + flex-direction: row-reverse; + } + + hero { + flex: 1 1 0; + min-width: 0; + max-width: 48%; + margin-left: 32dp; + } + + body.mirrored hero { + align-items: flex-end; + } + + hero img { + width: 100%; + } + + #menu-list { + flex: 1 1 0; + min-width: 0; + max-width: 52%; + align-items: flex-end; + } + + #menu-list button { + width: 100%; + max-width: 100%; + text-align: right; + } + + #menu-list button:hover, + #menu-list button:focus-visible { + decorator: horizontal-gradient(#FEE68500 #FEE685FF); + } + + body.mirrored #menu-list { + align-items: flex-start; + } + + body.mirrored #menu-list button { + text-align: left; + } + + body.mirrored #menu-list button:hover, + body.mirrored #menu-list button:focus-visible { + decorator: horizontal-gradient(#FEE685FF #FEE68500); + } + + .eyebrow { + display: none; + } + + disc-info { + right: 32dp; + left: auto; + bottom: 32dp; + top: auto; + text-align: right; + font-size: 16dp; + } + + #disc-status { + justify-content: flex-end; + } + + #disc-status icon { + font-size: 20dp; + } + + #disc-version { + font-size: 16dp; + } + + version-info { + right: 32dp; + left: auto; + bottom: auto; + top: 32dp; + text-align: right; + font-size: 16dp; + } + + .update { + font-size: 16dp; + } + + body.mirrored disc-info { + right: auto; + left: 32dp; + bottom: 32dp; + top: auto; + text-align: left; + } + + body.mirrored version-info { + right: auto; + left: 32dp; + bottom: auto; + top: 32dp; + text-align: left; + } + + body.mirrored #disc-status { + justify-content: flex-start; + } +} diff --git a/res/rml/tabbing.rcss b/res/rml/tabbing.rcss new file mode 100644 index 0000000000..194ca89f05 --- /dev/null +++ b/res/rml/tabbing.rcss @@ -0,0 +1,81 @@ +tab-bar { + display: flex; + position: relative; + min-width: 0; + overflow: auto hidden; + clip: always; + text-transform: uppercase; +} + +tab-bar scrollbarhorizontal, +tab-bar scrollbarhorizontal sliderarrowdec, +tab-bar scrollbarhorizontal sliderarrowinc, +tab-bar scrollbarhorizontal slidertrack, +tab-bar scrollbarhorizontal sliderbar { + width: 0; + height: 0; +} + +tab-bar tab { + flex: 0 0 auto; + padding: 0 24dp; + line-height: 64dp; + white-space: nowrap; + decorator: vertical-gradient(#c2a42d00 #c2a42d00); + transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out; + cursor: pointer; +} + +tab-bar tab:selected { + opacity: 1; + border-bottom: 4dp #C2A42D; + font-effect: glow(0dp 4dp 0dp 4dp black); +} + +tab-bar tab:focus-visible, +tab-bar tab:hover { + opacity: 1; + font-effect: glow(0dp 4dp 0dp 4dp black); + decorator: vertical-gradient(#c2a42d00 #c2a42d26); +} + +tab-bar tab:active { + decorator: vertical-gradient(#c2a42d10 #c2a42d40); +} + +tab-bar[closable] tab-end-spacer { + display: block; + flex: 0 0 64dp; + width: 64dp; + pointer-events: none; +} + +tab-bar[closable] close { + display: block; + position: fixed; + top: 8dp; + right: 8dp; + z-index: 1; + width: 48dp; + height: 48dp; + font-family: "Material Symbols Rounded"; + font-weight: normal; + font-size: 24dp; + color: rgba(224, 219, 200, 70%); + backdrop-filter: blur(2dp); + border-radius: 6dp; + decorator: text("" center center); + transition: color background-color 0.12s linear-in-out; + cursor: pointer; +} + +tab-bar[closable] close:hover, +tab-bar[closable] close:focus-visible { + color: #fff; + background-color: rgba(194, 164, 45, 24%); +} + +tab-bar[closable] close:active { + color: #fff; + background-color: rgba(194, 164, 45, 40%); +} diff --git a/res/rml/tuner.rcss b/res/rml/tuner.rcss new file mode 100644 index 0000000000..86dd2043f6 --- /dev/null +++ b/res/rml/tuner.rcss @@ -0,0 +1,147 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + overflow: visible; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: "Fira Sans Condensed"; + font-size: 24dp; + color: #FFFFFF; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: stretch; +} + +.tuner-root { + width: 100%; + min-height: 45%; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: stretch; + decorator: vertical-gradient(#00000000 #151610F2); + filter: opacity(0); + transition: filter 0.2s linear-in-out; +} + +.tuner-root[open] { + filter: opacity(1); +} + +.tuner { + width: 100%; + max-width: 1216dp; + margin-left: auto; + margin-right: auto; + display: flex; + flex-direction: column; + gap: 24dp; + padding: 48dp 64dp; +} + +@media (max-height: 800dp) { + .tuner-root { + min-height: 38%; + } + + .tuner { + gap: 16dp; + padding: 32dp 48dp; + } +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24dp; +} + +.carousel-container { + flex: 1 1 auto; + display: flex; + justify-content: flex-end; + min-width: 0; +} + +.description { + font-size: 18dp; + line-height: 22dp; + color: rgba(255, 255, 255, 50%); +} + +.divider { + margin: 1dp 0; + border-top: 1dp rgba(217, 217, 217, 50%); +} + +.footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24dp; +} + +footer-button { + display: block; + width: 100%; + max-width: 220dp; + border: 0; + padding: 0; + background-color: transparent; + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 20dp; + line-height: 24dp; + text-transform: uppercase; + color: #FFFFFF; + opacity: 1; + cursor: pointer; +} + +footer-button.return { + text-align: left; +} + +footer-button.reset { + text-align: right; +} + +.stepped-carousel { + display: flex; + align-items: center; + justify-content: center; + gap: 16dp; + width: auto; + min-width: 246dp; + padding: 0; + background-color: transparent; + font-family: "Fira Sans Condensed"; + font-weight: bold; +} + +.stepped-carousel-value { + line-height: 29dp; + min-width: 166dp; + text-align: center; + white-space: nowrap; + opacity: 0.9; +} + +.stepped-carousel-arrow { + width: 24dp; + height: 24dp; + min-width: 24dp; + padding: 0; + border: 0; + background-color: transparent; + opacity: 1; + cursor: pointer; + font-family: "Material Symbols Rounded"; + font-weight: normal; +} diff --git a/res/rml/window.rcss b/res/rml/window.rcss new file mode 100644 index 0000000000..45473f312f --- /dev/null +++ b/res/rml/window.rcss @@ -0,0 +1,506 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + display: flex; + width: 100%; + height: 100%; + padding: 64dp; + font-family: "Fira Sans"; + font-weight: normal; + font-style: normal; + font-size: 15dp; + color: #E0DBC8; +} + +window { + display: flex; + flex-flow: column; + position: relative; + height: 100%; + width: 100%; + max-width: 1088dp; + max-height: 768dp; + margin: auto; + border-radius: 14dp; + overflow: hidden; + border: 2dp #92875B; + backdrop-filter: blur(5dp); + box-shadow: 0 0 25dp 5dp; + background-color: rgba(21, 22, 16, 90%); + filter: opacity(0); + transform: scale(0.9); + transform-origin: center; + transition: filter transform 0.2s cubic-in-out; +} + +window.small { + height: auto; + width: auto; +} + +window.modal { + max-width: 640dp; +} + +window[open] { + filter: opacity(1); + transform: scale(1); +} + +@media (max-height: 640dp) { + body { + padding: 16dp; + } + window { + box-shadow: none; + } +} + +window tab-bar { + flex: 0 0 64dp; + height: 64dp; + background-color: rgba(217, 217, 217, 10%); + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 18dp; + border-bottom: 2dp #92875B; +} + +window tab-bar tab { + opacity: 0.25; +} + +window content { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +window content pane { + display: flex; + flex-flow: column; + flex: 1 1 0; + min-width: 0; + min-height: 0; + padding: 24dp; + padding-bottom: 0dp; + gap: 8dp; + overflow: hidden auto; + font-size: 20dp; +} + +window content pane:not(:last-of-type) { + border-right: 1dp #92875B; +} + +window content pane > * { + flex: 0 0 auto; +} + +window content pane:last-of-type > div { + line-height: 1.625; +} + +window content pane > spacer { + display: block; + /* Completes the 24dp bottom inset after the pane's 8dp gap. */ + flex: 0 0 16dp; + height: 16dp; + pointer-events: none; +} + +scrollbarvertical { + width: 8dp; + margin: 4dp 4dp 4dp 0; +} + +scrollbarvertical sliderarrowdec, +scrollbarvertical sliderarrowinc { + width: 0; + height: 0; +} + +scrollbarvertical slidertrack { + width: 8dp; +} + +scrollbarvertical sliderbar { + width: 8dp; + min-height: 24dp; + background-color: rgba(224, 219, 200, 45%); + border-radius: 2dp; + transition: background-color 0.2s cubic-in-out; +} + +scrollbarvertical sliderbar:hover, +scrollbarvertical sliderbar:active { + background-color: rgba(194, 164, 45, 80%); +} + +scrollbarhorizontal { + height: 0; +} + +scrollbarhorizontal sliderarrowdec, +scrollbarhorizontal sliderarrowinc { + width: 0; + height: 0; +} + +scrollbarhorizontal slidertrack, +scrollbarhorizontal sliderbar { + width: 0; + height: 0; +} + +.section-heading { + font-family: "Fira Sans Condensed"; + font-weight: bold; + text-transform: uppercase; + font-size: 22dp; + opacity: 0.25; +} + +.section-heading:not(:first-of-type) { + padding-top: 12dp; +} + +button { + text-align: center; + background-color: rgba(17, 16, 10, 20%); + opacity: 0.9; + padding: 8dp 16dp; + border-radius: 14dp; + box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; + font-size: 20dp; + transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; + cursor: pointer; + focus: auto; +} + +button:not(:disabled):hover, +button:not(:disabled):focus-visible { + background-color: rgba(204, 184, 119, 20%); + box-shadow: #C2A42D 0 0 0 2dp; +} + +button:not(:disabled):selected { + opacity: 1; + background-color: rgba(204, 184, 119, 40%); +} + +button:not(:disabled):active { + opacity: 1; + background-color: rgba(204, 184, 119, 40%); + box-shadow: #C2A42D 0 0 0 2dp; +} + +button.modal-btn { + flex: 1 1 0; + text-align: center; +} + +select-button { + display: flex; + align-items: center; + gap: 8dp; + background-color: rgba(17, 16, 10, 20%); + opacity: 0.9; + padding: 8dp 16dp; + border-radius: 14dp; + box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; + transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; + cursor: pointer; + focus: auto; +} + +select-button:not(:disabled):hover, +select-button:not(:disabled):focus-visible { + background-color: rgba(204, 184, 119, 20%); + box-shadow: #C2A42D 0 0 0 2dp; +} + +select-button:not(:disabled):selected { + opacity: 1; + background-color: rgba(204, 184, 119, 40%); +} + +select-button:not(:disabled):active { + opacity: 1; + background-color: rgba(204, 184, 119, 40%); + box-shadow: #C2A42D 0 0 0 2dp; +} + +select-button:disabled { + opacity: 0.35; + cursor: default; +} + +select-button key { + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 18dp; + text-transform: uppercase; + flex: 0 1 auto; +} + +select-button value { + flex: 1 1 auto; + text-align: right; + font-size: 20dp; +} + +select-button value.modified { + font-weight: bold; +} + +select-button input { + text-align: right; + font-size: 20dp; +} + +icon { + width: 1em; + height: 1em; + font-family: "Material Symbols Rounded"; + font-weight: normal; + display: inline-block; + vertical-align: middle; +} + +icon.warning { + decorator: text("" center center); +} + +icon.error { + decorator: text("" center center); +} + +icon.verifying { + decorator: text("" center center); +} + +icon.celebration { + decorator: text("" center center); +} + +icon.question-mark { + decorator: text("" center center); +} + +.achievement-total { + position: absolute; + top: 0; + right: 64dp; + height: 64dp; + line-height: 64dp; + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 16dp; + color: rgba(224, 219, 200, 55%); + pointer-events: none; +} + +.achievement-row { + display: flex; + align-items: flex-start; + gap: 10dp; + padding: 12dp 0; + border-bottom: 1dp rgba(146, 135, 91, 30%); +} + +.achievement-info { + display: block; + flex: 1 1 0; + min-width: 0; +} + +.achievement-header { + display: flex; + align-items: center; +} + +.achievement-name { + flex: 1; + font-weight: bold; +} + +.achievement-name.unlocked { + color: #ffa826; +} + +.achievement-badge { + font-size: 14dp; + opacity: 0.7; +} + +.achievement-badge.unlocked { + color: #44cc55; + opacity: 1; +} + +.achievement-badge.locked { + color: #cc4444; + opacity: 1; +} + +.achievement-desc { + display: block; + color: rgba(224, 219, 200, 55%); + font-size: 16dp; + margin: 4dp 0 0 0; +} + +.achievement-progress { + display: block; + font-size: 13dp; + color: rgba(224, 219, 200, 45%); +} + +progress { + display: block; + width: 100%; + height: 6dp; + border-radius: 3dp; + background-color: rgba(255, 255, 255, 10%); + margin: 6dp 0 2dp 0; +} + +progress.progress-done fill { + background-color: #44aa22; + border-radius: 3dp; +} + +progress.progress-ongoing fill { + background-color: #2255bb; + border-radius: 3dp; +} + +button.achievement-clear { + flex: 0 0 auto; + align-self: center; + font-size: 14dp; + padding: 2dp 8dp; + opacity: 0.45; +} + +.preset-grid { + display: flex; + flex-direction: row; + gap: 20dp; + flex: 0 1 auto; + align-items: flex-start; + width: 100%; +} + +.preset-col { + display: flex; + flex-flow: column; + gap: 12dp; + flex: 1 1 0; +} + +.preset-desc { + display: block; + font-size: 16dp; + text-align: center; +} + +.modal-dialog { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 24dp; + gap: 20dp; + flex: 0 1 auto; + width: 100%; + text-align: left; +} + +window.modal.danger { + border: 2dp #852221; +} + +.modal-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; + flex: 0 0 auto; + gap: 16dp; +} + +.modal-header icon { + font-size: 24dp; + color: #92875B; +} + +.modal-title { + display: block; + font-family: "Fira Sans Condensed"; + font-weight: bold; + text-transform: uppercase; + font-size: 18dp; + color: #92875B; + flex: 1 1 auto; +} + +window.modal.danger .modal-title, +window.modal.danger .modal-header icon { + color: #B3261E; +} + +.modal-body { + display: block; + width: 100%; + flex: 0 1 auto; + min-width: 0; + font-size: 20dp; + color: #FFFFFF; + font-weight: normal; +} + +.modal-body span.tip { + font-size: 14dp; + color: #92875B; +} + +.verification-progress { + display: flex; + flex-direction: column; + gap: 10dp; + width: 100%; +} + +.verification-file { + display: block; + font-size: 17dp; + color: #FFFFFF; +} + +progress.verification-progress-bar { + height: 8dp; + margin: 2dp 0 0 0; +} + +.verification-detail { + display: block; + font-size: 14dp; + color: rgba(224, 219, 200, 65%); +} + +.modal-actions { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: stretch; + gap: 12dp; + width: 100%; + flex: 0 0 auto; + padding-top: 4dp; +} diff --git a/src/SSystem/SComponent/c_lib.cpp b/src/SSystem/SComponent/c_lib.cpp index 6441d6ceb8..d73dfb8627 100644 --- a/src/SSystem/SComponent/c_lib.cpp +++ b/src/SSystem/SComponent/c_lib.cpp @@ -468,9 +468,20 @@ s16 cLib_targetAngleX(cXyz const* lhs, cXyz const* rhs) { void cLib_offsetPos(cXyz* pdest, cXyz const* psrc, s16 angle, cXyz const* vec) { f32 cos = cM_scos(angle); f32 sin = cM_ssin(angle); + // MWCC loads vec members into registers before writing to pdest; other compilers may not, + // which corrupts results when pdest and vec alias the same memory. +#if !__MWERKS__ + f32 vx = vec->x; + f32 vy = vec->y; + f32 vz = vec->z; + pdest->x = psrc->x + (vx * cos + vz * sin); + pdest->y = psrc->y + vy; + pdest->z = psrc->z + (vz * cos - vx * sin); +#else pdest->x = psrc->x + (vec->x * cos + vec->z * sin); pdest->y = psrc->y + vec->y; pdest->z = psrc->z + (vec->z * cos - vec->x * sin); +#endif } /** diff --git a/src/Z2AudioLib/Z2Audience.cpp b/src/Z2AudioLib/Z2Audience.cpp index 5bb91eef31..dda1a4b2d0 100644 --- a/src/Z2AudioLib/Z2Audience.cpp +++ b/src/Z2AudioLib/Z2Audience.cpp @@ -1,5 +1,10 @@ #include "Z2AudioLib/Z2Audience.h" #include "Z2AudioLib/Z2SoundInfo.h" +#if TARGET_PC +#include "dusk/audio/DuskDsp.hpp" +#include "dusk/settings.h" +#include +#endif #include "Z2AudioLib/Z2Calc.h" #include "Z2AudioLib/Z2Param.h" #include "JSystem/JAudio2/JAISound.h" @@ -701,6 +706,11 @@ f32 Z2Audience::calcRelPosVolume(const Vec& param_0, f32 param_1, int camID) { f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) { Vec local_54 = param_0; local_54.y = 0.0f; +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + local_54.x = -local_54.x; + } +#endif f32 dVar6 = VECMag(&local_54); if (dVar6 < 0.1f) { @@ -734,9 +744,22 @@ f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) { f32 Z2Audience::calcRelPosDolby(const Vec& param_0, int camID) { f32 fVar1 = param_0.z + mAudioCamera[camID].getDolbyCenterZ(); +#if TARGET_PC + if (dusk::audio::EnableHrtf) { + // Normalize the direction so result is purely front/back orientation, + // independent of how far away the sound is + f32 lenSq = param_0.x * param_0.x + param_0.y * param_0.y + param_0.z * param_0.z; + if (lenSq < 0.0001f) { + return 0.5f; + } + f32 zNorm = param_0.z / sqrtf(lenSq); + f32 t = (zNorm + 1.0f) * 0.5f; + return 0.5f - 0.5f * cosf(t * static_cast(M_PI)); + } +#endif if (fVar1 > mSetting.field_0x48) { return 1.0f; - } + } if (fVar1 < mSetting.field_0x44) { return 0.0f; diff --git a/src/Z2AudioLib/Z2WolfHowlMgr.cpp b/src/Z2AudioLib/Z2WolfHowlMgr.cpp index 4866af7a5d..f12d453fbe 100644 --- a/src/Z2AudioLib/Z2WolfHowlMgr.cpp +++ b/src/Z2AudioLib/Z2WolfHowlMgr.cpp @@ -369,7 +369,7 @@ void Z2WolfHowlMgr::setCorrectData(s8 curveID, Z2WolfHowlData* data) { #if TARGET_PC case Z2WOLFHOWL_TIMESONG: cPitchUp = 1.3348f; - cPitchCenter = 1.0f; + cPitchCenter = 0.8909f; cPitchDown = 0.7937f; break; #endif diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index e6943baf96..0204278ddc 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -51,6 +51,7 @@ #include "d/actor/d_a_ni.h" #include "d/d_s_play.h" +#include "dusk/frame_interpolation.h" #include "dusk/settings.h" #include "res/Object/Alink.h" #include @@ -4962,13 +4963,16 @@ int daAlink_c::create() { setArcName(checkWolf()); setOriginalHeap(&mpArcHeap, 0xA2800); + JKRHEAP_NAME(mpArcHeap, "Alink ArcHeap"); if (dComIfG_resLoad(&mPhaseReq, mArcName, mpArcHeap) != cPhs_COMPLEATE_e) { return cPhs_INIT_e; } setShieldArcName(); setOriginalHeap(&mpShieldArcHeap, 0x7000); - if (dComIfG_resLoad(&mShieldPhaseReq, mShieldArcName, mpShieldArcHeap) != cPhs_COMPLEATE_e) { + JKRHEAP_NAME(mpShieldArcHeap, "Alink ShieldArcHeap"); + if (dComIfG_resLoad(&mShieldPhaseReq, mShieldArcName, mpShieldArcHeap) != cPhs_COMPLEATE_e) + { return cPhs_INIT_e; } @@ -5877,6 +5881,11 @@ void daAlink_c::setItemMatrix(int param_0) { modelCalc(mSheathModel); int var_r26; + + #if AVOID_UB + var_r26 = 0; + #endif + if (!checkNoResetFlg3(FLG3_UNK_4000000)) { if (mEquipItem == 0x103 || param_0 != 0) { mSwordModel->setBaseTRMtx(mpLinkModel->getAnmMtx(mLeftItemJntNo)); @@ -14779,6 +14788,10 @@ void daAlink_c::deleteEquipItem(BOOL i_isPlaySound, BOOL i_isDeleteKantera) { mIronBallChainPos = NULL; mIronBallChainAngle = NULL; field_0x3848 = NULL; +#if TARGET_PC + mIBChainInterpPrevValid = false; + mIBChainInterpCurrValid = false; +#endif field_0x0774 = NULL; field_0x0778 = NULL; mpHookshotLinChk = NULL; @@ -18936,11 +18949,20 @@ void daAlink_c::setDrawHand() { mpLinkHandModel->setBaseTRMtx(mpLinkModel->getBaseTRMtx()); mpLinkHandModel->calc(); +#if TARGET_PC + // FRAME INTERP NOTE: Always set these, otherwise the hands occasionally zip to origin. + // Doing it regardless of interpolation being active seems harmless. + mpLinkHandModel->setAnmMtx(1, mpLinkModel->getAnmMtx(9)); + mpLinkHandModel->setAnmMtx(2, mpLinkModel->getAnmMtx(0xE)); +#endif + if (var_r30 == 0xFE || var_r30 == 0xFB) { field_0x06d0 = field_0x06d8; } else { field_0x06d0 = mpLinkHandModel->getModelData()->getMaterialNodePointer(var_r30)->getShape(); +#if !TARGET_PC mpLinkHandModel->setAnmMtx(1, mpLinkModel->getAnmMtx(9)); +#endif } if (var_r30 == 0xFB) { @@ -18959,7 +18981,9 @@ void daAlink_c::setDrawHand() { field_0x06d4 = field_0x06dc; } else { field_0x06d4 = mpLinkHandModel->getModelData()->getMaterialNodePointer(var_r29)->getShape(); +#if !TARGET_PC mpLinkHandModel->setAnmMtx(2, mpLinkModel->getAnmMtx(0xE)); +#endif } if (var_r29 == 0xFB) { @@ -19698,6 +19722,27 @@ int daAlink_c::draw() { ) { dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0); + +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation && + mEquipItem == dItemNo_IRONBALL_e && + mIronBallChainPos != NULL && mIronBallChainAngle != NULL) + { + if (mIBChainInterpCurrValid) { + memcpy(mIBChainInterpPrevPos, mIBChainInterpCurrPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz)); + memcpy(mIBChainInterpPrevAngle, mIBChainInterpCurrAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz)); + mIBChainInterpPrevHandRoot = mIBChainInterpCurrHandRoot; + mIBChainInterpPrevValid = true; + } + + memcpy(mIBChainInterpCurrPos, mIronBallChainPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz)); + memcpy(mIBChainInterpCurrAngle, mIronBallChainAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz)); + mIBChainInterpCurrHandRoot = mHookshotTopPos; + mIBChainInterpCurrValid = true; + + dusk::frame_interp::add_interpolation_callback(&ironBallChainInterpCallback, this); + } +#endif } } diff --git a/src/d/actor/d_a_alink_damage.inc b/src/d/actor/d_a_alink_damage.inc index ac5175e314..bccdbf7381 100644 --- a/src/d/actor/d_a_alink_damage.inc +++ b/src/d/actor/d_a_alink_damage.inc @@ -8,6 +8,10 @@ #include "d/actor/d_a_horse.h" #include "d/actor/d_a_crod.h" #include "d/d_msg_object.h" +#ifdef TARGET_PC +#include "d/actor/d_a_obj_carry.h" +#include "dusk/achievements.h" +#endif #if DEBUG #include "d/d_s_menu.h" @@ -201,6 +205,9 @@ int daAlink_c::setDamagePoint(int i_dmgAmount, BOOL i_checkZoraMag, BOOL i_setDm dComIfGp_setItemLifeCount(-i_dmgAmount, 0); } +#if TARGET_PC + dusk::AchievementSystem::get().signal("player_damaged"); +#endif onResetFlg1(RFLG1_DAMAGE_IMPACT); mSwordUpTimer = 0; @@ -677,6 +684,15 @@ BOOL daAlink_c::checkDamageAction() { } setDamagePoint(dmg, at_mtrl == dCcD_MTRL_FIRE || at_mtrl == dCcD_MTRL_ICE, TRUE, 0); + +#ifdef TARGET_PC + if (tghit_ac_name == fpcNm_Obj_Carry_e) { + auto* carry = static_cast(tghit_ac); + if (carry->prm_chk_type_ironball() && carry->checkCannon()) { + dusk::AchievementSystem::get().signal("iron_ball_hit_player"); + } + } +#endif if (armor_no_dmg && at_mtrl != dCcD_MTRL_ELECTRIC && at_mtrl != dCcD_MTRL_ICE) { setGuardSe(var_r29); diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index aa77b24129..5ac30a3cb9 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,6 +23,7 @@ #include "d/actor/d_a_npc_tkc.h" #include +#include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" BOOL daAlink_c::checkEventRun() const { @@ -4005,6 +4006,15 @@ int daAlink_c::procGanonFinishInit() { field_0x37c8 = current.pos; onEndResetFlg1(ERFLG1_SHIELD_BACKBONE); + +#if TARGET_PC + if (dusk::getSettings().game.speedrunMode) { + if (dusk::m_speedrunInfo.m_isRunStarted) { + dusk::m_speedrunInfo.stopRun(); + } + } +#endif + return 1; } diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index d53476aa91..8c1d415ae0 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -44,16 +44,14 @@ void daAlink_c::handleWolfHowl() { bool canHowl = false; if (mLinkAcch.ChkGroundHit() && !checkModeFlg(MODE_PLAYER_FLY) && !checkMagneBootsOn()) { - if (!checkForestOldCentury()) { - if (checkMidnaRide()) { - if ((checkWolf() && - (checkModeFlg(MODE_UNK_1000) || dComIfGp_checkPlayerStatus0(0, 0x10))) || - (!checkWolf() && - (checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) && - (checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10)))) - { - canHowl = true; - } + if (checkMidnaRide()) { + if ((checkWolf() && + (checkModeFlg(MODE_UNK_1000) || dComIfGp_checkPlayerStatus0(0, 0x10))) || + (!checkWolf() && + (checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) && + (checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10)))) + { + canHowl = true; } } } @@ -124,16 +122,14 @@ void daAlink_c::handleQuickTransform() { bool canTransform = false; if (mLinkAcch.ChkGroundHit() && !checkModeFlg(MODE_PLAYER_FLY) && !checkMagneBootsOn()) { - if (!checkForestOldCentury()) { - if (checkMidnaRide()) { - if ((checkWolf() && - (checkModeFlg(MODE_UNK_1000) || dComIfGp_checkPlayerStatus0(0, 0x10))) || - (!checkWolf() && - (checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) && - (checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10)))) - { - canTransform = true; - } + if (checkMidnaRide()) { + if ((checkWolf() && + (checkModeFlg(MODE_UNK_1000) || dComIfGp_checkPlayerStatus0(0, 0x10))) || + (!checkWolf() && + (checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) && + (checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10)))) + { + canTransform = true; } } } diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc index d152190161..36fba0c6d8 100644 --- a/src/d/actor/d_a_alink_hook.inc +++ b/src/d/actor/d_a_alink_hook.inc @@ -8,6 +8,8 @@ #include "d/actor/d_a_obj_swhang.h" #include "d/actor/d_a_obj_chandelier.h" #include "JSystem/J3DGraphBase/J3DMaterial.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" enum { HS_MODE_NONE_e, @@ -17,6 +19,10 @@ enum { HS_MODE_RETURN_e = 6, }; +#if TARGET_PC +static const int HS_CHAIN_MAX_LINKS = 600; +#endif + void daAlink_c::hsChainShape_c::draw() { static const int dummy = 0; @@ -161,7 +167,11 @@ void daAlink_c::hsChainShape_c::draw() { } (void)0; - while (maxDistanceF > var_f30) { +#if TARGET_PC + int chainLinks = 0; +#endif + + while (maxDistanceF > var_f30 IF_DUSK(&&chainLinks < HS_CHAIN_MAX_LINKS)) { temp_f27 = var_f28 * cM_fsin(sp34 * var_f30); s16 spC = cM_atan2s(temp_f27 - var_f26, 5.0f); sp64.x = sp6C.x + spC; @@ -183,6 +193,10 @@ void daAlink_c::hsChainShape_c::draw() { var_f26 = temp_f27; var_f30 += fabsf(cM_scos(spC)) * 5.0f; + +#if TARGET_PC + chainLinks++; +#endif } } @@ -198,7 +212,11 @@ void daAlink_c::hsChainShape_c::draw() { sp98 = subChainTopPos; sp6C.set(maxDistance.atan2sY_XZ(), maxDistance.atan2sX_Z(), 0); - while (maxDistanceF > var_f30) { +#if TARGET_PC + int subChainLinks = 0; +#endif + + while (maxDistanceF > var_f30 IF_DUSK(&&subChainLinks < HS_CHAIN_MAX_LINKS)) { mDoMtx_stack_c::copy(j3dSys.getViewMtx()); mDoMtx_stack_c::transM(sp98); mDoMtx_stack_c::ZXYrotM(sp6C); @@ -211,11 +229,39 @@ void daAlink_c::hsChainShape_c::draw() { sp98 += maxDistance * 5.0f; ANGLE_ADD_2(sp6C.z, 0x3000); var_f30 += 5.0f; +#if TARGET_PC + subChainLinks++; +#endif } } } } +#if TARGET_PC +static void ironBallChainInterpCallback(bool isSimFrame, void* pUserWork) { + static_cast(pUserWork)->onIronBallChainInterpCallback(); +} + +void daAlink_c::onIronBallChainInterpCallback() { + if (!mIBChainInterpPrevValid || !mIBChainInterpCurrValid) { + return; + } + if (mIronBallChainPos == NULL || mIronBallChainAngle == NULL) { + return; + } + + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + + for (int i = 0; i < IRON_BALL_CHAIN_COUNT; i++) { + mIronBallChainPos[i] = mIBChainInterpPrevPos[i] + (mIBChainInterpCurrPos[i] - mIBChainInterpPrevPos[i]) * alpha; + mIronBallChainAngle[i].x = mIBChainInterpPrevAngle[i].x + (s16)((s16)(mIBChainInterpCurrAngle[i].x - mIBChainInterpPrevAngle[i].x) * alpha); + mIronBallChainAngle[i].y = mIBChainInterpPrevAngle[i].y + (s16)((s16)(mIBChainInterpCurrAngle[i].y - mIBChainInterpPrevAngle[i].y) * alpha); + mIronBallChainAngle[i].z = mIBChainInterpPrevAngle[i].z + (s16)((s16)(mIBChainInterpCurrAngle[i].z - mIBChainInterpPrevAngle[i].z) * alpha); + } + mHookshotTopPos = mIBChainInterpPrevHandRoot + (mIBChainInterpCurrHandRoot - mIBChainInterpPrevHandRoot) * alpha; +} +#endif + void daAlink_c::hookshotAtHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor, dCcD_GObjInf* i_tgObjInf) { if (i_tgActor != NULL && fopAcM_IsActor(i_tgActor) && !i_tgObjInf->ChkTgHookshotThrough()) { diff --git a/src/d/actor/d_a_alink_horse.inc b/src/d/actor/d_a_alink_horse.inc index ac7ce73e50..f0c585f1b8 100644 --- a/src/d/actor/d_a_alink_horse.inc +++ b/src/d/actor/d_a_alink_horse.inc @@ -2721,7 +2721,7 @@ int daAlink_c::procHorseRun() { } if (mProcVar2.field_0x300c == 0) { - set3DStatus(BUTTON_STATUS_HOLD_ON, 4); + set3DStatus(BUTTON_STATUS_HOLD_ON, IF_DUSK(dusk::getSettings().game.enableMirrorMode ? 1 :) 4); } } else { if (mProcVar3.field_0x300e != 0) { @@ -2731,7 +2731,7 @@ int daAlink_c::procHorseRun() { } if (mProcVar2.field_0x300c == 0) { - set3DStatus(BUTTON_STATUS_HOLD_ON, 1); + set3DStatus(BUTTON_STATUS_HOLD_ON, IF_DUSK(dusk::getSettings().game.enableMirrorMode ? 4 :) 1); } } diff --git a/src/d/actor/d_a_alink_swindow.inc b/src/d/actor/d_a_alink_swindow.inc index 81c7a1ca18..9016f14205 100644 --- a/src/d/actor/d_a_alink_swindow.inc +++ b/src/d/actor/d_a_alink_swindow.inc @@ -41,12 +41,11 @@ void daAlink_c::setOriginalHeap(JKRExpHeap** i_ppheap, u32 i_size) { u32 var_r28 = 0x10; u32 size = ROUND(i_size, 16); #if TARGET_PC - size *= 2; + size *= 20; // Increase Link's heap size to prevent mods from crashing with higher-quality models. #endif JKRHeap* parent = mDoExt_getGameHeap(); JKRExpHeap* heap = JKRExpHeap::create(size + (var_r29 + var_r28), parent, true); - JKRHEAP_NAME(heap, "Alink original"); *i_ppheap = heap; } } diff --git a/src/d/actor/d_a_arrow.cpp b/src/d/actor/d_a_arrow.cpp index 769c3273e9..c18d19e149 100644 --- a/src/d/actor/d_a_arrow.cpp +++ b/src/d/actor/d_a_arrow.cpp @@ -17,6 +17,9 @@ #include "d/actor/d_a_e_pz.h" #include "d/actor/d_a_horse.h" #include "d/actor/d_a_hozelda.h" +#if TARGET_PC +#include "dusk/achievements.h" +#endif int daArrow_c::createHeap() { J3DModelData* model_data; @@ -92,7 +95,12 @@ void daArrow_c::atHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor, d if (dist_to_hitpos < field_0x998) { field_0x998 = dist_to_hitpos; mHitAcID = fopAcM_GetID(i_tgActor); - +#if TARGET_PC + if (fopAcM_GetGroup(i_tgActor) == fopAc_ENEMY_e && + current.pos.abs(mStartPos) > 10000.0f) { + dusk::AchievementSystem::get().signal("arrow_hit_100m"); + } +#endif if (mArrowType == 1) { field_0x9a8 = *hit_pos_p; } else if (i_tgObjInf->ChkTgShield()) { diff --git a/src/d/actor/d_a_b_gnd.cpp b/src/d/actor/d_a_b_gnd.cpp index 2272c35376..f5b03be1b1 100644 --- a/src/d/actor/d_a_b_gnd.cpp +++ b/src/d/actor/d_a_b_gnd.cpp @@ -17,6 +17,12 @@ #include "Z2AudioLib/Z2Instances.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" +#if TARGET_PC +#include "dusk/achievements.h" +#endif + class daB_GND_HIO_c : public JORReflexible { public: daB_GND_HIO_c(); @@ -279,6 +285,30 @@ static int h_nodeCallBack(J3DJoint* i_joint, int param_2) { return 1; } +#if TARGET_PC +static void b_gnd_rein_interp_callback(bool isSimFrame, void* pUserWork) { + b_gnd_class* i_this = (b_gnd_class*)pUserWork; + if (!i_this->mReinsInterpPrevValid || !i_this->mReinsInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + for (int r = 0; r < 2; r++) { + cXyz* dst = i_this->mHorseReins[r].getPos(0); + for (int i = 0; i < 16; i++) { + const cXyz& p0 = i_this->mReinsInterpPrev[r][i]; + const cXyz& p1 = i_this->mReinsInterpCurr[r][i]; + dst[i] = p0 + (p1 - p0) * alpha; + } + } + cXyz* dst = i_this->field_0x21e8.getPos(0); + for (int i = 0; i < 2; i++) { + const cXyz& p0 = i_this->mReinsTexInterpPrev[i]; + const cXyz& p1 = i_this->mReinsTexInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daB_GND_Draw(b_gnd_class* i_this) { fopAc_ac_c* a_this = (fopAc_ac_c*)i_this; @@ -366,6 +396,21 @@ static int daB_GND_Draw(b_gnd_class* i_this) { i_this->field_0x21e8.update(2, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->field_0x21e8); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mReinsInterpCurrValid) { + memcpy(i_this->mReinsInterpPrev, i_this->mReinsInterpCurr, sizeof(i_this->mReinsInterpCurr)); + memcpy(i_this->mReinsTexInterpPrev, i_this->mReinsTexInterpCurr, sizeof(i_this->mReinsTexInterpCurr)); + i_this->mReinsInterpPrevValid = true; + } + for (int r = 0; r < 2; r++) { + memcpy(i_this->mReinsInterpCurr[r], i_this->mHorseReins[r].getPos(0), 16 * sizeof(cXyz)); + } + memcpy(i_this->mReinsTexInterpCurr, i_this->field_0x21e8.getPos(0), 2 * sizeof(cXyz)); + i_this->mReinsInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&b_gnd_rein_interp_callback, i_this); + } +#endif } return 1; @@ -1189,10 +1234,16 @@ static void b_gnd_h_end(b_gnd_class* i_this) { if (i_this->mDemoCamMode < 32) { i_this->mDemoCamMode = 32; +#if TARGET_PC + i_this->mDemoCamSyncTicks = 2; +#endif } else { i_this->mDemoCamMode = 34; i_this->mDemoCamTimer = 0; i_this->mMoveMode = 2; +#if TARGET_PC + i_this->mDemoCamSyncTicks = 2; +#endif } } break; @@ -1241,6 +1292,9 @@ static void b_gnd_g_wait(b_gnd_class* i_this) { if (i_this->mMoveMode < 5 && i_this->mPlayerDistXZ < 600.0f) { i_this->mMoveMode = 5; i_this->field_0xc44[0] = 10; +#if TARGET_PC + dusk::AchievementSystem::get().signal("ganondorf_fishing_rod"); +#endif } } else if (i_this->mMoveMode == 5) { i_this->mMoveMode = 6; @@ -2887,6 +2941,9 @@ static void demo_camera(b_gnd_class* i_this) { cXyz spF0; s8 sp8 = false; +#if TARGET_PC + const s16 entry_demo_cam_mode = i_this->mDemoCamMode; +#endif switch (i_this->mDemoCamMode) { case 0: break; @@ -3725,6 +3782,15 @@ static void demo_camera(b_gnd_class* i_this) { i_this->mDemoCamTimer = 10000; } } +#if TARGET_PC + if (entry_demo_cam_mode != i_this->mDemoCamMode) { + i_this->mDemoCamSyncTicks = 2; + } + if (i_this->mDemoCamSyncTicks > 0) { + dusk::frame_interp::request_presentation_sync(); + i_this->mDemoCamSyncTicks--; + } +#endif } static void anm_se_set(b_gnd_class* i_this) { diff --git a/src/d/actor/d_a_balloon_2D.cpp b/src/d/actor/d_a_balloon_2D.cpp index 6b5b5a2bd2..8627505371 100644 --- a/src/d/actor/d_a_balloon_2D.cpp +++ b/src/d/actor/d_a_balloon_2D.cpp @@ -6,6 +6,7 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep #include "d/actor/d_a_balloon_2D.h" +#include "dusk/frame_interpolation.h" #include "JSystem/J2DGraph/J2DGrafContext.h" #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" @@ -438,7 +439,12 @@ void daBalloon2D_c::setComboAlpha() { void daBalloon2D_c::drawAddScore() { for (s32 i = 19; i >= 0; i--) { if (field_0x5f8[i].field_0xe != 0) { - field_0x5f8[i].field_0xe--; +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x5f8[i].field_0xe--; + } s32 score3; s32 score2; s32 score = field_0x5f8[i].field_0xc; @@ -446,8 +452,13 @@ void daBalloon2D_c::drawAddScore() { u8 local_88 = 0xff; f32 dVar11 = 30.0f; f32 dVar9 = 30.0f; - field_0x5f8[i].field_0x0.x += cM_ssin(temp0) * 0.3f; - field_0x5f8[i].field_0x0.y -= 1.0f; +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x5f8[i].field_0x0.x += cM_ssin(temp0) * 0.3f; + field_0x5f8[i].field_0x0.y -= 1.0f; + } if (field_0x5f8[i].field_0xe < 10) { f32 fVar5 = field_0x5f8[i].field_0xe / 10.0f; local_88 = fVar5 * 255.0f; diff --git a/src/d/actor/d_a_door_boss.cpp b/src/d/actor/d_a_door_boss.cpp index 476a9c8ad6..bf042b3323 100644 --- a/src/d/actor/d_a_door_boss.cpp +++ b/src/d/actor/d_a_door_boss.cpp @@ -254,7 +254,11 @@ BOOL daBdoor_c::checkArea() { if (fabsf(vec.z) > 100.0f) { return false; } +#ifdef TARGET_PC + return (s16)((s32)fabs(current.angle.y - 0x7fff - player->current.angle.y) & 0xffff) <= 0x4000 ? 1 : 0; +#else return (s16)fabs((f64)(current.angle.y - 0x7fff - player->current.angle.y)) <= 0x4000 ? 1 : 0; +#endif } BOOL daBdoor_c::checkFront() { diff --git a/src/d/actor/d_a_door_bossL1.cpp b/src/d/actor/d_a_door_bossL1.cpp index a756cc6fd2..649ebbd5f3 100644 --- a/src/d/actor/d_a_door_bossL1.cpp +++ b/src/d/actor/d_a_door_bossL1.cpp @@ -825,7 +825,11 @@ int daBdoorL1_c::checkArea() { if (fabsf(local_48.z) > 100.0f) { return 0; } +#ifdef TARGET_PC + if ((s16)((s32)fabs(current.angle.y - 0x7fff - player->current.angle.y) & 0xffff) <= 0x4000) { +#else if ((s16)fabs((f64)(current.angle.y - 0x7fff - player->current.angle.y)) <= 0x4000) { +#endif return 1; } else { return 0; diff --git a/src/d/actor/d_a_door_bossL5.cpp b/src/d/actor/d_a_door_bossL5.cpp index d71fddad1f..34dac15fa9 100644 --- a/src/d/actor/d_a_door_bossL5.cpp +++ b/src/d/actor/d_a_door_bossL5.cpp @@ -348,7 +348,11 @@ int daBdoorL5_c::checkArea() { if (fabsf(local_48.z) > 100.0f) { return 0; } +#ifdef TARGET_PC + if ((s16)((s32)fabs(current.angle.y - 0x7fff - player->current.angle.y) & 0xffff) <= 0x4000) { +#else if ((s16)fabs((f64)(current.angle.y - 0x7fff - player->current.angle.y)) <= 0x4000) { +#endif return 1; } else { return 0; diff --git a/src/d/actor/d_a_door_mbossL1.cpp b/src/d/actor/d_a_door_mbossL1.cpp index 1f37fbcc7b..5dfd3bb96c 100644 --- a/src/d/actor/d_a_door_mbossL1.cpp +++ b/src/d/actor/d_a_door_mbossL1.cpp @@ -1317,8 +1317,12 @@ int daMBdoorL1_c::checkArea() { if (fabsf(local_48.z) > 110.0f) { return 0; } - + +#ifdef TARGET_PC + if ((s16)((s32)fabs(angle - 0x7fff - player->current.angle.y) & 0xffff) > 0x4000) { +#else if ((s16)fabs((f64)(angle - 0x7fff - player->current.angle.y)) > 0x4000) { +#endif return 0; } else { return 1; diff --git a/src/d/actor/d_a_door_shutter.cpp b/src/d/actor/d_a_door_shutter.cpp index a481762132..ebd1c9a185 100644 --- a/src/d/actor/d_a_door_shutter.cpp +++ b/src/d/actor/d_a_door_shutter.cpp @@ -17,6 +17,11 @@ #include #include +#if TARGET_PC +#include +#include +#endif + char* daDoor20_c::getStopBmdName() { switch (door_param2_c::getKind(this)) { case 3: @@ -196,6 +201,7 @@ void daDoor20_c::setEventPrm() { } else { roomNo = FRoomNo; } + if (dComIfGp_roomControl_checkStatusFlag(roomNo, 1)) { if (door_param2_c::getKind(this) == 9) { if (daPy_py_c::checkNowWolf()) { @@ -564,6 +570,11 @@ int daDoor20_c::openEnd(int param_1) { openEnd_1(); break; } + + #if TARGET_PC + triggerAutoSave(); + #endif + return 1; } diff --git a/src/d/actor/d_a_e_db.cpp b/src/d/actor/d_a_e_db.cpp index cbd21176b9..4d60fbd6f6 100644 --- a/src/d/actor/d_a_e_db.cpp +++ b/src/d/actor/d_a_e_db.cpp @@ -10,6 +10,10 @@ #include "f_op/f_op_kankyo_mng.h" #include "f_op/f_op_actor_enemy.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + class daE_DB_HIO_c : public JORReflexible { public: daE_DB_HIO_c(); @@ -66,6 +70,22 @@ static BOOL leaf_anm_init(e_db_class* i_this, int i_anm, f32 i_morf, u8 i_mode, return FALSE; } +#if TARGET_PC +static void daE_DB_interp_callback(bool isSimFrame, void* pUserWork) { + e_db_class* i_this = (e_db_class*)pUserWork; + if (!i_this->mStalkLineInterpPrevValid || !i_this->mStalkLineInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->stalkLine.getPos(0); + for (int i = 0; i < 12; i++) { + const cXyz& p0 = i_this->mStalkLineInterpPrev[i]; + const cXyz& p1 = i_this->mStalkLineInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daE_DB_Draw(e_db_class* i_this) { fopAc_ac_c* actor = &i_this->enemy; @@ -95,6 +115,17 @@ static int daE_DB_Draw(e_db_class* i_this) { static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF}; i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mStalkLineInterpCurrValid) { + memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); + i_this->mStalkLineInterpPrevValid = true; + } + memcpy(i_this->mStalkLineInterpCurr, i_this->stalkLine.getPos(0), 12 * sizeof(cXyz)); + i_this->mStalkLineInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_DB_interp_callback, i_this); + } +#endif for (int i = 1; i < 11; i++) { if (i_this->thornModel[i] != NULL) { diff --git a/src/d/actor/d_a_e_hb.cpp b/src/d/actor/d_a_e_hb.cpp index d3177ba0f6..8ed9058c6f 100644 --- a/src/d/actor/d_a_e_hb.cpp +++ b/src/d/actor/d_a_e_hb.cpp @@ -9,6 +9,10 @@ #include "d/actor/d_a_e_hb_leaf.h" #include "f_op/f_op_actor_enemy.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + enum daE_HB_ACTION { ACTION_STAY, ACTION_APPEAR, @@ -64,6 +68,22 @@ static BOOL leaf_anm_init(e_hb_class* i_this, int i_anm, f32 i_morf, u8 i_mode, return FALSE; } +#if TARGET_PC +static void daE_HB_interp_callback(bool isSimFrame, void* pUserWork) { + e_hb_class* i_this = (e_hb_class*)pUserWork; + if (!i_this->mStalkLineInterpPrevValid || !i_this->mStalkLineInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->stalkLine.getPos(0); + for (int i = 0; i < 12; i++) { + const cXyz& p0 = i_this->mStalkLineInterpPrev[i]; + const cXyz& p1 = i_this->mStalkLineInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daE_HB_Draw(e_hb_class* i_this) { fopAc_ac_c* actor = &i_this->enemy; @@ -82,6 +102,17 @@ static int daE_HB_Draw(e_hb_class* i_this) { static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF}; i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mStalkLineInterpCurrValid) { + memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); + i_this->mStalkLineInterpPrevValid = true; + } + memcpy(i_this->mStalkLineInterpCurr, i_this->stalkLine.getPos(0), 12 * sizeof(cXyz)); + i_this->mStalkLineInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_HB_interp_callback, i_this); + } +#endif for (int i = 1; i < 11; i++) { if (i_this->thornModel[i] != NULL) { diff --git a/src/d/actor/d_a_e_mb.cpp b/src/d/actor/d_a_e_mb.cpp index 939b253475..aa77a99612 100644 --- a/src/d/actor/d_a_e_mb.cpp +++ b/src/d/actor/d_a_e_mb.cpp @@ -12,6 +12,8 @@ #include "d/d_bomb.h" #include "c/c_damagereaction.h" #include "Z2AudioLib/Z2Instances.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" #define ACTION_STANDBY 0 #define ACTION_WALK1 1 @@ -63,6 +65,22 @@ static void anm_init(e_mb_class* i_this, int i_anmID, f32 i_morf, u8 i_attr, f32 i_this->mAnm = i_anmID; } +#if TARGET_PC +static void e_mb_rope_interp_callback(bool isSimFrame, void* pUserWork) { + e_mb_class* i_this = (e_mb_class*)pUserWork; + if (!i_this->mRopeInterpPrevValid || !i_this->mRopeInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->mRopeMat.getPos(0); + for (int i = 0; i < 16; i++) { + const cXyz& p0 = i_this->mRopeInterpPrev[i]; + const cXyz& p1 = i_this->mRopeInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daE_MB_Draw(e_mb_class* i_this) { fopAc_ac_c* a_this = (fopAc_ac_c*)i_this; @@ -86,6 +104,17 @@ static int daE_MB_Draw(e_mb_class* i_this) { static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF}; i_this->mRopeMat.update(16, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mRopeMat); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mRopeInterpCurrValid) { + memcpy(i_this->mRopeInterpPrev, i_this->mRopeInterpCurr, sizeof(i_this->mRopeInterpCurr)); + i_this->mRopeInterpPrevValid = true; + } + memcpy(i_this->mRopeInterpCurr, i_this->mRopeMat.getPos(0), 16 * sizeof(cXyz)); + i_this->mRopeInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&e_mb_rope_interp_callback, i_this); + } +#endif return 1; } diff --git a/src/d/actor/d_a_e_oct_bg.cpp b/src/d/actor/d_a_e_oct_bg.cpp index 14a421a3b6..94fa69596b 100644 --- a/src/d/actor/d_a_e_oct_bg.cpp +++ b/src/d/actor/d_a_e_oct_bg.cpp @@ -517,6 +517,12 @@ void daE_OctBg_c::core_fish_attack() { field_0xbaf = cM_rndFX(80.0f) + 100.0f; } } + #if AVOID_UB + else { + in_f31 = cM_rndF(400.0f) + 80.0f; + field_0xbaf = cM_rndFX(80.0f) + 100.0f; + } + #endif } else if (current.pos.abs(cStack_5c) < 400.0f) { in_f31 = cM_rndF(50.0f) + 20.0f; field_0xbaf = cM_rndFX(20.0f) + 40.0f; diff --git a/src/d/actor/d_a_e_s1.cpp b/src/d/actor/d_a_e_s1.cpp index 48cd7d6800..a5a05fce20 100644 --- a/src/d/actor/d_a_e_s1.cpp +++ b/src/d/actor/d_a_e_s1.cpp @@ -14,6 +14,8 @@ #include "d/d_s_play.h" #include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_camera_mng.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" #include class daE_S1_HIO_c { @@ -99,6 +101,25 @@ static void anm_init(e_s1_class* i_this, int i_resNo, f32 i_morf, u8 i_attr, f32 i_this->mAnm = i_resNo; } +#if TARGET_PC +static void daE_S1_interp_callback(bool isSimFrame, void* pUserWork) { + e_s1_class* i_this = (e_s1_class*)pUserWork; + if (!i_this->mHairInterpPrevValid || !i_this->mHairInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + for (int s = 0; s < e_s1_class::HAIR_STRAND_COUNT; s++) { + cXyz* dst = i_this->mLineMat.getPos(s); + for (int i = 0; i < e_s1_class::HAIR_SEGMENT_COUNT; i++) { + int idx = s * e_s1_class::HAIR_SEGMENT_COUNT + i; + const cXyz& p0 = i_this->mHairInterpPrev[idx]; + const cXyz& p1 = i_this->mHairInterpCurr[idx]; + dst[i] = p0 + (p1 - p0) * alpha; + } + } +} +#endif + static int daE_S1_Draw(e_s1_class* i_this) { if (i_this->field_0x306c != 0) { return 1; @@ -132,6 +153,22 @@ static int daE_S1_Draw(e_s1_class* i_this) { i_this->mLineMat.update(16, line_color, &i_this->tevStr); dComIfGd_set3DlineMatDark(&i_this->mLineMat); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mHairInterpCurrValid) { + memcpy(i_this->mHairInterpPrev, i_this->mHairInterpCurr, sizeof(i_this->mHairInterpCurr)); + i_this->mHairInterpPrevValid = true; + } + for (int s = 0; s < e_s1_class::HAIR_STRAND_COUNT; s++) { + cXyz* src = i_this->mLineMat.getPos(s); + memcpy(&i_this->mHairInterpCurr[s * e_s1_class::HAIR_SEGMENT_COUNT], src, + e_s1_class::HAIR_SEGMENT_COUNT * sizeof(cXyz)); + } + i_this->mHairInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_S1_interp_callback, i_this); + } +#endif + dComIfGd_setList(); return 1; } @@ -2149,6 +2186,11 @@ static int daE_S1_Create(fopAc_ac_c* i_this) { return cPhs_ERROR_e; } +#if TARGET_PC + a_this->mHairInterpPrevValid = false; + a_this->mHairInterpCurrValid = false; +#endif + OS_REPORT("//////////////E_S1 SET 2 !!\n"); if (path_no != 0xFF) { diff --git a/src/d/actor/d_a_e_sm2.cpp b/src/d/actor/d_a_e_sm2.cpp index fdbc338670..f55c4411cb 100644 --- a/src/d/actor/d_a_e_sm2.cpp +++ b/src/d/actor/d_a_e_sm2.cpp @@ -14,6 +14,10 @@ #include "f_op/f_op_camera_mng.h" #include +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + class daE_SM2_HIO_c : public fOpAcm_HIO_entry_c { public: daE_SM2_HIO_c(); @@ -76,8 +80,62 @@ static int nodeCallBack(J3DJoint* i_joint, int param_1) { return 1; } +#if TARGET_PC +static void daE_SM2_interp_callback(bool isSimFrame, void* pUserWork) { + e_sm2_class* i_this = static_cast(pUserWork); + if (i_this == NULL) { + return; + } + + fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->enemy; + g_env_light.settingTevStruct(0, &actor->current.pos, &actor->tevStr); + + if (!i_this->isPiece) { + if (i_this->modelMorf == NULL) { + return; + } + J3DModel* model = i_this->modelMorf->getModel(); + if (model == NULL) { + return; + } + g_env_light.setLightTevColorType_MAJI(model, &actor->tevStr); + + J3DMaterial* material = model->getModelData()->getMaterialNodePointer(0); + material->getTevKColor(1)->r = i_this->color_R; + material->getTevKColor(1)->g = i_this->color_G; + material->getTevKColor(1)->b = i_this->color_B; + material->getTevKColor(1)->a = 217.0f * i_this->color_alpha; + + if (i_this->pbtk != NULL) { + i_this->pbtk->entry(model->getModelData()); + } + } else { + if (i_this->pieceModelMorf == NULL) { + return; + } + J3DModel* model = i_this->pieceModelMorf->getModel(); + if (model == NULL) { + return; + } + + J3DMaterial* material = model->getModelData()->getMaterialNodePointer(0); + material->getTevKColor(1)->r = i_this->color_R; + material->getTevKColor(1)->g = i_this->color_G; + material->getTevKColor(1)->b = i_this->color_B; + material->getTevKColor(1)->a = 217.0f * i_this->color_alpha; + + g_env_light.setLightTevColorType_MAJI(model, &actor->tevStr); + } +} +#endif + static int daE_SM2_Draw(e_sm2_class* i_this) { fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->enemy; + +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&daE_SM2_interp_callback, i_this); +#endif + g_env_light.settingTevStruct(0, &actor->current.pos, &actor->tevStr); J3DModel* model; diff --git a/src/d/actor/d_a_e_th.cpp b/src/d/actor/d_a_e_th.cpp index f963350da4..056e9f565e 100644 --- a/src/d/actor/d_a_e_th.cpp +++ b/src/d/actor/d_a_e_th.cpp @@ -12,6 +12,9 @@ #include "c/c_damagereaction.h" #include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_camera_mng.h" +#if TARGET_PC +#include "dusk/achievements.h" +#endif class daE_TH_HIO_c : public JORReflexible { public: @@ -282,6 +285,11 @@ static void e_th_spin_B(e_th_class* i_this) { i_this->current.pos += spC; f32 speed_target; + + #if AVOID_UB + speed_target = 0; + #endif + f32 anm_frame = i_this->mpModelMorf->getFrame(); switch (i_this->mMode) { @@ -537,6 +545,7 @@ static void damage_check(e_th_class* i_this) { if (i_this->field_0x6a4 == 0 && i_this->mAction != ACTION_SPIN) { daPy_py_c* player = (daPy_py_c*)dComIfGp_getPlayer(0); OS_REPORT("E_th HP1 %d\n", i_this->health); + s16 prevHealth = i_this->health; cc_at_check(i_this, &i_this->mAtInfo); OS_REPORT("E_th HP2 %d\n", i_this->health); @@ -549,6 +558,11 @@ static void damage_check(e_th_class* i_this) { dComIfGs_onOneZoneSwitch(3, -1); if (i_this->health <= 0) { +#if TARGET_PC + if (prevHealth == i_this->field_0x560) { + dusk::AchievementSystem::get().signal("dark_hammer_one_hit"); + } +#endif i_this->mAction = ACTION_END; i_this->mMode = 0; i_this->field_0x68a |= 4; diff --git a/src/d/actor/d_a_e_wb.cpp b/src/d/actor/d_a_e_wb.cpp index 75031c42bb..47004737cb 100644 --- a/src/d/actor/d_a_e_wb.cpp +++ b/src/d/actor/d_a_e_wb.cpp @@ -5852,6 +5852,8 @@ static int daE_WB_Create(fopAc_ac_c* actor) { daE_WB_Execute(i_this); c_start = 0; + // Note: this flag makes king bulblin 1 instant die when set, as it only requires 2 laps + // for insta-kill to trigger. if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[88])) { i_this->lap_num = 1; } diff --git a/src/d/actor/d_a_e_yd.cpp b/src/d/actor/d_a_e_yd.cpp index bdcd044eed..18809e05ea 100644 --- a/src/d/actor/d_a_e_yd.cpp +++ b/src/d/actor/d_a_e_yd.cpp @@ -12,6 +12,10 @@ #include "d/d_cc_uty.h" #include "f_op/f_op_actor_enemy.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + class daE_YD_HIO_c { public: daE_YD_HIO_c(); @@ -73,6 +77,22 @@ static s32 leaf_anm_init(e_yd_class* i_this, int param_1, f32 param_2, u8 param_ return false; } +#if TARGET_PC +static void daE_YD_interp_callback(bool isSimFrame, void* pUserWork) { + e_yd_class* i_this = (e_yd_class*)pUserWork; + if (!i_this->mLineMatInterpPrevValid || !i_this->mLineMatInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->mLineMat.getPos(0); + for (int i = 0; i < 12; i++) { + const cXyz& p0 = i_this->mLineMatInterpPrev[i]; + const cXyz& p1 = i_this->mLineMatInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static s32 daE_YD_Draw(e_yd_class* i_this) { static GXColor l_color = { 0x14, 0x0F, 0x00, 0xFF }; @@ -86,6 +106,17 @@ static s32 daE_YD_Draw(e_yd_class* i_this) { i_this->mpMorf->entryDL(); i_this->mLineMat.update(12, l_color, &i_this->actor.tevStr); dComIfGd_set3DlineMat(&i_this->mLineMat); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mLineMatInterpCurrValid) { + memcpy(i_this->mLineMatInterpPrev, i_this->mLineMatInterpCurr, sizeof(i_this->mLineMatInterpCurr)); + i_this->mLineMatInterpPrevValid = true; + } + memcpy(i_this->mLineMatInterpCurr, i_this->mLineMat.getPos(0), 12 * sizeof(cXyz)); + i_this->mLineMatInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_YD_interp_callback, i_this); + } +#endif for (s32 i = 1; i < 11; i++) { if (i_this->field_0x77c[i] != 0) { g_env_light.setLightTevColorType_MAJI(i_this->field_0x77c[i], &i_this->actor.tevStr); diff --git a/src/d/actor/d_a_e_yg.cpp b/src/d/actor/d_a_e_yg.cpp index 9ab6c80e6d..44a8aee249 100644 --- a/src/d/actor/d_a_e_yg.cpp +++ b/src/d/actor/d_a_e_yg.cpp @@ -10,6 +10,8 @@ #include "f_op/f_op_kankyo_mng.h" #include "d/actor/d_a_obj_carry.h" #include "Z2AudioLib/Z2Instances.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" #include "f_op/f_op_actor_enemy.h" enum E_yg_RES_File_ID { @@ -134,7 +136,26 @@ static BOOL pl_check(e_yg_class* i_this, f32 i_dist) { return FALSE; } -static int daE_YG_Draw(e_yg_class* i_this) { +#if TARGET_PC +static void daE_YG_interp_callback(bool isSimFrame, void* pUserWork) { + e_yg_class* i_this = (e_yg_class*)pUserWork; + if (!i_this->mTentacleInterpPrevValid || !i_this->mTentacleInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + for (int s = 0; s < e_yg_class::TENTACLE_STRAND_COUNT; s++) { + cXyz* dst = i_this->mLineMat.getPos(s); + for (int i = 0; i < e_yg_class::TENTACLE_SEGMENT_COUNT; i++) { + int idx = s * e_yg_class::TENTACLE_SEGMENT_COUNT + i; + const cXyz& p0 = i_this->mTentacleInterpPrev[idx]; + const cXyz& p1 = i_this->mTentacleInterpCurr[idx]; + dst[i] = p0 + (p1 - p0) * alpha; + } + } +} +#endif + +static int daE_YG_Draw(e_yg_class* i_this) { if (i_this->mDispFlag) { return 1; } @@ -160,6 +181,23 @@ static int daE_YG_Draw(e_yg_class* i_this) { color.a = 0xFF; i_this->mLineMat.update(10, color, &actor->tevStr); dComIfGd_set3DlineMatDark(&i_this->mLineMat); + +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mTentacleInterpCurrValid) { + memcpy(i_this->mTentacleInterpPrev, i_this->mTentacleInterpCurr, sizeof(i_this->mTentacleInterpCurr)); + i_this->mTentacleInterpPrevValid = true; + } + for (int s = 0; s < e_yg_class::TENTACLE_STRAND_COUNT; s++) { + cXyz* src = i_this->mLineMat.getPos(s); + memcpy(&i_this->mTentacleInterpCurr[s * e_yg_class::TENTACLE_SEGMENT_COUNT], src, + e_yg_class::TENTACLE_SEGMENT_COUNT * sizeof(cXyz)); + } + i_this->mTentacleInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_YG_interp_callback, i_this); + } +#endif + dComIfGd_setList(); return 1; @@ -1378,6 +1416,11 @@ static cPhs_Step daE_YG_Create(fopAc_ac_c* actor) { return cPhs_ERROR_e; } +#if TARGET_PC + i_this->mTentacleInterpPrevValid = false; + i_this->mTentacleInterpCurrValid = false; +#endif + if (!hio_set) { i_this->mIsFirstSpawn = 1; hio_set = true; diff --git a/src/d/actor/d_a_e_yh.cpp b/src/d/actor/d_a_e_yh.cpp index bbbd3e5129..cf9955a7db 100644 --- a/src/d/actor/d_a_e_yh.cpp +++ b/src/d/actor/d_a_e_yh.cpp @@ -12,6 +12,10 @@ #include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_kankyo_mng.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + class daE_YH_HIO_c : public JORReflexible { public: daE_YH_HIO_c(); @@ -85,6 +89,22 @@ static BOOL leaf_anm_init(e_yh_class* i_this, int param_2, f32 param_3, u8 param return FALSE; } +#if TARGET_PC +static void daE_YH_interp_callback(bool isSimFrame, void* pUserWork) { + e_yh_class* i_this = (e_yh_class*)pUserWork; + if (!i_this->mLineInterpPrevValid || !i_this->mLineInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->mLine.getPos(0); + for (int i = 0; i < 12; i++) { + const cXyz& p0 = i_this->mLineInterpPrev[i]; + const cXyz& p1 = i_this->mLineInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daE_YH_Draw(e_yh_class* i_this) { fopAc_ac_c* a_this = (fopAc_ac_c*)i_this; @@ -114,6 +134,17 @@ static int daE_YH_Draw(e_yh_class* i_this) { i_this->mLine.update(12, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mLine); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mLineInterpCurrValid) { + memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, sizeof(i_this->mLineInterpCurr)); + i_this->mLineInterpPrevValid = true; + } + memcpy(i_this->mLineInterpCurr, i_this->mLine.getPos(0), 12 * sizeof(cXyz)); + i_this->mLineInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_YH_interp_callback, i_this); + } +#endif for (int i = 1; i < 11; i++) { if (i_this->mModels[i] != NULL) { diff --git a/src/d/actor/d_a_e_ym.cpp b/src/d/actor/d_a_e_ym.cpp index 758bec6269..3f6bef6788 100644 --- a/src/d/actor/d_a_e_ym.cpp +++ b/src/d/actor/d_a_e_ym.cpp @@ -1069,7 +1069,7 @@ void daE_YM_c::executeDown() { if (mAcch.ChkGroundHit()) { if (mFlyType != 1) { #if TARGET_PC - bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 2.0f : 1.0f); + bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears ? 2.0f : 1.0f); #else bckSet(6, 0, 0.0f, 1.0f); #endif @@ -1093,7 +1093,7 @@ void daE_YM_c::executeDown() { mSound.startCreatureSound(Z2SE_CM_BODYFALL_WATER_M, 0, -1); mSound.startCreatureSound(Z2SE_EN_YM_MOGAKU, 0, -1); #if TARGET_PC - bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 2.0f : 1.0f); + bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears ? 2.0f : 1.0f); #else bckSet(6, 0, 0.0f, 1.0f); #endif @@ -1115,7 +1115,7 @@ void daE_YM_c::executeDown() { || dComIfG_Bgsp().GetGroundCode(gnd_chk) == 5) { #if TARGET_PC - bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 2.0f : 1.0f); + bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears ? 2.0f : 1.0f); #else bckSet(6, 0, 0.0f, 1.0f); #endif diff --git a/src/d/actor/d_a_kago.cpp b/src/d/actor/d_a_kago.cpp index 147a031f7e..9444237f12 100644 --- a/src/d/actor/d_a_kago.cpp +++ b/src/d/actor/d_a_kago.cpp @@ -3519,7 +3519,15 @@ void daKago_c::action() { checkSizeBg(); setFlyEffect(); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + mStickX = -mDoCPd_c::getStickX3D(PAD_1); + } else { + mStickX = mDoCPd_c::getStickX3D(PAD_1); + } +#else mStickX = mDoCPd_c::getStickX3D(PAD_1); +#endif mStickY = mDoCPd_c::getStickY(PAD_1); u8 prevIsWaterfall = mIsWaterfall; diff --git a/src/d/actor/d_a_mant.cpp b/src/d/actor/d_a_mant.cpp index 282502419f..6a559c0e1c 100644 --- a/src/d/actor/d_a_mant.cpp +++ b/src/d/actor/d_a_mant.cpp @@ -13,9 +13,12 @@ #if TARGET_PC #include "dusk/dvd_asset.hpp" #include "dusk/frame_interpolation.h" -static u8* l_Egnd_mantTEX_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", 0x1C00, 0x4000), true); return buf; } -static u8* l_Egnd_mantTEX_U_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", 0x5C00, 0x4000), true); return buf; } -static u8* l_Egnd_mantPAL_get() { alignas(32) static u8 buf[0x60]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", 0x9C00, 0x60), true); return buf; } + +using GameVersion = dusk::version::GameVersion; + +static u8* l_Egnd_mantTEX_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x1C00}, {GameVersion::GcnPal, 0x1C00}}, 0x4000), true); return buf; } +static u8* l_Egnd_mantTEX_U_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x5C00}, {GameVersion::GcnPal, 0x5C00}}, 0x4000), true); return buf; } +static u8* l_Egnd_mantPAL_get() { alignas(32) static u8 buf[0x60]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x9C00}, {GameVersion::GcnPal, 0x9C00}}, 0x60), true); return buf; } #define l_Egnd_mantTEX (l_Egnd_mantTEX_get()) #define l_Egnd_mantTEX_U (l_Egnd_mantTEX_U_get()) #define l_Egnd_mantPAL (l_Egnd_mantPAL_get()) @@ -251,7 +254,9 @@ static u32 l_texCoord[338] = { }; #if TARGET_PC -static u8* l_Egnd_mantDL_get() { alignas(32) static u8 buf[0x3EC]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", 0xA9A0, 0x3EC), true); return buf; } +using GameVersion = dusk::version::GameVersion; + +static u8* l_Egnd_mantDL_get() { alignas(32) static u8 buf[0x3EC]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0xA9A0}, {GameVersion::GcnPal, 0xA9A0}}, 0x3EC), true); return buf; } #define l_Egnd_mantDL (l_Egnd_mantDL_get()) #else #include "assets/l_Egnd_mantDL.h" diff --git a/src/d/actor/d_a_mg_fish.cpp b/src/d/actor/d_a_mg_fish.cpp index 90781345ad..6791bcb61c 100644 --- a/src/d/actor/d_a_mg_fish.cpp +++ b/src/d/actor/d_a_mg_fish.cpp @@ -22,6 +22,7 @@ #include "d/d_s_play.h" #include "d/d_vibration.h" #include "f_op/f_op_kankyo_mng.h" +#include "dusk/version.hpp" #include #define ANM_MG_FISH_MOUTH_CLOSE 4 @@ -3230,7 +3231,17 @@ static int daMg_Fish_Execute(mg_fish_class* i_this) { daPy_py_c* player = daPy_getPlayerActorClass(); -#if VERSION == VERSION_GCN_JPN +#if TARGET_PC + if (dusk::version::isRegionJpn()) { + lit_1008 = 0; + } else if (dusk::version::isRegionPal()) { + if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { + lit_1008 = 2; + } else { + lit_1008 = 0; + } + } +#elif VERSION == VERSION_GCN_JPN lit_1008 = 0; #elif VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { @@ -3843,7 +3854,19 @@ static int daMg_Fish_Create(fopAc_ac_c* i_this) { a_this->mResName = "O_gD_bott"; } -#if VERSION == VERSION_GCN_JPN +#if TARGET_PC + if (dusk::version::isRegionJpn()) { + lit_1008 = 0; + } else if (dusk::version::isRegionPal()) { + if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { + lit_1008 = 2; + } else { + lit_1008 = 0; + } + } else { + lit_1008 = 1; + } +#elif VERSION == VERSION_GCN_JPN lit_1008 = 0; #elif VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { diff --git a/src/d/actor/d_a_mg_fshop.cpp b/src/d/actor/d_a_mg_fshop.cpp index 99f39e410e..0d5b6dd2c9 100644 --- a/src/d/actor/d_a_mg_fshop.cpp +++ b/src/d/actor/d_a_mg_fshop.cpp @@ -5,14 +5,15 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep +#include "Z2AudioLib/Z2Instances.h" +#include "d/actor/d_a_mg_fish.h" #include "d/actor/d_a_mg_fshop.h" #include "d/actor/d_a_npc_henna.h" -#include "d/actor/d_a_mg_fish.h" #include "d/actor/d_a_player.h" -#include "f_op/f_op_camera_mng.h" -#include "d/d_timer.h" #include "d/d_s_play.h" -#include "Z2AudioLib/Z2Instances.h" +#include "d/d_timer.h" +#include "dusk/version.hpp" +#include "f_op/f_op_camera_mng.h" #if TARGET_PC #include "dusk/gyro.h" @@ -728,10 +729,12 @@ static void koro2_game(fshop_class* i_this) { cLib_addCalcAngleS2(&i_this->field_0x4020.z, 0, 2, 0x200); case 2: #if TARGET_PC - if (dusk::getSettings().game.enableGyroRollgoal) { + if (dusk::gyro::rollgoal_gyro_enabled()) { if (!dusk::gyro::get_sensor_keep_alive()) { dusk::gyro::set_sensor_keep_alive(true); } + } else if (dusk::gyro::get_sensor_keep_alive()) { + dusk::gyro::set_sensor_keep_alive(false); } #endif @@ -752,7 +755,7 @@ static void koro2_game(fshop_class* i_this) { old_stick_x = mDoCPd_c::getSubStickX(PAD_1); cLib_addCalcAngleS2(&i_this->field_0x4060, i_this->field_0x4062, 4, 0x1000); #if TARGET_PC - if (dusk::getSettings().game.enableGyroRollgoal) { + if (dusk::gyro::rollgoal_gyro_enabled()) { dusk::gyro::rollgoalTick(true, i_this->field_0x4060); } #endif @@ -790,7 +793,7 @@ static void koro2_game(fshop_class* i_this) { s16 gyro_ax = 0; s16 gyro_az = 0; #if TARGET_PC - if (dusk::getSettings().game.enableGyroRollgoal) { + if (dusk::gyro::rollgoal_gyro_enabled()) { dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az); } #endif @@ -1742,7 +1745,18 @@ static int daFshop_Create(fopAc_ac_c* actor) { fopAcM_createChild(fpcNm_FSHOP_e, fopAcM_GetID(actor), 0xFFFFFF23, &actor->current.pos, fopAcM_GetRoomNo(actor), NULL, NULL, -1, NULL); u8 sp10; -#if VERSION == VERSION_GCN_PAL || VERSION == VERSION_WII_PAL +#if TARGET_PC + if (dusk::version::isRegionPal()) { + if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { + sp10 = 2; + } else { + sp10 = 0; + } + } else { + sp10 = 1; + } + +#elif VERSION == VERSION_GCN_PAL || VERSION == VERSION_WII_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { sp10 = 2; } else { diff --git a/src/d/actor/d_a_mg_rod.cpp b/src/d/actor/d_a_mg_rod.cpp index 96ad7aa148..956a9e3cff 100644 --- a/src/d/actor/d_a_mg_rod.cpp +++ b/src/d/actor/d_a_mg_rod.cpp @@ -25,6 +25,8 @@ #include #include +#include "dusk/version.hpp" + class dmg_rod_HIO_c : public JORReflexible { public: dmg_rod_HIO_c(); @@ -5734,10 +5736,22 @@ static void play_camera_u(dmg_rod_class* i_this) { static int dmg_rod_Execute(dmg_rod_class* i_this) { fopAc_ac_c* actor = &i_this->actor; + #if TARGET_PC + if (dusk::version::isPalOrAtLeastWiiR2()) { + if (dComIfGs_getPalLanguage() == 0) { + data_804BBBD4 = 2; + } else { + data_804BBBD4 = 0; + } + } else if (dusk::version::isRegionJpn()) { + data_804BBBD4 = 0; + } else { + data_804BBBD4 = 1; + } //TODO: It seems possible that dComIfGs_getPalLanguage returns a constant value for non-PAL // versions (causing the first block to be elided), and it's also possible that the value // being compared against is an enum value with per-version definitions. - #if VERSION == VERSION_SHIELD_DEBUG + #elif VERSION == VERSION_SHIELD_DEBUG if (dComIfGs_getPalLanguage() == 1) { data_804BBBD4 = 2; } else { @@ -6303,7 +6317,19 @@ static int dmg_rod_Create(fopAc_ac_c* i_this) { heap_size = 0xC9A0; } - #if VERSION == VERSION_SHIELD_DEBUG + #if TARGET_PC + if (dusk::version::isPalOrAtLeastWiiR2()) { + if (dComIfGs_getPalLanguage() == 0) { + data_804BBBD4 = 2; + } else { + data_804BBBD4 = 0; + } + } else if (dusk::version::isRegionJpn()) { + data_804BBBD4 = 0; + } else { + data_804BBBD4 = 1; + } + #elif VERSION == VERSION_SHIELD_DEBUG if (dComIfGs_getPalLanguage() == 1) { data_804BBBD4 = 2; } else { diff --git a/src/d/actor/d_a_midna.cpp b/src/d/actor/d_a_midna.cpp index 7bb844932e..3bc500991d 100644 --- a/src/d/actor/d_a_midna.cpp +++ b/src/d/actor/d_a_midna.cpp @@ -419,9 +419,30 @@ int daMidna_c::createHeap() { return 0; } +#if TARGET_PC + if (mpDemoFCTongueBmd != NULL) { + if(!daAlink_c::initDemoBck(&mpDemoFCTmpBck, "demo00_Midna_cut00_FC_tmp.bck")) { + return 0; + } + + // Update Midna's eye maxLOD to prevent the eyes from disappearing + J3DTexture* tex = mpDemoFCTongueBmd->getModelData()->getTexture(); + JUTNameTab* nametable = mpDemoFCTongueBmd->getModelData()->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != NULL && strcmp(tex_name, "midona_eyeball") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } + } +#else if (mpDemoFCTongueBmd != NULL && !daAlink_c::initDemoBck(&mpDemoFCTmpBck, "demo00_Midna_cut00_FC_tmp.bck")) { return 0; } +#endif modelData = (J3DModelData*)dComIfG_getObjectRes(dStage_roomControl_c::getDemoArcName(), "demo00_Midna_cut00_BD_tmp.bmd"); @@ -433,6 +454,21 @@ int daMidna_c::createHeap() { modelData->getMaterialNodePointer(2)->setMaterialAnm(mpEyeMatAnm[0]); modelData->getMaterialNodePointer(3)->setMaterialAnm(mpEyeMatAnm[1]); + +#if TARGET_PC + // Update Midna's eye maxLOD to prevent the eyes from disappearing + J3DTexture* tex = modelData->getTexture(); + JUTNameTab* nametable = modelData->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != NULL && strcmp(tex_name, "midona_eyeball") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } +#endif } if (!initDemoModel(&mpDemoBDMaskBmd, "demo00_Midna_cut00_BD_mask.bmd", 0)) { @@ -463,6 +499,23 @@ int daMidna_c::createHeap() { JKRReadIdxResource(mBckHeap[0].getBuffer(), mBckHeap[0].getBufferSize(), 0x1DC, dComIfGp_getAnmArchive()); J3DAnmTransform* md_anm = (J3DAnmTransform*)J3DAnmLoaderDataBase::load(mBckHeap[0].getBuffer()); modelData = (J3DModelData*)dComIfG_getObjectRes(l_arcName, 14); + +#if TARGET_PC + J3DTexture* tex = modelData->getTexture(); + JUTNameTab* nametable = modelData->getTextureName(); + if (tex != NULL && nametable != NULL) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* name = nametable->getName(i); + if (name != NULL && strcmp(name, "midona_eye") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->mipmapEnabled = false; + tex->loadGXTexObj(i); + break; + } + } + } +#endif + JUT_ASSERT(852, modelData != NULL); mpMorf = JKR_NEW mDoExt_McaMorfSO(modelData, &mMorfCB, NULL, md_anm, J3DFrameCtrl::EMode_LOOP, 1.0f, 0, -1, NULL, 0, 0x11000284); if (mpMorf == NULL || mpMorf->getModel() == NULL) { diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index fb4142c0bb..5c5218328e 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -40,6 +40,7 @@ dMirror_packet_c::dMirror_packet_c() { void dMirror_packet_c::reset() { #if TARGET_PC mbReset = true; + mbHadEntry = false; #else mModelCount = 0; #endif @@ -96,6 +97,9 @@ int dMirror_packet_c::entryModel(J3DModel* i_model) { } mModels[mModelCount++] = i_model; +#if TARGET_PC + mbHadEntry = true; +#endif return 1; } @@ -624,6 +628,12 @@ int daMirror_c::draw() { mDoExt_modelUpdateDL(mpModel); } +#if TARGET_PC + if (mPacket.mbReset && !mPacket.mbHadEntry) { + mPacket.mModelCount = 0; + } + mPacket.mbHadEntry = true; +#endif dComIfGd_getOpaListBG()->entryImm(&mPacket, 0); return 1; } diff --git a/src/d/actor/d_a_movie_player.cpp b/src/d/actor/d_a_movie_player.cpp index 079584f714..1f8bdeecd7 100644 --- a/src/d/actor/d_a_movie_player.cpp +++ b/src/d/actor/d_a_movie_player.cpp @@ -2855,7 +2855,7 @@ void* daMP_Reader(void*) { #endif } -static u8 daMP_ReadThreadStack[0x2000]; +static u8 daMP_ReadThreadStack[DUSK_IF_ELSE(8, 0x2000)]; #if TARGET_PC static BOOL VideoThreadCancelled; @@ -2880,7 +2880,7 @@ static BOOL daMP_CreateReadThread(s32 param_0) { static OSThread daMP_VideoDecodeThread; -static u8 daMP_VideoDecodeThreadStack[0x64000]; +static u8 daMP_VideoDecodeThreadStack[DUSK_IF_ELSE(8, 0x64000)]; static OSMessageQueue daMP_FreeTextureSetQueue; @@ -3132,7 +3132,7 @@ static BOOL AudioThreadCancelled; static OSThread daMP_AudioDecodeThread; -static u8 daMP_AudioDecodeThreadStack[0x64000]; +static u8 daMP_AudioDecodeThreadStack[DUSK_IF_ELSE(8, 0x64000)]; static OSMessageQueue daMP_FreeAudioBufferQueue; diff --git a/src/d/actor/d_a_npc_ne.cpp b/src/d/actor/d_a_npc_ne.cpp index 1324c56cf5..90e33adea5 100644 --- a/src/d/actor/d_a_npc_ne.cpp +++ b/src/d/actor/d_a_npc_ne.cpp @@ -956,7 +956,7 @@ static void npc_ne_tame(npc_ne_class* i_this) { i_this->mpMorf->setPlaySpeed(i_this->mAnmSpeed); /* dSv_event_flag_c::F_0470 - Fishing Pond - Reserved for fishing */ - if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])) { + if (IF_DUSK(dusk::getSettings().game.no2ndFishForCat) || dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])) { if (fpcEx_Search(s_fish_sub, _this) != NULL) { i_this->mAction = npc_ne_class::ACT_HOME; i_this->mMode = 10; @@ -2948,8 +2948,7 @@ static int daNpc_Ne_Execute(npc_ne_class* i_this) { if (i_this->mWantsFish && (i_this->mCounter & 0xf) == 0) { /* dSv_event_flag_c::F_0470 - Fishing Pond - Reserved for fishing */ - if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470]) - && i_this->mDistToTarget < 1500.0f) { + if ((IF_DUSK(dusk::getSettings().game.no2ndFishForCat) || dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])) && i_this->mDistToTarget < 1500.0f) { if (fopAcM_SearchByName(fpcNm_MG_ROD_e) != NULL) { i_this->mNoFollow = false; } else { diff --git a/src/d/actor/d_a_npc_toby.cpp b/src/d/actor/d_a_npc_toby.cpp index 543d4b9abf..d175d199b7 100644 --- a/src/d/actor/d_a_npc_toby.cpp +++ b/src/d/actor/d_a_npc_toby.cpp @@ -14,6 +14,7 @@ #include "d/actor/d_a_obj_automata.h" #include "d/d_msg_object.h" #include "d/actor/d_a_obj_scannon.h" +#include "dusk/frame_interpolation.h" #include const daNpc_Toby_HIOParam daNpc_Toby_Param_c::m = { @@ -1398,6 +1399,7 @@ int daNpc_Toby_c::cutRepairSCannon(int arg0) { old.pos = current.pos; setAngle(cM_deg2s(5.0f * f32(mPath.getArg0()))); mEventTimer = mPath.getArg2(); + dusk::frame_interp::request_presentation_sync(); } } else if (!mHide) { mHide = 1; diff --git a/src/d/actor/d_a_obj_balloon.cpp b/src/d/actor/d_a_obj_balloon.cpp index 41430f97f0..d857a569ba 100644 --- a/src/d/actor/d_a_obj_balloon.cpp +++ b/src/d/actor/d_a_obj_balloon.cpp @@ -62,6 +62,16 @@ void daObj_Balloon_c::saveBestScore() { dComIfGp_setMessageCountNumber(m_balloon_score); } +#if TARGET_PC +static void minigameReset() { + // !@bug d_a_obj_balloon.rel unload used to zero these file-statics; with static linking they dangle across scenes. + m_combo_type = 0xFFFFFFFF; + m_combo_count = 0; + m_combo_next_score = 0; + m_balloon_score = 0; +} +#endif + static u8 hio_set; static daObj_Balloon_HIO_c l_HIO; @@ -246,6 +256,7 @@ int daObj_Balloon_c::create() { } if (!hio_set) { + IF_DUSK(minigameReset()); mHIOInit = true; hio_set = true; l_HIO.field_0x04 = -1; diff --git a/src/d/actor/d_a_obj_drop.cpp b/src/d/actor/d_a_obj_drop.cpp index 275a3d6e12..18d27840f7 100644 --- a/src/d/actor/d_a_obj_drop.cpp +++ b/src/d/actor/d_a_obj_drop.cpp @@ -265,8 +265,8 @@ int daObjDrop_c::modeParentWait() { mModeAction = 1; #if TARGET_PC - mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 20 : 40; - if (dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0) { + mModeTimer = dusk::getSettings().game.fastTears ? 0 : 40; + if (dusk::getSettings().game.fastTears) { current.pos.y += 100.0f; } else { current.pos.y += 300.0f; @@ -285,7 +285,7 @@ int daObjDrop_c::modeParentWait() { case 2: createBodyEffect(); #if TARGET_PC - mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 5 : 45; + mModeTimer = dusk::getSettings().game.fastTears ? 0 : 45; #else mModeTimer = 45; #endif @@ -331,7 +331,7 @@ int daObjDrop_c::modeWait() { case 2: case 50: #if TARGET_PC - if (dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0) { + if (dusk::getSettings().game.fastTears) { f32 player_dist = current.pos.abs(daPy_getPlayerActorClass()->current.pos); f32 home_dist = current.pos.abs(home.pos); diff --git a/src/d/actor/d_a_obj_fchain.cpp b/src/d/actor/d_a_obj_fchain.cpp index e681fec4e7..024b4ecf8b 100644 --- a/src/d/actor/d_a_obj_fchain.cpp +++ b/src/d/actor/d_a_obj_fchain.cpp @@ -10,6 +10,8 @@ #include "JSystem/J3DGraphBase/J3DDrawBuffer.h" #include "SSystem/SComponent/c_math.h" #include "d/d_com_inf_game.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" #include static char const l_arcName[] = "Fchain"; @@ -65,6 +67,10 @@ int daObjFchain_c::create() { local_48++; } rv = cPhs_COMPLEATE_e; +#if TARGET_PC + mChainInterpPrevValid = false; + mChainInterpCurrValid = false; +#endif break; } return rv; @@ -289,6 +295,26 @@ void daObjFchain_shape_c::draw() { } } +#if TARGET_PC +static void fchain_interp_callback(bool isSimFrame, void* pUserWork) { + static_cast(pUserWork)->onInterpCallback(); +} + +void daObjFchain_c::onInterpCallback() { + if (!mChainInterpPrevValid || !mChainInterpCurrValid) { + return; + } + + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + + for (int i = 0; i < CHAIN_COUNT; i++) { + const cXyz& p0 = mChainInterpPrev[i]; + const cXyz& p1 = mChainInterpCurr[i]; + field_0x694[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + int daObjFchain_c::draw() { if (field_0x584 != 0) { g_env_light.settingTevStruct(0, ¤t.pos, &tevStr); @@ -297,6 +323,19 @@ int daObjFchain_c::draw() { return 1; } dComIfGd_getOpaListDark()->entryImm(&mShape, 0); + +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (mChainInterpCurrValid) { + memcpy(mChainInterpPrev, mChainInterpCurr, sizeof(mChainInterpCurr)); + mChainInterpPrevValid = true; + } + + memcpy(mChainInterpCurr, field_0x694, sizeof(mChainInterpCurr)); + mChainInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&fchain_interp_callback, this); + } +#endif } return 1; } diff --git a/src/d/actor/d_a_obj_item.cpp b/src/d/actor/d_a_obj_item.cpp index 6c7e82ab3e..e45de1933d 100644 --- a/src/d/actor/d_a_obj_item.cpp +++ b/src/d/actor/d_a_obj_item.cpp @@ -16,6 +16,10 @@ #include "f_op/f_op_camera_mng.h" #include "m_Do/m_Do_mtx.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + static f32 Reflect(cXyz* i_vec, cBgS_PolyInfo const& i_polyinfo, f32 i_scale) { cM3dGPla plane; @@ -31,6 +35,27 @@ static f32 Reflect(cXyz* i_vec, cBgS_PolyInfo const& i_polyinfo, f32 i_scale) { return 0.0f; } +#if TARGET_PC +static void d_a_obj_item_interp_callback(bool isSimFrame, void* pUserWork) { + daItem_c* item = static_cast(pUserWork); + if (item == NULL || item->mpModel == NULL || !item->chkDraw()) { + return; + } + item->setTevStr(); + if (item->mpBrkAnm != NULL) { + s8 tevFrm = item->getTevFrm(); + if (tevFrm != -1) { + item->mpBrkAnm->entry(item->mpModel->getModelData(), tevFrm); + } else { + item->mpBrkAnm->entry(item->mpModel->getModelData()); + } + } + if (item->chkFlag(4)) { + fopAcM_setEffectMtx(item, item->mpModel->getModelData()); + } +} +#endif + const daItemBase_data& daItemBase_c::getData() { return m_data; } @@ -353,6 +378,10 @@ int daItem_c::_daItem_draw() { return 1; } +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&d_a_obj_item_interp_callback, this); +#endif + if (chkDraw()) { return DrawBase(); } diff --git a/src/d/actor/d_a_obj_klift00.cpp b/src/d/actor/d_a_obj_klift00.cpp index 99b204592e..e44cce7467 100644 --- a/src/d/actor/d_a_obj_klift00.cpp +++ b/src/d/actor/d_a_obj_klift00.cpp @@ -11,6 +11,8 @@ #include "d/d_bg_w.h" #include "d/d_cc_uty.h" #include "d/d_com_inf_game.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" struct daObjKLift00_HIO_c : public mDoHIO_entry_c { daObjKLift00_HIO_c(); @@ -295,6 +297,11 @@ int daObjKLift00_c::Create() { if(getLock()) mStopSwingingFrames = 5; +#if TARGET_PC + mChainInterpPrevValid = false; + mChainInterpCurrValid = false; +#endif + return 1; } @@ -436,6 +443,34 @@ int daObjKLift00_c::Execute(Mtx** i_mtx) { return 1; } +#if TARGET_PC +static void klift00_interp_callback(bool isSimFrame, void* pUserWork) { + static_cast(pUserWork)->onInterpCallback(); +} + +void daObjKLift00_c::onInterpCallback() { + if (!mChainInterpPrevValid || !mChainInterpCurrValid) { + return; + } + + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz savedPositions[64]; + + for (int i = 0; i < mNumChains; i++) { + savedPositions[i] = mChainPositions[i].mCurrentPos; + const cXyz& p0 = mChainInterpPrev[i]; + const cXyz& p1 = mChainInterpCurr[i]; + mChainPositions[i].mCurrentPos = p0 + (p1 - p0) * alpha; + } + + setMtx(); + + for (int i = 0; i < mNumChains; i++) { + mChainPositions[i].mCurrentPos = savedPositions[i]; + } +} +#endif + int daObjKLift00_c::Draw() { g_env_light.settingTevStruct(16, ¤t.pos, &tevStr); g_env_light.setLightTevColorType_MAJI(mpLiftPlatform, &tevStr); @@ -457,6 +492,22 @@ int daObjKLift00_c::Draw() { dComIfGd_setList(); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (mChainInterpCurrValid) { + memcpy(mChainInterpPrev, mChainInterpCurr, mNumChains * sizeof(cXyz)); + mChainInterpPrevValid = true; + } + + for (int i = 0; i < mNumChains; i++) { + mChainInterpCurr[i] = mChainPositions[i].mCurrentPos; + } + + mChainInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&klift00_interp_callback, this); + } +#endif + return 1; } diff --git a/src/d/actor/d_a_obj_lv8Lift.cpp b/src/d/actor/d_a_obj_lv8Lift.cpp index 4f475a626c..8fa9422325 100644 --- a/src/d/actor/d_a_obj_lv8Lift.cpp +++ b/src/d/actor/d_a_obj_lv8Lift.cpp @@ -10,6 +10,10 @@ #include "d/d_path.h" #include "d/d_bg_w.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + daL8Lift_HIO_c::daL8Lift_HIO_c() { mStopDisappearTime = 30; mStartMoveTime = 60; @@ -380,7 +384,44 @@ void daL8Lift_c::setNextPoint() { mCurrentPoint = next_point; } +#if TARGET_PC +void daL8Lift_interp_callback(bool isSimFrame, void* pUserWork) { + daL8Lift_c* lift = static_cast(pUserWork); + if (lift == NULL || lift->mpModel == NULL) { + return; + } + + g_env_light.settingTevStruct(0x10, &lift->current.pos, &lift->tevStr); + g_env_light.setLightTevColorType_MAJI(lift->mpModel, &lift->tevStr); + + J3DModelData* modelData = lift->mpModel->getModelData(); + J3DMaterial* materialp = modelData->getMaterialNodePointer(0); + + if (materialp->getTexGenBlock()->getTexMtx(1) != NULL) { + J3DTexMtxInfo* mtx_info = &materialp->getTexGenBlock()->getTexMtx(1)->getTexMtxInfo(); + if (mtx_info != NULL) { + Mtx m; + C_MTXLightOrtho(m, 100.0f, -100.0f, -100.0f, 100.0f, 1.0f, 1.0f, 0.0f, 0.0f); + mDoMtx_stack_c::XrotS(0x4000); + mDoMtx_stack_c::transM(-lift->current.pos.x, -lift->current.pos.y, -lift->current.pos.z); + cMtx_concat(m, mDoMtx_stack_c::get(), mtx_info->mEffectMtx); + } + } + + lift->mBtk.entry(modelData); + + J3DGXColor* color = materialp->getTevKColor(1); + color->r = l_HIO.mColorR; + color->g = l_HIO.mColorG; + color->b = l_HIO.mColorB; +} +#endif + int daL8Lift_c::Draw() { +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&daL8Lift_interp_callback, this); +#endif + g_env_light.settingTevStruct(16, ¤t.pos, &tevStr); g_env_light.setLightTevColorType_MAJI(mpModel, &tevStr); J3DModelData* modelData = mpModel->getModelData(); diff --git a/src/d/actor/d_a_obj_lv8OptiLift.cpp b/src/d/actor/d_a_obj_lv8OptiLift.cpp index a8610989a7..6b6c806d64 100644 --- a/src/d/actor/d_a_obj_lv8OptiLift.cpp +++ b/src/d/actor/d_a_obj_lv8OptiLift.cpp @@ -11,6 +11,10 @@ #include "d/d_com_inf_game.h" #include "d/d_path.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + daOptiLift_HIO_c::daOptiLift_HIO_c() { mStopDisappearTime = 30; mStartMoveTime = 30; @@ -412,7 +416,44 @@ void daOptiLift_c::setNextPoint() { mCurrentPoint = next_point; } +#if TARGET_PC +static void daOptiLift_interp_callback(bool isSimFrame, void* pUserWork) { + daOptiLift_c* lift = static_cast(pUserWork); + if (lift == NULL || lift->mpModel == NULL) { + return; + } + + g_env_light.settingTevStruct(0x10, &lift->current.pos, &lift->tevStr); + g_env_light.setLightTevColorType_MAJI(lift->mpModel, &lift->tevStr); + + J3DModelData* modelData = lift->mpModel->getModelData(); + J3DMaterial* materialp = modelData->getMaterialNodePointer(0); + + if (materialp->getTexGenBlock()->getTexMtx(1) != NULL) { + J3DTexMtxInfo* mtx_info = &materialp->getTexGenBlock()->getTexMtx(1)->getTexMtxInfo(); + if (mtx_info != NULL) { + Mtx m; + C_MTXLightOrtho(m, 100.0f, -100.0f, -100.0f, 100.0f, 1.0f, 1.0f, 0.0f, 0.0f); + mDoMtx_stack_c::XrotS(0x4000); + mDoMtx_stack_c::transM(-lift->current.pos.x, -lift->current.pos.y, -lift->current.pos.z); + cMtx_concat(m, mDoMtx_stack_c::get(), mtx_info->mEffectMtx); + } + } + + lift->mBtk.entry(modelData); + + J3DGXColor* color = materialp->getTevKColor(1); + color->r = l_HIO.mColorR; + color->g = l_HIO.mColorG; + color->b = l_HIO.mColorB; +} +#endif + int daOptiLift_c::Draw() { +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&daOptiLift_interp_callback, this); +#endif + g_env_light.settingTevStruct(0x10, ¤t.pos, &tevStr); g_env_light.setLightTevColorType_MAJI(mpModel, &tevStr); diff --git a/src/d/actor/d_a_player.cpp b/src/d/actor/d_a_player.cpp index 8ceb0ef237..6148d23e3a 100644 --- a/src/d/actor/d_a_player.cpp +++ b/src/d/actor/d_a_player.cpp @@ -369,18 +369,17 @@ JKRHeap* daPy_anmHeap_c::setAnimeHeap() { #if !PLATFORM_WII #if TARGET_PC #include "dusk/dvd_asset.hpp" +using GameVersion = dusk::version::GameVersion; static const u8* l_sightDL_get() { static u8 buf[0x89]; static bool _ = ( dusk::LoadDolAsset( buf, - #if VERSION == VERSION_GCN_PAL - 0x803BBDA0, - #elif VERSION == VERSION_GCN_JPN - 0x803B4220, - #elif VERSION == VERSION_GCN_USA - 0x803BA0C0, - #endif +{ + {GameVersion::GcnUsa, 0x803BA0C0}, + {GameVersion::GcnPal, 0x803BBDA0}, + {GameVersion::GcnJpn, 0x803B4220} + }, 0x89 ), true diff --git a/src/d/actor/d_a_title.cpp b/src/d/actor/d_a_title.cpp index 6df52abc5b..a75a96cb2e 100644 --- a/src/d/actor/d_a_title.cpp +++ b/src/d/actor/d_a_title.cpp @@ -1,21 +1,22 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep +#include "JSystem/J2DGraph/J2DScreen.h" +#include "JSystem/J2DGraph/J2DTextBox.h" +#include "JSystem/JKernel/JKRExpHeap.h" +#include "JSystem/JKernel/JKRMemArchive.h" #include "d/actor/d_a_title.h" +#include "d/d_com_inf_game.h" +#include "d/d_demo.h" +#include "d/d_menu_collect.h" +#include "d/d_pane_class_alpha.h" #include "d/d_s_logo.h" #include "d/d_s_play.h" -#include "d/d_demo.h" -#include "d/d_pane_class_alpha.h" -#include "d/d_menu_collect.h" +#include "dusk/version.hpp" +#include "f_op/f_op_msg_mng.h" +#include "f_op/f_op_overlap_mng.h" +#include "f_op/f_op_scene_mng.h" #include "m_Do/m_Do_Reset.h" #include "m_Do/m_Do_controller_pad.h" -#include "d/d_com_inf_game.h" -#include "JSystem/JKernel/JKRExpHeap.h" -#include "f_op/f_op_overlap_mng.h" -#include "f_op/f_op_msg_mng.h" -#include "f_op/f_op_scene_mng.h" -#include "JSystem/J2DGraph/J2DScreen.h" -#include "JSystem/JKernel/JKRMemArchive.h" -#include "JSystem/J2DGraph/J2DTextBox.h" #include "m_Do/m_Do_graphic.h" #ifdef TARGET_PC @@ -49,7 +50,10 @@ static u8 const lit_3772[12] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC +using namespace dusk::version; +#define l_arcName versionSelect({{GameVersion::GcnPal, "TitlePal"}}, "Title") +#elif VERSION == VERSION_GCN_PAL static char const l_arcName[] = "TitlePal"; #else static char const l_arcName[] = "Title"; @@ -59,7 +63,7 @@ daTit_HIO_c::daTit_HIO_c() { mPSScaleX = 1.0f; mPSScaleY = 1.0f; - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL switch (OSGetLanguage()) { case OS_LANGUAGE_ENGLISH: case OS_LANGUAGE_GERMAN: diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 58daa354a1..4024d5ad6a 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -7,17 +7,13 @@ #include "dusk/frame_interpolation.h" -#if TARGET_PC -const u16 l_J_Ohana00_64TEX__width = 64; -const u16 l_J_Ohana00_64TEX__height = 64; -#else const u16 l_J_Ohana00_64TEX__width = 63; const u16 l_J_Ohana00_64TEX__height = 63; -#endif #if TARGET_PC #include "dusk/dvd_asset.hpp" -static u8* l_J_Ohana00_64TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x9060, 0x800), true); return buf; } +using GameVersion = dusk::version::GameVersion; +static u8* l_J_Ohana00_64TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9060}, {GameVersion::GcnPal, 0x9060}}, 0x800), true); return buf; } #define l_J_Ohana00_64TEX (l_J_Ohana00_64TEX_get()) #else #include "assets/l_J_Ohana00_64TEX.h" @@ -113,10 +109,12 @@ static u8 l_flowerTexCoord[] = { 0x3E, 0xA7, 0x72, 0xD6, 0xBD, 0x2F, 0x46, 0xAA}; #if TARGET_PC -static u8* l_J_hana00DL_get() { static u8 buf[0x150]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x9D20, 0x150), true); return buf; } -static u8* l_J_hana00_cDL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x9E80, 0xDE), true); return buf; } -static u8* l_matDL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x9F60, 0x99), true); return buf; } -static u8* l_matLight4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xA000, 0x99), true); return buf; } +using GameVersion = dusk::version::GameVersion; + +static u8* l_J_hana00DL_get() { static u8 buf[0x150]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9D20}, {GameVersion::GcnPal, 0x9D20}}, 0x150), true); return buf; } +static u8* l_J_hana00_cDL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9E80}, {GameVersion::GcnPal, 0x9E80}}, 0xDE), true); return buf; } +static u8* l_matDL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9F60}, {GameVersion::GcnPal, 0x9F60}}, 0x99), true); return buf; } +static u8* l_matLight4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xA000}, {GameVersion::GcnPal, 0xA000}}, 0x99), true); return buf; } #define l_J_hana00DL (l_J_hana00DL_get()) #define l_J_hana00_cDL (l_J_hana00_cDL_get()) #define l_matDL (l_matDL_get()) @@ -133,16 +131,12 @@ l_matDL__d_a_grass(l_J_Ohana00_64TEX) l_matLight4DL(l_J_Ohana00_64TEX) #endif -#if TARGET_PC -const u16 l_J_Ohana01_64128_0419TEX__width = 64; -const u16 l_J_Ohana01_64128_0419TEX__height = 128; -#else const u16 l_J_Ohana01_64128_0419TEX__width = 63; const u16 l_J_Ohana01_64128_0419TEX__height = 127; -#endif #if TARGET_PC -static u8* l_J_Ohana01_64128_0419TEX_get() { static u8 buf[0x1000]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xA0A0, 0x1000), true); return buf; } +using GameVersion = dusk::version::GameVersion; +static u8* l_J_Ohana01_64128_0419TEX_get() { static u8 buf[0x1000]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xA0A0}, {GameVersion::GcnPal, 0xA0A0}}, 0x1000), true); return buf; } #define l_J_Ohana01_64128_0419TEX (l_J_Ohana01_64128_0419TEX_get()) #else #include "assets/l_J_Ohana01_64128_0419TEX.h" @@ -274,11 +268,13 @@ static u8 l_flowerTexCoord2[] = { 0x40, 0x1B, 0x7D, 0x52, 0x3F, 0x80, 0x3F, 0x79, 0x40, 0x1B, 0x7D, 0x52, 0x3F, 0x51, 0x10, 0x6F}; #if TARGET_PC -static u8* l_J_hana01DL_get() { static u8 buf[0x138]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xB7C0, 0x138), true); return buf; } -static u8* l_J_hana01_c_00DL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xB900, 0xDE), true); return buf; } -static u8* l_J_hana01_c_01DL_get() { static u8 buf[0x128]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xB9E0, 0x128), true); return buf; } -static u8* l_mat2DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xBB20, 0x99), true); return buf; } -static u8* l_mat2Light4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xBBC0, 0x99), true); return buf; } +using GameVersion = dusk::version::GameVersion; + +static u8* l_J_hana01DL_get() { static u8 buf[0x138]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xB7C0}, {GameVersion::GcnPal, 0xB7C0}}, 0x138), true); return buf; } +static u8* l_J_hana01_c_00DL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xB900}, {GameVersion::GcnPal, 0xB900}}, 0xDE), true); return buf; } +static u8* l_J_hana01_c_01DL_get() { static u8 buf[0x128]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xB9E0}, {GameVersion::GcnPal, 0xB9E0}}, 0x128), true); return buf; } +static u8* l_mat2DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xBB20}, {GameVersion::GcnPal, 0xBB20}}, 0x99), true); return buf; } +static u8* l_mat2Light4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xBBC0}, {GameVersion::GcnPal, 0xBBC0}}, 0x99), true); return buf; } #define l_J_hana01DL (l_J_hana01DL_get()) #define l_J_hana01_c_00DL (l_J_hana01_c_00DL_get()) #define l_J_hana01_c_01DL (l_J_hana01_c_01DL_get()) @@ -586,11 +582,11 @@ dFlower_packet_c::dFlower_packet_c() { #if TARGET_PC GXInitTexObj(&mTexObj_l_J_Ohana00_64TEX, l_J_Ohana00_64TEX, - l_J_Ohana00_64TEX__width, l_J_Ohana00_64TEX__height, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE + l_J_Ohana00_64TEX__width + 1, l_J_Ohana00_64TEX__height + 1, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE ); GXInitTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, l_J_Ohana01_64128_0419TEX, - l_J_Ohana01_64128_0419TEX__width, l_J_Ohana01_64128_0419TEX__height, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE + l_J_Ohana01_64128_0419TEX__width + 1, l_J_Ohana01_64128_0419TEX__height + 1, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE ); #endif @@ -789,6 +785,10 @@ void dFlower_packet_c::draw() { GXColor sp28; + #if AVOID_UB + sp28 = {1, 1, 1, 1}; + #endif + //u8 sp26, sp25, sp24; GXColor sp24; sp24.r = -0.4f * temp_r29->AmbCol.r * var_f29; diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index ab2724060f..3d67a57007 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -20,8 +20,9 @@ const u16 l_M_kusa05_RGBATEX__height = 31; #if TARGET_PC #include "dusk/dvd_asset.hpp" -static u8* l_M_kusa05_RGBATEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x7680, 0x800), true); return buf; } -static u8* l_M_Hijiki00TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x7E80, 0x800), true); return buf; } +using GameVersion = dusk::version::GameVersion; +static u8* l_M_kusa05_RGBATEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x7680}, {GameVersion::GcnPal, 0x7680}}, 0x800), true); return buf; } +static u8* l_M_Hijiki00TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x7E80}, {GameVersion::GcnPal, 0x7E80}}, 0x800), true); return buf; } #define l_M_kusa05_RGBATEX (l_M_kusa05_RGBATEX_get()) #define l_M_Hijiki00TEX (l_M_Hijiki00TEX_get()) #else @@ -113,12 +114,14 @@ static u8 l_texCoord[160] = { }; #if TARGET_PC -static u8* l_M_Kusa_9qDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8B00, 0xCB), true); return buf; } -static u8* l_M_Kusa_9q_cDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8BE0, 0xCB), true); return buf; } -static u8* l_M_TenGusaDL_get() { static u8 buf[0xD4]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8CC0, 0xD4), true); return buf; } -static u8* l_Tengusa_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8DA0, 0xA8), true); return buf; } -static u8* l_kusa9q_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8E60, 0xA8), true); return buf; } -static u8* l_kusa9q_l4_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8F20, 0xA8), true); return buf; } +using GameVersion = dusk::version::GameVersion; + +static u8* l_M_Kusa_9qDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8B00}, {GameVersion::GcnPal, 0x8B00}}, 0xCB), true); return buf; } +static u8* l_M_Kusa_9q_cDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8BE0}, {GameVersion::GcnPal, 0x8BE0}}, 0xCB), true); return buf; } +static u8* l_M_TenGusaDL_get() { static u8 buf[0xD4]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8CC0}, {GameVersion::GcnPal, 0x8CC0}}, 0xD4), true); return buf; } +static u8* l_Tengusa_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8DA0}, {GameVersion::GcnPal, 0x8DA0}}, 0xA8), true); return buf; } +static u8* l_kusa9q_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8E60}, {GameVersion::GcnPal, 0x8E60}}, 0xA8), true); return buf; } +static u8* l_kusa9q_l4_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8F20}, {GameVersion::GcnPal, 0x8F20}}, 0xA8), true); return buf; } #define l_M_Kusa_9qDL (l_M_Kusa_9qDL_get()) #define l_M_Kusa_9q_cDL (l_M_Kusa_9q_cDL_get()) #define l_M_TenGusaDL (l_M_TenGusaDL_get()) @@ -491,11 +494,11 @@ dGrass_packet_c::dGrass_packet_c() { #if TARGET_PC GXInitTexObj(&mTexObj_l_M_kusa05_RGBATEX, l_M_kusa05_RGBATEX, - l_M_kusa05_RGBATEX__width, l_M_kusa05_RGBATEX__height, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE + l_M_kusa05_RGBATEX__width + 1, l_M_kusa05_RGBATEX__height + 1, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE ); GXInitTexObj(&mTexObj_l_M_Hijiki00TEX, l_M_Hijiki00TEX, - l_M_Hijiki00TEX__width, l_M_Hijiki00TEX__height, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE + l_M_Hijiki00TEX__width + 1, l_M_Hijiki00TEX__height + 1, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE ); #endif @@ -643,18 +646,14 @@ void dGrass_packet_c::draw() { } if (var_r29->field_0x05 <= 3 || var_r29->field_0x05 >= 10) { -#if TARGET_PC - GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0); -#endif + IF_DUSK(GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0)); if (sp48 <= 3) { GXCallDisplayList(mp_kusa9q_14_DL, m_kusa9q_DL_14_size); } else { GXCallDisplayList(mp_kusa9q_DL, m_kusa9q_DL_size); } } else { -#if TARGET_PC - GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0); -#endif + IF_DUSK(GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0)); GXCallDisplayList(l_Tengusa_matDL, 0xA0); } @@ -680,12 +679,14 @@ void dGrass_packet_c::draw() { while (var_r29 != NULL) { if (var_r29->field_0x05 <= 3 || var_r29->field_0x05 >= 10) { + IF_DUSK(GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0)); if (sp48 <= 2) { GXCallDisplayList(mp_kusa9q_14_DL, m_kusa9q_DL_14_size); } else { GXCallDisplayList(mp_kusa9q_DL, m_kusa9q_DL_size); } } else { + IF_DUSK(GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0)); GXCallDisplayList(l_Tengusa_matDL, 0xA0); } diff --git a/src/d/d_attention.cpp b/src/d/d_attention.cpp index efa3bfec26..1bb9e84f70 100644 --- a/src/d/d_attention.cpp +++ b/src/d/d_attention.cpp @@ -12,6 +12,10 @@ #include "SSystem/SComponent/c_counter.h" #include +#if TARGET_PC +#include "dusk/settings.h" +#endif + #define DRAW_TYPE_YELLOW 0 #define DRAW_TYPE_RED 1 @@ -1422,6 +1426,11 @@ int dAttention_c::Run() { } void dAttention_c::Draw() { +#if TARGET_PC +if (dusk::getSettings().game.recordingMode) { + return; +} +#endif if (mAttParam.CheckFlag(dAttParam_c::EFlag_ARROW_OFF)) { draw[0].field_0x173 = 3; draw[1].field_0x173 = 3; diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index c3fcc806d5..3ee98be5c7 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -9,6 +9,8 @@ #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" #include "d/d_msg_string.h" +#include "dusk/livesplit.h" +#include "dusk/imgui/ImGuiConsole.hpp" #include "m_Do/m_Do_controller_pad.h" dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) { @@ -138,6 +140,17 @@ void dBrightCheck_c::modeWait() {} void dBrightCheck_c::modeMove() { if (mDoCPd_c::getTrigA(PAD_1) || mDoCPd_c::getTrigStart(PAD_1)) { mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0); +#ifdef TARGET_PC + dusk::speedrun::start(); + + if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) { + // start a new run if a run isn't already in progress + if (!dusk::m_speedrunInfo.m_isRunStarted) { + dusk::ImGuiMenuGame::resetForSpeedrunMode(); + dusk::m_speedrunInfo.startRun(); + } + } +#endif mCompleteCheck = true; mMode = MODE_WAIT_e; } diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index f805a6a08b..0adf6d4bd6 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -31,6 +31,7 @@ #if TARGET_PC #include "dusk/frame_interpolation.h" #include "dusk/logging.h" +#include "imgui.h" #endif namespace { @@ -794,6 +795,9 @@ void dCamera_c::updatePad() { if (mTriggerLeftLast > mCamSetup.ManualEndVal()) { if (mLockLActive == 0) { + #if TARGET_PC + mCamParam.mManualMode = 0; + #endif mLockLJustActivated = 1; } else { mLockLJustActivated = 0; @@ -833,6 +837,12 @@ void dCamera_c::updatePad() { mHoldB = mDoCPd_c::getHoldB(mPadID) ? true : false; mTrigB = mDoCPd_c::getTrigB(mPadID) ? true : false; + #if TARGET_PC + if (mCamParam.mManualMode) { + return; + } + #endif + bool sp6B = true; bool sp6C = true; int temp1; @@ -1031,6 +1041,11 @@ void dCamera_c::debugDrawInit() { bool dCamera_c::Run() { #if TARGET_PC ResetView(); + if (executeDebugFlyCam()) { + mFrameCounter++; + mTicks++; + return true; + } #endif daAlink_c* link = daAlink_getAlinkActorClass(); @@ -1166,7 +1181,14 @@ bool dCamera_c::Run() { clrFlag(0x200000); } } else { + #if TARGET_PC + if (mCamParam.Algorythmn(mCamStyle) != 1) { + mCamParam.mManualMode = 0; + } + #endif + sp0F = (this->*engine_tbl[mCamParam.Algorythmn(mCamStyle)])(mCamStyle); + field_0x170++; field_0x160++; mCurCamStyleTimer++; @@ -1471,7 +1493,7 @@ void dCamera_c::CalcTrimSize() { mTrimHeight += -mTrimHeight * 0.25f; break; case 2: -#if WIDESCREEN_SUPPORT +#if !TARGET_PC && WIDESCREEN_SUPPORT if (mDoGph_gInf_c::isWide() && mDoGph_gInf_c::isWideZoom()) { mTrimHeight += (16.0f - mTrimHeight) * 0.25f; break; @@ -3078,6 +3100,7 @@ bool dCamera_c::bumpCheck(u32 i_flags) { } else { field_0x968 *= mMonitor.field_0xc / 5.0f; } + f32 tmp = field_0x96c * (mIsWolf == 1 ? 30.0f : 30.0f); center += vec3.norm() * (tmp * globe.V().Sin()); cSGlobe globe2(vec2 - center); @@ -4182,6 +4205,11 @@ bool dCamera_c::chaseCamera(s32 param_0) { chase->field_0x8 -= chase->field_0xc; chase->field_0x8c = 0; chase->field_0x90 = false; + + #if TARGET_PC + freeCamera(); + #endif + return true; } @@ -4604,6 +4632,7 @@ bool dCamera_c::chaseCamera(s32 param_0) { sp110 = mViewCache.mDirection.R(); mViewCache.mDirection.R(mViewCache.mDirection.R() + (fVar55 - mViewCache.mDirection.R()) * chase->field_0x74); + chase->field_0x64 = mViewCache.mCenter + mViewCache.mDirection.Xyz(); mViewCache.mEye = chase->field_0x64; @@ -4618,6 +4647,11 @@ bool dCamera_c::chaseCamera(s32 param_0) { if (chase->field_0x1c != 0) { chase->field_0x1c--; } + + #if TARGET_PC + freeCamera(); + #endif + return true; } @@ -7065,10 +7099,12 @@ bool dCamera_c::subjectCamera(s32 param_0) { cXyz sp1E0(val0, val2, val1); #if TARGET_PC - f32 aspect = mDoGph_gInf_c::getAspect(); - f32 baseAspect = FB_WIDTH / FB_HEIGHT; - if (aspect > baseAspect) { - sp1E0.z += (aspect - baseAspect) * 4; + if (sp13) { + f32 aspect = mDoGph_gInf_c::getAspect(); + f32 baseAspect = FB_WIDTH / FB_HEIGHT; + if (aspect > baseAspect) { + sp1E0.z += (aspect - baseAspect) * 4; + } } #endif @@ -7444,6 +7480,201 @@ bool dCamera_c::test2Camera(s32 param_0) { return false; } +static constexpr f32 FLYCAM_SPEED = 0.5f; +static constexpr f32 FLYCAM_FAST_SPEED = 4.0f; +static constexpr f32 FLYCAM_ROTATION_SPEED = 0.002f; +static constexpr f32 FLYCAM_TRIGGER_DEADZONE = 20.0f; +static constexpr s16 FLYCAM_ROLL_SPEED = 256; +static ImVec2 sFlyCamLastMousePos = {-1.f, -1.f}; + +#if TARGET_PC +bool dCamera_c::executeDebugFlyCam() { + if (!dusk::getSettings().game.debugFlyCam) { + if (mDebugFlyCam.initialized) { + deactivateDebugFlyCam(); + } + sFlyCamLastMousePos = {-1.f, -1.f}; + return false; + } + + dEvt_control_c* event = dComIfGp_getEvent(); + if (event == nullptr) { + return false; + } + + if (!mDebugFlyCam.initialized && (event->mEventStatus != 0 || dComIfGp_isPauseFlag())) { + dusk::getSettings().game.debugFlyCam.setValue(false); + return false; + } + + if (!mDebugFlyCam.initialized) { + mDebugFlyCam.savedCenter = mCenter; + mDebugFlyCam.savedEye = mEye; + mDebugFlyCam.savedFovy = mFovy; + mDebugFlyCam.savedBank = mBank; + + f32 dx = mCenter.x - mEye.x; + f32 dy = mCenter.y - mEye.y; + f32 dz = mCenter.z - mEye.z; + mDebugFlyCam.yaw = atan2f(dz, dx); + f32 horizontal = sqrtf(dx * dx + dz * dz); + mDebugFlyCam.pitch = atan2f(dy, horizontal); + + mDebugFlyCam.initialized = true; + } + + if (dusk::getSettings().game.debugFlyCamLockEvents) { + event->mEventStatus = 1; + dComIfGp_getEventManager().setCameraPlay(1); + } else { + if (event->mEventStatus != 0) { + event->mEventStatus = 0; + } + dComIfGp_getEventManager().setCameraPlay(0); + } + + f32 stickY = 0.f; + f32 stickX = 0.f; + f32 cStickY = 0.f; + f32 cStickX = 0.f; + f32 trigL = 0.f; + f32 trigR = 0.f; + f32 rollInput = 0.f; + bool fast = false; + + if (dusk::getSettings().game.debugFlyCamLockEvents) { + interface_of_controller_pad& pad = mDoCPd_c::getCpadInfo(0); + stickY = pad.mMainStickPosY * 72.0f; + stickX = pad.mMainStickPosX * 72.0f; + cStickY = pad.mCStickPosY * 59.0f; + cStickX = pad.mCStickPosX * 59.0f; + trigL = pad.mTriggerLeft * 150.0f; + trigR = pad.mTriggerRight * 150.0f; + fast = mDoCPd_c::getHoldZ(PAD_1); + if (mDoCPd_c::getHoldY(PAD_1)) rollInput -= 1.f; + if (mDoCPd_c::getHoldX(PAD_1)) rollInput += 1.f; + } + + { + ImGuiIO& io = ImGui::GetIO(); + if (!io.WantCaptureKeyboard) { + f32 kbX = 0.0f, kbY = 0.0f; + if (ImGui::IsKeyDown(ImGuiKey_W) || ImGui::IsKeyDown(ImGuiKey_UpArrow)) kbY += 1.f; + if (ImGui::IsKeyDown(ImGuiKey_S) || ImGui::IsKeyDown(ImGuiKey_DownArrow)) kbY -= 1.f; + if (ImGui::IsKeyDown(ImGuiKey_D) || ImGui::IsKeyDown(ImGuiKey_RightArrow)) kbX += 1.f; + if (ImGui::IsKeyDown(ImGuiKey_A) || ImGui::IsKeyDown(ImGuiKey_LeftArrow)) kbX -= 1.f; + f32 len = sqrtf(kbX * kbX + kbY * kbY); + if (len > 1.f) { kbX /= len; kbY /= len; } + stickX += kbX * 72.0f; + stickY += kbY * 72.0f; + if (ImGui::IsKeyDown(ImGuiKey_Space)) trigR += 150.0f; + if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) trigL += 150.0f; + if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) fast = true; + if (ImGui::IsKeyDown(ImGuiKey_Q)) rollInput -= 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_E)) rollInput += 1.0f; + } + bool mouseValid = !io.WantCaptureMouse && io.MousePos.x >= 0.0f && io.MousePos.y >= 0.0f; + if (mouseValid && sFlyCamLastMousePos.x >= 0.0f) { + cStickX -= (io.MousePos.x - sFlyCamLastMousePos.x) * 2.0f; + cStickY -= (io.MousePos.y - sFlyCamLastMousePos.y) * 2.0f; + } + sFlyCamLastMousePos = mouseValid ? io.MousePos : ImVec2{-1.0f, -1.0f}; + } + + f32 verticalDisp = 0.0f; + if (trigR >= FLYCAM_TRIGGER_DEADZONE) { + verticalDisp += trigR; + } + if (trigL >= FLYCAM_TRIGGER_DEADZONE) { + verticalDisp -= trigL; + } + + f32 moveDy = stickY * sinf(mDebugFlyCam.pitch) + verticalDisp; + f32 moveDx = stickY * cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) - stickX * sinf(mDebugFlyCam.yaw); + f32 moveDz = stickY * sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) + stickX * cosf(mDebugFlyCam.yaw); + + f32 speed = fast ? FLYCAM_FAST_SPEED : FLYCAM_SPEED; + + mEye.x += speed * moveDx; + mEye.y += speed * moveDy; + mEye.z += speed * moveDz; + + static constexpr f32 FLYCAM_TARGET_DIST = 100.0f; + mCenter.x = mEye.x + cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST; + mCenter.z = mEye.z + sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST; + mCenter.y = mEye.y + sinf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST; + + mBank = mBank + static_cast(rollInput * FLYCAM_ROLL_SPEED * (fast ? FLYCAM_FAST_SPEED / FLYCAM_SPEED : 1.f)); + Reset(mCenter, mEye); + + f32 yawInput = dusk::getSettings().game.invertCameraXAxis ? cStickX : -cStickX; + mDebugFlyCam.yaw += yawInput * FLYCAM_ROTATION_SPEED; + mDebugFlyCam.yaw = fmodf(mDebugFlyCam.yaw + 2.0f * (f32)M_PI, 2.0f * (f32)M_PI); + + f32 maxPitch = (f32)M_PI / 2.0f - 0.1f; + f32 minPitch = -(f32)M_PI / 2.0f + 0.1f; + mDebugFlyCam.pitch = std::clamp(mDebugFlyCam.pitch + cStickY * FLYCAM_ROTATION_SPEED, minPitch, maxPitch); + + return true; +} + +void dCamera_c::deactivateDebugFlyCam() { + Reset(mDebugFlyCam.savedCenter, mDebugFlyCam.savedEye, mDebugFlyCam.savedFovy, mDebugFlyCam.savedBank.Val()); + + dEvt_control_c* event = dComIfGp_getEvent(); + if (event != nullptr && event->mEventStatus != 0) { + event->mEventStatus = 0; + } + dComIfGp_getEventManager().setCameraPlay(0); + mDebugFlyCam.initialized = false; +} + +bool dCamera_c::freeCamera() { + if (dusk::getSettings().game.freeCamera && mGear == 1) { + mGear = 0; + } + + if (!dusk::getSettings().game.freeCamera || mCamStyle == 70) + { + mCamParam.mManualMode = 0; + return false; + } + + if (!mCamParam.mManualMode) { + mCamParam.freeXAngle = mViewCache.mDirection.mAzimuth.Degree(); + mCamParam.freeYAngle = mViewCache.mDirection.mInclination.Degree(); + } + + cXyz camMovement = {mPadInfo.mCStick.mLastPosX, mPadInfo.mCStick.mLastPosY, 0.0f}; + f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY); + + if (mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY != 0) { + mCamParam.mManualMode = 1; + camMovement = camMovement.normalize(); + camMovement.y *= dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f; + mCamParam.freeXAngle += camMovement.x * magnitude * dusk::getSettings().game.freeCameraSensitivity * 5.0f; + mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraSensitivity * 5.0f; + } + + fopAc_ac_c* player = dComIfGp_getPlayer(0); + if (!mCamParam.mManualMode || player == nullptr) { + return false; + } + + f32 minYAngle = -30.0f; + f32 maxAngle = 50.0f; + + mCamParam.freeYAngle = std::clamp(mCamParam.freeYAngle, minYAngle, maxAngle); + mViewCache.mDirection.mAzimuth = cSAngle(mCamParam.freeXAngle); + mViewCache.mDirection.mInclination = cSAngle(mCamParam.freeYAngle); + + cXyz finalEye = mViewCache.mCenter + mViewCache.mDirection.Xyz(); + mViewCache.mEye = finalEye; + + return true; +} +#endif + bool dCamera_c::towerCamera(s32 param_0) { cSAngle stack_444 = cSAngle(mCamSetup.ChargeLatitude()); f32 sp224 = mCamSetup.ChargeBRatio(); @@ -11031,6 +11262,26 @@ static int camera_execute(camera_process_class* i_this) { return 1; } +#ifdef TARGET_PC +void set_ar_corrected_trim(dDlst_window_c* window, float trim_height) { + const auto viewport = window->getViewPort(); + + if (mDoGph_gInf_c::isWideZoom()) { + const auto target_ar = FB_WIDTH / (FB_HEIGHT - trim_height * 2.0f); + const auto current_ar = mDoGph_gInf_c::m_safeWidthF / mDoGph_gInf_c::m_safeHeightF; + + if (current_ar < target_ar) { + trim_height = FB_HEIGHT / 2.0f * (1.0f - current_ar / target_ar); + } else { + trim_height = 0.0f; + } + } + + trim_height *= viewport->height / FB_HEIGHT; + window->setScissor(0.0f, trim_height, viewport->width, viewport->height - trim_height * 2.0f); +} +#endif + static int camera_draw(camera_process_class* i_this) { camera_class* a_this = (camera_class*)i_this; dCamera_c* body = &i_this->mCamera; @@ -11083,12 +11334,44 @@ static int camera_draw(camera_process_class* i_this) { } #endif +#if TARGET_PC + set_ar_corrected_trim(window, body->TrimHeight()); + + if (dusk::getSettings().game.enableFrameInterpolation) { + dusk::frame_interp::add_interpolation_callback([](bool _, void* pUserWork) { + const auto i_this = static_cast(pUserWork); + const auto camera = &i_this->mCamera; + + const auto trim_size = camera->mTrimSize; + + if (camera->mCurState != 2 && trim_size >= 0 && trim_size <= 3) { + // derive trim height at previous tick using current camera state + f32 target; + switch (trim_size) { + case 0: + target = 0.0f; + break; + case 1: + target = camera->mCamSetup.VistaTrimHeight(); + break; + case 2: + case 3: + target = camera->mCamSetup.CinemaScopeTrimHeight(); + break; + } + + const auto step = dusk::frame_interp::get_interpolation_step(); + const auto cur = camera->TrimHeight(); + const auto prev = (4.0f * cur - target) / 3.0f; + const auto trim_height = prev + (cur - prev) * step; + + set_ar_corrected_trim(get_window((camera_class*)i_this), trim_height); + } + }, i_this); + } +#else int trim_height = body->TrimHeight(); -#if TARGET_PC - trim_height *= viewport->height / FB_HEIGHT; - window->setScissor(0.0f, trim_height, viewport->width, viewport->height - trim_height * 2.0f); -#else window->setScissor(0.0f, trim_height, FB_WIDTH, FB_HEIGHT - trim_height * 2.0f); #endif diff --git a/src/d/d_cc_uty.cpp b/src/d/d_cc_uty.cpp index 7d71e490ac..ec9a177683 100644 --- a/src/d/d_cc_uty.cpp +++ b/src/d/d_cc_uty.cpp @@ -13,6 +13,9 @@ #include "d/d_s_play.h" #include "d/d_com_inf_game.h" #include "f_op/f_op_actor_mng.h" +#if TARGET_PC +#include "dusk/achievements.h" +#endif static int plCutLRC[58] = { 0, // @@ -434,6 +437,11 @@ fopAc_ac_c* cc_at_check(fopAc_ac_c* i_enemy, dCcU_AtInfo* i_AtInfo) { if (i_AtInfo->mAttackPower != 0 && i_enemy->health <= 0) { i_AtInfo->mHitStatus = 2; i_enemy->health = 0; +#if TARGET_PC + if (fopAcM_GetGroup(i_enemy) == fopAc_ENEMY_e) { + dusk::AchievementSystem::get().signal("enemy_killed"); + } +#endif } int uvar8; diff --git a/src/d/d_demo.cpp b/src/d/d_demo.cpp index d6425848d3..2f099f2c2a 100644 --- a/src/d/d_demo.cpp +++ b/src/d/d_demo.cpp @@ -11,6 +11,62 @@ #include "JSystem/JGadget/define.h" #include +#include "dusk/logging.h" + +#if TARGET_PC +#include "dusk/ui/ui.hpp" + +namespace { +static int sJaiSkip = -1; + +static JSUList* get_stream_list() { + return Z2GetSoundMgr()->getStreamMgr()->getStreamList(); +} + +static int get_stream_count(JSUList* list) { + int i = 0; + for (JSULink* l = list != nullptr ? list->getFirst() : nullptr; l != nullptr; + l = l->getNext()) { + i++; + } + return i; +} + +static void pause_stream(int skip_first, bool paused) { + int i = 0; + JSUList* list = get_stream_list(); + for (JSULink* l = list->getFirst(); l != nullptr; l = l->getNext(), ++i) { + if (i >= skip_first) { + l->getObject()->pause(paused); + } + } +} + +static void pause_streams(int skip_first) { + if (!dusk::ui::is_prelaunch_open()) { + return; + } + JSUList* list = get_stream_list(); + if (list == nullptr || get_stream_count(list) <= skip_first) { + return; + } + pause_stream(skip_first, true); + sJaiSkip = skip_first; +} + +static void unpause_streams(bool require_prelaunch_hidden) { + if (sJaiSkip < 0) { + return; + } + if (require_prelaunch_hidden && dusk::ui::is_prelaunch_open()) { + return; + } + pause_stream(sJaiSkip, false); + sJaiSkip = -1; +} +} // namespace +#endif + s16 dDemo_c::m_branchId = -1; namespace { @@ -1006,7 +1062,16 @@ int dDemo_c::start(u8 const* p_data, cXyz* p_translation, f32 rotationY) { m_control->setSuspend(0); } +#if TARGET_PC + const int existing_streams = get_stream_count(get_stream_list()); +#endif + m_control->forward(0); + +#if TARGET_PC + pause_streams(existing_streams); +#endif + m_translation = p_translation; if (m_translation != NULL) { @@ -1034,6 +1099,10 @@ static void dummyString2() { void dDemo_c::end() { JUT_ASSERT(1956, m_system != NULL); +#if TARGET_PC + unpause_streams(false); +#endif + m_control->destroyObject_all(); m_object->remove(); m_data = NULL; @@ -1054,6 +1123,10 @@ void dDemo_c::branch() { int dDemo_c::update() { JUT_ASSERT(2064, m_system != NULL); +#if TARGET_PC + unpause_streams(true); +#endif + if (m_data == NULL) { if (m_branchData == NULL) { return 0; diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp index 373791f994..1f16b2f655 100644 --- a/src/d/d_drawlist.cpp +++ b/src/d/d_drawlist.cpp @@ -22,6 +22,10 @@ #include "dusk/frame_interpolation.h" #include "dusk/gx_helper.h" #include "dusk/logging.h" + +static const void* getInterpKey(const void* base, int idx) { + return reinterpret_cast(reinterpret_cast(base) ^ idx); +} #endif class dDlst_2Dm_c { @@ -1062,7 +1066,15 @@ void dDlst_shadowReal_c::reset() { } void dDlst_shadowReal_c::imageDraw(Mtx param_0) { - GXSetProjection(mRenderProjMtx, GX_ORTHOGRAPHIC); +#ifdef TARGET_PC + Mtx render_proj_mtx; + if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 2), render_proj_mtx)) { + GXSetProjection(render_proj_mtx, GX_ORTHOGRAPHIC); + } else +#endif + { + GXSetProjection(mRenderProjMtx, GX_ORTHOGRAPHIC); + } JUT_ASSERT(1916, mModelNum); J3DModelData* model_data; J3DModel** models = mpModels; @@ -1075,7 +1087,15 @@ void dDlst_shadowReal_c::imageDraw(Mtx param_0) { for (u16 j = 0; j < model_data->getShapeNum(); j++) { if (!model_data->getShapeNodePointer(j)->checkFlag(1)) { shape_pkt = (*models)->getShapePacket(j); - shape_pkt->setBaseMtxPtr(&mViewMtx); +#ifdef TARGET_PC + Mtx view_mtx; + if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx)) { + shape_pkt->setBaseMtxPtr(&view_mtx); + } else +#endif + { + shape_pkt->setBaseMtxPtr(&mViewMtx); + } shape_pkt->drawFast(); shape_pkt->setBaseMtxPtr((Mtx*)param_0); } @@ -1096,7 +1116,18 @@ void dDlst_shadowReal_c::draw() { GXSetVtxDesc(GX_VA_POS, GX_DIRECT); GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); GXSetCurrentMtx(GX_PNMTX0); - GXLoadTexMtxImm(mReceiverProjMtx, GX_TEXMTX0, GX_MTX3x4); +#ifdef TARGET_PC + Mtx view_mtx, recv_proj_mtx; + const auto have_view_mtx = dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx); + const auto have_recv_proj_mtx = dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 3), recv_proj_mtx); + if (have_view_mtx && have_recv_proj_mtx) { + cMtx_concat(recv_proj_mtx, view_mtx, recv_proj_mtx); + GXLoadTexMtxImm(recv_proj_mtx, GX_TEXMTX0, GX_MTX3x4); + } else +#endif + { + GXLoadTexMtxImm(mReceiverProjMtx, GX_TEXMTX0, GX_MTX3x4); + } mShadowRealPoly.draw(); } @@ -1253,6 +1284,13 @@ u8 dDlst_shadowReal_c::setShadowRealMtx(cXyz* param_0, cXyz* param_1, f32 param_ cMtx_lookAt(mViewMtx, &local_64, param_1, 0); C_MTXOrtho(mRenderProjMtx, param_2, -param_2, -param_2, param_2, 1.0f, 10000.0f); C_MTXLightOrtho(mReceiverProjMtx, param_2, -param_2, -param_2, param_2, 0.5f, -0.5f, 0.5f, 0.5f); + +#ifdef TARGET_PC + const auto keybase = mpModels[0]; + dusk::frame_interp::record_final_mtx(mViewMtx, getInterpKey(keybase, 1)); + dusk::frame_interp::record_final_mtx(mRenderProjMtx, getInterpKey(keybase, 2)); + dusk::frame_interp::record_final_mtx(mReceiverProjMtx, getInterpKey(keybase, 3)); +#endif cMtx_concat(mReceiverProjMtx, mViewMtx, mReceiverProjMtx); return r29; } @@ -1277,6 +1315,10 @@ u32 dDlst_shadowReal_c::set(u32 i_key, J3DModel* i_model, cXyz* param_2, f32 par } } +#ifdef TARGET_PC + // provide a stable key for interpolation + mpModels[0] = i_model; +#endif field_0x1 = setShadowRealMtx(&sp60, param_2, param_3, param_4, param_7, param_5); if (!field_0x1) { @@ -1370,12 +1412,6 @@ void dDlst_shadowSimple_c::draw() { GXCallDisplayList(l_shadowVolumeDL, 0x40); } -#if TARGET_PC -static const void* getInterpKey(const void* base, int idx) { - return reinterpret_cast(reinterpret_cast(base) ^ idx); -} -#endif - void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* param_3, s16 param_4, f32 param_5, TGXTexObj* param_6) { if (param_5 < 0.0f) { diff --git a/src/d/d_file_sel_info.cpp b/src/d/d_file_sel_info.cpp index f305f59a9e..0c987e6d63 100644 --- a/src/d/d_file_sel_info.cpp +++ b/src/d/d_file_sel_info.cpp @@ -14,6 +14,8 @@ #include #include +#include "dusk/version.hpp" + dFile_info_c::dFile_info_c(JKRArchive* i_archive, u8 param_1) { mArchive = i_archive; field_0x22 = param_1; @@ -169,7 +171,18 @@ void dFile_info_c::setSaveDate(dSv_save_c* i_savedata) { OSCalendarTime time; OSTicksToCalendarTime(i_savedata->getPlayer().getPlayerStatusB().getDateIpl(), &time); - #if (VERSION == VERSION_GCN_JPN) || (VERSION == VERSION_WII_JPN) + #if TARGET_PC + if (dusk::version::isRegionJpn()) { + sprintf(mSaveDate, "%d.%02d.%02d %02d:%02d", time.year, time.mon + 1, time.mday, + time.hour, time.min); + } else if (dusk::version::isRegionPal() && dComIfGs_getPalLanguage() != dSv_player_config_c::LANGUAGE_ENGLISH) { + sprintf(mSaveDate, "%02d/%02d/%d %02d:%02d", time.mday, time.mon + 1, time.year, time.hour, + time.min); + } else { + sprintf(mSaveDate, "%02d/%02d/%d %02d:%02d", time.mon + 1, time.mday, time.year, time.hour, + time.min); + } + #elif (VERSION == VERSION_GCN_JPN) || (VERSION == VERSION_WII_JPN) sprintf(mSaveDate, "%d.%02d.%02d %02d:%02d", time.year, time.mon + 1, time.mday, time.hour, time.min); #elif VERSION == VERSION_GCN_PAL diff --git a/src/d/d_kankyo_rain.cpp b/src/d/d_kankyo_rain.cpp index 64cf2247e0..09e230c9fd 100644 --- a/src/d/d_kankyo_rain.cpp +++ b/src/d/d_kankyo_rain.cpp @@ -5962,6 +5962,8 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { fopAc_ac_c* player = dComIfGp_getPlayer(0); if (evil_packet != NULL) { + IF_DUSK(GXPushDebugGroup("dKyr_evil_draw2")); + j3dSys.reinitGX(); if (dComIfGd_getView() != NULL) { MTXInverse(dComIfGd_getView()->viewMtxNoTrans, camMtx); @@ -5986,7 +5988,12 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { TGXTexObj texobj; dKyr_set_btitex(&texobj, (ResTIMG*)tex[1]); - rot += 0.7f; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + rot += 0.7f; + } MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); @@ -6037,7 +6044,14 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { sp34.y = 80.0f; sp34.z = 80.0f; mDoLib_project(&sp7C, &proj); - if (!(proj.x > -sp34.x) || !(proj.x < (FB_WIDTH + sp34.x)) || +#if TARGET_PC + f32 cullMinX = mDoGph_gInf_c::getSafeMinXF() - sp34.x; + f32 cullMaxX = mDoGph_gInf_c::getSafeMinXF() + mDoGph_gInf_c::getSafeWidthF() + sp34.x; +#else + f32 cullMinX = -sp34.x; + f32 cullMaxX = FB_WIDTH + sp34.x; +#endif + if (!(proj.x > cullMinX) || !(proj.x < cullMaxX) || !(proj.y > -sp34.y) || !(proj.y < (458.0f + sp34.z))) { continue; @@ -6150,6 +6164,8 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { } } + IF_DUSK(GXPopDebugGroup()); + GXSetClipMode(GX_CLIP_ENABLE); J3DShape::resetVcdVatCache(); } @@ -6187,6 +6203,8 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { f32 sp60 = fabsf(cM_ssin(g_Counter.mCounter0 * 215)); if (evil_packet != NULL) { + IF_DUSK(GXPushDebugGroup("dKyr_evil_draw")); + j3dSys.reinitGX(); if (dComIfGd_getView() != NULL) { MTXInverse(dComIfGd_getView()->viewMtxNoTrans, camMtx); @@ -6208,14 +6226,19 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { TGXTexObj texobj; dKyr_set_btitex(&texobj, (ResTIMG*)tex[0]); - rot += 1.0f; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + rot += 1.0f; + } MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); GXLoadPosMtxImm(drawMtx, GX_PNMTX0); GXSetCurrentMtx(GX_PNMTX0); - GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGBA, GX_F32, 0); - GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_CLR_RGBA, GX_RGBA4, 8); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_RGBA4, 8); GXClearVtxDesc(); GXSetVtxDesc(GX_VA_POS, GX_DIRECT); GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); @@ -6238,6 +6261,19 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { GXSetClipMode(GX_CLIP_DISABLE); GXSetNumIndStages(0); +#if TARGET_PC + // move color_reg0 to vtx for perf + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT); + GXSetNumChans(1); + GXSetChanCtrl(GX_COLOR0A0, GX_FALSE, GX_SRC_REG, GX_SRC_VTX, 0, GX_DF_NONE, GX_AF_NONE); + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_C1, GX_CC_RASC, GX_CC_TEXC, GX_CC_ZERO); + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_TEXA, GX_CA_RASA, GX_CA_ZERO); + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); +#endif + dComIfG_Ccsp()->PrepareMass(); for (int i = 0; i < g_env_light.field_0x1054; i++) { @@ -6262,7 +6298,14 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { sp44.z = 80.0f; mDoLib_project(&spA4, &proj); - if (!(proj.x > -sp44.x) || !(proj.x < (FB_WIDTH + sp44.x)) || +#if TARGET_PC + f32 cullMinX = mDoGph_gInf_c::getSafeMinXF() - sp44.x; + f32 cullMaxX = mDoGph_gInf_c::getSafeMinXF() + mDoGph_gInf_c::getSafeWidthF() + sp44.x; +#else + f32 cullMinX = -sp44.x; + f32 cullMaxX = FB_WIDTH + sp44.x; +#endif + if (!(proj.x > cullMinX) || !(proj.x < cullMaxX) || !(proj.y > -sp44.y) || !(proj.y < (458.0f + sp44.z))) { continue; @@ -6349,7 +6392,7 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { color_reg0.b = (115.0f * sp28) + (15.0f * fabsf(sp2C - sp64)); } - GXSetTevColor(GX_TEVREG0, color_reg0); + IF_NOT_DUSK(GXSetTevColor(GX_TEVREG0, color_reg0)); GXSetTevColor(GX_TEVREG1, color_reg1); spC8 = spA4; @@ -6388,12 +6431,16 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { GXBegin(GX_QUADS, GX_VTXFMT0, 4); GXPosition3f32(pos[0].x, pos[0].y, pos[0].z); + IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a)); GXTexCoord2s16(0, 0); GXPosition3f32(pos[1].x, pos[1].y, pos[1].z); + IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a)); GXTexCoord2s16(0xFF, 0); GXPosition3f32(pos[2].x, pos[2].y, pos[2].z); + IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a)); GXTexCoord2s16(0xFF, 0xFF); GXPosition3f32(pos[3].x, pos[3].y, pos[3].z); + IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a)); GXTexCoord2s16(0, 0xFF); GXEnd(); } @@ -6401,6 +6448,8 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { } } + IF_DUSK(GXPopDebugGroup()); + J3DShape::resetVcdVatCache(); GXSetClipMode(GX_CLIP_ENABLE); diff --git a/src/d/d_map.cpp b/src/d/d_map.cpp index 7dc5187a88..25d792f309 100644 --- a/src/d/d_map.cpp +++ b/src/d/d_map.cpp @@ -13,6 +13,9 @@ #include "SSystem/SComponent/c_math.h" #include "d/actor/d_a_player.h" #include "d/d_com_inf_game.h" +#if TARGET_PC +#include +#endif #include #if DEBUG @@ -539,17 +542,14 @@ void renderingAmap_c::rendering(dDrawPath_c::poly_class const* i_poly) { } } -/* Enabling the following definition will modify the following function to - * make the map look worse for extra speed in the emulator, especially in large - * areas such as hyrule field. - */ -#define HYRULE_FIELD_SPEEDHACK bool renderingAmap_c::isDrawOutSideTrim() { bool rt = false; - #ifdef HYRULE_FIELD_SPEEDHACK - return 0; + #if TARGET_PC + if (!dusk::getSettings().game.enableMapBackground) { + return 0; + } #endif if (getDispType() == 0 || getDispType() == 4 || getDispType() == 3 || getDispType() == 2 || @@ -1218,6 +1218,9 @@ void dMap_c::changeTextureSize(int param_1, int param_2, int param_3) { void dMap_c::_remove() { if (mImage_p != NULL) { +#if TARGET_PC + GXDestroyCopyTex(mImage_p); +#endif JKR_DELETE_ARRAY(mImage_p); mImage_p = NULL; } @@ -1592,7 +1595,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) { calcMapCmPerTexel(field_0x80, &field_0x58); getPack(field_0x80, &mPackX, &mPackZ); - mCenterX += mPackX; + mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX; mCenterZ -= mPackZ; mCenterX += field_0x64; mCenterZ += mPackPlusZ; @@ -1654,7 +1657,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) { calcMapCmPerTexel(field_0x80, &field_0x58); getPack(field_0x80, &mPackX, &mPackZ); - mCenterX += mPackX; + mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX; mCenterZ -= mPackZ; } break; @@ -1734,7 +1737,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) { calcMapCmPerTexel(field_0x80, &field_0x58); getPack(field_0x80, &mPackX, &mPackZ); - mCenterX += mPackX; + mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX; mCenterZ -= mPackZ; field_0x8f = 4; #if DEBUG @@ -1826,7 +1829,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) { sp14 += temp_f31_2 * (spC - sp14); sp10 += temp_f31_2 * (sp8 - sp10); - mCenterX += sp14; + mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -sp14 :) sp14; mCenterZ -= sp10; break; } diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index 8288a2ef9f..f5e447a2aa 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -15,12 +15,47 @@ #include #ifdef TARGET_PC +#include +#include +#include + constexpr u16 kMapResolutionMultiplier = 4; +constexpr u16 kMapImageSide = 16 * kMapResolutionMultiplier; +constexpr u32 kMapImageTotalPixels = kMapImageSide * kMapImageSide; + +typedef std::function PaintI8Fn; + +void paint_i8(std::span dst, size_t width, PaintI8Fn paint) { + const auto blocksAcross = width >> 3; + + for (size_t i = 0; i < dst.size(); i++) { + // 8x4 block swizzling for I8 + const auto blockIdx = i >> 5; + const auto localIdx = i & 31; + + const auto blockY = blockIdx / blocksAcross; + const auto blockX = blockIdx % blocksAcross; + + const auto localY = localIdx >> 3; + const auto localX = localIdx & 7; + + const auto x = (blockX << 3) + localX; + const auto y = (blockY << 2) + localY; + + dst[i] = paint(x, y); + } +} #endif void dMpath_n::dTexObjAggregate_c::create() { static int const data[7] = { - 79, 80, 77, 78, 76, 81, 82, + 79, // 0: im_map_icon_square_4i.bti + 80, // 1: im_map_icon_tresurebox_4i.bti + 77, // 2: im_map_icon_enter_4i.bti + 78, // 3: im_map_icon_nijumaru_4i.bti + 76, // 4: im_map_icon_circle_4i.bti + 81, // 5: im_map_icon_try_force_4i.bti + 82, // 6: map_icon_circle16x16_4i.bti }; for (int lp1 = 0; lp1 < 7; lp1++) { @@ -32,6 +67,104 @@ void dMpath_n::dTexObjAggregate_c::create() { JUT_ASSERT(74, image->magFilter == GX_NEAR); mDoLib_setResTimgObj(image, mp_texObj[lp1], 0, NULL); } + +#if TARGET_PC + static bool hqTexsDrawn = false; + + static u8 hqCircleData[kMapImageTotalPixels]; + static u8 hqCircleAltData[kMapImageTotalPixels]; + static u8 hqNijumaruData[kMapImageTotalPixels]; + static u8 hqEnterData[kMapImageTotalPixels]; + static u8 hqTryForceData[kMapImageTotalPixels]; + + if (!hqTexsDrawn) { + constexpr auto center = kMapImageSide / 2.0f; + constexpr auto radiusSq = center * center; + + // 6: map_icon_circle16x16_4i.bti - simple circle + paint_i8(std::span{hqCircleData}, kMapImageSide, [=](auto x, auto y) { + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + return (dx * dx + dy * dy < radiusSq) ? 0x11 : 0; + }); + + // 4: im_map_icon_circle_4i.bti - outlined circle + paint_i8(std::span{hqCircleAltData}, kMapImageSide, [=](auto x, auto y) { + constexpr auto innerRadius = kMapImageSide * 3.0f / 8.0f; + constexpr auto innerRadiusSq = innerRadius * innerRadius; + + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + const auto dSq = dx * dx + dy * dy; + + return dSq < radiusSq ? (dSq < innerRadiusSq ? 0x22 : 0x11) : 0; + }); + + // 3: im_map_icon_nijumaru_4i.bti - concentric rings + paint_i8(std::span{hqNijumaruData}, kMapImageSide, [=](auto x, auto y) { + constexpr u8 nijumaruRings[] = {0x11, 0x22, 0x11, 0x11, 0x22, 0x22}; + + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + const auto dSq = dx * dx + dy * dy; + + if (dSq < radiusSq) { + const auto ringIndex = + static_cast(std::trunc(std::sqrt(dSq) / kMapImageSide * 12)); + return nijumaruRings[ringIndex]; + } + return u8{0}; + }); + + // 2: im_map_icon_enter_4i.bti - outlined octagram + paint_i8(std::span{hqEnterData}, kMapImageSide, [=](auto x, auto y) { + constexpr auto outlineWidth = kMapImageSide / 6.0f; + + const auto adx = std::abs((x + 0.5f) - center); + const auto ady = std::abs((y + 0.5f) - center); + const auto dist = + std::min(adx + ady, std::max(adx, ady) * std::numbers::sqrt2_v) - + kMapImageSide / 2.0f; + + return dist > 0.0f ? 0 : (dist > -outlineWidth ? 0x22 : 0x33); + }); + + // 5: im_map_icon_try_force_4i.bti - outlined circle with triangle + paint_i8(std::span{hqTryForceData}, kMapImageSide, [=](auto x, auto y) { + constexpr auto innerRadiusNorm = 5.0f / 12.0f; + constexpr auto innerRadius = kMapImageSide * innerRadiusNorm; + constexpr auto innerRadiusSq = innerRadius * innerRadius; + constexpr auto triRadius = kMapImageSide * innerRadiusNorm / 2.0f; + + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + const auto dSq = dx * dx + dy * dy; + const auto triSideDist = (std::numbers::sqrt3_v * std::abs(dx) - dy) * 0.5f; + const auto insideTri = std::max(dy, triSideDist) < triRadius; + + return insideTri ? 0x22 : (dSq < radiusSq ? (dSq < innerRadiusSq ? 0x33 : 0x22) : 0); + }); + + hqTexsDrawn = true; + } + + constexpr auto replacements = std::to_array >({ + {2, hqEnterData}, + {3, hqNijumaruData}, + {4, hqCircleAltData}, + {5, hqTryForceData}, + {6, hqCircleData}, + }); + + for (const auto& [idx, data] : replacements) { + JKR_DELETE(mp_texObj[idx]); + const auto texobj = JKR_NEW TGXTexObj(); + GXInitTexObj( + texobj, data, kMapImageSide, kMapImageSide, GX_TF_I8, GX_CLAMP, GX_CLAMP, GX_FALSE); + GXInitTexObjLOD(texobj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + mp_texObj[idx] = texobj; + } +#endif } void dMpath_n::dTexObjAggregate_c::remove() { @@ -497,12 +630,6 @@ void dRenderingFDAmap_c::postRenderingMap() { dMpath_n::dTexObjAggregate_c dMpath_n::m_texObjAgg; -/* Enabling the following definition will modify the following function to - * make the map look worse for extra speed in the emulator, especially in large - * areas such as hyrule field. - */ -#define HYRULE_FIELD_SPEEDHACK - void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_line) { s32 width = getDecorationLineWidth(p_line->field_0x1); if (width <= 0) { @@ -527,8 +654,32 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li lineColor.r = lineColor.r - 4; GXSetTevColor(GX_TEVREG1, lineColor); +#if TARGET_PC + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0); + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_KONST); + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXBegin(GX_LINESTRIP, GX_VTXFMT0, 2 * (data_num - 1)); + for (int i = 0; i < data_num - 1; i++) { + GXPosition1x16(data_p[i]); + GXTexCoord2f32(0, 0); + GXPosition1x16(data_p[i + 1]); + GXTexCoord2f32(0, 0); + } + GXEnd(); + + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_KONST, GX_CC_TEXC, GX_CC_C1); + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA); + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXBegin(GX_POINTS, GX_VTXFMT0, data_num); + for (int i = 0; i < data_num; i++) { + GXPosition1x16(data_p[i]); + GXTexCoord2f32(0, 0); + } + GXEnd(); +#else for (int i = 0; i < data_num; i++) { -#ifndef HYRULE_FIELD_SPEEDHACK if (i < data_num - 1) { GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0); GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, @@ -547,7 +698,6 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA); GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); -#endif GXBegin(GX_POINTS, GX_VTXFMT0, 1); GXPosition1x16(data_p[0]); @@ -555,6 +705,7 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li GXEnd(); data_p++; } +#endif setTevSettingNonTextureDirectColor(); GXClearVtxDesc(); diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index bb557efdac..b39c9bb287 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -856,7 +856,46 @@ void dMenu_DmapBg_c::decGoldFrameAlphaRate() { setGoldFrameAlphaRate(rate); } +void dMenu_DmapBg_c::dMapBgWide() { + // Scale Base HUD + mBaseScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); + mBaseScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); + + // Boss Key, Compass & Map icons + mBaseScreen->search(MULTI_CHAR('key_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + mBaseScreen->search(MULTI_CHAR('con_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + mBaseScreen->search(MULTI_CHAR('map_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + + // Text Header + mBaseScreen->search(MULTI_CHAR('t_t00'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + mBaseScreen->search(MULTI_CHAR('f_t_00'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + + // C Button + mBaseScreen->search(MULTI_CHAR('c_btn2'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + + // Scale Buttons HUD + mButtonScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); + mButtonScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); + + // Buttons + mButtonScreen->search(MULTI_CHAR('cont_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + + // C Button + mButtonScreen->search(MULTI_CHAR('c_btn'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + mButtonScreen->search(MULTI_CHAR('c_text_s'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + mButtonScreen->search(MULTI_CHAR('c_text'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + mButtonScreen->search(MULTI_CHAR('f_text_s'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + mButtonScreen->search(MULTI_CHAR('f_text'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + + // Decorations + mButtonScreen->search(MULTI_CHAR('kazari_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); +} + void dMenu_DmapBg_c::draw() { + #if TARGET_PC + dMapBgWide(); + #endif + u32 scissor_left; u32 scissor_top; u32 scissor_width; @@ -903,7 +942,10 @@ void dMenu_DmapBg_c::draw() { f32 local_28c = mpBackTexture->getBounds().i.x; mpBackTexture->setBlackWhite(color_black, color_white); mpBackTexture->draw(local_28c, field_0xd94 + mpBackTexture->getBounds().i.y, mpBackTexture->getWidth(), - mpBackTexture->getHeight(), false, false, false); + mpBackTexture->getHeight(), + IF_DUSK(dusk::getSettings().game.enableMirrorMode ? true :) false, + false, + false); grafContext->scissor(field_0xd94 + mDoGph_gInf_c::getMinXF(), scissor_top, mDoGph_gInf_c::getWidthF(), diff --git a/src/d/d_menu_dmap_map.cpp b/src/d/d_menu_dmap_map.cpp index 8971b2630d..0bb9717952 100644 --- a/src/d/d_menu_dmap_map.cpp +++ b/src/d/d_menu_dmap_map.cpp @@ -11,6 +11,9 @@ #include "d/d_menu_dmap_map.h" #include "f_op/f_op_msg_mng.h" #include "m_Do/m_Do_graphic.h" +#if TARGET_PC +#include +#endif struct dMdm_HIO_prm_res_dst_s { static void* m_res; @@ -291,6 +294,9 @@ void dMenu_DmapMap_c::_create(u16 param_0, u16 param_1, u16 param_2, u16 param_3 void dMenu_DmapMap_c::_delete() { for (int i = 0; i < 2; i++) { if (mMapImage_p[i] != NULL) { +#if TARGET_PC + GXDestroyCopyTex(mMapImage_p[i]); +#endif JKR_DELETE_ARRAY(mMapImage_p[i]); } @@ -362,7 +368,7 @@ void dMenu_StageMapCtrl_c::initGetTreasureList(u8 param_0, s8 param_1) { } inline static s16 rightModeCnvRot(s16 param_0) { - return param_0; + return IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -param_0 :) param_0; } bool dMenu_StageMapCtrl_c::getTreasureList(f32* o_posX, f32* o_posY, s8* param_2, u8* o_swbit, @@ -399,7 +405,7 @@ bool dMenu_StageMapCtrl_c::getTreasureList(f32* o_posX, f32* o_posY, s8* param_2 } inline static f32 rightModeCnvPos(f32 param_0) { - return param_0; + return IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -param_0 :) param_0; } void dMenu_StageMapCtrl_c::cnvPosTo2Dpos(f32 param_0, f32 param_1, f32* param_2, diff --git a/src/d/d_menu_fishing.cpp b/src/d/d_menu_fishing.cpp index 615323767b..b76002d540 100644 --- a/src/d/d_menu_fishing.cpp +++ b/src/d/d_menu_fishing.cpp @@ -16,6 +16,8 @@ #include "m_Do/m_Do_graphic.h" #include +#include "dusk/version.hpp" + typedef void (dMenu_Fishing_c::*initFunc)(); initFunc map_init_process[] = { &dMenu_Fishing_c::wait_init, @@ -135,9 +137,9 @@ bool dMenu_Fishing_c::isSync() { } void dMenu_Fishing_c::init() { - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL BOOL isEnglish = FALSE; - if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { + if (dusk::version::isRegionUsa() || (dusk::version::isRegionPal() && dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH)) { isEnglish = TRUE; } #endif @@ -145,7 +147,7 @@ void dMenu_Fishing_c::init() { for (int i = 0; i < MAX_FINDABLE_FISHES; i++) { if (dComIfGs_getFishNum(i) != 0) { // Fish has been caught once, display it along with it's params - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (isEnglish) { setFishParam(i, dComIfGs_getFishNum(i), dComIfGs_getFishSize(i) / 2.54f); } else { diff --git a/src/d/d_menu_fmap.cpp b/src/d/d_menu_fmap.cpp index 149e03349d..d2b06e962c 100644 --- a/src/d/d_menu_fmap.cpp +++ b/src/d/d_menu_fmap.cpp @@ -919,9 +919,20 @@ void dMenu_Fmap_c::region_map_proc() { } mpDraw2DBack->regionMapMove(mpStick); int stage_no, room_no; + +#if TARGET_PC + f32 arrow_pos_x = mpDraw2DBack->getArrowPos2DX(); + if (dusk::getSettings().game.enableMirrorMode) { + arrow_pos_x = mpDraw2DBack->getMirrorPosX(arrow_pos_x, 0.0f); + } + + f32 pos_x = arrow_pos_x - mDoGph_gInf_c::getMinXF() - mDoGph_gInf_c::getWidthF() * 0.5f; +#else f32 pos_x = mpDraw2DBack->getArrowPos2DX() - mDoGph_gInf_c::getMinXF() - mDoGph_gInf_c::getWidthF() * 0.5f; +#endif f32 pos_y = mpDraw2DBack->getArrowPos2DY() - mDoGph_gInf_c::getHeightF() * 0.5f; + mpMenuFmapMap->getPointStagePathInnerNo(getNowFmapRegionData(), pos_x, pos_y, mStayStageNo, &stage_no, &room_no); if (mStageCursor != stage_no || mRoomCursor != room_no || mResetAreaName) { @@ -2464,6 +2475,13 @@ void dMenu_Fmap_c::portalWarpMapMove(STControl* i_stick) { f32 arrow_y = mpDraw2DBack->getArrowPos2DY(); u8 uVar6 = 0xff; +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + arrow_x = mpDraw2DBack->getMirrorPosX(arrow_x, 0.0f); + } +#endif + + for (int i = 0; i < portal_dat->mCount; i++) { if (portals[i].mRegionNo == mpDraw2DBack->getRegionCursor() + 1 && checkDrawPortalIcon(portals[i].mStageNo, portals[i].mSwitchNo)) diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 8692156daf..d62f7d1037 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -20,6 +20,15 @@ #include "dusk/frame_interpolation.h" #include +#if TARGET_PC +void dMenu_Fmap2DBack_c::fMapBackWide() { + mpBaseScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); + mpBaseScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); + mpBackScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); + mpBackScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f); +} +#endif + dMenu_Fmap2DBack_c::dMenu_Fmap2DBack_c() { dMeter2Info_setMapDrugFlag(0); @@ -267,6 +276,10 @@ dMenu_Fmap2DBack_c::~dMenu_Fmap2DBack_c() { } void dMenu_Fmap2DBack_c::draw() { + #if TARGET_PC + fMapBackWide(); + #endif + calcBlink(); J2DGrafContext* grafPort = dComIfGp_getCurrentGrafPort(); @@ -1030,6 +1043,12 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) { calcAllMapPos2D((mArrowPos3DX + control_xpos) - mStageTransX, (mArrowPos3DZ + control_ypos) - mStageTransZ, &sp14, &sp10); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + sp14 = getMirrorPosX(sp14, 0.0f); + } +#endif + mSelectRegion = 0xff; for (int i = 7; i >= 0; i--) { int val = field_0x1230[i]; @@ -1199,7 +1218,7 @@ f32 dMenu_Fmap2DBack_c::getMapScissorAreaSizeX() { } f32 dMenu_Fmap2DBack_c::getMapScissorAreaSizeRealX() { -#if PLATFORM_GCN && !TARGET_PC +#if PLATFORM_GCN return getMapScissorAreaSizeX(); #else return getMapScissorAreaSizeX() * mDoGph_gInf_c::getScale(); @@ -1384,6 +1403,15 @@ void dMenu_Fmap2DBack_c::regionTextureDraw() { if (uVar10 != uVar9) { bool b = 0; f32 v = mTransX + (dVar14 + (mRegionMinMapX[uVar10] + field_0xf0c[uVar10])); + + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + b = true; + v = getMirrorPosX(mTransX + (dVar14 + (mRegionMinMapX[uVar10] + field_0xf0c[uVar10])), + mRegionMapSizeX[uVar10] * mZoom * 0.5f); + } + #endif + mpAreaTex[uVar10]->draw( v, mTransZ + (dVar13 + (mRegionMinMapY[uVar10] + field_0xf2c[uVar10])), mRegionMapSizeX[uVar10] * mZoom, mRegionMapSizeY[uVar10] * mZoom, b, false, @@ -1391,6 +1419,15 @@ void dMenu_Fmap2DBack_c::regionTextureDraw() { } else { bool b = 0; f32 v = mTransX + (dVar14 + (mRegionMinMapX[uVar9] + field_0xf0c[uVar9])); + + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + b = true; + v = getMirrorPosX(mTransX + (dVar14 + (mRegionMinMapX[uVar9] + field_0xf0c[uVar9])), + mRegionMapSizeX[uVar9] * mZoom * 0.5f); + } + #endif + mpAreaTex[uVar9]->draw( v, mTransZ + (dVar13 + (mRegionMinMapY[uVar9] + field_0xf2c[uVar9])), mRegionMapSizeX[uVar9] * mZoom, mRegionMapSizeY[uVar9] * mZoom, b, false, @@ -1407,6 +1444,11 @@ void dMenu_Fmap2DBack_c::stageTextureDraw() { mpSpotTexture->setAlpha(mAlphaRate * 255.0f * field_0xfa8 * mSpotTextureFadeAlpha); } +#if TARGET_PC + JUTPalette* pPalette = mpSpotTexture->getTexture(0)->getPalette(); + pPalette->dataUploaded(); +#endif + mpSpotTexture->draw(mTransX + getMapScissorAreaLX(), mTransZ + getMapScissorAreaLY(), getMapScissorAreaSizeRealX(), getMapScissorAreaSizeRealY(), false, false, false); @@ -2179,6 +2221,17 @@ void dMenu_Fmap2DBack_c::setArrowPosAxis(f32 i_posX, f32 i_posZ) { control_ypos = 0.0f; } +#if TARGET_PC +void dMenu_Fmap2DTop_c::fMapTopWide() { + mpTitleScreen->search(MULTI_CHAR('spot0_n'))->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); + mpTitleScreen->search(MULTI_CHAR('spot2_n'))->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f); + mpTitleScreen->search(MULTI_CHAR('name_n'))->translate(mDoGph_gInf_c::ScaleHUDXLeft(-243.0f), -169.0f); + mpTitleScreen->search(MULTI_CHAR('sub_n_n'))->translate(mDoGph_gInf_c::ScaleHUDXLeft(-80.0f), -154.0f); + mpTitleScreen->search(MULTI_CHAR('btn_i_n'))->translate(mDoGph_gInf_c::ScaleHUDXLeft(-241.0f), 177.0f); + mpTitleScreen->search(MULTI_CHAR('cont_n'))->translate(mDoGph_gInf_c::ScaleHUDXRight(515.0f), 83.0f); +} +#endif + dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { mpHeap = i_heap; mTransX = 0.0f; @@ -2572,6 +2625,10 @@ void dMenu_Fmap2DTop_c::setAllAlphaRate(f32 i_rate, bool i_init) { } void dMenu_Fmap2DTop_c::draw() { + #if TARGET_PC + fMapTopWide(); + #endif + u32 scissor_left, scissor_top, scissor_width, scissor_height; J2DOrthoGraph* ctx = static_cast(dComIfGp_getCurrentGrafPort()); ctx->setup2D(); diff --git a/src/d/d_menu_fmap_map.cpp b/src/d/d_menu_fmap_map.cpp index 3d1a9e2523..c500aff0be 100644 --- a/src/d/d_menu_fmap_map.cpp +++ b/src/d/d_menu_fmap_map.cpp @@ -8,6 +8,9 @@ #include "d/d_debug_viewer.h" #include "d/d_menu_fmap_map.h" #include "m_Do/m_Do_graphic.h" +#if TARGET_PC +#include +#endif #include static u8 twoValueLineInterpolation(u8 i_value1, u8 i_value2, f32 i_param) { @@ -494,6 +497,9 @@ void dMenu_FmapMap_c::_delete() { mResTIMG = NULL; } if (mMapImage_p != NULL) { +#if TARGET_PC + GXDestroyCopyTex(mMapImage_p); +#endif JKR_DELETE_ARRAY(mMapImage_p); mMapImage_p = NULL; } diff --git a/src/d/d_menu_letter.cpp b/src/d/d_menu_letter.cpp index a0155ef6b4..4bfda4b9cd 100644 --- a/src/d/d_menu_letter.cpp +++ b/src/d/d_menu_letter.cpp @@ -17,6 +17,10 @@ #include "d/d_msg_scrn_arrow.h" #include "d/d_lib.h" +#ifdef TARGET_PC +#include "dusk/achievements.h" +#endif + #if VERSION == VERSION_GCN_JPN #define D_MENU_LETTER_LINE_MAX 9 #else @@ -514,6 +518,10 @@ void dMenu_Letter_c::read_open_init() { setAButtonString(0); setBButtonString(0); mpBlackTex->setAlpha(0); + + #ifdef TARGET_PC + dusk::AchievementSystem::get().signal("open_letter"); + #endif } void dMenu_Letter_c::read_open_move() { @@ -957,7 +965,8 @@ void dMenu_Letter_c::screenSetBase() { } if (field_0x374 > 1) { J2DPane* pJVar6 = mpBaseScreen->search('pi_n'); - f32 dVar18 = field_0x1f0[1]->getBounds().i.x - field_0x1f0[0]->getBounds().i.x; + f32 x1 = field_0x1f0[1]->getBounds().i.x; + f32 dVar18 = x1 - field_0x1f0[0]->getBounds().i.x; f32 dVar17 = dVar18 * (field_0x374 - 1); f32 dVar16 = (pJVar6->getWidth() / 2) - (dVar17 / 2); for (int i = 0; i < 9; i++) { diff --git a/src/d/d_menu_map_common.cpp b/src/d/d_menu_map_common.cpp index e7d0fef7ad..0017975c4e 100644 --- a/src/d/d_menu_map_common.cpp +++ b/src/d/d_menu_map_common.cpp @@ -343,6 +343,11 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4 } f32 pos_x = icon_pos_x + i_posX; + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + pos_x = getMirrorCenterPosX(pos_x, 0.0f); + } + #endif mpDrawCursor->setPos(pos_x, icon_pos_y + i_posY); mpDrawCursor->setScale(mIconInfo[info_idx].scale * g_fmapHIO.mMapIconHIO.mPortalCursorScale); mpDrawCursor->draw(); @@ -364,6 +369,12 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4 } f32 pos_x = (icon_pos_x + i_posX); + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + pos_x = getMirrorCenterPosX(pos_x, 0.0f); + } + #endif + mpPortalIcon->setPos(pos_x, icon_pos_y + i_posY); mpPortalIcon->setScale(mIconInfo[info_idx].scale * g_fmapHIO.mMapIconHIO.mPortalIconScale); mpPortalIcon->draw(); @@ -399,6 +410,12 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4 } f32 pos_x = i_posX + (icon_pos_x - (icon_size_x / 2)); + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + pos_x = getMirrorCenterPosX(i_posX + (icon_pos_x - (icon_size_x / 2)), icon_size_x / 2); + } + #endif + mPictures[mIconInfo[info_idx].icon_no]->draw(pos_x, (i_posY + (icon_pos_y - icon_size_y / 2)), icon_size_x, icon_size_y, false, false, false); diff --git a/src/d/d_meter2.cpp b/src/d/d_meter2.cpp index 5bd321e7a8..097cde7c37 100644 --- a/src/d/d_meter2.cpp +++ b/src/d/d_meter2.cpp @@ -24,9 +24,10 @@ #include "d/actor/d_a_horse.h" #include +#if TARGET_PC #include "dusk/memory.h" - -#include "dusk/memory.h" +#include "dusk/settings.h" +#endif int dMeter2_c::_create() { stage_stag_info_class* stag_info = dComIfGp_getStageStagInfo(); @@ -316,6 +317,14 @@ int dMeter2_c::_execute() { } int dMeter2_c::_draw() { + #if TARGET_PC + if (dusk::getSettings().game.recordingMode || dusk::getSettings().game.minimalHUD || + dusk::getSettings().game.debugFlyCam) + { + return 1; + } + #endif + if (mpMap != NULL) { mpMap->_draw(); } @@ -424,12 +433,6 @@ void dMeter2_c::setLifeZero() { void dMeter2_c::checkStatus() { mStatus = 0; - #if TARGET_PC - if (dusk::getSettings().game.disableMainHUD) { - mStatus |= 0xF0000000; - } - #endif - field_0x12c = field_0x128; field_0x128 = daPy_py_c::checkNowWolf(); diff --git a/src/d/d_meter_HIO.cpp b/src/d/d_meter_HIO.cpp index 2df28b2655..584a2852f6 100644 --- a/src/d/d_meter_HIO.cpp +++ b/src/d/d_meter_HIO.cpp @@ -2306,6 +2306,10 @@ void dMeter_drawHIO_c::updateOnWide() { // River Canoe Minigame g_drawHIO.mMiniGame.mCounterPosX[1] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mCounterPosX[1]); g_drawHIO.mMiniGame.mIconPosX[1] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mIconPosX[1]); + + // Bulblin Count in Hidden Village + g_drawHIO.mMiniGame.mCounterPosX[2] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mCounterPosX[2]); + g_drawHIO.mMiniGame.mIconPosX[2] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mIconPosX[2]); #endif } diff --git a/src/d/d_meter_button.cpp b/src/d/d_meter_button.cpp index d36a124d17..e55b42bc1a 100644 --- a/src/d/d_meter_button.cpp +++ b/src/d/d_meter_button.cpp @@ -368,8 +368,22 @@ void dMeterButton_c::draw() { } if (var_r3) { +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) { + mWasListen[i] = var_r22; + mWasRepeat[i] = var_r23; + } else { + var_r22 = mWasListen[i]; + var_r23 = mWasRepeat[i]; + } +#endif if (var_r22) { - if (field_0x2e8[i] == 18.0f) { +#ifdef TARGET_PC + if (field_0x2e8[i] == 18.0f && dusk::frame_interp::get_ui_tick_pending()) +#else + if (field_0x2e8[i] == 18.0f) +#endif + { mDoAud_seStart(Z2SE_SY_HINT_BUTTON_BLINK, NULL, 0, 0); } diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index 7af18f0732..f67d6054e6 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -307,7 +307,7 @@ static u8 getOutFontNumberType(int param_0) { } } -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC || VERSION == VERSION_GCN_PAL static void setPlayerName(char* i_player_name, u8 param_2) { if (param_2 != 0) { strcpy(i_player_name, dComIfGs_getPlayerName()); @@ -1485,7 +1485,7 @@ bool jmessage_tMeasureProcessor::do_tag(u32 i_tag, void const* i_data, u32 i_siz char buffer[40]; switch (i_tag & 0xFF00FFFF) { case MSGTAG_PLAYER_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setPlayerName(buffer, 1); } else { @@ -1495,7 +1495,7 @@ bool jmessage_tMeasureProcessor::do_tag(u32 i_tag, void const* i_data, u32 i_siz push_word(buffer); return true; case MSGTAG_HORSE_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setHorseName(buffer, 1); } else { @@ -2066,7 +2066,7 @@ bool jmessage_tSequenceProcessor::do_isReady() { case 0: case 5: case 6: - if (mDoCPd_c::getTrigA(PAD_1) || field_0xb2 != 0) { + if (mDoCPd_c::getTrigA(PAD_1) || field_0xb2 != 0 IF_DUSK(|| (dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0)))) { field_0xa4 = 0; pReference->onBatchFlag(); pReference->setCharCnt(D_MSG_CLASS_CHAR_CNT_MAX); @@ -4270,7 +4270,7 @@ bool jmessage_string_tMeasureProcessor::do_tag(u32 i_tag, void const* i_data, u3 char buffer[40]; switch (i_tag & 0xFF00FFFF) { case MSGTAG_PLAYER_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setPlayerName(buffer, 1); } else { @@ -4281,7 +4281,7 @@ bool jmessage_string_tMeasureProcessor::do_tag(u32 i_tag, void const* i_data, u3 stack_pushCurrent(buffer); break; case MSGTAG_HORSE_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setHorseName(buffer, 1); } else { @@ -4878,7 +4878,7 @@ bool jmessage_string_tRenderingProcessor::do_tag(u32 i_tag, void const* i_data, char buffer[40]; switch (i_tag & 0xFF00FFFF) { case MSGTAG_PLAYER_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setPlayerName(buffer, 1); } else { @@ -4889,7 +4889,7 @@ bool jmessage_string_tRenderingProcessor::do_tag(u32 i_tag, void const* i_data, push_word(buffer); break; case MSGTAG_HORSE_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setHorseName(buffer, 1); } else { diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index 9b7b64ebda..44b5c66571 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -24,12 +24,17 @@ #include "f_op/f_op_msg_mng.h" #include #include + +#include "JSystem/JKernel/JKRExpHeap.h" +#include "dusk/version.hpp" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_lib.h" -#include "JSystem/JKernel/JKRExpHeap.h" #if TARGET_PC #include "dusk/settings.h" +#include +#include +#include #endif static void dMsgObject_addFundRaising(s16 param_0); @@ -422,6 +427,17 @@ static void dummyStrings() { dMsgObject_HIO_c g_MsgObject_HIO_c; int dMsgObject_c::_execute() { +// TODO: enabling wii message overrides fixes direction text, but gives wrong item control text +/*#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + // enable wii message index override + g_MsgObject_HIO_c.mMessageDisplay = 1; + } else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) { + g_MsgObject_HIO_c.mMessageDisplay = 0; + } +#endif*/ + + field_0x4c7 = 0; if (mpTalkHeap != NULL) { field_0x148 = mDoExt_setCurrentHeap(mpTalkHeap); @@ -1592,7 +1608,7 @@ u8 dMsgObject_c::isSend() { return 2; } } else { - if (IF_DUSK((dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0)) ||) + if (IF_DUSK((dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0) && !isShopItemMessage()) ||) mDoCPd_c::getTrigA(0) != 0 || mDoCPd_c::getTrigB(0) != 0) { return 2; } @@ -1643,7 +1659,30 @@ void dMsgObject_c::readMessageGroupLocal(mDoDvdThd_mountXArchive_c** p_arcMount) #else #if TARGET_PC // Original game UB - snprintf(arcName, sizeof(arcName), "/res/Msgus/bmgres%d.arc", msgGroup); + + if (dusk::version::isRegionPal()) { + switch (dComIfGs_getPalLanguage()) { + case dSv_player_config_c::LANGUAGE_GERMAN: + snprintf(arcName, sizeof(arcName), "/res/Msgde/bmgres%d.arc", msgGroup); + break; + case dSv_player_config_c::LANGUAGE_FRENCH: + snprintf(arcName, sizeof(arcName), "/res/Msgfr/bmgres%d.arc", msgGroup); + break; + case dSv_player_config_c::LANGUAGE_SPANISH: + snprintf(arcName, sizeof(arcName), "/res/Msgsp/bmgres%d.arc", msgGroup); + break; + case dSv_player_config_c::LANGUAGE_ITALIAN: + snprintf(arcName, sizeof(arcName), "/res/Msgit/bmgres%d.arc", msgGroup); + break; + default: + snprintf(arcName, sizeof(arcName), "/res/Msguk/bmgres%d.arc", msgGroup); + } + } else if (dusk::version::isRegionJpn()) { + snprintf(arcName, sizeof(arcName), "/res/Msgjp/bmgres%d.arc", msgGroup); + } else { + snprintf(arcName, sizeof(arcName), "/res/Msgus/bmgres%d.arc", msgGroup); + } + #else sprintf(arcName, "/res/Msgus/bmgres%d.arc", msgGroup); #endif @@ -1841,6 +1880,39 @@ bool dMsgObject_c::isTalkMessage() { return true; } +#if TARGET_PC +bool dMsgObject_c::isShopItemMessage() { + + // Probably a better way to do this than just listing every message id, but this works for now + // Note: Keep contents sorted so we can use binary search + const auto shopMsgIds = std::to_array>({ + {}, + // zel_01.bmg - Seras Shop + {7001, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7013, 7014, 7022, 7023, 7028, 7029, + 7044, 7045, 7053}, + // zel_02.bmg - Kakariko Shops + {5181, 5182, 5251, 5253, 5254, 5256, 5258, 5259, 5653, 5654, 5656, 5660, 5661, 5664, 5665, + 5697, 5698, 5699, 5803, 5804, 5806, 5810, 5811, 5812, 5814, 5821, 5823, 5824, 5987, 5988, + 5989, 5990, 5991, 5992, 5993, 5994, 5995, 5996, 5997, 5998, 5999}, + // zel_03.bmg - Death Mountain Shop + {5303, 5304, 5306, 5310, 5311, 5314, 5315, 5322, 5323, 5324, 5496, 5497, 5498, 5499}, + // zel_04.bmg - Castle Town Shops + {5407, 5408, 5409, 5410, 5411, 5412, 5413, 5414, 5415, 5416, 5417, 5418, 5419, 5420, 5431, + 5432, 5434, 5435, 5436, 5437, 5438, 5439, 5440, 5441, 5444, 5449, 5450, 5451, 5452, 5462}, + // zel_05.bmg - Oocca Shop + {9428, 9429, 9430, 9431, 9432, 9437, 9443, 9448, 9449, 9451, 9459} + }); + + u16 id = mMessageID; + s16 group = dMsgObject_getGroupID(); + if (group < shopMsgIds.size()) { + return std::ranges::binary_search(shopMsgIds[group], id); + } + return false; + +} +#endif + const char* dMsgObject_c::getSmellName() { JMSMesgInfo_c* info_header_p = (JMSMesgInfo_c*)((char*)mpMsgRes + 0x20); char* data_ptr = (char*)info_header_p + info_header_p->header.size; diff --git a/src/d/d_msg_scrn_arrow.cpp b/src/d/d_msg_scrn_arrow.cpp index 7ee3b0503c..cea0dae145 100644 --- a/src/d/d_msg_scrn_arrow.cpp +++ b/src/d/d_msg_scrn_arrow.cpp @@ -6,6 +6,10 @@ #include "d/d_com_inf_game.h" #include "d/d_pane_class.h" +#if TARGET_PC +#include "dusk/settings.h" +#endif + dMsgScrnArrow_c::dMsgScrnArrow_c() { mpScreen = JKR_NEW J2DScreen(); JUT_ASSERT(0, mpScreen != NULL); @@ -65,6 +69,11 @@ dMsgScrnArrow_c::~dMsgScrnArrow_c() { } void dMsgScrnArrow_c::draw() { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort(); mpScreen->draw(0.0f, 0.0f, graf_ctx); } diff --git a/src/d/d_msg_scrn_base.cpp b/src/d/d_msg_scrn_base.cpp index 84899c2824..fdd9e57f9c 100644 --- a/src/d/d_msg_scrn_base.cpp +++ b/src/d/d_msg_scrn_base.cpp @@ -8,6 +8,10 @@ #include "d/d_pane_class.h" #include +#if TARGET_PC +#include "dusk/settings.h" +#endif + dMsgScrnBase_c::dMsgScrnBase_c() { init(); } @@ -57,12 +61,22 @@ void dMsgScrnBase_c::init() { } void dMsgScrnBase_c::multiDraw() { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif if (field_0x48 != NULL) { dComIfGd_set2DOpa(field_0x48); } } void dMsgScrnBase_c::draw() { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort(); ctx->setup2D(); @@ -72,10 +86,20 @@ void dMsgScrnBase_c::draw() { } void dMsgScrnBase_c::drawSelf() { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif drawOutFont(0.0f, 0.0f, 1.0f); } void dMsgScrnBase_c::drawOutFont(f32 param_0, f32 param_1, f32 param_2) { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif mpOutFont->draw(NULL, param_0, param_1, param_2); } diff --git a/src/d/d_msg_scrn_boss.cpp b/src/d/d_msg_scrn_boss.cpp index 64141770b3..54938ceb72 100644 --- a/src/d/d_msg_scrn_boss.cpp +++ b/src/d/d_msg_scrn_boss.cpp @@ -6,6 +6,10 @@ #include "d/d_msg_object.h" #include "d/d_pane_class.h" +#if TARGET_PC +#include "dusk/settings.h" +#endif + dMsgScrnBoss_c::dMsgScrnBoss_c() { static u64 t_tag[7] = { MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'), @@ -91,6 +95,11 @@ void dMsgScrnBoss_c::exec() { } void dMsgScrnBoss_c::drawSelf() { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort(); ctx->setup2D(); drawOutFont(0.0f, 0.0f, 1.0f); diff --git a/src/d/d_msg_scrn_howl.cpp b/src/d/d_msg_scrn_howl.cpp index 1d39cba1a1..c701ea0b2b 100644 --- a/src/d/d_msg_scrn_howl.cpp +++ b/src/d/d_msg_scrn_howl.cpp @@ -20,6 +20,7 @@ #include "Z2AudioLib/Z2WolfHowlMgr.h" #if TARGET_PC +#include "dusk/frame_interpolation.h" #include "dusk/settings.h" #endif @@ -580,9 +581,14 @@ void dMsgScrnHowl_c::drawWave() { f17 = local_60; f18 = local_64; } else { - field_0x2134++; - if (field_0x2134 > 30) { - field_0x2134 = 0; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x2134++; + if (field_0x2134 > 30) { + field_0x2134 = 0; + } } if (field_0x2134 < 15) { local_dc = field_0x2134 / 15.0f; diff --git a/src/d/d_msg_scrn_kanban.cpp b/src/d/d_msg_scrn_kanban.cpp index a9edbef7df..d6e91b12f6 100644 --- a/src/d/d_msg_scrn_kanban.cpp +++ b/src/d/d_msg_scrn_kanban.cpp @@ -13,6 +13,10 @@ #include "d/d_msg_out_font.h" #include "d/d_pane_class.h" +#if TARGET_PC +#include "dusk/settings.h" +#endif + dMsgScrnKanban_c::dMsgScrnKanban_c(JKRExpHeap* param_0) { if (param_0 != NULL) { field_0xd4 = param_0; @@ -176,6 +180,11 @@ void dMsgScrnKanban_c::exec() { } void dMsgScrnKanban_c::draw() { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort(); grafContext->setup2D(); mpScreen->draw(0.0f, 0.0f, grafContext); diff --git a/src/d/d_msg_scrn_place.cpp b/src/d/d_msg_scrn_place.cpp index b2c28efc95..93c2ec7c20 100644 --- a/src/d/d_msg_scrn_place.cpp +++ b/src/d/d_msg_scrn_place.cpp @@ -12,6 +12,10 @@ #include "d/d_msg_object.h" #include "d/d_pane_class.h" +#if TARGET_PC +#include "dusk/settings.h" +#endif + dMsgScrnPlace_c::dMsgScrnPlace_c() { static u64 t_tag[7] = { MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'), @@ -127,6 +131,11 @@ void dMsgScrnPlace_c::exec() { } void dMsgScrnPlace_c::drawSelf() { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort(); grafContext->setup2D(); drawOutFont(0.0f, 0.0f, 1.0f); diff --git a/src/d/d_msg_scrn_talk.cpp b/src/d/d_msg_scrn_talk.cpp index c9f925e784..ace5abd3bb 100644 --- a/src/d/d_msg_scrn_talk.cpp +++ b/src/d/d_msg_scrn_talk.cpp @@ -20,6 +20,10 @@ #include "JSystem/J2DGraph/J2DScreen.h" #include +#if TARGET_PC +#include "dusk/settings.h" +#endif + dMsgScrnTalk_c::dMsgScrnTalk_c(u8 param_1, u8 param_2, JKRExpHeap* param_3) { if (param_3 != NULL) { field_0xe4 = param_3; @@ -303,6 +307,11 @@ void dMsgScrnTalk_c::exec() { } void dMsgScrnTalk_c::drawSelf() { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif J2DGrafContext* grafContext[1]; grafContext[0] = dComIfGp_getCurrentGrafPort(); grafContext[0]->setup2D(); diff --git a/src/d/d_msg_scrn_tree.cpp b/src/d/d_msg_scrn_tree.cpp index 0dee41ef5a..d17ced9bd8 100644 --- a/src/d/d_msg_scrn_tree.cpp +++ b/src/d/d_msg_scrn_tree.cpp @@ -9,6 +9,10 @@ #include "d/d_msg_out_font.h" #include "d/d_pane_class.h" +#if TARGET_PC +#include "dusk/settings.h" +#endif + dMsgScrnTree_c::dMsgScrnTree_c(JUTFont* param_0, JKRExpHeap* param_1) { if (param_1 != NULL) { field_0xd8 = param_1; @@ -187,6 +191,11 @@ void dMsgScrnTree_c::exec() { } void dMsgScrnTree_c::draw() { +#if TARGET_PC + if (dusk::getSettings().game.recordingMode) { + return; + } +#endif J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort(); grafContext->setup2D(); mpScreen->draw(0.0f, 0.0f, grafContext); diff --git a/src/d/d_msg_unit.cpp b/src/d/d_msg_unit.cpp index f773abd373..7bbe8b3d70 100644 --- a/src/d/d_msg_unit.cpp +++ b/src/d/d_msg_unit.cpp @@ -334,11 +334,11 @@ void dMsgUnit_c::setTag(int i_type, int i_value, char* o_buffer, bool param_4) { vals[1] = ((dMsgUnit_inf1_section_t*)pInfoBlock)->entries[i_type].endFrame; #endif -#if REGION_PAL +#if TARGET_PC || REGION_PAL if (i_value == 1 || (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_FRENCH && i_value == 0)) { -#elif !REGION_USA +#elif !REGION_USA // Dusk TODO: What? This checks for Spanish when *not* PAL or USA? if (i_value == 1 || (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_SPANISH && i_value == 0)) { diff --git a/src/d/d_name.cpp b/src/d/d_name.cpp index e4232eff34..1f3ccbb1d7 100644 --- a/src/d/d_name.cpp +++ b/src/d/d_name.cpp @@ -8,9 +8,15 @@ #include "m_Do/m_Do_controller_pad.h" #include #include + #include "JSystem/J2DGraph/J2DAnmLoader.h" +#include "dusk/version.hpp" #include "f_op/f_op_msg_mng.h" +static bool isPalOrJpn() { + return dusk::version::isRegionPal() || dusk::version::isRegionJpn(); +} + static const char* l_mojiHira[65] = { "あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し", "す", "せ", "そ", "た", "ち", "つ", "て", "と", "な", "に", "ぬ", "ね", "の", "は", @@ -66,15 +72,31 @@ static const char* l_mojiEisu[65] = { "X", "k", "x", ",", "L", "Y", "l", "y", ".", "M", "Z", "m", "z", " ", }; -#if REGION_PAL -static char* l_mojiEisuPal_1[65] = { +#if TARGET_PC +// The game normally mutates this string list to fill in the real character codes. +// That can't work on a modern platform, so instead I've filled them out ahead of time. +static const char* l_mojiEisuPal_1[65] = { + "A", "N", "\xC0", "\xCF", "1", "B", "O", "\xC1", "\xD0", "2", "C", "P", "\xC2", "\xD1", "3", "D", "Q", + "\xC3", "\xD2", "4", "E", "R", "\xC4", "\xD3", "5", "F", "S", "\xC5", "\xD4", "6", "G", "T", "\xC6", "\xD5", + "7", "H", "U", "\xC7", "\xD6", "8", "I", "V", "\xC8", "\xD7", "9", "J", "W", "\xC9", "\xD8", "0", "K", + "X", "\xCA", "\xD9", ",", "L", "Y", "\xCB", "\xDA", ".", "M", "Z", "\xCC", "\xDB", " ", +}; + +static const char* l_mojiEisuPal_2[65] = { + "a", "n", "\xE0", "\xEF", "1", "b", "o", "\xE1", "\xF0", "2", "c", "p", "\xE2", "\xF1", "3", "d", "q", + "\xE3", "\xF2", "4", "e", "r", "\xE4", "\xF3", "5", "f", "s", "\xE5", "\xF4", "6", "g", "t", "\xE6", + "\xF5", "7", "h", "u", "\xE7", "\xF6", "8", "i", "v", "\xE8", "\xF7", "9", "j", "w", "\xE9", "\xF8", "0", + "k", "x", "\xEA", "\xF9", ",", "l", "y", "\xEB", "\xFA", ".", "m", "z", "\xEC", "\xFB", " ", +}; +#elif REGION_PAL +static const char* l_mojiEisuPal_1[65] = { "A", "N", "AA", "BB", "1", "B", "O", "CC", "DD", "2", "C", "P", "EE", "FF", "3", "D", "Q", "GG", "HH", "4", "E", "R", "II", "JJ", "5", "F", "S", "KK", "LL", "6", "G", "T", "MM", "NN", "7", "H", "U", "OO", "PP", "8", "I", "V", "QQ", "RR", "9", "J", "W", "SS", "TT", "0", "K", "X", "UU", "VV", ",", "L", "Y", "WW", "XX", ".", "M", "Z", "YY", "ZZ", " ", }; -static char* l_mojiEisuPal_2[65] = { +static const char* l_mojiEisuPal_2[65] = { "a", "n", "aa", "bb", "1", "b", "o", "cc", "dd", "2", "c", "p", "ee", "ff", "3", "d", "q", "gg", "hh", "4", "e", "r", "ii", "jj", "5", "f", "s", "kk", "ll", "6", "g", "t", "mm", "nn", "7", "h", "u", "oo", "pp", "8", "i", "v", "qq", "rr", "9", "j", "w", "ss", "tt", "0", @@ -82,6 +104,16 @@ static char* l_mojiEisuPal_2[65] = { }; #endif +#if TARGET_PC +// ' ' (full-width space) +#define SPACE_MAYBE_FULL (dusk::version::isRegionJpn() ? '\x81\x40' : ' ') +#elif REGION_JPN +// ' ' (full-width space) +#define SPACE_MAYBE_FULL '\x81\x40' +#else +#define SPACE_MAYBE_FULL ' ' +#endif + static dNm_HIO_c g_nmHIO; typedef void (dName_c::*selProcFunc)(void); @@ -163,13 +195,18 @@ void dName_c::init() { field_0x2ac = mSelProc; field_0x2ad = mSelProc; field_0x2ae = field_0x2ac; - #if REGION_PAL || REGION_JPN + #if TARGET_PC + mMojiSet = isPalOrJpn() ? MOJI_HIRA : MOJI_EIGO; + #elif REGION_PAL || REGION_JPN mMojiSet = MOJI_HIRA; #else mMojiSet = MOJI_EIGO; #endif mPrevMojiSet = 255; - #if REGION_PAL || REGION_JPN + #if TARGET_PC + mSelMenu = isPalOrJpn() ? MENU_HIRA : MENU_END; + mPrevSelMenu = isPalOrJpn() ? MENU_HIRA : MENU_END; + #elif REGION_PAL || REGION_JPN mSelMenu = MENU_HIRA; mPrevSelMenu = MENU_HIRA; #else @@ -187,7 +224,8 @@ void dName_c::initial() { mNextNameStr[0] = 0; } - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN + IF_DUSK_BLOCK(isPalOrJpn()) if (mSelProc == PROC_MOJI_SELECT) { mMenuIcon[mMojiSet]->scale(g_nmHIO.mMenuScale, g_nmHIO.mMenuScale); mMenuText[mMojiSet]->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); @@ -196,6 +234,7 @@ void dName_c::initial() { mMenuText[mPrevMojiSet]->setWhite(JUtility::TColor(0x96, 0x96, 0x96, 0xFF)); } } + IF_DUSK_BLOCK_END #endif } @@ -230,8 +269,8 @@ void dName_c::_move() { stick->checkTrigger(); (this->*SelProc[mSelProc])(); - #if REGION_PAL || REGION_JPN - if (mDoCPd_c::getTrigY(PAD_1)) { + #if TARGET_PC || REGION_PAL || REGION_JPN + if (IF_DUSK(isPalOrJpn() &&) mDoCPd_c::getTrigY(PAD_1)) { mDoAud_seStart(Z2SE_SY_DUMMY, 0, 0, 0); mPrevMojiSet = mMojiSet; mMojiSet++; @@ -245,8 +284,8 @@ void dName_c::_move() { mojiListChange(); } else { #endif - #if REGION_JPN - if (mDoCPd_c::getTrigX(PAD_1)) { + #if TARGET_PC || REGION_JPN + if (IF_DUSK(dusk::version::isRegionJpn() &&) mDoCPd_c::getTrigX(PAD_1)) { if (mCurPos != 0) { if (mojiChange(mCurPos - 1) == 1) { mDoAud_seStart(Z2SE_SY_DUMMY, 0, 0, 0); @@ -283,12 +322,14 @@ void dName_c::_move() { backSpace(); } } else if (mDoCPd_c::getTrigStart(PAD_1)) { +#define EIGO_OR_END DUSK_IF_ELSE((dusk::version::isRegionPal() ? MENU_EIGO : MENU_END), MENU_END) + #if REGION_PAL if ((mSelProc != PROC_MENU_SELECT || mSelMenu != MENU_EIGO) && (mSelProc == PROC_MENU_SELECT || mSelProc == PROC_MOJI_SELECT)) { #else - if ((mSelProc != PROC_MENU_SELECT || mSelMenu != MENU_END) && + if ((mSelProc != PROC_MENU_SELECT || mSelMenu != EIGO_OR_END) && (mSelProc == PROC_MENU_SELECT || mSelProc == PROC_MOJI_SELECT)) { #endif @@ -297,7 +338,7 @@ void dName_c::_move() { #if REGION_PAL mSelMenu = MENU_EIGO; #else - mSelMenu = MENU_END; + mSelMenu = EIGO_OR_END; #endif switch (mSelProc) { @@ -314,10 +355,10 @@ void dName_c::_move() { } } } - #if REGION_JPN + #if TARGET_PC || REGION_JPN } #endif - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN } #endif @@ -327,10 +368,9 @@ void dName_c::_move() { int dName_c::nameCheck() { for (int i = 8, len = 7; i > 0; i--) { #if REGION_JPN - // ' ' (full-width space) if (mChrInfo[len].mCharacter != ' ' && mChrInfo[len].mCharacter != '\x81\x40') { #else - if (mChrInfo[len].mCharacter != ' ') { + if (mChrInfo[len].mCharacter != ' ' IF_DUSK(&& (!dusk::version::isRegionJpn() || mChrInfo[len].mCharacter != '\x81\x40'))) { #endif return len + 1; } @@ -344,8 +384,8 @@ void dName_c::playNameSet(int nameLength) { char* str = mInputStr; for (int i = 0; i < nameLength; i++) { - #if REGION_JPN - if (mChrInfo[i].mMojiSet == 2) { + #if TARGET_PC || REGION_JPN + if (!dusk::version::isRegionJpn() || mChrInfo[i].mMojiSet == 2) { *str = mChrInfo[i].mCharacter; str += 1; } else { @@ -727,7 +767,34 @@ int dName_c::getMoji() { int result = -1; const char* moji; - #if REGION_PAL + #if TARGET_PC + if (dusk::version::isRegionPal()) { + switch (mMojiSet) { + case MOJI_HIRA: + moji = l_mojiEisuPal_1[mCharRow + mCharColumn * 5]; + break; + case MOJI_KATA: + moji = l_mojiEisuPal_2[mCharRow + mCharColumn * 5]; + break; + default: + abort(); + } + } else { + switch (mMojiSet) { + case MOJI_HIRA: + moji = l_mojiHira[mCharRow + mCharColumn * 5]; + break; + case MOJI_KATA: + moji = l_mojikata[mCharRow + mCharColumn * 5]; + break; + case MOJI_EIGO: + moji = l_mojiEisu[mCharRow + mCharColumn * 5]; + break; + default: + abort(); + } + } + #elif REGION_PAL switch (mMojiSet) { case MOJI_HIRA: moji = l_mojiEisuPal_1[mCharRow + mCharColumn * 5]; @@ -750,7 +817,17 @@ int dName_c::getMoji() { } #endif - #if REGION_JPN + #if TARGET_PC + if (dusk::version::isRegionJpn()) { + if (*(u8*)moji >> 4 == 0x8 || *(u8*)moji >> 4 == 0x9) { + result = *(u16*)moji; + } else { + result = *moji; + } + } else { + result = *moji; + } + #elif REGION_JPN if (*(u8*)moji >> 4 == 0x8 || *(u8*)moji >> 4 == 0x9) { result = *(u16*)moji; } else { @@ -763,6 +840,14 @@ int dName_c::getMoji() { return result; } +#if TARGET_PC +#define CHAR_TRUNC(val) (dusk::version::isRegionPal() ? val & 0xFF : val) +#elif REGION_PAL +#define CHAR_TRUNC(val) (val & 0xFF) +#else +#define CHAR_TRUNC(val) val +#endif + void dName_c::setMoji(int moji) { if (mCurPos == 8 || nameCheck() == 8) { mDoAud_seStart(Z2SE_SYS_ERROR, NULL, 0, 0); @@ -771,24 +856,14 @@ void dName_c::setMoji(int moji) { s32 notEmpty = false; for (int i = mCurPos; i < 8; i++) { - #if REGION_JPN - // ' ' (full-width space) - if (mChrInfo[i].mCharacter != '\x81\x40') { - #else - if (mChrInfo[i].mCharacter != ' ') { - #endif + if (mChrInfo[i].mCharacter != SPACE_MAYBE_FULL) { notEmpty = true; break; } } if (notEmpty) { - #if REGION_JPN - // ' ' (full-width space) - if (mChrInfo[7].mCharacter == '\x81\x40') { - #else - if (mChrInfo[7].mCharacter == ' ') { - #endif + if (mChrInfo[7].mCharacter == SPACE_MAYBE_FULL) { for (int i = 6; i >= mCurPos; i--) { mChrInfo[i + 1] = mChrInfo[i]; } @@ -797,11 +872,7 @@ void dName_c::setMoji(int moji) { mChrInfo[mCurPos].mRow = mCharRow; mChrInfo[mCurPos].mMojiSet = mMojiSet; mChrInfo[mCurPos].field_0x3 = 1; - #if REGION_PAL - mChrInfo[mCurPos].mCharacter = moji & 0xFF; - #else - mChrInfo[mCurPos].mCharacter = moji; - #endif + mChrInfo[mCurPos].mCharacter = CHAR_TRUNC(moji); if (mCurPos != 8) { mLastCurPos = mCurPos; @@ -814,11 +885,7 @@ void dName_c::setMoji(int moji) { mChrInfo[mCurPos].mRow = mCharRow; mChrInfo[mCurPos].mMojiSet = mMojiSet; mChrInfo[mCurPos].field_0x3 = 1; - #if REGION_PAL - mChrInfo[mCurPos].mCharacter = moji & 0xFF; - #else - mChrInfo[mCurPos].mCharacter = moji; - #endif + mChrInfo[mCurPos].mCharacter = CHAR_TRUNC(moji); if (mCurPos != 8) { mLastCurPos = mCurPos; @@ -844,13 +911,8 @@ void dName_c::setNameText() { "CR\x1b" "CC[000000]\x1bGM[0]%c\x1bHM\x1b" "CC[ffffff]\x1bGM[0]%c", - #if REGION_PAL - (u8)mChrInfo[i].mCharacter & 0xFF, - (u8)mChrInfo[i].mCharacter & 0xFF - #else - (u8)mChrInfo[i].mCharacter, - (u8)mChrInfo[i].mCharacter - #endif + CHAR_TRUNC((u8)mChrInfo[i].mCharacter), + CHAR_TRUNC((u8)mChrInfo[i].mCharacter) ); #if REGION_JPN } else { @@ -889,7 +951,29 @@ void dName_c::nameCursorMove() { void dName_c::selectCursorMove() { int idx; - #if REGION_PAL + #if TARGET_PC + if (dusk::version::isRegionPal()) { + if (mCharColumn < 3) { + idx = 0; + } else if (mCharColumn < 6) { + idx = 1; + } else if (mCharColumn >= 6) { + idx = 2; + } + } else if (dusk::version::isRegionJpn()) { + if (mCharColumn < 3) { + idx = 0; + } else if (mCharColumn < 6) { + idx = 1; + } else if (mCharColumn < 8) { + idx = 2; + } else if (mCharColumn >= 8) { + idx = 3; + } + } else { + idx = 3; + } + #elif REGION_PAL if (mCharColumn < 3) { idx = 0; } else if (mCharColumn < 6) { @@ -930,7 +1014,36 @@ void dName_c::selectCursorMove() { void dName_c::menuCursorPosSet() { mPrevSelMenu = mSelMenu; - #if REGION_PAL + #if TARGET_PC + if (dusk::version::isRegionPal()) { + if (mCharColumn < 3) { + mSelMenu = MENU_HIRA; + } else if (mCharColumn < 6) { + mSelMenu = MENU_KATA; + } else if (mCharColumn >= 6) { + mSelMenu = MENU_EIGO; + } + } else if (dusk::version::isRegionJpn()) { + if (mCharColumn < 3) { + mSelMenu = MENU_HIRA; + return; + } + if (mCharColumn < 6) { + mSelMenu = MENU_KATA; + return; + } + if (mCharColumn < 8) { + mSelMenu = MENU_EIGO; + return; + } + if (mCharColumn >= 8) { + mSelMenu = MENU_END; + return; + } + } else { + mSelMenu = MENU_END; + } + #elif REGION_PAL if (mCharColumn < 3) { mSelMenu = MENU_HIRA; } else if (mCharColumn < 6) { @@ -961,28 +1074,28 @@ void dName_c::menuCursorPosSet() { } void dName_c::MenuSelect() { - #if REGION_PAL || REGION_JPN - if (stick->checkRightTrigger()) { + #if TARGET_PC || REGION_PAL || REGION_JPN + if (isPalOrJpn() && stick->checkRightTrigger()) { mDoAud_seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0); mPrevSelMenu = mSelMenu; mSelMenu++; #if REGION_PAL if (mSelMenu > MENU_EIGO) { #else - if (mSelMenu > MENU_END) { + if (mSelMenu > EIGO_OR_END) { #endif mSelMenu = MENU_HIRA; } MenuSelectAnmInit(); mSelProc = PROC_MENU_SEL_ANM; - } else if (stick->checkLeftTrigger()) { + } else if (isPalOrJpn() && stick->checkLeftTrigger()) { mDoAud_seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0); mPrevSelMenu = mSelMenu; if (mSelMenu == MENU_HIRA) { #if REGION_JPN mSelMenu = MENU_END; #else - mSelMenu = MENU_EIGO; + mSelMenu = dusk::version::isRegionJpn() ? MENU_END : MENU_EIGO; #endif } else { mSelMenu--; @@ -1009,7 +1122,7 @@ void dName_c::MenuSelect() { #if REGION_PAL if (mSelMenu == MENU_EIGO) { #else - if (mSelMenu == MENU_END) { + if (mSelMenu == EIGO_OR_END) { #endif if (nameCheck() != 0) { mDoAud_seStart(Z2SE_SY_NAME_OK, NULL, 0, 0); @@ -1024,7 +1137,7 @@ void dName_c::MenuSelect() { #if REGION_PAL if (mSelMenu == MENU_EIGO) { #else - if (mSelMenu == MENU_END) { + if (mSelMenu == EIGO_OR_END) { #endif if (nameCheck() != 0) { mDoAud_seStart(Z2SE_SY_NAME_OK, NULL, 0, 0); @@ -1067,9 +1180,11 @@ void dName_c::MenuSelectAnm2() { if (canMove == true) { if (prevMenu_i != mojiSet_i) { mMenuText[prevMenu_i]->setWhite(JUtility::TColor(0x96, 0x96, 0x96, 0xFF)); - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN + IF_DUSK_BLOCK(isPalOrJpn()) mMenuIcon[mojiSet_i]->scale(g_nmHIO.mMenuScale, g_nmHIO.mMenuScale); mMenuText[mojiSet_i]->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); + IF_DUSK_BLOCK_END #endif } selectCursorMove(); @@ -1081,6 +1196,11 @@ void dName_c::MenuSelectAnm2() { void dName_c::MenuSelectAnm3() {} void dName_c::menuAbtnSelect() { +#if TARGET_PC + if (dusk::version::isRegionPal() && mSelMenu == MENU_EIGO) { + goto pal_eigo; + } +#endif switch (mSelMenu) { case MENU_HIRA: case MENU_KATA: @@ -1098,6 +1218,7 @@ void dName_c::menuAbtnSelect() { #else case MENU_END: #endif + IF_DUSK(pal_eigo:) int nameNum = nameCheck(); if (nameNum != 0) { playNameSet(nameNum); @@ -1116,44 +1237,33 @@ void dName_c::backSpace() { if (mCurPos != 0) { mDoAud_seStart(Z2SE_SY_NAME_DELETE, NULL, 0, 0); - #if REGION_JPN - // ' ' (full-width space) - if (mCurPos == 8 && mChrInfo[7].mCharacter != '\x81\x40') { - #else - if (mCurPos == 8 && mChrInfo[7].mCharacter != ' ') { - #endif + if (mCurPos == 8 && mChrInfo[7].mCharacter != SPACE_MAYBE_FULL) { mChrInfo[7].mColumn = 7; mChrInfo[7].mRow = 1; - #if REGION_PAL || REGION_JPN + #if TARGET_PC + mChrInfo[7].mMojiSet = isPalOrJpn() ? MOJI_HIRA : MOJI_EIGO; + #elif REGION_PAL || REGION_JPN mChrInfo[7].mMojiSet = MOJI_HIRA; #else mChrInfo[7].mMojiSet = MOJI_EIGO; #endif mChrInfo[7].field_0x3 = 1; - #if REGION_JPN - // ' ' (full-width space) - mChrInfo[7].mCharacter = '\x81\x40'; - #else - mChrInfo[7].mCharacter = ' '; - #endif + mChrInfo[7].mCharacter = SPACE_MAYBE_FULL; } else { for (int i = mCurPos - 1; i < 7; i++) { mChrInfo[i] = mChrInfo[i + 1]; } mChrInfo[7].mColumn = 7; mChrInfo[7].mRow = 1; - #if REGION_PAL || REGION_JPN +#if TARGET_PC + mChrInfo[7].mMojiSet = isPalOrJpn() ? MOJI_HIRA : MOJI_EIGO; +#elif REGION_PAL || REGION_JPN mChrInfo[7].mMojiSet = MOJI_HIRA; #else mChrInfo[7].mMojiSet = MOJI_EIGO; #endif mChrInfo[7].field_0x3 = 1; - #if REGION_JPN - // ' ' (full-width space) - mChrInfo[7].mCharacter = '\x81\x40'; - #else - mChrInfo[7].mCharacter = ' '; - #endif + mChrInfo[7].mCharacter = SPACE_MAYBE_FULL; } setNameText(); @@ -1164,7 +1274,31 @@ void dName_c::backSpace() { } void dName_c::mojiListChange() { - #if REGION_PAL + #if TARGET_PC + const char** mojiSet; + if (dusk::version::isRegionPal()) { + switch (mMojiSet) { + case MOJI_HIRA: + mojiSet = l_mojiEisuPal_1; + break; + case MOJI_KATA: + mojiSet = l_mojiEisuPal_2; + break; + } + } else { + switch (mMojiSet) { + case MOJI_HIRA: + mojiSet = l_mojiHira; + break; + case MOJI_KATA: + mojiSet = l_mojikata; + break; + case MOJI_EIGO: + mojiSet = l_mojiEisu; + break; + } + } + #elif REGION_PAL char** mojiSet; switch (mMojiSet) { @@ -1212,7 +1346,8 @@ void dName_c::mojiListChange() { strcpy(mMojiText[i], buf); } - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN + IF_DUSK_BLOCK(isPalOrJpn()) if (mSelProc == PROC_MOJI_SELECT) { mMenuIcon[mMojiSet]->scale(g_nmHIO.mMenuScale, g_nmHIO.mMenuScale); mMenuText[mMojiSet]->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); @@ -1221,6 +1356,7 @@ void dName_c::mojiListChange() { mMenuText[mPrevMojiSet]->setWhite(JUtility::TColor(0x96, 0x96, 0x96, 0xFF)); } } + IF_DUSK_BLOCK_END #endif } @@ -1241,9 +1377,11 @@ void dName_c::menuCursorMove2() { if (menu_i != mojiSet_i) { mMenuIcon[menu_i]->scale(g_nmHIO.mMenuScale, g_nmHIO.mMenuScale); mMenuText[menu_i]->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN + IF_DUSK_BLOCK(isPalOrJpn()) mMenuIcon[mojiSet_i]->scale(1.0f, 1.0f); mMenuText[mojiSet_i]->setWhite(JUtility::TColor(0x96, 0x96, 0x96, 0xFF)); + IF_DUSK_BLOCK_END #endif } @@ -1265,7 +1403,9 @@ void dName_c::selectCursorPosSet(int row) { mCharColumn = 3; break; case MENU_EIGO: - #if REGION_PAL + #if TARGET_PC + mCharColumn = dusk::version::isRegionPal() ? 8 : 6; + #elif REGION_PAL mCharColumn = 8; #else mCharColumn = 6; @@ -1481,9 +1621,11 @@ void dName_c::screenSet() { #endif } - #if !(REGION_PAL || REGION_JPN) + #if TARGET_PC || !(REGION_PAL || REGION_JPN) + IF_DUSK_BLOCK(!isPalOrJpn()) mMenuIcon[0]->hide(); mMenuIcon[1]->hide(); + IF_DUSK_BLOCK_END #endif mMojiPane = nameIn.NameInScr->search(MULTI_CHAR('moji_n')); @@ -1501,25 +1643,28 @@ void dName_c::screenSet() { ((J2DTextBox*)nameTagPane[i])->setFont(nameIn.font); ((J2DTextBox*)nameTagPane[i])->setString(72, ""); ((J2DTextBox*)nameTagPane[i])->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); - #if REGION_PAL + #if TARGET_PC || REGION_PAL + IF_DUSK_BLOCK(dusk::version::isRegionPal()) ((J2DTextBox*)nameTagPane[i])->resize(24.0f, 23.0f); + IF_DUSK_BLOCK_END #endif mNameText[i] = ((J2DTextBox*)nameTagPane[i])->getStringPtr(); } - #if REGION_PAL + #if REGION_PAL // DUSK version note: this code mutates strings. We just edit the table. + IF_DUSK_BLOCK(dusk::version::isRegionPal()) int idx = 2; - static u8 palMoji00[13] = { + static const u8 palMoji00[13] = { 0xC0, 0xC1, 0xC2, 0xC4, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, }; - static u8 palMoji01[13] = { + static const u8 palMoji01[13] = { 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD6, 0x8C, 0xD9, 0xDA, 0xDB, 0xDC, 0x2D, }; - static u8 palMoji10[13] = { + static const u8 palMoji10[13] = { 0xE0, 0xE1, 0xE2, 0xE4, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, }; - static u8 palMoji11[13] = { + static const u8 palMoji11[13] = { 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF6, 0x9C, 0xF9, 0xFA, 0xFB, 0xFC, 0xDF, }; @@ -1536,6 +1681,7 @@ void dName_c::screenSet() { l_mojiEisuPal_2[idx + 1][0] = palMoji11[i]; l_mojiEisuPal_2[idx + 1][1] = 0; } + IF_DUSK_BLOCK_END #endif mCharColumn = 0; @@ -1574,18 +1720,15 @@ void dName_c::displayInit() { mNameCursor[i]->hide(); mChrInfo[i].mColumn = 7; mChrInfo[i].mRow = 1; - #if REGION_PAL || REGION_JPN + #if TARGET_PC + mChrInfo[i].mMojiSet = isPalOrJpn() ? MOJI_HIRA : MOJI_EIGO; + #elif REGION_PAL || REGION_JPN mChrInfo[i].mMojiSet = MOJI_HIRA; #else mChrInfo[i].mMojiSet = MOJI_EIGO; #endif mChrInfo[i].field_0x3 = 1; - #if REGION_JPN - // ' ' (full-width space) - mChrInfo[i].mCharacter = '\x81\x40'; - #else - mChrInfo[i].mCharacter = ' '; - #endif + mChrInfo[i].mCharacter = SPACE_MAYBE_FULL; } mIsInputEnd = false; @@ -1596,7 +1739,63 @@ void dName_c::NameStrSet() { int i = 0; while (*moji != 0) { - #if REGION_PAL + #if TARGET_PC + if (dusk::version::isRegionPal()) { + mChrInfo[i].mCharacter = static_cast(*moji); + + for (int j = 0; j < 65; j++) { + if (mChrInfo[i].mCharacter == *(u8*)l_mojiEisuPal_1[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojiEisuPal_2[j]) + { + mChrInfo[i].mColumn = j / 5; + mChrInfo[i].mRow = j % 5; + mChrInfo[i].mMojiSet = MOJI_HIRA; + break; + } + } + moji++; + i++; + } else { + if (*(u8*)moji >> 4 == 8 || *(u8*)moji >> 4 == 9) { + mChrInfo[i].mCharacter = *(u16*)moji; + + for (int j = 0; j < 65; j++) { + if (mChrInfo[i].mCharacter == *(u16*)l_mojiHira[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojiHira2[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojiHira3[j]) + { + mChrInfo[i].mColumn = j / 5; + mChrInfo[i].mRow = j % 5; + mChrInfo[i].mMojiSet = MOJI_HIRA; + break; + } else if (mChrInfo[i].mCharacter == *(u16*)l_mojikata[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojikata2[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojikata3[j]) + { + mChrInfo[i].mColumn = j / 5; + mChrInfo[i].mRow = j % 5; + mChrInfo[i].mMojiSet = MOJI_KATA; + break; + } + } + moji += 2; + i++; + } else { + mChrInfo[i].mCharacter = *moji; + + for (int j = 0; j < 65; j++) { + if (mChrInfo[i].mCharacter == *(u8*)l_mojiEisu[j]) { + mChrInfo[i].mColumn = j / 5; + mChrInfo[i].mRow = j % 5; + mChrInfo[i].mMojiSet = MOJI_EIGO; + break; + } + } + moji++; + i++; + } + } + #elif REGION_PAL mChrInfo[i].mCharacter = static_cast(*moji); for (int j = 0; j < 65; j++) { @@ -1669,7 +1868,9 @@ s32 dName_c::getMenuPosIdx(u8 selPos) { result = 1; break; case 2: - #if REGION_PAL + #if TARGET_PC + result = dusk::version::isRegionPal() ? 3 : 2; + #elif REGION_PAL result = 3; #else result = 2; diff --git a/src/d/d_particle.cpp b/src/d/d_particle.cpp index 248424e9a2..9b89afc7b1 100644 --- a/src/d/d_particle.cpp +++ b/src/d/d_particle.cpp @@ -1972,7 +1972,12 @@ void dPa_light8PcallBack::draw(JPABaseEmitter* param_1, JPABaseParticle* param_2 JGeometry::TVec3 local_154; JGeometry::TVec3 local_160; JGeometry::TVec3 local_16c; - dPa_setWindPower(param_2); +#if TARGET_PC + if (dusk::frame_interp::is_sim_frame()) +#endif + { + dPa_setWindPower(param_2); + } MTXIdentity(local_60); MTXIdentity(auStack_90); param_2->getBaseAxis(&local_10c); diff --git a/src/d/d_resorce.cpp b/src/d/d_resorce.cpp index 73f031beae..bb982c4afb 100644 --- a/src/d/d_resorce.cpp +++ b/src/d/d_resorce.cpp @@ -102,11 +102,7 @@ static void setIndirectTex(J3DModelData* i_modelData) { texture->setResTIMG(i, *mDoGph_gInf_c::getFrameBufferTimg()); } if (memcmp(textureName, "Zbuffer", 8) == 0) { -#if !TARGET_PC texture->setResTIMG(i, *mDoGph_gInf_c::getZbufferTimg()); -#else - DuskLog.warn("Zbuffer texture binding not yet supported"); -#endif } } } diff --git a/src/d/d_s_logo.cpp b/src/d/d_s_logo.cpp index f04df0fa39..db0cfa1387 100644 --- a/src/d/d_s_logo.cpp +++ b/src/d/d_s_logo.cpp @@ -24,6 +24,7 @@ #include "JSystem/JUtility/JUTConsole.h" #include "dusk/logging.h" +#include "dusk/version.hpp" #if !PLATFORM_GCN #include @@ -44,7 +45,12 @@ struct homeBtnData { }; #endif -#if VERSION == VERSION_SHIELD +#if TARGET_PC +using namespace dusk::version; + +#define LOGO_ARC versionSelect({{GameVersion::GcnJpn, "Logo"}, {GameVersion::GcnPal, "LogoPal"}}, "LogoUs") +#define MSG_PATH versionSelect({{GameVersion::GcnJpn, "/res/Msgjp/bmgres.arc"}}, "/res/Msgus/bmgres.arc") +#elif VERSION == VERSION_SHIELD #define LOGO_ARC "LogoUs" #define MSG_PATH "/res/Msgcn/bmgres.arc" #elif VERSION == VERSION_GCN_JPN @@ -789,7 +795,13 @@ dScnLogo_c::~dScnLogo_c() { JKR_DELETE(mProgressiveNo); JKR_DELETE(mProgressiveSel); - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC + if (getGameVersion() == GameVersion::GcnPal) { + mpPalLogoResCommand->getArchive()->removeResourceAll(); + mpPalLogoResCommand->getArchive()->unmount(); + mpPalLogoResCommand->destroy(); + } + #elif VERSION == VERSION_GCN_PAL mpPalLogoResCommand->getArchive()->removeResourceAll(); mpPalLogoResCommand->getArchive()->unmount(); mpPalLogoResCommand->destroy(); @@ -951,7 +963,8 @@ static int phase_0(dScnLogo_c* i_this) { JUT_ASSERT(1528, i_this->mLogo01Heap != NULL); JKRHEAP_NAME(i_this->mLogo01Heap, "Logo01"); - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL + IF_DUSK_BLOCK(getGameVersion() == GameVersion::GcnPal) switch (i_this->getPalLanguage()) { case 1: i_this->mpPalLogoResCommand = mDoDvdThd_mountArchive_c::create("/res/Layout/LogoPalGm.arc", 0, NULL); @@ -970,6 +983,7 @@ static int phase_0(dScnLogo_c* i_this) { i_this->mpPalLogoResCommand = mDoDvdThd_mountArchive_c::create("/res/Layout/LogoPalUk.arc", 0, NULL); break; } + IF_DUSK_BLOCK_END #endif #if PLATFORM_WII || PLATFORM_SHIELD @@ -990,7 +1004,8 @@ static int phase_1(dScnLogo_c* i_this) { } #endif - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL + IF_DUSK_BLOCK(getGameVersion() == GameVersion::GcnPal) if (!mDoDvdThd::SyncWidthSound) { return cPhs_INIT_e; } @@ -998,6 +1013,7 @@ static int phase_1(dScnLogo_c* i_this) { if (!i_this->mpPalLogoResCommand->sync()) { return cPhs_INIT_e; } + IF_DUSK_BLOCK_END #endif int rt; @@ -1104,26 +1120,12 @@ int dScnLogo_c::create() { checkProgSelect(); if (field_0x20a != 0) { mExecCommand = EXEC_PROG_IN; - #if TARGET_PC - mTimer = dusk::getSettings().game.skipWarningScreen ? 1 : 30; - #else mTimer = 30; - #endif field_0x218 = getProgressiveMode(); } else { #if TARGET_PC - if (dusk::getSettings().game.skipWarningScreen) { - mTimer = 0; // Possibly unnecessary but just in case - mExecCommand = EXEC_DVD_WAIT; - } else { - if (mDoRst::getWarningDispFlag()) { - mTimer = 90; - mExecCommand = EXEC_NINTENDO_IN; - } else { - mTimer = 120; - mExecCommand = EXEC_WARNING_IN; - } - } + mTimer = 0; // Possibly unnecessary but just in case + mExecCommand = EXEC_DVD_WAIT; #else if (mDoRst::getWarningDispFlag()) { mTimer = 90; @@ -1237,7 +1239,113 @@ void dScnLogo_c::logoInitGC() { ResTIMG* dolbyImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 3); mDolbyLogo = JKR_NEW dDlst_2D_c(dolbyImg, 189, 150, 232, 112, 255); -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC + if (getGameVersion() == GameVersion::GcnPal) { + u8 language = getPalLanguage(); + if (language >= 5) { + language = 0; + } + + static const char* choice[] = { + "50_60_choice_eng.bti", + "50_60_choice_ger.bti", + "50_60_choice_fra.bti", + "50_60_choice_spa.bti", + "50_60_choice_ita.bti", + }; + + static const char* yes[] = { + "60_set_eng.bti", + "60_set_ger.bti", + "60_set_fra.bti", + "60_set_spa.bti", + "60_set_ita.bti", + }; + + static const char* no[] = { + "50_set_eng.bti", + "50_set_ger.bti", + "50_set_fra.bti", + "50_set_spa.bti", + "50_set_ita.bti", + }; + + static const char* prog[] = { + "progressive_pro.bti", + "progressive_pro_gm.bti", + "progressive_pro_fr.bti", + "progressive_pro_sp.bti", + "progressive_pro_it.bti", + }; + + static const char* intr[] = { + "progressive_inter.bti", + "progressive_inter_gm.bti", + "progressive_inter_fr.bti", + "progressive_inter_sp.bti", + "progressive_inter_it.bti", + }; + + static const char* warning[] = { + "warning.bti", + "warning_gm.bti", + "warning_fr.bti", + "warning_sp.bti", + "warning_it.bti", + }; + + static const char* warningPs[] = { + "warning_pstart.bti", + "warning_pstart_gm.bti", + "warning_pstart_fr.bti", + "warning_pstart_sp.bti", + "warning_pstart_it.bti", + }; + + ResTIMG* warningImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', warning[language]); + mWarning = JKR_NEW dDlst_2D_c(warningImg, 0, 0, FB_WIDTH, FB_HEIGHT, 255); + + ResTIMG* warnStartImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', warningPs[language]); + mWarningStart = JKR_NEW dDlst_2D_c(warnStartImg, 0, 359, FB_WIDTH, 48, 255); + + ResTIMG* progChoiceImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', choice[language]); + mProgressiveChoice = JKR_NEW dDlst_2D_c(progChoiceImg, 113, 143, 416, 210, 255); + + ResTIMG* progYesImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', yes[language]); + mProgressiveYes = JKR_NEW dDlst_2D_c(progYesImg, 121, 352, 200, 72, 255); + mProgressiveYes->getPicture()->setWhite(JUtility::TColor(160, 160, 160, 255)); + + ResTIMG* progNoImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', no[language]); + mProgressiveNo = JKR_NEW dDlst_2D_c(progNoImg, 320, 352, 200, 72, 255); + mProgressiveNo->getPicture()->setWhite(JUtility::TColor(160, 160, 160, 255)); + + mProgressivePro = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', prog[language]); + mProgressiveInter = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', intr[language]); + mProgressiveSel = JKR_NEW dDlst_2D_c(mProgressivePro, 153, 309, 336, 88, 255); + } else { + ResTIMG* warningImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 10); + mWarning = JKR_NEW dDlst_2D_c(warningImg, 0, 0, FB_WIDTH, FB_HEIGHT, 255); + + ResTIMG* warnStartImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 11); + mWarningStart = JKR_NEW dDlst_2D_c(warnStartImg, 0, 359, FB_WIDTH, 48, 255); + + ResTIMG* progChoiceImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 5); + mProgressiveChoice = JKR_NEW dDlst_2D_c(progChoiceImg, 113, 281, 416, 72, 255); + + ResTIMG* progYesImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 9); + mProgressiveYes = JKR_NEW dDlst_2D_c(progYesImg, 211, 372, 80, 32, 255); + mProgressiveYes->getPicture()->setWhite(JUtility::TColor(160, 160, 160, 255)); + + ResTIMG* progNoImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 7); + mProgressiveNo = JKR_NEW dDlst_2D_c(progNoImg, 350, 372, 80, 32, 255); + mProgressiveNo->getPicture()->setWhite(JUtility::TColor(160, 160, 160, 255)); + + mProgressivePro = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 8); + mProgressiveInter = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 6); + mProgressiveSel = JKR_NEW dDlst_2D_c(mProgressivePro, 153, 309, 336, 88, 255); + } + +#elif VERSION == VERSION_GCN_PAL u8 language = getPalLanguage(); if (language >= 5) { language = 0; @@ -1395,7 +1503,30 @@ void dScnLogo_c::dvdDataLoad() { mpButtonCommand = aramMount(BUTTON_RES_PATH, mDoExt_getJ2dHeap()); mpCardIconCommand = aramMount(ICON_RES_PATH, mDoExt_getJ2dHeap()); - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC + if (getGameVersion() == GameVersion::GcnPal) { + switch (getPalLanguage()) { + case 1: + mpBmgResCommand = onMemMount("/res/Msgde/bmgres.arc"); + break; + case 2: + mpBmgResCommand = onMemMount("/res/Msgfr/bmgres.arc"); + break; + case 3: + mpBmgResCommand = onMemMount("/res/Msgsp/bmgres.arc"); + break; + case 4: + mpBmgResCommand = onMemMount("/res/Msgit/bmgres.arc"); + break; + case 0: + default: + mpBmgResCommand = onMemMount("/res/Msguk/bmgres.arc"); + break; + } + } else { + mpBmgResCommand = onMemMount(MSG_PATH); + } + #elif VERSION == VERSION_GCN_PAL switch (getPalLanguage()) { case 1: mpBmgResCommand = onMemMount("/res/Msgde/bmgres.arc"); @@ -1435,7 +1566,10 @@ void dScnLogo_c::dvdDataLoad() { mpMsgResCommand[1] = aramMount(MSG_RES1_PATH, mDoExt_getJ2dHeap()); mpMsgResCommand[2] = aramMount(MSG_RES2_PATH, mDoExt_getJ2dHeap()); mpMsgResCommand[3] = aramMount(MSG_RES3_PATH, mDoExt_getJ2dHeap()); -#if VERSION == VERSION_GCN_JPN +#if TARGET_PC + const auto res4Path = versionSelect({{GameVersion::GcnJpn, "/res/Layout/msgres04.arc"}}, "/res/Layout/msgres04F.arc"); + mpMsgResCommand[4] = aramMount( res4Path, mDoExt_getJ2dHeap()); +#elif VERSION == VERSION_GCN_JPN mpMsgResCommand[4] = aramMount("/res/Layout/msgres04.arc", mDoExt_getJ2dHeap()); #else mpMsgResCommand[4] = aramMount("/res/Layout/msgres04F.arc", mDoExt_getJ2dHeap()); @@ -1445,7 +1579,23 @@ void dScnLogo_c::dvdDataLoad() { mpMain2DCommand = onMemMount(MAIN2D_PATH); -#if VERSION == VERSION_GCN_JPN +#if TARGET_PC + const auto fontResPath = versionSelect( + { + {GameVersion::GcnJpn, "/res/Fontjp/fontres.arc"}, + {GameVersion::GcnPal, "/res/Fonteu/fontres.arc"}, + }, "/res/Fontus/fontres.arc"); + const auto fontRubyPath = versionSelect( + { + {GameVersion::GcnJpn, "/res/Fontjp/rubyres.arc"}, + {GameVersion::GcnPal, "/res/Fonteu/rubyres.arc"}, + }, "/res/Fontus/rubyres.arc"); + + // Note: GCN_JPN mounts this archive as tail instead of head. + // I'm guessing this is fine since we have more RAM. + mpFontResCommand = onMemMount(fontResPath); + mpRubyResCommand = onMemMount(fontRubyPath); +#elif VERSION == VERSION_GCN_JPN mpFontResCommand = mDoDvdThd_mountXArchive_c::create("/res/Fontjp/fontres.arc", 1, JKRArchive::MOUNT_MEM, NULL); mpRubyResCommand = onMemMount("/res/Fontjp/rubyres.arc"); #elif VERSION == VERSION_GCN_PAL @@ -1545,7 +1695,7 @@ static int dScnLogo_IsDelete(dScnLogo_c* i_this) { return 1; } -#if VERSION == VERSION_GCN_PAL || PLATFORM_WII || PLATFORM_SHIELD +#if TARGET_PC || VERSION == VERSION_GCN_PAL || PLATFORM_WII || PLATFORM_SHIELD u8 dScnLogo_c::getPalLanguage() { u8 language; diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index 735ce4d0ca..6baa07da30 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -5,19 +5,20 @@ #include "d/dolzel.h" // IWYU pragma: keep -#include "d/d_s_name.h" #include "JSystem/JKernel/JKRExpHeap.h" #include "d/d_com_inf_game.h" #include "d/d_meter2_info.h" +#include "d/d_s_name.h" +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/memory.h" +#include "dusk/settings.h" +#include "f_op/f_op_overlap_mng.h" #include "f_op/f_op_scene_mng.h" #include "m_Do/m_Do_Reset.h" #include "m_Do/m_Do_graphic.h" #include "m_Do/m_Do_machine.h" -#include "m_Do/m_Do_mtx.h" #include "m_Do/m_Do_main.h" -#include "f_op/f_op_overlap_mng.h" -#include "dusk/memory.h" -#include "dusk/settings.h" +#include "m_Do/m_Do_mtx.h" #if TARGET_PC #define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen) @@ -412,6 +413,16 @@ void dScnName_c::changeGameScene() { dKy_clear_game_init(); dComIfGs_resetDan(); dComIfGs_setRestartRoomParam(0); + +#if TARGET_PC + if (dusk::getSettings().game.speedrunMode && dusk::getSettings().game.hideTvSettingsScreen) { + // start a new run on file load if a run isn't already in progress + if (!dusk::m_speedrunInfo.m_isRunStarted) { + dusk::ImGuiMenuGame::resetForSpeedrunMode(); + dusk::m_speedrunInfo.startRun(); + } + } +#endif } } diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp index 38308d0668..f7ebf6a20d 100644 --- a/src/d/d_s_play.cpp +++ b/src/d/d_s_play.cpp @@ -40,7 +40,9 @@ #include "JSystem/JKernel/JKRAramArchive.h" #if TARGET_PC +#include "dusk/autosave.h" #include "dusk/memory.h" +#include "dusk/ui/ui.hpp" #endif #if DEBUG @@ -700,6 +702,10 @@ static u8 lbl_8074CAE4; static u32 l_sceneChangeStartTick; #endif +#if TARGET_PC +static BOOL autoSaved; +#endif + static int dScnPly_Execute(dScnPly_c* i_this) { #if DEBUG fapGm_HIO_c::startCpuTimer(); @@ -742,6 +748,15 @@ static int dScnPly_Execute(dScnPly_c* i_this) { } } + #if TARGET_PC + if (!dComIfGp_event_runCheck() && !fopOvlpM_IsPeek() && !dComIfG_resetToOpening(i_this) && + !dComIfGp_isEnableNextStage() && autoSaved == FALSE) + { + triggerAutoSave(); + autoSaved = TRUE; + } + #endif + dKy_itudemo_se(); #if DEBUG @@ -780,7 +795,17 @@ static int dScnPly_Execute(dScnPly_c* i_this) { dJprev_c::get()->update(); #endif +#if TARGET_PC + if (!dusk::ui::is_prelaunch_open()) { + dDemo_c::update(); + } else if (dusk::getSettings().audio.menuSounds) { + s8 reverb = dComIfGp_getReverb(dComIfGp_roomControl_getStayNo()); + f32 fxMix = reverb / 127.0f; + g_mEnvSeMgr.field_0x144.startEnvSeDirLevel(JA_SE_ATM_WIND_1, fxMix, 1.0f); + } +#else dDemo_c::update(); +#endif #if DEBUG dJcame_c::get()->update(); @@ -1593,6 +1618,11 @@ static int dScnPly_Create(scene_class* i_this) { dScnPly_c* a_this = (dScnPly_c*)i_this; int phase_state = dComLbG_PhaseHandler(&a_this->field_0x1c4, l_method, a_this); + + #if TARGET_PC + autoSaved = FALSE; + #endif + return phase_state; } diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index 3289a49c64..8e4af75322 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -16,6 +16,8 @@ #include #include +#include "dusk/version.hpp" + #if PLATFORM_WII || PLATFORM_SHIELD #include #include @@ -25,7 +27,11 @@ #include "lingcod/lingcod.h" #endif +#if TARGET_PC #include "dusk/settings.h" +#include +#include +#endif static u8 dSv_item_rename(u8 i_itemNo) { switch (i_itemNo) { @@ -343,6 +349,10 @@ void dSv_player_item_c::setItem(int i_slotNo, u8 i_itemNo) { dComIfGp_setSelectItem(i); } } + + #if TARGET_PC + triggerAutoSave(); + #endif } u8 dSv_player_item_c::getItem(int i_slotNo, bool i_checkCombo) const { @@ -1027,7 +1037,7 @@ void dSv_player_config_c::init() { mAttentionType = 0; mVibration = 1; -#if DEBUG +#if DEBUG // DUSK VERSION SUPPORT: This field isn't used, so we can ignore it. mLanguage = SCGetLanguage(); #elif REGION_PAL || VERSION >= VERSION_WII_USA_R2 mLanguage = OSGetLanguage(); @@ -1072,7 +1082,8 @@ void dSv_player_config_c::setVibration(u8 i_status) { } u8 dSv_player_config_c::getPalLanguage() const { -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC || VERSION == VERSION_GCN_PAL + IF_DUSK_BLOCK(dusk::version::getGameVersion() == dusk::version::GameVersion::GcnPal) switch (OSGetLanguage()) { case 0: return LANGUAGE_ENGLISH; @@ -1085,6 +1096,7 @@ u8 dSv_player_config_c::getPalLanguage() const { case 4: return LANGUAGE_ITALIAN; } + IF_DUSK_BLOCK_END #elif VERSION >= VERSION_WII_USA_R0 switch (SCGetLanguage()) { case 1: diff --git a/src/dusk/OSReport.cpp b/src/dusk/OSReport.cpp new file mode 100644 index 0000000000..aee1e17bc6 --- /dev/null +++ b/src/dusk/OSReport.cpp @@ -0,0 +1,141 @@ +#include + +#include "aurora/lib/logging.hpp" +#include "os_report.h" + +aurora::Module Log("dusk::osReport"); + +bool dusk::OSReportReallyForceEnable = false; + +u8 __OSReport_disable; + +void OSReportDisable() { + __OSReport_disable = true; +} + +void OSReportEnable() { + __OSReport_disable = false; +} + +static bool checkEnabled() { + return !__OSReport_disable || dusk::OSReportReallyForceEnable; +} + +#ifndef va_copy +#define va_copy(d, s) ((d) = (s)) +#endif + +static std::string FormatToString(const char* msg, va_list list) { + size_t size = (strlen(msg) * 2) + 50; + std::string str; + va_list ap; + int attempts = 0; + while (true) { + str.resize(size); + va_copy(ap, list); + int n = vsnprintf(str.data(), size, msg, ap); + va_end(ap); + if (n > -1 && n < size) { + str.resize(n); + break; + } + + ++attempts; + if (attempts >= 3) { + if (n == -1) { + str.clear(); + } + break; + } + if (n > -1) { + size = n + 1; + } else { + size *= 2; + } + } + return str; +} + +void OSReport(const char* fmt, ...) { + if (!checkEnabled()) { + return; + } + va_list args; + va_start(args, fmt); + const auto str = FormatToString(fmt, args); + va_end(args); + + Log.info("{}", str); +} + +void OSPanic(const char* file, int line, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + const auto str = FormatToString(fmt, args); + va_end(args); + + Log.fatal("[{}:{}] {}", file, line, str); +} + +void OSReport_Error(const char* fmt, ...) { + if (!checkEnabled()) { + return; + } + + va_list args; + va_start(args, fmt); + const auto str = FormatToString(fmt, args); + va_end(args); + + Log.error("{}", str); +} + +void OSReport_FatalError(const char* fmt, ...) { + if (!checkEnabled()) { + return; + } + + va_list args; + va_start(args, fmt); + const auto str = FormatToString(fmt, args); + va_end(args); + + Log.fatal("{}", str); +} + +void OSReport_Warning(const char* fmt, ...) { + if (!checkEnabled()) { + return; + } + + va_list args; + va_start(args, fmt); + const auto str = FormatToString(fmt, args); + va_end(args); + + Log.warn("{}", str); +} + +void OSReport_System(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + OSVAttention(fmt, args); + va_end(args); +} + +void OSVAttention(const char* fmt, va_list args) { + if (!checkEnabled()) { + return; + } + + const auto str = FormatToString(fmt, args); + + Log.info("{}", str); +} + +void OSAttention(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + OSVAttention(fmt, args); + va_end(args); +} \ No newline at end of file diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp new file mode 100644 index 0000000000..28e66c891e --- /dev/null +++ b/src/dusk/achievements.cpp @@ -0,0 +1,1147 @@ +#include "dusk/achievements.h" +#include "dusk/io.hpp" +#include "dusk/main.h" +#include "d/d_com_inf_game.h" +#include "d/d_item_data.h" +#include "d/d_map_path_fmap.h" +#include "d/d_stage.h" +#include "d/d_menu_fmap.h" +#include "JSystem/JKernel/JKRArchive.h" +#include "d/d_meter2_info.h" +#include "d/actor/d_a_alink.h" +#include "d/actor/d_a_ni.h" +#include "d/actor/d_a_npc4.h" +#include "d/actor/d_a_b_ob.h" +#include "d/actor/d_a_player.h" +#include "d/d_demo.h" +#include "dusk/ui/ui.hpp" +#include "f_pc/f_pc_name.h" +#include "f_op/f_op_actor_mng.h" +#include "f_pc/f_pc_name.h" + +#include +#include + +namespace dusk { + +using json = nlohmann::json; + +static void* s_cucco_play_search(void* i_actor, void*) { + if (!fopAcM_IsActor(i_actor) || fopAcM_GetName((fopAc_ac_c*)i_actor) != fpcNm_NI_e) { + return nullptr; + } + auto* ni = static_cast(i_actor); + return ni->mAction == ACTION_PLAY_e ? i_actor : nullptr; +} + +static void checkGoatHerding(Achievement& a, int32_t threshMs) { + if (dMeter2Info_getMaxCount() != 20 || dMeter2Info_getNowCount() != 20) { + return; + } + const int32_t elapsed = dMeter2Info_getTimeMs(); + if (elapsed > 0 && elapsed <= threshMs) { + a.progress = 1; + } +} + +static constexpr auto ACHIEVEMENTS_FILENAME = "achievements.json"; + +std::vector AchievementSystem::makeEntries() { + return { + // Challenge + { + { + "hero_of_twilight", + "Hero of Twilight", + "Deliver the finishing blow to Ganondorf.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link != nullptr && link->mProcID == daAlink_c::PROC_GANON_FINISH) { + a.progress = 1; + } + }, + {} + }, + { + { + "completionist", + "Completionist", + "Complete the game after collecting all equipment, heart containers, portals, bugs, poes, and hidden skills.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + if (dComIfGs_getMaxLife() < 100) { + return; + } + for (int i = 0; i < 4; ++i) { + if (!dComIfGs_isCollectMirror(i)) { + return; + } + } + for (int i = 0; i < 3; ++i) { + if (!dComIfGs_isCollectCrystal(i)) { + return; + } + } + static const u16 skillBits[] = { + dSv_event_flag_c::F_0338, dSv_event_flag_c::F_0339, + dSv_event_flag_c::F_0340, dSv_event_flag_c::F_0341, + dSv_event_flag_c::F_0342, dSv_event_flag_c::F_0343, + dSv_event_flag_c::F_0344 + }; + for (u16 bit : skillBits) { + if (!dComIfGs_isEventBit(bit)) { + return; + } + } + if (dComIfGs_checkGetInsectNum() < 24) { + return; + } + if (dComIfGs_getPohSpiritNum() < 60) { + return; + } + if (dComIfGs_getWalletSize() < 2) { + return; + } + if (dComIfGs_getArrowMax() < 100) { + return; + } + if (!dComIfGs_isCollectSword(COLLECT_MASTER_SWORD)) { + return; + } + if (!dComIfGs_isCollectShield(COLLECT_HYLIAN_SHIELD)) { + return; + } + if (!dComIfGs_isCollectClothes(KOKIRI_CLOTHES_FLAG)) { + return; + } + if (!dComIfGs_isItemFirstBit(dItemNo_WEAR_ZORA_e)) { + return; + } + if (!dComIfGs_isItemFirstBit(dItemNo_ARMOR_e)) { + return; + } + static const struct { int stage; int sw; } warpPortals[] = { + { dStage_SaveTbl_ORDON, 52 }, // Ordon Spring + { dStage_SaveTbl_FARON, 71 }, // South Faron Woods + { dStage_SaveTbl_FARON, 2 }, // North Faron Woods + { dStage_SaveTbl_GROVE, 100 }, // Sacred Grove + { dStage_SaveTbl_FIELD, 21 }, // Gorge + { dStage_SaveTbl_ELDIN, 31 }, // Kakariko Village + { dStage_SaveTbl_ELDIN, 21 }, // Death Mountain + { dStage_SaveTbl_FIELD, 99 }, // Bridge of Eldin + { dStage_SaveTbl_FIELD, 3 }, // Castle Town + { dStage_SaveTbl_LANAYRU, 10 }, // Lake Hylia + { dStage_SaveTbl_LANAYRU, 2 }, // Zora's Domain + { dStage_SaveTbl_LANAYRU, 21 }, // Upper Zora's River + { dStage_SaveTbl_SNOWPEAK, 21 }, // Snowpeak + { dStage_SaveTbl_DESERT, 21 }, // Gerudo Mesa + { dStage_SaveTbl_DESERT, 40 }, // Mirror Chamber + }; + for (const auto& p : warpPortals) { + if (!g_dComIfG_gameInfo.info.getSavedata().getSave(p.stage).getBit().isSwitch(p.sw)) { + return; + } + } + if (dComIfGs_getCollectSmell() == dItemNo_NONE_e) { + return; + } + + if (dMeter2Info_getRecieveLetterNum() == 0) { + return; + } + + bool hasJournal = false; + for (int fi = 0; fi < 6; ++fi) { + if (dComIfGs_getFishNum(fi) != 0) { + hasJournal = true; + break; + } + } + if (!hasJournal) { + return; + } + + int bottleCount = 0; + for (int i = 0; i < dSv_player_item_c::BOTTLE_MAX; ++i) { + if (dComIfGs_getItem(SLOT_11 + i, false) != dItemNo_NONE_e) { + bottleCount++; + } + } + if (bottleCount < 4) { + return; + } + + int bombBagCount = 0; + for (int i = 0; i < dSv_player_item_c::BOMB_BAG_MAX; ++i) { + if (dComIfGs_getItem(SLOT_15 + i, false) != dItemNo_NONE_e) { + bombBagCount++; + } + } + if (bombBagCount < 3) { + return; + } + + bool hasJewelRod = false; + for (int slot = 0; slot < 24 && !hasJewelRod; ++slot) { + const u8 item = dComIfGs_getItem(slot, false); + if (item == dItemNo_JEWEL_ROD_e || item == dItemNo_JEWEL_BEE_ROD_e || item == dItemNo_JEWEL_WORM_ROD_e) { + hasJewelRod = true; + } + } + if (!hasJewelRod) { + return; + } + + static const u8 requiredWheelItems[] = { + dItemNo_BOOMERANG_e, + dItemNo_BOW_e, + dItemNo_W_HOOKSHOT_e, + dItemNo_SPINNER_e, + dItemNo_IRONBALL_e, + dItemNo_COPY_ROD_e, + dItemNo_HVY_BOOTS_e, + dItemNo_KANTERA_e, + dItemNo_PACHINKO_e, + dItemNo_HAWK_EYE_e, + dItemNo_ANCIENT_DOCUMENT_e, + dItemNo_HORSE_FLUTE_e, + }; + for (u8 required : requiredWheelItems) { + bool found = false; + for (int slot = 0; slot < 24; ++slot) { + if (dComIfGs_getItem(slot, false) == required) { + found = true; + break; + } + } + if (!found) { + return; + } + } + a.progress = 1; + }, + {} + }, + // Collection + { + { + "princess_of_bugs", + "The Princess of Bugs", + "Deliver all 24 golden bugs to Agitha.", + AchievementCategory::Collection, + true, 24, 0, false + }, + [](Achievement& a, json&) { + a.progress = dComIfGs_checkGetInsectNum(); + }, + {} + }, + { + { + "all_poes", + "Poe Collector", + "Collect all 60 Poe Souls.", + AchievementCategory::Collection, + true, 60, 0, false + }, + [](Achievement& a, json&) { + a.progress = dComIfGs_getPohSpiritNum(); + }, + {} + }, + { + { + "hylian_loach", + "Legendary Catch", + "Catch a Hylian Loach.", + AchievementCategory::Collection, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_getFishNum(1) > 0) { + a.progress = 1; + } + }, + {} + }, + { + { + "all_fish", + "Gone Fishin'", + "Catch all 6 species of fish.", + AchievementCategory::Collection, + true, 6, 0, false + }, + [](Achievement& a, json&) { + int nUniqueFish = 0; + for (int i = 0; i < 6; ++i) { + if (dComIfGs_getFishNum(i) != 0) { + nUniqueFish++; + } + } + a.progress = nUniqueFish; + }, + {} + }, + { + { + "a_big_heart", + "A Big Heart", + "Reach maximum health with all 20 heart containers.", + AchievementCategory::Collection, + true, 20, 0, false + }, + [](Achievement& a, json&) { + a.progress = dComIfGs_getMaxLife() / 5; + }, + {} + }, + { + { + "all_bottles", + "Glassware Guardian", + "Obtain all 4 bottles.", + AchievementCategory::Collection, + true, 4, 0, false + }, + [](Achievement& a, json&) { + if (daPy_getPlayerActorClass() == nullptr) { + return; + } + int count = 0; + for (int i = 0; i < dSv_player_item_c::BOTTLE_MAX; ++i) { + if (dComIfGs_getItem(SLOT_11 + i, false) != dItemNo_NONE_e) { + count++; + } + } + a.progress = count; + }, + {} + }, + { + { + "all_hidden_skills", + "Master of Secrets", + "Learn all 7 Hidden Skills.", + AchievementCategory::Collection, + true, 7, 0, false + }, + [](Achievement& a, json&) { + static const u16 skillBits[] = { + dSv_event_flag_c::F_0338, dSv_event_flag_c::F_0339, + dSv_event_flag_c::F_0340, dSv_event_flag_c::F_0341, + dSv_event_flag_c::F_0342, dSv_event_flag_c::F_0343, + dSv_event_flag_c::F_0344 + }; + int count = 0; + for (u16 bit : skillBits) { + if (dComIfGs_isEventBit(bit)) { + count++; + } + } + a.progress = count; + }, + {} + }, + { + { + "all_letters", + "We Deliver!", + "Collect all 16 postman letters.", + AchievementCategory::Collection, + true, 16, 0, false + }, + [](Achievement& a, json&) { + a.progress = dMeter2Info_getRecieveLetterNum(); + }, + {} + }, + { + { + "cave_of_ordeals", + "Conqueror of Ordeals", + "Clear all 50 floors of the Cave of Ordeals.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daNpcF_chkEvtBit(0x1F9)) { + a.progress = 1; + } + }, + {} + }, + { + { + "cave_of_ordeals_heartless", + "Indomitable", + "Clear all 50 floors of the Cave of Ordeals with only 3 heart containers.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) { + a.progress = 1; + } + }, + {} + }, + { + { + "speedrun_12h", + "Been There Done That", + "Defeat Ganondorf with a total save file play time under 12 hours.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); + if (ticks / OS_TIMER_CLOCK < 12 * 3600) { + a.progress = 1; + } + }, + {} + }, + { + { + "speedrun_8h", + "Swift Blade", + "Defeat Ganondorf with a total save file play time under 6 hours.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); + if (ticks / OS_TIMER_CLOCK < 8 * 3600) { + a.progress = 1; + } + }, + {} + }, + { + { + "dark_hammer_one_hit", + "Mortal Edge", + "Defeat Dark Hammer in a single hit.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (AchievementSystem::get().hasSignal("dark_hammer_one_hit")) { + a.progress = 1; + } + }, + {} + }, + { + { + "no_deaths_clear", + "Deathless", + "Defeat Ganondorf with 0 deaths on your save file.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + if (dComIfGs_getDeathCount() == 0) { + a.progress = 1; + } + }, + {} + }, + { + { + "untouchable", + "Untouchable", + "Kill 25 enemies in a row without taking damage.", + AchievementCategory::Challenge, + true, 25, 0, false + }, + [](Achievement& a, json&) { + auto& sys = AchievementSystem::get(); + if (sys.hasSignal("player_damaged")) { + a.progress = 0; + } + if (sys.hasSignal("enemy_killed")) { + a.progress++; + } + }, + {} + }, + { + { + "bow_100m_hit", + "Long Shot", + "Hit an enemy from over 100 meters away with the bow.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (AchievementSystem::get().hasSignal("arrow_hit_100m")) { + a.progress = 1; + } + }, + {} + }, + // Minigame + { + { + "plumm_max", + "Thank You Berry Much", + "Score 61,454 points in the Plumm minigame.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_getBalloonScore() >= 61454) { + a.progress = 1; + } + }, + {} + }, + { + { + "rollgoal_8", + "Rollgoal Novice", + "Complete the first 8 rollgoal stages.", + AchievementCategory::Minigame, + true, 8, 0, false + }, + [](Achievement& a, json&) { + a.progress = std::min((int)dComIfGs_getEventReg(0xf63f), 8); + }, + {} + }, + { + { + "rollgoal_all", + "Lost Your Marbles", + "Complete all rollgoal stages.", + AchievementCategory::Minigame, + true, 64, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isEventBit(dSv_event_flag_c::KORO2_ALLCLEAR)) { + a.progress = 64; + } else { + a.progress = dComIfGs_getEventReg(0xf63f); + } + }, + {} + }, + { + { + "goat_30s", + "Ranch Hand", + "Herd all 20 goats into the pen in under 30 seconds.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + checkGoatHerding(a, 30000); + }, + {} + }, + { + { + "goat_20s", + "Bane of Howard", + "Herd all 20 goats into the pen in under 20 seconds.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + checkGoatHerding(a, 20000); + }, + {} + }, + { + { + "goat_18s", + "King of the Ranch", + "Herd all 20 goats into the pen in under 18 seconds.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + checkGoatHerding(a, 18000); + }, + {} + }, + { + { + "snowboard_70s", + "Downhill Dash", + "Finish the snowboarding minigame in under 70 seconds.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const int32_t bestMs = dComIfGs_getRaceGameTime(); + if (dComIfGs_isEventBit(dSv_event_flag_c::F_0481) && + bestMs > 0 && bestMs <= 70000) { + a.progress = 1; + } + }, + {} + }, + { + { + "canoe_perfect", + "River Raider", + "Achieve a perfect score in the canoe minigame.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr) { + return; + } + static bool wasInCanoe = false; + bool inCanoe = link->mProcID >= daAlink_c::PROC_CANOE_RIDE && + link->mProcID <= daAlink_c::PROC_CANOE_KANDELAAR_POUR; + if (wasInCanoe && !inCanoe && dMeter2Info_getNowCount() >= 30) { + a.progress = 1; + } + wasInCanoe = inCanoe; + }, + {} + }, + { + { + "star_2_under_40s", + "Rising Star", + "Complete the STAR Prize 2 minigame in under 40 seconds.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if(dComIfGs_getHookGameTime() > 0 && dComIfGs_getHookGameTime() <= 40000) { + a.progress = 1; + } + }, + {} + }, + { + { + "star_2_under_30s", + "Shooting Star", + "Complete the STAR Prize 2 minigame in under 30 seconds.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if(dComIfGs_getHookGameTime() > 0 && dComIfGs_getHookGameTime() <= 30000) { + a.progress = 1; + } + }, + {} + }, + // Misc + { + { + "friendly_fire", + "Friendly Fire", + "Get hit by your own cannonball.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (AchievementSystem::get().hasSignal("iron_ball_hit_player")) { + a.progress = 1; + } + }, + {} + }, + { + { + "long_jump_attack", + "Long Jump Attack", + "Travel more than 20 meters in a single jump attack before landing.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + static bool inJump = false; + static float startX = 0.0f, startZ = 0.0f; + + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr) { + inJump = false; + return; + } + + if (!inJump) { + if (link->mProcID == daAlink_c::PROC_CUT_JUMP) { + inJump = true; + startX = link->current.pos.x; + startZ = link->current.pos.z; + } + } else if (link->mProcID == daAlink_c::PROC_CUT_JUMP_LAND) { + inJump = false; + const float dx = link->current.pos.x - startX; + const float dz = link->current.pos.z - startZ; + if (dx * dx + dz * dz >= 2000.0f * 2000.0f) { + a.progress = 1; + } + } else if (link->mProcID != daAlink_c::PROC_CUT_JUMP) { + inJump = false; + } + }, + {} + }, + { + { + "email_me", + "Email Me", + "Read a letter during the Dark Beast Ganon fight.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e); + if (dbgExists && AchievementSystem::get().hasSignal("open_letter")) { + a.progress = 1; + } + }, + {} + }, + { + { + "heavy_hitter", + "Heavy Hitter", + "Wear the Iron Boots during the end credits.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + if (daPy_getPlayerActorClass()->checkEquipHeavyBoots()) { + a.progress = 1; + } + }, + {} + }, + { + { + "fishing_rod_ganondorf", + "Here Fishy Fishy", + "Confuse Ganondorf with the fishing rod.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (AchievementSystem::get().hasSignal("ganondorf_fishing_rod")) { + a.progress = 1; + } + }, + {} + }, + { + { + "steal_from_trill", + "Petty Theft", + "Steal from Trill.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isEventBit(dSv_event_flag_c::F_0758)) { + a.progress = 1; + } + }, + {} + }, + { + { + "cucco_control", + "Cucco Whisperer", + "Take control of a cucco.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (fopAcM_Search(s_cucco_play_search, nullptr) != nullptr) { + a.progress = 1; + } + }, + {} + }, + // Glitched + { + { + "back_in_time", + "Back in Time", + "Perform the Back in Time glitch to play on the title screen.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (fopAcM_SearchByName(fpcNm_TITLE_e) == nullptr) { + return; + } + const auto* player = static_cast(daPy_getPlayerActorClass()); + + if (player != nullptr && player->mDemo.getDemoMode() == 1) { + a.progress = 1; + } + }, + {} + }, + { + { + "early_master_sword", + "Early Master Sword", + "Obtain the Master Sword before completing Midna's Desperate Hour.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isCollectSword(COLLECT_MASTER_SWORD) && !dComIfGs_isEventBit(0x1E08)) { + a.progress = 1; + } + }, + {} + }, + { + { + "earliest_master_sword", + "Earliest Master Sword", + "Obtain the Master Sword before meeting Midna.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isCollectSword(COLLECT_MASTER_SWORD) && !dComIfGs_isTransformLV(0)) { + a.progress = 1; + } + }, + {} + }, + { + { + "speedrun_4h", + "Hero of Time", + "Defeat Ganondorf with a total save file play time under 4 hours.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); + if (ticks / OS_TIMER_CLOCK < 4 * 3600) { + a.progress = 1; + } + }, + {} + }, + { + { + "no_fish_suit", + "No Fish Suit No Problem", + "Defeat Morpheel without equipping Zora Armor.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + static bool prevMorpheelAlive = false; + static bool inArena = false; + static bool zoraWorn = false; + const auto* morpheel = static_cast(fopAcM_SearchByName(fpcNm_B_OB_e)); + const bool morpheelAlive = morpheel != nullptr && morpheel->mAnmID != 0x14; + const bool morpheelDead = morpheel != nullptr && morpheel->mAnmID == 0x14; + const bool lakebedCleared = dComIfGs_isEventBit(dSv_event_flag_c::M_045); + + if (inArena && morpheel == nullptr) { + zoraWorn = false; + } + + if (morpheelAlive && !lakebedCleared) { + inArena = true; + if (daPy_py_c::checkZoraWearFlg()) { + zoraWorn = true; + } + } + + if (prevMorpheelAlive && morpheelDead && inArena && !zoraWorn) { + a.progress = 1; + } + + prevMorpheelAlive = morpheelAlive; + }, + {} + }, + { + { + "null_item", + "Null Item", + "Obtain the mysterious black rupee in the item wheel.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daPy_getPlayerActorClass() == nullptr) { + return; + } + for (int i = 0; i < 24; ++i) { + if (dComIfGs_getItem(i, false) == 0x00) { + a.progress = 1; + break; + } + } + }, + {} + }, + { + { + "stallord_skip", + "Stallord Skip", + "Leave Stallord's arena through the exit without defeating Stallord.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + static bool seenStallord = false; + if (strcmp(dComIfGp_getStartStageName(), "D_MN10A") != 0) { + seenStallord = false; + return; + } + if (dComIfGs_isEventBit(dSv_event_flag_c::F_0265)) { + seenStallord = false; + return; + } + if (fopAcM_SearchByName(fpcNm_B_DS_e) != nullptr) { + seenStallord = true; + } + if (seenStallord && + dComIfGp_isEnableNextStage() && + strcmp(dComIfGp_getNextStageName(), "F_SP125") == 0) { + a.progress = 1; + } + }, + {} + }, + { + { + "lakebed_before_lanayru", + "White Midna Glitch", + "Clear the Lakebed Temple before clearing Lanayru's Twilight.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isEventBit(dSv_event_flag_c::M_045) && + !dComIfGs_isDarkClearLV(2)) { + a.progress = 1; + } + }, + {} + }, + { + { + "early_hidden_village", + "Quick Detour", + "Rescue the Hidden Village before clearing Goron Mines.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isEventBit(dSv_event_flag_c::F_0278) && + !dComIfGs_isEventBit(dSv_event_flag_c::M_031)) { + a.progress = 1; + } + }, + {} + }, + { + { + "forest_temple_no_boomerang", + "Must Have Been The Wind", + "Complete the Forest Temple without obtaining the Gale Boomerang.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (!dComIfGs_isEventBit(dSv_event_flag_c::M_022)) { + return; + } + if (daPy_getPlayerActorClass() == nullptr) { + return; + } + for (int i = 0; i < 24; ++i) { + if (dComIfGs_getItem(i, false) == dItemNo_BOOMERANG_e) { + return; + } + } + a.progress = 1; + }, + {} + } + }; +} + +AchievementSystem::AchievementSystem() : m_entries(makeEntries()) {} + +AchievementSystem& AchievementSystem::get() { + static AchievementSystem instance; + return instance; +} + +std::vector AchievementSystem::getAchievements() const { + std::vector result; + result.reserve(m_entries.size()); + for (const auto& e : m_entries) { + result.push_back(e.achievement); + } + return result; +} + +void AchievementSystem::load() { + m_loaded = true; + const auto filePath = dusk::ConfigPath / ACHIEVEMENTS_FILENAME; + if (!std::filesystem::exists(filePath)) { + return; + } + try { + auto data = io::FileStream::ReadAllBytes(filePath); + auto j = json::parse(data); + if (!j.is_object()) { + return; + } + for (auto& e : m_entries) { + if (!j.contains(e.achievement.key)) { + continue; + } + const auto& entry = j[e.achievement.key]; + if (entry.contains("progress")) { + e.achievement.progress = entry["progress"].get(); + } + if (entry.contains("unlocked")) { + e.achievement.unlocked = entry["unlocked"].get(); + } + if (entry.contains("extra")) { + e.extra = entry["extra"]; + } + } + } catch (const std::exception&) {} +} + +void AchievementSystem::save() { + json j = json::object(); + for (const auto& e : m_entries) { + json entry = { + {"progress", e.achievement.progress}, + {"unlocked", e.achievement.unlocked}, + }; + if (!e.extra.is_null()) { + entry["extra"] = e.extra; + } + j[e.achievement.key] = std::move(entry); + } + try { + io::FileStream::WriteAllText( + dusk::ConfigPath / ACHIEVEMENTS_FILENAME, + j.dump(2) + ); + } catch (const std::exception&) {} +} + +void AchievementSystem::clearAll() { + m_entries = makeEntries(); + save(); +} + +void AchievementSystem::signal(const char* key) { + m_signals.insert(key); +} + +bool AchievementSystem::hasSignal(const char* key) const { + return m_signals.count(key) > 0; +} + +void AchievementSystem::clearOne(const char* key) { + for (auto& e : m_entries) { + if (std::string(e.achievement.key) == key) { + e.achievement.progress = 0; + e.achievement.unlocked = false; + e.extra = {}; + break; + } + } + save(); +} + +void AchievementSystem::processEntry(Entry& e) { + if (e.achievement.unlocked) { + return; + } + const int32_t prevProgress = e.achievement.progress; + e.check(e.achievement, e.extra); + + const bool progressChanged = e.achievement.progress != prevProgress; + const bool nowUnlocked = e.achievement.isCounter ? + e.achievement.progress >= e.achievement.goal : + e.achievement.progress > 0; + + if (nowUnlocked) { + e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1; + e.achievement.unlocked = true; + if (getSettings().game.enableAchievementToasts) { + ui::push_toast({ + .type = "achievement", + .title = "Achievement Unlocked!", + .content = e.achievement.name, + .duration = std::chrono::seconds(5), + }); + } + m_dirty = true; + } else if (progressChanged) { + m_dirty = true; + } +} + +void AchievementSystem::tick() { + if (!m_loaded) { + load(); + } + if (!dusk::IsGameLaunched) { + return; + } + for (auto& e : m_entries) { + processEntry(e); + } + m_signals.clear(); + if (m_dirty) { + save(); + m_dirty = false; + } +} + +} // namespace dusk diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index f576a5d0f1..697371702f 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -48,6 +48,20 @@ f32 dusk::audio::MasterVolume = 1.0f; f32 dusk::audio::PrevMasterVolume = 1.0f; bool dusk::audio::EnableReverb = true; bool dusk::audio::DumpAudio = false; +bool dusk::audio::EnableHrtf = false; +f32 dusk::audio::HrtfGain = 0.5f; + + +// 3dB at 5kHz. +static constexpr f32 HRTF_LP_K = 0.75f; +static constexpr f32 HRTF_ALLPASS_G = 0.3f; +// Front never drops below (1 - HRTF_EXTRACT_MAX). +static constexpr f32 HRTF_EXTRACT_MAX = 0.6f; + +static f32 sHrtfLp1 = 0.0f; +static f32 sHrtfLp2 = 0.0f; +static f32 sHrtfApIn1 = 0.0f; +static f32 sHrtfApOut1 = 0.0f; /** * Validate that a DSP channel's format is actually something we know how to play. @@ -283,6 +297,9 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { DspSubframe reverbInputR = {}; bool anyReverbInput = false; + DspSubframe surroundBus = {}; + bool anySurroundInput = false; + for (int i = 0; i < channels.size(); i++) { auto& channel = channels[i]; auto& channelAux = ChannelAux[i]; @@ -324,6 +341,21 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { } } + if (EnableHrtf && channel.mAutoMixerBeenSet) { + f32 dolby = (channel.mAutoMixerPanDolby & 0xFF) / 127.0f; + if (dolby > 0.0f) { + anySurroundInput = true; + f32 extract = dolby * HRTF_EXTRACT_MAX; + f32 frontScale = 1.0f - extract; + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + f32 mono = (channelSubframe.channels[0][j] + channelSubframe.channels[1][j]) * 0.5f; + surroundBus[j] += mono * extract; + channelSubframe.channels[0][j] *= frontScale; + channelSubframe.channels[1][j] *= frontScale; + } + } + } + if (DumpAudio && sChannelDumpFiles[i]) { f32 interleaved[DSP_SUBFRAME_SIZE * 2]; for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { @@ -349,6 +381,28 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { ReverbHasTail = wetEnergy >= REVERB_ENERGY_EPSILON; } + if (EnableHrtf && anySurroundInput) { + // Two-pole LPF: -12 dB/oct above 3 kHz + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + sHrtfLp1 = (1.0f - HRTF_LP_K) * sHrtfLp1 + HRTF_LP_K * surroundBus[j]; + sHrtfLp2 = (1.0f - HRTF_LP_K) * sHrtfLp2 + HRTF_LP_K * sHrtfLp1; + surroundBus[j] = sHrtfLp2; + } + + // Mix into L and R + // L gets the filtered signal directly; R gets it allpass for mild decorrelation + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + f32 s = surroundBus[j]; + + subframe.channels[0][j] += s * HrtfGain; + + f32 r = -HRTF_ALLPASS_G * s + sHrtfApIn1 + HRTF_ALLPASS_G * sHrtfApOut1; + sHrtfApIn1 = s; + sHrtfApOut1 = r; + subframe.channels[1][j] += r * HrtfGain; + } + } + for (auto& channel : subframe.channels) { ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume); } diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index 8000e627a1..cfcbfe3f46 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -133,4 +133,6 @@ namespace dusk::audio { extern f32 PrevMasterVolume; extern bool EnableReverb; extern bool DumpAudio; + extern bool EnableHrtf; + extern f32 HrtfGain; } diff --git a/src/dusk/autosave.cpp b/src/dusk/autosave.cpp new file mode 100644 index 0000000000..779cb19915 --- /dev/null +++ b/src/dusk/autosave.cpp @@ -0,0 +1,92 @@ +#include "dusk/autosave.h" +#include "dusk/ui/ui.hpp" +#include "imgui/ImGuiConsole.hpp" + +u8 mSaveBuffer[QUEST_LOG_SIZE * 3]; +u8 mAutoSaveProc = 0; +int autoSaveWriteState = 0; + +typedef void (*AutoSaveFuncs)(); +static AutoSaveFuncs AutoSaveFuncsProc[] = { + noAutoSave, enterAutoSave, autoSaving, waitingForWrite, endAutoSave, +}; + +void noAutoSave() {} + +void triggerAutoSave() { + if (dusk::getSettings().game.autoSave && mAutoSaveProc == 0 && + strcmp(dComIfGp_getStartStageName(), "F_SP102") != 0) + { + mAutoSaveProc = 1; + } +} + +void updateAutoSave() { + (AutoSaveFuncsProc[mAutoSaveProc])(); +} + +void writeAutoSave() { + int stageNo = dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo()); + + dComIfGs_putSave(stageNo); + dComIfGs_setMemoryToCard(mSaveBuffer, dComIfGs_getDataNum()); + mDoMemCdRWm_SetCheckSumGameData(mSaveBuffer, dComIfGs_getDataNum()); + + u8* save = mSaveBuffer; + for (int i = 0; i < 3; i++) { + mDoMemCdRWm_TestCheckSumGameData(save); + save += QUEST_LOG_SIZE; + } + + g_mDoMemCd_control.save(mSaveBuffer, sizeof(mSaveBuffer), 0); +} + +void autoSaving() { + int cardState = g_mDoMemCd_control.LoadSync(mSaveBuffer, sizeof(mSaveBuffer), 0); + if (cardState != 0) { + if (cardState == 2) { + mAutoSaveProc = 1; + } else if (cardState == 1) { + writeAutoSave(); + mAutoSaveProc = 3; + } + } +} + +void enterAutoSave() { + u32 cardStatus = g_mDoMemCd_control.getStatus(0); + + if (cardStatus != 14) { + switch (cardStatus) { + case 2: + g_mDoMemCd_control.load(); + mAutoSaveProc = 2; + break; + case 3: + case 4: + case 5: + break; + default: + mAutoSaveProc = 0; + break; + } + } +} + +void waitingForWrite() { + autoSaveWriteState = g_mDoMemCd_control.SaveSync(); + + if (autoSaveWriteState == 2) { + mAutoSaveProc = 0; + } else if (autoSaveWriteState == 1) { + mAutoSaveProc = 4; + } +} + +void endAutoSave() { + dusk::ui::push_toast({ + .type = "autosave", + .duration = std::chrono::milliseconds(1500), + }); + mAutoSaveProc = 0; +} \ No newline at end of file diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 176967359d..8225536d34 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -23,8 +23,8 @@ aurora::Module DuskConfigLog("dusk::config"); static absl::flat_hash_map RegisteredConfigVars; static bool RegistrationDone = false; -static std::string GetConfigJsonPath() { - return (dusk::ConfigPath / ConfigFileName).string(); +static std::u8string GetConfigJsonPath() { + return (dusk::ConfigPath / ConfigFileName).u8string(); } ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) { @@ -154,6 +154,9 @@ namespace dusk::config { template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; + template class ConfigImpl; + template class ConfigImpl; + template class ConfigImpl; } void dusk::config::Register(ConfigVarBase& configVar) { @@ -186,7 +189,7 @@ void dusk::config::LoadFromUserPreferences() { if (configJsonPath.empty()) { return; } - LoadFromFileName(configJsonPath.c_str()); + LoadFromFileName(reinterpret_cast(configJsonPath.c_str())); } static void LoadFromPath(const char* path) { @@ -238,7 +241,9 @@ void dusk::config::Save() { return; } - DuskConfigLog.info("Saving config to '{}'", configJsonPath); + DuskConfigLog.info( + "Saving config to '{}'", + reinterpret_cast(configJsonPath.c_str())); json j; @@ -248,7 +253,7 @@ void dusk::config::Save() { } } - io::FileStream::WriteAllText(configJsonPath.c_str(), j.dump(4)); + io::FileStream::WriteAllText(reinterpret_cast(configJsonPath.c_str()), j.dump(4)); } ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) { diff --git a/src/dusk/crash_reporting.cpp b/src/dusk/crash_reporting.cpp index 0499f0d6a1..657e5ef5d2 100644 --- a/src/dusk/crash_reporting.cpp +++ b/src/dusk/crash_reporting.cpp @@ -167,7 +167,6 @@ void InitializeCrashReporting() { sentry_set_tag("git_branch", DUSK_WC_BRANCH); sentry_set_tag("build_type", DUSK_BUILD_TYPE); - sentry_set_tag("tp_version", DUSK_TP_VERSION); g_sentryInitialized = true; DuskLog.info("Initialized Sentry crash reporting"); diff --git a/src/dusk/discord.cpp b/src/dusk/discord.cpp new file mode 100644 index 0000000000..f20541bfa4 --- /dev/null +++ b/src/dusk/discord.cpp @@ -0,0 +1,867 @@ +#ifdef DUSK_DISCORD + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "discord.hpp" + +#include "dusk/logging.h" +#include "nlohmann/json.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMCX +#define NOSERVICE +#define NOIME +#include +#else +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace dusk::discord::rpc { +namespace { + +using json = nlohmann::json; + +constexpr uint32_t kRpcVersion = 1; +constexpr size_t kFrameHeaderSize = sizeof(uint32_t) * 2; +constexpr size_t kMaxFramePayloadSize = 64 * 1024; +constexpr auto kIoWait = std::chrono::milliseconds(500); +constexpr auto kShutdownClearTimeout = std::chrono::milliseconds(200); +constexpr auto kInitialReconnectDelay = std::chrono::milliseconds(500); +constexpr auto kMaxReconnectDelay = std::chrono::milliseconds(60 * 1000); + +enum class Opcode : uint32_t { + Handshake = 0, + Frame = 1, + Close = 2, + Ping = 3, + Pong = 4, +}; + +enum class ConnectionState { + Disconnected, + SentHandshake, + Connected, +}; + +enum class DisconnectCode : int { + PipeClosed = 1, + ReadCorrupt = 2, + BadFrame = 3, +}; + +struct Frame { + Opcode opcode = Opcode::Frame; + std::string payload; +}; + +struct QueuedEvent { + enum class Type { + Ready, + Disconnected, + Error, + }; + + Type type = Type::Ready; + User user; + int code = 0; + std::string message; +}; + +class Backoff { +public: + std::chrono::milliseconds next_delay() { + const auto delay = currentDelay; + currentDelay = std::min(currentDelay * 2, kMaxReconnectDelay); + return delay; + } + + void reset() { currentDelay = kInitialReconnectDelay; } + +private: + std::chrono::milliseconds currentDelay = kInitialReconnectDelay; +}; + +class IpcConnection { +public: + IpcConnection() = default; + ~IpcConnection() { close(); } + + IpcConnection(const IpcConnection&) = delete; + IpcConnection& operator=(const IpcConnection&) = delete; + + bool open() { +#ifdef _WIN32 + wchar_t pipeName[] = L"\\\\?\\pipe\\discord-ipc-0"; + constexpr size_t kPipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2; + + for (wchar_t pipeNumber = L'0'; pipeNumber <= L'9'; ++pipeNumber) { + pipeName[kPipeDigit] = pipeNumber; + pipe = CreateFileW( + pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); + if (pipe != INVALID_HANDLE_VALUE) { + return true; + } + + if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(pipeName, 10000)) { + --pipeNumber; + } + } + return false; +#else + const auto tempPaths = get_temp_paths(); + for (const std::string& tempPath : tempPaths) { + for (int pipeNumber = 0; pipeNumber < 10; ++pipeNumber) { + socketFd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socketFd == -1) { + return false; + } + + sockaddr_un pipeAddress{}; + pipeAddress.sun_family = AF_UNIX; + const std::string socketPath = + tempPath + "/discord-ipc-" + std::to_string(pipeNumber); + if (socketPath.size() >= sizeof(pipeAddress.sun_path)) { + close(); + continue; + } + + std::strncpy( + pipeAddress.sun_path, socketPath.c_str(), sizeof(pipeAddress.sun_path) - 1); + if (connect(socketFd, reinterpret_cast(&pipeAddress), + sizeof(pipeAddress)) == 0) + { + fcntl(socketFd, F_SETFL, O_NONBLOCK); +#ifdef SO_NOSIGPIPE + int optval = 1; + setsockopt(socketFd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval)); +#endif + return true; + } + close(); + } + } + return false; +#endif + } + + void close() { +#ifdef _WIN32 + if (pipe != INVALID_HANDLE_VALUE) { + CloseHandle(pipe); + pipe = INVALID_HANDLE_VALUE; + } +#else + if (socketFd != -1) { + ::close(socketFd); + socketFd = -1; + } +#endif + pendingRead.clear(); + } + + bool is_open() const { +#ifdef _WIN32 + return pipe != INVALID_HANDLE_VALUE; +#else + return socketFd != -1; +#endif + } + + bool write_frame(const Frame& frame) { + std::array header{}; + write_u32(header.data(), static_cast(frame.opcode)); + write_u32(header.data() + sizeof(uint32_t), static_cast(frame.payload.size())); + + return write_all(header.data(), header.size()) && + write_all( + reinterpret_cast(frame.payload.data()), frame.payload.size()); + } + + enum class ReadStatus { + None, + Frame, + Closed, + Corrupt, + }; + + ReadStatus read_frame(Frame& frame) { + if (!read_available()) { + return is_open() ? ReadStatus::None : ReadStatus::Closed; + } + + if (pendingRead.size() < kFrameHeaderSize) { + return ReadStatus::None; + } + + const uint32_t payloadLength = read_u32(pendingRead.data() + sizeof(uint32_t)); + if (payloadLength > kMaxFramePayloadSize) { + return ReadStatus::Corrupt; + } + + const size_t frameLength = kFrameHeaderSize + payloadLength; + if (pendingRead.size() < frameLength) { + return ReadStatus::None; + } + + frame.opcode = static_cast(read_u32(pendingRead.data())); + frame.payload.assign( + reinterpret_cast(pendingRead.data() + kFrameHeaderSize), payloadLength); + pendingRead.erase( + pendingRead.begin(), pendingRead.begin() + static_cast(frameLength)); + return ReadStatus::Frame; + } + +private: +#ifndef _WIN32 + static std::vector get_temp_paths() { + std::vector paths; + for (const char* name : {"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"}) { + if (const char* value = std::getenv(name); value && value[0] != '\0') { + if (std::find(paths.begin(), paths.end(), value) == paths.end()) { + paths.emplace_back(value); + } + } + } + if (std::find(paths.begin(), paths.end(), "/tmp") == paths.end()) { + paths.emplace_back("/tmp"); + } + return paths; + } +#endif + + static void write_u32(uint8_t* out, uint32_t value) { + out[0] = static_cast(value & 0xff); + out[1] = static_cast((value >> 8) & 0xff); + out[2] = static_cast((value >> 16) & 0xff); + out[3] = static_cast((value >> 24) & 0xff); + } + + static uint32_t read_u32(const uint8_t* in) { + return static_cast(in[0]) | (static_cast(in[1]) << 8) | + (static_cast(in[2]) << 16) | (static_cast(in[3]) << 24); + } + + bool write_all(const uint8_t* data, size_t length) { + size_t written = 0; + while (written < length) { +#ifdef _WIN32 + DWORD bytesWritten = 0; + if (WriteFile(pipe, data + written, static_cast(length - written), &bytesWritten, + nullptr) == FALSE || + bytesWritten == 0) + { + close(); + return false; + } + written += bytesWritten; +#else +#ifdef MSG_NOSIGNAL + constexpr int kMsgFlags = MSG_NOSIGNAL; +#else + constexpr int kMsgFlags = 0; +#endif + const ssize_t bytesWritten = + send(socketFd, data + written, length - written, kMsgFlags); + if (bytesWritten < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + close(); + return false; + } + if (bytesWritten == 0) { + close(); + return false; + } + written += static_cast(bytesWritten); +#endif + } + return true; + } + + bool read_available() { + std::array buffer{}; + bool readAny = false; + + for (;;) { +#ifdef _WIN32 + DWORD bytesAvailable = 0; + if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr) == FALSE) { + close(); + return readAny; + } + if (bytesAvailable == 0) { + return readAny; + } + + const DWORD bytesToRead = + std::min(bytesAvailable, static_cast(buffer.size())); + DWORD bytesRead = 0; + if (ReadFile(pipe, buffer.data(), bytesToRead, &bytesRead, nullptr) == FALSE) { + close(); + return readAny; + } + if (bytesRead == 0) { + close(); + return readAny; + } + pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead); + readAny = true; +#else +#ifdef MSG_NOSIGNAL + constexpr int kMsgFlags = MSG_NOSIGNAL; +#else + constexpr int kMsgFlags = 0; +#endif + const ssize_t bytesRead = recv(socketFd, buffer.data(), buffer.size(), kMsgFlags); + if (bytesRead < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return readAny; + } + close(); + return readAny; + } + if (bytesRead == 0) { + close(); + return readAny; + } + pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead); + readAny = true; +#endif + } + } + +#ifdef _WIN32 + HANDLE pipe = INVALID_HANDLE_VALUE; +#else + int socketFd = -1; +#endif + std::vector pendingRead; +}; + +int current_process_id() { +#ifdef _WIN32 + return static_cast(GetCurrentProcessId()); +#else + return static_cast(getpid()); +#endif +} + +std::string next_nonce() { + static std::atomic_uint64_t sNonce{1}; + return std::to_string(sNonce.fetch_add(1)); +} + +const json* find_member(const json& object, const char* key) { + if (!object.is_object()) { + return nullptr; + } + + const auto member = object.find(key); + if (member == object.end()) { + return nullptr; + } + return &*member; +} + +std::string json_string_member(const json& object, const char* key) { + const json* member = find_member(object, key); + if (!member || !member->is_string()) { + return {}; + } + return member->get(); +} + +int json_int_member(const json& object, const char* key, int defaultValue) { + const json* member = find_member(object, key); + if (!member || !member->is_number_integer()) { + return defaultValue; + } + + try { + return member->get(); + } catch (const json::exception&) { + return defaultValue; + } +} + +json make_presence_activity(const Presence& presence) { + json activity = json::object(); + + if (!presence.state.empty()) { + activity["state"] = presence.state; + } + if (!presence.details.empty()) { + activity["details"] = presence.details; + } + if (presence.startTimestamp != 0) { + activity["timestamps"] = {{"start", presence.startTimestamp}}; + } + if (!presence.largeImageKey.empty() || !presence.largeImageText.empty()) { + json assets = json::object(); + if (!presence.largeImageKey.empty()) { + assets["large_image"] = presence.largeImageKey; + } + if (!presence.largeImageText.empty()) { + assets["large_text"] = presence.largeImageText; + } + activity["assets"] = std::move(assets); + } + + return activity; +} + +Frame make_handshake_frame(std::string_view applicationId) { + return { + Opcode::Handshake, + json{ + {"v", kRpcVersion}, + {"client_id", std::string(applicationId)}, + } + .dump(), + }; +} + +Frame make_set_activity_frame(std::string nonce, int pid, std::optional presence) { + json args = {{"pid", pid}}; + if (presence) { + args["activity"] = make_presence_activity(*presence); + } else { + args["activity"] = nullptr; + } + + return { + Opcode::Frame, + json{ + {"cmd", "SET_ACTIVITY"}, + {"nonce", std::move(nonce)}, + {"args", std::move(args)}, + } + .dump(), + }; +} + +class Client { +public: + void initialize(std::string applicationId, EventHandlers handlers) { + shutdown(); + + { + std::lock_guard lock(mutex); + this->applicationId = std::move(applicationId); + this->handlers = std::move(handlers); + shouldRun = true; + queuedPresence.reset(); + hasQueuedPresence = false; + clearRequested = false; + sentInitialConnectLog = false; + } + + ioThread = std::thread([this] { io_loop(); }); + } + + void run_callbacks() { + std::deque events; + EventHandlers localHandlers; + { + std::lock_guard lock(mutex); + events.swap(queuedEvents); + localHandlers = handlers; + } + + for (const QueuedEvent& event : events) { + switch (event.type) { + case QueuedEvent::Type::Ready: + if (localHandlers.ready) { + localHandlers.ready(event.user); + } + break; + case QueuedEvent::Type::Disconnected: + if (localHandlers.disconnected) { + localHandlers.disconnected(event.code, event.message); + } + break; + case QueuedEvent::Type::Error: + if (localHandlers.error) { + localHandlers.error(event.code, event.message); + } + break; + } + } + } + + void update_presence(Presence presence) { + { + std::lock_guard lock(mutex); + if (!shouldRun) { + return; + } + queuedPresence = std::move(presence); + hasQueuedPresence = true; + } + cv.notify_all(); + } + + void clear_presence() { + { + std::lock_guard lock(mutex); + if (!shouldRun) { + return; + } + queuedPresence.reset(); + hasQueuedPresence = false; + clearRequested = true; + } + cv.notify_all(); + } + + void shutdown() { + { + std::lock_guard lock(mutex); + if (!shouldRun && !ioThread.joinable()) { + return; + } + shouldRun = false; + clearRequested = true; + } + cv.notify_all(); + + if (ioThread.joinable()) { + ioThread.join(); + } + + std::lock_guard lock(mutex); + queuedPresence.reset(); + hasQueuedPresence = false; + clearRequested = false; + queuedEvents.clear(); + handlers = {}; + applicationId.clear(); + } + +private: + void io_loop() { + IpcConnection connection; + ConnectionState state = ConnectionState::Disconnected; + Backoff reconnectBackoff; + auto nextConnect = std::chrono::steady_clock::now(); + const int pid = current_process_id(); + std::string localApplicationId; + + for (;;) { + { + std::unique_lock lock(mutex); + if (!shouldRun) { + break; + } + localApplicationId = applicationId; + } + + const auto now = std::chrono::steady_clock::now(); + if (state == ConnectionState::Disconnected && now >= nextConnect) { + if (connection.open()) { + if (connection.write_frame(make_handshake_frame(localApplicationId))) { + state = ConnectionState::SentHandshake; + } else { + connection.close(); + } + } + + if (state == ConnectionState::Disconnected) { + log_waiting_for_discord_once(); + nextConnect = now + reconnectBackoff.next_delay(); + } + } + + if (state != ConnectionState::Disconnected) { + process_reads(connection, state, reconnectBackoff, nextConnect); + } + + if (state == ConnectionState::Connected) { + flush_pending_presence(connection, pid); + } + + std::unique_lock lock(mutex); + if (!shouldRun) { + break; + } + cv.wait_for(lock, kIoWait); + } + + flush_shutdown_clear(connection, state, pid); + connection.close(); + } + + void process_reads(IpcConnection& connection, ConnectionState& state, Backoff& reconnectBackoff, + std::chrono::steady_clock::time_point& nextConnect) { + for (;;) { + Frame frame; + const auto status = connection.read_frame(frame); + if (status == IpcConnection::ReadStatus::None) { + return; + } + if (status == IpcConnection::ReadStatus::Closed) { + handle_disconnect(connection, state, reconnectBackoff, nextConnect, + static_cast(DisconnectCode::PipeClosed), "Pipe closed"); + return; + } + if (status == IpcConnection::ReadStatus::Corrupt) { + handle_disconnect(connection, state, reconnectBackoff, nextConnect, + static_cast(DisconnectCode::ReadCorrupt), "Oversized Discord IPC frame"); + return; + } + + switch (frame.opcode) { + case Opcode::Frame: + process_json_frame(frame.payload, state, reconnectBackoff); + break; + case Opcode::Close: + process_close_frame( + frame.payload, connection, state, reconnectBackoff, nextConnect); + return; + case Opcode::Ping: + connection.write_frame({Opcode::Pong, frame.payload}); + break; + case Opcode::Pong: + break; + case Opcode::Handshake: + default: + handle_disconnect(connection, state, reconnectBackoff, nextConnect, + static_cast(DisconnectCode::BadFrame), "Bad Discord IPC frame"); + return; + } + } + } + + void process_json_frame( + const std::string& payload, ConnectionState& state, Backoff& reconnectBackoff) { + json message; + try { + message = json::parse(payload); + } catch (const json::parse_error&) { + enqueue_error( + static_cast(DisconnectCode::ReadCorrupt), "Invalid Discord IPC JSON"); + return; + } + + const std::string cmd = json_string_member(message, "cmd"); + const std::string evt = json_string_member(message, "evt"); + + if (state == ConnectionState::SentHandshake && cmd == "DISPATCH" && evt == "READY") { + state = ConnectionState::Connected; + reconnectBackoff.reset(); + enqueue_ready(message); + return; + } + + if (evt == "ERROR") { + const json* data = find_member(message, "data"); + enqueue_error(data ? json_int_member(*data, "code", 0) : 0, + data ? json_string_member(*data, "message") : std::string{}); + } + } + + void process_close_frame(const std::string& payload, IpcConnection& connection, + ConnectionState& state, Backoff& reconnectBackoff, + std::chrono::steady_clock::time_point& nextConnect) { + int code = static_cast(DisconnectCode::PipeClosed); + std::string message = "Discord closed IPC connection"; + + try { + const json closePayload = json::parse(payload); + code = json_int_member(closePayload, "code", code); + const std::string closeMessage = json_string_member(closePayload, "message"); + if (!closeMessage.empty()) { + message = closeMessage; + } + } catch (const json::exception&) { + } + + handle_disconnect(connection, state, reconnectBackoff, nextConnect, code, message); + } + + void handle_disconnect(IpcConnection& connection, ConnectionState& state, + Backoff& reconnectBackoff, std::chrono::steady_clock::time_point& nextConnect, int code, + std::string_view message) { + const bool wasConnected = + state == ConnectionState::Connected || state == ConnectionState::SentHandshake; + connection.close(); + state = ConnectionState::Disconnected; + nextConnect = std::chrono::steady_clock::now() + reconnectBackoff.next_delay(); + if (wasConnected) { + enqueue_disconnected(code, message); + } + } + + void flush_pending_presence(IpcConnection& connection, int pid) { + std::optional presence; + bool shouldClear = false; + { + std::lock_guard lock(mutex); + if (hasQueuedPresence) { + presence = queuedPresence; + hasQueuedPresence = false; + } else if (clearRequested) { + shouldClear = true; + clearRequested = false; + } + } + + if (presence) { + if (!connection.write_frame( + make_set_activity_frame(next_nonce(), pid, std::move(presence)))) + { + requeue_presence(); + } + } else if (shouldClear) { + connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt)); + } + } + + void flush_shutdown_clear(IpcConnection& connection, ConnectionState state, int pid) { + if (state != ConnectionState::Connected || !connection.is_open()) { + return; + } + + connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt)); + const auto deadline = std::chrono::steady_clock::now() + kShutdownClearTimeout; + while (std::chrono::steady_clock::now() < deadline) { + Frame frame; + const auto status = connection.read_frame(frame); + if (status == IpcConnection::ReadStatus::None) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + if (status != IpcConnection::ReadStatus::Frame) { + break; + } + if (frame.opcode == Opcode::Ping) { + connection.write_frame({Opcode::Pong, frame.payload}); + } + } + } + + void requeue_presence() { + std::lock_guard lock(mutex); + if (queuedPresence) { + hasQueuedPresence = true; + } + } + + void enqueue_ready(const json& readyMessage) { + User user; + const auto data = readyMessage.find("data"); + if (data != readyMessage.end() && data->is_object()) { + const auto userIt = data->find("user"); + if (userIt != data->end() && userIt->is_object()) { + user.id = json_string_member(*userIt, "id"); + user.username = json_string_member(*userIt, "username"); + user.discriminator = json_string_member(*userIt, "discriminator"); + user.avatar = json_string_member(*userIt, "avatar"); + } + } + + std::lock_guard lock(mutex); + queuedEvents.push_back({QueuedEvent::Type::Ready, std::move(user)}); + } + + void enqueue_disconnected(int code, std::string_view message) { + std::lock_guard lock(mutex); + QueuedEvent event; + event.type = QueuedEvent::Type::Disconnected; + event.code = code; + event.message = message; + queuedEvents.push_back(std::move(event)); + } + + void enqueue_error(int code, std::string_view message) { + std::lock_guard lock(mutex); + QueuedEvent event; + event.type = QueuedEvent::Type::Error; + event.code = code; + event.message = message; + queuedEvents.push_back(std::move(event)); + } + + void log_waiting_for_discord_once() { + bool shouldLog = false; + { + std::lock_guard lock(mutex); + if (!sentInitialConnectLog) { + sentInitialConnectLog = true; + shouldLog = true; + } + } + if (shouldLog) { + DuskLog.info("Discord: Waiting for local Discord IPC"); + } + } + + std::mutex mutex; + std::condition_variable cv; + std::thread ioThread; + std::string applicationId; + EventHandlers handlers; + std::deque queuedEvents; + std::optional queuedPresence; + bool hasQueuedPresence = false; + bool clearRequested = false; + bool shouldRun = false; + bool sentInitialConnectLog = false; +}; + +Client& client() { + static Client sClient; + return sClient; +} + +} // namespace + +void initialize(std::string applicationId, EventHandlers handlers) { + client().initialize(std::move(applicationId), std::move(handlers)); +} + +void run_callbacks() { + client().run_callbacks(); +} + +void update_presence(Presence presence) { + client().update_presence(std::move(presence)); +} + +void clear_presence() { + client().clear_presence(); +} + +void shutdown() { + client().shutdown(); +} + +} // namespace dusk::discord::rpc + +#endif // DUSK_DISCORD diff --git a/src/dusk/discord.hpp b/src/dusk/discord.hpp new file mode 100644 index 0000000000..67f54ff2f3 --- /dev/null +++ b/src/dusk/discord.hpp @@ -0,0 +1,41 @@ +#pragma once + +#ifdef DUSK_DISCORD + +#include +#include +#include +#include + +namespace dusk::discord::rpc { + +struct User { + std::string id; + std::string username; + std::string discriminator; + std::string avatar; +}; + +struct Presence { + std::string state; + std::string details; + int64_t startTimestamp = 0; + std::string largeImageKey; + std::string largeImageText; +}; + +struct EventHandlers { + std::function ready; + std::function disconnected; + std::function error; +}; + +void initialize(std::string applicationId, EventHandlers handlers); +void run_callbacks(); +void update_presence(Presence presence); +void clear_presence(); +void shutdown(); + +} // namespace dusk::discord::rpc + +#endif // DUSK_DISCORD diff --git a/src/dusk/discord_presence.cpp b/src/dusk/discord_presence.cpp index 3b12075c8b..6b2c7c912b 100644 --- a/src/dusk/discord_presence.cpp +++ b/src/dusk/discord_presence.cpp @@ -1,38 +1,39 @@ -#ifdef DUSK_DISCORD_RPC +#ifdef DUSK_DISCORD #include "dusk/discord_presence.hpp" +#include "d/d_com_inf_game.h" +#include "discord.hpp" #include "dusk/logging.h" #include "dusk/main.h" #include "dusk/map_loader_definitions.h" -#include "d/d_com_inf_game.h" -#include "discord_rpc.h" #include "fmt/format.h" #include -#include #include +#include +#include -namespace dusk { -namespace discord { +namespace dusk::discord { static int64_t g_startTime = 0; static bool g_initialized = false; -static const char* APPLICATION_ID = "1495632471994405035"; +static constexpr const char* kApplicationId = "1495632471994405035"; -static void OnReady(const DiscordUser* user) { - DuskLog.info("Discord: Connected as {}", user->username); +static void on_ready(const rpc::User& user) { + DuskLog.info("Discord: Connected as {}", user.username); } -static void OnDisconnected(int errorCode, const char* message) { +static void on_disconnected(int errorCode, std::string_view message) { DuskLog.warn("Discord: Disconnected ({}: {})", errorCode, message); } -static void OnError(int errorCode, const char* message) { +static void on_error(int errorCode, std::string_view message) { DuskLog.warn("Discord: Error ({}: {})", errorCode, message); } -static const char* LookupMapName(const char* mapFile) { - if (!mapFile || mapFile[0] == '\0') return nullptr; +static const char* lookup_map_name(const char* mapFile) { + if (!mapFile || mapFile[0] == '\0') + return nullptr; for (const auto& region : gameRegions) { for (const auto& map : region.maps) { if (map.mapFile && strcmp(mapFile, map.mapFile) == 0) { @@ -43,80 +44,80 @@ static const char* LookupMapName(const char* mapFile) { return nullptr; } -void Initialize() { - g_startTime = static_cast( - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch() - ).count() - ); +void initialize() { + g_startTime = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); - DiscordEventHandlers handlers{}; - handlers.ready = OnReady; - handlers.disconnected = OnDisconnected; - handlers.errored = OnError; - Discord_Initialize(APPLICATION_ID, &handlers, 0, nullptr); + rpc::EventHandlers handlers{}; + handlers.ready = on_ready; + handlers.disconnected = on_disconnected; + handlers.error = on_error; + rpc::initialize(kApplicationId, std::move(handlers)); g_initialized = true; DuskLog.info("Discord Rich Presence initialized"); } -void RunCallbacks() { - if (!g_initialized) return; - Discord_RunCallbacks(); +void run_callbacks() { + if (!g_initialized) + return; + rpc::run_callbacks(); } -void UpdatePresence() { - if (!g_initialized) return; +void update_presence() { + if (!g_initialized) + return; - static auto lastUpdate = std::chrono::steady_clock::time_point{}; + static auto sLastUpdate = std::chrono::steady_clock::time_point{}; const auto now = std::chrono::steady_clock::now(); - if (now - lastUpdate < std::chrono::seconds(15)) return; - lastUpdate = now; + if (now - sLastUpdate < std::chrono::seconds(15)) + return; + sLastUpdate = now; - static std::string detailsBuf; - static std::string stateBuf; + static std::string sDetailsBuf; + static std::string sStateBuf; - DiscordRichPresence presence{}; + rpc::Presence presence{}; presence.startTimestamp = g_startTime; presence.largeImageKey = "icon"; presence.largeImageText = "Dusk"; - if (dusk::IsGameLaunched) { + if (IsGameLaunched) { const char* stageName = dComIfGp_getLastPlayStageName(); // stageName is empty until a room is actually entered if (stageName[0] != '\0') { - const char* locationName = LookupMapName(stageName); + const char* locationName = lookup_map_name(stageName); if (locationName) { - detailsBuf = locationName; - } - else { - detailsBuf = "Twilight Princess"; + sDetailsBuf = locationName; + } else { + sDetailsBuf = "Twilight Princess"; } - presence.details = detailsBuf.c_str(); + presence.details = sDetailsBuf; - stateBuf = fmt::format(FMT_STRING("{}/{} \u2665 | {} Rupees"), + sStateBuf = fmt::format(FMT_STRING("{}/{} \u2665 | {} Rupees"), dComIfGs_getLife() / 4, dComIfGs_getMaxLife() / 5, dComIfGs_getRupee()); - presence.state = stateBuf.c_str(); + presence.state = sStateBuf; } } - Discord_UpdatePresence(&presence); + rpc::update_presence(std::move(presence)); DuskLog.debug("Discord Rich Presence sent"); } -void Shutdown() { - if (!g_initialized) return; - Discord_ClearPresence(); - Discord_Shutdown(); +void shutdown() { + if (!g_initialized) + return; + rpc::clear_presence(); + rpc::shutdown(); g_initialized = false; DuskLog.info("Discord Rich Presence shut down"); } -} // namespace discord -} // namespace dusk +} // namespace dusk::discord -#endif // DUSK_DISCORD_RPC +#endif // DUSK_DISCORD diff --git a/src/dusk/dvd_asset.cpp b/src/dusk/dvd_asset.cpp index b9e0729be9..75c82107cc 100644 --- a/src/dusk/dvd_asset.cpp +++ b/src/dusk/dvd_asset.cpp @@ -80,8 +80,19 @@ static s32 DolVaToFileOffset(u32 va) { return -1; } -bool LoadDolAsset(void* dst, u32 virtualAddress, s32 size) { - s32 fileOffset = DolVaToFileOffset(virtualAddress); +static u32 GetOffsetForVersion(std::initializer_list version) { + const auto gameVersion = dusk::version::getGameVersion(); + for (auto elem : version) { + if (elem.mGameVersion == gameVersion) { + return elem.mOffset; + } + } + + DuskLog.fatal("Unable to find offset for this game version!"); +} + +bool LoadDolAsset(void* dst, std::initializer_list virtualAddress, s32 size) { + s32 fileOffset = DolVaToFileOffset(GetOffsetForVersion(virtualAddress)); if (fileOffset < 0) { return false; } @@ -95,13 +106,16 @@ bool LoadDolAsset(void* dst, u32 virtualAddress, s32 size) { return true; } -bool LoadRelAsset(void* dst, const char* dvdPath, s32 offset, s32 size) { - void* p = JKRDvdRipper::loadToMainRAM(dvdPath, (u8*)dst, EXPAND_SWITCH_UNKNOWN1, (u32)size, nullptr, JKRDvdRipper::ALLOC_DIRECTION_FORWARD, (u32)offset, nullptr, nullptr); - if (!p) DuskLog.fatal("dvd_asset: failed to load {} (offset={:#x} size={:#x})", dvdPath, offset, size); +bool LoadRelAsset(void* dst, const char* dvdPath, std::initializer_list offset, s32 size) { + auto resOffset = GetOffsetForVersion(offset); + void* p = JKRDvdRipper::loadToMainRAM(dvdPath, (u8*)dst, EXPAND_SWITCH_UNKNOWN1, (u32)size, nullptr, JKRDvdRipper::ALLOC_DIRECTION_FORWARD, resOffset, nullptr, nullptr); + if (!p) DuskLog.fatal("dvd_asset: failed to load {} (offset={:#x} size={:#x})", dvdPath, resOffset, size); return p != nullptr; } -bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, s32 offset, s32 size) { +bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, std::initializer_list offset, s32 size) { + auto resOffset = GetOffsetForVersion(offset); + // On TARGET_PC, cDyl_InitCallback skips DynamicModuleControl::initialize() due to static linking // Mount RELS.arc on first use so sArchive is available static bool s_mountAttempted = false; @@ -118,7 +132,7 @@ bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, s32 o DuskLog.fatal("dvd_asset: {} not found in RELS archive", relFileName); return false; } - std::memcpy(dst, rel + offset, size); + std::memcpy(dst, rel + resOffset, size); return true; } diff --git a/src/dusk/file_select.cpp b/src/dusk/file_select.cpp index fcda233c56..188d491bf3 100644 --- a/src/dusk/file_select.cpp +++ b/src/dusk/file_select.cpp @@ -1,9 +1,16 @@ #include "file_select.hpp" #include +#include #include #include +#include + +#if defined(__ANDROID__) || defined(ANDROID) +#include +#include +#endif #if defined(__APPLE__) #include @@ -19,6 +26,92 @@ namespace dusk { namespace { +std::string fallback_display_name(std::string_view path) { + if (path.empty()) { + return {}; + } + + std::string pathString(path); + const std::size_t slash = pathString.find_last_of("/\\"); + if (slash == std::string::npos || slash + 1 >= pathString.size()) { + return pathString; + } + return pathString.substr(slash + 1); +} + +#if defined(__ANDROID__) || defined(ANDROID) +bool clear_pending_exception(JNIEnv* env) { + if (env == nullptr || !env->ExceptionCheck()) { + return false; + } + env->ExceptionClear(); + return true; +} + +std::string to_string(JNIEnv* env, jstring value) { + if (env == nullptr || value == nullptr) { + return {}; + } + + const char* utf8 = env->GetStringUTFChars(value, nullptr); + if (utf8 == nullptr) { + clear_pending_exception(env); + return {}; + } + + std::string result(utf8); + env->ReleaseStringUTFChars(value, utf8); + return result; +} + +std::string android_display_name(std::string_view path) { + auto* env = static_cast(SDL_GetAndroidJNIEnv()); + if (env == nullptr) { + return {}; + } + + jobject activity = static_cast(SDL_GetAndroidActivity()); + if (activity == nullptr || clear_pending_exception(env)) { + if (activity != nullptr) { + env->DeleteLocalRef(activity); + } + return {}; + } + + jclass activityClass = env->GetObjectClass(activity); + if (activityClass == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return {}; + } + + jmethodID getDisplayName = env->GetMethodID( + activityClass, "getDisplayNameForUri", "(Ljava/lang/String;)Ljava/lang/String;"); + env->DeleteLocalRef(activityClass); + if (getDisplayName == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return {}; + } + + jstring uri = env->NewStringUTF(std::string(path).c_str()); + if (uri == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return {}; + } + + auto* displayName = + static_cast(env->CallObjectMethod(activity, getDisplayName, uri)); + env->DeleteLocalRef(uri); + env->DeleteLocalRef(activity); + if (displayName == nullptr || clear_pending_exception(env)) { + return {}; + } + + std::string result = to_string(env, displayName); + env->DeleteLocalRef(displayName); + return result; +} +#endif + #if USE_IOS_DIALOG struct IOSDialogCallbackState { FileCallback callback; @@ -88,4 +181,16 @@ void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window, default_location, allow_many); #endif } + +std::string display_name_for_path(std::string_view path) { +#if defined(__ANDROID__) || defined(ANDROID) + if (path.starts_with("content:") || path.starts_with("file:")) { + std::string displayName = android_display_name(path); + if (!displayName.empty()) { + return displayName; + } + } +#endif + return fallback_display_name(path); +} } // namespace dusk diff --git a/src/dusk/file_select.hpp b/src/dusk/file_select.hpp index 175c5aa086..8bef51cfe6 100644 --- a/src/dusk/file_select.hpp +++ b/src/dusk/file_select.hpp @@ -2,6 +2,9 @@ #include +#include +#include + struct SDL_Window; namespace dusk { @@ -9,7 +12,9 @@ namespace dusk { using FileCallback = void (*)(void* userdata, const char* path, const char* error); void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window, - const SDL_DialogFileFilter* filters, int nfilters, const char* default_location, - bool allow_many); + const SDL_DialogFileFilter* filters, int nfilters, const char* default_location, + bool allow_many); + +std::string display_name_for_path(std::string_view path); } // namespace dusk diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index f3f8a842e1..941c9ed318 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -292,12 +292,20 @@ void interp_view(::view_class* view) { return; const f32 step = get_interpolation_step(); + const bool is_cam_curr_authoritative = g_is_sim_frame && step <= 0.0f; + cXyz eye; cXyz center; cXyz up; - lerp_xyz(&eye, s_cam_prev.eye, s_cam_curr.eye, step); - lerp_xyz(¢er, s_cam_prev.center, s_cam_curr.center, step); - lerp_xyz(&up, s_cam_prev.up, s_cam_curr.up, step); + if (is_cam_curr_authoritative) { + eye = s_cam_curr.eye; + center = s_cam_curr.center; + up = s_cam_curr.up; + } else { + lerp_xyz(&eye, s_cam_prev.eye, s_cam_curr.eye, step); + lerp_xyz(¢er, s_cam_prev.center, s_cam_curr.center, step); + lerp_xyz(&up, s_cam_prev.up, s_cam_curr.up, step); + } if (!up.normalizeRS()) { up = s_cam_curr.up; up.normalizeRS(); @@ -306,19 +314,25 @@ void interp_view(::view_class* view) { view->lookat.eye = eye; view->lookat.center = center; view->lookat.up = up; - view->bank = lerp_bank(s_cam_prev.bank, s_cam_curr.bank, step); - view->fovy = s_cam_prev.fovy + (s_cam_curr.fovy - s_cam_prev.fovy) * step; - view->aspect = s_cam_prev.aspect + (s_cam_curr.aspect - s_cam_prev.aspect) * step; - view->near_ = s_cam_prev.near_ + (s_cam_curr.near_ - s_cam_prev.near_) * step; - view->far_ = s_cam_prev.far_ + (s_cam_curr.far_ - s_cam_prev.far_) * step; + if (is_cam_curr_authoritative) { + view->bank = s_cam_curr.bank; + view->fovy = s_cam_curr.fovy; + view->aspect = s_cam_curr.aspect; + view->near_ = s_cam_curr.near_; + view->far_ = s_cam_curr.far_; + } else { + view->bank = lerp_bank(s_cam_prev.bank, s_cam_curr.bank, step); + view->fovy = s_cam_prev.fovy + (s_cam_curr.fovy - s_cam_prev.fovy) * step; + view->aspect = s_cam_prev.aspect + (s_cam_curr.aspect - s_cam_prev.aspect) * step; + view->near_ = s_cam_prev.near_ + (s_cam_curr.near_ - s_cam_prev.near_) * step; + view->far_ = s_cam_prev.far_ + (s_cam_curr.far_ - s_cam_prev.far_) * step; + } // FRAME INTERP TODO: It might be better if I rewired the game to not clear this flag until the // next sim frame, but I don't care enough to right now #if WIDESCREEN_SUPPORT - if (mDoGph_gInf_c::isWide() && !mDoGph_gInf_c::isWideZoom() && step >= 0.5f ? - s_cam_curr.wideZoom : - s_cam_prev.wideZoom) - { + const f32 wide_step = is_cam_curr_authoritative ? 1.0f : step; + if (mDoGph_gInf_c::isWide() && !mDoGph_gInf_c::isWideZoom() && wide_step >= 0.5f ? s_cam_curr.wideZoom : s_cam_prev.wideZoom) { mDoGph_gInf_c::onWideZoom(); } #endif diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index abe22909c1..680d500631 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -1,5 +1,9 @@ #include "dusk/gyro.h" +#include "dusk/ui/ui.hpp" #include "d/actor/d_a_alink.h" + +#include +#include #include namespace dusk::gyro { @@ -12,11 +16,14 @@ constexpr float kGravityEmaAlpha = 0.1f; constexpr float kMinGravityProjection = 0.2f; // Let roll contribute more strongly as the pad approaches an upright posture. constexpr float kRollAimBoostMax = 2.0f; +constexpr float kMousePixelToRad = 0.0025f; bool s_sensor_enabled = false; bool s_accel_enabled = false; bool s_was_aiming = false; bool s_have_gravity_baseline = false; +bool s_mouse_enabled = false; +bool s_mouse_relative = false; float s_smooth_gx = 0.0f; float s_smooth_gy = 0.0f; float s_smooth_gz = 0.0f; @@ -36,6 +43,7 @@ void reset_filter_state() { s_baseline_gravity_y = s_baseline_gravity_z = 0.0f; s_was_aiming = false; s_have_gravity_baseline = false; + s_mouse_enabled = false; s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f; s_rollgoal_ax = s_rollgoal_az = 0; } @@ -46,14 +54,29 @@ float apply_deadband(float v, float deadband_rad_s) { } return v; } + +void disable_pad_sensors() { + if (s_sensor_enabled) { + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE); + s_sensor_enabled = false; + } + if (s_accel_enabled) { + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE); + s_accel_enabled = false; + } +} } // namespace bool s_sensor_keep_alive = false; bool get_sensor_keep_alive() { return s_sensor_keep_alive; } void set_sensor_keep_alive(bool value) { s_sensor_keep_alive = value; } +bool rollgoal_gyro_enabled() { + return getSettings().game.enableGyroRollgoal && getSettings().game.gyroMode.getValue() != GyroMode::Mouse; +} + bool queryGyroAimContext() { - if (!static_cast(dusk::getSettings().game.enableGyroAim)) { + if (!static_cast(getSettings().game.enableGyroAim)) { return false; } @@ -71,15 +94,28 @@ void read(float dt) { const bool aim_just_ended = !aim_active && s_was_aiming; s_was_aiming = aim_active; + const bool mouse_mode = getSettings().game.gyroMode.getValue() == GyroMode::Mouse; + const bool mouse_gyro_active = !ui::any_document_visible() && mouse_mode && (aim_active || s_sensor_keep_alive); + SDL_Window* window = aurora::window::get_sdl_window(); + if (window != nullptr && mouse_gyro_active != s_mouse_relative && + SDL_SetWindowRelativeMouseMode(window, mouse_gyro_active)) + { + s_mouse_relative = mouse_gyro_active; + } + + if (mouse_gyro_active && !s_mouse_enabled && window != nullptr) { + const AuroraWindowSize sz = aurora::window::get_window_size(); + const float cx = static_cast(sz.width) * 0.5f; + const float cy = static_cast(sz.height) * 0.5f; + SDL_WarpMouseInWindow(window, cx, cy); + float discard_x = 0.0f; + float discard_y = 0.0f; + SDL_GetRelativeMouseState(&discard_x, &discard_y); + } + s_mouse_enabled = mouse_gyro_active; + if (!s_sensor_keep_alive && !aim_active) { - if (s_sensor_enabled) { - PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE); - s_sensor_enabled = false; - } - if (s_accel_enabled) { - PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE); - s_accel_enabled = false; - } + disable_pad_sensors(); reset_filter_state(); return; } @@ -90,6 +126,31 @@ void read(float dt) { s_have_gravity_baseline = false; } + if (mouse_mode && !mouse_gyro_active) { + s_pitch_rad = 0.0f; + s_yaw_rad = 0.0f; + s_roll_rad = 0.0f; + return; + } + + if (mouse_mode) { + disable_pad_sensors(); + + float mx_rel = 0.0f; + float my_rel = 0.0f; + SDL_GetRelativeMouseState(&mx_rel, &my_rel); + // Convert pixels to radians + s_pitch_rad = my_rel * kMousePixelToRad * getSettings().game.gyroSensitivityY; + s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX; + s_roll_rad = 0.0f; + + s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; + s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; + s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; + + return; + } + if (!s_sensor_enabled) { if (!PADHasSensor(PAD_CHAN0, PAD_SENSOR_GYRO)) { return; @@ -112,8 +173,8 @@ void read(float dt) { return; } - const float smooth_alpha = kGyroEmaAlphaMax + dusk::getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax); - const float deadband = dusk::getSettings().game.gyroDeadband; + const float smooth_alpha = kGyroEmaAlphaMax + getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax); + const float deadband = getSettings().game.gyroDeadband; s_smooth_gx += smooth_alpha * (gyro[0] - s_smooth_gx); s_smooth_gy += smooth_alpha * (gyro[1] - s_smooth_gy); @@ -123,8 +184,8 @@ void read(float dt) { const float yaw_rate = apply_deadband(s_smooth_gy, deadband); const float roll_rate = apply_deadband(s_smooth_gz, deadband); - s_pitch_rad = -pitch_rate * dt * dusk::getSettings().game.gyroSensitivityX; - s_roll_rad = roll_rate * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X + s_pitch_rad = -pitch_rate * dt * getSettings().game.gyroSensitivityY; + s_roll_rad = roll_rate * dt * getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X float horizontal_rate = yaw_rate; if (aim_active && s_accel_enabled) { @@ -162,11 +223,11 @@ void read(float dt) { } } - s_yaw_rad = horizontal_rate * dt * dusk::getSettings().game.gyroSensitivityY; + s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityX; - s_pitch_rad = dusk::getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; - s_yaw_rad = dusk::getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; - s_yaw_rad = dusk::getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; + s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; + s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; + s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; } void getAimDeltas(float& out_yaw, float& out_pitch) { @@ -180,9 +241,9 @@ void rollgoalTick(bool play_active, s16 camera_yaw) { return; } - float pitch_rad = -s_pitch_rad * dusk::getSettings().game.gyroSensitivityRollgoal; - float roll_rad = s_roll_rad * dusk::getSettings().game.gyroSensitivityRollgoal; - roll_rad = dusk::getSettings().game.enableMirrorMode ? -roll_rad : roll_rad; + float pitch_rad = -s_pitch_rad * getSettings().game.gyroSensitivityRollgoal; + float roll_rad = s_roll_rad * getSettings().game.gyroSensitivityRollgoal; + roll_rad = getSettings().game.enableMirrorMode ? -roll_rad : roll_rad; s_rollgoal_az += cM_rad2s(roll_rad); cXyz in(roll_rad, 0.0f, pitch_rad); diff --git a/src/dusk/http/android.cpp b/src/dusk/http/android.cpp new file mode 100644 index 0000000000..3f4eef8d84 --- /dev/null +++ b/src/dusk/http/android.cpp @@ -0,0 +1,402 @@ +#include "http.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace dusk::http { +namespace { + +constexpr int JavaErrorNone = 0; +constexpr int JavaErrorInvalidUrl = 1; +constexpr int JavaErrorUnsupportedScheme = 2; +constexpr int JavaErrorTimeout = 3; +constexpr int JavaErrorTooLarge = 4; + +int timeout_ms(std::chrono::milliseconds timeout) { + const auto count = std::max(1, timeout.count()); + return static_cast( + std::min(count, std::numeric_limits::max())); +} + +jlong max_body_bytes(size_t maxBodyBytes) { + return static_cast(std::min( + maxBodyBytes, static_cast(std::numeric_limits::max()))); +} + +bool clear_pending_exception(JNIEnv* env) { + if (env == nullptr || !env->ExceptionCheck()) { + return false; + } + env->ExceptionClear(); + return true; +} + +std::string to_string(JNIEnv* env, jstring value) { + if (env == nullptr || value == nullptr) { + return {}; + } + + const char* utf8 = env->GetStringUTFChars(value, nullptr); + if (utf8 == nullptr) { + clear_pending_exception(env); + return {}; + } + + std::string result(utf8); + env->ReleaseStringUTFChars(value, utf8); + return result; +} + +jstring to_jstring(JNIEnv* env, std::string_view value) { + if (env == nullptr) { + return nullptr; + } + return env->NewStringUTF(std::string(value).c_str()); +} + +Error map_java_error(int error) { + switch (error) { + case JavaErrorNone: + return Error::None; + case JavaErrorInvalidUrl: + return Error::InvalidUrl; + case JavaErrorUnsupportedScheme: + return Error::UnsupportedScheme; + case JavaErrorTimeout: + return Error::Timeout; + case JavaErrorTooLarge: + return Error::TooLarge; + default: + return Error::Network; + } +} + +jclass load_dusk_class(JNIEnv* env, jobject activity, const char* className) { + jclass activityClass = env->GetObjectClass(activity); + if (activityClass == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + jmethodID getClassLoader = + env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;"); + env->DeleteLocalRef(activityClass); + if (getClassLoader == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + jobject classLoader = env->CallObjectMethod(activity, getClassLoader); + if (classLoader == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + jclass classLoaderClass = env->FindClass("java/lang/ClassLoader"); + if (classLoaderClass == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(classLoader); + return nullptr; + } + + jmethodID loadClass = env->GetMethodID( + classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + env->DeleteLocalRef(classLoaderClass); + if (loadClass == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(classLoader); + return nullptr; + } + + jstring javaClassName = env->NewStringUTF(className); + if (javaClassName == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(classLoader); + return nullptr; + } + + auto* loadedClass = + static_cast(env->CallObjectMethod(classLoader, loadClass, javaClassName)); + env->DeleteLocalRef(javaClassName); + env->DeleteLocalRef(classLoader); + if (loadedClass == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + return loadedClass; +} + +jobjectArray make_string_array(JNIEnv* env, const std::vector
& headers, bool names) { + jclass stringClass = env->FindClass("java/lang/String"); + if (stringClass == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + jobjectArray array = + env->NewObjectArray(static_cast(headers.size()), stringClass, nullptr); + env->DeleteLocalRef(stringClass); + if (array == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + for (jsize i = 0; i < static_cast(headers.size()); ++i) { + const std::string& value = names ? headers[static_cast(i)].name : + headers[static_cast(i)].value; + jstring javaValue = to_jstring(env, value); + if (javaValue == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(array); + return nullptr; + } + env->SetObjectArrayElement(array, i, javaValue); + env->DeleteLocalRef(javaValue); + if (clear_pending_exception(env)) { + env->DeleteLocalRef(array); + return nullptr; + } + } + + return array; +} + +std::vector
read_headers(JNIEnv* env, jobjectArray names, jobjectArray values) { + std::vector
headers; + if (names == nullptr || values == nullptr) { + return headers; + } + + const jsize count = std::min(env->GetArrayLength(names), env->GetArrayLength(values)); + headers.reserve(static_cast(count)); + for (jsize i = 0; i < count; ++i) { + auto* name = static_cast(env->GetObjectArrayElement(names, i)); + auto* value = static_cast(env->GetObjectArrayElement(values, i)); + if (clear_pending_exception(env)) { + if (name != nullptr) { + env->DeleteLocalRef(name); + } + if (value != nullptr) { + env->DeleteLocalRef(value); + } + headers.clear(); + return headers; + } + + if (name != nullptr) { + headers.push_back({ + .name = to_string(env, name), + .value = to_string(env, value), + }); + } + + if (name != nullptr) { + env->DeleteLocalRef(name); + } + if (value != nullptr) { + env->DeleteLocalRef(value); + } + } + + return headers; +} + +std::string read_body(JNIEnv* env, jbyteArray body) { + if (body == nullptr) { + return {}; + } + + const jsize bodySize = env->GetArrayLength(body); + std::string result(static_cast(bodySize), '\0'); + if (bodySize > 0) { + env->GetByteArrayRegion(body, 0, bodySize, reinterpret_cast(result.data())); + if (clear_pending_exception(env)) { + return {}; + } + } + return result; +} + +Result result_from_response(JNIEnv* env, jobject response) { + if (response == nullptr) { + return { + .error = Error::Network, + .message = "Android HTTP request did not return a response", + }; + } + + jclass responseClass = env->GetObjectClass(response); + if (responseClass == nullptr || clear_pending_exception(env)) { + return { + .error = Error::Network, + .message = "Failed to inspect Android HTTP response", + }; + } + + jfieldID errorField = env->GetFieldID(responseClass, "error", "I"); + jfieldID messageField = env->GetFieldID(responseClass, "message", "Ljava/lang/String;"); + jfieldID statusField = env->GetFieldID(responseClass, "statusCode", "I"); + jfieldID headerNamesField = + env->GetFieldID(responseClass, "headerNames", "[Ljava/lang/String;"); + jfieldID headerValuesField = + env->GetFieldID(responseClass, "headerValues", "[Ljava/lang/String;"); + jfieldID bodyField = env->GetFieldID(responseClass, "body", "[B"); + env->DeleteLocalRef(responseClass); + if (errorField == nullptr || messageField == nullptr || statusField == nullptr || + headerNamesField == nullptr || headerValuesField == nullptr || bodyField == nullptr || + clear_pending_exception(env)) + { + return { + .error = Error::Network, + .message = "Android HTTP response shape was not recognized", + }; + } + + const int javaError = env->GetIntField(response, errorField); + auto* message = static_cast(env->GetObjectField(response, messageField)); + auto* headerNames = static_cast(env->GetObjectField(response, headerNamesField)); + auto* headerValues = + static_cast(env->GetObjectField(response, headerValuesField)); + auto* body = static_cast(env->GetObjectField(response, bodyField)); + if (clear_pending_exception(env)) { + return { + .error = Error::Network, + .message = "Failed to read Android HTTP response", + }; + } + + Response httpResponse{ + .statusCode = static_cast(env->GetIntField(response, statusField)), + .headers = read_headers(env, headerNames, headerValues), + .body = read_body(env, body), + }; + + std::string messageString = to_string(env, message); + + if (message != nullptr) { + env->DeleteLocalRef(message); + } + if (headerNames != nullptr) { + env->DeleteLocalRef(headerNames); + } + if (headerValues != nullptr) { + env->DeleteLocalRef(headerValues); + } + if (body != nullptr) { + env->DeleteLocalRef(body); + } + + return { + .error = map_java_error(javaError), + .message = std::move(messageString), + .response = std::move(httpResponse), + }; +} + +} // namespace + +bool available() noexcept { + return true; +} + +Backend backend() noexcept { + return Backend::Android; +} + +const char* backend_name() noexcept { + return "Android"; +} + +Result get(const Request& request) { + if (request.url.empty()) { + return { + .error = Error::InvalidUrl, + .message = "URL is empty", + }; + } + if (!request.url.starts_with("https://")) { + return { + .error = Error::UnsupportedScheme, + .message = "Only https:// URLs are supported", + }; + } + + auto* env = static_cast(SDL_GetAndroidJNIEnv()); + if (env == nullptr) { + return { + .error = Error::Network, + .message = "Failed to access Android JNI environment", + }; + } + + jobject activity = static_cast(SDL_GetAndroidActivity()); + if (activity == nullptr || clear_pending_exception(env)) { + if (activity != nullptr) { + env->DeleteLocalRef(activity); + } + return { + .error = Error::Network, + .message = "Failed to access Android activity", + }; + } + + jclass clientClass = + load_dusk_class(env, activity, "dev.twilitrealm.dusk.DuskHttpClient"); + env->DeleteLocalRef(activity); + if (clientClass == nullptr) { + return { + .error = Error::Network, + .message = "Failed to load Android HTTP helper", + }; + } + + jmethodID getMethod = env->GetStaticMethodID(clientClass, "get", + "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;IJ)" + "Ldev/twilitrealm/dusk/DuskHttpClient$Response;"); + if (getMethod == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(clientClass); + return { + .error = Error::Network, + .message = "Failed to find Android HTTP helper method", + }; + } + + jstring url = to_jstring(env, request.url); + jobjectArray headerNames = make_string_array(env, request.headers, true); + jobjectArray headerValues = make_string_array(env, request.headers, false); + if (url == nullptr || headerNames == nullptr || headerValues == nullptr || + clear_pending_exception(env)) + { + if (url != nullptr) { + env->DeleteLocalRef(url); + } + if (headerNames != nullptr) { + env->DeleteLocalRef(headerNames); + } + if (headerValues != nullptr) { + env->DeleteLocalRef(headerValues); + } + env->DeleteLocalRef(clientClass); + return { + .error = Error::Network, + .message = "Failed to prepare Android HTTP request", + }; + } + + jobject response = env->CallStaticObjectMethod(clientClass, getMethod, url, headerNames, + headerValues, timeout_ms(request.timeout), max_body_bytes(request.maxBodyBytes)); + env->DeleteLocalRef(url); + env->DeleteLocalRef(headerNames); + env->DeleteLocalRef(headerValues); + env->DeleteLocalRef(clientClass); + if (clear_pending_exception(env)) { + return { + .error = Error::Network, + .message = "Android HTTP request failed with a Java exception", + }; + } + + Result result = result_from_response(env, response); + if (response != nullptr) { + env->DeleteLocalRef(response); + } + return result; +} + +} // namespace dusk::http diff --git a/src/dusk/http/curl.cpp b/src/dusk/http/curl.cpp new file mode 100644 index 0000000000..5fdaf88a64 --- /dev/null +++ b/src/dusk/http/curl.cpp @@ -0,0 +1,206 @@ +#include "http.hpp" + +#include + +#include +#include +#include +#include + +namespace dusk::http { +namespace { + +struct CurlHeaders { + curl_slist* list = nullptr; + + ~CurlHeaders() { + if (list != nullptr) { + curl_slist_free_all(list); + } + } + + bool append(const std::string& header) { + curl_slist* next = curl_slist_append(list, header.c_str()); + if (next == nullptr) { + return false; + } + list = next; + return true; + } +}; + +struct CurlContext { + Response response; + size_t maxBodyBytes = 0; + bool tooLarge = false; +}; + +void initialize_curl() { + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +std::string trim_header_value(std::string_view value) { + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.remove_prefix(1); + } + while (!value.empty() && + (value.back() == '\r' || value.back() == '\n' || value.back() == ' ' || + value.back() == '\t')) { + value.remove_suffix(1); + } + return std::string(value); +} + +size_t write_body(char* ptr, size_t size, size_t nmemb, void* userdata) { + auto* context = static_cast(userdata); + const size_t bytes = size * nmemb; + if (bytes > context->maxBodyBytes || + context->response.body.size() > context->maxBodyBytes - bytes) { + context->tooLarge = true; + return 0; + } + + context->response.body.append(ptr, bytes); + return bytes; +} + +size_t write_header(char* ptr, size_t size, size_t nmemb, void* userdata) { + auto* context = static_cast(userdata); + const std::string_view line(ptr, size * nmemb); + if (line.starts_with("HTTP/")) { + context->response.headers.clear(); + return size * nmemb; + } + + const size_t colon = line.find(':'); + if (colon == std::string_view::npos) { + return size * nmemb; + } + + context->response.headers.push_back({ + .name = std::string(line.substr(0, colon)), + .value = trim_header_value(line.substr(colon + 1)), + }); + return size * nmemb; +} + +Error map_curl_error(CURLcode code, bool tooLarge) { + if (tooLarge) { + return Error::TooLarge; + } + + switch (code) { + case CURLE_OK: + return Error::None; + case CURLE_URL_MALFORMAT: + return Error::InvalidUrl; + case CURLE_UNSUPPORTED_PROTOCOL: + return Error::UnsupportedScheme; + case CURLE_OPERATION_TIMEDOUT: + return Error::Timeout; + default: + return Error::Network; + } +} + +long timeout_ms(std::chrono::milliseconds timeout) { + return std::max(1, timeout.count()); +} + +} // namespace + +bool available() noexcept { + return true; +} + +Backend backend() noexcept { + return Backend::LibCurl; +} + +const char* backend_name() noexcept { + return "libcurl"; +} + +Result get(const Request& request) { + if (request.url.empty()) { + return { + .error = Error::InvalidUrl, + .message = "URL is empty", + }; + } + if (!request.url.starts_with("https://")) { + return { + .error = Error::UnsupportedScheme, + .message = "Only https:// URLs are supported", + }; + } + + static std::once_flag initFlag; + std::call_once(initFlag, initialize_curl); + + CURL* curl = curl_easy_init(); + if (curl == nullptr) { + return { + .error = Error::Network, + .message = "Failed to create libcurl request", + }; + } + + CurlHeaders headers; + for (const Header& header : request.headers) { + if (!headers.append(header.name + ": " + header.value)) { + curl_easy_cleanup(curl); + return { + .error = Error::Network, + .message = "Failed to allocate libcurl headers", + }; + } + } + + CurlContext context{ + .maxBodyBytes = request.maxBodyBytes, + }; + + curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers.list); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms(request.timeout)); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms(request.timeout)); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_body); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &context); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_header); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &context); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); +#if CURL_AT_LEAST_VERSION(7, 85, 0) + curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https"); + curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https"); +#else + curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); + curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS); +#endif + + const CURLcode code = curl_easy_perform(curl); + long statusCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode); + curl_easy_cleanup(curl); + + context.response.statusCode = static_cast(statusCode); + if (code == CURLE_OK) { + return { + .response = std::move(context.response), + }; + } + + const Error error = map_curl_error(code, context.tooLarge); + return { + .error = error, + .message = error == Error::TooLarge ? "Response body exceeded the configured limit" + : curl_easy_strerror(code), + .response = std::move(context.response), + }; +} + +} // namespace dusk::http diff --git a/src/dusk/http/http.hpp b/src/dusk/http/http.hpp new file mode 100644 index 0000000000..54bef6eec1 --- /dev/null +++ b/src/dusk/http/http.hpp @@ -0,0 +1,60 @@ +#ifndef DUSK_HTTP_HTTP_HPP +#define DUSK_HTTP_HTTP_HPP + +#include +#include +#include +#include + +namespace dusk::http { + +enum class Backend { + None, + WinHttp, + UrlSession, + LibCurl, + Android, +}; + +enum class Error { + None, + NoBackend, + InvalidUrl, + UnsupportedScheme, + Timeout, + TooLarge, + Network, +}; + +struct Header { + std::string name; + std::string value; +}; + +struct Request { + std::string url; + std::vector
headers; + std::chrono::milliseconds timeout{10000}; + size_t maxBodyBytes = 1024 * 1024; +}; + +struct Response { + int statusCode = 0; + std::vector
headers; + std::string body; +}; + +struct Result { + Error error = Error::None; + std::string message; + Response response; +}; + +bool available() noexcept; +Backend backend() noexcept; +const char* backend_name() noexcept; +Result get(const Request& request); + +} // namespace dusk::http + +#endif // DUSK_HTTP_HTTP_HPP diff --git a/src/dusk/http/no_backend.cpp b/src/dusk/http/no_backend.cpp new file mode 100644 index 0000000000..4b42afb3c7 --- /dev/null +++ b/src/dusk/http/no_backend.cpp @@ -0,0 +1,24 @@ +#include "http.hpp" + +namespace dusk::http { + +bool available() noexcept { + return false; +} + +Backend backend() noexcept { + return Backend::None; +} + +const char* backend_name() noexcept { + return "none"; +} + +Result get(const Request&) { + return { + .error = Error::NoBackend, + .message = "No HTTP backend is available", + }; +} + +} // namespace dusk::http diff --git a/src/dusk/http/url_session.mm b/src/dusk/http/url_session.mm new file mode 100644 index 0000000000..01dd699a9c --- /dev/null +++ b/src/dusk/http/url_session.mm @@ -0,0 +1,238 @@ +#include "http.hpp" + +#import + +#include +#include +#include + +@interface DuskHttpRequestDelegate : NSObject +@property(nonatomic) dispatch_semaphore_t semaphore; +@property(nonatomic) size_t maxBodyBytes; +@property(nonatomic, strong) NSMutableData* data; +@property(nonatomic, strong) NSURLResponse* response; +@property(nonatomic, strong) NSError* error; +@property(nonatomic) BOOL tooLarge; +- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes; +@end + +@implementation DuskHttpRequestDelegate + +- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes { + self = [super init]; + if (self != nil) { + _semaphore = dispatch_semaphore_create(0); + _maxBodyBytes = maxBodyBytes; + _data = [NSMutableData data]; + } + return self; +} + +- (void)URLSession:(NSURLSession*)session + task:(NSURLSessionTask*)task + willPerformHTTPRedirection:(NSHTTPURLResponse*)response + newRequest:(NSURLRequest*)request + completionHandler:(void (^)(NSURLRequest*))completionHandler { + if ([[request.URL.scheme lowercaseString] isEqualToString:@"https"]) { + completionHandler(request); + } else { + completionHandler(nil); + } +} + +- (void)URLSession:(NSURLSession*)session + dataTask:(NSURLSessionDataTask*)dataTask +didReceiveResponse:(NSURLResponse*)response + completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { + self.response = response; + completionHandler(NSURLSessionResponseAllow); +} + +- (void)URLSession:(NSURLSession*)session + dataTask:(NSURLSessionDataTask*)dataTask + didReceiveData:(NSData*)data { + if (data.length > self.maxBodyBytes || + self.data.length > self.maxBodyBytes - data.length) { + self.tooLarge = YES; + [dataTask cancel]; + return; + } + [self.data appendData:data]; +} + +- (void)URLSession:(NSURLSession*)session + task:(NSURLSessionTask*)task + didCompleteWithError:(NSError*)error { + if (error != nil && !self.tooLarge) { + self.error = error; + } + dispatch_semaphore_signal(self.semaphore); +} + +@end + +namespace dusk::http { +namespace { + +NSString* to_nsstring(std::string_view value) { + return [[NSString alloc] initWithBytes:value.data() + length:value.size() + encoding:NSUTF8StringEncoding]; +} + +std::string to_string(NSString* value) { + if (value == nil) { + return {}; + } + + const char* utf8 = [value UTF8String]; + return utf8 == nullptr ? std::string() : std::string(utf8); +} + +Error map_nsurl_error(NSError* error) { + if (error == nil || ![error.domain isEqualToString:NSURLErrorDomain]) { + return Error::Network; + } + + switch (error.code) { + case NSURLErrorTimedOut: + return Error::Timeout; + case NSURLErrorBadURL: + case NSURLErrorUnsupportedURL: + return Error::InvalidUrl; + default: + return Error::Network; + } +} + +dispatch_time_t timeout_deadline(std::chrono::milliseconds timeout) { + const auto milliseconds = std::max(1, timeout.count()); + return dispatch_time(DISPATCH_TIME_NOW, + static_cast(milliseconds) * static_cast(NSEC_PER_MSEC)); +} + +} // namespace + +bool available() noexcept { + return true; +} + +Backend backend() noexcept { + return Backend::UrlSession; +} + +const char* backend_name() noexcept { + return "NSURLSession"; +} + +Result get(const Request& request) { + @autoreleasepool { + if (request.url.empty()) { + return { + .error = Error::InvalidUrl, + .message = "URL is empty", + }; + } + if (!request.url.starts_with("https://")) { + return { + .error = Error::UnsupportedScheme, + .message = "Only https:// URLs are supported", + }; + } + + NSString* urlString = to_nsstring(request.url); + if (urlString == nil) { + return { + .error = Error::InvalidUrl, + .message = "URL is not valid UTF-8", + }; + } + + NSURL* url = [NSURL URLWithString:urlString]; + if (url == nil || ![[url.scheme lowercaseString] isEqualToString:@"https"]) { + return { + .error = Error::InvalidUrl, + .message = "Failed to parse URL", + }; + } + + NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url]; + urlRequest.HTTPMethod = @"GET"; + urlRequest.timeoutInterval = request.timeout.count() / 1000.0; + urlRequest.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + for (const Header& header : request.headers) { + NSString* name = to_nsstring(header.name); + NSString* value = to_nsstring(header.value); + if (name == nil || value == nil) { + return { + .error = Error::InvalidUrl, + .message = "Request header is not valid UTF-8", + }; + } + [urlRequest setValue:value forHTTPHeaderField:name]; + } + + NSURLSessionConfiguration* configuration = + [NSURLSessionConfiguration ephemeralSessionConfiguration]; + configuration.timeoutIntervalForRequest = request.timeout.count() / 1000.0; + configuration.timeoutIntervalForResource = request.timeout.count() / 1000.0; + + DuskHttpRequestDelegate* delegate = + [[DuskHttpRequestDelegate alloc] initWithMaxBodyBytes:request.maxBodyBytes]; + NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration + delegate:delegate + delegateQueue:nil]; + NSURLSessionDataTask* task = [session dataTaskWithRequest:urlRequest]; + [task resume]; + + if (dispatch_semaphore_wait(delegate.semaphore, timeout_deadline(request.timeout)) != 0) { + [task cancel]; + [session invalidateAndCancel]; + return { + .error = Error::Timeout, + .message = "Request timed out", + }; + } + + [session finishTasksAndInvalidate]; + + Response response; + if ([delegate.response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)delegate.response; + response.statusCode = static_cast(httpResponse.statusCode); + NSDictionary* headers = httpResponse.allHeaderFields; + for (id key in headers) { + id value = headers[key]; + response.headers.push_back({ + .name = to_string([key description]), + .value = to_string([value description]), + }); + } + } + if (delegate.data != nil && delegate.data.length > 0) { + response.body.assign(static_cast(delegate.data.bytes), + static_cast(delegate.data.length)); + } + + if (delegate.tooLarge) { + return { + .error = Error::TooLarge, + .message = "Response body exceeded the configured limit", + .response = std::move(response), + }; + } + if (delegate.error != nil) { + return { + .error = map_nsurl_error(delegate.error), + .message = to_string(delegate.error.localizedDescription), + .response = std::move(response), + }; + } + + return { + .response = std::move(response), + }; + } +} + +} // namespace dusk::http diff --git a/src/dusk/http/winhttp.cpp b/src/dusk/http/winhttp.cpp new file mode 100644 index 0000000000..937579f076 --- /dev/null +++ b/src/dusk/http/winhttp.cpp @@ -0,0 +1,320 @@ +#include "http.hpp" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +#include +#include +#include +#include +#include + +namespace dusk::http { +namespace { + +struct WinHttpHandle { + HINTERNET handle = nullptr; + + WinHttpHandle() = default; + explicit WinHttpHandle(HINTERNET handle) : handle(handle) {} + WinHttpHandle(const WinHttpHandle&) = delete; + WinHttpHandle& operator=(const WinHttpHandle&) = delete; + + ~WinHttpHandle() { + if (handle != nullptr) { + WinHttpCloseHandle(handle); + } + } + + operator HINTERNET() const { return handle; } +}; + +std::wstring utf8_to_wide(std::string_view value) { + if (value.empty()) { + return {}; + } + + const int required = MultiByteToWideChar( + CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), nullptr, 0); + if (required <= 0) { + return {}; + } + + std::wstring result(static_cast(required), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), + result.data(), required); + return result; +} + +std::string wide_to_utf8(std::wstring_view value) { + if (value.empty()) { + return {}; + } + + const int required = WideCharToMultiByte( + CP_UTF8, 0, value.data(), static_cast(value.size()), nullptr, 0, nullptr, nullptr); + if (required <= 0) { + return {}; + } + + std::string result(static_cast(required), '\0'); + WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), result.data(), + required, nullptr, nullptr); + return result; +} + +DWORD timeout_ms(std::chrono::milliseconds timeout) { + const auto count = std::max(1, timeout.count()); + return static_cast( + std::min(count, std::numeric_limits::max())); +} + +Error map_winhttp_error(DWORD error) { + switch (error) { + case ERROR_WINHTTP_TIMEOUT: + return Error::Timeout; + case ERROR_WINHTTP_INVALID_URL: + case ERROR_WINHTTP_UNRECOGNIZED_SCHEME: + return Error::InvalidUrl; + case ERROR_WINHTTP_SECURE_FAILURE: + case ERROR_WINHTTP_CANNOT_CONNECT: + case ERROR_WINHTTP_CONNECTION_ERROR: + default: + return Error::Network; + } +} + +Result fail_from_last_error(const char* message) { + const DWORD error = GetLastError(); + return { + .error = map_winhttp_error(error), + .message = std::string(message) + " (" + std::to_string(error) + ")", + }; +} + +std::string trim_header_value(std::string_view value) { + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.remove_prefix(1); + } + while (!value.empty() && (value.back() == '\r' || value.back() == '\n' || value.back() == ' ' || + value.back() == '\t')) + { + value.remove_suffix(1); + } + return std::string(value); +} + +void parse_headers(std::wstring_view rawHeaders, Response& response) { + size_t start = 0; + bool firstLine = true; + while (start < rawHeaders.size()) { + size_t end = rawHeaders.find(L"\r\n", start); + if (end == std::wstring_view::npos) { + end = rawHeaders.size(); + } + + const std::wstring_view line = rawHeaders.substr(start, end - start); + if (!line.empty() && !firstLine) { + const size_t colon = line.find(L':'); + if (colon != std::wstring_view::npos) { + response.headers.push_back({ + .name = wide_to_utf8(line.substr(0, colon)), + .value = trim_header_value(wide_to_utf8(line.substr(colon + 1))), + }); + } + } + firstLine = false; + + if (end == rawHeaders.size()) { + break; + } + start = end + 2; + } +} + +bool read_status(HINTERNET request, Response& response) { + DWORD statusCode = 0; + DWORD statusCodeSize = sizeof(statusCode); + if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX)) + { + return false; + } + response.statusCode = static_cast(statusCode); + return true; +} + +bool read_headers(HINTERNET request, Response& response) { + DWORD headerBytes = 0; + WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, + WINHTTP_NO_OUTPUT_BUFFER, &headerBytes, WINHTTP_NO_HEADER_INDEX); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return false; + } + + std::wstring rawHeaders(headerBytes / sizeof(wchar_t), L'\0'); + if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, + rawHeaders.data(), &headerBytes, WINHTTP_NO_HEADER_INDEX)) + { + return false; + } + if (!rawHeaders.empty() && rawHeaders.back() == L'\0') { + rawHeaders.pop_back(); + } + parse_headers(rawHeaders, response); + return true; +} + +} // namespace + +bool available() noexcept { + return true; +} + +Backend backend() noexcept { + return Backend::WinHttp; +} + +const char* backend_name() noexcept { + return "WinHTTP"; +} + +Result get(const Request& request) { + if (request.url.empty()) { + return { + .error = Error::InvalidUrl, + .message = "URL is empty", + }; + } + + std::wstring wideUrl = utf8_to_wide(request.url); + if (wideUrl.empty()) { + return { + .error = Error::InvalidUrl, + .message = "URL is not valid UTF-8", + }; + } + + URL_COMPONENTS components{}; + components.dwStructSize = sizeof(components); + components.dwSchemeLength = static_cast(-1); + components.dwHostNameLength = static_cast(-1); + components.dwUrlPathLength = static_cast(-1); + components.dwExtraInfoLength = static_cast(-1); + if (!WinHttpCrackUrl(wideUrl.c_str(), static_cast(wideUrl.size()), 0, &components)) { + return fail_from_last_error("Failed to parse URL"); + } + if (components.nScheme != INTERNET_SCHEME_HTTPS) { + return { + .error = Error::UnsupportedScheme, + .message = "Only https:// URLs are supported", + }; + } + + const std::wstring host(components.lpszHostName, components.dwHostNameLength); + std::wstring path; + if (components.lpszUrlPath != nullptr && components.dwUrlPathLength > 0) { + path.assign(components.lpszUrlPath, components.dwUrlPathLength); + } + if (components.lpszExtraInfo != nullptr && components.dwExtraInfoLength > 0) { + path.append(components.lpszExtraInfo, components.dwExtraInfoLength); + } + if (path.empty()) { + path = L"/"; + } + + WinHttpHandle session(WinHttpOpen(L"Dusk", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)); + if (session.handle == nullptr) { + return fail_from_last_error("Failed to create WinHTTP session"); + } + + const DWORD timeout = timeout_ms(request.timeout); + WinHttpSetTimeouts(session, timeout, timeout, timeout, timeout); + + WinHttpHandle connection(WinHttpConnect(session, host.c_str(), components.nPort, 0)); + if (connection.handle == nullptr) { + return fail_from_last_error("Failed to connect"); + } + + WinHttpHandle httpRequest(WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr, + WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE)); + if (httpRequest.handle == nullptr) { + return fail_from_last_error("Failed to create request"); + } + + DWORD redirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP; + WinHttpSetOption( + httpRequest, WINHTTP_OPTION_REDIRECT_POLICY, &redirectPolicy, sizeof(redirectPolicy)); + DWORD maxRedirects = 5; + WinHttpSetOption(httpRequest, WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS, &maxRedirects, + sizeof(maxRedirects)); + + for (const Header& header : request.headers) { + const std::wstring wideHeader = utf8_to_wide(header.name + ": " + header.value); + if (wideHeader.empty()) { + return { + .error = Error::InvalidUrl, + .message = "Request header is not valid UTF-8", + }; + } + if (!WinHttpAddRequestHeaders(httpRequest, wideHeader.c_str(), + static_cast(wideHeader.size()), WINHTTP_ADDREQ_FLAG_ADD)) + { + return fail_from_last_error("Failed to add request header"); + } + } + + if (!WinHttpSendRequest( + httpRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) + { + return fail_from_last_error("Failed to send request"); + } + if (!WinHttpReceiveResponse(httpRequest, nullptr)) { + return fail_from_last_error("Failed to receive response"); + } + + Response response; + if (!read_status(httpRequest, response)) { + return fail_from_last_error("Failed to read response status"); + } + read_headers(httpRequest, response); + + for (;;) { + DWORD availableBytes = 0; + if (!WinHttpQueryDataAvailable(httpRequest, &availableBytes)) { + return fail_from_last_error("Failed to query response body"); + } + if (availableBytes == 0) { + break; + } + if (availableBytes > request.maxBodyBytes || + response.body.size() > request.maxBodyBytes - availableBytes) + { + return { + .error = Error::TooLarge, + .message = "Response body exceeded the configured limit", + .response = std::move(response), + }; + } + + std::vector buffer(availableBytes); + DWORD bytesRead = 0; + if (!WinHttpReadData(httpRequest, buffer.data(), availableBytes, &bytesRead)) { + return fail_from_last_error("Failed to read response body"); + } + response.body.append(buffer.data(), bytesRead); + } + + return { + .response = std::move(response), + }; +} + +} // namespace dusk::http diff --git a/src/dusk/imgui/ImGuiActorSpawner.cpp b/src/dusk/imgui/ImGuiActorSpawner.cpp new file mode 100644 index 0000000000..f3aefd4c41 --- /dev/null +++ b/src/dusk/imgui/ImGuiActorSpawner.cpp @@ -0,0 +1,142 @@ +#include "imgui.h" + +#include "ImGuiMenuTools.hpp" +#include "d/actor/d_a_alink.h" +#include "d/d_com_inf_game.h" +#include "f_op/f_op_actor_mng.h" +#include "SSystem/SComponent/c_sxyz.h" +#include "SSystem/SComponent/c_xyz.h" + +namespace dusk { +namespace { + +struct ActorSpawnerState { + int actorId = 0; + int params = -1; + int argument = -1; + int angleX = 0; + int angleY = 0; + int angleZ = 0; + float scaleX = 1.0f; + float scaleY = 1.0f; + float scaleZ = 1.0f; + bool usePlayerRoom = true; + int manualRoom = 0; + int spawnCount = 1; + bool hasResult = false; + unsigned int lastResult = 0; + int lastAttempted = 0; +}; + +ActorSpawnerState s_state; + +} // namespace + +void ImGuiMenuTools::ShowActorSpawner() { + if (!m_showActorSpawner) { + return; + } + + if (!ImGui::Begin("Actor Spawner", &m_showActorSpawner)) { + ImGui::End(); + return; + } + + daAlink_c* player = (daAlink_c*)dComIfGp_getPlayer(0); + + ImGui::SeparatorText("Actor"); + ImGui::InputInt("Actor ID", &s_state.actorId); + ImGui::InputInt("Params (hex)", &s_state.params, 0, 0, ImGuiInputTextFlags_CharsHexadecimal); + ImGui::InputInt("Argument", &s_state.argument); + s_state.argument = (s_state.argument < -128) ? -128 : (s_state.argument > 127) ? 127 : s_state.argument; + + ImGui::SeparatorText("Angle"); + ImGui::InputInt("Angle X", &s_state.angleX); + ImGui::InputInt("Angle Y", &s_state.angleY); + ImGui::InputInt("Angle Z", &s_state.angleZ); + + ImGui::SeparatorText("Scale"); + ImGui::InputFloat("Scale X", &s_state.scaleX, 0.1f, 1.0f); + ImGui::InputFloat("Scale Y", &s_state.scaleY, 0.1f, 1.0f); + ImGui::InputFloat("Scale Z", &s_state.scaleZ, 0.1f, 1.0f); + + ImGui::SeparatorText("Spawn"); + ImGui::InputInt("Count", &s_state.spawnCount); + if (s_state.spawnCount < 1) { + s_state.spawnCount = 1; + } + + ImGui::SeparatorText("Position"); + ImGui::Checkbox("Use player room", &s_state.usePlayerRoom); + if (!s_state.usePlayerRoom) { + ImGui::InputInt("Room No", &s_state.manualRoom); + } + + if (player != nullptr) { + ImGui::Text("Spawn pos: %.2f, %.2f, %.2f", + player->current.pos.x, player->current.pos.y, player->current.pos.z); + } else { + ImGui::TextDisabled("Player not available"); + } + + ImGui::Separator(); + + bool canSpawn = player != nullptr; + if (!canSpawn) { + ImGui::BeginDisabled(); + } + + if (ImGui::Button("Spawn", ImVec2(-1, 0))) { + cXyz pos = player->current.pos; + csXyz angle; + angle.set((s16)s_state.angleX, (s16)s_state.angleY, (s16)s_state.angleZ); + cXyz scale(s_state.scaleX, s_state.scaleY, s_state.scaleZ); + int roomNo = s_state.usePlayerRoom ? player->current.roomNo : s_state.manualRoom; + + layer_class* savedLayer = fpcLy_CurrentLayer(); + base_process_class* playScene = fpcM_SearchByName(fpcNm_PLAY_SCENE_e); + if (playScene != nullptr) { + fpcLy_SetCurrentLayer(&((process_node_class*)playScene)->layer); + } + + s_state.lastResult = 0; + s_state.lastAttempted = s_state.spawnCount; + for (int i = 0; i < s_state.spawnCount; ++i) { + unsigned int result = fopAcM_create( + (s16)s_state.actorId, + (u32)s_state.params, + &pos, + roomNo, + &angle, + &scale, + (s8)s_state.argument + ); + if (result != 0) { + s_state.lastResult = result; + } + } + s_state.hasResult = true; + + fpcLy_SetCurrentLayer(savedLayer); + } + + if (!canSpawn) { + ImGui::EndDisabled(); + } + + if (s_state.hasResult) { + if (s_state.lastResult != 0) { + if (s_state.lastAttempted == 1) { + ImGui::Text("Spawned: proc ID %u", s_state.lastResult); + } else { + ImGui::Text("Spawned %d (last proc ID %u)", s_state.lastAttempted, s_state.lastResult); + } + } else { + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "Spawn failed (returned 0)"); + } + } + + ImGui::End(); +} + +} // namespace dusk diff --git a/src/dusk/imgui/ImGuiAudio.cpp b/src/dusk/imgui/ImGuiAudio.cpp index 9abe4c5261..ce9290efa5 100644 --- a/src/dusk/imgui/ImGuiAudio.cpp +++ b/src/dusk/imgui/ImGuiAudio.cpp @@ -1,5 +1,7 @@ #include "ImGuiConsole.hpp" #include "ImGuiMenuTools.hpp" +#include +#include "JSystem/JAudio2/JAISeq.h" #include "JSystem/JAudio2/JAISeMgr.h" #include "JSystem/JAudio2/JAISeqMgr.h" #include "JSystem/JAudio2/JAIStreamMgr.h" @@ -15,6 +17,24 @@ static std::array lastResetCounts = {}; static bool sortUpdateCount = true; +static void DrawDirectionGauge(float pan, float dolby) { + constexpr float R = 20.0f; + constexpr float SIZE = R * 2.0f + 4.0f; + + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImGui::Dummy(ImVec2(SIZE, SIZE)); + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 c = ImVec2(origin.x + SIZE * 0.5f, origin.y + SIZE * 0.5f); + + dl->AddCircle(c, R, IM_COL32(90, 90, 90, 255), 32); + + float dx = (pan - 0.5f) * 2.0f; + float dy = dolby * 2.0f - 1.0f; + float len = sqrtf(dx * dx + dy * dy); + if (len > 1.0f) { dx /= len; dy /= len; } + dl->AddLine(c, ImVec2(c.x + dx * R, c.y + dy * R), IM_COL32(255, 200, 50, 255), 1.5f); +} + static void DisplayDspChannel(int i) { using namespace dusk::audio; @@ -52,8 +72,10 @@ static void DisplayDspChannel(int i) { auto fxMix = (channel.mAutoMixerFxMix >> 8) / 127.5f; auto volume = VolumeFromU16(channel.mAutoMixerVolume); auto pitch = channel.mPitch / 4096.0f; + DrawDirectionGauge(pan, dolby); + ImGui::SameLine(); ImGui::Text( - "Auto mixer active (pan: %f, dolby: %f, fx: %f, volume: %f, pitch %f)", + "pan: %.2f dolby: %.2f\nfx: %.2f vol: %.2f pitch: %.2f", pan, dolby, fxMix, volume, pitch); } else { ImGui::Text( @@ -183,6 +205,10 @@ static void ShowAllJAISes() { if (ImGui::Button("Pause All")) { category->pause(true); } + ImGui::SameLine(); + if (ImGui::Button("Resume All")) { + category->pause(false); + } for (auto seLink = category->getSeList()->getFirst(); seLink != nullptr; seLink = seLink->getNext()) { const auto se = seLink->getObject(); @@ -196,6 +222,33 @@ static void ShowAllJAISes() { } +static void ShowSeqTracks(JAISeq& seq) { + JASTrack& root = seq.inner_.outputTrack; + + for (int group = 0; group < 2; group++) { + JASTrack* groupTrack = root.getChild(group); + if (groupTrack == nullptr) { + continue; + } + + for (int j = 0; j < JASTrack::MAX_CHILDREN; j++) { + JASTrack* track = groupTrack->getChild(j); + if (track == nullptr) { + continue; + } + + int trackIdx = group * 16 + j; + char label[64]; + snprintf(label, sizeof(label), "Track %d (bank %hu, prog %hu)##%p", + trackIdx, track->getBankNumber(), track->getProgNumber(), track); + bool muted = track->mFlags.mute; + if (ImGui::Checkbox(label, &muted)) { + track->mute(muted); + } + } + } +} + static void ShowAllJAISeqs() { auto& mgr = *JAISeqMgr::getInstance(); @@ -206,10 +259,32 @@ static void ShowAllJAISeqs() { if (ImGui::Button("Unpause")) { mgr.pause(false); } + + ImGui::Text("Active sequences: %d", mgr.getNumActiveSeqs()); + + auto* seqList = mgr.getSeqList(); + for (auto* link = seqList->getFirst(); link != nullptr; link = link->getNext()) { + JAISeq* seq = link->getObject(); + if (seq == nullptr) { + continue; + } + + char buf[32]; + snprintf(buf, sizeof(buf), "%p", seq); + + if (ImGui::BeginChild(buf, ImVec2(), ImGuiChildFlags_Border | ImGuiChildFlags_AutoResizeY)) { + ImGui::Text("Seq [%p]", seq); + ShowSeqTracks(*seq); + } + + ImGui::EndChild(); + } } void dusk::ImGuiMenuTools::ShowAudioDebug() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug)) { + if (!getSettings().backend.enableAdvancedSettings || + !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug)) + { return; } @@ -255,7 +330,9 @@ void dusk::ImGuiMenuTools::ShowAudioDebug() { } void dusk::ImGuiMenuTools::ShowSaveEditor() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showSaveEditor)) { + if (!getSettings().backend.enableAdvancedSettings || + !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showSaveEditor)) + { return; } m_saveEditor.draw(m_showSaveEditor); diff --git a/src/dusk/imgui/ImGuiCameraOverlay.cpp b/src/dusk/imgui/ImGuiCameraOverlay.cpp index aa3d1ac093..0d2168924c 100644 --- a/src/dusk/imgui/ImGuiCameraOverlay.cpp +++ b/src/dusk/imgui/ImGuiCameraOverlay.cpp @@ -1,13 +1,18 @@ #include "f_op/f_op_camera_mng.h" #include "SSystem/SComponent/c_xyz.h" +#include "d/d_com_inf_game.h" #include "imgui.h" +#include "ImGuiConfig.hpp" #include "ImGuiConsole.hpp" #include "ImGuiMenuTools.hpp" +#include "dusk/settings.h" namespace dusk { void ImGuiMenuTools::ShowCameraOverlay() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F9, m_showCameraOverlay)) { + if (!getSettings().backend.enableAdvancedSettings || + !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F9, m_showCameraOverlay)) + { return; } @@ -46,73 +51,44 @@ namespace dusk { ImGui::InputFloat("Camera FOV", &dCam->mFovy); - ImGui::SeparatorText("Free-look Data"); + ImGui::SeparatorText("Options"); - static float eyeYawDeg = 0.0f; - static float moveSpeed = 5000.0f; - static float rotSpeed = 5.0f; - static cXyz freeLookPos = cXyz::Zero; - static bool freeLookActive = false; - - bool changed = false; - - if (ImGui::IsKeyDown(ImGuiKey_LeftArrow)) { - eyeYawDeg += rotSpeed; - if (eyeYawDeg >= 360.0f) - eyeYawDeg -= 360.0f; - - changed = true; + bool eventRunning = (dComIfGp_event_runCheck() || dComIfGp_isPauseFlag()) && !getSettings().game.debugFlyCam; + if (eventRunning) { + ImGui::BeginDisabled(); } - else if (ImGui::IsKeyDown(ImGuiKey_RightArrow)) { - eyeYawDeg -= rotSpeed; - if (eyeYawDeg < 0.0f) - eyeYawDeg += 360.0f; - - changed = true; + config::ImGuiCheckbox("Fly Mode", getSettings().game.debugFlyCam); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + if (eventRunning) { + ImGui::SetTooltip("Cannot enable while paused or during an active event."); + } else { + ImGui::SetTooltip("Detach camera and fly freely.\n" + "WASD/Arrows/Left stick: move, Mouse/C-stick: look\n" + "Ctrl/L: down, Space/R: up, Shift/Z: fast\n" + "Q Key/Y: roll left, R Key/X: roll right"); + } } - cSAngle yawAngle = cSAngle(eyeYawDeg); - cXyz frontDir = cXyz(yawAngle.Sin(), 0.0f, yawAngle.Cos()); - - if (ImGui::IsKeyDown(ImGuiKey_UpArrow)) { - freeLookPos -= frontDir * moveSpeed * ImGui::GetIO().DeltaTime; - changed = true; - } - else if (ImGui::IsKeyDown(ImGuiKey_DownArrow)) { - freeLookPos += frontDir * moveSpeed * ImGui::GetIO().DeltaTime; - changed = true; + if (eventRunning) { + ImGui::EndDisabled(); } - if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) { - freeLookPos += cXyz::BaseY * moveSpeed * ImGui::GetIO().DeltaTime; - changed = true; + if (!getSettings().game.debugFlyCam) { + ImGui::BeginDisabled(); } - - if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) { - freeLookPos -= cXyz::BaseY * moveSpeed * ImGui::GetIO().DeltaTime; - changed = true; + config::ImGuiCheckbox("Lock Events", getSettings().game.debugFlyCamLockEvents); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + if (!getSettings().game.debugFlyCam) { + ImGui::SetTooltip("Enable Fly Mode first."); + } else { + ImGui::SetTooltip("Freeze game events while flying."); + } } - - if (!freeLookActive && changed) { - freeLookPos += dCam->Center(); - freeLookActive = true; + if (!getSettings().game.debugFlyCam) { + ImGui::EndDisabled(); } - if (ImGui::IsKeyDown(ImGuiKey_R)) { - freeLookPos = cXyz::Zero; - freeLookActive = false; - } - - if (freeLookActive) { - dCam->Reset(freeLookPos, freeLookPos + (frontDir * 100.0f)); - } - - ImGui::InputFloat("Free-look Yaw", &eyeYawDeg); - ImGui::InputFloat3("Free-look Position", &freeLookPos.x); - ImGui::InputFloat("Free-look Move Speed", &moveSpeed); - ImGui::InputFloat("Free-look Rotation Speed", &rotSpeed); - ShowCornerContextMenu(m_cameraOverlayCorner, 0); ImGui::End(); } -} \ No newline at end of file +} diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index bed1ee1a3f..f1246bbcb8 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -10,16 +10,19 @@ #include "fmt/format.h" #include "ImGuiConsole.hpp" +#include "ImGuiEngine.hpp" #include "JSystem/JUtility/JUTGamePad.h" -#include "SDL3/SDL_events.h" #include "SDL3/SDL_mouse.h" -#include "aurora/lib/window.hpp" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" #include "dusk/dusk.h" #include "dusk/frame_interpolation.h" +#include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/settings.h" +#include "dusk/ui/ui.hpp" +#include "f_pc/f_pc_manager.h" +#include "f_pc/f_pc_name.h" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_main.h" #include "tracy/Tracy.hpp" @@ -33,14 +36,6 @@ using namespace std::string_literals; using namespace std::string_view_literals; namespace { -ImVec2 TouchEventToScreenPos(const SDL_TouchFingerEvent& touch) { - const AuroraWindowSize size = aurora::window::get_window_size(); - return ImVec2{ - touch.x * static_cast(size.width), - touch.y * static_cast(size.height), - }; -} - ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) { while (window != nullptr) { const bool canScrollX = window->ScrollMax.x > 0.0f; @@ -64,6 +59,10 @@ namespace dusk { ImGui::TextUnformatted(text.data(), text.data() + text.size()); } + void DuskToast(std::string_view message, float duration) { + g_imguiConsole.AddToast(message, duration); + } + void ImGuiTextCenter(std::string_view text) { ImGui::NewLine(); float fontSize = ImGui::CalcTextSize( @@ -235,48 +234,7 @@ namespace dusk { ImGuiConsole::ImGuiConsole() {} void ImGuiConsole::HandleSDLEvent(const SDL_Event& event) { - if (!IsGameLaunched) { - return; - } - - switch (event.type) { - case SDL_EVENT_FINGER_DOWN: - if (!m_touchTapActive) { - m_touchTapActive = true; - m_touchTapMoved = false; - m_touchTapFingerId = event.tfinger.fingerID; - m_touchTapStartPos = TouchEventToScreenPos(event.tfinger); - } - break; - case SDL_EVENT_FINGER_MOTION: - if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) { - const auto currentPos = TouchEventToScreenPos(event.tfinger); - const auto delta = currentPos - m_touchTapStartPos; - if (ImLengthSqr(delta) > 144.0f) { - m_touchTapMoved = true; - } - } - break; - case SDL_EVENT_FINGER_UP: - if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) { - const bool shouldToggle = - !m_touchTapMoved && (m_isHidden || !ImGui::GetIO().WantCaptureMouse); - m_touchTapActive = false; - m_touchTapMoved = false; - if (shouldToggle) { - m_isHidden = !m_isHidden; - getSettings().backend.duskMenuOpen.setValue(!m_isHidden); - Save(); - } - } - break; - case SDL_EVENT_FINGER_CANCELED: - m_touchTapActive = false; - m_touchTapMoved = false; - break; - default: - break; - } + (void)event; } void ImGuiConsole::UpdateSettings() { @@ -296,33 +254,21 @@ namespace dusk { UpdateSettings(); - if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && - ImGui::IsKeyPressed(ImGuiKey_R)) - { - JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; - } - if (ImGui::IsKeyPressed(ImGuiKey_F11)) { - ImGuiMenuGame::ToggleFullscreen(); + getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen); + VISetWindowFullscreen(getSettings().video.enableFullscreen); + config::Save(); } - if (ImGui::IsKeyPressed(ImGuiKey_Escape) && getSettings().video.enableFullscreen) { - ImGuiMenuGame::ToggleFullscreen(); - } - - if (!dusk::IsGameLaunched) { - m_preLaunchWindow.draw(); - } - - m_isHidden = !getSettings().backend.duskMenuOpen; - bool showMenu = !dusk::IsGameLaunched || !CheckMenuViewToggle(ImGuiKey_F1, m_isHidden); - if (dusk::IsGameLaunched) { - const bool menuOpen = !m_isHidden; - if (getSettings().backend.duskMenuOpen != menuOpen) { - getSettings().backend.duskMenuOpen.setValue(menuOpen); - Save(); + if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) { + if (getSettings().backend.enableAdvancedSettings) { + m_isHidden = !m_isHidden; + } else { + m_isHidden = true; } } + + bool showMenu = !m_isHidden; // The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg, // so make the window bg fully transparent temporarily @@ -332,38 +278,94 @@ namespace dusk { m_menuTools.draw(); m_menuMods.draw(); - const auto fpsLabel = - fmt::format(FMT_STRING("FPS: {:.2f}\n"), ImGui::GetIO().Framerate); - const auto fpsSize = - ImGui::CalcTextSize(fpsLabel.data(), fpsLabel.data() + fpsLabel.size()); - ImGui::SetCursorPosX( - ImMax(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() - - ImGui::GetStyle().DisplaySafeAreaPadding.x - - fpsSize.x - ImGui::GetStyle().ItemSpacing.x)); - ImGuiStringViewText(fpsLabel); - ImGui::EndMainMenuBar(); } ImGui::PopStyleColor(); - if (!getSettings().backend.wasPresetChosen) { - m_firstRunPreset.draw(); - return; - } - if (dusk::IsGameLaunched && !m_isLaunchInitialized) { - m_toasts.emplace_back(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ? - "Tap to toggle menu"s : - "Press F1 to toggle menu"s, - 2.5f); m_isLaunchInitialized = true; + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::connectLiveSplit(); + } } UpdateDragScroll(); - m_menuGame.windowControllerConfig(); - m_menuGame.windowInputViewer(); - if (dusk::IsGameLaunched) { + // Show message when Aurora backend is Null + if (aurora_get_backend() == BACKEND_NULL) { + auto& io = ImGui::GetIO(); + ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y)); + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowBgAlpha(0.65f); + ImGui::Begin("Pre Launch Window", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoBringToFrontOnFocus); + ImGui::NewLine(); + if (ImGuiEngine::duskLogo) { + const auto& windowSize = ImGui::GetWindowSize(); + ImGui::NewLine(); + float iconSize = 150.f; + float width = iconSize * 2.5f; + ImGui::SameLine(windowSize.x / 2 - width + (width / 2)); + ImGui::Image(ImGuiEngine::duskLogo, ImVec2{width, iconSize}); + } else { + ImGui::PushFont(ImGuiEngine::fontExtraLarge); + ImGuiTextCenter("Dusk"); + ImGui::PopFont(); + } + ImGui::PushFont(ImGuiEngine::fontLarge); + ImGuiTextCenter("Failed to initialize any graphics backend."); + ImGuiTextCenter("\nYour system may be misconfigured, or your hardware may not support the required versions of any of the available backends."); + ImGuiTextCenter("\nA clean reinstall of Dusk may help. For further assistance, please visit #tech-support on the Twilit Realm Discord server."); + const auto& style = ImGui::GetStyle(); + const auto retrySize = ImGui::CalcTextSize("Retry (Auto backend)"); + const auto quitSize = ImGui::CalcTextSize("Quit"); + float buttonsWidth = quitSize.x + style.FramePadding.x * 2.0f; + if constexpr (SupportsProcessRestart) { + buttonsWidth += retrySize.x + style.FramePadding.x * 2.0f + style.ItemSpacing.x; + } +#if DUSK_CAN_OPEN_DATA_FOLDER + const auto openSize = ImGui::CalcTextSize("Open Data Folder"); + buttonsWidth += openSize.x + style.FramePadding.x * 2.0f + style.ItemSpacing.x; +#endif + ImGui::NewLine(); + ImGui::SetCursorPosX( + ImMax(style.WindowPadding.x, (ImGui::GetWindowSize().x - buttonsWidth) * 0.5f)); + if constexpr (SupportsProcessRestart) { + if (ImGui::Button("Retry (Auto backend)")) { + getSettings().backend.graphicsBackend.setValue("auto"); + config::Save(); + RestartRequested = true; + IsRunning = false; + } + ImGui::SameLine(); + } +#if DUSK_CAN_OPEN_DATA_FOLDER + if (ImGui::Button("Open Data Folder")) { + OpenDataFolder(); + } + ImGui::SameLine(); +#endif + if (ImGui::Button("Quit")) { + IsRunning = false; + } + ImGui::PopFont(); + ImGui::End(); + } + + m_menuTools.ShowInputViewer(); + m_menuGame.drawSpeedrunTimerOverlay(); + + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::updateLiveSplit(); + if (dusk::speedrun::consumeConnectedEvent()) + AddToast("LiveSplit connected"); + else if (dusk::speedrun::consumeDisconnectedEvent()) + AddToast("LiveSplit disconnected"); + } + + if (dusk::IsGameLaunched && !dusk::getSettings().game.speedrunMode) { m_menuTools.ShowDebugOverlay(); m_menuTools.ShowCameraOverlay(); m_menuTools.ShowProcessManager(); @@ -375,23 +377,23 @@ namespace dusk { m_menuTools.ShowAudioDebug(); m_menuTools.ShowSaveEditor(); m_menuMods.showModsWindow(); + m_menuTools.ShowStateShare(); + m_menuTools.ShowActorSpawner(); } - m_menuTools.ShowStateShare(); - DuskDebugPad(); // temporary, remove later // Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds. - ImGuiIO& io = ImGui::GetIO(); - if (showMenu) { - mouseHideTimer = 0.0f; - ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. - } else if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) { - mouseHideTimer = 0.0f; - ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. - } else if (mouseHideTimer <= 3.0f) { - mouseHideTimer += ImGui::GetIO().DeltaTime; - } else { - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; - SDL_HideCursor(); + if (dusk::getSettings().game.gyroMode.getValue() != GyroMode::Mouse) + { + ImGuiIO& io = ImGui::GetIO(); + if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) { + mouseHideTimer = 0.0f; + ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. + } else if (mouseHideTimer <= 3.0f) { + mouseHideTimer += ImGui::GetIO().DeltaTime; + } else { + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; + SDL_HideCursor(); + } } ShowToasts(); @@ -548,6 +550,10 @@ namespace dusk { return false; } + void ImGuiConsole::AddToast(std::string_view message, float duration) { + m_toasts.emplace_back(std::string(message), duration); + } + void ImGuiConsole::ShowToasts() { if (m_toasts.empty()) { return; diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 932b4ec951..8c9d6b1929 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -6,13 +6,11 @@ #include #include -#include -#include "ImGuiFirstRunPreset.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiMenuMods.hpp" #include "ImGuiMenuTools.hpp" -#include "ImGuiPreLaunchWindow.hpp" +#include "dusk/main.h" #include "imgui.h" union SDL_Event; @@ -27,7 +25,8 @@ public: void PreDraw(); void PostDraw(); - static bool CheckMenuViewToggle(ImGuiKey key, bool& active); + static bool CheckMenuViewToggle(ImGuiKey key, bool& active); + void AddToast(std::string_view message, float duration = 3.f); private: struct Toast { @@ -42,18 +41,12 @@ private: bool m_isHidden = true; bool m_isLaunchInitialized = false; - bool m_touchTapActive = false; - bool m_touchTapMoved = false; - SDL_FingerID m_touchTapFingerId = 0; - ImVec2 m_touchTapStartPos = {}; ImGuiWindow* m_dragScrollWindow = nullptr; ImVec2 m_dragScrollLastMousePos = {}; std::deque m_toasts; - ImGuiFirstRunPreset m_firstRunPreset; ImGuiMenuGame m_menuGame; ImGuiMenuMods m_menuMods; - ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last ImGuiMenuTools m_menuTools; @@ -72,6 +65,7 @@ std::string BytesToString(size_t bytes); void SetOverlayWindowLocation(int corner); bool ShowCornerContextMenu(int& corner, int avoidCorner); void ImGuiStringViewText(std::string_view text); +void DuskToast(std::string_view message, float duration = 3.f); void ImGuiBeginGroupPanel(const char* name, const ImVec2& size); void ImGuiEndGroupPanel(); void ImGuiTextCenter(std::string_view text); @@ -79,6 +73,4 @@ bool ImGuiButtonCenter(std::string_view text); float ImGuiScale(); } // namespace dusk -void DuskDebugPad(); - #endif // DUSK_IMGUI_HPP diff --git a/src/dusk/imgui/ImGuiControllerOverlay.cpp b/src/dusk/imgui/ImGuiControllerOverlay.cpp index dd0a15806c..75637e5e61 100644 --- a/src/dusk/imgui/ImGuiControllerOverlay.cpp +++ b/src/dusk/imgui/ImGuiControllerOverlay.cpp @@ -3,12 +3,11 @@ #include "imgui.h" #include #include "ImGuiConsole.hpp" -#include "ImGuiMenuGame.hpp" #include namespace dusk { - void ImGuiMenuGame::windowInputViewer() { + void ImGuiMenuTools::ShowInputViewer() { if (!m_showInputViewer) { return; } diff --git a/src/dusk/imgui/ImGuiDebugPad.cpp b/src/dusk/imgui/ImGuiDebugPad.cpp deleted file mode 100644 index b1591f1968..0000000000 --- a/src/dusk/imgui/ImGuiDebugPad.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "m_Do/m_Do_controller_pad.h" - -#include "imgui.h" -#include "ImGuiConsole.hpp" - -void DuskDebugPad() { - auto& pad = mDoCPd_c::getCpadInfo(PAD_1); - auto KeyFlag = [&](ImGuiKey key, u32 padFlag) { - if (ImGui::IsKeyDown(key)) - pad.mButtonFlags |= padFlag; - if (ImGui::IsKeyPressed(key)) - pad.mPressedButtonFlags |= padFlag; - - }; - - KeyFlag(ImGuiKey_K, PAD_BUTTON_A); - KeyFlag(ImGuiKey_J, PAD_BUTTON_B); - KeyFlag(ImGuiKey_L, PAD_BUTTON_X); - KeyFlag(ImGuiKey_I, PAD_BUTTON_Y); - KeyFlag(ImGuiKey_H, PAD_BUTTON_START); - KeyFlag(ImGuiKey_O, PAD_TRIGGER_Z); - KeyFlag(ImGuiKey_Keypad8, PAD_BUTTON_UP); - KeyFlag(ImGuiKey_Keypad2, PAD_BUTTON_DOWN); - KeyFlag(ImGuiKey_Keypad6, PAD_BUTTON_RIGHT); - KeyFlag(ImGuiKey_Keypad4, PAD_BUTTON_LEFT); - - if (ImGui::IsKeyDown(ImGuiKey_W)) { - pad.mMainStickPosY = 1.0f; - pad.mMainStickValue = 1.0f; - pad.mMainStickAngle = 0x8000; - } - - if (ImGui::IsKeyDown(ImGuiKey_S)) { - pad.mMainStickPosY = -1.0f; - pad.mMainStickValue = 1.0f; - pad.mMainStickAngle = 0; - } - - if (ImGui::IsKeyDown(ImGuiKey_D)) { - pad.mMainStickPosX = 1.0f; - pad.mMainStickValue = 1.0f; - pad.mMainStickAngle = 0x4000; - } - - if (ImGui::IsKeyDown(ImGuiKey_A)) { - pad.mMainStickPosX = -1.0f; - pad.mMainStickValue = 1.0f; - pad.mMainStickAngle = -0x4000; - } - - if (ImGui::IsKeyDown(ImGuiKey_Q)) { - pad.mTriggerLeft = 1.0; - pad.mTrigLockL = 1; - pad.mHoldLockL = 1; - } - if (ImGui::IsKeyDown(ImGuiKey_E)) { - pad.mTriggerRight = 1.0; - pad.mTrigLockR = 1; - pad.mHoldLockR = 1; - } -} diff --git a/src/dusk/imgui/ImGuiEngine.cpp b/src/dusk/imgui/ImGuiEngine.cpp index 4b6a7fb531..3e742de853 100644 --- a/src/dusk/imgui/ImGuiEngine.cpp +++ b/src/dusk/imgui/ImGuiEngine.cpp @@ -219,7 +219,7 @@ void ImGuiEngine_AddTextures() { ImGuiEngine::orgIcon = AddTexture("org-icon.png"); } if (ImGuiEngine::duskLogo == 0) { - ImGuiEngine::duskLogo = AddTexture("logo.png"); + ImGuiEngine::duskLogo = AddTexture("logo-mascot.png"); } } } // namespace dusk diff --git a/src/dusk/imgui/ImGuiEventFlags.hpp b/src/dusk/imgui/ImGuiEventFlags.hpp index 694336da03..5c110821f6 100644 --- a/src/dusk/imgui/ImGuiEventFlags.hpp +++ b/src/dusk/imgui/ImGuiEventFlags.hpp @@ -3,6 +3,10 @@ #include #include +#include +#include +#include +#include struct duskImguiEventFlagEntry { uint8_t byteIndex; @@ -839,4 +843,2681 @@ inline const MultiBitEventFlag duskImguiSwappedU16Events[] = { { 0xfd, "Rupees owed to Trill" }, }; -#endif // !DUSK_IMGUI_EVENTFLAGS_HPP \ No newline at end of file +enum class AreaFlagType { Item, Switch, Tbox }; + +struct AreaFlagInd { + constexpr AreaFlagInd() noexcept = default; + constexpr explicit AreaFlagInd(uint16_t areaFlag) noexcept; + constexpr AreaFlagInd(AreaFlagType _type, int _flag) noexcept : type(_type), flagID(_flag) {} + AreaFlagType type; + int flagID; +}; + +constexpr auto byteIndexToAreaFlagType(uint8_t byteIndex) -> AreaFlagType { + if (byteIndex < 2 * sizeof(u32)) { + return AreaFlagType::Tbox; + } else if (byteIndex < 6 * sizeof(u32)) { + return AreaFlagType::Switch; + } else { + return AreaFlagType::Item; + } +}; + +constexpr auto byteIndexToAreaByteIndex(uint8_t byteIndex) -> uint8_t { + if (byteIndex < 2 * sizeof(u32)) { + return byteIndex; + } else if (byteIndex < 6 * sizeof(u32)) { + return byteIndex - 2 * sizeof(u32); + } else { + return byteIndex - 6 * sizeof(u32); + } +}; + +constexpr auto byteIndexToAreaU32Index(uint8_t byteIndex) -> uint8_t { + return byteIndexToAreaByteIndex(byteIndex) / sizeof(u32); +}; + +constexpr auto eventFlagToAreaFlagFormat(uint16_t eventFlag) -> AreaFlagInd { + constexpr auto makeMask = [](uint8_t size) -> uint16_t { return (1 << size) - 1; }; + constexpr size_t areaIndexSize = 5; + + const auto byteInd = eventFlag >> 8; + const auto eventU32Ind = byteIndexToAreaU32Index(byteInd); + const auto relativeByteInd = byteIndexToAreaByteIndex(byteInd); + // if we're looking at 0x580, that would be byte 5, and check if 0x80 is set on that byte + // the event flags are structured differently than area flags + // B is byte index, b is the flag mask to check + // event flags are BBBBBBBB bbbbbbbb + // for area flags, they check bitIndex, not mask, i is index + // also area uses u32 index, not byte index + // area flags are BBBiiiii + // so we need to convert from bit mask to index + // also our byte index has to become a u32 index + + // dividing byte index by sizeof(u32) gets us the u32 index + // but in big endian, the first byte is the highest order byte of the u32 + // so we skip 24 bytes for the first byte, 16 for the second, etc + // essentially (3 - (x % 4)), reversing the modulus, 0=3, 1=2 + const auto bitsToSkip = 8 * ((sizeof(u32) - 1) - (relativeByteInd % sizeof(u32))); + const int areaFlag = ((eventU32Ind) << areaIndexSize) | + ((std::countr_zero(eventFlag) + bitsToSkip) & makeMask(areaIndexSize)); + + return AreaFlagInd{byteIndexToAreaFlagType(byteInd), areaFlag}; +} + +constexpr AreaFlagInd::AreaFlagInd(uint16_t eventFlag) noexcept { + *this = eventFlagToAreaFlagFormat(eventFlag); +} + +struct AreaFlagMultibit { + constexpr AreaFlagMultibit() noexcept = default; + + constexpr explicit AreaFlagMultibit(uint16_t eventFlagMask) noexcept { + const auto byteIndex = eventFlagMask >> 8; + type = byteIndexToAreaFlagType(byteIndex); + index = byteIndexToAreaU32Index(byteIndex); + mask = (eventFlagMask & 0xff) + << (8 * ((sizeof(u32) - 1) - (byteIndexToAreaByteIndex(byteIndex) % sizeof(u32)))); + shift = std::countr_zero(mask); + } + + constexpr explicit AreaFlagMultibit(uint16_t eventFlagMaskHigh, uint16_t eventFlagMaskLow) { + const uint8_t highByteIndex = eventFlagMaskHigh >> 8; + const uint8_t lowByteIndex = eventFlagMaskLow >> 8; + + assert(byteIndexToAreaU32Index(highByteIndex) == byteIndexToAreaU32Index(lowByteIndex)); + const auto byteIndex = byteIndexToAreaU32Index(highByteIndex); + + assert(highByteIndex + 1 == lowByteIndex); + + const uint8_t eventHighMask = eventFlagMaskHigh & 0xff; + const uint8_t eventLowMask = eventFlagMaskLow & 0xff; + assert(eventHighMask != 0); + assert(eventLowMask != 0); + + assert(eventHighMask & 1); + assert(eventLowMask & 0x80); + + const uint16_t combinedMask = (eventHighMask << 8) | eventLowMask; + + const uint32_t finalMask = + combinedMask << (8 * ((sizeof(u32) - 1) - + (byteIndexToAreaByteIndex(lowByteIndex) % sizeof(u32)))); + + assert(std::has_single_bit( + static_cast(finalMask) + (1 << std::countr_zero(finalMask)))); + + type = byteIndexToAreaFlagType(highByteIndex); + index = byteIndexToAreaU32Index(highByteIndex); + mask = finalMask; + shift = std::countr_zero(finalMask); + } + + constexpr AreaFlagMultibit(AreaFlagType _type, uint32_t _mask, uint8_t _index) noexcept + : type(_type), + mask(_mask), + shift(std::countr_zero(_mask)), + index(_index) {} + + uint32_t mask; + uint8_t index; + uint8_t shift; + AreaFlagType type; +}; + +struct EventAreaFlags { + uint8_t byteIndex; + uint8_t bitIndex; + AreaFlagInd flag; + std::string description; + constexpr uint16_t GetFlagID() const { return (byteIndex << 8) | bitIndex; } +}; + +constexpr AreaFlagInd test{0x0b80}; +static_assert(test.type == AreaFlagType::Switch); +static_assert((test.flagID >> 5) == 0); +static_assert((test.flagID & 0x1f) == 7); + +inline EventAreaFlags eventAreaFlagsAG[] = { + { 0x00, 0x80, AreaFlagInd{0x0080}, "Ooccoo Map Flag" }, + { 0x00, 0x40, AreaFlagInd{0x0040}, "fourth small chest epic spinner room" }, + { 0x00, 0x20, AreaFlagInd{0x0020}, "third small chest epic spinner room" }, + { 0x00, 0x10, AreaFlagInd{0x0010}, "first small chest epic spinner room" }, + { 0x00, 0x08, AreaFlagInd{0x0008}, "second small chest epic spinner room" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "PoH first big chest epic spinner room" }, + { 0x00, 0x02, AreaFlagInd{0x0002}, "small chest first west room" }, + { 0x00, 0x01, AreaFlagInd{0x0001}, "small key big chest elevator room B1" }, + { 0x01, 0x80, AreaFlagInd{0x0180}, "small key big chest first room" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "small key small chest fouth east room" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "big key chest" }, + { 0x01, 0x08, AreaFlagInd{0x0108}, "PoH right big chest main room" }, + { 0x01, 0x04, AreaFlagInd{0x0104}, "map left big chest main room" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "west small chest in second west room" }, + { 0x01, 0x01, AreaFlagInd{0x0101}, "north east small chest in second west room" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "Spinner big chest" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "small key small chest first east room B1" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "small key big chest second east room 2F" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "compass big chest second east room 2F" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "big chest first west room" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "killed second poe (triggers flame getting lit cs)" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "killed first poe" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "open gate to spinner in mini-boss room cs" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "unlock exit in mini-boss room (despawns mini-boss)" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "unlocked boss door" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "started pulling chain in second east room 1F (stays set)" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "stairs to lower east room appeared in main room" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "risen tracks on pillar before boss" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "unlocked door in second west room" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "killed second poe (duplicate)" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "Tiny bugs spawn in first room small key area (duplicate)" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "Tiny bugs spawn in first room small key area" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "stairs to lower west room appeared in main room cs" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "turn walls in third room 2B cs" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "turn walls in third room 2B" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "open gates in first room" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "open gates in first room cs" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "Stallord Phase 2" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "killed third poe (triggers flame getting lit cs)" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "Main Hall Dig Spot" }, + { 0x0C, 0x01, AreaFlagInd{0x0C01}, "turned walls in first east room (stays set)" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "turned slab in first east room B1 (stays set)" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "Intro Cutscene" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "unlocked door in second east room 2F" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "open poe door once all poe flames are present (removes poe bodies)" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "poe flame top left present" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "poe flame bottom right present" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "poe flame top right present" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "poe flame bottom left present" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "killed fourth poe (triggers flame getting lit cs)" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "flame from third poe leaving lantern cs" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "killed third poe (duplicate)" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "flame from second poe leaving lantern cs" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "open walls in third east room" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "unlocked door in elevator room 2B" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "unlocked door in first room" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "killed stalfos in second west room" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "killed gibdo in first east room B1" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "reach boss door (unset once you enter)" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "killed third poe (duplicate)" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "unlock door in second room" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "lit right torch in second room" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "lit left torch in second room" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "close spinner slot for walls in third room 2B" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "unlocked door in first east room 1F" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "risen tracks on pillar before boss cs" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "pushed block in first west room" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "taken down the cube in the second east room 1F" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "placed cube at the right spot in the second east room 1F" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "unlock door in fouth east room" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "close poe door (unsets after lighting torches)" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "main room poes taking flames cs" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "explored first room" }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "open exit door in boss room" }, + { 0x15, 0x80, AreaFlagInd{0x1580}, "Midna Text after defeating boss" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "killed first stalfos in room before mini-boss" }, + { 0x15, 0x04, AreaFlagInd{0x1504}, "killed second stalfos in room before mini-boss" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "killed third stalfos in room before mini-boss" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "extend platform to exit in boss room" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "open gate to spinner in mini-boss room cs" }, + { 0x16, 0x40, AreaFlagInd{0x1640}, "open gate to spinner in mini-boss room" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "open wall in third west room cs (unset once you leave)" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "pulled chain in main room" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "pulled the chain in first room (triggers cs)" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "open wall in third west room" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "killed gibdos in third east room" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "Ooccoo Rescued" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "opened gates in room before mini-boss" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "killed fourth poe (duplicate)" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "dig spot in third east room" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "Spinner big chest (set after)" }, +}; + +inline EventAreaFlags eventAreaFlagsCastleTown[] = +{ + { 0x03, 0x02, AreaFlagInd{0x0302}, "small chest on doctor's balcony" }, + { 0x06, 0x10, AreaFlagInd{0x0610}, "tear of light next to Telma's bar" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "killed poe in Jovani's house (set if you dig into the house)" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "explored 3nd room Jovani-Sewers " }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "explored 2nd room Jovani-Sewers " }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "?" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "?" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "?" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "?" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "intro cs twlight" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "midna text after listening to Telma and Ilia spirits during twilight" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "midna breaking castle barrier cs" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "warp you to Lanayru spring (trigger twilight end)" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "fan girl text next to star tent" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "star tent intro cs" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "invisible wall behind doctor gone" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "?" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "water is back in town (twilight)" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "explored east alley that leads south" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "star tent has double clawshot mini-game" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "explored area with star tent" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "explored alley with entrance to Jovani's house" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "explored alley with entrance to agitha's house" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "explored area with entrance to Telma's bar" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "STAR 1 Completed" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "map marker hero's shade" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "malo mart posters on shop" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "magic armor bought malo mart" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "?" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "enter Telma's bar from top intro cs" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "first right door to castle pushed once (default)" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "first left door to castle pushed once (default)" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "first right door to castle pushed twice" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "first right door to castle half closed" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "first left door to castle closed" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "first left door to castle pushed twice" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "Jovani's house intro cs" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "Jovani text after killing poe" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "open path to sewers (Jovani's house)" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "?" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "killed light bug next to Telma's bar" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "Gengle free (Jovani's house)" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "Midna opens map to look for the last light bug" }, +}; + +inline EventAreaFlags eventAreaFlagsCitS[] = +{ + { 0x00, 0x08, AreaFlagInd{0x0008}, "south east underwater big chest outside shop" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "big chest east wing second room 2F" }, + { 0x00, 0x02, AreaFlagInd{0x0002}, "big chest west wing main room 2F" }, + { 0x00, 0x01, AreaFlagInd{0x0001}, "small chest main room 4F outside" }, + { 0x01, 0x80, AreaFlagInd{0x0180}, "small chest west wing north room 2F" }, + { 0x01, 0x40, AreaFlagInd{0x0140}, "center small chest west wing main room 1F" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "small chest west wing main room B1" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "north east small chest west wing main room 1F" }, + { 0x01, 0x08, AreaFlagInd{0x0108}, "small chest east wing second room 1F" }, + { 0x01, 0x04, AreaFlagInd{0x0104}, "small chest east wing second room 2F" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "big chest ouside north wing" }, + { 0x02, 0x80, AreaFlagInd{0x0280}, "small chest west wing main room 3F" }, + { 0x02, 0x40, AreaFlagInd{0x0240}, "small chest main room 3F inside" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "big key chest" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "big chest main room 4F outside" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "small chest west wing main room 2F" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "PoH big chest west wing main room 3F" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "small chest west wing north room 3F" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "PoH big chest west wing north room 2F" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "small key big chest west wing main room 1F" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "compass big chest east wing first room B1" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "south west underwater big chest outside shop" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "map big chest east wing fourth room 1F" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "double clawshot big chest east wing fifth room B3" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "turn on wind in east wing second room 2F" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "open gate in east wing second room 2F" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "explored west wing main room 1F (unset if you enter main room)" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "explored east wing first room 1F (unset if you enter main room)" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "killed big baba west wing north room 1F" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "double clawshot big chest east wing fifth room B3 (set after)" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "west bridge broken" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "east wing fifth room B3 intro cs" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "open gate in east wing third room 2F" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "spawn baba serpants under east bridge" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "extended east bridge (spinner slot needs to be closed)" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "extended west bridge (spinner slot needs to be closed)" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "small key big chest west wing main room 1F (set after)" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "explored main room 1F" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "turn on wind in east wing third room 2F" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "open door in east wing third room 1F" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "fan on ceiling of main room active" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "open gate in east wing fourth room 1F" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "unlock boss door" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "unlock east bridge door 1F" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "east bridge extended (close east spinner slot)" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "west bridge extended (close west spinner slot)" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "open gate outside shop" }, + { 0x0C, 0x08, AreaFlagInd{0x0C08}, "north wing main room intro cs" }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "east wing fourth room 2F intro cs" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "went beyond first gate outside shop intro cs" }, + { 0x0C, 0x01, AreaFlagInd{0x0C01}, "Intro CS" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "killed left dynalfos in east wing third room 1F" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "killed right dynalfos in east wing third room 1F" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "?" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "west bridge destroyed cs (triggered when you have a key)" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "killed helmasaurus in main room 1F" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "west bridge extented cs" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "east bridge extented cs" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "small key big chest west wing main room 1F (set after)" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "exited dungeon with midna warp" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "exited dungeon with midna warp" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "save promt after boss" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "open gate in west wing main room B2" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "killed left aeralfos in north wing main room 1F" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "killed right aeralfos in north wing main room 1F" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "open gate in east wing fifth room B3" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "stop fan in east wing fifth room B3" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "killed third baba serpant under east bridge" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "killed third tile worm in east wing second room 1F" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "killed second tile worm in east wing second room 1F" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "killed first tile worm in east wing second room 1F" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "killed poe in main room 4F outside" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "killed poe in west wing main room 2F" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "small key big chest west wing main room 1F (set after)" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "latched on to first pillar in east wing first room B2" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "open door in east wing fifth room B3" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "?" }, + { 0x15, 0x10, AreaFlagInd{0x1510}, "activate north path fan " }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "killed east dynalfos in main room 3F outside" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "killed north dynalfos in main room 3F outside" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "killed dynalfos in west wing north room 3F" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "killed first baba serpent in path to east spinner slot" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "killed second baba serpent in path to east spinner slot" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "killed first baba serpant under east bridge" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "killed second baba serpant under east bridge" }, +}; + +inline EventAreaFlags eventAreaFlagsCoO[] = +{ + { 0x03, 0x01, AreaFlagInd{0x0301}, "big chest Lanayru Ice Puzzle" }, + { 0x04, 0x80, AreaFlagInd{0x0480}, "big chest right right right path Eldin Long Cave" }, + { 0x04, 0x40, AreaFlagInd{0x0440}, "big chest right right left right path Eldin Long Cave" }, + { 0x04, 0x20, AreaFlagInd{0x0420}, "small chest left right path Eldin Long Cave" }, + //{ 0x08, 0x80, AreaFlagInd{0x0880}, "position of block 1 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x08, 0x40, AreaFlagInd{0x0840}, "position of block 1 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x08, 0x20, AreaFlagInd{0x0820}, "position of block 1 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x08, 0x10, AreaFlagInd{0x0810}, "position of block 1 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x08, 0x08, AreaFlagInd{0x0808}, "position of block 2 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x08, 0x04, AreaFlagInd{0x0804}, "position of block 2 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x08, 0x02, AreaFlagInd{0x0802}, "position of block 2 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x08, 0x01, AreaFlagInd{0x0801}, "position of block 2 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x09, 0x80, AreaFlagInd{0x0980}, "position of block 3 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x09, 0x40, AreaFlagInd{0x0940}, "position of block 3 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x09, 0x20, AreaFlagInd{0x0920}, "position of block 3 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x09, 0x10, AreaFlagInd{0x0910}, "position of block 3 puzzle 2 Lanayru Ice Puzzle" }, + //{ 0x09, 0x04, AreaFlagInd{0x0904}, "position of block 1 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x09, 0x02, AreaFlagInd{0x0902}, "position of block 1 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x09, 0x01, AreaFlagInd{0x0901}, "position of block 1 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x0A, 0x80, AreaFlagInd{0x0A80}, "position of block 1 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x0A, 0x20, AreaFlagInd{0x0A20}, "position of block 2 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x0A, 0x10, AreaFlagInd{0x0A10}, "position of block 2 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x0A, 0x08, AreaFlagInd{0x0A08}, "position of block 2 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x0A, 0x04, AreaFlagInd{0x0A04}, "position of block 2 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x0B, 0x08, AreaFlagInd{0x0B08}, "position of block 3 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x0B, 0x04, AreaFlagInd{0x0B04}, "position of block 3 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x0B, 0x02, AreaFlagInd{0x0B02}, "position of block 3 puzzle 1 Lanayru Ice Puzzle" }, + //{ 0x0B, 0x01, AreaFlagInd{0x0B01}, "position of block 3 puzzle 1 Lanayru Ice Puzzle" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "broke right ice blocking first doorway in Lanayru Ice Puzzle" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "broke left ice blocking first doorway in Lanayru Ice Puzzle" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "open fourth gate in Lanayru Ice Puzzle" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "open third gate in Lanayru Ice Puzzle" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "open second gate in Lanayru Ice Puzzle" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "open first gate in Lanayru Ice Puzzle" }, + //{ 0x0E, 0x40, AreaFlagInd{0x0E40}, "position of block 1 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0E, 0x20, AreaFlagInd{0x0E20}, "position of block 1 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0E, 0x10, AreaFlagInd{0x0E10}, "position of block 1 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0E, 0x08, AreaFlagInd{0x0E08}, "position of block 1 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0E, 0x04, AreaFlagInd{0x0E04}, "position of block 1 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0E, 0x02, AreaFlagInd{0x0E02}, "position of block 2 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0E, 0x01, AreaFlagInd{0x0E01}, "position of block 2 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0F, 0x80, AreaFlagInd{0x0F80}, "position of block 2 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0F, 0x40, AreaFlagInd{0x0F40}, "position of block 2 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0F, 0x20, AreaFlagInd{0x0F20}, "position of block 2 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0F, 0x10, AreaFlagInd{0x0F10}, "position of block 3 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0F, 0x08, AreaFlagInd{0x0F08}, "position of block 3 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0F, 0x04, AreaFlagInd{0x0F04}, "position of block 3 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0F, 0x02, AreaFlagInd{0x0F02}, "position of block 3 puzzle 3 Lanayru Ice Puzzle" }, + //{ 0x0F, 0x01, AreaFlagInd{0x0F01}, "position of block 3 puzzle 3 Lanayru Ice Puzzle" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "spawn big chest right right left right path Eldin Long Cave" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "lit torch 3 (right right right path) Eldin Long Cave" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "killed poe on floor 44 in Cave of Ordeals" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "killed poe on floor 33 in Cave of Ordeals" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "killed poe on floor 17 in Cave of Ordeals" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "Cave of Ordeals intro cs" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "obtained fairy's tears (if unset, you can get it even if you already have some) (also spawns that extra darknut on floor 49)" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "open floor 1 door in Cave of Ordeals cs" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "broke ice on floor 21 in Cave of Ordeals" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "explored section 2 right right left right path Eldin Long Cave" }, + { 0x14, 0x20, AreaFlagInd{0x1420}, "explored section 1 right right left right path Eldin Long Cave" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "explored section 2 right right left left path Eldin Long Cave" }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "explored section 1 right right left left path Eldin Long Cave" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "explored section 3 right right left path Eldin Long Cave" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "explored section 2 right right left path Eldin Long Cave" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "explored section 1 right right left path Eldin Long Cave" }, + { 0x15, 0x80, AreaFlagInd{0x1580}, "explored section 2 right right right path Eldin Long Cave" }, + { 0x15, 0x40, AreaFlagInd{0x1540}, "explored section 1 right right right path Eldin Long Cave" }, + { 0x15, 0x20, AreaFlagInd{0x1520}, "explored section 3 right right path Eldin Long Cave" }, + { 0x15, 0x10, AreaFlagInd{0x1510}, "explored section 2 right right path Eldin Long Cave" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "explored section 1 right right path Eldin Long Cave" }, + { 0x15, 0x04, AreaFlagInd{0x1504}, "explored section 1 right left path Eldin Long Cave" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "explored section 6 right path Eldin Long Cave" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "explored section 5 right path Eldin Long Cave" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "explored section 4 right path Eldin Long Cave" }, + { 0x16, 0x40, AreaFlagInd{0x1640}, "explored section 3 right path Eldin Long Cave" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "explored section 2 right path Eldin Long Cave" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "explored section 1 right path Eldin Long Cave" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "explored section 2 left right path Eldin Long Cave" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "explored section 1 right right path Eldin Long Cave" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "explored section 2 left left path Eldin Long Cave" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "explored section 1 right right path Eldin Long Cave" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "explored section 2 left path Eldin Long Cave" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "explored section 1 right path Eldin Long Cave" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "explored section 2 Eldin Long Cave" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "explored section 1 Eldin Long Cave" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "lit torch 2 (right path) Eldin Long Cave" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "lit left torch right right left right path Eldin Long Cave" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "lit right torch right right left right path Eldin Long Cave" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "killed poe right right left left path Eldin Long Cave" }, +}; + +inline EventAreaFlags eventAreaFlagsEldin[] = +{ + { 0x00, 0x01, AreaFlagInd{0x0001}, "Big chest from torches graveyard" }, + { 0x01, 0x80, AreaFlagInd{0x0180}, "Small chest in the inn" }, + { 0x01, 0x40, AreaFlagInd{0x0140}, "PoH big chest death mountain archer alcove" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "PoH big chest underwater behind spring" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "Big chest next to watch tower" }, + { 0x01, 0x04, AreaFlagInd{0x0104}, "?" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "Big chest watch house" }, + { 0x01, 0x01, AreaFlagInd{0x0101}, "tear of light death mountain hot spring water" }, + { 0x02, 0x80, AreaFlagInd{0x0280}, "tear of light next to howling stone death mountain " }, + { 0x02, 0x40, AreaFlagInd{0x0240}, "tear of light death mountain base" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "tear of light sancuary basement" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "tear of light Barnes storage house" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "tear of light watch tower" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "tear of light at Malo mart" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "tear of light inn fire room" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "tear of light Barnes shop" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "tear of light graveyard" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "tear of light Barnes storage house" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "tear of light Barnes storage house" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "tear of light sancuary basement" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "tear of light sancuary basement" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "tear of light ant house" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "tear of light inn back room" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "Kakariko Village Portal" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "kakariko shadow beasts barrier appears (unset after fight)" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "DM big rock fell" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "done midna jumps 4 death mountain (hot spring water)" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "done midna jumps 3 death mountain (1st floor)" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "killed death mountain shadow beasts cs (unset later?)" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "done midna jumps 1 death mountain (entrance)" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "Malo mart has banners inside" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "DM proper human intro CS (Map marker GM)" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "Death Mountain Portal" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "death mountain dark beasts barrier appears" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "?" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "?" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "killed HV bublins (unset when cats spawn)" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "killed light bug death mountain hot spring water" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "killed light bug next to howling stone death mountain " }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "killed light bug death mountain base" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "trigger twilight end cs (vessel of light filled) (Hot Spring Water Present on map)" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "killed light bug sancuary basement" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "killed light bug watch tower" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "killed light bug malo mart" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "killed light bug inn fire room" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "killed light bug Barnes shop" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "killed light bug graveyard" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "killed light bug sancuary basement" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "killed light bug sancuary basement" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "killed light bug ant house" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "killed light bug inn bedroom" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "saw light bug enter Barnes storage house twilight cs" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "malo mart sells hawkeye (moves potion to the right) (set when starting bow mini-game)" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "malo mart sells arrows" }, + { 0x0C, 0x10, AreaFlagInd{0x0C10}, "broke sanctuary roof patch during twilight (duplicate)" }, + { 0x0C, 0x08, AreaFlagInd{0x0C08}, "box moved ant house (spawns light bug)" }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "Unknown crashes if rocks are pushed" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "malo mart hylian shield bought" }, + { 0x0C, 0x01, AreaFlagInd{0x0C01}, "ant house explored" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "entered sanctuary from roof during twilight" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "enter Barnes shop through the window in twilight cs" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "light bug comes out of box in ant house cs" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "spawn PoH cat mini game hidden village" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "malo mart hawkeye sold out (needs to be set for arrows to show up)" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "lit all torches in sanctuary twilight (opens basment)" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "light bug in inn fireplace jumps out cs" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "lit inn fireplace" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "knocked down dresser in Barnes shop twilight cs" }, + { 0x0E, 0x40, AreaFlagInd{0x0E40}, "?" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "? (malo mart explored)" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "lit west torch in sanctuary twilight" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "lit north east torch in sanctuary twilight" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "lit east torch in sanctuary twilight" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "lit north west torch in sanctuary twilight" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "broke ant house roof patch (duplicate)" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "moved death mountain rock to exit" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "Eldin spirit talks to Link after getting kakariko Portal cs" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "broke ant house roof patch" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "broke window of Barnes shop twilight" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "broke sanctuary roof patch during twilight" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "Renado talks to Link after trying to go up death mountain trail" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "killed kakariko shadow beasts cs" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "Killed poe watch tower" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "Killed poe Barnes storage house" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "blown up rock underwater to lake hylia from graveward" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "Graveyard intro cs" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "explored zora tombstone area" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "broke floor patch in graveyard during twilight" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "Killed poe death mountain" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "Killed poe graveyard center" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "Killed poe graveyard tombstone" }, + { 0x11, 0x40, AreaFlagInd{0x1140}, "gorons in kakariko hot spring water gone" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "Map Marker Sanctuary" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "Midna text after DM big rock fell" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "done midna jumps 2 death mountain (base floor)" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "death mountain proper intro cs twilight" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "moved death mountain rock to hot spring water" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "sanctuary basement canon room explored" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "done Midna jumps in sanctuary basement" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "Midna text before Midna jumps in sanctuary basement" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "sanctuary twilight cs" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "killed right shadow bulblin inside inn twilight" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "killed left shadow bulblin inside inn twilight" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "killed top shadow bulblin inside inn twilight" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "enter central room of inn during twilight cs" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "sanctuary basement hallway to canon explored" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "Malo mart shield on counter (requires reload)" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "Kakariko intro cs" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "blown up rocks on kakariko cliff PoH" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "Blown up rock to secret passage to back of spring (updates map)" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "Barnes storage house not blown up (unset once you enter) (twilight)" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "Barnes storage house blew up cs (also changes map)" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "Killed poe hidden village" }, + { 0x14, 0x80, AreaFlagInd{0x1480}, "Midna text after sanctuary twilight cs" }, + { 0x14, 0x20, AreaFlagInd{0x1420}, "light bug comes out of dresser in Barnes shop twilight" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "GM save prompt" }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "Barnes sells Bombs" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "midna text after shad leaves canon room" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "Hero's shade map marker" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "done midna jumps to top of sanctuary (also unlocks it)" }, + { 0x15, 0x80, AreaFlagInd{0x1580}, "blown up rock underwater behind spring" }, + { 0x15, 0x40, AreaFlagInd{0x1540}, "malo mart sells red potion on right slot" }, + { 0x15, 0x20, AreaFlagInd{0x1520}, "beat bow mini-game" }, + { 0x15, 0x10, AreaFlagInd{0x1510}, "malo mart sells red potion on left slot (default)" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "explored bedroom in the inn" }, + { 0x15, 0x04, AreaFlagInd{0x1504}, "explored central room in the inn" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "blown up right rock underwater zora tombstone area" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "midna text after charging dominion rod" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "Goron lets you enter elevator in sumo hall" }, + { 0x16, 0x40, AreaFlagInd{0x1640}, "Midna text after blowing up Barnes storage house" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "hit third target in bow mini-game" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "saved Colin cs (malo mart now open)" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "Midna text after meeting the kids after twilight" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "Started Rutela escort" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "hit second target in bow mini-game" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "hit first target in bow mini-game" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "rock to zora tombstone gone + Barnes sells water bombs" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "Followed Rutela to graveyard" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "death mountain path intro cs twilight" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "owl statue possessed cs (sanctuary basement)" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "done midna jumps ant house (also unlocks it)" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "Impaz in her house (hidden village)" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "can't transform in hidden village (if bit 2 is set you can)" }, + { 0x1A, 0x40, AreaFlagInd{0x1A40}, "picked up rupees from right rock underwater zora tombstone area" }, + { 0x1A, 0x20, AreaFlagInd{0x1A20}, "picked up rupees from rock underwater behind spring" }, + { 0x1A, 0x10, AreaFlagInd{0x1A10}, "Free standing PoH kakariko cliff" }, + { 0x1A, 0x08, AreaFlagInd{0x1A08}, "Free standing PoH cat mini game hidden village" }, + { 0x1A, 0x04, AreaFlagInd{0x1A04}, "picked up silver rupee from bell above sanctuary" }, + { 0x1A, 0x02, AreaFlagInd{0x1A02}, "picked up yellow rupee in box secret passage to spring" }, + { 0x1A, 0x01, AreaFlagInd{0x1A01}, "picked up blue rupee in box secret passage to spring" }, + { 0x1B, 0x80, AreaFlagInd{0x1B80}, "picked up yellow rupee in box next to kakariko hot spring water" }, + { 0x1B, 0x40, AreaFlagInd{0x1B40}, "picked up yellow rupee behind dresser inn bedroom" }, + { 0x1B, 0x20, AreaFlagInd{0x1B20}, "picked up red rupee in box on platform above ant house" }, + { 0x1B, 0x10, AreaFlagInd{0x1B10}, "picked up blue rupee behind dresser inn 2nd floor" }, + { 0x1B, 0x08, AreaFlagInd{0x1B08}, "picked up yellow rupee 1 on leadge of top of death montain" }, + { 0x1B, 0x04, AreaFlagInd{0x1B04}, "picked up yellow rupee 2 on leadge of top of death montain" }, + { 0x1B, 0x02, AreaFlagInd{0x1B02}, "picked up yellow rupee 3 on leadge of top of death montain" }, + { 0x1B, 0x01, AreaFlagInd{0x1B01}, "picked up rupee in death montain in rock below pipe (need BaC and rang)" }, +}; + +inline EventAreaFlags eventAreaFlagsFaron[] = +{ + { 0x00, 0x80, AreaFlagInd{0x0080}, "small chest in lantern cave" }, + { 0x00, 0x40, AreaFlagInd{0x0040}, "HP Chest Coro-Mist shortcut" }, + { 0x00, 0x20, AreaFlagInd{0x0020}, "big chest mist area" }, + { 0x00, 0x10, AreaFlagInd{0x0010}, "small chest mist area trunk" }, + { 0x00, 0x08, AreaFlagInd{0x0008}, "small chest mist area next to key cave" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "PoH chest key cave" }, + { 0x00, 0x01, AreaFlagInd{0x0001}, "Small key cave key chest" }, + { 0x01, 0x80, AreaFlagInd{0x0180}, "Dig Tear #1" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "Final Tear #1" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "Final Tear #2" }, + { 0x01, 0x04, AreaFlagInd{0x0104}, "Wall Tear #1 (Mist Area)" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "Wall Tear #2 (Mist Area)" }, + { 0x02, 0x40, AreaFlagInd{0x0240}, "Tree Tear #1" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "Tree Tear #2" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "Tree Tear #3" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "Gate Tear #1" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "Coro Tear #1 (inside)" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "Dig Tear #2" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "Tear #2" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "Gate Tear #2" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "Coro Tear #2 (inside)" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "Tear #1" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "Coro Tear (outisde)" }, + { 0x04, 0x80, AreaFlagInd{0x0480}, "Yellow Rupee Chest (N-Faron)" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "explored east section of mist area after midna jump 1 twilight (gets unset)" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "explored section with south entrance of mist area (gets unset)" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "went up east section of mist area after midna jump 1 twilight" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "S Faron warp twilight fences fall cs" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "Sky character under owl statue" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "midna jump 1 mist area (duplicate)" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "unlock midna jump 1 mist area" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "Coro spirits talk after killing light bugs in his house" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "midna jump to Coro's house" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "Map marker owl statue" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "opened mist area gate to N faron" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "lit torch 2 in lantern cave" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "Talked to Midna next to the deku babas before Coro" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "Rescued Monkey from Puppets" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "dug behind Coro gate in twilight" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "took Midna back s warp fight" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "S warp shadow beast revive cs" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "trigger twilight end cs (vessel of light filled)" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "unlocked Coro gate" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "Explored path to sacred grove" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "burned cobweb in front of forest temple" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "see spirit in front of FT twilight cs" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "enter twilight cs" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "talked to the light spirit in twilight" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "saw light bugs dig underground mist area exit twilight" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "Blown up rock next to Coro" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "n warp fight fences appear cs" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "North Faron Portal" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "FT save prompt" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "got lantern back after monkey follow" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "saw light bugs move inside Coro's house cs" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "in monkey follow (gets unset afterwards)" }, + { 0x0C, 0x10, AreaFlagInd{0x0C10}, "Trill lets you shop" }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "killed left bokoblin before Trill after twilight" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "killed right bokoblin before Trill after twilight" }, + { 0x0C, 0x01, AreaFlagInd{0x0C01}, "explored section 6 in lantern cave" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "explored section 5 in lantern cave" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "explored section 4 in lantern cave" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "explored section 3 in lantern cave" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "explored section 2 in lantern cave" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "explored section 1 in lantern cave" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "got Coro key from Coro" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "?" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "killed right bokoblin next to Talo's cage" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "killed left bokoblin next to Talo's cage" }, + { 0x0E, 0x40, AreaFlagInd{0x0E40}, "broke Talo's cage" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "killed both bokoblins next to Talo's cage" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "enter Faron intro cs" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "lit Coro's soup" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "saved Talo cs" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "see light spirit from far away in twilight cs" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "enter mist area twilight cs" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "found Talo's stick (child chase)" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "explored top of east slope mist area twilight" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "talked to Coro spirit before killing bugs" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "entered mist area as human" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "Talked to midna before talking to Coro spirit" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "explored section with north exit of mist area (gets unset)" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "saw light bugs on trunk mist area (gets unset)" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "Map marker Rusl" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "lit torch 1 in lantern cave" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "Killed poe mist area" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "spawn PoH chest key cave" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "lit right torch in key cave" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "lit left torch in key cave" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "explored section 15 in lantern cave" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "add mist area mist to mini-map" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "explored section 14 in lantern cave" }, + { 0x11, 0x40, AreaFlagInd{0x1140}, "explored section 13 in lantern cave" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "explored section 12 in lantern cave" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "explored section 11 in lantern cave" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "explored section 10 in lantern cave" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "explored section 9 in lantern cave" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "explored section 3 of branching path in lantern cave" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "explored section 8 in lantern cave" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "explored section 7 in lantern cave" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "Midna text before jumping to Lost Woods" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "killed light bug outside Coro's house (duplicate)" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "saw light bug run behind Coro gate" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "Midna text warp to N faron for bridge" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "saw first 2 light bugs run away from you" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "S warp shadow beasts are spawned" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "South Faron Portal" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "explored section 2 of branching path in lantern cave" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "killed right bokoblin in front of FT after twilight" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "killed left bokoblin in front of FT after twilight" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "explored section 1 of branching path in lantern cave" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "S warp shadow beasts killed" }, + { 0x14, 0x80, AreaFlagInd{0x1480}, "lit torch 4 in lantern cave" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "killed light bug mist area north exit" }, + { 0x14, 0x20, AreaFlagInd{0x1420}, "did midna jump 2 mist area" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "killed light bug mist area north exit" }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "explored section 4 of branching path in lantern cave" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "killed light bug in front of Forest Temple" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "killed light bug in front of Forest Temple" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "killed both light bugs in front of FT cs (map marker FT)" }, + { 0x15, 0x80, AreaFlagInd{0x1580}, "killed light bug mist area south entrance" }, + { 0x15, 0x40, AreaFlagInd{0x1540}, "killed light bug mist area south entrance" }, + { 0x15, 0x20, AreaFlagInd{0x1520}, "did midna jump 5 mist area" }, + { 0x15, 0x10, AreaFlagInd{0x1510}, "did midna jump 3 mist area" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "killed light bug mist area trunk" }, + { 0x15, 0x04, AreaFlagInd{0x1504}, "killed light bug mist area trunk" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "killed light bug mist area trunk" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "killed light bug between latern cave and gate" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "map marker Talo (only during child save)" }, + { 0x16, 0x40, AreaFlagInd{0x1640}, "killed light bug outside Coro's house" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "Map marker key cave (only during child save)" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "remove map marker key cave" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "did midna jump 4 mist area" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "killed light bug hallway to Coro (closer to spring)" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "killed light bug between latern cave and gate" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "killed both light bugs in Coro's house cs" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "killed light bug inside Coro's house" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "killed light bug inside Coro's house" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "killed light bug hallway to Coro (closer to Coro)" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "did midna jump 6 mist area" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "explored east section of mist area after midna jump 1 twilight (gets unset)" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "lit torch 3 in lantern cave" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "burned cobweb 2 in lantern cave" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "burned cobweb 1 in lantern cave" }, + { 0x1B, 0x01, AreaFlagInd{0x1B01}, "picked up rupees from rock next to Coro" }, +}; + +inline EventAreaFlags eventAreaFlagsFishingPond[] = +{ + { 0x1B, 0x01, AreaFlagInd{0x1B01}, "free standing PoH" }, +}; + +inline EventAreaFlags eventAreaFlagsFT[] = +{ + { 0x00, 0x80, AreaFlagInd{0x0080}, "PoH big chest behind Deku Like" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "big chest underwater Tiny Cave" }, + { 0x00, 0x02, AreaFlagInd{0x0002}, "small key big chest on pillar tile worm room" }, + { 0x00, 0x01, AreaFlagInd{0x0001}, "Small Chest tile worm room" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "big chest compass main room" }, + { 0x01, 0x08, AreaFlagInd{0x0108}, "PoH big chest behind stairs tile worm room" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "saved monkey in front of big pit" }, + { 0x02, 0x80, AreaFlagInd{0x0280}, "saved monkey in boomerang bombling room" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "saved monkey in tile worm room" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "saved monkey in big baba room" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "saved 2nd monkey" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "first Monkey saved" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "Small Chest 2nd Monkey room" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "small chest first room" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "Big Baba key acquired" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "small key big chest in single wind bridge room" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "small key big chest in boomerang bombling room" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "Big Chest in broken stairs room" }, + { 0x06, 0x01, AreaFlagInd{0x0601}, "Ooccoo map marker" }, + { 0x07, 0x80, AreaFlagInd{0x0780}, "big chest with map main room" }, + { 0x07, 0x40, AreaFlagInd{0x0740}, "Big Key Chest" }, + { 0x07, 0x20, AreaFlagInd{0x0720}, "small chest behind bombable wall main room" }, + { 0x07, 0x04, AreaFlagInd{0x0704}, "Outside Monkey saved" }, + { 0x07, 0x02, AreaFlagInd{0x0702}, "saved monkey in spider cave room" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "Midna Boomerang text seen" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "midna text after bridge before Ook broken" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "Big Baba killed cutscene (still spawned)" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "hit totem of 2nd monkey once cs" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "Midna first monkey text end" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "Boss Door opened" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "bridge before Ook broken" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "burned cob web big key room" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "bridge 2nd monkey room broken" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "Compass Chest knocked down" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "Monkey freed in spider room" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "unlocked door to 2nd monkey room" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "broke cage of 2nd monkey cs" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "bokoblins see you in single wind bridge room cs" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "turned wind bridge in single wind bridge room cs" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "Unlock Windbridge East Door" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "Girl Monkey helps Link" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "opened cage of monkey in big baba room" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "killed higher bokoblin in room before boss" }, + { 0x0C, 0x10, AreaFlagInd{0x0C10}, "killed lower bokoblin in room before boss" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "blown up rock to Ooccoo" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "blown up rock blocking tile worm room" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "blown up main room bombable wall" }, + //{ 0x0D, 0x20, AreaFlagInd{0x0D20}, "small key big chest tile worm room position" }, + //{ 0x0D, 0x10, AreaFlagInd{0x0D10}, "small key big chest tile worm room position" }, + //{ 0x0D, 0x08, AreaFlagInd{0x0D08}, "small key big chest tile worm room position" }, + //{ 0x0D, 0x04, AreaFlagInd{0x0D04}, "small key big chest tile worm room position" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "Staircase rises main room" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "tile worm room intro cs" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "opened cage of monkey in tile worm room" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "gate to big key opened" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "2nd Monkey room intro cs" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "lit bottom right torch main room" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "lit bottom left torch main room" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "lit upper right torch main room" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "lit upper left torch main room" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "big baba room intro cs" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "enter room before boss Midna text" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "Midna text after saveing a monkey after Ook" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "Small Bridge breaks scene seen" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "enter southwest door outside intro cs" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "Midna text after compass" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "boomerang obtained" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "burned west cob web main room" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "5th monkey added to room before boss" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "4 Monkeys in main room cutscene" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "turned wind bridge in single wind bridge room" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "Monkeys regroup in Main Room" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "4 Monkeys gather in Main Room" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "blown up 1st rock in boomeang bombling room" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "Ooccoo Freed" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "2nd Monkey Bokoblins Killed" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "Midna first Monkey text prompt (makes vines climbable)" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "killed big baba cs" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "?" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "?" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "Diababa killed" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "Diababa Phase 2 (unset post kill)" }, + { 0x14, 0x20, AreaFlagInd{0x1420}, "broke bridge over water that leads to big key" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "monkeys form a rope in room before boss cs" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "monkeys start to form a rope in room before boss" }, + { 0x15, 0x80, AreaFlagInd{0x1580}, "All Monkeys form rope" }, + { 0x15, 0x40, AreaFlagInd{0x1540}, "Ook exit door opened" }, + { 0x15, 0x20, AreaFlagInd{0x1520}, "opened gate to monkey in broken stairs room" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "blown up 2nd rock in boomerang bombling room" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "saved 5th monkey cs" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "killed bokoblin next to first monkey" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "see locked monkey post Ook" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "Ook defeated" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "monkeys form a rope in room before boss cs" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "Staircase rising cutscene for 4th Monkey" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "Intro Cutscene" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "2nd Monkey Pillar scene" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "enter room before boss intro cs" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "turned mill for the first time in Ook room cs" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "big chest compass main room (set after)" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "hit totem of 2nd monkey once" }, + { 0x1B, 0x10, AreaFlagInd{0x1B10}, "picked up red rupee from deku like in spider room" }, +}; + +inline EventAreaFlags eventAreaFlagsGerudoDesert[] = +{ + { 0x02, 0x80, AreaFlagInd{0x0280}, "big chest next to Arbiter's Grounds" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "small chest next to camp entrance" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "small chest on pillar weat of entrance to desert" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "small chest under tower of left bulblin outside camp" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "small chest under tower of right bulblin outside camp" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "small chest next to fire outside camp" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "small chest island north of cave of ordeals" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "small chest east canyon" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "small chest next to pillar in the middle of the messa" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "small chest behind 2 breakable walls (the west one)" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "small chest behind 2 breakable walls (the east one)" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "big chest owl statue" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "big chest in south secret path" }, + { 0x04, 0x80, AreaFlagInd{0x0480}, "both small chests and key in camp (each one sets it)" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "?" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "?" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "explored island north of cave of ordeals" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "explored part 9 of bulblin camp (KB battle area)" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "explored part 8 of bulblin camp" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "explored part 7 of bulblin camp" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "explored part 6 of bulblin camp" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "explored part 5 of bulblin camp" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "Desert Intro CS (PoT)" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "Gerudo Messa Portal" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "explored part 2 of bulblin camp" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "explored part 4 of bulblin camp" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "explored part 3 of bulblin camp" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "?" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "explored south secret path to big chest" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "Mirror chamber intro cs" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "save prompt after beating Arbiter's Grounds" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "killed archer bulblin on bore outside camp" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "killed rider bulblin on bore outside camp" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "killed lone bulblin on bore outside camp" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "killed bulblin on left tower outside camp" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "killed bulblin on right tower outside camp" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "watched outside AG cutscene after bublin camp" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "destroyed fire outside camp" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "destroyed bore meat in camp" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "killed poe next to entrance to camp" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "Hero's Shade map marker" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "spawn big chest next to Arbiter's Grounds" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "lit right torch for big chest next to Arbiter's Grounds entrance" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "lit left torch for big chest next to Arbiter's Grounds entrance" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "explored area with Arbiter's Gounds entrance" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "mirror light stairs appear" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "Mirror Chamber Portal" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "sky character under owl statue" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "map marker owl statue" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "Mirror Raised Cutscene Flag (Places Boar at desert entrance)" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "broke left wall that prevents access to east small chest (set once pieces despawn)" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "killed poe next to cave of ordeals" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "killed poe south of desert entrance" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "killed poe on elevated platform with grotto (messa)" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "killed poe next to Arbiter's Grounds entrance" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "destroyed tower of right bulbin outside camp" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "destroyed tower of left bulbin outside camp" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "broke first right wall to camp (set once pieces despawn)" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "broke first middle wall to camp (set once pieces despawn)" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "broke first left wall to camp (set once pieces despawn)" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "broke second right wall to camp (set once pieces despawn)" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "broke second left wall to camp (set once pieces despawn)" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "explored elavated section with poe over grotto" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "Desert Intro CS" }, + { 0x14, 0x80, AreaFlagInd{0x1480}, "started climbing stairs to mirror chamber" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "exit Arbiters Grounds to go to mirror chamber" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "map marker palace of twilight (explored mirror chamber)" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "killed poe in bulblin camp" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "broke right wall that prevents access to east small chest (set once pieces despawn)" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "broke left wall that prevents access to west small chest (set once pieces despawn)" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "broke right wall that prevents access to west small chest (set once pieces despawn)" }, + { 0x18, 0x80, AreaFlagInd{0x1880}, "free standing PoH bore meat bulblin camp" }, +}; + +inline EventAreaFlags eventAreaFlagsGM[] = +{ + { 0x00, 0x40, AreaFlagInd{0x0040}, "big chest main room top floor" }, + { 0x00, 0x10, AreaFlagInd{0x0010}, "small chest next to switch in toadpoli room" }, + { 0x00, 0x08, AreaFlagInd{0x0008}, "small chest first room" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "big chest elder 3 room" }, + { 0x00, 0x02, AreaFlagInd{0x0002}, "small chest elder 2 room" }, + { 0x00, 0x01, AreaFlagInd{0x0001}, "small chest elder 1 room" }, + { 0x01, 0x40, AreaFlagInd{0x0140}, "Ooccoo Map Flag" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "big chest underwater outside room" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "PoH big chest room 3" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "map big chest elder 1 room" }, + { 0x01, 0x01, AreaFlagInd{0x0101}, "small key underwater big chest toadpoli room" }, + { 0x02, 0x80, AreaFlagInd{0x0280}, "small key small chest outside room" }, + { 0x02, 0x40, AreaFlagInd{0x0240}, "big chest outside room clawshot" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "small chest room leading to elder 2" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "compass big chest" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "small key big chest main room bottom floor" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "PoH big chest toadpoli room (the one after the gate)" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "big chest bow" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "cut rope of door outside room cs" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "pressed second button main room floor 2 cs" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "pressed third button in first room cs" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "pressed second button in first room cs" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "cut rope of door in toadpoli room cs" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "unlocked boss door" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "pressed first button main room floor 2 cs" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "killed beamos outside room" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "unlock mini-boss doors" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "pressed button outside room" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "cut rope of door in room before boss" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "cut rope of door in toadpoli room" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "cut rope of door in bow room" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "cut rope of door at top floor of main room" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "pressed first button main room floor 2" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "reach west locked door in main room bottom floor" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "lowered platform to open gate in first room (read only)" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "unlocked north door in toadpoli room" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "pressed second button main room floor 2" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "unlock west locked door in main room bottom floor" }, + { 0x0C, 0x08, AreaFlagInd{0x0C08}, "killed right beamos in toadpoli room" }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "killed left beamos in toadpoli room" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "pressed button outside room cs" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "pressed button at top floor of main room" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "unlocked east door outside room" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "broke underwater wood barrier outside room" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "replace tektites with toadpolis in toadpoli room (set after obtaining bow)" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "main room intro cs" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "main room intro cs" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "broke second wood barrier in outside room" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "broke first wood barrier in outside room" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "Oocco Freed" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "elder 2 lets you climb ladder in his room" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "elder 1 lets you climb ladder in his room" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "pulled beamos outside room" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "outside room intro cs" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "outside room getting shot at after mini-boss cs" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "outside room getting shot at after mini-boss cs" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "outside room intro cs" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "room after bow intro cs" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "pulled beamos outside room cs" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "activate first force field in toadpoli room" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "pressed first button in toadpoli room cs" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "open gate in toadpoli room cs" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "pressed second button in toadpoli room cs" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "activate second force field in toadpoli room" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "room before elder 1 intro cs" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "room before elder 1 intro cs" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "intro cs" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "pressed first button in first room cs" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "open gate in first room cs" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "pressed button in room after bow" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "hit switch in room after bow" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "press button room 3" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "killed south-east beamos bow room" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "killed south-west beamos bow room" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "killed west beamos bow room" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "killed north-east beamos bow room" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "killed north-west beamos bow room" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "killed east beamos bow room" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "killed south beamos bow room" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "pulled south-east beamos bow room" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "pulled south-west beamos bow room" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "pulled west beamos bow room" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "pulled east beamos bow room" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "pulled south beamos bow room" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "pressed first button in toadpoli room" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "pressed second button in toadpoli room" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "reached area past pullable wall in room 3 (first in path to elder 1)" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "reached bottom of water in room before elder 1" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "reached area past pullable wall in room 3 (first in path to elder 1)" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "knocked down fence in room after bow" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "pullable wall in room 3 closed itself" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "outside room killed leader bulblin archer" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "cut rope of door outside room" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "pressed unerwater button in room before elder" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "main room floor 2 intro cs" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "main room floor 2 intro cs" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "hit switch in room after bow cs" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "room after bow intro cs" }, +}; + +inline EventAreaFlags eventAreaFlagsGrotto[] = +{ + { 0x01, 0x20, AreaFlagInd{0x0120}, "big chest grotto 5-4" }, + { 0x01, 0x08, AreaFlagInd{0x0108}, "big chest grotto 4-3" }, + { 0x01, 0x04, AreaFlagInd{0x0104}, "left small chest grotto 2-0" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "right small chest grotto 2-0" }, + { 0x01, 0x01, AreaFlagInd{0x0101}, "small chest grotto 5-3" }, + { 0x02, 0x80, AreaFlagInd{0x0280}, "big chest grotto 4-2" }, + { 0x02, 0x40, AreaFlagInd{0x0240}, "big chest grotto 3-0" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "south small chest grotto 2-1" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "east small chest grotto 2-1" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "north small chest grotto 2-1" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "small chest grotto 1-1" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "big chest grotto 4-1" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "big chest grotto 1-2" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "big chest grotto 1-1" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "big chest grotto 5-2" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "big chest grotto 4-0" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "big chest grotto 2-2" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "big chest grotto 2-0" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "big chest grotto 5-0" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "big chest grotto 1-0" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "lit right torch grotto 1-1" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "lit left torch grotto 1-1" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "killed red chu-chu grotto 3-3" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "killed chu-chu grotto 3-2" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "killed blue chu-chu grotto 3-1" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "killed chu-chu grotto 2-0" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "killed chu-chu grotto 1-2" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "killed right freezard grotto 4-2" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "broke ice wall 1 grotto 4-2" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "killed chu-chu grotto 1-1" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "killed all enemies grotto 5-4 (spawm chest)" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "killed left freezard grotto 4-2" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "killed blue chu-chu grotto 3-3" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "broke ice wall 2 grotto 4-2" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "broke ice wall 4 grotto 4-2" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "killed poe in the back entrance grotto 3-0" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "killed poe next to entrance grotto 3-0" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "broke ice wall 3 grotto 4-2" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "broke ice wall 6 grotto 4-2" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "broke ice wall 5 grotto 4-2" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "killed middle poe grotto 1-3" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "killed right poe grotto 1-3" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "spawn big chest grotto 1-1" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "killed all enemies grotto 4-1 (spawm chest)" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "killed middle freezard grotto 4-2" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "blown up rocks in grotto 3-2" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "killed all enemies grotto 5-2 (spawm chest)" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "killed all enemies grotto 4-0 (spawm chest)" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "killed all enemies grotto 2-2 (spawm chest)" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "killed all enemies grotto 2-0 (spawm chest)" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "killed all enemies grotto 5-0 (spawm chest)" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "killed all enemies grotto 1-0 (spawm chest)" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "spawn big chest grotto 4-3" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "lit torch 3 grotto 4-3" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "lit torch 2 grotto 4-3" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "lit torch 1 grotto 4-3" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "spawn big chest grotto 3-0" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "lit right torch grotto 3-0" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "lit middle torch grotto 3-0" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "lit left torch grotto 3-0" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "spawn big chest grotto 1-2" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "lit middle torch grotto 1-2" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "lit right torch grotto 1-2" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "lit left torch grotto 1-2" }, + { 0x1A, 0x02, AreaFlagInd{0x1A02}, "picked up yellow rupee from pot grotto 1-2" }, + { 0x1A, 0x01, AreaFlagInd{0x1A01}, "picked up blue rupee from pot grotto 1-2" }, + { 0x1B, 0x80, AreaFlagInd{0x1B80}, "picked up blue rupee from pot grotto 1-2" }, + { 0x1B, 0x40, AreaFlagInd{0x1B40}, "picked up blue rupee from pot grotto 1-2" }, + { 0x1B, 0x20, AreaFlagInd{0x1B20}, "picked up blue rupee from pot grotto 1-2" }, + { 0x1B, 0x10, AreaFlagInd{0x1B10}, "picked up blue rupee from pot grotto 1-2" }, + { 0x1B, 0x08, AreaFlagInd{0x1B08}, "picked up blue rupee from pot grotto 1-2" }, + { 0x1B, 0x04, AreaFlagInd{0x1B04}, "picked up yellow rupee from pot grotto 1-2" }, + { 0x1B, 0x02, AreaFlagInd{0x1B02}, "picked up red rupee from pot grotto 1-2" }, + { 0x1B, 0x01, AreaFlagInd{0x1B01}, "picked up red rupee from pot grotto 1-2" }, +}; + +inline EventAreaFlags eventAreaFlagsHC[] = +{ + { 0x00, 0x40, AreaFlagInd{0x0040}, "big chest behind first gate in graveyard" }, + { 0x00, 0x20, AreaFlagInd{0x0020}, "second small chest treasure room" }, + { 0x00, 0x10, AreaFlagInd{0x0010}, "eighth small chest treasure room" }, + { 0x00, 0x08, AreaFlagInd{0x0008}, "seventh small chest treasure room" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "sixth small chest treasure room" }, + { 0x00, 0x02, AreaFlagInd{0x0002}, "big chest north room 2F" }, + { 0x01, 0x80, AreaFlagInd{0x0180}, "fourth small chest treasure room" }, + { 0x01, 0x40, AreaFlagInd{0x0140}, "fifth small chest treasure room" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "third small chest treasure room" }, + { 0x01, 0x08, AreaFlagInd{0x0108}, "fifth big chest treasure room" }, + { 0x01, 0x04, AreaFlagInd{0x0104}, "fourth big chest treasure room" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "third big chest treasure room" }, + { 0x01, 0x01, AreaFlagInd{0x0101}, "second big chest treasure room" }, + { 0x02, 0x80, AreaFlagInd{0x0280}, "first big chest treasure room" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "north west big chest center room 2F" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "small chest east garden" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "north small chest west garden" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "compass north east big chest center room 2F" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "small key big chest behind third gate in graveyard" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "center small chest west garden" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "map big chest east garden" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "king bulblin small key" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "south west big chest center room 2F" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "small key big chest outside 2F" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "big key chest" }, + { 0x07, 0x08, AreaFlagInd{0x0708}, "first small chest treasure room" }, + { 0x07, 0x04, AreaFlagInd{0x0704}, "east small chest behind first gate in graveyard" }, + { 0x07, 0x02, AreaFlagInd{0x0702}, "west small chest behind first gate in graveyard" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "killed darknut in north room 2F (spawn big chest)" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "reach end of specter maze 3F" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "darknut 4F intro cs part 2" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "darknut 4F intro cs part 1" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "cut painting in north east room 2F cs" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "cut painting in north east room 2F cs" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "killed all enemies in center room 1F cs 1/2" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "spawn north east big chest center room 2F" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "killed lizalfos garding big key chest" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "open third gate in graveyard cs" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "blown up rock in graveyard" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "lit torch behind first gate in graveyard (stops rain)" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "graveyard intro cs" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "open second gate in graveyard cs" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "open first gate in graveyard cs" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "open third gate in graveyard" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "open first gate in graveyard" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "open second gate in graveyard" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "defeated dark beast Ganon" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "cut first left painting in north west room 2F" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "cut second left painting in north west room 2F" }, + { 0x0C, 0x10, AreaFlagInd{0x0C10}, "cut third left painting in north west room 2F" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "killed all enemies in center room 1F" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "despawn yellow magic walls in center room 1F" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "spawn yellow magic walls in center room 1F" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "outside 4F intro cs" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "outside 4F intro cs" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "killed all east bokoblins in south garden" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "killed all west bokoblins in south garden" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "east room 2F intro cs" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "east room 2F intro cs" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "killed right lezalfos in north west room 2F" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "killed left lezalfos in north west room 2F" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "midna text at the end of east garden (talk about wall)" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "explored graveyard" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "killed both lizalfos in 4F (removes yellow magic barrior)" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "killed both lizalfos in 3F (removes yellow magic barrior)" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "south garden intro cs" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "Midna text prompt after king bulblin" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "killed all bokoblins in west garden" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "prevent all torches form extinguishing north east room 2F" }, + { 0x11, 0x40, AreaFlagInd{0x1140}, "unlock door in north east room 2F cs" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "open second gate in west garden" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "east garden intro cs" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "east garden intro cs" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "open gate in east garden" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "open gate in east garden cs" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "unlock door in north east room 2F" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "lit all torches correctly in north east room 2F" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "lit all torches correctly in north east room 2F cs" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "unlock door outside 3F" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "open gate to big key chest" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "killed arealfos outside 2F" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "killed all enemies in center room 1F cs 1/2" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "Double Darknut room intro cs" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "spawn north west big chest center room 2F" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "west room 2F intro cs" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "cut right painting in north west room 2F" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "unlock boss door" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "unlock door in south garden" }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "midna text seen after small key in graveyard" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "midna text promt after small key in graveyard" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "Midna text seen after king bulblin" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "north west room 2F intro cs" }, + { 0x15, 0x40, AreaFlagInd{0x1540}, "cut painting in north east room 2F" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "second gate in west garden stops moving" }, + { 0x15, 0x04, AreaFlagInd{0x1504}, "open second gate in west garden cs" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "killed dynalfos in east room 2F (unlocks doors)" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "unlock treasure room door 4F" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "killed darknuts in west room 2F (unlocks doors)" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "reach big chest in north room 2F" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "lit south east torch north room 2F for the first time cs" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "lit north east torch north room 2F for the first time cs" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "unlock door in north west room 2F" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "killed darknut 4F (removes yellow magic barrier)" }, +}; + +inline EventAreaFlags eventAreaFlagsHyruleField[] = +{ + { 0x02, 0x80, AreaFlagInd{0x0280}, "Big chest owl statue hylia bridge" }, + { 0x02, 0x40, AreaFlagInd{0x0240}, "Big chest spinner south of castle town" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "Big chest double clawshot south of castle town" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "Big chest tight rope south of castle town" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "Big chest owl statue next to castle town" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "Big chest on hylia bridge" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "Big chest next to poe past hylia bridge" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "Big chest spinner tracks (Lanayru)" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "Big chest underwater Lanayru field" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "Big chest under bridge faron field" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "PoH big chest double clawshot Eldin gorge" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "Big chest Eldin gorge owl statue" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "PoH big chest owl statue Eldin bridge" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "PoH big chest leage Eldin field after kakariko" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "Map marker Telma carrage past hylia bridge" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "Map marker Telma carrage west of castle town" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "Lanayru field intro cs twilight" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "blown up rock blocking Eldin long cave" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "Hidden village path open" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "East Castle Town Bridge Flag" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "blown up rocks blocking path to zora's domain" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "Midna pulls up the map to show you can warp" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "midna text Eldin gorge bridge gone" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "exit flight by foul after lanayru twilight cleared" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "Kakariko Gorge Portal" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "spawn barriers Eldin gorge dark beasts (unset once killed)" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "bridge of Eldin placed (cs)" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "Eldin bridge getting stolen cs trigger" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "blown up rocks past Eldin bridge that lead to Lanayru" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "see gorge gate cs" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "blown up rocks to spinner tracks (Lanayru field side)" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "blown up rocks to spinner tracks (hylia bridge side)" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "entered Lanayru twilight cs" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "entered Eldin twilight cs" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "killed left shadow bulblin behind gorge gate twilight" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "killed right shadow bulblin behind gorge gate twilight" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "jumped over gorge fence after obtaining Epona" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "Lanayru Main Feild has water on map" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "seeing Lanayru twilight from up close cs" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "seeing Eldin twilight from up close cs" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "spawn dark beasts castle town portal (stays set)" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "Castle Town Portal" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "?" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "blown up rocks from Eldin gorge to Eldin field" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "blown up second rock to poe past hylia bridge" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "Epona is accessible to Link (without calling her) (after lanayru twilight)" }, + { 0x0C, 0x08, AreaFlagInd{0x0C08}, "killed poe past hylia bridge" }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "killed poe Eldin gorge" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "killed poe faron field" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "Midna text after warping gorge bridge back" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "?" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "spinner path next to hidden village explored" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "killed poe Lanayru field" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "enter field west of castle town from the east" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "killed poe south of castle town" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "bridge of Hylia intro cs twilight" }, + { 0x0E, 0x40, AreaFlagInd{0x0E40}, "blown up rock blocking ice cave Lanayru field" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "faron field intro cs" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "blown up rock on leadge Eldin field after kakariko" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "blown up first rock to poe past hylia bridge" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "Hero's shade map marker (south castle town)" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "Hero's shade map marker (west castle town)" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "enter bridge east of castle town" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "enter field south of castle town" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "enter field west of castle town from the north" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "?" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "open path from faron field to south of castle town" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "Midna text after Lanayru field twilight cs" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "blown up rocks Eldin field after kakariko" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "Eldin gorge bridge placed cs" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "see Ilia's bag from far away" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "Zora rivver boat path on map (Eldin)" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "Sky letter next to castle town" }, + { 0x11, 0x40, AreaFlagInd{0x1140}, "Map marker owl stature next to castle town" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "Sky letter hylia bridge" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "Map marker owl stature Hylia bridge" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "Sky letter Eldin bridge" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "Map marker owl stature Eldin bridge" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "Sky letter Eldin gorge" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "Map marker owl stature Eldin gorge" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "blown up northern rock in field west of castle town" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "blown up most northern rock in field west of castle town" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "blown up rock south of castle town" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "killed poe next to owl statue next to castle town" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "killed poe east of castle town" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "?" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "?" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "Epona isn't accessible to Link (without calling her) (after lanayru twilight)" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "?" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "Map marker hidden village" }, + { 0x14, 0x80, AreaFlagInd{0x1480}, "blown up southern rock underwater Lanayru field" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "blown up northern rock underwater Lanayru field" }, + { 0x14, 0x20, AreaFlagInd{0x1420}, "blown up eastern rock Lanayru field " }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "blown up rock next to hylia bridge owl statue" }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "blown up rock closest to faron after hylia bridge" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "blown up rock next to free standing PoH Elding gorge" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "blown up rock next to Eldin gorge owl statue" }, + { 0x15, 0x80, AreaFlagInd{0x1580}, "blown up rock past Eldin bridge" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "Midna text after getting Ilia's scent" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "Midna text after entering Lanayru twilight" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "Midna text when seeing Lanayru twilight from far away" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "Midna text after getting Youth's scent" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "Midna text after entering Eldin twilight" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "Midna text when seeing Eldin twilight from far away" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "got Ilia's scent cs" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "got youth's scent cs" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "Epona can't cross Eldin bridge (set when stolen, unset when fixed)" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "see broken wooden sword from far away cs" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "Bridge of Eldin Portal" }, + { 0x1A, 0x80, AreaFlagInd{0x1A80}, "picked up rupees from rock past Eldin bridge" }, + { 0x1A, 0x40, AreaFlagInd{0x1A40}, "picked up rupees from rock next to free standing PoH Elding gorge" }, + { 0x1A, 0x20, AreaFlagInd{0x1A20}, "picked up rupees from rock next to Eldin gorge owl statue" }, + { 0x1A, 0x10, AreaFlagInd{0x1A10}, "picked up rupees from rock closest to faron after hylia bridge" }, + { 0x1A, 0x08, AreaFlagInd{0x1A08}, "picked up rupees from rock next to hylia bridge owl statue" }, + { 0x1A, 0x04, AreaFlagInd{0x1A04}, "picked up rupees from rocks to spinner tracks (hylia bridge side)" }, + { 0x1A, 0x02, AreaFlagInd{0x1A02}, "picked up rupees from rocks to spinner tracks (Lanayru field side)" }, + { 0x1A, 0x01, AreaFlagInd{0x1A01}, "picked up rupees from eastern rock Lanayru field " }, + { 0x1B, 0x80, AreaFlagInd{0x1B80}, "picked up rupees from northern rock underwater Lanayru field" }, + { 0x1B, 0x40, AreaFlagInd{0x1B40}, "picked up rupees from southern rock underwater Lanayru field" }, + { 0x1B, 0x20, AreaFlagInd{0x1B20}, "picked up rupees from northern rock in field west of castle town" }, + { 0x1B, 0x10, AreaFlagInd{0x1B10}, "picked up rupees from most northern rock in field west of castle town" }, + { 0x1B, 0x08, AreaFlagInd{0x1B08}, "picked up rupees from rock south of castle town" }, + { 0x1B, 0x04, AreaFlagInd{0x1B04}, "free standing PoH Elding gorge" }, + { 0x1B, 0x02, AreaFlagInd{0x1B02}, "free standing PoH faron field" }, + { 0x1B, 0x01, AreaFlagInd{0x1B01}, "free standing PoH goron Eldin field" }, +}; + +inline EventAreaFlags eventAreaFlagsLanayru[] = +{ + { 0x00, 0x40, AreaFlagInd{0x0040}, "small chest next to mother and child iles zora's domain" }, + { 0x00, 0x20, AreaFlagInd{0x0020}, "big chest fountain back room" }, + { 0x00, 0x10, AreaFlagInd{0x0010}, "left small chest fountain back room" }, + { 0x00, 0x08, AreaFlagInd{0x0008}, "right small chest fountain back room" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "small chest midna jumps zora's domain" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "west big chest zora's domain throne room" }, + { 0x01, 0x08, AreaFlagInd{0x0108}, "east big chest zora's domain throne room" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "small chest large underwater pillar in fountain" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "small chest small underwater pillar in fountain" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "small chest floor 3 flight by foul" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "small chest floor 2 flight by foul" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "big chest right pillar in front of fountain Lake Hylia" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "big chest underwater next to Lake Hylia warp" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "east big chest fountain" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "west big chest fountain" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "big chest floor 4 flight by foul" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "small chest left pillar in front of fountain Lae Hylia" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "big chest floor 5 flight by foul" }, + { 0x04, 0x40, AreaFlagInd{0x0440}, "tear of light throne room zora's domain" }, + { 0x04, 0x20, AreaFlagInd{0x0420}, "first tear of light flying" }, + { 0x04, 0x10, AreaFlagInd{0x0410}, "tear of light midna jumps zora's domain" }, + { 0x04, 0x08, AreaFlagInd{0x0408}, "tear of light on top of water zora's domain" }, + { 0x04, 0x04, AreaFlagInd{0x0404}, "third tear of light flying" }, + { 0x04, 0x02, AreaFlagInd{0x0402}, "fourth tear of light flying" }, + { 0x05, 0x80, AreaFlagInd{0x0580}, "tear of light next to Iza's shop" }, + { 0x05, 0x40, AreaFlagInd{0x0540}, "second tear of light flying" }, + { 0x05, 0x20, AreaFlagInd{0x0520}, "final tear of light" }, + { 0x05, 0x04, AreaFlagInd{0x0504}, "tear of light behind of Fyer's canon" }, + { 0x05, 0x02, AreaFlagInd{0x0502}, "tear of light island east of Lake Hylia" }, + { 0x05, 0x01, AreaFlagInd{0x0501}, "tear of light southmost island Lake Hylia" }, + { 0x06, 0x80, AreaFlagInd{0x0680}, "tear of light next to fountain Lake Hylia" }, + { 0x06, 0x40, AreaFlagInd{0x0640}, "tear of light on top of water zora's domain" }, + { 0x06, 0x20, AreaFlagInd{0x0620}, "tear of light next to mother and child iles zora's domain" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "see frozen zora spirits in domain twilight" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "Lake hylia intro cs twilight" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "blown up underwater rock in center of fountain" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "the two zoras in upper zora river went down stream (twilight)" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "saw light bug come out next to Iza spirit cs" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "blown up rock to Lake hylia long cave" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "spawned UZR portal fight barriers" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "opened Upper Zora's River Portal cs" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "talked to Iza before UZR portal" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "Upper Zora's River Portal" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "spawn big chest fountain back room" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "lit left torch fountain back room" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "lit right torch fountain back room" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "seeing Twilight Bloat with sense cs" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "Twilight Bloat comes out of water cs" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "seeing Twilit Bloat move from far away cs" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "Save Prompt after Lakebed (if disabled, triggers MDH after Lakebed)" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "twilight end cs trigger (also map marker Lakebed Temple)" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "obtained vessel of light (unset after twilight)" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "spawn dark beasts Lake Hylia (unset once killed)" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "Lake Hylia Portal" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "Zora's river intro cs during twilight (flying with bird)" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "midna text promt saying you can call down Kargarok to fly" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "Rutella cs in domain twilight" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "Zora river boat path on map" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "entered Twilight Bloat arena (unset once killed)" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "explored entrance to snowpeak" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "got PoH flight by foul" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "Zora's Domain Portal" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "spawn barriers Zora's domain dark beasts fight" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "Zora's domain intro cs twilight (frozen)" }, + { 0x0C, 0x08, AreaFlagInd{0x0C08}, "blown up south underwater rock zora's domain " }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "blown up north underwater rock zora's domain" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "blown up underwater rock in back of fountain" }, + { 0x0C, 0x01, AreaFlagInd{0x0C01}, "Midna text after landing in Lake hylia twilight" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "Iza text after Upper Zora's River Portal" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "can now exit from the sides of zora's domain inside (set during domain outside cs after melting it)" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "Midna text after domain outside cs after melting it" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "domain outside cs after melting it" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "map marker Auru" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "Upper zora's river intro cs during twilight" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "Midna text after leaving lake hylia after Lanayru twilight" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "Zora's domain waterfall is going fast (during twilight only) (unset when you come back to Lake hylia)" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "blown up rock blocking lakebed entrance cs" }, + { 0x0E, 0x40, AreaFlagInd{0x0E40}, "opened stream next to lakebed entance" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "Midna text after arriving at upper zora's river twilight" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "melted zora's domain (Lake Hylia water on map (top part))" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "twilight end cs watched" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "map marker Snowpeak" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "Midna text after coming back to lake after filling it (twilight)" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "blown up rock blocking lakebed entrance" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "went down the fast water in zora's domain (void out) (unset after twilight)" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "Lake hylia filled twilight intro cs (domain water level normal)" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "blown up first rock to throne room zora's domain" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "Midna text prompt to tell you to look under the ice in zora's domain twilight" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "Midna text before midna jumps fozen zora's domain twilight" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "Midna text before midna jumps melted zora's domain twilight" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "Midna text after melting domain (water everywhere on map)" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "blown up underwater rock zora river town path" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "spawn west big chest zora's domain throne room" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "spawn east big chest zora's domain throne room" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "lit east torch zora's domain throne room" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "extingushed north torch zora's domain throne room" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "lit west torch zora's domain throne room" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "blown up west underwater rock Lakebed area" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "blown up south underwater rock Lakebed area" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "saw the two zora spirits from far away in upper zora river cs (twilight)" }, + { 0x11, 0x40, AreaFlagInd{0x1140}, "saw light bug moving after exiting lanayru spring" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "went down zora's river with bird during twilight" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "blown up underwater rock zora river lake path" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "midna text after seeing the frozen zoras in domain twilight" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "paid Fyer to launch you to the flight by foul platform (unset once you enter the canon)" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "blown up north underwater rock throne room zora's domain" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "Lanayru spring map marker" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "Midna Text after frozen zora's domain twilight intro cs" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "killed poe underneath flight by foul" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "killed poe next to watch tower to desert" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "killed poe east of lake Hylia" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "killed poe next to mother and chile iles Zora's Domain" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "killed poe midna jumps Zora's Domain" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "killed poe in Upper Zora's River" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "killed poe on flight by foul prizes" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "killed poe next to fountain Lake Hylia" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "blew up rock in Iza's house" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "Iza text after blowing the rocks inside her house" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "blew up rock in Iza's house cs" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "killed light bug throne room zora's domain" }, + { 0x14, 0x20, AreaFlagInd{0x1420}, "killed first light bug flying" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "killed light bug midna jumps zora's domain" }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "killed light bug on top of water zora's domain" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "killed third light bug flying" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "killed fourth light bug flying" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "killed light bug behind of Fyer's canon" }, + { 0x15, 0x80, AreaFlagInd{0x1580}, "killed light bug next to Iza's shop" }, + { 0x15, 0x40, AreaFlagInd{0x1540}, "killed second light bug flying" }, + { 0x15, 0x20, AreaFlagInd{0x1520}, "killed final light bug (Twilight Bloat)" }, + { 0x15, 0x10, AreaFlagInd{0x1510}, "killed light bug island east of Lake Hylia" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "killed light bug next to mother and child iles zora's domain" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "?" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "Watched CS of Ooccoo running to Sky Cannon" }, + { 0x16, 0x40, AreaFlagInd{0x1640}, "killed light bug on top of water zora's domain" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "killed light bug southmost island Lake Hylia" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "killed light bug next to fountain Lake Hylia" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "talked to light spirit during twilight" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "went up zora's river after melting domain in twilight" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "blown up rock zora river on land" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "went up zora's river after melting domain in twilight (also set when failing to do so)" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "blown up second rock to throne room zora's domain" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "?" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "Midna text after Zora's domain portal" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "blown up underwater rock zora river middle" }, + { 0x18, 0x08, AreaFlagInd{0x1808}, "picked up rupees from south underwater rock Lakebed area" }, + { 0x18, 0x04, AreaFlagInd{0x1804}, "picked up rupees from west underwater rock Lakebed area" }, + { 0x18, 0x02, AreaFlagInd{0x1802}, "picked up green rupees domain midna jumps twilight" }, + { 0x18, 0x01, AreaFlagInd{0x1801}, "picked up fourth green rupee domain midna jumps twilight" }, + { 0x19, 0x80, AreaFlagInd{0x1980}, "picked up third blue rupee midna jumps zora's domain" }, + { 0x19, 0x40, AreaFlagInd{0x1940}, "picked up second blue rupee midna jumps zora's domain" }, + { 0x19, 0x20, AreaFlagInd{0x1920}, "picked up first blue rupee midna jumps zora's domain" }, + { 0x19, 0x10, AreaFlagInd{0x1910}, "picked up rupees from rock zora river on land" }, + { 0x19, 0x08, AreaFlagInd{0x1908}, "picked up rupees from underwater rock zora river lake path" }, + { 0x19, 0x04, AreaFlagInd{0x1904}, "picked up rupees from underwater rock zora river town path" }, + { 0x19, 0x02, AreaFlagInd{0x1902}, "picked up rupees from underwater rock zora river middle" }, + { 0x19, 0x01, AreaFlagInd{0x1901}, "picked up rupees from underwater rock back fountain" }, + { 0x1A, 0x80, AreaFlagInd{0x1A80}, "picked up rupees from underwater rock center fountain" }, + { 0x1A, 0x40, AreaFlagInd{0x1A40}, "picked up rupees from south underwater rock zora's domain " }, + { 0x1A, 0x20, AreaFlagInd{0x1A20}, "picked up rupees from north underwater rock zora's domain" }, + { 0x1A, 0x10, AreaFlagInd{0x1A10}, "picked up rupees from first rock to throne room zora's domain" }, + { 0x1A, 0x08, AreaFlagInd{0x1A08}, "picked up rupees from second rock to throne room zora's domain" }, + { 0x1A, 0x04, AreaFlagInd{0x1A04}, "picked up east blue rupees behind underwater bridge throne room zora's domain" }, + { 0x1A, 0x02, AreaFlagInd{0x1A02}, "picked up west blue rupees behind underwater bridge throne room zora's domain" }, + { 0x1A, 0x01, AreaFlagInd{0x1A01}, "picked up south yellow rupee unerwater throne room zora's domain" }, + { 0x1B, 0x80, AreaFlagInd{0x1B80}, "picked up north yellow rupee unerwater throne room zora's domain" }, + { 0x1B, 0x40, AreaFlagInd{0x1B40}, "picked up east yellow rupee unerwater throne room zora's domain" }, + { 0x1B, 0x20, AreaFlagInd{0x1B20}, "picked up yellow rupee above north underwater rock throne room zora's domain" }, + { 0x1B, 0x10, AreaFlagInd{0x1B10}, "picked up first yellow rupee midna jumps zora's domain" }, + { 0x1B, 0x08, AreaFlagInd{0x1B08}, "picked up second yellow rupee midna jumps zora's domain" }, + { 0x1B, 0x02, AreaFlagInd{0x1B02}, "picked up yellow rupee from broken pillar lakebed area" }, + { 0x1B, 0x01, AreaFlagInd{0x1B01}, "picked up yellow rupee from broken pillar lakebed area" }, +}; + +inline EventAreaFlags eventAreaFlagsLBT[] = +{ + { 0x00, 0x40, AreaFlagInd{0x0040}, "small chest first west room 1F" }, + { 0x00, 0x20, AreaFlagInd{0x0020}, "south small chest first room" }, + { 0x00, 0x10, AreaFlagInd{0x0010}, "west small chest first room" }, + { 0x00, 0x08, AreaFlagInd{0x0008}, "small chest second east room 4F" }, + { 0x00, 0x02, AreaFlagInd{0x0002}, "underwater big chest in first west room 2F" }, + { 0x00, 0x01, AreaFlagInd{0x0001}, "small chest first east room 2F" }, + { 0x01, 0x40, AreaFlagInd{0x0140}, "Ooccoo Map Flag" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "small chest second west room 4F" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "compass big chest second west room 4F" }, + { 0x01, 0x08, AreaFlagInd{0x0108}, "center small chest first west room 2F" }, + { 0x01, 0x04, AreaFlagInd{0x0104}, "small chest in room before big key" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "map big chest main room 1F" }, + { 0x01, 0x01, AreaFlagInd{0x0101}, "big chest under boss door main room B1" }, + { 0x02, 0x80, AreaFlagInd{0x0280}, "south underwater big chest in room before mini-boss" }, + { 0x02, 0x40, AreaFlagInd{0x0240}, "north underwater big chest in room before mini-boss" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "north west big chest first west room 2F" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "big key chest" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "big chest second east room 4F" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "small key big chest first east room 2F" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "PoH big chest behind gate in first east room 1F" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "south small chest in first west room 2F" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "small key big chest first east room 1F" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "PoH big chest main room 2F" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "small chest second room" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "small key big chest room before mini-boss" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "small chest main room 1F" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "clawshot big chest mini-boss room" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "raised water in first east room 1F" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "water flowing into first east room 1F cs" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "?" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "Midna Stalactite text second room" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "water flowing into first east room 1F" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "west water flowing into stairs in main room" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "east water flowing into stairs in main room" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "water flowing into east wing 1F" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "water flowing into west wing 1F" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "water in main room raised twice" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "water in main room raised once" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "west water not flowing into stairs in main room" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "east water not flowing into stairs in main room" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "west water is flowing into main room" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "staircase top is west main room" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "staircase top is east main room" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "east water is flowing into main room" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "let water flow out of room west wing 2F" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "let water flow out of room east wing 2F" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "spawn clawshot big chest" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "set staircase top is east main room" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "set staircase top is north main room" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "set staircase top is west main room" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "set staircase top is south main room (default)" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "PoH big chest behind gate in first east room 1F (set after)" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "horizontal wheel is turning in first east room" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "unlock east door main room 2F" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "vertical wheel is turning in first east room 2F" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "small key big chest first east room 1F (set after)" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "east wing water switch 4F" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "west wing water switch 4F" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "opened north gate first east room 2F" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "opened gate in first room" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "blown up second rock in room before big key" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "blown up first rock in room before big key" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "unlocked door in second east room 2F" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "unlocked door in room before mini-boss" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "horizontal wheel is turning in first east room cs" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "horizontal wheels turning in first west room" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "killed lizalfos in south hallway to main room 2F" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "killed lizalfos behind gate in first east room 1F " }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "blown up rock in room before mini-boss" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "Ooccoo Freed" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "unlocked boss door" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "a stalactite fell in second room (other than the first one)" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "stalactite fell in first west room 2F" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "stalactite falls by itself in second room" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "left stalactite fell in first east room 2F" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "right stalactite fell in first east room 2F" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "blown up rock in first east room 2F" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "south stalactite fell in first east room 1F" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "north stalactite fell in first east room 1F" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "south-east stalactite fell second room" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "north east stalactite fell in second room" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "south Stalactite fell second room" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "north west stalactite fell in second room" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "south west stalactile fell in second room" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "killed first shell blade in room before big key" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "killed second shell blade in room before big key" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "opened north gate in first west room 2F" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "opened south gate in first west room 2F" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "opened gates in mini-boss room" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "explore main room" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "main room intro cs" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "south Hallway to Main room intro cs" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "first room intro cs" }, + { 0x15, 0x20, AreaFlagInd{0x1520}, "move staircase main room cs" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "west water flowing into staircase main room cs" }, + { 0x15, 0x04, AreaFlagInd{0x1504}, "east water flowing into staircase main room cs" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "water in main room raised rwice cs" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "west water is flowing into main room cs" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "water in main room raised once cs" }, + { 0x16, 0x40, AreaFlagInd{0x1640}, "east water is flowing into main room cs" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "big key chest (set after)" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "compass big chest second west room 4F (set after)" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "bubble worm cs second east room 2F" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "enter/exit mini-boss room from 1F door (spawns enemies in room before)" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "killed right lizalfos in first west room 2F" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "killed left lizalfos in first west room 2F" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "killed lizalfos in first east room 2F" }, +}; + +inline EventAreaFlags eventAreaFlagsLHLC[] = +{ + { 0x02, 0x40, AreaFlagInd{0x0240}, "big chest room 5 lake hylia long cave" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "east big chest room 10 Lake Hylia Long Cave" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "north small chest room 9 Lake Hylia Long Cave" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "east small chest room 8 Lake Hylia Long Cave" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "north small chest room 6 Lake hylia Long Cave" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "south small chest room 7 Lake Hylia Long Cave" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "north small chest room 5 Lake hylia Long Cave" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "north small chest room 4 Lake Hylia Long Cave" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "west small chest room 2 Lake hylia Long Cave" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "west big chest room 7 Lake Hylia Long Cave" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "north big chest room 8 Lake Hylia Long Cave" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "big chest room 11 lake hylia long cave" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "north small chest room 1 Lake hylia Long Cave" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "west small chest room 1 Lake hylia Long Cave" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "east small chest room 3 Lake Hylia Long Cave" }, + { 0x04, 0x80, AreaFlagInd{0x0480}, "small chest Goron Stock Cave" }, + { 0x04, 0x40, AreaFlagInd{0x0440}, "big chest in front of exit Goron Stock Cave" }, + { 0x04, 0x20, AreaFlagInd{0x0420}, "big chest spawned form torches Goron Stock Cave" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "explored section 18 Lake hylia Long Cave" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "explored section 15 Lake hylia Long Cave" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "explored section 12 Lake hylia Long Cave" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "explored section 11 Lake hylia Long Cave" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "explored section 9 Lake hylia Long Cave" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "explored section 6 Lake hylia Long Cave" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "explored section 4 Lake hylia Long Cave" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "explored section 27 (room 11) Lake hylia Long Cave" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "explored section 24 (room 10) Lake hylia Long Cave" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "explored section 23 (room 9) Lake hylia Long Cave" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "explored section 22 Lake hylia Long Cave" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "explored east area room 8 Lake hylia Long Cave" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "explored north area room 8 Lake hylia Long Cave" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "explored section 21 (room 8) Lake hylia Long Cave" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "explored section 17 Lake hylia Long Cave" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "explored south area room 7 Lake hylia Long Cave" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "explored west area room 7 Lake hylia Long Cave" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "explored section 16 (room 7) Lake hylia Long Cave" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "explored section 14 Lake hylia Long Cave" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "explored north area room 6 Lake hylia Long Cave" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "explored section 13 (room 6) Lake hylia Long Cave" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "explored section 10 (room 5) Lake hylia Long Cave" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "explored section 8 Lake hylia Long Cave" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "explored north area room 4 Lake hylia Long Cave" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "explored section 7 (room 4) Lake hylia Long Cave" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "explored section 5 (room 3) Lake hylia Long Cave" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "explored section 3 (room 2) Lake hylia Long Cave" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "explored section 2 Lake hylia Long Cave" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "explored north area room 1 Lake hylia Long Cave" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "explored west area room 1 Lake hylia Long Cave" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "explored section 1 (room 1) Lake hylia Long Cave" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "blown up north rock room 7 Lake hylia long cave" }, + { 0x0C, 0x10, AreaFlagInd{0x0C10}, "blown up north rock room 3 Lake hylia long cave" }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "blown up north rock room 4 Lake hylia long cave" }, + { 0x0C, 0x01, AreaFlagInd{0x0C01}, "blown up south rock room 8 Lake hylia long cave" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "blown up east rock room 6 Lake Hylia Long Cave" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "blown up west rock room 6 Lake Hylia Long Cave" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "blown up north rock room 5 Lake Hylia Long Cave" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "blown up north rock room 9 Lake Hylia Long Cave" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "blown up east rock room 3 Lake hylia long cave" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "lit right torch room 5 lake hylia long cave" }, + { 0x0E, 0x40, AreaFlagInd{0x0E40}, "lit left torch room 5 lake hylia long cave" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "spawn big chest room 11 lake hylia long cave" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "lit right torch room 11 Lake Hylia Long Cave" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "lit left torch room 11 Lake Hylia Long Cave" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "explored south area room 11 Lake hylia Long Cave" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "explored east area room 10 Lake hylia Long Cave" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "explored north area room 9 Lake hylia Long Cave" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "explored east area room 6 Lake hylia Long Cave" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "explored north area room 5 Lake hylia Long Cave" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "explored east area room 3 Lake hylia Long Cave" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "explored west area room 2 Lake hylia Long Cave" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "explored section 25 Lake hylia Long Cave" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "explored section 26 Lake hylia Long Cave" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "explored section 20 Lake hylia Long Cave" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "explored section 19 Lake hylia Long Cave" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "killed poe in room 11 Lake Hylia Long Cave" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "killed poe in room 3 Lake Hylia Long Cave" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "killed poe in room 8 Lake Hylia Long Cave" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "lit torch 9 (room 7) Lake hylia Long Cave" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "lit torch 5 (room 4) Lake hylia Long Cave" }, + { 0x11, 0x40, AreaFlagInd{0x1140}, "lit torch 3 (room 2) Lake hylia Long Cave" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "lit torch 2 (room 1) Lake hylia Long Cave" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "lit torch 12 (room 10) Lake hylia Long Cave" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "lit torch 10 (room 8) Lake hylia Long Cave" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "spawn big chest room 5 lake hylia long cave" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "lit torch 4 (room 3) Lake hylia Long Cave" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "blown up east rock room 9 Lake hylia long cave" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "blown up west rock room 2 Lake hylia Long Cave" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "blown up east rock room 2 Lake hylia Long Cave" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "blown up north rock room 6 Lake hylia Long Cave" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "blown up west rock room 5 Lake hylia Long Cave" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "blown up east rock room 8 Lake Hylia Long Cave" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "blown up east rock room 10 Lake Hylia Long Cave" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "blown up north rock room 8 Lake hylia Long Cave" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "blown up west rock room 7 Lake hylia Long Cave" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "blown up south rock room 7 Lake hylia Long Cave" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "blown up south rock room 10 Lake hylia Long Cave" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "blown up west rock room 4 Lake hylia Long Cave" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "blown up west rock room 1 Lake hylia Long Cave" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "blown up north rock room 1 Lake hylia Long Cave" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "blown up east rock room 1 Lake hylia Long Cave" }, + { 0x14, 0x80, AreaFlagInd{0x1480}, "spawn big chest bottom floor Goron Stock Cave" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "lit right torch bottom floor Goron Stock Cave" }, + { 0x14, 0x20, AreaFlagInd{0x1420}, "lit left torch bottom floor Goron Stock Cave" }, +}; + +inline EventAreaFlags eventAreaFlagsOrdon[] = +{ + { 0x03, 0x10, AreaFlagInd{0x0310}, "wooden sword big chest" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "iron boots big chest" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "Big chest link's basement" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "despawn Bo and Jaggle after scaring them away (wolf night)" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "scare away Bo and Jaggle after hearing them (wolf night)" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "started sword training" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "obtained wooden sword" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "midna text after getting ordon shield (spawns sword)" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "got ordon shield (will despawn it if set)" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "midna text after getting ordon sword" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "got ordon sword" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "exit shield house as wolf cs" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "Goats 2" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "day 3 intro cs (spawn in ranch)" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "knocked down bee's nest day 2 cs" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "? (wolf in village night)" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "Ranch first time CS" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "Rusl moving during wolf night" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "killed spider on top of Link's house" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "spawn 2 spiders around Link,s house (day 2) (set after buying slingshot)" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "Ilia spring CS watched" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "Ilia spring CS started" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "Ordon Village first time CS" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "Ilia spring CS Trigger" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "killed spider on ladder to link's house" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "Epona being in spring" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "finished slingshot training" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "Entered spring Area first time" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "Intro CS watched" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "finished sword training" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "killed bulblin ordon woods" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "killed second bulblin Link's house" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "killed first bulblin Link's house" }, + { 0x0C, 0x10, AreaFlagInd{0x0C10}, "spawn wooden sword big chest" }, + { 0x0C, 0x08, AreaFlagInd{0x0C08}, "Day 2 intro CS" }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "talked to owl as wolf for the first time" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "Goats 1" }, + { 0x0C, 0x01, AreaFlagInd{0x0C01}, "set after getting ordon shield" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "approach faron twilight to enter it with midna cs" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "spawn shadow beast" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "Ordon Spring Portal" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "hear Bo and Jaggle talk about shield from far away (wolf night)" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "entered shield house as wolf cs" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "ordon shield fell down cs" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "midna text leaving spring" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "started midna jump to ordon shield inside house" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "Hanch started attacking (unlocks midna jump to top of shop)" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "midna cs after hearing Bo and Jaggle talk about shield" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "day 3 Fado intro text" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "Allows Link to stop goat in Village" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "started midna jumps to top of shop" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "midna text before doing jumps to top of shop" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "Rusl talking to his wife cs (wolf night)" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "unlock midna jumps to shield house" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "scared Hanch" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "torch next to Hanch is lit (wolf night)" }, + { 0x12, 0x01, AreaFlagInd{0x1201}, "? (wolf in village night)" }, + { 0x13, 0x80, AreaFlagInd{0x1380}, "iron boots big chest (duplicate)" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "spawn iron boots chest in Bo's house" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "brigthen up area under Hanch after getting spoted" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "set after midna text after getting ordon shield" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "Hanch spoted you cs (wolf night)" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "randomly set during wolf night (often during Hanch part)" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "Hero's shade map marker" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "Ordon day 1 (save promt)" }, + { 0x14, 0x80, AreaFlagInd{0x1480}, "set after midna text after getting ordon sword" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "Jump to eagle grass next to Jaggle day 2 (removes Jaggle text)" }, + { 0x15, 0x10, AreaFlagInd{0x1510}, "did midna jumps to top of shop" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "King Bulblin cs" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "enter village as wolf intro cs" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "explored area with link's house (wolf)" }, + { 0x19, 0x80, AreaFlagInd{0x1980}, "picked up yellow rupee on top of Hanch's house" }, + { 0x19, 0x40, AreaFlagInd{0x1940}, "picked up green rupee in grass in front of Bo's house" }, + { 0x19, 0x20, AreaFlagInd{0x1920}, "picked up green rupees in pond next to Rusl's house (day 2)" }, + { 0x19, 0x10, AreaFlagInd{0x1910}, "picked up purple rupee behind Jaggle's house" }, + { 0x19, 0x01, AreaFlagInd{0x1901}, "picked up green rupee in the back of the grass to the right of Link's house" }, + { 0x1A, 0x80, AreaFlagInd{0x1A80}, "picked up green rupee in grass right of gate to village " }, + { 0x1A, 0x40, AreaFlagInd{0x1A40}, "picked up green rupee next to rock in the grass to the right of Link's house" }, + { 0x1A, 0x20, AreaFlagInd{0x1A20}, "picked up green rupee in grass to the right of Link's house" }, + { 0x1A, 0x10, AreaFlagInd{0x1A10}, "picked up first green rupee in crawl space" }, + { 0x1A, 0x08, AreaFlagInd{0x1A08}, "picked up yellow rupee on top of Bo's house" }, + { 0x1A, 0x02, AreaFlagInd{0x1A02}, "picked up blue rupee on top of Hanch's house" }, + { 0x1A, 0x01, AreaFlagInd{0x1A01}, "Picked up Orange Rupee by Rusl's house" }, + { 0x1B, 0x80, AreaFlagInd{0x1B80}, "picked up green rupees on top of Hanch's house (day 2)" }, + { 0x1B, 0x40, AreaFlagInd{0x1B40}, "picked up green rupees on top of Bo's house (day 2)" }, + { 0x1B, 0x08, AreaFlagInd{0x1B08}, "Picked up Ordon Sword" }, + { 0x1B, 0x04, AreaFlagInd{0x1B04}, "picked up green rupee under bridge (day 2)" }, + { 0x1B, 0x02, AreaFlagInd{0x1B02}, "picked up yellow rupees on top of Rusl's house" }, + { 0x1B, 0x01, AreaFlagInd{0x1B01}, "picked up yellow rupee on cliff next to Bo's house" }, +}; + +inline EventAreaFlags eventAreaFlagsPoT[] = +{ + { 0x00, 0x40, AreaFlagInd{0x0040}, "PoH hidden big chest west room 1" }, + { 0x00, 0x20, AreaFlagInd{0x0020}, "north west small chest east room 2" }, + { 0x00, 0x10, AreaFlagInd{0x0010}, "north east small chest east room 2" }, + { 0x00, 0x08, AreaFlagInd{0x0008}, "west small chest east room 1" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "small key big chest north room 3" }, + { 0x00, 0x02, AreaFlagInd{0x0002}, "south higher big chest west room 2" }, + { 0x00, 0x01, AreaFlagInd{0x0001}, "small key big chest north room 2" }, + { 0x01, 0x80, AreaFlagInd{0x0180}, "big key chest" }, + { 0x01, 0x40, AreaFlagInd{0x0140}, "small key big chest north room 1" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "placed west sol" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "placed east sol" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "small key east big chest east room 2" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "small key north east big chest east room 1" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "small key north big chest west room 2" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "small key big chest west room 1" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "compass lower south big chest west room 2" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "PoH east big chest east room 1" }, + { 0x07, 0x10, AreaFlagInd{0x0710}, "north west small chest east room 1" }, + { 0x07, 0x02, AreaFlagInd{0x0702}, "map west big chest east room 2" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "spawn south fence in east room 3" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "spawn north fence in east room 3" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "spawn south fence in west room 3" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "spawn north fence in west room 3" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "spawn in main room from midna warp" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "save prompt after boss" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "killed east phantom Zant (unlocks door)" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "killed west phantom Zant (unlocks door)" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "Phantom Zant 1 CS" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "?" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "?" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "killed all Zant heads in east room 2 (spawn big chest)" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "killed Zant head in east room 1 (spawn chest)" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "killed Zant head in west room 2 (spawn 2 big chests)" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "killed Zant head in west room 1 (spawn chest)" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "unlock boss door" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "unlock door in north room 3" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "unlock door in east room 2" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "unlock door in west room 2" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "intro cs" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "?" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "get light master sword cs" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "? (main room)" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "? (main room)" }, + { 0x0C, 0x10, AreaFlagInd{0x0C10}, "Watched CS of platform to east wing being activated" }, + { 0x0C, 0x08, AreaFlagInd{0x0C08}, "Platform to east wing is active" }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "Midna text when west sol is placed" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "Midna text when west hand steals sol" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "killed south west Zant head in east room 2" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "killed south east Zant head in east room 2" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "killed center Zant head in east room 2" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "killed first Zant head in east room 2" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "unlock door in north room 2" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "killed dark beasts in north room 3" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "crossed black fog waterfall main room" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "midna text black fog west room 1" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "Midna text finding west sol" }, + { 0x0E, 0x40, AreaFlagInd{0x0E40}, "midna text black fog waterfall main room" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "midna text when seeing a civilian seen" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "midna text after warping back to main room after boss" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "killed second Zant head in north room 3 (spawn big chest)" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "killed first Zant head in north room 3 (spawn head 2)" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "midna text promt when seeing a civilian" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "midna text after light master sword" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "unlock door in north room 1" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "unlock door in east room 1" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "unlock door in west room 1" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "killed south Zant head in north room 1 (wave 1)" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "killed center Zant head in north room 1 (wave 1)" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "killed north west Zant head in north room 1 (wave 1)" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "killed north east Zant head in north room 1 (wave 1)" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "?" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "?" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "? (main room)" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "Heal Twili citizen outside west wing" }, + { 0x11, 0x40, AreaFlagInd{0x1140}, "? (main room)" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "Heal Twili near PoT entrance" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "Heal Twili citizen outside east wing" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "Heal Twili citizen next to west sol" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "Heal Twili citizen next to east sol" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "Heal Twili citizen north of Light Sword" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "Heal Twili citizen near fog waterfall" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "Opened Big Key Chest (causes fence to fall in room before Zant)" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "Midna text after re-entering west wing after sol stolen" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "remove fog in north room 4" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "remove fog and spawn stairs in north room 1" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "placed east sol in north room 1" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "placed west sol in north room 1" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "midna intro text" }, + { 0x15, 0x40, AreaFlagInd{0x1540}, "killed all wave 4 Zant heads in north room 2 (spawn big chest)" }, + { 0x15, 0x20, AreaFlagInd{0x1520}, "killed east Zant head north room 2 (wave 4)" }, + { 0x15, 0x10, AreaFlagInd{0x1510}, "killed west Zant head north room 2 (wave 4)" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "killed Zant head wave 3 in north room 2 (spawn wave 4)" }, + { 0x15, 0x04, AreaFlagInd{0x1504}, "killed all wave 2 Zant heads in north room 2 (spawn wave 3)" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "killed west Zant head north room 2 (wave 2)" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "killed east Zant head north room 2 (wave 2)" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "killed middle Zant head north room 2 (wave 2)" }, + { 0x16, 0x40, AreaFlagInd{0x1640}, "killed first Zant head north room 2 (spawn wave 2)" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "Watched east wing second room stairs CS" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "?" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "?" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "killed dark beasts in east room 2" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "spawn fog in east room 3" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "spawn fog in west room 3" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "killed all wave 2 Zant heads in north room 1 (spawn big chest)" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "killed north west Zant head north room 1 (wave 2)" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "killed north east Zant head north room 1 (wave 2)" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "killed all wave 1 Zant heads in north room 1 (spawn wave 2)" }, + { 0x1B, 0x01, AreaFlagInd{0x1B01}, "heart container" }, +}; + +inline EventAreaFlags eventAreaFlagsSacredGrove[] = +{ + { 0x03, 0x08, AreaFlagInd{0x0308}, "Big chest lost woods 2 torches" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "Big chest spinner in skull kid wolf battle area" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "PoH big chest temple of time (past)" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "lit left torch chest lost woods" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "killed poe temple of time (past)" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "midna text after pushing block human" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "cs after pushing block human" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "intro cs lost woods" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "?" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "?" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "explored section after window before dungeon (includes map marker for dungeon)" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "stairs to temple of time area near (dungeon)" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "strike MS into pedestal in past" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "window and stairs to dungeon work properly" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "killed poe lost woods water" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "killed poe in skull kid wolf battle area" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "killed poe in master sword area (present)" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "skull kid wolf battle area explored" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "temple of time (present) main room explored" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "temple of time (past) main room explored" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "blown up rock in skull kid wolf battle area" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "block pushed human" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "Master Sword Puzzle Complete" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "Master sword area explored" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "Temple of time (present) human entrence explored" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "spawn big chest lost woods 2 torches" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "lit right torch chest lost woods" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "Master Sword Pulled (does not despawn)" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "Blocks entrance to Woods (auto-set upon re-entering Grove before Skull Kid 2" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "Lost woods skull kid human chase (unset once done)" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "skull kid human defeated" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "lost woods turns to day after skull kid fight human" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "Skull Kid Appears (set automatically)" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "Transition to day after Skull Kid" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "dark beasts spawned" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "Sacred Grove Portal (removes statue blocking door of time (to past)" }, +}; + +inline EventAreaFlags eventAreaFlagsSewer[] = +{ + { 0x00, 0x80, AreaFlagInd{0x0080}, "0" }, + { 0x00, 0x40, AreaFlagInd{0x0040}, "1" }, + { 0x00, 0x20, AreaFlagInd{0x0020}, "2" }, + { 0x00, 0x10, AreaFlagInd{0x0010}, "3" }, + { 0x00, 0x08, AreaFlagInd{0x0008}, "4" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "5" }, + { 0x00, 0x02, AreaFlagInd{0x0002}, "6" }, + { 0x00, 0x01, AreaFlagInd{0x0001}, "7" }, + { 0x01, 0x80, AreaFlagInd{0x0180}, "8" }, + { 0x01, 0x40, AreaFlagInd{0x0140}, "9" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "10" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "11" }, + { 0x01, 0x08, AreaFlagInd{0x0108}, "12" }, + { 0x01, 0x04, AreaFlagInd{0x0104}, "13" }, + { 0x01, 0x02, AreaFlagInd{0x0102}, "14" }, + { 0x01, 0x01, AreaFlagInd{0x0101}, "15" }, + { 0x02, 0x80, AreaFlagInd{0x0280}, "16" }, + { 0x02, 0x40, AreaFlagInd{0x0240}, "17" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "18" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "19" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "20" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "twilight final cs" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "Zelda cs twilight" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "midna text after first gate sewers (how to fight)" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "midna text after exiting to rooftops (twilight)" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "wake up in jail cs" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "midna promt to use sense to see gard (entrance to sewers)" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "pushed box outside (MDH)" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "pulled lever of first water gate in sewers" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "dug up blue rupee after crawl space (sewers) (twilight)" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "second water gate in sewers cs (unset afterwards)" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "first water gate in sewers cs (unset afterwards)" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "midna cs after diging out of jail" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "midna intro cs" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "waited long enough in jail" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "broke fragile floor first jump of stairway (unlocks first midna jump)" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "started midna jumps 3 stairway" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "started midna jumps 2 stairway" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "zelda tower intro cs" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "midna went to the other side of the fence in sewers" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "started midna jumps on rooftops" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "started midna jumps 4 stairway (top to door)" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "broke box inside Link's cell" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "did midna jumps 1 stairway (broke small platform)" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "outside top door intro cs" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "broke fire in Zelda's room" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "midna rejoinded link after crawl space" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "went to the other side of the fence in sewers cs" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "top of stairway intro cs" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "stairway intro cs" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "read midna promt to open door" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "opened door in cell next to Link's" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "dug out of Link's cell" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "killed first bulblin outside (MDH)" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "killed second bulblin outside (MDH)" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "killed bulblin at top of stairway (on the patform) (MDH)" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "killed fourth bulblin in stairway (MDH)" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "killed third bulblin in stairway (MDH)" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "killed second bulblin in stairway (MDH)" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "killed first bulblin in stairway (MDH)" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "talked to midna after using sense on first rooftop gard" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "midna text when approching first rooftop gard" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "listened to first gard in sewers" }, + { 0x0F, 0x04, AreaFlagInd{0x0F04}, "saw first rooftop gard with sense" }, + { 0x0F, 0x02, AreaFlagInd{0x0F02}, "saw first gard in sewers" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "midna cs after diging out of jail" }, + { 0x1B, 0x08, AreaFlagInd{0x1B08}, "picked up green rupee in box in secret passge next to fence (sewers) (twilight)" }, + { 0x1B, 0x04, AreaFlagInd{0x1B04}, "picked up blue rupee in box in left secret way (sewers) (twilight)" }, + { 0x1B, 0x02, AreaFlagInd{0x1B02}, "picked up purple rupee in boxes (first windy bridge) (MDH)" }, + { 0x1B, 0x01, AreaFlagInd{0x1B01}, "picked up yellow rupee in Zelda's fire" }, +}; + +inline EventAreaFlags eventAreaFlagsSnowpeak[] = +{ + { 0x03, 0x01, AreaFlagInd{0x0301}, "Big chest transition cave" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "montain top explored" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "Map marker Ashei" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "montain top cs (pan towards tree)" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "Post SPR Save Prompt" }, + { 0x09, 0x80, AreaFlagInd{0x0980}, "?" }, + { 0x09, 0x40, AreaFlagInd{0x0940}, "?" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "Snowpeak top portal" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "explored second section up montain" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "explored first section up montain" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "Midna text in front of SPR" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "intro cs" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "spawn big chest transition cave" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "lit left torch transition cave" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "lit right torch transition cave" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "snow fell down next to howling stone" }, + { 0x14, 0x80, AreaFlagInd{0x1480}, "killed poe transition cave" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "killed poe next to snowpeak ruins" }, + { 0x14, 0x20, AreaFlagInd{0x1420}, "killed first poe up the montain" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "killed second poe next to lone tree on cliff" }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "killed third poe near 2 trees before howling stone" }, +}; + +inline EventAreaFlags eventAreaFlagsSPR[] = +{ + { 0x00, 0x02, AreaFlagInd{0x0002}, "PoH big chest under broken ceiling" }, + { 0x01, 0x80, AreaFlagInd{0x0180}, "left small chest behind armor in first room" }, + { 0x01, 0x40, AreaFlagInd{0x0140}, "right small chest behind armor in first room" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "PoH big chest in first room 2F" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "west small chest behind ice in north west room" }, + { 0x01, 0x08, AreaFlagInd{0x0108}, "east small chest behind ice in north west room" }, + { 0x01, 0x04, AreaFlagInd{0x0104}, "small key small chest pumpkin room" }, + { 0x01, 0x01, AreaFlagInd{0x0101}, "Ooccoo" }, + { 0x02, 0x80, AreaFlagInd{0x0280}, "south east small chest in courtyard" }, + { 0x02, 0x40, AreaFlagInd{0x0240}, "small key buried east small chest in courtyard" }, + { 0x02, 0x20, AreaFlagInd{0x0220}, "small key buried west small chest in courtyard" }, + { 0x02, 0x08, AreaFlagInd{0x0208}, "big key chest" }, + { 0x02, 0x04, AreaFlagInd{0x0204}, "pumpkin big chest" }, + { 0x03, 0x80, AreaFlagInd{0x0380}, "small key big chest in compass room 2F" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "cheese big chest" }, + { 0x03, 0x08, AreaFlagInd{0x0308}, "south small chest behind ice in courtyard" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "compass big chest" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "small chest compass room 1F" }, + //{ 0x08, 0x80, AreaFlagInd{0x0880}, "position of second block in ice puzzle room" }, + //{ 0x08, 0x40, AreaFlagInd{0x0840}, "position of second block in ice puzzle room" }, + //{ 0x08, 0x20, AreaFlagInd{0x0820}, "position of second block in ice puzzle room" }, + //{ 0x08, 0x10, AreaFlagInd{0x0810}, "position of second block in ice puzzle room" }, + //{ 0x08, 0x08, AreaFlagInd{0x0808}, "position of second block in ice puzzle room" }, + //{ 0x08, 0x04, AreaFlagInd{0x0804}, "position of third block in ice puzzle room (frozen one)" }, + //{ 0x08, 0x02, AreaFlagInd{0x0802}, "position of third block in ice puzzle room (frozen one)" }, + //{ 0x08, 0x01, AreaFlagInd{0x0801}, "position of third block in ice puzzle room (frozen one)" }, + //{ 0x09, 0x80, AreaFlagInd{0x0980}, "position of third block in ice puzzle room (frozen one)" }, + //{ 0x09, 0x40, AreaFlagInd{0x0940}, "position of third block in ice puzzle room (frozen one)" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "killed poe in armor in first room" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "unlock west living room door" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "unlock north living room door" }, + { 0x09, 0x02, AreaFlagInd{0x0902}, "lowered ball transport (west hallway-freezard)" }, + { 0x09, 0x01, AreaFlagInd{0x0901}, "lowered ball transport (courtyard 2F)" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "dug first time west buried small chest in courtyard" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "lowered ball transport (west hallway-courtyard)" }, + { 0x0A, 0x20, AreaFlagInd{0x0A20}, "dug second time west buried small chest in courtyard" }, + { 0x0A, 0x10, AreaFlagInd{0x0A10}, "talked to Yeta for the first time (gives map)" }, + { 0x0A, 0x04, AreaFlagInd{0x0A04}, "destroyed ice in courtyard 2F" }, + { 0x0A, 0x02, AreaFlagInd{0x0A02}, "Yeta enters bedroom cs (in courtyard)" }, + { 0x0A, 0x01, AreaFlagInd{0x0A01}, "unlock bedroom door" }, + { 0x0B, 0x80, AreaFlagInd{0x0B80}, "enter courtyard" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "compass big chest (set after)" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "broke ice in pumpkin room 2F" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "dug up east small chest in courtyard" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "open west door 2F in ice puzzle room" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "open west door 1F in ice puzzle room" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "destroyed first left armor in first room" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "destroyed second left armor in first room" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "destroyed third left armor in first room" }, + //{ 0x0C, 0x08, AreaFlagInd{0x0C08}, "canon in north west room orientation" }, + //{ 0x0C, 0x04, AreaFlagInd{0x0C04}, "canon in north west room orientation" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "killed freezard in courtyard 1F" }, + { 0x0C, 0x01, AreaFlagInd{0x0C01}, "explored compass room" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "killed first freezard in cage" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "killed freezard in courtyard 2F" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "killed west freezard in room above livng room (2F)" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "killed north freezard in room above livng room (2F)" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "broke ice in first room 2F" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "destroyed ice on block in ice puzzle room" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "unlock door in south east room 2F" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "unlocked door to first room in freezard cage room" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "unlock doors in room before pumpkin" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "unlock door in east outside hallway" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "unlock west door in courtyard" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "explored courtyard 1F" }, + { 0x0E, 0x02, AreaFlagInd{0x0E02}, "explored room above ice puzzle (2F)" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "explored room above pumpkin (2F)" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "puched left block in room above living room (2F)" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "puched right block in room above living room (2F)" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "2nd floor block pushed down in ice puzzle room" }, + //{ 0x0F, 0x10, AreaFlagInd{0x0F10}, "position of first block in ice puzzle room" }, + //{ 0x0F, 0x08, AreaFlagInd{0x0F08}, "position of first block in ice puzzle room" }, + //{ 0x0F, 0x04, AreaFlagInd{0x0F04}, "position of first block in ice puzzle room" }, + //{ 0x0F, 0x02, AreaFlagInd{0x0F02}, "position of first block in ice puzzle room" }, + //{ 0x0F, 0x01, AreaFlagInd{0x0F01}, "position of first block in ice puzzle room" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "Picked up B&C" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "?" }, + { 0x10, 0x20, AreaFlagInd{0x1020}, "Yeta lets you open door to kitchen" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "north west room 1F intro cs" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "Midna big key text prompt" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "pumpkin room ambush cs trigger" }, + //{ 0x10, 0x01, AreaFlagInd{0x1001}, "courtyard 2F canon orientation" }, + //{ 0x11, 0x80, AreaFlagInd{0x1180}, "courtyard 2F canon orientation" }, + //{ 0x11, 0x40, AreaFlagInd{0x1140}, "courtyard 1F canon orientation" }, + //{ 0x11, 0x20, AreaFlagInd{0x1120}, "courtyard 1F canon orientation" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "unlock mini-boss doors" }, + { 0x11, 0x01, AreaFlagInd{0x1101}, "pushed block in west outside hallway" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "unlock doors in pumpkin room" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "explored path to bedroom 2F & 3F" }, + { 0x12, 0x10, AreaFlagInd{0x1210}, "killed second freezard in cage" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "explored west outside hallway 2F (canon)" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "unlock door to big key in big key room" }, + //{ 0x12, 0x02, AreaFlagInd{0x1202}, "freezard cage canon orientation" }, + //{ 0x12, 0x01, AreaFlagInd{0x1201}, "freezard cage canon orientation" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "killed chilfos in pumpkin room (unlock south door)" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "?" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "killed chilfos in big key room" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "broke center ice in Ice puzzle room" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "broke first right armor in first room" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "broke second right armor in first room (spawns poe)" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "broke third right armor in first room" }, + { 0x14, 0x80, AreaFlagInd{0x1480}, "killed poe in south east room 2F" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "Midna big key text seen" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "explored east outside hallway 2F " }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "explored west outside hallway 1F (canonballs)" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "explored room above living room (2F)" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "explored freezard in cages room 2F" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "Intro Cutscene" }, + { 0x15, 0x80, AreaFlagInd{0x1580}, "unlock west door in pumpkin room" }, + { 0x15, 0x40, AreaFlagInd{0x1540}, "destroyed first right armor in cheese room" }, + { 0x15, 0x20, AreaFlagInd{0x1520}, "freezard in cage cs" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "courtyard intro cs" }, + { 0x15, 0x04, AreaFlagInd{0x1504}, "killed poe in center of first room" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "pumpkin room intro cs" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "?" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "?" }, + { 0x16, 0x40, AreaFlagInd{0x1640}, "Yeta points to kitchen cs" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "Midna Cheese text prompt" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "Midna Cheese text seen" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "Midna pumpkin text seen" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "Darkhammer (unsets after defeat)" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "Midna pumpkin text prompt" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "unlock exit door in big key room" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "map marker bedroom key" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "map marker ordon cheese" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "map marker ordon pumpkin" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "Blizzeta Cutscene" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "broke first ice south door in north west room 1F" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "broke second ice south door in north west room 1F" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "broke third ice south door in north west room 1F" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "broke ice wall in compass room 2F" }, +}; + +inline EventAreaFlags eventAreaFlagsToT[] = +{ + { 0x00, 0x80, AreaFlagInd{0x0080}, "map marker statue in room 2" }, + { 0x00, 0x40, AreaFlagInd{0x0040}, "map marker statue will spawn in room 2" }, + { 0x00, 0x20, AreaFlagInd{0x0020}, "map marker statue in room 3" }, + { 0x00, 0x10, AreaFlagInd{0x0010}, "map marker statue will spawn in room 3" }, + { 0x00, 0x08, AreaFlagInd{0x0008}, "map marker statue in room 4" }, + { 0x00, 0x04, AreaFlagInd{0x0004}, "map marker statue will spawn in room 4" }, + { 0x00, 0x02, AreaFlagInd{0x0002}, "map marker statue in room 5" }, + { 0x00, 0x01, AreaFlagInd{0x0001}, "map marker statue will spawn in room 5" }, + { 0x01, 0x80, AreaFlagInd{0x0180}, "map marker statue in room 6" }, + { 0x01, 0x40, AreaFlagInd{0x0140}, "map marker statue will spawn in room 6" }, + { 0x01, 0x20, AreaFlagInd{0x0120}, "map marker statue in room 7" }, + { 0x01, 0x10, AreaFlagInd{0x0110}, "map marker statue will spawn in room 7" }, + { 0x02, 0x10, AreaFlagInd{0x0210}, "PoH big chest room 4" }, + { 0x02, 0x02, AreaFlagInd{0x0202}, "small key big chest room 6 7F" }, + { 0x02, 0x01, AreaFlagInd{0x0201}, "big key chest" }, + { 0x03, 0x40, AreaFlagInd{0x0340}, "compass big chest room 4" }, + { 0x03, 0x20, AreaFlagInd{0x0320}, "PoH big chest south room 5F" }, + { 0x03, 0x10, AreaFlagInd{0x0310}, "small key big chest south room 5F" }, + { 0x03, 0x04, AreaFlagInd{0x0304}, "small chest room 2 2F" }, + { 0x03, 0x02, AreaFlagInd{0x0302}, "map big chest room 2 3F" }, + { 0x03, 0x01, AreaFlagInd{0x0301}, "dominion rod big chest" }, + { 0x04, 0x80, AreaFlagInd{0x0480}, "Ooccoo" }, + { 0x05, 0x01, AreaFlagInd{0x0501}, "small chest big key room" }, + { 0x06, 0x80, AreaFlagInd{0x0680}, "small chest room 5 7F" }, + { 0x06, 0x40, AreaFlagInd{0x0640}, "south small chest south room 5F" }, + { 0x06, 0x20, AreaFlagInd{0x0620}, "small chest room 2 3F" }, + { 0x06, 0x10, AreaFlagInd{0x0610}, "big chest room 5 6F" }, + { 0x06, 0x08, AreaFlagInd{0x0608}, "big chest room 6 8F" }, + { 0x06, 0x02, AreaFlagInd{0x0602}, "small key big chest room 1" }, + { 0x07, 0x02, AreaFlagInd{0x0702}, "map marker statue in room 1" }, + { 0x07, 0x01, AreaFlagInd{0x0701}, "map marker statue will spawn in room 1" }, + { 0x08, 0x80, AreaFlagInd{0x0880}, "Midna text asking to look at missing statue room 1" }, + { 0x08, 0x40, AreaFlagInd{0x0840}, "Midna text after looking at missing statue room 1" }, + { 0x08, 0x20, AreaFlagInd{0x0820}, "unlock door in room 6 8F" }, + { 0x08, 0x10, AreaFlagInd{0x0810}, "unlock door in room 3 5F (spawn baby and young gohmas in south room 5F)" }, + { 0x08, 0x08, AreaFlagInd{0x0808}, "unlock door in room 1" }, + { 0x08, 0x04, AreaFlagInd{0x0804}, "dominion rod big chest (set after)" }, + { 0x08, 0x02, AreaFlagInd{0x0802}, "killed poe in room 3 3F" }, + { 0x08, 0x01, AreaFlagInd{0x0801}, "killed poe in room 5 7F" }, + { 0x09, 0x20, AreaFlagInd{0x0920}, "intro cs" }, + { 0x09, 0x10, AreaFlagInd{0x0910}, "scale intro cs room 5 6F" }, + { 0x09, 0x08, AreaFlagInd{0x0908}, "change balance of scale for the first time cs" }, + { 0x09, 0x04, AreaFlagInd{0x0904}, "change balance of scale for the first time" }, + { 0x0A, 0x80, AreaFlagInd{0x0A80}, "entered room 7" }, + { 0x0A, 0x40, AreaFlagInd{0x0A40}, "spawn west big chest in south room 5F" }, + { 0x0A, 0x08, AreaFlagInd{0x0A08}, "statue getting possessed for the first time cs" }, + { 0x0B, 0x40, AreaFlagInd{0x0B40}, "deactivate statue slot in room 1 (opens door and deactivates statue)" }, + { 0x0B, 0x20, AreaFlagInd{0x0B20}, "deactivate bell in room 2 (required to spawn in room 1)" }, + { 0x0B, 0x10, AreaFlagInd{0x0B10}, "deactivate bell in room 3 (required to spawn in room 2)" }, + { 0x0B, 0x08, AreaFlagInd{0x0B08}, "deactivate bell in room 4 (required to spawn in room 3)" }, + { 0x0B, 0x04, AreaFlagInd{0x0B04}, "deactivate bell in room 5 (required to spawn in room 4)" }, + { 0x0B, 0x02, AreaFlagInd{0x0B02}, "deactivate bell in room 6 (required to spawn in room 5)" }, + { 0x0B, 0x01, AreaFlagInd{0x0B01}, "deactivate bell in room 7 (required to spawn in room 6)" }, + { 0x0C, 0x80, AreaFlagInd{0x0C80}, "unlock door in south room 5F" }, + { 0x0C, 0x40, AreaFlagInd{0x0C40}, "killed armos in room 2 3F (spawn big chest)" }, + { 0x0C, 0x20, AreaFlagInd{0x0C20}, "killed armos in big key room (unlocks door)" }, + { 0x0C, 0x10, AreaFlagInd{0x0C10}, "lit both torches in room 1 (unset if you leave) (spawn big chest)" }, + { 0x0C, 0x04, AreaFlagInd{0x0C04}, "broke gate in room 2 3F" }, + { 0x0C, 0x02, AreaFlagInd{0x0C02}, "broke fouth gate in room 2 2F" }, + { 0x0C, 0x01, AreaFlagInd{0x0C01}, "broke first gate in room 2 2F" }, + { 0x0D, 0x80, AreaFlagInd{0x0D80}, "broke third gate in room 2 2F" }, + { 0x0D, 0x40, AreaFlagInd{0x0D40}, "broke second gate in room 2 2F" }, + { 0x0D, 0x20, AreaFlagInd{0x0D20}, "broke second sliding door in room 4 4F" }, + { 0x0D, 0x10, AreaFlagInd{0x0D10}, "broke third sliding door in room 4 5F" }, + { 0x0D, 0x08, AreaFlagInd{0x0D08}, "broke second sliding door in room 4 5F" }, + { 0x0D, 0x04, AreaFlagInd{0x0D04}, "broke first sliding door in room 4 5F" }, + { 0x0D, 0x02, AreaFlagInd{0x0D02}, "broke first sliding door in room 4 4F" }, + { 0x0D, 0x01, AreaFlagInd{0x0D01}, "broke first beamos in room before boss" }, + { 0x0E, 0x80, AreaFlagInd{0x0E80}, "broke left beamos in room before boss" }, + { 0x0E, 0x40, AreaFlagInd{0x0E40}, "broke right beamos in room before boss" }, + { 0x0E, 0x20, AreaFlagInd{0x0E20}, "broke beamos in room 6 7F" }, + { 0x0E, 0x10, AreaFlagInd{0x0E10}, "broke first beamos in room 4 4F" }, + { 0x0E, 0x08, AreaFlagInd{0x0E08}, "broke second beamos in room 4 4F" }, + { 0x0E, 0x04, AreaFlagInd{0x0E04}, "broke beamos in room 3 3F" }, + { 0x0E, 0x01, AreaFlagInd{0x0E01}, "broke gate to bell in room 3 3F" }, + { 0x0F, 0x80, AreaFlagInd{0x0F80}, "broke gate to poe in room 3 3F" }, + { 0x0F, 0x40, AreaFlagInd{0x0F40}, "broke gate in room 6 8F" }, + { 0x0F, 0x20, AreaFlagInd{0x0F20}, "open door to poe in room 3 3F" }, + { 0x0F, 0x10, AreaFlagInd{0x0F10}, "spawn baby gohmas in beamos room 3 3F" }, + { 0x0F, 0x08, AreaFlagInd{0x0F08}, "broke gate in room 6 8F cs" }, + { 0x0F, 0x01, AreaFlagInd{0x0F01}, "unlock boss door" }, + { 0x10, 0x80, AreaFlagInd{0x1080}, "open big door in room 1 cs part 2" }, + { 0x10, 0x40, AreaFlagInd{0x1040}, "open big door in room 1 cs part 1" }, + { 0x10, 0x10, AreaFlagInd{0x1010}, "killed a baby gohma in room 6 8F" }, + { 0x10, 0x08, AreaFlagInd{0x1008}, "killed a baby gohma in room 6 8F" }, + { 0x10, 0x04, AreaFlagInd{0x1004}, "killed a baby gohma in room 6 8F" }, + { 0x10, 0x02, AreaFlagInd{0x1002}, "killed a baby gohma in room 6 8F" }, + { 0x10, 0x01, AreaFlagInd{0x1001}, "killed a baby gohma in room 6 8F" }, + { 0x11, 0x80, AreaFlagInd{0x1180}, "killed a baby gohma in room 6 8F" }, + { 0x11, 0x40, AreaFlagInd{0x1140}, "killed a baby gohma in room 6 8F" }, + { 0x11, 0x20, AreaFlagInd{0x1120}, "killed a baby gohma in room 6 8F" }, + { 0x11, 0x10, AreaFlagInd{0x1110}, "killed a baby gohma in room 6 8F" }, + { 0x11, 0x08, AreaFlagInd{0x1108}, "killed a baby gohma in room 6 8F" }, + { 0x11, 0x04, AreaFlagInd{0x1104}, "killed a baby gohma in room 6 8F" }, + { 0x11, 0x02, AreaFlagInd{0x1102}, "killed a baby gohma in room 6 8F" }, + { 0x12, 0x80, AreaFlagInd{0x1280}, "pressed button in room 1 (or 6 8F) for the first time cs" }, + { 0x12, 0x40, AreaFlagInd{0x1240}, "pressed button in room 6 7F for the first time cs" }, + { 0x12, 0x20, AreaFlagInd{0x1220}, "killed both armos in room 6 8F (opens gate)" }, + { 0x12, 0x08, AreaFlagInd{0x1208}, "pressed buttons in room 3 5F for the first time cs" }, + { 0x12, 0x04, AreaFlagInd{0x1204}, "pressed buttons in room 2 3F for the first time cs" }, + { 0x12, 0x02, AreaFlagInd{0x1202}, "pressed button in room 2 2F for the first time cs" }, + { 0x13, 0x40, AreaFlagInd{0x1340}, "open gate to dominion rod in room 7" }, + { 0x13, 0x20, AreaFlagInd{0x1320}, "statue spawning out of bell in room 1 cs trigger" }, + { 0x13, 0x10, AreaFlagInd{0x1310}, "statue spawning out of bell in room 1 cs" }, + { 0x13, 0x08, AreaFlagInd{0x1308}, "killed all baby gohmas in room 6 8F (spawn big chest)" }, + { 0x13, 0x04, AreaFlagInd{0x1304}, "killed all enemies in room 5 6F (spawn big chest)" }, + { 0x13, 0x02, AreaFlagInd{0x1302}, "killed west armos in south room 5F" }, + { 0x13, 0x01, AreaFlagInd{0x1301}, "killed east armos in south room 5F" }, + { 0x14, 0x40, AreaFlagInd{0x1440}, "unlock door in room 7" }, + { 0x14, 0x20, AreaFlagInd{0x1420}, "statue getting in bell in room 7 cs first part" }, + { 0x14, 0x10, AreaFlagInd{0x1410}, "statue getting in bell in room 7 cs second part" }, + { 0x14, 0x08, AreaFlagInd{0x1408}, "statue getting in bell in room 7 cs trigger" }, + { 0x14, 0x04, AreaFlagInd{0x1404}, "open gate to dominion rod in room 7 cs" }, + { 0x14, 0x02, AreaFlagInd{0x1402}, "statue spawn out of bell in room 6" }, + { 0x14, 0x01, AreaFlagInd{0x1401}, "statue spawning out of bell in room 6 cs trigger" }, + { 0x15, 0x80, AreaFlagInd{0x1580}, "statue spawning out of bell in room 6 cs part 1" }, + { 0x15, 0x40, AreaFlagInd{0x1540}, "statue getting in bell in room 6 cs trigger" }, + { 0x15, 0x20, AreaFlagInd{0x1520}, "statue getting in bell in room 6 cs" }, + { 0x15, 0x10, AreaFlagInd{0x1510}, "statue spawning out of bell in room 5 cs trigger" }, + { 0x15, 0x08, AreaFlagInd{0x1508}, "statue spawning out of bell in room 5 cs" }, + { 0x15, 0x04, AreaFlagInd{0x1504}, "statue getting in bell in room 5 cs trigger" }, + { 0x15, 0x02, AreaFlagInd{0x1502}, "statue getting in bell in room 5 cs" }, + { 0x15, 0x01, AreaFlagInd{0x1501}, "statue spawning out of bell in room 4 cs trigger" }, + { 0x16, 0x80, AreaFlagInd{0x1680}, "statue spawning out of bell in room 4 cs" }, + { 0x16, 0x40, AreaFlagInd{0x1640}, "statue getting in bell in room 4 cs trigger" }, + { 0x16, 0x20, AreaFlagInd{0x1620}, "statue getting in bell in room 4 cs" }, + { 0x16, 0x10, AreaFlagInd{0x1610}, "statue spawning out of bell in room 3 cs trigger" }, + { 0x16, 0x08, AreaFlagInd{0x1608}, "statue spawning out of bell in room 3 cs" }, + { 0x16, 0x04, AreaFlagInd{0x1604}, "statue getting in bell in room 3 cs trigger" }, + { 0x16, 0x02, AreaFlagInd{0x1602}, "statue getting in bell in room 3 cs" }, + { 0x16, 0x01, AreaFlagInd{0x1601}, "statue spawning out of bell in room 2 cs trigger" }, + { 0x17, 0x80, AreaFlagInd{0x1780}, "statue spawning out of bell in room 2 cs" }, + { 0x17, 0x40, AreaFlagInd{0x1740}, "statue getting in bell in room 2 cs trigger" }, + { 0x17, 0x20, AreaFlagInd{0x1720}, "statue getting in bell in room 2 cs" }, + { 0x17, 0x10, AreaFlagInd{0x1710}, "deactivate spawning bell in room 7" }, + { 0x17, 0x08, AreaFlagInd{0x1708}, "big door in room 1 opens" }, + { 0x17, 0x04, AreaFlagInd{0x1704}, "open big door in room 1 cs part 2 trigger" }, + { 0x17, 0x02, AreaFlagInd{0x1702}, "open big door in room 1 cs part 1 trigger" }, + { 0x17, 0x01, AreaFlagInd{0x1701}, "statue placed in slot in room 1" }, +}; + +struct MultiByteAreaFlag +{ + // flags treated as bool, shift left after checking bool + const char* name; + AreaFlagMultibit flag; + std::map enumValues; +}; + +constexpr uint16_t AREA_FLAG_NONE = 0; + +inline MultiByteAreaFlag ForestTempleMultiByteFlags[] = +{ + { + "Worm Room position", + AreaFlagMultibit{0x0D3C}, + { + { 0x0, "On Totem", }, + { 0x8, "North", }, + { 0x7, "North North East", }, + { 0x6, "North East", }, + { 0x5, "East North East", }, + { 0x4, "East", }, + { 0x3, "East South East", }, + { 0x2, "South South East", }, + { 0x1, "South", }, + { 0xF, "South South West", }, + { 0xE, "South West", }, + { 0xD, "West South West", }, + { 0xC, "West", }, + { 0xA, "West North West", }, + { 0xB, "North West", }, + { 0x9, "North North West", }, + }, + } +}; + +inline const std::map _SPRIceBlockPuzzleLocations = +{ + { 0x00, "N/A" }, + { 0x0C, "(-2, 2)" }, + { 0x0B, "(-1, 2)" }, + { 0x0A, "( 0, 2)" }, + { 0x09, "( 1, 2)" }, + { 0x08, "( 2, 2)" }, + + { 0x0D, "(-2, 1)" }, + { 0x14, "(-1, 1)" }, + { 0x15, "( 0, 1)" }, + { 0x16, "( 1, 1)" }, + { 0x07, "( 2, 1)" }, + + { 0x0E, "(-2, 0)" }, + { 0x13, "(-1, 0)" }, + { 0x1A, "( 0, 0)" }, + { 0x17, "( 1, 0)" }, + { 0x06, "( 2, 0)" }, + + { 0x0F, "(-2, -1)" }, + { 0x12, "(-1, -1)" }, + { 0x19, "( 0, -1)" }, + { 0x18, "( 1, -1)" }, + { 0x05, "( 2, -1)" }, + + { 0x10, "(-2, -2)" }, + { 0x11, "(-1, -2)" }, + { 0x02, "( 0, -2)" }, + { 0x03, "( 1, -2)" }, + { 0x04, "( 2, -2)" }, + + { 0x01, "( 0, -3)" }, +}; + +inline MultiByteAreaFlag SPRMultiByteFlags[] = { + { + "Courtyard First Floor Cannon", + AreaFlagMultibit{0x1160}, + { + { 1, "North" }, + { 0, "South" }, + { 2, "East" }, + { 3, "West" } + }, + }, + { + "Courtyard Second Floor Cannon", + AreaFlagMultibit{0x1001, 0x1180}, + { + { 3, "North East" }, + { 0, "South East" }, + { 2, "South West" }, + { 1, "North West" }, + }, + }, + { + "Freezard Cage Room Cannon", + AreaFlagMultibit{0x1203}, + { + { 0, "North" }, + { 2, "East" }, + { 1, "South" }, + { 3, "West" }, + }, + }, + { + "North West Room Cannon", + AreaFlagMultibit{0x0C0C}, + { + { 3, "North" }, + { 0, "East" }, + { 2, "South" }, + { 1, "West" }, + }, + }, + { + "Ice Block 1 Location", + AreaFlagMultibit{0x0F1F}, + _SPRIceBlockPuzzleLocations + }, + { + "Ice Block 2 Location", + AreaFlagMultibit{0x08F8}, + _SPRIceBlockPuzzleLocations + }, + { + "Ice Block 3 Location", + AreaFlagMultibit{0x0807, 0x09C0}, + _SPRIceBlockPuzzleLocations + } +}; + +inline const std::map _CoOBlockPuzzle1Locations = { + { 0x00, "N/A" }, + { 0x0B, "(-1, 2)" }, + { 0x0C, "( 0, 2)" }, + { 0x01, "( 1, 2)" }, + + { 0x0A, "(-1, 1)" }, + { 0x0D, "( 0, 1)" }, + { 0x02, "( 1, 1)" }, + + { 0x09, "(-1, 0)" }, + { 0x0E, "( 0, 0)" }, + { 0x03, "( 1, 0)" }, + + { 0x08, "(-1, -1)" }, + { 0x0F, "( 0, -1)" }, + { 0x04, "( 1, -1)" }, + + { 0x07, "(-1, -2)" }, + { 0x06, "( 0, -2)" }, + { 0x05, "( 1, -2)" }, +}; + +inline const std::map _CoOBlockPuzzle2Locations = { + { 0x00, "N/A" }, + { 0x01, "(-1, 2)" }, + { 0x02, "( 0, 2)" }, + { 0x03, "( 1, 2)" }, + + { 0x0C, "(-1, 1)" }, + { 0x0D, "( 0, 1)" }, + { 0x04, "( 1, 1)" }, + { 0x05, "( 2, 1)" }, + + { 0x0B, "(-1, 0)" }, + { 0x0E, "( 0, 0)" }, + { 0x0F, "( 1, 0)" }, + { 0x06, "( 2, 0)" }, + + { 0x0A, "(-1, -1)" }, + { 0x09, "( 0, -1)" }, + { 0x08, "( 1, -1)" }, + { 0x07, "( 2, -1)" }, +}; + +inline const std::map _CoOBlockPuzzle3Locations = { + { 0x00, "N/A" }, + { 0x01, "(-2, 2)" }, + { 0x02, "(-1, 2)" }, + { 0x03, "( 0, 2)" }, + { 0x04, "( 1, 2)" }, + { 0x05, "( 2, 2)" }, + + { 0x10, "(-2, 1)" }, + { 0x11, "(-1, 1)" }, + { 0x12, "( 0, 1)" }, + { 0x13, "( 1, 1)" }, + { 0x06, "( 2, 1)" }, + + { 0x0F, "(-2, 0)" }, + { 0x18, "(-1, 0)" }, + { 0x19, "( 0, 0)" }, + { 0x14, "( 1, 0)" }, + { 0x07, "( 2, 0)" }, + + { 0x0E, "(-2, -1)" }, + { 0x17, "(-1, -1)" }, + { 0x16, "( 0, -1)" }, + { 0x15, "( 1, -1)" }, + { 0x08, "( 2, -1)" }, + + { 0x0D, "(-2, -2)" }, + { 0x0C, "(-1, -2)" }, + { 0x0B, "( 0, -2)" }, + { 0x0A, "( 1, -2)" }, + { 0x09, "( 2, -2)" }, +}; + +inline MultiByteAreaFlag CoOMultiByteFlags[] = { + { + "Puzzle 1 Block 1 Location", + AreaFlagMultibit{0x0907, 0x0A80}, + _CoOBlockPuzzle1Locations + }, + { + "Puzzle 1 Block 2 Location", + AreaFlagMultibit{0x0A3C}, + _CoOBlockPuzzle1Locations + }, + { + "Puzzle 1 Block 3 Location", + AreaFlagMultibit{0x0B0F}, + _CoOBlockPuzzle1Locations + }, + { + "Puzzle 2 Block 1 Location", + AreaFlagMultibit{0x08F0}, + _CoOBlockPuzzle2Locations + }, + { + "Puzzle 2 Block 2 Location", + AreaFlagMultibit{0x080F}, + _CoOBlockPuzzle2Locations + }, + { + "Puzzle 2 Block 3 Location", + AreaFlagMultibit{0x09F0}, + _CoOBlockPuzzle2Locations + }, + { + "Puzzle 3 Block 1 Location", + AreaFlagMultibit{0x0E7C}, + _CoOBlockPuzzle3Locations + }, + { + "Puzzle 3 Block 2 Location", + AreaFlagMultibit{0x0E03, 0x0FE0}, + _CoOBlockPuzzle3Locations + }, + { + "Puzzle 3 Block 3 Location", + AreaFlagMultibit{0x0F1F}, + _CoOBlockPuzzle3Locations + } +}; + +struct AreaFlagIter { + std::span bitFlags; + std::span multibyteFlags; +}; + +inline std::map imguiAreaFlagLookup = +{ + { 0x00, AreaFlagIter{ eventAreaFlagsOrdon, {} } }, + { 0x01, AreaFlagIter{ eventAreaFlagsSewer, {} } }, + { 0x02, AreaFlagIter{ eventAreaFlagsFaron, {} } }, + { 0x03, AreaFlagIter{ eventAreaFlagsEldin, {} } }, + { 0x04, AreaFlagIter{ eventAreaFlagsLanayru, {} } }, + // 5 unused + { 0x06, AreaFlagIter{ eventAreaFlagsHyruleField, {} } }, + { 0x07, AreaFlagIter{ eventAreaFlagsSacredGrove, {} } }, + { 0x08, AreaFlagIter{ eventAreaFlagsSnowpeak, {} } }, + { 0x09, AreaFlagIter{ eventAreaFlagsCastleTown, {} } }, + { 0x0A, AreaFlagIter{ eventAreaFlagsGerudoDesert, {} } }, + { 0x0B, AreaFlagIter{ eventAreaFlagsFishingPond, {} } }, + // C-F unused + { 0x10, AreaFlagIter{ eventAreaFlagsFT, ForestTempleMultiByteFlags } }, + { 0x11, AreaFlagIter{ eventAreaFlagsGM, {} } }, + { 0x12, AreaFlagIter{ eventAreaFlagsLBT, {} } }, + { 0x13, AreaFlagIter{ eventAreaFlagsAG, {} } }, + { 0x14, AreaFlagIter{ eventAreaFlagsSPR, SPRMultiByteFlags } }, + { 0x15, AreaFlagIter{ eventAreaFlagsToT, {} } }, + { 0x16, AreaFlagIter{ eventAreaFlagsCitS, {} } }, + { 0x17, AreaFlagIter{ eventAreaFlagsPoT, {} } }, + { 0x18, AreaFlagIter{ eventAreaFlagsHC, {} } }, + { 0x19, AreaFlagIter{ eventAreaFlagsCoO, CoOMultiByteFlags } }, + { 0x1A, AreaFlagIter{ eventAreaFlagsLHLC, {} } }, + { 0x1B, AreaFlagIter{ eventAreaFlagsGrotto, {} } }, +}; + +#endif // !DUSK_IMGUI_EVENTFLAGS_HPP diff --git a/src/dusk/imgui/ImGuiFirstRunPreset.cpp b/src/dusk/imgui/ImGuiFirstRunPreset.cpp deleted file mode 100644 index fb2d181562..0000000000 --- a/src/dusk/imgui/ImGuiFirstRunPreset.cpp +++ /dev/null @@ -1,137 +0,0 @@ -#include "ImGuiFirstRunPreset.hpp" - -#include "imgui.h" -#include "ImGuiConsole.hpp" -#include "ImGuiEngine.hpp" -#include "dusk/settings.h" -#include "dusk/config.hpp" -#include - -namespace dusk { - -static void ApplyPresetClassic() { - auto& s = getSettings(); - s.video.lockAspectRatio.setValue(true); - s.game.bloomMode.setValue(BloomMode::Classic); - AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); -} - -static void ApplyPresetHD() { - auto& s = getSettings(); - s.game.bloomMode.setValue(BloomMode::Classic); - s.game.hideTvSettingsScreen.setValue(true); - s.game.skipWarningScreen.setValue(true); - s.game.noReturnRupees.setValue(true); - s.game.disableRupeeCutscenes.setValue(true); - s.game.noSwordRecoil.setValue(true); - s.game.fastClimbing.setValue(true); - s.game.noMissClimbing.setValue(true); - s.game.fastTears.setValue(true); - s.game.biggerWallets.setValue(true); - s.game.invertCameraXAxis.setValue(true); -} - -static void ApplyPresetDusk() { - ApplyPresetHD(); - - auto& s = getSettings(); - s.game.enableQuickTransform.setValue(true); - s.game.instantSaves.setValue(true); - s.game.midnasLamentNonStop.setValue(true); - s.game.enableFrameInterpolation.setValue(true); - s.game.sunsSong.setValue(true); - s.game.bloomMode.setValue(BloomMode::Dusk); -} - -// ========================================================================= - -void ImGuiFirstRunPreset::draw() { - const char* modalTitle = "Welcome to Dusk!"; - - if (m_done) return; - - if (!m_opened) { - ImGui::OpenPopup(modalTitle); - m_opened = true; - } - - const ImGuiViewport* viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(800.0f * ImGuiScale(), 0.0f), ImGuiCond_Always); - - if (!ImGui::BeginPopupModal(modalTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { - // force the user to actually pick one, and not just hit escape to skip the dialog - m_opened = false; - return; - } - - ImGui::TextWrapped("Choose a preset to get started. You can change any setting later from the Enhancements menu."); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - int chosen = -1; - - if (ImGui::BeginTable("##presets", 5, ImGuiTableFlags_None)) { - ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthFixed, 16.0f * ImGuiScale()); - ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthFixed, 16.0f * ImGuiScale()); - ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(); - - ImGui::PushFont(ImGuiEngine::fontLarge); - - ImGui::TableSetColumnIndex(0); - if (ImGui::Button("Classic##btn", ImVec2(ImGui::GetContentRegionAvail().x, 80.0f * ImGuiScale()))) { - chosen = 0; - } - - ImGui::TableSetColumnIndex(2); - if (ImGui::Button("HD##btn", ImVec2(ImGui::GetContentRegionAvail().x, 80.0f * ImGuiScale()))) { - chosen = 1; - } - - ImGui::TableSetColumnIndex(4); - if (ImGui::Button("Dusk##btn", ImVec2(ImGui::GetContentRegionAvail().x, 80.0f * ImGuiScale()))) - { - chosen = 2; - } - - ImGui::PopFont(); - - ImGui::TableNextRow(); - - ImGui::TableSetColumnIndex(0); - ImGui::Spacing(); - ImGui::TextWrapped("All enhancements disabled to match the GameCube version. Good for speedrunning or simple nostalgia!"); - - ImGui::TableSetColumnIndex(2); - ImGui::Spacing(); - ImGui::TextWrapped("Some enhancements enabled to match the HD version. A good starting point for most players!"); - - ImGui::TableSetColumnIndex(4); - ImGui::Spacing(); - ImGui::TextWrapped("More enhancements enabled than the HD preset. Veteran players will appreciate the additional tweaks!"); - - ImGui::EndTable(); - } - - if (chosen >= 0) { - if (chosen == 0) ApplyPresetClassic(); - if (chosen == 1) ApplyPresetHD(); - if (chosen == 2) ApplyPresetDusk(); - - getSettings().backend.wasPresetChosen.setValue(true); - config::Save(); - - m_done = true; - - ImGui::CloseCurrentPopup(); - } - - ImGui::EndPopup(); -} - -} // namespace dusk diff --git a/src/dusk/imgui/ImGuiFirstRunPreset.hpp b/src/dusk/imgui/ImGuiFirstRunPreset.hpp deleted file mode 100644 index 33ba6b3d0f..0000000000 --- a/src/dusk/imgui/ImGuiFirstRunPreset.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -namespace dusk { - -class ImGuiFirstRunPreset { -public: - void draw(); - -private: - bool m_opened = false; - bool m_done = false; -}; - -} // namespace dusk diff --git a/src/dusk/imgui/ImGuiHeapOverlay.cpp b/src/dusk/imgui/ImGuiHeapOverlay.cpp index 577db12fc1..33209ab0d4 100644 --- a/src/dusk/imgui/ImGuiHeapOverlay.cpp +++ b/src/dusk/imgui/ImGuiHeapOverlay.cpp @@ -22,7 +22,9 @@ namespace dusk { void ShowHeapDetailed(JKRHeap* heap, OpenHeapData& data, bool& open); void ImGuiMenuTools::ShowHeapOverlay() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F4, m_showHeapOverlay)) { + if (!getSettings().backend.enableAdvancedSettings || + !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F4, m_showHeapOverlay)) + { return; } @@ -180,9 +182,9 @@ namespace dusk { void ShowHeapDetailed(JKRHeap* heap, OpenHeapData& data, bool& open) { char title[128]; const char* name = data.Safe ? heap->getName() : "INVALID"; - snprintf(title, sizeof(title), "Heap %s##%p", heap->getName(), static_cast(heap)); + snprintf(title, sizeof(title), "Heap %s##%p", name, static_cast(heap)); - if (!ImGui::Begin(name, &open)) { + if (!ImGui::Begin(title, &open)) { ImGui::End(); return; } @@ -195,7 +197,7 @@ namespace dusk { heap->lock(); - ImGui::Text("Name: %s", heap->getName()); + ImGui::Text("Name: %s", name); const auto size = BytesToString(heap->getSize()); const auto freeSize = BytesToString(heap->getFreeSize()); ImGui::Text("Size: %08X (%s), free: %08X (%s)", heap->getSize(), size.c_str(), heap->getFreeSize(), freeSize.c_str()); diff --git a/src/dusk/imgui/ImGuiMapLoader.cpp b/src/dusk/imgui/ImGuiMapLoader.cpp index 63f4befb44..cd6f292152 100644 --- a/src/dusk/imgui/ImGuiMapLoader.cpp +++ b/src/dusk/imgui/ImGuiMapLoader.cpp @@ -9,7 +9,9 @@ namespace dusk { void ImGuiMenuTools::ShowMapLoader() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader)) { + if (!getSettings().backend.enableAdvancedSettings || + !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader)) + { return; } diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 7e54f2ffa3..783bbbdeec 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -1,974 +1,100 @@ #include "fmt/format.h" #include "imgui.h" -#include "ImGuiEngine.hpp" #include "ImGuiConsole.hpp" -#include "ImGuiMenuGame.hpp" #include "ImGuiConfig.hpp" -#include "JSystem/JUtility/JUTGamePad.h" -#include "dusk/audio/DuskAudioSystem.h" -#include "dusk/audio/DuskDsp.hpp" #include "dusk/main.h" -#include "dusk/hotkeys.h" -#include "dusk/settings.h" -#include "m_Do/m_Do_controller_pad.h" -#include "m_Do/m_Do_graphic.h" - -#include -#include - -namespace { -constexpr int kInternalResolutionScaleMax = 12; -} // namespace - -namespace aurora::gx { -extern bool enableLodBias; -} +#include "m_Do/m_Do_main.h" namespace dusk { - void ImGuiMenuGame::ToggleFullscreen() { - getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen); - VISetWindowFullscreen(getSettings().video.enableFullscreen); - config::Save(); - } - ImGuiMenuGame::ImGuiMenuGame() {} - void ImGuiMenuGame::draw() { - if (ImGui::BeginMenu("Settings")) { - drawAudioMenu(); - drawCheatsMenu(); - drawGameplayMenu(); - drawGraphicsMenu(); - drawInputMenu(); - drawInterfaceMenu(); + void ImGuiMenuGame::draw() {} - ImGui::Separator(); + static std::string GetFormattedTime(OSTime ticks) { + OSCalendarTime time; + OSTicksToCalendarTime(ticks, &time); - if (ImGui::MenuItem("Reset", hotkeys::DO_RESET)) { - JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; - } - - if (!IsMobile && ImGui::MenuItem("Exit")) { - dusk::IsRunning = false; - } - - ImGui::EndMenu(); - } + return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", time.hour, time.min, time.sec, time.msec); } - void ImGuiMenuGame::drawGraphicsMenu() { - if (ImGui::BeginMenu("Graphics")) { - ImGui::SeparatorText("Display"); + void ImGuiMenuGame::resetForSpeedrunMode() { + // reset settings that should be off for speedrun mode + mDoMain::developmentMode = -1; - if (!IsMobile) { - if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { - ToggleFullscreen(); - } + getSettings().game.damageMultiplier.setValue(1); + getSettings().game.instantDeath.setValue(false); + getSettings().game.noHeartDrops.setValue(false); - if (ImGui::Button("Restore Default Window Size")) { - getSettings().video.enableFullscreen.setValue(false); - VISetWindowFullscreen(false); - VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); - VICenterWindow(); - } - } + getSettings().game.infiniteHearts.setValue(false); + getSettings().game.infiniteArrows.setValue(false); + getSettings().game.infiniteBombs.setValue(false); + getSettings().game.infiniteOil.setValue(false); + getSettings().game.infiniteOxygen.setValue(false); + getSettings().game.infiniteRupees.setValue(false); + getSettings().game.enableIndefiniteItemDrops.setValue(false); - ImGui::Separator(); + getSettings().game.moonJump.setValue(false); + getSettings().game.superClawshot.setValue(false); + getSettings().game.alwaysGreatspin.setValue(false); + getSettings().game.enableFastIronBoots.setValue(false); + getSettings().game.canTransformAnywhere.setValue(false); + getSettings().game.fastSpinner.setValue(false); + getSettings().game.freeMagicArmor.setValue(false); - bool vsync = getSettings().video.enableVsync; - if (ImGui::Checkbox("Enable VSync", &vsync)) { - getSettings().video.enableVsync.setValue(vsync); - aurora_enable_vsync(vsync); - config::Save(); - } - - bool lockAspect = getSettings().video.lockAspectRatio; - if (ImGui::Checkbox("Force 4:3 Aspect Ratio", &lockAspect)) { - getSettings().video.lockAspectRatio.setValue(lockAspect); - - if (lockAspect) { - AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); - } else { - AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); - } - - config::Save(); - } - - ImGui::SeparatorText("Resolution"); - - u32 internalResolutionWidth = 0; - u32 internalResolutionHeight = 0; - AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight); - ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth, - internalResolutionHeight); - - int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, - kInternalResolutionScaleMax); - if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax, - scale == 0 ? "Auto" : "%dx")) - { - getSettings().game.internalResolutionScale.setValue(scale); - VISetFrameBufferScale(static_cast(scale)); - config::Save(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Auto renders at the native window resolution.\n" - "Higher values scale the game's internal framebuffer."); - } - - config::ImGuiSliderInt("Shadow Resolution", getSettings().game.shadowResolutionMultiplier, 1, 8, "x%d"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Improves the shadow resolution, making them higher quality."); - } - - ImGui::SeparatorText("Post-Processing"); - - constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"}; - int bloomMode = static_cast(getSettings().game.bloomMode.getValue()); - if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) { - for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) { - const bool selected = bloomMode == i; - if (ImGui::Selectable(bloomModeNames[i], selected)) { - getSettings().game.bloomMode.setValue(static_cast(i)); - config::Save(); - } - if (selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - bool bloomOff = bloomMode == static_cast(BloomMode::Off); - if (bloomOff) ImGui::BeginDisabled(); - float mult = getSettings().game.bloomMultiplier.getValue(); - if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) { - getSettings().game.bloomMultiplier.setValue(mult); - config::Save(); - } - if (bloomOff) ImGui::EndDisabled(); - - ImGui::SeparatorText("Rendering"); - - config::ImGuiCheckbox("Unlock Framerate", getSettings().game.enableFrameInterpolation); - const bool frameInterpolationHovered = ImGui::IsItemHovered(); - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); - ImGui::TextUnformatted("[EXPERIMENTAL]"); - ImGui::PopStyleColor(); - if (frameInterpolationHovered || ImGui::IsItemHovered()) { - ImGui::SetTooltip("Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, animation glitches, or instability may occur."); - } - - ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); - - config::ImGuiCheckbox("Enable Depth of Field", getSettings().game.enableDepthOfField); - - ImGui::EndMenu(); - } + getSettings().game.enableTurboKeybind.setValue(false); + getSettings().game.debugFlyCam.setValue(false); + getSettings().game.autoSave.setValue(false); } - void ImGuiMenuGame::drawGameplayMenu() { - if (ImGui::BeginMenu("Gameplay")) { - ImGui::SeparatorText("General"); + SpeedrunInfo m_speedrunInfo; - config::ImGuiCheckbox("Mirror Mode", getSettings().game.enableMirrorMode); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Mirrors the world horizontally, matching the Wii version of the game."); - } - - config::ImGuiCheckbox("Disable Main HUD", getSettings().game.disableMainHUD); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disables the main HUD of the game.\n" - "Useful for recording or a more immersive experience!"); - } - - config::ImGuiCheckbox("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n" - "the first released version."); - } - - config::ImGuiCheckbox("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables rotating Link in the collection menu with the C-Stick"); - } - - ImGui::SeparatorText("Difficulty"); - - config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); - - config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Any hit will instantly kill you."); - } - - config::ImGuiCheckbox("No Heart Drops", getSettings().game.noHeartDrops); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hearts will never drop from enemies,\n" - "pots and various other places."); - } - - ImGui::SeparatorText("Quality of Life"); - - config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Wallet sizes are like in the HD version. (500, 1000, 2000)"); - } - - config::ImGuiCheckbox("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time."); - } - - config::ImGuiCheckbox("Faster Climbing", getSettings().game.fastClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version."); - } - - config::ImGuiCheckbox("Faster Tears of Light", getSettings().game.fastTears); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version."); - } - - config::ImGuiCheckbox("Instant Saves", getSettings().game.instantSaves); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the delay when writing to the Memory Card."); - } - - config::ImGuiCheckbox("Hold B for Instant Text", getSettings().game.instantText); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Make text scroll immediately by holding B."); - } - - config::ImGuiCheckbox("No Climbing Miss Animation", getSettings().game.noMissClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents Link from playing a struggle animation\n" - "when grabbing ledges or climbing on vines."); - } - - config::ImGuiCheckbox("No Rupee Returns", getSettings().game.noReturnRupees); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full."); - } - - config::ImGuiCheckbox("No Sword Recoil", getSettings().game.noSwordRecoil); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Link won't recoil when his sword hits walls."); - } - - config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save."); - } - - config::ImGuiCheckbox("Skip Warning Screen", getSettings().game.skipWarningScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the warning screen shown when starting the game."); - } - - config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day."); - } - - config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); - } - - ImGui::EndMenu(); - } - } - - void ImGuiMenuGame::drawCheatsMenu() { - if (ImGui::BeginMenu("Cheats")) { - ImGui::SeparatorText("Resources"); - config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); - config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); - config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs); - config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); - config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); - config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); - config::ImGuiCheckbox("Items Don't Despawn", getSettings().game.enableIndefiniteItemDrops); - ImGui::SetItemTooltip("Items Don't Despawn Unless You Load A Different Room In Which Case They Do But Even Under Some Circumstances They Don't, It Is Quite Rare Though"); - - ImGui::SeparatorText("Abilities"); - config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); - config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot); - config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin); - config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots); - - config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows you to transform even if NPCs are looking."); - } - - config::ImGuiCheckbox("Fast Spinner", getSettings().game.fastSpinner); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Speeds up Spinner movement when holding R."); - } - - config::ImGuiCheckbox("Free Magic Armor", getSettings().game.freeMagicArmor); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Makes the magic armor work without rupees."); - } - - ImGui::EndMenu(); - } - } - - void ImGuiMenuGame::drawAudioMenu() { - if (ImGui::BeginMenu("Audio")) { - - ImGui::SeparatorText("Volume"); - - ImGui::Text("Master Volume"); - if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { - dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); - } - - /* - // TODO: Implement additional settings - ImGui::Text("Main Music Volume"); - ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0, 100); - - ImGui::Text("Sub Music Volume"); - ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0, 100); - - ImGui::Text("Sound Effects Volume"); - ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0, 100); - - ImGui::Text("Fanfare Volume"); - ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0, 100); - - Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface(); - if (audioMgr != nullptr) { - } - */ - - ImGui::SeparatorText("Effects"); - - if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { - dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); - } - - - ImGui::SeparatorText("Tweaks"); - - config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disable the beeping sound when having low health."); - } - - config::ImGuiCheckbox("Non-Stop Midna's Lament", getSettings().game.midnasLamentNonStop); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents enemy music while Midna's Lament is playing."); - } - - ImGui::EndMenu(); - } - } - - void ImGuiMenuGame::drawInputMenu() { - if (ImGui::BeginMenu("Input")) { - ImGui::SeparatorText("Controller"); - - if (ImGui::Button("Configure Controller")){ - m_showControllerConfig = !m_showControllerConfig; - } - - ImGui::SeparatorText("Camera"); - - config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); - - ImGui::SeparatorText("Gyro"); - - config::ImGuiCheckbox("Gyro Aim", getSettings().game.enableGyroAim); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers\n" - "while in look mode (C-Up) and while aiming the\n" - "Slingshot, Gale Boomerang, Hero's Bow, Clawshot(s),\n" - "Ball and Chain, and Dominion Rod."); - } - - config::ImGuiCheckbox("Gyro Rollgoal", getSettings().game.enableGyroRollgoal); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers to\n" - "tilt the Rollgoal table in Hena's Cabin."); - } - - if (getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Gyro Pitch Sensitivity", getSettings().game.gyroSensitivityY, 0.25f, 4.0f, "%.2f"); - config::ImGuiSliderFloat("Gyro Yaw Sensitivity", getSettings().game.gyroSensitivityX, 0.25f, 4.0f, "%.2f"); - - if (getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Rollgoal Sensitivity", getSettings().game.gyroSensitivityRollgoal, 0.25f, 4.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Additional multiplier for scaling how strongly\n" - "the gyroscope affects the Rollgoal table."); - } - } - - config::ImGuiSliderFloat("Gyro Deadband", getSettings().game.gyroDeadband, 0.0f, 0.5f, "%.3f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Angular rates below this magnitude are treated as zero,\n" - "reducing drift and jitter when the controller is still."); - } - - config::ImGuiSliderFloat("Gyro Smoothing", getSettings().game.gyroSmoothing, 0.0f, 1.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Low values track raw gyro input more closely,\n" - "while higher values smooth out input over time."); - } - - config::ImGuiCheckbox("Invert Gyro Pitch", getSettings().game.gyroInvertPitch); - config::ImGuiCheckbox("Invert Gyro Yaw", getSettings().game.gyroInvertYaw); - } - - ImGui::SeparatorText("Tools"); - - config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); - } - - ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); - - ImGui::EndMenu(); - } - } - - void ImGuiMenuGame::drawInterfaceMenu() { - if (ImGui::BeginMenu("Interface")) { - config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); - config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); -#if DUSK_ENABLE_SENTRY_NATIVE - config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); -#endif - if (!IsMobile) { - config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost); - } - - ImGui::EndMenu(); - } - } - - static void drawVirtualStick(const char* id, const ImVec2& stick) { - float scale = ImGuiScale(); - ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 45 * scale, ImGui::GetCursorPos().y + 10)); - - ImGui::BeginChild(id, ImVec2(80 * scale, 80 * scale), 0, ImGuiWindowFlags_NoBackground); - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImVec2 p = ImGui::GetCursorScreenPos(); - - float radius = 30.0f * scale; - ImVec2 pos = ImVec2(p.x + radius, p.y + radius); - - constexpr ImU32 stickGray = IM_COL32(150, 150, 150, 255); - constexpr ImU32 white = IM_COL32(255, 255, 255, 255); - constexpr ImU32 red = IM_COL32(230, 0, 0, 255); - - dl->AddCircleFilled(pos, radius, stickGray, 8); - dl->AddCircleFilled(ImVec2(pos.x + stick.x * (radius), pos.y + -stick.y * (radius)), 3 * scale, red); - ImGui::EndChild(); - } - - struct SpecificButtonName { - SDL_GamepadType Type; - const char* Name; - }; - - struct ButtonNames { - SDL_GamepadButton Button; - std::vector Names; - }; - -// clang-format off - static const std::vector GamepadButtonNames = { - { SDL_GAMEPAD_BUTTON_LEFT_STICK, { - {SDL_GAMEPAD_TYPE_PS3, "L3"}, - {SDL_GAMEPAD_TYPE_PS4, "L3"}, - {SDL_GAMEPAD_TYPE_PS5, "L3"}, - {SDL_GAMEPAD_TYPE_XBOX360, "Left Stick"}, - {SDL_GAMEPAD_TYPE_XBOXONE, "Left Stick"}, - {SDL_GAMEPAD_TYPE_GAMECUBE, "Control Stick"}, - }}, - { SDL_GAMEPAD_BUTTON_RIGHT_STICK, { - {SDL_GAMEPAD_TYPE_PS3, "R3"}, - {SDL_GAMEPAD_TYPE_PS4, "R3"}, - {SDL_GAMEPAD_TYPE_PS5, "R3"}, - {SDL_GAMEPAD_TYPE_XBOX360, "Right Stick"}, - {SDL_GAMEPAD_TYPE_XBOXONE, "Right Stick"}, - {SDL_GAMEPAD_TYPE_GAMECUBE, "C Stick"}, - }}, - { SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, { - {SDL_GAMEPAD_TYPE_PS3, "L1"}, - {SDL_GAMEPAD_TYPE_PS4, "L1"}, - {SDL_GAMEPAD_TYPE_PS5, "L1"}, - {SDL_GAMEPAD_TYPE_XBOX360, "LB"}, - {SDL_GAMEPAD_TYPE_XBOXONE, "LB"}, - }}, - { SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, { - {SDL_GAMEPAD_TYPE_PS3, "R1"}, - {SDL_GAMEPAD_TYPE_PS4, "R1"}, - {SDL_GAMEPAD_TYPE_PS5, "R1"}, - {SDL_GAMEPAD_TYPE_XBOX360, "RB"}, - {SDL_GAMEPAD_TYPE_XBOXONE, "RB"}, - {SDL_GAMEPAD_TYPE_GAMECUBE, "Z"}, - }}, - { SDL_GAMEPAD_BUTTON_BACK, { - {SDL_GAMEPAD_TYPE_PS3, "Select"}, - {SDL_GAMEPAD_TYPE_PS4, "Share"}, - {SDL_GAMEPAD_TYPE_PS5, "Create"}, - {SDL_GAMEPAD_TYPE_XBOX360, "Back"}, - {SDL_GAMEPAD_TYPE_XBOXONE, "View"}, - }}, - { SDL_GAMEPAD_BUTTON_START, { - {SDL_GAMEPAD_TYPE_PS3, "Start"}, - {SDL_GAMEPAD_TYPE_PS4, "Options"}, - {SDL_GAMEPAD_TYPE_PS5, "Options"}, - {SDL_GAMEPAD_TYPE_XBOX360, "Start"}, - {SDL_GAMEPAD_TYPE_XBOXONE, "Menu"}, - {SDL_GAMEPAD_TYPE_GAMECUBE, "Start/Pause"}, - }}, - }; -// clang-format on - - static const char* GetNameForGamepadButton(SDL_Gamepad* gamepad, u32 buttonUntyped) { - if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) { - return "Not bound"; - } - - auto button = static_cast(buttonUntyped); - auto label = SDL_GetGamepadButtonLabel(gamepad, button); - - switch (label) { - case SDL_GAMEPAD_BUTTON_LABEL_A: - return "A"; - case SDL_GAMEPAD_BUTTON_LABEL_B: - return "B"; - case SDL_GAMEPAD_BUTTON_LABEL_X: - return "X"; - case SDL_GAMEPAD_BUTTON_LABEL_Y: - return "Y"; - case SDL_GAMEPAD_BUTTON_LABEL_CROSS: - return "Cross"; - case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: - return "Circle"; - case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: - return "Triangle"; - case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: - return "Square"; - default:; // Fall through - } - - auto padType = SDL_GetGamepadType(gamepad); - for (const auto& buttonNames : GamepadButtonNames) { - if (buttonNames.Button != button) { - continue; - } - - for (const auto& name : buttonNames.Names) { - if (name.Type == padType) { - return name.Name; - } - } - } - - switch (button) { - case SDL_GAMEPAD_BUTTON_DPAD_LEFT: - return "D-pad left"; - case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: - return "D-pad right"; - case SDL_GAMEPAD_BUTTON_DPAD_UP: - return "D-pad up"; - case SDL_GAMEPAD_BUTTON_DPAD_DOWN: - return "D-pad down"; - default: - return PADGetNativeButtonName(buttonUntyped); - } - } - - void ImGuiMenuGame::windowControllerConfig() { - if (!m_showControllerConfig) { + void ImGuiMenuGame::drawSpeedrunTimerOverlay() { + if (!getSettings().game.speedrunMode) { return; } - // if pending for a button mapping, check to set new input - if (m_controllerConfig.m_pendingButtonMapping != nullptr) { - s32 nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort); - if (nativeButton != -1) { - m_controllerConfig.m_pendingButtonMapping->nativeButton = nativeButton; - m_controllerConfig.m_pendingButtonMapping = nullptr; - m_controllerConfig.m_pendingPort = -1; - PADBlockInput(false); - PADSerializeMappings(); + // L+R+A+Start to reset timer + if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) { + m_speedrunInfo.reset(); + } + + // L+R+A+Z to manually stop timer + if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) { + if (m_speedrunInfo.m_isRunStarted) { + m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp; + m_speedrunInfo.m_isRunStarted = false; } } - // if pending for an axis mapping, check to set new input - if (m_controllerConfig.m_pendingAxisMapping != nullptr) { - auto nativeAxis = PADGetNativeAxisPulled(m_controllerConfig.m_pendingPort); - if (nativeAxis.nativeAxis != -1) { - m_controllerConfig.m_pendingAxisMapping->nativeAxis = nativeAxis; - m_controllerConfig.m_pendingAxisMapping->nativeButton = -1; - m_controllerConfig.m_pendingAxisMapping = nullptr; - m_controllerConfig.m_pendingPort = -1; - PADBlockInput(false); - PADSerializeMappings(); - } else { - auto nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort); - if (nativeButton != -1) { - m_controllerConfig.m_pendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE}; - m_controllerConfig.m_pendingAxisMapping->nativeButton = nativeButton; - m_controllerConfig.m_pendingAxisMapping = nullptr; - m_controllerConfig.m_pendingPort = -1; - PADBlockInput(false); - PADSerializeMappings(); - } + ImGui::SetNextWindowBgAlpha(0.65f); + ImGuiWindowFlags flags = + ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoDocking + | ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoScrollbar; + + if (ImGui::Begin("##SpeedrunTimerWindow", nullptr, flags)) { + OSTime elapsedTime = 0; + if (m_speedrunInfo.m_isRunStarted) { + elapsedTime = OSGetTime() - m_speedrunInfo.m_startTimestamp; + } else if (m_speedrunInfo.m_endTimestamp != 0) { + elapsedTime = m_speedrunInfo.m_endTimestamp; } - } - float scale = ImGuiScale(); - ImGuiWindowFlags windowFlags = - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_AlwaysAutoResize; + ImGui::Text("RTA"); + ImGui::SameLine(60.0f); + ImGuiStringViewText(GetFormattedTime(elapsedTime)); - // ImGui::SetNextWindowBgAlpha(0.65f); - - if (!ImGui::Begin("Controller Config", &m_showControllerConfig, windowFlags)) { - ImGui::End(); - return; - } - - // port tabs - ImGui::BeginTabBar("##ControllerTabs"); - for (int i = PAD_1; i <= PAD_4; i++) { - if (ImGui::BeginTabItem(fmt::format("Port {}", i + 1).c_str())) { - m_controllerConfig.m_selectedPort = i; - ImGui::EndTabItem(); + if (!m_speedrunInfo.m_isPauseIGT) { + m_speedrunInfo.m_igtTimer = elapsedTime - m_speedrunInfo.m_totalLoadTime; } + + ImGui::Text("IGT"); + ImGui::SameLine(60.0f); + ImGuiStringViewText(GetFormattedTime(m_speedrunInfo.m_igtTimer)); } - ImGui::EndTabBar(); - - // if tab is changed while waiting for input, cancel pending - if ((m_controllerConfig.m_pendingButtonMapping != nullptr || - m_controllerConfig.m_pendingAxisMapping != nullptr) && - m_controllerConfig.m_pendingPort != m_controllerConfig.m_selectedPort) - { - m_controllerConfig.m_pendingButtonMapping = nullptr; - m_controllerConfig.m_pendingAxisMapping = nullptr; - m_controllerConfig.m_pendingPort = -1; - PADBlockInput(false); - } - - // get a list of all available controller's names - std::vector controllerList; - controllerList.push_back("None"); - for (int i = 0; i < PADCount(); i++) { - // attach index to name for unique name - controllerList.push_back(fmt::format("{}-{}", PADGetNameForControllerIndex(i), i)); - } - - // get current controller name - const char* tmpName = PADGetName(m_controllerConfig.m_selectedPort); - std::string currentName = "None"; - if (tmpName != nullptr) { - currentName = fmt::format("{}-{}", tmpName, PADGetIndexForPort(m_controllerConfig.m_selectedPort)); - } - - // controller selection combo box - bool changedController = false; - int changedControllerIndex = 0; - ImGui::SetNextItemWidth(400.0f * scale); - if (ImGui::BeginCombo("##ControllerDeviceList", currentName.c_str())) { - for (int i = 0; const auto& name : controllerList) { - if (ImGui::Selectable(name.c_str(), currentName == name)) { - changedControllerIndex = i; - changedController = true; - } - i++; - } - ImGui::EndCombo(); - } - - // handle controller change - if (changedController) { - if (changedControllerIndex > 0) { - PADSetPortForIndex(changedControllerIndex - 1, m_controllerConfig.m_selectedPort); - } - else if (changedControllerIndex == 0) { - // if "None" selected - PADClearPort(m_controllerConfig.m_selectedPort); - } - PADSerializeMappings(); - } - - // restore defaults button - ImGui::SameLine(); - if (ImGui::Button("Default")) { - PADRestoreDefaultMapping(m_controllerConfig.m_selectedPort); - PADSerializeMappings(); - } - - // buttons panel - const float uiButtonSize = 40 * scale; - ImVec2 btnSize(110.0f * scale, 30.0f * scale); - - ImGuiBeginGroupPanel("Buttons", ImVec2(150 * scale, 20 * scale)); - - SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(PADGetIndexForPort(m_controllerConfig.m_selectedPort)); - u32 buttonCount; - PADButtonMapping* btnMappingList = PADGetButtonMappings(m_controllerConfig.m_selectedPort, &buttonCount); - if (btnMappingList != nullptr) { - for (int i = 0; i < buttonCount; i++) { - const char* btnName = PADGetButtonName(btnMappingList[i].padButton); - ImVec2 len = ImGui::CalcTextSize(btnName); - ImVec2 pos = ImGui::GetCursorPos(); - - ImGui::SetCursorPosY(pos.y + len.y / 4); - ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize)); - ImGui::Text("%s", btnName); - ImGui::SameLine(); - - ImGui::SetCursorPosY(pos.y); - - std::string dispName; - if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingButtonMapping == &btnMappingList[i]) { - dispName = fmt::format("Press a Key...##{}", btnName); - } else { - const char* nativeName = GetNameForGamepadButton(gamepad, btnMappingList[i].nativeButton); - if (nativeName == nullptr) { - nativeName = "[unbound]"; - } - dispName = fmt::format("{0}##-{1}", nativeName, i); - } - bool pressed = ImGui::Button(dispName.c_str(), - btnSize); - - if (pressed) { - m_controllerConfig.m_isReading = true; - m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; - m_controllerConfig.m_pendingButtonMapping = &btnMappingList[i]; - PADBlockInput(true); - } - } - } - - ImGuiEndGroupPanel(); - ImGui::SameLine(); - - uint32_t axisCount; - PADAxisMapping* axisMappingList = PADGetAxisMappings(m_controllerConfig.m_selectedPort, &axisCount); - - ImGuiBeginGroupPanel("Analog Triggers", ImVec2(150 * scale, 20 * scale)); - - PADAxis triggers[] = {PAD_AXIS_TRIGGER_L, PAD_AXIS_TRIGGER_R}; - if (axisMappingList != nullptr) { - for (PADAxis trigger : triggers) { - const char* axisName = PADGetAxisName(axisMappingList[trigger].padAxis); - ImVec2 len = ImGui::CalcTextSize(axisName); - ImVec2 pos = ImGui::GetCursorPos(); - - ImGui::SetCursorPosY(pos.y + len.y / 4); - ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize)); - ImGui::Text("%s", axisName); - ImGui::SameLine(); - - ImGui::SetCursorPosY(pos.y); - - std::string dispName; - if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[trigger]) { - dispName = fmt::format("Press a Key...##{}", axisName); - } else { - dispName = fmt::format("{0}##-{1}", PADGetNativeAxisName(axisMappingList[trigger].nativeAxis), trigger); - } - bool pressed = ImGui::Button(dispName.c_str(), - btnSize); - - if (pressed) { - m_controllerConfig.m_isReading = true; - m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; - m_controllerConfig.m_pendingAxisMapping = &axisMappingList[trigger]; - PADBlockInput(true); - } - } - } - - int port = m_controllerConfig.m_selectedPort; - PADDeadZones* deadZones = PADGetDeadZones(port); - - if (deadZones != nullptr) { - ImGui::Text("L Threshold"); - ImGui::SameLine(); - { - float tmp = static_cast(deadZones->leftTriggerActivationZone * 100.f) / 32767.f; - if (ImGui::DragFloat("##LThreshold", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) { - deadZones->leftTriggerActivationZone = static_cast((tmp / 100.f) * 32767); - PADSerializeMappings(); - } - } - } - - if (deadZones != nullptr) { - ImGui::Text("R Threshold"); - ImGui::SameLine(); - { - float tmp = static_cast(deadZones->rightTriggerActivationZone * 100.f) / 32767.f; - if (ImGui::DragFloat("##RThreshold", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) { - deadZones->rightTriggerActivationZone = static_cast((tmp / 100.f) * 32767); - PADSerializeMappings(); - } - } - } - - ImGuiEndGroupPanel(); - ImGui::SameLine(); - - // main stick panel - ImGuiBeginGroupPanel("Control Stick", ImVec2(150 * scale, 20 * scale)); - - drawVirtualStick("##mainStick", ImVec2{ mDoCPd_c::getStickX(port), mDoCPd_c::getStickY(port) }); - - if (axisMappingList != nullptr) { - const PADAxis lStickAxes[] = {PAD_AXIS_LEFT_Y_POS, PAD_AXIS_LEFT_Y_NEG, PAD_AXIS_LEFT_X_NEG, PAD_AXIS_LEFT_X_POS}; - for (auto axis : lStickAxes) { - const char* label = PADGetAxisDirectionLabel(axis); - ImVec2 len = ImGui::CalcTextSize(label); - ImVec2 pos = ImGui::GetCursorPos(); - - ImGui::SetCursorPosY(pos.y + len.y / 4); - ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize)); - ImGui::Text("%s", label); - ImGui::SameLine(); - - ImGui::SetCursorPosY(pos.y); - - std::string dispName; - if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) { - dispName = fmt::format("Press a Key...##{}", label); - } else { - if (axisMappingList[axis].nativeAxis.nativeAxis != -1) { - const char* signStr; - if (axis == PAD_AXIS_TRIGGER_L || axis == PAD_AXIS_TRIGGER_R) { - signStr = ""; - } else if (axisMappingList[axis].nativeAxis.sign == AXIS_SIGN_POSITIVE) { - signStr = "+"; - } else { - signStr = "-"; - } - dispName = fmt::format("{0}{1}##-{2}", PADGetNativeAxisName(axisMappingList[axis].nativeAxis), signStr, axis); - } else { - assert(axisMappingList[axis].nativeButton != -1); - dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis); - } - } - bool pressed = ImGui::Button(dispName.c_str(), btnSize); - - if (pressed) { - m_controllerConfig.m_isReading = true; - m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; - m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis]; - PADBlockInput(true); - } - } - } - - if (deadZones != nullptr) { - ImGui::Text("Dead Zone"); - { - float tmp = static_cast(deadZones->stickDeadZone * 100.f) / 32767.f; - if (ImGui::DragFloat("##mainDeadZone", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) { - deadZones->stickDeadZone = static_cast((tmp / 100.f) * 32767); - PADSerializeMappings(); - } - } - } - - ImGuiEndGroupPanel(); - ImGui::SameLine(); - - // sub stick panel - ImGuiBeginGroupPanel("C Stick", ImVec2(150 * scale, 20 * scale)); - - drawVirtualStick("##subStick", ImVec2{ mDoCPd_c::getSubStickX(port), mDoCPd_c::getSubStickY(port) }); - - if (axisMappingList != nullptr) { - const PADAxis rStickAxes[] = {PAD_AXIS_RIGHT_Y_POS, PAD_AXIS_RIGHT_Y_NEG, PAD_AXIS_RIGHT_X_NEG, PAD_AXIS_RIGHT_X_POS}; - for (auto axis : rStickAxes) { - const char* label = PADGetAxisDirectionLabel(axisMappingList[axis].padAxis); - ImVec2 len = ImGui::CalcTextSize(label); - ImVec2 pos = ImGui::GetCursorPos(); - - ImGui::SetCursorPosY(pos.y + len.y / 4); - ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize)); - ImGui::Text("%s", label); - ImGui::SameLine(); - - ImGui::SetCursorPosY(pos.y); - - std::string dispName; - if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) { - dispName = fmt::format("Press a Key...##sub{}", label); - } else { - if (axisMappingList[axis].nativeAxis.nativeAxis != -1) { - const char* signStr; - if (axis == PAD_AXIS_TRIGGER_L || axis == PAD_AXIS_TRIGGER_R) { - signStr = ""; - } else if (axisMappingList[axis].nativeAxis.sign == AXIS_SIGN_POSITIVE) { - signStr = "+"; - } else { - signStr = "-"; - } - dispName = fmt::format("{0}{1}##-{2}", PADGetNativeAxisName(axisMappingList[axis].nativeAxis), signStr, axis); - } else { - assert(axisMappingList[axis].nativeButton != -1); - dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis); - } - } - bool pressed = ImGui::Button(fmt::format("{0}##sub{1}", dispName, label).c_str(), btnSize); - - if (pressed) { - m_controllerConfig.m_isReading = true; - m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; - m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis]; - PADBlockInput(true); - } - } - } - - if (deadZones != nullptr) { - ImGui::Text("Dead Zone"); - { - float tmp = static_cast(deadZones->substickDeadZone * 100.f) / 32767.f; - if (ImGui::DragFloat("##subDeadZone", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) { - deadZones->substickDeadZone = static_cast((tmp / 100.f) * 32767); - PADSerializeMappings(); - } - } - } - - ImGuiEndGroupPanel(); - ImGui::SameLine(); - - // Options panel - ImGuiBeginGroupPanel("Options", ImVec2(150 * scale, 20 * scale)); - - if (deadZones != nullptr) { - if (ImGui::Checkbox("Enable Dead Zones", &deadZones->useDeadzones)) { - PADSerializeMappings(); - } - if (ImGui::Checkbox("Emulate Triggers", &deadZones->emulateTriggers)) { - PADSerializeMappings(); - } - } - - ImGuiEndGroupPanel(); - ImGui::End(); } } diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index e21374c8f4..225a505438 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -8,38 +8,50 @@ #include "imgui.h" namespace dusk { + struct SpeedrunInfo { + void startRun() { + m_isRunStarted = true; + m_startTimestamp = OSGetTime(); + } + + void stopRun() { + m_isRunStarted = false; + m_endTimestamp = OSGetTime() - m_startTimestamp; + } + + void reset() { + m_isRunStarted = false; + m_startTimestamp = 0; + m_endTimestamp = 0; + m_isPauseIGT = false; + m_loadStartTimestamp = 0; + m_totalLoadTime = 0; + m_igtTimer = 0; + } + + bool m_isRunStarted = false; + OSTime m_startTimestamp = 0; + OSTime m_endTimestamp = 0; + + bool m_isPauseIGT = false; + OSTime m_loadStartTimestamp = 0; + OSTime m_totalLoadTime = 0; + OSTime m_igtTimer = 0; + }; + + extern SpeedrunInfo m_speedrunInfo; + class ImGuiMenuGame { public: ImGuiMenuGame(); void draw(); - void windowInputViewer(); - void windowControllerConfig(); + void drawSpeedrunTimerOverlay(); - static void ToggleFullscreen(); + static void resetForSpeedrunMode(); private: - void drawAudioMenu(); - void drawInputMenu(); - void drawGraphicsMenu(); - void drawGameplayMenu(); - void drawCheatsMenu(); - void drawInterfaceMenu(); - - struct { - int m_selectedPort = 0; - bool m_isReading = false; - PADButtonMapping* m_pendingButtonMapping = nullptr; - PADAxisMapping* m_pendingAxisMapping = nullptr; - int m_pendingPort = -1; - } m_controllerConfig; - - bool m_showControllerConfig = false; - - bool m_showInputViewer = false; - bool m_showInputViewerGyro = false; - int m_inputOverlayCorner = 3; - std::string m_controllerName; + bool m_showTimerWindow = false; }; } diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 00a03e635b..1cb0ec474c 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -23,23 +23,9 @@ #include #endif -#if defined(_WIN32) || (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || (defined(__linux__) && !defined(__ANDROID__)) -#define DUSK_CAN_OPEN_DATA_FOLDER 1 - -namespace fs = std::filesystem; - -static void OpenDataFolder() { - const std::string path = fs::absolute(dusk::ConfigPath).generic_string(); -#if defined(_WIN32) - const std::string url = std::string("file:///") + path; -#else - const std::string url = std::string("file://") + path; -#endif - (void)SDL_OpenURL(url.c_str()); +namespace aurora::gx { +extern bool enableLodBias; } -#else -#define DUSK_CAN_OPEN_DATA_FOLDER 0 -#endif namespace dusk { ImGuiMenuTools::ImGuiMenuTools() {} @@ -50,14 +36,21 @@ namespace dusk { ImGui::BeginDisabled(); } + ImGui::BeginDisabled(getSettings().game.speedrunMode); + ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader); ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); + ImGui::EndDisabled(); + if (!dusk::IsGameLaunched) { ImGui::EndDisabled(); } + ImGui::Separator(); + ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); + #if DUSK_CAN_OPEN_DATA_FOLDER ImGui::Separator(); if (ImGui::MenuItem("Open Data Folder")) { @@ -69,6 +62,8 @@ namespace dusk { } if (ImGui::BeginMenu("Debug")) { + ImGui::BeginDisabled(getSettings().game.speedrunMode); + bool developmentMode = mDoMain::developmentMode == 1; if (ImGui::Checkbox("Development Mode", &developmentMode)) { mDoMain::developmentMode = developmentMode ? 1 : -1; @@ -83,6 +78,7 @@ namespace dusk { getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction); config::Save(); } + ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); ImGui::EndMenu(); } @@ -111,18 +107,24 @@ namespace dusk { ImGui::MenuItem("Audio Debug", hotkeys::SHOW_AUDIO_DEBUG, &m_showAudioDebug); ImGui::MenuItem("Bloom", nullptr, &m_showBloomWindow); ImGui::MenuItem("Stub Log", nullptr, &m_showStubLog); + ImGui::MenuItem("Actor Spawner", nullptr, &m_showActorSpawner); if (!dusk::IsGameLaunched) { ImGui::EndDisabled(); } ImGui::MenuItem("OSReport Force", nullptr, &OSReportReallyForceEnable); + + ImGui::EndDisabled(); + ImGui::EndMenu(); } } void ImGuiMenuTools::ShowDebugOverlay() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F3, m_showDebugOverlay)) { + if (!getSettings().backend.enableAdvancedSettings || + !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F3, m_showDebugOverlay)) + { return; } @@ -186,7 +188,9 @@ namespace dusk { } void ImGuiMenuTools::ShowPlayerInfo() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showPlayerInfo)) { + if (!getSettings().backend.enableAdvancedSettings || + !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showPlayerInfo)) + { return; } diff --git a/src/dusk/imgui/ImGuiMenuTools.hpp b/src/dusk/imgui/ImGuiMenuTools.hpp index e48aab2742..1c17ddbbe9 100644 --- a/src/dusk/imgui/ImGuiMenuTools.hpp +++ b/src/dusk/imgui/ImGuiMenuTools.hpp @@ -2,6 +2,7 @@ #define DUSK_IMGUI_MENUTOOLS_HPP #include +#include #include #include "imgui.h" @@ -26,6 +27,8 @@ namespace dusk { void ShowAudioDebug(); void ShowSaveEditor(); void ShowStateShare(); + void ShowInputViewer(); + void ShowActorSpawner(); private: bool m_showDebugOverlay = false; @@ -65,6 +68,12 @@ namespace dusk { bool m_showStateShare = false; ImGuiStateShare m_stateShare; + + bool m_showInputViewer = false; + bool m_showInputViewerGyro = false; + bool m_showActorSpawner = false; + int m_inputOverlayCorner = 3; + std::string m_controllerName; }; } diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp deleted file mode 100644 index 2727e78718..0000000000 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ /dev/null @@ -1,262 +0,0 @@ -#include "imgui.h" - -#include "ImGuiConfig.hpp" -#include "ImGuiEngine.hpp" -#include "ImGuiPreLaunchWindow.hpp" - -#include "../file_select.hpp" -#include "../iso_validate.hpp" -#include "ImGuiConsole.hpp" -#include "dusk/main.h" -#include "dusk/settings.h" - -#include -#include - -#include "aurora/lib/internal.hpp" -#include "aurora/lib/window.hpp" - -namespace dusk { - -typedef void (ImGuiPreLaunchWindow::*drawFunc)(); - -drawFunc drawTable[2] = {&ImGuiPreLaunchWindow::drawMainMenu, &ImGuiPreLaunchWindow::drawOptions}; - -static constexpr std::array skGameDiscFileFilters{{ - {"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"}, - {"All Files", "*"}, -}}; - -static std::string ShowIsoInvalidError(const iso::ValidationError code) { - using namespace std::literals::string_literals; - - switch (code) { - case iso::ValidationError::IOError: - return "Unknown IO error occurred"s; - case iso::ValidationError::InvalidImage: - return "Unable to interpret selected file as a disc image"s; - case iso::ValidationError::WrongGame: - return "Selected disc image is for a different game"s; - case iso::ValidationError::WrongVersion: - return "Selected disc image is for an unsupported version of the game. Only North American GameCube (NTSC/GZ2E01) is supported at this time."s; - case iso::ValidationError::ExecutableMismatch: - return "Selected disc image contains modified executable files."s; - default: - return "Unknown error"s; - } -} - -static std::string_view card_type_name(CARDFileType type) { - switch (type) { - case CARD_GCIFOLDER: - return "GCI Folder"sv; - case CARD_RAWIMAGE: - return "Card Image"sv; - default: - return ""sv; - } -} - -void fileDialogCallback(void* userdata, const char* path, const char* error) { - auto* self = static_cast(userdata); - if (error != nullptr) { - self->m_selectedIsoPath.clear(); - self->m_errorString = fmt::format("File dialog error: {}", error); - return; - } - - if (path == nullptr) { - self->m_selectedIsoPath.clear(); - return; - } - - self->m_selectedIsoPath = path; - getSettings().backend.isoPath.setValue(self->m_selectedIsoPath); - config::Save(); -} - -ImGuiPreLaunchWindow::ImGuiPreLaunchWindow() = default; - -bool ImGuiPreLaunchWindow::isSelectedPathValid() const { -#if TARGET_ANDROID - return !m_selectedIsoPath.empty(); // unsure why SDL_GetPathInfo doesnt work here -#else - return !m_selectedIsoPath.empty() && SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr); -#endif -} - -void ImGuiPreLaunchWindow::draw() { - if (m_IsFirstDraw) { - m_selectedIsoPath = getSettings().backend.isoPath; - m_initialGraphicsBackend = getSettings().backend.graphicsBackend; - m_IsFirstDraw = false; - } - - if (isSelectedPathValid() && getSettings().backend.skipPreLaunchUI) { - dusk::IsGameLaunched = true; - return; - } - - auto& io = ImGui::GetIO(); - - ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y)); - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowBgAlpha(0.65f); - - ImGui::Begin("Pre Launch Window", nullptr, - ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | - ImGuiWindowFlags_NoBringToFrontOnFocus); - - const auto& windowSize = ImGui::GetWindowSize(); - - for (int i = 0; i < 5; i++) - ImGui::NewLine(); - - float iconSize = 150.f; - ImGui::SameLine(windowSize.x / 2 - iconSize + (iconSize / 2)); - if (ImGuiEngine::orgIcon != 0) { - ImGui::Image(ImGuiEngine::orgIcon, ImVec2{iconSize, iconSize}); - } - ImGuiTextCenter("Twilit Realm presents"); - if (ImGuiEngine::duskLogo) { - ImGui::NewLine(); - float width = iconSize * 2.5f; - ImGui::SameLine(windowSize.x / 2 - width + (width / 2)); - ImGui::Image(ImGuiEngine::duskLogo, ImVec2{width, iconSize}); - } else { - ImGui::PushFont(ImGuiEngine::fontExtraLarge); - ImGuiTextCenter("Dusk"); - ImGui::PopFont(); - } - - (this->*drawTable[m_CurMenu])(); - - ImGui::End(); -} - -void ImGuiPreLaunchWindow::drawMainMenu() { - const auto& windowSize = ImGui::GetWindowSize(); - ImGui::SetCursorPosY(windowSize.y - 200); - - ImGui::PushFont(ImGuiEngine::fontLarge); - - if (!isSelectedPathValid()) { - if (!m_errorString.empty()) { - ImGuiTextCenter(m_errorString); - } - - if (ImGuiButtonCenter("Select disc image...")) { - ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(), - skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr, - false); - } - } else { - if (ImGuiButtonCenter("Start game")) { - dusk::IsGameLaunched = true; - } - } - - if (ImGuiButtonCenter("Options")) { - m_CurMenu = 1; - } - - ImGui::PopFont(); -} - -void ImGuiPreLaunchWindow::drawOptions() { - const auto& windowSize = ImGui::GetWindowSize(); - - ImGui::NewLine(); - - ImGui::PushFont(ImGuiEngine::fontLarge); - ImGuiTextCenter("Options"); - ImGui::Separator(); - ImGui::PopFont(); - - auto cursorY = ImGui::GetCursorPosY(); - float endCursorY = windowSize.y - 100; - - float childWidth = windowSize.x - 400; - - ImGui::SetCursorPosX(windowSize.x / 2 - (childWidth / 2)); - if (ImGui::BeginChild("OptionsChild", ImVec2(childWidth, endCursorY - cursorY), - ImGuiChildFlags_None, ImGuiWindowFlags_NoBackground)) - { - if (!m_errorString.empty()) { - ImGuiTextCenter(m_errorString); - } - - ImGui::InputText("Game ISO Path", &m_selectedIsoPath, ImGuiInputTextFlags_ReadOnly); - ImGui::SameLine(); - if (ImGui::Button("Set")) { - ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(), - skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr, - false); - } - - AuroraBackend configuredBackend = BACKEND_AUTO; - const std::string& configuredBackendId = getSettings().backend.graphicsBackend; - if (!try_parse_backend(configuredBackendId, configuredBackend)) { - configuredBackend = BACKEND_AUTO; - } - - if (ImGui::BeginCombo("Graphics Backend", backend_name(configuredBackend).data())) { - if (ImGui::Selectable("Auto", configuredBackend == BACKEND_AUTO)) { - getSettings().backend.graphicsBackend.setValue("auto"); - config::Save(); - } - - size_t backendCount = 0; - const AuroraBackend* availableBackends = aurora_get_available_backends(&backendCount); - for (size_t i = 0; i < backendCount; ++i) { - const AuroraBackend backend = availableBackends[i]; - const bool isSelected = configuredBackend == backend; - if (ImGui::Selectable(backend_name(backend).data(), isSelected)) { - getSettings().backend.graphicsBackend.setValue( - std::string(backend_id(backend))); - config::Save(); - } - if (isSelected) { - ImGui::SetItemDefaultFocus(); - } - } - - ImGui::EndCombo(); - } - if (configuredBackendId != m_initialGraphicsBackend) { - ImGui::TextDisabled("Restart Required"); - } - auto curFileType = (CARDFileType)getSettings().backend.cardFileType.getValue(); - - if (ImGui::BeginCombo("Save File Type", card_type_name(curFileType).data())) { - - if (ImGui::Selectable("GCI Folder", curFileType == CARD_GCIFOLDER)) { - getSettings().backend.cardFileType.setValue(CARD_GCIFOLDER); - config::Save(); - } - - if (ImGui::Selectable("Card Image", curFileType == CARD_RAWIMAGE)) { - getSettings().backend.cardFileType.setValue(CARD_RAWIMAGE); - config::Save(); - } - - ImGui::EndCombo(); - } - - ImGui::EndChild(); - } - - ImGui::SetCursorPosY(endCursorY); - ImGui::NewLine(); - - ImGui::Separator(); - - ImGui::PushFont(ImGuiEngine::fontLarge); - if (ImGuiButtonCenter("Back")) { - m_CurMenu = 0; - } - ImGui::PopFont(); -} - -} // namespace dusk diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp b/src/dusk/imgui/ImGuiPreLaunchWindow.hpp deleted file mode 100644 index 5d16a6ba0b..0000000000 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -namespace dusk { -class ImGuiPreLaunchWindow { -private: - int m_CurMenu = 0; - bool m_IsFirstDraw = true; - std::string m_initialGraphicsBackend; - - bool isSelectedPathValid() const; - -public: - ImGuiPreLaunchWindow(); - void draw(); - - void drawMainMenu(); - void drawOptions(); - - std::string m_selectedIsoPath; - std::string m_errorString; -}; -} // namespace dusk diff --git a/src/dusk/imgui/ImGuiProcessOverlay.cpp b/src/dusk/imgui/ImGuiProcessOverlay.cpp index ed71e6c485..806a9ea0f6 100644 --- a/src/dusk/imgui/ImGuiProcessOverlay.cpp +++ b/src/dusk/imgui/ImGuiProcessOverlay.cpp @@ -126,7 +126,9 @@ namespace dusk { } void ImGuiMenuTools::ShowProcessManager() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F2, m_showProcessManagement)) { + if (!getSettings().backend.enableAdvancedSettings || + !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F2, m_showProcessManagement)) + { return; } diff --git a/src/dusk/imgui/ImGuiSaveEditor.cpp b/src/dusk/imgui/ImGuiSaveEditor.cpp index eced44c7f7..b08ae2860b 100644 --- a/src/dusk/imgui/ImGuiSaveEditor.cpp +++ b/src/dusk/imgui/ImGuiSaveEditor.cpp @@ -1295,8 +1295,33 @@ namespace dusk { membit.offDungeonItem(flag); } } + + static void genCommonAreaFlags(dSv_memBit_c& membit) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10.0f); - void genMembitFlags(const char* id, dSv_memBit_c& membit) { + genDungeonItemCheckbox(membit, "Got Map", dSv_memBit_c::MAP); + ImGui::SameLine(230.0f); + genDungeonItemCheckbox(membit, "Got Compass", dSv_memBit_c::COMPASS); + + genDungeonItemCheckbox(membit, "Got Boss Key", dSv_memBit_c::BOSS_KEY); + ImGui::SameLine(230.0f); + genDungeonItemCheckbox(membit, "Saw Boss Demo", dSv_memBit_c::STAGE_BOSS_DEMO); + + genDungeonItemCheckbox(membit, "Got Heart Container", dSv_memBit_c::STAGE_LIFE); + ImGui::SameLine(230.0f); + genDungeonItemCheckbox(membit, "Defeated Boss", dSv_memBit_c::STAGE_BOSS_ENEMY); + + genDungeonItemCheckbox(membit, "Defeated Miniboss", dSv_memBit_c::STAGE_BOSS_ENEMY_2); + ImGui::SameLine(230.0f); + genDungeonItemCheckbox(membit, "Got Ooccoo", dSv_memBit_c::OOCCOO_NOTE); + + int keyTemp = membit.getKeyNum(); + if (ImGui::SliderInt("Keys", &keyTemp, 0, 5)) { + membit.setKeyNum(keyTemp); + } + } + + static void genMembitFlags(const char* id, dSv_memBit_c& membit) { ImGuiBeginGroupPanel("Chest", { 100, 100 }); for (int j = 0; j < 2; j++) { drawFlagList(fmt::format("##_tbox{}", j).c_str(), membit.mTbox[j]); @@ -1322,99 +1347,329 @@ namespace dusk { drawFlagList(fmt::format("##_item{}", j).c_str(), membit.mItem[j]); } ImGuiEndGroupPanel(); + ImVec2 post_item_custor = ImGui::GetCursorPos(); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10.0f); + ImGui::SetCursorPos({post_item_custor.x, post_switch_cursor.y}); + // genCommonAreaFlags(membit); + } - genDungeonItemCheckbox(membit, "Got Map", dSv_memBit_c::MAP); - ImGui::SameLine(230.0f); - genDungeonItemCheckbox(membit, "Got Compass", dSv_memBit_c::COMPASS); + template + requires requires(FlagIter a, FlagTester tester) { + --a; ++a; a < a; *a; + a + 1; + { tester(*a) } -> std::convertible_to; + } + static void sortByFlags(FlagIter begin, FlagIter end, FlagTester&& flagTester) { + if (begin == end) return; - genDungeonItemCheckbox(membit, "Got Boss Key", dSv_memBit_c::BOSS_KEY); - ImGui::SameLine(230.0f); - genDungeonItemCheckbox(membit, "Saw Boss Demo", dSv_memBit_c::STAGE_BOSS_DEMO); + auto fullEnd = end; - genDungeonItemCheckbox(membit, "Got Heart Container", dSv_memBit_c::STAGE_LIFE); - ImGui::SameLine(230.0f); - genDungeonItemCheckbox(membit, "Defeated Boss", dSv_memBit_c::STAGE_BOSS_ENEMY); + // We want to find the location of where we can swap our `On` flags to. + // We're gonna put the `Off` bits first, and the `On` bits last. 0 < 1 + // We can achieve this by skipping all the `On` bits at the end. - genDungeonItemCheckbox(membit, "Defeated Miniboss", dSv_memBit_c::STAGE_BOSS_ENEMY_2); - ImGui::SameLine(230.0f); - genDungeonItemCheckbox(membit, "Got Ooccoo", dSv_memBit_c::OOCCOO_NOTE); + // backtrack until we find a bit that is off + while (begin < --end && flagTester(*end)) { + // move the end pointer back while we find on bits + } - int keyTemp = membit.getKeyNum(); - if (ImGui::SliderInt("Keys", &keyTemp, 0, 5)) { - membit.setKeyNum(keyTemp); + // end should now be pointing to a bit that is off + while (begin < end) { + // if there's a flag that's on + if (flagTester(*begin)) { + // move it to the end + std::rotate(begin, begin + 1, fullEnd); + // move back the end of where we're checking + --end; + // begin will now point to the next piece of data + // because we've rotated the data >= begin to the left + } else { + // not on, check next flag + ++begin; + } + } + } + + + static void genAreaFlagTable(uint8_t areaIndex, dSv_memBit_c& membit) { + const auto LoadFlag = [&](const EventAreaFlags& flag) -> bool { + switch (flag.flag.type) { + case AreaFlagType::Item: { + return membit.isItem(flag.flag.flagID); + } break; + case AreaFlagType::Switch: { + return membit.isSwitch(flag.flag.flagID); + } break; + case AreaFlagType::Tbox: { + return membit.isTbox(flag.flag.flagID); + } break; + } + return false; + }; + + const auto SetFlag = [&](const AreaFlagInd& flag, bool set) -> void { + if (set) { + switch (flag.type) { + case AreaFlagType::Item: { + membit.onItem(flag.flagID); + } break; + case AreaFlagType::Switch: { + membit.onSwitch(flag.flagID); + } break; + case AreaFlagType::Tbox: { + membit.onTbox(flag.flagID); + } break; + } + } else { + switch (flag.type) { + case AreaFlagType::Item: { + membit.offItem(flag.flagID); + } break; + case AreaFlagType::Switch: { + membit.offSwitch(flag.flagID); + } break; + case AreaFlagType::Tbox: { + membit.offTbox(flag.flagID); + } break; + } + } + }; + + const auto LoadMultiByteFlag = [&](const AreaFlagMultibit& flag) -> uint8_t { + BE(u32)* areaFlags = nullptr; + switch (flag.type) { + case AreaFlagType::Item: { + areaFlags = membit.mItem; + } break; + case AreaFlagType::Switch: { + areaFlags = membit.mSwitch; + } break; + case AreaFlagType::Tbox: { + areaFlags = membit.mTbox; + } break; + } + assert(areaFlags != nullptr); + return (areaFlags[flag.index] & flag.mask) >> flag.shift; + }; + + const auto SetMultiByteFlag = [&](const AreaFlagMultibit& flag, uint8_t val) -> void { + BE(u32)* areaFlags = nullptr; + switch (flag.type) { + case AreaFlagType::Item: { + areaFlags = membit.mItem; + } break; + case AreaFlagType::Switch: { + areaFlags = membit.mSwitch; + } break; + case AreaFlagType::Tbox: { + areaFlags = membit.mTbox; + } break; + } + + areaFlags[flag.index] &= ~flag.mask; + areaFlags[flag.index] |= (val << flag.shift) & flag.mask; + }; + + auto iter = imguiAreaFlagLookup.find(areaIndex); + if (iter == imguiAreaFlagLookup.end()) return; + + auto& areaFlags = iter->second; + + static ImGuiTextFilter filter; + filter.Draw(); // Search bar + + ImVec2 flagTableSize = {700, 400}; + if (ImGui::BeginTable("Area Flags", 3, + ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX | + ImGuiTableFlags_Sortable, + flagTableSize)) + { + ImGui::TableSetupScrollFreeze(0, 1); + constexpr int COLUMN_FLAG = 0, COLUMN_BIT = 1, COLUMN_DESC = 2; + ImGui::TableSetupColumn("Flag"); + ImGui::TableSetupColumn("Byte:Bit"); + ImGui::TableSetupColumn("Description"); + ImGui::TableHeadersRow(); + + // if we're sorting by whether the flag is set or not, + // we want to re-sort whenever a flag updates, which means every frame cuz we don't + // know when it changes. otherwise only re-sort when the sort is dirty + if (auto* sort = ImGui::TableGetSortSpecs(); + sort != nullptr && sort->SpecsCount > 0 && + (sort->SpecsDirty || sort->Specs[0].ColumnIndex == COLUMN_FLAG)) + { + const auto column = sort->Specs[0].ColumnIndex; + const auto direction = sort->Specs[0].SortDirection; + + // if we're sorting by flags, do special sort, regular sort is bad for sorting + // bools it can swap values that are the same, and that causes constant + // reordering + if (column == COLUMN_FLAG) { + if (direction == ImGuiSortDirection_Ascending) { + sortByFlags(std::begin(areaFlags.bitFlags), std::end(areaFlags.bitFlags), + LoadFlag); + } else { + sortByFlags(std::rbegin(areaFlags.bitFlags), std::rend(areaFlags.bitFlags), + LoadFlag); + } + } else { + const auto cmp = [column](const EventAreaFlags& l, + const EventAreaFlags& r) -> bool { + switch (column) { + case COLUMN_DESC: + return l.description < r.description; + case COLUMN_BIT: + return l.GetFlagID() < r.GetFlagID(); + } + return false; + }; + + if (direction == ImGuiSortDirection_Ascending) { + std::sort(std::begin(areaFlags.bitFlags), std::end(areaFlags.bitFlags), + cmp); + } else { + std::sort(std::rbegin(areaFlags.bitFlags), std::rend(areaFlags.bitFlags), + cmp); + } + } + + sort->SpecsDirty = false; + } + + for (const auto& e : areaFlags.bitFlags) { + std::string formattedBitLocation = + fmt::format("{0:02X}:{1:02X}", e.byteIndex, e.bitIndex); + + if (!filter.PassFilter(e.description.c_str()) && + !filter.PassFilter(formattedBitLocation.c_str())) + { + continue; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + bool flag = LoadFlag(e); + if (ImGui::Checkbox(fmt::format("##_unused_area_flag_{}", e.flag.flagID).c_str(), &flag)) { + SetFlag(e.flag, flag); + } + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(formattedBitLocation.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(e.description.c_str()); + } + ImGui::EndTable(); + } + + for (const auto& multiByteFlag : areaFlags.multibyteFlags) { + auto flagValue = LoadMultiByteFlag(multiByteFlag.flag); + + const char* currentVal = "UNKNOWN"; + + auto enumValIter = multiByteFlag.enumValues.find(flagValue); + if (enumValIter != multiByteFlag.enumValues.end()) { + currentVal = enumValIter->second; + } + + if (ImGui::BeginCombo(multiByteFlag.name, currentVal)) { + for (const auto& [val, name] : multiByteFlag.enumValues) { + if (ImGui::Selectable(name)) { + SetMultiByteFlag(multiByteFlag.flag, val); + } + } + ImGui::EndCombo(); + } + } + + genCommonAreaFlags(membit); + } + + static void drawCurrentRegionFlags() + { + dSv_memBit_c& membit = g_dComIfG_gameInfo.info.mMemory.mBit; + auto* stageData = dComIfGp_getStageStagInfo(); + if (!stageData) + return; + uint8_t stageIndex = dStage_stagInfo_GetSaveTbl(stageData); + + genAreaFlagTable(stageIndex, membit); + + if (ImGui::TreeNode("Flag Matrix")) { + genMembitFlags("##TempSceneFlags", membit); + ImGui::TreePop(); + } + + stage_stag_info_class* pstag = dComIfGp_getStageStagInfo(); + if (pstag != nullptr) { + int stageNo = dStage_stagInfo_GetSaveTbl(pstag); + if (ImGui::Button("Save##SaveTempFlags")) { + dComIfGs_putSave(stageNo); + } + + ImGui::SameLine(); + + if (ImGui::Button("Load##LoadSaveFlags")) { + dComIfGs_getSave(stageNo); + } } } void ImGuiSaveEditor::drawFlagsTab() { if (ImGui::TreeNode("Current Region Flags")) { - dSv_memBit_c& membit = g_dComIfG_gameInfo.info.mMemory.mBit; - genMembitFlags("##TempSceneFlags", membit); - - stage_stag_info_class* pstag = dComIfGp_getStageStagInfo(); - if (pstag != nullptr) { - int stageNo = dStage_stagInfo_GetSaveTbl(pstag); - if (ImGui::Button("Save##SaveTempFlags")) { - dComIfGs_putSave(stageNo); - } - - ImGui::SameLine(); - - if (ImGui::Button("Load##LoadSaveFlags")) { - dComIfGs_getSave(stageNo); - } - } - + drawCurrentRegionFlags(); ImGui::TreePop(); } if (ImGui::TreeNode("Region Saved Flags")) { - static std::array regionNames = { - "Ordon", - "Hyrule Sewers", - "Faron", - "Eldin", - "Lanayru", - "Reserved", - "Hyrule Field", - "Sacred Grove", - "Snowpeak", - "Castle Town", - "Gerudo Desert", - "Fishing Pond", - "Reserved", - "Reserved", - "Reserved", - "Reserved", - "Forest Temple", - "Goron Mines", - "Lakebed Temple", - "Arbiter's Grounds", - "Snowpeak Ruins", - "Temple of Time", - "City in the Sky", - "Palace of Twilight", - "Hyrule Castle", - "Caves", - "Grottos", + static const std::map regionNames = { + { 0x00, "Ordon" }, + { 0x01, "Hyrule Sewers" }, + { 0x02, "Faron" }, + { 0x03, "Eldin" }, + { 0x04, "Lanayru" }, + { 0x06, "Hyrule Field" }, + { 0x07, "Sacred Grove" }, + { 0x08, "Snowpeak" }, + { 0x09, "Castle Town" }, + { 0x0A, "Gerudo Desert" }, + { 0x0B, "Fishing Pond" }, + { 0x10, "Forest Temple" }, + { 0x11, "Goron Mines" }, + { 0x12, "Lakebed Temple" }, + { 0x13, "Arbiter's Grounds" }, + { 0x14, "Snowpeak Ruins" }, + { 0x15, "Temple of Time" }, + { 0x16, "City in the Sky" }, + { 0x17, "Palace of Twilight" }, + { 0x18, "Hyrule Castle" }, + { 0x19, "Caves" }, + { 0x1A, "Lake Hylia Long Cave"}, + { 0x1B, "Grottos" } }; - if (ImGui::BeginCombo("Region", regionNames[m_selectedRegion])) { - for (int i = 0; i < regionNames.size(); i++) { - if (strcmp(regionNames[i], "Reserved") == 0) continue; + if (m_selectedRegion.name == nullptr) + { + const auto& firstRegion = *regionNames.find(0); + m_selectedRegion = { firstRegion.first, firstRegion.second }; + } - if (ImGui::Selectable(regionNames[i])) { - m_selectedRegion = i; + if (ImGui::BeginCombo("Region", m_selectedRegion.name)) { + for (const auto& [id, name] : regionNames) { + if (ImGui::Selectable(name)) { + m_selectedRegion = {id, name}; } } - ImGui::EndCombo(); } - dSv_memBit_c* membit = &dComIfGs_getSaveData()->mSave[m_selectedRegion].mBit; - if (membit != nullptr) { - genMembitFlags("##SaveSceneFlags", *membit); + dSv_memBit_c& membit = dComIfGs_getSaveData()->mSave[m_selectedRegion.id].mBit; + + genAreaFlagTable(m_selectedRegion.id, membit); + + if (ImGui::TreeNode("Flag Matrix")) { + genMembitFlags("##SaveSceneFlags", membit); + + ImGui::TreePop(); } ImGui::TreePop(); @@ -1446,35 +1701,50 @@ namespace dusk { sort != nullptr && sort->SpecsCount > 0 && (sort->SpecsDirty || sort->Specs[0].ColumnIndex == COLUMN_FLAG)) { - auto column = sort->Specs->ColumnIndex; - const auto cmp = [&](const duskImguiEventFlagEntry& l, - const duskImguiEventFlagEntry& r) -> bool { - switch (column) { - case COLUMN_FLAG: - return (bool)event.isEventBit(l.flagID) < - (bool)event.isEventBit(r.flagID); - case COLUMN_NAME: - return l.flagName < r.flagName; - case COLUMN_LOC: - return l.location < r.location; - case COLUMN_DESC: - return l.description < r.description; - } - return false; - }; - + const auto column = sort->Specs[0].ColumnIndex; const auto direction = sort->Specs[0].SortDirection; - if (direction == ImGuiSortDirection_Ascending) { - std::sort(std::begin(duskImguiEventFlags), std::end(duskImguiEventFlags), cmp); + // if we're sorting by flags, do special sort, regular sort is bad for sorting bools + // it can swap values that are the same, and that causes constant reordering + if (column == COLUMN_FLAG) { + const auto testEventFunc = [&event](const duskImguiEventFlagEntry& flag) -> bool { + return event.isEventBit(flag.flagID); + }; + + if (direction == ImGuiSortDirection_Ascending) { + sortByFlags(std::begin(duskImguiEventFlags), + std::end(duskImguiEventFlags), testEventFunc); + } else { + sortByFlags(std::rbegin(duskImguiEventFlags), + std::rend(duskImguiEventFlags), testEventFunc); + } } else { - std::sort(std::rbegin(duskImguiEventFlags), std::rend(duskImguiEventFlags), cmp); + const auto cmp = [column](const duskImguiEventFlagEntry& l, + const duskImguiEventFlagEntry& r) -> bool { + switch (column) { + case COLUMN_NAME: return l.flagName < r.flagName; + case COLUMN_LOC: return l.location < r.location; + case COLUMN_DESC: return l.description < r.description; + default: return false; + } + }; + + if (direction == ImGuiSortDirection_Ascending) { + std::sort(std::begin(duskImguiEventFlags), + std::end(duskImguiEventFlags), cmp); + } else { + std::sort(std::rbegin(duskImguiEventFlags), + std::rend(duskImguiEventFlags), cmp); + } } + sort->SpecsDirty = false; } for (const auto& e : duskImguiEventFlags) { - if (!filter.PassFilter((e.location + "\n" + e.description + "\n" + e.flagName).c_str())) + if (!filter.PassFilter(e.location.c_str()) && + !filter.PassFilter(e.description.c_str()) && + !filter.PassFilter(e.flagName.c_str())) { continue; } diff --git a/src/dusk/imgui/ImGuiSaveEditor.hpp b/src/dusk/imgui/ImGuiSaveEditor.hpp index dc7c122079..a18262862f 100644 --- a/src/dusk/imgui/ImGuiSaveEditor.hpp +++ b/src/dusk/imgui/ImGuiSaveEditor.hpp @@ -21,7 +21,10 @@ namespace dusk { void drawConfigTab(); private: - int m_selectedRegion = 0; + struct { + uint8_t id; + const char* name; + } m_selectedRegion = {0, nullptr}; }; } diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index adbe5b6c67..48849e2bc3 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -48,19 +48,18 @@ void ImGuiStateShare::onMergeFileSelected(void* userdata, const char* path, cons -static std::string GetStatesFilePath() { - return (dusk::ConfigPath / STATES_FILENAME).string(); +static std::filesystem::path GetStatesFilePath() { + return ConfigPath / STATES_FILENAME; } void ImGuiStateShare::loadStatesFile() { m_loaded = true; - const std::filesystem::path filePath = dusk::ConfigPath / STATES_FILENAME; + const std::filesystem::path filePath = GetStatesFilePath(); if (!std::filesystem::exists(filePath)) { return; } try { - const std::string pathStr = filePath.string(); - auto data = io::FileStream::ReadAllBytes(pathStr.c_str()); + auto data = io::FileStream::ReadAllBytes(filePath); auto j = json::parse(data); if (!j.is_array()) { return; @@ -85,7 +84,7 @@ void ImGuiStateShare::saveStatesFile() { j.push_back(json{{"name", s.name}, {"data", s.encoded}}); } try { - io::FileStream::WriteAllText(GetStatesFilePath().c_str(), j.dump(2)); + io::FileStream::WriteAllText(GetStatesFilePath(), j.dump(2)); } catch (const std::exception& e) { m_statusMsg = fmt::format("Failed to save states: {}", e.what()); } @@ -417,7 +416,9 @@ void ImGuiStateShare::draw(bool& open) { } void ImGuiMenuTools::ShowStateShare() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) { + if (!getSettings().backend.enableAdvancedSettings || + !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) + { return; } m_stateShare.draw(m_showStateShare); diff --git a/src/dusk/io.cpp b/src/dusk/io.cpp index 50b5cab5bd..3c87a66acf 100644 --- a/src/dusk/io.cpp +++ b/src/dusk/io.cpp @@ -1,7 +1,8 @@ -#include "dusk/io.hpp" #include #include +#include "dusk/io.hpp" + using namespace dusk::io; #if _WIN32 @@ -58,7 +59,7 @@ static FILE* OpenCore(const char* path, const MODE_TYPE* mode) { FileStream::FileStream() noexcept : file(nullptr) { } -FileStream::FileStream(void* file) : file(file) { +FileStream::FileStream(FILE* file) : file(file) { if (!file) { CRASH("Invalid file handle"); } @@ -78,10 +79,18 @@ FileStream FileStream::OpenRead(const char* utf8Path) { return FileStream(OpenCore(utf8Path, MODE("rb"))); } +FileStream FileStream::OpenRead(const std::filesystem::path& path) { + return FileStream(OpenCore(path, MODE("rb"))); +} + FileStream FileStream::Create(const char* utf8Path) { return FileStream(OpenCore(utf8Path, MODE("wb"))); } +FileStream FileStream::Create(const std::filesystem::path& path) { + return FileStream(OpenCore(path, MODE("wb"))); +} + std::vector FileStream::ReadFull() { const auto fileHandle = ThrowIfNotOpen(*this); @@ -128,7 +137,11 @@ std::vector FileStream::ReadFull() { } std::vector FileStream::ReadAllBytes(const char* utf8Path) { - auto handle = OpenRead(utf8Path); + return ReadAllBytes(reinterpret_cast(utf8Path)); +} + +std::vector FileStream::ReadAllBytes(const std::filesystem::path& path) { + auto handle = OpenRead(path); return std::move(handle.ReadFull()); } @@ -142,6 +155,16 @@ void FileStream::Write(const char* data, size_t dataLen) { } void FileStream::WriteAllText(const char* utf8Path, const std::string_view text) { - auto handle = Create(utf8Path); + WriteAllText(reinterpret_cast(utf8Path), text); +} + +void FileStream::WriteAllText(const std::filesystem::path& path, const std::string_view text) { + auto handle = Create(path); handle.Write(text.data(), text.size()); } + +FILE* FileStream::ToInner() { + auto handle = file; + file = nullptr; + return handle; +} \ No newline at end of file diff --git a/src/dusk/iso_validate.cpp b/src/dusk/iso_validate.cpp index 755b4eede0..00cd365ca9 100644 --- a/src/dusk/iso_validate.cpp +++ b/src/dusk/iso_validate.cpp @@ -1,43 +1,113 @@ #include "iso_validate.hpp" +#include #include -#include +#include -#include "SDL3/SDL_iostream.h" +#include +#include +#include +#include + +#include "dusk/logging.h" + +namespace { + +constexpr uint8_t hex_nibble_to_u8(char c) { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + throw std::invalid_argument("invalid hex character"); +} + +constexpr uint64_t parse_u64_hex(std::string_view s) { + if (s.size() != 16) + throw std::invalid_argument("expected 16 hex chars for uint64"); + + uint64_t value = 0; + for (char c : s) { + value = (value << 4) | hex_nibble_to_u8(c); + } + return value; +} + +constexpr XXH128_hash_t parse_xxh128(std::string_view hex) { + if (hex.size() != 32) + throw std::invalid_argument("expected 32 hex chars for XXH128"); + + return XXH128_hash_t{ + .low64 = parse_u64_hex(hex.substr(16, 16)), + .high64 = parse_u64_hex(hex.substr(0, 16)), + }; +} + +const char* verification_state_name(dusk::DiscVerificationState state) noexcept { + switch (state) { + case dusk::DiscVerificationState::Success: + return "verified"; + case dusk::DiscVerificationState::HashMismatch: + return "hash mismatch"; + case dusk::DiscVerificationState::Unknown: + default: + return "unknown"; + } +} + +} // namespace namespace dusk::iso { -constexpr const char* TP_GAME_IDS[] = { - "GZ2E01", // GCN USA - "GZ2P01", // GCN PAL - "GZ2J01", // GCN JPN - "RZDE01", // Wii USA - "RZDP01", // Wii PAL - "RZDJ01", // Wii JPN - "RZDK01", // Wii KOR +enum class Platform : u8 { + GameCube, + Wii, }; -constexpr const char* SUPPORTED_TP_GAME_IDS[] = { - "GZ2E01", // GCN USA +enum class Region : u8 { + NorthAmerica, + Europe, + Japan, + Korea, }; -template -constexpr bool matches(const char (&id)[6], const char* const (&valid)[N]) { - for (auto elem : valid) { - if (strncmp(id, elem, 6) == 0) { - return true; - } +struct KnownDisc { + std::string_view id; + Platform platform; + Region region; + bool supported = false; + XXH128_hash_t hash{}; + + constexpr KnownDisc(std::string_view id, Platform platform, Region region) + : id(id), platform(platform), region(region) {} + constexpr KnownDisc( + std::string_view id, Platform platform, Region region, const std::string_view hash) + : id(id), platform(platform), region(region), supported(true), hash(parse_xxh128(hash)) {} +}; + +constexpr auto KNOWN_DISCS = std::to_array({ + {"GZ2E01", Platform::GameCube, Region::NorthAmerica, "14e886f08e548a000afde98a3195e788"}, + {"GZ2J01", Platform::GameCube, Region::Japan}, + {"GZ2P01", Platform::GameCube, Region::Europe, "9ef597588b0035ca9e91b333fa9a8a7e"}, + {"RZDE01", Platform::Wii, Region::NorthAmerica}, + {"RZDJ01", Platform::Wii, Region::Japan}, + {"RZDK01", Platform::Wii, Region::Korea}, + {"RZDP01", Platform::Wii, Region::Europe}, +}); + +constexpr const KnownDisc* find_disc(std::string_view id) { + for (const auto& disc : KNOWN_DISCS) { + if (disc.id == id) + return &disc; } - - return false; + return nullptr; } struct NodHandleWrapper { NodHandle* handle; - NodHandleWrapper() : handle(nullptr) { - } - + NodHandleWrapper() : handle(nullptr) {} ~NodHandleWrapper() { if (handle != nullptr) { nod_free(handle); @@ -46,7 +116,7 @@ struct NodHandleWrapper { } }; -static ValidationError convertNodError(NodResult result) { +static ValidationError convert_nod_error(NodResult result) { switch (result) { case NOD_RESULT_ERR_IO: return ValidationError::IOError; @@ -61,66 +131,141 @@ s64 StreamReadAt(void* user_data, u64 offset, void* out, size_t len) { if (len == 0) { return 0; } - - auto io = static_cast(user_data); - - auto ret = SDL_SeekIO(io, static_cast(offset), SDL_IO_SEEK_SET); + auto* io = static_cast(user_data); + const auto ret = SDL_SeekIO(io, static_cast(offset), SDL_IO_SEEK_SET); if (ret < 0) { return -1; } - - auto read = SDL_ReadIO(io, out, len); + const auto read = SDL_ReadIO(io, out, len); if (read == 0) { if (SDL_GetIOStatus(io) == SDL_IO_STATUS_EOF) { return 0; } - return -1; } - return static_cast(read); } s64 StreamLength(void* user_data) { - auto io = static_cast(user_data); - return SDL_GetIOSize(io); + return SDL_GetIOSize(static_cast(user_data)); } void StreamClose(void* user_data) { - auto io = static_cast(user_data); - SDL_CloseIO(io); + SDL_CloseIO(static_cast(user_data)); } -ValidationError validate(const char* path) { - NodHandleWrapper disc; +ValidationError verify_disc(NodHandle* disc, VerificationStatus& status) { + std::unique_ptr hashState( + XXH3_createState(), XXH3_freeState); + if (!hashState) { + return ValidationError::Unknown; + } + XXH3_128bits_reset(hashState.get()); + while (true) { + if (status.shouldCancel.load(std::memory_order_relaxed)) { + return ValidationError::Canceled; + } + + size_t bytesAvail; + const auto buf = nod_buf_read(disc, &bytesAvail); + if (!bytesAvail) + break; + + XXH3_128bits_update(hashState.get(), buf, bytesAvail); + + status.bytesRead.fetch_add(bytesAvail, std::memory_order_relaxed); + nod_buf_consume(disc, bytesAvail); + } + + const auto hash = XXH3_128bits_digest(hashState.get()); + if (!XXH128_isEqual(hash, status.knownDisc->hash)) { + return ValidationError::HashMismatch; + } + return ValidationError::Success; +} + +ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info) { const auto sdlStream = SDL_IOFromFile(path, "rb"); - const NodDiscStream nod_stream { + if (sdlStream == nullptr) { + return ValidationError::IOError; + } + + NodHandleWrapper disc; + const NodDiscStream nod_stream{ .user_data = sdlStream, .read_at = StreamReadAt, .stream_len = StreamLength, .close = StreamClose, }; - auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle); if (disc.handle == nullptr || result != NOD_RESULT_OK) { - return convertNodError(result); + return convert_nod_error(result); + } + + status.bytesTotal.store(nod_disc_size(disc.handle), std::memory_order_relaxed); + + NodDiscHeader header{}; + result = nod_disc_header(disc.handle, &header); + if (result != NOD_RESULT_OK) { + return convert_nod_error(result); + } + + const auto knownDisc = find_disc(std::string_view(header.game_id, 6)); + if (!knownDisc) { + return ValidationError::WrongGame; + } + status.knownDisc = knownDisc; + info.isPal = knownDisc->region == Region::Europe; + if (!knownDisc->supported) { + return ValidationError::WrongVersion; + } + return verify_disc(disc.handle, status); +} + +ValidationError inspect(const char* path, DiscInfo& info) { + const auto sdlStream = SDL_IOFromFile(path, "rb"); + if (sdlStream == nullptr) { + return ValidationError::IOError; + } + + NodHandleWrapper disc; + const NodDiscStream nod_stream{ + .user_data = sdlStream, + .read_at = StreamReadAt, + .stream_len = StreamLength, + .close = StreamClose, + }; + auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle); + if (disc.handle == nullptr || result != NOD_RESULT_OK) { + return convert_nod_error(result); } NodDiscHeader header{}; result = nod_disc_header(disc.handle, &header); if (result != NOD_RESULT_OK) { - return convertNodError(result); + return convert_nod_error(result); } - if (!matches(header.game_id, TP_GAME_IDS)) { + const auto knownDisc = find_disc(std::string_view(header.game_id, 6)); + if (!knownDisc) { return ValidationError::WrongGame; } - - if (!matches(header.game_id, SUPPORTED_TP_GAME_IDS)) { + info.isPal = knownDisc->region == Region::Europe; + if (!knownDisc->supported) { return ValidationError::WrongVersion; } - return ValidationError::Success; } -} // namespace dusk::iso \ No newline at end of file + +bool isPal(const char* path) { + DiscInfo info{}; + return inspect(path, info) == ValidationError::Success && info.isPal; +} + +void log_verification_state(std::string_view path, DiscVerificationState state) { + const std::string pathText = path.empty() ? "" : std::string(path); + DuskLog.info( + "Disc verification status: {} (path: {})", verification_state_name(state), pathText); +} +} // namespace dusk::iso diff --git a/src/dusk/iso_validate.hpp b/src/dusk/iso_validate.hpp index da1ef1f2a6..176284fd02 100644 --- a/src/dusk/iso_validate.hpp +++ b/src/dusk/iso_validate.hpp @@ -1,18 +1,38 @@ #ifndef DUSK_ISO_VALIDATE_HPP #define DUSK_ISO_VALIDATE_HPP -namespace dusk::iso { - enum class ValidationError : u8 { - Success = 0, - IOError, - InvalidImage, - WrongGame, - WrongVersion, - ExecutableMismatch, - Unknown - }; +#include - ValidationError validate(const char* path); -} +namespace dusk::iso { +struct KnownDisc; + +enum class ValidationError : u8 { + Unknown = 0, + IOError, + InvalidImage, + WrongGame, + WrongVersion, + Canceled, + HashMismatch, + Success +}; + +struct VerificationStatus { + std::atomic_size_t bytesRead = 0; + std::atomic_size_t bytesTotal = 0; + const KnownDisc* knownDisc = nullptr; + std::atomic_bool shouldCancel = false; +}; + +struct DiscInfo { + bool isPal = false; +}; + +ValidationError inspect(const char* path, DiscInfo& info); +ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info); +bool isPal(const char* path); +void log_verification_state(std::string_view path, DiscVerificationState state); + +} // namespace dusk::iso #endif // DUSK_ISO_VALIDATE_HPP diff --git a/src/dusk/livesplit.cpp b/src/dusk/livesplit.cpp new file mode 100644 index 0000000000..cec765f223 --- /dev/null +++ b/src/dusk/livesplit.cpp @@ -0,0 +1,183 @@ +#if _WIN32 + #include + #include + using socket_t = SOCKET; + static void closeSocket(socket_t s) { closesocket(s); } +#else + #include + #include + #include + #include + #include + #include + using socket_t = int; + static void closeSocket(socket_t s) { close(s); } + #ifndef INVALID_SOCKET + #define INVALID_SOCKET -1 + #endif +#endif + +#include +#include "dusk/livesplit.h" +#include "f_op/f_op_overlap_mng.h" + +namespace dusk::speedrun { + +static bool running = false; +static uint64_t frameCount = 0; +static socket_t sock = INVALID_SOCKET; +static bool wasLoading = false; +static bool connected = false; +static bool connectPending = false; +static bool disconnectPending = false; + +static void sendCmd(const char* cmd) { + if (sock == INVALID_SOCKET) { + return; + } + + char msg[64]; + int len = snprintf(msg, sizeof(msg), "%s\r\n", cmd); + + if (send(sock, msg, len, 0) >= 0) { + if (!connected) { + connected = connectPending = true; + } + + return; + } + +#if _WIN32 + int err = WSAGetLastError(); + if (err == WSAEWOULDBLOCK || err == WSAENOTCONN) { + return; + } +#else + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == ENOTCONN) { + return; + } +#endif + + if (connected) disconnectPending = true; + closeSocket(sock); + sock = INVALID_SOCKET; + connected = connectPending = false; +} + +uint64_t getFrameCount() { + return frameCount; +} + +void onGameFrame() { + if (!running) { + return; + } + + bool loading = fopOvlpM_IsDoingReq() != 0; + + if (loading != wasLoading) { + sendCmd(loading ? "pausegametime" : "unpausegametime"); + wasLoading = loading; + } + + if (!loading) { + ++frameCount; + } +} + +void start() { + if (running) { + return; + } + + running = true; + frameCount = 0; + wasLoading = false; + sendCmd("initgametime"); + sendCmd("reset"); + sendCmd("starttimer"); +} + +void reset() { + running = false; + frameCount = 0; + wasLoading = false; + sendCmd("reset"); +} + +void connectLiveSplit(const char* host, int port) { +#if _WIN32 + WSADATA wd{}; WSAStartup(MAKEWORD(2, 2), &wd); +#endif + + if (sock != INVALID_SOCKET) { + closeSocket(sock); sock = INVALID_SOCKET; + } + + sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + if (sock == INVALID_SOCKET) { + return; + } + +#if _WIN32 + u_long nb = 1; + ioctlsocket(sock, FIONBIO, &nb); +#else + fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK); +#endif + + sockaddr_in addr{}; addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + inet_pton(AF_INET, host, &addr.sin_addr); + connect(sock, (sockaddr*)&addr, sizeof(addr)); + sendCmd("initgametime"); +} + +void disconnectLiveSplit() { + if (sock != INVALID_SOCKET) { + closeSocket(sock); + sock = INVALID_SOCKET; + connected = false; + } +} + +bool consumeConnectedEvent() { bool v = connectPending; connectPending = false; return v; } +bool consumeDisconnectedEvent() { bool v = disconnectPending; disconnectPending = false; return v; } + +void updateLiveSplit() { + if (sock == INVALID_SOCKET) { + return; + } + + if (!connected) { + sendCmd("initgametime"); + return; + } + + if (!running) { + return; + } + + const uint64_t totalMs = frameCount * 1000 / 30; + const uint64_t totalSec = totalMs / 1000; + char cmd[32]; + + snprintf(cmd, sizeof(cmd), "setgametime %u:%02u:%02u.%03u", + (uint32_t)(totalSec / 3600), + (uint32_t)((totalSec / 60) % 60), + (uint32_t)(totalSec % 60), + (uint32_t)(totalMs % 1000) + ); + + sendCmd(cmd); +} + +void shutdown() { + disconnectLiveSplit(); +#if _WIN32 + WSACleanup(); +#endif +} + +} diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index 172059aea4..1c0a6ecbdb 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -1,5 +1,6 @@ #include "dusk/logging.h" #include +#include #include #include #include @@ -7,6 +8,7 @@ #include #include +#include "dusk/io.hpp" #include "tracy/Tracy.hpp" #if TARGET_ANDROID @@ -32,9 +34,33 @@ static constexpr std::string_view StubFragments[] = { }; namespace { -std::mutex g_logMutex; -FILE* g_logFile = nullptr; -std::string g_logFilePath; +// On macOS, std::mutex becomes poisoned when its dtor is run. +// We use this to check if the LogState is destroyed before attempting to acquire it. +std::atomic g_logStateAlive(true); + +struct LogState { + std::mutex mutex; + FILE* file = nullptr; + std::u8string filePath; + + ~LogState() { + CloseFile(); + g_logStateAlive.store(false, std::memory_order_release); + } + + void CloseFile() { + if (!g_logStateAlive.load(std::memory_order_acquire)) { + return; + } + std::lock_guard lock(mutex); + if (file != nullptr) { + std::fflush(file); + std::fclose(file); + file = nullptr; + } + } +}; +LogState g_logState; const char* LogLevelString(AuroraLogLevel level) { switch (level) { @@ -152,10 +178,10 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m FILE* out = LogStreamForLevel(level); WriteLogLine(out, levelStr, module, message, len); - { - std::lock_guard lock(g_logMutex); - if (g_logFile != nullptr) { - WriteLogLine(g_logFile, levelStr, module, message, len); + if (g_logStateAlive.load(std::memory_order_acquire)) { + std::lock_guard lock(g_logState.mutex); + if (g_logState.file != nullptr) { + WriteLogLine(g_logState.file, levelStr, module, message, len); } } @@ -169,8 +195,11 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m aurora::Module DuskLog("dusk"); void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel) { - std::lock_guard lock(g_logMutex); - if (g_logFile != nullptr || configDir.empty()) { + if (!g_logStateAlive.load(std::memory_order_acquire)) { + return; + } + std::lock_guard lock(g_logState.mutex); + if (g_logState.file != nullptr || configDir.empty()) { return; } @@ -179,36 +208,36 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL std::filesystem::create_directories(logsDir, ec); if (ec) { std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n", - logsDir.string().c_str(), ec.message().c_str()); + io::fs_path_to_string(logsDir).c_str(), ec.message().c_str()); return; } const std::filesystem::path logPath = logsDir / MakeTimestampedLogName(); - g_logFile = std::fopen(logPath.string().c_str(), "wb"); - if (g_logFile == nullptr) { + g_logState.file = io::FileStream::Create(logPath).ToInner(); + if (g_logState.file == nullptr) { std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n", - logPath.string().c_str()); + io::fs_path_to_string(logPath).c_str()); return; } - g_logFilePath = logPath.string(); + g_logState.filePath = logPath.u8string(); aurora::g_config.logCallback = &aurora_log_callback; aurora::g_config.logLevel = logLevel; - WriteLogLine(g_logFile, "INFO", "dusk", "File logging initialized", 24); + WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24); } void dusk::ShutdownFileLogging() { - std::lock_guard lock(g_logMutex); - if (g_logFile == nullptr) { + if (!g_logStateAlive.load(std::memory_order_acquire)) { return; } - - std::fflush(g_logFile); - std::fclose(g_logFile); - g_logFile = nullptr; + g_logState.CloseFile(); } const char* dusk::GetLogFilePath() { - std::lock_guard lock(g_logMutex); - return g_logFilePath.empty() ? nullptr : g_logFilePath.c_str(); + if (!g_logStateAlive.load(std::memory_order_acquire)) { + return nullptr; + } + std::lock_guard lock(g_logState.mutex); + return reinterpret_cast( + g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str()); } diff --git a/src/dusk/main.cpp b/src/dusk/main.cpp index 79244cb203..08e582f3e0 100644 --- a/src/dusk/main.cpp +++ b/src/dusk/main.cpp @@ -5,17 +5,111 @@ #endif #include +#include "dusk/main.h" +#include "dusk/io.hpp" +#include +#include +#include #include #include +#include +#include #include #include #include +#if !defined(_WIN32) +#include +#if defined(__APPLE__) +#include +#endif +#endif + int game_main(int argc, char* argv[]); namespace { +bool RestartProcess(int argc, char* argv[]) { +#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ + (defined(TARGET_OS_TV) && TARGET_OS_TV) + (void)argc; + (void)argv; + return false; +#elif _WIN32 + std::wstring commandLine = GetCommandLineW(); + STARTUPINFOW startupInfo{}; + startupInfo.cb = sizeof(startupInfo); + PROCESS_INFORMATION processInfo{}; + if (!CreateProcessW(nullptr, commandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, + &startupInfo, &processInfo)) + { + fprintf(stderr, "Failed to restart Dusk: CreateProcessW error %lu\n", GetLastError()); + return false; + } + + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + return true; +#else + std::filesystem::path executablePath; + +#if defined(__APPLE__) + uint32_t pathSize = 0; + _NSGetExecutablePath(nullptr, &pathSize); + if (pathSize > 0) { + std::string path(pathSize, '\0'); + if (_NSGetExecutablePath(path.data(), &pathSize) == 0) { + path.resize(std::strlen(path.c_str())); + std::error_code ec; + executablePath = std::filesystem::weakly_canonical(path, ec); + if (ec) { + executablePath = path; + } + } + } +#elif defined(__linux__) + std::array path{}; + const ssize_t len = readlink("/proc/self/exe", path.data(), path.size() - 1); + if (len > 0) { + path[static_cast(len)] = '\0'; + executablePath = path.data(); + } +#endif + + if (executablePath.empty() && argc > 0 && argv[0] != nullptr && argv[0][0] != '\0') { + std::error_code ec; + executablePath = std::filesystem::absolute(argv[0], ec); + if (ec) { + executablePath = argv[0]; + } + } + + if (executablePath.empty()) { + fprintf(stderr, "Failed to restart Dusk: unable to resolve executable path\n"); + return false; + } + + std::vector args; + args.reserve(static_cast(std::max(argc, 1))); + args.push_back(dusk::io::fs_path_to_string(executablePath)); + for (int i = 1; i < argc; ++i) { + args.emplace_back(argv[i] != nullptr ? argv[i] : ""); + } + + std::vector execArgv; + execArgv.reserve(args.size() + 1); + for (auto& arg : args) { + execArgv.push_back(arg.data()); + } + execArgv.push_back(nullptr); + + execv(executablePath.c_str(), execArgv.data()); + fprintf(stderr, "Failed to restart Dusk: execv failed: %s\n", std::strerror(errno)); + return false; +#endif +} + #if _WIN32 bool ShouldShowWindowsConsole(int argc, char* argv[]) { if (const auto* env = std::getenv("DUSK_CONSOLE")) { @@ -53,19 +147,25 @@ void WindowsSetupConsole(bool showConsole) { SetConsoleOutputCP(CP_UTF8); if (const HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); - stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr) { + stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr) + { DWORD consoleMode = 0; if (GetConsoleMode(stdoutHandle, &consoleMode)) { SetConsoleMode(stdoutHandle, - consoleMode | ENABLE_PROCESSED_OUTPUT - | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + consoleMode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); } } } int DuskMain(int argc, char* argv[]) { WindowsSetupConsole(ShouldShowWindowsConsole(argc, argv)); - return game_main(argc, argv); + const int result = game_main(argc, argv); + if constexpr (dusk::SupportsProcessRestart) { + if (dusk::RestartRequested) { + return RestartProcess(argc, argv) ? 0 : result; + } + } + return result; } std::vector WideArgsToUtf8(int argc, wchar_t** argv) { @@ -81,8 +181,8 @@ std::vector WideArgsToUtf8(int argc, wchar_t** argv) { } std::vector utf8Buffer(static_cast(requiredSize)); - WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr, - nullptr); + WideCharToMultiByte( + CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr, nullptr); utf8Args.emplace_back(utf8Buffer.data()); } @@ -109,7 +209,11 @@ int RunWindowsGuiEntryPoint() { } #else int DuskMain(int argc, char* argv[]) { - return game_main(argc, argv); + const int result = game_main(argc, argv); + if (dusk::RestartRequested && RestartProcess(argc, argv)) { + return 0; + } + return result; } #endif diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 3e6ee733fe..f0d10d4226 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -8,6 +8,8 @@ UserSettings g_userSettings = { .enableFullscreen {"video.enableFullscreen", false}, .enableVsync {"video.enableVsync", true}, .lockAspectRatio {"video.lockAspectRatio", false}, + .enableFpsOverlay {"game.enableFpsOverlay", false}, + .fpsOverlayCorner {"game.fpsOverlayCorner", 0}, }, .audio = { @@ -17,48 +19,56 @@ UserSettings g_userSettings = { .soundEffectsVolume {"audio.soundEffectsVolume", 100}, .fanfareVolume {"audio.fanfareVolume", 100}, .enableReverb {"audio.enableReverb", true}, + .enableHrtf {"audio.enableHrtf", false}, + .menuSounds {"audio.menuSounds", true}, }, .game = { + .language { "game.language", GameLanguage::English }, + // Quality of Life .enableQuickTransform {"game.enableQuickTransform", false}, - .hideTvSettingsScreen {"game.hideTvSettingsScreen", false}, - .skipWarningScreen {"game.skipWarningScreen", false}, + .hideTvSettingsScreen {"game.hideTvSettingsScreen", true}, .biggerWallets {"game.biggerWallets", false}, .noReturnRupees {"game.noReturnRupees", false}, .disableRupeeCutscenes {"game.disableRupeeCutscenes", false}, .noSwordRecoil {"game.noSwordRecoil", false}, .damageMultiplier {"game.damageMultiplier", 1}, - .noHeartDrops{"game.noHeartDrops", false}, + .noHeartDrops {"game.noHeartDrops", false}, .instantDeath {"game.instantDeath", false}, .fastClimbing {"game.fastClimbing", false}, .noMissClimbing {"game.noMissClimbing", false}, .fastTears {"game.fastTears", false}, + .no2ndFishForCat {"game.no2ndFishForCat", false}, .instantSaves {"game.instantSaves", false}, .instantText {"game.instantText", false}, .sunsSong {"game.sunsSong", false}, + .autoSave {"game.autoSave", false}, // Preferences .enableMirrorMode {"game.enableMirrorMode", false}, - .invertCameraXAxis {"game.invertCameraXAxis", false}, - .disableMainHUD {"game.disableMainHUD", false}, + .minimalHUD {"game.minimalHUD", false}, .pauseOnFocusLost {"game.pauseOnFocusLost", false}, - .enableLinkDollRotation = {"game.enableLinkDollRotation", false }, + .enableLinkDollRotation {"game.enableLinkDollRotation", false}, + .enableAchievementToasts {"game.enableAchievementToasts", true}, + .enableControllerToasts {"game.enableControllerToasts", true}, // Graphics - .bloomMode {"game.bloomMode", BloomMode::Classic}, + .bloomMode {"game.bloomMode", BloomMode::Dusk}, .bloomMultiplier {"game.bloomMultiplier", 1.0f}, .disableWaterRefraction {"game.disableWaterRefraction", false}, - .enableFrameInterpolation = {"game.enableFrameInterpolation", false}, + .enableFrameInterpolation {"game.enableFrameInterpolation", false}, .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, .enableDepthOfField {"game.enableDepthOfField", true}, + .enableMapBackground {"game.enableMapBackground", true}, // Audio .noLowHpSound {"game.noLowHpSound", false}, .midnasLamentNonStop {"game.midnasLamentNonStop", false}, // Input + .gyroMode {"game.gyroMode", GyroMode::Sensor}, .enableGyroAim {"game.enableGyroAim", false}, .enableGyroRollgoal {"game.enableGyroRollgoal", false}, .gyroSensitivityX {"game.gyroSensitivityX", 1.0f}, @@ -68,18 +78,25 @@ UserSettings g_userSettings = { .gyroDeadband {"game.gyroDeadband", 0.04f}, .gyroInvertPitch {"game.gyroInvertPitch", false}, .gyroInvertYaw {"game.gyroInvertYaw", false}, + .freeCamera {"game.freeCamera", false}, + .invertCameraXAxis {"game.invertCameraXAxis", false}, + .invertCameraYAxis {"game.invertCameraYAxis", false}, + .freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f}, + .debugFlyCam {"game.debugFlyCam", false}, + .debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true}, + .allowBackgroundInput {"game.allowBackgroundInput", true}, // Cheats .infiniteHearts {"game.infiniteHearts", false}, - .infiniteArrows{"game.infiniteArrows", false}, - .infiniteBombs{"game.infiniteBombs", false}, - .infiniteOil{"game.infiniteOil", false}, - .infiniteOxygen{"game.infiniteOxygen", false}, - .infiniteRupees{"game.infiniteRupees", false}, + .infiniteArrows {"game.infiniteArrows", false}, + .infiniteBombs {"game.infiniteBombs", false}, + .infiniteOil {"game.infiniteOil", false}, + .infiniteOxygen {"game.infiniteOxygen", false}, + .infiniteRupees {"game.infiniteRupees", false}, .enableIndefiniteItemDrops {"game.enableIndefiniteItemDrops", false}, - .moonJump{"game.moonJump", false}, - .superClawshot{"game.superClawshot", false}, - .alwaysGreatspin{"game.alwaysGreatspin", false}, + .moonJump {"game.moonJump", false}, + .superClawshot {"game.superClawshot", false}, + .alwaysGreatspin {"game.alwaysGreatspin", false}, .enableFastIronBoots {"game.enableFastIronBoots", false}, .canTransformAnywhere {"game.canTransformAnywhere", false}, .fastSpinner {"game.fastSpinner", false}, @@ -89,18 +106,25 @@ UserSettings g_userSettings = { .restoreWiiGlitches {"game.restoreWiiGlitches", false}, // Controls - .enableTurboKeybind {"game.enableTurboKeybind", false} + .enableTurboKeybind {"game.enableTurboKeybind", false}, + + // Tools + .speedrunMode {"game.speedrunMode", false}, + .liveSplitEnabled {"game.liveSplitEnabled", false}, + .recordingMode {"game.recordingMode", false} }, .backend = { .isoPath {"backend.isoPath", ""}, + .isoVerification {"backend.isoVerification", DiscVerificationState::Unknown}, .graphicsBackend {"backend.graphicsBackend", "auto"}, .skipPreLaunchUI {"backend.skipPreLaunchUI", false}, .showPipelineCompilation {"backend.showPipelineCompilation", false}, .wasPresetChosen {"backend.wasPresetChosen", false}, .enableCrashReporting {"backend.enableCrashReporting", true}, - .duskMenuOpen {"backend.duskMenuOpen", false}, - .cardFileType {"backend.cardFileType", static_cast(CARD_GCIFOLDER)} + .checkForUpdates {"backend.checkForUpdates", true}, + .cardFileType {"backend.cardFileType", static_cast(CARD_GCIFOLDER)}, + .enableAdvancedSettings {"backend.enableAdvancedSettings", false}, } }; @@ -113,6 +137,8 @@ void registerSettings() { Register(g_userSettings.video.enableFullscreen); Register(g_userSettings.video.enableVsync); Register(g_userSettings.video.lockAspectRatio); + Register(g_userSettings.video.enableFpsOverlay); + Register(g_userSettings.video.fpsOverlayCorner); // Audio Register(g_userSettings.audio.masterVolume); @@ -121,11 +147,13 @@ void registerSettings() { Register(g_userSettings.audio.soundEffectsVolume); Register(g_userSettings.audio.fanfareVolume); Register(g_userSettings.audio.enableReverb); + Register(g_userSettings.audio.enableHrtf); + Register(g_userSettings.audio.menuSounds); // Game + Register(g_userSettings.game.language); Register(g_userSettings.game.enableQuickTransform); Register(g_userSettings.game.hideTvSettingsScreen); - Register(g_userSettings.game.skipWarningScreen); Register(g_userSettings.game.biggerWallets); Register(g_userSettings.game.noReturnRupees); Register(g_userSettings.game.disableRupeeCutscenes); @@ -135,12 +163,16 @@ void registerSettings() { Register(g_userSettings.game.instantDeath); Register(g_userSettings.game.fastClimbing); Register(g_userSettings.game.fastTears); + Register(g_userSettings.game.no2ndFishForCat); Register(g_userSettings.game.instantSaves); Register(g_userSettings.game.instantText); Register(g_userSettings.game.sunsSong); + Register(g_userSettings.game.autoSave); Register(g_userSettings.game.enableMirrorMode); Register(g_userSettings.game.invertCameraXAxis); - Register(g_userSettings.game.disableMainHUD); + Register(g_userSettings.game.invertCameraYAxis); + Register(g_userSettings.game.freeCameraSensitivity); + Register(g_userSettings.game.minimalHUD); Register(g_userSettings.game.pauseOnFocusLost); Register(g_userSettings.game.bloomMode); Register(g_userSettings.game.bloomMultiplier); @@ -148,15 +180,21 @@ void registerSettings() { Register(g_userSettings.game.internalResolutionScale); Register(g_userSettings.game.shadowResolutionMultiplier); Register(g_userSettings.game.enableDepthOfField); + Register(g_userSettings.game.enableMapBackground); Register(g_userSettings.game.enableFastIronBoots); Register(g_userSettings.game.canTransformAnywhere); Register(g_userSettings.game.freeMagicArmor); Register(g_userSettings.game.restoreWiiGlitches); Register(g_userSettings.game.enableLinkDollRotation); + Register(g_userSettings.game.enableAchievementToasts); + Register(g_userSettings.game.enableControllerToasts); Register(g_userSettings.game.noMissClimbing); Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); Register(g_userSettings.game.enableTurboKeybind); + Register(g_userSettings.game.speedrunMode); + Register(g_userSettings.game.liveSplitEnabled); + Register(g_userSettings.game.recordingMode); Register(g_userSettings.game.fastSpinner); Register(g_userSettings.game.infiniteHearts); Register(g_userSettings.game.infiniteArrows); @@ -169,6 +207,7 @@ void registerSettings() { Register(g_userSettings.game.superClawshot); Register(g_userSettings.game.alwaysGreatspin); Register(g_userSettings.game.enableFrameInterpolation); + Register(g_userSettings.game.gyroMode); Register(g_userSettings.game.enableGyroAim); Register(g_userSettings.game.enableGyroRollgoal); Register(g_userSettings.game.gyroSensitivityX); @@ -178,15 +217,21 @@ void registerSettings() { Register(g_userSettings.game.gyroSmoothing); Register(g_userSettings.game.gyroInvertPitch); Register(g_userSettings.game.gyroInvertYaw); + Register(g_userSettings.game.freeCamera); + Register(g_userSettings.game.debugFlyCam); + Register(g_userSettings.game.debugFlyCamLockEvents); + Register(g_userSettings.game.allowBackgroundInput); Register(g_userSettings.backend.isoPath); + Register(g_userSettings.backend.isoVerification); Register(g_userSettings.backend.graphicsBackend); Register(g_userSettings.backend.skipPreLaunchUI); Register(g_userSettings.backend.showPipelineCompilation); Register(g_userSettings.backend.wasPresetChosen); Register(g_userSettings.backend.enableCrashReporting); - Register(g_userSettings.backend.duskMenuOpen); + Register(g_userSettings.backend.checkForUpdates); Register(g_userSettings.backend.cardFileType); + Register(g_userSettings.backend.enableAdvancedSettings); } // Transient settings diff --git a/src/dusk/stubs.cpp b/src/dusk/stubs.cpp index 07e420445c..c6832d2cac 100644 --- a/src/dusk/stubs.cpp +++ b/src/dusk/stubs.cpp @@ -1020,10 +1020,6 @@ void GXInitTexCacheRegion(GXTexRegion* region, GXBool is_32b_mipmap, u32 tmem_ev // XXX, this should be some struct? // GXRenderModeObj GXNtsc480IntDf; //GXRenderModeObj GXNtsc480Int; -void GXPeekZ(u16 x, u16 y, u32* z) { - STUB_LOG(); - *z = 0; -} void GXReadXfRasMetric(u32* xf_wait_in, u32* xf_wait_out, u32* ras_busy, u32* clocks) { STUB_LOG(); *xf_wait_in = 0; diff --git a/src/dusk/ui/achievements.cpp b/src/dusk/ui/achievements.cpp new file mode 100644 index 0000000000..b631d444f5 --- /dev/null +++ b/src/dusk/ui/achievements.cpp @@ -0,0 +1,231 @@ +#include "achievements.hpp" + +#include "Z2AudioLib/Z2SeMgr.h" +#include "dusk/achievements.h" +#include "fmt/format.h" +#include "m_Do/m_Do_audio.h" +#include "nav_types.hpp" +#include "pane.hpp" + +namespace dusk::ui { +namespace { + +struct CategoryInfo { + AchievementCategory cat; + const char* label; +}; + +constexpr CategoryInfo kCategories[] = { + {AchievementCategory::Challenge, "Challenge"}, + {AchievementCategory::Collection, "Collection"}, + {AchievementCategory::Minigame, "Minigame"}, + {AchievementCategory::Misc, "Misc"}, + {AchievementCategory::Glitched, "Glitched"}, +}; + +Rml::String build_achievement_info_rml(const Achievement& a) { + Rml::String s = fmt::format( + R"(
)" + R"({})" + R"({})" + R"(
)" + R"(

{}

)", + a.unlocked ? " unlocked" : "", + a.name, + a.unlocked ? " unlocked" : " locked", + a.unlocked ? "Unlocked" : "Locked", + a.description + ); + + if (a.isCounter) { + float fraction = a.goal > 0 ? float(a.progress) / float(a.goal) : 1.0f; + s += fmt::format( + R"()" + R"({} / {})", + fraction, + a.unlocked ? "progress-done" : "progress-ongoing", + a.progress, + a.goal + ); + } + + return s; +} + +class AchievementRow : public FluentComponent { +public: + AchievementRow(Rml::Element* parent, const Achievement& a) + : FluentComponent(createRowRoot(parent)) + { + auto& btn = add_child + + + + + +)RML"; + +constexpr std::array kDiscFileFilters{{ + {"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"}, + {"All Files", "*"}, +}}; + +struct DiscVerificationResult { + std::string path; + iso::DiscInfo info; + iso::ValidationError validation = iso::ValidationError::Unknown; +}; + +struct DiscVerificationTask { + explicit DiscVerificationTask(std::string discPath) : path(std::move(discPath)) { + worker = std::thread([this] { + try { + validation = iso::validate(path.c_str(), status, info); + } catch (const std::exception& e) { + PrelaunchLog.error( + "Disc verification failed with exception for '{}': {}", path, e.what()); + validation = iso::ValidationError::Unknown; + } catch (...) { + PrelaunchLog.error( + "Disc verification failed with unknown exception for '{}'", path); + validation = iso::ValidationError::Unknown; + } + done.store(true, std::memory_order_release); + }); + } + + ~DiscVerificationTask() { + status.shouldCancel.store(true, std::memory_order_relaxed); + join(); + } + + void join() { + if (worker.joinable()) { + worker.join(); + } + } + + [[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); } + + std::string path; + iso::DiscInfo info; + iso::VerificationStatus status; + iso::ValidationError validation = iso::ValidationError::Unknown; + std::atomic_bool done = false; + std::thread worker; +}; + +std::unique_ptr sDiscVerificationTask; +bool sDiscVerificationModalPushed = false; + +struct UpdateCheckTask { + UpdateCheckTask() { + worker = std::thread([this] { + try { + result = update_check::check_latest_github_release("TwilitRealm", "dusk"); + } catch (const std::exception& e) { + result = { + .status = update_check::Status::Failed, + .message = fmt::format("Update check failed with exception: {}", e.what()), + }; + } catch (...) { + result = { + .status = update_check::Status::Failed, + .message = "Update check failed with an unknown exception", + }; + } + done.store(true, std::memory_order_release); + }); + } + + ~UpdateCheckTask() { join(); } + + void join() { + if (worker.joinable()) { + worker.join(); + } + } + + [[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); } + + update_check::Result result; + std::atomic_bool done = false; + std::thread worker; +}; + +std::unique_ptr sUpdateCheckTask; +std::optional sUpdateCheckResult; + +bool verification_state_allows_launch(iso::ValidationError validation) noexcept { + return validation == iso::ValidationError::Unknown || + validation == iso::ValidationError::Success || + validation == iso::ValidationError::HashMismatch; +} + +iso::ValidationError verification_from_config(DiscVerificationState value) noexcept { + switch (value) { + case DiscVerificationState::Success: + return iso::ValidationError::Success; + case DiscVerificationState::HashMismatch: + return iso::ValidationError::HashMismatch; + default: + return iso::ValidationError::Unknown; + } +} + +DiscVerificationState verification_to_config(iso::ValidationError validation) { + switch (validation) { + case iso::ValidationError::Success: + return DiscVerificationState::Success; + case iso::ValidationError::HashMismatch: + return DiscVerificationState::HashMismatch; + default: + return DiscVerificationState::Unknown; + } +} + +std::string format_bytes(std::size_t bytes) { + constexpr double KiB = 1024.0; + constexpr double MiB = KiB * 1024.0; + constexpr double GiB = MiB * 1024.0; + if (bytes >= static_cast(GiB)) { + return fmt::format("{:.2f} GiB", static_cast(bytes) / GiB); + } + if (bytes >= static_cast(MiB)) { + return fmt::format("{:.0f} MiB", static_cast(bytes) / MiB); + } + if (bytes >= static_cast(KiB)) { + return fmt::format("{:.0f} KiB", static_cast(bytes) / KiB); + } + return fmt::format("{} B", bytes); +} + +void begin_disc_verification(std::string path) noexcept { + if (path.empty()) { + return; + } + if (sDiscVerificationTask != nullptr) { + sDiscVerificationTask->status.shouldCancel.store(true, std::memory_order_relaxed); + sDiscVerificationTask.reset(); + } + sDiscVerificationTask = std::make_unique(std::move(path)); + sDiscVerificationModalPushed = false; +} + +std::optional take_finished_disc_verification() { + if (sDiscVerificationTask == nullptr || !sDiscVerificationTask->finished()) { + return std::nullopt; + } + DiscVerificationResult result{ + .path = sDiscVerificationTask->path, + .info = sDiscVerificationTask->info, + .validation = sDiscVerificationTask->validation, + }; + sDiscVerificationTask->join(); + sDiscVerificationTask.reset(); + sDiscVerificationModalPushed = false; + return result; +} + +void begin_update_check() { + if (!getSettings().backend.checkForUpdates.getValue()) { + return; + } + if (sUpdateCheckTask != nullptr || sUpdateCheckResult.has_value()) { + return; + } + sUpdateCheckTask = std::make_unique(); +} + +std::optional take_finished_update_check() { + if (sUpdateCheckTask == nullptr || !sUpdateCheckTask->finished()) { + return std::nullopt; + } + + sUpdateCheckTask->join(); + auto result = std::move(sUpdateCheckTask->result); + sUpdateCheckTask.reset(); + return result; +} + +std::string update_release_label(const update_check::Release& release) { + std::string_view tagName = release.tagName; + if (!tagName.empty() && tagName.front() == 'v') { + tagName.remove_prefix(1); + } + return std::string(tagName); +} + +void open_update_release() { + if (!sUpdateCheckResult.has_value() || + sUpdateCheckResult->status != update_check::Status::UpdateAvailable) + { + return; + } + + const std::string url = sUpdateCheckResult->latest.htmlUrl; + if (url.empty()) { + PrelaunchLog.warn("Update is available, but the release did not include a download URL"); + return; + } + if (!SDL_OpenURL(url.c_str())) { + PrelaunchLog.warn("Failed to open update URL '{}': {}", url, SDL_GetError()); + } +} + +std::string get_error_msg(iso::ValidationError error) { + switch (error) { + default: + return "The selected disc image could not be validated."; + case iso::ValidationError::IOError: + return "Unable to read the selected file."; + case iso::ValidationError::InvalidImage: + return "The selected file is not a valid disc image."; + case iso::ValidationError::WrongGame: + return "The selected game is not supported by Dusk."; + case iso::ValidationError::WrongVersion: + return "Dusk currently supports GameCube USA and PAL disc images only."; + case iso::ValidationError::Canceled: + return "Disc verification was canceled. Dusk cannot guarantee the selected disc image " + "is compatible."; + case iso::ValidationError::HashMismatch: + return "The selected disc image did not pass hash verification. It may be corrupt or " + "modified."; + case iso::ValidationError::Success: + return "The selected disc image is valid."; + } +} + +void persist_disc_choice(const std::string& path, iso::ValidationError validation) { + const auto previousPath = getSettings().backend.isoPath.getValue(); + const auto previousVerification = getSettings().backend.isoVerification.getValue(); + const auto verification = verification_to_config(validation); + + getSettings().backend.isoPath.setValue(path); + getSettings().backend.isoVerification.setValue(verification); + config::Save(); + + if (previousPath != path || previousVerification != verification) { + iso::log_verification_state(path, verification); + } +} + +void apply_valid_disc_result( + const std::string& path, const iso::DiscInfo& info, iso::ValidationError validation) { + auto& state = prelaunch_state(); + state.configuredDiscPath = path; + state.configuredDiscCanLaunch = true; + state.configuredDiscInfo = info; + state.configuredDiscValidation = validation; + if (state.activeDiscPath.empty() || path == state.activeDiscPath) { + state.activeDiscPath = path; + state.activeDiscInfo = info; + } + persist_disc_choice(path, validation); +} + +void apply_disc_verification_result(const DiscVerificationResult& result) { + auto& state = prelaunch_state(); + + if (result.validation == iso::ValidationError::HashMismatch || + result.validation == iso::ValidationError::Canceled) + { + state.pendingDiscPath = result.path; + state.pendingDiscInfo = result.info; + state.pendingDiscValidation = result.validation; + state.errorString = escape(get_error_msg(result.validation)); + return; + } + + if (result.validation == iso::ValidationError::Success) { + apply_valid_disc_result(result.path, result.info, result.validation); + state.errorString.clear(); + state.pendingDiscPath.clear(); + state.pendingDiscInfo = {}; + state.pendingDiscValidation = iso::ValidationError::Unknown; + return; + } + + state.pendingDiscPath.clear(); + state.pendingDiscInfo = {}; + state.pendingDiscValidation = iso::ValidationError::Unknown; + state.errorString = escape(get_error_msg(result.validation)); +} + +class DiscVerificationModal : public WindowSmall { +public: + DiscVerificationModal() : WindowSmall("modal", "modal-dialog") { + auto* header = append(mDialog, "div"); + header->SetClass("modal-header", true); + + auto* title = append(header, "div"); + title->SetClass("modal-title", true); + title->SetInnerRML("Verifying disc image"); + + auto* icon = append(header, "icon"); + icon->SetClass("verifying", true); + + auto* body = append(mDialog, "div"); + body->SetClass("modal-body", true); + + auto* content = append(body, "div"); + content->SetClass("verification-progress", true); + + mFileName = append(content, "div"); + mFileName->SetClass("verification-file", true); + + mProgress = append(content, "progress"); + mProgress->SetClass("progress-ongoing", true); + mProgress->SetClass("verification-progress-bar", true); + mProgress->SetAttribute("value", 0.f); + + mDetail = append(content, "div"); + mDetail->SetClass("verification-detail", true); + + auto* actions = append(mDialog, "div"); + actions->SetClass("modal-actions", true); + mCancelButton = std::make_unique