diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 41758382cc..e81135d5cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -187,6 +187,8 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v4 + with: + packages: platform-tools - name: Install Android SDK packages run: sdkmanager "platforms;android-36" "build-tools;36.1.0" "ndk;${ANDROID_NDK_VERSION}" diff --git a/CMakeLists.txt b/CMakeLists.txt index 834713ed00..c0ab5aa26e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,8 @@ option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF) option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT}) option(DUSK_ENABLE_CODE_MODS "Enable code mods" ON) +option(DUSK_ENABLE_OPUS "Enable loading Opus audio files for mods" OFF) + set(DUSK_HAS_FUNCHOOK OFF) if (DUSK_ENABLE_CODE_MODS AND (NOT APPLE OR CMAKE_SYSTEM_NAME STREQUAL "Darwin")) @@ -180,8 +182,34 @@ FetchContent_Declare(miniz DOWNLOAD_EXTRACT_TIMESTAMP TRUE EXCLUDE_FROM_ALL ) +message(STATUS "dusklight: Fetching PicoSHA2") +FetchContent_Declare(picosha2 + URL https://github.com/okdshin/PicoSHA2/archive/refs/tags/v1.0.1.tar.gz + URL_HASH SHA256=9983136544234e573fe07cc1a22fdf978ad7979043e7be2e9082f6d4991ff8a8 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + EXCLUDE_FROM_ALL +) + +set(_fetch_content_deps miniz picosha2) + +if (DUSK_ENABLE_OPUS) + message(STATUS "dusklight: Fetching opusfile") + + # Opusfile options + SET(OP_DISABLE_HTTP ON CACHE BOOL "" FORCE) + SET(OP_DISABLE_DOCS ON CACHE BOOL "" FORCE) + SET(OP_DISABLE_EXAMPLES ON CACHE BOOL "" FORCE) + + message(STATUS "dusklight: Fetching opusfile") + FetchContent_Declare( + opusfile + GIT_REPOSITORY https://github.com/xiph/opusfile.git + GIT_TAG 6dfd29e7adb87f2e193575fc3fa88cbf1a0b27df + ) + + list(APPEND _fetch_content_deps opusfile) +endif () -set(_fetch_content_deps miniz) if (DUSK_HAS_FUNCHOOK) message(STATUS "dusklight: Fetching funchook") # cmake/PatchFunchook.cmake patches funchook's cmake/capstone.cmake.in to inject a @@ -216,6 +244,14 @@ if (DUSK_HAS_FUNCHOOK) endif () FetchContent_MakeAvailable(${_fetch_content_deps}) +if (DUSK_HAS_FUNCHOOK AND APPLE) + target_sources(funchook-static PRIVATE src/dusk/mods/loader/code_patch_macos.cpp) + target_include_directories(funchook-static PRIVATE src/dusk/mods/loader) + set_source_files_properties(src/dusk/mods/loader/code_patch_macos.cpp + TARGET_DIRECTORY funchook-static PROPERTIES + COMPILE_OPTIONS "-O2;-fno-sanitize=all;-fno-stack-protector") +endif () + # Use signed char on ARM to match the original game (and x86) string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _arch) if(_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU") @@ -241,11 +277,15 @@ include(cmake/GameABIConfig.cmake) find_package(Threads REQUIRED) set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1) set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd aurora::thp - aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::io borealis::log borealis::net borealis::presentation borealis::sentry borealis::update borealis::ws freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt - Threads::Threads zstd::libzstd dusklight_game_headers) + aurora::card borealis::http borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::io borealis::log borealis::net borealis::presentation borealis::sentry borealis::update borealis::ws freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt + Threads::Threads zstd::libzstd dusklight_game_headers picosha2 PNG::PNG) if (DUSK_HAS_FUNCHOOK) list(APPEND GAME_LIBS funchook-static) endif () +if (DUSK_ENABLE_OPUS) + list(APPEND GAME_LIBS OpusFile::opusfile) + list(APPEND GAME_COMPILE_DEFS DUSK_OPUS=1) +endif () if (WIN32) list(APPEND GAME_LIBS Ws2_32) @@ -502,6 +542,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR add_subdirectory(mods/custom_actor_demo) add_subdirectory(mods/cosmetics) add_subdirectory(mods/randomizer) + add_subdirectory(mods/audio_mod) endif () if (APPLE) diff --git a/README.md b/README.md index 473efd8b68..7445e43ea9 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ Dusklight 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. +> [!IMPORTANT] +> Dusklight's official website is https://twilitrealm.dev/, any other website is not affiliated and may be promoting AI-generated misinformation. + # Setup > [!IMPORTANT] diff --git a/cmake/PatchFunchook.cmake b/cmake/PatchFunchook.cmake index 6d5a6e36b4..cbd91fc38e 100644 --- a/cmake/PatchFunchook.cmake +++ b/cmake/PatchFunchook.cmake @@ -2,59 +2,55 @@ file(READ "${SOURCE_DIR}/cmake/capstone.cmake.in" _content) # Insert PATCH_COMMAND before CONFIGURE_COMMAND in the ExternalProject_Add. # Bracket args prevent cmake from substituting ${...} while writing this file. -string(REPLACE - " CONFIGURE_COMMAND \"\"" - [=[ PATCH_COMMAND "${CMAKE_COMMAND}" -DDIR=${CMAKE_CURRENT_BINARY_DIR}/capstone-src -P "${CAPSTONE_FIX_SCRIPT}" +if (NOT _content MATCHES "CAPSTONE_FIX_SCRIPT") + string(REPLACE + " CONFIGURE_COMMAND \"\"" + [=[ PATCH_COMMAND "${CMAKE_COMMAND}" -DDIR=${CMAKE_CURRENT_BINARY_DIR}/capstone-src -P "${CAPSTONE_FIX_SCRIPT}" CONFIGURE_COMMAND ""]=] - _content "${_content}") - -file(WRITE "${SOURCE_DIR}/cmake/capstone.cmake.in" "${_content}") - -file(READ "${SOURCE_DIR}/src/funchook_unix.c" _unix_content) - -# macOS rejects the POSIX mprotect RWX/RW transition for executable image pages on arm64. -# Use Mach VM_PROT_COPY for the short patch window, then restore RX permissions. -if (NOT _unix_content MATCHES "VM_PROT_READ \\| VM_PROT_WRITE \\| VM_PROT_COPY") - string(REPLACE - [=[ rv = mprotect(mstate->addr, mstate->size, prot);]=] - [=[#ifdef __APPLE__ - kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr, - (vm_size_t)mstate->size, FALSE, - VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY); - if (kr == KERN_SUCCESS) { - funchook_log(funchook, " unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR")\n", - mstate->addr, mstate->size, start, len); - return 0; - } - funchook_set_error_message(funchook, "Failed to unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR", error=%s)", - mstate->addr, mstate->size, start, len, - mach_error_string(kr)); - return FUNCHOOK_ERROR_MEMORY_FUNCTION; -#endif - rv = mprotect(mstate->addr, mstate->size, prot);]=] - _unix_content "${_unix_content}") - - string(REPLACE - [=[ char errbuf[128]; - int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=] - [=[ char errbuf[128]; -#ifdef __APPLE__ - kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr, - (vm_size_t)mstate->size, FALSE, - VM_PROT_READ | VM_PROT_EXECUTE); - - if (kr == KERN_SUCCESS) { - funchook_log(funchook, " protect memory %p (size=%"PRIuPTR", prot=read,exec)\n", - mstate->addr, mstate->size); - return 0; - } - funchook_set_error_message(funchook, "Failed to protect memory %p (size=%"PRIuPTR", prot=read,exec, error=%s)", - mstate->addr, mstate->size, - mach_error_string(kr)); - return FUNCHOOK_ERROR_MEMORY_FUNCTION; -#endif - int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=] - _unix_content "${_unix_content}") + _content "${_content}") + file(WRITE "${SOURCE_DIR}/cmake/capstone.cmake.in" "${_content}") endif () -file(WRITE "${SOURCE_DIR}/src/funchook_unix.c" "${_unix_content}") +file(READ "${SOURCE_DIR}/src/funchook.c" _content) +if (NOT _content MATCHES "commit_code_patch") + string(REPLACE "#include \"funchook_internal.h\"" + "#include \"funchook_internal.h\"\n#ifdef __APPLE__\n#include \"code_patch_macos.hpp\"\n#endif" + _content "${_content}") + + foreach(_operation install uninstall) + if (_operation STREQUAL "install") + set(_expected old_code) + set(_replacement new_code) + else () + set(_expected new_code) + set(_replacement old_code) + endif () + set(_original " mem_state_t mstate; + int rv = funchook_unprotect_begin(funchook, &mstate, entry->target_func, JUMP32_BYTE_SIZE); + + if (rv != 0) { + return rv; + } + memcpy(entry->target_func, entry->${_replacement}, JUMP32_BYTE_SIZE); + rv = funchook_unprotect_end(funchook, &mstate); + if (rv != 0) { + return rv; + } + flush_instruction_cache(entry->target_func, JUMP32_BYTE_SIZE);") + string(FIND "${_content}" "${_original}" _position) + if (_position EQUAL -1) + message(FATAL_ERROR "Funchook ${_operation} patch site changed") + endif () + string(REPLACE "${_original}" "#ifdef __APPLE__ + int rv = commit_code_patch(entry->target_func, entry->${_expected}, + entry->${_replacement}, JUMP32_BYTE_SIZE); + if (rv != 0) { + funchook_set_error_message(funchook, \"Code patch commit failed (Mach error %d)\", rv); + return FUNCHOOK_ERROR_MEMORY_FUNCTION; + } +#else +${_original} +#endif" _content "${_content}") + endforeach () + file(WRITE "${SOURCE_DIR}/src/funchook.c" "${_content}") +endif () diff --git a/cmake/SymbolManifest.cmake b/cmake/SymbolManifest.cmake index 7c7e09eaa1..5f4347e005 100644 --- a/cmake/SymbolManifest.cmake +++ b/cmake/SymbolManifest.cmake @@ -2,7 +2,7 @@ include_guard(GLOBAL) get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) -set(_SYMGEN_VERSION "1.3.4") +set(_SYMGEN_VERSION "1.3.6") set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}") set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release") mark_as_advanced(SYMGEN_PATH) @@ -15,32 +15,32 @@ function(symgen_host_asset out_name out_hash) if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") if (_host_processor MATCHES "^(arm64|aarch64)$") set(_asset "symgen-macos-arm64") - set(_asset_hash "SHA256=983ef6278d30bee38f240f451ca736fd294da0e5705ee15029accac5c7a33829") + set(_asset_hash "SHA256=6b836f4fd7e57fc198ca6bfb3e0161504c98f2be2622819849bfc3f265edf4af") elseif (_host_processor MATCHES "^(x86_64|amd64)$") set(_asset "symgen-macos-x86_64") - set(_asset_hash "SHA256=b6ab1720a4f04fadcf291f42d133e2ee68ef2e9a75d143e51584929e808af8a2") + set(_asset_hash "SHA256=93218f786d34d3ec58f96624e0494ff605e6102cdb31cd7c9922364cfc7885a5") endif () elseif (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") if (_host_processor MATCHES "^(aarch64|arm64)$") set(_asset "symgen-linux-aarch64") - set(_asset_hash "SHA256=ba84486ff1ceb753973e6213c9bc5f195b5ff6f220f5d3322fd861966cdc791c") + set(_asset_hash "SHA256=7891446505c4dd428f4b84c7ceed1441719d910b3d5a47d3540aa890538449ae") elseif (_host_processor MATCHES "^(x86_64|amd64)$") set(_asset "symgen-linux-x86_64") - set(_asset_hash "SHA256=8b81a00a2ef8f5dd4b59adba1badfaca57e472bf272516bc12c4625cefb37718") + set(_asset_hash "SHA256=236bc5cd4bda03a8dd53a4531efff1fec3bcd19b09890893b9b6f8f5ae77f041") elseif (_host_processor MATCHES "^(i[3-6]86|x86)$") set(_asset "symgen-linux-i686") - set(_asset_hash "SHA256=01aa85d84148a6806fdf69839dea7e962e6ac46956aed63b9ecb7d596f179f93") + set(_asset_hash "SHA256=e3b4e8e978d630f07ed6aa58d387145356026116b2e8fa99801902dfac73b2ea") endif () elseif (CMAKE_HOST_WIN32) if (_host_processor MATCHES "^(arm64|aarch64)$") set(_asset "symgen-windows-arm64.exe") - set(_asset_hash "SHA256=e116099f859177d7e29fc3444d67083596330e5c4fb3a0461b21402c89249d96") + set(_asset_hash "SHA256=36e2c68bb4f9e3abea728c4cd7ddc01ff1f462be7156af18baa20621b7d39df8") elseif (_host_processor MATCHES "^(x86_64|amd64)$") set(_asset "symgen-windows-x86_64.exe") - set(_asset_hash "SHA256=57ef1c2f563e33fa4915c40194c4ced540f985c48d9cf081047c6bbcb585fa37") + set(_asset_hash "SHA256=171995a33bfd5a5271b5bb78d3f2c1d4aec58721bf3a894aaad5607ff648e2a4") elseif (_host_processor MATCHES "^(i[3-6]86|x86)$") set(_asset "symgen-windows-x86.exe") - set(_asset_hash "SHA256=cc0ba337c798d3f63d8acbb2d16d531d0c2da878fbdd2b6307702334ec880528") + set(_asset_hash "SHA256=9ed095a01e05eb05e91472bfa7b95b84dfeaeead0941a80708196199c95188db") endif () endif () @@ -116,6 +116,7 @@ function(setup_symbol_manifest target) return() endif () add_dependencies(${target} symgen) + set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS "${SYMGEN_EXE}") # Reserve an ELF program-header entry when the linker supports it (mold). # symgen can replace the PT_NULL entry without relocating the table, keeping later post-link tools safe. @@ -148,6 +149,7 @@ function(setup_symbol_manifest target) # signature. add_custom_command(TARGET ${target} POST_BUILD COMMAND "${SYMGEN_EXE}" manifest ${_input} --embed "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" COMMENT "Embedding symbol manifest" VERBATIM) endfunction() diff --git a/docs/images/jaudio.mermaid b/docs/images/jaudio.mermaid new file mode 100644 index 0000000000..e5e3ee221e --- /dev/null +++ b/docs/images/jaudio.mermaid @@ -0,0 +1,58 @@ +flowchart TD + Start[Start Sound Effect] + subgraph BST + params[Look up sound parameters] + end + Start-->|Sound ID|BST + + subgraph BSC + bms_start[Look up BMS start] + end + Start-->|Sound ID|BSC + + BSC-->|BMS data| bms[BMS interpreter] + + subgraph IBNK + bnk[Look up instrument parameters] + end + + bms-->|Plays notes| IBNK + + subgraph WSYS + wsys[Look up wave sample] + end + + IBNK-->|Wave ID|WSYS + + +flowchart TD + Start[Start Sound Effect] + subgraph BST + params[Look up sound parameters] + end + Start-->|Sound ID|BST + + BST-->|File path| ast[Stream /AudioRes/Stream/xxx.ast from disc] + + +flowchart TD + Start[Start Sound Effect] + subgraph BST + params[Look up sound parameters] + end + Start-->|Sound ID|BST + + BST-->|Resource ID| arc[Z2SoundSeqs.arc] + arc-->|BMS file| bms[BMS interpreter] + + subgraph IBNK + bnk[Look up instrument parameters] + end + + bms-->|Plays notes| IBNK + + subgraph WSYS + wsys[Look up wave sample] + end + + IBNK-->|Wave ID|WSYS diff --git a/docs/images/jaudio_se_flow.svg b/docs/images/jaudio_se_flow.svg new file mode 100644 index 0000000000..03d4bbb29b --- /dev/null +++ b/docs/images/jaudio_se_flow.svg @@ -0,0 +1 @@ +

Sound ID

Sound ID

BMS data

Plays notes

Wave ID

WSYS

Look up wave sample

IBNK

Look up instrument parameters

BSC

Look up BMS start

BST

Look up sound parameters

Start Sound Effect

BMS interpreter

\ No newline at end of file diff --git a/docs/images/jaudio_seq_flow.svg b/docs/images/jaudio_seq_flow.svg new file mode 100644 index 0000000000..f39ee7ca5b --- /dev/null +++ b/docs/images/jaudio_seq_flow.svg @@ -0,0 +1 @@ +

BMS file

Sound ID

Resource ID

Plays notes

Wave ID

WSYS

Look up wave sample

IBNK

Look up instrument parameters

BST

Look up sound parameters

Start Sound Effect

Z2SoundSeqs.arc

BMS interpreter

\ No newline at end of file diff --git a/docs/images/jaudio_stream_flow.svg b/docs/images/jaudio_stream_flow.svg new file mode 100644 index 0000000000..47bee926eb --- /dev/null +++ b/docs/images/jaudio_stream_flow.svg @@ -0,0 +1 @@ +

Sound ID

File path

BST

Look up sound parameters

Start Sound Effect

Stream /AudioRes/Stream/xxx.ast from disc

\ No newline at end of file diff --git a/docs/imhex/jaudio/bsc.hexpat b/docs/imhex/jaudio/bsc.hexpat new file mode 100644 index 0000000000..a9f8e3ca6e --- /dev/null +++ b/docs/imhex/jaudio/bsc.hexpat @@ -0,0 +1,26 @@ +#pragma endian big + +#include "std/sys.pat" +#include "type/magic.pat" +#include "std/array.pat" + +/** + * Wrapper for a file-global pointer that allows it to be stored in an array. + */ +struct Offset { + T* offset : u32 [[inline]]; +}; + +struct JAUSeqCollectionTable { + u32 mEntryCount; + u32 mBmsOffset[mEntryCount]; +}; + +struct JAUSeqCollectionData { + type::Magic<"SC"> mMagic; + u16 mNumSoundCategories; + u32 mSectionSize; + Offset mTableOffsets[mNumSoundCategories]; +}; + +JAUSeqCollectionData data @ 0x0; diff --git a/docs/imhex/jaudio/bst.hexpat b/docs/imhex/jaudio/bst.hexpat new file mode 100644 index 0000000000..07bf28684e --- /dev/null +++ b/docs/imhex/jaudio/bst.hexpat @@ -0,0 +1,71 @@ +#pragma endian big + +#include "std/sys.pat" +#include "type/magic.pat" +#include "std/array.pat" + +/** + * Wrapper for a file-global pointer that allows it to be stored in an array. + */ +struct Offset { + T* offset : u32 [[inline]]; +}; + +struct ItemSoundEffect { + u8 mPriority; + u8 mVolume; + padding[2]; + u32 mSwBit; + float mPitch; +}; + +struct ItemSequence { + u8 mPriority; + u8 mVolume; + u16 mResourceId; +}; + +struct ItemStream { + u8 mPriority; + u8 mVolume; + u16 mStreamPanParameters; + char* mStreamFilePath[] : u32; +}; + +struct SoundTableGroupEntry { + u8 mTypeId; + if (mTypeId == 0x51) { + ItemSoundEffect* mOffset : u24 [[inline]]; + } else if (mTypeId == 0x60) { + ItemSequence* mOffset : u24 [[inline]]; + } else if (mTypeId == 0x70 || mTypeId == 0x71) { + ItemStream* mOffset : u24 [[inline]]; + } else { + u24 mOffset; + } +}; + +struct TGroup { + u32 mNumItems; + padding[4]; + SoundTableGroupEntry mEntries[mNumItems]; +}; + +struct TSection { + u32 mNumGroups; + Offset mGroupOffsets[mNumGroups]; +}; + +struct Root { + u32 mSectionNumber; + Offset mSectionOffsets[mSectionNumber]; +}; + +struct THeader { + type::Magic<"BST "> mMagic; + + padding[8]; + Root* mRoot : u32; +}; + +THeader header @ 0x0; \ No newline at end of file diff --git a/docs/imhex/jaudio/wsys.hexpat b/docs/imhex/jaudio/wsys.hexpat new file mode 100644 index 0000000000..aa131ea226 --- /dev/null +++ b/docs/imhex/jaudio/wsys.hexpat @@ -0,0 +1,228 @@ +#pragma endian big + +#include "std/sys.pat" +#include "type/magic.pat" +#include "std/array.pat" + +/* +WSYS file +The WSYS file contains roughly two sections: WBCT and WINF. +WBCT contains the mapping of Wave ID -> wave archives. +WINF are the wave archives themselves and their metadata. +*/ + +/** + * Wrapper for a file-global pointer that allows it to be stored in an array. + */ +struct Offset { + T* offset : u32 [[inline]]; +}; + +/** + * Format that audio data is in.. + */ +enum WaveFormat : u8 { + /** + * 16-samples-per-9-bytes custom Nintendo ADPCM. + */ + ADPCM4 = 0, + + /** + * 16-samples-per-5-bytes custom Nintendo ADPCM. + */ + ADPCM2 = 1, + + /** + * 8-bit-per-sample PCM. + */ + PCM8 = 2, + + /** + * 16-bit-per-sample PCM. + */ + PCM16 = 3, +}; + +/** + * Defines playback info for a single audio "wave". + * This data is stored per archive, making it duplicated (except for mAWOffsetStart) + */ +struct TWave { + padding[1]; // unknown + WaveFormat mWaveFormat; + + /** + * Key (as in like, the musical term) this sample is in. + */ + u8 mBaseKey; + padding[1]; + + /** + * Sample rate of the audio in Hz. + */ + float mSampleRate; + + /** + * Position where the sample data starts. + * This is in the .aw file for the archive containing this TWave. + */ + u32 mAWOffsetStart; + + /** + * Byte length of the sample data. + */ + u32 mAWLength; + + /** + * Indicates whether the sample should loop or not. All bits appear set if so. + */ + u32 mLoopFlags; + + /** + * Audio sample at which the loop starts. + */ + u32 mLoopStartSample; + + /** + * Audio sample at which the loop ends (and goes back to mLoopStartSample). + */ + u32 mLoopEndSample; + + /** + * Total sample count in this wave. + */ + u32 mSampleCount; + + /** + * Last sample for continuing ADPCM decode after loop. + */ + s16 mpLast; + + /** + * Penult sample for continuing ADPCM decode after loop. + */ + s16 mpPenult; +}; + +/** + * A single wave archive on disk. + * These are paired 1:1 with TCtrlScene objects. + */ +struct TWaveArchive { + /** + * Filename of the raw sample data on disc. Relative to /Audiores/Waves/ + */ + char mFileName[0x70]; + + /** + * Amount of waves in this archive. + * Matches the count in the paired TCtrl object. + */ + u32 mWaveCount; + + /** + * Offsets to the wave metadata (not sample data) in the WSYS. + * These are paired 1:1 to the TCtrlWaves, and the TCtrlWave contains the actual + * "Wave ID" used by the game for lookups. + */ + Offset waveOffsets[mWaveCount]; +}; + +/** + * Header containing data for wave archives and their metadata. + */ +struct TWaveArchiveBank { + type::Magic<"WINF"> mMagic; + + /** + * Amount of archives in this wave bank. + * Matches the value in TCtrlGroup. + */ + u32 mArchiveCounts; + Offset mArchiveOffsets[mArchiveCounts]; +}; + +/** + * Definition for a single wave in a control group. + */ +struct TCtrlWave { + /** + * Group ID matches the index of the control group this item is referenced by. + */ + u16 mGroupId; + + /** + * Wave ID used by the game to look this wave up. + */ + u16 mWaveId; +}; + +/** + * Contains the actual data for a TCtrlScene. + * Why is this separate? Who knows. + */ +struct TCtrl { + // Other versions of this struct with different magic (C-EX and C-ST) also exist in the file. + // They aren't pointed to so we don't need to worry about them. + type::Magic<"C-DF"> mMagic; + + /** + * Amount of waves in this group. + * Matches the value in TWaveArchive. + */ + u32 waveCount; + Offset mWaveOffsets[waveCount]; +}; + +/** + * A single scene or "group" of waves that are loaded at once. + */ +struct TCtrlScene { + type::Magic<"SCNE"> mMagic; + + padding[8]; // unknown + TCtrl* mCtrlOffset : u32 [[inline]]; +}; + +/** + * Contains the "control" section of the WSYS. + */ +struct TCtrlGroup { + type::Magic<"WBCT"> mMagic; + + padding[4]; // unknown + u32 mGroupCount; + + Offset mCtrlSceneOffsets[mGroupCount]; +}; + +struct THeader { + type::Magic<"WSYS"> mMagic; + + /** + * Size of WSYS in bytes. + */ + u32 mSize; + + /** + * ID of wave bank. + * This matches the value passed to the BAA load command. + * The game originally does not use this value itself, but Dusklight does rely on it. + */ + u32 mId; + + /** + * Total amount of waves in this wave bank. + * (not groups! Waves!) + */ + u32 mWaveTableSize; + + TWaveArchiveBank* archiveBankOffset : u32; + TCtrlGroup* ctrlGroupOffset : u32; +}; + +THeader header @ 0x0; + +std::assert( + header.archiveBankOffset.mArchiveCounts == header.ctrlGroupOffset.mGroupCount, + "Control group and archive count does not match!"); \ No newline at end of file diff --git a/docs/jaudio.md b/docs/jaudio.md new file mode 100644 index 0000000000..84a0d9dcfa --- /dev/null +++ b/docs/jaudio.md @@ -0,0 +1,241 @@ +# JAudio + +**JAudio** is the name for the audio engine used by Twilight Princess (along with many other Nintendo games from the era). TP uses exclusively JAudio v2, while other games use v1 or a mix of v1 and v2 infrastructure. This document will primarily focus on TP's use case, but best efforts will be made to document where behavior is TP-specific. + +## Common concepts + +Audio is almost exclusively + +### `JAISoundID` + +A "sound", be that a _sound effect_ or music, is referenced in-code through the `JAISoundID` type. This is a `u32` that the game uses to look up the relevant sound. The actual value is bitpacked (BE) from the following fields: + +| Type | Field | Description | +|-------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `u8` | Section ID | Section of the Sound Table this sound effect is located in. 0 for sound effects, 1 for sequenced music, 2 for streamed music.[^sectionids] | +| `u8` | Group ID | ID of group further used to organize inside the Sound Table's Section. Sound effects are grouped into things like "SYSTEM" and "ENEMY", other sections leave this at 0. | +| `u16` | "Wave ID" | Index inside the group to look up the sound at.
**Note** that the term "wave" in code is extremely confusing[^waveterm]; it does **not** refer to audio samples ("waves") in the wave banks directly. | + +[^sectionids]: JAudio itself can seemingly work outside this convention, however it is enforced by some TP-specific game code. + +[^waveterm]: Possibly vestigial from JAudio v1, where I believe there were less layers of indirection. + +## Disc files + +All audio data is stored in `/Audiores` on the disc. Files are as follows: + +### `/Audiores/Seqs/Z2SoundSeqs.arc` + +Contains the BMS instructions for all BMS-based music. Not all data is kept in memory at once. + +### `/Audiores/Stream/*.ast` + +Contains individual streamed music. Each file is a separate music track. See [this page](https://www.lumasworkshop.com/wiki/AST_(File_Format)) for file format description. + +### `/Audiores/Waves/*.aw` + +Contains audio sample data for sequenced music and sound effects. These files are pure meat, no bone: they are loaded directly into ARAM and all metadata is stored in the BAA WSYS sections. + +Each file contains audio samples for one "scene", with factors like the current level determining what "scenes" are made resident in memory. There is tons of duplicate data between scenes, presumably to increase simplicity and loading performance. + +### `/Audiores/Z2Sound.baa` + +Contains all remaining metadata for the audio system. This is effectively a container for a bunch of different sub-sections. The file starts with a bunch of "commands" that indicate where other data in the file is. Each command has a 4-character identifier and depending on the command will be followed by some extra arguments before the next command. + +The commands used by TP's BAA file are as follows (note that the decompiled code has support for more load commands, which are unused): + +| Command/Argument | Value | Description | +|------------------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| Command | `AA_<` | Start of BAA commands. Must be at the start of the file and only appear once. | +| Command | `>_AA` | End of BAA commands. | +| Command | `ws `[^spaces] | Wave bank/WSYS data. Defines where audio samples are located on disc. | +| Argument | u32 | Wave bank ID, max 255. In TP, 0 is sound effects, 1 is music samples. | +| Argument | u32 | File offset for start of `WSYS` data | +| Argument | u32 | Bit field selecting which groups (= `.aw` files) to load immediately. TP leaves this at zero.[^32ws] | +| Command | `bnk `[^spaces] | Instrument bank/IBNK | +| Argument | u32 | Target wave bank ID | +| Argument | u32 | File offset for start of `IBNK` data | +| Command | `bsc `[^spaces] | Sound effect sequence collection. | +| Argument | u32 | File offset for start of `SC` data. | +| Argument | u32 | File offset for end of `SC` data. | +| Command | `bst `[^spaces] | Sound table. Defines parameters for music and sound effects. | +| Argument | u32 | File offset for start of `BST `[^spaces] data. | +| Argument | u32 | File offset for end of `BST `[^spaces] data. | +| Command | `bstn` | Sound name table. Defines names of all music and sound effects. Present on disc, but loading is disabled on release versions of the game. | +| Argument | u32 | File offset for start of `BSTN` data. | +| Argument | u32 | File offset for end of `BSTN` data. | +| Command | `bfca` | Unknown, something related to initialization of DSP FX data. | +| Argument | u32 | File offset for start of `RARC` data. | + +[^spaces]: Padded with spaces. + +[^32ws]: Both of TP's wave banks have far more than 32 groups, and seemingly this mechanism would not be able to deal with that. + +Following is data descriptions for the remaining data in the `.BAA`, as pointed to by the above commands. + +### `WSYS` / Wave banks + +`WSYS` / Wave bank data defines where a set of audio samples can be found on disc. Each wave bank is made of multiple "groups", where each group corresponds to one `.aw` file on disc. Each group has a set of "wave IDs" it contains, along with the metadata (e.g. sample rate) and data offset in the `.aw` file. + +Multiple groups can contain the same wave ID, thus meaning the raw audio samples can be duplicated on disk. + +*Relevant classes: `JASWSParser`, `JASBasicWaveBank`, `JASSimpleWaveBank`.* For the actual binary layout of this data, check [the ImHex pattern](imhex/jaudio/wsys.hexpat) + +### `BST ` / Sound Table + +The `BST` / Sound table defines various parameters for music and sound effect playback. + +The layout is pretty simple: it's hierarchical with sections (sound effects, music sequences, streamed music)[^sectionids], +which have groups (only sound effects use this) and each group just has a flat list of items. +These indices match directly to the fields of the `JAISoundID`. + +*Relevant classes: `JAUSoundTable`.* For the actual binary layout of this data, check [the ImHex pattern](imhex/jaudio/bst.hexpat) + +Each item has a Type ID and a set of data. Types used by TP are as follows: + +#### `0x51` / sound effect + +Defines that this is a sound effect. The actual BMS code to execute (and unlike the other types, including its location) is looked up in the BSC. + +Layout: + +``` +u8 mPriority; // Priority relative to other sound effects. +u8 mVolume; // Converted to float: mVolume * (1.0/127.0) +padding[2]; +u32 mSwBit; // See below. +float mPitch; // Pitch multiplier +``` + +```cpp +// Values for mSwBit on sound effect items. (taken from JAUSoundTable.h) + +/** + * Sound is always calculated as max priority (0). + */ +#define SOUND_SW_ALWAYS_MAX_PRIORITY 0x0000'0001 + +/** + * Don't calculate volume by distance. + */ +#define SOUND_SW_IGNORE_DISTANCE_VOL 0x0000'0002 + +/** + * Don't calculate FX mix (reverb) by distance. + */ +#define SOUND_SW_IGNORE_FX_MIX 0x0000'0004 + +/** + * Mute all BGM sequences while this sound is playing. + */ +#define SOUND_SW_MUTE_BGM 0x0000'0008 + +/** + * Offset to shift to access @see SOUND_SW_RANDOM_PITCH_MASK + */ +#define SOUND_SW_RANDOM_PITCH_OFFSET 4 + +/** + * 4-bit value (0-15) to control the power of pitch randomization on sound playback. + * Code acts different for values above 8, not sure what the exact implication is. + */ +#define SOUND_SW_RANDOM_PITCH_MASK 0x0000'00F0 + +/** + * Offset to shift to access @see SOUND_SW_DOPPLER_POWER_MASK + */ +#define SOUND_SW_DOPPLER_POWER_OFFSET 8 + +/** + * 4-bit value (0-15) to scale the power of the Doppler effect for this sound. + */ +#define SOUND_SW_DOPPLER_POWER_MASK 0x0000'0F00 + +/** + * Don't calculate panning (left/right) values for this sound. + */ +#define SOUND_SW_IGNORE_PAN 0x0000'1000 + +/** + * Don't calculate Dolby (behind/front) values for this sound. + */ +#define SOUND_SW_IGNORE_DOLBY 0x0000'2000 + +/** + * Unsure. Relates to Z2 pooling of sound handles. + */ +#define SOUND_SW_POOL_FLAG_1 0x0000'4000 + +/** + * Unsure. Relates to Z2 pooling of sound handles. + */ +#define SOUND_SW_POOL_FLAG_2 0x0000'8000 + +/** + * 3-bit mask used to select a volume distance/falloff class for this sound. + */ +#define SOUND_SW_VOL_DIST_BIT_MASK 0x0007'0000 +#define SOUND_SW_VOL_DIST_BIT_OFFSET 16 + +/** + * Limit minimum volume of this sound (after distance falloff) to 0.2. + */ +#define SOUND_SW_CLAMP_MIN_VOLUME 0x0008'0000 + +/** + * 3-bit mask used to select a *different* volume distance/falloff class for this sound. + * @see SOUND_SW_VOL_DIST_BIT_MASK must be zero for this to work. + */ +#define SOUND_SW_VOL_DIST_BIT_2_MASK 0x0070'0000 +#define SOUND_SW_VOL_DIST_BIT_2_OFFSET 20 + +/** + * Mark sound as "far away" or "culled" when at max distance (selected by distance class). + * This affects a bunch of stuff like culling, automatic stopping, priorities, etc. + */ +#define SOUND_SW_CULL_AT_MAX_DISTANCE 0x0080'0000 + +/** + * Not sure what this does. + * Something causing volume/pan/dolby adjustment in Z2Audible::setOuterParams? + */ +#define SOUND_SW_VOL_SOMETHING_MASK 0x0F00'0000 + +/** + * Offset to shift to access @see SOUND_SW_RANDOM_VOLUME_MASK + */ +#define SOUND_SW_RANDOM_VOLUME_OFFSET 28 + +/** + * 4-bit value (0-15) to control the power of volume randomization on sound playback. + */ +#define SOUND_SW_RANDOM_VOLUME_MASK 0xF000'0000 +``` + +#### `0x60` / music sequence + +Defines background music that plays through the BMS system. + +``` +u8 mPriority; +u8 mVolume; // Converted to float: mVolume * (1.0/127.0) +u16 mResourceId; // ID to look up in Z2SoundSeqs.arc +``` + +#### `0x70` / `0x71` / music sequence + +Defines a streamed music track. Difference between `0x70` and `0x71` seems to only be that `0x71` stops automatically +on scene changes in TP's game code. + +``` +u8 mPriority; +u8 mVolume; // Converted to float: mVolume * (1.0/127.0) +u16 mStreamPanParameters; // Bitpacked, two bits per channel determining whether a channel is center (01), left (10), or right (11). +char* mStreamFilePath[] : u32; // File path to the .ast on disc. +``` + +## Further reading & credits + +* https://www.lumasworkshop.com/wiki/SMR.szs (note that details like the exact layout of the BAA are not the same as TP) +* XAYRGA for doing much RE work and making [JAMTools](https://xayr.gay/tools/SoundModdingToolkit/) +* The decompiled source code, duh. diff --git a/docs/modding.md b/docs/modding.md index 7d7fa142a0..8488440d07 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -852,8 +852,9 @@ per-window RCSS. A non-`MOD_OK` result from `build`/`update` fails your mod, as callback. **Controls:** `pane_add_control` adds an input row described by a `UiControlDesc`: `UI_CONTROL_BUTTON`, -`UI_CONTROL_GROUP`, `UI_CONTROL_TOGGLE`, `UI_CONTROL_NUMBER`, `UI_CONTROL_STRING`, `UI_CONTROL_SELECT`, or -`UI_CONTROL_COLOR`. Values bind with callbacks or directly to a config var. +`UI_CONTROL_GROUP`, `UI_CONTROL_TOGGLE`, `UI_CONTROL_NUMBER`, `UI_CONTROL_STRING`, `UI_CONTROL_SELECT`, +`UI_CONTROL_COLOR`, `UI_CONTROL_FILE_PICKER`, `UI_CONTROL_ICON_BUTTON`, or `UI_CONTROL_DROPDOWN`. Bind values with +callbacks or directly to a config var. ```cpp UiControlDesc control = UI_CONTROL_DESC_INIT; @@ -862,18 +863,32 @@ control.label = "Enable rainbows"; control.help_rml = "Shown in the help pane while focused."; control.binding = UI_BINDING_CONFIG_VAR; control.config_var = myBoolVar; // from svc_config->register_var -svc_ui->pane_add_control(mod_ctx, leftPane, &control, nullptr); +svc_ui->pane_add_control(mod_ctx, pane, &control, nullptr); + +UiRowDesc rowDesc = UI_ROW_DESC_INIT; +rowDesc.align = UI_ROW_ALIGN_CENTER; +UiElementHandle row = 0; +svc_ui->pane_add_row(mod_ctx, pane, &rowDesc, &row); + +UiControlDesc play = UI_CONTROL_DESC_INIT; +play.kind = UI_CONTROL_ICON_BUTTON; +play.icon = "play_arrow"; +play.label = "Play"; +play.on_pressed = play_track; +svc_ui->pane_add_control(mod_ctx, row, &play, nullptr); ``` `UI_BINDING_CONFIG_VAR` wires persistence, change notifications, and the modified indicator automatically. The var -type must match the control: `TOGGLE` = bool, `NUMBER` and `SELECT` = int, `STRING` and `COLOR` = string. Float vars -are not bindable; use callbacks and convert. `help_rml` and `SELECT` option lists render in a help pane, so `SELECT` -controls are only available inside window tabs. +type must match the control: `TOGGLE` = bool, `NUMBER`, `SELECT`, and `DROPDOWN` = int, `STRING`, `COLOR`, and +`FILE_PICKER` = string. Float vars are not bindable; use callbacks and convert. `help_rml` and `SELECT` option lists +render in a help pane, so `SELECT` controls are only available inside window tabs. `pane_add_group` adds a category button to a window tab's left pane. Focusing the button clears the paired right pane and calls the group's build callback with that pane, which is useful for organizing related controls without adding more tabs. +`pane_add_row` adds a horizontal container that other controls may be nested inside. + **Lists:** `pane_add_list` adds a scrollable virtualized list of items that can be efficiently updated and filtered. Keys must be unique and remain stable across replacements. @@ -1371,12 +1386,16 @@ HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the o Class member functions must include `Class*` as the first argument. -Two spellings work on every platform: +There are three ways to refer to a function by symbol: -- **Display names** (`daAlink_c::posMove`, `fapGm_Before`): the qualified name with no parameter list. They carry no - signature, so overload sets (and file-local statics sharing a name) return `MOD_CONFLICT`. -- **Decorated names** (`_ZN9daAlink_c7posMoveEv` / `?posMove@daAlink_c@@...`): the platform's mangled spelling in - dlsym convention (no Mach-O leading underscore). The escape hatch for overloads. +- **Display names** (`daAlink_c::posMove`, `fapGm_Before`): the qualified name with no parameter list. Since they have + no signature, overloads (and file-local statics sharing a name) return `MOD_CONFLICT`. +- **Decorated names** (`_ZN9daAlink_c7posMoveEv` / `?posMove@daAlink_c@@...`): the platform's mangled name, like you'd + pass to dlsym(). Useful for overloads, but must be specified separately for Windows (`#ifdef _MSVC_LANG`) and other + platforms. +- **Translation unit aliases** (`src/d/actor/d_a_b_gnd.cpp#action`): the source path relative to the repository root, + followed by `#` and then the function display name without parameters. This allows disambiguating static functions + with the same name across different source files. Installing fails with `MOD_UNAVAILABLE` when it didn't resolve (missing, ambiguous, or no symbol manifest). Unlike `DEFINE_HOOK`, the signature is **not** compiler-checked: a mismatched signature will corrupt the diff --git a/extern/aurora b/extern/aurora index 749d6ee7a2..7f2801cd01 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 749d6ee7a22bdfab78c8ece9047bca5d79aa72ca +Subproject commit 7f2801cd0133c9333eadb4e2e6b24100c328d328 diff --git a/extern/borealis b/extern/borealis index f55910bd79..0bdba6c50a 160000 --- a/extern/borealis +++ b/extern/borealis @@ -1 +1 @@ -Subproject commit f55910bd79248db250ebd9debb6624d78538c21c +Subproject commit 0bdba6c50a46409c4862474c72b4a3a631fbe0ec diff --git a/files.cmake b/files.cmake index bb5bebeaf1..ec1e6d7e12 100644 --- a/files.cmake +++ b/files.cmake @@ -1422,6 +1422,8 @@ set(DUSK_FILES src/dusk/achievements.cpp src/dusk/action_bindings.cpp src/dusk/action_bindings.h + src/dusk/archive.cpp + src/dusk/archive.hpp src/dusk/asserts.cpp src/dusk/autosave.cpp src/dusk/config.cpp @@ -1432,10 +1434,10 @@ set(DUSK_FILES src/dusk/dvd_asset.cpp src/dusk/dvd_asset.hpp src/dusk/extras.c - src/dusk/frame_interpolation.cpp src/dusk/commands.cpp src/dusk/commands.hpp src/dusk/game_clock.cpp + src/dusk/hash.hpp src/dusk/game_mode.cpp src/dusk/gamepad_color.cpp src/dusk/globals.cpp @@ -1462,6 +1464,13 @@ set(DUSK_FILES src/dusk/imgui/ImGuiStateShare.cpp src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStubLog.cpp + src/dusk/interp/camera.cpp + src/dusk/interp/frame_interpolation.cpp + src/dusk/interp/line.cpp + src/dusk/interp/material.cpp + src/dusk/interp/particle.cpp + src/dusk/interp/samples.cpp + src/dusk/interp/vertex.cpp src/dusk/io.cpp src/dusk/iso_validate.cpp src/dusk/language.cpp @@ -1478,10 +1487,23 @@ set(DUSK_FILES src/dusk/mods/loader/depgraph.hpp src/dusk/mods/loader/loader.cpp src/dusk/mods/loader/loader.hpp + src/dusk/mods/loader/manifest.cpp + src/dusk/mods/loader/manifest.hpp + src/dusk/mods/loader/natives.cpp + src/dusk/mods/loader/natives.hpp + src/dusk/mods/loader/packages.cpp + src/dusk/mods/loader/packages.hpp src/dusk/mods/loader/native_module.cpp src/dusk/mods/loader/native_module.hpp src/dusk/mods/loader/prepatch.cpp src/dusk/mods/loader/prepatch.hpp + src/dusk/mods/updates.cpp + src/dusk/mods/updates.hpp + src/dusk/mods/catalog.cpp + src/dusk/mods/catalog.hpp + src/dusk/mods/queue.cpp + src/dusk/mods/queue.hpp + src/dusk/mods/services.hpp src/dusk/mods/item.hpp src/dusk/mods/item_actor.cpp src/dusk/mods/item_checks.cpp @@ -1491,6 +1513,14 @@ set(DUSK_FILES src/dusk/mods/manifest.cpp src/dusk/mods/manifest.hpp src/dusk/mods/svc/actor.cpp + src/dusk/mods/svc/audio_res/audio_res.hpp + src/dusk/mods/svc/audio_res/audio_res.cpp + src/dusk/mods/svc/audio_res/bst.cpp + src/dusk/mods/svc/audio_res/bst.hpp + src/dusk/mods/svc/audio_res/wsys.cpp + src/dusk/mods/svc/audio_res/wsys.hpp + src/dusk/mods/svc/audio_res/wave.cpp + src/dusk/mods/svc/audio_res/opus.cpp src/dusk/mods/svc/camera.cpp src/dusk/mods/svc/config.cpp src/dusk/mods/svc/config.hpp @@ -1506,6 +1536,8 @@ set(DUSK_FILES src/dusk/mods/svc/websocket.cpp src/dusk/mods/svc/item.cpp src/dusk/mods/svc/item.hpp + src/dusk/mods/svc/id_allocator.cpp + src/dusk/mods/svc/id_allocator.hpp src/dusk/mods/svc/log.cpp src/dusk/mods/svc/overlay.cpp src/dusk/mods/svc/registry.cpp @@ -1539,6 +1571,8 @@ set(DUSK_FILES src/dusk/ui/bool_button.hpp src/dusk/ui/button.cpp src/dusk/ui/button.hpp + src/dusk/ui/clamped_text.cpp + src/dusk/ui/clamped_text.hpp src/dusk/ui/color_input.cpp src/dusk/ui/color_input.hpp src/dusk/ui/component.cpp @@ -1548,6 +1582,10 @@ set(DUSK_FILES src/dusk/ui/controls.hpp src/dusk/ui/document.cpp src/dusk/ui/document.hpp + src/dusk/ui/drop_install_modal.cpp + src/dusk/ui/drop_install_modal.hpp + src/dusk/ui/dropdown_button.cpp + src/dusk/ui/dropdown_button.hpp src/dusk/ui/editor.cpp src/dusk/ui/editor.hpp src/dusk/ui/event.cpp @@ -1568,21 +1606,41 @@ set(DUSK_FILES src/dusk/ui/list.hpp src/dusk/ui/menu_bar.cpp src/dusk/ui/menu_bar.hpp + src/dusk/ui/mod_browser.cpp + src/dusk/ui/mod_browser.hpp + src/dusk/ui/online_mods.cpp + src/dusk/ui/online_mods.hpp + src/dusk/ui/package_row.cpp + src/dusk/ui/package_row.hpp src/dusk/ui/mod_texture_provider.cpp src/dusk/ui/mod_texture_provider.hpp + src/dusk/ui/remote_texture_provider.cpp + src/dusk/ui/remote_texture_provider.hpp + src/dusk/ui/runtime_image.cpp + src/dusk/ui/runtime_image.hpp src/dusk/ui/mod_window.cpp src/dusk/ui/mod_window.hpp src/dusk/ui/modal.cpp src/dusk/ui/modal.hpp + src/dusk/ui/mod_updates.cpp + src/dusk/ui/mod_updates.hpp src/dusk/ui/mods_window.cpp src/dusk/ui/mods_window.hpp src/dusk/ui/nav_types.hpp src/dusk/ui/nav_group.cpp src/dusk/ui/nav_group.hpp + src/dusk/ui/context_menu.cpp + src/dusk/ui/context_menu.hpp + src/dusk/ui/icon_button.cpp + src/dusk/ui/icon_button.hpp + src/dusk/ui/tooltip.cpp + src/dusk/ui/tooltip.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/row.cpp + src/dusk/ui/row.hpp src/dusk/ui/pane.cpp src/dusk/ui/pane.hpp src/dusk/ui/popover.cpp @@ -1616,7 +1674,7 @@ set(DUSK_FILES src/dusk/version.cpp src/dusk/utilities.cpp src/helpers/batch.cpp - src/helpers/endian.cpp src/helpers/offset_ptr.cpp src/helpers/string.cpp + src/helpers/cast.cpp ) diff --git a/include/Z2AudioLib/Z2Audience.h b/include/Z2AudioLib/Z2Audience.h index e6d2f7da3a..2f2dc359f9 100644 --- a/include/Z2AudioLib/Z2Audience.h +++ b/include/Z2AudioLib/Z2Audience.h @@ -9,6 +9,8 @@ #include "JSystem/JAudio2/JAUAudibleParam.h" #include "JSystem/TPosition3.h" +#define Z2_AUDIO_PLAYERS 1 + struct Z2Audible; struct Z2AudibleAbsPos { @@ -49,10 +51,10 @@ struct Z2AudioCamera { f32 getCamDist() const { return mCamDist; } - /* 0x00 */ JGeometry::TPosition3f32 field_0x0; + /* 0x00 */ JGeometry::TPosition3f32 mViewMatrix; /* 0x30 */ JGeometry::TVec3 mVel; /* 0x3C */ JGeometry::TVec3 mPos; - /* 0x48 */ JGeometry::TVec3 field_0x48; + /* 0x48 */ JGeometry::TVec3 mLastPos; /* 0x54 */ f32 mFovySin; /* 0x58 */ f32 mVolCenterZ; /* 0x5C */ f32 mTargetVolume; @@ -83,14 +85,14 @@ struct Z2SpotMic { /* 0x04 */ f32 field_0x4; /* 0x08 */ f32 field_0x8; /* 0x0C */ f32 field_0xc; - /* 0x10 */ Z2AudioCamera* field_0x10[1]; + /* 0x10 */ Z2AudioCamera* field_0x10[Z2_AUDIO_PLAYERS]; /* 0x14 */ Vec* mPosPtr; - /* 0x18 */ f32 field_0x18[1]; + /* 0x18 */ f32 field_0x18[Z2_AUDIO_PLAYERS]; /* 0x1C */ f32 field_0x1c; - /* 0x20 */ f32 field_0x20[1]; + /* 0x20 */ f32 field_0x20[Z2_AUDIO_PLAYERS]; /* 0x24 */ bool mIgnoreIfOut; /* 0x25 */ bool mMicOn; - /* 0x26 */ bool field_0x26[1]; + /* 0x26 */ bool field_0x26[Z2_AUDIO_PLAYERS]; }; // Size: 0x28 struct Z2Audience3DSetting { @@ -102,62 +104,99 @@ struct Z2Audience3DSetting { void updateDolbyDist(f32, f32); void calcVolumeFactorAll() { - field_0x0[1] = 1.25f * field_0x0[0]; - field_0x0[2] = 1.5f * field_0x0[0]; - field_0x0[3] = 2.0f * field_0x0[0]; - field_0x0[4] = 3.0f * field_0x0[0]; - field_0x0[5] = 4.0f * field_0x0[0]; - field_0x0[6] = 6.0f * field_0x0[0]; - field_0x0[7] = 8.0f * field_0x0[0]; - field_0x0[8] = 0.9f * field_0x0[0]; - field_0x0[9] = 0.8f * field_0x0[0]; - field_0x0[10] = 0.7f * field_0x0[0]; - field_0x0[11] = 0.6f * field_0x0[0]; - field_0x0[12] = 0.5f * field_0x0[0]; - field_0x0[13] = 0.4f * field_0x0[0]; - field_0x0[14] = 0.3f * field_0x0[0]; + mDistanceMaxes[1] = 1.25f * mDistanceMaxes[0]; + mDistanceMaxes[2] = 1.5f * mDistanceMaxes[0]; + mDistanceMaxes[3] = 2.0f * mDistanceMaxes[0]; + mDistanceMaxes[4] = 3.0f * mDistanceMaxes[0]; + mDistanceMaxes[5] = 4.0f * mDistanceMaxes[0]; + mDistanceMaxes[6] = 6.0f * mDistanceMaxes[0]; + mDistanceMaxes[7] = 8.0f * mDistanceMaxes[0]; + mDistanceMaxes[8] = 0.9f * mDistanceMaxes[0]; + mDistanceMaxes[9] = 0.8f * mDistanceMaxes[0]; + mDistanceMaxes[10] = 0.7f * mDistanceMaxes[0]; + mDistanceMaxes[11] = 0.6f * mDistanceMaxes[0]; + mDistanceMaxes[12] = 0.5f * mDistanceMaxes[0]; + mDistanceMaxes[13] = 0.4f * mDistanceMaxes[0]; + mDistanceMaxes[14] = 0.3f * mDistanceMaxes[0]; for (int i = 0; i < 15; i++) { - field_0x70[i] = (field_0x40 - 1.0f) / (field_0x0[i] - field_0x3c); + mVolumeFactor[i] = (mMinDistanceVolume - 1.0f) / (mDistanceMaxes[i] - mMaxVolumeDistance); } } void calcPriorityFactorAll() { for (int i = 0; i < 15; i++) { - field_0xac[i] = field_0x64 / (field_0x0[i] - field_0x3c); + mPriorityFactor[i] = mMaxDistancePriority / (mDistanceMaxes[i] - mMaxVolumeDistance); } } void calcFxMixFactorAll() { for (int i = 0; i < 15; i++) { - field_0xe8[i] = (field_0x54 - field_0x50) / (field_0x0[i] - field_0x3c); + mFxMixFactor[i] = (mMaxDistanceFxMix - mMinDistanceFxMix) / (mDistanceMaxes[i] - mMaxVolumeDistance); } } - /* 0x000 */ f32 field_0x0[15]; - /* 0x03C */ f32 field_0x3c; - /* 0x040 */ f32 field_0x40; - /* 0x044 */ f32 field_0x44; - /* 0x048 */ f32 field_0x48; - /* 0x04C */ f32 field_0x4c; - /* 0x050 */ f32 field_0x50; - /* 0x054 */ f32 field_0x54; - /* 0x058 */ f32 field_0x58; - /* 0x05C */ f32 field_0x5c; + /** + * Maximum distance a sound can reach before being "far away" + * Being far away affects stuff like culling, lowering its priority, forcibly stopping it, etc. + * Sounds select which entry they use based on their VolBits. + */ + /* 0x000 */ f32 mDistanceMaxes[15]; + + /** + * Distance at which the max volume of a sound is reached. + * i.e. sounds do *not* get louder if they get closer than this. + */ + /* 0x03C */ f32 mMaxVolumeDistance; + + /** + * FX Mix value at maximum distance (@ref mDistanceMaxes) + */ + /* 0x040 */ f32 mMinDistanceVolume; + /* 0x044 */ f32 mDolbyFrontDistanceMax; + /* 0x048 */ f32 mDolbyBehindDistanceMax; + /* 0x04C */ f32 mDolbyCenterValue; + + /** + * FX Mix value at minimum distance (@ref mMaxVolumeDistance) + */ + /* 0x050 */ f32 mMinDistanceFxMix; + + /** + * FX Mix value at maximum distance (@ref mDistanceMaxes) + */ + /* 0x054 */ f32 mMaxDistanceFxMix; + /* 0x058 */ f32 mPanFactor; + /* 0x05C */ f32 mSonicSpeed; // Used for doppler effect calculations. /* 0x060 */ f32 field_0x60; - /* 0x064 */ u32 field_0x64; + + /** + * Priority that sounds receive when "far away". + * @see mDistanceMaxes + */ + /* 0x064 */ u32 mMaxDistancePriority; /* 0x068 */ f32 field_0x68; /* 0x06C */ f32 field_0x6c; - /* 0x070 */ f32 field_0x70[15]; - /* 0x0AC */ f32 field_0xac[15]; - /* 0x0E8 */ f32 field_0xe8[15]; + /* 0x070 */ f32 mVolumeFactor[15]; + /* 0x0AC */ f32 mPriorityFactor[15]; + /* 0x0E8 */ f32 mFxMixFactor[15]; /* 0x124 */ bool mVolumeDistInit; /* 0x125 */ bool mDolbyDistInit; }; // Size: 0x128 struct Z2AudibleRelPos { - /* 0x00 */ JGeometry::TVec3 field_0x00; - /* 0x0C */ f32 field_0xC; - /* 0x10 */ f32 field_0x10; + /* 0x00 */ JGeometry::TVec3 mCameraRelative; + + /** + * Distance from mCameraRelative. This is from the object root and not the + * exact distance used for volume/priority calculations. + */ + /* 0x0C */ f32 mTrueDistance; + + /** + * Distance from mCameraRelative but offset by mVolCenterZ. + * This presumably means the distance is more centered on the object than mTrueDistance. + */ + /* 0x10 */ f32 mCenterDistance; }; struct Z2AudibleChannel { @@ -171,7 +210,7 @@ struct Z2AudibleChannel { } /* 0x00 */ JASSoundParams mParams; - /* 0x14 */ Z2AudibleRelPos field_0x14; + /* 0x14 */ Z2AudibleRelPos mRelPos; /* 0x28 */ f32 field_0x28; /* 0x2c */ f32 mPan; /* 0x30 */ f32 mDolby; @@ -200,8 +239,8 @@ struct Z2Audible : public JAIAudible, public JASPoolAllocObject { /* 0x10 */ JAUAudibleParam mParam; /* 0x14 */ Z2AudibleAbsPos mAbsPos; - /* 0x2C */ Z2AudibleChannel mChannel[1]; - /* 0x64 */ f32 field_0x64[1]; + /* 0x2C */ Z2AudibleChannel mChannel[Z2_AUDIO_PLAYERS]; + /* 0x64 */ f32 mMicDistances[Z2_AUDIO_PLAYERS]; }; struct Z2Audience : public JAIAudience, public JASGlobalInstance { @@ -223,7 +262,8 @@ struct Z2Audience : public JAIAudience, public JASGlobalInstance { virtual ~Z2Audience(); virtual JAIAudible* newAudible(const JGeometry::TVec3& pos, JAISoundID soundID, - const JGeometry::TVec3*, u32); + const JGeometry::TVec3*, u32 + IF_DUSK_ARG(dusk::mods::svc::audio_res::bst::SoundTableReplacementSlot const*)); virtual int getMaxChannels(); virtual void deleteAudible(JAIAudible* audible); virtual u32 calcPriority(JAIAudible* audible); @@ -237,7 +277,7 @@ struct Z2Audience : public JAIAudience, public JASGlobalInstance { } Z2Audience3DSetting* getSetting() { return &mSetting; } - const Z2AudioCamera* getAudioCamera(int camID) const { return &mAudioCamera[camID]; } + const Z2AudioCamera* getAudioCamera(int camID) const { return &mAudioCamera[camID]; } void setUsingOffMicVol(bool value) { mUsingOffMicVol = value; } diff --git a/include/Z2AudioLib/Z2SoundInfo.h b/include/Z2AudioLib/Z2SoundInfo.h index e2fae177a3..404e0ad5df 100644 --- a/include/Z2AudioLib/Z2SoundInfo.h +++ b/include/Z2AudioLib/Z2SoundInfo.h @@ -6,23 +6,29 @@ #include "JSystem/JAudio2/JAUSoundInfo.h" #include "JSystem/JAudio2/JAUSoundTable.h" +#define STRM_CH_SHIFT_ 2 + +#define STRM_CH_CENTER 1 +#define STRM_CH_LEFT 2 +#define STRM_CH_RIGHT 3 + class Z2SoundInfo : public JAISoundInfo, public JAUSoundInfo, public JAIStreamDataMgr, public JASGlobalInstance { public: Z2SoundInfo() : JAISoundInfo(true), JAUSoundInfo(true), JASGlobalInstance(true) {} - virtual u16 getAudibleSw(JAISoundID soundID) const; + virtual u16 getAudibleSw(JAISoundID soundID IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) const; virtual u16 getBgmSeqResourceID(JAISoundID soundID) const; - virtual s32 getStreamFileEntry(JAISoundID soundID); + virtual s32 getStreamFileEntry(JAISoundID soundID IF_DUSK_ARG(StreamReplacementSlot const* replacement)); virtual int getSoundType(JAISoundID soundID) const; virtual int getCategory(JAISoundID soundID) const; - virtual u32 getPriority(JAISoundID soundID) const; - virtual void getSeInfo(JAISoundID soundID, JAISe* sePtr) const; + virtual u32 getPriority(JAISoundID soundID IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) const; + virtual void getSeInfo(JAISoundID soundID, JAISe* sePtr IF_DUSK_ARG(SoundEffectReplacementSlot const* replacement)) const; virtual void getSeqInfo(JAISoundID soundID, JAISeq* seqPtr) const; - virtual void getStreamInfo(JAISoundID soundID, JAIStream* streamPtr) const; + virtual void getStreamInfo(JAISoundID soundID, JAIStream* streamPtr IF_DUSK_ARG(StreamReplacementSlot const* replacement)) const; virtual ~Z2SoundInfo() {} - JAUAudibleParam getAudibleSwFull(JAISoundID soundID); - const char* getStreamFilePath(JAISoundID soundID); - int getSwBit(JAISoundID soundID) const; + JAUAudibleParam getAudibleSwFull(JAISoundID soundID IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)); + const char* getStreamFilePath(JAISoundID soundID IF_DUSK_ARG(StreamReplacementSlot const* replacement)); + int getSwBit(JAISoundID soundID IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) const; void getSoundInfo_(JAISoundID soundID, JAISound* soundPtr) const; BOOL isValid() const { diff --git a/include/Z2AudioLib/Z2SoundMgr.h b/include/Z2AudioLib/Z2SoundMgr.h index eabf9d2614..24a6762e3c 100644 --- a/include/Z2AudioLib/Z2SoundMgr.h +++ b/include/Z2AudioLib/Z2SoundMgr.h @@ -41,7 +41,7 @@ public: /* 0x004 */ JAISeMgr seMgr_; /* 0x728 */ JAISeqMgr seqMgr_; /* 0x79C */ JAIStreamMgr streamMgr_; - /* 0x80C */ JAISoundID soundID_; + /* 0x80C */ JAISoundID bgmMuter; }; // Size: 0x810 #if VERSION != VERSION_SHIELD_DEBUG diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 75ac1aac2b..4d459d4597 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -124,7 +124,11 @@ public: BOOL create(); void update(); virtual void draw(); +#if TARGET_PC + virtual ~daAlink_lockCursor_c(); +#else virtual ~daAlink_lockCursor_c() {} +#endif void initFrame() { field_0x4 = 0; @@ -4556,31 +4560,7 @@ public: bool checkAimContext(); bool checkAimInputContext(); - 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; - - cXyz mHsChainInterpPrevTop; - cXyz mHsChainInterpCurrTop; - cXyz mHsChainInterpPrevRoot; - cXyz mHsChainInterpCurrRoot; - cXyz mHsChainInterpPrevSubRoot; - cXyz mHsChainInterpCurrSubRoot; - cXyz mHsChainInterpPrevSubTop; - cXyz mHsChainInterpCurrSubTop; - bool mHsChainInterpPrevValid; - bool mHsChainInterpCurrValid; - bool mIsRollstab = false; - void* mAnmBuffers[3] = {}; #endif }; // Size: 0x385C diff --git a/include/d/actor/d_a_b_gm.h b/include/d/actor/d_a_b_gm.h index f153541772..f14438da6e 100644 --- a/include/d/actor/d_a_b_gm.h +++ b/include/d/actor/d_a_b_gm.h @@ -110,6 +110,9 @@ public: /* 0x1CFC */ int field_0x1cfc; /* 0x1D00 */ f32 mKankyoBlend; /* 0x1D04 */ u8 mInitHIO; +#if TARGET_PC + s8 mDemoCamSyncTicks; +#endif }; STATIC_ASSERT(sizeof(b_gm_class) == 0x1d08); diff --git a/include/d/actor/d_a_b_gnd.h b/include/d/actor/d_a_b_gnd.h index 0827102eec..dcf6c94905 100644 --- a/include/d/actor/d_a_b_gnd.h +++ b/include/d/actor/d_a_b_gnd.h @@ -189,12 +189,6 @@ public: /* 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 }; diff --git a/include/d/actor/d_a_balloon_2D.h b/include/d/actor/d_a_balloon_2D.h index 80aa3e3b02..04d450665c 100644 --- a/include/d/actor/d_a_balloon_2D.h +++ b/include/d/actor/d_a_balloon_2D.h @@ -72,7 +72,7 @@ public: struct ScoreCount { cXyz field_0x0; u16 field_0xc; - u8 field_0xe; + DUSK_IF_ELSE(f32, u8) field_0xe; u8 field_0xf; }; diff --git a/include/d/actor/d_a_boomerang.h b/include/d/actor/d_a_boomerang.h index 2b527afe45..5c4f6f8ede 100644 --- a/include/d/actor/d_a_boomerang.h +++ b/include/d/actor/d_a_boomerang.h @@ -17,7 +17,11 @@ public: void setSight(cXyz const*, int); virtual void draw(); +#if TARGET_PC + virtual ~daBoomerang_sight_c(); +#else virtual ~daBoomerang_sight_c() {} +#endif u8 getReserve() const { return mReserve; } u8 getAlpha(int i_index) { return m_alpha[i_index]; } diff --git a/include/d/actor/d_a_e_db.h b/include/d/actor/d_a_e_db.h index 4f10715443..a95722a6d1 100644 --- a/include/d/actor/d_a_e_db.h +++ b/include/d/actor/d_a_e_db.h @@ -80,12 +80,6 @@ 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 3069bcd325..a7e0241007 100644 --- a/include/d/actor/d_a_e_hb.h +++ b/include/d/actor/d_a_e_hb.h @@ -73,12 +73,6 @@ 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 527cf30eac..f36c744879 100644 --- a/include/d/actor/d_a_e_mb.h +++ b/include/d/actor/d_a_e_mb.h @@ -44,12 +44,6 @@ 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 5cbae84696..a631ba47ac 100644 --- a/include/d/actor/d_a_e_s1.h +++ b/include/d/actor/d_a_e_s1.h @@ -81,15 +81,6 @@ 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_wb.h b/include/d/actor/d_a_e_wb.h index aecbac96a1..770e537f2e 100644 --- a/include/d/actor/d_a_e_wb.h +++ b/include/d/actor/d_a_e_wb.h @@ -221,12 +221,6 @@ public: /* 0x17E4 */ u8 field_0x17e4[0x17e8 - 0x17e4]; /* 0x17E8 */ f32 ride_speed_max; ///< @brief Speed rate for riding calculations. #if TARGET_PC - cXyz himo_mat_interp_prev[2][16]; - cXyz himo_mat_interp_curr[2][16]; - cXyz himo_tex_interp_prev[2]; - cXyz himo_tex_interp_curr[2]; - bool himo_interp_prev_valid; - bool himo_interp_curr_valid; s8 demo_cam_sync_ticks; #endif }; diff --git a/include/d/actor/d_a_e_yd.h b/include/d/actor/d_a_e_yd.h index 44d6036600..188e435ba5 100644 --- a/include/d/actor/d_a_e_yd.h +++ b/include/d/actor/d_a_e_yd.h @@ -74,12 +74,6 @@ 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 6be19933cb..69a85430a8 100644 --- a/include/d/actor/d_a_e_yg.h +++ b/include/d/actor/d_a_e_yg.h @@ -63,15 +63,6 @@ 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 63e8ac1882..519e5e2779 100644 --- a/include/d/actor/d_a_e_yh.h +++ b/include/d/actor/d_a_e_yh.h @@ -77,12 +77,6 @@ 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_horse.h b/include/d/actor/d_a_horse.h index bb7ce7ccb1..2074570517 100644 --- a/include/d/actor/d_a_horse.h +++ b/include/d/actor/d_a_horse.h @@ -196,9 +196,6 @@ public: void copyReinPos(); void setReinPosHandSubstance(int); void setReinPosNormalSubstance(); -#if TARGET_PC - void lerpControlPoints(f32 alpha); -#endif void bgCheck(); bool checkSpecialWallHitSubstance(cXyz const&) const; void setServiceWaitTimer(); diff --git a/include/d/actor/d_a_mg_rod.h b/include/d/actor/d_a_mg_rod.h index 97a0bb1c72..76863f33d5 100644 --- a/include/d/actor/d_a_mg_rod.h +++ b/include/d/actor/d_a_mg_rod.h @@ -299,13 +299,6 @@ public: /* 0x168C */ u8 field_0x168c; /* 0x168D */ u8 field_0x168d; /* 0x168E */ u8 HIOInit; - -#if TARGET_PC - cXyz mLineInterpPrev[MG_ROD_LURE_LINE_LEN]; - cXyz mLineInterpCurr[MG_ROD_LURE_LINE_LEN]; - bool mLineInterpPrevValid; - bool mLineInterpCurrValid; -#endif }; #endif /* D_A_MG_ROD_H */ diff --git a/include/d/actor/d_a_obj_fchain.h b/include/d/actor/d_a_obj_fchain.h index 80e96f097b..e9250fbe24 100644 --- a/include/d/actor/d_a_obj_fchain.h +++ b/include/d/actor/d_a_obj_fchain.h @@ -31,10 +31,6 @@ public: csXyz* getAngle() { return field_0x8a4; } J3DModelData* getModelData() { return mModelData; } -#if TARGET_PC - void onInterpCallback(); -#endif - /* 0x568 */ request_of_phase_process_class mPhase; /* 0x570 */ J3DModelData* mModelData; /* 0x574 */ daObjFchain_shape_c mShape; @@ -45,14 +41,6 @@ public: /* 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_keyhole.h b/include/d/actor/d_a_obj_keyhole.h index b8a813776d..fd39641ad8 100644 --- a/include/d/actor/d_a_obj_keyhole.h +++ b/include/d/actor/d_a_obj_keyhole.h @@ -66,18 +66,9 @@ public: /* 0x2CA7 */ s8 hide_lock; /* 0x2CA8 */ cXyz field_0x2ca8; /* 0x2CB4 */ u8 field_0x2cb4; - -#if TARGET_PC - Mtx mChainInterpPrev[6][16]; - Mtx mChainInterpCurr[6][16]; - bool mChainInterpPrevValid; - bool mChainInterpCurrValid; -#endif }; -#if !TARGET_PC STATIC_ASSERT(sizeof(obj_keyhole_class) == 0x2CB8); -#endif class daObj_Keyhole_HIO_c : public JORReflexible { public: diff --git a/include/d/actor/d_a_obj_klift00.h b/include/d/actor/d_a_obj_klift00.h index d87c2d4459..fb4c05fde0 100644 --- a/include/d/actor/d_a_obj_klift00.h +++ b/include/d/actor/d_a_obj_klift00.h @@ -17,7 +17,7 @@ class daObjKLift00_c : public dBgS_MoveBgActor, public request_of_phase_process_class { public: cPhs_Step create1st(); - void setMtx(); + void setMtx(IF_DUSK(bool presentation = false)); void rideActor(fopAc_ac_c*); int CreateHeap(); int Create(); @@ -25,10 +25,6 @@ public: int Draw(); int Delete(); -#if TARGET_PC - void onInterpCallback(); -#endif - enum Param_e { LOCK_e = (1 << 6), NO_BASE_DISP = (1 << 7) }; @@ -53,13 +49,6 @@ public: /* 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 5c2ab58219..1fa4d4b7f6 100644 --- a/include/d/actor/d_a_obj_lv8Lift.h +++ b/include/d/actor/d_a_obj_lv8Lift.h @@ -58,9 +58,6 @@ 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/actor/d_a_player.h b/include/d/actor/d_a_player.h index ff8a165ef8..6eb4757c18 100644 --- a/include/d/actor/d_a_player.h +++ b/include/d/actor/d_a_player.h @@ -33,9 +33,13 @@ class daPy_sightPacket_c : public dDlst_base_c { public: daPy_sightPacket_c() {} virtual void draw(); +#if TARGET_PC + virtual ~daPy_sightPacket_c(); +#else virtual ~daPy_sightPacket_c() {} +#endif - void setSight(); + void setSight(IF_DUSK(bool registerPacket = true)); void setSightImage(ResTIMG* i_img); u8 getDrawFlg() { return mDrawFlag; } @@ -89,14 +93,6 @@ public: #define PLAYER_CREATE_ANM_HEAP(heap, type, name) (heap).createHeap(type) #endif -#if TARGET_PC -inline u32 daPy_getAnmResourceSize(u16 i_resId, u32 i_minSize) { - JKRArchive* archive = dComIfGp_getAnmArchive(); - u32 size = archive->getFileSize(archive->findIdxResource(i_resId)); - return size > i_minSize ? size : i_minSize; -} -#endif - class daPy_anmHeap_c { public: enum daAlinkHEAP_TYPE { @@ -113,9 +109,6 @@ public: void* mallocBuffer(); #if TARGET_PC void createHeap(daAlinkHEAP_TYPE i_heapType, const char* name); - void reserveBuffer(u16 i_resId); - void* allocTempBuffer(u16 i_resId, u32* io_size); - void freeTempBuffers(); #else void createHeap(daAlinkHEAP_TYPE i_heapType); #endif @@ -145,10 +138,6 @@ public: /* 0x08 */ u32 mBufferSize; /* 0x0C */ u8* mBuffer; /* 0x10 */ JKRSolidHeap* mAnimeHeap; -#if TARGET_PC - u8* mOwnedBuffer = NULL; - void** mTempBuffers = NULL; -#endif }; // Size = 0x14 class daPy_actorKeep_c { diff --git a/include/d/actor/d_a_title.h b/include/d/actor/d_a_title.h index 691e2df3e3..f6e70b4276 100644 --- a/include/d/actor/d_a_title.h +++ b/include/d/actor/d_a_title.h @@ -68,7 +68,7 @@ public: /* 0x5FB */ u8 mProcID; /* 0x5FC */ u8 mWaitTimer; /* 0x600 */ CPaneMgrAlpha* field_0x600; - /* 0x604 */ u8 field_0x604; + /* 0x604 */ DUSK_IF_ELSE(f32, u8) field_0x604; }; // Size: 0x608 typedef void (daTitle_c::*procFunc)(); diff --git a/include/d/d_drawlist.h b/include/d/d_drawlist.h index f792f544cc..4baf6ed324 100644 --- a/include/d/d_drawlist.h +++ b/include/d/d_drawlist.h @@ -441,7 +441,7 @@ public: } #if TARGET_PC - void refresh3DlineMats(const cXyz& eye); + void refresh3DlineMats(); #endif void peekZdata() { mPeekZ.peekData(); } diff --git a/include/d/d_file_sel_warning.h b/include/d/d_file_sel_warning.h index c8740b5f74..b4a3790688 100644 --- a/include/d/d_file_sel_warning.h +++ b/include/d/d_file_sel_warning.h @@ -35,6 +35,9 @@ public: void drawSelf(); void setText(u32); void setFontColor(JUtility::TColor, JUtility::TColor); +#if TARGET_PC + void presentAnims(); +#endif virtual ~dFile_warning_c(); @@ -47,7 +50,7 @@ public: /* 0x1C */ CPaneMgr* mpRootPane; /* 0x20 */ J2DTextBox* field_0x20; /* 0x24 */ J2DAnmTransform* field_0x24; - /* 0x28 */ int field_0x28; + /* 0x28 */ DUSK_IF_ELSE(f32, int) field_0x28; /* 0x2C */ int field_0x2c; /* 0x30 */ u8 field_0x30[4]; /* 0x34 */ f32 field_0x34; diff --git a/include/d/d_file_select.h b/include/d/d_file_select.h index dd4d5a3aae..c68f68db5d 100644 --- a/include/d/d_file_select.h +++ b/include/d/d_file_select.h @@ -385,7 +385,7 @@ public: void menuCursorShow(); void yesnoWakuAlpahAnmInit(u8, u8, u8, u8); bool yesnoWakuAlpahAnm(u8); - #if TARGET_PC +#if TARGET_PC void fileSelectWide(); bool pointerDataSelect(); bool pointerMenuSelect(); @@ -398,7 +398,10 @@ public: modoruTxtDispAnmInit(0); mDataSelProc = DATASELPROC_NAME_TO_DATA_SELECT_MOVE; } - #endif + void presentLayoutAnims(); + void presentAnims(); + void presentNameInputFade(); +#endif void _draw(); void errorMoveAnmInitSet(int, int); bool errorMoveAnm(); @@ -523,7 +526,7 @@ public: /* 0x00A0 */ s32 field_0x00a0; /* 0x00A4 */ CPaneMgr* mBaseMovePane; /* 0x00A8 */ u8 field_0x00a8[0xb0-0xa8]; - /* 0x00B0 */ s32 mBaseMoveAnmFrame; + /* 0x00B0 */ DUSK_IF_ELSE(f32, s32) mBaseMoveAnmFrame; /* 0x00B4 */ s32 mBaseMoveAnmFrameMax; /* 0x00B8 */ u8 field_0x00b8; /* 0x00B9 */ u8 field_0x00b9; @@ -532,26 +535,26 @@ public: /* 0x00BC */ CPaneMgr* mSelFilePanes[3]; /* 0x00C8 */ f32 field_0x00c8[3]; /* 0x00D4 */ f32 field_0x00d4[3]; - /* 0x00E0 */ s32 field_0x00e0[3]; + /* 0x00E0 */ DUSK_IF_ELSE(f32, s32) field_0x00e0[3]; /* 0x00EC */ s32 field_0x00ec; /* 0x00F0 */ CPaneMgr* mYnSelPane[2]; - /* 0x00F8 */ s32 field_0x00f8[2]; - /* 0x0100 */ s32 field_0x0100; + /* 0x00F8 */ DUSK_IF_ELSE(f32, s32) field_0x00f8[2]; + /* 0x0100 */ DUSK_IF_ELSE(f32, s32) field_0x0100; /* 0x0104 */ s32 field_0x0104; /* 0x0108 */ bool field_0x0108; /* 0x0109 */ bool field_0x0109; /* 0x010A */ u8 field_0x10a[2]; /* 0x010C */ J2DPane* mBaseSubPane; - /* 0x0110 */ int field_0x0110; + /* 0x0110 */ DUSK_IF_ELSE(f32, int) field_0x0110; /* 0x0114 */ int field_0x0114; /* 0x0118 */ J2DPane* m3mMenuPane; /* 0x011C */ J2DPane* mNameBasePane; - /* 0x0120 */ int field_0x0120; + /* 0x0120 */ DUSK_IF_ELSE(f32, int) field_0x0120; /* 0x0124 */ int field_0x0124; /* 0x0128 */ bool field_0x0128; /* 0x0129 */ u8 field_0x0129[0x012C - 0x0129]; /* 0x012C */ J2DPane* mErrorMsgPane; - /* 0x0130 */ int field_0x0130; + /* 0x0130 */ DUSK_IF_ELSE(f32, int) field_0x0130; /* 0x0134 */ int field_0x0134; /* 0x0138 */ CPaneMgrAlpha* mErrorMsgTxtPane[2]; /* 0x0140 */ TEXT_SPAN mErrorMsgStringPtr[2]; @@ -578,24 +581,24 @@ public: /* 0x01BE */ u8 field_0x01be[2]; /* 0x01C0 */ CPaneMgr* mYnSelTxtPane[2]; /* 0x01C8 */ J2DAnmColor* mFileSelBpk; - /* 0x01CC */ s32 mSelFileBpkFrame; + /* 0x01CC */ DUSK_IF_ELSE(f32, s32) mSelFileBpkFrame; /* 0x01D0 */ J2DAnmTextureSRTKey* mFileSel05Btk; - /* 0x01D4 */ s32 mSelFileBtk05Frame; + /* 0x01D4 */ DUSK_IF_ELSE(f32, s32) mSelFileBtk05Frame; /* 0x01D8 */ J2DAnmColor* mSelFileBookBpk; - /* 0x01DC */ s32 mSelFileBookBpkFrame; + /* 0x01DC */ DUSK_IF_ELSE(f32, s32) mSelFileBookBpkFrame; /* 0x01E0 */ J2DAnmTextureSRTKey* mSelFileBookBtk; - /* 0x01E4 */ s32 mSelFileBookBtkFrame; + /* 0x01E4 */ DUSK_IF_ELSE(f32, s32) mSelFileBookBtkFrame; /* 0x01E8 */ J2DAnmTevRegKey* mSelFileBookBrk; - /* 0x01EC */ s32 mSelFileBookBrkFrame; + /* 0x01EC */ DUSK_IF_ELSE(f32, s32) mSelFileBookBrkFrame; /* 0x01F0 */ J2DAnmTextureSRTKey* mDtEffBtk; - /* 0x01F4 */ s32 mDtEffBtkFrame; + /* 0x01F4 */ DUSK_IF_ELSE(f32, s32) mDtEffBtkFrame; /* 0x01F8 */ J2DAnmTextureSRTKey* mCpEffBtk; - /* 0x01FC */ s32 mCpEffBtkFrame; + /* 0x01FC */ DUSK_IF_ELSE(f32, s32) mCpEffBtkFrame; /* 0x0200 */ J2DAnmTevRegKey* mCpDtEffBrk; - /* 0x0204 */ s32 mCpDtEffBrkFrame; + /* 0x0204 */ DUSK_IF_ELSE(f32, s32) mCpDtEffBrkFrame; /* 0x0208 */ u8 field_0x0208; /* 0x0209 */ u8 field_0x0209; - /* 0x020A */ u8 mFadeTimer; + /* 0x020A */ DUSK_IF_ELSE(f32, u8) mFadeTimer; /* 0x020B */ u8 field_0x020b; /* 0x020C */ CPaneMgrAlpha* mHeaderTxtPane[2]; /* 0x0214 */ TEXT_SPAN mHeaderStringPtr[2]; @@ -648,7 +651,7 @@ public: /* 0x02A0 */ J2DAnmTransform* mCpSelBck2; /* 0x02A4 */ CPaneMgr* mCpSelPane[3]; /* 0x02B0 */ u8 field_0x02b0[0x2b4-0x2b0]; - /* 0x02B4 */ int field_0x02b4[3]; + /* 0x02B4 */ DUSK_IF_ELSE(f32, int) field_0x02b4[3]; /* 0x02C0 */ CPaneMgr* mCpSelPane_moyo[2]; /* 0x02C8 */ CPaneMgr* mCpSelPane_gold[2]; /* 0x02D0 */ CPaneMgr* mCpSelPane_gold2[2]; @@ -658,30 +661,30 @@ public: /* 0x02E4 */ u8 field_0x02e4[2]; /* 0x02E4 */ u8 field_0x02e6[0x2e8-0x2e6]; /* 0x02E8 */ J2DAnmColor* mCpSelBpk; - /* 0x02EC */ s32 mCpSelBpkFrame; + /* 0x02EC */ DUSK_IF_ELSE(f32, s32) mCpSelBpkFrame; /* 0x02F0 */ J2DAnmTextureSRTKey* mCpSel03Btk; - /* 0x02F4 */ s32 mCpSel03BtkFrame; + /* 0x02F4 */ DUSK_IF_ELSE(f32, s32) mCpSel03BtkFrame; /* 0x02F8 */ J2DAnmColor* mCpSelBookBpk; - /* 0x02FC */ s32 mCpSelBookBpkFrame; + /* 0x02FC */ DUSK_IF_ELSE(f32, s32) mCpSelBookBpkFrame; /* 0x0300 */ J2DAnmTextureSRTKey* mCpSelBookBtk; - /* 0x0304 */ s32 mCpSelBookBtkFrame; + /* 0x0304 */ DUSK_IF_ELSE(f32, s32) mCpSelBookBtkFrame; /* 0x0308 */ J2DAnmTevRegKey* mCpSelBookBrk; - /* 0x030C */ s32 mCpSelBookBrkFrame; + /* 0x030C */ DUSK_IF_ELSE(f32, s32) mCpSelBookBrkFrame; /* 0x0310 */ J2DAnmTransform* mYnSelBck; /* 0x0314 */ J2DAnmTransform* mYnSelBck2; /* 0x0318 */ J2DAnmColor* mYnSelBpk; - /* 0x031C */ s32 mYnSelBpkFrame; + /* 0x031C */ DUSK_IF_ELSE(f32, s32) mYnSelBpkFrame; /* 0x0320 */ J2DAnmTextureSRTKey* mYnSelBtk; - /* 0x0324 */ s32 mYnSelBtkFrame; + /* 0x0324 */ DUSK_IF_ELSE(f32, s32) mYnSelBtkFrame; /* 0x0328 */ J2DAnmTransform* m3mBck; /* 0x032C */ J2DAnmTransform* m3mBck2; /* 0x0330 */ J2DAnmColor* m3mBpk; - /* 0x0334 */ s32 m3mBpkFrame; + /* 0x0334 */ DUSK_IF_ELSE(f32, s32) m3mBpkFrame; /* 0x0338 */ J2DAnmTextureSRTKey* m3mBtk; - /* 0x033C */ s32 m3mBtkFrame; + /* 0x033C */ DUSK_IF_ELSE(f32, s32) m3mBtkFrame; /* 0x0340 */ CPaneMgr* m3mSelPane[3]; - /* 0x034C */ s32 field_0x034c[3]; - /* 0x0358 */ int field_0x0358; + /* 0x034C */ DUSK_IF_ELSE(f32, s32) field_0x034c[3]; + /* 0x0358 */ DUSK_IF_ELSE(f32, int) field_0x0358; /* 0x035C */ int field_0x035c; /* 0x0360 */ bool field_0x0360; /* 0x0361 */ u8 field_0x0361[3]; @@ -694,7 +697,7 @@ public: /* 0x0390 */ u8 field_0x0391[3]; /* 0x0394 */ CPaneMgr* m3mSelTextPane[3]; /* 0x03A0 */ J2DAnmTextureSRTKey* mSelDtBtk; - /* 0x03A4 */ s32 mSelDtBtkFrame; + /* 0x03A4 */ DUSK_IF_ELSE(f32, s32) mSelDtBtkFrame; /* 0x03A8 */ CPaneMgr* mSelDtPane_mset; /* 0x03AC */ s16 mWaitTimer; /* 0x03AE */ u8 field_0x03AE[0x03B0 - 0x03AE]; @@ -720,6 +723,7 @@ public: dDlst_FileSelFade_c mFadeDlst; bool mGameModeSaveStartBuildUi = true; GameModeNewSaveState mGameModeNewSaveState = GAME_MODE_STATE_PENDING; + bool mNameMoveRequested = false; #endif }; diff --git a/include/d/d_menu_collect.h b/include/d/d_menu_collect.h index d2a4028b2e..0a7dcbf0d3 100644 --- a/include/d/d_menu_collect.h +++ b/include/d/d_menu_collect.h @@ -194,6 +194,9 @@ public: /* 0x25F */ u8 field_0x25f; /* 0x260 */ u8 field_0x260; /* 0x261 */ u8 field_0x261; +#if TARGET_PC + f32 mBackAlpha = 0.0f; +#endif }; class dMenu_Collect3D_c { diff --git a/include/d/d_menu_dmap.h b/include/d/d_menu_dmap.h index 039b42aab2..2dd7630ba8 100644 --- a/include/d/d_menu_dmap.h +++ b/include/d/d_menu_dmap.h @@ -91,10 +91,6 @@ public: void calcCursor(); void drawCursor(); - #if TARGET_PC - void dMapBgWide(); - #endif - void setDPDFloorSelCurPos(s8 i_pos) { field_0xdd6 = i_pos; } f32 getMapWidth() { return mMapWidth; } @@ -108,7 +104,7 @@ public: } #if TARGET_PC - void resetScrollArrowMask() { field_0xdda = 0; } + void dMapBgWide(); #endif /* 0xC98 */ JKRExpHeap* mpHeap; @@ -174,16 +170,15 @@ public: /* 0xDCC */ f32 field_0xdcc; /* 0xDD0 */ u8 field_0xdd0; /* 0xDD1 */ u8 field_0xdd1; - /* 0xDD2 */ u8 field_0xdd2; + /* 0xDD2 */ DUSK_IF_ELSE(f32, u8) field_0xdd2; /* 0xDD3 */ u8 field_0xdd3; /* 0xDD4 */ s8 field_0xdd4; /* 0xDD5 */ u8 field_0xdd5; /* 0xDD6 */ s8 field_0xdd6; - /* 0xDD7 */ u8 field_0xdd7; + /* 0xDD7 */ DUSK_IF_ELSE(f32, u8) field_0xdd7; /* 0xDD8 */ u8 field_0xdd8; /* 0xDD9 */ u8 field_0xdd9; /* 0xDDA */ u8 field_0xdda; - #if TARGET_PC J2DTextBox* mpPoeCountPane; J2DPicture* mpPoeCountIcon; @@ -210,6 +205,10 @@ public: void mapControl(); bool isOpen(); bool isClose(); +#if TARGET_PC + void presentAnims(); + void presentMapView(); +#endif void _draw(); void itemInfo_init_proc(); void itemInfo_proc(); diff --git a/include/d/d_menu_dmap_map.h b/include/d/d_menu_dmap_map.h index 3f3eb37a0f..6b16730360 100644 --- a/include/d/d_menu_dmap_map.h +++ b/include/d/d_menu_dmap_map.h @@ -191,10 +191,10 @@ public: /* 0xEE */ s8 field_0xee; /* 0xEF */ u8 field_0xef; /* 0xF0 */ u8 field_0xf0; - /* 0xF1 */ u8 field_0xf1; - /* 0xF2 */ u8 field_0xf2; - /* 0xF3 */ u8 field_0xf3; - /* 0xF4 */ s8 field_0xf4; + /* 0xF1 */ DUSK_IF_ELSE(f32, u8) field_0xf1; + /* 0xF2 */ DUSK_IF_ELSE(f32, u8) field_0xf2; + /* 0xF3 */ DUSK_IF_ELSE(f32, u8) field_0xf3; + /* 0xF4 */ DUSK_IF_ELSE(f32, s8) field_0xf4; /* 0xF5 */ u8 field_0xf5; /* 0xF6 */ u8 field_0xf6; /* 0xF7 */ s8 field_0xf7; diff --git a/include/d/d_menu_fmap.h b/include/d/d_menu_fmap.h index 3104d7e76b..c3ea6ec10b 100644 --- a/include/d/d_menu_fmap.h +++ b/include/d/d_menu_fmap.h @@ -112,12 +112,20 @@ public: class dMenu_Fmap_c { public: typedef void (dMenu_Fmap_c::*process)(); - + dMenu_Fmap_c(JKRExpHeap*, STControl*, CSTControl*, u8, u8, u8, f32, f32, u8); void _create(); void _delete(); void _move(); void _draw(); +#if TARGET_PC + void presentAnims(); + void presentZoomView(u8 process, f32 zoomLevel, f32 zoomBlend); + void captureRenderState(); + void captureZoomEnd(u8 process); + void resetRenderState(); + void resetZoomEnd(); +#endif u8 getNextStatus(u8*); bool isSync(); void all_map_init(); @@ -266,7 +274,7 @@ public: /* 0x1B */ PROC_HOWL_DEMO1, /* 0x1C */ PROC_HOWL_DEMO2, /* 0x1D */ PROC_HOWL_DEMO3, - }; + }; static DUSK_GAME_DATA dMenu_Fmap_c* MyClass; diff --git a/include/d/d_menu_fmap2D.h b/include/d/d_menu_fmap2D.h index 3c37c3da74..be638899bd 100644 --- a/include/d/d_menu_fmap2D.h +++ b/include/d/d_menu_fmap2D.h @@ -156,9 +156,6 @@ public: void setRegionCursor(u8 i_value) { mRegionCursor = i_value; } void setMapDrawFlag(bool i_flag) { mMapDrawFlag = i_flag; } void resetDrug() { field_0x1238 = 0; } -#if TARGET_PC - void resetScrollArrowMask() { field_0x122d = 0; } -#endif void offArrowDrawFlag() { mArrowDrawFlag = false; } void onArrowDrawFlag() { mArrowDrawFlag = true; } @@ -288,8 +285,8 @@ public: /* 0x120C */ JUtility::TColor field_0x120c; /* 0x1210 */ float field_0x1210; /* 0x1214 */ float field_0x1214; - /* 0x1218 */ s16 field_0x1218; - /* 0x121A */ s16 field_0x121a; + /* 0x1218 */ DUSK_IF_ELSE(f32, s16) field_0x1218; + /* 0x121A */ DUSK_IF_ELSE(f32, s16) field_0x121a; /* 0x121C */ u8 mRegionTextureReadNum[8]; /* 0x1224 */ u8 mSpotNum; /* 0x1225 */ u8 mStageDataNum; diff --git a/include/d/d_menu_fmap_map.h b/include/d/d_menu_fmap_map.h index b0d1063d18..b92cceec76 100644 --- a/include/d/d_menu_fmap_map.h +++ b/include/d/d_menu_fmap_map.h @@ -172,6 +172,10 @@ public: ~dMenu_FmapMap_c(); void _create(u16, u16, u16, u16, void*); void _delete(); +#if TARGET_PC + void presentAnims(); + void presentRendering(dMenu_Fmap_world_data_c*, int, f32, f32, f32, f32); +#endif void draw(); void rendering(line_class const*); int getLineWidth(int); @@ -224,9 +228,15 @@ public: /* 0xDC */ int mStageCursor; /* 0xE0 */ int mLastStageCursor; /* 0xE4 */ u8 mRegionCursor; - /* 0xE5 */ u8 mFlashTimer; + /* 0xE5 */ DUSK_IF_ELSE(f32, u8) mFlashTimer; /* 0xE8 */ u8* mp_roomList; /* 0xEC */ int m_roomListNumber; +#if TARGET_PC + f32 mFlashFrame = 0.0f; + f32 mPaletteFrame[2] = {}; + bool mResetFlashFrame = true; + bool mPaletteFrameInitialized = false; +#endif static dMenu_FmapMap_c* mMySelfPointer; }; diff --git a/include/d/d_menu_letter.h b/include/d/d_menu_letter.h index 8e112f04d2..f714205853 100644 --- a/include/d/d_menu_letter.h +++ b/include/d/d_menu_letter.h @@ -57,6 +57,7 @@ public: #if TARGET_PC bool pointerWait(); + void presentAnims(); #endif virtual void draw() { _draw(); } @@ -109,7 +110,7 @@ public: /* 0x360 */ f32 field_0x360; /* 0x364 */ f32 field_0x364; /* 0x368 */ s16 field_0x368; - /* 0x36A */ s16 field_0x36a; + /* 0x36A */ DUSK_IF_ELSE(f32, s16) field_0x36a; /* 0x36C */ u8 mStatus; /* 0x36D */ u8 mProcess; /* 0x36E */ u8 mIndex; diff --git a/include/d/d_menu_map_common.h b/include/d/d_menu_map_common.h index 55de3d0f6c..87ffa25136 100644 --- a/include/d/d_menu_map_common.h +++ b/include/d/d_menu_map_common.h @@ -123,8 +123,8 @@ public: /* 0x06C */ u32 _6c; /* 0x070 */ IconInfo_s mIconInfo[128]; /* 0xC70 */ u16 mIconNum; - /* 0xC72 */ s16 mBlinkTimer; - /* 0xC74 */ s16 mLightDropFlashTimer; + /* 0xC72 */ DUSK_IF_ELSE(f32, s16) mBlinkTimer; + /* 0xC74 */ DUSK_IF_ELSE(f32, s16) mLightDropFlashTimer; /* 0xC76 */ u16 _c76; /* 0xC78 */ f32 mBlinkAlpha; /* 0xC7C */ f32 _c7c; diff --git a/include/d/d_menu_option.h b/include/d/d_menu_option.h index ff4b84f5e3..8ccacf6490 100644 --- a/include/d/d_menu_option.h +++ b/include/d/d_menu_option.h @@ -96,6 +96,8 @@ public: bool dpdMenuMove(); #if TARGET_PC bool pointerConfirmSelect(); + void presentLayoutAnims(); + void presentAnims(); #endif void paneResize(u64); void initialize(); @@ -190,11 +192,11 @@ public: /* 0x3B4 */ f32 field_0x3b4; /* 0x3B8 */ JUtility::TColor field_0x3b8; /* 0x3BC */ JUtility::TColor field_0x3bc; - /* 0x3C0 */ int field_0x3c0; - /* 0x3C4 */ int field_0x3c4; - /* 0x3C8 */ int field_0x3c8[4]; + /* 0x3C0 */ DUSK_IF_ELSE(f32, int) field_0x3c0; + /* 0x3C4 */ DUSK_IF_ELSE(f32, int) field_0x3c4; + /* 0x3C8 */ DUSK_IF_ELSE(f32, int) field_0x3c8[4]; /* 0x3D8 */ s16 mFrame; - /* 0x3DA */ s16 field_0x3da; + /* 0x3DA */ DUSK_IF_ELSE(f32, s16) field_0x3da; /* 0x3DC */ u16 field_0x3dc; /* 0x3DE */ u16 field_0x3de; /* 0x3E0 */ u8 field_0x3e0; diff --git a/include/d/d_menu_ring.h b/include/d/d_menu_ring.h index fa4f97496f..eb569da86b 100644 --- a/include/d/d_menu_ring.h +++ b/include/d/d_menu_ring.h @@ -27,6 +27,10 @@ public: void _delete(); void _move(); void _draw(); +#if TARGET_PC + void advanceSelectItem(); + void captureRenderState(); +#endif void setKanteraPos(f32, f32); bool isOpen(); bool isMoveEnd(); @@ -166,7 +170,7 @@ public: /* 0x67E */ s16 field_0x67e; /* 0x680 */ s16 mCursorSpeed; /* 0x682 */ s16 field_0x682; - /* 0x684 */ s16 field_0x684; + /* 0x684 */ DUSK_IF_ELSE(f32, s16) field_0x684; /* 0x686 */ s16 field_0x686[4]; /* 0x68E */ s16 field_0x68e; /* 0x690 */ u8 mItemSlots[MAX_ITEM_SLOTS]; @@ -207,16 +211,6 @@ public: /* 0x6D2 */ u8 field_0x6d2; /* 0x6D3 */ u8 field_0x6d3; #if TARGET_PC - f32 mSelectItemSlideElapsed[4]; - f32 mCursorInterpPrevX; - f32 mCursorInterpPrevY; - f32 mCursorInterpCurrX; - f32 mCursorInterpCurrY; - s16 mCursorInterpPrevAngle; - s16 mCursorInterpCurrAngle; - bool mCursorInterpPrevAngular; - bool mCursorInterpCurrAngular; - bool mCursorInterpInit; bool mPointerTouchPressHoveredCurrent; #endif }; diff --git a/include/d/d_menu_save.h b/include/d/d_menu_save.h index a237bbb849..b12322ba8c 100644 --- a/include/d/d_menu_save.h +++ b/include/d/d_menu_save.h @@ -263,11 +263,12 @@ public: void setInitSaveData(); void _draw(); - #if TARGET_PC +#if TARGET_PC void menuSaveWide(); bool pointerSaveSelect(); bool pointerYesNoSelect(bool errorSelect, u8 errParam = 0, u8 soundParam = 0); - #endif + void presentLayoutAnims(); +#endif void _draw2(); @@ -293,16 +294,16 @@ public: /* 0x0054 */ u8 mSelectedFile; /* 0x0055 */ u8 mLastSelFile; /* 0x0058 */ CPaneMgr* mpSelectMoveBase; - /* 0x005C */ int mDataBaseMoveAnmFrame; + /* 0x005C */ DUSK_IF_ELSE(f32, int) mDataBaseMoveAnmFrame; /* 0x0060 */ int mDataBaseMoveFrameMax; /* 0x0064 */ u8 field_0x64; /* 0x0065 */ u8 field_0x65; /* 0x0068 */ CPaneMgr* mpSelData[3]; - /* 0x0074 */ int field_0x74[3]; + /* 0x0074 */ DUSK_IF_ELSE(f32, int) field_0x74[3]; /* 0x0080 */ int field_0x80; /* 0x0084 */ CPaneMgr* mpNoYes[2]; - /* 0x008C */ int field_0x8c[2]; - /* 0x0094 */ int mYesNoMoveAnmFrame; + /* 0x008C */ DUSK_IF_ELSE(f32, int) field_0x8c[2]; + /* 0x0094 */ DUSK_IF_ELSE(f32, int) mYesNoMoveAnmFrame; /* 0x0098 */ int mYesNoMoveAnmMax; /* 0x009C */ u8 field_0x9c; /* 0x009D */ u8 field_0x9d; @@ -310,7 +311,7 @@ public: /* 0x00A0 */ J2DPane* field_0xa0; /* 0x00A4 */ u8 field_0xa4[0x10]; /* 0x00B4 */ J2DPane* field_0xb4; - /* 0x00B8 */ int field_0xb8; + /* 0x00B8 */ DUSK_IF_ELSE(f32, int) field_0xb8; /* 0x00BC */ int field_0xbc; /* 0x00C0 */ CPaneMgrAlpha* mpErrTxtPane[2]; /* 0x00C8 */ TEXT_SPAN mpErrTxt[2]; @@ -334,15 +335,15 @@ public: /* 0x0134 */ u8 mNoYesWakuAnmTimer[2]; /* 0x0138 */ CPaneMgr* mpNoYesTxt[2]; /* 0x0140 */ J2DAnmColor* mpFileWakuAnm; - /* 0x0144 */ int mFileWakuAnmFrame; + /* 0x0144 */ DUSK_IF_ELSE(f32, int) mFileWakuAnmFrame; /* 0x0148 */ J2DAnmTextureSRTKey* mpFileWakuRotAnm; - /* 0x014C */ int mFileWakuRotAnmFrame; + /* 0x014C */ DUSK_IF_ELSE(f32, int) mFileWakuRotAnmFrame; /* 0x0150 */ J2DAnmColor* field_0x150; - /* 0x0154 */ int field_0x154; + /* 0x0154 */ DUSK_IF_ELSE(f32, int) field_0x154; /* 0x0158 */ J2DAnmTextureSRTKey* field_0x158; - /* 0x015C */ int field_0x15c; + /* 0x015C */ DUSK_IF_ELSE(f32, int) field_0x15c; /* 0x0160 */ J2DAnmTevRegKey* field_0x160; - /* 0x0164 */ int field_0x164; + /* 0x0164 */ DUSK_IF_ELSE(f32, int) field_0x164; /* 0x0168 */ CPaneMgrAlpha* mpHeaderTxtPane[2]; /* 0x0170 */ TEXT_SPAN mpHeaderTxt[2]; /* 0x0178 */ u8 mHeaderTxtType; // 0: Select Menu 1: YesNo Menu @@ -394,6 +395,9 @@ public: /* 0x21A1 */ u8 field_0x21a1; /* 0x21A2 */ u8 field_0x21a2; /* 0x21A3 */ u8 field_0x21a3; +#if TARGET_PC + u8 mYesNoSelAnmCol; +#endif }; #endif /* D_MENU_D_MENU_SAVE_H */ diff --git a/include/d/d_menu_skill.h b/include/d/d_menu_skill.h index 2c2dc445d1..bd5e16e879 100644 --- a/include/d/d_menu_skill.h +++ b/include/d/d_menu_skill.h @@ -51,6 +51,7 @@ public: #if TARGET_PC bool pointerWait(); + void presentAnims(); #endif virtual void draw() { _draw(); } @@ -90,7 +91,7 @@ public: /* 0x1F4 */ f32 mBarScale[2]; /* 0x1FC */ u32 mStringID; /* 0x200 */ s16 mFrame; - /* 0x202 */ s16 mProcFrame; + /* 0x202 */ DUSK_IF_ELSE(f32, s16) mProcFrame; /* 0x204 */ u8 mStatus; /* 0x205 */ u8 mProcess; /* 0x206 */ u8 mIndex; diff --git a/include/d/d_meter2.h b/include/d/d_meter2.h index 4246d9249d..b3bde323ae 100644 --- a/include/d/d_meter2.h +++ b/include/d/d_meter2.h @@ -100,6 +100,11 @@ public: bool isShowFlag(int i_no) { return field_0x1e6 & (1 << i_no); } void onShowFlag(int i_no) { field_0x1e6 |= (1 << i_no); } +#if TARGET_PC + void presentAnims(); + void presentMap(); +#endif + /* 0x0FC */ int field_0xfc; /* 0x100 */ JKRExpHeap* mpHeap; /* 0x104 */ JKRExpHeap* mpSubHeap; @@ -299,6 +304,16 @@ public: /* 0x460 */ u8 field_0x460[0x4bc - 0x460]; /* 0x4BC */ u8 field_0x4bc; /* 0x4BC */ u8 field_0x4bd; + +#if TARGET_PC + struct { + f32 vesselX, vesselY, vesselScale, vesselAlpha; + f32 buttonAX[2], buttonAY[2]; + f32 buttonBX[2], buttonBY[2]; + f32 crossX, crossY; + bool ready; + } mPresentationTargets; +#endif }; #endif /* D_METER_D_METER2_H */ diff --git a/include/d/d_meter2_draw.h b/include/d/d_meter2_draw.h index c9dc8217e9..927d71d63c 100644 --- a/include/d/d_meter2_draw.h +++ b/include/d/d_meter2_draw.h @@ -11,6 +11,7 @@ class JKRExpHeap; class JKRHeap; class dKantera_icon_c; +IF_DUSK(class dMeterMap_c); class dMeter2Draw_c : public dDlst_base_c { public: @@ -46,7 +47,8 @@ public: void playPikariBpkAnimation(f32); void playOxygenBpkAnimation(J2DAnmColor*); void drawPikari(f32, f32, f32*, f32, JUtility::TColor, JUtility::TColor, - JUtility::TColor, JUtility::TColor, f32, u8); + JUtility::TColor, JUtility::TColor, f32, u8 + IF_DUSK_ARG(f32 elapsedFrames = -1.0f)); void drawPikari(CPaneMgr*, f32*, f32, JUtility::TColor, JUtility::TColor, JUtility::TColor, JUtility::TColor, f32, u8); void drawPikariHakusha(f32, f32, f32, f32, JUtility::TColor, JUtility::TColor, @@ -59,11 +61,11 @@ public: void drawKanteraScreen(u8); void drawMagic(s16, s16, f32, f32); void setAlphaMagicChange(bool); - void drawKantera(s32, s32, f32, f32); + void drawKantera(s32, DUSK_IF_ELSE(f32, s32), f32, f32); void setAlphaKanteraChange(bool); void setAlphaKanteraAnimeMin(); void setAlphaKanteraAnimeMax(); - void drawOxygen(s32, s32, f32, f32); + void drawOxygen(s32, DUSK_IF_ELSE(f32, s32), f32, f32); void setAlphaOxygenChange(bool); void setAlphaOxygenAnimeMin(); void setAlphaOxygenAnimeMax(); @@ -134,7 +136,11 @@ public: J2DScreen* getMainScreenPtr() { return mpScreen; } bool isEmphasisC() { return field_0x766 == 7 ? true : false; } +#if TARGET_PC + f32 getMeterGaugeAlphaRate(u8 i_no); +#else f32 getMeterGaugeAlphaRate(u8 i_no) { return mMeterAlphaRate[i_no]; } +#endif bool isEmphasisA() { return field_0x761 == 7 ? true : false; } bool isEmphasisB() { return field_0x762 == 7 ? true : false; } bool isEmphasisBin() { return field_0x763 == 7 ? true : false; } @@ -152,6 +158,27 @@ public: constexpr f32 getButtonZAlpha() const { return mButtonZAlpha; } + + void beginExecution(); + void endExecution(); + bool isExecuting() const { return mExecuting; } + void setExecuting(bool executing) { mExecuting = executing; } + + void publishKantera(s32 now, s32 max); + void publishOxygen(s32 now, s32 max); + + void present(); + void presentVessel(f32 i_posX, f32 i_posY, f32 i_scale); + void presentButtonA(f32 i_posX, f32 i_posY, f32 i_textPosX, f32 i_textPosY, f32 i_scale); + void presentButtonB(f32 i_posX, f32 i_posY, f32 i_textPosX, f32 i_textPosY, f32 i_scale); + void presentMapAlpha(dMeterMap_c* map); + + void showLightDrop(bool show, bool get_path = false); + void showCross(bool show); + void showMap(bool show); + + bool lightDropRowUsed() const; + void resetRows(); #endif /* 0x004 */ item_params mItemParams[4]; @@ -284,7 +311,7 @@ public: /* 0x742 */ s16 field_0x742[3]; /* 0x748 */ u8 field_0x748[0xC]; /* 0x756 */ u16 field_0x754; - /* 0x756 */ s16 field_0x756; + /* 0x756 */ DUSK_IF_ELSE(f32, s16) field_0x756; /* 0x758 */ u8 field_0x758; /* 0x759 */ u8 field_0x759; /* 0x75A */ u8 field_0x75a; @@ -363,6 +390,9 @@ public: /* 0x858 */ GXColor mButtonZTextColor; /* 0x85C */ GXColor mButtonXYTextColor; /* 0x860 */ u8 field_0x860[2]; +#if TARGET_PC + bool mExecuting; +#endif }; #endif /* D_METER_D_METER2_DRAW_H */ diff --git a/include/d/d_meter_button.h b/include/d/d_meter_button.h index 2d9a4bb5d8..04a2f7f488 100644 --- a/include/d/d_meter_button.h +++ b/include/d/d_meter_button.h @@ -255,7 +255,7 @@ public: /* 0x4B2 */ u16 mMsgID; /* 0x4B4 */ s16 field_0x4b4; /* 0x4B6 */ s16 field_0x4b6; - /* 0x4B8 */ s16 field_0x4b8[2]; + /* 0x4B8 */ DUSK_IF_ELSE(f32, s16) field_0x4b8[2]; /* 0x4BC */ u8 field_0x4bc[2]; /* 0x4BE */ u8 field_0x4be[2]; /* 0x4C0 */ bool mPlayedButtonSound[BUTTON_NUM]; @@ -344,10 +344,8 @@ public: /* 0x624 */ f32 mMidonaPosX; /* 0x628 */ f32 mMidonaPosY; /* 0x62C */ f32 mMidonaScale; - -#ifdef TARGET_PC - bool mWasListen[2]; - bool mWasRepeat[2]; +#if TARGET_PC + u16 mBlinkButton = 0; #endif }; diff --git a/include/d/d_meter_map.h b/include/d/d_meter_map.h index 648fb5aef5..630d50b71d 100644 --- a/include/d/d_meter_map.h +++ b/include/d/d_meter_map.h @@ -4,6 +4,10 @@ #include "d/d_map_path_dmap.h" #include "JSystem/JHostIO/JORReflexible.h" +#if TARGET_PC +#include "global.h" +#endif + class J2DPicture; class dMap_c; class dMap_HIO_c; @@ -111,13 +115,16 @@ public: /* 0x1C */ f32 mDrawPosY; /* 0x20 */ f32 mSizeW; /* 0x24 */ f32 mSizeH; - /* 0x28 */ s16 mSlidePositionOffset; + /* 0x28 */ DUSK_IF_ELSE(f32, s16) mSlidePositionOffset; /* 0x2A */ u8 field_0x2a; /* 0x2B */ u8 field_0x2b; /* 0x2C */ u8 mMapAlpha; /* 0x2D */ u8 mMapIsInside; /* 0x2E */ u8 field_0x2e; /* 0x30 */ int field_0x30; +#if TARGET_PC + f32 mSlidePositionOffsetTarget; +#endif }; #endif /* D_METER_D_METER_MAP_H */ diff --git a/include/d/d_meter_string.h b/include/d/d_meter_string.h index 382fcbf99b..a02daea8d7 100644 --- a/include/d/d_meter_string.h +++ b/include/d/d_meter_string.h @@ -17,7 +17,12 @@ public: virtual int _execute(u32); virtual int _delete(); - f32 acc(s16 param_0, s16 param_1, s16 param_2) { + f32 acc(DUSK_IF_ELSE(f32, s16) param_0, DUSK_IF_ELSE(f32, s16) param_1, DUSK_IF_ELSE(f32, s16) param_2) { +#if TARGET_PC + if (param_0 == param_2) { + return 1.0f; + } +#endif return ((f32)(param_1 - param_2) * (f32)(param_1 - param_2)) / ((f32)(param_0 - param_2) * (f32)(param_0 - param_2)); } diff --git a/include/d/d_msg_class.h b/include/d/d_msg_class.h index 49a99a6ef1..bd26cae084 100644 --- a/include/d/d_msg_class.h +++ b/include/d/d_msg_class.h @@ -197,12 +197,16 @@ struct jmessage_tReference : public JMessage::TReference { void addDrawLightCount() { mDrawLightCount++; } void setCharAlpha(f32 alpha) { mCharAlpha = alpha; } +#if TARGET_PC + void addCharAlpha(); +#else void addCharAlpha() { mCharAlpha += mAddCharAlpha; if (mCharAlpha > 255.0f) { mCharAlpha = 255.0f; } } +#endif void setLineLength(int i_no, f32 i_strLen, f32 i_spaceLen) { mStrLength[i_no] = i_strLen; @@ -215,12 +219,16 @@ struct jmessage_tReference : public JMessage::TReference { } } +#if TARGET_PC + void addCharAllAlphaRate(); +#else void addCharAllAlphaRate() { mCharAllAlphaRate += mAddCharAllAlphaRate; if (mCharAllAlphaRate > 1.0f) { mCharAllAlphaRate = 1.0f; } } +#endif void addLineLength(int param_0, f32 param_1, f32 param_2) { mStrLength[param_0] += param_1; diff --git a/include/d/d_msg_object.h b/include/d/d_msg_object.h index 3e8c535800..84dbcc9f8d 100644 --- a/include/d/d_msg_object.h +++ b/include/d/d_msg_object.h @@ -21,6 +21,9 @@ public: int _create(msg_class*); int _execute(); int _draw(); +#if TARGET_PC + void presentAnims(); +#endif int _delete(); void setMessageIndex(u32, u32, bool); void setMessageIndexDemo(u32, bool); @@ -285,7 +288,7 @@ public: /* 0x164 */ u16 mNodeIdx; /* 0x166 */ u16 field_0x166; /* 0x168 */ u16 field_0x168; - /* 0x16A */ s16 field_0x16a; + /* 0x16A */ DUSK_IF_ELSE(f32, s16) field_0x16a; /* 0x16C */ s16 mCurrentGroupID; // group whose BMG is parsed; -1 none, 0 common, 1-8 stage /* 0x16E */ s16 field_0x16e; /* 0x170 */ s16 mNowTalkFlowNo; diff --git a/include/d/d_msg_out_font.h b/include/d/d_msg_out_font.h index 3bd41ca825..df9b9e4368 100644 --- a/include/d/d_msg_out_font.h +++ b/include/d/d_msg_out_font.h @@ -39,7 +39,7 @@ class COutFont_c { public: COutFont_c(u8); void initialize(); - void setBlendAnime(J2DPicture*, s16); + void setBlendAnime(J2DPicture*, DUSK_IF_ELSE(f32, s16)); const char* getBtiName(int); virtual ~COutFont_c(); @@ -57,7 +57,7 @@ public: /* 0x1A8 */ f32 mAlphaRatio; /* 0x1AC */ f32 field_0x1ac; /* 0x1B0 */ f32 field_0x1b0; - /* 0x1B4 */ s16 field_0x1b4[70]; + /* 0x1B4 */ DUSK_IF_ELSE(f32, s16) field_0x1b4[70]; /* 0x240 */ bool field_0x240; /* 0x241 */ u8 mRupeeColor; /* 0x242 */ u8 field_0x242; diff --git a/include/d/d_msg_scrn_3select.h b/include/d/d_msg_scrn_3select.h index 35c831d3c5..de74919d4f 100644 --- a/include/d/d_msg_scrn_3select.h +++ b/include/d/d_msg_scrn_3select.h @@ -26,10 +26,16 @@ public: dMsgScrn3Select_c(); virtual ~dMsgScrn3Select_c(); bool isSelect(); +#if TARGET_PC + bool isAnimeActive() { return mProcess < PROC_MAX_e; } +#endif void setString(DUSK_CONST char*, DUSK_CONST char*, DUSK_CONST char*); void setRubyString(DUSK_CONST char*, DUSK_CONST char*, DUSK_CONST char*); void translate(f32, f32); void draw(f32, f32); +#if TARGET_PC + void presentAnims(); +#endif void selAnimeInit(u8, u8, u8, f32, u8); bool selAnimeMove(u8, u8, bool); bool selAnimeEnd(); @@ -101,6 +107,9 @@ public: /* 0x115 */ u8 mDPDPoint; /* 0x116 */ u8 field_0x116; /* 0x117 */ u8 field_0x117; +#if TARGET_PC + bool mPresenting; +#endif }; // Size: 0x118 #endif /* MSG_SCRN_D_MSG_SCRN_3SELECT_H */ diff --git a/include/d/d_msg_scrn_base.h b/include/d/d_msg_scrn_base.h index 17fd7380d3..fe91363970 100644 --- a/include/d/d_msg_scrn_base.h +++ b/include/d/d_msg_scrn_base.h @@ -37,6 +37,10 @@ public: virtual void fukiAlpha(f32); virtual void fontAlpha(f32); virtual void fukiPosCalc(u8) {} +#if TARGET_PC + virtual void presentAnims(); + virtual bool isSelectAnimeActive() { return false; } +#endif f32 getSelTextBoxPosX(int idx) { return mSelTextBoxPosX[idx]; } f32 getSelTextBoxPosY(int idx) { return mSelTextBoxPosY[idx]; } diff --git a/include/d/d_msg_scrn_explain.h b/include/d/d_msg_scrn_explain.h index 1b638f9b83..2868d173f9 100644 --- a/include/d/d_msg_scrn_explain.h +++ b/include/d/d_msg_scrn_explain.h @@ -23,6 +23,9 @@ public: dMsgScrnExplain_c(STControl*, u8, bool, u8); void move(); void draw(J2DOrthoGraph*); +#if TARGET_PC + void presentAnims(); +#endif void wait_init(); void wait_proc(); void open_request_init(); @@ -67,7 +70,7 @@ public: /* 0x50 */ f32 field_0x50; /* 0x54 */ u32 mOpenMsgId; /* 0x58 */ bool field_0x58; - /* 0x5A */ s16 field_0x5a; + /* 0x5A */ DUSK_IF_ELSE(f32, s16) field_0x5a; /* 0x5C */ s16 field_0x5c; /* 0x5E */ s16 mKeyWaitTimer; /* 0x60 */ u8 mStatus; diff --git a/include/d/d_msg_scrn_howl.h b/include/d/d_msg_scrn_howl.h index 5db5db0232..c168b1f4f3 100644 --- a/include/d/d_msg_scrn_howl.h +++ b/include/d/d_msg_scrn_howl.h @@ -8,6 +8,9 @@ struct dMsgScrnHowl_c : public dMsgScrnBase_c { ~dMsgScrnHowl_c(); void exec(); void drawSelf(); +#if TARGET_PC + void presentAnims(); +#endif void guide_on_init(); void guide_on_proc(); void guide_off_init(); @@ -77,7 +80,7 @@ struct dMsgScrnHowl_c : public dMsgScrnBase_c { /* 0x1A18 */ f32 field_0x1a18[30]; /* 0x1A90 */ f32 field_0x1a90[30]; /* 0x1B08 */ f32 field_0x1b08[3]; - /* 0x1B14 */ s16 field_0x1b14[0x300]; + /* 0x1B14 */ DUSK_IF_ELSE(f32, s16) field_0x1b14[0x300]; /* 0x2114 */ u32 field_0x2114; /* 0x2118 */ u32 field_0x2118; /* 0x211C */ u32 field_0x211c; @@ -113,6 +116,8 @@ struct dMsgScrnHowl_c : public dMsgScrnBase_c { #if TARGET_PC u8 showCursor; + f32 mSampleAcc; + f32 mPulseFrame; #endif }; diff --git a/include/d/d_msg_scrn_item.h b/include/d/d_msg_scrn_item.h index 865071a2af..a0387b8127 100644 --- a/include/d/d_msg_scrn_item.h +++ b/include/d/d_msg_scrn_item.h @@ -13,6 +13,9 @@ struct dMsgScrnItem_c : public dMsgScrnBase_c { ~dMsgScrnItem_c(); void exec(); void drawSelf(); +#if TARGET_PC + void presentAnims(); +#endif void arwAnimeInit(); void arwAnimeMove(); void dotAnimeInit(); @@ -23,6 +26,9 @@ struct dMsgScrnItem_c : public dMsgScrnBase_c { void selectAnimeInit(u8, u8, f32, u8); bool selectAnimeMove(u8, u8, bool); bool selectAnimeEnd(); +#if TARGET_PC + bool isSelectAnimeActive(); +#endif void fukiScale(f32); void fukiTrans(f32, f32); void fukiAlpha(f32); diff --git a/include/d/d_msg_scrn_kanban.h b/include/d/d_msg_scrn_kanban.h index bba4ebd734..7e9fcb1caf 100644 --- a/include/d/d_msg_scrn_kanban.h +++ b/include/d/d_msg_scrn_kanban.h @@ -13,6 +13,9 @@ struct dMsgScrnKanban_c : public dMsgScrnBase_c { ~dMsgScrnKanban_c(); void exec(); void draw(); +#if TARGET_PC + void presentAnims(); +#endif void fukiAlpha(f32); void fukiScale(f32); void fukiTrans(f32, f32); diff --git a/include/d/d_msg_scrn_talk.h b/include/d/d_msg_scrn_talk.h index 1c8283ad44..c33b67a76c 100644 --- a/include/d/d_msg_scrn_talk.h +++ b/include/d/d_msg_scrn_talk.h @@ -23,6 +23,10 @@ struct dMsgScrnTalk_c : public dMsgScrnBase_c { void selectAnimeInit(u8, u8, f32, u8); bool selectAnimeMove(u8, u8, bool); bool selectAnimeEnd(); +#if TARGET_PC + void presentAnims(); + bool isSelectAnimeActive(); +#endif void fukiScale(f32); void fukiTrans(f32, f32); void fukiAlpha(f32); @@ -42,7 +46,7 @@ struct dMsgScrnTalk_c : public dMsgScrnBase_c { /* 0x0F4 */ f32 field_0xf4; /* 0x0F8 */ f32 field_0xf8[150]; /* 0x350 */ f32 field_0x350[3]; - /* 0x35C */ s16 field_0x35c[150]; + /* 0x35C */ DUSK_IF_ELSE(f32, s16) field_0x35c[150]; /* 0x488 */ u8 field_0x488; }; diff --git a/include/d/d_msg_scrn_tree.h b/include/d/d_msg_scrn_tree.h index f30f8a6193..844d378efd 100644 --- a/include/d/d_msg_scrn_tree.h +++ b/include/d/d_msg_scrn_tree.h @@ -11,6 +11,9 @@ struct dMsgScrnTree_c : public dMsgScrnBase_c { virtual ~dMsgScrnTree_c(); void exec(); void draw(); +#if TARGET_PC + void presentAnims(); +#endif void fukiAlpha(f32); void fukiScale(f32); void fukiTrans(f32, f32); diff --git a/include/d/d_name.h b/include/d/d_name.h index de084defb3..95779b576a 100644 --- a/include/d/d_name.h +++ b/include/d/d_name.h @@ -111,9 +111,9 @@ public: void menuCursorMove2(); void selectCursorPosSet(int); - #if TARGET_PC +#if TARGET_PC void nameWide(); - #endif +#endif void _draw(); void screenSet(); @@ -135,9 +135,9 @@ public: /* 0x00C */ dDlst_NameIN_c nameIn; /* 0x020 */ dSelect_cursor_c* mSelIcon; /* 0x024 */ J2DAnmColorKey* mCursorColorKey; - /* 0x028 */ int mCurColAnmF; + /* 0x028 */ DUSK_IF_ELSE(f32, int) mCurColAnmF; /* 0x02C */ J2DAnmTextureSRTKey* mCursorTexKey; - /* 0x030 */ int mCurTexAnmF; + /* 0x030 */ DUSK_IF_ELSE(f32, int) mCurTexAnmF; /* 0x034 */ CPaneMgrAlpha* mNameCursor[8]; /* 0x054 */ TEXT_SPAN mNameText[8]; /* 0x074 */ CPaneMgr* mMojiIcon[65]; diff --git a/include/d/d_pane_class.h b/include/d/d_pane_class.h index ffc067e411..29cee0c2ea 100644 --- a/include/d/d_pane_class.h +++ b/include/d/d_pane_class.h @@ -4,6 +4,10 @@ #include "JSystem/JUtility/TColor.h" #include "d/d_pane_class_alpha.h" +#if TARGET_PC +#include +#endif + class JKRHeap; void dPaneClass_showNullPane(J2DScreen*); @@ -26,6 +30,9 @@ public: void setBlackWhite(JUtility::TColor, JUtility::TColor); void paneTrans(f32, f32); void paneScale(f32, f32); +#if TARGET_PC + void presentAnime(); +#endif bool scaleAnime(s16, f32, f32, u8); bool colorAnime(s16, JUtility::TColor, JUtility::TColor, JUtility::TColor, JUtility::TColor, u8); @@ -54,8 +61,14 @@ public: void resize(f32 x, f32 y) { getPanePtr()->resize(x, y); } void move(f32 x, f32 y) { getPanePtr()->move(x, y); } - void scaleAnimeStart(s16 v) { mScaleAnime = v; } - void colorAnimeStart(s16 start) { mColorAnime = start; } + void scaleAnimeStart(DUSK_IF_ELSE(f32, s16) v) { + mScaleAnime = v; + IF_DUSK(mScaleAnimation.active = false;) + } + void colorAnimeStart(DUSK_IF_ELSE(f32, s16) start) { + mColorAnime = start; + IF_DUSK(mColorAnimation.active = false;) + } f32 getPosX() { return getPanePtr()->getBounds().i.x; } f32 getPosY() { return getPanePtr()->getBounds().i.y; } @@ -106,10 +119,14 @@ public: /* 0x5C */ JUtility::TColor mInitBlack; /* 0x60 */ s16 field_0x60; /* 0x62 */ s16 field_0x62; - /* 0x64 */ s16 mScaleAnime; + /* 0x64 */ DUSK_IF_ELSE(f32, s16) mScaleAnime; /* 0x66 */ s16 field_0x66; /* 0x68 */ s16 field_0x68; - /* 0x6A */ s16 mColorAnime; + /* 0x6A */ DUSK_IF_ELSE(f32, s16) mColorAnime; +#if TARGET_PC + Animation mScaleAnimation; + Animation> mColorAnimation; +#endif }; #endif /* D_PANE_D_PANE_CLASS_H */ diff --git a/include/d/d_pane_class_alpha.h b/include/d/d_pane_class_alpha.h index cf38229b97..f55227a3ee 100644 --- a/include/d/d_pane_class_alpha.h +++ b/include/d/d_pane_class_alpha.h @@ -9,6 +9,16 @@ class JKRExpHeap; class CPaneMgrAlpha { public: +#if TARGET_PC + template + struct Animation { + bool active = false; + s16 duration; + u8 curve; + T start, end; + }; +#endif + virtual ~CPaneMgrAlpha(); virtual void setAlpha(u8); @@ -18,9 +28,12 @@ public: void show(); void hide(); bool isVisible(); - f32 rateCalc(s16, s16, u8); + f32 rateCalc(DUSK_IF_ELSE(f32, s16), DUSK_IF_ELSE(f32, s16), u8); void setAlphaRate(f32); f32 getAlphaRate(); +#if TARGET_PC + void presentAlphaAnime(); +#endif bool alphaAnime(s16 timer, u8 startAlpha, u8 endAlpha, u8 calcType); bool alphaAnimeLoop(s16, u8, u8, u8); void childPaneCount(J2DPane*); @@ -29,8 +42,11 @@ public: J2DPane* getPanePtr() { return mPane; } u8 getAlpha() { return getPanePtr()->getAlpha(); } - s16 getAlphaTimer() { return mAlphaTimer; } - void alphaAnimeStart(s16 start) { mAlphaTimer = start; } + DUSK_IF_ELSE(f32, s16) getAlphaTimer() { return mAlphaTimer; } + void alphaAnimeStart(DUSK_IF_ELSE(f32, s16) start) { + mAlphaTimer = start; + IF_DUSK(mAlphaAnimation.active = false;) + } u8 getInitAlpha() { return mInitAlpha; } /* 0x04 */ J2DPane* mPane; @@ -38,9 +54,12 @@ public: /* 0x0C */ void* mpFirstStackAlpha; /* 0x10 */ u8* field_0x10; /* 0x14 */ s16 mChildPaneCount; - /* 0x16 */ s16 mAlphaTimer; + /* 0x16 */ DUSK_IF_ELSE(f32, s16) mAlphaTimer; /* 0x18 */ u8 mInitAlpha; /* 0x19 */ u8 mFlags; +#if TARGET_PC + Animation mAlphaAnimation; +#endif }; class CPaneMgrAlphaMorf : public CPaneMgrAlpha { diff --git a/include/d/d_select_cursor.h b/include/d/d_select_cursor.h index 766463b5b3..891280e801 100644 --- a/include/d/d_select_cursor.h +++ b/include/d/d_select_cursor.h @@ -42,12 +42,18 @@ public: virtual ~dSelect_cursor_c(); virtual void draw(); +#if TARGET_PC + void setInterp(bool enabled); + void setPos(f32 x, f32 y, const cXyz* world = nullptr); + void setWorldPos(const cXyz& position, f32 offsetX, f32 offsetY) { setPos(offsetX, offsetY, &position); } +#else void setPos(f32 x, f32 y) { mPositionX = x; mPositionY = y; } +#endif -#ifdef TARGET_PC +#if TARGET_PC f32 getPositionX() const { return mPositionX; } f32 getPositionY() const { return mPositionY; } @@ -100,6 +106,11 @@ public: /* 0xB5 */ u8 mNameIdx; /* 0xB6 */ u8 field_0xb6; /* 0xB7 */ bool mUpdateFlag; +#if TARGET_PC + cXyz mWorldPosition{0.0f, 0.0f, 0.0f}; + bool mWorldCursor = false; + bool mInterpolatePosition = true; +#endif }; #endif /* D_D_SELECT_CURSOR_H */ diff --git a/include/d/d_timer.h b/include/d/d_timer.h index de145650ac..235b22a981 100644 --- a/include/d/d_timer.h +++ b/include/d/d_timer.h @@ -57,6 +57,9 @@ public: #endif void drawPikari(int); +#if TARGET_PC + void presentAnims(); +#endif virtual void draw(); virtual ~dDlst_TimerScrnDraw_c(); @@ -68,10 +71,15 @@ public: void resetCowID() { mCowID = 0; } bool isVisible() { return mTimerVisible; } - f32 acc(s16 param_0, s16 param_1, s16 param_2) { + f32 acc(DUSK_IF_ELSE(f32, s16) param_0, DUSK_IF_ELSE(f32, s16) param_1, DUSK_IF_ELSE(f32, s16) param_2) { +#if TARGET_PC + if (param_0 == param_2) { + return 1.0f; + } +#endif return ((f32)(param_1 - param_2) * (f32)(param_1 - param_2)) / ((f32)(param_0 - param_2) * (f32)(param_0 - param_2)); } - + void setTimerTrans(f32 x, f32 y) { mTimerTransX = x; mTimerTransY = y; @@ -103,7 +111,7 @@ public: /* 0x3CC */ int field_0x3CC; /* 0x3D0 */ int field_0x3D0; /* 0x3D4 */ int field_0x3D4; - /* 0x3D8 */ int field_0x3D8; + /* 0x3D8 */ DUSK_IF_ELSE(f32, int) field_0x3D8; /* 0x3DC */ u8 mCowID; /* 0x3DD */ u8 mHIOType; /* 0x3DE */ u8 field_0x3DE; @@ -111,6 +119,12 @@ public: /* 0x3E0 */ u8 field_0x3E0; /* 0x3E1 */ u8 mTimerVisible; /* 0x3E2 */ u8 field_0x3e2; +#if TARGET_PC + bool mPresentStep = false; + bool mPresentOpening = false; + bool mPresentClosing = false; + bool mPresentTimerSlide = false; +#endif }; class dTimer_c : public msg_class { diff --git a/include/helpers/alignment.hpp b/include/helpers/alignment.hpp new file mode 100644 index 0000000000..54f46b0678 --- /dev/null +++ b/include/helpers/alignment.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace dusk::helpers { + +/** + * Read data from an address that may not be aligned properly. + * @tparam T Type of data to read. + * @param ptr Address to read from. + * @return The copied value. + */ +template + requires std::is_trivially_copyable_v +[[nodiscard]] constexpr T read_unaligned(u8 const* ptr) { + T copy; + memcpy(©, ptr, sizeof(T)); + return copy; +} + +} // namespace dusk::helpers diff --git a/include/helpers/cast.hpp b/include/helpers/cast.hpp new file mode 100644 index 0000000000..ef6d8421a6 --- /dev/null +++ b/include/helpers/cast.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +/** + * Helper functions for performing casts. + */ +namespace dusk::helpers::cast { +/** + * Implementation details of dusk::helpers::cast. + */ +namespace _impl { +[[noreturn]] void overrun_high(); +[[noreturn]] void overrun_low(); +} // namespace _impl + +template +concept IntCastable = std::is_integral_v && std::is_trivially_copyable_v; + +/** + * Helper type that allows easily casting between integer types, + * that safely aborts if an overflow were to occur. + * Usage: + * @code + * size_t foobar = 20; + * int real = bounded_cast(foobar); + * @endcode + */ +template +struct bounded_cast { + Source src; + + template + [[nodiscard]] constexpr operator Target() const { + if (std::cmp_greater(src, std::numeric_limits::max())) [[unlikely]] { + _impl::overrun_high(); + } + + if (std::cmp_less(src, std::numeric_limits::min())) [[unlikely]] { + _impl::overrun_low(); + } + + return static_cast(src); + } +}; +} // namespace dusk::helpers::cast diff --git a/include/helpers/endian.h b/include/helpers/endian.h index 689f9f0dab..b9727ce4c7 100644 --- a/include/helpers/endian.h +++ b/include/helpers/endian.h @@ -28,35 +28,96 @@ #define BSWAP32(x) (x) #endif +// clang-format off +// yeah ofc Microsoft's _byteswap_* aren't constexpr like the GCC/Clang ones. +constexpr u16 be16_manual(u16 val) +{ + return (val >> 8) | + (val << 8); +} + +constexpr u32 be32_manual(u32 val) +{ + return (val >> 24) | + ((val << 8) & 0x00FF0000) | + ((val >> 8) & 0x0000FF00) | + (val << 24); +} + +constexpr u64 be64_manual(u64 val) +{ + return (val >> 56) | + ((val << 40) & 0x00FF000000000000) | + ((val << 24) & 0x0000FF0000000000) | + ((val << 8) & 0x000000FF00000000) | + ((val >> 8) & 0x00000000FF000000) | + ((val >> 24) & 0x0000000000FF0000) | + ((val >> 40) & 0x000000000000FF00) | + (val << 56); +} +// clang-format on + // Big-Endian to Host conversion -inline u16 be16(u16 val) { return BSWAP16(val); } -inline s16 be16s(s16 val) { return (s16)BSWAP16((u16)val); } -inline u32 be32(u32 val) { return BSWAP32(val); } -inline s32 be32s(s32 val) { return (s32)BSWAP32((u32)val); } -inline u64 be64(u64 val) { return BSWAP64(val); } -inline s64 be64s(s64 val) { return (s64)BSWAP64((u64)val); } +// _manual is behind std::is_constant_evaluated() checks, +// to avoid pessimizing debug perf too much. +constexpr u16 be16(u16 val) { + if (std::is_constant_evaluated()) { + return be16_manual(val); + } + return BSWAP16(val); +} +constexpr s16 be16s(s16 val) { + if (std::is_constant_evaluated()) { + return (s16)be16_manual((u16)val); + } + return (s16)BSWAP16((u16)val); +} +constexpr u32 be32(u32 val) { + if (std::is_constant_evaluated()) { + return be32_manual(val); + } + return BSWAP32(val); +} +constexpr s32 be32s(s32 val) { + if (std::is_constant_evaluated()) { + return (s32)be32_manual((u32)val); + } + return (s32)BSWAP32((u32)val); +} +constexpr u64 be64(u64 val) { + if (std::is_constant_evaluated()) { + return be64_manual(val); + } + return BSWAP64(val); +} +constexpr s64 be64s(s64 val) { + if (std::is_constant_evaluated()) { + return (s64)be64_manual((u64)val); + } + return (s64)BSWAP64((u64)val); +} #ifdef TARGET_PC // Helper wrappers so code below reads nicely: -static inline u16 RES_U16(u16 v) { +constexpr u16 RES_U16(u16 v) { return be16(v); } -static inline s16 RES_S16(s16 v) { +constexpr s16 RES_S16(s16 v) { return be16s(v); } -static inline u32 RES_U32(u32 v) { +constexpr u32 RES_U32(u32 v) { return be32(v); } -static inline s32 RES_S32(s32 v) { +constexpr s32 RES_S32(s32 v) { return be32s(v); } -static inline u64 RES_U64(u64 v) { +constexpr u64 RES_U64(u64 v) { return be64(v); } -static inline s64 RES_S64(s64 v) { +constexpr s64 RES_S64(s64 v) { return be64s(v); } -static inline f32 RES_F32(f32 v) { +constexpr f32 RES_F32(f32 v) { return std::bit_cast(RES_S32(std::bit_cast(v))); } #else @@ -76,33 +137,33 @@ static inline f32 RES_F32(f32 v) { template struct BE { T inner; - BE() = default; - BE(const T& from) { + constexpr BE() noexcept = default; + constexpr BE(const T& from) noexcept { inner = swap(from); } // post-ops - T operator--(int) { + constexpr T operator--(int) { T orig = inner; *this -= 1; return swap(orig); } - T operator++(int) { + constexpr T operator++(int) { T orig = inner; *this += 1; return swap(orig); } - operator T() const { + constexpr operator T() const noexcept { return swap(inner); } - T host[[nodiscard]]() const { + constexpr T host[[nodiscard]]() const noexcept { return swap(inner); } - static T swap[[nodiscard]](T val); + static constexpr T swap[[nodiscard]](T val) noexcept; }; #define BIN_ASSIGN_OP(op) \ @@ -124,44 +185,43 @@ BIN_ASSIGN_OP(^=); #undef BIN_ASSIGN_OP - template<> -inline u16 BE::swap(u16 val) { +constexpr u16 BE::swap(u16 val) noexcept { return RES_U16(val); } template<> -inline s16 BE::swap(s16 val) { +constexpr s16 BE::swap(s16 val) noexcept { return RES_S16(val); } template<> -inline u32 BE::swap(u32 val) { +constexpr u32 BE::swap(u32 val) noexcept { return RES_U32(val); } template<> -inline s32 BE::swap(s32 val) { +constexpr s32 BE::swap(s32 val) noexcept { return RES_S32(val); } template<> -inline s64 BE::swap(s64 val) { +constexpr s64 BE::swap(s64 val) noexcept { return RES_S64(val); } template<> -inline u64 BE::swap(u64 val) { +constexpr u64 BE::swap(u64 val) noexcept { return RES_U64(val); } template<> -inline f32 BE::swap(f32 val) { +constexpr f32 BE::swap(f32 val) noexcept { return RES_F32(val); } template<> -inline S16Vec BE::swap(S16Vec val) { +constexpr S16Vec BE::swap(S16Vec val) noexcept { return { BE::swap(val.x), BE::swap(val.y), @@ -175,23 +235,23 @@ struct BE { BE y; BE z; - BE() = default; - BE(f32 x, f32 y, f32 z) { + constexpr BE() noexcept = default; + constexpr BE(f32 x, f32 y, f32 z) noexcept { this->x = x; this->y = y; this->z = z; } - BE(const Vec& from) { + constexpr BE(const Vec& from) noexcept { x = from.x; y = from.y; z = from.z; } - operator Vec() const { + constexpr operator Vec() const noexcept { return { x, y, z }; } - static Vec swap(Vec val) { + constexpr static Vec swap(Vec val) noexcept { return { BE::swap(val.x), BE::swap(val.y), @@ -204,7 +264,7 @@ template <> struct BE { BE contents[4][4]; - auto& operator[](int x) const { + constexpr auto& operator[](int x) const noexcept { return contents[x]; } }; @@ -213,11 +273,11 @@ template <> struct BE { BE contents[3][4]; - auto& operator[](int x) const { + constexpr auto& operator[](int x) const noexcept { return contents[x]; } - void to_host(Mtx& mtx) const { + constexpr void to_host(Mtx& mtx) const noexcept { for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { mtx[i][j] = contents[i][j]; @@ -231,15 +291,15 @@ template <> struct BE { BE contents[2][3]; - auto& operator[](int x) { + constexpr auto& operator[](int x) noexcept { return contents[x]; } - auto& operator[](int x) const { + constexpr auto& operator[](int x) const noexcept { return contents[x]; } - void to_host(Mtx23& mtx) const { + constexpr void to_host(Mtx23& mtx) const noexcept { for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { mtx[i][j] = contents[i][j]; @@ -249,26 +309,26 @@ struct BE { }; template -void be_swap(T& val) { +constexpr void be_swap(T& val) noexcept { val = BE::swap(val); } template -void be_swap(T (& val)[N]) { +constexpr void be_swap(T (& val)[N]) noexcept { for (u32 i = 0; i < N; i++) { be_swap(val[i]); } } template -void be_swap(T array[], const u32 size) { +constexpr void be_swap(T array[], const u32 size) noexcept { for (u32 i = 0; i < size; i++) { be_swap(array[i]); } } template<> -inline void be_swap(Mtx44& val) { +constexpr void be_swap(Mtx44& val) noexcept { for (auto & x : val) { for (float & y : x) { be_swap(y); @@ -277,7 +337,7 @@ inline void be_swap(Mtx44& val) { } template<> -inline void be_swap(Mtx& val) { +constexpr void be_swap(Mtx& val) noexcept { for (auto & x : val) { for (float & y : x) { be_swap(y); @@ -285,6 +345,7 @@ inline void be_swap(Mtx& val) { } } +#define LE(T) T #define BE(T) BE #define BE_HOST(T) (T.host()) #else diff --git a/include/helpers/endian_gx.hpp b/include/helpers/endian_gx.hpp index 1193cf1d7c..201e667844 100644 --- a/include/helpers/endian_gx.hpp +++ b/include/helpers/endian_gx.hpp @@ -3,12 +3,28 @@ #include "dolphin/gx/GXStruct.h" #include "endian.h" + +#define IMPL_ENUM(type) \ +template <> \ +constexpr type BE::swap(type val) noexcept { \ + return static_cast(be32(val)); \ +} + +IMPL_ENUM(GXCullMode); +IMPL_ENUM(GXAttr); +IMPL_ENUM(GXAttrType); +IMPL_ENUM(GXCompType); +IMPL_ENUM(GXCompCnt); + +#undef IMPL_ENUM + + template <> struct BE { BE attr; BE type; - static GXVtxDescList swap[[nodiscard]](GXVtxDescList val); + constexpr static GXVtxDescList swap[[nodiscard]](GXVtxDescList val) noexcept; }; template <> @@ -18,5 +34,31 @@ struct BE { BE type; u8 frac; - static GXVtxAttrFmtList swap[[nodiscard]](GXVtxAttrFmtList val); + constexpr static GXVtxAttrFmtList swap[[nodiscard]](GXVtxAttrFmtList val) noexcept; }; + +template <> +constexpr GXColorS10 BE::swap(GXColorS10 val) noexcept { + return { + be16s(val.r), + be16s(val.g), + be16s(val.b), + be16s(val.a), + }; +} + +constexpr GXVtxDescList BE::swap(GXVtxDescList val) noexcept { + return { + BE::swap(val.attr), + BE::swap(val.type), + }; +} + +constexpr GXVtxAttrFmtList BE::swap(GXVtxAttrFmtList val) noexcept { + return { + BE::swap(val.attr), + BE::swap(val.cnt), + BE::swap(val.type), + val.frac + }; +} diff --git a/include/helpers/endian_ssystem.h b/include/helpers/endian_ssystem.h index b383ef6e43..cea10ea2df 100644 --- a/include/helpers/endian_ssystem.h +++ b/include/helpers/endian_ssystem.h @@ -60,3 +60,11 @@ struct BE { }; } }; + +template<> +constexpr cXy BE::swap(cXy val) noexcept { + return { + BE::swap(val.x), + BE::swap(val.y), + }; +} diff --git a/include/m_Do/m_Do_ext.h b/include/m_Do/m_Do_ext.h index bd1baac2df..83a6a78230 100644 --- a/include/m_Do/m_Do_ext.h +++ b/include/m_Do/m_Do_ext.h @@ -80,7 +80,13 @@ public: int remove(J3DModelData* i_modelData) { return i_modelData->removeTexMtxAnimator(mpAnm); } void entryFrame() { entryFrame(getFrame()); } - void entryFrame(f32 frame) { mpAnm->setFrame(frame); } +#if TARGET_PC + void entryFrame(f32 frame); +#else + void entryFrame(f32 frame) { + mpAnm->setFrame(frame); + } +#endif J3DAnmTextureSRTKey* getBtkAnm() const { return mpAnm; } @@ -110,7 +116,13 @@ public: int remove(J3DModelData* i_modelData) { return i_modelData->removeTevRegAnimator(mpAnm); } void entryFrame() { entryFrame(getFrame()); } - void entryFrame(f32 frame) { mpAnm->setFrame(frame); } +#if TARGET_PC + void entryFrame(f32 frame); +#else + void entryFrame(f32 frame) { + mpAnm->setFrame(frame); + } +#endif J3DAnmTevRegKey* getBrkAnm() const { return mpAnm; } @@ -542,10 +554,18 @@ public: virtual void setMaterial() = 0; virtual void draw() = 0; #if TARGET_PC - virtual void refreshGeometryForPresentationEye(const cXyz& eye) {} + virtual void captureInterpPoints() {} + virtual void refreshGeometryForPresentation() {} #endif /* 0x4 */ mDoExt_3DlineMat_c* field_0x4; + +#if TARGET_PC +protected: + u8 mInterpKind; + f32 mInterpWidth; + u16 mInterpTaper; +#endif }; class mDoExt_3DlineMat0_c : public mDoExt_3DlineMat_c { @@ -567,6 +587,10 @@ public: virtual int getMaterialID() { return 0; } virtual void setMaterial(); virtual void draw(); +#if TARGET_PC + void captureInterpPoints() override; + void refreshGeometryForPresentation() override; +#endif cXyz* getPos(int param_0) { return field_0x18[param_0].field_0x0; } f32* getSize(int param_0) { return field_0x18[param_0].field_0x4; } @@ -585,18 +609,14 @@ class dKy_tevstr_c; class mDoExt_3DlineMat1_c : public mDoExt_3DlineMat_c { public: int init(u16, u16, ResTIMG*, int); -#if TARGET_PC - void update(int, GXColor&, dKy_tevstr_c*, const cXyz* presentationEye = nullptr); - void update(int, f32, GXColor&, u16, dKy_tevstr_c*, const cXyz* presentationEye = nullptr); -#else void update(int, GXColor&, dKy_tevstr_c*); void update(int, f32, GXColor&, u16, dKy_tevstr_c*); -#endif int getMaterialID() { return 1; } void setMaterial(); void draw(); #if TARGET_PC - void refreshGeometryForPresentationEye(const cXyz& eye) override; + void captureInterpPoints() override; + void refreshGeometryForPresentation() override; #endif cXyz* getPos(int i_idx) { return mpLines[i_idx].field_0x0; } @@ -611,11 +631,6 @@ private: /* 0x34 */ u16 field_0x34; /* 0x36 */ u8 mIsDrawn; /* 0x38 */ mDoExt_3Dline_c* mpLines; -#if TARGET_PC - u8 mInterpLineKind; - f32 mInterpLineF; - u16 mInterpLineU16; -#endif }; class mDoExt_3DlineMat2_c : public mDoExt_3DlineMat1_c { diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DManage.h b/libs/JSystem/include/JSystem/J2DGraph/J2DManage.h index 27218d0d0d..3eb1967e2c 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DManage.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DManage.h @@ -3,6 +3,10 @@ #include +#if TARGET_PC + #include "helpers/endian.h" +#endif + class JSUInputStream; /** diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DAnimation.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DAnimation.h index f006183d07..80af43182b 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DAnimation.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DAnimation.h @@ -6,9 +6,9 @@ #include #include "global.h" +#if TARGET_PC #include "helpers/endian.h" -#if TARGET_PC #define OFFSET_PTR_V0 BE(u32) #else #define OFFSET_PTR_V0 void* @@ -478,27 +478,45 @@ struct J3DAnmClusterFullTable { */ class J3DAnmBase { public: +#if TARGET_PC + J3DAnmBase(); +#else J3DAnmBase() { mAttribute = 0; field_0x5 = 0; mFrameMax = 0; mFrame = 0.0f; } +#endif +#if TARGET_PC + J3DAnmBase(s16 frameMax); +#else J3DAnmBase(s16 frameMax) { mAttribute = 0; field_0x5 = 0; mFrameMax = frameMax; mFrame = 0.0f; } +#endif +#if TARGET_PC + virtual ~J3DAnmBase(); +#else virtual ~J3DAnmBase() {} +#endif virtual s32 getKind() const = 0; u8 getAttribute() const { return mAttribute; } s16 getFrameMax() const { return mFrameMax; } f32 getFrame() const { return mFrame; } - void setFrame(f32 frame) { mFrame = frame; } +#if TARGET_PC + void setFrame(f32 frame); +#else + void setFrame(f32 frame) { + mFrame = frame; + } +#endif /* 0x4 */ u8 mAttribute; /* 0x5 */ u8 field_0x5; @@ -960,30 +978,66 @@ public: void init(int endFrame) { init((s16)endFrame); } BOOL checkPass(f32); void update(); +#if TARGET_PC + virtual ~J3DFrameCtrl(); +#else virtual ~J3DFrameCtrl() {} +#endif u8 getAttribute() const { return mAttribute; } - void setAttribute(u8 attr) { mAttribute = attr; } +#if TARGET_PC + void setAttribute(u8 attr); +#else + void setAttribute(u8 attr) { + mAttribute = attr; + } +#endif u8 getState() const { return mState; } bool checkState(u8 state) const { return mState & state ? true : false; } s16 getStart() const { return mStart; } +#if TARGET_PC + void setStart(s16 start); +#else void setStart(s16 start) { mStart = start; mFrame = start; } +#endif s16 getEnd() const { return mEnd; } - void setEnd(s16 end) { mEnd = end; } +#if TARGET_PC + void setEnd(s16 end); +#else + void setEnd(s16 end) { + mEnd = end; + } +#endif s16 getLoop() const { return mLoop; } - void setLoop(s16 loop) { mLoop = loop; } +#if TARGET_PC + void setLoop(s16 loop); +#else + void setLoop(s16 loop) { + mLoop = loop; + } +#endif f32 getRate() const { return mRate; } void setRate(f32 rate) { mRate = rate; } f32 getFrame() const { return mFrame; } - void setFrame(f32 frame) { mFrame = frame; } +#if TARGET_PC + void setFrame(f32 frame); +#else + void setFrame(f32 frame) { + mFrame = frame; + } +#endif +#if TARGET_PC + void reset(); +#else void reset() { mFrame = mStart; mRate = 1.0f; mState = 0; } +#endif /* 0x04 */ u8 mAttribute; /* 0x05 */ u8 mState; diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DMaterialAnm.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DMaterialAnm.h index 4c3f0f75bb..94d3315d2e 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DMaterialAnm.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DMaterialAnm.h @@ -31,10 +31,15 @@ public: void setAnmFlag(bool flag) { mAnmFlag = flag; } bool getAnmFlag() const { return mAnmFlag; } +#if TARGET_PC + void calc(GXColor* pColor) const; + J3DAnmColor* getAnimation() const { return mAnmColor; } +#else void calc(GXColor* pColor) const { J3D_ASSERT_NULLPTR(507, pColor != NULL); mAnmColor->getColor(field_0x0, pColor); } +#endif private: /* 0x0 */ u16 field_0x0; @@ -66,10 +71,15 @@ public: void setAnmFlag(bool flag) { mAnmFlag = flag; } +#if TARGET_PC + void calc(J3DTextureSRTInfo* pSRTInfo) const; + J3DAnmTextureSRTKey* getAnimation() const { return mAnmTransform; } +#else void calc(J3DTextureSRTInfo* pSRTInfo) const { J3D_ASSERT_NULLPTR(519, pSRTInfo != NULL); mAnmTransform->getTransform(field_0x0, pSRTInfo); } +#endif bool getAnmFlag() const { return mAnmFlag; } @@ -108,6 +118,9 @@ public: void setAnmFlag(bool flag) { mAnmFlag = flag; } bool getAnmFlag() const { return mAnmFlag; } +#if TARGET_PC + J3DAnmTexPattern* getAnimation() const { return mAnmTexPattern; } +#endif J3DAnmTexPattern* getAnmTexPattern() { return mAnmTexPattern; } private: @@ -141,10 +154,15 @@ public: void setAnmFlag(bool flag) { mAnmFlag = flag; } bool getAnmFlag() const { return mAnmFlag; } +#if TARGET_PC + void calc(GXColorS10* pColor) const; + J3DAnmTevRegKey* getAnimation() const { return mAnmTevReg; } +#else void calc(GXColorS10* pColor) const { J3D_ASSERT_NULLPTR(545, pColor != NULL); mAnmTevReg->getTevColorReg(field_0x0, pColor); } +#endif private: /* 0x0 */ u16 field_0x0; @@ -177,10 +195,15 @@ public: void setAnmFlag(bool flag) { mAnmFlag = flag; } bool getAnmFlag() const { return mAnmFlag; } +#if TARGET_PC + void calc(GXColor* pColor) const; + J3DAnmTevRegKey* getAnimation() const { return mAnmTevReg; } +#else void calc(GXColor* pColor) const { J3D_ASSERT_NULLPTR(558, pColor != NULL); mAnmTevReg->getTevKonstReg(field_0x0, pColor); } +#endif private: /* 0x0 */ u16 field_0x0; @@ -197,6 +220,9 @@ public: J3DMaterialAnm() { initialize(); } void initialize(); +#if TARGET_PC + bool hasMaterialAnimation() const; +#endif void setMatColorAnm(int, J3DMatColorAnm*); void setTexMtxAnm(int, J3DTexMtxAnm*); void setTexNoAnm(int, J3DTexNoAnm*); @@ -207,6 +233,12 @@ public: virtual void calc(J3DMaterial*) const; const J3DTexMtxAnm& getTexMtxAnm(int i) const { return mTexMtxAnm[i]; } +#if TARGET_PC + const J3DMatColorAnm& getMatColorAnm(int i) const { return mMatColorAnm[i]; } + const J3DTexNoAnm& getTexNoAnm(int i) const { return mTexNoAnm[i]; } + const J3DTevColorAnm& getTevColorAnm(int i) const { return mTevColorAnm[i]; } + const J3DTevKColorAnm& getTevKColorAnm(int i) const { return mTevKColorAnm[i]; } +#endif private: /* 0x04 */ J3DMatColorAnm mMatColorAnm[2]; diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h index d55bb82465..042dc81dd3 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h @@ -79,7 +79,8 @@ public: virtual ~J3DModel() {} #if TARGET_PC - static void interp_callback(bool isSimFrame, void* pUserWork); + void calc_presentation_base_mtx(); + void prepare_presentation_view(); #endif J3DModelData* getModelData() { return mModelData; } @@ -107,6 +108,7 @@ public: Vec* getBaseScale() { return &mBaseScale; } #if TARGET_PC void setAnmMtx(int jointNo, Mtx m); + void forgetMtx(); #else void setAnmMtx(int jointNo, Mtx m) { mMtxBuffer->setAnmMtx(jointNo, m); @@ -133,6 +135,9 @@ public: /* 0xD0 */ J3DVtxColorCalc* mVtxColorCalc; /* 0xD4 */ J3DUnkCalc1* mUnkCalc1; /* 0xD8 */ J3DUnkCalc2* mUnkCalc2; +#if TARGET_PC + Mtx mPresentationBase; +#endif }; #endif /* J3DMODEL_H */ diff --git a/libs/JSystem/include/JSystem/JAudio2/JAIAudience.h b/libs/JSystem/include/JSystem/JAudio2/JAIAudience.h index 1b565a069e..20d700097c 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAIAudience.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAIAudience.h @@ -3,6 +3,10 @@ #include "JSystem/JGeometry.h" +namespace dusk::mods::svc::audio_res::bst { +struct SoundTableReplacementSlot; +} + class JAIAudible; class JAISoundID; struct JASSoundParams; @@ -14,7 +18,8 @@ struct JASSoundParams; struct JAIAudience { virtual ~JAIAudience(); virtual JAIAudible* newAudible(JGeometry::TVec3 const&, JAISoundID, - JGeometry::TVec3 const*, u32) = 0; + JGeometry::TVec3 const*, u32 + IF_DUSK_ARG(dusk::mods::svc::audio_res::bst::SoundTableReplacementSlot const*)) = 0; virtual int getMaxChannels() = 0; virtual void deleteAudible(JAIAudible*) = 0; virtual u32 calcPriority(JAIAudible*) = 0; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAISe.h b/libs/JSystem/include/JSystem/JAudio2/JAISe.h index 408a84703a..2ff990bec0 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAISe.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAISe.h @@ -43,7 +43,7 @@ public: void startTrack_(const JASSoundParams& params); void JAISeCategoryMgr_mixOut_(bool, const JASSoundParams& params, JAISoundActivity activity); void JAISeCategoryMgr_calc_(); - void JAISeMgr_startID_(JAISoundID id, const JGeometry::TVec3* posPtr, JAIAudience* audience); + void JAISeMgr_startID_(JAISoundID id, const JGeometry::TVec3* posPtr, JAIAudience* audience IF_DUSK_ARG(std::shared_ptr replacement)); bool prepare_getSeqData_(); void prepare_(); diff --git a/libs/JSystem/include/JSystem/JAudio2/JAISeMgr.h b/libs/JSystem/include/JSystem/JAudio2/JAISeMgr.h index 928d85525b..57fdbd114a 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAISeMgr.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAISeMgr.h @@ -104,7 +104,7 @@ public: JAISe* newSe_(int category, u32 priority); void calc(); void mixOut(); - bool startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry::TVec3* posPtr); + bool startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry::TVec3* posPtr IF_DUSK_ARG(std::shared_ptr replacement)); int getNumActiveSe() const; /* 0x004 */ JAISoundActivity mSoundActivity; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAISound.h b/libs/JSystem/include/JSystem/JAudio2/JAISound.h index fa262a7426..9dbd84d35a 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAISound.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAISound.h @@ -6,7 +6,16 @@ #include "JSystem/JUtility/JUTAssert.h" #include "global.h" #include "helpers/endian.h" -#include + +#if TARGET_PC +#include + +namespace dusk::mods::svc::audio_res::bst { +struct SoundTableReplacementSlot; +struct SoundEffectReplacementSlot; +struct StreamReplacementSlot; +} +#endif class JAISound; @@ -101,7 +110,7 @@ struct JAISoundStatus_ { bool isMute() const { return field_0x0.flags.mute; } bool isPaused() const { return field_0x0.flags.paused; } void pauseWhenOut() { - field_0x1.flags.flag3 = 1; + field_0x1.flags.mPauseWhenOut = 1; } /* 0x0 */ union { @@ -120,9 +129,9 @@ struct JAISoundStatus_ { /* 0x1 */ union { u8 value; struct { - u8 flag1 : 1; + u8 mComesBack : 1; u8 flag2 : 1; - u8 flag3 : 1; + u8 mPauseWhenOut : 1; u8 flag4 : 1; u8 flag5 : 1; u8 flag6 : 1; @@ -271,10 +280,16 @@ class JAITempoMgr; */ class JAISound { public: +#if TARGET_PC + using SoundTableReplacementSlot = dusk::mods::svc::audio_res::bst::SoundTableReplacementSlot; + using SoundEffectReplacementSlot = dusk::mods::svc::audio_res::bst::SoundEffectReplacementSlot; + using StreamReplacementSlot = dusk::mods::svc::audio_res::bst::StreamReplacementSlot; +#endif + void releaseHandle(); void attachHandle(JAISoundHandle* handle); JAISound(); - void start_JAISound_(JAISoundID id, const JGeometry::TVec3* posPtr, JAIAudience* audience); + void start_JAISound_(JAISoundID id, const JGeometry::TVec3* posPtr, JAIAudience* audience IF_DUSK_ARG(std::shared_ptr replacement)); bool acceptsNewAudible() const; void newAudible(const JGeometry::TVec3&, JGeometry::TVec3 const*, u32, JAIAudience*); @@ -297,6 +312,11 @@ public: virtual bool JAISound_tryDie_() = 0; JAISoundID getID() const { return soundID_; } +#if TARGET_PC + SoundTableReplacementSlot* getReplacement() const { + return replacement.get(); + } +#endif u32 getAnimationState() const { return status_.state.flags.animationState; } bool isAnimated() const { return getAnimationState() != 0; } void setAnimationState(u32 state) { @@ -309,7 +329,7 @@ public: bool hasLifeTime() const { return status_.field_0x1.flags.flag2; } void removeLifeTime_() { - status_.field_0x1.flags.flag1 = false; + status_.field_0x1.flags.mComesBack = false; status_.field_0x1.flags.flag2 = 0; } @@ -346,7 +366,7 @@ public: void setComesBack(bool param_0) { JUT_ASSERT(354, status_.state.flags.calcedOnce == 0); - status_.field_0x1.flags.flag1 = 1; + status_.field_0x1.flags.mComesBack = 1; if (param_0) { status_.pauseWhenOut(); } @@ -380,6 +400,9 @@ public: /* 0x34 */ u32 priority_; /* 0x38 */ s32 count_; /* 0x3C */ JAISoundParams params_; +#if TARGET_PC + std::shared_ptr replacement; +#endif }; // Size: 0x98 STATIC_ASSERT(sizeof(JAISound) == 0x98); diff --git a/libs/JSystem/include/JSystem/JAudio2/JAISoundInfo.h b/libs/JSystem/include/JSystem/JAudio2/JAISoundInfo.h index 4f536a0f63..23713d96a9 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAISoundInfo.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAISoundInfo.h @@ -9,13 +9,17 @@ * */ struct JAISoundInfo : public JASGlobalInstance { + IF_DUSK(using SoundTableReplacementSlot = dusk::mods::svc::audio_res::bst::SoundTableReplacementSlot); + IF_DUSK(using SoundEffectReplacementSlot = dusk::mods::svc::audio_res::bst::SoundEffectReplacementSlot); + IF_DUSK(using StreamReplacementSlot = dusk::mods::svc::audio_res::bst::StreamReplacementSlot); + JAISoundInfo(bool); virtual int getSoundType(JAISoundID) const = 0; virtual int getCategory(JAISoundID) const = 0; - virtual u32 getPriority(JAISoundID) const = 0; - virtual void getSeInfo(JAISoundID, JAISe*) const = 0; + virtual u32 getPriority(JAISoundID IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) const = 0; + virtual void getSeInfo(JAISoundID, JAISe* IF_DUSK_ARG(SoundEffectReplacementSlot const* replacement)) const = 0; virtual void getSeqInfo(JAISoundID, JAISeq*) const = 0; - virtual void getStreamInfo(JAISoundID, JAIStream*) const = 0; + virtual void getStreamInfo(JAISoundID, JAIStream* IF_DUSK_ARG(StreamReplacementSlot const* replacement)) const = 0; virtual ~JAISoundInfo(); }; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAISoundParams.h b/libs/JSystem/include/JSystem/JAudio2/JAISoundParams.h index 22d175154a..eccdc8deac 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAISoundParams.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAISoundParams.h @@ -9,14 +9,14 @@ */ struct JAISoundParamsProperty { void init() { - field_0x0 = 1.0f; - field_0x4 = 0.0f; - field_0x8 = 1.0f; + mVolume = 1.0f; + mFxMix = 0.0f; + mPitch = 1.0f; } - /* 0x00 */ f32 field_0x0; - /* 0x04 */ f32 field_0x4; - /* 0x08 */ f32 field_0x8; + /* 0x00 */ f32 mVolume; + /* 0x04 */ f32 mFxMix; + /* 0x08 */ f32 mPitch; }; // Size: 0xC /** diff --git a/libs/JSystem/include/JSystem/JAudio2/JAIStream.h b/libs/JSystem/include/JSystem/JAudio2/JAIStream.h index d6d6694be0..31ef4da4f5 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAIStream.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAIStream.h @@ -20,7 +20,7 @@ public: JAIStream(JAIStreamMgr* streamMgr, JAISoundStrategyMgr* soundStrategyMgr); void JAIStreamMgr_startID_(JAISoundID id, s32 streamFileEntry, const JGeometry::TVec3* posPtr, JAIAudience* audience, - int category); + int category IF_DUSK_ARG(std::shared_ptr replacement)); bool prepare_prepareStream_(); void prepare_(); void prepare_startStream_(); diff --git a/libs/JSystem/include/JSystem/JAudio2/JAIStreamDataMgr.h b/libs/JSystem/include/JSystem/JAudio2/JAIStreamDataMgr.h index eee03cc168..d528a25f85 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAIStreamDataMgr.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAIStreamDataMgr.h @@ -8,7 +8,9 @@ * */ struct JAIStreamDataMgr { - virtual s32 getStreamFileEntry(JAISoundID) = 0; + IF_DUSK(using StreamReplacementSlot2 = dusk::mods::svc::audio_res::bst::StreamReplacementSlot); + + virtual s32 getStreamFileEntry(JAISoundID IF_DUSK_ARG(StreamReplacementSlot2 const*)) = 0; virtual ~JAIStreamDataMgr(); }; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAIStreamMgr.h b/libs/JSystem/include/JSystem/JAudio2/JAIStreamMgr.h index 7c8e68a852..27d69d57df 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAIStreamMgr.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAIStreamMgr.h @@ -15,7 +15,7 @@ struct JAIStreamDataMgr; class JAIStreamMgr : public JASGlobalInstance { public: JAIStreamMgr(bool setInstance); - bool startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry::TVec3* posPtr); + bool startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry::TVec3* posPtr IF_DUSK_ARG(std::shared_ptr replacement)); void freeDeadStream_(); void calc(); void stop(); diff --git a/libs/JSystem/include/JSystem/JAudio2/JASBasicInst.h b/libs/JSystem/include/JSystem/JAudio2/JASBasicInst.h index 5fe6b84db4..fdd141a5c5 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASBasicInst.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASBasicInst.h @@ -14,20 +14,20 @@ const int OSC_MAX = 2; */ struct JASInstParam : public JASSoundParams { JASInstParam() { - field_0x14 = NULL; - field_0x18 = 0; - field_0x1a = 0; - field_0x1c = 0; - field_0x1d = 0; - field_0x1e = 0; + mOscillators = NULL; + mDirectRelease = 0; + mWaveId = 0; + mChannelType = 0; + mOscillatorCount = 0; + mDontSetKey = 0; } - JASOscillator::Data** field_0x14; - u16 field_0x18; - u16 field_0x1a; - u8 field_0x1c; - u8 field_0x1d; - u8 field_0x1e; + JASOscillator::Data** mOscillators; + u16 mDirectRelease; + u16 mWaveId; + u8 mChannelType; // Always CHANNEL_WAVE in practice + u8 mOscillatorCount; + u8 mDontSetKey; }; /** @@ -52,10 +52,10 @@ struct JASBasicInst : public JASInst { void setHighKey(int key) { mHighKey = key; } /* 0x0 */ s32 mHighKey; - /* 0x4 */ u16 field_0x4; + /* 0x4 */ u16 mWaveId; /* 0x6 */ u16 field_0x6; - /* 0x8 */ f32 field_0x8; - /* 0xC */ f32 field_0xc; + /* 0x8 */ f32 mVolumeMult; + /* 0xC */ f32 mPitchMult; }; JASBasicInst(); @@ -74,7 +74,7 @@ struct JASBasicInst : public JASInst { /* 0x04 */ f32 mVolume; /* 0x08 */ f32 mPitch; - /* 0x0C */ JASOscillator::Data const* field_0xc[OSC_MAX]; + /* 0x0C */ JASOscillator::Data const* mOscillators[OSC_MAX]; /* 0x10 */ u32 mKeymapCount; /* 0x14 */ TKeymap* mKeymap; }; diff --git a/libs/JSystem/include/JSystem/JAudio2/JASBasicWaveBank.h b/libs/JSystem/include/JSystem/JAudio2/JASBasicWaveBank.h index f6a10fa0c7..ed4ddbf4cd 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASBasicWaveBank.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASBasicWaveBank.h @@ -12,21 +12,27 @@ struct JASBasicWaveBank : public JASWaveBank { struct TWaveHandle : public JASWaveHandle { TWaveHandle() { mHeap = NULL; } virtual intptr_t getWavePtr() const; - virtual const JASWaveInfo* getWaveInfo() const { return &field_0x4; } + virtual const JASWaveInfo* getWaveInfo() const { return &mWaveInfo; } +#if TARGET_PC + /** + * @see JASChannel::mAramBaseAddress + */ + [[nodiscard]] void const* getAramBaseAddress() const override { return nullptr; } +#endif bool compareHeap(JASHeap* heap) const { return mHeap == heap;} - /* 0x04 */ JASWaveInfo field_0x4; + /* 0x04 */ JASWaveInfo mWaveInfo; /* 0x28 */ JASHeap* mHeap; }; struct TGroupWaveInfo { TGroupWaveInfo() { - field_0x0 = 0xffff; - field_0x4 = -1; + waveId = 0xffff; + mOffsetStart = -1; } - /* 0x00 */ u16 field_0x0; - /* 0x04 */ int field_0x4; + /* 0x00 */ u16 waveId; + /* 0x04 */ int mOffsetStart; }; struct TWaveGroup : JASWaveArc { @@ -44,7 +50,7 @@ struct JASBasicWaveBank : public JASWaveBank { u32 getWaveCount() const { return mWaveCount; } }; - JASBasicWaveBank(); + JASBasicWaveBank(IF_DUSK(u32 bankId)); ~JASBasicWaveBank(); TWaveGroup* getWaveGroup(u32); void setGroupCount(u32, JKRHeap*); @@ -56,7 +62,7 @@ struct JASBasicWaveBank : public JASWaveBank { JASWaveArc* getWaveArc(u32 param_0) { return getWaveGroup(param_0); } u32 getArcCount() const { return mGroupCount; } - /* 0x04 */ OSMutex field_0x4; + /* 0x04 */ OSMutex mWaveTableMutex; /* 0x1C */ TWaveHandle* mWaveTable; /* 0x20 */ TWaveGroup* mWaveGroupArray; /* 0x24 */ u16 mHandleCount; diff --git a/libs/JSystem/include/JSystem/JAudio2/JASChannel.h b/libs/JSystem/include/JSystem/JAudio2/JASChannel.h index af922638da..77bfb45966 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASChannel.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASChannel.h @@ -8,6 +8,7 @@ #include "JSystem/JAudio2/JASWaveInfo.h" #include "JSystem/JAudio2/JASDSPInterface.h" #include +#include struct JASDSPChannel; @@ -15,6 +16,10 @@ namespace JASDsp { struct TChannel; } +#if TARGET_PC +#include "JSystem/JAudio2/JASSampleDataReference.h" +#endif + /** * @ingroup jsystem-jaudio * @@ -159,13 +164,30 @@ public: struct { u32 mChannelType; // CHANNEL_WAVE or CHANNEL_OSCILLATOR JASWaveInfo mWaveInfo; - } field_0xdc; + } mAnon; union { u32 mWaveAramAddress; u32 mOscillatorSomething; u32 field_0x104; }; +#if TARGET_PC + /** + * Memory address at which this sound effect should consider ARAM to start. + * + * By changing this, sound effects can effectively be played back from anywhere in memory, + * rather than just the emulated ARAM space. + * + * If nullptr, the regular emulated ARAM is used. + */ + void const* mAramBaseAddress; + + /** + * @see JASSampleDataReference + */ + std::unique_ptr mSampleReference; +#endif + static DUSK_GAME_DATA OSMessageQueue sBankDisposeMsgQ; static DUSK_GAME_DATA OSMessage sBankDisposeMsg[16]; static DUSK_GAME_DATA OSMessage sBankDisposeList[16]; diff --git a/libs/JSystem/include/JSystem/JAudio2/JASDSPInterface.h b/libs/JSystem/include/JSystem/JAudio2/JASDSPInterface.h index b413052da7..c45b72f2d8 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASDSPInterface.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASDSPInterface.h @@ -109,7 +109,9 @@ namespace JASDsp { * Pitch shift via changing playback speed. */ /* 0x004 */ u16 mPitch; +#if !TARGET_PC /* 0x006 */ short _unused1; +#endif /** * Set to 1 when playback starts, cleared by DSP later, @@ -118,19 +120,27 @@ namespace JASDsp { * (Corroborated by fields JASAramStream checks never being cleared explicitly by CPU.) */ /* 0x008 */ u16 mResetFlag; +#if !TARGET_PC /* 0x00A */ u8 _unused2[0x00C - 0x00A]; +#endif /* 0x00C */ s16 mPauseFlag; +#if !TARGET_PC /* 0x00E */ short _unused3; +#endif /* 0x010 */ OutputChannelConfig mOutputChannels[DSP_OUTPUT_CHANNELS]; +#if !TARGET_PC /* 0x040 */ u8 _unused4[0x050 - 0x040]; +#endif /* 0x050 */ u16 mAutoMixerPanDolby; // pan is upper 8 bits, dolby lower 8. /* 0x052 */ u16 mAutoMixerFxMix; /* 0x054 */ u16 mAutoMixerInitVolume; /* 0x056 */ u16 mAutoMixerVolume; /* 0x058 */ u16 mAutoMixerBeenSet; +#if !TARGET_PC /* 0x05A */ u8 _unused5[0x060 - 0x05A]; /* 0x060 */ short field_0x060; // Only cleared to zero, presumed used by DSP. /* 0x062 */ u8 _unused6[0x064 - 0x062]; +#endif /** * Samples per ADPCM frame for ADPCM audio. Seems just set to 1 for PCM formats. @@ -139,7 +149,9 @@ namespace JASDsp { /* 0x064 */ u16 mSamplesPerBlock; /* 0x066 */ short field_0x066; // Only cleared to zero, presumed used by DSP. /* 0x068 */ u32 mSamplePosition; // Only ever initialized by code, name is guess. +#if !TARGET_PC /* 0x06C */ u8 _unused7[0x070 - 0x06C]; +#endif /** * Current audio read position in ARAM. Updated by DSP. @@ -151,11 +163,13 @@ namespace JASDsp { * Gets written by DSP, but also CPU. */ /* 0x074 */ u32 mSamplesLeft; // Never directly cleared to zero. Seems sus. Cleared by DSP? +#if !TARGET_PC /* 0x078 */ short field_0x078[4]; // Only cleared to zero, presumed used by DSP. /* 0x080 */ short field_0x080[20]; // Only cleared to zero, presumed used by DSP. /* 0x0A8 */ short field_0x0a8[4]; // Only cleared to zero, presumed used by DSP. /* 0x0B0 */ u16 field_0x0b0[16]; // Only cleared to zero, presumed used by DSP. /* 0x0D0 */ u8 _unused8[0x100 - 0x0D0]; +#endif /* 0x100 */ u16 mBytesPerBlock; /* 0x102 */ u16 mLoopFlag; @@ -176,9 +190,26 @@ namespace JASDsp { /* 0x118 */ u32 mWaveAramAddress; /* 0x11C */ int mSampleCount; /* 0x120 */ s16 fir_filter_params[8]; +#if !TARGET_PC /* 0x130 */ u8 _unused9[0x148 - 0x130]; +#endif /* 0x148 */ s16 iir_filter_params[8]; +#if !TARGET_PC /* 0x158 */ u8 _unused10[0x180 - 0x158]; +#endif + +#if TARGET_PC + /** + * Memory address at which this sound effect should consider ARAM to start. + * This means fields like mWaveAramAddress will be relative to this pointer instead. + * + * By changing this, sound effects can effectively be played back from anywhere in memory, + * rather than just the emulated ARAM space. + * + * If nullptr, the regular emulated ARAM is used. + */ + void const* mAramBaseAddress; +#endif }; void boot(void (*)(void*)); diff --git a/libs/JSystem/include/JSystem/JAudio2/JASDrumSet.h b/libs/JSystem/include/JSystem/JAudio2/JASDrumSet.h index 8e5c8df244..f791b21865 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASDrumSet.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASDrumSet.h @@ -21,10 +21,10 @@ struct JASDrumSet : public JASInst { /* 0x00 */ f32 mVolume; /* 0x04 */ f32 mPitch; /* 0x08 */ f32 mPan; - /* 0x0C */ u16 field_0xc; - /* 0x0E */ u16 field_0xe; - /* 0x10 */ f32 field_0x10; - /* 0x14 */ f32 field_0x14; + /* 0x0C */ u16 mDirectRelease; + /* 0x0E */ u16 mWaveId; + /* 0x10 */ f32 mVolumeMult; + /* 0x14 */ f32 mPitchMult; }; JASDrumSet(); diff --git a/libs/JSystem/include/JSystem/JAudio2/JASSampleDataReference.h b/libs/JSystem/include/JSystem/JAudio2/JASSampleDataReference.h new file mode 100644 index 0000000000..d196b4f34c --- /dev/null +++ b/libs/JSystem/include/JSystem/JAudio2/JASSampleDataReference.h @@ -0,0 +1,18 @@ +#ifndef DUSKLIGHT_JASSAMPLEDATAREFERENCE_H +#define DUSKLIGHT_JASSAMPLEDATAREFERENCE_H + +#if TARGET_PC +/** + * An object to manage the lifetime of the sample data pointed to by JASChannel. + * + * JASChannel can (optionally) accept an unique_ptr to an object to this type, + * in which case it will be destroyed when the JASChannel is done. + * + * @see JASChannel::mSampleReference + */ +struct JASSampleDataReference { + virtual ~JASSampleDataReference() = default; +}; +#endif + +#endif // DUSKLIGHT_JASSAMPLEDATAREFERENCE_H diff --git a/libs/JSystem/include/JSystem/JAudio2/JASSimpleWaveBank.h b/libs/JSystem/include/JSystem/JAudio2/JASSimpleWaveBank.h index 4fbeee32b9..8ccbc659bd 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASSimpleWaveBank.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASSimpleWaveBank.h @@ -10,12 +10,18 @@ struct JASSimpleWaveBank : JASWaveBank, JASWaveArc { intptr_t getWavePtr() const; TWaveHandle(); const JASWaveInfo* getWaveInfo() const; +#if TARGET_PC + /** + * @see JASChannel::mAramBaseAddress + */ + [[nodiscard]] void const* getAramBaseAddress() const override { return nullptr; } +#endif /* 0x04 */ JASWaveInfo mWaveInfo; /* 0x28 */ JASHeap* mHeap; }; - JASSimpleWaveBank(); + JASSimpleWaveBank(IF_DUSK(u32 bankId)); ~JASSimpleWaveBank(); void setWaveTableSize(u32, JKRHeap*); JASWaveHandle* getWaveHandle(u32) const; diff --git a/libs/JSystem/include/JSystem/JAudio2/JASWSParser.h b/libs/JSystem/include/JSystem/JAudio2/JASWSParser.h index 9fe201308c..da9af7c37c 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASWSParser.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASWSParser.h @@ -26,7 +26,9 @@ public: }; struct TCtrlWave { - /* 0x0 */ BE(u32) _00; + // Wave ID in lower half. + // Upper bits appear to be group ID, which is unused by the game code. + /* 0x0 */ BE(u32) mWaveAndGroupId; }; struct TWave { @@ -57,13 +59,14 @@ public: }; struct TCtrl { - /* 0x0 */ u8 _00[4]; + /* 0x0 */ BE(u32) mMagic; // 'C-DF'. /* 0x4 */ BE(u32) mWaveCount; /* 0x8 */ TOffset mCtrlWaveOffsets[0]; }; struct TCtrlScene { - /* 0x0 */ u8 _00[0xC]; + /* 0x0 */ BE(u32) mMagic; // 'SCNE' + /* 0x4 */ u8 _00[0x8]; /* 0xC */ TOffset mCtrlOffset; }; diff --git a/libs/JSystem/include/JSystem/JAudio2/JASWaveArcLoader.h b/libs/JSystem/include/JSystem/JAudio2/JASWaveArcLoader.h index 0ad4e044c8..7caa344c8f 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASWaveArcLoader.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASWaveArcLoader.h @@ -66,7 +66,7 @@ struct JASWaveArc : JASDisposer { }; /* 0x04 */ mutable JASHeap mHeap; - /* 0x48 */ u32 _48; + /* 0x48 */ u32 mCurrentlyLoaded; /* 0x4C */ volatile s32 mStatus; /* 0x50 */ int mEntryNum; /* 0x54 */ u32 mFileLength; diff --git a/libs/JSystem/include/JSystem/JAudio2/JASWaveInfo.h b/libs/JSystem/include/JSystem/JAudio2/JASWaveInfo.h index 04bd541204..a092f7450e 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASWaveInfo.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASWaveInfo.h @@ -2,6 +2,11 @@ #define JASWAVEINFO_H #include +#include + +#if TARGET_PC +#include "JSystem/JAudio2/JASSampleDataReference.h" +#endif struct JASWaveArc; @@ -17,7 +22,7 @@ struct JASWaveArc; struct JASWaveInfo { JASWaveInfo() { mBaseKey = 0x3c; - field_0x20 = &one; + mpLoaded = &one; } /* 0x00 */ u8 mWaveFormat; @@ -31,7 +36,7 @@ struct JASWaveInfo { /* 0x18 */ int mSampleCount; /* 0x1C */ s16 mpLast; /* 0x1E */ s16 mpPenult; - /* 0x20 */ const u32* field_0x20; + /* 0x20 */ const u32* mpLoaded; static DUSK_GAME_DATA u32 one; }; @@ -45,6 +50,17 @@ public: virtual ~JASWaveHandle() {} virtual const JASWaveInfo* getWaveInfo() const = 0; virtual intptr_t getWavePtr() const = 0; +#if TARGET_PC + /** + * @see JASChannel::mAramBaseAddress + */ + [[nodiscard]] virtual void const* getAramBaseAddress() const = 0; + + /** + * Create a @ref JASSampleDataReference to keep the sample data for this wave alive. + */ + [[nodiscard]] virtual std::unique_ptr getSampleReference() const { return nullptr; } +#endif }; /** @@ -53,6 +69,12 @@ public: */ class JASWaveBank { public: +#if TARGET_PC + const u32 bankId; + + JASWaveBank(u32 bankId) : bankId(bankId) { } +#endif + virtual ~JASWaveBank() {} virtual JASWaveHandle* getWaveHandle(u32) const = 0; virtual JASWaveArc* getWaveArc(u32) = 0; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h b/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h index cc5653fe25..4c0ade1282 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h @@ -10,7 +10,7 @@ */ struct JAUAudibleParam { f32 getDopplerPower() const { - return field_0x0.bytes.b0_0 * (1.0f / 15.0f); + return field_0x0.bytes.mDopplerPower * (1.0f / 15.0f); } union { @@ -19,14 +19,18 @@ struct JAUAudibleParam { BE(u16) f1; } half; struct { - u8 b0_0 : 4; - u8 b0_4 : 1; - u8 b0_5 : 1; - u8 b0_6 : 1; - u8 b0_7 : 1; - u8 b1_0 : 1; - u8 b1_1 : 1; - u8 b1_2_7 : 6; + u8 mDopplerPower : 4; + u8 mCalculatePriority : 1; + u8 mCalcDistanceVolume : 1; + u8 mCalcFxMix : 1; + u8 mCullAtMaxDistance : 1; + u8 mCalcPan : 1; + u8 mCalcDolby : 1; + /** + * Most bits in this field are unused, 8 indicates clamping of volume + * to ensure it doesn't go below 0.2 in mixChannelOut() + */ + u8 mClampMinVolume : 6; u8 b2; u8 b3; } bytes; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAUSoundInfo.h b/libs/JSystem/include/JSystem/JAudio2/JAUSoundInfo.h index 86fa88f913..4083b4e774 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAUSoundInfo.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAUSoundInfo.h @@ -10,8 +10,10 @@ */ class JAUSoundInfo : public JASGlobalInstance { public: + IF_DUSK(using SoundTableReplacementSlot2 = dusk::mods::svc::audio_res::bst::SoundTableReplacementSlot); + JAUSoundInfo(bool param_0) : JASGlobalInstance(param_0) {} - virtual u16 getAudibleSw(JAISoundID) const = 0; + virtual u16 getAudibleSw(JAISoundID IF_DUSK_ARG(SoundTableReplacementSlot2 const*)) const = 0; virtual u16 getBgmSeqResourceID(JAISoundID) const = 0; }; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAUSoundTable.h b/libs/JSystem/include/JSystem/JAudio2/JAUSoundTable.h index fbd3427649..3b38ff2eb6 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAUSoundTable.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAUSoundTable.h @@ -5,16 +5,179 @@ #include "JSystem/JAudio2/JASGadget.h" #include "helpers/endian.h" +// Constants for TypeIDs of JAUSoundTable entries. + +/** + * Used as a test to see if the type ID is valid. All other type IDs contain this bit. + */ +#define SOUND_TYPEID_VALID 0x40 + +/** + * Sound is a sound effect. + */ +#define SOUND_TYPEID_SOUND_EFFECT 0x51 + +/** + * Sound is a music sequence. + */ +#define SOUND_TYPEID_SEQUENCE 0x60 + +/** + * Sound is a streamed music file. + */ +#define SOUND_TYPEID_STREAM 0x70 + +/** + * Sound is a streamed music file. Stopped automatically on scene change by Z2SceneMgr.cpp + */ +#define SOUND_TYPEID_STREAM_ALT 0x71 + +// +// Values for mSwBit on sound effect items. +// + +/** + * Sound is always calculated as max priority (0). + */ +#define SOUND_SW_ALWAYS_MAX_PRIORITY 0x0000'0001 + +/** + * Don't calculate volume by distance. + */ +#define SOUND_SW_IGNORE_DISTANCE_VOL 0x0000'0002 + +/** + * Don't calculate FX mix (reverb) by distance. + */ +#define SOUND_SW_IGNORE_FX_MIX 0x0000'0004 + +/** + * Mute all BGM sequences while this sound is playing. + */ +#define SOUND_SW_MUTE_BGM 0x0000'0008 + +/** + * Offset to shift to access @see SOUND_SW_RANDOM_PITCH_MASK + */ +#define SOUND_SW_RANDOM_PITCH_OFFSET 4 + +/** + * 4-bit value (0-15) to control the power of pitch randomization on sound playback. + * Code acts different for values above 8, not sure what the exact implication is. + */ +#define SOUND_SW_RANDOM_PITCH_MASK 0x0000'00F0 + +/** + * Offset to shift to access @see SOUND_SW_DOPPLER_POWER_MASK + */ +#define SOUND_SW_DOPPLER_POWER_OFFSET 8 + +/** + * 4-bit value (0-15) to scale the power of the Doppler effect for this sound. + */ +#define SOUND_SW_DOPPLER_POWER_MASK 0x0000'0F00 + +/** + * Don't calculate panning (left/right) values for this sound. + */ +#define SOUND_SW_IGNORE_PAN 0x0000'1000 + +/** + * Don't calculate Dolby (behind/front) values for this sound. + */ +#define SOUND_SW_IGNORE_DOLBY 0x0000'2000 + +/** + * Unsure. Relates to Z2 pooling of sound handles. + */ +#define SOUND_SW_POOL_FLAG_1 0x0000'4000 + +/** + * Unsure. Relates to Z2 pooling of sound handles. + */ +#define SOUND_SW_POOL_FLAG_2 0x0000'8000 + +/** + * 3-bit mask used to select a volume distance/falloff class for this sound. + */ +#define SOUND_SW_VOL_DIST_BIT_MASK 0x0007'0000 +#define SOUND_SW_VOL_DIST_BIT_OFFSET 16 + +/** + * Limit minimum volume of this sound (after distance falloff) to 0.2. + */ +#define SOUND_SW_CLAMP_MIN_VOLUME 0x0008'0000 + +/** + * 3-bit mask used to select a *different* volume distance/falloff class for this sound. + * @see SOUND_SW_VOL_DIST_BIT_MASK must be zero for this to work. + */ +#define SOUND_SW_VOL_DIST_BIT_2_MASK 0x0070'0000 +#define SOUND_SW_VOL_DIST_BIT_2_OFFSET 20 + +/** + * Mark sound as "far away" or "culled" when at max distance (selected by distance class). + * This affects a bunch of stuff like culling, automatic stopping, priorities, etc. + */ +#define SOUND_SW_CULL_AT_MAX_DISTANCE 0x0080'0000 + +/** + * Not sure what this does. + * Something causing volume/pan/dolby adjustment in Z2Audible::setOuterParams? + */ +#define SOUND_SW_VOL_SOMETHING_MASK 0x0F00'0000 + +/** + * Offset to shift to access @see SOUND_SW_RANDOM_VOLUME_MASK + */ +#define SOUND_SW_RANDOM_VOLUME_OFFSET 28 + +/** + * 4-bit value (0-15) to control the power of volume randomization on sound playback. + */ +#define SOUND_SW_RANDOM_VOLUME_MASK 0xF000'0000 + +#define SOUND_VOL_DIST_BIT_MASK_SHIFTED (SOUND_SW_VOL_DIST_BIT_MASK >> 16) +#define SOUND_VOL_DIST_BIT_2_MASK_SHIFTED (SOUND_SW_VOL_DIST_BIT_2_MASK >> 16) +#define SOUND_VOL_SOMETHING_MASK_SHIFTED (SOUND_SW_VOL_SOMETHING_MASK >> 16) + /** * @ingroup jsystem-jaudio - * + * A single entry in the sound table. + * Fields are interpreted differently depending on the type ID of the entry, + * which is not stored in this struct. */ struct JAUSoundTableItem { u8 mPriority; - u8 field_0x1; - BE(u16) mResourceId; - BE(u32) field_0x4; - BE(f32) field_0x8; + u8 mVolume; + union { + /** + * For music sequences: the resource ID of the sequence. + */ + BE(u16) mResourceId; + + /** + * For streamed music: bitpacked channel configuration. + */ + BE(u16) mStreamPanParameters; + }; + union { + /** + * For sound effects: bitpacked field controlling a *bunch* of audio parameters. + */ + BE(u32) mSwBit; + + /** + * For streamed music: offset (relative to start of BST) + * to null-terminated string containing music file path. + */ + BE(u32) mStreamFilePath; + }; + + /** + * For sound effects: pitch multiplier. + */ + BE(f32) mPitch; }; /** @@ -24,23 +187,25 @@ struct JAUSoundTableItem { template struct JAUSoundTable_ { JAUSoundTable_() { - field_0x0 = NULL; - field_0x4 = 0; + mData = NULL; + mRoot = 0; } void reset() { - field_0x0 = NULL; - field_0x4 = NULL; + mData = NULL; + mRoot = NULL; } - void init(const void* param_0) { - field_0x0 = param_0; + void init(const void* dataStart) { + mData = dataStart; // magic number is not in debug rom. I'm not sure what this comparison is (maybe some sort of '' number?) // I also do not know how it is different between JAUSoundTable and JAUSoundNameTable - if (*(BE(u32)*)field_0x0 + 0xbdad0000 != Root::magicNumber()) { - field_0x0 = NULL; + // Future person here: This is checking for either "BST " or "BSTN", with the second two letters in Root::magicNumber(). + // Idk why the operations here are all weird but I can't use objdiff right now. + if (*(BE(u32)*)mData + 0xbdad0000 != Root::magicNumber()) { + mData = NULL; } else { - field_0x4 = (Root*)((u8*)field_0x0 + *((BE(u32)*)field_0x0 + 3)); + mRoot = (Root*)((u8*)mData + *((BE(u32)*)mData + 3)); } } @@ -48,34 +213,34 @@ struct JAUSoundTable_ { if (index < 0) { return NULL; } - if ((u32)index >= field_0x4->mSectionNumber) { + if ((u32)index >= mRoot->mSectionNumber) { return NULL; } - u32 offset = field_0x4->mSectionOffsets[index]; + u32 offset = mRoot->mSectionOffsets[index]; if (offset == 0) { return NULL; } - return (Section*)((u8*)field_0x0 + offset); + return (Section*)((u8*)mData + offset); } - Group* getGroup(Section* param_1, int index) const { + Group* getGroup(Section* section, int index) const { int iVar1; if (index < 0) { return NULL; } - if ((u32)index >= param_1->mNumGroups) { + if ((u32)index >= section->mNumGroups) { return NULL; } - u32 offset = param_1->getGroupOffset(index); + u32 offset = section->getGroupOffset(index); if (offset == 0) { return NULL; } - return (Group*)((u8*)field_0x0 + offset); + return (Group*)((u8*)mData + offset); } - const void* field_0x0; - Root* field_0x4; + const void* mData; + Root* mRoot; }; /** @@ -83,7 +248,7 @@ struct JAUSoundTable_ { * */ struct JAUSoundTableRoot { - static inline u32 magicNumber() { return 0x5420; } + static inline u32 magicNumber() { return 'T '; } // Second half of "BST " BE(u32) mSectionNumber; BE(u32) mSectionOffsets[0]; }; @@ -134,7 +299,7 @@ struct JAUSoundTableGroup { BE(u32) mNumItems; BE(u32) field_0x4; - u8 mTypeIds[0]; + u8 mTypeIds[0]; // TODO: Should probably be BE(u32), but I can't objdiff rn. }; /** @@ -142,13 +307,17 @@ struct JAUSoundTableGroup { * */ struct JAUSoundTable : public JASGlobalInstance { - JAUSoundTable(bool param_0) : JASGlobalInstance(param_0) { +#if TARGET_PC + using SoundTableReplacementSlot = dusk::mods::svc::audio_res::bst::SoundTableReplacementSlot; +#endif + + JAUSoundTable(bool setInstance) : JASGlobalInstance(setInstance) { } ~JAUSoundTable() {} void init(void const*); - u8 getTypeID(JAISoundID) const; - JAUSoundTableItem* getData(JAISoundID) const; + u8 getTypeID(JAISoundID IF_DUSK_ARG(SoundTableReplacementSlot const*)) const; + JAUSoundTableItem DUSK_CONST* getData(JAISoundID IF_DUSK_ARG(SoundTableReplacementSlot const*)) const; int getNumGroups_inSection(u8) const; int getNumItems_inGroup(u8, u8) const; @@ -157,11 +326,11 @@ struct JAUSoundTable : public JASGlobalInstance { if (offset == 0) { return NULL; } - return (JAUSoundTableItem*)((u8*)field_0x0.field_0x0 + offset); + return (JAUSoundTableItem*)((u8*)field_0x0.mData + offset); } - const void* getResource() const { return field_0x0.field_0x0; } - bool isValid() const { return field_0x0.field_0x0 != NULL; } + const void* getResource() const { return field_0x0.mData; } + bool isValid() const { return field_0x0.mData != NULL; } JAUSoundTable_ field_0x0; }; @@ -171,7 +340,7 @@ struct JAUSoundTable : public JASGlobalInstance { * */ struct JAUSoundNameTableRoot { - static inline u32 magicNumber() { return 0x544e; } + static inline u32 magicNumber() { return 'TN'; } // Second half of "BSTN" BE(u32) mSectionNumber; BE(u32) mSectionOffsets[0]; }; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAUStreamFileTable.h b/libs/JSystem/include/JSystem/JAudio2/JAUStreamFileTable.h index 65eef3f8b9..1f29d54f9d 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAUStreamFileTable.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAUStreamFileTable.h @@ -29,7 +29,7 @@ struct JAUStreamFileTable { * */ struct JAUStreamDataMgr_StreamFileTable : public JAIStreamDataMgr, JAUStreamFileTable { - virtual s32 getStreamFileEntry(JAISoundID); + virtual s32 getStreamFileEntry(JAISoundID IF_DUSK_ARG(StreamReplacementSlot2 const*)); virtual ~JAUStreamDataMgr_StreamFileTable() {} }; diff --git a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h index 84fc657387..deca5cc102 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h +++ b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h @@ -4,13 +4,12 @@ #include #if TARGET_PC -#include - struct ParticleDrawCtx { bool batch; // off = immediate mode bool useTexMtx; // UVs transformed by texMtx bool useClr0; // prm color in GX_VA_CLR0 bool useClr1; // env color in GX_VA_CLR1 + float age; Mtx texMtx; GXColor clr0; GXColor clr1; @@ -232,11 +231,5 @@ void JPARegistPrmAlphaEnv(JPA_DRAW_PARTICLE_ARGS); #undef JPA_DRAW_PARTICLE_ARGS -#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 c864a5cd4a..842f293286 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPAParticle.h +++ b/libs/JSystem/include/JSystem/JParticle/JPAParticle.h @@ -24,9 +24,6 @@ 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/src/J3DGraphAnimator/J3DAnimation.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DAnimation.cpp index 8bbdc68204..3ad9093ff8 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DAnimation.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DAnimation.cpp @@ -5,7 +5,12 @@ #include "JSystem/J3DGraphBase/J3DStruct.h" #include "JSystem/JMath/JMath.h" +#if TARGET_PC +#include "dusk/interp/material.h" +#endif + void J3DFrameCtrl::init(s16 endFrame) { + IF_DUSK(dusk::interp::material::reset(this)); mAttribute = EMode_LOOP; mState = 0; mStart = 0; @@ -134,6 +139,7 @@ int J3DFrameCtrl::checkPass(f32 passFrame) { void J3DFrameCtrl::update() { + IF_DUSK(dusk::interp::material::Update materialUpdate(*this)); mState = 0; mFrame += mRate; @@ -1456,3 +1462,67 @@ void J3DAnmTevRegKey::searchUpdateMaterialID(J3DModelData* pModelData) { J3D_ASSERT_NULLPTR(2119, pModelData != NULL); searchUpdateMaterialID(&pModelData->getMaterialTable()); } + +#if TARGET_PC +J3DAnmBase::J3DAnmBase() { + dusk::interp::material::reset(this); + mAttribute = 0; + field_0x5 = 0; + mFrameMax = 0; + mFrame = 0.0f; +} + +J3DAnmBase::J3DAnmBase(s16 frameMax) { + dusk::interp::material::reset(this); + mAttribute = 0; + field_0x5 = 0; + mFrameMax = frameMax; + mFrame = 0.0f; +} + +J3DAnmBase::~J3DAnmBase() { + dusk::interp::material::reset(this); +} + +void J3DAnmBase::setFrame(f32 frame) { + dusk::interp::material::reset(this); + mFrame = frame; +} + +J3DFrameCtrl::~J3DFrameCtrl() { + dusk::interp::material::reset(this); +} + +void J3DFrameCtrl::setAttribute(u8 attr) { + dusk::interp::material::reset(this); + mAttribute = attr; +} + +void J3DFrameCtrl::setStart(s16 start) { + dusk::interp::material::reset(this); + mStart = start; + mFrame = start; +} + +void J3DFrameCtrl::setEnd(s16 end) { + dusk::interp::material::reset(this); + mEnd = end; +} + +void J3DFrameCtrl::setLoop(s16 loop) { + dusk::interp::material::reset(this); + mLoop = loop; +} + +void J3DFrameCtrl::setFrame(f32 frame) { + dusk::interp::material::reset(this); + mFrame = frame; +} + +void J3DFrameCtrl::reset() { + dusk::interp::material::reset(this); + mFrame = mStart; + mRate = 1.0f; + mState = 0; +} +#endif diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DCluster.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DCluster.cpp index 77dc45365b..49771243d2 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DCluster.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DCluster.cpp @@ -8,7 +8,12 @@ #include "JSystem/JSupport/JSupport.h" +#if TARGET_PC +#include "dusk/interp/vertex.h" +#endif + J3DDeformData::J3DDeformData() { + IF_DUSK(dusk::interp::vertex::invalidate(this)); mClusterNum = 0; mClusterKeyNum = 0; mClusterVertexNum = 0; @@ -67,6 +72,7 @@ void J3DDeformData::deform(J3DVertexBuffer* buffer) { } void J3DDeformData::setAnm(J3DAnmCluster* anm) { + IF_DUSK(dusk::interp::vertex::invalidate(this)); for (u16 i = 0; i < mClusterNum; i++) { DEFORMER(i)->setAnmCluster(anm); } diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DMaterialAnm.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DMaterialAnm.cpp index f280a1583e..cd18d9ec82 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DMaterialAnm.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DMaterialAnm.cpp @@ -3,6 +3,10 @@ #include "JSystem/J3DGraphAnimator/J3DMaterialAnm.h" #include "JSystem/J3DGraphBase/J3DMaterial.h" +#if TARGET_PC +#include "dusk/interp/material.h" +#endif + void J3DMaterialAnm::initialize() { for (int i = 0; i < ARRAY_SIZE(mMatColorAnm); i++) { mMatColorAnm[i].setAnmFlag(false); @@ -114,3 +118,44 @@ void J3DMaterialAnm::setTevKColorAnm(int idx, J3DTevKColorAnm* pTevKColorAnm) { mTevKColorAnm[idx] = *pTevKColorAnm; } } + +#if TARGET_PC +bool J3DMaterialAnm::hasMaterialAnimation() const { + for (const auto& track : mMatColorAnm) { + if (track.getAnmFlag()) return true; + } + for (const auto& track : mTexMtxAnm) { + if (track.getAnmFlag()) return true; + } + for (const auto& track : mTevColorAnm) { + if (track.getAnmFlag()) return true; + } + for (const auto& track : mTevKColorAnm) { + if (track.getAnmFlag()) return true; + } + return false; +} + +void J3DMatColorAnm::calc(GXColor* pColor) const { + J3D_ASSERT_NULLPTR(507, pColor != NULL); + dusk::interp::material::Sample materialSample(mAnmColor); + mAnmColor->getColor(field_0x0, pColor); +} + +void J3DTexMtxAnm::calc(J3DTextureSRTInfo* pSRTInfo) const { + J3D_ASSERT_NULLPTR(519, pSRTInfo != NULL); + dusk::interp::material::sample_texture(mAnmTransform, field_0x0, pSRTInfo); +} + +void J3DTevColorAnm::calc(GXColorS10* pColor) const { + J3D_ASSERT_NULLPTR(545, pColor != NULL); + dusk::interp::material::Sample materialSample(mAnmTevReg); + mAnmTevReg->getTevColorReg(field_0x0, pColor); +} + +void J3DTevKColorAnm::calc(GXColor* pColor) const { + J3D_ASSERT_NULLPTR(558, pColor != NULL); + dusk::interp::material::Sample materialSample(mAnmTevReg); + mAnmTevReg->getTevKonstReg(field_0x0, pColor); +} +#endif diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp index 51f3951e38..7f7c999832 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp @@ -10,7 +10,9 @@ #include "JSystem/JKernel/JKRHeap.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/material.h" +#include "dusk/interp/vertex.h" #endif #define J3D_ASSERTMSG(LINE, COND, MSG) JUT_ASSERT_MSG(LINE, (COND) != 0, MSG) @@ -29,6 +31,7 @@ void J3DModel::initialize() { MTXIdentity(mBaseTransformMtx); MTXIdentity(mInternalView); + IF_DUSK(MTXIdentity(mPresentationBase)); mMtxBuffer = NULL; mMatPacket = NULL; @@ -101,17 +104,37 @@ s32 J3DModel::entryModelData(J3DModelData* pModelData, u32 mdlFlags, u32 mtxNum) } #if TARGET_PC -void J3DModel::interp_callback(bool isSimFrame, void* pUserWork) { - J3DModel* i_this = static_cast(pUserWork); - if (!isSimFrame) { - i_this->calcMaterial(); - i_this->diff(); +void J3DModel::calc_presentation_base_mtx() { + Mtx identity; + MTXIdentity(identity); + J3DCalcViewBaseMtx(identity, mBaseScale, mBaseTransformMtx, mPresentationBase); + dusk::interp::record_final_mtx(mPresentationBase); + prepare_presentation_view(); +} + +void J3DModel::prepare_presentation_view() { + Mtx replacement; + MtxP presentationBase = mPresentationBase; + if (dusk::interp::lookup_replacement(mPresentationBase, replacement)) { + presentationBase = replacement; } + MTXConcat(j3dSys.getViewMtx(), presentationBase, mInternalView); } void J3DModel::setAnmMtx(int jointNo, Mtx m) { mMtxBuffer->setAnmMtx(jointNo, m); - dusk::frame_interp::record_final_mtx(mMtxBuffer->getAnmMtx(jointNo)); + dusk::interp::record_final_mtx(mMtxBuffer->getAnmMtx(jointNo)); +} + +void J3DModel::forgetMtx() { + dusk::interp::vertex::reset(&mVertexBuffer); + dusk::interp::forget_mtx(mPresentationBase); + for (u16 i = 0; i < mModelData->getJointNum(); ++i) { + dusk::interp::forget_mtx(getAnmMtx(i)); + } + for (u16 i = 0; i < mModelData->getWEvlpMtxNum(); ++i) { + dusk::interp::forget_mtx(getWeightAnmMtx(i)); + } } #endif @@ -299,7 +322,16 @@ void J3DModel::calcMaterial() { material->getMaterialAnm()->calc(material); } - material->calc(getAnmMtx(material->getJoint()->getJntNo())); + MtxP jointMtx = getAnmMtx(material->getJoint()->getJntNo()); +#if TARGET_PC + Mtx presentedJoint; + if (dusk::interp::is_presentation_active() && + dusk::interp::lookup_replacement(jointMtx, presentedJoint)) + { + jointMtx = presentedJoint; + } +#endif + material->calc(jointMtx); } } @@ -468,13 +500,15 @@ void J3DModel::calc() { mCalcCallBack(this, 0); } -#ifdef TARGET_PC +#if TARGET_PC + dusk::interp::vertex::capture(&mVertexBuffer, mDeformData); + for (u16 i = 0; i < mModelData->getJointNum(); ++i) { - dusk::frame_interp::record_final_mtx(getAnmMtx(i)); + dusk::interp::record_final_mtx(getAnmMtx(i)); } for (u16 i = 0; i < mModelData->getWEvlpMtxNum(); ++i) { - dusk::frame_interp::record_final_mtx(getWeightAnmMtx(i)); + dusk::interp::record_final_mtx(getWeightAnmMtx(i)); } #endif } @@ -505,8 +539,9 @@ void J3DModel::entry() { } #if TARGET_PC - if (mModelData->needsInterpCallBack()) - dusk::frame_interp::add_interpolation_callback(&J3DModel::interp_callback, this); + if (mModelData->needsInterpCallBack()) { + dusk::interp::material::record_model(this); + } #endif } @@ -516,18 +551,20 @@ void J3DModel::viewCalc() { if (getModelData()->checkFlag(0x10)) { if (getMtxCalcMode() == 2) { +#if TARGET_PC + calc_presentation_base_mtx(); +#else J3DCalcViewBaseMtx(j3dSys.getViewMtx(), mBaseScale, mBaseTransformMtx, (MtxP)&mInternalView); -#ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mInternalView); #endif } } else if (isCpuSkinningOn()) { if (getMtxCalcMode() == 2) { +#if TARGET_PC + calc_presentation_base_mtx(); +#else J3DCalcViewBaseMtx(j3dSys.getViewMtx(), mBaseScale, mBaseTransformMtx, (MtxP)&mInternalView); -#ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mInternalView); #endif } } else if (checkFlag(J3DMdlFlag_SkinPosCpu)) { @@ -549,15 +586,6 @@ void J3DModel::viewCalc() { DCStoreRange(getNrmMtxPtr(), mModelData->getDrawMtxNum() * sizeof(Mtx33)); } -#ifdef TARGET_PC - Mtx* drawMtx = getDrawMtxPtr(); - if (drawMtx != J3DMtxBuffer::sNoUseDrawMtxPtr) { - for (u16 i = 0; i < mModelData->getDrawMtxNum(); ++i) { - dusk::frame_interp::record_final_mtx(drawMtx[i]); - } - } -#endif - prepareShapePackets(); } diff --git a/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp b/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp index 48d4731ed9..1bd619c0e0 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp @@ -4,6 +4,10 @@ #include "JSystem/J3DGraphBase/J3DMaterial.h" #include "JSystem/JKernel/JKRHeap.h" +#if TARGET_PC +#include "JSystem/J3DGraphAnimator/J3DMaterialAnm.h" +#endif + J3DColorBlock* J3DMaterial::createColorBlock(u32 flags) { J3DColorBlock* rv = NULL; switch (flags) { @@ -373,6 +377,9 @@ s32 J3DMaterial::newSingleSharedDisplayList(u32 dlSize) { #if TARGET_PC bool J3DMaterial::needsInterpCallBack() const { + if (mMaterialAnm != nullptr && mMaterialAnm->hasMaterialAnimation()) { + return true; + } for (int i = 0, n = getTexGenNum(); i < n; i++) { J3DTexMtx* pTexMtx = mTexGenBlock->getTexMtx(i); if (pTexMtx != NULL) { diff --git a/libs/JSystem/src/J3DGraphBase/J3DPacket.cpp b/libs/JSystem/src/J3DGraphBase/J3DPacket.cpp index 38cfddd260..fb82b8244d 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DPacket.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DPacket.cpp @@ -342,6 +342,11 @@ int J3DShapePacket::newDifferedDisplayList(u32 diffFlags) { void J3DShapePacket::prepareDraw() const { mpModel->getVertexBuffer()->setArray(); j3dSys.setModel(mpModel); +#if TARGET_PC + if (mpModel->getMtxCalcMode() == 2) { + mpModel->prepare_presentation_view(); + } +#endif j3dSys.setShapePacket((J3DShapePacket*)this); J3DShapeMtx::setLODFlag(mpModel->checkFlag(J3DMdlFlag_EnableLOD) != 0); diff --git a/libs/JSystem/src/J3DGraphBase/J3DShapeMtx.cpp b/libs/JSystem/src/J3DGraphBase/J3DShapeMtx.cpp index cedc451cb4..ae9021f76c 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DShapeMtx.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DShapeMtx.cpp @@ -8,14 +8,14 @@ #include "JSystem/J3DGraphBase/J3DTexture.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif DUSK_GAME_DATA u16 J3DShapeMtx::sMtxLoadCache[10]; #if TARGET_PC static void J3DFrameInterpConcat(MtxP lhs, MtxP rhs, Mtx out) { - if (!dusk::frame_interp::lookup_concat_replacement(lhs, rhs, out)) { + if (!dusk::interp::lookup_concat_replacement(lhs, rhs, out)) { MTXConcat(lhs, rhs, out); } } diff --git a/libs/JSystem/src/J3DGraphBase/J3DSys.cpp b/libs/JSystem/src/J3DGraphBase/J3DSys.cpp index f1810ca158..0f5beb9db5 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DSys.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DSys.cpp @@ -7,7 +7,7 @@ #include "global.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "helpers/gx_helper.h" #include @@ -379,7 +379,7 @@ void J3DSys::reinitPixelProc() { #if TARGET_PC void J3DSys::setViewMtx(const Mtx m) { Mtx patched; - if (dusk::frame_interp::lookup_replacement(m, patched)) { + if (dusk::interp::lookup_replacement(m, patched)) { m = patched; } MTXCopy(m, mViewMtx); diff --git a/libs/JSystem/src/J3DGraphBase/J3DVertex.cpp b/libs/JSystem/src/J3DGraphBase/J3DVertex.cpp index 4c1944478a..6d605e64c6 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DVertex.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DVertex.cpp @@ -6,6 +6,10 @@ #include "JSystem/JKernel/JKRHeap.h" #include +#if TARGET_PC +#include "dusk/interp/vertex.h" +#endif + J3DVertexData::J3DVertexData() { mVtxNum = 0; mNrmNum = 0; @@ -31,6 +35,7 @@ J3DVertexData::J3DVertexData() { } void J3DVertexBuffer::setVertexData(J3DVertexData* pVtxData) { + IF_DUSK(dusk::interp::vertex::reset(this)); J3D_ASSERT_NULLPTR(175, pVtxData != NULL); mVtxData = pVtxData; @@ -50,6 +55,7 @@ void J3DVertexBuffer::setVertexData(J3DVertexData* pVtxData) { } void J3DVertexBuffer::init() { + IF_DUSK(dusk::interp::vertex::reset(this)); mVtxData = NULL; mVtxPosArray[0] = mVtxPosArray[1] = NULL; @@ -65,11 +71,15 @@ void J3DVertexBuffer::init() { frameInit(); } -J3DVertexBuffer::~J3DVertexBuffer() {} +J3DVertexBuffer::~J3DVertexBuffer() { + IF_DUSK(dusk::interp::vertex::reset(this)); +} void J3DVertexBuffer::setArray() const { - j3dSys.setVtxPos(mCurrentVtxPos, mVtxData->getVtxNum()); - j3dSys.setVtxNrm(mCurrentVtxNrm, mVtxData->getNrmNum()); + j3dSys.setVtxPos(DUSK_IF_ELSE(dusk::interp::vertex::positions(this, mCurrentVtxPos), mCurrentVtxPos), + mVtxData->getVtxNum()); + j3dSys.setVtxNrm(DUSK_IF_ELSE(dusk::interp::vertex::normals(this, mCurrentVtxNrm), mCurrentVtxNrm), + mVtxData->getNrmNum()); j3dSys.setVtxCol(mCurrentVtxCol, mVtxData->getColNum()); } diff --git a/libs/JSystem/src/JAudio2/JAISe.cpp b/libs/JSystem/src/JAudio2/JAISe.cpp index 504c875bad..16f3c284da 100644 --- a/libs/JSystem/src/JAudio2/JAISe.cpp +++ b/libs/JSystem/src/JAudio2/JAISe.cpp @@ -3,6 +3,7 @@ #include "JSystem/JAudio2/JAISe.h" #include "JSystem/JAudio2/JAIAudience.h" #include "JSystem/JAudio2/JAISeMgr.h" +#include "dusk/mods/svc/audio_res/bst.hpp" JAISe::JAISe(JAISeMgr* seMgr, JAISoundStrategyMgr* soundStrategyMgr, u32 priority) : JSULink(this) { inner_.mSoundStrategyMgr = soundStrategyMgr; @@ -70,7 +71,7 @@ void JAISe::JAISeCategoryMgr_mixOut_(bool param_0, const JASSoundParams& params, if (inner_.field_0x26c) { switch (inner_.track.getStatus()) { case JASTrack::STATUS_STOPPED: - if (status_.field_0x1.flags.flag1) { + if (status_.field_0x1.flags.mComesBack) { startTrack_(params); } else { stop_JAISound_(); @@ -84,8 +85,8 @@ void JAISe::JAISeCategoryMgr_mixOut_(bool param_0, const JASSoundParams& params, startTrack_(params); } } - } else if (status_.field_0x1.flags.flag1) { - if (status_.field_0x1.flags.flag3) { + } else if (status_.field_0x1.flags.mComesBack) { + if (status_.field_0x1.flags.mPauseWhenOut) { inner_.track.pause(true); } else { stopTrack_(); @@ -129,10 +130,10 @@ bool JAISe::JAISound_tryDie_() { return false; } -void JAISe::JAISeMgr_startID_(JAISoundID id, const JGeometry::TVec3* posPtr, JAIAudience* audience) { +void JAISe::JAISeMgr_startID_(JAISoundID id, const JGeometry::TVec3* posPtr, JAIAudience* audience IF_DUSK_ARG(std::shared_ptr replacement)) { JUT_ASSERT(221, inner_.track.getStatus() == JASTrack::STATUS_FREE); - start_JAISound_(id, posPtr, audience); + start_JAISound_(id, posPtr, audience IF_DUSK_ARG(std::static_pointer_cast(replacement))); inner_.field_0x26c = 0; if (inner_.mSoundStrategyMgr) { inner_.field_0x278 = inner_.mSoundStrategyMgr->calc(id); diff --git a/libs/JSystem/src/JAudio2/JAISeMgr.cpp b/libs/JSystem/src/JAudio2/JAISeMgr.cpp index 0809ede5c6..825ac8d78e 100644 --- a/libs/JSystem/src/JAudio2/JAISeMgr.cpp +++ b/libs/JSystem/src/JAudio2/JAISeMgr.cpp @@ -4,6 +4,7 @@ #include "JSystem/JAudio2/JAISoundHandles.h" #include "JSystem/JAudio2/JAISoundInfo.h" #include "JSystem/JAudio2/JASReport.h" +#include "dusk/mods/svc/audio_res/bst.hpp" bool JAISeCategoryMgr::isUsingSeqData(const JAISeqDataRegion& seqDataRegion) { { @@ -167,7 +168,7 @@ JAISeMgr::JAISeMgr(bool setInstance) : JASGlobalInstance(setInstance) } bool JAISeMgr::isUsingSeqData(const JAISeqDataRegion& seqDataRegion) { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { if (mCategoryMgrs[i].JAISeCategoryMgr::isUsingSeqData(seqDataRegion)) { return true; } @@ -177,7 +178,7 @@ bool JAISeMgr::isUsingSeqData(const JAISeqDataRegion& seqDataRegion) { int JAISeMgr::releaseSeqData(const JAISeqDataRegion& seqDataRegion) { bool r30 = 0; - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { switch (mCategoryMgrs[i].JAISeCategoryMgr::releaseSeqData(seqDataRegion)) { case 0: return 0; @@ -191,14 +192,14 @@ int JAISeMgr::releaseSeqData(const JAISeqDataRegion& seqDataRegion) { } void JAISeMgr::setCategoryArrangement(const JAISeCategoryArrangement& arrangement) { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { mCategoryMgrs[i].setMaxActiveSe(arrangement.mItems[i].mMaxActiveSe); mCategoryMgrs[i].setMaxInactiveSe(arrangement.mItems[i].mMaxInactiveSe); } } void JAISeMgr::getCategoryArrangement(JAISeCategoryArrangement* arrangement) { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { int active = mCategoryMgrs[i].getMaxActiveSe(); JUT_ASSERT(299, active <= 255); arrangement->mItems[i].mMaxActiveSe = active; @@ -209,19 +210,19 @@ void JAISeMgr::getCategoryArrangement(JAISeCategoryArrangement* arrangement) { } void JAISeMgr::stop() { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { mCategoryMgrs[i].stop(); } } void JAISeMgr::stopSoundID(JAISoundID id) { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { mCategoryMgrs[i].stopSoundID(id); } } void JAISeMgr::initParams() { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { mCategoryMgrs[i].getParams()->init(); } } @@ -270,21 +271,21 @@ JAISe* JAISeMgr::newSe_(int category, u32 priority) { void JAISeMgr::calc() { mParams.calc(); - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { mCategoryMgrs[i].JAISeMgr_calc_(); } - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { mCategoryMgrs[i].JAISeMgr_freeDeadSe_(); } } void JAISeMgr::mixOut() { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { mCategoryMgrs[i].JAISeMgr_mixOut_(mParams, mSoundActivity); } } -bool JAISeMgr::startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry::TVec3* posPtr) { +bool JAISeMgr::startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry::TVec3* posPtr IF_DUSK_ARG(std::shared_ptr replacement)) { if (handle && *handle) { (*handle)->stop(); } @@ -294,7 +295,7 @@ bool JAISeMgr::startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry u32 category; if (soundInfoPtr != NULL) { category = soundInfoPtr->getCategory(id); - priority = soundInfoPtr->getPriority(id); + priority = soundInfoPtr->getPriority(id IF_DUSK_ARG(replacement.get())); } else { category = id.id_.info.type.parts.groupID; priority = 0; @@ -309,10 +310,10 @@ bool JAISeMgr::startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry } JAIAudience* audiencePtr = getAudience(category); - se->JAISeMgr_startID_(id, posPtr, audiencePtr); + se->JAISeMgr_startID_(id, posPtr, audiencePtr IF_DUSK_ARG(replacement)); if (soundInfoPtr != NULL) { - soundInfoPtr->getSeInfo(id, se); + soundInfoPtr->getSeInfo(id, se IF_DUSK_ARG(replacement.get())); } if (handle != NULL) { @@ -324,7 +325,7 @@ bool JAISeMgr::startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry int JAISeMgr::getNumActiveSe() const { int num = 0; - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_CATEGORIES; i++) { num += mCategoryMgrs[i].getNumSe(); } return num; diff --git a/libs/JSystem/src/JAudio2/JAISeq.cpp b/libs/JSystem/src/JAudio2/JAISeq.cpp index c05494b82e..36895d2d63 100644 --- a/libs/JSystem/src/JAudio2/JAISeq.cpp +++ b/libs/JSystem/src/JAudio2/JAISeq.cpp @@ -28,7 +28,7 @@ void JAISeq::JAISeqMgr_startID_(JAISoundID id, const JGeometry::TVec3* posP JAIAudience* audience, int category, int param_4) { inner_.field_0x39c = category; reserveChildTracks_(param_4); - start_JAISound_(id, posPtr, audience); + start_JAISound_(id, posPtr, audience IF_DUSK_ARG(nullptr)); if (inner_.strategyMgr) { field_0x3a8 = inner_.strategyMgr->calc(id); diff --git a/libs/JSystem/src/JAudio2/JAISound.cpp b/libs/JSystem/src/JAudio2/JAISound.cpp index fe6b45bb8b..78141503ed 100644 --- a/libs/JSystem/src/JAudio2/JAISound.cpp +++ b/libs/JSystem/src/JAudio2/JAISound.cpp @@ -54,16 +54,16 @@ s32 JAISoundStatus_::unlockIfLocked() { } void JAISoundParams::mixOutAll(const JASSoundParams& inParams, JASSoundParams* outParams, f32 param_2) { - outParams->mVolume = move_.params_.mVolume * (inParams.mVolume * property_.field_0x0) * param_2; - outParams->mFxMix = move_.params_.mFxMix + (inParams.mFxMix + property_.field_0x4); - outParams->mPitch = move_.params_.mPitch * (inParams.mPitch * property_.field_0x8); + outParams->mVolume = move_.params_.mVolume * (inParams.mVolume * property_.mVolume) * param_2; + outParams->mFxMix = move_.params_.mFxMix + (inParams.mFxMix + property_.mFxMix); + outParams->mPitch = move_.params_.mPitch * (inParams.mPitch * property_.mPitch); outParams->mPan = (inParams.mPan + move_.params_.mPan) - 0.5f; outParams->mDolby = inParams.mDolby + move_.params_.mDolby; } JAISound::JAISound() : params_() {} -void JAISound::start_JAISound_(JAISoundID id, const JGeometry::TVec3* posPtr, JAIAudience* audience) { +void JAISound::start_JAISound_(JAISoundID id, const JGeometry::TVec3* posPtr, JAIAudience* audience IF_DUSK_ARG(std::shared_ptr replacement)) { handle_ = NULL; soundID_ = id; status_.init(); @@ -72,9 +72,10 @@ void JAISound::start_JAISound_(JAISoundID id, const JGeometry::TVec3* posPt audience_ = audience; prepareCount_ = 0; count_ = 0; + IF_DUSK(this->replacement = std::move(replacement)); if (posPtr != NULL && audience_ != NULL) { - audible_ = audience_->newAudible(*posPtr, soundID_, NULL, 0); + audible_ = audience_->newAudible(*posPtr, soundID_, NULL, 0 IF_DUSK_ARG(getReplacement())); } else { audible_ = NULL; } @@ -98,7 +99,7 @@ void JAISound::newAudible(const JGeometry::TVec3& pos, } JUT_ASSERT(157, audience_); - audible_ = audience_->newAudible(pos, soundID_, param_1, param_2); + audible_ = audience_->newAudible(pos, soundID_, param_1, param_2 IF_DUSK_ARG(getReplacement())); } void JAISound::stop(u32 fadeTime) { @@ -181,7 +182,7 @@ bool JAISound::calc_JAISound_() { } if (audience_ != NULL && audible_ != NULL) { - if ((priority_ = audience_->calcPriority(audible_)) == 0xFFFFFFFF && status_.field_0x1.flags.flag1 == 0) { + if ((priority_ = audience_->calcPriority(audible_)) == 0xFFFFFFFF && status_.field_0x1.flags.mComesBack == 0) { stop_JAISound_(); } } else { diff --git a/libs/JSystem/src/JAudio2/JAIStream.cpp b/libs/JSystem/src/JAudio2/JAIStream.cpp index 5799080b12..e75292a34b 100644 --- a/libs/JSystem/src/JAudio2/JAIStream.cpp +++ b/libs/JSystem/src/JAudio2/JAIStream.cpp @@ -5,6 +5,7 @@ #include "JSystem/JAudio2/JAISoundChild.h" #include "JSystem/JAudio2/JAIStreamDataMgr.h" #include "JSystem/JAudio2/JAIAudience.h" +#include "dusk/mods/svc/audio_res/bst.hpp" static void JAIStream_JASAramStreamCallback_(u32 type, JASAramStream* aramStream, void* userData) { JAIStream* stream = (JAIStream*)userData; @@ -34,10 +35,10 @@ JAIStream::JAIStream(JAIStreamMgr* streamMgr, JAISoundStrategyMgr* so void JAIStream::JAIStreamMgr_startID_(JAISoundID id, s32 streamFileEntry, const JGeometry::TVec3* posPtr, JAIAudience* audience, - int category) { + int category IF_DUSK_ARG(std::shared_ptr replacement)) { field_0x298 = category; field_0x294 = streamFileEntry; - start_JAISound_(id, posPtr, audience); + start_JAISound_(id, posPtr, audience IF_DUSK_ARG(std::static_pointer_cast(replacement))); field_0x290 = 0; if (field_0x2c0 != NULL) { diff --git a/libs/JSystem/src/JAudio2/JAIStreamMgr.cpp b/libs/JSystem/src/JAudio2/JAIStreamMgr.cpp index 2baafdf24a..ca982bafc7 100644 --- a/libs/JSystem/src/JAudio2/JAIStreamMgr.cpp +++ b/libs/JSystem/src/JAudio2/JAIStreamMgr.cpp @@ -14,13 +14,13 @@ JAIStreamMgr::JAIStreamMgr(bool setInstance) : JASGlobalInstance(s mActivity.init(); } -bool JAIStreamMgr::startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry::TVec3* posPtr) { +bool JAIStreamMgr::startSound(JAISoundID id, JAISoundHandle* handle, const JGeometry::TVec3* posPtr IF_DUSK_ARG(std::shared_ptr replacement)) { JUT_ASSERT(37, streamDataMgr_); if (handle != NULL && *handle) { (*handle)->stop(); } - s32 streamFileEntry = streamDataMgr_->getStreamFileEntry(id); + s32 streamFileEntry = streamDataMgr_->getStreamFileEntry(id IF_DUSK_ARG(replacement.get())); if (streamFileEntry < 0) { JUT_WARN(46, "Cannot find the stream file entry for ID:%08x\n", id.id_.composite_) return false; @@ -38,9 +38,9 @@ bool JAIStreamMgr::startSound(JAISoundID id, JAISoundHandle* handle, const JGeom return false; } - stream->JAIStreamMgr_startID_(id, streamFileEntry, posPtr, mAudience, category); + stream->JAIStreamMgr_startID_(id, streamFileEntry, posPtr, mAudience, category IF_DUSK_ARG(replacement)); if (soundInfo != NULL) { - soundInfo->getStreamInfo(id, stream); + soundInfo->getStreamInfo(id, stream IF_DUSK_ARG(replacement.get())); } if (handle != NULL) { diff --git a/libs/JSystem/src/JAudio2/JASAiCtrl.cpp b/libs/JSystem/src/JAudio2/JASAiCtrl.cpp index e59c9d45fe..dc0289ce0f 100644 --- a/libs/JSystem/src/JAudio2/JASAiCtrl.cpp +++ b/libs/JSystem/src/JAudio2/JASAiCtrl.cpp @@ -46,7 +46,7 @@ DUSK_GAME_DATA u32 JASDriver::sOutputRate; DUSK_GAME_DATA JASMixMode JASDriver::sMixMode = MIX_MODE_EXTRA; -DUSK_GAME_DATA f32 JASDriver::sDacRate = 32028.5f; +DUSK_GAME_DATA f32 JASDriver::sDacRate = DUSK_IF_ELSE(32000.0f, 32028.5f); DUSK_GAME_DATA u32 JASDriver::sSubFrames = 0x00000007; @@ -111,9 +111,7 @@ void JASDriver::setOutputRate(JASOutputRate param_0) { sDacRate = 48000.0f; } -#if !TARGET_PC - sDacRate *= 1.0008897f; -#endif + IF_NOT_DUSK(sDacRate *= 1.0008897f); } DUSK_GAME_DATA const JASDriver::MixFunc JASDriver::sMixFuncs[4] = { diff --git a/libs/JSystem/src/JAudio2/JASAramStream.cpp b/libs/JSystem/src/JAudio2/JASAramStream.cpp index 6a15869a7c..74026d94ff 100644 --- a/libs/JSystem/src/JAudio2/JASAramStream.cpp +++ b/libs/JSystem/src/JAudio2/JASAramStream.cpp @@ -713,7 +713,7 @@ void JASAramStream::channelStart() { wave_info.mpPenult = 0; // probably a fake match, this should be set in the JASWaveInfo constructor static u32 const one = 1; - wave_info.field_0x20 = &one; + wave_info.mpLoaded = &one; JASChannel* jc = JKR_NEW JASChannel(channelCallback, this); JUT_ASSERT(963, jc); jc->setPriority(0x7f7f); @@ -722,9 +722,9 @@ void JASAramStream::channelStart() { } jc->setInitPitch(mSampleRate / JASDriver::getDacRate()); jc->setOscInit(0, &OSC_ENV); - jc->field_0xdc.mWaveInfo = wave_info; + jc->mAnon.mWaveInfo = wave_info; jc->mWaveAramAddress = mAramAddress + sBlockSize * mAramBlocksPerChannel * i; - jc->field_0xdc.mChannelType = 0; + jc->mAnon.mChannelType = 0; int ret = jc->playForce(); JUT_ASSERT(977, ret); JUT_ASSERT_MSG(979, mChannels[i] == NULL, "channelStart for already playing channel"); diff --git a/libs/JSystem/src/JAudio2/JASBNKParser.cpp b/libs/JSystem/src/JAudio2/JASBNKParser.cpp index ad8aa1ee46..6fbd3ad5cf 100644 --- a/libs/JSystem/src/JAudio2/JASBNKParser.cpp +++ b/libs/JSystem/src/JAudio2/JASBNKParser.cpp @@ -115,9 +115,9 @@ JASBasicBank* JASBNKParser::Ver1::createBasicBank(void const* stream, JKRHeap* h JASBasicInst::TKeymap* keymap = instp->getKeyRegion(j); keymap->setHighKey(*data >> 0x18); u32 fVar4 = data[1]; - keymap->field_0x4 = JSULoHalf(data[3]); - keymap->field_0x8 = *(BE(f32)*)&data[4]; - keymap->field_0xc = *(BE(f32)*)&data[5]; + keymap->mWaveId = JSULoHalf(data[3]); + keymap->mVolumeMult = *(BE(f32)*)&data[4]; + keymap->mPitchMult = *(BE(f32)*)&data[5]; data += 2; for (int k = 0; k < fVar4; k++) { data += 4; @@ -159,9 +159,9 @@ JASBasicBank* JASBNKParser::Ver1::createBasicBank(void const* stream, JKRHeap* h ptr++; } u32 pVar6 = ptr[0]; - percp->field_0xe = JSULoHalf(ptr[2]); - percp->field_0x10 = *(BE(f32)*)&ptr[3]; - percp->field_0x14 = *(BE(f32)*)&ptr[4]; + percp->mWaveId = JSULoHalf(ptr[2]); + percp->mVolumeMult = *(BE(f32)*)&ptr[3]; + percp->mPitchMult = *(BE(f32)*)&ptr[4]; for (int k = 0; k < pVar6; k++) {} drump->setPerc(j, percp); } @@ -260,9 +260,9 @@ JASBasicBank* JASBNKParser::Ver0::createBasicBank(void const* stream, JKRHeap* h TKeymap* tkeymap = tinst->mKeymapOffset[j].ptr(header); keymap->setHighKey(tkeymap->mHighKey); TVmap* tvmap = tkeymap->mVmapOffset.ptr(header); - keymap->field_0x4 = JSULoHalf(tvmap->field_0x4); - keymap->field_0x8 = tvmap->field_0x8; - keymap->field_0xc = tvmap->field_0xc; + keymap->mWaveId = JSULoHalf(tvmap->field_0x4); + keymap->mVolumeMult = tvmap->field_0x8; + keymap->mPitchMult = tvmap->field_0xc; } bank->setInst(i, instp); @@ -288,9 +288,9 @@ JASBasicBank* JASBNKParser::Ver0::createBasicBank(void const* stream, JKRHeap* h percp->setRelease(tperc->mRelease[j]); } TVmap* vmap = tpmap->mVmapOffset.ptr(header); - percp->field_0xe = JSULoHalf(vmap->field_0x4); - percp->field_0x10 = vmap->field_0x8; - percp->field_0x14 = vmap->field_0xc; + percp->mWaveId = JSULoHalf(vmap->field_0x4); + percp->mVolumeMult = vmap->field_0x8; + percp->mPitchMult = vmap->field_0xc; setp->setPerc(j, percp); } } @@ -314,8 +314,8 @@ JASOscillator::Data* JASBNKParser::Ver0::findOscPtr(JASBasicBank* bank, THeader if (inst != NULL) { JASInstParam param; inst->getParam(0x3c, 0x7f, ¶m); - if (j < param.field_0x1d) { - return param.field_0x14[j]; + if (j < param.mOscillatorCount) { + return param.mOscillators[j]; } } } diff --git a/libs/JSystem/src/JAudio2/JASBank.cpp b/libs/JSystem/src/JAudio2/JASBank.cpp index 29ebe11cb5..0acc5b9ed4 100644 --- a/libs/JSystem/src/JAudio2/JASBank.cpp +++ b/libs/JSystem/src/JAudio2/JASBank.cpp @@ -1,64 +1,83 @@ #include "JSystem/JSystem.h" // IWYU pragma: keep #include "JSystem/JAudio2/JASBank.h" + +#include "dusk/mods/svc/audio_res/wsys.hpp" #include "JSystem/JAudio2/JASAiCtrl.h" #include "JSystem/JAudio2/JASBasicInst.h" #include "JSystem/JAudio2/JASBasicWaveBank.h" #include "JSystem/JAudio2/JASChannel.h" +using namespace dusk::mods::svc::audio_res::wsys; + // NONMATCHING JASPoolAllocObject_MultiThreaded<_> locations -JASChannel* JASBank::noteOn(JASBank const* param_0, int param_1, u8 param_2, u8 param_3, u16 param_4, - void (*param_5)(u32, JASChannel*, JASDsp::TChannel*, void*), - void* param_6) { - if (param_1 >= 0xf0) { - return noteOnOsc(param_1 - 0xf0, param_2, param_3, param_4, param_5, param_6); +JASChannel* JASBank::noteOn(JASBank const* bank, int program, u8 key, u8 velocity, u16 priority, + void (*callback)(u32, JASChannel*, JASDsp::TChannel*, void*), + void* callback_data) { + if (program >= 0xf0) { + return noteOnOsc(program - 0xf0, key, velocity, priority, callback, callback_data); } - if (!param_0) { + if (!bank) { return NULL; } - JASInstParam stack_60; - if (!param_0->getInstParam(param_1, param_2, param_3, &stack_60)) { + JASInstParam instParam; + if (!bank->getInstParam(program, key, velocity, &instParam)) { return NULL; } - JASWaveBank* waveBank = param_0->getWaveBank(); + JASWaveBank* waveBank = bank->getWaveBank(); if (!waveBank) { return NULL; } - JASWaveHandle* waveHandle = waveBank->getWaveHandle(stack_60.field_0x1a); + JASWaveHandle* waveHandle = waveBank->getWaveHandle(instParam.mWaveId); if (!waveHandle) { return NULL; } +#if TARGET_PC + auto const wave_key = AudioWaveKey(static_cast(waveBank->bankId), instParam.mWaveId); + std::lock_guard lock(s_replacements_mutex); + auto const found_replacement = s_replacements.find(wave_key); + if (found_replacement != s_replacements.end()) { + waveHandle = &found_replacement->second; + } + + auto const aramBase = waveHandle->getAramBaseAddress(); +#endif + const JASWaveInfo* waveInfo = waveHandle->getWaveInfo(); if (!waveInfo) { return NULL; } intptr_t wavePtr = waveHandle->getWavePtr(); - if (!wavePtr) { + if (!wavePtr IF_DUSK(&& !aramBase)) { return NULL; } - JASChannel* channel = JKR_NEW JASChannel(param_5, param_6); + JASChannel* channel = JKR_NEW JASChannel(callback, callback_data); if (!channel) { return NULL; } - channel->setPriority(param_4); - channel->field_0xdc.mWaveInfo = *waveInfo; + channel->setPriority(priority); + channel->mAnon.mWaveInfo = *waveInfo; channel->mWaveAramAddress = wavePtr; - channel->field_0xdc.mChannelType = stack_60.field_0x1c; - channel->setBankDisposeID(param_0); - channel->setInitPitch(stack_60.mPitch * (waveInfo->mSampleRate / JASDriver::getDacRate())); - if (stack_60.field_0x1e == 0) { - channel->setKey(param_2 - waveInfo->mBaseKey); +#if TARGET_PC + channel->mAramBaseAddress = aramBase; + channel->mSampleReference = waveHandle->getSampleReference(); +#endif + channel->mAnon.mChannelType = instParam.mChannelType; + channel->setBankDisposeID(bank); + channel->setInitPitch(instParam.mPitch * (waveInfo->mSampleRate / JASDriver::getDacRate())); + if (instParam.mDontSetKey == 0) { + channel->setKey(key - waveInfo->mBaseKey); } - channel->setInitVolume(stack_60.mVolume); - channel->setVelocity(param_3); - channel->setInitPan(stack_60.mPan); - channel->setInitFxmix(stack_60.mFxMix); - channel->setInitDolby(stack_60.mDolby); - for (u32 i = 0; i < stack_60.field_0x1d; i++) { - channel->setOscInit(i, stack_60.field_0x14[i]); + channel->setInitVolume(instParam.mVolume); + channel->setVelocity(velocity); + channel->setInitPan(instParam.mPan); + channel->setInitFxmix(instParam.mFxMix); + channel->setInitDolby(instParam.mDolby); + for (u32 i = 0; i < instParam.mOscillatorCount; i++) { + channel->setOscInit(i, instParam.mOscillators[i]); } - channel->setDirectRelease(stack_60.field_0x18); + channel->setDirectRelease(instParam.mDirectRelease); if (!channel->play()) { return NULL; } @@ -66,24 +85,24 @@ JASChannel* JASBank::noteOn(JASBank const* param_0, int param_1, u8 param_2, u8 } // NONMATCHING JASPoolAllocObject_MultiThreaded<_> locations -JASChannel* JASBank::noteOnOsc(int param_0, u8 param_1, u8 param_2, u16 param_3, - void (*param_4)(u32, JASChannel*, JASDsp::TChannel*, void*), - void* param_5) { +JASChannel* JASBank::noteOnOsc(int param_0, u8 key, u8 velocity, u16 priority, + void (*callback)(u32, JASChannel*, JASDsp::TChannel*, void*), + void* callback_data) { static JASOscillator::Point const OSC_RELEASE_TABLE[2] = { {0x0001, 0x000A, 0x0000}, {0x000F, 0x0000, 0x0000}, }; static const JASOscillator::Data OSC_ENV = {0, 1.0f, NULL, OSC_RELEASE_TABLE, 1.0f, 0.0f}; - JASChannel* channel = JKR_NEW JASChannel(param_4, param_5); + JASChannel* channel = JKR_NEW JASChannel(callback, callback_data); if (!channel) { return NULL; } - channel->setPriority(param_3); + channel->setPriority(priority); channel->mOscillatorSomething = param_0; - channel->field_0xdc.mChannelType = 2; + channel->mAnon.mChannelType = CHANNEL_OSCILLATOR; channel->setInitPitch(16736.016f / JASDriver::getDacRate()); - channel->setKey(param_1 - channel->field_0xdc.mWaveInfo.mBaseKey); - channel->setVelocity(param_2); + channel->setKey(key - channel->mAnon.mWaveInfo.mBaseKey); + channel->setVelocity(velocity); channel->setOscInit(0, &OSC_ENV); if (!channel->play()) { return NULL; diff --git a/libs/JSystem/src/JAudio2/JASBasicBank.cpp b/libs/JSystem/src/JAudio2/JASBasicBank.cpp index 9b3c032bbb..16fef1d746 100644 --- a/libs/JSystem/src/JAudio2/JASBasicBank.cpp +++ b/libs/JSystem/src/JAudio2/JASBasicBank.cpp @@ -14,20 +14,20 @@ void JASBasicBank::newInstTable(u8 num, JKRHeap* heap) { mInstNumMax = num; mInstTable = JKR_NEW_ARRAY_ARGS(JASInst*, mInstNumMax, heap, 0); #if TARGET_ANDROID - JASCalc::_bzero(mInstTable, mInstNumMax * 4); + JASCalc::_bzero(mInstTable, mInstNumMax * sizeof(mInstTable[0])); #else - JASCalc::bzero(mInstTable, mInstNumMax * 4); + JASCalc::bzero(mInstTable, mInstNumMax * sizeof(mInstTable[0])); #endif } } -bool JASBasicBank::getInstParam(int prg_no, int param_1, int param_2, +bool JASBasicBank::getInstParam(int prg_no, int key, int velocity, JASInstParam* o_param) const { JASInst* inst = getInst(prg_no); if (inst == NULL) { return false; } - return inst->getParam(param_1, param_2, o_param); + return inst->getParam(key, velocity, o_param); } void JASBasicBank::setInst(int prg_no, JASInst* inst) { diff --git a/libs/JSystem/src/JAudio2/JASBasicInst.cpp b/libs/JSystem/src/JAudio2/JASBasicInst.cpp index fba53d010f..50e764eeac 100644 --- a/libs/JSystem/src/JAudio2/JASBasicInst.cpp +++ b/libs/JSystem/src/JAudio2/JASBasicInst.cpp @@ -14,9 +14,9 @@ JASBasicInst::JASBasicInst() { mKeymapCount = 0; mKeymap = NULL; #if TARGET_ANDROID - JASCalc::_bzero(field_0xc, sizeof(field_0xc)); + JASCalc::_bzero(mOscillators, sizeof(mOscillators)); #else - JASCalc::bzero(field_0xc, sizeof(field_0xc)); + JASCalc::bzero(mOscillators, sizeof(mOscillators)); #endif } @@ -24,18 +24,18 @@ JASBasicInst::~JASBasicInst() { JKR_DELETE_ARRAY(mKeymap); } -bool JASBasicInst::getParam(int param_0, int param_1, JASInstParam* param_2) const { - UNUSED(param_1); - param_2->field_0x1c = 0; - param_2->field_0x1e = 0; - param_2->field_0x14 = (JASOscillator::Data**)&field_0xc; - param_2->field_0x1d = 2; - param_2->mVolume = mVolume; - param_2->mPitch = mPitch; +bool JASBasicInst::getParam(int key, int velocity, JASInstParam* o_param) const { + UNUSED(velocity); + o_param->mChannelType = 0; + o_param->mDontSetKey = 0; + o_param->mOscillators = (JASOscillator::Data**)&mOscillators; + o_param->mOscillatorCount = 2; + o_param->mVolume = mVolume; + o_param->mPitch = mPitch; TKeymap* keyMap = NULL; for (int i = 0; i < mKeymapCount; i++) { - if (param_0 <= mKeymap[i].mHighKey) { + if (key <= mKeymap[i].mHighKey) { keyMap = &mKeymap[i]; break; } @@ -45,9 +45,9 @@ bool JASBasicInst::getParam(int param_0, int param_1, JASInstParam* param_2) con return false; } - param_2->mVolume *= keyMap->field_0x8; - param_2->mPitch *= keyMap->field_0xc; - param_2->field_0x1a = u16(keyMap->field_0x4); + o_param->mVolume *= keyMap->mVolumeMult; + o_param->mPitch *= keyMap->mPitchMult; + o_param->mWaveId = u16(keyMap->mWaveId); return true; } @@ -61,7 +61,7 @@ void JASBasicInst::setKeyRegionCount(u32 count, JKRHeap* param_1) { void JASBasicInst::setOsc(int index, JASOscillator::Data const* param_1) { JUT_ASSERT(128, index < OSC_MAX); JUT_ASSERT(129, index >= 0); - field_0xc[index] = param_1; + mOscillators[index] = param_1; } JASBasicInst::TKeymap* JASBasicInst::getKeyRegion(int index) { diff --git a/libs/JSystem/src/JAudio2/JASBasicWaveBank.cpp b/libs/JSystem/src/JAudio2/JASBasicWaveBank.cpp index ac11742e6b..9f9e16325c 100644 --- a/libs/JSystem/src/JAudio2/JASBasicWaveBank.cpp +++ b/libs/JSystem/src/JAudio2/JASBasicWaveBank.cpp @@ -6,12 +6,12 @@ #include "JSystem/JUtility/JUTAssert.h" #include -JASBasicWaveBank::JASBasicWaveBank() { +JASBasicWaveBank::JASBasicWaveBank(IF_DUSK(u32 bankId)) IF_DUSK(: JASWaveBank(bankId)) { mWaveTable = NULL; mWaveGroupArray = NULL; mHandleCount = 0; mGroupCount = 0; - OSInitMutex(&field_0x4); + OSInitMutex(&mWaveTableMutex); } JASBasicWaveBank::~JASBasicWaveBank() { @@ -44,13 +44,13 @@ void JASBasicWaveBank::setWaveTableSize(u32 param_0, JKRHeap* param_1) { } void JASBasicWaveBank::incWaveTable(JASBasicWaveBank::TWaveGroup const* param_0) { - JASMutexLock lock(&field_0x4); + JASMutexLock lock(&mWaveTableMutex); for (u32 i = 0; i < param_0->getWaveCount(); i++) { TWaveHandle* handle = mWaveTable + param_0->getWaveID(i); if (!handle->mHeap) { handle->mHeap = ¶m_0->mHeap; - handle->field_0x4.field_0x20 = ¶m_0->_48; - handle->field_0x4.mOffsetStart = param_0->mCtrlWaveArray[i].field_0x4; + handle->mWaveInfo.mpLoaded = ¶m_0->mCurrentlyLoaded; + handle->mWaveInfo.mOffsetStart = param_0->mCtrlWaveArray[i].mOffsetStart; } } } @@ -58,13 +58,13 @@ void JASBasicWaveBank::incWaveTable(JASBasicWaveBank::TWaveGroup const* param_0) DUSK_GAME_DATA u32 JASBasicWaveBank::mNoLoad; void JASBasicWaveBank::decWaveTable(JASBasicWaveBank::TWaveGroup const* param_0) { - JASMutexLock lock(&field_0x4); + JASMutexLock lock(&mWaveTableMutex); for (u32 i = 0; i < param_0->getWaveCount(); i++) { TWaveHandle* handle = mWaveTable + param_0->getWaveID(i); if (handle->mHeap == ¶m_0->mHeap) { handle->mHeap = NULL; - handle->field_0x4.field_0x20 = &mNoLoad; - handle->field_0x4.mOffsetStart = -1; + handle->mWaveInfo.mpLoaded = &mNoLoad; + handle->mWaveInfo.mOffsetStart = -1; } } } @@ -80,15 +80,15 @@ JASWaveHandle* JASBasicWaveBank::getWaveHandle(u32 param_0) const { } void JASBasicWaveBank::setWaveInfo(JASBasicWaveBank::TWaveGroup* wgrp, int index, - u16 param_2, JASWaveInfo const& param_3) { + u16 waveId, JASWaveInfo const& param_3) { JUT_ASSERT(204, wgrp); JUT_ASSERT(205, index < wgrp->mWaveCount); JUT_ASSERT(206, index >= 0); - mWaveTable[param_2].field_0x4 = param_3; - mWaveTable[param_2].field_0x4.field_0x20 = &mNoLoad; - mWaveTable[param_2].field_0x4.mOffsetStart = -1; - wgrp->mCtrlWaveArray[index].field_0x0 = param_2; - wgrp->mCtrlWaveArray[index].field_0x4 = param_3.mOffsetStart; + mWaveTable[waveId].mWaveInfo = param_3; + mWaveTable[waveId].mWaveInfo.mpLoaded = &mNoLoad; + mWaveTable[waveId].mWaveInfo.mOffsetStart = -1; + wgrp->mCtrlWaveArray[index].waveId = waveId; + wgrp->mCtrlWaveArray[index].mOffsetStart = param_3.mOffsetStart; } JASBasicWaveBank::TWaveGroup::TWaveGroup() { @@ -122,7 +122,7 @@ void JASBasicWaveBank::TWaveGroup::onEraseDone() { u32 JASBasicWaveBank::TWaveGroup::getWaveID(int index) const { JUT_ASSERT(298, index < mWaveCount); JUT_ASSERT(299, index >= 0); - return mCtrlWaveArray[index].field_0x0; + return mCtrlWaveArray[index].waveId; } intptr_t JASBasicWaveBank::TWaveHandle::getWavePtr() const { @@ -131,5 +131,5 @@ intptr_t JASBasicWaveBank::TWaveHandle::getWavePtr() const { if (base == 0) { return 0; } - return (intptr_t)base + field_0x4.mOffsetStart; + return (intptr_t)base + mWaveInfo.mOffsetStart; } diff --git a/libs/JSystem/src/JAudio2/JASChannel.cpp b/libs/JSystem/src/JAudio2/JASChannel.cpp index a99c13ce63..5b4a377321 100644 --- a/libs/JSystem/src/JAudio2/JASChannel.cpp +++ b/libs/JSystem/src/JAudio2/JASChannel.cpp @@ -26,6 +26,9 @@ JASChannel::JASChannel(Callback i_callback, void* i_callbackData) : mCallback(i_callback), mCallbackData(i_callbackData), mUpdateTimer(0), +#if TARGET_PC + mAramBaseAddress(nullptr), +#endif mBankDisposeID(NULL), mKey(0), mVelocity(0x7f), @@ -34,7 +37,7 @@ JASChannel::JASChannel(Callback i_callback, void* i_callbackData) : mKeySweepCount(0), mSkipSamples(0) { - field_0xdc.mChannelType = 0; + mAnon.mChannelType = 0; field_0x104 = 0; mMixConfig[0].whole = 0x150; mMixConfig[1].whole = 0x210; @@ -173,11 +176,7 @@ void JASChannel::updateEffectorParam(JASDsp::TChannel* i_channel, u16* i_mixerVo f32 pan = 0.5f; f32 dolby = 0.0f; -#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; @@ -237,7 +236,7 @@ s32 JASChannel::initialUpdateDSPChannel(JASDsp::TChannel* i_channel) { mCallback(CB_START, this, i_channel, mCallbackData); } - if (field_0xdc.mWaveInfo.field_0x20[0] == 0) { + if (mAnon.mWaveInfo.mpLoaded[0] == 0) { JUT_WARN_DEVICE(346, 2, "%s", "Lost wave data while playing"); mDspCh->free(); mDspCh = NULL; @@ -253,9 +252,12 @@ s32 JASChannel::initialUpdateDSPChannel(JASDsp::TChannel* i_channel) { return -1; } - switch (field_0xdc.mChannelType) { + switch (mAnon.mChannelType) { case 0: - i_channel->setWaveInfo(field_0xdc.mWaveInfo, mWaveAramAddress, mSkipSamples); + i_channel->setWaveInfo(mAnon.mWaveInfo, mWaveAramAddress, mSkipSamples); +#if TARGET_PC + i_channel->mAramBaseAddress = mAramBaseAddress; +#endif break; case 2: i_channel->setOscInfo(mOscillatorSomething); @@ -315,7 +317,7 @@ s32 JASChannel::updateDSPChannel(JASDsp::TChannel* i_channel) { mCallback(CB_PLAY, this, i_channel, mCallbackData); } - if (field_0xdc.mWaveInfo.field_0x20[0] == 0) { + if (mAnon.mWaveInfo.mpLoaded[0] == 0) { JUT_WARN_DEVICE(456, 2, "%s","Lost wave data while playing"); mDspCh->free(); mDspCh = NULL; diff --git a/libs/JSystem/src/JAudio2/JASDSPInterface.cpp b/libs/JSystem/src/JAudio2/JASDSPInterface.cpp index 7a9b5cda3e..375447dfbb 100644 --- a/libs/JSystem/src/JAudio2/JASDSPInterface.cpp +++ b/libs/JSystem/src/JAudio2/JASDSPInterface.cpp @@ -523,9 +523,12 @@ void JASDsp::TChannel::init() { void JASDsp::TChannel::playStart() { JUT_ASSERT(508, dspMutex); field_0x10c = 0; +#if !TARGET_PC field_0x060 = 0; +#endif mResetFlag = 1; field_0x066 = 0; +#if !TARGET_PC int i; for (i = 0; i < 4; i++) { field_0x078[i] = 0; @@ -534,6 +537,7 @@ void JASDsp::TChannel::playStart() { for (i = 0; i < 20; i++) { field_0x080[i] = 0; } +#endif mIsActive = 1; } @@ -601,9 +605,11 @@ void JASDsp::TChannel::setWaveInfo(JASWaveInfo const& waveInfo, u32 aramAddress, break; } } +#if !TARGET_PC for (i = 0; i < 16; i++) { field_0x0b0[i] = 0; } +#endif } } diff --git a/libs/JSystem/src/JAudio2/JASDriverIF.cpp b/libs/JSystem/src/JAudio2/JASDriverIF.cpp index 87a88adca0..a08e3339ea 100644 --- a/libs/JSystem/src/JAudio2/JASDriverIF.cpp +++ b/libs/JSystem/src/JAudio2/JASDriverIF.cpp @@ -3,6 +3,7 @@ #include "JSystem/JAudio2/JASDriverIF.h" #include "JSystem/JAudio2/JASAiCtrl.h" #include "JSystem/JAudio2/JASDSPInterface.h" +#include "dusk/settings.h" #include void JASDriver::setDSPLevel(f32 param_0) { @@ -30,7 +31,18 @@ void JASDriver::setOutputMode(u32 mode) { } u32 JASDriver::getOutputMode() { +#ifdef TARGET_PC + switch (dusk::getSettings().audio.outputMode) { + case dusk::AudioOutputMode::StereoSpeakers: + return JAS_OUTPUT_STEREO; + case dusk::AudioOutputMode::StereoHeadphones: + case dusk::AudioOutputMode::Surround6ch: + case dusk::AudioOutputMode::Surround8ch: + return JAS_OUTPUT_SURROUND; + } +#else return JASDriver::JAS_SYSTEM_OUTPUT_MODE; +#endif } void JASDriver::waitSubFrame() { diff --git a/libs/JSystem/src/JAudio2/JASDrumSet.cpp b/libs/JSystem/src/JAudio2/JASDrumSet.cpp index 399cc117ec..4b6a95b289 100644 --- a/libs/JSystem/src/JAudio2/JASDrumSet.cpp +++ b/libs/JSystem/src/JAudio2/JASDrumSet.cpp @@ -26,8 +26,8 @@ void JASDrumSet::newPercArray(u8 num, JKRHeap* heap) { } } -bool JASDrumSet::getParam(int key, int param_1, JASInstParam* param_2) const { - UNUSED(param_1); +bool JASDrumSet::getParam(int key, int velocity, JASInstParam* o_param) const { + UNUSED(velocity); JUT_ASSERT(48, key >= 0); if (mPercArray == NULL) { return false; @@ -43,12 +43,12 @@ bool JASDrumSet::getParam(int key, int param_1, JASInstParam* param_2) const { return false; } - param_2->field_0x1c = 0; - param_2->field_0x1e = 1; - param_2->mVolume = perc->mVolume; - param_2->mPitch = perc->mPitch; - param_2->mPan = perc->mPan; - param_2->field_0x18 = u16(perc->field_0xc); + o_param->mChannelType = 0; + o_param->mDontSetKey = 1; + o_param->mVolume = perc->mVolume; + o_param->mPitch = perc->mPitch; + o_param->mPan = perc->mPan; + o_param->mDirectRelease = u16(perc->mDirectRelease); static JASOscillator::Data osc; @@ -61,11 +61,11 @@ bool JASDrumSet::getParam(int key, int param_1, JASInstParam* param_2) const { static JASOscillator::Data* oscp = &osc; - param_2->field_0x14 = &oscp; - param_2->field_0x1d = 1; - param_2->mVolume *= perc->field_0x10; - param_2->mPitch *= perc->field_0x14; - param_2->field_0x1a = u16(perc->field_0xe); + o_param->mOscillators = &oscp; + o_param->mOscillatorCount = 1; + o_param->mVolume *= perc->mVolumeMult; + o_param->mPitch *= perc->mPitchMult; + o_param->mWaveId = u16(perc->mWaveId); return true; } @@ -107,12 +107,12 @@ JASDrumSet::TPerc::TPerc() { mVolume = 1.0f; mPitch = 1.0f; mPan = 0.5f; - field_0xc = 1000; + mDirectRelease = 1000; } void JASDrumSet::TPerc::setRelease(u32 release) { JUT_ASSERT(224, release < 0x10000); - field_0xc = release; + mDirectRelease = release; } u32 JASDrumSet::getType() const { return 'PERC'; } diff --git a/libs/JSystem/src/JAudio2/JASSimpleWaveBank.cpp b/libs/JSystem/src/JAudio2/JASSimpleWaveBank.cpp index 52935324d8..f9e4382512 100644 --- a/libs/JSystem/src/JAudio2/JASSimpleWaveBank.cpp +++ b/libs/JSystem/src/JAudio2/JASSimpleWaveBank.cpp @@ -3,7 +3,7 @@ #include "JSystem/JAudio2/JASSimpleWaveBank.h" #include -JASSimpleWaveBank::JASSimpleWaveBank() { +JASSimpleWaveBank::JASSimpleWaveBank(IF_DUSK(u32 bankId)) IF_DUSK(: JASWaveBank(bankId)) { mWaveTable = NULL; mWaveTableSize = 0; } @@ -28,7 +28,7 @@ JASWaveHandle* JASSimpleWaveBank::getWaveHandle(u32 no) const { void JASSimpleWaveBank::setWaveInfo(u32 no, JASWaveInfo const& waveInfo) { mWaveTable[no].mWaveInfo = waveInfo; - mWaveTable[no].mWaveInfo.field_0x20 = &_48; + mWaveTable[no].mWaveInfo.mpLoaded = &mCurrentlyLoaded; mWaveTable[no].mHeap = &mHeap; } diff --git a/libs/JSystem/src/JAudio2/JASTrack.cpp b/libs/JSystem/src/JAudio2/JASTrack.cpp index 4b1a7ba7aa..32e66e140c 100644 --- a/libs/JSystem/src/JAudio2/JASTrack.cpp +++ b/libs/JSystem/src/JAudio2/JASTrack.cpp @@ -288,13 +288,13 @@ void JASTrack::setLatestKey(u8 param_0) { field_0x22b += getTransposeTotal(); } -JASChannel* JASTrack::channelStart(JASTrack::TChannelMgr* i_channelMgr, u32 param_1, u32 param_2, +JASChannel* JASTrack::channelStart(JASTrack::TChannelMgr* i_channelMgr, u32 key, u32 velocity, u32 i_updateTimer) { JASBank* bank = NULL; if (mBankTable != NULL) { bank = mBankTable->getBank(mBankNumber); } - JASChannel* channel = JASBank::noteOn(bank, mProgNumber, param_1, param_2, + JASChannel* channel = JASBank::noteOn(bank, mProgNumber, key, velocity, mNoteOnPrio | mReleasePrio << 8, channelUpdateCallback, i_channelMgr); if (channel == NULL) { @@ -315,17 +315,17 @@ JASChannel* JASTrack::channelStart(JASTrack::TChannelMgr* i_channelMgr, u32 para return channel; } -int JASTrack::noteOn(u32 noteid, u32 param_1, u32 param_2) { +int JASTrack::noteOn(u32 noteid, u32 key, u32 velocity) { JUT_ASSERT(486, noteid != 0 && noteid < TChannelMgr::CHANNEL_MAX); if (isMute()) { return 0; } int ret = 1; - param_1 += getTransposeTotal(); + key += getTransposeTotal(); for (u32 i = 0; i < mChannelMgrCount; i++) { if (mChannelMgrs[i] != NULL) { mChannelMgrs[i]->noteOff(noteid, 0); - JASChannel* channel = channelStart(mChannelMgrs[i], param_1, param_2, 0); + JASChannel* channel = channelStart(mChannelMgrs[i], key, velocity, 0); if (channel == NULL) { ret = 0; } @@ -356,7 +356,7 @@ int JASTrack::gateOn(u32 param_0, u32 i_velocity, f32 i_time, u32 i_flags) { } else { JASChannel* channel = channel_mgr->mChannels[0]; if (channel != NULL) { - channel->setKey(uvar7 - channel->field_0xdc.mWaveInfo.mBaseKey); + channel->setKey(uvar7 - channel->mAnon.mWaveInfo.mBaseKey); channel->setVelocity(i_velocity); channel->setUpdateTimer(update_timer); } diff --git a/libs/JSystem/src/JAudio2/JASVoiceBank.cpp b/libs/JSystem/src/JAudio2/JASVoiceBank.cpp index 0d3cd3adb2..3ba1dff173 100644 --- a/libs/JSystem/src/JAudio2/JASVoiceBank.cpp +++ b/libs/JSystem/src/JAudio2/JASVoiceBank.cpp @@ -15,9 +15,9 @@ bool JASVoiceBank::getInstParam(int param_0, int param_1, int param_2, return false; } sOscTable = (JASOscillator::Data*)&sOscData; - param_3->field_0x1a = param_0; - param_3->field_0x1d = 1; - param_3->field_0x14 = &sOscTable; + param_3->mWaveId = param_0; + param_3->mOscillatorCount = 1; + param_3->mOscillators = &sOscTable; return true; } diff --git a/libs/JSystem/src/JAudio2/JASWSParser.cpp b/libs/JSystem/src/JAudio2/JASWSParser.cpp index 519eaa3307..91552b24c5 100644 --- a/libs/JSystem/src/JAudio2/JASWSParser.cpp +++ b/libs/JSystem/src/JAudio2/JASWSParser.cpp @@ -30,7 +30,7 @@ JASBasicWaveBank* JASWSParser::createBasicWaveBank(void const* stream, JKRHeap* u32 free_size = heap->getFreeSize(); THeader* header = (THeader*)stream; - JASBasicWaveBank* wave_bank = JKR_NEW_ARGS (heap, 0) JASBasicWaveBank; + JASBasicWaveBank* wave_bank = JKR_NEW_ARGS (heap, 0) JASBasicWaveBank IF_DUSK((header->mId)); if (wave_bank == NULL) { return NULL; } @@ -60,7 +60,7 @@ JASBasicWaveBank* JASWSParser::createBasicWaveBank(void const* stream, JKRHeap* wave_info.mpLast = wave->mpLast; wave_info.mpPenult = wave->mpPenult; TCtrlWave* ctrl_wave = ctrl->mCtrlWaveOffsets[j].ptr(header); - u16 local_74 = JSULoHalf(ctrl_wave->_00); + u16 local_74 = JSULoHalf(ctrl_wave->mWaveAndGroupId); wave_bank->setWaveInfo(wave_group, j, local_74, wave_info); } wave_group->setFileName(archive->mFileName); @@ -82,7 +82,7 @@ JASSimpleWaveBank* JASWSParser::createSimpleWaveBank(void const* stream, JKRHeap return NULL; } - JASSimpleWaveBank* wave_bank = JKR_NEW_ARGS (heap, 0) JASSimpleWaveBank; + JASSimpleWaveBank* wave_bank = JKR_NEW_ARGS (heap, 0) JASSimpleWaveBank IF_DUSK((header->mId)); if (wave_bank == NULL) { return NULL; } @@ -94,7 +94,7 @@ JASSimpleWaveBank* JASWSParser::createSimpleWaveBank(void const* stream, JKRHeap TWaveArchive* archive = archiveBank->mArchiveOffsets[0].ptr(header); for (int i = 0; i < ctrl->mWaveCount; i++) { TCtrlWave* ctrlWave = ctrl->mCtrlWaveOffsets[i].ptr(header); - u32 tmp = JSULoHalf(ctrlWave->_00); + u32 tmp = JSULoHalf(ctrlWave->mWaveAndGroupId); if (max < tmp) { max = tmp; } @@ -116,8 +116,8 @@ JASSimpleWaveBank* JASWSParser::createSimpleWaveBank(void const* stream, JKRHeap wave_info.mpLast = wave->mpLast; wave_info.mpPenult = wave->mpPenult; TCtrlWave* ctrl_wave = ctrl->mCtrlWaveOffsets[i].ptr(header); - u32 tmp = JSULoHalf(ctrl_wave->_00); - wave_bank->setWaveInfo(tmp, wave_info); + u32 waveId = JSULoHalf(ctrl_wave->mWaveAndGroupId); + wave_bank->setWaveInfo(waveId, wave_info); } wave_bank->setFileName(archive->mFileName); diff --git a/libs/JSystem/src/JAudio2/JASWaveArcLoader.cpp b/libs/JSystem/src/JAudio2/JASWaveArcLoader.cpp index 02730f46b4..5ba257c423 100644 --- a/libs/JSystem/src/JAudio2/JASWaveArcLoader.cpp +++ b/libs/JSystem/src/JAudio2/JASWaveArcLoader.cpp @@ -38,7 +38,7 @@ char* JASWaveArcLoader::getCurrentDir() { } JASWaveArc::JASWaveArc() : mHeap(this) { - _48 = 0; + mCurrentlyLoaded = 0; mStatus = 0; mEntryNum = -1; mFileLength = 0; @@ -57,7 +57,7 @@ bool JASWaveArc::loadSetup(u32 param_0) { if (mStatus != 1) { return false; } - _48 = 1; + mCurrentlyLoaded = 1; mStatus = 2; return true; } @@ -71,7 +71,7 @@ bool JASWaveArc::eraseSetup() { mStatus = 0; return false; } - _48 = 0; + mCurrentlyLoaded = 0; mStatus = 0; return true; } @@ -91,7 +91,7 @@ void JASWaveArc::loadToAramCallback(void* this_) { bool JASWaveArc::sendLoadCmd() { JASMutexLock mutexLock(&mMutex); - _48 = 0; + mCurrentlyLoaded = 0; mStatus = 1; loadToAramCallbackParams commandInfo; commandInfo.mWavArc = this; diff --git a/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp b/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp index cc94097712..fe12281a01 100644 --- a/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp +++ b/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp @@ -47,7 +47,12 @@ namespace { mStreamFileDVDEntryNums[i] = DVDConvertPathToEntrynum(stack_14.getFilePath(i)); } } - virtual s32 getStreamFileEntry(JAISoundID id) { + virtual s32 getStreamFileEntry(JAISoundID id IF_DUSK_ARG(StreamReplacementSlot2 const* replacement)) { +#if TARGET_PC + if (replacement) { + CRASH("TStreamDataMgr::getStreamFileEntry not implemented for replacements"); + } +#endif u32 short_id = id.id_.info.waveID; if (short_id >= mNumStreamFiles) { return -1; diff --git a/libs/JSystem/src/JAudio2/JAUSoundTable.cpp b/libs/JSystem/src/JAudio2/JAUSoundTable.cpp index cab2e45a3c..802b1c3cb9 100644 --- a/libs/JSystem/src/JAudio2/JAUSoundTable.cpp +++ b/libs/JSystem/src/JAudio2/JAUSoundTable.cpp @@ -6,6 +6,8 @@ #include "JSystem/JAudio2/JAUSoundTable.h" +#include "dusk/mods/svc/audio_res/bst.hpp" + void JAUSoundTable::init(void const* param_0) { if (param_0 != NULL) { field_0x0.init(param_0); @@ -14,11 +16,16 @@ void JAUSoundTable::init(void const* param_0) { } } -u8 JAUSoundTable::getTypeID(JAISoundID param_0) const { +u8 JAUSoundTable::getTypeID(JAISoundID param_0 IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) const { #if DUSK_AUDIO_DISABLED if (this == NULL) { return 0xff; } +#endif +#if TARGET_PC + if (replacement) { + return replacement->get_type_id(); + } #endif if (param_0.isAnonymous()) { return 0xff; @@ -34,11 +41,16 @@ u8 JAUSoundTable::getTypeID(JAISoundID param_0) const { return group->getTypeID(param_0.id_.info.waveID); } -JAUSoundTableItem* JAUSoundTable::getData(JAISoundID param_0) const { +JAUSoundTableItem DUSK_CONST* JAUSoundTable::getData(JAISoundID param_0 IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) const { #if DUSK_AUDIO_DISABLED if (this == NULL) { return NULL; } +#endif +#if TARGET_PC + if (replacement) { + return &replacement->item; + } #endif if (param_0.isAnonymous()) { return NULL; diff --git a/libs/JSystem/src/JAudio2/JAUStreamFileTable.cpp b/libs/JSystem/src/JAudio2/JAUStreamFileTable.cpp index f30c5c4a01..7f37f21f3e 100644 --- a/libs/JSystem/src/JAudio2/JAUStreamFileTable.cpp +++ b/libs/JSystem/src/JAudio2/JAUStreamFileTable.cpp @@ -31,7 +31,12 @@ const char* JAUStreamFileTable::getFilePath(int index) const { return (char*)(data + *(int*)(data + 8 + (index * sizeof(s32)))); } -s32 JAUStreamDataMgr_StreamFileTable::getStreamFileEntry(JAISoundID soundId) { +s32 JAUStreamDataMgr_StreamFileTable::getStreamFileEntry(JAISoundID soundId IF_DUSK_ARG(StreamReplacementSlot2 const* replacement)) { +#if TARGET_PC + if (replacement) { + CRASH("JAUStreamDataMgr_StreamFileTable::getStreamFileEntry not implemented for replacements"); + } +#endif const char* filePath = getFilePath(soundId.id_.info.waveID); if (filePath == NULL) { return -1; diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index 8d6f4eb868..939659205e 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -14,7 +14,8 @@ #if TARGET_PC #include "dusk/dusk.h" -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/logging.h" #include "dusk/settings.h" #include "dusk/time.h" @@ -218,12 +219,13 @@ void JFWDisplay::endGX() { if (mFader != NULL) { ortho.setPort(); -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) { - mFader->advance(); - } - if (mFader->getStatus() != JUTFader::Wait) { - mFader->draw(); +#if TARGET_PC + if (dusk::game_clock::g_frameTiming.separatePresentation) { + if (mFader->getStatus() != JUTFader::Wait) { + mFader->draw(); + } + } else { + mFader->control(); } #else mFader->control(); @@ -382,7 +384,7 @@ static void waitForTick(u32 p1, u16 p2) { #if TARGET_PC static Limiter limiter; - if (dusk::frame_interp::is_enabled() || dusk::getTransientSettings().turboMode) { + if (dusk::interp::is_enabled() || dusk::getTransientSettings().turboMode) { limiter.Reset(); dusk::frameUsagePct = 0.f; return; diff --git a/libs/JSystem/src/JKernel/JKRArchivePri.cpp b/libs/JSystem/src/JKernel/JKRArchivePri.cpp index f81082cc2f..eed317a357 100644 --- a/libs/JSystem/src/JKernel/JKRArchivePri.cpp +++ b/libs/JSystem/src/JKernel/JKRArchivePri.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include "JSystem/JKernel/JKRDvdRipper.h" #if _WIN32 #include @@ -22,8 +21,6 @@ std::atomic JKRArchive::sArcOverlayGeneration{0}; namespace { -inline constexpr borealis::Log Log{"JKRArchivePri"}; - void* alloc_overlay_buffer(u32 size) { #if _WIN32 return _aligned_malloc(size, alignof(std::max_align_t)); @@ -455,18 +452,12 @@ bool JKRArchive::copyOverlayData(void* buffer, u32 bufferSize, SDIFileEntry* ent return false; } - if (overlaySize > bufferSize) { - std::string path; - getOverlayPath(entry, path); - Log.error("Overlay %s is %u bytes but the game reserved %u\n", path.c_str(), overlaySize, - bufferSize); - return false; - } - if (overlaySize != 0) { - memcpy(buffer, overlayData, overlaySize); + const u32 copySize = overlaySize < bufferSize ? overlaySize : bufferSize; + if (copySize != 0) { + memcpy(buffer, overlayData, copySize); } if (outSize != nullptr) { - *outSize = overlaySize; + *outSize = copySize; } return true; } diff --git a/libs/JSystem/src/JParticle/JPABaseShape.cpp b/libs/JSystem/src/JParticle/JPABaseShape.cpp index bab7e89646..fff0a6e5bc 100644 --- a/libs/JSystem/src/JParticle/JPABaseShape.cpp +++ b/libs/JSystem/src/JParticle/JPABaseShape.cpp @@ -10,7 +10,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/particle.h" #include #endif @@ -302,7 +302,7 @@ void JPAGenTexCrdMtxPrj(JPAEmitterWorkData* param_0) { void JPAGenCalcTexCrdMtxAnm(JPAEmitterWorkData* work) { ZoneScoped; JPABaseShape* shape = work->mpRes->getBsp(); - f32 dVar16 = work->mpEmtr->mTick; + f32 dVar16 = DUSK_IF_ELSE(dusk::interp::particle::emitter_age(work->mpEmtr), work->mpEmtr->mTick); f32 dVar15 = 0.5f * (1.0f + shape->getTilingS()); f32 dVar14 = 0.5f * (1.0f + shape->getTilingT()); f32 dVar11 = (dVar16 * shape->getIncTransX()) + shape->getInitTransX(); @@ -334,7 +334,7 @@ void JPAGenCalcTexCrdMtxAnm(JPAEmitterWorkData* work) { void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData* work, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { ZoneScoped; JPABaseShape* shape = work->mpRes->getBsp(); - f32 dVar16 = param_1->mAge; + f32 dVar16 = DUSK_IF_ELSE(ctx->age, param_1->mAge); f32 dVar15 = 0.5f * (1.0f + shape->getTilingS()); f32 dVar14 = 0.5f * (1.0f + shape->getTilingT()); f32 dVar11 = (dVar16 * shape->getIncTransX()) + shape->getInitTransX(); @@ -459,7 +459,7 @@ void loadPrj(JPAEmitterWorkData const* work, const Mtx srt) { static void loadPrjAnm(JPAEmitterWorkData const* work, const Mtx srt) { JPABaseShape* shape = work->mpRes->getBsp(); - f32 dVar16 = work->mpEmtr->getAge(); + f32 dVar16 = DUSK_IF_ELSE(dusk::interp::particle::emitter_age(work->mpEmtr), work->mpEmtr->getAge()); f32 dVar15 = 0.5f * (1.0f + shape->getTilingS()); f32 dVar14 = 0.5f * (1.0f + shape->getTilingT()); f32 dVar11 = (dVar16 * shape->getIncTransX()) + shape->getInitTransX(); @@ -548,24 +548,6 @@ static void submit_particle_quad( p_prj[work->mPrjType](work, posMtx); GXCallDisplayList(dl, dlSize); } - -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 JPA_DRAW_CTX_PARAM) { @@ -575,13 +557,6 @@ void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_C ZoneScoped; 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); } @@ -614,16 +589,6 @@ void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRA 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); @@ -735,7 +700,7 @@ void dirTypePosInv(JPAEmitterWorkData const* work, JPABaseParticle const* param_ void dirTypeEmtrDir(JPAEmitterWorkData const* work, JPABaseParticle const* param_1, JGeometry::TVec3* param_2) { - param_2->set(work->mGlobalEmtrDir); + param_2->set(DUSK_IF_ELSE(dusk::interp::particle::emitter_direction(work->mpEmtr, work->mGlobalEmtrDir), work->mGlobalEmtrDir)); } void dirTypePrevPtcl(JPAEmitterWorkData const* work, JPABaseParticle const* param_1, @@ -744,9 +709,14 @@ void dirTypePrevPtcl(JPAEmitterWorkData const* work, JPABaseParticle const* para param_1->getGlobalPosition(&aTStack_24); if (work->mpCurNode->getPrev() != work->mpAlivePtcl->getEnd()) { +#if TARGET_PC + param_2->set(dusk::interp::particle::position(work->mpCurNode->getPrev()->getObject())); +#else work->mpCurNode->getPrev()->getObject()->getGlobalPosition(param_2); +#endif } else { work->mpEmtr->calcEmitterGlobalPosition(param_2); + IF_DUSK(param_2->set(dusk::interp::particle::emitter_position(work->mpEmtr, *param_2))); } param_2->sub(aTStack_24); } @@ -876,171 +846,6 @@ static u8* p_dl[2] = { jpa_dl_x, }; -#if TARGET_PC -static bool make_direction_mtx(JPAEmitterWorkData* work, JPABaseParticle* ptcl, Mtx posMtx) { - JGeometry::TVec3 axisY; - JGeometry::TVec3 axisZ; - JGeometry::TVec3 baseAxis(ptcl->mBaseAxis); - p_direction[work->mDirType](work, ptcl, &axisY); - if (axisY.isZero()) { - return false; - } - - axisY.normalize(); - axisZ.cross(baseAxis, axisY); - if (axisZ.isZero()) { - return false; - } - - axisZ.normalize(); - baseAxis.cross(axisY, axisZ); - baseAxis.normalize(); - ptcl->mBaseAxis.set(baseAxis); - - f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; - f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; - posMtx[0][0] = baseAxis.x; - posMtx[0][1] = axisY.x; - posMtx[0][2] = axisZ.x; - posMtx[0][3] = ptcl->mPosition.x; - posMtx[1][0] = baseAxis.y; - posMtx[1][1] = axisY.y; - posMtx[1][2] = axisZ.y; - posMtx[1][3] = ptcl->mPosition.y; - posMtx[2][0] = baseAxis.z; - posMtx[2][1] = axisY.z; - posMtx[2][2] = axisZ.z; - posMtx[2][3] = ptcl->mPosition.z; - p_plane[work->mPlaneType](posMtx, scaleX, scaleY); - return true; -} - -static bool make_rot_direction_mtx(JPAEmitterWorkData* work, JPABaseParticle* ptcl, Mtx posMtx) { - f32 sinRot = JMASSin(ptcl->mRotateAngle); - f32 cosRot = JMASCos(ptcl->mRotateAngle); - JGeometry::TVec3 axisY; - JGeometry::TVec3 axisZ; - JGeometry::TVec3 baseAxis(ptcl->mBaseAxis); - p_direction[work->mDirType](work, ptcl, &axisY); - if (axisY.isZero()) { - return false; - } - - axisY.normalize(); - axisZ.cross(baseAxis, axisY); - if (axisZ.isZero()) { - return false; - } - - axisZ.normalize(); - baseAxis.cross(axisY, axisZ); - baseAxis.normalize(); - ptcl->mBaseAxis.set(baseAxis); - - f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; - f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; - Mtx rotMtx; - Mtx dirMtx; - p_rot[work->mRotType](sinRot, cosRot, rotMtx); - p_plane[work->mPlaneType](rotMtx, scaleX, scaleY); - dirMtx[0][0] = baseAxis.x; - dirMtx[0][1] = axisY.x; - dirMtx[0][2] = axisZ.x; - dirMtx[0][3] = ptcl->mPosition.x; - dirMtx[1][0] = baseAxis.y; - dirMtx[1][1] = axisY.y; - dirMtx[1][2] = axisZ.y; - dirMtx[1][3] = ptcl->mPosition.y; - dirMtx[2][0] = baseAxis.z; - dirMtx[2][1] = axisY.z; - dirMtx[2][2] = axisZ.z; - dirMtx[2][3] = ptcl->mPosition.z; - MTXConcat(dirMtx, rotMtx, posMtx); - return true; -} - -void JPAInterpDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { - 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(); - 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 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 JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; @@ -1049,13 +854,6 @@ void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_C ZoneScoped; Mtx posMtx; -#if TARGET_PC - if (!dusk::frame_interp::lookup_replacement(ptcl, posMtx) && - !make_direction_mtx(work, ptcl, posMtx)) - { - return; - } -#else { JGeometry::TVec3 axisY; JGeometry::TVec3 axisZ; @@ -1091,7 +889,6 @@ void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_C posMtx[2][3] = ptcl->mPosition.z; p_plane[work->mPlaneType](posMtx, scaleX, scaleY); } -#endif MTXConcat(work->mPosCamMtx, posMtx, posMtx); #if TARGET_PC @@ -1112,13 +909,6 @@ void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRA Mtx mtx1; Mtx mtx2; -#if TARGET_PC - if (!dusk::frame_interp::lookup_replacement(ptcl, mtx1) && - !make_rot_direction_mtx(work, ptcl, mtx1)) - { - return; - } -#else { f32 sinRot = JMASSin(ptcl->mRotateAngle); f32 cosRot = JMASCos(ptcl->mRotateAngle); @@ -1158,7 +948,6 @@ void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRA mtx2[2][3] = ptcl->mPosition.z; MTXConcat(mtx2, mtx1, mtx1); } -#endif MTXConcat(work->mPosCamMtx, mtx1, mtx2); #if TARGET_PC submit_particle_quad(work, ctx, mtx2, p_dl[work->mDLType], sizeof(jpa_dl)); @@ -1330,6 +1119,11 @@ void JPADrawStripe(JPAEmitterWorkData* param_0) { node = node_func(node), coord += step) { param_0->mpCurNode = node; JPABaseParticle* particle = node->getObject(); +#if TARGET_PC + JPABaseParticle scratch; + float age; + particle = dusk::interp::particle::present(particle, scratch, age); +#endif local_ec.set(particle->mPosition); dVar11 = JMASSin(particle->mRotateAngle); dVar12 = JMASCos(particle->mRotateAngle); @@ -1421,6 +1215,11 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { node = node_func(node), coord += step) { param_0->mpCurNode = node; JPABaseParticle* particle = node->getObject(); +#if TARGET_PC + JPABaseParticle scratch; + float age; + particle = dusk::interp::particle::present(particle, scratch, age); +#endif local_b4.set(particle->mPosition); dVar11 = JMASSin(particle->mRotateAngle); dVar12 = JMASCos(particle->mRotateAngle); @@ -1471,6 +1270,11 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { node = node_func(node), coord += step) { param_0->mpCurNode = node; JPABaseParticle* particle = node->getObject(); +#if TARGET_PC + JPABaseParticle scratch; + float age; + particle = dusk::interp::particle::present(particle, scratch, age); +#endif local_b4.set(particle->mPosition); dVar11 = JMASCos(particle->mRotateAngle); dVar12 = -JMASSin(particle->mRotateAngle); diff --git a/libs/JSystem/src/JParticle/JPAEmitter.cpp b/libs/JSystem/src/JParticle/JPAEmitter.cpp index aa168a44a4..e033c9bc7e 100644 --- a/libs/JSystem/src/JParticle/JPAEmitter.cpp +++ b/libs/JSystem/src/JParticle/JPAEmitter.cpp @@ -7,10 +7,15 @@ #include "JSystem/JParticle/JPABaseShape.h" #include +#if TARGET_PC +#include "dusk/interp/particle.h" +#endif + JPAEmitterCallBack::~JPAEmitterCallBack() { } void JPABaseEmitter::init(JPAEmitterManager* param_0, JPAResource* param_1) { + IF_DUSK(dusk::interp::particle::reset(this)); mpEmtrMgr = param_0; pRes = param_1; pRes->getDyn()->getEmitterScl(&mLocalScl); diff --git a/libs/JSystem/src/JParticle/JPAParticle.cpp b/libs/JSystem/src/JParticle/JPAParticle.cpp index 99ba95453c..a98f228337 100644 --- a/libs/JSystem/src/JParticle/JPAParticle.cpp +++ b/libs/JSystem/src/JParticle/JPAParticle.cpp @@ -8,7 +8,7 @@ #include "JSystem/JParticle/JPAExtraShape.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/particle.h" #endif JPAParticleCallBack::~JPAParticleCallBack() { @@ -16,6 +16,7 @@ JPAParticleCallBack::~JPAParticleCallBack() { } void JPABaseParticle::init_p(JPAEmitterWorkData* work) { + IF_DUSK(dusk::interp::particle::reset(this)); JPABaseEmitter* emtr = work->mpEmtr; JPAExtraShape* esp = work->mpRes->getEsp(); JPABaseShape* bsp = work->mpRes->getBsp(); @@ -126,6 +127,7 @@ void JPABaseParticle::init_p(JPAEmitterWorkData* work) { } void JPABaseParticle::init_c(JPAEmitterWorkData* work, JPABaseParticle* parent) { + IF_DUSK(dusk::interp::particle::reset_child(this, parent, work)); JPABaseEmitter* emtr = work->mpEmtr; JPAChildShape* csp = work->mpRes->getCsp(); @@ -208,27 +210,6 @@ void JPABaseParticle::init_c(JPAEmitterWorkData* work, JPABaseParticle* parent) mTexAnmIdx = 0; } -#if TARGET_PC -void JPABaseParticle::interp(JPAEmitterWorkData* work, void const* drawFunc) { - if (!dusk::frame_interp::is_enabled()) - 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; @@ -267,21 +248,13 @@ bool JPABaseParticle::calc_p(JPAEmitterWorkData* work) { } } + IF_DUSK(dusk::interp::particle::capture_birth(this, work)); mLocalPosition.add(mVelocity); mPosition.set(mOffsetPosition.x + mLocalPosition.x * work->mPublicScale.x, 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 + IF_DUSK(dusk::interp::particle::capture(this)); return false; } @@ -320,27 +293,13 @@ bool JPABaseParticle::calc_c(JPAEmitterWorkData* work) { work->mpRes->calc_c(work, this); mRotateAngle += mRotateSpeed; + IF_DUSK(dusk::interp::particle::capture_birth(this, work)); mLocalPosition.add(mVelocity); mPosition.set(mOffsetPosition.x + mLocalPosition.x * work->mPublicScale.x, 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 + IF_DUSK(dusk::interp::particle::capture(this)); return false; } diff --git a/libs/JSystem/src/JParticle/JPAResource.cpp b/libs/JSystem/src/JParticle/JPAResource.cpp index 1eef1b08a5..aaddb1ae5f 100644 --- a/libs/JSystem/src/JParticle/JPAResource.cpp +++ b/libs/JSystem/src/JParticle/JPAResource.cpp @@ -15,7 +15,7 @@ #include "global.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/particle.h" #include @@ -837,15 +837,6 @@ bool JPAResource::calc(JPAEmitterWorkData* work, JPABaseEmitter* emtr) { } } -#ifdef TARGET_PC - if (((pBsp && pBsp->getDirType() == 3) || (pCsp && pCsp->getDirType() == 3)) && - dusk::frame_interp::is_enabled()) - { - // ensure mGlobalEmtrDir is valid - calcWorkData_d(work); - } -#endif - JPANode* next = NULL; for (JPANode* node = emtr->mAlivePtclBase.getFirst(); node != emtr->mAlivePtclBase.getEnd(); node = next) { next = node->getNext(); @@ -862,6 +853,7 @@ bool JPAResource::calc(JPAEmitterWorkData* work, JPABaseEmitter* emtr) { } emtr->mTick++; + IF_DUSK(dusk::interp::particle::capture(emtr)); } return false; @@ -872,6 +864,7 @@ void JPAResource::draw(JPAEmitterWorkData* work, JPABaseEmitter* emtr) { work->mpEmtr = emtr; work->mpRes = this; work->mDrawCount = 0; + IF_DUSK(dusk::interp::particle::EmitterDraw presentation(emtr)); calcWorkData_d(work); pBsp->setGX(work); for (s32 i = 1; i <= emtr->getDrawTimes(); i++) { @@ -1049,8 +1042,10 @@ static bool draw_particle_batch(JPAEmitterWorkData* work) { GXBegin(GX_QUADS, GX_VTXFMT1, GX_AUTO); while (node != work->mpEmtr->mAlivePtclBase.getEnd()) { work->mpCurNode = node; + JPABaseParticle scratch; + JPABaseParticle* particle = dusk::interp::particle::present(node->getObject(), scratch, ctx.age); for (int i = res->mpDrawParticleFuncListNum - 1; i >= 0; i--) { - (*res->mpDrawParticleFuncList[i])(work, node->getObject(), &ctx); + (*res->mpDrawParticleFuncList[i])(work, particle, &ctx); } node = fwdAhead ? node->getPrev() : node->getNext(); } @@ -1111,9 +1106,13 @@ void JPAResource::drawP(JPAEmitterWorkData* work) { JPANode* node = work->mpEmtr->mAlivePtclBase.getLast(); for (; node != work->mpEmtr->mAlivePtclBase.getEnd(); node = node->getPrev()) { work->mpCurNode = node; +#if TARGET_PC + JPABaseParticle scratch; + JPABaseParticle* particle = dusk::interp::particle::present(node->getObject(), scratch, ctx.age); +#endif if (mpDrawParticleFuncList != NULL) { for (int i = mpDrawParticleFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); + (*mpDrawParticleFuncList[i])(work, DUSK_IF_ELSE(particle, node->getObject()) JPA_DRAW_CTX_ARG); } } } @@ -1121,9 +1120,13 @@ void JPAResource::drawP(JPAEmitterWorkData* work) { JPANode* node = work->mpEmtr->mAlivePtclBase.getFirst(); for (; node != work->mpEmtr->mAlivePtclBase.getEnd(); node = node->getNext()) { work->mpCurNode = node; +#if TARGET_PC + JPABaseParticle scratch; + JPABaseParticle* particle = dusk::interp::particle::present(node->getObject(), scratch, ctx.age); +#endif if (mpDrawParticleFuncList != NULL) { for (int i = mpDrawParticleFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); + (*mpDrawParticleFuncList[i])(work, DUSK_IF_ELSE(particle, node->getObject()) JPA_DRAW_CTX_ARG); } } } @@ -1178,9 +1181,13 @@ void JPAResource::drawC(JPAEmitterWorkData* work) { JPANode* node = work->mpEmtr->mAlivePtclChld.getLast(); for (; node != work->mpEmtr->mAlivePtclChld.getEnd(); node = node->getPrev()) { work->mpCurNode = node; +#if TARGET_PC + JPABaseParticle scratch; + JPABaseParticle* particle = dusk::interp::particle::present(node->getObject(), scratch, ctx.age); +#endif if (mpDrawParticleChildFuncList != NULL) { for (int i = mpDrawParticleChildFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleChildFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); + (*mpDrawParticleChildFuncList[i])(work, DUSK_IF_ELSE(particle, node->getObject()) JPA_DRAW_CTX_ARG); } } } @@ -1188,9 +1195,13 @@ void JPAResource::drawC(JPAEmitterWorkData* work) { JPANode* node = work->mpEmtr->mAlivePtclChld.getFirst(); for (; node != work->mpEmtr->mAlivePtclChld.getEnd(); node = node->getNext()) { work->mpCurNode = node; +#if TARGET_PC + JPABaseParticle scratch; + JPABaseParticle* particle = dusk::interp::particle::present(node->getObject(), scratch, ctx.age); +#endif if (mpDrawParticleChildFuncList != NULL) { for (int i = mpDrawParticleChildFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleChildFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); + (*mpDrawParticleChildFuncList[i])(work, DUSK_IF_ELSE(particle, node->getObject()) JPA_DRAW_CTX_ARG); } } } diff --git a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp index 2f408d6184..b6f41098e5 100644 --- a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp +++ b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp @@ -4,7 +4,7 @@ #if TARGET_PC #include "dusk/audio.h" -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/settings.h" #endif @@ -655,10 +655,10 @@ value_or_fun: value: #if TARGET_PC - if (dusk::frame_interp::is_enabled() && u <= 5 && + if (dusk::interp::is_enabled() && u <= 5 && (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) { - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); } #endif adaptor->adaptor_setVariableValue(control, u, operation, param_2, param_3); @@ -666,11 +666,11 @@ value: value_n: #if TARGET_PC - if (dusk::frame_interp::is_enabled() && + if (dusk::interp::is_enabled() && (pN == TAdaptor_camera::sauVariableValue_3_POSITION_XYZ || pN == TAdaptor_camera::sauVariableValue_3_TARGET_POSITION_XYZ) && (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) { - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); } #endif adaptor->adaptor_setVariableValue_n(control, pN, u, operation, param_2, param_3); diff --git a/libs/JSystem/src/JUtility/JUTFader.cpp b/libs/JSystem/src/JUtility/JUTFader.cpp index a8d9fe6028..cf9a9e2364 100644 --- a/libs/JSystem/src/JUtility/JUTFader.cpp +++ b/libs/JSystem/src/JUtility/JUTFader.cpp @@ -8,9 +8,10 @@ #include "JSystem/JUtility/JUTFader.h" #include "JSystem/J2DGraph/J2DOrthoGraph.h" -#ifdef TARGET_PC +#if TARGET_PC +#include "dusk/game_clock.h" + #include -#include "dusk/frame_interpolation.h" #endif JUTFader::JUTFader(int x, int y, int width, int height, JUtility::TColor pColor) @@ -38,6 +39,7 @@ void JUTFader::advance() { case FadeIn: #if AVOID_UB if (mDuration == 0) { + IF_DUSK(mColor.a = 0); mStatus = Wait; break; } @@ -52,6 +54,7 @@ void JUTFader::advance() { case FadeOut: #if AVOID_UB if (mDuration == 0) { + IF_DUSK(mColor.a = 0xFF); mStatus = None; break; } @@ -68,26 +71,26 @@ void JUTFader::advance() { void JUTFader::control() { advance(); + IF_DUSK_BLOCK(getStatus() != Wait) draw(); + IF_DUSK_BLOCK_END } 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); - } +#if TARGET_PC + JUtility::TColor color = mColor; + if (dusk::game_clock::g_frameTiming.separatePresentation && mDuration != 0 && + (mStatus == FadeIn || mStatus == FadeOut)) + { + const f32 timer = std::min(mTimer + dusk::game_clock::sample_interpolation_step(), + static_cast(mDuration)); + const u8 alpha = static_cast((timer * 0xFF) / mDuration); + color.a = mStatus == FadeIn ? 0xFF - alpha : alpha; + } #endif + if (DUSK_IF_ELSE(color, mColor).a != 0) { J2DOrthoGraph orthograph; - orthograph.setColor(mColor); + orthograph.setColor(DUSK_IF_ELSE(color, mColor)); orthograph.fillBox(mBox); } } diff --git a/libs/freeverb/include/freeverb/tuning.h b/libs/freeverb/include/freeverb/tuning.h index baaa9ce004..727c01b680 100644 --- a/libs/freeverb/include/freeverb/tuning.h +++ b/libs/freeverb/include/freeverb/tuning.h @@ -7,54 +7,50 @@ #ifndef _tuning_ #define _tuning_ -const int numcombs = 8; -const int numallpasses = 4; -const float muted = 0; -const float fixedgain = 0.015f; -const float scalewet = 3; -const float scaledry = 2; -const float scaledamp = 0.4f; -const float scaleroom = 0.28f; -const float offsetroom = 0.7f; -const float initialroom = 0.5f; -const float initialdamp = 0.5f; -const float initialwet = 1/scalewet; -const float initialdry = 0; -const float initialwidth = 1; -const float initialmode = 0; -const float freezemode = 0.5f; -const int stereospread = 23; +constexpr int numcombs = 8; +constexpr int numallpasses = 4; +constexpr float muted = 0; +constexpr float fixedgain = 0.015f; +constexpr float scalewet = 3; +constexpr float scaledry = 2; +constexpr float scaledamp = 0.4f; +constexpr float scaleroom = 0.28f; +constexpr float offsetroom = 0.7f; +constexpr float initialroom = 0.5f; +constexpr float initialdamp = 0.5f; +constexpr float initialwet = 1/scalewet; +constexpr float initialdry = 0; +constexpr float initialwidth = 1; +constexpr float initialmode = 0; +constexpr float freezemode = 0.5f; +constexpr float srscale = 48000.0f/32000.0f; +constexpr int stereospread = 23*srscale; -// These values assume 44.1KHz sample rate -// they will probably be OK for 48KHz sample rate -// but would need scaling for 96KHz (or other) sample rates. -// The values were obtained by listening tests. -const int combtuningL1 = 1116; -const int combtuningR1 = 1116+stereospread; -const int combtuningL2 = 1188; -const int combtuningR2 = 1188+stereospread; -const int combtuningL3 = 1277; -const int combtuningR3 = 1277+stereospread; -const int combtuningL4 = 1356; -const int combtuningR4 = 1356+stereospread; -const int combtuningL5 = 1422; -const int combtuningR5 = 1422+stereospread; -const int combtuningL6 = 1491; -const int combtuningR6 = 1491+stereospread; -const int combtuningL7 = 1557; -const int combtuningR7 = 1557+stereospread; -const int combtuningL8 = 1617; -const int combtuningR8 = 1617+stereospread; -const int allpasstuningL1 = 556; -const int allpasstuningR1 = 556+stereospread; -const int allpasstuningL2 = 441; -const int allpasstuningR2 = 441+stereospread; -const int allpasstuningL3 = 341; -const int allpasstuningR3 = 341+stereospread; -const int allpasstuningL4 = 225; -const int allpasstuningR4 = 225+stereospread; +constexpr int combtuningL1 = 1116*srscale; +constexpr int combtuningR1 = combtuningL1+stereospread; +constexpr int combtuningL2 = 1188*srscale; +constexpr int combtuningR2 = combtuningL2+stereospread; +constexpr int combtuningL3 = 1277*srscale; +constexpr int combtuningR3 = combtuningL3+stereospread; +constexpr int combtuningL4 = 1356*srscale; +constexpr int combtuningR4 = combtuningL4+stereospread; +constexpr int combtuningL5 = 1422*srscale; +constexpr int combtuningR5 = combtuningL5+stereospread; +constexpr int combtuningL6 = 1491*srscale; +constexpr int combtuningR6 = combtuningL6+stereospread; +constexpr int combtuningL7 = 1557*srscale; +constexpr int combtuningR7 = combtuningL7+stereospread; +constexpr int combtuningL8 = 1617*srscale; +constexpr int combtuningR8 = combtuningL8+stereospread; +constexpr int allpasstuningL1 = 556*srscale; +constexpr int allpasstuningR1 = allpasstuningL1+stereospread; +constexpr int allpasstuningL2 = 441*srscale; +constexpr int allpasstuningR2 = allpasstuningL2+stereospread; +constexpr int allpasstuningL3 = 341*srscale; +constexpr int allpasstuningR3 = allpasstuningL3+stereospread; +constexpr int allpasstuningL4 = 225*srscale; +constexpr int allpasstuningR4 = allpasstuningL4+stereospread; #endif//_tuning_ //ends - diff --git a/libs/freeverb/revmodel.cpp b/libs/freeverb/revmodel.cpp index d4f9cf2513..80b143be9d 100644 --- a/libs/freeverb/revmodel.cpp +++ b/libs/freeverb/revmodel.cpp @@ -6,6 +6,7 @@ #include "revmodel.hpp" #include "denormals.h" +#include revmodel::revmodel() { @@ -194,8 +195,9 @@ void revmodel::update() for(i=0; iinfo(mod_ctx, "template_mod initialized"); + AudioWaveHandle handle; + svc_audio_res->replace_wave( + mod_ctx, AUDIO_WAVE_BANK_SOUND_EFFECTS, 4238, "res/go.opus", nullptr, &handle); + svc_audio_res->replace_wave( + mod_ctx, AUDIO_WAVE_BANK_SOUND_EFFECTS, 4237, "res/go.opus", nullptr, &handle); + + constexpr AudioSoundTableEffectInfo effect(128, 1, 1.5); + + svc_audio_res->replace_sound_table_effect(mod_ctx, SE_CATEGORY_CHARA_SE, + 0, // MIDNA_APPEAR + &effect, nullptr); + + return MOD_OK; +} + +MOD_EXPORT ModResult mod_update(ModError*) { + return MOD_OK; +} + +MOD_EXPORT ModResult mod_shutdown(ModError*) { + svc_log->info(mod_ctx, "template_mod unloaded"); + return MOD_OK; +} +} diff --git a/mods/luau_runtime/CMakeLists.txt b/mods/luau_runtime/CMakeLists.txt index fa38b64b8a..f7729d3db2 100644 --- a/mods/luau_runtime/CMakeLists.txt +++ b/mods/luau_runtime/CMakeLists.txt @@ -26,15 +26,37 @@ FetchContent_Declare(luau ) FetchContent_MakeAvailable(luau) +FetchContent_Declare(luabridge3 + URL https://github.com/TwilitRealm/LuaBridge3/archive/refs/tags/twilit-v1.zip + URL_HASH SHA256=aa65e6e3446363d03842134182db28e6206e08d366b8efb8f66d26f96a3314be + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) +FetchContent_MakeAvailable(luabridge3) + add_mod(luau_runtime SOURCES - src/bindings.cpp - src/config.cpp + src/services/bindings.cpp + src/services/config.cpp + src/services/config.hpp + src/services/services.cpp + src/services/services.hpp + src/services/ui.cpp + src/services/ui.hpp + src/lua_bridge_helpers.cpp + src/lua_bridge_helpers.hpp + src/lua_helpers.cpp + src/lua_helpers.hpp + src/runtime.hpp src/runtime.cpp - src/ui.cpp MOD_JSON mod.json RES_DIR res BUNDLE ) -target_link_libraries(luau_runtime PRIVATE Luau.VM Luau.Compiler) +target_precompile_headers(luau_runtime PRIVATE src/lua_bridge_helpers.hpp src/lua_helpers.hpp) + +# As far as I can tell, Luau already does stack checks. +target_compile_definitions(luau_runtime PRIVATE LUABRIDGE_SAFE_STACK_CHECKS=0) +add_subdirectory(src/services/audio_res) + +target_link_libraries(luau_runtime PRIVATE Luau.VM Luau.Compiler LuaBridge) diff --git a/mods/luau_runtime/src/lua_bridge_helpers.cpp b/mods/luau_runtime/src/lua_bridge_helpers.cpp new file mode 100644 index 0000000000..6825fd08f8 --- /dev/null +++ b/mods/luau_runtime/src/lua_bridge_helpers.cpp @@ -0,0 +1,38 @@ +#include "lua_bridge_helpers.hpp" + +luabridge::TypeResult> +luabridge::Stack::get(lua_State* L, int) { + auto& ref = luau_runtime::vm_from_registry(L); + return std::reference_wrapper(ref); +} + +luabridge::TypeResult> +luabridge::Stack::get(lua_State* L, int) { + auto const& ref = luau_runtime::vm_from_registry(L); + return std::reference_wrapper(ref); +} + +namespace luau_runtime { + +BridgeScriptHandle::BridgeScriptHandle(uint64_t const handle) noexcept : handle(handle) {} + +BridgeScriptHandle::~BridgeScriptHandle() = default; + +void BridgeScriptHandle::unregister(lua_State* state, Vm& vm) { + if (handle == 0) { + luaL_error(state, "Stale handle"); + } + + try { + unregister_impl(state, vm); + } catch (std::exception const&) { + // If an exception occurs, disallow the Lua code from attempting to re-run unregister. + // Since we assume the object may be in an undefined state (but hopefully not?) + handle = 0; + throw; + } + + handle = 0; +} + +} // namespace luau_runtime diff --git a/mods/luau_runtime/src/lua_bridge_helpers.hpp b/mods/luau_runtime/src/lua_bridge_helpers.hpp new file mode 100644 index 0000000000..2f51274573 --- /dev/null +++ b/mods/luau_runtime/src/lua_bridge_helpers.hpp @@ -0,0 +1,112 @@ +#pragma once + +// clang-format off +#include "lua.h" +#include "lualib.h" +#include "LuaBridge/LuaBridge.h" +// clang-format on +#include "runtime.hpp" + +/** + * Defines (i.e. implements) a LuaBridge3 Stack converter for an enum that is passed by string + * name. + * + * @param type The type name of the enum to convert. + * @param names The constant field containing the string name <-> enum value mapping. + * @see DECLARE_STRING_ENUM + */ +#define DEFINE_STRING_ENUM(type, names) \ + Result Stack::push(lua_State* L, type value) { \ + return Stack::push( \ + L, ::luau_runtime::enum_value_to_str(L, value, names)); \ + } \ + \ + TypeResult Stack::get(lua_State* L, int index) { \ + const char* str = lua_tolstring(L, index, nullptr); \ + return ::luau_runtime::enum_str_to_value(L, str, names); \ + } + +/** + * Declares a LuaBridge3 Stack converter for an enum that is passed by string name. + * + * @param type The type name of the enum to convert. + * @see DEFINE_STRING_ENUM + */ +#define DECLARE_STRING_ENUM(type) \ + template <> \ + struct Stack { \ + [[nodiscard]] static Result push(lua_State* L, type value); \ + [[nodiscard]] static TypeResult get(lua_State* L, int index); \ + } + +/** + * Template specialization to allow luau_runtime::Vm& to be directly acquired + * from a LuaBridge3 function. + */ +template <> +struct luabridge::Stack { + [[nodiscard]] static TypeResult> get( + lua_State* L, int); +}; + +/** + * Template specialization to allow luau_runtime::Vm const& to be directly acquired + * from a LuaBridge3 function. + */ +template <> +struct luabridge::Stack { + [[nodiscard]] static TypeResult> get( + lua_State* L, int); +}; + +namespace luau_runtime { + +/** + * Base type for "handle" types using LuaBridge3. + * + * Inherit from it, then register your derived class with LuaBridge3. + * Provide the base class @ref unregister member function. + */ +class BridgeScriptHandle { +protected: + uint64_t handle; + explicit BridgeScriptHandle(uint64_t handle) noexcept; + virtual void unregister_impl(lua_State* state, Vm& vm) = 0; + +public: + virtual ~BridgeScriptHandle(); + void unregister(lua_State* state, Vm& vm); +}; + +} // namespace luau_runtime + +/** + * Convert a @ref luaBridge::LuaRef to an *optional* value. + * + * This uses the default LuaBridge3 handling for std::optional, + * but just makes the type specification and casting a bit less verbose. + * + * @tparam T The type contained in the resulting optional. + * @tparam TLuaRef The type of the lua ref. Let this be inferred, as table items are unnameable. + * @param value Lua ref to convert to the desired type. + */ +template +std::optional opt(TLuaRef const& value) { + return value.template cast>().value(); +} + +/** + * Convert a @ref luaBridge::LuaRef to a value, with a default fallback if nil. + * + * This uses the default LuaBridge3 handling for std::optional, + * but just makes the type specification and casting a bit less verbose. + * + * @tparam T The resulting type. + * @tparam TLuaRef The type of the lua ref. Let this be inferred, as table items are unnameable. + * @param value Lua ref to convert to the desired type. + * @param default_value The default value if the lua item is nil. + */ +template +T opt_or(TLuaRef const& value, T const& default_value) { + return opt(value).value_or(default_value); +} diff --git a/mods/luau_runtime/src/lua_helpers.cpp b/mods/luau_runtime/src/lua_helpers.cpp new file mode 100644 index 0000000000..e09f582945 --- /dev/null +++ b/mods/luau_runtime/src/lua_helpers.cpp @@ -0,0 +1,118 @@ +#include "lua_helpers.hpp" + +#include + +namespace luau_runtime { + +bool get_optional_bool(lua_State* state, int table, const char* field, bool fallback) { + lua_getfield(state, table, field); + const bool value = lua_isnil(state, -1) ? fallback : luaL_checkboolean(state, -1) != 0; + lua_pop(state, 1); + return value; +} + +bool to_int64(lua_State* state, int index, int64_t& outValue) { + if (lua_isinteger64(state, index)) { + outValue = lua_tointeger64(state, index, nullptr); + return true; + } + if (!lua_isnumber(state, index)) { + return false; + } + const double value = lua_tonumber(state, index); + constexpr double kMaxSafeInteger = 9007199254740991.0; + if (!std::isfinite(value) || value < -kMaxSafeInteger || value > kMaxSafeInteger || + std::trunc(value) != value) + { + return false; + } + outValue = static_cast(value); + return true; +} + +int64_t check_int64(lua_State* state, int index) { + int64_t value = 0; + if (!to_int64(state, index, value)) { + luaL_argerror(state, index, "integer value expected"); + } + return value; +} + +int64_t get_optional_int(lua_State* state, int table, const char* field, int64_t fallback) { + lua_getfield(state, table, field); + const int64_t value = lua_isnil(state, -1) ? fallback : check_int64(state, -1); + lua_pop(state, 1); + return value; +} + +double get_optional_number(lua_State* state, int table, const char* field, double fallback) { + lua_getfield(state, table, field); + const double value = lua_isnil(state, -1) ? fallback : luaL_checknumber(state, -1); + lua_pop(state, 1); + return value; +} + +double get_number(lua_State* state, int table, const char* field) { + lua_getfield(state, table, field); + const double value = luaL_checknumber(state, -1); + lua_pop(state, 1); + return value; +} + +uint32_t get_optional_uint32(lua_State* state, int table, const char* field, uint32_t fallback) { + lua_getfield(state, table, field); + const uint32_t value = lua_isnil(state, -1) ? fallback : luaL_checkunsigned(state, -1); + lua_pop(state, 1); + return value; +} + +uint32_t get_uint32(lua_State* state, int table, const char* field) { + lua_getfield(state, table, field); + const uint32_t value = luaL_checkunsigned(state, -1); + lua_pop(state, 1); + return value; +} + +int32_t get_optional_int32(lua_State* state, int table, const char* field, int32_t fallback) { + lua_getfield(state, table, field); + const int32_t value = lua_isnil(state, -1) ? fallback : luaL_checkinteger(state, -1); + lua_pop(state, 1); + return value; +} + +std::string get_optional_string( + lua_State* state, int table, const char* field, std::string_view fallback) { + std::string result; + lua_getfield(state, table, field); + if (!lua_isnil(state, -1)) { + size_t length = 0; + const char* value = luaL_checklstring(state, -1, &length); + result.assign(value, length); + } else { + result = fallback; + } + lua_pop(state, 1); + return result; +} + +int ref_optional_function(lua_State* state, int table, const char* field) { + lua_getfield(state, table, field); + if (lua_isnil(state, -1)) { + lua_pop(state, 1); + return LUA_NOREF; + } + luaL_argexpected(state, lua_isfunction(state, -1), table, "function field"); + const int ref = lua_ref(state, -1); + lua_pop(state, 1); + return ref; +} + +int ref_required_function(lua_State* state, int table, const char* field) { + const int ref = ref_optional_function(state, table, field); + if (ref == LUA_NOREF) { + luaL_error(state, "field '%s' is required", field); + } + return ref; +} + +} // namespace luau_runtime diff --git a/mods/luau_runtime/src/lua_helpers.hpp b/mods/luau_runtime/src/lua_helpers.hpp new file mode 100644 index 0000000000..52e3b8210f --- /dev/null +++ b/mods/luau_runtime/src/lua_helpers.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include "lua.h" +#include "lualib.h" + +#include +#include + +/* + * Helper functions for API bound between Lua <-> C++ + */ + +namespace luau_runtime { + +bool get_optional_bool(lua_State* state, int table, const char* field, bool fallback); +double get_optional_number(lua_State* state, int table, const char* field, double fallback); +int32_t get_optional_int32(lua_State* state, int table, const char* field, int32_t fallback); +uint32_t get_optional_uint32(lua_State* state, int table, const char* field, uint32_t fallback); +std::string get_optional_string( + lua_State* state, int table, const char* field, std::string_view fallback = {}); + +bool to_int64(lua_State* state, int index, int64_t& outValue); +int64_t check_int64(lua_State* state, int index); +int64_t get_optional_int(lua_State* state, int table, const char* field, int64_t fallback); +int ref_optional_function(lua_State* state, int table, const char* field); +int ref_required_function(lua_State* state, int table, const char* field); + +double get_number(lua_State* state, int table, const char* field); +uint32_t get_uint32(lua_State* state, int table, const char* field); + +/** + * Concept that demands things be an enum. C++ is such a cool language. + */ +template +concept Enum = std::is_enum_v; + +/** + * Specifies a name/value pair for an enum. This is used by helpers to do automatic conversions + * between the native and (string) Lua form. + * @tparam T The enum type + */ +template +struct EnumName { + T value; + std::string_view name; + + constexpr EnumName(T value, std::string_view name) : value(value), name(name) {} +}; + +/** + * Convert an enum's string name to the actual C++ value. + * + * Raises a Lua error if the string is not mapped. + * + * @tparam T Type of the enum + * @param state The Lua state. + * @param str The string value, from Lua. + * @param options The predefined set of possible enum names. + */ +template +T enum_str_to_value(lua_State* state, char const* str, EnumName const (&options)[N]) { + std::string_view strv(str); + for (size_t i = 0; i < N; ++i) { + if (options[i].name == strv) { + return static_cast(i); + } + } + + luaL_errorL(state, "Invalid enum value for %s: '%s'", typeid(T).name(), str); +} + +/** + * Convert an enum's C++ value to the Lua-exposed string. + * + * Raises a Lua error if the string is not mapped. + * + * @tparam T Type of the enum + * @param state The Lua state. + * @param value The C++ enum value. + * @param options The predefined set of possible enum names. + */ +template +std::string_view enum_value_to_str(lua_State* state, T value, EnumName const (&options)[N]) { + for (auto [nameValue, name] : options) { + if (nameValue == value) { + return name; + } + } + + luaL_errorL(state, "Attempted to return invalid enum to Lua!"); +} + +} // namespace luau_runtime diff --git a/mods/luau_runtime/src/runtime.cpp b/mods/luau_runtime/src/runtime.cpp index da69541872..87c103e3b3 100644 --- a/mods/luau_runtime/src/runtime.cpp +++ b/mods/luau_runtime/src/runtime.cpp @@ -1,9 +1,11 @@ #include "runtime.hpp" +#include "services/services.hpp" #include "Luau/Common.h" #include "luacode.h" #include "mods/runtime.h" #include "mods/service.hpp" +#include "mods/svc/audio_res.h" #include #include @@ -26,10 +28,15 @@ IMPORT_OPTIONAL_SERVICE(ResourceService, svc_resource); IMPORT_OPTIONAL_SERVICE(OverlayService, svc_overlay); IMPORT_OPTIONAL_SERVICE(TextureService, svc_texture); IMPORT_OPTIONAL_SERVICE(UiService, svc_ui); +IMPORT_OPTIONAL_SERVICE(AudioResService, svc_audio_res); namespace luau_runtime { namespace { +// Just here to provide a globally-unique address to use as a registry index. +// Index into the Lua registry to store the Vm*. +int vm_registry_index; + std::unordered_map> s_vms; void* limited_realloc(void* userData, void* pointer, size_t oldSize, size_t newSize) { @@ -147,38 +154,73 @@ std::optional normalize_module_path( } normalized.append(part); } - if (!normalized.ends_with(".luau")) { - normalized += ".luau"; - } + return normalized; } -ModuleOpenFn module_factory(std::string_view name) { - if (name == "dusklight.log") { - return open_log; +std::optional try_load_direct_lua_files(lua_State* state, ModContext* mod, char const* modName, std::string const& fileWithoutExt) { + // Try .luau first. + auto try_luau = fileWithoutExt + ".luau"; + std::optional found_path; + if (svc_resource->file_exists(mod, try_luau.c_str())) { + found_path = try_luau; } - if (name == "dusklight.host") { - return open_host; + + // Try .lua, if both it and .luau exist, there's a conflict. + auto try_lua = fileWithoutExt + ".lua"; + if (svc_resource->file_exists(mod, try_lua.c_str())) { + if (found_path.has_value()) { + luaL_error(state, "Cannot load module '%s' if both '%s' and '%s' exist", modName, try_luau.c_str(), try_lua.c_str()); + } + + found_path = try_lua; } - if (name == "dusklight.resource") { - return open_resource; + + return found_path; +} + +std::string resolve_module_path(lua_State* state, ModContext* mod, std::string_view currentPath, std::string_view requested) { + // For reference: https://github.com/luau-lang/rfcs/blob/79c722717ffd96e5c5b5a9493ff01fda8a5171be/docs/amended-require-resolution.md + + // Normalize to remove ./ and ../ and such. Should be a path relative to res/ after this. + auto normalized = normalize_module_path(currentPath, requested); + if (!normalized.has_value()) { + luaL_error(state, "module paths must be relative and remain inside the mod's res directory"); } - if (name == "dusklight.overlay") { - return open_overlay; + + auto has_lua_ext = normalized->ends_with(".lua") || normalized->ends_with(".luau"); + if (has_lua_ext) { + // No fancy logic in this case. + if (!svc_resource->file_exists(mod, normalized->c_str())) { + luaL_error(state, "Cannot load '%s': file does not exist", normalized->c_str()); + } + + return std::move(*normalized); } - if (name == "dusklight.texture") { - return open_texture; + + auto found_path = try_load_direct_lua_files(state, mod, normalized->c_str(), *normalized); + if (found_path.has_value()) { + // If a .lua(u) file *and* a matching directory exist, it's a conflict. + if (svc_resource->directory_exists(mod, normalized->c_str())) { + luaL_error(state, "Cannot load '%s' because conflicting directory '%s' also exists", found_path->c_str(), normalized->c_str()); + } + } else { + // Check for directory/init.lua(u) + auto initPath = *normalized + "/init"; + + found_path = try_load_direct_lua_files(state, mod, normalized->c_str(), initPath); } - if (name == "dusklight.config") { - return open_config; + + if (!found_path.has_value()) { + luaL_error(state, "Cannot load '%s': unable to locate '%s.lua(u)' or '%s/init.lua(u)'", + found_path->c_str(), normalized->c_str(), normalized->c_str()); } - if (name == "dusklight.ui") { - return open_ui; - } - return nullptr; + + return std::move(*found_path); } int module_require(lua_State* state); +int print(lua_State* state); void install_module_require(lua_State* state, Vm& vm, std::string_view currentPath) { lua_pushlightuserdata(state, &vm); @@ -187,6 +229,11 @@ void install_module_require(lua_State* state, Vm& vm, std::string_view currentPa lua_setglobal(state, "require"); } +void install_globals(Vm& vm) { + push_vm_closure(vm.state, vm, print, "print"); + lua_setglobal(vm.state, "print"); +} + bool load_source_module( lua_State* caller, Vm& vm, const std::string& path, bool keepResult, std::string& outError) { if (svc_resource == nullptr) { @@ -262,19 +309,13 @@ int module_require(lua_State* state) { ModuleOpenFn factory = nullptr; if (requested.starts_with("dusklight.")) { moduleName = requested; - factory = module_factory(moduleName); + factory = services::module_factory(moduleName); if (factory == nullptr) { luaL_error(state, "unknown module '%s'", moduleName.c_str()); } } else { const char* currentPath = lua_tostring(state, lua_upvalueindex(2)); - const auto normalized = - normalize_module_path(currentPath != nullptr ? currentPath : "main.luau", requested); - if (!normalized.has_value()) { - luaL_error( - state, "module paths must be relative and remain inside the mod's res directory"); - } - moduleName = *normalized; + moduleName = resolve_module_path(state, vm.subject, currentPath != nullptr ? currentPath : "main.luau", requested); } if (const auto found = vm.moduleRefs.find(moduleName); found != vm.moduleRefs.end()) { @@ -299,6 +340,29 @@ int module_require(lua_State* state) { return 1; } +int print(lua_State* state) { + Vm& vm = vm_from_upvalue(state); + if (svc_log == nullptr) { + service_unavailable(state, "LogService"); + } + + std::string value; + int const numArgs = lua_gettop(state); + for (int i = 0; i < numArgs; i += 1) { + if (i != 0) { + value.push_back('\t'); + } + + size_t length; + char const* str = luaL_tolstring(state, i + 1, &length); + value.append(str, length); + lua_pop(state, 1); + } + + svc_log->info(vm.subject, value.data()); + return 0; +} + ModResult runtime_activate(ModContext*, ModContext* subject, ModError* outError) { if (subject == nullptr) { return set_error(outError, MOD_INVALID_ARGUMENT, "Delegated mod context is null"); @@ -313,7 +377,10 @@ ModResult runtime_activate(ModContext*, ModContext* subject, ModError* outError) if (vm->state == nullptr) { return set_error(outError, MOD_ERROR, "Failed to create Luau VM"); } + lua_pushlightuserdata(vm->state, vm.get()); + lua_rawsetp(vm->state, LUA_REGISTRYINDEX, &vm_registry_index); lua_callbacks(vm->state)->userdata = vm.get(); +#if NDEBUG // Annoying for debuggers lua_callbacks(vm->state)->interrupt = [](lua_State* state, int gc) { auto* current = static_cast(lua_callbacks(state)->userdata); if (gc < 0 && current != nullptr && current->deadlineActive && @@ -322,7 +389,9 @@ ModResult runtime_activate(ModContext*, ModContext* subject, ModError* outError) luaL_error(state, "script execution exceeded its time budget"); } }; +#endif luaL_openlibs(vm->state); + install_globals(*vm); luaL_sandbox(vm->state); std::string error; @@ -405,6 +474,16 @@ Vm& vm_from_upvalue(lua_State* state) { return *vm; } +Vm& vm_from_registry(lua_State* state) { + lua_rawgetp(state, LUA_REGISTRYINDEX, &vm_registry_index); + auto* vm = static_cast(lua_tolightuserdata(state, -1)); + if (vm == nullptr) { + luaL_error(state, "missing Luau runtime context"); + } + lua_pop(state, 1); + return *vm; +} + void push_vm_closure(lua_State* state, Vm& vm, lua_CFunction function, const char* name) { lua_pushlightuserdata(state, &vm); lua_pushcclosure(state, function, name, 1); @@ -443,86 +522,6 @@ void check_result(lua_State* state, ModResult result, const char* operation) { luaL_error(state, "%s failed: %s", operation, resultName); } -bool get_optional_bool(lua_State* state, int table, const char* field, bool fallback) { - lua_getfield(state, table, field); - const bool value = lua_isnil(state, -1) ? fallback : luaL_checkboolean(state, -1) != 0; - lua_pop(state, 1); - return value; -} - -bool to_int64(lua_State* state, int index, int64_t& outValue) { - if (lua_isinteger64(state, index)) { - outValue = lua_tointeger64(state, index, nullptr); - return true; - } - if (!lua_isnumber(state, index)) { - return false; - } - const double value = lua_tonumber(state, index); - constexpr double kMaxSafeInteger = 9007199254740991.0; - if (!std::isfinite(value) || value < -kMaxSafeInteger || value > kMaxSafeInteger || - std::trunc(value) != value) - { - return false; - } - outValue = static_cast(value); - return true; -} - -int64_t check_int64(lua_State* state, int index) { - int64_t value = 0; - if (!to_int64(state, index, value)) { - luaL_argerror(state, index, "integer value expected"); - } - return value; -} - -int64_t get_optional_int(lua_State* state, int table, const char* field, int64_t fallback) { - lua_getfield(state, table, field); - const int64_t value = lua_isnil(state, -1) ? fallback : check_int64(state, -1); - lua_pop(state, 1); - return value; -} - -double get_optional_number(lua_State* state, int table, const char* field, double fallback) { - lua_getfield(state, table, field); - const double value = lua_isnil(state, -1) ? fallback : luaL_checknumber(state, -1); - lua_pop(state, 1); - return value; -} - -std::string get_optional_string( - lua_State* state, int table, const char* field, std::string fallback) { - lua_getfield(state, table, field); - if (!lua_isnil(state, -1)) { - size_t length = 0; - const char* value = luaL_checklstring(state, -1, &length); - fallback.assign(value, length); - } - lua_pop(state, 1); - return fallback; -} - -int ref_optional_function(lua_State* state, int table, const char* field) { - lua_getfield(state, table, field); - if (lua_isnil(state, -1)) { - lua_pop(state, 1); - return LUA_NOREF; - } - luaL_argexpected(state, lua_isfunction(state, -1), table, "function field"); - const int ref = lua_ref(state, -1); - lua_pop(state, 1); - return ref; -} - -int ref_required_function(lua_State* state, int table, const char* field) { - const int ref = ref_optional_function(state, table, field); - if (ref == LUA_NOREF) { - luaL_error(state, "field '%s' is required", field); - } - return ref; -} - Callback& retain_callback(Vm& vm) { auto callback = std::make_unique(); callback->vm = &vm; diff --git a/mods/luau_runtime/src/runtime.hpp b/mods/luau_runtime/src/runtime.hpp index 2916a817e5..fd743efcfd 100644 --- a/mods/luau_runtime/src/runtime.hpp +++ b/mods/luau_runtime/src/runtime.hpp @@ -56,6 +56,9 @@ struct Vm { unsigned callDepth = 0; bool deadlineActive = false; + Vm() = default; + Vm(Vm const&) = delete; + Vm(Vm&&) = delete; ~Vm(); }; @@ -70,6 +73,7 @@ enum class HandleKind : uint8_t { UiStyle, UiMenuTab, UiList, + UiContextMenu, }; struct ScriptHandle { @@ -82,22 +86,13 @@ struct ScriptHandle { using ModuleOpenFn = int (*)(lua_State* state); Vm& vm_from_upvalue(lua_State* state); +Vm& vm_from_registry(lua_State* state); void push_vm_closure(lua_State* state, Vm& vm, lua_CFunction function, const char* name); void set_function(lua_State* state, Vm& vm, const char* name, lua_CFunction function); [[noreturn]] void service_unavailable(lua_State* state, const char* name); void check_result(lua_State* state, ModResult result, const char* operation); -bool get_optional_bool(lua_State* state, int table, const char* field, bool fallback); -bool to_int64(lua_State* state, int index, int64_t& outValue); -int64_t check_int64(lua_State* state, int index); -int64_t get_optional_int(lua_State* state, int table, const char* field, int64_t fallback); -double get_optional_number(lua_State* state, int table, const char* field, double fallback); -std::string get_optional_string( - lua_State* state, int table, const char* field, std::string fallback = {}); -int ref_optional_function(lua_State* state, int table, const char* field); -int ref_required_function(lua_State* state, int table, const char* field); - Callback& retain_callback(Vm& vm); bool call_ref(Vm& vm, int ref, int argumentCount, int resultCount, std::chrono::steady_clock::duration budget, std::string& outError); @@ -110,15 +105,4 @@ ScriptHandle& check_handle(lua_State* state, int index, const char* metatable, H void push_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind, const char* metatable, ConfigVarType configType = CONFIG_VAR_BOOL); -void push_config_value(lua_State* state, const ConfigVarValue& value); -void push_ui_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind); - -int open_log(lua_State* state); -int open_host(lua_State* state); -int open_resource(lua_State* state); -int open_overlay(lua_State* state); -int open_texture(lua_State* state); -int open_config(lua_State* state); -int open_ui(lua_State* state); - } // namespace luau_runtime diff --git a/mods/luau_runtime/src/services/audio_res/CMakeLists.txt b/mods/luau_runtime/src/services/audio_res/CMakeLists.txt new file mode 100644 index 0000000000..e4a1029377 --- /dev/null +++ b/mods/luau_runtime/src/services/audio_res/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources(luau_runtime PRIVATE + audio_res.cpp + bst.cpp + bst.hpp + wsys.cpp + wsys.hpp +) diff --git a/mods/luau_runtime/src/services/audio_res/audio_res.cpp b/mods/luau_runtime/src/services/audio_res/audio_res.cpp new file mode 100644 index 0000000000..315f6c904c --- /dev/null +++ b/mods/luau_runtime/src/services/audio_res/audio_res.cpp @@ -0,0 +1,52 @@ +#include "../../lua_bridge_helpers.hpp" +#include "../../runtime.hpp" +#include "../services.hpp" +#include "LuaBridge/LuaBridge.h" + +#include "mods/svc/audio_res.h" + +#include "bst.hpp" +#include "wsys.hpp" + +namespace luau_runtime::services { +namespace { + +constexpr auto kServiceName = "AudioResService"; + +} + +int open_audio_res(lua_State* state) { + using namespace audio_res; + + if (svc_audio_res == nullptr) { + service_unavailable(state, kServiceName); + } + + lua_newtable(state); + + luabridge::getNamespaceFromStack(state) + // WSYS + .addVariable("DEFAULT_KEY", AUDIO_RES_DEFAULT_KEY) + .addFunction("default_wave_info", default_wave_info) + .addFunction("replace_wave", replace_wave) + .addFunction("add_wave", add_wave) + .beginClass("AudioWaveHandle") + .addFunction("unregister", &LuaAudioWaveHandle::unregister) + .endClass() + // BST + .addFunction("default_effect_info", default_effect_info) + .addFunction("replace_sound_table_effect", replace_sound_table_effect) + .addFunction("add_sound_table_effect", add_sound_table_effect) + .addVariable("STREAM_MAX_CHILDREN", STREAM_MAX_CHILDREN) + .addFunction("default_stream_info", default_stream_info) + .addFunction("replace_sound_table_stream", replace_sound_table_stream) + .addFunction("add_sound_table_stream", add_sound_table_stream) + .beginClass("AudioSoundTableHandle") + .addFunction("unregister", &LuaSoundTableHandle::unregister) + .endClass(); + + lua_setreadonly(state, -1, true); + return 1; +} + +} // namespace luau_runtime::services diff --git a/mods/luau_runtime/src/services/audio_res/bst.cpp b/mods/luau_runtime/src/services/audio_res/bst.cpp new file mode 100644 index 0000000000..fe670e4a9f --- /dev/null +++ b/mods/luau_runtime/src/services/audio_res/bst.cpp @@ -0,0 +1,205 @@ +#include "bst.hpp" + +namespace luau_runtime::services::audio_res { + +LuaSoundTableHandle::LuaSoundTableHandle(AudioSoundTableHandle handle) + : BridgeScriptHandle(handle) {} + +void LuaSoundTableHandle::unregister_impl(lua_State*, Vm& vm) { + svc_audio_res->remove_sound_table(vm.subject, handle); +} + +AudioSoundTableEffectInfo const& default_effect_info() { + return *svc_audio_res->default_effect_info; +} + +LuaSoundTableHandle replace_sound_table_effect(SoundEffectCategory category_id, uint16_t effect_id, + std::optional info, lua_State* state, Vm& vm) { + AudioSoundTableEffectInfo const* effect_info = nullptr; + if (info.has_value()) { + effect_info = &*info; + } + + AudioSoundTableHandle handle; + + check_result(state, + svc_audio_res->replace_sound_table_effect( + vm.subject, category_id, effect_id, effect_info, &handle), + "audio_res.replace_sound_table_effect"); + + return LuaSoundTableHandle(handle); +} + +std::tuple add_sound_table_effect(SoundEffectCategory category_id, + std::optional info, lua_State* state, Vm& vm) { + AudioSoundTableEffectInfo const* effect_info = nullptr; + if (info.has_value()) { + effect_info = &*info; + } + + uint16_t out_effect_id; + AudioSoundTableHandle handle; + + check_result(state, + svc_audio_res->add_sound_table_effect( + vm.subject, category_id, effect_info, &handle, &out_effect_id), + "audio_res.add_sound_table_effect"); + + return {out_effect_id, LuaSoundTableHandle(handle)}; +} + +AudioSoundTableStreamInfo const& default_stream_info() { + return *svc_audio_res->default_stream_info; +} + +LuaSoundTableHandle replace_sound_table_stream(uint16_t stream_id, char const* file_path, + std::optional info, lua_State* state, Vm& vm) { + AudioSoundTableStreamInfo const* stream_info = nullptr; + if (info.has_value()) { + stream_info = &*info; + } + + AudioSoundTableHandle handle; + + check_result(state, + svc_audio_res->replace_sound_table_stream( + vm.subject, stream_id, file_path, stream_info, &handle), + "audio_res.replace_sound_table_stream"); + + return LuaSoundTableHandle(handle); +} + +std::tuple add_sound_table_stream(char const* file_path, + std::optional info, lua_State* state, Vm& vm) { + AudioSoundTableStreamInfo const* stream_info = nullptr; + if (info.has_value()) { + stream_info = &*info; + } + + uint16_t out_stream_id; + AudioSoundTableHandle handle; + + check_result(state, + svc_audio_res->add_sound_table_stream( + vm.subject, file_path, stream_info, &handle, &out_stream_id), + "audio_res.add_sound_table_stream"); + + return {out_stream_id, LuaSoundTableHandle(handle)}; +} + +} // namespace luau_runtime::services::audio_res + +namespace luabridge { +namespace { + +using namespace std::string_view_literals; + +constexpr luau_runtime::EnumName kSoundEffectCategoryNames[] = { + // clang-format off + { SE_CATEGORY_SYSTEM_SE, "system_se"sv }, + { SE_CATEGORY_PLAYER_VOICE, "player_voice"sv }, + { SE_CATEGORY_PLAYER_SE, "player_se"sv }, + { SE_CATEGORY_FOOTNOTE_SE, "footnote_se"sv }, + { SE_CATEGORY_COLLISION_SE, "collision_se"sv }, + { SE_CATEGORY_CHARA_VOICE, "chara_voice"sv }, + { SE_CATEGORY_CHARA_SE, "chara_se"sv }, + { SE_CATEGORY_ENEMY_SE, "enemy_se"sv }, + { SE_CATEGORY_OBJECT_SE, "object_se"sv }, + { SE_CATEGORY_ENV_SE, "env_se"sv }, + // clang-format on +}; + +constexpr luau_runtime::EnumName kStreamPanNames[] = { + // clang-format off + { STREAM_PAN_CENTER, "center"sv }, + { STREAM_PAN_LEFT, "left"sv }, + { STREAM_PAN_RIGHT, "right"sv }, + // clang-format on +}; + +} // namespace + +DEFINE_STRING_ENUM(SoundEffectCategory, kSoundEffectCategoryNames); +DEFINE_STRING_ENUM(StreamPan, kStreamPanNames); + +TypeResult Stack::get( + lua_State* L, int index) { + auto const ref = LuaRef::fromStack(L, index); + + return AudioSoundTableEffectInfo{ + .priority = ref["priority"], + .volume = ref["volume"], + .pitch = ref["pitch"], + .always_max_priority = ref["always_max_priority"], + .ignore_distance_volume = ref["ignore_distance_volume"], + .ignore_distance_fx_mix = ref["ignore_distance_fx_mix"], + .ignore_pan = ref["ignore_pan"], + .ignore_dolby = ref["ignore_dolby"], + .random_volume = ref["random_volume"], + .random_pitch = ref["random_pitch"], + .doppler_power = ref["doppler_power"], + .volume_dist_class = ref["volume_dist_class"], + .clamp_min_volume = ref["clamp_min_volume"], + .cull_at_max_distance = ref["cull_at_max_distance"], + }; +} + +Result Stack::push( + lua_State* L, const AudioSoundTableEffectInfo& value) { + LuaRef const ref = newTable(L); + + ref["priority"] = value.priority; + ref["volume"] = value.volume; + ref["pitch"] = value.pitch; + ref["always_max_priority"] = value.always_max_priority; + ref["ignore_distance_volume"] = value.ignore_distance_volume; + ref["ignore_distance_fx_mix"] = value.ignore_distance_fx_mix; + ref["ignore_pan"] = value.ignore_pan; + ref["ignore_dolby"] = value.ignore_dolby; + ref["random_volume"] = value.random_volume; + ref["random_pitch"] = value.random_pitch; + ref["doppler_power"] = value.doppler_power; + ref["volume_dist_class"] = value.volume_dist_class; + ref["clamp_min_volume"] = value.clamp_min_volume; + ref["cull_at_max_distance"] = value.cull_at_max_distance; + + ref.push(); + + return {}; +} + +TypeResult Stack::get( + lua_State* L, int index) { + auto const ref = LuaRef::fromStack(L, index); + + std::array, STREAM_MAX_CHILDREN> const pan_parameters = + ref["pan_parameters"]; + + auto info = AudioSoundTableStreamInfo{ + .priority = ref["priority"], + .volume = ref["volume"], + .stop_on_scene_change = ref["stop_on_scene_change"], + }; + + for (int i = 0; i < STREAM_MAX_CHILDREN; i++) { + info.pan_parameters[i] = pan_parameters[i].value_or(STREAM_PAN_CENTER); + } + + return info; +} + +Result Stack::push( + lua_State* L, const AudioSoundTableStreamInfo& value) { + LuaRef const ref = newTable(L); + + ref["priority"] = value.priority; + ref["volume"] = value.volume; + ref["pan_parameters"] = value.pan_parameters; + ref["stop_on_scene_change"] = value.stop_on_scene_change; + + ref.push(); + + return {}; +} + +} // namespace luabridge diff --git a/mods/luau_runtime/src/services/audio_res/bst.hpp b/mods/luau_runtime/src/services/audio_res/bst.hpp new file mode 100644 index 0000000000..a4d7cba591 --- /dev/null +++ b/mods/luau_runtime/src/services/audio_res/bst.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "../../lua_bridge_helpers.hpp" +#include "../../lua_helpers.hpp" +#include "mods/svc/audio_res.h" + +namespace luau_runtime::services::audio_res { + +class LuaSoundTableHandle final : public BridgeScriptHandle { +protected: + void unregister_impl(lua_State*, Vm& vm) override; + +public: + explicit LuaSoundTableHandle(AudioSoundTableHandle handle); +}; + +AudioSoundTableEffectInfo const& default_effect_info(); + +LuaSoundTableHandle replace_sound_table_effect(SoundEffectCategory category_id, uint16_t effect_id, + std::optional info, lua_State* state, Vm& vm); + +std::tuple add_sound_table_effect(SoundEffectCategory category_id, + std::optional info, lua_State* state, Vm& vm); + +AudioSoundTableStreamInfo const& default_stream_info(); + +LuaSoundTableHandle replace_sound_table_stream(uint16_t stream_id, char const* file_path, + std::optional info, lua_State* state, Vm& vm); + +std::tuple add_sound_table_stream( + char const* file_path, std::optional info, lua_State* state, Vm& vm); + +} // namespace luau_runtime::services::audio_res + +namespace luabridge { + +template <> +struct Stack { + [[nodiscard]] static Result push(lua_State* L, const AudioSoundTableEffectInfo& value); + [[nodiscard]] static TypeResult get(lua_State* L, int index); +}; + +template <> +struct Stack { + [[nodiscard]] static Result push(lua_State* L, const AudioSoundTableStreamInfo& value); + [[nodiscard]] static TypeResult get(lua_State* L, int index); +}; + +DECLARE_STRING_ENUM(SoundEffectCategory); +DECLARE_STRING_ENUM(StreamPan); + +} // namespace luabridge diff --git a/mods/luau_runtime/src/services/audio_res/wsys.cpp b/mods/luau_runtime/src/services/audio_res/wsys.cpp new file mode 100644 index 0000000000..cf4bc19039 --- /dev/null +++ b/mods/luau_runtime/src/services/audio_res/wsys.cpp @@ -0,0 +1,138 @@ +#include "wsys.hpp" + +namespace luau_runtime::services::audio_res { + +LuaAudioWaveHandle::LuaAudioWaveHandle(AudioWaveHandle handle) : BridgeScriptHandle(handle) {} + +void LuaAudioWaveHandle::unregister_impl(lua_State*, Vm& vm) { + svc_audio_res->remove_wave(vm.subject, handle); +} + +AudioWaveInfo const& default_wave_info() { + return *svc_audio_res->default_wave_info; +} + +LuaAudioWaveHandle replace_wave(AudioWaveBank bank, uint16_t wave_id, char const* file, + std::optional info, lua_State* state, Vm& vm) { + AudioWaveInfo const* wave_info = nullptr; + if (info.has_value()) { + wave_info = &info->info; + } + + AudioWaveHandle handle; + + check_result(state, + svc_audio_res->replace_wave(vm.subject, bank, wave_id, file, wave_info, &handle), + "audio_res.replace_wave"); + + return LuaAudioWaveHandle(handle); +} + +std::tuple add_wave(AudioWaveBank bank, char const* file, + std::optional info, lua_State* state, Vm& vm) { + AudioWaveInfo const* wave_info = nullptr; + if (info.has_value()) { + wave_info = &info->info; + } + + AudioWaveHandle handle; + uint16_t out_wave_id; + + check_result(state, + svc_audio_res->add_wave(vm.subject, bank, file, wave_info, &handle, &out_wave_id), + "audio_res.replace_wave"); + + return {out_wave_id, LuaAudioWaveHandle(handle)}; +} + +} // namespace luau_runtime::services::audio_res + +namespace luabridge { + +Result Stack::push(lua_State* L, const AudioWaveInfo& value) { + LuaRef const ref = newTable(L); + + ref["base_key"] = value.base_key; + ref["loop"] = value.loop; + ref["loop_start_sample"] = value.loop_start_sample; + ref["loop_end_sample"] = value.loop_end_sample; + if (value.raw_wave) { + ref["raw_wave"] = *value.raw_wave; + } + ref.push(); + + return {}; +} + +Result Stack::push(lua_State* L, const AudioRawWave& value) { + LuaRef const ref = newTable(L); + + ref["format"] = value.format; + ref["sample_rate"] = value.sample_rate; + ref["sample_value_last"] = value.sample_value_last; + ref["sample_value_penult"] = value.sample_value_penult; + + ref.push(); + + return {}; +} + +TypeResult Stack::get(lua_State* L, int index) { + auto const ref = LuaRef::fromStack(L, index); + + return AudioRawWave{ + .format = ref["format"], + .sample_rate = ref["sample_rate"], + .sample_value_last = opt_or(ref["sample_value_last"], 0), + .sample_value_penult = opt_or(ref["sample_value_penult"], 0), + }; +} + +using namespace luau_runtime::services::audio_res; + +Result Stack::push(lua_State* L, const OwnedWaveInfo& value) { + return Stack::push(L, value.info); +} + +TypeResult Stack::get(lua_State* L, int index) { + auto const ref = LuaRef::fromStack(L, index); + + AudioWaveInfo info{ + .base_key = opt_or(ref["base_key"], AUDIO_RES_DEFAULT_KEY), + .loop = opt_or(ref["loop"], false), + .loop_start_sample = opt_or(ref["loop_start_sample"], 0), + .loop_end_sample = + opt_or(ref["loop_end_sample"], std::numeric_limits::max()), + }; + + std::unique_ptr raw; + if (!ref["raw_wave"].isNil()) { + raw = std::make_unique(ref["raw_wave"]); + info.raw_wave = raw.get(); + } + + return OwnedWaveInfo{info, std::move(raw)}; +} + +namespace { + +using namespace std::string_view_literals; + +constexpr luau_runtime::EnumName kWaveFormatNames[] = { + {AUDIO_WAVE_FORMAT_ADPCM4, "adpcm4"sv}, + {AUDIO_WAVE_FORMAT_ADPCM2, "adpcm2"sv}, + {AUDIO_WAVE_FORMAT_PCM8, "pcm8"sv}, + {AUDIO_WAVE_FORMAT_PCM16, "pcm16"sv}, +}; + +constexpr luau_runtime::EnumName kWaveBankNames[] = { + {AUDIO_WAVE_BANK_SOUND_EFFECTS, "sound_effects"sv}, + {AUDIO_WAVE_BANK_MUSIC_SAMPLES, "music_samples"sv}, +}; + +} // namespace + +DEFINE_STRING_ENUM(AudioWaveFormat, kWaveFormatNames); +DEFINE_STRING_ENUM(AudioWaveBank, kWaveBankNames); + +} // namespace luabridge diff --git a/mods/luau_runtime/src/services/audio_res/wsys.hpp b/mods/luau_runtime/src/services/audio_res/wsys.hpp new file mode 100644 index 0000000000..0d35a9b266 --- /dev/null +++ b/mods/luau_runtime/src/services/audio_res/wsys.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "LuaBridge/LuaBridge.h" +#include "lua.h" +#include "lualib.h" + +#include "../../lua_helpers.hpp" +#include "../../runtime.hpp" +#include "mods/svc/audio_res.h" + +#include + +namespace luau_runtime::services::audio_res { + +struct OwnedWaveInfo { + AudioWaveInfo info; + std::unique_ptr raw_wave; +}; + +class LuaAudioWaveHandle final : public BridgeScriptHandle { +protected: + void unregister_impl(lua_State*, Vm& vm) override; + +public: + explicit LuaAudioWaveHandle(AudioWaveHandle handle); +}; + +AudioWaveInfo const& default_wave_info(); +LuaAudioWaveHandle replace_wave(AudioWaveBank bank, uint16_t wave_id, char const* file, + std::optional info, lua_State* state, Vm& vm); +std::tuple add_wave(AudioWaveBank bank, char const* file, + std::optional info, lua_State* state, Vm& vm); + +} // namespace luau_runtime::services::audio_res + +namespace luabridge { + +template <> +struct Stack { + [[nodiscard]] static Result push(lua_State* L, const AudioWaveInfo& value); +}; + +template <> +struct Stack { + [[nodiscard]] static Result push(lua_State* L, const AudioRawWave& value); + [[nodiscard]] static TypeResult get(lua_State* L, int index); +}; + +template <> +struct Stack { + [[nodiscard]] static Result push( + lua_State* L, const luau_runtime::services::audio_res::OwnedWaveInfo& value); + [[nodiscard]] static TypeResult get( + lua_State* L, int index); +}; + +DECLARE_STRING_ENUM(AudioWaveFormat); +DECLARE_STRING_ENUM(AudioWaveBank); + +} // namespace luabridge diff --git a/mods/luau_runtime/src/bindings.cpp b/mods/luau_runtime/src/services/bindings.cpp similarity index 98% rename from mods/luau_runtime/src/bindings.cpp rename to mods/luau_runtime/src/services/bindings.cpp index 27f4218a83..967adc32aa 100644 --- a/mods/luau_runtime/src/bindings.cpp +++ b/mods/luau_runtime/src/services/bindings.cpp @@ -1,10 +1,11 @@ -#include "runtime.hpp" +#include "../lua_helpers.hpp" +#include "../runtime.hpp" #include #include #include -namespace luau_runtime { +namespace luau_runtime::services { namespace { constexpr char kOverlayMetatable[] = "dusklight.overlay_handle"; @@ -337,4 +338,4 @@ int open_texture(lua_State* state) { return 1; } -} // namespace luau_runtime +} // namespace luau_runtime::services diff --git a/mods/luau_runtime/src/config.cpp b/mods/luau_runtime/src/services/config.cpp similarity index 98% rename from mods/luau_runtime/src/config.cpp rename to mods/luau_runtime/src/services/config.cpp index b198bdb6e7..28d6939e4e 100644 --- a/mods/luau_runtime/src/config.cpp +++ b/mods/luau_runtime/src/services/config.cpp @@ -1,9 +1,12 @@ -#include "runtime.hpp" +#include "config.hpp" +#include "../runtime.hpp" #include #include -namespace luau_runtime { +#include "../lua_helpers.hpp" + +namespace luau_runtime::services { namespace { constexpr char kConfigVarMetatable[] = "dusklight.config_var"; @@ -240,4 +243,4 @@ int open_config(lua_State* state) { return 1; } -} // namespace luau_runtime +} // namespace luau_runtime::services diff --git a/mods/luau_runtime/src/services/config.hpp b/mods/luau_runtime/src/services/config.hpp new file mode 100644 index 0000000000..1eb83e0c8f --- /dev/null +++ b/mods/luau_runtime/src/services/config.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "lua.h" +#include "mods/svc/config.h" + +namespace luau_runtime::services { + +void push_config_value(lua_State* state, const ConfigVarValue& value); + +} diff --git a/mods/luau_runtime/src/services/services.cpp b/mods/luau_runtime/src/services/services.cpp new file mode 100644 index 0000000000..1933aa4d30 --- /dev/null +++ b/mods/luau_runtime/src/services/services.cpp @@ -0,0 +1,32 @@ +#include "services.hpp" + +namespace luau_runtime::services { + +namespace { + +using namespace std::string_view_literals; + +constexpr std::pair kModules[] = { + {"dusklight.log"sv, open_log}, + {"dusklight.host"sv, open_host}, + {"dusklight.resource"sv, open_resource}, + {"dusklight.overlay"sv, open_overlay}, + {"dusklight.texture"sv, open_texture}, + {"dusklight.config"sv, open_config}, + {"dusklight.ui"sv, open_ui}, + {"dusklight.audio_res"sv, open_audio_res}, +}; + +} // namespace + +ModuleOpenFn module_factory(std::string_view name) { + for (auto [modName, ptr] : kModules) { + if (name == modName) { + return ptr; + } + } + + return nullptr; +} + +} // namespace luau_runtime::services diff --git a/mods/luau_runtime/src/services/services.hpp b/mods/luau_runtime/src/services/services.hpp new file mode 100644 index 0000000000..71660cfa69 --- /dev/null +++ b/mods/luau_runtime/src/services/services.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "../runtime.hpp" + +#include "lua.h" + +namespace luau_runtime::services { + +int open_audio_res(lua_State* state); +int open_log(lua_State* state); +int open_host(lua_State* state); +int open_resource(lua_State* state); +int open_overlay(lua_State* state); +int open_texture(lua_State* state); +int open_config(lua_State* state); +int open_ui(lua_State* state); + +ModuleOpenFn module_factory(std::string_view name); + +} // namespace luau_runtime::services diff --git a/mods/luau_runtime/src/ui.cpp b/mods/luau_runtime/src/services/ui.cpp similarity index 78% rename from mods/luau_runtime/src/ui.cpp rename to mods/luau_runtime/src/services/ui.cpp index 7c3acc1296..86edf99f5b 100644 --- a/mods/luau_runtime/src/ui.cpp +++ b/mods/luau_runtime/src/services/ui.cpp @@ -1,19 +1,27 @@ -#include "runtime.hpp" +#include "ui.hpp" + +#include "../runtime.hpp" #include +#include #include #include #include -namespace luau_runtime { +#include "../lua_helpers.hpp" + +namespace luau_runtime::services { namespace { +using namespace std::string_view_literals; + constexpr char kUiWindowMetatable[] = "dusklight.ui_window"; constexpr char kUiDialogMetatable[] = "dusklight.ui_dialog"; constexpr char kUiElementMetatable[] = "dusklight.ui_element"; constexpr char kUiStyleMetatable[] = "dusklight.ui_style"; constexpr char kUiMenuTabMetatable[] = "dusklight.ui_menu_tab"; constexpr char kUiListMetatable[] = "dusklight.ui_list"; +constexpr char kUiContextMenuMetatable[] = "dusklight.ui_context_menu"; const char* ui_metatable(HandleKind kind) { switch (kind) { @@ -27,6 +35,8 @@ const char* ui_metatable(HandleKind kind) { return kUiMenuTabMetatable; case HandleKind::UiList: return kUiListMetatable; + case HandleKind::UiContextMenu: + return kUiContextMenuMetatable; default: return kUiElementMetatable; } @@ -91,6 +101,7 @@ void control_get(ModContext*, void* userData, UiControlValue* outValue) { outValue->bool_value = lua_toboolean(state, -1) != 0; break; case UI_CONTROL_NUMBER: + case UI_CONTROL_DROPDOWN: case UI_CONTROL_SELECT: { int64_t value = 0; if (!to_int64(state, -1, value)) { @@ -130,6 +141,7 @@ void control_set(ModContext*, void* userData, const UiControlValue* value) { lua_pushboolean(state, value->bool_value); break; case UI_CONTROL_NUMBER: + case UI_CONTROL_DROPDOWN: case UI_CONTROL_SELECT: lua_pushinteger64(state, value->int_value); break; @@ -285,40 +297,56 @@ std::vector list_items(lua_State* state, int table, std::vector kControlKindNames[] = { + {UI_CONTROL_BUTTON, "button"sv}, + {UI_CONTROL_TOGGLE, "toggle"sv}, + {UI_CONTROL_NUMBER, "number"sv}, + {UI_CONTROL_STRING, "string"sv}, + {UI_CONTROL_SELECT, "select"sv}, + {UI_CONTROL_COLOR, "color"sv}, + {UI_CONTROL_GROUP, "group"sv}, + {UI_CONTROL_FILE_PICKER, "file_picker"sv}, + {UI_CONTROL_ICON_BUTTON, "icon_button"sv}, + {UI_CONTROL_DROPDOWN, "dropdown"sv}, +}; + UiControlKind control_kind(lua_State* state, const std::string& kind) { - if (kind == "button") - return UI_CONTROL_BUTTON; - if (kind == "toggle") - return UI_CONTROL_TOGGLE; - if (kind == "number") - return UI_CONTROL_NUMBER; - if (kind == "string") - return UI_CONTROL_STRING; - if (kind == "select") - return UI_CONTROL_SELECT; - if (kind == "color") - return UI_CONTROL_COLOR; - if (kind == "group") - return UI_CONTROL_GROUP; - if (kind == "file_picker") - return UI_CONTROL_FILE_PICKER; - luaL_error(state, "unknown UI control kind '%s'", kind.c_str()); + return enum_str_to_value(state, kind.data(), kControlKindNames); } +constexpr EnumName kStyleScopeNames[] = { + {UI_SCOPE_PRELAUNCH, "prelaunch"sv}, + {UI_SCOPE_WINDOW, "window"sv}, + {UI_SCOPE_MENU_BAR, "menu_bar"sv}, + {UI_SCOPE_OVERLAY, "overlay"sv}, + {UI_SCOPE_TOUCH_CONTROLS, "touch_controls"sv}, + {UI_SCOPE_GRAPHICS_TUNER, "graphics_tuner"sv}, +}; + UiStyleScope style_scope(lua_State* state, const std::string& scope) { - if (scope == "prelaunch") - return UI_SCOPE_PRELAUNCH; - if (scope == "window") - return UI_SCOPE_WINDOW; - if (scope == "menu_bar") - return UI_SCOPE_MENU_BAR; - if (scope == "overlay") - return UI_SCOPE_OVERLAY; - if (scope == "touch_controls") - return UI_SCOPE_TOUCH_CONTROLS; - if (scope == "graphics_tuner") - return UI_SCOPE_GRAPHICS_TUNER; - luaL_error(state, "unknown UI style scope '%s'", scope.c_str()); + return enum_str_to_value(state, scope.data(), kStyleScopeNames); +} + +int pane_add_row(lua_State* state) { + auto& pane = check_ui_handle(state, 1, HandleKind::UiElement); + luaL_checktype(state, 2, LUA_TTABLE); + constexpr EnumName alignNames[] = { + {UI_ROW_ALIGN_START, "start"sv}, + {UI_ROW_ALIGN_END, "end"sv}, + {UI_ROW_ALIGN_CENTER, "center"sv}, + {UI_ROW_ALIGN_SPACE_BETWEEN, "space_between"sv}, + }; + UiRowDesc desc = UI_ROW_DESC_INIT; + const auto align = get_optional_string(state, 2, "align"); + if (!align.empty()) { + desc.align = enum_str_to_value(state, align.c_str(), alignNames); + } + desc.wrap = get_optional_bool(state, 2, "wrap", false); + UiElementHandle row = 0; + check_result( + state, svc_ui->pane_add_row(pane.vm->subject, pane.value, &desc, &row), "ui pane_add_row"); + push_ui_handle(state, *pane.vm, row, HandleKind::UiElement); + return 1; } int pane_add_section(lua_State* state) { @@ -368,10 +396,14 @@ int pane_add_control(lua_State* state) { const std::string kind = get_optional_string(state, 2, "kind"); const std::string label = get_optional_string(state, 2, "label"); const std::string help = get_optional_string(state, 2, "help_rml"); + const std::string icon = get_optional_string(state, 2, "icon"); + const std::string tooltip = get_optional_string(state, 2, "tooltip"); const std::string prefix = get_optional_string(state, 2, "prefix"); const std::string suffix = get_optional_string(state, 2, "suffix"); desc.kind = control_kind(state, kind); desc.label = label.c_str(); + desc.icon = icon.empty() ? nullptr : icon.c_str(); + desc.tooltip = tooltip.empty() ? nullptr : tooltip.c_str(); desc.help_rml = help.empty() ? nullptr : help.c_str(); desc.min = get_optional_int(state, 2, "min", 0); desc.max = get_optional_int(state, 2, "max", 0); @@ -388,16 +420,34 @@ int pane_add_control(lua_State* state) { std::vector options = string_array(state, 2, "options"); std::vector optionPointers; optionPointers.reserve(options.size()); - for (const auto& option : options) + for (const auto& option : options) { optionPointers.push_back(option.c_str()); + } desc.options = optionPointers.data(); desc.option_count = optionPointers.size(); + std::unique_ptr enabled; + lua_getfield(state, 2, "option_enabled"); + if (!lua_isnil(state, -1)) { + luaL_checktype(state, -1, LUA_TTABLE); + if (lua_objlen(state, -1) != static_cast(options.size())) { + luaL_error(state, "option_enabled must have one boolean per option"); + } + enabled = std::make_unique(options.size()); + for (size_t i = 0; i < options.size(); ++i) { + lua_rawgeti(state, -1, static_cast(i + 1)); + enabled[i] = luaL_checkboolean(state, -1) != 0; + lua_pop(state, 1); + } + } + lua_pop(state, 1); + desc.option_enabled = enabled.get(); std::vector presets = string_array(state, 2, "color_presets"); std::vector presetPointers; presetPointers.reserve(presets.size()); - for (const auto& preset : presets) + for (const auto& preset : presets) { presetPointers.push_back(preset.c_str()); + } desc.color_presets = presetPointers.data(); desc.color_preset_count = presetPointers.size(); @@ -442,7 +492,9 @@ int pane_add_control(lua_State* state) { auto& variable = check_config_var(state, -1); desc.binding = UI_BINDING_CONFIG_VAR; desc.config_var = variable.value; - } else if (desc.kind != UI_CONTROL_BUTTON && desc.kind != UI_CONTROL_GROUP) { + } else if (desc.kind != UI_CONTROL_BUTTON && desc.kind != UI_CONTROL_GROUP && + desc.kind != UI_CONTROL_ICON_BUTTON) + { desc.binding = UI_BINDING_CALLBACKS; callback.refs[0] = ref_required_function(state, 2, "get"); callback.refs[1] = ref_required_function(state, 2, "set"); @@ -488,8 +540,9 @@ int pane_add_list(lua_State* state) { std::vector labels; std::vector items; lua_getfield(state, 2, "items"); - if (!lua_isnil(state, -1)) + if (!lua_isnil(state, -1)) { items = list_items(state, -1, labels); + } lua_pop(state, 1); UiListDesc desc = UI_LIST_DESC_INIT; @@ -540,6 +593,130 @@ int element_set_class(lua_State* state) { return 0; } +int control_set_label(lua_State* state) { + auto& element = check_ui_handle(state, 1, HandleKind::UiElement); + check_result(state, + svc_ui->control_set_label(element.vm->subject, element.value, luaL_checkstring(state, 2)), + "ui control_set_label"); + return 0; +} + +int control_set_icon(lua_State* state) { + auto& element = check_ui_handle(state, 1, HandleKind::UiElement); + check_result(state, + svc_ui->control_set_icon(element.vm->subject, element.value, luaL_checkstring(state, 2)), + "ui control_set_icon"); + return 0; +} + +int control_set_tooltip(lua_State* state) { + auto& element = check_ui_handle(state, 1, HandleKind::UiElement); + check_result(state, + svc_ui->control_set_tooltip( + element.vm->subject, element.value, luaL_optstring(state, 2, "")), + "ui control_set_tooltip"); + return 0; +} + +int control_set_options(lua_State* state) { + auto& element = check_ui_handle(state, 1, HandleKind::UiElement); + luaL_checktype(state, 2, LUA_TTABLE); + const int count = lua_objlen(state, 2); + std::vector labels; + std::vector options; + labels.reserve(count); + options.reserve(count); + for (int i = 1; i <= count; ++i) { + lua_rawgeti(state, 2, i); + luaL_checktype(state, -1, LUA_TTABLE); + labels.push_back(get_optional_string(state, -1, "label")); + UiControlOption option = UI_CONTROL_OPTION_INIT; + option.label = labels.back().c_str(); + option.enabled = get_optional_bool(state, -1, "enabled", true); + options.push_back(option); + lua_pop(state, 1); + } + check_result(state, + svc_ui->control_set_options( + element.vm->subject, element.value, options.data(), options.size()), + "ui control_set_options"); + return 0; +} + +int element_set_visible(lua_State* state) { + auto& element = check_ui_handle(state, 1, HandleKind::UiElement); + check_result(state, + svc_ui->elem_set_visible( + element.vm->subject, element.value, luaL_checkboolean(state, 2) != 0), + "ui elem_set_visible"); + return 0; +} + +int element_focus(lua_State* state) { + auto& element = check_ui_handle(state, 1, HandleKind::UiElement); + const auto result = svc_ui->elem_focus(element.vm->subject, element.value); + if (result != MOD_UNAVAILABLE) { + check_result(state, result, "ui elem_focus"); + } + lua_pushboolean(state, result == MOD_OK); + return 1; +} + +void context_menu_action(ModContext*, void* userData) { + auto& callback = *static_cast(userData); + call_callback(callback, callback.refs[0], 0, 0, "context menu action"); +} + +int context_menu_push(lua_State* state) { + auto& anchor = check_ui_handle(state, 1, HandleKind::UiElement); + luaL_checktype(state, 2, LUA_TTABLE); + lua_getfield(state, 2, "items"); + luaL_checktype(state, -1, LUA_TTABLE); + const int count = lua_objlen(state, -1); + std::vector labels; + std::vector icons; + std::vector items; + labels.reserve(count); + icons.reserve(count); + items.reserve(count); + for (int i = 1; i <= count; ++i) { + lua_rawgeti(state, -1, i); + luaL_checktype(state, -1, LUA_TTABLE); + labels.push_back(get_optional_string(state, -1, "label")); + icons.push_back(get_optional_string(state, -1, "icon")); + Callback& callback = retain_callback(*anchor.vm); + callback.refs[0] = ref_optional_function(state, -1, "on_pressed"); + UiContextMenuItem item = UI_CONTEXT_MENU_ITEM_INIT; + item.label = labels.back().c_str(); + item.icon = icons.back().c_str(); + item.on_pressed = callback.refs[0] != LUA_NOREF ? context_menu_action : nullptr; + item.user_data = &callback; + item.enabled = get_optional_bool(state, -1, "enabled", true); + item.selected = get_optional_bool(state, -1, "selected", false); + item.destructive = get_optional_bool(state, -1, "destructive", false); + item.separator_before = get_optional_bool(state, -1, "separator_before", false); + items.push_back(item); + lua_pop(state, 1); + } + lua_pop(state, 1); + UiContextMenuDesc desc = UI_CONTEXT_MENU_DESC_INIT; + desc.items = items.data(); + desc.item_count = items.size(); + UiContextMenuHandle handle = 0; + check_result(state, svc_ui->context_menu_push(anchor.vm->subject, anchor.value, &desc, &handle), + "ui context_menu_push"); + push_ui_handle(state, *anchor.vm, handle, HandleKind::UiContextMenu); + return 1; +} + +int context_menu_close(lua_State* state) { + auto& menu = check_ui_handle(state, 1, HandleKind::UiContextMenu); + check_result( + state, svc_ui->context_menu_close(menu.vm->subject, menu.value), "ui context_menu_close"); + menu.value = 0; + return 0; +} + int list_set_items(lua_State* state) { auto& list = check_ui_handle(state, 1, HandleKind::UiList); std::vector labels; @@ -634,8 +811,9 @@ int window_push(lua_State* state) { lua_pop(state, 1); } lua_pop(state, 1); - for (size_t i = 0; i < tabs.size(); ++i) + for (size_t i = 0; i < tabs.size(); ++i) { tabs[i].title = titles[i].c_str(); + } const std::string rcss = get_optional_string(state, 1, "rcss"); Callback& closed = retain_callback(vm); @@ -653,12 +831,15 @@ int window_push(lua_State* state) { } UiDialogVariant dialog_variant(lua_State* state, const std::string& variant) { - if (variant.empty() || variant == "normal") + if (variant.empty() || variant == "normal") { return UI_DIALOG_NORMAL; - if (variant == "warning") + } + if (variant == "warning") { return UI_DIALOG_WARNING; - if (variant == "danger") + } + if (variant == "danger") { return UI_DIALOG_DANGER; + } luaL_error(state, "unknown dialog variant '%s'", variant.c_str()); } @@ -692,8 +873,9 @@ int dialog_push(lua_State* state) { lua_pop(state, 1); } lua_pop(state, 1); - for (size_t i = 0; i < actions.size(); ++i) + for (size_t i = 0; i < actions.size(); ++i) { actions[i].label = labels[i].c_str(); + } Callback& callback = retain_callback(vm); callback.refs[0] = ref_optional_function(state, 1, "on_dismiss"); @@ -814,6 +996,13 @@ int open_ui(lua_State* state) { {"add_control", pane_add_control}, {"add_group", pane_add_group}, {"add_list", pane_add_list}, + {"add_row", pane_add_row}, + {"set_label", control_set_label}, + {"set_icon", control_set_icon}, + {"set_options", control_set_options}, + {"set_tooltip", control_set_tooltip}, + {"set_visible", element_set_visible}, + {"focus", element_focus}, {"set_text", element_set_text}, {"set_rml", element_set_rml}, {"set_progress", element_set_progress}, @@ -837,11 +1026,15 @@ int open_ui(lua_State* state) { create_handle_metatable(state, kUiStyleMetatable, kStyleMethods, "UiStyle"); create_handle_metatable(state, kUiMenuTabMetatable, kMenuMethods, "UiMenuTab"); create_handle_metatable(state, kUiListMetatable, kListMethods, "UiList"); + static const luaL_Reg kContextMenuMethods[] = { + {"close", context_menu_close}, {nullptr, nullptr}}; + create_handle_metatable(state, kUiContextMenuMetatable, kContextMenuMethods, "UiContextMenu"); lua_newtable(state); set_function(state, vm, "register_mods_panel", register_mods_panel); set_function(state, vm, "window_push", window_push); set_function(state, vm, "dialog_push", dialog_push); + set_function(state, vm, "context_menu_push", context_menu_push); set_function(state, vm, "is_any_document_visible", is_any_document_visible); set_function(state, vm, "register_styles", register_styles); set_function(state, vm, "register_styles_file", register_styles_file); @@ -853,4 +1046,4 @@ int open_ui(lua_State* state) { return 1; } -} // namespace luau_runtime +} // namespace luau_runtime::services diff --git a/mods/luau_runtime/src/services/ui.hpp b/mods/luau_runtime/src/services/ui.hpp new file mode 100644 index 0000000000..c1b8fb532d --- /dev/null +++ b/mods/luau_runtime/src/services/ui.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "../runtime.hpp" +#include "lua.h" + +namespace luau_runtime::services { + +void push_ui_handle(lua_State* state, Vm& vm, uint64_t value, HandleKind kind); + +} diff --git a/res/rml/command_console.rcss b/res/rml/command_console.rcss index b5378b5d7a..d19246692a 100644 --- a/res/rml/command_console.rcss +++ b/res/rml/command_console.rcss @@ -1,8 +1,6 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { + --console-command-color: #FFD966; + display: block; width: 100%; height: 100%; @@ -19,11 +17,11 @@ console { width: 50%; display: flex; flex-direction: column; - background-color: rgba(0, 0, 0, 60%); + background-color: rgba(var(--color-black-rgb), 60%); pointer-events: auto; - font-family: "Noto Mono"; - font-size: 14dp; - color: #FFFFFF; + font-family: var(--font-family-monospace); + font-size: var(--font-size-sm); + color: var(--color-white); transition: background-color 0.8s linear-in-out; } @@ -31,7 +29,7 @@ output { display: block; overflow: hidden; max-height: 480dp; - padding: 4dp 8dp; + padding: var(--space-xs) var(--space-sm); line-height: 1.4em; } @@ -46,11 +44,11 @@ console:not([open]) { } console:not([open])[fading] { - background-color: rgba(0, 0, 0, 0%); + background-color: rgba(var(--color-black-rgb), 0%); } console[open] { - background-color: rgba(0, 0, 0, 60%); + background-color: rgba(var(--color-black-rgb), 60%); transition: none; } @@ -75,19 +73,19 @@ output[open] line { } line.cmd { - color: #FFD966; + color: var(--console-command-color); } console input { display: none; width: 100%; - background-color: rgba(0, 0, 0, 40%); + background-color: rgba(var(--color-black-rgb), 40%); border: 0dp; - border-top: 1dp rgba(255, 255, 255, 20%); - color: #FFFFFF; - font-family: "Noto Mono"; - font-size: 14dp; - padding: 4dp 8dp; + border-top: 1dp rgba(var(--color-white-rgb), 20%); + color: var(--color-white); + font-family: var(--font-family-monospace); + font-size: var(--font-size-sm); + padding: var(--space-xs) var(--space-sm); } console[open] input { diff --git a/res/rml/logs.rcss b/res/rml/logs.rcss index 126684f9d2..fd1933ed3a 100644 --- a/res/rml/logs.rcss +++ b/res/rml/logs.rcss @@ -2,19 +2,20 @@ window.logs content { flex-flow: column; } -window.logs .log-toolbar { +window.logs log-toolbar { display: flex; flex-flow: row; - flex: 0 0 64dp; - height: 64dp; + flex: 0 0 var(--toolbar-height); + height: var(--toolbar-height); align-items: center; - gap: 8dp; + gap: var(--space-sm); padding-right: 72dp; - background-color: rgba(217, 217, 217, 10%); - border-bottom: 2dp #92875B; - font-family: "Fira Sans Condensed"; + background-color: rgba(var(--color-neutral-rgb), 10%); + border-bottom-width: 2dp; + border-bottom-color: var(--color-border); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; + font-size: var(--font-size-xl); } window.logs > close { @@ -22,76 +23,77 @@ window.logs > close { right: 8dp; } -window.logs .log-title { +window.logs log-title { align-self: stretch; flex: 0 0 auto; - padding: 0 24dp; - line-height: 64dp; + padding: 0 var(--space-xl); + line-height: var(--toolbar-height); text-transform: uppercase; - border-bottom: 4dp #C2A42D; + border-bottom-width: 4dp; + border-bottom-color: var(--color-accent); font-effect: glow(0dp 4dp 0dp 4dp black); } -window.logs .log-title-mod { +window.logs log-title-mod { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 15dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-base); + color: rgba(var(--color-text-rgb), 55%); } -.log-toolbar-spacer { +log-toolbar-spacer { flex: 1 1 0; } -.log-toolbar button { +log-toolbar button { flex: 0 0 auto; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 15dp; - padding: 5dp 12dp; + font-size: var(--font-size-base); + padding: 5dp var(--space-md); } window.logs content pane.log-view { flex: 1 1 0; - padding: 12dp 16dp; + padding: var(--space-md) var(--space-lg); padding-bottom: 0dp; gap: 0dp; } -.log-lines { +log-lines { display: block; } -.log-line { +log-line { display: block; - font-family: "Noto Mono"; - font-size: 13dp; + font-family: var(--font-family-monospace); + font-size: var(--font-size-xs); line-height: 1.5; word-break: break-word; white-space: pre-wrap; } -.log-line .log-time { - color: rgba(224, 219, 200, 45%); +log-line log-time { + color: rgba(var(--color-text-rgb), 45%); } -.log-line .log-mod { - color: rgba(194, 164, 45, 80%); +log-line log-mod { + color: rgba(var(--color-accent-rgb), 80%); } -.log-line.lvl-trace, -.log-line.lvl-debug { +log-line.lvl-trace, +log-line.lvl-debug { opacity: 0.55; } -.log-line.lvl-warn .log-msg { - color: #ffa826; +log-line.lvl-warn log-msg { + color: var(--color-warning); } -.log-line.lvl-error .log-msg { - color: #cc4444; +log-line.lvl-error log-msg { + color: var(--color-error); } diff --git a/res/rml/mod_browser.rcss b/res/rml/mod_browser.rcss new file mode 100644 index 0000000000..defe48448c --- /dev/null +++ b/res/rml/mod_browser.rcss @@ -0,0 +1,922 @@ +window.mod-browser, +window.mod-browser-detail, +window.screenshot-viewer { + background-color: rgba(var(--color-surface-rgb), 96%); +} + +window.mod-browser > content { + flex-flow: row; +} + +catalog-filters { + display: flex; + flex-flow: column; + flex: 0 0 264dp; + min-width: 0; + padding: var(--space-xl) 18dp; + gap: var(--space-sm); + border-right-width: 1dp; + border-right-color: var(--color-border); + background-color: rgba(var(--color-control-rgb), 45%); +} + +catalog-filters h1, +catalog-results header h1 { + margin: 0; + font-family: var(--font-family-heading); + font-size: var(--font-size-5xl); + font-weight: bold; +} + +catalog-filters h1 { + padding: 0 var(--space-sm) var(--space-sm) var(--space-sm); +} + +catalog-filters h2 { + margin: var(--space-md) var(--space-sm) 0 var(--space-sm); + font-family: var(--font-family-heading); + font-size: var(--font-size-xs); + font-weight: bold; + text-transform: uppercase; + opacity: 0.42; +} + +catalog-filters select-button { + padding: var(--space-sm) 10dp; + border-radius: var(--radius-panel); +} + +catalog-filters select-button key { + font-size: var(--font-size-xs); +} + +catalog-filters select-button value, +catalog-filters select-button input { + font-size: var(--font-size-sm); +} + +catalog-results { + display: flex; + flex-flow: column; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + padding: var(--space-xl); + gap: var(--space-lg); +} + +catalog-results header { + display: block; + flex: 0 0 auto; + padding-right: var(--space-2xl); +} + +catalog-results > header small { + display: block; + margin-top: var(--space-2xs); + font-size: var(--font-size-xs); + opacity: 0.5; +} + +catalog-viewport { + display: block; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + overflow: hidden auto; +} + +catalog-grid { + display: flex; + flex-flow: row wrap; + min-width: 0; + gap: 14dp; + padding: var(--space-2xs); +} + +.catalog-card { + display: flex; + flex-flow: column; + flex: 0 0 48%; + min-width: 260dp; + height: 310dp; + padding: 0; + overflow: hidden; + text-align: left; + border-radius: 12dp; +} + +catalog-card-art { + display: block; + position: relative; + flex: 0 0 118dp; + min-height: 118dp; +} + +catalog-card-art-image { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + mask-image: linear-gradient(180deg, #fff 45%, transparent); +} + +catalog-card-body { + display: flex; + position: relative; + flex-flow: column; + flex: 1 1 auto; + min-height: 0; + padding: var(--space-xl) var(--space-lg) var(--space-lg); + gap: 7dp; +} + +catalog-official-badge { + padding: var(--space-2xs) 5dp; + border-radius: var(--radius-small); + background-color: rgba(var(--color-accent-rgb), 28%); + color: var(--color-info); + font-style: normal; + font-size: var(--font-size-3xs); +} + +catalog-card-body > section > b { + display: block; + font-family: var(--font-family-heading); + font-size: var(--font-size-sm); + font-weight: bold; + text-transform: uppercase; + color: var(--color-accent); +} + +catalog-card-body > small { + position: absolute; + top: var(--space-md); + right: var(--space-lg); + margin: 0; + padding: 0; + font-size: var(--font-size-sm); + font-weight: normal; + text-transform: none; + opacity: 0.45; +} + +.catalog-card mod-icon { + display: block; + position: absolute; + left: 14dp; + bottom: -18dp; + z-index: 1; + width: 56dp; + height: 56dp; + border-radius: var(--radius-panel); + overflow: hidden; + box-shadow: rgba(var(--color-black-rgb), 60%) 0 6dp 16dp; +} + +mod-icon-image { + display: block; + width: 100%; + height: 100%; + border-radius: var(--radius-panel); +} + +catalog-card-body > section { + display: block; + margin: 0; + padding: 0; +} + +catalog-card-body > section h2 { + display: block; + margin: 0; + font-family: var(--font-family-body); + font-size: var(--font-size-3xl); + font-weight: bold; + color: var(--color-neutral); + line-height: 1.5; +} + +catalog-card-body > section small { + display: block; + font-size: var(--font-size-3xs); + opacity: 0.55; +} + +catalog-card-body > p { + flex: 1 1 auto; + min-height: 0; + margin: 0; + overflow: hidden; + font-size: var(--font-size-base); + line-height: 1.35; + color: rgba(var(--color-text-rgb), 68%); +} + +catalog-card-body > footer { + display: flex; + align-items: center; + gap: 10dp; + font-size: var(--font-size-3xs); +} + +catalog-card-body > footer stat { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 3dp; + white-space: nowrap; + opacity: 0.52; +} + +catalog-card-body > footer icon { + flex: 0 0 13dp; + font-size: var(--font-size-sm); + line-height: 1; +} + +catalog-card-body > footer .size { + margin-left: auto; + font-size: var(--font-size-sm); + white-space: nowrap; + opacity: 0.52; +} + +catalog-card-body > footer .size.installed { + color: var(--color-success); +} + +catalog-card-body > footer .size.update-available { + color: var(--color-accent); + opacity: 1; +} + +catalog-pagination { + display: flex; + flex-flow: row; + align-items: center; + flex: 0 0 auto; + gap: var(--space-sm); +} + +catalog-pagination-label { + flex: 1 1 auto; + text-align: center; + font-size: var(--font-size-xs); + opacity: 0.5; +} + +catalog-pagination button { + font-size: var(--font-size-sm); + padding: 6dp var(--space-md); +} + +catalog-results-status, +catalog-detail-status { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + height: 100%; + gap: var(--space-sm); + text-align: center; +} + +catalog-results-status h2, +catalog-detail-status h2 { + display: block; + margin: 0; + font-family: var(--font-family-heading); + font-size: var(--font-size-4xl); + font-weight: bold; +} + +catalog-results-status p, +catalog-detail-status p { + display: block; + margin: 0; + opacity: 0.58; +} + +catalog-results-status button, +catalog-detail-status button { + margin-top: var(--space-sm); + font-size: var(--font-size-base); +} + +window.mod-browser-detail > content { + display: block; +} + +detail-scroll { + display: flex; + flex-flow: column; + width: 100%; + height: 100%; + min-width: 0; + overflow: hidden auto; + padding-bottom: var(--space-2xl); +} + +catalog-detail-hero { + display: flex; + position: relative; + flex-flow: column; + justify-content: space-between; + flex: 0 0 220dp; + min-height: 220dp; + padding: 18dp var(--space-xl) 20dp var(--space-xl); +} + +catalog-detail-hero-image { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + mask-image: linear-gradient(180deg, #fff 40%, transparent); +} + +catalog-detail-actions { + display: flex; + position: relative; + z-index: 1; + flex-flow: row; + gap: var(--space-sm); +} + +catalog-detail-actions button, +catalog-source-actions button { + font-size: var(--font-size-sm); + padding: 7dp var(--space-md); + --button-background: rgba(var(--color-control-rgb), 75%); + --button-background-hover: rgba(var(--color-control-rgb), 90%); + --button-background-selected: rgba(var(--color-control-rgb), 90%); + --button-background-active: rgba(var(--color-surface-rgb), 90%); +} + +.catalog-icon-action { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.catalog-icon-action icon { + flex: 0 0 18dp; + font-size: var(--font-size-xl); + line-height: 1; +} + +.catalog-install-action { + display: flex; + position: relative; + align-items: center; + justify-content: center; + gap: 10dp; + min-width: 132dp; + padding: 8dp 22dp; + border-radius: var(--radius-window); + overflow: hidden; + font-size: var(--font-size-2xl); + opacity: 1; + --button-background: rgba(var(--color-control-rgb), 20%); + --button-background-hover: rgba(var(--color-interactive-rgb), 20%); + --button-background-selected: rgba(var(--color-interactive-rgb), 20%); + --button-background-active: rgba(var(--color-interactive-rgb), 20%); + box-shadow: var(--color-accent) 0 0 0 2dp; +} + +.catalog-install-action icon { + flex: 0 0 22dp; + font-size: var(--font-size-3xl); + line-height: 1; +} + +.catalog-install-action > span { + white-space: nowrap; +} + +.catalog-install-action.idle { + --button-background: rgba(var(--color-interactive-rgb), 18%); + --button-background-hover: rgba(var(--color-interactive-rgb), 42%); + --button-background-selected: rgba(var(--color-interactive-rgb), 42%); + --button-background-active: rgba(var(--color-interactive-rgb), 65%); + box-shadow: rgba(var(--color-accent-rgb), 65%) 0 0 0 1dp; +} + +.catalog-install-action.paused { + box-shadow: rgba(var(--color-border-rgb), 25%) 0 0 0 1dp; +} + +.catalog-install-action.paused:not(:disabled):hover, +.catalog-install-action.paused:not(:disabled):focus-visible { + box-shadow: var(--color-accent) 0 0 0 2dp; +} + +.catalog-install-action progress { + position: absolute; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 4dp; + margin: 0; + border-radius: 0; + overflow: hidden; + background-color: rgba(var(--color-white-rgb), 10%); +} + +.catalog-install-action progress fill { + border-radius: 0; + background-color: rgba(var(--color-accent-rgb), 80%); +} + +.catalog-install-action.paused progress fill { + background-color: rgba(var(--color-text-rgb), 35%); +} + +.catalog-install-action.retrying { + color: var(--color-warning); + box-shadow: rgba(var(--color-warning-rgb), 60%) 0 0 0 2dp; +} + +.catalog-install-action.retrying progress fill { + background-color: rgba(var(--color-warning-rgb), 60%); +} + +.catalog-install-action.failed { + color: var(--color-white); + --button-background: rgba(var(--color-error-rgb), 20%); + --button-background-hover: rgba(var(--color-error-rgb), 35%); + --button-background-selected: rgba(var(--color-error-rgb), 35%); + --button-background-active: rgba(var(--color-error-rgb), 35%); + box-shadow: var(--color-error) 0 0 0 2dp; +} + +.catalog-install-action.failed progress fill { + background-color: rgba(var(--color-error-rgb), 70%); +} + +.catalog-install-action.installed { + color: var(--color-success); + box-shadow: rgba(var(--color-success-rgb), 50%) 0 0 0 2dp; +} + +.catalog-install-action.installing progress fill { + background-color: rgba(var(--color-info-rgb), 80%); +} + +catalog-install-control { + display: flex; + flex-flow: column; + align-items: flex-end; + flex: 0 0 auto; + gap: var(--space-xs); +} + +catalog-install-caption { + display: block; + max-width: 240dp; + overflow: hidden; + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 50%); + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +catalog-install-caption.failed { + color: var(--color-error); +} + +catalog-detail-identity { + display: flex; + position: relative; + z-index: 1; + align-items: flex-end; + gap: var(--space-lg); +} + +catalog-detail-identity mod-icon { + display: block; + flex: 0 0 70dp; + width: 70dp; + height: 70dp; + border-radius: var(--radius-panel); + overflow: hidden; + box-shadow: rgba(var(--color-black-rgb), 65%) 0 8dp 20dp; +} + +catalog-detail-identity > header { + display: block; + flex: 1 1 auto; + min-width: 0; +} + +catalog-detail-identity > header > b { + display: block; + font-family: var(--font-family-heading); + font-size: var(--font-size-sm); + font-weight: bold; + text-transform: uppercase; + color: var(--color-accent); +} + +catalog-detail-identity h1 { + display: block; + margin: 1dp 0; + font-family: var(--font-family-body); + font-size: var(--font-size-5xl); + font-weight: bold; + color: var(--color-neutral); +} + +catalog-detail-identity h1 small { + margin-left: 9dp; + font-family: var(--font-family-body); + font-size: var(--font-size-xs); + font-weight: normal; + opacity: 0.55; +} + +catalog-detail-identity p { + display: block; + margin: 0; + font-size: var(--font-size-xs); + opacity: 0.72; +} + +catalog-detail-stats { + display: flex; + flex-flow: row; + align-items: center; + gap: 28dp; + margin: 0 var(--space-xl); + font-size: var(--font-size-2xs); + color: rgba(var(--color-text-rgb), 58%); +} + +catalog-detail-stats > stat { + display: flex; + align-items: center; + gap: 5dp; +} + +catalog-detail-stats icon { + flex: 0 0 17dp; + font-size: var(--font-size-lg); + line-height: 1; +} + +catalog-detail-body { + display: flex; + flex-flow: row; + align-items: flex-start; + gap: 28dp; + padding: var(--space-xl); +} + +catalog-detail-body main { + display: flex; + flex-flow: column; + flex: 1 1 auto; + min-width: 0; + gap: 28dp; +} + +catalog-detail-body section { + display: block; +} + +catalog-detail-body section.catalog-scroll-anchor { + focus: auto; +} + +catalog-detail-body section.catalog-scroll-anchor:focus-visible { + border-radius: var(--radius-control); + box-shadow: rgba(var(--color-accent-rgb), 45%) 0 0 0 1dp; +} + +catalog-detail-body h2, +catalog-detail-body h3 { + display: block; + margin: 0 0 10dp 0; + font-family: var(--font-family-heading); + font-size: var(--font-size-2xl); + font-weight: bold; +} + +catalog-detail-body h2 small { + margin-left: 7dp; + font-family: var(--font-family-body); + font-size: var(--font-size-3xs); + font-weight: normal; + opacity: 0.5; +} + +catalog-fragment { + display: block; + font-size: var(--font-size-md); + line-height: 1.55; + color: var(--color-neutral); +} + +catalog-fragment p, +catalog-fragment ul, +catalog-fragment ol { + display: block; + margin: 0 0 10dp 0; +} + +catalog-fragment ul, +catalog-fragment ol { + padding-left: var(--space-xl); +} + +catalog-fragment li { + display: block; + position: relative; + margin: 3dp 0; +} + +catalog-list-marker { + display: block; + position: absolute; + right: 100%; + width: var(--space-xl); + padding-right: var(--space-sm); + text-align: right; +} + +catalog-fragment h1, +catalog-fragment h2, +catalog-fragment h3, +catalog-fragment h4, +catalog-fragment h5, +catalog-fragment h6 { + display: block; + margin: 15dp 0 5dp 0; + font-family: var(--font-family-heading); + font-weight: bold; + font-size: var(--font-size-xl); + color: var(--color-neutral); +} + +catalog-fragment h1 { + font-size: var(--font-size-5xl); +} + +catalog-fragment h2 { + font-size: var(--font-size-4xl); +} + +catalog-fragment h3 { + font-size: var(--font-size-2xl); +} + +window.screenshot-viewer > close { + display: none; +} + +catalog-gallery { + display: flex; + flex-flow: row; + height: 210dp; + gap: var(--space-sm); +} + +.catalog-screenshot { + position: relative; + overflow: hidden; + flex: 1 1 0; + height: 100%; + min-width: 0; + padding: 0; + border-radius: var(--radius-panel); + --button-background: rgba(var(--color-border-rgb), 14%); + font-size: var(--font-size-5xl); +} + +.catalog-screenshot.primary { + flex: 2 1 0; +} + +catalog-screenshot-image, +catalog-screenshot-more { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; +} + +catalog-screenshot-more { + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(var(--color-black-rgb), 60%); + color: var(--color-white); +} + +catalog-dependencies { + display: flex; + flex-flow: column; + gap: 7dp; + font-size: var(--font-size-xs); +} + +catalog-dependency { + display: flex; + flex-flow: column; + padding: 9dp 11dp; + border-radius: var(--radius-control); + background-color: rgba(var(--color-border-rgb), 9%); +} + +catalog-dependency-status { + display: block; + font-size: var(--font-size-3xs); + opacity: 0.56; +} + +catalog-dependency.missing { + color: var(--color-warning); +} + +catalog-detail-body aside { + display: flex; + flex-flow: column; + flex: 0 0 264dp; + min-width: 0; + padding: var(--space-lg); + gap: var(--space-md); + border-radius: 9dp; + background-color: rgba(var(--color-control-rgb), 42%); + box-shadow: rgba(var(--color-border-rgb), 24%) 0 0 0 1dp; +} + +catalog-detail-body dl { + display: flex; + flex-flow: row wrap; + margin: 0; + font-size: var(--font-size-sm); +} + +catalog-detail-body dt { + flex: 0 0 42%; + padding: 5dp 0; + font-weight: bold; + opacity: 0.8; +} + +catalog-detail-body dd { + flex: 1 1 52%; + margin: 0; + padding: 5dp 0; + text-align: right; +} + +window.screenshot-viewer > content { + flex-flow: column; + padding: 18dp; + gap: var(--space-md); +} + +catalog-screenshot-full { + display: block; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + background-color: rgba(var(--color-black-rgb), 45%); +} + +catalog-screenshot-actions { + display: flex; + flex-flow: row; + justify-content: center; + flex: 0 0 auto; + gap: var(--space-sm); +} + +catalog-screenshot-actions button { + font-size: var(--font-size-sm); + padding: 7dp 13dp; +} + +@media (max-height: 640dp) { + catalog-filters { + flex-basis: 232dp; + padding: 15dp var(--space-md); + gap: 5dp; + } + + catalog-filters h1, + catalog-results header h1 { + font-size: var(--font-size-4xl); + } + + catalog-filters h2 { + margin-top: 6dp; + } + + catalog-results { + padding: 15dp; + gap: 10dp; + } + + .catalog-card { + min-width: 220dp; + height: 254dp; + } + + catalog-card-art { + flex-basis: 78dp; + min-height: 78dp; + } + + catalog-card-body { + padding: 18dp 10dp var(--space-sm) 10dp; + } + + .catalog-card mod-icon { + left: 10dp; + bottom: -14dp; + width: 40dp; + height: 40dp; + } + + catalog-card-body > section > b, + catalog-card-body > small { + font-size: var(--font-size-3xs); + } + + catalog-card-body > section h2 { + font-size: var(--font-size-md); + } + + catalog-card-body > p { + font-size: var(--font-size-3xs); + } + + catalog-detail-hero { + flex-basis: 160dp; + min-height: 160dp; + padding: var(--space-md) 18dp; + } + + catalog-detail-identity mod-icon { + flex-basis: 56dp; + width: 56dp; + height: 56dp; + } + + catalog-detail-identity { + gap: var(--space-md); + } + + catalog-detail-identity > header > b { + font-size: var(--font-size-2xs); + } + + .catalog-install-action { + gap: var(--space-sm); + min-width: 112dp; + padding: 7dp 18dp; + border-radius: 12dp; + font-size: var(--font-size-md); + } + + .catalog-install-action icon { + flex-basis: 18dp; + font-size: var(--font-size-xl); + } + + catalog-install-control { + gap: 3dp; + } + + catalog-install-caption { + max-width: 190dp; + font-size: var(--font-size-3xs); + } + + catalog-detail-identity h1 { + font-size: var(--font-size-3xl); + } + + catalog-detail-body { + padding: 18dp; + gap: 18dp; + } + + catalog-gallery { + height: 160dp; + } +} diff --git a/res/rml/mod_common.rcss b/res/rml/mod_common.rcss new file mode 100644 index 0000000000..f221049b6e --- /dev/null +++ b/res/rml/mod_common.rcss @@ -0,0 +1,36 @@ +update-badge { + display: inline-block; + flex: 0 0 auto; + align-self: center; + min-width: 20dp; + height: 20dp; + margin-left: 8dp; + padding: 0 6dp; + border-radius: 10dp; + background-color: var(--color-accent); + color: #19170f; + font-family: var(--font-family-body); + font-size: 12dp; + font-weight: bold; + line-height: 20dp; + text-align: center; + white-space: nowrap; + vertical-align: middle; + pointer-events: none; + transform: translateY(0dp); +} + +.mod-update-link { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.mod-update-link > update-label { + display: block; + flex: 0 0 auto; +} + +.mod-update-link > update-badge { + margin-left: 0; +} diff --git a/res/rml/mods.rcss b/res/rml/mods.rcss index 34e86f485b..d6c3f394fb 100644 --- a/res/rml/mods.rcss +++ b/res/rml/mods.rcss @@ -1,8 +1,8 @@ window.mods content pane.mod-list { flex: 0 0 360dp; - padding: 16dp; + padding: var(--space-lg); padding-bottom: 0dp; - gap: 4dp; + gap: var(--space-xs); } @media (max-height: 640dp) { @@ -12,29 +12,44 @@ window.mods content pane.mod-list { } window.mods content pane.mod-detail { - gap: 12dp; + gap: var(--space-md); } -.mod-info-row { +mod-entry.online { + min-height: 76dp; +} + +mod-entry.online mod-icon { + color: var(--color-accent); + decorator: text("" center center); +} + +mod-list-separator { + display: block; + height: 1dp; + margin: var(--space-sm) 10dp; + background-color: rgba(var(--color-border-rgb), 30%); +} + +mod-info-row { display: flex; align-items: center; - gap: 12dp; - padding: 4dp 0; + gap: var(--space-md); + padding: var(--space-xs) 0; } -.mod-info-label { - font-family: "Fira Sans Condensed"; - font-weight: bold; +mod-info-row > b { + font-family: var(--font-family-heading); opacity: 0.55; flex: 0 0 auto; } -.mod-info-value { +mod-info-row > span { flex: 1 1 0; } .mod-path { - font-size: 14dp; + font-size: var(--font-size-sm); word-break: break-all; opacity: 0.7; } @@ -42,106 +57,138 @@ window.mods content pane.mod-detail { mod-entry { display: flex; flex-flow: row; - gap: 12dp; + flex: 0 0 auto; + gap: var(--space-md); padding: 10dp; border-radius: 10dp; - decorator: vertical-gradient(#c2a42d00 #c2a42d00); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0%) rgba(var(--color-accent-rgb), 0%)); transition: decorator 0.1s linear-in-out; cursor: pointer; focus: auto; } mod-entry.current { - box-shadow: rgba(146, 135, 91, 40%) 0 0 0 1dp; + box-shadow: rgba(var(--color-border-rgb), 40%) 0 0 0 1dp; } mod-entry:hover, mod-entry:focus-visible { - decorator: vertical-gradient(#c2a42d00 #c2a42d26); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0) rgba(var(--color-accent-rgb), 38)); } mod-entry:selected { - decorator: vertical-gradient(#c2a42d10 #c2a42d40); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 16) rgba(var(--color-accent-rgb), 64)); } -mod-entry .mod-icon { +mod-icon { + display: block; flex: 0 0 auto; width: 56dp; height: 56dp; - border-radius: 8dp; -} - -mod-entry icon.mod-icon { + border-radius: var(--radius-panel); + font-family: var(--font-family-icons); font-size: 36dp; - background-color: rgba(17, 16, 10, 20%); - color: rgba(224, 219, 200, 45%); + background-color: rgba(var(--color-control-rgb), 20%); + color: rgba(var(--color-text-rgb), 45%); decorator: text("" center center); + overflow: hidden; } -mod-entry .mod-entry-info { +mod-icon img { + display: block; + width: 100%; + height: 100%; + border-radius: var(--radius-panel); +} + +mod-info { display: flex; flex-flow: column; flex: 1 1 0; min-width: 0; - gap: 2dp; + gap: var(--space-2xs); } -mod-entry .mod-entry-name { +mod-info header { display: flex; flex-flow: row; - align-items: baseline; + align-items: center; gap: 6dp; } -mod-entry .mod-entry-name-text { +mod-info header b { flex: 0 1 auto; min-width: 0; - font-weight: bold; white-space: nowrap; overflow: hidden; + text-overflow: ellipsis; } -mod-entry .mod-entry-version { +mod-info header update-badge { + margin-left: 0; +} + +mod-meta { + display: flex; + align-items: baseline; + gap: 5dp; + min-width: 0; + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 50%); +} + +mod-meta > * { + display: block; flex: 0 0 auto; - font-size: 13dp; - color: rgba(224, 219, 200, 50%); + white-space: nowrap; } -mod-entry .mod-entry-status.active { - color: #44cc55; +mod-meta > mod-author { + flex: 0 1 auto; + min-width: 0; + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 50%); + overflow: hidden; + text-overflow: ellipsis; } -mod-entry .mod-entry-status.failed { - color: #cc4444; +mod-status.active { + color: var(--color-success); } -mod-entry .mod-entry-network { - margin-left: 6dp; +mod-status.failed { + color: var(--color-error); +} + +mod-network { + flex: 0 0 auto; padding: 1dp 5dp; + font-size: var(--font-size-xs); border-radius: 5dp; - background-color: rgba(67, 151, 219, 20%); - color: #6fb7ef; + background-color: rgba(var(--color-info-rgb), 20%); + color: var(--color-info); } -mod-entry .mod-entry-desc { - font-size: 14dp; +mod-info > p { + display: block; + margin: 0; + font-size: var(--font-size-sm); line-height: 1.3; - color: rgba(224, 219, 200, 65%); + color: rgba(var(--color-text-rgb), 65%); max-height: 2.6em; overflow: hidden; - white-space: pre-wrap; } -mod-entry .mod-entry-sub { - font-size: 13dp; - color: rgba(224, 219, 200, 50%); +mod-info > small { + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 50%); } -mod-entry.inactive .mod-icon { +mod-entry.inactive mod-icon { filter: grayscale(1); } -mod-entry.inactive .mod-entry-info { +mod-entry.inactive mod-info { opacity: 0.5; } @@ -155,20 +202,37 @@ mod-header.has-banner { margin: -24dp -24dp 0dp -24dp; } -mod-header .mod-actions { +mod-header-image { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + mask-image: linear-gradient(180deg, #fff 40%, transparent); + filter: grayscale(0); +} + +mod-header.inactive mod-header-image { + filter: grayscale(1); +} + +mod-actions { position: absolute; top: 24dp; left: 24dp; display: flex; flex-flow: row; - gap: 8dp; + gap: var(--space-sm); } -mod-header .mod-actions button { - font-size: 16dp; - padding: 6dp 14dp; - background-color: rgba(21, 22, 16, 80%); - box-shadow: rgba(146, 135, 91, 60%) 0 0 0 1dp; +mod-actions button { + --button-background: rgba(var(--color-surface-rgb), 80%); + --button-background-hover: rgba(var(--color-control-rgb), 90%); + --button-background-selected: rgba(var(--color-control-rgb), 90%); + --button-background-active: rgba(var(--color-surface-rgb), 90%); + box-shadow: rgba(var(--color-border-rgb), 60%) 0 0 0 1dp; } mod-header.no-banner { @@ -177,56 +241,236 @@ mod-header.no-banner { align-items: center; } -mod-header.no-banner .mod-actions { +mod-header.no-banner mod-actions { position: static; } -window.mods .mod-title { +mod-title { display: block; - font-size: 28dp; + font-size: var(--font-size-5xl); font-weight: bold; } -window.mods .mod-title .mod-title-version { +mod-title small { font-weight: normal; - font-size: 16dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-md); + color: rgba(var(--color-text-rgb), 55%); } -window.mods .mod-author { +mod-author { display: block; - font-size: 15dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-base); + color: rgba(var(--color-text-rgb), 55%); } -window.mods .mod-restart-note { - font-size: 15dp; - color: #ffa826; +mod-restart-note { + font-size: var(--font-size-base); + color: var(--color-warning); opacity: 0.85; } -window.mods .mod-description { +mod-description { line-height: 1.5; } -.status-badge { - font-size: 14dp; +status-badge { + font-size: var(--font-size-sm); opacity: 0.7; } -.status-badge.active, -.mod-info-label.active { - color: #44cc55; +status-badge.active, +mod-info-row > b.active { + color: var(--color-success); opacity: 1; } -.status-badge.failed, -.mod-info-label.failed { - color: #cc4444; +status-badge.failed, +mod-info-row > b.failed { + color: var(--color-error); + opacity: 1; +} +status-badge.network { + color: var(--color-info); opacity: 1; } -.status-badge.network { - color: #6fb7ef; - opacity: 1; +button.browse-mods-action { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-lg); + margin-right: 48dp; + padding: 22dp; + text-align: left; + --button-background: rgba(var(--color-accent-rgb), 18%); + --button-background-hover: rgba(var(--color-accent-rgb), 28%); + --button-background-selected: rgba(var(--color-accent-rgb), 28%); + --button-background-active: rgba(var(--color-accent-rgb), 38%); + box-shadow: rgba(var(--color-accent-rgb), 65%) 0 0 0 1dp; +} + +browse-copy { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-width: 0; + gap: var(--space-sm); +} + +browse-copy h2 { + display: block; + margin: 0; + font-family: var(--font-family-heading); + font-size: var(--font-size-5xl); + font-weight: bold; +} + +browse-copy p { + display: block; + margin: 0; + font-size: var(--font-size-md); + line-height: 1.4; + color: rgba(var(--color-text-rgb), 70%); +} + +.browse-mods-action > icon { + flex: 0 0 auto; + font-size: var(--font-size-4xl); + color: var(--color-accent); +} + +updates-header, update-card, update-changelog, queue-row { + display: block; +} + +updates-header { + padding: var(--space-sm) 0; +} + +updates-header h2, online-section-heading h2 { + display: block; + flex: 1 1 auto; + margin: 0; + font-family: var(--font-family-heading); + font-size: var(--font-size-3xl); + font-weight: bold; +} + +updates-heading, online-section-heading { + display: flex; + align-items: center; + gap: var(--space-sm); + margin-bottom: var(--space-sm); +} + +online-section-heading { + flex-wrap: wrap; + justify-content: space-between; + padding-top: var(--space-sm); +} + +online-section-heading button { + flex: 0 0 auto; + padding: 6dp 12dp; + font-size: var(--font-size-sm); +} + +updates-heading button.icon-button { + flex: 0 0 30dp; + width: 30dp; + height: 30dp; + padding: 4dp; + font-size: 22dp; +} + +updates-header p { + display: block; + margin: 0 0 var(--space-sm); + font-size: var(--font-size-sm); + line-height: 1.4; +} + +updates-actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); +} + +updates-actions button.update-all { + display: flex; + align-items: center; + justify-content: center; + gap: 8dp; + padding: 8dp 14dp; + font-size: var(--font-size-md); + box-shadow: rgba(var(--color-accent-rgb), 40%) 0 0 0 1dp; +} + +updates-actions button.update-all icon { + flex: 0 0 20dp; + font-size: var(--font-size-xl); +} + +updates-actions button.update-all > span { + white-space: nowrap; +} + +queue-row { + border-bottom-width: 1dp; + border-bottom-color: rgba(var(--color-border-rgb), 30%); +} + +queue-row package-row { + padding: 8dp 0 14dp; + border-bottom-width: 0; +} + +window.mods package-row > section > header { + flex-wrap: wrap; +} + +window.mods package-row h3 { + flex-wrap: wrap; +} + +window.mods package-row.failed footer > small { + white-space: normal; + line-height: 1.3; +} + +update-card { + padding: 12dp 0; + border-bottom-width: 1dp; + border-bottom-color: #ffffff22; +} + +update-card package-row { + padding: 0; + border-bottom-width: 0; +} + +update-changelog { + padding: 8dp 12dp; + margin-top: 8dp; + background-color: #00000022; + border-radius: var(--radius-small); + font-size: var(--font-size-md); + line-height: 1.5; +} + +update-changelog p, +update-changelog h1, +update-changelog h2, +update-changelog h3, +update-changelog ul, +update-changelog ol, +update-changelog blockquote, +update-changelog pre { + display: block; + margin: 0 0 var(--space-sm); +} +update-changelog h1, +update-changelog h2, +update-changelog h3 { + font-weight: bold; } diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index 98f8bfa27a..a200005ca1 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -8,10 +8,10 @@ body { height: 100%; margin: 0; padding: 0; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 20dp; - color: #E0DBC8; + font-size: var(--font-size-2xl); + color: var(--color-text); display: flex; flex-direction: column; justify-content: flex-end; @@ -24,16 +24,22 @@ fps, pipeline-progress, toast { position: absolute; - border: 1dp #92875B; - background-color: rgba(21, 22, 16, 80%); + border-width: 1dp; + background-color: rgba(var(--color-surface-rgb), 80%); +} + +fps, +pipeline-progress { + border-color: var(--color-border); } toast { + border-color: var(--toast-border-color); top: 40dp; right: 40dp; display: flex; flex-flow: column; - border-radius: 14dp; + border-radius: var(--radius-window); overflow: hidden; backdrop-filter: blur(5dp); box-shadow: 0 0 15dp 3dp; @@ -41,8 +47,8 @@ toast { transform: scale(0.9); transform-origin: center; transition: filter transform 0.2s cubic-in-out; - padding: 18dp 24dp; - gap: 8dp; + padding: 18dp var(--space-xl); + gap: var(--space-sm); } toast[open] { @@ -50,15 +56,6 @@ toast[open] { transform: scale(1); } -/*toast:hover { - cursor: pointer; - background-color: rgba(61, 59, 36, 80%); -} - -toast:active { - background-color: rgba(45, 43, 26, 80%); -}*/ - b { font-weight: bold; } @@ -67,14 +64,14 @@ toast heading { display: flex; gap: 18dp; align-items: center; - font-family: "Fira Sans Condensed"; - font-size: 18dp; + font-family: var(--font-family-heading); + font-size: var(--font-size-xl); font-weight: bold; text-transform: uppercase; - color: #92875B; + color: var(--toast-heading-color); } -toast heading > span { +toast heading > toast-title { flex: 1 0 auto; } @@ -82,13 +79,13 @@ toast heading > row { flex: 1 0 auto; display: flex; align-items: center; - gap: 4dp; + gap: var(--space-xs); } toast message { display: flex; flex-flow: column; - gap: 8dp; + gap: var(--space-sm); } toast message row { @@ -99,6 +96,50 @@ toast message row.muted { opacity: 0.5; } +toast.mod-installed row { + align-items: center; + gap: var(--space-md); +} + +mod-icon { + flex: 0 0 42dp; + width: 42dp; + height: 42dp; + overflow: hidden; + border-radius: var(--radius-panel); + background-color: rgba(var(--color-control-rgb), 45%); + color: rgba(var(--color-text-rgb), 45%); + font-family: var(--font-family-icons); + font-size: var(--font-size-4xl); + decorator: text("" center center); +} + +mod-icon img { + width: 100%; + height: 100%; + border-radius: var(--radius-panel); +} + +mod-info { + display: flex; + flex-flow: column; + min-width: 0; + gap: var(--space-xs); +} + +mod-name { + color: var(--color-white); +} + +toast.mod-installed small { + font-size: var(--font-size-sm); + color: rgba(var(--color-text-rgb), 55%); +} + +toast.mod-installed small.version { + margin-left: var(--space-sm); +} + progress { height: 4dp; position: absolute; @@ -108,7 +149,7 @@ progress { } progress fill { - background-color: rgba(194, 164, 45, 80%); + background-color: rgba(var(--color-accent-rgb), 80%); } pipeline-progress { @@ -119,8 +160,8 @@ pipeline-progress { z-index: 100; min-width: 260dp; max-width: 90%; - padding: 10dp 16dp 12dp; - border-radius: 7dp; + padding: 10dp var(--space-lg) var(--space-md); + border-radius: var(--radius-control); overflow: hidden; filter: opacity(0); transition: filter 0.2s linear-in-out; @@ -134,8 +175,8 @@ pipeline-progress[open] { pipeline-status { display: flex; align-items: center; - gap: 8dp; - font-size: 18dp; + gap: var(--space-sm); + font-size: var(--font-size-xl); font-weight: normal; white-space: nowrap; } @@ -145,26 +186,16 @@ icon.pipeline-spinner { height: 1.2em; line-height: 1.2em; font-size: 1.2em; - color: #C2A42D; + color: var(--color-accent); text-align: center; transform-origin: center; animation: 1s linear infinite pipeline-spinner-spin; } -toast.achievement { - border: 1dp #C2A42D; -} - -toast.achievement heading { - color: #C2A42D; -} - +toast.achievement, toast.warning { - border: 1dp #C2A42D; -} - -toast.warning heading { - color: #C2A42D; + --toast-border-color: var(--color-accent); + --toast-heading-color: var(--color-accent); } toast.controller-warning { @@ -181,8 +212,8 @@ toast.controller-warning[open] { transform: translateX(-50%) scale(1); } -toast.controller-warning heading { - color: #C2A42D; +toast.controller-warning { + --toast-heading-color: var(--color-accent); } toast.menu-notification { @@ -209,7 +240,7 @@ toast.menu-notification message row { } icon { - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; display: inline-block; vertical-align: middle; @@ -243,13 +274,20 @@ icon.warning { decorator: text("" center center); } +icon.download-done { + width: 1.2em; + height: 1.2em; + font-size: 1.2em; + decorator: text("" center center); +} + fps { display: none; z-index: 99; - font-size: 18dp; + font-size: var(--font-size-xl); font-weight: bold; - padding: 9dp 12dp; - border-radius: 7dp; + padding: 9dp var(--space-md); + border-radius: var(--radius-control); pointer-events: none; white-space: nowrap; } @@ -260,12 +298,12 @@ speedrun-timer { bottom: 0; right: 0; z-index: 99; - background-color: rgba(0, 0, 0, 65%); - padding: 2dp 4dp; + background-color: rgba(var(--color-black-rgb), 65%); + padding: var(--space-2xs) var(--space-xs); pointer-events: none; - font-family: "Noto Mono"; - font-size: 16dp; - color: #ffffff; + font-family: var(--font-family-monospace); + font-size: var(--font-size-md); + color: var(--color-white); white-space: nowrap; } @@ -329,7 +367,7 @@ logo img { left: 0; width: 100%; height: 100%; - filter: drop-shadow(#0008 0 0 14dp); + filter: drop-shadow(rgba(var(--color-black-rgb), 53.333333%) 0 0 14dp); transform-origin: center; } diff --git a/res/rml/popover.rcss b/res/rml/popover.rcss index 1db4f64a05..4b01ebdc1f 100644 --- a/res/rml/popover.rcss +++ b/res/rml/popover.rcss @@ -3,6 +3,8 @@ } body { + --button-background: rgba(var(--color-control-rgb), 35%); + width: 100%; height: 100%; z-index: 10; @@ -12,14 +14,15 @@ popover { position: absolute; display: flex; flex-flow: column; - font-family: "Fira Sans"; - font-size: 14dp; - color: #E0DBC8; - border-radius: 14dp; - border: 2dp #92875B; - background-color: rgba(21, 22, 16, 96%); + font-family: var(--font-family-body); + font-size: var(--font-size-sm); + color: var(--color-text); + border-radius: var(--radius-window); + border-width: 2dp; + border-color: var(--color-border); + background-color: rgba(var(--color-surface-rgb), 96%); backdrop-filter: blur(5dp); - box-shadow: 0 6dp 24dp 2dp rgba(0, 0, 0, 55%); + box-shadow: 0 6dp 24dp 2dp rgba(var(--color-black-rgb), 55%); filter: opacity(0); transform: scale(0.95); transform-origin: center; @@ -43,8 +46,8 @@ color-sv { position: relative; width: 240dp; height: 150dp; - border-radius: 8dp; - box-shadow: rgba(146, 135, 91, 50%) 0 0 0 1dp; + border-radius: var(--radius-panel); + box-shadow: rgba(var(--color-border-rgb), 50%) 0 0 0 1dp; drag: drag; focus: auto; } @@ -55,8 +58,8 @@ color-alpha { position: relative; width: 240dp; height: 14dp; - border-radius: 7dp; - box-shadow: rgba(146, 135, 91, 50%) 0 0 0 1dp; + border-radius: var(--radius-control); + box-shadow: rgba(var(--color-border-rgb), 50%) 0 0 0 1dp; drag: drag; focus: auto; } @@ -64,13 +67,13 @@ color-alpha { color-sv:focus-visible, color-hue:focus-visible, color-alpha:focus-visible { - box-shadow: #C2A42D 0 0 0 2dp; + box-shadow: var(--color-accent) 0 0 0 2dp; } color-sv.adjusting, color-hue.adjusting, color-alpha.adjusting { - box-shadow: #FFFFFF 0 0 0 3dp; + box-shadow: var(--color-white) 0 0 0 3dp; } color-hue { @@ -82,20 +85,21 @@ color-cursor { position: absolute; width: 14dp; height: 14dp; - border-radius: 7dp; - border: 2dp #ffffff; - box-shadow: 0 0 4dp 1dp rgba(0, 0, 0, 70%); + border-radius: var(--radius-control); + border-width: 2dp; + border-color: var(--color-white); + box-shadow: 0 0 4dp 1dp rgba(var(--color-black-rgb), 70%); pointer-events: none; } color-heading { display: block; - margin-top: 2dp; - font-family: "Fira Sans Condensed"; + margin-top: var(--space-2xs); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 13dp; + font-size: var(--font-size-xs); text-transform: uppercase; - color: rgba(224, 219, 200, 55%); + color: rgba(var(--color-text-rgb), 55%); } color-presets { @@ -114,7 +118,7 @@ button.color-swatch-button { width: 25dp; height: 25dp; padding: 0; - border-radius: 7dp; + border-radius: var(--radius-control); } color-chip { @@ -123,62 +127,132 @@ color-chip { width: 20dp; height: 20dp; border-radius: 5dp; - box-shadow: rgba(255, 255, 255, 45%) 0 0 0 1dp; + box-shadow: rgba(var(--color-white-rgb), 45%) 0 0 0 1dp; } button.color-swatch-button color-chip { width: 25dp; height: 25dp; - border-radius: 7dp; + border-radius: var(--radius-control); } color-chip.empty, color-swatch.empty { - background-color: rgba(224, 219, 200, 12%); - decorator: linear-gradient(135deg, rgba(224, 219, 200, 0) 45%, - rgba(194, 164, 45, 70%) 48%, rgba(194, 164, 45, 70%) 52%, - rgba(224, 219, 200, 0) 55%); + background-color: rgba(var(--color-text-rgb), 12%); + decorator: linear-gradient(135deg, rgba(var(--color-text-rgb), 0) 45%, + rgba(var(--color-accent-rgb), 70%) 48%, rgba(var(--color-accent-rgb), 70%) 52%, + rgba(var(--color-text-rgb), 0) 55%); } color-footer { display: flex; align-items: center; gap: 6dp; - padding-top: 2dp; + padding-top: var(--space-2xs); } button { - background-color: rgba(17, 16, 10, 35%); - padding: 4dp 8dp; - border-radius: 8dp; - box-shadow: rgba(146, 135, 91, 30%) 0 0 0 1dp; - color: #E0DBC8; + background-color: var(--button-background); + padding: var(--space-xs) var(--space-sm); + border-radius: var(--radius-panel); + box-shadow: rgba(var(--color-border-rgb), 30%) 0 0 0 1dp; + color: var(--button-color); cursor: pointer; focus: auto; } button:hover, button:focus-visible { - background-color: rgba(204, 184, 119, 20%); - box-shadow: #C2A42D 0 0 0 2dp; + background-color: var(--button-background-hover); + box-shadow: var(--color-accent) 0 0 0 2dp; } button:active { - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-active); } color-value { display: block; flex: 1 1 auto; text-align: right; - font-family: "Noto Mono"; - font-size: 12dp; - color: #FFFFFF; + font-family: var(--font-family-monospace); + font-size: var(--font-size-2xs); + color: var(--color-white); cursor: pointer; focus: auto; } color-value:hover, color-value:focus-visible { - color: #C2A42D; + color: var(--color-accent); +} + +popover.context-menu { + min-width: 200dp; + max-width: 90%; + max-height: 90%; + overflow-y: auto; + padding: 6dp; + gap: 2dp; + transform-origin: left top; +} + +.context-menu button { + display: flex; + align-items: center; + gap: 10dp; + padding: 8dp 12dp; + flex: 0 0 auto; + white-space: nowrap; + --button-background: transparent; + background-color: var(--button-background); + box-shadow: none; + transition: background-color 0.1s linear-in-out; +} + +.context-menu button:selected { + --button-background: var(--button-background-selected); +} + +.context-menu button:selected icon { + color: var(--color-accent); +} + +.context-menu button:not(:disabled):hover, +.context-menu button:not(:disabled):focus-visible { + background-color: var(--button-background-hover); + box-shadow: none; +} + +.context-menu button:not(:disabled):active { + background-color: var(--button-background-active); +} + +.context-menu button:disabled { + background-color: transparent; + box-shadow: none; + opacity: 0.4; + cursor: unavailable; +} + +.context-menu button.destructive { + color: var(--color-error); +} + +.context-menu icon { + display: block; + flex: 0 0 1em; + width: 1em; + height: 1em; + font-family: var(--font-family-icons); + font-weight: normal; + line-height: 1; +} + +menu-separator { + display: block; + flex: 0 0 1dp; + height: 1dp; + margin: 4dp 6dp; + background-color: var(--color-border); } diff --git a/res/rml/popup.rcss b/res/rml/popup.rcss index effc80344d..1ed1652ecd 100644 --- a/res/rml/popup.rcss +++ b/res/rml/popup.rcss @@ -8,10 +8,10 @@ body { height: 100%; margin: 0; padding: 0; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; - color: #E0DBC8; + font-size: var(--font-size-xl); + color: var(--color-text); } button { @@ -23,11 +23,12 @@ popup { width: 100%; display: flex; align-items: stretch; - height: 64dp; - background-color: rgba(21, 22, 16, 80%); - border-bottom: 2dp #92875B; + height: var(--toolbar-height); + background-color: rgba(var(--color-surface-rgb), 80%); + border-bottom-width: 2dp; + border-bottom-color: var(--color-border); backdrop-filter: blur(5dp); - transform: translateY(-64dp); + transform: translateY(var(--toolbar-hidden-offset)); transition: transform 0.2s cubic-in-out; } @@ -41,5 +42,5 @@ popup tab-bar { popup tab-bar tab { opacity: 0.35; - color: #E0DBC8; + color: var(--color-text); } diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss index 6cc5da65ae..f3110a4fec 100644 --- a/res/rml/prelaunch.rcss +++ b/res/rml/prelaunch.rcss @@ -1,32 +1,40 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { + --color-prelaunch-accent: #FEE685; + --color-prelaunch-muted: #A6A09B; + --color-disc-error: #FFC9C9; + --color-disc-mismatch: #FFD6A7; + --color-ready: #D8F999; + --menu-button-decorator: horizontal-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-black-rgb), 0%)); + --menu-button-decorator-hover: horizontal-gradient(#FEE685FF #FEE68500); + width: 100%; height: 100%; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 20dp; - color: #FFFFFF; + font-size: var(--font-size-2xl); + color: var(--color-white); filter: opacity(0); transition: filter 1s 0.2s linear-in-out; z-index: -1; } -.gradient { +body.mirrored { + --menu-button-decorator-hover: horizontal-gradient(#FEE68500 #FEE685FF); +} + +prelaunch-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); + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 100%) rgba(var(--color-black-rgb), 0%)); } -body.mirrored .gradient { - decorator: horizontal-gradient(#00000000 #000000FF); +body.mirrored prelaunch-gradient { + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-black-rgb), 100%)); } -.background { +prelaunch-background { position: absolute; width: 100%; height: 100%; @@ -39,11 +47,11 @@ body[open] { filter: opacity(1); } -body[open] .background { +body[open] prelaunch-background { opacity: 1; } -body.disc-ready .background { +body.disc-ready prelaunch-background { opacity: 0; } @@ -84,7 +92,7 @@ hero { display: flex; flex-direction: column; align-items: flex-start; - gap: 4dp; + gap: var(--space-xs); } body.mirrored hero { @@ -96,8 +104,8 @@ hero img { } eyebrow { - font-family: "Alegreya SC"; - font-size: 32dp; + font-family: var(--font-family-display); + font-size: var(--font-size-6xl); } @media (min-width: 1216dp) { @@ -107,36 +115,36 @@ eyebrow { } } -eyebrow span { +eyebrow studio-name { font-weight: bold; } -#menu-list { +menu-list { display: flex; flex-direction: column; - gap: 12dp; + gap: var(--space-md); align-items: flex-start; } #menu-list button { width: 428dp; height: 54dp; - padding: 8dp 16dp; - border-radius: 8dp; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-panel); text-align: left; text-transform: uppercase; - font-family: "Fira Sans Condensed"; - font-size: 32dp; + font-family: var(--font-family-heading); + font-size: var(--font-size-6xl); font-weight: normal; cursor: pointer; /* Define a fully transparent gradient as the default state, otherwise a white flash occurs */ - decorator: horizontal-gradient(#00000000 #00000000); + decorator: var(--menu-button-decorator); } #menu-list button:disabled { opacity: 0.75; cursor: default; - decorator: horizontal-gradient(#00000000 #00000000); + decorator: var(--menu-button-decorator); } #menu-list button.anim-done { @@ -157,7 +165,7 @@ eyebrow span { width: 100%; height: 100%; overflow: hidden; - border-radius: 8dp; + border-radius: var(--radius-panel); pointer-events: none; z-index: 0; } @@ -169,7 +177,7 @@ eyebrow span { left: 0; width: 100%; height: 100%; - padding: 8dp 16dp; + padding: var(--space-sm) var(--space-lg); opacity: 0; text-overflow: ellipsis; white-space: nowrap; @@ -189,8 +197,8 @@ eyebrow span { height: 54dp; align-items: center; justify-content: center; - color: #FFFFFF; - font-family: "Material Symbols Rounded"; + color: var(--color-white); + font-family: var(--font-family-icons); font-weight: normal; font-size: 30dp; z-index: 1; @@ -213,8 +221,8 @@ eyebrow span { #menu-list button:hover, #menu-list button:focus-visible { - color: black; - decorator: horizontal-gradient(#FEE685FF #FEE68500); + color: var(--color-black); + decorator: var(--menu-button-decorator-hover); } body.mirrored #menu-list { @@ -225,9 +233,12 @@ 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); +body.mirrored #menu-list button.mod-update-link { + justify-content: flex-end; +} + +#menu-list update-badge { + font-size: var(--font-size-md); } disc-info { @@ -237,8 +248,8 @@ disc-info { bottom: 72dp; display: flex; flex-direction: column; - gap: 12dp; - font-size: 24dp; + gap: var(--space-md); + font-size: var(--font-size-4xl); font-effect: glow(0dp 4dp 0dp 4dp black); text-align: left; } @@ -256,9 +267,9 @@ version-info { bottom: 72dp; display: flex; flex-direction: column; - gap: 12dp; + gap: var(--space-md); text-align: right; - font-size: 24dp; + font-size: var(--font-size-4xl); font-effect: glow(0dp 4dp 0dp 4dp black); text-align: right; } @@ -272,40 +283,40 @@ body.mirrored version-info { #disc-status { display: flex; align-items: center; - gap: 8dp; + gap: var(--space-sm); } #disc-status[status=good] { - color: #D8F999; + color: var(--color-ready); } #disc-status[status=bad] { - color: #FFC9C9; + color: var(--color-disc-error); } #disc-status[status=verifying] { - color: #FFFFFF; + color: var(--color-white); } #disc-status[status=mismatch] { - color: #FFD6A7; + color: var(--color-disc-mismatch); } #disc-status[status=unknown] { - color: rgba(224, 219, 200, 65%); + color: rgba(var(--color-text-rgb), 65%); } #disc-status[status=pending] { - color: #FEE685; + color: var(--color-prelaunch-accent); } #disc-status icon { display: none; width: 24dp; height: 24dp; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; - font-size: 24dp; + font-size: var(--font-size-4xl); } #disc-status[status] icon { @@ -337,24 +348,24 @@ body.mirrored version-info { } #disc-version { - font-size: 20dp; + font-size: var(--font-size-2xl); } -.update { +update-status { display: none; - color: #A6A09B; + color: var(--color-prelaunch-muted); align-items: center; justify-content: flex-end; - gap: 8dp; - font-size: 20dp; + gap: var(--space-sm); + font-size: var(--font-size-2xl); } -.update[state=checking], -.update[state=failed] { +update-status[state=checking], +update-status[state=failed] { display: block; } -.update[state=available] { +update-status[state=available] { display: flex; } @@ -364,33 +375,33 @@ body.mirrored version-info { padding: 0dp; border-width: 0dp; background-color: transparent; - color: #D8F999; + color: var(--color-ready); cursor: pointer; text-transform: uppercase; font-weight: bold; - decorator: horizontal-gradient(#00000000 #00000000); + decorator: var(--menu-button-decorator); } -.update[state=available] #update-download { +update-status[state=available] #update-download { display: flex; align-items: center; - gap: 2dp; + gap: var(--space-2xs); } #update-download icon { display: block; width: 18dp; height: 18dp; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; decorator: text("" center center); } -.detail { - color: #A6A09B; +disc-version { + color: var(--color-prelaunch-muted); } -body.mirrored .update { +body.mirrored update-status { justify-content: flex-start; } @@ -436,12 +447,20 @@ body.animate-in .intro-item { /* Mobile layout */ @media (max-height: 640dp) { - .gradient { - decorator: horizontal-gradient(#00000000 #000000FF); + body { + --menu-button-decorator-hover: horizontal-gradient(#FEE68500 #FEE685FF); } - body.mirrored .gradient { - decorator: horizontal-gradient(#000000FF #00000000); + body.mirrored { + --menu-button-decorator-hover: horizontal-gradient(#FEE685FF #FEE68500); + } + + prelaunch-gradient { + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-black-rgb), 100%)); + } + + body.mirrored prelaunch-gradient { + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 100%) rgba(var(--color-black-rgb), 0%)); } menu { @@ -453,7 +472,7 @@ body.animate-in .intro-item { flex-direction: row; align-items: center; justify-content: space-between; - gap: 16dp; + gap: var(--space-lg); } body.mirrored menu { @@ -466,7 +485,7 @@ body.animate-in .intro-item { flex: 1 1 0; min-width: 0; max-width: 48%; - margin-left: 32dp; + margin-left: var(--space-2xl); } body.mirrored hero { @@ -490,11 +509,6 @@ body.animate-in .intro-item { 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; } @@ -503,11 +517,6 @@ body.animate-in .intro-item { text-align: left; } - body.mirrored #menu-list button:hover, - body.mirrored #menu-list button:focus-visible { - decorator: horizontal-gradient(#FEE685FF #FEE68500); - } - eyebrow { display: none; } @@ -518,8 +527,8 @@ body.animate-in .intro-item { bottom: 32dp; top: auto; text-align: right; - font-size: 16dp; - gap: 8dp; + font-size: var(--font-size-md); + gap: var(--space-sm); } #disc-status { @@ -527,11 +536,11 @@ body.animate-in .intro-item { } #disc-status icon { - font-size: 20dp; + font-size: var(--font-size-2xl); } #disc-version { - font-size: 16dp; + font-size: var(--font-size-md); } version-info { @@ -540,12 +549,12 @@ body.animate-in .intro-item { bottom: auto; top: 32dp; text-align: right; - font-size: 16dp; - gap: 8dp; + font-size: var(--font-size-md); + gap: var(--space-sm); } - .update { - font-size: 16dp; + update-status { + font-size: var(--font-size-md); } body.mirrored disc-info { diff --git a/res/rml/tabbing.rcss b/res/rml/tabbing.rcss index 8f42dd84d5..99f87bd8c3 100644 --- a/res/rml/tabbing.rcss +++ b/res/rml/tabbing.rcss @@ -18,17 +18,18 @@ tab-bar scrollbarhorizontal sliderbar { tab-bar tab { flex: 0 0 auto; - padding: 0 24dp; - line-height: 64dp; + padding: 0 var(--space-xl); + line-height: var(--toolbar-height); white-space: nowrap; - decorator: vertical-gradient(#c2a42d00 #c2a42d00); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0%) rgba(var(--color-accent-rgb), 0%)); 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; + border-bottom-width: 4dp; + border-bottom-color: var(--color-accent); font-effect: glow(0dp 4dp 0dp 4dp black); } @@ -36,17 +37,21 @@ tab-bar tab:focus-visible, tab-bar tab:hover { opacity: 1; font-effect: glow(0dp 4dp 0dp 4dp black); - decorator: vertical-gradient(#c2a42d00 #c2a42d26); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0) rgba(var(--color-accent-rgb), 38)); } tab-bar tab:active { - decorator: vertical-gradient(#c2a42d10 #c2a42d40); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 16) rgba(var(--color-accent-rgb), 64)); +} + +tab-bar update-badge { + font-effect: none; } tab-bar[closable] tab-end-spacer { display: block; - flex: 0 0 64dp; - width: 64dp; + flex: 0 0 var(--toolbar-height); + width: var(--toolbar-height); pointer-events: none; } @@ -56,13 +61,13 @@ window > close { position: fixed; top: 8dp; right: 8dp; - z-index: 1; + z-index: 2; width: 48dp; height: 48dp; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; - font-size: 24dp; - color: rgba(224, 219, 200, 70%); + font-size: var(--font-size-4xl); + color: rgba(var(--color-text-rgb), 70%); backdrop-filter: blur(2dp); border-radius: 6dp; decorator: text("" center center); @@ -74,8 +79,8 @@ tab-bar[closable] close:hover, tab-bar[closable] close:focus-visible, window > close:hover, window > close:focus-visible { - color: #fff; - background-color: rgba(194, 164, 45, 24%); + color: var(--color-white); + background-color: rgba(var(--color-accent-rgb), 24%); } window > close { @@ -85,6 +90,6 @@ window > close { tab-bar[closable] close:active, window > close:active { - color: #fff; - background-color: rgba(194, 164, 45, 40%); + color: var(--color-white); + background-color: rgba(var(--color-accent-rgb), 40%); } diff --git a/res/rml/theme.rcss b/res/rml/theme.rcss new file mode 100644 index 0000000000..8d49f32a9c --- /dev/null +++ b/res/rml/theme.rcss @@ -0,0 +1,80 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + --font-family-body: Fira Sans; + --font-family-heading: Fira Sans Condensed; + --font-family-monospace: Noto Mono; + --font-family-icons: Material Symbols Rounded; + --font-family-display: Alegreya SC; + + --font-size-3xs: 11dp; + --font-size-2xs: 12dp; + --font-size-xs: 13dp; + --font-size-sm: 14dp; + --font-size-base: 15dp; + --font-size-md: 16dp; + --font-size-lg: 17dp; + --font-size-xl: 18dp; + --font-size-2xl: 20dp; + --font-size-3xl: 22dp; + --font-size-4xl: 24dp; + --font-size-5xl: 28dp; + --font-size-6xl: 32dp; + + --space-2xs: 2dp; + --space-xs: 4dp; + --space-sm: 8dp; + --space-md: 12dp; + --space-lg: 16dp; + --space-xl: 24dp; + --space-2xl: 32dp; + + --radius-small: 4dp; + --radius-control: 7dp; + --radius-panel: 8dp; + --radius-window: 14dp; + + --toolbar-height: 64dp; + --toolbar-hidden-offset: -64dp; + + --color-text-rgb: 224, 219, 200; + --color-text: rgb(var(--color-text-rgb)); + --color-accent-rgb: 194, 164, 45; + --color-accent: rgb(var(--color-accent-rgb)); + --color-border-rgb: 146, 135, 91; + --color-border: rgb(var(--color-border-rgb)); + --color-surface-rgb: 21, 22, 16; + --color-interactive-rgb: 204, 184, 119; + --color-control-rgb: 17, 16, 10; + --color-neutral-rgb: 217, 217, 217; + --color-neutral: rgb(var(--color-neutral-rgb)); + --color-white-rgb: 255, 255, 255; + --color-white: rgb(var(--color-white-rgb)); + --color-black-rgb: 0, 0, 0; + --color-black: rgb(var(--color-black-rgb)); + + --color-success-rgb: 68, 204, 85; + --color-success: rgb(var(--color-success-rgb)); + --color-info-rgb: 111, 183, 239; + --color-info: rgb(var(--color-info-rgb)); + --color-warning-rgb: 255, 168, 38; + --color-warning: rgb(var(--color-warning-rgb)); + --color-error-rgb: 204, 68, 68; + --color-error: rgb(var(--color-error-rgb)); + + --color-progress-done: #44AA22; + --color-progress-ongoing: #2255BB; + --color-danger-border: #852221; + --color-danger-heading: #B3261E; + + --button-color: var(--color-text); + --button-background: rgba(var(--color-control-rgb), 20%); + --button-background-hover: rgba(var(--color-interactive-rgb), 20%); + --button-background-selected: rgba(var(--color-interactive-rgb), 40%); + --button-background-active: rgba(var(--color-interactive-rgb), 40%); + + --toast-border-color: var(--color-border); + --toast-heading-color: var(--color-border); +} diff --git a/res/rml/touch_controls.rcss b/res/rml/touch_controls.rcss index 4c8057d6c0..85320315bf 100644 --- a/res/rml/touch_controls.rcss +++ b/res/rml/touch_controls.rcss @@ -1,16 +1,28 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { + --color-oil-border: rgba(42, 32, 18, 82%); + --color-oil-background: rgba(18, 14, 10, 70%); + --color-oil-fill: rgb(255, 232, 74); + --color-button-a: rgba(34, 112, 123, 62%); + --color-button-b: rgba(161, 61, 66, 58%); + --color-button-x: rgba(83, 115, 151, 56%); + --color-button-y: rgba(113, 91, 150, 54%); + --color-stick-background: rgba(18, 20, 24, 35%); + --color-stick-knob: rgba(238, 236, 226, 55%); + + --button-color: rgba(248, 244, 232, 90%); + --button-background: rgba(22, 24, 28, 48%); + --button-background-active: rgba(63, 78, 90, 68%); + --button-border-color: rgba(var(--color-white-rgb), 22%); + --button-border-color-active: rgba(var(--color-white-rgb), 48%); + width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - color: rgba(248, 244, 232, 90%); + color: var(--button-color); z-index: 1; filter: opacity(0); transition: filter 0.2s linear-in-out; @@ -30,12 +42,11 @@ button { justify-content: center; decorator: none; padding: 0; - border: 1dp rgba(255, 255, 255, 22%); - background-color: rgba(22, 24, 28, 48%); - color: rgba(248, 244, 232, 90%); + border-width: 1dp; + border-color: var(--button-border-color); + background-color: var(--button-background); + color: var(--button-color); text-align: center; - /* backdrop-filter: blur(7dp); */ - /* box-shadow: 0 6dp 18dp rgba(0, 0, 0, 28%); */ transform-origin: center; transition: background-color border-color filter transform 0.08s linear-in-out, opacity 0.2s linear-in-out; @@ -43,8 +54,8 @@ button { button.pressed, button.active { - background-color: rgba(63, 78, 90, 68%); - border-color: rgba(255, 255, 255, 48%); + background-color: var(--button-background-active); + border-color: var(--button-border-color-active); filter: brightness(1.18); } @@ -68,9 +79,9 @@ button icon { button icon glyph { display: block; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; - font-size: 24dp; + font-size: var(--font-size-4xl); line-height: 1; } @@ -92,9 +103,9 @@ button icon glyph { position: absolute; } -.trigger-l.active { - background-color: rgba(57, 116, 133, 74%); - border-color: rgba(128, 222, 234, 72%); +.trigger-l { + --button-background-active: rgba(57, 116, 133, 74%); + --button-border-color-active: rgba(128, 222, 234, 72%); } .trigger, @@ -103,12 +114,14 @@ button icon glyph { } .trigger { - font-size: 22dp; + font-size: var(--font-size-3xl); } .button-z { - background-color: rgba(118, 79, 158, 58%); - border-color: rgba(203, 170, 255, 36%); + --button-background: rgba(118, 79, 158, 58%); + --button-background-active: rgba(139, 91, 187, 82%); + --button-border-color: rgba(203, 170, 255, 36%); + --button-border-color-active: rgba(220, 194, 255, 70%); } .midna-icon { @@ -121,7 +134,7 @@ button icon glyph { .button-z.has-icon span, .face.has-item span { position: absolute; - font-size: 13dp; + font-size: var(--font-size-xs); line-height: 1; } @@ -130,20 +143,14 @@ button icon glyph { bottom: 7dp; } -.button-z.pressed { - background-color: rgba(139, 91, 187, 82%); - border-color: rgba(220, 194, 255, 70%); -} - action-bar { position: absolute; display: flex; align-items: center; - border: 1dp rgba(255, 255, 255, 22%); + border-width: 1dp; + border-color: var(--button-border-color); border-radius: 23dp; - background-color: rgba(22, 24, 28, 48%); - /* backdrop-filter: blur(7dp); */ - /* box-shadow: 0 -6dp 18dp rgba(0, 0, 0, 28%); */ + background-color: var(--button-background); overflow: hidden; opacity: 1; transform-origin: center; @@ -175,7 +182,7 @@ action-bar:hidden separator { } .utility.pressed { - background-color: rgba(63, 78, 90, 68%); + background-color: var(--button-background-active); } .utility.pressed, @@ -185,7 +192,7 @@ action-bar:hidden separator { .skip { z-index: 1; - border-color: rgba(255, 255, 255, 36%); + border-color: rgba(var(--color-white-rgb), 36%); } separator { @@ -193,7 +200,7 @@ separator { flex: 0 0 1dp; width: 1dp; height: 24dp; - background-color: rgba(255, 255, 255, 18%); + background-color: rgba(var(--color-white-rgb), 18%); opacity: 1; transition: opacity 0.2s linear-in-out; } @@ -201,7 +208,7 @@ separator { .face { position: absolute; border-radius: 29dp; - font-size: 24dp; + font-size: var(--font-size-4xl); overflow: visible; } @@ -219,10 +226,10 @@ separator { min-width: 17dp; height: 15dp; padding: 1dp 3dp; - border-radius: 7dp; - background-color: rgba(0, 0, 0, 52%); - color: rgba(255, 255, 255, 92%); - font-size: 12dp; + border-radius: var(--radius-control); + background-color: rgba(var(--color-black-rgb), 52%); + color: rgba(var(--color-white-rgb), 92%); + font-size: var(--font-size-2xs); line-height: 13dp; text-align: center; } @@ -233,11 +240,10 @@ separator { bottom: -5dp; width: 34dp; height: 8dp; - padding: 2dp; - border: 1dp rgba(42, 32, 18, 82%); - border-radius: 4dp; - background-color: rgba(18, 14, 10, 70%); - /* box-shadow: 0 2dp 6dp rgba(0, 0, 0, 35%); */ + padding: var(--space-2xs); + border: 1dp var(--color-oil-border); + border-radius: var(--radius-small); + background-color: var(--color-oil-background); } oil-fill { @@ -245,31 +251,31 @@ oil-fill { width: 0%; height: 100%; border-radius: 2dp; - background-color: rgb(255, 232, 74); + background-color: var(--color-oil-fill); } .face.has-item span { right: 6dp; bottom: 6dp; - color: rgba(255, 255, 255, 88%); + color: rgba(var(--color-white-rgb), 88%); } .face.a { border-radius: 37dp; font-size: 31dp; - background-color: rgba(34, 112, 123, 62%); + background-color: var(--color-button-a); } .face.b { - background-color: rgba(161, 61, 66, 58%); + background-color: var(--color-button-b); } .face.x { - background-color: rgba(83, 115, 151, 56%); + background-color: var(--color-button-x); } .face.y { - background-color: rgba(113, 91, 150, 54%); + background-color: var(--color-button-y); } button.control.docked-top, @@ -306,10 +312,9 @@ touch-stick { width: 124dp; height: 124dp; border-radius: 62dp; - background-color: rgba(18, 20, 24, 35%); - border: 1dp rgba(255, 255, 255, 20%); - /* backdrop-filter: blur(7dp); */ - /* box-shadow: 0 8dp 24dp rgba(0, 0, 0, 24%); */ + background-color: var(--color-stick-background); + border-width: 1dp; + border-color: rgba(var(--color-white-rgb), 20%); opacity: 0; pointer-events: none; transition: opacity 0.18s linear-in-out; @@ -326,7 +331,8 @@ stick-ring { width: 88dp; height: 88dp; border-radius: 44dp; - border: 1dp rgba(255, 255, 255, 18%); + border-width: 1dp; + border-color: rgba(var(--color-white-rgb), 18%); } stick-knob { @@ -334,6 +340,7 @@ stick-knob { width: 48dp; height: 48dp; border-radius: 24dp; - background-color: rgba(238, 236, 226, 55%); - border: 1dp rgba(255, 255, 255, 45%); + background-color: var(--color-stick-knob); + border-width: 1dp; + border-color: rgba(var(--color-white-rgb), 45%); } diff --git a/res/rml/touch_controls_editor.rcss b/res/rml/touch_controls_editor.rcss index 2ca99d935a..dbf0ae74e4 100644 --- a/res/rml/touch_controls_editor.rcss +++ b/res/rml/touch_controls_editor.rcss @@ -1,5 +1,10 @@ body.touch-editor { - background-color: rgba(4, 6, 8, 34%); + --color-editor-backdrop: rgba(4, 6, 8, 34%); + --color-editor-handle: rgba(34, 37, 42, 86%); + --color-editor-accent-rgb: 255, 232, 128; + --color-editor-highlight-rgb: 255, 244, 190; + + background-color: var(--color-editor-backdrop); z-index: 8; } @@ -14,7 +19,7 @@ body.touch-editor .control:hover, body.touch-editor action-bar:hover, body.touch-editor .control.editor-selected, body.touch-editor action-bar.editor-selected { - border-color: rgba(255, 232, 128, 80%); + border-color: rgba(var(--color-editor-accent-rgb), 80%); filter: brightness(1.15); } @@ -27,8 +32,9 @@ selection-frame { display: none; position: absolute; z-index: 20; - border: 2dp rgba(255, 232, 128, 88%); - background-color: rgba(255, 232, 128, 7%); + border-width: 2dp; + border-color: rgba(var(--color-editor-accent-rgb), 88%); + background-color: rgba(var(--color-editor-accent-rgb), 7%); pointer-events: none; } @@ -41,9 +47,10 @@ resize-handle { position: absolute; width: 22dp; height: 22dp; - border: 2dp rgba(255, 244, 190, 96%); + border-width: 2dp; + border-color: rgba(var(--color-editor-highlight-rgb), 96%); border-radius: 11dp; - background-color: rgba(34, 37, 42, 86%); + background-color: var(--color-editor-handle); pointer-events: auto; } @@ -90,6 +97,12 @@ resize-handle.corner.bottom { } editor-toolbar { + --button-background: rgba(17, 19, 24, 88%); + --button-background-hover: rgba(78, 85, 96, 92%); + --button-border-color: rgba(var(--color-white-rgb), 26%); + --button-border-color-hover: rgba(var(--color-editor-highlight-rgb), 92%); + --button-color: rgba(255, 250, 232, 94%); + display: flex; position: absolute; left: 24dp; @@ -98,7 +111,7 @@ editor-toolbar { z-index: 30; height: 48dp; margin-top: -24dp; - gap: 8dp; + gap: var(--space-sm); justify-content: center; pointer-events: auto; } @@ -108,12 +121,13 @@ editor-toolbar button.editor-command { min-width: 96dp; height: 48dp; padding: 0 14dp; - border-radius: 8dp; - border: 1dp rgba(255, 255, 255, 26%); - background-color: rgba(17, 19, 24, 88%); - color: rgba(255, 250, 232, 94%); - font-family: "Fira Sans"; - font-size: 18dp; + border-radius: var(--radius-panel); + border-width: 1dp; + border-color: var(--button-border-color); + background-color: var(--button-background); + color: var(--button-color); + font-family: var(--font-family-body); + font-size: var(--font-size-xl); line-height: 48dp; opacity: 1; cursor: pointer; @@ -127,12 +141,12 @@ editor-toolbar button.editor-command span { } editor-toolbar button.editor-command.primary { - border-color: rgba(255, 232, 128, 70%); - background-color: rgba(96, 82, 38, 90%); + --button-border-color: rgba(var(--color-editor-accent-rgb), 70%); + --button-background: rgba(96, 82, 38, 90%); } editor-toolbar button.editor-command:hover, editor-toolbar button.editor-command:focus-visible { - border-color: rgba(255, 244, 190, 92%); - background-color: rgba(78, 85, 96, 92%); + border-color: var(--button-border-color-hover); + background-color: var(--button-background-hover); } diff --git a/res/rml/tuner.rcss b/res/rml/tuner.rcss index 86dd2043f6..5e0d96e855 100644 --- a/res/rml/tuner.rcss +++ b/res/rml/tuner.rcss @@ -1,90 +1,87 @@ -*, *: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; + font-family: var(--font-family-heading); + font-size: var(--font-size-4xl); + color: var(--color-white); display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; } -.tuner-root { +tuner-root { width: 100%; min-height: 45%; display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; - decorator: vertical-gradient(#00000000 #151610F2); + decorator: vertical-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-surface-rgb), 242)); filter: opacity(0); transition: filter 0.2s linear-in-out; } -.tuner-root[open] { +tuner-root[open] { filter: opacity(1); } -.tuner { +graphics-tuner { width: 100%; max-width: 1216dp; margin-left: auto; margin-right: auto; display: flex; flex-direction: column; - gap: 24dp; + gap: var(--space-xl); padding: 48dp 64dp; } @media (max-height: 800dp) { - .tuner-root { + tuner-root { min-height: 38%; } - .tuner { - gap: 16dp; - padding: 32dp 48dp; + graphics-tuner { + gap: var(--space-lg); + padding: var(--space-2xl) 48dp; } } -.header { +tuner-header { display: flex; justify-content: space-between; align-items: center; - gap: 24dp; + gap: var(--space-xl); } -.carousel-container { +carousel-container { flex: 1 1 auto; display: flex; justify-content: flex-end; min-width: 0; } -.description { - font-size: 18dp; +tuner-description { + font-size: var(--font-size-xl); line-height: 22dp; - color: rgba(255, 255, 255, 50%); + color: rgba(var(--color-white-rgb), 50%); } -.divider { +tuner-divider { margin: 1dp 0; - border-top: 1dp rgba(217, 217, 217, 50%); + border-top-width: 1dp; + border-top-color: rgba(var(--color-neutral-rgb), 50%); } -.footer { +tuner-footer { display: flex; justify-content: space-between; align-items: center; - gap: 24dp; + gap: var(--space-xl); } footer-button { @@ -94,12 +91,12 @@ footer-button { border: 0; padding: 0; background-color: transparent; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - font-size: 20dp; + font-size: var(--font-size-2xl); line-height: 24dp; text-transform: uppercase; - color: #FFFFFF; + color: var(--color-white); opacity: 1; cursor: pointer; } @@ -112,20 +109,20 @@ footer-button.reset { text-align: right; } -.stepped-carousel { +stepped-carousel { display: flex; align-items: center; justify-content: center; - gap: 16dp; + gap: var(--space-lg); width: auto; min-width: 246dp; padding: 0; background-color: transparent; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; } -.stepped-carousel-value { +stepped-carousel-value { line-height: 29dp; min-width: 166dp; text-align: center; @@ -142,6 +139,6 @@ footer-button.reset { background-color: transparent; opacity: 1; cursor: pointer; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; } diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 98b63dfcd9..f820de9735 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -1,17 +1,13 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { display: flex; width: 100%; height: 100%; padding: 64dp; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; font-style: normal; - font-size: 15dp; - color: #E0DBC8; + font-size: var(--font-size-base); + color: var(--color-text); } b { @@ -27,12 +23,13 @@ window { max-width: 1088dp; max-height: 768dp; margin: auto; - border-radius: 14dp; + border-radius: var(--radius-window); overflow: hidden; - border: 2dp #92875B; + border-width: 2dp; + border-color: var(--color-border); backdrop-filter: blur(5dp); box-shadow: 0 0 25dp 5dp; - background-color: rgba(21, 22, 16, 90%); + background-color: rgba(var(--color-surface-rgb), 90%); filter: opacity(0); transform: scale(0.9); transform-origin: center; @@ -61,7 +58,7 @@ window[open] { @media (max-height: 640dp) { body { - padding: 16dp; + padding: var(--space-lg); } window { box-shadow: none; @@ -70,7 +67,7 @@ window[open] { @media (max-width: 768dp) { body { - padding: 16dp; + padding: var(--space-lg); } window.modal { width: 100%; @@ -79,13 +76,14 @@ window[open] { } window tab-bar { - flex: 0 0 64dp; - height: 64dp; - background-color: rgba(217, 217, 217, 10%); - font-family: "Fira Sans Condensed"; + flex: 0 0 var(--toolbar-height); + height: var(--toolbar-height); + background-color: rgba(var(--color-neutral-rgb), 10%); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; - border-bottom: 2dp #92875B; + font-size: var(--font-size-xl); + border-bottom-width: 2dp; + border-bottom-color: var(--color-border); } window tab-bar tab { @@ -106,14 +104,15 @@ window content pane { flex: 1 1 0; min-width: 0; min-height: 0; - padding: 24dp; - gap: 8dp; + padding: var(--space-xl); + gap: var(--space-sm); overflow: hidden auto; - font-size: 20dp; + font-size: var(--font-size-2xl); } window content pane:not(:last-of-type) { - border-right: 1dp #92875B; + border-right-width: 1dp; + border-right-color: var(--color-border); } window content pane > * { @@ -129,7 +128,7 @@ ui-list { } window content pane > ui-list, -.modal-content pane > ui-list { +modal-content pane > ui-list { flex: 1 1 0; min-width: 0; min-height: 0; @@ -146,7 +145,7 @@ ui-list-viewport { ui-list-content { display: flex; flex-flow: column; - gap: 8dp; + gap: var(--space-sm); min-width: 0; } @@ -156,7 +155,7 @@ ui-list-content > button.ui-list-row { ui-list-empty { display: block; - padding: 16dp; + padding: var(--space-lg); text-align: center; opacity: 0.45; } @@ -176,33 +175,33 @@ window content pane > ui-list:last-child { window content pane > ui-list ui-list-content, window content pane > ui-list ui-list-empty { - padding-left: 24dp; - padding-right: 24dp; + padding-left: var(--space-xl); + padding-right: var(--space-xl); } window content pane > ui-list:first-child ui-list-content, window content pane > ui-list:first-child ui-list-empty { - padding-top: 24dp; + padding-top: var(--space-xl); } window content pane > ui-list:last-child ui-list-content, window content pane > ui-list:last-child ui-list-empty { - padding-bottom: 24dp; + padding-bottom: var(--space-xl); } window content pane:last-of-type > div { line-height: 1.625; } -.data-folder-current { +data-folder-current { display: block; - font-size: 16dp; - color: rgba(224, 219, 200, 65%); + font-size: var(--font-size-md); + color: rgba(var(--color-text-rgb), 65%); } scrollbarvertical { width: 8dp; - margin: 4dp 4dp 4dp 0; + margin: var(--space-xs) var(--space-xs) var(--space-xs) 0; } scrollbarvertical sliderarrowdec, @@ -218,14 +217,14 @@ scrollbarvertical slidertrack { scrollbarvertical sliderbar { width: 8dp; min-height: 24dp; - background-color: rgba(224, 219, 200, 45%); + background-color: rgba(var(--color-text-rgb), 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%); + background-color: rgba(var(--color-accent-rgb), 80%); } scrollbarhorizontal { @@ -244,26 +243,27 @@ scrollbarhorizontal sliderbar { height: 0; } -.section-heading { - font-family: "Fira Sans Condensed"; +section-heading { + font-family: var(--font-family-heading); font-weight: bold; text-transform: uppercase; - font-size: 22dp; + font-size: var(--font-size-3xl); opacity: 0.25; } -.section-heading:not(:first-of-type) { - padding-top: 12dp; +section-heading:not(:first-of-type) { + padding-top: var(--space-md); } button { text-align: center; - background-color: rgba(17, 16, 10, 20%); + color: var(--button-color); + background-color: var(--button-background); opacity: 0.9; - padding: 8dp 16dp; - border-radius: 14dp; - box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; - font-size: 20dp; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-window); + box-shadow: rgba(var(--color-border-rgb), 25%) 0 0 0 1dp; + font-size: var(--font-size-2xl); transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; cursor: pointer; focus: auto; @@ -271,19 +271,19 @@ button { button:not(:disabled):hover, button:not(:disabled):focus-visible { - background-color: rgba(204, 184, 119, 20%); - box-shadow: #C2A42D 0 0 0 2dp; + background-color: var(--button-background-hover); + box-shadow: var(--color-accent) 0 0 0 2dp; } button:not(:disabled):selected { opacity: 1; - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-selected); } button:not(:disabled):active { opacity: 1; - background-color: rgba(204, 184, 119, 40%); - box-shadow: #C2A42D 0 0 0 2dp; + background-color: var(--button-background-active); + box-shadow: var(--color-accent) 0 0 0 2dp; } button:disabled { @@ -299,12 +299,13 @@ button.modal-btn { select-button { display: flex; align-items: center; - gap: 8dp; - background-color: rgba(17, 16, 10, 20%); + gap: var(--space-sm); + color: var(--button-color); + background-color: var(--button-background); opacity: 0.9; - padding: 8dp 16dp; - border-radius: 14dp; - box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-window); + box-shadow: rgba(var(--color-border-rgb), 25%) 0 0 0 1dp; transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; cursor: pointer; focus: auto; @@ -312,19 +313,19 @@ select-button { 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; + background-color: var(--button-background-hover); + box-shadow: var(--color-accent) 0 0 0 2dp; } select-button:not(:disabled):selected { opacity: 1; - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-selected); } select-button:not(:disabled):active { opacity: 1; - background-color: rgba(204, 184, 119, 40%); - box-shadow: #C2A42D 0 0 0 2dp; + background-color: var(--button-background-active); + box-shadow: var(--color-accent) 0 0 0 2dp; } select-button:disabled { @@ -333,17 +334,22 @@ select-button:disabled { } select-button key { - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; + font-size: var(--font-size-xl); text-transform: uppercase; - flex: 0 1 auto; + flex: 0 0 auto; + white-space: nowrap; } select-button value { flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; text-align: right; - font-size: 20dp; + font-size: var(--font-size-2xl); } select-button value.modified { @@ -352,7 +358,7 @@ select-button value.modified { select-button input { text-align: right; - font-size: 20dp; + font-size: var(--font-size-2xl); } select-button.group-button icon { @@ -360,8 +366,8 @@ select-button.group-button icon { margin-left: auto; width: 24dp; height: 24dp; - font-size: 24dp; - color: inherit; + font-size: var(--font-size-4xl); + color: var(--button-color); decorator: text("" center center); } @@ -369,10 +375,29 @@ select-button.group-button value { display: none; } +select-button.dropdown-button icon { + display: none; +} + +select-button.dropdown-button dropdown-indicator { + display: block; + flex: 0 0 16dp; + width: 16dp; + height: 16dp; + font-family: var(--font-family-icons); + font-size: 16dp; + font-weight: normal; + line-height: 1; + transition: transform 0.1s linear-in-out; +} + +select-button.dropdown-button.expanded dropdown-indicator { + transform: rotate(180deg); +} + select-button.color-input value { - min-width: 0; - font-family: "Noto Mono"; - font-size: 15dp; + font-family: var(--font-family-monospace); + font-size: var(--font-size-base); } select-button.color-input color-swatch { @@ -380,19 +405,19 @@ select-button.color-input color-swatch { flex: 0 0 48dp; width: 48dp; height: 24dp; - border-radius: 7dp; - box-shadow: rgba(255, 255, 255, 45%) 0 0 0 1dp; + border-radius: var(--radius-control); + box-shadow: rgba(var(--color-white-rgb), 45%) 0 0 0 1dp; } select-button.color-input color-swatch.empty { - background-color: rgba(224, 219, 200, 12%); - decorator: linear-gradient(135deg, rgba(224, 219, 200, 0) 45%, rgba(194, 164, 45, 70%) 48%, rgba(194, 164, 45, 70%) 52%, rgba(224, 219, 200, 0) 55%); + background-color: rgba(var(--color-text-rgb), 12%); + decorator: linear-gradient(135deg, rgba(var(--color-text-rgb), 0) 45%, rgba(var(--color-accent-rgb), 70%) 48%, rgba(var(--color-accent-rgb), 70%) 52%, rgba(var(--color-text-rgb), 0) 55%); } icon { width: 1em; height: 1em; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; display: inline-block; vertical-align: middle; @@ -410,6 +435,10 @@ icon.verifying { decorator: text("" center center); } +icon.download { + decorator: text("" center center); +} + icon.celebration { decorator: text("" center center); } @@ -418,73 +447,74 @@ icon.question-mark { decorator: text("" center center); } -.achievement-total { +achievement-total { position: absolute; top: 0; - right: 64dp; - height: 64dp; - line-height: 64dp; - font-family: "Fira Sans Condensed"; + right: var(--toolbar-height); + height: var(--toolbar-height); + line-height: var(--toolbar-height); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 16dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-md); + color: rgba(var(--color-text-rgb), 55%); pointer-events: none; } -.achievement-row { +achievement-row { display: flex; align-items: flex-start; gap: 10dp; - padding: 12dp 0; - border-bottom: 1dp rgba(146, 135, 91, 30%); + padding: var(--space-md) 0; + border-bottom-width: 1dp; + border-bottom-color: rgba(var(--color-border-rgb), 30%); } -.achievement-info { +achievement-info { display: block; flex: 1 1 0; min-width: 0; } -.achievement-header { +achievement-header { display: flex; align-items: center; } -.achievement-name { +achievement-name { flex: 1; font-weight: bold; } -.achievement-name.unlocked { - color: #ffa826; +achievement-name.unlocked { + color: var(--color-warning); } -.achievement-badge { - font-size: 14dp; +achievement-badge { + font-size: var(--font-size-sm); opacity: 0.7; } -.achievement-badge.unlocked { - color: #44cc55; +achievement-badge.unlocked { + color: var(--color-success); opacity: 1; } -.achievement-badge.locked { - color: #cc4444; +achievement-badge.locked { + color: var(--color-error); opacity: 1; } .achievement-desc { display: block; - color: rgba(224, 219, 200, 55%); - font-size: 16dp; - margin: 4dp 0 0 0; + color: rgba(var(--color-text-rgb), 55%); + font-size: var(--font-size-md); + margin: var(--space-xs) 0 0 0; } -.achievement-progress { +achievement-progress { display: block; - font-size: 13dp; - color: rgba(224, 219, 200, 45%); + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 45%); } progress { @@ -492,32 +522,32 @@ progress { width: 100%; height: 6dp; border-radius: 3dp; - background-color: rgba(255, 255, 255, 10%); - margin: 6dp 0 2dp 0; + background-color: rgba(var(--color-white-rgb), 10%); + margin: 6dp 0 var(--space-2xs) 0; } progress fill { - background-color: rgba(194, 164, 45, 80%); + background-color: rgba(var(--color-accent-rgb), 80%); border-radius: 3dp; } progress.progress-done fill { - background-color: #44aa22; + background-color: var(--color-progress-done); } progress.progress-ongoing fill { - background-color: #2255bb; + background-color: var(--color-progress-ongoing); } button.achievement-clear { flex: 0 0 auto; align-self: center; - font-size: 14dp; - padding: 2dp 8dp; + font-size: var(--font-size-sm); + padding: var(--space-2xs) var(--space-sm); opacity: 0.45; } -.preset-grid { +preset-grid { display: flex; flex-direction: row; gap: 20dp; @@ -526,24 +556,24 @@ button.achievement-clear { width: 100%; } -.preset-col { +preset-option { display: flex; flex-flow: column; - gap: 12dp; + gap: var(--space-md); flex: 1 1 0; } -.preset-desc { +preset-description { display: block; - font-size: 16dp; + font-size: var(--font-size-md); text-align: center; } -.modal-dialog { +modal-dialog { display: flex; flex-direction: column; align-items: flex-start; - padding: 24dp; + padding: var(--space-xl); gap: 20dp; flex: 0 1 auto; min-height: 0; @@ -553,55 +583,55 @@ button.achievement-clear { } window.modal.danger { - border: 2dp #852221; + border: 2dp var(--color-danger-border); } -.modal-header { +modal-header { display: flex; flex-direction: row; align-items: center; justify-content: space-between; width: 100%; flex: 0 0 auto; - gap: 16dp; + gap: var(--space-lg); } -.modal-header icon { - font-size: 24dp; - color: #92875B; +modal-header icon { + font-size: var(--font-size-4xl); + color: var(--color-border); } -.modal-title { +modal-title { display: block; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; text-transform: uppercase; - font-size: 18dp; - color: #92875B; + font-size: var(--font-size-xl); + color: var(--color-border); flex: 1 1 auto; } -window.modal.danger .modal-title, -window.modal.danger .modal-header icon { - color: #B3261E; +window.modal.danger modal-title, +window.modal.danger modal-header icon { + color: var(--color-danger-heading); } -.modal-body { +modal-body { display: block; width: 100%; flex: 0 0 auto; min-width: 0; - font-size: 20dp; - color: #FFFFFF; + font-size: var(--font-size-2xl); + color: var(--color-white); font-weight: normal; } -.modal-body span.tip { - font-size: 14dp; - color: #92875B; +modal-body modal-tip { + font-size: var(--font-size-sm); + color: var(--color-border); } -.modal-content { +modal-content { display: none; width: 100%; flex: 1 1 auto; @@ -609,88 +639,363 @@ window.modal.danger .modal-header icon { overflow: hidden; } -.modal-content.active { +modal-content.active { display: flex; flex-direction: column; } -.modal-content pane { +modal-content pane { display: flex; flex: 1 1 auto; flex-direction: column; min-height: 0; width: 100%; - gap: 8dp; + gap: var(--space-sm); overflow: hidden auto; } -.modal-content pane > * { +modal-content pane > * { flex: 0 0 auto; } -.verification-progress { +window.modal.drop-install { + max-height: 720dp; +} + +window.modal.drop-install package-row { + padding-right: 0; +} + +package-row { + display: flex; + flex-direction: row; + align-items: flex-start; + position: relative; + width: 100%; + gap: 10dp; + padding: var(--space-md) 0; + border-bottom-width: 1dp; + border-bottom-color: rgba(var(--color-border-rgb), 30%); +} + +package-row:last-child { + border-bottom-width: 0; +} + +package-row > mod-icon { + display: none; + flex: 0 0 36dp; + width: 36dp; + height: 36dp; + margin-top: var(--space-2xs); + overflow: hidden; + border-radius: var(--radius-panel); + background-color: rgba(var(--color-control-rgb), 45%); + color: rgba(var(--color-text-rgb), 45%); + font-family: var(--font-family-icons); + font-size: var(--font-size-4xl); + decorator: text("" center center); +} + +package-row > mod-icon.visible { + display: block; +} + +package-row > mod-icon.has-image { + background-color: transparent; +} + +package-row.paused > mod-icon, +package-row.retrying > mod-icon, +package-row.failed > mod-icon { + filter: grayscale(1); + opacity: 0.55; +} + +package-row > section { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-width: 0; + margin: 0; + padding: 0; +} + +package-row > section > header { + display: flex; + width: 100%; + gap: var(--space-sm); + align-items: center; + margin: 0; + padding: 0 0 6dp 0; +} + +package-row h3 { + display: flex; + align-items: baseline; + flex: 1 1 auto; + min-width: 0; + gap: var(--space-xs); + margin: 0; + padding: 0; + overflow: hidden; +} + +package-row h3 > span { + display: block; + flex: 0 1 auto; + min-width: 0; + font-weight: bold; + color: var(--color-white); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +package-row h3 > small { + display: block; + flex: 0 0 auto; + font-size: var(--font-size-xs); + font-weight: normal; + color: rgba(var(--color-text-rgb), 50%); +} + +package-row header > small { + display: block; + flex: 0 0 auto; + font-size: var(--font-size-sm); + color: rgba(var(--color-text-rgb), 60%); +} + +package-row.downloading header > small { + color: var(--color-accent); +} + +package-row.retrying header > small { + color: var(--color-warning); +} + +package-row.failed header > small, +package-row.failed footer > small { + color: var(--color-error); +} + +package-row.installed header > small, +package-row.installed footer > small { + color: var(--color-success); +} + +package-row progress { + width: 100%; + height: 6dp; + margin: 0 0 var(--space-sm) 0; + border-radius: 3dp; +} + +package-row.failed progress fill { + background-color: var(--color-error); +} + +package-row.paused progress fill { + background-color: rgba(var(--color-text-rgb), 35%); +} + +package-row.retrying progress fill { + background-color: rgba(var(--color-warning-rgb), 55%); +} + +package-row.installed progress fill { + background-color: var(--color-success); +} + +package-row footer { + display: flex; + align-items: flex-start; + width: 100%; + min-width: 0; + gap: var(--space-sm); + margin: 0; + padding: 0; +} + +package-row footer > small { + display: block; + flex: 1 1 0; + min-width: 0; + font-size: var(--font-size-xs); + line-height: 1; + color: rgba(var(--color-text-rgb), 45%); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +package-row nav { + display: flex; + flex: 0 0 auto; + gap: 6dp; + margin: 0; + padding: 0; +} + +package-row nav > button.icon-action { + align-items: center; + justify-content: center; + flex: 0 0 30dp; + width: 30dp; + min-width: 30dp; + height: 26dp; + padding: 0; + border-radius: 14dp; + font-size: var(--font-size-md); +} + +package-row nav > button.icon-action icon { + flex: 0 0 var(--font-size-md); + width: var(--font-size-md); + height: var(--font-size-md); + font-size: var(--font-size-md); + line-height: 1; +} + +package-row.installed { + align-items: center; +} + +package-row.installed > mod-icon { + margin-top: 0; +} + +package-row.installed > section { + padding-right: 38dp; +} + +package-row.installed header > small { + display: none; +} + +package-row.installed nav { + position: absolute; + top: 17dp; + right: 0; +} + +verification-progress { display: flex; flex-direction: column; gap: 10dp; width: 100%; } -.verification-file { +verification-file { display: block; - font-size: 17dp; - color: #FFFFFF; + font-size: var(--font-size-lg); + color: var(--color-white); } progress.verification-progress-bar { height: 8dp; - margin: 2dp 0 0 0; + margin: var(--space-2xs) 0 0 0; } -.verification-detail { +verification-detail { display: block; - font-size: 14dp; - color: rgba(224, 219, 200, 65%); + font-size: var(--font-size-sm); + color: rgba(var(--color-text-rgb), 65%); } -.modal-actions { +modal-actions { display: flex; flex-direction: row; flex-wrap: nowrap; align-items: stretch; - gap: 12dp; + gap: var(--space-md); width: 100%; flex: 0 0 auto; - padding-top: 4dp; + padding-top: var(--space-xs); } -.modal-actions-vertical { +modal-actions.vertical { flex-direction: column; align-items: stretch; } -.modal-actions-vertical button.modal-btn { +modal-actions.vertical button.modal-btn { flex: 0 0 auto; width: 100%; } @media (max-height: 640dp) { - .modal-dialog { - padding: 16dp; - gap: 12dp; + modal-dialog { + padding: var(--space-lg); + gap: var(--space-md); } - .modal-body { - font-size: 17dp; + modal-body { + font-size: var(--font-size-lg); } } @media (max-width: 640dp) { - .modal-actions { + modal-actions { flex-direction: column; } - .modal-actions button.modal-btn { + modal-actions button.modal-btn { flex: 0 0 auto; width: 100%; } } + +ui-row { + display: flex; + flex-flow: row; + align-items: center; + gap: var(--space-sm); + min-width: 0; + focus: none; +} + +ui-row > * { + flex: 0 1 auto; + min-width: 0; +} + +button.icon-button { + display: flex; + align-items: center; + justify-content: center; + width: 44dp; + height: 44dp; + padding: var(--space-sm); + box-sizing: border-box; + flex-shrink: 0; + font-size: var(--font-size-5xl); +} + +button.icon-button icon { + display: block; + flex-shrink: 0; + line-height: 1; + pointer-events: none; +} + +ui-tooltip { + display: none; + position: absolute; + z-index: 1000; + max-width: 240dp; + padding: 6dp 10dp; + border: 1dp var(--color-border); + border-radius: var(--radius-panel); + background-color: rgba(var(--color-surface-rgb), 96%); + color: var(--color-text); + font-size: var(--font-size-md); + word-break: break-word; + pointer-events: none; + focus: none; +} + +ui-tooltip.visible { + display: block; +} diff --git a/sdk/include/mods/api.h b/sdk/include/mods/api.h index 3fde6c9f6a..3015130ae3 100644 --- a/sdk/include/mods/api.h +++ b/sdk/include/mods/api.h @@ -42,6 +42,8 @@ extern "C" { #define MOD_ABI_VERSION 1u #define MOD_ERROR_MESSAGE_SIZE 512u +#define DUSKLIGHT_SERVICE_ID_PREFIX "dev.twilitrealm.dusklight." + typedef struct ModContext ModContext; typedef enum ModResult { diff --git a/sdk/include/mods/meta.hpp b/sdk/include/mods/meta.hpp index edf226a266..6028a3df9c 100644 --- a/sdk/include/mods/meta.hpp +++ b/sdk/include/mods/meta.hpp @@ -13,11 +13,15 @@ * modmeta records. Each IMPORT_SERVICE/EXPORT_SERVICE/DEFINE_HOOK use places one * constant-initialized record object in the metadata section. */ +#if defined(_MSVC_LANG) && !defined(__clang__) +#define MOD_META_NO_ASAN +#else #if defined(__has_attribute) && __has_attribute(no_sanitize) #define MOD_META_NO_ASAN __attribute__((no_sanitize("address"))) #else #define MOD_META_NO_ASAN #endif +#endif #if defined(_WIN32) #pragma section("modmeta$a", read, write) diff --git a/sdk/include/mods/svc/actor.h b/sdk/include/mods/svc/actor.h index 5876ddcaf2..0048b70332 100644 --- a/sdk/include/mods/svc/actor.h +++ b/sdk/include/mods/svc/actor.h @@ -3,7 +3,7 @@ #include #include -#define ACTOR_SERVICE_ID "dev.twilitrealm.dusklight.actor" +#define ACTOR_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "actor" #define ACTOR_SERVICE_MAJOR 1u #define ACTOR_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/audio_res.h b/sdk/include/mods/svc/audio_res.h new file mode 100644 index 0000000000..f225fef1e3 --- /dev/null +++ b/sdk/include/mods/svc/audio_res.h @@ -0,0 +1,434 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#define AUDIO_RES_SERVICE_ID "dev.twilitrealm.dusklight.audio_res" +#define AUDIO_RES_SERVICE_MAJOR 1u +#define AUDIO_RES_SERVICE_MINOR 0u + +/* + * Defines APIs for replacing and adding audio resources. + * + * TP's audio engine (JAudioV2) is extremely complex. For more of an overview through its + * functionality, please see docs/jaudio.md in the repo. + */ + +/* + * WSYS structs + */ + +/** + * "Default" key for wave definitions. Key like musical key, not map keys or whatever. + */ +#define AUDIO_RES_DEFAULT_KEY 0x3C + +/** + * Defines TP's raw wave banks, which can be modified. + */ +typedef enum AudioWaveBank : uint8_t { + AUDIO_WAVE_BANK_SOUND_EFFECTS = 0, + AUDIO_WAVE_BANK_MUSIC_SAMPLES = 1, +} AudioWaveBank; + +/** + * Defines formats for raw sample data the DSP can play back. + */ +typedef enum AudioWaveFormat : uint8_t { + /** + * 16 samples per 9 bytes custom Nintendo ADPCM. + */ + AUDIO_WAVE_FORMAT_ADPCM4 = 0, + + /** + * 16 samples per 5 bytes custom Nintendo ADPCM. + */ + AUDIO_WAVE_FORMAT_ADPCM2 = 1, + + /** + * 1-byte-per-sample simple PCM. + */ + AUDIO_WAVE_FORMAT_PCM8 = 2, + + /** + * 2-byte-per-sample simple PCM. + */ + AUDIO_WAVE_FORMAT_PCM16 = 3, +} AudioWaveFormat; + +/** + * Handle to reference wave replacements/additions by mods. + */ +typedef uint64_t AudioWaveHandle; + +/** + * Data needed to load a raw audio data file. + * + * @see AudioWaveInfo + */ +typedef struct AudioRawWave { + /** + * Format of the raw sample data. + */ + AudioWaveFormat format; + + /** + * Sample rate, in Hertz. + */ + float sample_rate; + + /** + * Last audio sample before ADPCM loop start point. (unused if not looping) + */ + int16_t sample_value_last; + + /** + * Second-to-last audio sample before ADPCM loop start point. (unused if not looping) + */ + int16_t sample_value_penult; +} AudioRawWave; + +/** + * Metadata needed to play a wave. + */ +typedef struct AudioWaveInfo { + /** + * Base key (musical key). Set to AUDIO_RES_DEFAULT_KEY if you don't care. + */ + uint8_t base_key; + + /** + * If true, the sample loops. + */ + bool loop; + + /** + * Sample number where loop resumes when end is reached. + * Does nothing if not looping. + */ + uint32_t loop_start_sample; + + /** + * End of the wave. Stops playback if not looping. + * Automatically clamped to the sample count from the file, if greater. + */ + uint32_t loop_end_sample; + + /** + * If provided, specifies that the passed file is "raw" and only contains sample data. + * This must be used if you want to ship ADPCM samples, as other containers do not support that. + */ + AudioRawWave const* raw_wave; +} AudioWaveInfo; + +/* + * BST structs + */ + +/** + * Game defined categories (groups) for sound effects. + */ +typedef enum SoundEffectCategory : uint8_t { + SE_CATEGORY_SYSTEM_SE, + SE_CATEGORY_PLAYER_VOICE, + SE_CATEGORY_PLAYER_SE, + SE_CATEGORY_FOOTNOTE_SE, + SE_CATEGORY_COLLISION_SE, + SE_CATEGORY_CHARA_VOICE, + SE_CATEGORY_CHARA_SE, + SE_CATEGORY_ENEMY_SE, + SE_CATEGORY_OBJECT_SE, + SE_CATEGORY_ENV_SE +} SoundEffectCategory; + +/** + * Data that can be defined for a sound effect in the sound table. + */ +typedef struct AudioSoundTableEffectInfo { + /** + * Priority relative to other sound effects. Can affect culling and such if + * many effects are playing. + */ + uint8_t priority; + + /** + * Volume multiplier for this sound effect. Clamped to range 0-2. + */ + float volume; + + /** + * Pitch multiplier for this sound effect. + */ + float pitch; + + /** + * If true, the effect is always treated as max priority, regardless of factors like distance. + */ + bool always_max_priority; + + /** + * Don't calculate volume changes by distance. + */ + bool ignore_distance_volume; + + /** + * Don't FX Mix (reverb) changes by distance. + */ + bool ignore_distance_fx_mix; + + /** + * Don't calculate panning (left/right balance). + */ + bool ignore_pan; + + /** + * Don't calculate dolby (front/back balance). + */ + bool ignore_dolby; + + /** + * 0-15 value controlling volume randomization strength. + */ + uint8_t random_volume; + + /** + * 0-15 value controlling pitch randomization strength. + */ + uint8_t random_pitch; + + /** + * 0-15 value controlling Doppler effect strength. + */ + uint8_t doppler_power; + + /** + * Value from 0-15 selecting "volume distance" class. This effectively selects a fixed curve for + * parameters like volume by distance. + */ + uint8_t volume_dist_class; + + /** + * Limit minimum volume of sound (after distance drop-off) to 0.2. + */ + bool clamp_min_volume; + + /** + * Treat this sound as "far away" or "culled" at "max distance." + * Affects things like automatic stopping. + */ + bool cull_at_max_distance; +} AudioSoundTableEffectInfo; + +/** + * Stereo panning parameters for stream channels. + */ +typedef enum StreamPan : uint8_t { + STREAM_PAN_CENTER, + STREAM_PAN_LEFT, + STREAM_PAN_RIGHT, +} StreamPan; + +/** + * Maximum amount of channels a streamed track can have. + */ +#define STREAM_MAX_CHILDREN 6 + +/** + * Data that can be defined for a stream in the sound table. + */ +typedef struct AudioSoundTableStreamInfo { + /** + * Unsure if used. + */ + uint8_t priority; + + /** + * Volume multiplier for this stream. Clamped to range 0-2. + */ + float volume; + + /** + * For each channel in the loaded .ast file, specifies the panning position for said channel. + */ + StreamPan pan_parameters[STREAM_MAX_CHILDREN]; + + /** + * Whether this stream automatically stops on scene change. + */ + bool stop_on_scene_change; +} AudioSoundTableStreamInfo; + +/** + * Handle to reference sound table replacements/additions by mods. + */ +typedef uint64_t AudioSoundTableHandle; + +/** + * Defines APIs for replacing and adding audio resources. + */ +typedef struct AudioResService { + ServiceHeader header; + + /* + * WSYS API + */ + + /** + * Default wave info if none is provided: default key, no loop, not raw audio. + */ + AudioWaveInfo const* default_wave_info; + + /** + * Replace an existing audio wave in the game. This replacement will follow mod order + * prioritization. + * + * The sound effect will remain permanently resident in memory. + * + * Sound effects can be provided as raw samples (if raw_wave data is provided in wave_info), + * WAVE (.wav) file, or OGG Opus (.opus, if Dusklight is compiled with support). Container + * formats are detected based on header, not based on file name. + * + * @param ctx Pointer to your mod's context. + * @param bank Which wave bank to replace a sound effect in. + * @param wave_id ID of the wave to replace. This does *not* directly correlate to sound table + * entries or JAISound values in any way! + * @param file_name Path of the audio file to load in the mod's data, e.g. res/foo.opus. This + * does *not* need to be an overlay file! + * @param wave_info Optional: metadata for the new wave. If not provided will use reasonable + * defaults (@ref default_wave_info). + * @param out_handle Optional: pointer receives the handle for the replacement. This can be used + * with @ref remove_wave. + */ + ModResult (*replace_wave)( + ModContext* ctx, + AudioWaveBank bank, + uint16_t wave_id, + char const* file_name, + AudioWaveInfo const* wave_info, + AudioWaveHandle* out_handle); + + /** + * Add a new audio wave to the game. The service allocates the placed ID for you. + * + * The sound effect will remain permanently resident in memory. + * + * Sound effects can be provided as raw samples (if raw_wave data is provided in wave_info), + * WAVE (.wav) file, or OGG Opus (.opus, if Dusklight is compiled with support). Container + * formats are detected based on header, not based on file name. + * + * @param ctx Pointer to your mod's context. + * @param bank Which wave bank to replace a sound effect in. + * @param file_name Path of the audio file to load in the mod's data, e.g. res/foo.opus. This does *not* need to be an overlay file! + * @param wave_info Optional: metadata for the new wave. If not provided will use reasonable defaults (@ref default_wave_info). + * @param out_handle Optional: pointer receives the handle for the addition. This can be used with @ref remove_wave. + * @param out_wave_id Receives the allocated ID in the wave bank. + */ + ModResult (*add_wave)( + ModContext* ctx, + AudioWaveBank bank, + char const* file_name, + AudioWaveInfo const* wave_info, + AudioWaveHandle* out_handle, + uint16_t* out_wave_id); + + /** + * Remove a wave addition/replacement previously created by this mod. + * + * @param ctx Pointer to your mod's context. + * @param handle The handle identifying which wave to remove. + */ + ModResult (*remove_wave)(ModContext* ctx, AudioWaveHandle handle); + + /* + * BST API + */ + + /** + * Default sound effect info if none is provided: default volume, pitch, medium priority. + */ + AudioSoundTableEffectInfo const* default_effect_info; + + /** + * Replace a sound effect's parameters in the sound table. This replacement will follow mod order + * prioritization. + * + * @param ctx Pointer to your mod's context. + * @param category_id Category of the sound effect. + * @param effect_id ID of the effect. + * @param info Optional: new parameters for the sound. Falls back to @ref default_effect_info if not provided. + * @param out_handle Optional: pointer receives the handle for the replacement. This can be used with @ref remove_sound_table. + */ + ModResult (*replace_sound_table_effect)( + ModContext* ctx, + SoundEffectCategory category_id, + uint16_t effect_id, + AudioSoundTableEffectInfo const* info, + AudioSoundTableHandle* out_handle); + + /** + * Add a sound effect to the sound table. The service allocates the placed ID for you. + * + * @param ctx Pointer to your mod's context. + * @param category_id Category of the sound effect. + * @param info Optional: new parameters for the sound. Falls back to @ref default_effect_info if not provided. + * @param out_handle Optional: pointer receives the handle for the replacement. This can be used with @ref remove_sound_table. + * @param out_effect_id Receives the allocated ID of the sound effect. + */ + ModResult (*add_sound_table_effect)( + ModContext* ctx, + SoundEffectCategory category_id, + AudioSoundTableEffectInfo const* info, + AudioSoundTableHandle* out_handle, + uint16_t* out_effect_id); + + /** + * Default sound effect info if none is provided: default volume, medium priority, stereo channels, *no* stop on scene change. + */ + AudioSoundTableStreamInfo const* default_stream_info; + + /** + * Replace a sound effect's parameters in the sound table. This replacement will follow mod order + * prioritization. + * + * @param ctx Pointer to your mod's context. + * @param stream_id ID of the stream. + * @param file_path Path of the .ast on disc. If you're providing one yourself, use an overlay! + * @param info Optional: new parameters for the stream. Falls back to @ref default_stream_info if not provided. + * @param out_handle Optional: pointer receives the handle for the replacement. This can be used with @ref remove_sound_table. + */ + ModResult (*replace_sound_table_stream)( + ModContext* ctx, + uint16_t stream_id, + char const* file_path, + AudioSoundTableStreamInfo const* info, + AudioSoundTableHandle* out_handle); + + /** + * Add a sound effect to the sound table. The service allocates the placed ID for you. + * + * @param ctx Pointer to your mod's context. + * @param file_path Path of the .ast on disc. If you're providing one yourself, use an overlay! + * @param info Optional: new parameters for the stream. Falls back to @ref default_stream_info if not provided. + * @param out_handle Optional: pointer receives the handle for the replacement. This can be used with @ref remove_sound_table. + * @param out_stream_id Receives the allocated ID of the stream. + */ + ModResult (*add_sound_table_stream)( + ModContext* ctx, + char const* file_path, + AudioSoundTableStreamInfo const* info, + AudioSoundTableHandle* out_handle, + uint16_t* out_stream_id); + + /** + * Remove a sound table addition/replacement previously created by this mod. + * + * @param ctx Pointer to your mod's context. + * @param handle The handle identifying which wave to remove. + */ + ModResult (*remove_sound_table)(ModContext* ctx, AudioSoundTableHandle handle); +} AudioResService; + +MOD_DECLARE_SERVICE(AudioResService, svc_audio_res, AUDIO_RES_SERVICE_ID, AUDIO_RES_SERVICE_MAJOR, AUDIO_RES_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/camera.h b/sdk/include/mods/svc/camera.h index 1dccecfba6..a901c2b739 100644 --- a/sdk/include/mods/svc/camera.h +++ b/sdk/include/mods/svc/camera.h @@ -6,7 +6,7 @@ #include #endif -#define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera" +#define CAMERA_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "camera" #define CAMERA_SERVICE_MAJOR 1u #define CAMERA_SERVICE_MINOR 1u diff --git a/sdk/include/mods/svc/config.h b/sdk/include/mods/svc/config.h index fb04f6e9ea..55a0bca131 100644 --- a/sdk/include/mods/svc/config.h +++ b/sdk/include/mods/svc/config.h @@ -6,7 +6,7 @@ #include #endif -#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config" +#define CONFIG_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "config" #define CONFIG_SERVICE_MAJOR 1u #define CONFIG_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/file.h b/sdk/include/mods/svc/file.h index 4d5c82b5a1..0383c54066 100644 --- a/sdk/include/mods/svc/file.h +++ b/sdk/include/mods/svc/file.h @@ -6,7 +6,7 @@ #include #endif -#define FILE_SERVICE_ID "dev.twilitrealm.dusklight.file" +#define FILE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "file" #define FILE_SERVICE_MAJOR 1u #define FILE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/flow.h b/sdk/include/mods/svc/flow.h index a9028df0a4..1099d86fd9 100644 --- a/sdk/include/mods/svc/flow.h +++ b/sdk/include/mods/svc/flow.h @@ -6,7 +6,7 @@ #include #endif -#define FLOW_SERVICE_ID "dev.twilitrealm.dusklight.flow" +#define FLOW_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "flow" #define FLOW_SERVICE_MAJOR 1u #define FLOW_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/game.h b/sdk/include/mods/svc/game.h index 32de49903f..61a10968e6 100644 --- a/sdk/include/mods/svc/game.h +++ b/sdk/include/mods/svc/game.h @@ -15,7 +15,7 @@ * ordinary version check then fails mods built against the old epoch with a clear message instead * of letting them corrupt memory. */ -#define GAME_SERVICE_ID "dev.twilitrealm.dusklight.game" +#define GAME_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "game" #define GAME_SERVICE_MAJOR 2u #define GAME_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/game_mode.h b/sdk/include/mods/svc/game_mode.h index 8ccc5d64f1..d17a8f003c 100644 --- a/sdk/include/mods/svc/game_mode.h +++ b/sdk/include/mods/svc/game_mode.h @@ -3,7 +3,7 @@ #include #include -#define GAME_MODE_SERVICE_ID "dev.twilitrealm.dusklight.gamemode" +#define GAME_MODE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "gamemode" #define GAME_MODE_SERVICE_MAJOR 1u #define GAME_MODE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/gfx.h b/sdk/include/mods/svc/gfx.h index 0eee026984..787958716b 100644 --- a/sdk/include/mods/svc/gfx.h +++ b/sdk/include/mods/svc/gfx.h @@ -31,7 +31,7 @@ * should be released in mod_shutdown. The device outlives all mods. */ -#define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx" +#define GFX_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "gfx" #define GFX_SERVICE_MAJOR 1u #define GFX_SERVICE_MINOR 2u diff --git a/sdk/include/mods/svc/hook.h b/sdk/include/mods/svc/hook.h index 288db3cc14..30b199b386 100644 --- a/sdk/include/mods/svc/hook.h +++ b/sdk/include/mods/svc/hook.h @@ -20,7 +20,7 @@ * (file-local statics included). */ -#define HOOK_SERVICE_ID "dev.twilitrealm.dusklight.hook" +#define HOOK_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "hook" #define HOOK_SERVICE_MAJOR 1u #define HOOK_SERVICE_MINOR 1u diff --git a/sdk/include/mods/svc/hook.hpp b/sdk/include/mods/svc/hook.hpp index eab3115d01..7ef02538b3 100644 --- a/sdk/include/mods/svc/hook.hpp +++ b/sdk/include/mods/svc/hook.hpp @@ -136,8 +136,7 @@ struct NamedHook : HookImpl, R, A...> {}; * mods::hook::add_pre(on_link_execute); * * DEFINE_HOOK_SYMBOL names may be the platform mangled name (dlopen convention, no Mach-O - * leading underscore) or the demangled qualified display name; overloaded display names are - * ambiguous and need the mangled form. + * leading underscore), a qualified display name, or a source.cpp#qualified_name alias. */ #if defined(__GNUC__) && !defined(__clang__) && defined(__ELF__) #define DEFINE_HOOK(target, alias) \ diff --git a/sdk/include/mods/svc/host.h b/sdk/include/mods/svc/host.h index 45d89eea3e..2b751f34c3 100644 --- a/sdk/include/mods/svc/host.h +++ b/sdk/include/mods/svc/host.h @@ -11,7 +11,7 @@ * Always available; every other service can be reached from it. */ -#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host" +#define HOST_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "host" #define HOST_SERVICE_MAJOR 2u #define HOST_SERVICE_MINOR 2u diff --git a/sdk/include/mods/svc/http.h b/sdk/include/mods/svc/http.h index fd4d9a98db..2c880b3b80 100644 --- a/sdk/include/mods/svc/http.h +++ b/sdk/include/mods/svc/http.h @@ -6,7 +6,7 @@ #include #endif -#define HTTP_SERVICE_ID "dev.twilitrealm.dusklight.http" +#define HTTP_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "http" #define HTTP_SERVICE_MAJOR 1u #define HTTP_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/item.h b/sdk/include/mods/svc/item.h index ff4f31b547..58cf7743e4 100644 --- a/sdk/include/mods/svc/item.h +++ b/sdk/include/mods/svc/item.h @@ -6,7 +6,7 @@ #include #endif -#define ITEM_SERVICE_ID "dev.twilitrealm.dusklight.item" +#define ITEM_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "item" #define ITEM_SERVICE_MAJOR 2u #define ITEM_SERVICE_MINOR 3u diff --git a/sdk/include/mods/svc/log.h b/sdk/include/mods/svc/log.h index 1040710492..2401b4dccf 100644 --- a/sdk/include/mods/svc/log.h +++ b/sdk/include/mods/svc/log.h @@ -11,7 +11,7 @@ * (prefixed with its ID). */ -#define LOG_SERVICE_ID "dev.twilitrealm.dusklight.log" +#define LOG_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "log" #define LOG_SERVICE_MAJOR 1u #define LOG_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/message.h b/sdk/include/mods/svc/message.h index b170e6f09b..d3833d904e 100644 --- a/sdk/include/mods/svc/message.h +++ b/sdk/include/mods/svc/message.h @@ -6,7 +6,7 @@ #include #endif -#define MESSAGE_SERVICE_ID "dev.twilitrealm.dusklight.message" +#define MESSAGE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "message" #define MESSAGE_SERVICE_MAJOR 1u #define MESSAGE_SERVICE_MINOR 1u diff --git a/sdk/include/mods/svc/net.h b/sdk/include/mods/svc/net.h index 2c5e9a531a..9202ff50ad 100644 --- a/sdk/include/mods/svc/net.h +++ b/sdk/include/mods/svc/net.h @@ -6,7 +6,7 @@ #include #endif -#define NET_SERVICE_ID "dev.twilitrealm.dusklight.net" +#define NET_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "net" #define NET_SERVICE_MAJOR 1u #define NET_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/overlay.h b/sdk/include/mods/svc/overlay.h index ea46bc8798..ee4318ea4f 100644 --- a/sdk/include/mods/svc/overlay.h +++ b/sdk/include/mods/svc/overlay.h @@ -6,7 +6,7 @@ #include #endif -#define OVERLAY_SERVICE_ID "dev.twilitrealm.dusklight.overlay" +#define OVERLAY_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "overlay" #define OVERLAY_SERVICE_MAJOR 1u #define OVERLAY_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/resource.h b/sdk/include/mods/svc/resource.h index 60b609d2a2..474c781b5b 100644 --- a/sdk/include/mods/svc/resource.h +++ b/sdk/include/mods/svc/resource.h @@ -12,9 +12,9 @@ * for temporary storage. */ -#define RESOURCE_SERVICE_ID "dev.twilitrealm.dusklight.resource" +#define RESOURCE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "resource" #define RESOURCE_SERVICE_MAJOR 1u -#define RESOURCE_SERVICE_MINOR 0u +#define RESOURCE_SERVICE_MINOR 1u /* * A loaded resource, allocated by the service. Return every successful load with free; @@ -44,6 +44,9 @@ typedef struct ResourceService { * already-freed buffer. */ void (*free)(ModContext* ctx, ResourceBuffer* buffer); + + bool (*file_exists)(ModContext* ctx, char const* relative_path); + bool (*directory_exists)(ModContext* ctx, char const* relative_path); } ResourceService; MOD_DECLARE_SERVICE(ResourceService, svc_resource, RESOURCE_SERVICE_ID, RESOURCE_SERVICE_MAJOR, diff --git a/sdk/include/mods/svc/save.h b/sdk/include/mods/svc/save.h index 0d6943f96a..d9eb7432be 100644 --- a/sdk/include/mods/svc/save.h +++ b/sdk/include/mods/svc/save.h @@ -6,7 +6,7 @@ #include #endif -#define SAVE_SERVICE_ID "dev.twilitrealm.dusklight.save" +#define SAVE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "save" #define SAVE_SERVICE_MAJOR 1u #define SAVE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/stage.h b/sdk/include/mods/svc/stage.h index a9f8198919..9dbd94cff1 100644 --- a/sdk/include/mods/svc/stage.h +++ b/sdk/include/mods/svc/stage.h @@ -6,7 +6,7 @@ #include #endif -#define STAGE_SERVICE_ID "dev.twilitrealm.dusklight.stage" +#define STAGE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "stage" #define STAGE_SERVICE_MAJOR 1u #define STAGE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/texture.h b/sdk/include/mods/svc/texture.h index dac73fbade..0de813cd04 100644 --- a/sdk/include/mods/svc/texture.h +++ b/sdk/include/mods/svc/texture.h @@ -6,7 +6,7 @@ #include #endif -#define TEXTURE_SERVICE_ID "dev.twilitrealm.dusklight.texture" +#define TEXTURE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "texture" #define TEXTURE_SERVICE_MAJOR 1u #define TEXTURE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index d0badfda05..300f803721 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -8,20 +8,18 @@ #include #endif -#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui" +#define UI_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "ui" #define UI_SERVICE_MAJOR 2u -#define UI_SERVICE_MINOR 2u +#define UI_SERVICE_MINOR 3u /* * UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts, * scoped RCSS stylesheets and menu bar tabs. * * All calls must be made on the game thread from mod callbacks (initialize, update, hooks, or UI - * callbacks). Handles are opaque, generation-checked ids; a stale or unknown handle fails with - * MOD_INVALID_ARGUMENT. Element handles die with the content that owns them: a panel or tab rebuild - * destroys the previous build's elements, so re-acquire handles in each build callback and use them - * only until the next rebuild. Strings are UTF-8 and, in both directions, only valid for the - * duration of the call. + * callbacks). Handles are opaque IDs; a stale or unknown handle fails with MOD_INVALID_ARGUMENT. + * A panel or tab rebuild destroys the previous build's elements, so re-acquire handles in each + * build callback and use them only until the next rebuild. Strings are UTF-8. */ /* 0 is never a valid handle. */ @@ -30,6 +28,7 @@ typedef uint64_t UiDialogHandle; typedef uint64_t UiElementHandle; typedef uint64_t UiStyleHandle; typedef uint64_t UiMenuTabHandle; +typedef uint64_t UiContextMenuHandle; typedef enum UiStyleScope { UI_SCOPE_PRELAUNCH = 0, /* the pre-launch menu */ @@ -54,18 +53,21 @@ typedef enum UiControlKind { UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */ UI_CONTROL_COLOR = 5, /* RGB/RGBA color string with a picker */ UI_CONTROL_GROUP = 6, /* navigation row (on_pressed) */ - UI_CONTROL_FILE_PICKER = 7, /* file/folder picker with an opaque string location */ + /* Minor version 2 */ + UI_CONTROL_FILE_PICKER = 7, /* file/folder picker */ + /* Minor version 3 */ + UI_CONTROL_ICON_BUTTON = 8, /* icon button with a tooltip */ + UI_CONTROL_DROPDOWN = 9, /* dropdown select; the value is the option index */ } UiControlKind; typedef enum UiControlBinding { /* Values flow through the `get`/`set` callbacks. Getters are polled every frame while the control is visible and must be cheap. */ UI_BINDING_CALLBACKS = 0, - /* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod) - * directly: persistence, change notifications and the modified indicator (value != default) are - * wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and - * SELECT = int, STRING, COLOR and FILE_PICKER = string. Float vars are not bindable; use - * callbacks. */ + /* The control reads and writes `config_var` (a ConfigService handle) directly. Persistence, + * change notifications and the modified indicator are bound automatically. The var type must + * match the control kind: TOGGLE = bool, NUMBER, SELECT and DROPDOWN = int, STRING, COLOR and + * FILE_PICKER = string. Float vars are not bindable; use callbacks. */ UI_BINDING_CONFIG_VAR = 1, } UiControlBinding; @@ -74,10 +76,12 @@ typedef enum UiStringSetMode { UI_STRING_SET_ON_CHANGE = 1, /* invokes `set` on every text change (e.g. while typing) */ } UiStringSetMode; -/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING, - * COLOR and FILE_PICKER read string_value. string_value passed to a setter is only valid during the - * call; a getter should point it at storage owned by the mod (e.g. a static buffer) that stays valid - * until the next call into the mod. The host copies it right after the getter returns. */ +/* + * Value for each type of control: + * - bool_value: TOGGLE + * - int_value: NUMBER, SELECT and DROPDOWN + * - string_value: STRING, COLOR and FILE_PICKER + */ typedef struct UiControlValue { uint32_t struct_size; bool bool_value; @@ -101,11 +105,11 @@ typedef struct UiControlDesc { /* Optional RML shown as contextual help when the control is focused or hovered. Only rendered * where a help pane exists (mod window tabs). */ const char* help_rml; - UiControlBinding binding; /* ignored for BUTTON and GROUP */ + UiControlBinding binding; /* ignored for BUTTON, GROUP, and ICON_BUTTON */ ConfigVarHandle config_var; /* UI_BINDING_CONFIG_VAR */ - UiControlGetFn get; /* UI_BINDING_CALLBACKS (all kinds but BUTTON/GROUP) */ - UiControlSetFn set; /* UI_BINDING_CALLBACKS (all kinds but BUTTON/GROUP) */ - UiPressedFn on_pressed; /* BUTTON/GROUP only. Required for both. */ + UiControlGetFn get; /* UI_BINDING_CALLBACKS (all kinds but BUTTON/GROUP/ICON_BUTTON) */ + UiControlSetFn set; /* UI_BINDING_CALLBACKS (all kinds but BUTTON/GROUP/ICON_BUTTON) */ + UiPressedFn on_pressed; /* BUTTON/GROUP/ICON_BUTTON: required. */ UiPredicateFn is_disabled; /* optional */ /* Optional override for the modified indicator. CONFIG_VAR controls derive it from value != * default when this is NULL. */ @@ -119,9 +123,7 @@ typedef struct UiControlDesc { int64_t step; const char* prefix; /* NUMBER: optional text before the value */ const char* suffix; /* NUMBER: optional text after the value */ - /* SELECT: option labels (plain text). Required for SELECT. SELECT controls - * present their options in the help pane, so they are only available where - * one exists (mod window tabs); MOD_UNSUPPORTED elsewhere. */ + /* SELECT/DROPDOWN: option labels (plain text). At least one is required. */ const char* const* options; size_t option_count; int32_t max_length; /* STRING: maximum input length; < 1 means unlimited */ @@ -129,18 +131,71 @@ typedef struct UiControlDesc { const char* const* color_presets; size_t color_preset_count; bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */ - UiPredicateFn is_selected; /* BUTTON/GROUP: optional selected state */ + UiPredicateFn is_selected; /* BUTTON/GROUP/ICON_BUTTON: optional selected state */ UiStringSetMode string_set_mode; /* STRING: when to invoke the setter */ /* FILE_PICKER: optional file filters and folder selection mode. */ const FileFilter* file_filters; size_t file_filter_count; bool directory_mode; + const char* icon; /* ICON_BUTTON: supported Material Symbols name */ + /* DROPDOWN: option_count values (optional). Labels and flags are copied. */ + const bool* option_enabled; + /* Optional plain-text tooltip. ICON_BUTTON uses label if not specified. */ + const char* tooltip; } UiControlDesc; #define UI_CONTROL_DESC_INIT \ {sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \ NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false, NULL, \ - UI_STRING_SET_ON_COMMIT, NULL, 0u, false} + UI_STRING_SET_ON_COMMIT, NULL, 0u, false, NULL, NULL, NULL} + +typedef struct UiControlOption { + uint32_t struct_size; + const char* label; + bool enabled; +} UiControlOption; + +#define UI_CONTROL_OPTION_INIT {sizeof(UiControlOption), NULL, true} + +typedef void (*UiContextMenuActionFn)(ModContext* ctx, void* user_data); + +typedef struct UiContextMenuItem { + uint32_t struct_size; + const char* label; /* required, plain text */ + const char* icon; /* optional Material Symbols name */ + UiContextMenuActionFn on_pressed; /* NULL disables the item */ + void* user_data; + bool enabled; + bool selected; + bool destructive; + bool separator_before; +} UiContextMenuItem; + +#define UI_CONTEXT_MENU_ITEM_INIT \ + {sizeof(UiContextMenuItem), NULL, NULL, NULL, NULL, true, false, false, false} + +typedef struct UiContextMenuDesc { + uint32_t struct_size; + const UiContextMenuItem* items; /* at least one; copied */ + size_t item_count; +} UiContextMenuDesc; + +#define UI_CONTEXT_MENU_DESC_INIT {sizeof(UiContextMenuDesc), NULL, 0u} + +typedef enum UiRowAlign { + UI_ROW_ALIGN_START = 0, + UI_ROW_ALIGN_END = 1, + UI_ROW_ALIGN_CENTER = 2, + UI_ROW_ALIGN_SPACE_BETWEEN = 3, +} UiRowAlign; + +typedef struct UiRowDesc { + uint32_t struct_size; + UiRowAlign align; + bool wrap; +} UiRowDesc; + +#define UI_ROW_DESC_INIT {sizeof(UiRowDesc), UI_ROW_ALIGN_START, false} typedef uint64_t UiListHandle; @@ -302,7 +357,7 @@ typedef struct UiService { /* Register or replace the panel shown in the calling mod's Mods-window tab. */ ModResult (*register_mods_panel)(ModContext* ctx, const UiModsPanelDesc* desc); - /* Content builders. `pane` is a panel or tab pane handle; out_elem (where + /* Content builders. `pane` is a panel, tab/dialog pane, or row handle; out_elem (where * present, optional) receives a handle for later elem_set_* updates. */ ModResult (*pane_add_section)(ModContext* ctx, UiElementHandle pane, const char* title); ModResult (*pane_add_text)( @@ -374,6 +429,30 @@ typedef struct UiService { /* Replace all items in a list with a new set. */ ModResult (*list_set_items)( ModContext* ctx, UiListHandle list, const UiListItem* items, size_t item_count); + + /* Minor version 3 */ + + /* Horizontal container. parent must be a pane or row. */ + ModResult (*pane_add_row)( + ModContext* ctx, UiElementHandle parent, const UiRowDesc* desc, UiElementHandle* out_row); + + /* Update a control's label. */ + ModResult (*control_set_label)(ModContext* ctx, UiElementHandle control, const char* label); + /* ICON_BUTTON only. Unknown or empty icon names return MOD_INVALID_ARGUMENT. */ + ModResult (*control_set_icon)(ModContext* ctx, UiElementHandle control, const char* icon); + /* DROPDOWN only. Empty options disables the control. */ + ModResult (*control_set_options)(ModContext* ctx, UiElementHandle control, + const UiControlOption* options, size_t option_count); + ModResult (*control_set_tooltip)(ModContext* ctx, UiElementHandle control, const char* tooltip); + + ModResult (*elem_set_visible)(ModContext* ctx, UiElementHandle elem, bool visible); + /* Focus a visible, enabled element/container. MOD_UNAVAILABLE if it cannot receive focus. */ + ModResult (*elem_focus)(ModContext* ctx, UiElementHandle elem); + + /* Creates a context menu. Anchor must be a visible and enabled element. */ + ModResult (*context_menu_push)(ModContext* ctx, UiElementHandle anchor, + const UiContextMenuDesc* desc, UiContextMenuHandle* out_menu); + ModResult (*context_menu_close)(ModContext* ctx, UiContextMenuHandle menu); } UiService; MOD_DECLARE_SERVICE(UiService, svc_ui, UI_SERVICE_ID, UI_SERVICE_MAJOR, UI_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/websocket.h b/sdk/include/mods/svc/websocket.h index bfc12d8fca..abd36410ef 100644 --- a/sdk/include/mods/svc/websocket.h +++ b/sdk/include/mods/svc/websocket.h @@ -7,7 +7,7 @@ #include #endif -#define WEBSOCKET_SERVICE_ID "dev.twilitrealm.dusklight.websocket" +#define WEBSOCKET_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "websocket" #define WEBSOCKET_SERVICE_MAJOR 1u #define WEBSOCKET_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/window.h b/sdk/include/mods/svc/window.h index 7cee8a5abe..843e388ed8 100644 --- a/sdk/include/mods/svc/window.h +++ b/sdk/include/mods/svc/window.h @@ -8,7 +8,7 @@ #include -#define WINDOW_SERVICE_ID "dev.twilitrealm.dusklight.window" +#define WINDOW_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "window" #define WINDOW_SERVICE_MAJOR 1u #define WINDOW_SERVICE_MINOR 0u diff --git a/sdk/luau/dusklight.d.luau b/sdk/luau/dusklight.d.luau index 619e2f4124..9d6a7ddad0 100644 --- a/sdk/luau/dusklight.d.luau +++ b/sdk/luau/dusklight.d.luau @@ -49,21 +49,35 @@ export type TextureModule = { register_data: (key: TextureKey, data: TextureData) -> TextureHandle, } +export type UiRowDesc = {align: ("start" | "center" | "end" | "space_between")?, wrap: boolean?} +export type UiControlOption = {label: string, enabled: boolean?} +export type UiContextMenuItem = { + label: string, icon: string?, on_pressed: (() -> ())?, enabled: boolean?, + selected: boolean?, destructive: boolean?, separator_before: boolean?, +} +export type UiContextMenu = {close: (self: UiContextMenu) -> ()} export type UiElement = { - add_section: (self: UiElement, title: string) -> (), - add_text: (self: UiElement, text: string) -> UiElement, - add_rml: (self: UiElement, rml: string) -> UiElement, - add_progress: (self: UiElement, value: number) -> UiElement, add_control: (self: UiElement, desc: UiControlDesc) -> UiElement, add_group: (self: UiElement, target: UiElement, desc: UiGroupDesc) -> UiElement, add_list: (self: UiElement, desc: UiListDesc) -> UiList, - set_text: (self: UiElement, text: string) -> (), - set_rml: (self: UiElement, rml: string) -> (), - set_progress: (self: UiElement, value: number) -> (), + add_progress: (self: UiElement, value: number) -> UiElement, + add_rml: (self: UiElement, rml: string) -> UiElement, + add_row: (self: UiElement, desc: UiRowDesc) -> UiElement, + add_section: (self: UiElement, title: string) -> (), + add_text: (self: UiElement, text: string) -> UiElement, + focus: (self: UiElement) -> boolean, set_class: (self: UiElement, name: string, active: boolean) -> (), + set_icon: (self: UiElement, icon: string) -> (), + set_label: (self: UiElement, label: string) -> (), + set_options: (self: UiElement, options: {UiControlOption}) -> (), + set_progress: (self: UiElement, value: number) -> (), + set_rml: (self: UiElement, rml: string) -> (), + set_text: (self: UiElement, text: string) -> (), + set_tooltip: (self: UiElement, text: string?) -> (), + set_visible: (self: UiElement, visible: boolean) -> (), } export type UiControlDesc = { - kind: "button" | "toggle" | "number" | "string" | "select" | "color" | "group" | "file_picker", + kind: "button" | "toggle" | "number" | "string" | "select" | "color" | "group" | "file_picker" | "icon_button" | "dropdown", label: string, help_rml: string?, config_var: ConfigVar?, @@ -85,6 +99,9 @@ export type UiControlDesc = { string_set_mode: ("commit" | "change")?, file_filters: {{name: string, pattern: string}}?, directory_mode: boolean?, + icon: string?, + tooltip: string?, + option_enabled: {boolean}?, } export type UiGroupDesc = { label: string, build: (pane: UiElement) -> () } export type UiListItem = { key: IntegerInput, label: string } @@ -109,6 +126,7 @@ export type UiTabDesc = { update: (() -> ())?, } export type UiModule = { + context_menu_push: (anchor: UiElement, desc: {items: {UiContextMenuItem}}) -> UiContextMenu, register_mods_panel: (desc: {build: (panel: UiElement) -> (), update: (() -> ())?}) -> (), window_push: (desc: {tabs: {UiTabDesc}, rcss: string?, on_closed: ((window: UiWindow) -> ())?}) -> UiWindow, dialog_push: (desc: { @@ -150,6 +168,85 @@ export type HostModule = { } export type ResourceModule = { load: (relativePath: string) -> string } +-- +-- audio_res +-- + +export type AudioWaveFormat = "adpcm4" | "adpcm2" | "pcm8" | "pcm16" + +export type AudioRawWave = { + format: AudioWaveFormat, + sample_rate: number, + sample_value_last: number | nil, + sample_value_penult: number | nil +} + +export type AudioWaveInfo = { + base_key: number | nil, + loop: boolean | nil, + loop_start_sample: number | nil, + loop_end_sample: number | nil, + raw_wave: AudioRawWave | nil +} + +export type AudioWaveHandle = { + unregister: (self: AudioWaveHandle) -> (), +} + +export type AudioWaveBank = "sound_effects" | "music_samples" + +export type AudioSoundTableEffectInfo = { + priority: number, + volume: number, + pitch: number, + always_max_priority: boolean, + ignore_distance_volume: boolean, + ignore_distance_fx_mix: boolean, + ignore_pan: boolean, + ignore_dolby: boolean, + random_volume: number, + random_pitch: number, + doppler_power: number, + volume_dist_class: number, + clamp_min_volume: boolean, + cull_at_max_distance: boolean, +} + +export type SoundEffectCategory = "system_se" | "player_voice" | "player_se" | "footnote_se" | "collision_se" | "chara_voice" | "chara_se" | "enemy_se" | "object_se" | "env_se" + +export type AudioSoundTableHandle = { + unregister: (self: AudioSoundTableHandle) -> () +} + +export type StreamPan = "center" | "left" | "right" + +export type AudioSoundTableStreamInfo = { + priority: number, + volume: number, + pan_parameters: {StreamPan}, -- max STREAM_MAX_CHILDREN elems + stop_on_scene_change: boolean, +} + +export type AudioResModule = { + -- wsys + DEFAULT_KEY: number, + default_wave_info: () -> AudioWaveInfo, + replace_wave: (bank: AudioWaveBank, wave_id: number, file: string, info: AudioWaveInfo | nil) -> AudioWaveHandle, + add_wave: (bank: AudioWaveBank, file: string, info: AudioWaveInfo | nil) -> (number, AudioWaveHandle), + + -- bst + default_effect_info: () -> AudioSoundTableEffectInfo, + replace_sound_table_effect: (category: SoundEffectCategory, effect_id: number, info: AudioSoundTableEffectInfo | nil) -> AudioSoundTableHandle, + add_sound_table_effect: (category: SoundEffectCategory, info: AudioSoundTableEffectInfo | nil) -> (number, AudioSoundTableHandle), + + STREAM_MAX_CHILDREN: number, + default_stream_info: () -> AudioSoundTableStreamInfo, + replace_sound_table_stream: (stream_id: number, file_path: string, info: AudioSoundTableStreamInfo | nil) -> AudioSoundTableHandle, + add_sound_table_stream: (file_path: string, info: AudioSoundTableStreamInfo | nil) -> (number, AudioSoundTableHandle), +} + + + declare function require(path: "dusklight.log"): LogModule declare function require(path: "dusklight.host"): HostModule declare function require(path: "dusklight.config"): ConfigModule @@ -157,4 +254,5 @@ declare function require(path: "dusklight.resource"): ResourceModule declare function require(path: "dusklight.overlay"): OverlayModule declare function require(path: "dusklight.texture"): TextureModule declare function require(path: "dusklight.ui"): UiModule +declare function require(path: "dusklight.audio_res"): AudioResModule declare function require(path: string): any diff --git a/src/Z2AudioLib/Z2Audience.cpp b/src/Z2AudioLib/Z2Audience.cpp index dda1a4b2d0..831d356d2f 100644 --- a/src/Z2AudioLib/Z2Audience.cpp +++ b/src/Z2AudioLib/Z2Audience.cpp @@ -36,14 +36,14 @@ Z2Audible::Z2Audible(const JGeometry::TVec3& pos, const JGeometry::TVec3= 0); - JUT_ASSERT(81, index < 1); + JUT_ASSERT(81, index < 1); // Z2_AUDIO_PLAYERS return &mChannel[index].mParams; } void Z2Audible::setOuterParams(const JASSoundParams& outParams, const JASSoundParams& inParams, int index) { JUT_ASSERT(89, index >= 0); - JUT_ASSERT(90, index < 1); + JUT_ASSERT(90, index < 1); // Z2_AUDIO_PLAYERS Z2AudibleChannel* channel = &mChannel[index]; - u8 iStack_94 = (mParam.field_0x0.half.f1 & 0xf00) >> 8; - if (iStack_94 == 0) { + u8 volSomething = (mParam.field_0x0.half.f1 & SOUND_VOL_SOMETHING_MASK_SHIFTED) >> 8; + if (volSomething == 0) { channel->mParams.combine(outParams, inParams); return; } @@ -91,7 +91,7 @@ void Z2Audible::setOuterParams(const JASSoundParams& outParams, const JASSoundPa f32 dVar9 = inParams.mVolume * local_b8.length(); f32 dVar10 = 1.0f; - f32 dVar12 = Z2Calc::linearTransform(iStack_94, 0.0f, 15.0f, 1.0f, 0.3f, true); + f32 dVar12 = Z2Calc::linearTransform(volSomething, 0.0f, 15.0f, 1.0f, 0.3f, true); if (inParams.mVolume > dVar12 && dVar9 > 0.001f) { dVar10 = Z2Calc::getParamByExp(dVar9, 0.3f, 0.001f, 0.1f, dVar12, 1.0f, Z2Calc::CURVE_POSITIVE); @@ -141,18 +141,18 @@ void Z2Audible::setOuterParams(const JASSoundParams& outParams, const JASSoundPa Z2AudibleChannel* Z2Audible::getChannel(int index) { JUT_ASSERT(220, index >= 0); - JUT_ASSERT(221, index < 1); + JUT_ASSERT(221, index < 1); // Z2_AUDIO_PLAYERS return &mChannel[index]; } u32 Z2Audible::getDistVolBit() { u16 uVar1 = getAudibleParam()->field_0x0.half.f1; if (uVar1 != 0) { - if ((uVar1 & 7) != 0) { - return uVar1 & 7; + if ((uVar1 & SOUND_VOL_DIST_BIT_MASK_SHIFTED) != 0) { + return uVar1 & SOUND_VOL_DIST_BIT_MASK_SHIFTED; } - if ((uVar1 & 0x70) != 0) { - return ((int)(uVar1 & 0x70) >> 4) + 7; + if ((uVar1 & SOUND_VOL_DIST_BIT_2_MASK_SHIFTED) != 0) { + return ((int)(uVar1 & SOUND_VOL_DIST_BIT_2_MASK_SHIFTED) >> 4) + 7; } } @@ -166,15 +166,15 @@ Z2Audience3DSetting::Z2Audience3DSetting() { } void Z2Audience3DSetting::init() { - field_0x40 = Z2Param::MIN_DISTANCE_VOLUME; - field_0x48 = Z2Param::DOLBY_BEHIND_DISTANCE_MAX; - field_0x44 = Z2Param::DOLBY_FLONT_DISTANCE_MAX; - field_0x54 = Z2Param::DISTANCE_FX_PARAM; - field_0x50 = 0.0f; - field_0x4c = Z2Param::DOLBY_CENTER_VALUE; - field_0x58 = 0.5f; - field_0x64 = 0xff; - field_0x5c = Z2Param::SONIC_SPEED; + mMinDistanceVolume = Z2Param::MIN_DISTANCE_VOLUME; + mDolbyBehindDistanceMax = Z2Param::DOLBY_BEHIND_DISTANCE_MAX; + mDolbyFrontDistanceMax = Z2Param::DOLBY_FLONT_DISTANCE_MAX; + mMaxDistanceFxMix = Z2Param::DISTANCE_FX_PARAM; + mMinDistanceFxMix = 0.0f; + mDolbyCenterValue = Z2Param::DOLBY_CENTER_VALUE; + mPanFactor = 0.5f; + mMaxDistancePriority = 0xff; + mSonicSpeed = Z2Param::SONIC_SPEED; field_0x60 = 1.5f; initVolumeDist(); @@ -184,8 +184,8 @@ void Z2Audience3DSetting::init() { void Z2Audience3DSetting::initVolumeDist() { if (mVolumeDistInit) return; - field_0x0[0] = Z2Param::DISTANCE_MAX; - field_0x3c = Z2Param::MAX_VOLUME_DISTANCE; + mDistanceMaxes[0] = Z2Param::DISTANCE_MAX; + mMaxVolumeDistance = Z2Param::MAX_VOLUME_DISTANCE; calcVolumeFactorAll(); calcPriorityFactorAll(); @@ -197,12 +197,12 @@ void Z2Audience3DSetting::initVolumeDist() { static f32 cNearFarRatio = Z2Param::MAX_VOLUME_DISTANCE / Z2Param::DISTANCE_MAX; void Z2Audience3DSetting::updateVolumeDist(f32 param_0) { - field_0x0[0] = param_0; + mDistanceMaxes[0] = param_0; if (param_0 > Z2Param::DISTANCE_MAX) { - field_0x3c = cNearFarRatio * param_0; + mMaxVolumeDistance = cNearFarRatio * param_0; } else { - field_0x3c = Z2Param::MAX_VOLUME_DISTANCE; + mMaxVolumeDistance = Z2Param::MAX_VOLUME_DISTANCE; } calcVolumeFactorAll(); @@ -214,29 +214,29 @@ void Z2Audience3DSetting::updateVolumeDist(f32 param_0) { void Z2Audience3DSetting::initDolbyDist() { if (!mDolbyDistInit) { - field_0x44 = Z2Param::DOLBY_FLONT_DISTANCE_MAX; - field_0x48 = Z2Param::DOLBY_BEHIND_DISTANCE_MAX; - field_0x68 = -field_0x4c / field_0x44; - field_0x6c = (1.0f - field_0x4c) / field_0x48; + mDolbyFrontDistanceMax = Z2Param::DOLBY_FLONT_DISTANCE_MAX; + mDolbyBehindDistanceMax = Z2Param::DOLBY_BEHIND_DISTANCE_MAX; + field_0x68 = -mDolbyCenterValue / mDolbyFrontDistanceMax; + field_0x6c = (1.0f - mDolbyCenterValue) / mDolbyBehindDistanceMax; mDolbyDistInit = true; } } void Z2Audience3DSetting::updateDolbyDist(f32 param_0, f32 param_1) { if (param_0 > -1.0f * Z2Param::DOLBY_FLONT_DISTANCE_MAX) { - field_0x44 = -1.0f * param_0; + mDolbyFrontDistanceMax = -1.0f * param_0; } else { - field_0x44 = Z2Param::DOLBY_FLONT_DISTANCE_MAX; + mDolbyFrontDistanceMax = Z2Param::DOLBY_FLONT_DISTANCE_MAX; } - if (param_1 < 2.0f * field_0x44) { - param_1 = 2.0f * field_0x44; + if (param_1 < 2.0f * mDolbyFrontDistanceMax) { + param_1 = 2.0f * mDolbyFrontDistanceMax; } if (param_1 > Z2Param::DOLBY_BEHIND_DISTANCE_MAX) { - field_0x48 = param_1; + mDolbyBehindDistanceMax = param_1; } else { - field_0x48 = Z2Param::DOLBY_BEHIND_DISTANCE_MAX; + mDolbyBehindDistanceMax = Z2Param::DOLBY_BEHIND_DISTANCE_MAX; } mDolbyDistInit = false; @@ -254,28 +254,28 @@ Z2AudioCamera::Z2AudioCamera() { } void Z2AudioCamera::init() { - JGeometry::TPosition3f32 aTStack_38; - aTStack_38.identity(); + JGeometry::TPosition3f32 viewMatrix; + viewMatrix.identity(); JGeometry::TVec3 VStack_44; VStack_44.set(100000.0f, 100000.0f, 100000.0f); - setCameraState(aTStack_38, *(Vec*)&VStack_44, true); + setCameraState(viewMatrix, *(Vec*)&VStack_44, true); } -void Z2AudioCamera::setCameraState(f32 const (*param_0)[4], Vec& pos, bool param_2) { - field_0x0.set(param_0); - if (param_2) { +void Z2AudioCamera::setCameraState(f32 const (*viewMatrix)[4], Vec& pos, bool initial) { + mViewMatrix.set(viewMatrix); + if (initial) { mPos.set(pos); - field_0x48.set(mPos); + mLastPos.set(mPos); mVel.zero(); } else { - field_0x48.set(mPos); + mLastPos.set(mPos); mPos.set(pos); - mVel.sub(mPos, field_0x48); + mVel.sub(mPos, mLastPos); } } -void Z2AudioCamera::setCameraState(f32 (*param_0)[4], Vec& pos, Vec& param_2, f32 param_3, - f32 param_4, bool param_5, bool param_6) { +void Z2AudioCamera::setCameraState(f32 (*viewMatrix)[4], Vec& pos, Vec& param_2, f32 param_3, + f32 param_4, bool param_5, bool initial) { JGeometry::TVec3 aTStack_c0; VECSubtract(¶m_2, &pos, (Vec*)&aTStack_c0); mCamDist = aTStack_c0.length(); @@ -312,7 +312,7 @@ void Z2AudioCamera::setCameraState(f32 (*param_0)[4], Vec& pos, Vec& param_2, f3 Z2GetAudience()->getSetting()->updateDolbyDist(mCamDist, mCamDist); } } else { - Vec aTStack_cc = {param_0[0][0], param_0[0][1], param_0[0][2]}; + Vec aTStack_cc = {viewMatrix[0][0], viewMatrix[0][1], viewMatrix[0][2]}; Mtx rotMtx; MTXRotAxisRad(rotMtx, &aTStack_cc, (M_PI / 180.0f) * (-1.0f * dVar10)); JGeometry::TVec3 aTStack_d8; @@ -324,7 +324,7 @@ void Z2AudioCamera::setCameraState(f32 (*param_0)[4], Vec& pos, Vec& param_2, f3 } } - setCameraState(param_0, pos, param_6); + setCameraState(viewMatrix, pos, initial); } void Z2AudioCamera::convertAbsToRel(Z2Audible* audible, int channelNum) { @@ -337,17 +337,17 @@ void Z2AudioCamera::convertAbsToRel(Z2Audible* audible, int channelNum) { return; } - Z2AudibleRelPos* relPos = &channel->field_0x14; - MTXMultVec(field_0x0, (Vec*)&audible->getPos(), (Vec*)&relPos->field_0x00); - relPos->field_0xC = relPos->field_0x00.length(); + Z2AudibleRelPos* relPos = &channel->mRelPos; + MTXMultVec(mViewMatrix, (Vec*)&audible->getPos(), (Vec*)&relPos->mCameraRelative); + relPos->mTrueDistance = relPos->mCameraRelative.length(); - JGeometry::TVec3 aTStack_38(relPos->field_0x00); + JGeometry::TVec3 aTStack_38(relPos->mCameraRelative); aTStack_38.z += mVolCenterZ; - relPos->field_0x10 = aTStack_38.length(); + relPos->mCenterDistance = aTStack_38.length(); } bool Z2AudioCamera::convertAbsToRel(Vec& src, Vec* dst) const { - MTXMultVec(field_0x0, &src, dst); + MTXMultVec(mViewMatrix, &src, dst); return isInSight(*dst); } @@ -395,7 +395,7 @@ void Z2SpotMic::calcVolumeFactor(int camID) { void Z2SpotMic::setMicState(Z2AudioCamera* camera, int camID) { JUT_ASSERT(622, camID >= 0); - JUT_ASSERT(623, camID < 1); + JUT_ASSERT(623, camID < 1); // Z2_AUDIO_PLAYERS if (mMicOn && mPosPtr != 0 && camera != NULL) { clearMicState(camID); @@ -406,8 +406,8 @@ void Z2SpotMic::setMicState(Z2AudioCamera* camera, int camID) { aVStack_58.z += camera->getVolCenterZ(); f32 dVar10 = aVStack_58.length(); - f32 dVar11 = Z2GetAudience()->getSetting()->field_0x0[0]; - f32 dVar12 = Z2GetAudience()->getSetting()->field_0x3c; + f32 dVar11 = Z2GetAudience()->getSetting()->mDistanceMaxes[0]; + f32 dVar12 = Z2GetAudience()->getSetting()->mMaxVolumeDistance; if (dVar10 > dVar11) { field_0x18[camID] = field_0xc; } else { @@ -443,21 +443,21 @@ f32 Z2SpotMic::calcMicDist(Z2Audible* audible) { return aTStack_1c.length(); } -u32 Z2SpotMic::calcMicPriority(f32 param_0) { - if (param_0 > field_0x4) { - return Z2GetAudience()->getSetting()->field_0x64; +u32 Z2SpotMic::calcMicPriority(f32 micDistance) { + if (micDistance > field_0x4) { + return Z2GetAudience()->getSetting()->mMaxDistancePriority; } - if (param_0 < field_0x0) { + if (micDistance < field_0x0) { return 0; } - return field_0x1c * (param_0 - field_0x0); + return field_0x1c * (micDistance - field_0x0); } f32 Z2SpotMic::calcMicVolume(f32 param_0, int camID, f32 param_2) { JUT_ASSERT(687, camID >= 0); - JUT_ASSERT(688, camID < 1); + JUT_ASSERT(688, camID < 1); // Z2_AUDIO_PLAYERS if (mMicOn == false) { return param_2; @@ -497,7 +497,7 @@ f32 Z2SpotMic::calcMicVolume(f32 param_0, int camID, f32 param_2) { } Z2Audience::Z2Audience() : JASGlobalInstance(true), field_0x4(1.0f), field_0x8(0x7f) { - mNumPlayers = 1; + mNumPlayers = Z2_AUDIO_PLAYERS; mUsingOffMicVol = false; mAudioCamera[0].init(); mAudioCamera[0].setMainCamera(true); @@ -512,24 +512,25 @@ bool Z2Audience::isActive() const { return Z2Audible::getTotalMemCount() != Z2Audible::getFreeMemCount(); } -void Z2Audience::setAudioCamera(f32 (*param_0)[4], Vec& pos, Vec& param_2, f32 param_3, - f32 param_4, bool param_5, int camID, bool param_7) { +void Z2Audience::setAudioCamera(f32 (*viewMatrix)[4], Vec& pos, Vec& param_2, f32 param_3, + f32 param_4, bool param_5, int camID, bool initial) { JUT_ASSERT(687, camID >= 0); JUT_ASSERT(688, camID < mNumPlayers); - mAudioCamera[camID].setCameraState(param_0, pos, param_2, param_3, param_4, param_5, param_7); + mAudioCamera[camID].setCameraState(viewMatrix, pos, param_2, param_3, param_4, param_5, initial); mLinkMic->setMicState(&mAudioCamera[camID], camID); } JAIAudible* Z2Audience::newAudible(const JGeometry::TVec3& pos, JAISoundID soundID, - const JGeometry::TVec3* param_2, u32 param_3) { + const JGeometry::TVec3* param_2, u32 param_3 + IF_DUSK_ARG(dusk::mods::svc::audio_res::bst::SoundTableReplacementSlot const* replacement)) { u32 channelNum = param_3 | ~((1 << mNumPlayers) + -1); if (channelNum == 0xFFFFFFFF) { JUT_WARN(826, "%s", "You masked all audible channels !"); return NULL; } - JAUAudibleParam params = Z2GetSoundInfo()->getAudibleSwFull(soundID); - bool x = params.field_0x0.bytes.b0_0 != 0; + JAUAudibleParam params = Z2GetSoundInfo()->getAudibleSwFull(soundID IF_DUSK_ARG(replacement)); + bool x = params.field_0x0.bytes.mDopplerPower != 0; Z2Audible* audible = JKR_NEW Z2Audible(pos, param_2, channelNum, x); if (audible == NULL) { @@ -543,13 +544,13 @@ JAIAudible* Z2Audience::newAudible(const JGeometry::TVec3& pos, JAISoundID if (channel != NULL) { u32 distVolBit = audible->getDistVolBit(); mAudioCamera[i].convertAbsToRel(audible, i); - calcDeltaPriority_(channel->field_0x14.field_0x10, distVolBit, false); + calcDeltaPriority_(channel->mRelPos.mCenterDistance, distVolBit, false); } } - for (int i = 0; i < 1; i++) { + for (int i = 0; i < Z2_AUDIO_PLAYERS; i++) { if (mSpotMic[i].isOn()) { - audible->field_0x64[i] = mSpotMic[i].calcMicDist(audible); + audible->mMicDistances[i] = mSpotMic[i].calcMicDist(audible); } } @@ -569,7 +570,7 @@ Z2Audible::~Z2Audible() {} u32 Z2Audience::calcPriority(JAIAudible* audible) { Z2Audible* Z2audible = (Z2Audible*)audible; - if (!Z2audible->getAudibleParam()->field_0x0.bytes.b0_4) { + if (!Z2audible->getAudibleParam()->field_0x0.bytes.mCalculatePriority) { for (int i = 0; i < mNumPlayers; i++) { mAudioCamera[i].convertAbsToRel(Z2audible, i); } @@ -584,17 +585,17 @@ u32 Z2Audience::calcPriority(JAIAudible* audible) { mAudioCamera[i].convertAbsToRel(Z2audible, i); u32 distvolBit = Z2audible->getDistVolBit(); const JAUAudibleParam* puVar1 = Z2audible->getAudibleParam(); - deltaPriority[i] = calcDeltaPriority_(channel->field_0x14.field_0x10, distvolBit, puVar1->field_0x0.bytes.b0_7); + deltaPriority[i] = calcDeltaPriority_(channel->mRelPos.mCenterDistance, distvolBit, puVar1->field_0x0.bytes.mCullAtMaxDistance); if (deltaPriority[i] < rv) { rv = deltaPriority[i]; } } } - for (int i = 0; i < 1; i++) { + for (int i = 0; i < Z2_AUDIO_PLAYERS; i++) { if (mSpotMic[i].isOn()) { - Z2audible->field_0x64[i] = mSpotMic[i].calcMicDist(Z2audible); - u32 micPriority = mSpotMic[i].calcMicPriority(Z2audible->field_0x64[i]); + Z2audible->mMicDistances[i] = mSpotMic[i].calcMicDist(Z2audible); + u32 micPriority = mSpotMic[i].calcMicPriority(Z2audible->mMicDistances[i]); if (micPriority < rv) { rv = micPriority; } @@ -619,31 +620,31 @@ void Z2Audience::mixChannelOut(const JASSoundParams& outParams, JAIAudible* audi return; } - JASSoundParams local_60; + JASSoundParams calcParams; u32 distVolBit = Z2audible->getDistVolBit(); - if (Z2audible->getAudibleParam()->field_0x0.bytes.b0_5) { - local_60.mVolume = calcVolume_(channel->field_0x14.field_0x10, distVolBit); - f32 dVar9 = mLinkMic->calcMicVolume(Z2audible->field_0x64[0], channelNum, local_60.mVolume); + if (Z2audible->getAudibleParam()->field_0x0.bytes.mCalcDistanceVolume) { + calcParams.mVolume = calcVolume_(channel->mRelPos.mCenterDistance, distVolBit); + f32 dVar9 = mLinkMic->calcMicVolume(Z2audible->mMicDistances[0], channelNum, calcParams.mVolume); if (dVar9 < 0.0f) { - dVar9 = calcOffMicSound(local_60.mVolume); + dVar9 = calcOffMicSound(calcParams.mVolume); } - local_60.mVolume = dVar9; + calcParams.mVolume = dVar9; JAUAudibleParam* params = Z2audible->getAudibleParam(); - if ((params->field_0x0.bytes.b1_2_7 & 8) && (local_60.mVolume <= 0.2f)) { - local_60.mVolume = 0.2f; + if ((params->field_0x0.bytes.mClampMinVolume & 8) && (calcParams.mVolume <= 0.2f)) { + calcParams.mVolume = 0.2f; } } else { - local_60.mVolume = 1.0f; + calcParams.mVolume = 1.0f; } - if (Z2audible->getAudibleParam()->field_0x0.bytes.b1_1 && mNumPlayers == 1) { - local_60.mDolby = calcRelPosDolby(*(Vec*)&channel->field_0x14.field_0x00, channelNum); + if (Z2audible->getAudibleParam()->field_0x0.bytes.mCalcDolby && mNumPlayers == 1) { + calcParams.mDolby = calcRelPosDolby(*(Vec*)&channel->mRelPos.mCameraRelative, channelNum); } else { - local_60.mDolby = 0.5f; + calcParams.mDolby = 0.5f; } - if (Z2audible->getAudibleParam()->field_0x0.bytes.b1_0) { + if (Z2audible->getAudibleParam()->field_0x0.bytes.mCalcPan) { if (mNumPlayers > 2) { f32 fVar1; if (channelNum & 1) { @@ -651,24 +652,24 @@ void Z2Audience::mixChannelOut(const JASSoundParams& outParams, JAIAudible* audi } else { fVar1 = 0.0f; } - local_60.mPan = fVar1; + calcParams.mPan = fVar1; } else { - local_60.mPan = calcRelPosPan(*(Vec*)&channel->field_0x14.field_0x00, channelNum); + calcParams.mPan = calcRelPosPan(*(Vec*)&channel->mRelPos.mCameraRelative, channelNum); } } else { - local_60.mPan = 0.5f; + calcParams.mPan = 0.5f; } - local_60.mPitch = calcPitch_(channel, Z2audible, &mAudioCamera[channelNum]); - f32 dVar9; - if (Z2audible->getAudibleParam()->field_0x0.bytes.b0_6) { - dVar9 = calcFxMix_(channel->field_0x14.field_0xC, distVolBit); + calcParams.mPitch = calcPitch_(channel, Z2audible, &mAudioCamera[channelNum]); + f32 fxMix; + if (Z2audible->getAudibleParam()->field_0x0.bytes.mCalcFxMix) { + fxMix = calcFxMix_(channel->mRelPos.mTrueDistance, distVolBit); } else { - dVar9 = 0.0f; + fxMix = 0.0f; } - local_60.mFxMix = dVar9; - local_60.clamp(); - Z2audible->setOuterParams(outParams, local_60, channelNum); + calcParams.mFxMix = fxMix; + calcParams.clamp(); + Z2audible->setOuterParams(outParams, calcParams, channelNum); } @@ -688,18 +689,18 @@ f32 Z2Audience::calcRelPosVolume(const Vec& param_0, f32 param_1, int camID) { aTStack_3c.y *= 1.5f; f32 len = aTStack_3c.length(); - if (len > mSetting.field_0x0[0] * param_1) { - return mSetting.field_0x40; + if (len > mSetting.mDistanceMaxes[0] * param_1) { + return mSetting.mMinDistanceVolume; } - if (len < mSetting.field_0x3c) { + if (len < mSetting.mMaxVolumeDistance) { return 1.0f; } return JGeometry::TUtil::clamp( - 1.0f + ((mSetting.field_0x40 - 1.0f) / - ((mSetting.field_0x0[0] * param_1) - mSetting.field_0x3c)) * - (len - mSetting.field_0x3c), + 1.0f + ((mSetting.mMinDistanceVolume - 1.0f) / + ((mSetting.mDistanceMaxes[0] * param_1) - mSetting.mMaxVolumeDistance)) * + (len - mSetting.mMaxVolumeDistance), 0.0f, 1.0f); } @@ -717,7 +718,7 @@ f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) { return 0.5f; } - dVar6 = (0.5f + mSetting.field_0x58 * (local_54.x / dVar6)); + dVar6 = (0.5f + mSetting.mPanFactor * (local_54.x / dVar6)); if (local_54.z <= 0.0f) { f32 fovySin = mAudioCamera[camID].getFovySin(); if (dVar6 < 0.5f) { @@ -745,64 +746,74 @@ 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) { + const auto mode = dusk::getSettings().audio.outputMode.getValue(); + if (mode >= dusk::AudioOutputMode::StereoHeadphones) { // 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; + f32 lenSq = param_0.x * param_0.x + param_0.z * param_0.z; + if (mode == dusk::AudioOutputMode::StereoHeadphones) { + // original HRTF math + lenSq += param_0.y * param_0.y; + } 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)); + if (mode == dusk::AudioOutputMode::StereoHeadphones) { + // original HRTF math + return 0.5f - 0.5f * cosf(t * static_cast(M_PI)); + } else { + return t; + } } #endif - if (fVar1 > mSetting.field_0x48) { + if (fVar1 > mSetting.mDolbyBehindDistanceMax) { return 1.0f; } - if (fVar1 < mSetting.field_0x44) { + if (fVar1 < mSetting.mDolbyFrontDistanceMax) { return 0.0f; } if (fVar1 < 0.0f) { return Z2Calc::getParamByExp( - fVar1, mSetting.field_0x44, 0.0f, + fVar1, mSetting.mDolbyFrontDistanceMax, 0.0f, 0.3f, 0.0f, - mSetting.field_0x4c, Z2Calc::CURVE_POSITIVE); + mSetting.mDolbyCenterValue, Z2Calc::CURVE_POSITIVE); } return Z2Calc::getParamByExp( - fVar1, 0.0f, mSetting.field_0x48, - 0.3f, mSetting.field_0x4c, + fVar1, 0.0f, mSetting.mDolbyBehindDistanceMax, + 0.3f, mSetting.mDolbyCenterValue, 1.0f, Z2Calc::CURVE_NEGATIVE); } f32 Z2Audience::calcVolume_(f32 param_0, int distVolBit) const { - if (param_0 > mSetting.field_0x0[distVolBit]) { - return mSetting.field_0x40; + if (param_0 > mSetting.mDistanceMaxes[distVolBit]) { + return mSetting.mMinDistanceVolume; } - if (param_0 < mSetting.field_0x3c) { + if (param_0 < mSetting.mMaxVolumeDistance) { return 1.0f; } - return JGeometry::TUtil::clamp((mSetting.field_0x70[distVolBit] * (param_0 - mSetting.field_0x3c)) + 1.0f, 0.0f, 1.0f); + return JGeometry::TUtil::clamp((mSetting.mVolumeFactor[distVolBit] * (param_0 - mSetting.mMaxVolumeDistance)) + 1.0f, 0.0f, 1.0f); } -u32 Z2Audience::calcDeltaPriority_(f32 param_0, int distVolBit, bool param_2) const { - if (param_0 > mSetting.field_0x0[distVolBit]) { - if (param_2) { +u32 Z2Audience::calcDeltaPriority_(f32 distance, int distVolBit, bool cullMaxDistance) const { + if (distance > mSetting.mDistanceMaxes[distVolBit]) { + if (cullMaxDistance) { return -1; } - return mSetting.field_0x64; + return mSetting.mMaxDistancePriority; } - if (param_0 < mSetting.field_0x3c) { + if (distance < mSetting.mMaxVolumeDistance) { return 0; } - return mSetting.field_0xac[distVolBit] * (param_0 - mSetting.field_0x3c); + return mSetting.mPriorityFactor[distVolBit] * (distance - mSetting.mMaxVolumeDistance); } f32 Z2Audience::calcPitchDoppler_(const JGeometry::TVec3& param_0, @@ -810,24 +821,24 @@ f32 Z2Audience::calcPitchDoppler_(const JGeometry::TVec3& param_0, const JGeometry::TVec3& param_2, f32 param_3) const { f32 dVar8 = param_0.dot(param_1); f32 dVar7 = param_0.dot(param_2); - return (mSetting.field_0x5c + param_3 * dVar8) / (mSetting.field_0x5c + param_3 * dVar7); + return (mSetting.mSonicSpeed + param_3 * dVar8) / (mSetting.mSonicSpeed + param_3 * dVar7); } -f32 Z2Audience::calcFxMix_(f32 param_0, int distVolBit) const { - if (param_0 > mSetting.field_0x0[distVolBit]) { - return mSetting.field_0x54; +f32 Z2Audience::calcFxMix_(f32 distance, int distVolBit) const { + if (distance > mSetting.mDistanceMaxes[distVolBit]) { + return mSetting.mMaxDistanceFxMix; } - if (param_0 < mSetting.field_0x3c) { - return mSetting.field_0x50; + if (distance < mSetting.mMaxVolumeDistance) { + return mSetting.mMinDistanceFxMix; } - return mSetting.field_0x50 + mSetting.field_0xe8[distVolBit] * (param_0 - mSetting.field_0x3c); + return mSetting.mMinDistanceFxMix + mSetting.mFxMixFactor[distVolBit] * (distance - mSetting.mMaxVolumeDistance); } f32 Z2Audience::calcPitch_(Z2AudibleChannel* channel, const Z2Audible* audible, const Z2AudioCamera* camera) const { JAUAudibleParam audParam = *audible->getAudibleParam(); - if (audParam.field_0x0.bytes.b0_0) { + if (audParam.field_0x0.bytes.mDopplerPower) { JGeometry::TVec3 aTStack_4c; - aTStack_4c.normalize(channel->field_0x14.field_0x00); + aTStack_4c.normalize(channel->mRelPos.mCameraRelative); JAUAudibleParam audParam = *audible->getAudibleParam(); f32 doppler = audParam.getDopplerPower(); diff --git a/src/Z2AudioLib/Z2SceneMgr.cpp b/src/Z2AudioLib/Z2SceneMgr.cpp index 49c0ed5b61..d8f52ce0de 100644 --- a/src/Z2AudioLib/Z2SceneMgr.cpp +++ b/src/Z2AudioLib/Z2SceneMgr.cpp @@ -1642,7 +1642,7 @@ void Z2SceneMgr::setSceneName(char* spot, s32 room, s32 layer) { JSUList* stream_list = Z2GetSoundMgr()->getStreamMgr()->getStreamList(); JSULink* stream; for (stream = stream_list->getFirst(); stream != NULL; stream = stream->getNext()) { - if (bVar2 || sound_table->getTypeID(stream->getObject()->getID()) != 0x71) { + if (bVar2 || sound_table->getTypeID(stream->getObject()->getID() IF_DUSK_ARG(stream->getObject()->getReplacement())) != SOUND_TYPEID_STREAM) { stream->getObject()->stop(Z2Param::SCENE_CHANGE_BGM_FADEOUT_TIME); } } diff --git a/src/Z2AudioLib/Z2SoundHandles.cpp b/src/Z2AudioLib/Z2SoundHandles.cpp index de7e265ae7..99e11e0784 100644 --- a/src/Z2AudioLib/Z2SoundHandles.cpp +++ b/src/Z2AudioLib/Z2SoundHandles.cpp @@ -89,7 +89,7 @@ Z2SoundHandlePool* Z2SoundHandles::getLowPrioSound(JAISoundID soundID) { return handle; } - u32 prio = soundInfo->getPriority((*handle)->getID()); + u32 prio = soundInfo->getPriority((*handle)->getID() IF_DUSK_ARG((*handle)->getReplacement())); if (prio < low_prio) { low_prio = prio; rethandle = handle; @@ -102,7 +102,7 @@ Z2SoundHandlePool* Z2SoundHandles::getLowPrioSound(JAISoundID soundID) { } #endif - if (soundInfo->getPriority(soundID) >= low_prio) { + if (soundInfo->getPriority(soundID IF_DUSK_ARG(nullptr)) >= low_prio) { return rethandle; } diff --git a/src/Z2AudioLib/Z2SoundInfo.cpp b/src/Z2AudioLib/Z2SoundInfo.cpp index cb67b4c805..394034e322 100644 --- a/src/Z2AudioLib/Z2SoundInfo.cpp +++ b/src/Z2AudioLib/Z2SoundInfo.cpp @@ -1,11 +1,13 @@ #include "Z2AudioLib/Z2SoundInfo.h" -#include "JSystem/JAudio2/JAISeq.h" + #include "JSystem/JAudio2/JAISe.h" +#include "JSystem/JAudio2/JAISeq.h" #include "JSystem/JAudio2/JAISoundChild.h" #include "JSystem/JAudio2/JAIStream.h" #include "JSystem/JAudio2/JAUSoundTable.h" #include "JSystem/JUtility/JUTAssert.h" #include "Z2AudioLib/Z2Calc.h" +#include "dusk/mods/svc/audio_res/bst.hpp" struct JAUStdSoundTableType { static DUSK_GAME_DATA const u32 STRM_CH_SHIFT; @@ -18,12 +20,12 @@ struct JAUStdSoundTableType { u16 Z2SoundInfo::getBgmSeqResourceID(JAISoundID soundID) const { JUT_ASSERT(20, isValid()); - JAUSoundTableItem* data = JASGlobalInstance::getInstance()->getData(soundID); - u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID); + JAUSoundTableItem DUSK_CONST* data = JASGlobalInstance::getInstance()->getData(soundID IF_DUSK_ARG(nullptr)); + u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID IF_DUSK_ARG(nullptr)); if (data != NULL) { switch ((typeID & 0xf0)) { - case 0x60: + case SOUND_TYPEID_SEQUENCE: return (u16)data->mResourceId; } } @@ -48,83 +50,101 @@ int Z2SoundInfo::getCategory(JAISoundID soundID) const { return soundID.id_.info.type.parts.groupID; } -u32 Z2SoundInfo::getPriority(JAISoundID soundID) const { +u32 Z2SoundInfo::getPriority(JAISoundID soundID IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) const { JUT_ASSERT(63, isValid()); - JAUSoundTableItem* data = JASGlobalInstance::getInstance()->getData(soundID); - u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID); - if (data != NULL && (typeID & 0x40) != 0) { +#if TARGET_PC + // This function gets called at least once in a context where we can't easily pass in the replacement. + // So check it here too. + // It's only a tiny priority difference, highly unlikely to cause any issues. + std::shared_ptr replacement_shared; + if (!replacement) { + replacement_shared = dusk::mods::svc::audio_res::bst::get_override_for(soundID); + replacement = replacement_shared.get(); + } +#endif + + JAUSoundTableItem DUSK_CONST* data = JASGlobalInstance::getInstance()->getData(soundID IF_DUSK_ARG(replacement)); + u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID IF_DUSK_ARG(replacement)); + + if (data != NULL && (typeID & SOUND_TYPEID_VALID) != 0) { return data->mPriority; } return 0; } -JAUAudibleParam Z2SoundInfo::getAudibleSwFull(JAISoundID soundID) { +#define SW_BIT DUSK_IF_ELSE(sw_bit, getSwBit(soundID)) + +JAUAudibleParam Z2SoundInfo::getAudibleSwFull(JAISoundID soundID IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) { JAUAudibleParam audibleParam; JUT_ASSERT(82, isValid()); int iVar1, uVar7; - u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID); + IF_DUSK(int sw_bit;) + + u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID IF_DUSK_ARG(replacement)); switch (typeID) { - case 81: - audibleParam.field_0x0.bytes.b0_0 = (u32)getSwBit(soundID) >> 8; - if ((getSwBit(soundID) & 1) != 0) { - audibleParam.field_0x0.bytes.b0_4 = 0; + case SOUND_TYPEID_SOUND_EFFECT: + IF_DUSK(sw_bit = getSwBit(soundID, replacement);) + + audibleParam.field_0x0.bytes.mDopplerPower = (u32)SW_BIT >> SOUND_SW_DOPPLER_POWER_OFFSET; + if ((SW_BIT & SOUND_SW_ALWAYS_MAX_PRIORITY) != 0) { + audibleParam.field_0x0.bytes.mCalculatePriority = 0; } else { - audibleParam.field_0x0.bytes.b0_4 = 1; + audibleParam.field_0x0.bytes.mCalculatePriority = 1; } - if ((getSwBit(soundID) & 2) != 0) { - audibleParam.field_0x0.bytes.b0_5 = 0; + if ((SW_BIT & SOUND_SW_IGNORE_DISTANCE_VOL) != 0) { + audibleParam.field_0x0.bytes.mCalcDistanceVolume = 0; } else { - audibleParam.field_0x0.bytes.b0_5 = 1; + audibleParam.field_0x0.bytes.mCalcDistanceVolume = 1; } - if ((getSwBit(soundID) & 4) != 0) { - audibleParam.field_0x0.bytes.b0_6 = 0; + if ((SW_BIT & SOUND_SW_IGNORE_FX_MIX) != 0) { + audibleParam.field_0x0.bytes.mCalcFxMix = 0; } else { - audibleParam.field_0x0.bytes.b0_6 = 1; + audibleParam.field_0x0.bytes.mCalcFxMix = 1; } - if ((getSwBit(soundID) & 0x800000) != 0) { - audibleParam.field_0x0.bytes.b0_7 = 1; + if ((SW_BIT & SOUND_SW_CULL_AT_MAX_DISTANCE) != 0) { + audibleParam.field_0x0.bytes.mCullAtMaxDistance = 1; } else { - audibleParam.field_0x0.bytes.b0_7 = 0; + audibleParam.field_0x0.bytes.mCullAtMaxDistance = 0; } - if ((getSwBit(soundID) & 0x1000) != 0) { - audibleParam.field_0x0.bytes.b1_0 = 0; + if ((SW_BIT & SOUND_SW_IGNORE_PAN) != 0) { + audibleParam.field_0x0.bytes.mCalcPan = 0; } else { - audibleParam.field_0x0.bytes.b1_0 = 1; + audibleParam.field_0x0.bytes.mCalcPan = 1; } - if ((getSwBit(soundID) & 0x2000) != 0) { - audibleParam.field_0x0.bytes.b1_1 = 0; + if ((SW_BIT & SOUND_SW_IGNORE_DOLBY) != 0) { + audibleParam.field_0x0.bytes.mCalcDolby = 0; } else { - audibleParam.field_0x0.bytes.b1_1 = 1; + audibleParam.field_0x0.bytes.mCalcDolby = 1; } uVar7 = 0; - if ((getSwBit(soundID) & 0x80000) != 0) { + if ((SW_BIT & SOUND_SW_CLAMP_MIN_VOLUME) != 0) { uVar7 = 8; } - iVar1 = (getSwBit(soundID) >> 16) & 0x7; - iVar1 += (getSwBit(soundID) >> 16) & 0x70; - iVar1 += (getSwBit(soundID) >> 16) & 0xf00; - audibleParam.field_0x0.bytes.b1_2_7 = uVar7; + iVar1 = (SW_BIT >> 16) & SOUND_VOL_DIST_BIT_MASK_SHIFTED; + iVar1 += (SW_BIT >> 16) & SOUND_VOL_DIST_BIT_2_MASK_SHIFTED; + iVar1 += (SW_BIT >> 16) & SOUND_VOL_SOMETHING_MASK_SHIFTED; + audibleParam.field_0x0.bytes.mClampMinVolume = uVar7; audibleParam.field_0x0.half.f1 = iVar1; break; default: - audibleParam.field_0x0.bytes.b0_0 = 0; - audibleParam.field_0x0.bytes.b0_4 = 1; - audibleParam.field_0x0.bytes.b0_5 = 1; - audibleParam.field_0x0.bytes.b0_6 = 1; - audibleParam.field_0x0.bytes.b0_7 = 0; - audibleParam.field_0x0.bytes.b1_0 = 1; - audibleParam.field_0x0.bytes.b1_1 = 1; - audibleParam.field_0x0.bytes.b1_2_7 = 0; + audibleParam.field_0x0.bytes.mDopplerPower = 0; + audibleParam.field_0x0.bytes.mCalculatePriority = 1; + audibleParam.field_0x0.bytes.mCalcDistanceVolume = 1; + audibleParam.field_0x0.bytes.mCalcFxMix = 1; + audibleParam.field_0x0.bytes.mCullAtMaxDistance = 0; + audibleParam.field_0x0.bytes.mCalcPan = 1; + audibleParam.field_0x0.bytes.mCalcDolby = 1; + audibleParam.field_0x0.bytes.mClampMinVolume = 0; audibleParam.field_0x0.half.f1 = 0; break; } @@ -132,60 +152,64 @@ JAUAudibleParam Z2SoundInfo::getAudibleSwFull(JAISoundID soundID) { return audibleParam; } -u16 Z2SoundInfo::getAudibleSw(JAISoundID soundID) const { +u16 Z2SoundInfo::getAudibleSw(JAISoundID soundID IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) const { JAUAudibleParam audibleParam; JUT_ASSERT(184, isValid()); int iVar1, uVar7; - u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID); + IF_DUSK(int sw_bit); + + u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID IF_DUSK_ARG(replacement)); switch (typeID) { - case 81: - audibleParam.field_0x0.bytes.b0_0 = (u32)getSwBit(soundID) >> 8; - if ((getSwBit(soundID) & 1) != 0) { - audibleParam.field_0x0.bytes.b0_4 = 0; + case SOUND_TYPEID_SOUND_EFFECT: + IF_DUSK(sw_bit = getSwBit(soundID, replacement);) + + audibleParam.field_0x0.bytes.mDopplerPower = (u32)SW_BIT >> SOUND_SW_DOPPLER_POWER_OFFSET; + if ((SW_BIT & SOUND_SW_ALWAYS_MAX_PRIORITY) != 0) { + audibleParam.field_0x0.bytes.mCalculatePriority = 0; } else { - audibleParam.field_0x0.bytes.b0_4 = 1; + audibleParam.field_0x0.bytes.mCalculatePriority = 1; } - if ((getSwBit(soundID) & 2) != 0) { - audibleParam.field_0x0.bytes.b0_5 = 0; + if ((SW_BIT & SOUND_SW_IGNORE_DISTANCE_VOL) != 0) { + audibleParam.field_0x0.bytes.mCalcDistanceVolume = 0; } else { - audibleParam.field_0x0.bytes.b0_5 = 1; + audibleParam.field_0x0.bytes.mCalcDistanceVolume = 1; } - if ((getSwBit(soundID) & 4) != 0) { - audibleParam.field_0x0.bytes.b0_6 = 0; + if ((SW_BIT & SOUND_SW_IGNORE_FX_MIX) != 0) { + audibleParam.field_0x0.bytes.mCalcFxMix = 0; } else { - audibleParam.field_0x0.bytes.b0_6 = 1; + audibleParam.field_0x0.bytes.mCalcFxMix = 1; } - if ((getSwBit(soundID) & 0x800000) != 0) { - audibleParam.field_0x0.bytes.b0_7 = 1; + if ((SW_BIT & SOUND_SW_CULL_AT_MAX_DISTANCE) != 0) { + audibleParam.field_0x0.bytes.mCullAtMaxDistance = 1; } else { - audibleParam.field_0x0.bytes.b0_7 = 0; + audibleParam.field_0x0.bytes.mCullAtMaxDistance = 0; } - if ((getSwBit(soundID) & 0x1000) != 0) { - audibleParam.field_0x0.bytes.b1_0 = 0; + if ((SW_BIT & SOUND_SW_IGNORE_PAN) != 0) { + audibleParam.field_0x0.bytes.mCalcPan = 0; } else { - audibleParam.field_0x0.bytes.b1_0 = 1; + audibleParam.field_0x0.bytes.mCalcPan = 1; } - if ((getSwBit(soundID) & 0x2000) != 0) { - audibleParam.field_0x0.bytes.b1_1 = 0; + if ((SW_BIT & SOUND_SW_IGNORE_DOLBY) != 0) { + audibleParam.field_0x0.bytes.mCalcDolby = 0; } else { - audibleParam.field_0x0.bytes.b1_1 = 1; + audibleParam.field_0x0.bytes.mCalcDolby = 1; } uVar7 = 0; - if ((getSwBit(soundID) & 0x80000) != 0) { + if ((SW_BIT & SOUND_SW_CLAMP_MIN_VOLUME) != 0) { uVar7 = 8; } - iVar1 = (getSwBit(soundID) >> 16) & 0x7; - iVar1 += (getSwBit(soundID) >> 16) & 0x70; - iVar1 += (getSwBit(soundID) >> 16) & 0xf00; - audibleParam.field_0x0.bytes.b1_2_7 = uVar7; + iVar1 = (SW_BIT >> 16) & SOUND_VOL_DIST_BIT_MASK_SHIFTED; + iVar1 += (SW_BIT >> 16) & SOUND_VOL_DIST_BIT_2_MASK_SHIFTED; + iVar1 += (SW_BIT >> 16) & SOUND_VOL_SOMETHING_MASK_SHIFTED; + audibleParam.field_0x0.bytes.mClampMinVolume = uVar7; audibleParam.field_0x0.half.f1 = iVar1; break; default: @@ -197,30 +221,32 @@ u16 Z2SoundInfo::getAudibleSw(JAISoundID soundID) const { return audibleParam.field_0x0.half.f0; } -void Z2SoundInfo::getSeInfo(JAISoundID soundID, JAISe* sePtr) const { +#undef SW_BIT + +void Z2SoundInfo::getSeInfo(JAISoundID soundID, JAISe* sePtr IF_DUSK_ARG(SoundEffectReplacementSlot const* replacement)) const { getSoundInfo_(soundID, sePtr); JUT_ASSERT(292, isValid()); - JAUSoundTableItem* data = JASGlobalInstance::getInstance()->getData(soundID); - u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID); + JAUSoundTableItem DUSK_CONST* data = JASGlobalInstance::getInstance()->getData(soundID IF_DUSK_ARG(replacement)); + u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID IF_DUSK_ARG(replacement)); if (data == NULL) { return; } switch(typeID) { - case 81: - sePtr->getProperty().field_0x8 *= data->field_0x8; - u32 uStack_6c = (getSwBit(soundID) & 0xf0) >> 4; - if (uStack_6c > 8) { - sePtr->getProperty().field_0x8 += Z2Calc::linearTransform(uStack_6c, 8.0f, 15.0f, 16.0f, 24.0f, true) / 48.0f * Z2Calc::getRandom_0_1(); + case SOUND_TYPEID_SOUND_EFFECT: + sePtr->getProperty().mPitch *= data->mPitch; + u32 pitchParam = (getSwBit(soundID IF_DUSK_ARG(replacement)) & SOUND_SW_RANDOM_PITCH_MASK) >> SOUND_SW_RANDOM_PITCH_OFFSET; + if (pitchParam > 8) { + sePtr->getProperty().mPitch += Z2Calc::linearTransform(pitchParam, 8.0f, 15.0f, 16.0f, 24.0f, true) / 48.0f * Z2Calc::getRandom_0_1(); } else { - sePtr->getProperty().field_0x8 += (uStack_6c / 48.0f) * Z2Calc::getRandom_0_1(); + sePtr->getProperty().mPitch += (pitchParam / 48.0f) * Z2Calc::getRandom_0_1(); } - u32 uVar1 = (u32)getSwBit(soundID) >> 0x1c; + u32 uVar1 = (u32)getSwBit(soundID IF_DUSK_ARG(replacement)) >> SOUND_SW_RANDOM_VOLUME_OFFSET; if (uVar1 != 0) { f32 dVar18 = (uVar1 / 15.0f) * Z2Calc::getRandom_0_1(); - sePtr->getProperty().field_0x0 -= dVar18 < 0.0f ? 0.0f : (dVar18 > 1.0f ? 1.0f : dVar18); + sePtr->getProperty().mVolume -= dVar18 < 0.0f ? 0.0f : (dVar18 > 1.0f ? 1.0f : dVar18); } break; } @@ -230,23 +256,23 @@ void Z2SoundInfo::getSeqInfo(JAISoundID soundID, JAISeq* seqPtr) const { getSoundInfo_(soundID, seqPtr); } -DUSK_GAME_DATA const u32 JAUStdSoundTableType::STRM_CH_SHIFT = 2; +DUSK_GAME_DATA const u32 JAUStdSoundTableType::STRM_CH_SHIFT = STRM_CH_SHIFT_; -void Z2SoundInfo::getStreamInfo(JAISoundID soundID, JAIStream* streamPtr) const { +void Z2SoundInfo::getStreamInfo(JAISoundID soundID, JAIStream* streamPtr IF_DUSK_ARG(StreamReplacementSlot const* replacement)) const { int numChild; - JAUSoundTableItem* data; + JAUSoundTableItem DUSK_CONST* data; getSoundInfo_(soundID, streamPtr); JUT_ASSERT(349, isValid()); - u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID); + u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID IF_DUSK_ARG(replacement)); switch (typeID & 0xf0) { - case 0x70: + case SOUND_TYPEID_STREAM: u16 uVar1; s32 iVar4; - data = JASGlobalInstance::getInstance()->getData(soundID); + data = JASGlobalInstance::getInstance()->getData(soundID IF_DUSK_ARG(replacement)); JUT_ASSERT(356, data); - uVar1 = data->mResourceId; + uVar1 = data->mStreamPanParameters; numChild = streamPtr->getNumChild(); iVar4 = 0; for (; iVar4 < numChild && uVar1 != 0; uVar1 >>= JAUStdSoundTableType::STRM_CH_SHIFT, iVar4++) { @@ -255,13 +281,13 @@ void Z2SoundInfo::getStreamInfo(JAISoundID soundID, JAIStream* streamPtr) const JAISoundChild* child = streamPtr->getChild(iVar4); if (child != NULL) { switch (uVar2) { - case 1: + case STRM_CH_CENTER: child->mMove.params_.mPan = 0.5f; break; - case 2: + case STRM_CH_LEFT: child->mMove.params_.mPan = 0.0f; break; - case 3: + case STRM_CH_RIGHT: child->mMove.params_.mPan = 1.0f; break; } @@ -271,37 +297,43 @@ void Z2SoundInfo::getStreamInfo(JAISoundID soundID, JAIStream* streamPtr) const } } -const char* Z2SoundInfo::getStreamFilePath(JAISoundID soundID) { +const char* Z2SoundInfo::getStreamFilePath(JAISoundID soundID IF_DUSK_ARG(StreamReplacementSlot const* replacement)) { JUT_ASSERT(387, isValid()); - JAUSoundTableItem* data; + JAUSoundTableItem DUSK_CONST* data; const void* resource; - switch (JASGlobalInstance::getInstance()->getTypeID(soundID) & 0xf0) { - case 0x70: - data = JASGlobalInstance::getInstance()->getData(soundID); + switch (JASGlobalInstance::getInstance()->getTypeID(soundID IF_DUSK_ARG(replacement)) & 0xf0) { + case SOUND_TYPEID_STREAM: +#if TARGET_PC + if (replacement) { + return replacement->file_path.c_str(); + } +#endif + + data = JASGlobalInstance::getInstance()->getData(soundID IF_DUSK_ARG(replacement)); JUT_ASSERT(394, data); resource = JASGlobalInstance::getInstance()->getResource(); JUT_ASSERT(398, resource); - return JAUStdSoundTableType::StringOffset::getString(resource, data->field_0x4); + return JAUStdSoundTableType::StringOffset::getString(resource, data->mStreamFilePath); default: return NULL; } } -s32 Z2SoundInfo::getStreamFileEntry(JAISoundID soundID) { - const char* path = getStreamFilePath(soundID); +s32 Z2SoundInfo::getStreamFileEntry(JAISoundID soundID IF_DUSK_ARG(StreamReplacementSlot const* replacement)) { + const char* path = getStreamFilePath(soundID IF_DUSK_ARG(replacement)); return !path ? -1 : DVDConvertPathToEntrynum(path); } -int Z2SoundInfo::getSwBit(JAISoundID soundID) const { +int Z2SoundInfo::getSwBit(JAISoundID soundID IF_DUSK_ARG(SoundTableReplacementSlot const* replacement)) const { JUT_ASSERT(418, isValid()); - JAUSoundTableItem* data = JASGlobalInstance::getInstance()->getData(soundID); + JAUSoundTableItem DUSK_CONST* data = JASGlobalInstance::getInstance()->getData(soundID IF_DUSK_ARG(replacement)); - u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID); + u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID IF_DUSK_ARG(replacement)); if (data != NULL) { switch(typeID) { - case 81: - return data->field_0x4; + case SOUND_TYPEID_SOUND_EFFECT: + return data->mSwBit; } } @@ -310,10 +342,10 @@ int Z2SoundInfo::getSwBit(JAISoundID soundID) const { void Z2SoundInfo::getSoundInfo_(JAISoundID soundID, JAISound* soundPtr) const { JUT_ASSERT(440, isValid()); - JAUSoundTableItem* data = JASGlobalInstance::getInstance()->getData(soundID); + JAUSoundTableItem DUSK_CONST* data = JASGlobalInstance::getInstance()->getData(soundID IF_DUSK_ARG(soundPtr->getReplacement())); - u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID); - if (data != NULL && (typeID & 0x40) != 0) { - soundPtr->getProperty().field_0x0 = (1.0f / 127.0f) * data->field_0x1; + u8 typeID = JASGlobalInstance::getInstance()->getTypeID(soundID IF_DUSK_ARG(soundPtr->getReplacement())); + if (data != NULL && (typeID & SOUND_TYPEID_VALID) != 0) { + soundPtr->getProperty().mVolume = (1.0f / 127.0f) * data->mVolume; } } diff --git a/src/Z2AudioLib/Z2SoundMgr.cpp b/src/Z2AudioLib/Z2SoundMgr.cpp index 6978a61585..43bd6c7d5c 100644 --- a/src/Z2AudioLib/Z2SoundMgr.cpp +++ b/src/Z2AudioLib/Z2SoundMgr.cpp @@ -1,14 +1,15 @@ #include "Z2AudioLib/Z2SoundMgr.h" -#include "Z2AudioLib/Z2Param.h" -#include "Z2AudioLib/Z2SceneMgr.h" -#include "Z2AudioLib/Z2SpeechMgr2.h" -#include "Z2AudioLib/Z2SeqMgr.h" -#include "Z2AudioLib/Z2SeMgr.h" -#include "Z2AudioLib/Z2SoundInfo.h" #include "JSystem/JAudio2/JASCalc.h" #include "JSystem/JAudio2/JASDriverIF.h" #include "JSystem/JAudio2/JAUSectionHeap.h" +#include "Z2AudioLib/Z2Param.h" +#include "Z2AudioLib/Z2SceneMgr.h" +#include "Z2AudioLib/Z2SeMgr.h" +#include "Z2AudioLib/Z2SeqMgr.h" +#include "Z2AudioLib/Z2SoundInfo.h" +#include "Z2AudioLib/Z2SpeechMgr2.h" #include "d/d_com_inf_game.h" +#include "dusk/mods/svc/audio_res/bst.hpp" #if PLATFORM_WII || PLATFORM_SHIELD #include "Z2AudioCS/Z2AudioCS.h" @@ -113,19 +114,25 @@ Z2SoundMgr::Z2SoundMgr() : seqMgr_(true), streamMgr_(true) { - soundID_.setAnonymous(); + bgmMuter.setAnonymous(); } bool Z2SoundMgr::startSound(JAISoundID soundID, JAISoundHandle* handle, const JGeometry::TVec3* posPtr) { DUSK_AUDIO_SKIP(true); + IF_DUSK(using namespace dusk::mods::svc::audio_res::bst); int soundType = Z2GetSoundInfo()->getSoundType(soundID); switch (soundType) { case 0: { - if (Z2GetSoundInfo()->getSwBit(soundID) & 8) { + #if TARGET_PC + { + auto replace_se = get_override_for_se(soundID); + #endif + + if (Z2GetSoundInfo()->getSwBit(soundID IF_DUSK_ARG(replace_se.get())) & SOUND_SW_MUTE_BGM) { OS_REPORT("[Z2SoundMgr::startSound] se seq Mute! id = %08x\n", *(u32*)&soundID); Z2GetSeqMgr()->bgmAllMute(3, 0.3f); - soundID_ = soundID; + bgmMuter = soundID; } #if PLATFORM_WII || PLATFORM_SHIELD @@ -136,15 +143,16 @@ bool Z2SoundMgr::startSound(JAISoundID soundID, JAISoundHandle* handle, const JG } #endif - bool result = seMgr_.startSound(soundID, handle, posPtr); + bool result = seMgr_.startSound(soundID, handle, posPtr IF_DUSK_ARG(std::move(replace_se))); return result; + IF_DUSK_BLOCK_END } case 1: if (soundID == Z2BGM_LUTERA_DEMO) { Z2GetSeqMgr()->bgmStart(Z2BGM_LUTERA2, 0, 0); Z2GetSeqMgr()->unMuteSceneBgm(0); Z2GetSeqMgr()->changeBgmStatus(0); - return seMgr_.startSound(Z2SE_NO_SOUND, handle, posPtr); + return seMgr_.startSound(Z2SE_NO_SOUND, handle, posPtr IF_DUSK_ARG(nullptr)); } else { bool loaded = false; JAUSectionHeap* sectionHeap = JASGlobalInstance::getInstance(); @@ -172,15 +180,21 @@ bool Z2SoundMgr::startSound(JAISoundID soundID, JAISoundHandle* handle, const JG return seqMgr_.startSound(soundID, handle, posPtr); } case 2: +#if TARGET_PC + { + auto replace_stream = get_override_for_stream(soundID); +#endif + if (soundID == 0x2000001 || soundID == 0x200004d) { streamMgr_.stop(180); - return seMgr_.startSound(Z2SE_NO_SOUND, handle, posPtr); + return seMgr_.startSound(Z2SE_NO_SOUND, handle, posPtr IF_DUSK_ARG(nullptr)); } if (soundID == 0x2000005) { seMgr_.getCategory(9)->pause(false); seMgr_.getCategory(9)->getParams()->moveVolume(1.0f, 45); } - return streamMgr_.startSound(soundID, handle, posPtr); + return streamMgr_.startSound(soundID, handle, posPtr IF_DUSK_ARG(std::move(replace_stream))); + IF_DUSK_BLOCK_END default: char error[64]; SAFE_SPRINTF(error, "Unknown Sound-Type id :%08x\n", (u32)soundID); @@ -192,11 +206,11 @@ bool Z2SoundMgr::startSound(JAISoundID soundID, JAISoundHandle* handle, const JG void Z2SoundMgr::calc() { seMgr_.calc(); - if (!soundID_.isAnonymous() && !isPlayingSoundID(soundID_)) { + if (!bgmMuter.isAnonymous() && !isPlayingSoundID(bgmMuter)) { if (Z2GetSceneMgr()->isSceneExist()) { Z2GetSeqMgr()->bgmAllUnMute(3); } - soundID_.setAnonymous(); + bgmMuter.setAnonymous(); } seqMgr_.calc(); streamMgr_.calc(); diff --git a/src/Z2AudioLib/Z2SoundObject.cpp b/src/Z2AudioLib/Z2SoundObject.cpp index 23e8171736..c45a638b3c 100644 --- a/src/Z2AudioLib/Z2SoundObject.cpp +++ b/src/Z2AudioLib/Z2SoundObject.cpp @@ -70,7 +70,7 @@ void Z2SoundObjBase::dispose() { for (i = getFirst(); i != NULL; i = i->getNext()) { handle = i->getObject(); if (handle != NULL && *handle) { - if ((Z2GetSoundInfo()->getSwBit((*handle)->getID()) & 0x8000) != 0) { + if ((Z2GetSoundInfo()->getSwBit((*handle)->getID() IF_DUSK_ARG((*handle)->getReplacement())) & SOUND_SW_POOL_FLAG_2) != 0) { handle->releaseSound(); } else { (*handle)->stop(); @@ -82,7 +82,7 @@ void Z2SoundObjBase::dispose() { } bool Z2SoundObjBase::stopOK(Z2SoundHandlePool& handlePool) { - if ((Z2GetSoundInfo()->getSwBit(handlePool->getID()) & 0x8000) != 0) { + if ((Z2GetSoundInfo()->getSwBit(handlePool->getID() IF_DUSK_ARG(handlePool->getReplacement())) & SOUND_SW_POOL_FLAG_2) != 0) { return false; } else { return true; @@ -112,7 +112,7 @@ Z2SoundHandlePool* Z2SoundObjBase::startSound(JAISoundID soundID, u32 mapinfo, s Z2SoundHandlePool* handle = getHandleSoundID(soundID); if (handle != NULL) { - if ((Z2GetSoundInfo()->getSwBit(soundID) & 0x4000) != 0) { + if ((Z2GetSoundInfo()->getSwBit(soundID IF_DUSK_ARG((*handle)->getReplacement())) & SOUND_SW_POOL_FLAG_1) != 0) { handle = NULL; } else { return NULL; @@ -222,7 +222,7 @@ Z2SoundHandlePool* Z2SoundObjBase::startCollisionSE(u32 hitID, u32 mapinfo, Z2So if (30 <= mapinfo && mapinfo <= 52) { Z2Audible* audible = (Z2Audible*)(*handle)->getAudible(); if (audible != NULL) { - audible->getAudibleParam()->field_0x0.bytes.b1_2_7 = 8; + audible->getAudibleParam()->field_0x0.bytes.mClampMinVolume = 8; } } } diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 4d1bf182db..ef4fdca240 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -53,11 +53,24 @@ #if TARGET_PC #include "dusk/action_bindings.h" -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/sight.h" #include "dusk/settings.h" #include "res/Object/Alink.h" #include #include + +static const int IRON_BALL_CHAIN_COUNT = 102; +static const int HS_CHAIN_ANCHOR_COUNT = 4; + +namespace { +struct AlinkInterp { + dusk::interp::Samples ib_pos; + dusk::interp::Samples ib_angle; + dusk::interp::Samples ib_hand; + dusk::interp::Samples hs_chain; +}; +} // namespace #endif static int daAlink_Create(fopAc_ac_c* i_this); @@ -4313,9 +4326,7 @@ int daAlink_c::createHeap() { return 0; } - IF_DUSK(mFaceBckHeap.reserveBuffer(dRes_ID_ALANM_BCK_FAT_e);) - JKRReadIdxResource(mFaceBckHeap.getBuffer(), DUSK_IF_ELSE(mFaceBckHeap.getBufferSize(), 0xC00), - dRes_ID_ALANM_BCK_FAT_e, dComIfGp_getAnmArchive()); + JKRReadIdxResource(mFaceBckHeap.getBuffer(), 0xC00, dRes_ID_ALANM_BCK_FAT_e, dComIfGp_getAnmArchive()); J3DAnmTransform* bck = (J3DAnmTransform*)J3DAnmLoaderDataBase::load(mFaceBckHeap.getBuffer()); if (!mFaceBck.init(bck, FALSE, J3DFrameCtrl::EMode_LOOP, 1.0f, 0, -1, false)) { return 0; @@ -5993,7 +6004,7 @@ void daAlink_c::setItemMatrix(int param_0) { mDoMtx_stack_c::XrotS(-0x8000); #ifdef TARGET_PC - if (dusk::frame_interp::is_enabled()) { + if (dusk::interp::is_enabled()) { Mtx boot_mtx; mDoMtx_concat(mpLinkModel->getAnmMtx(0x18), mDoMtx_stack_c::get(), boot_mtx); mpLinkBootModels[1]->setAnmMtx(1, boot_mtx); @@ -14309,11 +14320,7 @@ BOOL daAlink_c::checkMagicArmorWearAbility() const { J3DModelData* daAlink_c::loadAramBmd(u16 i_resIdx, u32 i_bufSize) { JKRArchive* anmArchive = dComIfGp_getAnmArchive(); -#if TARGET_PC - u8* tmpBuffer = (u8*)mItemHeap[field_0x2fa0].allocTempBuffer(i_resIdx, &i_bufSize); -#else u8* tmpBuffer = JKR_NEW_ARRAY_ARGS(u8, i_bufSize, 0x20); -#endif JKRReadIdxResource(tmpBuffer, i_bufSize, i_resIdx, anmArchive); #if DEBUG @@ -14334,11 +14341,7 @@ J3DModelData* daAlink_c::loadAramBmd(u16 i_resIdx, u32 i_bufSize) { } void* daAlink_c::loadAram(u16 i_resIdx, u32 i_bufSize) { -#if TARGET_PC - u8* tmpBuffer = (u8*)mItemHeap[field_0x2fa0].allocTempBuffer(i_resIdx, &i_bufSize); -#else u8* tmpBuffer = JKR_NEW_ARRAY_ARGS(u8, i_bufSize, 0x20); -#endif JKRReadIdxResource(tmpBuffer, i_bufSize, i_resIdx, dComIfGp_getAnmArchive()); #if DEBUG daPy_aramBufferCheck(tmpBuffer, i_bufSize); @@ -14816,10 +14819,13 @@ void daAlink_c::deleteEquipItem(BOOL i_isPlaySound, BOOL i_isDeleteKantera) { mIronBallChainAngle = NULL; field_0x3848 = NULL; #if TARGET_PC - mIBChainInterpPrevValid = false; - mIBChainInterpCurrValid = false; - mHsChainInterpPrevValid = false; - mHsChainInterpCurrValid = false; + { + auto& interp = dusk::interp::get(this); + interp.ib_pos.reset(); + interp.ib_angle.reset(); + interp.ib_hand.reset(); + interp.hs_chain.reset(); + } #endif field_0x0774 = NULL; field_0x0778 = NULL; @@ -19792,36 +19798,21 @@ int daAlink_c::draw() { dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0); #if TARGET_PC - if (dusk::frame_interp::is_enabled()) { + if (dusk::interp::should_capture()) { + auto& interp = dusk::interp::get(this); if (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); + interp.ib_pos.capture(mIronBallChainPos, IRON_BALL_CHAIN_COUNT); + interp.ib_angle.capture(mIronBallChainAngle, IRON_BALL_CHAIN_COUNT); + interp.ib_hand.capture(&mHookshotTopPos, 1); } else { - if (mHsChainInterpCurrValid) { - mHsChainInterpPrevTop = mHsChainInterpCurrTop; - mHsChainInterpPrevRoot = mHsChainInterpCurrRoot; - mHsChainInterpPrevSubRoot = mHsChainInterpCurrSubRoot; - mHsChainInterpPrevSubTop = mHsChainInterpCurrSubTop; - mHsChainInterpPrevValid = true; - } - mHsChainInterpCurrTop = mHookshotTopPos; - mHsChainInterpCurrRoot = mHeldItemRootPos; - mHsChainInterpCurrSubRoot = field_0x3810; - mHsChainInterpCurrSubTop = mIronBallBgChkPos; - mHsChainInterpCurrValid = true; + cXyz hsAnchors[HS_CHAIN_ANCHOR_COUNT]; + hsAnchors[0] = mHookshotTopPos; + hsAnchors[1] = mHeldItemRootPos; + hsAnchors[2] = field_0x3810; + hsAnchors[3] = mIronBallBgChkPos; + interp.hs_chain.capture(hsAnchors, HS_CHAIN_ANCHOR_COUNT); } } #endif diff --git a/src/d/actor/d_a_alink_cut.inc b/src/d/actor/d_a_alink_cut.inc index ba3c0ba89e..8be20f53ad 100644 --- a/src/d/actor/d_a_alink_cut.inc +++ b/src/d/actor/d_a_alink_cut.inc @@ -165,6 +165,12 @@ BOOL daAlink_c::checkCutTypeNoBlur() const { } bool daAlink_c::checkCutTurnInput() const { + #if TARGET_PC + if (mDoCPd_c::getHoldR(PAD_1) && dusk::getSettings().game.easyQuickSpin) { + return true; + } + #endif + return 0xF800 < abs(field_0x3180); } diff --git a/src/d/actor/d_a_alink_effect.inc b/src/d/actor/d_a_alink_effect.inc index b3a9d7bdc4..724d9d01ab 100644 --- a/src/d/actor/d_a_alink_effect.inc +++ b/src/d/actor/d_a_alink_effect.inc @@ -2110,6 +2110,12 @@ void daAlink_blur_c::draw() { J3DShape::resetVcdVatCache(); } +#if TARGET_PC +daAlink_lockCursor_c::~daAlink_lockCursor_c() { + dusk::interp::erase_owned_samples(this); +} +#endif + BOOL daAlink_lockCursor_c::create() { JKRArchive* arc = dComIfG_getObjectResInfo(l_arcName)->getArchive(); @@ -2217,6 +2223,11 @@ void daAlink_lockCursor_c::update() { } cLib_chaseUC(&field_0x4, 0x80, 30); +#if TARGET_PC + const dusk::interp::SightAnimation frame = {mLockCursorAngle, field_0x30, + static_cast(field_0x4), static_cast(field_0x1c->getFrameMax())}; + dusk::interp::get>(this).capture(&frame, 1); +#endif } void daAlink_lockCursor_c::draw() { @@ -2224,28 +2235,28 @@ void daAlink_lockCursor_c::draw() { return; } - field_0x28->setFrame(mLockCursorAngle); +#if TARGET_PC + auto& samples = dusk::interp::get>(this); + const dusk::interp::SightAnimation current = { + mLockCursorAngle, field_0x30, static_cast(field_0x4), static_cast(field_0x1c->getFrameMax())}; + const auto frame = samples.read(0, current); +#endif + field_0x28->setFrame(DUSK_IF_ELSE(frame.bck, mLockCursorAngle)); - f32 var_f30; - if (mLockCursorAngle > 21.0f) { - var_f30 = 21.0f; - } else { - var_f30 = mLockCursorAngle; - } - - field_0x24->setFrame(var_f30); - field_0x1c->setFrame(field_0x30); + field_0x24->setFrame(DUSK_IF_ELSE(frame.bck, mLockCursorAngle) > 21.0f + ? 21.0f : DUSK_IF_ELSE(frame.bck, mLockCursorAngle)); + field_0x1c->setFrame(DUSK_IF_ELSE(frame.brk, field_0x30)); mScrn->animation(); field_0xc->scale(0.6f, 0.6f); field_0xc->translate(mPosX, mPosY); f32 var_f31; - if (!(mLockCursorAngle < 15.0f)) { - if (mLockCursorAngle < 21.0f) { - var_f31 = ((mLockCursorAngle - 15.0f) * 40.0f) * 0.16666667f; + if (!(DUSK_IF_ELSE(frame.bck, mLockCursorAngle) < 15.0f)) { + if (DUSK_IF_ELSE(frame.bck, mLockCursorAngle) < 21.0f) { + var_f31 = ((DUSK_IF_ELSE(frame.bck, mLockCursorAngle) - 15.0f) * 40.0f) * 0.16666667f; } else { - var_f31 = ((field_0x4 * 0.00390625f) + 0.5f) * 40.0f; + var_f31 = ((DUSK_IF_ELSE(frame.alpha, field_0x4) * 0.00390625f) + 0.5f) * 40.0f; } mCursor0->translate(0.0f, -var_f31); @@ -2267,6 +2278,7 @@ BOOL daAlink_sight_c::create() { void daAlink_sight_c::draw() { if (mLockFlag) { + IF_DUSK(setSight(false);) mLockCursor.setPos(mProjMtx[0][3], mProjMtx[1][3]); mLockCursor.draw(); } else { @@ -2279,5 +2291,6 @@ void daAlink_sight_c::onLockFlg() { return; } mLockFlag = true; + IF_DUSK(dusk::interp::erase_owned_samples(&mLockCursor);) mLockCursor.initFrame(); } diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc index cbc8bdc664..631632e0b4 100644 --- a/src/d/actor/d_a_alink_hook.inc +++ b/src/d/actor/d_a_alink_hook.inc @@ -10,7 +10,8 @@ #include "JSystem/J3DGraphBase/J3DMaterial.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/samples.h" #include "dusk/settings.h" static const int HS_CHAIN_MAX_LINKS = 600; @@ -55,8 +56,18 @@ void daAlink_c::hsChainShape_c::draw() { GXLoadLightObjImm(chainLight->getLightObj(), GX_LIGHT0); if (alink->checkIronBallEquip()) { - cXyz* ppos = alink->getIronBallChainPos(); - csXyz* pangle = alink->getIronBallChainAngle(); +#if TARGET_PC + auto& samples = dusk::interp::get(alink); + cXyz chainPositions[IRON_BALL_CHAIN_COUNT]; + csXyz chainAngles[IRON_BALL_CHAIN_COUNT]; + for (int i = 0; i < IRON_BALL_CHAIN_COUNT; ++i) { + chainPositions[i] = samples.ib_pos.read(i, alink->getIronBallChainPos()[i]); + chainAngles[i] = samples.ib_angle.read(i, alink->getIronBallChainAngle()[i]); + } + cXyz chainHand = samples.ib_hand.read(0, alink->getIronBallChainHandRootPos()); +#endif + cXyz* ppos = DUSK_IF_ELSE(chainPositions, alink->getIronBallChainPos()); + csXyz* pangle = DUSK_IF_ELSE(chainAngles, alink->getIronBallChainAngle()); ppos++; pangle++; @@ -86,19 +97,19 @@ void daAlink_c::hsChainShape_c::draw() { s16 spE; if (i == 0) { - sp48 = alink->getIronBallChainPos() + alink->getIronBallHandChainNum(); - spC8 = alink->getIronBallChainHandRootPos() - *sp48; + sp48 = DUSK_IF_ELSE(chainPositions, alink->getIronBallChainPos()) + alink->getIronBallHandChainNum(); + spC8 = DUSK_IF_ELSE(chainHand, alink->getIronBallChainHandRootPos()) - *sp48; - sp44 = alink->getIronBallChainAngle() + alink->getIronBallHandChainNum(); + sp44 = DUSK_IF_ELSE(chainAngles, alink->getIronBallChainAngle()) + alink->getIronBallHandChainNum(); spE = 0x3000; } else { - sp48 = alink->getIronBallChainPos(); - mDoMtx_stack_c::transS(*(alink->getIronBallChainPos() + 1)); - mDoMtx_stack_c::ZXYrotM(*(alink->getIronBallChainAngle() + 1)); + sp48 = DUSK_IF_ELSE(chainPositions, alink->getIronBallChainPos()); + mDoMtx_stack_c::transS(*(DUSK_IF_ELSE(chainPositions, alink->getIronBallChainPos()) + 1)); + mDoMtx_stack_c::ZXYrotM(*(DUSK_IF_ELSE(chainAngles, alink->getIronBallChainAngle()) + 1)); mDoMtx_stack_c::multVec(&l_ironBallChainVec, &spC8); spC8 -= *sp48; - sp44 = alink->getIronBallChainAngle() + 1; + sp44 = DUSK_IF_ELSE(chainAngles, alink->getIronBallChainAngle()) + 1; spE = -0x3000; } @@ -136,19 +147,11 @@ void daAlink_c::hsChainShape_c::draw() { } } else { #if TARGET_PC - cXyz hsInterpTop, hsInterpRoot, hsInterpSubRoot, hsInterpSubTop; - if (dusk::frame_interp::is_enabled() && alink->mHsChainInterpPrevValid && alink->mHsChainInterpCurrValid) { - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - hsInterpTop = alink->mHsChainInterpPrevTop + (alink->mHsChainInterpCurrTop - alink->mHsChainInterpPrevTop) * alpha; - hsInterpRoot = alink->mHsChainInterpPrevRoot + (alink->mHsChainInterpCurrRoot - alink->mHsChainInterpPrevRoot) * alpha; - hsInterpSubRoot = alink->mHsChainInterpPrevSubRoot + (alink->mHsChainInterpCurrSubRoot - alink->mHsChainInterpPrevSubRoot) * alpha; - hsInterpSubTop = alink->mHsChainInterpPrevSubTop + (alink->mHsChainInterpCurrSubTop - alink->mHsChainInterpPrevSubTop) * alpha; - } else { - hsInterpTop = alink->getHsChainTopPos(); - hsInterpRoot = alink->getHsChainRootPos(); - hsInterpSubRoot = alink->getHsSubChainRootPos(); - hsInterpSubTop = alink->getHsSubChainTopPos(); - } + auto& samples = dusk::interp::get(alink).hs_chain; + cXyz hsInterpTop = samples.read(0, alink->getHsChainTopPos()); + cXyz hsInterpRoot = samples.read(1, alink->getHsChainRootPos()); + cXyz hsInterpSubRoot = samples.read(2, alink->getHsSubChainRootPos()); + cXyz hsInterpSubTop = samples.read(3, alink->getHsSubChainTopPos()); #endif const cXyz& chainRootPos = DUSK_IF_ELSE(hsInterpRoot, alink->getHsChainRootPos()); const cXyz& chainTopPos = DUSK_IF_ELSE(hsInterpTop, alink->getHsChainTopPos()); @@ -243,31 +246,6 @@ void daAlink_c::hsChainShape_c::draw() { } } -#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 7389decb73..f271ed401e 100644 --- a/src/d/actor/d_a_alink_horse.inc +++ b/src/d/actor/d_a_alink_horse.inc @@ -340,18 +340,31 @@ void daAlink_c::setHorseStirrup() { mDoMtx_stack_c::copy(mpLinkModel->getAnmMtx(field_0x30bc)); mDoMtx_stack_c::transM(-2.0f, -11.0f, 1.5f); mDoMtx_stack_c::ZXYrotM(0, -0x8000, 0x4000); +#if TARGET_PC + horse->m_model->setAnmMtx(0x17, mDoMtx_stack_c::get()); +#else mDoMtx_copy(mDoMtx_stack_c::get(), horse->getLeftStirrupMtx()); +#endif } if (field_0x2fab & 2) { mDoMtx_stack_c::copy(mpLinkModel->getAnmMtx(field_0x30be)); mDoMtx_stack_c::transM(-2.0f, 11.0f, 1.5f); mDoMtx_stack_c::ZrotM(-0x4000); +#if TARGET_PC + horse->m_model->setAnmMtx(0x19, mDoMtx_stack_c::get()); +#else mDoMtx_copy(mDoMtx_stack_c::get(), horse->getRightStirrupMtx()); +#endif } if (field_0x2fab & 3) { horse->calcWeightEnvMtx(); +#if TARGET_PC + for (u16 i = 0; i < horse->m_model->getModelData()->getWEvlpMtxNum(); i++) { + dusk::interp::record_final_mtx(horse->m_model->getWeightAnmMtx(i)); + } +#endif } int hand_type = getReinHandType(); diff --git a/src/d/actor/d_a_alink_swindow.inc b/src/d/actor/d_a_alink_swindow.inc index 0359bc99bb..33f1fee37b 100644 --- a/src/d/actor/d_a_alink_swindow.inc +++ b/src/d/actor/d_a_alink_swindow.inc @@ -11,16 +11,6 @@ static int daAlink_modelCallBack(J3DJoint* i_joint, int param_1); static int daAlink_headModelCallBack(J3DJoint* i_joint, int param_1); static int daAlink_wolfModelCallBack(J3DJoint* i_joint, int param_1); -#if TARGET_PC -static void* read_anm_resource(void** o_buffer, u16 i_resIdx, u32 i_fixedSize) { - u32 size = daPy_getAnmResourceSize(i_resIdx, i_fixedSize); - *o_buffer = JKRAllocFromSysHeap(size, 0x20); - JUT_ASSERT(__LINE__, *o_buffer); - JKRReadIdxResource(*o_buffer, size, i_resIdx, dComIfGp_getAnmArchive()); - return *o_buffer; -} -#endif - void daAlink_c::setArcName(BOOL i_isWolf) { if (i_isWolf) { mArcName = l_wArcName; @@ -257,10 +247,8 @@ void daAlink_c::changeModelDataDirectWolf(int param_0) { void daAlink_c::initStatusWindow() { onNoResetFlg2(FLG2_STATUS_WINDOW_DRAW); -#if !TARGET_PC void* tmpBuffer = JKR_NEW_ARRAY_ARGS(void*, 0x500, 0x20); JUT_ASSERT(394, tmpBuffer); -#endif u16 bckResIdx, btpResIdx, btkResIdx; if (checkWolf()) { @@ -280,11 +268,7 @@ void daAlink_c::initStatusWindow() { changeModelDataDirect(0); } -#if TARGET_PC - void* tmpBuffer = read_anm_resource(&mAnmBuffers[0], bckResIdx, 0x1400); -#else JKRReadIdxResource(tmpBuffer, 0x1400, bckResIdx, dComIfGp_getAnmArchive()); -#endif m_sWindowBck = JKR_NEW mDoExt_bckAnm(); JUT_ASSERT(428, m_sWindowBck); @@ -292,28 +276,20 @@ void daAlink_c::initStatusWindow() { JUT_ASSERT(433, FALSE); } -#if TARGET_PC - tmpBuffer = read_anm_resource(&mAnmBuffers[1], btpResIdx, 0x400); -#else tmpBuffer = JKR_NEW_ARRAY_ARGS(void*, 0x100, 0x20); JUT_ASSERT(437, tmpBuffer); JKRReadIdxResource(tmpBuffer, 0x400, btpResIdx, dComIfGp_getAnmArchive()); -#endif // this should call J3DAnmLoaderDataBase::load(const void*) but it breaks retail J3DAnmTexPattern* btp = (J3DAnmTexPattern*)J3DAnmLoaderDataBase::load(tmpBuffer, J3DLOADER_UNK_FLAG0); btp->setFrame(0.0f); btp->searchUpdateMaterialID(field_0x06c0); field_0x06c0->entryTexNoAnimator(btp); -#if TARGET_PC - tmpBuffer = read_anm_resource(&mAnmBuffers[2], btkResIdx, 0x400); -#else tmpBuffer = JKR_NEW_ARRAY_ARGS(void*, 0x100, 0x20); JUT_ASSERT(449, tmpBuffer); JKRReadIdxResource(tmpBuffer, 0x400, btkResIdx, dComIfGp_getAnmArchive()); -#endif // this should call J3DAnmLoaderDataBase::load(const void*) but it breaks retail J3DAnmTextureSRTKey* btk = (J3DAnmTextureSRTKey*)J3DAnmLoaderDataBase::load(tmpBuffer, J3DLOADER_UNK_FLAG0); btk->setFrame(0.0f); @@ -411,12 +387,4 @@ void daAlink_c::resetStatusWindow() { offNoResetFlg2(FLG2_STATUS_WINDOW_DRAW); m_sWindowBck = NULL; -#if TARGET_PC - for (int i = 0; i < 3; i++) { - if (mAnmBuffers[i] != NULL) { - JKRFreeToSysHeap(mAnmBuffers[i]); - mAnmBuffers[i] = NULL; - } - } -#endif } diff --git a/src/d/actor/d_a_b_gm.cpp b/src/d/actor/d_a_b_gm.cpp index 2b65b8095b..7b0b86168d 100644 --- a/src/d/actor/d_a_b_gm.cpp +++ b/src/d/actor/d_a_b_gm.cpp @@ -17,6 +17,10 @@ #include "f_op/f_op_msg_mng.h" #include "Z2AudioLib/Z2Instances.h" +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif + #define ANM_EYE_TEST 6 #define ANM_GM_BEAM 7 #define ANM_GOMA_ATTACK_01 8 @@ -1037,6 +1041,7 @@ static void demo_camera(b_gm_class* i_this) { spBC.set(0.0f, 0.0f, 2418.0f); daPy_getPlayerActorClass()->setPlayerPosAndAngle(&spBC, 0x8000, 0); + IF_DUSK(dusk::interp::request_presentation_sync()); dComIfGp_getEvent()->startCheckSkipEdge(i_this); @@ -1048,6 +1053,7 @@ static void demo_camera(b_gm_class* i_this) { if (i_this->mDemoModeTimer == 1) { i_this->mDemoCamCenter.set(-400.0f, 130.0f, 1883.0f); i_this->mDemoCamEye.set(0.0f, 197.0f, 2680.0f); + IF_DUSK(dusk::interp::request_presentation_sync()); } cLib_addCalc2(&i_this->mDemoCamCenter.x, 400.0f, 0.05f, i_this->field_0x1cdc * 4.0f); @@ -1068,6 +1074,7 @@ static void demo_camera(b_gm_class* i_this) { i_this->mDemoCamCenter.set(-1090.0f, 227.0f, -1070.0f); i_this->mDemoCamEye.set(-688.0f, 155.0f, -680.0f); + IF_DUSK(dusk::interp::request_presentation_sync()); } break; case 3: @@ -1095,6 +1102,7 @@ static void demo_camera(b_gm_class* i_this) { i_this->field_0x1ce4 = player->shape_angle.y; i_this->mDemoCamCenter.set(-973.0f, 192.0f, -991.0f); i_this->mDemoCamEye.set(-1368.0f, 158.0f, -1397.0f); + IF_DUSK(dusk::interp::request_presentation_sync()); i_this->field_0x1cdc = 0.0f; i_this->field_0x6f5 = 0; i_this->field_0x1cfc = 2; @@ -1184,6 +1192,7 @@ static void demo_camera(b_gm_class* i_this) { if (i_this->mDemoModeTimer == 0) { i_this->mDemoCamCenter = spA4; i_this->mDemoCamEye = spB0; + IF_DUSK(dusk::interp::request_presentation_sync()); } else { cLib_addCalc2(&i_this->mDemoCamCenter.x, spA4.x, 0.1f, 5.0f); cLib_addCalc2(&i_this->mDemoCamCenter.z, spA4.z, 0.1f, 5.0f); @@ -1223,6 +1232,7 @@ static void demo_camera(b_gm_class* i_this) { if (i_this->mDemoModeTimer == 0) { i_this->mDemoCamCenter = spA4; i_this->mDemoCamEye = spB0; + IF_DUSK(dusk::interp::request_presentation_sync()); } else { cLib_addCalc2(&i_this->mDemoCamCenter.x, spA4.x, 0.1f, 5.0f); cLib_addCalc2(&i_this->mDemoCamCenter.z, spA4.z, 0.1f, 5.0f); @@ -1259,6 +1269,11 @@ static void demo_camera(b_gm_class* i_this) { Z2GetAudioMgr()->bgmStreamPrepare(0x200004E); Z2GetAudioMgr()->bgmStreamPlay(); case 31: +#if TARGET_PC + if (i_this->mDemoModeTimer == 0) { + dusk::interp::request_presentation_sync(); + } +#endif if (i_this->mDemoModeTimer < 10) { cMtx_YrotS(*calc_mtx, cM_atan2s(-a_this->current.pos.x, -a_this->current.pos.z)); spC8.x = 1100.0f; @@ -1309,6 +1324,7 @@ static void demo_camera(b_gm_class* i_this) { i_this->mDemoMode = 32; i_this->mDemoModeTimer = 0; i_this->mDemoCamFovy = 65.0f; + IF_DUSK(dusk::interp::request_presentation_sync()); case 32: cMtx_YrotS(*calc_mtx, player->shape_angle.y + 8000); spC8.x = 0.0f; @@ -1406,6 +1422,7 @@ static void demo_camera(b_gm_class* i_this) { a_this->current.pos = i_this->field_0x1cec; a_this->eyePos = i_this->field_0x1cec; + IF_DUSK(dusk::interp::request_presentation_sync()); i_this->field_0x1ce4 = -25000; i_this->field_0x1ce8 = -2500; @@ -1528,6 +1545,7 @@ static void demo_camera(b_gm_class* i_this) { if (i_this->mDemoModeTimer == 0) { i_this->mDemoCamCenter = spA4; i_this->mDemoCamEye = spB0; + IF_DUSK(dusk::interp::request_presentation_sync()); } else { cLib_addCalc2(&i_this->mDemoCamCenter.x, spA4.x, 0.1f, 5.0f); cLib_addCalc2(&i_this->mDemoCamCenter.z, spA4.z, 0.1f, 5.0f); @@ -1537,6 +1555,7 @@ static void demo_camera(b_gm_class* i_this) { if (i_this->mDemoModeTimer == 60) { i_this->mDemoMode = 41; + IF_DUSK(i_this->mDemoCamSyncTicks = 2); i_this->mDemoModeTimer = 40; i_this->mDemoCamFovy = 55.0f; camera->mCamera.SetTrimSize(3); @@ -1589,6 +1608,7 @@ static void demo_camera(b_gm_class* i_this) { } camera->mCamera.Reset(i_this->mDemoCamCenter, i_this->mDemoCamEye); + IF_DUSK(dusk::interp::request_presentation_sync()); camera->mCamera.Start(); camera->mCamera.SetTrimSize(0); dComIfGp_event_reset(); @@ -1606,6 +1626,12 @@ static void demo_camera(b_gm_class* i_this) { camera->mCamera.Set(center, eye, i_this->mDemoCamFovy, 0); i_this->mDemoModeTimer++; } +#if TARGET_PC + if (i_this->mDemoCamSyncTicks > 0) { + dusk::interp::request_presentation_sync(); + i_this->mDemoCamSyncTicks--; + } +#endif } static int daB_GM_Execute(b_gm_class* i_this) { diff --git a/src/d/actor/d_a_b_gnd.cpp b/src/d/actor/d_a_b_gnd.cpp index 80cca37f6e..56f04e9205 100644 --- a/src/d/actor/d_a_b_gnd.cpp +++ b/src/d/actor/d_a_b_gnd.cpp @@ -17,10 +17,10 @@ #include "Z2AudioLib/Z2Instances.h" -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" #if TARGET_PC #include "dusk/achievements.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/settings.h" #endif class daB_GND_HIO_c : public JORReflexible { @@ -285,30 +285,6 @@ 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; @@ -396,21 +372,6 @@ 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::frame_interp::is_enabled()) { - 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; @@ -1234,16 +1195,12 @@ 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 + IF_DUSK(i_this->mDemoCamSyncTicks = 2); } else { i_this->mDemoCamMode = 34; i_this->mDemoCamTimer = 0; i_this->mMoveMode = 2; -#if TARGET_PC - i_this->mDemoCamSyncTicks = 2; -#endif + IF_DUSK(i_this->mDemoCamSyncTicks = 2); } } break; @@ -3791,7 +3748,7 @@ static void demo_camera(b_gnd_class* i_this) { i_this->mDemoCamSyncTicks = 2; } if (i_this->mDemoCamSyncTicks > 0) { - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); i_this->mDemoCamSyncTicks--; } #endif diff --git a/src/d/actor/d_a_balloon_2D.cpp b/src/d/actor/d_a_balloon_2D.cpp index a9d6d0db4f..aa9fb8a163 100644 --- a/src/d/actor/d_a_balloon_2D.cpp +++ b/src/d/actor/d_a_balloon_2D.cpp @@ -6,7 +6,6 @@ #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" @@ -21,6 +20,13 @@ #include "m_Do/m_Do_lib.h" #include +#if TARGET_PC +#include "dusk/game_clock.h" +#include "dusk/interp/user_interface.h" + +#include +#endif + class daBalloon2D_HIO_c : public mDoHIO_entry_c { public: inline daBalloon2D_HIO_c() { @@ -267,12 +273,12 @@ int daBalloon2D_c::draw() { int daBalloon2D_c::execute() { +#if TARGET_PC + updateOnWide(); +#else setAllAlpha(); setComboAlpha(); - - #if TARGET_PC - updateOnWide(); - #endif +#endif setHIO(false); return 1; @@ -280,6 +286,10 @@ int daBalloon2D_c::execute() { void daBalloon2D_c::drawMeter() { update(); +#if TARGET_PC + setAllAlpha(); + setComboAlpha(); +#endif mScreen->draw(0.0f, 0.0f, dComIfGp_getCurrentGrafPort()); drawAddScore(); } @@ -341,7 +351,7 @@ void daBalloon2D_c::setComboNum(u8 comboNum) { field_0x5a8[5]->changeTexture(resTimg, 0); resTimg = (ResTIMG*)dComIfGp_getMain2DArchive()->getResource('TIMG', dMeter2Info_getNumberTextureName(comboNum % 10)); field_0x5a8[6]->changeTexture(resTimg, 0); - setComboAlpha(); + IF_NOT_DUSK(setComboAlpha()); } void daBalloon2D_c::setBalloonSize(u8 balloonSize) { @@ -394,10 +404,14 @@ void daBalloon2D_c::setAllAlpha() { mAllAlpha = field_0x578->getAlphaRate(); if (isVisible()) { if (mAllAlpha != 1.0f) { +#if TARGET_PC + dusk::vdt::present_addCalc2(&mAllAlpha, 1.0f, 0.4f, 0.5f, 0.1f); +#else cLib_addCalc2(&mAllAlpha, 1.0f, 0.4f, 0.5f); if (fabsf(mAllAlpha - 1.0f) < 0.1f) { mAllAlpha = 1.0f; } +#endif field_0x578->setAlphaRate(mAllAlpha); field_0x580->setAlphaRate(l_HOSTIO.m.m5DNumberAlpha * mAllAlpha); field_0x594->setAlphaRate(l_HOSTIO.m.m2DNumberComboAlpha * mAllAlpha); @@ -406,10 +420,14 @@ void daBalloon2D_c::setAllAlpha() { field_0x5a0->setAlphaRate(l_HOSTIO.m.mBalloonSmallAlpha * mAllAlpha); } } else if (mAllAlpha != 1.0f) { +#if TARGET_PC + dusk::vdt::present_addCalc2(&mAllAlpha, 0.0f, 0.4f, 0.5f, 0.1f); +#else cLib_addCalc2(&mAllAlpha, 0.0f, 0.4f, 0.5f); if (fabsf(mAllAlpha) < 0.1f) { mAllAlpha = 0.0f; } +#endif field_0x578->setAlphaRate(mAllAlpha); field_0x580->setAlphaRate(l_HOSTIO.m.m5DNumberAlpha * mAllAlpha); field_0x594->setAlphaRate(l_HOSTIO.m.m2DNumberComboAlpha * mAllAlpha); @@ -424,10 +442,14 @@ void daBalloon2D_c::setComboAlpha() { mComboAlpha = field_0x5a4->getAlphaRate() * field_0x578->getAlphaRate(); if (mComboNum != 0) { if (mComboAlpha != 1.0f) { +#if TARGET_PC + dusk::vdt::present_addCalc2(&mComboAlpha, 1.0f, 0.4f, 0.5f, 0.1f); +#else cLib_addCalc2(&mComboAlpha, 1.0f, 0.4f, 0.5f); if (fabsf(mComboAlpha - 1.0f) < 0.1f) { mComboAlpha = 1.0f; } +#endif field_0x5a4->setAlphaRate(mComboAlpha); } } else if (mComboAlpha != 0.0f) { @@ -439,26 +461,20 @@ void daBalloon2D_c::setComboAlpha() { void daBalloon2D_c::drawAddScore() { for (s32 i = 19; i >= 0; i--) { if (field_0x5f8[i].field_0xe != 0) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x5f8[i].field_0xe--; - } + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(field_0x5f8[i].field_0xe, 0.0f, 1.0f), field_0x5f8[i].field_0xe--); s32 score3; s32 score2; s32 score = field_0x5f8[i].field_0xc; +#if TARGET_PC + s16 temp0 = (s16)(fmodf(field_0x5f8[i].field_0xe, 60.0f) * 1024.0f); +#else s16 temp0 = (field_0x5f8[i].field_0xe % 60) * 1024; +#endif u8 local_88 = 0xff; f32 dVar11 = 30.0f; f32 dVar9 = 30.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; - } + field_0x5f8[i].field_0x0.x += cM_ssin(temp0) * 0.3f IF_DUSK(* dusk::game_clock::original_frames()); + field_0x5f8[i].field_0x0.y -= 1.0f IF_DUSK(* dusk::game_clock::original_frames()); 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_boomerang.cpp b/src/d/actor/d_a_boomerang.cpp index 9deb7b46b8..96453cb221 100644 --- a/src/d/actor/d_a_boomerang.cpp +++ b/src/d/actor/d_a_boomerang.cpp @@ -14,6 +14,35 @@ #include "Z2AudioLib/Z2Instances.h" #include "SSystem/SComponent/c_math.h" +#if TARGET_PC +#include "dusk/interp/sight.h" + +namespace { +struct SightFrame { + cXyz position; + dusk::interp::SightAnimation animation; +}; + +void lerp(SightFrame& out, const SightFrame& a, const SightFrame& b, f32 step) { + dusk::interp::lerp(out.position, a.position, b.position, step); + dusk::interp::lerp(out.animation, a.animation, b.animation, step); +} + +struct SightSamples { + dusk::interp::Samples slots[BOOMERANG_LOCK_MAX + 1]; +}; + +SightFrame sight_frame(const daBoomerang_sight_c& sight, int i) { + return {sight.m_pos[i], {sight.field_0x98[i], sight.field_0xb0[i], + static_cast(sight.m_alpha[i]), static_cast(sight.m_cursorYellow2Brk->getFrameMax())}}; +} +} // namespace + +daBoomerang_sight_c::~daBoomerang_sight_c() { + dusk::interp::erase_owned_samples(this); +} +#endif + int daBoomerang_sight_c::createHeap() { void* tmpData; JKRArchive* archive = dComIfG_getObjectResInfo(daAlink_c::getAlinkArcName())->getArchive(); @@ -158,6 +187,7 @@ static const u32 l_lockSeFlg[BOOMERANG_LOCK_MAX] = { }; void daBoomerang_sight_c::initialize() { + IF_DUSK(dusk::interp::erase_owned_samples(this);) m_cursorYellowAllPane = m_cursorYellowScrn->search(MULTI_CHAR('n_all')); m_cursorYellow0Pane = m_cursorYellowScrn->search(MULTI_CHAR('cursor0')); m_cursorYellow1Pane = m_cursorYellowScrn->search(MULTI_CHAR('cursor1')); @@ -300,6 +330,7 @@ int daBoomerang_sight_c::playAnime(int param_0, int param_1) { } void daBoomerang_sight_c::initFrame(int i_no) { + IF_DUSK(dusk::interp::get(this).slots[i_no].reset();) m_alpha[i_no] = 0; field_0x98[i_no] = 4.0f; field_0xb0[i_no] = 0.0f; @@ -328,6 +359,10 @@ void daBoomerang_sight_c::copyNumData(int i_no) { cXyz temp_pos = m_pos[i_no]; m_pos[i_no] = m_pos[next_no]; m_pos[next_no] = temp_pos; +#if TARGET_PC + auto& slots = dusk::interp::get(this).slots; + std::swap(slots[i_no], slots[next_no]); +#endif } void daBoomerang_sight_c::setSight(const cXyz* i_pos, int i_no) { @@ -337,7 +372,13 @@ void daBoomerang_sight_c::setSight(const cXyz* i_pos, int i_no) { } Vec proj; - mDoLib_project(&m_pos[i_no], &proj); +#if TARGET_PC + auto& samples = dusk::interp::get(this).slots[i_no]; + const SightFrame current = sight_frame(*this, i_no); + samples.capture(¤t, 1); + cXyz position = samples.read(0, current).position; +#endif + mDoLib_project(DUSK_IF_ELSE(&position, &m_pos[i_no]), &proj); m_proj_posX[i_no] = proj.x; m_proj_posY[i_no] = proj.y; } @@ -357,11 +398,16 @@ void daBoomerang_sight_c::draw() { for (int i = 0; i < 6; i++, alpha_p++) { if (*alpha_p != 0) { - m_cursorYellowBck->setFrame(field_0x98[i]); - m_cursorYellowBpk->setFrame(field_0x98[i] > 21.0f ? 21.0f : field_0x98[i]); +#if TARGET_PC + setSight(NULL, i); + const auto frame = dusk::interp::get(this).slots[i].read(0, sight_frame(*this, i)).animation; +#endif + m_cursorYellowBck->setFrame(DUSK_IF_ELSE(frame.bck, field_0x98[i])); + m_cursorYellowBpk->setFrame(DUSK_IF_ELSE(frame.bck, field_0x98[i]) > 21.0f + ? 21.0f : DUSK_IF_ELSE(frame.bck, field_0x98[i])); if (i == 5) { - m_cursorYellow2Brk->setFrame(field_0xb0[i]); + m_cursorYellow2Brk->setFrame(DUSK_IF_ELSE(frame.brk, field_0xb0[i])); cursor0_pane = m_cursorYellow0Pane; cursor1_pane = m_cursorYellow1Pane; cursor2_pane = m_cursorYellow2Pane; @@ -369,7 +415,7 @@ void daBoomerang_sight_c::draw() { cursorAll_pane = m_cursorYellowAllPane; var_f31 = 80.0f; } else if (i == 0 && m_redSight) { - m_cursorRed2Brk->setFrame(field_0xb0[i]); + m_cursorRed2Brk->setFrame(DUSK_IF_ELSE(frame.brk, field_0xb0[i])); cursor0_pane = m_cursorRed0Pane; cursor1_pane = m_cursorRed1Pane; cursor2_pane = m_cursorRed2Pane; @@ -377,7 +423,7 @@ void daBoomerang_sight_c::draw() { cursorAll_pane = m_cursorRedAllPane; var_f31 = 35.0f; } else { - m_cursorOrange2Brk->setFrame(field_0xb0[i]); + m_cursorOrange2Brk->setFrame(DUSK_IF_ELSE(frame.brk, field_0xb0[i])); cursor0_pane = m_cursorOrange0Pane; cursor1_pane = m_cursorOrange1Pane; cursor2_pane = m_cursorOrange2Pane; @@ -389,15 +435,15 @@ void daBoomerang_sight_c::draw() { screen->animation(); cursorAll_pane->scale(0.6f, 0.6f); cursorAll_pane->translate(m_proj_posX[i], m_proj_posY[i]); - field_0x98[i] = field_0x98[i]; + IF_NOT_DUSK(field_0x98[i] = field_0x98[i]); - if (!(field_0x98[i] < 15.0f)) { - if (field_0x98[i] < 21.0f) { - var_f30 = var_f31 * (field_0x98[i] - 15.0f) * 0.16666667f; + if (!(DUSK_IF_ELSE(frame.bck, field_0x98[i]) < 15.0f)) { + if (DUSK_IF_ELSE(frame.bck, field_0x98[i]) < 21.0f) { + var_f30 = var_f31 * (DUSK_IF_ELSE(frame.bck, field_0x98[i]) - 15.0f) * 0.16666667f; } else if (i == 5) { - var_f30 = var_f31 * (*alpha_p * 0.00390625f + 0.5f); + var_f30 = var_f31 * (DUSK_IF_ELSE(frame.alpha, *alpha_p) * 0.00390625f + 0.5f); } else { - var_f30 = var_f31 * (*alpha_p * 0.0019607844f + 0.5f); + var_f30 = var_f31 * (DUSK_IF_ELSE(frame.alpha, *alpha_p) * 0.0019607844f + 0.5f); } cursor0_pane->translate(0.0f, -var_f30); @@ -406,6 +452,10 @@ void daBoomerang_sight_c::draw() { } screen->draw(0.0f, 0.0f, ctx); +#if TARGET_PC + } else { + dusk::interp::get(this).slots[i].reset(); +#endif } } } diff --git a/src/d/actor/d_a_e_bee.cpp b/src/d/actor/d_a_e_bee.cpp index 09909f9e0a..3cfadae812 100644 --- a/src/d/actor/d_a_e_bee.cpp +++ b/src/d/actor/d_a_e_bee.cpp @@ -15,6 +15,10 @@ #include "SSystem/SComponent/c_math.h" #include "Z2AudioLib/Z2Instances.h" +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif + static bool hio_set; static daE_Bee_HIO_c l_HIO; @@ -50,6 +54,26 @@ static int daE_Bee_Draw(e_bee_class* i_this) { return 1; } +#if TARGET_PC +static void bee_interp(bee_s* i_bee) { + if (!dusk::interp::is_enabled()) { + return; + } + + J3DModel* models[] = { + i_bee->mpModel1, + i_bee->mpModel2, + i_bee->mpModel3, + i_bee->mpModel4, + }; + MtxP mtx = mDoMtx_stack_c::get(); + for (int i = 0; i < 4; i++) { + models[i]->setBaseTRMtx(mtx); + models[i]->calc(); + } +} +#endif + static void bee_mtxset(bee_s* i_bee) { mDoMtx_stack_c::transS(i_bee->mPos.x, i_bee->mPos.y, i_bee->mPos.z); mDoMtx_stack_c::YrotM(i_bee->mAngle.y); @@ -62,6 +86,7 @@ static void bee_mtxset(bee_s* i_bee) { } else { i_bee->mpModel2->setBaseTRMtx(mDoMtx_stack_c::get()); } + IF_DUSK(bee_interp(i_bee)); } static void bee_ground_ang_set(bee_s* i_bee) { @@ -332,6 +357,7 @@ static void bee_nest_action(e_bee_class* i_this, bee_s* i_bee, s8 i_nestHealth) i_bee->mpModel4->setBaseTRMtx(mDoMtx_stack_c::get()); } } + IF_DUSK(bee_interp(i_bee)); if (i_nestHealth == 1) { i_bee->mAction = bee_s::ACT_FLY; diff --git a/src/d/actor/d_a_e_db.cpp b/src/d/actor/d_a_e_db.cpp index dcc3849756..65f91843a3 100644 --- a/src/d/actor/d_a_e_db.cpp +++ b/src/d/actor/d_a_e_db.cpp @@ -10,10 +10,6 @@ #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(); @@ -70,22 +66,6 @@ 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; @@ -115,17 +95,6 @@ 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::frame_interp::is_enabled()) { - 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_gb.cpp b/src/d/actor/d_a_e_gb.cpp index 09b6556548..a710c53511 100644 --- a/src/d/actor/d_a_e_gb.cpp +++ b/src/d/actor/d_a_e_gb.cpp @@ -15,6 +15,10 @@ #include "f_op/f_op_camera_mng.h" #include +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif + enum Head_Action { /* 0x0 */ HEAD_ACTION_WAIT, /* 0x1 */ HEAD_ACTION_ATTACK_1, @@ -1129,6 +1133,7 @@ static void demo_camera(e_gb_class* i_this) { i_this->keyYRot = KREG_S(4) + 0x5000; i_this->demoCamCenter.set(-5689.0f, 4747.0f, 15000.0f); i_this->demoCamEye.set(-5700.0f, 4740.0f, 14644.0f); + IF_DUSK(dusk::interp::request_presentation_sync()); i_this->demoCamCenterTarget.set(-5689.0f, 4740.0f, 14643.0f); i_this->demoCamEyeTarget.set(-5723.0f, 4733.0f, 14293.0f); i_this->demoCamEyeTargetDist.x = fabsf(i_this->demoCamEyeTarget.x - i_this->demoCamEye.x); @@ -1156,6 +1161,7 @@ static void demo_camera(e_gb_class* i_this) { if (i_this->demoCounter != 170) break; i_this->demoCamCenter.set(-5690.0f, 4432.0f, 9610.0f); i_this->demoCamEye.set(-5709.0f, 4445.0f, 9961.0f); + IF_DUSK(dusk::interp::request_presentation_sync()); i_this->demoCamCenterTarget.set(-5741.0f, 4432.0f, 10530.0f); i_this->demoCamEyeTarget.set(-5760.0f, 4496.0f, 10896.0f); i_this->demoCamEyeTargetDist.x = fabsf(i_this->demoCamEyeTarget.x - i_this->demoCamEye.x); @@ -1199,6 +1205,7 @@ static void demo_camera(e_gb_class* i_this) { mDoAud_seStart(Z2SE_EN_GF_OPEN, &actor->home.pos, 0, 0); i_this->demoCamCenter.set(-5706.0f, 4526.0f, 11379.0f); i_this->demoCamEye.set(-5687.0f, 4505.0f, 11008.0f); + IF_DUSK(dusk::interp::request_presentation_sync()); i_this->demoCamCenterTarget.set(-5624.0f, 4439.0f, 9818.0f); i_this->demoCamEyeTarget.set(-5604.0f, 4418.0f, 9447.0f); i_this->demoCamEyeTargetDist.x = fabsf(i_this->demoCamEyeTarget.x - i_this->demoCamEye.x); @@ -1270,6 +1277,11 @@ static void demo_camera(e_gb_class* i_this) { i_this->demoCamEye.x += YREG_F(13); i_this->demoCamEye.y += YREG_F(14) + 300.0f; i_this->demoCamEye.z += YREG_F(15) + -600.0f; +#if TARGET_PC + if (i_this->demoCounter == 0) { + dusk::interp::request_presentation_sync(); + } +#endif if (i_this->demoCounter == 100) { i_this->demoMode = 100; @@ -1278,6 +1290,7 @@ static void demo_camera(e_gb_class* i_this) { case 100: camera->mCamera.Reset(i_this->demoCamCenter, i_this->demoCamEye); + IF_DUSK(dusk::interp::request_presentation_sync()); camera->mCamera.Start(); camera->mCamera.SetTrimSize(0); dComIfGp_event_reset(); diff --git a/src/d/actor/d_a_e_hb.cpp b/src/d/actor/d_a_e_hb.cpp index f0dfbda3a6..b39f4750a1 100644 --- a/src/d/actor/d_a_e_hb.cpp +++ b/src/d/actor/d_a_e_hb.cpp @@ -9,10 +9,6 @@ #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, @@ -68,22 +64,6 @@ 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; @@ -102,17 +82,6 @@ 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::frame_interp::is_enabled()) { - 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 1fff337b33..9eaef0cc1c 100644 --- a/src/d/actor/d_a_e_mb.cpp +++ b/src/d/actor/d_a_e_mb.cpp @@ -12,8 +12,6 @@ #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 @@ -65,22 +63,6 @@ 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; @@ -104,17 +86,6 @@ 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::frame_interp::is_enabled()) { - 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_s1.cpp b/src/d/actor/d_a_e_s1.cpp index cd48a103e1..3c7c6700e5 100644 --- a/src/d/actor/d_a_e_s1.cpp +++ b/src/d/actor/d_a_e_s1.cpp @@ -14,8 +14,6 @@ #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 { @@ -101,32 +99,6 @@ 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; - } - } - GXColor line_color; - line_color.r = JREG_S(0) + 5; - line_color.g = JREG_S(1) + 10; - line_color.b = JREG_S(2) + 10; - line_color.a = 0xFF; - - i_this->mLineMat.update(16, line_color, &i_this->tevStr); -} -#endif - static int daE_S1_Draw(e_s1_class* i_this) { if (i_this->field_0x306c != 0) { return 1; @@ -160,22 +132,6 @@ 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::frame_interp::is_enabled()) { - 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; } @@ -2193,11 +2149,6 @@ 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 808301527e..5c0d9838ea 100644 --- a/src/d/actor/d_a_e_sm2.cpp +++ b/src/d/actor/d_a_e_sm2.cpp @@ -14,10 +14,6 @@ #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(); @@ -80,62 +76,9 @@ 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_wb.cpp b/src/d/actor/d_a_e_wb.cpp index a7f2e4f7b3..cf9e4ccbf7 100644 --- a/src/d/actor/d_a_e_wb.cpp +++ b/src/d/actor/d_a_e_wb.cpp @@ -18,10 +18,12 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include "res/Object/Always.h" -#include "dusk/dusk.h" -#include "dusk/frame_interpolation.h" #include +#if TARGET_PC +#include "dusk/dusk.h" +#include "dusk/interp/frame_interpolation.h" +#endif class daE_WB_HIO_c : public JORReflexible { public: @@ -186,30 +188,6 @@ static bool hio_set; static daE_WB_HIO_c l_HIO; -#if TARGET_PC -static void e_wb_rein_interp_callback(bool isSimFrame, void* pUserWork) { - e_wb_class* i_this = (e_wb_class*)pUserWork; - if (!i_this->himo_interp_prev_valid || !i_this->himo_interp_curr_valid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - for (int r = 0; r < 2; r++) { - cXyz* dst = i_this->himo_mat[r].getPos(0); - for (int i = 0; i < 16; i++) { - const cXyz& p0 = i_this->himo_mat_interp_prev[r][i]; - const cXyz& p1 = i_this->himo_mat_interp_curr[r][i]; - dst[i] = p0 + (p1 - p0) * alpha; - } - } - cXyz* dst = i_this->himo_tex.getPos(0); - for (int i = 0; i < 2; i++) { - const cXyz& p0 = i_this->himo_tex_interp_prev[i]; - const cXyz& p1 = i_this->himo_tex_interp_curr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - static void himo_control1(e_wb_class* i_this, cXyz* i_pos, int i_no, s8 param_3) { fopEn_enemy_c* enemy = &i_this->enemy; cXyz mae, ato; @@ -534,21 +512,6 @@ static int daE_WB_Draw(e_wb_class* i_this) { dComIfGd_set3DlineMat(&i_this->himo_mat[1]); i_this->himo_tex.update(2, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->himo_tex); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->himo_interp_curr_valid) { - memcpy(i_this->himo_mat_interp_prev, i_this->himo_mat_interp_curr, sizeof(i_this->himo_mat_interp_curr)); - memcpy(i_this->himo_tex_interp_prev, i_this->himo_tex_interp_curr, sizeof(i_this->himo_tex_interp_curr)); - i_this->himo_interp_prev_valid = true; - } - for (int r = 0; r < 2; r++) { - memcpy(i_this->himo_mat_interp_curr[r], i_this->himo_mat[r].getPos(0), 16 * sizeof(cXyz)); - } - memcpy(i_this->himo_tex_interp_curr, i_this->himo_tex.getPos(0), 2 * sizeof(cXyz)); - i_this->himo_interp_curr_valid = true; - dusk::frame_interp::add_interpolation_callback(&e_wb_rein_interp_callback, i_this); - } -#endif } return 1; @@ -4542,7 +4505,7 @@ static void demo_camera(e_wb_class* i_this) { i_this->demo_cam_way_spd.z = fabsf(i_this->demo_cam_way.z - i_this->demo_cam_ctr.z); i_this->demo_cam_morf = 0; pla->setPlayerPosAndAngle(&pla->current.pos, pla->shape_angle.y - 4000, 0); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } if (i_this->demo_timer == 345) { daPy_getPlayerActorClass()->setThrowDamage(boss->enemy.shape_angle.y - 8000 + TREG_S(8), @@ -4789,7 +4752,7 @@ static void demo_camera(e_wb_class* i_this) { i_this->demo_cam_eye.x += 300.0f + VREG_F(8); i_this->demo_cam_eye.y += 150.0f + VREG_F(9); i_this->demo_cam_eye.z -= 1400.0f + VREG_F(10); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } } else { i_this->demo_cam_eye = enemy->current.pos; @@ -5050,7 +5013,7 @@ static void demo_camera(e_wb_class* i_this) { i_this->demo_cam_sync_ticks = 2; } if (i_this->demo_cam_sync_ticks > 0) { - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); i_this->demo_cam_sync_ticks--; } #endif diff --git a/src/d/actor/d_a_e_yd.cpp b/src/d/actor/d_a_e_yd.cpp index 52a5be7fdc..bdc7c527e2 100644 --- a/src/d/actor/d_a_e_yd.cpp +++ b/src/d/actor/d_a_e_yd.cpp @@ -12,10 +12,6 @@ #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(); @@ -77,22 +73,6 @@ 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 }; @@ -106,17 +86,6 @@ 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::frame_interp::is_enabled()) { - 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 71756d6f57..e6bcd05098 100644 --- a/src/d/actor/d_a_e_yg.cpp +++ b/src/d/actor/d_a_e_yg.cpp @@ -10,8 +10,6 @@ #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 { @@ -136,33 +134,6 @@ static BOOL pl_check(e_yg_class* i_this, f32 i_dist) { return FALSE; } -#if TARGET_PC -static void daE_YG_interp_callback(bool isSimFrame, void* pUserWork) { - e_yg_class* i_this = (e_yg_class*)pUserWork; - fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->actor; - 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; - } - } - GXColor color; - color.r = JREG_S(0) + 20; - color.g = JREG_S(1) + 20; - color.b = JREG_S(2) + 20; - color.a = 0xFF; - - i_this->mLineMat.update(10, color, &actor->tevStr); -} -#endif - static int daE_YG_Draw(e_yg_class* i_this) { if (i_this->mDispFlag) { return 1; @@ -190,22 +161,6 @@ static int daE_YG_Draw(e_yg_class* i_this) { i_this->mLineMat.update(10, color, &actor->tevStr); dComIfGd_set3DlineMatDark(&i_this->mLineMat); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - 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; @@ -1424,11 +1379,6 @@ 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 fb00a4bab1..55f175d78e 100644 --- a/src/d/actor/d_a_e_yh.cpp +++ b/src/d/actor/d_a_e_yh.cpp @@ -12,10 +12,6 @@ #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(); @@ -89,22 +85,6 @@ 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; @@ -134,17 +114,6 @@ 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::frame_interp::is_enabled()) { - 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_horse.cpp b/src/d/actor/d_a_horse.cpp index 72b1fb1e7b..b738460bb8 100644 --- a/src/d/actor/d_a_horse.cpp +++ b/src/d/actor/d_a_horse.cpp @@ -20,23 +20,6 @@ #include #include -#if TARGET_PC -#include "dusk/dusk.h" -#include "dusk/frame_interpolation.h" - -namespace { -// FRAME INTERP NOTE: Sim tick control point snapshots for interpolation -constexpr int kHorseReinSimMax = 75; -cXyz s_horseReinSimPrev[kHorseReinSimMax]; -cXyz s_horseReinSimCurr[kHorseReinSimMax]; -int s_horseReinSimNumPrev; -int s_horseReinSimNumCurr; -bool s_horseReinSimPrevValid; -bool s_horseReinSimCurrValid; -uint64_t s_horseReinSimRolledSeq; -} // namespace -#endif - #define ANM_HS_BACK_WALK 6 #define ANM_HS_WALK_START 7 #define ANM_HS_EXCITEMENT 8 @@ -3033,24 +3016,6 @@ void daHorse_c::copyReinPos() { for (i = rein->field_0x8[0] - 1; i >= 0; i--, pos_p++) { *pos_p = rein->field_0x0[0][i]; } -#if TARGET_PC - if (field_0x1204 > 0) { - const uint64_t simSeq = dusk::frame_interp::sim_tick_seq(); - if (simSeq != s_horseReinSimRolledSeq) { - s_horseReinSimRolledSeq = simSeq; - if (s_horseReinSimCurrValid && s_horseReinSimNumCurr > 0) { - memcpy(s_horseReinSimPrev, s_horseReinSimCurr, s_horseReinSimNumCurr * sizeof(cXyz)); - s_horseReinSimNumPrev = s_horseReinSimNumCurr; - s_horseReinSimPrevValid = true; - } - } - memcpy(s_horseReinSimCurr, m_reinLine.getPos(0), field_0x1204 * sizeof(cXyz)); - s_horseReinSimNumCurr = field_0x1204; - s_horseReinSimCurrValid = true; - } else { - s_horseReinSimCurrValid = false; - } -#endif } void daHorse_c::setReinPosHandSubstance(int param_0) { @@ -3162,30 +3127,6 @@ void daHorse_c::setReinPosNormalSubstance() { copyReinPos(); } -#if TARGET_PC -void daHorse_c::lerpControlPoints(f32 alpha) { - // FRAME INTERP NOTE: Currently only lerping points for Epona's reins. Need a more global solution. - if (!dusk::frame_interp::is_enabled() || !s_horseReinSimPrevValid || !s_horseReinSimCurrValid) { - return; - } - const int nCurr = s_horseReinSimNumCurr; - const int nPrev = s_horseReinSimNumPrev; - if (nCurr <= 0) { - return; - } - int n = nPrev < nCurr ? nPrev : nCurr; - if (n <= 0 || n > kHorseReinSimMax) { - return; - } - cXyz* dst = m_reinLine.getPos(0); - for (int i = 0; i < n; i++) { - const cXyz& p0 = s_horseReinSimPrev[i]; - const cXyz& p1 = s_horseReinSimCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - void daHorse_c::bgCheck() { if (m_procID != PROC_LARGE_DAMAGE_e) { static DUSK_CONSTEXPR cXyz localCenterPos(0.0f, 100.0f, 0.0f); diff --git a/src/d/actor/d_a_mant.cpp b/src/d/actor/d_a_mant.cpp index 3f9c2deae1..4f175e794f 100644 --- a/src/d/actor/d_a_mant.cpp +++ b/src/d/actor/d_a_mant.cpp @@ -12,7 +12,7 @@ #if TARGET_PC #include "dusk/dvd_asset.hpp" -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include @@ -212,8 +212,8 @@ void daMant_packet_c::draw() { MtxP src25 = model->getAnmMtx(25); Mtx joint_34_scratch; Mtx joint_25_scratch; - MtxP joint_34 = dusk::frame_interp::lookup_replacement(src34, joint_34_scratch) ? joint_34_scratch : src34; - MtxP joint_25 = dusk::frame_interp::lookup_replacement(src25, joint_25_scratch) ? joint_25_scratch : src25; + MtxP joint_34 = dusk::interp::lookup_replacement(src34, joint_34_scratch) ? joint_34_scratch : src34; + MtxP joint_25 = dusk::interp::lookup_replacement(src25, joint_25_scratch) ? joint_25_scratch : src25; cXyz presented_anchor_a; cXyz presented_anchor_b; @@ -232,7 +232,7 @@ void daMant_packet_c::draw() { } } - const f32 step = dusk::frame_interp::get_interpolation_step(); + const f32 step = dusk::interp::get_interpolation_step(); for (int i = 0; i < 169; ++i) { cXyz curr_local; MTXMultVec(curr_frame_inverse, &curr_pos[i], &curr_local); diff --git a/src/d/actor/d_a_mg_rod.cpp b/src/d/actor/d_a_mg_rod.cpp index 6cf1e76024..53dba36b74 100644 --- a/src/d/actor/d_a_mg_rod.cpp +++ b/src/d/actor/d_a_mg_rod.cpp @@ -26,7 +26,6 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" #include "dusk/mods/item.hpp" #include "dusk/settings.h" #include "dusk/version.hpp" @@ -183,25 +182,6 @@ static int Worm_nodeCallBack(J3DJoint* i_joint, int param_1) { return 1; } -#if TARGET_PC -static void dmg_rod_interp_callback(bool isSimFrame, void* pUserWork) { - dmg_rod_class* i_this = (dmg_rod_class*)pUserWork; - if (!i_this->mLineInterpPrevValid || !i_this->mLineInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - const int count = i_this->kind == MG_ROD_KIND_LURE ? MG_ROD_LURE_LINE_LEN : MG_ROD_UKI_LINE_LEN; - cXyz* dst = i_this->linemat.getPos(0); - for (int i = 0; i < count; i++) { - const cXyz& p0 = i_this->mLineInterpPrev[i]; - const cXyz& p1 = i_this->mLineInterpCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } - static GXColor l_color = {0xFF, 0xFF, 0x96, 0xFF}; - i_this->linemat.update(count, l_color, &i_this->actor.tevStr); -} -#endif - static int dmg_rod_Draw(dmg_rod_class* i_this) { int unused; fopAc_ac_c* actor = &i_this->actor; @@ -242,18 +222,6 @@ static int dmg_rod_Draw(dmg_rod_class* i_this) { i_this->linemat.update(MG_ROD_LURE_LINE_LEN, l_color, &i_this->actor.tevStr); dComIfGd_set3DlineMat(&i_this->linemat); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mLineInterpCurrValid) { - memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, MG_ROD_LURE_LINE_LEN * sizeof(cXyz)); - i_this->mLineInterpPrevValid = true; - } - memcpy(i_this->mLineInterpCurr, i_this->linemat.getPos(0), MG_ROD_LURE_LINE_LEN * sizeof(cXyz)); - i_this->mLineInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&dmg_rod_interp_callback, i_this); - } -#endif - model = i_this->rod_modelMorf->getModel(); g_env_light.setLightTevColorType_MAJI(model, &i_this->actor.tevStr); i_this->rod_modelMorf->entryDL(); @@ -278,18 +246,6 @@ static int dmg_rod_Draw(dmg_rod_class* i_this) { i_this->linemat.update(MG_ROD_UKI_LINE_LEN, l_color, &i_this->actor.tevStr); dComIfGd_set3DlineMat(&i_this->linemat); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mLineInterpCurrValid) { - memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, MG_ROD_UKI_LINE_LEN * sizeof(cXyz)); - i_this->mLineInterpPrevValid = true; - } - memcpy(i_this->mLineInterpCurr, i_this->linemat.getPos(0), MG_ROD_UKI_LINE_LEN * sizeof(cXyz)); - i_this->mLineInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&dmg_rod_interp_callback, i_this); - } -#endif - for (int i = 0; i < 15; i++) { g_env_light.setLightTevColorType_MAJI(i_this->rod_uki_model[i], &actor->tevStr); mDoExt_modelUpdateDL(i_this->rod_uki_model[i]); @@ -6481,11 +6437,6 @@ static int dmg_rod_Create(fopAc_ac_c* i_this) { return cPhs_ERROR_e; } -#if TARGET_PC - rod->mLineInterpPrevValid = false; - rod->mLineInterpCurrValid = false; -#endif - OS_REPORT("//////////////MG_ROD SET 2 !!\n"); if (!hio_set) { rod->HIOInit = TRUE; diff --git a/src/d/actor/d_a_midna.cpp b/src/d/actor/d_a_midna.cpp index 47d3da98c8..f58b6e68d4 100644 --- a/src/d/actor/d_a_midna.cpp +++ b/src/d/actor/d_a_midna.cpp @@ -16,7 +16,7 @@ #include "d/d_debug_viewer.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif static f32 dummy_lit_3777(int idx, u8 foo) { @@ -499,7 +499,6 @@ int daMidna_c::createHeap() { } } - IF_DUSK(mBckHeap[0].reserveBuffer(0x1DC);) 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); @@ -1110,10 +1109,10 @@ void daMidna_c::setBodyPartMatrix() { mpModel->setAnmMtx(i, mpShadowModel->getAnmMtx(i)); } mpModel->calcWeightEnvelopeMtx(); -#ifdef TARGET_PC +#if TARGET_PC // FRAME INTERP NOTE: Record weight envelopes for Midna here, as they are otherwise missed causing distortion for (u16 i = 0; i < mpModel->getModelData()->getWEvlpMtxNum(); i++) { - dusk::frame_interp::record_final_mtx(mpModel->getWeightAnmMtx(i)); + dusk::interp::record_final_mtx(mpModel->getWeightAnmMtx(i)); } #endif } @@ -2168,6 +2167,13 @@ void daMidna_c::setAnm() { } if (anm == ANM_S_APPEAR || anm == ANM_S_APPEARBL) { +#if TARGET_PC + mpShadowModel->forgetMtx(); + mpShadowMaskBmd->forgetMtx(); + mpShadowHandsBmd->forgetMtx(); + mpShadowHairhandBmd->forgetMtx(); + mpGokouBmd->forgetMtx(); +#endif mSound.startCreatureSound(Z2SE_MIDNA_APPEAR, 0, -1); } else if (anm == ANM_S_RETURN || anm == ANM_RETURN) { mSound.startCreatureSound(Z2SE_MIDNA_DISAPPEAR, 0, -1); diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index d02d81d834..123279e8af 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -14,10 +14,6 @@ #include #include "m_Do/m_Do_lib.h" -#if TARGET_PC -#include "dusk/frame_interpolation.h" -#endif - static BOOL daMirror_c_createHeap(fopAc_ac_c* i_this) { return ((daMirror_c*)i_this)->createHeap(); } diff --git a/src/d/actor/d_a_npc_ne.cpp b/src/d/actor/d_a_npc_ne.cpp index de48a76851..530654400f 100644 --- a/src/d/actor/d_a_npc_ne.cpp +++ b/src/d/actor/d_a_npc_ne.cpp @@ -21,7 +21,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif static home_path_pnt home_path[38] = { @@ -2659,7 +2659,7 @@ static void demo_camera(npc_ne_class* i_this) { i_this->mCameraFovY = 55.0f; camera->mCamera.SetTrimSize(3); daPy_getPlayerActorClass()->changeOriginalDemo(); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); // fallthrough case 2: @@ -2688,7 +2688,7 @@ static void demo_camera(npc_ne_class* i_this) { if (i_this->mDemoCounter == 0) { i_this->mCameraCenter1.set(387.0f, 133.0f, -866.0f); i_this->mCameraEye1.set(284.0f, 208.0f, -585.0f); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } if (i_this->mDemoCounter == 12) { @@ -2725,7 +2725,7 @@ static void demo_camera(npc_ne_class* i_this) { i_this->mCameraFovY = 45.0f; camera->mCamera.SetTrimSize(3); daPy_getPlayerActorClass()->changeOriginalDemo(); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); // fallthrough case 11: @@ -2806,10 +2806,10 @@ static void demo_camera(npc_ne_class* i_this) { MtxPosition(&vec, &i_this->mCameraEye2); i_this->mCameraEye2 += player->current.pos; player->changeDemoParam2(2); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } else if (i_this->mDemoCounter == 120) { player->changeDemoParam2(0); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } } } @@ -2862,7 +2862,7 @@ static void demo_camera(npc_ne_class* i_this) { i_this->mCameraCenter1 = _this->current.pos; i_this->mCameraCenter1.y += 20.0f; i_this->mCameraFovY = 55.0f; - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } camera->mCamera.Set(i_this->mCameraCenter1, i_this->mCameraEye1, diff --git a/src/d/actor/d_a_npc_toby.cpp b/src/d/actor/d_a_npc_toby.cpp index 120cfac318..d42787e2a2 100644 --- a/src/d/actor/d_a_npc_toby.cpp +++ b/src/d/actor/d_a_npc_toby.cpp @@ -17,7 +17,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif DUSK_GAME_DATA const daNpc_Toby_HIOParam daNpc_Toby_Param_c::m = { @@ -1402,7 +1402,7 @@ int daNpc_Toby_c::cutRepairSCannon(int arg0) { old.pos = current.pos; setAngle(cM_deg2s(5.0f * f32(mPath.getArg0()))); mEventTimer = mPath.getArg2(); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } } else if (!mHide) { mHide = 1; diff --git a/src/d/actor/d_a_obj_fchain.cpp b/src/d/actor/d_a_obj_fchain.cpp index b4d473c424..e8cbb10168 100644 --- a/src/d/actor/d_a_obj_fchain.cpp +++ b/src/d/actor/d_a_obj_fchain.cpp @@ -10,10 +10,18 @@ #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 +#if TARGET_PC +#include "dusk/interp/samples.h" + +static const int CHAIN_COUNT = 22; +struct ChainInterp { + dusk::interp::Samples positions; + dusk::interp::Samples angles; +}; +#endif + static char const l_arcName[] = "Fchain"; int daObjFchain_c::createHeap() { @@ -67,10 +75,6 @@ int daObjFchain_c::create() { local_48++; } rv = cPhs_COMPLEATE_e; -#if TARGET_PC - mChainInterpPrevValid = false; - mChainInterpCurrValid = false; -#endif break; } return rv; @@ -256,6 +260,17 @@ void daObjFchain_shape_c::draw() { daObjFchain_c* i_this = (daObjFchain_c*)getUserArea(); cXyz* pPos = i_this->getPos(); csXyz* pAngle = i_this->getAngle(); +#if TARGET_PC + cXyz positions[CHAIN_COUNT]; + csXyz angles[CHAIN_COUNT]; + auto& samples = dusk::interp::get(i_this); + for (int i = 0; i < CHAIN_COUNT; ++i) { + positions[i] = samples.positions.read(i, pPos[i]); + angles[i] = samples.angles.read(i, pAngle[i]); + } + pPos = positions; + pAngle = angles; +#endif J3DModelData* modelData = i_this->getModelData(); J3DMaterial* material = modelData->getMaterialNodePointer(0); dKy_tevstr_c* tevStr = &i_this->tevStr; @@ -295,26 +310,6 @@ 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); @@ -325,16 +320,9 @@ int daObjFchain_c::draw() { dComIfGd_getOpaListDark()->entryImm(&mShape, 0); #if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - 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); - } + auto& samples = dusk::interp::get(this); + samples.positions.capture(getPos(), CHAIN_COUNT); + samples.angles.capture(getAngle(), CHAIN_COUNT); #endif } return 1; diff --git a/src/d/actor/d_a_obj_groundwater.cpp b/src/d/actor/d_a_obj_groundwater.cpp index 59f310678d..5877de3b80 100644 --- a/src/d/actor/d_a_obj_groundwater.cpp +++ b/src/d/actor/d_a_obj_groundwater.cpp @@ -9,6 +9,10 @@ #include "d/d_com_inf_game.h" #include "m_Do/m_Do_graphic.h" +#if TARGET_PC +#include "dusk/interp/material.h" +#endif + static daGrdWater_HIO_c l_HIO; static daGrdWater_c::modeFunc l_mode_func[5] = { @@ -299,13 +303,17 @@ int daGrdWater_c::Draw() { if (material->getTexGenBlock()->getTexMtx(0) != NULL) { J3DTexMtxInfo* mtxInfo = &material->getTexGenBlock()->getTexMtx(0)->getTexMtxInfo(); if (mtxInfo != NULL) { +#if TARGET_PC + dusk::interp::material::set_view_projection(mtxInfo, 1.0f, 1.0f, -0.01f, 0.0f); +#else Mtx afStack_50; C_MTXLightPerspective(afStack_50, dComIfGd_getView()->fovy, dComIfGd_getView()->aspect, 1.0f, 1.0f, -0.01f, 0.0f); - #if WIDESCREEN_SUPPORT +#if WIDESCREEN_SUPPORT mDoGph_gInf_c::setWideZoomLightProjection(afStack_50); - #endif +#endif mtxInfo->setEffectMtx(afStack_50); +#endif modelData2->simpleCalcMaterial(0, (MtxP)j3dDefaultMtx); } } diff --git a/src/d/actor/d_a_obj_item.cpp b/src/d/actor/d_a_obj_item.cpp index e06eb37e50..1c9cc1d5a8 100644 --- a/src/d/actor/d_a_obj_item.cpp +++ b/src/d/actor/d_a_obj_item.cpp @@ -16,10 +16,6 @@ #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; @@ -35,27 +31,6 @@ 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; } @@ -206,7 +181,7 @@ void daItem_c::CreateInit() { initBaseMtx(); animPlay(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f); - if (m_itemNo == dItemNo_BOOMERANG_e IF_DUSK(&&!mItemOverridden)) { + if (DUSK_IF_ELSE(mOriginalItemNo, m_itemNo) == dItemNo_BOOMERANG_e) { itemGetNextExecute(); } else if ((m_itemNo == dItemNo_ORANGE_RUPEE_e || m_itemNo == dItemNo_SILVER_RUPEE_e) && mSparkleEmtr.getEmitter() == NULL) @@ -411,10 +386,6 @@ 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(); } @@ -563,7 +534,7 @@ void daItem_c::procWaitGetDemoEvent() { dComIfGp_event_setItemPartnerId(m_item_id); } } else { - if (m_itemNo == dItemNo_BOOMERANG_e IF_DUSK(&&!mItemOverridden)) { + if (DUSK_IF_ELSE(mOriginalItemNo, m_itemNo) == dItemNo_BOOMERANG_e) { fopAcM_orderItemEvent(this, 0, 0); eventInfo.onCondition(dEvtCnd_CANGETITEM_e); return; @@ -787,32 +758,44 @@ void daItem_c::mode_wait() { mAcch.SetGrndNone(); } - switch (m_itemNo) { - case dItemNo_HEART_e: - itemActionForHeart(); - break; - case dItemNo_ARROW_10_e: - case dItemNo_ARROW_20_e: - case dItemNo_ARROW_30_e: - case dItemNo_ARROW_1_e: - case dItemNo_PACHINKO_SHOT_e: - case dItemNo_LIGHT_ARROW_e: - itemActionForArrow(); - break; - case dItemNo_BOOMERANG_e: +#if TARGET_PC + if (mOriginalItemNo == dItemNo_BOOMERANG_e) { itemActionForBoomerang(); - break; - case dItemNo_GREEN_RUPEE_e: - case dItemNo_BLUE_RUPEE_e: - case dItemNo_YELLOW_RUPEE_e: - case dItemNo_RED_RUPEE_e: - case dItemNo_PURPLE_RUPEE_e: - case dItemNo_ORANGE_RUPEE_e: - case dItemNo_SILVER_RUPEE_e: - default: - itemActionForRupee(); - break; + } else { +#endif + switch (m_itemNo) { + case dItemNo_HEART_e: + itemActionForHeart(); + break; + case dItemNo_ARROW_10_e: + case dItemNo_ARROW_20_e: + case dItemNo_ARROW_30_e: + case dItemNo_ARROW_1_e: + case dItemNo_PACHINKO_SHOT_e: + case dItemNo_LIGHT_ARROW_e: + itemActionForArrow(); + break; + case dItemNo_BOOMERANG_e: + // The boomerang check is already handled above, so if we got here, it's guaranteed to be + // an override. Fallthrough to the rupee action +#if !TARGET_PC + itemActionForBoomerang(); + break; +#endif + case dItemNo_GREEN_RUPEE_e: + case dItemNo_BLUE_RUPEE_e: + case dItemNo_YELLOW_RUPEE_e: + case dItemNo_RED_RUPEE_e: + case dItemNo_PURPLE_RUPEE_e: + case dItemNo_ORANGE_RUPEE_e: + case dItemNo_SILVER_RUPEE_e: + default: + itemActionForRupee(); + break; + } +#if TARGET_PC } +#endif if (field_0x9c0 == 0 && mAcch.ChkWaterHit() && mAcch.m_wtr.GetHeight() > current.pos.y) { mode_water_init(); @@ -868,55 +851,64 @@ void daItem_c::itemGetNextExecute() { setFlag(FLAG_INIT_GET_ITEM_e); BOOL haveItem = false; - switch (m_itemNo) { - case dItemNo_HEART_e: - case dItemNo_GREEN_RUPEE_e: - case dItemNo_ARROW_10_e: - case dItemNo_ARROW_20_e: - case dItemNo_ARROW_30_e: - case dItemNo_ARROW_1_e: - procInitSimpleGetDemo(); - itemGet(); - break; - case dItemNo_BLUE_RUPEE_e: - case dItemNo_YELLOW_RUPEE_e: - case dItemNo_RED_RUPEE_e: - case dItemNo_PURPLE_RUPEE_e: - case dItemNo_ORANGE_RUPEE_e: - case dItemNo_SILVER_RUPEE_e: - case dItemNo_PACHINKO_SHOT_e: - if (daPy_getPlayerActorClass()->checkCanoeRide() || - daPy_getPlayerActorClass()->checkHorseRide()) - { - if (checkItemGet(m_itemNo, 1)) { - haveItem = true; - } - procInitSimpleGetDemo(); - itemGet(); - - if (!haveItem) { - dComIfGs_offItemFirstBit(m_itemNo); - } - } else if (!checkItemGet(m_itemNo, 1)) { - procInitGetDemoEvent(); - } else { - procInitSimpleGetDemo(); - itemGet(); - } - break; - case dItemNo_BOOMERANG_e: - procInitGetDemoEvent(); - break; - default: #if TARGET_PC - if (mItemOverridden) { + // Always call demo event for the original boomerang check + if (mOriginalItemNo == dItemNo_BOOMERANG_e) { + procInitGetDemoEvent(); + } else { +#endif + switch (m_itemNo) { + case dItemNo_HEART_e: + case dItemNo_GREEN_RUPEE_e: + case dItemNo_ARROW_10_e: + case dItemNo_ARROW_20_e: + case dItemNo_ARROW_30_e: + case dItemNo_ARROW_1_e: + procInitSimpleGetDemo(); + itemGet(); + break; + case dItemNo_BLUE_RUPEE_e: + case dItemNo_YELLOW_RUPEE_e: + case dItemNo_RED_RUPEE_e: + case dItemNo_PURPLE_RUPEE_e: + case dItemNo_ORANGE_RUPEE_e: + case dItemNo_SILVER_RUPEE_e: + case dItemNo_PACHINKO_SHOT_e: + if (daPy_getPlayerActorClass()->checkCanoeRide() || + daPy_getPlayerActorClass()->checkHorseRide()) + { + if (checkItemGet(m_itemNo, 1)) { + haveItem = true; + } + procInitSimpleGetDemo(); + itemGet(); + + if (!haveItem) { + dComIfGs_offItemFirstBit(m_itemNo); + } + } else if (!checkItemGet(m_itemNo, 1)) { + procInitGetDemoEvent(); + } else { + procInitSimpleGetDemo(); + itemGet(); + } + break; + case dItemNo_BOOMERANG_e: procInitGetDemoEvent(); break; - } + default: +#if TARGET_PC + if (mItemOverridden) { + procInitGetDemoEvent(); + break; + } #endif - // "[daItem_c] Get process not defined[%d]\n" - OS_REPORT_ERROR("[daItem_c]ゲット処理が定義されていません[%d]\n", m_itemNo); + // "[daItem_c] Get process not defined[%d]\n" + OS_REPORT_ERROR("[daItem_c]ゲット処理が定義されていません[%d]\n", m_itemNo); + } +#if TARGET_PC } +#endif fopAcM_onItem(this, mItemBitNo); mCcCyl.SetTgType(0); @@ -968,6 +960,13 @@ void daItem_c::itemGet() { execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_BOOMERANG_e: +#if TARGET_PC + if (mItemOverridden) { + mDoAud_seStart(Z2SE_CONSUMP_ITEM_GET, NULL, 0, 0); + execItemGet(m_itemNo, mItemGiveTag, this); + break; + } +#endif break; case dItemNo_ARROW_10_e: case dItemNo_ARROW_20_e: @@ -1297,7 +1296,7 @@ void daItem_c::initSpeed(BOOL i_noTypeChk) { u8 type = daItem_prm::getType(this); if (!i_noTypeChk) { - if (type == TYPE_WAIT_e || type == TYPE_BOOM_HIT_e || m_itemNo == dItemNo_BOOMERANG_e) { + if (type == TYPE_WAIT_e || type == TYPE_BOOM_HIT_e || DUSK_IF_ELSE(mOriginalItemNo, m_itemNo) == dItemNo_BOOMERANG_e) { y_speed = 0.0f; speedf = 0.0f; } else if (type == TYPE_LAUNCH_NO_RND_e || type == TYPE_FIXED_PLACE_e) { diff --git a/src/d/actor/d_a_obj_keyhole.cpp b/src/d/actor/d_a_obj_keyhole.cpp index d56f39f40b..8b120f77e3 100644 --- a/src/d/actor/d_a_obj_keyhole.cpp +++ b/src/d/actor/d_a_obj_keyhole.cpp @@ -10,9 +10,6 @@ #include "d/d_s_play.h" #include "d/actor/d_a_player.h" #include "Z2AudioLib/Z2Instances.h" -#if TARGET_PC -#include "dusk/frame_interpolation.h" -#endif daObj_Keyhole_HIO_c::daObj_Keyhole_HIO_c() { id = -1; @@ -56,21 +53,6 @@ static int daObj_Keyhole_Draw(obj_keyhole_class* i_this) { for (int i = 0; i < 6; i++) { kh_chain_s* chain_s = &i_this->chain_s[i]; for (int j = 0; j < i_this->chain_num; j++) { -#if TARGET_PC - if (dusk::frame_interp::is_enabled() && i_this->mChainInterpPrevValid && i_this->mChainInterpCurrValid) { - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - Mtx mtx; - const f32* p0 = (const f32*)i_this->mChainInterpPrev[i][j]; - const f32* p1 = (const f32*)i_this->mChainInterpCurr[i][j]; - f32* dst = (f32*)mtx; - for (int k = 0; k < 12; k++) { - dst[k] = p0[k] + (p1[k] - p0[k]) * alpha; - } - chain_s->model[j]->setBaseTRMtx(mtx); - g_env_light.setLightTevColorType_MAJI(chain_s->model[j], &actor->tevStr); - mDoExt_modelUpdateDL(chain_s->model[j]); - } else -#endif dComIfGp_entrySimpleModel(chain_s->model[j], fopAcM_GetRoomNo(actor)); } } @@ -388,21 +370,6 @@ static void chain_move(obj_keyhole_class* i_this) { ANGLE_ADD(sp8, TREG_S(0) + 0x3D00); } } - -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mChainInterpCurrValid) { - memcpy(i_this->mChainInterpPrev, i_this->mChainInterpCurr, sizeof(i_this->mChainInterpCurr)); - i_this->mChainInterpPrevValid = true; - } - for (int i = 0; i < 6; i++) { - for (int j = 0; j < i_this->chain_num; j++) { - MTXCopy(i_this->chain_s[i].model[j]->getBaseTRMtx(), i_this->mChainInterpCurr[i][j]); - } - } - i_this->mChainInterpCurrValid = true; - } -#endif } static void open(obj_keyhole_class* i_this) { @@ -783,11 +750,6 @@ static int daObj_Keyhole_Create(fopAc_ac_c* a_this) { return cPhs_ERROR_e; } -#if TARGET_PC - i_this->mChainInterpPrevValid = false; - i_this->mChainInterpCurrValid = false; -#endif - OS_REPORT("//////////////OBJ_KEYHOLE SET 2 !!\n"); if (i_this->arg0 == 3) { diff --git a/src/d/actor/d_a_obj_klift00.cpp b/src/d/actor/d_a_obj_klift00.cpp index fbccb64edb..bec1c33aba 100644 --- a/src/d/actor/d_a_obj_klift00.cpp +++ b/src/d/actor/d_a_obj_klift00.cpp @@ -11,8 +11,13 @@ #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" + +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/samples.h" + +using KLiftInterp = dusk::interp::Samples; +#endif struct daObjKLift00_HIO_c : public mDoHIO_entry_c { daObjKLift00_HIO_c(); @@ -141,7 +146,14 @@ cPhs_Step daObjKLift00_c::create1st() { static const int l_bmdidx[3] = {5, 6, 4}; -void daObjKLift00_c::setMtx() { +void daObjKLift00_c::setMtx(IF_DUSK(bool presentation)) { +#if TARGET_PC + const auto* samples = presentation ? &dusk::interp::get(this) : nullptr; + const auto chainPosition = [&](int i) { + const cXyz& position = mChainPositions[i].mCurrentPos; + return samples != nullptr ? samples->read(i, position) : position; + }; +#endif Mtx nonFoundationChainRotationMatrix; mDoMtx_stack_c::transS(current.pos.x, current.pos.y, current.pos.z); mDoMtx_stack_c::YrotM(shape_angle.y); @@ -149,13 +161,16 @@ void daObjKLift00_c::setMtx() { if(!getNoBaseDisp()) mpChainBase->setBaseTRMtx(mDoMtx_stack_c::get()); + IF_DUSK_BLOCK(!presentation) MTXCopy(mDoMtx_stack_c::get(), mCullMtx); + IF_DUSK_BLOCK_END s16 negativeHomeYAngle = -home.angle.y; // Compute rotation matrices to make the chains' rotation match their change in position for(int i = 0; i < mNumChainModels; i++) { - cXyz vectorToLowerChain = mChainPositions[i + 1].mCurrentPos - mChainPositions[i].mCurrentPos; + cXyz vectorToLowerChain = DUSK_IF_ELSE(chainPosition(i + 1) - chainPosition(i), + mChainPositions[i + 1].mCurrentPos - mChainPositions[i].mCurrentPos); cXyz crossProductXAxis; VECCrossProduct(&cXyz::BaseZ, &vectorToLowerChain, &crossProductXAxis); @@ -178,7 +193,7 @@ void daObjKLift00_c::setMtx() { } negativeHomeYAngle += static_cast(0x4000); - mDoMtx_stack_c::transS(mChainPositions[i].mCurrentPos); + mDoMtx_stack_c::transS(DUSK_IF_ELSE(chainPosition(i), mChainPositions[i].mCurrentPos)); mDoMtx_stack_c::concat(nonFoundationChainRotationMatrix); mDoMtx_stack_c::ZrotM(negativeHomeYAngle); @@ -187,7 +202,9 @@ void daObjKLift00_c::setMtx() { } Mtx foundationChainRotationMatrix; - cXyz vectorFromSecondLowestChainToFoundation = mChainPositions[mNumChainModels].mCurrentPos - mChainPositions[mNumChainModels - 1].mCurrentPos; + cXyz vectorFromSecondLowestChainToFoundation = + DUSK_IF_ELSE(chainPosition(mNumChainModels) - chainPosition(mNumChainModels - 1), + mChainPositions[mNumChainModels].mCurrentPos - mChainPositions[mNumChainModels - 1].mCurrentPos); cXyz crossProduct; Vec inverseBaseY = {0.0f, -1.0f, 0.0f}; if(!cM3d_IsZero(vectorFromSecondLowestChainToFoundation.getSquareMag())) { @@ -208,12 +225,14 @@ void daObjKLift00_c::setMtx() { MTXIdentity(foundationChainRotationMatrix); } - mDoMtx_stack_c::transS(mChainPositions[mNumChainModels].mCurrentPos); + mDoMtx_stack_c::transS(DUSK_IF_ELSE(chainPosition(mNumChainModels), mChainPositions[mNumChainModels].mCurrentPos)); mDoMtx_stack_c::concat(foundationChainRotationMatrix); mDoMtx_stack_c::YrotM(-negativeHomeYAngle); mpLiftPlatform->setBaseTRMtx(mDoMtx_stack_c::get()); mDoMtx_stack_c::scaleM(scale.x, scale.y, scale.z); + IF_DUSK_BLOCK(!presentation) MTXCopy(mDoMtx_stack_c::get(), mNewBgMtx); + IF_DUSK_BLOCK_END } void daObjKLift00_c::rideActor(fopAc_ac_c* riding_actor) { @@ -297,11 +316,6 @@ int daObjKLift00_c::Create() { if(getLock()) mStopSwingingFrames = 5; -#if TARGET_PC - mChainInterpPrevValid = false; - mChainInterpCurrValid = false; -#endif - return 1; } @@ -443,35 +457,16 @@ 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() { +#if TARGET_PC + if (dusk::interp::is_presentation_active()) { + setMtx(true); + } else if (dusk::interp::should_capture()) { + dusk::interp::get(this).capture(mNumChains, [&](int i) { + return mChainPositions[i].mCurrentPos; + }); + } +#endif g_env_light.settingTevStruct(16, ¤t.pos, &tevStr); g_env_light.setLightTevColorType_MAJI(mpLiftPlatform, &tevStr); @@ -492,22 +487,6 @@ int daObjKLift00_c::Draw() { dComIfGd_setList(); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - 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 9ac8206f6b..38809bbfee 100644 --- a/src/d/actor/d_a_obj_lv8Lift.cpp +++ b/src/d/actor/d_a_obj_lv8Lift.cpp @@ -10,10 +10,6 @@ #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; @@ -384,44 +380,7 @@ 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 e367b89b36..dc49f3f69b 100644 --- a/src/d/actor/d_a_obj_lv8OptiLift.cpp +++ b/src/d/actor/d_a_obj_lv8OptiLift.cpp @@ -11,10 +11,6 @@ #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; @@ -416,44 +412,7 @@ 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_obj_wchain.cpp b/src/d/actor/d_a_obj_wchain.cpp index a8f5c39086..5ac92ff23a 100644 --- a/src/d/actor/d_a_obj_wchain.cpp +++ b/src/d/actor/d_a_obj_wchain.cpp @@ -13,6 +13,20 @@ #include "JSystem/J3DGraphBase/J3DMaterial.h" #include "JSystem/J3DGraphBase/J3DDrawBuffer.h" +#if TARGET_PC +#include "dusk/interp/samples.h" + +namespace { +constexpr int CHAIN_COUNT = 16; +struct WchainInterp { + dusk::interp::Samples positions; + dusk::interp::Samples angles; + dusk::interp::Samples rotations; +}; + +} +#endif + static char const l_arcName[7] = "Wchain"; int daObjWchain_c::createHeap() { @@ -309,6 +323,23 @@ void daObjWchain_shape_c::draw() { cXyz* pos = chain->getChainPos(); csXyz* angle = chain->getChainAngle(); s16* rotation = chain->getChainAngleZ(); +#if TARGET_PC + auto& samples = dusk::interp::get(chain); + cXyz positions[CHAIN_COUNT]; + csXyz angles[CHAIN_COUNT]; + s16 rotations[CHAIN_COUNT]; + s16 yaw = chain->shape_angle.y; + for (int i = 0; i < CHAIN_COUNT; ++i) { + positions[i] = samples.positions.read(i, pos[i]); + angles[i] = samples.angles.read(i, angle[i]); + csXyz orientation = samples.rotations.read(i, csXyz(rotation[i], yaw, 0)); + rotations[i] = orientation.x; + } + yaw = samples.rotations.read(0, csXyz(rotation[0], yaw, 0)).y; + pos = positions; + angle = angles; + rotation = rotations; +#endif J3DModelData* model_data = chain->getChainModelData(); J3DMaterial* material = model_data->getMaterialNodePointer(0); dKy_tevstr_c& tevstr = chain->tevStr; @@ -335,21 +366,22 @@ void daObjWchain_shape_c::draw() { mDoMtx_stack_c::copy(j3dSys.getViewMtx()); mDoMtx_stack_c::transM(*pos); mDoMtx_stack_c::ZrotM(*rotation); - mDoMtx_stack_c::ZXYrotM(angle->x, chain->shape_angle.y, angle->z); + mDoMtx_stack_c::ZXYrotM(angle->x, DUSK_IF_ELSE(yaw, chain->shape_angle.y), angle->z); mDoMtx_stack_c::transM(0.0f, 0.0f, -8.75f); GXLoadPosMtxImm(mDoMtx_stack_c::get(), 0); GXLoadNrmMtxImm(mDoMtx_stack_c::get(), 0); material->getShape()->simpleDrawCache(); } cXyz roof_pos(chain->getRoofPos().x, chain->getRealRoofY(), chain->getRoofPos().z); - cXyz delta = roof_pos - *chain->getChainPos(); + IF_DUSK(roof_pos = samples.positions.read(CHAIN_COUNT, roof_pos)); + cXyz delta = roof_pos - DUSK_IF_ELSE(positions[0], *chain->getChainPos()); f32 len = delta.abs(); if (len > 17.5f) { - cXyz pos = *chain->getChainPos(); + cXyz pos = DUSK_IF_ELSE(positions[0], *chain->getChainPos()); csXyz angle( delta.atan2sY_XZ(), - chain->getChainAngle()->y, - chain->getChainAngle()->z + 0x3000 + DUSK_IF_ELSE(angles[0].y, chain->getChainAngle()->y), + DUSK_IF_ELSE(angles[0].z, chain->getChainAngle()->z) + 0x3000 ); delta *= (17.5f / len); for (; len > 17.5f; len -= 17.5f, pos += delta, angle.z += 0x3000) { @@ -371,6 +403,16 @@ int daObjWchain_c::draw() { mDoExt_modelUpdateDL(mpHandleModel); g_env_light.setLightTevColorType_MAJI(mpChainModelData, &tevStr); dComIfGd_getOpaList()->entryImm(&mShape, 0); +#if TARGET_PC + auto& samples = dusk::interp::get(this); + samples.positions.capture(CHAIN_COUNT + 1, [&](int i) { + return i < CHAIN_COUNT ? mChainPos[i] : cXyz(mRoofPos.x, mRealRoofY, mRoofPos.z); + }); + samples.angles.capture(mChainAngle, CHAIN_COUNT); + samples.rotations.capture(CHAIN_COUNT, [&](int i) { + return csXyz(mChainRotation[i], shape_angle.y, 0); + }); +#endif return 1; } diff --git a/src/d/actor/d_a_player.cpp b/src/d/actor/d_a_player.cpp index e10f7e490c..b91b7d012c 100644 --- a/src/d/actor/d_a_player.cpp +++ b/src/d/actor/d_a_player.cpp @@ -15,6 +15,10 @@ #include "d/actor/d_a_midna.h" #include "d/actor/d_a_spinner.h" +#if TARGET_PC +#include "dusk/interp/samples.h" +#endif + bool daPy_frameCtrl_c::checkAnmEnd() { if (getEndFlg() != 0 && getNowSetFlg() == 0) { return true; @@ -224,12 +228,6 @@ daPy_anmHeap_c::~daPy_anmHeap_c() { if (mAnimeHeap != NULL) { mDoExt_destroySolidHeap(mAnimeHeap); } -#if TARGET_PC - freeTempBuffers(); - if (mOwnedBuffer != NULL) { - JKRFreeToSysHeap(mOwnedBuffer); - } -#endif } void daPy_anmHeap_c::initData() { @@ -243,56 +241,6 @@ void* daPy_anmHeap_c::mallocBuffer() { return mBuffer; } -#if TARGET_PC -constexpr u32 kAlignment = 0x20; - -void daPy_anmHeap_c::reserveBuffer(u16 i_resId) { - // Ensure mBuffer is large enough to hold the resource - u32 size = daPy_getAnmResourceSize(i_resId, mBufferSize); - if (size <= mBufferSize) { - return; - } - - // If not, replace it with a new buffer allocated from the system heap. Callers still copy - // archive data in on every load: setAnmTransform bswaps key tables in place, so we can't point - // it at the archive's cached copy directly. - u8* buffer = static_cast(JKRAllocFromSysHeap(size, kAlignment)); - JUT_ASSERT(__LINE__, buffer != NULL); - if (mOwnedBuffer != NULL) { - JKRFreeToSysHeap(mOwnedBuffer); - } - mOwnedBuffer = buffer; // Mark it as owned so we release it later - mBuffer = buffer; - mBufferSize = size; -} - -void* daPy_anmHeap_c::allocTempBuffer(u16 i_resId, u32* io_size) { - // Check if the resource can fit in io_size - u32 size = daPy_getAnmResourceSize(i_resId, *io_size); - if (size <= *io_size) { - return JKR_NEW_ARRAY_ARGS(u8, *io_size, kAlignment); - } - - // If not, allocate a new temp buffer from the system heap, plus kAlignment extra bytes. - // We stash a pointer to the next buffer at the beginning, forming a chain so we can free - // them all later. - void** temp = static_cast(JKRAllocFromSysHeap(kAlignment + size, kAlignment)); - JUT_ASSERT(__LINE__, temp != NULL); - *temp = mTempBuffers; - mTempBuffers = temp; - *io_size = size; - return reinterpret_cast(temp) + kAlignment; -} - -void daPy_anmHeap_c::freeTempBuffers() { - while (mTempBuffers != NULL) { - void** temp = mTempBuffers; - mTempBuffers = static_cast(*temp); - JKRFreeToSysHeap(temp); - } -} -#endif - void daPy_anmHeap_c::createHeap(daPy_anmHeap_c::daAlinkHEAP_TYPE i_heapType, const char* name) { u32 size; @@ -366,7 +314,6 @@ void* daPy_anmHeap_c::loadData(u16 i_resId) { }; if (mArcNo == 0xFFFF) { - IF_DUSK(reserveBuffer(i_resId);) JKRReadIdxResource(mBuffer, mBufferSize, i_resId, dComIfGp_getAnmArchive()); #if DEBUG daPy_aramBufferCheck(mBuffer, mBufferSize); @@ -420,7 +367,6 @@ void* daPy_anmHeap_c::loadDataDemoRID(u16 i_resID, u16 i_arcNo) { JKRHeap* daPy_anmHeap_c::setAnimeHeap() { mAnimeHeap->freeAll(); - IF_DUSK(freeTempBuffers();) return mDoExt_setCurrentHeap(mAnimeHeap); } @@ -429,11 +375,16 @@ JKRHeap* daPy_anmHeap_c::setAnimeHeap() { #include "assets/l_sightDL__d_a_player.h" #endif +#if TARGET_PC +daPy_sightPacket_c::~daPy_sightPacket_c() { + dusk::interp::erase_owned_samples(this); +} +#endif + void daPy_sightPacket_c::draw() { ZoneScoped; -#if !TARGET_PC - TGXTexObj texObj; -#endif + IF_DUSK(setSight(false);) + IF_NOT_DUSK(TGXTexObj texObj); j3dSys.reinitGX(); GXSetNumIndStages(0); @@ -499,13 +450,21 @@ void daPy_sightPacket_c::draw() { J3DShape::resetVcdVatCache(); } -void daPy_sightPacket_c::setSight() { +void daPy_sightPacket_c::setSight(IF_DUSK(bool registerPacket)) { Vec proj; mDoLib_project(&mPos, &proj); +#if TARGET_PC + auto& positions = dusk::interp::get>(this); + const cXyz screen(proj); + positions.capture(&screen, 1); + proj = positions.read(0, screen); +#endif mDoMtx_stack_c::transS(proj.x, proj.y, proj.z); mDoMtx_stack_c::scaleM(32.0f, 32.0f, 32.0f); mDoMtx_copy(mDoMtx_stack_c::get(), mProjMtx); + IF_DUSK_BLOCK(registerPacket) dComIfGd_set2DXlu(this); + IF_DUSK_BLOCK_END } void daPy_sightPacket_c::setSightImage(ResTIMG* i_img) { diff --git a/src/d/actor/d_a_title.cpp b/src/d/actor/d_a_title.cpp index fd550070f3..982bda141a 100644 --- a/src/d/actor/d_a_title.cpp +++ b/src/d/actor/d_a_title.cpp @@ -19,7 +19,9 @@ #include "m_Do/m_Do_graphic.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/user_interface.h" #include "dusk/version.hpp" #endif @@ -151,6 +153,8 @@ int daTitle_c::create() { g_daTitHIO.id = mDoHIO_CREATE_CHILD("タイトルロゴ", &g_daTitHIO); + IF_DUSK(draw_interp_frame = true); + return phase_state; } @@ -168,20 +172,14 @@ int daTitle_c::Execute() { return 1; } -#ifdef TARGET_PC - if (!dusk::frame_interp::is_enabled()) { -#endif - dMenu_Collect3D_c::setViewPortOffsetY(0.0f); -#ifdef TARGET_PC - } -#endif + IF_NOT_DUSK(dMenu_Collect3D_c::setViewPortOffsetY(0.0f)); if (mDoRst::isReset()) { return 1; } (this->*daTitleProc[mProcID])(); - KeyWaitAnm(); + IF_NOT_DUSK(KeyWaitAnm()); #if VERSION == VERSION_SHIELD_DEBUG KeyWaitPosMove(); @@ -191,6 +189,45 @@ int daTitle_c::Execute() { } void daTitle_c::KeyWaitAnm() { +#if TARGET_PC + if (field_0x5f9 == 0) { + return; + } + + const f32 duration = g_daTitHIO.mArrow; + if (duration <= 0.0f) { + field_0x600->alphaAnimeStart(0.0f); + field_0x600->setAlpha(255); + return; + } + + f32 remaining = dusk::game_clock::original_frames(); + f32 frame = field_0x600->getAlphaTimer(); + if (field_0x5fa != 0) { + const f32 step = std::min(remaining, std::max(0.0f, duration - frame)); + frame += step; + remaining -= step; + if (frame < duration) { + field_0x600->alphaAnimeStart(frame); + field_0x600->setAlpha(255.0f * frame / duration); + return; + } + field_0x5fa = 0; + frame = 0.0f; + field_0x604 = std::max(1.0f, 2.0f * g_daTitHIO.field_0x1a - 1.0f); + } + + const f32 holdStep = std::min(remaining, field_0x604); + field_0x604 -= holdStep; + remaining -= holdStep; + const f32 period = duration + std::max(0.0f, g_daTitHIO.field_0x1a - 1.0f); + frame = std::fmod(frame + remaining, period); + field_0x600->alphaAnimeStart(frame); + const f32 pulse = std::min(frame, duration); + const f32 half = std::max(1.0f, std::floor(duration / 2.0f)); + const f32 rate = std::min(1.0f, std::min(pulse, duration - pulse) / half); + field_0x600->setAlpha(255.0f - 127.0f * rate); +#else if (field_0x5f9 != 0) { if (field_0x604 == 0) { if (field_0x5fa != 0) { @@ -211,6 +248,7 @@ void daTitle_c::KeyWaitAnm() { field_0x604--; } } +#endif } #if VERSION == VERSION_SHIELD_DEBUG @@ -352,7 +390,7 @@ void daTitle_c::fastLogoDispInit() { mWaitTimer = 30; mProcID = 5; - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } void daTitle_c::fastLogoDisp() { @@ -380,6 +418,15 @@ int daTitle_c::getDemoPrm() { } int daTitle_c::Draw() { +#if TARGET_PC + if (field_0x600 != NULL && dusk::game_clock::is_presentation_frame()) { + if (!fopOvlpM_IsPeek() && !mDoRst::isReset()) { + KeyWaitAnm(); + } + field_0x600->presentAlphaAnime(); + } +#endif + J3DModelData* modelData = mpModel->getModelData(); cMtx_trans(mpModel->getBaseTRMtx(), IREG_F(7), IREG_F(8), IREG_F(9) + -430.0f); mpModel->getBaseScale()->x = -1.0f; @@ -389,6 +436,7 @@ int daTitle_c::Draw() { mBrk.entry(modelData); mBtk.entry(modelData); + IF_DUSK(dMenu_Collect3D_c::setViewPortOffsetY(0.0f)); dComIfGd_setListItem3D(); mDoExt_modelUpdateDL(mpModel); dComIfGd_setList(); diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 91eba47906..e42cc7ded0 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -6,7 +6,7 @@ #include "SSystem/SComponent/c_counter.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif const u16 l_J_Ohana00_64TEX__width = 63; @@ -1165,16 +1165,12 @@ void dFlower_packet_c::draw() { GXSetChanAmbColor(GX_COLOR0A0, sp64); if (!cLib_checkBit(sp44->m_state, 4) && !cLib_checkBit(sp44->m_state, 0x40)) { -#ifdef TARGET_PC - Mtx flower_mtx; - if (dusk::frame_interp::lookup_replacement(&sp44->m_modelMtx, flower_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx); - GXLoadPosMtxImm(flower_mtx, 0); - } else +#if TARGET_PC + Mtx flowerMtx; + GXLoadPosMtxImm(get_model_mtx(sp44->m_modelMtx, flowerMtx), 0); +#else + GXLoadPosMtxImm(sp44->m_modelMtx, 0); #endif - { - GXLoadPosMtxImm(sp44->m_modelMtx, 0); - } GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); #if TARGET_PC @@ -1324,16 +1320,12 @@ void dFlower_packet_c::draw() { sp30++; if (!cLib_checkBit(sp34->m_state, 4) && cLib_checkBit(sp34->m_state, 0x40)) { -#ifdef TARGET_PC - Mtx flower_mtx; - if (dusk::frame_interp::lookup_replacement(&sp34->m_modelMtx, flower_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx); - GXLoadPosMtxImm(flower_mtx, 0); - } else +#if TARGET_PC + Mtx flowerMtx; + GXLoadPosMtxImm(get_model_mtx(sp34->m_modelMtx, flowerMtx), 0); +#else + GXLoadPosMtxImm(sp34->m_modelMtx, 0); #endif - { - GXLoadPosMtxImm(sp34->m_modelMtx, 0); - } GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); #if TARGET_PC GXLoadTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, GX_TEXMAP0); @@ -1463,9 +1455,13 @@ void dFlower_packet_c::update() { mDoMtx_stack_c::copy(temp_r28); mDoMtx_stack_c::scaleM(temp_f31, temp_f31, temp_f31); +#if TARGET_PC + cMtx_copy(temp_r28, data_p->m_modelMtx); +#else cMtx_concat(j3dSys.getViewMtx(), temp_r28, data_p->m_modelMtx); +#endif #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(temp_r28, data_p->m_modelMtx); + dusk::interp::record_final_mtx(temp_r28, data_p->m_modelMtx); #endif } } diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index 780a7a052d..fd911083f4 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -13,7 +13,7 @@ #if TARGET_PC #include "dusk/dvd_asset.hpp" -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" using GameVersion = dusk::version::GameVersion; @@ -608,11 +608,12 @@ dGrass_packet_c::dGrass_packet_c() { #if TARGET_PC static MtxP get_model_mtx(Mtx modelMtx, Mtx storage) { - if (dusk::frame_interp::lookup_replacement(modelMtx, storage)) { + if (dusk::interp::lookup_replacement(modelMtx, storage)) { cMtx_concat(j3dSys.getViewMtx(), storage, storage); - return storage; + } else { + cMtx_concat(j3dSys.getViewMtx(), modelMtx, storage); } - return modelMtx; + return storage; } static void transform_positions( @@ -1193,16 +1194,12 @@ void dGrass_packet_c::draw() { GXSetChanAmbColor(GX_COLOR0A0, sp38); if (!cLib_checkBit(var_r29->field_0x01, 2)) { -#ifdef TARGET_PC - Mtx grass_mtx; - if (dusk::frame_interp::lookup_replacement(reinterpret_cast(&var_r29->m_modelMtx), grass_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), grass_mtx, grass_mtx); - GXLoadPosMtxImm(grass_mtx, 0); - } else +#if TARGET_PC + Mtx grassMtx; + GXLoadPosMtxImm(get_model_mtx(var_r29->m_modelMtx, grassMtx), 0); +#else + GXLoadPosMtxImm(var_r29->m_modelMtx, 0); #endif - { - GXLoadPosMtxImm(var_r29->m_modelMtx, 0); - } GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); if (var_r29->field_0x05 <= 3 || var_r29->field_0x05 >= 10) { if (var_r29->field_0x02 < -1) { @@ -1445,7 +1442,11 @@ void dGrass_packet_c::update() { } } +#if TARGET_PC + cMtx_copy(mDoMtx_stack_c::get(), data_p->m_modelMtx); +#else cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), data_p->m_modelMtx); +#endif } else { mDoMtx_stack_c::transS(data_p->m_pos.x, data_p->m_pos.y, data_p->m_pos.z); mDoMtx_stack_c::YrotM(i * 3535); @@ -1456,11 +1457,13 @@ void dGrass_packet_c::update() { f32 scale = ((((s16)data_p->m_pos.x * 3535) & 0xFFF) / 4096.0f) * 0.3f + 0.7f; mDoMtx_stack_c::scaleM(scale, scale, scale); +#if TARGET_PC + cMtx_copy(mDoMtx_stack_c::get(), data_p->m_modelMtx); +#else cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), data_p->m_modelMtx); - } -#ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), data_p->m_modelMtx); #endif + } + IF_DUSK(dusk::interp::record_final_mtx(mDoMtx_stack_c::get(), data_p->m_modelMtx)); } } data_p++; diff --git a/src/d/d_attention.cpp b/src/d/d_attention.cpp index beea40e806..49c7465d48 100644 --- a/src/d/d_attention.cpp +++ b/src/d/d_attention.cpp @@ -164,6 +164,7 @@ dAttention_c::dAttention_c(fopAc_ac_c* i_player, u32 i_padNo) { mPadNo = i_padNo; mLockTargetID = fpcM_ERROR_PROCESS_ID_e; + IF_DUSK(mTargetActorID = fpcM_ERROR_PROCESS_ID_e); field_0x32e = 0; field_0x32f = 0; @@ -1455,6 +1456,13 @@ if (dusk::getSettings().game.recordingMode) { } #endif +#if TARGET_PC + if (mTargetActorID != fopAcM_GetID(lockon_actor) || + (fopAcM_GetName(lockon_actor) == fpcNm_Tag_Wljump_e && + mDrawAttnPos != lockon_actor->attention_info.position)) { + draw[0].mModel[draw[0].mDrawType]->forgetMtx(); + } +#endif draw[0].draw(lockon_actor->attention_info.position, inv_m); if (mLockonCount >= 2 && draw[1].field_0x173 == 2) { @@ -1483,6 +1491,12 @@ if (dusk::getSettings().game.recordingMode) { fopAc_ac_c* actor = fopAcM_SearchByID(mTargetActorID); if (actor != NULL) { +#if TARGET_PC + if (fopAcM_GetName(actor) == fpcNm_Tag_Wljump_e && + mDrawAttnPos != actor->attention_info.position) { + draw[0].mModel[draw[0].mDrawType]->forgetMtx(); + } +#endif draw[0].draw(actor->attention_info.position, inv_m); mDrawAttnPos = actor->attention_info.position; } else { @@ -1506,6 +1520,7 @@ void dAttention_c::lockSoundStart(u32 i_sfxID) { void dAttDraw_c::setAnm(u8 i_drawType, f32 i_anmSpeed) { mDrawType = i_drawType; + IF_DUSK(mModel[mDrawType]->forgetMtx()); mNoticeCursorBck[mDrawType].reset(); mNoticeCursorBck[mDrawType].setPlaySpeed(i_anmSpeed); mNoticeCursorBpk[mDrawType].reset(); diff --git a/src/d/d_bg_s.cpp b/src/d/d_bg_s.cpp index 23f87e4e4c..e8643f83fe 100644 --- a/src/d/d_bg_s.cpp +++ b/src/d/d_bg_s.cpp @@ -315,7 +315,7 @@ f32 cBgS::GroundCross(cBgS_GndChk* pgndchk) { } template <> -void be_swap(cBgD_Vtx_t& val) { +constexpr void be_swap(cBgD_Vtx_t& val) noexcept { be_swap(val.x); be_swap(val.y); be_swap(val.z); diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 9bb5b6f383..fe3bb91d2e 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -32,7 +32,9 @@ #include "dusk/action_bindings.h" #include "dusk/camera_operators.hpp" #include "dusk/commands.hpp" -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/camera.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/logging.h" #include "dusk/mouse.h" #include "dusk/settings.h" @@ -10503,13 +10505,13 @@ bool dCamera_c::eventCamera(s32 param_0) { #endif #if TARGET_PC - if (dusk::frame_interp::is_enabled()) { + if (dusk::interp::is_enabled()) { switch (var_r29) { case 3: case 4: case 5: case 12: - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); break; default: DuskLog.debug( @@ -10591,9 +10593,11 @@ bool dCamera_c::eventCamera(s32 param_0) { if (getEvIntData(&sp24, "LoadPos") != 0) { if ((sp24 == 0) || (sp24 == 1)) { popInfo(&mSavedViewStack[sp24]); + IF_DUSK(dusk::interp::request_presentation_sync()); } if (sp24 == 2) { popInfo(&mSavedView); + IF_DUSK(dusk::interp::request_presentation_sync()); } } } @@ -10788,6 +10792,7 @@ s16 dCamera_c::runEventRecoveryTrans() { void dCamera_c::EventRecoverNotime() { mRecovery.field_0x4 = 1; Reset(mRecovery.field_0x8.mCenter, mRecovery.field_0x8.mEye, mRecovery.field_0x8.mFovy, mRecovery.field_0x8.mBank); + IF_DUSK(dusk::interp::request_presentation_sync()); } int dCamera_c::Set(cXyz i_center, cXyz i_eye) { @@ -11359,7 +11364,7 @@ void widezoom_correction(camera_process_class* i_this, float trim_height) { trim_width = FB_WIDTH_BASE / 2.0f * (1.0f - target_ar_real / current_ar); } - if (dusk::frame_interp::is_sim_frame()) { + if (dusk::game_clock::is_sim_frame()) { constexpr auto base_ar = static_cast(FB_WIDTH_BASE) / static_cast(FB_HEIGHT_BASE); const auto ar_corr = base_ar / std::min(current_ar, target_ar_real); @@ -11397,8 +11402,8 @@ static int camera_execute(camera_process_class* i_this) { #ifdef TARGET_PC widezoom_correction(i_this, i_this->mCamera.TrimHeight()); - if (dusk::frame_interp::is_enabled()) { - dusk::frame_interp::add_interpolation_callback([](bool _, void* pUserWork) { + if (dusk::interp::is_enabled()) { + dusk::interp::add_interpolation_callback([](void* pUserWork) { const auto i_this = static_cast(pUserWork); const auto camera = &i_this->mCamera; @@ -11407,7 +11412,7 @@ static int camera_execute(camera_process_class* i_this) { if (camera->mCurState != 2 && trim_size >= 0 && trim_size <= 3) { // derive trim height at previous tick using current camera state const auto target = get_target_trim_height(i_this); - const auto step = dusk::frame_interp::get_interpolation_step(); + const auto step = dusk::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; @@ -11418,10 +11423,10 @@ static int camera_execute(camera_process_class* i_this) { } // record new camera for our sim frame - dusk::frame_interp::record_camera(i_this, get_camera_id(i_this)); + dusk::interp::record_camera(i_this, get_camera_id(i_this)); // interpolate the view now so that this sim frame's view matrix matches what // we'll be rendering with later - dusk::frame_interp::interp_view(&i_this->view); + dusk::interp::interp_view(&i_this->view); #endif view_setup(i_this); diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp index fd1a82ba2f..d50c0500f3 100644 --- a/src/d/d_drawlist.cpp +++ b/src/d/d_drawlist.cpp @@ -14,16 +14,18 @@ #include "m_Do/m_Do_mtx.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/logging.h" #include "helpers/gx_helper.h" +#include "d/d_menu_collect.h" + #include "JSystem/JKernel/JKRHeap.h" #include #include -#include #include static const void* getInterpKey(const void* base, int idx) { @@ -1081,7 +1083,7 @@ void dDlst_shadowReal_c::reset() { void dDlst_shadowReal_c::imageDraw(Mtx param_0) { #ifdef TARGET_PC Mtx render_proj_mtx; - if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 2), render_proj_mtx)) { + if (dusk::interp::lookup_replacement(getInterpKey(mpModels[0], 2), render_proj_mtx)) { GXSetProjection(render_proj_mtx, GX_ORTHOGRAPHIC); } else #endif @@ -1102,7 +1104,7 @@ void dDlst_shadowReal_c::imageDraw(Mtx param_0) { shape_pkt = (*models)->getShapePacket(j); #ifdef TARGET_PC Mtx view_mtx; - if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx)) { + if (dusk::interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx)) { shape_pkt->setBaseMtxPtr(&view_mtx); } else #endif @@ -1131,8 +1133,8 @@ void dDlst_shadowReal_c::draw() { GXSetCurrentMtx(GX_PNMTX0); #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); + const auto have_view_mtx = dusk::interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx); + const auto have_recv_proj_mtx = dusk::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); @@ -1300,9 +1302,9 @@ u8 dDlst_shadowReal_c::setShadowRealMtx(cXyz* param_0, cXyz* param_1, f32 param_ #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)); + dusk::interp::record_final_mtx(mViewMtx, getInterpKey(keybase, 1)); + dusk::interp::record_final_mtx(mRenderProjMtx, getInterpKey(keybase, 2)); + dusk::interp::record_final_mtx(mReceiverProjMtx, getInterpKey(keybase, 3)); #endif cMtx_concat(mReceiverProjMtx, mViewMtx, mReceiverProjMtx); return r29; @@ -1364,6 +1366,16 @@ bool dDlst_shadowReal_c::add(J3DModel* i_model) { return true; } +#if TARGET_PC +static MtxP get_simple_shadow_mtx(Mtx worldMtx, const void* key, Mtx storage) { + if (!dusk::interp::lookup_replacement(key, storage)) { + cMtx_copy(worldMtx, storage); + } + cMtx_concat(j3dSys.getViewMtx(), storage, storage); + return storage; +} +#endif + void dDlst_shadowSimple_c::draw() { static GXColor l_color = {0, 0, 0, 64}; l_color.a = mAlpha; @@ -1371,31 +1383,23 @@ void dDlst_shadowSimple_c::draw() { GXSetTevColor(GX_TEVREG0, l_color); GXClearVtxDesc(); GXSetVtxDesc(GX_VA_POS, GX_INDEX8); -#ifdef TARGET_PC - Mtx volume_mtx; - if (dusk::frame_interp::lookup_replacement(mVolumeMtxKey, volume_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), volume_mtx, volume_mtx); - GXLoadPosMtxImm(volume_mtx, GX_PNMTX0); - } else +#if TARGET_PC + Mtx volumeMtx; + GXLoadPosMtxImm(get_simple_shadow_mtx(mVolumeMtx, mVolumeMtxKey, volumeMtx), GX_PNMTX0); +#else + GXLoadPosMtxImm(mVolumeMtx, GX_PNMTX0); #endif - { - GXLoadPosMtxImm(mVolumeMtx, GX_PNMTX0); - } GXSetCurrentMtx(GX_PNMTX0); GXCallDisplayList(l_frontMat, 0x40); GXCallDisplayList(l_shadowVolumeDL, 0x40); GXCallDisplayList(l_backSubMat, 0x20); GXCallDisplayList(l_shadowVolumeDL, 0x40); -#ifdef TARGET_PC - Mtx shadow_mtx; - if (dusk::frame_interp::lookup_replacement(mMtxKey, shadow_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), shadow_mtx, shadow_mtx); - GXLoadPosMtxImm(shadow_mtx, GX_PNMTX1); - } else +#if TARGET_PC + Mtx shadowMtx; + GXLoadPosMtxImm(get_simple_shadow_mtx(mMtx, mMtxKey, shadowMtx), GX_PNMTX1); +#else + GXLoadPosMtxImm(mMtx, GX_PNMTX1); #endif - { - GXLoadPosMtxImm(mMtx, GX_PNMTX1); - } GXSetCurrentMtx(GX_PNMTX1); if (mpTexObj != NULL) { @@ -1450,9 +1454,11 @@ void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* pa mDoMtx_stack_c::scaleM(param_2, f30 + f30 + 16.0f, param_2 * param_5); #if TARGET_PC mVolumeMtxKey = getInterpKey(param_0, 0x1); - dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), mVolumeMtxKey); -#endif + dusk::interp::record_final_mtx(mDoMtx_stack_c::get(), mVolumeMtxKey); + cMtx_copy(mDoMtx_stack_c::get(), mVolumeMtx); +#else cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), mVolumeMtx); +#endif f32 f31 = JMAFastSqrt(1.0f - param_3->x * param_3->x); f32 f29; f32 f28; @@ -1477,11 +1483,13 @@ void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* pa mDoMtx_stack_c::get()[2][3] = param_0->z; mDoMtx_stack_c::YrotM(param_4); mDoMtx_stack_c::scaleM(param_2, 1.0f, param_2 * param_5); -#ifdef TARGET_PC +#if TARGET_PC mMtxKey = getInterpKey(param_0, 0x2); - dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), mMtxKey); -#endif + dusk::interp::record_final_mtx(mDoMtx_stack_c::get(), mMtxKey); + cMtx_copy(mDoMtx_stack_c::get(), mMtx); +#else cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), mMtx); +#endif mpTexObj = param_6; } @@ -1642,7 +1650,7 @@ void dDlst_shadowControl_c::draw(Mtx param_0) { GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); #ifdef TARGET_PC Mtx draw_mtx; - if (dusk::frame_interp::lookup_replacement(param_0, draw_mtx)) { + if (dusk::interp::lookup_replacement(param_0, draw_mtx)) { GXLoadPosMtxImm(draw_mtx, GX_PNMTX0); } else { #endif @@ -1925,6 +1933,7 @@ dDlst_list_c::~dDlst_list_c() { } void dDlst_list_c::reset() { + IF_DUSK(dMenu_Collect3D_c::setViewPortOffsetY(-100.0f)); J3DDrawBuffer** buffer = mDrawBuffers; for (int i = 0; i < 21; i++) { J3DDrawBuffer* tmp = *buffer; @@ -1991,7 +2000,7 @@ void dDlst_list_c::drawXluListItem3d() { } int dDlst_list_c::set(dDlst_base_c**& p_start, dDlst_base_c**& p_end, dDlst_base_c* p_newDlst) { - if (p_start >= p_end) { + if (p_start >= p_end IF_DUSK(|| !dusk::game_clock::is_sim_frame())) { return 0; } *p_start = p_newDlst; @@ -2060,7 +2069,7 @@ void dDlst_list_c::wipeIn(f32 i_wipeSpeed) { void dDlst_list_c::calcWipe() { if (mWipe) { - mWipeRate += mWipeSpeed; + mWipeRate += mWipeSpeed IF_DUSK(* dusk::game_clock::original_frames()); if (mWipeRate < 0.0f) { mWipeRate = 0.0f; } else if (mWipeRate > 1.0f) { @@ -2074,10 +2083,10 @@ void dDlst_list_c::calcWipe() { } #if TARGET_PC -void dDlst_list_c::refresh3DlineMats(const cXyz& eye) { +void dDlst_list_c::refresh3DlineMats() { for (int i = 0; i < 3; i++) { for (mDoExt_3DlineMat_c* mat = m3DLineMatSortPacket[i].getFirstMat(); mat != NULL; mat = mat->field_0x4) { - mat->refreshGeometryForPresentationEye(eye); + mat->refreshGeometryForPresentation(); } } } diff --git a/src/d/d_ev_camera.cpp b/src/d/d_ev_camera.cpp index 78a2d0b657..d46ee8a94b 100644 --- a/src/d/d_ev_camera.cpp +++ b/src/d/d_ev_camera.cpp @@ -14,6 +14,8 @@ #include #if TARGET_PC +#include "dusk/interp/frame_interpolation.h" + #include "helpers/string.hpp" #define LOAD_4BYTE_STRING_LITERAL(x) read_big_endian_u32(x) @@ -1832,6 +1834,7 @@ bool dCamera_c::restorePosEvCamera() { mViewCache.mDirection.Val(restorePos->field_0x48.mEye - restorePos->field_0x48.mCenter); mViewCache.mEye = mViewCache.mCenter + mViewCache.mDirection.Xyz(); mViewCache.mFovy = restorePos->field_0x48.mFovy; + IF_DUSK(dusk::interp::request_presentation_sync()); } } @@ -2683,6 +2686,11 @@ bool dCamera_c::loadEvCamera() { } if (mCurCamStyleTimer < load->mTimer) { +#if TARGET_PC + if (mCurCamStyleTimer == 0 && load->mTimer == 1) { + dusk::interp::request_presentation_sync(); + } +#endif f32 fVar5; if (load->mType != 0) { mEventData.field_0xf0.Step(); @@ -3488,6 +3496,11 @@ bool dCamera_c::fixedFramesEvCamera() { mViewCache.mEye = fframes_p->field_0x10; mViewCache.mFovy = fframes_p->field_0x28; mViewCache.mDirection.Val(mViewCache.mEye - mViewCache.mCenter); +#if TARGET_PC + if (mCurCamStyleTimer == 0) { + dusk::interp::request_presentation_sync(); + } +#endif if (fframes_p->field_0x0 && mCurCamStyleTimer < fframes_p->mTimer) { return 0; } @@ -3541,6 +3554,11 @@ bool dCamera_c::bSplineEvCamera() { } if (bSpline->field_0x1c == 0 && mEventData.field_0xf0.Step()) { +#if TARGET_PC + if (mCurCamStyleTimer == 0) { + dusk::interp::request_presentation_sync(); + } +#endif cXyz tmpcenters = mEventData.field_0xf0.Calc(bSpline->mCenters); cXyz tmpeyes = mEventData.field_0xf0.Calc(bSpline->mEyes); diff --git a/src/d/d_file_sel_warning.cpp b/src/d/d_file_sel_warning.cpp index 97b667bb45..b76998c038 100644 --- a/src/d/d_file_sel_warning.cpp +++ b/src/d/d_file_sel_warning.cpp @@ -13,6 +13,7 @@ #include "d/d_pane_class.h" #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/version.hpp" #endif @@ -61,6 +62,10 @@ void dFile_warning_c::screenSet() { mFileWarn.Scr->setAnimation(field_0x24); field_0x24->setFrame(2849.0f); mFileWarn.Scr->animation(); +#if TARGET_PC + field_0x28 = 2849; + field_0x2c = 2849; +#endif mFileWarn.mFont = mDoExt_getMesgFont(); mpRootPane = JKR_NEW CPaneMgr(mFileWarn.Scr, MULTI_CHAR('Nm_02'), 0, NULL); @@ -116,6 +121,7 @@ bool dFile_warning_c::baseMoveAnm() { bool rt; if (field_0x28 != field_0x2c) { +#if !TARGET_PC if (field_0x28 < field_0x2c) { field_0x28 += 2; @@ -132,6 +138,7 @@ bool dFile_warning_c::baseMoveAnm() { field_0x24->setFrame(field_0x28); mFileWarn.Scr->animation(); +#endif rt = false; } @@ -149,6 +156,19 @@ bool dFile_warning_c::baseMoveAnm() { return rt; } +#if TARGET_PC +void dFile_warning_c::presentAnims() { + if (field_0x28 == field_0x2c) { + return; + } + dusk::vdt::present_toward(field_0x28, (f32)field_0x2c, field_0x24); + mFileWarn.Scr->animation(); + if (mPosY != 0.0f) { + mpRootPane->translate(mpRootPane->getTranslateX(), mPosY - field_0x34); + } +} +#endif + void dFile_warning_c::openInit() { field_0x28 = 2849; field_0x2c = 2859; diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index ed73d5f9cf..9a2a7d194b 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -24,7 +24,9 @@ #include #if TARGET_PC +#include "dusk/game_clock.h" #include "dusk/game_mode.hpp" +#include "dusk/interp/user_interface.h" #include "dusk/menu_pointer.h" #include "dusk/mods/svc/save.hpp" #include "dusk/utilities.hpp" @@ -357,6 +359,8 @@ static DataSelProcFunc DataSelProc[] = { }; void dFile_select_c::_move() { + IF_DUSK(mNameMoveRequested = false); + #if DEBUG if (g_fsHIO.title_mesg_check) { titleMsgCheck(); @@ -393,6 +397,7 @@ void dFile_select_c::_move() { (this->*DataSelProc[mDataSelProc])(); +#if !TARGET_PC selFileWakuAnm(); bookIconAnm(); dataDelEffAnm(); @@ -408,6 +413,7 @@ void dFile_select_c::_move() { copyBookIconAnm(); mCpSel.Scr->animation(); } +#endif mpFileWarning->_move(); mpFileSelect3d->_move(); @@ -477,6 +483,15 @@ void dFile_select_c::errorMsgCheck() { #endif void dFile_select_c::selFileWakuAnm() { +#if TARGET_PC + dusk::vdt::present_looping(mSelFileBpkFrame, mFileSelBpk, 2.0f); + dusk::vdt::present_looping(mSelFileBtk05Frame, mFileSel05Btk, 2.0f); + dusk::vdt::present_looping(mYnSelBpkFrame, mYnSelBpk, 2.0f); + dusk::vdt::present_looping(mYnSelBtkFrame, mYnSelBtk, 2.0f); + dusk::vdt::present_looping(m3mBpkFrame, m3mBpk, 2.0f); + dusk::vdt::present_looping(m3mBtkFrame, m3mBtk, 2.0f); + dusk::vdt::present_looping(mSelDtBtkFrame, mSelDtBtk, 2.0f); +#else mSelFileBpkFrame += 2; if (mSelFileBpkFrame >= mFileSelBpk->getFrameMax()) mSelFileBpkFrame -= mFileSelBpk->getFrameMax(); @@ -511,9 +526,15 @@ void dFile_select_c::selFileWakuAnm() { if (mSelDtBtkFrame >= mSelDtBtk->getFrameMax()) mSelDtBtkFrame -= mSelDtBtk->getFrameMax(); mSelDtBtk->setFrame(mSelDtBtkFrame); +#endif } void dFile_select_c::bookIconAnm() { +#if TARGET_PC + dusk::vdt::present_looping(mSelFileBookBpkFrame, mSelFileBookBpk, 2.0f); + dusk::vdt::present_looping(mSelFileBookBtkFrame, mSelFileBookBtk, 2.0f); + dusk::vdt::present_looping(mSelFileBookBrkFrame, mSelFileBookBrk, 2.0f); +#else mSelFileBookBpkFrame += 2; if (mSelFileBookBpkFrame >= mSelFileBookBpk->getFrameMax()) mSelFileBookBpkFrame -= mSelFileBookBpk->getFrameMax(); @@ -528,9 +549,14 @@ void dFile_select_c::bookIconAnm() { if (mSelFileBookBrkFrame >= mSelFileBookBrk->getFrameMax()) mSelFileBookBrkFrame -= mSelFileBookBrk->getFrameMax(); mSelFileBookBrk->setFrame(mSelFileBookBrkFrame); +#endif } void dFile_select_c::selCopyFileWakuAnm() { +#if TARGET_PC + dusk::vdt::present_looping(mCpSelBpkFrame, mCpSelBpk, 2.0f); + dusk::vdt::present_looping(mCpSel03BtkFrame, mCpSel03Btk, 2.0f); +#else mCpSelBpkFrame += 2; if (mCpSelBpkFrame >= mCpSelBpk->getFrameMax()) mCpSelBpkFrame -= mCpSelBpk->getFrameMax(); @@ -540,9 +566,15 @@ void dFile_select_c::selCopyFileWakuAnm() { if (mCpSel03BtkFrame >= mCpSel03Btk->getFrameMax()) mCpSel03BtkFrame -= mCpSel03Btk->getFrameMax(); mCpSel03Btk->setFrame(mCpSel03BtkFrame); +#endif } void dFile_select_c::copyBookIconAnm() { +#if TARGET_PC + dusk::vdt::present_looping(mCpSelBookBpkFrame, mCpSelBookBpk, 2.0f); + dusk::vdt::present_looping(mCpSelBookBtkFrame, mCpSelBookBtk, 2.0f); + dusk::vdt::present_looping(mCpSelBookBrkFrame, mCpSelBookBrk, 2.0f); +#else mCpSelBookBpkFrame += 2; if (mCpSelBookBpkFrame >= mCpSelBookBpk->getFrameMax()) mCpSelBookBpkFrame -= mCpSelBookBpk->getFrameMax(); @@ -557,10 +589,15 @@ void dFile_select_c::copyBookIconAnm() { if (mCpSelBookBrkFrame >= mCpSelBookBrk->getFrameMax()) mCpSelBookBrkFrame -= mCpSelBookBrk->getFrameMax(); mCpSelBookBrk->setFrame(mCpSelBookBrkFrame); +#endif } void dFile_select_c::dataDelEffAnm() { if (field_0x0208 != 0) { +#if TARGET_PC + dusk::vdt::present_looping(mCpDtEffBrkFrame, mCpDtEffBrk, 2.0f); + dusk::vdt::present_looping(mDtEffBtkFrame, mDtEffBtk, 2.0f); +#else mCpDtEffBrkFrame += 2; if (mCpDtEffBrkFrame >= mCpDtEffBrk->getFrameMax()) mCpDtEffBrkFrame -= mCpDtEffBrk->getFrameMax(); @@ -570,11 +607,16 @@ void dFile_select_c::dataDelEffAnm() { if (mDtEffBtkFrame >= mDtEffBtk->getFrameMax()) mDtEffBtkFrame -= mDtEffBtk->getFrameMax(); mDtEffBtk->setFrame(mDtEffBtkFrame); +#endif } } void dFile_select_c::dataCopyEffAnm() { if (field_0x0209 != 0) { +#if TARGET_PC + dusk::vdt::present_looping(mCpDtEffBrkFrame, mCpDtEffBrk, 2.0f); + dusk::vdt::present_looping(mCpEffBtkFrame, mCpEffBtk, 2.0f); +#else mCpDtEffBrkFrame += 2; if (mCpDtEffBrkFrame >= mCpDtEffBrk->getFrameMax()) mCpDtEffBrkFrame -= mCpDtEffBrk->getFrameMax(); @@ -584,6 +626,7 @@ void dFile_select_c::dataCopyEffAnm() { if (mCpEffBtkFrame >= mCpEffBtk->getFrameMax()) mCpEffBtkFrame -= mCpEffBtk->getFrameMax(); mCpEffBtk->setFrame(mCpEffBtkFrame); +#endif } } @@ -602,6 +645,7 @@ void dFile_select_c::selectDataBaseMoveAnmInitSet(int i_frame, int i_frameMax) { bool dFile_select_c::selectDataBaseMoveAnm() { bool ret; if (mBaseMoveAnmFrame != mBaseMoveAnmFrameMax) { +#if !TARGET_PC if (mBaseMoveAnmFrame < mBaseMoveAnmFrameMax) { mBaseMoveAnmFrame += 2; if (mBaseMoveAnmFrame > mBaseMoveAnmFrameMax) @@ -614,6 +658,7 @@ bool dFile_select_c::selectDataBaseMoveAnm() { mBaseMoveAnm->setFrame(mBaseMoveAnmFrame); mBaseMovePane->getPanePtr()->animationTransform(); +#endif ret = false; } else { if (mBaseMoveAnmFrame == 33) { @@ -753,6 +798,7 @@ void dFile_select_c::dataSelectInit() { isKetteiTxtDisp = ketteiTxtDispAnm(); if (field_0x00e0[mSelectNum] != SelEndFrameTbl[mSelectNum]) { +#if !TARGET_PC field_0x00e0[mSelectNum] += 2; if (field_0x00e0[mSelectNum] > SelEndFrameTbl[mSelectNum]) @@ -760,6 +806,7 @@ void dFile_select_c::dataSelectInit() { mBaseMoveAnm->setFrame(field_0x00e0[mSelectNum]); mSelFilePanes[mSelectNum]->getPanePtr()->animationTransform(); +#endif check = false; } } @@ -1058,6 +1105,7 @@ bool dFile_select_c::selectDataMoveAnm() { bool ret; if (field_0x00e0[mSelectNum] != field_0x00ec) { +#if !TARGET_PC if (field_0x00e0[mSelectNum] < field_0x00ec) { field_0x00e0[mSelectNum] += 2; @@ -1077,6 +1125,7 @@ bool dFile_select_c::selectDataMoveAnm() { mSelFilePanes[i]->getPanePtr()->animationTransform(); } mBaseSubPane->animationTransform(); +#endif } if (field_0x00e0[mSelectNum] == field_0x00ec) { @@ -1133,6 +1182,7 @@ void dFile_select_c::dataSelectMoveAnime() { iVar6 = selectWakuAlpahAnm(mLastSelectNum); if (field_0x00e0[mLastSelectNum] != SelStartFrameTbl[mLastSelectNum]) { +#if !TARGET_PC field_0x00e0[mLastSelectNum] -= 2; if (field_0x00e0[mLastSelectNum] < SelStartFrameTbl[mLastSelectNum]) @@ -1140,6 +1190,7 @@ void dFile_select_c::dataSelectMoveAnime() { field_0x0088->setFrame(field_0x00e0[mLastSelectNum]); mSelFilePanes[mLastSelectNum]->getPanePtr()->animationTransform(); +#endif bVar1 = false; } } @@ -1151,6 +1202,7 @@ void dFile_select_c::dataSelectMoveAnime() { iVar5 = mSelFilePane_Book_l[mSelectNum]->alphaAnime(g_fsHIO.base_effect_appear_frames, 0, 0xff, 1); if (field_0x00e0[mSelectNum] != SelEndFrameTbl[mSelectNum]) { +#if !TARGET_PC field_0x00e0[mSelectNum] += 2; if (field_0x00e0[mSelectNum] > SelEndFrameTbl[mSelectNum]) @@ -1158,6 +1210,7 @@ void dFile_select_c::dataSelectMoveAnime() { mBaseMoveAnm->setFrame(field_0x00e0[mSelectNum]); mSelFilePanes[mSelectNum]->getPanePtr()->animationTransform(); +#endif bVar2 = false; } } @@ -1488,6 +1541,7 @@ bool dFile_select_c::menuMoveAnm() { bool ret; if (field_0x0358 != field_0x035c) { +#if !TARGET_PC if (field_0x0358 < field_0x035c) { field_0x0358 += 2; @@ -1502,6 +1556,7 @@ bool dFile_select_c::menuMoveAnm() { m3mBck->setFrame(field_0x0358); m3mMenuPane->animationTransform(); +#endif ret = false; } else { m3mMenuPane->setAnimation((J2DAnmTransform*)NULL); @@ -1561,6 +1616,7 @@ void dFile_select_c::menuSelectMoveAnm() { if (mSelectMenuNum != 0xFF && field_0x034c[mSelectMenuNum] != MenuSelStartFrameTbl[mSelectMenuNum]) { +#if !TARGET_PC if (field_0x034c[mSelectMenuNum] > MenuSelStartFrameTbl[mSelectMenuNum]) { field_0x034c[mSelectMenuNum] -= 2; @@ -1575,6 +1631,7 @@ void dFile_select_c::menuSelectMoveAnm() { m3mBck->setFrame(field_0x034c[mSelectMenuNum]); m3mSelPane[mSelectMenuNum]->getPanePtr()->animationTransform(); +#endif tmp1 = false; } @@ -1583,6 +1640,7 @@ void dFile_select_c::menuSelectMoveAnm() { if (mLastSelectMenuNum != 0xFF) { if (field_0x034c[mLastSelectMenuNum] != MenuSelEndFrameTbl[mLastSelectMenuNum]) { +#if !TARGET_PC if (field_0x034c[mLastSelectMenuNum] < MenuSelEndFrameTbl[mLastSelectMenuNum]) { field_0x034c[mLastSelectMenuNum] += 2; @@ -1597,6 +1655,7 @@ void dFile_select_c::menuSelectMoveAnm() { } m3mBck2->setFrame(field_0x034c[mLastSelectMenuNum]); m3mSelPane[mLastSelectMenuNum]->getPanePtr()->animationTransform(); +#endif tmp2 = false; } @@ -1718,14 +1777,14 @@ void dFile_select_c::nameToDataSelectMove() { } void dFile_select_c::nameInputFade() { - mFadeTimer--; + IF_NOT_DUSK(mFadeTimer--); - #if PLATFORM_GCN +#if !TARGET_PC && PLATFORM_GCN u8 alpha = (1.0f - (mFadeTimer / 15.0f)) * 255.0f; mpFadePict->setAlpha(alpha); - #endif +#endif - if (mFadeTimer == 0) { + if (mFadeTimer DUSK_IF_ELSE(<=, ==) 0) { char name[32]; headerTxtSet(900, 1, 1); dMeter2Info_getString(899, name, NULL); @@ -1746,14 +1805,14 @@ void dFile_select_c::nameInputFade() { void dFile_select_c::nameInput2Move() { if (!mDoRst::isReset()) { - mFadeTimer--; + IF_NOT_DUSK(mFadeTimer--); - #if PLATFORM_GCN +#if !TARGET_PC && PLATFORM_GCN u8 alpha = (mFadeTimer / 15.0f) * 255.0f; mpFadePict->setAlpha(alpha); - #endif +#endif - if (mFadeTimer == 0) { + if (mFadeTimer DUSK_IF_ELSE(<=, ==) 0) { mpName->showIcon(); mDataSelProc = DATASELPROC_NAME_INPUT2; } @@ -1805,14 +1864,14 @@ void dFile_select_c::nameInput2() { } void dFile_select_c::backNameInputMove0() { - mFadeTimer--; + IF_NOT_DUSK(mFadeTimer--); - #if PLATFORM_GCN +#if !TARGET_PC && PLATFORM_GCN u8 alpha = (1.0f - (mFadeTimer / 15.0f)) * 255.0f; mpFadePict->setAlpha(alpha); - #endif +#endif - if (mFadeTimer == 0) { + if (mFadeTimer DUSK_IF_ELSE(<=, ==) 0) { headerTxtSet(901, 1, 1); mpName->setNextNameStr(dComIfGs_getPlayerName()); mpName->initial(); @@ -1831,14 +1890,14 @@ void dFile_select_c::backNameInputMove0() { void dFile_select_c::backNameInputMove() { if (!mDoRst::isReset()) { - mFadeTimer--; + IF_NOT_DUSK(mFadeTimer--); - #if PLATFORM_GCN +#if !TARGET_PC && PLATFORM_GCN u8 alpha = (mFadeTimer / 15.0f) * 255.0f; mpFadePict->setAlpha(alpha); - #endif +#endif - if (mFadeTimer == 0) { + if (mFadeTimer DUSK_IF_ELSE(<=, ==) 0) { modoruTxtChange(1); mDataSelProc = DATASELPROC_NAME_INPUT_WAIT; } @@ -2084,12 +2143,14 @@ void dFile_select_c::copyDataToSelectMoveAnm() { iVar7 = mCpSelPane_book[field_0x026c]->alphaAnime(g_fsHIO.base_effect_appear_frames, 0xff, 0, 1); iVar6 = copySelectWakuAlpahAnm(field_0x026c); if (field_0x02b4[field_0x026c] != 109) { +#if !TARGET_PC field_0x02b4[field_0x026c] += 2; if (field_0x02b4[field_0x026c] > 109) { field_0x02b4[field_0x026c] = 109; } mCpSelBck2->setFrame(field_0x02b4[field_0x026c]); mCpSelPane[field_0x026c]->getPanePtr()->animationTransform(); +#endif bVar1 = false; } } @@ -2099,12 +2160,14 @@ void dFile_select_c::copyDataToSelectMoveAnm() { if (field_0x026b != 0xff) { iVar5 = mCpSelPane_book[field_0x026b]->alphaAnime(g_fsHIO.base_effect_appear_frames, 0, 0xff, 1); if (field_0x02b4[field_0x026b] != 99) { +#if !TARGET_PC field_0x02b4[field_0x026b] -= 2; if (field_0x02b4[field_0x026b] < 99) { field_0x02b4[field_0x026b] = 99; } mCpSelBck->setFrame(field_0x02b4[field_0x026b]); mCpSelPane[field_0x026b]->getPanePtr()->animationTransform(); +#endif bVar2 = false; } } @@ -2259,6 +2322,7 @@ void dFile_select_c::yesnoMenuMoveAnmInitSet(int param_1, int param_2) { bool dFile_select_c::yesnoMenuMoveAnm() { bool rv; if (field_0x0100 != field_0x0104) { +#if !TARGET_PC if (field_0x0100 < field_0x0104) { field_0x0100 += 2; if (field_0x0100 > field_0x0104) { @@ -2274,6 +2338,7 @@ bool dFile_select_c::yesnoMenuMoveAnm() { mYnSelBck3->setFrame(field_0x0100); mYnSelPane[0]->getPanePtr()->animationTransform(); mYnSelPane[1]->getPanePtr()->animationTransform(); +#endif rv = false; } else { mYnSelPane[0]->getPanePtr()->setAnimation((J2DAnmTransform*)NULL); @@ -2337,6 +2402,7 @@ bool dFile_select_c::yesnoSelectMoveAnm() { bool bVar1 = true; if (field_0x0269 != 0xff && field_0x00f8[field_0x0269] != YnSelStartFrameTbl[field_0x0269]) { +#if !TARGET_PC if (field_0x00f8[field_0x0269] < YnSelStartFrameTbl[field_0x0269]) { field_0x00f8[field_0x0269] += 2; if (field_0x00f8[field_0x0269] > YnSelStartFrameTbl[field_0x0269]) { @@ -2351,11 +2417,13 @@ bool dFile_select_c::yesnoSelectMoveAnm() { mYnSelBck->setFrame(field_0x00f8[field_0x0269]); mYnSelPane[field_0x0269]->getPanePtr()->animationTransform(); +#endif bVar1 = false; } bool bVar2 = true; if (field_0x0268 != 0xff && field_0x00f8[field_0x0268] != YnSelEndFrameTbl[field_0x0268]) { +#if !TARGET_PC if (field_0x00f8[field_0x0268] < YnSelEndFrameTbl[field_0x0268]) { field_0x00f8[field_0x0268] += 2; if (field_0x00f8[field_0x0268] > YnSelEndFrameTbl[field_0x0268]) { @@ -2370,6 +2438,7 @@ bool dFile_select_c::yesnoSelectMoveAnm() { mYnSelBck2->setFrame(field_0x00f8[field_0x0268]); mYnSelPane[field_0x0268]->getPanePtr()->animationTransform(); +#endif bVar2 = false; } @@ -3577,9 +3646,7 @@ void dFile_select_c::screenSet() { timg, NULL); mpFadePict->setBlackWhite(black, white); mpFadePict->setAlpha(0); -#ifdef TARGET_PC - mFadeDlst.mpPict = mpFadePict; -#endif + IF_DUSK(mFadeDlst.mpPict = mpFadePict); #endif } @@ -3782,6 +3849,10 @@ void dFile_select_c::screenSet3Menu() { m3mMenuPane = m3mSel.Scr3m->search(MULTI_CHAR('wmenu_n')); m3mMenuPane->setAnimation(m3mBck); +#if TARGET_PC + field_0x0358 = 799; + field_0x035c = 799; +#endif m3mBck->setFrame(799.0f); m3mMenuPane->animationTransform(); @@ -4396,14 +4467,157 @@ void dFile_select_c::fileSelectWide() { break; } } + +void dFile_select_c::presentLayoutAnims() { + dusk::vdt::present_toward(mBaseMoveAnmFrame, (f32)mBaseMoveAnmFrameMax, mBaseMoveAnm, + mBaseMovePane != NULL ? mBaseMovePane->getPanePtr() : NULL); + + if (mBaseSubPane != NULL && mBaseSubPane->mTransform == mBaseMoveAnm && mSelectNum != 0xFF) { + dusk::vdt::present_toward(field_0x00e0[mSelectNum], (f32)field_0x00ec, mBaseMoveAnm); + for (int i = 0; i < 3; i++) { + J2DPane* pane = mSelFilePanes[i]->getPanePtr(); + if (pane != NULL && pane->mTransform != NULL) { + pane->animationTransform(); + } + } + mBaseSubPane->animationTransform(); + } else { + for (int i = 0; i < 3; i++) { + J2DPane* pane = mSelFilePanes[i]->getPanePtr(); + dusk::vdt::present_selected(field_0x00e0[i], pane, mBaseMoveAnm, (f32)SelEndFrameTbl[i], + field_0x0088, (f32)SelStartFrameTbl[i]); + } + } + + dusk::vdt::present_toward(field_0x0358, (f32)field_0x035c, m3mBck, m3mMenuPane); + + for (int i = 0; i < 3; i++) { + J2DPane* pane = m3mSelPane[i]->getPanePtr(); + dusk::vdt::present_selected(field_0x034c[i], pane, m3mBck, (f32)MenuSelStartFrameTbl[i], + m3mBck2, (f32)MenuSelEndFrameTbl[i]); + } + + if (field_0x0100 != field_0x0104) { + dusk::vdt::present_shared(field_0x0100, (f32)field_0x0104, mYnSelBck3, + {mYnSelPane[0]->getPanePtr(), mYnSelPane[1]->getPanePtr()}); + } + + for (int i = 0; i < 2; i++) { + J2DPane* pane = mYnSelPane[i]->getPanePtr(); + dusk::vdt::present_selected(field_0x00f8[i], pane, mYnSelBck, (f32)YnSelStartFrameTbl[i], + mYnSelBck2, (f32)YnSelEndFrameTbl[i]); + } + + for (int i = 0; i < 2; i++) { + if (mCpSelPane[i] == NULL) { + continue; + } + J2DPane* pane = mCpSelPane[i]->getPanePtr(); + dusk::vdt::present_selected(field_0x02b4[i], pane, mCpSelBck, 99.0f, mCpSelBck2, 109.0f); + } + + dusk::vdt::present_toward(field_0x0130, (f32)field_0x0134, field_0x0090, mErrorMsgPane); + + if (mBaseSubPane != NULL && mBaseSubPane->mTransform == field_0x009c) { + dusk::vdt::present_toward(field_0x0110, (f32)field_0x0114, field_0x009c, mBaseSubPane); + } + + if (mNameMoveRequested) { + dusk::vdt::present_toward(field_0x0120, (f32)field_0x0124, field_0x0094, mNameBasePane); + } +} + +void dFile_select_c::presentNameInputFade() { + bool fadeOut; + switch (mDataSelProc) { + case DATASELPROC_NAME_INPUT_FADE: + case DATASELPROC_BACK_NAME_INPUT_MOVE0: + fadeOut = true; + break; + case DATASELPROC_NAME_INPUT2_MOVE: + case DATASELPROC_BACK_NAME_INPUT_MOVE: + if (mDoRst::isReset()) { + return; + } + fadeOut = false; + break; + default: + return; + } + + mFadeTimer -= dusk::game_clock::original_frames(); + if (mFadeTimer < 0.0f) { + mFadeTimer = 0.0f; + } + + f32 t = mFadeTimer / 15.0f; + u8 alpha = fadeOut ? (1.0f - t) * 255.0f : t * 255.0f; + mpFadePict->setAlpha(alpha); +} + +void dFile_select_c::presentAnims() { + if (mpFileWarning != NULL) { + mpFileWarning->presentAnims(); + } + + presentNameInputFade(); + selFileWakuAnm(); + bookIconAnm(); + dataDelEffAnm(); + dataCopyEffAnm(); + fileSel.Scr->animation(); + mYnSel.ScrYn->animation(); + m3mSel.Scr3m->animation(); + mSelDt.ScrDt->animation(); + if (mCpSel.isShow) { + selCopyFileWakuAnm(); + copyBookIconAnm(); + mCpSel.Scr->animation(); + } + presentLayoutAnims(); + for (int i = 0; i < 3; ++i) { + mSelFilePanes[i]->presentAnime(); + mDeleteEfPane[i]->presentAlphaAnime(); + mCopyEfPane[i]->presentAlphaAnime(); + mSelFileMoyoPane[i]->presentAnime(); + mSelFileGoldPane[i]->presentAnime(); + mSelFileGold2Pane[i]->presentAnime(); + mSelFilePane_Book_l[i]->presentAnime(); + mFileInfoDatBasePane[i]->presentAlphaAnime(); + mFileInfoNoDatBasePane[i]->presentAlphaAnime(); + m3mSelPane_mo[i]->presentAnime(); + m3mSelPane_g[i]->presentAnime(); + m3mSelPane_gr[i]->presentAnime(); + m3mSelTextPane[i]->presentAnime(); + } + for (int i = 0; i < 2; ++i) { + mErrorMsgTxtPane[i]->presentAlphaAnime(); + mYnSelPane_m[i]->presentAlphaAnime(); + mYnSelPane_g[i]->presentAlphaAnime(); + mYnSelPane_gr[i]->presentAlphaAnime(); + mYnSelTxtPane[i]->presentAnime(); + mHeaderTxtPane[i]->presentAlphaAnime(); + mCpSelPane_moyo[i]->presentAnime(); + mCpSelPane_gold[i]->presentAnime(); + mCpSelPane_gold2[i]->presentAnime(); + mCpSelPane_book[i]->presentAnime(); + } + mBbtnPane->presentAlphaAnime(); + mAbtnPane->presentAlphaAnime(); + mModoruTxtPane->presentAlphaAnime(); + mKetteiTxtPane->presentAlphaAnime(); + fileSelectWide(); +} #endif void dFile_select_c::_draw() { - #if TARGET_PC - fileSelectWide(); - #endif - if (!mHasDrawn) { +#if TARGET_PC + if (dusk::game_clock::is_presentation_frame()) { + presentAnims(); + } +#endif + dComIfGd_set2DOpa(&fileSel); for (int i = 0; i < 3; i++) { @@ -4505,6 +4719,7 @@ void dFile_select_c::errorMoveAnmInitSet(int param_1, int param_2) { bool dFile_select_c::errorMoveAnm() { bool ret; if (field_0x0130 != field_0x0134) { +#if !TARGET_PC if (field_0x0130 < field_0x0134) { field_0x0130 += 2; @@ -4519,6 +4734,7 @@ bool dFile_select_c::errorMoveAnm() { field_0x0090->setFrame(field_0x0130); mErrorMsgPane->animationTransform(); +#endif ret = false; } else { mErrorMsgPane->setAnimation((J2DAnmTransform*)NULL); @@ -5797,6 +6013,7 @@ bool dFile_select_c::fileInfoScaleAnm() { bool ret; if (field_0x0110 != field_0x0114) { +#if !TARGET_PC if (field_0x0110 < field_0x0114) { field_0x0110 += 2; @@ -5811,6 +6028,7 @@ bool dFile_select_c::fileInfoScaleAnm() { field_0x009c->setFrame(field_0x0110); mBaseSubPane->animationTransform(); +#endif ret = false; } @@ -5823,6 +6041,13 @@ bool dFile_select_c::fileInfoScaleAnm() { } void dFile_select_c::nameMoveAnmInitSet(int param_1, int param_2) { +#if TARGET_PC + f32 startFrame = param_1; + if (mNameBasePane->mTransform == field_0x0094 && field_0x0124 == param_1) { + startFrame = field_0x0120; + } + mNameMoveRequested = false; +#endif if (param_1 == 3359) { field_0x0128 = true; } @@ -5830,15 +6055,17 @@ void dFile_select_c::nameMoveAnmInitSet(int param_1, int param_2) { mpName->hideIcon(); } mNameBasePane->setAnimation(field_0x0094); - field_0x0120 = param_1; + field_0x0120 = DUSK_IF_ELSE(startFrame, param_1); field_0x0124 = param_2; field_0x0094->setFrame(field_0x0120); mNameBasePane->animationTransform(); } bool dFile_select_c::nameMoveAnm() { + IF_DUSK(mNameMoveRequested = true); bool ret; if (field_0x0120 != field_0x0124) { +#if !TARGET_PC if (field_0x0120 < field_0x0124) { field_0x0120 += 2; @@ -5854,6 +6081,7 @@ bool dFile_select_c::nameMoveAnm() { } field_0x0094->setFrame(field_0x0120); mNameBasePane->animationTransform(); +#endif ret = false; } else { mNameBasePane->setAnimation((J2DAnmTransform*)0); @@ -5987,6 +6215,7 @@ void dFile_select3D_c::_move() { } void dFile_select3D_c::draw() { + IF_DUSK_BLOCK(dusk::game_clock::is_sim_frame()) if (mpModel) { dComIfGd_setListItem3D(); g_env_light.settingTevStruct(13, &field_0x03a4, &mTevstr); @@ -5995,6 +6224,7 @@ void dFile_select3D_c::draw() { mDoExt_modelUpdateDL(mpModel); dComIfGd_setList(); } + IF_DUSK_BLOCK_END } void dFile_select3D_c::setJ3D(char const* param_0, char const* param_1, char const* param_2) { diff --git a/src/d/d_gameover.cpp b/src/d/d_gameover.cpp index ac85b5d2b4..2dfd843f6f 100644 --- a/src/d/d_gameover.cpp +++ b/src/d/d_gameover.cpp @@ -15,6 +15,7 @@ #include #if TARGET_PC +#include "dusk/game_clock.h" #include "helpers/gx_helper.h" #endif @@ -192,6 +193,9 @@ int dGameover_c::_create() { } (this->*init_process[mProc])(); + + IF_DUSK(base.draw_interp_frame = true); + return cPhs_COMPLEATE_e; return phase; @@ -335,9 +339,11 @@ void dGameover_c::deleteWait_init() {} void dGameover_c::deleteWait_proc() {} int dGameover_c::_draw() { + IF_DUSK_BLOCK(dusk::game_clock::is_sim_frame() && (mIsDemoSave && mProc >= PROC_DISP_WAIT)) if (dgo_capture_c != NULL && dComIfGp_isPauseFlag()) { dComIfGd_set2DOpa(dgo_capture_c); } + IF_DUSK_BLOCK_END if (mIsDemoSave && mProc >= PROC_DISP_WAIT) { if (dgo_screen_c != NULL) { diff --git a/src/d/d_kankyo.cpp b/src/d/d_kankyo.cpp index 1201da30e9..3b7f4d9ad4 100644 --- a/src/d/d_kankyo.cpp +++ b/src/d/d_kankyo.cpp @@ -32,11 +32,11 @@ #include "JSystem/JKernel/JKRSolidHeap.h" #include #include + #if TARGET_PC -#include "dusk/imgui/ImGuiBloomWindow.hpp" -#include "dusk/settings.h" -#include "dusk/frame_interpolation.h" #include "dusk/game_clock.h" +#include "dusk/interp/material.h" +#include "dusk/imgui/ImGuiBloomWindow.hpp" static f32 timeScale = 1.0f; #endif @@ -1800,9 +1800,7 @@ void dScnKy_env_light_c::setLight_palno_get(u8* prev_envr_id_p, u8* next_envr_id u8 psel_idx = 0; int i; int sp14 = 0; -#if TARGET_PC - const f32 timeScale = (pattern_ratio_p == &g_env_light.pat_ratio) ? ::timeScale : 1.0f; -#endif + IF_DUSK(const f32 timeScale = pattern_ratio_p == &g_env_light.pat_ratio ? ::timeScale : 1.0f); if (*init_timer_p != 0) { (*init_timer_p)++; @@ -2344,10 +2342,7 @@ void dScnKy_env_light_c::setLight() { u8 next_pal_start_id; u8 prev_pal_end_id; u8 next_pal_end_id; -#if TARGET_PC - const f32 deltaTime = dusk::game_clock::consume_interval(this); - timeScale = deltaTime / dusk::game_clock::kSimPeriod; -#endif + IF_DUSK(timeScale = dusk::game_clock::original_frames()); setLight_palno_get(&g_env_light.PrevCol, &g_env_light.UseCol, &g_env_light.wether_pat0, &g_env_light.wether_pat1, &prev_pal_start_id, &prev_pal_end_id, &next_pal_start_id, &next_pal_end_id, &color_ratio, &start_pat_pal_id, @@ -4485,6 +4480,7 @@ static void setLightTevColorType_MAJI_sub(J3DMaterial* material_p, dKy_tevstr_c* } } } + IF_DUSK(dusk::interp::material::record_light_view(material_p)); } } diff --git a/src/d/d_kankyo_rain.cpp b/src/d/d_kankyo_rain.cpp index 02dcd94d68..d682736690 100644 --- a/src/d/d_kankyo_rain.cpp +++ b/src/d/d_kankyo_rain.cpp @@ -13,9 +13,34 @@ #include "m_Do/m_Do_lib.h" #include -#include "dusk/version.hpp" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/samples.h" +#include "dusk/interp/user_interface.h" +#include "dusk/version.hpp" + +namespace { +dusk::interp::Samples s_rain; +dusk::interp::Samples s_snow_pos; +dusk::interp::Samples s_snow_base; +dusk::interp::Samples s_housi_world; +dusk::interp::Samples s_housi_scale; +dusk::interp::Samples s_cloud; +dusk::interp::Samples s_vrkumo; +} // namespace + +namespace dusk::interp { +void clear_weather_samples() { + s_rain = {}; + s_snow_pos = {}; + s_snow_base = {}; + s_housi_world = {}; + s_housi_scale = {}; + s_cloud = {}; + s_vrkumo = {}; +} +} // namespace dusk::interp #endif static void vectle_calc(DOUBLE_POS* i_pos, cXyz* o_out) { @@ -129,24 +154,73 @@ static GXTexObj* load_cached_tex(CachedTexObjs& cache, ResTIMG* img, GXTexMap } #endif +#if TARGET_PC +static void dKyr_place_sun(camera_class* camera, cXyz* o_sunpos) { + cXyz lightDir; + u32 stage_type = dStage_stagInfo_GetSTType(dComIfGp_getStage()->getStagInfo()); + if (g_env_light.base_light.mColor.r == 0 && stage_type != ST_ROOM) { + dKyr_get_vectle_calc(&camera->view.lookat.eye, &g_env_light.base_light.mPosition, + &lightDir); + } else { + dKyr_get_vectle_calc(&camera->view.lookat.eye, &g_env_light.sun_light_pos, &lightDir); + } + o_sunpos->x = camera->view.lookat.eye.x + 8000.0f * lightDir.x; + o_sunpos->y = camera->view.lookat.eye.y + 8000.0f * lightDir.y; + o_sunpos->z = camera->view.lookat.eye.z + 8000.0f * lightDir.z; +} + +static void dKyr_place_lenzflare(camera_class* camera, cXyz* sunpos, cXyz* o_positions) { + cXyz eyeVect; + cXyz sunDirSmth; + cXyz camFwd; + + dKy_set_eyevect_calc(camera, &eyeVect, 4000.0f, 4000.0f); + dKyr_get_vectle_calc(&eyeVect, sunpos, &sunDirSmth); + o_positions[0] = *sunpos; + o_positions[1] = *sunpos; + + dKyr_get_vectle_calc(&camera->view.lookat.eye, &camera->view.lookat.center, &camFwd); + + for (int i = 2; i < 8; i++) { + if (i == 2) { + f32 size = 250.0f + 600.0f * sunDirSmth.abs(camFwd); + o_positions[i].x = sunpos->x - sunDirSmth.x * size * i; + o_positions[i].y = sunpos->y - sunDirSmth.y * size * i; + o_positions[i].z = sunpos->z - sunDirSmth.z * size * i; + } else { + f32 size = 250.0f + 110.0f * sunDirSmth.abs(camFwd); + o_positions[i].x = sunpos->x - (4100.0f * sunDirSmth.x + sunDirSmth.x * size * i); + o_positions[i].y = sunpos->y - (4100.0f * sunDirSmth.y + sunDirSmth.y * size * i); + o_positions[i].z = sunpos->z - (4100.0f * sunDirSmth.z + sunDirSmth.z * size * i); + } + } +} +#endif + void dKyr_lenzflare_move() { dKankyo_sun_Packet* sun_packet = g_env_light.mpSunPacket; dKankyo_sunlenz_Packet* lenz_packet = g_env_light.mpSunLenzPacket; camera_process_class* camera = dComIfGp_getCamera(0); +#if !TARGET_PC cXyz eyeVect; cXyz field_0x3c; cXyz sunDirSmth; cXyz camFwd; +#endif if (sun_packet->mVisibility < 0.0001f) { return; } +#if TARGET_PC + dKyr_place_lenzflare(camera, sun_packet->mPos, lenz_packet->mPositions); +#else dKy_set_eyevect_calc(camera, &eyeVect, 4000.0f, 4000.0f); dKyr_get_vectle_calc(&eyeVect, sun_packet->mPos, &sunDirSmth); lenz_packet->mPositions[0] = sun_packet->mPos[0]; lenz_packet->mPositions[1] = sun_packet->mPos[0]; +#endif cXyz vect; cXyz proj; @@ -162,6 +236,7 @@ void dKyr_lenzflare_move() { lenz_packet->field_0x94 *= S2DEG_CONSTANT; // convert from short angle to degrees lenz_packet->field_0x94 += 180.0f; +#if !TARGET_PC dKyr_get_vectle_calc(&camera->view.lookat.eye, &camera->view.lookat.center, &camFwd); for (int i = 2; i < 8; i++) { @@ -177,6 +252,7 @@ void dKyr_lenzflare_move() { lenz_packet->mPositions[i].z = sun_packet->mPos[0].z - (4100.0f * sunDirSmth.z + sunDirSmth.z * size * i); } } +#endif } static BOOL dKyr_moon_arrival_check() { @@ -205,6 +281,10 @@ void dKyr_sun_move() { u32 stage_type = dStage_stagInfo_GetSTType(dComIfGp_getStage()->getStagInfo()); +#if TARGET_PC + dKyr_place_sun(camera_p2, &sun_packet->mPos[0]); + dKyr_get_vectle_calc(&camera_p2->view.lookat.eye, &sun_packet->mPos[0], &lightDir); +#else if (g_env_light.base_light.mColor.r == 0 && stage_type != ST_ROOM) { dKyr_get_vectle_calc(&camera_p2->view.lookat.eye, &g_env_light.base_light.mPosition, &lightDir); @@ -215,6 +295,7 @@ void dKyr_sun_move() { sun_packet->mPos[0].x = camera_p2->view.lookat.eye.x + 8000.0f * lightDir.x; sun_packet->mPos[0].y = camera_p2->view.lookat.eye.y + 8000.0f * lightDir.y; sun_packet->mPos[0].z = camera_p2->view.lookat.eye.z + 8000.0f * lightDir.z; +#endif f32 horizon_y = (sun_packet->mPos[0].y - camera_p2->view.lookat.eye.y) / 8000.0f; if (horizon_y < 0.0f) { @@ -477,6 +558,7 @@ void dKyr_rain_init() { g_env_light.mpRainPacket->mRainEff[i].mStatus = 0; } g_env_light.mpRainPacket->raincnt = 0; + IF_DUSK(s_rain.reset()); } static void rain_bg_chk(dKankyo_rain_Packet* i_packet, int i_idx) { @@ -901,6 +983,12 @@ void dKyr_rain_move() { rain_packet->mRainEff[i].mAlpha = var_f31 * (1.0f + cM_rndFX(0.5f)); } } + +#if TARGET_PC + s_rain.capture(rain_packet->raincnt, [&](int i) { + return rain_packet->mRainEff[i].mBasePos + rain_packet->mRainEff[i].mPosition; + }); +#endif } static BOOL d_krain_cut_turn_check() { @@ -974,6 +1062,10 @@ void dKyr_housi_move() { } if (housi_packet->mHousiCount == 0) { +#if TARGET_PC + s_housi_world.reset(); + s_housi_scale.reset(); +#endif return; } @@ -1263,6 +1355,15 @@ void dKyr_housi_move() { f32 temp_f25 = var_f1_8 / 2000.0f; effect->field_0x48 = 1.0f - (temp_f25 * temp_f25); } + +#if TARGET_PC + s_housi_world.capture(housi_packet->mHousiCount, [&](int i) { + return housi_packet->mHousiEff[i].mBasePos + housi_packet->mHousiEff[i].mPosition; + }); + s_housi_scale.capture(housi_packet->mHousiCount, [&](int i) { + return housi_packet->mHousiEff[i].mScale; + }); +#endif } void dKyr_snow_init() { @@ -1279,6 +1380,10 @@ void dKyr_snow_init() { g_env_light.mpSnowPacket->mSnowEff[i].mStatus = 0; } +#if TARGET_PC + s_snow_pos.reset(); + s_snow_base.reset(); +#endif g_env_light.mpSnowPacket->field_0x6d88 = 0; g_env_light.mpSnowPacket->field_0x6d74 = camera->view.lookat.eye; g_env_light.mpSnowPacket->field_0x6d80 = 0.0f; @@ -1313,6 +1418,10 @@ void dKyr_snow_move() { } if (snow_packet->field_0x6d88 == 0) { +#if TARGET_PC + s_snow_pos.reset(); + s_snow_base.reset(); +#endif return; } @@ -1557,6 +1666,15 @@ void dKyr_snow_move() { } } } + +#if TARGET_PC + s_snow_pos.capture(snow_packet->field_0x6d88, [&](int i) { + return snow_packet->mSnowEff[i].mPosition; + }); + s_snow_base.capture(snow_packet->field_0x6d88, [&](int i) { + return snow_packet->mSnowEff[i].mBasePos; + }); +#endif } void dKyr_star_init() { @@ -1840,6 +1958,12 @@ void cloud_shadow_move() { cLib_addCalc(&packet->mCloudEff[i].mAlpha, alpha_max * alpha_target, 0.1f, 0.1f, 0.001f); } } + +#if TARGET_PC + s_cloud.capture(packet->mCount, [&](int i) { + return packet->mCloudEff[i].mBasePos + packet->mCloudEff[i].mPosition; + }); +#endif } void vrkumo_move() { @@ -2058,6 +2182,12 @@ void vrkumo_move() { max_alpha *= spC; cLib_addCalc(&vrkumo_packet->mVrkumoEff[i].mAlpha, max_alpha, 0.5f, alpha_step, 0.001f); } + +#if TARGET_PC + s_vrkumo.capture(100, [&](int i) { + return vrkumo_packet->mVrkumoEff[i].mPosition; + }); +#endif } static void dKr_cullVtx_Set(IF_DUSK(bool const vtxColor = false)) { @@ -2414,15 +2544,17 @@ void dKyr_drawSun(Mtx drawMtx, cXyz* ppos, GXColor& unused, u8** tex) { sunpos.y = ppos->y; sunpos.z = ppos->z; + IF_DUSK(dKyr_place_sun(camera, &sunpos)); + u32 stage_type = dStage_stagInfo_GetSTType(dComIfGp_getStage()->getStagInfo()); if (g_env_light.base_light.mColor.r == 0 && stage_type != ST_ROOM) { if (g_env_light.daytime > 285.0f || g_env_light.daytime < 105.0f) { draw_moon = false; } - spB4.x = ppos->x; - spB4.y = ppos->y; - spB4.z = ppos->z; + spB4.x = DUSK_IF_ELSE(sunpos.x, ppos->x); + spB4.y = DUSK_IF_ELSE(sunpos.y, ppos->y); + spB4.z = DUSK_IF_ELSE(sunpos.z, ppos->z); } else { if (strcmp(dComIfGp_getStartStageName(), "F_SP200") == 0 && dComIfG_play_c::getLayerNo(0) == 0) { spB4 = envlight->moon_pos; @@ -2588,7 +2720,8 @@ void dKyr_drawSun(Mtx drawMtx, cXyz* ppos, GXColor& unused, u8** tex) { }; if (strcmp(dComIfGp_getStartStageName(), "F_SP200") != 0) { - dKyr_get_vectle_calc(&camera->view.lookat.eye, &camera->view.lookat.center, &camfwd); + dKyr_get_vectle_calc(&camera->view.lookat.eye, + &camera->view.lookat.center, &camfwd); f32 cam_distXZ = JMAFastSqrt((camfwd.x * camfwd.x) + (camfwd.z * camfwd.z)); f32 cam_theta = atan2f(camfwd.x, camfwd.z); f32 cam_phi = atan2f(camfwd.y, cam_distXZ); @@ -2755,8 +2888,8 @@ void dKyr_drawLenzflare(Mtx drawMtx, cXyz* ppos, GXColor& param_2, u8** tex) { dKankyo_sun_Packet* sun_packet = g_env_light.mpSunPacket; camera_class* camera = (camera_class*)dComIfGp_getCamera(0); - static s16 S_rot_work1 = 0; - static s16 S_rot_work2 = 0; + static DUSK_IF_ELSE(f32, s16) S_rot_work1 = 0; + static DUSK_IF_ELSE(f32, s16) S_rot_work2 = 0; #if TARGET_PC || VERSION == VERSION_GCN_JPN IF_DUSK_BLOCK(dusk::version::isRegionJpn()) @@ -2783,6 +2916,13 @@ void dKyr_drawLenzflare(Mtx drawMtx, cXyz* ppos, GXColor& param_2, u8** tex) { f32 spA8 = sun_packet->mVisibility * sun_packet->mVisibility; if (!(sun_visibility < 0.1f)) { +#if TARGET_PC + cXyz sunpos; + cXyz positions[8]; + dKyr_place_sun(camera, &sunpos); + dKyr_place_lenzflare(camera, &sunpos, positions); + ppos = positions; +#endif dKy_set_eyevect_calc2(camera, &spFC, 8000.0f, 8000.0f); GXColor color_reg0; @@ -2835,10 +2975,10 @@ void dKyr_drawLenzflare(Mtx drawMtx, cXyz* ppos, GXColor& param_2, u8** tex) { GXSetCurrentMtx(GX_PNMTX0); if (sun_packet->field_0x6c > 0.0f) { - spC = S_rot_work1 - 0x7F6; - spA = S_rot_work2 + 0x416B; - S_rot_work1 += 8; - S_rot_work2 -= 14; + spC = (s16)S_rot_work1 - 0x7F6; + spA = (s16)S_rot_work2 + 0x416B; + S_rot_work1 += 8 IF_DUSK(* dusk::game_clock::original_frames()); + S_rot_work2 -= 14 IF_DUSK(* dusk::game_clock::original_frames()); if (dComIfGd_getView() != NULL) { MTXInverse(dComIfGd_getView()->viewMtxNoTrans, camMtx); @@ -2962,25 +3102,25 @@ void dKyr_drawLenzflare(Mtx drawMtx, cXyz* ppos, GXColor& param_2, u8** tex) { spE4.y = sp9C; spE4.z = 0.0f; cMtx_multVec(camMtx, &spE4, &spD8); - pos[0].x = sun_packet->mPos[0].x + spD8.x; - pos[0].y = sun_packet->mPos[0].y + spD8.y; - pos[0].z = sun_packet->mPos[0].z + spD8.z; + pos[0].x = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).x + spD8.x; + pos[0].y = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).y + spD8.y; + pos[0].z = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).z + spD8.z; spE4.x = sp98; spE4.y = sp94; spE4.z = 0.0f; cMtx_multVec(camMtx, &spE4, &spD8); - pos[1].x = sun_packet->mPos[0].x + spD8.x; - pos[1].y = sun_packet->mPos[0].y + spD8.y; - pos[1].z = sun_packet->mPos[0].z + spD8.z; + pos[1].x = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).x + spD8.x; + pos[1].y = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).y + spD8.y; + pos[1].z = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).z + spD8.z; spE4.x = sp90; spE4.y = sp8C; spE4.z = 0.0f; cMtx_multVec(camMtx, &spE4, &spD8); - pos[2].x = sun_packet->mPos[0].x + spD8.x; - pos[2].y = sun_packet->mPos[0].y + spD8.y; - pos[2].z = sun_packet->mPos[0].z + spD8.z; + pos[2].x = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).x + spD8.x; + pos[2].y = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).y + spD8.y; + pos[2].z = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).z + spD8.z; GXBegin(GX_TRIANGLES, GX_VTXFMT0, 3); GXPosition3f32(pos[0].x, pos[0].y, pos[0].z); @@ -3267,6 +3407,7 @@ void dKyr_drawRain(Mtx drawMtx, u8** tex) { sp3C.x = rain_packet->mRainEff[i].mBasePos.x + rain_packet->mRainEff[i].mPosition.x; sp3C.y = rain_packet->mRainEff[i].mBasePos.y + rain_packet->mRainEff[i].mPosition.y; sp3C.z = rain_packet->mRainEff[i].mBasePos.z + rain_packet->mRainEff[i].mPosition.z; + IF_DUSK(sp3C = s_rain.read(i, sp3C, 300.0f);) f32 dist = 0.1f + (sp3C.abs(camera->view.lookat.eye) / 1500.0f); if (dist > 1.0f) { @@ -3369,7 +3510,7 @@ void dKyr_drawSibuki(Mtx drawMtx, u8** tex) { alpha = 200.0f; } - cLib_addCalc(&rain_packet->mSibukiAlpha, alpha, 0.2f, 30.0f, 0.001f); + DUSK_IF_ELSE(dusk::vdt::present_addCalc, cLib_addCalc)(&rain_packet->mSibukiAlpha, alpha, 0.2f, 30.0f, 0.001f); dKy_set_eyevect_calc(camera, &eyevect, 7000.0f, 4000.0f); cXyz camdir; dKyr_get_vectle_calc(&camera->view.lookat.eye, &camera->view.lookat.center, &camdir); @@ -3621,7 +3762,7 @@ void dKyr_drawHousi(Mtx drawMtx, u8** tex) { GXSetNumIndStages(0); dKr_cullVtx_Set(IF_DUSK(true)); - rot += 1.2f; + rot += 1.2f IF_DUSK(* dusk::game_clock::original_frames()); MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); @@ -3634,6 +3775,13 @@ void dKyr_drawHousi(Mtx drawMtx, u8** tex) { GXBegin(GX_QUADS, GX_VTXFMT0, vertCount); #endif +#if TARGET_PC + f32 housi_counter = (f32)g_Counter.mCounter0; + if (dusk::interp::is_enabled()) { + housi_counter += dusk::interp::get_interpolation_step() - 1.0f; + } +#endif + for (int j = 0; j < housi_packet->mHousiCount; j++) { fopAc_ac_c* player = dComIfGp_getPlayer(0); @@ -3643,6 +3791,7 @@ void dKyr_drawHousi(Mtx drawMtx, u8** tex) { housi_packet->mHousiEff[j].mBasePos.y + housi_packet->mHousiEff[j].mPosition.y; spD0.z = housi_packet->mHousiEff[j].mBasePos.z + housi_packet->mHousiEff[j].mPosition.z; + IF_DUSK(spD0 = s_housi_world.read(j, spD0, 300.0f);) if (i == 1 && j == 0) { #if TARGET_PC @@ -3687,10 +3836,9 @@ void dKyr_drawHousi(Mtx drawMtx, u8** tex) { var_f27 = housi_packet->mHousiEff[j].field_0x48 * 18.0f; } - f32 temp_f28 = - (var_f27 * 0.2f) * cM_fsin(housi_packet->mHousiEff[j].mScale.x * 5.0f); - f32 temp_f30 = - (var_f27 * 0.2f) * cM_fcos(housi_packet->mHousiEff[j].mScale.y * 6.0f); + IF_DUSK(cXyz scale = s_housi_scale.read(j, housi_packet->mHousiEff[j].mScale, 1.0f)); + f32 temp_f28 = (var_f27 * 0.2f) * cM_fsin(DUSK_IF_ELSE(scale.x, housi_packet->mHousiEff[j].mScale.x) * 5.0f); + f32 temp_f30 = (var_f27 * 0.2f) * cM_fcos(DUSK_IF_ELSE(scale.y, housi_packet->mHousiEff[j].mScale.y) * 6.0f); if (dKy_darkworld_check() == 1 || isPalaceOfTwilight == 1) { cXyz sp7C[] = { @@ -3704,7 +3852,7 @@ void dKyr_drawHousi(Mtx drawMtx, u8** tex) { cXyz spAC; cXyz spA0; - f32 temp_f26_2 = cM_ssin((f32)j * 123.0f + (f32)(g_Counter.mCounter0 * 600)); + f32 temp_f26_2 = cM_ssin((f32)j * 123.0f + DUSK_IF_ELSE(housi_counter * 600.0f, (f32)(g_Counter.mCounter0 * 600))); cXyz* temp_r3 = &sp7C[k]; spAC.x = temp_r3->x * (8.0f * (1.0f + (temp_f26_2 * 0.3f))); @@ -3736,11 +3884,11 @@ void dKyr_drawHousi(Mtx drawMtx, u8** tex) { 0.2f + (housi_packet->mHousiEff[j].field_0x34 * (fabsf(cM_ssin((f32)j * 213.0f + - (f32)(g_Counter.mCounter0 * 330))) * + DUSK_IF_ELSE(housi_counter * 330.0f, (f32)(g_Counter.mCounter0 * 330)))) * 0.8f)); } else { var_f24 = cM_ssin((f32)j * 123.0f + - (f32)(g_Counter.mCounter0 * 80)); + DUSK_IF_ELSE(housi_counter * 80.0f, (f32)(g_Counter.mCounter0 * 80))); } f32 var_f2; @@ -3751,10 +3899,10 @@ void dKyr_drawHousi(Mtx drawMtx, u8** tex) { var_f24 = housi_packet->mHousiEff[j].field_0x34 * fabsf(cM_ssin((f32)j * 250.0f + - (f32)(g_Counter.mCounter0 * 88))); + DUSK_IF_ELSE(housi_counter * 88.0f, (f32)(g_Counter.mCounter0 * 88)))); } else { var_f24 = cM_ssin((f32)j * 685.0f + - (f32)(g_Counter.mCounter0 * 20)); + DUSK_IF_ELSE(housi_counter * 20.0f, (f32)(g_Counter.mCounter0 * 20))); } } else { var_f2 = 6.0f; @@ -3770,7 +3918,7 @@ void dKyr_drawHousi(Mtx drawMtx, u8** tex) { housi_packet->mHousiEff[j].mStatus == 3) { housi_packet->mHousiEff[j].field_0x38 += - 483.0f * (0.5f + (var_f24 * 0.5f)); + 483.0f * (0.5f + (var_f24 * 0.5f)) IF_DUSK(* dusk::game_clock::original_frames()); housi_packet->mHousiEff[j].field_0x44 = (s16)housi_packet->mHousiEff[j].field_0x38; @@ -3780,19 +3928,21 @@ void dKyr_drawHousi(Mtx drawMtx, u8** tex) { } else { if (housi_packet->mHousiEff[j].mStatus == 2) { if (g_env_light.fishing_hole_season == 3) { - housi_packet->mHousiEff[j].field_0x38 += var_f24 * 30.0f; + housi_packet->mHousiEff[j].field_0x38 += var_f24 * 30.0f IF_DUSK(* dusk::game_clock::original_frames()); } else { housi_packet->mHousiEff[j].field_0x38 += - var_f24 * 100.0f; + var_f24 * 100.0f IF_DUSK(* dusk::game_clock::original_frames()); } } if (housi_packet->mHousiEff[j].field_0x38 > 32765.0f) { - cLib_addCalc(&housi_packet->mHousiEff[j].field_0x44, - -16384.0f, 0.1f, 500.0f, 0.0001f); + DUSK_IF_ELSE(dusk::vdt::present_addCalc, cLib_addCalc)( + &housi_packet->mHousiEff[j].field_0x44, + -16384.0f, 0.1f, 500.0f, 0.0001f); } else { - cLib_addCalc(&housi_packet->mHousiEff[j].field_0x44, - 16384.0f, 0.1f, 500.0f, 0.0001f); + DUSK_IF_ELSE(dusk::vdt::present_addCalc, cLib_addCalc)( + &housi_packet->mHousiEff[j].field_0x44, + 16384.0f, 0.1f, 500.0f, 0.0001f); } mDoMtx_stack_c::YrotM(housi_packet->mHousiEff[j].field_0x38); @@ -3973,7 +4123,7 @@ void dKyr_drawSnow(Mtx drawMtx, u8** tex) { Mtx rotMtx; MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); - rot += 5.0f + cM_rndFX(2.0f); + rot += 5.0f DUSK_IF_ELSE(* dusk::game_clock::original_frames(), + cM_rndFX(2.0f)); if (rot > 719.0f) { rot = 0.0f; } @@ -3983,7 +4133,8 @@ void dKyr_drawSnow(Mtx drawMtx, u8** tex) { GXSetCurrentMtx(GX_PNMTX0); if (g_env_light.field_0xe92 == 0 && sp4C == 0) { - cLib_addCalc(&S_fubuki_ratio, 0.0f, 0.25f, 0.1f, 0.001f); + DUSK_IF_ELSE(dusk::vdt::present_addCalc, cLib_addCalc)( + &S_fubuki_ratio, 0.0f, 0.25f, 0.1f, 0.001f); if (S_fubuki_ratio > 0.0f) { spC = 2; } @@ -3991,7 +4142,8 @@ void dKyr_drawSnow(Mtx drawMtx, u8** tex) { if (g_env_light.field_0xe92) { spC = 2; } - cLib_addCalc(&S_fubuki_ratio, 1.0f, 0.25f, 0.1f, 0.001f); + DUSK_IF_ELSE(dusk::vdt::present_addCalc, cLib_addCalc)( + &S_fubuki_ratio, 1.0f, 0.25f, 0.1f, 0.001f); } f32 sp50; @@ -4020,12 +4172,14 @@ void dKyr_drawSnow(Mtx drawMtx, u8** tex) { f32 sp40, sp3C; if (j == 0) { sp7C = snow_packet->mSnowEff[i].mPosition; + IF_DUSK(sp7C = s_snow_pos.read(i, sp7C, 200.0f);) sp40 = snow_packet->mSnowEff[i].mScale * snow_packet->mSnowEff[i].mScale; sp3C = (snow_packet->mSnowEff[i].mScale - sp40) + snow_packet->mSnowEff[i].mScale; color_reg0.a = 180.0f * ((snow_packet->mSnowEff[i].mScale * 0.8f) + temp_f29); } else { sp7C = snow_packet->mSnowEff[i].mBasePos; + IF_DUSK(sp7C = s_snow_base.read(i, sp7C, 300.0f);) sp40 = snow_packet->mSnowEff[i].field_0x30 * snow_packet->mSnowEff[i].field_0x30; sp3C = (snow_packet->mSnowEff[i].field_0x30 - sp40) + snow_packet->mSnowEff[i].field_0x30; @@ -4307,7 +4461,7 @@ void dKyr_drawStar(Mtx drawMtx, u8** tex) { GXLoadPosMtxImm(drawMtx, GX_PNMTX0); GXSetCurrentMtx(GX_PNMTX0); - rot += 0.65f; + rot += 0.65f IF_DUSK(* dusk::game_clock::original_frames()); if (rot > 719.0f) { rot = 0.0f; } @@ -4599,7 +4753,7 @@ void drawCloudShadow(Mtx drawMtx, u8** tex) { MTXConcat(camMtx, rotMtx, camMtx); GXLoadPosMtxImm(drawMtx, GX_PNMTX0); - rot -= 0.45f; + rot -= 0.45f IF_DUSK(* dusk::game_clock::original_frames()); if (rot < 0.0f) { rot = 719.0f; } @@ -4633,7 +4787,7 @@ void drawCloudShadow(Mtx drawMtx, u8** tex) { #endif cMtx_concat(sp120, j3dSys.getViewMtx(), spF0); - rot += 2.0f; + rot += 2.0f IF_DUSK(* dusk::game_clock::original_frames()); MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); @@ -4688,6 +4842,7 @@ void drawCloudShadow(Mtx drawMtx, u8** tex) { sp5C.x = cloud_packet->mCloudEff[i].mBasePos.x + cloud_packet->mCloudEff[i].mPosition.x; sp5C.y = cloud_packet->mCloudEff[i].mBasePos.y + cloud_packet->mCloudEff[i].mPosition.y; sp5C.z = cloud_packet->mCloudEff[i].mBasePos.z + cloud_packet->mCloudEff[i].mPosition.z; + IF_DUSK(sp5C = s_cloud.read(i, sp5C, 300.0f);) sp74.x = -size; sp74.y = size; @@ -4964,7 +5119,7 @@ void drawVrkumo(Mtx drawMtx, GXColor& color, u8** tex) { sp50 = cM_fsin(j + (0.0001f * howa_loop_cnt)); sp50 *= vrkumo_packet->mVrkumoEff[k].mDistFalloff; - howa_loop_cnt += 1.5f * sp58; + howa_loop_cnt += 1.5f * sp58 IF_DUSK(* dusk::game_clock::original_frames()); sp68 += (0.05f * sp68 * sp50); sp64 = sp68 + (sp68 * vrkumo_packet->mVrkumoEff[k].mHeight); @@ -5001,6 +5156,7 @@ void drawVrkumo(Mtx drawMtx, GXColor& color, u8** tex) { sp60 = sp68 * (0.2f + (0.2f * (k / 100.0f))); sp5C = sp68 * (0.55f + (0.3f * (k / 100.0f))); spFC = vrkumo_packet->mVrkumoEff[k].mPosition; + IF_DUSK(spFC = s_vrkumo.read(k, spFC, 1000.0f);) spA4 = 0.0f; spA0 = 0.0f; @@ -5635,7 +5791,7 @@ void dKyr_odour_draw(Mtx drawMtx, u8** tex) { C_MTXLightPerspective(sp120, window_cam->view.fovy, window_cam->view.aspect, scale, -scale, 0.5f, 0.5f); cMtx_concat(sp120, j3dSys.getViewMtx(), spF0); - rot += 2.0f; + rot += 2.0f IF_DUSK(* dusk::game_clock::original_frames()); MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); @@ -6207,12 +6363,7 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { dKyr_set_btitex(&texobj, (ResTIMG*)tex[1]); #endif -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - rot += 0.7f; - } + rot += 0.7f IF_DUSK(* dusk::game_clock::original_frames()); MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); @@ -6451,12 +6602,7 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { dKyr_set_btitex(&texobj, (ResTIMG*)tex[0]); #endif -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - rot += 1.0f; - } + rot += 1.0f IF_DUSK(* dusk::game_clock::original_frames()); MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); @@ -6574,7 +6720,8 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { } else { sp5C = 0.0f; } - cLib_addCalc(&effect->field_0x2c, sp5C, 0.5f, 0.1f, 0.01f); + DUSK_IF_ELSE(dusk::vdt::present_addCalc, cLib_addCalc)( + &effect->field_0x2c, sp5C, 0.5f, 0.1f, 0.01f); daPy_py_c* player = (daPy_py_c*)dComIfGp_getPlayer(0); if (player != NULL && player->getKandelaarFlamePos() != NULL) { diff --git a/src/d/d_map_path_dmap.cpp b/src/d/d_map_path_dmap.cpp index f9d2f4d5f9..020f818cf1 100644 --- a/src/d/d_map_path_dmap.cpp +++ b/src/d/d_map_path_dmap.cpp @@ -12,6 +12,36 @@ #include "d/d_meter_HIO.h" #include "float.h" +#if TARGET_PC +#include "dusk/interp/samples.h" + +namespace { +struct MapPlayer { + dusk::interp::Samples position; + dusk::interp::Samples angle; + fpc_ProcID id = fpcM_ERROR_PROCESS_ID_e; + int room = -1; +}; + +MapPlayer& map_player() { + static MapPlayer samples; + auto* player = daPy_getPlayerActorClass(); + const int room = dComIfGp_roomControl_getStayNo(); + const fpc_ProcID id = player != nullptr ? fopAcM_GetID(player) : fpcM_ERROR_PROCESS_ID_e; + if (samples.id != id || samples.room != room) { + samples = {}; + samples.id = id; + samples.room = room; + } + if (player != nullptr) { + samples.position.capture(&player->current.pos, 1); + samples.angle.capture(&player->shape_angle, 1); + } + return samples; +} +} // namespace +#endif + bool dMapInfo_n::chkGetCompass() { return dComIfGs_isDungeonItemCompass() ? true : false; } @@ -60,7 +90,7 @@ Vec dMapInfo_n::getMapPlayerPos() { BE(Vec) pos; fopAc_ac_c* player = daPy_getPlayerActorClass(); if (player != NULL) { - pos = player->current.pos; + pos = DUSK_IF_ELSE(map_player().position.read(0, player->current.pos, 2000.0f), player->current.pos); } else { pos.x = 0.0f; pos.y = 0.0f; @@ -82,7 +112,7 @@ s16 dMapInfo_n::getMapPlayerAngleY() { daPy_py_c* player = daPy_getPlayerActorClass(); if (player != NULL) { - angle = player->shape_angle.y; + angle = DUSK_IF_ELSE(map_player().angle.read(0, player->shape_angle).y, player->shape_angle.y); } dStage_FileList2_dt_c* fileList2_p = dStage_roomControl_c::getFileList2(stayNo); diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index 32c0a360c2..dbb90e9b01 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -38,6 +38,8 @@ #include "m_Do/m_Do_mtx.h" #if TARGET_PC +#include "dusk/game_clock.h" +#include "dusk/interp/user_interface.h" #include "dusk/menu_pointer.h" #include "dusk/utilities.hpp" #endif @@ -999,10 +1001,14 @@ void dMenu_Collect2D_c::animationSet() { void dMenu_Collect2D_c::btkAnimeLoop0(J2DAnmTextureSRTKey* i_SRTKey) { if (i_SRTKey != NULL) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(mFrame, 1.0f, i_SRTKey->getFrameMax()); +#else mFrame++; if (mFrame >= i_SRTKey->getFrameMax()) { mFrame -= i_SRTKey->getFrameMax(); } +#endif i_SRTKey->setFrame(mFrame); } else { mFrame = 0.0f; @@ -1016,6 +1022,11 @@ void dMenu_Collect2D_c::btkAnimeLoop0(J2DAnmTextureSRTKey* i_SRTKey) { } void dMenu_Collect2D_c::setBackAlpha() { +#if TARGET_PC + const f32 target = mProcess >= 1 && mProcess <= 18 ? 1.0f : 0.0f; + dusk::vdt::advance_toward_frame(mBackAlpha, target, 0.2f); + mpBlackTex->setAlpha(mBackAlpha * 150.0f); +#else f32 alpha = mpBlackTex->getAlpha() / 150.0f; switch (mProcess) { @@ -1056,6 +1067,7 @@ void dMenu_Collect2D_c::setBackAlpha() { } mpBlackTex->setAlpha(alpha * 150.0f); +#endif } // Not sure if this works without gotos @@ -2435,17 +2447,22 @@ void dMenu_Collect2D_c::_move() { if (mProcess != last_process) { (this->*init[mProcess])(); } +#if !TARGET_PC btkAnimeLoop0(mpAnmKey); mpScreen->animation(); setBackAlpha(); +#endif setHIO(false); } void dMenu_Collect2D_c::_draw() { - #if TARGET_PC +#if TARGET_PC + btkAnimeLoop0(mpAnmKey); + mpScreen->animation(); + setBackAlpha(); menuCollectWide(); - #endif +#endif J2DGrafContext* grafPort = dComIfGp_getCurrentGrafPort(); grafPort->setup2D(); @@ -3056,7 +3073,7 @@ DUSK_GAME_DATA f32 dMenu_Collect3D_c::mViewOffsetY = -100.0f; void dMenu_Collect3D_c::setupItem3D(Mtx param_0) { GXSetViewport(0.0f, mViewOffsetY, FB_WIDTH, FB_HEIGHT, 0.0f, 1.0f); - mViewOffsetY = -100.0f; + IF_NOT_DUSK(mViewOffsetY = -100.0f); Mtx44 projection; C_MTXPerspective(projection, 45.0f, mDoGph_gInf_c::getAspect(), 1.0f, 100000.0f); GXSetProjection(projection, GX_PERSPECTIVE); @@ -3157,6 +3174,8 @@ void dMenu_Collect_c::_move() { void dMenu_Collect_c::draw() { dComIfGd_set2DOpa(mpCollect2D); + IF_DUSK_BLOCK(dusk::game_clock::is_sim_frame()) mpCollect3D->draw(); + IF_DUSK_BLOCK_END mpCollect2D->drawTop(); } diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index b84b596aff..476c9a0f5c 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -26,7 +26,10 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/menus.h" +#include "dusk/interp/user_interface.h" #include "dusk/settings.h" #include "dusk/version.hpp" #include "helpers/string.hpp" @@ -240,12 +243,14 @@ void dMenu_DmapBg_c::mapScreenInit() { void dMenu_DmapBg_c::mapScreenAnime() { if (0.0f == field_0xd94 && 0.0f == field_0xd98) { for (int i = 0; i < 1; i++) { +#if !TARGET_PC field_0xdc8[i] += 0.4f; if (field_0xdc8[i] >= field_0xd28[i]->getFrameMax()) { field_0xdc8[i] -= field_0xd28[i]->getFrameMax(); } field_0xd28[i]->setFrame(field_0xdc8[i]); mMapScreen[i]->animation(); +#endif } } } @@ -277,7 +282,9 @@ bool dMenu_DmapBg_c::iconScaleAnm() { return true; } - f32 dVar7 = fopMsgM_valueIncrease(field_0xdd1, field_0xdd2, 4); + IF_DUSK(dusk::vdt::advance_toward_frame(field_0xdd2, field_0xdd1, 1.0f)); + f32 dVar7 = DUSK_IF_ELSE(dusk::vdt::present_sine_ease(field_0xdd1, field_0xdd2), + fopMsgM_valueIncrease(field_0xdd1, field_0xdd2, 4)); f32 dVar8; f32 dVar1; if (field_0xdac > field_0xdb0) { @@ -291,7 +298,7 @@ bool dMenu_DmapBg_c::iconScaleAnm() { iconScale(1, dVar7, dVar8, dVar1); - field_0xdd2++; + IF_NOT_DUSK(field_0xdd2++); if (field_0xdd2 >= field_0xdd1) { rv = true; } @@ -861,6 +868,11 @@ dMenu_DmapBg_c::~dMenu_DmapBg_c() { } void dMenu_DmapBg_c::setAllAlphaRate(f32 i_rate, bool param_2) { +#if TARGET_PC + if (!param_2 && i_rate == 1.0f && field_0xd9c == 1.0f) { + return; + } +#endif field_0xd9c = i_rate; if (param_2) { @@ -923,8 +935,8 @@ void dMenu_DmapBg_c::addGoldFrameAlphaRate() { rate = 1.0f; setGoldAnimation(true); } else { - field_0xdd7++; - if (field_0xdd7 == g_fmapHIO.mDisplayFrameNum) { + DUSK_IF_ELSE(field_0xdd7 += dusk::game_clock::original_frames(), field_0xdd7++); + if (field_0xdd7 DUSK_IF_ELSE(>=, ==) g_fmapHIO.mDisplayFrameNum) { setGoldAnimation(true); } rate = (f32)(field_0xdd7 * field_0xdd7) / (f32)(g_fmapHIO.mDisplayFrameNum * g_fmapHIO.mDisplayFrameNum); @@ -1112,7 +1124,7 @@ void dMenu_DmapBg_c::draw() { -35.0f + (local_224.x - local_218.x), -35.0f + (local_224.y - local_218.y)); #if TARGET_PC - if (!dusk::frame_interp::is_enabled()) { + if (!dusk::interp::is_enabled()) { field_0xdda = 0; } #else @@ -1217,7 +1229,7 @@ void dMenu_DmapBg_c::update() { } if (mpBackTexture != NULL && field_0xdbc < 255.0f) { - field_0xdbc += 25.5f; + field_0xdbc += 25.5f IF_DUSK(* dusk::game_clock::original_frames()); if (field_0xdbc > 255.0f) { field_0xdbc = 255.0f; } @@ -1647,6 +1659,7 @@ void dMenu_Dmap_c::getIconPos(s8 i_floorNo, f32 param_2) { } dMenu_Dmap_c::~dMenu_Dmap_c() { + IF_DUSK(dusk::interp::erase_owned_samples(this)); s32 iVar2 = field_0xe8->getTotalFreeSize(); s32 iVar4 = field_0xe8->getTotalFreeSize(); @@ -2008,7 +2021,7 @@ void dMenu_Dmap_c::_move() { var_f31 = 0.70588237f; } - cLib_addCalc2(&field_0x110, var_f31, 0.4f, 0.5f); + DUSK_IF_ELSE(dusk::vdt::present_addCalc2, cLib_addCalc2)(&field_0x110, var_f31, 0.4f, 0.5f IF_DUSK_ARG(0.1f)); mpDrawBg->mpBlack->setAlphaRate(field_0x110); } @@ -2024,9 +2037,10 @@ void dMenu_Dmap_c::_move() { setMapTexture(); mapBgAnime(); mpDrawBg->calcCursor(); - mpDrawBg->addGoldFrameAlphaRate(); + IF_NOT_DUSK(mpDrawBg->addGoldFrameAlphaRate()); mDoExt_setCurrentHeap(prev_heap); + IF_DUSK(dusk::interp::capture_menu_pose(this, {field_0x104, field_0x108, field_0x10c})); } void dMenu_Dmap_c::setMapTexture() { @@ -2035,13 +2049,13 @@ void dMenu_Dmap_c::setMapTexture() { } void dMenu_Dmap_c::mapBgAnime() { - mpDrawBg->mapScreenAnime(); + IF_NOT_DUSK(mpDrawBg->mapScreenAnime()); } void dMenu_Dmap_c::mapControl() { u8 temp_r27 = field_0x17e; if (m_process == 0 || field_0x183 == 0) { - mMapCtrl->move(); + IF_NOT_DUSK(mMapCtrl->move()); getIconPos(mMapCtrl->getDispFloorNo(), mMapCtrl->getMapBlendPer()); getIconPos(mMapCtrl->getDispFloor2No(), 1.0f - mMapCtrl->getMapBlendPer()); @@ -2067,6 +2081,7 @@ void dMenu_Dmap_c::mapControl() { if (stick_value >= sp28 && field_0x181 != 2) { var_r28 = true; +#if !TARGET_PC f32 var_f31 = mMapCtrl->getStageMapSizeX(); if (var_f31 < mMapCtrl->getStageMapSizeZ()) { var_f31 = mMapCtrl->getStageMapSizeZ(); @@ -2084,9 +2099,10 @@ void dMenu_Dmap_c::mapControl() { f32 sp14 = temp_f28 * cM_scos(stick_angle); mMapCtrl->setPlusZoomCenterX(IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -sp18 :) sp18); mMapCtrl->setPlusZoomCenterZ(sp14); +#endif } - mMapCtrl->move(); + IF_NOT_DUSK(mMapCtrl->move()); getIconPos(mMapCtrl->getDispFloorNo(), mMapCtrl->getMapBlendPer()); getIconPos(mMapCtrl->getDispFloor2No(), 1.0f - mMapCtrl->getMapBlendPer()); @@ -2186,6 +2202,7 @@ bool dMenu_Dmap_c::isOpen() { mpDrawBg->setAllAlphaRate(field_0x10c, var_r27); mpDrawBg->setGoldFrameAlphaRate(0.0f); + IF_DUSK(dusk::interp::capture_menu_pose(this, {field_0x104, field_0x108, field_0x10c})); return var_r28; } @@ -2225,15 +2242,112 @@ bool dMenu_Dmap_c::isClose() { field_0x10c = temp_f31; mpDrawBg->setAllAlphaRate(field_0x10c, var_r29); mpDrawBg->decGoldFrameAlphaRate(); + IF_DUSK(dusk::interp::capture_menu_pose(this, {field_0x104, field_0x108, field_0x10c})); return var_r30; } -void dMenu_Dmap_c::_draw() { - if (mMapCtrl != NULL) { - mMapCtrl->draw(); +#if TARGET_PC +void dMenu_Dmap_c::presentAnims() { + if (mMapCtrl == NULL || mpDrawBg == NULL) { + return; + } + for (int i = 0; i < 8; ++i) { + mSelFloor[i]->presentAnime(); + } + for (int i = 0; i < 3; ++i) { + field_0x88[i]->presentAnime(); + } + + if (field_0x10c == 1.0f) { + mpDrawBg->addGoldFrameAlphaRate(); + + if (0.0f == mpDrawBg->field_0xd94 && 0.0f == mpDrawBg->field_0xd98) { + for (int i = 0; i < 1; i++) { + dusk::vdt::present_looping(mpDrawBg->field_0xdc8[i], mpDrawBg->field_0xd28[i], + 0.4f); + mpDrawBg->mMapScreen[i]->animation(); + } + } + mpDrawBg->clearIconInfo(); + presentMapView(); + if (field_0x17e == 1 || field_0x17e == 2) { + mpDrawBg->iconScaleAnm(); + } + } +} + +void dMenu_Dmap_c::presentMapView() { + const bool can_stick_scroll = m_process != 0 && field_0x183 != 0 && mMapCtrl->isEndZoomIn(); + f32 disp_center_x = mMapCtrl->getDispCenterX(); + f32 disp_center_z = mMapCtrl->getDispCenterZ(); + bool scrolled = false; + if (can_stick_scroll) { + f32 sp28 = g_fmapHIO.mScrollSpeedSlowBound < g_fmapHIO.mScrollSpeedFastBound ? + g_fmapHIO.mScrollSpeedSlowBound : + g_fmapHIO.mScrollSpeedFastBound; + f32 sp24 = g_fmapHIO.mScrollSpeedSlowBound > g_fmapHIO.mScrollSpeedFastBound ? + g_fmapHIO.mScrollSpeedSlowBound : + g_fmapHIO.mScrollSpeedFastBound; + f32 stick_value = mpCStick->getValueStick(); + s16 stick_angle = mpCStick->getAngleStick(); + if (stick_value >= sp28 && field_0x181 != 2) { + scrolled = true; + f32 var_f31 = mMapCtrl->getStageMapSizeX(); + if (var_f31 < mMapCtrl->getStageMapSizeZ()) { + var_f31 = mMapCtrl->getStageMapSizeZ(); + } + f32 var_f29 = stick_value < sp24 ? g_fmapHIO.mScrollSpeedDungeonMapSlow : + g_fmapHIO.mScrollSpeedDungeonMapFast; + f32 temp_f28 = (var_f29 / 100.0f) * var_f31 * dusk::game_clock::original_frames(); + f32 sp18 = temp_f28 * cM_ssin(stick_angle); + f32 sp14 = temp_f28 * cM_scos(stick_angle); + mMapCtrl->setPlusZoomCenterX(dusk::getSettings().game.enableMirrorMode ? -sp18 : sp18); + mMapCtrl->setPlusZoomCenterZ(sp14); + } + } + mMapCtrl->move(); + if (can_stick_scroll) { + if (scrolled && (disp_center_x != mMapCtrl->getDispCenterX() || + disp_center_z != mMapCtrl->getDispCenterZ())) + { + Z2GetAudioMgr()->seStartLevel(Z2SE_SY_MAP_SCROLL, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); + } + if (mpDrawBg->mpMeterHaihai != NULL && isMapMoveState() && + (getCMessageNum() == 0x37B || getCMessageNum() == 0x569)) + { + mpDrawBg->field_0xdda = 0; + if (mMapCtrl->getDispCenterX() > mMapCtrl->getZoomCenterMinX()) { + mpDrawBg->field_0xdda |= 1; + } + if (mMapCtrl->getDispCenterX() < mMapCtrl->getZoomCenterMaxX()) { + mpDrawBg->field_0xdda |= 4; + } + if (mMapCtrl->getDispCenterZ() > mMapCtrl->getZoomCenterMinZ()) { + mpDrawBg->field_0xdda |= 8; + } + if (mMapCtrl->getDispCenterZ() < mMapCtrl->getZoomCenterMaxZ()) { + mpDrawBg->field_0xdda |= 2; + } + mpDrawBg->mpMeterHaihai->_execute(0); + } + } + getIconPos(mMapCtrl->getDispFloorNo(), mMapCtrl->getMapBlendPer()); + getIconPos(mMapCtrl->getDispFloor2No(), 1.0f - mMapCtrl->getMapBlendPer()); +} +#endif + +void dMenu_Dmap_c::_draw() { + IF_DUSK(const auto pose = dusk::interp::read_menu_pose(this, {field_0x104, field_0x108, field_0x10c})); + if (mMapCtrl != NULL) { + IF_NOT_DUSK(mMapCtrl->draw()); if (mpDrawBg != NULL) { - mpDrawBg->setAllTrans(field_0x104, field_0x108); + IF_DUSK_BLOCK(dusk::game_clock::is_presentation_frame()) +#if TARGET_PC + mpDrawBg->setAllAlphaRate(pose.alpha, false); + presentAnims(); +#endif + mpDrawBg->setAllTrans(DUSK_IF_ELSE(pose.x, field_0x104), DUSK_IF_ELSE(pose.y, field_0x108)); if (mpDrawBg->mpBackTexture != NULL) { f32 var_f31 = 100.0f * mpDrawBg->field_0xd88; @@ -2269,8 +2383,10 @@ void dMenu_Dmap_c::_draw() { sp14[1] = -(field_0x138 + 100.0f * (sp20 * mMapCtrl->getPixelPerCm())); Vec spC; +#if !TARGET_PC spC.x = mpDrawBg->mMapScreen[0]->search(MULTI_CHAR('center_n'))->getGlbBounds().i.x + (mpDrawBg->mMapScreen[0]->search(MULTI_CHAR('center_n'))->getWidth() / 2); spC.y = mpDrawBg->mMapScreen[0]->search(MULTI_CHAR('center_n'))->getGlbBounds().i.y + (mpDrawBg->mMapScreen[0]->search(MULTI_CHAR('center_n'))->getHeight() / 2); +#endif CPaneMgr sp70; //!@bug It's unclear what this is supposed to be, but a stack pointer being converted to a bool is probably not intended. @@ -2282,9 +2398,15 @@ void dMenu_Dmap_c::_draw() { mpDrawBg->mpBackTexture->move(sp14[0] + (spC.x - (sp1C[0] / 2)), sp14[1] + (spC.y - (sp1C[1] / 2))); mpDrawBg->mpBackTexture->resize(sp1C[0], sp1C[1]); } + IF_DUSK_BLOCK_END - dComIfGd_set2DOpa(mpDrawBg); + IF_NOT_DUSK(dComIfGd_set2DOpa(mpDrawBg)); } + +#if TARGET_PC + mMapCtrl->draw(); + dComIfGd_set2DOpa(mpDrawBg); +#endif } } @@ -2759,7 +2881,7 @@ void dMenu_Dmap_c::zoomIn_init_proc() { void dMenu_Dmap_c::zoomIn_proc() { bool temp_r30 = mMapCtrl->isEndZoomIn(); - bool temp_r29 = mpDrawBg->iconScaleAnm(); + bool temp_r29 = DUSK_IF_ELSE(mpDrawBg->field_0xdd2 >= mpDrawBg->field_0xdd1, mpDrawBg->iconScaleAnm()); bool temp_r28 = true; if (temp_r30 == true && temp_r29 == true && temp_r28 == true) { @@ -2774,8 +2896,8 @@ void dMenu_Dmap_c::zoomIn_proc() { void dMenu_Dmap_c::zoomOut_init_proc() { #if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - mpDrawBg->resetScrollArrowMask(); + if (dusk::interp::is_enabled()) { + mpDrawBg->field_0xdda = 0; } #endif Z2GetAudioMgr()->seStart(Z2SE_SY_MAP_ZOOMOUT, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); @@ -2793,7 +2915,7 @@ void dMenu_Dmap_c::zoomOut_init_proc() { void dMenu_Dmap_c::zoomOut_proc() { bool temp_r30 = mMapCtrl->isEndZoomOut(); - bool temp_r29 = mpDrawBg->iconScaleAnm(); + bool temp_r29 = DUSK_IF_ELSE(mpDrawBg->field_0xdd2 >= mpDrawBg->field_0xdd1, mpDrawBg->iconScaleAnm()); bool temp_r28 = true; if (temp_r30 == true && temp_r29 == true && temp_r28 == true) { diff --git a/src/d/d_menu_dmap_map.cpp b/src/d/d_menu_dmap_map.cpp index b0be1dbf4b..3abb6c13c1 100644 --- a/src/d/d_menu_dmap_map.cpp +++ b/src/d/d_menu_dmap_map.cpp @@ -13,7 +13,7 @@ #include "m_Do/m_Do_graphic.h" #if TARGET_PC -#include +#include "dusk/interp/user_interface.h" #endif struct dMdm_HIO_prm_res_dst_s { @@ -462,7 +462,7 @@ void dMenu_StageMapCtrl_c::setPlusNowStayFloorNo(s8 param_0, u8 param_1) { } void dMenu_StageMapCtrl_c::moveFloor() { - if ((field_0xf3 == 0) && (field_0xf1 == 0)) { + if ((field_0xf3 DUSK_IF_ELSE(<= , ==) 0) && (field_0xf1 DUSK_IF_ELSE(<= , ==) 0)) { if (field_0xea != 0) { field_0xf3 = field_0xf0; field_0x90 = 0.0f; @@ -489,7 +489,7 @@ void dMenu_StageMapCtrl_c::moveFloor() { field_0xea = 0; } - } else if (field_0xf1 != 0 && field_0xea != 0) { + } else if (field_0xf1 DUSK_IF_ELSE(>, !=) 0 && field_0xea != 0) { if (field_0xea > 0) { if (field_0xe7 < field_0xed) { if ((field_0xe7 == std::floor(field_0xc4) && field_0xe8 == field_0xe7 + field_0xea) || field_0x90 > 0.5f) { @@ -521,22 +521,22 @@ void dMenu_StageMapCtrl_c::moveFloor() { field_0xeb = field_0xe7; } - if (field_0xf1 != 0) { + if (field_0xf1 DUSK_IF_ELSE(>, !=) 0) { f32 temp_f31 = (f32)(field_0xf0 - field_0xf3) / (f32)field_0xf0; field_0xeb = getFloorNo(field_0xc4); field_0xec = getFloorNo(field_0xc0); field_0x90 = getBlendPer(temp_f31 - std::floor(temp_f31)); - field_0xf1--; - if (field_0xf1 == 0) { + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(field_0xf1, 0.0f, 1.0f), field_0xf1--); + if (field_0xf1 DUSK_IF_ELSE(<=, ==) 0) { field_0xe8 = field_0xe7; } } - if (field_0xf3 != 0) { - field_0xf3--; - if (field_0xf3 != 0 && field_0xf1 != 0) { + if (field_0xf3 DUSK_IF_ELSE(>, !=) 0) { + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(field_0xf3, 0.0f, 1.0f), field_0xf3--); + if (field_0xf3 DUSK_IF_ELSE(>, !=) 0 && field_0xf1 DUSK_IF_ELSE(>, !=) 0) { f32 temp_f30 = (f32)(field_0xf0 - field_0xf3) / (f32)field_0xf0; field_0xeb = getFloorNo(field_0xc4); field_0xec = getFloorNo(field_0xc0); @@ -805,12 +805,14 @@ void dMenu_StageMapCtrl_c::zoomCalcSet(f32 param_0) { } void dMenu_StageMapCtrl_c::zoomIn_proc() { - f32 temp_f1 = fopMsgM_valueIncrease(field_0xf2, field_0xf2 - field_0xf4, 4); + IF_DUSK(dusk::vdt::advance_toward_frame(field_0xf4, 0.0f, 1.0f)); + f32 temp_f1 = DUSK_IF_ELSE(dusk::vdt::present_sine_ease(field_0xf2, field_0xf2 - field_0xf4), + fopMsgM_valueIncrease(field_0xf2, field_0xf2 - field_0xf4, 4)); field_0xd8 = temp_f1; zoomCalcSet(temp_f1); - field_0xf4--; - if (field_0xf4 < 0) { + IF_NOT_DUSK(field_0xf4--); + if (field_0xf4 DUSK_IF_ELSE(<=, <) 0) { field_0xf5 = 2; } } @@ -844,7 +846,7 @@ void dMenu_StageMapCtrl_c::zoomWait_init_proc() { } bool dMenu_DmapMapCtrl_c::isEnableZoomMove() const { - return field_0xf3 == 0 && (!getDisableZoomMoveFlgX() || !getDisableZoomMoveFlgZ()); + return field_0xf3 DUSK_IF_ELSE(<=, ==) 0 && (!getDisableZoomMoveFlgX() || !getDisableZoomMoveFlgZ()); } void dMenu_StageMapCtrl_c::zoomWait_proc() { @@ -888,12 +890,14 @@ void dMenu_StageMapCtrl_c::zoomOut_init_proc() { } void dMenu_StageMapCtrl_c::zoomOut_proc() { - f32 temp_f1 = fopMsgM_valueIncrease(field_0xf2, field_0xf2 - field_0xf4, 4); + IF_DUSK(dusk::vdt::advance_toward_frame(field_0xf4, 0.0f, 1.0f)); + f32 temp_f1 = DUSK_IF_ELSE(dusk::vdt::present_sine_ease(field_0xf2, field_0xf2 - field_0xf4), + fopMsgM_valueIncrease(field_0xf2, field_0xf2 - field_0xf4, 4)); zoomCalcSet(1.0f - temp_f1); field_0xd8 = 1.0f - temp_f1; - field_0xf4--; - if (field_0xf4 < 0) { + IF_NOT_DUSK(field_0xf4--); + if (field_0xf4 DUSK_IF_ELSE(<=, <) 0) { field_0xf5 = 0; } } @@ -991,7 +995,7 @@ void dMenu_StageMapCtrl_c::_delete() { bool dMenu_StageMapCtrl_c::isEnableZoomIn() { bool var_r30 = 0; - if (field_0xf3 == 0 && dStage_roomControl_c::getFileList2(field_0xe6) != NULL) { + if (field_0xf3 DUSK_IF_ELSE(<=, ==) 0 && dStage_roomControl_c::getFileList2(field_0xe6) != NULL) { var_r30 = 1; } @@ -999,7 +1003,7 @@ bool dMenu_StageMapCtrl_c::isEnableZoomIn() { } bool dMenu_StageMapCtrl_c::isEnableZoomOut() { - return field_0xf3 == 0; + return field_0xf3 DUSK_IF_ELSE(<=, ==) 0; } void dMenu_StageMapCtrl_c::setPlusZoomCenterX(f32 param_0) { diff --git a/src/d/d_menu_fmap.cpp b/src/d/d_menu_fmap.cpp index c2809c4e43..2615ad0b01 100644 --- a/src/d/d_menu_fmap.cpp +++ b/src/d/d_menu_fmap.cpp @@ -24,7 +24,10 @@ #include "d/actor/d_a_midna.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/menus.h" +#include "dusk/interp/user_interface.h" #include "dusk/memory.h" #include "dusk/version.hpp" #include "helpers/string.hpp" @@ -276,6 +279,7 @@ dMenu_Fmap_c::dMenu_Fmap_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i } dMenu_Fmap_c::~dMenu_Fmap_c() { + IF_DUSK(dusk::interp::erase_owned_samples(this)); mpHeap->getTotalFreeSize(); if (mpFieldDat != NULL) { mpHeap->free(mpFieldDat); @@ -494,28 +498,54 @@ void dMenu_Fmap_c::_delete() { /* empty function */ } +#if TARGET_PC +namespace { +struct FmapZoomView { + u8 process = 0xff; + f32 level = 0.0f; + f32 blend = 0.0f; +}; + +void lerp(FmapZoomView& out, const FmapZoomView& lhs, const FmapZoomView& rhs, f32 step) { + out = {rhs.process, dusk::interp::lerp(lhs.level, rhs.level, step), + dusk::interp::lerp(lhs.blend, rhs.blend, step)}; +} + +struct FmapZoomSamples { + dusk::interp::Samples views; + FmapZoomView end; + bool needsRestore = false; +}; +} +#endif + void dMenu_Fmap_c::_move() { + IF_DUSK(resetZoomEnd()); u8 process = mProcess; field_0x310 = 0; mpDraw2DBack->clearIconInfo(); (this->*move_process[mProcess])(); - - mpDraw2DTop->_execute(); + + IF_NOT_DUSK(mpDraw2DTop->_execute()); mpDraw2DBack->setBaseBackAlpha(g_fmHIO.mBaseBackAlpha); - mpDraw2DBack->btkAnimeLoop(g_fmHIO.mBackAnimeStep); + IF_NOT_DUSK(mpDraw2DBack->btkAnimeLoop(g_fmHIO.mBackAnimeStep)); mpDraw2DTop->setMoyaAlpha(g_fmHIO.mMoyaAlpha); - mpDraw2DTop->btkAnimeLoop(g_fmHIO.mTopAnimeStep); + IF_NOT_DUSK(mpDraw2DTop->btkAnimeLoop(g_fmHIO.mTopAnimeStep)); if (mProcess != process) { + IF_DUSK(captureZoomEnd(process)); (this->*init_process[mProcess])(); + IF_DUSK(resetRenderState()); } +#if !TARGET_PC if (mProcess == PROC_PORTAL_WARP_FORBID) { mpDraw2DBack->calcBackAlpha(true); } else { mpDraw2DBack->calcBackAlpha(false); } +#endif mpDraw2DBack->setAllTrans(mTransX, mTransY); mpDraw2DTop->setAllTrans(mTransX, mTransY); @@ -524,6 +554,7 @@ void dMenu_Fmap_c::_move() { drawDebugStage(); } +#if !TARGET_PC if (mSpotTextureFadeAlpha != 1.0f) { cLib_addCalc2(&mSpotTextureFadeAlpha, 1.0f, 0.4f, 0.5f); if (fabsf(mSpotTextureFadeAlpha - 1.0f) < 0.1f) { @@ -531,6 +562,7 @@ void dMenu_Fmap_c::_move() { } mpDraw2DBack->setSpotTextureFadeAlpha(mSpotTextureFadeAlpha); } +#endif #if TARGET_PC u8 region = mpDraw2DBack->getSelectRegion(); @@ -540,30 +572,120 @@ void dMenu_Fmap_c::_move() { mpDraw2DTop->mSelectRegionNo = 0xFF; } + captureRenderState(); #endif } +#if TARGET_PC +void dMenu_Fmap_c::captureRenderState() { + dusk::interp::capture_menu_pose(this, {mTransX, mTransY, mAlphaRatio}); + auto& zoom = dusk::interp::get(this); + if (zoom.end.process == 0xff) { + const FmapZoomView view{mProcess, (f32)mZoomLevel, field_0x1ec}; + zoom.views.capture(&view, 1); + } +} + +void dMenu_Fmap_c::captureZoomEnd(u8 process) { + switch (process) { + case PROC_ZOOM_ALL_TO_REGION: + case PROC_ZOOM_REGION_TO_ALL: + case PROC_ZOOM_REGION_TO_SPOT: + case PROC_ZOOM_SPOT_TO_REGION: + case PROC_PORTAL_DEMO1: + case PROC_PORTAL_DEMO3: + case PROC_YAMIBOSS_DEMO4: + case PROC_TABLE_DEMO1: + case PROC_TABLE_DEMO2: + case PROC_HOWL_DEMO1: + break; + default: + return; + } + + auto& zoom = dusk::interp::get(this); + zoom.end = {process, (f32)mZoomLevel, field_0x1ec}; + zoom.views.capture(&zoom.end, 1); +} + +void dMenu_Fmap_c::resetRenderState() { + if (dusk::interp::get(this).end.process != 0xff) { + dusk::interp::get(this).poses.reset(); + } else { + dusk::interp::erase_owned_samples(this); + } +} + +void dMenu_Fmap_c::resetZoomEnd() { + auto& zoom = dusk::interp::get(this); + if (zoom.needsRestore) { + presentZoomView(zoom.end.process, zoom.end.level, zoom.end.blend); + } + if (zoom.end.process != 0xff) { + zoom.views.reset(); + const FmapZoomView view{mProcess, (f32)mZoomLevel, field_0x1ec}; + zoom.views.capture(&view, 1); + } + zoom.end.process = 0xff; + zoom.needsRestore = false; +} +#endif + void dMenu_Fmap_c::_draw() { + IF_DUSK(const auto pose = dusk::interp::read_menu_pose(this, {mTransX, mTransY, mAlphaRatio})); +#if TARGET_PC + auto& zoom = dusk::interp::get(this); + const auto [renderProcess, zoomLevel, zoomBlend] = + zoom.views.read(0, {mProcess, (f32)mZoomLevel, field_0x1ec}); + const bool finishingZoom = renderProcess != mProcess; +#endif if (mpDraw2DBack != NULL && mpDraw2DTop != NULL) { +#if TARGET_PC + if (dusk::game_clock::is_presentation_frame()) { + mpDraw2DTop->setAllAlphaRate(pose.alpha, false); + presentAnims(); + presentZoomView(renderProcess, zoomLevel, zoomBlend); + zoom.needsRestore |= finishingZoom; + mpDraw2DBack->setAllTrans(pose.x, pose.y); + mpDraw2DTop->setAllTrans(pose.x, pose.y); + mpDraw2DBack->setAllAlphaRate(pose.alpha, false); + mpDraw2DTop->setMoyaAlpha(g_fmHIO.mMoyaAlpha); + if (field_0x305) { + mpMenuFmapMap->presentRendering(mpWorldData, mStartStageNo, + mpDraw2DBack->getRenderingPosX(), + mpDraw2DBack->getRenderingPosZ(), + mpDraw2DBack->getRenderingScale(), + mpDraw2DBack->getMapZoomRate()); + } + mpDraw2DBack->clearIconInfo(); + } +#endif if (field_0x305) { + IF_DUSK_BLOCK(dusk::game_clock::is_sim_frame()) mpMenuFmapMap->setRendering(mpWorldData, mStartStageNo, mpDraw2DBack->getRenderingPosX(), mpDraw2DBack->getRenderingPosZ(), mpDraw2DBack->getRenderingScale(), mpDraw2DBack->getMapZoomRate()); mpDraw2DBack->setStageInfo(mSpotNum, mpMenuFmapMap); - drawIcon(field_0x1ec, false); - if (mProcess == PROC_ZOOM_REGION_TO_SPOT || mProcess == PROC_ZOOM_SPOT_TO_REGION - || mProcess == PROC_YAMIBOSS_DEMO4 || mProcess == PROC_LIGHT_DEMO1 - || mProcess == PROC_TABLE_DEMO2 || mProcess == PROC_HOWL_DEMO1) + IF_DUSK_BLOCK_END + drawIcon(DUSK_IF_ELSE(zoomBlend, field_0x1ec), false); + if (DUSK_IF_ELSE(renderProcess, mProcess) == PROC_ZOOM_REGION_TO_SPOT || + DUSK_IF_ELSE(renderProcess, mProcess) == PROC_ZOOM_SPOT_TO_REGION || + DUSK_IF_ELSE(renderProcess, mProcess) == PROC_YAMIBOSS_DEMO4 || + DUSK_IF_ELSE(renderProcess, mProcess) == PROC_LIGHT_DEMO1 || + DUSK_IF_ELSE(renderProcess, mProcess) == PROC_TABLE_DEMO2 || + DUSK_IF_ELSE(renderProcess, mProcess) == PROC_HOWL_DEMO1) { - f32 scale = 1.0f - field_0x1ec; + f32 scale = 1.0f - DUSK_IF_ELSE(zoomBlend, field_0x1ec); mpDraw2DBack->iconScale(0, scale, scale, 1.0f - scale); } } else { drawPortalIcon(); - if (mProcess == PROC_ZOOM_ALL_TO_REGION || mProcess == PROC_ZOOM_REGION_TO_ALL) { - f32 scale = 1.0f - (f32)mZoomLevel / 10.0f; + if (DUSK_IF_ELSE(renderProcess, mProcess) == PROC_ZOOM_ALL_TO_REGION || + DUSK_IF_ELSE(renderProcess, mProcess) == PROC_ZOOM_REGION_TO_ALL) + { + f32 scale = 1.0f - DUSK_IF_ELSE(zoomLevel, (f32)mZoomLevel) / 10.0f; mpDraw2DBack->iconScale(0, scale, scale, 1.0f - scale); } } @@ -574,6 +696,61 @@ void dMenu_Fmap_c::_draw() { } } +#if TARGET_PC +void dMenu_Fmap_c::presentAnims() { + if (mAlphaRatio == 1.0f) { + if (mProcess == PROC_ALL_MAP) { + mpDraw2DBack->allmap_move2(mpStick); + } else if (mProcess == PROC_REGION_MAP) { + mpDraw2DBack->regionMapMove(mpStick); + } else if (mProcess == PROC_SPOT_MAP) { + mpDraw2DBack->stageMapMove(mpStick, 1, true); + } else if (mProcess == PROC_PORTAL_WARP_MAP || mProcess == PROC_PORTAL_DEMO5) { + mpDraw2DBack->regionMapMove(mpStick); + } + + mpDraw2DTop->_execute(); + mpDraw2DBack->calcBackAlpha(mProcess == PROC_PORTAL_WARP_FORBID); + if (mSpotTextureFadeAlpha != 1.0f) { + dusk::vdt::present_addCalc2(&mSpotTextureFadeAlpha, 1.0f, 0.4f, 0.5f, 0.1f); + mpDraw2DBack->setSpotTextureFadeAlpha(mSpotTextureFadeAlpha); + } + } + if (mpDraw2DBack != NULL && mpDraw2DTop != NULL) { + mpDraw2DBack->btkAnimeLoop(g_fmHIO.mBackAnimeStep); + mpDraw2DTop->btkAnimeLoop(g_fmHIO.mTopAnimeStep); + mpDraw2DTop->setMoyaAlpha(g_fmHIO.mMoyaAlpha); + } +} + +void dMenu_Fmap_c::presentZoomView(u8 process, f32 zoomLevel, f32 zoomBlend) { + switch (process) { + case PROC_ZOOM_ALL_TO_REGION: + case PROC_ZOOM_REGION_TO_ALL: + case PROC_PORTAL_DEMO1: + case PROC_PORTAL_DEMO3: + mpDraw2DBack->zoomMapCalc(std::clamp(zoomLevel / 10.0f, 0.0f, 1.0f)); + break; + case PROC_TABLE_DEMO1: + if (zoomLevel > 0) { + mpDraw2DBack->zoomMapCalc(zoomLevel / 10.0f); + } + break; + case PROC_ZOOM_REGION_TO_SPOT: + case PROC_ZOOM_SPOT_TO_REGION: + case PROC_YAMIBOSS_DEMO4: + mpDraw2DBack->zoomMapCalc2(zoomBlend); + break; + case PROC_TABLE_DEMO2: + case PROC_HOWL_DEMO1: + if (zoomLevel > 0) { + mpDraw2DBack->zoomMapCalc2(zoomBlend); + } + break; + } +} +#endif + u8 dMenu_Fmap_c::getNextStatus(u8* param_0) { u8 ret = 8; *param_0 = 0; @@ -774,9 +951,9 @@ void dMenu_Fmap_c::all_map_proc() { } } else { u8 region1 = mpDraw2DBack->getSelectRegion(); - mpDraw2DBack->allmap_move2(mpStick); + IF_NOT_DUSK(mpDraw2DBack->allmap_move2(mpStick)); u8 region2 = mpDraw2DBack->getSelectRegion(); - if (region1 != region2 || mResetAreaName) { + if (DUSK_IF_ELSE(region_change, region1 != region2) || mResetAreaName) { mResetAreaName = false; if (mpDraw2DBack->getSelectRegion() != 0xff && mpDraw2DBack->isShowRegion(mpDraw2DBack->getSelectRegion())) @@ -932,7 +1109,7 @@ void dMenu_Fmap_c::region_map_proc() { if (g_fmapHIO.mDisplayReferenceArea) { mpDraw2DBack->zoomMapCalc(1.0f); } - mpDraw2DBack->regionMapMove(mpStick); + IF_NOT_DUSK(mpDraw2DBack->regionMapMove(mpStick)); int stage_no, room_no; f32 pos_x = mpDraw2DBack->getArrowPos2DX() - mDoGph_gInf_c::getMinXF() @@ -1151,9 +1328,8 @@ void dMenu_Fmap_c::zoom_spot_to_region_init() { mZoomLevel = 10; field_0x1ec = 1.0f; #if TARGET_PC - // Frame interp note: field_0x122d used to be set every draw, causing flickering. Do it here instead. - if (dusk::frame_interp::is_enabled()) { - mpDraw2DBack->resetScrollArrowMask(); + if (dusk::interp::is_enabled()) { + mpDraw2DBack->field_0x122d = 0; } #endif Z2GetAudioMgr()->seStart(Z2SE_SY_MAP_ZOOMOUT, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); @@ -1215,7 +1391,7 @@ void dMenu_Fmap_c::spot_map_proc() { } else if (dMw_A_TRIGGER() && !dMeter2Info_isTouchKeyCheck(0xc) && dMeter2Info_getMeterClass()->getMeterDrawPtr()->getInsideObjCheck() != 1) { - mpDraw2DBack->stageMapMove(mpStick, 1, true); + IF_NOT_DUSK(mpDraw2DBack->stageMapMove(mpStick, 1, true)); } else if (dMw_Z_TRIGGER() && mpDraw2DTop->isWarpAccept()) { #if TARGET_PC || VERSION >= VERSION_GCN_JPN IF_DUSK_BLOCK(dusk::version::isRegionJpn()) @@ -1264,7 +1440,7 @@ void dMenu_Fmap_c::spot_map_proc() { mpDraw2DBack->zoomMapCalc2(field_0x1ec); } - mpDraw2DBack->stageMapMove(mpStick, 1, true); + IF_NOT_DUSK(mpDraw2DBack->stageMapMove(mpStick, 1, true)); int stage_no, room_no; f32 pos_x = mpDraw2DBack->getMapAreaGlobalCenterPosX() - mDoGph_gInf_c::getMinXF() @@ -1758,10 +1934,15 @@ bool dMenu_Fmap_c::isOpen() { mpDraw2DTop->setAllAlphaRate(mAlphaRatio, init); mpDraw2DBack->setSpotTextureFadeAlpha(mSpotTextureFadeAlpha); +#if TARGET_PC + mpDraw2DTop->setMoyaAlpha(g_fmHIO.mMoyaAlpha); + captureRenderState(); +#endif return ret; } bool dMenu_Fmap_c::isClose() { + IF_DUSK(resetZoomEnd()); bool ret = true; bool bVar2 = false; @@ -1799,6 +1980,10 @@ bool dMenu_Fmap_c::isClose() { mpDraw2DTop->setAllTrans(mTransX, mTransY); mpDraw2DTop->setAllAlphaRate(mAlphaRatio, bVar2); +#if TARGET_PC + mpDraw2DTop->setMoyaAlpha(g_fmHIO.mMoyaAlpha); + captureRenderState(); +#endif return ret; } @@ -2475,7 +2660,7 @@ void dMenu_Fmap_c::setAreaNameZero() { } void dMenu_Fmap_c::portalWarpMapMove(STControl* i_stick) { - mpDraw2DBack->regionMapMove(i_stick); + IF_NOT_DUSK(mpDraw2DBack->regionMapMove(i_stick)); dMenu_Fmap_portal_data_c* portal_dat = mpPortalDat; dMenu_Fmap_portal_data_c::data* portals = portal_dat->mData; f32 arrow_x = mpDraw2DBack->getArrowPos2DX(); diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 125bd91f39..e33dc65dcf 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -20,7 +20,9 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/user_interface.h" #include "dusk/ui/touch_controls.hpp" #include "dusk/version.hpp" @@ -358,13 +360,8 @@ void dMenu_Fmap2DBack_c::draw() { scrollAreaDraw(); } -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - blinkMove(30); - moveLightDropAnime(); - } + blinkMove(30); + moveLightDropAnime(); setCenterPosX(field_0x11dc, 1); drawIcon(mTransX, mTransZ, mAlphaRate, field_0xfa8 * mSpotTextureFadeAlpha); @@ -399,15 +396,10 @@ void dMenu_Fmap2DBack_c::draw() { (mArrowPos3DZ + control_ypos + fVar3) - fVar5, &mArrowPos2DX, &mArrowPos2DY); -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x11e0 -= g_fmapHIO.mCursorSpeed; + field_0x11e0 -= g_fmapHIO.mCursorSpeed IF_DUSK(* dusk::game_clock::original_frames()); - if (field_0x11e0 < 0.0f) { - field_0x11e0 += 360.0f; - } + if (field_0x11e0 < 0.0f) { + field_0x11e0 += 360.0f; } mpPointParent->getPanePtr()->rotate(mpPointParent->getSizeX() / 2.0f, @@ -448,7 +440,7 @@ void dMenu_Fmap2DBack_c::draw() { if (field_0x122d) { mpMeterHaihai->drawHaihai(field_0x122d); #if TARGET_PC - if (!dusk::frame_interp::is_enabled()) { + if (!dusk::interp::is_enabled()) { field_0x122d = 0; } #else @@ -1023,8 +1015,8 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) { } f32 speed = (sp24 / 100.0f) * zoomRate; - f32 delta_y = speed * cM_ssin(angle); - f32 delta_x = speed * cM_scos(angle); + f32 delta_y = speed * cM_ssin(angle) IF_DUSK(* dusk::game_clock::original_frames()); + f32 delta_x = speed * cM_scos(angle) IF_DUSK(* dusk::game_clock::original_frames()); #ifdef TARGET_PC if (dusk::getSettings().game.enableMirrorMode) { @@ -1869,20 +1861,20 @@ void dMenu_Fmap2DBack_c::calcBlink() { g_fmapHIO.mMapBlink[i].mUnselectedRegion.mBlinkSpeed); #if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x1218++; - if (field_0x1218 >= selected_blink_speed) { - field_0x1218 = 0; - } - - field_0x121a++; - if (field_0x121a >= unselected_blink_speed) { - field_0x121a = 0; - } + dusk::vdt::advance_looping_frame(field_0x1218, 1.0f, selected_blink_speed); + dusk::vdt::advance_looping_frame(field_0x121a, 1.0f, unselected_blink_speed); +#else + field_0x1218++; + if (field_0x1218 >= selected_blink_speed) { + field_0x1218 = 0; } + field_0x121a++; + if (field_0x121a >= unselected_blink_speed) { + field_0x121a = 0; + } +#endif + f32 t_selected = 0.0f; f32 t_unselected = 0.0f; @@ -1907,6 +1899,9 @@ void dMenu_Fmap2DBack_c::calcBlink() { } void dMenu_Fmap2DBack_c::calcBackAlpha(bool param_0) { +#if TARGET_PC + dusk::vdt::present_addCalc2(&mBackAlpha, param_0 ? 1.0f : 0.0f, 0.4f, 0.5f, 0.1f); +#else if (param_0) { if (mBackAlpha != 1.0f) { cLib_addCalc2(&mBackAlpha, 1.0f, 0.4f, 0.5f); @@ -1922,14 +1917,19 @@ void dMenu_Fmap2DBack_c::calcBackAlpha(bool param_0) { } } } +#endif } void dMenu_Fmap2DBack_c::btkAnimeLoop(f32 i_step) { if (mpBaseAnm) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(mAnmFrame, i_step, mpBaseAnm->getFrameMax()); +#else mAnmFrame += i_step; if (mAnmFrame >= mpBaseAnm->getFrameMax()) { mAnmFrame -= mpBaseAnm->getFrameMax(); } +#endif mpBaseAnm->setFrame(mAnmFrame); } else { mAnmFrame = 0.0f; @@ -1973,8 +1973,8 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) { base_speed = g_fmapHIO.mScrollSpeedRegionFast; } f32 speed = base_speed / 100.0f * local_78; - f32 speed_y = speed * cM_ssin(angle); - f32 speed_x = speed * cM_scos(angle); + f32 speed_y = speed * cM_ssin(angle) IF_DUSK(* dusk::game_clock::original_frames()); + f32 speed_x = speed * cM_scos(angle) IF_DUSK(* dusk::game_clock::original_frames()); control_xpos += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_y :) speed_y; control_ypos += speed_x; } @@ -2043,8 +2043,8 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param base_speed = g_fmapHIO.mScrollSpeedRegionZoomFast; } f32 speed = base_speed / 100.0f * local_78; - f32 speed_x = speed * cM_ssin(angle); - f32 speed_z = speed * cM_scos(angle); + f32 speed_x = speed * cM_ssin(angle) IF_DUSK(* dusk::game_clock::original_frames()); + f32 speed_z = speed * cM_scos(angle) IF_DUSK(* dusk::game_clock::original_frames()); mStageTransX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_x :) speed_x; mStageTransZ += speed_z; } else if (!param_2) { @@ -2110,6 +2110,11 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param } void dMenu_Fmap2DBack_c::setAllAlphaRate(f32 i_rate, bool i_init) { +#if TARGET_PC + if (!i_init && i_rate == 1.0f && mAlphaRate == 1.0f) { + return; + } +#endif mAlphaRate = i_rate; if (i_init) { mpBaseRoot->setBackupAlpha(); @@ -2304,6 +2309,7 @@ dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { mpHeap = i_heap; mTransX = 0.0f; mTransY = 0.0f; + IF_DUSK(mAlphaRate = 0.0f;) mpPortalBin = NULL; mpScrnExplain = NULL; @@ -2793,6 +2799,11 @@ void dMenu_Fmap2DTop_c::_execute() { } void dMenu_Fmap2DTop_c::setAllAlphaRate(f32 i_rate, bool i_init) { +#if TARGET_PC + if (!i_init && i_rate == 1.0f && mAlphaRate == 1.0f) { + return; + } +#endif mAlphaRate = i_rate; if (i_init) { mpTitleRoot->setBackupAlpha(); @@ -2850,10 +2861,14 @@ void dMenu_Fmap2DTop_c::draw() { void dMenu_Fmap2DTop_c::btkAnimeLoop(J2DAnmTextureSRTKey* i_anm, f32 i_delta) { if (i_anm) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(mAnmFrame, i_delta, i_anm->getFrameMax()); +#else mAnmFrame += i_delta; if (mAnmFrame >= i_anm->getFrameMax()) { mAnmFrame -= i_anm->getFrameMax(); } +#endif i_anm->setFrame(mAnmFrame); } else { mAnmFrame = 0.0f; @@ -3112,9 +3127,9 @@ void dMenu_Fmap2DTop_c::setArrowAlphaRatio(u8 i_mask, f32 i_rate) { } void dMenu_Fmap2DTop_c::setAlphaAnimeMin(CPaneMgrAlpha* i_pane) { - s16 timer = i_pane->getAlphaTimer(); + DUSK_IF_ELSE(f32, s16) timer = i_pane->getAlphaTimer(); if (timer > 0 || i_pane->getAlphaRate() != 0.0f) { - timer--; + DUSK_IF_ELSE(timer -= dusk::game_clock::original_frames(), timer--); if (timer < 0) { timer = 0; } @@ -3124,12 +3139,24 @@ void dMenu_Fmap2DTop_c::setAlphaAnimeMin(CPaneMgrAlpha* i_pane) { } void dMenu_Fmap2DTop_c::setAlphaAnimeMid(CPaneMgrAlpha* i_pane) { - s16 timer = i_pane->getAlphaTimer(); + DUSK_IF_ELSE(f32, s16) timer = i_pane->getAlphaTimer(); if (timer != 3 || i_pane->getAlphaRate() != 0.25f) { if (timer > 3) { - timer--; +#if TARGET_PC + timer -= dusk::game_clock::original_frames(); + if (timer < 3) { + timer = 3; + } +#endif + IF_NOT_DUSK(timer--); } else if (timer < 3) { - timer++; +#if TARGET_PC + timer += dusk::game_clock::original_frames(); + if (timer > 3) { + timer = 3; + } +#endif + IF_NOT_DUSK(timer++); } i_pane->alphaAnimeStart(timer); i_pane->setAlphaRate(timer / 6.0f * 0.5f); @@ -3137,9 +3164,9 @@ void dMenu_Fmap2DTop_c::setAlphaAnimeMid(CPaneMgrAlpha* i_pane) { } void dMenu_Fmap2DTop_c::setAlphaAnimeMax(CPaneMgrAlpha* i_pane) { - s16 timer = i_pane->getAlphaTimer(); + DUSK_IF_ELSE(f32, s16) timer = i_pane->getAlphaTimer(); if (timer < 5 || i_pane->getAlphaRate() != 1.0f) { - timer++; + DUSK_IF_ELSE(timer += dusk::game_clock::original_frames(), timer++); if (timer > 5) { timer = 5; } diff --git a/src/d/d_menu_fmap_map.cpp b/src/d/d_menu_fmap_map.cpp index 8a3c34fbde..c1e96bc56a 100644 --- a/src/d/d_menu_fmap_map.cpp +++ b/src/d/d_menu_fmap_map.cpp @@ -11,7 +11,7 @@ #include #if TARGET_PC -#include +#include "dusk/interp/user_interface.h" #endif static u8 twoValueLineInterpolation(u8 i_value1, u8 i_value2, f32 i_param) { @@ -481,6 +481,10 @@ dMenu_FmapMap_c::~dMenu_FmapMap_c() { } void dMenu_FmapMap_c::_create(u16 i_texWidth, u16 i_texHeight, u16 param_2, u16 param_3, void* i_res) { +#if TARGET_PC + mResetFlashFrame = true; + mPaletteFrameInitialized = false; +#endif m_res = (dMfm_prm_res_s*)i_res; m_palette = m_res->palette_data; field_0xcc = 0x6c; @@ -508,10 +512,55 @@ void dMenu_FmapMap_c::_delete() { DUSK_GAME_DATA const dMfm_HIO_prm_res_src_s dMfm_HIO_prm_res_src_s::m_other = {30}; +#if TARGET_PC +void dMenu_FmapMap_c::presentAnims() { + const f32 duration = dMfm_HIO_prm_res_src_s::m_other.mFlashDuration; + if (mResetFlashFrame) { + mFlashFrame = 0.0f; + mResetFlashFrame = false; + } else { + dusk::vdt::advance_looping_frame(mFlashFrame, 1.0f, duration + 1.0f); + } + mFlashTimer = std::max(0.0f, duration - mFlashFrame); + + const int periods[] = {m_res->field_0x168, m_res->field_0x16a}; + for (int i = 0; i < 2; ++i) { + if (periods[i] <= 0) { + mPaletteFrame[i] = 0.0f; + } else if (!mPaletteFrameInitialized) { + mPaletteFrame[i] = g_Counter.mCounter0 % periods[i]; + } else { + dusk::vdt::advance_looping_frame(mPaletteFrame[i], 1.0f, periods[i]); + } + } + mPaletteFrameInitialized = true; +} + +void dMenu_FmapMap_c::presentRendering(dMenu_Fmap_world_data_c* i_worldData, int i_startStageNo, + f32 i_posX, f32 i_posY, f32 i_scale, f32 i_zoomRate) +{ + mZoomRate = i_zoomRate; + mpWorldData = i_worldData; + mStartStageNo = i_startStageNo; + mPosX = i_posX; + mPosZ = i_posY; + mCmPerTexel = i_scale; + field_0x8 = mCmPerTexel * field_0x20 * mDoGph_gInf_c::getScale(); + field_0xc = mCmPerTexel * field_0x22; +} +#endif + void dMenu_FmapMap_c::draw() { { int unused; } f32 f30 = 0.0f; +#if TARGET_PC + presentAnims(); + const f32 half = dMfm_HIO_prm_res_src_s::m_other.mFlashDuration / 2; + if (half > 0.0f) { + f30 = std::clamp(std::fabs(mFlashTimer - half) / half, 0.0f, 1.0f); + } +#else if (mFlashTimer < dMfm_HIO_prm_res_src_s::m_other.mFlashDuration / 2) { f30 = (int)(dMfm_HIO_prm_res_src_s::m_other.mFlashDuration / 2 - mFlashTimer) / @@ -521,16 +570,19 @@ void dMenu_FmapMap_c::draw() { (int)(mFlashTimer - dMfm_HIO_prm_res_src_s::m_other.mFlashDuration / 2) / (f32)(int)(dMfm_HIO_prm_res_src_s::m_other.mFlashDuration / 2); } +#endif setPointColor(1.0f - f30); GXColor color; f32 f31; - f31 = getRateWithFrameCount(m_res->field_0x168); + f31 = DUSK_IF_ELSE(dusk::vdt::clamped_fraction(mPaletteFrame[0], m_res->field_0x168), + getRateWithFrameCount(m_res->field_0x168)); f31 = cM_ssin(f31 * 0x10000 - 0x8000) * 0.5f + 0.5f; twoColorLineInterporation(m_res->field_0xd8, m_res->field_0xdc, f31, color); setFmapPaletteColor(PALETTE_19, color); - f31 = getRateWithFrameCount(m_res->field_0x16a); + f31 = DUSK_IF_ELSE(dusk::vdt::clamped_fraction(mPaletteFrame[1], m_res->field_0x16a), + getRateWithFrameCount(m_res->field_0x16a)); f31 = cM_ssin(f31 * 0x10000 - 0x8000) * 0.5f + 0.5f; twoColorLineInterporation(m_res->field_0xe0, m_res->field_0xe4, f31, color); setFmapPaletteColor(PALETTE_1A, color); @@ -681,15 +733,18 @@ void dMenu_FmapMap_c::setRendering(dMenu_Fmap_world_data_c* i_worldData, int i_s f32 i_posX, f32 i_posY, f32 i_scale, f32 i_zoomRate) { mZoomRate = i_zoomRate; if (mFlash != mLastFlash || mStageCursor != mLastStageCursor) { + IF_DUSK(mResetFlashFrame = true); mFlashTimer = dMfm_HIO_prm_res_src_s::m_other.mFlashDuration; mLastFlash = mFlash; mLastStageCursor = mStageCursor; } else { +#if !TARGET_PC if (mFlashTimer != 0) { mFlashTimer--; } else { mFlashTimer = dMfm_HIO_prm_res_src_s::m_other.mFlashDuration; } +#endif } entry(i_worldData, i_startStageNo, i_posX, i_posY, i_scale); } diff --git a/src/d/d_menu_insect.cpp b/src/d/d_menu_insect.cpp index 684dd1235c..828e39384d 100644 --- a/src/d/d_menu_insect.cpp +++ b/src/d/d_menu_insect.cpp @@ -23,6 +23,7 @@ #include #if TARGET_PC +#include "dusk/game_clock.h" #include "dusk/menu_pointer.h" #include "dusk/version.hpp" #endif @@ -164,6 +165,11 @@ void dMenu_Insect_c::_move() { void dMenu_Insect_c::_draw() { if (mpArchive != NULL) { +#if TARGET_PC + if (dusk::game_clock::is_presentation_frame()) { + mpExpParent->presentAnime(); + } +#endif J2DGrafContext* grafPort = dComIfGp_getCurrentGrafPort(); mpBlackTex->setAlpha(0xff); @@ -193,6 +199,7 @@ void dMenu_Insect_c::_draw() { // operations applied which cannot easily be reverse engineered mpSelect_c->translate(g_drawHIO.mInsectListScreen.mConfirmOptionPosX_4x3 + 486.0f, g_drawHIO.mInsectListScreen.mConfirmOptionPosY_4x3 + 209.0f); + IF_DUSK(mpSelect_c->presentAnims()); mpSelect_c->draw(0.0f, 0.0f); mpIconScreen->draw(0.0f, 0.0f, grafPort); } diff --git a/src/d/d_menu_item_explain.cpp b/src/d/d_menu_item_explain.cpp index e6134e6c8b..0ca70af9e7 100644 --- a/src/d/d_menu_item_explain.cpp +++ b/src/d/d_menu_item_explain.cpp @@ -26,6 +26,7 @@ #include "d/d_msg_scrn_arrow.h" #if TARGET_PC +#include "dusk/game_clock.h" #include "dusk/version.hpp" #endif @@ -355,6 +356,7 @@ void dMenu_ItemExplain_c::draw(J2DOrthoGraph* i_graph) { // were likely either chosen by hand or had multiple arithmetic // operations applied which cannot easily be reverse engineered mpSelect_c->translate(486.0f, 209.0f); + IF_DUSK(mpSelect_c->presentAnims()); mpSelect_c->draw(0.0f, 0.0f); } if (mpArrow != NULL) { @@ -443,7 +445,7 @@ void dMenu_ItemExplain_c::open_init() { } void dMenu_ItemExplain_c::open_proc() { - mAlphaRatio += 2.0f; + mAlphaRatio += 2.0f IF_DUSK(* dusk::game_clock::original_frames()); if (mAlphaRatio >= 201.0f) { mAlphaRatio = 201.0f; mStatus = 2; diff --git a/src/d/d_menu_letter.cpp b/src/d/d_menu_letter.cpp index a1349e9fe5..55a240abac 100644 --- a/src/d/d_menu_letter.cpp +++ b/src/d/d_menu_letter.cpp @@ -17,8 +17,9 @@ #include "d/d_msg_scrn_arrow.h" #include "d/d_lib.h" -#ifdef TARGET_PC +#if TARGET_PC #include "dusk/achievements.h" +#include "dusk/interp/user_interface.h" #include "dusk/menu_pointer.h" #include "dusk/ui/touch_controls.hpp" #include "dusk/version.hpp" @@ -29,9 +30,7 @@ static void enable_turn_page_controls(bool enabled) { dusk::ui::set_control_override(dusk::ui::Control::L, controlOverride); dusk::ui::set_control_override(dusk::ui::Control::R, controlOverride); } -#endif -#if TARGET_PC #define D_MENU_LETTER_LINE_MAX (dusk::version::isRegionJpn() ? 9 : 12) #elif VERSION == VERSION_GCN_JPN #define D_MENU_LETTER_LINE_MAX 9 @@ -226,6 +225,7 @@ void dMenu_Letter_c::_draw() { if (mpArchive == NULL) { return; } + IF_DUSK(presentAnims()); J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort(); u8 prevAlpha = mpBlackTex->getAlpha(); mpBlackTex->setAlpha(0xff); @@ -531,7 +531,7 @@ void dMenu_Letter_c::slide_right_init() { } void dMenu_Letter_c::slide_right_move() { - cLib_addCalc2(&field_0x358, 0.0f, 0.5f, 50.0f); + IF_NOT_DUSK(cLib_addCalc2(&field_0x358, 0.0f, 0.5f, 50.0f)); if (fabsf(field_0x358) < 0.1f) { field_0x358 = 0.0f; mProcess = 0; @@ -552,7 +552,7 @@ void dMenu_Letter_c::slide_left_init() { } void dMenu_Letter_c::slide_left_move() { - cLib_addCalc2(&field_0x358, 0.0f, 0.5f, 50.0f); + IF_NOT_DUSK(cLib_addCalc2(&field_0x358, 0.0f, 0.5f, 50.0f)); if (fabsf(field_0x358) < 0.1f) { field_0x358 = 0.0f; mProcess = 0; @@ -607,7 +607,7 @@ void dMenu_Letter_c::read_open_init() { void dMenu_Letter_c::read_open_move() { s16 openLetterFrame = g_drawHIO.mLetterSelectScreen.mOpenFrame[dMeter_drawLetterHIO_c::LETTER_FRAME]; - field_0x36a++; + IF_NOT_DUSK(field_0x36a++); if (field_0x36a >= openLetterFrame) { mProcess = 4; for (int i = 0; i < 2; i++) { @@ -695,7 +695,7 @@ void dMenu_Letter_c::read_next_fadeout_init() { void dMenu_Letter_c::read_next_fadeout_move() { s16 closeWindowFrame = g_drawHIO.mLetterSelectScreen.mCloseFrame[dMeter_drawLetterHIO_c::WINDOW_FRAME]; - field_0x36a--; + IF_NOT_DUSK(field_0x36a--); if (field_0x36a <= 0) { mProcess = 6; for (int i = 0; i < 2; i++) { @@ -744,7 +744,7 @@ void dMenu_Letter_c::read_next_fadein_init() { void dMenu_Letter_c::read_next_fadein_move() { s16 openLetterFrame = g_drawHIO.mLetterSelectScreen.mOpenFrame[dMeter_drawLetterHIO_c::LETTER_FRAME]; - field_0x36a++; + IF_NOT_DUSK(field_0x36a++); if (field_0x36a >= openLetterFrame) { mProcess = 4; for (int i = 0; i < 2; i++) { @@ -773,7 +773,7 @@ void dMenu_Letter_c::read_close_init() { void dMenu_Letter_c::read_close_move() { s16 closeLetterFrame = g_drawHIO.mLetterSelectScreen.mCloseFrame[dMeter_drawLetterHIO_c::LETTER_FRAME]; - field_0x36a--; + IF_NOT_DUSK(field_0x36a--); if (field_0x36a <= 0) { mProcess = 0; for (int i = 0; i < 2; i++) { @@ -792,6 +792,41 @@ void dMenu_Letter_c::read_close_move() { } } +#if TARGET_PC +void dMenu_Letter_c::presentAnims() { + if (mProcess == 1 || mProcess == 2) { + dusk::vdt::present_addCalc2(&field_0x358, 0.0f, 0.5f, 50.0f, 0.1f); + return; + } + + const auto& hio = g_drawHIO.mLetterSelectScreen; + const bool opening = mProcess == 3 || mProcess == 6; + const bool page = mProcess == 5 || mProcess == 6; + if (!opening && mProcess != 5 && mProcess != 7) { + return; + } + const f32 duration = opening ? hio.mOpenFrame[dMeter_drawLetterHIO_c::LETTER_FRAME] : + hio.mCloseFrame[page ? dMeter_drawLetterHIO_c::WINDOW_FRAME : dMeter_drawLetterHIO_c::LETTER_FRAME]; + dusk::vdt::advance_toward_frame(field_0x36a, opening ? duration : 0.0f, 1.0f); + const f32 alpha = duration <= 0.0f ? (opening ? 1.0f : 0.0f) : + dusk::vdt::clamped_fraction(field_0x36a, duration); + for (int i = 0; i < 2; i++) { + if (page) { + field_0x2ec[i]->setAlphaRate(alpha); + if (field_0x2f4[i] != NULL) { + field_0x2f4[i]->setAlphaRate(alpha); + } + } else { + mpTextParent[i]->scale(hio.mLetterWindowScale * alpha, hio.mLetterWindowScale * alpha); + mpTextParent[i]->setAlphaRate(alpha); + } + } + if (!page) { + mpBlackTex->setAlpha(hio.mWindowBGAlpha * alpha); + } +} +#endif + void dMenu_Letter_c::screenSetMenu() { static const u64 tag_sub0[6] = { MULTI_CHAR('menu_t0s'), MULTI_CHAR('menu_t1s'), MULTI_CHAR('menu_t2s'), MULTI_CHAR('menu_t3s'), MULTI_CHAR('menu_t4s'), MULTI_CHAR('menu_t5s'), diff --git a/src/d/d_menu_map_common.cpp b/src/d/d_menu_map_common.cpp index c18f5825ff..c4c1485826 100644 --- a/src/d/d_menu_map_common.cpp +++ b/src/d/d_menu_map_common.cpp @@ -6,6 +6,10 @@ #include "d/d_select_cursor.h" #include "global.h" +#if TARGET_PC +#include "dusk/interp/user_interface.h" +#endif + struct map_icon_size_t { f32 size_x; f32 size_y; @@ -584,10 +588,16 @@ void dMenuMapCommon_c::setBlendRatio(u8 i_iconNo, f32 param_2, f32 param_3) { } void dMenuMapCommon_c::blinkMove(s16 param_1) { +#if TARGET_PC + f32 phase = mBlinkTimer + 15.0f; + dusk::vdt::advance_looping_frame(phase, 1.0f, std::max(0, (int)param_1) + 15.0f); + mBlinkTimer = phase - 15.0f; +#else mBlinkTimer++; if (mBlinkTimer >= param_1) { S16_SUB(mBlinkTimer, param_1 + 15); } +#endif if (mBlinkTimer < 0) { mBlinkAlpha = 1.0f; @@ -610,10 +620,20 @@ void dMenuMapCommon_c::moveLightDropAnime() { u8 flash_start_alpha = g_fmapHIO.mMapIconHIO.mLightDropFlashStartAlphaOut[bVar6]; u8 flash_end_alpha = g_fmapHIO.mMapIconHIO.mLightDropFlashEndAlphaOut[bVar6]; +#if TARGET_PC + if (flash_frame_num <= 0) { + mLightDropFlashTimer = 0.0f; + _c80 = flash_start_alpha; + _c7c = flash_start_scale; + return; + } + dusk::vdt::advance_looping_frame(mLightDropFlashTimer, 1.0f, flash_frame_num); +#else mLightDropFlashTimer++; if (mLightDropFlashTimer >= flash_frame_num) { mLightDropFlashTimer -= flash_frame_num; } +#endif f32 fVar7; if (mLightDropFlashTimer <= flash_frame_num / 2.0f) { diff --git a/src/d/d_menu_option.cpp b/src/d/d_menu_option.cpp index 7f18fa426b..fa10149e08 100644 --- a/src/d/d_menu_option.cpp +++ b/src/d/d_menu_option.cpp @@ -27,6 +27,8 @@ #include "JSystem/JAudio2/JASDriverIF.h" #if TARGET_PC +#include "dusk/game_clock.h" +#include "dusk/interp/user_interface.h" #include "dusk/menu_pointer.h" #include "dusk/ui/touch_controls.hpp" #include "dusk/version.hpp" @@ -568,7 +570,7 @@ skip: } (this->*process[OPTION_PROC(field_0x3ef)])(); - mpSelectScreen->animation(); + IF_NOT_DUSK(mpSelectScreen->animation()); if (oldValue != field_0x3ef) { (this->*init[OPTION_PROC(field_0x3ef)])(); } @@ -578,6 +580,11 @@ skip: void dMenu_Option_c::_draw() { if (mpArchive != NULL) { +#if TARGET_PC + if (dusk::game_clock::is_presentation_frame()) { + presentAnims(); + } +#endif J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort(); #if (PLATFORM_WII || PLATFORM_SHIELD) if (mpCalibration != NULL && field_0x3f4 != 5 && field_0x3f4 != 0 && field_0x3f4 != 4) { @@ -667,6 +674,10 @@ bool dMenu_Option_c::checkRightTrigger() { } void dMenu_Option_c::setAnimation() { +#if TARGET_PC + dusk::vdt::present_looping(field_0x3c0, field_0x2c, 2.0f); + dusk::vdt::present_looping(field_0x3c4, field_0x30, 2.0f); +#else s16 frameMax; field_0x3c0 += 2; @@ -682,6 +693,7 @@ void dMenu_Option_c::setAnimation() { field_0x3c4 = field_0x3c4 - frameMax; } field_0x30->setFrame(field_0x3c4); +#endif } bool dMenu_Option_c::_open() { @@ -876,7 +888,7 @@ void dMenu_Option_c::vib_init() { void dMenu_Option_c::vib_move() { bool upTrigger = mpStick->checkUpTrigger(); - bool downTrigger = mpStick->checkDownTrigger(); + IF_NOT_DUSK(bool downTrigger =) mpStick->checkDownTrigger(); bool leftTrigger = checkLeftTrigger(); bool rightTrigger = checkRightTrigger(); @@ -891,10 +903,14 @@ void dMenu_Option_c::vib_move() { field_0x3ef = PROC_ATTEN_e; #endif Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); - } else if (downTrigger) { + } +#ifndef TARGET_PC + else if (downTrigger) { field_0x3ef = OPTION_SELECT(PROC_SOUND_e); Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); - } else if (leftTrigger) { + } +#endif + else if (leftTrigger) { if (isRumbleSupported()) { if (field_0x3ea == 0) { field_0x3ea = 1; @@ -1010,6 +1026,7 @@ void dMenu_Option_c::change_init() { } void dMenu_Option_c::change_move() { +#if !TARGET_PC f32 x = 0.0f; if (field_0x3da > 0) { @@ -1018,15 +1035,16 @@ void dMenu_Option_c::change_move() { field_0x3da++; } u8 index; +#endif switch (OPTION_PROC(field_0x3f5)) { case PROC_ATTEN_e: - index = field_0x3f5; + IF_NOT_DUSK(index = field_0x3f5); if (field_0x3da == 0) { setAttenString(); } break; case PROC_RUBY_e: - index = field_0x3f5; + IF_NOT_DUSK(index = field_0x3f5); if (field_0x3da == 0) { IF_DUSK_BLOCK(dusk::version::isRegionJpn()) setRubyString(); @@ -1034,18 +1052,19 @@ void dMenu_Option_c::change_move() { } break; case PROC_VIB_e: - index = field_0x3f5; + IF_NOT_DUSK(index = field_0x3f5); if (field_0x3da == 0) { setVibString(); } break; case PROC_SOUND_e: - index = field_0x3f5; + IF_NOT_DUSK(index = field_0x3f5); if (field_0x3da == 0) { setSoundString(); } break; } +#if !TARGET_PC if (field_0x3da > 0) { f32 initPosX = (5 - field_0x3da) / 5.0f; if (mpMenuText[index][3] != NULL) { @@ -1065,6 +1084,7 @@ void dMenu_Option_c::change_move() { mpMenuText[index][i]->paneTrans(x + field_0x3b4, 0.0f); } } +#endif if (field_0x3da == 0) { for (int i = 0; i < 6; i++) { for (int j = 2; j < 6; j++) { @@ -1098,18 +1118,20 @@ void dMenu_Option_c::confirm_open_move() { u32 status = mpWarning->getStatus(); bool yesNoMenuMove = yesnoMenuMoveAnm(); +#if !TARGET_PC if (field_0x374 != 1.0f) { cLib_addCalc2(&field_0x374, 1.0f, 0.4f, 0.5f); if (fabsf(field_0x374 - 1.0f) < 0.1f) { field_0x374 = 1.0f; } } +#endif if (status == 1 && yesNoMenuMove == 1 && field_0x374 == 1.0f) { yesnoCursorShow(); field_0x3ef = OPTION_SELECT(PROC_CONFIRM_MOVE_MOVE_e); } mpWarning->_move(); - setAnimation(); + IF_NOT_DUSK(setAnimation()); } void dMenu_Option_c::confirm_move_init() { @@ -1139,13 +1161,11 @@ void dMenu_Option_c::confirm_move_move() { field_0x3ef = OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e); dMeter2Info_set2DVibrationM(); mpWarning->_move(); - setAnimation(); return; } yesnoSelectAnmSet(); field_0x3ef = OPTION_SELECT(PROC_CONFIRM_SELECT_MOVE_e); mpWarning->_move(); - setAnimation(); return; } if (clicked) { @@ -1153,7 +1173,6 @@ void dMenu_Option_c::confirm_move_move() { field_0x3ef = OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e); dMeter2Info_set2DVibrationM(); mpWarning->_move(); - setAnimation(); return; } } @@ -1188,7 +1207,7 @@ void dMenu_Option_c::confirm_move_move() { } } mpWarning->_move(); - setAnimation(); + IF_NOT_DUSK(setAnimation()); } void dMenu_Option_c::confirm_select_init() { @@ -1221,14 +1240,13 @@ void dMenu_Option_c::confirm_select_move() { field_0x3ef = OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e); dMeter2Info_set2DVibrationM(); mpWarning->_move(); - setAnimation(); return; } #endif field_0x3ef = OPTION_SELECT(PROC_CONFIRM_MOVE_MOVE_e); } mpWarning->_move(); - setAnimation(); + IF_NOT_DUSK(setAnimation()); } void dMenu_Option_c::confirm_close_init() { @@ -1243,12 +1261,14 @@ void dMenu_Option_c::confirm_close_init() { void dMenu_Option_c::confirm_close_move() { u32 status = mpWarning->getStatus(); yesnoMenuMoveAnm(); +#if !TARGET_PC if (field_0x374 != 0.0f) { cLib_addCalc2(&field_0x374, 0.0f, 0.4f, 0.5); if (fabsf(field_0x374) < 0.1f) { field_0x374 = 0.0f; } } +#endif if (status == 1 && status == 1 && field_0x374 == 0.0f) { if (field_0x3f7 == 1) { if (field_0x3f9 == 1) { @@ -1286,7 +1306,7 @@ void dMenu_Option_c::confirm_close_move() { } } mpWarning->_move(); - setAnimation(); + IF_NOT_DUSK(setAnimation()); } void dMenu_Option_c::tv_open1_move() { @@ -1369,8 +1389,7 @@ void dMenu_Option_c::calibration_close2_move() { void dMenu_Option_c::menuVisible() { for (int i = 0; i < 6; i++) { - if (i < OPTION_SELECT(PROC_CHANGE_MOVE_e)) - { + if (i < OPTION_SELECT(DUSK_IF_ELSE(PROC_SOUND_e, PROC_CHANGE_MOVE_e))) { menuShow(i); } else { menuHide(i); @@ -2478,7 +2497,7 @@ bool dMenu_Option_c::isRumbleSupported() { #if TARGET_PC bool dMenu_Option_c::pointerConfirmSelect() { dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); - for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) { + for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) { if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { dusk::menu_pointer::set_hover_target(i); return false; @@ -2502,7 +2521,7 @@ bool dMenu_Option_c::pointerConfirmSelect() { bool dMenu_Option_c::dpdMenuMove() { #if TARGET_PC dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); - for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) { + for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) { if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { continue; } @@ -2582,20 +2601,6 @@ bool dMenu_Option_c::dpdMenuMove() { -1.0f, 0); } return true; - case PROC_SOUND_e: - if (field_0x3e9 == 0) { - field_0x3e9 = 2; - } else { - field_0x3e9--; - } - field_0x3da = 5; - mDoAud_setOutputMode(dMo_soundMode[field_0x3e9]); - setSoundMode(dMo_soundMode[field_0x3e9]); - field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); - field_0x3f5 = OPTION_SELECT(PROC_SOUND_e); - Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, - -1.0f, 0); - return true; } } #endif @@ -2671,6 +2676,7 @@ void dMenu_Option_c::yesnoMenuMoveAnmInitSet(int param_0, int param_1) { bool dMenu_Option_c::yesnoMenuMoveAnm() { bool ret; if (field_0x3c8[2] != field_0x3c8[3]) { +#if !TARGET_PC if (field_0x3c8[2] < field_0x3c8[3]) { field_0x3c8[2] += 2; if (field_0x3c8[2] > field_0x3c8[3]) { @@ -2685,6 +2691,7 @@ bool dMenu_Option_c::yesnoMenuMoveAnm() { field_0x20->setFrame(field_0x3c8[2]); mpYesNoSelBase_c[0]->getPanePtr()->animationTransform(); mpYesNoSelBase_c[1]->getPanePtr()->animationTransform(); +#endif ret = 0; } if (field_0x3c8[2] == field_0x3c8[3]) { @@ -2709,6 +2716,101 @@ static s32 OptYnSelStartFrameTbl[2] = {1251, 1236}; static s32 OptYnSelEndFrameTbl[2] = {1236, 1251}; +#if TARGET_PC +void dMenu_Option_c::presentAnims() { + if (field_0x3da != 0.0f) { + dusk::vdt::advance_toward_frame(field_0x3da, 0.0f, 1.0f); + const u8 index = field_0x3f5; + if (field_0x3da == 0.0f) { + switch (OPTION_PROC(field_0x3f5)) { + case PROC_ATTEN_e: + setAttenString(); + break; + case PROC_RUBY_e: + IF_DUSK_BLOCK(dusk::version::isRegionJpn()) + setRubyString(); + IF_DUSK_BLOCK_END + break; + case PROC_VIB_e: + setVibString(); + break; + case PROC_SOUND_e: + setSoundString(); + break; + } + for (int i = 0; i < 6; i++) { + for (int j = 2; j < 6; j++) { + if (mpMenuText[i][j] != NULL) { + mpMenuText[i][j]->hide(); + } + } + } + for (int i = 0; i < 2; i++) { + if (mpMenuText[index][i] != NULL) { + mpMenuText[index][i]->show(); + mpMenuText[index][i]->paneTrans(field_0x3b4, 0.0f); + } + } + } else { + f32 x = 0.0f; + if (field_0x3da > 0) { + f32 initPosX = (5 - field_0x3da) / 5.0f; + if (mpMenuText[index][3] != NULL) { + x = mpMenuText[index][3]->getInitPosX() - mpMenuText[index][0]->getInitPosX(); + } + x *= initPosX; + } else if (field_0x3da < 0) { + f32 initPosX = (field_0x3da + 5) / 5.0f; + if (mpMenuText[index][5] != NULL) { + x = mpMenuText[index][5]->getInitPosX() - mpMenuText[index][0]->getInitPosX(); + } + x *= initPosX; + } + for (int i = 0; i < 6; i++) { + if (mpMenuText[index][i] != NULL) { + mpMenuText[index][i]->show(); + mpMenuText[index][i]->paneTrans(x + field_0x3b4, 0.0f); + } + } + } + } + setAnimation(); + if (OPTION_PROC(field_0x3ef) == PROC_CONFIRM_OPEN_MOVE_e) { + dusk::vdt::present_addCalc2(&field_0x374, 1.0f, 0.4f, 0.5f, 0.1f); + } else if (OPTION_PROC(field_0x3ef) == PROC_CONFIRM_CLOSE_MOVE_e) { + dusk::vdt::present_addCalc2(&field_0x374, 0.0f, 0.4f, 0.5f, 0.1f); + } + if (mpSelectScreen != NULL) { + mpSelectScreen->animation(); + } + presentLayoutAnims(); + for (int i = 0; i < 2; ++i) { + mpYesNoTxt_c[i]->presentAnime(); + mpYesNoCurWaku_c[i]->presentAnime(); + mpYesNoCurWakuG0_c[i]->presentAnime(); + mpYesNoCurWakuG1_c[i]->presentAnime(); + } +} + +void dMenu_Option_c::presentLayoutAnims() { + if (mpWarning != NULL) { + mpWarning->presentAnims(); + } + + if (field_0x3c8[2] != field_0x3c8[3]) { + dusk::vdt::present_shared(field_0x3c8[2], field_0x3c8[3], field_0x20, + {mpYesNoSelBase_c[0] != NULL ? mpYesNoSelBase_c[0]->getPanePtr() : NULL, + mpYesNoSelBase_c[1] != NULL ? mpYesNoSelBase_c[1]->getPanePtr() : NULL}); + } + + for (int i = 0; i < 2; i++) { + J2DPane* pane = mpYesNoSelBase_c[i] != NULL ? mpYesNoSelBase_c[i]->getPanePtr() : NULL; + dusk::vdt::present_selected(field_0x3c8[i], pane, field_0x24, (f32)OptYnSelStartFrameTbl[i], + field_0x28, (f32)OptYnSelEndFrameTbl[i]); + } +} +#endif + u8 dMenu_Option_c::yesnoSelectMoveAnm() { u8 ret = 0; bool bVar1; @@ -2716,6 +2818,7 @@ u8 dMenu_Option_c::yesnoSelectMoveAnm() { if (field_0x3fa != 0xff) { if (field_0x3c8[field_0x3fa] != OptYnSelStartFrameTbl[field_0x3fa]) { +#if !TARGET_PC if (field_0x3c8[field_0x3fa] < OptYnSelStartFrameTbl[field_0x3fa]) { field_0x3c8[field_0x3fa] += 2; if (field_0x3c8[field_0x3fa] > OptYnSelStartFrameTbl[field_0x3fa]) { @@ -2729,6 +2832,7 @@ u8 dMenu_Option_c::yesnoSelectMoveAnm() { } field_0x24->setFrame(field_0x3c8[field_0x3fa]); mpYesNoSelBase_c[field_0x3fa]->getPanePtr()->animationTransform(); +#endif } if (field_0x3c8[field_0x3fa] == OptYnSelStartFrameTbl[field_0x3fa]) { bVar1 = true; @@ -2740,6 +2844,7 @@ u8 dMenu_Option_c::yesnoSelectMoveAnm() { } if (field_0x3f9 != 0xff) { if (field_0x3c8[field_0x3f9] != OptYnSelEndFrameTbl[field_0x3f9]) { +#if !TARGET_PC if (field_0x3c8[field_0x3f9] < OptYnSelEndFrameTbl[field_0x3f9]) { field_0x3c8[field_0x3f9] += 2; if (field_0x3c8[field_0x3f9] > OptYnSelEndFrameTbl[field_0x3f9]) { @@ -2753,6 +2858,7 @@ u8 dMenu_Option_c::yesnoSelectMoveAnm() { } field_0x28->setFrame(field_0x3c8[field_0x3f9]); mpYesNoSelBase_c[field_0x3f9]->getPanePtr()->animationTransform(); +#endif } if (field_0x3c8[field_0x3f9] == OptYnSelEndFrameTbl[field_0x3f9]) { bVar2 = true; diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index 2b05a46fae..6adb3945af 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -30,8 +30,8 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" -#include "dusk/game_clock.h" +#include "dusk/interp/menus.h" +#include "dusk/interp/user_interface.h" #include "dusk/menu_pointer.h" #include "dusk/settings.h" #include "dusk/ui/touch_controls.hpp" @@ -101,7 +101,7 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i mCenterPosX = 0.0f; mCenterPosY = -FB_HEIGHT_BASE; } - if (mRingOrigin == 3) { + IF_DUSK(else) if (mRingOrigin == 3) { mCenterPosX = FB_WIDTH_BASE; mCenterPosY = 0.0f; } else if (mRingOrigin == 1) { @@ -190,23 +190,9 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i field_0x682 = 0xc000; break; } -#if TARGET_PC - mCursorInterpPrevX = 0.0f; - mCursorInterpPrevY = 0.0f; - mCursorInterpCurrX = 0.0f; - mCursorInterpCurrY = 0.0f; - mCursorInterpPrevAngle = 0; - mCursorInterpCurrAngle = 0; - mCursorInterpPrevAngular = false; - mCursorInterpCurrAngular = false; - mCursorInterpInit = false; - mPointerTouchPressHoveredCurrent = false; -#endif + IF_DUSK(mPointerTouchPressHoveredCurrent = false); for (int i = 0; i < 4; i++) { field_0x674[i] = 0; -#if TARGET_PC - mSelectItemSlideElapsed[i] = 0.0f; -#endif field_0x518[i] = 0.0f; field_0x528[i] = 0.0f; field_0x538[i] = 0.0f; @@ -567,6 +553,7 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i } dMenu_Ring_c::~dMenu_Ring_c() { + IF_DUSK(dusk::interp::erase_owned_samples(this)); mpHeap->getTotalFreeSize(); dMeter2Info_setItemExplainWindowStatus(0); for (int i = 0; i < 4; i++) { @@ -701,9 +688,32 @@ void dMenu_Ring_c::_move() { mRingCursorScale = g_ringHIO.mCursorScale; mpDrawCursor->setScale(g_ringHIO.mCursorScale); } + IF_DUSK(captureRenderState()); } +#if TARGET_PC +namespace { +struct RingSelectionSamples { + dusk::interp::Samples frames; +}; +} + +void dMenu_Ring_c::captureRenderState() { + dusk::interp::capture_menu_pose(this, {mCenterPosX, mCenterPosY, mAlphaRate}); + dusk::interp::get(this).frames.capture(4, [&](int i) { + return (f32)field_0x674[i]; + }); +} +#endif + void dMenu_Ring_c::_draw() { +#if TARGET_PC + const auto [mCenterPosX, mCenterPosY, mAlphaRate] = + dusk::interp::read_menu_pose(this, {this->mCenterPosX, this->mCenterPosY, this->mAlphaRate}); + if (mDrawFlag == 0) { + dusk::vdt::advance_looping_frame(field_0x684, 1.0f, g_ringHIO.mItemAlphaFlashDuration); + } +#endif J2DGrafContext* grafPort = dComIfGp_getCurrentGrafPort(); grafPort->setup2D(); if (mDrawFlag == 0) { @@ -756,71 +766,7 @@ void dMenu_Ring_c::_draw() { } else { drawSelectItem(); drawItem2(); -#if TARGET_PC - f32 simX = 0.0f; - f32 simY = 0.0f; - bool restoreSimPos = false; - if (dusk::frame_interp::is_enabled() && mAlphaRate >= 1.0f) { - simX = mpDrawCursor->getPositionX(); - simY = mpDrawCursor->getPositionY(); - - const bool isAngular = (mStatus == STATUS_MOVE) && !mDirectSelectActive; - - if (dusk::frame_interp::get_ui_tick_pending()) { - mCursorInterpPrevX = mCursorInterpCurrX; - mCursorInterpPrevY = mCursorInterpCurrY; - mCursorInterpPrevAngle = mCursorInterpCurrAngle; - mCursorInterpPrevAngular = mCursorInterpCurrAngular; - - mCursorInterpCurrX = simX; - mCursorInterpCurrY = simY; - mCursorInterpCurrAngle = field_0x66e; - mCursorInterpCurrAngular = isAngular; - - // reset prev = curr for first render pass or - // when angle modes prev/curr differ - // to prevent arrival jitter - if (!mCursorInterpInit || - mCursorInterpPrevAngular != mCursorInterpCurrAngular) { - mCursorInterpPrevX = mCursorInterpCurrX; - mCursorInterpPrevY = mCursorInterpCurrY; - mCursorInterpPrevAngle = mCursorInterpCurrAngle; - mCursorInterpPrevAngular = mCursorInterpCurrAngular; - mCursorInterpInit = true; - } - } - if (mCursorInterpInit) { - const f32 step = dusk::frame_interp::get_interpolation_step(); - if (mCursorInterpPrevAngular && mCursorInterpCurrAngular) { - const s16 delta = mCursorInterpCurrAngle - mCursorInterpPrevAngle; - const s16 lerpedAngle = mCursorInterpPrevAngle + (s16)(delta * step); - - // yoinked from stick_move_proc() - const f32 x = g_ringHIO.mItemRingPosX + FB_WIDTH_BASE / 2 + - mRingRadiusH * cM_ssin(lerpedAngle); - const f32 y = g_ringHIO.mItemRingPosY + FB_HEIGHT_BASE / 2 + - mRingRadiusV * cM_scos(lerpedAngle); - mpDrawCursor->setPos(x, y); - } else { - mpDrawCursor->setPos( - mCursorInterpPrevX + (mCursorInterpCurrX - mCursorInterpPrevX) * step, - mCursorInterpPrevY + (mCursorInterpCurrY - mCursorInterpPrevY) * step - ); - } - restoreSimPos = true; - } - } else { - mCursorInterpInit = false; - } -#endif mpDrawCursor->draw(); -#if TARGET_PC - // prevents offsetting at destination on the next frame - // since stick_wait_proc doesn't call setPos and we clobbered mPositionX/Y - if (restoreSimPos) { - mpDrawCursor->setPos(simX, simY); - } -#endif mpItemExplain->trans(mCenterPosX, mCenterPosY); mpItemExplain->draw((J2DOrthoGraph*)grafPort); drawFlag0(); @@ -851,7 +797,7 @@ bool dMenu_Ring_c::isOpen() { mCenterPosX = (1.0f - mAlphaRate) * -FB_WIDTH_BASE; mCenterPosY = 0.0f; } - if (mOpenCloseFrames >= g_ringHIO.mOpenFrames) { + if (mOpenCloseFrames >= g_ringHIO.mOpenFrames) { // Opening is finished, set to g_ringHIO.mCloseFrames as a // preparation for when the player closes the item wheel mOpenCloseFrames = g_ringHIO.mCloseFrames; @@ -868,6 +814,7 @@ bool dMenu_Ring_c::isOpen() { mpDrawCursor->setParam(1.0f, 1.0f, 0.1f, 0.6f, 0.5f); } + IF_DUSK(captureRenderState()); return opened; } @@ -931,6 +878,7 @@ bool dMenu_Ring_c::isClose() { mpDrawCursor->setParam(1.0f, 1.0f, 0.1f, 0.6f, 0.5f); } + IF_DUSK(captureRenderState()); return closed; } @@ -1223,9 +1171,6 @@ void dMenu_Ring_c::setJumpItem(bool i_useVibrationM) { field_0x6b8[0] != dComIfGs_getMixItemIndex(0)) { field_0x674[0] = 1; -#if TARGET_PC - mSelectItemSlideElapsed[0] = 0.0f; -#endif } } else if (field_0x6b3 == 1) { field_0x538[0] = g_ringHIO.mUnselectItemScale; @@ -1234,9 +1179,6 @@ void dMenu_Ring_c::setJumpItem(bool i_useVibrationM) { field_0x6b8[1] != dComIfGs_getMixItemIndex(1)) { field_0x674[1] = 1; -#if TARGET_PC - mSelectItemSlideElapsed[1] = 0.0f; -#endif } } if (field_0x674[0] == 1) { @@ -1460,10 +1402,15 @@ void dMenu_Ring_c::setMixItem() { } void dMenu_Ring_c::drawItem() { +#if TARGET_PC + const auto [mCenterPosX, mCenterPosY, mAlphaRate] = + dusk::interp::read_menu_pose(this, {this->mCenterPosX, this->mCenterPosY, this->mAlphaRate}); +#else field_0x684++; if (field_0x684 >= g_ringHIO.mItemAlphaFlashDuration) { field_0x684 = 0; } +#endif s32 halfFlashDuration = g_ringHIO.mItemAlphaFlashDuration / 2; f32 fVar16; if (field_0x684 < halfFlashDuration) { @@ -1517,6 +1464,10 @@ void dMenu_Ring_c::drawItem() { } void dMenu_Ring_c::drawItem2() { +#if TARGET_PC + const auto [mCenterPosX, mCenterPosY, mAlphaRate] = + dusk::interp::read_menu_pose(this, {this->mCenterPosX, this->mCenterPosY, this->mAlphaRate}); +#endif s32 idx = mCurrentSlot; if (mStatus == STATUS_WAIT || mStatus == STATUS_EXPLAIN || mStatus == STATUS_EXPLAIN_FORCE) { J2DDrawFrame(mItemSlotPosX[idx] - 24.0f + mCenterPosX, mItemSlotPosY[idx] - 24.0f + mCenterPosY, @@ -1804,18 +1755,25 @@ void dMenu_Ring_c::setSelectItem(int i_idx, u8 i_itemNo) { mpSelectItemTexBuf[i_idx][field_0x6be[i_idx]][0]->height / 48.0f * texScale; } +#if TARGET_PC +void dMenu_Ring_c::advanceSelectItem() { + mpDrawCursor->setPos(mpDrawCursor->getPositionX(), mpDrawCursor->getPositionY()); + for (int i = 0; i < 4; ++i) { + if (field_0x674[i] >= 10) { + setSelectItemForce(i); + } else if (field_0x674[i] != 0) { + ++field_0x674[i]; + } + } + captureRenderState(); +} +#endif + void dMenu_Ring_c::drawSelectItem() { + IF_DUSK(const auto mAlphaRate = dusk::interp::read_menu_pose(this, {mCenterPosX, mCenterPosY, this->mAlphaRate}).alpha); for (int i = 0; i < 4; i++) { if (field_0x674[i] != 0) { -#if TARGET_PC - mSelectItemSlideElapsed[i] += dusk::game_clock::consume_interval(this); - const f32 u = std::min(mSelectItemSlideElapsed[i] / (dusk::game_clock::kSimPeriod * 10.0f), 1.0f); - if (u >= 1.0f) { - setSelectItemForce(i); - } else { -#else - if (field_0x674[i] < 10) { -#endif + if (DUSK_IF_ELSE(true, field_0x674[i] < 10)) { #if TARGET_PC f32 initSizeX; f32 initSizeY; @@ -1852,7 +1810,9 @@ void dMenu_Ring_c::drawSelectItem() { #endif #if TARGET_PC - f32 fVar14 = 0.1f + 0.8f * u; + const auto& samples = dusk::interp::get(this).frames; + const f32 frame = field_0x674[i] == 1 ? 1.0f : samples.read(i, (f32)field_0x674[i]); + f32 fVar14 = std::clamp(frame, 1.0f, 9.0f) / 10.0f; #else f32 fVar14 = field_0x674[i] / 10.0f; #endif @@ -1891,9 +1851,6 @@ void dMenu_Ring_c::setSelectItemForce(int i_idx) { if (field_0x674[i_idx] != 0) { dComIfGs_setSelectItemIndex(i_idx, field_0x6b4[i_idx]); field_0x674[i_idx] = 0; -#if TARGET_PC - mSelectItemSlideElapsed[i_idx] = 0.0f; -#endif } } else if (field_0x674[i_idx] != 0) { for (int i = 0; i < 2; i++) { @@ -1901,9 +1858,6 @@ void dMenu_Ring_c::setSelectItemForce(int i_idx) { dComIfGs_setSelectItemIndex(i, field_0x6b4[i]); } field_0x674[i_idx] = 0; -#if TARGET_PC - mSelectItemSlideElapsed[i_idx] = 0.0f; -#endif } } @@ -2116,6 +2070,7 @@ void dMenu_Ring_c::setCombineBomb(int param_0) { } void dMenu_Ring_c::drawNumber(int i_itemNum, int i_itemMaxNum, f32 i_posX, f32 i_posY) { + IF_DUSK(const auto mAlphaRate = dusk::interp::read_menu_pose(this, {mCenterPosX, mCenterPosY, this->mAlphaRate}).alpha); if (i_itemNum > 100) { i_itemNum = 100; } diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index aaa5fe622b..1fec2ec552 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -22,7 +22,8 @@ #include "m_Do/m_Do_graphic.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/user_interface.h" #include "dusk/menu_pointer.h" #include "dusk/mods/svc/save.hpp" #include "dusk/settings.h" @@ -102,6 +103,8 @@ dMenu_save_c::dMenu_save_c() { } mpSelectMoveBase = NULL; + IF_DUSK(mYesNoSelAnmCol = 0); + for (int i = 0; i < 2; i++) { mpNoYes[i] = NULL; mNoYesBase[i] = NULL; @@ -809,9 +812,7 @@ void dMenu_save_c::_move() { } (this->*MenuSaveProc[mMenuProc])(); -#if !TARGET_PC - saveSelAnm(); -#endif + IF_NOT_DUSK(saveSelAnm()); if (mWarning != NULL) { mWarning->_move(); @@ -829,46 +830,47 @@ void dMenu_save_c::saveSelAnm() { void dMenu_save_c::selFileWakuAnm() { #if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - mFileWakuAnmFrame += 2; - if (mFileWakuAnmFrame >= mpFileWakuAnm->getFrameMax()) { - mFileWakuAnmFrame -= mpFileWakuAnm->getFrameMax(); - } + dusk::vdt::present_looping(mFileWakuAnmFrame, mpFileWakuAnm, 2.0f); + dusk::vdt::present_looping(mFileWakuRotAnmFrame, mpFileWakuRotAnm, 2.0f); +#else + mFileWakuAnmFrame += 2; + if (mFileWakuAnmFrame >= mpFileWakuAnm->getFrameMax()) { + mFileWakuAnmFrame -= mpFileWakuAnm->getFrameMax(); + } - mFileWakuRotAnmFrame += 2; - if (mFileWakuRotAnmFrame >= mpFileWakuRotAnm->getFrameMax()) { - mFileWakuRotAnmFrame -= mpFileWakuRotAnm->getFrameMax(); - } + mFileWakuRotAnmFrame += 2; + if (mFileWakuRotAnmFrame >= mpFileWakuRotAnm->getFrameMax()) { + mFileWakuRotAnmFrame -= mpFileWakuRotAnm->getFrameMax(); } mpFileWakuAnm->setFrame(mFileWakuAnmFrame); mpFileWakuRotAnm->setFrame(mFileWakuRotAnmFrame); +#endif } void dMenu_save_c::bookIconAnm() { #if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x154 += 2; - if (field_0x154 >= field_0x150->getFrameMax()) { - field_0x154 -= field_0x150->getFrameMax(); - } + dusk::vdt::present_looping(field_0x154, field_0x150, 2.0f); + dusk::vdt::present_looping(field_0x15c, field_0x158, 2.0f); + dusk::vdt::present_looping(field_0x164, field_0x160, 2.0f); +#else + field_0x154 += 2; + if (field_0x154 >= field_0x150->getFrameMax()) { + field_0x154 -= field_0x150->getFrameMax(); + } - field_0x15c += 2; - if (field_0x15c >= field_0x158->getFrameMax()) { - field_0x15c -= field_0x158->getFrameMax(); - } + field_0x15c += 2; + if (field_0x15c >= field_0x158->getFrameMax()) { + field_0x15c -= field_0x158->getFrameMax(); + } - field_0x164 += 2; - if (field_0x164 >= field_0x160->getFrameMax()) { - field_0x164 -= field_0x160->getFrameMax(); - } + field_0x164 += 2; + if (field_0x164 >= field_0x160->getFrameMax()) { + field_0x164 -= field_0x160->getFrameMax(); } field_0x150->setFrame(field_0x154); field_0x158->setFrame(field_0x15c); field_0x160->setFrame(field_0x164); +#endif } void dMenu_save_c::memCardWatch() { @@ -1842,6 +1844,7 @@ void dMenu_save_c::openSaveSelect3() { var_r29 = ketteiTxtDispAnm(); if (field_0x74[mSelectedFile] != SelEndFrameTbl[mSelectedFile]) { +#if !TARGET_PC field_0x74[mSelectedFile] += 2; if (field_0x74[mSelectedFile] > SelEndFrameTbl[mSelectedFile]) { @@ -1850,6 +1853,7 @@ void dMenu_save_c::openSaveSelect3() { field_0x40->setFrame(field_0x74[mSelectedFile]); mpSelData[mSelectedFile]->getPanePtr()->animationTransform(); +#endif var_r28 = false; } } @@ -2057,6 +2061,7 @@ void dMenu_save_c::saveSelectMoveAnime() { selWakuAnmComplete = selectWakuAlpahAnm(mLastSelFile); if (field_0x74[mLastSelFile] != SelStartFrameTbl[mLastSelFile]) { +#if !TARGET_PC field_0x74[mLastSelFile] -= 2; if (field_0x74[mLastSelFile] < SelStartFrameTbl[mLastSelFile]) { @@ -2065,6 +2070,7 @@ void dMenu_save_c::saveSelectMoveAnime() { field_0x44->setFrame(field_0x74[mLastSelFile]); mpSelData[mLastSelFile]->getPanePtr()->animationTransform(); +#endif var_r29 = false; } } @@ -2075,6 +2081,7 @@ void dMenu_save_c::saveSelectMoveAnime() { var_r28 = mpBookWaku[mSelectedFile]->alphaAnime(g_msHIO.mEffectDispFrames, 0, 0xFF, 1); if (field_0x74[mSelectedFile] != SelEndFrameTbl[mSelectedFile]) { +#if !TARGET_PC field_0x74[mSelectedFile] += 2; if (field_0x74[mSelectedFile] > SelEndFrameTbl[mSelectedFile]) { @@ -2083,6 +2090,7 @@ void dMenu_save_c::saveSelectMoveAnime() { field_0x40->setFrame(field_0x74[mSelectedFile]); mpSelData[mSelectedFile]->getPanePtr()->animationTransform(); +#endif var_r27 = false; } } @@ -2197,6 +2205,8 @@ void dMenu_save_c::saveMoveDisp2() { } void dMenu_save_c::yesnoSelectAnmSet(u8 param_0) { + IF_DUSK(mYesNoSelAnmCol = param_0); + if (mYesNoPrevCursor != 0xFF) { yesnoWakuAlpahAnmInit(mYesNoPrevCursor, 255, 0, g_msHIO.mSelectFrames); mpNoYes[mYesNoPrevCursor]->getPanePtr()->setAnimation(field_0x40); @@ -2534,6 +2544,7 @@ void dMenu_save_c::selectDataBaseMoveAnmInitSet(int param_0, int param_1) { bool dMenu_save_c::selectDataBaseMoveAnm() { if (mDataBaseMoveAnmFrame != mDataBaseMoveFrameMax) { +#if !TARGET_PC if (mDataBaseMoveAnmFrame < mDataBaseMoveFrameMax) { mDataBaseMoveAnmFrame += 2; @@ -2550,6 +2561,7 @@ bool dMenu_save_c::selectDataBaseMoveAnm() { field_0x40->setFrame(mDataBaseMoveAnmFrame); mpSelectMoveBase->getPanePtr()->animationTransform(); +#endif return false; } else { #if TARGET_PC @@ -2613,6 +2625,7 @@ void dMenu_save_c::selectDataMoveAnmInitSet(int param_0, int param_1) { bool dMenu_save_c::selectDataMoveAnm() { if (field_0x74[mSelectedFile] != field_0x80) { +#if !TARGET_PC if (field_0x74[mSelectedFile] < field_0x80) { field_0x74[mSelectedFile] += 2; @@ -2631,6 +2644,7 @@ bool dMenu_save_c::selectDataMoveAnm() { for (int i = 0; i < 3; i++) { mpSelData[i]->getPanePtr()->animationTransform(); } +#endif return false; } else { @@ -2668,6 +2682,7 @@ void dMenu_save_c::yesnoMenuMoveAnmInitSet(int anmFrame, int frameMax, u8 param_ bool dMenu_save_c::yesnoMenuMoveAnm() { if (mYesNoMoveAnmFrame != mYesNoMoveAnmMax) { +#if !TARGET_PC if (mYesNoMoveAnmFrame < mYesNoMoveAnmMax) { mYesNoMoveAnmFrame += 2; @@ -2685,6 +2700,7 @@ bool dMenu_save_c::yesnoMenuMoveAnm() { field_0x48->setFrame(mYesNoMoveAnmFrame); mpNoYes[0]->getPanePtr()->animationTransform(); mpNoYes[1]->getPanePtr()->animationTransform(); +#endif return false; } else { mpNoYes[0]->getPanePtr()->setAnimation((J2DAnmTransformKey*)NULL); @@ -2719,6 +2735,7 @@ bool dMenu_save_c::yesnoSelectMoveAnm(u8 param_0) { bool var_r30 = true; if (mYesNoPrevCursor != 0xFF) { if (field_0x8c[mYesNoPrevCursor] != YnSelStartFrameTbl[mYesNoPrevCursor][param_0]) { +#if !TARGET_PC if (field_0x8c[mYesNoPrevCursor] < YnSelStartFrameTbl[mYesNoPrevCursor][param_0]) { field_0x8c[mYesNoPrevCursor] += 2; @@ -2735,6 +2752,7 @@ bool dMenu_save_c::yesnoSelectMoveAnm(u8 param_0) { field_0x40->setFrame(field_0x8c[mYesNoPrevCursor]); mpNoYes[mYesNoPrevCursor]->getPanePtr()->animationTransform(); +#endif var_r30 = false; } } @@ -2742,6 +2760,7 @@ bool dMenu_save_c::yesnoSelectMoveAnm(u8 param_0) { bool var_r7 = true; if (mYesNoCursor != 0xFF) { if (field_0x8c[mYesNoCursor] != YnSelEndFrameTbl[mYesNoCursor][param_0]) { +#if !TARGET_PC if (field_0x8c[mYesNoCursor] < YnSelEndFrameTbl[mYesNoCursor][param_0]) { field_0x8c[mYesNoCursor] += 2; @@ -2758,6 +2777,7 @@ bool dMenu_save_c::yesnoSelectMoveAnm(u8 param_0) { field_0x44->setFrame(field_0x8c[mYesNoCursor]); mpNoYes[mYesNoCursor]->getPanePtr()->animationTransform(); +#endif var_r7 = false; } } @@ -2804,6 +2824,7 @@ void dMenu_save_c::errorMoveAnmInitSet(int param_0, int param_1) { bool dMenu_save_c::errorMoveAnm() { if (field_0xb8 != field_0xbc) { +#if !TARGET_PC if (field_0xb8 < field_0xbc) { field_0xb8 += 2; @@ -2820,6 +2841,7 @@ bool dMenu_save_c::errorMoveAnm() { field_0x4c->setFrame(field_0xb8); field_0xb4->animationTransform(); +#endif return 0; } else { field_0x4c->setFrame(field_0xbc); @@ -3078,22 +3100,85 @@ void dMenu_save_c::menuSaveWide() { } #endif } + +void dMenu_save_c::presentLayoutAnims() { + if (mWarning != NULL) { + mWarning->presentAnims(); + } + + dusk::vdt::present_toward(mDataBaseMoveAnmFrame, (f32)mDataBaseMoveFrameMax, field_0x40, mpSelectMoveBase != NULL ? mpSelectMoveBase->getPanePtr() : NULL); + + bool sharedSel = true; + for (int i = 0; i < 3; i++) { + J2DPane* pane = mpSelData[i] != NULL ? mpSelData[i]->getPanePtr() : NULL; + if (pane == NULL || pane->mTransform != field_0x40) { + sharedSel = false; + break; + } + } + if (sharedSel && mSelectedFile != 0xFF) { + dusk::vdt::present_toward(field_0x74[mSelectedFile], (f32)field_0x80, field_0x40); + for (int i = 0; i < 3; i++) { + mpSelData[i]->getPanePtr()->animationTransform(); + } + } else { + for (int i = 0; i < 3; i++) { + J2DPane* pane = mpSelData[i] != NULL ? mpSelData[i]->getPanePtr() : NULL; + dusk::vdt::present_selected(field_0x74[i], pane, field_0x40, (f32)SelEndFrameTbl[i], + field_0x44, (f32)SelStartFrameTbl[i]); + } + } + + if (mYesNoMoveAnmFrame != mYesNoMoveAnmMax) { + dusk::vdt::present_shared(mYesNoMoveAnmFrame, (f32)mYesNoMoveAnmMax, field_0x48, + {mpNoYes[0] != NULL ? mpNoYes[0]->getPanePtr() : NULL, + mpNoYes[1] != NULL ? mpNoYes[1]->getPanePtr() : NULL}); + } + + for (int i = 0; i < 2; i++) { + J2DPane* pane = mpNoYes[i] != NULL ? mpNoYes[i]->getPanePtr() : NULL; + dusk::vdt::present_selected(field_0x8c[i], pane, field_0x40, (f32)YnSelStartFrameTbl[i][mYesNoSelAnmCol], + field_0x44, (f32)YnSelEndFrameTbl[i][mYesNoSelAnmCol]); + } + + dusk::vdt::present_toward(field_0xb8, (f32)field_0xbc, field_0x4c, field_0xb4); + + for (int i = 0; i < 3; ++i) { + mpBookWaku[i]->presentAnime(); + mpSelWakuMoyo[i]->presentAnime(); + mpSelWakuGold[i]->presentAnime(); + mpSelWakuGold2[i]->presentAnime(); + } + for (int i = 0; i < 2; ++i) { + mNoYesBase[i]->presentAnime(); + mNoYesGold[i]->presentAnime(); + mNoYesGold2[i]->presentAnime(); + mpNoYesTxt[i]->presentAnime(); + mpErrTxtPane[i]->presentAlphaAnime(); + mpHeaderTxtPane[i]->presentAlphaAnime(); + } + mpBBtnIcon->presentAlphaAnime(); + mpABtnIcon->presentAlphaAnime(); + mpBackTxt->presentAlphaAnime(); + mpConfirmTxt->presentAlphaAnime(); +} #endif void dMenu_save_c::_draw2() { - if (field_0x21a1 == 0) { #if TARGET_PC + if (mDisplayMenu && dusk::game_clock::is_presentation_frame()) { saveSelAnm(); + presentLayoutAnims(); + menuSaveWide(); + } #endif + + if (field_0x21a1 == 0) { if (mpScrnExplain != NULL) { dComIfGd_set2DOpa(&mMenuSaveExplain); } if (mDisplayMenu) { - #if TARGET_PC - menuSaveWide(); - #endif - dComIfGd_set2DOpa(&mSaveSel); for (int i = 0; i < 3; i++) { diff --git a/src/d/d_menu_skill.cpp b/src/d/d_menu_skill.cpp index 90d4a3f92b..fddfa61b51 100644 --- a/src/d/d_menu_skill.cpp +++ b/src/d/d_menu_skill.cpp @@ -21,6 +21,7 @@ #include "dusk/version.hpp" #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/menu_pointer.h" #endif @@ -146,6 +147,7 @@ void dMenu_Skill_c::_move() { void dMenu_Skill_c::_draw() { if (mpArchive != NULL) { + IF_DUSK(presentAnims()); J2DGrafContext* context = dComIfGp_getCurrentGrafPort(); u8 alpha = mpBlackTex->mAlpha; mpBlackTex->setAlpha(0xff); @@ -362,7 +364,7 @@ void dMenu_Skill_c::read_open_init() { void dMenu_Skill_c::read_open_move() { s16 openSkillDescFrame = g_drawHIO.mSkillListScreen.mOpenFrame[dMeter_drawSkillHIO_c::SKILL_DESC]; - mProcFrame++; + IF_NOT_DUSK(mProcFrame++); if (mProcFrame >= openSkillDescFrame) { mProcess = PROC_OPEN_MOVE; mpTextParent->setAlphaRate(1.0f); @@ -402,7 +404,7 @@ void dMenu_Skill_c::read_close_init() { void dMenu_Skill_c::read_close_move() { s16 closeSkillDescFrame = g_drawHIO.mSkillListScreen.mCloseFrame[dMeter_drawSkillHIO_c::SKILL_DESC]; - mProcFrame--; + IF_NOT_DUSK(mProcFrame--); if (mProcFrame <= 0) { mProcess = PROC_CLOSE_MOVE; mpTextParent->setAlphaRate(0.0f); @@ -414,6 +416,26 @@ void dMenu_Skill_c::read_close_move() { } } +#if TARGET_PC +void dMenu_Skill_c::presentAnims() { + const auto& hio = g_drawHIO.mSkillListScreen; + f32 duration; + if (mProcess == PROC_WAIT_MOVE) { + duration = hio.mOpenFrame[dMeter_drawSkillHIO_c::SKILL_DESC]; + dusk::vdt::advance_toward_frame(mProcFrame, duration, 1.0f); + } else if (mProcess == PROC_MOVE_MOVE) { + duration = hio.mCloseFrame[dMeter_drawSkillHIO_c::SKILL_DESC]; + dusk::vdt::advance_toward_frame(mProcFrame, 0.0f, 1.0f); + } else { + return; + } + const f32 alpha = duration <= 0.0f ? (mProcess == PROC_WAIT_MOVE ? 1.0f : 0.0f) : + dusk::vdt::clamped_fraction(mProcFrame, duration); + mpTextParent->setAlphaRate(alpha); + mpBlackTex->setAlpha(hio.mWindowBGalpha * alpha); +} +#endif + void dMenu_Skill_c::screenSetMenu() { static const u64 tag_sub0[7] = { MULTI_CHAR('menu_t0s'), MULTI_CHAR('menu_t1s'), MULTI_CHAR('menu_t2s'), MULTI_CHAR('menu_t3s'), MULTI_CHAR('menu_t4s'), MULTI_CHAR('menu_t5s'), MULTI_CHAR('menu_t51'), diff --git a/src/d/d_menu_window.cpp b/src/d/d_menu_window.cpp index 0e05e8ac4e..aee078fb1c 100644 --- a/src/d/d_menu_window.cpp +++ b/src/d/d_menu_window.cpp @@ -27,7 +27,7 @@ #include "m_Do/m_Do_controller_pad.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif class dDlst_MENU_CAPTURE_c : public dDlst_base_c { @@ -126,7 +126,7 @@ public: void setCaptureFlag() { mFlag = 1; #ifdef TARGET_PC - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); #endif } @@ -1589,10 +1589,18 @@ int dMw_c::_create() { field_0x144 = 3; dMeter2Info_setWindowStatus(0); + + IF_DUSK(base.draw_interp_frame = true); + return cPhs_COMPLEATE_e; } int dMw_c::_execute() { +#if TARGET_PC + if (mpMenuRing != NULL) { + mpMenuRing->advanceSelectItem(); + } +#endif if (field_0x151 != 0) { field_0x151--; } diff --git a/src/d/d_meter2.cpp b/src/d/d_meter2.cpp index a90124ebb6..80dd19c7b1 100644 --- a/src/d/d_meter2.cpp +++ b/src/d/d_meter2.cpp @@ -36,9 +36,12 @@ f32 dGetUserHudScale() { } } // namespace + +#include "d_meter2_vdt.inc" #endif int dMeter2_c::_create() { + IF_DUSK(mPresentationTargets = {}); stage_stag_info_class* stag_info = dComIfGp_getStageStagInfo(); if (dStage_stagInfo_GetUpButton(stag_info) == 1) { mpHeap = fopMsgM_createExpHeap(HEAP_SIZE(0x5A400, 0xA0000), NULL); @@ -256,10 +259,19 @@ int dMeter2_c::_create() { mpHeap->getTotalFreeSize(); field_0x11c = NULL; mDoExt_setCurrentHeap(heap); + + IF_DUSK(base.draw_interp_frame = true); + return cPhs_COMPLEATE_e; } int dMeter2_c::_execute() { +#if TARGET_PC + if (mpMeterDraw != NULL) { + mpMeterDraw->beginExecution(); + } +#endif + JKRHeap* heap = mDoExt_setCurrentHeap(mpHeap); if (!dComIfGs_isCollectMirror(0) @@ -321,6 +333,13 @@ int dMeter2_c::_execute() { dComIfGp_setBottleStatus(0, 0); dComIfGp_setCStickStatus(0, 0, 0); +#if TARGET_PC + if (mpMeterDraw != NULL) { + mPresentationTargets.ready = true; + mpMeterDraw->endExecution(); + } +#endif + mDoExt_setCurrentHeap(heap); return 1; } @@ -820,11 +839,12 @@ void dMeter2_c::moveKantera() { } if (draw_kantera == true) { - mpMeterDraw->drawKantera(mMaxOil, mNowOil, x_pos, y_pos); + IF_NOT_DUSK(mpMeterDraw->drawKantera(mMaxOil, mNowOil, x_pos, y_pos)); } alphaAnimeKantera(); dComIfGp_setItemNowOil(mNowOil); + IF_DUSK(mpMeterDraw->publishKantera(mNowOil, mMaxOil)); } void dMeter2_c::moveOxygen() { @@ -941,11 +961,12 @@ void dMeter2_c::moveOxygen() { } if (draw_oxygen == true) { - mpMeterDraw->drawOxygen(mMaxOxygen, mNowOxygen, x_pos, y_pos); + IF_NOT_DUSK(mpMeterDraw->drawOxygen(mMaxOxygen, mNowOxygen, x_pos, y_pos)); } alphaAnimeOxygen(); dComIfGp_setNowOxygen(mNowOxygen); + IF_DUSK(mpMeterDraw->publishOxygen(mNowOxygen, mMaxOxygen)); } void dMeter2_c::moveLightDrop() { @@ -1002,6 +1023,12 @@ void dMeter2_c::moveLightDrop() { alpha = g_drawHIO.mLightDrop.mVesselAlpha[0]; } +#if TARGET_PC + mPresentationTargets.vesselX = pos_x; + mPresentationTargets.vesselY = pos_y; + mPresentationTargets.vesselScale = scale; + mPresentationTargets.vesselAlpha = alpha; +#else if (mVesselPosX != pos_x) { cLib_addCalc2(&mVesselPosX, pos_x, 1.0f, 10.0f); draw_lightdrop = true; @@ -1033,6 +1060,7 @@ void dMeter2_c::moveLightDrop() { mVesselAlpha = alpha; } } +#endif if (draw_lightdrop == true) { mpMeterDraw->drawLightDrop(mLightDropNum, mNeedLightDropNum, mVesselPosX, mVesselPosY, @@ -1384,6 +1412,12 @@ void dMeter2_c::moveButtonA() { var_f31 = 1.0f; } +#if TARGET_PC + for (int i = 0; i < 2; i++) { + mPresentationTargets.buttonAX[i] = pos_x[i]; + mPresentationTargets.buttonAY[i] = pos_y[i]; + } +#else for (int i = 0; i < 2; i++) { if (mButtonATalkPosX[i] != pos_x[i]) { cLib_addCalc2(&mButtonATalkPosX[i], pos_x[i], 1.0f, 10.0f); @@ -1409,6 +1443,7 @@ void dMeter2_c::moveButtonA() { field_0x144 = var_f31; } } +#endif if (field_0x200 != dMsgObject_isTalkNowCheck()) { field_0x200 = dMsgObject_isTalkNowCheck(); @@ -1589,6 +1624,12 @@ void dMeter2_c::moveButtonB() { draw_buttonB = true; } +#if TARGET_PC + for (int i = 0; i < 2; i++) { + mPresentationTargets.buttonBX[i] = pos_x[i]; + mPresentationTargets.buttonBY[i] = pos_y[i]; + } +#else for (int i = 0; i < 2; i++) { if (field_0x148[i] != pos_x[i]) { cLib_addCalc2(&field_0x148[i], pos_x[i], 1.0f, 10.0f); @@ -1614,6 +1655,7 @@ void dMeter2_c::moveButtonB() { field_0x158 = var_f31; } } +#endif if (g_drawHIO.mItemScaleAdjustON && field_0x4bc != g_drawHIO.mItemScalePercent) { field_0x4bc = g_drawHIO.mItemScalePercent; @@ -2199,6 +2241,10 @@ void dMeter2_c::moveButtonCross() { temp_f30 = mButtonCrossOFFPosX + (((f32)field_0x1b4 / (f32)g_drawHIO.mButtonCrossMoveFrame) * (mButtonCrossONPosX - mButtonCrossOFFPosX)); +#if TARGET_PC + mPresentationTargets.crossX = temp_f30; + mPresentationTargets.crossY = var_f31; +#else if (field_0x15c != var_f31) { cLib_addCalc2(&field_0x15c, var_f31, 0.5f, 50.0f); if ((f32)fabs(field_0x15c - var_f31) < 0.5f) { @@ -2206,6 +2252,7 @@ void dMeter2_c::moveButtonCross() { } draw_cross = true; } +#endif if (draw_cross == true) { mpMeterDraw->drawButtonCross(temp_f30, field_0x15c); @@ -2850,7 +2897,15 @@ void dMeter2_c::alphaAnimeLightDrop() { if (!isShowLightDrop()) { mpMeterDraw->setAlphaLightDropAnimeMin(); } else { +#if TARGET_PC + const bool dropGetPath = + dMeter2Info_getLightDropGetFlag(dComIfGp_getStartStageDarkArea()) > 1 && + dMeter2Info_getLightDropGetFlag(dComIfGp_getStartStageDarkArea()) != 0xFF; + + mpMeterDraw->showLightDrop(true, dropGetPath); +#else mpMeterDraw->setAlphaLightDropAnimeMax(); +#endif } mpMeterDraw->setAlphaLightDropChange(false); @@ -2994,36 +3049,34 @@ void dMeter2_c::alphaAnimeButtonCross() { { mpMeterDraw->setAlphaButtonCrossAnimeMin(); - if ((!dComIfGp_event_chkEventFlag(0x40) || dMeter2Info_isGameStatus(2) || - (mStatus & 0x100)) && - field_0x190 > 0) - { - field_0x190--; + if ((!dComIfGp_event_chkEventFlag(0x40) || dMeter2Info_isGameStatus(2) || (mStatus & 0x100)) IF_NOT_DUSK(&& field_0x190 > 0)) { + DUSK_IF_ELSE(mpMeterDraw->showMap(false), field_0x190--); } } else if (dMeter2Info_isSub2DStatus(1) || dMeter2Info_isFloatingMessageVisible()) { mpMeterDraw->setAlphaButtonCrossAnimeMin(); +#if TARGET_PC + mpMeterDraw->showMap(true); +#else if (field_0x190 < 5) { field_0x190++; } +#endif } else { #if TARGET_PC - if (dusk::getSettings().game.enableTouchControls) { - mpMeterDraw->setAlphaButtonCrossAnimeMin(); - } else { - mpMeterDraw->setAlphaButtonCrossAnimeMax(); - } + mpMeterDraw->showCross(!dusk::getSettings().game.enableTouchControls); + mpMeterDraw->showMap(true); #else mpMeterDraw->setAlphaButtonCrossAnimeMax(); -#endif if (field_0x190 < 5) { field_0x190++; } +#endif } if (mpMap != NULL) { - mpMap->setMapAlpha((field_0x190 * 255.0f) / 5.0f); + IF_NOT_DUSK(mpMap->setMapAlpha((field_0x190 * 255.0f) / 5.0f)); } } diff --git a/src/d/d_meter2_draw.cpp b/src/d/d_meter2_draw.cpp index bcefa0a3dc..9771e030c4 100644 --- a/src/d/d_meter2_draw.cpp +++ b/src/d/d_meter2_draw.cpp @@ -23,14 +23,23 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/user_interface.h" #include "dusk/settings.h" #include "dusk/ui/icon_provider.hpp" #include "dusk/version.hpp" +#include "d/d_meter2.h" + #include namespace { +f32 s_button_a_scale = 1.0f; +f32 s_button_a_text_scale = 1.0f; +int s_button_b_kind; +f32 s_button_b_scale = 1.0f; +f32 s_button_b_text_scale = 1.0f; // Reads the user HUD scale setting, clamped to a safe range. f32 dGetUserHudScale() { @@ -56,12 +65,14 @@ void dAnchorHudScale(CPaneMgr* i_pane, HudCorner i_corner, f32* io_x, f32* io_y, *io_x += dirX * i_pane->getInitSizeX() * half * i_pull; *io_y += dirY * i_pane->getInitSizeY() * half; } - } // namespace + +#include "d_meter2_draw_vdt.inc" #endif dMeter2Draw_c::dMeter2Draw_c(JKRExpHeap* mp_heap) { #if TARGET_PC + setExecuting(false); // correct hio data here because we can't do runtime disc checks in sinit data constructors if (dusk::version::isRegionJpn()) { g_drawHIO.mButtonATextSpacing = -2.0f; @@ -607,6 +618,7 @@ void dMeter2Draw_c::init() { initButton(); initButtonCross(); field_0x772 = 0; + IF_DUSK(resetRows()); } void dMeter2Draw_c::exec(u32 i_status) { @@ -691,6 +703,15 @@ void dMeter2Draw_c::exec(u32 i_status) { } void dMeter2Draw_c::draw() { +#if TARGET_PC + if (dusk::game_clock::is_presentation_frame()) { + advance_fade_rows(); + if (dMeter2_c* meter = dMeter2Info_getMeterClass()) { + meter->presentAnims(); + } + } +#endif + J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort(); graf_ctx->setup2D(); @@ -707,16 +728,12 @@ void dMeter2Draw_c::draw() { drawKanteraScreen(1); drawKanteraScreen(2); -#if TARGET_PC - if (!touchControlsEnabled) { -#endif + IF_DUSK_BLOCK(!touchControlsEnabled) for (int i = 0; i < 2; i++) { if (mpItemXY[i] != NULL) { for (int j = 0; j < 3; j++) { f32 temp_f30 = mItemParams[i].num_scale * 16.0f; -#if TARGET_PC - temp_f30 *= dGetUserHudScale(); -#endif + IF_DUSK(temp_f30 *= dGetUserHudScale()); Vec vtx0 = mpItemXY[i]->getPanePtr()->getGlbVtx(0); Vec vtx3 = mpItemXY[i]->getPanePtr()->getGlbVtx(3); @@ -758,9 +775,7 @@ void dMeter2Draw_c::draw() { } } } -#if TARGET_PC - } -#endif + IF_DUSK_BLOCK_END if (mpLightDropParent->getAlphaRate() != 0.0f) { f32 var_f28 = g_drawHIO.mLightDrop.mPikariScaleNormal; @@ -769,40 +784,33 @@ void dMeter2Draw_c::draw() { if (field_0x756 >= 0) { var_f29 = g_drawHIO.mLightDrop.mDropPikariAnimSpeed_Completed; int temp_r5_2 = g_drawHIO.mLightDrop.mPikariInterval * 15; -#ifdef TARGET_PC - // FRAME INTERP NOTE: Set even if not advancing - var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (field_0x756 <= temp_r5_2) { - int temp_r4 = (field_0x756 % g_drawHIO.mLightDrop.mPikariInterval); - int temp_r3_5 = field_0x756 / g_drawHIO.mLightDrop.mPikariInterval; + if (field_0x756 <= temp_r5_2) { + int temp_r4 = (IF_DUSK((int))field_0x756 % g_drawHIO.mLightDrop.mPikariInterval); + int temp_r3_5 = IF_DUSK((int))field_0x756 / g_drawHIO.mLightDrop.mPikariInterval; - if (temp_r4 == 0 && field_0x62c[temp_r3_5] == 0.0f) { - field_0x62c[temp_r3_5] = 18.0f; + if (temp_r4 == 0 && field_0x62c[temp_r3_5] == 0.0f) { + field_0x62c[temp_r3_5] = 18.0f; + } + + var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; + DUSK_IF_ELSE(field_0x756 += dusk::game_clock::original_frames(), field_0x756++); + } else { + int temp_r5_3 = temp_r5_2 + 1; + + if (field_0x756 DUSK_IF_ELSE(< temp_r5_3 + 1, == temp_r5_3)) { + if (field_0x62c[15] == 0.0f) { + DUSK_IF_ELSE(field_0x756 += dusk::game_clock::original_frames(), field_0x756++); } - var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; - field_0x756++; - } else { - int temp_r5_3 = temp_r5_2 + 1; - - if (field_0x756 == temp_r5_3) { - if (field_0x62c[15] == 0.0f) { - field_0x756++; - } - var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; - } else if (field_0x756 >= g_drawHIO.mLightDrop.field_0x54 + temp_r5_3) { - for (int i = 0; i < 16; i++) { - field_0x62c[i] = 18.0f - var_f29; - field_0x66c[i] = 18.0f - g_drawHIO.mLightDrop.mPikariLoopAnimSpeed; - } - - field_0x756 = -1; - } else { - field_0x756++; + } else if (field_0x756 >= g_drawHIO.mLightDrop.field_0x54 + temp_r5_3) { + for (int i = 0; i < 16; i++) { + field_0x62c[i] = 18.0f - var_f29; + field_0x66c[i] = 18.0f - g_drawHIO.mLightDrop.mPikariLoopAnimSpeed; } + + field_0x756 = -1; + } else { + DUSK_IF_ELSE(field_0x756 += dusk::game_clock::original_frames(), field_0x756++); } } } @@ -988,8 +996,7 @@ void dMeter2Draw_c::initMagic() { g_drawHIO.mMagicMeterPosY + offsetY); setAlphaMagicChange(true); - drawKantera(dComIfGs_getMaxOil(), dComIfGs_getOil(), g_drawHIO.mLanternMeterPosX + offsetX, - g_drawHIO.mNoMagicPosY + (g_drawHIO.mLanternMeterPosY + offsetY)); + IF_NOT_DUSK(drawKantera(dComIfGs_getMaxOil(), dComIfGs_getOil(), g_drawHIO.mLanternMeterPosX + offsetX, g_drawHIO.mNoMagicPosY + (g_drawHIO.mLanternMeterPosY + offsetY))); setAlphaKanteraChange(true); } @@ -1469,7 +1476,7 @@ void dMeter2Draw_c::drawPikari(CPaneMgr* i_pane, f32* i_framep, f32 i_scale, void dMeter2Draw_c::drawPikari(f32 i_posX, f32 i_posY, f32* i_framep, f32 i_scale, JUtility::TColor i_moyaBlack, JUtility::TColor i_moyaWhite, JUtility::TColor i_moyabsBlack, JUtility::TColor i_moyabsWhite, - f32 param_8, u8 param_9) { + f32 param_8, u8 param_9 IF_DUSK_ARG(f32 elapsedFrames)) { f32 var_f31 = 28.0f; if (param_9 == 4) { @@ -1494,25 +1501,45 @@ void dMeter2Draw_c::drawPikari(f32 i_posX, f32 i_posY, f32* i_framep, f32 i_scal if (param_9 != 3 && param_9 != 4 && param_9 != 5 && dMsgObject_isTalkNowCheck()) { *i_framep = 0.0f; } else { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - *i_framep += param_8; - if (*i_framep > var_f31) { - if (param_9 == 1 || param_9 == 2 || param_9 == 3) { - *i_framep = 18.0f; - } else { - *i_framep = 0.0f; - } - } +#if TARGET_PC + const f32 previous = *i_framep; + const f32 frames = elapsedFrames >= 0.0f ? elapsedFrames : dusk::game_clock::original_frames(); + const f32 next = previous + param_8 * frames; + const bool looping = param_9 == 1 || param_9 == 2 || param_9 == 3; + *i_framep = next > var_f31 ? (looping ? 18.0f : 0.0f) : next; - if (*i_framep == 18.0f && param_9 == 1) { + if (previous <= 18.0f && next > 18.0f) { + switch (param_9) { + case 1: mDoAud_seStart(Z2SE_NAVI_BLINK, NULL, 0, 0); - } else if (*i_framep == 18.0f && param_9 == 2) { + break; + case 2: mDoAud_seStart(Z2SE_SY_ITEM_COMBINE_ICON, NULL, 0, 0); + break; + case 3: + if (i_framep == &field_0x738) { + mDoAud_seStart(Z2SE_SY_HINT_BUTTON_BLINK, NULL, 0, 0); + dMeter2Info_set2DVibration(); + } + break; } } +#else + *i_framep += param_8; + if (*i_framep > var_f31) { + if (param_9 == 1 || param_9 == 2 || param_9 == 3) { + *i_framep = 18.0f; + } else { + *i_framep = 0.0f; + } + } + + if (*i_framep == 18.0f && param_9 == 1) { + mDoAud_seStart(Z2SE_NAVI_BLINK, NULL, 0, 0); + } else if (*i_framep == 18.0f && param_9 == 2) { + mDoAud_seStart(Z2SE_SY_ITEM_COMBINE_ICON, NULL, 0, 0); + } +#endif playPikariBckAnimation(*i_framep); playPikariBpkAnimation(*i_framep); @@ -1658,6 +1685,7 @@ void dMeter2Draw_c::drawLife(s16 i_maxLife, s16 i_life, f32 i_posX, f32 i_posY) } void dMeter2Draw_c::setAlphaLifeChange(bool param_0) { + IF_DUSK_BLOCK(!isExecuting()) bool var_r31 = false; bool var_r30 = false; bool var_r29 = false; @@ -1703,20 +1731,29 @@ void dMeter2Draw_c::setAlphaLifeChange(bool param_0) { if (var_r31 || var_r28 || param_0) { mpBigHeart->setAlphaRate(mBigHeartAlpha * mLifeParentAlpha); } + IF_DUSK_BLOCK_END } void dMeter2Draw_c::setAlphaLifeAnimeMin() { +#if TARGET_PC + write_fade_row(this, kFadeRowLife, false); +#else if (mpLifeParent->getAlphaRate() != 0.0f) { mpLifeParent->setAlphaRate(g_drawHIO.mParentAlpha); setAlphaAnimeMin(mpLifeParent, 5); } +#endif } void dMeter2Draw_c::setAlphaLifeAnimeMax() { +#if TARGET_PC + write_fade_row(this, kFadeRowLife, true); +#else if (mpLifeParent->getAlphaRate() != g_drawHIO.mParentAlpha) { mpLifeParent->setAlphaRate(g_drawHIO.mParentAlpha); setAlphaAnimeMax(mpLifeParent, 5); } +#endif } void dMeter2Draw_c::drawKanteraScreen(u8 i_meterType) { @@ -1842,7 +1879,7 @@ static f32 dummyLiteralOrder() { return 5.0f; } -void dMeter2Draw_c::drawKantera(s32 i_max, s32 i_oil, f32 i_posX, f32 i_posY) { +void dMeter2Draw_c::drawKantera(s32 i_max, DUSK_IF_ELSE(f32, s32) i_oil, f32 i_posX, f32 i_posY) { f32 var_f6 = mpMagicFrameR->getInitPosX() - mpMagicFrameL->getInitPosX(); f32 var_f7 = 0.0f; f32 var_f4 = 0.0f; @@ -1865,6 +1902,7 @@ void dMeter2Draw_c::drawKantera(s32 i_max, s32 i_oil, f32 i_posX, f32 i_posY) { } void dMeter2Draw_c::setAlphaKanteraChange(bool i_forceSet) { + IF_DUSK_BLOCK(!isExecuting()) bool meter_parent_alpha_set = false; bool meter_alpha_set = false; bool meter_frame_alpha_set = false; @@ -1892,9 +1930,13 @@ void dMeter2Draw_c::setAlphaKanteraChange(bool i_forceSet) { mpMagicFrameL->setAlphaRate(mLanternMeterFrameAlpha * field_0x7b0); mpMagicFrameR->setAlphaRate(mLanternMeterFrameAlpha * field_0x7b0); } + IF_DUSK_BLOCK_END } void dMeter2Draw_c::setAlphaKanteraAnimeMin() { +#if TARGET_PC + write_fade_row(this, kFadeRowKantera, false); +#else if (field_0x742[1] <= 0) { mMeterAlphaRate[1] = 0.0f; } else { @@ -1905,9 +1947,13 @@ void dMeter2Draw_c::setAlphaKanteraAnimeMin() { mMeterAlphaRate[1] = (field_0x742[1] / 5.0f) * g_drawHIO.mParentAlpha; } +#endif } void dMeter2Draw_c::setAlphaKanteraAnimeMax() { +#if TARGET_PC + write_fade_row(this, kFadeRowKantera, true); +#else if (field_0x742[1] >= 5) { mMeterAlphaRate[1] = g_drawHIO.mParentAlpha; } else { @@ -1918,9 +1964,10 @@ void dMeter2Draw_c::setAlphaKanteraAnimeMax() { mMeterAlphaRate[1] = (field_0x742[1] / 5.0f) * g_drawHIO.mParentAlpha; } +#endif } -void dMeter2Draw_c::drawOxygen(s32 i_max, s32 i_oxygen, f32 i_posX, f32 i_posY) { +void dMeter2Draw_c::drawOxygen(s32 i_max, DUSK_IF_ELSE(f32, s32) i_oxygen, f32 i_posX, f32 i_posY) { f32 var_f6 = mpMagicFrameR->getInitPosX() - mpMagicFrameL->getInitPosX(); f32 var_f7 = 0.0f; f32 var_f4 = 0.0f; @@ -1943,6 +1990,7 @@ void dMeter2Draw_c::drawOxygen(s32 i_max, s32 i_oxygen, f32 i_posX, f32 i_posY) } void dMeter2Draw_c::setAlphaOxygenChange(bool i_forceSet) { + IF_DUSK_BLOCK(!isExecuting()) bool meter_parent_alpha_set = false; bool meter_alpha_set = false; bool meter_frame_alpha_set = false; @@ -1970,9 +2018,13 @@ void dMeter2Draw_c::setAlphaOxygenChange(bool i_forceSet) { mpMagicFrameL->setAlphaRate(mOxygenMeterFrameAlpha * field_0x7bc); mpMagicFrameR->setAlphaRate(mOxygenMeterFrameAlpha * field_0x7bc); } + IF_DUSK_BLOCK_END } void dMeter2Draw_c::setAlphaOxygenAnimeMin() { +#if TARGET_PC + write_fade_row(this, kFadeRowOxygen, false); +#else if (field_0x742[2] <= 0) { mMeterAlphaRate[2] = 0.0f; } else { @@ -1983,9 +2035,13 @@ void dMeter2Draw_c::setAlphaOxygenAnimeMin() { mMeterAlphaRate[2] = (field_0x742[2] / 5.0f) * g_drawHIO.mParentAlpha; } +#endif } void dMeter2Draw_c::setAlphaOxygenAnimeMax() { +#if TARGET_PC + write_fade_row(this, kFadeRowOxygen, true); +#else if (field_0x742[2] >= 5) { mMeterAlphaRate[2] = g_drawHIO.mParentAlpha; } else { @@ -1996,6 +2052,7 @@ void dMeter2Draw_c::setAlphaOxygenAnimeMax() { mMeterAlphaRate[2] = (field_0x742[2] / 5.0f) * g_drawHIO.mParentAlpha; } +#endif } void dMeter2Draw_c::drawLightDrop(u8 i_num, u8 i_needNum, f32 i_posX, f32 i_posY, f32 i_vesselScale, @@ -2044,23 +2101,14 @@ void dMeter2Draw_c::drawLightDrop(u8 i_num, u8 i_needNum, f32 i_posX, f32 i_posY } } + IF_DUSK_BLOCK(!isExecuting()) field_0x6fc = param_5; mLightDropVesselScale = i_vesselScale; -#if TARGET_PC - const f32 lightDropUserScale = dGetUserHudScale(); - const f32 lightDropScale = mLightDropVesselScale * field_0x6f8 * lightDropUserScale; - mpLightDropParent->scale(lightDropScale, lightDropScale); - - f32 lightDropPosX = i_posX; - f32 lightDropPosY = i_posY; - dAnchorHudScale(mpLightDropParent, HudCorner::TopRight, &lightDropPosX, &lightDropPosY); - mpLightDropParent->paneTrans(lightDropPosX, lightDropPosY); -#else - mpLightDropParent->scale(mLightDropVesselScale * field_0x6f8, - mLightDropVesselScale * field_0x6f8); - + mpLightDropParent->scale(mLightDropVesselScale * field_0x6f8 IF_DUSK(* dGetUserHudScale()), + mLightDropVesselScale * field_0x6f8 IF_DUSK(* dGetUserHudScale())); + IF_DUSK(dAnchorHudScale(mpLightDropParent, HudCorner::TopRight, &i_posX, &i_posY)); mpLightDropParent->paneTrans(i_posX, i_posY); -#endif + IF_DUSK_BLOCK_END } void dMeter2Draw_c::setAlphaLightDropChange(bool unused) {} @@ -2082,6 +2130,9 @@ f32 dMeter2Draw_c::getNowLightDropRateCalc() { } void dMeter2Draw_c::setAlphaLightDropAnimeMin() { +#if TARGET_PC + showLightDrop(false); +#else if (mpLightDropParent->getAlphaRate() != 0.0f) { mpLightDropParent->setAlphaRate(g_drawHIO.mParentAlpha * field_0x6fc); setAlphaAnimeMin(mpLightDropParent, 5); @@ -2101,14 +2152,20 @@ void dMeter2Draw_c::setAlphaLightDropAnimeMin() { } } } +#endif } void dMeter2Draw_c::setAlphaLightDropAnimeMax() { +#if TARGET_PC + const bool dropGetPath = dMeter2Info_getLightDropGetFlag(dComIfGp_getStartStageDarkArea()) > 1 && + dMeter2Info_getLightDropGetFlag(dComIfGp_getStartStageDarkArea()) != 0xFF; + + showLightDrop(true, dropGetPath); +#else f32 temp_f31 = g_drawHIO.mParentAlpha * field_0x6fc; if (dMeter2Info_getLightDropGetFlag(dComIfGp_getStartStageDarkArea()) > 1 && - dMeter2Info_getLightDropGetFlag(dComIfGp_getStartStageDarkArea()) != 0xFF) - { + dMeter2Info_getLightDropGetFlag(dComIfGp_getStartStageDarkArea()) != 0xFF) { if (mpLightDropParent->getAlphaTimer() == 0) { Z2GetAudioMgr()->seStart(Z2SE_SY_LIGHT_POT_EQUIP, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); @@ -2133,13 +2190,8 @@ void dMeter2Draw_c::setAlphaLightDropAnimeMax() { field_0x6f8 = 1.0f; } -#if TARGET_PC - const f32 dropAnimScale = mLightDropVesselScale * field_0x6f8 * dGetUserHudScale(); - mpLightDropParent->scale(dropAnimScale, dropAnimScale); -#else mpLightDropParent->scale(mLightDropVesselScale * field_0x6f8, - mLightDropVesselScale * field_0x6f8); -#endif + mLightDropVesselScale * field_0x6f8); if (g_drawHIO.mLightDrop.mDropGetScaleAnimFrameNum == mpLightDropParent->getAlphaTimer()) { dMeter2Info_setLightDropGetFlag(dComIfGp_getStartStageDarkArea(), 0xFF); @@ -2171,6 +2223,7 @@ void dMeter2Draw_c::setAlphaLightDropAnimeMax() { mpSIParts[i][2]->setAlphaRate(mpSIParent[1]->getAlphaRate() * 0.3f); } } +#endif } void dMeter2Draw_c::drawRupee(s16 i_rupeeNum) { @@ -2245,6 +2298,7 @@ void dMeter2Draw_c::drawRupee(s16 i_rupeeNum) { } void dMeter2Draw_c::setAlphaRupeeChange(bool param_0) { + IF_DUSK_BLOCK(!isExecuting()) bool set_parent = false; bool set_rupeekey = false; bool set_rupee = false; @@ -2297,9 +2351,13 @@ void dMeter2Draw_c::setAlphaRupeeChange(bool param_0) { } } } + IF_DUSK_BLOCK_END } void dMeter2Draw_c::setAlphaRupeeAnimeMin() { +#if TARGET_PC + write_fade_row(this, kFadeRowRupee, false); +#else f32 alphas[3]; alphas[0] = g_drawHIO.mRupeeAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mRupeeKeyAlpha); alphas[1] = g_drawHIO.mRupeeFrameAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mRupeeKeyAlpha); @@ -2311,9 +2369,13 @@ void dMeter2Draw_c::setAlphaRupeeAnimeMin() { setAlphaAnimeMin(mpRupeeParent[i], 5); } } +#endif } void dMeter2Draw_c::setAlphaRupeeAnimeMax() { +#if TARGET_PC + write_fade_row(this, kFadeRowRupee, true); +#else f32 alphas[3]; alphas[0] = g_drawHIO.mRupeeAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mRupeeKeyAlpha); alphas[1] = g_drawHIO.mRupeeFrameAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mRupeeKeyAlpha); @@ -2325,6 +2387,7 @@ void dMeter2Draw_c::setAlphaRupeeAnimeMax() { setAlphaAnimeMax(mpRupeeParent[i], 5); } } +#endif } void dMeter2Draw_c::drawKey(s16 i_keyNum) { @@ -2359,6 +2422,7 @@ void dMeter2Draw_c::drawKey(s16 i_keyNum) { } void dMeter2Draw_c::setAlphaKeyChange(bool param_0) { + IF_DUSK_BLOCK(!isExecuting()) bool set_parent = false; bool set_rupeekey = false; bool set_key = false; @@ -2393,24 +2457,33 @@ void dMeter2Draw_c::setAlphaKeyChange(bool param_0) { mpKeyTexture[i]->setAlphaRate(field_0x7e0 * (field_0x7e4 * (mKeyNumAlpha * mKeyAlpha))); } } + IF_DUSK_BLOCK_END } void dMeter2Draw_c::setAlphaKeyAnimeMin() { +#if TARGET_PC + write_fade_row(this, kFadeRowKey, false); +#else f32 alpha = g_drawHIO.mKeyAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mRupeeKeyAlpha); if (mpKeyParent->getAlphaRate() != 0.0f) { mpKeyParent->setAlphaRate(alpha); setAlphaAnimeMin(mpKeyParent, 5); } +#endif } void dMeter2Draw_c::setAlphaKeyAnimeMax() { +#if TARGET_PC + write_fade_row(this, kFadeRowKey, true); +#else f32 alpha = g_drawHIO.mKeyAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mRupeeKeyAlpha); if (mpKeyParent->getAlphaRate() != alpha) { mpKeyParent->setAlphaRate(alpha); setAlphaAnimeMax(mpKeyParent, 5); } +#endif } void dMeter2Draw_c::drawButtonA(u8 i_action, f32 i_posX, f32 i_posY, f32 i_textPosX, f32 i_textPosY, @@ -2470,11 +2543,16 @@ void dMeter2Draw_c::drawButtonA(u8 i_action, f32 i_posX, f32 i_posY, f32 i_textP } } +#if TARGET_PC + s_button_a_scale = var_f31; + s_button_a_text_scale = var_f30; +#else mpButtonA->scale(var_f31 * i_scale, var_f31 * i_scale); mpButtonA->paneTrans(i_posX, i_posY); mpTextA->scale(var_f30 * i_scale, var_f30 * i_scale); mpTextA->paneTrans(g_drawHIO.mButtonATextPosX + i_textPosX, g_drawHIO.mButtonATextPosY + i_textPosY IF_DUSK(+ (dusk::version::isLessThanWiiJpn() ? 6.0f : 0.0f))); +#endif } void dMeter2Draw_c::drawButtonB(u8 i_action, bool param_1, f32 i_posX, f32 i_posY, f32 i_textPosX, @@ -2561,9 +2639,15 @@ void dMeter2Draw_c::drawButtonB(u8 i_action, bool param_1, f32 i_posX, f32 i_pos mpScreen->search(MULTI_CHAR('item_b_n'))->hide(); } +#if TARGET_PC + s_button_b_kind = var_r31; + s_button_b_scale = var_f31; + s_button_b_text_scale = var_f30; +#endif mpItemB->getPanePtr()->rotate(mpItemB->getSizeX() * 0.5f, mpItemB->getSizeY() * 0.5f, ROTATE_Z, g_drawHIO.mButtonBItemRotation[var_r31]); +#if !TARGET_PC field_0x730 = var_f31 * i_scale; mpButtonB->scale(field_0x730 * field_0x734, field_0x730 * field_0x734); mpButtonB->paneTrans(g_drawHIO.mButtonBPosX + i_posX, g_drawHIO.mButtonBPosY + i_posY); @@ -2580,6 +2664,7 @@ void dMeter2Draw_c::drawButtonB(u8 i_action, bool param_1, f32 i_posX, f32 i_pos mpTextB->scale(var_f30 * i_scale, var_f30 * i_scale); mpTextB->paneTrans(g_drawHIO.mButtonBFontPosX + i_textPosX, g_drawHIO.mButtonBFontPosY + i_textPosY); +#endif } void dMeter2Draw_c::drawButtonR(u8 unused0, u8 i_action, bool unused1, bool unused2) { @@ -2831,27 +2916,20 @@ f32 dMeter2Draw_c::getButtonCrossParentInitTransY() { } void dMeter2Draw_c::drawButtonCross(f32 i_posX, f32 i_posY) { -#if TARGET_PC - const f32 buttonCrossUserScale = dGetUserHudScale(); - const f32 buttonCrossScale = g_drawHIO.mButtonCrossScale * buttonCrossUserScale; - mpButtonCrossParent->scale(buttonCrossScale, buttonCrossScale); -#else - mpButtonCrossParent->scale(g_drawHIO.mButtonCrossScale, g_drawHIO.mButtonCrossScale); -#endif + IF_DUSK_BLOCK(!isExecuting()) + mpButtonCrossParent->scale(g_drawHIO.mButtonCrossScale IF_DUSK(* dGetUserHudScale()), + g_drawHIO.mButtonCrossScale IF_DUSK(* dGetUserHudScale())); mpTextI->scale(g_drawHIO.mButtonCrossTextScale, g_drawHIO.mButtonCrossTextScale); mpTextM->scale(g_drawHIO.mButtonCrossTextScale, g_drawHIO.mButtonCrossTextScale); - -#if TARGET_PC - f32 buttonCrossPosX = i_posX; - f32 buttonCrossPosY = i_posY; - dAnchorHudScale(mpButtonCrossParent, HudCorner::BottomLeft, &buttonCrossPosX, &buttonCrossPosY); - mpButtonCrossParent->paneTrans(buttonCrossPosX, buttonCrossPosY); -#else + IF_DUSK(dAnchorHudScale(mpButtonCrossParent, HudCorner::BottomLeft, &i_posX, &i_posY)); mpButtonCrossParent->paneTrans(i_posX, i_posY); -#endif + IF_DUSK_BLOCK_END } void dMeter2Draw_c::setAlphaButtonCrossAnimeMin() { +#if TARGET_PC + showCross(false); +#else if (mpButtonCrossParent->getAlphaRate() != 0.0f) { mpButtonCrossParent->setAlphaRate(g_drawHIO.mParentAlpha); setAlphaAnimeMin(mpButtonCrossParent, 5); @@ -2859,6 +2937,7 @@ void dMeter2Draw_c::setAlphaButtonCrossAnimeMin() { setAlphaButtonCrossItemAnimeMin(); setAlphaButtonCrossMapAnimeMin(); +#endif } void dMeter2Draw_c::setAlphaButtonCrossAnimeMax() { @@ -2937,6 +3016,7 @@ void dMeter2Draw_c::setAlphaButtonCrossMapAnimeMax() { } void dMeter2Draw_c::setAlphaButtonChange(bool param_0) { + IF_DUSK_BLOCK(!isExecuting()) bool set_parent = false; bool set_buttonA = false; bool set_buttonB = false; @@ -3132,9 +3212,13 @@ void dMeter2Draw_c::setAlphaButtonChange(bool param_0) { static_cast(mpXYText[4][1]->getPanePtr()) ->setFontColor(g_drawHIO.mButtonXYTextColor, g_drawHIO.mButtonXYTextColor); } + IF_DUSK_BLOCK_END } void dMeter2Draw_c::setAlphaButtonAnimeMin() { +#if TARGET_PC + write_fade_row(this, kFadeRowButtonParent, false); +#else if (mpButtonParent->getAlphaRate() != 0.0f) { mpButtonParent->setAlphaRate(g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); setAlphaAnimeMin(mpButtonParent, 5); @@ -3143,9 +3227,13 @@ void dMeter2Draw_c::setAlphaButtonAnimeMin() { mpUzu->setAlphaRate(mButtonBaseAlpha * mpButtonParent->getAlphaRate()); } } +#endif } void dMeter2Draw_c::setAlphaButtonAnimeMax() { +#if TARGET_PC + write_fade_row(this, kFadeRowButtonParent, true); +#else if (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha != mpButtonParent->getAlphaRate()) { mpButtonParent->setAlphaRate(g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); setAlphaAnimeMax(mpButtonParent, 5); @@ -3160,9 +3248,13 @@ void dMeter2Draw_c::setAlphaButtonAnimeMax() { dMeter2Info_setLightDropGetFlag(dComIfGp_getStartStageDarkArea(), 2); } } +#endif } void dMeter2Draw_c::setAlphaButtonAAnimeMin() { +#if TARGET_PC + write_fade_row(this, kFadeRowButtonA, false); +#else if (mpButtonA->getAlphaRate() != 0.0f) { mpButtonA->setAlphaRate(g_drawHIO.mButtonAAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha)); @@ -3175,9 +3267,13 @@ void dMeter2Draw_c::setAlphaButtonAAnimeMin() { setAlphaAnimeMin(mpAText[i], 5); } } +#endif } void dMeter2Draw_c::setAlphaButtonAAnimeMax() { +#if TARGET_PC + write_fade_row(this, kFadeRowButtonA, true); +#else if (g_drawHIO.mButtonAAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha) != mpButtonA->getAlphaRate()) { @@ -3192,9 +3288,13 @@ void dMeter2Draw_c::setAlphaButtonAAnimeMax() { setAlphaAnimeMax(mpAText[i], 5); } } +#endif } void dMeter2Draw_c::setAlphaButtonBAnimeMin() { +#if TARGET_PC + write_fade_row(this, kFadeRowButtonB, false); +#else if (mpButtonB->getAlphaRate() != 0.0f) { mpButtonB->setAlphaRate(g_drawHIO.mButtonBAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha)); @@ -3216,9 +3316,13 @@ void dMeter2Draw_c::setAlphaButtonBAnimeMin() { setAlphaAnimeMin(mpBText[i], 5); } } +#endif } void dMeter2Draw_c::setAlphaButtonBAnimeMax() { +#if TARGET_PC + write_fade_row(this, kFadeRowButtonB, true); +#else if (mpButtonB->getAlphaRate() != g_drawHIO.mButtonBAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha)) { @@ -3242,83 +3346,116 @@ void dMeter2Draw_c::setAlphaButtonBAnimeMax() { setAlphaAnimeMax(mpBText[i], 5); } } +#endif } void dMeter2Draw_c::setButtonIconAAlpha(u8 unused0, u32 unused1, bool unused2) { - if (mpButtonA->isVisible()) { - u8 alpha = g_drawHIO.mButtonAAlpha * - (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha) * - (f32)mpButtonA->getInitAlpha(); - f32 alpha_rate = mpButtonParent->getAlphaRate(); + if (DUSK_IF_ELSE(isExecuting() ? mpButtonA->isVisible() : s_follow_a.live, mpButtonA->isVisible())) { +#if TARGET_PC + if (isExecuting()) { + follow_button_a(true, !dMeter2Info_isUseButton(1) && !dComIfGp_isDoSetFlag(4), !dMeter2Info_isUseButton(1)); + } else +#endif + { + u8 alpha = g_drawHIO.mButtonAAlpha * + (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha) * + (f32)mpButtonA->getInitAlpha(); + f32 alpha_rate = DUSK_IF_ELSE(button_parent_rate(this), mpButtonParent->getAlphaRate()); - if (!dMeter2Info_isUseButton(1) && !dComIfGp_isDoSetFlag(4)) { - alpha = g_drawHIO.mButtonXYBaseDimAlpha; - } - - mpButtonA->setAlpha(alpha * alpha_rate); - - for (int i = 0; i < 5; i++) { - u8 alpha = mpAText[i]->getInitAlpha(); - if (!dMeter2Info_isUseButton(1)) { - alpha = (f32)alpha * ((f32)g_drawHIO.field_0x42c / 255.0f); + if (DUSK_IF_ELSE(s_follow_a.dim, !dMeter2Info_isUseButton(1) && !dComIfGp_isDoSetFlag(4))) { + alpha = g_drawHIO.mButtonXYBaseDimAlpha; } - mpAText[i]->setAlpha(alpha * alpha_rate); + mpButtonA->setAlpha(alpha * alpha_rate); + + for (int i = 0; i < 5; i++) { + u8 alpha = mpAText[i]->getInitAlpha(); + if (DUSK_IF_ELSE(s_follow_a.dim_txt, !dMeter2Info_isUseButton(1))) { + alpha = (f32)alpha * ((f32)g_drawHIO.field_0x42c / 255.0f); + } + + mpAText[i]->setAlpha(alpha * alpha_rate); + } } +#if TARGET_PC + } else { + if (isExecuting()) { + follow_button_a(false, false, false); + } +#endif } } void dMeter2Draw_c::setButtonIconBAlpha(u8 unused0, u32 unused1, bool param_2) { - if (mpItemB->isVisible() || mpLightB->isVisible() || mpButtonB->isVisible()) { - f32 temp_f30 = - g_drawHIO.mButtonBAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); + if (DUSK_IF_ELSE(isExecuting() ? (mpItemB->isVisible() || mpLightB->isVisible() || mpButtonB->isVisible()) + : s_follow_b.live, mpItemB->isVisible() || mpLightB->isVisible() || mpButtonB->isVisible())) + { +#if TARGET_PC + if (isExecuting()) { + follow_button_b(true, !(dMeter2Info_isUseButton(2) & 1) && !dMeter2Info_isSub2DStatus(1), !dMeter2Info_isUseButton(2) && !dMeter2Info_isSub2DStatus(1), param_2, mpItemB->isVisible()); + } else +#endif + { + f32 temp_f30 = + g_drawHIO.mButtonBAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); - f32 var_f2; - if (mpItemB->isVisible()) { - var_f2 = mItemBBaseAlpha[0]; - } else { - var_f2 = mItemBBaseAlpha[1]; - } - - u8 var_r25 = mpItemB->getInitAlpha(); - u8 var_r26 = var_f2 * (temp_f30 * (f32)mpLightB->getInitAlpha()); - u8 var_r27 = temp_f30 * (f32)mpButtonB->getInitAlpha(); - f32 temp_f31 = mpButtonParent->getAlphaRate(); - - if (!(dMeter2Info_isUseButton(2) & 1) && !dMeter2Info_isSub2DStatus(1)) { - var_r25 = g_drawHIO.mButtonXYItemDimAlpha; - var_r26 = g_drawHIO.mButtonXYItemDimAlpha; - var_r27 = g_drawHIO.mButtonXYBaseDimAlpha; - } - - if (!param_2) { - var_r26 = 0; - } - - mpItemB->setAlpha(var_r25 * temp_f31); - mpLightB->setAlpha(var_r26 * temp_f31); - mpButtonB->setAlpha(var_r27 * temp_f31); - - for (int i = 0; i < 5; i++) { - u8 alpha = mpBText[i]->getInitAlpha(); - if (!dMeter2Info_isUseButton(2) && !dMeter2Info_isSub2DStatus(1)) { - alpha = (f32)alpha * ((f32)g_drawHIO.field_0x42c / 255.0f); + f32 var_f2; + if (DUSK_IF_ELSE(s_follow_b.item_visible, mpItemB->isVisible())) { + var_f2 = mItemBBaseAlpha[0]; + } else { + var_f2 = mItemBBaseAlpha[1]; } - mpBText[i]->setAlpha(alpha * temp_f31); + u8 var_r25 = mpItemB->getInitAlpha(); + u8 var_r26 = var_f2 * (temp_f30 * (f32)mpLightB->getInitAlpha()); + u8 var_r27 = temp_f30 * (f32)mpButtonB->getInitAlpha(); + f32 temp_f31 = DUSK_IF_ELSE(button_parent_rate(this), mpButtonParent->getAlphaRate()); + + if (DUSK_IF_ELSE(s_follow_b.dim, !(dMeter2Info_isUseButton(2) & 1) && !dMeter2Info_isSub2DStatus(1))) { + var_r25 = g_drawHIO.mButtonXYItemDimAlpha; + var_r26 = g_drawHIO.mButtonXYItemDimAlpha; + var_r27 = g_drawHIO.mButtonXYBaseDimAlpha; + } + + if (!DUSK_IF_ELSE(s_follow_b.light_on, param_2)) { + var_r26 = 0; + } + + mpItemB->setAlpha(var_r25 * temp_f31); + mpLightB->setAlpha(var_r26 * temp_f31); + mpButtonB->setAlpha(var_r27 * temp_f31); + + for (int i = 0; i < 5; i++) { + u8 alpha = mpBText[i]->getInitAlpha(); + if (DUSK_IF_ELSE(s_follow_b.dim_txt, !dMeter2Info_isUseButton(2) && !dMeter2Info_isSub2DStatus(1))) { + alpha = (f32)alpha * ((f32)g_drawHIO.field_0x42c / 255.0f); + } + + mpBText[i]->setAlpha(alpha * temp_f31); + } } +#if TARGET_PC + } else { + if (isExecuting()) { + follow_button_b(false, false, false, false, false); + } +#endif } } void dMeter2Draw_c::setButtonIconMidonaAlpha(u32 param_0) { +#if TARGET_PC + bool midona_live = false; + bool midona_hold = false; + f32 midona_z_target = 0.0f; +#endif mpButtonMidona->scale(g_drawHIO.mMidnaIconScale, g_drawHIO.mMidnaIconScale); mpButtonMidona->paneTrans(g_drawHIO.mMidnaIconPosX, g_drawHIO.mMidnaIconPosY); if (mpButtonMidona->isVisible()) { - f32 temp_f30 = - g_drawHIO.mMidnaIconAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); + IF_NOT_DUSK(f32 temp_f30 = g_drawHIO.mMidnaIconAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha)); f32 var_f29 = (f32)mpButtonMidona->getInitAlpha() / 255.0f; - f32 temp_f31 = mpButtonParent->getAlphaRate(); + f32 temp_f31 = DUSK_IF_ELSE(fade_row_factor(s_fade_rows[kFadeRowButtonParent]), mpButtonParent->getAlphaRate()); bool var_r31 = 1; if (getCanoeFishing() || @@ -3340,12 +3477,14 @@ void dMeter2Draw_c::setButtonIconMidonaAlpha(u32 param_0) { field_0x738 = 18.0f; } +#if !TARGET_PC if (field_0x738 == 18.0f) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON_BLINK, NULL, 0, 0); dMeter2Info_set2DVibration(); } +#endif - mButtonZAlpha = var_f29; + IF_NOT_DUSK(mButtonZAlpha = var_f29); var_r31 = 0; } @@ -3354,6 +3493,7 @@ void dMeter2Draw_c::setButtonIconMidonaAlpha(u32 param_0) { field_0x738 = 0.0f; } +#if !TARGET_PC if (mButtonZAlpha != var_f29) { cLib_addCalc2(&mButtonZAlpha, var_f29, 0.4f, 0.5f); @@ -3361,24 +3501,34 @@ void dMeter2Draw_c::setButtonIconMidonaAlpha(u32 param_0) { mButtonZAlpha = var_f29; } } +#endif } +#if TARGET_PC + midona_live = true; + midona_hold = var_r31 == 0; + midona_z_target = var_f29; +#else mpButtonMidona->setAlpha(temp_f30 * (255.0f * mButtonZAlpha * temp_f31)); +#endif } else { - mButtonZAlpha = 0.0f; + IF_NOT_DUSK(mButtonZAlpha = 0.0f); } f32 var_f29_2 = (g_drawHIO.mButtonZAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha) * (f32)mpButtonXY[2]->getInitAlpha()) / 255.0f; - f32 temp_f30_2 = mpButtonParent->getAlphaRate(); + IF_NOT_DUSK(f32 temp_f30_2 = mpButtonParent->getAlphaRate()); if (param_0 & 0x1000000) { var_f29_2 = 0.0f; } else if (!dMeter2Info_isUseButton(0x800)) { var_f29_2 = (f32)g_drawHIO.mButtonXYBaseDimAlpha / 255.0f; } +#if TARGET_PC + chase_midona(midona_live, midona_hold, midona_z_target, var_f29_2); +#else if (field_0x724 != var_f29_2) { cLib_addCalc2(&field_0x724, var_f29_2, 0.4f, 0.5f); @@ -3388,91 +3538,102 @@ void dMeter2Draw_c::setButtonIconMidonaAlpha(u32 param_0) { } mpButtonXY[2]->setAlpha(255.0f * field_0x724 * temp_f30_2); - -#if TARGET_PC - dusk::ui::update_midna_icon_texture(mpButtonMidona != NULL ? mpButtonMidona->getPanePtr() : NULL); #endif + + IF_DUSK(dusk::ui::update_midna_icon_texture(mpButtonMidona != NULL ? mpButtonMidona->getPanePtr() : NULL)); } void dMeter2Draw_c::setButtonIconAlpha(int i_no, u8 unused0, u32 unused1, bool unused2) { JUT_ASSERT(0, i_no < SELECT_MAX_e); - if (mpItemXY[i_no]->isVisible() || mpLightXY[i_no]->isVisible() || - mpButtonXY[i_no]->isVisible()) - { - f32 var_f30 = 1.0f; + if (DUSK_IF_ELSE(isExecuting() ? (mpItemXY[i_no]->isVisible() || mpLightXY[i_no]->isVisible() || mpButtonXY[i_no]->isVisible()) + : s_follow_xy[i_no].live, mpItemXY[i_no]->isVisible() || mpLightXY[i_no]->isVisible() || mpButtonXY[i_no]->isVisible())) { +#if TARGET_PC + if (isExecuting()) { + follow_button_xy(i_no, true, !dMeter2Info_isUseButton(i_no == 0 ? 4 : 8), getFishingType() != 0, mpItemXY[i_no]->isVisible()); + } else +#endif + { + f32 var_f30 = 1.0f; - int var_r26 = 1; - if (mpItemXY[i_no]->isVisible()) { - var_r26 = 0; - } + int var_r26 = 1; + if (DUSK_IF_ELSE(s_follow_xy[i_no].item_visible, mpItemXY[i_no]->isVisible())) { + var_r26 = 0; + } - f32 var_f2; - if (i_no == 0) { - var_f2 = - g_drawHIO.mButtonXAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); - var_f30 = g_drawHIO.mButtonXItemBaseAlpha[var_r26]; - } else if (i_no == 1) { - var_f2 = - g_drawHIO.mButtonYAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); - var_f30 = g_drawHIO.mButtonYItemBaseAlpha[var_r26]; - } else { - var_f2 = - g_drawHIO.mButtonZAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); - } + f32 var_f2; + if (i_no == 0) { + var_f2 = + g_drawHIO.mButtonXAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); + var_f30 = g_drawHIO.mButtonXItemBaseAlpha[var_r26]; + } else if (i_no == 1) { + var_f2 = + g_drawHIO.mButtonYAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); + var_f30 = g_drawHIO.mButtonYItemBaseAlpha[var_r26]; + } else { + var_f2 = + g_drawHIO.mButtonZAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); + } - u8 var_r28 = mpItemXY[i_no]->getInitAlpha(); - u8 var_r27 = var_f30 * (var_f2 * (f32)mpLightXY[i_no]->getInitAlpha()); - u8 var_r26_2 = var_f2 * (f32)mpButtonXY[i_no]->getInitAlpha(); - f32 temp_f31 = mpButtonParent->getAlphaRate(); + u8 var_r28 = mpItemXY[i_no]->getInitAlpha(); + u8 var_r27 = var_f30 * (var_f2 * (f32)mpLightXY[i_no]->getInitAlpha()); + u8 var_r26_2 = var_f2 * (f32)mpButtonXY[i_no]->getInitAlpha(); + f32 temp_f31 = DUSK_IF_ELSE(button_parent_rate(this), mpButtonParent->getAlphaRate()); - if (i_no == 0) { - if (!dMeter2Info_isUseButton(4)) { - if (getFishingType()) { - var_r28 = 0; - } else { - var_r28 = g_drawHIO.mButtonXYItemDimAlpha; + if (i_no == 0) { + if (DUSK_IF_ELSE(s_follow_xy[i_no].dim, !dMeter2Info_isUseButton(4))) { + if (DUSK_IF_ELSE(s_follow_xy[i_no].fishing, getFishingType())) { + var_r28 = 0; + } else { + var_r28 = g_drawHIO.mButtonXYItemDimAlpha; + } + + var_r27 = g_drawHIO.mButtonXYItemDimAlpha; + var_r26_2 = g_drawHIO.mButtonXYBaseDimAlpha; + } + } else if (i_no == 1) { + if (DUSK_IF_ELSE(s_follow_xy[i_no].dim, !dMeter2Info_isUseButton(8))) { + if (DUSK_IF_ELSE(s_follow_xy[i_no].fishing, getFishingType())) { + var_r28 = 0; + } else { + var_r28 = g_drawHIO.mButtonXYItemDimAlpha; + } + + var_r27 = g_drawHIO.mButtonXYItemDimAlpha; + var_r26_2 = g_drawHIO.mButtonXYBaseDimAlpha; + } + } + + mpItemXY[i_no]->setAlpha((f32)var_r28 * temp_f31); + mpLightXY[i_no]->setAlpha((f32)var_r27 * temp_f31); + mpButtonXY[i_no]->setAlpha((f32)var_r26_2 * temp_f31); + + int var_r26_3 = 1; + if (i_no == 0) { + if (DUSK_IF_ELSE(s_follow_xy[i_no].dim, !dMeter2Info_isUseButton(4))) { + var_r26_3 = 0; + } + } else if (i_no == 1) { + if (DUSK_IF_ELSE(s_follow_xy[i_no].dim, !dMeter2Info_isUseButton(8))) { + var_r26_3 = 0; + } + } + + for (int i = 0; i < 5; i++) { + u8 var_r0 = mpXYText[i][i_no]->getInitAlpha(); + if (!var_r26_3) { + var_r0 = (f32)var_r0 * (g_drawHIO.field_0x42c / 255.0f); } - var_r27 = g_drawHIO.mButtonXYItemDimAlpha; - var_r26_2 = g_drawHIO.mButtonXYBaseDimAlpha; - } - } else if (i_no == 1) { - if (!dMeter2Info_isUseButton(8)) { - if (getFishingType()) { - var_r28 = 0; - } else { - var_r28 = g_drawHIO.mButtonXYItemDimAlpha; - } - - var_r27 = g_drawHIO.mButtonXYItemDimAlpha; - var_r26_2 = g_drawHIO.mButtonXYBaseDimAlpha; + mpXYText[i][i_no]->setAlpha((f32)var_r0 * temp_f31); } } - - mpItemXY[i_no]->setAlpha((f32)var_r28 * temp_f31); - mpLightXY[i_no]->setAlpha((f32)var_r27 * temp_f31); - mpButtonXY[i_no]->setAlpha((f32)var_r26_2 * temp_f31); - - int var_r26_3 = 1; - if (i_no == 0) { - if (!dMeter2Info_isUseButton(4)) { - var_r26_3 = 0; - } - } else if (i_no == 1) { - if (!dMeter2Info_isUseButton(8)) { - var_r26_3 = 0; - } - } - - for (int i = 0; i < 5; i++) { - u8 var_r0 = mpXYText[i][i_no]->getInitAlpha(); - if (!var_r26_3) { - var_r0 = (f32)var_r0 * (g_drawHIO.field_0x42c / 255.0f); - } - - mpXYText[i][i_no]->setAlpha((f32)var_r0 * temp_f31); +#if TARGET_PC + } else { + if (isExecuting()) { + follow_button_xy(i_no, false, false, false, false); } +#endif } } @@ -3637,6 +3798,24 @@ void dMeter2Draw_c::changeTextureItemXY(int i_no, u8 i_itemNo) { } void dMeter2Draw_c::setAlphaAnimeMin(CPaneMgrAlpha* i_pane, s16 i_min) { +#if TARGET_PC + if (i_pane == NULL) { + return; + } + if (i_min <= 0) { + i_pane->alphaAnimeStart(0.0f); + i_pane->setAlphaRate(0.0f); + return; + } + const f32 alphaRate = i_pane->getAlphaRate(); + f32 alphaTimer = i_pane->getAlphaTimer(); + if (alphaTimer > i_min) { + alphaTimer = i_min; + } + dusk::vdt::advance_toward_frame(alphaTimer, 0.0f, 1.0f); + i_pane->alphaAnimeStart(alphaTimer); + i_pane->setAlphaRate(alphaRate * dusk::vdt::clamped_fraction(alphaTimer, i_min)); +#else f32 alpha_rate = i_pane->getAlphaRate(); s16 alpha_timer = i_pane->getAlphaTimer(); if (alpha_timer > i_min) { @@ -3656,9 +3835,23 @@ void dMeter2Draw_c::setAlphaAnimeMin(CPaneMgrAlpha* i_pane, s16 i_min) { i_pane->alphaAnimeStart(alpha_timer); i_pane->setAlphaRate(alpha_rate * ((f32)alpha_timer / (f32)i_min)); } +#endif } void dMeter2Draw_c::setAlphaAnimeMax(CPaneMgrAlpha* i_pane, s16 i_max) { +#if TARGET_PC + if (i_pane == NULL) { + return; + } + if (i_max <= 0) { + return; + } + const f32 alphaRate = i_pane->getAlphaRate(); + f32 alphaTimer = i_pane->getAlphaTimer(); + dusk::vdt::advance_toward_frame(alphaTimer, i_max, 1.0f); + i_pane->alphaAnimeStart(alphaTimer); + i_pane->setAlphaRate(alphaRate * dusk::vdt::clamped_fraction(alphaTimer, i_max)); +#else f32 alpha_rate = i_pane->getAlphaRate(); s16 alpha_timer = i_pane->getAlphaTimer(); @@ -3675,6 +3868,7 @@ void dMeter2Draw_c::setAlphaAnimeMax(CPaneMgrAlpha* i_pane, s16 i_max) { i_pane->alphaAnimeStart(alpha_timer); i_pane->setAlphaRate(alpha_rate * ((f32)alpha_timer / (f32)i_max)); } +#endif } void dMeter2Draw_c::setItemNum(u8 i_button, u8 i_num, u8 i_max) { @@ -3772,7 +3966,7 @@ void dMeter2Draw_c::drawKanteraMeter(u8 i_button, f32 i_alphaRate) { } u8 dMeter2Draw_c::isButtonVisible() { - return (mpButtonParent->getAlphaRate() == 0.0f) ^ 1; + return (DUSK_IF_ELSE(fade_row_factor(s_fade_rows[kFadeRowButtonParent]), mpButtonParent->getAlphaRate()) == 0.0f) ^ 1; } void dMeter2Draw_c::setItemParamX(u8 i_itemNo) { @@ -4198,7 +4392,7 @@ bool dMeter2Draw_c::isBButtonShow(bool param_0) { } s16 dMeter2Draw_c::getButtonTimer() { - return mpButtonParent->getAlphaTimer(); + return DUSK_IF_ELSE((s16)s_fade_rows[kFadeRowButtonParent].timer, mpButtonParent->getAlphaTimer()); } // unused diff --git a/src/d/d_meter2_draw_vdt.inc b/src/d/d_meter2_draw_vdt.inc new file mode 100644 index 0000000000..2e8bb1e8d1 --- /dev/null +++ b/src/d/d_meter2_draw_vdt.inc @@ -0,0 +1,581 @@ +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/user_interface.h" + +#include "d/d_com_inf_game.h" +#include "d/d_meter2_info.h" +#include "d/d_meter_HIO.h" +#include "d/d_meter_map.h" +#include "d/d_pane_class.h" + +namespace { + +struct FadeRow { + bool used; + bool show; + bool follow; + bool got_free; + bool got_follow; + f32 timer; + f32 duration; + f32 full; +}; + +struct FollowA { + bool live; + bool dim; + bool dim_txt; +}; + +struct FollowB { + bool live; + bool dim; + bool dim_txt; + bool light_on; + bool item_visible; +}; + +struct FollowXY { + bool live; + bool dim; + bool fishing; + bool item_visible; +}; + +struct MidonaChase { + bool used; + bool live; + bool hold; + f32 z_target; + f32 xy_target; +}; + +struct GaugeFill { + bool used; + s32 prev; + s32 now; + s32 max; +}; + +enum FadeRowId { + kFadeRowLife, + kFadeRowKantera, + kFadeRowOxygen, + kFadeRowLightDrop, + kFadeRowRupee, + kFadeRowKey, + kFadeRowButtonParent, + kFadeRowButtonA, + kFadeRowButtonB, + kFadeRowCross, + kFadeRowCrossItem, + kFadeRowCrossMap, + kFadeRowMap, + kFadeRowCount +}; + +FadeRow s_fade_rows[kFadeRowCount]; +FollowA s_follow_a; +FollowB s_follow_b; +FollowXY s_follow_xy[2]; +MidonaChase s_midona; +GaugeFill s_kantera_fill; +GaugeFill s_oxygen_fill; +bool s_light_drop_get; + +f32 fade_row_factor(FadeRow const& r) { + if (r.duration <= 0.0f) { + return r.show ? 1.0f : 0.0f; + } + return dusk::vdt::clamped_fraction(r.timer, r.duration); +} + +void publish_fade_row(FadeRow& c, bool show, f32 duration, f32 full) { + c.used = true; + c.duration = duration > 0.0f ? duration : 1.0f; + if (c.timer > c.duration) { + c.timer = c.duration; + } + c.full = full; + c.show = show; + c.got_free = true; +} + +void publish_follow_row(FadeRow& c) { + c.used = true; + c.got_follow = true; +} + +f32 button_parent_rate(dMeter2Draw_c* d) { + if (s_fade_rows[kFadeRowButtonParent].used) { + return s_fade_rows[kFadeRowButtonParent].full * fade_row_factor(s_fade_rows[kFadeRowButtonParent]); + } + if (d->mpButtonParent != NULL) { + return d->mpButtonParent->getAlphaRate(); + } + return 0.0f; +} + +void reset_follow_intent(FadeRow& r) { + r.got_free = false; + r.got_follow = false; +} + +void resolve_follow_row(FadeRow& r, FadeRow const& parent) { + if (r.got_follow) { + r.follow = true; + } else if (r.follow && r.got_free) { + r.follow = false; + r.timer = fade_row_factor(parent) * r.duration; + } +} + +void step_fade_row(FadeRow& c) { + if (c.used && !c.follow && (c.show ? c.timer < c.duration : c.timer > 0.0f)) { + dusk::vdt::advance_toward_frame(c.timer, c.show ? c.duration : 0.0f, 1.0f); + } +} + +f32 const kFadeDuration = 5.0f; + +f32 fade_full(dMeter2Draw_c* d, FadeRowId id) { + f32 const parent = g_drawHIO.mParentAlpha; + f32 const buttons = parent * g_drawHIO.mMainHUDButtonsAlpha; + f32 const rupee_key = parent * g_drawHIO.mRupeeKeyAlpha; + switch (id) { + case kFadeRowLife: + case kFadeRowKantera: + case kFadeRowOxygen: + case kFadeRowCross: + return parent; + case kFadeRowLightDrop: + return parent * d->field_0x6fc; + case kFadeRowRupee: + return g_drawHIO.mRupeeAlpha * rupee_key; + case kFadeRowKey: + return g_drawHIO.mKeyAlpha * rupee_key; + case kFadeRowButtonParent: + return buttons; + case kFadeRowButtonA: + return g_drawHIO.mButtonAAlpha * buttons; + case kFadeRowButtonB: + return g_drawHIO.mButtonBAlpha * buttons; + case kFadeRowCrossItem: + return parent * g_drawHIO.mButtonCrossITEMAlpha; + case kFadeRowCrossMap: + return parent * g_drawHIO.mButtonCrossMAPAlpha; + case kFadeRowMap: + return 1.0f; + default: + return 0.0f; + } +} + +void apply_parent_rate(CPaneMgrAlpha* pane, FadeRow const& row) { + if (!row.used || pane == NULL || pane->getPanePtr() == NULL) { + return; + } + const f32 rate = row.full * fade_row_factor(row); + if (pane->getAlphaRate() != rate) { + pane->setAlphaRate(rate); + } +} + +void apply_meter_rate(dMeter2Draw_c* d, int index, FadeRow const& row) { + if (!row.used) { + return; + } + d->mMeterAlphaRate[index] = row.full * fade_row_factor(row); +} + +void apply_cross_text(CPaneMgr* text, CPaneMgr** juji, FadeRow const& row) { + if (!row.used) { + return; + } + const f32 factor = fade_row_factor(row); + if (text != NULL) { + text->setAlphaRate(row.full * factor); + } + const f32 juji_rate = g_drawHIO.mParentAlpha * factor; + for (int i = 0; i < 5; i++) { + if (juji[i] != NULL) { + juji[i]->setAlphaRate(juji_rate); + } + } +} + +void apply_light_drop(dMeter2Draw_c* d) { + if (!s_fade_rows[kFadeRowLightDrop].used || d->mpLightDropParent == NULL) { + return; + } + + const f32 parent_rate = s_fade_rows[kFadeRowLightDrop].full * + fade_row_factor(s_fade_rows[kFadeRowLightDrop]); + if (d->mpLightDropParent->getAlphaRate() != parent_rate) { + d->mpLightDropParent->setAlphaRate(parent_rate); + } + + f32 si_parent_rate[2]; + for (int i = 0; i < 2; i++) { + si_parent_rate[i] = g_drawHIO.mLightDrop.mVesselAlpha[i + 2] * parent_rate; + if (d->mpSIParent[i] != NULL) { + d->mpSIParent[i]->setAlphaRate(si_parent_rate[i]); + } + } + + if (s_light_drop_get && s_fade_rows[kFadeRowLightDrop].show) { + if (g_drawHIO.mLightDrop.mDropGetScaleAnimFrameNum > 1) { + const f32 duration = g_drawHIO.mLightDrop.mDropGetScaleAnimFrameNum; + const f32 remainder = duration - s_fade_rows[kFadeRowLightDrop].timer; + const f32 scale = g_drawHIO.mLightDrop.mDropGetScale - 1.0f; + d->field_0x6f8 = 1.0f + (scale * remainder) / (duration - 1.0f); + } else { + d->field_0x6f8 = 1.0f; + } + } + + const int drop_num = dComIfGs_getLightDropNum(dComIfGp_getStartStageDarkArea()); + const f32 si1 = si_parent_rate[1]; + for (int i = 0; i < 16; i++) { + const f32 rate = i < drop_num ? si1 : si1 * 0.3f; + if (d->mpSIParts[i][1] != NULL) { + d->mpSIParts[i][1]->setAlphaRate(rate); + } + if (d->mpSIParts[i][2] != NULL) { + d->mpSIParts[i][2]->setAlphaRate(rate); + } + } +} + +void apply_rupee(dMeter2Draw_c* d) { + if (!s_fade_rows[kFadeRowRupee].used) { + return; + } + + const f32 factor = fade_row_factor(s_fade_rows[kFadeRowRupee]); + const f32 parent_rate[3] = { + s_fade_rows[kFadeRowRupee].full * factor, + g_drawHIO.mRupeeFrameAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mRupeeKeyAlpha) * factor, + g_drawHIO.mRupeeFrameAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mRupeeKeyAlpha) * factor, + }; + + for (int i = 0; i < 3; i++) { + if (d->mpRupeeParent[i] != NULL && d->mpRupeeParent[i]->getAlphaRate() != parent_rate[i]) { + d->mpRupeeParent[i]->setAlphaRate(parent_rate[i]); + } + } +} + +void apply_button_parent(dMeter2Draw_c* d) { + if (!s_fade_rows[kFadeRowButtonParent].used || d->mpButtonParent == NULL) { + return; + } + + apply_parent_rate(d->mpButtonParent, s_fade_rows[kFadeRowButtonParent]); + const f32 parent_rate = s_fade_rows[kFadeRowButtonParent].full * + fade_row_factor(s_fade_rows[kFadeRowButtonParent]); + if (d->mpUzu != NULL) { + d->mpUzu->setAlphaRate(d->mButtonBaseAlpha * parent_rate); + } +} + +void apply_button_a(dMeter2Draw_c* d) { + if (!s_fade_rows[kFadeRowButtonA].used || d->mpButtonA == NULL) { + return; + } + + if (s_fade_rows[kFadeRowButtonA].follow) { + d->setButtonIconAAlpha(0, 0, false); + return; + } + + const f32 factor = fade_row_factor(s_fade_rows[kFadeRowButtonA]); + d->mpButtonA->setAlphaRate(s_fade_rows[kFadeRowButtonA].full * factor); + const f32 text_rate = (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha) * factor; + for (int i = 0; i < 5; i++) { + if (d->mpAText[i] != NULL) { + d->mpAText[i]->setAlphaRate(text_rate); + } + } +} + +void apply_button_b(dMeter2Draw_c* d) { + if (!s_fade_rows[kFadeRowButtonB].used || d->mpButtonB == NULL) { + return; + } + + if (s_fade_rows[kFadeRowButtonB].follow) { + d->setButtonIconBAlpha(0, 0, false); + return; + } + + const f32 factor = fade_row_factor(s_fade_rows[kFadeRowButtonB]); + const f32 button_rate = s_fade_rows[kFadeRowButtonB].full * factor; + const f32 text_rate = (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha) * factor; + d->mpButtonB->setAlphaRate(button_rate); + if (d->mpLightB != NULL) { + d->mpLightB->setAlphaRate(button_rate); + } + if (d->mpItemB != NULL) { + d->mpItemB->setAlphaRate(text_rate); + } + for (int i = 0; i < 5; i++) { + if (d->mpBText[i] != NULL) { + d->mpBText[i]->setAlphaRate(text_rate); + } + } +} + +void advance_fade_rows() { + for (int i = 0; i < kFadeRowCount; i++) { + step_fade_row(s_fade_rows[i]); + } +} + +void write_fade_row(dMeter2Draw_c* d, FadeRowId id, bool show) { + if (id >= kFadeRowCount) { + return; + } + publish_fade_row(s_fade_rows[id], show, kFadeDuration, fade_full(d, id)); + if (id == kFadeRowButtonParent && show) { + if (fade_row_factor(s_fade_rows[kFadeRowButtonParent]) >= 1.0f) { + if (dMeter2Info_getLightDropGetFlag(dComIfGp_getStartStageDarkArea()) == 1) { + dMeter2Info_setLightDropGetFlag(dComIfGp_getStartStageDarkArea(), 2); + } + } + } +} + +void follow_button_a(bool live, bool dim, bool dim_txt) { + s_follow_a = {live, dim, dim_txt}; + if (live) { + publish_follow_row(s_fade_rows[kFadeRowButtonA]); + } +} + +void follow_button_b(bool live, bool dim, bool dim_txt, bool light_on, bool item_visible) { + s_follow_b = {live, dim, dim_txt, light_on, item_visible}; + if (live) { + publish_follow_row(s_fade_rows[kFadeRowButtonB]); + } +} + +void follow_button_xy(int i_no, bool live, bool dim, bool fishing, bool item_visible) { + if (i_no < 0 || i_no > 1) { + return; + } + s_follow_xy[i_no] = {live, dim, fishing, item_visible}; +} + +void chase_midona(bool live, bool hold, f32 z_target, f32 xy_target) { + s_midona = {true, live, hold, z_target, xy_target}; +} + +void seed_gauge(GaugeFill& g, s32 now) { + g.prev = now; +} + +void publish_gauge(GaugeFill& g, s32 now, s32 max) { + g.used = true; + g.now = now; + g.max = max; +} + +f32 gauge_draw(GaugeFill const& g) { + const f32 step = dusk::interp::get_interpolation_step(); + return (f32)g.prev + ((f32)g.now - (f32)g.prev) * step; +} +} // namespace + +f32 dMeter2Draw_c::getMeterGaugeAlphaRate(u8 i_no) { + if (i_no == 1) { + return fade_row_factor(s_fade_rows[kFadeRowKantera]) * g_drawHIO.mParentAlpha; + } + if (i_no == 2) { + return fade_row_factor(s_fade_rows[kFadeRowOxygen]) * g_drawHIO.mParentAlpha; + } + return mMeterAlphaRate[i_no]; +} + +void dMeter2Draw_c::beginExecution() { + mExecuting = true; + reset_follow_intent(s_fade_rows[kFadeRowButtonA]); + reset_follow_intent(s_fade_rows[kFadeRowButtonB]); + seed_gauge(s_kantera_fill, dComIfGp_getItemNowOil()); + seed_gauge(s_oxygen_fill, dComIfGp_getNowOxygen()); +} + +void dMeter2Draw_c::endExecution() { + resolve_follow_row(s_fade_rows[kFadeRowButtonA], s_fade_rows[kFadeRowButtonParent]); + resolve_follow_row(s_fade_rows[kFadeRowButtonB], s_fade_rows[kFadeRowButtonParent]); + mExecuting = false; +} + +void dMeter2Draw_c::publishKantera(s32 now, s32 max) { + publish_gauge(s_kantera_fill, now, max); +} + +void dMeter2Draw_c::publishOxygen(s32 now, s32 max) { + publish_gauge(s_oxygen_fill, now, max); +} + +void dMeter2Draw_c::present() { + apply_parent_rate(this->mpLifeParent, s_fade_rows[kFadeRowLife]); + if (s_fade_rows[kFadeRowLife].used) { + this->setAlphaLifeChange(false); + } + + apply_meter_rate(this, 1, s_fade_rows[kFadeRowKantera]); + if (s_fade_rows[kFadeRowKantera].used) { + this->setAlphaKanteraChange(true); + } + if (s_kantera_fill.used) { + drawKantera(s_kantera_fill.max, gauge_draw(s_kantera_fill), + g_drawHIO.mLanternMeterPosX, g_drawHIO.mLanternMeterPosY); + } + + apply_meter_rate(this, 2, s_fade_rows[kFadeRowOxygen]); + if (s_fade_rows[kFadeRowOxygen].used) { + this->setAlphaOxygenChange(false); + } + if (s_oxygen_fill.used) { + drawOxygen(s_oxygen_fill.max, gauge_draw(s_oxygen_fill), + g_drawHIO.mOxygenMeterPosX, g_drawHIO.mOxygenMeterPosY); + } + + apply_light_drop(this); + if (s_fade_rows[kFadeRowLightDrop].used) { + this->setAlphaLightDropChange(false); + } + + apply_rupee(this); + if (s_fade_rows[kFadeRowRupee].used) { + this->setAlphaRupeeChange(false); + } + + apply_parent_rate(this->mpKeyParent, s_fade_rows[kFadeRowKey]); + if (s_fade_rows[kFadeRowKey].used) { + this->setAlphaKeyChange(false); + } + + apply_button_parent(this); + apply_button_a(this); + apply_button_b(this); + setButtonIconAlpha(0, 0, 0, false); + setButtonIconAlpha(1, 0, 0, false); + if (s_fade_rows[kFadeRowButtonParent].used) { + this->setAlphaButtonChange(false); + } + + apply_parent_rate(this->mpButtonCrossParent, s_fade_rows[kFadeRowCross]); + apply_cross_text(this->mpTextI, this->mpJujiI, s_fade_rows[kFadeRowCrossItem]); + apply_cross_text(this->mpTextM, this->mpJujiM, s_fade_rows[kFadeRowCrossMap]); + + if (!s_midona.used || this->mpButtonMidona == NULL) { + return; + } + + const f32 parent_rate = button_parent_rate(this); + if (s_midona.live) { + if (s_midona.hold) { + this->mButtonZAlpha = s_midona.z_target; + } else { + dusk::vdt::present_addCalc2(&this->mButtonZAlpha, s_midona.z_target, 0.4f, 0.5f, 0.1f); + } + const f32 full = g_drawHIO.mMidnaIconAlpha * (g_drawHIO.mParentAlpha * g_drawHIO.mMainHUDButtonsAlpha); + this->mpButtonMidona->setAlpha((u8)(full * (255.0f * this->mButtonZAlpha * parent_rate))); + } else { + this->mButtonZAlpha = 0.0f; + } + dusk::vdt::present_addCalc2(&this->field_0x724, s_midona.xy_target, 0.4f, 0.5f, 0.1f); + this->mpButtonXY[2]->setAlpha((u8)(255.0f * this->field_0x724 * parent_rate)); +} + +void dMeter2Draw_c::presentVessel(f32 i_posX, f32 i_posY, f32 i_scale) { + mLightDropVesselScale = i_scale; + const f32 lightDropScale = mLightDropVesselScale * field_0x6f8 * dGetUserHudScale(); + mpLightDropParent->scale(lightDropScale, lightDropScale); + dAnchorHudScale(mpLightDropParent, HudCorner::TopRight, &i_posX, &i_posY); + mpLightDropParent->paneTrans(i_posX, i_posY); +} + +void dMeter2Draw_c::presentButtonA(f32 i_posX, f32 i_posY, f32 i_textPosX, f32 i_textPosY, f32 i_scale) { + mpButtonA->scale(s_button_a_scale * i_scale, s_button_a_scale * i_scale); + mpButtonA->paneTrans(i_posX, i_posY); + mpTextA->scale(s_button_a_text_scale * i_scale, s_button_a_text_scale * i_scale); + mpTextA->paneTrans(g_drawHIO.mButtonATextPosX + i_textPosX, + g_drawHIO.mButtonATextPosY + i_textPosY + (dusk::version::isLessThanWiiJpn() ? 6.0f : 0.0f)); +} + +void dMeter2Draw_c::presentButtonB(f32 i_posX, f32 i_posY, f32 i_textPosX, f32 i_textPosY, f32 i_scale) { + const int kind = s_button_b_kind; + field_0x730 = s_button_b_scale * i_scale; + mpButtonB->scale(field_0x730 * field_0x734, field_0x730 * field_0x734); + mpButtonB->paneTrans(g_drawHIO.mButtonBPosX + i_posX, g_drawHIO.mButtonBPosY + i_posY); + + field_0x728 = g_drawHIO.mButtonBItemScale[kind] * i_scale; + mpItemB->scale(field_0x728 * field_0x734, field_0x728 * field_0x734); + mpItemB->paneTrans(field_0x6dc + (g_drawHIO.mButtonBItemPosX[kind] + i_posX), field_0x6e0 + (g_drawHIO.mButtonBItemPosY[kind] + i_posY)); + + field_0x72c = g_drawHIO.mItemBBaseScale[0] * i_scale; + mpLightB->scale(field_0x72c * field_0x734, field_0x72c * field_0x734); + mpLightB->paneTrans(g_drawHIO.mItemBBasePosX[0] + i_posX, g_drawHIO.mItemBBasePosY[0] + i_posY); + + mpTextB->scale(s_button_b_text_scale * i_scale, s_button_b_text_scale * i_scale); + mpTextB->paneTrans(g_drawHIO.mButtonBFontPosX + i_textPosX, + g_drawHIO.mButtonBFontPosY + i_textPosY + (dusk::version::isLessThanWiiJpn() ? 4.0f : 0.0f)); +} + +void dMeter2Draw_c::presentMapAlpha(dMeterMap_c* map) { + if (map == NULL) { + return; + } + if (!s_fade_rows[kFadeRowMap].used) { + map->setMapAlpha(0); + return; + } + map->setMapAlpha((u8)(fade_row_factor(s_fade_rows[kFadeRowMap]) * 255.0f)); +} + +void dMeter2Draw_c::showLightDrop(bool show, bool get_path) { + if (show && get_path) { + if (s_fade_rows[kFadeRowLightDrop].timer <= 0.0f) { + Z2GetAudioMgr()->seStart(Z2SE_SY_LIGHT_POT_EQUIP, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); + } + if (fade_row_factor(s_fade_rows[kFadeRowLightDrop]) >= 1.0f) { + dMeter2Info_setLightDropGetFlag(dComIfGp_getStartStageDarkArea(), 0xFF); + } + } + const f32 duration = get_path ? (f32)g_drawHIO.mLightDrop.mDropGetScaleAnimFrameNum : kFadeDuration; + publish_fade_row(s_fade_rows[kFadeRowLightDrop], show, duration, fade_full(this, kFadeRowLightDrop)); + s_light_drop_get = get_path; +} + +void dMeter2Draw_c::showCross(bool show) { + write_fade_row(this, kFadeRowCross, show); + write_fade_row(this, kFadeRowCrossItem, show && dMeter2Info_isItemOpenCheck()); + write_fade_row(this, kFadeRowCrossMap, show && dMeter2Info_isMapOpenCheck()); +} + +void dMeter2Draw_c::showMap(bool show) { + write_fade_row(this, kFadeRowMap, show); +} + +bool dMeter2Draw_c::lightDropRowUsed() const { + return s_fade_rows[kFadeRowLightDrop].used; +} + +void dMeter2Draw_c::resetRows() { + mExecuting = false; + for (int i = 0; i < kFadeRowCount; i++) { + s_fade_rows[i] = FadeRow{}; + } + s_follow_a = FollowA{}; + s_follow_b = FollowB{}; + s_follow_xy[0] = FollowXY{}; + s_follow_xy[1] = FollowXY{}; + s_midona = MidonaChase{}; + s_kantera_fill = GaugeFill{}; + s_oxygen_fill = GaugeFill{}; + s_light_drop_get = false; +} diff --git a/src/d/d_meter2_vdt.inc b/src/d/d_meter2_vdt.inc new file mode 100644 index 0000000000..17f6452335 --- /dev/null +++ b/src/d/d_meter2_vdt.inc @@ -0,0 +1,40 @@ +#include "dusk/interp/user_interface.h" + +#include "d/d_meter_map.h" + +void dMeter2_c::presentAnims() { + if (mpMeterDraw != NULL) { + mpMeterDraw->present(); + + if (!mPresentationTargets.ready) { + return; + } + + if (mpMeterDraw->lightDropRowUsed()) { + dusk::vdt::present_addCalc2(&mVesselPosX, mPresentationTargets.vesselX, 1.0f, 10.0f, 0.1f); + dusk::vdt::present_addCalc2(&mVesselPosY, mPresentationTargets.vesselY, 1.0f, 10.0f, 0.1f); + dusk::vdt::present_addCalc2(&mVesselScale, mPresentationTargets.vesselScale, 0.2f, 1.0f, 0.1f); + dusk::vdt::present_addCalc2(&mVesselAlpha, mPresentationTargets.vesselAlpha, 0.2f, 1.0f, 0.1f); + mpMeterDraw->field_0x6fc = mVesselAlpha; + mpMeterDraw->presentVessel(mVesselPosX, mVesselPosY, mVesselScale); + } + + for (int i = 0; i < 2; i++) { + dusk::vdt::present_addCalc2(&mButtonATalkPosX[i], mPresentationTargets.buttonAX[i], 1.0f, 10.0f, 0.1f); + dusk::vdt::present_addCalc2(&mButtonATalkPosY[i], mPresentationTargets.buttonAY[i], 1.0f, 10.0f, 0.1f); + dusk::vdt::present_addCalc2(&field_0x148[i], mPresentationTargets.buttonBX[i], 1.0f, 10.0f, 0.1f); + dusk::vdt::present_addCalc2(&field_0x150[i], mPresentationTargets.buttonBY[i], 1.0f, 10.0f, 0.1f); + } + dusk::vdt::present_addCalc2(&field_0x15c, mPresentationTargets.crossY, 0.5f, 50.0f, 0.5f); + mpMeterDraw->presentButtonA(mButtonATalkPosX[0], mButtonATalkPosY[0], mButtonATalkPosX[1], mButtonATalkPosY[1], 1.0f); + mpMeterDraw->presentButtonB(field_0x148[0], field_0x150[0], field_0x148[1], field_0x150[1], 1.0f); + mpMeterDraw->drawButtonCross(mPresentationTargets.crossX, field_0x15c); + } +} + +void dMeter2_c::presentMap() { + if (mpMeterDraw == NULL || mpMap == NULL) { + return; + } + mpMeterDraw->presentMapAlpha(mpMap); +} diff --git a/src/d/d_meter_button.cpp b/src/d/d_meter_button.cpp index ac11d183ce..068fcf3b53 100644 --- a/src/d/d_meter_button.cpp +++ b/src/d/d_meter_button.cpp @@ -19,9 +19,29 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/user_interface.h" #include "dusk/version.hpp" #include "helpers/string.hpp" + +namespace { +bool advance_repeat_hit(f32& frame, u8& cycle, f32 period, f32& elapsedFrames) { + const f32 frames = dusk::game_clock::original_frames(); + elapsedFrames = frames; + if (period <= 0.0f || frames <= 0.0f) { + return false; + } + f32 phase = frame + cycle * period; + const bool pulse = phase == 0.0f || phase + frames > 2.0f * period; + dusk::vdt::advance_looping_frame(phase, 1.0f, 2.0f * period); + if (pulse) { + elapsedFrames = phase == 0.0f ? 2.0f * period : phase; + } + cycle = phase >= period; + frame = phase - cycle * period; + return pulse; +} +} // namespace #endif #if TARGET_PC || VERSION == VERSION_GCN_JPN @@ -51,6 +71,10 @@ int dMeterButton_c::_execute(u32 i_flags, bool i_drawA, bool i_drawB, bool i_dra bool i_draw3DB, bool i_drawNURE, bool i_drawReel, bool i_drawReel2, bool i_drawAB, bool i_drawTate, bool i_drawNunZ, bool i_drawNunC, bool i_drawBin) { +#if TARGET_PC + mBlinkButton = g_meter2_info.mBlinkButton; + dMeter2Info_resetBlinkButton(); +#endif updateButton(); updateText(i_flags); field_0x4b0 = 0; @@ -228,6 +252,7 @@ int dMeterButton_c::_execute(u32 i_flags, bool i_drawA, bool i_drawB, bool i_dra } } +#if !TARGET_PC for (int i = 0; i < 2; i++) { if (field_0x2f4[i] != 0.0f) { field_0x2fc[i] = field_0x2f4[i]; @@ -241,6 +266,7 @@ int dMeterButton_c::_execute(u32 i_flags, bool i_drawA, bool i_drawB, bool i_dra trans_button(i, field_0x2fc[i]); } +#endif return 1; } @@ -248,6 +274,23 @@ void dMeterButton_c::draw() { J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort(); graf_ctx->setup2D(); +#if TARGET_PC + if (isButtonShowBit(BUTTON_Z_e) && mpButtonZ->getAlphaTimer() < 5.0f) { + mpButtonZ->setAlphaRate(1.0f); + dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonZ, 5); + } + + for (int i = 0; i < 2; i++) { + if (field_0x2f4[i] != 0.0f) { + field_0x2fc[i] = field_0x2f4[i]; + } else { + dusk::vdt::present_chase(&field_0x2fc[i], field_0x2f4[i], 1.0f, 10.0f, 0.1f); + } + + trans_button(i, field_0x2fc[i]); + } +#endif + mpButtonScreen->draw(0.0f, 0.0f, graf_ctx); if (field_0x00c != NULL) { field_0x00c->draw(0.0f, 0.0f, graf_ctx); @@ -281,20 +324,28 @@ void dMeterButton_c::draw() { SAFE_STRCPY(static_cast(mpTm_c[0]->getPanePtr())->getStringPtr(), tmp_buf); } +#if TARGET_PC + const bool blinkA = (mBlinkButton & 1) != 0; + const bool blinkB = (mBlinkButton & 2) != 0; +#endif + for (int i = 0; i < 2; i++) { bool var_r22 = 0; bool var_r23 = 0; + IF_DUSK(f32 repeatFrames = 0.0f); if (field_0x4be[i] == BUTTON_A_e && dComIfGp_isDoSetFlag(4)) { var_r23 = 1; - if (field_0x4b8[i] == 0 && field_0x4bc[i] == 0) { + if (DUSK_IF_ELSE(advance_repeat_hit(field_0x4b8[i], field_0x4bc[i], g_drawHIO.mEmpButton.mRepeatHitFrameNum, repeatFrames), + field_0x4b8[i] == 0 && field_0x4bc[i] == 0)) + { field_0x2e8[i] = 18.0f - g_drawHIO.mEmpButton.mPikariRepeatHitAnimSpeed; } s16 temp_r6 = g_drawHIO.mEmpButton.mRepeatHitFrameNum; s16 temp_r6_2 = g_drawHIO.mEmpButton.mRepeatHitFrameNum / 2; - IF_DUSK_BLOCK(dusk::frame_interp::get_ui_tick_pending()) +#if !TARGET_PC field_0x4b8[i]++; if (field_0x4b8[i] >= temp_r6) { @@ -306,7 +357,7 @@ void dMeterButton_c::draw() { field_0x4bc[i] = 0; } } - IF_DUSK_BLOCK_END +#endif f32 var_f2; if (temp_r6_2 < field_0x4b8[i]) { @@ -320,9 +371,9 @@ void dMeterButton_c::draw() { } else if ((field_0x4be[i] == BUTTON_A_e && (dComIfGp_getDoStatus() == 0x3B || dComIfGp_getDoStatus() == 0x3F || dComIfGp_getDoStatus() == 0x40) && - dMeter2Info_isBlinkButton(1)) || + DUSK_IF_ELSE(blinkA, dMeter2Info_isBlinkButton(1))) || (field_0x4be[i] == BUTTON_B_e && dComIfGp_getAStatus() == 0x3A && - dMeter2Info_isBlinkButton(2))) + DUSK_IF_ELSE(blinkB, dMeter2Info_isBlinkButton(2)))) { var_r22 = 1; if (field_0x2e8[i] <= 0.0f) { @@ -378,18 +429,8 @@ void dMeterButton_c::draw() { } if (var_r3) { -#if 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 IF_DUSK(&& dusk::frame_interp::get_ui_tick_pending())) - { + if (field_0x2e8[i] == 18.0f) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON_BLINK, NULL, 0, 0); } @@ -419,7 +460,7 @@ void dMeterButton_c::draw() { g_drawHIO.mEmpButton.mPikariRepeatHitFrontInner, g_drawHIO.mEmpButton.mPikariRepeatHitBackOuter, g_drawHIO.mEmpButton.mPikariRepeatHitBackInner, - g_drawHIO.mEmpButton.mPikariRepeatHitAnimSpeed, 0); + g_drawHIO.mEmpButton.mPikariRepeatHitAnimSpeed, 0 IF_DUSK_ARG(repeatFrames)); } else if (isFastSet(i)) { dMeter2Info_getMeterClass()->getMeterDrawPtr()->drawPikari( vtx.x, vtx.y, &field_0x2e8[i], g_drawHIO.mEmpButton.mPikariFastScale, @@ -441,7 +482,7 @@ void dMeterButton_c::draw() { } } - dMeter2Info_resetBlinkButton(); + IF_NOT_DUSK(dMeter2Info_resetBlinkButton()); } int dMeterButton_c::_delete() { @@ -2075,7 +2116,7 @@ void dMeterButton_c::setAlphaButtonAAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonA, 5); } - if (mpButtonA->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_A_e]) { + if (DUSK_IF_ELSE(true, mpButtonA->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_A_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_A_e] = true; } @@ -2111,7 +2152,7 @@ void dMeterButton_c::setAlphaButtonBAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonB, 5); } - if (mpButtonB->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_B_e]) { + if (DUSK_IF_ELSE(true, mpButtonB->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_B_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_B_e] = true; } @@ -2147,7 +2188,7 @@ void dMeterButton_c::setAlphaButtonRAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonR, 5); } - if (mpButtonR->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_R_e]) { + if (DUSK_IF_ELSE(true, mpButtonR->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_R_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_R_e] = true; } @@ -2166,7 +2207,7 @@ void dMeterButton_c::setAlphaButtonZAnimeMin() { mpButtonZ->setAlphaRate(0.0f); mpButtonZ->alphaAnimeStart(0); - if (mpButtonZ->getAlphaRate() == 0.0f) { + if (DUSK_IF_ELSE(true, mpButtonZ->getAlphaRate() == 0.0f)) { hide_button(BUTTON_Z_e); mPlayedButtonSound[BUTTON_Z_e] = false; } @@ -2178,12 +2219,14 @@ void dMeterButton_c::setAlphaButtonZAnimeMax() { if (dComIfGp_isZSetFlag(1)) { mpButtonZ->setAlphaRate(1.0f); mpButtonZ->alphaAnimeStart(5); +#if !TARGET_PC } else { mpButtonZ->setAlphaRate(1.0f); dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonZ, 5); +#endif } - if (mpButtonZ->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_Z_e]) { + if (DUSK_IF_ELSE(true, mpButtonZ->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_Z_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_Z_e] = true; } @@ -2220,7 +2263,7 @@ void dMeterButton_c::setAlphaButton3DAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButton3D, 5); } - if (mpButton3D->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_3D_e]) { + if (DUSK_IF_ELSE(true, mpButton3D->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_3D_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_3D_e] = true; } @@ -2264,7 +2307,7 @@ void dMeterButton_c::setAlphaButtonCAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonC, 5); } - if (mpButtonC->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_C_e]) { + if (DUSK_IF_ELSE(true, mpButtonC->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_C_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_C_e] = true; } @@ -2329,7 +2372,7 @@ void dMeterButton_c::setAlphaButtonSAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonS, 5); } - if (mpButtonS->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_S_e]) { + if (DUSK_IF_ELSE(true, mpButtonS->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_S_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_S_e] = true; } @@ -2365,7 +2408,7 @@ void dMeterButton_c::setAlphaButtonXAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonX, 5); } - if (mpButtonX->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_X_e]) { + if (DUSK_IF_ELSE(true, mpButtonX->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_X_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_X_e] = true; } @@ -2401,7 +2444,7 @@ void dMeterButton_c::setAlphaButtonYAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonY, 5); } - if (mpButtonY->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_Y_e]) { + if (DUSK_IF_ELSE(true, mpButtonY->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_Y_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_Y_e] = true; } @@ -2440,7 +2483,7 @@ void dMeterButton_c::setAlphaButtonNunAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonNun, 5); } - if (mpButtonNun->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_NUN_e]) { + if (DUSK_IF_ELSE(true, mpButtonNun->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_NUN_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_NUN_e] = true; } @@ -2480,7 +2523,7 @@ void dMeterButton_c::setAlphaButtonRemoAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonRemo, 5); } - if (mpButtonRemo->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_REMO_e]) { + if (DUSK_IF_ELSE(true, mpButtonRemo->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_REMO_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_REMO_e] = true; } @@ -2520,7 +2563,7 @@ void dMeterButton_c::setAlphaButtonRemo2AnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonRemo2, 5); } - if (mpButtonRemo2->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_REMO2_e]) { + if (DUSK_IF_ELSE(true, mpButtonRemo2->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_REMO2_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_REMO2_e] = true; } @@ -2562,7 +2605,7 @@ void dMeterButton_c::setAlphaButtonARAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonAR, 5); } - if (mpButtonAR->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_AR_e]) { + if (DUSK_IF_ELSE(true, mpButtonAR->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_AR_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_AR_e] = true; } @@ -2604,7 +2647,7 @@ void dMeterButton_c::setAlphaButton3DBAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButton3DB, 5); } - if (mpButton3DB->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_3DB_e]) { + if (DUSK_IF_ELSE(true, mpButton3DB->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_3DB_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_3DB_e] = true; } @@ -2646,7 +2689,7 @@ void dMeterButton_c::setAlphaButtonNUREAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonNURE, 5); } - if (mpButtonNURE->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_NURE_e]) { + if (DUSK_IF_ELSE(true, mpButtonNURE->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_NURE_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_NURE_e] = true; } @@ -2688,7 +2731,7 @@ void dMeterButton_c::setAlphaButtonReelAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonReel, 5); } - if (mpButtonReel->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_REEL_e]) { + if (DUSK_IF_ELSE(true, mpButtonReel->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_REEL_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_REEL_e] = true; } @@ -2720,7 +2763,7 @@ void dMeterButton_c::setAlphaButtonReel2AnimeMax() { mpButtonReel2->setAlphaRate(1.0f); mpButtonReel2->alphaAnimeStart(5); - if (mpButtonReel2->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_REEL2_e]) { + if (DUSK_IF_ELSE(true, mpButtonReel2->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_REEL2_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_REEL2_e] = true; } @@ -2752,7 +2795,7 @@ void dMeterButton_c::setAlphaButtonABAnimeMax() { mpButtonAB->setAlphaRate(1.0f); mpButtonAB->alphaAnimeStart(5); - if (mpButtonAB->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_AB_e]) { + if (DUSK_IF_ELSE(true, mpButtonAB->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_AB_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_AB_e] = true; } @@ -2784,7 +2827,7 @@ void dMeterButton_c::setAlphaButtonTateAnimeMax() { mpButtonTate->setAlphaRate(1.0f); mpButtonTate->alphaAnimeStart(5); - if (mpButtonTate->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_TATE_e]) { + if (DUSK_IF_ELSE(true, mpButtonTate->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_TATE_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_TATE_e] = true; } @@ -2826,7 +2869,7 @@ void dMeterButton_c::setAlphaButtonNunZAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonNunZ, 5); } - if (mpButtonNunZ->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_NUNZ_e]) { + if (DUSK_IF_ELSE(true, mpButtonNunZ->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_NUNZ_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_NUNZ_e] = true; } @@ -2868,7 +2911,7 @@ void dMeterButton_c::setAlphaButtonNunCAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonNunC, 5); } - if (mpButtonNunC->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_NUNC_e]) { + if (DUSK_IF_ELSE(true, mpButtonNunC->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_NUNC_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_NUNC_e] = true; } @@ -2910,7 +2953,7 @@ void dMeterButton_c::setAlphaButtonBinAnimeMax() { dMeter2Info_getMeterClass()->getMeterDrawPtr()->setAlphaAnimeMax(mpButtonBin, 5); } - if (mpButtonBin->getAlphaRate() == 1.0f && !mPlayedButtonSound[BUTTON_BIN_e]) { + if (DUSK_IF_ELSE(true, mpButtonBin->getAlphaRate() == 1.0f) && !mPlayedButtonSound[BUTTON_BIN_e]) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON, NULL, 0, 0); mPlayedButtonSound[BUTTON_BIN_e] = true; } diff --git a/src/d/d_meter_haihai.cpp b/src/d/d_meter_haihai.cpp index 5ea5dad5b7..6eadfca9d9 100644 --- a/src/d/d_meter_haihai.cpp +++ b/src/d/d_meter_haihai.cpp @@ -11,7 +11,10 @@ #include "d/d_com_inf_game.h" #include "d/d_meter_HIO.h" #include "d/d_pane_class.h" -#include "dusk/frame_interpolation.h" + +#if TARGET_PC +#include "dusk/interp/user_interface.h" +#endif dMeterHaihai_c::dMeterHaihai_c(u8 i_type) { mType = i_type; @@ -56,12 +59,13 @@ int dMeterHaihai_c::_create() { int dMeterHaihai_c::_execute(u32 i_flags) { mFlags = i_flags; - updateHaihai(); + DUSK_IF_ELSE(setScale(1.0f), updateHaihai()); alphaAnimeHaihai(i_flags); return 1; } void dMeterHaihai_c::draw() { + IF_DUSK(updateHaihai()); if ((mFlags & 8) || (mFlags & 0x10) || (mFlags & 0x1000000) || (mFlags & 0x800000) || (mFlags & 0x20) || (mFlags & 0x4000000)) { @@ -129,6 +133,7 @@ void dMeterHaihai_c::draw() { } void dMeterHaihai_c::drawHaihai(u8 i_direction) { + IF_DUSK(updateHaihai()); f32 center_x = FB_WIDTH_BASE / 2; f32 center_y = FB_HEIGHT_BASE / 2; @@ -184,6 +189,7 @@ void dMeterHaihai_c::drawHaihai(u8 i_direction) { } void dMeterHaihai_c::drawHaihai(u8 i_direction, f32 i_posX, f32 i_posY, f32 param_3, f32 param_4) { + IF_DUSK(updateHaihai()); if (mType == 1) { i_posX += (3.0f + g_drawHIO.mScrollArrowCenterPosX); i_posY += (g_drawHIO.mScrollArrowCenterPosY - 5.0f); @@ -281,31 +287,36 @@ void dMeterHaihai_c::updateHaihai() { playBtkAnime(mpCursorBtk); playBpkAnime(mpCursorBpk); mpHaihaiScreen->animation(); - setScale(1.0f); + IF_NOT_DUSK(setScale(1.0f)); } void dMeterHaihai_c::playBckAnime(J2DAnmTransformKey* i_bck) { if (checkPlayAnime(1)) { if (i_bck != NULL) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mType == 4) { - mBckFrame += g_drawHIO.mWiiLockArrowBCKAnimSpeed; - } else { - mBckFrame += g_drawHIO.mScrollArrowBCKAnimSpeed; - } - - if (mBckFrame >= i_bck->getFrameMax()) { - mBckFrame -= i_bck->getFrameMax(); - } +#if TARGET_PC + f32 speed; + if (mType == 4) { + speed = g_drawHIO.mWiiLockArrowBCKAnimSpeed; + } else { + speed = g_drawHIO.mScrollArrowBCKAnimSpeed; } + dusk::vdt::present_looping(mBckFrame, i_bck, speed); +#else + if (mType == 4) { + mBckFrame += g_drawHIO.mWiiLockArrowBCKAnimSpeed; + } else { + mBckFrame += g_drawHIO.mScrollArrowBCKAnimSpeed; + } + + if (mBckFrame >= i_bck->getFrameMax()) { + mBckFrame -= i_bck->getFrameMax(); + } +#endif } else { mBtkFrame = 1.0f; } - i_bck->setFrame(mBckFrame); + IF_NOT_DUSK(i_bck->setFrame(mBckFrame)); mpHaihaiScreen->search(MULTI_CHAR('n_anim'))->setAnimation(i_bck); mpHaihaiScreen->search(MULTI_CHAR('n_anim'))->animationTransform(); mpHaihaiScreen->search(MULTI_CHAR('n_anim'))->setAnimation((J2DAnmTransform*)NULL); @@ -315,25 +326,30 @@ void dMeterHaihai_c::playBckAnime(J2DAnmTransformKey* i_bck) { void dMeterHaihai_c::playBtkAnime(J2DAnmTextureSRTKey* i_btk) { if (checkPlayAnime(2)) { if (i_btk != NULL) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mType == 4) { - mBtkFrame += g_drawHIO.mWiiLockArrowBTKAnimSpeed; - } else { - mBtkFrame += g_drawHIO.mScrollArrowBTKAnimSpeed; - } - - if (mBtkFrame >= i_btk->getFrameMax()) { - mBtkFrame -= i_btk->getFrameMax(); - } +#if TARGET_PC + f32 speed; + if (mType == 4) { + speed = g_drawHIO.mWiiLockArrowBTKAnimSpeed; + } else { + speed = g_drawHIO.mScrollArrowBTKAnimSpeed; } + dusk::vdt::present_looping(mBtkFrame, i_btk, speed); +#else + if (mType == 4) { + mBtkFrame += g_drawHIO.mWiiLockArrowBTKAnimSpeed; + } else { + mBtkFrame += g_drawHIO.mScrollArrowBTKAnimSpeed; + } + + if (mBtkFrame >= i_btk->getFrameMax()) { + mBtkFrame -= i_btk->getFrameMax(); + } +#endif } else { mBtkFrame = 1.0f; } - i_btk->setFrame(mBtkFrame); + IF_NOT_DUSK(i_btk->setFrame(mBtkFrame)); mpHaihaiScreen->search(MULTI_CHAR('yaji00'))->setAnimation(i_btk); mpHaihaiScreen->search(MULTI_CHAR('yaji01'))->setAnimation(i_btk); } @@ -342,25 +358,30 @@ void dMeterHaihai_c::playBtkAnime(J2DAnmTextureSRTKey* i_btk) { void dMeterHaihai_c::playBpkAnime(J2DAnmColor* i_bpk) { if (checkPlayAnime(0)) { if (i_bpk != NULL) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mType == 4) { - mBpkFrame += g_drawHIO.mWiiLockArrowBPKAnimSpeed; - } else { - mBpkFrame += g_drawHIO.mScrollArrowBPKAnimSpeed; - } - - if (mBpkFrame >= i_bpk->getFrameMax()) { - mBpkFrame -= i_bpk->getFrameMax(); - } +#if TARGET_PC + f32 speed; + if (mType == 4) { + speed = g_drawHIO.mWiiLockArrowBPKAnimSpeed; + } else { + speed = g_drawHIO.mScrollArrowBPKAnimSpeed; } + dusk::vdt::present_looping(mBpkFrame, i_bpk, speed); +#else + if (mType == 4) { + mBpkFrame += g_drawHIO.mWiiLockArrowBPKAnimSpeed; + } else { + mBpkFrame += g_drawHIO.mScrollArrowBPKAnimSpeed; + } + + if (mBpkFrame >= i_bpk->getFrameMax()) { + mBpkFrame -= i_bpk->getFrameMax(); + } +#endif } else { mBpkFrame = 1.0f; } - i_bpk->setFrame(mBpkFrame); + IF_NOT_DUSK(i_bpk->setFrame(mBpkFrame)); mpHaihaiScreen->search(MULTI_CHAR('npc_l1'))->setAnimation(i_bpk); mpHaihaiScreen->search(MULTI_CHAR('yaji_l'))->setAnimation(i_bpk); } diff --git a/src/d/d_meter_hakusha.cpp b/src/d/d_meter_hakusha.cpp index aa03e1755a..a9fc1421ad 100644 --- a/src/d/d_meter_hakusha.cpp +++ b/src/d/d_meter_hakusha.cpp @@ -15,6 +15,10 @@ #include "d/d_pane_class.h" #include +#if TARGET_PC +#include "dusk/interp/user_interface.h" +#endif + dMeterHakusha_c::dMeterHakusha_c(void* i_screen) { field_0x004 = (J2DScreen*)i_screen; _create(); @@ -115,6 +119,20 @@ void dMeterHakusha_c::draw() { mpButtonScreen->draw(0.0f, 0.0f, graf_ctx); for (int i = 0; i < getHakushaNum(); i++) { +#if TARGET_PC + if (mHakushaAnimFrame[i] > 0.0f) { + if (mHakushaStatus[i] == 0) { + dusk::vdt::advance_toward_frame(mHakushaAnimFrame[i], 28.0f, g_drawHIO.mSpurIconPikariAnimSpeed); + } else { + dusk::vdt::advance_toward_frame(mHakushaAnimFrame[i], 28.0f, g_drawHIO.mSpurIconRevivePikariAnimSpeed); + } + + if (mHakushaAnimFrame[i] >= 28.0f) { + mHakushaAnimFrame[i] = 0.0f; + } + } +#endif + if (mHakushaData[i].flags & 1) { mpHakushaOn->show(); } else { @@ -226,6 +244,7 @@ void dMeterHakusha_c::updateHakusha() { } for (int i = 0; i < getHakushaNum(); i++) { +#if !TARGET_PC if (mHakushaAnimFrame[i] > 0.0f) { if (mHakushaStatus[i] == 0) { mHakushaAnimFrame[i] += g_drawHIO.mSpurIconPikariAnimSpeed; @@ -237,6 +256,7 @@ void dMeterHakusha_c::updateHakusha() { mHakushaAnimFrame[i] = 0.0f; } } +#endif mHakushaData[i].pos_x = abtn_x_offset; mHakushaData[i].pos_y = abtn_y_offset; diff --git a/src/d/d_meter_map.cpp b/src/d/d_meter_map.cpp index d526d7877f..e5fef0783e 100644 --- a/src/d/d_meter_map.cpp +++ b/src/d/d_meter_map.cpp @@ -16,14 +16,19 @@ #include "f_op/f_op_overlap_mng.h" #include "m_Do/m_Do_controller_pad.h" #include "d/d_camera.h" -#if TARGET_PC -#include "dusk/settings.h" -#include -#endif #include #if TARGET_PC #include "dusk/action_bindings.h" +#include "dusk/game_clock.h" +#include "dusk/interp/user_interface.h" +#include "dusk/settings.h" + +#include "d/d_meter2.h" + +#include "JSystem/JUtility/JUTPalette.h" + +#include namespace { @@ -489,15 +494,18 @@ void dMeterMap_c::_create(J2DScreen* unused) { if (mMapIsInside != 0) { mMapIsInside = 1; mSlidePositionOffset = getDispPosInside_OffsetX(); + IF_DUSK(mSlidePositionOffsetTarget = mSlidePositionOffset); dMeter2Info_setMapStatus(1); } else { mMapIsInside = 0; mSlidePositionOffset = getDispPosOutSide_OffsetX(); + IF_DUSK(mSlidePositionOffsetTarget = mSlidePositionOffset); dMeter2Info_setMapStatus(0); } field_0x2e = 0; mSlidePositionOffset = 0; + IF_DUSK(mSlidePositionOffsetTarget = 0); field_0x30 = 0; /* dSv_event_flag_c::M_085 - Twilight Hyrule Field - Midna dialogue right before Boss Bug's Tear of Light appears */ field_0x2b = dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[118]); @@ -506,11 +514,13 @@ void dMeterMap_c::_create(J2DScreen* unused) { void dMeterMap_c::setDispPosOutSide() { mMapIsInside = 0; mSlidePositionOffset = getDispPosOutSide_OffsetX(); + IF_DUSK(mSlidePositionOffsetTarget = mSlidePositionOffset); } void dMeterMap_c::setDispPosInSide() { mMapIsInside = 1; mSlidePositionOffset = getDispPosInside_OffsetX(); + IF_DUSK(mSlidePositionOffsetTarget = mSlidePositionOffset); } void dMeterMap_c::_delete() { @@ -572,6 +582,8 @@ void dMeterMap_c::_move(u32 param_0) { } if (mMapIsInside != 0) { + IF_DUSK(mSlidePositionOffsetTarget = getDispPosInside_OffsetX()); +#if !TARGET_PC if (mSlidePositionOffset != getDispPosInside_OffsetX()) { if (!cLib_addCalcAngleS(&mSlidePositionOffset, getDispPosInside_OffsetX(), 2, 60, 10)) { #if DEBUG @@ -579,8 +591,10 @@ void dMeterMap_c::_move(u32 param_0) { #endif } } +#endif } else { - cLib_addCalcAngleS(&mSlidePositionOffset, getDispPosOutSide_OffsetX(), 2, 60, 10); + IF_DUSK(mSlidePositionOffsetTarget = getDispPosOutSide_OffsetX()); + IF_NOT_DUSK(cLib_addCalcAngleS(&mSlidePositionOffset, getDispPosOutSide_OffsetX(), 2, 60, 10)); } Vec map_pos = dMapInfo_n::getMapPlayerPos(); @@ -622,6 +636,13 @@ void dMeterMap_c::_draw() { } void dMeterMap_c::draw() { +#if TARGET_PC + if (dusk::game_clock::is_presentation_frame()) { + if (dMeter2_c* meter = dMeter2Info_getMeterClass()) { + meter->presentMap(); + } + } +#endif if ( #if DEBUG !g_meter_mapHIO.mMapDisplayProhibited && @@ -635,6 +656,12 @@ void dMeterMap_c::draw() { f32 drawPosX = mDrawPosX; f32 drawPosY = mDrawPosY; +#if TARGET_PC + dusk::vdt::present_addCalc(&mSlidePositionOffset, mSlidePositionOffsetTarget, 0.5f, 60.0f, 10.0f); + mDrawPosX = mSlidePositionOffset + getMapDispEdgeLeftX_Layout(); + drawPosX = mDrawPosX; +#endif + u8 alpha = mMapAlpha; #if DEBUG if (g_meter_mapHIO.mMapAlphaTest) { @@ -644,6 +671,11 @@ void dMeterMap_c::draw() { mMapJ2DPicture->setAlpha(alpha); #if TARGET_PC + // Ensure minimap palette gets reuploaded since it is modified for the pulsating border + // effect. + JUTPalette* pPalette = mMapJ2DPicture->getTexture(0)->getPalette(); + pPalette->dataUploaded(); + // Scale the minimap with the user HUD scale and shift down so its bottom-left // corner stays anchored to the same screen position as at scale 1.0. const f32 userHudScale = dGetUserHudScale(); diff --git a/src/d/d_meter_string.cpp b/src/d/d_meter_string.cpp index c99e7c564e..0e19fa1c04 100644 --- a/src/d/d_meter_string.cpp +++ b/src/d/d_meter_string.cpp @@ -16,9 +16,12 @@ #include "d/d_meter2_info.h" #include "d/d_meter_HIO.h" #include "d/d_pane_class.h" -#include "dusk/frame_interpolation.h" #include +#if TARGET_PC +#include "dusk/interp/user_interface.h" +#endif + dMeterString_c::dMeterString_c(int i_stringID) { mpMapArchive = dComIfGp_getAllMapArchive(); field_0x28 = 0; @@ -107,26 +110,19 @@ void dMeterString_c::draw() { if (mAnimFrame < 60.0f) { #if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - mAnimFrame += g_drawHIO.mMiniGame.mReadyFightTextAnimSpeed; - if (mAnimFrame > 60.0f) { - mAnimFrame = 60.0f; - } + dusk::vdt::advance_toward_frame(mAnimFrame, 60.0f, g_drawHIO.mMiniGame.mReadyFightTextAnimSpeed); +#else + mAnimFrame += g_drawHIO.mMiniGame.mReadyFightTextAnimSpeed; + if (mAnimFrame > 60.0f) { + mAnimFrame = 60.0f; } +#endif playBckAnimation(mAnimFrame); } else if (mAnimFrame < (f32)g_drawHIO.mMiniGame.mReadyFightTextWaitFrames + 60.0f) { -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - mAnimFrame += var_f30; + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(mAnimFrame, (f32)g_drawHIO.mMiniGame.mReadyFightTextWaitFrames + 60.0f, 1.0f), mAnimFrame += var_f30); } else if (mAnimFrame < var_f31) { -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - mAnimFrame += var_f30; + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(mAnimFrame, var_f31, 1.0f), mAnimFrame += var_f30); var_f30 = acc(g_drawHIO.mMiniGame.field_0x172, var_f31 - mAnimFrame, 0); } @@ -140,21 +136,13 @@ void dMeterString_c::draw() { if (mPikariAnimFrame > 0.0f) { drawPikari(); } else if (mPikariAnimFrame == -1.0f && -#if TARGET_PC - dusk::frame_interp::get_ui_tick_pending() && -#endif mAnimFrame > g_drawHIO.mMiniGame.mReadyFightPikariAppearFrames) { mPikariAnimFrame = 18.0f - g_drawHIO.mMiniGame.mReadyFightPikariAnimSpeed; } -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mAnimFrame >= var_f31) { - dMeter2Info_resetMeterString(); - } + if (mAnimFrame >= var_f31) { + dMeter2Info_resetMeterString(); } } } diff --git a/src/d/d_model.cpp b/src/d/d_model.cpp index bdff472367..c236b5f2db 100644 --- a/src/d/d_model.cpp +++ b/src/d/d_model.cpp @@ -6,7 +6,7 @@ #include "d/d_com_inf_game.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif void dMdl_c::draw() { @@ -52,7 +52,7 @@ void dMdl_c::entryObj(dMdl_obj_c* i_obj) { #ifdef TARGET_PC // if field_0x1a is false, this dMdl_c is not in the drawlist // if true, we need to make sure with interp enabled - if (dusk::frame_interp::is_enabled() && field_0x1a) { + if (dusk::interp::is_enabled() && field_0x1a) { auto pkt = dComIfGd_getListPacket()->mpBuffer[0]; while (pkt && pkt != this) { pkt = pkt->getNextPacket(); diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index 0c2c3cf358..e0f9584a9c 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -13,6 +13,7 @@ #include "JSystem/JUtility/JUTFont.h" #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/menu_pointer.h" #include "dusk/scope_guard.hpp" #include "dusk/version.hpp" @@ -907,6 +908,16 @@ bool jmessage_tReference::isLightEnd() { return false; } +#if TARGET_PC +void jmessage_tReference::addCharAlpha() { + dusk::vdt::advance_toward_frame(mCharAlpha, 255.0f, mAddCharAlpha); +} + +void jmessage_tReference::addCharAllAlphaRate() { + dusk::vdt::advance_toward_frame(mCharAllAlphaRate, 1.0f, mAddCharAllAlphaRate); +} +#endif + void jmessage_tReference::decideOutFontRupeeColor(int i_inputVal) { int price_diff = getObjectPtr()->getNowTotalPrice() - getObjectPtr()->getNowTotalPayment(); diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index 072bd5039b..0157d2521f 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -30,9 +30,7 @@ #include "m_Do/m_Do_lib.h" #if TARGET_PC -#include -#include -#include +#include "dusk/interp/user_interface.h" #include "dusk/language.hpp" #include "dusk/logging.h" #include "dusk/menu_pointer.h" @@ -40,6 +38,10 @@ #include "dusk/mods/svc/flow.hpp" #include "dusk/settings.h" #include "dusk/version.hpp" + +#include +#include +#include #endif static void dMsgObject_addFundRaising(s16 param_0); @@ -569,6 +571,97 @@ int dMsgObject_c::_draw() { return 1; } +#if TARGET_PC +void dMsgObject_c::presentAnims() { + if (mpScrnDraw == NULL || mpOutFont == NULL) { + return; + } + + const u16 status = getStatusLocal(); + f32 target = 0.0f; + if (status == 2) { + if (isPlaceMessage() || isStaffMessage()) { + target = g_MsgObject_HIO_c.mStageTitleFadeIn; + } else if (isBossMessage()) { + target = g_MsgObject_HIO_c.mBossNameFadeIn; + } else if (isBookMessage()) { + target = g_MsgObject_HIO_c.mBoxAppearFrame + g_MsgObject_HIO_c.mWaitFrame + + g_MsgObject_HIO_c.mLightAppearFrame; + } else { + target = g_MsgObject_HIO_c.mBoxAppearFrame; + } + dusk::vdt::advance_toward_frame(field_0x16a, target, 1.0f); + if (isKanbanMessage() || isPlaceMessage() || isStaffMessage() || isBossMessage()) { + f32 ratio = dusk::vdt::clamped_fraction(field_0x16a, target); + mpScrnDraw->fukiScale(ratio); + mpScrnDraw->fukiAlpha(ratio); + mpOutFont->setAlphaRatio(ratio); + } else if (isBookMessage()) { + if (field_0x16a <= g_MsgObject_HIO_c.mBoxAppearFrame) { + f32 ratio = dusk::vdt::clamped_fraction(field_0x16a, g_MsgObject_HIO_c.mBoxAppearFrame); + mpScrnDraw->fukiAlpha(ratio); + mpOutFont->setAlphaRatio(ratio); + } else { + mpScrnDraw->fukiAlpha(1.0f); + mpOutFont->setAlphaRatio(1.0f); + } + s16 waitEnd = g_MsgObject_HIO_c.mBoxAppearFrame + g_MsgObject_HIO_c.mWaitFrame; + if (field_0x16a >= waitEnd && + field_0x16a <= waitEnd + g_MsgObject_HIO_c.mLightAppearFrame) + { + f32 scale = dusk::vdt::clamped_fraction(field_0x16a - waitEnd, g_MsgObject_HIO_c.mLightAppearFrame); + mpScrnDraw->fukiScale(scale); + } + } else { + f32 ratio = dusk::vdt::clamped_fraction(field_0x16a, target); + mpScrnDraw->fukiScale(1.0f); + mpScrnDraw->fukiAlpha(ratio); + mpOutFont->setAlphaRatio(ratio); + } + } else if (status == 17) { + if (isKanbanMessage() || isBookMessage()) { + target = g_MsgObject_HIO_c.field_0x304; + } else if (isPlaceMessage() || isStaffMessage()) { + target = g_MsgObject_HIO_c.mStageTitleFadeOut; + } else if (isBossMessage()) { + target = g_MsgObject_HIO_c.mBossNameFadeOut; + } else { + target = 5.0f; + } + dusk::vdt::advance_toward_frame(field_0x16a, target, 1.0f); + f32 ratio = dusk::vdt::clamped_fraction(field_0x16a, target); + mpScrnDraw->fukiAlpha(1.0f - ratio); + if (isBookMessage()) { + mpScrnDraw->fukiScale(1.0f - ratio); + } + mpOutFont->setAlphaRatio(1.0f - ratio); + } else if (status == 6) { + if (isBookMessage() && field_0x16a > 0) { + dusk::vdt::advance_toward_frame(field_0x16a, 0.0f, 1.0f); + f32 alpha = (10 - field_0x16a) / 10.0f; + mpScrnDraw->fontAlpha(alpha); + mpOutFont->setAlphaRatio(alpha); + } + jmessage_tReference* pRef = (jmessage_tReference*)mpRenProc->getReference(); + if (pRef->getCharAllAlphaRate() < 1.0f) { + pRef->addCharAllAlphaRate(); + f32 alpha = pRef->getCharAllAlphaRate(); + mpScrnDraw->setCharAlphaRate(alpha); + mpOutFont->setAlphaRatio(alpha); + } + } else if (status == 5 && isBookMessage() && + (isSend() || field_0x16a > 0)) + { + dusk::vdt::advance_toward_frame(field_0x16a, 10.0f, 1.0f); + if (field_0x16a <= 10) { + f32 alpha = (10 - field_0x16a) / 10.0f; + mpScrnDraw->fontAlpha(alpha); + mpOutFont->setAlphaRatio(alpha); + } + } +} +#endif + int dMsgObject_c::_delete() { mpResCont->destroyResource_all(); if (mpScrnDraw != NULL) { @@ -961,7 +1054,7 @@ void dMsgObject_c::waitProc() { void dMsgObject_c::openProc() { if (isMidonaMessage()) { bool uVar12 = 0; - if (field_0x16a == 0) { + if (DUSK_IF_ELSE(!mpScrnDraw->isSelectAnimeActive(), field_0x16a == 0)) { jmessage_tReference* pRef = (jmessage_tReference*)mpRenProc->getReference(); field_0x1a3 = 0; if (mpRefer->getMsgID() == 0x7fa) { @@ -995,7 +1088,7 @@ void dMsgObject_c::openProc() { } } } - field_0x16a++; + IF_NOT_DUSK(field_0x16a++); s16 sVar7; if (isKanbanMessage()) { sVar7 = g_MsgObject_HIO_c.mBoxAppearFrame; @@ -1062,12 +1155,12 @@ void dMsgObject_c::openProc() { void dMsgObject_c::outnowProc() { mpRefer->shiftCharCountBuffer(); - if (isBookMessage() && field_0x16a > 0) { - field_0x16a--; + if (isBookMessage() && field_0x16a DUSK_IF_ELSE(>, !=) 0) { + IF_NOT_DUSK(field_0x16a--); f32 alpha = (10 - field_0x16a) / 10.0f; mpScrnDraw->fontAlpha(alpha); mpOutFont->setAlphaRatio(alpha); - if (field_0x16a > 0) { + if (field_0x16a DUSK_IF_ELSE(>, !=) 0) { return; } } @@ -1077,7 +1170,7 @@ void dMsgObject_c::outnowProc() { if (mDoCPd_c::getTrigA(0)) { pRef->setCharAllAlphaRate(1.0f); } else { - pRef->addCharAllAlphaRate(); + IF_NOT_DUSK(pRef->addCharAllAlphaRate()); } f32 alpha = pRef->getCharAllAlphaRate(); mpScrnDraw->setCharAlphaRate(alpha); @@ -1131,8 +1224,8 @@ void dMsgObject_c::outwaitProc() { (jmessage_tReference*)mpRenProc->getReference(); mpScrnDraw->arwAnimeMove(); if (isBookMessage()) { - if (isSend() || field_0x16a > 0) { - field_0x16a++; + if (isSend() || field_0x16a DUSK_IF_ELSE(>, !=) 0) { + IF_NOT_DUSK(field_0x16a++); if (field_0x16a <= 10) { f32 alpha = (10 - field_0x16a) / 10.0f; mpScrnDraw->fontAlpha(alpha); @@ -1413,7 +1506,7 @@ void dMsgObject_c::finishProc() { } void dMsgObject_c::endProc() { - field_0x16a++; + IF_NOT_DUSK(field_0x16a++); s16 sVar4 = 5; if (isKanbanMessage() || isBookMessage()) { sVar4 = g_MsgObject_HIO_c.field_0x304; diff --git a/src/d/d_msg_out_font.cpp b/src/d/d_msg_out_font.cpp index 57b85b18b2..9cab28a328 100644 --- a/src/d/d_msg_out_font.cpp +++ b/src/d/d_msg_out_font.cpp @@ -8,7 +8,7 @@ #include "f_op/f_op_msg_mng.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/user_interface.h" #include "dusk/version.hpp" #endif @@ -312,20 +312,12 @@ void COutFont_c::setAlphaRatio(f32 i_ratio) { } void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param_3) { - s16 sp256[70]; + DUSK_IF_ELSE(f32, s16) sp256[70]; for (int i = 0; i < 70; i++) { sp256[i] = field_0x1b4[i]; } -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) { - for (int i = 0; i < 70; i++) { - sp256[i] = -1; - } - } -#endif - for (int i = 0; i < 35; i++) { u8 type = mpOfs[i]->getType(); J2DTextBox* tbox = mpOfs[i]->getTextBoxPtr(); @@ -413,11 +405,14 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param } case 9: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 80.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 80) { field_0x1b4[type] = 0; } +#endif setBlendAnime(mpPane[type], field_0x1b4[type]); } @@ -433,55 +428,70 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param break; case 14: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 20.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 20) { field_0x1b4[type] = 0; } +#endif setBlendAnime(mpPane[type], field_0x1b4[type]); } mpPane[type]->draw(posX, posY, sizeX, sizeY, false, false, true); break; case 15: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 20.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 20) { field_0x1b4[type] = 0; } +#endif setBlendAnime(mpPane[type], field_0x1b4[type]); } mpPane[type]->draw(posX, posY, sizeX, sizeY, true, true, true); break; case 16: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 20.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 20) { field_0x1b4[type] = 0; } +#endif setBlendAnime(mpPane[type], field_0x1b4[type]); } mpPane[type]->draw(posX, posY, sizeX, sizeY, false, false, false); break; case 17: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 20.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 20) { field_0x1b4[type] = 0; } +#endif setBlendAnime(mpPane[type], field_0x1b4[type]); } mpPane[type]->draw(posX, posY, sizeX, sizeY, true, true, false); break; case 18: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 40.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 40) { field_0x1b4[type] = 0; } +#endif setBlendAnime(mpPane[type], field_0x1b4[type]); } @@ -493,11 +503,14 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param break; case 19: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 40.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 40) { field_0x1b4[type] = 0; } +#endif setBlendAnime(mpPane[type], field_0x1b4[type]); } @@ -528,24 +541,28 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param case 20: case 21: case 22: - IF_DUSK_BLOCK(dusk::frame_interp::get_ui_tick_pending()) +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 28.0f); +#else field_0x1b4[type]++; if (field_0x1b4[type] >= 28) { field_0x1b4[type] = 0; } - IF_DUSK_BLOCK_END - +#endif mpPane[type]->rotate(0.5f * sizeX, 0.5f * sizeY, ROTATE_Z, (360.0f * (f32)field_0x1b4[type]) / 28.0f); mpPane[type]->draw(posX, posY, sizeX, sizeY, false, false, false); break; case 25: // some issues in here, 2020 if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 18.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 18) { field_0x1b4[type] -= 18; } +#endif } f32 alpha; @@ -622,11 +639,14 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param case 67: case 68: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 40.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 40) { field_0x1b4[type] = 0; } +#endif setBlendAnime(mpPane[type], field_0x1b4[type]); } @@ -642,23 +662,28 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param case 59: case 60: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 40.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 40) { field_0x1b4[type] = 0; } +#endif setBlendAnime(mpPane[type], field_0x1b4[type]); } mpPane[type]->draw(posX, posY, sizeX, sizeY, false, false, false); break; case 62: if (sp256[type] == field_0x1b4[type]) { +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x1b4[type], 1.0f, 10.0f); +#else field_0x1b4[type]++; - if (field_0x1b4[type] >= 10) { field_0x1b4[type] = 0; } - +#endif if (field_0x1b4[type] < 5) { mpPane[type]->setBlendRatio(1.0f, 0.0f); } else { @@ -697,14 +722,22 @@ void COutFont_c::reset(J2DTextBox* i_textbox) { } } -void COutFont_c::setBlendAnime(J2DPicture* i_pic, s16 param_1) { +void COutFont_c::setBlendAnime(J2DPicture* i_pic, DUSK_IF_ELSE(f32, s16) param_1) { +#if TARGET_PC + f32 i = fmodf(param_1, 20.0f); + if (i < 0.0f) { + i += 20.0f; + } +#else int i = param_1 % 20; +#endif if (i < 10) { - f32 dVar6 = fopMsgM_valueIncrease(10, i, 0); + f32 dVar6 = DUSK_IF_ELSE((i / 10.0f) * (i / 10.0f), fopMsgM_valueIncrease(10, i, 0)); i_pic->setBlendRatio(1.0f - dVar6, dVar6); } else { - f32 dVar6 = fopMsgM_valueIncrease(10, i - 10, 0); + f32 dVar6 = DUSK_IF_ELSE(((i - 10.0f) / 10.0f) * ((i - 10.0f) / 10.0f), + fopMsgM_valueIncrease(10, i - 10, 0)); i_pic->setBlendRatio(dVar6, 1.0f - dVar6); } } diff --git a/src/d/d_msg_scrn_3select.cpp b/src/d/d_msg_scrn_3select.cpp index b7e0e8a8db..1545f51584 100644 --- a/src/d/d_msg_scrn_3select.cpp +++ b/src/d/d_msg_scrn_3select.cpp @@ -17,6 +17,7 @@ #include "d/d_pane_class.h" #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/menu_pointer.h" #include "dusk/version.hpp" @@ -315,6 +316,7 @@ dMsgScrn3Select_c::dMsgScrn3Select_c() { mDPDPoint = 0xFF; field_0x116 = 0xFF; field_0x117 = 0; + IF_DUSK(mPresenting = false); mSelMsgInactiveCol.set(0xFF, 0xFF, 0xFF, 0xFF); } @@ -441,6 +443,34 @@ void dMsgScrn3Select_c::translate(f32 i_x, f32 i_y) { mpParent->translate(i_x, i_y); } +#if TARGET_PC +void dMsgScrn3Select_c::presentAnims() { + if (mProcess >= PROC_MAX_e) { + return; + } + mPresenting = true; + if (mProcess == PROC_OPEN2_e || mProcess == PROC_CHANGE_e || mProcess == PROC_CLOSE_e) { + (this->*process[mProcess])(); + } + dusk::vdt::present_looping(mBpkFrame, mpAnmBpk, 1.0f); + dusk::vdt::present_looping(mBtkFrame, mpAnmBtk, 1.0f); + mpScreen->animation(); + + for (int i = 0; i < 3; i++) { + if (i == mSelNo) { + mpSelCld_c[i]->getPanePtr()->scale(1.0f, 1.0f); + mpSelCldr_c[i]->getPanePtr()->scale(1.0f, 1.0f); + } else { + mpSelCld_c[i]->getPanePtr()->scale(0.0f, 0.0f); + mpSelCldr_c[i]->getPanePtr()->scale(0.0f, 0.0f); + } + } + selectTrans(); + + mPresenting = false; +} +#endif + void dMsgScrn3Select_c::draw(f32 i_xPos, f32 i_yPos) { J2DGrafContext* port = dComIfGp_getCurrentGrafPort(); port->setup2D(); @@ -574,10 +604,14 @@ bool dMsgScrn3Select_c::selAnimeMove(u8 i_selNum, u8 param_1, bool param_2) { field_0x108 = param_2; #if TARGET_PC pointerMove(); + if (mProcess >= PROC_MAX_e) { + return false; + } #endif (this->*process[mProcess])(); +#if !TARGET_PC mBpkFrame++; if (mBpkFrame >= mpAnmBpk->getFrameMax()) { mBpkFrame -= mpAnmBpk->getFrameMax(); @@ -603,6 +637,7 @@ bool dMsgScrn3Select_c::selAnimeMove(u8 i_selNum, u8 param_1, bool param_2) { } selectTrans(); +#endif for (int i = 0; i < 3; i++) { if (i == mSelNo) { @@ -689,6 +724,7 @@ bool dMsgScrn3Select_c::selAnimeEnd() { (this->*process[mProcess])(); +#if !TARGET_PC mBpkFrame++; if (mBpkFrame >= mpAnmBpk->getFrameMax()) { mBpkFrame -= mpAnmBpk->getFrameMax(); @@ -702,6 +738,7 @@ bool dMsgScrn3Select_c::selAnimeEnd() { mpAnmBtk->setFrame(mBtkFrame); mpScreen->animation(); +#endif for (int i = 0; i < 3; i++) { if (i == mSelNo) { @@ -778,7 +815,13 @@ void dMsgScrn3Select_c::open1Proc() { void dMsgScrn3Select_c::open2Proc() { f32 sel_anm_frame[3] = {204.0f, 304.0f, 404.0f}; +#if TARGET_PC + if (mPresenting) { + dusk::vdt::advance_toward_frame(mBckFrame, 199.0f, 1.0f); + } +#else mBckFrame++; +#endif mpSelectCursor->setAlphaRate(1.0f); if (mBckFrame >= 199.0f) { @@ -903,7 +946,13 @@ void dMsgScrn3Select_c::changeProc() { if (field_0x108 != 0) { mBckFrame = 303.0f; } else { +#if TARGET_PC + if (mPresenting) { + dusk::vdt::advance_toward_frame(mBckFrame, 303.0f, 1.0f); + } +#else mBckFrame++; +#endif } selectAnimeTransform(0); @@ -915,7 +964,13 @@ void dMsgScrn3Select_c::changeProc() { if (field_0x108 != 0) { mBckFrame = 403.0f; } else { +#if TARGET_PC + if (mPresenting) { + dusk::vdt::advance_toward_frame(mBckFrame, 403.0f, 1.0f); + } +#else mBckFrame++; +#endif } selectAnimeTransform(0); @@ -930,7 +985,13 @@ void dMsgScrn3Select_c::changeProc() { if (field_0x108 != 0) { mBckFrame = 300.0f; } else { +#if TARGET_PC + if (mPresenting) { + dusk::vdt::advance_toward_frame(mBckFrame, 300.0f, 1.0f); + } +#else mBckFrame--; +#endif } selectAnimeTransform(1); @@ -942,7 +1003,13 @@ void dMsgScrn3Select_c::changeProc() { if (field_0x108 != 0) { mBckFrame = 403.0f; } else { +#if TARGET_PC + if (mPresenting) { + dusk::vdt::advance_toward_frame(mBckFrame, 403.0f, 1.0f); + } +#else mBckFrame++; +#endif } selectAnimeTransform(1); @@ -957,7 +1024,13 @@ void dMsgScrn3Select_c::changeProc() { if (field_0x108 != 0) { mBckFrame = 400.0f; } else { +#if TARGET_PC + if (mPresenting) { + dusk::vdt::advance_toward_frame(mBckFrame, 400.0f, 1.0f); + } +#else mBckFrame--; +#endif } selectAnimeTransform(2); @@ -969,7 +1042,13 @@ void dMsgScrn3Select_c::changeProc() { if (field_0x108 != 0) { mBckFrame = 300.0f; } else { +#if TARGET_PC + if (mPresenting) { + dusk::vdt::advance_toward_frame(mBckFrame, 300.0f, 1.0f); + } +#else mBckFrame--; +#endif } selectAnimeTransform(2); @@ -985,7 +1064,13 @@ void dMsgScrn3Select_c::changeProc() { void dMsgScrn3Select_c::closeProc() { mpSelectCursor->setAlphaRate(0.0f); +#if TARGET_PC + if (mPresenting) { + dusk::vdt::advance_toward_frame(mBckFrame, 190.0f, 1.0f); + } +#else mBckFrame--; +#endif for (int i = 0; i < 3; i++) { f32 scale_x = mpSel_c[i]->getPanePtr()->getScaleX(); diff --git a/src/d/d_msg_scrn_arrow.cpp b/src/d/d_msg_scrn_arrow.cpp index cea0dae145..587eb6a21a 100644 --- a/src/d/d_msg_scrn_arrow.cpp +++ b/src/d/d_msg_scrn_arrow.cpp @@ -7,6 +7,7 @@ #include "d/d_pane_class.h" #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/settings.h" #endif @@ -73,6 +74,14 @@ void dMsgScrnArrow_c::draw() { if (dusk::getSettings().game.recordingMode) { return; } + if (mpArw_c->isVisible()) { + dusk::vdt::present_looping(mBckFrame, mpBck, 1.0f); + dusk::vdt::present_looping(mBpkFrame, mpBpk, 1.0f); + mpScreen->animation(); + } else if (mpDot_c->isVisible()) { + dusk::vdt::present_looping(mBpkFrame, mpBpk, 1.0f); + mpScreen->animation(); + } #endif J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort(); mpScreen->draw(0.0f, 0.0f, graf_ctx); @@ -103,6 +112,7 @@ void dMsgScrnArrow_c::arwAnimeMove() { mpDot_c->hide(); } +#if !TARGET_PC mBckFrame += 1.0f; if (mBckFrame >= (f32)mpBck->getFrameMax()) { mBckFrame -= (f32)mpBck->getFrameMax(); @@ -114,6 +124,7 @@ void dMsgScrnArrow_c::arwAnimeMove() { mBpkFrame -= (f32)mpBpk->getFrameMax(); } mpBpk->setFrame(mBpkFrame); +#endif mpScreen->animation(); } @@ -136,11 +147,14 @@ void dMsgScrnArrow_c::dotAnimeMove() { mpDot_c->show(); } +#if !TARGET_PC mBpkFrame += 1.0f; if (mBpkFrame >= (f32)mpBpk->getFrameMax()) { mBpkFrame -= (f32)mpBpk->getFrameMax(); } mpBpk->setFrame(mBpkFrame); +#endif + mpScreen->animation(); } diff --git a/src/d/d_msg_scrn_base.cpp b/src/d/d_msg_scrn_base.cpp index 5488b76648..638bf1fc8b 100644 --- a/src/d/d_msg_scrn_base.cpp +++ b/src/d/d_msg_scrn_base.cpp @@ -71,8 +71,19 @@ void dMsgScrnBase_c::multiDraw() { } } +#if TARGET_PC +void dMsgScrnBase_c::presentAnims() { + if (dMsgObject_c* msg = dMsgObject_getMsgObjectClass()) { + if (msg->getScrnDrawPtrLocal() == this) { + msg->presentAnims(); + } + } +} +#endif + void dMsgScrnBase_c::draw() { #if TARGET_PC + presentAnims(); if (dusk::getSettings().game.recordingMode) { return; } diff --git a/src/d/d_msg_scrn_explain.cpp b/src/d/d_msg_scrn_explain.cpp index 4771257ccd..1d90898017 100644 --- a/src/d/d_msg_scrn_explain.cpp +++ b/src/d/d_msg_scrn_explain.cpp @@ -15,6 +15,7 @@ #include #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/version.hpp" #endif @@ -326,6 +327,7 @@ dMsgScrnExplain_c::~dMsgScrnExplain_c() { void dMsgScrnExplain_c::move() { u8 currentStatus = mStatus; +#if !TARGET_PC f32 y_offset = 0.0f; if (field_0x66 == 2) { y_offset = -35.0f; @@ -335,6 +337,7 @@ void dMsgScrnExplain_c::move() { mpRoot_c[i]->paneTrans(field_0x48 + g_MsgObject_HIO_c.mChoicePos[0][6], field_0x4c + g_MsgObject_HIO_c.mBoxPos[0][6] + y_offset); } +#endif (this->*move_process[mStatus])(); @@ -346,7 +349,29 @@ void dMsgScrnExplain_c::move() { g_MsgObject_HIO_c.mBoxTalkScaleY); } +#if TARGET_PC +void dMsgScrnExplain_c::presentAnims() { + if (mStatus == STATUS_OPEN_e || mStatus == STATUS_CLOSE_e) { + dusk::vdt::advance_toward_frame(field_0x5a, mStatus == STATUS_OPEN_e ? 5.0f : 0.0f, 1.0f); + field_0x48 = FB_WIDTH_BASE * getAlphaRatio(); + for (int i = 0; i < 2; i++) { + mpRoot_c[i]->setAlphaRate(1.0f - getAlphaRatio()); + } + if (mpBackTex != NULL) { + mpBackTex->setAlpha((1.0f - getAlphaRatio()) * 150.0f); + } + } + + const f32 y_offset = field_0x66 == 2 ? -35.0f : 0.0f; + for (int i = 0; i < 2; i++) { + mpRoot_c[i]->paneTrans(field_0x48 + g_MsgObject_HIO_c.mChoicePos[0][6], + field_0x4c + g_MsgObject_HIO_c.mBoxPos[0][6] + y_offset); + } +} +#endif + void dMsgScrnExplain_c::draw(J2DOrthoGraph* i_graf) { + IF_DUSK(presentAnims()); if (mStatus == STATUS_WAIT_e || mStatus == STATUS_OPEN_REQ_e) { return; } @@ -389,6 +414,7 @@ void dMsgScrnExplain_c::draw(J2DOrthoGraph* i_graf) { mpSelect_c->translate(486.0f + g_MsgObject_HIO_c.mChoicePos[0][7], 209.0f + g_MsgObject_HIO_c.mBoxPos[0][7] + y_offset); + IF_DUSK(mpSelect_c->presentAnims()); mpSelect_c->draw(0.0f, 0.0f); } @@ -445,7 +471,7 @@ void dMsgScrnExplain_c::open_init() { } void dMsgScrnExplain_c::open_proc() { - field_0x5a++; + IF_NOT_DUSK(field_0x5a++); if (field_0x5a >= 5) { field_0x5a = 5; if (field_0x64 == 1 || field_0x64 == 2) { @@ -627,7 +653,7 @@ void dMsgScrnExplain_c::close_proc() { iVar1 = true; } - field_0x5a--; + IF_NOT_DUSK(field_0x5a--); if (field_0x5a <= 0) { field_0x5a = 0; if (iVar1) { diff --git a/src/d/d_msg_scrn_howl.cpp b/src/d/d_msg_scrn_howl.cpp index 4e5c59546c..a661526b9c 100644 --- a/src/d/d_msg_scrn_howl.cpp +++ b/src/d/d_msg_scrn_howl.cpp @@ -21,8 +21,8 @@ #include "m_Do/m_Do_graphic.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" +#include "dusk/game_clock.h" +#include "dusk/interp/user_interface.h" #endif // POSIX already defines a macro with this name, but we know that this specific name is @@ -148,6 +148,10 @@ dMsgScrnHowl_c::dMsgScrnHowl_c() { field_0x2124 = 0; field_0x2128 = 0; mPlotTime = 0; +#if TARGET_PC + mSampleAcc = 0.0f; + mPulseFrame = 0.0f; +#endif field_0x212c = 0; field_0x212e = 0; field_0x2126 = 0; @@ -289,18 +293,52 @@ dMsgScrnHowl_c::~dMsgScrnHowl_c() { dComIfGp_getMsgArchive(5)->removeResourceAll();; } +#if TARGET_PC +void dMsgScrnHowl_c::presentAnims() { + dMsgScrnBase_c::presentAnims(); + for (int i = 0; i < 0x300; i++) { + if (field_0x1b14[i] > 0) { + dusk::vdt::advance_toward_frame(field_0x1b14[i], 0.0f, 1.0f); + } + } + f32 alphaRate = mpPmP_c->getAlphaRate(); + f32 fVar1; + f32 fVar2; + if (field_0x2798 == 3) { + fVar1 = 0.5f; + fVar2 = 0.0f; + } else { + fVar1 = 1.0f; + fVar2 = 1.0f; + } + if (field_0x1994 != fVar1) { + dusk::vdt::present_addCalc2(&field_0x1994, fVar1, 0.2f, 1.0f, 0.1f); + } + if (field_0x1998 != fVar2) { + dusk::vdt::present_addCalc2(&field_0x1998, fVar2, 0.2f, 1.0f, 0.1f); + } + mpButtonIcon[0]->setAlphaRate(field_0x1994 * alphaRate); + mpButtonIcon[1]->setAlphaRate(field_0x1994 * alphaRate); + mpButtonText[0]->setAlphaRate(field_0x1998 * alphaRate); + mpButtonText[1]->setAlphaRate(field_0x1998 * alphaRate); +} +#endif + void dMsgScrnHowl_c::exec() { field_0x2799 = field_0x2798; (this->*process[field_0x2798])(); +#if !TARGET_PC for (int i = 0; i < 0x300; i++) { if (field_0x1b14[i] > 0) { field_0x1b14[i]--; } } +#endif if (field_0x2799 != field_0x2798) { (this->*init_proc[field_0x2798])(); } - + +#if !TARGET_PC f32 alphaRate = mpPmP_c->getAlphaRate(); f32 fVar1; f32 fVar2; @@ -327,6 +365,7 @@ void dMsgScrnHowl_c::exec() { mpButtonIcon[1]->setAlphaRate(field_0x1994 * alphaRate); mpButtonText[0]->setAlphaRate(field_0x1998 * alphaRate); mpButtonText[1]->setAlphaRate(field_0x1998 * alphaRate); +#endif #if TARGET_PC showCursor = true; @@ -467,6 +506,7 @@ void dMsgScrnHowl_c::resetLine() { field_0x2195 = 0; field_0x2128 = 0; mPlotTime = 0; + IF_DUSK(mSampleAcc = 0.0f); field_0x212c = 0; field_0x212e = 0; field_0x2124 = 0; @@ -503,7 +543,7 @@ void dMsgScrnHowl_c::drawWave() { if (local_80 < 0) { local_80 = 0; } - s32 local_94 = 0; + DUSK_IF_ELSE(f32, s32) local_94 = 0; Vec fVar12 = field_0x128; Vec this_02 = field_0x140; #if TARGET_PC // TODO: make this actually use the scissor @@ -541,7 +581,7 @@ void dMsgScrnHowl_c::drawWave() { for (int iVar10 = 0; iVar10 < field_0x2128 - 1; iVar10++) { f32 local_54 = local_e0; f32 local_c8 = field_0x180[sVar14]; - s32 sVar3 = field_0x1b14[sVar14]; + DUSK_IF_ELSE(f32, s32) sVar3 = field_0x1b14[sVar14]; local_e0 += field_0x1980; local_68 += field_0x1984; if (local_68 > 255.0f) { @@ -549,7 +589,7 @@ void dMsgScrnHowl_c::drawWave() { } sVar14 = addCount(sVar14); f32 fVar2 = field_0x180[sVar14]; - s16 temp_r4 = field_0x1b14[sVar14]; + DUSK_IF_ELSE(f32, s16) temp_r4 = field_0x1b14[sVar14]; if (iVar10 == field_0x2128 - 2) { local_60 = local_e0; local_64 = fVar2; @@ -597,19 +637,23 @@ void dMsgScrnHowl_c::drawWave() { f18 = local_64; } else { #if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x2134++; - if (field_0x2134 > 30) { - field_0x2134 = 0; - } + dusk::vdt::advance_looping_frame(mPulseFrame, 1.0f, 31.0f); + if (mPulseFrame < 15.0f) { + local_dc = mPulseFrame / 15.0f; + } else { + local_dc = (30.0f - mPulseFrame) / 15.0f; + } +#else + field_0x2134++; + if (field_0x2134 > 30) { + field_0x2134 = 0; } if (field_0x2134 < 15) { local_dc = field_0x2134 / 15.0f; } else { local_dc = (30.0f - field_0x2134) / 15.0f; } +#endif f17 = f26; f18 = local_e4; } @@ -882,6 +926,19 @@ void dMsgScrnHowl_c::drawEffect() { } void dMsgScrnHowl_c::calcMain() { +#if TARGET_PC + mSampleAcc += dusk::game_clock::original_frames(); + while (mSampleAcc >= 1.0f) { + mSampleAcc -= 1.0f; + if (mPlotTime < field_0x2138 + 380) { + mPlotTime++; + } else { + field_0x212c--; + } + calcWave(); + calcGuide(); + } +#else if (mPlotTime < field_0x2138 + 380) { mPlotTime++; } else { @@ -889,6 +946,7 @@ void dMsgScrnHowl_c::calcMain() { } calcWave(); calcGuide(); +#endif } void dMsgScrnHowl_c::calcWave() { diff --git a/src/d/d_msg_scrn_item.cpp b/src/d/d_msg_scrn_item.cpp index 17007403dc..33a975b6e9 100644 --- a/src/d/d_msg_scrn_item.cpp +++ b/src/d/d_msg_scrn_item.cpp @@ -25,6 +25,7 @@ #include #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/version.hpp" #endif @@ -396,6 +397,25 @@ dMsgScrnItem_c::~dMsgScrnItem_c() { dComIfGp_getMsgCommonArchive()->removeResourceAll();; } +#if TARGET_PC +void dMsgScrnItem_c::presentAnims() { + dMsgScrnBase_c::presentAnims(); + mpSelect_c->presentAnims(); + for (int i = 0; i < 1; i++) { + dusk::vdt::present_looping(field_0x140[i], field_0x118[i], g_MsgObject_HIO_c.mBoxItemAnmSpeed); + } + for (int i = 0; i < 2; i++) { + dusk::vdt::present_looping(field_0x154[i], field_0x12c[i], g_MsgObject_HIO_c.mBoxItemAnmSpeed); + } + mpScreen->animation(); + if (isTalkNow()) { + fukiAlpha(1.0f); + } else { + fukiAlpha(field_0x13c); + } +} +#endif + void dMsgScrnItem_c::exec() { f32 dVar12 = field_0x13c; if (!field_0x19d) { @@ -404,6 +424,7 @@ void dMsgScrnItem_c::exec() { setBpk1Animation(field_0x12c[1]); field_0x19d = true; } +#if !TARGET_PC for (int i = 0; i < 1; i++) { field_0x140[i] += g_MsgObject_HIO_c.mBoxItemAnmSpeed; if (field_0x140[i] >= field_0x118[i]->getFrameMax()) { @@ -425,7 +446,8 @@ void dMsgScrnItem_c::exec() { } else { fukiAlpha(dVar12); } - +#endif + f32 yOffset; switch(field_0x19c) { case 1: @@ -578,6 +600,12 @@ bool dMsgScrnItem_c::selectAnimeEnd() { return mpSelect_c->selAnimeEnd(); } +#if TARGET_PC +bool dMsgScrnItem_c::isSelectAnimeActive() { + return mpSelect_c != NULL && mpSelect_c->isAnimeActive(); +} +#endif + void dMsgScrnItem_c::fukiScale(f32 param_0) { } diff --git a/src/d/d_msg_scrn_kanban.cpp b/src/d/d_msg_scrn_kanban.cpp index 7f254bbafb..41efff8824 100644 --- a/src/d/d_msg_scrn_kanban.cpp +++ b/src/d/d_msg_scrn_kanban.cpp @@ -14,6 +14,7 @@ #include "d/d_pane_class.h" #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/settings.h" #include "dusk/version.hpp" #endif @@ -204,7 +205,16 @@ dMsgScrnKanban_c::~dMsgScrnKanban_c() { dComIfGp_getMsgArchive(2)->removeResourceAll(); } +#if TARGET_PC +void dMsgScrnKanban_c::presentAnims() { + dMsgScrnBase_c::presentAnims(); + dusk::vdt::present_looping(field_0xd8, field_0xd0, 1.0f); + mpScreen->animation(); +} +#endif + void dMsgScrnKanban_c::exec() { +#if !TARGET_PC field_0xd8 += 1.0f; if (field_0xd8 >= field_0xd0->getFrameMax()) { field_0xd8 -= field_0xd0->getFrameMax(); @@ -212,6 +222,7 @@ void dMsgScrnKanban_c::exec() { field_0xd0->setFrame(field_0xd8); mpScreen->animation(); +#endif if (isTalkNow()) { fukiAlpha(1.0f); @@ -222,6 +233,7 @@ void dMsgScrnKanban_c::exec() { void dMsgScrnKanban_c::draw() { #if TARGET_PC + presentAnims(); if (dusk::getSettings().game.recordingMode) { return; } diff --git a/src/d/d_msg_scrn_light.cpp b/src/d/d_msg_scrn_light.cpp index 1f6a176066..0434da93b1 100644 --- a/src/d/d_msg_scrn_light.cpp +++ b/src/d/d_msg_scrn_light.cpp @@ -8,7 +8,7 @@ #include "d/d_pane_class.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/user_interface.h" #endif class dMsgScrnLight_HIO_c { @@ -206,15 +206,14 @@ void dMsgScrnLight_c::draw(f32* i_anmFrame, f32 i_posX, f32 i_posY, f32 i_scaleX } if (mPlayAnim) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - *i_anmFrame += 1.0f; - if (*i_anmFrame >= mpBck->getFrameMax()) { - *i_anmFrame = 0.0f; - } +#if TARGET_PC + dusk::vdt::advance_looping_frame(*i_anmFrame, 1.0f, mpBck->getFrameMax()); +#else + *i_anmFrame += 1.0f; + if (*i_anmFrame >= mpBck->getFrameMax()) { + *i_anmFrame = 0.0f; } +#endif mBckFrame = *i_anmFrame; mBpkFrame = *i_anmFrame; @@ -229,16 +228,14 @@ void dMsgScrnLight_c::draw(f32* i_anmFrame, f32 i_posX, f32 i_posY, f32 i_scaleX mpParent_c->setBlackWhite(i_black, i_white); if (mPlayAnim) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - *i_anmFrame += i_anmRate; - - if (*i_anmFrame >= mpBck->getFrameMax()) { - *i_anmFrame = 0.0f; - } +#if TARGET_PC + dusk::vdt::advance_looping_frame(*i_anmFrame, i_anmRate, mpBck->getFrameMax()); +#else + *i_anmFrame += i_anmRate; + if (*i_anmFrame >= mpBck->getFrameMax()) { + *i_anmFrame = 0.0f; } +#endif mBckFrame = *i_anmFrame; mBpkFrame = *i_anmFrame; diff --git a/src/d/d_msg_scrn_talk.cpp b/src/d/d_msg_scrn_talk.cpp index 1e63d67a9b..d4b5d4e24f 100644 --- a/src/d/d_msg_scrn_talk.cpp +++ b/src/d/d_msg_scrn_talk.cpp @@ -21,6 +21,7 @@ #include #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/settings.h" #include "dusk/version.hpp" #endif @@ -373,6 +374,15 @@ void dMsgScrnTalk_c::exec() { } } +#if TARGET_PC +void dMsgScrnTalk_c::presentAnims() { + dMsgScrnBase_c::presentAnims(); + if (mpSelect_c != NULL) { + mpSelect_c->presentAnims(); + } +} +#endif + void dMsgScrnTalk_c::drawSelf() { #if TARGET_PC if (dusk::getSettings().game.recordingMode) { @@ -404,7 +414,7 @@ void dMsgScrnTalk_c::drawSelf() { } } if (field_0x35c[i] > 0) { - field_0x35c[i]--; + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(field_0x35c[i], 0.0f, 1.0f), field_0x35c[i]--); } else { mpLight_c->draw( &mCharInfoPtr[i].field_0x14, mCharInfoPtr[i].field_0x00, mCharInfoPtr[i].field_0x04, @@ -489,6 +499,12 @@ bool dMsgScrnTalk_c::selectAnimeEnd() { return false; } +#if TARGET_PC +bool dMsgScrnTalk_c::isSelectAnimeActive() { + return mpSelect_c != NULL && mpSelect_c->isAnimeActive(); +} +#endif + void dMsgScrnTalk_c::fukiScale(f32 param_0) { } diff --git a/src/d/d_msg_scrn_tree.cpp b/src/d/d_msg_scrn_tree.cpp index bbed47cb05..36d4b9494f 100644 --- a/src/d/d_msg_scrn_tree.cpp +++ b/src/d/d_msg_scrn_tree.cpp @@ -10,6 +10,7 @@ #include "d/d_pane_class.h" #if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/settings.h" #include "dusk/version.hpp" #endif @@ -216,7 +217,17 @@ dMsgScrnTree_c::~dMsgScrnTree_c() { dComIfGp_getMsgArchive(2)->removeResourceAll(); } +#if TARGET_PC +void dMsgScrnTree_c::presentAnims() { + dMsgScrnBase_c::presentAnims(); + dusk::vdt::present_looping(field_0xdc, field_0xd0, 1.0f); + dusk::vdt::present_looping(field_0xe0, field_0xd4, 1.0f); + mpScreen->animation(); +} +#endif + void dMsgScrnTree_c::exec() { +#if !TARGET_PC field_0xdc += 1.0f; if (field_0xdc >= field_0xd0->getFrameMax()) { field_0xdc -= field_0xd0->getFrameMax(); @@ -229,6 +240,7 @@ void dMsgScrnTree_c::exec() { } field_0xd4->setFrame(field_0xe0); mpScreen->animation(); +#endif if (isTalkNow()) { fukiAlpha(1.0f); @@ -246,6 +258,7 @@ void dMsgScrnTree_c::exec() { void dMsgScrnTree_c::draw() { #if TARGET_PC + presentAnims(); if (dusk::getSettings().game.recordingMode) { return; } diff --git a/src/d/d_name.cpp b/src/d/d_name.cpp index 89506e8078..c41d90ed37 100644 --- a/src/d/d_name.cpp +++ b/src/d/d_name.cpp @@ -12,6 +12,8 @@ #include "f_op/f_op_msg_mng.h" #if TARGET_PC +#include "dusk/game_clock.h" +#include "dusk/interp/user_interface.h" #include "dusk/utilities.hpp" #include "dusk/version.hpp" @@ -431,7 +433,7 @@ void dName_c::_move() { } #endif - cursorAnm(); + IF_NOT_DUSK(cursorAnm()); } int dName_c::nameCheck() { @@ -476,6 +478,10 @@ void dName_c::playNameSet(int nameLength) { } void dName_c::cursorAnm() { +#if TARGET_PC + dusk::vdt::present_looping(mCurColAnmF, mCursorColorKey, 2.0f); + dusk::vdt::present_looping(mCurTexAnmF, mCursorTexKey, 2.0f); +#else mCurColAnmF += 2; if (mCurColAnmF >= mCursorColorKey->getFrameMax()) { mCurColAnmF -= mCursorColorKey->getFrameMax(); @@ -487,6 +493,7 @@ void dName_c::cursorAnm() { mCurTexAnmF -= mCursorTexKey->getFrameMax(); } mCursorTexKey->setFrame(mCurTexAnmF); +#endif nameIn.NameInScr->animation(); } @@ -1615,9 +1622,20 @@ void dName_c::nameWide() { #endif void dName_c::_draw() { - #if TARGET_PC - nameWide(); - #endif +#if TARGET_PC + if (dusk::game_clock::is_presentation_frame()) { + for (int i = 0; i < 65; ++i) { + mMojiIcon[i]->presentAnime(); + } + for (int i = 0; i < 4; ++i) { + if (mMenuIcon[i] != NULL) { + mMenuIcon[i]->presentAnime(); + } + } + cursorAnm(); + nameWide(); + } +#endif dComIfGd_set2DOpa(&nameIn); dComIfGd_set2DOpa(mSelIcon); diff --git a/src/d/d_pane_class.cpp b/src/d/d_pane_class.cpp index 4d5104ffcf..f833857873 100644 --- a/src/d/d_pane_class.cpp +++ b/src/d/d_pane_class.cpp @@ -13,7 +13,16 @@ #include "m_Do/m_Do_ext.h" #include "m_Do/m_Do_graphic.h" +#if TARGET_PC +#include "dusk/interp/lerp.h" +#include "dusk/interp/user_interface.h" +#endif + CPaneMgr::CPaneMgr() { +#if TARGET_PC + mScaleAnimation.active = false; + mColorAnimation.active = false; +#endif mpFirstStackAlpha = NULL; mpFirstStackSize = NULL; } @@ -45,6 +54,12 @@ void CPaneMgr::setAlpha(u8 alpha) { } void CPaneMgr::reinit() { +#if TARGET_PC + mAlphaAnimation.active = false; + mScaleAnimation.active = false; + mColorAnimation.active = false; +#endif + mInitPos.x = getPanePtr()->getBounds().i.x; mInitPos.y = getPanePtr()->getBounds().i.y; @@ -66,6 +81,12 @@ void CPaneMgr::reinit() { } void CPaneMgr::initiate(J2DPane* p_pane, JKRExpHeap* p_heap) { +#if TARGET_PC + mAlphaAnimation.active = false; + mScaleAnimation.active = false; + mColorAnimation.active = false; +#endif + mPane = p_pane; if (p_heap) { @@ -269,7 +290,29 @@ void CPaneMgr::paneScale(f32 x, f32 y) { } } +#if TARGET_PC +void CPaneMgr::presentAnime() { + presentAlphaAnime(); + dusk::vdt::present_animation(mScaleAnimation, mScaleAnime, *this, [&](f32 rate) { + const f32 scale = dusk::interp::lerp(mScaleAnimation.start, mScaleAnimation.end, rate); + getPanePtr()->scale(getInitScaleX() * scale, getInitScaleY() * scale); + }); + dusk::vdt::present_animation(mColorAnimation, mColorAnime, *this, [&](f32 rate) { + setBlackWhite( + dusk::interp::lerp(mColorAnimation.start.first, mColorAnimation.end.first, rate), + dusk::interp::lerp(mColorAnimation.start.second, mColorAnimation.end.second, rate)); + }); +} +#endif + bool CPaneMgr::scaleAnime(s16 param_0, f32 param_1, f32 param_2, u8 param_3) { +#if TARGET_PC + if (!dusk::vdt::request_animation(mScaleAnimation, mScaleAnime, param_0, param_1, param_2, param_3)) { + return false; + } + getPanePtr()->scale(mInitScale.x * param_2, mInitScale.y * param_2); + return true; +#else if (mScaleAnime < param_0 - 1) { mScaleAnime++; f32 rate = rateCalc(param_0, mScaleAnime, param_3); @@ -281,10 +324,20 @@ bool CPaneMgr::scaleAnime(s16 param_0, f32 param_1, f32 param_2, u8 param_3) { return true; } return false; +#endif } bool CPaneMgr::colorAnime(s16 anmTimer, JUtility::TColor startBlack, JUtility::TColor endBlack, - JUtility::TColor startWhite, JUtility::TColor endWhite, u8 calcType) { + JUtility::TColor startWhite, JUtility::TColor endWhite, u8 calcType) { +#if TARGET_PC + if (!dusk::vdt::request_animation(mColorAnimation, mColorAnime, anmTimer, + {startBlack, startWhite}, {endBlack, endWhite}, calcType)) + { + return false; + } + setBlackWhite(endBlack, endWhite); + return true; +#else if (mColorAnime < anmTimer - 1) { mColorAnime++; f32 rate = rateCalc(anmTimer, mColorAnime, calcType); @@ -349,6 +402,7 @@ bool CPaneMgr::colorAnime(s16 anmTimer, JUtility::TColor startBlack, JUtility::T } return false; +#endif } Vec CPaneMgr::getGlobalVtx(J2DPane* p_pane, Mtx* param_1, u8 param_2, bool param_3, diff --git a/src/d/d_pane_class_alpha.cpp b/src/d/d_pane_class_alpha.cpp index 31bc8bc9b4..d82b77d220 100644 --- a/src/d/d_pane_class_alpha.cpp +++ b/src/d/d_pane_class_alpha.cpp @@ -5,7 +5,13 @@ #include "JSystem/JKernel/JKRExpHeap.h" #include "m_Do/m_Do_ext.h" -CPaneMgrAlpha::CPaneMgrAlpha() {} +#if TARGET_PC +#include "dusk/interp/user_interface.h" +#endif + +CPaneMgrAlpha::CPaneMgrAlpha() { + IF_DUSK(mAlphaAnimation.active = false); +} CPaneMgrAlpha::CPaneMgrAlpha(J2DScreen* p_screen, u64 tag, u8 flags, JKRExpHeap* p_heap) { J2DPane* pane = p_screen->search(tag); @@ -21,6 +27,8 @@ CPaneMgrAlpha::~CPaneMgrAlpha() { } void CPaneMgrAlpha::initiateAlpha(J2DPane* p_pane, JKRExpHeap* p_heap) { + IF_DUSK(mAlphaAnimation.active = false); + mPane = p_pane; if (p_heap != NULL) { @@ -68,7 +76,7 @@ bool CPaneMgrAlpha::isVisible() { return mPane->isVisible(); } -f32 CPaneMgrAlpha::rateCalc(s16 maxTimer, s16 curTimer, u8 calcType) { +f32 CPaneMgrAlpha::rateCalc(DUSK_IF_ELSE(f32, s16) maxTimer, DUSK_IF_ELSE(f32, s16) curTimer, u8 calcType) { if (maxTimer <= curTimer) { return 1.0f; } @@ -105,7 +113,22 @@ f32 CPaneMgrAlpha::getAlphaRate() { return rate; } +#if TARGET_PC +void CPaneMgrAlpha::presentAlphaAnime() { + dusk::vdt::present_animation(mAlphaAnimation, mAlphaTimer, *this, [&](f32 rate) { + setAlpha(mAlphaAnimation.start + rate * (f32)(mAlphaAnimation.end - mAlphaAnimation.start)); + }); +} +#endif + bool CPaneMgrAlpha::alphaAnime(s16 timer, u8 startAlpha, u8 endAlpha, u8 calcType) { +#if TARGET_PC + if (!dusk::vdt::request_animation(mAlphaAnimation, mAlphaTimer, timer, startAlpha, endAlpha, calcType)) { + return false; + } + setAlpha(endAlpha); + return true; +#else if (mAlphaTimer < timer - 1) { mAlphaTimer++; f32 rate = rateCalc(timer, mAlphaTimer, calcType); @@ -117,20 +140,41 @@ bool CPaneMgrAlpha::alphaAnime(s16 timer, u8 startAlpha, u8 endAlpha, u8 calcTyp } return false; +#endif } bool CPaneMgrAlpha::alphaAnimeLoop(s16 param_0, u8 param_1, u8 param_2, u8 param_3) { bool ret = false; s16 temp_r4 = param_0 / 2; +#if TARGET_PC + mAlphaAnimation.active = false; + if (param_0 <= 0) { + mAlphaTimer = 0.0f; + setAlpha(param_1); + return true; + } + const f32 frames = dusk::game_clock::original_frames(); + if (frames <= 0.0f) { + return false; + } + if (mAlphaTimer >= param_0) { + mAlphaTimer = 0.0f; + ret = true; + } else { + ret = mAlphaTimer + frames >= param_0; + dusk::vdt::advance_looping_frame(mAlphaTimer, 1.0f, param_0); + } +#else mAlphaTimer++; if (mAlphaTimer >= param_0) { mAlphaTimer = 0; ret = true; } +#endif - s16 tmp; + DUSK_IF_ELSE(f32, s16) tmp; if (mAlphaTimer < temp_r4) { tmp = mAlphaTimer; } else { diff --git a/src/d/d_particle.cpp b/src/d/d_particle.cpp index 9e443aa81a..d1e175dfc1 100644 --- a/src/d/d_particle.cpp +++ b/src/d/d_particle.cpp @@ -26,7 +26,7 @@ #include "SSystem/SComponent/c_math.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" #include #endif @@ -1976,7 +1976,7 @@ void dPa_light8PcallBack::draw(JPABaseEmitter* param_1, JPABaseParticle* param_2 JGeometry::TVec3 local_160; JGeometry::TVec3 local_16c; #if TARGET_PC - if (dusk::frame_interp::is_sim_frame()) + if (dusk::game_clock::is_sim_frame()) #endif { dPa_setWindPower(param_2); diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index 7faabde76b..12caa544c1 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -19,6 +19,7 @@ #if TARGET_PC #include "dusk/autosave.h" +#include "dusk/game_clock.h" #include "dusk/game_mode.hpp" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/memory.h" @@ -147,6 +148,8 @@ s32 dScnName_c::create() { #endif dComIfGp_getVibration().Init(); + + IF_DUSK(base.draw_interp_frame = true); } return phase_state; } @@ -243,7 +246,9 @@ s32 dScnName_c::execute() { } s32 dScnName_c::draw() { + IF_DUSK_BLOCK(dusk::game_clock::is_sim_frame()) dComIfGp_getVibration().Run(); + IF_DUSK_BLOCK_END switch (mDrawProc) { case 0: diff --git a/src/d/d_select_cursor.cpp b/src/d/d_select_cursor.cpp index 640f8072cd..b651c617cd 100644 --- a/src/d/d_select_cursor.cpp +++ b/src/d/d_select_cursor.cpp @@ -5,9 +5,22 @@ #include "d/d_com_inf_game.h" #include "JSystem/J2DGraph/J2DAnimation.h" #include "JSystem/J2DGraph/J2DAnmLoader.h" -#include "dusk/frame_interpolation.h" #include +#if TARGET_PC +#include "dusk/interp/samples.h" +#include "dusk/interp/user_interface.h" + +#include "m_Do/m_Do_lib.h" + +namespace { +struct CursorSamples { + dusk::interp::Samples positions; + dusk::interp::Samples alpha; +}; +} // namespace +#endif + dSelect_cursorHIO_c::dSelect_cursorHIO_c() { field_0x8 = 1.0f; mXAxisExpansion = 1.0f; @@ -202,6 +215,7 @@ dSelect_cursor_c::dSelect_cursor_c(u8 param_0, f32 param_1, JKRArchive* param_2) dSelect_cursor_c::~dSelect_cursor_c() { + IF_DUSK(dusk::interp::erase_owned_samples(this);) JKR_DELETE(mpScreen); mpScreen = NULL; if (mpPaneMgr) { @@ -247,6 +261,15 @@ dSelect_cursor_c::~dSelect_cursor_c() { } void dSelect_cursor_c::draw() { +#if TARGET_PC + const f32 alpha = mpPaneMgr->getAlphaRate(); + const bool visible = mpPaneMgr->isVisible(); + const f32 presentedAlpha = dusk::interp::get(this).alpha.read(0, alpha); + mpPaneMgr->setAlphaRate(presentedAlpha); + if (alpha == 0.0f && presentedAlpha > 0.0f) { + mpPaneMgr->show(); + } +#endif mpPaneMgr->getAlphaRate(); update(); J2DGrafContext* gphCtx = dComIfGp_getCurrentGrafPort(); @@ -255,6 +278,12 @@ void dSelect_cursor_c::draw() { if (mpSelectIcon) { mpSelectIcon->drawSelf(); // inline here, but not sure how to properly define it } +#if TARGET_PC + mpPaneMgr->setAlphaRate(alpha); + if (!visible) { + mpPaneMgr->hide(); + } +#endif } void dSelect_cursor_c::update() { @@ -262,14 +291,31 @@ void dSelect_cursor_c::update() { if (field_0xb6 == 3) { fVar1 = 0.5f; } -#ifdef TARGET_PC + +#if TARGET_PC if (mpPane) { Vec pos = mpPaneMgr->getGlobalVtxCenter(mpPane, false, 0); mPositionX = pos.x; mPositionY = pos.y; } + + auto& samples = dusk::interp::get(this); + cXyz position(mPositionX, mPositionY, 0.0f); + if (mpPane == nullptr) { + if (mInterpolatePosition) { + position = samples.positions.read(0, position); + } + if (mWorldCursor) { + cXyz world = samples.positions.read(1, mWorldPosition); + Vec projected; + mDoLib_project(&world, &projected); + position.x += projected.x; + position.y += projected.y; + } + } #endif - mpPaneMgr->translate(mPositionX, mPositionY); + + mpPaneMgr->translate(DUSK_IF_ELSE(position.x, mPositionX), DUSK_IF_ELSE(position.y, mPositionY)); if (mpCursorHIO->mDebugON) { mParam1 = mpCursorHIO->mXAxisExpansion; mParam2 = mpCursorHIO->mYAxisExpansion; @@ -281,21 +327,22 @@ void dSelect_cursor_c::update() { if (mUpdateFlag) { if (field_0x30) { if (chkPlayAnime(0)) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mNameIdx == 1) { - field_0x44 += mpCursorHIO->field_0x8 * fVar1; - } else { - field_0x44 += fVar1; - } - - if (field_0x44 >= field_0x30->getFrameMax()) { - field_0x44 -= field_0x30->getFrameMax(); - } +#if TARGET_PC + dusk::vdt::advance_looping_frame( + field_0x44, mNameIdx == 1 ? mpCursorHIO->field_0x8 * fVar1 : fVar1, + field_0x30->getFrameMax()); +#else + if (mNameIdx == 1) { + field_0x44 += mpCursorHIO->field_0x8 * fVar1; + } else { + field_0x44 += fVar1; } + if (field_0x44 >= field_0x30->getFrameMax()) { + field_0x44 -= field_0x30->getFrameMax(); + } +#endif + field_0x30->setFrame(field_0x44); setBpkAnimation(field_0x30); } else { @@ -310,19 +357,20 @@ void dSelect_cursor_c::update() { for (int i = 0; i < 2; i++) { if (field_0x34[i]) { if ((i == 0 && chkPlayAnime(2)) || (i == 1 && chkPlayAnime(3))) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mNameIdx == 1) { - field_0x48[i] += mpCursorHIO->field_0x8 * fVar1; - } else { - field_0x48[i] += fVar1; - } - if (field_0x48[i] >= field_0x34[i]->getFrameMax()) { - field_0x48[i] -= field_0x34[i]->getFrameMax(); - } +#if TARGET_PC + dusk::vdt::advance_looping_frame( + field_0x48[i], mNameIdx == 1 ? mpCursorHIO->field_0x8 * fVar1 : fVar1, + field_0x34[i]->getFrameMax()); +#else + if (mNameIdx == 1) { + field_0x48[i] += mpCursorHIO->field_0x8 * fVar1; + } else { + field_0x48[i] += fVar1; } + if (field_0x48[i] >= field_0x34[i]->getFrameMax()) { + field_0x48[i] -= field_0x34[i]->getFrameMax(); + } +#endif field_0x34[i]->setFrame(field_0x48[i]); } @@ -331,19 +379,20 @@ void dSelect_cursor_c::update() { } if (field_0x2C && chkPlayAnime(1)) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mNameIdx == 1) { - field_0x40 += mpCursorHIO->field_0x8 * fVar1; - } else { - field_0x40 += fVar1; - } - if (field_0x40 >= field_0x2C->getFrameMax()) { - field_0x40 -= field_0x2C->getFrameMax(); - } +#if TARGET_PC + dusk::vdt::advance_looping_frame( + field_0x40, mNameIdx == 1 ? mpCursorHIO->field_0x8 * fVar1 : fVar1, + field_0x2C->getFrameMax()); +#else + if (mNameIdx == 1) { + field_0x40 += mpCursorHIO->field_0x8 * fVar1; + } else { + field_0x40 += fVar1; } + if (field_0x40 >= field_0x2C->getFrameMax()) { + field_0x40 -= field_0x2C->getFrameMax(); + } +#endif field_0x2C->setFrame(field_0x40); setBckAnimation(field_0x2C); @@ -351,12 +400,7 @@ void dSelect_cursor_c::update() { } if (chkPlayAnime(1) && mNameIdx == 0) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - setCursorAnimation(); - } + setCursorAnimation(); } mpScreen->animation(); @@ -454,6 +498,7 @@ void dSelect_cursor_c::setAlphaRate(f32 i_alphaRate) { } mpPaneMgr->setAlphaRate(i_alphaRate); + IF_DUSK(dusk::interp::get(this).alpha.capture(&i_alphaRate, 1)); } int dSelect_cursor_c::addAlpha() { @@ -468,7 +513,7 @@ int dSelect_cursor_c::addAlpha() { } else { alpha_timer++; mpPaneMgr->alphaAnimeStart(alpha_timer); - mpPaneMgr->setAlphaRate(alpha_timer/5.0f); + DUSK_IF_ELSE(setAlphaRate, mpPaneMgr->setAlphaRate)(alpha_timer/5.0f); } return 0; @@ -485,7 +530,7 @@ int dSelect_cursor_c::decAlpha() { } else { alpha_timer--; mpPaneMgr->alphaAnimeStart(alpha_timer); - mpPaneMgr->setAlphaRate(alpha_timer/5.0f); + DUSK_IF_ELSE(setAlphaRate, mpPaneMgr->setAlphaRate)(alpha_timer/5.0f); } return 0; @@ -543,10 +588,14 @@ void dSelect_cursor_c::setCursorAnimation() { fVar1 = 0.5f; } +#if TARGET_PC + dusk::vdt::advance_looping_frame(field_0x40, fVar1, 20.0f); +#else field_0x40 += fVar1; if (field_0x40 >= 20.0f) { field_0x40 -= 20.0f; } +#endif f32 fVar2; f32 param3 = mParam3; fVar2 = field_0x40; @@ -576,7 +625,32 @@ void dSelect_cursor_c::moveCenter(J2DPane* i_pane, f32 i_x, f32 i_y) { i_pane->translate(i_x,i_y); } -#ifdef TARGET_PC +#if TARGET_PC +void dSelect_cursor_c::setInterp(bool enabled) { + if (mInterpolatePosition != enabled) { + mInterpolatePosition = enabled; + dusk::interp::get(this).positions.reset(); + } +} + +void dSelect_cursor_c::setPos(f32 x, f32 y, const cXyz* world) { + auto& positions = dusk::interp::get(this).positions; + if (mWorldCursor != (world != nullptr)) { + positions.reset(); + } + mWorldCursor = world != nullptr; + mPositionX = x; + mPositionY = y; + if (world) { + mpPane = nullptr; + mWorldPosition = *world; + } + if (mInterpolatePosition) { + const cXyz values[] = {cXyz(x, y, 0.0f), mWorldPosition}; + positions.capture(values, mWorldCursor ? 2 : 1); + } +} + void dSelect_cursor_c::refreshAspectScale(f32 param_0) { mParam1 = mBaseParam1 * param_0; } diff --git a/src/d/d_select_icon.cpp b/src/d/d_select_icon.cpp index 32be9c15e2..895146c986 100644 --- a/src/d/d_select_icon.cpp +++ b/src/d/d_select_icon.cpp @@ -2,33 +2,31 @@ #include "d/d_select_icon.h" #include "JSystem/J2DGraph/J2DAnimation.h" -#include "dusk/frame_interpolation.h" + +#if TARGET_PC +#include "dusk/interp/user_interface.h" +#endif dSi_HIO_c::dSi_HIO_c() {} void dSelect_icon_c::animation() { if (field_0x10->getAlpha() != 0) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x20 += field_0x2c; - if (field_0x20 >= field_0x1c->getFrameMax()) { - field_0x20 = 0.0f; - } - field_0x1c->setFrame(field_0x20); - - field_0x28 += field_0x2c; - if (field_0x28 >= field_0x24->getFrameMax()) { - field_0x28 = 0.0f; - } +#if TARGET_PC + dusk::vdt::present_looping(field_0x20, field_0x1c, field_0x2c); + dusk::vdt::present_looping(field_0x28, field_0x24, field_0x2c); +#else + field_0x20 += field_0x2c; + if (field_0x20 >= field_0x1c->getFrameMax()) { + field_0x20 = 0.0f; } -#ifdef TARGET_PC - // FRAME INTERP NOTE: Set even if not advancing field_0x1c->setFrame(field_0x20); -#endif + field_0x28 += field_0x2c; + if (field_0x28 >= field_0x24->getFrameMax()) { + field_0x28 = 0.0f; + } field_0x24->setFrame(field_0x28); +#endif field_0x8->animation(); field_0x10->paneScale(field_0x14, field_0x14); } diff --git a/src/d/d_shop_system.cpp b/src/d/d_shop_system.cpp index 1ff45e19a7..2b26ca2a4a 100644 --- a/src/d/d_shop_system.cpp +++ b/src/d/d_shop_system.cpp @@ -19,6 +19,9 @@ #include "d/actor/d_a_tag_shop_item.h" #include +#if TARGET_PC +#include "dusk/interp/lerp.h" +#endif static daTag_ShopItem_c* dShopSystem_itemActor[7] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, @@ -876,10 +879,14 @@ int dShopSystem_c::seq_start(fopAc_ac_c* actor, dMsgFlow_c* i_flow) { } cXyz pos3d; - cXyz pos2d; + cXyz pos2d IF_DUSK((0.0f, 0.0f, 0.0f)); pos3d.set(mItemCtrl.getCurrentPos(0)); +#if TARGET_PC + mpDrawCursor->setWorldPos(pos3d, 0.0f, g_cursorHIO.mShopCursorOffsetY); +#else pos3Dto2D(&pos3d, &pos2d); mpDrawCursor->setPos(pos2d.x, pos2d.y + g_cursorHIO.mShopCursorOffsetY); +#endif if (chkSpMode() && !beforeStartSeqAction(i_flow, field_0xf5c)) { return 0; @@ -1093,9 +1100,9 @@ int dShopSystem_c::seq_select(fopAc_ac_c* actor, dMsgFlow_c* i_flow) { if (old_cursor != 0) { cXyz pos3d; - cXyz pos2d; + cXyz pos2d IF_DUSK((0.0f, 0.0f, 0.0f)); pos3d.set(mItemCtrl.getCurrentPos(old_cursor - 1)); - pos3Dto2D(&pos3d, &pos2d); + IF_NOT_DUSK(pos3Dto2D(&pos3d, &pos2d)); if (old_cursor == 7) { pos2d.x += g_cursorHIO.mMagicArmorCursorOffsetX; @@ -1104,7 +1111,11 @@ int dShopSystem_c::seq_select(fopAc_ac_c* actor, dMsgFlow_c* i_flow) { pos2d.y += g_cursorHIO.mShopCursorOffsetY; } +#if TARGET_PC + mpDrawCursor->setWorldPos(pos3d, pos2d.x, pos2d.y); +#else mpDrawCursor->setPos(pos2d.x, pos2d.y); +#endif } return 0; @@ -1115,11 +1126,11 @@ int dShopSystem_c::seq_moving(fopAc_ac_c*, dMsgFlow_c*) { cXyz last_pos3d; cXyz pos3d; - cXyz last_pos2d; - cXyz pos2d; + cXyz last_pos2d IF_DUSK((0.0f, 0.0f, 0.0f)); + cXyz pos2d IF_DUSK((0.0f, 0.0f, 0.0f)); pos3d.set(mItemCtrl.getCurrentPos(mCursorPos - 1)); - pos3Dto2D(&pos3d, &pos2d); + IF_NOT_DUSK(pos3Dto2D(&pos3d, &pos2d)); if (mCursorPos == 7) { pos2d.x += g_cursorHIO.mMagicArmorCursorOffsetX; @@ -1130,7 +1141,7 @@ int dShopSystem_c::seq_moving(fopAc_ac_c*, dMsgFlow_c*) { if (mLastCursorPos != 0) { last_pos3d.set(mItemCtrl.getCurrentPos(mLastCursorPos - 1)); - pos3Dto2D(&last_pos3d, &last_pos2d); + IF_NOT_DUSK(pos3Dto2D(&last_pos3d, &last_pos2d)); if (mLastCursorPos == 7) { last_pos2d.x += g_cursorHIO.mMagicArmorCursorOffsetX; @@ -1140,11 +1151,22 @@ int dShopSystem_c::seq_moving(fopAc_ac_c*, dMsgFlow_c*) { } f32 tmp = (f32)(field_0xf68 * field_0xf68) / 9.0f; +#if TARGET_PC + cXyz position; + dusk::interp::lerp(position, last_pos3d, pos3d, tmp); + dusk::interp::lerp(pos2d, last_pos2d, pos2d, tmp); + mpDrawCursor->setWorldPos(position, pos2d.x, pos2d.y); +#else mpDrawCursor->setPos(last_pos2d.x + tmp * (pos2d.x - last_pos2d.x), last_pos2d.y + tmp * (pos2d.y - last_pos2d.y)); +#endif } else { +#if TARGET_PC + mpDrawCursor->setWorldPos(pos3d, pos2d.x, pos2d.y); +#else mpDrawCursor->setPos(pos2d.x, pos2d.y); +#endif } if (field_0xf68 >= 3) { diff --git a/src/d/d_timer.cpp b/src/d/d_timer.cpp index e07cc3af80..8b138d507c 100644 --- a/src/d/d_timer.cpp +++ b/src/d/d_timer.cpp @@ -23,8 +23,10 @@ #include "m_Do/m_Do_lib.h" #include -#include "dusk/frame_interpolation.h" +#if TARGET_PC +#include "dusk/interp/user_interface.h" #include "dusk/version.hpp" +#endif static int dTimer_createStart2D(s32 param_0, u16 param_1); @@ -213,6 +215,9 @@ int dTimer_c::_execute() { } if (m_timer_mode == 3 || m_timer_mode == 4 || m_timer_mode == 6 || m_timer_mode == 5) { +#if TARGET_PC + mp_tm_scrn->mPresentTimerSlide = true; +#else f32 pos_y = mp_tm_scrn->getTimerTransY(); if (pos_y < 145.0f) { pos_y += 25.0f; @@ -222,6 +227,7 @@ int dTimer_c::_execute() { mp_tm_scrn->setTimerTrans(0.0f, pos_y); } +#endif } break; default: @@ -881,6 +887,7 @@ void dDlst_TimerScrnDraw_c::setTimer(int i_time) { } } +#if !TARGET_PC if (mpTimeParent != NULL) { if ((dComIfGp_event_getMode() == 1 && field_0x3DF != 0) || !isVisible()) { s16 alpha_time = mpTimeParent->getAlphaTimer(); @@ -900,6 +907,7 @@ void dDlst_TimerScrnDraw_c::setTimer(int i_time) { } } } +#endif } void dDlst_TimerScrnDraw_c::setCounter(u8 i_count, u8 i_max) { @@ -1004,6 +1012,7 @@ void dDlst_TimerScrnDraw_c::setCounter(u8 i_count, u8 i_max) { } } +#if !TARGET_PC if ((dComIfGp_event_getMode() == 1 && field_0x3E0 != 0) || !isVisible()) { if (mpCowParent != NULL) { s16 alpha_time = mpCowParent->getAlphaTimer(); @@ -1053,6 +1062,7 @@ void dDlst_TimerScrnDraw_c::setCounter(u8 i_count, u8 i_max) { } } } +#endif } void dDlst_TimerScrnDraw_c::setParentPos(f32 i_posX, f32 i_posY) { @@ -1110,17 +1120,24 @@ void dDlst_TimerScrnDraw_c::setShowType(u8 i_type) { } void dDlst_TimerScrnDraw_c::anime() { +#if TARGET_PC + if (!mPresentStep) { + mPresentOpening = true; + return; + } + const f32 previousFrame = field_0x3D8; +#endif static const s16 animeFrame[] = {7, 15, 22}; if (field_0x3DE == 0) { if (field_0x3D8 <= animeFrame[2]) { - field_0x3D8++; + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(field_0x3D8, 23.0f, 1.0f), field_0x3D8++); } else { field_0x3DE = 1; } - if (field_0x3D8 <= animeFrame[1]) { - f32 temp_f31 = acc(animeFrame[1], field_0x3D8, 0); + if (DUSK_IF_ELSE(previousFrame, field_0x3D8) <= animeFrame[1]) { + f32 temp_f31 = acc(animeFrame[1], DUSK_IF_ELSE(std::min(field_0x3D8, (f32)animeFrame[1]), field_0x3D8), 0); f32 temp_f1 = (1.0f - temp_f31) * -50.0f; if (mpTimeParent != NULL) { @@ -1144,13 +1161,13 @@ void dDlst_TimerScrnDraw_c::anime() { } } - if (field_0x3D8 == animeFrame[1]) { + if (field_0x3D8 DUSK_IF_ELSE(>=, ==) animeFrame[1]) { field_0x3DF = 1; } } - if (field_0x3D8 > animeFrame[0] && field_0x3D8 <= animeFrame[2]) { - f32 temp_f31 = acc(animeFrame[1], field_0x3D8 - animeFrame[0], 0); + if (field_0x3D8 > animeFrame[0] && DUSK_IF_ELSE(previousFrame, field_0x3D8) <= animeFrame[2]) { + f32 temp_f31 = acc(animeFrame[1], DUSK_IF_ELSE(std::min(field_0x3D8, (f32)animeFrame[2]), field_0x3D8) - animeFrame[0], 0); f32 temp_f1 = (1.0f - temp_f31) * -50.0f; if (mpCowParent != NULL) { @@ -1171,7 +1188,7 @@ void dDlst_TimerScrnDraw_c::anime() { g_drawHIO.mMiniGame.mIconSizeY[mHIOType]); } - if (field_0x3D8 == animeFrame[2]) { + if (field_0x3D8 DUSK_IF_ELSE(>=, ==) animeFrame[2]) { field_0x3E0 = 1; } } @@ -1179,8 +1196,14 @@ void dDlst_TimerScrnDraw_c::anime() { } BOOL dDlst_TimerScrnDraw_c::closeAnime() { +#if TARGET_PC + if (!mPresentStep) { + mPresentClosing = true; + return field_0x3D8 >= 7.0f; + } +#endif BOOL var_r31 = false; - field_0x3D8++; + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(field_0x3D8, 7.0f, 1.0f), field_0x3D8++); if (field_0x3D8 <= 7) { f32 temp_f31 = acc(7, field_0x3D8, 0); @@ -1330,7 +1353,54 @@ s32 dDlst_TimerScrnDraw_c::createStart(u16 i_messageID) { return 0; } +#if TARGET_PC +void dDlst_TimerScrnDraw_c::presentAnims() { + mPresentStep = true; + if (mPresentTimerSlide) { + dusk::vdt::advance_toward_frame(mTimerTransY, 145.0f, 25.0f); + if (field_0x3e2 != 0 && mHIOType == 0) { + setTimerPos(g_drawHIO.mMiniGame.mTimerPosX_4x3, + g_drawHIO.mMiniGame.mTimerPosY_4x3); + } else { + setTimerPos(g_drawHIO.mMiniGame.mTimerPosX[mHIOType], + g_drawHIO.mMiniGame.mTimerPosY[mHIOType]); + } + } + if (mPresentClosing) { + closeAnime(); + } else { + if (mPresentOpening) { + anime(); + } + const bool event = dComIfGp_event_getMode() == 1; + auto fade = [&](CPaneMgr* pane, bool hidden, bool ready, f32 alpha, bool timer) { + if (pane == NULL) { + return; + } + f32 frame = pane->getAlphaTimer(); + const f32 previous = frame; + dusk::vdt::advance_toward_frame(frame, hidden ? 5.0f : 0.0f, 1.0f); + if (ready || frame != previous) { + const f32 rate = timer ? acc(5.0f, 5.0f - frame, 0.0f) : + 1.0f - acc(5.0f, frame, 0.0f); + pane->setAlphaRate(mParentAlpha * alpha * rate); + } + pane->alphaAnimeStart(frame); + }; + fade(mpTimeParent, (event && field_0x3DF != 0) || !isVisible(), field_0x3DF != 0, + g_drawHIO.mMiniGame.mTimerAlpha[mHIOType], true); + const bool hideCounter = (event && field_0x3E0 != 0) || !isVisible(); + fade(mpCowParent, hideCounter, field_0x3E0 != 0, + g_drawHIO.mMiniGame.mCounterAlpha[mHIOType], false); + fade(mpImageParent, hideCounter, field_0x3E0 != 0, + g_drawHIO.mMiniGame.mIconAlpha[mHIOType], false); + } + mPresentStep = false; +} +#endif + void dDlst_TimerScrnDraw_c::draw() { + IF_DUSK(presentAnims()); J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort(); graf_ctx->setup2D(); @@ -1340,21 +1410,20 @@ void dDlst_TimerScrnDraw_c::draw() { ((f32)g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f); for (int i = 0; i < 51; i++) { + if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { + if (m_getin_info[i].bck_frame < 60.0f) { #if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { - if (m_getin_info[i].bck_frame < 60.0f) { - m_getin_info[i].bck_frame += g_drawHIO.mMiniGame.mGetInTextAnimSpeed; - if (m_getin_info[i].bck_frame > 60.0f) { - m_getin_info[i].bck_frame = 60.0f; - } - } else if (m_getin_info[i].bck_frame < g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f) { - m_getin_info[i].bck_frame++; - } else if (m_getin_info[i].bck_frame < temp) { - m_getin_info[i].bck_frame++; + dusk::vdt::advance_toward_frame(m_getin_info[i].bck_frame, 60.0f, g_drawHIO.mMiniGame.mGetInTextAnimSpeed); +#else + m_getin_info[i].bck_frame += g_drawHIO.mMiniGame.mGetInTextAnimSpeed; + if (m_getin_info[i].bck_frame > 60.0f) { + m_getin_info[i].bck_frame = 60.0f; } +#endif + } else if (m_getin_info[i].bck_frame < g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f) { + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(m_getin_info[i].bck_frame, (f32)g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f, 1.0f), m_getin_info[i].bck_frame++); + } else if (m_getin_info[i].bck_frame < temp) { + DUSK_IF_ELSE(dusk::vdt::advance_toward_frame(m_getin_info[i].bck_frame, temp, 1.0f), m_getin_info[i].bck_frame++); } } @@ -1396,19 +1465,14 @@ void dDlst_TimerScrnDraw_c::draw() { if (m_getin_info[i].pikari_frame > 0.0f) { drawPikari(i); } else if (m_getin_info[i].pikari_frame == -1.0f) { -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (m_getin_info[i].field_0xc == 0) { - if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mGetInPikariAppearFrames) { - m_getin_info[i].pikari_frame = - 18.0f - g_drawHIO.mMiniGame.mGetInPikariAnimSpeed; - } - } else if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mStartPikariAppearFrames) { + if (m_getin_info[i].field_0xc == 0) { + if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mGetInPikariAppearFrames) { m_getin_info[i].pikari_frame = - 18.0f - g_drawHIO.mMiniGame.mStartPikariAnimSpeed; + 18.0f - g_drawHIO.mMiniGame.mGetInPikariAnimSpeed; } + } else if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mStartPikariAppearFrames) { + m_getin_info[i].pikari_frame = + 18.0f - g_drawHIO.mMiniGame.mStartPikariAnimSpeed; } } } diff --git a/src/dusk/archive.cpp b/src/dusk/archive.cpp new file mode 100644 index 0000000000..c9ba131bc8 --- /dev/null +++ b/src/dusk/archive.cpp @@ -0,0 +1,175 @@ +#include "archive.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace dusk::archive { +namespace { + +constexpr std::array ZipMagic{'P', 'K', '\x03', '\x04'}; + +PackageFormat detect_package_format(mz_zip_archive& zip) { + size_t modManifests = 0; + for (mz_uint index = 0, count = mz_zip_reader_get_num_files(&zip); index < count; ++index) { + mz_zip_archive_file_stat stat{}; + if (!mz_zip_reader_file_stat(&zip, index, &stat) || + mz_zip_reader_is_file_a_directory(&zip, index)) + { + continue; + } + const std::string_view name{stat.m_filename}; + modManifests += name == "mod.json"; + } + if (modManifests == 1) { + return PackageFormat::Mod; + } + return PackageFormat::Unknown; +} + +} // namespace + +struct ZipArchive::Impl { + ~Impl() { + if (open) { + mz_zip_reader_end(&zip); + } + } + + static size_t read_zip(void* opaque, mz_uint64 offset, void* buffer, const size_t size) { + auto& archive = *static_cast(opaque); + std::error_code error; + return archive.file.read_at(offset, {static_cast(buffer), size}, error); + } + + borealis::io::RandomAccessFile file; + mz_zip_archive zip{}; + PackageFormat format = PackageFormat::Unknown; + bool open = false; + std::mutex mutex; +}; + +ZipArchive::ZipArchive(const std::filesystem::path& path) : m_impl{std::make_unique()} { + auto opened = borealis::io::RandomAccessFile::open(path); + if (opened.status != borealis::io::Status::Ok) { + throw std::runtime_error(opened.message); + } + m_impl->file = std::move(opened.file); + + std::array header{}; + std::error_code error; + const auto read = m_impl->file.read_at( + 0, {reinterpret_cast(header.data()), header.size()}, error); + if (error) { + throw std::runtime_error(fmt::format("Reading ZIP magic failed: {}", error.message())); + } + if (read != header.size() || header != ZipMagic) { + throw std::runtime_error("File does not have ZIP magic"); + } + + m_impl->zip.m_pRead = Impl::read_zip; + m_impl->zip.m_pIO_opaque = m_impl.get(); + if (!mz_zip_reader_init(&m_impl->zip, m_impl->file.size(), 0)) { + const auto zipError = mz_zip_get_last_error(&m_impl->zip); + throw std::runtime_error( + fmt::format("Opening ZIP failed: {}", mz_zip_get_error_string(zipError))); + } + m_impl->open = true; + m_impl->format = detect_package_format(m_impl->zip); +} + +ZipArchive::~ZipArchive() = default; + +ZipArchive::ZipArchive(ZipArchive&&) noexcept = default; +ZipArchive& ZipArchive::operator=(ZipArchive&&) noexcept = default; + +PackageFormat ZipArchive::package_format() const noexcept { + return m_impl->format; +} + +std::vector ZipArchive::read_file(const std::string_view name) { + std::lock_guard lock{m_impl->mutex}; + const std::string fileName{name}; + size_t size = 0; + void* extracted = mz_zip_reader_extract_file_to_heap(&m_impl->zip, fileName.c_str(), &size, 0); + if (extracted == nullptr) { + throw std::runtime_error(fmt::format("File does not exist: {}", name)); + } + + const std::unique_ptr owner{extracted, &mz_free}; + const std::span data{static_cast(owner.get()), size}; + std::vector result; + result.assign(data.begin(), data.end()); + return result; +} + +std::vector ZipArchive::file_names() { + std::lock_guard lock{m_impl->mutex}; + std::vector results; + for (mz_uint index = 0, count = mz_zip_reader_get_num_files(&m_impl->zip); index < count; + ++index) + { + mz_zip_archive_file_stat stat{}; + if (!mz_zip_reader_file_stat(&m_impl->zip, index, &stat) || + mz_zip_reader_is_file_a_directory(&m_impl->zip, index)) + { + continue; + } + results.emplace_back(stat.m_filename); + } + return results; +} + +std::optional stat_zip_entry( + mz_zip_archive* pZip, std::string_view name) { + const std::string fileName{name}; + uint32_t fileIdx; + if (!mz_zip_reader_locate_file_v2(pZip, fileName.c_str(), nullptr, 0, &fileIdx)) { + return std::nullopt; + } + + mz_zip_archive_file_stat stat; + if (!mz_zip_reader_file_stat(pZip, fileIdx, &stat)) { + return std::nullopt; + } + + return stat; +} + +bool ZipArchive::file_exists(const std::string& name) const { + std::lock_guard lock{m_impl->mutex}; + auto result = stat_zip_entry(&m_impl->zip, name); + if (!result.has_value()) { + return false; + } + + return !result->m_is_directory; +} + +bool ZipArchive::directory_exists(const std::string& name) const { + std::lock_guard lock{m_impl->mutex}; + auto result = stat_zip_entry(&m_impl->zip, name); + if (!result.has_value()) { + return false; + } + + return result->m_is_directory; +} + +size_t ZipArchive::file_size(const std::string& name) { + std::lock_guard lock{m_impl->mutex}; + auto stat = stat_zip_entry(&m_impl->zip, name); + if (!stat.has_value()) { + throw std::runtime_error(fmt::format("Unable to locate file in ZIP: {}", name)); + } + + return static_cast(stat->m_uncomp_size); +} + +} // namespace dusk::archive diff --git a/src/dusk/archive.hpp b/src/dusk/archive.hpp new file mode 100644 index 0000000000..5005620073 --- /dev/null +++ b/src/dusk/archive.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::archive { + +enum class PackageFormat { + Unknown, + Mod, + // Save, +}; + +class ZipArchive { +public: + explicit ZipArchive(const std::filesystem::path& path); + ~ZipArchive(); + + ZipArchive(ZipArchive&&) noexcept; + ZipArchive& operator=(ZipArchive&&) noexcept; + ZipArchive(const ZipArchive&) = delete; + ZipArchive& operator=(const ZipArchive&) = delete; + + PackageFormat package_format() const noexcept; + std::vector read_file(std::string_view name); + std::vector file_names(); + size_t file_size(const std::string& name); + bool file_exists(const std::string& name) const; + bool directory_exists(const std::string& name) const; + +private: + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace dusk::archive diff --git a/src/dusk/audio/DuskAudioSystem.cpp b/src/dusk/audio/DuskAudioSystem.cpp index 13275ed830..bdb2acaf65 100644 --- a/src/dusk/audio/DuskAudioSystem.cpp +++ b/src/dusk/audio/DuskAudioSystem.cpp @@ -20,7 +20,7 @@ using namespace dusk::audio; static OutputSubframe OutBuffer; -static std::array OutInterleaveBuffer; +static std::array OutInterleaveBufferFull; static SDL_AudioStream* PlaybackStream; @@ -43,24 +43,62 @@ static int RenderNewAudioFrame(); /** * Render an audio subframe and output it to SDL3. */ -static void RenderAudioSubframe(); +static int RenderAudioSubframe(); -static void InitSDL3Output() { - SDL_Init(SDL_INIT_AUDIO); +static size_t GetChannelCountForOutputMode(dusk::AudioOutputMode config) { + switch (config) { + default: + case dusk::AudioOutputMode::StereoSpeakers: + case dusk::AudioOutputMode::StereoHeadphones: + return 2; + case dusk::AudioOutputMode::Surround6ch: + return 6; + case dusk::AudioOutputMode::Surround8ch: + return 8; + } +} - constexpr SDL_AudioSpec spec = { +static bool InitSDL3Output() { + const auto speakerConfig = dusk::getSettings().audio.outputMode.getValue(); + const auto desiredChannelCount = GetChannelCountForOutputMode(speakerConfig); + const bool hrtf = speakerConfig == dusk::AudioOutputMode::StereoHeadphones; + + if (PlaybackStream && desiredChannelCount == OutChannelCount) { + JASCriticalSection section; + EnableHrtf = hrtf; + return false; + } + + if (PlaybackStream) { + SDL_PauseAudioStreamDevice(PlaybackStream); + SDL_DestroyAudioStream(PlaybackStream); + } else { + SDL_Init(SDL_INIT_AUDIO); + } + + const SDL_AudioSpec spec = { SDL_AUDIO_F32, - 2, + static_cast(desiredChannelCount), SampleRate, }; - PlaybackStream = SDL_OpenAudioDeviceStream( - SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, - &spec, - &GetNewAudio, - nullptr); + SDL_AudioStream* newStream = + SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, &GetNewAudio, nullptr); + + { + JASCriticalSection section; + EnableHrtf = hrtf; + OutChannelCount = desiredChannelCount; + PlaybackStream = newStream; + } + + return true; } void dusk::audio::Initialize() { + // enable 48 kHz mode + // this will scale voice pitch and track tempo accordingly + JASDriver::setOutputRate(static_cast(-1)); + InitSDL3Output(); DspInit(); @@ -72,6 +110,22 @@ void dusk::audio::Initialize() { SDL_ResumeAudioStreamDevice(PlaybackStream); } +void dusk::audio::Reinitialize() { + // don't re-init unless we've initialized first (using PlaybackStream being set as proxy) + if (PlaybackStream && InitSDL3Output()) { + SDL_ResumeAudioStreamDevice(PlaybackStream); + } +} + +void dusk::audio::Shutdown() { + if (PlaybackStream) { + SDL_DestroyAudioStream(PlaybackStream); + PlaybackStream = nullptr; + } + + SDL_QuitSubSystem(SDL_INIT_AUDIO); +} + void dusk::audio::SetMasterVolume(const f32 value) { JASCriticalSection section; @@ -113,53 +167,122 @@ int RenderNewAudioFrame() { ZoneScoped; JASCriticalSection section; const u32 countSubframes = JASDriver::getSubFrames(); + int bytesWritten = 0; JASAudioThread::setDSPSyncCount(countSubframes); for (u32 i = 0; i < countSubframes; i++) { - RenderAudioSubframe(); + bytesWritten += RenderAudioSubframe(); JASAudioThread::snIntCount -= 1; } - return static_cast(countSubframes) * sizeof(OutputSubframe); + return bytesWritten; } static void InterleaveOutputData(const OutputSubframe& data, std::span target) { - assert(target.size() >= data.channels[0].size() * OutputSubframe::NUM_CHANNELS); + assert(target.size() >= data.channels[0].size() * OutChannelCount); size_t outPos = 0; for (size_t inPos = 0; inPos < data.channels[0].size(); inPos++) { - for (size_t channelIdx = 0; channelIdx < OutputSubframe::NUM_CHANNELS; channelIdx++) { + for (size_t channelIdx = 0; channelIdx < OutChannelCount; channelIdx++) { target[outPos++] = data.channels[channelIdx][inPos]; } } } -void RenderAudioSubframe() { +constexpr auto kExtChannels = 2; +constexpr auto kExtSrcInSampleCount = DSP_SUBFRAME_SIZE * 2; +constexpr auto kExtSrcOutSampleCount = DSP_SUBFRAME_SIZE * 3; + +// simple 3:2 resampler +struct LinearResampler { + f32 prev = 0.0f; + int phase = 0; + + void resample(std::span in, std::span out, int skip = 1, int offset = 0) { + const auto inSize = in.size() / skip; + const auto outSize = out.size() / skip; + assert(inSize * 3 == outSize * 2); + + int idx = -1; + int phi = phase; + int n = 0; + + while (n < outSize) { + f32 s0 = (idx < 0) ? prev : in[idx * skip + offset] / 32768.0f; + f32 s1 = in[idx * skip + skip + offset] / 32768.0f; + f32 frac = phi * (1.0f / 3.0f); + + out[n++ * skip + offset] = s0 + frac * (s1 - s0); + + phi += 2; + if (phi >= 3) { + phi -= 3; + idx++; + } + } + + prev = in[(inSize - 1) * skip + offset] / 32768.0f; + phase = phi; + } +}; + +static struct { + std::array buf; + int available = 0; + LinearResampler leftSrc; + LinearResampler rightSrc; + + bool hungry() const { return available <= 0; } + + void feed(std::span in) { + const auto out = std::span{buf}; + leftSrc.resample(in, out, kExtChannels, 0); + rightSrc.resample(in, out, kExtChannels, 1); + available = buf.size(); + } + + std::span drain_subframe() { + constexpr auto kDrainSize = DSP_SUBFRAME_SIZE * kExtChannels; + assert(available >= kDrainSize); + const auto rv = std::span{&buf[buf.size() - available], kDrainSize}; + available -= kDrainSize; + return rv; + } +} extResampler; + +int RenderAudioSubframe() { ZoneScoped; OutBuffer = {}; JASDriver::updateDSP(); DspRender(OutBuffer); + std::span OutInterleaveBuffer{OutInterleaveBufferFull.data(), static_cast(DSP_SUBFRAME_SIZE * OutChannelCount)}; InterleaveOutputData(OutBuffer, OutInterleaveBuffer); if (JASDriver::extMixCallback != nullptr && JASDriver::sMixMode == MIX_MODE_INTERLEAVE) { - static_assert(OutputSubframe::NUM_CHANNELS == 2); // This code only works with Stereo so far. - // NOTE: In the real game, this gets called on the entire audio frame, rather than the subframe. - // That's probably more efficient, but I didn't wanna change the code to calculate the - // entire audio buffers at once. - // This is only used for the movie player, and it seems to work fine with the smaller calls. - const auto mixData = JASDriver::extMixCallback(DSP_SUBFRAME_SIZE); - if (mixData) { - for (int i = 0; i < OutInterleaveBuffer.size(); i++) { - OutInterleaveBuffer[i] += static_cast(mixData[i]) / static_cast(0x7FFF); + if (extResampler.hungry()) { + const auto mixData = JASDriver::extMixCallback(kExtSrcInSampleCount); + if (mixData) { + extResampler.feed(std::span{mixData, kExtSrcInSampleCount * kExtChannels}); + } + } + + if (!extResampler.hungry()) { + const auto inBuf = extResampler.drain_subframe(); + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + const auto oi = i * OutChannelCount; + OutInterleaveBuffer[oi] += inBuf[i * kExtChannels]; + OutInterleaveBuffer[oi + 1] += inBuf[i * kExtChannels + 1]; } } } - SDL_PutAudioStreamData(PlaybackStream, &OutInterleaveBuffer, sizeof(OutInterleaveBuffer)); + auto bytesToWrite = OutInterleaveBuffer.size_bytes(); + SDL_PutAudioStreamData(PlaybackStream, OutInterleaveBuffer.data(), bytesToWrite); + return bytesToWrite; } u32 dusk::audio::GetResetCount(int channelIdx) { diff --git a/src/dusk/audio/DuskAudioSystem.h b/src/dusk/audio/DuskAudioSystem.h index 780187f9d2..8a9f9634b7 100644 --- a/src/dusk/audio/DuskAudioSystem.h +++ b/src/dusk/audio/DuskAudioSystem.h @@ -19,6 +19,10 @@ namespace dusk::audio { */ void Initialize(); + void Reinitialize(); + + void Shutdown(); + void SetEnableReverb(bool value); void SetMasterVolume(f32 value); diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index 01b07be90c..92bfb74cb3 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include using namespace dusk::audio; @@ -50,11 +52,11 @@ bool dusk::audio::EnableReverb = true; bool dusk::audio::DumpAudio = false; bool dusk::audio::EnableHrtf = false; f32 dusk::audio::HrtfGain = 0.5f; - +u8 dusk::audio::OutChannelCount = 0; // 3dB at 5kHz. -static constexpr f32 HRTF_LP_K = 0.75f; -static constexpr f32 HRTF_ALLPASS_G = 0.3f; +static constexpr f32 HRTF_LP_K = (SampleRate == 48000) ? 0.6267f : 0.75f; +static constexpr f32 HRTF_ALLPASS_G = (SampleRate == 48000) ? 0.448f : 0.3f; // Front never drops below (1 - HRTF_EXTRACT_MAX). static constexpr f32 HRTF_EXTRACT_MAX = 0.6f; @@ -101,28 +103,6 @@ static u32 ConvertSamplesToDataLength(const JASDsp::TChannel& channel, u32 sampl return (samples / channel.mSamplesPerBlock) * BlockBytes(channel); } -/** - * Render the audio data contributed by a single DSP channel. Reads & decodes new input samples. - */ -static void RenderChannel( - JASDsp::TChannel& channel, - ChannelAuxData& channelAux, - OutputSubframe& subframe); - -static void RenderOutputChannel( - const JASDsp::TChannel& sourceChannel, - ChannelAuxData& aux, - OutputChannel outputChannel, - const std::span inputSamples, - OutputSubframe& fullOutputSubframe); - -/** - * Converts a pitch value on a DSP channel to a sample rate. - */ -constexpr static int PitchToSampleRate(u16 value) { - return static_cast(static_cast(SampleRate) * value / 4096); -} - /** * Reset state for a DSP channel between independent playbacks. */ @@ -164,6 +144,12 @@ static void MixSubframe(DspSubframe& dst, const DspSubframe& src) { } } +static void MixOutputSubframe(OutputSubframe& dst, const OutputSubframe& src) { + for (int i = 0; i < OutChannelCount; i++) { + MixSubframe(dst.channels[i], src.channels[i]); + } +} + enum class OscType : u16 { SQUARE_WAVE_PW_50 = 0, SAW_WAVE = 1, @@ -203,16 +189,14 @@ static void GenerateEvolvingHarmonic() { } } - static void RenderOscChannel( JASDsp::TChannel& channel, ChannelAuxData& channelAux, - OutputSubframe& subframe) { + DspSubframe& buf) { if (channel.mResetFlag) ResetChannel(channel, channelAux); const u32 pitch = channel.mPitch; - DspSubframe buf = {}; const auto oscType = static_cast(channel.mBytesPerBlock); switch (oscType) { @@ -270,142 +254,6 @@ static void RenderOscChannel( DuskLog.error("RenderOscChannel: unimplemented oscillator type {}", channel.mBytesPerBlock); break; } - - auto samples = std::span(buf).subspan(0, DSP_SUBFRAME_SIZE); - RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, samples, subframe); - RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, samples, subframe); -} - - -void dusk::audio::DspRender(OutputSubframe& subframe) { - ZoneScoped; - if (DumpAudio != sDumpWasActive) { - sDumpWasActive = DumpAudio; - if (DumpAudio) { - OpenChannelDumpFiles(); - } else { - CloseChannelDumpFiles(); - } - } - - GenerateEvolvingHarmonic(); - - std::span channels(JASDsp::CH_BUF, DSP_CHANNELS); - - DspSubframe reverbInputL = {}; - 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]; - - if (!channel.mIsActive) { - continue; - } - else if (channel.mPauseFlag) { - // Not really sure what the practical difference between pause and - // deactivation is. Either avoids clearing state or allows the DSP to avoid popping? - continue; - } - else if (channel.mForcedStop) { - channel.mIsFinished = true; - continue; - } - - OutputSubframe channelSubframe = {}; - if (channel.mWaveAramAddress == 0) { - RenderOscChannel(channel, channelAux, channelSubframe); - } else { - ValidateChannel(channel); - RenderChannel(channel, channelAux, channelSubframe); - } - - if (EnableReverb) { - // scale the input to the reverb rather than using wet/dry on the output. - // this way the reverb's internal buffers accumulate energy proportional to mAutoMixerFxMix, - // so any tail always decays at the correct level regardless of mAutoMixerFxMix changes - // prevents transients when the next sound starts playing with a different reverb level - // 600.0f was pulled out of my ass and just sounds good enough for console - f32 inputGain = (channel.mAutoMixerFxMix >> 8) / 600.0f; - if (inputGain > 0) { - anyReverbInput = true; - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - reverbInputL[j] += channelSubframe.channels[0][j] * inputGain; - reverbInputR[j] += channelSubframe.channels[1][j] * inputGain; - } - } - } - - 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++) { - interleaved[j * 2 + 0] = channelSubframe.channels[0][j]; - interleaved[j * 2 + 1] = channelSubframe.channels[1][j]; - } - fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]); - } - - for (int o = 0; o < subframe.channels.size(); o++) { - MixSubframe(subframe.channels[o], channelSubframe.channels[o]); - } - } - - if (EnableReverb && (anyReverbInput || ReverbHasTail)) { - // Equivalent to -80 dBFS: rms = 1e-4, rms^2 = 1e-8, sumSq = 2 * N * 1e-8 - constexpr f32 REVERB_ENERGY_EPSILON = 2.0f * DSP_SUBFRAME_SIZE * 1e-8f; - f32 wetEnergy = SharedReverb.processmix( - reverbInputL.data(), reverbInputR.data(), - subframe.channels[0].data(), subframe.channels[1].data(), - DSP_SUBFRAME_SIZE, 1, 1.0f - ); - 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); - } - PrevMasterVolume = MasterVolume; } /** @@ -455,7 +303,8 @@ static int ReadChannelSamplesChunk( assert(desiredSamples >= 0); - auto aramBase = static_cast(ARGetStorageAddress()) + channel.mWaveAramAddress; + auto aramBase = static_cast(channel.mAramBaseAddress ? channel.mAramBaseAddress : ARGetStorageAddress()); + aramBase += channel.mWaveAramAddress; auto curSamplePosition = channel.mSamplePosition; u32 skipSamples = curSamplePosition % channel.mSamplesPerBlock; @@ -539,37 +388,154 @@ static void FillDecodeBuf(JASDsp::TChannel& channel, ChannelAuxData& aux, int ne } /** - * Get the expected BusConnect value needed to define the given output channel in a DSP channel. + * Adapts IIR biquad filter coeffs for fs = 32kHz -> 48kHz, Hz-preserving. */ -constexpr u16 GetBusConnect(const OutputChannel channel) { - switch (channel) { - // TODO: This is a guess for now. - case OutputChannel::LEFT: - return 0x0D00; - case OutputChannel::RIGHT: - return 0x0D60; - default: - CRASH("Invalid output channel!"); +static BiquadCoeffs RemapBiquad32to48(float b1, float b2, float a1, float a2) { + using cplx = std::complex; + + constexpr auto kOldRate = 32000.0; + constexpr auto kNewRate = f64(SampleRate); + constexpr auto kGamma = kOldRate / kNewRate; + + if (b1 == 0 && b2 == 0 && a1 == 0 && a2 == 0) { + return {0, 0, 0, 0}; } + + auto mapPoint = [](cplx z) { + if (std::abs(z.imag()) < 1e-9 * std::max(std::abs(z.real()), 1.0)) { + f64 r = z.real(); + f64 sign = (r >= 0.0) ? 1.0 : -1.0; + return cplx(sign * std::pow(std::abs(r), kGamma), 0.0); + } + f64 r = std::abs(z); + f64 theta = std::arg(z); + return std::polar(std::pow(r, kGamma), theta * kGamma); + }; + + f64 disc = f64(a1) * a1 + 4.0 * a2; + cplx p1, p2; + if (disc >= 0.0) { + f64 sq = std::sqrt(disc); + p1 = {(a1 + sq) * 0.5, 0.0}; + p2 = {(a1 - sq) * 0.5, 0.0}; + } else { + f64 sq = std::sqrt(-disc); + p1 = {a1 * 0.5, sq * 0.5}; + p2 = {a1 * 0.5, -sq * 0.5}; + } + cplx p1n = mapPoint(p1); + cplx p2n = mapPoint(p2); + f64 a1n = (p1n + p2n).real(); + f64 a2n = -(p1n * p2n).real(); + + f64 z0n = (b1 == 0.0f) ? 0.0 : mapPoint(cplx(-f64(b2) / b1, 0.0)).real(); + + f64 denOldDc = 1.0 - a1 - a2; + f64 fRef = (std::abs(denOldDc) > 1e-6) ? 0.0 : 1.0; + + auto H = [](cplx z, cplx p1_, cplx p2_, f64 bb1, f64 bb2) { + return (bb1 * z + bb2) / (z * (z - p1_) * (z - p2_)); + }; + + cplx zOldRef = std::polar(1.0, 2.0 * M_PI * fRef / kOldRate); + cplx zNewRef = std::polar(1.0, 2.0 * M_PI * fRef / kNewRate); + + cplx hOldRef = H(zOldRef, p1, p2, b1, b2) * zOldRef; + cplx shapeNewRef = H(zNewRef, p1n, p2n, 1.0, -z0n) * zNewRef; + + f64 K = (hOldRef / shapeNewRef).real(); + + return {float(K), float(-K * z0n), float(a1n), float(a2n)}; } /** - * For a DSP channel the JASDsp::OutputChannelConfig value targeting the given output channel. - * Returns null if the DSP channel does not output to this output channel. + * Render the audio data contributed by a single DSP channel. Reads & decodes new input samples. */ -static const JASDsp::OutputChannelConfig* GetOutputConfig( - const JASDsp::TChannel& sourceChannel, - OutputChannel channel) { +static void RenderChannel( + JASDsp::TChannel& channel, + ChannelAuxData& aux, + DspSubframe& buf) { - auto busConnect = GetBusConnect(channel); - for (const auto& mOutputChannel : sourceChannel.mOutputChannels) { - auto config = &mOutputChannel; - if (config->mBusConnect == busConnect) { - return config; + if (channel.mResetFlag) { + ResetChannel(channel, aux); + } + + // how many input samples we step per output sample, aka the resampling ratio + auto step = static_cast(channel.mPitch) / 4096.0f; + + // how many input samples to resample to DSP_SUBFRAME_SIZE output samples + int needed = static_cast(aux.resamplePos + DSP_SUBFRAME_SIZE * step) + 2; + + FillDecodeBuf(channel, aux, needed); + + // source ran dry, channel is finished + if(aux.decodeBufCount < needed) { + channel.mIsFinished = true; + } + + f32 pos = aux.resamplePos; + s16 prev = aux.resamplePrev; + s16 next = aux.decodeBufCount > 0 ? aux.decodeBuf[0] : prev; + int srcIdx = 0; + + // linear resampling and f32 conversion + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + buf[i] = (prev + pos * (next - prev)) / 32768.0f; + pos += step; + while (pos >= 1.0f) { + pos -= 1.0f; + prev = next; + srcIdx++; + next = srcIdx < aux.decodeBufCount ? aux.decodeBuf[srcIdx] : prev; } } - return nullptr; + // save resampler state for the next subframe, prevents popping on pitch change + aux.resamplePos = pos; + aux.resamplePrev = prev; + + // IIR FILTER + + // IIR part 1, low-pass: out[n] = (in[n] - in[n-1]) * (coeff/128) + out[n-1] + // removed: seems to be dead code that doesn't work as intended anyway + + // IIR part 2, biquad: out[n] = (b1*in[n-1] + b2*in[n-2] + a1*out[n-1] + a2*out[n-2]) / 32768 + if ((channel.mFilterMode & 0x20) != 0) { + std::span newCoefs{channel.iir_filter_params, 4}; + if (!std::ranges::equal(newCoefs, aux.curBiquadCoefs)) { + aux.remappedBiquadCoefs = RemapBiquad32to48( + newCoefs[0] / 32768.0f, + newCoefs[1] / 32768.0f, + newCoefs[2] / 32768.0f, + newCoefs[3] / 32768.0f + ); + std::ranges::copy(newCoefs, aux.curBiquadCoefs.begin()); + } + + const auto coefs = aux.remappedBiquadCoefs; + for (f32& sample : buf) { + f32 out = std::clamp(( + coefs.b1 * aux.biq_in1 + + coefs.b2 * aux.biq_in2 + + coefs.a1 * aux.biq_out1 + + coefs.a2 * aux.biq_out2 + ), -1.0f, 1.0f); + + // shift history, then store new input and output + aux.biq_in2 = aux.biq_in1; // in[n-2] = in[n-1] + aux.biq_in1 = sample; // in[n-1] = in[n] + aux.biq_out2 = aux.biq_out1; // out[n-2] = out[n-1] + sample = aux.biq_out1 = out; // out[n-1] = out[n] + } + } + + // move any remaining samples in the decode buf to the beginning + int remainingDecodeBuf = aux.decodeBufCount - srcIdx; + if (remainingDecodeBuf > 0) { + memmove(aux.decodeBuf, aux.decodeBuf + srcIdx, remainingDecodeBuf * sizeof(s16)); + } + + aux.decodeBufCount = std::max(0, remainingDecodeBuf); } struct VolumeValue { @@ -577,191 +543,11 @@ struct VolumeValue { f32 Init; }; -/** - * Get the volume that the given DSP channel should render to the given output channel at. - */ -static VolumeValue GetVolumeForOutputChannel( - const JASDsp::TChannel& sourceChannel, - OutputChannel outputChannel) { +using VolumeArray = std::array; - u16 volume; - u16 initVolume; - f32 panValue = 1; - if (sourceChannel.mAutoMixerBeenSet) { - volume = sourceChannel.mAutoMixerVolume; - initVolume = sourceChannel.mAutoMixerInitVolume; - - auto autoMixerPan = static_cast(sourceChannel.mAutoMixerPanDolby >> 8) / 127; - - switch (outputChannel) { - case OutputChannel::LEFT: - panValue = 1 - autoMixerPan; - break; - case OutputChannel::RIGHT: - panValue = autoMixerPan; - break; - default: - CRASH("Unhandled output channel: OutputChannel"); - } - - } else { - auto config = GetOutputConfig(sourceChannel, outputChannel); - if (config == nullptr) { - return {0, 0}; - } - - volume = config->mTargetVolume; - initVolume = config->mCurrentVolume; - } - - // TODO: interpolate to avoid popping. - f32 targetRatio = VolumeFromU16(volume); - targetRatio *= panValue; - - f32 initRatio = VolumeFromU16(initVolume); - initRatio *= panValue; - - return {targetRatio, initRatio}; -} - -/** - * Given decoded & resampled input samples, render a DSP channel to a given output channel. - */ -static void RenderOutputChannel( - const JASDsp::TChannel& sourceChannel, - ChannelAuxData& aux, - OutputChannel outputChannel, - const std::span inputSamples, - OutputSubframe& fullOutputSubframe) { - - auto& outputSubframe = fullOutputSubframe[outputChannel]; - assert(inputSamples.size() <= outputSubframe.size()); - - auto volume = GetVolumeForOutputChannel(sourceChannel, outputChannel); - - f32 targetVolume = volume.Target; - auto& prevVolume = aux.PrevVolume(outputChannel); - if (std::isnan(prevVolume)) { - // Initialize previous volume to new volume on first render. - prevVolume = volume.Init; - } - - if (prevVolume == 0 && targetVolume == 0) { - return; - } - - ApplyVolume(outputSubframe, inputSamples, prevVolume, targetVolume); - prevVolume = targetVolume; -} - -/** - * Fetch, decode, resample, output - */ -static void RenderChannel( - JASDsp::TChannel& channel, - ChannelAuxData& channelAux, - OutputSubframe& subframe) { - - if (channel.mResetFlag) { - ResetChannel(channel, channelAux); - } - - // how many input samples we step per output sample, aka the resampling ratio - f32 step = (f32)PitchToSampleRate(channel.mPitch) / SampleRate; - - // how many input samples to resample to DSP_SUBFRAME_SIZE output samples - int needed = static_cast(channelAux.resamplePos + DSP_SUBFRAME_SIZE * step) + 2; - - FillDecodeBuf(channel, channelAux, needed); - - // source ran dry, channel is finished - if(channelAux.decodeBufCount < needed) { - channel.mIsFinished = true; - } - - DspSubframe audioLoadBuffer = {}; - f32 pos = channelAux.resamplePos; - s16 prev = channelAux.resamplePrev; - s16 next = channelAux.decodeBufCount > 0 ? channelAux.decodeBuf[0] : prev; - int srcIdx = 0; - - // linear resampling and f32 conversion - for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { - audioLoadBuffer[i] = (prev + pos * (next - prev)) / 32768.0f; - pos += step; - while (pos >= 1.0f) { - pos -= 1.0f; - prev = next; - srcIdx++; - next = srcIdx < channelAux.decodeBufCount ? channelAux.decodeBuf[srcIdx] : prev; - } - } - - // save resampler state for the next subframe, prevents popping on pitch change - channelAux.resamplePos = pos; - channelAux.resamplePrev = prev; - - // IIR FILTER - - // IIR part 1, low-pass: out[n] = (in[n] - in[n-1]) * (coeff/128) + out[n-1] - if (s16 coeff = channel.iir_filter_params[4]; coeff != 0) { - for (f32& sample : audioLoadBuffer) { - f32 out = std::clamp( - (sample - channelAux.prev_lp_in) * ((f32)coeff / 128.0f) + channelAux.prev_lp_out, -1.0f, 1.0f - ); - - channelAux.prev_lp_in = sample; // in[n-1] = in[n] - sample = channelAux.prev_lp_out = out; // out[n-1] = out[n] - } - } - - // IIR part 2, biquad: out[n] = (b1*in[n-1] + b2*in[n-2] + a1*out[n-1] + a2*out[n-2]) / 32768 - if ((channel.mFilterMode & 0x20) != 0) { - for (f32& sample : audioLoadBuffer) { - f32 out = std::clamp(( - channel.iir_filter_params[0] * channelAux.biq_in1 + // b1 - channel.iir_filter_params[1] * channelAux.biq_in2 + // b2 - channel.iir_filter_params[2] * channelAux.biq_out1 + // a1 - channel.iir_filter_params[3] * channelAux.biq_out2 // a2 - ) / 32768.0f, -1.0f, 1.0f); - - // shift history, then store new input and output - channelAux.biq_in2 = channelAux.biq_in1; // in[n-2] = in[n-1] - channelAux.biq_in1 = sample; // in[n-1] = in[n] - channelAux.biq_out2 = channelAux.biq_out1; // out[n-2] = out[n-1] - sample = channelAux.biq_out1 = out; // out[n-1] = out[n] - } - } - - // move any remaining samples in the decode buf to the beginning - int remainingDecodeBuf = channelAux.decodeBufCount - srcIdx; - if (remainingDecodeBuf > 0) { - memmove(channelAux.decodeBuf, channelAux.decodeBuf + srcIdx, remainingDecodeBuf * sizeof(s16)); - } - - channelAux.decodeBufCount = std::max(0, remainingDecodeBuf); - - auto hasReadSamples = std::span(audioLoadBuffer).subspan(0, DSP_SUBFRAME_SIZE); - - static_assert(OutputSubframe::NUM_CHANNELS == 2, "Keep RenderChannel in sync!"); - - RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, hasReadSamples, subframe); - RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, hasReadSamples, subframe); -} - -void dusk::audio::DspInit() { - SharedReverb.setwet(1.0f); - SharedReverb.setdry(0.0f); - SharedReverb.setroomsize(0.5f); - SharedReverb.setdamp(0.7f); - SharedReverb.setwidth(1.0f); - SharedReverb.setmode(0.0f); - SharedReverb.mute(); -} - -void dusk::audio::ApplyVolume( +static void ApplyVolume( std::span dst, - const std::span src, + std::span src, const f32 startVolume, const f32 endVolume) { assert(dst.size() >= src.size()); @@ -777,3 +563,391 @@ void dusk::audio::ApplyVolume( } } } + +struct SpeakerPlacement { + OutputChannel channel; + f32 angle; +}; + +struct SpeakerPair { + OutputChannel first; + OutputChannel second; + f32 lo; + f32 span; +}; + +template +constexpr auto BuildSpeakerPairs(const std::array& config) { + std::array pairs = {}; + constexpr f32 kDegToRad = std::numbers::pi_v / 180.0f; + + for (std::size_t i = 0; i < N; i++) { + std::size_t j = (i + 1) % N; + float lo = config[i].angle; + float hi = config[j].angle; + float span = hi - lo; + if (span < 0) span += 360.0f; + + pairs[i] = {config[i].channel, config[j].channel, lo * kDegToRad, span * kDegToRad}; + } + + return pairs; +} + +constexpr auto Placement6ch = std::to_array({ + // the "rear" channels are actually surround left/right, + // changed to match SDL order + {OutputChannel::FRONT_RIGHT, -30}, + {OutputChannel::FRONT_CENTER, 0}, + {OutputChannel::FRONT_LEFT, 30}, + {OutputChannel::REAR_LEFT, 120}, + {OutputChannel::REAR_RIGHT, -120}, +}); + +constexpr auto Placement8ch = std::to_array({ + {OutputChannel::SURROUND_RIGHT, -90}, + {OutputChannel::FRONT_RIGHT, -30}, + {OutputChannel::FRONT_CENTER, 0}, + {OutputChannel::FRONT_LEFT, 30}, + {OutputChannel::SURROUND_LEFT, 90}, + {OutputChannel::REAR_LEFT, 150}, + {OutputChannel::REAR_RIGHT, -150}, +}); + +constexpr auto Pairs6ch = BuildSpeakerPairs(Placement6ch); +constexpr auto Pairs8ch = BuildSpeakerPairs(Placement8ch); + +static void CalcStereoChannelVolumes( + const JASDsp::TChannel& voice, + VolumeArray& volumes) +{ + const auto volume = VolumeFromU16(voice.mAutoMixerVolume); + const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume); + + const auto right = static_cast(voice.mAutoMixerPanDolby >> 8) / 127.0f; + const auto left = 1.0f - right; + + volumes[0] = {left * volume, left * initVolume}; + volumes[1] = {right * volume, right * initVolume}; +} + +static void CalcSurroundChannelVolumes( + const JASDsp::TChannel& voice, + VolumeArray& volumes) +{ + constexpr f32 kTurn = 2.0f * std::numbers::pi_v; + + const auto omniGain = 1.0f / static_cast(OutChannelCount - 1); + const auto pan = static_cast(voice.mAutoMixerPanDolby >> 8) / 63.5f - 1.0f; + const auto dolby = static_cast(voice.mAutoMixerPanDolby & 0xFF) / 63.5f - 1.0f; + const auto focus = std::min(std::sqrt(pan * pan + dolby * dolby), 1.0f); + + f32 angle = std::atan2(-pan, -dolby); + angle = std::fmod(angle, kTurn); + if (angle < 0) angle += kTurn; + + std::array gains = {}; + gains.fill(omniGain * (1.0f - focus)); + + using Pairs = std::span; + const auto pairs = OutChannelCount == 6 ? Pairs{Pairs6ch} : Pairs{Pairs8ch}; + + for (const auto& pair : pairs) { + const auto offset = std::fmod(angle - pair.lo + kTurn, kTurn); + + if (offset <= pair.span) { + const auto first = static_cast(pair.first); + const auto second = static_cast(pair.second); + const auto t = std::clamp(offset / pair.span, 0.0f, 1.0f); + + const auto firstGain = std::cos(t * std::numbers::pi_v / 2.0f); + const auto secondGain = std::sin(t * std::numbers::pi_v / 2.0f); + + gains[first] += focus * firstGain; + gains[second] += focus * secondGain; + + break; + } + } + + const auto volume = VolumeFromU16(voice.mAutoMixerVolume); + const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume); + + for (size_t i = 0; i < OutChannelCount; i++) { + volumes[i].Target = volume * gains[i]; + volumes[i].Init = initVolume * gains[i]; + } +} + +static void ApplyPanning( + const JASDsp::TChannel& voice, + ChannelAuxData& aux, + const DspSubframe& input, + OutputSubframe& output) +{ + VolumeArray volumes = {}; + + if (voice.mAutoMixerBeenSet) { + if (OutChannelCount > 2) { + CalcSurroundChannelVolumes(voice, volumes); + } else { + CalcStereoChannelVolumes(voice, volumes); + } + } else { + for (const auto& outChannel : voice.mOutputChannels) { + std::optional ch; + + switch (outChannel.mBusConnect) { + case 0x0D00: + ch = OutputChannel::FRONT_LEFT; + break; + case 0x0D60: + ch = OutputChannel::FRONT_RIGHT; + break; + default: + break; + } + + if (ch) { + auto& v = volumes[static_cast(*ch)]; + v.Target = VolumeFromU16(outChannel.mTargetVolume); + v.Init = VolumeFromU16(outChannel.mCurrentVolume); + } + } + } + + for (size_t i = 0; i < OutChannelCount; i++) { + const auto ch = static_cast(i); + if (ch == OutputChannel::LFE) { + continue; + } + + const auto& volume = volumes[i]; + + const f32 targetVolume = volume.Target; + auto& prevVolume = aux.PrevVolume(ch); + + if (std::isnan(prevVolume)) { + // Initialize previous volume to new volume on first render. + prevVolume = volume.Init; + } + + if (prevVolume == 0 && targetVolume == 0) { + continue; + } + + ApplyVolume(output[ch], input, prevVolume, targetVolume); + prevVolume = targetVolume; + } +} + +static void DownmixSurroundToStereo( + const OutputSubframe& input, + OutputSubframe& output) +{ + auto& left = output.channels[0]; + auto& right = output.channels[1]; + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + const auto fc = input.channels[2][i] * 0.5f; + left[i] = input.channels[0][i] + fc + input.channels[4][i]; + right[i] = input.channels[1][i] + fc + input.channels[5][i]; + if (OutChannelCount > 6) { + left[i] += input.channels[6][i]; + right[i] += input.channels[7][i]; + left[i] /= 3.5f; + right[i] /= 3.5f; + } else { + left[i] /= 2.5f; + right[i] /= 2.5f; + } + } +} + +static void UpmixStereoToSurroundInplace(OutputSubframe& buf) { + // pseudoinverse of downmix matrix + const auto w = OutChannelCount > 6 ? 1.0f / 12.0f : 1.0f / 8.0f; + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + const auto le = buf.channels[0][i] * (1.0f + w) + buf.channels[1][i] * -w; + const auto re = buf.channels[1][i] * (1.0f + w) + buf.channels[0][i] * -w; + const auto c = buf.channels[0][i] * 0.5f + buf.channels[1][i] * 0.5f; + /* FL */ buf.channels[0][i] = le; + /* FR */ buf.channels[1][i] = re; + /* FC */ buf.channels[2][i] = c; + /* LFE */ buf.channels[3][i] = 0.0f; + /* BL */ buf.channels[4][i] = le; + /* BR */ buf.channels[5][i] = re; + if (OutChannelCount > 6) { + /* SL */ buf.channels[6][i] = le; + /* SR */ buf.channels[7][i] = re; + } + } +} + +static void AccumulateReverbInput( + DspSubframe& dstL, DspSubframe& dstR, + const DspSubframe& srcL, const DspSubframe& srcR, + f32 gain) +{ + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + dstL[j] += srcL[j] * gain; + dstR[j] += srcR[j] * gain; + } +} + +void dusk::audio::DspInit() { + SharedReverb.setwet(1.0f); + SharedReverb.setdry(0.0f); + SharedReverb.setroomsize(0.5f); + SharedReverb.setdamp(0.7f); + SharedReverb.setwidth(1.0f); + SharedReverb.setmode(0.0f); + SharedReverb.mute(); +} + +void dusk::audio::DspRender(OutputSubframe& subframe) { + ZoneScoped; + if (DumpAudio != sDumpWasActive) { + sDumpWasActive = DumpAudio; + if (DumpAudio) { + OpenChannelDumpFiles(); + } else { + CloseChannelDumpFiles(); + } + } + + GenerateEvolvingHarmonic(); + + std::span voices(JASDsp::CH_BUF, DSP_CHANNELS); + + DspSubframe reverbInputL = {}; + DspSubframe reverbInputR = {}; + bool anyReverbInput = false; + + DspSubframe surroundBus = {}; + bool anySurroundInput = false; + + for (int i = 0; i < voices.size(); i++) { + auto& voice = voices[i]; + auto& aux = ChannelAux[i]; + + if (!voice.mIsActive) { + continue; + } + else if (voice.mPauseFlag) { + // Not really sure what the practical difference between pause and + // deactivation is. Either avoids clearing state or allows the DSP to avoid popping? + continue; + } + else if (voice.mForcedStop) { + voice.mIsFinished = true; + continue; + } + + DspSubframe monoBuf = {}; + if (voice.mWaveAramAddress == 0 && !voice.mAramBaseAddress) { + RenderOscChannel(voice, aux, monoBuf); + } else { + ValidateChannel(voice); + RenderChannel(voice, aux, monoBuf); + } + + OutputSubframe buf = {}; + ApplyPanning(voice, aux, monoBuf, buf); + + if (EnableReverb) { + // scale the input to the reverb rather than using wet/dry on the output. + // this way the reverb's internal buffers accumulate energy proportional to mAutoMixerFxMix, + // so any tail always decays at the correct level regardless of mAutoMixerFxMix changes + // prevents transients when the next sound starts playing with a different reverb level + // 600.0f was pulled out of my ass and just sounds good enough for console + f32 inputGain = (voice.mAutoMixerFxMix >> 8) / 600.0f; + if (inputGain > 0) { + anyReverbInput = true; + if (OutChannelCount > 2) { + OutputSubframe downmix; + DownmixSurroundToStereo(buf, downmix); + AccumulateReverbInput(reverbInputL, reverbInputR, downmix.channels[0], downmix.channels[1], inputGain); + } else { + AccumulateReverbInput(reverbInputL, reverbInputR, buf.channels[0], buf.channels[1], inputGain); + } + } + } + + if (EnableHrtf && voice.mAutoMixerBeenSet) { + f32 dolby = (voice.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 = (buf.channels[0][j] + buf.channels[1][j]) * 0.5f; + surroundBus[j] += mono * extract; + buf.channels[0][j] *= frontScale; + buf.channels[1][j] *= frontScale; + } + } + } + + if (DumpAudio && sChannelDumpFiles[i]) { + f32 interleaved[DSP_SUBFRAME_SIZE * 2]; + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + interleaved[j * 2 + 0] = buf.channels[0][j]; + interleaved[j * 2 + 1] = buf.channels[1][j]; + } + fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]); + } + + MixOutputSubframe(subframe, buf); + } + + if (EnableReverb && (anyReverbInput || ReverbHasTail)) { + // Equivalent to -80 dBFS: rms = 1e-4, rms^2 = 1e-8, sumSq = 2 * N * 1e-8 + constexpr f32 REVERB_ENERGY_EPSILON = 2.0f * DSP_SUBFRAME_SIZE * 1e-8f; + f32 wetEnergy = 0.0f; + if (OutChannelCount > 2) { + OutputSubframe reverbOut; + wetEnergy = SharedReverb.processreplace( + reverbInputL.data(), reverbInputR.data(), + reverbOut.channels[0].data(), reverbOut.channels[1].data(), + DSP_SUBFRAME_SIZE, 1, 1.0f + ); + UpmixStereoToSurroundInplace(reverbOut); + MixOutputSubframe(subframe, reverbOut); + } else { + wetEnergy = SharedReverb.processmix( + reverbInputL.data(), reverbInputR.data(), + subframe.channels[0].data(), subframe.channels[1].data(), + DSP_SUBFRAME_SIZE, 1, 1.0f + ); + } + 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 (int i = 0; i < OutChannelCount; i++) { + auto& channel = subframe.channels[i]; + ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume); + } + PrevMasterVolume = MasterVolume; +} diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index 8d7b74a1d6..ef1eb98a6e 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -8,17 +8,25 @@ #include #include -#include namespace dusk::audio { - constexpr int SampleRate = 32000; + constexpr int SampleRate = 48000; enum class OutputChannel : u8 { - LEFT, - RIGHT, + // same as SDL channel layout for 7.1 + FRONT_LEFT, + FRONT_RIGHT, + FRONT_CENTER, + LFE, + REAR_LEFT, + REAR_RIGHT, + SURROUND_LEFT, + SURROUND_RIGHT, OutputChannel_MAX }; + struct BiquadCoeffs { float b1, b2, a1, a2; }; + /** * Data stored by DSP implementation for each DSP channel. */ @@ -59,6 +67,9 @@ namespace dusk::audio { f32 prev_lp_out; // out[n-1] f32 prev_lp_in; // in[n-1] + std::array curBiquadCoefs; + BiquadCoeffs remappedBiquadCoefs; + // biquad state f32 biq_in1; // in[n-1] f32 biq_in2; // in[n-2] @@ -122,16 +133,11 @@ namespace dusk::audio { return channel.mBytesPerBlock; } - /** - * Apply a volume level to audio data. - * Interpolates across the two provided volume levels to avoid clicking. - */ - void ApplyVolume(std::span dst, std::span src, f32 startVolume, f32 endVolume); - extern f32 MasterVolume; extern f32 PrevMasterVolume; extern bool EnableReverb; extern bool DumpAudio; extern bool EnableHrtf; extern f32 HrtfGain; + extern u8 OutChannelCount; } diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index b9bbe88b70..693d0384ed 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -316,6 +316,7 @@ template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; +template class ConfigImpl; template <> void ConfigImpl::loadFromJson( @@ -477,6 +478,11 @@ static void LoadFromPath(const char* path) { return; } + // Configure mod update checks from the existing Dusklight updates cvar + if (!j.contains("backend.checkForModUpdates") && j.contains("backend.checkForUpdates")) { + j["backend.checkForModUpdates"] = j["backend.checkForUpdates"]; + } + UnregisteredConfigVars.clear(); for (const auto& el : j.items()) { @@ -639,4 +645,4 @@ void shutdown() { s_activeChangeNotifications.clear(); } -} // namespace dusk::config \ No newline at end of file +} // namespace dusk::config diff --git a/src/dusk/extras.c b/src/dusk/extras.c index a1fb26e998..ec81d45f9d 100644 --- a/src/dusk/extras.c +++ b/src/dusk/extras.c @@ -68,11 +68,3 @@ int strnicmp(const char* str1, const char* str2, int n) { return 0; } #endif - -void DCZeroRange(void* addr, uint32_t nBytes) { -#if defined(_MSC_VER) || TARGET_ANDROID - memset(addr, 0, nBytes); -#else - bzero(addr, nBytes); -#endif -} diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp deleted file mode 100644 index ca30ec0db5..0000000000 --- a/src/dusk/frame_interpolation.cpp +++ /dev/null @@ -1,463 +0,0 @@ -#include "dusk/frame_interpolation.h" - -#include "f_op/f_op_camera_mng.h" -#include "m_Do/m_Do_graphic.h" -#include "mtx.h" - -#include - -namespace { - -struct Recording { - absl::flat_hash_map matrix_values; -}; - -bool s_initialized = false; - -bool g_enabled = false; -bool g_recording = false; -bool g_interpolating = false; -bool g_sync_presentation = false; - -float g_step = 0.0f; -bool g_is_sim_frame = false; -bool g_ui_tick_pending = false; -uint64_t g_sim_tick_seq = 0; - -Recording g_current_recording; -Recording g_previous_recording; - -absl::flat_hash_map g_replacements; - -struct CameraSnapshot { - cXyz eye{}; - cXyz center{}; - cXyz up{}; - s16 bank{}; - f32 fovy{}; - f32 aspect{}; - f32 near_{}; - f32 far_{}; - bool wideZoom{}; - bool valid{}; -}; - -CameraSnapshot s_cam_prev{}; -CameraSnapshot s_cam_curr{}; - -view_class s_presentation_view_backup{}; -int s_presentation_depth = 0; - -struct InterpolationCallBackWork { - dusk::frame_interp::InterpolationCallBack pCallBack; - void* pUserWork; -}; - -std::vector s_interpolationCallBackWork; - -void copy_view_to_snap(CameraSnapshot* dst, const view_class& v) { - dst->eye = v.lookat.eye; - dst->center = v.lookat.center; - dst->up = v.lookat.up; - dst->bank = v.bank; - dst->fovy = v.fovy; - dst->aspect = v.aspect; - dst->near_ = v.near_; - dst->far_ = v.far_; - dst->valid = true; -} - -inline void lerp_matrix(Mtx out, const Mtx lhs, const Mtx rhs, float step) { - for (size_t row = 0; row < 3; ++row) { - for (size_t col = 0; col < 4; ++col) { - const float l = lhs[row][col]; - out[row][col] = l + (rhs[row][col] - l) * step; - } - } -} - -inline void lerp_xyz(cXyz* out, const cXyz& lhs, const cXyz& rhs, float step) { - out->x = lhs.x + (rhs.x - lhs.x) * step; - out->y = lhs.y + (rhs.y - lhs.y) * step; - out->z = lhs.z + (rhs.z - lhs.z) * step; -} - -static s16 lerp_bank(s16 a, s16 b, f32 t) { - const f32 ra = S2RAD(a); - const f32 d = remainderf(S2RAD(b) - ra, 2.0f * static_cast(M_PI)); - return cAngle::Radian_to_SAngle(ra + d * t); -} - -inline bool matrix_differs(const Mtx lhs, const Mtx rhs, float epsilon = 0.0001f) { - for (size_t row = 0; row < 3; ++row) { - for (size_t col = 0; col < 4; ++col) { - if (std::abs(lhs[row][col] - rhs[row][col]) > epsilon) { - return true; - } - } - } - return false; -} - -const Mtx* resolve_replacement(const Mtx* source, Mtx* scratch) { - if (!g_interpolating || source == nullptr || dusk::frame_interp::presentation_sync_active()) { - return source; - } - - auto it = g_replacements.find(reinterpret_cast(source)); - if (it == g_replacements.end()) { - return source; - } - - MTXCopy(it->second, *scratch); - return scratch; -} - -bool has_recording_data(const Recording& recording) { - return !recording.matrix_values.empty(); -} - -void clear_replacements() { - g_replacements.clear(); -} - -} // namespace - -namespace dusk::frame_interp { -void ensure_initialized() { - s_initialized = true; -} - -void begin_sim_tick() { - ensure_initialized(); - if (!g_enabled) { - return; - } - - s_interpolationCallBackWork.clear(); - s_cam_prev = std::move(s_cam_curr); - ++g_sim_tick_seq; -} - -uint64_t sim_tick_seq() { - return g_sim_tick_seq; -} - -void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step) { - g_enabled = mode != FrameInterpMode::Off; - g_is_sim_frame = is_sim_frame; - g_step = std::clamp(step, 0.0f, 1.0f); - if (!g_enabled) { - g_interpolating = false; - clear_replacements(); - } -} - -bool is_enabled() { - return g_enabled; -} - -bool is_sim_frame() { - return g_is_sim_frame; -} - -void begin_record() { - ensure_initialized(); - - if (!g_enabled) { - g_interpolating = false; - g_sync_presentation = false; - g_previous_recording = {}; - g_current_recording = {}; - clear_replacements(); - s_cam_prev.valid = false; - s_cam_curr.valid = false; - return; - } - - g_sync_presentation = false; - g_previous_recording = std::move(g_current_recording); - g_current_recording = {}; - g_recording = true; - g_interpolating = false; - clear_replacements(); - - ::camera_process_class* cam = dComIfGp_getCamera(0); - if (cam == nullptr) { - s_cam_prev.valid = false; - s_cam_curr.valid = false; - return; - } -} - -void end_record() { - g_recording = false; -} - -void interpolate() { - ensure_initialized(); - clear_replacements(); - g_interpolating = g_enabled && !g_recording && !g_sync_presentation && has_recording_data(g_current_recording); - if (!g_interpolating) { - return; - } - for (auto const& old : g_previous_recording.matrix_values) { - if (auto it = g_current_recording.matrix_values.find(old.first); - it != g_current_recording.matrix_values.end()) - { - lerp_matrix(g_replacements[old.first], old.second, it->second, g_step); - } - } -} - -void request_presentation_sync() { - ensure_initialized(); - if (!g_enabled) { - return; - } - g_sync_presentation = true; -} - -bool presentation_sync_active() { - if (!s_initialized || !g_enabled) { - return false; - } - return g_sync_presentation; -} - -float get_interpolation_step() { - ensure_initialized(); - return presentation_sync_active() ? 1.0f : g_step; -} - -void set_ui_tick_pending(bool value) { - if (g_ui_tick_pending == value) { return; } - g_ui_tick_pending = value; -} - -bool get_ui_tick_pending() { - ensure_initialized(); - return g_enabled ? g_ui_tick_pending : true; -} - -void record_final_mtx(Mtx m, const void* key) { - if (!s_initialized || !g_recording || m == nullptr) { - return; - } - - auto& it = g_current_recording.matrix_values[reinterpret_cast(key)]; - MTXCopy(m, it); -} - -void record_final_mtx(Mtx m) { - record_final_mtx(m, m); -} - -bool lookup_replacement(const void* key, Mtx out) { - if (presentation_sync_active() || !g_interpolating || key == nullptr) { - return false; - } - - auto it = g_replacements.find(reinterpret_cast(key)); - if (it == g_replacements.end()) { - return false; - } - - MTXCopy(it->second, out); - return true; -} - -bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out) { - if (presentation_sync_active() || !g_interpolating || lhs == nullptr || rhs == nullptr) { - return false; - } - - Mtx lhs_scratch; - Mtx rhs_scratch; - const Mtx* resolved_lhs = resolve_replacement(reinterpret_cast(lhs), &lhs_scratch); - const Mtx* resolved_rhs = resolve_replacement(reinterpret_cast(rhs), &rhs_scratch); - if (resolved_lhs == reinterpret_cast(lhs) && resolved_rhs == reinterpret_cast(rhs)) { - return false; - } - - MTXConcat(*resolved_lhs, *resolved_rhs, out); - return true; -} - -void record_camera(::camera_process_class* cam, int camera_id) { - if (!g_enabled || camera_id != 0 || cam == nullptr) { - return; - } - copy_view_to_snap(&s_cam_curr, cam->view); -#if WIDESCREEN_SUPPORT - s_cam_curr.wideZoom = mDoGph_gInf_c::isWideZoom(); -#endif -} - -void interp_view(::view_class* view) { - if (!g_enabled) - return; - - if (!s_cam_prev.valid || !s_cam_curr.valid) - 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; - 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(); - } - - view->lookat.eye = eye; - view->lookat.center = center; - view->lookat.up = up; - 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 - 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 -} - -static void run_interpolation_callbacks() { - for (size_t i = 0; i < s_interpolationCallBackWork.size(); i++) { - auto const& work = s_interpolationCallBackWork[i]; - work.pCallBack(g_is_sim_frame, work.pUserWork); - } -} - -void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork) { - if (!is_enabled() || s_presentation_depth > 0 || !g_is_sim_frame) - return; - - s_interpolationCallBackWork.emplace_back(pCallBack, pUserWork); -} - -void begin_presentation_camera() { - ensure_initialized(); - if (!g_enabled) { - return; - } - if (s_presentation_depth > 0) { - s_presentation_depth++; - return; - } - if (!s_cam_prev.valid || !s_cam_curr.valid) { - return; - } - - view_class* const view = dComIfGd_getView(); - if (view == nullptr) { - return; - } - - std::memcpy(&s_presentation_view_backup, view, sizeof(view_class)); - interp_view(view); - - // FRAME INTERP TODO: Largely copied from d_camera's camera_draw function from this point, got any better ideas? - C_MTXPerspective(view->projMtx, view->fovy, view->aspect, view->near_, view->far_); - mDoMtx_lookAt(view->viewMtx, &view->lookat.eye, &view->lookat.center, &view->lookat.up, view->bank); -#if WIDESCREEN_SUPPORT - mDoGph_gInf_c::setWideZoomProjection(view->projMtx); -#endif - j3dSys.setViewMtx(view->viewMtx); - cMtx_inverse(view->viewMtx, view->invViewMtx); - - bool camera_attention_status = dComIfGp_getCameraAttentionStatus(0) & 0x80; - Z2GetAudience()->setAudioCamera(view->viewMtx, view->lookat.eye, view->lookat.center, view->fovy, view->aspect, camera_attention_status, 0, false); - - dBgS_GndChk gndchk; - gndchk.OnWaterGrp(); - gndchk.SetPos(&view->lookat.eye); - f32 cross = dComIfG_Bgsp().GroundCross(&gndchk); - if (cross != -G_CM3D_F_INF) { - if (dComIfG_Bgsp().ChkGrpInf(gndchk, 0x100)) { - mDoAud_getCameraMapInfo(6); - } else { - mDoAud_getCameraMapInfo(dComIfG_Bgsp().GetMtrlSndId(gndchk)); - } - mDoAud_setCameraGroupInfo(dComIfG_Bgsp().GetGrpSoundId(gndchk)); - Vec spDC; - spDC.x = view->lookat.eye.x; - spDC.y = cross; - spDC.z = view->lookat.eye.z; - Z2AudioMgr::getInterface()->setCameraPolygonPos(&spDC); - } else { - Z2AudioMgr::getInterface()->setCameraPolygonPos(nullptr); - } - - MTXCopy(view->viewMtx, view->viewMtxNoTrans); - view->viewMtxNoTrans[0][3] = 0.0f; - view->viewMtxNoTrans[1][3] = 0.0f; - view->viewMtxNoTrans[2][3] = 0.0f; - cMtx_concatProjView(view->projMtx, view->viewMtx, view->projViewMtx); - - f32 far_; - f32 var_f30; - if (dComIfGp_getCameraAttentionStatus(0) & 8) { - far_ = view->far_; - } else { -#if DEBUG - if (g_envHIO.mOther.mAdjustCullFar != 0) { - var_f30 = g_envHIO.mOther.mCullFarValue; - } else -#endif - { - var_f30 = dStage_stagInfo_GetCullPoint(dComIfGp_getStageStagInfo()); - } - far_ = var_f30; - } - - mDoLib_clipper::setup(view->fovy, view->aspect, view->near_, far_); - - // FRAME INTERP NOTE: Removed the call to offWideZoom that was here, it causes problems with presentation during cutscenes. - - s_presentation_depth = 1; - - run_interpolation_callbacks(); -} - -void end_presentation_camera() { - if (s_presentation_depth == 0) { - return; - } - s_presentation_depth--; - if (s_presentation_depth > 0) { - return; - } - - view_class* const view = dComIfGd_getView(); - if (view != nullptr) { - std::memcpy(view, &s_presentation_view_backup, sizeof(view_class)); - } -} -} // namespace dusk::frame_interp diff --git a/src/dusk/frame_interpolation.h b/src/dusk/frame_interpolation.h deleted file mode 100644 index 5f2f3d134b..0000000000 --- a/src/dusk/frame_interpolation.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "settings.h" - -class camera_process_class; -class view_class; - -#ifdef __cplusplus -namespace dusk { -namespace frame_interp { - -void ensure_initialized(); - -void begin_record(); -void end_record(); -void begin_sim_tick(); -uint64_t sim_tick_seq(); -void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step); -void interpolate(); -float get_interpolation_step(); - -void request_presentation_sync(); -bool presentation_sync_active(); - -bool is_enabled(); - -// TODO: These should be phased out as UI is progressively updated to use game_clock -void set_ui_tick_pending(bool value); -bool get_ui_tick_pending(); - -bool is_sim_frame(); - -void record_camera(::camera_process_class* cam, int camera_id); -void interp_view(::view_class* view); -void record_final_mtx(Mtx m, const void *key); -void record_final_mtx(Mtx m); - -bool lookup_replacement(const void* key, Mtx out); -bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out); - -typedef void (*InterpolationCallBack)(bool isSimFrame, void* pUserWork); -// call on a sim tick, will get called during presentation -void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork); - -void begin_presentation_camera(); -void end_presentation_camera(); - -} // namespace frame_interp -} // namespace dusk -#endif diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index 178ec7803a..52472f42a9 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -1,7 +1,5 @@ #include "dusk/game_clock.h" -#include "dusk/frame_interpolation.h" - #include #include @@ -13,23 +11,36 @@ namespace dusk::game_clock { using native_clock = aurora::time::native_clock; using game_clock = aurora::time::game_clock; -FrameTiming g_frameTiming; +FrameTiming g_frameTiming{.dt = kUiInitialDt}; namespace { bool s_initialized = false; -bool s_fixedStepActive = false; bool s_simTickActive = false; native_clock::time_point s_previousNativeSample{}; game_clock::time_point s_latestGameSample{}; game_clock::time_point s_currentSnapshotTime{}; game_clock::time_point s_pendingSimTime{}; - std::unordered_map s_intervalLastSample; +uint64_t s_presentationEpoch = 1; +bool s_timingModeInitialized = false; +bool s_previousSeparatePresentation = false; +bool s_previousInterpolating = false; +bool s_previousTimeStopped = false; +float s_presentationDtSeconds = kUiInitialDt; constexpr game_clock::duration kSimPeriodDuration = std::chrono::duration_cast(std::chrono::duration(kSimPeriod)); constexpr native_clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250); constexpr int kMaxSimTicksPerFrame = static_cast(aurora::time::kMaximumTimeScale) * 4; + +float ui_dt() { + if (s_simTickActive) { + return kSimPeriod; + } + + const float maximumDt = kUiMaximumDt * aurora::time::scale(); + return std::clamp(s_presentationDtSeconds, 0.0f, maximumDt); +} } // namespace void initialize() { @@ -49,6 +60,7 @@ void reset() { s_currentSnapshotTime = s_latestGameSample - kSimPeriodDuration; s_pendingSimTime = s_currentSnapshotTime; s_simTickActive = false; + ++s_presentationEpoch; } void set_sim_rate(float hz) { @@ -65,11 +77,16 @@ const FrameTiming& advance() { const auto nativeNow = native_clock::now(); const auto gameNow = game_clock::now(); const auto nativeFrameGap = nativeNow - s_previousNativeSample; + const auto gameFrameGap = gameNow - s_latestGameSample; s_previousNativeSample = nativeNow; s_latestGameSample = gameNow; auto& out = g_frameTiming; - out = {.dt = std::chrono::duration().count()}; + out = { + .dt = std::chrono::duration(gameFrameGap).count(), + .presentationEpoch = s_presentationEpoch, + }; + s_presentationDtSeconds = out.dt; const float timeScale = aurora::time::scale(); const bool interpolating = @@ -77,7 +94,21 @@ const FrameTiming& advance() { const bool separatePresentation = interpolating || timeScale != 1.0f; out.interpolating = interpolating; out.separatePresentation = separatePresentation; - s_fixedStepActive = separatePresentation; + + const bool timeStopped = timeScale == 0.0f; + const bool timingModeChanged = + s_timingModeInitialized && + (separatePresentation != s_previousSeparatePresentation || + interpolating != s_previousInterpolating || timeStopped != s_previousTimeStopped); + const bool abnormalGap = nativeFrameGap > kAbnormalGapResetThreshold; + if (timingModeChanged || abnormalGap) { + ++s_presentationEpoch; + out.presentationEpoch = s_presentationEpoch; + } + s_timingModeInitialized = true; + s_previousSeparatePresentation = separatePresentation; + s_previousInterpolating = interpolating; + s_previousTimeStopped = timeStopped; if (!separatePresentation) { s_currentSnapshotTime = gameNow; @@ -86,7 +117,7 @@ const FrameTiming& advance() { } const auto simulationTarget = interpolating ? gameNow - kSimPeriodDuration : gameNow; - if (timeScale == 0.f || nativeFrameGap > kAbnormalGapResetThreshold) { + if (timeStopped || abnormalGap) { s_currentSnapshotTime = simulationTarget; out.numSimTicks = 0; return out; @@ -109,8 +140,8 @@ const FrameTiming& advance() { } void begin_sim_tick() { - s_pendingSimTime = - s_fixedStepActive ? s_currentSnapshotTime + kSimPeriodDuration : s_latestGameSample; + s_pendingSimTime = g_frameTiming.separatePresentation ? s_currentSnapshotTime + kSimPeriodDuration : + s_latestGameSample; s_simTickActive = true; } @@ -123,6 +154,14 @@ void commit_sim_tick() { } } +bool is_sim_frame() { + return !g_frameTiming.separatePresentation || s_simTickActive; +} + +bool is_presentation_frame() { + return !g_frameTiming.separatePresentation || !s_simTickActive; +} + float sample_interpolation_step() { const float step = std::chrono::duration(game_clock::now() - s_currentSnapshotTime).count() / @@ -130,6 +169,10 @@ float sample_interpolation_step() { return std::clamp(step, 0.0f, 1.0f); } +float original_frames() { + return ui_dt() / kSimPeriod; +} + float consume_interval(const void* consumer) { const auto key = reinterpret_cast(consumer); const auto now = s_simTickActive ? s_pendingSimTime : game_clock::now(); diff --git a/src/dusk/game_clock.h b/src/dusk/game_clock.h index 5e71ea4c0f..df04d7d420 100644 --- a/src/dusk/game_clock.h +++ b/src/dusk/game_clock.h @@ -7,6 +7,8 @@ constexpr float kSimPeriod = 1.0f / 30.0f; constexpr float kUiMaximumDt = 0.05f; constexpr float kUiInitialDt = 1.0f / 60.0f; +float original_frames(); + struct FrameTiming { // Amount of time elapsed in seconds since the last advance float dt; @@ -16,6 +18,8 @@ struct FrameTiming { bool separatePresentation; // Number of simulation ticks to run int numSimTicks; + // Changes whenever presentation history must be discarded and re-anchored. + uint64_t presentationEpoch; }; extern FrameTiming g_frameTiming; @@ -26,10 +30,13 @@ void begin_sim_tick(); void commit_sim_tick(); float sample_interpolation_step(); +bool is_sim_frame(); +bool is_presentation_frame(); + float consume_interval(const void* consumer); // Sets the effective simulation rate through the game clock time scale. void set_sim_rate(float hz); float get_sim_rate(); -} // namespace dusk::game_clock +} // namespace dusk::game_clock diff --git a/src/dusk/hash.hpp b/src/dusk/hash.hpp new file mode 100644 index 0000000000..da88aa5547 --- /dev/null +++ b/src/dusk/hash.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include +#include +#include + +namespace dusk::hash { + +class Sha256 { +public: + void update(std::span bytes) { + mHasher.process(bytes.begin(), bytes.end()); + } + + std::string finish() { + mHasher.finish(); + return picosha2::get_hash_hex_string(mHasher); + } + +private: + picosha2::hash256_one_by_one mHasher; +}; + +} // namespace dusk::hash diff --git a/src/dusk/imgui/ImGuiAudio.cpp b/src/dusk/imgui/ImGuiAudio.cpp index ce9290efa5..8cdc462c2a 100644 --- a/src/dusk/imgui/ImGuiAudio.cpp +++ b/src/dusk/imgui/ImGuiAudio.cpp @@ -1,14 +1,15 @@ +#include #include "ImGuiConsole.hpp" #include "ImGuiMenuTools.hpp" -#include -#include "JSystem/JAudio2/JAISeq.h" #include "JSystem/JAudio2/JAISeMgr.h" +#include "JSystem/JAudio2/JAISeq.h" #include "JSystem/JAudio2/JAISeqMgr.h" #include "JSystem/JAudio2/JAIStreamMgr.h" #include "JSystem/JAudio2/JASCriticalSection.h" #include "JSystem/JAudio2/JASDSPChannel.h" #include "JSystem/JAudio2/JASDSPInterface.h" #include "JSystem/JAudio2/JASTrack.h" +#include "JSystem/JAudio2/JAUSectionHeap.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" @@ -281,6 +282,37 @@ static void ShowAllJAISeqs() { } } +static void ShowSeqDataBlock(JAUSeqDataBlock const& block) { + char buf[64]; + SAFE_SPRINTF(buf, "%p", &block); + if (ImGui::BeginChild(buf, ImVec2(), ImGuiChildFlags_Border | ImGuiChildFlags_AutoResizeY)) { + ImGui::Text("Sound: %08X", block.field_0x10.id_.composite_.host()); + ImGui::Text("Region: %p %08X", block.region.addr, block.region.size); + } + + ImGui::EndChild(); +} + +static void ShowSeqDataBlocks() { + auto const& heap = *JAUSectionHeap::getInstance(); + auto const& blocks = heap.sectionHeapData_.seqDataBlocks; + + ImGui::Text("Loaded:"); + for (auto link = blocks.mLoadedBlocks.getFirst(); link != nullptr; link = link->getNext()) { + ShowSeqDataBlock(*link->getObject()); + } + + ImGui::Text("Loading:"); + for (auto link = blocks.field_0xc.getFirst(); link != nullptr; link = link->getNext()) { + ShowSeqDataBlock(*link->getObject()); + } + + ImGui::Text("Free:"); + for (auto link = blocks.mFreeBlocks.getFirst(); link != nullptr; link = link->getNext()) { + ShowSeqDataBlock(*link->getObject()); + } +} + void dusk::ImGuiMenuTools::ShowAudioDebug() { if (!getSettings().backend.enableAdvancedSettings || !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug)) @@ -322,6 +354,11 @@ void dusk::ImGuiMenuTools::ShowAudioDebug() { ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("SeqDataBlocks")) { + ShowSeqDataBlocks(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } } diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 97a13efc3d..badf7297c2 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -6,7 +6,6 @@ #include "dusk/action_bindings.h" #include "dusk/config.hpp" #include "dusk/data.hpp" -#include "dusk/frame_interpolation.h" #include "dusk/game_mode.hpp" #include "dusk/livesplit.h" #include "dusk/main.h" diff --git a/src/dusk/interp/camera.cpp b/src/dusk/interp/camera.cpp new file mode 100644 index 0000000000..19ab197214 --- /dev/null +++ b/src/dusk/interp/camera.cpp @@ -0,0 +1,218 @@ +#include "dusk/interp/camera.h" + +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/lerp.h" + +#include "d/d_com_inf_game.h" +#include "f_op/f_op_camera_mng.h" +#include "m_Do/m_Do_graphic.h" + +#include +#include + +namespace { + +struct CameraSnapshot { + cXyz eye{}; + cXyz center{}; + cXyz up{}; + s16 bank{}; + f32 fovy{}; + f32 aspect{}; + f32 near_{}; + f32 far_{}; + bool wideZoom{}; + bool valid{}; +}; + +CameraSnapshot s_camPrev{}; +CameraSnapshot s_camCurr{}; + +view_class s_presentationViewBackup{}; + +void copy_view_to_snap(CameraSnapshot* dst, const view_class& v) { + dst->eye = v.lookat.eye; + dst->center = v.lookat.center; + dst->up = v.lookat.up; + dst->bank = v.bank; + dst->fovy = v.fovy; + dst->aspect = v.aspect; + dst->near_ = v.near_; + dst->far_ = v.far_; + dst->valid = true; +} + +void apply_presented_view(view_class* view) { + // FRAME INTERP TODO: Largely copied from d_camera's camera_draw function from this point, got any + // better ideas? + C_MTXPerspective(view->projMtx, view->fovy, view->aspect, view->near_, view->far_); + mDoMtx_lookAt(view->viewMtx, &view->lookat.eye, &view->lookat.center, &view->lookat.up, + view->bank); +#if WIDESCREEN_SUPPORT + mDoGph_gInf_c::setWideZoomProjection(view->projMtx); +#endif + j3dSys.setViewMtx(view->viewMtx); + cMtx_inverse(view->viewMtx, view->invViewMtx); + + bool camera_attention_status = dComIfGp_getCameraAttentionStatus(0) & 0x80; + Z2GetAudience()->setAudioCamera(view->viewMtx, view->lookat.eye, view->lookat.center, view->fovy, + view->aspect, camera_attention_status, 0, false); + + dBgS_GndChk gndchk; + gndchk.OnWaterGrp(); + gndchk.SetPos(&view->lookat.eye); + f32 cross = dComIfG_Bgsp().GroundCross(&gndchk); + if (cross != -G_CM3D_F_INF) { + if (dComIfG_Bgsp().ChkGrpInf(gndchk, 0x100)) { + mDoAud_getCameraMapInfo(6); + } else { + mDoAud_getCameraMapInfo(dComIfG_Bgsp().GetMtrlSndId(gndchk)); + } + mDoAud_setCameraGroupInfo(dComIfG_Bgsp().GetGrpSoundId(gndchk)); + Vec spDC; + spDC.x = view->lookat.eye.x; + spDC.y = cross; + spDC.z = view->lookat.eye.z; + Z2AudioMgr::getInterface()->setCameraPolygonPos(&spDC); + } else { + Z2AudioMgr::getInterface()->setCameraPolygonPos(nullptr); + } + + MTXCopy(view->viewMtx, view->viewMtxNoTrans); + view->viewMtxNoTrans[0][3] = 0.0f; + view->viewMtxNoTrans[1][3] = 0.0f; + view->viewMtxNoTrans[2][3] = 0.0f; + cMtx_concatProjView(view->projMtx, view->viewMtx, view->projViewMtx); + + f32 far_; + f32 var_f30; + if (dComIfGp_getCameraAttentionStatus(0) & 8) { + far_ = view->far_; + } else { +#if DEBUG + if (g_envHIO.mOther.mAdjustCullFar != 0) { + var_f30 = g_envHIO.mOther.mCullFarValue; + } else +#endif + { + var_f30 = dStage_stagInfo_GetCullPoint(dComIfGp_getStageStagInfo()); + } + far_ = var_f30; + } + + mDoLib_clipper::setup(view->fovy, view->aspect, view->near_, far_); + + // FRAME INTERP NOTE: Removed the call to offWideZoom that was here, it causes problems with + // presentation during cutscenes. +} + +} // namespace + +namespace dusk::interp { + +void record_camera(::camera_process_class* cam, int camera_id) { + if (!is_enabled() || camera_id != 0 || cam == nullptr) { + return; + } + copy_view_to_snap(&s_camCurr, cam->view); +#if WIDESCREEN_SUPPORT + s_camCurr.wideZoom = mDoGph_gInf_c::isWideZoom(); +#endif +} + +void interp_view(::view_class* view) { + if (!is_enabled()) + return; + + if (!s_camPrev.valid || !s_camCurr.valid) + return; + + const f32 step = get_interpolation_step(); + const bool is_cam_curr_authoritative = game_clock::is_sim_frame() && step <= 0.0f; + + cXyz eye; + cXyz center; + cXyz up; + if (is_cam_curr_authoritative) { + eye = s_camCurr.eye; + center = s_camCurr.center; + up = s_camCurr.up; + } else { + lerp(eye, s_camPrev.eye, s_camCurr.eye, step); + lerp(center, s_camPrev.center, s_camCurr.center, step); + lerp(up, s_camPrev.up, s_camCurr.up, step); + } + if (!up.normalizeRS()) { + up = s_camCurr.up; + up.normalizeRS(); + } + + view->lookat.eye = eye; + view->lookat.center = center; + view->lookat.up = up; + if (is_cam_curr_authoritative) { + view->bank = s_camCurr.bank; + view->fovy = s_camCurr.fovy; + view->aspect = s_camCurr.aspect; + view->near_ = s_camCurr.near_; + view->far_ = s_camCurr.far_; + } else { + view->bank = lerp(s_camPrev.bank, s_camCurr.bank, step); + view->fovy = s_camPrev.fovy + (s_camCurr.fovy - s_camPrev.fovy) * step; + view->aspect = s_camPrev.aspect + (s_camCurr.aspect - s_camPrev.aspect) * step; + view->near_ = s_camPrev.near_ + (s_camCurr.near_ - s_camPrev.near_) * step; + view->far_ = s_camPrev.far_ + (s_camCurr.far_ - s_camPrev.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 + 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_camCurr.wideZoom : s_camPrev.wideZoom) + { + mDoGph_gInf_c::onWideZoom(); + } +#endif +} + +void camera_on_sim_tick() { + s_camPrev = std::move(s_camCurr); +} + +void camera_invalidate_snapshots() { + s_camPrev.valid = false; + s_camCurr.valid = false; +} + +void camera_on_begin_record() { + if (dComIfGp_getCamera(0) == nullptr) { + camera_invalidate_snapshots(); + } +} + +bool camera_apply_presentation() { + if (!s_camPrev.valid || !s_camCurr.valid) { + return false; + } + + view_class* const view = dComIfGd_getView(); + if (view == nullptr) { + return false; + } + + std::memcpy(&s_presentationViewBackup, view, sizeof(view_class)); + interp_view(view); + apply_presented_view(view); + return true; +} + +void camera_restore_presentation() { + view_class* const view = dComIfGd_getView(); + if (view != nullptr) { + std::memcpy(view, &s_presentationViewBackup, sizeof(view_class)); + } +} + +} // namespace dusk::interp diff --git a/src/dusk/interp/camera.h b/src/dusk/interp/camera.h new file mode 100644 index 0000000000..e4a1824979 --- /dev/null +++ b/src/dusk/interp/camera.h @@ -0,0 +1,13 @@ +#pragma once + +class camera_process_class; +class view_class; + +#ifdef __cplusplus +namespace dusk::interp { + +void record_camera(::camera_process_class* cam, int camera_id); +void interp_view(::view_class* view); + +} // namespace dusk::interp +#endif diff --git a/src/dusk/interp/frame_interpolation.cpp b/src/dusk/interp/frame_interpolation.cpp new file mode 100644 index 0000000000..598ddfb272 --- /dev/null +++ b/src/dusk/interp/frame_interpolation.cpp @@ -0,0 +1,312 @@ +#include "dusk/interp/frame_interpolation.h" + +#include "dusk/game_clock.h" +#include "dusk/interp/lerp.h" +#include "dusk/interp/material.h" +#include "dusk/interp/particle.h" +#include "dusk/interp/samples.h" +#include "dusk/interp/vertex.h" + +#include "mtx.h" + +#include +#include +#include + +namespace dusk::interp { +void camera_on_sim_tick(); +void camera_on_begin_record(); +bool camera_apply_presentation(); +void camera_restore_presentation(); +void camera_invalidate_snapshots(); +void clear_weather_samples(); +} // namespace dusk::interp + +namespace { + +struct Recording { + absl::flat_hash_map matrix_values; +}; + +bool s_recording = false; +bool s_replacementsActive = false; +bool s_syncPresentation = false; + +float s_step = 0.0f; +uint64_t s_simTickSeq = 0; +uint64_t s_observedPresentationEpoch = 0; + +Recording s_currentRecording; +Recording s_previousRecording; + +absl::flat_hash_map g_replacements; + +int s_presentationDepth = 0; +bool s_cameraPresentationActive = false; + +const Mtx* resolve_replacement(const Mtx* source, Mtx* scratch) { + if (!s_replacementsActive || source == nullptr || dusk::interp::presentation_sync_active()) { + return source; + } + + auto it = g_replacements.find(reinterpret_cast(source)); + if (it == g_replacements.end()) { + return source; + } + + MTXCopy(it->second, *scratch); + return scratch; +} + +bool has_recording_data(const Recording& recording) { + return !recording.matrix_values.empty(); +} + +void clear_replacements() { + g_replacements.clear(); +} + +void interpolate_replacements() { + clear_replacements(); + s_replacementsActive = dusk::interp::is_enabled() && !s_recording && !s_syncPresentation && + has_recording_data(s_currentRecording); + if (!s_replacementsActive) { + return; + } + for (auto const& old : s_previousRecording.matrix_values) { + if (auto it = s_currentRecording.matrix_values.find(old.first); + it != s_currentRecording.matrix_values.end()) + { + dusk::interp::lerp(g_replacements[old.first], old.second, it->second, s_step); + } + } +} + +struct InterpolationCallBackWork { + dusk::interp::InterpolationCallBack pCallBack; + void* pUserWork; + std::shared_ptr owner; +}; + +std::vector s_interpolationCallBackWork; + +void clear_callbacks() { + s_interpolationCallBackWork.clear(); +} + +void callbacks_run() { + for (const auto& work : s_interpolationCallBackWork) { + if (work.pCallBack != nullptr) { + work.pCallBack(work.pUserWork); + } + } +} + +void clear_interpolation_history() { + s_recording = false; + s_replacementsActive = false; + s_syncPresentation = false; + s_previousRecording = {}; + s_currentRecording = {}; + clear_replacements(); + clear_callbacks(); + dusk::interp::camera_invalidate_snapshots(); + dusk::interp::clear_owned_samples(); + dusk::interp::clear_weather_samples(); + dusk::interp::material::clear(); + dusk::interp::particle::clear(); + dusk::interp::vertex::clear(); + s_presentationDepth = 0; + s_cameraPresentationActive = false; +} + +} // namespace + +namespace dusk::interp { + +void begin_sim_tick() { + if (!is_enabled()) { + return; + } + + clear_callbacks(); + camera_on_sim_tick(); + ++s_simTickSeq; + material::prune(); + particle::prune(); + vertex::prune(); +} + +uint64_t sim_tick_seq() { + return s_simTickSeq; +} + +void begin_frame(float step) { + const game_clock::FrameTiming& timing = game_clock::g_frameTiming; + if (s_observedPresentationEpoch != timing.presentationEpoch) { + s_observedPresentationEpoch = timing.presentationEpoch; + clear_interpolation_history(); + } + + s_step = std::clamp(step, 0.0f, 1.0f); + if (!is_enabled()) { + clear_interpolation_history(); + } +} + +bool is_enabled() { + return game_clock::g_frameTiming.interpolating; +} + +bool should_capture() { + return is_enabled() && game_clock::is_sim_frame(); +} + +void begin_record() { + if (!is_enabled()) { + clear_interpolation_history(); + return; + } + + s_syncPresentation = false; + s_previousRecording = std::move(s_currentRecording); + s_currentRecording = {}; + s_recording = true; + s_replacementsActive = false; + clear_replacements(); + camera_on_begin_record(); +} + +void end_record() { + s_recording = false; +} + +void request_presentation_sync() { + if (!is_enabled()) { + return; + } + s_syncPresentation = true; +} + +bool presentation_sync_active() { + if (!is_enabled()) { + return false; + } + return s_syncPresentation; +} + +float get_interpolation_step() { + return presentation_sync_active() ? 1.0f : s_step; +} + +void record_final_mtx(Mtx m, const void* key) { + if (!s_recording || m == nullptr) { + return; + } + + auto& it = s_currentRecording.matrix_values[reinterpret_cast(key)]; + MTXCopy(m, it); +} + +void record_final_mtx(Mtx m) { + record_final_mtx(m, m); +} + +void forget_mtx(const void* key) { + const auto address = reinterpret_cast(key); + s_previousRecording.matrix_values.erase(address); + s_currentRecording.matrix_values.erase(address); + g_replacements.erase(address); +} + +bool lookup_replacement(const void* key, Mtx out) { + if (presentation_sync_active() || !s_replacementsActive || key == nullptr) { + return false; + } + + auto it = g_replacements.find(reinterpret_cast(key)); + if (it == g_replacements.end()) { + return false; + } + + MTXCopy(it->second, out); + return true; +} + +bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out) { + if (presentation_sync_active() || !s_replacementsActive || lhs == nullptr || rhs == nullptr) { + return false; + } + + Mtx lhs_scratch; + Mtx rhs_scratch; + const Mtx* resolved_lhs = resolve_replacement(reinterpret_cast(lhs), &lhs_scratch); + const Mtx* resolved_rhs = resolve_replacement(reinterpret_cast(rhs), &rhs_scratch); + if (resolved_lhs == reinterpret_cast(lhs) && + resolved_rhs == reinterpret_cast(rhs)) + { + return false; + } + + MTXConcat(*resolved_lhs, *resolved_rhs, out); + return true; +} + +void begin_presentation(float step) { + begin_frame(step); + if (!is_enabled()) { + return; + } + + interpolate_replacements(); + + if (s_presentationDepth > 0) { + s_presentationDepth++; + return; + } + s_presentationDepth = 1; + s_cameraPresentationActive = camera_apply_presentation(); + if (s_cameraPresentationActive) { + callbacks_run(); + } +} + +void end_presentation() { + if (s_presentationDepth == 0) { + return; + } + s_presentationDepth--; + if (s_presentationDepth > 0) { + return; + } + + if (s_cameraPresentationActive) { + camera_restore_presentation(); + s_cameraPresentationActive = false; + } +} + +bool is_presentation_active() { + return s_presentationDepth > 0; +} + +void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork) { + if (!is_enabled() || is_presentation_active() || !game_clock::is_sim_frame()) { + return; + } + if (pCallBack == nullptr) { + return; + } + + s_interpolationCallBackWork.push_back({pCallBack, pUserWork}); +} + +void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork, + std::shared_ptr owner) { + if (!should_capture() || is_presentation_active() || pCallBack == nullptr) { + return; + } + s_interpolationCallBackWork.push_back({pCallBack, pUserWork, std::move(owner)}); +} + +} // namespace dusk::interp diff --git a/src/dusk/interp/frame_interpolation.h b/src/dusk/interp/frame_interpolation.h new file mode 100644 index 0000000000..028a111c3e --- /dev/null +++ b/src/dusk/interp/frame_interpolation.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include +#include + +#ifdef __cplusplus +namespace dusk::interp { + +void begin_record(); +void end_record(); +void begin_sim_tick(); +uint64_t sim_tick_seq(); +void begin_frame(float step); +float get_interpolation_step(); + +void request_presentation_sync(); +bool presentation_sync_active(); + +bool is_enabled(); + +bool should_capture(); + +void record_final_mtx(Mtx m, const void* key); +void record_final_mtx(Mtx m); + +void forget_mtx(const void* key); + +bool lookup_replacement(const void* key, Mtx out); +bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out); + +void begin_presentation(float step); +void end_presentation(); +bool is_presentation_active(); + +typedef void (*InterpolationCallBack)(void* pUserWork); +void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork); +void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork, std::shared_ptr owner); + +} // namespace dusk::interp +#endif diff --git a/src/dusk/interp/lerp.h b/src/dusk/interp/lerp.h new file mode 100644 index 0000000000..3083b6b463 --- /dev/null +++ b/src/dusk/interp/lerp.h @@ -0,0 +1,70 @@ +#pragma once + +#include "JSystem/JUtility/TColor.h" +#include "SSystem/SComponent/c_angle.h" +#include "SSystem/SComponent/c_sxyz.h" +#include "SSystem/SComponent/c_xyz.h" + +#include +#include + +#ifdef __cplusplus +namespace dusk::interp { + +inline s16 lerp(s16 lhs, s16 rhs, float step) { + const f32 ra = S2RAD(lhs); + const f32 d = remainderf(S2RAD(rhs) - ra, 2.0f * M_PI); + return cAngle::Radian_to_SAngle(ra + d * step); +} + +inline u16 lerp(u16 lhs, u16 rhs, float step) { + return static_cast(lerp(static_cast(lhs), static_cast(rhs), step)); +} + +inline f32 lerp(f32 lhs, f32 rhs, float step) { + return lhs + (rhs - lhs) * step; +} + +inline void lerp(f32& out, const f32& lhs, const f32& rhs, float step) { + out = lerp(lhs, rhs, step); +} + +inline u8 lerp(u8 lhs, u8 rhs, float step) { + return static_cast(std::lround(lerp(static_cast(lhs), static_cast(rhs), step))); +} + +inline void lerp(cXyz& out, const cXyz& lhs, const cXyz& rhs, float step) { + out.x = lerp(lhs.x, rhs.x, step); + out.y = lerp(lhs.y, rhs.y, step); + out.z = lerp(lhs.z, rhs.z, step); +} + +inline void lerp(csXyz& out, const csXyz& lhs, const csXyz& rhs, float step) { + out.x = lerp(lhs.x, rhs.x, step); + out.y = lerp(lhs.y, rhs.y, step); + out.z = lerp(lhs.z, rhs.z, step); +} + +inline void lerp(Mtx& out, const Mtx& lhs, const Mtx& rhs, float step) { + for (size_t row = 0; row < 3; ++row) { + for (size_t col = 0; col < 4; ++col) { + const float l = lhs[row][col]; + out[row][col] = l + (rhs[row][col] - l) * step; + } + } +} + +inline Vec lerp(const Vec& lhs, const Vec& rhs, float step) { + return {lerp(lhs.x, rhs.x, step), lerp(lhs.y, rhs.y, step), lerp(lhs.z, rhs.z, step)}; +} + +inline JUtility::TColor lerp(const JUtility::TColor& lhs, const JUtility::TColor& rhs, float step) { + return JUtility::TColor( + lerp(lhs.r, rhs.r, step), + lerp(lhs.g, rhs.g, step), + lerp(lhs.b, rhs.b, step), + lerp(lhs.a, rhs.a, step)); +} + +} // namespace dusk::interp +#endif diff --git a/src/dusk/interp/line.cpp b/src/dusk/interp/line.cpp new file mode 100644 index 0000000000..04e54bd603 --- /dev/null +++ b/src/dusk/interp/line.cpp @@ -0,0 +1,159 @@ +#include "dusk/interp/line.h" + +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/lerp.h" +#include "dusk/interp/samples.h" + +#include "m_Do/m_Do_ext.h" + +#include +#include +#include +#include + +namespace { + +struct Record { + Record() + : store(nullptr), previous(nullptr), current(nullptr), sample_capacity(0), tick(0), + strand_count(0), point_count(0), previous_valid(false), current_valid(false) {} + + ~Record() { delete[] store; } + + Record(const Record&) = delete; + Record& operator=(const Record&) = delete; + + void invalidate() { + previous_valid = false; + current_valid = false; + } + + bool ensure(size_t required) { + if (required <= sample_capacity) { + return store != nullptr; + } + if (required > std::numeric_limits::max() / (2 * sizeof(cXyz))) { + return false; + } + + cXyz* next = new (std::nothrow) cXyz[required * 2]; + if (next == nullptr) { + return false; + } + + delete[] store; + store = next; + previous = store; + current = store + required; + sample_capacity = required; + invalidate(); + return true; + } + + cXyz* store; + cXyz* previous; + cXyz* current; + size_t sample_capacity; + uint64_t tick; + u16 strand_count; + u16 point_count; + bool previous_valid; + bool current_valid; +}; + +bool valid(const dusk::interp::line::Points& points) { + if (points.strands == nullptr || points.strand_count == 0 || points.point_count == 0) { + return false; + } + for (u16 strand = 0; strand < points.strand_count; ++strand) { + if (points.strands[strand].field_0x0 == nullptr) { + return false; + } + } + return true; +} + +bool same_layout(const Record& record, const dusk::interp::line::Points& points) { + return record.current_valid && record.strand_count == points.strand_count && + record.point_count == points.point_count; +} + +void copy_points(cXyz* destination, const dusk::interp::line::Points& points) { + const size_t points_size = static_cast(points.point_count) * sizeof(cXyz); + for (u16 strand = 0; strand < points.strand_count; ++strand) { + std::memcpy(destination + static_cast(strand) * points.point_count, + points.strands[strand].field_0x0, points_size); + } +} + +} // namespace + +namespace dusk::interp::line { + +void reset(const void* owner) { + erase_owned_samples(owner); +} + +void capture(const void* owner, Points points) { + if (owner == nullptr || !should_capture()) { + return; + } + + Record& record = get(owner); + if (!valid(points)) { + record.invalidate(); + return; + } + + const size_t required = static_cast(points.strand_count) * points.point_count; + if (!record.ensure(required)) { + record.invalidate(); + return; + } + + const uint64_t tick = sim_tick_seq(); + const bool layout_matches = same_layout(record, points); + + if (layout_matches && tick == record.tick) { + copy_points(record.current, points); + return; + } + + if (layout_matches && tick == record.tick + 1) { + std::swap(record.previous, record.current); + record.previous_valid = true; + } else { + record.previous_valid = false; + } + + copy_points(record.current, points); + record.tick = tick; + record.strand_count = points.strand_count; + record.point_count = points.point_count; + record.current_valid = true; +} + +void write(const void* owner, Points points) { + if (owner == nullptr || !is_enabled() || !valid(points)) { + return; + } + + Record& record = get(owner); + if (!record.previous_valid || !same_layout(record, points) || + record.tick != sim_tick_seq()) + { + return; + } + + const f32 step = get_interpolation_step(); + for (u16 strand = 0; strand < points.strand_count; ++strand) { + cXyz* destination = points.strands[strand].field_0x0; + const size_t offset = static_cast(strand) * points.point_count; + for (u16 point = 0; point < points.point_count; ++point) { + lerp(destination[point], record.previous[offset + point], + record.current[offset + point], step); + } + } +} + +} // namespace dusk::interp::line diff --git a/src/dusk/interp/line.h b/src/dusk/interp/line.h new file mode 100644 index 0000000000..7b4f3b56bc --- /dev/null +++ b/src/dusk/interp/line.h @@ -0,0 +1,19 @@ +#pragma once + +#include "dolphin/types.h" + +class mDoExt_3Dline_c; + +namespace dusk::interp::line { + +struct Points { + mDoExt_3Dline_c* strands; + u16 strand_count; + u16 point_count; +}; + +void reset(const void* owner); +void capture(const void* owner, Points points); +void write(const void* owner, Points points); + +} // namespace dusk::interp::line diff --git a/src/dusk/interp/material.cpp b/src/dusk/interp/material.cpp new file mode 100644 index 0000000000..f8a16162a3 --- /dev/null +++ b/src/dusk/interp/material.cpp @@ -0,0 +1,577 @@ +#include "material.h" + +#include "frame_interpolation.h" + +#include "dusk/game_clock.h" + +#include "d/d_com_inf_game.h" +#include "m_Do/m_Do_graphic.h" + +#include "JSystem/J3DGraphAnimator/J3DAnimation.h" +#include "JSystem/J3DGraphAnimator/J3DModel.h" +#include "JSystem/J3DGraphAnimator/J3DModelData.h" +#include "JSystem/J3DGraphBase/J3DMaterial.h" + +#include +#include +#include +#include +#include +#include + +namespace dusk::interp::material { + +namespace { +struct ViewProjection { + f32 scaleS, scaleT, transS, transT; + + void apply(J3DTexMtxInfo* info) const { + Mtx projection; + const auto* view = dComIfGd_getView(); + C_MTXLightPerspective(projection, view->fovy, view->aspect, scaleS, scaleT, transS, transT); +#if WIDESCREEN_SUPPORT + mDoGph_gInf_c::setWideZoomLightProjection(projection); +#endif + info->setEffectMtx(projection); + } +}; + +struct LightView { + Mtx inverse; + + void apply(J3DLightInfo* light, bool rotateDirection) const { + Mtx changeOfView; + MTXConcat(j3dSys.getViewMtx(), inverse, changeOfView); + MTXMultVec(changeOfView, &light->mLightPosition, &light->mLightPosition); + if (rotateDirection) { + MTXMultVecSR(changeOfView, &light->mLightDirection, &light->mLightDirection); + } + } +}; + +struct Tables { + std::unordered_map controllers; + std::unordered_map resources; + std::unordered_map projections; + std::unordered_map lightViews; +}; + +Tables& tables() { + static Tables stored; + return stored; +} + +template +Frames lookup(const Map& map, Key key) { + auto it = map.find(key); + return it == map.end() ? Frames{} : it->second; +} + +f32 sample_texture_translation(const J3DAnmKeyTableBase& table, const BE(f32)* values, f32 frame, f32 evaluated) { + if (table.mMaxFrame < 2 || table.mType != 1) { + return evaluated; + } + + const auto* keys = values + table.mOffset; + for (u16 i = 1; i < table.mMaxFrame; ++i) { + const auto* left = keys + (i - 1) * 4; + const auto* right = left + 4; + if ((f32)left[0] >= frame) { + break; + } + if ((f32)right[0] <= frame || (f32)right[0] - (f32)left[0] != 1.0f) { + continue; + } + const f32 slope = left[2]; + const f32 jump = (f32)right[1] - (f32)left[1] - slope; + const f32 tiles = std::round(jump); + if (std::abs((f32)right[3] - slope) < 0.0001f && std::abs(tiles) >= 1.0f && + std::abs(jump - tiles) < 0.0001f) + { + return (f32)left[1] + slope * (frame - (f32)left[0]); + } + } + + return evaluated; +} +} // namespace + +Frames get_frames(const J3DFrameCtrl* controller) { + return lookup(tables().controllers, controller); +} + +Frames get_frames(const J3DAnmBase* resource) { + return lookup(tables().resources, resource); +} + +void reset(const J3DFrameCtrl* controller) { + tables().controllers.erase(controller); +} + +void reset(const J3DAnmBase* resource) { + tables().resources.erase(resource); +} + +void bind(J3DAnmBase* resource, const J3DFrameCtrl* controller) { + Frames frames = get_frames(controller); + if (frames.valid && frames.tick == sim_tick_seq() && + frames.epoch == game_clock::g_frameTiming.presentationEpoch) + { + tables().resources[resource] = frames; + } else { + reset(resource); + } +} + +void swap_frames(J3DAnmBase* resource, Frames& frames) { + Frames saved = get_frames(resource); + if (frames.valid) { + tables().resources[resource] = frames; + } else { + reset(resource); + } + frames = saved; +} + +void clear() { + tables().controllers.clear(); + tables().resources.clear(); + tables().projections.clear(); + tables().lightViews.clear(); +} + +void prune() { + tables().projections.clear(); + tables().lightViews.clear(); + const uint64_t tick = sim_tick_seq(); + const uint64_t epoch = game_clock::g_frameTiming.presentationEpoch; + auto stale = [=](const auto& entry) { + return entry.second.epoch != epoch || entry.second.tick < tick - (tick != 0); + }; + std::erase_if(tables().controllers, stale); + std::erase_if(tables().resources, stale); +} + +f32 Frames::read(f32 requested) const { + if (!valid || !smooth || !is_enabled() || !is_presentation_active() || tick != sim_tick_seq() || + epoch != game_clock::g_frameTiming.presentationEpoch || requested != current) + { + return requested; + } + + const f32 step = get_interpolation_step(); + if (step <= 0.0f) { + return previous; + } + if (step >= 1.0f) { + return current; + } + + f32 frame = previous + (loop ? travel : current - previous) * step; + if (loop && (frame < loopStart || frame >= loopEnd)) { + if ((travel > 0.0f && frame >= loopEnd) || (travel < 0.0f && frame < loopStart)) { + const f32 length = loopEnd - loopStart; + frame = loopStart + std::fmod(frame - loopStart, length); + if (frame < loopStart) { + frame += length; + } + } + } + + return frame; +} + +Update::Update(J3DFrameCtrl& controller) : m_controller(controller), m_before(controller.getFrame()), m_continuous(true) { + m_travel = controller.getRate(); + const f32 next = m_before + m_travel; + const f32 start = controller.getStart(); + const f32 end = controller.getEnd(); + m_continuous = std::isfinite(m_before) && std::isfinite(next) && end > start; + switch (controller.getAttribute()) { + case J3DFrameCtrl::EMode_NONE: + break; + case J3DFrameCtrl::EMode_RESET: + m_continuous &= next < end; + break; + case J3DFrameCtrl::EMode_LOOP: + m_continuous &= end - start > 1.0f; + m_loopStart = m_travel < 0.0f ? start : controller.getLoop(); + m_loopEnd = m_travel < 0.0f ? controller.getLoop() : end; + if (m_loopEnd > m_loopStart && controller.getLoop() >= start && controller.getLoop() <= end) { + m_continuous &= m_before >= start && m_before < end; + } else { + m_loopStart = m_loopEnd = 0.0f; + m_continuous &= next >= start && next < end; + } + break; + case J3DFrameCtrl::EMode_REVERSE: + m_continuous &= next >= start && next < end; + break; + case J3DFrameCtrl::EMode_LOOP_REVERSE: + m_continuous &= next >= start && next < end - 1.0f; + break; + default: + m_continuous = false; + break; + } +} + +Update::~Update() { + auto& stored = tables().controllers; + Frames frames = get_frames(&m_controller); + if (!should_capture() || is_presentation_active()) { + reset(&m_controller); + return; + } + const uint64_t tick = sim_tick_seq(); + const uint64_t epoch = game_clock::g_frameTiming.presentationEpoch; + const bool same_tick = frames.tick == tick && frames.epoch == epoch && frames.valid; + const f32 current = m_controller.getFrame(); + const bool loop = m_loopEnd > m_loopStart; + const bool compatible = !same_tick || + (frames.loop == loop && (!loop || (frames.loopStart == m_loopStart && + frames.loopEnd == m_loopEnd && frames.travel * m_travel >= 0.0f))); + const bool continuous = compatible && m_continuous && std::isfinite(current) && + (!same_tick || (frames.smooth && frames.current == m_before)); + const f32 previous = same_tick ? frames.previous : m_before; + Frames captured{continuous ? previous : current, current, tick, epoch, true, continuous}; + captured.loop = continuous && loop; + captured.travel = same_tick ? frames.travel + m_travel : m_travel; + captured.loopStart = m_loopStart; + captured.loopEnd = m_loopEnd; + stored[&m_controller] = captured; +} + +Sample::Sample(J3DAnmBase* animation) : m_animation(animation), m_frame(animation->getFrame()) { + animation->mFrame = get_frames(animation).read(m_frame); +} + +Sample::~Sample() { + m_animation->mFrame = m_frame; +} + +void sample_texture(const J3DAnmTextureSRTKey* animation, u16 track, J3DTextureSRTInfo* result) { + const Frames frames = get_frames(animation); + const f32 current = animation->getFrame(); + const f32 sampled = frames.read(current); + animation->calcTransform(sampled, track, result); + + if (!is_enabled() || !is_presentation_active() || sampled == current) { + return; + } + + const auto* table = animation->mAnmTable + track * 3; + result->mTranslationX = sample_texture_translation(table[0].mTranslateInfo, animation->mTransData, + sampled, result->mTranslationX); + result->mTranslationY = sample_texture_translation(table[1].mTranslateInfo, animation->mTransData, + sampled, result->mTranslationY); +} + +namespace { +template +struct Values { + struct Entry { + T* target; + T captured; + T saved; + }; + std::vector entries; + + bool capture(T* target) { + if (target == nullptr || std::any_of(entries.begin(), entries.end(), + [=](const Entry& entry) { + return entry.target == target; + })) + { + return false; + } + entries.push_back({target, *target, *target}); + return true; + } + + void apply() { + for (auto& entry : entries) { + entry.saved = *entry.target; + *entry.target = entry.captured; + } + } + + void restore() { + for (auto& entry : entries) { + *entry.target = entry.saved; + } + } +}; + +template +bool same_color(const Color& a, const Color& b) { + return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; +} +} // namespace + +struct ModelBindings::State { + std::tuple, + Values, Values, Values, + Values, Values, Values, + Values, Values, Values, + Values, Values, Values, + Values, Values, Values, + Values, Values, Values> values; + + struct Animation { + J3DMaterial* material; + J3DMaterialAnm tracks; + }; + + struct Resource { + J3DAnmBase* target; + f32 frame; + Frames frames; + f32 savedFrame; + Frames savedFrames; + }; + + struct Projection { + J3DTexMtxInfo* target; + ViewProjection recipe; + }; + + struct Light { + J3DLightInfo* target; + LightView view; + bool rotateDirection; + }; + + std::vector animations; + std::vector resources; + std::vector projections; + std::vector lights; + + template + bool capture(T* target) { + return std::get>(values).capture(target); + } +}; + +ModelBindings::ModelBindings() : m_state(std::make_unique()) {} +ModelBindings::~ModelBindings() = default; + +void ModelBindings::capture(J3DMaterial* material) { + auto& state = *m_state; + if (!state.capture(material)) { + return; + } + + auto* color = material->getColorBlock(); + switch (color->getType()) { + case 'CLOF': state.capture(static_cast(color)); break; + case 'CLAB': state.capture(static_cast(color)); break; + case 'CLON': state.capture(static_cast(color)); break; + } + + auto* texgen = material->getTexGenBlock(); + switch (texgen->getType()) { + case 'TGPT': state.capture(static_cast(texgen)); break; + case 'TGB4': state.capture(static_cast(texgen)); break; + case 'TGBC': state.capture(static_cast(texgen)); break; + } + + auto* tev = material->getTevBlock(); + switch (tev->getType()) { + case 'TVPT': state.capture(static_cast(tev)); break; + case 'TVB1': state.capture(static_cast(tev)); break; + case 'TVB2': state.capture(static_cast(tev)); break; + case 'TVB4': state.capture(static_cast(tev)); break; + case 'TV16': state.capture(static_cast(tev)); break; + } + + auto* indirect = material->getIndBlock(); + if (indirect->getType() == 'IBLF') { + state.capture(static_cast(indirect)); + } + + auto* pe = material->getPEBlock(); + switch (pe->getType()) { + case 'PEFG': state.capture(static_cast(pe)); break; + case 'PEFL': state.capture(static_cast(pe)); break; + } + + for (int i = 0; i < 8; ++i) { + if (auto* light = color->getLight(i)) { + if (state.capture(light->getLightInfo())) { + if (auto it = tables().lightViews.find(material); i != 1 && it != tables().lightViews.end()) { + state.lights.push_back({light->getLightInfo(), it->second, i >= 2}); + } + } + state.capture(&light->mLightObj); + } + if (auto* coord = texgen->getTexCoord(i)) { + state.capture(&coord->mTexMtxReg); + } + if (auto* matrix = texgen->getTexMtx(i); state.capture(matrix)) { + auto* info = &matrix->getTexMtxInfo(); + if (auto it = tables().projections.find(info); it != tables().projections.end()) { + state.projections.push_back({info, it->second}); + } + } + } + + auto* animation = material->getMaterialAnm(); + if (animation == nullptr) { + return; + } + + auto& tracks = state.animations.emplace_back(State::Animation{material, *animation}).tracks; + for (int i = 0; i < 2; ++i) { + const auto& track = tracks.getMatColorAnm(i); + if (!track.getAnmFlag()) { + continue; + } + capture(track.getAnimation()); + GXColor evaluated; + track.calc(&evaluated); + if (!same_color(*material->getMatColor(i), evaluated)) { + tracks.setMatColorAnm(i, nullptr); + } + } + for (int i = 0; i < 8; ++i) { + const auto& track = tracks.getTexNoAnm(i); + if (!track.getAnmFlag()) { + continue; + } + capture(track.getAnimation()); + u16 evaluated; + track.calc(&evaluated); + if (material->getTexNo(i) != evaluated) { + tracks.setTexNoAnm(i, nullptr); + } + } + for (int i = 0; i < 8; ++i) { + const auto& track = tracks.getTexMtxAnm(i); + if (!track.getAnmFlag()) { + continue; + } + capture(track.getAnimation()); + J3DTextureSRTInfo evaluated; + track.calc(&evaluated); + if (!material->getTexMtx(i)->getTexMtxInfo().mSRT.operator==(evaluated)) { + tracks.setTexMtxAnm(i, nullptr); + } + } + for (int i = 0; i < 3; ++i) { + const auto& track = tracks.getTevColorAnm(i); + if (!track.getAnmFlag()) { + continue; + } + capture(track.getAnimation()); + GXColorS10 evaluated; + track.calc(&evaluated); + if (!same_color(*material->getTevColor(i), evaluated)) { + tracks.setTevColorAnm(i, nullptr); + } + } + for (int i = 0; i < 4; ++i) { + const auto& track = tracks.getTevKColorAnm(i); + if (!track.getAnmFlag()) { + continue; + } + capture(track.getAnimation()); + GXColor evaluated; + track.calc(&evaluated); + if (!same_color(*material->getTevKColor(i), evaluated)) { + tracks.setTevKColorAnm(i, nullptr); + } + } +} + +void ModelBindings::capture(J3DAnmBase* resource) { + if (resource == nullptr) { + return; + } + + for (const auto& entry : m_state->resources) { + if (entry.target == resource) { + return; + } + } + + m_state->resources.push_back({resource, resource->getFrame(), get_frames(resource)}); +} + +void ModelBindings::apply() { + std::apply([](auto&... values) { (values.apply(), ...); }, m_state->values); + + for (auto& entry : m_state->animations) { + entry.material->setMaterialAnm(&entry.tracks); + } + + for (auto& entry : m_state->resources) { + entry.savedFrame = entry.target->mFrame; + entry.target->mFrame = entry.frame; + entry.savedFrames = entry.frames; + swap_frames(entry.target, entry.savedFrames); + } + + for (const auto& entry : m_state->projections) { + entry.recipe.apply(entry.target); + } + + for (const auto& entry : m_state->lights) { + entry.view.apply(entry.target, entry.rotateDirection); + } +} + +void ModelBindings::restore() { + for (auto& entry : m_state->resources) { + entry.target->mFrame = entry.savedFrame; + swap_frames(entry.target, entry.savedFrames); + } + + std::apply([](auto&... values) { (values.restore(), ...); }, m_state->values); +} + +void record_model(J3DModel* model) { + if (!should_capture() || is_presentation_active()) { + return; + } + + struct Recording { + J3DModel* model; + ModelBindings bindings; + }; + + auto recording = std::make_shared(); + recording->model = model; + J3DModelData* data = model->getModelData(); + for (u16 i = 0; i < data->getMaterialNum(); ++i) { + recording->bindings.capture(data->getMaterialNodePointer(i)); + } + + add_interpolation_callback([](void* work) { + auto& recording = *static_cast(work); + ModelScope scope(recording.bindings); + recording.model->calcMaterial(); + recording.model->diff(); + }, recording.get(), recording); +} + +void set_view_projection(J3DTexMtxInfo* info, f32 scaleS, f32 scaleT, f32 transS, f32 transT) { + ViewProjection recipe{scaleS, scaleT, transS, transT}; + recipe.apply(info); + if (should_capture() && !is_presentation_active()) { + tables().projections.insert_or_assign(info, recipe); + } +} + +void record_light_view(J3DMaterial* material) { + if (!should_capture() || is_presentation_active()) { + return; + } + + LightView view; + if (MTXInverse(j3dSys.getViewMtx(), view.inverse)) { + tables().lightViews.insert_or_assign(material, view); + } +} + +} // namespace dusk::interp::material diff --git a/src/dusk/interp/material.h b/src/dusk/interp/material.h new file mode 100644 index 0000000000..cfd7f5abc7 --- /dev/null +++ b/src/dusk/interp/material.h @@ -0,0 +1,104 @@ +#pragma once + +#include "JSystem/J3DGraphAnimator/J3DMaterialAnm.h" +#include "JSystem/J3DGraphBase/J3DMatBlock.h" + +#include +#include +#include + +class J3DAnmBase; +class J3DFrameCtrl; +class J3DModel; + +namespace dusk::interp::material { + +class ModelBindings; + +struct Frames { + f32 previous = 0.0f; + f32 current = 0.0f; + uint64_t tick = 0; + uint64_t epoch = 0; + bool valid = false; + bool smooth = false; + + bool loop = false; + f32 travel = 0.0f; + f32 loopStart = 0.0f; + f32 loopEnd = 0.0f; + + f32 read(f32 requested) const; +}; + +class Update { +public: + explicit Update(J3DFrameCtrl& controller); + ~Update(); + Update(const Update&) = delete; + Update& operator=(const Update&) = delete; + +private: + J3DFrameCtrl& m_controller; + f32 m_before; + bool m_continuous; + f32 m_travel = 0.0f; + f32 m_loopStart = 0.0f; + f32 m_loopEnd = 0.0f; +}; + +class Sample { +public: + explicit Sample(J3DAnmBase* animation); + ~Sample(); + Sample(const Sample&) = delete; + Sample& operator=(const Sample&) = delete; + +private: + J3DAnmBase* m_animation; + f32 m_frame; +}; + +class ModelBindings { +public: + ModelBindings(); + ~ModelBindings(); + ModelBindings(const ModelBindings&) = delete; + ModelBindings& operator=(const ModelBindings&) = delete; + void capture(J3DMaterial* material); + void apply(); + void restore(); + +private: + void capture(J3DAnmBase* resource); + struct State; + std::unique_ptr m_state; +}; + +class ModelScope { +public: + explicit ModelScope(ModelBindings& bindings) : m_bindings(bindings) { m_bindings.apply(); } + ~ModelScope() { m_bindings.restore(); } + ModelScope(const ModelScope&) = delete; + ModelScope& operator=(const ModelScope&) = delete; +private: + ModelBindings& m_bindings; +}; + +void record_model(J3DModel* model); + +void set_view_projection(J3DTexMtxInfo* info, f32 scaleS, f32 scaleT, f32 transS, f32 transT); +void record_light_view(J3DMaterial* material); + +void sample_texture(const J3DAnmTextureSRTKey* animation, u16 track, J3DTextureSRTInfo* result); + +Frames get_frames(const J3DFrameCtrl* controller); +Frames get_frames(const J3DAnmBase* resource); +void reset(const J3DFrameCtrl* controller); +void reset(const J3DAnmBase* resource); +void bind(J3DAnmBase* resource, const J3DFrameCtrl* controller); +void swap_frames(J3DAnmBase* resource, Frames& frames); +void prune(); +void clear(); + +} // namespace dusk::interp::material diff --git a/src/dusk/interp/menus.h b/src/dusk/interp/menus.h new file mode 100644 index 0000000000..185ee195e7 --- /dev/null +++ b/src/dusk/interp/menus.h @@ -0,0 +1,31 @@ +#pragma once + +#include "samples.h" + +namespace dusk::interp { + +struct MenuPose { + f32 x; + f32 y; + f32 alpha; +}; + +inline void lerp(MenuPose& out, const MenuPose& lhs, const MenuPose& rhs, float step) { + out = {lerp(lhs.x, rhs.x, step), lerp(lhs.y, rhs.y, step), lerp(lhs.alpha, rhs.alpha, step)}; +} + +struct MenuTransition { + Samples poses; +}; + +inline void capture_menu_pose(const void* owner, MenuPose pose) { + if (should_capture()) { + get(owner).poses.capture(&pose, 1); + } +} + +inline MenuPose read_menu_pose(const void* owner, MenuPose current) { + return get(owner).poses.read(0, current); +} + +} // namespace dusk::interp diff --git a/src/dusk/interp/particle.cpp b/src/dusk/interp/particle.cpp new file mode 100644 index 0000000000..8ec079e9e4 --- /dev/null +++ b/src/dusk/interp/particle.cpp @@ -0,0 +1,340 @@ +#include "particle.h" + +#include "frame_interpolation.h" +#include "lerp.h" + +#include "dusk/game_clock.h" + +#include "JSystem/JParticle/JPAChildShape.h" + +#include +#include +#include + +namespace dusk::interp::particle { +namespace { +struct ParticleState { + JGeometry::TVec3 position, localPosition, offsetPosition, velocity, baseAxis; + float scaleX, scaleY, time, age; + u16 angle; + u8 alpha; + GXColor prm, env; + + explicit ParticleState(const JPABaseParticle* p) : ParticleState(p, p->mPosition) {} + + ParticleState(const JPABaseParticle* p, const JGeometry::TVec3& pos) + : position(pos), localPosition(p->mLocalPosition), + offsetPosition(p->mOffsetPosition), velocity(p->mVelocity), baseAxis(p->mBaseAxis), + scaleX(p->mParticleScaleX), scaleY(p->mParticleScaleY), time(p->mTime), age(p->mAge), + angle(p->mRotateAngle), alpha(p->mPrmColorAlphaAnm), prm(p->mPrmClr), + env{p->mEnvClr.r, p->mEnvClr.g, p->mEnvClr.b, 0} {} +}; + +struct EmitterState { + EmitterVisualState visual; + JGeometry::TVec3 position, direction; + float age; + + explicit EmitterState(JPABaseEmitter* e) : visual(e), age(e->mTick) { + e->calcEmitterGlobalPosition(&position); + JPAEmitterWorkData drawWork; + drawWork.mpEmtr = e; + e->pRes->calcWorkData_d(&drawWork); + direction = drawWork.mGlobalEmtrDir; + } +}; + +template +struct History { + T previous, current; + uint64_t tick, epoch; +}; + +std::unordered_map> s_particles; +std::unordered_map> s_emitters; + +// Infuriatingly, some particles are recreated every frame rather than having a real +// lifespan, so it's necessary to track their relationship to be able to do interpolation +struct ReplacementTrack { + uint64_t generation = 0, tick = 0, epoch = 0; + std::optional> frames; +}; + +void capture_replacement(ReplacementTrack& track, History& h) { + if (track.frames && track.frames->epoch == h.epoch) { + const auto& last = *track.frames; + if (last.tick == h.tick) { + h.previous = last.previous; + } else if (last.tick + 1 == h.tick) { + h.previous = last.current; + } + } + track.frames = h; + track.tick = h.tick; + track.epoch = h.epoch; +} + +struct ChildBirth { + const JPABaseParticle* parent; + uint64_t generation, tick, epoch; +}; + +std::unordered_map s_childTracks; +std::unordered_map s_childBirths; +std::unordered_map s_singleFrameParticles; +uint64_t s_childGeneration = 0; + +template +void capture_state(Map& map, Key key, const State& current) { + const uint64_t tick = sim_tick_seq(); + const uint64_t epoch = game_clock::g_frameTiming.presentationEpoch; + auto [it, added] = map.try_emplace(key, History{current, current, tick, epoch}); + auto& h = it->second; + if (!added) { + if (h.epoch != epoch || (h.tick != tick && h.tick + 1 != tick) || current.age < h.current.age) { + h.previous = current; + } else if (h.tick != tick) { + h.previous = h.current; + } + h.current = current; + h.tick = tick; + h.epoch = epoch; + } +} + +template +const Map::mapped_type* history(const Map& map, Key key) { + if (!is_enabled() || !is_presentation_active()) { + return nullptr; + } + auto it = map.find(key); + if (it == map.end() || it->second.tick != sim_tick_seq() || + it->second.epoch != game_clock::g_frameTiming.presentationEpoch) + { + return nullptr; + } + return &it->second; +} + +u16 blend_angle(u16 a, u16 b, float t) { + int delta = (static_cast(b) - a + 32768) & 65535; + delta -= 32768; + return static_cast(static_cast(a) + std::lround(delta * t)); +} + +template +void prune_map(Map& map) { + for (auto it = map.begin(); it != map.end();) { + if (it->second.epoch != game_clock::g_frameTiming.presentationEpoch || + it->second.tick + 1 < sim_tick_seq()) + { + it = map.erase(it); + } else { + ++it; + } + } +} +} // namespace + +void clear() { + s_particles.clear(); + s_emitters.clear(); + s_childTracks.clear(); + s_childBirths.clear(); + s_singleFrameParticles.clear(); +} + +void prune() { + prune_map(s_particles); + prune_map(s_emitters); + prune_map(s_childTracks); + prune_map(s_childBirths); + prune_map(s_singleFrameParticles); +} + +void reset(const JPABaseParticle* p) { + s_particles.erase(p); + s_childTracks.erase(p); + s_childBirths.erase(p); +} + +void reset_child(const JPABaseParticle* p, const JPABaseParticle* parent, + const JPAEmitterWorkData* work) { + reset(p); + const auto* shape = work->mpRes->getCsp(); + if (!should_capture() || shape->getLife() != 1 || shape->getRate() != 1 || shape->getStep() != 0) { + return; + } + const uint64_t tick = sim_tick_seq(); + const uint64_t epoch = game_clock::g_frameTiming.presentationEpoch; + auto [it, added] = s_childTracks.try_emplace(parent, ReplacementTrack{0, tick, epoch, std::nullopt}); + if (added) { + it->second.generation = ++s_childGeneration; + } + s_childBirths.insert_or_assign(p, ChildBirth{parent, it->second.generation, tick, epoch}); +} + +void reset(const JPABaseEmitter* e) { + s_emitters.erase(e); + s_singleFrameParticles.erase(e); +} + +void capture_birth(const JPABaseParticle* p, const JPAEmitterWorkData* work) { + if (!should_capture() || p->mAge != 0 || s_particles.contains(p)) { + return; + } + const JGeometry::TVec3 position(p->mOffsetPosition.x + p->mLocalPosition.x * work->mPublicScale.x, + p->mOffsetPosition.y + p->mLocalPosition.y * work->mPublicScale.y, + p->mOffsetPosition.z + p->mLocalPosition.z * work->mPublicScale.z); + capture_state(s_particles, p, ParticleState(p, position)); +} + +void capture(const JPABaseParticle* p) { + if (should_capture() && p->mAge >= 0) { + capture_state(s_particles, p, ParticleState(p)); + if (auto birth = s_childBirths.find(p); birth != s_childBirths.end()) { + auto track = s_childTracks.find(birth->second.parent); + auto& h = s_particles.at(p); + if (p->mAge == 0 && birth->second.tick == h.tick && birth->second.epoch == h.epoch && + track != s_childTracks.end() && track->second.generation == birth->second.generation) + { + capture_replacement(track->second, h); + } + s_childBirths.erase(birth); + } + } +} + +void capture(JPABaseEmitter* e) { + if (should_capture()) { + capture_state(s_emitters, e, EmitterState(e)); + + const auto* dynamics = e->pRes->getDyn(); + if (e->mLifeTime == 1 && e->mRate == 1.0f && e->mRateStep == 0 && + dynamics->getRateRndm() == 0.0f && dynamics->getLifetimeRndm() == 0.0f && + e->mAlivePtclBase.getNum() == 1) + { + const auto* particle = e->mAlivePtclBase.getFirst()->getObject(); + if (particle->mAge == 0 && particle->mLifeTime == 1) { + auto& track = s_singleFrameParticles[e]; + capture_state(s_particles, particle, ParticleState(particle)); + auto& h = s_particles.at(particle); + capture_replacement(track, h); + return; + } + } + s_singleFrameParticles.erase(e); + } +} + +JPABaseParticle* present(JPABaseParticle* p, JPABaseParticle& scratch, float& age) { + age = p->mAge; + if (!game_clock::is_presentation_frame()) { + return p; + } + scratch = *p; + const auto* h = history(s_particles, p); + const float t = get_interpolation_step(); + if (h == nullptr || t >= 1.0f) { + return &scratch; + } + const auto& a = h->previous; + const auto& b = h->current; + scratch.mPosition = lerp(a.position, b.position, t); + scratch.mLocalPosition = lerp(a.localPosition, b.localPosition, t); + scratch.mOffsetPosition = lerp(a.offsetPosition, b.offsetPosition, t); + scratch.mVelocity = lerp(a.velocity, b.velocity, t); + scratch.mBaseAxis = lerp(a.baseAxis, b.baseAxis, t); + if (scratch.mBaseAxis.isZero()) { + scratch.mBaseAxis = b.baseAxis; + } + scratch.mParticleScaleX = lerp(a.scaleX, b.scaleX, t); + scratch.mParticleScaleY = lerp(a.scaleY, b.scaleY, t); + scratch.mRotateAngle = blend_angle(a.angle, b.angle, t); + scratch.mPrmColorAlphaAnm = lerp(a.alpha, b.alpha, t); + scratch.mPrmClr = lerp(a.prm, b.prm, t); + const GXColor env = lerp(a.env, b.env, t); + scratch.mEnvClr.r = env.r; + scratch.mEnvClr.g = env.g; + scratch.mEnvClr.b = env.b; + scratch.mTime = lerp(a.time, b.time, t); + age = lerp(a.age, b.age, t); + return &scratch; +} + +JGeometry::TVec3 position(const JPABaseParticle* p) { + const auto* h = history(s_particles, p); + const float t = get_interpolation_step(); + if (h != nullptr && t < 1.0f) { + return lerp(h->previous.position, h->current.position, t); + } + return p->mPosition; +} + +JGeometry::TVec3 emitter_position(const JPABaseEmitter* e, const JGeometry::TVec3& current) { + const auto* h = history(s_emitters, e); + const float t = get_interpolation_step(); + if (h != nullptr && t < 1.0f) { + return lerp(h->previous.position, h->current.position, t); + } + return current; +} + +JGeometry::TVec3 emitter_direction(const JPABaseEmitter* e, const JGeometry::TVec3& current) { + const auto* h = history(s_emitters, e); + const float t = get_interpolation_step(); + if (h != nullptr && t < 1.0f) { + return lerp(h->previous.direction, h->current.direction, t); + } + return current; +} + +float emitter_age(const JPABaseEmitter* e) { + const auto* h = history(s_emitters, e); + const float t = get_interpolation_step(); + if (h != nullptr && t < 1.0f) { + return lerp(h->previous.age, h->current.age, t); + } + return e->mTick; +} + +EmitterVisualState::EmitterVisualState(const JPABaseEmitter* e) + : scale(e->mGlobalPScl), prm(e->mPrmClr), env(e->mEnvClr), globalPrm(e->mGlobalPrmClr), + globalEnv(e->mGlobalEnvClr) {} + +void EmitterVisualState::apply(JPABaseEmitter* e) const { + e->mGlobalPScl = scale; + e->mPrmClr = prm; + e->mEnvClr = env; + e->mGlobalPrmClr = globalPrm; + e->mGlobalEnvClr = globalEnv; +} + +EmitterDraw::EmitterDraw(JPABaseEmitter* e) : m_emitter(nullptr), m_saved(e) { + if (!game_clock::is_presentation_frame()) { + return; + } + m_emitter = e; + const auto* h = history(s_emitters, e); + const float t = get_interpolation_step(); + if (h == nullptr || t >= 1.0f) { + return; + } + const auto& a = h->previous.visual; + const auto& b = h->current.visual; + EmitterVisualState visual(e); + visual.scale.set(lerp(a.scale.x, b.scale.x, t), lerp(a.scale.y, b.scale.y, t)); + visual.prm = lerp(a.prm, b.prm, t); + visual.env = lerp(a.env, b.env, t); + visual.globalPrm = lerp(a.globalPrm, b.globalPrm, t); + visual.globalEnv = lerp(a.globalEnv, b.globalEnv, t); + visual.apply(e); +} + +EmitterDraw::~EmitterDraw() { + if (m_emitter != nullptr) { + m_saved.apply(m_emitter); + } +} + +} // namespace dusk::interp::particle diff --git a/src/dusk/interp/particle.h b/src/dusk/interp/particle.h new file mode 100644 index 0000000000..acadf55619 --- /dev/null +++ b/src/dusk/interp/particle.h @@ -0,0 +1,42 @@ +#pragma once + +#include "JSystem/JParticle/JPAEmitter.h" +#include "JSystem/JParticle/JPAParticle.h" + +namespace dusk::interp::particle { + +void clear(); +void prune(); +void reset(const JPABaseParticle*); +void reset(const JPABaseEmitter*); +void reset_child(const JPABaseParticle*, const JPABaseParticle* parent, const JPAEmitterWorkData*); +void capture_birth(const JPABaseParticle*, const JPAEmitterWorkData*); +void capture(const JPABaseParticle*); +void capture(JPABaseEmitter*); + +JPABaseParticle* present(JPABaseParticle*, JPABaseParticle& scratch, float& age); +JGeometry::TVec3 position(const JPABaseParticle*); +JGeometry::TVec3 emitter_position(const JPABaseEmitter*, const JGeometry::TVec3&); +JGeometry::TVec3 emitter_direction(const JPABaseEmitter*, const JGeometry::TVec3&); +float emitter_age(const JPABaseEmitter*); + +struct EmitterVisualState { + JGeometry::TVec2 scale; + GXColor prm, env, globalPrm, globalEnv; + explicit EmitterVisualState(const JPABaseEmitter*); + void apply(JPABaseEmitter*) const; +}; + +class EmitterDraw { +public: + explicit EmitterDraw(JPABaseEmitter*); + ~EmitterDraw(); + EmitterDraw(const EmitterDraw&) = delete; + EmitterDraw& operator=(const EmitterDraw&) = delete; + +private: + JPABaseEmitter* m_emitter; + EmitterVisualState m_saved; +}; + +} // namespace dusk::interp::particle diff --git a/src/dusk/interp/samples.cpp b/src/dusk/interp/samples.cpp new file mode 100644 index 0000000000..4221814f9c --- /dev/null +++ b/src/dusk/interp/samples.cpp @@ -0,0 +1,65 @@ +#include "dusk/interp/samples.h" + +#include +#include + +namespace dusk::interp { +namespace { + +struct Slot { + const void* type; + void* ptr; + void (*destroy)(void*); +}; + +using OwnerMap = absl::flat_hash_map>; + +OwnerMap& owner_map() { + static OwnerMap s_map; + return s_map; +} + +} // namespace + +void* detail::acquire(const void* key, const void* type, void* (*make)(), void (*destroy)(void*)) { + const uintptr_t id = reinterpret_cast(key); + auto& slots = owner_map()[id]; + for (Slot& slot : slots) { + if (slot.type == type) { + return slot.ptr; + } + } + + void* ptr = make(); + slots.push_back({type, ptr, destroy}); + return ptr; +} + +void erase_owned_samples(const void* key) { + if (key == nullptr) { + return; + } + + OwnerMap& stored = owner_map(); + auto it = stored.find(reinterpret_cast(key)); + if (it == stored.end()) { + return; + } + + for (Slot& slot : it->second) { + slot.destroy(slot.ptr); + } + stored.erase(it); +} + +void clear_owned_samples() { + OwnerMap& stored = owner_map(); + for (auto& entry : stored) { + for (Slot& slot : entry.second) { + slot.destroy(slot.ptr); + } + } + stored.clear(); +} + +} // namespace dusk::interp diff --git a/src/dusk/interp/samples.h b/src/dusk/interp/samples.h new file mode 100644 index 0000000000..e1ccc661ad --- /dev/null +++ b/src/dusk/interp/samples.h @@ -0,0 +1,106 @@ +#pragma once + +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/lerp.h" + +#include + +namespace dusk::interp { + +template +class Samples { +public: + void reset() { m_samples.clear(); } + + template + void capture(int count, Sample sample) { + if (!should_capture()) { + return; + } + if (count <= 0) { + reset(); + return; + } + + const uint64_t tick = sim_tick_seq(); + const uint64_t epoch = game_clock::g_frameTiming.presentationEpoch; + const bool continuous = epoch == m_epoch && (tick == m_tick || tick == m_tick + 1); + if (!continuous) { + reset(); + } + if (count < m_samples.size()) { + m_samples.erase(m_samples.begin() + count, m_samples.end()); + } + m_samples.reserve(count); + for (int i = 0; i < count; ++i) { + T current = sample(i); + if (i == m_samples.size()) { + m_samples.push_back({current, current}); + continue; + } + auto& entry = m_samples[i]; + if (tick != m_tick) { + entry.previous = entry.current; + } + entry.current = current; + } + m_tick = tick; + m_epoch = epoch; + } + + void capture(const T* source, int count) { + capture(source != nullptr ? count : 0, [&](int i) { return source[i]; }); + } + + T read(int index, const T& current) const { + if (!valid(index)) { + return current; + } + T result = current; + const auto& entry = m_samples[index]; + lerp(result, entry.previous, entry.current, get_interpolation_step()); + return result; + } + + T read(int index, const T& current, f32 snap_distance) const { + if (!valid(index) || + m_samples[index].previous.abs(m_samples[index].current) > snap_distance) + { + return current; + } + return read(index, current); + } + +private: + bool valid(int index) const { + return is_enabled() && is_presentation_active() && m_tick == sim_tick_seq() && + m_epoch == game_clock::g_frameTiming.presentationEpoch && + index >= 0 && static_cast(index) < m_samples.size(); + } + + struct Entry { + T previous; + T current; + }; + std::vector m_samples; + uint64_t m_tick = 0; + uint64_t m_epoch = 0; +}; + +namespace detail { +void* acquire(const void* key, const void* type, void* (*make)(), void (*destroy)(void*)); +} + +template +Record& get(const void* key) { + static const char token{}; + return *static_cast(detail::acquire( + key, &token, []() -> void* { return new Record; }, + [](void* p) { delete static_cast(p); })); +} + +void erase_owned_samples(const void* key); +void clear_owned_samples(); + +} // namespace dusk::interp diff --git a/src/dusk/interp/sight.h b/src/dusk/interp/sight.h new file mode 100644 index 0000000000..c56c186cae --- /dev/null +++ b/src/dusk/interp/sight.h @@ -0,0 +1,23 @@ +#pragma once + +#include "dusk/interp/samples.h" + +namespace dusk::interp { + +struct SightAnimation { + f32 bck, brk, alpha, brkEnd; +}; + +inline void lerp(SightAnimation& out, const SightAnimation& a, const SightAnimation& b, f32 step) { + out.bck = lerp(a.bck, b.bck < a.bck ? b.bck + 29.0f : b.bck, step); + if (out.bck >= 50.0f) { + out.bck -= 29.0f; + } + out.brk = lerp(a.brk, b.brk < a.brk ? b.brk + b.brkEnd : b.brk, step); + if (out.brk >= b.brkEnd) { + out.brk -= b.brkEnd; + } + out.alpha = lerp(a.alpha, b.alpha, step); +} + +} // namespace dusk::interp diff --git a/src/dusk/interp/user_interface.h b/src/dusk/interp/user_interface.h new file mode 100644 index 0000000000..30ddbf2034 --- /dev/null +++ b/src/dusk/interp/user_interface.h @@ -0,0 +1,158 @@ +#pragma once + +#include "dusk/game_clock.h" + +#include "JSystem/J2DGraph/J2DAnimation.h" +#include "JSystem/J2DGraph/J2DPane.h" +#include "SSystem/SComponent/c_lib.h" +#include "SSystem/SComponent/c_math.h" + +#include + +#include +#include +#include + +namespace dusk::vdt { + +inline f32 clamped_fraction(f32 timer, f32 duration) { + if (duration <= 0.0f) { + return 1.0f; + } + return std::clamp(timer / duration, 0.0f, 1.0f); +} + +inline void advance_looping_frame(f32& frame, f32 speed, f32 max) { + if (max <= 0.0f) { + return; + } + frame += speed * game_clock::original_frames(); + if (frame >= max) { + frame = fmodf(frame, max); + } +} + +inline void advance_toward_frame(f32& frame, f32 target, f32 speed) { + if (frame == target) { + return; + } + const f32 step = speed * game_clock::original_frames(); + frame = frame < target ? std::min(frame + step, target) : std::max(frame - step, target); +} + +template +bool request_animation(Animation& animation, f32& frame, s16 time, + decltype(animation.start) from, decltype(animation.end) to, u8 curve) { + if (frame < time) { + animation = {true, time, curve, from, to}; + return false; + } + animation.active = false; + frame = time; + return true; +} + +template +void present_animation(Animation& animation, f32& frame, Pane& pane, Apply apply) { + if (!animation.active) { + return; + } + advance_toward_frame(frame, animation.duration, 1.0f); + apply(pane.rateCalc(animation.duration, frame, animation.curve)); + if (frame >= animation.duration) { + animation.active = false; + } +} + +inline void present_looping(f32& frame, J2DAnmBase* anm, f32 speed) { + if (anm == nullptr) { + return; + } + advance_looping_frame(frame, speed, anm->getFrameMax()); + anm->setFrame(frame); +} + +inline void present_toward(f32& frame, f32 target, J2DAnmTransform* anm, J2DPane* pane = nullptr) { + if (anm == nullptr) { + return; + } + if (pane != nullptr && pane->mTransform != anm) { + return; + } + advance_toward_frame(frame, target, 2.0f); + anm->setFrame(frame); + if (pane != nullptr) { + pane->animationTransform(); + } +} + +inline void present_selected(f32& frame, J2DPane* pane, J2DAnmTransform* first, f32 firstTarget, + J2DAnmTransform* second, f32 secondTarget) +{ + if (pane != nullptr) { + if (pane->mTransform == first) { + present_toward(frame, firstTarget, first, pane); + } else if (pane->mTransform == second) { + present_toward(frame, secondTarget, second, pane); + } + } +} + +inline void present_shared(f32& frame, f32 target, J2DAnmTransform* anm, + std::initializer_list panes) +{ + if (frame == target) { + return; + } + bool advanced = false; + for (J2DPane* pane : panes) { + if (pane != nullptr && pane->mTransform == anm) { + if (!advanced) { + present_toward(frame, target, anm); + advanced = true; + } + pane->animationTransform(); + } + } +} + +inline void present_addCalc(f32* value, f32 target, f32 scale, f32 maxStep, f32 minStep) { + const f32 frames = game_clock::original_frames(); + if (*value == target || frames <= 0.0f) { + return; + } + const f32 timedScale = frames == 1.0f ? scale : 1.0f - std::pow(1.0f - scale, frames); + cLib_addCalc(value, target, timedScale, maxStep * frames, minStep * frames); +} + +inline void present_addCalc2(f32* value, f32 target, f32 scale, f32 maxStep, f32 snap) { + const f32 frames = game_clock::original_frames(); + if (*value == target || frames <= 0.0f) { + return; + } + const f32 timedScale = frames == 1.0f ? scale : 1.0f - std::pow(1.0f - scale, frames); + cLib_addCalc2(value, target, timedScale, maxStep * frames); + if (fabsf(*value - target) < snap) { + *value = target; + } +} + +inline f32 present_sine_ease(f32 max, f32 value) { + if (max <= 0.0f) { + return 1.0f; + } + value = std::clamp(value, 0.0f, max); + f32 rate = value / max; + rate = cM_ssin((int)(0.5f * (32768.0f * rate))); + return rate * rate; +} + +inline bool present_chase(f32* value, f32 target, f32 scale, f32 maxStep, f32 snap) { + if (*value == target) { + return false; + } + present_addCalc2(value, target, scale, maxStep, snap); + return true; +} + +} // namespace dusk::vdt diff --git a/src/dusk/interp/vertex.cpp b/src/dusk/interp/vertex.cpp new file mode 100644 index 0000000000..790f508484 --- /dev/null +++ b/src/dusk/interp/vertex.cpp @@ -0,0 +1,131 @@ +#include "vertex.h" + +#include "frame_interpolation.h" +#include "dusk/game_clock.h" +#include "JSystem/J3DGraphBase/J3DVertex.h" + +#include +#include +#include + +namespace dusk::interp::vertex { +namespace { + +struct Array { + std::vector previous; + std::vector current; + std::vector presentation; + const void* source = nullptr; + float cached_step = -1.0f; + + void capture(const void* data, u32 count, bool continuous, bool same_tick) { + source = data; + cached_step = -1.0f; + if (data == nullptr || count == 0) { + *this = {}; + return; + } + continuous &= current.size() == count; + if (continuous && !same_tick) previous.swap(current); + const auto* values = static_cast(data); + current.assign(values, values + count); + if (!continuous) previous = current; + } + + void* read(void* requested, float step) { + if (requested != source || current.empty() || step >= 1.0f) return requested; + if (step <= 0.0f) return previous.data(); + if (step != cached_step) { + presentation.resize(current.size()); + for (size_t i = 0; i < current.size(); ++i) { + const auto& a = previous[i]; + const auto& b = current[i]; + presentation[i] = {a.x + (b.x - a.x) * step, + a.y + (b.y - a.y) * step, + a.z + (b.z - a.z) * step}; + } + cached_step = step; + } + return presentation.data(); + } +}; + +struct Record { + const J3DDeformData* deformation = nullptr; + const J3DVertexData* layout = nullptr; + uint64_t tick = 0; + uint64_t epoch = 0; + Array positions; + Array normals; +}; + +auto& records() { + static std::unordered_map stored; + return stored; +} + +void* read(const J3DVertexBuffer* buffer, void* current, Array Record::* array) { + if (!is_enabled() || !is_presentation_active()) return current; + auto it = records().find(buffer); + if (it == records().end() || it->second.tick != sim_tick_seq() || + it->second.epoch != game_clock::g_frameTiming.presentationEpoch) + { + return current; + } + return (it->second.*array).read(current, get_interpolation_step()); +} + +} // namespace + +void capture(J3DVertexBuffer* buffer, const J3DDeformData* deformation) { + if (!should_capture() || is_presentation_active()) return; + if (deformation == nullptr) { + reset(buffer); + return; + } + auto* layout = buffer->getVertexData(); + void* positions = buffer->getCurrentVtxPos(); + void* normals = buffer->getCurrentVtxNrm(); + if (layout->getVtxNum() == 0 || layout->getVtxPosType() != GX_F32 || + positions == layout->getVtxPosArray()) positions = nullptr; + if (layout->getNrmNum() == 0 || layout->getVtxNrmType() != GX_F32 || + normals == layout->getVtxNrmArray()) normals = nullptr; + if (positions == nullptr && normals == nullptr) { + reset(buffer); + return; + } + + auto& record = records()[buffer]; + const uint64_t tick = sim_tick_seq(); + const uint64_t epoch = game_clock::g_frameTiming.presentationEpoch; + const bool same_tick = record.tick == tick; + const bool continuous = record.deformation == deformation && record.layout == layout && + record.epoch == epoch && (same_tick || record.tick + 1 == tick); + + record.positions.capture(positions, layout->getVtxNum(), continuous, same_tick); + record.normals.capture(normals, layout->getNrmNum(), continuous, same_tick); + record.deformation = deformation; + record.layout = layout; + record.tick = tick; + record.epoch = epoch; +} + +void* positions(const J3DVertexBuffer* buffer, void* current) { return read(buffer, current, &Record::positions); } +void* normals(const J3DVertexBuffer* buffer, void* current) { return read(buffer, current, &Record::normals); } +void reset(const J3DVertexBuffer* buffer) { records().erase(buffer); } + +void invalidate(const J3DDeformData* deformation) { + std::erase_if(records(), [=](const auto& entry) { return entry.second.deformation == deformation; }); +} + +void prune() { + const uint64_t tick = sim_tick_seq(); + const uint64_t epoch = game_clock::g_frameTiming.presentationEpoch; + std::erase_if(records(), [=](const auto& entry) { + return entry.second.epoch != epoch || entry.second.tick < tick - (tick != 0); + }); +} + +void clear() { records().clear(); } + +} // namespace dusk::interp::vertex diff --git a/src/dusk/interp/vertex.h b/src/dusk/interp/vertex.h new file mode 100644 index 0000000000..ce00a84c1c --- /dev/null +++ b/src/dusk/interp/vertex.h @@ -0,0 +1,16 @@ +#pragma once + +class J3DVertexBuffer; +class J3DDeformData; + +namespace dusk::interp::vertex { + +void capture(J3DVertexBuffer* buffer, const J3DDeformData* deformation); +void* positions(const J3DVertexBuffer* buffer, void* current); +void* normals(const J3DVertexBuffer* buffer, void* current); +void reset(const J3DVertexBuffer* buffer); +void invalidate(const J3DDeformData* deformation); +void prune(); +void clear(); + +} // namespace dusk::interp::vertex diff --git a/src/dusk/iso_validate.hpp b/src/dusk/iso_validate.hpp index e5a8eac1ef..b7740b636f 100644 --- a/src/dusk/iso_validate.hpp +++ b/src/dusk/iso_validate.hpp @@ -38,7 +38,7 @@ using VerificationStatus = borealis::disc::Progress; struct DiscInfo { Platform platform = Platform::Unknown; Region region = Region::NorthAmerica; - std::uint8_t revision = 0; + uint8_t revision = 0; }; ValidationError inspect(const char* path, DiscInfo& info); diff --git a/src/dusk/mod_loader.hpp b/src/dusk/mod_loader.hpp index d07f0d6f2c..3d5094ca70 100644 --- a/src/dusk/mod_loader.hpp +++ b/src/dusk/mod_loader.hpp @@ -2,6 +2,7 @@ #include "dusk/config.hpp" #include "dusk/config_var.hpp" +#include "dusk/mods/updates.hpp" #include "mods/api.h" #include "mods/runtime.h" @@ -11,6 +12,7 @@ #include #include #include +#include #include namespace dusk::mods { @@ -83,6 +85,19 @@ struct ModSearchDir { std::filesystem::path nativeLibDir; }; +struct ModOperation { + enum class State : u8 { + Pending, + Succeeded, + Failed, + }; + + State state = State::Pending; + std::string message; +}; + +using ModOperationHandle = std::shared_ptr; + struct ModMetaParsed { uint32_t abiVersion = 0; std::vector imports; @@ -177,6 +192,14 @@ enum class NativeModStatus : u8 { }; struct LoadedMod { + struct FileIdentity { + std::uintmax_t size = 0; + std::filesystem::file_time_type modified{}; + bool valid = false; + + bool operator==(const FileIdentity&) const = default; + }; + ModMetadata metadata; std::filesystem::path modPath; std::filesystem::path dir; @@ -186,8 +209,12 @@ struct LoadedMod { std::string dataDirUtf8; uint32_t searchDirIndex = 0; - // Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported. - bool inPlace = false; + bool fromDirectory = false; + // Native lib is dlopen'd in place. + bool nativeInPlace = false; + bool hasUserPackage = false; + bool hasBundledCopy = false; + FileIdentity fileIdentity; std::unique_ptr> cvarIsEnabled; config::Subscription enabledSubscription = 0; @@ -229,8 +256,15 @@ struct LoadedMod { // Mods this mod imports services from, and mods importing services from this mod. std::vector dependencies; std::vector dependents; + + [[nodiscard]] bool is_enabled() const { + return cvarIsEnabled != nullptr && cvarIsEnabled->getValue(); + } + [[nodiscard]] bool activation_failed() const { return loadFailed || (is_enabled() && !active); } }; +struct PackageCandidate; + class ModLoader { public: static ModLoader& instance(); @@ -243,9 +277,22 @@ public: void request_enable(std::string_view id); void request_disable(std::string_view id); - void request_reload(std::string_view id); + ModOperationHandle request_reload(std::string_view id); + ModOperationHandle request_install( + std::filesystem::path path, std::optional update = {}); + ModOperationHandle request_uninstall(std::string_view id); + ModOperationHandle request_reactivate(std::string_view id); void notify_mod_failure(LoadedMod& mod, bool firstFailure); + [[nodiscard]] std::filesystem::path user_mods_dir() const; + [[nodiscard]] bool can_uninstall(const LoadedMod& mod) const; + [[nodiscard]] bool can_update(const LoadedMod& mod) const; + [[nodiscard]] LoadedMod* find_mod(std::string_view id); + [[nodiscard]] const LoadedMod* find_mod(std::string_view id) const; + + [[nodiscard]] bool initialized() const noexcept { return m_startupComplete; } + [[nodiscard]] uint64_t generation() const noexcept { return m_generation; } + [[nodiscard]] auto mods() const { return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; }); } @@ -255,10 +302,31 @@ public: } private: - enum class RequestKind : u8 { Enable, Disable, Reload }; - struct Request { + enum class LifecycleAction : u8 { Enable, Disable, Reactivate }; + struct LifecycleRequest { std::string modId; - RequestKind kind; + LifecycleAction action; + std::shared_ptr operation; + }; + struct InstallRequest { + std::filesystem::path stagedPath; + std::optional update; + std::shared_ptr operation; + }; + struct ReloadRequest { + std::string modId; + std::shared_ptr operation; + }; + struct UninstallRequest { + std::string modId; + std::shared_ptr operation; + }; + using Request = std::variant; + + struct OperationResult { + bool success = true; + std::string message; + LoadedMod* mod = nullptr; }; // ModLoader::tick runs inside fapGm_Execute, so code from an unloading mod can still be // live on the stack (its frame unwinds after the tick). dlclose is therefore deferred to @@ -274,10 +342,12 @@ private: std::vector m_pendingRequests; std::vector m_pendingFailures; std::vector m_retiredNatives; + uint64_t m_generation = 0; bool m_initialized = false; bool m_startupComplete = false; - void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex); + LoadedMod* try_load_mod(const std::filesystem::path& modPath, bool fromDir, + uint32_t searchDirIndex, std::unique_ptr bundle = {}); void load_native(LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries); bool load_native_if_present(LoadedMod& mod); @@ -296,20 +366,32 @@ private: [[nodiscard]] std::string describe_missing_import( const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const; - LoadedMod* find_mod(std::string_view id) const; void drain_retired_natives(); void apply_pending_requests(); + [[nodiscard]] OperationResult install_staged( + const std::filesystem::path& path, const std::optional& update); + [[nodiscard]] OperationResult load_runtime_mod(const std::filesystem::path& path); + [[nodiscard]] OperationResult reload_runtime_mod( + LoadedMod& mod, const PackageCandidate* replacement = nullptr); + [[nodiscard]] OperationResult uninstall_runtime_mod(LoadedMod& mod); + [[nodiscard]] OperationResult runtime_result(LoadedMod& mod); + void forget_mod(LoadedMod& mod); void flush_toasts(); void on_enabled_changed(LoadedMod& mod); // Deactivates `target` (if needed) and its transitive dependents, optionally re-reads the // bundle from disk, then reactivates whatever the current cvar/provider state allows. - void apply_lifecycle_change(LoadedMod& target, bool reload); + void apply_lifecycle_change( + LoadedMod& target, bool reload, const PackageCandidate* replacement = nullptr); // `target` plus transitive active/suspended dependents, in m_mods (init) order. - std::vector collect_lifecycle_set(LoadedMod& target); + std::vector collect_lifecycle_set(LoadedMod& target) const; + void resume_lifecycle_set(const std::vector& mods); bool reload_bundle(LoadedMod& mod); bool ensure_native_loaded(LoadedMod& mod); }; +bool inspect_mod_bundle(const std::filesystem::path& path, ModMetadata& metadata, + std::string& error, bool* hasNative = nullptr) noexcept; + using ModIndex = std::ranges::range_difference_t().mods())>; } // namespace dusk::mods diff --git a/src/dusk/mods/catalog.cpp b/src/dusk/mods/catalog.cpp new file mode 100644 index 0000000000..c060cfdb61 --- /dev/null +++ b/src/dusk/mods/catalog.cpp @@ -0,0 +1,611 @@ +#include "catalog.hpp" + +#include "dusk/app_info.hpp" +#include "fmt/format.h" +#include "nlohmann/json.hpp" + +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#endif + +namespace dusk::mods::catalog { +namespace { + +using json = nlohmann::json; +using namespace std::chrono_literals; + +constexpr std::string_view apiUrl = + "https://staging.twilitrealm.workers.dev/api/v1/games/dusklight"; + +std::string_view sort_value(Sort sort) noexcept { + switch (sort) { + case Sort::Featured: + return "featured"; + case Sort::Endorsements: + return "endorsements"; + case Sort::Updated: + return "updated"; + case Sort::Newest: + return "newest"; + case Sort::Name: + return "name"; + case Sort::Downloads: + default: + return "downloads"; + } +} + +std::string_view catalog_platform() noexcept { +#if defined(_WIN32) && defined(_M_ARM64) + return "windows-arm64"; +#elif defined(_WIN32) && defined(_M_X64) + return "windows-amd64"; +#elif defined(__ANDROID__) && defined(__aarch64__) + return "android-aarch64"; +#elif defined(__APPLE__) && TARGET_OS_IOS + return "ios-arm64"; +#elif defined(__APPLE__) && !TARGET_OS_TV && defined(__aarch64__) + return "macos-arm64"; +#elif defined(__APPLE__) && !TARGET_OS_TV && defined(__x86_64__) + return "macos-x86_64"; +#elif defined(__linux__) && defined(__aarch64__) + return "linux-aarch64"; +#elif defined(__linux__) && defined(__x86_64__) + return "linux-x86_64"; +#else + // The catalog rejects platforms outside its published package matrix. + return {}; +#endif +} + +std::string url_encode(std::string_view value) { + constexpr char hex[] = "0123456789ABCDEF"; + std::string encoded; + encoded.reserve(value.size()); + for (const unsigned char c : value) { + const bool unreserved = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || + c == '~'; + if (unreserved) { + encoded.push_back(static_cast(c)); + } else { + encoded.push_back('%'); + encoded.push_back(hex[c >> 4]); + encoded.push_back(hex[c & 0x0f]); + } + } + return encoded; +} + +void append_query(std::string& url, std::string_view name, std::string_view value) { + fmt::format_to(std::back_inserter(url), "{}{}={}", + url.find('?') == std::string::npos ? '?' : '&', name, url_encode(value)); +} + +std::string make_url(const Query& query) { + std::string url{fmt::format("{}/mods", apiUrl)}; + if (!query.search.empty()) { + append_query(url, "q", query.search); + } + if (!query.category.empty()) { + append_query(url, "category", query.category); + } + append_query(url, "sort", sort_value(query.sort)); + append_query(url, "page", fmt::format("{}", std::max(query.page, 1))); + if (query.thisDevice) { + const auto platform = catalog_platform(); + if (!platform.empty()) { + append_query(url, "platform", platform); + } + } + return url; +} + +std::string make_detail_url(std::string_view id) { + return fmt::format("{}/{}", fmt::format("{}/mods", apiUrl), url_encode(id)); +} + +const json& required_field(const json& object, const char* name) { + if (!object.is_object()) { + throw std::runtime_error{"expected an object"}; + } + const auto iter = object.find(name); + if (iter == object.end()) { + throw std::runtime_error{fmt::format("missing field '{}'", name)}; + } + return *iter; +} + +const json& required_array(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (!value.is_array()) { + throw std::runtime_error{fmt::format("field '{}' is not an array", name)}; + } + return value; +} + +std::string required_string(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (!value.is_string()) { + throw std::runtime_error{fmt::format("field '{}' is not a string", name)}; + } + return value.get(); +} + +bool required_bool(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (!value.is_boolean()) { + throw std::runtime_error{fmt::format("field '{}' is not a boolean", name)}; + } + return value.get(); +} + +uint64_t required_count(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (value.is_number_unsigned()) { + return value.get(); + } + if (value.is_number_integer()) { + const auto count = value.get(); + if (count >= 0) { + return static_cast(count); + } + } + throw std::runtime_error{fmt::format("field '{}' is not a non-negative integer", name)}; +} + +int required_int(const json& object, const char* name) { + const uint64_t value = required_count(object, name); + if (value > static_cast(std::numeric_limits::max())) { + throw std::runtime_error{fmt::format("field '{}' is too large", name)}; + } + return static_cast(value); +} + +std::optional optional_string(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (value.is_null()) { + return std::nullopt; + } + if (!value.is_string()) { + throw std::runtime_error{fmt::format("field '{}' is not a string or null", name)}; + } + return value.get(); +} + +uint16_t required_u16(const json& object, const char* name) { + const auto value = required_count(object, name); + if (value > std::numeric_limits::max()) { + throw std::runtime_error{fmt::format("field '{}' is too large", name)}; + } + return static_cast(value); +} + +std::optional optional_u32(const json& object, const char* name) { + if (required_field(object, name).is_null()) { + return std::nullopt; + } + const auto value = required_count(object, name); + if (value > std::numeric_limits::max()) { + throw std::runtime_error{fmt::format("field '{}' is too large", name)}; + } + return static_cast(value); +} + +Download parse_download(const json& value) { + return { + .url = required_string(value, "url"), + .sha256 = required_string(value, "sha256"), + .size = required_count(value, "size"), + }; +} + +std::vector parse_service_imports(const json& object) { + const auto& imports = required_array(object, "service_imports"); + std::vector result; + result.reserve(imports.size()); + for (const auto& service : imports) { + result.push_back({ + .id = required_string(service, "id"), + .major = required_u16(service, "major"), + .minMinor = required_u16(service, "min_minor"), + .optional = required_bool(service, "optional"), + }); + } + return result; +} + +Image parse_image(const json& value) { + const auto width = required_count(value, "width"); + const auto height = required_count(value, "height"); + if (width > std::numeric_limits::max() || + height > std::numeric_limits::max()) + { + throw std::runtime_error{"image dimensions are too large"}; + } + Image image{ + .width = static_cast(width), + .height = static_cast(height), + }; + const auto& sources = required_array(value, "sources"); + image.sources.reserve(sources.size()); + for (const auto& source : sources) { + const auto sourceWidth = required_count(source, "width"); + if (sourceWidth > std::numeric_limits::max()) { + throw std::runtime_error{"image source width is too large"}; + } + image.sources.push_back({ + .width = static_cast(sourceWidth), + .pngUrl = required_string(source, "png_url"), + }); + } + if (image.sources.empty()) { + throw std::runtime_error{"image has no sources"}; + } + return image; +} + +Category parse_category(const json& value) { + return { + .slug = required_string(value, "slug"), + .name = required_string(value, "name"), + .modCount = required_count(value, "mod_count"), + }; +} + +Category parse_mod_category(const json& value) { + return { + .slug = required_string(value, "slug"), + .name = required_string(value, "name"), + }; +} + +Tag parse_tag(const json& value) { + return { + .slug = required_string(value, "slug"), + .name = required_string(value, "name"), + }; +} + +Author parse_author(const json& value) { + return { + .name = required_string(value, "name"), + .handle = required_string(value, "handle"), + .official = required_bool(value, "official"), + }; +} + +Mod parse_mod(const json& value) { + Mod mod{ + .id = required_string(value, "id"), + .name = required_string(value, "name"), + .version = required_string(value, "version"), + .author = parse_author(required_field(value, "author")), + .summary = required_string(value, "summary"), + .downloads = required_count(value, "downloads"), + .endorsements = required_count(value, "endorsements"), + .publishedAt = required_string(value, "published_at"), + .updatedAt = required_string(value, "updated_at"), + .packageSize = required_count(value, "package_size"), + .containsNativeCode = required_bool(value, "contains_native_code"), + }; + + const auto& category = required_field(value, "category"); + if (!category.is_null()) { + mod.category = parse_mod_category(category); + } + + const auto& tags = required_array(value, "tags"); + mod.tags.reserve(tags.size()); + for (const auto& tag : tags) { + mod.tags.push_back(parse_tag(tag)); + } + + const auto& platforms = required_array(value, "supported_platforms"); + mod.supportedPlatforms.reserve(platforms.size()); + for (const auto& platform : platforms) { + if (!platform.is_string()) { + throw std::runtime_error{"supported platform is not a string"}; + } + mod.supportedPlatforms.push_back(platform.get()); + } + + const auto& icon = required_field(value, "icon"); + if (!icon.is_null()) { + mod.icon = parse_image(icon); + } + const auto& banner = required_field(value, "banner"); + if (!banner.is_null()) { + mod.banner = parse_image(banner); + } + return mod; +} + +Detail parse_detail(std::string_view body) { + const json root = json::parse(body); + Detail detail{ + .mod = parse_mod(root), + .siteUrl = required_string(root, "site_url"), + .sourceUrl = optional_string(root, "source_url"), + .license = optional_string(root, "license"), + .descriptionHtml = required_string(root, "description_html"), + .changelogHtml = required_string(root, "changelog_html"), + .download = parse_download(required_field(root, "download")), + .modAbi = optional_u32(root, "mod_abi"), + }; + + const auto& screenshots = required_array(root, "screenshots"); + detail.screenshots.reserve(screenshots.size()); + for (const auto& screenshot : screenshots) { + detail.screenshots.push_back({ + .altText = required_string(screenshot, "alt_text"), + .image = parse_image(required_field(screenshot, "image")), + }); + } + + detail.serviceImports = parse_service_imports(root); + return detail; +} + +Page parse_page(std::string_view body) { + const json root = json::parse(body); + const auto& game = required_field(root, "game"); + if (required_string(game, "id") != "dusklight") { + throw std::runtime_error{"catalog response is for a different game"}; + } + + Page page; + const auto& categories = required_array(root, "categories"); + page.categories.reserve(categories.size()); + for (const auto& category : categories) { + page.categories.push_back(parse_category(category)); + } + + const auto& mods = required_array(root, "mods"); + page.mods.reserve(mods.size()); + for (const auto& mod : mods) { + page.mods.push_back(parse_mod(mod)); + } + + const auto& pagination = required_field(root, "pagination"); + page.pagination = { + .page = required_int(pagination, "page"), + .pageSize = required_int(pagination, "page_size"), + .pageCount = required_int(pagination, "page_count"), + .total = required_count(pagination, "total"), + }; + return page; +} + +std::string api_error(const borealis::http::Response& response) { + try { + const auto body = json::parse(response.body); + const auto& error = required_field(body, "error"); + return required_string(error, "message"); + } catch (...) { + return fmt::format("The catalog returned HTTP {}.", response.statusCode); + } +} + +FetchResult finish_request(borealis::http::Result result) { + if (result.error != borealis::http::Error::None) { + return {.error = result.message.empty() ? "The catalog request failed." : + std::move(result.message)}; + } + if (result.response.statusCode != 200) { + return {.error = api_error(result.response)}; + } + try { + return {.page = parse_page(result.response.body)}; + } catch (const std::exception& exception) { + return {.error = fmt::format("The catalog response was invalid: {}", exception.what())}; + } catch (...) { + return {.error = "The catalog response was invalid."}; + } +} + +DetailFetchResult finish_detail_request(borealis::http::Result result) { + if (result.error != borealis::http::Error::None) { + return {.error = + result.message.empty() ? "The mod request failed." : std::move(result.message)}; + } + if (result.response.statusCode != 200) { + return {.error = api_error(result.response)}; + } + try { + return {.detail = parse_detail(result.response.body)}; + } catch (const std::exception& exception) { + return {.error = fmt::format("The mod response was invalid: {}", exception.what())}; + } catch (...) { + return {.error = "The mod response was invalid."}; + } +} + +borealis::http::Request make_request(std::string url) { + return { + .url = std::move(url), + .headers = + { + {.name = "User-Agent", .value = borealis::user_agent(dusk::AppInfo)}, + {.name = "Accept", .value = "application/json"}, + }, + .connectTimeout = 10s, + .idleTimeout = 10s, + .totalTimeout = 20s, + }; +} + +} // namespace + +borealis::Task fetch_page(Query query) { + return borealis::http::start(make_request(make_url(query))).map(finish_request); +} + +borealis::Task fetch_detail(std::string id) { + return borealis::http::start(make_request(make_detail_url(id))).map(finish_detail_request); +} + +std::string_view platform() noexcept { + return catalog_platform(); +} + +borealis::Task fetch_updates( + UpdateEnvironment environment, std::vector targets) { + json platformValue = environment.platform; + if (environment.platform.empty()) { + platformValue = nullptr; + } + json body{ + {"app_version", BOREALIS_APP_VERSION}, + {"platform", platformValue}, + {"mod_abi", environment.abi}, + {"targets", targets}, + {"services", json::array()}, + {"mods", json::array()}, + }; + for (const auto& service : environment.services) { + json provider = service.providerId; + if (service.providerId.empty()) { + provider = nullptr; + } + body["services"].push_back({ + {"id", service.id}, + {"major", service.major}, + {"minor", service.minor}, + {"provider_mod_id", provider}, + }); + } + for (const auto& mod : environment.mods) { + json imports = json::array(); + for (const auto& service : mod.imports) { + imports.push_back({ + {"id", service.id}, + {"major", service.major}, + {"min_minor", service.minMinor}, + }); + } + body["mods"].push_back({ + {"id", mod.id}, + {"version", mod.version}, + {"enabled", mod.enabled}, + {"required_imports", std::move(imports)}, + }); + } + auto request = make_request(fmt::format("{}/update-check", apiUrl)); + request.method = borealis::http::Method::Post; + request.headers.push_back({"Content-Type", "application/json"}); + request.body = body.dump(); + request.maxBodyBytes = 8 * 1024 * 1024; + return borealis::http::start(std::move(request)) + .map([environment = std::move(environment), targets = std::move(targets)]( + borealis::http::Result result) -> UpdateFetchResult { + if (result.error != borealis::http::Error::None) { + return { + .error = + result.message.empty() ? "Could not check mod updates." : result.message, + .retryable = result.error == borealis::http::Error::Network || + result.error == borealis::http::Error::Timeout, + }; + } + if (result.response.statusCode != 200) { + int retryAfter = 0; + for (const auto& header : result.response.headers) { + if (header.name == "Retry-After" || header.name == "retry-after") { + try { + retryAfter = std::clamp(std::stoi(header.value), 0, 3600); + } catch (...) { + } + } + } + return { + .error = api_error(result.response), + .retryable = + result.response.statusCode == 429 || result.response.statusCode >= 500, + .retryAfter = retryAfter, + }; + } + try { + const auto root = json::parse(result.response.body); + const auto& rows = required_field(root, "mods"); + if (!rows.is_array() || rows.size() != targets.size()) { + throw std::runtime_error{"Incomplete update response"}; + } + std::vector updates; + for (const auto& row : rows) { + ModUpdate update{ + .id = required_string(row, "id"), + .installedVersion = required_string(row, "installed_version"), + .published = required_bool(row, "published"), + .yankedInstalled = required_bool(row, "yanked_installed"), + }; + const auto installed = + std::ranges::find(environment.mods, update.id, &InstalledPackage::id); + if (std::ranges::find(targets, update.id) == targets.end() || + std::ranges::find(updates, update.id, &ModUpdate::id) != updates.end() || + installed == environment.mods.end() || + installed->version != update.installedVersion) + { + throw std::runtime_error{ + "Update response does not match the installed inventory"}; + } + const auto& latest = required_field(row, "latest"); + if (!latest.is_null()) { + update.latestVersion = required_string(latest, "version"); + const auto& blockers = required_field(latest, "blockers"); + if (!blockers.is_array()) { + throw std::runtime_error{"Invalid blockers"}; + } + for (const auto& blocker : blockers) { + update.blockers.push_back(required_string(blocker, "message")); + } + } + const auto& target = required_field(row, "latest_compatible"); + if (!target.is_null()) { + UpdateTarget parsed; + parsed.version = required_string(target, "version"); + parsed.changelogHtml = required_string(target, "changelog_html"); + parsed.download = parse_download(required_field(target, "download")); + auto& compatibility = parsed.compatibility; + compatibility.containsNativeCode = + required_bool(target, "contains_native_code"); + compatibility.platforms = required_field(target, "supported_platforms") + .get>(); + compatibility.abi = optional_u32(target, "mod_abi"); + compatibility.imports = parse_service_imports(target); + const auto& exports = required_array(target, "service_exports"); + for (const auto& service : exports) { + compatibility.exports.push_back({ + required_string(service, "id"), + required_u16(service, "major"), + required_u16(service, "minor"), + {}, + }); + } + if (!parsed.download.url.starts_with("https://") || + parsed.download.size == 0 || parsed.download.sha256.size() != 64 || + !std::ranges::all_of(parsed.download.sha256, + [](char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); + })) + { + throw std::runtime_error{"Invalid update download"}; + } + update.target = std::move(parsed); + } + updates.push_back(std::move(update)); + } + return {.updates = std::move(updates)}; + } catch (const std::exception& error) { + return {.error = fmt::format("Invalid mod update response: {}", error.what())}; + } + }); +} + +} // namespace dusk::mods::catalog diff --git a/src/dusk/mods/catalog.hpp b/src/dusk/mods/catalog.hpp new file mode 100644 index 0000000000..4b90cb58aa --- /dev/null +++ b/src/dusk/mods/catalog.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include "updates.hpp" + +#include + +#include +#include +#include +#include + +namespace dusk::mods::catalog { + +enum class Sort { + Featured, + Downloads, + Endorsements, + Updated, + Newest, + Name, +}; + +struct Query { + std::string search; + std::string category; + Sort sort = Sort::Featured; + int page = 1; + bool thisDevice = true; +}; + +struct Category { + std::string slug; + std::string name; + uint64_t modCount = 0; +}; + +struct Tag { + std::string slug; + std::string name; +}; + +struct Author { + std::string name; + std::string handle; + bool official = false; +}; + +struct ImageSource { + uint32_t width = 0; + std::string pngUrl; +}; + +struct Image { + uint32_t width = 0; + uint32_t height = 0; + std::vector sources; +}; + +struct Mod { + std::string id; + std::string name; + std::string version; + Author author; + std::string summary; + std::optional category; + std::vector tags; + uint64_t downloads = 0; + uint64_t endorsements = 0; + std::string publishedAt; + std::string updatedAt; + uint64_t packageSize = 0; + bool containsNativeCode = false; + std::vector supportedPlatforms; + std::optional icon; + std::optional banner; +}; + +struct Screenshot { + std::string altText; + Image image; +}; + +struct Detail { + Mod mod; + std::string siteUrl; + std::optional sourceUrl; + std::optional license; + std::string descriptionHtml; + std::string changelogHtml; + Download download; + std::optional modAbi; + std::vector screenshots; + std::vector serviceImports; +}; + +struct Pagination { + int page = 1; + int pageSize = 0; + int pageCount = 0; + uint64_t total = 0; +}; + +struct Page { + std::vector categories; + std::vector mods; + Pagination pagination; +}; + +struct FetchResult { + std::optional page; + std::string error; +}; + +struct DetailFetchResult { + std::optional detail; + std::string error; +}; + +struct UpdateFetchResult { + std::optional> updates; + std::string error; + bool retryable = false; + int retryAfter = 0; +}; + +std::string_view platform() noexcept; +borealis::Task fetch_updates( + UpdateEnvironment environment, std::vector targets); + +/** Fetches one filtered page from the Dusklight catalog. */ +borealis::Task fetch_page(Query query); + +/** Fetches the full catalog record for one mod. */ +borealis::Task fetch_detail(std::string id); + +} // namespace dusk::mods::catalog diff --git a/src/dusk/mods/loader/bundle_disk.cpp b/src/dusk/mods/loader/bundle_disk.cpp index c9903b9b38..a94044e596 100644 --- a/src/dusk/mods/loader/bundle_disk.cpp +++ b/src/dusk/mods/loader/bundle_disk.cpp @@ -58,4 +58,12 @@ std::filesystem::path ModBundleDisk::toRealPath(const std::string& fileName) con return root_path / filePath; } +bool ModBundleDisk::file_exists(const std::string& fileName) { + return std::filesystem::is_regular_file(toRealPath(fileName)); +} + +bool ModBundleDisk::directory_exists(const std::string& fileName) { + return std::filesystem::is_directory(toRealPath(fileName)); +} + } // namespace dusk::mods diff --git a/src/dusk/mods/loader/bundle_zip.cpp b/src/dusk/mods/loader/bundle_zip.cpp index 6075dd5dc1..6c3e798952 100644 --- a/src/dusk/mods/loader/bundle_zip.cpp +++ b/src/dusk/mods/loader/bundle_zip.cpp @@ -1,69 +1,33 @@ #include "loader.hpp" -#include - -#include +#include namespace dusk::mods { -ModBundleZip::ModBundleZip(std::vector&& data) : zip_data(std::move(data)) { - if (!mz_zip_reader_init_mem(&res_zip, zip_data.data(), zip_data.size(), 0)) { - const auto error = mz_zip_get_last_error(&res_zip); - throw std::runtime_error( - fmt::format("Opening zip failed: {}", mz_zip_get_error_string(error))); +ModBundleZip::ModBundleZip(const std::filesystem::path& path) : m_archive{path} { + if (m_archive.package_format() != archive::PackageFormat::Mod) { + throw std::runtime_error("Archive is not a valid mod package"); } } -ModBundleZip::~ModBundleZip() { - mz_zip_reader_end(&res_zip); -} - std::vector ModBundleZip::readFile(const std::string& fileName) { - std::lock_guard lock{m_mutex}; - size_t size; - const auto ptr = mz_zip_reader_extract_file_to_heap(&res_zip, fileName.c_str(), &size, 0); - - if (!ptr) { - throw std::runtime_error(fmt::format("File does not exist: {}", fileName)); - } - - std::span data(static_cast(ptr), size); - std::vector vec(data.begin(), data.end()); - - mz_free(ptr); - - return vec; + return m_archive.read_file(fileName); } std::vector ModBundleZip::getFileNames() { - std::lock_guard lock{m_mutex}; - std::vector results; - - for (mz_uint i = 0, n = mz_zip_reader_get_num_files(&res_zip); i < n; ++i) { - mz_zip_archive_file_stat stat{}; - if (!mz_zip_reader_file_stat(&res_zip, i, &stat)) { - continue; - } - if (mz_zip_reader_is_file_a_directory(&res_zip, i)) { - continue; - } - - results.emplace_back(stat.m_filename); - } - - return results; + return m_archive.file_names(); } size_t ModBundleZip::getFileSize(const std::string& fileName) { - std::lock_guard lock{m_mutex}; - const auto idx = mz_zip_reader_locate_file(&res_zip, fileName.c_str(), nullptr, 0); - if (idx < 0) { - throw std::runtime_error(fmt::format("Unable to locate file in zip: {}", fileName)); - } + return m_archive.file_size(fileName); +} - mz_zip_archive_file_stat stat{}; - mz_zip_reader_file_stat(&res_zip, idx, &stat); - return stat.m_uncomp_size; +bool ModBundleZip::file_exists(const std::string& fileName) { + return m_archive.file_exists(fileName); +} + +bool ModBundleZip::directory_exists(const std::string& fileName) { + return m_archive.directory_exists(fileName); } } // namespace dusk::mods diff --git a/src/dusk/mods/loader/code_patch_macos.cpp b/src/dusk/mods/loader/code_patch_macos.cpp new file mode 100644 index 0000000000..64a7b4a606 --- /dev/null +++ b/src/dusk/mods/loader/code_patch_macos.cpp @@ -0,0 +1,223 @@ +#include "code_patch_macos.hpp" + +#include +#include +#include +#include +#include +#include + +#define PATCH_CODE \ + __attribute__((section("__TEXT,__code_patch,regular,pure_instructions"), noinline)) + +extern const char kPatchBegin[] asm("section$start$__TEXT$__code_patch"); +extern const char kPatchEnd[] asm("section$end$__TEXT$__code_patch"); + +namespace { + +constexpr unsigned kMaxThreads = 1024; +constexpr unsigned kMaxAttempts = 16; +constexpr size_t kMaxPatchSize = 16; +pthread_mutex_t sPatchMutex = PTHREAD_MUTEX_INITIALIZER; + +struct ThreadList { + thread_act_array_t threads = nullptr; + mach_msg_type_number_t count = 0; +}; + +PATCH_CODE void release_threads(ThreadList& list) { + for (unsigned i = 0; i < list.count; ++i) { + mach_port_deallocate(mach_task_self(), list.threads[i]); + } + if (list.threads != nullptr) { + vm_deallocate(mach_task_self(), reinterpret_cast(list.threads), + list.count * sizeof(thread_t)); + } +} + +PATCH_CODE kern_return_t read_pc(thread_t thread, uintptr_t& pc) { +#if defined(__aarch64__) + arm_thread_state64_t state{}; + mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT; + const auto result = thread_get_state( + thread, ARM_THREAD_STATE64, reinterpret_cast(&state), &count); + pc = arm_thread_state64_get_pc(state); +#elif defined(__x86_64__) + x86_thread_state64_t state{}; + mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT; + const auto result = thread_get_state( + thread, x86_THREAD_STATE64, reinterpret_cast(&state), &count); + pc = state.__rip; +#else +#error Unsupported macOS architecture +#endif + return result; +} + +PATCH_CODE __attribute__((aligned(16384))) kern_return_t commit_patch(uintptr_t target, + const unsigned char* expected, const unsigned char* replacement, size_t size, uintptr_t page, + size_t pageSize, thread_t currentThread) { + ThreadList initial; + auto result = task_threads(mach_task_self(), &initial.threads, &initial.count); + if (result != KERN_SUCCESS) { + return result; + } + thread_t suspended[kMaxThreads]; + unsigned suspendedCount = 0; + if (initial.count > kMaxThreads) { + release_threads(initial); + return KERN_RESOURCE_SHORTAGE; + } + + for (unsigned i = 0; i < initial.count; ++i) { + const auto thread = initial.threads[i]; + if (thread == currentThread) { + continue; + } + result = thread_suspend(thread); + if (result != KERN_SUCCESS) { + break; + } + suspended[suspendedCount++] = thread; + uintptr_t pc = 0; + result = read_pc(thread, pc); + if (result != KERN_SUCCESS) { + break; + } + if (pc >= target && pc < target + size) { + result = KERN_ABORTED; + break; + } + } + + if (result == KERN_SUCCESS) { + ThreadList current; + result = task_threads(mach_task_self(), ¤t.threads, ¤t.count); + if (result == KERN_SUCCESS) { + for (unsigned i = 0; i < current.count; ++i) { + bool known = false; + for (unsigned j = 0; j < initial.count; ++j) { + known |= current.threads[i] == initial.threads[j]; + } + if (!known) { + result = KERN_ABORTED; + break; + } + } + } + release_threads(current); + } + + if (result == KERN_SUCCESS) { + const auto* bytes = reinterpret_cast(target); + for (size_t i = 0; i < size; ++i) { + if (bytes[i] != expected[i]) { + result = KERN_INVALID_VALUE; + break; + } + } + } + + if (result == KERN_SUCCESS) { + result = mach_vm_protect( + mach_task_self(), page, pageSize, false, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY); + if (result == KERN_SUCCESS) { + auto* bytes = reinterpret_cast(target); + for (size_t i = 0; i < size; ++i) { + bytes[i] = replacement[i]; + } + sys_icache_invalidate(reinterpret_cast(target), size); + result = mach_vm_protect( + mach_task_self(), page, pageSize, false, VM_PROT_READ | VM_PROT_EXECUTE); + if (result != KERN_SUCCESS) { + for (size_t i = 0; i < size; ++i) { + bytes[i] = expected[i]; + } + sys_icache_invalidate(reinterpret_cast(target), size); + if (mach_vm_protect(mach_task_self(), page, pageSize, false, + VM_PROT_READ | VM_PROT_EXECUTE) != KERN_SUCCESS) + { + __builtin_trap(); // Can't resume into non-executable code + } + } + } + } + + for (unsigned i = 0; i < suspendedCount; ++i) { + if (thread_resume(suspended[i]) != KERN_SUCCESS) { + __builtin_trap(); + } + } + release_threads(initial); + return result; +} + +} // namespace + +extern "C" int commit_code_patch( + void* targetPointer, const void* expected, const void* replacement, size_t size) { + if (targetPointer == nullptr || expected == nullptr || replacement == nullptr || size == 0 || + size > kMaxPatchSize) + { + return KERN_INVALID_ARGUMENT; + } + const auto target = reinterpret_cast(targetPointer); + const size_t pageSize = vm_page_size; + if (target > UINTPTR_MAX - size - pageSize) { + return KERN_INVALID_ADDRESS; + } + const auto page = target & ~(pageSize - 1); + const size_t length = ((target + size + pageSize - 1) & ~(pageSize - 1)) - page; + if (page < reinterpret_cast(kPatchEnd) && + page + length > reinterpret_cast(kPatchBegin)) + { + return KERN_PROTECTION_FAILURE; + } + + unsigned char oldCode[kMaxPatchSize]; + unsigned char newCode[kMaxPatchSize]; + for (size_t i = 0; i < size; ++i) { + oldCode[i] = static_cast(expected)[i]; + newCode[i] = static_cast(replacement)[i]; + } + + pthread_mutex_lock(&sPatchMutex); + mach_vm_address_t region = page; + mach_vm_size_t regionSize = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + auto result = mach_vm_region(mach_task_self(), ®ion, ®ionSize, VM_REGION_BASIC_INFO_64, + reinterpret_cast(&info), &count, &object); + if (object != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), object); + } + if (result == KERN_SUCCESS && (region > page || regionSize < page + length - region || + info.protection != (VM_PROT_READ | VM_PROT_EXECUTE))) + { + result = KERN_PROTECTION_FAILURE; + } + if (result == KERN_SUCCESS) { + const auto currentThread = mach_thread_self(); + uintptr_t pc = 0; + read_pc(currentThread, pc); + thread_suspend(MACH_PORT_NULL); + thread_resume(MACH_PORT_NULL); + vm_deallocate(mach_task_self(), 0, 0); + mach_port_deallocate(mach_task_self(), MACH_PORT_NULL); + sys_icache_invalidate(targetPointer, size); + result = mach_vm_protect(mach_task_self(), page, length, false, info.protection); + if (result == KERN_SUCCESS) { + for (unsigned attempt = 0; attempt < kMaxAttempts; ++attempt) { + result = commit_patch(target, oldCode, newCode, size, page, length, currentThread); + if (result != KERN_ABORTED || attempt + 1 == kMaxAttempts) { + break; + } + usleep(1000); + } + } + mach_port_deallocate(mach_task_self(), currentThread); + } + pthread_mutex_unlock(&sPatchMutex); + return result; +} diff --git a/src/dusk/mods/loader/code_patch_macos.hpp b/src/dusk/mods/loader/code_patch_macos.hpp new file mode 100644 index 0000000000..540e806404 --- /dev/null +++ b/src/dusk/mods/loader/code_patch_macos.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int commit_code_patch(void* target, const void* expected, const void* replacement, size_t size); + +#ifdef __cplusplus +} +#endif diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index 71b17b7cff..6479985f84 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -1,536 +1,81 @@ #include "loader.hpp" -#include "../manifest.hpp" #include "depgraph.hpp" +#include "manifest.hpp" #include "native_module.hpp" +#include "natives.hpp" +#include "packages.hpp" #if DUSK_HAS_PREPATCH #include "prepatch.hpp" #endif #include "dusk/config.hpp" #include "dusk/data.hpp" -#include "dusk/io.hpp" #include "dusk/logging.h" #include "dusk/mod_loader.hpp" #include "dusk/mods/log_buffer.hpp" +#include "dusk/mods/manifest.hpp" +#include "dusk/mods/path.hpp" +#include "dusk/mods/queue.hpp" #include "dusk/mods/svc/config.hpp" #include "dusk/mods/svc/hook.hpp" #include "dusk/mods/svc/registry.hpp" +#include "dusk/mods/updates.hpp" +#include "dusk/ui/mod_texture_provider.hpp" #include "dusk/ui/mods_window.hpp" #include "dusk/ui/ui.hpp" #include -#include -#include +#include +#include #include -#include #include #include -#include #include #include #include #include -using namespace std::string_literals; -using namespace std::string_view_literals; - -#if defined(_WIN32) -#if defined(_M_ARM64) -static constexpr std::string_view k_nativePlatform = "windows-arm64"sv; -#elif defined(_M_X64) -static constexpr std::string_view k_nativePlatform = "windows-amd64"sv; -#elif defined(_M_IX86) -static constexpr std::string_view k_nativePlatform = "windows-x86"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -#endif -static constexpr std::string_view k_nativeLibName = "mod.dll"sv; -#elif defined(__ANDROID__) -#if defined(__aarch64__) -static constexpr std::string_view k_nativePlatform = "android-aarch64"sv; -#elif defined(__x86_64__) -static constexpr std::string_view k_nativePlatform = "android-x86_64"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -#endif -static constexpr std::string_view k_nativeLibName = "mod.so"sv; -#elif defined(__APPLE__) -#include -#if TARGET_OS_IOS -static constexpr std::string_view k_nativePlatform = "ios-arm64"sv; -#elif TARGET_OS_TV -static constexpr std::string_view k_nativePlatform = "tvos-arm64"sv; -#elif defined(__aarch64__) -static constexpr std::string_view k_nativePlatform = "macos-arm64"sv; -#elif defined(__x86_64__) -static constexpr std::string_view k_nativePlatform = "macos-x86_64"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -#endif -static constexpr std::string_view k_nativeLibName = "mod.so"sv; -#elif defined(__linux__) -#if defined(__aarch64__) -static constexpr std::string_view k_nativePlatform = "linux-aarch64"sv; -#elif defined(__x86_64__) -static constexpr std::string_view k_nativePlatform = "linux-x86_64"sv; -#elif defined(__i386__) -static constexpr std::string_view k_nativePlatform = "linux-x86"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -#endif -static constexpr std::string_view k_nativeLibName = "mod.so"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -static constexpr std::string_view k_nativeLibName = ""sv; -#endif +namespace fs = std::filesystem; namespace dusk::mods { namespace { constexpr borealis::Log Log{"dusk::mods::loader"}; ModLoader g_modLoader; -constexpr std::string_view k_nativeLibDir = "lib/"sv; -class DirectoryRollback { -public: - ~DirectoryRollback() { - if (!mPath.empty()) { - std::error_code ec; - std::filesystem::remove_all(mPath, ec); - } - } - - void set_path(std::filesystem::path path) { mPath = std::move(path); } - void release() { mPath.clear(); } - -private: - std::filesystem::path mPath; -}; - -std::unique_ptr load_bundle(const std::filesystem::path& modPath, bool fromDir) { - if (fromDir) { - return std::make_unique(modPath); - } else { - std::vector data = io::FileStream::ReadAllBytes(modPath); - return std::make_unique(std::move(data)); +void complete_operation(const std::shared_ptr& operation, const bool success = true, + std::string message = {}) { + if (operation == nullptr) { + return; } + operation->state = success ? ModOperation::State::Succeeded : ModOperation::State::Failed; + operation->message = std::move(message); } -struct NativeRuntimeLocation { - std::string entry; - std::vector runtimeEntries; - bool anyLibs = false; -}; - -struct NativeLocateFailure { - NativeModStatus status; - std::string logMessage; -}; - -using NativeLocateResult = std::variant; - -bool has_native_library_extension(std::string_view name) { - const auto endsWith = [name](std::string_view extension) { - if (name.size() < extension.size()) { - return false; - } - const auto suffix = name.substr(name.size() - extension.size()); - return std::ranges::equal(suffix, extension, [](char lhs, char rhs) { - const auto lower = [](char value) { - return value >= 'A' && value <= 'Z' ? static_cast(value + ('a' - 'A')) : - value; - }; - return lower(lhs) == lower(rhs); - }); - }; - return endsWith(".dll"sv) || endsWith(".so"sv) || endsWith(".dylib"sv); -} - -NativeLocateResult locate_native_runtime(ModBundle& bundle) { - NativeRuntimeLocation result; - const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); - const std::string nativeEntry = platformPrefix + std::string{k_nativeLibName}; - for (const auto& name : bundle.getFileNames()) { - if (name.find('/') == std::string::npos && has_native_library_extension(name)) { - return NativeLocateFailure{ - NativeModStatus::InvalidBundle, - fmt::format( - "native library '{}' found at the root (natives go in /lib/{{platform}})", - name), - }; - } - if (!name.starts_with(k_nativeLibDir)) { - continue; - } - - const std::string_view libPath{ - name.data() + k_nativeLibDir.size(), name.size() - k_nativeLibDir.size()}; - const auto platformEnd = libPath.find('/'); - if (platformEnd != std::string_view::npos) { - const auto entryName = libPath.substr(platformEnd + 1); - if (entryName.find('/') == std::string_view::npos && - (entryName == "mod.dll"sv || entryName == "mod.so"sv)) - { - result.anyLibs = true; - } - } - - if (!k_nativePlatform.empty() && name.starts_with(platformPrefix)) { - const std::string_view relativeName{ - name.data() + platformPrefix.size(), name.size() - platformPrefix.size()}; - if (!is_safe_resource_path(relativeName)) { - continue; - } - result.runtimeEntries.push_back(name); - } - if (name == nativeEntry) { - result.entry = name; - } +LoadedMod::FileIdentity file_identity(const fs::path& path) { + std::error_code error; + const bool isDirectory = fs::is_directory(path, error); + if (error) { + return {}; } - std::ranges::sort(result.runtimeEntries); - result.runtimeEntries.erase( - std::unique(result.runtimeEntries.begin(), result.runtimeEntries.end()), - result.runtimeEntries.end()); - return result; + const auto size = isDirectory ? 0 : fs::file_size(path, error); + if (error) { + return {}; + } + const auto modified = fs::last_write_time(path, error); + if (error) { + return {}; + } + return {.size = size, .modified = modified, .valid = true}; } + } // namespace ModLoader& ModLoader::instance() { return g_modLoader; } -class InvalidModDataException : public std::runtime_error { -public: - explicit InvalidModDataException(const std::string& msg) : runtime_error(msg) {} - explicit InvalidModDataException(const char* msg) : runtime_error(msg) {} -}; - -static void validate_mod_id(std::string_view const str) { - if (str.empty()) { - throw InvalidModDataException("Missing ID value in mod metadata!"); - } - - bool lastWasPeriod = false; - for (auto const chr : str) { - if (chr == '.') { - if (lastWasPeriod) { - throw InvalidModDataException("Cannot have two consecutive periods in mod ID!"); - } - lastWasPeriod = true; - continue; - } - - lastWasPeriod = false; - - if (chr == '_') - continue; - - if (chr >= '0' && chr <= '9') - continue; - - if (chr >= 'a' && chr <= 'z') - continue; - - if (chr >= 'A' && chr <= 'Z') - continue; - - throw InvalidModDataException( - fmt::format("Invalid character '{}' in mod ID. Valid characters are period, " - "underscore, and alphanumerics.", - chr)); - } -} - -static bool bundle_has_file(ModBundle& bundle, const std::string& path) { - try { - bundle.getFileSize(path); - return true; - } catch (const std::runtime_error&) { - return false; - } -} - -static std::string resolve_image_path(ModBundle& bundle, const std::string& modId, - std::string_view key, const std::string& manifestPath, const std::string& defaultPath) { - if (!manifestPath.empty()) { - if (!is_safe_resource_path(manifestPath)) { - log::write( - modId, LOG_LEVEL_WARN, "invalid {} path '{}' in mod.json", key, manifestPath); - } else if (!bundle_has_file(bundle, manifestPath)) { - log::write( - modId, LOG_LEVEL_WARN, "{} path '{}' not found in bundle", key, manifestPath); - } else { - return manifestPath; - } - } - if (bundle_has_file(bundle, defaultPath)) { - return defaultPath; - } - return {}; -} - -struct LoadedManifest { - ModMetadata metadata; - std::optional runtime; -}; - -static uint16_t parse_runtime_version_component(std::string_view text, std::string_view fieldName) { - uint32_t value = 0; - const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); - if (text.empty() || error != std::errc{} || end != text.data() + text.size() || - value > UINT16_MAX) - { - throw InvalidModDataException(fmt::format("Invalid {} in runtime version pin", fieldName)); - } - return static_cast(value); -} - -static std::optional parse_runtime(const nlohmann::json& manifest) { - const auto field = manifest.find("runtime"); - if (field == manifest.end()) { - return std::nullopt; - } - if (!field->is_string()) { - throw InvalidModDataException("runtime must be a string"); - } - - const std::string pin = field->get(); - const auto at = pin.rfind('@'); - if (at == std::string::npos || at == 0 || at + 1 == pin.size() || pin.find('@') != at || - at >= MOD_META_SERVICE_ID_SIZE) - { - throw InvalidModDataException( - "runtime must be a service id followed by @major or @major.minor"); - } - - const std::string_view version{pin.data() + at + 1, pin.size() - at - 1}; - const auto dot = version.find('.'); - if (dot != std::string_view::npos && version.find('.', dot + 1) != std::string_view::npos) { - throw InvalidModDataException("runtime version pin has too many components"); - } - - DelegatedModRuntime result; - result.id = pin.substr(0, at); - result.major = parse_runtime_version_component( - dot == std::string_view::npos ? version : version.substr(0, dot), "major version"); - if (dot != std::string_view::npos) { - result.minMinor = parse_runtime_version_component(version.substr(dot + 1), "minor version"); - } - return result; -} - -static LoadedManifest load_manifest(const std::filesystem::path& modPath, ModBundle& bundle) { - const auto metaJson = bundle.readFile("mod.json"); - auto j = nlohmann::json::parse(metaJson); - - std::string metaId = j.value("id", ""); - std::string metaName = j.value("name", ""); - std::string metaVersion = j.value("version", ""); - std::string metaAuthor = j.value("author", ""); - std::string metaDescription = j.value("description", ""); - std::string metaIcon = j.value("icon", ""); - std::string metaBanner = j.value("banner", ""); - - validate_mod_id(metaId); - - if (metaName.empty()) { - metaName = borealis::io::fs_path_to_string(modPath.stem()); - } - if (metaVersion.empty()) { - metaVersion = "?"s; - } - if (metaAuthor.empty()) { - metaAuthor = "unknown"s; - } - - std::string iconPath = resolve_image_path(bundle, metaId, "icon", metaIcon, "res/icon.png"s); - std::string bannerPath = - resolve_image_path(bundle, metaId, "banner", metaBanner, "res/banner.png"s); - - return LoadedManifest{ - .metadata = - { - std::move(metaId), - std::move(metaName), - std::move(metaVersion), - std::move(metaAuthor), - std::move(metaDescription), - std::move(iconPath), - std::move(bannerPath), - }, - .runtime = parse_runtime(j), - }; -} - -// True if the first `capacity` bytes of `str` contain a NUL. -static bool terminated_within(const char* str, size_t capacity) { - return std::memchr(str, '\0', capacity) != nullptr; -} - -static bool parse_meta(NativeMod& native, LoadedMod& mod) { - const ModMeta* meta = native.meta; - if (meta->struct_size < sizeof(ModMeta)) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta descriptor has invalid size {}", - meta->struct_size); - mod.nativeStatus = NativeModStatus::InvalidMetadata; - return false; - } - const auto* cursor = static_cast(meta->records_begin); - const auto* end = static_cast(meta->records_end); - if (cursor == nullptr || end == nullptr || cursor > end || - (reinterpret_cast(cursor) & 7) != 0) - { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta section bounds are invalid"); - mod.nativeStatus = NativeModStatus::InvalidMetadata; - return false; - } - - ModMetaParsed parsed; - size_t headerCount = 0; - const auto invalid = [&](std::string_view why) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "invalid metadata record at offset {}: {}", - cursor - static_cast(meta->records_begin), why); - mod.nativeStatus = NativeModStatus::InvalidMetadata; - return false; - }; - - while (cursor < end) { - if (end - cursor < 8) { - return invalid("trailing bytes"); - } - uint64_t first = 0; - std::memcpy(&first, cursor, sizeof(first)); - if (first == 0) { // linker padding / bounds sentinel - cursor += 8; - continue; - } - - const auto* rec = reinterpret_cast(cursor); - const size_t size = rec->size; - if (size < 8 || size % 8 != 0 || size > static_cast(end - cursor)) { - return invalid("bad record size"); - } - - switch (rec->kind) { - case MOD_META_PAD: - break; - case MOD_META_HEADER: { - if (size < sizeof(ModMetaHeader)) { - return invalid("truncated header record"); - } - const auto* header = reinterpret_cast(rec); - ++headerCount; - parsed.abiVersion = header->abi_version; - break; - } - case MOD_META_IMPORT: { - if (size < sizeof(ModMetaImport)) { - return invalid("truncated import record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { - return invalid("unterminated import service id"); - } - parsed.imports.push_back(record); - break; - } - case MOD_META_EXPORT: { - if (size < sizeof(ModMetaExport)) { - return invalid("truncated export record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { - return invalid("unterminated export service id"); - } - parsed.exports.push_back(record); - break; - } - case MOD_META_HOOK_FN: { - if (size < sizeof(ModMetaHookFn)) { - return invalid("truncated hook record"); - } - parsed.hookFns.push_back( - reinterpret_cast(const_cast(cursor))); - break; - } - case MOD_META_HOOK_MEM: { - if (size <= sizeof(ModMetaHookMem)) { - return invalid("truncated hook record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMem); - const size_t capacity = size - sizeof(ModMetaHookMem); - if (!terminated_within(strings, capacity)) { - return invalid("unterminated hook vtable symbol"); - } - const size_t vtableLen = std::char_traits::length(strings); - if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { - return invalid("unterminated hook display name"); - } - parsed.hookMems.push_back(record); - break; - } - case MOD_META_HOOK_MEM_EXT: { - if (size <= sizeof(ModMetaHookMemExt)) { - return invalid("truncated extended hook record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - if (record->pmf_size <= MOD_META_HOOK_MEM_CAPACITY || - record->pmf_size > MOD_META_HOOK_MEM_EXT_CAPACITY || record->materialize == nullptr) - { - return invalid("bad extended hook member-pointer size"); - } - const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMemExt); - const size_t capacity = size - sizeof(ModMetaHookMemExt); - if (!terminated_within(strings, capacity)) { - return invalid("unterminated extended hook vtable symbol"); - } - const size_t vtableLen = std::char_traits::length(strings); - if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { - return invalid("unterminated extended hook display name"); - } - parsed.hookMemExts.push_back(record); - break; - } - case MOD_META_HOOK_NAME: { - if (size <= sizeof(ModMetaHookName)) { - return invalid("truncated hook record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - const char* name = reinterpret_cast(cursor) + sizeof(ModMetaHookName); - if (!terminated_within(name, size - sizeof(ModMetaHookName))) { - return invalid("unterminated hook symbol name"); - } - parsed.hookNames.push_back(record); - break; - } - default: - // Additive record kinds may appear within a format version; skip them. - log::write(mod.metadata.id, LOG_LEVEL_DEBUG, "skipping unknown metadata record kind {}", - rec->kind); - break; - } - cursor += size; - } - - if (headerCount != 1) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expected 1 metadata header record, found {}", - headerCount); - mod.nativeStatus = NativeModStatus::InvalidMetadata; - return false; - } - if (parsed.abiVersion != MOD_ABI_VERSION) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", - parsed.abiVersion, MOD_ABI_VERSION); - mod.nativeStatus = NativeModStatus::ApiVersionMismatch; - return false; - } - - native.parsed = std::move(parsed); - return true; -} - static std::string lifecycle_error_message( const char* fnName, const ModResult result, const ModError& error) { if (error.message[0] != '\0') { @@ -539,269 +84,6 @@ static std::string lifecycle_error_message( return fmt::format("{} failed with result {}", fnName, static_cast(result)); } -static std::string native_status_message(const NativeModStatus status) { - switch (status) { - case NativeModStatus::BuildDisabled: - return "Code mods are disabled on this Dusklight build"; - case NativeModStatus::ModMissingPlatform: - return fmt::format("Mod not supported on this platform ({})", k_nativePlatform); - case NativeModStatus::ApiVersionMismatch: - // TODO: differentiate whether mod or Dusklight is out of date - return "Mod ABI version mismatch"; - case NativeModStatus::MissingExport: - return "Missing required mod API exports"; - case NativeModStatus::InvalidMetadata: - return "Invalid mod metadata records"; - case NativeModStatus::InvalidBundle: - return "Invalid mod bundle layout (old mod?)"; - case NativeModStatus::Unknown: - return "Unknown mod load failure"; - case NativeModStatus::None: - case NativeModStatus::Loaded: - break; - } - return "native mod failed to load"; -} - -std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod) const { - namespace fs = std::filesystem; - if (k_nativeLibName.empty()) { - return {}; - } - const auto& libDir = m_searchDirs[mod.searchDirIndex].nativeLibDir; - if (libDir.empty()) { - return {}; - } - fs::path path = libDir / fs::path(mod.metadata.id + borealis::io::fs_path_to_string( - fs::path(k_nativeLibName).extension())); - std::error_code ec; - if (!fs::is_regular_file(path, ec)) { - return {}; - } - return path; -} - -void ModLoader::load_native( - LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries) { - if (!EnableCodeMods) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build"); - mod.nativeStatus = NativeModStatus::BuildDisabled; - return; - } - - namespace fs = std::filesystem; - - const fs::path cacheDir = m_cacheDir / mod.metadata.id; - const fs::path scratchDir = cacheDir / "data"; - std::error_code ec; - fs::create_directories(scratchDir, ec); - if (ec) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create mod directory {}: {}", - data::abbreviated_path_string(scratchDir), ec.message()); - return; - } - mod.dir = fs::absolute(scratchDir); - mod.dirUtf8 = borealis::io::fs_path_to_string(mod.dir); - - fs::path libPath; - fs::path runtimeDir; - DirectoryRollback runtimeDirRollback; - if (mod.inPlace) { - if (!dllEntry.empty()) { - libPath = mod.modPath / dllEntry; - } else if (auto external = external_native_lib_path(mod); !external.empty()) { - libPath = std::move(external); - } else { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "no native library named {} found; skipping", k_nativeLibName); - mod.nativeStatus = NativeModStatus::ModMissingPlatform; - return; - } - runtimeDir = libPath.parent_path(); - } else { - if (dllEntry.empty()) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "no native library named {} found; skipping", k_nativeLibName); - mod.nativeStatus = NativeModStatus::ModMissingPlatform; - return; - } - - // Every generation gets a new directory. The main module and all of its runtime - // libraries therefore have fresh paths and can coexist with a previous generation - // that is still unwinding after a reload. - runtimeDir = cacheDir / fmt::format("g{}", ++mod.cacheGeneration); - runtimeDirRollback.set_path(runtimeDir); - fs::create_directories(runtimeDir, ec); - if (ec) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "failed to create native runtime directory {}: {}", - data::abbreviated_path_string(runtimeDir), ec.message()); - return; - } - - const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); - for (const auto& entry : runtimeEntries) { - if (!entry.starts_with(platformPrefix)) { - continue; - } - const std::string_view relativeName{ - entry.data() + platformPrefix.size(), entry.size() - platformPrefix.size()}; - if (!is_safe_resource_path(relativeName)) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "unsafe native runtime path '{}'; skipping", entry); - return; - } - - const fs::path outputPath = runtimeDir / fs::path{relativeName}; - fs::create_directories(outputPath.parent_path(), ec); - if (ec) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "failed to create directory for {}: {}", entry, ec.message()); - return; - } - - std::vector data; - try { - data = mod.bundle->readFile(entry); - } catch (const std::exception& e) { - log::write( - mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}: {}", entry, e.what()); - return; - } - - std::ofstream out(outputPath, std::ios::binary | std::ios::out); - if (!out) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); - return; - } - out.write(reinterpret_cast(data.data()), - static_cast(data.size())); - if (!out) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); - return; - } - } - - libPath = runtimeDir / fs::path{dllEntry}.filename(); - } - - auto nativeMod = std::make_unique(); - try { - nativeMod->handle = std::make_unique(libPath); - } catch (const std::runtime_error& e) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to open {}: {}", - data::abbreviated_path_string(libPath), e.what()); - return; - } - - nativeMod->meta = nativeMod->handle->LookupSymbol("mod_meta"); - nativeMod->contextSymbol = nativeMod->handle->LookupSymbol("mod_ctx"); - nativeMod->fn_initialize = nativeMod->handle->LookupSymbol("mod_initialize"); - nativeMod->fn_update = nativeMod->handle->LookupSymbol("mod_update"); - nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol("mod_shutdown"); - - if (!nativeMod->meta || !nativeMod->contextSymbol || !nativeMod->fn_initialize || - !nativeMod->fn_update || !nativeMod->fn_shutdown) - { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "{} missing required mod API exports; skipping", - data::abbreviated_path_string(libPath)); - mod.nativeStatus = NativeModStatus::MissingExport; - return; - } - - if (!parse_meta(*nativeMod, mod)) { - return; - } - - if (nativeMod->contextSymbol == nullptr) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); - mod.nativeStatus = NativeModStatus::MissingExport; - return; - } - *nativeMod->contextSymbol = mod.context.get(); - - mod.nativePath = fs::absolute(libPath); - mod.nativeDir = fs::absolute(runtimeDir); - mod.nativeDirUtf8 = borealis::io::fs_path_to_string(mod.nativeDir); - mod.native = std::move(nativeMod); - mod.nativeStatus = NativeModStatus::Loaded; - runtimeDirRollback.release(); -} - -bool ModLoader::load_native_if_present(LoadedMod& mod) { - const auto result = locate_native_runtime(*mod.bundle); - if (const auto* failure = std::get_if(&result)) { - mod.nativeStatus = failure->status; - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{}", failure->logMessage); - fail_mod(mod, MOD_ERROR, native_status_message(failure->status)); - return false; - } - - const auto& native = std::get(result); - if (mod.runtime.has_value() && - (native.anyLibs || (mod.inPlace && !external_native_lib_path(mod).empty()))) - { - mod.nativeStatus = NativeModStatus::InvalidBundle; - fail_mod(mod, MOD_CONFLICT, "A mod cannot declare both runtime and native code"); - return false; - } - if (!native.anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) { - mod.nativeStatus = NativeModStatus::None; - return true; - } - - mod.nativeStatus = NativeModStatus::Unknown; - load_native(mod, native.entry, native.runtimeEntries); - if (mod.nativeStatus != NativeModStatus::Loaded) { - fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); - return false; - } - return true; -} - -void ModLoader::unload_native(LoadedMod& mod) { - if (!mod.native || mod.inPlace) { - return; - } - // Deferred dlclose: this mod's code may still be on the stack below the current tick - m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativeDir)}); - mod.nativePath.clear(); - mod.nativeDir.clear(); - mod.nativeDirUtf8.clear(); -} - -void ModLoader::drain_retired_natives() { - for (auto& retired : m_retiredNatives) { - retired.native.reset(); - if (!retired.directory.empty()) { - std::error_code ec; - std::filesystem::remove_all(retired.directory, ec); - } - } - m_retiredNatives.clear(); -} - -static ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) { - ModManifestInfo info; - info.imports.reserve(parsed.imports.size()); - for (const auto* record : parsed.imports) { - if (!svc::valid_service_id(record->service_id.chars)) { - continue; - } - info.imports.push_back({record->service_id.chars, record->major_version, - record->min_minor_version, (record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0}); - } - info.exports.reserve(parsed.exports.size()); - for (const auto* record : parsed.exports) { - if (!svc::valid_service_id(record->service_id.chars)) { - continue; - } - info.exports.push_back({record->service_id.chars, record->major_version}); - } - return info; -} - std::string escape_mod_id_for_config(std::string_view const id) { std::string buf; @@ -851,16 +133,16 @@ static void warn_unpublished_deferred_exports(const LoadedMod& mod) { } } -void ModLoader::try_load_mod( - const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex) { - namespace fs = std::filesystem; - - std::unique_ptr bundle; - try { - bundle = load_bundle(modPath, fromDir); - } catch (const std::exception& e) { - Log.error("Failed to open {} bundle: {}", data::abbreviated_path_string(modPath), e.what()); - return; +LoadedMod* ModLoader::try_load_mod(const fs::path& modPath, bool fromDir, uint32_t searchDirIndex, + std::unique_ptr bundle) { + if (bundle == nullptr) { + try { + bundle = load_bundle(modPath, fromDir); + } catch (const std::exception& e) { + Log.error( + "Failed to open {} bundle: {}", data::abbreviated_path_string(modPath), e.what()); + return nullptr; + } } LoadedManifest manifest; @@ -868,7 +150,7 @@ void ModLoader::try_load_mod( manifest = load_manifest(modPath, *bundle); } catch (const std::exception& e) { Log.error("bad mod.json in {}: {}", data::abbreviated_path_string(modPath), e.what()); - return; + return nullptr; } if (const auto* existing = find_mod(manifest.metadata.id)) { @@ -881,7 +163,7 @@ void ModLoader::try_load_mod( log::write(manifest.metadata.id, LOG_LEVEL_ERROR, "duplicate mod id, not loading {}", data::abbreviated_path_string(modPath)); } - return; + return nullptr; } const auto& inserted = m_mods.emplace_back(std::make_unique()); @@ -889,7 +171,9 @@ void ModLoader::try_load_mod( mod.active = true; mod.modPath = fs::absolute(modPath); mod.searchDirIndex = searchDirIndex; - mod.inPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir; + mod.fromDirectory = fromDir; + mod.nativeInPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir; + mod.fileIdentity = file_identity(modPath); mod.metadata = std::move(manifest.metadata); mod.runtime = std::move(manifest.runtime); mod.bundle = std::move(bundle); @@ -919,6 +203,7 @@ void ModLoader::try_load_mod( log::write(mod.metadata.id, LOG_LEVEL_INFO, "found '{}' v{} by {} ({})", mod.metadata.name, mod.metadata.version, mod.metadata.author, data::abbreviated_path_string(modPath)); + return &mod; } bool ModLoader::activate_mod(LoadedMod& mod) { @@ -1072,55 +357,30 @@ void ModLoader::init() { m_cacheDir = m_searchDirs.front().path / ".cache"; } - namespace fs = std::filesystem; std::error_code ec; // Stale libs from previous sessions (see load_native). fs::remove_all(m_cacheDir, ec); - for (size_t dirIndex = 0; dirIndex < m_searchDirs.size(); ++dirIndex) { - const auto& searchDir = m_searchDirs[dirIndex]; - - // --mods can point the user dir at the bundled dir; don't scan the same dir twice. - bool alreadyScanned = false; - for (size_t earlier = 0; earlier < dirIndex && !alreadyScanned; ++earlier) { - alreadyScanned = fs::equivalent(m_searchDirs[earlier].path, searchDir.path, ec); - } - if (alreadyScanned) { + const auto packages = scan_packages(m_searchDirs); + for (const auto& package : packages) { + const auto* selected = select_package(packages, package.metadata.id); + if (selected != &package) { + log::write(package.metadata.id, LOG_LEVEL_INFO, "{} v{} shadowed by {} v{}", + data::abbreviated_path_string(package.path), package.metadata.version, + data::abbreviated_path_string(selected->path), selected->metadata.version); continue; } - - if (!fs::is_directory(searchDir.path)) { - if (dirIndex == 0) { - Log.info( - "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); - } else { - Log.debug( - "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); - } - continue; - } - - std::vector entries; - for (auto& e : fs::directory_iterator(searchDir.path, ec)) { - if (e.is_directory() && std::filesystem::exists(e.path() / "mod.json")) { - entries.push_back(e); - } else if (e.is_regular_file() && e.path().extension() == ".dusk") { - entries.push_back(e); - } - } - std::sort(entries.begin(), entries.end(), - [](const fs::directory_entry& a, const fs::directory_entry& b) { - return a.path().filename() < b.path().filename(); - }); - - for (auto& entry : entries) { - try_load_mod(entry.path(), entry.is_directory(), static_cast(dirIndex)); + if (auto* mod = try_load_mod(package.path, package.fromDirectory, package.searchDirIndex)) { + record_package_sources(*mod, packages); } } if (m_mods.empty()) { + init_services(); Log.info("no mods found"); + svc::modules_lifecycle_applied(); + m_startupComplete = true; return; } @@ -1185,10 +445,19 @@ void ModLoader::init() { m_startupComplete = true; } -LoadedMod* ModLoader::find_mod(std::string_view id) const { - for (auto& mod : mods()) { - if (mod.metadata.id == id) { - return &mod; +LoadedMod* ModLoader::find_mod(std::string_view id) { + for (auto& mod : m_mods) { + if (mod->metadata.id == id) { + return mod.get(); + } + } + return nullptr; +} + +const LoadedMod* ModLoader::find_mod(std::string_view id) const { + for (const auto& mod : m_mods) { + if (mod->metadata.id == id) { + return mod.get(); } } return nullptr; @@ -1206,8 +475,63 @@ void ModLoader::request_disable(std::string_view id) { } } -void ModLoader::request_reload(std::string_view id) { - m_pendingRequests.push_back({std::string{id}, RequestKind::Reload}); +ModOperationHandle ModLoader::request_reload(std::string_view id) { + auto operation = std::make_shared(); + if (find_mod(id) != nullptr) { + m_pendingRequests.push_back(ReloadRequest{ + .modId = std::string{id}, + .operation = operation, + }); + } else { + complete_operation(operation, false, "The mod is no longer installed"); + } + return operation; +} + +ModOperationHandle ModLoader::request_install( + fs::path path, std::optional update) { + auto operation = std::make_shared(); + m_pendingRequests.push_back(InstallRequest{ + .stagedPath = std::move(path), + .update = std::move(update), + .operation = operation, + }); + return operation; +} + +ModOperationHandle ModLoader::request_uninstall(std::string_view id) { + auto operation = std::make_shared(); + if (find_mod(id) != nullptr) { + m_pendingRequests.push_back(UninstallRequest{ + .modId = std::string{id}, + .operation = operation, + }); + } else { + complete_operation(operation); + } + return operation; +} + +ModOperationHandle ModLoader::request_reactivate(std::string_view id) { + auto operation = std::make_shared(); + m_pendingRequests.push_back(LifecycleRequest{ + .modId = std::string{id}, + .action = LifecycleAction::Reactivate, + .operation = operation, + }); + return operation; +} + +fs::path ModLoader::user_mods_dir() const { + return m_searchDirs.empty() ? fs::path{} : m_searchDirs.front().path; +} + +bool ModLoader::can_uninstall(const LoadedMod& mod) const { + return mod.hasUserPackage; +} + +bool ModLoader::can_update(const LoadedMod& mod) const { + return mod.searchDirIndex != 0 || (!mod.fromDirectory && can_uninstall(mod)); } void ModLoader::notify_mod_failure(LoadedMod& mod, bool firstFailure) { @@ -1218,7 +542,10 @@ void ModLoader::notify_mod_failure(LoadedMod& mod, bool firstFailure) { if (!m_startupComplete) { return; } - m_pendingRequests.push_back({mod.metadata.id, RequestKind::Disable}); + m_pendingRequests.emplace_back(LifecycleRequest{ + .modId = mod.metadata.id, + .action = LifecycleAction::Disable, + }); } void ModLoader::flush_toasts() { @@ -1251,7 +578,7 @@ void ModLoader::flush_toasts() { ui::push_toast(std::move(toast)); } -std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) { +std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) const { std::vector included{&target}; std::vector pending{&target}; while (!pending.empty()) { @@ -1280,15 +607,7 @@ std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) { return ordered; } -bool ModLoader::ensure_native_loaded(LoadedMod& mod) { - if (mod.native || mod.nativeStatus == NativeModStatus::None) { - return true; - } - return load_native_if_present(mod); -} - bool ModLoader::reload_bundle(LoadedMod& mod) { - namespace fs = std::filesystem; log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}", data::abbreviated_path_string(mod.modPath)); @@ -1314,6 +633,7 @@ bool ModLoader::reload_bundle(LoadedMod& mod) { mod.runtime = std::move(newManifest.runtime); // In-flight readers of the old bundle keep it alive through their shared_ptr. mod.bundle = std::move(newBundle); + mod.fileIdentity = file_identity(mod.modPath); mod.loadFailed = false; mod.failureReason.clear(); @@ -1343,7 +663,48 @@ bool ModLoader::reload_bundle(LoadedMod& mod) { return true; } -void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { +void ModLoader::resume_lifecycle_set(const std::vector& affected) { + for (auto* mod : affected) { + if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { + continue; + } + if (!ensure_native_loaded(*mod)) { + continue; + } + if (mod->native && !mod->servicesRegistered) { + if (register_static_service_exports(*mod)) { + mod->servicesRegistered = true; + } else { + log::write(mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); + deactivate_mod(*mod); + } + } + } + + for (auto* mod : affected) { + if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { + continue; + } + if (!required_deps_active(*mod)) { + mod->suspendedByProvider = true; + log::write( + mod->metadata.id, LOG_LEVEL_INFO, "suspended: a required provider is disabled"); + continue; + } + mod->suspendedByProvider = false; + activate_mod(*mod); + } + + for (auto* mod : affected) { + if (!mod->active && mod->servicesRegistered) { + svc::remove_services_for_provider(*mod); + mod->servicesRegistered = false; + } + } +} + +void ModLoader::apply_lifecycle_change( + LoadedMod& target, const bool reload, const PackageCandidate* replacement) { auto affected = collect_lifecycle_set(target); // Dependents first (reverse init order), like shutdown. @@ -1363,6 +724,17 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { } } + if (replacement != nullptr) { + target.modPath = replacement->path; + target.searchDirIndex = replacement->searchDirIndex; + target.fromDirectory = replacement->fromDirectory; + target.nativeInPlace = + replacement->fromDirectory && m_searchDirs[replacement->searchDirIndex].inPlaceNative; + std::stable_sort(m_mods.begin(), m_mods.end(), + [](const auto& a, const auto& b) { return a->searchDirIndex > b->searchDirIndex; }); + loader::sort_mods(m_mods); + } + if (reload) { // On failure the target is failed and stays down; dependents get resume attempts below // and suspend against the failed provider where required. @@ -1380,48 +752,7 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { affected = std::move(reordered); } - // Mirror startup: publish every candidate's static exports before any of them initialize, - // so importers within the set resolve providers regardless of initialization order - // (optional cycles rely on this). - for (auto* mod : affected) { - if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { - continue; - } - if (!ensure_native_loaded(*mod)) { - continue; - } - if (mod->native && !mod->servicesRegistered) { - if (register_static_service_exports(*mod)) { - mod->servicesRegistered = true; - } else { - log::write(mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); - deactivate_mod(*mod); - } - } - } - - // Providers first (init order). The target is naturally first among the affected mods. - for (auto* mod : affected) { - if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { - continue; - } - if (!required_deps_active(*mod)) { - mod->suspendedByProvider = true; - log::write( - mod->metadata.id, LOG_LEVEL_INFO, "suspended: a required provider is disabled"); - continue; - } - mod->suspendedByProvider = false; - activate_mod(*mod); - } - - // Mods that stayed down must not leave their exports resolvable. - for (auto* mod : affected) { - if (!mod->active && mod->servicesRegistered) { - svc::remove_services_for_provider(*mod); - mod->servicesRegistered = false; - } - } + resume_lifecycle_set(affected); } void ModLoader::on_enabled_changed(LoadedMod& mod) { @@ -1435,13 +766,335 @@ void ModLoader::on_enabled_changed(LoadedMod& mod) { } if (mod.suspendedByProvider) { if (!mod.cvarIsEnabled->getValue()) { - // The user disabled a suspended mod; stop waiting for its providers. mod.suspendedByProvider = false; } return; } - m_pendingRequests.push_back({mod.metadata.id, - mod.cvarIsEnabled->getValue() ? RequestKind::Enable : RequestKind::Disable}); + m_pendingRequests.push_back(LifecycleRequest{ + .modId = mod.metadata.id, + .action = + mod.cvarIsEnabled->getValue() ? LifecycleAction::Enable : LifecycleAction::Disable, + }); +} + +void ModLoader::forget_mod(LoadedMod& mod) { + auto affected = collect_lifecycle_set(mod); + std::vector blocked; + std::vector pending{&mod}; + while (!pending.empty()) { + auto* provider = pending.back(); + pending.pop_back(); + for (const auto& edge : provider->dependents) { + if (!edge.required || edge.mod == nullptr || + std::ranges::find(blocked, edge.mod) != blocked.end()) + { + continue; + } + blocked.push_back(edge.mod); + pending.push_back(edge.mod); + } + } + + for (auto* affectedMod : affected | std::views::reverse) { + const bool wasActive = affectedMod->active; + if (affectedMod->active || affectedMod->initialized || affectedMod->native != nullptr) { + log::write(affectedMod->metadata.id, LOG_LEVEL_INFO, "deactivating mod"); + deactivate_mod(*affectedMod); + } + if (affectedMod != &mod && wasActive) { + affectedMod->suspendedByProvider = true; + } + } + + const std::string modId = mod.metadata.id; + auto* removedMod = &mod; + if (mod.enabledSubscription != 0) { + config::unsubscribe(mod.enabledSubscription); + mod.enabledSubscription = 0; + } + unregister(*mod.cvarIsEnabled); + + std::vector remaining; + remaining.reserve(affected.size()); + for (auto* affectedMod : affected) { + if (affectedMod != &mod) { + remaining.push_back(affectedMod); + } + } + std::erase_if( + m_mods, [removedMod](const auto& candidate) { return candidate.get() == removedMod; }); + loader::sort_mods(m_mods); + + std::vector resumable; + for (auto* affectedMod : remaining) { + const bool needsRemovedProvider = std::ranges::find(blocked, affectedMod) != blocked.end(); + affectedMod->suspendedByProvider = needsRemovedProvider; + if (needsRemovedProvider) { + log::write(affectedMod->metadata.id, LOG_LEVEL_INFO, + "suspended: required provider '{}' was removed", modId); + } else { + resumable.push_back(affectedMod); + } + } + resume_lifecycle_set(resumable); + + ++m_generation; + log::write(modId, LOG_LEVEL_INFO, "forgot removed package"); +} + +ModLoader::OperationResult ModLoader::load_runtime_mod(const fs::path& requestedPath) { + const auto path = fs::absolute(requestedPath).lexically_normal(); + std::error_code error; + const auto status = fs::status(path, error); + if (error || !fs::exists(status)) { + return { + .success = false, + .message = error ? fmt::format("Could not inspect the package: {}", error.message()) : + "The package was not found", + }; + } + + const bool fromDir = fs::is_directory(status); + std::unique_ptr bundle; + std::optional metadata; + try { + bundle = load_bundle(path, fromDir); + metadata = load_manifest(path, *bundle).metadata; + } catch (const std::exception& exception) { + return { + .success = false, + .message = fmt::format("Invalid mod package: {}", exception.what()), + }; + } + if (const auto* duplicate = find_mod(metadata->id)) { + return { + .success = false, + .message = fmt::format("A mod with this ID is already loaded from {}", + data::abbreviated_path_string(duplicate->modPath)), + }; + } + auto* mod = try_load_mod(path, fromDir, 0, std::move(bundle)); + if (mod == nullptr) { + return { + .success = false, + .message = "The mod could not be loaded", + }; + } + + mod->enabledSubscription = Register( + *mod->cvarIsEnabled, [this, mod](const bool&, const bool&) { on_enabled_changed(*mod); }); + loader::sort_mods(m_mods); + if (!mod->cvarIsEnabled->getValue()) { + mod->active = false; + mod->suspendedByProvider = false; + log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed disabled by config"); + } else if (!mod->loadFailed) { + mod->active = false; + apply_lifecycle_change(*mod, false); + } + ++m_generation; + log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed at runtime"); + return runtime_result(*mod); +} + +ModLoader::OperationResult ModLoader::reload_runtime_mod( + LoadedMod& mod, const PackageCandidate* replacement) { + if (mod.nativeInPlace && replacement == nullptr) { + return { + .success = false, + .message = "An in-place native library cannot be reloaded", + .mod = &mod, + }; + } + apply_lifecycle_change(mod, true, replacement); + ++m_generation; + return runtime_result(mod); +} + +ModLoader::OperationResult ModLoader::uninstall_runtime_mod(LoadedMod& mod) { + std::vector packages; + try { + packages = scan_packages(m_searchDirs); + } catch (const std::exception& exception) { + return {.success = false, .message = exception.what()}; + } + record_package_sources(mod, packages); + if (!can_uninstall(mod)) { + return {.success = false, .message = "No installed package to remove"}; + } + + std::string removalError; + std::erase_if(packages, [&](const auto& package) { + if (package.metadata.id != mod.metadata.id || package.searchDirIndex != 0 || + package.fromDirectory || package.symlink) + { + return false; + } + std::error_code error; + fs::remove(package.path, error); + if (error) { + removalError = fmt::format("Could not remove {}: {}", + data::abbreviated_path_string(package.path), error.message()); + } + return !error; + }); + + OperationResult result; + if (const auto* selected = select_package(packages, mod.metadata.id)) { + if (selected->path != mod.modPath) { + result = reload_runtime_mod(mod, selected); + } else { + result.mod = &mod; + ++m_generation; + } + record_package_sources(mod, packages); + } else { + forget_mod(mod); + } + if (!removalError.empty()) { + result.success = false; + result.message = std::move(removalError); + } + return result; +} + +ModLoader::OperationResult ModLoader::runtime_result(LoadedMod& mod) { + if (mod.loadFailed) { + return { + .success = false, + .message = mod.failureReason.empty() ? "Mod failed to activate" : mod.failureReason, + .mod = &mod, + }; + } + if (mod.cvarIsEnabled->getValue() && !mod.active) { + return { + .success = false, + .message = "A required provider is unavailable", + .mod = &mod, + }; + } + if (!mod.cvarIsEnabled->getValue()) { + return { + .message = "Installed, disabled by config", + .mod = &mod, + }; + } + return {.mod = &mod}; +} + +ModLoader::OperationResult ModLoader::install_staged( + const fs::path& requestedPath, const std::optional& update) { + if (m_searchDirs.empty()) { + return { + .success = false, + .message = "No writable mods directory is configured", + }; + } + std::error_code error; + const auto userDir = fs::weakly_canonical(m_searchDirs.front().path, error); + if (error) { + return { + .success = false, + .message = + fmt::format("Could not resolve the user mods directory: {}", error.message()), + }; + } + const auto stagingDir = userDir / ".staging"; + const auto path = fs::weakly_canonical(requestedPath, error); + const bool stagedName = path.extension() == ".part" && path.stem().extension() == ".dusk"; + if (error || path.parent_path() != stagingDir || !stagedName || + !fs::is_regular_file(path, error)) + { + Log.error("refusing staged install from {}", data::abbreviated_path_string(requestedPath)); + return { + .success = false, + .message = "The package is not in the mod staging directory", + }; + } + + ModMetadata metadata; + std::string validationError; + if (!inspect_mod_bundle(path, metadata, validationError)) { + return { + .success = false, + .message = fmt::format("Invalid mod package: {}", validationError), + }; + } + + if (update) { + if (metadata.id != update->modId || metadata.version != update->targetVersion) { + return {.success = false, .message = "The update package identity changed"}; + } + if (auto reason = updates::validate(*update); !reason.empty()) { + return {.success = false, .message = std::move(reason)}; + } + } + + const auto destination = userDir / fmt::format("{}.dusk", safe_filename(metadata.id)); + auto* installed = find_mod(metadata.id); + if (installed != nullptr && !can_update(*installed)) { + return { + .success = false, + .message = "Cannot install mod over a development directory", + }; + } + + for (const auto& mod : mods()) { + if (mod.metadata.id != metadata.id && fs::equivalent(mod.modPath, destination, error)) { + return { + .success = false, + .message = "The destination filename belongs to a different mod", + }; + } + } + error.clear(); + + if (!borealis::update::parse_version(metadata.version)) { + return {.success = false, .message = "The package version is invalid"}; + } + std::vector packages; + try { + packages = scan_packages(m_searchDirs); + } catch (const std::exception& exception) { + return {.success = false, .message = exception.what()}; + } + if (const auto* selected = select_package(packages, metadata.id); + selected && compare_package_versions(metadata.version, selected->metadata.version) < 0) + { + return { + .success = false, + .message = fmt::format( + "A newer version ({}) is already installed", selected->metadata.version), + }; + } + + const auto packageResult = install_package(path, destination, metadata.id); + if (!packageResult.replaced) { + return {.success = false, .message = packageResult.error}; + } + std::erase_if(packages, [&](const auto& package) { + if (package.metadata.id != metadata.id || package.searchDirIndex != 0 || + package.fromDirectory) + { + return false; + } + std::error_code statusError; + return fs::equivalent(package.path, destination, statusError) || + !fs::exists(package.path, statusError); + }); + packages.push_back({.path = destination, .metadata = metadata}); + const auto* selected = select_package(packages, metadata.id); + auto result = installed != nullptr ? reload_runtime_mod(*installed, selected) : + load_runtime_mod(selected->path); + if (result.mod != nullptr) { + record_package_sources(*result.mod, packages); + } + + if (result.success && !packageResult.error.empty()) { + result.success = false; + result.message = packageResult.error; + } + return result; } void ModLoader::apply_pending_requests() { @@ -1452,16 +1105,90 @@ void ModLoader::apply_pending_requests() { return; } - // Coalesce per mod, last request wins. Failures during apply re-enqueue for next tick. const auto requests = std::exchange(m_pendingRequests, {}); - std::vector coalesced; + std::vector coalesced; for (const auto& request : requests) { - const auto existing = std::ranges::find_if( - coalesced, [&](const Request& r) { return r.modId == request.modId; }); + if (const auto* install = std::get_if(&request)) { + auto result = install_staged(install->stagedPath, install->update); + if (result.success && result.mod != nullptr) { + const auto& metadata = result.mod->metadata; + const std::string iconRml = + metadata.iconPath.empty() ? + std::string{} : + fmt::format(R"()", + ui::escape(ui::mod_image_source(*result.mod, metadata.iconPath))); + ui::push_toast({ + .type = "mod-installed", + .title = "Mod installed", + .content = fmt::format( + R"({}{}v{}{})", + iconRml, ui::escape(metadata.name), ui::escape(metadata.version), + ui::escape(metadata.author)), + .duration = std::chrono::seconds{4}, + }); + } else if (!result.success && result.mod == nullptr) { + ui::push_toast({ + .type = "warning", + .title = "Mod install failed", + .content = + result.message.empty() ? "The loader rejected the package" : result.message, + .duration = std::chrono::seconds{6}, + }); + } + if (!result.success && !m_searchDirs.empty()) { + // request_install owns only files under the configured staging directory. + std::error_code error; + const auto userDir = fs::weakly_canonical(m_searchDirs.front().path, error); + if (!error) { + const auto stagedPath = fs::weakly_canonical(install->stagedPath, error); + if (!error && stagedPath.parent_path() == userDir / ".staging") { + fs::remove(stagedPath, error); + } + } + } + complete_operation(install->operation, result.success, std::move(result.message)); + continue; + } + if (const auto* reload = std::get_if(&request)) { + auto* mod = find_mod(reload->modId); + if (mod == nullptr) { + complete_operation(reload->operation, false, "The mod is no longer installed"); + continue; + } + auto result = reload_runtime_mod(*mod); + complete_operation(reload->operation, result.success, std::move(result.message)); + continue; + } + if (const auto* uninstall = std::get_if(&request)) { + auto* mod = find_mod(uninstall->modId); + if (mod == nullptr) { + complete_operation(uninstall->operation); + continue; + } + const auto removedName = mod->metadata.name; + const auto removedId = mod->metadata.id; + auto result = uninstall_runtime_mod(*mod); + if (result.success) { + queue::remove_by_mod_id(removedId); + ui::push_toast({ + .title = result.mod != nullptr ? "User update removed" : "Mod uninstalled", + .content = removedName, + .duration = std::chrono::seconds{2}, + }); + } + complete_operation(uninstall->operation, result.success, std::move(result.message)); + continue; + } + + const auto& lifecycle = std::get(request); + const auto existing = + std::ranges::find(coalesced, lifecycle.modId, &LifecycleRequest::modId); if (existing != coalesced.end()) { - existing->kind = request.kind; + complete_operation( + existing->operation, false, "Superseded by a newer lifecycle request"); + *existing = lifecycle; } else { - coalesced.push_back(request); + coalesced.push_back(lifecycle); } } @@ -1469,19 +1196,36 @@ void ModLoader::apply_pending_requests() { auto* mod = find_mod(request.modId); if (mod == nullptr) { Log.warn("lifecycle request for unknown mod '{}'", request.modId); + complete_operation(request.operation, false, "The mod is no longer installed"); continue; } - if (request.kind == RequestKind::Reload && mod->inPlace) { - log::write(mod->metadata.id, LOG_LEVEL_WARN, "is a built-in mod and can't be reloaded"); + if (request.action == LifecycleAction::Enable && mod->enabledApplied) { continue; } - if (request.kind == RequestKind::Enable && mod->enabledApplied) { + if (request.action == LifecycleAction::Disable && !mod->enabledApplied && !mod->active) { continue; } - if (request.kind == RequestKind::Disable && !mod->enabledApplied && !mod->active) { - continue; + + if (request.action == LifecycleAction::Reactivate) { + mod->loadFailed = false; + mod->failureReason.clear(); + mod->suspendedByProvider = false; + if (!mod->cvarIsEnabled->getValue()) { + mod->cvarIsEnabled->setValue(true); + } + } + apply_lifecycle_change(*mod, false); + + if (request.action == LifecycleAction::Reactivate) { + std::string error; + if (mod->loadFailed) { + error = + mod->failureReason.empty() ? "The mod failed to activate" : mod->failureReason; + } else if (mod->cvarIsEnabled->getValue() && !mod->active) { + error = "A required provider is unavailable"; + } + complete_operation(request.operation, error.empty(), std::move(error)); } - apply_lifecycle_change(*mod, request.kind == RequestKind::Reload); } svc::modules_lifecycle_applied(); diff --git a/src/dusk/mods/loader/loader.hpp b/src/dusk/mods/loader/loader.hpp index 6b9fd39232..010abdb2d3 100644 --- a/src/dusk/mods/loader/loader.hpp +++ b/src/dusk/mods/loader/loader.hpp @@ -1,10 +1,9 @@ #pragma once #include -#include #include -#include "miniz.h" +#include "dusk/archive.hpp" #include "dusk/mod_loader.hpp" namespace dusk::mods { @@ -24,21 +23,22 @@ public: virtual std::vector readFile(const std::string& fileName) = 0; virtual std::vector getFileNames() = 0; virtual size_t getFileSize(const std::string& fileName) = 0; + virtual bool file_exists(std::string const& fileName) = 0; + virtual bool directory_exists(std::string const& fileName) = 0; }; class ModBundleZip final : public ModBundle { public: - explicit ModBundleZip(std::vector&& data); - ~ModBundleZip() override; + explicit ModBundleZip(const std::filesystem::path& path); + ~ModBundleZip() override = default; std::vector readFile(const std::string& fileName) override; std::vector getFileNames() override; size_t getFileSize(const std::string& fileName) override; + bool file_exists(const std::string& fileName) override; + bool directory_exists(const std::string& fileName) override; private: - std::vector zip_data; - mz_zip_archive res_zip{}; - bool res_zip_open = false; - std::mutex m_mutex; + archive::ZipArchive m_archive; }; class ModBundleDisk final : public ModBundle { @@ -48,6 +48,8 @@ public: std::vector readFile(const std::string& fileName) override; std::vector getFileNames() override; size_t getFileSize(const std::string& fileName) override; + bool file_exists(const std::string& fileName) override; + bool directory_exists(const std::string& fileName) override; private: [[nodiscard]] std::filesystem::path toRealPath(const std::string& fileName) const; diff --git a/src/dusk/mods/loader/manifest.cpp b/src/dusk/mods/loader/manifest.cpp new file mode 100644 index 0000000000..8c8243bd0a --- /dev/null +++ b/src/dusk/mods/loader/manifest.cpp @@ -0,0 +1,203 @@ +#include "manifest.hpp" + +#include "loader.hpp" +#include "natives.hpp" +#include "packages.hpp" + +#include "dusk/mods/log_buffer.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +using namespace std::string_literals; +namespace fs = std::filesystem; + +namespace dusk::mods { +namespace { + +class InvalidModDataException : public std::runtime_error { +public: + explicit InvalidModDataException(const std::string& msg) : runtime_error(msg) {} + explicit InvalidModDataException(const char* msg) : runtime_error(msg) {} +}; + +void validate_mod_id(std::string_view const str) { + if (str.empty()) { + throw InvalidModDataException("Missing ID value in mod metadata"); + } + + bool lastWasPeriod = false; + for (auto const chr : str) { + if (chr == '.') { + if (lastWasPeriod) { + throw InvalidModDataException("Cannot have two consecutive periods in mod ID"); + } + lastWasPeriod = true; + continue; + } + + lastWasPeriod = false; + + if (chr == '_') + continue; + + if (chr >= '0' && chr <= '9') + continue; + + if (chr >= 'a' && chr <= 'z') + continue; + + if (chr >= 'A' && chr <= 'Z') + continue; + + throw InvalidModDataException( + fmt::format("Invalid character '{}' in mod ID. Valid characters are period, " + "underscore, and alphanumerics.", + chr)); + } +} + +bool bundle_has_file(ModBundle& bundle, const std::string& path) { + try { + bundle.getFileSize(path); + return true; + } catch (const std::runtime_error&) { + return false; + } +} + +std::string resolve_image_path(ModBundle& bundle, const std::string& modId, std::string_view key, + const std::string& manifestPath, const std::string& defaultPath) { + if (!manifestPath.empty()) { + if (!is_safe_resource_path(manifestPath)) { + log::write( + modId, LOG_LEVEL_WARN, "invalid {} path '{}' in mod.json", key, manifestPath); + } else if (!bundle_has_file(bundle, manifestPath)) { + log::write( + modId, LOG_LEVEL_WARN, "{} path '{}' not found in bundle", key, manifestPath); + } else { + return manifestPath; + } + } + if (bundle_has_file(bundle, defaultPath)) { + return defaultPath; + } + return {}; +} + +uint16_t parse_runtime_version_component(std::string_view text, std::string_view fieldName) { + uint32_t value = 0; + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); + if (text.empty() || error != std::errc{} || end != text.data() + text.size() || + value > UINT16_MAX) + { + throw InvalidModDataException(fmt::format("Invalid {} in runtime version pin", fieldName)); + } + return static_cast(value); +} + +std::optional parse_runtime(const nlohmann::json& manifest) { + const auto field = manifest.find("runtime"); + if (field == manifest.end()) { + return std::nullopt; + } + if (!field->is_string()) { + throw InvalidModDataException("runtime must be a string"); + } + + const std::string pin = field->get(); + const auto at = pin.rfind('@'); + if (at == std::string::npos || at == 0 || at + 1 == pin.size() || pin.find('@') != at || + at >= MOD_META_SERVICE_ID_SIZE) + { + throw InvalidModDataException( + "runtime must be a service id followed by @major or @major.minor"); + } + + const std::string_view version{pin.data() + at + 1, pin.size() - at - 1}; + const auto dot = version.find('.'); + if (dot != std::string_view::npos && version.find('.', dot + 1) != std::string_view::npos) { + throw InvalidModDataException("runtime version pin has too many components"); + } + + DelegatedModRuntime result; + result.id = pin.substr(0, at); + result.major = parse_runtime_version_component( + dot == std::string_view::npos ? version : version.substr(0, dot), "major version"); + if (dot != std::string_view::npos) { + result.minMinor = parse_runtime_version_component(version.substr(dot + 1), "minor version"); + } + return result; +} + +} // namespace + +LoadedManifest load_manifest(const std::filesystem::path& modPath, ModBundle& bundle) { + const auto metaJson = bundle.readFile("mod.json"); + auto j = nlohmann::json::parse(metaJson); + + std::string metaId = j.value("id", ""); + std::string metaName = j.value("name", ""); + std::string metaVersion = j.value("version", ""); + std::string metaAuthor = j.value("author", ""); + std::string metaDescription = j.value("description", ""); + std::string metaIcon = j.value("icon", ""); + std::string metaBanner = j.value("banner", ""); + + validate_mod_id(metaId); + + if (metaName.empty()) { + metaName = borealis::io::fs_path_to_string(modPath.stem()); + } + if (metaVersion.empty()) { + metaVersion = "?"s; + } + if (metaAuthor.empty()) { + metaAuthor = "unknown"s; + } + + std::string iconPath = resolve_image_path(bundle, metaId, "icon", metaIcon, "res/icon.png"s); + std::string bannerPath = + resolve_image_path(bundle, metaId, "banner", metaBanner, "res/banner.png"s); + + return LoadedManifest{ + .metadata = + { + std::move(metaId), + std::move(metaName), + std::move(metaVersion), + std::move(metaAuthor), + std::move(metaDescription), + std::move(iconPath), + std::move(bannerPath), + }, + .runtime = parse_runtime(j), + }; +} + +bool inspect_mod_bundle( + const fs::path& path, ModMetadata& metadata, std::string& error, bool* hasNative) noexcept { + try { + auto bundle = load_bundle(path, false); + metadata = load_manifest(path, *bundle).metadata; + if (hasNative != nullptr) { + *hasNative = std::ranges::any_of(bundle->getFileNames(), + [](const auto& name) { return has_native_library_extension(name); }); + } + error.clear(); + return true; + } catch (const std::exception& exception) { + error = exception.what(); + } catch (...) { + error = "Unknown bundle validation error"; + } + return false; +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/manifest.hpp b/src/dusk/mods/loader/manifest.hpp new file mode 100644 index 0000000000..e3eea70298 --- /dev/null +++ b/src/dusk/mods/loader/manifest.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "dusk/mod_loader.hpp" + +namespace dusk::mods { + +struct LoadedManifest { + ModMetadata metadata; + std::optional runtime; +}; + +LoadedManifest load_manifest(const std::filesystem::path& modPath, ModBundle& bundle); + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/natives.cpp b/src/dusk/mods/loader/natives.cpp new file mode 100644 index 0000000000..e3802f4b52 --- /dev/null +++ b/src/dusk/mods/loader/natives.cpp @@ -0,0 +1,620 @@ +#include "natives.hpp" + +#include "loader.hpp" +#include "native_module.hpp" + +#include "dusk/data.hpp" +#include "dusk/mods/log_buffer.hpp" +#include "dusk/mods/svc/registry.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_view_literals; +namespace fs = std::filesystem; + +#if defined(_WIN32) +#if defined(_M_ARM64) +static constexpr std::string_view k_nativePlatform = "windows-arm64"sv; +#elif defined(_M_X64) +static constexpr std::string_view k_nativePlatform = "windows-amd64"sv; +#elif defined(_M_IX86) +static constexpr std::string_view k_nativePlatform = "windows-x86"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +#endif +static constexpr std::string_view k_nativeLibName = "mod.dll"sv; +#elif defined(__ANDROID__) +#if defined(__aarch64__) +static constexpr std::string_view k_nativePlatform = "android-aarch64"sv; +#elif defined(__x86_64__) +static constexpr std::string_view k_nativePlatform = "android-x86_64"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +#endif +static constexpr std::string_view k_nativeLibName = "mod.so"sv; +#elif defined(__APPLE__) +#include +#if TARGET_OS_IOS +static constexpr std::string_view k_nativePlatform = "ios-arm64"sv; +#elif TARGET_OS_TV +static constexpr std::string_view k_nativePlatform = "tvos-arm64"sv; +#elif defined(__aarch64__) +static constexpr std::string_view k_nativePlatform = "macos-arm64"sv; +#elif defined(__x86_64__) +static constexpr std::string_view k_nativePlatform = "macos-x86_64"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +#endif +static constexpr std::string_view k_nativeLibName = "mod.so"sv; +#elif defined(__linux__) +#if defined(__aarch64__) +static constexpr std::string_view k_nativePlatform = "linux-aarch64"sv; +#elif defined(__x86_64__) +static constexpr std::string_view k_nativePlatform = "linux-x86_64"sv; +#elif defined(__i386__) +static constexpr std::string_view k_nativePlatform = "linux-x86"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +#endif +static constexpr std::string_view k_nativeLibName = "mod.so"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +static constexpr std::string_view k_nativeLibName = ""sv; +#endif + +namespace dusk::mods { + +bool has_native_library_extension(std::string_view name) { + const auto endsWith = [name](std::string_view extension) { + if (name.size() < extension.size()) { + return false; + } + const auto suffix = name.substr(name.size() - extension.size()); + return std::ranges::equal(suffix, extension, [](char lhs, char rhs) { + const auto lower = [](char value) { + return value >= 'A' && value <= 'Z' ? static_cast(value + ('a' - 'A')) : + value; + }; + return lower(lhs) == lower(rhs); + }); + }; + return endsWith(".dll"sv) || endsWith(".so"sv) || endsWith(".dylib"sv); +} + +namespace { + +constexpr std::string_view k_nativeLibDir = "lib/"sv; + +class DirectoryRollback { +public: + ~DirectoryRollback() { + if (!mPath.empty()) { + std::error_code ec; + fs::remove_all(mPath, ec); + } + } + + void set_path(fs::path path) { mPath = std::move(path); } + void release() { mPath.clear(); } + +private: + fs::path mPath; +}; + +struct NativeRuntimeLocation { + std::string entry; + std::vector runtimeEntries; + bool anyLibs = false; +}; + +struct NativeLocateFailure { + NativeModStatus status; + std::string logMessage; +}; + +using NativeLocateResult = std::variant; + +NativeLocateResult locate_native_runtime(ModBundle& bundle) { + NativeRuntimeLocation result; + const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); + const std::string nativeEntry = platformPrefix + std::string{k_nativeLibName}; + for (const auto& name : bundle.getFileNames()) { + if (name.find('/') == std::string::npos && has_native_library_extension(name)) { + return NativeLocateFailure{ + NativeModStatus::InvalidBundle, + fmt::format( + "native library '{}' found at the root (natives go in /lib/{{platform}})", + name), + }; + } + if (!name.starts_with(k_nativeLibDir)) { + continue; + } + + const std::string_view libPath{ + name.data() + k_nativeLibDir.size(), name.size() - k_nativeLibDir.size()}; + const auto platformEnd = libPath.find('/'); + if (platformEnd != std::string_view::npos) { + const auto entryName = libPath.substr(platformEnd + 1); + if (entryName.find('/') == std::string_view::npos && + (entryName == "mod.dll"sv || entryName == "mod.so"sv)) + { + result.anyLibs = true; + } + } + + if (!k_nativePlatform.empty() && name.starts_with(platformPrefix)) { + const std::string_view relativeName{ + name.data() + platformPrefix.size(), name.size() - platformPrefix.size()}; + if (!is_safe_resource_path(relativeName)) { + continue; + } + result.runtimeEntries.push_back(name); + } + if (name == nativeEntry) { + result.entry = name; + } + } + std::ranges::sort(result.runtimeEntries); + result.runtimeEntries.erase( + std::unique(result.runtimeEntries.begin(), result.runtimeEntries.end()), + result.runtimeEntries.end()); + return result; +} + +// True if the first `capacity` bytes of `str` contain a NUL. +bool terminated_within(const char* str, size_t capacity) { + return std::memchr(str, '\0', capacity) != nullptr; +} + +bool parse_meta(NativeMod& native, LoadedMod& mod) { + const ModMeta* meta = native.meta; + if (meta->struct_size < sizeof(ModMeta)) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta descriptor has invalid size {}", + meta->struct_size); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + } + const auto* cursor = static_cast(meta->records_begin); + const auto* end = static_cast(meta->records_end); + if (cursor == nullptr || end == nullptr || cursor > end || + (reinterpret_cast(cursor) & 7) != 0) + { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta section bounds are invalid"); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + } + + ModMetaParsed parsed; + size_t headerCount = 0; + const auto invalid = [&](std::string_view why) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "invalid metadata record at offset {}: {}", + cursor - static_cast(meta->records_begin), why); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + }; + + while (cursor < end) { + if (end - cursor < 8) { + return invalid("trailing bytes"); + } + uint64_t first = 0; + std::memcpy(&first, cursor, sizeof(first)); + if (first == 0) { // linker padding / bounds sentinel + cursor += 8; + continue; + } + + const auto* rec = reinterpret_cast(cursor); + const size_t size = rec->size; + if (size < 8 || size % 8 != 0 || size > static_cast(end - cursor)) { + return invalid("bad record size"); + } + + switch (rec->kind) { + case MOD_META_PAD: + break; + case MOD_META_HEADER: { + if (size < sizeof(ModMetaHeader)) { + return invalid("truncated header record"); + } + const auto* header = reinterpret_cast(rec); + ++headerCount; + parsed.abiVersion = header->abi_version; + break; + } + case MOD_META_IMPORT: { + if (size < sizeof(ModMetaImport)) { + return invalid("truncated import record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { + return invalid("unterminated import service id"); + } + parsed.imports.push_back(record); + break; + } + case MOD_META_EXPORT: { + if (size < sizeof(ModMetaExport)) { + return invalid("truncated export record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { + return invalid("unterminated export service id"); + } + parsed.exports.push_back(record); + break; + } + case MOD_META_HOOK_FN: { + if (size < sizeof(ModMetaHookFn)) { + return invalid("truncated hook record"); + } + parsed.hookFns.push_back( + reinterpret_cast(const_cast(cursor))); + break; + } + case MOD_META_HOOK_MEM: { + if (size <= sizeof(ModMetaHookMem)) { + return invalid("truncated hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMem); + const size_t capacity = size - sizeof(ModMetaHookMem); + if (!terminated_within(strings, capacity)) { + return invalid("unterminated hook vtable symbol"); + } + const size_t vtableLen = std::char_traits::length(strings); + if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { + return invalid("unterminated hook display name"); + } + parsed.hookMems.push_back(record); + break; + } + case MOD_META_HOOK_MEM_EXT: { + if (size <= sizeof(ModMetaHookMemExt)) { + return invalid("truncated extended hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (record->pmf_size <= MOD_META_HOOK_MEM_CAPACITY || + record->pmf_size > MOD_META_HOOK_MEM_EXT_CAPACITY || record->materialize == nullptr) + { + return invalid("bad extended hook member-pointer size"); + } + const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMemExt); + const size_t capacity = size - sizeof(ModMetaHookMemExt); + if (!terminated_within(strings, capacity)) { + return invalid("unterminated extended hook vtable symbol"); + } + const size_t vtableLen = std::char_traits::length(strings); + if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { + return invalid("unterminated extended hook display name"); + } + parsed.hookMemExts.push_back(record); + break; + } + case MOD_META_HOOK_NAME: { + if (size <= sizeof(ModMetaHookName)) { + return invalid("truncated hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + const char* name = reinterpret_cast(cursor) + sizeof(ModMetaHookName); + if (!terminated_within(name, size - sizeof(ModMetaHookName))) { + return invalid("unterminated hook symbol name"); + } + parsed.hookNames.push_back(record); + break; + } + default: + // Additive record kinds may appear within a format version; skip them. + log::write(mod.metadata.id, LOG_LEVEL_DEBUG, "skipping unknown metadata record kind {}", + rec->kind); + break; + } + cursor += size; + } + + if (headerCount != 1) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expected 1 metadata header record, found {}", + headerCount); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + } + if (parsed.abiVersion != MOD_ABI_VERSION) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", + parsed.abiVersion, MOD_ABI_VERSION); + mod.nativeStatus = NativeModStatus::ApiVersionMismatch; + return false; + } + + native.parsed = std::move(parsed); + return true; +} + +std::string native_status_message(const NativeModStatus status) { + switch (status) { + case NativeModStatus::BuildDisabled: + return "Code mods are disabled on this Dusklight build"; + case NativeModStatus::ModMissingPlatform: + return fmt::format("Mod not supported on this platform ({})", k_nativePlatform); + case NativeModStatus::ApiVersionMismatch: + // TODO: differentiate whether mod or Dusklight is out of date + return "Mod ABI version mismatch"; + case NativeModStatus::MissingExport: + return "Missing required mod API exports"; + case NativeModStatus::InvalidMetadata: + return "Invalid mod metadata records"; + case NativeModStatus::InvalidBundle: + return "Invalid mod bundle layout (old mod?)"; + case NativeModStatus::Unknown: + return "Unknown mod load failure"; + case NativeModStatus::None: + case NativeModStatus::Loaded: + break; + } + return "native mod failed to load"; +} + +} // namespace + +fs::path ModLoader::external_native_lib_path(const LoadedMod& mod) const { + if (k_nativeLibName.empty()) { + return {}; + } + const auto& libDir = m_searchDirs[mod.searchDirIndex].nativeLibDir; + if (libDir.empty()) { + return {}; + } + const auto filename = fmt::format("{}{}", mod.metadata.id, + borealis::io::fs_path_to_string(fs::path{k_nativeLibName}.extension())); + fs::path path = libDir / fs::path{filename}; + std::error_code ec; + if (!fs::is_regular_file(path, ec)) { + return {}; + } + return path; +} + +void ModLoader::load_native( + LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries) { + if (!EnableCodeMods) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build"); + mod.nativeStatus = NativeModStatus::BuildDisabled; + return; + } + + const fs::path cacheDir = m_cacheDir / mod.metadata.id; + const fs::path scratchDir = cacheDir / "data"; + std::error_code ec; + fs::create_directories(scratchDir, ec); + if (ec) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create mod directory {}: {}", + data::abbreviated_path_string(scratchDir), ec.message()); + return; + } + mod.dir = fs::absolute(scratchDir); + mod.dirUtf8 = borealis::io::fs_path_to_string(mod.dir); + + fs::path libPath; + fs::path runtimeDir; + DirectoryRollback runtimeDirRollback; + if (mod.nativeInPlace) { + if (!dllEntry.empty()) { + libPath = mod.modPath / dllEntry; + } else if (auto external = external_native_lib_path(mod); !external.empty()) { + libPath = std::move(external); + } else { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "no native library named {} found; skipping", k_nativeLibName); + mod.nativeStatus = NativeModStatus::ModMissingPlatform; + return; + } + runtimeDir = libPath.parent_path(); + } else { + if (dllEntry.empty()) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "no native library named {} found; skipping", k_nativeLibName); + mod.nativeStatus = NativeModStatus::ModMissingPlatform; + return; + } + + // Every generation gets a new directory. The main module and all of its runtime + // libraries therefore have fresh paths and can coexist with a previous generation + // that is still unwinding after a reload. + runtimeDir = cacheDir / fmt::format("g{}", ++mod.cacheGeneration); + runtimeDirRollback.set_path(runtimeDir); + fs::create_directories(runtimeDir, ec); + if (ec) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "failed to create native runtime directory {}: {}", + data::abbreviated_path_string(runtimeDir), ec.message()); + return; + } + + const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); + for (const auto& entry : runtimeEntries) { + if (!entry.starts_with(platformPrefix)) { + continue; + } + const std::string_view relativeName{ + entry.data() + platformPrefix.size(), entry.size() - platformPrefix.size()}; + if (!is_safe_resource_path(relativeName)) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "unsafe native runtime path '{}'; skipping", entry); + return; + } + + const fs::path outputPath = runtimeDir / fs::path{relativeName}; + fs::create_directories(outputPath.parent_path(), ec); + if (ec) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "failed to create directory for {}: {}", entry, ec.message()); + return; + } + + std::vector data; + try { + data = mod.bundle->readFile(entry); + } catch (const std::exception& e) { + log::write( + mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}: {}", entry, e.what()); + return; + } + + std::ofstream out(outputPath, std::ios::binary | std::ios::out); + if (!out) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); + return; + } + out.write(reinterpret_cast(data.data()), + static_cast(data.size())); + if (!out) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); + return; + } + } + + libPath = runtimeDir / fs::path{dllEntry}.filename(); + } + + auto nativeMod = std::make_unique(); + try { + nativeMod->handle = std::make_unique(libPath); + } catch (const std::runtime_error& e) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to open {}: {}", + data::abbreviated_path_string(libPath), e.what()); + return; + } + + nativeMod->meta = nativeMod->handle->LookupSymbol("mod_meta"); + nativeMod->contextSymbol = nativeMod->handle->LookupSymbol("mod_ctx"); + nativeMod->fn_initialize = nativeMod->handle->LookupSymbol("mod_initialize"); + nativeMod->fn_update = nativeMod->handle->LookupSymbol("mod_update"); + nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol("mod_shutdown"); + + if (!nativeMod->meta || !nativeMod->contextSymbol || !nativeMod->fn_initialize || + !nativeMod->fn_update || !nativeMod->fn_shutdown) + { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "{} missing required mod API exports; skipping", + data::abbreviated_path_string(libPath)); + mod.nativeStatus = NativeModStatus::MissingExport; + return; + } + + if (!parse_meta(*nativeMod, mod)) { + return; + } + + if (nativeMod->contextSymbol == nullptr) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); + mod.nativeStatus = NativeModStatus::MissingExport; + return; + } + *nativeMod->contextSymbol = mod.context.get(); + + mod.nativePath = fs::absolute(libPath); + mod.nativeDir = fs::absolute(runtimeDir); + mod.nativeDirUtf8 = borealis::io::fs_path_to_string(mod.nativeDir); + mod.native = std::move(nativeMod); + mod.nativeStatus = NativeModStatus::Loaded; + runtimeDirRollback.release(); +} + +bool ModLoader::load_native_if_present(LoadedMod& mod) { + const auto result = locate_native_runtime(*mod.bundle); + if (const auto* failure = std::get_if(&result)) { + mod.nativeStatus = failure->status; + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{}", failure->logMessage); + fail_mod(mod, MOD_ERROR, native_status_message(failure->status)); + return false; + } + + const auto& native = std::get(result); + if (mod.runtime.has_value() && + (native.anyLibs || (mod.nativeInPlace && !external_native_lib_path(mod).empty()))) + { + mod.nativeStatus = NativeModStatus::InvalidBundle; + fail_mod(mod, MOD_CONFLICT, "A mod cannot declare both runtime and native code"); + return false; + } + if (!native.anyLibs && !(mod.nativeInPlace && !external_native_lib_path(mod).empty())) { + mod.nativeStatus = NativeModStatus::None; + return true; + } + + mod.nativeStatus = NativeModStatus::Unknown; + load_native(mod, native.entry, native.runtimeEntries); + if (mod.nativeStatus != NativeModStatus::Loaded) { + fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); + return false; + } + return true; +} + +void ModLoader::unload_native(LoadedMod& mod) { + if (!mod.native) { + return; + } + // Deferred dlclose: this mod's code may still be on the stack below the current tick + m_retiredNatives.push_back( + {std::move(mod.native), mod.nativeInPlace ? fs::path{} : std::move(mod.nativeDir)}); + mod.nativePath.clear(); + mod.nativeDir.clear(); + mod.nativeDirUtf8.clear(); +} + +void ModLoader::drain_retired_natives() { + for (auto& retired : m_retiredNatives) { + retired.native.reset(); + if (!retired.directory.empty()) { + std::error_code ec; + fs::remove_all(retired.directory, ec); + } + } + m_retiredNatives.clear(); +} + +bool ModLoader::ensure_native_loaded(LoadedMod& mod) { + if (mod.native || mod.nativeStatus == NativeModStatus::None) { + return true; + } + return load_native_if_present(mod); +} + +ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) { + ModManifestInfo info; + info.imports.reserve(parsed.imports.size()); + for (const auto* record : parsed.imports) { + if (!svc::valid_service_id(record->service_id.chars)) { + continue; + } + info.imports.push_back({ + .id = record->service_id.chars, + .major = record->major_version, + .minMinor = record->min_minor_version, + .required = (record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0, + }); + } + info.exports.reserve(parsed.exports.size()); + for (const auto* record : parsed.exports) { + if (!svc::valid_service_id(record->service_id.chars)) { + continue; + } + info.exports.push_back({ + .id = record->service_id.chars, + .major = record->major_version, + }); + } + return info; +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/natives.hpp b/src/dusk/mods/loader/natives.hpp new file mode 100644 index 0000000000..657f3d011a --- /dev/null +++ b/src/dusk/mods/loader/natives.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace dusk::mods { + +struct ModManifestInfo; +struct ModMetaParsed; + +bool has_native_library_extension(std::string_view name); +ModManifestInfo build_manifest_info(const ModMetaParsed& parsed); + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/packages.cpp b/src/dusk/mods/loader/packages.cpp new file mode 100644 index 0000000000..071f5b276e --- /dev/null +++ b/src/dusk/mods/loader/packages.cpp @@ -0,0 +1,201 @@ +#include "packages.hpp" + +#include "loader.hpp" +#include "manifest.hpp" + +#include "dusk/data.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +namespace fs = std::filesystem; + +namespace dusk::mods { +namespace { + +constexpr borealis::Log Log{"dusk::mods::loader"}; + +} // namespace + +std::unique_ptr load_bundle(const fs::path& modPath, bool fromDir) { + if (fromDir) { + return std::make_unique(modPath); + } else { + return std::make_unique(modPath); + } +} + +int compare_package_versions(std::string_view lhs, std::string_view rhs) { + const auto left = borealis::update::parse_version(lhs); + const auto right = borealis::update::parse_version(rhs); + if (left && right) { + return borealis::update::compare_version(*left, *right); + } + return static_cast(left.has_value()) - static_cast(right.has_value()); +} + +std::vector scan_packages(std::span searchDirs) { + std::vector packages; + for (size_t dirIndex = 0; dirIndex < searchDirs.size(); ++dirIndex) { + const auto& searchDir = searchDirs[dirIndex]; + std::error_code error; + bool alreadyScanned = false; + for (size_t earlier = 0; earlier < dirIndex && !alreadyScanned; ++earlier) { + alreadyScanned = fs::equivalent(searchDirs[earlier].path, searchDir.path, error); + } + if (alreadyScanned || !fs::is_directory(searchDir.path)) { + continue; + } + std::vector entries; + for (const auto& entry : fs::directory_iterator{searchDir.path}) { + if ((entry.is_directory() && fs::exists(entry.path() / "mod.json")) || + (entry.is_regular_file() && entry.path().extension() == ".dusk")) + { + entries.push_back(entry); + } + } + std::ranges::sort(entries, {}, &fs::directory_entry::path); + for (const auto& entry : entries) { + try { + const bool fromDirectory = entry.is_directory(); + auto bundle = load_bundle(entry.path(), fromDirectory); + packages.push_back({ + .path = fs::absolute(entry.path()), + .metadata = load_manifest(entry.path(), *bundle).metadata, + .searchDirIndex = static_cast(dirIndex), + .fromDirectory = fromDirectory, + .symlink = entry.is_symlink(), + }); + } catch (const std::exception& exception) { + Log.error("bad mod package {}: {}", + data::abbreviated_path_string(entry.path()), exception.what()); + } + } + } + return packages; +} + +const PackageCandidate* select_package(std::span packages, + std::string_view modId) { + const PackageCandidate* selected = nullptr; + for (const auto& package : packages) { + if (package.metadata.id != modId) { + continue; + } + const int order = selected ? + compare_package_versions(package.metadata.version, selected->metadata.version) : 1; + if (order > 0 || (order == 0 && + (package.searchDirIndex < selected->searchDirIndex || + (package.searchDirIndex == selected->searchDirIndex && package.path < selected->path)))) + { + selected = &package; + } + } + return selected; +} + +void record_package_sources(LoadedMod& mod, std::span packages) { + mod.hasUserPackage = false; + mod.hasBundledCopy = false; + for (const auto& package : packages) { + if (package.metadata.id != mod.metadata.id) { + continue; + } + if (package.searchDirIndex != 0) { + mod.hasBundledCopy = true; + } else if (!package.fromDirectory && !package.symlink) { + mod.hasUserPackage = true; + } + } +} + +PackageInstallResult install_package(const fs::path& path, const fs::path& destination, + std::string_view modId) { + const auto userDir = destination.parent_path(); + std::error_code error; + std::string validationError; + const auto destinationStatus = fs::symlink_status(destination, error); + if (error == std::errc::no_such_file_or_directory) { + error.clear(); + } + if (error) { + return {.replaced = false, + .error = fmt::format("Could not inspect the destination: {}", error.message())}; + } + if (fs::exists(destinationStatus)) { + ModMetadata existingMetadata; + if (!fs::is_regular_file(destinationStatus) || + !inspect_mod_bundle(destination, existingMetadata, validationError) || + existingMetadata.id != modId) + { + return { + .replaced = false, + .error = "The destination filename is already used by another package", + }; + } + } + std::vector duplicates; + for (fs::directory_iterator entry{userDir, error}, end; !error && entry != end; + entry.increment(error)) + { + const auto& candidate = entry->path(); + if (candidate.extension() != ".dusk") { + continue; + } + const bool regularFile = entry->is_regular_file(error); + if (error) { + break; + } + if (!regularFile) { + continue; + } + ModMetadata candidateMetadata; + if (inspect_mod_bundle(candidate, candidateMetadata, validationError) && + candidateMetadata.id == modId) + { + duplicates.push_back(candidate); + } + } + if (error) { + return {.replaced = false, + .error = fmt::format("Could not scan the mods directory: {}", error.message())}; + } + + std::string replaceError; + if (!borealis::io::atomic_replace(path, destination, replaceError)) { + return { + .replaced = false, + .error = std::move(replaceError), + }; + } + + std::string cleanupError; + for (const auto& duplicate : duplicates) { + // Replacing a file on a case-insensitive filesystem can preserve its old spelling. + const bool destinationAlias = + fs::equivalent(duplicate, destination, error) && !fs::is_symlink(duplicate, error); + error.clear(); + if (destinationAlias) { + if (duplicate.filename() == destination.filename()) { + continue; + } + fs::rename(duplicate, destination, error); + } else { + fs::remove(duplicate, error); + } + if (error) { + cleanupError = fmt::format("Installed, but could not consolidate '{}': {}", + data::abbreviated_path_string(duplicate), error.message()); + Log.warn("{}", cleanupError); + } + } + return {.replaced = true, .error = std::move(cleanupError)}; +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/packages.hpp b/src/dusk/mods/loader/packages.hpp new file mode 100644 index 0000000000..0271dca5fe --- /dev/null +++ b/src/dusk/mods/loader/packages.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "dusk/mod_loader.hpp" + +#include +#include +#include +#include +#include +#include + +namespace dusk::mods { + +struct PackageCandidate { + std::filesystem::path path; + ModMetadata metadata; + uint32_t searchDirIndex = 0; + bool fromDirectory = false; + bool symlink = false; +}; + +std::vector scan_packages(std::span searchDirs); +int compare_package_versions(std::string_view lhs, std::string_view rhs); +const PackageCandidate* select_package( + std::span packages, std::string_view modId); +void record_package_sources(LoadedMod& mod, std::span packages); + +std::unique_ptr load_bundle(const std::filesystem::path& modPath, bool fromDir); + +struct PackageInstallResult { + bool replaced = false; + std::string error; +}; + +PackageInstallResult install_package(const std::filesystem::path& path, + const std::filesystem::path& destination, std::string_view modId); + +} // namespace dusk::mods diff --git a/src/dusk/mods/path.hpp b/src/dusk/mods/path.hpp new file mode 100644 index 0000000000..4213c3fbf6 --- /dev/null +++ b/src/dusk/mods/path.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace dusk::mods { + +inline std::string safe_filename(std::string_view value) { + std::string result{value}; + std::ranges::replace_if( + result, + [](char character) { + return !((character >= 'a' && character <= 'z') || + (character >= 'A' && character <= 'Z') || + (character >= '0' && character <= '9') || character == '.' || + character == '_' || character == '-'); + }, + '_'); + return result; +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/queue.cpp b/src/dusk/mods/queue.cpp new file mode 100644 index 0000000000..c092fb5ad5 --- /dev/null +++ b/src/dusk/mods/queue.cpp @@ -0,0 +1,782 @@ +#include "queue.hpp" +#include "updates.hpp" + +#include "dusk/hash.hpp" +#include "dusk/mod_loader.hpp" +#include "dusk/mods/path.hpp" +#include "dusk/ui/ui.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::mods::queue { +namespace { + +using clock = std::chrono::steady_clock; + +struct VerifyResult { + std::string error; + ModMetadata metadata; + std::filesystem::path stagedPath; + bool canceled = false; +}; + +enum class PendingIntent { None, Pause, Cancel }; + +struct QueueItem { + std::string key; + Request request; + std::string previousVersion; + State state = State::Queued; + std::filesystem::path partialPath; + uint64_t completed = 0; + uint64_t total = 0; + std::string message; + int retryCount = 0; + clock::time_point retryAt{}; + borealis::Task task; + borealis::Task verification; + ModOperationHandle operation; + PendingIntent pendingIntent = PendingIntent::None; +}; + +std::vector queueItems; +uint64_t nextQueueKey = 1; + +QueueItem* find_queue_item(std::string_view key) { + const auto item = std::ranges::find(queueItems, key, + [](const QueueItem& candidate) { return std::string_view{candidate.key}; }); + return item == queueItems.end() ? nullptr : &*item; +} + +QueueItem* find_queue_item_by_mod_id(std::string_view id) { + const auto item = std::ranges::find(queueItems, id, + [](const QueueItem& candidate) { return std::string_view{candidate.request.id}; }); + return item == queueItems.end() ? nullptr : &*item; +} + +const Download* url_source(const QueueItem& item) { + return std::get_if(&item.request.source); +} + +const LocalFile* local_source(const QueueItem& item) { + return std::get_if(&item.request.source); +} + +std::string lowercase(std::string value) { + std::ranges::transform(value, value.begin(), [](char character) { + return character >= 'A' && character <= 'Z' ? static_cast(character + ('a' - 'A')) : + character; + }); + return value; +} + +bool valid_sha256(std::string_view value) { + return value.size() == 64 && std::ranges::all_of(value, [](char character) { + return (character >= '0' && character <= '9') || (character >= 'a' && character <= 'f') || + (character >= 'A' && character <= 'F'); + }); +} + +std::string sha256_file( + const std::filesystem::path& path, borealis::TaskContext& context, std::string& error) { + std::ifstream input{path, std::ios::binary}; + if (!input) { + error = "Could not open the downloaded package"; + return {}; + } + + hash::Sha256 hash; + std::array buffer{}; + uint64_t completed = 0; + while (input) { + if (context.cancel_requested()) { + error = "Canceled"; + return {}; + } + input.read(reinterpret_cast(buffer.data()), buffer.size()); + const auto count = input.gcount(); + if (count > 0) { + hash.update(std::span{buffer.data(), static_cast(count)}); + completed += static_cast(count); + context.report_progress(completed); + } + } + if (!input.eof()) { + error = "Could not read the downloaded package"; + return {}; + } + + return hash.finish(); +} + +std::filesystem::path staging_path( + const std::filesystem::path& stagingDir, std::string_view modId, std::string_view key) { + return stagingDir / fmt::format("{}-{}.dusk.part", safe_filename(modId), safe_filename(key)); +} + +bool copy_to_staging(const std::filesystem::path& source, const std::filesystem::path& destination, + uint64_t total, borealis::TaskContext& context, std::string& error) { + std::error_code filesystemError; + std::filesystem::create_directories(destination.parent_path(), filesystemError); + if (filesystemError) { + error = + fmt::format("Could not create the staging directory: {}", filesystemError.message()); + return false; + } + std::ifstream input{source, std::ios::binary}; + std::ofstream output{destination, std::ios::binary | std::ios::trunc}; + if (!input || !output) { + error = "Could not stage the local package"; + return false; + } + std::array buffer{}; + uint64_t completed = 0; + while (input) { + if (context.cancel_requested()) { + error = "Canceled"; + output.close(); + std::filesystem::remove(destination, filesystemError); + return false; + } + input.read(buffer.data(), buffer.size()); + const auto count = input.gcount(); + if (count > 0) { + output.write(buffer.data(), count); + completed += static_cast(count); + context.report_progress(completed, total); + } + } + output.close(); + if (!input.eof() || !output) { + error = "Could not copy the local package"; + std::filesystem::remove(destination, filesystemError); + return false; + } + return true; +} + +VerifyResult verify_url_package(const std::filesystem::path& path, const Request& request, + const Download& source, const std::filesystem::path& stagingDir, std::string key, + borealis::TaskContext& context) { + std::error_code ec; + const auto actualSize = std::filesystem::file_size(path, ec); + if (ec) { + return {.error = fmt::format("Could not read the downloaded package: {}", ec.message())}; + } + if (actualSize != source.size) { + return {.error = "Package size mismatch"}; + } + + std::string error; + const auto actualHash = sha256_file(path, context, error); + if (!error.empty()) { + return {.error = std::move(error)}; + } + if (context.cancel_requested()) { + return {.canceled = true}; + } + if (actualHash != lowercase(source.sha256)) { + return {.error = "Package checksum mismatch"}; + } + + ModMetadata metadata; + if (!inspect_mod_bundle(path, metadata, error)) { + return {.error = fmt::format("Invalid mod package: {}", error)}; + } + if (metadata.id != request.id) { + return {.error = "Package ID does not match the catalog entry"}; + } + if (metadata.version != request.version) { + return {.error = "Package version does not match the catalog entry"}; + } + if (context.cancel_requested()) { + return {.canceled = true}; + } + const auto stagedPath = staging_path(stagingDir, metadata.id, key); + std::filesystem::create_directories(stagedPath.parent_path(), ec); + if (ec) { + return {.error = fmt::format("Could not create the staging directory: {}", ec.message())}; + } + std::string replaceError; + if (!borealis::io::atomic_replace(path, stagedPath, replaceError)) { + return {.error = std::move(replaceError)}; + } + return {.metadata = std::move(metadata), .stagedPath = stagedPath}; +} + +VerifyResult verify_local_package(const LocalFile& source, const std::filesystem::path& stagingDir, + std::string key, borealis::TaskContext& context) { + std::error_code ec; + const auto size = std::filesystem::file_size(source.path, ec); + if (ec) { + return {.error = fmt::format("Could not read the local package: {}", ec.message())}; + } + context.report_progress(0, size); + const auto stagedPath = stagingDir / fmt::format("{}.dusk.part", key); + std::string error; + if (!copy_to_staging(source.path, stagedPath, size, context, error)) { + return {.error = std::move(error), .canceled = context.cancel_requested()}; + } + // Validate the bytes handed to the loader; the source may change during copying. + ModMetadata metadata; + if (!inspect_mod_bundle(stagedPath, metadata, error)) { + std::filesystem::remove(stagedPath, ec); + return {.error = fmt::format("Invalid mod package: {}", error)}; + } + return {.metadata = std::move(metadata), .stagedPath = stagedPath}; +} + +void remove_partial(const QueueItem& item) { + std::error_code ec; + if (!item.partialPath.empty()) { + std::filesystem::remove(item.partialPath, ec); + auto metadataPath = item.partialPath; + metadataPath += ".borealis-resume.json"; + std::filesystem::remove(metadataPath, ec); + } +} + +void fail(QueueItem& item, std::string message, bool discardPartial) { + item.task = {}; + item.verification = {}; + item.state = State::Failed; + item.message = std::move(message); + item.pendingIntent = PendingIntent::None; + if (discardPartial) { + remove_partial(item); + item.completed = 0; + } + const char* title = + local_source(item) != nullptr ? "Mod package failed" : "Mod download failed"; + ui::push_toast({ + .type = "warning", + .title = title, + .content = fmt::format("{}: {}", item.request.name, item.message), + .duration = std::chrono::seconds{6}, + }); +} + +bool retryable(const borealis::http::Result& result) { + if (result.error == borealis::http::Error::Network || + result.error == borealis::http::Error::Timeout) + { + return true; + } + const int status = result.response.statusCode; + return result.error == borealis::http::Error::None && + (status == 408 || status == 425 || status == 429 || status >= 500); +} + +void schedule_retry(QueueItem& item, std::string message) { + ++item.retryCount; + const int delaySeconds = std::min(30, 1 << std::min(item.retryCount, 4)); + item.retryAt = clock::now() + std::chrono::seconds{delaySeconds}; + item.state = State::Retrying; + item.message = std::move(message); + item.task = {}; +} + +void start_download(QueueItem& item) { + if (item.request.update) { + if (auto error = updates::validate(*item.request.update); !error.empty()) { + fail(item, std::move(error), false); + return; + } + } + const auto* source = url_source(item); + if (source == nullptr) { + fail(item, "The install source is not a URL", false); + return; + } + const auto userDir = ModLoader::instance().user_mods_dir(); + if (userDir.empty()) { + fail(item, "No writable mods directory is configured", false); + return; + } + + item.partialPath = + userDir / ".downloads" / fmt::format("{}.dusk.part", safe_filename(item.request.id)); + std::error_code ec; + std::filesystem::create_directories(item.partialPath.parent_path(), ec); + if (ec) { + fail(item, fmt::format("Could not create the download directory: {}", ec.message()), false); + return; + } + + item.pendingIntent = PendingIntent::None; + item.message.clear(); + item.total = source->size; + item.state = State::Downloading; + item.task = borealis::http::start({ + .url = source->url, + .downloadTo = item.partialPath, + .connectTimeout = std::chrono::seconds{10}, + .idleTimeout = std::chrono::seconds{15}, + .totalTimeout = std::nullopt, + }); +} + +void start_local_verification(QueueItem& item) { + const auto* source = local_source(item); + if (source == nullptr) { + fail(item, "The install source is not a local file", false); + return; + } + const auto userDir = ModLoader::instance().user_mods_dir(); + if (userDir.empty()) { + fail(item, "No writable mods directory is configured", false); + return; + } + item.state = State::Verifying; + item.message.clear(); + item.completed = 0; + std::error_code error; + item.total = std::filesystem::file_size(source->path, error); + const auto stagingDir = userDir / ".staging"; + const auto local = *source; + item.verification = + borealis::spawn([local, stagingDir, key = item.key](borealis::TaskContext& context) { + return verify_local_package(local, stagingDir, key, context); + }); +} + +void finish_download(QueueItem& item) { + const auto progress = item.task.progress(); + item.completed = std::max(item.completed, progress.completed); + + std::optional completed; + std::string taskError; + bool taskFailed = false; + try { + completed = item.task.try_take(); + } catch (const std::exception& exception) { + taskError = exception.what(); + taskFailed = true; + } catch (...) { + taskError = "The download failed"; + taskFailed = true; + } + if (!completed && !taskFailed) { + return; + } + item.task = {}; + + switch (std::exchange(item.pendingIntent, PendingIntent::None)) { + case PendingIntent::Cancel: + remove_partial(item); + item.completed = 0; + item.state = State::Canceled; + return; + case PendingIntent::Pause: + return; + case PendingIntent::None: + break; + } + + if (taskFailed) { + schedule_retry(item, std::move(taskError)); + return; + } + + if (completed->error != borealis::http::Error::None || completed->response.statusCode < 200 || + completed->response.statusCode >= 300) + { + const auto message = !completed->message.empty() ? completed->message : + completed->response.statusCode != 0 ? + fmt::format("Server returned HTTP {}", + completed->response.statusCode) : + "The download failed"; + if (retryable(*completed)) { + schedule_retry(item, message); + } else { + fail(item, message, true); + } + return; + } + + const auto* source = url_source(item); + if (source == nullptr) { + fail(item, "The install source changed", true); + return; + } + item.completed = source->size; + item.state = State::Verifying; + item.message.clear(); + const auto stagingDir = ModLoader::instance().user_mods_dir() / ".staging"; + item.verification = + borealis::spawn([path = item.partialPath, request = item.request, source = *source, + stagingDir, key = item.key](borealis::TaskContext& context) { + return verify_url_package(path, request, source, stagingDir, key, context); + }); +} + +void finish_verification(QueueItem& item) { + VerifyResult result; + try { + auto completed = item.verification.try_take(); + if (!completed) { + return; + } + result = std::move(*completed); + } catch (const std::exception& exception) { + result.error = exception.what(); + } catch (...) { + result.error = "Package verification failed"; + } + item.verification = {}; + if (result.canceled || item.pendingIntent == PendingIntent::Cancel) { + item.pendingIntent = PendingIntent::None; + if (!result.stagedPath.empty()) { + std::error_code error; + std::filesystem::remove(result.stagedPath, error); + } + remove_partial(item); + item.state = State::Canceled; + item.completed = 0; + return; + } + if (!result.error.empty()) { + fail(item, std::move(result.error), true); + return; + } + remove_partial(item); + item.partialPath = result.stagedPath; + if (const auto duplicate = find_queue_item_by_mod_id(result.metadata.id); + duplicate != nullptr && duplicate != &item && !is_terminal(duplicate->state)) + { + fail(item, "This mod already has an active install", true); + return; + } + if (local_source(item) != nullptr && !item.request.id.empty() && + (item.request.id != result.metadata.id || item.request.version != result.metadata.version)) + { + fail(item, "The local package changed after confirmation", true); + return; + } + if (item.request.update) { + if (auto error = updates::validate(*item.request.update); !error.empty()) { + fail(item, std::move(error), true); + return; + } + } + item.request.id = result.metadata.id; + item.request.name = result.metadata.name; + item.request.version = result.metadata.version; + item.completed = item.total; + item.state = State::Handoff; + item.operation = + ModLoader::instance().request_install(std::move(result.stagedPath), item.request.update); +} + +Item snapshot(const QueueItem& item) { + Item result{ + .id = item.key, + .modId = item.request.id, + .name = item.request.name, + .version = item.request.version, + .previousVersion = item.previousVersion, + .state = item.state, + .completed = item.completed, + .total = item.total, + .message = item.message, + .local = local_source(item) != nullptr, + .icon = item.request.icon, + }; + if (item.task) { + result.completed = std::max(result.completed, item.task.progress().completed); + } + if (item.verification) { + const auto progress = item.verification.progress(); + result.completed = progress.completed; + if (progress.total) { + result.total = *progress.total; + } + } + if (item.state == State::Retrying) { + const auto remaining = item.retryAt - clock::now(); + result.retrySeconds = std::max( + 0, static_cast(std::chrono::ceil(remaining).count())); + } + return result; +} + +} // namespace + +bool enqueue(Request request, std::string* keyOut) { + const auto* source = std::get_if(&request.source); + const auto* local = std::get_if(&request.source); + if (source != nullptr) { + if (request.id.empty() || request.version.empty() || source->size == 0 || + !source->url.starts_with("https://") || !valid_sha256(source->sha256)) + { + return false; + } + } else if (local == nullptr || request.id.empty() || request.version.empty()) { + return false; + } + const auto total = source == nullptr ? 0 : source->size; + if (request.name.empty()) { + request.name = request.id; + } + + std::string previousVersion; + if (const auto* installed = ModLoader::instance().find_mod(request.id); + installed && installed->metadata.version != request.version) + { + previousVersion = installed->metadata.version; + } + if (auto* existing = find_queue_item_by_mod_id(request.id)) { + if (!is_terminal(existing->state)) { + return false; + } + existing->request = std::move(request); + existing->previousVersion = std::move(previousVersion); + existing->state = State::Queued; + existing->completed = 0; + existing->total = total; + existing->partialPath.clear(); + existing->message.clear(); + existing->retryCount = 0; + existing->operation.reset(); + existing->pendingIntent = PendingIntent::None; + if (keyOut != nullptr) { + *keyOut = existing->key; + } + return true; + } + + const auto key = fmt::format("queue-{}", nextQueueKey++); + if (keyOut != nullptr) { + *keyOut = key; + } + queueItems.push_back({ + .key = key, + .request = std::move(request), + .previousVersion = std::move(previousVersion), + .total = total, + }); + return true; +} + +void update() { + for (auto item = queueItems.begin(); item != queueItems.end();) { + if (item->task && item->task.ready()) { + finish_download(*item); + } + if (item->state == State::Verifying && item->verification && item->verification.ready()) { + finish_verification(*item); + } + if (item->state == State::Handoff && item->operation && + item->operation->state != ModOperation::State::Pending) + { + item->message = item->operation->message; + item->state = item->operation->state == ModOperation::State::Succeeded ? + State::Installed : + State::InstallFailed; + item->operation.reset(); + } + ++item; + } + + for (auto& item : queueItems) { + if (item.task || item.verification || item.operation) { + return; + } + if (is_terminal(item.state) || item.state == State::Paused) { + continue; + } + if (item.state == State::Downloading || item.state == State::Verifying) { + return; + } + if (item.state == State::Retrying && clock::now() < item.retryAt) { + return; + } + if (item.state == State::Queued || item.state == State::Retrying) { + if (local_source(item) != nullptr) { + start_local_verification(item); + } else { + start_download(item); + } + } + return; + } +} + +void shutdown() noexcept { + for (auto& item : queueItems) { + if (item.task) { + item.task.cancel(); + } + if (item.verification) { + item.verification.cancel(); + } + } + queueItems.clear(); +} + +std::vector items() { + std::vector result; + result.reserve(queueItems.size()); + for (const auto& item : queueItems) { + result.push_back(snapshot(item)); + } + return result; +} + +std::optional find(std::string_view id) { + const auto* item = find_queue_item(id); + return item == nullptr ? std::nullopt : std::optional{snapshot(*item)}; +} + +std::optional find_by_mod_id(std::string_view id) { + const auto* item = find_queue_item_by_mod_id(id); + return item == nullptr ? std::nullopt : std::optional{snapshot(*item)}; +} + +bool has_active_items() { + return std::ranges::any_of( + queueItems, [](const QueueItem& item) { return !is_terminal(item.state); }); +} + +size_t item_count() noexcept { + return queueItems.size(); +} + +size_t active_items_ahead(std::string_view id) noexcept { + size_t result = 0; + for (const auto& item : queueItems) { + if (item.request.id == id) { + break; + } + if (!is_terminal(item.state)) { + ++result; + } + } + return result; +} + +void pause(std::string_view id) { + auto* item = find_queue_item(id); + if (item == nullptr || local_source(*item) != nullptr || + item->pendingIntent == PendingIntent::Cancel) + { + return; + } + if (item->state == State::Queued || item->state == State::Retrying) { + item->state = State::Paused; + return; + } + if (item->state == State::Downloading && item->task) { + item->completed = std::max(item->completed, item->task.progress().completed); + item->pendingIntent = PendingIntent::Pause; + item->state = State::Paused; + item->task.cancel(); + } +} + +void resume(std::string_view id) { + auto* item = find_queue_item(id); + if (item == nullptr || item->state != State::Paused || + item->pendingIntent == PendingIntent::Cancel) + { + return; + } + item->state = State::Queued; +} + +void retry(std::string_view id) { + auto* item = find_queue_item(id); + if (item == nullptr) { + return; + } + if (item->state == State::InstallFailed) { + auto* mod = ModLoader::instance().find_mod(item->request.id); + if (mod != nullptr) { + if (mod->activation_failed()) { + item->message.clear(); + item->state = State::Handoff; + item->operation = ModLoader::instance().request_reactivate(item->request.id); + } else { + item->message.clear(); + item->state = State::Installed; + } + return; + } + } else if (item->state != State::Failed) { + return; + } + remove_partial(*item); + item->completed = 0; + item->retryCount = 0; + item->message.clear(); + item->state = State::Queued; +} + +void cancel(std::string_view id) { + auto* item = find_queue_item(id); + if (item == nullptr || item->state == State::Handoff || is_terminal(item->state)) { + return; + } + if (item->task) { + item->pendingIntent = PendingIntent::Cancel; + item->message = "Canceling..."; + item->task.cancel(); + return; + } + if (item->verification) { + item->pendingIntent = PendingIntent::Cancel; + item->message = "Canceling..."; + item->verification.cancel(); + return; + } + remove_partial(*item); + item->pendingIntent = PendingIntent::None; + item->completed = 0; + item->state = State::Canceled; +} + +void clear(std::string_view id) { + const auto item = std::ranges::find( + queueItems, id, [](const QueueItem& candidate) { return std::string_view{candidate.key}; }); + if (item != queueItems.end() && is_terminal(item->state)) { + queueItems.erase(item); + } +} + +void remove_by_mod_id(std::string_view id) { + std::erase_if(queueItems, + [id](const QueueItem& item) { return std::string_view{item.request.id} == id; }); +} + +void pause_all() { + std::vector ids; + for (const auto& item : queueItems) { + if (item.state == State::Queued || item.state == State::Retrying || + item.state == State::Downloading) + { + ids.push_back(item.key); + } + } + for (const auto& id : ids) { + pause(id); + } +} + +} // namespace dusk::mods::queue diff --git a/src/dusk/mods/queue.hpp b/src/dusk/mods/queue.hpp new file mode 100644 index 0000000000..20c047d8cd --- /dev/null +++ b/src/dusk/mods/queue.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include "updates.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::mods::queue { + +enum class State { + Queued, + Downloading, + Paused, + Retrying, + Verifying, + Handoff, + Installed, + InstallFailed, + Failed, + Canceled, +}; + +[[nodiscard]] constexpr bool is_failed(State state) noexcept { + return state == State::InstallFailed || state == State::Failed; +} + +[[nodiscard]] constexpr bool is_completed(State state) noexcept { + return state == State::Installed || state == State::Canceled; +} + +[[nodiscard]] constexpr bool is_terminal(State state) noexcept { + return is_completed(state) || is_failed(state); +} + +[[nodiscard]] constexpr bool is_install_result(State state) noexcept { + return state == State::Installed || state == State::InstallFailed; +} + +struct LocalFile { + std::filesystem::path path; +}; + +using Source = std::variant; + +struct Icon { + std::string url; + uint32_t width = 0; + uint32_t height = 0; +}; + +struct Request { + std::string id; + std::string name; + std::string version; + Source source; + std::optional icon; + std::optional update; +}; + +struct Item { + // Queue key, independent of the mod ID. + std::string id; + std::string modId; + std::string name; + std::string version; + std::string previousVersion; + State state = State::Queued; + uint64_t completed = 0; + uint64_t total = 0; + std::string message; + int retrySeconds = 0; + bool local = false; + std::optional icon; +}; + +/** Adds an install, replacing failed or canceled work for the same package ID. */ +bool enqueue(Request request, std::string* key = nullptr); + +void update(); +void shutdown() noexcept; + +[[nodiscard]] std::vector items(); +[[nodiscard]] std::optional find(std::string_view key); +[[nodiscard]] std::optional find_by_mod_id(std::string_view id); +[[nodiscard]] bool has_active_items(); +[[nodiscard]] size_t item_count() noexcept; +[[nodiscard]] size_t active_items_ahead(std::string_view id) noexcept; + +void pause(std::string_view id); +void resume(std::string_view id); +void retry(std::string_view id); +void cancel(std::string_view id); +void clear(std::string_view id); +void remove_by_mod_id(std::string_view id); +void pause_all(); + +} // namespace dusk::mods::queue diff --git a/src/dusk/mods/services.hpp b/src/dusk/mods/services.hpp new file mode 100644 index 0000000000..57a9157d8e --- /dev/null +++ b/src/dusk/mods/services.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +namespace dusk::mods { + +struct ServiceImport { + std::string id; + uint16_t major = 0; + uint16_t minMinor = 0; + bool optional = false; + bool operator==(const ServiceImport&) const = default; +}; + +struct ServiceExport { + std::string id; + uint16_t major = 0; + uint16_t minor = 0; + std::string providerId; + bool operator==(const ServiceExport&) const = default; +}; + +inline std::string service_key(std::string_view id, uint16_t major) { + return std::string{id} + '\x1f' + std::to_string(major); +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/svc/audio_res/audio_res.cpp b/src/dusk/mods/svc/audio_res/audio_res.cpp new file mode 100644 index 0000000000..3735408106 --- /dev/null +++ b/src/dusk/mods/svc/audio_res/audio_res.cpp @@ -0,0 +1,56 @@ +#include "audio_res.hpp" + +#include "helpers/cast.hpp" +#include "mods/svc/audio_res.h" + +#include "../registry.hpp" + +namespace dusk::mods::svc { + +namespace audio_res { +namespace { + +using namespace dusk::helpers::cast; + +constexpr AudioResService s_audioResService{ + .header = SERVICE_HEADER(AudioResService, AUDIO_RES_SERVICE_MAJOR, AUDIO_RES_SERVICE_MINOR), + .default_wave_info = &wsys::default_wave_info, + .replace_wave = &wsys::insert_replace_wave, + .add_wave = &wsys::insert_add_wave, + .remove_wave = &wsys::remove_wave, + .default_effect_info = &bst::default_effect_info, + .replace_sound_table_effect = &bst::replace_sound_table_effect, + .add_sound_table_effect = &bst::add_sound_table_effect, + .default_stream_info = &bst::default_stream_info, + .replace_sound_table_stream = &bst::replace_sound_table_stream, + .add_sound_table_stream = &bst::add_sound_table_stream, + .remove_sound_table = &bst::remove_sound_table, +}; + +void frame_end() { + wsys::frame_end(); + bst::frame_end(); +} + +void mod_detached(LoadedMod& mod) { + wsys::remove_mod(mod); + bst::remove_mod(mod); +} + +void sync_audio_replacements() { + wsys::sync_audio_replacements(); + bst::sync_audio_replacements(); +} + +} // namespace + +} // namespace audio_res + +constinit const ServiceModule g_audioResModule{.id = AUDIO_RES_SERVICE_ID, + .majorVersion = AUDIO_RES_SERVICE_MAJOR, + .minorVersion = AUDIO_RES_SERVICE_MINOR, + .service = &audio_res::s_audioResService, + .modDetached = audio_res::mod_detached, + .lifecycleApplied = audio_res::sync_audio_replacements, + .frameEnd = audio_res::frame_end}; +}; // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/audio_res/audio_res.hpp b/src/dusk/mods/svc/audio_res/audio_res.hpp new file mode 100644 index 0000000000..6e7b04e47b --- /dev/null +++ b/src/dusk/mods/svc/audio_res/audio_res.hpp @@ -0,0 +1,56 @@ +#pragma once +#include "mods/svc/audio_res.h" + +namespace dusk::mods { +struct LoadedMod; +} + +namespace dusk::mods::svc::audio_res { + +namespace wsys { + +void frame_end(); +void remove_mod(LoadedMod& mod); +void sync_audio_replacements(); + +extern AudioWaveInfo const default_wave_info; + +ModResult insert_replace_wave(ModContext* ctx, AudioWaveBank bank, u16 wave_id, + char const* file_name, AudioWaveInfo const* wave_info, AudioWaveHandle* out_handle); + +ModResult insert_add_wave(ModContext* ctx, AudioWaveBank bank, char const* file_name, + AudioWaveInfo const* wave_info, AudioWaveHandle* out_handle, u16* out_wave_id); + +ModResult remove_wave(ModContext* ctx, AudioWaveHandle handle); + +} // namespace wsys + +namespace bst { + +void frame_end(); +void remove_mod(LoadedMod const& mod); +void sync_audio_replacements(); + +extern AudioSoundTableEffectInfo const default_effect_info; + +ModResult replace_sound_table_effect(ModContext* ctx, SoundEffectCategory category_id, + uint16_t effect_id, AudioSoundTableEffectInfo const* info, AudioSoundTableHandle* out_handle); + +ModResult add_sound_table_effect(ModContext* ctx, SoundEffectCategory category_id, + AudioSoundTableEffectInfo const* info, AudioSoundTableHandle* out_handle, + uint16_t* out_effect_id); + +extern AudioSoundTableStreamInfo const default_stream_info; + +ModResult replace_sound_table_stream(ModContext* ctx, uint16_t stream_id, char const* file_path, + AudioSoundTableStreamInfo const* info, AudioSoundTableHandle* out_handle); + +ModResult add_sound_table_stream(ModContext* ctx, char const* file_path, + AudioSoundTableStreamInfo const* info, AudioSoundTableHandle* out_handle, + uint16_t* out_stream_id); + +ModResult remove_sound_table(ModContext* ctx, AudioSoundTableHandle handle); + +} // namespace bst + +} // namespace dusk::mods::svc::audio_res diff --git a/src/dusk/mods/svc/audio_res/bst.cpp b/src/dusk/mods/svc/audio_res/bst.cpp new file mode 100644 index 0000000000..917697bed7 --- /dev/null +++ b/src/dusk/mods/svc/audio_res/bst.cpp @@ -0,0 +1,368 @@ +#include "bst.hpp" + +#include "audio_res.hpp" +#include "aurora/lib/logging.hpp" +#include "dusk/mods/loader/loader.hpp" +#include "dusk/mods/svc/id_allocator.hpp" +#include "dusk/mods/svc/internal.hpp" + +namespace dusk::mods::svc::audio_res::bst { + +namespace { + +bool sound_replacements_dirty = false; + +aurora::Module Log("dusk::mods::svc::audio_res"); + +SlotMap> sound_replacements; + +std::array sound_effect_id_allocator = { + PlainIdAllocator(0x1000), // SYSTEM_SE + PlainIdAllocator(0x1000), // PLAYER_VOICE + PlainIdAllocator(0x1000), // PLAYER_SE + PlainIdAllocator(0x1000), // FOOTNOTE_SE + PlainIdAllocator(0x1000), // COLLISION_SE + PlainIdAllocator(0x1000), // CHARA_VOICE + PlainIdAllocator(0x1000), // CHARA_SE + PlainIdAllocator(0x1000), // ENEMY_SE + PlainIdAllocator(0x1000), // OBJECT_SE + PlainIdAllocator(0x1000), // ENV_SE +}; + +PlainIdAllocator stream_id_allocator(0x1000); + +bool validate_category(SoundEffectCategory const category) { + return category <= SE_CATEGORY_ENV_SE; +} + +uint8_t volume_to_item(float volume) { + volume = std::clamp(volume, 0.0f, 2.0f); + return static_cast(volume * 127); +} + +absl::flat_hash_map> + active_se_replacements; +absl::flat_hash_map> active_stream_replacements; +std::mutex active_replacements_mutex; + +} // namespace + +SoundTableReplacementSlot::SoundTableReplacementSlot(bool const mod_defined, u16 const id) + : mod_defined(mod_defined), id(id), item() {} + +SoundTableReplacementSlot::~SoundTableReplacementSlot() = default; + +SoundEffectReplacementSlot::SoundEffectReplacementSlot( + bool mod_defined, u16 id, SoundEffectCategory category, const AudioSoundTableEffectInfo& info) + : SoundTableReplacementSlot(mod_defined, id), category(category) { + item.mPriority = info.priority; + item.mVolume = volume_to_item(info.volume); + item.mPitch = info.pitch; + + u32 sw_bit = 0; + if (info.always_max_priority) + sw_bit |= SOUND_SW_ALWAYS_MAX_PRIORITY; + if (info.ignore_distance_volume) + sw_bit |= SOUND_SW_IGNORE_DISTANCE_VOL; + if (info.ignore_distance_fx_mix) + sw_bit |= SOUND_SW_IGNORE_FX_MIX; + if (info.ignore_pan) + sw_bit |= SOUND_SW_IGNORE_PAN; + if (info.ignore_dolby) + sw_bit |= SOUND_SW_IGNORE_DOLBY; + sw_bit |= (info.random_volume << SOUND_SW_RANDOM_VOLUME_OFFSET) & SOUND_SW_RANDOM_VOLUME_MASK; + sw_bit |= (info.random_pitch << SOUND_SW_RANDOM_PITCH_OFFSET) & SOUND_SW_RANDOM_PITCH_MASK; + sw_bit |= (info.doppler_power << SOUND_SW_DOPPLER_POWER_OFFSET) & SOUND_SW_DOPPLER_POWER_MASK; + + if (info.volume_dist_class < 8) { + sw_bit |= + (info.volume_dist_class << SOUND_SW_VOL_DIST_BIT_OFFSET) & SOUND_SW_VOL_DIST_BIT_MASK; + } else { + sw_bit |= ((info.volume_dist_class - 8) << SOUND_SW_VOL_DIST_BIT_2_OFFSET) & + SOUND_SW_VOL_DIST_BIT_2_MASK; + } + + if (info.clamp_min_volume) + sw_bit |= SOUND_SW_CLAMP_MIN_VOLUME; + if (info.cull_at_max_distance) + sw_bit |= SOUND_SW_CULL_AT_MAX_DISTANCE; + + item.mSwBit = sw_bit; +} + +u8 SoundEffectReplacementSlot::get_type_id() const { + return SOUND_TYPEID_SOUND_EFFECT; +} + +StreamReplacementSlot::StreamReplacementSlot( + bool mod_defined, u16 id, char const* file_path, const AudioSoundTableStreamInfo& info) + : SoundTableReplacementSlot(mod_defined, id), file_path(file_path) { + stop_on_scene_change = info.stop_on_scene_change; + item.mPriority = info.priority; + item.mVolume = volume_to_item(info.volume); + + u16 panParam = 0; + for (int i = STREAM_MAX_CHILDREN - 1; i >= 0; i--) { + panParam <<= STRM_CH_SHIFT_; + + switch (info.pan_parameters[i]) { + case STREAM_PAN_CENTER: + panParam |= STRM_CH_CENTER; + break; + case STREAM_PAN_LEFT: + panParam |= STRM_CH_LEFT; + break; + case STREAM_PAN_RIGHT: + panParam |= STRM_CH_RIGHT; + break; + } + } + item.mStreamPanParameters = panParam; +} + +u8 StreamReplacementSlot::get_type_id() const { + return stop_on_scene_change ? SOUND_TYPEID_STREAM_ALT : SOUND_TYPEID_STREAM; +} + +std::shared_ptr get_override_for(JAISoundID id) { + switch (id.id_.info.type.parts.sectionID) { + case 0: + return get_override_for_se(id); + case 2: + return get_override_for_stream(id); + default: + return nullptr; + } +} + +std::shared_ptr get_override_for_se(JAISoundID id) { + if (id.id_.info.type.parts.sectionID != 0) { + return nullptr; + } + + std::lock_guard lock(active_replacements_mutex); + + SoundEffectKey const key( + static_cast(id.id_.info.type.parts.groupID), id.id_.info.waveID); + + auto const entry = active_se_replacements.find(key); + if (entry == active_se_replacements.end()) { + return nullptr; + } + + return entry->second; +} + +std::shared_ptr get_override_for_stream(JAISoundID id) { + if (id.id_.info.type.parts.sectionID != 2) { + return nullptr; + } + + std::lock_guard lock(active_replacements_mutex); + + auto const entry = active_stream_replacements.find(id.id_.info.waveID); + if (entry == active_stream_replacements.end()) { + return nullptr; + } + + return entry->second; +} + +void remove_mod(LoadedMod const& mod) { + sound_replacements.erase_all(mod); + + sound_replacements_dirty = true; +} + +void frame_end() { + if (sound_replacements_dirty) { + wsys::sync_audio_replacements(); + } +} + +void sync_audio_replacements() { + sound_replacements_dirty = false; + + absl::flat_hash_map> + new_se_replacements; + absl::flat_hash_map> new_stream_replacements; + + for (auto const& mod : ModLoader::instance().active_mods()) { + sound_replacements.for_each([&](auto, auto const& entry) { + if (entry.owner != &mod) { + return; + } + + std::shared_ptr const& value = entry.value; + auto se = std::dynamic_pointer_cast(value); + if (se) { + new_se_replacements.emplace(SoundEffectKey(se->category, se->id), std::move(se)); + return; + } + + auto stream = std::dynamic_pointer_cast(value); + if (stream) { + new_stream_replacements.emplace(stream->id, std::move(stream)); + return; + } + }); + } + + std::lock_guard lock(active_replacements_mutex); + + std::exchange(active_se_replacements, std::move(new_se_replacements)); + std::exchange(active_stream_replacements, std::move(new_stream_replacements)); +} + +static ModResult insert_sound_table_effect_core(ModContext* ctx, SoundEffectCategory category_id, + uint16_t effect_id, bool mod_defined, AudioSoundTableEffectInfo const* info, + AudioSoundTableHandle* out_handle) { + if (out_handle != nullptr) { + *out_handle = 0; + } + + auto mod = mod_from_context(ctx); + if (mod == nullptr || !validate_category(category_id)) { + return MOD_INVALID_ARGUMENT; + } + + if (info == nullptr) { + info = &default_effect_info; + } + + auto slot = + std::make_shared(mod_defined, effect_id, category_id, *info); + sound_replacements_dirty = true; + + auto const handle = sound_replacements.emplace(*mod, std::move(slot)); + if (out_handle) { + *out_handle = handle; + } + + return MOD_OK; +} + +ModResult replace_sound_table_effect(ModContext* ctx, SoundEffectCategory category_id, + uint16_t effect_id, AudioSoundTableEffectInfo const* info, AudioSoundTableHandle* out_handle) { + return insert_sound_table_effect_core(ctx, category_id, effect_id, false, info, out_handle); +} + +AudioSoundTableEffectInfo const default_effect_info(128, 1, 1); + +ModResult add_sound_table_effect(ModContext* ctx, SoundEffectCategory category_id, + AudioSoundTableEffectInfo const* info, AudioSoundTableHandle* out_handle, + uint16_t* out_effect_id) { + if (out_effect_id == nullptr || !validate_category(category_id)) { + return MOD_INVALID_ARGUMENT; + } + + assert(category_id < sound_effect_id_allocator.size()); + + auto& allocator = sound_effect_id_allocator[category_id]; + auto const effect_id = allocator.alloc(); + + auto const result = + insert_sound_table_effect_core(ctx, category_id, effect_id, true, info, out_handle); + if (result != MOD_OK) { + allocator.free(effect_id); + } + + *out_effect_id = effect_id; + + return result; +} + +static ModResult insert_sound_table_stream_core(ModContext* ctx, uint16_t stream_id, + bool mod_defined, char const* file_path, AudioSoundTableStreamInfo const* info, + AudioSoundTableHandle* out_handle) { + if (out_handle != nullptr) { + *out_handle = 0; + } + + auto mod = mod_from_context(ctx); + if (mod == nullptr || file_path == nullptr) { + return MOD_INVALID_ARGUMENT; + } + + if (info == nullptr) { + info = &default_stream_info; + } + + auto slot = std::make_shared(mod_defined, stream_id, file_path, *info); + sound_replacements_dirty = true; + + auto const handle = sound_replacements.emplace(*mod, std::move(slot)); + if (out_handle) { + *out_handle = handle; + } + + return MOD_OK; +} + +AudioSoundTableStreamInfo const default_stream_info(128, 1, {STREAM_PAN_LEFT, STREAM_PAN_RIGHT}); + +ModResult replace_sound_table_stream(ModContext* ctx, uint16_t stream_id, char const* file_path, + AudioSoundTableStreamInfo const* info, AudioSoundTableHandle* out_handle) { + return insert_sound_table_stream_core(ctx, stream_id, false, file_path, info, out_handle); +} + +ModResult add_sound_table_stream(ModContext* ctx, char const* file_path, + AudioSoundTableStreamInfo const* info, AudioSoundTableHandle* out_handle, + uint16_t* out_stream_id) { + if (out_stream_id == nullptr) { + return MOD_INVALID_ARGUMENT; + } + + auto const stream_id = stream_id_allocator.alloc(); + + auto const result = + insert_sound_table_stream_core(ctx, stream_id, true, file_path, info, out_handle); + if (result != MOD_OK) { + stream_id_allocator.free(stream_id); + } + + *out_stream_id = stream_id; + + return result; +} + +static PlainIdAllocator& id_allocator_for(SoundTableReplacementSlot const* slot) { + if (dynamic_cast(slot)) + return stream_id_allocator; + + if (auto const se = dynamic_cast(slot)) + return sound_effect_id_allocator[se->id]; + + CRASH("Unknown type???"); +} + +static bool sound_table_remove(LoadedMod const& mod, AudioSoundTableHandle const handle) { + auto const found = sound_replacements.find_owned(handle, mod); + if (found == nullptr) { + return false; + } + + if (found->value->mod_defined) { + auto& allocator = id_allocator_for(found->value.get()); + allocator.free(found->value->id); + } + + sound_replacements.erase_owned(handle, mod); + return true; +} + +ModResult remove_sound_table(ModContext* ctx, AudioSoundTableHandle handle) { + auto* mod = mod_from_context(ctx); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + + if (!sound_table_remove(*mod, handle)) { + Log.error("[{}] remove sound table failed: unknown handle {}", mod->metadata.id, handle); + return MOD_INVALID_ARGUMENT; + } + + return MOD_OK; +} + +} // namespace dusk::mods::svc::audio_res::bst diff --git a/src/dusk/mods/svc/audio_res/bst.hpp b/src/dusk/mods/svc/audio_res/bst.hpp new file mode 100644 index 0000000000..0db6bd7c1f --- /dev/null +++ b/src/dusk/mods/svc/audio_res/bst.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +#include "JSystem/JAudio2/JAUSoundTable.h" +#include "mods/svc/audio_res.h" + +namespace dusk::mods::svc::audio_res::bst { + +struct SoundTableReplacementSlot { + bool mod_defined; + u16 id; + JAUSoundTableItem item; + + SoundTableReplacementSlot(bool mod_defined, u16 id); + + virtual ~SoundTableReplacementSlot() = 0; + + [[nodiscard]] virtual u8 get_type_id() const = 0; +}; + +struct SoundEffectReplacementSlot final : SoundTableReplacementSlot { + SoundEffectCategory category; + + SoundEffectReplacementSlot(bool mod_defined, u16 id, SoundEffectCategory category, + const AudioSoundTableEffectInfo& info); + + ~SoundEffectReplacementSlot() override = default; + + [[nodiscard]] u8 get_type_id() const override; +}; + +struct StreamReplacementSlot final : SoundTableReplacementSlot { + std::string file_path; + bool stop_on_scene_change; + + StreamReplacementSlot( + bool mod_defined, u16 id, char const* file_path, const AudioSoundTableStreamInfo& info); + + ~StreamReplacementSlot() override = default; + + [[nodiscard]] u8 get_type_id() const override; +}; + +struct SoundEffectKey { + SoundEffectCategory category; + u16 id; + + template + friend H AbslHashValue(H h, const SoundEffectKey& k) { + return H::combine(std::move(h), k.category, k.id); + } + + [[nodiscard]] bool operator==(const SoundEffectKey& other) const { + return other.category == category && other.id == id; + } +}; + +std::shared_ptr get_override_for(JAISoundID id); +std::shared_ptr get_override_for_se(JAISoundID id); +std::shared_ptr get_override_for_stream(JAISoundID id); + +} // namespace dusk::mods::svc::audio_res::bst diff --git a/src/dusk/mods/svc/audio_res/opus.cpp b/src/dusk/mods/svc/audio_res/opus.cpp new file mode 100644 index 0000000000..afa346cfba --- /dev/null +++ b/src/dusk/mods/svc/audio_res/opus.cpp @@ -0,0 +1,93 @@ +#if DUSK_OPUS + +#include "aurora/lib/logging.hpp" +#include "dusk/mod_loader.hpp" +#include "helpers/cast.hpp" +#include "opusfile.h" +#include "wsys.hpp" + +namespace { + +aurora::Module Log("dusk::mods::svc::audio_res::wsys::opus"); + +struct OpusHandle { + OggOpusFile* file; + + ~OpusHandle() { op_free(file); } + + operator OggOpusFile*() const { return file; } +}; + +} // namespace + +namespace dusk::mods::svc::audio_res::wsys { +ModResult load_opus( + LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span fileData) { + OpusHead head; + const int result = op_test(&head, fileData.data(), fileData.size()); + if (result != 0) { + return MOD_UNSUPPORTED; + } + + if (head.channel_count != 1) { + Log.error("[{}] replace_wave: file '{}' must be mono but actually has {} channels", + mod.metadata.id, slot.bundle_path, head.channel_count); + return MOD_INVALID_ARGUMENT; + } + + if (head.stream_count != 1) { + Log.error("[{}] replace_wave: file '{}' has multiple Opus streams!", mod.metadata.id, + slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + int error; + auto const handle_raw = op_open_memory(fileData.data(), fileData.size(), &error); + if (error != 0) { + Log.error("[{}] replace_wave: file '{}': unable to open Opus file: {}", mod.metadata.id, + slot.bundle_path, error); + return MOD_ERROR; + } + + const OpusHandle handle(handle_raw); + auto const length = op_pcm_total(handle, -1); + if (length < 0) { + Log.error("[{}] replace_wave: file '{}': failed to check stream length?", mod.metadata.id, + slot.bundle_path); + return MOD_ERROR; + } + + SampleDataPcm16 pcm_buffer; + pcm_buffer.data.resize(length); + + std::span span_pcm(pcm_buffer.data); + + while (!span_pcm.empty()) { + auto read_result = + op_read(handle, span_pcm.data(), helpers::cast::bounded_cast(span_pcm.size()), nullptr); + + if (read_result <= 0) { + Log.error("[{}] replace_wave: file '{}': failed to read Opus: {}", mod.metadata.id, + slot.bundle_path, read_result); + return MOD_ERROR; + } + + if (read_result > span_pcm.size()) { + Log.fatal("What teh fuck?"); + } + + span_pcm = span_pcm.subspan(read_result); + } + + pcm_buffer.be_swap(); + + slot.data = std::make_shared(std::move(pcm_buffer)); + slot.sample_rate = 48000; // Opus always decodes at 48 kHz. + slot.sample_count = length; + slot.format = AUDIO_WAVE_FORMAT_PCM16; + + return MOD_OK; +} +} // namespace dusk::mods::svc::audio_res::wsys + +#endif diff --git a/src/dusk/mods/svc/audio_res/wave.cpp b/src/dusk/mods/svc/audio_res/wave.cpp new file mode 100644 index 0000000000..7ab8ac8773 --- /dev/null +++ b/src/dusk/mods/svc/audio_res/wave.cpp @@ -0,0 +1,153 @@ +#include "aurora/lib/logging.hpp" +#include "dusk/mod_loader.hpp" +#include "helpers/alignment.hpp" +#include "wsys.hpp" + +namespace { + +aurora::Module Log("dusk::mods::svc::audio_res::wsys::wav"); + +struct ChunkHeader { + char magic[4]; + LE(u32) size; +}; + +struct RiffChunk { + ChunkHeader header; // magic "RIFF" + char id[4]; // "WAVE" + // Data after chunk. +}; + +constexpr u16 WAVE_FORMAT_PCM = 0x0001; + +struct FmtPcmChunkData { + ChunkHeader header; + + u16 wFormatTag; + u16 nChannels; + u32 nSamplesPerSec; + u32 nAvgBytesPerSec; + u16 nBlockAlign; + u16 wBitsPerSample; +}; + +bool check_four_cc(char const (&field)[4], char const (&expected)[5]) { + return memcmp(field, expected, 4) == 0; +} + +} // namespace + +namespace dusk::mods::svc::audio_res::wsys { + +ModResult load_wav( + LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span fileData) { + using namespace helpers; + + if (fileData.size() < sizeof(RiffChunk)) { + return MOD_UNSUPPORTED; + } + + auto const riffChunk = read_unaligned(&fileData[0]); + if (!check_four_cc(riffChunk.header.magic, "RIFF")) { + return MOD_UNSUPPORTED; + } + + if (!check_four_cc(riffChunk.id, "WAVE")) { + return MOD_UNSUPPORTED; + } + + if (sizeof(ChunkHeader) + riffChunk.header.size > fileData.size() || riffChunk.header.size < 4) + { + Log.error("[{}] wav file {}: invalid size!", mod.metadata.id, slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + bool has_read_fmt = false; + SampleDataPcm16 pcmData; + + auto waveData = fileData.subspan(sizeof(RiffChunk), riffChunk.header.size - 4); + while (waveData.size() > sizeof(ChunkHeader)) { + auto const chunkHeader = read_unaligned(&waveData[0]); + + if (check_four_cc(chunkHeader.magic, "fmt ")) { + if (has_read_fmt) { + Log.error( + "[{}] wav file {}: multiple fmt chunks!", mod.metadata.id, slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + if (waveData.size() < sizeof(FmtPcmChunkData)) { + Log.error( + "[{}] wav file {}: fmt chunk too small!", mod.metadata.id, slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + auto const fmtChunk = read_unaligned(&waveData[0]); + + if (fmtChunk.nChannels != 1) { + Log.error("[{}] wav file {}: only mono audio is supported", mod.metadata.id, + slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + if (fmtChunk.wFormatTag != WAVE_FORMAT_PCM) { + Log.error("[{}] wav file {}: only 16-bit PCM is supported", mod.metadata.id, + slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + if (fmtChunk.wBitsPerSample != 16) { + Log.error("[{}] wav file {}: only 16-bit PCM is supported", mod.metadata.id, + slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + has_read_fmt = true; + slot.sample_rate = static_cast(fmtChunk.nSamplesPerSec); + + } else if (check_four_cc(chunkHeader.magic, "data")) { + if (sizeof(chunkHeader) + chunkHeader.size > fileData.size()) { + Log.error("[{}] wav file {}: unexpected EOF reading sample data", mod.metadata.id, + slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + auto const sampleData = waveData.subspan(sizeof(chunkHeader), chunkHeader.size); + for (size_t i = 0; i + 1 < sampleData.size(); i += 2) { + pcmData.data.push_back(read_unaligned(&sampleData[i])); + } + } + + auto skip = sizeof(chunkHeader) + chunkHeader.size; + if (skip % 2 != 0) { + skip += 1; + } + + if (waveData.size() < skip) { + Log.error("[{}] wav file {}: unexpected EOF", mod.metadata.id, slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + waveData = waveData.subspan(skip); + } + + if (!has_read_fmt) { + Log.error("[{}] wav file {}: missing fmt chunk", mod.metadata.id, slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + if (pcmData.data.empty()) { + Log.error("[{}] wav file {}: missing data chunk", mod.metadata.id, slot.bundle_path); + return MOD_INVALID_ARGUMENT; + } + + pcmData.be_swap(); + + slot.format = AUDIO_WAVE_FORMAT_PCM16; + slot.sample_count = pcmData.size() / sizeof(u16); + slot.data = std::make_unique(std::move(pcmData)); + + return MOD_OK; +} + +} // namespace dusk::mods::svc::audio_res::wsys diff --git a/src/dusk/mods/svc/audio_res/wsys.cpp b/src/dusk/mods/svc/audio_res/wsys.cpp new file mode 100644 index 0000000000..689df9936f --- /dev/null +++ b/src/dusk/mods/svc/audio_res/wsys.cpp @@ -0,0 +1,319 @@ +#include "wsys.hpp" +#include "../id_allocator.hpp" +#include "../internal.hpp" +#include "audio_res.hpp" +#include "aurora/lib/logging.hpp" +#include "dusk/audio/DuskAudioSystem.h" +#include "dusk/mods/loader/loader.hpp" +#include "helpers/cast.hpp" + +namespace dusk::mods::svc::audio_res::wsys { + +namespace { + +using namespace dusk::helpers::cast; + +bool wave_replacements_dirty = false; + +aurora::Module Log("dusk::mods::svc::audio_res"); + +SlotMap s_waveReplacements; + +constexpr ContainerLoadFunction s_containerLoadFunctions[] = { + load_wav, +#if DUSK_OPUS + load_opus, +#endif +}; + +PlainIdAllocator sound_effect_id_allocator(5'000); +PlainIdAllocator music_sample_id_allocator(1'000); + +PlainIdAllocator& id_allocator_for_bank(AudioWaveBank const bank) { + return bank == AUDIO_WAVE_BANK_SOUND_EFFECTS ? sound_effect_id_allocator : + music_sample_id_allocator; +} + +bool validate_raw_size(LoadedMod const& mod, std::string const& path, uintptr_t actual_size, + AudioRawWave const& raw, u32& sample_count) { + u32 samples_per_block; + u32 bytes_per_block; + switch (raw.format) { + case AUDIO_WAVE_FORMAT_ADPCM4: + samples_per_block = 16; + bytes_per_block = 9; + break; + case AUDIO_WAVE_FORMAT_PCM16: + samples_per_block = 1; + bytes_per_block = 2; + break; + default: + Log.error("[{}] unimplemented wave format (ADPCM2/PCM8): '{}'", mod.metadata.id, path); + return false; + } + + if (actual_size % bytes_per_block != 0) { + Log.error( + "[{}] raw file is not divisible by format block size: '{}'", mod.metadata.id, path); + return false; + } + + sample_count = (actual_size / bytes_per_block) * samples_per_block; + return true; +} + +ModResult load_raw( + LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, AudioRawWave const& raw) { + auto file_contents = mod.bundle->readFile(slot.bundle_path); + u32 sample_count = 0; + + if (!validate_raw_size(mod, slot.bundle_path, file_contents.size(), raw, sample_count)) { + Log.error( + "[{}] replace_wave: file '{}' is a raw .abf file, but raw_wave data was not specified", + mod.metadata.id, slot.bundle_path); + + return MOD_INVALID_ARGUMENT; + } + + if (slot.format == AUDIO_WAVE_FORMAT_PCM16) { + SampleDataPcm16 pcm16; + pcm16.data.resize(sample_count); + memcpy(pcm16.data.data(), file_contents.data(), file_contents.size()); + slot.data = std::make_shared(std::move(pcm16)); + } else { + slot.data = std::make_shared(std::move(file_contents)); + } + + slot.sample_count = sample_count; + slot.sample_rate = raw.sample_rate; + slot.format = raw.format; + slot.sample_value_penult = raw.sample_value_penult; + slot.sample_value_last = raw.sample_value_last; + return MOD_OK; +} + +/** + * Load an audio file stored in some sort of container format. + * File type is determined by looking at contents, not extension. + */ +ModResult load_container(LoadedMod const& mod, RuntimeWaveReplacementSlot& slot) { + auto file_contents = mod.bundle->readFile(slot.bundle_path); + + for (auto const loader : s_containerLoadFunctions) { + auto const result = loader(mod, slot, file_contents); + if (result == MOD_OK) { + return MOD_OK; + } + + if (result != MOD_UNSUPPORTED) { + return result; + } + } + + return MOD_UNSUPPORTED; +} + +JASWaveInfo wave_info_from_slot(RuntimeWaveReplacementSlot const& slot) { + JASWaveInfo info; + info.mWaveFormat = static_cast(slot.format); + info.mBaseKey = slot.base_key; + info.mLoopFlag = slot.loop ? 0xFF : 0; + info.mSampleRate = slot.sample_rate; + info.mOffsetStart = 0; // Unused but just init it ig. + info.mOffsetLength = bounded_cast(slot.data->size()); + info.mLoopStartSample = slot.loop_start_sample; + info.mLoopEndSample = bounded_cast(slot.loop_end_sample); + info.mSampleCount = bounded_cast(slot.sample_count); + info.mpLast = slot.sample_value_last; + info.mpPenult = slot.sample_value_penult; + // mpLoaded is initialized to point to a 1, so we're good there. + return info; +} + +bool wave_remove(LoadedMod const& mod, AudioWaveHandle const handle) { + auto const found = s_waveReplacements.find_owned(handle, mod); + if (found == nullptr) { + return false; + } + + if (found->value.mod_defined) { + auto& allocator = id_allocator_for_bank(found->value.bank); + allocator.free(found->value.wave_id); + } + + s_waveReplacements.erase_owned(handle, mod); + return true; +} + +ModResult insert_replace_wave_core(ModContext* ctx, AudioWaveBank bank, u16 wave_id, + bool mod_defined, char const* file_name, AudioWaveInfo const* wave_info, + AudioWaveHandle* out_handle) { + if (out_handle != nullptr) { + *out_handle = 0; + } + + auto mod = mod_from_context(ctx); + if (mod == nullptr || file_name == nullptr || !is_safe_resource_path(file_name)) { + return MOD_INVALID_ARGUMENT; + } + + RuntimeWaveReplacementSlot slot{}; + slot.bundle_path = file_name; + slot.bank = bank; + slot.wave_id = wave_id; + slot.mod_defined = mod_defined; + + ModResult result; + if (wave_info && wave_info->raw_wave) { + result = load_raw(*mod, slot, *wave_info->raw_wave); + } else { + result = load_container(*mod, slot); + } + if (result != MOD_OK) { + return result; + } + + if (wave_info != nullptr) { + slot.base_key = wave_info->base_key; + slot.loop = wave_info->loop; + slot.loop_start_sample = wave_info->loop_start_sample; + slot.loop_end_sample = wave_info->loop_end_sample; + + if (slot.loop_end_sample > slot.sample_count) { + slot.loop_end_sample = slot.sample_count; + } + + if (slot.loop_start_sample >= slot.loop_end_sample) { + Log.error("[{}] wave has start >= end", mod->metadata.id); + return MOD_INVALID_ARGUMENT; + } + } else { + slot.base_key = AUDIO_RES_DEFAULT_KEY; + slot.loop = false; + slot.loop_start_sample = 0; + slot.loop_end_sample = slot.sample_count; + } + + wave_replacements_dirty = true; + + const auto handle = s_waveReplacements.emplace(*mod, std::move(slot)); + if (out_handle) { + *out_handle = handle; + } + + return MOD_OK; +} + +} // namespace + +absl::flat_hash_map s_replacements; +std::mutex s_replacements_mutex; + +AudioWaveInfo const default_wave_info( + AUDIO_RES_DEFAULT_KEY, false, 0, std::numeric_limits::max(), nullptr); + +ModResult remove_wave(ModContext* ctx, AudioWaveHandle handle) { + auto* mod = mod_from_context(ctx); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + if (!wave_remove(*mod, handle)) { + Log.error("[{}] remove wave failed: unknown handle {}", mod->metadata.id, handle); + return MOD_INVALID_ARGUMENT; + } + return MOD_OK; +} + +ModResult insert_replace_wave(ModContext* ctx, AudioWaveBank bank, u16 wave_id, + char const* file_name, AudioWaveInfo const* wave_info, AudioWaveHandle* out_handle) { + return insert_replace_wave_core(ctx, bank, wave_id, false, file_name, wave_info, out_handle); +} + +ModResult insert_add_wave(ModContext* ctx, AudioWaveBank bank, char const* file_name, + AudioWaveInfo const* wave_info, AudioWaveHandle* out_handle, u16* out_wave_id) { + if (out_wave_id == nullptr) { + return MOD_INVALID_ARGUMENT; + } + + *out_wave_id = 0; + + if (bank != AUDIO_WAVE_BANK_SOUND_EFFECTS && bank != AUDIO_WAVE_BANK_MUSIC_SAMPLES) { + return MOD_INVALID_ARGUMENT; + } + + auto& allocator = id_allocator_for_bank(bank); + auto const new_wave_id = allocator.alloc(); + + auto const result = + insert_replace_wave_core(ctx, bank, new_wave_id, true, file_name, wave_info, out_handle); + if (result != MOD_OK) { + allocator.free(new_wave_id); + } + + *out_wave_id = new_wave_id; + return result; +} + +void remove_mod(LoadedMod& mod) { + s_waveReplacements.erase_all(mod); + + wave_replacements_dirty = true; +} + +void sync_audio_replacements() { + wave_replacements_dirty = false; + + absl::flat_hash_map new_map; + + for (auto const& mod : ModLoader::instance().active_mods()) { + s_waveReplacements.for_each([&](auto, auto entry) { + if (entry.owner != &mod) { + return; + } + + auto const wave_info = wave_info_from_slot(entry.value); + new_map.emplace(AudioWaveKey(entry.value.bank, entry.value.wave_id), + AudioWaveReplacementValue(wave_info, entry.value.data)); + }); + } + + // Log.info("new: {}, old: {}", new_map.size(), s_replacements.size()); + + std::lock_guard lock(s_replacements_mutex); + // Note: new_map will contain the old contents, and is dropped *outside* the lock. + // As to avoid holding the lock any longer than necessary. + std::exchange(s_replacements, std::move(new_map)); + + // Log.info("new: {}, old: {}", new_map.size(), s_replacements.size()); +} + +void frame_end() { + if (wave_replacements_dirty) { + sync_audio_replacements(); + } +} + +AudioWaveReplacementValue::~AudioWaveReplacementValue() = default; +const JASWaveInfo* AudioWaveReplacementValue::getWaveInfo() const { + return &wave_info; +} + +void const* AudioWaveReplacementValue::getAramBaseAddress() const { + return data->get_data().data(); +} + +intptr_t AudioWaveReplacementValue::getWavePtr() const { + return 0; +} + +std::unique_ptr AudioWaveReplacementValue::getSampleReference() const { + return std::make_unique(data); +} + +void SampleDataPcm16::be_swap() { + for (auto& sample : data) { + ::be_swap(sample); + } +} + +} // namespace dusk::mods::svc::audio_res::wsys diff --git a/src/dusk/mods/svc/audio_res/wsys.hpp b/src/dusk/mods/svc/audio_res/wsys.hpp new file mode 100644 index 0000000000..d609d04206 --- /dev/null +++ b/src/dusk/mods/svc/audio_res/wsys.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include "JSystem/JAudio2/JASWaveInfo.h" +#include "absl/container/flat_hash_map.h" +#include "mods/svc/audio_res.h" + +namespace dusk::mods { +struct LoadedMod; +} + +namespace dusk::mods::svc::audio_res::wsys { + +struct SampleDataBuf { + virtual ~SampleDataBuf() = default; + [[nodiscard]] virtual std::span get_data() const = 0; + [[nodiscard]] size_t size() const { return get_data().size(); } +}; + +// Making a separate type for this rather than just using a vector +// because I don't want aliasing or alignment to eat my face. +struct SampleDataPcm16 : SampleDataBuf { + std::vector data; + + [[nodiscard]] std::span get_data() const override { + return as_bytes(std::span(data)); + } + + void be_swap(); +}; + +struct SampleDataU8 : SampleDataBuf { + std::vector data; + + explicit SampleDataU8(std::vector data) : data(std::move(data)) {} + + [[nodiscard]] std::span get_data() const override { + return as_bytes(std::span(data)); + } +}; + +struct SampleReference : JASSampleDataReference { + explicit SampleReference(std::shared_ptr data) : data(std::move(data)) {} + + std::shared_ptr data; +}; + +struct AudioWaveKey { + AudioWaveBank bank; + u32 wave_id; + + AudioWaveKey(AudioWaveBank bank, u32 wave_id) : bank(bank), wave_id(wave_id) {}; + + template + friend H AbslHashValue(H h, const AudioWaveKey& k) { + return H::combine(std::move(h), k.bank, k.wave_id); + } + + [[nodiscard]] bool operator==(const AudioWaveKey& other) const { + return other.bank == bank && other.wave_id == wave_id; + } +}; + +struct AudioWaveReplacementValue : JASWaveHandle { + AudioWaveReplacementValue(const JASWaveInfo& wave_info, std::shared_ptr data) + : wave_info(wave_info), data(std::move(data)) {}; + ~AudioWaveReplacementValue() override; + [[nodiscard]] const JASWaveInfo* getWaveInfo() const override; + [[nodiscard]] intptr_t getWavePtr() const override; + [[nodiscard]] void const* getAramBaseAddress() const override; + [[nodiscard]] std::unique_ptr getSampleReference() const override; + + JASWaveInfo wave_info; + std::shared_ptr data; +}; + +extern absl::flat_hash_map s_replacements; +extern std::mutex s_replacements_mutex; + +struct RuntimeWaveReplacementSlot { + std::string bundle_path; + AudioWaveBank bank; + bool mod_defined; + u16 wave_id; + + u8 base_key; + bool loop; + u32 loop_start_sample; + u32 loop_end_sample; + + AudioWaveFormat format; + f32 sample_rate; + u32 sample_count; + s16 sample_value_last; + s16 sample_value_penult; + + std::shared_ptr data; +}; + +using ContainerLoadFunction = ModResult (*)( + LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span fileData); + +ModResult load_wav( + LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span fileData); +ModResult load_opus( + LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span fileData); + +} // namespace dusk::mods::svc::audio_res::wsys diff --git a/src/dusk/mods/svc/gfx.cpp b/src/dusk/mods/svc/gfx.cpp index 9ec8f2415a..f398e29410 100644 --- a/src/dusk/mods/svc/gfx.cpp +++ b/src/dusk/mods/svc/gfx.cpp @@ -27,6 +27,10 @@ namespace dusk::mods { namespace { +// Oops! We forgot the FIFO thread exists. The design of this service isn't thread safe. +// Encounter said he'd fix it, this is the temporary workaround. +#define OH_FUCK_SYNC AuroraGXSync(); + constexpr borealis::Log Log{"dusk::mods::gfx"}; enum class GfxSlotKind : uint8_t { @@ -1082,6 +1086,8 @@ ModResult gfx_get_device_info(ModContext* context, GfxDeviceInfo* outInfo) { } ModResult gfx_get_scene_target_layout(ModContext* context, GfxRenderTargetLayout* outLayout) { + OH_FUCK_SYNC + if (outLayout == nullptr || outLayout->struct_size < sizeof(GfxRenderTargetLayout) || mod_from_context(context) == nullptr) { @@ -1172,6 +1178,8 @@ ModResult gfx_register_window_present_target_impl(ModContext* context, WindowHan ModResult gfx_resize_present_target_impl( ModContext* context, GfxPresentTargetHandle handle, uint32_t width, uint32_t height) { + OH_FUCK_SYNC + auto* mod = mod_from_context(context); if (mod == nullptr || handle == 0 || width == 0 || height == 0) { return MOD_INVALID_ARGUMENT; @@ -1189,6 +1197,8 @@ ModResult gfx_unregister_present_target_impl(ModContext* context, GfxPresentTarg ModResult gfx_push_present_impl( ModContext* context, GfxPresentTargetHandle handle, const void* payload, size_t payloadSize) { + OH_FUCK_SYNC + auto* mod = mod_from_context(context); if (mod == nullptr || handle == 0 || payloadSize > GFX_INLINE_DRAW_PAYLOAD_SIZE || (payloadSize > 0 && payload == nullptr)) @@ -1229,6 +1239,8 @@ ModResult gfx_unregister_draw_type_impl(ModContext* context, GfxDrawTypeHandle h ModResult gfx_push_draw_impl( ModContext* context, GfxDrawTypeHandle handle, const void* payload, size_t payloadSize) { + OH_FUCK_SYNC + auto* mod = mod_from_context(context); if (mod == nullptr || handle == 0 || payloadSize > GFX_INLINE_DRAW_PAYLOAD_SIZE || (payloadSize > 0 && payload == nullptr)) @@ -1252,21 +1264,29 @@ ModResult gfx_push_stream_impl(ModContext* context, GfxStreamBuffer buffer, cons ModResult gfx_push_verts_impl( ModContext* context, const void* data, size_t size, size_t alignment, GfxRange* outRange) { + OH_FUCK_SYNC + return gfx_push_stream_impl(context, GfxStreamBuffer::Verts, data, size, alignment, outRange); } ModResult gfx_push_indices_impl( ModContext* context, const void* data, size_t size, size_t alignment, GfxRange* outRange) { + OH_FUCK_SYNC + return gfx_push_stream_impl(context, GfxStreamBuffer::Indices, data, size, alignment, outRange); } ModResult gfx_push_uniform_impl( ModContext* context, const void* data, size_t size, GfxRange* outRange) { + OH_FUCK_SYNC + return gfx_push_stream_impl(context, GfxStreamBuffer::Uniform, data, size, 0, outRange); } ModResult gfx_push_storage_impl( ModContext* context, const void* data, size_t size, GfxRange* outRange) { + OH_FUCK_SYNC + return gfx_push_stream_impl(context, GfxStreamBuffer::Storage, data, size, 0, outRange); } @@ -1307,6 +1327,8 @@ ModResult gfx_unregister_stage_hook_impl(ModContext* context, GfxStageHookHandle ModResult gfx_resolve_pass_impl( ModContext* context, const GfxResolveDesc* desc, GfxResolvedTargets* outTargets) { + OH_FUCK_SYNC + if (outTargets != nullptr && outTargets->struct_size >= sizeof(GfxResolvedTargets)) { *outTargets = GfxResolvedTargets{.struct_size = sizeof(GfxResolvedTargets)}; } @@ -1321,6 +1343,8 @@ ModResult gfx_resolve_pass_impl( } ModResult gfx_create_pass_impl(ModContext* context, uint32_t width, uint32_t height) { + OH_FUCK_SYNC + auto* mod = mod_from_context(context); if (mod == nullptr || width == 0 || height == 0) { return MOD_INVALID_ARGUMENT; @@ -1359,6 +1383,8 @@ ModResult gfx_unregister_compute_type_impl(ModContext* context, GfxComputeTypeHa ModResult gfx_push_compute_impl( ModContext* context, GfxComputeTypeHandle handle, const void* payload, size_t payloadSize) { + OH_FUCK_SYNC + auto* mod = mod_from_context(context); if (mod == nullptr || handle == 0 || payloadSize > GFX_INLINE_DRAW_PAYLOAD_SIZE || (payloadSize > 0 && payload == nullptr)) diff --git a/src/dusk/mods/svc/hook.cpp b/src/dusk/mods/svc/hook.cpp index d8a9005deb..0cc983c02d 100644 --- a/src/dusk/mods/svc/hook.cpp +++ b/src/dusk/mods/svc/hook.cpp @@ -297,27 +297,32 @@ bool install_backend( #endif } -void deactivate_backend(void* target, InstalledBackend& backend) { +bool deactivate_backend(void* target, InstalledBackend& backend) { #if DUSK_HAS_PREPATCH if (backend.kind == BackendKind::Prepatch) { prepatch::publish(backend.prepatchSite, nullptr); backend = {}; - return; + return true; } #endif #if DUSK_HAS_FUNCHOOK if (backend.kind == BackendKind::Funchook) { const int uninst = funchook_uninstall(backend.handle, 0); + if (uninst != 0) { + DuskLog.warn("HookSystem: funchook uninstall for {:p} failed: {}", target, + funchook_error_message(backend.handle)); + return false; + } const int destr = funchook_destroy(backend.handle); - if (uninst != 0 || destr != 0) { - DuskLog.warn("HookSystem: funchook uninstall/destroy for {:p} returned {}/{}", target, - uninst, destr); + if (destr != 0) { + DuskLog.warn("HookSystem: funchook destroy for {:p} returned {}", target, destr); } } #else (void)target; #endif backend = {}; + return true; } bool handoff_backend( @@ -352,8 +357,8 @@ bool handoff_hook(void* target, InstalledHook& entry) { #else constexpr bool prepatched = false; #endif - if (!prepatched) { - deactivate_backend(target, entry.backend); + if (!prepatched && !deactivate_backend(target, entry.backend)) { + DuskLog.fatal("HookSystem: cannot hand off a hook that remains installed at {:p}", target); } entry.active = nullptr; @@ -535,13 +540,20 @@ ModResult hook_uninstall(ModContext* context, void* fnAddr, void** originalFnSlo return MOD_INVALID_ARGUMENT; } + const bool removedActive = entry.activeStore == originalFnSlot; +#if DUSK_HAS_FUNCHOOK + if (removedActive && entry.backend.kind == BackendKind::Funchook && + !deactivate_backend(fnAddr, entry.backend)) { + return MOD_ERROR; + } +#endif + if (const auto registryIt = s_registry.find(key); registryIt != s_registry.end() && erase_callbacks(registryIt->second, context)) { s_registry.erase(registryIt); } - const bool removedActive = entry.activeStore == originalFnSlot; entry.candidates.erase(candidateIt); *originalFnSlot = nullptr; if (!removedActive) { @@ -745,7 +757,8 @@ bool resolve_symbol_checked(const char* symbol, bool requireCode, void** out, st why = fmt::format("symbol '{}' not found", symbol); return false; case manifest::ResolveStatus::Ambiguous: - why = fmt::format("'{}' maps to more than one address; use the mangled name", symbol); + why = fmt::format( + "'{}' maps to more than one address; use a qualified or mangled name", symbol); return false; } why = "unexpected resolve failure"; @@ -897,7 +910,9 @@ void hook_remove_mod(LoadedMod& mod) { auto* target = reinterpret_cast(it->first); if (entry.candidates.empty()) { - deactivate_backend(target, entry.backend); + if (!deactivate_backend(target, entry.backend)) { + DuskLog.fatal("HookSystem: cannot detach a mod with a live hook at {:p}", target); + } it = s_installed.erase(it); continue; } diff --git a/src/dusk/mods/svc/http.cpp b/src/dusk/mods/svc/http.cpp index 2277e66e01..423bd50b70 100644 --- a/src/dusk/mods/svc/http.cpp +++ b/src/dusk/mods/svc/http.cpp @@ -179,7 +179,7 @@ borealis::http::Result publish_download(borealis::http::Result result, } std::filesystem::path temporary = destination; - temporary += "." + borealis::io::fs_path_to_string(staging.filename()) + ".part"; + temporary += fmt::format(".{}.part", borealis::io::fs_path_to_string(staging.filename())); std::error_code ec; std::filesystem::copy_file( staging, temporary, std::filesystem::copy_options::overwrite_existing, ec); @@ -188,7 +188,7 @@ borealis::http::Result publish_download(borealis::http::Result result, std::error_code ignored; std::filesystem::remove(temporary, ignored); result.error = borealis::http::Error::Io; - result.message = "Failed to publish download: " + copyError; + result.message = fmt::format("Failed to publish download: {}", copyError); return result; } @@ -196,14 +196,14 @@ borealis::http::Result publish_download(borealis::http::Result result, if (!borealis::io::atomic_replace(temporary, destination, replaceError)) { std::filesystem::remove(temporary, ec); result.error = borealis::http::Error::Io; - result.message = "Failed to publish download: " + replaceError; + result.message = fmt::format("Failed to publish download: {}", replaceError); return result; } std::filesystem::remove(staging, ec); return result; } catch (const std::exception& exception) { result.error = borealis::http::Error::Io; - result.message = std::string{"Failed to publish download: "} + exception.what(); + result.message = fmt::format("Failed to publish download: {}", exception.what()); return result; } catch (...) { result.error = borealis::http::Error::Io; diff --git a/src/dusk/mods/svc/id_allocator.cpp b/src/dusk/mods/svc/id_allocator.cpp new file mode 100644 index 0000000000..3778bc7b81 --- /dev/null +++ b/src/dusk/mods/svc/id_allocator.cpp @@ -0,0 +1,9 @@ +#include "id_allocator.hpp" + +namespace dusk::mods::svc { + +void id_allocator_exhausted() { + CRASH("ID allocator exhausted!"); +} + +} // namespace dusk::mods::svc \ No newline at end of file diff --git a/src/dusk/mods/svc/id_allocator.hpp b/src/dusk/mods/svc/id_allocator.hpp new file mode 100644 index 0000000000..2ee1118028 --- /dev/null +++ b/src/dusk/mods/svc/id_allocator.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +namespace dusk::mods::svc { + +[[noreturn]] void id_allocator_exhausted(); + +template + requires std::is_integral_v +class PlainIdAllocator { + std::vector reusable; + T alloc_next; + T alloc_max; + +public: + constexpr explicit PlainIdAllocator(T first, T max = std::numeric_limits::max()) + : alloc_next(first), alloc_max(max) {} + + T alloc() { + if (reusable.empty()) { + if (alloc_next == alloc_max) { + id_allocator_exhausted(); + } + + return alloc_next++; + } + + auto val = reusable.back(); + reusable.pop_back(); + return val; + } + + void free(T value) { + assert(value < alloc_max && value < alloc_next); + + reusable.push_back(value); + } +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 33ceede924..8c2e5af50b 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -4,24 +4,25 @@ #include "dusk/logging.h" #include "dusk/mods/loader/loader.hpp" +#include + +#include +#include #include #include +#include #include +#include #include namespace dusk::mods::svc { namespace { +uint64_t s_serviceGeneration = 0; std::unordered_map s_services; +std::unordered_set s_unavailableServices; std::vector s_modules; -std::string service_key(std::string_view id, const uint16_t majorVersion) { - std::string key{id}; - key.push_back('\x1f'); - key += std::to_string(majorVersion); - return key; -} - const char* mod_id(const LoadedMod* mod) { return mod != nullptr ? mod->metadata.id.c_str() : AppName; } @@ -47,12 +48,34 @@ bool validate_service_header(const ServiceHeader* header, const char* serviceId, } void clear_services() { + ++s_serviceGeneration; s_services.clear(); + s_unavailableServices.clear(); s_modules.clear(); } } // namespace +uint64_t services_generation() noexcept { + return s_serviceGeneration; +} + +std::vector list_services() { + std::vector services; + for (const auto& [key, record] : s_services) { + if (record.service == nullptr || + (record.provider != nullptr && !record.provider->is_enabled())) + { + continue; + } + services.push_back({record.id, record.majorVersion, record.minorVersion, + record.provider != nullptr ? record.provider->metadata.id : std::string{}}); + } + std::ranges::sort( + services, {}, [](const auto& service) { return std::tie(service.id, service.major); }); + return services; +} + bool valid_service_id(const char* serviceId) { return serviceId != nullptr && serviceId[0] != '\0'; } @@ -76,6 +99,7 @@ ModResult register_service(const char* serviceId, const uint16_t majorVersion, return MOD_CONFLICT; } + ++s_serviceGeneration; s_services.emplace(key, ServiceRecord{ serviceId, majorVersion, @@ -109,12 +133,14 @@ ModResult publish_deferred_service( return MOD_INVALID_ARGUMENT; } + ++s_serviceGeneration; record.service = service; record.minorVersion = header->minor_version; return MOD_OK; } void remove_services_for_provider(const LoadedMod& provider) { + ++s_serviceGeneration; std::erase_if( s_services, [&](const auto& entry) { return entry.second.provider == &provider; }); } @@ -137,8 +163,38 @@ const ServiceRecord* find_service_record(const char* serviceId, const uint16_t m return it != s_services.end() ? &it->second : nullptr; } +std::string describe_missing_service(const char* serviceId, const uint16_t majorVersion, + const uint16_t minMinorVersion) { + const char* message = "Mod requires a service that is unavailable"; + if (std::string_view{serviceId}.starts_with(DUSKLIGHT_SERVICE_ID_PREFIX) && + !s_unavailableServices.contains(service_key(serviceId, majorVersion))) + { + if (const auto* record = find_service_record(serviceId, majorVersion)) { + if (record->provider == nullptr && record->service != nullptr && + record->minorVersion < minMinorVersion) + { + message = "Mod requires a newer Dusklight version"; + } + } else { + std::optional highestMajor; + for (const auto& [key, record] : s_services) { + if (record.provider == nullptr && record.service != nullptr && record.id == serviceId) { + highestMajor = std::max(highestMajor.value_or(0), record.majorVersion); + } + } + if (highestMajor) { + message = majorVersion > *highestMajor ? + "Mod requires a newer Dusklight version" : + "Mod must be updated for the current Dusklight version"; + } + } + } + return fmt::format("{} (missing: {})", message, serviceId); +} + ModResult register_module(const ServiceModule& module) { if (module.available != nullptr && !module.available()) { + s_unavailableServices.insert(service_key(module.id, module.majorVersion)); return MOD_UNAVAILABLE; } const auto result = register_service( @@ -146,6 +202,7 @@ ModResult register_module(const ServiceModule& module) { if (result != MOD_OK) { return result; } + s_unavailableServices.erase(service_key(module.id, module.majorVersion)); s_modules.push_back(&module); if (module.initialize != nullptr) { module.initialize(); @@ -227,6 +284,7 @@ void ModLoader::init_services() { &svc::g_cameraModule, &svc::g_windowModule, &svc::g_gfxModule, + &svc::g_audioResModule, &svc::g_saveModule, &svc::g_stageModule, &svc::g_itemModule, @@ -271,6 +329,9 @@ bool ModLoader::register_static_service_exports(LoadedMod& mod) { std::string ModLoader::describe_missing_import( const char* serviceId, const uint16_t majorVersion, const uint16_t minMinorVersion) const { + if (std::string_view{serviceId}.starts_with(DUSKLIGHT_SERVICE_ID_PREFIX)) { + return svc::describe_missing_service(serviceId, majorVersion, minMinorVersion); + } if (const auto* record = svc::find_service_record(serviceId, majorVersion)) { if (record->service == nullptr) { return fmt::format("Required service {}@{} was never published by provider '{}'", @@ -297,7 +358,7 @@ std::string ModLoader::describe_missing_import( } } - return fmt::format("Required service unavailable: {}@{}", serviceId, majorVersion); + return svc::describe_missing_service(serviceId, majorVersion, minMinorVersion); } bool ModLoader::resolve_service_imports(LoadedMod& mod) { diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index 470208fad1..f2ed6f370c 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -1,6 +1,7 @@ #pragma once #include "dusk/mod_loader.hpp" +#include "dusk/mods/services.hpp" #include "mods/svc/host.h" #include "mods/svc/log.h" @@ -48,6 +49,9 @@ struct ServiceModule { void (*shutdown)() = nullptr; }; +std::vector list_services(); +uint64_t services_generation() noexcept; + bool valid_service_id(const char* serviceId); ModResult register_service(const char* serviceId, uint16_t majorVersion, uint16_t minorVersion, const void* service, LoadedMod* provider, bool deferred); @@ -58,6 +62,8 @@ const ServiceRecord* find_service( const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion); // Unlike find_service, also returns deferred records that have not been published yet. const ServiceRecord* find_service_record(const char* serviceId, uint16_t majorVersion); +std::string describe_missing_service(const char* serviceId, uint16_t majorVersion, + uint16_t minMinorVersion); ModResult register_module(const ServiceModule& module); void modules_mod_deactivating(LoadedMod& mod); @@ -84,6 +90,7 @@ extern const ServiceModule g_gameModule; extern const ServiceModule g_cameraModule; extern const ServiceModule g_windowModule; extern const ServiceModule g_gfxModule; +extern const ServiceModule g_audioResModule; extern const ServiceModule g_saveModule; extern const ServiceModule g_stageModule; extern const ServiceModule g_itemModule; diff --git a/src/dusk/mods/svc/resource.cpp b/src/dusk/mods/svc/resource.cpp index d92630317e..57d3f6bdc0 100644 --- a/src/dusk/mods/svc/resource.cpp +++ b/src/dusk/mods/svc/resource.cpp @@ -35,6 +35,10 @@ void resource_remove_mod(LoadedMod& mod) { } } +std::string prefix_path(const char* relativePath) { + return fmt::format("res/{}", relativePath); +} + ModResult resource_load(ModContext* context, const char* relativePath, ResourceBuffer* outBuffer) { if (outBuffer == nullptr || outBuffer->struct_size < sizeof(ResourceBuffer)) { return MOD_INVALID_ARGUMENT; @@ -46,7 +50,7 @@ ModResult resource_load(ModContext* context, const char* relativePath, ResourceB return MOD_INVALID_ARGUMENT; } - const auto entry = fmt::format("res/{}", relativePath); + const auto entry = prefix_path(relativePath); std::vector data; try { data = mod->bundle->readFile(entry); @@ -91,10 +95,31 @@ void resource_free(ModContext* context, ResourceBuffer* buffer) { buffer->size = 0; } + +bool file_exists(ModContext* ctx, char const* relative_path) { + auto* mod = mod_from_context(ctx); + if (mod == nullptr || relative_path == nullptr || !is_safe_resource_path(relative_path)) { + return MOD_INVALID_ARGUMENT; + } + + return mod->bundle->file_exists(prefix_path(relative_path)); +} + +bool directory_exists(ModContext* ctx, char const* relative_path) { + auto* mod = mod_from_context(ctx); + if (mod == nullptr || relative_path == nullptr || !is_safe_resource_path(relative_path)) { + return MOD_INVALID_ARGUMENT; + } + + return mod->bundle->directory_exists(prefix_path(relative_path)); +} + constexpr ResourceService s_resourceService{ .header = SERVICE_HEADER(ResourceService, RESOURCE_SERVICE_MAJOR, RESOURCE_SERVICE_MINOR), .load = resource_load, .free = resource_free, + .file_exists = file_exists, + .directory_exists = directory_exists, }; } // namespace diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index f10794d1ab..0e763005cb 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -8,18 +8,22 @@ #include "dusk/mod_loader.hpp" #include "dusk/mods/loader/loader.hpp" #include "dusk/mods/log_buffer.hpp" +#include "dusk/ui/context_menu.hpp" +#include "dusk/ui/dropdown_button.hpp" +#include "dusk/ui/icon_button.hpp" #include "dusk/ui/list.hpp" #include "dusk/ui/menu_bar.hpp" #include "dusk/ui/mod_window.hpp" #include "dusk/ui/modal.hpp" +#include "dusk/ui/row.hpp" #include "dusk/ui/ui.hpp" #include "mods/svc/ui.h" +#include +#include #include #include #include -#include -#include #include #include @@ -44,6 +48,10 @@ constexpr size_t kUiControlSelectedSize = constexpr size_t kUiControlStringSetModeSize = offsetof(UiControlDesc, string_set_mode) + sizeof(UiStringSetMode); constexpr size_t kUiControlFilePickerSize = offsetof(UiControlDesc, directory_mode) + sizeof(bool); +constexpr size_t kUiControlIconSize = offsetof(UiControlDesc, icon) + sizeof(const char*); +constexpr size_t kUiControlOptionsSize = + offsetof(UiControlDesc, option_enabled) + sizeof(const bool*); +constexpr size_t kUiControlTooltipSize = offsetof(UiControlDesc, tooltip) + sizeof(const char*); constexpr size_t kUiListItemV21Size = offsetof(UiListItem, label) + sizeof(const char*); constexpr size_t kUiListDescV21Size = offsetof(UiListDesc, user_data) + sizeof(void*); @@ -58,12 +66,14 @@ enum class UiSlotKind : u8 { Window, Dialog, Pane, + Row, Text, Progress, Control, List, Style, MenuTab, + ContextMenu, }; const char* slot_kind_name(UiSlotKind kind) { @@ -72,6 +82,8 @@ const char* slot_kind_name(UiSlotKind kind) { return "window"; case UiSlotKind::Dialog: return "dialog"; + case UiSlotKind::Row: + return "row"; case UiSlotKind::Pane: return "pane"; case UiSlotKind::Text: @@ -86,6 +98,8 @@ const char* slot_kind_name(UiSlotKind kind) { return "style"; case UiSlotKind::MenuTab: return "menu tab"; + case UiSlotKind::ContextMenu: + return "context menu"; default: return "unknown"; } @@ -95,14 +109,20 @@ const char* slot_kind_name(UiSlotKind kind) { // (ui::update), or in the loader's deactivate paths. struct UiSlot { UiSlotKind kind = UiSlotKind::Window; - // Pane/Text/Progress/Control: freed automatically when the element is destroyed + // Pane/Row/Text/Progress/Control: freed automatically when the element is destroyed Rml::Element* element = nullptr; - // Pane payload + // Container payload + ui::Component* component = nullptr; + ui::Component* container = nullptr; + UiControlKind controlKind = UI_CONTROL_BUTTON; + std::unique_ptr displayOverride; + bool hidden = false; + uint64_t anchorHandle = 0; ui::Pane* pane = nullptr; ui::Pane* helpPane = nullptr; // List payload ui::List* list = nullptr; - // Window/Dialog payload (non-owning; the document stack owns the document) + // Window/Dialog payload ui::Document* document = nullptr; UiWindowClosedFn onClosed = nullptr; void* onClosedUserData = nullptr; @@ -111,9 +131,10 @@ struct UiSlot { std::string styleId; // Cached rendered values for element setters. These make the natural "set every update" // style cheap when the displayed value has not changed. - std::string elementRml; + std::string elementValue; float elementFloat = 0.0f; bool hasElementValue = false; + bool elementValueIsRml = false; }; SlotMap s_slots; @@ -123,6 +144,7 @@ struct ModUiPanel { UiPanelUpdateFn update = nullptr; void* userData = nullptr; }; + std::unordered_map s_modPanels; struct ModMenuTab { @@ -131,6 +153,7 @@ struct ModMenuTab { UiPressedFn onSelected = nullptr; void* userData = nullptr; }; + std::unordered_map> s_modMenuTabs; bool s_menuTabsDirty = false; @@ -156,6 +179,35 @@ UiSlot* resolve(LoadedMod& mod, uint64_t handle, UiSlotKind kind, const char* wh return &entry->value; } +UiSlot* resolve_element(LoadedMod& mod, uint64_t handle) { + auto* entry = s_slots.find_owned(handle, mod); + return entry != nullptr && entry->value.element != nullptr ? &entry->value : nullptr; +} + +UiSlot* resolve_container(LoadedMod& mod, uint64_t handle, const char* what) { + auto* entry = s_slots.find_owned(handle, mod); + if (entry == nullptr || + (entry->value.kind != UiSlotKind::Pane && entry->value.kind != UiSlotKind::Row)) + { + Log.error( + "[{}] {}: stale or invalid container handle {:#x}", mod.metadata.id, what, handle); + return nullptr; + } + return &entry->value; +} + +bool element_available(const UiSlot& slot) { + if (!slot.element->IsVisible(true)) { + return false; + } + for (auto* node = slot.element; node != nullptr; node = node->GetParentNode()) { + if (node->IsPseudoClassSet("disabled") || node->HasAttribute("disabled")) { + return false; + } + } + return slot.component == nullptr || !slot.component->disabled(); +} + // Whether the registration a callback was created under is still live. Callbacks captured by // host-owned UI must check this before calling into the mod: `mod->active` alone is true again // once a reload completes, but captured fn pointers still target the unloaded image. Teardown @@ -180,6 +232,16 @@ public: void OnDetach(Rml::Element*) override { s_slots.erase(m_handle); + std::vector menus; + s_slots.for_each([&](uint64_t, const auto& entry) { + if (entry.value.kind == UiSlotKind::ContextMenu && entry.value.anchorHandle == m_handle) + { + menus.push_back(static_cast(entry.value.document)); + } + }); + for (auto* menu : menus) { + menu->dismiss(false); + } delete this; } @@ -236,6 +298,8 @@ void invoke_mod_ui_callback(LoadedMod& mod, const char* what, Fn&& fn) { uint64_t wrap_pane(LoadedMod& mod, ui::Pane& pane, ui::Pane* helpPane) { uint64_t handle = 0; auto& slot = alloc_slot(mod, UiSlotKind::Pane, handle); + slot.component = &pane; + slot.container = &pane; slot.pane = &pane; slot.helpPane = helpPane; track_element(handle, slot, *pane.root()); @@ -293,7 +357,11 @@ void wire_callback_binding( break; case UI_CONTROL_NUMBER: case UI_CONTROL_SELECT: - spec.getInt = [getValue] { return clamp_to_int(getValue().int_value); }; + case UI_CONTROL_DROPDOWN: + spec.getInt = [getValue, dropdown = desc.kind == UI_CONTROL_DROPDOWN] { + const auto value = getValue().int_value; + return dropdown && (value < 0 || value > INT_MAX) ? -1 : clamp_to_int(value); + }; spec.setInt = [setValue](int value) { UiControlValue raw = UI_CONTROL_VALUE_INIT; raw.int_value = value; @@ -353,7 +421,8 @@ bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModC return true; } case UI_CONTROL_NUMBER: - case UI_CONTROL_SELECT: { + case UI_CONTROL_SELECT: + case UI_CONTROL_DROPDOWN: { const auto find = [modPtr, varHandle] { return static_cast*>( config_find_var(*modPtr, varHandle, CONFIG_VAR_INT)); @@ -361,9 +430,10 @@ bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModC if (find() == nullptr) { return false; } - spec.getInt = [find] { + spec.getInt = [find, dropdown = desc.kind == UI_CONTROL_DROPDOWN] { const auto* var = find(); - return var != nullptr ? clamp_to_int(var->getValue()) : 0; + const auto value = var != nullptr ? var->getValue() : (dropdown ? -1 : 0); + return dropdown && (value < 0 || value > INT_MAX) ? -1 : clamp_to_int(value); }; spec.setInt = [find](int value) { auto* var = find(); @@ -530,24 +600,59 @@ void ui_update_mods_panels(LoadedMod& mod) { [&](ModError* error) { return panel.update(mod.context.get(), panel.userData, error); }); } -ModResult ui_pane_add_section(LoadedMod& mod, uint64_t pane, const char* title) { - auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_section"); +ModResult ui_pane_add_row( + LoadedMod& mod, uint64_t parent, const UiRowDesc& desc, uint64_t* outRow) { + auto* slot = resolve_container(mod, parent, "pane_add_row"); if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - slot->pane->add_section(title); + auto* pane = slot->pane; + auto* helpPane = slot->helpPane; + auto align = ui::Row::Align::Start; + switch (desc.align) { + case UI_ROW_ALIGN_CENTER: + align = ui::Row::Align::Center; + break; + case UI_ROW_ALIGN_END: + align = ui::Row::Align::End; + break; + case UI_ROW_ALIGN_SPACE_BETWEEN: + align = ui::Row::Align::SpaceBetween; + break; + default: + break; + } + auto& row = slot->container->add_child(ui::Row::Props{ + .align = align, + .wrap = desc.wrap, + }); + auto& rowSlot = alloc_slot(mod, UiSlotKind::Row, *outRow); + rowSlot.component = &row; + rowSlot.container = &row; + rowSlot.pane = pane; + rowSlot.helpPane = helpPane; + track_element(*outRow, rowSlot, *row.root()); + return MOD_OK; +} + +ModResult ui_pane_add_section(LoadedMod& mod, uint64_t pane, const char* title) { + auto* slot = resolve_container(mod, pane, "pane_add_section"); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + slot->container->add_section(title); return MOD_OK; } ModResult ui_pane_add_text(LoadedMod& mod, uint64_t pane, const char* text, uint64_t* outElem) { - auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_text"); + auto* slot = resolve_container(mod, pane, "pane_add_text"); if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - auto* elem = slot->pane->add_text(text); + auto* elem = slot->container->add_text(text); if (outElem != nullptr) { auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem); - elemSlot.elementRml = ui::escape(text); + elemSlot.elementValue = text; elemSlot.hasElementValue = true; track_element(*outElem, elemSlot, *elem); } @@ -555,22 +660,23 @@ ModResult ui_pane_add_text(LoadedMod& mod, uint64_t pane, const char* text, uint } ModResult ui_pane_add_rml(LoadedMod& mod, uint64_t pane, const char* rml, uint64_t* outElem) { - auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_rml"); + auto* slot = resolve_container(mod, pane, "pane_add_rml"); if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - auto* elem = slot->pane->add_rml(rml); + auto* elem = slot->container->add_rml(rml); if (outElem != nullptr) { auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem); - elemSlot.elementRml = rml; + elemSlot.elementValue = rml; elemSlot.hasElementValue = true; + elemSlot.elementValueIsRml = true; track_element(*outElem, elemSlot, *elem); } return MOD_OK; } ModResult ui_pane_add_progress(LoadedMod& mod, uint64_t pane, float value, uint64_t* outElem) { - auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_progress"); + auto* slot = resolve_container(mod, pane, "pane_add_progress"); if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } @@ -587,7 +693,7 @@ ModResult ui_pane_add_progress(LoadedMod& mod, uint64_t pane, float value, uint6 ModResult ui_pane_add_control( LoadedMod& mod, uint64_t pane, const UiControlDesc& desc, uint64_t* outElem) { - auto* slot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_control"); + auto* slot = resolve_container(mod, pane, "pane_add_control"); if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } @@ -595,13 +701,24 @@ ModResult ui_pane_add_control( ui::ModControlSpec spec; spec.label = desc.label; spec.helpRml = desc.help_rml != nullptr ? desc.help_rml : ""; - spec.isDisabled = wrap_predicate(mod, desc.is_disabled, desc.user_data, pane); + spec.isDisabled = [modPtr = &mod, pane, + predicate = wrap_predicate(mod, desc.is_disabled, desc.user_data, pane)] { + return !slot_live(pane) || !modPtr->active || (predicate && predicate()); + }; + if (desc.struct_size >= kUiControlTooltipSize && desc.tooltip != nullptr) { + spec.tooltip = desc.tooltip; + } spec.isModified = wrap_predicate(mod, desc.is_modified, desc.user_data, pane); switch (desc.kind) { + case UI_CONTROL_ICON_BUTTON: case UI_CONTROL_BUTTON: case UI_CONTROL_GROUP: spec.kind = desc.kind == UI_CONTROL_BUTTON ? ui::ModControlSpec::Kind::Button : ui::ModControlSpec::Kind::Group; + if (desc.kind == UI_CONTROL_ICON_BUTTON) { + spec.kind = ui::ModControlSpec::Kind::IconButton; + spec.icon = desc.icon; + } if (desc.struct_size >= kUiControlSelectedSize) { spec.isSelected = wrap_predicate(mod, desc.is_selected, desc.user_data, pane); } @@ -650,6 +767,16 @@ ModResult ui_pane_add_control( spec.fileFilters.push_back({desc.file_filters[i].name, desc.file_filters[i].pattern}); } break; + case UI_CONTROL_DROPDOWN: + spec.kind = ui::ModControlSpec::Kind::Dropdown; + for (size_t i = 0; i < desc.option_count; ++i) { + spec.dropdownOptions.push_back({ + desc.options[i], + desc.struct_size < kUiControlOptionsSize || desc.option_enabled == nullptr || + desc.option_enabled[i], + }); + } + break; case UI_CONTROL_SELECT: spec.kind = ui::ModControlSpec::Kind::Select; if (slot->helpPane == nullptr) { @@ -665,7 +792,9 @@ ModResult ui_pane_add_control( return MOD_INVALID_ARGUMENT; } - if (desc.kind != UI_CONTROL_BUTTON && desc.kind != UI_CONTROL_GROUP) { + if (desc.kind != UI_CONTROL_BUTTON && desc.kind != UI_CONTROL_GROUP && + desc.kind != UI_CONTROL_ICON_BUTTON) + { if (desc.binding == UI_BINDING_CONFIG_VAR) { if (!wire_config_var_binding(mod, desc, spec)) { Log.error("[{}] pane_add_control: config var handle {:#x} is unknown or its type " @@ -681,12 +810,15 @@ ModResult ui_pane_add_control( // Copy the pane pointers out: allocating the control's slot below may reallocate s_slots auto* paneComponent = slot->pane; auto* helpPane = slot->helpPane; - auto* control = ui::build_mod_control(*paneComponent, helpPane, std::move(spec)); + auto* control = + ui::build_mod_control(*slot->container, *paneComponent, helpPane, std::move(spec)); if (control == nullptr) { return MOD_UNSUPPORTED; } if (outElem != nullptr) { auto& elemSlot = alloc_slot(mod, UiSlotKind::Control, *outElem); + elemSlot.component = control; + elemSlot.controlKind = desc.kind; track_element(*outElem, elemSlot, *control->root()); } return MOD_OK; @@ -695,11 +827,11 @@ ModResult ui_pane_add_control( ModResult ui_pane_add_list(LoadedMod& mod, uint64_t pane, const UiListDesc& desc, std::vector items, uint64_t& outHandle) { outHandle = 0; - auto* paneSlot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_list"); + auto* paneSlot = resolve_container(mod, pane, "pane_add_list"); if (paneSlot == nullptr) { return MOD_INVALID_ARGUMENT; } - auto* paneComponent = paneSlot->pane; + auto* paneComponent = paneSlot->container; uint64_t handle = 0; alloc_slot(mod, UiSlotKind::List, handle); @@ -740,6 +872,7 @@ ModResult ui_pane_add_list(LoadedMod& mod, uint64_t pane, const UiListDesc& desc if (listSlot == nullptr) { return MOD_ERROR; } + listSlot->component = &list; listSlot->list = &list; track_element(handle, *listSlot, *list.root()); outHandle = handle; @@ -783,6 +916,8 @@ ModResult ui_pane_add_group(LoadedMod& mod, uint64_t groupPaneHandle, uint64_t t if (outElem != nullptr) { auto& elemSlot = alloc_slot(mod, UiSlotKind::Control, *outElem); + elemSlot.component = &button; + elemSlot.controlKind = UI_CONTROL_GROUP; track_element(*outElem, elemSlot, *button.root()); } return MOD_OK; @@ -793,13 +928,13 @@ ModResult ui_elem_set_text(LoadedMod& mod, uint64_t elem, const char* text) { if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - const std::string rml = ui::escape(text); - if (slot->hasElementValue && slot->elementRml == rml) { + if (slot->hasElementValue && !slot->elementValueIsRml && slot->elementValue == text) { return MOD_OK; } - slot->elementRml = rml; + slot->elementValue = text; slot->hasElementValue = true; - slot->element->SetInnerRML(slot->elementRml); + slot->elementValueIsRml = false; + ui::set_text_content(slot->element, slot->elementValue); return MOD_OK; } @@ -808,11 +943,12 @@ ModResult ui_elem_set_rml(LoadedMod& mod, uint64_t elem, const char* rml) { if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - if (slot->hasElementValue && slot->elementRml == rml) { + if (slot->hasElementValue && slot->elementValueIsRml && slot->elementValue == rml) { return MOD_OK; } - slot->elementRml = rml; + slot->elementValue = rml; slot->hasElementValue = true; + slot->elementValueIsRml = true; slot->element->SetInnerRML(rml); return MOD_OK; } @@ -842,6 +978,201 @@ ModResult ui_elem_set_class(LoadedMod& mod, uint64_t elem, const char* name, boo return MOD_OK; } +ModResult ui_control_set_label(LoadedMod& mod, uint64_t handle, const char* label) { + auto* slot = resolve(mod, handle, UiSlotKind::Control, "control_set_label"); + if (slot == nullptr || slot->component == nullptr) { + return MOD_INVALID_ARGUMENT; + } + if (slot->controlKind == UI_CONTROL_ICON_BUTTON) { + if (label[0] == '\0') { + return MOD_INVALID_ARGUMENT; + } + static_cast(slot->component)->set_label(label); + } else if (slot->controlKind == UI_CONTROL_BUTTON) { + static_cast(slot->component)->set_text(label); + } else { + static_cast(slot->component)->set_key(label); + } + return MOD_OK; +} + +ModResult ui_control_set_icon(LoadedMod& mod, uint64_t handle, const char* icon) { + auto* slot = resolve(mod, handle, UiSlotKind::Control, "control_set_icon"); + if (slot == nullptr || slot->controlKind != UI_CONTROL_ICON_BUTTON || + slot->component == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + static_cast(slot->component)->set_icon(icon); + return MOD_OK; +} + +ModResult ui_control_set_tooltip(LoadedMod& mod, uint64_t handle, const char* text) { + auto* slot = resolve(mod, handle, UiSlotKind::Control, "control_set_tooltip"); + if (slot == nullptr || slot->component == nullptr) { + return MOD_INVALID_ARGUMENT; + } + slot->component->set_tooltip(text != nullptr ? text : ""); + return MOD_OK; +} + +ModResult ui_control_set_options( + LoadedMod& mod, uint64_t handle, std::vector options) { + auto* slot = resolve(mod, handle, UiSlotKind::Control, "control_set_options"); + if (slot == nullptr || slot->controlKind != UI_CONTROL_DROPDOWN || slot->component == nullptr) { + return MOD_INVALID_ARGUMENT; + } + static_cast(slot->component)->set_options(std::move(options)); + return MOD_OK; +} + +ModResult ui_elem_set_visible(LoadedMod& mod, uint64_t handle, bool visible) { + auto* slot = resolve_element(mod, handle); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + if (slot->hidden == !visible) { + return MOD_OK; + } + auto element = slot->element->GetObserverPtr(); + auto* context = element->GetContext(); + bool restoreFocus = context != nullptr && element->Contains(context->GetFocusElement()); + slot->hidden = !visible; + if (visible) { + if (slot->displayOverride) { + element->SetProperty(Rml::PropertyId::Display, *slot->displayOverride); + slot->displayOverride.reset(); + } else { + element->RemoveProperty(Rml::PropertyId::Display); + } + } else { + const auto& properties = element->GetLocalStyleProperties(); + if (const auto it = properties.find(Rml::PropertyId::Display); it != properties.end()) { + slot->displayOverride = std::make_unique(it->second); + } + ui::set_display(element.get(), Rml::Style::Display::None); + } + // Focus and menu callbacks may allocate slots. + element->GetOwnerDocument()->UpdateDocument(); + if (!visible && element != nullptr) { + std::vector menus; + s_slots.for_each([&](uint64_t, const auto& entry) { + if (entry.value.kind == UiSlotKind::ContextMenu) { + auto* anchor = slot_from_handle(entry.value.anchorHandle); + if (anchor != nullptr && element->Contains(anchor->element)) { + menus.push_back(static_cast(entry.value.document)); + } + } + }); + for (auto* menu : menus) { + restoreFocus = restoreFocus || menu->has_focus(); + menu->dismiss(false); + } + if (restoreFocus) { + if (auto* top = ui::top_document(); top && top->owns_element(element.get())) { + top->focus(); + } + } + } + return MOD_OK; +} + +ModResult ui_elem_focus(LoadedMod& mod, uint64_t handle) { + auto* slot = resolve_element(mod, handle); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + auto* element = slot->element; + auto* top = ui::top_document(); + if (top == nullptr || !top->owns_element(element)) { + return MOD_UNAVAILABLE; + } + element->GetOwnerDocument()->UpdateDocument(); + slot = resolve_element(mod, handle); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + if (!element_available(*slot)) { + return MOD_UNAVAILABLE; + } + slot = resolve_element(mod, handle); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + auto* component = slot->component; + return (component != nullptr ? component->focus() : element->Focus(true)) ? MOD_OK : + MOD_UNAVAILABLE; +} + +class ModContextMenu final : public ui::ContextMenu { +public: + ModContextMenu(Rml::Element* anchor, std::vector items, uint64_t anchorHandle) + : ContextMenu{anchor, std::move(items)}, mAnchorHandle{anchorHandle} {} + + void update() override { + auto* anchor = slot_from_handle(mAnchorHandle); + if (visible() && (anchor == nullptr || !element_available(*anchor))) { + dismiss(false); + } + ContextMenu::update(); + } + +private: + uint64_t mAnchorHandle; +}; + +ModResult ui_context_menu_push(LoadedMod& mod, uint64_t anchorHandle, + const std::vector& source, uint64_t& outHandle) { + auto* anchor = resolve_element(mod, anchorHandle); + if (anchor == nullptr) { + return MOD_INVALID_ARGUMENT; + } + auto* top = ui::top_document(); + auto* element = anchor->element; + if (top == nullptr || !top->owns_element(anchor->element) || !element_available(*anchor)) { + return MOD_UNAVAILABLE; + } + if (!slot_live(anchorHandle)) { + return MOD_INVALID_ARGUMENT; + } + std::vector items; + items.reserve(source.size()); + for (const auto& item : source) { + items.push_back({ + .text = item.label, + .icon = item.icon != nullptr ? item.icon : "", + .onPressed = + [modPtr = &mod, anchorHandle, fn = item.on_pressed, data = item.user_data] { + if (slot_live(anchorHandle) && fn != nullptr) { + guarded_call(*modPtr, "context menu action", + [&] { fn(modPtr->context.get(), data); }); + } + }, + .enabled = item.enabled && item.on_pressed != nullptr, + .destructive = item.destructive, + .separatorBefore = item.separator_before, + .selected = item.selected, + }); + } + auto menu = std::make_unique(element, std::move(items), anchorHandle); + menu->body()->SetAttribute("mod-id", mod.metadata.id); + auto& slot = alloc_slot(mod, UiSlotKind::ContextMenu, outHandle); + slot.document = menu.get(); + slot.anchorHandle = anchorHandle; + menu->on_close([handle = outHandle] { s_slots.erase(handle); }); + ui::push_document(std::move(menu)); + return MOD_OK; +} + +ModResult ui_context_menu_close(LoadedMod& mod, uint64_t handle) { + auto* slot = resolve(mod, handle, UiSlotKind::ContextMenu, "context_menu_close"); + if (slot == nullptr || slot->document == nullptr) { + return MOD_INVALID_ARGUMENT; + } + static_cast(slot->document)->dismiss(); + return MOD_OK; +} + ModResult ui_window_push(LoadedMod& mod, const UiWindowDesc& desc, uint64_t& outHandle) { outHandle = 0; if (!aurora::rmlui::is_initialized()) { @@ -936,7 +1267,7 @@ ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& out default: break; } - props.title = ui::escape(desc.title); + props.title = desc.title; props.bodyRml = desc.body_rml; props.icon = desc.icon != nullptr ? desc.icon : defaultIcon; props.onDismiss = [modPtr = &mod, handle, fn = desc.on_dismiss, userData = desc.user_data]( @@ -1181,6 +1512,11 @@ void ui_remove_mod(LoadedMod& mod) { } break; } + case UiSlotKind::ContextMenu: + if (slot.document != nullptr) { + slot.document->force_hide(true); + } + break; case UiSlotKind::Style: ui::unregister_scoped_styles(slot.styleScope, slot.styleId); break; @@ -1258,6 +1594,10 @@ bool valid_control_desc(const UiControlDesc& desc) { return false; } switch (desc.kind) { + case UI_CONTROL_ICON_BUTTON: + return desc.struct_size >= kUiControlIconSize && desc.icon != nullptr && + desc.label[0] != '\0' && ui::material_icon(desc.icon)[0] != '\0' && + desc.on_pressed != nullptr; case UI_CONTROL_BUTTON: case UI_CONTROL_GROUP: return desc.on_pressed != nullptr; @@ -1265,6 +1605,7 @@ bool valid_control_desc(const UiControlDesc& desc) { case UI_CONTROL_NUMBER: case UI_CONTROL_STRING: case UI_CONTROL_SELECT: + case UI_CONTROL_DROPDOWN: break; case UI_CONTROL_COLOR: if (desc.struct_size < kColorDescSize) { @@ -1292,8 +1633,8 @@ bool valid_control_desc(const UiControlDesc& desc) { { return false; } - if (desc.kind == UI_CONTROL_SELECT) { - if (desc.options == nullptr || desc.option_count == 0) { + if (desc.kind == UI_CONTROL_SELECT || desc.kind == UI_CONTROL_DROPDOWN) { + if (desc.options == nullptr || desc.option_count == 0 || desc.option_count > INT_MAX) { return false; } for (size_t i = 0; i < desc.option_count; ++i) { @@ -1354,6 +1695,129 @@ bool copy_list_items( return true; } +template +bool copy_records(const T* records, size_t count, size_t minimumSize, Fn&& copy) { + if (count > INT_MAX || (count != 0 && records == nullptr)) { + return false; + } + auto cursor = reinterpret_cast(records); + for (size_t i = 0; i < count; ++i) { + const auto* record = reinterpret_cast(cursor); + const size_t size = record->struct_size; + if (size < minimumSize || size % alignof(T) != 0 || + cursor > std::numeric_limits::max() - size || !copy(*record)) + { + return false; + } + cursor += size; + } + return true; +} + +ModResult ui_control_set_label(ModContext* context, UiElementHandle handle, const char* label) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0 || label == nullptr) { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_control_set_label(*mod, handle, label); +} + +ModResult ui_control_set_icon(ModContext* context, UiElementHandle handle, const char* icon) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0 || icon == nullptr || ui::material_icon(icon)[0] == '\0') { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_control_set_icon(*mod, handle, icon); +} + +ModResult ui_control_set_tooltip(ModContext* context, UiElementHandle handle, const char* text) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_control_set_tooltip(*mod, handle, text); +} + +ModResult ui_control_set_options( + ModContext* context, UiElementHandle handle, const UiControlOption* options, size_t count) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + std::vector copy; + if (!copy_records(options, count, offsetof(UiControlOption, enabled) + sizeof(bool), + [&](const UiControlOption& option) { + if (option.label == nullptr) { + return false; + } + copy.push_back({option.label, option.enabled}); + return true; + })) + { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_control_set_options(*mod, handle, std::move(copy)); +} + +ModResult ui_elem_set_visible(ModContext* context, UiElementHandle handle, bool visible) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_elem_set_visible(*mod, handle, visible); +} + +ModResult ui_elem_focus(ModContext* context, UiElementHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_elem_focus(*mod, handle); +} + +ModResult ui_context_menu_push(ModContext* context, UiElementHandle anchor, + const UiContextMenuDesc* desc, UiContextMenuHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || anchor == 0 || desc == nullptr || + desc->struct_size < sizeof(UiContextMenuDesc) || desc->item_count == 0) + { + return MOD_INVALID_ARGUMENT; + } + std::vector items; + if (!copy_records(desc->items, desc->item_count, + offsetof(UiContextMenuItem, separator_before) + sizeof(bool), + [&](const UiContextMenuItem& item) { + if (item.label == nullptr || item.label[0] == '\0' || + (item.icon != nullptr && item.icon[0] != '\0' && + ui::material_icon(item.icon)[0] == '\0')) + { + return false; + } + items.push_back(item); + return true; + })) + { + return MOD_INVALID_ARGUMENT; + } + uint64_t handle = 0; + const auto result = ui_impl::ui_context_menu_push(*mod, anchor, items, handle); + if (result == MOD_OK && outHandle != nullptr) { + *outHandle = handle; + } + return result; +} + +ModResult ui_context_menu_close(ModContext* context, UiContextMenuHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_context_menu_close(*mod, handle); +} + ModResult ui_register_mods_panel(ModContext* context, const UiModsPanelDesc* desc) { auto* mod = mod_from_context(context); if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiModsPanelDesc) || @@ -1364,6 +1828,21 @@ ModResult ui_register_mods_panel(ModContext* context, const UiModsPanelDesc* des return ui_impl::ui_register_mods_panel(*mod, *desc); } +ModResult ui_pane_add_row( + ModContext* context, UiElementHandle parent, const UiRowDesc* desc, UiElementHandle* outRow) { + if (outRow != nullptr) { + *outRow = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || parent == 0 || outRow == nullptr || desc == nullptr || + desc->struct_size < offsetof(UiRowDesc, wrap) + sizeof(bool) || + desc->align < UI_ROW_ALIGN_START || desc->align > UI_ROW_ALIGN_SPACE_BETWEEN) + { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_pane_add_row(*mod, parent, *desc, outRow); +} + ModResult ui_pane_add_section(ModContext* context, UiElementHandle pane, const char* title) { auto* mod = mod_from_context(context); if (mod == nullptr || pane == 0 || title == nullptr) { @@ -1818,6 +2297,15 @@ constexpr UiService s_uiService{ .set_clipboard_text = ui_set_clipboard_text, .pane_add_list = ui_pane_add_list, .list_set_items = ui_list_set_items, + .pane_add_row = ui_pane_add_row, + .control_set_label = ui_control_set_label, + .control_set_icon = ui_control_set_icon, + .control_set_options = ui_control_set_options, + .control_set_tooltip = ui_control_set_tooltip, + .elem_set_visible = ui_elem_set_visible, + .elem_focus = ui_elem_focus, + .context_menu_push = ui_context_menu_push, + .context_menu_close = ui_context_menu_close, }; } // namespace diff --git a/src/dusk/mods/updates.cpp b/src/dusk/mods/updates.cpp new file mode 100644 index 0000000000..038b9b2091 --- /dev/null +++ b/src/dusk/mods/updates.cpp @@ -0,0 +1,522 @@ +#include "updates.hpp" + +#include "catalog.hpp" +#include "dusk/mod_loader.hpp" +#include "dusk/mods/queue.hpp" +#include "dusk/mods/svc/registry.hpp" +#include "dusk/settings.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#endif + +namespace dusk::mods::updates { +namespace { + +using Clock = std::chrono::steady_clock; +constexpr borealis::Log Log{"dusk::mods::updates"}; +constexpr size_t batchSize = 64; + +struct Store { + State state = State::Idle; + bool initialized = false; + bool automatic = false; + bool requested = false; + bool tracking = false; + bool fresh = false; + uint64_t generation = 0; + uint64_t loaderGeneration = 0; + uint64_t serviceGeneration = 0; + UpdateEnvironment environment; + UpdateEnvironment requestEnvironment; + borealis::Task task; + std::vector targets; + size_t nextTarget = 0; + std::vector pending; + std::vector entries; + std::string error; + int retries = 0; + Clock::time_point checkAt{}; +}; + +Store store; + +bool hosted_id(std::string_view id) { + if (id.empty() || id.size() > 128 || id.front() == '.' || id.back() == '.' || + id.find("..") != id.npos || id.find('.') == id.npos) + { + return false; + } + return std::ranges::all_of(id, [](char c) { + return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '.'; + }); +} + +UpdatePrecondition precondition(const Entry& entry) { + return { + .modId = entry.result.id, + .installedVersion = entry.result.installedVersion, + .targetVersion = entry.result.target->version, + .compatibility = entry.result.target->compatibility, + }; +} + +void refresh_entries() { + for (auto& entry : store.entries) { + std::string reason; + std::string queueKey; + if (const auto queued = queue::find_by_mod_id(entry.result.id); + queued && !queue::is_completed(queued->state)) + { + queueKey = queued->id; + } + const auto* local = ModLoader::instance().find_mod(entry.result.id); + if (local == nullptr || local->metadata.version != entry.result.installedVersion) { + reason = "The installed version changed."; + } else if (!store.fresh) { + reason = "Check for updates again."; + } else if (entry.result.target) { + if (!ModLoader::instance().can_update(*local)) { + reason = "Development directories cannot be updated in-game."; + } else { + reason = validate(store.environment, precondition(entry)); + } + } else if (!entry.result.blockers.empty()) { + reason = entry.result.blockers.front(); + } + const bool actionable = entry.result.target && reason.empty() && queueKey.empty(); + if (entry.reason != reason || entry.queueKey != queueKey || entry.actionable != actionable) + { + entry.reason = std::move(reason); + entry.queueKey = std::move(queueKey); + entry.actionable = actionable; + ++store.generation; + } + } +} + +void start_batch() { + const auto end = std::min(store.nextTarget + batchSize, store.targets.size()); + std::vector targets{ + store.targets.begin() + store.nextTarget, store.targets.begin() + end}; + store.nextTarget = end; + store.task = catalog::fetch_updates(store.requestEnvironment, std::move(targets)); +} + +void begin_check() { + store.requested = false; + store.requestEnvironment = store.environment; + store.targets.clear(); + store.pending.clear(); + store.nextTarget = 0; + for (const auto& mod : store.environment.mods) { + if (hosted_id(mod.id) && borealis::update::parse_version(mod.version)) { + store.targets.push_back(mod.id); + } + } + store.error.clear(); + store.state = State::Checking; + ++store.generation; + if (store.environment.mods.size() > 512 || store.environment.services.size() > 4096) { + store.state = State::Failed; + store.error = "The installed mod inventory exceeds the catalog limit."; + } else if (store.targets.empty()) { + store.entries.clear(); + store.state = State::Ready; + store.fresh = true; + } else { + start_batch(); + } +} + +UpdateEnvironment capture_environment() { + UpdateEnvironment result; + result.platform = catalog::platform(); + result.abi = MOD_ABI_VERSION; +#if defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV) + result.nativeUpdates = false; +#endif + result.services = svc::list_services(); + for (const auto& mod : ModLoader::instance().mods()) { + InstalledPackage installed{ + .id = mod.metadata.id, .version = mod.metadata.version, .enabled = mod.is_enabled()}; + for (const auto& service : mod.manifestInfo.imports) { + if (service.required) { + installed.imports.push_back({service.id, service.major, service.minMinor, false}); + } + } + if (mod.runtime) { + installed.imports.push_back( + {mod.runtime->id, mod.runtime->major, mod.runtime->minMinor, false}); + } + std::ranges::sort(installed.imports, {}, [](const auto& service) { + return std::tie(service.id, service.major, service.minMinor); + }); + // Runtime pins can also be present in the native manifest's required imports. + std::vector imports; + for (const auto& service : installed.imports) { + if (!imports.empty() && imports.back().id == service.id && + imports.back().major == service.major) + { + imports.back().minMinor = std::max(imports.back().minMinor, service.minMinor); + } else { + imports.push_back(service); + } + } + installed.imports = std::move(imports); + result.mods.push_back(std::move(installed)); + } + std::ranges::sort(result.mods, {}, &InstalledPackage::id); + return result; +} + +} // namespace + +std::string validate(const UpdateEnvironment& environment, const UpdatePrecondition& update) { + const auto mod = std::ranges::find(environment.mods, update.modId, &InstalledPackage::id); + if (mod == environment.mods.end()) { + return "The mod is no longer installed."; + } + if (mod->version != update.installedVersion) { + return "The installed version changed. Check for updates again."; + } + const auto installedVersion = borealis::update::parse_version(mod->version); + const auto targetVersion = borealis::update::parse_version(update.targetVersion); + if (!installedVersion || !targetVersion) { + return "The mod version cannot be compared."; + } + if (borealis::update::compare_version(*targetVersion, *installedVersion) <= 0) { + return "This version is already installed or older."; + } + const auto& candidate = update.compatibility; + if (candidate.containsNativeCode) { + if (!environment.nativeUpdates) { + return "Native mod updates are not supported on this device."; + } + if (environment.platform.empty() || std::ranges::find(candidate.platforms, + environment.platform) == candidate.platforms.end()) + { + return "This release does not support this device."; + } + if (candidate.abi != environment.abi) { + return "This release requires a different Dusklight ABI."; + } + } + + std::unordered_map services; + for (const auto& service : environment.services) { + if (service.providerId != update.modId) { + services.emplace(service_key(service.id, service.major), service); + } + } + if (mod->enabled) { + for (auto service : candidate.exports) { + service.providerId = update.modId; + if (!services.emplace(service_key(service.id, service.major), service).second) { + return fmt::format( + "Another provider already supplies {}@{}.", service.id, service.major); + } + } + } + const auto available = [&](const std::string& id, uint16_t major, uint16_t minor) { + const auto service = services.find(service_key(id, major)); + return service != services.end() && service->second.minor >= minor; + }; + for (const auto& service : environment.services) { + if (service.providerId == update.modId && + !available(service.id, service.major, service.minor)) + { + return fmt::format("This release removes or lowers {}@{}.{}.", service.id, + service.major, service.minor); + } + } + for (const auto& required : candidate.imports) { + if (!required.optional && !available(required.id, required.major, required.minMinor)) { + return fmt::format( + "Requires {}@{}.{} or newer.", required.id, required.major, required.minMinor); + } + } + + std::unordered_map> graph; + for (const auto& installed : environment.mods) { + if (!installed.enabled) { + continue; + } + const auto& imports = installed.id == update.modId ? candidate.imports : installed.imports; + for (const auto& required : imports) { + const auto service = services.find(service_key(required.id, required.major)); + if (!required.optional && service != services.end() && + !service->second.providerId.empty() && service->second.providerId != installed.id) + { + graph[installed.id].push_back(service->second.providerId); + } + } + } + std::unordered_set visited; + const std::function reaches_target = [&](const std::string& id) { + if (id == update.modId) { + return true; + } + if (!visited.insert(id).second) { + return false; + } + const auto edges = graph.find(id); + return edges != graph.end() && std::ranges::any_of(edges->second, reaches_target); + }; + if (std::ranges::any_of(graph[update.modId], reaches_target)) { + return "This release introduces a required dependency cycle."; + } + return {}; +} + +std::string validate(const UpdatePrecondition& value) { + const auto* mod = ModLoader::instance().find_mod(value.modId); + if (mod != nullptr && !ModLoader::instance().can_update(*mod)) { + return "Development directories cannot be updated in-game."; + } + return validate(capture_environment(), value); +} + +void update() { + auto& loader = ModLoader::instance(); + if (!loader.initialized()) { + return; + } + if (!borealis::http::available()) { + if (store.state != State::Unavailable) { + store.state = State::Unavailable; + ++store.generation; + } + return; + } + const bool automatic = getSettings().backend.checkForModUpdates.getValue(); + if (automatic && !store.automatic) { + store.requested = true; + store.checkAt = Clock::now() + std::chrono::seconds{1}; + } else if (!automatic && !store.tracking) { + store.requested = false; + } + store.automatic = automatic; + bool changed = !store.initialized || store.loaderGeneration != loader.generation() || + store.serviceGeneration != svc::services_generation(); + if (!changed) { + for (const auto& mod : store.environment.mods) { + const auto* current = loader.find_mod(mod.id); + if (current == nullptr || current->is_enabled() != mod.enabled) { + changed = true; + break; + } + } + } + if (changed) { + auto environment = capture_environment(); + if (!store.initialized || environment != store.environment) { + store.environment = std::move(environment); + store.fresh = false; + if (store.automatic || store.tracking) { + store.requested = true; + store.checkAt = Clock::now() + std::chrono::seconds{1}; + } + ++store.generation; + } + store.initialized = true; + store.loaderGeneration = loader.generation(); + store.serviceGeneration = svc::services_generation(); + } + if (store.task && store.task.ready()) { + auto result = store.task.try_take(); + store.task = {}; + if (store.requestEnvironment != store.environment) { + store.requested = store.automatic || store.tracking; + store.checkAt = Clock::now() + std::chrono::seconds{1}; + } else if (result && result->updates) { + store.pending.insert(store.pending.end(), + std::make_move_iterator(result->updates->begin()), + std::make_move_iterator(result->updates->end())); + if (store.nextTarget < store.targets.size()) { + start_batch(); + } else { + store.entries.clear(); + for (auto& entry : store.pending) { + store.entries.push_back({.result = std::move(entry)}); + } + store.pending.clear(); + store.fresh = true; + store.state = State::Ready; + store.retries = 0; + store.requested = false; + ++store.generation; + Log.info("Checked {} installed mods for updates", store.entries.size()); + } + } else { + store.state = State::Failed; + store.error = result ? result->error : "The update check did not return a result."; + if (result && result->retryable && store.retries < 3 && + (store.automatic || store.tracking)) + { + store.requested = true; + store.checkAt = Clock::now() + std::chrono::seconds{std::max( + result->retryAfter, 5 << store.retries++)}; + } + Log.warn("Mod update check failed: {}", store.error); + ++store.generation; + } + } + if (!store.task && store.requested && Clock::now() >= store.checkAt) { + begin_check(); + } + refresh_entries(); +} + +void shutdown() noexcept { + store = {}; +} + +void request_check() { + store.tracking = true; + if (store.task) { + return; + } + store.requested = true; + store.retries = 0; + store.checkAt = Clock::now(); +} + +State state() noexcept { + return store.state; +} + +uint64_t generation() noexcept { + return store.generation; +} + +const std::vector& entries() noexcept { + return store.entries; +} + +const Entry* find(std::string_view id) { + const auto entry = std::ranges::find( + store.entries, id, [](const Entry& entry) { return std::string_view{entry.result.id}; }); + return entry == store.entries.end() ? nullptr : &*entry; +} + +size_t actionable_count() noexcept { + return std::ranges::count_if(store.entries, &Entry::actionable); +} + +uint64_t download_size() noexcept { + uint64_t total = 0; + for (const auto& entry : store.entries) { + if (entry.actionable) { + total += entry.result.target->download.size; + } + } + return total; +} + +std::string status_text() { + if (!store.initialized) { + return "Waiting for mods to initialize."; + } + if (store.state == State::Unavailable) { + return "Online mod updates are unavailable."; + } + if (store.state == State::Checking) { + return "Checking for mod updates…"; + } + if (store.state == State::Idle) { + return store.requested ? "Checking for mod updates…" : "Automatic update checks are off."; + } + if (!store.error.empty()) { + return fmt::format("Could not check: {}", store.error); + } + if (!store.fresh) { + return store.requested || store.task ? "Installed mods changed. Checking again…" : + "Installed mods changed. Check again."; + } + if (const auto count = actionable_count()) { + return fmt::format("{} update{} available", count, count == 1 ? "" : "s"); + } + if (std::ranges::any_of( + store.entries, [](const Entry& entry) { return !entry.queueKey.empty(); })) + { + return "No other updates available."; + } + if (std::ranges::any_of(store.entries, + [](const Entry& entry) { + return entry.result.target || !entry.result.blockers.empty(); + })) + { + return "No updates ready to install."; + } + return "Your mods are up to date."; +} + +EnqueueResult enqueue_update(std::string_view id) { + const auto* entry = find(id); + if (!entry || !entry->result.target) { + return {.skipped = 1, .error = "Check for updates first."}; + } + if (const auto existing = queue::find_by_mod_id(id); + existing && !queue::is_terminal(existing->state)) + { + return {.skipped = 1, .queueKey = existing->id}; + } + if (!store.fresh) { + return {.skipped = 1, .error = "Check for updates again."}; + } + auto expected = precondition(*entry); + if (auto error = validate(expected); !error.empty()) { + return {.skipped = 1, .error = std::move(error)}; + } + const auto* local = ModLoader::instance().find_mod(id); + const auto& target = *entry->result.target; + std::string key; + if (!queue::enqueue( + { + .id = std::string{id}, + .name = local->metadata.name, + .version = target.version, + .source = target.download, + .update = std::move(expected), + }, + &key)) + { + return {.skipped = 1, .error = "Could not enqueue this update."}; + } + refresh_entries(); + return {.accepted = 1, .queueKey = std::move(key)}; +} + +EnqueueResult enqueue_all() { + std::vector ids; + for (const auto& entry : store.entries) { + if (entry.actionable) { + ids.push_back(entry.result.id); + } + } + EnqueueResult result; + for (const auto& id : ids) { + auto item = enqueue_update(id); + result.accepted += item.accepted; + result.skipped += item.skipped; + if (!item.error.empty()) { + result.error = std::move(item.error); + } + } + return result; +} + +} // namespace dusk::mods::updates diff --git a/src/dusk/mods/updates.hpp b/src/dusk/mods/updates.hpp new file mode 100644 index 0000000000..1526616898 --- /dev/null +++ b/src/dusk/mods/updates.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include "services.hpp" + +#include +#include +#include +#include +#include +#include + +namespace dusk::mods { + +struct Download { + std::string url; + std::string sha256; + uint64_t size = 0; +}; + +struct InstalledPackage { + std::string id; + std::string version; + bool enabled = false; + std::vector imports; + bool operator==(const InstalledPackage&) const = default; +}; + +struct UpdateEnvironment { + std::string platform; + uint32_t abi = 0; + bool nativeUpdates = true; + std::vector services; + std::vector mods; + bool operator==(const UpdateEnvironment&) const = default; +}; + +struct UpdateCompatibility { + bool containsNativeCode = false; + std::vector platforms; + std::optional abi; + std::vector imports; + std::vector exports; +}; + +struct UpdatePrecondition { + std::string modId; + std::string installedVersion; + std::string targetVersion; + UpdateCompatibility compatibility; +}; + +struct UpdateTarget { + std::string version; + Download download; + UpdateCompatibility compatibility; + std::string changelogHtml; +}; + +struct ModUpdate { + std::string id; + std::string installedVersion; + bool published = false; + bool yankedInstalled = false; + std::string latestVersion; + std::vector blockers; + std::optional target; +}; + +} // namespace dusk::mods + +namespace dusk::mods::updates { + +enum class State { Idle, Checking, Ready, Failed, Unavailable }; + +struct Entry { + ModUpdate result; + std::string reason; + std::string queueKey; + bool actionable = false; +}; + +struct EnqueueResult { + size_t accepted = 0; + size_t skipped = 0; + std::string error; + std::string queueKey; +}; + +void update(); +void shutdown() noexcept; +void request_check(); +State state() noexcept; +std::string status_text(); +uint64_t generation() noexcept; +const std::vector& entries() noexcept; +const Entry* find(std::string_view id); +size_t actionable_count() noexcept; +uint64_t download_size() noexcept; +EnqueueResult enqueue_update(std::string_view id); +EnqueueResult enqueue_all(); + +/** Returns the reason an update cannot be applied, or an empty string. */ +std::string validate(const UpdateEnvironment& environment, const UpdatePrecondition& update); + +/** Rechecks an update against the current loader and callable services. */ +std::string validate(const UpdatePrecondition& precondition); + +} // namespace dusk::mods::updates diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index e519d724a1..b690bf0500 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -25,13 +25,13 @@ UserSettings g_userSettings = { }, .audio = { + .outputMode {"audio.outputMode", AudioOutputMode::StereoSpeakers}, .masterVolume {"audio.masterVolume", 60}, .mainMusicVolume {"audio.mainMusicVolume", 100}, .subMusicVolume {"audio.subMusicVolume", 100}, .soundEffectsVolume {"audio.soundEffectsVolume", 100}, .fanfareVolume {"audio.fanfareVolume", 100}, .enableReverb {"audio.enableReverb", true}, - .enableHrtf {"audio.enableHrtf", false}, .menuSounds {"audio.menuSounds", true}, }, @@ -149,6 +149,7 @@ UserSettings g_userSettings = { .fastSpinner {"game.fastSpinner", false}, .armorRupeeDrain {"game.armorRupeeDrain", MagicArmorMode::NORMAL}, .invincibleEnemies {"game.invincibleEnemies", false}, + .easyQuickSpin {"game.easyQuickSpin", false}, // Technical .restoreWiiGlitches {"game.restoreWiiGlitches", false}, @@ -177,6 +178,7 @@ UserSettings g_userSettings = { .skipPreLaunchUI {"backend.skipPreLaunchUI", false}, .wasPresetChosen {"backend.wasPresetChosen", false}, .checkForUpdates {"backend.checkForUpdates", true}, + .checkForModUpdates {"backend.checkForModUpdates", true}, .cardFileType {"backend.cardFileType", static_cast(CARD_GCIFOLDER)}, .enableAdvancedSettings {"backend.enableAdvancedSettings", false}, }, @@ -257,13 +259,13 @@ void registerSettings() { [](const int&, const int&) { dusk::ui::apply_scale(); }); // Audio + Register(g_userSettings.audio.outputMode); Register(g_userSettings.audio.masterVolume); Register(g_userSettings.audio.mainMusicVolume); Register(g_userSettings.audio.subMusicVolume); 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 @@ -355,6 +357,8 @@ void registerSettings() { Register(g_userSettings.game.superClawshot); Register(g_userSettings.game.alwaysGreatspin); Register(g_userSettings.game.invincibleEnemies); + Register(g_userSettings.game.easyQuickSpin); + Register(g_userSettings.game.enableFrameInterpolation); Register(g_userSettings.game.enableGyroAim); Register(g_userSettings.game.enableGyroRollgoal); @@ -390,6 +394,7 @@ void registerSettings() { Register(g_userSettings.backend.skipPreLaunchUI); Register(g_userSettings.backend.wasPresetChosen); Register(g_userSettings.backend.checkForUpdates); + Register(g_userSettings.backend.checkForModUpdates); Register(g_userSettings.backend.cardFileType); Register(g_userSettings.backend.enableAdvancedSettings); diff --git a/src/dusk/settings.h b/src/dusk/settings.h index d03e084bb2..498f2621f4 100644 --- a/src/dusk/settings.h +++ b/src/dusk/settings.h @@ -68,6 +68,13 @@ enum class MagicArmorMode : u8 { COSMETIC = 4, }; +enum class AudioOutputMode : u8 { + StereoSpeakers = 0, + StereoHeadphones = 1, // spatial audio + Surround6ch = 2, // discrete 5.1 + Surround8ch = 3, // discrete 7.1 +}; + namespace config { template <> struct ConfigEnumRange { @@ -123,6 +130,12 @@ struct ConfigEnumRange { static constexpr auto max = MagicArmorMode::COSMETIC; }; +template <> +struct ConfigEnumRange { + static constexpr auto min = AudioOutputMode::StereoSpeakers; + static constexpr auto max = AudioOutputMode::Surround8ch; +}; + template <> struct ConfigValueTraits { static constexpr bool enabled = true; @@ -150,13 +163,13 @@ struct UserSettings { struct { // Audio + ConfigVar outputMode; ConfigVar masterVolume; ConfigVar mainMusicVolume; ConfigVar subMusicVolume; ConfigVar soundEffectsVolume; ConfigVar fanfareVolume; ConfigVar enableReverb; - ConfigVar enableHrtf; ConfigVar menuSounds; } audio; @@ -271,6 +284,7 @@ struct UserSettings { ConfigVar fastSpinner; ConfigVar armorRupeeDrain; ConfigVar invincibleEnemies; + ConfigVar easyQuickSpin; // Technical ConfigVar restoreWiiGlitches; @@ -300,6 +314,7 @@ struct UserSettings { ConfigVar skipPreLaunchUI; ConfigVar wasPresetChosen; ConfigVar checkForUpdates; + ConfigVar checkForModUpdates; ConfigVar cardFileType; ConfigVar enableAdvancedSettings; } backend; diff --git a/src/dusk/stubs.cpp b/src/dusk/stubs.cpp index 3cd99264c4..fb6ef4cf41 100644 --- a/src/dusk/stubs.cpp +++ b/src/dusk/stubs.cpp @@ -239,28 +239,6 @@ void SoundRevID(int a, int b) { STUB_LOG(); } -#pragma mark DC - -void DCFlushRange(void* addr, u32 nBytes) { - // Not needed on PC. -} - -void DCFlushRangeNoSync(void* addr, u32 nBytes) { - // Not needed on PC. -} - -void DCInvalidateRange(void* addr, u32 nBytes) { - // Not needed on PC. -} - -void DCStoreRange(void* addr, u32 nBytes) { - // Not needed on PC. -} - -void DCStoreRangeNoSync(void* addr, u32 nBytes) { - // Not needed on PC. -} - #pragma mark EXI BOOL EXIDeselect(int chan) { @@ -298,12 +276,6 @@ BOOL EXIUnlock(int chan) { return FALSE; } -#pragma mark LC - -void LCEnable() { - STUB_LOG(); -} - // OS-related functions consolidated under "# pragma mark OS" further up #pragma mark VI @@ -1036,11 +1008,6 @@ extern "C" void KPADEnableDPD(s32) { STUB_LOG(); } -// LC (consolidated above) -void LCDisable(void) { - STUB_LOG(); -} - #pragma mark PPC Arch // MSR stuff? void PPCHalt() { diff --git a/src/dusk/ui/achievements.cpp b/src/dusk/ui/achievements.cpp index 3fe713520e..29676f3a66 100644 --- a/src/dusk/ui/achievements.cpp +++ b/src/dusk/ui/achievements.cpp @@ -18,47 +18,39 @@ struct CategoryInfo { }; constexpr CategoryInfo kCategories[] = { - {AchievementCategory::Challenge, "Challenge"}, + {AchievementCategory::Challenge, "Challenge"}, {AchievementCategory::Collection, "Collection"}, - {AchievementCategory::Minigame, "Minigame"}, - {AchievementCategory::Misc, "Misc"}, - {AchievementCategory::Glitched, "Glitched"}, + {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 - ); +void append_achievement_info(Rml::Element* parent, const Achievement& a) { + auto* header = append(parent, "achievement-header"); + auto* name = append(header, "achievement-name"); + name->SetClass("unlocked", a.unlocked); + append_text(name, a.name); + auto* badge = append(header, "achievement-badge"); + badge->SetClass(a.unlocked ? "unlocked" : "locked", true); + append_text(badge, a.unlocked ? "Unlocked" : "Locked"); + auto* description = append(parent, "p"); + description->SetClass("achievement-desc", true); + append_text(description, 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 - ); + const float fraction = a.goal > 0 ? float(a.progress) / float(a.goal) : 1.0f; + auto* progress = append(parent, "progress"); + progress->SetAttribute("value", fraction); + progress->SetClass(a.unlocked ? "progress-done" : "progress-ongoing", true); + append_text( + append(parent, "achievement-progress"), fmt::format("{} / {}", a.progress, a.goal)); } - - return s; } class AchievementRow : public FluentComponent { public: AchievementRow(Rml::Element* parent, const Achievement& a) - : FluentComponent(createRowRoot(parent)) - { + : FluentComponent(createRowRoot(parent)) { auto& btn = add_child - + @@ -162,22 +167,6 @@ DiscVerificationState verification_to_config(iso::ValidationError validation) { } } -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; @@ -329,7 +318,7 @@ void apply_disc_verification_result(const DiscVerificationResult& result) { state.pendingDiscPath = result.path; state.pendingDiscInfo = result.info; state.pendingDiscValidation = result.validation; - state.errorString = escape(get_error_msg(result.validation)); + state.errorString = get_error_msg(result.validation); return; } @@ -345,41 +334,34 @@ void apply_disc_verification_result(const DiscVerificationResult& result) { state.pendingDiscPath.clear(); state.pendingDiscInfo = {}; state.pendingDiscValidation = iso::ValidationError::Unknown; - state.errorString = escape(get_error_msg(result.validation)); + state.errorString = 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); + DiscVerificationModal() : WindowSmall("modal") { + auto* header = append(mDialog, "modal-header"); - auto* title = append(header, "div"); - title->SetClass("modal-title", true); - title->SetInnerRML("Verifying disc image"); + auto* title = append(header, "modal-title"); + append_text(title, "Verifying disc image"); auto* icon = append(header, "icon"); icon->SetClass("verifying", true); - auto* body = append(mDialog, "div"); - body->SetClass("modal-body", true); + auto* body = append(mDialog, "modal-body"); - auto* content = append(body, "div"); - content->SetClass("verification-progress", true); + auto* content = append(body, "verification-progress"); - mFileName = append(content, "div"); - mFileName->SetClass("verification-file", true); + mFileName = append(content, "verification-file"); 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); + mDetail = append(content, "verification-detail"); - auto* actions = append(mDialog, "div"); - actions->SetClass("modal-actions", true); + auto* actions = append(mDialog, "modal-actions"); mCancelButton = std::make_unique