diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..dfe07704 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 83f1cf84..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,300 +0,0 @@ -name: Build - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build-glfw-linux-x86_64: - name: Build (GLFW/Linux x86_64) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Get commit info - id: commit-info - run: | - echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT - - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y cmake libglfw3-dev libbz2-dev - - - name: Configure - run: cmake -B build -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Release "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}" - - - name: Build - run: cmake --build build -j$(nproc) - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: butterscotch-glfw-linux-x86_64 - path: build/butterscotch - - build-glfw-windows-x86_64: - name: Build (GLFW/Windows x86_64) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Get commit info - id: commit-info - run: | - echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT - - - name: Install MinGW and dependencies - run: | - sudo apt-get update - sudo apt-get install -y cmake mingw-w64 - - - name: Build GLFW for MinGW - run: | - git clone https://github.com/glfw/glfw.git /tmp/glfw - git -C /tmp/glfw checkout fdd14e65b1c29e4e6df875fb5669ec00d6793531 - cmake -B /tmp/glfw/build -S /tmp/glfw \ - -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/cmake/mingw-w64.cmake \ - -DCMAKE_INSTALL_PREFIX=/usr/x86_64-w64-mingw32 \ - -DGLFW_BUILD_EXAMPLES=OFF -DGLFW_BUILD_TESTS=OFF -DGLFW_BUILD_DOCS=OFF - make -C /tmp/glfw/build -j$(nproc) - sudo make -C /tmp/glfw/build install - - - name: Build bzip2 for MinGW - run: | - curl -L https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz -o /tmp/bzip2.tar.gz - tar -xf /tmp/bzip2.tar.gz -C /tmp - make -C /tmp/bzip2-1.0.8 libbz2.a CC=x86_64-w64-mingw32-gcc AR=x86_64-w64-mingw32-ar RANLIB=x86_64-w64-mingw32-ranlib -j$(nproc) - sudo cp /tmp/bzip2-1.0.8/libbz2.a /usr/x86_64-w64-mingw32/lib/ - sudo cp /tmp/bzip2-1.0.8/bzlib.h /usr/x86_64-w64-mingw32/include/ - - - name: Configure - run: cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64.cmake -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Release "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}" - - - name: Build - run: cmake --build build -j$(nproc) - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: butterscotch-glfw-windows-x86_64 - path: build/butterscotch.exe - - build-glfw-linux-x86: - name: Build (GLFW/Linux x86) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Get commit info - id: commit-info - run: | - echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT - - - name: Install 32-bit toolchain and dependencies - run: | - sudo dpkg --add-architecture i386 - sudo apt-get update - sudo apt-get install -y cmake gcc-multilib g++-multilib pkg-config libbz2-dev:i386 libx11-dev:i386 libxrandr-dev:i386 libxinerama-dev:i386 libxcursor-dev:i386 libxi-dev:i386 libgl-dev:i386 - - - name: Build GLFW (32-bit) - run: | - git clone https://github.com/glfw/glfw.git /tmp/glfw - git -C /tmp/glfw checkout fdd14e65b1c29e4e6df875fb5669ec00d6793531 - cmake -B /tmp/glfw/build -S /tmp/glfw \ - -DCMAKE_C_FLAGS=-m32 \ - -DCMAKE_INSTALL_PREFIX=/usr/local/glfw-x86 \ - -DGLFW_BUILD_EXAMPLES=OFF -DGLFW_BUILD_TESTS=OFF -DGLFW_BUILD_DOCS=OFF -DGLFW_BUILD_WAYLAND=OFF - make -C /tmp/glfw/build -j$(nproc) - sudo make -C /tmp/glfw/build install - - - name: Configure - run: | - export PKG_CONFIG_PATH=/usr/local/glfw-x86/lib/pkgconfig:/usr/lib/i386-linux-gnu/pkgconfig - cmake -B build -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-m32 -DCMAKE_EXE_LINKER_FLAGS=-m32 "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}" - - - name: Build - run: cmake --build build -j$(nproc) - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: butterscotch-glfw-linux-x86 - path: build/butterscotch - - build-glfw-windows-x86: - name: Build (GLFW/Windows x86) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Get commit info - id: commit-info - run: | - echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT - - - name: Install MinGW and dependencies - run: | - sudo apt-get update - sudo apt-get install -y cmake mingw-w64 - - - name: Build GLFW for MinGW (32-bit) - run: | - git clone https://github.com/glfw/glfw.git /tmp/glfw - git -C /tmp/glfw checkout fdd14e65b1c29e4e6df875fb5669ec00d6793531 - cmake -B /tmp/glfw/build -S /tmp/glfw \ - -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/cmake/mingw-w64-i686.cmake \ - -DCMAKE_INSTALL_PREFIX=/usr/i686-w64-mingw32 \ - -DGLFW_BUILD_EXAMPLES=OFF -DGLFW_BUILD_TESTS=OFF -DGLFW_BUILD_DOCS=OFF - make -C /tmp/glfw/build -j$(nproc) - sudo make -C /tmp/glfw/build install - - - name: Build bzip2 for MinGW (32-bit) - run: | - curl -L https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz -o /tmp/bzip2.tar.gz - tar -xf /tmp/bzip2.tar.gz -C /tmp - make -C /tmp/bzip2-1.0.8 libbz2.a CC=i686-w64-mingw32-gcc AR=i686-w64-mingw32-ar RANLIB=i686-w64-mingw32-ranlib -j$(nproc) - sudo cp /tmp/bzip2-1.0.8/libbz2.a /usr/i686-w64-mingw32/lib/ - sudo cp /tmp/bzip2-1.0.8/bzlib.h /usr/i686-w64-mingw32/include/ - - - name: Configure - run: cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-i686.cmake -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Release "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}" - - - name: Build - run: cmake --build build -j$(nproc) - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: butterscotch-glfw-windows-x86 - path: build/butterscotch.exe - - build-ps2: - name: Build (PS2)${{ matrix.label }} - runs-on: ubuntu-latest - container: - image: ps2dev/ps2dev:latest - strategy: - fail-fast: false - matrix: - include: - - label: "" - artifact: butterscotch-ps2 - extra_flags: "" - - label: " [Bytecode Version 16]" - artifact: butterscotch-ps2-bc16 - extra_flags: "-DENABLE_BC16=ON -DENABLE_BC17=OFF" - - label: " [Bytecode Version 17]" - artifact: butterscotch-ps2-bc17 - extra_flags: "-DENABLE_BC16=OFF -DENABLE_BC17=ON" - steps: - - name: Install build tools - run: apk add --no-cache cmake make gmp-dev mpfr-dev mpc1-dev git - - - uses: actions/checkout@v4 - - # Yeah, the safe.directory is needed because "fatal: detected dubious ownership in repository at '/__w/Butterscotch/Butterscotch'" - - name: Get commit info - id: commit-info - run: | - git config --global --add safe.directory "$GITHUB_WORKSPACE" - echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - echo "date=$(git log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT - - - name: Configure - run: | - mkdir -p build - export CFLAGS="-fopt-info-inline-optimized=$PWD/build/inline_report.txt" - cmake -B build -DCMAKE_TOOLCHAIN_FILE=$PS2SDK/ps2dev.cmake -DPLATFORM=ps2 ${{ matrix.extra_flags }} -DCMAKE_BUILD_TYPE=Release "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}" - - - name: Build - run: | - cmake --build build -j$(nproc) - cp build/butterscotch build/butterscotch.elf - - - name: Check executeLoop I-Cache fit - run: | - # Extract executeLoop function size(s) from the ELF symbol table. - # GCC IPA passes (constprop/isra/part) may clone a static function and - # suffix the clones (e.g. "executeLoop.constprop.0"). Sum all variants - # since they all share the 16 KB I-Cache budget. - # nm -S prints: address size type name - echo "Getting executeLoop size from ELF symbol table..." - SYMBOL_LINES=$(mips64r5900el-ps2-elf-nm -S build/butterscotch.elf | grep -E ' executeLoop(\..*)?$' || true) - if [ -z "$SYMBOL_LINES" ]; then - echo "::error::Could not find executeLoop symbol in ELF (is it stripped?)" - exit 1 - fi - echo "$SYMBOL_LINES" - SIZE_DEC=0 - VARIANT_COUNT=0 - for SIZE_HEX in $(echo "$SYMBOL_LINES" | awk '{print $2}'); do - SIZE_DEC=$((SIZE_DEC + 0x$SIZE_HEX)) - VARIANT_COUNT=$((VARIANT_COUNT + 1)) - done - LIMIT=16384 - if [ "$SIZE_DEC" -gt "$LIMIT" ]; then - echo "::error::executeLoop is $SIZE_DEC bytes across $VARIANT_COUNT variant(s), exceeding the PS2 16 KB I-Cache ($LIMIT bytes) by $((SIZE_DEC - LIMIT)) bytes" - echo "| Function | Size | Limit | Status |" >> $GITHUB_STEP_SUMMARY - echo "|----------|------|-------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| \`executeLoop\` ($VARIANT_COUNT variant(s)) | $SIZE_DEC bytes | $LIMIT bytes (16 KB) | **EXCEEDED** by $((SIZE_DEC - LIMIT)) bytes |" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "| Function | Size | Limit | Status |" >> $GITHUB_STEP_SUMMARY - echo "|----------|------|-------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| \`executeLoop\` ($VARIANT_COUNT variant(s)) | $SIZE_DEC bytes | $LIMIT bytes (16 KB) | $((LIMIT - SIZE_DEC)) bytes remaining |" >> $GITHUB_STEP_SUMMARY - - - name: Report executeLoop inlining - run: | - REPORT=build/inline_report.txt - if [ ! -s "$REPORT" ]; then - echo "::warning::No inline report produced at $REPORT" - exit 0 - fi - # Extract callees that GCC reported as inlined into executeLoop - INLINED=$(grep -E "Inlin(ed|ing) .* into executeLoop" "$REPORT" \ - | awk '{for(i=1;i<=NF;i++) if($i=="into" && $(i+1) ~ /^executeLoop/) {name=$(i-1); sub(/\/[0-9]+$/,"",name); print name}}' \ - | sort -u) - # Cross-check: jal targets still present in the compiled executeLoop - # (including GCC-cloned variants like executeLoop.constprop.0). - VARIANT_NAMES=$(mips64r5900el-ps2-elf-nm build/butterscotch.elf | awk '/ executeLoop(\..*)?$/ {print $3}') - DISASM_ARGS="" - for NAME in $VARIANT_NAMES; do - DISASM_ARGS="$DISASM_ARGS --disassemble=$NAME" - done - NOT_INLINED=$(mips64r5900el-ps2-elf-objdump -d $DISASM_ARGS build/butterscotch.elf 2>/dev/null \ - | grep -oE "jal[[:space:]]+[0-9a-f]+ <[^>]+>" \ - | sed -E 's/.*<([^>]+)>/\1/' \ - | sort -u) - INLINED_COUNT=$(printf "%s\n" "$INLINED" | grep -c . || true) - NOT_INLINED_COUNT=$(printf "%s\n" "$NOT_INLINED" | grep -c . || true) - { - echo "" - echo "### \`executeLoop\` inlining report" - echo "" - echo "
Inlined into executeLoop ($INLINED_COUNT callees, per GCC -fopt-info-inline-optimized)" - echo "" - echo '```' - printf "%s\n" "$INLINED" - echo '```' - echo "" - echo "
" - echo "" - echo "
Still called via jal from executeLoop ($NOT_INLINED_COUNT symbols, per objdump)" - echo "" - echo '```' - printf "%s\n" "$NOT_INLINED" - echo '```' - echo "" - echo "
" - } >> $GITHUB_STEP_SUMMARY - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact }} - path: build/butterscotch.elf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 180e3a3f..4ea31322 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ build_ps2/ .gitignore .cache +.vscode/tasks.json +resources/3ds/romfs/audio/ +resources/3ds/romfs/borders/ +resources/3ds/romfs/gfx/ +resources/3ds/romfs/*.bcwav diff --git a/CMakeLists.txt b/CMakeLists.txt index 0dfab0cb..bd7e4813 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,10 @@ endif() # performance on low end hardware! option(ENABLE_BC16 "Enable support for Bytecode Version 16" ON) option(ENABLE_BC17 "Enable support for Bytecode Version 17" ON) +if(PLATFORM STREQUAL "n3ds") + set(ENABLE_BC16 ON CACHE BOOL "Enable support for Bytecode Version 16" FORCE) + set(ENABLE_BC17 OFF CACHE BOOL "Enable support for Bytecode Version 17" FORCE) +endif() if(NOT ENABLE_BC16 AND NOT ENABLE_BC17) message(FATAL_ERROR "You need to build Butterscotch with at least one bytecode version enabled!") endif() @@ -35,6 +39,27 @@ if(ENABLE_BC17) add_compile_definitions(ENABLE_BC17) endif() +set(BUTTERSCOTCH_WIIU_ICON "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/icon.png") +set(BUTTERSCOTCH_WIIU_DRC_SPLASH "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/drc.png") +set(BUTTERSCOTCH_WIIU_TV_SPLASH "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/tv.png") +set(BUTTERSCOTCH_WIIU_NAME "UNDERTALE") +set(BUTTERSCOTCH_WIIU_SHORTNAME "UNDERTALE") +set(BUTTERSCOTCH_WIIU_APP_FOLDER "cinnamon") +set(BUTTERSCOTCH_3DS_NAME "cinnamon") +set(BUTTERSCOTCH_3DS_DESCRIPTION "cinnamon") +set(BUTTERSCOTCH_3DS_AUTHOR "Project Sunshine") +if(ENABLE_BC17 AND NOT ENABLE_BC16) + set(BUTTERSCOTCH_WIIU_ICON "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/icon-deltarune.png") + set(BUTTERSCOTCH_WIIU_DRC_SPLASH "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/drc-deltarune.png") + set(BUTTERSCOTCH_WIIU_TV_SPLASH "${CMAKE_SOURCE_DIR}/resources/wiiu/meta/tv-deltarune.png") + set(BUTTERSCOTCH_WIIU_NAME "DELTARUNE") + set(BUTTERSCOTCH_WIIU_SHORTNAME "DELTARUNE") +endif() +option(WIIU_USE_DELTARUNE_SD_PATH "Use /wiiu/apps/deltarune instead of /wiiu/apps/cinnamon on Wii U" OFF) +if(WIIU_USE_DELTARUNE_SD_PATH) + set(BUTTERSCOTCH_WIIU_APP_FOLDER "deltarune") +endif() + file(GLOB SOURCES src/*.c) # Platform specific files builds file(GLOB PLATFORM_SOURCES src/${PLATFORM}/*.c) @@ -53,8 +78,10 @@ target_include_directories(butterscotch PUBLIC vendor/stb/ds) if(PLATFORM STREQUAL "glfw") file(GLOB GL_SOURCES src/gl/*.c) + list(REMOVE_ITEM GL_SOURCES ${CMAKE_SOURCE_DIR}/src/gl/gl_renderer.c) target_sources(butterscotch PRIVATE ${GL_SOURCES}) target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/gl) + target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/vendor) # Butterscotch VM/interpreter profiler option(ENABLE_VM_GML_PROFILER "Enable Butterscotch VM/interpreter profiler" ON) @@ -146,6 +173,66 @@ if(PLATFORM STREQUAL "glfw") ${CMAKE_SOURCE_DIR}/vendor/gamecontrollerdb.txt $/gamecontrollerdb.txt ) +elseif(PLATFORM STREQUAL "wiiu") + add_compile_definitions(USE_FLOAT_REALS) + add_compile_definitions(NO_RVALUE_INT64) + # find_package(SDL2 CONFIG REQUIRED) + target_sources(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/gl/image_decoder.c) + target_compile_definitions(butterscotch PRIVATE BUTTERSCOTCH_WIIU_APP_FOLDER="${BUTTERSCOTCH_WIIU_APP_FOLDER}") + + if(DEFINED ENV{DEVKITPRO} AND NOT "$ENV{DEVKITPRO}" STREQUAL "") + file(TO_CMAKE_PATH "$ENV{DEVKITPRO}" DEVKITPRO_ROOT) + elseif(EXISTS "C:/devkitPro") + file(TO_CMAKE_PATH "C:/devkitPro" DEVKITPRO_ROOT) + else() + message(FATAL_ERROR "DEVKITPRO is not set and C:/devkitPro was not found.") + endif() + + # data.win and bytecode blobs are serialized little-endian, but Wii U's PowerPC CPU is big-endian. + target_compile_definitions(butterscotch PRIVATE IS_BIG_ENDIAN) + + target_compile_options(butterscotch PRIVATE + -O3 + -ffunction-sections -fdata-sections + -fsingle-precision-constant + -fomit-frame-pointer + -ffast-math + ) + + target_include_directories(butterscotch PRIVATE + ${CMAKE_SOURCE_DIR}/vendor + ${CMAKE_SOURCE_DIR}/vendor/stb/image + ${CMAKE_SOURCE_DIR}/vendor/stb/vorbis + ${CMAKE_SOURCE_DIR}/vendor/miniaudio + ${DEVKITPRO_ROOT}/wut/include + ${DEVKITPRO_ROOT}/portlibs/wiiu/include + ${DEVKITPRO_ROOT}/portlibs/ppc/include + ) + + target_link_directories(butterscotch PRIVATE + ${DEVKITPRO_ROOT}/portlibs/wiiu/lib + ${DEVKITPRO_ROOT}/portlibs/ppc/lib + ) + target_link_libraries(butterscotch PRIVATE bz2 SDL2) + + set_target_properties(butterscotch PROPERTIES OUTPUT_NAME undertale) + + if(NOT COMMAND wut_create_rpx) + message(FATAL_ERROR "wut_create_rpx is unavailable. Configure with the devkitPro Wii U toolchain file.") + endif() + + wut_create_rpx(butterscotch) + + wut_create_wuhb(butterscotch + NAME ${BUTTERSCOTCH_WIIU_NAME} + SHORTNAME ${BUTTERSCOTCH_WIIU_SHORTNAME} + AUTHOR "Toby Fox, Project Sunshine" + DRCSPLASH ${BUTTERSCOTCH_WIIU_DRC_SPLASH} + ICON ${BUTTERSCOTCH_WIIU_ICON} + TVSPLASH ${BUTTERSCOTCH_WIIU_TV_SPLASH} + CONTENT ${CMAKE_SOURCE_DIR}/resources/wiiu/content + ) + elseif(PLATFORM STREQUAL "ps2") # The PS2 EE has a single-precision FPU only, so double is software-emulated add_compile_definitions(USE_FLOAT_REALS) @@ -332,6 +419,78 @@ elseif(PLATFORM STREQUAL "ps2") ${CMAKE_SOURCE_DIR}/ps2dev-headers/gcc-include ) endif() +elseif(PLATFORM STREQUAL "n3ds") + option(N3DS_ENABLE_LTO "Enable link-time optimization for the 3DS target" ON) + option(N3DS_ENABLE_FAST_MATH "Enable fast-math on the 3DS target" OFF) + option(N3DS_USE_FLOAT_REALS "Use float instead of double for GMLReal on the 3DS target" OFF) + option(N3DS_DISABLE_RVALUE_INT64 "Store oversized int64 values as reals/int32 on the 3DS target" OFF) + option(N3DS_ENABLE_PERF_LOGS "Enable periodic 3DS renderer perf logs" OFF) + option(N3DS_DISABLE_BOTTOM_SCREEN "Disable 3DS bottom-screen rendering" OFF) + + if(DEFINED ENV{DEVKITPRO} AND NOT "$ENV{DEVKITPRO}" STREQUAL "") + file(TO_CMAKE_PATH "$ENV{DEVKITPRO}" DEVKITPRO_ROOT) + elseif(EXISTS "C:/devkitPro") + file(TO_CMAKE_PATH "C:/devkitPro" DEVKITPRO_ROOT) + else() + message(FATAL_ERROR "DEVKITPRO is not set and C:/devkitPro was not found.") + endif() + + target_include_directories(butterscotch PRIVATE + ${DEVKITPRO_ROOT}/libctru/include + ${CMAKE_SOURCE_DIR}/vendor + ${CMAKE_SOURCE_DIR}/vendor/stb/ds + ) + + target_link_directories(butterscotch PRIVATE + ${DEVKITPRO_ROOT}/libctru/lib + ) + + target_compile_options(butterscotch PRIVATE + -O3 + -fomit-frame-pointer + -ffunction-sections + -fdata-sections + $<$:-Os> + ) + target_link_options(butterscotch PRIVATE -Wl,--gc-sections) + + if(N3DS_ENABLE_LTO) + target_compile_options(butterscotch PRIVATE -flto) + target_link_options(butterscotch PRIVATE -flto) + endif() + if(N3DS_ENABLE_FAST_MATH) + target_compile_options(butterscotch PRIVATE -ffast-math) + endif() + if(N3DS_USE_FLOAT_REALS) + target_compile_definitions(butterscotch PRIVATE USE_FLOAT_REALS) + endif() + if(N3DS_DISABLE_RVALUE_INT64) + target_compile_definitions(butterscotch PRIVATE NO_RVALUE_INT64) + endif() + if(N3DS_ENABLE_PERF_LOGS) + target_compile_definitions(butterscotch PRIVATE N3DS_ENABLE_PERF_LOGS) + endif() + if(N3DS_DISABLE_BOTTOM_SCREEN) + target_compile_definitions(butterscotch PRIVATE N3DS_DISABLE_BOTTOM_SCREEN) + endif() + + target_link_libraries(butterscotch PRIVATE citro2d citro3d ctru m) + set_target_properties(butterscotch PROPERTIES OUTPUT_NAME cinnamon) + + set(N3DS_ROMFS_DIR "${CMAKE_SOURCE_DIR}/resources/3ds/romfs") + set(N3DS_SMDH_FILE "${CMAKE_CURRENT_BINARY_DIR}/cinnamon.default.smdh") + ctr_generate_smdh( + OUTPUT "${N3DS_SMDH_FILE}" + NAME "${BUTTERSCOTCH_3DS_NAME}" + DESCRIPTION "${BUTTERSCOTCH_3DS_DESCRIPTION}" + AUTHOR "${BUTTERSCOTCH_3DS_AUTHOR}" + ICON "${CMAKE_SOURCE_DIR}/resources/3ds/meta/icon.png" + ) + if(EXISTS "${N3DS_ROMFS_DIR}") + ctr_create_3dsx(cinnamon TARGET butterscotch SMDH "${N3DS_SMDH_FILE}" ROMFS "${N3DS_ROMFS_DIR}") + else() + ctr_create_3dsx(cinnamon TARGET butterscotch SMDH "${N3DS_SMDH_FILE}") + endif() else() message(FATAL_ERROR "Unknown platform! ${PLATFORM}") endif() diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..6393d099 --- /dev/null +++ b/Makefile @@ -0,0 +1,65 @@ +.SUFFIXES: + +.DEFAULT_GOAL := help + +ROOT_DIR := $(CURDIR) +export ROOT_DIR + +BUILD_3DS := build/n3ds +TARGET_3DS := cinnamon +SOURCES_3DS := src src/n3ds +INCLUDES_3DS := src src/n3ds vendor vendor/stb/ds + +BUILD_WIIU := build/wiiu + +CMAKE ?= cmake +ifeq ($(OS),Windows_NT) +ifneq ($(wildcard /opt/devkitpro/msys2/usr/bin/cmake.exe),) +CMAKE := /opt/devkitpro/msys2/usr/bin/cmake.exe +endif +endif + +DEVKITPRO_CMAKE_PATH := $(DEVKITPRO) +ifeq ($(OS),Windows_NT) +ifneq ($(strip $(DEVKITPRO)),) +CYGPATH := $(firstword $(wildcard C:/devkitPro/msys2/usr/bin/cygpath.exe) $(wildcard /usr/bin/cygpath)) +ifneq ($(strip $(CYGPATH)),) +DEVKITPRO_CMAKE_PATH := $(shell "$(CYGPATH)" -u "$(DEVKITPRO)") +endif +endif +endif + +.PHONY: help 3ds 3ds-clean wiiu wiiu-clean + +help: + @echo "Available targets: 3ds, 3ds-clean, wiiu, wiiu-clean" + +wiiu: + @"$(CMAKE)" --fresh -S "$(ROOT_DIR)" -B "$(ROOT_DIR)/$(BUILD_WIIU)" -G "Unix Makefiles" \ + -DCMAKE_TOOLCHAIN_FILE="$(DEVKITPRO_CMAKE_PATH)/cmake/WiiU.cmake" \ + -DPLATFORM=wiiu \ + -DCMAKE_BUILD_TYPE=Release + @"$(CMAKE)" --build "$(ROOT_DIR)/$(BUILD_WIIU)" + +wiiu-clean: + @rm -rf "$(ROOT_DIR)/$(BUILD_WIIU)" + +THREEDS_GOALS := 3ds + +ifneq ($(filter $(THREEDS_GOALS),$(MAKECMDGOALS)),) + +ifeq ($(strip $(DEVKITPRO)),) +$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=") +endif + +3ds: + @"$(CMAKE)" --fresh -S "$(ROOT_DIR)" -B "$(ROOT_DIR)/$(BUILD_3DS)" -G "Unix Makefiles" \ + -DCMAKE_TOOLCHAIN_FILE="$(DEVKITPRO_CMAKE_PATH)/cmake/3DS.cmake" \ + -DPLATFORM=n3ds \ + -DCMAKE_BUILD_TYPE=Release + @"$(CMAKE)" --build "$(ROOT_DIR)/$(BUILD_3DS)" + +endif + +3ds-clean: + @rm -rf "$(ROOT_DIR)/$(BUILD_3DS)" diff --git a/README.md b/README.md index 8236d8f5..d48f053f 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,44 @@ -

🥧 Butterscotch 🥧

+

Cinnamon

-

- +

+

+ +

+ +--- > [!IMPORTANT] -> Butterscotch is still VERY early in development and it is NOT that good yet. +> Cinnamon is not finished and will have bugs. When you create a game in GameMaker: Studio and export it, GameMaker: Studio exports the game code as bytecode instead of native compiled code, and that bytecode is compatible with any other GameMaker: Studio runner (also known as YoYo runner), as long as they have matching GameMaker: Studio versions. This is similar to how Java applications work. -This is how projects such as [Droidtale](https://mrpowergamerbr.com/projects/droidtale) (which was also made by yours truly) can exist. We exploit that GameMaker: Studio games compile to bytecode, which means they can be ran on *any* platform that has an official runner for it! +This is how projects such as Droidtale can exist. We exploit that GameMaker: Studio games compile to bytecode, which means they can be ran on any platform that has an official runner for it! + +If GameMaker games use bytecode, what prevents us from creating our own runner? And if we can write our own runner, what prevents us from porting GameMaker: Studio games to other platforms? + +Thats where projects like [Butterscotch](https://github.com/MrPowerGamerBR/Butterscotch) come in! Butterscotch is an open source reimplementation of GameMaker: Studio's runner. -Ever since I created Droidtale 10+ years ago, I had that lingering thought in my mind... If GameMaker games use bytecode, what prevents us from creating our *own* runner? And if we can write our *own* runner, what prevents us from porting GameMaker: Studio games to other platforms? +If this already exists, then whats stopping people from porting Butterscotch to MORE consoles? Whats stopping *us* from porting a variety of GameMaker: Studio games to the 3DS and Wii U? -And that's where Butterscotch comes in! Butterscotch is an open source re-implementation of GameMaker: Studio's runner. +This is where Cinnamon, a fork of Butterscotch comes in! + +Cinnamon aims to be a open source re-implementation of GameMaker Studios runner **for the 3DS and Wii U.** This opens up lots of opportunities for games like Pizza Tower, Undertale Yellow, Undertale, and Deltarune to run on the 3DS and Wii U. -**Butterscotch PlayStation 2 ISO Generator:** https://butterscotch.mrpowergamerbr.com/ ## Game Compatibility -Butterscotch's goal is to be able to have Undertale v1.08 (GameMaker: Studio 1.4.1804, Bytecode Version 16) fully playable. But we do want to support more GameMaker: Studio games in the future too! +Butterscotch's and Cinnamon's goal is to be able to have Undertale v1.08 (GameMaker: Studio 1.4.1804, Bytecode Version 16) fully playable. But we do want to support more GameMaker: Studio games in the future too! -While our target is Undertale v1.08, that doesn't mean that other games CAN'T run in Butterscotch! Because Butterscotch is a runner and not a Undertale port/remake, you CAN run other GameMaker: Studio games with it and, as long as the game is compiled with GameMaker: Studio 1.4.1804 and they only use GML variables and functions that Butterscotch supports, it should work fine. +While our target is Undertale v1.08, that doesn't mean that other games CAN'T run in Cinnamon! Because Cinnamon itself is a runner and not a Undertale port/remake, you CAN run other GameMaker: Studio games with it and, as long as the game is compiled with GameMaker: Studio 1.4.1804 and they only use GML variables and functions that Cinnamon supports, it should work fine. -Here are the Bytecode Versions that Butterscotch supports +Here are the Bytecode Versions that Cinnamon supports -* Bytecode Version 15 * Bytecode Version 16 * Bytecode Version 17 -However, that doesn't mean that a game that uses a compatible version WILL run! The bytecode support is still a WIP, and Butterscotch may have quirks that the original GameMaker: Studio runner may not have. +However, that doesn't mean that a game that uses a compatible version WILL run! The bytecode support is still a WIP, and Cinnamon may have quirks that the original GameMaker: Studio runner may not have. Of course, there are exceptions that break game compatibility altogether: @@ -38,128 +46,203 @@ Of course, there are exceptions that break game compatibility altogether: * Games compiled with the new [GMRT](https://github.com/YoYoGames/GMRT-Beta/tree/main), because they use native code instead of bytecode. ## Supported Platforms - -* Linux (GLFW, OpenGL) -* macOS (GLFW, OpenGL) -* Windows (GLFW, OpenGL, MinGW) -* PlayStation 2 (ps2sdk, gsKit) -* Haiku (GLFW) +* Nintendo 3DS +* Nintendo Wii U * ...and maybe more in the future! -## Community Ports +## Project Sunshine +* Project Sunshine is a project that aims to use Cinnamon to port a variety of games (such as UNDERTALE, DELTARUNE, and maybe more games in the future) to the Wii U, 3DS, and maybe more consoles like the GameCube in the future! You can get beta builds on our [Discord](https://discord.gg/AahyBCvVR2) aswell as news on the ports. +### UNDERTALE: Wii U Edition +* A released, full port of UNDERTALE on the Wii U. You can download this port on our releases page or on our Discord. +### DELTARUNE: Wii U Edition +* A full port of DELTARUNE to the Wii U. Still currently in development. Expect a Chapter 1 release date of before July. +### UNDERTALE: 3DS Edition +* A full port of UNDERTALE to the 3DS with 3DS exclusive features such as 3D and bottom screen features. +### DELTARUNE: 3DS Edition +* A full port of atleast Chapter One of DELTARUNE to the 3DS. + +## Building For Wii U + +You must have a proper devkitPro Wii U enviroment set up and configured for your platform. The `wiiu-sdl2` and `ppc-bzip2` devkitPro packages also need to be installed. + +On Windows, make sure MinGW is located in your system PATH (C:/MinGW/bin) before proceeding with build instructions. + +Configure with the Wii U CMake wrapper and then build: + +```bash +powerpc-eabi-cmake -S . -B build/wiiu -DPLATFORM=wiiu -DCMAKE_BUILD_TYPE=Release +cmake --build build/wiiu +``` + +This produces `Cinnamon.elf`, `Cinnamon.rpx`, and a `.wuhb` bundle in `build/wiiu`. + +You can also configure Wii U builds with the toolchain file directly: + +```bash +cmake -S . -B build/wiiu -DPLATFORM=wiiu \ + -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/WiiU.cmake" \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build/wiiu +``` + +On Windows, build from PowerShell with: + +```powershell +.\build-windows-wiiu.ps1 +``` + +## Building for 3DS + +You must have a proper devkitPro 3DS environment set up and configured for your platform. + +On Windows, make sure MinGW is located in your system PATH (C:/MinGW/bin) before proceeding with build instructions. + +Configure and build it with the devkitPro 3DS toolchain: + +```bash +arm-none-eabi-cmake -S . -B build/n3ds -DPLATFORM=n3ds -DCMAKE_BUILD_TYPE=Release +cmake --build build/n3ds +``` + +On Windows, build from PowerShell with: + + +```powershell +.\build-windows-n3ds.ps1 +``` + -* [Xbox 360 (Butterscotch-360)](https://github.com/ceilingtilefan/Butterscotch-360) by @ceilingtilefan -* [3DS and Wii U (Cinnamon)](https://github.com/Project-Sunshine-Native/cinnamon) by @casrielasriel, @grayforz24682, @d16.dorian, @ralcactus +You can also use the repository Makefile on Linux or from an MSYS2/devkitPro shell on Windows: -## Building Butterscotch +```bash +make 3ds +``` + +If you prefer plain CMake, pass the toolchain file explicitly: ```bash -mkdir build && cd build -cmake -DPLATFORM=glfw -DCMAKE_BUILD_TYPE=Debug .. -make +cmake -S . -B build/n3ds -DPLATFORM=n3ds \ + -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/3DS.cmake" \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build/n3ds ``` -If you are using CLion, set the platform in `Settings` > `Build, Execution, Deployment` > `CMake` and add `-DPLATFORM=glfw` +To build without bottom screen features, pass the disable flag like so: -Then run Butterscotch with `./butterscotch /path/to/data.win`! +```bash +cmake -S . -B build/n3ds -DN3DS_DISABLE_BOTTOM_SCREEN=ON +``` + +The main output is `build/n3ds/cinnamon.3dsx`. -## CLI parameters +The 3DS build will package `resources/3ds/romfs` into the `.3dsx` if that directory exists. The runner also checks `sdmc:/3ds/cinnamon` at runtime, so you can either bundle preprocessed assets into `romfs` or copy them onto the SD card. -The GLFW target has a lot of nifty CLI parameters that you can use to trace and debug games running on it. +## Using the 3DS preprocessor -* `--debug`: Enables debugging hotkeys -* `--screenshot=file_%d.png`: Screenshots the runner, requires `--screenshot-at-frame`. -* `--screenshot-at-frame=Frame`: Screenshots the runner at a specific frame. Can be used multiple times. -* `--headless`: Runs the runner in headless mode. When running in headless mode, the game will run at the max speed that your system can handle. -* `--print-rooms`: Prints all the rooms in the `data.win` file and exits. -* `--print-declared-functions`: Prints all the declared functions (scripts, object events, etc) in the `data.win` file and exists. -* `--trace-variable-reads`: Traces variable reads -* `--trace-variable-writes`: Traces variable writes -* `--trace-function-calls`: Traces function calls -* `--trace-alarms`: Traces alarms -* `--trace-instance-lifecycles`: Traces instance creations and deletions -* `--trace-events`: Traces events -* `--trace-event-inherited`: Traces event inherited calls -* `--trace-tiles`: Traces drawn tiles -* `--trace-opcodes`: Traces opcodes -* `--trace-stack`: Traces stack -* `--trace-frames`: Logs when a frame starts and when a frame ends, including how much time it took to process each frame. -* `--always-log-unknown-functions`: When enabled, Butterscotch will always log unknown functions instead of logging them once per script. -* `--always-log-stubbed-functions`: When enabled, Butterscotch will always log stubbed functions instead of logging them once per script. -* `--trace-bytecode-after-frame`: When set, controls when `--trace-opcodes` and `--trace-stack` will start logging. Useful when debugging interpreter-heavy scripts. -* `--exit-at-frame=Frame`: Automatically exit the runner after X frames. -* `--speed`: Speed multiplier -* `--seed=Seed`: Sets a fixed seed for the runner, useful for reproduceable runs. -* `--print-rooms`: Prints all rooms to the console, along with all objects present in the room. -* `--print-declared-functions`: Prints all declared GML scripts by the game -* `--disassemble`: Dissassembles a specific script -* `--record-inputs`: Records user inputs -* `--playback-inputs`: Playbacks user inputs -* `--os-type`: Allows changing the built-in `os_type` value. The default is Windows. Example: When running Undertale Xbox, you would need to set it to `--os-type xboxone`. -* `--profile-gml-scripts`: Logs which GML scripts are the heaviest in terms of time and executed instructions. -* `--profile-opcodes`: Ranks which GML opcodes were executed the most. +The 3DS port expects converted textures and audio instead of the original PC assets. Those files are generated by the standalone `n3ds-preprocess` host tool in `tools/n3ds-preprocess`. -## Debug Features +Build the preprocessor with: -When running Butterscotch with `--debug`, the following hotkeys are enabled: +```bash +cmake -S tools/n3ds-preprocess -B build/n3ds-preprocess -DCMAKE_BUILD_TYPE=Release +cmake --build build/n3ds-preprocess +``` -* `Page Up`: Moves forward one room -* `Page Down`: Moves backwards one room -* `P`: Pauses the game -* `O`: While paused, advances the game loop by one frame -* `F12`: Dumps the current runner state to the console -* `F11`: Dumps the current runner state to the console (JSON format), or dumps it to a file if `--dump-frame-json-file` is set. -* `F10`: Sets the `global.interact` flag to `0`. Useful in Undertale when you are moving through rooms and one of them starts a cutscene that doesn't let you move. +The preprocessor uses: -## Performance +* `tex3ds` from devkitPro for texture conversion +* `stb_vorbis` for decoding OGG vorbis audio files for converting to BCWAV 4-bit ADPCM -Performance is pretty good on any modern computer, but when running on low end targets (like the PS2) it is *very* slow when there's a lot of instances on screen, or when a instance does a for loop. +On Linux: + +* Ensure `tex3ds` is available (usually `/opt/devkitpro/tools/bin/tex3ds`). +* Run the built binary directly: + +```bash +build/n3ds-preprocess/n3ds-preprocess /path/to/data.win resources/3ds/romfs +``` + +If your tools are not in default locations, pass explicit paths: + +```bash +build/n3ds-preprocess/n3ds-preprocess /path/to/data.win resources/3ds/romfs \ + --tex3ds /opt/devkitpro/tools/bin/tex3ds +``` + +On Windows, running `n3ds-preprocess.exe` with no arguments starts an interactive setup that tries to find Undertale automatically and then writes to your SD card layout. + +Command line usage: + +```bash +n3ds-preprocess [options] +``` + +By default the preprocessor uses hybrid atlas formatting: sprite/background pages stay `rgba5551` for cleaner edges, while safer non-sprite pages may still use `etc1a4`. Use `--texture-format etc1a4`, `--texture-format rgba5551`, or `--page-format-overrides ` if you need to force a specific format. + +Useful output directory choices: + +* `resources/3ds/romfs` to bundle the generated assets into the next 3DS build +* Your SD card's `3ds/cinnamon` folder to test assets without rebuilding the `.3dsx` + +Example: generate bundled ROMFS assets from Undertale's `data.win`: + +```bash +build/n3ds-preprocess/n3ds-preprocess /path/to/data.win resources/3ds/romfs +``` + +Example: write directly to an SD card layout: + +```bash +build/n3ds-preprocess/n3ds-preprocess /path/to/data.win /path/to/SD/3ds/cinnamon +``` -## Then why not have a transpiler? +The preprocessor writes: -The issue with a transpiler is that, if you try transpiling the game in the "naive" way, that is, emitting VM calls like it was the original bytecode, you won't get any -*improvement* from it, you would need to create a *good* transpiler that actually transpiles it into *good* code, and that's way harder. +* `gfx/atlas.bin` and converted texture pages to `gfx/` +* `gfx/direct_assets.bin`, containing packed direct sprite/background/font `.t3x` data with seek metadata +* SOND data packed directly in `audio/sound_bank.bin` +* streamed music `.bcwav` files at the output root -Having a transpiler also have other disadvantages: +Optional sprite replacements can be placed in a `Sprite_replacements` folder next to the `n3ds-preprocess` executable or in `tools/n3ds-preprocess/Sprite_replacements`. The preprocessor accepts PNGs named by sprite name or sprite index, such as `spr_battlebutton_0.png`, `spr_battlebutton_0_frame_00000.png`, `spr_00042.png`, or `spr_00042_frame_00000.png`. Replacement PNGs must match the logical sprite frame size. -1. You lose the ability of debugging the runner at a "high level" by tracing opcodes. -2. Compilation is SLOW, transpiling Undertale in a naive way to C and building it takes 90 seconds on a modern computer, and building it to other targets is so slow that I wasn't even able to test it. +Optional room border PNGs can be placed in a `Borders` folder next to the `n3ds-preprocess` executable or in `tools/n3ds-preprocess/Borders`. Every top-level `*.png` in that folder is converted to `gfx/borders/.t3x`; useful names include `border_none.png`, `room_ruins.png`, `room_tundra.png`, `room_water.png`, `room_fire.png`, `room_castle.png`, `room_truelab.png`, and `room_gaster.png`. -## Screenshots +At runtime, direct textures are loaded from `gfx/direct_assets.bin` when present, with loose-file fallback for overrides/custom files. +Generated direct sprite/background/font `.t3x` files are removed after packing to keep output size down. +To reduce SD wear, the preprocessor stages intermediate/unfinalized files in a local temp folder next to the preprocessor executable, then syncs only finalized/changed outputs to your selected destination. -### Undertale (GLFW) [Bytecode Version 16] +## Showcase -Image -Image -Image -Image -Image -Image -Image -Image -Image -Image -Image -Image +### Wii U (Real Hardware) -### Undertale (PlayStation 2) [Bytecode Version 16] +- **UNDERTALE (Bytecode 16)** +Image +Image +Image -Here's a video :3 https://youtu.be/PuzBxe0VGtY +- **SURVEY_PROGRAM (Bytecode 16)** +Image +Image -### DELTARUNE (SURVEY_PROGRAM) (PlayStation 2) [Bytecode Version 16] +### Wii U (Cemu/Emulator) -Here's a video :3 https://youtu.be/TLJtV2WnrmQ +- **SURVEY_PROGRAM (Bytecode 16)** +Image +Image -### DELTARUNE Chapter 2 (GLFW) [Bytecode Version 17] +- **Pizza Tower Demo (Demo 1, Sage 2019 Demo) (Bytecode 16)** +Image +Image -image +### 3DS (Real Hardware) -### DELTARUNE Chapter 3 (GLFW) [Bytecode Version 17] +- **UNDERTALE (Bytecode 16)** +Image +Image +Image +Image -image -image -image +## Disclaimer -### DELTARUNE Chapter Selector (GLFW) [Bytecode Version 17] +Cinnamon has no association, endorsement, or any connection whatsoever with any of the software that it facilitates, and does not provide any of the software it can run by itself. In order to use Cinnamon, you will need to provide your own game files. -image diff --git a/build-windows-n3ds.ps1 b/build-windows-n3ds.ps1 new file mode 100644 index 00000000..6ef9441a --- /dev/null +++ b/build-windows-n3ds.ps1 @@ -0,0 +1,48 @@ +[CmdletBinding()] +param( + [string]$BuildDir = "build\n3ds", + [string]$Configuration = "Release", + [switch]$NoPause +) + +$ErrorActionPreference = "Stop" + +$cmakeExe = "C:\devkitPro\msys2\usr\bin\cmake.exe" +$msysBin = "C:\devkitPro\msys2\usr\bin" +$toolchainFile = "/opt/devkitpro/cmake/3DS.cmake" + +try { + if (-not (Test-Path $cmakeExe)) { + throw "devkitPro CMake was not found at '$cmakeExe'. Install devkitPro with the MSYS2 tools first." + } + + $env:PATH = "$msysBin;$env:PATH" + + & $cmakeExe --fresh -S . -B $BuildDir -G "Unix Makefiles" ` + "-DCMAKE_TOOLCHAIN_FILE=$toolchainFile" ` + -DPLATFORM=n3ds ` + "-DCMAKE_BUILD_TYPE=$Configuration" + if ($LASTEXITCODE -ne 0) { + throw "3DS CMake configure failed." + } + + & $cmakeExe --build $BuildDir + if ($LASTEXITCODE -ne 0) { + throw "3DS build failed." + } + + Write-Host "" + Write-Host "3DS build completed successfully." -ForegroundColor Green +} +catch { + Write-Host "" + Write-Host "3DS build failed." -ForegroundColor Red + Write-Host $_ + exit 1 +} +finally { + if (-not $NoPause) { + Write-Host "" + Read-Host "Press Enter to close" + } +} diff --git a/build-windows-wiiu.ps1 b/build-windows-wiiu.ps1 new file mode 100644 index 00000000..10c70ae8 --- /dev/null +++ b/build-windows-wiiu.ps1 @@ -0,0 +1,48 @@ +[CmdletBinding()] +param( + [string]$BuildDir = "build\wiiu", + [string]$Configuration = "Release", + [switch]$NoPause +) + +$ErrorActionPreference = "Stop" + +$cmakeExe = "C:\devkitPro\msys2\usr\bin\cmake.exe" +$msysBin = "C:\devkitPro\msys2\usr\bin" +$toolchainFile = "/opt/devkitpro/cmake/WiiU.cmake" + +try { + if (-not (Test-Path $cmakeExe)) { + throw "devkitPro CMake was not found at '$cmakeExe'. Install devkitPro with the MSYS2 tools first." + } + + $env:PATH = "$msysBin;$env:PATH" + + & $cmakeExe --fresh -S . -B $BuildDir -G "Unix Makefiles" ` + "-DCMAKE_TOOLCHAIN_FILE=$toolchainFile" ` + -DPLATFORM=wiiu ` + "-DCMAKE_BUILD_TYPE=$Configuration" + if ($LASTEXITCODE -ne 0) { + throw "Wii U CMake configure failed." + } + + & $cmakeExe --build $BuildDir + if ($LASTEXITCODE -ne 0) { + throw "Wii U build failed." + } + + Write-Host "" + Write-Host "Wii U build completed successfully." -ForegroundColor Green +} +catch { + Write-Host "" + Write-Host "Wii U build failed." -ForegroundColor Red + Write-Host $_ + exit 1 +} +finally { + if (-not $NoPause) { + Write-Host "" + Read-Host "Press Enter to close" + } +} diff --git a/cmake/StageN3DSRomfs.cmake b/cmake/StageN3DSRomfs.cmake new file mode 100644 index 00000000..d14829ff --- /dev/null +++ b/cmake/StageN3DSRomfs.cmake @@ -0,0 +1,36 @@ +if(NOT DEFINED SOURCE_DIR OR NOT DEFINED DEST_DIR OR NOT DEFINED STAMP_FILE) + message(FATAL_ERROR "SOURCE_DIR, DEST_DIR, and STAMP_FILE must be set.") +endif() + +file(REMOVE_RECURSE "${DEST_DIR}") +file(MAKE_DIRECTORY "${DEST_DIR}") + +file(GLOB_RECURSE ROMFS_FILES + RELATIVE "${SOURCE_DIR}" + LIST_DIRECTORIES false + "${SOURCE_DIR}/*" +) +list(SORT ROMFS_FILES) + +set(SKIP_PACKED_ATLAS_PAGE_FILES FALSE) +set(PACKED_ATLAS_PATH "${SOURCE_DIR}/gfx/atlas.bin") +if(EXISTS "${PACKED_ATLAS_PATH}") + file(READ "${PACKED_ATLAS_PATH}" PACKED_ATLAS_VERSION_HEX OFFSET 4 LIMIT 2 HEX) + if(PACKED_ATLAS_VERSION_HEX STREQUAL "0800") + set(SKIP_PACKED_ATLAS_PAGE_FILES TRUE) + endif() +endif() + +foreach(REL_PATH IN LISTS ROMFS_FILES) + if(SKIP_PACKED_ATLAS_PAGE_FILES AND REL_PATH MATCHES "^gfx/page_[0-9][0-9][0-9]\\.(t3x|i8)$") + continue() + endif() + set(SRC_PATH "${SOURCE_DIR}/${REL_PATH}") + set(DST_PATH "${DEST_DIR}/${REL_PATH}") + get_filename_component(DST_PARENT "${DST_PATH}" DIRECTORY) + file(MAKE_DIRECTORY "${DST_PARENT}") + file(COPY_FILE "${SRC_PATH}" "${DST_PATH}" ONLY_IF_DIFFERENT) +endforeach() + +list(LENGTH ROMFS_FILES ROMFS_FILE_COUNT) +file(WRITE "${STAMP_FILE}" "staged ${ROMFS_FILE_COUNT} romfs files\n") diff --git a/icon.png b/icon.png new file mode 100644 index 00000000..add868b3 Binary files /dev/null and b/icon.png differ diff --git a/resources/3ds/meta/AppInfo b/resources/3ds/meta/AppInfo new file mode 100644 index 00000000..4ca8aa39 --- /dev/null +++ b/resources/3ds/meta/AppInfo @@ -0,0 +1,9 @@ +APP_TITLE = UNDERTALE +APP_DESCRIPTION = OG game by Toby Fox. +APP_AUTHOR = Sunshine Dev Team +APP_PRODUCT_CODE = 000400000B0E2A00 +APP_UNIQUE_ID = 0x1929 +APP_VERSION_MAJOR = 0 +APP_VERSION_MINOR = 2 +APP_VERSION_MICRO = 0 +APP_SYSTEM_MODE = 0 diff --git a/resources/3ds/meta/audio.mp3 b/resources/3ds/meta/audio.mp3 new file mode 100644 index 00000000..7a2a592b Binary files /dev/null and b/resources/3ds/meta/audio.mp3 differ diff --git a/resources/3ds/meta/audio.wav b/resources/3ds/meta/audio.wav new file mode 100644 index 00000000..b79877c0 Binary files /dev/null and b/resources/3ds/meta/audio.wav differ diff --git a/resources/3ds/meta/banner.png b/resources/3ds/meta/banner.png new file mode 100644 index 00000000..3ff6936c Binary files /dev/null and b/resources/3ds/meta/banner.png differ diff --git a/resources/3ds/meta/icon.png b/resources/3ds/meta/icon.png new file mode 100644 index 00000000..ceb070c2 Binary files /dev/null and b/resources/3ds/meta/icon.png differ diff --git a/resources/3ds/meta/template.rsf b/resources/3ds/meta/template.rsf new file mode 100644 index 00000000..71d9f7e5 --- /dev/null +++ b/resources/3ds/meta/template.rsf @@ -0,0 +1,219 @@ +BasicInfo: + Title : $(APP_TITLE) + ProductCode : $(APP_PRODUCT_CODE) + Logo : Nintendo # Nintendo / Licensed / Distributed / iQue / iQueForSystem + +#RomFs: + # Specifies the root path of the read only file system to include in the ROM. + #RootPath : $(APP_ROMFS) + +TitleInfo: + Category : Application + UniqueId : $(APP_UNIQUE_ID) + +Option: + UseOnSD : true # true if App is to be installed to SD + FreeProductCode : true # Removes limitations on ProductCode + MediaFootPadding : false # If true CCI files are created with padding + EnableCrypt : $(APP_ENCRYPTED) # Enables encryption for NCCH and CIA + EnableCompress : true # Compresses where applicable (currently only exefs:/.code) + +AccessControlInfo: + CoreVersion : 2 + + # Exheader Format Version + DescVersion : 2 + + # Minimum Required Kernel Version (below is for 4.5.0) + ReleaseKernelMajor : "02" + ReleaseKernelMinor : "33" + + # ExtData + UseExtSaveData : false # enables ExtData + #ExtSaveDataId : 0x300 # only set this when the ID is different to the UniqueId + + # FS:USER Archive Access Permissions + # Uncomment as required + FileSystemAccess: + #- CategorySystemApplication + #- CategoryHardwareCheck + - CategoryFileSystemTool + #- Debug + #- TwlCardBackup + #- TwlNandData + #- Boss + - DirectSdmc + #- Core + #- CtrNandRo + #- CtrNandRw + #- CtrNandRoWrite + #- CategorySystemSettings + #- CardBoard + #- ExportImportIvs + - DirectSdmcWrite + #- SwitchCleanup + #- SaveDataMove + #- Shop + #- Shell + #- CategoryHomeMenu + + # Process Settings + MemoryType : Application # Application/System/Base + #SystemMode : $(APP_SYSTEM_MODE) # 64MB(Default)/96MB/80MB/72MB/32MB + IdealProcessor : 0 + AffinityMask : 1 + Priority : 16 + MaxCpu : 0x9E # Default + HandleTableSize : 0x200 + DisableDebug : false + EnableForceDebug : false + CanWriteSharedPage : true + CanUsePrivilegedPriority : false + CanUseNonAlphabetAndNumber : true + PermitMainFunctionArgument : true + CanShareDeviceMemory : false + RunnableOnSleep : false + SpecialMemoryArrange : false + + # New3DS Exclusive Process Settings + #SystemModeExt : Legacy # Legacy(Default)/124MB/178MB Legacy:Use Old3DS SystemMode + #CpuSpeed : 268MHz # 256MHz(Default)/804MHz + #EnableL2Cache : false # false(default)/true + #CanAccessCore2 : false + + # Virtual Address Mappings + #IORegisterMapping: + # - 1ff00000-1ff7ffff # DSP memory + #MemoryMapping: + # - 1f000000-1f5fffff:r # VRAM + + # Accessible SVCs, : + SystemCallAccess: + ArbitrateAddress: 34 + #Backdoor: 123 + Break: 60 + CancelTimer: 28 + ClearEvent: 25 + ClearTimer: 29 + CloseHandle: 35 + ConnectToPort: 45 + ControlMemory: 1 + ControlProcessMemory: 112 + CreateAddressArbiter: 33 + CreateEvent: 23 + CreateMemoryBlock: 30 + CreateMutex: 19 + CreateSemaphore: 21 + CreateThread: 8 + CreateTimer: 26 + DuplicateHandle: 39 + ExitProcess: 3 + ExitThread: 9 + GetCurrentProcessorNumber: 17 + GetHandleInfo: 41 + GetProcessId: 53 + GetProcessIdOfThread: 54 + GetProcessIdealProcessor: 6 + GetProcessInfo: 43 + GetResourceLimit: 56 + GetResourceLimitCurrentValues: 58 + GetResourceLimitLimitValues: 57 + GetSystemInfo: 42 + GetSystemTick: 40 + GetThreadContext: 59 + GetThreadId: 55 + GetThreadIdealProcessor: 15 + GetThreadInfo: 44 + GetThreadPriority: 11 + MapMemoryBlock: 31 + OutputDebugString: 61 + QueryMemory: 2 + ReleaseMutex: 20 + ReleaseSemaphore: 22 + SendSyncRequest1: 46 + SendSyncRequest2: 47 + SendSyncRequest3: 48 + SendSyncRequest4: 49 + SendSyncRequest: 50 + SetThreadPriority: 12 + SetTimer: 27 + SignalEvent: 24 + SleepThread: 10 + UnmapMemoryBlock: 32 + WaitSynchronization1: 36 + WaitSynchronizationN: 37 + + # Service List + # Maximum 34 services (32 if firmware is prior to 9.6.0) + ServiceAccessControl: + - APT:U + #- ac:u + #- am:net + #- boss:U + #- cam:u + #- cecd:u + #- cfg:nor + #- cfg:u + #- csnd:SND + - dsp::DSP + #- frd:u + - fs:USER + - gsp::Gpu + - hid:USER + #- http:C + #- ir:rst + #- ir:u + #- ir:USER + #- mic:u + #- ndm:u + #- news:u + #- nwm::UDS + #- ptm:u + #- pxi:dev + #- soc:U + #- ssl:C + #- y2r:u + + +SystemControlInfo: + SaveDataSize: 0KB # Change if the app uses savedata + RemasterVersion: 2 + StackSize: 0x40000 + + # Modules that run services listed above should be included below + # Maximum 48 dependencies + # : + #Dependency: + #ac: 0x0004013000002402 + #act: 0x0004013000003802 + #am: 0x0004013000001502 + #boss: 0x0004013000003402 + #camera: 0x0004013000001602 + #cecd: 0x0004013000002602 + #cfg: 0x0004013000001702 + #codec: 0x0004013000001802 + #csnd: 0x0004013000002702 + #dlp: 0x0004013000002802 + #dsp: 0x0004013000001a02 + #friends: 0x0004013000003202 + #gpio: 0x0004013000001b02 + # gsp: 0x0004013000001c02 + # hid: 0x0004013000001d02 + #http: 0x0004013000002902 + #i2c: 0x0004013000001e02 + #ir: 0x0004013000003302 + #mcu: 0x0004013000001f02 + #mic: 0x0004013000002002 + #ndm: 0x0004013000002b02 + #news: 0x0004013000003502 + #nfc: 0x0004013000004002 + #nim: 0x0004013000002c02 + #nwm: 0x0004013000002d02 + #pdn: 0x0004013000002102 + #ps: 0x0004013000003102 + #ptm: 0x0004013000002202 + #qtm: 0x0004013020004202 + #ro: 0x0004013000003702 + #socket: 0x0004013000002e02 + #spi: 0x0004013000002302 + #ssl: 0x0004013000002f02 diff --git a/resources/3ds/source/borders/border_none.png b/resources/3ds/source/borders/border_none.png new file mode 100644 index 00000000..e36a34ff Binary files /dev/null and b/resources/3ds/source/borders/border_none.png differ diff --git a/resources/3ds/source/borders/room_castle.png b/resources/3ds/source/borders/room_castle.png new file mode 100644 index 00000000..25e35443 Binary files /dev/null and b/resources/3ds/source/borders/room_castle.png differ diff --git a/resources/3ds/source/borders/room_fire.png b/resources/3ds/source/borders/room_fire.png new file mode 100644 index 00000000..aa45386c Binary files /dev/null and b/resources/3ds/source/borders/room_fire.png differ diff --git a/resources/3ds/source/borders/room_gaster.png b/resources/3ds/source/borders/room_gaster.png new file mode 100644 index 00000000..9bc1a6c8 Binary files /dev/null and b/resources/3ds/source/borders/room_gaster.png differ diff --git a/resources/3ds/source/borders/room_ruins.png b/resources/3ds/source/borders/room_ruins.png new file mode 100644 index 00000000..5137776e Binary files /dev/null and b/resources/3ds/source/borders/room_ruins.png differ diff --git a/resources/3ds/source/borders/room_truelab.png b/resources/3ds/source/borders/room_truelab.png new file mode 100644 index 00000000..710b9719 Binary files /dev/null and b/resources/3ds/source/borders/room_truelab.png differ diff --git a/resources/3ds/source/borders/room_tundra.png b/resources/3ds/source/borders/room_tundra.png new file mode 100644 index 00000000..fd6f4f63 Binary files /dev/null and b/resources/3ds/source/borders/room_tundra.png differ diff --git a/resources/3ds/source/borders/room_water.png b/resources/3ds/source/borders/room_water.png new file mode 100644 index 00000000..024801c6 Binary files /dev/null and b/resources/3ds/source/borders/room_water.png differ diff --git a/resources/3ds/textureOverrides/spr_actbt_center_0.png b/resources/3ds/textureOverrides/spr_actbt_center_0.png new file mode 100644 index 00000000..4e6bbf38 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_actbt_center_0.png differ diff --git a/resources/3ds/textureOverrides/spr_actbt_center_1.png b/resources/3ds/textureOverrides/spr_actbt_center_1.png new file mode 100644 index 00000000..c8490d99 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_actbt_center_1.png differ diff --git a/resources/3ds/textureOverrides/spr_actbt_center_hole_0.png b/resources/3ds/textureOverrides/spr_actbt_center_hole_0.png new file mode 100644 index 00000000..9fb95cbc Binary files /dev/null and b/resources/3ds/textureOverrides/spr_actbt_center_hole_0.png differ diff --git a/resources/3ds/textureOverrides/spr_actbt_center_hole_1.png b/resources/3ds/textureOverrides/spr_actbt_center_hole_1.png new file mode 100644 index 00000000..4e0741a9 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_actbt_center_hole_1.png differ diff --git a/resources/3ds/textureOverrides/spr_fightbt_0.png b/resources/3ds/textureOverrides/spr_fightbt_0.png new file mode 100644 index 00000000..a0711e89 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_fightbt_0.png differ diff --git a/resources/3ds/textureOverrides/spr_fightbt_1.png b/resources/3ds/textureOverrides/spr_fightbt_1.png new file mode 100644 index 00000000..1062648c Binary files /dev/null and b/resources/3ds/textureOverrides/spr_fightbt_1.png differ diff --git a/resources/3ds/textureOverrides/spr_fightbt_hollow_0.png b/resources/3ds/textureOverrides/spr_fightbt_hollow_0.png new file mode 100644 index 00000000..4f895d22 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_fightbt_hollow_0.png differ diff --git a/resources/3ds/textureOverrides/spr_fightbt_hollow_1.png b/resources/3ds/textureOverrides/spr_fightbt_hollow_1.png new file mode 100644 index 00000000..1237a2b6 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_fightbt_hollow_1.png differ diff --git a/resources/3ds/textureOverrides/spr_itembt_0.png b/resources/3ds/textureOverrides/spr_itembt_0.png new file mode 100644 index 00000000..e7df72db Binary files /dev/null and b/resources/3ds/textureOverrides/spr_itembt_0.png differ diff --git a/resources/3ds/textureOverrides/spr_itembt_1.png b/resources/3ds/textureOverrides/spr_itembt_1.png new file mode 100644 index 00000000..c80cfc2a Binary files /dev/null and b/resources/3ds/textureOverrides/spr_itembt_1.png differ diff --git a/resources/3ds/textureOverrides/spr_itembt_hollow_0.png b/resources/3ds/textureOverrides/spr_itembt_hollow_0.png new file mode 100644 index 00000000..72a2187c Binary files /dev/null and b/resources/3ds/textureOverrides/spr_itembt_hollow_0.png differ diff --git a/resources/3ds/textureOverrides/spr_itembt_hollow_1.png b/resources/3ds/textureOverrides/spr_itembt_hollow_1.png new file mode 100644 index 00000000..fc167885 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_itembt_hollow_1.png differ diff --git a/resources/3ds/textureOverrides/spr_mercybutton_normal_0.png b/resources/3ds/textureOverrides/spr_mercybutton_normal_0.png new file mode 100644 index 00000000..3468c7e7 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_mercybutton_normal_0.png differ diff --git a/resources/3ds/textureOverrides/spr_mercybutton_normal_1.png b/resources/3ds/textureOverrides/spr_mercybutton_normal_1.png new file mode 100644 index 00000000..7b799d9d Binary files /dev/null and b/resources/3ds/textureOverrides/spr_mercybutton_normal_1.png differ diff --git a/resources/3ds/textureOverrides/spr_savebt_0.png b/resources/3ds/textureOverrides/spr_savebt_0.png new file mode 100644 index 00000000..f9421bfa Binary files /dev/null and b/resources/3ds/textureOverrides/spr_savebt_0.png differ diff --git a/resources/3ds/textureOverrides/spr_savebt_1.png b/resources/3ds/textureOverrides/spr_savebt_1.png new file mode 100644 index 00000000..d8fc0e53 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_savebt_1.png differ diff --git a/resources/3ds/textureOverrides/spr_sparebt_0.png b/resources/3ds/textureOverrides/spr_sparebt_0.png new file mode 100644 index 00000000..bfb53170 Binary files /dev/null and b/resources/3ds/textureOverrides/spr_sparebt_0.png differ diff --git a/resources/3ds/textureOverrides/spr_sparebt_1.png b/resources/3ds/textureOverrides/spr_sparebt_1.png new file mode 100644 index 00000000..4b75cf8f Binary files /dev/null and b/resources/3ds/textureOverrides/spr_sparebt_1.png differ diff --git a/resources/readme/screenshots/pic1.jpg b/resources/readme/screenshots/pic1.jpg new file mode 100644 index 00000000..b96c08bd Binary files /dev/null and b/resources/readme/screenshots/pic1.jpg differ diff --git a/resources/wiiu/content/.gitkeep b/resources/wiiu/content/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/resources/wiiu/content/.gitkeep @@ -0,0 +1 @@ + diff --git a/resources/wiiu/content/loadingDog.png b/resources/wiiu/content/loadingDog.png new file mode 100644 index 00000000..5ba5d926 Binary files /dev/null and b/resources/wiiu/content/loadingDog.png differ diff --git a/resources/wiiu/meta/drc.png b/resources/wiiu/meta/drc.png new file mode 100644 index 00000000..3fae179c Binary files /dev/null and b/resources/wiiu/meta/drc.png differ diff --git a/resources/wiiu/meta/icon.png b/resources/wiiu/meta/icon.png new file mode 100644 index 00000000..39d6da90 Binary files /dev/null and b/resources/wiiu/meta/icon.png differ diff --git a/resources/wiiu/meta/tv.png b/resources/wiiu/meta/tv.png new file mode 100644 index 00000000..6e929865 Binary files /dev/null and b/resources/wiiu/meta/tv.png differ diff --git a/resources/wiiu/shaders/pos_col.gsh b/resources/wiiu/shaders/pos_col.gsh new file mode 100644 index 00000000..e1f2fbf3 Binary files /dev/null and b/resources/wiiu/shaders/pos_col.gsh differ diff --git a/resources/wiiu/shaders/pos_col.ps b/resources/wiiu/shaders/pos_col.ps new file mode 100644 index 00000000..152b6254 Binary files /dev/null and b/resources/wiiu/shaders/pos_col.ps differ diff --git a/resources/wiiu/shaders/pos_col.vs b/resources/wiiu/shaders/pos_col.vs new file mode 100644 index 00000000..73655a56 --- /dev/null +++ b/resources/wiiu/shaders/pos_col.vs @@ -0,0 +1,11 @@ +#version 420 core + +layout(location = 0) in vec4 aPosition; +layout(location = 1) in vec4 aColour; + +layout(location = 0) out vec4 vColour; + +void main() { + gl_Position = aPosition; + vColour = aColour; +} diff --git a/resources/wiiu/shaders/textured_quad.gsh b/resources/wiiu/shaders/textured_quad.gsh new file mode 100644 index 00000000..4c93756b Binary files /dev/null and b/resources/wiiu/shaders/textured_quad.gsh differ diff --git a/resources/wiiu/shaders/textured_quad.ps b/resources/wiiu/shaders/textured_quad.ps new file mode 100644 index 00000000..2199765f Binary files /dev/null and b/resources/wiiu/shaders/textured_quad.ps differ diff --git a/resources/wiiu/shaders/textured_quad.vs b/resources/wiiu/shaders/textured_quad.vs new file mode 100644 index 00000000..76c72e4f --- /dev/null +++ b/resources/wiiu/shaders/textured_quad.vs @@ -0,0 +1,14 @@ +#version 420 core + +layout(location = 0) in vec4 aPosition; +layout(location = 1) in vec2 aTexCoord; +layout(location = 2) in vec4 aColour; + +layout(location = 0) out vec2 vTexCoord; +layout(location = 1) out vec4 vColour; + +void main() { + gl_Position = aPosition; + vTexCoord = aTexCoord; + vColour = aColour; +} diff --git a/src/audio_system.h b/src/audio_system.h index 82dcd266..f90e751d 100644 --- a/src/audio_system.h +++ b/src/audio_system.h @@ -1,48 +1,50 @@ -#pragma once - -#include "common.h" -#include -#include - -#include "data_win.h" -#include "file_system.h" - -// ===[ AudioSystem Vtable ]=== - +#pragma once + +#include "common.h" +#include +#include + +#include "data_win.h" +#include "file_system.h" + +// ===[ AudioSystem Vtable ]=== + typedef struct AudioSystem AudioSystem; - -typedef struct { - void (*init)(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem); - void (*destroy)(AudioSystem* audio); - void (*update)(AudioSystem* audio, float deltaTime); - int32_t (*playSound)(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop); - void (*stopSound)(AudioSystem* audio, int32_t soundOrInstance); - void (*stopAll)(AudioSystem* audio); - bool (*isPlaying)(AudioSystem* audio, int32_t soundOrInstance); - void (*pauseSound)(AudioSystem* audio, int32_t soundOrInstance); - void (*resumeSound)(AudioSystem* audio, int32_t soundOrInstance); - void (*pauseAll)(AudioSystem* audio); - void (*resumeAll)(AudioSystem* audio); - void (*setSoundGain)(AudioSystem* audio, int32_t soundOrInstance, float gain, uint32_t timeMs); - float (*getSoundGain)(AudioSystem* audio, int32_t soundOrInstance); - void (*setSoundPitch)(AudioSystem* audio, int32_t soundOrInstance, float pitch); - float (*getSoundPitch)(AudioSystem* audio, int32_t soundOrInstance); - float (*getTrackPosition)(AudioSystem* audio, int32_t soundOrInstance); - void (*setTrackPosition)(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds); - // Total length of a sound in seconds. Accepts either a SOND index or an active sound instance id. - // Returns 0.0 if unknown (e.g. stream not yet loaded or invalid index). - float (*getSoundLength)(AudioSystem* audio, int32_t soundOrInstance); - void (*setMasterGain)(AudioSystem* audio, float gain); - void (*setChannelCount)(AudioSystem* audio, int32_t count); +typedef struct Runner Runner; + +typedef struct { + void (*init)(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem); + void (*destroy)(AudioSystem* audio); + void (*update)(AudioSystem* audio, float deltaTime); + int32_t (*playSound)(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop); + void (*stopSound)(AudioSystem* audio, int32_t soundOrInstance); + void (*stopAll)(AudioSystem* audio); + bool (*isPlaying)(AudioSystem* audio, int32_t soundOrInstance); + void (*pauseSound)(AudioSystem* audio, int32_t soundOrInstance); + void (*resumeSound)(AudioSystem* audio, int32_t soundOrInstance); + void (*pauseAll)(AudioSystem* audio); + void (*resumeAll)(AudioSystem* audio); + void (*setSoundGain)(AudioSystem* audio, int32_t soundOrInstance, float gain, uint32_t timeMs); + float (*getSoundGain)(AudioSystem* audio, int32_t soundOrInstance); + void (*setSoundPitch)(AudioSystem* audio, int32_t soundOrInstance, float pitch); + float (*getSoundPitch)(AudioSystem* audio, int32_t soundOrInstance); + float (*getTrackPosition)(AudioSystem* audio, int32_t soundOrInstance); + void (*setTrackPosition)(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds); + // Total length of a sound in seconds. Accepts either a SOND index or an active sound instance id. + // Returns 0.0 if unknown (e.g. stream not yet loaded or invalid index). + float (*getSoundLength)(AudioSystem* audio, int32_t soundOrInstance); + void (*setMasterGain)(AudioSystem* audio, float gain); + void (*setChannelCount)(AudioSystem* audio, int32_t count); void (*groupLoad)(AudioSystem* audio, int32_t groupIndex); bool (*groupIsLoaded)(AudioSystem* audio, int32_t groupIndex); int32_t (*createStream)(AudioSystem* audio, const char* filename); bool (*destroyStream)(AudioSystem* audio, int32_t streamIndex); + void (*prewarmRoom)(AudioSystem* audio, Runner* runner); } AudioSystemVtable; - -// ===[ AudioSystem Base Struct ]=== - -struct AudioSystem { - AudioSystemVtable* vtable; - DataWin** audioGroups; -}; + +// ===[ AudioSystem Base Struct ]=== + +struct AudioSystem { + AudioSystemVtable* vtable; + DataWin** audioGroups; +}; diff --git a/src/binary_reader.c b/src/binary_reader.c index fd61b999..a18ba179 100644 --- a/src/binary_reader.c +++ b/src/binary_reader.c @@ -1,156 +1,156 @@ -#include "binary_reader.h" -#include "binary_utils.h" -#include "utils.h" - -#include -#include - -BinaryReader BinaryReader_create(FILE* file, size_t fileSize) { - return (BinaryReader){.file = file, .fileSize = fileSize, .buffer = nullptr, .bufferBase = 0, .bufferSize = 0, .bufferPos = 0}; -} - -void BinaryReader_setBuffer(BinaryReader* reader, uint8_t* buffer, size_t baseOffset, size_t size) { - reader->buffer = buffer; - reader->bufferBase = baseOffset; - reader->bufferSize = size; - reader->bufferPos = 0; -} - -void BinaryReader_clearBuffer(BinaryReader* reader) { - reader->buffer = nullptr; - reader->bufferBase = 0; - reader->bufferSize = 0; - reader->bufferPos = 0; -} - -static void readCheck(BinaryReader* reader, void* dest, size_t bytes) { - if (reader->buffer != nullptr) { - if (reader->bufferPos + bytes > reader->bufferSize) { - size_t absPos = reader->bufferBase + reader->bufferPos; - fprintf(stderr, "BinaryReader: buffer read error at position 0x%zX (requested %zu bytes, buffer has %zu remaining)\n", absPos, bytes, reader->bufferSize - reader->bufferPos); - abort(); - } - memcpy(dest, reader->buffer + reader->bufferPos, bytes); - reader->bufferPos += bytes; - return; - } - - size_t read = fread(dest, 1, bytes, reader->file); - if (read != bytes) { - long pos = ftell(reader->file) - (long) read; - fprintf(stderr, "BinaryReader: read error at position 0x%lX (requested %zu bytes, got %zu, file size 0x%zX)\n", pos, bytes, read, reader->fileSize); - abort(); - } -} - -uint8_t BinaryReader_readUint8(BinaryReader* reader) { - uint8_t value; - readCheck(reader, &value, 1); - return value; -} - -int16_t BinaryReader_readInt16(BinaryReader* reader) { - uint16_t value; - readCheck(reader, &value, sizeof(value)); - return (int16_t) BinaryUtils_toLittle16(value); -} - -uint16_t BinaryReader_readUint16(BinaryReader* reader) { - uint16_t value; - readCheck(reader, &value, sizeof(value)); - return BinaryUtils_toLittle16(value); -} - -int32_t BinaryReader_readInt32(BinaryReader* reader) { - uint32_t value; - readCheck(reader, &value, sizeof(value)); - return (int32_t) BinaryUtils_toLittle32(value); -} - -uint32_t BinaryReader_readUint32(BinaryReader* reader) { - uint32_t value; - readCheck(reader, &value, sizeof(value)); - return BinaryUtils_toLittle32(value); -} - -float BinaryReader_readFloat32(BinaryReader* reader) { - uint32_t bits; - float value; - readCheck(reader, &bits, sizeof(bits)); - bits = BinaryUtils_toLittle32(bits); - memcpy(&value, &bits, sizeof(value)); - return value; -} - -uint64_t BinaryReader_readUint64(BinaryReader* reader) { - uint64_t value; - readCheck(reader, &value, sizeof(value)); - return BinaryUtils_toLittle64(value); -} - -int64_t BinaryReader_readInt64(BinaryReader* reader) { - uint64_t value; - readCheck(reader, &value, sizeof(value)); - return (int64_t) BinaryUtils_toLittle64(value); -} - -bool BinaryReader_readBool32(BinaryReader* reader) { - return BinaryReader_readUint32(reader) != 0; -} - -void BinaryReader_readBytes(BinaryReader* reader, void* dest, size_t count) { - readCheck(reader, dest, count); -} - -uint8_t* BinaryReader_readBytesAt(BinaryReader* reader, size_t offset, size_t count) { - uint8_t* buf = safeMalloc(count); - - if (reader->buffer != nullptr) { - if (offset < reader->bufferBase || offset + count > reader->bufferBase + reader->bufferSize) { - fprintf(stderr, "BinaryReader: readBytesAt offset 0x%zX+%zu out of buffer range [0x%zX, 0x%zX)\n", offset, count, reader->bufferBase, reader->bufferBase + reader->bufferSize); - abort(); - } - size_t savedPos = reader->bufferPos; - memcpy(buf, reader->buffer + (offset - reader->bufferBase), count); - reader->bufferPos = savedPos; - return buf; - } - - long savedPos = ftell(reader->file); - fseek(reader->file, (long) offset, SEEK_SET); - readCheck(reader, buf, count); - fseek(reader->file, savedPos, SEEK_SET); - return buf; -} - -void BinaryReader_skip(BinaryReader* reader, size_t bytes) { - if (reader->buffer != nullptr) { - reader->bufferPos += bytes; - return; - } - fseek(reader->file, (long) bytes, SEEK_CUR); -} - -void BinaryReader_seek(BinaryReader* reader, size_t position) { - if (reader->buffer != nullptr) { - if (position < reader->bufferBase || position > reader->bufferBase + reader->bufferSize) { - fprintf(stderr, "BinaryReader: buffer seek to 0x%zX out of buffer range [0x%zX, 0x%zX]\n", position, reader->bufferBase, reader->bufferBase + reader->bufferSize); - abort(); - } - reader->bufferPos = position - reader->bufferBase; - return; - } - - if (position > reader->fileSize) { - fprintf(stderr, "BinaryReader: seek to 0x%zX out of bounds (file size 0x%zX)\n", position, reader->fileSize); - abort(); - } - fseek(reader->file, (long) position, SEEK_SET); -} - -size_t BinaryReader_getPosition(BinaryReader* reader) { - if (reader->buffer != nullptr) { - return reader->bufferBase + reader->bufferPos; - } - return (size_t) ftell(reader->file); -} +#include "binary_reader.h" +#include "binary_utils.h" +#include "utils.h" + +#include +#include + +BinaryReader BinaryReader_create(FILE* file, size_t fileSize) { + return (BinaryReader){.file = file, .fileSize = fileSize, .buffer = nullptr, .bufferBase = 0, .bufferSize = 0, .bufferPos = 0}; +} + +void BinaryReader_setBuffer(BinaryReader* reader, uint8_t* buffer, size_t baseOffset, size_t size) { + reader->buffer = buffer; + reader->bufferBase = baseOffset; + reader->bufferSize = size; + reader->bufferPos = 0; +} + +void BinaryReader_clearBuffer(BinaryReader* reader) { + reader->buffer = nullptr; + reader->bufferBase = 0; + reader->bufferSize = 0; + reader->bufferPos = 0; +} + +static void readCheck(BinaryReader* reader, void* dest, size_t bytes) { + if (reader->buffer != nullptr) { + if (reader->bufferPos + bytes > reader->bufferSize) { + size_t absPos = reader->bufferBase + reader->bufferPos; + fprintf(stderr, "BinaryReader: buffer read error at position 0x%zX (requested %zu bytes, buffer has %zu remaining)\n", absPos, bytes, reader->bufferSize - reader->bufferPos); + abort(); + } + memcpy(dest, reader->buffer + reader->bufferPos, bytes); + reader->bufferPos += bytes; + return; + } + + size_t read = fread(dest, 1, bytes, reader->file); + if (read != bytes) { + long pos = ftell(reader->file) - (long) read; + fprintf(stderr, "BinaryReader: read error at position 0x%lX (requested %zu bytes, got %zu, file size 0x%zX)\n", pos, bytes, read, reader->fileSize); + abort(); + } +} + +uint8_t BinaryReader_readUint8(BinaryReader* reader) { + uint8_t value; + readCheck(reader, &value, 1); + return value; +} + +int16_t BinaryReader_readInt16(BinaryReader* reader) { + uint16_t value; + readCheck(reader, &value, sizeof(value)); + return (int16_t) BinaryUtils_toLittle16(value); +} + +uint16_t BinaryReader_readUint16(BinaryReader* reader) { + uint16_t value; + readCheck(reader, &value, sizeof(value)); + return BinaryUtils_toLittle16(value); +} + +int32_t BinaryReader_readInt32(BinaryReader* reader) { + uint32_t value; + readCheck(reader, &value, sizeof(value)); + return (int32_t) BinaryUtils_toLittle32(value); +} + +uint32_t BinaryReader_readUint32(BinaryReader* reader) { + uint32_t value; + readCheck(reader, &value, sizeof(value)); + return BinaryUtils_toLittle32(value); +} + +float BinaryReader_readFloat32(BinaryReader* reader) { + uint32_t bits; + float value; + readCheck(reader, &bits, sizeof(bits)); + bits = BinaryUtils_toLittle32(bits); + memcpy(&value, &bits, sizeof(value)); + return value; +} + +uint64_t BinaryReader_readUint64(BinaryReader* reader) { + uint64_t value; + readCheck(reader, &value, sizeof(value)); + return BinaryUtils_toLittle64(value); +} + +int64_t BinaryReader_readInt64(BinaryReader* reader) { + uint64_t value; + readCheck(reader, &value, sizeof(value)); + return (int64_t) BinaryUtils_toLittle64(value); +} + +bool BinaryReader_readBool32(BinaryReader* reader) { + return BinaryReader_readUint32(reader) != 0; +} + +void BinaryReader_readBytes(BinaryReader* reader, void* dest, size_t count) { + readCheck(reader, dest, count); +} + +uint8_t* BinaryReader_readBytesAt(BinaryReader* reader, size_t offset, size_t count) { + uint8_t* buf = safeMalloc(count); + + if (reader->buffer != nullptr) { + if (offset < reader->bufferBase || offset + count > reader->bufferBase + reader->bufferSize) { + fprintf(stderr, "BinaryReader: readBytesAt offset 0x%zX+%zu out of buffer range [0x%zX, 0x%zX)\n", offset, count, reader->bufferBase, reader->bufferBase + reader->bufferSize); + abort(); + } + size_t savedPos = reader->bufferPos; + memcpy(buf, reader->buffer + (offset - reader->bufferBase), count); + reader->bufferPos = savedPos; + return buf; + } + + long savedPos = ftell(reader->file); + fseek(reader->file, (long) offset, SEEK_SET); + readCheck(reader, buf, count); + fseek(reader->file, savedPos, SEEK_SET); + return buf; +} + +void BinaryReader_skip(BinaryReader* reader, size_t bytes) { + if (reader->buffer != nullptr) { + reader->bufferPos += bytes; + return; + } + fseek(reader->file, (long) bytes, SEEK_CUR); +} + +void BinaryReader_seek(BinaryReader* reader, size_t position) { + if (reader->buffer != nullptr) { + if (position < reader->bufferBase || position > reader->bufferBase + reader->bufferSize) { + fprintf(stderr, "BinaryReader: buffer seek to 0x%zX out of buffer range [0x%zX, 0x%zX]\n", position, reader->bufferBase, reader->bufferBase + reader->bufferSize); + abort(); + } + reader->bufferPos = position - reader->bufferBase; + return; + } + + if (position > reader->fileSize) { + fprintf(stderr, "BinaryReader: seek to 0x%zX out of bounds (file size 0x%zX)\n", position, reader->fileSize); + abort(); + } + fseek(reader->file, (long) position, SEEK_SET); +} + +size_t BinaryReader_getPosition(BinaryReader* reader) { + if (reader->buffer != nullptr) { + return reader->bufferBase + reader->bufferPos; + } + return (size_t) ftell(reader->file); +} diff --git a/src/binary_reader.h b/src/binary_reader.h index e61f6e0e..bdf44426 100644 --- a/src/binary_reader.h +++ b/src/binary_reader.h @@ -1,50 +1,50 @@ -#pragma once - -#include "common.h" -#include -#include -#include -#include - -typedef struct { - FILE* file; - size_t fileSize; - - // When non-null, reads come from this memory buffer instead of the FILE* - // bufferBase is the absolute file offset that buffer[0] corresponds to - uint8_t* buffer; - size_t bufferBase; - size_t bufferSize; - size_t bufferPos; // current read position relative to bufferBase -} BinaryReader; - -BinaryReader BinaryReader_create(FILE* file, size_t fileSize); - -// Sets a memory buffer for bulk chunk reading -// All subsequent reads will come from this buffer until it is cleared -// baseOffset is the absolute file offset that buffer[0] corresponds to -void BinaryReader_setBuffer(BinaryReader* reader, uint8_t* buffer, size_t baseOffset, size_t size); - -// Clears the memory buffer, reverting to FILE*-based reads -void BinaryReader_clearBuffer(BinaryReader* reader); - -uint8_t BinaryReader_readUint8(BinaryReader* reader); -int16_t BinaryReader_readInt16(BinaryReader* reader); -uint16_t BinaryReader_readUint16(BinaryReader* reader); -int32_t BinaryReader_readInt32(BinaryReader* reader); -uint32_t BinaryReader_readUint32(BinaryReader* reader); -float BinaryReader_readFloat32(BinaryReader* reader); -uint64_t BinaryReader_readUint64(BinaryReader* reader); -int64_t BinaryReader_readInt64(BinaryReader* reader); -bool BinaryReader_readBool32(BinaryReader* reader); - -// Copies 'count' bytes from the current position into 'dest'. -void BinaryReader_readBytes(BinaryReader* reader, void* dest, size_t count); - -// Reads 'count' bytes from position 'offset' into a newly allocated buffer. -// Caller must free the returned buffer. -uint8_t* BinaryReader_readBytesAt(BinaryReader* reader, size_t offset, size_t count); - -void BinaryReader_skip(BinaryReader* reader, size_t bytes); -void BinaryReader_seek(BinaryReader* reader, size_t position); -size_t BinaryReader_getPosition(BinaryReader* reader); +#pragma once + +#include "common.h" +#include +#include +#include +#include + +typedef struct { + FILE* file; + size_t fileSize; + + // When non-null, reads come from this memory buffer instead of the FILE* + // bufferBase is the absolute file offset that buffer[0] corresponds to + uint8_t* buffer; + size_t bufferBase; + size_t bufferSize; + size_t bufferPos; // current read position relative to bufferBase +} BinaryReader; + +BinaryReader BinaryReader_create(FILE* file, size_t fileSize); + +// Sets a memory buffer for bulk chunk reading +// All subsequent reads will come from this buffer until it is cleared +// baseOffset is the absolute file offset that buffer[0] corresponds to +void BinaryReader_setBuffer(BinaryReader* reader, uint8_t* buffer, size_t baseOffset, size_t size); + +// Clears the memory buffer, reverting to FILE*-based reads +void BinaryReader_clearBuffer(BinaryReader* reader); + +uint8_t BinaryReader_readUint8(BinaryReader* reader); +int16_t BinaryReader_readInt16(BinaryReader* reader); +uint16_t BinaryReader_readUint16(BinaryReader* reader); +int32_t BinaryReader_readInt32(BinaryReader* reader); +uint32_t BinaryReader_readUint32(BinaryReader* reader); +float BinaryReader_readFloat32(BinaryReader* reader); +uint64_t BinaryReader_readUint64(BinaryReader* reader); +int64_t BinaryReader_readInt64(BinaryReader* reader); +bool BinaryReader_readBool32(BinaryReader* reader); + +// Copies 'count' bytes from the current position into 'dest'. +void BinaryReader_readBytes(BinaryReader* reader, void* dest, size_t count); + +// Reads 'count' bytes from position 'offset' into a newly allocated buffer. +// Caller must free the returned buffer. +uint8_t* BinaryReader_readBytesAt(BinaryReader* reader, size_t offset, size_t count); + +void BinaryReader_skip(BinaryReader* reader, size_t bytes); +void BinaryReader_seek(BinaryReader* reader, size_t position); +size_t BinaryReader_getPosition(BinaryReader* reader); diff --git a/src/binary_utils.h b/src/binary_utils.h index ca720c20..54542e66 100644 --- a/src/binary_utils.h +++ b/src/binary_utils.h @@ -1,202 +1,202 @@ -#pragma once - -#include "common.h" -#include -#include -#include - -// Binary reads/writes from a raw byte buffer. -// When IS_BIG_ENDIAN is defined, reads are byte-swapped to interpret serialized little-endian data. - -#if defined(__clang__) || defined(__GNUC__) -static inline uint16_t BinaryUtils_bswap16(uint16_t value) { - return __builtin_bswap16(value); -} - -static inline uint32_t BinaryUtils_bswap32(uint32_t value) { - return __builtin_bswap32(value); -} - -static inline uint64_t BinaryUtils_bswap64(uint64_t value) { - return __builtin_bswap64(value); -} -#elif defined(_MSC_VER) -static inline uint16_t BinaryUtils_bswap16(uint16_t value) { - return _byteswap_ushort(value); -} - -static inline uint32_t BinaryUtils_bswap32(uint32_t value) { - return _byteswap_ulong(value); -} - -static inline uint64_t BinaryUtils_bswap64(uint64_t value) { - return _byteswap_uint64(value); -} -#else -static inline uint16_t BinaryUtils_bswap16(uint16_t value) { - return (uint16_t) ((value >> 8) | (value << 8)); -} - -static inline uint32_t BinaryUtils_bswap32(uint32_t value) { - return ((value & 0x000000FFu) << 24) | - ((value & 0x0000FF00u) << 8) | - ((value & 0x00FF0000u) >> 8) | - ((value & 0xFF000000u) >> 24); -} - -static inline uint64_t BinaryUtils_bswap64(uint64_t value) { - return ((value & 0x00000000000000FFull) << 56) | - ((value & 0x000000000000FF00ull) << 40) | - ((value & 0x0000000000FF0000ull) << 24) | - ((value & 0x00000000FF000000ull) << 8) | - ((value & 0x000000FF00000000ull) >> 8) | - ((value & 0x0000FF0000000000ull) >> 24) | - ((value & 0x00FF000000000000ull) >> 40) | - ((value & 0xFF00000000000000ull) >> 56); -} -#endif - -static inline uint16_t BinaryUtils_toLittle16(uint16_t value) { -#if defined(IS_BIG_ENDIAN) - return BinaryUtils_bswap16(value); -#else - return value; -#endif -} - -static inline uint32_t BinaryUtils_toLittle32(uint32_t value) { -#if defined(IS_BIG_ENDIAN) - return BinaryUtils_bswap32(value); -#else - return value; -#endif -} - -static inline uint64_t BinaryUtils_toLittle64(uint64_t value) { -#if defined(IS_BIG_ENDIAN) - return BinaryUtils_bswap64(value); -#else - return value; -#endif -} - -static inline uint8_t BinaryUtils_readUint8(const uint8_t* data) { - return data[0]; -} - -static inline uint16_t BinaryUtils_readUint16(const uint8_t* data) { - uint16_t val; - memcpy(&val, data, 2); - return BinaryUtils_toLittle16(val); -} - -static inline int16_t BinaryUtils_readInt16(const uint8_t* data) { - return (int16_t) BinaryUtils_readUint16(data); -} - -static inline uint32_t BinaryUtils_readUint32(const uint8_t* data) { - uint32_t val; - memcpy(&val, data, 4); - return BinaryUtils_toLittle32(val); -} - -static inline int32_t BinaryUtils_readInt32(const uint8_t* data) { - return (int32_t) BinaryUtils_readUint32(data); -} - -static inline uint64_t BinaryUtils_readUint64(const uint8_t* data) { - uint64_t val; - memcpy(&val, data, 8); - return BinaryUtils_toLittle64(val); -} - -static inline int64_t BinaryUtils_readInt64(const uint8_t* data) { - return (int64_t) BinaryUtils_readUint64(data); -} - -static inline float BinaryUtils_readFloat32(const uint8_t* data) { - uint32_t bits = BinaryUtils_readUint32(data); - float val; - memcpy(&val, &bits, 4); - return val; -} - -static inline double BinaryUtils_readFloat64(const uint8_t* data) { - uint64_t bits = BinaryUtils_readUint64(data); - double val; - memcpy(&val, &bits, 8); - return val; -} - -static inline void BinaryUtils_writeUint32(uint8_t* data, uint32_t val) { - val = BinaryUtils_toLittle32(val); - memcpy(data, &val, 4); -} - -static inline void BinaryUtils_writeUint16(uint8_t* data, uint16_t val) { - val = BinaryUtils_toLittle16(val); - memcpy(data, &val, 2); -} - -static inline void BinaryUtils_writeFloat32(uint8_t* data, float val) { - uint32_t bits; - memcpy(&bits, &val, 4); - bits = BinaryUtils_toLittle32(bits); - memcpy(data, &bits, 4); -} - -static inline void BinaryUtils_writeFloat64(uint8_t* data, double val) { - uint64_t bits; - memcpy(&bits, &val, 8); - bits = BinaryUtils_toLittle64(bits); - memcpy(data, &bits, 8); -} - -static inline void BinaryUtils_writeUint64(uint8_t* data, uint64_t val) { - val = BinaryUtils_toLittle64(val); - memcpy(data, &val, 8); -} - -static inline void BinaryUtils_writeInt64(uint8_t* data, int64_t val) { - BinaryUtils_writeUint64(data, (uint64_t) val); -} - -// ===[ Aligned reads ]=== -// These trust the caller to supply a pointer with matching natural alignment. -// Used on the VM dispatch hot path (bytecode instruction / operand fetch) where the bytecode buffer is guaranteed 4-byte aligned. - -static inline uint32_t BinaryUtils_readUint32Aligned(const uint8_t* data) { - uint32_t val; - memcpy(&val, __builtin_assume_aligned(data, 4), 4); - return BinaryUtils_toLittle32(val); -} - -static inline int32_t BinaryUtils_readInt32Aligned(const uint8_t* data) { - return (int32_t) BinaryUtils_readUint32Aligned(data); -} - -static inline int64_t BinaryUtils_readInt64Aligned(const uint8_t* data) { - // Note: GML bytecode places 8-byte extra-data at instruction + 4, so it is only 4-aligned. - uint64_t val; - memcpy(&val, __builtin_assume_aligned(data, 4), 8); - return (int64_t) BinaryUtils_toLittle64(val); -} - -static inline float BinaryUtils_readFloat32Aligned(const uint8_t* data) { - uint32_t bits; - memcpy(&bits, __builtin_assume_aligned(data, 4), 4); - bits = BinaryUtils_toLittle32(bits); - float val; - memcpy(&val, &bits, 4); - return val; -} - -static inline double BinaryUtils_readFloat64Aligned(const uint8_t* data) { - // Note: GML bytecode places 8-byte extra-data at instruction + 4, so it is only 4-aligned. - uint64_t bits; - memcpy(&bits, __builtin_assume_aligned(data, 4), 8); - bits = BinaryUtils_toLittle64(bits); - double val; - memcpy(&val, &bits, 8); - return val; -} +#pragma once + +#include "common.h" +#include +#include +#include + +// Binary reads/writes from a raw byte buffer. +// When IS_BIG_ENDIAN is defined, reads are byte-swapped to interpret serialized little-endian data. + +#if defined(__clang__) || defined(__GNUC__) +static inline uint16_t BinaryUtils_bswap16(uint16_t value) { + return __builtin_bswap16(value); +} + +static inline uint32_t BinaryUtils_bswap32(uint32_t value) { + return __builtin_bswap32(value); +} + +static inline uint64_t BinaryUtils_bswap64(uint64_t value) { + return __builtin_bswap64(value); +} +#elif defined(_MSC_VER) +static inline uint16_t BinaryUtils_bswap16(uint16_t value) { + return _byteswap_ushort(value); +} + +static inline uint32_t BinaryUtils_bswap32(uint32_t value) { + return _byteswap_ulong(value); +} + +static inline uint64_t BinaryUtils_bswap64(uint64_t value) { + return _byteswap_uint64(value); +} +#else +static inline uint16_t BinaryUtils_bswap16(uint16_t value) { + return (uint16_t) ((value >> 8) | (value << 8)); +} + +static inline uint32_t BinaryUtils_bswap32(uint32_t value) { + return ((value & 0x000000FFu) << 24) | + ((value & 0x0000FF00u) << 8) | + ((value & 0x00FF0000u) >> 8) | + ((value & 0xFF000000u) >> 24); +} + +static inline uint64_t BinaryUtils_bswap64(uint64_t value) { + return ((value & 0x00000000000000FFull) << 56) | + ((value & 0x000000000000FF00ull) << 40) | + ((value & 0x0000000000FF0000ull) << 24) | + ((value & 0x00000000FF000000ull) << 8) | + ((value & 0x000000FF00000000ull) >> 8) | + ((value & 0x0000FF0000000000ull) >> 24) | + ((value & 0x00FF000000000000ull) >> 40) | + ((value & 0xFF00000000000000ull) >> 56); +} +#endif + +static inline uint16_t BinaryUtils_toLittle16(uint16_t value) { +#if defined(IS_BIG_ENDIAN) + return BinaryUtils_bswap16(value); +#else + return value; +#endif +} + +static inline uint32_t BinaryUtils_toLittle32(uint32_t value) { +#if defined(IS_BIG_ENDIAN) + return BinaryUtils_bswap32(value); +#else + return value; +#endif +} + +static inline uint64_t BinaryUtils_toLittle64(uint64_t value) { +#if defined(IS_BIG_ENDIAN) + return BinaryUtils_bswap64(value); +#else + return value; +#endif +} + +static inline uint8_t BinaryUtils_readUint8(const uint8_t* data) { + return data[0]; +} + +static inline uint16_t BinaryUtils_readUint16(const uint8_t* data) { + uint16_t val; + memcpy(&val, data, 2); + return BinaryUtils_toLittle16(val); +} + +static inline int16_t BinaryUtils_readInt16(const uint8_t* data) { + return (int16_t) BinaryUtils_readUint16(data); +} + +static inline uint32_t BinaryUtils_readUint32(const uint8_t* data) { + uint32_t val; + memcpy(&val, data, 4); + return BinaryUtils_toLittle32(val); +} + +static inline int32_t BinaryUtils_readInt32(const uint8_t* data) { + return (int32_t) BinaryUtils_readUint32(data); +} + +static inline uint64_t BinaryUtils_readUint64(const uint8_t* data) { + uint64_t val; + memcpy(&val, data, 8); + return BinaryUtils_toLittle64(val); +} + +static inline int64_t BinaryUtils_readInt64(const uint8_t* data) { + return (int64_t) BinaryUtils_readUint64(data); +} + +static inline float BinaryUtils_readFloat32(const uint8_t* data) { + uint32_t bits = BinaryUtils_readUint32(data); + float val; + memcpy(&val, &bits, 4); + return val; +} + +static inline double BinaryUtils_readFloat64(const uint8_t* data) { + uint64_t bits = BinaryUtils_readUint64(data); + double val; + memcpy(&val, &bits, 8); + return val; +} + +static inline void BinaryUtils_writeUint32(uint8_t* data, uint32_t val) { + val = BinaryUtils_toLittle32(val); + memcpy(data, &val, 4); +} + +static inline void BinaryUtils_writeUint16(uint8_t* data, uint16_t val) { + val = BinaryUtils_toLittle16(val); + memcpy(data, &val, 2); +} + +static inline void BinaryUtils_writeFloat32(uint8_t* data, float val) { + uint32_t bits; + memcpy(&bits, &val, 4); + bits = BinaryUtils_toLittle32(bits); + memcpy(data, &bits, 4); +} + +static inline void BinaryUtils_writeFloat64(uint8_t* data, double val) { + uint64_t bits; + memcpy(&bits, &val, 8); + bits = BinaryUtils_toLittle64(bits); + memcpy(data, &bits, 8); +} + +static inline void BinaryUtils_writeUint64(uint8_t* data, uint64_t val) { + val = BinaryUtils_toLittle64(val); + memcpy(data, &val, 8); +} + +static inline void BinaryUtils_writeInt64(uint8_t* data, int64_t val) { + BinaryUtils_writeUint64(data, (uint64_t) val); +} + +// ===[ Aligned reads ]=== +// These trust the caller to supply a pointer with matching natural alignment. +// Used on the VM dispatch hot path (bytecode instruction / operand fetch) where the bytecode buffer is guaranteed 4-byte aligned. + +static inline uint32_t BinaryUtils_readUint32Aligned(const uint8_t* data) { + uint32_t val; + memcpy(&val, __builtin_assume_aligned(data, 4), 4); + return BinaryUtils_toLittle32(val); +} + +static inline int32_t BinaryUtils_readInt32Aligned(const uint8_t* data) { + return (int32_t) BinaryUtils_readUint32Aligned(data); +} + +static inline int64_t BinaryUtils_readInt64Aligned(const uint8_t* data) { + // Note: GML bytecode places 8-byte extra-data at instruction + 4, so it is only 4-aligned. + uint64_t val; + memcpy(&val, __builtin_assume_aligned(data, 4), 8); + return (int64_t) BinaryUtils_toLittle64(val); +} + +static inline float BinaryUtils_readFloat32Aligned(const uint8_t* data) { + uint32_t bits; + memcpy(&bits, __builtin_assume_aligned(data, 4), 4); + bits = BinaryUtils_toLittle32(bits); + float val; + memcpy(&val, &bits, 4); + return val; +} + +static inline double BinaryUtils_readFloat64Aligned(const uint8_t* data) { + // Note: GML bytecode places 8-byte extra-data at instruction + 4, so it is only 4-aligned. + uint64_t bits; + memcpy(&bits, __builtin_assume_aligned(data, 4), 8); + bits = BinaryUtils_toLittle64(bits); + double val; + memcpy(&val, &bits, 8); + return val; +} diff --git a/src/collision.h b/src/collision.h index b3294a6f..98620607 100644 --- a/src/collision.h +++ b/src/collision.h @@ -1,417 +1,417 @@ -#pragma once - -#include "common.h" -#include "data_win.h" -#include "instance.h" -#include "vm.h" - -#include - -// Checks if an instance matches a collision target. -// target >= 100000: instance ID (match specific instance) -// target == INSTANCE_ALL (-3): match any instance -// target >= 0 && < 100000: object index (match via parent chain) -static inline bool Collision_matchesTarget(DataWin* dataWin, Instance* inst, int32_t target) { - if (target >= 100000) return inst->instanceId == target; - if (target == INSTANCE_ALL) return true; - return VM_isObjectOrDescendant(dataWin, inst->objectIndex, target); -} - -typedef struct { - GMLReal left, right, top, bottom; - bool valid; -} InstanceBBox; - -// Returns the collision sprite for an instance (mask sprite if set, else display sprite) -static inline Sprite* Collision_getSprite(DataWin* dataWin, Instance* inst) { - int32_t sprIdx = (inst->maskIndex >= 0) ? inst->maskIndex : inst->spriteIndex; - if (0 > sprIdx || (uint32_t) sprIdx >= dataWin->sprt.count) return nullptr; - return &dataWin->sprt.sprites[sprIdx]; -} - -// Computes the axis-aligned bounding box for an instance using its collision sprite -static inline InstanceBBox Collision_computeBBox(DataWin* dataWin, Instance* inst) { - Sprite* spr = Collision_getSprite(dataWin, inst); - if (spr == nullptr) return (InstanceBBox){0, 0, 0, 0, false}; - - GMLReal marginL = (GMLReal) spr->marginLeft; - GMLReal marginR = (GMLReal) (spr->marginRight + 1); - GMLReal marginT = (GMLReal) spr->marginTop; - GMLReal marginB = (GMLReal) (spr->marginBottom + 1); - GMLReal originX = (GMLReal) spr->originX; - GMLReal originY = (GMLReal) spr->originY; - - if (GMLReal_fabs(inst->imageAngle) > 0.0001) { - // Compute rotated AABB: transform the 4 corners of the unrotated bbox - GMLReal rad = inst->imageAngle * M_PI / 180.0; - GMLReal cs = GMLReal_cos(rad); - GMLReal sn = GMLReal_sin(rad); - - // Local-space corners relative to origin, scaled - GMLReal lx0 = inst->imageXscale * (marginL - originX); - GMLReal ly0 = inst->imageYscale * (marginT - originY); - GMLReal lx1 = inst->imageXscale * (marginR - originX); - GMLReal ly1 = inst->imageYscale * (marginB - originY); - - // Rotate all 4 corners (CW rotation matching renderer's negated angle for Y-down screen coords) - GMLReal cx[4], cy[4]; - cx[0] = cs * lx0 + sn * ly0; cy[0] = -sn * lx0 + cs * ly0; - cx[1] = cs * lx1 + sn * ly0; cy[1] = -sn * lx1 + cs * ly0; - cx[2] = cs * lx0 + sn * ly1; cy[2] = -sn * lx0 + cs * ly1; - cx[3] = cs * lx1 + sn * ly1; cy[3] = -sn * lx1 + cs * ly1; - - GMLReal minX = cx[0], maxX = cx[0], minY = cy[0], maxY = cy[0]; - for (int c = 1; 4 > c; c++) { - if (minX > cx[c]) minX = cx[c]; - if (cx[c] > maxX) maxX = cx[c]; - if (minY > cy[c]) minY = cy[c]; - if (cy[c] > maxY) maxY = cy[c]; - } - - return (InstanceBBox){ - .left = inst->x + minX, - .right = inst->x + maxX, - .top = inst->y + minY, - .bottom = inst->y + maxY, - .valid = true - }; - } - - // No rotation fast path - GMLReal left = inst->x + inst->imageXscale * (marginL - originX); - GMLReal right = inst->x + inst->imageXscale * (marginR - originX); - GMLReal top = inst->y + inst->imageYscale * (marginT - originY); - GMLReal bottom = inst->y + inst->imageYscale * (marginB - originY); - - // Normalize if negative scale - if (left > right) { GMLReal tmp = left; left = right; right = tmp; } - if (top > bottom) { GMLReal tmp = top; top = bottom; bottom = tmp; } - - return (InstanceBBox){left, right, top, bottom, true}; -} - -static inline bool Collision_hasFrameMasks(Sprite* sprite) { - return sprite != nullptr && sprite->sepMasks == 1 && sprite->masks != nullptr && sprite->maskCount > 0; -} - -// Oriented bounding box for a sprite-bearing instance. -// Local rect is the sprite's collision-margin rectangle, scaled by image_xscale/yscale; (cs, sn) is the rotation that takes local-space points to world-space relative to the instance origin (matches Collision_computeBBox: world = inst.pos + (cs*lx + sn*ly, -sn*lx + cs*ly)). -typedef struct { - GMLReal x, y; // World position (instance origin). - GMLReal lx0, lx1; // Local rect X extents (lx0 <= lx1). - GMLReal ly0, ly1; // Local rect Y extents (ly0 <= ly1). - GMLReal cs, sn; // cos/sin of imageAngle (in radians). - bool rotated; // true if abs(imageAngle) > epsilon. -} InstanceOBB; - -static inline InstanceOBB Collision_instanceOBB(Sprite* spr, Instance* inst) { - InstanceOBB obb; - obb.x = inst->x; - obb.y = inst->y; - GMLReal marginL = (GMLReal) spr->marginLeft; - GMLReal marginR = (GMLReal) (spr->marginRight + 1); - GMLReal marginT = (GMLReal) spr->marginTop; - GMLReal marginB = (GMLReal) (spr->marginBottom + 1); - GMLReal originX = (GMLReal) spr->originX; - GMLReal originY = (GMLReal) spr->originY; - obb.lx0 = inst->imageXscale * (marginL - originX); - obb.lx1 = inst->imageXscale * (marginR - originX); - obb.ly0 = inst->imageYscale * (marginT - originY); - obb.ly1 = inst->imageYscale * (marginB - originY); - if (obb.lx0 > obb.lx1) { GMLReal t = obb.lx0; obb.lx0 = obb.lx1; obb.lx1 = t; } - if (obb.ly0 > obb.ly1) { GMLReal t = obb.ly0; obb.ly0 = obb.ly1; obb.ly1 = t; } - obb.rotated = GMLReal_fabs(inst->imageAngle) > 0.0001; - if (obb.rotated) { - GMLReal rad = inst->imageAngle * M_PI / 180.0; - obb.cs = GMLReal_cos(rad); - obb.sn = GMLReal_sin(rad); - } else { - obb.cs = 1.0; - obb.sn = 0.0; - } - return obb; -} - -// Inverse-transforms a world point into OBB local coordinates. -static inline void Collision_obbWorldToLocal(const InstanceOBB* obb, GMLReal wx, GMLReal wy, GMLReal* outLx, GMLReal* outLy) { - GMLReal dx = wx - obb->x; - GMLReal dy = wy - obb->y; - *outLx = dx * obb->cs - dy * obb->sn; - *outLy = dx * obb->sn + dy * obb->cs; -} - -// Returns true iff the OBB needs SAT-style testing rather than AABB. Only sepMasks == 2 sprites that are actually rotated qualify; everything else (axis-aligned, or precise sprites which fall through to per-pixel scans) is handled correctly by AABB. -static inline bool Collision_obbNeedsSAT(Sprite* spr, Instance* inst) { - return spr != nullptr && spr->sepMasks == 2 && GMLReal_fabs(inst->imageAngle) > 0.0001; -} - -static inline bool Collision_rectOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal rx1, GMLReal ry1, GMLReal rx2, GMLReal ry2) { - InstanceBBox bbox = Collision_computeBBox(dataWin, inst); - if (!bbox.valid) return false; - - if (rx1 >= bbox.right || bbox.left >= rx2 || ry1 >= bbox.bottom || bbox.top >= ry2) return false; - - Sprite* spr = Collision_getSprite(dataWin, inst); - if (!Collision_obbNeedsSAT(spr, inst)) return true; - - // OBB-vs-AABB SAT for sepMasks==2 with rotation. Native uses SeparatingAxisCollisionBox here. - InstanceOBB obb = Collision_instanceOBB(spr, inst); - - // Project the 4 world-rect corners onto the OBB's local axes; if they don't overlap the local rect on either axis, no collision. - GMLReal corners[4][2] = { {rx1, ry1}, {rx2, ry1}, {rx1, ry2}, {rx2, ry2} }; - GMLReal uMin = 0, uMax = 0, vMin = 0, vMax = 0; - repeat(4, c) { - GMLReal pu, pv; - Collision_obbWorldToLocal(&obb, corners[c][0], corners[c][1], &pu, &pv); - if (c == 0) { uMin = uMax = pu; vMin = vMax = pv; } - else { - if (pu < uMin) uMin = pu; else if (pu > uMax) uMax = pu; - if (pv < vMin) vMin = pv; else if (pv > vMax) vMax = pv; - } - } - if (uMin >= obb.lx1 || obb.lx0 >= uMax) return false; - if (vMin >= obb.ly1 || obb.ly0 >= vMax) return false; - return true; -} - -// Tests whether a world point lies inside the instance's collision rect (margins, rotated/scaled). Cheaper and more correct than Collision_pointInInstance for sepMasks != 1, since point_in_instance bounds-checks against the full sprite texture rather than the bbox margins. -static inline bool Collision_pointInsideInstanceBox(DataWin* dataWin, Instance* inst, GMLReal px, GMLReal py) { - InstanceBBox bbox = Collision_computeBBox(dataWin, inst); - if (!bbox.valid) return false; - if (bbox.left > px || px >= bbox.right || bbox.top > py || py >= bbox.bottom) return false; - - Sprite* spr = Collision_getSprite(dataWin, inst); - if (!Collision_obbNeedsSAT(spr, inst)) return true; - - InstanceOBB obb = Collision_instanceOBB(spr, inst); - GMLReal lx, ly; - Collision_obbWorldToLocal(&obb, px, py, &lx, &ly); - return lx >= obb.lx0 && obb.lx1 > lx && ly >= obb.ly0 && obb.ly1 > ly; -} - -// Circle (cx, cy, radius) vs instance collision rect. Falls back to circle-vs-AABB when the instance isn't a rotated sepMasks==2 sprite. -static inline bool Collision_circleOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal cx, GMLReal cy, GMLReal radius) { - InstanceBBox bbox = Collision_computeBBox(dataWin, inst); - if (!bbox.valid) return false; - GMLReal rSq = radius * radius; - - Sprite* spr = Collision_getSprite(dataWin, inst); - if (!Collision_obbNeedsSAT(spr, inst)) { - // Closest point on AABB to circle center. - GMLReal closestX = cx; - if (bbox.left > closestX) closestX = bbox.left; - if (closestX > bbox.right) closestX = bbox.right; - GMLReal closestY = cy; - if (bbox.top > closestY) closestY = bbox.top; - if (closestY > bbox.bottom) closestY = bbox.bottom; - GMLReal dx = closestX - cx, dy = closestY - cy; - return dx * dx + dy * dy <= rSq; - } - - // Loose AABB pre-pass. - GMLReal qx1 = cx - radius, qy1 = cy - radius, qx2 = cx + radius, qy2 = cy + radius; - if (qx1 >= bbox.right || bbox.left >= qx2 || qy1 >= bbox.bottom || bbox.top >= qy2) return false; - - // Transform circle center into OBB local frame; clamp to local rect; squared distance. - InstanceOBB obb = Collision_instanceOBB(spr, inst); - GMLReal lx, ly; - Collision_obbWorldToLocal(&obb, cx, cy, &lx, &ly); - GMLReal closestX = lx; - if (obb.lx0 > closestX) closestX = obb.lx0; - if (closestX > obb.lx1) closestX = obb.lx1; - GMLReal closestY = ly; - if (obb.ly0 > closestY) closestY = obb.ly0; - if (closestY > obb.ly1) closestY = obb.ly1; - GMLReal dx = closestX - lx, dy = closestY - ly; - return dx * dx + dy * dy <= rSq; -} - -// Liang-Barsky clip of a parametric line p(t) = p1 + t*(p2-p1), t in [0,1], against an axis-aligned rect [rx1,rx2] x [ry1,ry2]. Returns true if the segment intersects the rect. -static inline bool Collision_segmentVsAARect(GMLReal x1, GMLReal y1, GMLReal x2, GMLReal y2, GMLReal rx1, GMLReal ry1, GMLReal rx2, GMLReal ry2) { - GMLReal tEnter = 0.0, tExit = 1.0; - GMLReal dx = x2 - x1, dy = y2 - y1; - GMLReal p[4] = { -dx, dx, -dy, dy }; - GMLReal q[4] = { x1 - rx1, rx2 - x1, y1 - ry1, ry2 - y1 }; - for (int i = 0; 4 > i; i++) { - if (GMLReal_fabs(p[i]) < 1e-9) { - if (q[i] < 0) return false; - continue; - } - GMLReal t = q[i] / p[i]; - if (p[i] < 0) { - if (t > tEnter) tEnter = t; - } else { - if (t < tExit) tExit = t; - } - if (tEnter > tExit) return false; - } - return true; -} - -// Line segment (x1,y1)-(x2,y2) vs instance collision rect. -static inline bool Collision_lineOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal x1, GMLReal y1, GMLReal x2, GMLReal y2) { - InstanceBBox bbox = Collision_computeBBox(dataWin, inst); - if (!bbox.valid) return false; - - Sprite* spr = Collision_getSprite(dataWin, inst); - if (!Collision_obbNeedsSAT(spr, inst)) { - return Collision_segmentVsAARect(x1, y1, x2, y2, bbox.left, bbox.top, bbox.right, bbox.bottom); - } - - // For rotated OBB: transform line endpoints into local frame and clip against local rect. - InstanceOBB obb = Collision_instanceOBB(spr, inst); - GMLReal lx1, ly1, lx2, ly2; - Collision_obbWorldToLocal(&obb, x1, y1, &lx1, &ly1); - Collision_obbWorldToLocal(&obb, x2, y2, &lx2, &ly2); - return Collision_segmentVsAARect(lx1, ly1, lx2, ly2, obb.lx0, obb.ly0, obb.lx1, obb.ly1); -} - -// Tests if world point (px, py) is inside the given instance's collision shape. -// The point is inverse-transformed into sprite-local coords (translation, rotation, inverse scale, origin) and bounds-checked against the full sprite texture [0, spr.width) x [0, spr.height). -// Precise sprites (sepMasks == 1) additionally require the mask bit at the resulting local pixel to be set. -static inline bool Collision_pointInInstance(Sprite* spr, Instance* inst, GMLReal px, GMLReal py) { - if (spr == nullptr) return false; - - // Reject degenerate scales to avoid divide-by-zero. - if (0.0001 > GMLReal_fabs(inst->imageXscale)) return false; - if (0.0001 > GMLReal_fabs(inst->imageYscale)) return false; - - // Transform world coords to sprite-local coords - GMLReal dx = px - inst->x; - GMLReal dy = py - inst->y; - - // Inverse of CW rotation is standard CCW rotation (positive angle) - if (GMLReal_fabs(inst->imageAngle) > 0.0001) { - GMLReal rad = inst->imageAngle * M_PI / 180.0; - GMLReal cs = GMLReal_cos(rad); - GMLReal sn = GMLReal_sin(rad); - GMLReal rx = cs * dx - sn * dy; - GMLReal ry = sn * dx + cs * dy; - dx = rx; - dy = ry; - } - - // Inverse scale + add origin - GMLReal localX = dx / inst->imageXscale + (GMLReal) spr->originX; - GMLReal localY = dy / inst->imageYscale + (GMLReal) spr->originY; - - int32_t ix = (int32_t) localX; - int32_t iy = (int32_t) localY; - - // Bounds check - if (0 > ix || 0 > iy || ix >= (int32_t) spr->width || iy >= (int32_t) spr->height) return false; - - if (Collision_hasFrameMasks(spr)) { - // Pick mask for current frame - uint32_t frameIdx = ((uint32_t) inst->imageIndex) % spr->maskCount; - uint8_t* mask = spr->masks[frameIdx]; - uint32_t bytesPerRow = (spr->width + 7) / 8; - return (mask[iy * bytesPerRow + (ix >> 3)] & (1 << (7 - (ix & 7)))) != 0; - } - - return true; -} - -// Returns true if the two instances' collision shapes overlap. -// -// Matches the native GMS 1.4 runner's flow in FUN_0043fde0: -// 1. AABB overlap test on the two precomputed bboxes. -// 2. If neither sprite is precise (sepMasks == 1), the AABB overlap is enough. -// 3. Otherwise walk the pixel intersection and test BOTH instances on every -// pixel via Collision_pointInInstance. Both sides get inverse-transformed -// regardless of whether they're individually precise, so a rotated -// non-precise sprite collides as an OBB as long as its partner is precise. -static inline bool Collision_instancesOverlapPrecise(DataWin* dataWin, bool compatMode, Instance* a, Instance* b, InstanceBBox bboxA, InstanceBBox bboxB) { - // Compute world-space intersection of the two AABBs - GMLReal iLeft = GMLReal_fmax(bboxA.left, bboxB.left); - GMLReal iRight = GMLReal_fmin(bboxA.right, bboxB.right); - GMLReal iTop = GMLReal_fmax(bboxA.top, bboxB.top); - GMLReal iBottom = GMLReal_fmin(bboxA.bottom, bboxB.bottom); - - // AABB overlap test. Native uses identical semantics in both modern and compat for axis-aligned integer-coordinate cases (compat shifts bbox.right/bottom by -1 *and* the test by +1, which cancel). - if (iLeft >= iRight || iTop >= iBottom) return false; - - Sprite* sprA = Collision_getSprite(dataWin, a); - Sprite* sprB = Collision_getSprite(dataWin, b); - if (sprA == nullptr || sprB == nullptr) return false; - - bool preciseA = Collision_hasFrameMasks(sprA); - bool preciseB = Collision_hasFrameMasks(sprB); - - // Neither sprite precise? Then we need to check if either side is a rotated sepMasks==2 sprite, in which case the loose AABB engulfs space the rotated rect doesn't, and we need OBB-vs-OBB SAT to match native SeparatingAxisCollisionBox. - if (!preciseA && !preciseB) { - bool needSatA = Collision_obbNeedsSAT(sprA, a); - bool needSatB = Collision_obbNeedsSAT(sprB, b); - if (!needSatA && !needSatB) return true; - - InstanceOBB obbA = Collision_instanceOBB(sprA, a); - InstanceOBB obbB = Collision_instanceOBB(sprB, b); - - // Compute the 4 world corners of each OBB. Indices: 0=(lx0,ly0), 1=(lx1,ly0), 2=(lx0,ly1), 3=(lx1,ly1). - GMLReal ax[4], ay[4], bx[4], by[4]; - GMLReal lxA[2] = {obbA.lx0, obbA.lx1}, lyA[2] = {obbA.ly0, obbA.ly1}; - GMLReal lxB[2] = {obbB.lx0, obbB.lx1}, lyB[2] = {obbB.ly0, obbB.ly1}; - repeat(2, i) { - repeat(2, j) { - int k = j * 2 + i; - ax[k] = obbA.x + obbA.cs * lxA[i] + obbA.sn * lyA[j]; - ay[k] = obbA.y - obbA.sn * lxA[i] + obbA.cs * lyA[j]; - bx[k] = obbB.x + obbB.cs * lxB[i] + obbB.sn * lyB[j]; - by[k] = obbB.y - obbB.sn * lxB[i] + obbB.cs * lyB[j]; - } - } - - // SAT: 4 axes (each OBB has 2 unique edge normals). axes[0]/[1] are A's local-x/local-y in world space, axes[2]/[3] are B's. - GMLReal axes[4][2] = { - { obbA.cs, -obbA.sn }, - { obbA.sn, obbA.cs }, - { obbB.cs, -obbB.sn }, - { obbB.sn, obbB.cs } - }; - repeat(4, axIdx) { - GMLReal nx = axes[axIdx][0], ny = axes[axIdx][1]; - GMLReal aMin = ax[0]*nx + ay[0]*ny, aMax = aMin; - GMLReal bMin = bx[0]*nx + by[0]*ny, bMax = bMin; - for (int j = 1; 4 > j; j++) { - GMLReal pa = ax[j]*nx + ay[j]*ny; - GMLReal pb = bx[j]*nx + by[j]*ny; - if (pa < aMin) aMin = pa; else if (pa > aMax) aMax = pa; - if (pb < bMin) bMin = pb; else if (pb > bMax) bMax = pb; - } - if (aMax <= bMin || bMax <= aMin) return false; - } - return true; - } - - // Pixel scan over the AABB intersection. - // Modern: floor..ceil with exclusive upper bound, sample pixel centers (+0.5). - // Compatibility: truncated int range with inclusive upper bound, sample pixel corners (no +0.5). - int32_t startX, endX, startY, endY; - GMLReal sampleOffset; - if (compatMode) { - startX = (int32_t) iLeft; - endX = (int32_t) iRight; - startY = (int32_t) iTop; - endY = (int32_t) iBottom; - sampleOffset = 0.0; - } else { - startX = (int32_t) GMLReal_floor(iLeft); - endX = (int32_t) GMLReal_ceil(iRight); - startY = (int32_t) GMLReal_floor(iTop); - endY = (int32_t) GMLReal_ceil(iBottom); - sampleOffset = 0.5; - } - - for (int32_t py = startY; (compatMode ? py <= endY : py < endY); py++) { - for (int32_t px = startX; (compatMode ? px <= endX : px < endX); px++) { - GMLReal wpx = (GMLReal) px + sampleOffset; - GMLReal wpy = (GMLReal) py + sampleOffset; - - if (!Collision_pointInInstance(sprA, a, wpx, wpy)) continue; - if (!Collision_pointInInstance(sprB, b, wpx, wpy)) continue; - return true; - } - } - - return false; -} +#pragma once + +#include "common.h" +#include "data_win.h" +#include "instance.h" +#include "vm.h" + +#include + +// Checks if an instance matches a collision target. +// target >= 100000: instance ID (match specific instance) +// target == INSTANCE_ALL (-3): match any instance +// target >= 0 && < 100000: object index (match via parent chain) +static inline bool Collision_matchesTarget(DataWin* dataWin, Instance* inst, int32_t target) { + if (target >= 100000) return inst->instanceId == target; + if (target == INSTANCE_ALL) return true; + return VM_isObjectOrDescendant(dataWin, inst->objectIndex, target); +} + +typedef struct { + GMLReal left, right, top, bottom; + bool valid; +} InstanceBBox; + +// Returns the collision sprite for an instance (mask sprite if set, else display sprite) +static inline Sprite* Collision_getSprite(DataWin* dataWin, Instance* inst) { + int32_t sprIdx = (inst->maskIndex >= 0) ? inst->maskIndex : inst->spriteIndex; + if (0 > sprIdx || (uint32_t) sprIdx >= dataWin->sprt.count) return nullptr; + return &dataWin->sprt.sprites[sprIdx]; +} + +// Computes the axis-aligned bounding box for an instance using its collision sprite +static inline InstanceBBox Collision_computeBBox(DataWin* dataWin, Instance* inst) { + Sprite* spr = Collision_getSprite(dataWin, inst); + if (spr == nullptr) return (InstanceBBox){0, 0, 0, 0, false}; + + GMLReal marginL = (GMLReal) spr->marginLeft; + GMLReal marginR = (GMLReal) (spr->marginRight + 1); + GMLReal marginT = (GMLReal) spr->marginTop; + GMLReal marginB = (GMLReal) (spr->marginBottom + 1); + GMLReal originX = (GMLReal) spr->originX; + GMLReal originY = (GMLReal) spr->originY; + + if (GMLReal_fabs(inst->imageAngle) > 0.0001) { + // Compute rotated AABB: transform the 4 corners of the unrotated bbox + GMLReal rad = inst->imageAngle * M_PI / 180.0; + GMLReal cs = GMLReal_cos(rad); + GMLReal sn = GMLReal_sin(rad); + + // Local-space corners relative to origin, scaled + GMLReal lx0 = inst->imageXscale * (marginL - originX); + GMLReal ly0 = inst->imageYscale * (marginT - originY); + GMLReal lx1 = inst->imageXscale * (marginR - originX); + GMLReal ly1 = inst->imageYscale * (marginB - originY); + + // Rotate all 4 corners (CW rotation matching renderer's negated angle for Y-down screen coords) + GMLReal cx[4], cy[4]; + cx[0] = cs * lx0 + sn * ly0; cy[0] = -sn * lx0 + cs * ly0; + cx[1] = cs * lx1 + sn * ly0; cy[1] = -sn * lx1 + cs * ly0; + cx[2] = cs * lx0 + sn * ly1; cy[2] = -sn * lx0 + cs * ly1; + cx[3] = cs * lx1 + sn * ly1; cy[3] = -sn * lx1 + cs * ly1; + + GMLReal minX = cx[0], maxX = cx[0], minY = cy[0], maxY = cy[0]; + for (int c = 1; 4 > c; c++) { + if (minX > cx[c]) minX = cx[c]; + if (cx[c] > maxX) maxX = cx[c]; + if (minY > cy[c]) minY = cy[c]; + if (cy[c] > maxY) maxY = cy[c]; + } + + return (InstanceBBox){ + .left = inst->x + minX, + .right = inst->x + maxX, + .top = inst->y + minY, + .bottom = inst->y + maxY, + .valid = true + }; + } + + // No rotation fast path + GMLReal left = inst->x + inst->imageXscale * (marginL - originX); + GMLReal right = inst->x + inst->imageXscale * (marginR - originX); + GMLReal top = inst->y + inst->imageYscale * (marginT - originY); + GMLReal bottom = inst->y + inst->imageYscale * (marginB - originY); + + // Normalize if negative scale + if (left > right) { GMLReal tmp = left; left = right; right = tmp; } + if (top > bottom) { GMLReal tmp = top; top = bottom; bottom = tmp; } + + return (InstanceBBox){left, right, top, bottom, true}; +} + +static inline bool Collision_hasFrameMasks(Sprite* sprite) { + return sprite != nullptr && sprite->sepMasks == 1 && sprite->masks != nullptr && sprite->maskCount > 0; +} + +// Oriented bounding box for a sprite-bearing instance. +// Local rect is the sprite's collision-margin rectangle, scaled by image_xscale/yscale; (cs, sn) is the rotation that takes local-space points to world-space relative to the instance origin (matches Collision_computeBBox: world = inst.pos + (cs*lx + sn*ly, -sn*lx + cs*ly)). +typedef struct { + GMLReal x, y; // World position (instance origin). + GMLReal lx0, lx1; // Local rect X extents (lx0 <= lx1). + GMLReal ly0, ly1; // Local rect Y extents (ly0 <= ly1). + GMLReal cs, sn; // cos/sin of imageAngle (in radians). + bool rotated; // true if abs(imageAngle) > epsilon. +} InstanceOBB; + +static inline InstanceOBB Collision_instanceOBB(Sprite* spr, Instance* inst) { + InstanceOBB obb; + obb.x = inst->x; + obb.y = inst->y; + GMLReal marginL = (GMLReal) spr->marginLeft; + GMLReal marginR = (GMLReal) (spr->marginRight + 1); + GMLReal marginT = (GMLReal) spr->marginTop; + GMLReal marginB = (GMLReal) (spr->marginBottom + 1); + GMLReal originX = (GMLReal) spr->originX; + GMLReal originY = (GMLReal) spr->originY; + obb.lx0 = inst->imageXscale * (marginL - originX); + obb.lx1 = inst->imageXscale * (marginR - originX); + obb.ly0 = inst->imageYscale * (marginT - originY); + obb.ly1 = inst->imageYscale * (marginB - originY); + if (obb.lx0 > obb.lx1) { GMLReal t = obb.lx0; obb.lx0 = obb.lx1; obb.lx1 = t; } + if (obb.ly0 > obb.ly1) { GMLReal t = obb.ly0; obb.ly0 = obb.ly1; obb.ly1 = t; } + obb.rotated = GMLReal_fabs(inst->imageAngle) > 0.0001; + if (obb.rotated) { + GMLReal rad = inst->imageAngle * M_PI / 180.0; + obb.cs = GMLReal_cos(rad); + obb.sn = GMLReal_sin(rad); + } else { + obb.cs = 1.0; + obb.sn = 0.0; + } + return obb; +} + +// Inverse-transforms a world point into OBB local coordinates. +static inline void Collision_obbWorldToLocal(const InstanceOBB* obb, GMLReal wx, GMLReal wy, GMLReal* outLx, GMLReal* outLy) { + GMLReal dx = wx - obb->x; + GMLReal dy = wy - obb->y; + *outLx = dx * obb->cs - dy * obb->sn; + *outLy = dx * obb->sn + dy * obb->cs; +} + +// Returns true iff the OBB needs SAT-style testing rather than AABB. Only sepMasks == 2 sprites that are actually rotated qualify; everything else (axis-aligned, or precise sprites which fall through to per-pixel scans) is handled correctly by AABB. +static inline bool Collision_obbNeedsSAT(Sprite* spr, Instance* inst) { + return spr != nullptr && spr->sepMasks == 2 && GMLReal_fabs(inst->imageAngle) > 0.0001; +} + +static inline bool Collision_rectOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal rx1, GMLReal ry1, GMLReal rx2, GMLReal ry2) { + InstanceBBox bbox = Collision_computeBBox(dataWin, inst); + if (!bbox.valid) return false; + + if (rx1 >= bbox.right || bbox.left >= rx2 || ry1 >= bbox.bottom || bbox.top >= ry2) return false; + + Sprite* spr = Collision_getSprite(dataWin, inst); + if (!Collision_obbNeedsSAT(spr, inst)) return true; + + // OBB-vs-AABB SAT for sepMasks==2 with rotation. Native uses SeparatingAxisCollisionBox here. + InstanceOBB obb = Collision_instanceOBB(spr, inst); + + // Project the 4 world-rect corners onto the OBB's local axes; if they don't overlap the local rect on either axis, no collision. + GMLReal corners[4][2] = { {rx1, ry1}, {rx2, ry1}, {rx1, ry2}, {rx2, ry2} }; + GMLReal uMin = 0, uMax = 0, vMin = 0, vMax = 0; + repeat(4, c) { + GMLReal pu, pv; + Collision_obbWorldToLocal(&obb, corners[c][0], corners[c][1], &pu, &pv); + if (c == 0) { uMin = uMax = pu; vMin = vMax = pv; } + else { + if (pu < uMin) uMin = pu; else if (pu > uMax) uMax = pu; + if (pv < vMin) vMin = pv; else if (pv > vMax) vMax = pv; + } + } + if (uMin >= obb.lx1 || obb.lx0 >= uMax) return false; + if (vMin >= obb.ly1 || obb.ly0 >= vMax) return false; + return true; +} + +// Tests whether a world point lies inside the instance's collision rect (margins, rotated/scaled). Cheaper and more correct than Collision_pointInInstance for sepMasks != 1, since point_in_instance bounds-checks against the full sprite texture rather than the bbox margins. +static inline bool Collision_pointInsideInstanceBox(DataWin* dataWin, Instance* inst, GMLReal px, GMLReal py) { + InstanceBBox bbox = Collision_computeBBox(dataWin, inst); + if (!bbox.valid) return false; + if (bbox.left > px || px >= bbox.right || bbox.top > py || py >= bbox.bottom) return false; + + Sprite* spr = Collision_getSprite(dataWin, inst); + if (!Collision_obbNeedsSAT(spr, inst)) return true; + + InstanceOBB obb = Collision_instanceOBB(spr, inst); + GMLReal lx, ly; + Collision_obbWorldToLocal(&obb, px, py, &lx, &ly); + return lx >= obb.lx0 && obb.lx1 > lx && ly >= obb.ly0 && obb.ly1 > ly; +} + +// Circle (cx, cy, radius) vs instance collision rect. Falls back to circle-vs-AABB when the instance isn't a rotated sepMasks==2 sprite. +static inline bool Collision_circleOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal cx, GMLReal cy, GMLReal radius) { + InstanceBBox bbox = Collision_computeBBox(dataWin, inst); + if (!bbox.valid) return false; + GMLReal rSq = radius * radius; + + Sprite* spr = Collision_getSprite(dataWin, inst); + if (!Collision_obbNeedsSAT(spr, inst)) { + // Closest point on AABB to circle center. + GMLReal closestX = cx; + if (bbox.left > closestX) closestX = bbox.left; + if (closestX > bbox.right) closestX = bbox.right; + GMLReal closestY = cy; + if (bbox.top > closestY) closestY = bbox.top; + if (closestY > bbox.bottom) closestY = bbox.bottom; + GMLReal dx = closestX - cx, dy = closestY - cy; + return dx * dx + dy * dy <= rSq; + } + + // Loose AABB pre-pass. + GMLReal qx1 = cx - radius, qy1 = cy - radius, qx2 = cx + radius, qy2 = cy + radius; + if (qx1 >= bbox.right || bbox.left >= qx2 || qy1 >= bbox.bottom || bbox.top >= qy2) return false; + + // Transform circle center into OBB local frame; clamp to local rect; squared distance. + InstanceOBB obb = Collision_instanceOBB(spr, inst); + GMLReal lx, ly; + Collision_obbWorldToLocal(&obb, cx, cy, &lx, &ly); + GMLReal closestX = lx; + if (obb.lx0 > closestX) closestX = obb.lx0; + if (closestX > obb.lx1) closestX = obb.lx1; + GMLReal closestY = ly; + if (obb.ly0 > closestY) closestY = obb.ly0; + if (closestY > obb.ly1) closestY = obb.ly1; + GMLReal dx = closestX - lx, dy = closestY - ly; + return dx * dx + dy * dy <= rSq; +} + +// Liang-Barsky clip of a parametric line p(t) = p1 + t*(p2-p1), t in [0,1], against an axis-aligned rect [rx1,rx2] x [ry1,ry2]. Returns true if the segment intersects the rect. +static inline bool Collision_segmentVsAARect(GMLReal x1, GMLReal y1, GMLReal x2, GMLReal y2, GMLReal rx1, GMLReal ry1, GMLReal rx2, GMLReal ry2) { + GMLReal tEnter = 0.0, tExit = 1.0; + GMLReal dx = x2 - x1, dy = y2 - y1; + GMLReal p[4] = { -dx, dx, -dy, dy }; + GMLReal q[4] = { x1 - rx1, rx2 - x1, y1 - ry1, ry2 - y1 }; + for (int i = 0; 4 > i; i++) { + if (GMLReal_fabs(p[i]) < 1e-9) { + if (q[i] < 0) return false; + continue; + } + GMLReal t = q[i] / p[i]; + if (p[i] < 0) { + if (t > tEnter) tEnter = t; + } else { + if (t < tExit) tExit = t; + } + if (tEnter > tExit) return false; + } + return true; +} + +// Line segment (x1,y1)-(x2,y2) vs instance collision rect. +static inline bool Collision_lineOverlapsInstance(DataWin* dataWin, Instance* inst, GMLReal x1, GMLReal y1, GMLReal x2, GMLReal y2) { + InstanceBBox bbox = Collision_computeBBox(dataWin, inst); + if (!bbox.valid) return false; + + Sprite* spr = Collision_getSprite(dataWin, inst); + if (!Collision_obbNeedsSAT(spr, inst)) { + return Collision_segmentVsAARect(x1, y1, x2, y2, bbox.left, bbox.top, bbox.right, bbox.bottom); + } + + // For rotated OBB: transform line endpoints into local frame and clip against local rect. + InstanceOBB obb = Collision_instanceOBB(spr, inst); + GMLReal lx1, ly1, lx2, ly2; + Collision_obbWorldToLocal(&obb, x1, y1, &lx1, &ly1); + Collision_obbWorldToLocal(&obb, x2, y2, &lx2, &ly2); + return Collision_segmentVsAARect(lx1, ly1, lx2, ly2, obb.lx0, obb.ly0, obb.lx1, obb.ly1); +} + +// Tests if world point (px, py) is inside the given instance's collision shape. +// The point is inverse-transformed into sprite-local coords (translation, rotation, inverse scale, origin) and bounds-checked against the full sprite texture [0, spr.width) x [0, spr.height). +// Precise sprites (sepMasks == 1) additionally require the mask bit at the resulting local pixel to be set. +static inline bool Collision_pointInInstance(Sprite* spr, Instance* inst, GMLReal px, GMLReal py) { + if (spr == nullptr) return false; + + // Reject degenerate scales to avoid divide-by-zero. + if (0.0001 > GMLReal_fabs(inst->imageXscale)) return false; + if (0.0001 > GMLReal_fabs(inst->imageYscale)) return false; + + // Transform world coords to sprite-local coords + GMLReal dx = px - inst->x; + GMLReal dy = py - inst->y; + + // Inverse of CW rotation is standard CCW rotation (positive angle) + if (GMLReal_fabs(inst->imageAngle) > 0.0001) { + GMLReal rad = inst->imageAngle * M_PI / 180.0; + GMLReal cs = GMLReal_cos(rad); + GMLReal sn = GMLReal_sin(rad); + GMLReal rx = cs * dx - sn * dy; + GMLReal ry = sn * dx + cs * dy; + dx = rx; + dy = ry; + } + + // Inverse scale + add origin + GMLReal localX = dx / inst->imageXscale + (GMLReal) spr->originX; + GMLReal localY = dy / inst->imageYscale + (GMLReal) spr->originY; + + int32_t ix = (int32_t) localX; + int32_t iy = (int32_t) localY; + + // Bounds check + if (0 > ix || 0 > iy || ix >= (int32_t) spr->width || iy >= (int32_t) spr->height) return false; + + if (Collision_hasFrameMasks(spr)) { + // Pick mask for current frame + uint32_t frameIdx = ((uint32_t) inst->imageIndex) % spr->maskCount; + uint8_t* mask = spr->masks[frameIdx]; + uint32_t bytesPerRow = (spr->width + 7) / 8; + return (mask[iy * bytesPerRow + (ix >> 3)] & (1 << (7 - (ix & 7)))) != 0; + } + + return true; +} + +// Returns true if the two instances' collision shapes overlap. +// +// Matches the native GMS 1.4 runner's flow in FUN_0043fde0: +// 1. AABB overlap test on the two precomputed bboxes. +// 2. If neither sprite is precise (sepMasks == 1), the AABB overlap is enough. +// 3. Otherwise walk the pixel intersection and test BOTH instances on every +// pixel via Collision_pointInInstance. Both sides get inverse-transformed +// regardless of whether they're individually precise, so a rotated +// non-precise sprite collides as an OBB as long as its partner is precise. +static inline bool Collision_instancesOverlapPrecise(DataWin* dataWin, bool compatMode, Instance* a, Instance* b, InstanceBBox bboxA, InstanceBBox bboxB) { + // Compute world-space intersection of the two AABBs + GMLReal iLeft = GMLReal_fmax(bboxA.left, bboxB.left); + GMLReal iRight = GMLReal_fmin(bboxA.right, bboxB.right); + GMLReal iTop = GMLReal_fmax(bboxA.top, bboxB.top); + GMLReal iBottom = GMLReal_fmin(bboxA.bottom, bboxB.bottom); + + // AABB overlap test. Native uses identical semantics in both modern and compat for axis-aligned integer-coordinate cases (compat shifts bbox.right/bottom by -1 *and* the test by +1, which cancel). + if (iLeft >= iRight || iTop >= iBottom) return false; + + Sprite* sprA = Collision_getSprite(dataWin, a); + Sprite* sprB = Collision_getSprite(dataWin, b); + if (sprA == nullptr || sprB == nullptr) return false; + + bool preciseA = Collision_hasFrameMasks(sprA); + bool preciseB = Collision_hasFrameMasks(sprB); + + // Neither sprite precise? Then we need to check if either side is a rotated sepMasks==2 sprite, in which case the loose AABB engulfs space the rotated rect doesn't, and we need OBB-vs-OBB SAT to match native SeparatingAxisCollisionBox. + if (!preciseA && !preciseB) { + bool needSatA = Collision_obbNeedsSAT(sprA, a); + bool needSatB = Collision_obbNeedsSAT(sprB, b); + if (!needSatA && !needSatB) return true; + + InstanceOBB obbA = Collision_instanceOBB(sprA, a); + InstanceOBB obbB = Collision_instanceOBB(sprB, b); + + // Compute the 4 world corners of each OBB. Indices: 0=(lx0,ly0), 1=(lx1,ly0), 2=(lx0,ly1), 3=(lx1,ly1). + GMLReal ax[4], ay[4], bx[4], by[4]; + GMLReal lxA[2] = {obbA.lx0, obbA.lx1}, lyA[2] = {obbA.ly0, obbA.ly1}; + GMLReal lxB[2] = {obbB.lx0, obbB.lx1}, lyB[2] = {obbB.ly0, obbB.ly1}; + repeat(2, i) { + repeat(2, j) { + int k = j * 2 + i; + ax[k] = obbA.x + obbA.cs * lxA[i] + obbA.sn * lyA[j]; + ay[k] = obbA.y - obbA.sn * lxA[i] + obbA.cs * lyA[j]; + bx[k] = obbB.x + obbB.cs * lxB[i] + obbB.sn * lyB[j]; + by[k] = obbB.y - obbB.sn * lxB[i] + obbB.cs * lyB[j]; + } + } + + // SAT: 4 axes (each OBB has 2 unique edge normals). axes[0]/[1] are A's local-x/local-y in world space, axes[2]/[3] are B's. + GMLReal axes[4][2] = { + { obbA.cs, -obbA.sn }, + { obbA.sn, obbA.cs }, + { obbB.cs, -obbB.sn }, + { obbB.sn, obbB.cs } + }; + repeat(4, axIdx) { + GMLReal nx = axes[axIdx][0], ny = axes[axIdx][1]; + GMLReal aMin = ax[0]*nx + ay[0]*ny, aMax = aMin; + GMLReal bMin = bx[0]*nx + by[0]*ny, bMax = bMin; + for (int j = 1; 4 > j; j++) { + GMLReal pa = ax[j]*nx + ay[j]*ny; + GMLReal pb = bx[j]*nx + by[j]*ny; + if (pa < aMin) aMin = pa; else if (pa > aMax) aMax = pa; + if (pb < bMin) bMin = pb; else if (pb > bMax) bMax = pb; + } + if (aMax <= bMin || bMax <= aMin) return false; + } + return true; + } + + // Pixel scan over the AABB intersection. + // Modern: floor..ceil with exclusive upper bound, sample pixel centers (+0.5). + // Compatibility: truncated int range with inclusive upper bound, sample pixel corners (no +0.5). + int32_t startX, endX, startY, endY; + GMLReal sampleOffset; + if (compatMode) { + startX = (int32_t) iLeft; + endX = (int32_t) iRight; + startY = (int32_t) iTop; + endY = (int32_t) iBottom; + sampleOffset = 0.0; + } else { + startX = (int32_t) GMLReal_floor(iLeft); + endX = (int32_t) GMLReal_ceil(iRight); + startY = (int32_t) GMLReal_floor(iTop); + endY = (int32_t) GMLReal_ceil(iBottom); + sampleOffset = 0.5; + } + + for (int32_t py = startY; (compatMode ? py <= endY : py < endY); py++) { + for (int32_t px = startX; (compatMode ? px <= endX : px < endX); px++) { + GMLReal wpx = (GMLReal) px + sampleOffset; + GMLReal wpy = (GMLReal) py + sampleOffset; + + if (!Collision_pointInInstance(sprA, a, wpx, wpy)) continue; + if (!Collision_pointInInstance(sprB, b, wpx, wpy)) continue; + return true; + } + } + + return false; +} diff --git a/src/data_win.c b/src/data_win.c index ba0e5046..54fe7af6 100644 --- a/src/data_win.c +++ b/src/data_win.c @@ -1,2381 +1,2425 @@ -#include "data_win.h" -#include "binary_reader.h" - -#include -#include -#include -#include -#include - -#include "stb_ds.h" -#include "utils.h" - -// ===[ HELPERS ]=== - -// Reads a uint32 absolute file offset, resolves it into the pre-loaded STRG buffer, -// and returns a pointer to the null-terminated string content at that offset. -static const char* readStringPtr(BinaryReader* reader, DataWin* dw) { - uint32_t offset = BinaryReader_readUint32(reader); - if (offset == 0) return nullptr; - return (const char*) (dw->strgBuffer + (offset - dw->strgBufferBase)); -} - -// Reads a pointer list header: count + absolute-offset pointers. -// Caller must free the returned array. -static uint32_t* readPointerTable(BinaryReader* reader, uint32_t* outCount) { - *outCount = BinaryReader_readUint32(reader); - if (*outCount == 0) return nullptr; - uint32_t* ptrs = safeMalloc(*outCount * sizeof(uint32_t)); - repeat(*outCount, i) { - ptrs[i] = BinaryReader_readUint32(reader); - } - return ptrs; -} - -// Reads a PointerList of EventAction entries. Used by TMLN and OBJT. -static EventAction* readEventActions(BinaryReader* reader, DataWin* dw, uint32_t* outCount) { - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - *outCount = count; - if (count == 0) { free(ptrs); return nullptr; } - - EventAction* actions = safeMalloc(count * sizeof(EventAction)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - actions[i].libID = BinaryReader_readUint32(reader); - actions[i].id = BinaryReader_readUint32(reader); - actions[i].kind = BinaryReader_readUint32(reader); - actions[i].useRelative = BinaryReader_readBool32(reader); - actions[i].isQuestion = BinaryReader_readBool32(reader); - actions[i].useApplyTo = BinaryReader_readBool32(reader); - actions[i].exeType = BinaryReader_readUint32(reader); - actions[i].actionName = readStringPtr(reader, dw); - actions[i].codeId = BinaryReader_readInt32(reader); - actions[i].argumentCount = BinaryReader_readUint32(reader); - actions[i].who = BinaryReader_readInt32(reader); - actions[i].relative = BinaryReader_readBool32(reader); - actions[i].isNot = BinaryReader_readBool32(reader); - actions[i].unknownAlwaysZero = BinaryReader_readUint32(reader); - } - free(ptrs); - return actions; -} - -// ===[ PATH INTERNAL COMPUTATION ]=== -// Matches HTML5 yyPath.js algorithm exactly. - -// Dynamic array of InternalPathPoints for building during computation -static InternalPathPoint* tempIntPoints = nullptr; -static uint32_t tempIntPointCount = 0; - -static void addInternalPoint(float x, float y, float speed) { - InternalPathPoint pt = { .x = x, .y = y, .speed = speed, .l = 0.0 }; - arrput(tempIntPoints, pt); - tempIntPointCount++; -} - -// Recursive midpoint subdivision for smooth curves (yyPath.js:225-242) -static void handlePiece(int depth, float x1, float y1, float s1, float x2, float y2, float s2, float x3, float y3, float s3) { - if (depth == 0) return; - - float mx = (x1 + x2 + x2 + x3) / 4.0f; - float my = (y1 + y2 + y2 + y3) / 4.0f; - float ms = (s1 + s2 + s2 + s3) / 4.0f; - - if ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) > 16.0f) { - handlePiece(depth - 1, x1, y1, s1, (x2 + x1) / 2.0f, (y2 + y1) / 2.0f, (s2 + s1) / 2.0f, mx, my, ms); - } - - addInternalPoint(mx, my, ms); - - if ((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3) > 16.0f) { - handlePiece(depth - 1, mx, my, ms, (x3 + x2) / 2.0f, (y3 + y2) / 2.0f, (s3 + s2) / 2.0f, x3, y3, s3); - } -} - -void GamePath_computeInternal(GamePath* path) { - // Reset temp state - arrfree(tempIntPoints); - tempIntPoints = nullptr; - tempIntPointCount = 0; - - free(path->internalPoints); - path->internalPoints = nullptr; - path->internalPointCount = 0; - path->length = 0.0; - - if (path->pointCount == 0) - return; - - if (path->isSmooth) { - // ComputeCurved (yyPath.js:254-292) - if (!path->isClosed) { - addInternalPoint(path->points[0].x, path->points[0].y, path->points[0].speed); - } - - int n; - if (path->isClosed) { - n = (int) path->pointCount - 1; - } else { - n = (int) path->pointCount - 3; - } - - repeat(n + 1, i) { - PathPoint* p1 = &path->points[i % path->pointCount]; - PathPoint* p2 = &path->points[(i + 1) % path->pointCount]; - PathPoint* p3 = &path->points[(i + 2) % path->pointCount]; - handlePiece((int) path->precision, - (p1->x + p2->x) / 2.0f, (p1->y + p2->y) / 2.0f, (p1->speed + p2->speed) / 2.0f, - p2->x, p2->y, p2->speed, - (p2->x + p3->x) / 2.0f, (p2->y + p3->y) / 2.0f, (p2->speed + p3->speed) / 2.0f); - } - - if (!path->isClosed) { - PathPoint* last = &path->points[path->pointCount - 1]; - addInternalPoint(last->x, last->y, last->speed); - } else { - // Closed smooth: append the first internal point again - addInternalPoint(tempIntPoints[0].x, tempIntPoints[0].y, tempIntPoints[0].speed); - } - } else { - // ComputeLinear (yyPath.js:192-204) - repeat(path->pointCount, i) { - addInternalPoint(path->points[i].x, path->points[i].y, path->points[i].speed); - } - if (path->isClosed) { - addInternalPoint(path->points[0].x, path->points[0].y, path->points[0].speed); - } - } - - // ComputeLength (yyPath.js:150-160) - path->internalPointCount = tempIntPointCount; - path->internalPoints = safeMalloc(tempIntPointCount * sizeof(InternalPathPoint)); - memcpy(path->internalPoints, tempIntPoints, tempIntPointCount * sizeof(InternalPathPoint)); - arrfree(tempIntPoints); - tempIntPoints = nullptr; - tempIntPointCount = 0; - - path->length = 0.0; - if (path->internalPointCount > 0) { - path->internalPoints[0].l = 0.0; - repeat(path->internalPointCount - 1, j) { - uint32_t i = j + 1; - float dx = path->internalPoints[i].x - path->internalPoints[i - 1].x; - float dy = path->internalPoints[i].y - path->internalPoints[i - 1].y; - path->length += sqrtf(dx * dx + dy * dy); - path->internalPoints[i].l = path->length; - } - } -} - -// Get interpolated position at t in [0,1] (yyPath.js:362-409) -PathPositionResult GamePath_getPosition(GamePath* path, float t) { - PathPositionResult result = { .x = 0.0f, .y = 0.0f, .speed = 0.0f }; - - if (path->internalPointCount == 0) return result; - - if (path->internalPointCount == 1 || path->length == 0.0f || 0.0f >= t) { - result.x = path->internalPoints[0].x; - result.y = path->internalPoints[0].y; - result.speed = path->internalPoints[0].speed; - return result; - } - - if (t >= 1.0f) { - InternalPathPoint* last = &path->internalPoints[path->internalPointCount - 1]; - result.x = last->x; - result.y = last->y; - result.speed = last->speed; - return result; - } - - // Get the right interval via linear scan - float l = path->length * t; - uint32_t pos = 0; - while (path->internalPointCount - 2 > pos && l >= path->internalPoints[pos + 1].l) { - pos++; - } - - InternalPathPoint* node = &path->internalPoints[pos]; - float lRem = l - node->l; - float w = path->internalPoints[pos + 1].l - node->l; - - if (w != 0.0f) { - InternalPathPoint* next = &path->internalPoints[pos + 1]; - result.x = node->x + lRem * (next->x - node->x) / w; - result.y = node->y + lRem * (next->y - node->y) / w; - result.speed = node->speed + lRem * (next->speed - node->speed) / w; - } else { - result.x = node->x; - result.y = node->y; - result.speed = node->speed; - } - - return result; -} - -// ===[ CHUNK PARSERS ]=== - -static void parseGEN8(BinaryReader* reader, DataWin* dw) { - Gen8* g = &dw->gen8; - g->isDebuggerDisabled = BinaryReader_readUint8(reader); - g->bytecodeVersion = BinaryReader_readUint8(reader); - BinaryReader_skip(reader, 2); // padding - g->fileName = readStringPtr(reader, dw); - g->config = readStringPtr(reader, dw); - g->lastObj = BinaryReader_readUint32(reader); - g->lastTile = BinaryReader_readUint32(reader); - g->gameID = BinaryReader_readUint32(reader); - BinaryReader_readBytes(reader, g->directPlayGuid, 16); - g->name = readStringPtr(reader, dw); - g->major = BinaryReader_readUint32(reader); - g->minor = BinaryReader_readUint32(reader); - g->release = BinaryReader_readUint32(reader); - g->build = BinaryReader_readUint32(reader); - g->defaultWindowWidth = BinaryReader_readUint32(reader); - g->defaultWindowHeight = BinaryReader_readUint32(reader); - g->info = BinaryReader_readUint32(reader); - g->licenseCRC32 = BinaryReader_readUint32(reader); - BinaryReader_readBytes(reader, g->licenseMD5, 16); - g->timestamp = BinaryReader_readUint64(reader); - g->displayName = readStringPtr(reader, dw); - g->activeTargets = BinaryReader_readUint64(reader); - g->functionClassifications = BinaryReader_readUint64(reader); - g->steamAppID = BinaryReader_readInt32(reader); - if (g->bytecodeVersion >= 14) { - g->debuggerPort = BinaryReader_readUint32(reader); - } - - // Room order SimpleList - g->roomOrderCount = BinaryReader_readUint32(reader); - if (g->roomOrderCount > 0) { - g->roomOrder = safeMalloc(g->roomOrderCount * sizeof(int32_t)); - repeat(g->roomOrderCount, i) { - g->roomOrder[i] = BinaryReader_readInt32(reader); - } - } else { - g->roomOrder = nullptr; - } - - if (g->major >= 2) { - BinaryReader_skip(reader, 8); // firstRandom (int64) - BinaryReader_skip(reader, 8*4); // 4 Random Entries (one int64 or two int32) - - g->gms2FPS = BinaryReader_readFloat32(reader); - BinaryReader_skip(reader, 4); // AllowStatistics (bool32) - BinaryReader_skip(reader, 16); // GameGUID (16 Bytes, unknown it's use) - } - - // Seed the detected version from GEN8. - // Later chunk parsers may bump these upward when they identify newer-format features, because since GM:S 2 the value in the GEN8 chunk is not accurate. - DataWin_bumpVersionTo(dw, g->major, g->minor, g->release, g->build); -} - -static void parseOPTN(BinaryReader* reader, DataWin* dw) { - Optn* o = &dw->optn; - - int32_t marker = BinaryReader_readInt32(reader); - if (marker != (int32_t)0x80000000) { - fprintf(stderr, "OPTN: expected new format marker 0x80000000, got 0x%08X\n", (uint32_t)marker); - exit(1); - } - - int32_t shaderExtVersion = BinaryReader_readInt32(reader); - (void)shaderExtVersion; // always 2 - - o->info = BinaryReader_readUint64(reader); - o->scale = BinaryReader_readInt32(reader); - o->windowColor = BinaryReader_readUint32(reader); - o->colorDepth = BinaryReader_readUint32(reader); - o->resolution = BinaryReader_readUint32(reader); - o->frequency = BinaryReader_readUint32(reader); - o->vertexSync = BinaryReader_readUint32(reader); - o->priority = BinaryReader_readUint32(reader); - o->backImage = BinaryReader_readUint32(reader); - o->frontImage = BinaryReader_readUint32(reader); - o->loadImage = BinaryReader_readUint32(reader); - o->loadAlpha = BinaryReader_readUint32(reader); - - // Constants SimpleList - o->constantCount = BinaryReader_readUint32(reader); - if (o->constantCount > 0) { - o->constants = safeMalloc(o->constantCount * sizeof(OptnConstant)); - repeat(o->constantCount, i) { - o->constants[i].name = readStringPtr(reader, dw); - o->constants[i].value = readStringPtr(reader, dw); - } - } else { - o->constants = nullptr; - } -} - -static void parseLANG(BinaryReader* reader, DataWin* dw) { - Lang* l = &dw->lang; - l->unknown1 = BinaryReader_readUint32(reader); - l->languageCount = BinaryReader_readUint32(reader); - l->entryCount = BinaryReader_readUint32(reader); - - // Entry IDs - if (l->entryCount > 0) { - l->entryIds = safeMalloc(l->entryCount * sizeof(const char*)); - repeat(l->entryCount, i) { - l->entryIds[i] = readStringPtr(reader, dw); - } - } else { - l->entryIds = nullptr; - } - - // Languages - if (l->languageCount > 0) { - l->languages = safeMalloc(l->languageCount * sizeof(Language)); - repeat(l->languageCount, i) { - l->languages[i].name = readStringPtr(reader, dw); - l->languages[i].region = readStringPtr(reader, dw); - l->languages[i].entryCount = l->entryCount; - if (l->entryCount > 0) { - l->languages[i].entries = safeMalloc(l->entryCount * sizeof(const char*)); - repeat(l->entryCount, j) { - l->languages[i].entries[j] = readStringPtr(reader, dw); - } - } else { - l->languages[i].entries = nullptr; - } - } - } else { - l->languages = nullptr; - } -} - -static void parseEXTN(BinaryReader* reader, DataWin* dw) { - // TODO: Update EXTN parser because it is broken for newer GM:S 2 versions - Extn* e = &dw->extn; - - uint32_t extCount; - uint32_t* extPtrs = readPointerTable(reader, &extCount); - e->count = extCount; - - if (extCount == 0) { free(extPtrs); e->extensions = nullptr; return; } - - e->extensions = safeMalloc(extCount * sizeof(Extension)); - repeat(extCount, i) { - BinaryReader_seek(reader, extPtrs[i]); - Extension* ext = &e->extensions[i]; - ext->folderName = readStringPtr(reader, dw); - ext->name = readStringPtr(reader, dw); - ext->className = readStringPtr(reader, dw); - - // Files PointerList - uint32_t fileCount; - uint32_t* filePtrs = readPointerTable(reader, &fileCount); - ext->fileCount = fileCount; - - if (fileCount > 0) { - ext->files = safeMalloc(fileCount * sizeof(ExtensionFile)); - repeat(fileCount, j) { - BinaryReader_seek(reader, filePtrs[j]); - ExtensionFile* file = &ext->files[j]; - file->filename = readStringPtr(reader, dw); - file->cleanupScript = readStringPtr(reader, dw); - file->initScript = readStringPtr(reader, dw); - file->kind = BinaryReader_readUint32(reader); - - // Functions PointerList - uint32_t funcCount; - uint32_t* funcPtrs = readPointerTable(reader, &funcCount); - file->functionCount = funcCount; - - if (funcCount > 0) { - file->functions = safeMalloc(funcCount * sizeof(ExtensionFunction)); - repeat(funcCount, k) { - BinaryReader_seek(reader, funcPtrs[k]); - ExtensionFunction* func = &file->functions[k]; - func->name = readStringPtr(reader, dw); - func->id = BinaryReader_readUint32(reader); - func->kind = BinaryReader_readUint32(reader); - func->retType = BinaryReader_readUint32(reader); - func->extName = readStringPtr(reader, dw); - - // Arguments SimpleList - func->argumentCount = BinaryReader_readUint32(reader); - if (func->argumentCount > 0) { - func->arguments = safeMalloc(func->argumentCount * sizeof(uint32_t)); - repeat(func->argumentCount, a) { - func->arguments[a] = BinaryReader_readUint32(reader); - } - } else { - func->arguments = nullptr; - } - } - } else { - file->functions = nullptr; - } - free(funcPtrs); - } - } else { - ext->files = nullptr; - } - free(filePtrs); - } - free(extPtrs); - - // Product ID data (16 bytes per extension, bytecodeVersion >= 14) - // Skipped -- we seek to chunkEnd after parsing -} - -static void parseSOND(BinaryReader* reader, DataWin* dw) { - Sond* s = &dw->sond; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - s->count = count; - - if (count == 0) { free(ptrs); s->sounds = nullptr; return; } - - s->sounds = safeMalloc(count * sizeof(Sound)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - Sound* snd = &s->sounds[i]; - snd->name = readStringPtr(reader, dw); - snd->flags = BinaryReader_readUint32(reader); - snd->type = readStringPtr(reader, dw); - snd->file = readStringPtr(reader, dw); - snd->effects = BinaryReader_readUint32(reader); - snd->volume = BinaryReader_readFloat32(reader); - snd->pitch = BinaryReader_readFloat32(reader); - - // AudioGroup or preload field at offset +28 - // For GMS 1.4.x (bytecodeVersion >= 14) with Regular flag: resource_id - if ((snd->flags & 0x64) == 0x64) { - snd->audioGroup = BinaryReader_readInt32(reader); - } else { - int32_t preload = BinaryReader_readInt32(reader); - (void)preload; - snd->audioGroup = 0; // default audio group - } - - snd->audioFile = BinaryReader_readInt32(reader); - } - free(ptrs); -} - -static void parseAGRP(BinaryReader* reader, DataWin* dw) { - Agrp* a = &dw->agrp; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - a->count = count; - - if (count == 0) { free(ptrs); a->audioGroups = nullptr; return; } - - a->audioGroups = safeMalloc(count * sizeof(AudioGroup)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - a->audioGroups[i].name = readStringPtr(reader, dw); - } - free(ptrs); -} - -static void parseSPRT(BinaryReader* reader, DataWin* dw, bool skipLoadingPreciseMasksForNonPreciseSprites) { - Sprt* s = &dw->sprt; - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - s->count = count; - s->parsedCount = count; - - if (count == 0) { free(ptrs); s->sprites = nullptr; return; } - - s->sprites = safeCalloc(count, sizeof(Sprite)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - Sprite* spr = &s->sprites[i]; - spr->name = readStringPtr(reader, dw); - spr->width = BinaryReader_readUint32(reader); - spr->height = BinaryReader_readUint32(reader); - spr->marginLeft = BinaryReader_readInt32(reader); - spr->marginRight = BinaryReader_readInt32(reader); - spr->marginBottom = BinaryReader_readInt32(reader); - spr->marginTop = BinaryReader_readInt32(reader); - spr->transparent = BinaryReader_readBool32(reader); - spr->smooth = BinaryReader_readBool32(reader); - spr->preload = BinaryReader_readBool32(reader); - spr->bboxMode = BinaryReader_readUint32(reader); - spr->sepMasks = BinaryReader_readUint32(reader); - spr->originX = BinaryReader_readInt32(reader); - spr->originY = BinaryReader_readInt32(reader); - - // Detect special type vs normal: peek next int32 - int32_t check = BinaryReader_readInt32(reader); - uint32_t nineSliceOffset = 0; - if (check == -1) { - spr->specialType = true; - spr->sVersion = BinaryReader_readUint32(reader); - spr->sSpriteType = BinaryReader_readUint32(reader); - if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) { - spr->gms2PlaybackSpeed = BinaryReader_readFloat32(reader); - spr->gms2PlaybackSpeedType = BinaryReader_readUint32(reader); - if (spr->sVersion >= 2) { - BinaryReader_skip(reader, 4); //sequenceOffset; - if (spr->sVersion >= 3) { - nineSliceOffset = BinaryReader_readUint32(reader); - } - } - check = BinaryReader_readUint32(reader); - } - } - - // 'check' is the texture count (start of SimpleList) - spr->textureCount = (uint32_t)check; - if (spr->textureCount > 0) { - // Temporarily store the absolute file offsets here; parseTPAG resolves them in-place to TPAG indices once the TPAG table is known. - spr->tpagIndices = safeMalloc(spr->textureCount * sizeof(int32_t)); - repeat(spr->textureCount, j) { - spr->tpagIndices[j] = (int32_t) BinaryReader_readUint32(reader); - } - } else { - spr->tpagIndices = nullptr; - } - - // Collision mask data - // sepMasks: 0 = axis-aligned rect (no mask data stored in some cases) - // 1 = precise per-frame masks - // 2 = rotated rect (no mask data) - // Mask format: each bit = 1 pixel, MSB first, row-major - // Width in bytes = (spriteWidth + 7) / 8, total = widthInBytes * spriteHeight - // After all masks, data is padded to 4-byte alignment - uint32_t maskDataCount = BinaryReader_readUint32(reader); - spr->maskCount = maskDataCount; - if (maskDataCount > 0 && spr->width > 0 && spr->height > 0) { - uint32_t bytesPerRow = (spr->width + 7) / 8; - uint32_t bytesPerMask = bytesPerRow * spr->height; - - if (spr->sepMasks == 1 || !skipLoadingPreciseMasksForNonPreciseSprites) { - spr->masks = safeMalloc(maskDataCount * sizeof(uint8_t*)); - repeat(maskDataCount, j) { - spr->masks[j] = safeMalloc(bytesPerMask); - BinaryReader_readBytes(reader, spr->masks[j], bytesPerMask); - } - } else { - BinaryReader_skip(reader, bytesPerMask * maskDataCount); - spr->masks = nullptr; - } - // Pad the TOTAL mask data to 4-byte alignment (not per-mask) - uint32_t totalMaskBytes = bytesPerMask * maskDataCount; - uint32_t remainder = totalMaskBytes % 4; - if (remainder != 0) { - BinaryReader_skip(reader, 4 - remainder); - } - } else { - spr->masks = nullptr; - } - - // Nine-slice block (40 bytes). Located at nineSliceOffset (absolute file offset) elsewhere in the chunk. - if (nineSliceOffset != 0) { - size_t savedPos = BinaryReader_getPosition(reader); - BinaryReader_seek(reader, (size_t) nineSliceOffset); - spr->nsLeft = BinaryReader_readInt32(reader); - spr->nsTop = BinaryReader_readInt32(reader); - spr->nsRight = BinaryReader_readInt32(reader); - spr->nsBottom = BinaryReader_readInt32(reader); - spr->nineSliceEnabled = BinaryReader_readBool32(reader); - repeat(5, j) { - int32_t mode = BinaryReader_readInt32(reader); - spr->nsTileModes[j] = (uint8_t) mode; - } - BinaryReader_seek(reader, savedPos); - } - } - - free(ptrs); -} - -static void parseBGND(BinaryReader* reader, DataWin* dw) { - Bgnd* b = &dw->bgnd; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - b->count = count; - - if (count == 0) { free(ptrs); b->backgrounds = nullptr; return; } - - b->backgrounds = safeCalloc(count, sizeof(Background)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - Background* bg = &b->backgrounds[i]; - bg->name = readStringPtr(reader, dw); - bg->transparent = BinaryReader_readBool32(reader); - bg->smooth = BinaryReader_readBool32(reader); - bg->preload = BinaryReader_readBool32(reader); - // Temporarily store the absolute file offset; parseTPAG resolves it in-place to a TPAG index once the TPAG table is known. - bg->tpagIndex = (int32_t) BinaryReader_readUint32(reader); - if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) { - bg->gms2UnknownAlways2 = BinaryReader_readUint32(reader); - bg->gms2TileWidth = BinaryReader_readUint32(reader); - bg->gms2TileHeight = BinaryReader_readUint32(reader); - if (DataWin_isVersionAtLeast(dw, 2024, 14, 0, 1)) { - bg->gms2TileSeparationX = BinaryReader_readUint32(reader); - bg->gms2TileSeparationY = BinaryReader_readUint32(reader); - } - bg->gms2OutputBorderX = BinaryReader_readUint32(reader); - bg->gms2OutputBorderY = BinaryReader_readUint32(reader); - bg->gms2TileColumns = BinaryReader_readUint32(reader); - bg->gms2ItemsPerTileCount = BinaryReader_readUint32(reader); - bg->gms2TileCount = BinaryReader_readUint32(reader); - bg->gms2ExportedSpriteIndex = BinaryReader_readInt32(reader); - bg->gms2FrameLength = BinaryReader_readInt64(reader); - int tileIdCount = bg->gms2TileCount * bg->gms2ItemsPerTileCount; - bg->gms2TileIds = malloc(tileIdCount*sizeof(uint32_t)); - repeat(tileIdCount, j) { - bg->gms2TileIds[j] = BinaryReader_readUint32(reader); - } - } - } - free(ptrs); -} - -static void parsePATH(BinaryReader* reader, DataWin* dw) { - PathChunk* p = &dw->path; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - p->count = count; - - if (count == 0) { free(ptrs); p->paths = nullptr; return; } - - p->paths = safeMalloc(count * sizeof(GamePath)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - GamePath* path = &p->paths[i]; - path->internalPoints = nullptr; - path->internalPointCount = 0; - path->length = 0.0; - path->name = readStringPtr(reader, dw); - path->isSmooth = BinaryReader_readBool32(reader); - path->isClosed = BinaryReader_readBool32(reader); - path->precision = BinaryReader_readUint32(reader); - - // Points SimpleList - path->pointCount = BinaryReader_readUint32(reader); - if (path->pointCount > 0) { - path->points = safeMalloc(path->pointCount * sizeof(PathPoint)); - repeat(path->pointCount, j) { - path->points[j].x = BinaryReader_readFloat32(reader); - path->points[j].y = BinaryReader_readFloat32(reader); - path->points[j].speed = BinaryReader_readFloat32(reader); - } - } else { - path->points = nullptr; - } - - // Precompute internal representation for path following - GamePath_computeInternal(path); - } - free(ptrs); -} - -static void parseSCPT(BinaryReader* reader, DataWin* dw) { - Scpt* s = &dw->scpt; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - s->count = count; - - if (count == 0) { free(ptrs); s->scripts = nullptr; return; } - - s->scripts = safeMalloc(count * sizeof(Script)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - s->scripts[i].name = readStringPtr(reader, dw); - s->scripts[i].codeId = BinaryReader_readInt32(reader); - } - free(ptrs); -} - -static void parseGLOB(BinaryReader* reader, DataWin* dw) { - Glob* g = &dw->glob; - - g->count = BinaryReader_readUint32(reader); - if (g->count > 0) { - g->codeIds = safeMalloc(g->count * sizeof(int32_t)); - repeat(g->count, i) { - g->codeIds[i] = BinaryReader_readInt32(reader); - } - } else { - g->codeIds = nullptr; - } -} - -static void parseSHDR(BinaryReader* reader, DataWin* dw) { - Shdr* s = &dw->shdr; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - s->count = count; - - if (count == 0) { free(ptrs); s->shaders = nullptr; return; } - - s->shaders = safeMalloc(count * sizeof(Shader)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - Shader* sh = &s->shaders[i]; - sh->name = readStringPtr(reader, dw); - sh->type = BinaryReader_readUint32(reader) & 0x7FFFFFFF; - sh->glslES_Vertex = readStringPtr(reader, dw); - sh->glslES_Fragment = readStringPtr(reader, dw); - sh->glsl_Vertex = readStringPtr(reader, dw); - sh->glsl_Fragment = readStringPtr(reader, dw); - sh->hlsl9_Vertex = readStringPtr(reader, dw); - sh->hlsl9_Fragment = readStringPtr(reader, dw); - sh->hlsl11_VertexOffset = BinaryReader_readUint32(reader); - sh->hlsl11_PixelOffset = BinaryReader_readUint32(reader); - - // Vertex attributes SimpleList - sh->vertexAttributeCount = BinaryReader_readUint32(reader); - if (sh->vertexAttributeCount > 0) { - sh->vertexAttributes = safeMalloc(sh->vertexAttributeCount * sizeof(const char*)); - repeat(sh->vertexAttributeCount, j) { - sh->vertexAttributes[j] = readStringPtr(reader, dw); - } - } else { - sh->vertexAttributes = nullptr; - } - - // Version field (bytecodeVersion > 13) - sh->version = BinaryReader_readInt32(reader); - - sh->pssl_VertexOffset = BinaryReader_readUint32(reader); - sh->pssl_VertexLen = BinaryReader_readUint32(reader); - sh->pssl_PixelOffset = BinaryReader_readUint32(reader); - sh->pssl_PixelLen = BinaryReader_readUint32(reader); - sh->cgVita_VertexOffset = BinaryReader_readUint32(reader); - sh->cgVita_VertexLen = BinaryReader_readUint32(reader); - sh->cgVita_PixelOffset = BinaryReader_readUint32(reader); - sh->cgVita_PixelLen = BinaryReader_readUint32(reader); - - if (sh->version >= 2) { - sh->cgPS3_VertexOffset = BinaryReader_readUint32(reader); - sh->cgPS3_VertexLen = BinaryReader_readUint32(reader); - sh->cgPS3_PixelOffset = BinaryReader_readUint32(reader); - sh->cgPS3_PixelLen = BinaryReader_readUint32(reader); - } else { - sh->cgPS3_VertexOffset = 0; - sh->cgPS3_VertexLen = 0; - sh->cgPS3_PixelOffset = 0; - sh->cgPS3_PixelLen = 0; - } - - // Blob data follows but we skip it (pointer list seeking handles position) - } - free(ptrs); -} - -static void parseFONT(BinaryReader* reader, DataWin* dw) { - FontChunk* f = &dw->font; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - f->count = count; - - if (count == 0) { free(ptrs); f->fonts = nullptr; return; } - - // We need to figure out how many uint32 fields are between here and the PointerList - uint32_t fontOptionalCount = (dw->gen8.bytecodeVersion >= 17) ? 1u : 0u; - { - size_t baseAfterScaleY = (size_t) ptrs[0] + 40; - for (uint32_t trial = fontOptionalCount; 4 >= trial; trial++) { - size_t listStart = baseAfterScaleY + 4u * trial; - BinaryReader_seek(reader, listStart); - uint32_t probedGlyphCount = BinaryReader_readUint32(reader); - if (probedGlyphCount == 0 || probedGlyphCount > 0x10000) continue; - uint32_t probedFirstPtr = BinaryReader_readUint32(reader); - size_t expectedFirstPtr = listStart + 4u + 4u * probedGlyphCount; - if ((size_t) probedFirstPtr == expectedFirstPtr) { - fontOptionalCount = trial; - break; - } - } - } - - f->fonts = safeMalloc(count * sizeof(Font)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - Font* font = &f->fonts[i]; - font->name = readStringPtr(reader, dw); - font->displayName = readStringPtr(reader, dw); - font->emSize = BinaryReader_readUint32(reader); - font->bold = BinaryReader_readBool32(reader); - font->italic = BinaryReader_readBool32(reader); - font->rangeStart = BinaryReader_readUint16(reader); - font->charset = BinaryReader_readUint8(reader); - font->antiAliasing = BinaryReader_readUint8(reader); - font->rangeEnd = BinaryReader_readUint32(reader); - // Temporarily store the absolute file offset; parseTPAG resolves it in-place to a TPAG index once the TPAG table is known. - font->tpagIndex = (int32_t) BinaryReader_readUint32(reader); - font->scaleX = BinaryReader_readFloat32(reader); - font->scaleY = BinaryReader_readFloat32(reader); - // Optional fields appear in this order when present: AscenderOffset (BC17+), - // Ascender, SDFSpread, LineHeight. `fontOptionalCount` says how many are actually on disk. - font->ascenderOffset = 0; - font->ascender = 0; - font->sdfSpread = 0; - font->lineHeight = 0; - font->hasAscender = false; - font->hasSDFSpread = false; - font->hasLineHeight = false; - uint32_t readSoFar = 0; - if (dw->gen8.bytecodeVersion >= 17 && fontOptionalCount > readSoFar) { - font->ascenderOffset = BinaryReader_readInt32(reader); - readSoFar++; - } - if (fontOptionalCount > readSoFar) { - font->ascender = BinaryReader_readUint32(reader); - font->hasAscender = true; - readSoFar++; - } - if (fontOptionalCount > readSoFar) { - font->sdfSpread = BinaryReader_readUint32(reader); - font->hasSDFSpread = true; - readSoFar++; - } - if (fontOptionalCount > readSoFar) { - font->lineHeight = BinaryReader_readUint32(reader); - font->hasLineHeight = true; - readSoFar++; - } - font->isSpriteFont = false; - font->spriteIndex = -1; - - // Glyphs PointerList - uint32_t glyphCount; - uint32_t* glyphPtrs = readPointerTable(reader, &glyphCount); - font->glyphCount = glyphCount; - - uint32_t maxGlyphHeight = 0; - if (glyphCount > 0) { - font->glyphs = safeMalloc(glyphCount * sizeof(FontGlyph)); - repeat(glyphCount, j) { - BinaryReader_seek(reader, glyphPtrs[j]); - FontGlyph* glyph = &font->glyphs[j]; - glyph->character = BinaryReader_readUint16(reader); - glyph->sourceX = BinaryReader_readUint16(reader); - glyph->sourceY = BinaryReader_readUint16(reader); - glyph->sourceWidth = BinaryReader_readUint16(reader); - glyph->sourceHeight = BinaryReader_readUint16(reader); - glyph->shift = BinaryReader_readInt16(reader); - glyph->offset = BinaryReader_readInt16(reader); - - if (glyph->sourceHeight > maxGlyphHeight) maxGlyphHeight = glyph->sourceHeight; - - // Kerning SimpleListShort (uint16 count) - glyph->kerningCount = BinaryReader_readUint16(reader); - if (glyph->kerningCount > 0) { - glyph->kerning = safeMalloc(glyph->kerningCount * sizeof(KerningPair)); - for (uint16_t k = 0; glyph->kerningCount > k; k++) { - glyph->kerning[k].character = BinaryReader_readInt16(reader); - glyph->kerning[k].shiftModifier = BinaryReader_readInt16(reader); - } - } else { - glyph->kerning = nullptr; - } - } - } else { - font->glyphs = nullptr; - } - font->maxGlyphHeight = maxGlyphHeight; - Font_buildGlyphLUT(font); - free(glyphPtrs); - } - free(ptrs); - - // 512 bytes of trailing padding -- skipped by chunkEnd seek -} - -static void parseTMLN(BinaryReader* reader, DataWin* dw) { - Tmln* t = &dw->tmln; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - t->count = count; - - if (count == 0) { free(ptrs); t->timelines = nullptr; return; } - - t->timelines = safeMalloc(count * sizeof(Timeline)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - Timeline* tl = &t->timelines[i]; - tl->name = readStringPtr(reader, dw); - tl->momentCount = BinaryReader_readUint32(reader); - - if (tl->momentCount > 0) { - tl->moments = safeMalloc(tl->momentCount * sizeof(TimelineMoment)); - - // Pass 1: Read step + event pointer pairs - uint32_t* eventPtrs = safeMalloc(tl->momentCount * sizeof(uint32_t)); - repeat(tl->momentCount, j) { - tl->moments[j].step = BinaryReader_readUint32(reader); - eventPtrs[j] = BinaryReader_readUint32(reader); - } - - // Pass 2: Parse event action lists - repeat(tl->momentCount, j) { - BinaryReader_seek(reader, eventPtrs[j]); - tl->moments[j].actions = readEventActions(reader, dw, &tl->moments[j].actionCount); - } - free(eventPtrs); - } else { - tl->moments = nullptr; - } - } - free(ptrs); -} - -static void parseOBJT(BinaryReader* reader, DataWin* dw) { - Objt* o = &dw->objt; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - o->count = count; - - if (count == 0) { free(ptrs); o->objects = nullptr; return; } - - // Detect GMS 2022.5+ by probing the first game object's event list structure. - if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && !DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0)) { - // Skip the 16 fixed uint32 header fields (name..angularDamping) to reach physicsVertexCount. - BinaryReader_seek(reader, ptrs[0] + 16 * 4); - int32_t vertexCount = BinaryReader_readInt32(reader); - if (vertexCount >= 0) { - // Skip friction + awake + kinematic (12 bytes) and physics vertices (8 bytes each). - uint32_t skipCount = 12 + vertexCount * 8; - uint32_t newLocation = reader->bufferPos + skipCount; - bool isOldFormat = false; - if (newLocation < reader->bufferSize) { - BinaryReader_skip(reader, skipCount); - uint32_t eventTypeCount = BinaryReader_readUint32(reader); - if (eventTypeCount == OBJT_EVENT_TYPE_COUNT) { - uint32_t firstSubEventPtr = BinaryReader_readUint32(reader); - uint32_t currentAbsPos = (uint32_t) BinaryReader_getPosition(reader); - // The remaining 14 outer-list pointers sit between here and the first sub-event list. - if (firstSubEventPtr == currentAbsPos + 14 * 4) { - isOldFormat = true; - } - } - } - if (!isOldFormat) { - DataWin_bumpVersionTo(dw, 2022, 5, 0, 0); - } - } - } - - o->objects = safeMalloc(count * sizeof(GameObject)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - GameObject* obj = &o->objects[i]; - obj->name = readStringPtr(reader, dw); - obj->spriteId = BinaryReader_readInt32(reader); - obj->visible = BinaryReader_readBool32(reader); - if (DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0)) { - obj->managed = BinaryReader_readBool32(reader); - } else { - obj->managed = false; - } - obj->solid = BinaryReader_readBool32(reader); - obj->depth = BinaryReader_readInt32(reader); - obj->persistent = BinaryReader_readBool32(reader); - obj->parentId = BinaryReader_readInt32(reader); - obj->textureMaskId = BinaryReader_readInt32(reader); - obj->usesPhysics = BinaryReader_readBool32(reader); - obj->isSensor = BinaryReader_readBool32(reader); - obj->collisionShape = BinaryReader_readUint32(reader); - obj->density = BinaryReader_readFloat32(reader); - obj->restitution = BinaryReader_readFloat32(reader); - obj->group = BinaryReader_readUint32(reader); - obj->linearDamping = BinaryReader_readFloat32(reader); - obj->angularDamping = BinaryReader_readFloat32(reader); - obj->physicsVertexCount = BinaryReader_readInt32(reader); - obj->friction = BinaryReader_readFloat32(reader); - obj->awake = BinaryReader_readBool32(reader); - obj->kinematic = BinaryReader_readBool32(reader); - - // Physics vertices - if (obj->physicsVertexCount > 0) { - obj->physicsVertices = safeMalloc(obj->physicsVertexCount * sizeof(PhysicsVertex)); - for (int32_t j = 0; obj->physicsVertexCount > j; j++) { - obj->physicsVertices[j].x = BinaryReader_readFloat32(reader); - obj->physicsVertices[j].y = BinaryReader_readFloat32(reader); - } - } else { - obj->physicsVertices = nullptr; - } - - // Events: UndertalePointerList> - // Outer pointer list: one entry per event type - // Inner pointer list: events for that type - uint32_t eventTypeCount; - uint32_t* eventTypePtrs = readPointerTable(reader, &eventTypeCount); - - for (uint32_t eventType = 0; eventTypeCount > eventType && OBJT_EVENT_TYPE_COUNT > eventType; eventType++) { - BinaryReader_seek(reader, eventTypePtrs[eventType]); - - // Inner pointer list: events for this type - uint32_t eventCount; - uint32_t* eventPtrs = readPointerTable(reader, &eventCount); - - obj->eventLists[eventType].eventCount = eventCount; - - if (eventCount > 0) { - obj->eventLists[eventType].events = safeMalloc(eventCount * sizeof(ObjectEvent)); - repeat(eventCount, j) { - BinaryReader_seek(reader, eventPtrs[j]); - obj->eventLists[eventType].events[j].eventSubtype = BinaryReader_readUint32(reader); - obj->eventLists[eventType].events[j].actions = readEventActions(reader, dw, &obj->eventLists[eventType].events[j].actionCount); - } - } else { - obj->eventLists[eventType].events = nullptr; - } - - free(eventPtrs); - } - - // Zero-fill any unused event type slots - for (uint32_t eventType = eventTypeCount; OBJT_EVENT_TYPE_COUNT > eventType; eventType++) { - obj->eventLists[eventType].eventCount = 0; - obj->eventLists[eventType].events = nullptr; - } - - free(eventTypePtrs); - } - free(ptrs); -} - -// ===[ Room payload parsing helpers ]=== -// Each of these assumes the caller has seeked to the start of the relevant PointerList (where the uint32 "count" of the list is). -// They allocate and populate the corresponding fields on Room. -// They are used by both the eager parse path and the lazy load path (DataWin_loadRoomPayload). - -static void readRoomBackgrounds(BinaryReader* reader, Room* room) { - uint32_t bgCount; - uint32_t* bgPtrs = readPointerTable(reader, &bgCount); - room->backgrounds = safeMalloc(8 * sizeof(RoomBackground)); - uint32_t fillEnd = bgCount < 8 ? bgCount : 8; - for (uint32_t j = 0; fillEnd > j; j++) { - BinaryReader_seek(reader, bgPtrs[j]); - RoomBackground* bg = &room->backgrounds[j]; - bg->enabled = BinaryReader_readBool32(reader); - bg->foreground = BinaryReader_readBool32(reader); - bg->backgroundDefinition = BinaryReader_readInt32(reader); - bg->x = BinaryReader_readInt32(reader); - bg->y = BinaryReader_readInt32(reader); - bg->tileX = BinaryReader_readInt32(reader); - bg->tileY = BinaryReader_readInt32(reader); - bg->speedX = BinaryReader_readInt32(reader); - bg->speedY = BinaryReader_readInt32(reader); - bg->stretch = BinaryReader_readBool32(reader); - } - for (uint32_t j = fillEnd; 8 > j; j++) { - memset(&room->backgrounds[j], 0, sizeof(RoomBackground)); - } - free(bgPtrs); -} - -static void readRoomViews(BinaryReader* reader, Room* room) { - uint32_t viewCount; - uint32_t* viewPtrsArr = readPointerTable(reader, &viewCount); - room->views = safeMalloc(8 * sizeof(RoomView)); - for (uint32_t j = 0; viewCount > j && 8 > j; j++) { - BinaryReader_seek(reader, viewPtrsArr[j]); - RoomView* view = &room->views[j]; - view->enabled = BinaryReader_readBool32(reader); - view->viewX = BinaryReader_readInt32(reader); - view->viewY = BinaryReader_readInt32(reader); - view->viewWidth = BinaryReader_readInt32(reader); - view->viewHeight = BinaryReader_readInt32(reader); - view->portX = BinaryReader_readInt32(reader); - view->portY = BinaryReader_readInt32(reader); - view->portWidth = BinaryReader_readInt32(reader); - view->portHeight = BinaryReader_readInt32(reader); - view->borderX = BinaryReader_readUint32(reader); - view->borderY = BinaryReader_readUint32(reader); - view->speedX = BinaryReader_readInt32(reader); - view->speedY = BinaryReader_readInt32(reader); - view->objectId = BinaryReader_readInt32(reader); - } - for (uint32_t j = viewCount; 8 > j; j++) { - memset(&room->views[j], 0, sizeof(RoomView)); - } - free(viewPtrsArr); -} - -static void readRoomGameObjects(BinaryReader* reader, DataWin* dw, Room* room) { - uint32_t objCount; - uint32_t* objPtrs = readPointerTable(reader, &objCount); - room->gameObjectCount = objCount; - if (objCount > 0) { - room->gameObjects = safeMalloc(objCount * sizeof(RoomGameObject)); - repeat(objCount, j) { - BinaryReader_seek(reader, objPtrs[j]); - RoomGameObject* go = &room->gameObjects[j]; - go->x = BinaryReader_readInt32(reader); - go->y = BinaryReader_readInt32(reader); - go->objectDefinition = BinaryReader_readInt32(reader); - go->instanceID = BinaryReader_readUint32(reader); - go->creationCode = BinaryReader_readInt32(reader); - go->scaleX = BinaryReader_readFloat32(reader); - go->scaleY = BinaryReader_readFloat32(reader); - if (DataWin_isVersionAtLeast(dw, 2, 2, 2, 302)) { - go->imageSpeed = BinaryReader_readFloat32(reader); - go->imageIndex = BinaryReader_readInt32(reader); - } else { - go->imageSpeed = 1.0f; - go->imageIndex = 0; - } - go->color = BinaryReader_readUint32(reader); - go->rotation = BinaryReader_readFloat32(reader); - if (dw->gen8.bytecodeVersion >= 16) { - go->preCreateCode = BinaryReader_readInt32(reader); - } else { - go->preCreateCode = -1; - } - } - } else { - room->gameObjects = nullptr; - } - free(objPtrs); -} - -static void readRoomTiles(BinaryReader* reader, DataWin* dw, Room* room) { - uint32_t tileCount; - uint32_t* tilePtrs = readPointerTable(reader, &tileCount); - room->tileCount = tileCount; - if (tileCount > 0) { - room->tiles = safeMalloc(tileCount * sizeof(RoomTile)); - repeat(tileCount, j) { - BinaryReader_seek(reader, tilePtrs[j]); - RoomTile* tile = &room->tiles[j]; - tile->x = BinaryReader_readInt32(reader); - tile->y = BinaryReader_readInt32(reader); - tile->useSpriteDefinition = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0); - tile->backgroundDefinition = BinaryReader_readInt32(reader); - tile->sourceX = BinaryReader_readInt32(reader); - tile->sourceY = BinaryReader_readInt32(reader); - tile->width = BinaryReader_readUint32(reader); - tile->height = BinaryReader_readUint32(reader); - tile->tileDepth = BinaryReader_readInt32(reader); - tile->instanceID = BinaryReader_readUint32(reader); - tile->scaleX = BinaryReader_readFloat32(reader); - tile->scaleY = BinaryReader_readFloat32(reader); - tile->color = BinaryReader_readUint32(reader); - } - } else { - room->tiles = nullptr; - } - free(tilePtrs); -} - -static void readRoomLayers(BinaryReader* reader, DataWin* dw, Room* room) { - uint32_t layerCount; - uint32_t* layerPtrs = readPointerTable(reader, &layerCount); - room->layerCount = layerCount; - - if (layerCount == 0) { - room->layers = nullptr; - free(layerPtrs); - return; - } - - room->layers = safeMalloc(layerCount * sizeof(RoomLayer)); - repeat(layerCount, j) { - BinaryReader_seek(reader, layerPtrs[j]); - RoomLayer* layer = &room->layers[j]; - layer->name = readStringPtr(reader, dw); - layer->id = BinaryReader_readUint32(reader); - layer->type = BinaryReader_readUint32(reader); - layer->depth = BinaryReader_readInt32(reader); - layer->xOffset = BinaryReader_readFloat32(reader); - layer->yOffset = BinaryReader_readFloat32(reader); - layer->hSpeed = BinaryReader_readFloat32(reader); - layer->vSpeed = BinaryReader_readFloat32(reader); - layer->visible = BinaryReader_readBool32(reader); - layer->assetsData = nullptr; - layer->backgroundData = nullptr; - layer->instancesData = nullptr; - layer->tilesData = nullptr; - if (DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) { - // EffectEnabled (bool32), EffectType (string ptr), EffectProperties (SimpleList) - BinaryReader_skip(reader, 4); // EffectEnabled - BinaryReader_skip(reader, 4); // EffectType (string ptr) - uint32_t effectPropCount = BinaryReader_readUint32(reader); - // Each EffectProperty is 12 bytes: Kind(int32) + Name(ptr) + Value(ptr) - BinaryReader_skip(reader, effectPropCount * 12); - } - switch (layer->type) { - case RoomLayerType_Path: - case RoomLayerType_Path2: - break; // Nothing to do - case RoomLayerType_Effect: - // In GMS 2022.1+, Effect layer data is empty (fields moved to layer header). - if (!DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) { - BinaryReader_skip(reader, 4); // EffectType (string ptr) - uint32_t propCount = BinaryReader_readUint32(reader); - BinaryReader_skip(reader, propCount * 12); - } - break; - - case RoomLayerType_Assets: { - RoomLayerAssetsData* assets = safeMalloc(sizeof(RoomLayerAssetsData)); - uint32_t legacyTilesPtr = BinaryReader_readUint32(reader); - uint32_t spritesPtr = BinaryReader_readUint32(reader); - - BinaryReader_seek(reader, legacyTilesPtr); - uint32_t *innerTilePtrs = readPointerTable(reader, &assets->legacyTileCount); - if (assets->legacyTileCount > 0) { - assets->legacyTiles = safeMalloc(assets->legacyTileCount * sizeof(RoomTile)); - repeat(assets->legacyTileCount, k) { - BinaryReader_seek(reader, innerTilePtrs[k]); - RoomTile* tile = &assets->legacyTiles[k]; - tile->x = BinaryReader_readInt32(reader); - tile->y = BinaryReader_readInt32(reader); - tile->useSpriteDefinition = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0); - tile->backgroundDefinition = BinaryReader_readInt32(reader); - tile->sourceX = BinaryReader_readInt32(reader); - tile->sourceY = BinaryReader_readInt32(reader); - tile->width = BinaryReader_readUint32(reader); - tile->height = BinaryReader_readUint32(reader); - tile->tileDepth = BinaryReader_readInt32(reader); - tile->instanceID = BinaryReader_readUint32(reader); - tile->scaleX = BinaryReader_readFloat32(reader); - tile->scaleY = BinaryReader_readFloat32(reader); - tile->color = BinaryReader_readUint32(reader); - } - } else { - assets->legacyTiles = nullptr; - } - free(innerTilePtrs); - - BinaryReader_seek(reader, spritesPtr); - uint32_t *spritePtrs = readPointerTable(reader, &assets->spriteCount); - if (assets->spriteCount > 0) { - assets->sprites = safeMalloc(assets->spriteCount * sizeof(SpriteInstance)); - repeat(assets->spriteCount, k) { - BinaryReader_seek(reader, spritePtrs[k]); - SpriteInstance* sprite = &assets->sprites[k]; - sprite->name = readStringPtr(reader, dw); - sprite->spriteIndex = BinaryReader_readInt32(reader); - sprite->x = BinaryReader_readInt32(reader); - sprite->y = BinaryReader_readInt32(reader); - sprite->scaleX = BinaryReader_readFloat32(reader); - sprite->scaleY = BinaryReader_readFloat32(reader); - sprite->color = BinaryReader_readUint32(reader); - sprite->animationSpeed = BinaryReader_readFloat32(reader); - sprite->animationSpeedType = BinaryReader_readUint32(reader); - sprite->frameIndex = BinaryReader_readFloat32(reader); - sprite->rotation = BinaryReader_readFloat32(reader); - } - } else { - assets->sprites = nullptr; - } - free(spritePtrs); - - layer->assetsData = assets; - break; - } - - case RoomLayerType_Background: { - RoomLayerBackgroundData* bg = safeMalloc(sizeof(RoomLayerBackgroundData)); - bg->visible = BinaryReader_readBool32(reader); - bg->foreground = BinaryReader_readBool32(reader); - bg->spriteIndex = BinaryReader_readInt32(reader); - bg->hTiled = BinaryReader_readBool32(reader); - bg->vTiled = BinaryReader_readBool32(reader); - bg->stretch = BinaryReader_readBool32(reader); - bg->color = BinaryReader_readUint32(reader); - bg->firstFrame = BinaryReader_readFloat32(reader); - bg->animSpeed = BinaryReader_readFloat32(reader); - bg->animSpeedType = BinaryReader_readUint32(reader); - layer->backgroundData = bg; - break; - } - case RoomLayerType_Instances: { - RoomLayerInstancesData* inst = safeMalloc(sizeof(RoomLayerInstancesData)); - inst->instanceCount = BinaryReader_readUint32(reader); - if (inst->instanceCount > 0) { - inst->instanceIds = safeMalloc(inst->instanceCount * sizeof(uint32_t)); - repeat(inst->instanceCount, k) { - inst->instanceIds[k] = BinaryReader_readUint32(reader); - } - } else { - inst->instanceIds = nullptr; - } - layer->instancesData = inst; - break; - } - case RoomLayerType_Tiles: { - RoomLayerTilesData* tiles = safeMalloc(sizeof(RoomLayerTilesData)); - tiles->backgroundIndex = BinaryReader_readInt32(reader); - tiles->tilesX = BinaryReader_readUint32(reader); - tiles->tilesY = BinaryReader_readUint32(reader); - uint32_t totalTiles = tiles->tilesX * tiles->tilesY; - if (totalTiles > 0) { - tiles->tileData = safeMalloc(totalTiles * sizeof(uint32_t)); - repeat(totalTiles, k) { - tiles->tileData[k] = BinaryReader_readUint32(reader); - } - } else { - tiles->tileData = nullptr; - } - layer->tilesData = tiles; - break; - } - default: { - fprintf(stderr, "Unsupported Room Layer Type %u\n", layer->type); - exit(0); - } - } - } - free(layerPtrs); -} - -// Reads all 5 payload sections for a single room via the given reader. -// Assumes the caller has populated room->*FileOffset from the header pass. -static void readRoomPayload(BinaryReader* reader, DataWin* dw, Room* room) { - require(!room->payloadLoaded); - - BinaryReader_seek(reader, room->backgroundsFileOffset); - readRoomBackgrounds(reader, room); - - BinaryReader_seek(reader, room->viewsFileOffset); - readRoomViews(reader, room); - - BinaryReader_seek(reader, room->gameObjectsFileOffset); - readRoomGameObjects(reader, dw, room); - - BinaryReader_seek(reader, room->tilesFileOffset); - readRoomTiles(reader, dw, room); - - room->layerCount = 0; - room->layers = nullptr; - if (room->layersFileOffset != 0) { - BinaryReader_seek(reader, room->layersFileOffset); - readRoomLayers(reader, dw, room); - } - - room->payloadLoaded = true; -} - -// Returns true when "name" is in the eager-load set. -static bool isRoomNameInEagerList(const char* name, StringBooleanEntry* eagerSet) { - if (name == nullptr || eagerSet == nullptr) return false; - return shgeti(eagerSet, name) >= 0; -} - -static void parseROOM(BinaryReader* reader, DataWin* dw, bool lazyLoadRooms, StringBooleanEntry* eagerlyLoadedRooms) { - RoomChunk* rc = &dw->room; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - rc->count = count; - - if (count == 0) { free(ptrs); rc->rooms = nullptr; return; } - - // Detect whether RoomGameObject includes ImageSpeed/ImageIndex fields (added in GMS 2.2.2.302). - // UndertaleModTool detects this via the distance between the first two game object pointers: 40 bytes = legacy format, 48 bytes = new format with ImageSpeed+ImageIndex. - // We skip if we already know that we are at or above 2.2.2.302. - if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0) && !DataWin_isVersionAtLeast(dw, 2, 2, 2, 302)) { - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - // Room header layout (before gameObjectsPtr): name, caption, width, height, speed, persistent, - // bgColor, drawBgColor, creationCodeId, flags, backgroundsPtr, viewsPtr = 12 uint32s. - BinaryReader_skip(reader, 12 * 4); - uint32_t gameObjectsPtr = BinaryReader_readUint32(reader); - BinaryReader_seek(reader, gameObjectsPtr); - uint32_t objCount = BinaryReader_readUint32(reader); - if (objCount >= 2) { - uint32_t firstPtr = BinaryReader_readUint32(reader); - uint32_t secondPtr = BinaryReader_readUint32(reader); - if (secondPtr - firstPtr == 48) { - DataWin_bumpVersionTo(dw, 2, 2, 2, 302); - } - break; - } - } - } - - // Detect whether Layer headers include EffectEnabled/EffectType/EffectProperties fields (added in GMS 2022.1). - if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && !DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) { - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - // Room header before layersPtr: 22 uint32s (name..metersPerPixel). - BinaryReader_skip(reader, 22 * 4); - uint32_t layersPtr = BinaryReader_readUint32(reader); - uint32_t seqnPtr = BinaryReader_readUint32(reader); - BinaryReader_seek(reader, layersPtr); - uint32_t layerCount = BinaryReader_readUint32(reader); - if (layerCount == 0) continue; - uint32_t jumpOffset = BinaryReader_readUint32(reader); - uint32_t nextOffset = (layerCount == 1) ? seqnPtr : BinaryReader_readUint32(reader); - // Layer header: name(4) id(4) type(4) depth(4) xOff(4) yOff(4) hSpd(4) vSpd(4) visible(4) = 9 uint32s = 36 bytes. - // jumpOffset points to start of the layer; we seek to jumpOffset+8 to skip name+id then read type. - BinaryReader_seek(reader, jumpOffset + 8); - uint32_t layerType = BinaryReader_readUint32(reader); - if (layerType == RoomLayerType_Path || layerType == RoomLayerType_Path2) continue; - bool detected = false; - switch (layerType) { - case RoomLayerType_Background: { - // After type, there's depth+xOff+yOff+hSpd+vSpd+visible = 6*4 = 24, then 10 background fields = 40 bytes. - // Total legacy body after type read: 24 + 40 = 64 bytes. 2022.1 adds effect data > 64 bytes of additional data past the next layer boundary. - size_t absPos = BinaryReader_getPosition(reader); - if (nextOffset - absPos > 16 * 4) detected = true; - break; - } - case RoomLayerType_Instances: { - BinaryReader_skip(reader, 6 * 4); - uint32_t instanceCount = BinaryReader_readUint32(reader); - size_t absPos = BinaryReader_getPosition(reader); - if (nextOffset - absPos != instanceCount * 4) detected = true; - break; - } - case RoomLayerType_Assets: { - BinaryReader_skip(reader, 6 * 4); - uint32_t tileOffset = BinaryReader_readUint32(reader); - size_t absPos = BinaryReader_getPosition(reader); - if (tileOffset != absPos + 8 && tileOffset != absPos + 12) detected = true; - break; - } - case RoomLayerType_Tiles: { - BinaryReader_skip(reader, 7 * 4); - uint32_t tileMapWidth = BinaryReader_readUint32(reader); - uint32_t tileMapHeight = BinaryReader_readUint32(reader); - size_t absPos = BinaryReader_getPosition(reader); - if (nextOffset - absPos != tileMapWidth * tileMapHeight * 4) detected = true; - break; - } - case RoomLayerType_Effect: { - BinaryReader_skip(reader, 7 * 4); - uint32_t propertyCount = BinaryReader_readUint32(reader); - size_t absPos = BinaryReader_getPosition(reader); - if (nextOffset - absPos != propertyCount * 3 * 4) detected = true; - break; - } - } - if (detected) DataWin_bumpVersionTo(dw, 2022, 1, 0, 0); - break; - } - } - - rc->rooms = safeCalloc(count, sizeof(Room)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - Room* room = &rc->rooms[i]; - - // ===[ Header pass ]=== - room->name = readStringPtr(reader, dw); - room->caption = readStringPtr(reader, dw); - room->width = BinaryReader_readUint32(reader); - room->height = BinaryReader_readUint32(reader); - room->speed = BinaryReader_readUint32(reader); - room->persistent = BinaryReader_readBool32(reader); - room->backgroundColor = BinaryReader_readUint32(reader); - room->drawBackgroundColor = BinaryReader_readBool32(reader); - room->creationCodeId = BinaryReader_readInt32(reader); - room->flags = BinaryReader_readUint32(reader); - room->backgroundsFileOffset = BinaryReader_readUint32(reader); - room->viewsFileOffset = BinaryReader_readUint32(reader); - room->gameObjectsFileOffset = BinaryReader_readUint32(reader); - room->tilesFileOffset = BinaryReader_readUint32(reader); - room->world = BinaryReader_readBool32(reader); - room->top = BinaryReader_readUint32(reader); - room->left = BinaryReader_readUint32(reader); - room->right = BinaryReader_readUint32(reader); - room->bottom = BinaryReader_readUint32(reader); - room->gravityX = BinaryReader_readFloat32(reader); - room->gravityY = BinaryReader_readFloat32(reader); - room->metersPerPixel = BinaryReader_readFloat32(reader); - if (DataWin_isVersionAtLeast(dw, 2024, 13, 0, 0)) { - // skip instanceCreationOrderIDs - int icCount = BinaryReader_readInt32(reader); - BinaryReader_skip(reader, sizeof(int32_t) * icCount); - } - room->layersFileOffset = 0; - if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) { - room->layersFileOffset = BinaryReader_readUint32(reader); - if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0)) { - BinaryReader_skip(reader, 4); // sequencesPtr - } - } - - room->payloadLoaded = false; - room->eagerlyLoaded = false; - room->backgrounds = nullptr; - room->views = nullptr; - room->gameObjects = nullptr; - room->gameObjectCount = 0; - room->tiles = nullptr; - room->tileCount = 0; - room->layers = nullptr; - room->layerCount = 0; - - // Load the room payload if needed - bool eager = !lazyLoadRooms || isRoomNameInEagerList(room->name, eagerlyLoadedRooms); - if (eager) { - readRoomPayload(reader, dw, room); - if (lazyLoadRooms) { - room->eagerlyLoaded = true; - } - } - } - free(ptrs); -} - -// Sprite/Background/Font initially store an absolute file offset to their TexturePageItem (since SPRT/BGND/FONT are parsed before TPAG). -// resolveAllTPAGReferences translates those offsets to TPAG indices once the table is known. ptrs[] is the TPAG pointer table in monotonically increasing file order, so we can binary search it. -// Offsets that don't resolve (or are 0) become -1. -static int32_t findTPAGIndexByOffset(uint32_t* ptrs, uint32_t count, uint32_t offset) { - if (offset == 0) return -1; - uint32_t lo = 0, hi = count; - while (hi > lo) { - uint32_t mid = (lo + hi) >> 1; - uint32_t v = ptrs[mid]; - if (v == offset) return (int32_t) mid; - if (offset > v) lo = mid + 1; else hi = mid; - } - return -1; -} - -static void resolveAllTPAGReferences(DataWin* dw, uint32_t* ptrs, uint32_t count) { - repeat(dw->sprt.count, i) { - Sprite* spr = &dw->sprt.sprites[i]; - repeat(spr->textureCount, j) { - spr->tpagIndices[j] = findTPAGIndexByOffset(ptrs, count, (uint32_t) spr->tpagIndices[j]); - } - } - repeat(dw->bgnd.count, i) { - Background* bg = &dw->bgnd.backgrounds[i]; - bg->tpagIndex = findTPAGIndexByOffset(ptrs, count, (uint32_t) bg->tpagIndex); - } - repeat(dw->font.count, i) { - Font* fnt = &dw->font.fonts[i]; - fnt->tpagIndex = findTPAGIndexByOffset(ptrs, count, (uint32_t) fnt->tpagIndex); - } -} - -static void parseTPAG(BinaryReader* reader, DataWin* dw) { - Tpag* t = &dw->tpag; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - t->count = count; - - if (count == 0) { free(ptrs); t->items = nullptr; return; } - - t->items = safeMalloc(count * sizeof(TexturePageItem)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - TexturePageItem* item = &t->items[i]; - item->sourceX = BinaryReader_readUint16(reader); - item->sourceY = BinaryReader_readUint16(reader); - item->sourceWidth = BinaryReader_readUint16(reader); - item->sourceHeight = BinaryReader_readUint16(reader); - item->targetX = BinaryReader_readUint16(reader); - item->targetY = BinaryReader_readUint16(reader); - item->targetWidth = BinaryReader_readUint16(reader); - item->targetHeight = BinaryReader_readUint16(reader); - item->boundingWidth = BinaryReader_readUint16(reader); - item->boundingHeight = BinaryReader_readUint16(reader); - item->texturePageId = BinaryReader_readInt16(reader); - } - - resolveAllTPAGReferences(dw, ptrs, count); - - free(ptrs); -} - -static void parseCODE(BinaryReader* reader, DataWin* dw, uint32_t chunkLength, size_t chunkDataStart) { - Code* c = &dw->code; - - if (chunkLength == 0) { - // YYC-compiled game, no bytecode - c->count = 0; - c->entries = nullptr; - return; - } - - // Standard pointer list at chunk start. Each entry has a relative offset - // (bytecodeRelAddr) that points to the actual bytecode blob elsewhere in the chunk. - - uint32_t codeCount; - uint32_t* codePtrs = readPointerTable(reader, &codeCount); - c->count = codeCount; - - if (codeCount == 0) { free(codePtrs); c->entries = nullptr; return; } - - c->entries = safeMalloc(codeCount * sizeof(CodeEntry)); - repeat(codeCount, i) { - BinaryReader_seek(reader, codePtrs[i]); - CodeEntry* entry = &c->entries[i]; - entry->name = readStringPtr(reader, dw); - entry->length = BinaryReader_readUint32(reader); - entry->localsCount = BinaryReader_readUint16(reader); - entry->argumentsCount = BinaryReader_readUint16(reader); - - // bytecodeRelAddr is relative to the position of this field - size_t relAddrFieldPos = BinaryReader_getPosition(reader); - int32_t bytecodeRelAddr = BinaryReader_readInt32(reader); - entry->bytecodeAbsoluteOffset = (uint32_t)((int64_t)relAddrFieldPos + bytecodeRelAddr); - - entry->offset = BinaryReader_readUint32(reader); - } - free(codePtrs); - - // Compute bytecode blob range and load into owned buffer. - // The bytecode blob starts at the minimum bytecodeAbsoluteOffset and - // extends to the end of the CODE chunk. - uint32_t blobStart = c->entries[0].bytecodeAbsoluteOffset; - repeat(codeCount, i) { - if (c->entries[i].bytecodeAbsoluteOffset < blobStart) { - blobStart = c->entries[i].bytecodeAbsoluteOffset; - } - } - size_t chunkEnd = chunkDataStart + chunkLength; - size_t blobSize = chunkEnd - blobStart; - - dw->bytecodeBufferBase = blobStart; - dw->bytecodeBuffer = BinaryReader_readBytesAt(reader, blobStart, blobSize); -} - -static void parseVARI(BinaryReader* reader, DataWin* dw, uint32_t chunkLength) { - Vari* v = &dw->vari; - - v->varCount1 = BinaryReader_readUint32(reader); - v->varCount2 = BinaryReader_readUint32(reader); - v->maxLocalVarCount = BinaryReader_readUint32(reader); - - // Variable entries are packed sequentially (no pointer table) - // Number of entries = (chunkLength - 12) / 20 - v->variableCount = (chunkLength - 12) / 20; - - if (v->variableCount > 0) { - v->variables = safeMalloc(v->variableCount * sizeof(Variable)); - repeat(v->variableCount, i) { - Variable* var = &v->variables[i]; - var->name = readStringPtr(reader, dw); - var->instanceType = BinaryReader_readInt32(reader); - var->varID = BinaryReader_readInt32(reader); - var->occurrences = BinaryReader_readUint32(reader); - var->firstAddress = BinaryReader_readUint32(reader); - } - } else { - v->variables = nullptr; - } -} - -static void parseFUNC(BinaryReader* reader, DataWin* dw) { - Func* f = &dw->func; - - // Part 1: Functions SimpleList - f->functionCount = BinaryReader_readUint32(reader); - if (f->functionCount > 0) { - f->functions = safeMalloc(f->functionCount * sizeof(Function)); - repeat(f->functionCount, i) { - f->functions[i].name = readStringPtr(reader, dw); - f->functions[i].occurrences = BinaryReader_readUint32(reader); - uint32_t rawAddr = BinaryReader_readUint32(reader); - // In GMS 2.3+, firstAddress points to the operand word (instruction + 4), not the instruction itself - if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && rawAddr != (uint32_t) -1) { - rawAddr -= 4; - } - f->functions[i].firstAddress = rawAddr; - } - } else { - f->functions = nullptr; - } - - // Part 2: Code Locals SimpleList - f->codeLocalsCount = BinaryReader_readUint32(reader); - if (f->codeLocalsCount > 0) { - f->codeLocals = safeMalloc(f->codeLocalsCount * sizeof(CodeLocals)); - repeat(f->codeLocalsCount, i) { - CodeLocals* cl = &f->codeLocals[i]; - cl->localVarCount = BinaryReader_readUint32(reader); - cl->name = readStringPtr(reader, dw); - - if (cl->localVarCount > 0) { - cl->locals = safeMalloc(cl->localVarCount * sizeof(LocalVar)); - repeat(cl->localVarCount, j) { - cl->locals[j].varID = BinaryReader_readUint32(reader); - cl->locals[j].name = readStringPtr(reader, dw); - } - } else { - cl->locals = nullptr; - } - } - } else { - f->codeLocals = nullptr; - } -} - -static void parseSTRG(BinaryReader* reader, DataWin* dw) { - Strg* s = &dw->strg; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - s->count = count; - - if (count == 0) { free(ptrs); s->strings = nullptr; return; } - - s->strings = safeMalloc(count * sizeof(const char*)); - repeat(count, i) { - // Pointer table points to the string's length prefix. - // The actual string content starts 4 bytes after. - s->strings[i] = (const char*)(dw->strgBuffer + (ptrs[i] + 4 - dw->strgBufferBase)); - } - free(ptrs); -} - -static void parseTXTR(BinaryReader* reader, DataWin* dw, size_t chunkEnd) { - Txtr* t = &dw->txtr; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - t->count = count; - - if (count == 0) { free(ptrs); t->textures = nullptr; return; } - - // Read metadata entries - bool hasGeneratedMips = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0); - - // Detect GMS 2022.3+ (TextureBlockSize field) and 2022.9+ (Width/Height/IndexInGroup fields) by probing the distance between the first two entry pointers. - // Only works when there are at least 2 textures (which is almost always the case for real games). - // Layouts: - // pre-2022.3: scaled+generatedMips+blobOffset = 12 bytes - // 2022.3+: ... + textureBlockSize = 16 bytes - // 2022.9+: ... + width + height + indexInGroup = 28 bytes - bool has2022_3 = DataWin_isVersionAtLeast(dw, 2022, 3, 0, 0); - bool has2022_9 = DataWin_isVersionAtLeast(dw, 2022, 9, 0, 0); - if (count >= 2 && hasGeneratedMips && !has2022_9) { - uint32_t diff = ptrs[1] - ptrs[0]; - if (diff == 28) { - DataWin_bumpVersionTo(dw, 2022, 9, 0, 0); - has2022_3 = true; - has2022_9 = true; - } else if (diff == 16 && !has2022_3) { - DataWin_bumpVersionTo(dw, 2022, 3, 0, 0); - has2022_3 = true; - } - } - - t->textures = safeMalloc(count * sizeof(Texture)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - t->textures[i].scaled = BinaryReader_readUint32(reader); - if (hasGeneratedMips) { - t->textures[i].generatedMips = BinaryReader_readUint32(reader); - } else { - t->textures[i].generatedMips = 0; - } - if (has2022_3) { - t->textures[i].textureBlockSize = BinaryReader_readUint32(reader); - } else { - t->textures[i].textureBlockSize = 0; - } - if (has2022_9) { - t->textures[i].textureWidth = BinaryReader_readInt32(reader); - t->textures[i].textureHeight = BinaryReader_readInt32(reader); - t->textures[i].indexInGroup = BinaryReader_readInt32(reader); - } else { - t->textures[i].textureWidth = 0; - t->textures[i].textureHeight = 0; - t->textures[i].indexInGroup = 0; - } - t->textures[i].blobOffset = BinaryReader_readUint32(reader); - t->textures[i].blobData = nullptr; - } - free(ptrs); - - // Compute blob sizes from successive offsets - repeat(count, i) { - if (t->textures[i].blobOffset == 0) { - t->textures[i].blobSize = 0; // external texture - continue; - } - if (count > i + 1 && t->textures[i + 1].blobOffset != 0) { - t->textures[i].blobSize = t->textures[i + 1].blobOffset - t->textures[i].blobOffset; - } else { - t->textures[i].blobSize = (uint32_t)(chunkEnd - t->textures[i].blobOffset); - } - } - - // Load blob data into owned buffers - repeat(count, i) { - if (t->textures[i].blobOffset == 0 || t->textures[i].blobSize == 0) continue; - t->textures[i].blobData = BinaryReader_readBytesAt(reader, t->textures[i].blobOffset, t->textures[i].blobSize); - } -} - -static void parseAUDO(BinaryReader* reader, DataWin* dw) { - Audo* a = &dw->audo; - - uint32_t count; - uint32_t* ptrs = readPointerTable(reader, &count); - a->count = count; - - if (count == 0) { free(ptrs); a->entries = nullptr; return; } - - a->entries = safeMalloc(count * sizeof(AudioEntry)); - repeat(count, i) { - BinaryReader_seek(reader, ptrs[i]); - a->entries[i].dataSize = BinaryReader_readUint32(reader); - a->entries[i].dataOffset = (uint32_t)BinaryReader_getPosition(reader); - // Load audio data into owned buffer - if (a->entries[i].dataSize > 0) { - a->entries[i].data = safeMalloc(a->entries[i].dataSize); - BinaryReader_readBytes(reader, a->entries[i].data, a->entries[i].dataSize); - } else { - a->entries[i].data = nullptr; - } - } - free(ptrs); -} - -// ===[ MAIN PARSE FUNCTION ]=== - -DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options) { - FILE* file = fopen(filePath, "rb"); - if (!file) { - fprintf(stderr, "Failed to open file: %s\n", filePath); - exit(1); - } - - // Use a large read buffer to reduce the number of physical reads - // This is critical for slow I/O devices like the PS2 CDVD drive, where each fread - // call would otherwise trigger a separate disc read of just a few sectors - setvbuf(file, nullptr, _IOFBF, 128 * 1024); - - fseek(file, 0, SEEK_END); - long fileSize = ftell(file); - fseek(file, 0, SEEK_SET); - - if (fileSize <= 0) { - fprintf(stderr, "Invalid file size: %ld\n", fileSize); - fclose(file); - exit(1); - } - - // Allocate and zero-initialize DataWin - DataWin* dw = safeCalloc(1, sizeof(DataWin)); - - BinaryReader reader = BinaryReader_create(file, (size_t) fileSize); - - // Validate FORM header - char formMagic[4]; - BinaryReader_readBytes(&reader, formMagic, 4); - if (memcmp(formMagic, "FORM", 4) != 0) { - fprintf(stderr, "Invalid file: expected FORM magic, got '%.4s'\n", formMagic); - free(dw); - fclose(file); - exit(1); - } - - uint32_t formLength = BinaryReader_readUint32(&reader); - (void) formLength; - - // Pass 1: Count total chunks and find STRG chunk offset. - // All other chunks reference strings from STRG, so it must be loaded first. - // We also check if the CODE chunk exists. - int totalChunks = 0; - bool codeExists = false; - BinaryReader_seek(&reader, 8); // reset to after FORM header - - while ((size_t) fileSize > BinaryReader_getPosition(&reader)) { - if (BinaryReader_getPosition(&reader) + 8 > (size_t) fileSize) break; - - char chunkName[5] = {0}; - BinaryReader_readBytes(&reader, chunkName, 4); - uint32_t chunkLength = BinaryReader_readUint32(&reader); - size_t chunkDataStart = BinaryReader_getPosition(&reader); - - if (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) { - dw->strgBufferBase = chunkDataStart; - dw->strgBuffer = BinaryReader_readBytesAt(&reader, chunkDataStart, chunkLength); - } - - if ((memcmp(chunkName, "CODE", 4) == 0) && chunkLength > 0) { - codeExists = true; - } - - // Bump detected version based on chunk presence, so later chunks can use the right version during parsing (parseOBJT needs to know we're >= 2.3 to probe for the GMS 2022.5+ Managed field). - if (memcmp(chunkName, "ACRV", 4) == 0 || memcmp(chunkName, "SEQN", 4) == 0 || memcmp(chunkName, "TAGS", 4) == 0) { - DataWin_bumpVersionTo(dw, 2, 3, 0, 0); - } else if (memcmp(chunkName, "FEDS", 4) == 0) { - DataWin_bumpVersionTo(dw, 2, 3, 6, 0); - } else if (memcmp(chunkName, "FEAT", 4) == 0) { - DataWin_bumpVersionTo(dw, 2022, 8, 0, 0); - } else if (memcmp(chunkName, "UILR", 4) == 0) { - DataWin_bumpVersionTo(dw, 2024, 13, 0, 0); - } else if (memcmp(chunkName, "PSEM", 4) == 0 || memcmp(chunkName, "PSYS", 4) == 0) { - DataWin_bumpVersionTo(dw, 2023, 2, 0, 0); - } - - BinaryReader_seek(&reader, chunkDataStart + chunkLength); - totalChunks++; - } - - if (!codeExists && options.parseCode) { - fprintf(stderr, "CODE chunk does not exist or is empty! This usually means you're loading a YYC game.\n"); - fclose(file); - exit(1); - } - - // Pass 2: Parse all chunks - // For each chunk that will be parsed, we bulk-read the entire chunk into memory first - // and then parse from the memory buffer. This dramatically reduces the number of physical - // reads on slow I/O devices like the PS2 CDVD drive. - BinaryReader_seek(&reader, 8); // skip past FORM header - int chunkIndex = 0; - while ((size_t) fileSize > BinaryReader_getPosition(&reader)) { - if (BinaryReader_getPosition(&reader) + 8 > (size_t) fileSize) break; - - char chunkName[5] = {0}; - BinaryReader_readBytes(&reader, chunkName, 4); - uint32_t chunkLength = BinaryReader_readUint32(&reader); - size_t chunkDataStart = BinaryReader_getPosition(&reader); - size_t chunkEnd = chunkDataStart + chunkLength; - - if (options.progressCallback) { - options.progressCallback(chunkName, chunkIndex, totalChunks, dw, options.progressCallbackUserData); - } - - // Determine if this chunk will be parsed (and thus needs bulk loading) - bool shouldParse = - (options.parseGen8 && memcmp(chunkName, "GEN8", 4) == 0) || - (options.parseOptn && memcmp(chunkName, "OPTN", 4) == 0) || - (options.parseLang && memcmp(chunkName, "LANG", 4) == 0) || - (options.parseExtn && memcmp(chunkName, "EXTN", 4) == 0) || - (options.parseSond && memcmp(chunkName, "SOND", 4) == 0) || - (options.parseAgrp && memcmp(chunkName, "AGRP", 4) == 0) || - (options.parseSprt && memcmp(chunkName, "SPRT", 4) == 0) || - (options.parseBgnd && memcmp(chunkName, "BGND", 4) == 0) || - (options.parsePath && memcmp(chunkName, "PATH", 4) == 0) || - (options.parseScpt && memcmp(chunkName, "SCPT", 4) == 0) || - (options.parseGlob && memcmp(chunkName, "GLOB", 4) == 0) || - (options.parseShdr && memcmp(chunkName, "SHDR", 4) == 0) || - (options.parseFont && memcmp(chunkName, "FONT", 4) == 0) || - (options.parseTmln && memcmp(chunkName, "TMLN", 4) == 0) || - (options.parseObjt && memcmp(chunkName, "OBJT", 4) == 0) || - (options.parseRoom && memcmp(chunkName, "ROOM", 4) == 0) || - (options.parseTpag && memcmp(chunkName, "TPAG", 4) == 0) || - (options.parseCode && memcmp(chunkName, "CODE", 4) == 0) || - (options.parseVari && memcmp(chunkName, "VARI", 4) == 0) || - (options.parseFunc && memcmp(chunkName, "FUNC", 4) == 0) || - (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) || - (options.parseTxtr && memcmp(chunkName, "TXTR", 4) == 0) || - (options.parseAudo && memcmp(chunkName, "AUDO", 4) == 0); - - // Bulk-read the chunk data into memory for fast parsing - uint8_t* chunkBuffer = nullptr; - if (shouldParse && chunkLength > 0) { - chunkBuffer = safeMalloc(chunkLength); - size_t read = fread(chunkBuffer, 1, chunkLength, reader.file); - if (read != chunkLength) { - fprintf(stderr, "DataWin: short read on chunk %.4s (expected %u, got %zu)\n", chunkName, chunkLength, read); - exit(1); - } - BinaryReader_setBuffer(&reader, chunkBuffer, chunkDataStart, chunkLength); - } - - if (options.parseGen8 && memcmp(chunkName, "GEN8", 4) == 0) { - parseGEN8(&reader, dw); - } else if (options.parseOptn && memcmp(chunkName, "OPTN", 4) == 0) { - parseOPTN(&reader, dw); - } else if (options.parseLang && memcmp(chunkName, "LANG", 4) == 0) { - parseLANG(&reader, dw); - } else if (options.parseExtn && memcmp(chunkName, "EXTN", 4) == 0) { - parseEXTN(&reader, dw); - } else if (options.parseSond && memcmp(chunkName, "SOND", 4) == 0) { - parseSOND(&reader, dw); - } else if (options.parseAgrp && memcmp(chunkName, "AGRP", 4) == 0) { - parseAGRP(&reader, dw); - } else if (options.parseSprt && memcmp(chunkName, "SPRT", 4) == 0) { - parseSPRT(&reader, dw, options.skipLoadingPreciseMasksForNonPreciseSprites); - } else if (options.parseBgnd && memcmp(chunkName, "BGND", 4) == 0) { - parseBGND(&reader, dw); - } else if (options.parsePath && memcmp(chunkName, "PATH", 4) == 0) { - parsePATH(&reader, dw); - } else if (options.parseScpt && memcmp(chunkName, "SCPT", 4) == 0) { - parseSCPT(&reader, dw); - } else if (options.parseGlob && memcmp(chunkName, "GLOB", 4) == 0) { - parseGLOB(&reader, dw); - } else if (options.parseShdr && memcmp(chunkName, "SHDR", 4) == 0) { - parseSHDR(&reader, dw); - } else if (options.parseFont && memcmp(chunkName, "FONT", 4) == 0) { - parseFONT(&reader, dw); - } else if (options.parseTmln && memcmp(chunkName, "TMLN", 4) == 0) { - parseTMLN(&reader, dw); - } else if (options.parseObjt && memcmp(chunkName, "OBJT", 4) == 0) { - parseOBJT(&reader, dw); - } else if (options.parseRoom && memcmp(chunkName, "ROOM", 4) == 0) { - parseROOM(&reader, dw, options.lazyLoadRooms, options.eagerlyLoadedRooms); - } else if (memcmp(chunkName, "DAFL", 4) == 0) { - // Empty chunk, nothing to parse - } else if (memcmp(chunkName, "EMBI", 4) == 0) { - // Embedded Images chunk - } else if (memcmp(chunkName, "TGIN", 4) == 0) { - // Texture Group Info chunk (bytecodeVersion >= 17) - } else if (memcmp(chunkName, "ACRV", 4) == 0) { - // Animation Curves chunk (GMS 2.3+) - DataWin_bumpVersionTo(dw, 2, 3, 0, 0); - } else if (memcmp(chunkName, "SEQN", 4) == 0) { - // Sequences chunk (GMS 2.3+) - DataWin_bumpVersionTo(dw, 2, 3, 0, 0); - } else if (memcmp(chunkName, "TAGS", 4) == 0) { - // Tags chunk (GMS 2.3+) - DataWin_bumpVersionTo(dw, 2, 3, 0, 0); - } else if (memcmp(chunkName, "FEDS", 4) == 0) { - // Filter Effects Data chunk (GMS 2.3.6+) - DataWin_bumpVersionTo(dw, 2, 3, 6, 0); - } else if (options.parseTpag && memcmp(chunkName, "TPAG", 4) == 0) { - parseTPAG(&reader, dw); - } else if (options.parseCode && memcmp(chunkName, "CODE", 4) == 0) { - parseCODE(&reader, dw, chunkLength, chunkDataStart); - } else if (options.parseVari && memcmp(chunkName, "VARI", 4) == 0) { - parseVARI(&reader, dw, chunkLength); - } else if (options.parseFunc && memcmp(chunkName, "FUNC", 4) == 0) { - parseFUNC(&reader, dw); - } else if (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) { - parseSTRG(&reader, dw); - } else if (options.parseTxtr && memcmp(chunkName, "TXTR", 4) == 0) { - parseTXTR(&reader, dw, chunkEnd); - } else if (options.parseAudo && memcmp(chunkName, "AUDO", 4) == 0) { - parseAUDO(&reader, dw); - } else { - printf("Unknown chunk: %.4s (length %u at offset 0x%zX)\n", chunkName, chunkLength, chunkDataStart - 8); - } - - // Free the chunk buffer and revert to FILE*-based reads for the next header - if (chunkBuffer != nullptr) { - BinaryReader_clearBuffer(&reader); - free(chunkBuffer); - } - - // Seek to chunk end (skip any unread data or trailing padding) - fseek(reader.file, (long) chunkEnd, SEEK_SET); - chunkIndex++; - } - - // GMS2: apply default FPS to rooms with speed=0 - if (dw->gen8.gms2FPS > 0) { - repeat(dw->room.count, i) { - if (dw->room.rooms[i].speed == 0) { - dw->room.rooms[i].speed = (uint32_t) dw->gen8.gms2FPS; - } - } - } - - // If lazy-loading rooms, keep the file handle open for DataWin_loadRoomPayload, otherwise close it now - dw->lazyLoadRooms = options.lazyLoadRooms; - if (options.lazyLoadRooms) { - dw->lazyLoadFile = file; - dw->lazyLoadFilePath = safeStrdup(filePath); - } else { - dw->lazyLoadFile = nullptr; - dw->lazyLoadFilePath = nullptr; - fclose(file); - } - - return dw; -} - -// ===[ FREE ]=== - -void DataWin_free(DataWin* dw) { - if (!dw) return; - - // GEN8 - free(dw->gen8.roomOrder); - - // OPTN - free(dw->optn.constants); - - // LANG - free(dw->lang.entryIds); - if (dw->lang.languages) { - repeat(dw->lang.languageCount, i) { - free(dw->lang.languages[i].entries); - } - free(dw->lang.languages); - } - - // EXTN - if (dw->extn.extensions) { - repeat(dw->extn.count, i) { - Extension* ext = &dw->extn.extensions[i]; - if (ext->files) { - repeat(ext->fileCount, j) { - ExtensionFile* file = &ext->files[j]; - if (file->functions) { - repeat(file->functionCount, k) { - free(file->functions[k].arguments); - } - free(file->functions); - } - } - free(ext->files); - } - } - free(dw->extn.extensions); - } - - // SOND - free(dw->sond.sounds); - - // AGRP - free(dw->agrp.audioGroups); - - // SPRT - if (dw->sprt.sprites) { - repeat(dw->sprt.count, i) { - free(dw->sprt.sprites[i].tpagIndices); - if (dw->sprt.sprites[i].masks != nullptr) { - repeat(dw->sprt.sprites[i].maskCount, j) { - free(dw->sprt.sprites[i].masks[j]); - } - free(dw->sprt.sprites[i].masks); - } - // Runtime-allocated sprites (indices >= parsedCount) own their synthesized name - if (i >= dw->sprt.parsedCount) free((char*) dw->sprt.sprites[i].name); - } - free(dw->sprt.sprites); - } - - - // BGND - if (dw->bgnd.backgrounds) { - repeat(dw->bgnd.count, i) { - free(dw->bgnd.backgrounds[i].gms2TileIds); - } - } - free(dw->bgnd.backgrounds); - - // PATH - if (dw->path.paths) { - repeat(dw->path.count, i) { - free(dw->path.paths[i].points); - free(dw->path.paths[i].internalPoints); - } - free(dw->path.paths); - } - - // SCPT - free(dw->scpt.scripts); - - // GLOB - free(dw->glob.codeIds); - - // SHDR - if (dw->shdr.shaders) { - repeat(dw->shdr.count, i) { - free(dw->shdr.shaders[i].vertexAttributes); - } - free(dw->shdr.shaders); - } - - // FONT - if (dw->font.fonts) { - repeat(dw->font.count, i) { - Font* font = &dw->font.fonts[i]; - if (font->glyphs) { - repeat(font->glyphCount, j) { - free(font->glyphs[j].kerning); - } - free(font->glyphs); - } - } - free(dw->font.fonts); - } - - // TMLN - if (dw->tmln.timelines) { - repeat(dw->tmln.count, i) { - Timeline* tl = &dw->tmln.timelines[i]; - if (tl->moments) { - repeat(tl->momentCount, j) { - free(tl->moments[j].actions); - } - free(tl->moments); - } - } - free(dw->tmln.timelines); - } - - // OBJT - if (dw->objt.objects) { - repeat(dw->objt.count, i) { - GameObject* obj = &dw->objt.objects[i]; - free(obj->physicsVertices); - repeat(OBJT_EVENT_TYPE_COUNT, e) { - ObjectEventList* list = &obj->eventLists[e]; - if (list->events) { - repeat(list->eventCount, j) { - free(list->events[j].actions); - } - free(list->events); - } - } - } - free(dw->objt.objects); - } - - // ROOM - if (dw->room.rooms) { - repeat(dw->room.count, i) { - DataWin_freeRoomPayload(&dw->room.rooms[i]); - } - free(dw->room.rooms); - } - - // TPAG - free(dw->tpag.items); - - // CODE - free(dw->code.entries); - - // VARI - free(dw->vari.variables); - - // FUNC - free(dw->func.functions); - if (dw->func.codeLocals) { - repeat(dw->func.codeLocalsCount, i) { - free(dw->func.codeLocals[i].locals); - } - free(dw->func.codeLocals); - } - - // STRG - free(dw->strg.strings); - - // TXTR - if (dw->txtr.textures) { - repeat(dw->txtr.count, i) { - free(dw->txtr.textures[i].blobData); - } - free(dw->txtr.textures); - } - - // AUDO - if (dw->audo.entries) { - repeat(dw->audo.count, i) { - free(dw->audo.entries[i].data); - } - free(dw->audo.entries); - } - - // Owned buffers - free(dw->strgBuffer); - free(dw->bytecodeBuffer); - - // Close the lazy-load file handle (only open when lazyLoadRooms was enabled) - if (dw->lazyLoadFile != nullptr) { - fclose(dw->lazyLoadFile); - dw->lazyLoadFile = nullptr; - } - free(dw->lazyLoadFilePath); - - free(dw); -} - -// ===[ Lazy Room Payload ]=== - -void DataWin_freeRoomPayload(Room* room) { - requireNotNull(room); - free(room->backgrounds); - room->backgrounds = nullptr; - free(room->views); - room->views = nullptr; - free(room->gameObjects); - room->gameObjects = nullptr; - room->gameObjectCount = 0; - free(room->tiles); - room->tiles = nullptr; - room->tileCount = 0; - if (room->layerCount != 0 && room->layers != nullptr) { - repeat(room->layerCount, j) { - RoomLayer* layer = &room->layers[j]; - if (layer->assetsData) { - free(layer->assetsData->legacyTiles); - free(layer->assetsData->sprites); - free(layer->assetsData); - } - if (layer->backgroundData) free(layer->backgroundData); - if (layer->instancesData) { - free(layer->instancesData->instanceIds); - free(layer->instancesData); - } - if (layer->tilesData) { - free(layer->tilesData->tileData); - free(layer->tilesData); - } - } - } - free(room->layers); - room->layers = nullptr; - room->layerCount = 0; - room->payloadLoaded = false; -} - -void DataWin_loadRoomPayload(DataWin* dw, int32_t roomIndex) { - require(roomIndex >= 0 && dw->room.count > (uint32_t) roomIndex); - Room* room = &dw->room.rooms[roomIndex]; - if (room->payloadLoaded) return; - requireMessage(dw->lazyLoadFile != nullptr, "DataWin_loadRoomPayload called without an open lazy-load FILE*"); - - FILE* f = dw->lazyLoadFile; - // Find file size at lazy-load time (only needed for BinaryReader bounds checking). - long savedPos = ftell(f); - fseek(f, 0, SEEK_END); - size_t fileSize = (size_t) ftell(f); - fseek(f, savedPos, SEEK_SET); - - BinaryReader lazyReader = BinaryReader_create(f, fileSize); - readRoomPayload(&lazyReader, dw, room); -} - -// ===[ Dynamic Sprite Slot Allocation ]=== - -uint32_t DataWin_allocSpriteSlot(DataWin* dw, uint32_t startIndex) { - uint32_t newIndex; - for (uint32_t i = startIndex; dw->sprt.count > i; i++) { - if (dw->sprt.sprites[i].textureCount == 0) { - newIndex = i; - goto assignName; - } - } - newIndex = dw->sprt.count; - dw->sprt.count++; - dw->sprt.sprites = safeRealloc(dw->sprt.sprites, dw->sprt.count * sizeof(Sprite)); - memset(&dw->sprt.sprites[newIndex], 0, sizeof(Sprite)); -assignName: - // Match the native runner: set a "__newsprite" name so asset_get_index can find it. - // A reused slot preserves its name across glDeleteSprite's memset, so we only strdup when the slot is freshly appended (name is still NULL). - if (!dw->sprt.sprites[newIndex].name) { - char buf[32]; - snprintf(buf, sizeof(buf), "__newsprite%u", newIndex); - dw->sprt.sprites[newIndex].name = strdup(buf); - } - return newIndex; -} - -// ===[ Version Detection ]=== - -bool DataWin_isVersionAtLeast(const DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build) { - const DetectedFormat* f = &dw->detectedFormat; - if (f->major != major) return f->major > major; - if (f->minor != minor) return f->minor > minor; - if (f->release != release) return f->release > release; - return f->build >= build; -} - -void DataWin_bumpVersionTo(DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build) { - if (DataWin_isVersionAtLeast(dw, major, minor, release, build)) return; - dw->detectedFormat.major = major; - dw->detectedFormat.minor = minor; - dw->detectedFormat.release = release; - dw->detectedFormat.build = build; -} +#include "data_win.h" +#include "binary_reader.h" + +#include +#include +#include +#include +#include + +#include "stb_ds.h" +#include "utils.h" + +// ===[ HELPERS ]=== + +// Reads a uint32 absolute file offset, resolves it into the pre-loaded STRG buffer, +// and returns a pointer to the null-terminated string content at that offset. +static const char* readStringPtr(BinaryReader* reader, DataWin* dw) { + uint32_t offset = BinaryReader_readUint32(reader); + if (offset == 0) return nullptr; + return (const char*) (dw->strgBuffer + (offset - dw->strgBufferBase)); +} + +// Reads a pointer list header: count + absolute-offset pointers. +// Caller must free the returned array. +static uint32_t* readPointerTable(BinaryReader* reader, uint32_t* outCount) { + *outCount = BinaryReader_readUint32(reader); + if (*outCount == 0) return nullptr; + uint32_t* ptrs = safeMalloc(*outCount * sizeof(uint32_t)); + repeat(*outCount, i) { + ptrs[i] = BinaryReader_readUint32(reader); + } + return ptrs; +} + +// Reads a PointerList of EventAction entries. Used by TMLN and OBJT. +static EventAction* readEventActions(BinaryReader* reader, DataWin* dw, uint32_t* outCount) { + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + *outCount = count; + if (count == 0) { free(ptrs); return nullptr; } + + EventAction* actions = safeMalloc(count * sizeof(EventAction)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + actions[i].libID = BinaryReader_readUint32(reader); + actions[i].id = BinaryReader_readUint32(reader); + actions[i].kind = BinaryReader_readUint32(reader); + actions[i].useRelative = BinaryReader_readBool32(reader); + actions[i].isQuestion = BinaryReader_readBool32(reader); + actions[i].useApplyTo = BinaryReader_readBool32(reader); + actions[i].exeType = BinaryReader_readUint32(reader); + actions[i].actionName = readStringPtr(reader, dw); + actions[i].codeId = BinaryReader_readInt32(reader); + actions[i].argumentCount = BinaryReader_readUint32(reader); + actions[i].who = BinaryReader_readInt32(reader); + actions[i].relative = BinaryReader_readBool32(reader); + actions[i].isNot = BinaryReader_readBool32(reader); + actions[i].unknownAlwaysZero = BinaryReader_readUint32(reader); + } + free(ptrs); + return actions; +} + +// ===[ PATH INTERNAL COMPUTATION ]=== +// Matches HTML5 yyPath.js algorithm exactly. + +// Dynamic array of InternalPathPoints for building during computation +static InternalPathPoint* tempIntPoints = nullptr; +static uint32_t tempIntPointCount = 0; + +static void addInternalPoint(float x, float y, float speed) { + InternalPathPoint pt = { .x = x, .y = y, .speed = speed, .l = 0.0 }; + arrput(tempIntPoints, pt); + tempIntPointCount++; +} + +// Recursive midpoint subdivision for smooth curves (yyPath.js:225-242) +static void handlePiece(int depth, float x1, float y1, float s1, float x2, float y2, float s2, float x3, float y3, float s3) { + if (depth == 0) return; + + float mx = (x1 + x2 + x2 + x3) / 4.0f; + float my = (y1 + y2 + y2 + y3) / 4.0f; + float ms = (s1 + s2 + s2 + s3) / 4.0f; + + if ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) > 16.0f) { + handlePiece(depth - 1, x1, y1, s1, (x2 + x1) / 2.0f, (y2 + y1) / 2.0f, (s2 + s1) / 2.0f, mx, my, ms); + } + + addInternalPoint(mx, my, ms); + + if ((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3) > 16.0f) { + handlePiece(depth - 1, mx, my, ms, (x3 + x2) / 2.0f, (y3 + y2) / 2.0f, (s3 + s2) / 2.0f, x3, y3, s3); + } +} + +void GamePath_computeInternal(GamePath* path) { + // Reset temp state + arrfree(tempIntPoints); + tempIntPoints = nullptr; + tempIntPointCount = 0; + + free(path->internalPoints); + path->internalPoints = nullptr; + path->internalPointCount = 0; + path->length = 0.0; + + if (path->pointCount == 0) + return; + + if (path->isSmooth) { + // ComputeCurved (yyPath.js:254-292) + if (!path->isClosed) { + addInternalPoint(path->points[0].x, path->points[0].y, path->points[0].speed); + } + + int n; + if (path->isClosed) { + n = (int) path->pointCount - 1; + } else { + n = (int) path->pointCount - 3; + } + + repeat(n + 1, i) { + PathPoint* p1 = &path->points[i % path->pointCount]; + PathPoint* p2 = &path->points[(i + 1) % path->pointCount]; + PathPoint* p3 = &path->points[(i + 2) % path->pointCount]; + handlePiece((int) path->precision, + (p1->x + p2->x) / 2.0f, (p1->y + p2->y) / 2.0f, (p1->speed + p2->speed) / 2.0f, + p2->x, p2->y, p2->speed, + (p2->x + p3->x) / 2.0f, (p2->y + p3->y) / 2.0f, (p2->speed + p3->speed) / 2.0f); + } + + if (!path->isClosed) { + PathPoint* last = &path->points[path->pointCount - 1]; + addInternalPoint(last->x, last->y, last->speed); + } else { + // Closed smooth: append the first internal point again + addInternalPoint(tempIntPoints[0].x, tempIntPoints[0].y, tempIntPoints[0].speed); + } + } else { + // ComputeLinear (yyPath.js:192-204) + repeat(path->pointCount, i) { + addInternalPoint(path->points[i].x, path->points[i].y, path->points[i].speed); + } + if (path->isClosed) { + addInternalPoint(path->points[0].x, path->points[0].y, path->points[0].speed); + } + } + + // ComputeLength (yyPath.js:150-160) + path->internalPointCount = tempIntPointCount; + path->internalPoints = safeMalloc(tempIntPointCount * sizeof(InternalPathPoint)); + memcpy(path->internalPoints, tempIntPoints, tempIntPointCount * sizeof(InternalPathPoint)); + arrfree(tempIntPoints); + tempIntPoints = nullptr; + tempIntPointCount = 0; + + path->length = 0.0; + if (path->internalPointCount > 0) { + path->internalPoints[0].l = 0.0; + repeat(path->internalPointCount - 1, j) { + uint32_t i = j + 1; + float dx = path->internalPoints[i].x - path->internalPoints[i - 1].x; + float dy = path->internalPoints[i].y - path->internalPoints[i - 1].y; + path->length += sqrtf(dx * dx + dy * dy); + path->internalPoints[i].l = path->length; + } + } +} + +// Get interpolated position at t in [0,1] (yyPath.js:362-409) +PathPositionResult GamePath_getPosition(GamePath* path, float t) { + PathPositionResult result = { .x = 0.0f, .y = 0.0f, .speed = 0.0f }; + + if (path->internalPointCount == 0) return result; + + if (path->internalPointCount == 1 || path->length == 0.0f || 0.0f >= t) { + result.x = path->internalPoints[0].x; + result.y = path->internalPoints[0].y; + result.speed = path->internalPoints[0].speed; + return result; + } + + if (t >= 1.0f) { + InternalPathPoint* last = &path->internalPoints[path->internalPointCount - 1]; + result.x = last->x; + result.y = last->y; + result.speed = last->speed; + return result; + } + + // Get the right interval via linear scan + float l = path->length * t; + uint32_t pos = 0; + while (path->internalPointCount - 2 > pos && l >= path->internalPoints[pos + 1].l) { + pos++; + } + + InternalPathPoint* node = &path->internalPoints[pos]; + float lRem = l - node->l; + float w = path->internalPoints[pos + 1].l - node->l; + + if (w != 0.0f) { + InternalPathPoint* next = &path->internalPoints[pos + 1]; + result.x = node->x + lRem * (next->x - node->x) / w; + result.y = node->y + lRem * (next->y - node->y) / w; + result.speed = node->speed + lRem * (next->speed - node->speed) / w; + } else { + result.x = node->x; + result.y = node->y; + result.speed = node->speed; + } + + return result; +} + +// ===[ CHUNK PARSERS ]=== + +static void parseGEN8(BinaryReader* reader, DataWin* dw) { + Gen8* g = &dw->gen8; + g->isDebuggerDisabled = BinaryReader_readUint8(reader); + g->bytecodeVersion = BinaryReader_readUint8(reader); + BinaryReader_skip(reader, 2); // padding + g->fileName = readStringPtr(reader, dw); + g->config = readStringPtr(reader, dw); + g->lastObj = BinaryReader_readUint32(reader); + g->lastTile = BinaryReader_readUint32(reader); + g->gameID = BinaryReader_readUint32(reader); + BinaryReader_readBytes(reader, g->directPlayGuid, 16); + g->name = readStringPtr(reader, dw); + g->major = BinaryReader_readUint32(reader); + g->minor = BinaryReader_readUint32(reader); + g->release = BinaryReader_readUint32(reader); + g->build = BinaryReader_readUint32(reader); + g->defaultWindowWidth = BinaryReader_readUint32(reader); + g->defaultWindowHeight = BinaryReader_readUint32(reader); + g->info = BinaryReader_readUint32(reader); + g->licenseCRC32 = BinaryReader_readUint32(reader); + BinaryReader_readBytes(reader, g->licenseMD5, 16); + g->timestamp = BinaryReader_readUint64(reader); + g->displayName = readStringPtr(reader, dw); + g->activeTargets = BinaryReader_readUint64(reader); + g->functionClassifications = BinaryReader_readUint64(reader); + g->steamAppID = BinaryReader_readInt32(reader); + if (g->bytecodeVersion >= 14) { + g->debuggerPort = BinaryReader_readUint32(reader); + } + + // Room order SimpleList + g->roomOrderCount = BinaryReader_readUint32(reader); + if (g->roomOrderCount > 0) { + g->roomOrder = safeMalloc(g->roomOrderCount * sizeof(int32_t)); + repeat(g->roomOrderCount, i) { + g->roomOrder[i] = BinaryReader_readInt32(reader); + } + } else { + g->roomOrder = nullptr; + } + + if (g->major >= 2) { + BinaryReader_skip(reader, 8); // firstRandom (int64) + BinaryReader_skip(reader, 8*4); // 4 Random Entries (one int64 or two int32) + + g->gms2FPS = BinaryReader_readFloat32(reader); + BinaryReader_skip(reader, 4); // AllowStatistics (bool32) + BinaryReader_skip(reader, 16); // GameGUID (16 Bytes, unknown it's use) + } + + // Seed the detected version from GEN8. + // Later chunk parsers may bump these upward when they identify newer-format features, because since GM:S 2 the value in the GEN8 chunk is not accurate. + DataWin_bumpVersionTo(dw, g->major, g->minor, g->release, g->build); +} + +static void parseOPTN(BinaryReader* reader, DataWin* dw) { + Optn* o = &dw->optn; + + int32_t marker = BinaryReader_readInt32(reader); + if (marker != (int32_t)0x80000000) { + fprintf(stderr, "OPTN: expected new format marker 0x80000000, got 0x%08X\n", (uint32_t)marker); + exit(1); + } + + int32_t shaderExtVersion = BinaryReader_readInt32(reader); + (void)shaderExtVersion; // always 2 + + o->info = BinaryReader_readUint64(reader); + o->scale = BinaryReader_readInt32(reader); + o->windowColor = BinaryReader_readUint32(reader); + o->colorDepth = BinaryReader_readUint32(reader); + o->resolution = BinaryReader_readUint32(reader); + o->frequency = BinaryReader_readUint32(reader); + o->vertexSync = BinaryReader_readUint32(reader); + o->priority = BinaryReader_readUint32(reader); + o->backImage = BinaryReader_readUint32(reader); + o->frontImage = BinaryReader_readUint32(reader); + o->loadImage = BinaryReader_readUint32(reader); + o->loadAlpha = BinaryReader_readUint32(reader); + + // Constants SimpleList + o->constantCount = BinaryReader_readUint32(reader); + if (o->constantCount > 0) { + o->constants = safeMalloc(o->constantCount * sizeof(OptnConstant)); + repeat(o->constantCount, i) { + o->constants[i].name = readStringPtr(reader, dw); + o->constants[i].value = readStringPtr(reader, dw); + } + } else { + o->constants = nullptr; + } +} + +static void parseLANG(BinaryReader* reader, DataWin* dw) { + Lang* l = &dw->lang; + l->unknown1 = BinaryReader_readUint32(reader); + l->languageCount = BinaryReader_readUint32(reader); + l->entryCount = BinaryReader_readUint32(reader); + + // Entry IDs + if (l->entryCount > 0) { + l->entryIds = safeMalloc(l->entryCount * sizeof(const char*)); + repeat(l->entryCount, i) { + l->entryIds[i] = readStringPtr(reader, dw); + } + } else { + l->entryIds = nullptr; + } + + // Languages + if (l->languageCount > 0) { + l->languages = safeMalloc(l->languageCount * sizeof(Language)); + repeat(l->languageCount, i) { + l->languages[i].name = readStringPtr(reader, dw); + l->languages[i].region = readStringPtr(reader, dw); + l->languages[i].entryCount = l->entryCount; + if (l->entryCount > 0) { + l->languages[i].entries = safeMalloc(l->entryCount * sizeof(const char*)); + repeat(l->entryCount, j) { + l->languages[i].entries[j] = readStringPtr(reader, dw); + } + } else { + l->languages[i].entries = nullptr; + } + } + } else { + l->languages = nullptr; + } +} + +static void parseEXTN(BinaryReader* reader, DataWin* dw) { + // TODO: Update EXTN parser because it is broken for newer GM:S 2 versions + Extn* e = &dw->extn; + + uint32_t extCount; + uint32_t* extPtrs = readPointerTable(reader, &extCount); + e->count = extCount; + + if (extCount == 0) { free(extPtrs); e->extensions = nullptr; return; } + + e->extensions = safeMalloc(extCount * sizeof(Extension)); + repeat(extCount, i) { + BinaryReader_seek(reader, extPtrs[i]); + Extension* ext = &e->extensions[i]; + ext->folderName = readStringPtr(reader, dw); + ext->name = readStringPtr(reader, dw); + ext->className = readStringPtr(reader, dw); + + // Files PointerList + uint32_t fileCount; + uint32_t* filePtrs = readPointerTable(reader, &fileCount); + ext->fileCount = fileCount; + + if (fileCount > 0) { + ext->files = safeMalloc(fileCount * sizeof(ExtensionFile)); + repeat(fileCount, j) { + BinaryReader_seek(reader, filePtrs[j]); + ExtensionFile* file = &ext->files[j]; + file->filename = readStringPtr(reader, dw); + file->cleanupScript = readStringPtr(reader, dw); + file->initScript = readStringPtr(reader, dw); + file->kind = BinaryReader_readUint32(reader); + + // Functions PointerList + uint32_t funcCount; + uint32_t* funcPtrs = readPointerTable(reader, &funcCount); + file->functionCount = funcCount; + + if (funcCount > 0) { + file->functions = safeMalloc(funcCount * sizeof(ExtensionFunction)); + repeat(funcCount, k) { + BinaryReader_seek(reader, funcPtrs[k]); + ExtensionFunction* func = &file->functions[k]; + func->name = readStringPtr(reader, dw); + func->id = BinaryReader_readUint32(reader); + func->kind = BinaryReader_readUint32(reader); + func->retType = BinaryReader_readUint32(reader); + func->extName = readStringPtr(reader, dw); + + // Arguments SimpleList + func->argumentCount = BinaryReader_readUint32(reader); + if (func->argumentCount > 0) { + func->arguments = safeMalloc(func->argumentCount * sizeof(uint32_t)); + repeat(func->argumentCount, a) { + func->arguments[a] = BinaryReader_readUint32(reader); + } + } else { + func->arguments = nullptr; + } + } + } else { + file->functions = nullptr; + } + free(funcPtrs); + } + } else { + ext->files = nullptr; + } + free(filePtrs); + } + free(extPtrs); + + // Product ID data (16 bytes per extension, bytecodeVersion >= 14) + // Skipped -- we seek to chunkEnd after parsing +} + +static void parseSOND(BinaryReader* reader, DataWin* dw) { + Sond* s = &dw->sond; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + s->count = count; + + if (count == 0) { free(ptrs); s->sounds = nullptr; return; } + + s->sounds = safeMalloc(count * sizeof(Sound)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + Sound* snd = &s->sounds[i]; + snd->name = readStringPtr(reader, dw); + snd->flags = BinaryReader_readUint32(reader); + snd->type = readStringPtr(reader, dw); + snd->file = readStringPtr(reader, dw); + snd->effects = BinaryReader_readUint32(reader); + snd->volume = BinaryReader_readFloat32(reader); + snd->pitch = BinaryReader_readFloat32(reader); + + // AudioGroup or preload field at offset +28 + // For GMS 1.4.x (bytecodeVersion >= 14) with Regular flag: resource_id + if ((snd->flags & 0x64) == 0x64) { + snd->audioGroup = BinaryReader_readInt32(reader); + } else { + int32_t preload = BinaryReader_readInt32(reader); + (void)preload; + snd->audioGroup = 0; // default audio group + } + + snd->audioFile = BinaryReader_readInt32(reader); + } + free(ptrs); +} + +static void parseAGRP(BinaryReader* reader, DataWin* dw) { + Agrp* a = &dw->agrp; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + a->count = count; + + if (count == 0) { free(ptrs); a->audioGroups = nullptr; return; } + + a->audioGroups = safeMalloc(count * sizeof(AudioGroup)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + a->audioGroups[i].name = readStringPtr(reader, dw); + } + free(ptrs); +} + +static void parseSPRT(BinaryReader* reader, DataWin* dw, bool skipLoadingPreciseMasksForNonPreciseSprites) { + Sprt* s = &dw->sprt; + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + s->count = count; + s->parsedCount = count; + + if (count == 0) { free(ptrs); s->sprites = nullptr; return; } + + s->sprites = safeCalloc(count, sizeof(Sprite)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + Sprite* spr = &s->sprites[i]; + spr->name = readStringPtr(reader, dw); + spr->width = BinaryReader_readUint32(reader); + spr->height = BinaryReader_readUint32(reader); + spr->marginLeft = BinaryReader_readInt32(reader); + spr->marginRight = BinaryReader_readInt32(reader); + spr->marginBottom = BinaryReader_readInt32(reader); + spr->marginTop = BinaryReader_readInt32(reader); + spr->transparent = BinaryReader_readBool32(reader); + spr->smooth = BinaryReader_readBool32(reader); + spr->preload = BinaryReader_readBool32(reader); + spr->bboxMode = BinaryReader_readUint32(reader); + spr->sepMasks = BinaryReader_readUint32(reader); + spr->originX = BinaryReader_readInt32(reader); + spr->originY = BinaryReader_readInt32(reader); + + // Detect special type vs normal: peek next int32 + int32_t check = BinaryReader_readInt32(reader); + uint32_t nineSliceOffset = 0; + if (check == -1) { + spr->specialType = true; + spr->sVersion = BinaryReader_readUint32(reader); + spr->sSpriteType = BinaryReader_readUint32(reader); + if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) { + spr->gms2PlaybackSpeed = BinaryReader_readFloat32(reader); + spr->gms2PlaybackSpeedType = BinaryReader_readUint32(reader); + if (spr->sVersion >= 2) { + BinaryReader_skip(reader, 4); //sequenceOffset; + if (spr->sVersion >= 3) { + nineSliceOffset = BinaryReader_readUint32(reader); + } + } + check = BinaryReader_readUint32(reader); + } + } + + // 'check' is the texture count (start of SimpleList) + spr->textureCount = (uint32_t)check; + if (spr->textureCount > 0) { + // Temporarily store the absolute file offsets here; parseTPAG resolves them in-place to TPAG indices once the TPAG table is known. + spr->tpagIndices = safeMalloc(spr->textureCount * sizeof(int32_t)); + repeat(spr->textureCount, j) { + spr->tpagIndices[j] = (int32_t) BinaryReader_readUint32(reader); + } + } else { + spr->tpagIndices = nullptr; + } + + // Collision mask data + // sepMasks: 0 = axis-aligned rect (no mask data stored in some cases) + // 1 = precise per-frame masks + // 2 = rotated rect (no mask data) + // Mask format: each bit = 1 pixel, MSB first, row-major + // Width in bytes = (spriteWidth + 7) / 8, total = widthInBytes * spriteHeight + // After all masks, data is padded to 4-byte alignment + uint32_t maskDataCount = BinaryReader_readUint32(reader); + spr->maskCount = maskDataCount; + if (maskDataCount > 0 && spr->width > 0 && spr->height > 0) { + uint32_t bytesPerRow = (spr->width + 7) / 8; + uint32_t bytesPerMask = bytesPerRow * spr->height; + + if (spr->sepMasks == 1 || !skipLoadingPreciseMasksForNonPreciseSprites) { + spr->masks = safeMalloc(maskDataCount * sizeof(uint8_t*)); + repeat(maskDataCount, j) { + spr->masks[j] = safeMalloc(bytesPerMask); + BinaryReader_readBytes(reader, spr->masks[j], bytesPerMask); + } + } else { + BinaryReader_skip(reader, bytesPerMask * maskDataCount); + spr->masks = nullptr; + } + // Pad the TOTAL mask data to 4-byte alignment (not per-mask) + uint32_t totalMaskBytes = bytesPerMask * maskDataCount; + uint32_t remainder = totalMaskBytes % 4; + if (remainder != 0) { + BinaryReader_skip(reader, 4 - remainder); + } + } else { + spr->masks = nullptr; + } + + // Nine-slice block (40 bytes). Located at nineSliceOffset (absolute file offset) elsewhere in the chunk. + if (nineSliceOffset != 0) { + size_t savedPos = BinaryReader_getPosition(reader); + BinaryReader_seek(reader, (size_t) nineSliceOffset); + spr->nsLeft = BinaryReader_readInt32(reader); + spr->nsTop = BinaryReader_readInt32(reader); + spr->nsRight = BinaryReader_readInt32(reader); + spr->nsBottom = BinaryReader_readInt32(reader); + spr->nineSliceEnabled = BinaryReader_readBool32(reader); + repeat(5, j) { + int32_t mode = BinaryReader_readInt32(reader); + spr->nsTileModes[j] = (uint8_t) mode; + } + BinaryReader_seek(reader, savedPos); + } + } + + free(ptrs); +} + +static void parseBGND(BinaryReader* reader, DataWin* dw) { + Bgnd* b = &dw->bgnd; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + b->count = count; + + if (count == 0) { free(ptrs); b->backgrounds = nullptr; return; } + + b->backgrounds = safeCalloc(count, sizeof(Background)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + Background* bg = &b->backgrounds[i]; + bg->name = readStringPtr(reader, dw); + bg->transparent = BinaryReader_readBool32(reader); + bg->smooth = BinaryReader_readBool32(reader); + bg->preload = BinaryReader_readBool32(reader); + // Temporarily store the absolute file offset; parseTPAG resolves it in-place to a TPAG index once the TPAG table is known. + bg->tpagIndex = (int32_t) BinaryReader_readUint32(reader); + if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) { + bg->gms2UnknownAlways2 = BinaryReader_readUint32(reader); + bg->gms2TileWidth = BinaryReader_readUint32(reader); + bg->gms2TileHeight = BinaryReader_readUint32(reader); + if (DataWin_isVersionAtLeast(dw, 2024, 14, 0, 1)) { + bg->gms2TileSeparationX = BinaryReader_readUint32(reader); + bg->gms2TileSeparationY = BinaryReader_readUint32(reader); + } + bg->gms2OutputBorderX = BinaryReader_readUint32(reader); + bg->gms2OutputBorderY = BinaryReader_readUint32(reader); + bg->gms2TileColumns = BinaryReader_readUint32(reader); + bg->gms2ItemsPerTileCount = BinaryReader_readUint32(reader); + bg->gms2TileCount = BinaryReader_readUint32(reader); + bg->gms2ExportedSpriteIndex = BinaryReader_readInt32(reader); + bg->gms2FrameLength = BinaryReader_readInt64(reader); + int tileIdCount = bg->gms2TileCount * bg->gms2ItemsPerTileCount; + bg->gms2TileIds = malloc(tileIdCount*sizeof(uint32_t)); + repeat(tileIdCount, j) { + bg->gms2TileIds[j] = BinaryReader_readUint32(reader); + } + } + } + free(ptrs); +} + +static void parsePATH(BinaryReader* reader, DataWin* dw) { + PathChunk* p = &dw->path; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + p->count = count; + + if (count == 0) { free(ptrs); p->paths = nullptr; return; } + + p->paths = safeMalloc(count * sizeof(GamePath)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + GamePath* path = &p->paths[i]; + path->internalPoints = nullptr; + path->internalPointCount = 0; + path->length = 0.0; + path->name = readStringPtr(reader, dw); + path->isSmooth = BinaryReader_readBool32(reader); + path->isClosed = BinaryReader_readBool32(reader); + path->precision = BinaryReader_readUint32(reader); + + // Points SimpleList + path->pointCount = BinaryReader_readUint32(reader); + if (path->pointCount > 0) { + path->points = safeMalloc(path->pointCount * sizeof(PathPoint)); + repeat(path->pointCount, j) { + path->points[j].x = BinaryReader_readFloat32(reader); + path->points[j].y = BinaryReader_readFloat32(reader); + path->points[j].speed = BinaryReader_readFloat32(reader); + } + } else { + path->points = nullptr; + } + + // Precompute internal representation for path following + GamePath_computeInternal(path); + } + free(ptrs); +} + +static void parseSCPT(BinaryReader* reader, DataWin* dw) { + Scpt* s = &dw->scpt; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + s->count = count; + + if (count == 0) { free(ptrs); s->scripts = nullptr; return; } + + s->scripts = safeMalloc(count * sizeof(Script)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + s->scripts[i].name = readStringPtr(reader, dw); + s->scripts[i].codeId = BinaryReader_readInt32(reader); + } + free(ptrs); +} + +static void parseGLOB(BinaryReader* reader, DataWin* dw) { + Glob* g = &dw->glob; + + g->count = BinaryReader_readUint32(reader); + if (g->count > 0) { + g->codeIds = safeMalloc(g->count * sizeof(int32_t)); + repeat(g->count, i) { + g->codeIds[i] = BinaryReader_readInt32(reader); + } + } else { + g->codeIds = nullptr; + } +} + +static void parseSHDR(BinaryReader* reader, DataWin* dw) { + Shdr* s = &dw->shdr; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + s->count = count; + + if (count == 0) { free(ptrs); s->shaders = nullptr; return; } + + s->shaders = safeMalloc(count * sizeof(Shader)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + Shader* sh = &s->shaders[i]; + sh->name = readStringPtr(reader, dw); + sh->type = BinaryReader_readUint32(reader) & 0x7FFFFFFF; + sh->glslES_Vertex = readStringPtr(reader, dw); + sh->glslES_Fragment = readStringPtr(reader, dw); + sh->glsl_Vertex = readStringPtr(reader, dw); + sh->glsl_Fragment = readStringPtr(reader, dw); + sh->hlsl9_Vertex = readStringPtr(reader, dw); + sh->hlsl9_Fragment = readStringPtr(reader, dw); + sh->hlsl11_VertexOffset = BinaryReader_readUint32(reader); + sh->hlsl11_PixelOffset = BinaryReader_readUint32(reader); + + // Vertex attributes SimpleList + sh->vertexAttributeCount = BinaryReader_readUint32(reader); + if (sh->vertexAttributeCount > 0) { + sh->vertexAttributes = safeMalloc(sh->vertexAttributeCount * sizeof(const char*)); + repeat(sh->vertexAttributeCount, j) { + sh->vertexAttributes[j] = readStringPtr(reader, dw); + } + } else { + sh->vertexAttributes = nullptr; + } + + // Version field (bytecodeVersion > 13) + sh->version = BinaryReader_readInt32(reader); + + sh->pssl_VertexOffset = BinaryReader_readUint32(reader); + sh->pssl_VertexLen = BinaryReader_readUint32(reader); + sh->pssl_PixelOffset = BinaryReader_readUint32(reader); + sh->pssl_PixelLen = BinaryReader_readUint32(reader); + sh->cgVita_VertexOffset = BinaryReader_readUint32(reader); + sh->cgVita_VertexLen = BinaryReader_readUint32(reader); + sh->cgVita_PixelOffset = BinaryReader_readUint32(reader); + sh->cgVita_PixelLen = BinaryReader_readUint32(reader); + + if (sh->version >= 2) { + sh->cgPS3_VertexOffset = BinaryReader_readUint32(reader); + sh->cgPS3_VertexLen = BinaryReader_readUint32(reader); + sh->cgPS3_PixelOffset = BinaryReader_readUint32(reader); + sh->cgPS3_PixelLen = BinaryReader_readUint32(reader); + } else { + sh->cgPS3_VertexOffset = 0; + sh->cgPS3_VertexLen = 0; + sh->cgPS3_PixelOffset = 0; + sh->cgPS3_PixelLen = 0; + } + + // Blob data follows but we skip it (pointer list seeking handles position) + } + free(ptrs); +} + +static void parseFONT(BinaryReader* reader, DataWin* dw) { + FontChunk* f = &dw->font; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + f->count = count; + + if (count == 0) { free(ptrs); f->fonts = nullptr; return; } + + // We need to figure out how many uint32 fields are between here and the PointerList + uint32_t fontOptionalCount = (dw->gen8.bytecodeVersion >= 17) ? 1u : 0u; + { + size_t baseAfterScaleY = (size_t) ptrs[0] + 40; + for (uint32_t trial = fontOptionalCount; 4 >= trial; trial++) { + size_t listStart = baseAfterScaleY + 4u * trial; + BinaryReader_seek(reader, listStart); + uint32_t probedGlyphCount = BinaryReader_readUint32(reader); + if (probedGlyphCount == 0 || probedGlyphCount > 0x10000) continue; + uint32_t probedFirstPtr = BinaryReader_readUint32(reader); + size_t expectedFirstPtr = listStart + 4u + 4u * probedGlyphCount; + if ((size_t) probedFirstPtr == expectedFirstPtr) { + fontOptionalCount = trial; + break; + } + } + } + + f->fonts = safeMalloc(count * sizeof(Font)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + Font* font = &f->fonts[i]; + font->name = readStringPtr(reader, dw); + font->displayName = readStringPtr(reader, dw); + font->emSize = BinaryReader_readUint32(reader); + font->bold = BinaryReader_readBool32(reader); + font->italic = BinaryReader_readBool32(reader); + font->rangeStart = BinaryReader_readUint16(reader); + font->charset = BinaryReader_readUint8(reader); + font->antiAliasing = BinaryReader_readUint8(reader); + font->rangeEnd = BinaryReader_readUint32(reader); + // Temporarily store the absolute file offset; parseTPAG resolves it in-place to a TPAG index once the TPAG table is known. + font->tpagIndex = (int32_t) BinaryReader_readUint32(reader); + font->scaleX = BinaryReader_readFloat32(reader); + font->scaleY = BinaryReader_readFloat32(reader); + // Optional fields appear in this order when present: AscenderOffset (BC17+), + // Ascender, SDFSpread, LineHeight. `fontOptionalCount` says how many are actually on disk. + font->ascenderOffset = 0; + font->ascender = 0; + font->sdfSpread = 0; + font->lineHeight = 0; + font->hasAscender = false; + font->hasSDFSpread = false; + font->hasLineHeight = false; + uint32_t readSoFar = 0; + if (dw->gen8.bytecodeVersion >= 17 && fontOptionalCount > readSoFar) { + font->ascenderOffset = BinaryReader_readInt32(reader); + readSoFar++; + } + if (fontOptionalCount > readSoFar) { + font->ascender = BinaryReader_readUint32(reader); + font->hasAscender = true; + readSoFar++; + } + if (fontOptionalCount > readSoFar) { + font->sdfSpread = BinaryReader_readUint32(reader); + font->hasSDFSpread = true; + readSoFar++; + } + if (fontOptionalCount > readSoFar) { + font->lineHeight = BinaryReader_readUint32(reader); + font->hasLineHeight = true; + readSoFar++; + } + font->isSpriteFont = false; + font->spriteIndex = -1; + + // Glyphs PointerList + uint32_t glyphCount; + uint32_t* glyphPtrs = readPointerTable(reader, &glyphCount); + font->glyphCount = glyphCount; + + uint32_t maxGlyphHeight = 0; + if (glyphCount > 0) { + font->glyphs = safeMalloc(glyphCount * sizeof(FontGlyph)); + repeat(glyphCount, j) { + BinaryReader_seek(reader, glyphPtrs[j]); + FontGlyph* glyph = &font->glyphs[j]; + glyph->character = BinaryReader_readUint16(reader); + glyph->sourceX = BinaryReader_readUint16(reader); + glyph->sourceY = BinaryReader_readUint16(reader); + glyph->sourceWidth = BinaryReader_readUint16(reader); + glyph->sourceHeight = BinaryReader_readUint16(reader); + glyph->shift = BinaryReader_readInt16(reader); + glyph->offset = BinaryReader_readInt16(reader); + + if (glyph->sourceHeight > maxGlyphHeight) maxGlyphHeight = glyph->sourceHeight; + + // Kerning SimpleListShort (uint16 count) + glyph->kerningCount = BinaryReader_readUint16(reader); + if (glyph->kerningCount > 0) { + glyph->kerning = safeMalloc(glyph->kerningCount * sizeof(KerningPair)); + for (uint16_t k = 0; glyph->kerningCount > k; k++) { + glyph->kerning[k].character = BinaryReader_readInt16(reader); + glyph->kerning[k].shiftModifier = BinaryReader_readInt16(reader); + } + } else { + glyph->kerning = nullptr; + } + } + } else { + font->glyphs = nullptr; + } + font->maxGlyphHeight = maxGlyphHeight; + Font_buildGlyphLUT(font); + free(glyphPtrs); + } + free(ptrs); + + // 512 bytes of trailing padding -- skipped by chunkEnd seek +} + +static void parseTMLN(BinaryReader* reader, DataWin* dw) { + Tmln* t = &dw->tmln; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + t->count = count; + + if (count == 0) { free(ptrs); t->timelines = nullptr; return; } + + t->timelines = safeMalloc(count * sizeof(Timeline)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + Timeline* tl = &t->timelines[i]; + tl->name = readStringPtr(reader, dw); + tl->momentCount = BinaryReader_readUint32(reader); + + if (tl->momentCount > 0) { + tl->moments = safeMalloc(tl->momentCount * sizeof(TimelineMoment)); + + // Pass 1: Read step + event pointer pairs + uint32_t* eventPtrs = safeMalloc(tl->momentCount * sizeof(uint32_t)); + repeat(tl->momentCount, j) { + tl->moments[j].step = BinaryReader_readUint32(reader); + eventPtrs[j] = BinaryReader_readUint32(reader); + } + + // Pass 2: Parse event action lists + repeat(tl->momentCount, j) { + BinaryReader_seek(reader, eventPtrs[j]); + tl->moments[j].actions = readEventActions(reader, dw, &tl->moments[j].actionCount); + } + free(eventPtrs); + } else { + tl->moments = nullptr; + } + } + free(ptrs); +} + +static void parseOBJT(BinaryReader* reader, DataWin* dw) { + Objt* o = &dw->objt; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + o->count = count; + + if (count == 0) { free(ptrs); o->objects = nullptr; return; } + + // Detect GMS 2022.5+ by probing the first game object's event list structure. + if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && !DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0)) { + // Skip the 16 fixed uint32 header fields (name..angularDamping) to reach physicsVertexCount. + BinaryReader_seek(reader, ptrs[0] + 16 * 4); + int32_t vertexCount = BinaryReader_readInt32(reader); + if (vertexCount >= 0) { + // Skip friction + awake + kinematic (12 bytes) and physics vertices (8 bytes each). + uint32_t skipCount = 12 + vertexCount * 8; + uint32_t newLocation = reader->bufferPos + skipCount; + bool isOldFormat = false; + if (newLocation < reader->bufferSize) { + BinaryReader_skip(reader, skipCount); + uint32_t eventTypeCount = BinaryReader_readUint32(reader); + if (eventTypeCount == OBJT_EVENT_TYPE_COUNT) { + uint32_t firstSubEventPtr = BinaryReader_readUint32(reader); + uint32_t currentAbsPos = (uint32_t) BinaryReader_getPosition(reader); + // The remaining 14 outer-list pointers sit between here and the first sub-event list. + if (firstSubEventPtr == currentAbsPos + 14 * 4) { + isOldFormat = true; + } + } + } + if (!isOldFormat) { + DataWin_bumpVersionTo(dw, 2022, 5, 0, 0); + } + } + } + + o->objects = safeMalloc(count * sizeof(GameObject)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + GameObject* obj = &o->objects[i]; + obj->name = readStringPtr(reader, dw); + obj->spriteId = BinaryReader_readInt32(reader); + obj->visible = BinaryReader_readBool32(reader); + if (DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0)) { + obj->managed = BinaryReader_readBool32(reader); + } else { + obj->managed = false; + } + obj->solid = BinaryReader_readBool32(reader); + obj->depth = BinaryReader_readInt32(reader); + obj->persistent = BinaryReader_readBool32(reader); + obj->parentId = BinaryReader_readInt32(reader); + obj->textureMaskId = BinaryReader_readInt32(reader); + obj->usesPhysics = BinaryReader_readBool32(reader); + obj->isSensor = BinaryReader_readBool32(reader); + obj->collisionShape = BinaryReader_readUint32(reader); + obj->density = BinaryReader_readFloat32(reader); + obj->restitution = BinaryReader_readFloat32(reader); + obj->group = BinaryReader_readUint32(reader); + obj->linearDamping = BinaryReader_readFloat32(reader); + obj->angularDamping = BinaryReader_readFloat32(reader); + obj->physicsVertexCount = BinaryReader_readInt32(reader); + obj->friction = BinaryReader_readFloat32(reader); + obj->awake = BinaryReader_readBool32(reader); + obj->kinematic = BinaryReader_readBool32(reader); + + // Physics vertices + if (obj->physicsVertexCount > 0) { + obj->physicsVertices = safeMalloc(obj->physicsVertexCount * sizeof(PhysicsVertex)); + for (int32_t j = 0; obj->physicsVertexCount > j; j++) { + obj->physicsVertices[j].x = BinaryReader_readFloat32(reader); + obj->physicsVertices[j].y = BinaryReader_readFloat32(reader); + } + } else { + obj->physicsVertices = nullptr; + } + + // Events: UndertalePointerList> + // Outer pointer list: one entry per event type + // Inner pointer list: events for that type + uint32_t eventTypeCount; + uint32_t* eventTypePtrs = readPointerTable(reader, &eventTypeCount); + + for (uint32_t eventType = 0; eventTypeCount > eventType && OBJT_EVENT_TYPE_COUNT > eventType; eventType++) { + BinaryReader_seek(reader, eventTypePtrs[eventType]); + + // Inner pointer list: events for this type + uint32_t eventCount; + uint32_t* eventPtrs = readPointerTable(reader, &eventCount); + + obj->eventLists[eventType].eventCount = eventCount; + + if (eventCount > 0) { + obj->eventLists[eventType].events = safeMalloc(eventCount * sizeof(ObjectEvent)); + repeat(eventCount, j) { + BinaryReader_seek(reader, eventPtrs[j]); + obj->eventLists[eventType].events[j].eventSubtype = BinaryReader_readUint32(reader); + obj->eventLists[eventType].events[j].actions = readEventActions(reader, dw, &obj->eventLists[eventType].events[j].actionCount); + } + } else { + obj->eventLists[eventType].events = nullptr; + } + + free(eventPtrs); + } + + // Zero-fill any unused event type slots + for (uint32_t eventType = eventTypeCount; OBJT_EVENT_TYPE_COUNT > eventType; eventType++) { + obj->eventLists[eventType].eventCount = 0; + obj->eventLists[eventType].events = nullptr; + } + + free(eventTypePtrs); + } + free(ptrs); +} + +// ===[ Room payload parsing helpers ]=== +// Each of these assumes the caller has seeked to the start of the relevant PointerList (where the uint32 "count" of the list is). +// They allocate and populate the corresponding fields on Room. +// They are used by both the eager parse path and the lazy load path (DataWin_loadRoomPayload). + +static void readRoomBackgrounds(BinaryReader* reader, Room* room) { + uint32_t bgCount; + uint32_t* bgPtrs = readPointerTable(reader, &bgCount); + room->backgrounds = safeMalloc(8 * sizeof(RoomBackground)); + uint32_t fillEnd = bgCount < 8 ? bgCount : 8; + for (uint32_t j = 0; fillEnd > j; j++) { + BinaryReader_seek(reader, bgPtrs[j]); + RoomBackground* bg = &room->backgrounds[j]; + bg->enabled = BinaryReader_readBool32(reader); + bg->foreground = BinaryReader_readBool32(reader); + bg->backgroundDefinition = BinaryReader_readInt32(reader); + bg->x = BinaryReader_readInt32(reader); + bg->y = BinaryReader_readInt32(reader); + bg->tileX = BinaryReader_readInt32(reader); + bg->tileY = BinaryReader_readInt32(reader); + bg->speedX = BinaryReader_readInt32(reader); + bg->speedY = BinaryReader_readInt32(reader); + bg->stretch = BinaryReader_readBool32(reader); + } + for (uint32_t j = fillEnd; 8 > j; j++) { + memset(&room->backgrounds[j], 0, sizeof(RoomBackground)); + } + free(bgPtrs); +} + +static void readRoomViews(BinaryReader* reader, Room* room) { + uint32_t viewCount; + uint32_t* viewPtrsArr = readPointerTable(reader, &viewCount); + room->views = safeMalloc(8 * sizeof(RoomView)); + for (uint32_t j = 0; viewCount > j && 8 > j; j++) { + BinaryReader_seek(reader, viewPtrsArr[j]); + RoomView* view = &room->views[j]; + view->enabled = BinaryReader_readBool32(reader); + view->viewX = BinaryReader_readInt32(reader); + view->viewY = BinaryReader_readInt32(reader); + view->viewWidth = BinaryReader_readInt32(reader); + view->viewHeight = BinaryReader_readInt32(reader); + view->portX = BinaryReader_readInt32(reader); + view->portY = BinaryReader_readInt32(reader); + view->portWidth = BinaryReader_readInt32(reader); + view->portHeight = BinaryReader_readInt32(reader); + view->borderX = BinaryReader_readUint32(reader); + view->borderY = BinaryReader_readUint32(reader); + view->speedX = BinaryReader_readInt32(reader); + view->speedY = BinaryReader_readInt32(reader); + view->objectId = BinaryReader_readInt32(reader); + } + for (uint32_t j = viewCount; 8 > j; j++) { + memset(&room->views[j], 0, sizeof(RoomView)); + } + free(viewPtrsArr); +} + +static void readRoomGameObjects(BinaryReader* reader, DataWin* dw, Room* room) { + uint32_t objCount; + uint32_t* objPtrs = readPointerTable(reader, &objCount); + room->gameObjectCount = objCount; + if (objCount > 0) { + room->gameObjects = safeMalloc(objCount * sizeof(RoomGameObject)); + repeat(objCount, j) { + BinaryReader_seek(reader, objPtrs[j]); + RoomGameObject* go = &room->gameObjects[j]; + go->x = BinaryReader_readInt32(reader); + go->y = BinaryReader_readInt32(reader); + go->objectDefinition = BinaryReader_readInt32(reader); + go->instanceID = BinaryReader_readUint32(reader); + go->creationCode = BinaryReader_readInt32(reader); + go->scaleX = BinaryReader_readFloat32(reader); + go->scaleY = BinaryReader_readFloat32(reader); + if (DataWin_isVersionAtLeast(dw, 2, 2, 2, 302)) { + go->imageSpeed = BinaryReader_readFloat32(reader); + go->imageIndex = BinaryReader_readInt32(reader); + } else { + go->imageSpeed = 1.0f; + go->imageIndex = 0; + } + go->color = BinaryReader_readUint32(reader); + go->rotation = BinaryReader_readFloat32(reader); + if (dw->gen8.bytecodeVersion >= 16) { + go->preCreateCode = BinaryReader_readInt32(reader); + } else { + go->preCreateCode = -1; + } + } + } else { + room->gameObjects = nullptr; + } + free(objPtrs); +} + +static void readRoomTiles(BinaryReader* reader, DataWin* dw, Room* room) { + uint32_t tileCount; + uint32_t* tilePtrs = readPointerTable(reader, &tileCount); + room->tileCount = tileCount; + if (tileCount > 0) { + room->tiles = safeMalloc(tileCount * sizeof(RoomTile)); + repeat(tileCount, j) { + BinaryReader_seek(reader, tilePtrs[j]); + RoomTile* tile = &room->tiles[j]; + tile->x = BinaryReader_readInt32(reader); + tile->y = BinaryReader_readInt32(reader); + tile->useSpriteDefinition = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0); + tile->backgroundDefinition = BinaryReader_readInt32(reader); + tile->sourceX = BinaryReader_readInt32(reader); + tile->sourceY = BinaryReader_readInt32(reader); + tile->width = BinaryReader_readUint32(reader); + tile->height = BinaryReader_readUint32(reader); + tile->tileDepth = BinaryReader_readInt32(reader); + tile->instanceID = BinaryReader_readUint32(reader); + tile->scaleX = BinaryReader_readFloat32(reader); + tile->scaleY = BinaryReader_readFloat32(reader); + tile->color = BinaryReader_readUint32(reader); + } + } else { + room->tiles = nullptr; + } + free(tilePtrs); +} + +static void readRoomLayers(BinaryReader* reader, DataWin* dw, Room* room) { + uint32_t layerCount; + uint32_t* layerPtrs = readPointerTable(reader, &layerCount); + room->layerCount = layerCount; + + if (layerCount == 0) { + room->layers = nullptr; + free(layerPtrs); + return; + } + + room->layers = safeMalloc(layerCount * sizeof(RoomLayer)); + repeat(layerCount, j) { + BinaryReader_seek(reader, layerPtrs[j]); + RoomLayer* layer = &room->layers[j]; + layer->name = readStringPtr(reader, dw); + layer->id = BinaryReader_readUint32(reader); + layer->type = BinaryReader_readUint32(reader); + layer->depth = BinaryReader_readInt32(reader); + layer->xOffset = BinaryReader_readFloat32(reader); + layer->yOffset = BinaryReader_readFloat32(reader); + layer->hSpeed = BinaryReader_readFloat32(reader); + layer->vSpeed = BinaryReader_readFloat32(reader); + layer->visible = BinaryReader_readBool32(reader); + layer->assetsData = nullptr; + layer->backgroundData = nullptr; + layer->instancesData = nullptr; + layer->tilesData = nullptr; + if (DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) { + // EffectEnabled (bool32), EffectType (string ptr), EffectProperties (SimpleList) + BinaryReader_skip(reader, 4); // EffectEnabled + BinaryReader_skip(reader, 4); // EffectType (string ptr) + uint32_t effectPropCount = BinaryReader_readUint32(reader); + // Each EffectProperty is 12 bytes: Kind(int32) + Name(ptr) + Value(ptr) + BinaryReader_skip(reader, effectPropCount * 12); + } + switch (layer->type) { + case RoomLayerType_Path: + case RoomLayerType_Path2: + break; // Nothing to do + case RoomLayerType_Effect: + // In GMS 2022.1+, Effect layer data is empty (fields moved to layer header). + if (!DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) { + BinaryReader_skip(reader, 4); // EffectType (string ptr) + uint32_t propCount = BinaryReader_readUint32(reader); + BinaryReader_skip(reader, propCount * 12); + } + break; + + case RoomLayerType_Assets: { + RoomLayerAssetsData* assets = safeMalloc(sizeof(RoomLayerAssetsData)); + uint32_t legacyTilesPtr = BinaryReader_readUint32(reader); + uint32_t spritesPtr = BinaryReader_readUint32(reader); + + BinaryReader_seek(reader, legacyTilesPtr); + uint32_t *innerTilePtrs = readPointerTable(reader, &assets->legacyTileCount); + if (assets->legacyTileCount > 0) { + assets->legacyTiles = safeMalloc(assets->legacyTileCount * sizeof(RoomTile)); + repeat(assets->legacyTileCount, k) { + BinaryReader_seek(reader, innerTilePtrs[k]); + RoomTile* tile = &assets->legacyTiles[k]; + tile->x = BinaryReader_readInt32(reader); + tile->y = BinaryReader_readInt32(reader); + tile->useSpriteDefinition = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0); + tile->backgroundDefinition = BinaryReader_readInt32(reader); + tile->sourceX = BinaryReader_readInt32(reader); + tile->sourceY = BinaryReader_readInt32(reader); + tile->width = BinaryReader_readUint32(reader); + tile->height = BinaryReader_readUint32(reader); + tile->tileDepth = BinaryReader_readInt32(reader); + tile->instanceID = BinaryReader_readUint32(reader); + tile->scaleX = BinaryReader_readFloat32(reader); + tile->scaleY = BinaryReader_readFloat32(reader); + tile->color = BinaryReader_readUint32(reader); + } + } else { + assets->legacyTiles = nullptr; + } + free(innerTilePtrs); + + BinaryReader_seek(reader, spritesPtr); + uint32_t *spritePtrs = readPointerTable(reader, &assets->spriteCount); + if (assets->spriteCount > 0) { + assets->sprites = safeMalloc(assets->spriteCount * sizeof(SpriteInstance)); + repeat(assets->spriteCount, k) { + BinaryReader_seek(reader, spritePtrs[k]); + SpriteInstance* sprite = &assets->sprites[k]; + sprite->name = readStringPtr(reader, dw); + sprite->spriteIndex = BinaryReader_readInt32(reader); + sprite->x = BinaryReader_readInt32(reader); + sprite->y = BinaryReader_readInt32(reader); + sprite->scaleX = BinaryReader_readFloat32(reader); + sprite->scaleY = BinaryReader_readFloat32(reader); + sprite->color = BinaryReader_readUint32(reader); + sprite->animationSpeed = BinaryReader_readFloat32(reader); + sprite->animationSpeedType = BinaryReader_readUint32(reader); + sprite->frameIndex = BinaryReader_readFloat32(reader); + sprite->rotation = BinaryReader_readFloat32(reader); + } + } else { + assets->sprites = nullptr; + } + free(spritePtrs); + + layer->assetsData = assets; + break; + } + + case RoomLayerType_Background: { + RoomLayerBackgroundData* bg = safeMalloc(sizeof(RoomLayerBackgroundData)); + bg->visible = BinaryReader_readBool32(reader); + bg->foreground = BinaryReader_readBool32(reader); + bg->spriteIndex = BinaryReader_readInt32(reader); + bg->hTiled = BinaryReader_readBool32(reader); + bg->vTiled = BinaryReader_readBool32(reader); + bg->stretch = BinaryReader_readBool32(reader); + bg->color = BinaryReader_readUint32(reader); + bg->firstFrame = BinaryReader_readFloat32(reader); + bg->animSpeed = BinaryReader_readFloat32(reader); + bg->animSpeedType = BinaryReader_readUint32(reader); + layer->backgroundData = bg; + break; + } + case RoomLayerType_Instances: { + RoomLayerInstancesData* inst = safeMalloc(sizeof(RoomLayerInstancesData)); + inst->instanceCount = BinaryReader_readUint32(reader); + if (inst->instanceCount > 0) { + inst->instanceIds = safeMalloc(inst->instanceCount * sizeof(uint32_t)); + repeat(inst->instanceCount, k) { + inst->instanceIds[k] = BinaryReader_readUint32(reader); + } + } else { + inst->instanceIds = nullptr; + } + layer->instancesData = inst; + break; + } + case RoomLayerType_Tiles: { + RoomLayerTilesData* tiles = safeMalloc(sizeof(RoomLayerTilesData)); + tiles->backgroundIndex = BinaryReader_readInt32(reader); + tiles->tilesX = BinaryReader_readUint32(reader); + tiles->tilesY = BinaryReader_readUint32(reader); + uint32_t totalTiles = tiles->tilesX * tiles->tilesY; + if (totalTiles > 0) { + tiles->tileData = safeMalloc(totalTiles * sizeof(uint32_t)); + repeat(totalTiles, k) { + tiles->tileData[k] = BinaryReader_readUint32(reader); + } + } else { + tiles->tileData = nullptr; + } + layer->tilesData = tiles; + break; + } + default: { + fprintf(stderr, "Unsupported Room Layer Type %u\n", layer->type); + exit(0); + } + } + } + free(layerPtrs); +} + +// Reads all 5 payload sections for a single room via the given reader. +// Assumes the caller has populated room->*FileOffset from the header pass. +static void readRoomPayload(BinaryReader* reader, DataWin* dw, Room* room) { + require(!room->payloadLoaded); + + BinaryReader_seek(reader, room->backgroundsFileOffset); + readRoomBackgrounds(reader, room); + + BinaryReader_seek(reader, room->viewsFileOffset); + readRoomViews(reader, room); + + BinaryReader_seek(reader, room->gameObjectsFileOffset); + readRoomGameObjects(reader, dw, room); + + BinaryReader_seek(reader, room->tilesFileOffset); + readRoomTiles(reader, dw, room); + + room->layerCount = 0; + room->layers = nullptr; + if (room->layersFileOffset != 0) { + BinaryReader_seek(reader, room->layersFileOffset); + readRoomLayers(reader, dw, room); + } + + room->payloadLoaded = true; +} + +// Returns true when "name" is in the eager-load set. +static bool isRoomNameInEagerList(const char* name, StringBooleanEntry* eagerSet) { + if (name == nullptr || eagerSet == nullptr) return false; + return shgeti(eagerSet, name) >= 0; +} + +static void parseROOM(BinaryReader* reader, DataWin* dw, bool lazyLoadRooms, StringBooleanEntry* eagerlyLoadedRooms) { + RoomChunk* rc = &dw->room; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + rc->count = count; + + if (count == 0) { free(ptrs); rc->rooms = nullptr; return; } + + // Detect whether RoomGameObject includes ImageSpeed/ImageIndex fields (added in GMS 2.2.2.302). + // UndertaleModTool detects this via the distance between the first two game object pointers: 40 bytes = legacy format, 48 bytes = new format with ImageSpeed+ImageIndex. + // We skip if we already know that we are at or above 2.2.2.302. + if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0) && !DataWin_isVersionAtLeast(dw, 2, 2, 2, 302)) { + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + // Room header layout (before gameObjectsPtr): name, caption, width, height, speed, persistent, + // bgColor, drawBgColor, creationCodeId, flags, backgroundsPtr, viewsPtr = 12 uint32s. + BinaryReader_skip(reader, 12 * 4); + uint32_t gameObjectsPtr = BinaryReader_readUint32(reader); + BinaryReader_seek(reader, gameObjectsPtr); + uint32_t objCount = BinaryReader_readUint32(reader); + if (objCount >= 2) { + uint32_t firstPtr = BinaryReader_readUint32(reader); + uint32_t secondPtr = BinaryReader_readUint32(reader); + if (secondPtr - firstPtr == 48) { + DataWin_bumpVersionTo(dw, 2, 2, 2, 302); + } + break; + } + } + } + + // Detect whether Layer headers include EffectEnabled/EffectType/EffectProperties fields (added in GMS 2022.1). + if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && !DataWin_isVersionAtLeast(dw, 2022, 1, 0, 0)) { + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + // Room header before layersPtr: 22 uint32s (name..metersPerPixel). + BinaryReader_skip(reader, 22 * 4); + uint32_t layersPtr = BinaryReader_readUint32(reader); + uint32_t seqnPtr = BinaryReader_readUint32(reader); + BinaryReader_seek(reader, layersPtr); + uint32_t layerCount = BinaryReader_readUint32(reader); + if (layerCount == 0) continue; + uint32_t jumpOffset = BinaryReader_readUint32(reader); + uint32_t nextOffset = (layerCount == 1) ? seqnPtr : BinaryReader_readUint32(reader); + // Layer header: name(4) id(4) type(4) depth(4) xOff(4) yOff(4) hSpd(4) vSpd(4) visible(4) = 9 uint32s = 36 bytes. + // jumpOffset points to start of the layer; we seek to jumpOffset+8 to skip name+id then read type. + BinaryReader_seek(reader, jumpOffset + 8); + uint32_t layerType = BinaryReader_readUint32(reader); + if (layerType == RoomLayerType_Path || layerType == RoomLayerType_Path2) continue; + bool detected = false; + switch (layerType) { + case RoomLayerType_Background: { + // After type, there's depth+xOff+yOff+hSpd+vSpd+visible = 6*4 = 24, then 10 background fields = 40 bytes. + // Total legacy body after type read: 24 + 40 = 64 bytes. 2022.1 adds effect data > 64 bytes of additional data past the next layer boundary. + size_t absPos = BinaryReader_getPosition(reader); + if (nextOffset - absPos > 16 * 4) detected = true; + break; + } + case RoomLayerType_Instances: { + BinaryReader_skip(reader, 6 * 4); + uint32_t instanceCount = BinaryReader_readUint32(reader); + size_t absPos = BinaryReader_getPosition(reader); + if (nextOffset - absPos != instanceCount * 4) detected = true; + break; + } + case RoomLayerType_Assets: { + BinaryReader_skip(reader, 6 * 4); + uint32_t tileOffset = BinaryReader_readUint32(reader); + size_t absPos = BinaryReader_getPosition(reader); + if (tileOffset != absPos + 8 && tileOffset != absPos + 12) detected = true; + break; + } + case RoomLayerType_Tiles: { + BinaryReader_skip(reader, 7 * 4); + uint32_t tileMapWidth = BinaryReader_readUint32(reader); + uint32_t tileMapHeight = BinaryReader_readUint32(reader); + size_t absPos = BinaryReader_getPosition(reader); + if (nextOffset - absPos != tileMapWidth * tileMapHeight * 4) detected = true; + break; + } + case RoomLayerType_Effect: { + BinaryReader_skip(reader, 7 * 4); + uint32_t propertyCount = BinaryReader_readUint32(reader); + size_t absPos = BinaryReader_getPosition(reader); + if (nextOffset - absPos != propertyCount * 3 * 4) detected = true; + break; + } + } + if (detected) DataWin_bumpVersionTo(dw, 2022, 1, 0, 0); + break; + } + } + + rc->rooms = safeCalloc(count, sizeof(Room)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + Room* room = &rc->rooms[i]; + + // ===[ Header pass ]=== + room->name = readStringPtr(reader, dw); + room->caption = readStringPtr(reader, dw); + room->width = BinaryReader_readUint32(reader); + room->height = BinaryReader_readUint32(reader); + room->speed = BinaryReader_readUint32(reader); + room->persistent = BinaryReader_readBool32(reader); + room->backgroundColor = BinaryReader_readUint32(reader); + room->drawBackgroundColor = BinaryReader_readBool32(reader); + room->creationCodeId = BinaryReader_readInt32(reader); + room->flags = BinaryReader_readUint32(reader); + room->backgroundsFileOffset = BinaryReader_readUint32(reader); + room->viewsFileOffset = BinaryReader_readUint32(reader); + room->gameObjectsFileOffset = BinaryReader_readUint32(reader); + room->tilesFileOffset = BinaryReader_readUint32(reader); + room->world = BinaryReader_readBool32(reader); + room->top = BinaryReader_readUint32(reader); + room->left = BinaryReader_readUint32(reader); + room->right = BinaryReader_readUint32(reader); + room->bottom = BinaryReader_readUint32(reader); + room->gravityX = BinaryReader_readFloat32(reader); + room->gravityY = BinaryReader_readFloat32(reader); + room->metersPerPixel = BinaryReader_readFloat32(reader); + if (DataWin_isVersionAtLeast(dw, 2024, 13, 0, 0)) { + // skip instanceCreationOrderIDs + int icCount = BinaryReader_readInt32(reader); + BinaryReader_skip(reader, sizeof(int32_t) * icCount); + } + room->layersFileOffset = 0; + if (DataWin_isVersionAtLeast(dw, 2, 0, 0, 0)) { + room->layersFileOffset = BinaryReader_readUint32(reader); + if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0)) { + BinaryReader_skip(reader, 4); // sequencesPtr + } + } + + room->payloadLoaded = false; + room->eagerlyLoaded = false; + room->backgrounds = nullptr; + room->views = nullptr; + room->gameObjects = nullptr; + room->gameObjectCount = 0; + room->tiles = nullptr; + room->tileCount = 0; + room->layers = nullptr; + room->layerCount = 0; + + // Load the room payload if needed + bool eager = !lazyLoadRooms || isRoomNameInEagerList(room->name, eagerlyLoadedRooms); + if (eager) { + readRoomPayload(reader, dw, room); + if (lazyLoadRooms) { + room->eagerlyLoaded = true; + } + } + } + free(ptrs); +} + +// Sprite/Background/Font initially store an absolute file offset to their TexturePageItem (since SPRT/BGND/FONT are parsed before TPAG). +// resolveAllTPAGReferences translates those offsets to TPAG indices once the table is known. ptrs[] is the TPAG pointer table in monotonically increasing file order, so we can binary search it. +// Offsets that don't resolve (or are 0) become -1. +static int32_t findTPAGIndexByOffset(uint32_t* ptrs, uint32_t count, uint32_t offset) { + if (offset == 0) return -1; + uint32_t lo = 0, hi = count; + while (hi > lo) { + uint32_t mid = (lo + hi) >> 1; + uint32_t v = ptrs[mid]; + if (v == offset) return (int32_t) mid; + if (offset > v) lo = mid + 1; else hi = mid; + } + return -1; +} + +static void resolveAllTPAGReferences(DataWin* dw, uint32_t* ptrs, uint32_t count) { + repeat(dw->sprt.count, i) { + Sprite* spr = &dw->sprt.sprites[i]; + repeat(spr->textureCount, j) { + spr->tpagIndices[j] = findTPAGIndexByOffset(ptrs, count, (uint32_t) spr->tpagIndices[j]); + } + } + repeat(dw->bgnd.count, i) { + Background* bg = &dw->bgnd.backgrounds[i]; + bg->tpagIndex = findTPAGIndexByOffset(ptrs, count, (uint32_t) bg->tpagIndex); + } + repeat(dw->font.count, i) { + Font* fnt = &dw->font.fonts[i]; + fnt->tpagIndex = findTPAGIndexByOffset(ptrs, count, (uint32_t) fnt->tpagIndex); + } +} + +static void parseTPAG(BinaryReader* reader, DataWin* dw) { + Tpag* t = &dw->tpag; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + t->count = count; + + if (count == 0) { free(ptrs); t->items = nullptr; return; } + + t->items = safeMalloc(count * sizeof(TexturePageItem)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + TexturePageItem* item = &t->items[i]; + item->sourceX = BinaryReader_readUint16(reader); + item->sourceY = BinaryReader_readUint16(reader); + item->sourceWidth = BinaryReader_readUint16(reader); + item->sourceHeight = BinaryReader_readUint16(reader); + item->targetX = BinaryReader_readUint16(reader); + item->targetY = BinaryReader_readUint16(reader); + item->targetWidth = BinaryReader_readUint16(reader); + item->targetHeight = BinaryReader_readUint16(reader); + item->boundingWidth = BinaryReader_readUint16(reader); + item->boundingHeight = BinaryReader_readUint16(reader); + item->texturePageId = BinaryReader_readInt16(reader); + } + + resolveAllTPAGReferences(dw, ptrs, count); + + free(ptrs); +} + +static void parseCODE(BinaryReader* reader, DataWin* dw, uint32_t chunkLength, size_t chunkDataStart) { + Code* c = &dw->code; + + if (chunkLength == 0) { + // YYC-compiled game, no bytecode + c->count = 0; + c->entries = nullptr; + return; + } + + // Standard pointer list at chunk start. Each entry has a relative offset + // (bytecodeRelAddr) that points to the actual bytecode blob elsewhere in the chunk. + + uint32_t codeCount; + uint32_t* codePtrs = readPointerTable(reader, &codeCount); + c->count = codeCount; + + if (codeCount == 0) { free(codePtrs); c->entries = nullptr; return; } + + c->entries = safeMalloc(codeCount * sizeof(CodeEntry)); + repeat(codeCount, i) { + BinaryReader_seek(reader, codePtrs[i]); + CodeEntry* entry = &c->entries[i]; + entry->name = readStringPtr(reader, dw); + entry->length = BinaryReader_readUint32(reader); + entry->localsCount = BinaryReader_readUint16(reader); + entry->argumentsCount = BinaryReader_readUint16(reader); + + // bytecodeRelAddr is relative to the position of this field + size_t relAddrFieldPos = BinaryReader_getPosition(reader); + int32_t bytecodeRelAddr = BinaryReader_readInt32(reader); + entry->bytecodeAbsoluteOffset = (uint32_t)((int64_t)relAddrFieldPos + bytecodeRelAddr); + + entry->offset = BinaryReader_readUint32(reader); + } + free(codePtrs); + + // Compute bytecode blob range and load into owned buffer. + // The bytecode blob starts at the minimum bytecodeAbsoluteOffset and + // extends to the end of the CODE chunk. + uint32_t blobStart = c->entries[0].bytecodeAbsoluteOffset; + repeat(codeCount, i) { + if (c->entries[i].bytecodeAbsoluteOffset < blobStart) { + blobStart = c->entries[i].bytecodeAbsoluteOffset; + } + } + size_t chunkEnd = chunkDataStart + chunkLength; + size_t blobSize = chunkEnd - blobStart; + + dw->bytecodeBufferBase = blobStart; + dw->bytecodeBuffer = BinaryReader_readBytesAt(reader, blobStart, blobSize); +} + +static void parseVARI(BinaryReader* reader, DataWin* dw, uint32_t chunkLength) { + Vari* v = &dw->vari; + + v->varCount1 = BinaryReader_readUint32(reader); + v->varCount2 = BinaryReader_readUint32(reader); + v->maxLocalVarCount = BinaryReader_readUint32(reader); + + // Variable entries are packed sequentially (no pointer table) + // Number of entries = (chunkLength - 12) / 20 + v->variableCount = (chunkLength - 12) / 20; + + if (v->variableCount > 0) { + v->variables = safeMalloc(v->variableCount * sizeof(Variable)); + repeat(v->variableCount, i) { + Variable* var = &v->variables[i]; + var->name = readStringPtr(reader, dw); + var->instanceType = BinaryReader_readInt32(reader); + var->varID = BinaryReader_readInt32(reader); + var->occurrences = BinaryReader_readUint32(reader); + var->firstAddress = BinaryReader_readUint32(reader); + } + } else { + v->variables = nullptr; + } +} + +static void parseFUNC(BinaryReader* reader, DataWin* dw) { + Func* f = &dw->func; + + // Part 1: Functions SimpleList + f->functionCount = BinaryReader_readUint32(reader); + if (f->functionCount > 0) { + f->functions = safeMalloc(f->functionCount * sizeof(Function)); + repeat(f->functionCount, i) { + f->functions[i].name = readStringPtr(reader, dw); + f->functions[i].occurrences = BinaryReader_readUint32(reader); + uint32_t rawAddr = BinaryReader_readUint32(reader); + // In GMS 2.3+, firstAddress points to the operand word (instruction + 4), not the instruction itself + if (DataWin_isVersionAtLeast(dw, 2, 3, 0, 0) && rawAddr != (uint32_t) -1) { + rawAddr -= 4; + } + f->functions[i].firstAddress = rawAddr; + } + } else { + f->functions = nullptr; + } + + // Part 2: Code Locals SimpleList + f->codeLocalsCount = BinaryReader_readUint32(reader); + if (f->codeLocalsCount > 0) { + f->codeLocals = safeMalloc(f->codeLocalsCount * sizeof(CodeLocals)); + repeat(f->codeLocalsCount, i) { + CodeLocals* cl = &f->codeLocals[i]; + cl->localVarCount = BinaryReader_readUint32(reader); + cl->name = readStringPtr(reader, dw); + + if (cl->localVarCount > 0) { + cl->locals = safeMalloc(cl->localVarCount * sizeof(LocalVar)); + repeat(cl->localVarCount, j) { + cl->locals[j].varID = BinaryReader_readUint32(reader); + cl->locals[j].name = readStringPtr(reader, dw); + } + } else { + cl->locals = nullptr; + } + } + } else { + f->codeLocals = nullptr; + } +} + +static void parseSTRG(BinaryReader* reader, DataWin* dw) { + Strg* s = &dw->strg; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + s->count = count; + + if (count == 0) { free(ptrs); s->strings = nullptr; return; } + + s->strings = safeMalloc(count * sizeof(const char*)); + repeat(count, i) { + // Pointer table points to the string's length prefix. + // The actual string content starts 4 bytes after. + s->strings[i] = (const char*)(dw->strgBuffer + (ptrs[i] + 4 - dw->strgBufferBase)); + } + free(ptrs); +} + +static void parseTXTR(BinaryReader* reader, DataWin* dw, size_t chunkEnd) { + Txtr* t = &dw->txtr; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + t->count = count; + + if (count == 0) { free(ptrs); t->textures = nullptr; return; } + + // Read metadata entries + bool hasGeneratedMips = DataWin_isVersionAtLeast(dw, 2, 0, 0, 0); + + // Detect GMS 2022.3+ (TextureBlockSize field) and 2022.9+ (Width/Height/IndexInGroup fields) by probing the distance between the first two entry pointers. + // Only works when there are at least 2 textures (which is almost always the case for real games). + // Layouts: + // pre-2022.3: scaled+generatedMips+blobOffset = 12 bytes + // 2022.3+: ... + textureBlockSize = 16 bytes + // 2022.9+: ... + width + height + indexInGroup = 28 bytes + bool has2022_3 = DataWin_isVersionAtLeast(dw, 2022, 3, 0, 0); + bool has2022_9 = DataWin_isVersionAtLeast(dw, 2022, 9, 0, 0); + if (count >= 2 && hasGeneratedMips && !has2022_9) { + uint32_t diff = ptrs[1] - ptrs[0]; + if (diff == 28) { + DataWin_bumpVersionTo(dw, 2022, 9, 0, 0); + has2022_3 = true; + has2022_9 = true; + } else if (diff == 16 && !has2022_3) { + DataWin_bumpVersionTo(dw, 2022, 3, 0, 0); + has2022_3 = true; + } + } + + t->textures = safeMalloc(count * sizeof(Texture)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + t->textures[i].scaled = BinaryReader_readUint32(reader); + if (hasGeneratedMips) { + t->textures[i].generatedMips = BinaryReader_readUint32(reader); + } else { + t->textures[i].generatedMips = 0; + } + if (has2022_3) { + t->textures[i].textureBlockSize = BinaryReader_readUint32(reader); + } else { + t->textures[i].textureBlockSize = 0; + } + if (has2022_9) { + t->textures[i].textureWidth = BinaryReader_readInt32(reader); + t->textures[i].textureHeight = BinaryReader_readInt32(reader); + t->textures[i].indexInGroup = BinaryReader_readInt32(reader); + } else { + t->textures[i].textureWidth = 0; + t->textures[i].textureHeight = 0; + t->textures[i].indexInGroup = 0; + } + t->textures[i].blobOffset = BinaryReader_readUint32(reader); + t->textures[i].blobData = nullptr; + } + free(ptrs); + + // Compute blob sizes from successive offsets + repeat(count, i) { + if (t->textures[i].blobOffset == 0) { + t->textures[i].blobSize = 0; // external texture + continue; + } + if (count > i + 1 && t->textures[i + 1].blobOffset != 0) { + t->textures[i].blobSize = t->textures[i + 1].blobOffset - t->textures[i].blobOffset; + } else { + t->textures[i].blobSize = (uint32_t)(chunkEnd - t->textures[i].blobOffset); + } + } + + // Load blob data into owned buffers + repeat(count, i) { + if (t->textures[i].blobOffset == 0 || t->textures[i].blobSize == 0) continue; + t->textures[i].blobData = BinaryReader_readBytesAt(reader, t->textures[i].blobOffset, t->textures[i].blobSize); + } +} + +static void parseAUDO(BinaryReader* reader, DataWin* dw, bool headersOnly) { + Audo* a = &dw->audo; + + uint32_t count; + uint32_t* ptrs = readPointerTable(reader, &count); + a->count = count; + + if (count == 0) { free(ptrs); a->entries = nullptr; return; } + + a->entries = safeMalloc(count * sizeof(AudioEntry)); + repeat(count, i) { + BinaryReader_seek(reader, ptrs[i]); + a->entries[i].dataSize = BinaryReader_readUint32(reader); + a->entries[i].dataOffset = (uint32_t)BinaryReader_getPosition(reader); + // Load audio data into owned buffer + if (headersOnly) { + a->entries[i].data = nullptr; + } else if (a->entries[i].dataSize > 0) { + a->entries[i].data = safeMalloc(a->entries[i].dataSize); + BinaryReader_readBytes(reader, a->entries[i].data, a->entries[i].dataSize); + } else { + a->entries[i].data = nullptr; + } + } + free(ptrs); +} + +// ===[ MAIN PARSE FUNCTION ]=== + +DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options) { + FILE* file = fopen(filePath, "rb"); + if (!file) { + fprintf(stderr, "Failed to open file: %s\n", filePath); + exit(1); + } + + // Use a large read buffer to reduce the number of physical reads + // This is critical for slow I/O devices like the PS2 CDVD drive, where each fread + // call would otherwise trigger a separate disc read of just a few sectors + setvbuf(file, nullptr, _IOFBF, 128 * 1024); + + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + if (fileSize <= 0) { + fprintf(stderr, "Invalid file size: %ld\n", fileSize); + fclose(file); + exit(1); + } + + // Allocate and zero-initialize DataWin + DataWin* dw = safeCalloc(1, sizeof(DataWin)); + + BinaryReader reader = BinaryReader_create(file, (size_t) fileSize); + + // Validate FORM header + char formMagic[4]; + BinaryReader_readBytes(&reader, formMagic, 4); + if (memcmp(formMagic, "FORM", 4) != 0) { + fprintf(stderr, "Invalid file: expected FORM magic, got '%.4s'\n", formMagic); + free(dw); + fclose(file); + exit(1); + } + + uint32_t formLength = BinaryReader_readUint32(&reader); + (void) formLength; + + // Pass 1: Count total chunks and find STRG chunk offset. + // All other chunks reference strings from STRG, so it must be loaded first. + // We also check if the CODE chunk exists. + int totalChunks = 0; + bool codeExists = false; + BinaryReader_seek(&reader, 8); // reset to after FORM header + + while ((size_t) fileSize > BinaryReader_getPosition(&reader)) { + if (BinaryReader_getPosition(&reader) + 8 > (size_t) fileSize) break; + + char chunkName[5] = {0}; + BinaryReader_readBytes(&reader, chunkName, 4); + uint32_t chunkLength = BinaryReader_readUint32(&reader); + size_t chunkDataStart = BinaryReader_getPosition(&reader); + + if (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) { + dw->strgBufferBase = chunkDataStart; + dw->strgBuffer = BinaryReader_readBytesAt(&reader, chunkDataStart, chunkLength); + } + + if ((memcmp(chunkName, "CODE", 4) == 0) && chunkLength > 0) { + codeExists = true; + } + + // Bump detected version based on chunk presence, so later chunks can use the right version during parsing (parseOBJT needs to know we're >= 2.3 to probe for the GMS 2022.5+ Managed field). + if (memcmp(chunkName, "ACRV", 4) == 0 || memcmp(chunkName, "SEQN", 4) == 0 || memcmp(chunkName, "TAGS", 4) == 0) { + DataWin_bumpVersionTo(dw, 2, 3, 0, 0); + } else if (memcmp(chunkName, "FEDS", 4) == 0) { + DataWin_bumpVersionTo(dw, 2, 3, 6, 0); + } else if (memcmp(chunkName, "FEAT", 4) == 0) { + DataWin_bumpVersionTo(dw, 2022, 8, 0, 0); + } else if (memcmp(chunkName, "UILR", 4) == 0) { + DataWin_bumpVersionTo(dw, 2024, 13, 0, 0); + } else if (memcmp(chunkName, "PSEM", 4) == 0 || memcmp(chunkName, "PSYS", 4) == 0) { + DataWin_bumpVersionTo(dw, 2023, 2, 0, 0); + } + + BinaryReader_seek(&reader, chunkDataStart + chunkLength); + totalChunks++; + } + + if (!codeExists && options.parseCode) { + fprintf(stderr, "CODE chunk does not exist or is empty! This usually means you're loading a YYC game.\n"); + fclose(file); + exit(1); + } + + // Pass 2: Parse all chunks + // For each chunk that will be parsed, we bulk-read the entire chunk into memory first + // and then parse from the memory buffer. This dramatically reduces the number of physical + // reads on slow I/O devices like the PS2 CDVD drive. + BinaryReader_seek(&reader, 8); // skip past FORM header + int chunkIndex = 0; + while ((size_t) fileSize > BinaryReader_getPosition(&reader)) { + if (BinaryReader_getPosition(&reader) + 8 > (size_t) fileSize) break; + + char chunkName[5] = {0}; + BinaryReader_readBytes(&reader, chunkName, 4); + uint32_t chunkLength = BinaryReader_readUint32(&reader); + size_t chunkDataStart = BinaryReader_getPosition(&reader); + size_t chunkEnd = chunkDataStart + chunkLength; + + if (options.progressCallback) { + options.progressCallback(chunkName, chunkIndex, totalChunks, dw, options.progressCallbackUserData); + } + + // Determine if this chunk will be parsed (and thus needs bulk loading) + bool shouldParse = + (options.parseGen8 && memcmp(chunkName, "GEN8", 4) == 0) || + (options.parseOptn && memcmp(chunkName, "OPTN", 4) == 0) || + (options.parseLang && memcmp(chunkName, "LANG", 4) == 0) || + (options.parseExtn && memcmp(chunkName, "EXTN", 4) == 0) || + (options.parseSond && memcmp(chunkName, "SOND", 4) == 0) || + (options.parseAgrp && memcmp(chunkName, "AGRP", 4) == 0) || + (options.parseSprt && memcmp(chunkName, "SPRT", 4) == 0) || + (options.parseBgnd && memcmp(chunkName, "BGND", 4) == 0) || + (options.parsePath && memcmp(chunkName, "PATH", 4) == 0) || + (options.parseScpt && memcmp(chunkName, "SCPT", 4) == 0) || + (options.parseGlob && memcmp(chunkName, "GLOB", 4) == 0) || + (options.parseShdr && memcmp(chunkName, "SHDR", 4) == 0) || + (options.parseFont && memcmp(chunkName, "FONT", 4) == 0) || + (options.parseTmln && memcmp(chunkName, "TMLN", 4) == 0) || + (options.parseObjt && memcmp(chunkName, "OBJT", 4) == 0) || + (options.parseRoom && memcmp(chunkName, "ROOM", 4) == 0) || + (options.parseTpag && memcmp(chunkName, "TPAG", 4) == 0) || + (options.parseCode && memcmp(chunkName, "CODE", 4) == 0) || + (options.parseVari && memcmp(chunkName, "VARI", 4) == 0) || + (options.parseFunc && memcmp(chunkName, "FUNC", 4) == 0) || + (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) || + (options.parseTxtr && memcmp(chunkName, "TXTR", 4) == 0) || + (options.parseAudo && memcmp(chunkName, "AUDO", 4) == 0) || + (options.parseAudoHeadersOnly && memcmp(chunkName, "AUDO", 4) == 0); + + // Bulk-read the chunk data into memory for fast parsing. + bool audoHeadersOnly = options.parseAudoHeadersOnly && memcmp(chunkName, "AUDO", 4) == 0; + uint8_t* chunkBuffer = nullptr; + if (shouldParse && chunkLength > 0 && !audoHeadersOnly) { + chunkBuffer = safeMalloc(chunkLength); + size_t read = fread(chunkBuffer, 1, chunkLength, reader.file); + if (read != chunkLength) { + fprintf(stderr, "DataWin: short read on chunk %.4s (expected %u, got %zu)\n", chunkName, chunkLength, read); + exit(1); + } + BinaryReader_setBuffer(&reader, chunkBuffer, chunkDataStart, chunkLength); + } + + if (options.parseGen8 && memcmp(chunkName, "GEN8", 4) == 0) { + parseGEN8(&reader, dw); + } else if (options.parseOptn && memcmp(chunkName, "OPTN", 4) == 0) { + parseOPTN(&reader, dw); + } else if (options.parseLang && memcmp(chunkName, "LANG", 4) == 0) { + parseLANG(&reader, dw); + } else if (options.parseExtn && memcmp(chunkName, "EXTN", 4) == 0) { + parseEXTN(&reader, dw); + } else if (options.parseSond && memcmp(chunkName, "SOND", 4) == 0) { + parseSOND(&reader, dw); + } else if (options.parseAgrp && memcmp(chunkName, "AGRP", 4) == 0) { + parseAGRP(&reader, dw); + } else if (options.parseSprt && memcmp(chunkName, "SPRT", 4) == 0) { + parseSPRT(&reader, dw, options.skipLoadingPreciseMasksForNonPreciseSprites); + } else if (options.parseBgnd && memcmp(chunkName, "BGND", 4) == 0) { + parseBGND(&reader, dw); + } else if (options.parsePath && memcmp(chunkName, "PATH", 4) == 0) { + parsePATH(&reader, dw); + } else if (options.parseScpt && memcmp(chunkName, "SCPT", 4) == 0) { + parseSCPT(&reader, dw); + } else if (options.parseGlob && memcmp(chunkName, "GLOB", 4) == 0) { + parseGLOB(&reader, dw); + } else if (options.parseShdr && memcmp(chunkName, "SHDR", 4) == 0) { + parseSHDR(&reader, dw); + } else if (options.parseFont && memcmp(chunkName, "FONT", 4) == 0) { + parseFONT(&reader, dw); + } else if (options.parseTmln && memcmp(chunkName, "TMLN", 4) == 0) { + parseTMLN(&reader, dw); + } else if (options.parseObjt && memcmp(chunkName, "OBJT", 4) == 0) { + parseOBJT(&reader, dw); + } else if (options.parseRoom && memcmp(chunkName, "ROOM", 4) == 0) { + parseROOM(&reader, dw, options.lazyLoadRooms, options.eagerlyLoadedRooms); + } else if (memcmp(chunkName, "DAFL", 4) == 0) { + // Empty chunk, nothing to parse + } else if (memcmp(chunkName, "EMBI", 4) == 0) { + // Embedded Images chunk + } else if (memcmp(chunkName, "TGIN", 4) == 0) { + // Texture Group Info chunk (bytecodeVersion >= 17) + } else if (memcmp(chunkName, "ACRV", 4) == 0) { + // Animation Curves chunk (GMS 2.3+) + DataWin_bumpVersionTo(dw, 2, 3, 0, 0); + } else if (memcmp(chunkName, "SEQN", 4) == 0) { + // Sequences chunk (GMS 2.3+) + DataWin_bumpVersionTo(dw, 2, 3, 0, 0); + } else if (memcmp(chunkName, "TAGS", 4) == 0) { + // Tags chunk (GMS 2.3+) + DataWin_bumpVersionTo(dw, 2, 3, 0, 0); + } else if (memcmp(chunkName, "FEDS", 4) == 0) { + // Filter Effects Data chunk (GMS 2.3.6+) + DataWin_bumpVersionTo(dw, 2, 3, 6, 0); + } else if (options.parseTpag && memcmp(chunkName, "TPAG", 4) == 0) { + parseTPAG(&reader, dw); + } else if (options.parseCode && memcmp(chunkName, "CODE", 4) == 0) { + parseCODE(&reader, dw, chunkLength, chunkDataStart); + } else if (options.parseVari && memcmp(chunkName, "VARI", 4) == 0) { + parseVARI(&reader, dw, chunkLength); + } else if (options.parseFunc && memcmp(chunkName, "FUNC", 4) == 0) { + parseFUNC(&reader, dw); + } else if (options.parseStrg && memcmp(chunkName, "STRG", 4) == 0) { + parseSTRG(&reader, dw); + } else if (options.parseTxtr && memcmp(chunkName, "TXTR", 4) == 0) { + parseTXTR(&reader, dw, chunkEnd); + } else if ((options.parseAudo || options.parseAudoHeadersOnly) && memcmp(chunkName, "AUDO", 4) == 0) { + parseAUDO(&reader, dw, options.parseAudoHeadersOnly); + } else { + printf("Unknown chunk: %.4s (length %u at offset 0x%zX)\n", chunkName, chunkLength, chunkDataStart - 8); + } + + // Free the chunk buffer and revert to FILE*-based reads for the next header + if (chunkBuffer != nullptr) { + BinaryReader_clearBuffer(&reader); + free(chunkBuffer); + } + + // Seek to chunk end (skip any unread data or trailing padding) + fseek(reader.file, (long) chunkEnd, SEEK_SET); + chunkIndex++; + } + + // GMS2: apply default FPS to rooms with speed=0 + if (dw->gen8.gms2FPS > 0) { + repeat(dw->room.count, i) { + if (dw->room.rooms[i].speed == 0) { + dw->room.rooms[i].speed = (uint32_t) dw->gen8.gms2FPS; + } + } + } + + // If lazy-loading rooms, keep the file handle open for DataWin_loadRoomPayload, otherwise close it now + dw->lazyLoadRooms = options.lazyLoadRooms; + if (options.lazyLoadRooms) { + dw->lazyLoadFile = file; + dw->lazyLoadFilePath = safeStrdup(filePath); + dw->fileSize = (size_t) fileSize; + } else { + dw->lazyLoadFile = nullptr; + dw->lazyLoadFilePath = nullptr; + dw->fileSize = 0; + if (!options.parseAudoHeadersOnly) { + fclose(file); + } + } + + // If headers-only AUDO was used, open a dedicated file handle for on-demand audio reads. + // This is separate from lazyLoadFile so room-load and audio-load don't race on one FILE*. + if (options.parseAudoHeadersOnly) { + dw->lazyAudioFile = fopen(filePath, "rb"); + if (dw->lazyAudioFile == NULL) { + fprintf(stderr, "DataWin: failed to reopen %s for lazy audio reads\n", filePath); + } + if (!options.lazyLoadRooms) { + fclose(file); // close the parse-time handle now that we've reopened for audio + } + } else { + dw->lazyAudioFile = nullptr; + } + + return dw; +} + +// ===[ FREE ]=== + +void DataWin_free(DataWin* dw) { + if (!dw) return; + + // GEN8 + free(dw->gen8.roomOrder); + + // OPTN + free(dw->optn.constants); + + // LANG + free(dw->lang.entryIds); + if (dw->lang.languages) { + repeat(dw->lang.languageCount, i) { + free(dw->lang.languages[i].entries); + } + free(dw->lang.languages); + } + + // EXTN + if (dw->extn.extensions) { + repeat(dw->extn.count, i) { + Extension* ext = &dw->extn.extensions[i]; + if (ext->files) { + repeat(ext->fileCount, j) { + ExtensionFile* file = &ext->files[j]; + if (file->functions) { + repeat(file->functionCount, k) { + free(file->functions[k].arguments); + } + free(file->functions); + } + } + free(ext->files); + } + } + free(dw->extn.extensions); + } + + // SOND + free(dw->sond.sounds); + + // AGRP + free(dw->agrp.audioGroups); + + // SPRT + if (dw->sprt.sprites) { + repeat(dw->sprt.count, i) { + free(dw->sprt.sprites[i].tpagIndices); + if (dw->sprt.sprites[i].masks != nullptr) { + repeat(dw->sprt.sprites[i].maskCount, j) { + free(dw->sprt.sprites[i].masks[j]); + } + free(dw->sprt.sprites[i].masks); + } + // Runtime-allocated sprites (indices >= parsedCount) own their synthesized name + if (i >= dw->sprt.parsedCount) free((char*) dw->sprt.sprites[i].name); + } + free(dw->sprt.sprites); + } + + + // BGND + if (dw->bgnd.backgrounds) { + repeat(dw->bgnd.count, i) { + free(dw->bgnd.backgrounds[i].gms2TileIds); + } + } + free(dw->bgnd.backgrounds); + + // PATH + if (dw->path.paths) { + repeat(dw->path.count, i) { + free(dw->path.paths[i].points); + free(dw->path.paths[i].internalPoints); + } + free(dw->path.paths); + } + + // SCPT + free(dw->scpt.scripts); + + // GLOB + free(dw->glob.codeIds); + + // SHDR + if (dw->shdr.shaders) { + repeat(dw->shdr.count, i) { + free(dw->shdr.shaders[i].vertexAttributes); + } + free(dw->shdr.shaders); + } + + // FONT + if (dw->font.fonts) { + repeat(dw->font.count, i) { + Font* font = &dw->font.fonts[i]; + if (font->glyphs) { + repeat(font->glyphCount, j) { + free(font->glyphs[j].kerning); + } + free(font->glyphs); + } + } + free(dw->font.fonts); + } + + // TMLN + if (dw->tmln.timelines) { + repeat(dw->tmln.count, i) { + Timeline* tl = &dw->tmln.timelines[i]; + if (tl->moments) { + repeat(tl->momentCount, j) { + free(tl->moments[j].actions); + } + free(tl->moments); + } + } + free(dw->tmln.timelines); + } + + // OBJT + if (dw->objt.objects) { + repeat(dw->objt.count, i) { + GameObject* obj = &dw->objt.objects[i]; + free(obj->physicsVertices); + repeat(OBJT_EVENT_TYPE_COUNT, e) { + ObjectEventList* list = &obj->eventLists[e]; + if (list->events) { + repeat(list->eventCount, j) { + free(list->events[j].actions); + } + free(list->events); + } + } + } + free(dw->objt.objects); + } + + // ROOM + if (dw->room.rooms) { + repeat(dw->room.count, i) { + DataWin_freeRoomPayload(&dw->room.rooms[i]); + } + free(dw->room.rooms); + } + + // TPAG + free(dw->tpag.items); + + // CODE + free(dw->code.entries); + + // VARI + free(dw->vari.variables); + + // FUNC + free(dw->func.functions); + if (dw->func.codeLocals) { + repeat(dw->func.codeLocalsCount, i) { + free(dw->func.codeLocals[i].locals); + } + free(dw->func.codeLocals); + } + + // STRG + free(dw->strg.strings); + + // TXTR + if (dw->txtr.textures) { + repeat(dw->txtr.count, i) { + free(dw->txtr.textures[i].blobData); + } + free(dw->txtr.textures); + } + + // AUDO + if (dw->audo.entries) { + repeat(dw->audo.count, i) { + free(dw->audo.entries[i].data); + } + free(dw->audo.entries); + } + + // Owned buffers + free(dw->strgBuffer); + free(dw->bytecodeBuffer); + + // Close the lazy-load file handle (only open when lazyLoadRooms was enabled) + if (dw->lazyLoadFile != nullptr) { + fclose(dw->lazyLoadFile); + dw->lazyLoadFile = nullptr; + } + free(dw->lazyLoadFilePath); + + // Close the lazy audio file handle (only open when parseAudoHeadersOnly was used) + if (dw->lazyAudioFile != nullptr) { + fclose(dw->lazyAudioFile); + dw->lazyAudioFile = nullptr; + } + + free(dw); +} + +uint8_t* DataWin_readAudioEntryData(DataWin* dw, const AudioEntry* entry) { + if (dw == nullptr || entry == nullptr) return nullptr; + if (entry->dataSize == 0) return nullptr; + + if (entry->data != nullptr) { + uint8_t* copy = safeMalloc(entry->dataSize); + memcpy(copy, entry->data, entry->dataSize); + return copy; + } + + if (dw->lazyAudioFile == nullptr) return nullptr; + if (fseek(dw->lazyAudioFile, (long) entry->dataOffset, SEEK_SET) != 0) return nullptr; + + uint8_t* buf = safeMalloc(entry->dataSize); + size_t got = fread(buf, 1, entry->dataSize, dw->lazyAudioFile); + if (got != entry->dataSize) { + free(buf); + return nullptr; + } + return buf; +} + +// ===[ Lazy Room Payload ]=== + +void DataWin_freeRoomPayload(Room* room) { + requireNotNull(room); + free(room->backgrounds); + room->backgrounds = nullptr; + free(room->views); + room->views = nullptr; + free(room->gameObjects); + room->gameObjects = nullptr; + room->gameObjectCount = 0; + free(room->tiles); + room->tiles = nullptr; + room->tileCount = 0; + if (room->layerCount != 0 && room->layers != nullptr) { + repeat(room->layerCount, j) { + RoomLayer* layer = &room->layers[j]; + if (layer->assetsData) { + free(layer->assetsData->legacyTiles); + free(layer->assetsData->sprites); + free(layer->assetsData); + } + if (layer->backgroundData) free(layer->backgroundData); + if (layer->instancesData) { + free(layer->instancesData->instanceIds); + free(layer->instancesData); + } + if (layer->tilesData) { + free(layer->tilesData->tileData); + free(layer->tilesData); + } + } + } + free(room->layers); + room->layers = nullptr; + room->layerCount = 0; + room->payloadLoaded = false; +} + +void DataWin_loadRoomPayload(DataWin* dw, int32_t roomIndex) { + require(roomIndex >= 0 && dw->room.count > (uint32_t) roomIndex); + Room* room = &dw->room.rooms[roomIndex]; + if (room->payloadLoaded) return; + requireMessage(dw->lazyLoadFile != nullptr, "DataWin_loadRoomPayload called without an open lazy-load FILE*"); + + FILE* f = dw->lazyLoadFile; + BinaryReader lazyReader = BinaryReader_create(f, dw->fileSize); + readRoomPayload(&lazyReader, dw, room); +} + +// ===[ Dynamic Sprite Slot Allocation ]=== + +uint32_t DataWin_allocSpriteSlot(DataWin* dw, uint32_t startIndex) { + uint32_t newIndex; + for (uint32_t i = startIndex; dw->sprt.count > i; i++) { + if (dw->sprt.sprites[i].textureCount == 0) { + newIndex = i; + goto assignName; + } + } + newIndex = dw->sprt.count; + dw->sprt.count++; + dw->sprt.sprites = safeRealloc(dw->sprt.sprites, dw->sprt.count * sizeof(Sprite)); + memset(&dw->sprt.sprites[newIndex], 0, sizeof(Sprite)); +assignName: + // Match the native runner: set a "__newsprite" name so asset_get_index can find it. + // A reused slot preserves its name across glDeleteSprite's memset, so we only strdup when the slot is freshly appended (name is still NULL). + if (!dw->sprt.sprites[newIndex].name) { + char buf[32]; + snprintf(buf, sizeof(buf), "__newsprite%u", newIndex); + dw->sprt.sprites[newIndex].name = strdup(buf); + } + return newIndex; +} + +// ===[ Version Detection ]=== + +bool DataWin_isVersionAtLeast(const DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build) { + const DetectedFormat* f = &dw->detectedFormat; + if (f->major != major) return f->major > major; + if (f->minor != minor) return f->minor > minor; + if (f->release != release) return f->release > release; + return f->build >= build; +} + +void DataWin_bumpVersionTo(DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build) { + if (DataWin_isVersionAtLeast(dw, major, minor, release, build)) return; + dw->detectedFormat.major = major; + dw->detectedFormat.minor = minor; + dw->detectedFormat.release = release; + dw->detectedFormat.build = build; +} \ No newline at end of file diff --git a/src/data_win.h b/src/data_win.h index 4b929bc4..18fb9b90 100644 --- a/src/data_win.h +++ b/src/data_win.h @@ -1,864 +1,876 @@ -#pragma once - -#include "common.h" -#include -#include -#include -#include -#include - -#include "utils.h" - -// Forward declaration for progress callback -typedef struct DataWin DataWin; - -typedef struct { - bool parseGen8; - bool parseOptn; - bool parseLang; - bool parseExtn; - bool parseSond; - bool parseAgrp; - bool parseSprt; - bool parseBgnd; - bool parsePath; - bool parseScpt; - bool parseGlob; - bool parseShdr; - bool parseFont; - bool parseTmln; - bool parseObjt; - bool parseRoom; - bool parseTpag; - bool parseCode; - bool parseVari; - bool parseFunc; - bool parseStrg; - bool parseTxtr; - bool parseAudo; - // If true, precise masks will be skipped when the sprite does not have a precise state set - bool skipLoadingPreciseMasksForNonPreciseSprites; - - // If true, Room payloads (backgrounds, views, gameObjects, tiles, layers) are parsed on demand via DataWin_loadRoomPayload during gameplay. - bool lazyLoadRooms; - - // When lazyLoadRooms is true, this list indicates which rooms should be loaded during load time instead of demand. They will also not be freed. - StringBooleanEntry* eagerlyLoadedRooms; - - // Optional progress callback, called before each chunk is parsed. - // chunkName: 4-character chunk name (e.g. "GEN8", "SPRT") - // chunkIndex: 0-based index of the current chunk being parsed - // totalChunks: total number of chunks in the file - // dataWin: the DataWin being populated (earlier chunks may already be parsed) - // userData: user-provided pointer passed through from the options - void (*progressCallback)(const char* chunkName, int chunkIndex, int totalChunks, DataWin* dataWin, void* userData); - void* progressCallbackUserData; -} DataWinParserOptions; - -// ===[ GEN8 - General Info ]=== -typedef struct { - uint8_t isDebuggerDisabled; - uint8_t bytecodeVersion; - const char* fileName; - const char* config; - uint32_t lastObj; - uint32_t lastTile; - uint32_t gameID; - uint8_t directPlayGuid[16]; - const char* name; - uint32_t major; - uint32_t minor; - uint32_t release; - uint32_t build; - uint32_t defaultWindowWidth; - uint32_t defaultWindowHeight; - uint32_t info; - uint32_t licenseCRC32; - uint8_t licenseMD5[16]; - uint64_t timestamp; - const char* displayName; - uint64_t activeTargets; - uint64_t functionClassifications; - int32_t steamAppID; - uint32_t debuggerPort; - uint32_t roomOrderCount; - int32_t* roomOrder; - float gms2FPS; -} Gen8; - -// ===[ OPTN - Options ]=== -typedef struct { - const char* name; - const char* value; -} OptnConstant; - -typedef struct { - uint64_t info; - int32_t scale; - uint32_t windowColor; - uint32_t colorDepth; - uint32_t resolution; - uint32_t frequency; - uint32_t vertexSync; - uint32_t priority; - uint32_t backImage; - uint32_t frontImage; - uint32_t loadImage; - uint32_t loadAlpha; - uint32_t constantCount; - OptnConstant* constants; -} Optn; - -// ===[ LANG - Languages ]=== -typedef struct { - const char* name; - const char* region; - uint32_t entryCount; - const char** entries; -} Language; - -typedef struct { - uint32_t unknown1; - uint32_t languageCount; - uint32_t entryCount; - const char** entryIds; - Language* languages; -} Lang; - -// ===[ EXTN - Extensions ]=== -typedef struct { - const char* name; - uint32_t id; - uint32_t kind; - uint32_t retType; - const char* extName; - uint32_t argumentCount; - uint32_t* arguments; -} ExtensionFunction; - -typedef struct { - const char* filename; - const char* cleanupScript; - const char* initScript; - uint32_t kind; - uint32_t functionCount; - ExtensionFunction* functions; -} ExtensionFile; - -typedef struct { - const char* folderName; - const char* name; - const char* className; - uint32_t fileCount; - ExtensionFile* files; -} Extension; - -typedef struct { - uint32_t count; - Extension* extensions; -} Extn; - -// ===[ SOND - Sounds ]=== -typedef struct { - const char* name; - uint32_t flags; - const char* type; - const char* file; - uint32_t effects; - float volume; - float pitch; - int32_t audioGroup; - int32_t audioFile; -} Sound; - -typedef struct { - uint32_t count; - Sound* sounds; -} Sond; - -// ===[ AGRP - Audio Groups ]=== -typedef struct { - const char* name; -} AudioGroup; - -typedef struct { - uint32_t count; - AudioGroup* audioGroups; -} Agrp; - -// ===[ SPRT - Sprites ]=== -typedef struct { - const char* name; - uint32_t width; - uint32_t height; - int32_t marginLeft; - int32_t marginRight; - int32_t marginBottom; - int32_t marginTop; - bool transparent; - bool smooth; - bool preload; - uint32_t bboxMode; - uint32_t sepMasks; - int32_t originX; - int32_t originY; - uint32_t sVersion; - uint32_t sSpriteType; - float gms2PlaybackSpeed; - bool gms2PlaybackSpeedType; - bool specialType; - uint32_t textureCount; - int32_t* tpagIndices; // resolved TPAG indices (one per frame); -1 for unresolved - uint32_t maskCount; // number of collision masks (one per frame, or 0) - uint8_t** masks; // array of maskCount packed bit arrays (nullptr if none) - // Nine-slice (GMS2 sVersion >= 3). Present iff the sprite stored a non-zero nineSliceOffset. - bool nineSliceEnabled; - int32_t nsLeft; - int32_t nsTop; - int32_t nsRight; - int32_t nsBottom; - uint8_t nsTileModes[5]; // order: Left, Top, Right, Bottom, Center. 0=Stretch, 1=Repeat, 2=Mirror, 3=BlankRepeat, 4=Hide -} Sprite; - -typedef struct { - uint32_t count; - uint32_t parsedCount; // number of sprites loaded from SPRT; slots >= parsedCount are runtime-allocated and own their `name` - Sprite* sprites; -} Sprt; - -// ===[ BGND - Backgrounds ]=== -typedef struct { - const char* name; - bool transparent; - bool smooth; - bool preload; - int32_t tpagIndex; // resolved TPAG index, -1 if unresolved - uint32_t gms2UnknownAlways2; - uint32_t gms2TileWidth; - uint32_t gms2TileHeight; - uint32_t gms2TileSeparationX; - uint32_t gms2TileSeparationY; - uint32_t gms2OutputBorderX; - uint32_t gms2OutputBorderY; - uint32_t gms2TileColumns; - uint32_t gms2ItemsPerTileCount; - uint32_t gms2TileCount; - int gms2ExportedSpriteIndex; - int64_t gms2FrameLength; - uint32_t *gms2TileIds; -} Background; - -typedef struct { - uint32_t count; - Background* backgrounds; -} Bgnd; - -// ===[ PATH - Paths ]=== -typedef struct { - float x; - float y; - float speed; -} PathPoint; - -typedef struct { - float x; - float y; - float speed; - float l; // cumulative arc length from start -} InternalPathPoint; - -typedef struct { - float x; - float y; - float speed; -} PathPositionResult; - -typedef struct { - const char* name; - bool isSmooth; - bool isClosed; - uint32_t precision; - uint32_t pointCount; - PathPoint* points; - uint32_t internalPointCount; - InternalPathPoint* internalPoints; - float length; // total arc length -} GamePath; - -typedef struct { - uint32_t count; - GamePath* paths; -} PathChunk; - -// ===[ SCPT - Scripts ]=== -typedef struct { - const char* name; - int32_t codeId; -} Script; - -typedef struct { - uint32_t count; - Script* scripts; -} Scpt; - -// ===[ GLOB - Global Init Scripts ]=== -typedef struct { - uint32_t count; - int32_t* codeIds; -} Glob; - -// ===[ SHDR - Shaders ]=== -typedef struct { - const char* name; - uint32_t type; - const char* glslES_Vertex; - const char* glslES_Fragment; - const char* glsl_Vertex; - const char* glsl_Fragment; - const char* hlsl9_Vertex; - const char* hlsl9_Fragment; - uint32_t hlsl11_VertexOffset; - uint32_t hlsl11_PixelOffset; - uint32_t vertexAttributeCount; - const char** vertexAttributes; - int32_t version; - uint32_t pssl_VertexOffset; - uint32_t pssl_VertexLen; - uint32_t pssl_PixelOffset; - uint32_t pssl_PixelLen; - uint32_t cgVita_VertexOffset; - uint32_t cgVita_VertexLen; - uint32_t cgVita_PixelOffset; - uint32_t cgVita_PixelLen; - uint32_t cgPS3_VertexOffset; - uint32_t cgPS3_VertexLen; - uint32_t cgPS3_PixelOffset; - uint32_t cgPS3_PixelLen; -} Shader; - -typedef struct { - uint32_t count; - Shader* shaders; -} Shdr; - -// ===[ FONT - Fonts ]=== -typedef struct { - int16_t character; - int16_t shiftModifier; -} KerningPair; - -typedef struct { - uint16_t character; - uint16_t sourceX; - uint16_t sourceY; - uint16_t sourceWidth; - uint16_t sourceHeight; - int16_t shift; - int16_t offset; - uint16_t kerningCount; - KerningPair* kerning; -} FontGlyph; - -typedef struct { - const char* name; - const char* displayName; - uint32_t emSize; - bool bold; - bool italic; - uint16_t rangeStart; - uint8_t charset; - uint8_t antiAliasing; - uint32_t rangeEnd; - int32_t tpagIndex; // resolved TPAG index, -1 if unresolved - float scaleX; - float scaleY; - int32_t ascenderOffset; // bytecodeVersion >= 17 only - uint32_t ascender; // GMS 2022.2+ (0 when absent) - uint32_t sdfSpread; // GMS 2023.2 nonLTS+ (0 when absent) - uint32_t lineHeight; // GMS 2023.6+ (0 when absent) - bool hasAscender; - bool hasSDFSpread; - bool hasLineHeight; - uint32_t glyphCount; - FontGlyph* glyphs; - uint32_t maxGlyphHeight; // Computed after glyph parse: max sourceHeight across glyphs; HTML5 runner uses this for line stride (see yyFont.TextHeight) - // ASCII fast-path lookup: glyphLUT[ch] for ch < 128, populated by Font_buildGlyphLUT after glyphs[] is filled. - // Lets TextUtils_findGlyph skip the linear scan over glyphs[] for the (overwhelmingly common) ASCII case. - FontGlyph* glyphLUT[128]; - // Sprite font fields (only valid when isSpriteFont is true) - bool isSpriteFont; - int32_t spriteIndex; // source sprite index (-1 for regular fonts) -} Font; - -// Builds the ASCII fast-path lookup table from font->glyphs. Call after glyphs[] is fully populated. -static inline void Font_buildGlyphLUT(Font* font) { - memset(font->glyphLUT, 0, sizeof(font->glyphLUT)); - repeat(font->glyphCount, i) { - FontGlyph* g = &font->glyphs[i]; - if (128 > g->character && font->glyphLUT[g->character] == nullptr) { - font->glyphLUT[g->character] = g; - } - } -} - -typedef struct { - uint32_t count; - Font* fonts; -} FontChunk; - -// ===[ EventAction (shared by TMLN and OBJT) ]=== -typedef struct { - uint32_t libID; - uint32_t id; - uint32_t kind; - bool useRelative; - bool isQuestion; - bool useApplyTo; - uint32_t exeType; - const char* actionName; - int32_t codeId; - uint32_t argumentCount; - int32_t who; - bool relative; - bool isNot; - uint32_t unknownAlwaysZero; -} EventAction; - -// ===[ TMLN - Timelines ]=== -typedef struct { - uint32_t step; - uint32_t actionCount; - EventAction* actions; -} TimelineMoment; - -typedef struct { - const char* name; - uint32_t momentCount; - TimelineMoment* moments; -} Timeline; - -typedef struct { - uint32_t count; - Timeline* timelines; -} Tmln; - -// ===[ OBJT - Game Objects ]=== -#define OBJT_EVENT_TYPE_COUNT 15 - -typedef struct { - uint32_t eventSubtype; - uint32_t actionCount; - EventAction* actions; -} ObjectEvent; - -typedef struct { - uint32_t eventCount; - ObjectEvent* events; -} ObjectEventList; - -typedef struct { - float x; - float y; -} PhysicsVertex; - -typedef struct { - const char* name; - int32_t spriteId; - bool visible; - bool managed; // GMS 2022.5+ - bool solid; - int32_t depth; - bool persistent; - int32_t parentId; - int32_t textureMaskId; - bool usesPhysics; - bool isSensor; - uint32_t collisionShape; - float density; - float restitution; - uint32_t group; - float linearDamping; - float angularDamping; - int32_t physicsVertexCount; - float friction; - bool awake; - bool kinematic; - PhysicsVertex* physicsVertices; - ObjectEventList eventLists[OBJT_EVENT_TYPE_COUNT]; -} GameObject; - -typedef struct { - uint32_t count; - GameObject* objects; -} Objt; - -// ===[ ROOM - Rooms ]=== -typedef struct { - bool enabled; - bool foreground; - int32_t backgroundDefinition; - int32_t x; - int32_t y; - int32_t tileX; - int32_t tileY; - int32_t speedX; - int32_t speedY; - bool stretch; -} RoomBackground; - -typedef struct { - bool enabled; - int32_t viewX; - int32_t viewY; - int32_t viewWidth; - int32_t viewHeight; - int32_t portX; - int32_t portY; - int32_t portWidth; - int32_t portHeight; - uint32_t borderX; - uint32_t borderY; - int32_t speedX; - int32_t speedY; - int32_t objectId; -} RoomView; - -typedef struct { - int32_t x; - int32_t y; - int32_t objectDefinition; - uint32_t instanceID; - int32_t creationCode; - float scaleX; - float scaleY; - float imageSpeed; // GMS >= 2.2.2.302 only, otherwise 0.0f - int32_t imageIndex; // GMS >= 2.2.2.302 only, otherwise 0 - uint32_t color; - float rotation; - int32_t preCreateCode; -} RoomGameObject; - -typedef struct { - int32_t x; - int32_t y; - bool useSpriteDefinition; - int32_t backgroundDefinition; - int32_t sourceX; - int32_t sourceY; - uint32_t width; - uint32_t height; - int32_t tileDepth; - uint32_t instanceID; - float scaleX; - float scaleY; - uint32_t color; -} RoomTile; - -enum RoomLayerType : uint32_t -{ - RoomLayerType_Path = 0, - RoomLayerType_Background = 1, - RoomLayerType_Instances = 2, - RoomLayerType_Assets = 3, - RoomLayerType_Tiles = 4, - RoomLayerType_Effect = 6, - RoomLayerType_Path2 = 7 -}; - -typedef struct { - const char* name; - int32_t spriteIndex; // Direct index into SPRT chunk - int32_t x; - int32_t y; - float scaleX; - float scaleY; - uint32_t color; - float animationSpeed; - uint32_t animationSpeedType; - float frameIndex; - float rotation; -} SpriteInstance; - -typedef struct { - uint32_t legacyTileCount; - RoomTile *legacyTiles; - uint32_t spriteCount; - SpriteInstance *sprites; -} RoomLayerAssetsData; - -typedef struct { - bool visible; - bool foreground; - int32_t spriteIndex; // into SPRT (-1 = none) - bool hTiled; - bool vTiled; - bool stretch; - uint32_t color; - float firstFrame; - float animSpeed; - uint32_t animSpeedType; -} RoomLayerBackgroundData; - -typedef struct { - uint32_t instanceCount; - uint32_t* instanceIds; -} RoomLayerInstancesData; - -typedef struct { - int32_t backgroundIndex; // tileset (BGND index) - uint32_t tilesX; // grid width in tiles - uint32_t tilesY; // grid height in tiles - uint32_t* tileData; // flat array of tilesX * tilesY tile values (row-major) -} RoomLayerTilesData; - -typedef struct { - const char* name; - uint32_t id; - uint32_t type; - int32_t depth; - float xOffset; - float yOffset; - float hSpeed; - float vSpeed; - bool visible; - RoomLayerAssetsData *assetsData; - RoomLayerBackgroundData *backgroundData; - RoomLayerInstancesData *instancesData; - RoomLayerTilesData *tilesData; -} RoomLayer; - -typedef struct { - // Scalar header: always valid regardless of payloadLoaded. - const char* name; - const char* caption; - uint32_t width; - uint32_t height; - uint32_t speed; - bool persistent; - uint32_t backgroundColor; - bool drawBackgroundColor; - int32_t creationCodeId; - uint32_t flags; - bool world; - uint32_t top; - uint32_t left; - uint32_t right; - uint32_t bottom; - float gravityX; - float gravityY; - float metersPerPixel; - - // Lazy-load offsets: absolute file offsets to the PointerList head for each payload section. - // Captured during the header pass of parseROOM so DataWin_loadRoomPayload can seek directly. - uint32_t backgroundsFileOffset; - uint32_t viewsFileOffset; - uint32_t gameObjectsFileOffset; - uint32_t tilesFileOffset; - uint32_t layersFileOffset; // 0 if pre-GMS2 - bool payloadLoaded; - bool eagerlyLoaded; // set if this room's name matched DataWinParserOptions.eagerlyLoadedRooms; payload is preserved across transitions - - // Payload: valid only when payloadLoaded is true. Zeroed/null otherwise. Backgrounds/views point to a heap array of 8 entries when loaded. - RoomBackground* backgrounds; - RoomView* views; - uint32_t gameObjectCount; - RoomGameObject* gameObjects; - uint32_t tileCount; - RoomTile* tiles; - uint32_t layerCount; - RoomLayer* layers; -} Room; - -typedef struct { - uint32_t count; - Room* rooms; -} RoomChunk; - -// ===[ TPAG - Texture Page Items ]=== -typedef struct { - uint16_t sourceX; - uint16_t sourceY; - uint16_t sourceWidth; - uint16_t sourceHeight; - uint16_t targetX; - uint16_t targetY; - uint16_t targetWidth; - uint16_t targetHeight; - uint16_t boundingWidth; - uint16_t boundingHeight; - int16_t texturePageId; -} TexturePageItem; - -typedef struct { - uint32_t count; - TexturePageItem* items; -} Tpag; - -// ===[ CODE - Code Entries ]=== -typedef struct { - const char* name; - uint32_t length; - uint16_t localsCount; - uint16_t argumentsCount; - uint32_t bytecodeAbsoluteOffset; - uint32_t offset; -} CodeEntry; - -typedef struct { - uint32_t count; - CodeEntry* entries; -} Code; - -// ===[ VARI - Variables ]=== -typedef struct { - const char* name; - int32_t instanceType; - int32_t varID; - uint32_t occurrences; - uint32_t firstAddress; - int16_t builtinVarId; // Pre-resolved enum ID for built-in variables (varID == -6), -1 otherwise -} Variable; - -typedef struct { - uint32_t varCount1; - uint32_t varCount2; - uint32_t maxLocalVarCount; - uint32_t variableCount; - Variable* variables; -} Vari; - -// ===[ FUNC - Functions & Code Locals ]=== -typedef struct { - const char* name; - uint32_t occurrences; - uint32_t firstAddress; -} Function; - -typedef struct { - // UndertaleModTool calls this field "Index", but that's because that's how it seemingly worked in pre-bytecode version 17 - // After bytecode version 17+, this has shown that this is actually the varID of the local variable (it matches the Variable.varID) - uint32_t varID; - const char* name; -} LocalVar; - -typedef struct { - const char* name; - uint32_t localVarCount; - LocalVar* locals; -} CodeLocals; - -typedef struct { - uint32_t functionCount; - Function* functions; - uint32_t codeLocalsCount; - CodeLocals* codeLocals; -} Func; - -// ===[ STRG - Strings ]=== -typedef struct { - uint32_t count; - const char** strings; // pointers into strgBuffer -} Strg; - -// ===[ TXTR - Embedded Textures ]=== -typedef struct { - uint32_t scaled; - uint32_t generatedMips; // GMS 2.0.6+: number of generated mipmaps (0 for GMS 1.x) - uint32_t textureBlockSize; // GMS 2022.3+: size of the texture block (0 for older versions) - int32_t textureWidth; // GMS 2022.9+ - int32_t textureHeight; // GMS 2022.9+ - int32_t indexInGroup; // GMS 2022.9+ - uint32_t blobOffset; // absolute file offset to PNG data - uint32_t blobSize; // computed size of blob data - uint8_t* blobData; // owned copy of PNG data -} Texture; - -typedef struct { - uint32_t count; - Texture* textures; -} Txtr; - -// ===[ AUDO - Embedded Audio ]=== -typedef struct { - uint32_t dataOffset; // absolute file offset to audio data - uint32_t dataSize; // length of audio data - uint8_t* data; // owned copy of audio data -} AudioEntry; - -typedef struct { - uint32_t count; - AudioEntry* entries; -} Audo; - -// ===[ Detected Format ]=== -// The effective GMS version after heuristic detection. GEN8.version is unreliable since GM:S 2, -// so chunk parsers probe the data and bump these fields upward when they detect newer-format features. -typedef struct { - uint32_t major; - uint32_t minor; - uint32_t release; - uint32_t build; -} DetectedFormat; - -// ===[ Top-level DataWin container ]=== -typedef struct DataWin { - uint8_t* strgBuffer; // owned copy of STRG chunk raw data - // Absolute file offset of strgBuffer[0], we need this because data.win stores absolute offsets (from the beginning of the data.win file) instead of relative offsets - size_t strgBufferBase; - - uint8_t* bytecodeBuffer; // owned copy of CODE bytecode blob - // Absolute file offset of bytecodeBuffer[0], we need this because data.win stores absolute offsets (from the beginning of the data.win file) instead of relative offsets - size_t bytecodeBufferBase; - - Gen8 gen8; - Optn optn; - Lang lang; - Extn extn; - Sond sond; - Agrp agrp; - Sprt sprt; - Bgnd bgnd; - PathChunk path; - Scpt scpt; - Glob glob; - Shdr shdr; - FontChunk font; - Tmln tmln; - Objt objt; - RoomChunk room; - // DAFL is empty, no field needed - Tpag tpag; - Code code; - Vari vari; - Func func; - Strg strg; - Txtr txtr; - Audo audo; - - DetectedFormat detectedFormat; - - // Held open across the whole session when DataWinParserOptions.lazyLoadRooms is true. - // Used by DataWin_loadRoomPayload to satisfy on-demand room payload reads. - // nullptr when lazy loading is disabled. Closed by DataWin_free. - FILE* lazyLoadFile; - char* lazyLoadFilePath; // owned strdup of the original file path, for diagnostics - bool lazyLoadRooms; // mirrors the parser option so Runner can branch without re-reading options -} DataWin; - -DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options); -void DataWin_free(DataWin* dataWin); -void DataWin_printDebugSummary(DataWin* dataWin); -// Lazy room payload management. DataWin_loadRoomPayload is a no-op when the payload is already loaded. -void DataWin_loadRoomPayload(DataWin* dw, int32_t roomIndex); -void DataWin_freeRoomPayload(Room* room); -// Finds a reusable dynamic Sprite slot (textureCount == 0) at or above `startIndex`, or appends a new one. -uint32_t DataWin_allocSpriteSlot(DataWin* dw, uint32_t startIndex); -// Compares the detected effective GMS version (not the raw GEN8 version) against a lower bound. -// Returns true if the detected version >= (major, minor, release, build). -// -// Mirrors UndertaleModTool's IsVersionAtLeast. -bool DataWin_isVersionAtLeast(const DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build); -// Raises the detected effective version to at least (major, minor, release, build). No-op if the detected version is already >= the target. -void DataWin_bumpVersionTo(DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build); -void GamePath_computeInternal(GamePath* path); -PathPositionResult GamePath_getPosition(GamePath* path, float t); +#pragma once + +#include "common.h" +#include +#include +#include +#include +#include + +#include "utils.h" + +// Forward declaration for progress callback +typedef struct DataWin DataWin; + +typedef struct { + bool parseGen8; + bool parseOptn; + bool parseLang; + bool parseExtn; + bool parseSond; + bool parseAgrp; + bool parseSprt; + bool parseBgnd; + bool parsePath; + bool parseScpt; + bool parseGlob; + bool parseShdr; + bool parseFont; + bool parseTmln; + bool parseObjt; + bool parseRoom; + bool parseTpag; + bool parseCode; + bool parseVari; + bool parseFunc; + bool parseStrg; + bool parseTxtr; + bool parseAudo; + bool parseAudoHeadersOnly; + // If true, precise masks will be skipped when the sprite does not have a precise state set + bool skipLoadingPreciseMasksForNonPreciseSprites; + + // If true, Room payloads (backgrounds, views, gameObjects, tiles, layers) are parsed on demand via DataWin_loadRoomPayload during gameplay. + bool lazyLoadRooms; + + // When lazyLoadRooms is true, this list indicates which rooms should be loaded during load time instead of demand. They will also not be freed. + StringBooleanEntry* eagerlyLoadedRooms; + + // Optional progress callback, called before each chunk is parsed. + // chunkName: 4-character chunk name (e.g. "GEN8", "SPRT") + // chunkIndex: 0-based index of the current chunk being parsed + // totalChunks: total number of chunks in the file + // dataWin: the DataWin being populated (earlier chunks may already be parsed) + // userData: user-provided pointer passed through from the options + void (*progressCallback)(const char* chunkName, int chunkIndex, int totalChunks, DataWin* dataWin, void* userData); + void* progressCallbackUserData; +} DataWinParserOptions; + +// ===[ GEN8 - General Info ]=== +typedef struct { + uint8_t isDebuggerDisabled; + uint8_t bytecodeVersion; + const char* fileName; + const char* config; + uint32_t lastObj; + uint32_t lastTile; + uint32_t gameID; + uint8_t directPlayGuid[16]; + const char* name; + uint32_t major; + uint32_t minor; + uint32_t release; + uint32_t build; + uint32_t defaultWindowWidth; + uint32_t defaultWindowHeight; + uint32_t info; + uint32_t licenseCRC32; + uint8_t licenseMD5[16]; + uint64_t timestamp; + const char* displayName; + uint64_t activeTargets; + uint64_t functionClassifications; + int32_t steamAppID; + uint32_t debuggerPort; + uint32_t roomOrderCount; + int32_t* roomOrder; + float gms2FPS; +} Gen8; + +// ===[ OPTN - Options ]=== +typedef struct { + const char* name; + const char* value; +} OptnConstant; + +typedef struct { + uint64_t info; + int32_t scale; + uint32_t windowColor; + uint32_t colorDepth; + uint32_t resolution; + uint32_t frequency; + uint32_t vertexSync; + uint32_t priority; + uint32_t backImage; + uint32_t frontImage; + uint32_t loadImage; + uint32_t loadAlpha; + uint32_t constantCount; + OptnConstant* constants; +} Optn; + +// ===[ LANG - Languages ]=== +typedef struct { + const char* name; + const char* region; + uint32_t entryCount; + const char** entries; +} Language; + +typedef struct { + uint32_t unknown1; + uint32_t languageCount; + uint32_t entryCount; + const char** entryIds; + Language* languages; +} Lang; + +// ===[ EXTN - Extensions ]=== +typedef struct { + const char* name; + uint32_t id; + uint32_t kind; + uint32_t retType; + const char* extName; + uint32_t argumentCount; + uint32_t* arguments; +} ExtensionFunction; + +typedef struct { + const char* filename; + const char* cleanupScript; + const char* initScript; + uint32_t kind; + uint32_t functionCount; + ExtensionFunction* functions; +} ExtensionFile; + +typedef struct { + const char* folderName; + const char* name; + const char* className; + uint32_t fileCount; + ExtensionFile* files; +} Extension; + +typedef struct { + uint32_t count; + Extension* extensions; +} Extn; + +// ===[ SOND - Sounds ]=== +typedef struct { + const char* name; + uint32_t flags; + const char* type; + const char* file; + uint32_t effects; + float volume; + float pitch; + int32_t audioGroup; + int32_t audioFile; +} Sound; + +typedef struct { + uint32_t count; + Sound* sounds; +} Sond; + +// ===[ AGRP - Audio Groups ]=== +typedef struct { + const char* name; +} AudioGroup; + +typedef struct { + uint32_t count; + AudioGroup* audioGroups; +} Agrp; + +// ===[ SPRT - Sprites ]=== +typedef struct { + const char* name; + uint32_t width; + uint32_t height; + int32_t marginLeft; + int32_t marginRight; + int32_t marginBottom; + int32_t marginTop; + bool transparent; + bool smooth; + bool preload; + uint32_t bboxMode; + uint32_t sepMasks; + int32_t originX; + int32_t originY; + uint32_t sVersion; + uint32_t sSpriteType; + float gms2PlaybackSpeed; + bool gms2PlaybackSpeedType; + bool specialType; + uint32_t textureCount; + int32_t* tpagIndices; // resolved TPAG indices (one per frame); -1 for unresolved + uint32_t maskCount; // number of collision masks (one per frame, or 0) + uint8_t** masks; // array of maskCount packed bit arrays (nullptr if none) + // Nine-slice (GMS2 sVersion >= 3). Present iff the sprite stored a non-zero nineSliceOffset. + bool nineSliceEnabled; + int32_t nsLeft; + int32_t nsTop; + int32_t nsRight; + int32_t nsBottom; + uint8_t nsTileModes[5]; // order: Left, Top, Right, Bottom, Center. 0=Stretch, 1=Repeat, 2=Mirror, 3=BlankRepeat, 4=Hide +} Sprite; + +typedef struct { + uint32_t count; + uint32_t parsedCount; // number of sprites loaded from SPRT; slots >= parsedCount are runtime-allocated and own their `name` + Sprite* sprites; +} Sprt; + +// ===[ BGND - Backgrounds ]=== +typedef struct { + const char* name; + bool transparent; + bool smooth; + bool preload; + int32_t tpagIndex; // resolved TPAG index, -1 if unresolved + uint32_t gms2UnknownAlways2; + uint32_t gms2TileWidth; + uint32_t gms2TileHeight; + uint32_t gms2TileSeparationX; + uint32_t gms2TileSeparationY; + uint32_t gms2OutputBorderX; + uint32_t gms2OutputBorderY; + uint32_t gms2TileColumns; + uint32_t gms2ItemsPerTileCount; + uint32_t gms2TileCount; + int gms2ExportedSpriteIndex; + int64_t gms2FrameLength; + uint32_t *gms2TileIds; +} Background; + +typedef struct { + uint32_t count; + Background* backgrounds; +} Bgnd; + +// ===[ PATH - Paths ]=== +typedef struct { + float x; + float y; + float speed; +} PathPoint; + +typedef struct { + float x; + float y; + float speed; + float l; // cumulative arc length from start +} InternalPathPoint; + +typedef struct { + float x; + float y; + float speed; +} PathPositionResult; + +typedef struct { + const char* name; + bool isSmooth; + bool isClosed; + uint32_t precision; + uint32_t pointCount; + PathPoint* points; + uint32_t internalPointCount; + InternalPathPoint* internalPoints; + float length; // total arc length +} GamePath; + +typedef struct { + uint32_t count; + GamePath* paths; +} PathChunk; + +// ===[ SCPT - Scripts ]=== +typedef struct { + const char* name; + int32_t codeId; +} Script; + +typedef struct { + uint32_t count; + Script* scripts; +} Scpt; + +// ===[ GLOB - Global Init Scripts ]=== +typedef struct { + uint32_t count; + int32_t* codeIds; +} Glob; + +// ===[ SHDR - Shaders ]=== +typedef struct { + const char* name; + uint32_t type; + const char* glslES_Vertex; + const char* glslES_Fragment; + const char* glsl_Vertex; + const char* glsl_Fragment; + const char* hlsl9_Vertex; + const char* hlsl9_Fragment; + uint32_t hlsl11_VertexOffset; + uint32_t hlsl11_PixelOffset; + uint32_t vertexAttributeCount; + const char** vertexAttributes; + int32_t version; + uint32_t pssl_VertexOffset; + uint32_t pssl_VertexLen; + uint32_t pssl_PixelOffset; + uint32_t pssl_PixelLen; + uint32_t cgVita_VertexOffset; + uint32_t cgVita_VertexLen; + uint32_t cgVita_PixelOffset; + uint32_t cgVita_PixelLen; + uint32_t cgPS3_VertexOffset; + uint32_t cgPS3_VertexLen; + uint32_t cgPS3_PixelOffset; + uint32_t cgPS3_PixelLen; +} Shader; + +typedef struct { + uint32_t count; + Shader* shaders; +} Shdr; + +// ===[ FONT - Fonts ]=== +typedef struct { + int16_t character; + int16_t shiftModifier; +} KerningPair; + +typedef struct { + uint16_t character; + uint16_t sourceX; + uint16_t sourceY; + uint16_t sourceWidth; + uint16_t sourceHeight; + int16_t shift; + int16_t offset; + uint16_t kerningCount; + KerningPair* kerning; +} FontGlyph; + +typedef struct { + const char* name; + const char* displayName; + uint32_t emSize; + bool bold; + bool italic; + uint16_t rangeStart; + uint8_t charset; + uint8_t antiAliasing; + uint32_t rangeEnd; + int32_t tpagIndex; // resolved TPAG index, -1 if unresolved + float scaleX; + float scaleY; + int32_t ascenderOffset; // bytecodeVersion >= 17 only + uint32_t ascender; // GMS 2022.2+ (0 when absent) + uint32_t sdfSpread; // GMS 2023.2 nonLTS+ (0 when absent) + uint32_t lineHeight; // GMS 2023.6+ (0 when absent) + bool hasAscender; + bool hasSDFSpread; + bool hasLineHeight; + uint32_t glyphCount; + FontGlyph* glyphs; + uint32_t maxGlyphHeight; // Computed after glyph parse: max sourceHeight across glyphs; HTML5 runner uses this for line stride (see yyFont.TextHeight) + // ASCII fast-path lookup: glyphLUT[ch] for ch < 128, populated by Font_buildGlyphLUT after glyphs[] is filled. + // Lets TextUtils_findGlyph skip the linear scan over glyphs[] for the (overwhelmingly common) ASCII case. + FontGlyph* glyphLUT[128]; + // Sprite font fields (only valid when isSpriteFont is true) + bool isSpriteFont; + int32_t spriteIndex; // source sprite index (-1 for regular fonts) +} Font; + +// Builds the ASCII fast-path lookup table from font->glyphs. Call after glyphs[] is fully populated. +static inline void Font_buildGlyphLUT(Font* font) { + memset(font->glyphLUT, 0, sizeof(font->glyphLUT)); + repeat(font->glyphCount, i) { + FontGlyph* g = &font->glyphs[i]; + if (128 > g->character && font->glyphLUT[g->character] == nullptr) { + font->glyphLUT[g->character] = g; + } + } +} + +typedef struct { + uint32_t count; + Font* fonts; +} FontChunk; + +// ===[ EventAction (shared by TMLN and OBJT) ]=== +typedef struct { + uint32_t libID; + uint32_t id; + uint32_t kind; + bool useRelative; + bool isQuestion; + bool useApplyTo; + uint32_t exeType; + const char* actionName; + int32_t codeId; + uint32_t argumentCount; + int32_t who; + bool relative; + bool isNot; + uint32_t unknownAlwaysZero; +} EventAction; + +// ===[ TMLN - Timelines ]=== +typedef struct { + uint32_t step; + uint32_t actionCount; + EventAction* actions; +} TimelineMoment; + +typedef struct { + const char* name; + uint32_t momentCount; + TimelineMoment* moments; +} Timeline; + +typedef struct { + uint32_t count; + Timeline* timelines; +} Tmln; + +// ===[ OBJT - Game Objects ]=== +#define OBJT_EVENT_TYPE_COUNT 15 + +typedef struct { + uint32_t eventSubtype; + uint32_t actionCount; + EventAction* actions; +} ObjectEvent; + +typedef struct { + uint32_t eventCount; + ObjectEvent* events; +} ObjectEventList; + +typedef struct { + float x; + float y; +} PhysicsVertex; + +typedef struct { + const char* name; + int32_t spriteId; + bool visible; + bool managed; // GMS 2022.5+ + bool solid; + int32_t depth; + bool persistent; + int32_t parentId; + int32_t textureMaskId; + bool usesPhysics; + bool isSensor; + uint32_t collisionShape; + float density; + float restitution; + uint32_t group; + float linearDamping; + float angularDamping; + int32_t physicsVertexCount; + float friction; + bool awake; + bool kinematic; + PhysicsVertex* physicsVertices; + ObjectEventList eventLists[OBJT_EVENT_TYPE_COUNT]; +} GameObject; + +typedef struct { + uint32_t count; + GameObject* objects; +} Objt; + +// ===[ ROOM - Rooms ]=== +typedef struct { + bool enabled; + bool foreground; + int32_t backgroundDefinition; + int32_t x; + int32_t y; + int32_t tileX; + int32_t tileY; + int32_t speedX; + int32_t speedY; + bool stretch; +} RoomBackground; + +typedef struct { + bool enabled; + int32_t viewX; + int32_t viewY; + int32_t viewWidth; + int32_t viewHeight; + int32_t portX; + int32_t portY; + int32_t portWidth; + int32_t portHeight; + uint32_t borderX; + uint32_t borderY; + int32_t speedX; + int32_t speedY; + int32_t objectId; +} RoomView; + +typedef struct { + int32_t x; + int32_t y; + int32_t objectDefinition; + uint32_t instanceID; + int32_t creationCode; + float scaleX; + float scaleY; + float imageSpeed; // GMS >= 2.2.2.302 only, otherwise 0.0f + int32_t imageIndex; // GMS >= 2.2.2.302 only, otherwise 0 + uint32_t color; + float rotation; + int32_t preCreateCode; +} RoomGameObject; + +typedef struct { + int32_t x; + int32_t y; + bool useSpriteDefinition; + int32_t backgroundDefinition; + int32_t sourceX; + int32_t sourceY; + uint32_t width; + uint32_t height; + int32_t tileDepth; + uint32_t instanceID; + float scaleX; + float scaleY; + uint32_t color; +} RoomTile; + +enum RoomLayerType +{ + RoomLayerType_Path = 0, + RoomLayerType_Background = 1, + RoomLayerType_Instances = 2, + RoomLayerType_Assets = 3, + RoomLayerType_Tiles = 4, + RoomLayerType_Effect = 6, + RoomLayerType_Path2 = 7 +}; + +typedef struct { + const char* name; + int32_t spriteIndex; // Direct index into SPRT chunk + int32_t x; + int32_t y; + float scaleX; + float scaleY; + uint32_t color; + float animationSpeed; + uint32_t animationSpeedType; + float frameIndex; + float rotation; +} SpriteInstance; + +typedef struct { + uint32_t legacyTileCount; + RoomTile *legacyTiles; + uint32_t spriteCount; + SpriteInstance *sprites; +} RoomLayerAssetsData; + +typedef struct { + bool visible; + bool foreground; + int32_t spriteIndex; // into SPRT (-1 = none) + bool hTiled; + bool vTiled; + bool stretch; + uint32_t color; + float firstFrame; + float animSpeed; + uint32_t animSpeedType; +} RoomLayerBackgroundData; + +typedef struct { + uint32_t instanceCount; + uint32_t* instanceIds; +} RoomLayerInstancesData; + +typedef struct { + int32_t backgroundIndex; // tileset (BGND index) + uint32_t tilesX; // grid width in tiles + uint32_t tilesY; // grid height in tiles + uint32_t* tileData; // flat array of tilesX * tilesY tile values (row-major) +} RoomLayerTilesData; + +typedef struct { + const char* name; + uint32_t id; + uint32_t type; + int32_t depth; + float xOffset; + float yOffset; + float hSpeed; + float vSpeed; + bool visible; + RoomLayerAssetsData *assetsData; + RoomLayerBackgroundData *backgroundData; + RoomLayerInstancesData *instancesData; + RoomLayerTilesData *tilesData; +} RoomLayer; + +typedef struct { + // Scalar header: always valid regardless of payloadLoaded. + const char* name; + const char* caption; + uint32_t width; + uint32_t height; + uint32_t speed; + bool persistent; + uint32_t backgroundColor; + bool drawBackgroundColor; + int32_t creationCodeId; + uint32_t flags; + bool world; + uint32_t top; + uint32_t left; + uint32_t right; + uint32_t bottom; + float gravityX; + float gravityY; + float metersPerPixel; + + // Lazy-load offsets: absolute file offsets to the PointerList head for each payload section. + // Captured during the header pass of parseROOM so DataWin_loadRoomPayload can seek directly. + uint32_t backgroundsFileOffset; + uint32_t viewsFileOffset; + uint32_t gameObjectsFileOffset; + uint32_t tilesFileOffset; + uint32_t layersFileOffset; // 0 if pre-GMS2 + bool payloadLoaded; + bool eagerlyLoaded; // set if this room's name matched DataWinParserOptions.eagerlyLoadedRooms; payload is preserved across transitions + + // Payload: valid only when payloadLoaded is true. Zeroed/null otherwise. Backgrounds/views point to a heap array of 8 entries when loaded. + RoomBackground* backgrounds; + RoomView* views; + uint32_t gameObjectCount; + RoomGameObject* gameObjects; + uint32_t tileCount; + RoomTile* tiles; + uint32_t layerCount; + RoomLayer* layers; +} Room; + +typedef struct { + uint32_t count; + Room* rooms; +} RoomChunk; + +// ===[ TPAG - Texture Page Items ]=== +typedef struct { + uint16_t sourceX; + uint16_t sourceY; + uint16_t sourceWidth; + uint16_t sourceHeight; + uint16_t targetX; + uint16_t targetY; + uint16_t targetWidth; + uint16_t targetHeight; + uint16_t boundingWidth; + uint16_t boundingHeight; + int16_t texturePageId; +} TexturePageItem; + +typedef struct { + uint32_t count; + TexturePageItem* items; +} Tpag; + +// ===[ CODE - Code Entries ]=== +typedef struct { + const char* name; + uint32_t length; + uint16_t localsCount; + uint16_t argumentsCount; + uint32_t bytecodeAbsoluteOffset; + uint32_t offset; +} CodeEntry; + +typedef struct { + uint32_t count; + CodeEntry* entries; +} Code; + +// ===[ VARI - Variables ]=== +typedef struct { + const char* name; + int32_t instanceType; + int32_t varID; + uint32_t occurrences; + uint32_t firstAddress; + int16_t builtinVarId; // Pre-resolved enum ID for built-in variables (varID == -6), -1 otherwise +} Variable; + +typedef struct { + uint32_t varCount1; + uint32_t varCount2; + uint32_t maxLocalVarCount; + uint32_t variableCount; + Variable* variables; +} Vari; + +// ===[ FUNC - Functions & Code Locals ]=== +typedef struct { + const char* name; + uint32_t occurrences; + uint32_t firstAddress; +} Function; + +typedef struct { + // UndertaleModTool calls this field "Index", but that's because that's how it seemingly worked in pre-bytecode version 17 + // After bytecode version 17+, this has shown that this is actually the varID of the local variable (it matches the Variable.varID) + uint32_t varID; + const char* name; +} LocalVar; + +typedef struct { + const char* name; + uint32_t localVarCount; + LocalVar* locals; +} CodeLocals; + +typedef struct { + uint32_t functionCount; + Function* functions; + uint32_t codeLocalsCount; + CodeLocals* codeLocals; +} Func; + +// ===[ STRG - Strings ]=== +typedef struct { + uint32_t count; + const char** strings; // pointers into strgBuffer +} Strg; + +// ===[ TXTR - Embedded Textures ]=== +typedef struct { + uint32_t scaled; + uint32_t generatedMips; // GMS 2.0.6+: number of generated mipmaps (0 for GMS 1.x) + uint32_t textureBlockSize; // GMS 2022.3+: size of the texture block (0 for older versions) + int32_t textureWidth; // GMS 2022.9+ + int32_t textureHeight; // GMS 2022.9+ + int32_t indexInGroup; // GMS 2022.9+ + uint32_t blobOffset; // absolute file offset to PNG data + uint32_t blobSize; // computed size of blob data + uint8_t* blobData; // owned copy of PNG data +} Texture; + +typedef struct { + uint32_t count; + Texture* textures; +} Txtr; + +// ===[ AUDO - Embedded Audio ]=== +typedef struct { + uint32_t dataOffset; // absolute file offset to audio data + uint32_t dataSize; // length of audio data + uint8_t* data; // owned copy of audio data +} AudioEntry; + +typedef struct { + uint32_t count; + AudioEntry* entries; +} Audo; + +// ===[ Detected Format ]=== +// The effective GMS version after heuristic detection. GEN8.version is unreliable since GM:S 2, +// so chunk parsers probe the data and bump these fields upward when they detect newer-format features. +typedef struct { + uint32_t major; + uint32_t minor; + uint32_t release; + uint32_t build; +} DetectedFormat; + +// ===[ Top-level DataWin container ]=== +typedef struct DataWin { + uint8_t* strgBuffer; // owned copy of STRG chunk raw data + // Absolute file offset of strgBuffer[0], we need this because data.win stores absolute offsets (from the beginning of the data.win file) instead of relative offsets + size_t strgBufferBase; + + uint8_t* bytecodeBuffer; // owned copy of CODE bytecode blob + // Absolute file offset of bytecodeBuffer[0], we need this because data.win stores absolute offsets (from the beginning of the data.win file) instead of relative offsets + size_t bytecodeBufferBase; + + Gen8 gen8; + Optn optn; + Lang lang; + Extn extn; + Sond sond; + Agrp agrp; + Sprt sprt; + Bgnd bgnd; + PathChunk path; + Scpt scpt; + Glob glob; + Shdr shdr; + FontChunk font; + Tmln tmln; + Objt objt; + RoomChunk room; + // DAFL is empty, no field needed + Tpag tpag; + Code code; + Vari vari; + Func func; + Strg strg; + Txtr txtr; + Audo audo; + + DetectedFormat detectedFormat; + + // Held open across the whole session when DataWinParserOptions.lazyLoadRooms is true. + // Used by DataWin_loadRoomPayload to satisfy on-demand room payload reads. + // nullptr when lazy loading is disabled. Closed by DataWin_free. + FILE* lazyLoadFile; + char* lazyLoadFilePath; // owned strdup of the original file path, for diagnostics + size_t fileSize; // cached size of the DataWin, captured at parse time for lazy-load bounds checks + bool lazyLoadRooms; // mirrors the parser option so Runner can branch without re-reading options + + // Kept open when parseAudoHeadersOnly was set. Used by the audio system to read + // individual audio entry bytes on demand instead of loading everything upfront. + // Access must be serialized — call DataWin_readAudioEntryData for thread-safe reads. + // Closed by DataWin_free. + FILE* lazyAudioFile; +} DataWin; + +DataWin* DataWin_parse(const char* filePath, DataWinParserOptions options); +void DataWin_free(DataWin* dataWin); +void DataWin_printDebugSummary(DataWin* dataWin); +// Lazy room payload management. DataWin_loadRoomPayload is a no-op when the payload is already loaded. +void DataWin_loadRoomPayload(DataWin* dw, int32_t roomIndex); +void DataWin_freeRoomPayload(Room* room); +// Reads audio entry data on demand when parseAudoHeadersOnly was used. +// Returns a malloc'd buffer of entry->dataSize bytes that the caller must free, +// or NULL on failure. Safe to call from any thread. +uint8_t* DataWin_readAudioEntryData(DataWin* dw, const AudioEntry* entry); +// Finds a reusable dynamic Sprite slot (textureCount == 0) at or above `startIndex`, or appends a new one. +uint32_t DataWin_allocSpriteSlot(DataWin* dw, uint32_t startIndex); +// Compares the detected effective GMS version (not the raw GEN8 version) against a lower bound. +// Returns true if the detected version >= (major, minor, release, build). +// +// Mirrors UndertaleModTool's IsVersionAtLeast. +bool DataWin_isVersionAtLeast(const DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build); +// Raises the detected effective version to at least (major, minor, release, build). No-op if the detected version is already >= the target. +void DataWin_bumpVersionTo(DataWin* dw, uint32_t major, uint32_t minor, uint32_t release, uint32_t build); +void GamePath_computeInternal(GamePath* path); +PathPositionResult GamePath_getPosition(GamePath* path, float t); diff --git a/src/data_win_print.c b/src/data_win_print.c index 3a4334f5..79815960 100644 --- a/src/data_win_print.c +++ b/src/data_win_print.c @@ -1,278 +1,278 @@ -#include "data_win.h" - -#include -#include - -#include "utils.h" - -void DataWin_printDebugSummary(DataWin* dataWin) { - printf("===== data.win Summary =====\n\n"); - - // GEN8 - Gen8* g = &dataWin->gen8; - printf("-- GEN8 (General Info) --\n"); - printf(" Game Name: %s\n", g->name ? g->name : "(null)"); - printf(" Display Name: %s\n", g->displayName ? g->displayName : "(null)"); - printf(" File Name: %s\n", g->fileName ? g->fileName : "(null)"); - printf(" Config: %s\n", g->config ? g->config : "(null)"); - printf(" Bytecode Version: %u\n", g->bytecodeVersion); - printf(" Game ID: %u\n", g->gameID); - printf(" Version: %u.%u.%u.%u\n", g->major, g->minor, g->release, g->build); - printf(" Window Size: %ux%u\n", g->defaultWindowWidth, g->defaultWindowHeight); - printf(" Steam App ID: %d\n", g->steamAppID); - printf(" Room Order: %u rooms\n", g->roomOrderCount); - printf("\n"); - - // OPTN - printf("-- OPTN (Options) --\n"); - printf(" Constants: %u\n", dataWin->optn.constantCount); - if (dataWin->optn.constantCount > 0) { - uint32_t show = dataWin->optn.constantCount < 3 ? dataWin->optn.constantCount : 3; - forEachIndexed(OptnConstant, constant, idx, dataWin->optn.constants, show) { - printf(" [%u] %s = %s\n", idx, constant->name ? constant->name : "?", constant->value ? constant->value : "?"); - } - if (dataWin->optn.constantCount > 3) printf(" ... and %u more\n", dataWin->optn.constantCount - 3); - } - printf("\n"); - - // LANG - printf("-- LANG (Languages) --\n"); - printf(" Languages: %u\n", dataWin->lang.languageCount); - printf(" Entries: %u\n", dataWin->lang.entryCount); - printf("\n"); - - // EXTN - printf("-- EXTN (Extensions) --\n"); - printf(" Extensions: %u\n", dataWin->extn.count); - forEachIndexed(Extension, ext, idx, dataWin->extn.extensions, dataWin->extn.count) { - printf(" [%u] %s (%u files)\n", idx, ext->name ? ext->name : "?", ext->fileCount); - } - printf("\n"); - - // SOND - printf("-- SOND (Sounds) --\n"); - printf(" Sounds: %u\n", dataWin->sond.count); - if (dataWin->sond.count > 0) { - uint32_t show = dataWin->sond.count < 3 ? dataWin->sond.count : 3; - forEachIndexed(Sound, snd, idx, dataWin->sond.sounds, show) { - printf(" [%u] %s (%s)\n", idx, snd->name ? snd->name : "?", snd->type ? snd->type : "?"); - } - if (dataWin->sond.count > 3) printf(" ... and %u more\n", dataWin->sond.count - 3); - } - printf("\n"); - - // AGRP - printf("-- AGRP (Audio Groups) --\n"); - printf(" Audio Groups: %u\n", dataWin->agrp.count); - forEachIndexed(AudioGroup, ag, idx, dataWin->agrp.audioGroups, dataWin->agrp.count) { - printf(" [%u] %s\n", idx, ag->name ? ag->name : "?"); - } - printf("\n"); - - // SPRT - printf("-- SPRT (Sprites) --\n"); - printf(" Sprites: %u\n", dataWin->sprt.count); - if (dataWin->sprt.count > 0) { - uint32_t show = dataWin->sprt.count < 3 ? dataWin->sprt.count : 3; - forEachIndexed(Sprite, spr, idx, dataWin->sprt.sprites, show) { - printf(" [%u] %s (%ux%u, %u frames)\n", idx, spr->name ? spr->name : "?", spr->width, spr->height, spr->textureCount); - } - if (dataWin->sprt.count > 3) printf(" ... and %u more\n", dataWin->sprt.count - 3); - } - printf("\n"); - - // BGND - printf("-- BGND (Backgrounds) --\n"); - printf(" Backgrounds: %u\n", dataWin->bgnd.count); - if (dataWin->bgnd.count > 0) { - uint32_t show = dataWin->bgnd.count < 3 ? dataWin->bgnd.count : 3; - forEachIndexed(Background, bg, idx, dataWin->bgnd.backgrounds, show) { - printf(" [%u] %s\n", idx, bg->name ? bg->name : "?"); - } - if (dataWin->bgnd.count > 3) printf(" ... and %u more\n", dataWin->bgnd.count - 3); - } - printf("\n"); - - // PATH - printf("-- PATH (Paths) --\n"); - printf(" Paths: %u\n", dataWin->path.count); - printf("\n"); - - // SCPT - printf("-- SCPT (Scripts) --\n"); - printf(" Scripts: %u\n", dataWin->scpt.count); - if (dataWin->scpt.count > 0) { - uint32_t show = dataWin->scpt.count < 3 ? dataWin->scpt.count : 3; - forEachIndexed(Script, scr, idx, dataWin->scpt.scripts, show) { - printf(" [%u] %s -> code[%d]\n", idx, scr->name ? scr->name : "?", scr->codeId); - } - if (dataWin->scpt.count > 3) printf(" ... and %u more\n", dataWin->scpt.count - 3); - } - printf("\n"); - - // GLOB - printf("-- GLOB (Global Init Scripts) --\n"); - printf(" Init Scripts: %u\n", dataWin->glob.count); - printf("\n"); - - // SHDR - printf("-- SHDR (Shaders) --\n"); - printf(" Shaders: %u\n", dataWin->shdr.count); - forEachIndexed(Shader, shdr, idx, dataWin->shdr.shaders, dataWin->shdr.count) { - printf(" [%u] %s (version %d)\n", idx, shdr->name ? shdr->name : "?", shdr->version); - } - printf("\n"); - - // FONT - printf("-- FONT (Fonts) --\n"); - printf(" Fonts: %u\n", dataWin->font.count); - forEachIndexed(Font, fnt, idx, dataWin->font.fonts, dataWin->font.count) { - printf(" [%u] %s (%s, em=%u, %u glyphs)\n", idx, fnt->name ? fnt->name : "?", fnt->displayName ? fnt->displayName : "?", fnt->emSize, fnt->glyphCount); - } - printf("\n"); - - // TMLN - printf("-- TMLN (Timelines) --\n"); - printf(" Timelines: %u\n", dataWin->tmln.count); - printf("\n"); - - // OBJT - printf("-- OBJT (Game Objects) --\n"); - printf(" Objects: %u\n", dataWin->objt.count); - if (dataWin->objt.count > 0) { - uint32_t show = dataWin->objt.count < 3 ? dataWin->objt.count : 3; - forEachIndexed(GameObject, obj, idx, dataWin->objt.objects, show) { - uint32_t totalEvents = 0; - repeat(OBJT_EVENT_TYPE_COUNT, e) { - totalEvents += obj->eventLists[e].eventCount; - } - printf(" [%u] %s (sprite=%d, depth=%d, %u events)\n", idx, obj->name ? obj->name : "?", obj->spriteId, obj->depth, totalEvents); - } - if (dataWin->objt.count > 3) printf(" ... and %u more\n", dataWin->objt.count - 3); - } - printf("\n"); - - // ROOM - printf("-- ROOM (Rooms) --\n"); - printf(" Rooms: %u\n", dataWin->room.count); - if (dataWin->room.count > 0) { - uint32_t show = dataWin->room.count < 3 ? dataWin->room.count : 3; - forEachIndexed(Room, rm, idx, dataWin->room.rooms, show) { - if (rm->payloadLoaded) { - printf(" [%u] %s (%ux%u, %u objects, %u tiles)\n", idx, rm->name ? rm->name : "?", rm->width, rm->height, rm->gameObjectCount, rm->tileCount); - } else { - // Lazy room with payload not yet loaded: gameObjectCount/tileCount would be 0 and misleading. - printf(" [%u] %s (%ux%u, payload not loaded)\n", idx, rm->name ? rm->name : "?", rm->width, rm->height); - } - } - if (dataWin->room.count > 3) printf(" ... and %u more\n", dataWin->room.count - 3); - } - printf("\n"); - - // TPAG - printf("-- TPAG (Texture Page Items) --\n"); - printf(" Items: %u\n", dataWin->tpag.count); - printf("\n"); - - // CODE - printf("-- CODE (Code Entries) --\n"); - printf(" Entries: %u\n", dataWin->code.count); - if (dataWin->code.count > 0) { - uint32_t show = dataWin->code.count < 3 ? dataWin->code.count : 3; - forEachIndexed(CodeEntry, entry, idx, dataWin->code.entries, show) { - printf(" [%u] %s (%u bytes, %u locals, %u args)\n", idx, entry->name ? entry->name : "?", entry->length, entry->localsCount, entry->argumentsCount); - } - if (dataWin->code.count > 3) printf(" ... and %u more\n", dataWin->code.count - 3); - } - printf("\n"); - - // VARI - printf("-- VARI (Variables) --\n"); - printf(" Variables: %u\n", dataWin->vari.variableCount); - printf(" Max Locals: %u\n", dataWin->vari.maxLocalVarCount); - if (dataWin->vari.variableCount > 0) { - uint32_t show = dataWin->vari.variableCount < 3 ? dataWin->vari.variableCount : 3; - forEachIndexed(Variable, var, idx, dataWin->vari.variables, show) { - printf(" [%u] %s (type=%d, id=%d, %u refs)\n", idx, var->name ? var->name : "?", var->instanceType, var->varID, var->occurrences); - } - if (dataWin->vari.variableCount > 3) printf(" ... and %u more\n", dataWin->vari.variableCount - 3); - } - printf("\n"); - - // FUNC - printf("-- FUNC (Functions) --\n"); - printf(" Functions: %u\n", dataWin->func.functionCount); - printf(" Code Locals: %u\n", dataWin->func.codeLocalsCount); - if (dataWin->func.functionCount > 0) { - uint32_t show = dataWin->func.functionCount < 3 ? dataWin->func.functionCount : 3; - forEachIndexed(Function, fn, idx, dataWin->func.functions, show) { - printf(" [%u] %s (%u refs)\n", idx, fn->name ? fn->name : "?", fn->occurrences); - } - if (dataWin->func.functionCount > 3) printf(" ... and %u more\n", dataWin->func.functionCount - 3); - } - printf("\n"); - - // STRG - printf("-- STRG (Strings) --\n"); - printf(" Strings: %u\n", dataWin->strg.count); - if (dataWin->strg.count > 0) { - uint32_t show = dataWin->strg.count < 5 ? dataWin->strg.count : 5; - repeat(show, i) { - const char* str = dataWin->strg.strings[i]; - // Truncate long strings for display - if (str) { - size_t len = strlen(str); - if (len > 60) { - printf(" [%u] \"%.60s...\" (%zu chars)\n", i, str, len); - } else { - printf(" [%u] \"%s\"\n", i, str); - } - } else { - printf(" [%u] (null)\n", i); - } - } - if (dataWin->strg.count > 5) printf(" ... and %u more\n", dataWin->strg.count - 5); - } - printf("\n"); - - // TXTR - printf("-- TXTR (Textures) --\n"); - printf(" Textures: %u\n", dataWin->txtr.count); - if (dataWin->txtr.count > 0) { - forEachIndexed(Texture, tex, idx, dataWin->txtr.textures, dataWin->txtr.count) { - printf(" [%u] offset=0x%08X size=%u bytes\n", idx, tex->blobOffset, tex->blobSize); - } - } - printf("\n"); - - // AUDO - printf("-- AUDO (Audio) --\n"); - printf(" Audio Entries: %u\n", dataWin->audo.count); - if (dataWin->audo.count > 0) { - uint32_t show = dataWin->audo.count < 3 ? dataWin->audo.count : 3; - forEachIndexed(AudioEntry, ae, idx, dataWin->audo.entries, show) { - printf(" [%u] offset=0x%08X size=%u bytes\n", idx, ae->dataOffset, ae->dataSize); - } - if (dataWin->audo.count > 3) printf(" ... and %u more\n", dataWin->audo.count - 3); - } - printf("\n"); - - printf("-- Room Instances --\n"); - forEach(Room, room, dataWin->room.rooms, dataWin->room.count) { - printf("Room %s\n", room->name); - - if (!room->payloadLoaded) { - printf(" (payload not loaded)\n"); - continue; - } - - forEachIndexed(RoomGameObject, roomGameObject, idx, room->gameObjects, room->gameObjectCount) { - int32_t objectDefinitionId = roomGameObject->objectDefinition; - GameObject* objectDefinition = &dataWin->objt.objects[objectDefinitionId]; - printf(" Object %d (%s, x=%d, y=%d)\n", objectDefinitionId, objectDefinition->name, roomGameObject->x, roomGameObject->y); - } - } - - // Overall summary - printf("===== DataWin parse complete =====\n"); -} +#include "data_win.h" + +#include +#include + +#include "utils.h" + +void DataWin_printDebugSummary(DataWin* dataWin) { + printf("===== data.win Summary =====\n\n"); + + // GEN8 + Gen8* g = &dataWin->gen8; + printf("-- GEN8 (General Info) --\n"); + printf(" Game Name: %s\n", g->name ? g->name : "(null)"); + printf(" Display Name: %s\n", g->displayName ? g->displayName : "(null)"); + printf(" File Name: %s\n", g->fileName ? g->fileName : "(null)"); + printf(" Config: %s\n", g->config ? g->config : "(null)"); + printf(" Bytecode Version: %u\n", g->bytecodeVersion); + printf(" Game ID: %u\n", g->gameID); + printf(" Version: %u.%u.%u.%u\n", g->major, g->minor, g->release, g->build); + printf(" Window Size: %ux%u\n", g->defaultWindowWidth, g->defaultWindowHeight); + printf(" Steam App ID: %d\n", g->steamAppID); + printf(" Room Order: %u rooms\n", g->roomOrderCount); + printf("\n"); + + // OPTN + printf("-- OPTN (Options) --\n"); + printf(" Constants: %u\n", dataWin->optn.constantCount); + if (dataWin->optn.constantCount > 0) { + uint32_t show = dataWin->optn.constantCount < 3 ? dataWin->optn.constantCount : 3; + forEachIndexed(OptnConstant, constant, idx, dataWin->optn.constants, show) { + printf(" [%u] %s = %s\n", idx, constant->name ? constant->name : "?", constant->value ? constant->value : "?"); + } + if (dataWin->optn.constantCount > 3) printf(" ... and %u more\n", dataWin->optn.constantCount - 3); + } + printf("\n"); + + // LANG + printf("-- LANG (Languages) --\n"); + printf(" Languages: %u\n", dataWin->lang.languageCount); + printf(" Entries: %u\n", dataWin->lang.entryCount); + printf("\n"); + + // EXTN + printf("-- EXTN (Extensions) --\n"); + printf(" Extensions: %u\n", dataWin->extn.count); + forEachIndexed(Extension, ext, idx, dataWin->extn.extensions, dataWin->extn.count) { + printf(" [%u] %s (%u files)\n", idx, ext->name ? ext->name : "?", ext->fileCount); + } + printf("\n"); + + // SOND + printf("-- SOND (Sounds) --\n"); + printf(" Sounds: %u\n", dataWin->sond.count); + if (dataWin->sond.count > 0) { + uint32_t show = dataWin->sond.count < 3 ? dataWin->sond.count : 3; + forEachIndexed(Sound, snd, idx, dataWin->sond.sounds, show) { + printf(" [%u] %s (%s)\n", idx, snd->name ? snd->name : "?", snd->type ? snd->type : "?"); + } + if (dataWin->sond.count > 3) printf(" ... and %u more\n", dataWin->sond.count - 3); + } + printf("\n"); + + // AGRP + printf("-- AGRP (Audio Groups) --\n"); + printf(" Audio Groups: %u\n", dataWin->agrp.count); + forEachIndexed(AudioGroup, ag, idx, dataWin->agrp.audioGroups, dataWin->agrp.count) { + printf(" [%u] %s\n", idx, ag->name ? ag->name : "?"); + } + printf("\n"); + + // SPRT + printf("-- SPRT (Sprites) --\n"); + printf(" Sprites: %u\n", dataWin->sprt.count); + if (dataWin->sprt.count > 0) { + uint32_t show = dataWin->sprt.count < 3 ? dataWin->sprt.count : 3; + forEachIndexed(Sprite, spr, idx, dataWin->sprt.sprites, show) { + printf(" [%u] %s (%ux%u, %u frames)\n", idx, spr->name ? spr->name : "?", spr->width, spr->height, spr->textureCount); + } + if (dataWin->sprt.count > 3) printf(" ... and %u more\n", dataWin->sprt.count - 3); + } + printf("\n"); + + // BGND + printf("-- BGND (Backgrounds) --\n"); + printf(" Backgrounds: %u\n", dataWin->bgnd.count); + if (dataWin->bgnd.count > 0) { + uint32_t show = dataWin->bgnd.count < 3 ? dataWin->bgnd.count : 3; + forEachIndexed(Background, bg, idx, dataWin->bgnd.backgrounds, show) { + printf(" [%u] %s\n", idx, bg->name ? bg->name : "?"); + } + if (dataWin->bgnd.count > 3) printf(" ... and %u more\n", dataWin->bgnd.count - 3); + } + printf("\n"); + + // PATH + printf("-- PATH (Paths) --\n"); + printf(" Paths: %u\n", dataWin->path.count); + printf("\n"); + + // SCPT + printf("-- SCPT (Scripts) --\n"); + printf(" Scripts: %u\n", dataWin->scpt.count); + if (dataWin->scpt.count > 0) { + uint32_t show = dataWin->scpt.count < 3 ? dataWin->scpt.count : 3; + forEachIndexed(Script, scr, idx, dataWin->scpt.scripts, show) { + printf(" [%u] %s -> code[%d]\n", idx, scr->name ? scr->name : "?", scr->codeId); + } + if (dataWin->scpt.count > 3) printf(" ... and %u more\n", dataWin->scpt.count - 3); + } + printf("\n"); + + // GLOB + printf("-- GLOB (Global Init Scripts) --\n"); + printf(" Init Scripts: %u\n", dataWin->glob.count); + printf("\n"); + + // SHDR + printf("-- SHDR (Shaders) --\n"); + printf(" Shaders: %u\n", dataWin->shdr.count); + forEachIndexed(Shader, shdr, idx, dataWin->shdr.shaders, dataWin->shdr.count) { + printf(" [%u] %s (version %d)\n", idx, shdr->name ? shdr->name : "?", shdr->version); + } + printf("\n"); + + // FONT + printf("-- FONT (Fonts) --\n"); + printf(" Fonts: %u\n", dataWin->font.count); + forEachIndexed(Font, fnt, idx, dataWin->font.fonts, dataWin->font.count) { + printf(" [%u] %s (%s, em=%u, %u glyphs)\n", idx, fnt->name ? fnt->name : "?", fnt->displayName ? fnt->displayName : "?", fnt->emSize, fnt->glyphCount); + } + printf("\n"); + + // TMLN + printf("-- TMLN (Timelines) --\n"); + printf(" Timelines: %u\n", dataWin->tmln.count); + printf("\n"); + + // OBJT + printf("-- OBJT (Game Objects) --\n"); + printf(" Objects: %u\n", dataWin->objt.count); + if (dataWin->objt.count > 0) { + uint32_t show = dataWin->objt.count < 3 ? dataWin->objt.count : 3; + forEachIndexed(GameObject, obj, idx, dataWin->objt.objects, show) { + uint32_t totalEvents = 0; + repeat(OBJT_EVENT_TYPE_COUNT, e) { + totalEvents += obj->eventLists[e].eventCount; + } + printf(" [%u] %s (sprite=%d, depth=%d, %u events)\n", idx, obj->name ? obj->name : "?", obj->spriteId, obj->depth, totalEvents); + } + if (dataWin->objt.count > 3) printf(" ... and %u more\n", dataWin->objt.count - 3); + } + printf("\n"); + + // ROOM + printf("-- ROOM (Rooms) --\n"); + printf(" Rooms: %u\n", dataWin->room.count); + if (dataWin->room.count > 0) { + uint32_t show = dataWin->room.count < 3 ? dataWin->room.count : 3; + forEachIndexed(Room, rm, idx, dataWin->room.rooms, show) { + if (rm->payloadLoaded) { + printf(" [%u] %s (%ux%u, %u objects, %u tiles)\n", idx, rm->name ? rm->name : "?", rm->width, rm->height, rm->gameObjectCount, rm->tileCount); + } else { + // Lazy room with payload not yet loaded: gameObjectCount/tileCount would be 0 and misleading. + printf(" [%u] %s (%ux%u, payload not loaded)\n", idx, rm->name ? rm->name : "?", rm->width, rm->height); + } + } + if (dataWin->room.count > 3) printf(" ... and %u more\n", dataWin->room.count - 3); + } + printf("\n"); + + // TPAG + printf("-- TPAG (Texture Page Items) --\n"); + printf(" Items: %u\n", dataWin->tpag.count); + printf("\n"); + + // CODE + printf("-- CODE (Code Entries) --\n"); + printf(" Entries: %u\n", dataWin->code.count); + if (dataWin->code.count > 0) { + uint32_t show = dataWin->code.count < 3 ? dataWin->code.count : 3; + forEachIndexed(CodeEntry, entry, idx, dataWin->code.entries, show) { + printf(" [%u] %s (%u bytes, %u locals, %u args)\n", idx, entry->name ? entry->name : "?", entry->length, entry->localsCount, entry->argumentsCount); + } + if (dataWin->code.count > 3) printf(" ... and %u more\n", dataWin->code.count - 3); + } + printf("\n"); + + // VARI + printf("-- VARI (Variables) --\n"); + printf(" Variables: %u\n", dataWin->vari.variableCount); + printf(" Max Locals: %u\n", dataWin->vari.maxLocalVarCount); + if (dataWin->vari.variableCount > 0) { + uint32_t show = dataWin->vari.variableCount < 3 ? dataWin->vari.variableCount : 3; + forEachIndexed(Variable, var, idx, dataWin->vari.variables, show) { + printf(" [%u] %s (type=%d, id=%d, %u refs)\n", idx, var->name ? var->name : "?", var->instanceType, var->varID, var->occurrences); + } + if (dataWin->vari.variableCount > 3) printf(" ... and %u more\n", dataWin->vari.variableCount - 3); + } + printf("\n"); + + // FUNC + printf("-- FUNC (Functions) --\n"); + printf(" Functions: %u\n", dataWin->func.functionCount); + printf(" Code Locals: %u\n", dataWin->func.codeLocalsCount); + if (dataWin->func.functionCount > 0) { + uint32_t show = dataWin->func.functionCount < 3 ? dataWin->func.functionCount : 3; + forEachIndexed(Function, fn, idx, dataWin->func.functions, show) { + printf(" [%u] %s (%u refs)\n", idx, fn->name ? fn->name : "?", fn->occurrences); + } + if (dataWin->func.functionCount > 3) printf(" ... and %u more\n", dataWin->func.functionCount - 3); + } + printf("\n"); + + // STRG + printf("-- STRG (Strings) --\n"); + printf(" Strings: %u\n", dataWin->strg.count); + if (dataWin->strg.count > 0) { + uint32_t show = dataWin->strg.count < 5 ? dataWin->strg.count : 5; + repeat(show, i) { + const char* str = dataWin->strg.strings[i]; + // Truncate long strings for display + if (str) { + size_t len = strlen(str); + if (len > 60) { + printf(" [%u] \"%.60s...\" (%zu chars)\n", i, str, len); + } else { + printf(" [%u] \"%s\"\n", i, str); + } + } else { + printf(" [%u] (null)\n", i); + } + } + if (dataWin->strg.count > 5) printf(" ... and %u more\n", dataWin->strg.count - 5); + } + printf("\n"); + + // TXTR + printf("-- TXTR (Textures) --\n"); + printf(" Textures: %u\n", dataWin->txtr.count); + if (dataWin->txtr.count > 0) { + forEachIndexed(Texture, tex, idx, dataWin->txtr.textures, dataWin->txtr.count) { + printf(" [%u] offset=0x%08X size=%u bytes\n", idx, tex->blobOffset, tex->blobSize); + } + } + printf("\n"); + + // AUDO + printf("-- AUDO (Audio) --\n"); + printf(" Audio Entries: %u\n", dataWin->audo.count); + if (dataWin->audo.count > 0) { + uint32_t show = dataWin->audo.count < 3 ? dataWin->audo.count : 3; + forEachIndexed(AudioEntry, ae, idx, dataWin->audo.entries, show) { + printf(" [%u] offset=0x%08X size=%u bytes\n", idx, ae->dataOffset, ae->dataSize); + } + if (dataWin->audo.count > 3) printf(" ... and %u more\n", dataWin->audo.count - 3); + } + printf("\n"); + + printf("-- Room Instances --\n"); + forEach(Room, room, dataWin->room.rooms, dataWin->room.count) { + printf("Room %s\n", room->name); + + if (!room->payloadLoaded) { + printf(" (payload not loaded)\n"); + continue; + } + + forEachIndexed(RoomGameObject, roomGameObject, idx, room->gameObjects, room->gameObjectCount) { + int32_t objectDefinitionId = roomGameObject->objectDefinition; + GameObject* objectDefinition = &dataWin->objt.objects[objectDefinitionId]; + printf(" Object %d (%s, x=%d, y=%d)\n", objectDefinitionId, objectDefinition->name, roomGameObject->x, roomGameObject->y); + } + } + + // Overall summary + printf("===== DataWin parse complete =====\n"); +} diff --git a/src/file_system.h b/src/file_system.h index 2341fd7c..64d46dad 100644 --- a/src/file_system.h +++ b/src/file_system.h @@ -1,29 +1,29 @@ -#pragma once - -#include "common.h" -#include -// ===[ FileSystem Vtable ]=== -// Platform-agnostic file system interface - -typedef struct FileSystem FileSystem; - -typedef struct { - // Resolve a game-relative path to a full platform path (caller frees result) - char* (*resolvePath)(FileSystem* fs, const char* relativePath); - // Check if a file exists - bool (*fileExists)(FileSystem* fs, const char* relativePath); - // Read entire file contents into a string (caller frees result), returns nullptr if not found - char* (*readFileText)(FileSystem* fs, const char* relativePath); - // Write string contents to a file (creates/overwrites), returns true on success - bool (*writeFileText)(FileSystem* fs, const char* relativePath, const char* contents); - // Delete a file, returns true on success - bool (*deleteFile)(FileSystem* fs, const char* relativePath); - // Read entire file as binary data (caller frees *outData), returns true on success - bool (*readFileBinary)(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize); - // Write binary data to a file (creates/overwrites), returns true on success - bool (*writeFileBinary)(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size); -} FileSystemVtable; - -struct FileSystem { - FileSystemVtable* vtable; -}; +#pragma once + +#include "common.h" +#include +// ===[ FileSystem Vtable ]=== +// Platform-agnostic file system interface + +typedef struct FileSystem FileSystem; + +typedef struct { + // Resolve a game-relative path to a full platform path (caller frees result) + char* (*resolvePath)(FileSystem* fs, const char* relativePath); + // Check if a file exists + bool (*fileExists)(FileSystem* fs, const char* relativePath); + // Read entire file contents into a string (caller frees result), returns nullptr if not found + char* (*readFileText)(FileSystem* fs, const char* relativePath); + // Write string contents to a file (creates/overwrites), returns true on success + bool (*writeFileText)(FileSystem* fs, const char* relativePath, const char* contents); + // Delete a file, returns true on success + bool (*deleteFile)(FileSystem* fs, const char* relativePath); + // Read entire file as binary data (caller frees *outData), returns true on success + bool (*readFileBinary)(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize); + // Write binary data to a file (creates/overwrites), returns true on success + bool (*writeFileBinary)(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size); +} FileSystemVtable; + +struct FileSystem { + FileSystemVtable* vtable; +}; diff --git a/src/gl/gl_renderer.c b/src/gl/gl_renderer.c deleted file mode 100644 index 5b2515d2..00000000 --- a/src/gl/gl_renderer.c +++ /dev/null @@ -1,1467 +0,0 @@ -#include "gl_renderer.h" -#include "matrix_math.h" -#include "text_utils.h" - -#include -#include -#include -#include -#include - -#include "stb_image.h" -#include "stb_ds.h" -#include "utils.h" -#include "image_decoder.h" - -// ===[ Constants ]=== -#define MAX_QUADS 4096 -#define FLOATS_PER_VERTEX 8 // x, y, u, v, r, g, b, a -#define VERTICES_PER_QUAD 4 -#define INDICES_PER_QUAD 6 - -// ===[ Shader Sources ]=== -#ifdef ENABLE_GLES - #define GLSL_VERSION_DIRECTIVE "#version 300 es\n" - #define GLSL_VERTEX_PRECISION "precision highp float;\n" - #define GLSL_FRAGMENT_PRECISION "precision mediump float;\n" -#else - #define GLSL_VERSION_DIRECTIVE "#version 410 core\n" - #define GLSL_VERTEX_PRECISION "" - #define GLSL_FRAGMENT_PRECISION "" -#endif - -static const char* vertexShaderSource = - GLSL_VERSION_DIRECTIVE - GLSL_VERTEX_PRECISION - "layout(location = 0) in vec2 aPos;\n" - "layout(location = 1) in vec2 aTexCoord;\n" - "layout(location = 2) in vec4 aColor;\n" - "uniform mat4 uProjection;\n" - "out vec2 vTexCoord;\n" - "out vec4 vColor;\n" - "void main() {\n" - " gl_Position = uProjection * vec4(aPos, 0.0, 1.0);\n" - " vTexCoord = aTexCoord;\n" - " vColor = aColor;\n" - "}\n"; - -static const char* fragmentShaderSource = - GLSL_VERSION_DIRECTIVE - GLSL_FRAGMENT_PRECISION - "in vec2 vTexCoord;\n" - "in vec4 vColor;\n" - "uniform sampler2D uTexture;\n" - "uniform float uAlphaTestRef;\n" // negative = disabled - "out vec4 fragColor;\n" - "void main() {\n" - " vec4 c = texture(uTexture, vTexCoord) * vColor;\n" - " if (uAlphaTestRef >= c.a) discard;\n" - " fragColor = c;\n" - "}\n"; - -// ===[ Shader Compilation ]=== - -static GLuint compileShader(GLenum type, const char* source) { - GLuint shader = glCreateShader(type); - glShaderSource(shader, 1, &source, nullptr); - glCompileShader(shader); - - GLint success; - glGetShaderiv(shader, GL_COMPILE_STATUS, &success); - if (!success) { - char infoLog[512]; - glGetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog); - fprintf(stderr, "GL: Shader compilation failed: %s\n", infoLog); - abort(); - } - return shader; -} - -static GLuint linkProgram(GLuint vertShader, GLuint fragShader) { - GLuint program = glCreateProgram(); - glAttachShader(program, vertShader); - glAttachShader(program, fragShader); - glLinkProgram(program); - - GLint success; - glGetProgramiv(program, GL_LINK_STATUS, &success); - if (!success) { - char infoLog[512]; - glGetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog); - fprintf(stderr, "GL: Shader linking failed: %s\n", infoLog); - abort(); - } - return program; -} - -// ===[ Batch Flush ]=== - -static void flushBatch(GLRenderer* gl) { - if (gl->quadCount == 0) return; - - int32_t vertexCount = gl->quadCount * VERTICES_PER_QUAD; - int32_t indexCount = gl->quadCount * INDICES_PER_QUAD; - - // Bind the VAO so the EBO binding it carries is what glDrawElements uses. - // Without this, glDrawElements would treat the nullptr indices arg as a literal pointer to client memory and SEGV inside the driver during async upload. - glBindVertexArray(gl->vao); - - glBindBuffer(GL_ARRAY_BUFFER, gl->vbo); - glBufferSubData(GL_ARRAY_BUFFER, 0, vertexCount * FLOATS_PER_VERTEX * sizeof(float), gl->vertexData); - - glBindTexture(GL_TEXTURE_2D, gl->currentTextureId); - glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, nullptr); - - gl->quadCount = 0; -} - -// ===[ Vtable Implementations ]=== - -static void glInit(Renderer* renderer, DataWin* dataWin) { - GLRenderer* gl = (GLRenderer*) renderer; - renderer->dataWin = dataWin; - - // Compile shaders - GLuint vertShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource); - GLuint fragShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); - gl->shaderProgram = linkProgram(vertShader, fragShader); - glDeleteShader(vertShader); - glDeleteShader(fragShader); - - gl->uProjection = glGetUniformLocation(gl->shaderProgram, "uProjection"); - gl->uTexture = glGetUniformLocation(gl->shaderProgram, "uTexture"); - gl->uAlphaTestRef = glGetUniformLocation(gl->shaderProgram, "uAlphaTestRef"); - gl->alphaTestEnable = false; - gl->alphaTestRef = 0.0f; - glUseProgram(gl->shaderProgram); - glUniform1f(gl->uAlphaTestRef, -1.0f); - - // Create VAO/VBO/EBO - glGenVertexArrays(1, &gl->vao); - glGenBuffers(1, &gl->vbo); - glGenBuffers(1, &gl->ebo); - - glBindVertexArray(gl->vao); - - // VBO: sized for max quads - int32_t vboSize = MAX_QUADS * VERTICES_PER_QUAD * FLOATS_PER_VERTEX * (int32_t) sizeof(float); - glBindBuffer(GL_ARRAY_BUFFER, gl->vbo); - glBufferData(GL_ARRAY_BUFFER, vboSize, nullptr, GL_DYNAMIC_DRAW); - - // EBO: pre-fill with quad index pattern (0,1,2,2,3,0 repeated) - int32_t eboSize = MAX_QUADS * INDICES_PER_QUAD * (int32_t) sizeof(uint32_t); - uint32_t* indices = safeMalloc(eboSize); - for (int32_t i = 0; MAX_QUADS > i; i++) { - uint32_t base = (uint32_t) i * 4; - indices[i * 6 + 0] = base + 0; - indices[i * 6 + 1] = base + 1; - indices[i * 6 + 2] = base + 2; - indices[i * 6 + 3] = base + 2; - indices[i * 6 + 4] = base + 3; - indices[i * 6 + 5] = base + 0; - } - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gl->ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, eboSize, indices, GL_STATIC_DRAW); - free(indices); - - // Vertex attributes: pos(2f), texcoord(2f), color(4f) - int32_t stride = FLOATS_PER_VERTEX * (int32_t) sizeof(float); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, stride, (void*) 0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*) (2 * sizeof(float))); - glEnableVertexAttribArray(1); - glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, stride, (void*) (4 * sizeof(float))); - glEnableVertexAttribArray(2); - - glBindVertexArray(0); - - // Allocate CPU-side vertex buffer - gl->vertexData = safeMalloc(MAX_QUADS * VERTICES_PER_QUAD * FLOATS_PER_VERTEX * sizeof(float)); - - // Prepare texture slots for lazy loading (PNG decode deferred to first use) - gl->textureCount = dataWin->txtr.count; - gl->glTextures = safeMalloc(gl->textureCount * sizeof(GLuint)); - gl->textureWidths = safeMalloc(gl->textureCount * sizeof(int32_t)); - gl->textureHeights = safeMalloc(gl->textureCount * sizeof(int32_t)); - gl->textureLoaded = safeMalloc(gl->textureCount * sizeof(bool)); - - glGenTextures((GLsizei) gl->textureCount, gl->glTextures); - - for (uint32_t i = 0; gl->textureCount > i; i++) { - gl->textureWidths[i] = 0; - gl->textureHeights[i] = 0; - gl->textureLoaded[i] = false; - } - - // Create 1x1 white pixel texture for primitive drawing (rectangles, lines, etc.) - glGenTextures(1, &gl->whiteTexture); - glBindTexture(GL_TEXTURE_2D, gl->whiteTexture); - uint8_t whitePixel[4] = {255, 255, 255, 255}; - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - // Enable blending - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - gl->quadCount = 0; - gl->currentTextureId = 0; - - // Create FBO (texture will be allocated/resized in beginFrame) - glGenFramebuffers(1, &gl->fbo); - gl->fboTexture = 0; - gl->fboWidth = 0; - gl->fboHeight = 0; - - // Save original counts so we know which slots are from data.win vs dynamic - gl->originalTexturePageCount = gl->textureCount; - gl->originalTpagCount = dataWin->tpag.count; - gl->originalSpriteCount = dataWin->sprt.count; - - fprintf(stderr, "GL: Renderer initialized (%u texture pages)\n", gl->textureCount); -} - -static void glDestroy(Renderer* renderer) { - GLRenderer* gl = (GLRenderer*) renderer; - - if (gl->fboTexture != 0) glDeleteTextures(1, &gl->fboTexture); - glDeleteFramebuffers(1, &gl->fbo); - glDeleteTextures(1, &gl->whiteTexture); - - glDeleteTextures((GLsizei) gl->textureCount, gl->glTextures); - glDeleteProgram(gl->shaderProgram); - glDeleteVertexArrays(1, &gl->vao); - glDeleteBuffers(1, &gl->vbo); - glDeleteBuffers(1, &gl->ebo); - - free(gl->glTextures); - free(gl->textureWidths); - free(gl->textureHeights); - free(gl->textureLoaded); - free(gl->vertexData); - free(gl); -} - -static void glBeginFrame(Renderer* renderer, int32_t gameW, int32_t gameH, int32_t windowW, int32_t windowH) { - GLRenderer* gl = (GLRenderer*) renderer; - - gl->quadCount = 0; - gl->currentTextureId = 0; - gl->windowW = windowW; - gl->windowH = windowH; - gl->gameW = gameW; - gl->gameH = gameH; - - // Resize FBO to game resolution if needed - if (gameW != gl->fboWidth || gameH != gl->fboHeight) { - if (gl->fboTexture != 0) glDeleteTextures(1, &gl->fboTexture); - - glGenTextures(1, &gl->fboTexture); - glBindTexture(GL_TEXTURE_2D, gl->fboTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gameW, gameH, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - glBindFramebuffer(GL_FRAMEBUFFER, gl->fbo); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gl->fboTexture, 0); - - GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) { - fprintf(stderr, "GL: Framebuffer incomplete (status=0x%X)\n", status); - } - - gl->fboWidth = gameW; - gl->fboHeight = gameH; - fprintf(stderr, "GL: FBO resized to %dx%d\n", gameW, gameH); - } - - // Bind FBO and clear - glBindFramebuffer(GL_FRAMEBUFFER, gl->fbo); - glViewport(0, 0, gameW, gameH); -} - -static void glBeginView(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, int32_t portX, int32_t portY, int32_t portW, int32_t portH, float viewAngle) { - GLRenderer* gl = (GLRenderer*) renderer; - - gl->quadCount = 0; - gl->currentTextureId = 0; - - // Set viewport and scissor to the port rectangle within the FBO - // FBO uses game resolution, port coordinates are in game space - // OpenGL viewport Y is bottom-up, game Y is top-down - int32_t glPortY = gl->gameH - portY - portH; - glViewport(portX, glPortY, portW, portH); - glEnable(GL_SCISSOR_TEST); - glScissor(portX, glPortY, portW, portH); - - // Build orthographic projection (Y-down for GML coordinate system) - Matrix4f projection; - Matrix4f_identity(&projection); - Matrix4f_ortho(&projection, (float) viewX, (float) (viewX + viewW), (float) (viewY + viewH), (float) viewY, -1.0f, 1.0f); - - if (viewAngle != 0.0f) { - // GML view_angle: rotate camera by this angle (degrees, counter-clockwise) - // To rotate the camera, we rotate the world in the opposite direction around the view center - float cx = (float) viewX + (float) viewW / 2.0f; - float cy = (float) viewY + (float) viewH / 2.0f; - Matrix4f rot; - Matrix4f_identity(&rot); - Matrix4f_translate(&rot, cx, cy, 0.0f); - float angleRad = viewAngle * (float) M_PI / 180.0f; - Matrix4f_rotateZ(&rot, -angleRad); - Matrix4f_translate(&rot, -cx, -cy, 0.0f); - Matrix4f result; - Matrix4f_multiply(&result, &projection, &rot); - projection = result; - } - - glUseProgram(gl->shaderProgram); - glUniformMatrix4fv(gl->uProjection, 1, GL_FALSE, projection.m); - glUniform1i(gl->uTexture, 0); - glActiveTexture(GL_TEXTURE0); - - glBindVertexArray(gl->vao); -} - -static void glEndView(Renderer* renderer) { - GLRenderer* gl = (GLRenderer*) renderer; - flushBatch(gl); - glDisable(GL_SCISSOR_TEST); -} - -static void glBeginGUI(Renderer* renderer, int32_t guiW, int32_t guiH, int32_t portX, int32_t portY, int32_t portW, int32_t portH) { - GLRenderer* gl = (GLRenderer*) renderer; - - gl->quadCount = 0; - gl->currentTextureId = 0; - - int32_t glPortY = gl->gameH - portY - portH; - glViewport(portX, glPortY, portW, portH); - glEnable(GL_SCISSOR_TEST); - glScissor(portX, glPortY, portW, portH); - - Matrix4f projection; - Matrix4f_identity(&projection); - Matrix4f_ortho(&projection, 0.0f, (float) guiW, (float) guiH, 0.0f, -1.0f, 1.0f); - - glUseProgram(gl->shaderProgram); - glUniformMatrix4fv(gl->uProjection, 1, GL_FALSE, projection.m); - glUniform1i(gl->uTexture, 0); - glActiveTexture(GL_TEXTURE0); - - glBindVertexArray(gl->vao); -} - -static void glEndGUI(Renderer* renderer) { - GLRenderer* gl = (GLRenderer*) renderer; - flushBatch(gl); - glDisable(GL_SCISSOR_TEST); -} - -static void glEndFrame(Renderer* renderer) { - GLRenderer* gl = (GLRenderer*) renderer; - glBindVertexArray(0); - - int effectiveEndX, effectiveEndY; - int effectiveStartX, effectiveStartY; - - // Try and match the "intended" aspect ratio as closely - // as possible while still fitting on the screen - if ((gl->gameW * gl->windowH) / gl->gameH < gl->windowW) { - effectiveEndX = (gl->gameW * gl->windowH) / gl->gameH; - effectiveEndY = gl->windowH; - } else { - effectiveEndX = gl->windowW; - effectiveEndY = (gl->gameH * gl->windowW) / gl->gameW; - } - effectiveStartX = (gl->windowW - effectiveEndX) / 2; - effectiveStartY = (gl->windowH - effectiveEndY) / 2; - effectiveEndX += effectiveStartX; - effectiveEndY += effectiveStartY; - - // Blit the full game-resolution FBO to the window - glBindFramebuffer(GL_READ_FRAMEBUFFER, gl->fbo); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - glBlitFramebuffer(0, 0, gl->fboWidth, gl->fboHeight, effectiveStartX, effectiveStartY, effectiveEndX, effectiveEndY, GL_COLOR_BUFFER_BIT, GL_NEAREST); - glBindFramebuffer(GL_FRAMEBUFFER, 0); -} - -static void glRendererFlush(Renderer* renderer) { - flushBatch((GLRenderer*) renderer); -} - -// Lazily decodes and uploads a TXTR page on first access. -// Returns true if the texture is ready, false if it failed to decode. -static bool ensureTextureLoaded(GLRenderer* gl, uint32_t pageId) { - if (gl->textureLoaded[pageId]) return (gl->textureWidths[pageId] != 0); - - gl->textureLoaded[pageId] = true; - - DataWin* dw = gl->base.dataWin; - Texture* txtr = &dw->txtr.textures[pageId]; - - int w, h; - bool gm2022_5 = DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0); - uint8_t* pixels = ImageDecoder_decodeToRgba(txtr->blobData, (size_t) txtr->blobSize, gm2022_5, &w, &h); - if (pixels == nullptr) { - fprintf(stderr, "GL: Failed to decode TXTR page %u\n", pageId); - return false; - } - - gl->textureWidths[pageId] = w; - gl->textureHeights[pageId] = h; - - glBindTexture(GL_TEXTURE_2D, gl->glTextures[pageId]); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - free(pixels); - fprintf(stderr, "GL: Loaded TXTR page %u (%dx%d)\n", pageId, w, h); - return true; -} - -// Resolves a TPAG index to a loaded GL texture. Returns false if drawing should be skipped. -static bool resolveSpriteTexture(GLRenderer* gl, int32_t tpagIndex, TexturePageItem** outTpag, GLuint* outTexId, int32_t* outTexW, int32_t* outTexH) { - DataWin* dw = gl->base.dataWin; - if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return false; - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - int16_t pageId = tpag->texturePageId; - if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return false; - if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return false; - *outTpag = tpag; - *outTexId = gl->glTextures[pageId]; - *outTexW = gl->textureWidths[pageId]; - *outTexH = gl->textureHeights[pageId]; - return true; -} - -// Emits a single textured quad into the batch given 4 final screen-space corners (TL, TR, BR, BL), 4 UVs forming a rect (u0,v0)-(u1,v1), and a flat color/alpha. -// Handles texture rebinding and batch flushing. -static void emitTexturedQuad(GLRenderer* gl, GLuint texId, float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, float u0, float v0, float u1, float v1, float r, float g, float b, float alpha) { - if (gl->quadCount > 0 && gl->currentTextureId != texId) flushBatch(gl); - if (gl->quadCount >= MAX_QUADS) flushBatch(gl); - gl->currentTextureId = texId; - - float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX; - - // Vertex 0: top-left - verts[0] = x0; verts[1] = y0; verts[2] = u0; verts[3] = v0; - verts[4] = r; verts[5] = g; verts[6] = b; verts[7] = alpha; - - // Vertex 1: top-right - verts[8] = x1; verts[9] = y1; verts[10] = u1; verts[11] = v0; - verts[12] = r; verts[13] = g; verts[14] = b; verts[15] = alpha; - - // Vertex 2: bottom-right - verts[16] = x2; verts[17] = y2; verts[18] = u1; verts[19] = v1; - verts[20] = r; verts[21] = g; verts[22] = b; verts[23] = alpha; - - // Vertex 3: bottom-left - verts[24] = x3; verts[25] = y3; verts[26] = u0; verts[27] = v1; - verts[28] = r; verts[29] = g; verts[30] = b; verts[31] = alpha; - - gl->quadCount++; -} - -static void glDrawSprite(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) { - GLRenderer* gl = (GLRenderer*) renderer; - TexturePageItem* tpag; - GLuint texId; - int32_t texW, texH; - if (!resolveSpriteTexture(gl, tpagIndex, &tpag, &texId, &texW, &texH)) return; - - // Compute normalized UVs from TPAG source rect - float u0 = (float) tpag->sourceX / (float) texW; - float v0 = (float) tpag->sourceY / (float) texH; - float u1 = (float) (tpag->sourceX + tpag->sourceWidth) / (float) texW; - float v1 = (float) (tpag->sourceY + tpag->sourceHeight) / (float) texH; - - // Compute local quad corners (relative to origin, with target offset) - float localX0 = (float) tpag->targetX - originX; - float localY0 = (float) tpag->targetY - originY; - float localX1 = localX0 + (float) tpag->sourceWidth; - float localY1 = localY0 + (float) tpag->sourceHeight; - - // Build 2D transform: T(x,y) * R(-angleDeg) * S(xscale, yscale) - // GML rotation is counter-clockwise, OpenGL rotation is counter-clockwise, but - // since we have Y-down, we negate the angle to get the correct visual rotation - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - Matrix4f transform; - Matrix4f_setTransform2D(&transform, x, y, xscale, yscale, angleRad); - - // Transform 4 corners - float x0, y0, x1, y1, x2, y2, x3, y3; - Matrix4f_transformPoint(&transform, localX0, localY0, &x0, &y0); // top-left - Matrix4f_transformPoint(&transform, localX1, localY0, &x1, &y1); // top-right - Matrix4f_transformPoint(&transform, localX1, localY1, &x2, &y2); // bottom-right - Matrix4f_transformPoint(&transform, localX0, localY1, &x3, &y3); // bottom-left - - // Convert BGR color to RGB floats - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - emitTexturedQuad(gl, texId, x0, y0, x1, y1, x2, y2, x3, y3, u0, v0, u1, v1, r, g, b, alpha); -} - -static void glDrawSpritePart(Renderer* renderer, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { - GLRenderer* gl = (GLRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return; - - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - int16_t pageId = tpag->texturePageId; - if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return; - if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return; - - GLuint texId = gl->glTextures[pageId]; - int32_t texW = gl->textureWidths[pageId]; - int32_t texH = gl->textureHeights[pageId]; - - // Compute UVs for the sub-region within the atlas - float u0 = (float) (tpag->sourceX + srcOffX) / (float) texW; - float v0 = (float) (tpag->sourceY + srcOffY) / (float) texH; - float u1 = (float) (tpag->sourceX + srcOffX + srcW) / (float) texW; - float v1 = (float) (tpag->sourceY + srcOffY + srcH) / (float) texH; - - // Convert BGR color to RGB floats - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - // Quad corners (no origin offset - draw_sprite_part ignores sprite origin) - float cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3; - if (angleDeg == 0.0f) { - cx0 = x; cy0 = y; - cx1 = x + (float) srcW * xscale; cy1 = y; - cx2 = x + (float) srcW * xscale; cy2 = y + (float) srcH * yscale; - cx3 = x; cy3 = y + (float) srcH * yscale; - } else { - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - float cosA = cosf(angleRad); - float sinA = sinf(angleRad); - float qx0 = x, qy0 = y; - float qx1 = x + (float) srcW * xscale, qy1 = y; - float qx2 = x + (float) srcW * xscale, qy2 = y + (float) srcH * yscale; - float qx3 = x, qy3 = y + (float) srcH * yscale; - float dx, dy; - dx = qx0 - pivotX; dy = qy0 - pivotY; cx0 = cosA * dx - sinA * dy + pivotX; cy0 = sinA * dx + cosA * dy + pivotY; - dx = qx1 - pivotX; dy = qy1 - pivotY; cx1 = cosA * dx - sinA * dy + pivotX; cy1 = sinA * dx + cosA * dy + pivotY; - dx = qx2 - pivotX; dy = qy2 - pivotY; cx2 = cosA * dx - sinA * dy + pivotX; cy2 = sinA * dx + cosA * dy + pivotY; - dx = qx3 - pivotX; dy = qy3 - pivotY; cx3 = cosA * dx - sinA * dy + pivotX; cy3 = sinA * dx + cosA * dy + pivotY; - } - - emitTexturedQuad(gl, texId, cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3, u0, v0, u1, v1, r, g, b, alpha); -} - -static void glDrawSpritePos(Renderer* renderer, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) { - GLRenderer* gl = (GLRenderer*) renderer; - TexturePageItem* tpag; - GLuint texId; - int32_t texW, texH; - if (!resolveSpriteTexture(gl, tpagIndex, &tpag, &texId, &texW, &texH)) return; - - float u0 = (float) tpag->sourceX / (float) texW; - float v0 = (float) tpag->sourceY / (float) texH; - float u1 = (float) (tpag->sourceX + tpag->sourceWidth) / (float) texW; - float v1 = (float) (tpag->sourceY + tpag->sourceHeight) / (float) texH; - - emitTexturedQuad(gl, texId, x1, y1, x2, y2, x3, y3, x4, y4, u0, v0, u1, v1, 1.0f, 1.0f, 1.0f, alpha); -} - -// Emits a single colored quad into the batch using the white pixel texture -static void emitColoredQuad(GLRenderer* gl, float x0, float y0, float x1, float y1, float r, float g, float b, float a) { - // Flush if texture changed or batch full - if (gl->quadCount > 0 && gl->currentTextureId != gl->whiteTexture) flushBatch(gl); - if (gl->quadCount >= MAX_QUADS) flushBatch(gl); - - gl->currentTextureId = gl->whiteTexture; - - float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX; - - // All UVs point to (0.5, 0.5) center of the 1x1 white texture - // Vertex 0: top-left - verts[0] = x0; verts[1] = y0; verts[2] = 0.5f; verts[3] = 0.5f; - verts[4] = r; verts[5] = g; verts[6] = b; verts[7] = a; - - // Vertex 1: top-right - verts[8] = x1; verts[9] = y0; verts[10] = 0.5f; verts[11] = 0.5f; - verts[12] = r; verts[13] = g; verts[14] = b; verts[15] = a; - - // Vertex 2: bottom-right - verts[16] = x1; verts[17] = y1; verts[18] = 0.5f; verts[19] = 0.5f; - verts[20] = r; verts[21] = g; verts[22] = b; verts[23] = a; - - // Vertex 3: bottom-left - verts[24] = x0; verts[25] = y1; verts[26] = 0.5f; verts[27] = 0.5f; - verts[28] = r; verts[29] = g; verts[30] = b; verts[31] = a; - - gl->quadCount++; -} - -static void glDrawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline) { - GLRenderer* gl = (GLRenderer*) renderer; - - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - if (outline) { - // Draw 4 one-pixel-wide edges: top, bottom, left, right - emitColoredQuad(gl, x1, y1, x2 + 1, y1 + 1, r, g, b, alpha); // top - emitColoredQuad(gl, x1, y2, x2 + 1, y2 + 1, r, g, b, alpha); // bottom - emitColoredQuad(gl, x1, y1 + 1, x1 + 1, y2, r, g, b, alpha); // left - emitColoredQuad(gl, x2, y1 + 1, x2 + 1, y2, r, g, b, alpha); // right - } else { - // Filled rectangle: GML adds +1 to width/height for filled rects - emitColoredQuad(gl, x1, y1, x2 + 1, y2 + 1, r, g, b, alpha); - } -} - -// ===[ Line Drawing ]=== - -static void glDrawLine(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color, float alpha) { - GLRenderer* gl = (GLRenderer*) renderer; - - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - // Compute perpendicular offset for line thickness - float dx = x2 - x1; - float dy = y2 - y1; - float len = sqrtf(dx * dx + dy * dy); - if (0.0001f > len) return; - - float halfW = width * 0.5f; - float px = (-dy / len) * halfW; - float py = (dx / len) * halfW; - - // Emit quad as 4 vertices forming a rectangle along the line - if (gl->quadCount > 0 && gl->currentTextureId != gl->whiteTexture) { - flushBatch(gl); - } - if (gl->quadCount >= MAX_QUADS) { - flushBatch(gl); - } - gl->currentTextureId = gl->whiteTexture; - - float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX; - - // Vertex 0: start + perpendicular - verts[0] = x1 + px; verts[1] = y1 + py; verts[2] = 0.5f; verts[3] = 0.5f; - verts[4] = r; verts[5] = g; verts[6] = b; verts[7] = alpha; - - // Vertex 1: start - perpendicular - verts[8] = x1 - px; verts[9] = y1 - py; verts[10] = 0.5f; verts[11] = 0.5f; - verts[12] = r; verts[13] = g; verts[14] = b; verts[15] = alpha; - - // Vertex 2: end - perpendicular - verts[16] = x2 - px; verts[17] = y2 - py; verts[18] = 0.5f; verts[19] = 0.5f; - verts[20] = r; verts[21] = g; verts[22] = b; verts[23] = alpha; - - // Vertex 3: end + perpendicular - verts[24] = x2 + px; verts[25] = y2 + py; verts[26] = 0.5f; verts[27] = 0.5f; - verts[28] = r; verts[29] = g; verts[30] = b; verts[31] = alpha; - - gl->quadCount++; -} - -static void glDrawLineColor(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, uint32_t color2, float alpha) { - GLRenderer* gl = (GLRenderer*) renderer; - - float r1 = (float) BGR_R(color1) / 255.0f; - float g1 = (float) BGR_G(color1) / 255.0f; - float b1 = (float) BGR_B(color1) / 255.0f; - - float r2 = (float) BGR_R(color2) / 255.0f; - float g2 = (float) BGR_G(color2) / 255.0f; - float b2 = (float) BGR_B(color2) / 255.0f; - - // Compute perpendicular offset for line thickness - float dx = x2 - x1; - float dy = y2 - y1; - float len = sqrtf(dx * dx + dy * dy); - if (0.0001f > len) return; - - float halfW = width * 0.5f; - float px = (-dy / len) * halfW; - float py = (dx / len) * halfW; - - // Emit quad with per-vertex colors (color1 at start, color2 at end) - if (gl->quadCount > 0 && gl->currentTextureId != gl->whiteTexture) { - flushBatch(gl); - } - if (gl->quadCount >= MAX_QUADS) { - flushBatch(gl); - } - gl->currentTextureId = gl->whiteTexture; - - float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX; - - // Vertex 0: start + perpendicular (color1) - verts[0] = x1 + px; verts[1] = y1 + py; verts[2] = 0.5f; verts[3] = 0.5f; - verts[4] = r1; verts[5] = g1; verts[6] = b1; verts[7] = alpha; - - // Vertex 1: start - perpendicular (color1) - verts[8] = x1 - px; verts[9] = y1 - py; verts[10] = 0.5f; verts[11] = 0.5f; - verts[12] = r1; verts[13] = g1; verts[14] = b1; verts[15] = alpha; - - // Vertex 2: end - perpendicular (color2) - verts[16] = x2 - px; verts[17] = y2 - py; verts[18] = 0.5f; verts[19] = 0.5f; - verts[20] = r2; verts[21] = g2; verts[22] = b2; verts[23] = alpha; - - // Vertex 3: end + perpendicular (color2) - verts[24] = x2 + px; verts[25] = y2 + py; verts[26] = 0.5f; verts[27] = 0.5f; - verts[28] = r2; verts[29] = g2; verts[30] = b2; verts[31] = alpha; - - gl->quadCount++; -} - -static void glDrawTriangle(Renderer *renderer, float x1, float y1, float x2, float y2, float x3, float y3, bool outline) -{ - GLRenderer* gl = (GLRenderer*) renderer; - if(outline) - { - glDrawLine(renderer, x1, y1, x2, y2, 1, renderer->drawColor, 1.0); - glDrawLine(renderer, x2, y2, x3, y3, 1, renderer->drawColor, 1.0); - glDrawLine(renderer, x3, y3, x1, y1, 1, renderer->drawColor, 1.0); - } else { - float r = (float) BGR_R(renderer->drawColor) / 255.0f; - float g = (float) BGR_G(renderer->drawColor) / 255.0f; - float b = (float) BGR_B(renderer->drawColor) / 255.0f; - - flushBatch(gl); - - int i = 0; - float verts[24] = { - x1, y1, 0.0f, 0.0f, r, g, b, renderer->drawAlpha, - x2, y2, 0.0f, 0.0f, r, g, b, renderer->drawAlpha, - x3, y3, 0.0f, 0.0f, r, g, b, renderer->drawAlpha, - }; - - glBindBuffer(GL_ARRAY_BUFFER, gl->vbo); - glBufferSubData(GL_ARRAY_BUFFER, 0, 3 * FLOATS_PER_VERTEX * sizeof(float), verts); - - glBindTexture(GL_TEXTURE_2D, gl->whiteTexture); - glDrawArrays(GL_TRIANGLES, 0, 3); - } -} - -// ===[ Text Drawing ]=== - -// Resolved font state shared between glDrawText and glDrawTextColor -typedef struct { - Font* font; - TexturePageItem* fontTpag; // single TPAG for regular fonts (nullptr for sprite fonts) - GLuint texId; - int32_t texW, texH; - Sprite* spriteFontSprite; // source sprite for sprite fonts (nullptr for regular fonts) -} GlFontState; - -// Resolves font texture state -// Returns false if the font can't be drawn -static bool glResolveFontState(GLRenderer* gl, DataWin* dw, Font* font, GlFontState* state) { - state->font = font; - state->fontTpag = nullptr; - state->texId = 0; - state->texW = 0; - state->texH = 0; - state->spriteFontSprite = nullptr; - - if (!font->isSpriteFont) { - int32_t fontTpagIndex = font->tpagIndex; - if (0 > fontTpagIndex) return false; - - state->fontTpag = &dw->tpag.items[fontTpagIndex]; - int16_t pageId = state->fontTpag->texturePageId; - if (0 > pageId || (uint32_t) pageId >= gl->textureCount) return false; - if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return false; - - state->texId = gl->glTextures[pageId]; - state->texW = gl->textureWidths[pageId]; - state->texH = gl->textureHeights[pageId]; - } else if (font->spriteIndex >= 0 && dw->sprt.count > (uint32_t) font->spriteIndex) { - state->spriteFontSprite = &dw->sprt.sprites[font->spriteIndex]; - } - return true; -} - -// Resolves UV coordinates, texture ID, and local position for a single glyph -// Returns false if the glyph can't be drawn -static bool glResolveGlyph(GLRenderer* gl, DataWin* dw, GlFontState* state, FontGlyph* glyph, float cursorX, float cursorY, GLuint* outTexId, float* outU0, float* outV0, float* outU1, float* outV1, float* outLocalX0, float* outLocalY0) { - Font* font = state->font; - if (font->isSpriteFont && state->spriteFontSprite != nullptr) { - Sprite* sprite = state->spriteFontSprite; - int32_t glyphIndex = (int32_t) (glyph - font->glyphs); - if (0 > glyphIndex || glyphIndex >= (int32_t) sprite->textureCount) return false; - - int32_t tpagIdx = sprite->tpagIndices[glyphIndex]; - if (0 > tpagIdx) return false; - - TexturePageItem* glyphTpag = &dw->tpag.items[tpagIdx]; - int16_t pid = glyphTpag->texturePageId; - if (0 > pid || (uint32_t) pid >= gl->textureCount) return false; - if (!ensureTextureLoaded(gl, (uint32_t) pid)) return false; - - *outTexId = gl->glTextures[pid]; - int32_t tw = gl->textureWidths[pid]; - int32_t th = gl->textureHeights[pid]; - - *outU0 = (float) glyphTpag->sourceX / (float) tw; - *outV0 = (float) glyphTpag->sourceY / (float) th; - *outU1 = (float) (glyphTpag->sourceX + glyphTpag->sourceWidth) / (float) tw; - *outV1 = (float) (glyphTpag->sourceY + glyphTpag->sourceHeight) / (float) th; - - *outLocalX0 = cursorX + (float) glyph->offset; - *outLocalY0 = cursorY + (float) ((int32_t) glyphTpag->targetY - sprite->originY); - } else { - *outTexId = state->texId; - *outU0 = (float) (state->fontTpag->sourceX + glyph->sourceX) / (float) state->texW; - *outV0 = (float) (state->fontTpag->sourceY + glyph->sourceY) / (float) state->texH; - *outU1 = (float) (state->fontTpag->sourceX + glyph->sourceX + glyph->sourceWidth) / (float) state->texW; - *outV1 = (float) (state->fontTpag->sourceY + glyph->sourceY + glyph->sourceHeight) / (float) state->texH; - - *outLocalX0 = cursorX + glyph->offset; - *outLocalY0 = cursorY; - } - return true; -} - -static void glDrawText(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg) { - GLRenderer* gl = (GLRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - int32_t fontIndex = renderer->drawFont; - if (0 > fontIndex || dw->font.count <= (uint32_t) fontIndex) return; - - Font* font = &dw->font.fonts[fontIndex]; - - GlFontState fontState; - if (!glResolveFontState(gl, dw, font, &fontState)) return; - - uint32_t color = renderer->drawColor; - float alpha = renderer->drawAlpha; - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - int32_t textLen = (int32_t) strlen(text); - - // Count lines, treating \r\n and \n\r as single breaks - int32_t lineCount = TextUtils_countLines(text, textLen); - - // Per-line vertical stride. HTML5 runner's default `linesep` is `max_glyph_height * scaleY`. - // We apply scaleY via the transform matrix below, so keep the stride in pre-scale (local) coords. - float lineStride = TextUtils_lineStride(font); - - // Vertical alignment offset - float totalHeight = (float) lineCount * lineStride; - float valignOffset = 0; - if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f; - else if (renderer->drawValign == 2) valignOffset = -totalHeight; - - // Build transform matrix - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - Matrix4f transform; - Matrix4f_setTransform2D(&transform, x, y, xscale * font->scaleX, yscale * font->scaleY, angleRad); - - // Iterate through lines. HTML5 subtracts ascenderOffset from the per-line y offset - // (see yyFont.GR_Text_Draw), shifting glyphs up so the baseline aligns with the drawn y. - float cursorY = valignOffset - (float) font->ascenderOffset; - int32_t lineStart = 0; - - for (int32_t lineIdx = 0; lineCount > lineIdx; lineIdx++) { - // Find end of current line - int32_t lineEnd = lineStart; - while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) { - lineEnd++; - } - int32_t lineLen = lineEnd - lineStart; - - // Horizontal alignment offset for this line - float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineLen); - float halignOffset = 0; - if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f; - else if (renderer->drawHalign == 2) halignOffset = -lineWidth; - - float cursorX = halignOffset; - - // Render each glyph in the line - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning) - int32_t pos = 0; - uint16_t ch = 0; - bool hasCh = false; - if (lineLen > pos) { - ch = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); - hasCh = true; - } - - while (hasCh) { - FontGlyph* glyph = TextUtils_findGlyph(font, ch); - - uint16_t nextCh = 0; - bool hasNext = lineLen > pos; - if (hasNext) nextCh = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); - - if (glyph != nullptr) { - bool drewSuccessfully = false; - if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) { - float u0, v0, u1, v1; - float localX0, localY0; - GLuint glyphTexId; - - if (glResolveGlyph(gl, dw, &fontState, glyph, cursorX, cursorY, &glyphTexId, &u0, &v0, &u1, &v1, &localX0, &localY0)) { - // Flush if texture changed or batch full - if (gl->quadCount > 0 && gl->currentTextureId != glyphTexId) flushBatch(gl); - if (gl->quadCount >= MAX_QUADS) flushBatch(gl); - gl->currentTextureId = glyphTexId; - - float localX1 = localX0 + (float) glyph->sourceWidth; - float localY1 = localY0 + (float) glyph->sourceHeight; - - // Transform corners - float px0, py0, px1, py1, px2, py2, px3, py3; - Matrix4f_transformPoint(&transform, localX0, localY0, &px0, &py0); - Matrix4f_transformPoint(&transform, localX1, localY0, &px1, &py1); - Matrix4f_transformPoint(&transform, localX1, localY1, &px2, &py2); - Matrix4f_transformPoint(&transform, localX0, localY1, &px3, &py3); - - // Write 4 vertices - float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX; - - verts[0] = px0; verts[1] = py0; verts[2] = u0; verts[3] = v0; - verts[4] = r; verts[5] = g; verts[6] = b; verts[7] = alpha; - - verts[8] = px1; verts[9] = py1; verts[10] = u1; verts[11] = v0; - verts[12] = r; verts[13] = g; verts[14] = b; verts[15] = alpha; - - verts[16] = px2; verts[17] = py2; verts[18] = u1; verts[19] = v1; - verts[20] = r; verts[21] = g; verts[22] = b; verts[23] = alpha; - - verts[24] = px3; verts[25] = py3; verts[26] = u0; verts[27] = v1; - verts[28] = r; verts[29] = g; verts[30] = b; verts[31] = alpha; - - gl->quadCount++; - drewSuccessfully = true; - } - } - - cursorX += glyph->shift; - if (drewSuccessfully && hasNext) { - cursorX += TextUtils_getKerningOffset(glyph, nextCh); - } - } - - ch = nextCh; - hasCh = hasNext; - } - - cursorY += lineStride; - // Skip past the newline, treating \r\n and \n\r as single breaks - if (textLen > lineEnd) { - lineStart = TextUtils_skipNewline(text, lineEnd, textLen); - } else { - lineStart = lineEnd; - } - } -} - -static void glDrawTextColor(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg, int32_t _c1, int32_t _c2, int32_t _c3, int32_t _c4, float alpha) { - GLRenderer* gl = (GLRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - int32_t fontIndex = renderer->drawFont; - if (0 > fontIndex || dw->font.count <= (uint32_t) fontIndex) return; - - Font* font = &dw->font.fonts[fontIndex]; - - GlFontState fontState; - if (!glResolveFontState(gl, dw, font, &fontState)) return; - - int32_t textLen = (int32_t) strlen(text); - if(textLen == 0) return; - - // Count lines, treating \r\n and \n\r as single breaks - int32_t lineCount = TextUtils_countLines(text, textLen); - - float lineStride = TextUtils_lineStride(font); - - // Vertical alignment offset - float totalHeight = (float) lineCount * lineStride; - float valignOffset = 0; - if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f; - else if (renderer->drawValign == 2) valignOffset = -totalHeight; - - // Build transform matrix - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - Matrix4f transform; - Matrix4f_setTransform2D(&transform, x, y, xscale * font->scaleX, yscale * font->scaleY, angleRad); - - // Iterate through lines. HTML5 subtracts ascenderOffset from per-line y offset. - float cursorY = valignOffset - (float) font->ascenderOffset; - int32_t lineStart = 0; - - // get delta's (16.16 format) - int32_t left_r_dx = ((_c2 & 0xff0000) - (_c1 & 0xff0000)) / textLen; - int32_t left_g_dx = ((((_c2 & 0xff00) << 8) - ((_c1 & 0xff00) << 8))) / textLen; - int32_t left_b_dx = ((((_c2 & 0xff) << 16) - ((_c1 & 0xff) << 16))) / textLen; - - int32_t right_r_dx = ((_c3 & 0xff0000) - (_c4 & 0xff0000)) / textLen; - int32_t right_g_dx = ((((_c3 & 0xff00) << 8) - ((_c4 & 0xff00) << 8))) / textLen; - int32_t right_b_dx = ((((_c3 & 0xff) << 16) - ((_c4 & 0xff) << 16))) / textLen; - - int32_t left_delta_r = left_r_dx; - int32_t left_delta_g = left_g_dx; - int32_t left_delta_b = left_b_dx; - int32_t right_delta_r = right_r_dx; - int32_t right_delta_g = right_g_dx; - int32_t right_delta_b = right_b_dx; - - int32_t c1 = _c1; - int32_t c4 = _c4; - - for (int32_t lineIdx = 0; lineCount > lineIdx; lineIdx++) { - // Find end of current line - int32_t lineEnd = lineStart; - while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) { - lineEnd++; - } - int32_t lineLen = lineEnd - lineStart; - - // Horizontal alignment offset for this line - float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineLen); - float halignOffset = 0; - if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f; - else if (renderer->drawHalign == 2) halignOffset = -lineWidth; - - float cursorX = halignOffset; - - // Render each glyph in the line - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning) - int32_t pos = 0; - uint16_t ch = 0; - bool hasCh = false; - if (lineLen > pos) { - ch = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); - hasCh = true; - } - - while (hasCh) { - // do 16.16 maths - int32_t c2 = ((c1 & 0xff0000) + (left_delta_r & 0xff0000)) & 0xff0000; - c2 |= ((c1 & 0xff00) + (left_delta_g >> 8) & 0xff00) & 0xff00; - c2 |= ((c1 & 0xff) + (left_delta_b >> 16)) & 0xff; - int32_t c3 = ((c4 & 0xff0000) + (right_delta_r & 0xff0000)) & 0xff0000; - c3 |= ((c4 & 0xff00) + (right_delta_g >> 8) & 0xff00) & 0xff00; - c3 |= ((c4 & 0xff) + (right_delta_b >> 16)) & 0xff; - - left_delta_r += left_r_dx; - left_delta_g += left_g_dx; - left_delta_b += left_b_dx; - right_delta_r += right_r_dx; - right_delta_g += right_g_dx; - right_delta_b += right_b_dx; - - FontGlyph* glyph = TextUtils_findGlyph(font, ch); - - uint16_t nextCh = 0; - bool hasNext = lineLen > pos; - if (hasNext) nextCh = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); - - if (glyph != nullptr) { - bool drewSuccessfully = false; - if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) { - float u0, v0, u1, v1; - float localX0, localY0; - GLuint glyphTexId; - - if (glResolveGlyph(gl, dw, &fontState, glyph, cursorX, cursorY, &glyphTexId, &u0, &v0, &u1, &v1, &localX0, &localY0)) { - // Flush if texture changed or batch full - if (gl->quadCount > 0 && gl->currentTextureId != glyphTexId) flushBatch(gl); - if (gl->quadCount >= MAX_QUADS) flushBatch(gl); - gl->currentTextureId = glyphTexId; - - float localX1 = localX0 + (float) glyph->sourceWidth; - float localY1 = localY0 + (float) glyph->sourceHeight; - - // Transform corners - float px0, py0, px1, py1, px2, py2, px3, py3; - Matrix4f_transformPoint(&transform, localX0, localY0, &px0, &py0); - Matrix4f_transformPoint(&transform, localX1, localY0, &px1, &py1); - Matrix4f_transformPoint(&transform, localX1, localY1, &px2, &py2); - Matrix4f_transformPoint(&transform, localX0, localY1, &px3, &py3); - - // Write 4 vertices - float* verts = gl->vertexData + gl->quadCount * VERTICES_PER_QUAD * FLOATS_PER_VERTEX; - - // top left - verts[0] = px0; verts[1] = py0; verts[2] = u0; verts[3] = v0; - verts[4] = ((float) BGR_R(c1) / 255.0f); verts[5] = ((float) BGR_G(c1) / 255.0f); verts[6] = ((float) BGR_B(c1) / 255.0f); verts[7] = alpha; - - // top right - verts[8] = px1; verts[9] = py1; verts[10] = u1; verts[11] = v0; - verts[12] = ((float) BGR_R(c2) / 255.0f); verts[13] = ((float) BGR_G(c2) / 255.0f); verts[14] = ((float) BGR_B(c2) / 255.0f); verts[15] = alpha; - - // bottom right - verts[16] = px2; verts[17] = py2; verts[18] = u1; verts[19] = v1; - verts[20] = ((float) BGR_R(c3) / 255.0f); verts[21] = ((float) BGR_G(c3) / 255.0f); verts[22] = ((float) BGR_B(c3) / 255.0f); verts[23] = alpha; - - // bottom left - verts[24] = px3; verts[25] = py3; verts[26] = u0; verts[27] = v1; - verts[28] = ((float) BGR_R(c4) / 255.0f); verts[29] = ((float) BGR_G(c4) / 255.0f); verts[30] = ((float) BGR_B(c4) / 255.0f); verts[31] = alpha; - - gl->quadCount++; - drewSuccessfully = true; - } - } - - cursorX += glyph->shift; - if (drewSuccessfully) { - if (hasNext) cursorX += TextUtils_getKerningOffset(glyph, nextCh); - c4 = c3; // set left edge to be what the last right edge was.... - c1 = c2; - } - } - - ch = nextCh; - hasCh = hasNext; - } - - cursorY += lineStride; - // Skip past the newline, treating \r\n and \n\r as single breaks - if (textLen > lineEnd) { - lineStart = TextUtils_skipNewline(text, lineEnd, textLen); - } else { - lineStart = lineEnd; - } - } -} - -// ===[ Dynamic Sprite Creation/Deletion ]=== - -// Finds a free dynamic texture page slot (glTextures[i] == 0), or appends a new one. -static uint32_t findOrAllocTexturePageSlot(GLRenderer* gl) { - // Scan dynamic range for a reusable slot - for (uint32_t i = gl->originalTexturePageCount; gl->textureCount > i; i++) { - if (gl->glTextures[i] == 0) return i; - } - // No free slot found, grow the arrays - uint32_t newPageId = gl->textureCount; - gl->textureCount++; - gl->glTextures = safeRealloc(gl->glTextures, gl->textureCount * sizeof(GLuint)); - gl->textureWidths = safeRealloc(gl->textureWidths, gl->textureCount * sizeof(int32_t)); - gl->textureHeights = safeRealloc(gl->textureHeights, gl->textureCount * sizeof(int32_t)); - gl->textureLoaded = safeRealloc(gl->textureLoaded, gl->textureCount * sizeof(bool)); - gl->glTextures[newPageId] = 0; - gl->textureWidths[newPageId] = 0; - gl->textureHeights[newPageId] = 0; - gl->textureLoaded[newPageId] = false; - return newPageId; -} - -// Finds a free dynamic TPAG slot (texturePageId == -1), or appends a new one. -static uint32_t findOrAllocTpagSlot(DataWin* dw, uint32_t originalTpagCount) { - for (uint32_t i = originalTpagCount; dw->tpag.count > i; i++) { - if (dw->tpag.items[i].texturePageId == -1) return i; - } - uint32_t newIndex = dw->tpag.count; - dw->tpag.count++; - dw->tpag.items = safeRealloc(dw->tpag.items, dw->tpag.count * sizeof(TexturePageItem)); - memset(&dw->tpag.items[newIndex], 0, sizeof(TexturePageItem)); - dw->tpag.items[newIndex].texturePageId = -1; - return newIndex; -} - -static int32_t glCreateSpriteFromSurface(Renderer* renderer, int32_t x, int32_t y, int32_t w, int32_t h, bool removeback, bool smooth, int32_t xorig, int32_t yorig) { - GLRenderer* gl = (GLRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 >= w || 0 >= h) return -1; - - // Flush any pending draws before reading pixels - flushBatch(gl); - - // Read pixels from the FBO (application_surface) - glBindFramebuffer(GL_READ_FRAMEBUFFER, gl->fbo); - - uint8_t* pixels = safeMalloc((size_t) w * (size_t) h * 4); - if (pixels == nullptr) return -1; - - // OpenGL Y is bottom-up, GML Y is top-down, so flip the Y coordinate - int32_t glY = gl->fboHeight - y - h; - glReadPixels(x, glY, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - - // Flip vertically (OpenGL reads bottom-to-top) - size_t rowBytes = (size_t) w * 4; - uint8_t* rowTemp = safeMalloc(rowBytes); - repeat(h / 2, row) { - uint8_t* top = pixels + row * rowBytes; - uint8_t* bot = pixels + (h - 1 - row) * rowBytes; - memcpy(rowTemp, top, rowBytes); - memcpy(top, bot, rowBytes); - memcpy(bot, rowTemp, rowBytes); - } - free(rowTemp); - - // Create a new GL texture from the captured pixels - GLuint newTexId; - glGenTextures(1, &newTexId); - glBindTexture(GL_TEXTURE_2D, newTexId); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, smooth ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, smooth ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - free(pixels); - - // Find or allocate slots for texture page, TPAG, and sprite - uint32_t pageId = findOrAllocTexturePageSlot(gl); - gl->glTextures[pageId] = newTexId; - gl->textureWidths[pageId] = w; - gl->textureHeights[pageId] = h; - gl->textureLoaded[pageId] = true; - - uint32_t tpagIndex = findOrAllocTpagSlot(dw, gl->originalTpagCount); - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - tpag->sourceX = 0; - tpag->sourceY = 0; - tpag->sourceWidth = (uint16_t) w; - tpag->sourceHeight = (uint16_t) h; - tpag->targetX = 0; - tpag->targetY = 0; - tpag->targetWidth = (uint16_t) w; - tpag->targetHeight = (uint16_t) h; - tpag->boundingWidth = (uint16_t) w; - tpag->boundingHeight = (uint16_t) h; - tpag->texturePageId = (int16_t) pageId; - - uint32_t spriteIndex = DataWin_allocSpriteSlot(dw, gl->originalSpriteCount); - Sprite* sprite = &dw->sprt.sprites[spriteIndex]; - // name was set by DataWin_allocSpriteSlot ("__newsprite"); don't overwrite it here - sprite->width = (uint32_t) w; - sprite->height = (uint32_t) h; - sprite->originX = xorig; - sprite->originY = yorig; - sprite->textureCount = 1; - sprite->tpagIndices = safeMalloc(sizeof(int32_t)); - sprite->tpagIndices[0] = (int32_t) tpagIndex; - sprite->maskCount = 0; - sprite->masks = nullptr; - - fprintf(stderr, "GL: Created dynamic sprite %u (%dx%d) from surface at (%d,%d)\n", spriteIndex, w, h, x, y); - return (int32_t) spriteIndex; -} - -static void glDeleteSprite(Renderer* renderer, int32_t spriteIndex) { - GLRenderer* gl = (GLRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > spriteIndex || dw->sprt.count <= (uint32_t) spriteIndex) return; - - // Refuse to delete original data.win sprites - if (gl->originalSpriteCount > (uint32_t) spriteIndex) { - fprintf(stderr, "GL: Cannot delete data.win sprite %d\n", spriteIndex); - return; - } - - Sprite* sprite = &dw->sprt.sprites[spriteIndex]; - if (sprite->textureCount == 0) return; // already deleted - - // Clean up GL texture and TPAG entries owned by this sprite. - // Slots with index >= originalTpagCount are dynamically allocated and ours to free. - repeat(sprite->textureCount, i) { - int32_t tpagIdx = sprite->tpagIndices[i]; - if (tpagIdx >= 0 && (uint32_t) tpagIdx >= gl->originalTpagCount) { - TexturePageItem* tpag = &dw->tpag.items[tpagIdx]; - int16_t pageId = tpag->texturePageId; - if (pageId >= 0 && gl->textureCount > (uint32_t) pageId) { - glDeleteTextures(1, &gl->glTextures[pageId]); - gl->glTextures[pageId] = 0; - } - // Mark TPAG slot as free for reuse - tpag->texturePageId = -1; - } - } - - // Clear the sprite entry so it won't be drawn and can be reused. Preserve `name` across the memset: the slot is still in sprt.count and must keep a valid string for asset_get_index / name lookups. - free(sprite->tpagIndices); - const char* keepName = sprite->name; - memset(sprite, 0, sizeof(Sprite)); - sprite->name = keepName; - - fprintf(stderr, "GL: Deleted sprite %d\n", spriteIndex); -} - -static GLenum gmsBlendModeToGL(int mode) { - switch(mode) { - case bm_zero: return GL_ZERO; - case bm_one: return GL_ONE; - case bm_src_color: return GL_SRC_COLOR; - case bm_inv_src_color: return GL_ONE_MINUS_SRC_COLOR; - case bm_src_alpha: return GL_SRC_ALPHA; - case bm_inv_src_alpha: return GL_ONE_MINUS_SRC_ALPHA; - case bm_dest_alpha: return GL_DST_ALPHA; - case bm_inv_dest_alpha: return GL_ONE_MINUS_DST_ALPHA; - case bm_dest_color: return GL_DST_COLOR; - case bm_inv_dest_color: return GL_ONE_MINUS_DST_COLOR; - case bm_src_alpha_sat: return GL_SRC_ALPHA_SATURATE; - } - return GL_ONE; -} - -static GLenum gmsBlendModeToGLEquation(int mode) { - switch (mode) { - case bm_normal: - return GL_FUNC_ADD; - case bm_add: - return GL_FUNC_ADD; - case bm_subtract: - return GL_FUNC_ADD; - case bm_reverse_subtract: - return GL_FUNC_REVERSE_SUBTRACT; - case bm_min: - return GL_MIN; - case bm_max: - return GL_FUNC_ADD; - default: - return GL_FUNC_ADD; - } -} - -static GLenum gmsBlendModeToGLSFactor(int mode) { - switch (mode) { - case bm_normal: - return GL_SRC_ALPHA; - case bm_add: - return GL_SRC_ALPHA; - case bm_subtract: - return GL_ZERO; - case bm_reverse_subtract: - return GL_SRC_ALPHA; - case bm_min: - return GL_ONE; - case bm_max: - return GL_SRC_ALPHA; - default: - return gmsBlendModeToGL(mode); - } -} - -static GLenum gmsBlendModeToGLDFactor(int mode) { - switch (mode) { - case bm_normal: - return GL_ONE_MINUS_SRC_ALPHA; - case bm_add: - return GL_ONE; - case bm_subtract: - return GL_ONE_MINUS_SRC_COLOR; - case bm_reverse_subtract: - return GL_ONE; - case bm_min: - return GL_ONE; - case bm_max: - return GL_ONE_MINUS_SRC_COLOR; - default: - return gmsBlendModeToGL(mode); - } -} - -static void glGpuSetBlendMode(Renderer* renderer, int32_t mode) { - flushBatch((GLRenderer*)renderer); - glBlendEquation( - gmsBlendModeToGLEquation(mode) - ); - glBlendFunc( - gmsBlendModeToGLSFactor(mode), - gmsBlendModeToGLDFactor(mode) - ); -} - -static void glGpuSetBlendModeExt(Renderer* renderer, int32_t sfactor, int32_t dfactor) { - flushBatch((GLRenderer*)renderer); - glBlendFunc( - gmsBlendModeToGLSFactor(sfactor), - gmsBlendModeToGLDFactor(dfactor) - ); -} - -static void glGpuSetBlendEnable(Renderer* renderer, bool enable) { - flushBatch((GLRenderer*)renderer); - enable ? glEnable(GL_BLEND) : glDisable(GL_BLEND); -} - -static void glGpuSetAlphaTestEnable(Renderer* renderer, bool enable) { - GLRenderer* gl = (GLRenderer*) renderer; - if (gl->alphaTestEnable == enable) return; - flushBatch(gl); - gl->alphaTestEnable = enable; - glUseProgram(gl->shaderProgram); - glUniform1f(gl->uAlphaTestRef, enable ? gl->alphaTestRef : -1.0f); -} - -static void glGpuSetAlphaTestRef(Renderer* renderer, uint8_t ref) { - GLRenderer* gl = (GLRenderer*) renderer; - float refF = ref / 255.0f; - if (gl->alphaTestRef == refF) return; - flushBatch(gl); - gl->alphaTestRef = refF; - if (gl->alphaTestEnable) { - glUseProgram(gl->shaderProgram); - glUniform1f(gl->uAlphaTestRef, refF); - } -} - -static void glGpuSetColorWriteEnable(Renderer* renderer, bool red, bool green, bool blue, bool alpha) { - flushBatch((GLRenderer*)renderer); - glColorMask(red, green, blue, alpha); -} - -// ===[ Vtable ]=== - -static RendererVtable glVtable = { - .init = glInit, - .destroy = glDestroy, - .beginFrame = glBeginFrame, - .endFrame = glEndFrame, - .beginView = glBeginView, - .endView = glEndView, - .beginGUI = glBeginGUI, - .endGUI = glEndGUI, - .drawSprite = glDrawSprite, - .drawSpritePos = glDrawSpritePos, - .drawSpritePart = glDrawSpritePart, - .drawRectangle = glDrawRectangle, - .drawLine = glDrawLine, - .drawLineColor = glDrawLineColor, - .drawTriangle = glDrawTriangle, - .drawText = glDrawText, - .drawTextColor = glDrawTextColor, - .flush = glRendererFlush, - .createSpriteFromSurface = glCreateSpriteFromSurface, - .deleteSprite = glDeleteSprite, - .gpuSetBlendMode = glGpuSetBlendMode, - .gpuSetBlendModeExt = glGpuSetBlendModeExt, - .gpuSetBlendEnable = glGpuSetBlendEnable, - .gpuSetAlphaTestEnable = glGpuSetAlphaTestEnable, - .gpuSetAlphaTestRef = glGpuSetAlphaTestRef, - .gpuSetColorWriteEnable = glGpuSetColorWriteEnable, - .drawTile = nullptr, -}; - -// ===[ Public API ]=== - -Renderer* GLRenderer_create(void) { - GLRenderer* gl = safeCalloc(1, sizeof(GLRenderer)); - gl->base.vtable = &glVtable; - gl->base.drawColor = 0xFFFFFF; // white (BGR) - gl->base.drawAlpha = 1.0f; - gl->base.drawFont = -1; - gl->base.drawHalign = 0; - gl->base.drawValign = 0; - return (Renderer*) gl; -} diff --git a/src/gl/gl_renderer.h b/src/gl/gl_renderer.h deleted file mode 100644 index 02b944cc..00000000 --- a/src/gl/gl_renderer.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "common.h" -#include "renderer.h" -#include - -// ===[ GLRenderer Struct ]=== -// Exposed in the header so platform-specific code (main.c) can access FBO fields for screenshots. -typedef struct { - Renderer base; // Must be first field for struct embedding - - GLuint shaderProgram; - GLint uProjection; - GLint uTexture; - GLint uAlphaTestRef; - - bool alphaTestEnable; - float alphaTestRef; - - GLuint vao, vbo, ebo; - float* vertexData; // MAX_QUADS * VERTICES_PER_QUAD * FLOATS_PER_VERTEX floats - - int32_t quadCount; - GLuint currentTextureId; - - GLuint* glTextures; // one GL texture per TXTR page - int32_t* textureWidths; // needed for UV normalization - int32_t* textureHeights; - bool* textureLoaded; // lazy loading: true once PNG decoded and uploaded - uint32_t textureCount; - - GLuint whiteTexture; // 1x1 white pixel for drawing primitives (rectangles, lines, etc.) - - // FBO for render-to-texture (game renders here, then blitted to screen) - GLuint fbo; - GLuint fboTexture; - int32_t fboWidth; - int32_t fboHeight; - int32_t windowW; // stored from beginFrame for endFrame blit - int32_t windowH; - int32_t gameW; // game resolution (for FBO sizing) - int32_t gameH; - - // Original counts from data.win (dynamic slots start at these indices) - uint32_t originalTexturePageCount; - uint32_t originalTpagCount; - uint32_t originalSpriteCount; -} GLRenderer; - -Renderer* GLRenderer_create(void); diff --git a/src/gl/image_decoder.c b/src/gl/image_decoder.c index 16b2aacc..756b47df 100644 --- a/src/gl/image_decoder.c +++ b/src/gl/image_decoder.c @@ -3,7 +3,14 @@ #include #include #include + +#ifndef IMAGE_DECODER_HAS_BZ2 +#define IMAGE_DECODER_HAS_BZ2 1 +#endif + +#if IMAGE_DECODER_HAS_BZ2 #include +#endif #include "stb_image.h" @@ -119,6 +126,15 @@ static uint8_t* decodeQoi(const uint8_t* data, size_t dataSize, int* outW, int* // bytes 8..11 = uncompressed BZ2 length (LE uint32) -- ONLY when gm2022_5 is true // bytes 8.. (or 12.. if gm2022_5) = raw BZip2 stream, which decompresses into a full "fioq" QOI file. static uint8_t* decodeBz2Qoi(const uint8_t* blob, size_t blobSize, bool gm2022_5, int* outW, int* outH) { +#if !IMAGE_DECODER_HAS_BZ2 + (void) blob; + (void) blobSize; + (void) gm2022_5; + (void) outW; + (void) outH; + fprintf(stderr, "ImageDecoder: BZip2 support is disabled in this build\n"); + return nullptr; +#else size_t headerSize = gm2022_5 ? COMPRESSED_QOI_HEADER_SIZE_NEW : COMPRESSED_QOI_HEADER_SIZE_OLD; if (headerSize > blobSize) return nullptr; @@ -142,6 +158,7 @@ static uint8_t* decodeBz2Qoi(const uint8_t* blob, size_t blobSize, bool gm2022_5 uint8_t* result = decodeQoi(uncompressed, destLen, outW, outH); free(uncompressed); return result; +#endif } uint8_t* ImageDecoder_decodeToRgba(const uint8_t* blob, size_t blobSize, bool gm2022_5, int* outW, int* outH) { @@ -164,4 +181,4 @@ uint8_t* ImageDecoder_decodeToRgba(const uint8_t* blob, size_t blobSize, bool gm *outW = w; *outH = h; return pixels; -} +} \ No newline at end of file diff --git a/src/glfw/gl_legacy_renderer.c b/src/glfw/gl_legacy_renderer.c deleted file mode 100644 index c3bb8a22..00000000 --- a/src/glfw/gl_legacy_renderer.c +++ /dev/null @@ -1,1260 +0,0 @@ -#include "gl_legacy_renderer.h" -#include "matrix_math.h" -#include "text_utils.h" - -#include -#include -#include -#include -#include - -#include "stb_image.h" -#include "stb_ds.h" -#include "utils.h" -#include "image_decoder.h" - -// ===[ Helpers ]=== -static void glApplyViewport(GLLegacyRenderer* gl, int32_t x, int32_t y, int32_t w, int32_t h) { - int32_t effW, effH; - if ((gl->gameW * gl->windowH) / gl->gameH < gl->windowW) { - effW = (gl->gameW * gl->windowH) / gl->gameH; - effH = gl->windowH; - } else { - effW = gl->windowW; - effH = (gl->gameH * gl->windowW) / gl->gameW; - } - float scale = (float)effW / (float)gl->gameW; - int32_t offsetX = (gl->windowW - effW) / 2; - int32_t offsetY = (gl->windowH - effH) / 2; - - int32_t vpX = offsetX + (int32_t)(x * scale); - int32_t vpY = offsetY + (int32_t)((gl->gameH - y - h) * scale); - int32_t vpW = (int32_t)(w * scale); - int32_t vpH = (int32_t)(h * scale); - - glViewport(vpX, vpY, vpW, vpH); - glEnable(GL_SCISSOR_TEST); - glScissor(vpX, vpY, vpW, vpH); -} - -// ===[ Vtable Implementations ]=== - -static void glInit(Renderer* renderer, DataWin* dataWin) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - renderer->dataWin = dataWin; - - // Prepare texture slots for lazy loading (PNG decode deferred to first use) - glEnable(GL_TEXTURE_2D); - glDisable(GL_DEPTH_TEST); - glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - - gl->textureCount = dataWin->txtr.count; - gl->glTextures = safeMalloc(gl->textureCount * sizeof(GLuint)); - gl->textureWidths = safeMalloc(gl->textureCount * sizeof(int32_t)); - gl->textureHeights = safeMalloc(gl->textureCount * sizeof(int32_t)); - gl->textureLoaded = safeMalloc(gl->textureCount * sizeof(bool)); - - glGenTextures((GLsizei) gl->textureCount, gl->glTextures); - - for (uint32_t i = 0; gl->textureCount > i; i++) { - gl->textureWidths[i] = 0; - gl->textureHeights[i] = 0; - gl->textureLoaded[i] = false; - } - - // Create 1x1 white pixel texture for primitive drawing (rectangles, lines, etc.) - glGenTextures(1, &gl->whiteTexture); - glBindTexture(GL_TEXTURE_2D, gl->whiteTexture); - uint8_t whitePixel[4] = {255, 255, 255, 255}; - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - // Enable blending - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - glBindTexture(GL_TEXTURE_2D, 0); - - // Save original counts so we know which slots are from data.win vs dynamic - gl->originalTexturePageCount = gl->textureCount; - gl->originalTpagCount = dataWin->tpag.count; - gl->originalSpriteCount = dataWin->sprt.count; - - fprintf(stderr, "GL: Renderer initialized (%u texture pages)\n", gl->textureCount); -} - -static void glDestroy(Renderer* renderer) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - - glDeleteTextures(1, &gl->whiteTexture); - - glDeleteTextures((GLsizei) gl->textureCount, gl->glTextures); - - free(gl->glTextures); - free(gl->textureWidths); - free(gl->textureHeights); - free(gl); -} - -static void glBeginFrame(Renderer* renderer, int32_t gameW, int32_t gameH, int32_t windowW, int32_t windowH) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - - gl->windowW = windowW; - gl->windowH = windowH; - gl->gameW = gameW; - gl->gameH = gameH; - - glApplyViewport(gl, 0, 0, gameW, gameH); - glBindTexture(GL_TEXTURE_2D, 0); -} - -static void glBeginView(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, int32_t portX, int32_t portY, int32_t portW, int32_t portH, float viewAngle) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - - glBindTexture(GL_TEXTURE_2D, 0); - - // Set viewport and scissor to the port rectangle within the FBO - // FBO uses game resolution, port coordinates are in game space - // OpenGL viewport Y is bottom-up, game Y is top-down - glApplyViewport(gl, portX, portY, portW, portH); - - // Build orthographic projection (Y-down for GML coordinate system) - Matrix4f projection; - Matrix4f_identity(&projection); - Matrix4f_ortho(&projection, (float) viewX, (float) (viewX + viewW), (float) (viewY + viewH), (float) viewY, -1.0f, 1.0f); - - if (viewAngle != 0.0f) { - // GML view_angle: rotate camera by this angle (degrees, counter-clockwise) - // To rotate the camera, we rotate the world in the opposite direction around the view center - float cx = (float) viewX + (float) viewW / 2.0f; - float cy = (float) viewY + (float) viewH / 2.0f; - Matrix4f rot; - Matrix4f_identity(&rot); - Matrix4f_translate(&rot, cx, cy, 0.0f); - float angleRad = viewAngle * (float) M_PI / 180.0f; - Matrix4f_rotateZ(&rot, -angleRad); - Matrix4f_translate(&rot, -cx, -cy, 0.0f); - Matrix4f result; - Matrix4f_multiply(&result, &projection, &rot); - projection = result; - } - - glMatrixMode(GL_PROJECTION); - glLoadMatrixf(projection.m); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - glActiveTexture(GL_TEXTURE0); -} - -static void glEndView(MAYBE_UNUSED Renderer* renderer) { - glDisable(GL_SCISSOR_TEST); -} - -static void glBeginGUI(Renderer* renderer, int32_t guiW, int32_t guiH, int32_t portX, int32_t portY, int32_t portW, int32_t portH) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - - glBindTexture(GL_TEXTURE_2D, 0); - - glApplyViewport(gl, portX, portY, portW, portH); - - Matrix4f projection; - Matrix4f_identity(&projection); - Matrix4f_ortho(&projection, 0.0f, (float) guiW, (float) guiH, 0.0f, -1.0f, 1.0f); - - glMatrixMode(GL_PROJECTION); - glLoadMatrixf(projection.m); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - glActiveTexture(GL_TEXTURE0); -} - -static void glEndGUI(MAYBE_UNUSED Renderer* renderer) { - glDisable(GL_SCISSOR_TEST); -} - -static void glEndFrame(Renderer* renderer) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - - int effectiveEndX, effectiveEndY; - int effectiveStartX, effectiveStartY; - - // Try and match the "intended" aspect ratio as closely - // as possible while still fitting on the screen - if ((gl->gameW * gl->windowH) / gl->gameH < gl->windowW) { - effectiveEndX = (gl->gameW * gl->windowH) / gl->gameH; - effectiveEndY = gl->windowH; - } else { - effectiveEndX = gl->windowW; - effectiveEndY = (gl->gameH * gl->windowW) / gl->gameW; - } - effectiveStartX = (gl->windowW - effectiveEndX) / 2; - effectiveStartY = (gl->windowH - effectiveEndY) / 2; - effectiveEndX += effectiveStartX; - effectiveEndY += effectiveStartY; -} - -static void glRendererFlush(MAYBE_UNUSED Renderer* renderer) {} - -// Lazily decodes and uploads a TXTR page on first access. -// Returns true if the texture is ready, false if it failed to decode. -static bool ensureTextureLoaded(GLLegacyRenderer* gl, uint32_t pageId) { - if (gl->textureLoaded[pageId]) return (gl->textureWidths[pageId] != 0); - - gl->textureLoaded[pageId] = true; - - DataWin* dw = gl->base.dataWin; - Texture* txtr = &dw->txtr.textures[pageId]; - - int w, h; - bool gm2022_5 = DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0); - uint8_t* pixels = ImageDecoder_decodeToRgba(txtr->blobData, (size_t) txtr->blobSize, gm2022_5, &w, &h); - if (pixels == nullptr) { - fprintf(stderr, "GL: Failed to decode TXTR page %u\n", pageId); - return false; - } - - gl->textureWidths[pageId] = w; - gl->textureHeights[pageId] = h; - - glBindTexture(GL_TEXTURE_2D, gl->glTextures[pageId]); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - free(pixels); - fprintf(stderr, "GL: Loaded TXTR page %u (%dx%d)\n", pageId, w, h); - return true; -} - -static void glDrawSprite(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return; - - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - int16_t pageId = tpag->texturePageId; - if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return; - if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return; - - GLuint texId = gl->glTextures[pageId]; - int32_t texW = gl->textureWidths[pageId]; - int32_t texH = gl->textureHeights[pageId]; - - glBindTexture(GL_TEXTURE_2D, texId); - - // Compute normalized UVs from TPAG source rect - float u0 = (float) tpag->sourceX / (float) texW; - float v0 = (float) tpag->sourceY / (float) texH; - float u1 = (float) (tpag->sourceX + tpag->sourceWidth) / (float) texW; - float v1 = (float) (tpag->sourceY + tpag->sourceHeight) / (float) texH; - - // Compute local quad corners (relative to origin, with target offset) - float localX0 = (float) tpag->targetX - originX; - float localY0 = (float) tpag->targetY - originY; - float localX1 = localX0 + (float) tpag->sourceWidth; - float localY1 = localY0 + (float) tpag->sourceHeight; - - // Build 2D transform: T(x,y) * R(-angleDeg) * S(xscale, yscale) - // GML rotation is counter-clockwise, OpenGL rotation is counter-clockwise, but - // since we have Y-down, we negate the angle to get the correct visual rotation - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - Matrix4f transform; - Matrix4f_setTransform2D(&transform, x, y, xscale, yscale, angleRad); - - // Transform 4 corners - float x0, y0, x1, y1, x2, y2, x3, y3; - Matrix4f_transformPoint(&transform, localX0, localY0, &x0, &y0); // top-left - Matrix4f_transformPoint(&transform, localX1, localY0, &x1, &y1); // top-right - Matrix4f_transformPoint(&transform, localX1, localY1, &x2, &y2); // bottom-right - Matrix4f_transformPoint(&transform, localX0, localY1, &x3, &y3); // bottom-left - - // Convert BGR color to RGB floats - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - glBegin(GL_QUADS); - // Vertex 0: top-left - glColor4f(r, g, b, alpha); - glTexCoord2f(u0, v0); - glVertex2f(x0, y0); - - // Vertex 1: top-right - glColor4f(r, g, b, alpha); - glTexCoord2f(u1, v0); - glVertex2f(x1, y1); - - // Vertex 2: bottom-right - glColor4f(r, g, b, alpha); - glTexCoord2f(u1, v1); - glVertex2f(x2, y2); - - // Vertex 3: bottom-left - glColor4f(r, g, b, alpha); - glTexCoord2f(u0, v1); - glVertex2f(x3, y3); - glEnd(); -} - -static void glDrawSpritePos(Renderer* renderer, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return; - - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - int16_t pageId = tpag->texturePageId; - if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return; - if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return; - - GLuint texId = gl->glTextures[pageId]; - int32_t texW = gl->textureWidths[pageId]; - int32_t texH = gl->textureHeights[pageId]; - glBindTexture(GL_TEXTURE_2D, texId); - - float u0 = (float) tpag->sourceX / (float) texW; - float v0 = (float) tpag->sourceY / (float) texH; - float u1 = (float) (tpag->sourceX + tpag->sourceWidth) / (float) texW; - float v1 = (float) (tpag->sourceY + tpag->sourceHeight) / (float) texH; - - glBegin(GL_QUADS); - glColor4f(1.0f, 1.0f, 1.0f, alpha); - glTexCoord2f(u0, v0); - glVertex2f(x1, y1); - - glColor4f(1.0f, 1.0f, 1.0f, alpha); - glTexCoord2f(u1, v0); - glVertex2f(x2, y2); - - glColor4f(1.0f, 1.0f, 1.0f, alpha); - glTexCoord2f(u1, v1); - glVertex2f(x3, y3); - - glColor4f(1.0f, 1.0f, 1.0f, alpha); - glTexCoord2f(u0, v1); - glVertex2f(x4, y4); - glEnd(); -} - -static void glDrawSpritePart(Renderer* renderer, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > tpagIndex || dw->tpag.count <= (uint32_t) tpagIndex) return; - - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - int16_t pageId = tpag->texturePageId; - if (0 > pageId || gl->textureCount <= (uint32_t) pageId) return; - if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return; - - GLuint texId = gl->glTextures[pageId]; - int32_t texW = gl->textureWidths[pageId]; - int32_t texH = gl->textureHeights[pageId]; - - glBindTexture(GL_TEXTURE_2D, texId); - - // Compute UVs for the sub-region within the atlas - float u0 = (float) (tpag->sourceX + srcOffX) / (float) texW; - float v0 = (float) (tpag->sourceY + srcOffY) / (float) texH; - float u1 = (float) (tpag->sourceX + srcOffX + srcW) / (float) texW; - float v1 = (float) (tpag->sourceY + srcOffY + srcH) / (float) texH; - - // Convert BGR color to RGB floats - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - // Quad corners (no origin offset - draw_sprite_part ignores sprite origin) - float cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3; - if (angleDeg == 0.0f) { - cx0 = x; cy0 = y; - cx1 = x + (float) srcW * xscale; cy1 = y; - cx2 = x + (float) srcW * xscale; cy2 = y + (float) srcH * yscale; - cx3 = x; cy3 = y + (float) srcH * yscale; - } else { - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - float cosA = cosf(angleRad); - float sinA = sinf(angleRad); - float qx0 = x, qy0 = y; - float qx1 = x + (float) srcW * xscale, qy1 = y; - float qx2 = x + (float) srcW * xscale, qy2 = y + (float) srcH * yscale; - float qx3 = x, qy3 = y + (float) srcH * yscale; - float dx, dy; - dx = qx0 - pivotX; dy = qy0 - pivotY; cx0 = cosA * dx - sinA * dy + pivotX; cy0 = sinA * dx + cosA * dy + pivotY; - dx = qx1 - pivotX; dy = qy1 - pivotY; cx1 = cosA * dx - sinA * dy + pivotX; cy1 = sinA * dx + cosA * dy + pivotY; - dx = qx2 - pivotX; dy = qy2 - pivotY; cx2 = cosA * dx - sinA * dy + pivotX; cy2 = sinA * dx + cosA * dy + pivotY; - dx = qx3 - pivotX; dy = qy3 - pivotY; cx3 = cosA * dx - sinA * dy + pivotX; cy3 = sinA * dx + cosA * dy + pivotY; - } - - glBegin(GL_QUADS); - glColor4f(r, g, b, alpha); - glTexCoord2f(u0, v0); glVertex2f(cx0, cy0); - - glColor4f(r, g, b, alpha); - glTexCoord2f(u1, v0); glVertex2f(cx1, cy1); - - glColor4f(r, g, b, alpha); - glTexCoord2f(u1, v1); glVertex2f(cx2, cy2); - - glColor4f(r, g, b, alpha); - glTexCoord2f(u0, v1); glVertex2f(cx3, cy3); - glEnd(); -} - -// Emits a single colored quad into the batch using the white pixel texture -static void emitColoredQuad(GLLegacyRenderer* gl, float x0, float y0, float x1, float y1, float r, float g, float b, float a) { - glBindTexture(GL_TEXTURE_2D, gl->whiteTexture); - - glBegin(GL_QUADS); - // All UVs point to (0.5, 0.5) center of the 1x1 white texture - // Vertex 0: top-left - glColor4f(r, g, b, a); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x0, y0); - - // Vertex 1: top-right - glColor4f(r, g, b, a); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x1, y0); - - // Vertex 2: bottom-right - glColor4f(r, g, b, a); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x1, y1); - - // Vertex 3: bottom-left - glColor4f(r, g, b, a); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x0, y1); - glEnd(); -} - -static void glDrawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - if (outline) { - // Draw 4 one-pixel-wide edges: top, bottom, left, right - emitColoredQuad(gl, x1, y1, x2 + 1, y1 + 1, r, g, b, alpha); // top - emitColoredQuad(gl, x1, y2, x2 + 1, y2 + 1, r, g, b, alpha); // bottom - emitColoredQuad(gl, x1, y1 + 1, x1 + 1, y2, r, g, b, alpha); // left - emitColoredQuad(gl, x2, y1 + 1, x2 + 1, y2, r, g, b, alpha); // right - } else { - // Filled rectangle: GML adds +1 to width/height for filled rects - emitColoredQuad(gl, x1, y1, x2 + 1, y2 + 1, r, g, b, alpha); - } -} - -// ===[ Line Drawing ]=== - -static void glDrawLine(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color, float alpha) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - // Compute perpendicular offset for line thickness - float dx = x2 - x1; - float dy = y2 - y1; - float len = sqrtf(dx * dx + dy * dy); - if (0.0001f > len) return; - - float halfW = width * 0.5f; - float px = (-dy / len) * halfW; - float py = (dx / len) * halfW; - - glBindTexture(GL_TEXTURE_2D, gl->whiteTexture); - - // Vertex 0: start + perpendicular - glBegin(GL_QUADS); - glColor4f(r, g, b, alpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x1 + px, y1 + py); - - // Vertex 1: start - perpendicular - glColor4f(r, g, b, alpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x1 - px, y1 - py); - - // Vertex 2: end - perpendicular - glColor4f(r, g, b, alpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x2 - px, y2 - py); - - // Vertex 3: end + perpendicular - glColor4f(r, g, b, alpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x2 + px, y2 + py); - glEnd(); -} - -static void glDrawLineColor(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, uint32_t color2, float alpha) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - - float r1 = (float) BGR_R(color1) / 255.0f; - float g1 = (float) BGR_G(color1) / 255.0f; - float b1 = (float) BGR_B(color1) / 255.0f; - - float r2 = (float) BGR_R(color2) / 255.0f; - float g2 = (float) BGR_G(color2) / 255.0f; - float b2 = (float) BGR_B(color2) / 255.0f; - - // Compute perpendicular offset for line thickness - float dx = x2 - x1; - float dy = y2 - y1; - float len = sqrtf(dx * dx + dy * dy); - if (0.0001f > len) return; - - float halfW = width * 0.5f; - float px = (-dy / len) * halfW; - float py = (dx / len) * halfW; - - // Emit quad with per-vertex colors (color1 at start, color2 at end) - glBindTexture(GL_TEXTURE_2D, gl->whiteTexture); - - glBegin(GL_QUADS); - // Vertex 0: start + perpendicular (color1) - glColor4f(r1, g1, b1, alpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x1 + px, y1 + py); - - // Vertex 1: start - perpendicular (color1) - glColor4f(r1, g1, b1, alpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x1 - px, y1 - py); - - // Vertex 2: end - perpendicular (color2) - glColor4f(r2, g2, b2, alpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x2 - px, y2 - py); - - // Vertex 3: end + perpendicular (color2) - glColor4f(r2, g2, b2, alpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x2 + px, y2 + py); - glEnd(); -} - -static void glDrawTriangle(Renderer *renderer, float x1, float y1, float x2, float y2, float x3, float y3, bool outline) -{ - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - if(outline) - { - glDrawLine(renderer, x1, y1, x2, y2, 1, renderer->drawColor, 1.0); - glDrawLine(renderer, x2, y2, x3, y3, 1, renderer->drawColor, 1.0); - glDrawLine(renderer, x3, y3, x1, y1, 1, renderer->drawColor, 1.0); - } else { - float r = (float) BGR_R(renderer->drawColor) / 255.0f; - float g = (float) BGR_G(renderer->drawColor) / 255.0f; - float b = (float) BGR_B(renderer->drawColor) / 255.0f; - - glBindTexture(GL_TEXTURE_2D, gl->whiteTexture); - - glBegin(GL_TRIANGLES); - glColor4f(r, g, b, renderer->drawAlpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x1 , y1); - - glColor4f(r, g, b, renderer->drawAlpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x2, y2); - - glColor4f(r, g, b, renderer->drawAlpha); - glTexCoord2f(0.5f, 0.5f); - glVertex2f(x3, y3); - glEnd(); - } -} - -// ===[ Text Drawing ]=== - -// Resolved font state shared between glDrawText and glDrawTextColor -typedef struct { - Font* font; - TexturePageItem* fontTpag; // single TPAG for regular fonts (nullptr for sprite fonts) - GLuint texId; - int32_t texW, texH; - Sprite* spriteFontSprite; // source sprite for sprite fonts (nullptr for regular fonts) -} GlFontState; - -// Resolves font texture state -// Returns false if the font can't be drawn -static bool glResolveFontState(GLLegacyRenderer* gl, DataWin* dw, Font* font, GlFontState* state) { - state->font = font; - state->fontTpag = nullptr; - state->texId = 0; - state->texW = 0; - state->texH = 0; - state->spriteFontSprite = nullptr; - - if (!font->isSpriteFont) { - int32_t fontTpagIndex = font->tpagIndex; - if (0 > fontTpagIndex) return false; - - state->fontTpag = &dw->tpag.items[fontTpagIndex]; - int16_t pageId = state->fontTpag->texturePageId; - if (0 > pageId || (uint32_t) pageId >= gl->textureCount) return false; - if (!ensureTextureLoaded(gl, (uint32_t) pageId)) return false; - - state->texId = gl->glTextures[pageId]; - state->texW = gl->textureWidths[pageId]; - state->texH = gl->textureHeights[pageId]; - } else if (font->spriteIndex >= 0 && dw->sprt.count > (uint32_t) font->spriteIndex) { - state->spriteFontSprite = &dw->sprt.sprites[font->spriteIndex]; - } - return true; -} - -// Resolves UV coordinates, texture ID, and local position for a single glyph -// Returns false if the glyph can't be drawn -static bool glResolveGlyph(GLLegacyRenderer* gl, DataWin* dw, GlFontState* state, FontGlyph* glyph, float cursorX, float cursorY, GLuint* outTexId, float* outU0, float* outV0, float* outU1, float* outV1, float* outLocalX0, float* outLocalY0) { - Font* font = state->font; - if (font->isSpriteFont && state->spriteFontSprite != nullptr) { - Sprite* sprite = state->spriteFontSprite; - int32_t glyphIndex = (int32_t) (glyph - font->glyphs); - if (0 > glyphIndex || glyphIndex >= (int32_t) sprite->textureCount) return false; - - int32_t tpagIdx = sprite->tpagIndices[glyphIndex]; - if (0 > tpagIdx) return false; - - TexturePageItem* glyphTpag = &dw->tpag.items[tpagIdx]; - int16_t pid = glyphTpag->texturePageId; - if (0 > pid || (uint32_t) pid >= gl->textureCount) return false; - if (!ensureTextureLoaded(gl, (uint32_t) pid)) return false; - - *outTexId = gl->glTextures[pid]; - int32_t tw = gl->textureWidths[pid]; - int32_t th = gl->textureHeights[pid]; - - *outU0 = (float) glyphTpag->sourceX / (float) tw; - *outV0 = (float) glyphTpag->sourceY / (float) th; - *outU1 = (float) (glyphTpag->sourceX + glyphTpag->sourceWidth) / (float) tw; - *outV1 = (float) (glyphTpag->sourceY + glyphTpag->sourceHeight) / (float) th; - - *outLocalX0 = cursorX + (float) glyph->offset; - *outLocalY0 = cursorY + (float) ((int32_t) glyphTpag->targetY - sprite->originY); - } else { - *outTexId = state->texId; - *outU0 = (float) (state->fontTpag->sourceX + glyph->sourceX) / (float) state->texW; - *outV0 = (float) (state->fontTpag->sourceY + glyph->sourceY) / (float) state->texH; - *outU1 = (float) (state->fontTpag->sourceX + glyph->sourceX + glyph->sourceWidth) / (float) state->texW; - *outV1 = (float) (state->fontTpag->sourceY + glyph->sourceY + glyph->sourceHeight) / (float) state->texH; - - *outLocalX0 = cursorX + glyph->offset; - *outLocalY0 = cursorY; - } - return true; -} - -static void glDrawText(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - int32_t fontIndex = renderer->drawFont; - if (0 > fontIndex || dw->font.count <= (uint32_t) fontIndex) return; - - Font* font = &dw->font.fonts[fontIndex]; - - GlFontState fontState; - if (!glResolveFontState(gl, dw, font, &fontState)) return; - - uint32_t color = renderer->drawColor; - float alpha = renderer->drawAlpha; - float r = (float) BGR_R(color) / 255.0f; - float g = (float) BGR_G(color) / 255.0f; - float b = (float) BGR_B(color) / 255.0f; - - int32_t textLen = (int32_t) strlen(text); - - // Count lines, treating \r\n and \n\r as single breaks - int32_t lineCount = TextUtils_countLines(text, textLen); - - // Per-line vertical stride. HTML5 runner's default `linesep` is `max_glyph_height * scaleY`. - // We apply scaleY via the transform matrix below, so keep the stride in pre-scale (local) coords. - float lineStride = TextUtils_lineStride(font); - - // Vertical alignment offset - float totalHeight = (float) lineCount * lineStride; - float valignOffset = 0; - if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f; - else if (renderer->drawValign == 2) valignOffset = -totalHeight; - - // Build transform matrix - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - Matrix4f transform; - Matrix4f_setTransform2D(&transform, x, y, xscale * font->scaleX, yscale * font->scaleY, angleRad); - - // Iterate through lines. HTML5 subtracts ascenderOffset from the per-line y offset - // (see yyFont.GR_Text_Draw), shifting glyphs up so the baseline aligns with the drawn y. - float cursorY = valignOffset - (float) font->ascenderOffset; - int32_t lineStart = 0; - - for (int32_t lineIdx = 0; lineCount > lineIdx; lineIdx++) { - // Find end of current line - int32_t lineEnd = lineStart; - while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) { - lineEnd++; - } - int32_t lineLen = lineEnd - lineStart; - - // Horizontal alignment offset for this line - float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineLen); - float halignOffset = 0; - if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f; - else if (renderer->drawHalign == 2) halignOffset = -lineWidth; - - float cursorX = halignOffset; - - // Render each glyph in the line - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning) - int32_t pos = 0; - uint16_t ch = 0; - bool hasCh = false; - if (lineLen > pos) { - ch = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); - hasCh = true; - } - - while (hasCh) { - FontGlyph* glyph = TextUtils_findGlyph(font, ch); - - uint16_t nextCh = 0; - bool hasNext = lineLen > pos; - if (hasNext) nextCh = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); - - if (glyph != nullptr) { - bool drewSuccessfully = false; - if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) { - float u0, v0, u1, v1; - float localX0, localY0; - GLuint glyphTexId; - - if (glResolveGlyph(gl, dw, &fontState, glyph, cursorX, cursorY, &glyphTexId, &u0, &v0, &u1, &v1, &localX0, &localY0)) { - glBindTexture(GL_TEXTURE_2D, glyphTexId); - - float localX1 = localX0 + (float) glyph->sourceWidth; - float localY1 = localY0 + (float) glyph->sourceHeight; - - // Transform corners - float px0, py0, px1, py1, px2, py2, px3, py3; - Matrix4f_transformPoint(&transform, localX0, localY0, &px0, &py0); - Matrix4f_transformPoint(&transform, localX1, localY0, &px1, &py1); - Matrix4f_transformPoint(&transform, localX1, localY1, &px2, &py2); - Matrix4f_transformPoint(&transform, localX0, localY1, &px3, &py3); - - glBegin(GL_QUADS); - glColor4f(r, g, b, alpha); - glTexCoord2f(u0, v0); - glVertex2f(px0, py0); - - glColor4f(r, g, b, alpha); - glTexCoord2f(u1, v0); - glVertex2f(px1, py1); - - glColor4f(r, g, b, alpha); - glTexCoord2f(u1, v1); - glVertex2f(px2, py2); - - glColor4f(r, g, b, alpha); - glTexCoord2f(u0, v1); - glVertex2f(px3, py3); - glEnd(); - - drewSuccessfully = true; - } - } - - cursorX += glyph->shift; - if (drewSuccessfully && hasNext) { - cursorX += TextUtils_getKerningOffset(glyph, nextCh); - } - } - - ch = nextCh; - hasCh = hasNext; - } - - cursorY += lineStride; - // Skip past the newline, treating \r\n and \n\r as single breaks - if (textLen > lineEnd) { - lineStart = TextUtils_skipNewline(text, lineEnd, textLen); - } else { - lineStart = lineEnd; - } - } -} - -static void glDrawTextColor(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg, int32_t _c1, int32_t _c2, int32_t _c3, int32_t _c4, float alpha) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - int32_t fontIndex = renderer->drawFont; - if (0 > fontIndex || dw->font.count <= (uint32_t) fontIndex) return; - - Font* font = &dw->font.fonts[fontIndex]; - - GlFontState fontState; - if (!glResolveFontState(gl, dw, font, &fontState)) return; - - int32_t textLen = (int32_t) strlen(text); - if(textLen == 0) return; - - // Count lines, treating \r\n and \n\r as single breaks - int32_t lineCount = TextUtils_countLines(text, textLen); - - float lineStride = TextUtils_lineStride(font); - - // Vertical alignment offset - float totalHeight = (float) lineCount * lineStride; - float valignOffset = 0; - if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f; - else if (renderer->drawValign == 2) valignOffset = -totalHeight; - - // Build transform matrix - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - Matrix4f transform; - Matrix4f_setTransform2D(&transform, x, y, xscale * font->scaleX, yscale * font->scaleY, angleRad); - - // Iterate through lines. HTML5 subtracts ascenderOffset from per-line y offset. - float cursorY = valignOffset - (float) font->ascenderOffset; - int32_t lineStart = 0; - - // get delta's (16.16 format) - int32_t left_r_dx = ((_c2 & 0xff0000) - (_c1 & 0xff0000)) / textLen; - int32_t left_g_dx = ((((_c2 & 0xff00) << 8) - ((_c1 & 0xff00) << 8))) / textLen; - int32_t left_b_dx = ((((_c2 & 0xff) << 16) - ((_c1 & 0xff) << 16))) / textLen; - - int32_t right_r_dx = ((_c3 & 0xff0000) - (_c4 & 0xff0000)) / textLen; - int32_t right_g_dx = ((((_c3 & 0xff00) << 8) - ((_c4 & 0xff00) << 8))) / textLen; - int32_t right_b_dx = ((((_c3 & 0xff) << 16) - ((_c4 & 0xff) << 16))) / textLen; - - int32_t left_delta_r = left_r_dx; - int32_t left_delta_g = left_g_dx; - int32_t left_delta_b = left_b_dx; - int32_t right_delta_r = right_r_dx; - int32_t right_delta_g = right_g_dx; - int32_t right_delta_b = right_b_dx; - - int32_t c1 = _c1; - int32_t c4 = _c4; - - for (int32_t lineIdx = 0; lineCount > lineIdx; lineIdx++) { - // Find end of current line - int32_t lineEnd = lineStart; - while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) { - lineEnd++; - } - int32_t lineLen = lineEnd - lineStart; - - // Horizontal alignment offset for this line - float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineLen); - float halignOffset = 0; - if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f; - else if (renderer->drawHalign == 2) halignOffset = -lineWidth; - - float cursorX = halignOffset; - - // Render each glyph in the line - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning) - int32_t pos = 0; - uint16_t ch = 0; - bool hasCh = false; - if (lineLen > pos) { - ch = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); - hasCh = true; - } - - while (hasCh) { - // do 16.16 maths - int32_t c2 = ((c1 & 0xff0000) + (left_delta_r & 0xff0000)) & 0xff0000; - c2 |= ((c1 & 0xff00) + (left_delta_g >> 8) & 0xff00) & 0xff00; - c2 |= ((c1 & 0xff) + (left_delta_b >> 16)) & 0xff; - int32_t c3 = ((c4 & 0xff0000) + (right_delta_r & 0xff0000)) & 0xff0000; - c3 |= ((c4 & 0xff00) + (right_delta_g >> 8) & 0xff00) & 0xff00; - c3 |= ((c4 & 0xff) + (right_delta_b >> 16)) & 0xff; - - left_delta_r += left_r_dx; - left_delta_g += left_g_dx; - left_delta_b += left_b_dx; - right_delta_r += right_r_dx; - right_delta_g += right_g_dx; - right_delta_b += right_b_dx; - - FontGlyph* glyph = TextUtils_findGlyph(font, ch); - - uint16_t nextCh = 0; - bool hasNext = lineLen > pos; - if (hasNext) nextCh = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); - - if (glyph != nullptr) { - bool drewSuccessfully = false; - if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) { - float u0, v0, u1, v1; - float localX0, localY0; - GLuint glyphTexId; - - if (glResolveGlyph(gl, dw, &fontState, glyph, cursorX, cursorY, &glyphTexId, &u0, &v0, &u1, &v1, &localX0, &localY0)) { - glBindTexture(GL_TEXTURE_2D, glyphTexId); - - float localX1 = localX0 + (float) glyph->sourceWidth; - float localY1 = localY0 + (float) glyph->sourceHeight; - - // Transform corners - float px0, py0, px1, py1, px2, py2, px3, py3; - Matrix4f_transformPoint(&transform, localX0, localY0, &px0, &py0); - Matrix4f_transformPoint(&transform, localX1, localY0, &px1, &py1); - Matrix4f_transformPoint(&transform, localX1, localY1, &px2, &py2); - Matrix4f_transformPoint(&transform, localX0, localY1, &px3, &py3); - - glBegin(GL_QUADS); - glColor4ub(BGR_R(c1), BGR_G(c1), BGR_B(c1), alpha * 255); - glTexCoord2f(u0, v0); - glVertex2f(px0, py0); - - glColor4ub(BGR_R(c2), BGR_G(c2), BGR_B(c2), alpha * 255); - glTexCoord2f(u1, v0); - glVertex2f(px1, py1); - - glColor4ub(BGR_R(c3), BGR_G(c3), BGR_B(c3), alpha * 255); - glTexCoord2f(u1, v1); - glVertex2f(px2, py2); - - glColor4ub(BGR_R(c4), BGR_G(c4), BGR_B(c4), alpha * 255); - glTexCoord2f(u0, v1); - glVertex2f(px3, py3); - glEnd(); - - drewSuccessfully = true; - } - } - - cursorX += glyph->shift; - if (drewSuccessfully) { - if (hasNext) cursorX += TextUtils_getKerningOffset(glyph, nextCh); - c4 = c3; // set left edge to be what the last right edge was.... - c1 = c2; - } - } - - ch = nextCh; - hasCh = hasNext; - } - - cursorY += lineStride; - // Skip past the newline, treating \r\n and \n\r as single breaks - if (textLen > lineEnd) { - lineStart = TextUtils_skipNewline(text, lineEnd, textLen); - } else { - lineStart = lineEnd; - } - } -} - -// ===[ Dynamic Sprite Creation/Deletion ]=== - -// Finds a free dynamic texture page slot (glTextures[i] == 0), or appends a new one. -static uint32_t findOrAllocTexturePageSlot(GLLegacyRenderer* gl) { - // Scan dynamic range for a reusable slot - for (uint32_t i = gl->originalTexturePageCount; gl->textureCount > i; i++) { - if (gl->glTextures[i] == 0) return i; - } - // No free slot found, grow the arrays - uint32_t newPageId = gl->textureCount; - gl->textureCount++; - gl->glTextures = safeRealloc(gl->glTextures, gl->textureCount * sizeof(GLuint)); - gl->textureWidths = safeRealloc(gl->textureWidths, gl->textureCount * sizeof(int32_t)); - gl->textureHeights = safeRealloc(gl->textureHeights, gl->textureCount * sizeof(int32_t)); - gl->textureLoaded = safeRealloc(gl->textureLoaded, gl->textureCount * sizeof(bool)); - gl->glTextures[newPageId] = 0; - gl->textureWidths[newPageId] = 0; - gl->textureHeights[newPageId] = 0; - gl->textureLoaded[newPageId] = false; - return newPageId; -} - -// Finds a free dynamic TPAG slot (texturePageId == -1), or appends a new one. -static uint32_t findOrAllocTpagSlot(DataWin* dw, uint32_t originalTpagCount) { - for (uint32_t i = originalTpagCount; dw->tpag.count > i; i++) { - if (dw->tpag.items[i].texturePageId == -1) return i; - } - uint32_t newIndex = dw->tpag.count; - dw->tpag.count++; - dw->tpag.items = safeRealloc(dw->tpag.items, dw->tpag.count * sizeof(TexturePageItem)); - memset(&dw->tpag.items[newIndex], 0, sizeof(TexturePageItem)); - dw->tpag.items[newIndex].texturePageId = -1; - return newIndex; -} - -static int32_t glCreateSpriteFromSurface(Renderer* renderer, int32_t x, int32_t y, int32_t w, int32_t h, bool removeback, bool smooth, int32_t xorig, int32_t yorig) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 >= w || 0 >= h) return -1; - - - uint8_t* pixels = safeMalloc((size_t) w * (size_t) h * 4); - if (pixels == nullptr) return -1; - - // OpenGL Y is bottom-up, GML Y is top-down, so flip the Y coordinate - int32_t glY = gl->gameH - y - h; - glReadPixels(x, glY, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - - // Flip vertically (OpenGL reads bottom-to-top) - size_t rowBytes = (size_t) w * 4; - uint8_t* rowTemp = safeMalloc(rowBytes); - repeat(h / 2, row) { - uint8_t* top = pixels + row * rowBytes; - uint8_t* bot = pixels + (h - 1 - row) * rowBytes; - memcpy(rowTemp, top, rowBytes); - memcpy(top, bot, rowBytes); - memcpy(bot, rowTemp, rowBytes); - } - free(rowTemp); - - // Create a new GL texture from the captured pixels - GLuint newTexId; - glGenTextures(1, &newTexId); - glBindTexture(GL_TEXTURE_2D, newTexId); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, smooth ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, smooth ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - free(pixels); - - // Find or allocate slots for texture page, TPAG, and sprite - uint32_t pageId = findOrAllocTexturePageSlot(gl); - gl->glTextures[pageId] = newTexId; - gl->textureWidths[pageId] = w; - gl->textureHeights[pageId] = h; - gl->textureLoaded[pageId] = true; - - uint32_t tpagIndex = findOrAllocTpagSlot(dw, gl->originalTpagCount); - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - tpag->sourceX = 0; - tpag->sourceY = 0; - tpag->sourceWidth = (uint16_t) w; - tpag->sourceHeight = (uint16_t) h; - tpag->targetX = 0; - tpag->targetY = 0; - tpag->targetWidth = (uint16_t) w; - tpag->targetHeight = (uint16_t) h; - tpag->boundingWidth = (uint16_t) w; - tpag->boundingHeight = (uint16_t) h; - tpag->texturePageId = (int16_t) pageId; - - uint32_t spriteIndex = DataWin_allocSpriteSlot(dw, gl->originalSpriteCount); - Sprite* sprite = &dw->sprt.sprites[spriteIndex]; - // name was set by DataWin_allocSpriteSlot ("__newsprite"); don't overwrite it here - sprite->width = (uint32_t) w; - sprite->height = (uint32_t) h; - sprite->originX = xorig; - sprite->originY = yorig; - sprite->textureCount = 1; - sprite->tpagIndices = safeMalloc(sizeof(int32_t)); - sprite->tpagIndices[0] = (int32_t) tpagIndex; - sprite->maskCount = 0; - sprite->masks = nullptr; - - fprintf(stderr, "GL: Created dynamic sprite %u (%dx%d) from surface at (%d,%d)\n", spriteIndex, w, h, x, y); - return (int32_t) spriteIndex; -} - -static void glDeleteSprite(Renderer* renderer, int32_t spriteIndex) { - GLLegacyRenderer* gl = (GLLegacyRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > spriteIndex || dw->sprt.count <= (uint32_t) spriteIndex) return; - - // Refuse to delete original data.win sprites - if (gl->originalSpriteCount > (uint32_t) spriteIndex) { - fprintf(stderr, "GL: Cannot delete data.win sprite %d\n", spriteIndex); - return; - } - - Sprite* sprite = &dw->sprt.sprites[spriteIndex]; - if (sprite->textureCount == 0) return; // already deleted - - // Clean up GL texture and TPAG entries owned by this sprite. - // Slots with index >= originalTpagCount are dynamically allocated and ours to free. - repeat(sprite->textureCount, i) { - int32_t tpagIdx = sprite->tpagIndices[i]; - if (tpagIdx >= 0 && (uint32_t) tpagIdx >= gl->originalTpagCount) { - TexturePageItem* tpag = &dw->tpag.items[tpagIdx]; - int16_t pageId = tpag->texturePageId; - if (pageId >= 0 && gl->textureCount > (uint32_t) pageId) { - glDeleteTextures(1, &gl->glTextures[pageId]); - gl->glTextures[pageId] = 0; - } - // Mark TPAG slot as free for reuse - tpag->texturePageId = -1; - } - } - - // Clear the sprite entry so it won't be drawn and can be reused. Preserve `name` across the memset: the slot is still in sprt.count and must keep a valid string for asset_get_index / name lookups. - free(sprite->tpagIndices); - const char* keepName = sprite->name; - memset(sprite, 0, sizeof(Sprite)); - sprite->name = keepName; - - fprintf(stderr, "GL: Deleted sprite %d\n", spriteIndex); -} - -static GLenum gmsBlendModeToGL(int mode) { - switch(mode) { - case bm_zero: return GL_ZERO; - case bm_one: return GL_ONE; - case bm_src_color: return GL_SRC_COLOR; - case bm_inv_src_color: return GL_ONE_MINUS_SRC_COLOR; - case bm_src_alpha: return GL_SRC_ALPHA; - case bm_inv_src_alpha: return GL_ONE_MINUS_SRC_ALPHA; - case bm_dest_alpha: return GL_DST_ALPHA; - case bm_inv_dest_alpha: return GL_ONE_MINUS_DST_ALPHA; - case bm_dest_color: return GL_DST_COLOR; - case bm_inv_dest_color: return GL_ONE_MINUS_DST_COLOR; - case bm_src_alpha_sat: return GL_SRC_ALPHA_SATURATE; - } - return GL_ONE; -} - -static GLenum gmsBlendModeToGLEquation(int mode) { - switch (mode) { - case bm_normal: - return GL_FUNC_ADD; - case bm_add: - return GL_FUNC_ADD; - case bm_subtract: - return GL_FUNC_ADD; - case bm_reverse_subtract: - return GL_FUNC_REVERSE_SUBTRACT; - case bm_min: - return GL_MIN; - case bm_max: - return GL_FUNC_ADD; - default: - return GL_FUNC_ADD; - } -} - -static GLenum gmsBlendModeToGLSFactor(int mode) { - switch (mode) { - case bm_normal: - return GL_SRC_ALPHA; - case bm_add: - return GL_SRC_ALPHA; - case bm_subtract: - return GL_ZERO; - case bm_reverse_subtract: - return GL_SRC_ALPHA; - case bm_min: - return GL_ONE; - case bm_max: - return GL_SRC_ALPHA; - default: - return gmsBlendModeToGL(mode); - } -} - -static GLenum gmsBlendModeToGLDFactor(int mode) { - switch (mode) { - case bm_normal: - return GL_ONE_MINUS_SRC_ALPHA; - case bm_add: - return GL_ONE; - case bm_subtract: - return GL_ONE_MINUS_SRC_COLOR; - case bm_reverse_subtract: - return GL_ONE; - case bm_min: - return GL_ONE; - case bm_max: - return GL_ONE_MINUS_SRC_COLOR; - default: - return gmsBlendModeToGL(mode); - } -} - -static void glGpuSetBlendMode(Renderer* renderer, int32_t mode) { - glBlendEquation( - gmsBlendModeToGLEquation(mode) - ); - glBlendFunc( - gmsBlendModeToGLSFactor(mode), - gmsBlendModeToGLDFactor(mode) - ); -} - -static void glGpuSetBlendModeExt(Renderer* renderer, int32_t sfactor, int32_t dfactor) { - glBlendFunc( - gmsBlendModeToGLSFactor(sfactor), - gmsBlendModeToGLDFactor(dfactor) - ); -} - -static void glGpuSetBlendEnable(Renderer* renderer, bool enable) { - enable ? glEnable(GL_BLEND) : glDisable(GL_BLEND); -} - -static void glGpuSetAlphaTestEnable(Renderer* renderer, bool enable) { - enable ? glEnable(GL_ALPHA_TEST) : glDisable(GL_ALPHA_TEST); -} - -static void glGpuSetAlphaTestRef(Renderer* renderer, uint8_t ref) { - glAlphaFunc(GL_GREATER, ref/255.0f); -} - -static void glGpuSetColorWriteEnable(Renderer* renderer, bool red, bool green, bool blue, bool alpha) { - glColorMask(red, green, blue, alpha); -} - -// ===[ Vtable ]=== - -static RendererVtable glVtable = { - .init = glInit, - .destroy = glDestroy, - .beginFrame = glBeginFrame, - .endFrame = glEndFrame, - .beginView = glBeginView, - .endView = glEndView, - .beginGUI = glBeginGUI, - .endGUI = glEndGUI, - .drawSprite = glDrawSprite, - .drawSpritePos = glDrawSpritePos, - .drawSpritePart = glDrawSpritePart, - .drawRectangle = glDrawRectangle, - .drawLine = glDrawLine, - .drawLineColor = glDrawLineColor, - .drawTriangle = glDrawTriangle, - .drawText = glDrawText, - .drawTextColor = glDrawTextColor, - .flush = glRendererFlush, - .createSpriteFromSurface = glCreateSpriteFromSurface, - .deleteSprite = glDeleteSprite, - .gpuSetBlendMode = glGpuSetBlendMode, - .gpuSetBlendModeExt = glGpuSetBlendModeExt, - .gpuSetBlendEnable = glGpuSetBlendEnable, - .gpuSetAlphaTestEnable = glGpuSetAlphaTestEnable, - .gpuSetAlphaTestRef = glGpuSetAlphaTestRef, - .gpuSetColorWriteEnable = glGpuSetColorWriteEnable, - .drawTile = nullptr, -}; - -// ===[ Public API ]=== - -Renderer* GLLegacyRenderer_create(void) { - GLLegacyRenderer* gl = safeCalloc(1, sizeof(GLLegacyRenderer)); - gl->base.vtable = &glVtable; - gl->base.drawColor = 0xFFFFFF; // white (BGR) - gl->base.drawAlpha = 1.0f; - gl->base.drawFont = -1; - gl->base.drawHalign = 0; - gl->base.drawValign = 0; - return (Renderer*) gl; -} diff --git a/src/glfw/gl_legacy_renderer.h b/src/glfw/gl_legacy_renderer.h deleted file mode 100644 index 642b22d8..00000000 --- a/src/glfw/gl_legacy_renderer.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "common.h" -#include "renderer.h" -#include - -// ===[ GLLegacyRenderer Struct ]=== -// Exposed in the header so platform-specific code (main.c) can access FBO fields for screenshots. -typedef struct { - Renderer base; // Must be first field for struct embedding - - GLuint* glTextures; // one GL texture per TXTR page - int32_t* textureWidths; // needed for UV normalization - int32_t* textureHeights; - bool* textureLoaded; // lazy loading: true once PNG decoded and uploaded - uint32_t textureCount; - - GLuint whiteTexture; // 1x1 white pixel for drawing primitives (rectangles, lines, etc.) - - int32_t windowW; // stored from beginFrame for endFrame blit - int32_t windowH; - int32_t gameW; // game resolution (for FBO sizing) - int32_t gameH; - - // Original counts from data.win (dynamic slots start at these indices) - uint32_t originalTexturePageCount; - uint32_t originalTpagCount; - uint32_t originalSpriteCount; -} GLLegacyRenderer; - -Renderer* GLLegacyRenderer_create(void); diff --git a/src/glfw/glfw_file_system.c b/src/glfw/glfw_file_system.c deleted file mode 100644 index e66f11e7..00000000 --- a/src/glfw/glfw_file_system.c +++ /dev/null @@ -1,162 +0,0 @@ -#include "glfw_file_system.h" -#include "utils.h" - -#include -#include -#include -#include - -// ===[ Helpers ]=== - -// Replaces all backslashes (\) with forward slashes (/) in a path string. (dealing with windows paths) -static inline char* normalizePath(const char* path) { - if (path == nullptr) return nullptr; - char* normalized = safeStrdup(path); - for (int i = 0; normalized[i] != '\0'; i++) { - if (normalized[i] == '\\') { - normalized[i] = '/'; - } - } - return normalized; -} - -// The caller must make sure to free the returned string! -static char* buildFullPath(GlfwFileSystem* fs, const char* relativePath) { - char* normalizedPath = normalizePath(relativePath); - if (strstr(normalizedPath, fs->basePath) != nullptr) return normalizedPath; - size_t baseLen = strlen(fs->basePath); - size_t relLen = strlen(normalizedPath); - char* fullPath = safeMalloc(baseLen + relLen + 1); - memcpy(fullPath, fs->basePath, baseLen); - memcpy(fullPath + baseLen, normalizedPath, relLen); - fullPath[baseLen + relLen] = '\0'; - free(normalizedPath); - return fullPath; -} - -// ===[ Vtable Implementations ]=== - -// The caller must make sure to free the returned string! -static char* glfwResolvePath(FileSystem* fs, const char* relativePath) { - return buildFullPath((GlfwFileSystem*) fs, relativePath); -} - -static bool glfwFileExists(FileSystem* fs, const char* relativePath) { - char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath); - struct stat st; - bool exists = (stat(fullPath, &st) == 0); - free(fullPath); - return exists; -} - -static char* glfwReadFileText(FileSystem* fs, const char* relativePath) { - char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath); - FILE* f = fopen(fullPath, "rb"); - free(fullPath); - if (f == nullptr) - return nullptr; - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - char* content = safeMalloc((size_t) size + 1); - size_t bytesRead = fread(content, 1, (size_t) size, f); - content[bytesRead] = '\0'; - fclose(f); - return content; -} - -static bool glfwWriteFileText(FileSystem* fs, const char* relativePath, const char* contents) { - char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath); - FILE* f = fopen(fullPath, "wb"); - free(fullPath); - if (f == nullptr) - return false; - - size_t len = strlen(contents); - size_t written = fwrite(contents, 1, len, f); - fclose(f); - return written == len; -} - -static bool glfwDeleteFile(FileSystem* fs, const char* relativePath) { - char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath); - int result = remove(fullPath); - free(fullPath); - return result == 0; -} - -static bool glfwReadFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) { - char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath); - FILE* f = fopen(fullPath, "rb"); - free(fullPath); - if (f == nullptr) - return false; - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - uint8_t* data = safeMalloc((size_t) size); - size_t bytesRead = fread(data, 1, (size_t) size, f); - fclose(f); - - *outData = data; - *outSize = (int32_t) bytesRead; - return true; -} - -static bool glfwWriteFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) { - char* fullPath = buildFullPath((GlfwFileSystem*) fs, relativePath); - FILE* f = fopen(fullPath, "wb"); - free(fullPath); - if (f == nullptr) - return false; - - size_t written = fwrite(data, 1, (size_t) size, f); - fclose(f); - return written == (size_t) size; -} - -// ===[ Vtable ]=== - -static FileSystemVtable glfwFileSystemVtable = { - .resolvePath = glfwResolvePath, - .fileExists = glfwFileExists, - .readFileText = glfwReadFileText, - .writeFileText = glfwWriteFileText, - .deleteFile = glfwDeleteFile, - .readFileBinary = glfwReadFileBinary, - .writeFileBinary = glfwWriteFileBinary, -}; - -// ===[ Lifecycle ]=== - -GlfwFileSystem* GlfwFileSystem_create(const char* dataWinPath) { - GlfwFileSystem* fs = safeCalloc(1, sizeof(GlfwFileSystem)); - fs->base.vtable = &glfwFileSystemVtable; - - // Derive basePath by stripping the filename from dataWinPath - const char* lastSlash = strrchr(dataWinPath, '/'); - const char* lastBackslash = strrchr(dataWinPath, '\\'); - if (lastBackslash != nullptr && (lastSlash == nullptr || lastBackslash > lastSlash)) - lastSlash = lastBackslash; - if (lastSlash != nullptr) { - size_t dirLen = (size_t) (lastSlash - dataWinPath + 1); // include the trailing / - fs->basePath = safeMalloc(dirLen + 1); - memcpy(fs->basePath, dataWinPath, dirLen); - fs->basePath[dirLen] = '\0'; - } else { - // data.win is in current directory - fs->basePath = safeStrdup("./"); - } - - return fs; -} - -void GlfwFileSystem_destroy(GlfwFileSystem* fs) { - if (fs == nullptr) return; - free(fs->basePath); - free(fs); -} diff --git a/src/glfw/glfw_file_system.h b/src/glfw/glfw_file_system.h deleted file mode 100644 index addf2e63..00000000 --- a/src/glfw/glfw_file_system.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include "common.h" -#include "file_system.h" - -typedef struct { - FileSystem base; - char* basePath; // directory containing data.win, with trailing separator -} GlfwFileSystem; - -// Creates a GlfwFileSystem from the path to the data.win file -// The basePath is derived by stripping the filename from dataWinPath. -GlfwFileSystem* GlfwFileSystem_create(const char* dataWinPath); -void GlfwFileSystem_destroy(GlfwFileSystem* fs); diff --git a/src/glfw/glfw_gamepad.c b/src/glfw/glfw_gamepad.c deleted file mode 100644 index 467902c0..00000000 --- a/src/glfw/glfw_gamepad.c +++ /dev/null @@ -1,142 +0,0 @@ -#include "glfw_gamepad.h" - -#include -#include -#include - -// ===[ Internal helpers ]=== - -static float applyDeadzone(float value, float deadzone) { - if (value < 0.0f) { - if (value > -deadzone) return 0.0f; - return (value + deadzone) / (1.0f - deadzone); - } else { - if (value < deadzone) return 0.0f; - return (value - deadzone) / (1.0f - deadzone); - } -} - -enum { - IDX_LT = 6, - IDX_RT = 7, -}; - -static void mapGlfwToGml(const GLFWgamepadstate* glfwState, GamepadSlot* slot) { - memcpy(slot->buttonDownPrev, slot->buttonDown, sizeof(slot->buttonDown)); - memset(slot->buttonDown, 0, sizeof(slot->buttonDown)); - memset(slot->buttonPressed, 0, sizeof(slot->buttonPressed)); - memset(slot->buttonReleased, 0, sizeof(slot->buttonReleased)); - memset(slot->buttonValue, 0, sizeof(slot->buttonValue)); - memset(slot->axisValue, 0, sizeof(slot->axisValue)); - - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_A]) slot->buttonDown[0] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_B]) slot->buttonDown[1] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_X]) slot->buttonDown[2] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_Y]) slot->buttonDown[3] = true; - - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_LEFT_BUMPER]) slot->buttonDown[4] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER]) slot->buttonDown[5] = true; - - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_BACK]) slot->buttonDown[8] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_START]) slot->buttonDown[9] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_GUIDE]) slot->buttonDown[16] = true; - - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_LEFT_THUMB]) slot->buttonDown[10] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_RIGHT_THUMB]) slot->buttonDown[11] = true; - - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_DPAD_UP]) slot->buttonDown[12] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_DPAD_DOWN]) slot->buttonDown[13] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_DPAD_LEFT]) slot->buttonDown[14] = true; - if (glfwState->buttons[GLFW_GAMEPAD_BUTTON_DPAD_RIGHT]) slot->buttonDown[15] = true; - - float lt = glfwState->axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER]; - float rt = glfwState->axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER]; - if (lt < 0.0f) lt = 0.0f; - if (rt < 0.0f) rt = 0.0f; - slot->buttonValue[IDX_LT] = lt; - slot->buttonValue[IDX_RT] = rt; - if (lt >= slot->triggerThreshold) slot->buttonDown[IDX_LT] = true; - if (rt >= slot->triggerThreshold) slot->buttonDown[IDX_RT] = true; - - float lh = glfwState->axes[GLFW_GAMEPAD_AXIS_LEFT_X]; - float lv = glfwState->axes[GLFW_GAMEPAD_AXIS_LEFT_Y]; - float rh = glfwState->axes[GLFW_GAMEPAD_AXIS_RIGHT_X]; - float rv = glfwState->axes[GLFW_GAMEPAD_AXIS_RIGHT_Y]; - - slot->axisValue[0] = applyDeadzone(lh, slot->deadzone); - slot->axisValue[1] = applyDeadzone(lv, slot->deadzone); - slot->axisValue[2] = applyDeadzone(rh, slot->deadzone); - slot->axisValue[3] = applyDeadzone(rv, slot->deadzone); - - for (int i = 0; GP_BUTTON_COUNT > i; i++) { - if (i == IDX_LT || i == IDX_RT) continue; - slot->buttonValue[i] = slot->buttonDown[i] ? 1.0f : 0.0f; - } -} - -// ===[ Public API ]=== - -void GlfwGamepad_loadMappings(const char* mappings) { - if (mappings != NULL && mappings[0] != '\0') { - if (glfwUpdateGamepadMappings(mappings)) { - fprintf(stderr, "Gamepad: Loaded SDL gamecontroller mappings successfully\n"); - } else { - fprintf(stderr, "Gamepad: Failed to load SDL gamecontroller mappings\n"); - } - } -} - -void GlfwGamepad_poll(RunnerGamepadState* gp) { - for (int slotIdx = 0; slotIdx < 1 && slotIdx < MAX_GAMEPADS; slotIdx++) { - GamepadSlot* slot = &gp->slots[slotIdx]; - - bool currentlyConnected = false; - int foundJid = -1; - - for (int jid = GLFW_JOYSTICK_1; jid <= GLFW_JOYSTICK_16; jid++) { - if (glfwJoystickPresent(jid) && glfwJoystickIsGamepad(jid)) { - foundJid = jid; - currentlyConnected = true; - break; - } - } - - if (currentlyConnected) { - GLFWgamepadstate state; - if (glfwGetGamepadState(foundJid, &state)) { - mapGlfwToGml(&state, slot); - slot->jid = foundJid; - slot->connected = true; - - const char* name = glfwGetJoystickName(foundJid); - if (name != NULL) { - strncpy(slot->description, name, sizeof(slot->description) - 1); - slot->description[sizeof(slot->description) - 1] = '\0'; - } - - const char* guid = glfwGetJoystickGUID(foundJid); - if (guid != NULL) { - strncpy(slot->guid, guid, sizeof(slot->guid) - 1); - slot->guid[sizeof(slot->guid) - 1] = '\0'; - } else { - slot->guid[0] = '\0'; - } - } else { - slot->connected = false; - slot->guid[0] = '\0'; - } - } else { - slot->connected = false; - slot->guid[0] = '\0'; - } - - if (slot->connected) { - for (int btn = 0; GP_BUTTON_COUNT > btn; btn++) { - bool wasDown = slot->buttonDownPrev[btn]; - if (slot->buttonDown[btn] && !wasDown) slot->buttonPressed[btn] = true; - if (!slot->buttonDown[btn] && wasDown) slot->buttonReleased[btn] = true; - } - gp->connectedCount++; - } - } -} diff --git a/src/glfw/glfw_gamepad.h b/src/glfw/glfw_gamepad.h deleted file mode 100644 index 48874288..00000000 --- a/src/glfw/glfw_gamepad.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "../runner_gamepad.h" - -// Loads SDL gamecontroller mappings into GLFW (call after glfwInit). -void GlfwGamepad_loadMappings(const char* mappings); -// Reads the physical joystick state from GLFW and updates RunnerGamepadState. -void GlfwGamepad_poll(RunnerGamepadState* gp); diff --git a/src/glfw/ma_audio_system.c b/src/glfw/ma_audio_system.c deleted file mode 100644 index ea463886..00000000 --- a/src/glfw/ma_audio_system.c +++ /dev/null @@ -1,683 +0,0 @@ -// On Windows, include windows.h first so its headers are processed before stb_vorbis -// defines single-letter macros (L, C, R) that conflict with winnt.h struct field names. -#ifdef _WIN32 -#include -#endif - -// Include stb_vorbis BEFORE miniaudio so that STB_VORBIS_INCLUDE_STB_VORBIS_H is defined, -// which enables miniaudio's built-in OGG Vorbis decoding support. -#include "stb_vorbis.c" - -#define MINIAUDIO_IMPLEMENTATION -#include "miniaudio.h" - -#include "ma_audio_system.h" -#include "data_win.h" -#include "utils.h" - -#include -#include -#include -#include "stb_ds.h" - -// ===[ Helpers ]=== - -static SoundInstance* findFreeSlot(MaAudioSystem* ma) { - // First pass: find an inactive slot - repeat(MAX_SOUND_INSTANCES, i) { - if (!ma->instances[i].active) { - return &ma->instances[i]; - } - } - - // Second pass: evict the lowest-priority ended sound - SoundInstance* best = nullptr; - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (!ma_sound_is_playing(&inst->maSound)) { - if (best == nullptr || best->priority > inst->priority) { - best = inst; - } - } - } - - if (best != nullptr) { - ma_sound_uninit(&best->maSound); - if (best->ownsDecoder) { - ma_decoder_uninit(&best->decoder); - } - best->active = false; - } - - return best; -} - -static SoundInstance* findInstanceById(MaAudioSystem* ma, int32_t instanceId) { - int32_t slotIndex = instanceId - SOUND_INSTANCE_ID_BASE; - if (0 > slotIndex || slotIndex >= MAX_SOUND_INSTANCES) return nullptr; - SoundInstance* inst = &ma->instances[slotIndex]; - if (!inst->active || inst->instanceId != instanceId) return nullptr; - return inst; -} - -// Helper: resolve external audio file path from Sound entry -static char* resolveExternalPath(MaAudioSystem* ma, Sound* sound) { - const char* file = sound->file; - if (file == nullptr || file[0] == '\0') return nullptr; - - // If the filename has no extension, append ".ogg" - bool hasExtension = (strchr(file, '.') != nullptr); - - char filename[512]; - if (hasExtension) { - snprintf(filename, sizeof(filename), "%s", file); - } else { - snprintf(filename, sizeof(filename), "%s.ogg", file); - } - - return ma->fileSystem->vtable->resolvePath(ma->fileSystem, filename); -} - -// ===[ Vtable Implementations ]=== - -static void maInit(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - arrput(ma->base.audioGroups, dataWin); - ma->fileSystem = fileSystem; - - ma_engine_config config = ma_engine_config_init(); - ma_result result = ma_engine_init(&config, &ma->engine); - if (result != MA_SUCCESS) { - fprintf(stderr, "Audio: Failed to initialize miniaudio engine (error %d)\n", result); - return; - } - - memset(ma->instances, 0, sizeof(ma->instances)); - ma->nextInstanceCounter = 0; - - fprintf(stderr, "Audio: miniaudio engine initialized\n"); -} - -static void maDestroy(AudioSystem* audio) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - // Uninit all active sound instances - repeat(MAX_SOUND_INSTANCES, i) { - if (ma->instances[i].active) { - ma_sound_uninit(&ma->instances[i].maSound); - if (ma->instances[i].ownsDecoder) { - ma_decoder_uninit(&ma->instances[i].decoder); - } - ma->instances[i].active = false; - } - } - - // Free stream entries - repeat(MAX_AUDIO_STREAMS, i) { - if (ma->streams[i].active) { - free(ma->streams[i].filePath); - } - } - - // Free loaded audio groups. The main data.win is owned by the caller, so skip index 0. - if (arrlen(ma->base.audioGroups) > 1) { - for (int32_t i = 1; i < (int32_t) arrlen(ma->base.audioGroups); i++) { - DataWin_free(ma->base.audioGroups[i]); - } - } - arrfree(ma->base.audioGroups); - - ma_engine_uninit(&ma->engine); - free(ma); -} - -static void maUpdate(AudioSystem* audio, float deltaTime) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (!inst->active) continue; - - // Handle gain fading (for cases where we do manual fading) - if (inst->fadeTimeRemaining > 0.0f) { - inst->fadeTimeRemaining -= deltaTime; - if (0.0f >= inst->fadeTimeRemaining) { - inst->fadeTimeRemaining = 0.0f; - inst->currentGain = inst->targetGain; - } else { - float t = 1.0f - (inst->fadeTimeRemaining / inst->fadeTotalTime); - inst->currentGain = inst->startGain + (inst->targetGain - inst->startGain) * t; - } - ma_sound_set_volume(&inst->maSound, inst->currentGain); - } - - // Clean up ended non-looping sounds (ma_sound_at_end avoids reaping still-loading async sounds) - if (ma_sound_at_end(&inst->maSound) && !ma_sound_is_looping(&inst->maSound)) { - ma_sound_uninit(&inst->maSound); - if (inst->ownsDecoder) { - ma_decoder_uninit(&inst->decoder); - } - inst->active = false; - } - } -} - -static int32_t maPlaySound(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - // Check if this is a stream index (created by audio_create_stream) - bool isStream = (soundIndex >= AUDIO_STREAM_INDEX_BASE); - Sound* sound = nullptr; - char* streamPath = nullptr; - - if (isStream) { - int32_t streamSlot = soundIndex - AUDIO_STREAM_INDEX_BASE; - if (0 > streamSlot || streamSlot >= MAX_AUDIO_STREAMS || !ma->streams[streamSlot].active) { - fprintf(stderr, "Audio: Invalid stream index %d\n", soundIndex); - return -1; - } - streamPath = ma->streams[streamSlot].filePath; - } else { - DataWin* dw = ma->base.audioGroups[0]; // Audio Group 0 should always be data.win - if (0 > soundIndex || (uint32_t) soundIndex >= dw->sond.count) { - fprintf(stderr, "Audio: Invalid sound index %d\n", soundIndex); - return -1; - } - sound = &dw->sond.sounds[soundIndex]; - } - - SoundInstance* slot = findFreeSlot(ma); - if (slot == nullptr) { - fprintf(stderr, "Audio: No free sound slots for sound %d\n", soundIndex); - return -1; - } - - int32_t slotIndex = (int32_t) (slot - ma->instances); - ma_result result; - - if (isStream) { - // Stream audio: load from file path stored in stream entry - result = ma_sound_init_from_file(&ma->engine, streamPath, MA_SOUND_FLAG_ASYNC, nullptr, nullptr, &slot->maSound); - if (result != MA_SUCCESS) { - fprintf(stderr, "Audio: Failed to load stream file '%s' (error %d)\n", streamPath, result); - return -1; - } - slot->ownsDecoder = false; - } else { - bool isEmbedded = (sound->flags & 0x01) != 0; - bool isCompressed = (sound->flags & 0x02) != 0; - - if (isEmbedded || isCompressed) { - // Embedded audio: decode from AUDO chunk memory - if (0 > sound->audioFile || (uint32_t) sound->audioFile >= ma->base.audioGroups[sound->audioGroup]->audo.count) { - fprintf(stderr, "Audio: Invalid audio file index %d for sound '%s'\n", sound->audioFile, sound->name); - return -1; - } - - AudioEntry* entry = &ma->base.audioGroups[sound->audioGroup]->audo.entries[sound->audioFile]; - - ma_decoder_config decoderConfig = ma_decoder_config_init_default(); - result = ma_decoder_init_memory(entry->data, entry->dataSize, &decoderConfig, &slot->decoder); - if (result != MA_SUCCESS) { - fprintf(stderr, "Audio: Failed to init decoder for '%s' (error %d)\n", sound->name, result); - return -1; - } - slot->ownsDecoder = true; - - result = ma_sound_init_from_data_source(&ma->engine, &slot->decoder, 0, nullptr, &slot->maSound); - if (result != MA_SUCCESS) { - fprintf(stderr, "Audio: Failed to init sound from decoder for '%s' (error %d)\n", sound->name, result); - ma_decoder_uninit(&slot->decoder); - return -1; - } - } else { - // External audio: load from file - char* path = resolveExternalPath(ma, sound); - if (path == nullptr) { - fprintf(stderr, "Audio: Could not resolve path for sound '%s'\n", sound->name); - return -1; - } - - result = ma_sound_init_from_file(&ma->engine, path, MA_SOUND_FLAG_ASYNC, nullptr, nullptr, &slot->maSound); - if (result != MA_SUCCESS) { - fprintf(stderr, "Audio: Failed to load file for '%s' at '%s' (error %d)\n", sound->name, path, result); - free(path); - return -1; - } - free(path); - slot->ownsDecoder = false; - } - } - - // Apply properties - float volume = isStream ? 1.0f : sound->volume; - float pitch = isStream ? 1.0f : sound->pitch; - ma_sound_set_volume(&slot->maSound, volume); - if (pitch != 1.0f) { - ma_sound_set_pitch(&slot->maSound, pitch); - } - ma_sound_set_looping(&slot->maSound, loop); - - // Set up instance tracking - slot->active = true; - slot->soundIndex = soundIndex; - slot->instanceId = SOUND_INSTANCE_ID_BASE + slotIndex; - slot->currentGain = volume; - slot->targetGain = volume; - slot->fadeTimeRemaining = 0.0f; - slot->fadeTotalTime = 0.0f; - slot->startGain = volume; - slot->priority = priority; - - // Track unique IDs for disambiguation - ma->nextInstanceCounter++; - - ma_sound_start(&slot->maSound); - - return slot->instanceId; -} - -static void maStopSound(AudioSystem* audio, int32_t soundOrInstance) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - // Stop specific instance - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - if (inst != nullptr) { - ma_sound_stop(&inst->maSound); - ma_sound_uninit(&inst->maSound); - if (inst->ownsDecoder) { - ma_decoder_uninit(&inst->decoder); - } - inst->active = false; - } - } else { - // Stop all instances of this sound resource - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - ma_sound_stop(&inst->maSound); - ma_sound_uninit(&inst->maSound); - if (inst->ownsDecoder) { - ma_decoder_uninit(&inst->decoder); - } - inst->active = false; - } - } - } -} - -static void maStopAll(AudioSystem* audio) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active) { - ma_sound_stop(&inst->maSound); - ma_sound_uninit(&inst->maSound); - if (inst->ownsDecoder) { - ma_decoder_uninit(&inst->decoder); - } - inst->active = false; - } - } -} - -static bool maIsPlaying(AudioSystem* audio, int32_t soundOrInstance) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - return inst != nullptr && ma_sound_is_playing(&inst->maSound); - } else { - // Check if any instance of this sound resource is playing - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance && ma_sound_is_playing(&inst->maSound)) { - return true; - } - } - return false; - } -} - -static void maPauseSound(AudioSystem* audio, int32_t soundOrInstance) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - if (inst != nullptr) { - ma_sound_stop(&inst->maSound); - } - } else { - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - ma_sound_stop(&inst->maSound); - } - } - } -} - -static void maResumeSound(AudioSystem* audio, int32_t soundOrInstance) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - if (inst != nullptr) { - ma_sound_start(&inst->maSound); - } - } else { - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - ma_sound_start(&inst->maSound); - } - } - } -} - -static void maPauseAll(AudioSystem* audio) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && ma_sound_is_playing(&inst->maSound)) { - ma_sound_stop(&inst->maSound); - } - } -} - -static void maResumeAll(AudioSystem* audio) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active) { - ma_sound_start(&inst->maSound); - } - } -} - -static void maSetSoundGain(AudioSystem* audio, int32_t soundOrInstance, float gain, uint32_t timeMs) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - if (inst != nullptr) { - if (timeMs == 0) { - inst->currentGain = gain; - inst->targetGain = gain; - inst->fadeTimeRemaining = 0.0f; - ma_sound_set_volume(&inst->maSound, gain); - } else { - inst->startGain = inst->currentGain; - inst->targetGain = gain; - inst->fadeTotalTime = (float) timeMs / 1000.0f; - inst->fadeTimeRemaining = inst->fadeTotalTime; - } - } - } else { - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - if (timeMs == 0) { - inst->currentGain = gain; - inst->targetGain = gain; - inst->fadeTimeRemaining = 0.0f; - ma_sound_set_volume(&inst->maSound, gain); - } else { - inst->startGain = inst->currentGain; - inst->targetGain = gain; - inst->fadeTotalTime = (float) timeMs / 1000.0f; - inst->fadeTimeRemaining = inst->fadeTotalTime; - } - } - } - } -} - -static float maGetSoundGain(AudioSystem* audio, int32_t soundOrInstance) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - if (inst != nullptr) return inst->currentGain; - } else { - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - return inst->currentGain; - } - } - } - return 0.0f; -} - -static void maSetSoundPitch(AudioSystem* audio, int32_t soundOrInstance, float pitch) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - if (inst != nullptr) { - ma_sound_set_pitch(&inst->maSound, pitch); - } - } else { - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - ma_sound_set_pitch(&inst->maSound, pitch); - } - } - } -} - -static float maGetSoundPitch(AudioSystem* audio, int32_t soundOrInstance) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - if (inst != nullptr) return ma_sound_get_pitch(&inst->maSound); - } else { - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - return ma_sound_get_pitch(&inst->maSound); - } - } - } - return 1.0f; -} - -static float maGetTrackPosition(AudioSystem* audio, int32_t soundOrInstance) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - if (inst != nullptr) { - float cursor; - ma_result result = ma_sound_get_cursor_in_seconds(&inst->maSound, &cursor); - if (result == MA_SUCCESS) return cursor; - } - } else { - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - float cursor; - ma_result result = ma_sound_get_cursor_in_seconds(&inst->maSound, &cursor); - if (result == MA_SUCCESS) return cursor; - } - } - } - return 0.0f; -} - -static void maSetTrackPosition(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - SoundInstance* inst = findInstanceById(ma, soundOrInstance); - if (inst != nullptr) { - ma_sound_seek_to_pcm_frame(&inst->maSound, (ma_uint64) (positionSeconds * 44100.0f)); - } - } else { - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - ma_sound_seek_to_pcm_frame(&inst->maSound, (ma_uint64) (positionSeconds * 44100.0f)); - } - } - } -} - -// Total length of a loaded sound. Works on both SOND index and active instance ids. -// Uses miniaudio's ma_sound_get_length_in_seconds, which reads the decoded duration from the underlying data source (works for fully-decoded sounds AND streaming sounds). -static float maGetSoundLength(AudioSystem* audio, int32_t soundOrInstance) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - SoundInstance* match = nullptr; - if (soundOrInstance >= SOUND_INSTANCE_ID_BASE) { - match = findInstanceById(ma, soundOrInstance); - } else { - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance) { - match = inst; - break; - } - } - } - if (match == nullptr) return 0.0f; - - float seconds = 0.0f; - if (ma_sound_get_length_in_seconds(&match->maSound, &seconds) != MA_SUCCESS) return 0.0f; - return seconds; -} - -static void maSetMasterGain(AudioSystem* audio, float gain) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - ma_engine_set_volume(&ma->engine, gain); -} - -static void maSetChannelCount(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t count) { - // miniaudio handles channel management internally, this is a no-op -} - -static void maGroupLoad(AudioSystem* audio, int32_t groupIndex) { - if (groupIndex > 0) { - int sz = snprintf(nullptr, 0, "audiogroup%d.dat", groupIndex); - char buf[sz + 1]; - snprintf(buf, sizeof(buf), "audiogroup%d.dat", groupIndex); - DataWin *audioGroup = DataWin_parse(((MaAudioSystem*)audio)->fileSystem->vtable->resolvePath(((MaAudioSystem*)audio)->fileSystem, buf), - (DataWinParserOptions) { - .parseAudo = true, - }); - arrput(audio->audioGroups, audioGroup); - } -} - -static bool maGroupIsLoaded(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t groupIndex) { - return (arrlen(audio->audioGroups) > groupIndex); -} - -// ===[ Audio Streams ]=== - -static int32_t maCreateStream(AudioSystem* audio, const char* filename) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - // Find a free stream slot - int32_t freeSlot = -1; - repeat(MAX_AUDIO_STREAMS, i) { - if (!ma->streams[i].active) { - freeSlot = (int32_t) i; - break; - } - } - - if (0 > freeSlot) { - fprintf(stderr, "Audio: No free stream slots for '%s'\n", filename); - return -1; - } - - char* resolved = ma->fileSystem->vtable->resolvePath(ma->fileSystem, filename); - if (resolved == nullptr) { - fprintf(stderr, "Audio: Could not resolve path for stream '%s'\n", filename); - return -1; - } - - ma->streams[freeSlot].active = true; - ma->streams[freeSlot].filePath = resolved; - - int32_t streamIndex = AUDIO_STREAM_INDEX_BASE + freeSlot; - fprintf(stderr, "Audio: Created stream %d for '%s' -> '%s'\n", streamIndex, filename, resolved); - return streamIndex; -} - -static bool maDestroyStream(AudioSystem* audio, int32_t streamIndex) { - MaAudioSystem* ma = (MaAudioSystem*) audio; - - int32_t slotIndex = streamIndex - AUDIO_STREAM_INDEX_BASE; - if (0 > slotIndex || slotIndex >= MAX_AUDIO_STREAMS) { - fprintf(stderr, "Audio: Invalid stream index %d for destroy\n", streamIndex); - return false; - } - - AudioStreamEntry* entry = &ma->streams[slotIndex]; - if (!entry->active) return false; - - // Stop all sound instances that were playing this stream - repeat(MAX_SOUND_INSTANCES, i) { - SoundInstance* inst = &ma->instances[i]; - if (inst->active && inst->soundIndex == streamIndex) { - ma_sound_stop(&inst->maSound); - ma_sound_uninit(&inst->maSound); - if (inst->ownsDecoder) { - ma_decoder_uninit(&inst->decoder); - } - inst->active = false; - } - } - - free(entry->filePath); - entry->filePath = nullptr; - entry->active = false; - fprintf(stderr, "Audio: Destroyed stream %d\n", streamIndex); - return true; -} - -// ===[ Vtable ]=== - -static AudioSystemVtable maAudioSystemVtable = { - .init = maInit, - .destroy = maDestroy, - .update = maUpdate, - .playSound = maPlaySound, - .stopSound = maStopSound, - .stopAll = maStopAll, - .isPlaying = maIsPlaying, - .pauseSound = maPauseSound, - .resumeSound = maResumeSound, - .pauseAll = maPauseAll, - .resumeAll = maResumeAll, - .setSoundGain = maSetSoundGain, - .getSoundGain = maGetSoundGain, - .setSoundPitch = maSetSoundPitch, - .getSoundPitch = maGetSoundPitch, - .getTrackPosition = maGetTrackPosition, - .setTrackPosition = maSetTrackPosition, - .getSoundLength = maGetSoundLength, - .setMasterGain = maSetMasterGain, - .setChannelCount = maSetChannelCount, - .groupLoad = maGroupLoad, - .groupIsLoaded = maGroupIsLoaded, - .createStream = maCreateStream, - .destroyStream = maDestroyStream, -}; - -// ===[ Lifecycle ]=== - -MaAudioSystem* MaAudioSystem_create(void) { - MaAudioSystem* ma = safeCalloc(1, sizeof(MaAudioSystem)); - ma->base.vtable = &maAudioSystemVtable; - return ma; -} diff --git a/src/glfw/ma_audio_system.h b/src/glfw/ma_audio_system.h deleted file mode 100644 index 627c75d4..00000000 --- a/src/glfw/ma_audio_system.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include "common.h" -#include "audio_system.h" -#include "miniaudio.h" - -#define MAX_SOUND_INSTANCES 128 -#define SOUND_INSTANCE_ID_BASE 100000 -#define MAX_AUDIO_STREAMS 32 -// This is the index space that the native runner uses -#define AUDIO_STREAM_INDEX_BASE 300000 - -typedef struct { - bool active; - int32_t soundIndex; // SOND resource that spawned this - int32_t instanceId; // unique ID returned to GML - ma_sound maSound; // miniaudio sound object - ma_decoder decoder; // decoder for memory-based audio - bool ownsDecoder; // true if decoder needs uninit - float targetGain; - float currentGain; - float fadeTimeRemaining; - float fadeTotalTime; - float startGain; - int32_t priority; -} SoundInstance; - -typedef struct { - bool active; - char* filePath; // resolved file path (owned, freed on destroy) -} AudioStreamEntry; - -typedef struct { - AudioSystem base; - ma_engine engine; - SoundInstance instances[MAX_SOUND_INSTANCES]; - int32_t nextInstanceCounter; - FileSystem* fileSystem; - AudioStreamEntry streams[MAX_AUDIO_STREAMS]; -} MaAudioSystem; - -MaAudioSystem* MaAudioSystem_create(void); diff --git a/src/glfw/main.c b/src/glfw/main.c deleted file mode 100644 index e8bce9c3..00000000 --- a/src/glfw/main.c +++ /dev/null @@ -1,1197 +0,0 @@ -#include "data_win.h" -#include "glfw/gl_legacy_renderer.h" -#include "vm.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef _WIN32 -#include -#endif -#ifdef __GLIBC__ -#include -#endif - -#include "runner_keyboard.h" -#include "glfw_gamepad.h" -#include "runner.h" -#include "input_recording.h" -#include "debug_overlay.h" -#include "gl_renderer.h" -#include "glfw_file_system.h" -#include "ma_audio_system.h" -#include "noop_audio_system.h" -#include "stb_ds.h" -#include "stb_image_write.h" - -#include "utils.h" -#include "profiler.h" - -static void glfwErrorCallback(int code, const char* description) { - fprintf(stderr, "GLFW error 0x%x: %s\n", code, description); -} - -#ifndef ENABLE_GLES -static void APIENTRY glDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, MAYBE_UNUSED GLsizei length, const GLchar* message, MAYBE_UNUSED const void* userParam) { - const char* sourceStr; - switch (source) { - case GL_DEBUG_SOURCE_API: sourceStr = "API"; break; - case GL_DEBUG_SOURCE_WINDOW_SYSTEM: sourceStr = "Window System"; break; - case GL_DEBUG_SOURCE_SHADER_COMPILER: sourceStr = "Shader Compiler"; break; - case GL_DEBUG_SOURCE_THIRD_PARTY: sourceStr = "Third Party"; break; - case GL_DEBUG_SOURCE_APPLICATION: sourceStr = "Application"; break; - case GL_DEBUG_SOURCE_OTHER: sourceStr = "Other"; break; - default: sourceStr = "Unknown"; break; - } - - const char* typeStr; - switch (type) { - case GL_DEBUG_TYPE_ERROR: typeStr = "Error"; break; - case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: typeStr = "Deprecated Behaviour"; break; - case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: typeStr = "Undefined Behaviour"; break; - case GL_DEBUG_TYPE_PORTABILITY: typeStr = "Portability"; break; - case GL_DEBUG_TYPE_PERFORMANCE: typeStr = "Performance"; break; - case GL_DEBUG_TYPE_MARKER: typeStr = "Marker"; break; - case GL_DEBUG_TYPE_PUSH_GROUP: typeStr = "Push Group"; break; - case GL_DEBUG_TYPE_POP_GROUP: typeStr = "Pop Group"; break; - case GL_DEBUG_TYPE_OTHER: typeStr = "Other"; break; - default: typeStr = "Unknown"; break; - } - - const char* severityStr; - switch (severity) { - case GL_DEBUG_SEVERITY_HIGH: severityStr = "High"; break; - case GL_DEBUG_SEVERITY_MEDIUM: severityStr = "Medium"; break; - case GL_DEBUG_SEVERITY_LOW: severityStr = "Low"; break; - case GL_DEBUG_SEVERITY_NOTIFICATION: severityStr = "Notification"; break; - default: severityStr = "Unknown"; break; - } - - fprintf(stderr, "[OpenGL %s] id=%u Type: %s; Severity: %s; Message: %.*s\n", sourceStr, id, typeStr, severityStr, (int) length, message); -} - -static void installGLDebugCallback(void) { - if (!GLAD_GL_KHR_debug) { - fprintf(stderr, "OpenGL debug callback not available (driver does not expose GL_KHR_debug)\n"); - return; - } - - glEnable(GL_DEBUG_OUTPUT); - glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); - glDebugMessageCallbackKHR(glDebugCallback, nullptr); - glDebugMessageControlKHR(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); -} -#endif - -// ===[ COMMAND LINE ARGUMENTS ]=== -typedef struct { - int key; - // We need this dummy value, think that the ds_map is like a Java HashMap NOT a HashSet - // (Which is funny, because in Java HashSets are backed by HashMaps lol) - bool value; -} FrameSetEntry; - -typedef struct { - const char* dataWinPath; - const char* screenshotPattern; - FrameSetEntry* screenshotFrames; - FrameSetEntry* dumpFrames; - FrameSetEntry* dumpJsonFrames; - const char* dumpJsonFilePattern; - StringBooleanEntry* varReadsToBeTraced; - StringBooleanEntry* varWritesToBeTraced; - StringBooleanEntry* functionCallsToBeTraced; - StringBooleanEntry* alarmsToBeTraced; - StringBooleanEntry* instanceLifecyclesToBeTraced; - StringBooleanEntry* eventsToBeTraced; - StringBooleanEntry* opcodesToBeTraced; - StringBooleanEntry* stackToBeTraced; - StringBooleanEntry* disassemble; - StringBooleanEntry* tilesToBeTraced; - bool alwaysLogUnknownFunctions; - bool alwaysLogStubbedFunctions; - bool headless; - bool traceFrames; - bool printRooms; - bool printDeclaredFunctions; - int exitAtFrame; - int traceBytecodeAfterFrame; - double speedMultiplier; - double fastForwardSpeed; - int seed; - bool hasSeed; - bool debug; - bool traceEventInherited; - const char* recordInputsPath; - const char* playbackInputsPath; - const char* renderer; - YoYoOperatingSystem osType; - bool lazyRooms; - StringBooleanEntry* eagerRooms; // stb_ds string-keyed set of room names - int profilerFramesBetween; // 0 = disabled -#ifdef ENABLE_VM_OPCODE_PROFILER - bool opcodeProfiler; -#endif -} CommandLineArgs; - -typedef struct { const char* name; YoYoOperatingSystem value; } OsTypeNameEntry; - -static const OsTypeNameEntry OS_TYPE_NAMES[] = { - {"unknown", OS_UNKNOWN}, - {"windows", OS_WINDOWS}, - {"win32", OS_WINDOWS}, - {"macosx", OS_MACOSX}, - {"macos", OS_MACOSX}, - {"psp", OS_PSP}, - {"ios", OS_IOS}, - {"android", OS_ANDROID}, - {"symbian", OS_SYMBIAN}, - {"linux", OS_LINUX}, - {"winphone", OS_WINPHONE}, - {"tizen", OS_TIZEN}, - {"win8native", OS_WIN8NATIVE}, - {"wiiu", OS_WIIU}, - {"3ds", OS_3DS}, - {"psvita", OS_PSVITA}, - {"bb10", OS_BB10}, - {"ps4", OS_PS4}, - {"xboxone", OS_XBOXONE}, - {"ps3", OS_PS3}, - {"xbox360", OS_XBOX360}, - {"uwp", OS_UWP}, - {"amazon", OS_AMAZON}, - {"switch", OS_SWITCH}, -}; -#define OS_TYPE_NAMES_COUNT (sizeof(OS_TYPE_NAMES)/sizeof(OS_TYPE_NAMES[0])) - -static bool parseOsTypeArg(const char* s, YoYoOperatingSystem* out) { - forEach(const OsTypeNameEntry, entry, OS_TYPE_NAMES, OS_TYPE_NAMES_COUNT) { - if (strcmp(s, entry->name) == 0) { - *out = entry->value; - return true; - } - } - return false; -} - -static void printOsTypeNames(FILE* out) { - forEachIndexed(const OsTypeNameEntry, entry, i, OS_TYPE_NAMES, OS_TYPE_NAMES_COUNT) { - fprintf(out, "%s%s", i > 0 ? ", " : "", entry->name); - } -} - -static void parseCommandLineArgs(CommandLineArgs* args, int argc, char* argv[]) { - memset(args, 0, sizeof(CommandLineArgs)); - - static struct option longOptions[] = { - {"screenshot", required_argument, nullptr, 's'}, - {"screenshot-at-frame", required_argument, nullptr, 'f'}, - {"headless", no_argument, nullptr, 'h'}, - {"print-rooms", no_argument, nullptr, 'r'}, - {"print-declared-functions", no_argument, nullptr, 'p'}, - {"trace-variable-reads", required_argument, nullptr, 'R'}, - {"trace-variable-writes", required_argument, nullptr, 'W'}, - {"trace-function-calls", required_argument, nullptr, 'c'}, - {"trace-alarms", required_argument, nullptr, 'a'}, - {"trace-instance-lifecycles", required_argument, nullptr, 'l'}, - {"trace-events", required_argument, nullptr, 'e'}, - {"trace-event-inherited", no_argument, nullptr, 'E'}, - {"trace-tiles", required_argument, nullptr, 'T'}, - {"trace-opcodes", required_argument, nullptr, 'o'}, - {"trace-stack", required_argument, nullptr, 'S'}, - {"trace-frames", no_argument, nullptr, 'k'}, - {"always-log-unknown-functions", no_argument, nullptr, 'y'}, - {"always-log-stubbed-functions", no_argument, nullptr, 'Y'}, - {"exit-at-frame", required_argument, nullptr, 'x'}, - {"trace-bytecode-after-frame", required_argument, nullptr, 'F'}, - {"dump-frame", required_argument, nullptr, 'd'}, - {"dump-frame-json", required_argument, nullptr, 'j'}, - {"dump-frame-json-file", required_argument, nullptr, 'J'}, - {"speed", required_argument, nullptr, 'M'}, - {"fast-forward-speed", required_argument, nullptr, 'X'}, - {"seed", required_argument, nullptr, 'Z'}, - {"debug", no_argument, nullptr, 'D'}, - {"disassemble", required_argument, nullptr, 'A'}, - {"record-inputs", required_argument, nullptr, 'I'}, - {"playback-inputs", required_argument, nullptr, 'P'}, - {"renderer", required_argument, nullptr, 'g'}, - {"lazy-rooms", no_argument, nullptr, 'z'}, - {"eager-room", required_argument, nullptr, 'G'}, - {"os-type", required_argument, nullptr, 'O'}, - {"profile-gml-scripts", required_argument, nullptr, 'q'}, -#ifdef ENABLE_VM_OPCODE_PROFILER - {"profile-opcodes", no_argument, nullptr, 'Q'}, -#endif - {nullptr, 0, nullptr, 0 } - }; - - args->screenshotFrames = nullptr; - args->exitAtFrame = -1; - args->traceBytecodeAfterFrame = 0; - args->speedMultiplier = 1.0; - args->fastForwardSpeed = 0.0; - args->renderer = "gl"; - args->osType = OS_WINDOWS; - args->profilerFramesBetween = 0; - - int opt; - while ((opt = getopt_long(argc, argv, "", longOptions, nullptr)) != -1) { - switch (opt) { - case 's': - args->screenshotPattern = optarg; - break; - case 'f': { - char* endPtr; - long frame = strtol(optarg, &endPtr, 10); - if (*endPtr != '\0' || 0 > frame) { - fprintf(stderr, "Error: Invalid frame number '%s'\n", optarg); - exit(1); - } - - hmput(args->screenshotFrames, (int) frame, true); - break; - } - case 'h': - args->headless = true; - break; - case 'r': - args->printRooms = true; - break; - case 'p': - args->printDeclaredFunctions = true; - break; - case 'R': - shput(args->varReadsToBeTraced, optarg, true); - break; - case 'W': - shput(args->varWritesToBeTraced, optarg, true); - break; - case 'c': - shput(args->functionCallsToBeTraced, optarg, true); - break; - case 'a': - shput(args->alarmsToBeTraced, optarg, true); - break; - case 'l': - shput(args->instanceLifecyclesToBeTraced, optarg, true); - break; - case 'e': - shput(args->eventsToBeTraced, optarg, true); - break; - case 'o': - shput(args->opcodesToBeTraced, optarg, true); - break; - case 'S': - shput(args->stackToBeTraced, optarg, true); - break; - case 'k': - args->traceFrames = true; - break; - case 'y': - args->alwaysLogUnknownFunctions = true; - break; - case 'Y': - args->alwaysLogStubbedFunctions = true; - break; - case 'x': { - char* endPtr; - long frame = strtol(optarg, &endPtr, 10); - if (*endPtr != '\0' || 0 > frame) { - fprintf(stderr, "Error: Invalid frame number '%s' for --exit-at-frame\n", optarg); - exit(1); - } - args->exitAtFrame = (int) frame; - break; - } - case 'F': { - char* endPtr; - long frame = strtol(optarg, &endPtr, 10); - if (*endPtr != '\0' || 0 > frame) { - fprintf(stderr, "Error: Invalid frame number '%s' for --trace-bytecode-after-frame\n", optarg); - exit(1); - } - args->traceBytecodeAfterFrame = (int) frame; - break; - } - case 'd': { - char* endPtr; - long frame = strtol(optarg, &endPtr, 10); - if (*endPtr != '\0' || 0 > frame) { - fprintf(stderr, "Error: Invalid frame number '%s' for --dump-frame\n", optarg); - exit(1); - } - hmput(args->dumpFrames, (int) frame, true); - break; - } - case 'j': { - char* endPtr; - long frame = strtol(optarg, &endPtr, 10); - if (*endPtr != '\0' || 0 > frame) { - fprintf(stderr, "Error: Invalid frame number '%s' for --dump-frame-json\n", optarg); - exit(1); - } - hmput(args->dumpJsonFrames, (int) frame, true); - break; - } - case 'J': - args->dumpJsonFilePattern = optarg; - break; - case 'M': { - char* endPtr; - double speed = strtod(optarg, &endPtr); - if (*endPtr != '\0' || speed <= 0.0) { - fprintf(stderr, "Error: Invalid speed multiplier '%s' for --speed (must be > 0)\n", optarg); - exit(1); - } - args->speedMultiplier = speed; - break; - } - case 'X': { - char* endPtr; - double speed = strtod(optarg, &endPtr); - if (*endPtr != '\0' || speed <= 0.0) { - fprintf(stderr, "Error: Invalid speed '%s' for --fast-forward-speed (must be > 0)\n", optarg); - exit(1); - } - args->fastForwardSpeed = speed; - break; - } - case 'D': - args->debug = true; - break; - case 'g': - args->renderer = optarg; - break; - case 'z': - args->lazyRooms = true; - break; - case 'G': - shput(args->eagerRooms, optarg, true); - break; - case 'A': - shput(args->disassemble, optarg, true); - break; - case 'T': - shput(args->tilesToBeTraced, optarg, true); - break; - case 'E': - args->traceEventInherited = true; - break; - case 'Z': { - char* endPtr; - long seedVal = strtol(optarg, &endPtr, 10); - if (*endPtr != '\0') { - fprintf(stderr, "Error: Invalid seed value '%s' for --seed\n", optarg); - exit(1); - } - args->seed = (int) seedVal; - args->hasSeed = true; - break; - } - case 'I': - args->recordInputsPath = optarg; - break; - case 'P': - args->playbackInputsPath = optarg; - break; - case 'q': { - char* endPtr; - long framesBetween = strtol(optarg, &endPtr, 10); - if (*endPtr != '\0' || framesBetween <= 0) { - fprintf(stderr, "Error: Invalid frame count '%s' for --profile-gml-scripts (must be > 0)\n", optarg); - exit(1); - } - args->profilerFramesBetween = (int) framesBetween; - break; - } -#ifdef ENABLE_VM_OPCODE_PROFILER - case 'Q': - args->opcodeProfiler = true; - break; -#endif - case 'O': - if (!parseOsTypeArg(optarg, &args->osType)) { - fprintf(stderr, "Error: Invalid --os-type value '%s' (expected: ", optarg); - printOsTypeNames(stderr); - fprintf(stderr, ")\n"); - exit(1); - } - break; - default: - fprintf(stderr, "Usage: %s [--headless] [--screenshot=PATTERN] [--screenshot-at-frame=N ...] \n", argv[0]); - exit(1); - } - } - - if (optind >= argc) { - fprintf(stderr, "Usage: %s [--headless] [--screenshot=PATTERN] [--screenshot-at-frame=N ...] \n", argv[0]); - exit(1); - } - - args->dataWinPath = argv[optind]; - - if (hmlen(args->screenshotFrames) > 0 && args->screenshotPattern == nullptr) { - fprintf(stderr, "Error: --screenshot-at-frame requires --screenshot to be set\n"); - exit(1); - } - - if (args->headless && args->speedMultiplier != 1.0) { - fprintf(stderr, "You can't set the speed multiplier while running in headless mode! Headless mode always run in real time\n"); - exit(1); - } - -} - -static void freeCommandLineArgs(CommandLineArgs* args) { - hmfree(args->screenshotFrames); - hmfree(args->dumpFrames); - hmfree(args->dumpJsonFrames); - shfree(args->varReadsToBeTraced); - shfree(args->varWritesToBeTraced); - shfree(args->functionCallsToBeTraced); - shfree(args->alarmsToBeTraced); - shfree(args->instanceLifecyclesToBeTraced); - shfree(args->eventsToBeTraced); - shfree(args->opcodesToBeTraced); - shfree(args->stackToBeTraced); - shfree(args->disassemble); - shfree(args->tilesToBeTraced); -} - -// ===[ SCREENSHOT ]=== -static void captureScreenshot(const char* filenamePattern, int frameNumber, int width, int height) { - char filename[512]; - snprintf(filename, sizeof(filename), filenamePattern, frameNumber); - - int stride = width * 4; - unsigned char* pixels = safeMalloc(stride * height); - if (pixels == nullptr) { - fprintf(stderr, "Error: Failed to allocate memory for screenshot (%dx%d)\n", width, height); - return; - } - - glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - - // OpenGL reads bottom-to-top, but PNG is top-to-bottom. - // Use stb's negative stride trick: point to the last row and use a negative stride to flip vertically. - unsigned char* lastRow = pixels + (height - 1) * stride; - stbi_write_png(filename, width, height, 4, lastRow, -stride); - - free(pixels); - printf("Screenshot saved: %s\n", filename); -} - -// ===[ KEYBOARD INPUT ]=== - -static int32_t glfwKeyToGml(int glfwKey) { - // Letters: GLFW_KEY_A (65) -> 65 (same as GML) - if (glfwKey >= GLFW_KEY_A && glfwKey <= GLFW_KEY_Z) return glfwKey; - // Numbers: GLFW_KEY_0 (48) -> 48 - if (glfwKey >= GLFW_KEY_0 && glfwKey <= GLFW_KEY_9) return glfwKey; - // Special keys need mapping - switch (glfwKey) { - case GLFW_KEY_ESCAPE: return VK_ESCAPE; - case GLFW_KEY_ENTER: return VK_ENTER; - case GLFW_KEY_TAB: return VK_TAB; - case GLFW_KEY_BACKSPACE: return VK_BACKSPACE; - case GLFW_KEY_SPACE: return VK_SPACE; - case GLFW_KEY_LEFT_SHIFT: - case GLFW_KEY_RIGHT_SHIFT: return VK_SHIFT; - case GLFW_KEY_LEFT_CONTROL: - case GLFW_KEY_RIGHT_CONTROL: return VK_CONTROL; - case GLFW_KEY_LEFT_ALT: - case GLFW_KEY_RIGHT_ALT: return VK_ALT; - case GLFW_KEY_UP: return VK_UP; - case GLFW_KEY_DOWN: return VK_DOWN; - case GLFW_KEY_LEFT: return VK_LEFT; - case GLFW_KEY_RIGHT: return VK_RIGHT; - case GLFW_KEY_F1: return VK_F1; - case GLFW_KEY_F2: return VK_F2; - case GLFW_KEY_F3: return VK_F3; - case GLFW_KEY_F4: return VK_F4; - case GLFW_KEY_F5: return VK_F5; - case GLFW_KEY_F6: return VK_F6; - case GLFW_KEY_F7: return VK_F7; - case GLFW_KEY_F8: return VK_F8; - case GLFW_KEY_F9: return VK_F9; - case GLFW_KEY_F10: return VK_F10; - case GLFW_KEY_F11: return VK_F11; - case GLFW_KEY_F12: return VK_F12; - case GLFW_KEY_INSERT: return VK_INSERT; - case GLFW_KEY_DELETE: return VK_DELETE; - case GLFW_KEY_HOME: return VK_HOME; - case GLFW_KEY_END: return VK_END; - case GLFW_KEY_PAGE_UP: return VK_PAGEUP; - case GLFW_KEY_PAGE_DOWN: return VK_PAGEDOWN; - default: return -1; // Unknown - } -} - -static InputRecording* globalInputRecording = nullptr; - -#if defined(__has_feature) - #if __has_feature(address_sanitizer) - #define BUTTERSCOTCH_HAS_ASAN 1 - #endif -#endif -#if defined(__SANITIZE_ADDRESS__) - #define BUTTERSCOTCH_HAS_ASAN 1 -#endif - -#if BUTTERSCOTCH_HAS_ASAN -void __asan_set_death_callback(void (*callback)(void)); -#endif - -static volatile sig_atomic_t crashSaveInProgress = 0; - -static void saveRecordingOnCrash(void) { - if (crashSaveInProgress) return; - crashSaveInProgress = 1; - if (globalInputRecording != nullptr && globalInputRecording->isRecording) { - InputRecording_save(globalInputRecording); - } -} - -static void crashSignalHandler(int sig) { - saveRecordingOnCrash(); - signal(sig, SIG_DFL); - raise(sig); -} - -static void installCrashHandlers(void) { -#if BUTTERSCOTCH_HAS_ASAN - __asan_set_death_callback(saveRecordingOnCrash); -#endif - signal(SIGSEGV, crashSignalHandler); - signal(SIGABRT, crashSignalHandler); -#ifdef SIGBUS - signal(SIGBUS, crashSignalHandler); -#endif - signal(SIGFPE, crashSignalHandler); - signal(SIGILL, crashSignalHandler); -} - -static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { - (void) scancode; (void) mods; - Runner* runner = (Runner*) glfwGetWindowUserPointer(window); - // During playback, suppress real keyboard input (window events like close still work) - if (InputRecording_isPlaybackActive(globalInputRecording)) return; - int32_t gmlKey = glfwKeyToGml(key); - if (0 > gmlKey) return; - if (action == GLFW_PRESS) RunnerKeyboard_onKeyDown(runner->keyboard, gmlKey); - else if (action == GLFW_RELEASE) RunnerKeyboard_onKeyUp(runner->keyboard, gmlKey); - // GLFW_REPEAT is ignored (GML doesn't use key repeat) -} - -static void characterCallback(GLFWwindow* window, unsigned int codepoint) { - Runner* runner = (Runner*) glfwGetWindowUserPointer(window); - if (InputRecording_isPlaybackActive(globalInputRecording)) return; - RunnerKeyboard_onCharacter(runner->keyboard, codepoint); -} - -static void setGlfwWindowTitle(void* window, const char* title) { - glfwSetWindowTitle((GLFWwindow*) window, title); -} - -static bool getGlfwWindowFocus(void* window) { - return glfwGetWindowAttrib((GLFWwindow*) window, GLFW_FOCUSED) != 0; -} - -void saveInputRecording() { - // Save input recording if active, then free - if (globalInputRecording != nullptr) { - if (globalInputRecording->isRecording) { - InputRecording_save(globalInputRecording); - } - InputRecording_free(globalInputRecording); - globalInputRecording = nullptr; - } -} - -#ifndef _WIN32 -typedef struct { int key; struct sigaction value; } PreviousSignalActionEntry; -static PreviousSignalActionEntry* previousSignalActions = nullptr; - -static void onCrashSignal(int sig) { - saveInputRecording(); - // Restore the previous handler (ASAN) and re-raise so it can report the fault - sigaction(sig, &previousSignalActions[hmgeti(previousSignalActions, sig)].value, nullptr); - raise(sig); -} -#endif - -// ===[ MAIN ]=== -int main(int argc, char* argv[]) { - CommandLineArgs args; - parseCommandLineArgs(&args, argc, argv); - - printf("Loading %s...\n", args.dataWinPath); - - DataWin* dataWin = DataWin_parse( - args.dataWinPath, - (DataWinParserOptions) { - .parseGen8 = true, - .parseOptn = true, - .parseLang = true, - .parseExtn = false, - .parseSond = true, - .parseAgrp = true, - .parseSprt = true, - .parseBgnd = true, - .parsePath = true, - .parseScpt = true, - .parseGlob = true, - .parseShdr = true, - .parseFont = true, - .parseTmln = true, - .parseObjt = true, - .parseRoom = true, - .parseTpag = true, - .parseCode = true, - .parseVari = true, - .parseFunc = true, - .parseStrg = true, - .parseTxtr = true, - .parseAudo = true, - .skipLoadingPreciseMasksForNonPreciseSprites = true, - .lazyLoadRooms = args.lazyRooms, - .eagerlyLoadedRooms = args.eagerRooms - } - ); - - Gen8* gen8 = &dataWin->gen8; - printf("Loaded \"%s\" (%d) successfully! [Bytecode Version %u / GameMaker version %u.%u.%u.%u]\n", gen8->name, gen8->gameID, gen8->bytecodeVersion, dataWin->detectedFormat.major, dataWin->detectedFormat.minor, dataWin->detectedFormat.release, dataWin->detectedFormat.build); - - #ifdef __GLIBC__ - { - struct mallinfo2 mi = mallinfo2(); - printf("Memory after data.win parsing: used=%zu bytes (%.1f KB)\n", mi.uordblks, mi.uordblks / 1024.0f); - } - #endif - - // Build window title - char windowTitle[256]; - snprintf(windowTitle, sizeof(windowTitle), "Butterscotch - %s", gen8->displayName); - - // Initialize VM - VMContext* vm = VM_create(dataWin); - - Profiler_setEnabled(&vm->profiler, args.profilerFramesBetween > 0); -#ifdef ENABLE_VM_OPCODE_PROFILER - vm->opcodeProfilerEnabled = args.opcodeProfiler; - if (vm->opcodeProfilerEnabled) { - vm->opcodeVariantCounts = safeCalloc(256 * 256, sizeof(uint64_t)); - vm->opcodeRValueTypeCounts = safeCalloc(256 * 256, sizeof(uint64_t)); - } -#endif - - if (args.hasSeed) { - srand((unsigned int) args.seed); - vm->hasFixedSeed = true; - printf("Using fixed RNG seed: %d\n", args.seed); - } - - if (args.printRooms) { - // Under --lazy-rooms we load each room for display and then free it again so the dump - // reflects what each room contains without keeping all of them resident simultaneously. - forEachIndexed(Room, room, idx, dataWin->room.rooms, dataWin->room.count) { - bool loadedHere = false; - if (!room->payloadLoaded) { - DataWin_loadRoomPayload(dataWin, (int32_t) idx); - loadedHere = true; - } - - printf("[%d] %s ()\n", idx, room->name); - - forEachIndexed(RoomGameObject, roomGameObject, idx2, room->gameObjects, room->gameObjectCount) { - GameObject* gameObject = &dataWin->objt.objects[roomGameObject->objectDefinition]; - printf( - " [%d] %s (x=%d,y=%d,persistent=%d,solid=%d,spriteId=%d,preCreateCode=%d,creationCode=%d)\n", - idx2, - gameObject->name, - roomGameObject->x, - roomGameObject->y, - gameObject->persistent, - gameObject->solid, - gameObject->spriteId, - roomGameObject->preCreateCode, - roomGameObject->creationCode - ); - } - - if (loadedHere && !room->eagerlyLoaded) { - DataWin_freeRoomPayload(room); - } - } - VM_free(vm); - DataWin_free(dataWin); - return 0; - } - - if (args.printDeclaredFunctions) { - repeat(hmlen(vm->codeIndexByName), i) { - printf("[%d] %s\n", vm->codeIndexByName[i].value, vm->codeIndexByName[i].key); - } - VM_free(vm); - DataWin_free(dataWin); - return 0; - } - - if (shlen(args.disassemble) > 0) { - VM_buildCrossReferences(vm); - if (shgeti(args.disassemble, "*") >= 0) { - repeat(dataWin->code.count, i) { - VM_disassemble(vm, (int32_t) i); - } - } else { - for (ptrdiff_t i = 0; shlen(args.disassemble) > i; i++) { - const char* name = args.disassemble[i].key; - ptrdiff_t idx = shgeti(vm->codeIndexByName, (char*) name); - if (idx >= 0) { - VM_disassemble(vm, vm->codeIndexByName[idx].value); - } else { - fprintf(stderr, "Error: Script '%s' not found in funcMap\n", name); - } - } - } - VM_free(vm); - DataWin_free(dataWin); - freeCommandLineArgs(&args); - return 0; - } - - // Initialize the file system - GlfwFileSystem* glfwFileSystem = GlfwFileSystem_create(args.dataWinPath); - - // Init GLFW - glfwSetErrorCallback(glfwErrorCallback); - if (!glfwInit()) { - fprintf(stderr, "Failed to initialize GLFW\n"); - DataWin_free(dataWin); - freeCommandLineArgs(&args); - return 1; - } - - bool modernGL = strcmp(args.renderer, "legacy-gl") != 0; - if (!modernGL) { - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 1); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); - } else { -#ifdef ENABLE_GLES - glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); -#else - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); - glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE); -#endif - } - - // Load SDL gamecontroller mappings - { - const char* dbPath = "gamecontrollerdb.txt"; - FILE* f = fopen(dbPath, "r"); - if (f != NULL) { - fseek(f, 0, SEEK_END); - long len = ftell(f); - fseek(f, 0, SEEK_SET); - char* buffer = (char*) malloc(len + 1); - if (buffer != NULL) { - fread(buffer, 1, len, f); - buffer[len] = '\0'; - GlfwGamepad_loadMappings(buffer); - free(buffer); - } - fclose(f); - } else { - fprintf(stderr, "Gamepad: SDL gamecontrollerdb.txt not found at %s, using defaults\n", dbPath); - } - } - - if (args.headless) { - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); - } - - GLFWwindow* window = glfwCreateWindow((int) gen8->defaultWindowWidth, (int) gen8->defaultWindowHeight, windowTitle, nullptr, nullptr); - if (window == nullptr) { - fprintf(stderr, "Failed to create GLFW window\n"); - glfwTerminate(); - DataWin_free(dataWin); - freeCommandLineArgs(&args); - return 1; - } - - glfwMakeContextCurrent(window); - glfwSwapInterval(0); // Disable v-sync, we control timing ourselves - - // Load OpenGL function pointers via GLAD -#ifdef ENABLE_GLES - if (!gladLoadGLES2Loader((GLADloadproc) glfwGetProcAddress)) { -#else - if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) { -#endif - fprintf(stderr, "Failed to initialize GLAD\n"); - glfwDestroyWindow(window); - glfwTerminate(); - DataWin_free(dataWin); - freeCommandLineArgs(&args); - return 1; - } - - // Install the OpenGL debug message callback -#ifndef ENABLE_GLES - if (modernGL) - installGLDebugCallback(); -#endif - - // Initialize the renderer - Renderer* renderer = nullptr; -#ifdef ENABLE_GLES - if (strcmp(args.renderer, "legacy-gl") == 0) { - fprintf(stderr, "--renderer legacy-gl is not available in GLES builds; falling back to gl\n"); - } - renderer = GLRenderer_create(); -#else - if(strcmp(args.renderer, "legacy-gl") == 0) - renderer = GLLegacyRenderer_create(); - else - renderer = GLRenderer_create(); -#endif - - // Initialize the audio system - AudioSystem* audioSystem = nullptr; - if (!args.headless) { - audioSystem = (AudioSystem*) MaAudioSystem_create(); - } else { - audioSystem = (AudioSystem*) NoopAudioSystem_create(); - } - - // Initialize the runner - Runner* runner = Runner_create(dataWin, vm, renderer, (FileSystem*) glfwFileSystem, audioSystem); - runner->debugMode = args.debug; - runner->osType = args.osType; - runner->nativeWindow = window; - runner->setWindowTitle = setGlfwWindowTitle; - runner->windowHasFocus = getGlfwWindowFocus; - - // Set up input recording/playback (both can be active: playback then continue recording) - if (args.playbackInputsPath != nullptr) { - globalInputRecording = InputRecording_createPlayer(args.playbackInputsPath, args.recordInputsPath); - } else if (args.recordInputsPath != nullptr) { - globalInputRecording = InputRecording_createRecorder(args.recordInputsPath); - } - if (globalInputRecording != nullptr) { - installCrashHandlers(); - } - shcopyFromTo(args.varReadsToBeTraced, runner->vmContext->varReadsToBeTraced); - shcopyFromTo(args.varWritesToBeTraced, runner->vmContext->varWritesToBeTraced); - shcopyFromTo(args.functionCallsToBeTraced, runner->vmContext->functionCallsToBeTraced); - shcopyFromTo(args.alarmsToBeTraced, runner->vmContext->alarmsToBeTraced); - shcopyFromTo(args.instanceLifecyclesToBeTraced, runner->vmContext->instanceLifecyclesToBeTraced); - shcopyFromTo(args.eventsToBeTraced, runner->vmContext->eventsToBeTraced); - shcopyFromTo(args.opcodesToBeTraced, runner->vmContext->opcodesToBeTraced); - shcopyFromTo(args.stackToBeTraced, runner->vmContext->stackToBeTraced); - shcopyFromTo(args.tilesToBeTraced, runner->vmContext->tilesToBeTraced); - runner->vmContext->traceBytecodeAfterFrame = args.traceBytecodeAfterFrame; - runner->vmContext->alwaysLogUnknownFunctions = args.alwaysLogUnknownFunctions; - runner->vmContext->alwaysLogStubbedFunctions = args.alwaysLogStubbedFunctions; - runner->vmContext->traceEventInherited = args.traceEventInherited; - - // Set up keyboard input - glfwSetWindowUserPointer(window, runner); - glfwSetKeyCallback(window, keyCallback); - glfwSetCharCallback(window, characterCallback); - -#ifndef _WIN32 - struct sigaction sa = { .sa_handler = onCrashSignal }; - sigemptyset(&sa.sa_mask); - struct sigaction prev; - sigaction(SIGABRT, &sa, &prev); - hmput(previousSignalActions, SIGABRT, prev); - sigaction(SIGSEGV, &sa, &prev); - hmput(previousSignalActions, SIGSEGV, prev); -#endif - - // Initialize the first room and fire Game Start / Room Start events - Runner_initFirstRoom(runner); - - // Main loop - bool debugPaused = false; - bool debugShowCollisionMasks = false; - double lastFrameTime = glfwGetTime(); - while (!glfwWindowShouldClose(window) && !runner->shouldExit) { - // Clear last frame's pressed/released state, then poll new input events - RunnerKeyboard_beginFrame(runner->keyboard); - RunnerGamepad_beginFrame(runner->gamepads); - glfwPollEvents(); - GlfwGamepad_poll(runner->gamepads); - - // Process input recording/playback (must happen after glfwPollEvents, before Runner_step) - InputRecording_processFrame(globalInputRecording, runner->keyboard, runner->frameCount); - - // Debug key bindings - if (runner->debugMode) { - // Pause - if (RunnerKeyboard_checkPressed(runner->keyboard, 'P')) { - debugPaused = !debugPaused; - fprintf(stderr, "Debug: %s\n", debugPaused ? "Paused" : "Resumed"); - } - - // Go to next room - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_PAGEUP)) { - DataWin* dw = runner->dataWin; - if ((int32_t) dw->gen8.roomOrderCount > runner->currentRoomOrderPosition + 1) { - int32_t nextIdx = dw->gen8.roomOrder[runner->currentRoomOrderPosition + 1]; - runner->pendingRoom = nextIdx; - runner->audioSystem->vtable->stopAll(runner->audioSystem); - fprintf(stderr, "Debug: Going to next room -> %s\n", dw->room.rooms[nextIdx].name); - } - } - - // Go to previous room - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_PAGEDOWN)) { - DataWin* dw = runner->dataWin; - if (runner->currentRoomOrderPosition > 0) { - int32_t prevIdx = dw->gen8.roomOrder[runner->currentRoomOrderPosition - 1]; - runner->pendingRoom = prevIdx; - runner->audioSystem->vtable->stopAll(runner->audioSystem); - fprintf(stderr, "Debug: Going to previous room -> %s\n", dw->room.rooms[prevIdx].name); - } - } - - // Dump runner state to console - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F12)) { - fprintf(stderr, "Debug: Dumping runner state at frame %d\n", runner->frameCount); - Runner_dumpState(runner); - } - - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F11)) { - fprintf(stderr, "Debug: Dumping runner state at frame %d\n", runner->frameCount); - char* json = Runner_dumpStateJson(runner); - - if (args.dumpJsonFilePattern != nullptr) { - char filename[512]; - snprintf(filename, sizeof(filename), args.dumpJsonFilePattern, runner->frameCount); - FILE* f = fopen(filename, "w"); - if (f != nullptr) { - fwrite(json, 1, strlen(json), f); - fputc('\n', f); - fclose(f); - printf("JSON dump saved: %s\n", filename); - } else { - fprintf(stderr, "Error: Could not write JSON dump to '%s'\n", filename); - } - } else { - printf("%s\n", json); - } - - free(json); - } - - // Toggle the collision mask debug overlay - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F2)) { - debugShowCollisionMasks = !debugShowCollisionMasks; - fprintf(stderr, "Debug: Collision mask overlay %s!\n", debugShowCollisionMasks ? "enabled" : "disabled"); - } - - // Reset global interact state because I HATE when I get stuck while moving through rooms - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F10)) { - int32_t interactVarId = shget(runner->vmContext->globalVarNameMap, "interact"); - - runner->vmContext->globalVars[interactVarId] = RValue_makeInt32(0); - printf("Changed global.interact [%d] value!\n", interactVarId); - } - } - - // Run the game step if the game is paused - bool shouldStep = true; - if (runner->debugMode && debugPaused) { - shouldStep = RunnerKeyboard_checkPressed(runner->keyboard, 'O'); - if (shouldStep) fprintf(stderr, "Debug: Frame advance (frame %d)\n", runner->frameCount); - } - - double frameStartTime = 0; - - if (shouldStep) { - if (args.traceFrames) { - frameStartTime = glfwGetTime(); - fprintf(stderr, "Frame %d (Start)\n", runner->frameCount); - } - - // Run one game step (Begin Step, Keyboard, Alarms, Step, End Step, room transitions) - Runner_step(runner); - - if (args.profilerFramesBetween > 0 && runner->frameCount > 0 && runner->frameCount % args.profilerFramesBetween == 0) { - char* profilerReport = Profiler_createReport(vm->profiler, 20, args.profilerFramesBetween); - if (profilerReport != nullptr) { - fprintf(stderr, "%s\n", profilerReport); - free(profilerReport); - } - Profiler_reset(vm->profiler); - } - - // Update audio system (gain fading, cleanup ended sounds) - float dt = (float) (glfwGetTime() - lastFrameTime); - if (0.0f > dt) dt = 0.0f; - if (dt > 0.1f) dt = 0.1f; // cap delta to avoid huge fades on lag spikes - runner->audioSystem->vtable->update(runner->audioSystem, dt); - - // Dump full runner state if this frame was requested - if (hmget(args.dumpFrames, runner->frameCount)) { - Runner_dumpState(runner); - } - - // Dump runner state as JSON if this frame was requested - if (hmget(args.dumpJsonFrames, runner->frameCount)) { - char* json = Runner_dumpStateJson(runner); - if (args.dumpJsonFilePattern != nullptr) { - char filename[512]; - snprintf(filename, sizeof(filename), args.dumpJsonFilePattern, runner->frameCount); - FILE* f = fopen(filename, "w"); - if (f != nullptr) { - fwrite(json, 1, strlen(json), f); - fputc('\n', f); - fclose(f); - printf("JSON dump saved: %s\n", filename); - } else { - fprintf(stderr, "Error: Could not write JSON dump to '%s'\n", filename); - } - } else { - printf("%s\n", json); - } - free(json); - } - } - - // Query actual framebuffer size (differs from window size on Wayland with fractional scaling) - int fbWidth, fbHeight; - glfwGetFramebufferSize(window, &fbWidth, &fbHeight); - - // Clear the default framebuffer (window background) to black - if (!(strcmp(args.renderer, "legacy-gl") == 0)) { - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - glClear(GL_COLOR_BUFFER_BIT); - - int32_t gameW = (int32_t) gen8->defaultWindowWidth; - int32_t gameH = (int32_t) gen8->defaultWindowHeight; - - // The application surface (FBO) is sized to defaultWindowWidth x defaultWindowHeight. - // It is a bit hard to understand, but here's how it works: - // The Port X/Port Y controls the position of the game viewport within the application surface. - // The Port W/Port H controls the size of the game viewport within the application surface. - // Think of it like if you had an image (or... well, a framebuffer) and you are "pasting" it over the application surface. - // And the Port W/Port H are scaled by the window size too (set by the GEN8 chunk) - float displayScaleX; - float displayScaleY; - - Runner_computeViewDisplayScale(runner, gameW, gameH, &displayScaleX, &displayScaleY); - - renderer->vtable->beginFrame(renderer, gameW, gameH, fbWidth, fbHeight); - - // Clear FBO with room background color - if (runner->drawBackgroundColor) { - int rInt = BGR_R(runner->backgroundColor); - int gInt = BGR_G(runner->backgroundColor); - int bInt = BGR_B(runner->backgroundColor); - glClearColor(rInt / 255.0f, gInt / 255.0f, bInt / 255.0f, 1.0f); - } else { - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - } - glClear(GL_COLOR_BUFFER_BIT); - - Runner_drawViews(runner, gameW, gameH, displayScaleX, displayScaleY, debugShowCollisionMasks); - - renderer->vtable->endFrame(renderer); - - // Capture screenshot if this frame matches a requested frame - bool shouldScreenshot = hmget(args.screenshotFrames, runner->frameCount); - - if (shouldScreenshot) { - // Bind FBO so glReadPixels reads from the game's native-resolution texture - GLRenderer* gl = (GLRenderer*) renderer; - if (!(strcmp(args.renderer, "legacy-gl") == 0)) - glBindFramebuffer(GL_READ_FRAMEBUFFER, gl->fbo); - captureScreenshot(args.screenshotPattern, runner->frameCount, gameW, gameH); - if (!(strcmp(args.renderer, "legacy-gl") == 0)) - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - } - - if (args.exitAtFrame >= 0 && runner->frameCount >= args.exitAtFrame) { - printf("Exiting at frame %d (--exit-at-frame)\n", runner->frameCount); - glfwSetWindowShouldClose(window, GLFW_TRUE); - } - - if (shouldStep && args.traceFrames) { - double frameElapsedMs = (glfwGetTime() - frameStartTime) * 1000.0; - fprintf(stderr, "Frame %d (End, %.2f ms)\n", runner->frameCount, frameElapsedMs); - } - - glfwSwapBuffers(window); - - // Limit frame rate to room speed (skip in headless mode for max speed!!) - if (!args.headless && runner->currentRoom->speed > 0) { - static bool fastForwardActive = false; - static bool fastForwardTabPrev = false; - bool fastForwardTabNow = glfwGetKey(window, GLFW_KEY_TAB) == GLFW_PRESS; - if (args.fastForwardSpeed > 0.0 && fastForwardTabNow && !fastForwardTabPrev) { - fastForwardActive = !fastForwardActive; - lastFrameTime = glfwGetTime(); - } - fastForwardTabPrev = fastForwardTabNow; - double effectiveSpeed = (args.fastForwardSpeed > 0.0 && fastForwardActive) ? args.fastForwardSpeed : args.speedMultiplier; - double targetFrameTime = 1.0 / (runner->currentRoom->speed * effectiveSpeed); - double nextFrameTime = lastFrameTime + targetFrameTime; - // Sleep for most of the remaining time, then spin-wait for precision - double remaining = nextFrameTime - glfwGetTime(); - if (remaining > 0.002) { - #ifdef _WIN32 - Sleep((DWORD) ((remaining - 0.001) * 1000)); - #else - struct timespec ts = { - .tv_sec = 0, - .tv_nsec = (long) ((remaining - 0.001) * 1e9) - }; - nanosleep(&ts, nullptr); - #endif - } - while (glfwGetTime() < nextFrameTime) { - // Spin-wait for the remaining sub-millisecond - } - lastFrameTime = nextFrameTime; - } else { - lastFrameTime = glfwGetTime(); - } - } - - saveInputRecording(); - - // Cleanup - runner->audioSystem->vtable->destroy(runner->audioSystem); - runner->audioSystem = nullptr; - renderer->vtable->destroy(renderer); - - glfwDestroyWindow(window); - glfwTerminate(); - - Runner_free(runner); - GlfwFileSystem_destroy(glfwFileSystem); -#ifdef ENABLE_VM_OPCODE_PROFILER - VM_printOpcodeProfilerReport(vm); -#endif - VM_free(vm); - DataWin_free(dataWin); - - freeCommandLineArgs(&args); - - printf("Bye! :3\n"); - return 0; -} diff --git a/src/glfw/stb_impl.c b/src/glfw/stb_impl.c deleted file mode 100644 index 9edfcf40..00000000 --- a/src/glfw/stb_impl.c +++ /dev/null @@ -1,8 +0,0 @@ -#define STB_IMAGE_IMPLEMENTATION -#include "stb_image.h" - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include "stb_image_write.h" - -#define STB_DS_IMPLEMENTATION -#include "stb_ds.h" diff --git a/src/ini.c b/src/ini.c index b8137079..180ee524 100644 --- a/src/ini.c +++ b/src/ini.c @@ -1,326 +1,326 @@ -#include "ini.h" -#include "utils.h" - -#include -#include -#include - -#include "text_utils.h" - -// ===[ Internal Helpers ]=== - -static IniSection* findSection(const IniFile* ini, const char* name) { - repeat(ini->count, i) { - if (strcmp(ini->sections[i].name, name) == 0) { - return &ini->sections[i]; - } - } - return nullptr; -} - -static int findKeyIndex(const IniSection* section, const char* key) { - repeat(section->count, i) { - if (strcmp(section->keys[i], key) == 0) { - return i; - } - } - return -1; -} - -static IniSection* addSection(IniFile* ini, const char* name) { - if (ini->count >= ini->capacity) { - ini->capacity = (ini->capacity == 0) ? 4 : ini->capacity * 2; - ini->sections = safeRealloc(ini->sections, (size_t) ini->capacity * sizeof(IniSection)); - } - IniSection* section = &ini->sections[ini->count++]; - section->name = safeStrdup(name); - section->keys = nullptr; - section->values = nullptr; - section->count = 0; - section->capacity = 0; - return section; -} - -static void addKeyValue(IniSection* section, const char* key, const char* value) { - if (section->count >= section->capacity) { - section->capacity = (section->capacity == 0) ? 4 : section->capacity * 2; - section->keys = safeRealloc(section->keys, (size_t) section->capacity * sizeof(char*)); - section->values = safeRealloc(section->values, (size_t) section->capacity * sizeof(char*)); - } - section->keys[section->count] = safeStrdup(key); - section->values[section->count] = safeStrdup(value); - section->count++; -} - -static const char* skipWhitespace(const char* p) { - while (TextUtils_isWhitespaceChar(*p)) { - p++; - } - return p; -} - -static char* normalizeValue(char* value) { - TextUtils_trimTrailingWhitespace(value); - size_t length = strlen(value); - if (length >= 2 && value[0] == '"' && value[length - 1] == '"') { - value[length - 1] = '\0'; - return safeStrdup(value + 1); - } - return safeStrdup(value); -} - -// ===[ Lifecycle ]=== - -IniFile* Ini_parse(const char* text) { - IniFile* ini = safeCalloc(1, sizeof(IniFile)); - - if (text == nullptr || *text == '\0') { - return ini; - } - - // Make a mutable copy to tokenize - char* data = safeStrdup(text); - IniSection* currentSection = nullptr; - - char* line = data; - while (line != nullptr) { - // Find end of line - char* eol = strchr(line, '\n'); - if (eol != nullptr) { - *eol = '\0'; - } - - // Trim leading whitespace - const char* trimmed = skipWhitespace(line); - - // Skip empty lines and comments - if (*trimmed == '\0' || *trimmed == ';' || *trimmed == '#') { - line = eol != nullptr ? eol + 1 : nullptr; - continue; - } - - // Strip trailing whitespace/CR - char* mutableTrimmed = (char*) trimmed; - TextUtils_trimTrailingWhitespace(mutableTrimmed); - - if (*trimmed == '[') { - // Section header - const char* nameStart = trimmed + 1; - char* closeBracket = strchr(mutableTrimmed, ']'); - if (closeBracket != nullptr) { - *closeBracket = '\0'; - currentSection = findSection(ini, nameStart); - if (currentSection == nullptr) { - currentSection = addSection(ini, nameStart); - } - } else { - fprintf(stderr, "Ini: malformed section header: %s\n", trimmed); - } - } else { - // Key=value pair - char* equals = strchr(mutableTrimmed, '='); - if (equals != nullptr && currentSection != nullptr) { - *equals = '\0'; - char* key = mutableTrimmed; - char* value = equals + 1; - TextUtils_trimTrailingWhitespace(key); - value = (char*) skipWhitespace(value); - char* normalizedValue = normalizeValue(value); - - // Check if key already exists - overwrite if so - int existingIndex = findKeyIndex(currentSection, key); - if (existingIndex >= 0) { - free(currentSection->values[existingIndex]); - currentSection->values[existingIndex] = normalizedValue; - } else { - addKeyValue(currentSection, key, normalizedValue); - free(normalizedValue); - } - } - // Silently skip key=value lines outside any section (matching GML behavior) - } - - line = eol != nullptr ? eol + 1 : nullptr; - } - - free(data); - return ini; -} - -void Ini_free(IniFile* ini) { - if (ini == nullptr) - return; - - repeat(ini->count, i) { - IniSection* section = &ini->sections[i]; - repeat(section->count, j) { - free(section->keys[j]); - free(section->values[j]); - } - free(section->keys); - free(section->values); - free(section->name); - } - free(ini->sections); - free(ini); -} - -// ===[ Queries ]=== - -const char* Ini_getString(const IniFile* ini, const char* section, const char* key) { - IniSection* sec = findSection(ini, section); - if (sec == nullptr) - return nullptr; - - int idx = findKeyIndex(sec, key); - if (0 > idx) - return nullptr; - - return sec->values[idx]; -} - -bool Ini_hasSection(const IniFile* ini, const char* section) { - return findSection(ini, section) != nullptr; -} - -bool Ini_hasKey(const IniFile* ini, const char* section, const char* key) { - IniSection* sec = findSection(ini, section); - - if (sec == nullptr) - return false; - - return findKeyIndex(sec, key) >= 0; -} - -// ===[ Mutation ]=== - -void Ini_setString(IniFile* ini, const char* section, const char* key, const char* value) { - // If we are passing a null value, let's remove it! - if (value == nullptr) { - Ini_deleteKey(ini, section, key); - return; - } - - IniSection* sec = findSection(ini, section); - if (sec == nullptr) { - sec = addSection(ini, section); - } - - int idx = findKeyIndex(sec, key); - if (idx >= 0) { - free(sec->values[idx]); - sec->values[idx] = safeStrdup(value); - } else { - addKeyValue(sec, key, value); - } -} - -void Ini_deleteKey(IniFile* ini, const char* section, const char* key) { - IniSection* sec = findSection(ini, section); - if (sec == nullptr) - return; - - int idx = findKeyIndex(sec, key); - if (0 > idx) - return; - - free(sec->keys[idx]); - free(sec->values[idx]); - - // Shift remaining entries down - for (int i = idx; sec->count - 1 > i; i++) { - sec->keys[i] = sec->keys[i + 1]; - sec->values[i] = sec->values[i + 1]; - } - sec->count--; -} - -void Ini_deleteSection(IniFile* ini, const char* section) { - int sectionIndex = -1; - repeat(ini->count, i) { - if (strcmp(ini->sections[i].name, section) == 0) { - sectionIndex = (int) i; - break; - } - } - if (0 > sectionIndex) return; - - // Free the section's contents - IniSection* sec = &ini->sections[sectionIndex]; - repeat(sec->count, j) { - free(sec->keys[j]); - free(sec->values[j]); - } - free(sec->keys); - free(sec->values); - free(sec->name); - - // Shift remaining sections down - for (int i = sectionIndex; ini->count - 1 > i; i++) { - ini->sections[i] = ini->sections[i + 1]; - } - ini->count--; -} - -// ===[ Serialization ]=== - -char* Ini_serialize(const IniFile* ini, size_t initialCapacity) { - size_t capacity = initialCapacity; - size_t length = 0; - char* buffer = safeMalloc(capacity); - buffer[0] = '\0'; - - repeat(ini->count, i) { - IniSection* section = &ini->sections[i]; - - // Blank line before section (unless at start of output) - // Format: \n[name]\nkey=value\n... - const char* name = section->name; - size_t nameLen = strlen(name); - - // Calculate space needed for section header - // optional \n + [ + name + ] + \n - size_t needed = (length > 0 ? 1 : 0) + 1 + nameLen + 1 + 1; - - // Calculate space needed for all key=value pairs - repeat(section->count, j) { - needed += strlen(section->keys[j]) + 2 + strlen(section->values[j]) + 2; - } - - // Grow buffer if needed - while (length + needed + 1 > capacity) { - capacity *= 2; - } - buffer = safeRealloc(buffer, capacity); - - // Write section header - if (length > 0) { - buffer[length++] = '\n'; - } - buffer[length++] = '['; - memcpy(buffer + length, name, nameLen); - length += nameLen; - buffer[length++] = ']'; - buffer[length++] = '\n'; - - // Write key=value pairs - repeat(section->count, j) { - const char* key = section->keys[j]; - const char* value = section->values[j]; - size_t keyLen = strlen(key); - size_t valueLen = strlen(value); - - memcpy(buffer + length, key, keyLen); - length += keyLen; - buffer[length++] = '='; - buffer[length++] = '"'; - memcpy(buffer + length, value, valueLen); - length += valueLen; - buffer[length++] = '"'; - buffer[length++] = '\n'; - } - } - - buffer[length] = '\0'; - return buffer; -} +#include "ini.h" +#include "utils.h" + +#include +#include +#include + +#include "text_utils.h" + +// ===[ Internal Helpers ]=== + +static IniSection* findSection(const IniFile* ini, const char* name) { + repeat(ini->count, i) { + if (strcmp(ini->sections[i].name, name) == 0) { + return &ini->sections[i]; + } + } + return nullptr; +} + +static int findKeyIndex(const IniSection* section, const char* key) { + repeat(section->count, i) { + if (strcmp(section->keys[i], key) == 0) { + return i; + } + } + return -1; +} + +static IniSection* addSection(IniFile* ini, const char* name) { + if (ini->count >= ini->capacity) { + ini->capacity = (ini->capacity == 0) ? 4 : ini->capacity * 2; + ini->sections = safeRealloc(ini->sections, (size_t) ini->capacity * sizeof(IniSection)); + } + IniSection* section = &ini->sections[ini->count++]; + section->name = safeStrdup(name); + section->keys = nullptr; + section->values = nullptr; + section->count = 0; + section->capacity = 0; + return section; +} + +static void addKeyValue(IniSection* section, const char* key, const char* value) { + if (section->count >= section->capacity) { + section->capacity = (section->capacity == 0) ? 4 : section->capacity * 2; + section->keys = safeRealloc(section->keys, (size_t) section->capacity * sizeof(char*)); + section->values = safeRealloc(section->values, (size_t) section->capacity * sizeof(char*)); + } + section->keys[section->count] = safeStrdup(key); + section->values[section->count] = safeStrdup(value); + section->count++; +} + +static const char* skipWhitespace(const char* p) { + while (TextUtils_isWhitespaceChar(*p)) { + p++; + } + return p; +} + +static char* normalizeValue(char* value) { + TextUtils_trimTrailingWhitespace(value); + size_t length = strlen(value); + if (length >= 2 && value[0] == '"' && value[length - 1] == '"') { + value[length - 1] = '\0'; + return safeStrdup(value + 1); + } + return safeStrdup(value); +} + +// ===[ Lifecycle ]=== + +IniFile* Ini_parse(const char* text) { + IniFile* ini = safeCalloc(1, sizeof(IniFile)); + + if (text == nullptr || *text == '\0') { + return ini; + } + + // Make a mutable copy to tokenize + char* data = safeStrdup(text); + IniSection* currentSection = nullptr; + + char* line = data; + while (line != nullptr) { + // Find end of line + char* eol = strchr(line, '\n'); + if (eol != nullptr) { + *eol = '\0'; + } + + // Trim leading whitespace + const char* trimmed = skipWhitespace(line); + + // Skip empty lines and comments + if (*trimmed == '\0' || *trimmed == ';' || *trimmed == '#') { + line = eol != nullptr ? eol + 1 : nullptr; + continue; + } + + // Strip trailing whitespace/CR + char* mutableTrimmed = (char*) trimmed; + TextUtils_trimTrailingWhitespace(mutableTrimmed); + + if (*trimmed == '[') { + // Section header + const char* nameStart = trimmed + 1; + char* closeBracket = strchr(mutableTrimmed, ']'); + if (closeBracket != nullptr) { + *closeBracket = '\0'; + currentSection = findSection(ini, nameStart); + if (currentSection == nullptr) { + currentSection = addSection(ini, nameStart); + } + } else { + fprintf(stderr, "Ini: malformed section header: %s\n", trimmed); + } + } else { + // Key=value pair + char* equals = strchr(mutableTrimmed, '='); + if (equals != nullptr && currentSection != nullptr) { + *equals = '\0'; + char* key = mutableTrimmed; + char* value = equals + 1; + TextUtils_trimTrailingWhitespace(key); + value = (char*) skipWhitespace(value); + char* normalizedValue = normalizeValue(value); + + // Check if key already exists - overwrite if so + int existingIndex = findKeyIndex(currentSection, key); + if (existingIndex >= 0) { + free(currentSection->values[existingIndex]); + currentSection->values[existingIndex] = normalizedValue; + } else { + addKeyValue(currentSection, key, normalizedValue); + free(normalizedValue); + } + } + // Silently skip key=value lines outside any section (matching GML behavior) + } + + line = eol != nullptr ? eol + 1 : nullptr; + } + + free(data); + return ini; +} + +void Ini_free(IniFile* ini) { + if (ini == nullptr) + return; + + repeat(ini->count, i) { + IniSection* section = &ini->sections[i]; + repeat(section->count, j) { + free(section->keys[j]); + free(section->values[j]); + } + free(section->keys); + free(section->values); + free(section->name); + } + free(ini->sections); + free(ini); +} + +// ===[ Queries ]=== + +const char* Ini_getString(const IniFile* ini, const char* section, const char* key) { + IniSection* sec = findSection(ini, section); + if (sec == nullptr) + return nullptr; + + int idx = findKeyIndex(sec, key); + if (0 > idx) + return nullptr; + + return sec->values[idx]; +} + +bool Ini_hasSection(const IniFile* ini, const char* section) { + return findSection(ini, section) != nullptr; +} + +bool Ini_hasKey(const IniFile* ini, const char* section, const char* key) { + IniSection* sec = findSection(ini, section); + + if (sec == nullptr) + return false; + + return findKeyIndex(sec, key) >= 0; +} + +// ===[ Mutation ]=== + +void Ini_setString(IniFile* ini, const char* section, const char* key, const char* value) { + // If we are passing a null value, let's remove it! + if (value == nullptr) { + Ini_deleteKey(ini, section, key); + return; + } + + IniSection* sec = findSection(ini, section); + if (sec == nullptr) { + sec = addSection(ini, section); + } + + int idx = findKeyIndex(sec, key); + if (idx >= 0) { + free(sec->values[idx]); + sec->values[idx] = safeStrdup(value); + } else { + addKeyValue(sec, key, value); + } +} + +void Ini_deleteKey(IniFile* ini, const char* section, const char* key) { + IniSection* sec = findSection(ini, section); + if (sec == nullptr) + return; + + int idx = findKeyIndex(sec, key); + if (0 > idx) + return; + + free(sec->keys[idx]); + free(sec->values[idx]); + + // Shift remaining entries down + for (int i = idx; sec->count - 1 > i; i++) { + sec->keys[i] = sec->keys[i + 1]; + sec->values[i] = sec->values[i + 1]; + } + sec->count--; +} + +void Ini_deleteSection(IniFile* ini, const char* section) { + int sectionIndex = -1; + repeat(ini->count, i) { + if (strcmp(ini->sections[i].name, section) == 0) { + sectionIndex = (int) i; + break; + } + } + if (0 > sectionIndex) return; + + // Free the section's contents + IniSection* sec = &ini->sections[sectionIndex]; + repeat(sec->count, j) { + free(sec->keys[j]); + free(sec->values[j]); + } + free(sec->keys); + free(sec->values); + free(sec->name); + + // Shift remaining sections down + for (int i = sectionIndex; ini->count - 1 > i; i++) { + ini->sections[i] = ini->sections[i + 1]; + } + ini->count--; +} + +// ===[ Serialization ]=== + +char* Ini_serialize(const IniFile* ini, size_t initialCapacity) { + size_t capacity = initialCapacity; + size_t length = 0; + char* buffer = safeMalloc(capacity); + buffer[0] = '\0'; + + repeat(ini->count, i) { + IniSection* section = &ini->sections[i]; + + // Blank line before section (unless at start of output) + // Format: \n[name]\nkey=value\n... + const char* name = section->name; + size_t nameLen = strlen(name); + + // Calculate space needed for section header + // optional \n + [ + name + ] + \n + size_t needed = (length > 0 ? 1 : 0) + 1 + nameLen + 1 + 1; + + // Calculate space needed for all key=value pairs + repeat(section->count, j) { + needed += strlen(section->keys[j]) + 2 + strlen(section->values[j]) + 2; + } + + // Grow buffer if needed + while (length + needed + 1 > capacity) { + capacity *= 2; + } + buffer = safeRealloc(buffer, capacity); + + // Write section header + if (length > 0) { + buffer[length++] = '\n'; + } + buffer[length++] = '['; + memcpy(buffer + length, name, nameLen); + length += nameLen; + buffer[length++] = ']'; + buffer[length++] = '\n'; + + // Write key=value pairs + repeat(section->count, j) { + const char* key = section->keys[j]; + const char* value = section->values[j]; + size_t keyLen = strlen(key); + size_t valueLen = strlen(value); + + memcpy(buffer + length, key, keyLen); + length += keyLen; + buffer[length++] = '='; + buffer[length++] = '"'; + memcpy(buffer + length, value, valueLen); + length += valueLen; + buffer[length++] = '"'; + buffer[length++] = '\n'; + } + } + + buffer[length] = '\0'; + return buffer; +} diff --git a/src/ini.h b/src/ini.h index 8a7dddb8..9a7188ac 100644 --- a/src/ini.h +++ b/src/ini.h @@ -1,43 +1,43 @@ -#pragma once - -#include "common.h" -#include - -#define INI_SERIALIZE_DEFAULT_INITIAL_CAPACITY 256 - -// ===[ IniFile Types ]=== - -typedef struct { - char* name; - char** keys; - char** values; - int count; - int capacity; -} IniSection; - -typedef struct { - IniSection* sections; - int count; - int capacity; -} IniFile; - -// ===[ Lifecycle ]=== - -IniFile* Ini_parse(const char* text); -void Ini_free(IniFile* ini); - -// ===[ Queries ]=== - -const char* Ini_getString(const IniFile* ini, const char* section, const char* key); -bool Ini_hasSection(const IniFile* ini, const char* section); -bool Ini_hasKey(const IniFile* ini, const char* section, const char* key); - -// ===[ Mutation ]=== - -void Ini_setString(IniFile* ini, const char* section, const char* key, const char* value); -void Ini_deleteKey(IniFile* ini, const char* section, const char* key); -void Ini_deleteSection(IniFile* ini, const char* section); - -// ===[ Serialization ]=== - -char* Ini_serialize(const IniFile* ini, size_t initialCapacity); +#pragma once + +#include "common.h" +#include + +#define INI_SERIALIZE_DEFAULT_INITIAL_CAPACITY 256 + +// ===[ IniFile Types ]=== + +typedef struct { + char* name; + char** keys; + char** values; + int count; + int capacity; +} IniSection; + +typedef struct { + IniSection* sections; + int count; + int capacity; +} IniFile; + +// ===[ Lifecycle ]=== + +IniFile* Ini_parse(const char* text); +void Ini_free(IniFile* ini); + +// ===[ Queries ]=== + +const char* Ini_getString(const IniFile* ini, const char* section, const char* key); +bool Ini_hasSection(const IniFile* ini, const char* section); +bool Ini_hasKey(const IniFile* ini, const char* section, const char* key); + +// ===[ Mutation ]=== + +void Ini_setString(IniFile* ini, const char* section, const char* key, const char* value); +void Ini_deleteKey(IniFile* ini, const char* section, const char* key); +void Ini_deleteSection(IniFile* ini, const char* section); + +// ===[ Serialization ]=== + +char* Ini_serialize(const IniFile* ini, size_t initialCapacity); diff --git a/src/input_recording.c b/src/input_recording.c index 8e69432d..c8566f2e 100644 --- a/src/input_recording.c +++ b/src/input_recording.c @@ -1,213 +1,213 @@ -#include "input_recording.h" -#include "json_reader.h" -#include "json_writer.h" - -#include "utils.h" - -#include -#include -#include - -#include "stb_ds.h" - -InputRecording* InputRecording_createRecorder(const char* filePath) { - InputRecording* rec = safeCalloc(1, sizeof(InputRecording)); - rec->isRecording = true; - rec->recordFilePath = filePath; - return rec; -} - -InputRecording* InputRecording_createPlayer(const char* playbackFilePath, const char* recordFilePath) { - // Read the file contents - FILE* f = fopen(playbackFilePath, "r"); - if (f == nullptr) { - fprintf(stderr, "Error: Could not open input recording file '%s'\n", playbackFilePath); - exit(1); - } - - fseek(f, 0, SEEK_END); - long fileSize = ftell(f); - fseek(f, 0, SEEK_SET); - - char* contents = safeMalloc(fileSize + 1); - fread(contents, 1, fileSize, f); - contents[fileSize] = '\0'; - fclose(f); - - // Parse JSON - JsonValue* root = JsonReader_parse(contents); - free(contents); - - if (root == nullptr || !JsonReader_isObject(root)) { - fprintf(stderr, "Error: Invalid JSON in input recording file '%s'\n", playbackFilePath); - exit(1); - } - - // Find the highest frame number to determine array size - int objectLen = JsonReader_objectLength(root); - int32_t maxFrame = -1; - repeat(objectLen, i) { - const char* key = JsonReader_getObjectKey(root, i); - int32_t frameNum = (int32_t) strtol(key, nullptr, 10); - if (frameNum > maxFrame) maxFrame = frameNum; - } - - InputRecording* rec = safeCalloc(1, sizeof(InputRecording)); - rec->isPlayback = true; - rec->playbackFrameCount = maxFrame + 1; - - // If a record path was provided, also enable recording - if (recordFilePath != nullptr) { - rec->isRecording = true; - rec->recordFilePath = recordFilePath; - } - - // Allocate playbackFrames array (one stb_ds int32_t array per frame) - rec->playbackFrames = safeCalloc(rec->playbackFrameCount, sizeof(int32_t*)); - - repeat(objectLen, i) { - const char* key = JsonReader_getObjectKey(root, i); - JsonValue* val = JsonReader_getObjectValue(root, i); - int32_t frameNum = (int32_t) strtol(key, nullptr, 10); - - if (JsonReader_isArray(val)) { - int keyCount = JsonReader_arrayLength(val); - int32_t* keys = nullptr; - repeat(keyCount, k) { - JsonValue* keyVal = JsonReader_getArrayElement(val, k); - arrput(keys, (int32_t) JsonReader_getInt(keyVal)); - } - rec->playbackFrames[frameNum] = keys; - } - } - - JsonReader_free(root); - fprintf(stderr, "InputRecording: Loaded %d frames from '%s'\n", rec->playbackFrameCount, playbackFilePath); - return rec; -} - -void InputRecording_free(InputRecording* recording) { - if (recording == nullptr) return; - - if (recording->recordedFrames != nullptr) { - int32_t count = (int32_t) arrlen(recording->recordedFrames); - repeat(count, i) { - arrfree(recording->recordedFrames[i]); - } - arrfree(recording->recordedFrames); - } - - if (recording->playbackFrames != nullptr) { - repeat(recording->playbackFrameCount, i) { - arrfree(recording->playbackFrames[i]); - } - free(recording->playbackFrames); - } - - free(recording); -} - -void InputRecording_processFrame(InputRecording* recording, RunnerKeyboardState* kb, int frameNumber) { - if (recording == nullptr) return; - - // Playback: overwrite keyboard state from recorded data (while frames remain) - if (recording->isPlayback) { - if (recording->playbackFrameCount > frameNumber) { - int32_t* frameKeys = recording->playbackFrames[frameNumber]; - int32_t keyCount = (int32_t) arrlen(frameKeys); - - // Build a temporary "current held" array for this frame - bool currentKeyDown[GML_KEY_COUNT]; - memset(currentKeyDown, 0, sizeof(currentKeyDown)); - repeat(keyCount, i) { - int32_t key = frameKeys[i]; - if (GML_KEY_COUNT > key && key >= 0) { - currentKeyDown[key] = true; - } - } - - // Derive transitions by comparing against previousKeyDown - repeat(GML_KEY_COUNT, key) { - kb->keyDown[key] = currentKeyDown[key]; - kb->keyPressed[key] = currentKeyDown[key] && !recording->previousKeyDown[key]; - kb->keyReleased[key] = !currentKeyDown[key] && recording->previousKeyDown[key]; - if (kb->keyPressed[key]) { - kb->lastKey = (int32_t) key; - } - } - - memcpy(recording->previousKeyDown, currentKeyDown, sizeof(currentKeyDown)); - } else { - // Past the end of recorded data: release everything, then let real input through - if (!recording->playbackEnded) { - fprintf(stderr, "InputRecording: Playback ended at frame %d (recorded %d frames)\n", frameNumber, recording->playbackFrameCount); - recording->playbackEnded = true; - - repeat(GML_KEY_COUNT, key) { - kb->keyReleased[key] = recording->previousKeyDown[key]; - kb->keyDown[key] = false; - kb->keyPressed[key] = false; - } - memset(recording->previousKeyDown, 0, sizeof(recording->previousKeyDown)); - } - // After the first "ended" frame, real keyboard input flows through naturally - } - } - - // Recording: snapshot whatever the current keyboard state is (from real input or playback) - if (recording->isRecording) { - int32_t* heldKeys = nullptr; - repeat(GML_KEY_COUNT, key) { - if (kb->keyDown[key]) { - arrput(heldKeys, (int32_t) key); - } - } - arrput(recording->recordedFrames, heldKeys); - } -} - -bool InputRecording_save(InputRecording* recording) { - if (recording == nullptr || !recording->isRecording) return false; - - int32_t frameCount = (int32_t) arrlen(recording->recordedFrames); - - JsonWriter w = JsonWriter_create(); - JsonWriter_beginObject(&w); - - repeat(frameCount, f) { - // Frame number as string key - char frameKey[16]; - snprintf(frameKey, sizeof(frameKey), "%d", (int) f); - JsonWriter_key(&w, frameKey); - - JsonWriter_beginArray(&w); - int32_t* keys = recording->recordedFrames[f]; - int32_t keyCount = (int32_t) arrlen(keys); - repeat(keyCount, k) { - JsonWriter_int(&w, keys[k]); - } - JsonWriter_endArray(&w); - } - - JsonWriter_endObject(&w); - - FILE* f = fopen(recording->recordFilePath, "w"); - if (f == nullptr) { - fprintf(stderr, "Error: Could not write input recording to '%s'\n", recording->recordFilePath); - JsonWriter_free(&w); - return false; - } - - const char* output = JsonWriter_getOutput(&w); - fwrite(output, 1, JsonWriter_getLength(&w), f); - fputc('\n', f); - fclose(f); - - fprintf(stderr, "InputRecording: Saved %d frames to '%s'\n", frameCount, recording->recordFilePath); - JsonWriter_free(&w); - return true; -} - -bool InputRecording_isPlaybackActive(InputRecording* recording) { - return recording != nullptr && recording->isPlayback && !recording->playbackEnded; -} +#include "input_recording.h" +#include "json_reader.h" +#include "json_writer.h" + +#include "utils.h" + +#include +#include +#include + +#include + +InputRecording* InputRecording_createRecorder(const char* filePath) { + InputRecording* rec = safeCalloc(1, sizeof(InputRecording)); + rec->isRecording = true; + rec->recordFilePath = filePath; + return rec; +} + +InputRecording* InputRecording_createPlayer(const char* playbackFilePath, const char* recordFilePath) { + // Read the file contents + FILE* f = fopen(playbackFilePath, "r"); + if (f == nullptr) { + fprintf(stderr, "Error: Could not open input recording file '%s'\n", playbackFilePath); + exit(1); + } + + fseek(f, 0, SEEK_END); + long fileSize = ftell(f); + fseek(f, 0, SEEK_SET); + + char* contents = safeMalloc(fileSize + 1); + fread(contents, 1, fileSize, f); + contents[fileSize] = '\0'; + fclose(f); + + // Parse JSON + JsonValue* root = JsonReader_parse(contents); + free(contents); + + if (root == nullptr || !JsonReader_isObject(root)) { + fprintf(stderr, "Error: Invalid JSON in input recording file '%s'\n", playbackFilePath); + exit(1); + } + + // Find the highest frame number to determine array size + int objectLen = JsonReader_objectLength(root); + int32_t maxFrame = -1; + repeat(objectLen, i) { + const char* key = JsonReader_getObjectKey(root, i); + int32_t frameNum = (int32_t) strtol(key, nullptr, 10); + if (frameNum > maxFrame) maxFrame = frameNum; + } + + InputRecording* rec = safeCalloc(1, sizeof(InputRecording)); + rec->isPlayback = true; + rec->playbackFrameCount = maxFrame + 1; + + // If a record path was provided, also enable recording + if (recordFilePath != nullptr) { + rec->isRecording = true; + rec->recordFilePath = recordFilePath; + } + + // Allocate playbackFrames array (one stb_ds int32_t array per frame) + rec->playbackFrames = safeCalloc(rec->playbackFrameCount, sizeof(int32_t*)); + + repeat(objectLen, i) { + const char* key = JsonReader_getObjectKey(root, i); + JsonValue* val = JsonReader_getObjectValue(root, i); + int32_t frameNum = (int32_t) strtol(key, nullptr, 10); + + if (JsonReader_isArray(val)) { + int keyCount = JsonReader_arrayLength(val); + int32_t* keys = nullptr; + repeat(keyCount, k) { + JsonValue* keyVal = JsonReader_getArrayElement(val, k); + arrput(keys, (int32_t) JsonReader_getInt(keyVal)); + } + rec->playbackFrames[frameNum] = keys; + } + } + + JsonReader_free(root); + fprintf(stderr, "InputRecording: Loaded %d frames from '%s'\n", rec->playbackFrameCount, playbackFilePath); + return rec; +} + +void InputRecording_free(InputRecording* recording) { + if (recording == nullptr) return; + + if (recording->recordedFrames != nullptr) { + int32_t count = (int32_t) arrlen(recording->recordedFrames); + repeat(count, i) { + arrfree(recording->recordedFrames[i]); + } + arrfree(recording->recordedFrames); + } + + if (recording->playbackFrames != nullptr) { + repeat(recording->playbackFrameCount, i) { + arrfree(recording->playbackFrames[i]); + } + free(recording->playbackFrames); + } + + free(recording); +} + +void InputRecording_processFrame(InputRecording* recording, RunnerKeyboardState* kb, int frameNumber) { + if (recording == nullptr) return; + + // Playback: overwrite keyboard state from recorded data (while frames remain) + if (recording->isPlayback) { + if (recording->playbackFrameCount > frameNumber) { + int32_t* frameKeys = recording->playbackFrames[frameNumber]; + int32_t keyCount = (int32_t) arrlen(frameKeys); + + // Build a temporary "current held" array for this frame + bool currentKeyDown[GML_KEY_COUNT]; + memset(currentKeyDown, 0, sizeof(currentKeyDown)); + repeat(keyCount, i) { + int32_t key = frameKeys[i]; + if (GML_KEY_COUNT > key && key >= 0) { + currentKeyDown[key] = true; + } + } + + // Derive transitions by comparing against previousKeyDown + repeat(GML_KEY_COUNT, key) { + kb->keyDown[key] = currentKeyDown[key]; + kb->keyPressed[key] = currentKeyDown[key] && !recording->previousKeyDown[key]; + kb->keyReleased[key] = !currentKeyDown[key] && recording->previousKeyDown[key]; + if (kb->keyPressed[key]) { + kb->lastKey = (int32_t) key; + } + } + + memcpy(recording->previousKeyDown, currentKeyDown, sizeof(currentKeyDown)); + } else { + // Past the end of recorded data: release everything, then let real input through + if (!recording->playbackEnded) { + fprintf(stderr, "InputRecording: Playback ended at frame %d (recorded %d frames)\n", frameNumber, recording->playbackFrameCount); + recording->playbackEnded = true; + + repeat(GML_KEY_COUNT, key) { + kb->keyReleased[key] = recording->previousKeyDown[key]; + kb->keyDown[key] = false; + kb->keyPressed[key] = false; + } + memset(recording->previousKeyDown, 0, sizeof(recording->previousKeyDown)); + } + // After the first "ended" frame, real keyboard input flows through naturally + } + } + + // Recording: snapshot whatever the current keyboard state is (from real input or playback) + if (recording->isRecording) { + int32_t* heldKeys = nullptr; + repeat(GML_KEY_COUNT, key) { + if (kb->keyDown[key]) { + arrput(heldKeys, (int32_t) key); + } + } + arrput(recording->recordedFrames, heldKeys); + } +} + +bool InputRecording_save(InputRecording* recording) { + if (recording == nullptr || !recording->isRecording) return false; + + int32_t frameCount = (int32_t) arrlen(recording->recordedFrames); + + JsonWriter w = JsonWriter_create(); + JsonWriter_beginObject(&w); + + repeat(frameCount, f) { + // Frame number as string key + char frameKey[16]; + snprintf(frameKey, sizeof(frameKey), "%d", (int) f); + JsonWriter_key(&w, frameKey); + + JsonWriter_beginArray(&w); + int32_t* keys = recording->recordedFrames[f]; + int32_t keyCount = (int32_t) arrlen(keys); + repeat(keyCount, k) { + JsonWriter_int(&w, keys[k]); + } + JsonWriter_endArray(&w); + } + + JsonWriter_endObject(&w); + + FILE* f = fopen(recording->recordFilePath, "w"); + if (f == nullptr) { + fprintf(stderr, "Error: Could not write input recording to '%s'\n", recording->recordFilePath); + JsonWriter_free(&w); + return false; + } + + const char* output = JsonWriter_getOutput(&w); + fwrite(output, 1, JsonWriter_getLength(&w), f); + fputc('\n', f); + fclose(f); + + fprintf(stderr, "InputRecording: Saved %d frames to '%s'\n", frameCount, recording->recordFilePath); + JsonWriter_free(&w); + return true; +} + +bool InputRecording_isPlaybackActive(InputRecording* recording) { + return recording != nullptr && recording->isPlayback && !recording->playbackEnded; +} diff --git a/src/input_recording.h b/src/input_recording.h index 100648fe..b57ef2bb 100644 --- a/src/input_recording.h +++ b/src/input_recording.h @@ -1,40 +1,40 @@ -#pragma once - -#include "common.h" -#include - -#include "runner_keyboard.h" - -typedef struct InputRecording { - bool isRecording; - bool isPlayback; - const char* recordFilePath; - - // Recording: stb_ds array of stb_ds int32_t arrays (one per frame) - int32_t** recordedFrames; - - // Playback: same structure, loaded from JSON - int32_t** playbackFrames; - int32_t playbackFrameCount; - bool playbackEnded; - bool previousKeyDown[GML_KEY_COUNT]; -} InputRecording; - -// Create a recorder that snapshots keyboard state each frame -InputRecording* InputRecording_createRecorder(const char* filePath); - -// Create a player that loads recorded input from a JSON file. -// If recordFilePath is non-null, also enables recording (playback + record mode). -InputRecording* InputRecording_createPlayer(const char* playbackFilePath, const char* recordFilePath); - -// Free all resources -void InputRecording_free(InputRecording* recording); - -// Called each frame: plays back recorded input (if active), then snapshots keyboard state (if recording) -void InputRecording_processFrame(InputRecording* recording, RunnerKeyboardState* kb, int frameNumber); - -// Write recorded frames to the JSON file (returns true on success) -bool InputRecording_save(InputRecording* recording); - -// Null-safe check: returns true if recording is non-null and playback hasn't ended yet -bool InputRecording_isPlaybackActive(InputRecording* recording); +#pragma once + +#include "common.h" +#include + +#include "runner_keyboard.h" + +typedef struct InputRecording { + bool isRecording; + bool isPlayback; + const char* recordFilePath; + + // Recording: stb_ds array of stb_ds int32_t arrays (one per frame) + int32_t** recordedFrames; + + // Playback: same structure, loaded from JSON + int32_t** playbackFrames; + int32_t playbackFrameCount; + bool playbackEnded; + bool previousKeyDown[GML_KEY_COUNT]; +} InputRecording; + +// Create a recorder that snapshots keyboard state each frame +InputRecording* InputRecording_createRecorder(const char* filePath); + +// Create a player that loads recorded input from a JSON file. +// If recordFilePath is non-null, also enables recording (playback + record mode). +InputRecording* InputRecording_createPlayer(const char* playbackFilePath, const char* recordFilePath); + +// Free all resources +void InputRecording_free(InputRecording* recording); + +// Called each frame: plays back recorded input (if active), then snapshots keyboard state (if recording) +void InputRecording_processFrame(InputRecording* recording, RunnerKeyboardState* kb, int frameNumber); + +// Write recorded frames to the JSON file (returns true on success) +bool InputRecording_save(InputRecording* recording); + +// Null-safe check: returns true if recording is non-null and playback hasn't ended yet +bool InputRecording_isPlaybackActive(InputRecording* recording); diff --git a/src/instance.c b/src/instance.c index f7fcef6c..ca83fa90 100644 --- a/src/instance.c +++ b/src/instance.c @@ -1,182 +1,188 @@ -#include "instance.h" - -#include -#include -#include - -#include "stb_ds.h" -#include "utils.h" -#include "int_rvalue_hashmap.h" - -Instance* Instance_create(uint32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y) { - Instance* inst = safeCalloc(1, sizeof(Instance)); - inst->instanceId = instanceId; - inst->objectIndex = objectIndex; - inst->refCount = 0; - inst->structRegistryIndex = -1; - inst->x = (float) x; - inst->y = (float) y; - inst->xprevious = (float) x; - inst->yprevious = (float) y; - inst->xstart = (float) x; - inst->ystart = (float) y; - inst->maskIndex = -1; - inst->persistent = false; - inst->solid = false; - inst->active = true; - inst->visible = true; - inst->destroyed = false; +#include "instance.h" + +#include +#include +#include + +#include "stb_ds.h" +#include "utils.h" +#include "int_rvalue_hashmap.h" + +Instance* Instance_create(uint32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y) { + Instance* inst = safeCalloc(1, sizeof(Instance)); + inst->instanceId = instanceId; + inst->objectIndex = objectIndex; + inst->refCount = 0; + inst->structRegistryIndex = -1; + inst->x = (float) x; + inst->y = (float) y; + inst->xprevious = (float) x; + inst->yprevious = (float) y; + inst->xstart = (float) x; + inst->ystart = (float) y; + inst->maskIndex = -1; + inst->persistent = false; + inst->solid = false; + inst->active = true; + inst->visible = true; + inst->destroyed = false; inst->outsideRoom = false; inst->spatialGridDirty = false; inst->spriteIndex = -1; + inst->cachedDrawSpriteIndex = -1; + inst->cachedDrawSubimg = INT32_MIN; + inst->cachedDrawTPAGIndex = -1; inst->imageSpeed = 1.0f; - inst->imageIndex = 0.0f; - inst->imageXscale = 1.0f; - inst->imageYscale = 1.0f; - inst->imageAngle = 0.0f; - inst->imageAlpha = 1.0f; - inst->imageBlend = 0xFFFFFF; - inst->depth = 0; - inst->layer = -1; - inst->speed = 0.0f; - inst->direction = 0.0f; - inst->hspeed = 0.0f; - inst->vspeed = 0.0f; - inst->friction = 0.0f; - inst->gravity = 0.0f; - inst->gravityDirection = 270.0f; - inst->pathIndex = -1; - inst->pathScale = 1.0f; - - // Initialize alarms to -1 (inactive) - repeat(GML_ALARM_COUNT, i) { - inst->alarm[i] = -1; - } - - return inst; -} - -void Instance_structIncRef(Instance* inst) { - if (inst == nullptr) return; - inst->refCount++; -} - -void Instance_structDecRef(Instance* inst) { - if (inst == nullptr) return; - require(inst->refCount > 0); - inst->refCount--; - // Never free here. The runner-side sweep reaps structs whose refCount has dropped to 1. (that is: when only the structInstances holds it) -} - -uint32_t Instance_getInstanceId(Instance* inst) { - return inst != nullptr ? inst->instanceId : 0; -} - -void Instance_free(Instance* instance) { - if (instance == nullptr) return; - - // Free owned strings and decRef owned arrays in selfVars hashmap, then release the entries buffer. - IntRValueHashMap_freeAllValues(&instance->selfVars); - arrfree(instance->collisionCells); - - free(instance); -} - -void Instance_copyFields(Instance* source, Instance* destination) { - destination->x = source->x; - destination->y = source->y; - destination->xprevious = source->xprevious; - destination->yprevious = source->yprevious; - destination->xstart = source->xstart; - destination->ystart = source->ystart; - destination->persistent = source->persistent; - destination->solid = source->solid; - destination->active = source->active; - destination->visible = source->visible; + inst->imageIndex = 0.0f; + inst->imageXscale = 1.0f; + inst->imageYscale = 1.0f; + inst->imageAngle = 0.0f; + inst->imageAlpha = 1.0f; + inst->imageBlend = 0xFFFFFF; + inst->depth = 0; + inst->layer = -1; + inst->speed = 0.0f; + inst->direction = 0.0f; + inst->hspeed = 0.0f; + inst->vspeed = 0.0f; + inst->friction = 0.0f; + inst->gravity = 0.0f; + inst->gravityDirection = 270.0f; + inst->pathIndex = -1; + inst->pathScale = 1.0f; + + // Initialize alarms to -1 (inactive) + repeat(GML_ALARM_COUNT, i) { + inst->alarm[i] = -1; + } + + return inst; +} + +void Instance_structIncRef(Instance* inst) { + if (inst == nullptr) return; + inst->refCount++; +} + +void Instance_structDecRef(Instance* inst) { + if (inst == nullptr) return; + require(inst->refCount > 0); + inst->refCount--; + // Never free here. The runner-side sweep reaps structs whose refCount has dropped to 1. (that is: when only the structInstances holds it) +} + +uint32_t Instance_getInstanceId(Instance* inst) { + return inst != nullptr ? inst->instanceId : 0; +} + +void Instance_free(Instance* instance) { + if (instance == nullptr) return; + + // Free owned strings and decRef owned arrays in selfVars hashmap, then release the entries buffer. + IntRValueHashMap_freeAllValues(&instance->selfVars); + arrfree(instance->collisionCells); + + free(instance); +} + +void Instance_copyFields(Instance* source, Instance* destination) { + destination->x = source->x; + destination->y = source->y; + destination->xprevious = source->xprevious; + destination->yprevious = source->yprevious; + destination->xstart = source->xstart; + destination->ystart = source->ystart; + destination->persistent = source->persistent; + destination->solid = source->solid; + destination->active = source->active; + destination->visible = source->visible; destination->outsideRoom = source->outsideRoom; destination->maskIndex = source->maskIndex; destination->spriteIndex = source->spriteIndex; + destination->cachedDrawSpriteIndex = source->cachedDrawSpriteIndex; + destination->cachedDrawSubimg = source->cachedDrawSubimg; + destination->cachedDrawTPAGIndex = source->cachedDrawTPAGIndex; destination->imageSpeed = source->imageSpeed; - destination->imageIndex = source->imageIndex; - destination->imageXscale = source->imageXscale; - destination->imageYscale = source->imageYscale; - destination->imageAngle = source->imageAngle; - destination->imageAlpha = source->imageAlpha; - destination->imageBlend = source->imageBlend; - destination->depth = source->depth; - destination->layer = source->layer; - destination->speed = source->speed; - destination->direction = source->direction; - destination->hspeed = source->hspeed; - destination->vspeed = source->vspeed; - destination->friction = source->friction; - destination->gravity = source->gravity; - destination->gravityDirection = source->gravityDirection; - destination->pathIndex = source->pathIndex; - destination->pathPosition = source->pathPosition; - destination->pathPositionPrevious = source->pathPositionPrevious; - destination->pathSpeed = source->pathSpeed; - destination->pathScale = source->pathScale; - destination->pathOrientation = source->pathOrientation; - destination->pathEndAction = source->pathEndAction; - destination->pathXStart = source->pathXStart; - destination->pathYStart = source->pathYStart; - repeat(GML_ALARM_COUNT, i) { - destination->alarm[i] = source->alarm[i]; - } - destination->activeAlarmMask = source->activeAlarmMask; - - // Deep-copy self variables (Instance_setSelfVar handles string duplication + array incRef) - repeat(source->selfVars.capacity, i) { - IntRValueEntry* entry = &source->selfVars.entries[i]; - if (entry->key != INT_RVALUE_HASHMAP_EMPTY_KEY) { - Instance_setSelfVar(destination, entry->key, entry->value); - } - } -} - -// Compute speed and direction from hspeed/vspeed (HTML5: Compute_Speed1) -void Instance_computeSpeedFromComponents(Instance* inst) { - // Direction - if (inst->hspeed == 0.0f) { - if (inst->vspeed > 0.0f) { - inst->direction = 270.0f; - } else if (inst->vspeed < 0.0f) { - inst->direction = 90.0f; - } - // If both are 0, direction stays unchanged - } else { - GMLReal dd = clampFloat(180.0 * GMLReal_atan2(inst->vspeed, inst->hspeed) / M_PI); - if (dd <= 0.0) { - inst->direction = (float) -dd; - } else { - inst->direction = (float) (360.0 - dd); - } - } - - // Round direction if very close to integer - if (GMLReal_fabs(inst->direction - GMLReal_round(inst->direction)) < 0.0001) { - inst->direction = (float) GMLReal_round(inst->direction); - } - inst->direction = (float) GMLReal_fmod(inst->direction, 360.0); - - // Speed - inst->speed = (float) GMLReal_sqrt(inst->hspeed * inst->hspeed + inst->vspeed * inst->vspeed); - if (GMLReal_fabs(inst->speed - GMLReal_round(inst->speed)) < 0.0001) { - inst->speed = (float) GMLReal_round(inst->speed); - } -} - -// Compute hspeed/vspeed from speed and direction (HTML5: Compute_Speed2) -void Instance_computeComponentsFromSpeed(Instance* inst) { - inst->hspeed = (float) (inst->speed * clampFloat(GMLReal_cos(inst->direction * (M_PI / 180.0)))); - inst->vspeed = (float) (-inst->speed * clampFloat(GMLReal_sin(inst->direction * (M_PI / 180.0)))); - - // Round if very close to integer - if (GMLReal_fabs(inst->hspeed - GMLReal_round(inst->hspeed)) < 0.0001) { - inst->hspeed = (float) GMLReal_round(inst->hspeed); - } - if (GMLReal_fabs(inst->vspeed - GMLReal_round(inst->vspeed)) < 0.0001) { - inst->vspeed = (float) GMLReal_round(inst->vspeed); - } -} + destination->imageIndex = source->imageIndex; + destination->imageXscale = source->imageXscale; + destination->imageYscale = source->imageYscale; + destination->imageAngle = source->imageAngle; + destination->imageAlpha = source->imageAlpha; + destination->imageBlend = source->imageBlend; + destination->depth = source->depth; + destination->layer = source->layer; + destination->speed = source->speed; + destination->direction = source->direction; + destination->hspeed = source->hspeed; + destination->vspeed = source->vspeed; + destination->friction = source->friction; + destination->gravity = source->gravity; + destination->gravityDirection = source->gravityDirection; + destination->pathIndex = source->pathIndex; + destination->pathPosition = source->pathPosition; + destination->pathPositionPrevious = source->pathPositionPrevious; + destination->pathSpeed = source->pathSpeed; + destination->pathScale = source->pathScale; + destination->pathOrientation = source->pathOrientation; + destination->pathEndAction = source->pathEndAction; + destination->pathXStart = source->pathXStart; + destination->pathYStart = source->pathYStart; + repeat(GML_ALARM_COUNT, i) { + destination->alarm[i] = source->alarm[i]; + } + destination->activeAlarmMask = source->activeAlarmMask; + + // Deep-copy self variables (Instance_setSelfVar handles string duplication + array incRef) + repeat(source->selfVars.capacity, i) { + IntRValueEntry* entry = &source->selfVars.entries[i]; + if (entry->key != INT_RVALUE_HASHMAP_EMPTY_KEY) { + Instance_setSelfVar(destination, entry->key, entry->value); + } + } +} + +// Compute speed and direction from hspeed/vspeed (HTML5: Compute_Speed1) +void Instance_computeSpeedFromComponents(Instance* inst) { + // Direction + if (inst->hspeed == 0.0f) { + if (inst->vspeed > 0.0f) { + inst->direction = 270.0f; + } else if (inst->vspeed < 0.0f) { + inst->direction = 90.0f; + } + // If both are 0, direction stays unchanged + } else { + GMLReal dd = clampFloat(180.0 * GMLReal_atan2(inst->vspeed, inst->hspeed) / M_PI); + if (dd <= 0.0) { + inst->direction = (float) -dd; + } else { + inst->direction = (float) (360.0 - dd); + } + } + + // Round direction if very close to integer + if (GMLReal_fabs(inst->direction - GMLReal_round(inst->direction)) < 0.0001) { + inst->direction = (float) GMLReal_round(inst->direction); + } + inst->direction = (float) GMLReal_fmod(inst->direction, 360.0); + + // Speed + inst->speed = (float) GMLReal_sqrt(inst->hspeed * inst->hspeed + inst->vspeed * inst->vspeed); + if (GMLReal_fabs(inst->speed - GMLReal_round(inst->speed)) < 0.0001) { + inst->speed = (float) GMLReal_round(inst->speed); + } +} + +// Compute hspeed/vspeed from speed and direction (HTML5: Compute_Speed2) +void Instance_computeComponentsFromSpeed(Instance* inst) { + inst->hspeed = (float) (inst->speed * clampFloat(GMLReal_cos(inst->direction * (M_PI / 180.0)))); + inst->vspeed = (float) (-inst->speed * clampFloat(GMLReal_sin(inst->direction * (M_PI / 180.0)))); + + // Round if very close to integer + if (GMLReal_fabs(inst->hspeed - GMLReal_round(inst->hspeed)) < 0.0001) { + inst->hspeed = (float) GMLReal_round(inst->hspeed); + } + if (GMLReal_fabs(inst->vspeed - GMLReal_round(inst->vspeed)) < 0.0001) { + inst->vspeed = (float) GMLReal_round(inst->vspeed); + } +} diff --git a/src/instance.h b/src/instance.h index 11d3397c..5fcba4da 100644 --- a/src/instance.h +++ b/src/instance.h @@ -1,111 +1,114 @@ -#pragma once - -#include "common.h" -#include -#include "rvalue.h" -#include "gml_array.h" -#include "int_rvalue_hashmap.h" - -#define GML_ALARM_COUNT 12 - -// Forward decl for Instance_structDecRef -struct Runner; - -typedef struct Instance { - uint32_t instanceId; - int32_t objectIndex; - // Reference count for GML structs (objectIndex == -1 mode). Unused for game-object instances. - // The runner's structInstances registry holds an implicit +1 ref while the struct is registered, so a refCount of 1 means "only the registry references this"; the per-frame sweep (Runner_sweepDeadStructs) decRefs those to free them. RValues with ownsReference=true on RVALUE_STRUCT contribute one ref each. - int32_t refCount; - // Position of this struct in runner->structInstances (for O(1) swap-remove when freed). -1 when not registered. - int32_t structRegistryIndex; - // Native GMS runner stores all instance built-in variables as float (32-bit), - // even though RValues use double. This matches the native precision model. - float x, y; - float xprevious, yprevious; - float xstart, ystart; - bool persistent, solid, active, destroyed, visible, createEventFired, outsideRoom, spatialGridDirty; - // Used to track which alarms are set without looping through the entire alarm array - uint16_t activeAlarmMask; - int32_t maskIndex; // collision mask sprite override (-1 = use spriteIndex) - int32_t* collisionCells; // Used to track where we are - uint32_t lastCollisionQueryId; - - // Per-instance self variable storage (sparse open-addressed hashmap, keyed by varID). - IntRValueHashMap selfVars; - +#pragma once + +#include "common.h" +#include +#include "rvalue.h" +#include "gml_array.h" +#include "int_rvalue_hashmap.h" + +#define GML_ALARM_COUNT 12 + +// Forward decl for Instance_structDecRef +struct Runner; + +typedef struct Instance { + uint32_t instanceId; + int32_t objectIndex; + // Reference count for GML structs (objectIndex == -1 mode). Unused for game-object instances. + // The runner's structInstances registry holds an implicit +1 ref while the struct is registered, so a refCount of 1 means "only the registry references this"; the per-frame sweep (Runner_sweepDeadStructs) decRefs those to free them. RValues with ownsReference=true on RVALUE_STRUCT contribute one ref each. + int32_t refCount; + // Position of this struct in runner->structInstances (for O(1) swap-remove when freed). -1 when not registered. + int32_t structRegistryIndex; + // Native GMS runner stores all instance built-in variables as float (32-bit), + // even though RValues use double. This matches the native precision model. + float x, y; + float xprevious, yprevious; + float xstart, ystart; + bool persistent, solid, active, destroyed, visible, createEventFired, outsideRoom, spatialGridDirty; + // Used to track which alarms are set without looping through the entire alarm array + uint16_t activeAlarmMask; + int32_t maskIndex; // collision mask sprite override (-1 = use spriteIndex) + int32_t* collisionCells; // Used to track where we are + uint32_t lastCollisionQueryId; + + // Per-instance self variable storage (sparse open-addressed hashmap, keyed by varID). + IntRValueHashMap selfVars; + // Built-in instance properties int32_t spriteIndex; + int32_t cachedDrawSpriteIndex; + int32_t cachedDrawSubimg; + int32_t cachedDrawTPAGIndex; float imageSpeed, imageIndex; - float imageXscale, imageYscale, imageAngle, imageAlpha; - uint32_t imageBlend; - int32_t depth; - int32_t layer; - - // Motion properties - float speed, direction; - float hspeed, vspeed; - float friction; - float gravity, gravityDirection; - - // Path following state - int32_t pathIndex; // -1 = no path active - float pathPosition; // 0.0-1.0 - float pathPositionPrevious; - float pathSpeed; - float pathScale; // default 1.0 - float pathOrientation; // degrees, default 0.0 - int32_t pathEndAction; // 0=stop, 1=restart, 2=continue, 3=reverse - float pathXStart; // origin for relative paths - float pathYStart; - - int32_t alarm[GML_ALARM_COUNT]; -} Instance; - -Instance* Instance_create(uint32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y); -void Instance_free(Instance* instance); - -// GML-struct refcount helpers. Only meaningful when inst->objectIndex == -1. -// incRef: bumps the count. decRef: drops the count. Never frees on its own; the per-frame sweep (Runner_sweepDeadStructs) is the single point that physically frees a struct (after dropping the registry's implicit ref). -void Instance_structIncRef(Instance* inst); -void Instance_structDecRef(Instance* inst); - -// Deep-copy all mutable fields from source to dst: built-in properties, alarms, selfVars. -// Does NOT copy instanceId, objectIndex, destroyed, or createEventFired. Strings are duplicated so ownership stays independent. Arrays bump refCount (shared - CoW handles forking on first write). -void Instance_copyFields(Instance* dst, Instance* source); - -// Get a self variable by varID. Returns RVALUE_UNDEFINED if absent. The returned RValue is non-owning (weak view - do not RValue_free unless you incRef/strdup first to strengthen). -static inline RValue Instance_getSelfVar(Instance* inst, int32_t varID) { - requireNotNull(inst); - return IntRValueHashMap_get(&inst->selfVars, varID); -} - -// Set a self variable by varID. Frees the old value if present (decRefs owned arrays). -// Always takes an independent reference: strings are strdup'd, arrays are incRef'd, regardless of whether the caller's RValue was owning. -// The caller retains ownership of their original `val` and remains responsible for freeing it (via RValue_free) when done. -static inline void Instance_setSelfVar(Instance* inst, int32_t varID, RValue val) { - requireNotNull(inst); - // One lookup: returns the existing slot, or inserts UNDEFINED and returns the new slot. - RValue* slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varID); - RValue_free(slot); - if (val.type == RVALUE_STRING && val.string != nullptr) { - val = RValue_makeOwnedString(safeStrdup(val.string)); - } else if (val.type == RVALUE_ARRAY && val.array != nullptr) { - GMLArray_incRef(val.array); - val.ownsReference = true; -#if IS_BC17_OR_HIGHER_ENABLED - } else if (val.type == RVALUE_METHOD && val.method != nullptr) { - GMLMethod_incRef(val.method); - val.ownsReference = true; -#endif - } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) { - Instance_structIncRef(val.structInst); - val.ownsReference = true; - } - *slot = val; -} - -// Recompute speed/direction from hspeed/vspeed (called when hspeed or vspeed is set) -void Instance_computeSpeedFromComponents(Instance* inst); -// Recompute hspeed/vspeed from speed/direction (called when speed or direction is set) -void Instance_computeComponentsFromSpeed(Instance* inst); + float imageXscale, imageYscale, imageAngle, imageAlpha; + uint32_t imageBlend; + int32_t depth; + int32_t layer; + + // Motion properties + float speed, direction; + float hspeed, vspeed; + float friction; + float gravity, gravityDirection; + + // Path following state + int32_t pathIndex; // -1 = no path active + float pathPosition; // 0.0-1.0 + float pathPositionPrevious; + float pathSpeed; + float pathScale; // default 1.0 + float pathOrientation; // degrees, default 0.0 + int32_t pathEndAction; // 0=stop, 1=restart, 2=continue, 3=reverse + float pathXStart; // origin for relative paths + float pathYStart; + + int32_t alarm[GML_ALARM_COUNT]; +} Instance; + +Instance* Instance_create(uint32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y); +void Instance_free(Instance* instance); + +// GML-struct refcount helpers. Only meaningful when inst->objectIndex == -1. +// incRef: bumps the count. decRef: drops the count. Never frees on its own; the per-frame sweep (Runner_sweepDeadStructs) is the single point that physically frees a struct (after dropping the registry's implicit ref). +void Instance_structIncRef(Instance* inst); +void Instance_structDecRef(Instance* inst); + +// Deep-copy all mutable fields from source to dst: built-in properties, alarms, selfVars. +// Does NOT copy instanceId, objectIndex, destroyed, or createEventFired. Strings are duplicated so ownership stays independent. Arrays bump refCount (shared - CoW handles forking on first write). +void Instance_copyFields(Instance* dst, Instance* source); + +// Get a self variable by varID. Returns RVALUE_UNDEFINED if absent. The returned RValue is non-owning (weak view - do not RValue_free unless you incRef/strdup first to strengthen). +static inline RValue Instance_getSelfVar(Instance* inst, int32_t varID) { + requireNotNull(inst); + return IntRValueHashMap_get(&inst->selfVars, varID); +} + +// Set a self variable by varID. Frees the old value if present (decRefs owned arrays). +// Always takes an independent reference: strings are strdup'd, arrays are incRef'd, regardless of whether the caller's RValue was owning. +// The caller retains ownership of their original `val` and remains responsible for freeing it (via RValue_free) when done. +static inline void Instance_setSelfVar(Instance* inst, int32_t varID, RValue val) { + requireNotNull(inst); + // One lookup: returns the existing slot, or inserts UNDEFINED and returns the new slot. + RValue* slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varID); + RValue_free(slot); + if (val.type == RVALUE_STRING && val.string != nullptr) { + val = RValue_makeOwnedString(safeStrdup(val.string)); + } else if (val.type == RVALUE_ARRAY && val.array != nullptr) { + GMLArray_incRef(val.array); + val.ownsReference = true; +#if IS_BC17_OR_HIGHER_ENABLED + } else if (val.type == RVALUE_METHOD && val.method != nullptr) { + GMLMethod_incRef(val.method); + val.ownsReference = true; +#endif + } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) { + Instance_structIncRef(val.structInst); + val.ownsReference = true; + } + *slot = val; +} + +// Recompute speed/direction from hspeed/vspeed (called when hspeed or vspeed is set) +void Instance_computeSpeedFromComponents(Instance* inst); +// Recompute hspeed/vspeed from speed/direction (called when speed or direction is set) +void Instance_computeComponentsFromSpeed(Instance* inst); diff --git a/src/json_reader.c b/src/json_reader.c index 18ceea23..c3fa3aa4 100644 --- a/src/json_reader.c +++ b/src/json_reader.c @@ -1,503 +1,503 @@ -#include "json_reader.h" - -#include -#include -#include - -#include "utils.h" - -// ===[ Parser State ]=== - -typedef struct { - const char* input; - size_t position; - size_t length; -} JsonParser; - -// ===[ Internal Helpers ]=== - -static void skipWhitespace(JsonParser* parser) { - while (parser->position < parser->length) { - char c = parser->input[parser->position]; - if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { - parser->position++; - } else { - break; - } - } -} - -static char peek(JsonParser* parser) { - if (parser->position >= parser->length) return '\0'; - return parser->input[parser->position]; -} - -static char advance(JsonParser* parser) { - if (parser->position >= parser->length) return '\0'; - return parser->input[parser->position++]; -} - -static JsonValue* makeValue(JsonValueType type) { - JsonValue* value = safeCalloc(1, sizeof(JsonValue)); - if (value == nullptr) { - fprintf(stderr, "JsonReader: calloc failed\n"); - abort(); - } - value->type = type; - return value; -} - -// Forward declaration for recursive parsing -static JsonValue* parseValue(JsonParser* parser); - -static JsonValue* parseString(JsonParser* parser) { - // Skip opening quote - advance(parser); - - size_t capacity = 64; - size_t length = 0; - char* buffer = safeMalloc(capacity); - if (buffer == nullptr) { - fprintf(stderr, "JsonReader: malloc failed\n"); - abort(); - } - - while (parser->position < parser->length) { - char c = advance(parser); - - if (c == '"') { - // End of string - buffer[length] = '\0'; - JsonValue* value = makeValue(JSON_STRING); - value->stringValue = buffer; - return value; - } - - if (c == '\\') { - // Escape sequence - char escaped = advance(parser); - switch (escaped) { - case '"': c = '"'; break; - case '\\': c = '\\'; break; - case '/': c = '/'; break; - case 'b': c = '\b'; break; - case 'f': c = '\f'; break; - case 'n': c = '\n'; break; - case 'r': c = '\r'; break; - case 't': c = '\t'; break; - case 'u': { - // Parse 4-digit hex code point - char hex[5] = {0}; - repeat(4, i) { - hex[i] = advance(parser); - } - unsigned long codePoint = strtoul(hex, nullptr, 16); - - // Encode as UTF-8 - if (128 > codePoint) { - c = (char) codePoint; - } else if (2048 > codePoint) { - if (length + 2 >= capacity) { - capacity *= 2; - buffer = safeRealloc(buffer, capacity); - } - buffer[length++] = (char) (0xC0 | (codePoint >> 6)); - c = (char) (0x80 | (codePoint & 0x3F)); - } else { - if (length + 3 >= capacity) { - capacity *= 2; - buffer = safeRealloc(buffer, capacity); - } - buffer[length++] = (char) (0xE0 | (codePoint >> 12)); - buffer[length++] = (char) (0x80 | ((codePoint >> 6) & 0x3F)); - c = (char) (0x80 | (codePoint & 0x3F)); - } - break; - } - default: - fprintf(stderr, "JsonReader: unknown escape sequence '\\%c'\n", escaped); - free(buffer); - return nullptr; - } - } - - // Grow buffer if needed - if (length + 1 >= capacity) { - capacity *= 2; - buffer = safeRealloc(buffer, capacity); - if (buffer == nullptr) { - fprintf(stderr, "JsonReader: realloc failed\n"); - abort(); - } - } - buffer[length++] = c; - } - - // Unterminated string - fprintf(stderr, "JsonReader: unterminated string\n"); - free(buffer); - return nullptr; -} - -static JsonValue* parseNumber(JsonParser* parser) { - const char* start = parser->input + parser->position; - char* end = nullptr; - double number = strtod(start, &end); - if (end == start) { - fprintf(stderr, "JsonReader: invalid number\n"); - return nullptr; - } - parser->position += (size_t) (end - start); - - JsonValue* value = makeValue(JSON_NUMBER); - value->numberValue = number; - return value; -} - -static JsonValue* parseArray(JsonParser* parser) { - // Skip opening bracket - advance(parser); - - JsonValue* value = makeValue(JSON_ARRAY); - value->array.items = nullptr; - value->array.count = 0; - value->array.capacity = 0; - - skipWhitespace(parser); - if (peek(parser) == ']') { - advance(parser); - return value; - } - - while (true) { - skipWhitespace(parser); - JsonValue* item = parseValue(parser); - if (item == nullptr) { - JsonReader_free(value); - return nullptr; - } - - // Grow items array if needed - if (value->array.count >= value->array.capacity) { - int newCapacity = (value->array.capacity == 0) ? 8 : value->array.capacity * 2; - value->array.items = safeRealloc(value->array.items, (size_t) newCapacity * sizeof(JsonValue)); - if (value->array.items == nullptr) { - fprintf(stderr, "JsonReader: realloc failed\n"); - abort(); - } - value->array.capacity = newCapacity; - } - - // Copy item into array and free the container - value->array.items[value->array.count++] = *item; - free(item); - - skipWhitespace(parser); - if (peek(parser) == ',') { - advance(parser); - } else if (peek(parser) == ']') { - advance(parser); - return value; - } else { - fprintf(stderr, "JsonReader: expected ',' or ']' in array\n"); - JsonReader_free(value); - return nullptr; - } - } -} - -static JsonValue* parseObject(JsonParser* parser) { - // Skip opening brace - advance(parser); - - JsonValue* value = makeValue(JSON_OBJECT); - value->object.keys = nullptr; - value->object.values = nullptr; - value->object.count = 0; - value->object.capacity = 0; - - skipWhitespace(parser); - if (peek(parser) == '}') { - advance(parser); - return value; - } - - while (true) { - skipWhitespace(parser); - if (peek(parser) != '"') { - fprintf(stderr, "JsonReader: expected string key in object\n"); - JsonReader_free(value); - return nullptr; - } - - JsonValue* keyValue = parseString(parser); - if (keyValue == nullptr) { - JsonReader_free(value); - return nullptr; - } - char* key = keyValue->stringValue; - // Free just the JsonValue container, keep the string - free(keyValue); - - skipWhitespace(parser); - if (peek(parser) != ':') { - fprintf(stderr, "JsonReader: expected ':' after object key\n"); - free(key); - JsonReader_free(value); - return nullptr; - } - advance(parser); - - skipWhitespace(parser); - JsonValue* itemValue = parseValue(parser); - if (itemValue == nullptr) { - free(key); - JsonReader_free(value); - return nullptr; - } - - // Grow arrays if needed - if (value->object.count >= value->object.capacity) { - int newCapacity = (value->object.capacity == 0) ? 8 : value->object.capacity * 2; - value->object.keys = safeRealloc(value->object.keys, (size_t) newCapacity * sizeof(char*)); - value->object.values = safeRealloc(value->object.values, (size_t) newCapacity * sizeof(JsonValue)); - if (value->object.keys == nullptr || value->object.values == nullptr) { - fprintf(stderr, "JsonReader: realloc failed\n"); - abort(); - } - value->object.capacity = newCapacity; - } - - value->object.keys[value->object.count] = key; - value->object.values[value->object.count] = *itemValue; - value->object.count++; - free(itemValue); - - skipWhitespace(parser); - if (peek(parser) == ',') { - advance(parser); - } else if (peek(parser) == '}') { - advance(parser); - return value; - } else { - fprintf(stderr, "JsonReader: expected ',' or '}' in object\n"); - JsonReader_free(value); - return nullptr; - } - } -} - -static JsonValue* parseLiteral(JsonParser* parser, const char* literal, size_t literalLen) { - if (parser->position + literalLen > parser->length) { - return nullptr; - } - if (memcmp(parser->input + parser->position, literal, literalLen) != 0) { - return nullptr; - } - parser->position += literalLen; - return makeValue(JSON_NULL); // Caller overrides type as needed -} - -static JsonValue* parseValue(JsonParser* parser) { - skipWhitespace(parser); - char c = peek(parser); - - switch (c) { - case '"': - return parseString(parser); - case '{': - return parseObject(parser); - case '[': - return parseArray(parser); - case 't': { - JsonValue* value = parseLiteral(parser, "true", 4); - if (value == nullptr) { - fprintf(stderr, "JsonReader: invalid literal\n"); - return nullptr; - } - value->type = JSON_BOOL; - value->boolValue = true; - return value; - } - case 'f': { - JsonValue* value = parseLiteral(parser, "false", 5); - if (value == nullptr) { - fprintf(stderr, "JsonReader: invalid literal\n"); - return nullptr; - } - value->type = JSON_BOOL; - value->boolValue = false; - return value; - } - case 'n': { - JsonValue* value = parseLiteral(parser, "null", 4); - if (value == nullptr) { - fprintf(stderr, "JsonReader: invalid literal\n"); - return nullptr; - } - return value; - } - default: - if (c == '-' || (c >= '0' && c <= '9')) { - return parseNumber(parser); - } - fprintf(stderr, "JsonReader: unexpected character '%c' at position %zu\n", c, parser->position); - return nullptr; - } -} - -// ===[ Lifecycle ]=== - -JsonValue* JsonReader_parse(const char* json) { - if (json == nullptr) return nullptr; - - JsonParser parser = { - .input = json, - .position = 0, - .length = strlen(json), - }; - - JsonValue* result = parseValue(&parser); - - // Check for trailing non-whitespace - if (result != nullptr) { - skipWhitespace(&parser); - if (parser.position < parser.length) { - fprintf(stderr, "JsonReader: trailing content after JSON value at position %zu\n", parser.position); - JsonReader_free(result); - return nullptr; - } - } - - return result; -} - -// Frees the contents of a JsonValue without freeing the JsonValue struct itself. -// Used for inline values (array items, object values stored by value). -static void freeContents(JsonValue* value) { - switch (value->type) { - case JSON_STRING: - free(value->stringValue); - break; - case JSON_ARRAY: - repeat(value->array.count, i) { - freeContents(&value->array.items[i]); - } - free(value->array.items); - break; - case JSON_OBJECT: - repeat(value->object.count, i) { - free(value->object.keys[i]); - freeContents(&value->object.values[i]); - } - free(value->object.keys); - free(value->object.values); - break; - default: - break; - } -} - -void JsonReader_free(JsonValue* value) { - if (value == nullptr) return; - freeContents(value); - free(value); -} - -// ===[ Type Checks ]=== - -bool JsonReader_isNull(const JsonValue* value) { - return value != nullptr && value->type == JSON_NULL; -} - -bool JsonReader_isBool(const JsonValue* value) { - return value != nullptr && value->type == JSON_BOOL; -} - -bool JsonReader_isNumber(const JsonValue* value) { - return value != nullptr && value->type == JSON_NUMBER; -} - -bool JsonReader_isString(const JsonValue* value) { - return value != nullptr && value->type == JSON_STRING; -} - -bool JsonReader_isArray(const JsonValue* value) { - return value != nullptr && value->type == JSON_ARRAY; -} - -bool JsonReader_isObject(const JsonValue* value) { - return value != nullptr && value->type == JSON_OBJECT; -} - -// ===[ Value Getters ]=== - -bool JsonReader_getBool(const JsonValue* value) { - return value->boolValue; -} - -double JsonReader_getDouble(const JsonValue* value) { - return value->numberValue; -} - -int64_t JsonReader_getInt(const JsonValue* value) { - return (int64_t) value->numberValue; -} - -const char* JsonReader_getString(const JsonValue* value) { - return value->stringValue; -} - -// ===[ Array Access ]=== - -int JsonReader_arrayLength(const JsonValue* value) { - return value->array.count; -} - -JsonValue* JsonReader_getArrayElement(const JsonValue* value, int index) { - if (0 > index || index >= value->array.count) return nullptr; - return &value->array.items[index]; -} - -// ===[ Array Bulk Read ]=== - -void JsonReader_readFloatArray(const JsonValue* value, float* out, int expectedLen) { - require(value != nullptr && value->type == JSON_ARRAY); - require(value->array.count == expectedLen); - repeat(expectedLen, i) { - out[i] = (float) value->array.items[i].numberValue; - } -} - -void JsonReader_readInt32Array(const JsonValue* value, int32_t* out, int expectedLen) { - require(value != nullptr && value->type == JSON_ARRAY); - require(value->array.count == expectedLen); - repeat(expectedLen, i) { - out[i] = (int32_t) value->array.items[i].numberValue; - } -} - -// ===[ Object Access ]=== - -int JsonReader_objectLength(const JsonValue* value) { - return value->object.count; -} - -JsonValue* JsonReader_getObject(const JsonValue* value, const char* key) { - repeat(value->object.count, i) { - if (strcmp(value->object.keys[i], key) == 0) { - return &value->object.values[i]; - } - } - return nullptr; -} - -const char* JsonReader_getObjectKey(const JsonValue* value, int index) { - if (0 > index || index >= value->object.count) return nullptr; - return value->object.keys[index]; -} - -JsonValue* JsonReader_getObjectValue(const JsonValue* value, int index) { - if (0 > index || index >= value->object.count) return nullptr; - return &value->object.values[index]; -} +#include "json_reader.h" + +#include +#include +#include + +#include "utils.h" + +// ===[ Parser State ]=== + +typedef struct { + const char* input; + size_t position; + size_t length; +} JsonParser; + +// ===[ Internal Helpers ]=== + +static void skipWhitespace(JsonParser* parser) { + while (parser->position < parser->length) { + char c = parser->input[parser->position]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + parser->position++; + } else { + break; + } + } +} + +static char peek(JsonParser* parser) { + if (parser->position >= parser->length) return '\0'; + return parser->input[parser->position]; +} + +static char advance(JsonParser* parser) { + if (parser->position >= parser->length) return '\0'; + return parser->input[parser->position++]; +} + +static JsonValue* makeValue(JsonValueType type) { + JsonValue* value = safeCalloc(1, sizeof(JsonValue)); + if (value == nullptr) { + fprintf(stderr, "JsonReader: calloc failed\n"); + abort(); + } + value->type = type; + return value; +} + +// Forward declaration for recursive parsing +static JsonValue* parseValue(JsonParser* parser); + +static JsonValue* parseString(JsonParser* parser) { + // Skip opening quote + advance(parser); + + size_t capacity = 64; + size_t length = 0; + char* buffer = safeMalloc(capacity); + if (buffer == nullptr) { + fprintf(stderr, "JsonReader: malloc failed\n"); + abort(); + } + + while (parser->position < parser->length) { + char c = advance(parser); + + if (c == '"') { + // End of string + buffer[length] = '\0'; + JsonValue* value = makeValue(JSON_STRING); + value->stringValue = buffer; + return value; + } + + if (c == '\\') { + // Escape sequence + char escaped = advance(parser); + switch (escaped) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'u': { + // Parse 4-digit hex code point + char hex[5] = {0}; + repeat(4, i) { + hex[i] = advance(parser); + } + unsigned long codePoint = strtoul(hex, nullptr, 16); + + // Encode as UTF-8 + if (128 > codePoint) { + c = (char) codePoint; + } else if (2048 > codePoint) { + if (length + 2 >= capacity) { + capacity *= 2; + buffer = safeRealloc(buffer, capacity); + } + buffer[length++] = (char) (0xC0 | (codePoint >> 6)); + c = (char) (0x80 | (codePoint & 0x3F)); + } else { + if (length + 3 >= capacity) { + capacity *= 2; + buffer = safeRealloc(buffer, capacity); + } + buffer[length++] = (char) (0xE0 | (codePoint >> 12)); + buffer[length++] = (char) (0x80 | ((codePoint >> 6) & 0x3F)); + c = (char) (0x80 | (codePoint & 0x3F)); + } + break; + } + default: + fprintf(stderr, "JsonReader: unknown escape sequence '\\%c'\n", escaped); + free(buffer); + return nullptr; + } + } + + // Grow buffer if needed + if (length + 1 >= capacity) { + capacity *= 2; + buffer = safeRealloc(buffer, capacity); + if (buffer == nullptr) { + fprintf(stderr, "JsonReader: realloc failed\n"); + abort(); + } + } + buffer[length++] = c; + } + + // Unterminated string + fprintf(stderr, "JsonReader: unterminated string\n"); + free(buffer); + return nullptr; +} + +static JsonValue* parseNumber(JsonParser* parser) { + const char* start = parser->input + parser->position; + char* end = nullptr; + double number = strtod(start, &end); + if (end == start) { + fprintf(stderr, "JsonReader: invalid number\n"); + return nullptr; + } + parser->position += (size_t) (end - start); + + JsonValue* value = makeValue(JSON_NUMBER); + value->numberValue = number; + return value; +} + +static JsonValue* parseArray(JsonParser* parser) { + // Skip opening bracket + advance(parser); + + JsonValue* value = makeValue(JSON_ARRAY); + value->array.items = nullptr; + value->array.count = 0; + value->array.capacity = 0; + + skipWhitespace(parser); + if (peek(parser) == ']') { + advance(parser); + return value; + } + + while (true) { + skipWhitespace(parser); + JsonValue* item = parseValue(parser); + if (item == nullptr) { + JsonReader_free(value); + return nullptr; + } + + // Grow items array if needed + if (value->array.count >= value->array.capacity) { + int newCapacity = (value->array.capacity == 0) ? 8 : value->array.capacity * 2; + value->array.items = safeRealloc(value->array.items, (size_t) newCapacity * sizeof(JsonValue)); + if (value->array.items == nullptr) { + fprintf(stderr, "JsonReader: realloc failed\n"); + abort(); + } + value->array.capacity = newCapacity; + } + + // Copy item into array and free the container + value->array.items[value->array.count++] = *item; + free(item); + + skipWhitespace(parser); + if (peek(parser) == ',') { + advance(parser); + } else if (peek(parser) == ']') { + advance(parser); + return value; + } else { + fprintf(stderr, "JsonReader: expected ',' or ']' in array\n"); + JsonReader_free(value); + return nullptr; + } + } +} + +static JsonValue* parseObject(JsonParser* parser) { + // Skip opening brace + advance(parser); + + JsonValue* value = makeValue(JSON_OBJECT); + value->object.keys = nullptr; + value->object.values = nullptr; + value->object.count = 0; + value->object.capacity = 0; + + skipWhitespace(parser); + if (peek(parser) == '}') { + advance(parser); + return value; + } + + while (true) { + skipWhitespace(parser); + if (peek(parser) != '"') { + fprintf(stderr, "JsonReader: expected string key in object\n"); + JsonReader_free(value); + return nullptr; + } + + JsonValue* keyValue = parseString(parser); + if (keyValue == nullptr) { + JsonReader_free(value); + return nullptr; + } + char* key = keyValue->stringValue; + // Free just the JsonValue container, keep the string + free(keyValue); + + skipWhitespace(parser); + if (peek(parser) != ':') { + fprintf(stderr, "JsonReader: expected ':' after object key\n"); + free(key); + JsonReader_free(value); + return nullptr; + } + advance(parser); + + skipWhitespace(parser); + JsonValue* itemValue = parseValue(parser); + if (itemValue == nullptr) { + free(key); + JsonReader_free(value); + return nullptr; + } + + // Grow arrays if needed + if (value->object.count >= value->object.capacity) { + int newCapacity = (value->object.capacity == 0) ? 8 : value->object.capacity * 2; + value->object.keys = safeRealloc(value->object.keys, (size_t) newCapacity * sizeof(char*)); + value->object.values = safeRealloc(value->object.values, (size_t) newCapacity * sizeof(JsonValue)); + if (value->object.keys == nullptr || value->object.values == nullptr) { + fprintf(stderr, "JsonReader: realloc failed\n"); + abort(); + } + value->object.capacity = newCapacity; + } + + value->object.keys[value->object.count] = key; + value->object.values[value->object.count] = *itemValue; + value->object.count++; + free(itemValue); + + skipWhitespace(parser); + if (peek(parser) == ',') { + advance(parser); + } else if (peek(parser) == '}') { + advance(parser); + return value; + } else { + fprintf(stderr, "JsonReader: expected ',' or '}' in object\n"); + JsonReader_free(value); + return nullptr; + } + } +} + +static JsonValue* parseLiteral(JsonParser* parser, const char* literal, size_t literalLen) { + if (parser->position + literalLen > parser->length) { + return nullptr; + } + if (memcmp(parser->input + parser->position, literal, literalLen) != 0) { + return nullptr; + } + parser->position += literalLen; + return makeValue(JSON_NULL); // Caller overrides type as needed +} + +static JsonValue* parseValue(JsonParser* parser) { + skipWhitespace(parser); + char c = peek(parser); + + switch (c) { + case '"': + return parseString(parser); + case '{': + return parseObject(parser); + case '[': + return parseArray(parser); + case 't': { + JsonValue* value = parseLiteral(parser, "true", 4); + if (value == nullptr) { + fprintf(stderr, "JsonReader: invalid literal\n"); + return nullptr; + } + value->type = JSON_BOOL; + value->boolValue = true; + return value; + } + case 'f': { + JsonValue* value = parseLiteral(parser, "false", 5); + if (value == nullptr) { + fprintf(stderr, "JsonReader: invalid literal\n"); + return nullptr; + } + value->type = JSON_BOOL; + value->boolValue = false; + return value; + } + case 'n': { + JsonValue* value = parseLiteral(parser, "null", 4); + if (value == nullptr) { + fprintf(stderr, "JsonReader: invalid literal\n"); + return nullptr; + } + return value; + } + default: + if (c == '-' || (c >= '0' && c <= '9')) { + return parseNumber(parser); + } + fprintf(stderr, "JsonReader: unexpected character '%c' at position %zu\n", c, parser->position); + return nullptr; + } +} + +// ===[ Lifecycle ]=== + +JsonValue* JsonReader_parse(const char* json) { + if (json == nullptr) return nullptr; + + JsonParser parser = { + .input = json, + .position = 0, + .length = strlen(json), + }; + + JsonValue* result = parseValue(&parser); + + // Check for trailing non-whitespace + if (result != nullptr) { + skipWhitespace(&parser); + if (parser.position < parser.length) { + fprintf(stderr, "JsonReader: trailing content after JSON value at position %zu\n", parser.position); + JsonReader_free(result); + return nullptr; + } + } + + return result; +} + +// Frees the contents of a JsonValue without freeing the JsonValue struct itself. +// Used for inline values (array items, object values stored by value). +static void freeContents(JsonValue* value) { + switch (value->type) { + case JSON_STRING: + free(value->stringValue); + break; + case JSON_ARRAY: + repeat(value->array.count, i) { + freeContents(&value->array.items[i]); + } + free(value->array.items); + break; + case JSON_OBJECT: + repeat(value->object.count, i) { + free(value->object.keys[i]); + freeContents(&value->object.values[i]); + } + free(value->object.keys); + free(value->object.values); + break; + default: + break; + } +} + +void JsonReader_free(JsonValue* value) { + if (value == nullptr) return; + freeContents(value); + free(value); +} + +// ===[ Type Checks ]=== + +bool JsonReader_isNull(const JsonValue* value) { + return value != nullptr && value->type == JSON_NULL; +} + +bool JsonReader_isBool(const JsonValue* value) { + return value != nullptr && value->type == JSON_BOOL; +} + +bool JsonReader_isNumber(const JsonValue* value) { + return value != nullptr && value->type == JSON_NUMBER; +} + +bool JsonReader_isString(const JsonValue* value) { + return value != nullptr && value->type == JSON_STRING; +} + +bool JsonReader_isArray(const JsonValue* value) { + return value != nullptr && value->type == JSON_ARRAY; +} + +bool JsonReader_isObject(const JsonValue* value) { + return value != nullptr && value->type == JSON_OBJECT; +} + +// ===[ Value Getters ]=== + +bool JsonReader_getBool(const JsonValue* value) { + return value->boolValue; +} + +double JsonReader_getDouble(const JsonValue* value) { + return value->numberValue; +} + +int64_t JsonReader_getInt(const JsonValue* value) { + return (int64_t) value->numberValue; +} + +const char* JsonReader_getString(const JsonValue* value) { + return value->stringValue; +} + +// ===[ Array Access ]=== + +int JsonReader_arrayLength(const JsonValue* value) { + return value->array.count; +} + +JsonValue* JsonReader_getArrayElement(const JsonValue* value, int index) { + if (0 > index || index >= value->array.count) return nullptr; + return &value->array.items[index]; +} + +// ===[ Array Bulk Read ]=== + +void JsonReader_readFloatArray(const JsonValue* value, float* out, int expectedLen) { + require(value != nullptr && value->type == JSON_ARRAY); + require(value->array.count == expectedLen); + repeat(expectedLen, i) { + out[i] = (float) value->array.items[i].numberValue; + } +} + +void JsonReader_readInt32Array(const JsonValue* value, int32_t* out, int expectedLen) { + require(value != nullptr && value->type == JSON_ARRAY); + require(value->array.count == expectedLen); + repeat(expectedLen, i) { + out[i] = (int32_t) value->array.items[i].numberValue; + } +} + +// ===[ Object Access ]=== + +int JsonReader_objectLength(const JsonValue* value) { + return value->object.count; +} + +JsonValue* JsonReader_getObject(const JsonValue* value, const char* key) { + repeat(value->object.count, i) { + if (strcmp(value->object.keys[i], key) == 0) { + return &value->object.values[i]; + } + } + return nullptr; +} + +const char* JsonReader_getObjectKey(const JsonValue* value, int index) { + if (0 > index || index >= value->object.count) return nullptr; + return value->object.keys[index]; +} + +JsonValue* JsonReader_getObjectValue(const JsonValue* value, int index) { + if (0 > index || index >= value->object.count) return nullptr; + return &value->object.values[index]; +} diff --git a/src/json_reader.h b/src/json_reader.h index bc5a58a2..84e11486 100644 --- a/src/json_reader.h +++ b/src/json_reader.h @@ -1,67 +1,67 @@ -#pragma once - -#include "common.h" -#include -#include - -// ===[ JsonValue Types ]=== - -typedef enum { - JSON_NULL, - JSON_BOOL, - JSON_NUMBER, - JSON_STRING, - JSON_ARRAY, - JSON_OBJECT, -} JsonValueType; - -typedef struct JsonValue { - JsonValueType type; - union { - bool boolValue; - double numberValue; - char* stringValue; - struct { struct JsonValue* items; int count; int capacity; } array; - struct { char** keys; struct JsonValue* values; int count; int capacity; } object; - }; -} JsonValue; - -// ===[ Lifecycle ]=== - -JsonValue* JsonReader_parse(const char* json); -void JsonReader_free(JsonValue* value); - -// ===[ Type Checks ]=== - -bool JsonReader_isNull(const JsonValue* value); -bool JsonReader_isBool(const JsonValue* value); -bool JsonReader_isNumber(const JsonValue* value); -bool JsonReader_isString(const JsonValue* value); -bool JsonReader_isArray(const JsonValue* value); -bool JsonReader_isObject(const JsonValue* value); - -// ===[ Value Getters ]=== - -bool JsonReader_getBool(const JsonValue* value); -double JsonReader_getDouble(const JsonValue* value); -int64_t JsonReader_getInt(const JsonValue* value); -const char* JsonReader_getString(const JsonValue* value); - -// ===[ Array Access ]=== - -int JsonReader_arrayLength(const JsonValue* value); -JsonValue* JsonReader_getArrayElement(const JsonValue* value, int index); - -// ===[ Array Bulk Read ]=== - -// Reads a JSON number array into a float C array. Asserts the array has exactly expectedLen elements. -void JsonReader_readFloatArray(const JsonValue* value, float* out, int expectedLen); -// Reads a JSON number array into an int32 C array. Asserts the array has exactly expectedLen elements. -void JsonReader_readInt32Array(const JsonValue* value, int32_t* out, int expectedLen); - -// ===[ Object Access ]=== - -int JsonReader_objectLength(const JsonValue* value); -JsonValue* JsonReader_getObject(const JsonValue* value, const char* key); -const char* JsonReader_getObjectKey(const JsonValue* value, int index); -JsonValue* JsonReader_getObjectValue(const JsonValue* value, int index); +#pragma once + +#include "common.h" +#include +#include + +// ===[ JsonValue Types ]=== + +typedef enum { + JSON_NULL, + JSON_BOOL, + JSON_NUMBER, + JSON_STRING, + JSON_ARRAY, + JSON_OBJECT, +} JsonValueType; + +typedef struct JsonValue { + JsonValueType type; + union { + bool boolValue; + double numberValue; + char* stringValue; + struct { struct JsonValue* items; int count; int capacity; } array; + struct { char** keys; struct JsonValue* values; int count; int capacity; } object; + }; +} JsonValue; + +// ===[ Lifecycle ]=== + +JsonValue* JsonReader_parse(const char* json); +void JsonReader_free(JsonValue* value); + +// ===[ Type Checks ]=== + +bool JsonReader_isNull(const JsonValue* value); +bool JsonReader_isBool(const JsonValue* value); +bool JsonReader_isNumber(const JsonValue* value); +bool JsonReader_isString(const JsonValue* value); +bool JsonReader_isArray(const JsonValue* value); +bool JsonReader_isObject(const JsonValue* value); + +// ===[ Value Getters ]=== + +bool JsonReader_getBool(const JsonValue* value); +double JsonReader_getDouble(const JsonValue* value); +int64_t JsonReader_getInt(const JsonValue* value); +const char* JsonReader_getString(const JsonValue* value); + +// ===[ Array Access ]=== + +int JsonReader_arrayLength(const JsonValue* value); +JsonValue* JsonReader_getArrayElement(const JsonValue* value, int index); + +// ===[ Array Bulk Read ]=== + +// Reads a JSON number array into a float C array. Asserts the array has exactly expectedLen elements. +void JsonReader_readFloatArray(const JsonValue* value, float* out, int expectedLen); +// Reads a JSON number array into an int32 C array. Asserts the array has exactly expectedLen elements. +void JsonReader_readInt32Array(const JsonValue* value, int32_t* out, int expectedLen); + +// ===[ Object Access ]=== + +int JsonReader_objectLength(const JsonValue* value); +JsonValue* JsonReader_getObject(const JsonValue* value, const char* key); +const char* JsonReader_getObjectKey(const JsonValue* value, int index); +JsonValue* JsonReader_getObjectValue(const JsonValue* value, int index); diff --git a/src/json_writer.c b/src/json_writer.c index 9fbafa57..41a4b2f8 100644 --- a/src/json_writer.c +++ b/src/json_writer.c @@ -1,161 +1,161 @@ -#include "json_writer.h" -#include "utils.h" - -#include -#include -#include - -// ===[ Internal Helpers ]=== - -static void writeCommaIfNeeded(JsonWriter* writer) { - if (writer->needsComma) { - StringBuilder_appendChar(&writer->out, ','); - } -} - -static void writeEscapedString(JsonWriter* writer, const char* str) { - StringBuilder_appendChar(&writer->out, '"'); - for (const char* p = str; *p != '\0'; p++) { - unsigned char c = (unsigned char) *p; - switch (c) { - case '"': StringBuilder_append(&writer->out, "\\\""); break; - case '\\': StringBuilder_append(&writer->out, "\\\\"); break; - case '\b': StringBuilder_append(&writer->out, "\\b"); break; - case '\f': StringBuilder_append(&writer->out, "\\f"); break; - case '\n': StringBuilder_append(&writer->out, "\\n"); break; - case '\r': StringBuilder_append(&writer->out, "\\r"); break; - case '\t': StringBuilder_append(&writer->out, "\\t"); break; - default: - if (32 > c) { - StringBuilder_appendFormat(&writer->out, "\\u%04x", c); - } else { - StringBuilder_appendChar(&writer->out, (char) c); - } - break; - } - } - StringBuilder_appendChar(&writer->out, '"'); -} - -// ===[ Lifecycle ]=== - -JsonWriter JsonWriter_create(void) { - return (JsonWriter) { - .out = StringBuilder_create(256), - .needsComma = false, - }; -} - -void JsonWriter_free(JsonWriter* writer) { - StringBuilder_free(&writer->out); -} - -// ===[ Structure ]=== - -void JsonWriter_beginObject(JsonWriter* writer) { - writeCommaIfNeeded(writer); - StringBuilder_appendChar(&writer->out, '{'); - writer->needsComma = false; -} - -void JsonWriter_endObject(JsonWriter* writer) { - StringBuilder_appendChar(&writer->out, '}'); - writer->needsComma = true; -} - -void JsonWriter_beginArray(JsonWriter* writer) { - writeCommaIfNeeded(writer); - StringBuilder_appendChar(&writer->out, '['); - writer->needsComma = false; -} - -void JsonWriter_endArray(JsonWriter* writer) { - StringBuilder_appendChar(&writer->out, ']'); - writer->needsComma = true; -} - -// ===[ Object Keys ]=== - -void JsonWriter_key(JsonWriter* writer, const char* key) { - writeCommaIfNeeded(writer); - writeEscapedString(writer, key); - StringBuilder_appendChar(&writer->out, ':'); - writer->needsComma = false; -} - -// ===[ Values ]=== - -void JsonWriter_string(JsonWriter* writer, const char* value) { - writeCommaIfNeeded(writer); - if (value == nullptr) { - StringBuilder_append(&writer->out, "null"); - } else { - writeEscapedString(writer, value); - } - writer->needsComma = true; -} - -void JsonWriter_int(JsonWriter* writer, int64_t value) { - writeCommaIfNeeded(writer); - StringBuilder_appendFormat(&writer->out, "%lld", (long long) value); - writer->needsComma = true; -} - -void JsonWriter_double(JsonWriter* writer, double value) { - writeCommaIfNeeded(writer); - StringBuilder_appendFormat(&writer->out, "%.17g", value); - writer->needsComma = true; -} - -void JsonWriter_bool(JsonWriter* writer, bool value) { - writeCommaIfNeeded(writer); - StringBuilder_append(&writer->out, value ? "true" : "false"); - writer->needsComma = true; -} - -void JsonWriter_null(JsonWriter* writer) { - writeCommaIfNeeded(writer); - StringBuilder_append(&writer->out, "null"); - writer->needsComma = true; -} - -// ===[ Property Convenience ]=== - -void JsonWriter_propertyString(JsonWriter* writer, const char* key, const char* value) { - JsonWriter_key(writer, key); - JsonWriter_string(writer, value); -} - -void JsonWriter_propertyInt(JsonWriter* writer, const char* key, int64_t value) { - JsonWriter_key(writer, key); - JsonWriter_int(writer, value); -} - -void JsonWriter_propertyDouble(JsonWriter* writer, const char* key, double value) { - JsonWriter_key(writer, key); - JsonWriter_double(writer, value); -} - -void JsonWriter_propertyBool(JsonWriter* writer, const char* key, bool value) { - JsonWriter_key(writer, key); - JsonWriter_bool(writer, value); -} - -void JsonWriter_propertyNull(JsonWriter* writer, const char* key) { - JsonWriter_key(writer, key); - JsonWriter_null(writer); -} - -// ===[ Output ]=== - -const char* JsonWriter_getOutput(const JsonWriter* writer) { - return StringBuilder_data(&writer->out); -} - -char* JsonWriter_copyOutput(const JsonWriter* writer) { - return safeStrdup(StringBuilder_data(&writer->out)); -} - -size_t JsonWriter_getLength(const JsonWriter* writer) { - return StringBuilder_length(&writer->out); -} +#include "json_writer.h" +#include "utils.h" + +#include +#include +#include + +// ===[ Internal Helpers ]=== + +static void writeCommaIfNeeded(JsonWriter* writer) { + if (writer->needsComma) { + StringBuilder_appendChar(&writer->out, ','); + } +} + +static void writeEscapedString(JsonWriter* writer, const char* str) { + StringBuilder_appendChar(&writer->out, '"'); + for (const char* p = str; *p != '\0'; p++) { + unsigned char c = (unsigned char) *p; + switch (c) { + case '"': StringBuilder_append(&writer->out, "\\\""); break; + case '\\': StringBuilder_append(&writer->out, "\\\\"); break; + case '\b': StringBuilder_append(&writer->out, "\\b"); break; + case '\f': StringBuilder_append(&writer->out, "\\f"); break; + case '\n': StringBuilder_append(&writer->out, "\\n"); break; + case '\r': StringBuilder_append(&writer->out, "\\r"); break; + case '\t': StringBuilder_append(&writer->out, "\\t"); break; + default: + if (32 > c) { + StringBuilder_appendFormat(&writer->out, "\\u%04x", c); + } else { + StringBuilder_appendChar(&writer->out, (char) c); + } + break; + } + } + StringBuilder_appendChar(&writer->out, '"'); +} + +// ===[ Lifecycle ]=== + +JsonWriter JsonWriter_create(void) { + return (JsonWriter) { + .out = StringBuilder_create(256), + .needsComma = false, + }; +} + +void JsonWriter_free(JsonWriter* writer) { + StringBuilder_free(&writer->out); +} + +// ===[ Structure ]=== + +void JsonWriter_beginObject(JsonWriter* writer) { + writeCommaIfNeeded(writer); + StringBuilder_appendChar(&writer->out, '{'); + writer->needsComma = false; +} + +void JsonWriter_endObject(JsonWriter* writer) { + StringBuilder_appendChar(&writer->out, '}'); + writer->needsComma = true; +} + +void JsonWriter_beginArray(JsonWriter* writer) { + writeCommaIfNeeded(writer); + StringBuilder_appendChar(&writer->out, '['); + writer->needsComma = false; +} + +void JsonWriter_endArray(JsonWriter* writer) { + StringBuilder_appendChar(&writer->out, ']'); + writer->needsComma = true; +} + +// ===[ Object Keys ]=== + +void JsonWriter_key(JsonWriter* writer, const char* key) { + writeCommaIfNeeded(writer); + writeEscapedString(writer, key); + StringBuilder_appendChar(&writer->out, ':'); + writer->needsComma = false; +} + +// ===[ Values ]=== + +void JsonWriter_string(JsonWriter* writer, const char* value) { + writeCommaIfNeeded(writer); + if (value == nullptr) { + StringBuilder_append(&writer->out, "null"); + } else { + writeEscapedString(writer, value); + } + writer->needsComma = true; +} + +void JsonWriter_int(JsonWriter* writer, int64_t value) { + writeCommaIfNeeded(writer); + StringBuilder_appendFormat(&writer->out, "%lld", (long long) value); + writer->needsComma = true; +} + +void JsonWriter_double(JsonWriter* writer, double value) { + writeCommaIfNeeded(writer); + StringBuilder_appendFormat(&writer->out, "%.17g", value); + writer->needsComma = true; +} + +void JsonWriter_bool(JsonWriter* writer, bool value) { + writeCommaIfNeeded(writer); + StringBuilder_append(&writer->out, value ? "true" : "false"); + writer->needsComma = true; +} + +void JsonWriter_null(JsonWriter* writer) { + writeCommaIfNeeded(writer); + StringBuilder_append(&writer->out, "null"); + writer->needsComma = true; +} + +// ===[ Property Convenience ]=== + +void JsonWriter_propertyString(JsonWriter* writer, const char* key, const char* value) { + JsonWriter_key(writer, key); + JsonWriter_string(writer, value); +} + +void JsonWriter_propertyInt(JsonWriter* writer, const char* key, int64_t value) { + JsonWriter_key(writer, key); + JsonWriter_int(writer, value); +} + +void JsonWriter_propertyDouble(JsonWriter* writer, const char* key, double value) { + JsonWriter_key(writer, key); + JsonWriter_double(writer, value); +} + +void JsonWriter_propertyBool(JsonWriter* writer, const char* key, bool value) { + JsonWriter_key(writer, key); + JsonWriter_bool(writer, value); +} + +void JsonWriter_propertyNull(JsonWriter* writer, const char* key) { + JsonWriter_key(writer, key); + JsonWriter_null(writer); +} + +// ===[ Output ]=== + +const char* JsonWriter_getOutput(const JsonWriter* writer) { + return StringBuilder_data(&writer->out); +} + +char* JsonWriter_copyOutput(const JsonWriter* writer) { + return safeStrdup(StringBuilder_data(&writer->out)); +} + +size_t JsonWriter_getLength(const JsonWriter* writer) { + return StringBuilder_length(&writer->out); +} diff --git a/src/json_writer.h b/src/json_writer.h index 905ec26e..6b8ee7cb 100644 --- a/src/json_writer.h +++ b/src/json_writer.h @@ -1,51 +1,51 @@ -#pragma once - -#include "common.h" -#include "string_builder.h" -#include -#include - -// ===[ JsonWriter Type ]=== - -typedef struct { - StringBuilder out; - bool needsComma; -} JsonWriter; - -// ===[ Lifecycle ]=== - -JsonWriter JsonWriter_create(void); -void JsonWriter_free(JsonWriter* writer); - -// ===[ Structure ]=== - -void JsonWriter_beginObject(JsonWriter* writer); -void JsonWriter_endObject(JsonWriter* writer); -void JsonWriter_beginArray(JsonWriter* writer); -void JsonWriter_endArray(JsonWriter* writer); - -// ===[ Object Keys ]=== - -void JsonWriter_key(JsonWriter* writer, const char* key); - -// ===[ Values ]=== - -void JsonWriter_string(JsonWriter* writer, const char* value); -void JsonWriter_int(JsonWriter* writer, int64_t value); -void JsonWriter_double(JsonWriter* writer, double value); -void JsonWriter_bool(JsonWriter* writer, bool value); -void JsonWriter_null(JsonWriter* writer); - -// ===[ Property Convenience ]=== - -void JsonWriter_propertyString(JsonWriter* writer, const char* key, const char* value); -void JsonWriter_propertyInt(JsonWriter* writer, const char* key, int64_t value); -void JsonWriter_propertyDouble(JsonWriter* writer, const char* key, double value); -void JsonWriter_propertyBool(JsonWriter* writer, const char* key, bool value); -void JsonWriter_propertyNull(JsonWriter* writer, const char* key); - -// ===[ Output ]=== - -const char* JsonWriter_getOutput(const JsonWriter* writer); -char* JsonWriter_copyOutput(const JsonWriter* writer); -size_t JsonWriter_getLength(const JsonWriter* writer); +#pragma once + +#include "common.h" +#include "string_builder.h" +#include +#include + +// ===[ JsonWriter Type ]=== + +typedef struct { + StringBuilder out; + bool needsComma; +} JsonWriter; + +// ===[ Lifecycle ]=== + +JsonWriter JsonWriter_create(void); +void JsonWriter_free(JsonWriter* writer); + +// ===[ Structure ]=== + +void JsonWriter_beginObject(JsonWriter* writer); +void JsonWriter_endObject(JsonWriter* writer); +void JsonWriter_beginArray(JsonWriter* writer); +void JsonWriter_endArray(JsonWriter* writer); + +// ===[ Object Keys ]=== + +void JsonWriter_key(JsonWriter* writer, const char* key); + +// ===[ Values ]=== + +void JsonWriter_string(JsonWriter* writer, const char* value); +void JsonWriter_int(JsonWriter* writer, int64_t value); +void JsonWriter_double(JsonWriter* writer, double value); +void JsonWriter_bool(JsonWriter* writer, bool value); +void JsonWriter_null(JsonWriter* writer); + +// ===[ Property Convenience ]=== + +void JsonWriter_propertyString(JsonWriter* writer, const char* key, const char* value); +void JsonWriter_propertyInt(JsonWriter* writer, const char* key, int64_t value); +void JsonWriter_propertyDouble(JsonWriter* writer, const char* key, double value); +void JsonWriter_propertyBool(JsonWriter* writer, const char* key, bool value); +void JsonWriter_propertyNull(JsonWriter* writer, const char* key); + +// ===[ Output ]=== + +const char* JsonWriter_getOutput(const JsonWriter* writer); +char* JsonWriter_copyOutput(const JsonWriter* writer); +size_t JsonWriter_getLength(const JsonWriter* writer); diff --git a/src/matrix_math.h b/src/matrix_math.h index defff8d4..5f893fad 100644 --- a/src/matrix_math.h +++ b/src/matrix_math.h @@ -1,152 +1,152 @@ -#pragma once - -#include "common.h" -#include -#include - -// ===[ Matrix4f Type ]=== - -// Column-major 4x4 matrix (OpenGL native layout) -// Layout: -// m[0] m[4] m[8] m[12] (tx) -// m[1] m[5] m[9] m[13] (ty) -// m[2] m[6] m[10] m[14] (tz) -// m[3] m[7] m[11] m[15] (1) -typedef struct { - float m[16]; // m[col*4 + row] -} Matrix4f; - -// ===[ Identity / Copy ]=== - -static Matrix4f* Matrix4f_identity(Matrix4f* dest) { - memset(dest->m, 0, sizeof(dest->m)); - dest->m[0] = 1.0f; - dest->m[5] = 1.0f; - dest->m[10] = 1.0f; - dest->m[15] = 1.0f; - return dest; -} - -static Matrix4f* Matrix4f_copy(Matrix4f* dest, const Matrix4f* src) { - memcpy(dest->m, src->m, sizeof(dest->m)); - return dest; -} - -// ===[ Multiply ]=== - -// dest = a * b (safe if dest aliases a or b) -static Matrix4f* Matrix4f_multiply(Matrix4f* dest, const Matrix4f* a, const Matrix4f* b) { - float tmp[16]; - for (int col = 0; 4 > col; col++) { - for (int row = 0; 4 > row; row++) { - tmp[col * 4 + row] = - a->m[0 * 4 + row] * b->m[col * 4 + 0] + - a->m[1 * 4 + row] * b->m[col * 4 + 1] + - a->m[2 * 4 + row] * b->m[col * 4 + 2] + - a->m[3 * 4 + row] * b->m[col * 4 + 3]; - } - } - memcpy(dest->m, tmp, sizeof(tmp)); - return dest; -} - -// ===[ Orthographic Projection ]=== - -// Post-multiply orthographic projection onto dest: dest = dest * ortho(l, r, b, t, n, f) -static Matrix4f* Matrix4f_ortho(Matrix4f* dest, float left, float right, float bottom, float top, float near, float far) { - Matrix4f ortho; - memset(ortho.m, 0, sizeof(ortho.m)); - ortho.m[0] = 2.0f / (right - left); - ortho.m[5] = 2.0f / (top - bottom); - ortho.m[10] = -2.0f / (far - near); - ortho.m[12] = -(right + left) / (right - left); - ortho.m[13] = -(top + bottom) / (top - bottom); - ortho.m[14] = -(far + near) / (far - near); - ortho.m[15] = 1.0f; - return Matrix4f_multiply(dest, dest, &ortho); -} - -// ===[ Translate ]=== - -// Post-multiply translation onto dest: dest = dest * T(x, y, z) -// Optimized: only column 3 changes when post-multiplying a translation matrix -static Matrix4f* Matrix4f_translate(Matrix4f* dest, float x, float y, float z) { - dest->m[12] += dest->m[0] * x + dest->m[4] * y + dest->m[8] * z; - dest->m[13] += dest->m[1] * x + dest->m[5] * y + dest->m[9] * z; - dest->m[14] += dest->m[2] * x + dest->m[6] * y + dest->m[10] * z; - dest->m[15] += dest->m[3] * x + dest->m[7] * y + dest->m[11] * z; - return dest; -} - -// ===[ Rotate Z ]=== - -// Post-multiply Z-axis rotation onto dest: dest = dest * Rz(angleRadians) -static Matrix4f* Matrix4f_rotateZ(Matrix4f* dest, float angleRadians) { - float c = cosf(angleRadians); - float s = sinf(angleRadians); - // Columns 0 and 1 are affected: new_col0 = col0*c + col1*s, new_col1 = col0*(-s) + col1*c - for (int row = 0; 4 > row; row++) { - float a0 = dest->m[0 * 4 + row]; - float a1 = dest->m[1 * 4 + row]; - dest->m[0 * 4 + row] = a0 * c + a1 * s; - dest->m[1 * 4 + row] = a0 * (-s) + a1 * c; - } - return dest; -} - -// ===[ Scale ]=== - -// Post-multiply scale onto dest: dest = dest * S(sx, sy, sz) -// Optimized: scales each column directly -static Matrix4f* Matrix4f_scale(Matrix4f* dest, float sx, float sy, float sz) { - for (int row = 0; 4 > row; row++) { - dest->m[0 * 4 + row] *= sx; - dest->m[1 * 4 + row] *= sy; - dest->m[2 * 4 + row] *= sz; - } - return dest; -} - -// ===[ Set Transform 2D ]=== - -// Directly sets dest to a combined translate * rotateZ * scale matrix (no post-multiply) -// Equivalent to: identity -> translate(x, y, 0) -> rotateZ(angleRad) -> scale(sx, sy, 1) -static Matrix4f* Matrix4f_setTransform2D(Matrix4f* dest, float x, float y, float sx, float sy, float angleRad) { - float c = cosf(angleRad); - float s = sinf(angleRad); - - // Column 0: rotated+scaled X axis - dest->m[0] = c * sx; - dest->m[1] = s * sx; - dest->m[2] = 0.0f; - dest->m[3] = 0.0f; - - // Column 1: rotated+scaled Y axis - dest->m[4] = -s * sy; - dest->m[5] = c * sy; - dest->m[6] = 0.0f; - dest->m[7] = 0.0f; - - // Column 2: Z axis (identity for 2D) - dest->m[8] = 0.0f; - dest->m[9] = 0.0f; - dest->m[10] = 1.0f; - dest->m[11] = 0.0f; - - // Column 3: translation - dest->m[12] = x; - dest->m[13] = y; - dest->m[14] = 0.0f; - dest->m[15] = 1.0f; - - return dest; -} - -// ===[ Transform Point ]=== - -// Transform a 2D point (x, y) through the matrix (w=1), writing results to outX, outY -// Useful for CPU-side vertex transforms (e.g. PS2/gsKit software rendering) -static void Matrix4f_transformPoint(const Matrix4f* mat, float x, float y, float* outX, float* outY) { - *outX = mat->m[0] * x + mat->m[4] * y + mat->m[12]; - *outY = mat->m[1] * x + mat->m[5] * y + mat->m[13]; -} +#pragma once + +#include "common.h" +#include +#include + +// ===[ Matrix4f Type ]=== + +// Column-major 4x4 matrix (OpenGL native layout) +// Layout: +// m[0] m[4] m[8] m[12] (tx) +// m[1] m[5] m[9] m[13] (ty) +// m[2] m[6] m[10] m[14] (tz) +// m[3] m[7] m[11] m[15] (1) +typedef struct { + float m[16]; // m[col*4 + row] +} Matrix4f; + +// ===[ Identity / Copy ]=== + +static Matrix4f* Matrix4f_identity(Matrix4f* dest) { + memset(dest->m, 0, sizeof(dest->m)); + dest->m[0] = 1.0f; + dest->m[5] = 1.0f; + dest->m[10] = 1.0f; + dest->m[15] = 1.0f; + return dest; +} + +static Matrix4f* Matrix4f_copy(Matrix4f* dest, const Matrix4f* src) { + memcpy(dest->m, src->m, sizeof(dest->m)); + return dest; +} + +// ===[ Multiply ]=== + +// dest = a * b (safe if dest aliases a or b) +static Matrix4f* Matrix4f_multiply(Matrix4f* dest, const Matrix4f* a, const Matrix4f* b) { + float tmp[16]; + for (int col = 0; 4 > col; col++) { + for (int row = 0; 4 > row; row++) { + tmp[col * 4 + row] = + a->m[0 * 4 + row] * b->m[col * 4 + 0] + + a->m[1 * 4 + row] * b->m[col * 4 + 1] + + a->m[2 * 4 + row] * b->m[col * 4 + 2] + + a->m[3 * 4 + row] * b->m[col * 4 + 3]; + } + } + memcpy(dest->m, tmp, sizeof(tmp)); + return dest; +} + +// ===[ Orthographic Projection ]=== + +// Post-multiply orthographic projection onto dest: dest = dest * ortho(l, r, b, t, n, f) +static Matrix4f* Matrix4f_ortho(Matrix4f* dest, float left, float right, float bottom, float top, float near, float far) { + Matrix4f ortho; + memset(ortho.m, 0, sizeof(ortho.m)); + ortho.m[0] = 2.0f / (right - left); + ortho.m[5] = 2.0f / (top - bottom); + ortho.m[10] = -2.0f / (far - near); + ortho.m[12] = -(right + left) / (right - left); + ortho.m[13] = -(top + bottom) / (top - bottom); + ortho.m[14] = -(far + near) / (far - near); + ortho.m[15] = 1.0f; + return Matrix4f_multiply(dest, dest, &ortho); +} + +// ===[ Translate ]=== + +// Post-multiply translation onto dest: dest = dest * T(x, y, z) +// Optimized: only column 3 changes when post-multiplying a translation matrix +static Matrix4f* Matrix4f_translate(Matrix4f* dest, float x, float y, float z) { + dest->m[12] += dest->m[0] * x + dest->m[4] * y + dest->m[8] * z; + dest->m[13] += dest->m[1] * x + dest->m[5] * y + dest->m[9] * z; + dest->m[14] += dest->m[2] * x + dest->m[6] * y + dest->m[10] * z; + dest->m[15] += dest->m[3] * x + dest->m[7] * y + dest->m[11] * z; + return dest; +} + +// ===[ Rotate Z ]=== + +// Post-multiply Z-axis rotation onto dest: dest = dest * Rz(angleRadians) +static Matrix4f* Matrix4f_rotateZ(Matrix4f* dest, float angleRadians) { + float c = cosf(angleRadians); + float s = sinf(angleRadians); + // Columns 0 and 1 are affected: new_col0 = col0*c + col1*s, new_col1 = col0*(-s) + col1*c + for (int row = 0; 4 > row; row++) { + float a0 = dest->m[0 * 4 + row]; + float a1 = dest->m[1 * 4 + row]; + dest->m[0 * 4 + row] = a0 * c + a1 * s; + dest->m[1 * 4 + row] = a0 * (-s) + a1 * c; + } + return dest; +} + +// ===[ Scale ]=== + +// Post-multiply scale onto dest: dest = dest * S(sx, sy, sz) +// Optimized: scales each column directly +static Matrix4f* Matrix4f_scale(Matrix4f* dest, float sx, float sy, float sz) { + for (int row = 0; 4 > row; row++) { + dest->m[0 * 4 + row] *= sx; + dest->m[1 * 4 + row] *= sy; + dest->m[2 * 4 + row] *= sz; + } + return dest; +} + +// ===[ Set Transform 2D ]=== + +// Directly sets dest to a combined translate * rotateZ * scale matrix (no post-multiply) +// Equivalent to: identity -> translate(x, y, 0) -> rotateZ(angleRad) -> scale(sx, sy, 1) +static Matrix4f* Matrix4f_setTransform2D(Matrix4f* dest, float x, float y, float sx, float sy, float angleRad) { + float c = cosf(angleRad); + float s = sinf(angleRad); + + // Column 0: rotated+scaled X axis + dest->m[0] = c * sx; + dest->m[1] = s * sx; + dest->m[2] = 0.0f; + dest->m[3] = 0.0f; + + // Column 1: rotated+scaled Y axis + dest->m[4] = -s * sy; + dest->m[5] = c * sy; + dest->m[6] = 0.0f; + dest->m[7] = 0.0f; + + // Column 2: Z axis (identity for 2D) + dest->m[8] = 0.0f; + dest->m[9] = 0.0f; + dest->m[10] = 1.0f; + dest->m[11] = 0.0f; + + // Column 3: translation + dest->m[12] = x; + dest->m[13] = y; + dest->m[14] = 0.0f; + dest->m[15] = 1.0f; + + return dest; +} + +// ===[ Transform Point ]=== + +// Transform a 2D point (x, y) through the matrix (w=1), writing results to outX, outY +// Useful for CPU-side vertex transforms (e.g. PS2/gsKit software rendering) +static void Matrix4f_transformPoint(const Matrix4f* mat, float x, float y, float* outX, float* outY) { + *outX = mat->m[0] * x + mat->m[4] * y + mat->m[12]; + *outY = mat->m[1] * x + mat->m[5] * y + mat->m[13]; +} diff --git a/src/n3ds/main.c b/src/n3ds/main.c new file mode 100644 index 00000000..2fb74a70 --- /dev/null +++ b/src/n3ds/main.c @@ -0,0 +1,874 @@ +#include "../data_win.h" +#include "../runner.h" +#include "../runner_keyboard.h" +#include "../utils.h" +#include "../vm.h" + +#include "n3ds_audio_system.h" +#include "n3ds_file_system.h" +#include "n3ds_renderer.h" + +#include <3ds.h> +#include + +#include +#include +#include +#include +#include +#include + +#define N3DS_LOADING_TEXT_SCALE 0.42f +#define N3DS_BOOT_LOG_MAX_LINES 6 +#define N3DS_BOOT_LOG_LINE_CHARS 88 +#define N3DS_TOTAL_VRAM_BYTES (6u * 1024u * 1024u) +#define N3DS_DEBUG_ASRIEL_ROOM 330 + +void N3DS_tryTriggerAsrielLed(Runner* runner); + +typedef struct { + bool useCitro2D; + C3D_RenderTarget* target; + C2D_TextBuf textBuf; + PrintConsole console; + char statusLine[128]; + int chunkIndex; + int totalChunks; +} N3DSLoadingScreen; + +static char gN3DSBootLogLines[N3DS_BOOT_LOG_MAX_LINES][N3DS_BOOT_LOG_LINE_CHARS]; +static int gN3DSBootLogLineCount = 0; + +typedef struct { + C2D_TextBuf textBuf; + double displayedFps; + double displayedRenderMs; + double sampledRenderMs; + uint32_t sampledFrames; + u64 sampleStartMs; +} N3DSDebugMonitor; + +static bool fileExists(const char* path) { + struct stat st; + return stat(path, &st) == 0; +} + +static void N3DS_formatDebugSize(char* out, size_t outSize, uint32_t bytes) { + if (out == NULL || outSize == 0) return; + + double kib = (double) bytes / 1024.0; + if (kib < 1024.0) { + snprintf(out, outSize, "%.0f KB", kib); + return; + } + + snprintf(out, outSize, "%.2f MB", kib / 1024.0); +} + +static void N3DSDebugMonitor_init(N3DSDebugMonitor* monitor) { + if (monitor == NULL) return; + memset(monitor, 0, sizeof(*monitor)); + monitor->textBuf = C2D_TextBufNew(1024); + monitor->sampleStartMs = osGetTime(); +} + +static void N3DSDebugMonitor_free(N3DSDebugMonitor* monitor) { + if (monitor == NULL) return; + if (monitor->textBuf != NULL) { + C2D_TextBufDelete(monitor->textBuf); + monitor->textBuf = NULL; + } +} + +static void N3DSDebugMonitor_tickFrame(N3DSDebugMonitor* monitor, double renderMs) { + if (monitor == NULL) return; + + monitor->sampledFrames++; + monitor->sampledRenderMs += renderMs; + u64 nowMs = osGetTime(); + u64 elapsedMs = nowMs - monitor->sampleStartMs; + if (elapsedMs < 250) return; + + monitor->displayedFps = ((double) monitor->sampledFrames * 1000.0) / (double) elapsedMs; + monitor->displayedRenderMs = monitor->sampledFrames > 0 + ? monitor->sampledRenderMs / (double) monitor->sampledFrames + : 0.0; + monitor->sampledFrames = 0; + monitor->sampledRenderMs = 0.0; + monitor->sampleStartMs = nowMs; +} + +//font used in godmode9 that's easy to read. got lazy and just embedded the pbm bytes directly here + +static uint8_t N3DSDebugTinyFont_getRow(char c, uint32_t row) { + static const uint8_t font[95][10] = { + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00 }, + { 0x00, 0x0A, 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A, 0x00, 0x00 }, + { 0x00, 0x04, 0x0E, 0x15, 0x0C, 0x06, 0x15, 0x0E, 0x04, 0x00 }, + { 0x00, 0x19, 0x19, 0x02, 0x04, 0x08, 0x13, 0x13, 0x00, 0x00 }, + { 0x00, 0x04, 0x0A, 0x04, 0x09, 0x15, 0x12, 0x0D, 0x00, 0x00 }, + { 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x02, 0x04, 0x04, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00 }, + { 0x00, 0x08, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x00, 0x00 }, + { 0x00, 0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x18, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00 }, + { 0x00, 0x02, 0x02, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x00 }, + { 0x00, 0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x1F, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02, 0x00, 0x00 }, + { 0x00, 0x1F, 0x10, 0x10, 0x1E, 0x01, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x1F, 0x01, 0x02, 0x02, 0x04, 0x04, 0x04, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x18, 0x00 }, + { 0x00, 0x01, 0x02, 0x04, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x13, 0x15, 0x17, 0x10, 0x0F, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00, 0x00 }, + { 0x00, 0x1E, 0x09, 0x09, 0x0E, 0x09, 0x09, 0x1E, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x1E, 0x09, 0x09, 0x09, 0x09, 0x09, 0x1E, 0x00, 0x00 }, + { 0x00, 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F, 0x00, 0x00 }, + { 0x00, 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10, 0x00, 0x00 }, + { 0x00, 0x0F, 0x10, 0x10, 0x13, 0x11, 0x11, 0x0F, 0x00, 0x00 }, + { 0x00, 0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00, 0x00 }, + { 0x00, 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x1F, 0x00, 0x00 }, + { 0x00, 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x18, 0x00, 0x00 }, + { 0x00, 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00, 0x00 }, + { 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F, 0x00, 0x00 }, + { 0x00, 0x11, 0x1B, 0x15, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00 }, + { 0x00, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D, 0x00, 0x00 }, + { 0x00, 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x11, 0x00, 0x00 }, + { 0x00, 0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00 }, + { 0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00, 0x00 }, + { 0x00, 0x11, 0x11, 0x11, 0x11, 0x15, 0x15, 0x0A, 0x00, 0x00 }, + { 0x00, 0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11, 0x00, 0x00 }, + { 0x00, 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00 }, + { 0x00, 0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F, 0x00, 0x00 }, + { 0x00, 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x06, 0x00, 0x00 }, + { 0x00, 0x10, 0x10, 0x08, 0x08, 0x04, 0x04, 0x02, 0x02, 0x00 }, + { 0x00, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0C, 0x00, 0x00 }, + { 0x00, 0x04, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00 }, + { 0x00, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F, 0x00, 0x00 }, + { 0x00, 0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x1E, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x0E, 0x11, 0x10, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x01, 0x01, 0x0F, 0x11, 0x11, 0x13, 0x0D, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0F, 0x00, 0x00 }, + { 0x00, 0x06, 0x09, 0x08, 0x1E, 0x08, 0x08, 0x08, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x0D, 0x13, 0x11, 0x11, 0x0F, 0x01, 0x1E }, + { 0x00, 0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00, 0x00 }, + { 0x00, 0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x1F, 0x00, 0x00 }, + { 0x00, 0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x18 }, + { 0x00, 0x10, 0x10, 0x12, 0x14, 0x1C, 0x12, 0x11, 0x00, 0x00 }, + { 0x00, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x06, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x1A, 0x15, 0x15, 0x15, 0x15, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x1E, 0x10, 0x10 }, + { 0x00, 0x00, 0x00, 0x0F, 0x11, 0x11, 0x13, 0x0D, 0x01, 0x01 }, + { 0x00, 0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E, 0x00, 0x00 }, + { 0x00, 0x04, 0x04, 0x0E, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0F, 0x01, 0x1E }, + { 0x00, 0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F, 0x00, 0x00 }, + { 0x00, 0x02, 0x04, 0x04, 0x08, 0x04, 0x04, 0x02, 0x00, 0x00 }, + { 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 }, + { 0x00, 0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x08, 0x15, 0x02, 0x00, 0x00, 0x00, 0x00 }, + }; + + if (row >= 10u || c < 32 || c > 126) return 0; + return font[(uint8_t) c - 32u][row]; +} + +static void N3DSDebugTinyFont_drawText(const char* text, float x, float y, uint32_t color) { + if (text == NULL) return; + + const float pixel = 1.0f; + const float charAdvance = 6.0f; + for (const char* cursor = text; *cursor != '\0'; ++cursor) { + if (*cursor == ' ') { + x += charAdvance; + continue; + } + + repeat(10u, row) { + uint8_t bits = N3DSDebugTinyFont_getRow(*cursor, (uint32_t) row); + repeat(6u, col) { + if ((bits & (uint8_t) (1u << (5u - col))) == 0u) continue; + C2D_DrawRectSolid(x + (float) col * pixel, y + (float) row * pixel, 0.0f, pixel, pixel, color); + } + } + x += charAdvance; + } +} + +static void N3DSDebugMonitor_draw(N3DSDebugMonitor* monitor, Runner* runner, Renderer* renderer) { +#ifdef N3DS_DISABLE_BOTTOM_SCREEN + (void) monitor; + (void) runner; + (void) renderer; + return; +#endif + if (monitor == NULL || runner == NULL || renderer == NULL || monitor->textBuf == NULL) return; + + const char* roomName = "(none)"; + if (runner->currentRoom != NULL && runner->currentRoom->name != NULL && runner->currentRoom->name[0] != '\0') { + roomName = runner->currentRoom->name; + } + + uint32_t atlasVRAMBytes = N3DSRenderer_getResidentAtlasVRAMBytes(renderer); + uint32_t atlasVRAMLimitBytes = N3DSRenderer_getResidentAtlasVRAMLimitBytes(renderer); + uint32_t atlasPageCount = N3DSRenderer_getResidentAtlasPageCount(renderer); + uint32_t atlasPageLimit = N3DSRenderer_getResidentAtlasPageLimit(renderer); + uint32_t directVRAMBytes = N3DSRenderer_getResidentDirectAssetVRAMBytes(renderer); + uint32_t trackedVRAMBytes = atlasVRAMBytes + directVRAMBytes; + uint32_t ramFreeBytes = osGetMemRegionFree(MEMREGION_APPLICATION); + uint32_t ramTotalBytes = osGetMemRegionSize(MEMREGION_APPLICATION); + uint32_t linearFreeBytes = linearSpaceFree(); + + char fpsLine[96]; + char vramLine[64]; + char atlasLine[96]; + char ramLine[96]; + char audioLine[96]; + char roomLine[96]; + char vramUsed[24]; + char vramTotal[24]; + char atlasUsed[24]; + char atlasLimit[24]; + char directUsed[24]; + char ramFree[24]; + char ramTotal[24]; + char linearFree[24]; + char audioCached[24]; + char audioLimit[24]; + uint32_t cachedSounds = 0; + uint32_t totalSounds = 0; + uint32_t cachedSoundBytes = 0; + uint32_t cacheLimitBytes = 0; + + N3DSAudioSystem_getCacheStats(runner->audioSystem, &cachedSounds, &totalSounds, &cachedSoundBytes, &cacheLimitBytes); + + snprintf( + fpsLine, + sizeof(fpsLine), + "FPS %.1f R %.1fms", + monitor->displayedFps > 0.0 ? monitor->displayedFps : 0.0, + monitor->displayedRenderMs > 0.0 ? monitor->displayedRenderMs : 0.0 + ); + N3DS_formatDebugSize(vramUsed, sizeof(vramUsed), trackedVRAMBytes); + N3DS_formatDebugSize(vramTotal, sizeof(vramTotal), N3DS_TOTAL_VRAM_BYTES); + N3DS_formatDebugSize(atlasUsed, sizeof(atlasUsed), atlasVRAMBytes); + N3DS_formatDebugSize(atlasLimit, sizeof(atlasLimit), atlasVRAMLimitBytes); + N3DS_formatDebugSize(directUsed, sizeof(directUsed), directVRAMBytes); + N3DS_formatDebugSize(ramFree, sizeof(ramFree), ramFreeBytes); + N3DS_formatDebugSize(ramTotal, sizeof(ramTotal), ramTotalBytes); + N3DS_formatDebugSize(linearFree, sizeof(linearFree), linearFreeBytes); + N3DS_formatDebugSize(audioCached, sizeof(audioCached), cachedSoundBytes); + N3DS_formatDebugSize(audioLimit, sizeof(audioLimit), cacheLimitBytes); + snprintf(vramLine, sizeof(vramLine), "VRAM %s/%s", + vramUsed, + vramTotal); + snprintf(atlasLine, sizeof(atlasLine), "AT %lu/%lu %s DR %s", + (unsigned long) atlasPageCount, + (unsigned long) atlasPageLimit, + atlasUsed, + directUsed); + snprintf(ramLine, sizeof(ramLine), "RAM %s/%s L %s", + ramFree, + ramTotal, + linearFree); + snprintf(audioLine, sizeof(audioLine), "AUD %lu/%lu %s/%s", + (unsigned long) cachedSounds, + (unsigned long) totalSounds, + audioCached, + audioLimit); + snprintf(roomLine, sizeof(roomLine), "R %.28s", roomName); + + N3DSRenderer_beginBottomScreenGUI(renderer, 320, 240); + const float boxX = 92.0f; + const float boxY = 8.0f; + const float boxW = 220.0f; + const float boxH = 110.0f; + const float textX = boxX + 7.0f; + C2D_DrawRectSolid(boxX, boxY, 0.0f, boxW, boxH, C2D_Color32(8, 10, 16, 218)); + C2D_DrawRectSolid(boxX, boxY, 0.0f, boxW, 2.0f, C2D_Color32(77, 118, 255, 245)); + C2D_DrawRectSolid(boxX, boxY + boxH - 1.0f, 0.0f, boxW, 1.0f, C2D_Color32(32, 46, 78, 230)); + N3DSDebugTinyFont_drawText("DBG", textX, boxY + 7.0f, C2D_Color32(255, 255, 255, 255)); + N3DSDebugTinyFont_drawText(fpsLine, boxX + 42.0f, boxY + 7.0f, C2D_Color32(196, 230, 255, 255)); + N3DSDebugTinyFont_drawText(vramLine, textX, boxY + 25.0f, C2D_Color32(196, 230, 255, 255)); + N3DSDebugTinyFont_drawText(atlasLine, textX, boxY + 42.0f, C2D_Color32(146, 188, 236, 255)); + N3DSDebugTinyFont_drawText(ramLine, textX, boxY + 59.0f, C2D_Color32(196, 230, 255, 255)); + N3DSDebugTinyFont_drawText(audioLine, textX, boxY + 76.0f, C2D_Color32(196, 255, 196, 255)); + N3DSDebugTinyFont_drawText(roomLine, textX, boxY + 93.0f, C2D_Color32(255, 230, 163, 255)); + N3DSRenderer_endBottomScreenGUI(renderer); +} + +static bool N3DS_stringStartsWithIgnoreCase(const char* value, const char* prefix) { + if (value == NULL || prefix == NULL) return false; + while (*prefix != '\0') { + if (*value == '\0') return false; + if (tolower((unsigned char) *value) != tolower((unsigned char) *prefix)) return false; + ++value; + ++prefix; + } + return true; +} + +static const char* N3DS_getRoomBorderAssetName(const Room* room) { + const char* roomName = room != NULL ? room->name : NULL; + if (roomName == NULL || roomName[0] == '\0') return "border_none"; + + if (N3DS_stringStartsWithIgnoreCase(roomName, "room_gaster") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_mysteryman")) return "room_gaster"; + + if (N3DS_stringStartsWithIgnoreCase(roomName, "room_truelab")) return "room_truelab"; + + if (N3DS_stringStartsWithIgnoreCase(roomName, "room_castle") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_asghouse") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_asgoreroom") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_lastruins_corridor") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_sanscorridor") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_undertale_end")) return "room_castle"; + + if (N3DS_stringStartsWithIgnoreCase(roomName, "room_fire")) return "room_fire"; + + if (N3DS_stringStartsWithIgnoreCase(roomName, "room_water")) return "room_water"; + + if (N3DS_stringStartsWithIgnoreCase(roomName, "room_tundra") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_ice") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_fogroom")) return "room_tundra"; + + if (N3DS_stringStartsWithIgnoreCase(roomName, "room_ruins") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_area1") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_torhouse") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_torielroom") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_asrielroom") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_kitchen") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_basement") || + N3DS_stringStartsWithIgnoreCase(roomName, "room_ruinsexit")) return "room_ruins"; + + return "border_none"; +} + +static bool N3DS_tryLoadRoomBorderSprite(const char* assetName, C2D_Sprite* outSprite, C2D_SpriteSheet* outSheet) { + if (assetName == NULL || outSprite == NULL || outSheet == NULL) return false; + + char candidatePaths[2][256]; + snprintf(candidatePaths[0], sizeof(candidatePaths[0]), "sdmc:/3ds/cinnamon/gfx/borders/%s.t3x", assetName); + snprintf(candidatePaths[1], sizeof(candidatePaths[1]), "romfs:/gfx/borders/%s.t3x", assetName); + + repeat(2, i) { + const char* path = candidatePaths[i]; + C2D_SpriteSheet sheet = C2D_SpriteSheetLoad(path); + if (sheet == NULL) continue; + if (C2D_SpriteSheetCount(sheet) <= 0) { + C2D_SpriteSheetFree(sheet); + continue; + } + + *outSheet = sheet; + C2D_SpriteFromSheet(outSprite, sheet, 0); + C2D_SpriteSetPos(outSprite, 0.0f, 0.0f); + C2D_SpriteSetDepth(outSprite, 1.0f); + return true; + } + + return false; +} + +static bool N3DS_refreshRoomBorderSprite( + const Room* room, + C2D_Sprite* borderSprite, + C2D_SpriteSheet* borderSheet, + bool* haveRoomBorder, + char* loadedBorderAssetName, + size_t loadedBorderAssetNameSize +) { + if (borderSprite == NULL || borderSheet == NULL || haveRoomBorder == NULL || + loadedBorderAssetName == NULL || loadedBorderAssetNameSize == 0) { + return false; + } + + const char* desiredAssetName = N3DS_getRoomBorderAssetName(room); + if (strcmp(loadedBorderAssetName, desiredAssetName) == 0) return *haveRoomBorder; + + if (*borderSheet != NULL) { + C2D_SpriteSheetFree(*borderSheet); + *borderSheet = NULL; + } + + memset(borderSprite, 0, sizeof(*borderSprite)); + *haveRoomBorder = N3DS_tryLoadRoomBorderSprite(desiredAssetName, borderSprite, borderSheet); + if (!*haveRoomBorder && strcmp(desiredAssetName, "border_none") != 0) { + *haveRoomBorder = N3DS_tryLoadRoomBorderSprite("border_none", borderSprite, borderSheet); + snprintf(loadedBorderAssetName, loadedBorderAssetNameSize, "%s", *haveRoomBorder ? "border_none" : desiredAssetName); + return *haveRoomBorder; + } + + snprintf(loadedBorderAssetName, loadedBorderAssetNameSize, "%s", desiredAssetName); + return *haveRoomBorder; +} + +static void N3DS_drawFallbackRoomBorder(void) { + const float screenW = 400.0f; + const float screenH = 240.0f; + const float outer = 6.0f; + const float inner = 3.0f; + const uint32_t outerColor = C2D_Color32(24, 18, 8, 255); + const uint32_t trimColor = C2D_Color32(208, 170, 84, 255); + const uint32_t highlightColor = C2D_Color32(255, 232, 170, 255); + + const float overlayDepth = 1.0f; + + C2D_DrawRectSolid(0.0f, 0.0f, overlayDepth, screenW, outer, outerColor); + C2D_DrawRectSolid(0.0f, screenH - outer, overlayDepth, screenW, outer, outerColor); + C2D_DrawRectSolid(0.0f, outer, overlayDepth, outer, screenH - outer * 2.0f, outerColor); + C2D_DrawRectSolid(screenW - outer, outer, overlayDepth, outer, screenH - outer * 2.0f, outerColor); + + C2D_DrawRectSolid(outer, outer, overlayDepth, screenW - outer * 2.0f, inner, trimColor); + C2D_DrawRectSolid(outer, screenH - outer - inner, overlayDepth, screenW - outer * 2.0f, inner, trimColor); + C2D_DrawRectSolid(outer, outer + inner, overlayDepth, inner, screenH - (outer + inner) * 2.0f, trimColor); + C2D_DrawRectSolid(screenW - outer - inner, outer + inner, overlayDepth, inner, screenH - (outer + inner) * 2.0f, trimColor); + + C2D_DrawRectSolid(outer + inner, outer + inner, overlayDepth, screenW - (outer + inner) * 2.0f, 1.0f, highlightColor); + C2D_DrawRectSolid(outer + inner, screenH - outer - inner - 1.0f, overlayDepth, screenW - (outer + inner) * 2.0f, 1.0f, highlightColor); +} + +static char* chooseDataWinPath(void) { + if (fileExists("romfs:/data.win")) return safeStrdup("romfs:/data.win"); + return safeStrdup("sdmc:/3ds/cinnamon/data.win"); +} + +static void syncKey(RunnerKeyboardState* keyboard, bool* state, int32_t key, bool held) { + if (held && !*state) RunnerKeyboard_onKeyDown(keyboard, key); + if (!held && *state) RunnerKeyboard_onKeyUp(keyboard, key); + *state = held; +} + +static void N3DSLoadingScreen_free(N3DSLoadingScreen* screen) { + if (screen == NULL) return; + if (screen->textBuf != NULL) { + C2D_TextBufDelete(screen->textBuf); + screen->textBuf = NULL; + } + if (screen->target != NULL) { + C3D_RenderTargetDelete(screen->target); + screen->target = NULL; + } +} + +static void N3DS_appendBootLog(const char* message) { + if (message == NULL || message[0] == '\0') return; + + if (gN3DSBootLogLineCount < N3DS_BOOT_LOG_MAX_LINES) { + snprintf( + gN3DSBootLogLines[gN3DSBootLogLineCount], + sizeof(gN3DSBootLogLines[gN3DSBootLogLineCount]), + "%s", + message + ); + gN3DSBootLogLineCount++; + return; + } + + repeat(N3DS_BOOT_LOG_MAX_LINES - 1, i) { + snprintf( + gN3DSBootLogLines[i], + sizeof(gN3DSBootLogLines[i]), + "%s", + gN3DSBootLogLines[i + 1] + ); + } + snprintf( + gN3DSBootLogLines[N3DS_BOOT_LOG_MAX_LINES - 1], + sizeof(gN3DSBootLogLines[N3DS_BOOT_LOG_MAX_LINES - 1]), + "%s", + message + ); +} + +void Runner_platformBootLog(const char* message) { + N3DS_appendBootLog(message); +} + +static void N3DSLoadingScreen_draw(N3DSLoadingScreen* screen) { + if (screen == NULL) return; + + float progress = 0.0f; + if (screen->totalChunks > 0) { + progress = (float) screen->chunkIndex / (float) screen->totalChunks; + if (progress < 0.0f) progress = 0.0f; + if (progress > 1.0f) progress = 1.0f; + } + + if (screen->useCitro2D && screen->target != NULL && screen->textBuf != NULL) { + const float barX = 36.0f; + const float barY = 146.0f; + const float barW = 328.0f; + const float barH = 18.0f; + const float fillW = (barW - 4.0f) * progress; + + C2D_Text title; + C2D_Text status; + C2D_Text detail; + char detailLine[64]; + snprintf(detailLine, sizeof(detailLine), "%d / %d chunks", screen->chunkIndex, screen->totalChunks); + + C3D_FrameBegin(C3D_FRAME_SYNCDRAW); + C2D_TargetClear(screen->target, C2D_Color32(8, 10, 14, 255)); + C2D_SceneBegin(screen->target); + + C2D_DrawRectSolid(0.0f, 0.0f, 0.5f, 400.0f, 240.0f, C2D_Color32(8, 10, 14, 255)); + C2D_DrawRectSolid(barX, barY, 0.5f, barW, barH, C2D_Color32(42, 48, 60, 255)); + C2D_DrawRectSolid(barX + 2.0f, barY + 2.0f, 0.5f, fillW, barH - 4.0f, C2D_Color32(110, 224, 160, 255)); + + C2D_TextBufClear(screen->textBuf); + C2D_TextParse(&title, screen->textBuf, "Loading Game Data"); + C2D_TextParse(&status, screen->textBuf, screen->statusLine); + C2D_TextParse(&detail, screen->textBuf, detailLine); + C2D_TextOptimize(&title); + C2D_TextOptimize(&status); + C2D_TextOptimize(&detail); + + C2D_DrawText(&title, C2D_WithColor, 36.0f, 74.0f, 0.5f, 0.60f, 0.60f, C2D_Color32(255, 255, 255, 255)); + C2D_DrawText(&status, C2D_WithColor, 36.0f, 104.0f, 0.5f, N3DS_LOADING_TEXT_SCALE, N3DS_LOADING_TEXT_SCALE, C2D_Color32(210, 214, 224, 255)); + C2D_DrawText(&detail, C2D_WithColor, 36.0f, 172.0f, 0.5f, 0.34f, 0.34f, C2D_Color32(146, 153, 168, 255)); + + int firstLogLine = gN3DSBootLogLineCount > 3 ? gN3DSBootLogLineCount - 3 : 0; + float logY = 188.0f; + for (int i = firstLogLine; i < gN3DSBootLogLineCount; ++i) { + C2D_Text logLine; + C2D_TextParse(&logLine, screen->textBuf, gN3DSBootLogLines[i]); + C2D_TextOptimize(&logLine); + C2D_DrawText(&logLine, C2D_WithColor, 36.0f, logY, 0.5f, 0.25f, 0.25f, C2D_Color32(164, 176, 192, 255)); + logY += 12.0f; + } + + C3D_FrameEnd(0); + gspWaitForVBlank(); + return; + } + + int filled = (int) lroundf(progress * 24.0f); + if (filled < 0) filled = 0; + if (filled > 24) filled = 24; + + char bar[25]; + repeat(24, i) { + bar[i] = (int) i < filled ? '#' : '-'; + } + bar[24] = '\0'; + + consoleSelect(&screen->console); + printf("\x1b[2J"); + printf("\x1b[4;8HLoading Game Data"); + printf("\x1b[7;8H%s", screen->statusLine); + printf("\x1b[10;8H[%s]", bar); + printf("\x1b[12;8H%d / %d chunks", screen->chunkIndex, screen->totalChunks); + printf("\x1b[15;8HPlease wait..."); + int firstLogLine = gN3DSBootLogLineCount > 5 ? gN3DSBootLogLineCount - 5 : 0; + for (int i = firstLogLine; i < gN3DSBootLogLineCount; ++i) { + printf("\x1b[%d;4H%s", 17 + (i - firstLogLine), gN3DSBootLogLines[i]); + } + + gfxFlushBuffers(); + gfxSwapBuffers(); + gspWaitForVBlank(); +} + +static void N3DSLoadingScreen_set(N3DSLoadingScreen* screen, const char* statusLine, int chunkIndex, int totalChunks) { + if (screen == NULL) return; + snprintf(screen->statusLine, sizeof(screen->statusLine), "%s", statusLine != NULL ? statusLine : ""); + screen->chunkIndex = chunkIndex; + screen->totalChunks = totalChunks; + N3DSLoadingScreen_draw(screen); +} + +static void N3DS_waitForStartExitScreen(N3DSLoadingScreen* screen, const char* statusLine) { + N3DSLoadingScreen_set(screen, statusLine, 1, 1); + while (aptMainLoop()) { + hidScanInput(); + if (hidKeysDown() & KEY_START) break; + gspWaitForVBlank(); + } +} + +static s64 N3DS_ticksToNs(u64 ticks) { + return (s64) ((ticks * 1000000000ULL) / SYSCLOCK_ARM11); +} + +static void N3DS_sleepUntilTick(u64 targetTick) { + const u64 coarseGuardTicks = SYSCLOCK_ARM11 / 2000u; // ~0.5 ms + + while (true) { + u64 now = svcGetSystemTick(); + if (now >= targetTick) return; + + u64 remaining = targetTick - now; + if (remaining <= coarseGuardTicks) break; + + svcSleepThread(N3DS_ticksToNs(remaining - coarseGuardTicks)); + } + + while (svcGetSystemTick() < targetTick) { + } +} + +static void N3DS_beginPacedFrame(u64* nextFrameTick, u64 frameTicks) { + if (nextFrameTick == NULL || frameTicks == 0) return; + + u64 now = svcGetSystemTick(); + if (*nextFrameTick == 0) { + *nextFrameTick = now; + } else if (now > *nextFrameTick + frameTicks * 4u) { + *nextFrameTick = now; + } else { + while (now > *nextFrameTick) { + *nextFrameTick += frameTicks; + } + } + + N3DS_sleepUntilTick(*nextFrameTick); + *nextFrameTick += frameTicks; +} + +static void N3DSDataWinProgressCallback(const char* chunkName, int chunkIndex, int totalChunks, MAYBE_UNUSED DataWin* dataWin, void* userData) { + N3DSLoadingScreen* screen = (N3DSLoadingScreen*) userData; + if (screen == NULL) return; + + char status[128]; + snprintf(status, sizeof(status), "Parsing chunk %.4s", chunkName != NULL ? chunkName : "----"); + N3DSLoadingScreen_set(screen, status, chunkIndex + 1, totalChunks); +} + +int main(int argc, char** argv) { + (void) argc; + (void) argv; + + gfxInitDefault(); + romfsInit(); + APT_SetAppCpuTimeLimit(30); + osSetSpeedupEnable(true); + + bool citroReady = false; + + if (C3D_Init(0x80000) && C2D_Init(C2D_DEFAULT_MAX_OBJECTS)) { + C2D_Prepare(); + citroReady = true; + } else { + C2D_Fini(); + C3D_Fini(); + } + + mkdir("sdmc:/3ds", 0777); + mkdir("sdmc:/3ds/cinnamon", 0777); + + char* dataWinPath = chooseDataWinPath(); + + N3DSLoadingScreen loadingScreen = {0}; + if (citroReady) { + loadingScreen.useCitro2D = true; + loadingScreen.target = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT); + loadingScreen.textBuf = C2D_TextBufNew(512); + if (loadingScreen.target == NULL || loadingScreen.textBuf == NULL) { + N3DSLoadingScreen_free(&loadingScreen); + loadingScreen.useCitro2D = false; + } + } else { + consoleInit(GFX_TOP, &loadingScreen.console); + } + + N3DSLoadingScreen_set(&loadingScreen, "Scanning data.win", 0, 1); + + DataWin* dataWin = DataWin_parse( + dataWinPath, + (DataWinParserOptions) { + .parseGen8 = true, + .parseOptn = true, + .parseLang = true, + .parseExtn = false, + .parseSond = true, + .parseAgrp = true, + .parseSprt = true, + .parseBgnd = true, + .parsePath = true, + .parseScpt = true, + .parseGlob = true, + .parseShdr = true, + .parseFont = true, + .parseTmln = true, + .parseObjt = true, + .parseRoom = true, + .parseTpag = true, + .parseCode = true, + .parseVari = true, + .parseFunc = true, + .parseStrg = true, + .parseTxtr = false, + .parseAudo = false, + .skipLoadingPreciseMasksForNonPreciseSprites = true, + .progressCallback = N3DSDataWinProgressCallback, + .progressCallbackUserData = &loadingScreen, + } + ); + + free(dataWinPath); + + if (dataWin == NULL) { + N3DS_waitForStartExitScreen(&loadingScreen, "Failed to load data.win. Press START."); + N3DSLoadingScreen_free(&loadingScreen); + return 1; + } + + FileSystem* fileSystem = (FileSystem*) N3DSFileSystem_create("romfs:/", "sdmc:/3ds/cinnamon/"); + AudioSystem* audioSystem = (AudioSystem*) N3DSAudioSystem_create(); + N3DSLoadingScreen_set(&loadingScreen, "Loading sound bank", 1, 2); + audioSystem->vtable->init(audioSystem, dataWin, fileSystem); + + N3DSLoadingScreen_set(&loadingScreen, "Initializing renderer", 2, 2); + VMContext* vm = VM_create(dataWin); + Renderer* renderer = N3DSRenderer_create(); + Runner* runner = Runner_create(dataWin, vm, renderer, fileSystem, audioSystem); + + if (!N3DSRenderer_isReady(renderer)) { + const char* error = N3DSRenderer_getStartupError(renderer); + N3DS_waitForStartExitScreen(&loadingScreen, error != NULL ? error : "Renderer init failed. Press START."); + audioSystem->vtable->destroy(audioSystem); + renderer->vtable->destroy(renderer); + Runner_free(runner); + N3DSFileSystem_destroy((N3DSFileSystem*) fileSystem); + VM_free(vm); + DataWin_free(dataWin); + N3DSLoadingScreen_free(&loadingScreen); + if (citroReady) { + C2D_Fini(); + C3D_Fini(); + } + romfsExit(); + gfxExit(); + return 1; + } + + N3DSLoadingScreen_free(&loadingScreen); + N3DSDebugMonitor debugMonitor; + N3DSDebugMonitor_init(&debugMonitor); + + runner->osType = OS_3DS; + Runner_initFirstRoom(runner); + + bool haveRoomBorder = false; + C2D_Sprite roomBorder; + memset(&roomBorder, 0, sizeof(roomBorder)); + C2D_SpriteSheet roomBorderSheet = NULL; + char loadedRoomBorderAsset[32] = ""; + bool debugMonitorVisible = false; + N3DS_refreshRoomBorderSprite(runner->currentRoom, &roomBorder, &roomBorderSheet, &haveRoomBorder, loadedRoomBorderAsset, sizeof(loadedRoomBorderAsset)); + + bool leftHeld = false, rightHeld = false, upHeld = false, downHeld = false; + bool aHeld = false, bHeld = false, yHeld = false; + bool lHeld = false, rHeld = false; + + Gen8* gen8 = &dataWin->gen8; + int32_t gameW = (int32_t) gen8->defaultWindowWidth; + int32_t gameH = (int32_t) gen8->defaultWindowHeight; + + const u64 frameTicks = (SYSCLOCK_ARM11 + 15u) / 30u; + u64 nextFrameTick = svcGetSystemTick(); + while (aptMainLoop() && !runner->shouldExit) { + N3DS_beginPacedFrame(&nextFrameTick, frameTicks); + hidScanInput(); + u32 held = hidKeysHeld(); + u32 down = hidKeysDown(); + + if (down & KEY_START) break; + if (down & KEY_SELECT) debugMonitorVisible = !debugMonitorVisible; + bool debugWarpToAsriel = debugMonitorVisible && (down & KEY_L) && + (uint32_t) N3DS_DEBUG_ASRIEL_ROOM < runner->dataWin->room.count; + if (debugWarpToAsriel) { + runner->pendingRoom = N3DS_DEBUG_ASRIEL_ROOM; + } + + circlePosition circle; + hidCircleRead(&circle); + + syncKey(runner->keyboard, &leftHeld, VK_LEFT, (held & KEY_LEFT) || circle.dx < -80); + syncKey(runner->keyboard, &rightHeld, VK_RIGHT, (held & KEY_RIGHT) || circle.dx > 80); + syncKey(runner->keyboard, &upHeld, VK_UP, (held & KEY_UP) || circle.dy > 80); + syncKey(runner->keyboard, &downHeld, VK_DOWN, (held & KEY_DOWN) || circle.dy < -80); + + syncKey(runner->keyboard, &aHeld, 'Z', (held & KEY_A)); + syncKey(runner->keyboard, &bHeld, 'X', (held & KEY_B)); + syncKey(runner->keyboard, &yHeld, 'C', (held & KEY_Y) || (held & KEY_X)); + syncKey(runner->keyboard, &lHeld, VK_PAGEDOWN, (held & KEY_L) && !debugWarpToAsriel); + syncKey(runner->keyboard, &rHeld, VK_PAGEUP, (held & KEY_R)); + + Runner_step(runner); + N3DS_tryTriggerAsrielLed(runner); + runner->audioSystem->vtable->update(runner->audioSystem, 1.0f / 30.0f); + N3DS_refreshRoomBorderSprite(runner->currentRoom, &roomBorder, &roomBorderSheet, &haveRoomBorder, loadedRoomBorderAsset, sizeof(loadedRoomBorderAsset)); + + float displayScaleX = 1.0f; + float displayScaleY = 1.0f; + Runner_computeViewDisplayScale(runner, gameW, gameH, &displayScaleX, &displayScaleY); + u64 renderStartMs = osGetTime(); + C3D_FrameBegin(0); + renderer->vtable->beginFrame(renderer, gameW, gameH, 400, 240); + Runner_drawViews(runner, gameW, gameH, displayScaleX, displayScaleY, false); + N3DSRenderer_beginOverlay(renderer); + if (haveRoomBorder) C2D_DrawSprite(&roomBorder); + else N3DS_drawFallbackRoomBorder(); + if (debugMonitorVisible) { + N3DSDebugMonitor_draw(&debugMonitor, runner, renderer); + } + renderer->vtable->endFrame(renderer); + C3D_FrameEnd(0); + N3DSDebugMonitor_tickFrame(&debugMonitor, (double) (osGetTime() - renderStartMs)); + + RunnerKeyboard_beginFrame(runner->keyboard); + } + + audioSystem->vtable->destroy(audioSystem); + renderer->vtable->destroy(renderer); + Runner_free(runner); + N3DSFileSystem_destroy((N3DSFileSystem*) fileSystem); + VM_free(vm); + DataWin_free(dataWin); + N3DSDebugMonitor_free(&debugMonitor); + + if (roomBorderSheet != NULL) { + C2D_SpriteSheetFree(roomBorderSheet); + } + + if (citroReady) { + C2D_Fini(); + C3D_Fini(); + } + + romfsExit(); + gfxExit(); + return 0; +} diff --git a/src/n3ds/n3ds_audio_system.c b/src/n3ds/n3ds_audio_system.c new file mode 100644 index 00000000..8eed638a --- /dev/null +++ b/src/n3ds/n3ds_audio_system.c @@ -0,0 +1,3830 @@ +#include "n3ds_audio_system.h" +#include "n3ds_debug_log.h" +#include "n3ds_platform_config.h" + +#include "../data_win.h" +#include "../utils.h" + +#include <3ds.h> + +#include +#include +#include +#include +#include + +#define N3DS_ENABLE_LOGGING 0 + +#if !N3DS_ENABLE_LOGGING +#define fprintf(...) ((int) 0) +#endif + +#define N3DS_MAX_NDSP_CHANNELS 24 +#define N3DS_MAX_SOUND_INSTANCES 24 +#define N3DS_RESERVED_STREAM_INSTANCES 4 +#define N3DS_SOUND_INSTANCE_ID_BASE 100000 +#define N3DS_AUDIO_STREAM_INDEX_BASE 200000 +#define N3DS_MAX_STREAMS 64 +#define N3DS_STREAM_BUFFER_COUNT 3 +#define N3DS_STREAM_CHUNK_SAMPLES (14u * 256u) +#define N3DS_STREAM_ADPCM_CACHE_FRAMES 256u +#define N3DS_STREAM_WORKER_STACK_SIZE (32u * 1024u) +#define N3DS_STREAM_WORKER_PRIORITY 0x31 +#define N3DS_STREAM_WORKER_CORE_ID 1 +#define N3DS_STREAM_WORKER_CPU_LIMIT 20u +#define N3DS_AUDIO_FILE_BUFFER_SIZE (64u * 1024u) +#define N3DS_ENABLE_STREAM_WORKER 1 +#define N3DS_EAGER_SFX_PRELOAD 0 +#define N3DS_MAX_PRELOADED_SFX_BYTES_NEW3DS (15u * 1024u * 1024u) +#define N3DS_MAX_PRELOADED_SFX_BYTES_OLD3DS (15u * 1024u * 1024u) +#define N3DS_ROOM_PREWARM_SOUND_COUNT_NEW3DS 2u +#define N3DS_ROOM_PREWARM_BYTE_BUDGET_NEW3DS (192u * 1024u) +#define N3DS_BACKGROUND_PREWARM_SOUND_COUNT_NEW3DS 0u +#define N3DS_BACKGROUND_PREWARM_BYTE_BUDGET_NEW3DS 0u +#define N3DS_ROOM_PREWARM_SOUND_COUNT_OLD3DS 1u +#define N3DS_ROOM_PREWARM_BYTE_BUDGET_OLD3DS (96u * 1024u) +#define N3DS_BACKGROUND_PREWARM_SOUND_COUNT_OLD3DS 0u +#define N3DS_BACKGROUND_PREWARM_BYTE_BUDGET_OLD3DS 0u +#define N3DS_FORCE_PCM_BCWAV_PLAYBACK 0 +#define N3DS_ROMFS_AUDIO_BASE "romfs:/audio" +#define N3DS_ROMFS_MUSIC_BASE "romfs:/" +#define N3DS_SDMC_AUDIO_BASE "sdmc:/3ds/cinnamon/audio" +#define N3DS_SDMC_MUSIC_BASE "sdmc:/3ds/cinnamon" +#define N3DS_MAX_MISSING_AUDIO_PATHS 512 +#define N3DS_SOUND_BANK_MAGIC 0x314B4253u /* SBK1 */ +#define N3DS_SOUND_BANK_VERSION_BCWAV 1u +#define N3DS_SOUND_BANK_VERSION_PCM16 2u +#define N3DS_SOUND_BANK_ENTRY_CHANNEL_MASK 0x000000FFu +#define N3DS_SOUND_BANK_ENTRY_FLAG_LOOP 0x00000100u +#define N3DS_SOUND_BANK_ENTRY_FLAG_PCM16 0x00000200u +#define N3DS_PCM_END_FADE_FRAMES 256u + +typedef struct { + uint8_t predictorScale; + int16_t yn1; + int16_t yn2; +} N3DSDspContext; + +typedef struct { + uint16_t coefs[16]; + uint32_t dataOffset; + uint32_t dataSize; + N3DSDspContext startContext; + N3DSDspContext loopContext; +} N3DSBcwavChannel; + +typedef struct { + uint32_t sampleRate; + uint32_t sampleCount; + uint32_t loopStart; + uint32_t loopEnd; + bool loop; + uint8_t channelCount; + N3DSBcwavChannel channels[2]; +} N3DSBcwav; + +typedef struct { + bool active; + char* path; + uint8_t* blobData; + uint32_t blobSize; + N3DSBcwav bcwav; + float gain; + float pitch; + uint32_t refCount; +} N3DSStreamEntry; + +typedef struct { + uint32_t nextFrameIndex; + uint8_t predictorScale; + int16_t hist1; + int16_t hist2; +} N3DSStreamDecodeState; + +typedef struct { + uint32_t currentSample; + uint32_t pendingSamples; + uint32_t pendingOffset; + int16_t pendingPcm[14 * 2]; + N3DSStreamDecodeState decode[2]; + uint32_t fileCacheFrameBase[2]; + uint32_t fileCacheFrameCount[2]; + uint8_t fileCache[2][8u * N3DS_STREAM_ADPCM_CACHE_FRAMES]; +} N3DSStreamFillCursor; + +typedef struct { + bool requested; + bool inProgress; + bool ready; + bool success; + int bufferIndex; + uint32_t generation; + uint32_t bufferStartSample; + uint32_t sampleCount; + N3DSStreamFillCursor nextCursor; + ndspAdpcmData adpcmStates[2]; +} N3DSAsyncStreamFill; + +typedef struct { + bool active; + bool paused; + bool loop; + bool isStream; + bool streamFinished; + int32_t soundIndex; + int32_t instanceId; + int channelId; + int secondaryChannelId; + float gain; + float pitch; + float baseRate; + uint32_t sampleCount; + uint32_t sampleRate; + uint8_t channelCount; + bool useNativeAdpcm; + ndspWaveBuf waveBufs[N3DS_STREAM_BUFFER_COUNT]; + ndspWaveBuf secondaryWaveBufs[N3DS_STREAM_BUFFER_COUNT]; + ndspAdpcmData adpcmStates[N3DS_STREAM_BUFFER_COUNT * 2]; + uint32_t bufferStartSample[N3DS_STREAM_BUFFER_COUNT]; + uint8_t* streamAdpcm[2][N3DS_STREAM_BUFFER_COUNT]; + int16_t* streamPcm[N3DS_STREAM_BUFFER_COUNT]; + int16_t* pcmData; + FILE* streamFile; + const uint8_t* streamBlob; + uint32_t streamBlobSize; + bool ownsStreamBlob; + bool streamBlobLinear; + N3DSBcwav bcwav; + N3DSStreamDecodeState decode[2]; + uint32_t fileCacheFrameBase[2]; + uint32_t fileCacheFrameCount[2]; + uint8_t fileCache[2][8u * N3DS_STREAM_ADPCM_CACHE_FRAMES]; + uint32_t currentSample; + uint32_t pendingSamples; + uint32_t pendingOffset; + int16_t pendingPcm[14 * 2]; + uint32_t streamGeneration; + N3DSAsyncStreamFill asyncFill; + u64 playbackStartTick; + u64 playbackReleaseTick; +} N3DSSoundInstance; + +typedef struct { + bool attempted; + bool available; + bool headerAttempted; + bool headerAvailable; + bool pathAttempted; + bool blobOwned; + char* path; + uint8_t* blob; + uint8_t* nativeBlob; + uint32_t blobSize; + N3DSBcwav bcwav; +} N3DSCachedSound; + +typedef struct { + uint32_t offset; + uint32_t size; + uint32_t sampleRate; + uint32_t sampleCount; + uint32_t flags; +} N3DSPackedSoundBankEntry; + +struct N3DSAudioSystem { + AudioSystem base; + FileSystem* fileSystem; + bool initialized; + float masterGain; + N3DSStreamEntry streams[N3DS_MAX_STREAMS]; + N3DSSoundInstance instances[N3DS_MAX_SOUND_INSTANCES]; + N3DSCachedSound* cachedSounds; + uint32_t cachedSoundCount; + uint32_t cachedSoundBytes; + uint32_t cachedSoundPrewarmCursor; + uint32_t maxCachedSoundBytes; + uint32_t roomPrewarmSoundCount; + uint32_t roomPrewarmByteBudget; + uint32_t backgroundPrewarmSoundCount; + uint32_t backgroundPrewarmByteBudget; + uint32_t pendingPrewarmSoundCount; + uint32_t pendingPrewarmByteBudget; + bool isNew3DS; + int32_t lastResolveFailureSound; + bool ndspChannelInUse[N3DS_MAX_NDSP_CHANNELS]; + uint8_t nextNdspChannel; + LightLock lock; + LightLock missingPathLock; + CondVar workerCond; + LightEvent workerEvent; + Thread workerThread; + bool workerStop; + bool workerEnabled; + uint8_t* packedSoundBankData; + uint32_t packedSoundBankSize; + N3DSPackedSoundBankEntry* packedSoundBankEntries; + uint32_t packedSoundBankEntryCount; + uint32_t packedSoundBankVersion; +}; + +static char* gN3DSMissingAudioPaths[N3DS_MAX_MISSING_AUDIO_PATHS]; +static uint32_t gN3DSMissingAudioPathCount = 0; + +static void N3DSAudio_cancelAsyncFillLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst); +static bool N3DSAudio_hasActiveStreamPlaybackLocked(const N3DSAudioSystem* audio); +static char* N3DSAudio_tryResolveSfxCandidate(N3DSAudioSystem* audio, const char* candidate); + +static void N3DSAudio_captureFillCursor(const N3DSSoundInstance* inst, N3DSStreamFillCursor* cursor) { + if (inst == NULL || cursor == NULL) return; + memset(cursor, 0, sizeof(*cursor)); + cursor->currentSample = inst->currentSample; + cursor->pendingSamples = inst->pendingSamples; + cursor->pendingOffset = inst->pendingOffset; + memcpy(cursor->pendingPcm, inst->pendingPcm, sizeof(cursor->pendingPcm)); + memcpy(cursor->decode, inst->decode, sizeof(cursor->decode)); + memcpy(cursor->fileCacheFrameBase, inst->fileCacheFrameBase, sizeof(cursor->fileCacheFrameBase)); + memcpy(cursor->fileCacheFrameCount, inst->fileCacheFrameCount, sizeof(cursor->fileCacheFrameCount)); + memcpy(cursor->fileCache, inst->fileCache, sizeof(cursor->fileCache)); +} + +static void N3DSAudio_applyFillCursor(N3DSSoundInstance* inst, const N3DSStreamFillCursor* cursor) { + if (inst == NULL || cursor == NULL) return; + inst->currentSample = cursor->currentSample; + inst->pendingSamples = cursor->pendingSamples; + inst->pendingOffset = cursor->pendingOffset; + memcpy(inst->pendingPcm, cursor->pendingPcm, sizeof(inst->pendingPcm)); + memcpy(inst->decode, cursor->decode, sizeof(inst->decode)); + memcpy(inst->fileCacheFrameBase, cursor->fileCacheFrameBase, sizeof(inst->fileCacheFrameBase)); + memcpy(inst->fileCacheFrameCount, cursor->fileCacheFrameCount, sizeof(inst->fileCacheFrameCount)); + memcpy(inst->fileCache, cursor->fileCache, sizeof(inst->fileCache)); +} + +static uint16_t N3DSAudio_readU16(const uint8_t* ptr) { + return (uint16_t) (ptr[0] | (ptr[1] << 8)); +} + +static uint32_t N3DSAudio_readU32(const uint8_t* ptr) { + return (uint32_t) ptr[0] | + ((uint32_t) ptr[1] << 8) | + ((uint32_t) ptr[2] << 16) | + ((uint32_t) ptr[3] << 24); +} + +static void N3DSAudio_softenPcmTail(int16_t* pcm, uint32_t sampleCount, uint8_t channelCount) { + if (pcm == NULL || sampleCount == 0 || channelCount == 0) return; + + uint32_t fadeFrames = sampleCount; + if (fadeFrames > N3DS_PCM_END_FADE_FRAMES) fadeFrames = N3DS_PCM_END_FADE_FRAMES; + if (fadeFrames == 0) return; + if (fadeFrames == 1) { + for (uint8_t channel = 0; channel < channelCount; channel++) { + pcm[channel] = 0; + } + return; + } + + uint32_t startFrame = sampleCount - fadeFrames; + for (uint32_t frame = 0; frame < fadeFrames; frame++) { + uint32_t gainNumerator = fadeFrames - 1u - frame; + uint32_t gainDenominator = fadeFrames - 1u; + uint32_t frameOffset = (startFrame + frame) * channelCount; + for (uint8_t channel = 0; channel < channelCount; channel++) { + int32_t sample = pcm[frameOffset + channel]; + pcm[frameOffset + channel] = (int16_t) ((sample * (int32_t) gainNumerator) / (int32_t) gainDenominator); + } + } +} + +static int16_t N3DSAudio_readS16(const uint8_t* ptr) { + return (int16_t) N3DSAudio_readU16(ptr); +} + +static int N3DSAudio_signExtend4(int nibble) { + return (nibble & 0x8) ? (nibble - 16) : nibble; +} + +static void N3DSAudio_configureFileBuffer(FILE* file) { + if (file == NULL) return; + setvbuf(file, NULL, _IOFBF, N3DS_AUDIO_FILE_BUFFER_SIZE); +} + +static bool N3DSAudio_readFileFully(const char* path, uint8_t** outData, uint32_t* outSize) { + *outData = NULL; + *outSize = 0; + + FILE* file = fopen(path, "rb"); + if (file == NULL) return false; + N3DSAudio_configureFileBuffer(file); + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size <= 0) { + fclose(file); + return false; + } + + uint8_t* data = safeMalloc((size_t) size); + if (fread(data, 1, (size_t) size, file) != (size_t) size) { + fclose(file); + free(data); + return false; + } + + fclose(file); + *outData = data; + *outSize = (uint32_t) size; + return true; +} + +static bool N3DSAudio_readFileFullyLinear(const char* path, uint8_t** outData, uint32_t* outSize) { + *outData = NULL; + *outSize = 0; + + FILE* file = fopen(path, "rb"); + if (file == NULL) return false; + N3DSAudio_configureFileBuffer(file); + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size <= 0) { + fclose(file); + return false; + } + + uint8_t* data = linearAlloc((size_t) size); + if (data == NULL) { + fclose(file); + return false; + } + if (fread(data, 1, (size_t) size, file) != (size_t) size) { + fclose(file); + linearFree(data); + return false; + } + + fclose(file); + DSP_FlushDataCache(data, (size_t) size); + *outData = data; + *outSize = (uint32_t) size; + return true; +} + +static bool N3DSAudio_getFileSize(FILE* file, uint32_t* outSize) { + if (file == NULL || outSize == NULL) return false; + + if (fseek(file, 0, SEEK_END) != 0) return false; + long size = ftell(file); + if (size <= 0) { + fseek(file, 0, SEEK_SET); + return false; + } + if (fseek(file, 0, SEEK_SET) != 0) return false; + + *outSize = (uint32_t) size; + return true; +} + +static uint8_t* N3DSAudio_cloneBlobToLinear(const uint8_t* data, uint32_t size) { + if (data == NULL || size == 0) return NULL; + uint8_t* linearData = linearAlloc(size); + if (linearData == NULL) return NULL; + memcpy(linearData, data, size); + DSP_FlushDataCache(linearData, size); + return linearData; +} + +static const N3DSPackedSoundBankEntry* N3DSAudio_getPackedSoundEntry(const N3DSAudioSystem* audio, int32_t soundIndex) { + if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->packedSoundBankEntryCount) return NULL; + if (audio->packedSoundBankEntries == NULL) return NULL; + return &audio->packedSoundBankEntries[soundIndex]; +} + +static bool N3DSAudio_packedSoundEntryIsPcm16(const N3DSPackedSoundBankEntry* entry) { + return entry != NULL && (entry->flags & N3DS_SOUND_BANK_ENTRY_FLAG_PCM16) != 0; +} + +static uint8_t N3DSAudio_packedSoundEntryChannelCount(const N3DSPackedSoundBankEntry* entry) { + return entry != NULL ? (uint8_t) (entry->flags & N3DS_SOUND_BANK_ENTRY_CHANNEL_MASK) : 0; +} + +static bool N3DSAudio_getPackedSoundBlob( + const N3DSAudioSystem* audio, + int32_t soundIndex, + const uint8_t** outBlob, + uint32_t* outSize +) { + if (outBlob != NULL) *outBlob = NULL; + if (outSize != NULL) *outSize = 0; + if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->packedSoundBankEntryCount) return false; + if (audio->packedSoundBankData == NULL || audio->packedSoundBankEntries == NULL) return false; + + const N3DSPackedSoundBankEntry* entry = &audio->packedSoundBankEntries[soundIndex]; + if (entry->size == 0) return false; + if (entry->offset >= audio->packedSoundBankSize) return false; + if (entry->size > audio->packedSoundBankSize - entry->offset) return false; + + if (outBlob != NULL) *outBlob = audio->packedSoundBankData + entry->offset; + if (outSize != NULL) *outSize = entry->size; + return true; +} + +static bool N3DSAudio_loadPackedSoundBank(N3DSAudioSystem* audio) { + if (audio == NULL) return false; + if (audio->packedSoundBankData != NULL) return true; + + char* path = N3DSAudio_tryResolveSfxCandidate(audio, "sound_bank.bin"); + if (path == NULL) return false; + + uint8_t* bankData = NULL; + uint32_t bankSize = 0; + N3DSPackedSoundBankEntry* entries = NULL; + bool loaded = false; + + if (!N3DSAudio_readFileFully(path, &bankData, &bankSize)) goto cleanup; + if (bankSize < 16u) goto cleanup; + if (N3DSAudio_readU32(bankData + 0u) != N3DS_SOUND_BANK_MAGIC) goto cleanup; + uint32_t version = N3DSAudio_readU32(bankData + 4u); + if (version != N3DS_SOUND_BANK_VERSION_BCWAV && version != N3DS_SOUND_BANK_VERSION_PCM16) goto cleanup; + + uint32_t entryCount = N3DSAudio_readU32(bankData + 8u); + uint32_t dataOffset = N3DSAudio_readU32(bankData + 12u); + uint32_t entryStride = version >= N3DS_SOUND_BANK_VERSION_PCM16 ? 20u : 8u; + uint32_t tableBytes = entryCount * entryStride; + if (entryCount > 0 && tableBytes / entryStride != entryCount) goto cleanup; + if (dataOffset < 16u || dataOffset > bankSize) goto cleanup; + if (16u + tableBytes > dataOffset) goto cleanup; + + entries = safeCalloc(entryCount, sizeof(N3DSPackedSoundBankEntry)); + repeat(entryCount, i) { + uint32_t tableOffset = 16u + (uint32_t) i * entryStride; + uint32_t offset = N3DSAudio_readU32(bankData + tableOffset + 0u); + uint32_t size = N3DSAudio_readU32(bankData + tableOffset + 4u); + if (size != 0) { + if (offset < dataOffset || offset >= bankSize) goto cleanup; + if (size > bankSize - offset) goto cleanup; + } + entries[i].offset = offset; + entries[i].size = size; + if (version >= N3DS_SOUND_BANK_VERSION_PCM16) { + entries[i].sampleRate = N3DSAudio_readU32(bankData + tableOffset + 8u); + entries[i].sampleCount = N3DSAudio_readU32(bankData + tableOffset + 12u); + entries[i].flags = N3DSAudio_readU32(bankData + tableOffset + 16u); + } + } + + audio->packedSoundBankData = bankData; + audio->packedSoundBankSize = bankSize; + audio->packedSoundBankEntries = entries; + audio->packedSoundBankEntryCount = entryCount; + audio->packedSoundBankVersion = version; + bankData = NULL; + entries = NULL; + loaded = true; + N3DSDebugLog_event( + "audio_bank", + "loaded version=%lu entries=%lu sizeKB=%lu path=%s", + (unsigned long) version, + (unsigned long) entryCount, + (unsigned long) (bankSize / 1024u), + path + ); + +cleanup: + free(bankData); + free(entries); + free(path); + return loaded; +} + +static bool N3DSAudio_fileExists(N3DSAudioSystem* audio, const char* path) { + if (path == NULL) return false; + + if (audio != NULL) LightLock_Lock(&audio->missingPathLock); + repeat(gN3DSMissingAudioPathCount, i) { + if (strcmp(gN3DSMissingAudioPaths[i], path) == 0) { + if (audio != NULL) LightLock_Unlock(&audio->missingPathLock); + return false; + } + } + if (audio != NULL) LightLock_Unlock(&audio->missingPathLock); + + struct stat st; + if (stat(path, &st) != 0) { + if (audio != NULL) LightLock_Lock(&audio->missingPathLock); + if (gN3DSMissingAudioPathCount < N3DS_MAX_MISSING_AUDIO_PATHS) { + gN3DSMissingAudioPaths[gN3DSMissingAudioPathCount++] = safeStrdup(path); + } + if (audio != NULL) LightLock_Unlock(&audio->missingPathLock); + return false; + } + return true; +} + +static bool N3DSAudio_hasScheme(const char* path) { + return path != NULL && strstr(path, ":/") != NULL; +} + +static bool N3DSAudio_pathStartsWith(const char* path, const char* prefix) { + if (path == NULL || prefix == NULL) return false; + size_t prefixLen = strlen(prefix); + return strncmp(path, prefix, prefixLen) == 0; +} + +static char* N3DSAudio_joinPath(const char* base, const char* relativePath) { + if (base == NULL || relativePath == NULL) return NULL; + size_t baseLen = strlen(base); + size_t relLen = strlen(relativePath); + bool needSlash = baseLen > 0 && base[baseLen - 1] != '/' && relLen > 0 && relativePath[0] != '/'; + char* result = safeMalloc(baseLen + relLen + (needSlash ? 2 : 1)); + memcpy(result, base, baseLen); + size_t cursor = baseLen; + if (needSlash) result[cursor++] = '/'; + memcpy(result + cursor, relativePath, relLen); + result[cursor + relLen] = '\0'; + return result; +} + +static char* N3DSAudio_tryResolveCandidateInBases(N3DSAudioSystem* audio, const char* candidate, const char* const* bases, size_t baseCount) { + (void) audio; + if (candidate == NULL || candidate[0] == '\0') return NULL; + if (N3DSAudio_hasScheme(candidate)) { + return N3DSAudio_fileExists(audio, candidate) ? safeStrdup(candidate) : NULL; + } + + repeat(baseCount, i) { + char* resolved = N3DSAudio_joinPath(bases[i], candidate); + if (resolved != NULL && N3DSAudio_fileExists(audio, resolved)) { + return resolved; + } + free(resolved); + } + return NULL; +} + +static char* N3DSAudio_tryResolveCandidate(N3DSAudioSystem* audio, const char* candidate) { + const char* bases[] = { + N3DS_ROMFS_AUDIO_BASE, + N3DS_ROMFS_MUSIC_BASE, + N3DS_SDMC_AUDIO_BASE, + N3DS_SDMC_MUSIC_BASE, + }; + return N3DSAudio_tryResolveCandidateInBases(audio, candidate, bases, 4); +} + +static char* N3DSAudio_tryResolveMusicCandidate(N3DSAudioSystem* audio, const char* candidate) { + const char* bases[] = { + N3DS_ROMFS_MUSIC_BASE, + N3DS_ROMFS_AUDIO_BASE, + N3DS_SDMC_MUSIC_BASE, + N3DS_SDMC_AUDIO_BASE, + }; + return N3DSAudio_tryResolveCandidateInBases(audio, candidate, bases, 4); +} + +static char* N3DSAudio_tryResolveSfxCandidate(N3DSAudioSystem* audio, const char* candidate) { + const char* bases[] = { + N3DS_ROMFS_AUDIO_BASE, + N3DS_SDMC_AUDIO_BASE, + }; + return N3DSAudio_tryResolveCandidateInBases(audio, candidate, bases, 2); +} + +static char* N3DSAudio_resolveStreamPath(N3DSAudioSystem* audio, const char* name) { + if (name == NULL || name[0] == '\0') return NULL; + + char* resolved = N3DSAudio_tryResolveMusicCandidate(audio, name); + if (resolved != NULL) return resolved; + + char candidate[512]; + if (strchr(name, '.') == NULL) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", name); + return N3DSAudio_tryResolveMusicCandidate(audio, candidate); + } + + const char* slash = strrchr(name, '/'); + const char* base = slash != NULL ? slash + 1 : name; + const char* dot = strrchr(base, '.'); + if (dot == NULL || dot <= base) return NULL; + + size_t dirLen = slash != NULL ? (size_t) (slash - name + 1) : 0; + size_t baseLen = (size_t) (dot - base); + + if (dirLen + baseLen + 7 < sizeof(candidate)) { + memcpy(candidate, name, dirLen); + memcpy(candidate + dirLen, base, baseLen); + memcpy(candidate + dirLen + baseLen, ".bcwav", 7); + resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + + if (baseLen + 7 < sizeof(candidate)) { + memcpy(candidate, base, baseLen); + memcpy(candidate + baseLen, ".bcwav", 7); + resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + + return NULL; +} + +static char* N3DSAudio_resolveSoundPath(N3DSAudioSystem* audio, const Sound* sound, int32_t soundIndex); +static N3DSCachedSound* N3DSAudio_getCachedSoundIfAvailable(N3DSAudioSystem* audio, int32_t soundIndex); +static const char* N3DSAudio_getResolvedCachedSoundPath(N3DSAudioSystem* audio, int32_t soundIndex, const Sound* sound); +static bool N3DSAudio_getCachedSoundHeader(N3DSAudioSystem* audio, int32_t soundIndex, const Sound* sound, N3DSBcwav* out); +static uint8_t* N3DSAudio_getCachedNativeBlob(N3DSCachedSound* cachedSound); +static bool N3DSAudio_primeNativeAdpcm(N3DSSoundInstance* inst); +static void N3DSAudio_prewarmSoundCache(N3DSAudioSystem* audio, uint32_t maxSounds, uint32_t byteBudget); +static void N3DSAudio_requestCachePrewarmLocked(N3DSAudioSystem* audio, uint32_t maxSounds, uint32_t byteBudget); +static bool N3DSAudio_loadPackedSoundBank(N3DSAudioSystem* audio); +static bool N3DSAudio_getPackedSoundBlob( + const N3DSAudioSystem* audio, + int32_t soundIndex, + const uint8_t** outBlob, + uint32_t* outSize +); + +static bool N3DSAudio_stringContainsIgnoreCase(const char* haystack, const char* needle) { + if (haystack == NULL || needle == NULL || needle[0] == '\0') return false; + size_t needleLen = strlen(needle); + for (const char* cursor = haystack; *cursor != '\0'; ++cursor) { + size_t matched = 0; + while (matched < needleLen) { + char a = cursor[matched]; + if (a == '\0') return false; + char b = needle[matched]; + if (a >= 'A' && a <= 'Z') a = (char) (a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = (char) (b - 'A' + 'a'); + if (a != b) break; + matched++; + } + if (matched == needleLen) return true; + } + return false; +} + +static bool N3DSAudio_nameLooksLikeSfx(const char* value) { + if (value == NULL || value[0] == '\0') return false; + + const char* base = strrchr(value, '/'); + const char* backslash = strrchr(value, '\\'); + if (backslash != NULL && (base == NULL || backslash > base)) base = backslash; + base = base != NULL ? base + 1 : value; + + return strncmp(base, "mus_sfx", 7) == 0 || + strncmp(base, "sfx_", 4) == 0 || + strncmp(base, "snd_", 4) == 0 || + N3DSAudio_stringContainsIgnoreCase(base, "_sfx") || + N3DSAudio_stringContainsIgnoreCase(base, "sfx_"); +} + +static bool N3DSAudio_nameLooksLikeMusic(const char* value) { + if (value == NULL || value[0] == '\0') return false; + + const char* base = strrchr(value, '/'); + const char* backslash = strrchr(value, '\\'); + if (backslash != NULL && (base == NULL || backslash > base)) base = backslash; + base = base != NULL ? base + 1 : value; + if (N3DSAudio_nameLooksLikeSfx(base)) return false; + if (strncmp(base, "mus_", 4) == 0 || strncmp(base, "bgm_", 4) == 0) return true; + return N3DSAudio_stringContainsIgnoreCase(base, "music"); +} + +static bool N3DSAudio_soundLooksLikeSfx(const Sound* sound) { + if (sound == NULL) return false; + return N3DSAudio_nameLooksLikeSfx(sound->name) || + N3DSAudio_nameLooksLikeSfx(sound->file) || + N3DSAudio_stringContainsIgnoreCase(sound->type, "sfx") || + N3DSAudio_stringContainsIgnoreCase(sound->type, "effect"); +} + +static bool N3DSAudio_soundLooksLikeMusic(const Sound* sound) { + if (sound == NULL) return false; + if (N3DSAudio_soundLooksLikeSfx(sound)) return false; + return N3DSAudio_nameLooksLikeMusic(sound->name) || + N3DSAudio_nameLooksLikeMusic(sound->file) || + N3DSAudio_stringContainsIgnoreCase(sound->type, "music") || + N3DSAudio_stringContainsIgnoreCase(sound->type, "stream"); +} + +static bool N3DSAudio_pathLooksLikeBundledMusic(const char* path) { + if (path == NULL) return false; + if (N3DSAudio_pathStartsWith(path, "romfs:/audio/")) return false; + if (N3DSAudio_pathStartsWith(path, "sdmc:/3ds/cinnamon/audio/")) return false; + return N3DSAudio_pathStartsWith(path, "romfs:/") || N3DSAudio_pathStartsWith(path, "sdmc:/3ds/cinnamon/"); +} + +static bool N3DSAudio_extractBaseNameNoExt(const char* value, char* out, size_t outSize) { + if (out == NULL || outSize == 0) return false; + out[0] = '\0'; + if (value == NULL || value[0] == '\0') return false; + + const char* base = strrchr(value, '/'); + const char* backslash = strrchr(value, '\\'); + if (backslash != NULL && (base == NULL || backslash > base)) base = backslash; + base = (base != NULL) ? base + 1 : value; + if (base[0] == '\0') return false; + + const char* dot = strrchr(base, '.'); + size_t len = (dot != NULL && dot > base) ? (size_t) (dot - base) : strlen(base); + if (len == 0 || len + 1 > outSize) return false; + memcpy(out, base, len); + out[len] = '\0'; + return true; +} + +static bool N3DSAudio_shouldPreloadSound(const Sound* sound) { + return sound != NULL; +} + +static bool N3DSAudio_shouldPrewarmCachedSound(const Sound* sound, const N3DSBcwav* bcwav) { + if (sound == NULL || bcwav == NULL) return false; + if (!N3DSAudio_soundLooksLikeMusic(sound) && + bcwav->sampleRate > 0 && + bcwav->sampleCount > (bcwav->sampleRate * 10u)) { + return false; + } + return true; +} + +static bool N3DSAudio_parseBcwavInfo(const uint8_t* info, uint32_t infoSize, uint32_t dataBlockOffset, uint32_t fileSize, N3DSBcwav* out) { + memset(out, 0, sizeof(*out)); + if (infoSize < 0x20 || memcmp(info, "INFO", 4) != 0) return false; + + uint8_t encoding = info[0x08]; + if (encoding != 2) return false; + + out->loop = info[0x09] != 0; + out->sampleRate = N3DSAudio_readU32(info + 0x0C); + out->loopStart = N3DSAudio_readU32(info + 0x10); + out->loopEnd = N3DSAudio_readU32(info + 0x14); + out->sampleCount = out->loopEnd; + + uint32_t tableOffset = 0x1C; + uint32_t channelCount = N3DSAudio_readU32(info + tableOffset); + if (channelCount == 0 || channelCount > 2) return false; + if (infoSize < tableOffset + 4 + channelCount * 8) return false; + + out->channelCount = (uint8_t) channelCount; + uint32_t encodedBytes = ((out->sampleCount + 13u) / 14u) * 8u; + + repeat(channelCount, i) { + const uint8_t* channelRef = info + tableOffset + 4 + i * 8; + uint32_t channelInfoOffset = tableOffset + N3DSAudio_readU32(channelRef + 4); + if (channelInfoOffset + 0x14 > infoSize) return false; + + const uint8_t* channelInfo = info + channelInfoOffset; + uint32_t sampleOffset = N3DSAudio_readU32(channelInfo + 4); + uint32_t adpcmInfoOffset = channelInfoOffset + N3DSAudio_readU32(channelInfo + 12); + if (adpcmInfoOffset + 0x2E > infoSize) return false; + + N3DSBcwavChannel* channel = &out->channels[i]; + channel->dataOffset = dataBlockOffset + 8 + sampleOffset; + channel->dataSize = encodedBytes; + if (channel->dataOffset + channel->dataSize > fileSize) return false; + + const uint8_t* adpcmInfo = info + adpcmInfoOffset; + repeat(16, coef) { + channel->coefs[coef] = N3DSAudio_readU16(adpcmInfo + coef * 2); + } + channel->startContext.predictorScale = adpcmInfo[0x20]; + channel->startContext.yn1 = N3DSAudio_readS16(adpcmInfo + 0x22); + channel->startContext.yn2 = N3DSAudio_readS16(adpcmInfo + 0x24); + channel->loopContext.predictorScale = adpcmInfo[0x26]; + channel->loopContext.yn1 = N3DSAudio_readS16(adpcmInfo + 0x28); + channel->loopContext.yn2 = N3DSAudio_readS16(adpcmInfo + 0x2A); + } + + return true; +} + +static bool N3DSAudio_parseBcwavBlob(const uint8_t* data, uint32_t size, N3DSBcwav* out) { + if (size < 0x40 || memcmp(data, "CWAV", 4) != 0) return false; + uint32_t infoOffset = N3DSAudio_readU32(data + 0x18); + uint32_t infoSize = N3DSAudio_readU32(data + 0x1C); + uint32_t dataOffset = N3DSAudio_readU32(data + 0x24); + if (infoOffset + infoSize > size || dataOffset + 8 > size) return false; + return N3DSAudio_parseBcwavInfo(data + infoOffset, infoSize, dataOffset, size, out); +} + +static bool N3DSAudio_parseBcwavFile(const char* path, N3DSBcwav* out) { + if (path == NULL || out == NULL) return false; + + bool success = false; + FILE* file = fopen(path, "rb"); + if (file == NULL) return false; + N3DSAudio_configureFileBuffer(file); + + uint32_t fileSize = 0; + uint8_t header[0x28]; + uint8_t* infoData = NULL; + + if (!N3DSAudio_getFileSize(file, &fileSize)) goto cleanup; + if (fileSize < 0x40) goto cleanup; + if (fread(header, 1, sizeof(header), file) != sizeof(header)) goto cleanup; + if (memcmp(header, "CWAV", 4) != 0) goto cleanup; + + uint32_t infoOffset = N3DSAudio_readU32(header + 0x18); + uint32_t infoSize = N3DSAudio_readU32(header + 0x1C); + uint32_t dataOffset = N3DSAudio_readU32(header + 0x24); + if (infoSize == 0 || infoOffset + infoSize > fileSize || dataOffset + 8 > fileSize) goto cleanup; + + infoData = safeMalloc(infoSize); + if (fseek(file, (long) infoOffset, SEEK_SET) != 0) goto cleanup; + if (fread(infoData, 1, infoSize, file) != infoSize) goto cleanup; + + success = N3DSAudio_parseBcwavInfo(infoData, infoSize, dataOffset, fileSize, out); + +cleanup: + free(infoData); + fclose(file); + return success; +} + +static void N3DSAudio_decodeFrame(const N3DSBcwavChannel* channel, const uint8_t* frame, int16_t* hist1, int16_t* hist2, int16_t outSamples[14]) { + int predictor = frame[0] >> 4; + int scale = 1 << (frame[0] & 0x0F); + int coef1 = (int16_t) channel->coefs[predictor * 2 + 0]; + int coef2 = (int16_t) channel->coefs[predictor * 2 + 1]; + + int sampleIndex = 0; + for (int byteIndex = 1; byteIndex < 8; ++byteIndex) { + int hi = N3DSAudio_signExtend4(frame[byteIndex] >> 4); + int lo = N3DSAudio_signExtend4(frame[byteIndex] & 0x0F); + int nibbles[2] = { hi, lo }; + repeat(2, nibbleIndex) { + int sample = (nibbles[nibbleIndex] * scale) << 11; + sample += 1024 + coef1 * (*hist1) + coef2 * (*hist2); + sample >>= 11; + if (sample > 32767) sample = 32767; + if (sample < -32768) sample = -32768; + outSamples[sampleIndex++] = (int16_t) sample; + *hist2 = *hist1; + *hist1 = (int16_t) sample; + } + } +} + +static N3DSDspContext N3DSAudio_contextForSample(const N3DSBcwavChannel* channel, const N3DSStreamDecodeState* state, uint32_t sampleIndex) { + N3DSDspContext context; + if (channel != NULL && sampleIndex == 0) { + context = channel->startContext; + } else { + context.predictorScale = state->predictorScale; + context.yn1 = state->hist1; + context.yn2 = state->hist2; + } + return context; +} + +static int16_t* N3DSAudio_decodeBcwavToPcm(const uint8_t* fileData, const N3DSBcwav* bcwav) { + size_t totalSamples = (size_t) bcwav->sampleCount * bcwav->channelCount; + int16_t* pcm = linearAlloc(totalSamples * sizeof(int16_t)); + if (pcm == NULL) return NULL; + + int16_t frameSamples[2][14]; + int16_t hist1[2] = { + bcwav->channels[0].startContext.yn1, + bcwav->channels[1].startContext.yn1, + }; + int16_t hist2[2] = { + bcwav->channels[0].startContext.yn2, + bcwav->channels[1].startContext.yn2, + }; + + uint32_t frameCount = (bcwav->sampleCount + 13u) / 14u; + uint32_t sampleCursor = 0; + repeat(frameCount, frameIndex) { + repeat(bcwav->channelCount, channelIndex) { + const uint8_t* frame = fileData + bcwav->channels[channelIndex].dataOffset + frameIndex * 8u; + N3DSAudio_decodeFrame(&bcwav->channels[channelIndex], frame, &hist1[channelIndex], &hist2[channelIndex], frameSamples[channelIndex]); + } + + uint32_t samplesThisFrame = bcwav->sampleCount - sampleCursor; + if (samplesThisFrame > 14u) samplesThisFrame = 14u; + repeat(samplesThisFrame, sampleIndex) { + if (bcwav->channelCount == 1) { + pcm[sampleCursor + sampleIndex] = frameSamples[0][sampleIndex]; + } else { + size_t outIndex = ((size_t) sampleCursor + sampleIndex) * 2u; + pcm[outIndex + 0] = frameSamples[0][sampleIndex]; + pcm[outIndex + 1] = frameSamples[1][sampleIndex]; + } + } + sampleCursor += samplesThisFrame; + } + + DSP_FlushDataCache(pcm, totalSamples * sizeof(int16_t)); + return pcm; +} + +static char* N3DSAudio_tryResolveGeneratedSoundPath(N3DSAudioSystem* audio, const Sound* sound, int32_t soundIndex) { + char candidate[512]; + bool isMusic = N3DSAudio_soundLooksLikeMusic(sound); + char baseName[256]; + + if (isMusic) { + if (sound != NULL && N3DSAudio_extractBaseNameNoExt(sound->file, baseName, sizeof(baseName))) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName); + char* resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + + if (sound != NULL && N3DSAudio_extractBaseNameNoExt(sound->name, baseName, sizeof(baseName))) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName); + char* resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + } + + if (sound != NULL && sound->name != NULL && sound->name[0] != '\0') { + if (!isMusic) { + snprintf(candidate, sizeof(candidate), "audio/%s.bcwav", sound->name); + char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + + snprintf(candidate, sizeof(candidate), "%s.bcwav", sound->name); + resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + } + + if (!isMusic && sound != NULL) { + if (N3DSAudio_extractBaseNameNoExt(sound->file, baseName, sizeof(baseName))) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName); + char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + if (N3DSAudio_extractBaseNameNoExt(sound->name, baseName, sizeof(baseName))) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName); + char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + } + + if (!isMusic && soundIndex >= 0) { + snprintf(candidate, sizeof(candidate), "audio/sound_%05ld.bcwav", (long) soundIndex); + char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + + snprintf(candidate, sizeof(candidate), "sound_%05ld.bcwav", (long) soundIndex); + resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + + snprintf(candidate, sizeof(candidate), "audio/sound_%ld.bcwav", (long) soundIndex); + resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + + snprintf(candidate, sizeof(candidate), "sound_%ld.bcwav", (long) soundIndex); + resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + + if (!isMusic && sound != NULL && sound->audioFile >= 0) { + snprintf(candidate, sizeof(candidate), "audio/audo_%05ld.bcwav", (long) sound->audioFile); + char* resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + + snprintf(candidate, sizeof(candidate), "audo_%05ld.bcwav", (long) sound->audioFile); + resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + + if (sound != NULL && sound->name != NULL && sound->name[0] != '\0') { + char baseName[256]; + if (N3DSAudio_extractBaseNameNoExt(sound->name, baseName, sizeof(baseName))) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName); + char* resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, candidate) : N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + } + + return NULL; +} + +static char* N3DSAudio_resolveAudioFilePath(N3DSAudioSystem* audio, const char* name) { + if (name == NULL || name[0] == '\0') return NULL; + + char* resolved = N3DSAudio_resolveStreamPath(audio, name); + if (resolved != NULL) return resolved; + + char candidate[512]; + if (strchr(name, '.') == NULL) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", name); + return N3DSAudio_tryResolveMusicCandidate(audio, candidate); + } + + char baseName[256]; + if (N3DSAudio_extractBaseNameNoExt(name, baseName, sizeof(baseName))) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName); + resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + + return NULL; +} + +static char* N3DSAudio_resolveSoundPath(N3DSAudioSystem* audio, const Sound* sound, int32_t soundIndex) { + const char* name = sound != NULL ? sound->file : NULL; + if (name == NULL || name[0] == '\0') return NULL; + bool isMusic = N3DSAudio_soundLooksLikeMusic(sound); + + char candidate[512]; + char* resolved = NULL; + + if (strchr(name, '.') == NULL) { + if (isMusic) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", name); + resolved = N3DSAudio_tryResolveMusicCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } else { + snprintf(candidate, sizeof(candidate), "audio/%s.bcwav", name); + resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + + snprintf(candidate, sizeof(candidate), "%s.bcwav", name); + resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + + resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + return NULL; + } + + resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, name) : N3DSAudio_tryResolveSfxCandidate(audio, name); + if (resolved != NULL) return resolved; + + const char* slash = strrchr(name, '/'); + const char* base = slash != NULL ? slash + 1 : name; + const char* dot = strrchr(base, '.'); + if (dot != NULL && dot > base) { + size_t dirLen = slash != NULL ? (size_t) (slash - name + 1) : 0; + size_t baseLen = (size_t) (dot - base); + + if (dirLen + baseLen + 7 < sizeof(candidate)) { + memcpy(candidate, name, dirLen); + memcpy(candidate + dirLen, base, baseLen); + memcpy(candidate + dirLen + baseLen, ".bcwav", 7); + resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, candidate) : N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + + if (!isMusic && baseLen + 13 < sizeof(candidate)) { + memcpy(candidate, "audio/", 6); + memcpy(candidate + 6, base, baseLen); + memcpy(candidate + 6 + baseLen, ".bcwav", 7); + resolved = N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + + if (baseLen + 7 < sizeof(candidate)) { + memcpy(candidate, base, baseLen); + memcpy(candidate + baseLen, ".bcwav", 7); + resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, candidate) : N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + } + + if (sound != NULL && sound->name != NULL && sound->name[0] != '\0') { + char baseName[256]; + if (N3DSAudio_extractBaseNameNoExt(sound->name, baseName, sizeof(baseName))) { + snprintf(candidate, sizeof(candidate), "%s.bcwav", baseName); + resolved = isMusic ? N3DSAudio_tryResolveMusicCandidate(audio, candidate) : N3DSAudio_tryResolveSfxCandidate(audio, candidate); + if (resolved != NULL) return resolved; + } + } + + (void) soundIndex; + return NULL; +} + +static N3DSSoundInstance* N3DSAudio_findInstanceById(N3DSAudioSystem* audio, int32_t instanceId) { + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].instanceId == instanceId) { + return &audio->instances[i]; + } + } + return NULL; +} + +static bool N3DSAudio_isInstanceId(int32_t soundOrInstance) { + return soundOrInstance >= N3DS_SOUND_INSTANCE_ID_BASE && + soundOrInstance < N3DS_SOUND_INSTANCE_ID_BASE + N3DS_MAX_SOUND_INSTANCES; +} + +static int32_t N3DSAudio_streamSlotFromSoundIndex(int32_t soundIndex) { + int32_t slot = soundIndex - N3DS_AUDIO_STREAM_INDEX_BASE; + return (slot >= 0 && slot < N3DS_MAX_STREAMS) ? slot : -1; +} + +static int N3DSAudio_acquireChannel(N3DSAudioSystem* audio) { + if (audio == NULL) return -1; + + uint32_t start = (uint32_t) audio->nextNdspChannel % N3DS_MAX_NDSP_CHANNELS; + repeat(N3DS_MAX_NDSP_CHANNELS, offset) { + uint32_t channel = (start + offset) % N3DS_MAX_NDSP_CHANNELS; + if (!audio->ndspChannelInUse[channel]) { + audio->ndspChannelInUse[channel] = true; + audio->nextNdspChannel = (uint8_t) ((channel + 1u) % N3DS_MAX_NDSP_CHANNELS); + return (int) channel; + } + } + return -1; +} + +static void N3DSAudio_resetChannelPlayback(int channelId) { + if (channelId < 0 || channelId >= N3DS_MAX_NDSP_CHANNELS) return; + ndspChnSetPaused(channelId, true); + ndspChnWaveBufClear(channelId); + ndspChnReset(channelId); + ndspChnSetInterp(channelId, NDSP_INTERP_LINEAR); +} + +static void N3DSAudio_releaseChannel(N3DSAudioSystem* audio, int channelId) { + if (channelId < 0 || channelId >= N3DS_MAX_NDSP_CHANNELS) return; + N3DSAudio_resetChannelPlayback(channelId); + audio->ndspChannelInUse[channelId] = false; +} + +static uint32_t N3DSAudio_adpcmByteOffsetForSample(uint32_t sampleIndex) { + return (sampleIndex / 14u) * 8u; +} + +static bool N3DSAudio_isFrameAlignedSample(uint32_t sampleIndex) { + return (sampleIndex % 14u) == 0; +} + +static float N3DSAudio_sanitizeGain(float gain) { + if (!isfinite(gain)) return 1.0f; + if (gain < 0.0f) return 0.0f; + if (gain > 1.0f) return 1.0f; + return gain; +} + +static float N3DSAudio_sanitizePitch(float pitch) { + if (!isfinite(pitch) || pitch <= 0.0f) return 1.0f; + if (pitch < 0.0625f) return 0.0625f; + if (pitch > 4.0f) return 4.0f; + return pitch; +} + +static void N3DSAudio_refreshNonStreamReleaseTick(N3DSSoundInstance* inst) { + if (inst == NULL || inst->isStream || inst->loop || inst->sampleRate == 0 || inst->sampleCount == 0) { + return; + } + + float pitch = N3DSAudio_sanitizePitch(inst->pitch); + double durationSeconds = ((double) inst->sampleCount / (double) inst->sampleRate) / (double) pitch; + if (!(durationSeconds > 0.0)) durationSeconds = 0.001; + + u64 durationTicks = (u64) (durationSeconds * (double) SYSCLOCK_ARM11); + if (durationTicks == 0) durationTicks = 1; + inst->playbackReleaseTick = inst->playbackStartTick + durationTicks; +} + +static bool N3DSAudio_nonStreamPlaybackFinished(const N3DSSoundInstance* inst, u64 nowTick) { + if (inst == NULL || !inst->active || inst->isStream || inst->loop || inst->paused) return false; + if (nowTick < inst->playbackReleaseTick) return false; + if (inst->waveBufs[0].status != NDSP_WBUF_DONE) return false; + if (ndspChnIsPlaying(inst->channelId)) return false; + if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) { + if (inst->secondaryWaveBufs[0].status != NDSP_WBUF_DONE) return false; + if (ndspChnIsPlaying(inst->secondaryChannelId)) return false; + } + return true; +} + +static void N3DSAudio_applyMix(N3DSAudioSystem* audio, N3DSSoundInstance* inst) { + float mix[12]; + memset(mix, 0, sizeof(mix)); + inst->gain = N3DSAudio_sanitizeGain(inst->gain); + if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) { + mix[0] = inst->gain * audio->masterGain; + ndspChnSetMix(inst->channelId, mix); + + memset(mix, 0, sizeof(mix)); + mix[1] = inst->gain * audio->masterGain; + ndspChnSetMix(inst->secondaryChannelId, mix); + } else { + mix[0] = inst->gain * audio->masterGain; + mix[1] = inst->gain * audio->masterGain; + ndspChnSetMix(inst->channelId, mix); + } + + float pitch = N3DSAudio_sanitizePitch(inst->pitch); + float rate = inst->baseRate * pitch; + if (!isfinite(rate) || rate < 4000.0f) rate = 4000.0f; + if (rate > 96000.0f) rate = 96000.0f; + inst->pitch = pitch; + ndspChnSetRate(inst->channelId, rate); + if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) { + ndspChnSetRate(inst->secondaryChannelId, rate); + } +} + +static void N3DSAudio_resetInstanceWaveBufState(N3DSSoundInstance* inst) { + if (inst == NULL) return; + memset(inst->waveBufs, 0, sizeof(inst->waveBufs)); + memset(inst->secondaryWaveBufs, 0, sizeof(inst->secondaryWaveBufs)); + memset(inst->adpcmStates, 0, sizeof(inst->adpcmStates)); + memset(inst->bufferStartSample, 0, sizeof(inst->bufferStartSample)); +} + +static void N3DSAudio_rebuildInstanceChannels(N3DSAudioSystem* audio, N3DSSoundInstance* inst) { + if (audio == NULL || inst == NULL || inst->channelId < 0) return; + + N3DSAudio_resetChannelPlayback(inst->channelId); + if (inst->secondaryChannelId >= 0) { + N3DSAudio_resetChannelPlayback(inst->secondaryChannelId); + } + + if (inst->useNativeAdpcm) { + ndspChnSetFormat(inst->channelId, NDSP_FORMAT_MONO_ADPCM); + ndspChnSetAdpcmCoefs(inst->channelId, inst->bcwav.channels[0].coefs); + if (inst->secondaryChannelId >= 0) { + ndspChnSetFormat(inst->secondaryChannelId, NDSP_FORMAT_MONO_ADPCM); + ndspChnSetAdpcmCoefs(inst->secondaryChannelId, inst->bcwav.channels[1].coefs); + } + } else { + ndspChnSetFormat(inst->channelId, inst->channelCount == 2 ? NDSP_FORMAT_STEREO_PCM16 : NDSP_FORMAT_MONO_PCM16); + } + + N3DSAudio_applyMix(audio, inst); + ndspChnSetPaused(inst->channelId, inst->paused); + if (inst->secondaryChannelId >= 0) { + ndspChnSetPaused(inst->secondaryChannelId, inst->paused); + } +} + +static void N3DSAudio_releaseCachedSound(N3DSCachedSound* cachedSound) { + if (cachedSound == NULL) return; + free(cachedSound->path); + if (cachedSound->blobOwned) free(cachedSound->blob); + if (cachedSound->nativeBlob != NULL) linearFree(cachedSound->nativeBlob); + memset(cachedSound, 0, sizeof(*cachedSound)); +} + +static uint8_t* N3DSAudio_getCachedNativeBlob(N3DSCachedSound* cachedSound) { + if (cachedSound == NULL || !cachedSound->available || cachedSound->blob == NULL || cachedSound->blobSize == 0) return NULL; + if (cachedSound->nativeBlob != NULL) return cachedSound->nativeBlob; + + cachedSound->nativeBlob = N3DSAudio_cloneBlobToLinear(cachedSound->blob, cachedSound->blobSize); + return cachedSound->nativeBlob; +} + +static bool N3DSAudio_tryCacheSoundBlob(N3DSAudioSystem* audio, int32_t soundIndex) { + if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->cachedSoundCount) return false; + N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex]; + if (cachedSound->attempted) return cachedSound->available; + + cachedSound->attempted = true; + + DataWin* dw = audio->base.audioGroups[0]; + Sound* sound = &dw->sond.sounds[soundIndex]; + if (!N3DSAudio_shouldPreloadSound(sound)) return false; + + const uint8_t* packedBlob = NULL; + uint32_t packedBlobSize = 0; + const N3DSPackedSoundBankEntry* packedEntry = N3DSAudio_getPackedSoundEntry(audio, soundIndex); + if (!N3DSAudio_soundLooksLikeMusic(sound) && + packedEntry != NULL && + !N3DSAudio_packedSoundEntryIsPcm16(packedEntry) && + N3DSAudio_getPackedSoundBlob(audio, soundIndex, &packedBlob, &packedBlobSize)) { + N3DSBcwav bcwav; + if (!N3DSAudio_parseBcwavBlob(packedBlob, packedBlobSize, &bcwav)) return false; + if (!N3DSAudio_shouldPrewarmCachedSound(sound, &bcwav)) return false; + + cachedSound->available = true; + cachedSound->headerAttempted = true; + cachedSound->headerAvailable = true; + cachedSound->blobOwned = false; + cachedSound->blob = (uint8_t*) packedBlob; + cachedSound->blobSize = packedBlobSize; + cachedSound->bcwav = bcwav; + return true; + } + + const char* resolvedPath = N3DSAudio_getResolvedCachedSoundPath(audio, soundIndex, sound); + if (resolvedPath == NULL) return false; + char* path = safeStrdup(resolvedPath); + + uint8_t* blob = NULL; + uint32_t blobSize = 0; + if (!N3DSAudio_readFileFully(path, &blob, &blobSize)) { + free(path); + return false; + } + if (audio->cachedSoundBytes + blobSize > audio->maxCachedSoundBytes) { + free(blob); + free(path); + return false; + } + + N3DSBcwav bcwav; + if (!N3DSAudio_parseBcwavBlob(blob, blobSize, &bcwav)) { + free(blob); + free(path); + return false; + } + if (!N3DSAudio_shouldPrewarmCachedSound(sound, &bcwav)) { + free(blob); + free(path); + return false; + } + + cachedSound->available = true; + cachedSound->headerAttempted = true; + cachedSound->headerAvailable = true; + cachedSound->blobOwned = true; + cachedSound->path = path; + cachedSound->blob = blob; + cachedSound->blobSize = blobSize; + cachedSound->bcwav = bcwav; + audio->cachedSoundBytes += blobSize; + return true; +} + +static N3DSCachedSound* N3DSAudio_getCachedSoundIfAvailable(N3DSAudioSystem* audio, int32_t soundIndex) { + if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->cachedSoundCount) return NULL; + return audio->cachedSounds[soundIndex].available ? &audio->cachedSounds[soundIndex] : NULL; +} + +static const char* N3DSAudio_getResolvedCachedSoundPath(N3DSAudioSystem* audio, int32_t soundIndex, const Sound* sound) { + if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->cachedSoundCount) return NULL; + + N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex]; + if (cachedSound->path != NULL) return cachedSound->path; + if (cachedSound->pathAttempted) return NULL; + + cachedSound->pathAttempted = true; + cachedSound->path = N3DSAudio_resolveSoundPath(audio, sound, soundIndex); + if (cachedSound->path == NULL) { + cachedSound->path = N3DSAudio_tryResolveGeneratedSoundPath(audio, sound, soundIndex); + } + return cachedSound->path; +} + +static bool N3DSAudio_getCachedSoundHeader(N3DSAudioSystem* audio, int32_t soundIndex, const Sound* sound, N3DSBcwav* out) { + if (audio == NULL || soundIndex < 0 || (uint32_t) soundIndex >= audio->cachedSoundCount) return false; + + N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex]; + if (cachedSound->available || cachedSound->headerAvailable) { + if (out != NULL) *out = cachedSound->bcwav; + return true; + } + if (cachedSound->headerAttempted) return false; + + cachedSound->headerAttempted = true; + const uint8_t* packedBlob = NULL; + uint32_t packedBlobSize = 0; + const N3DSPackedSoundBankEntry* packedEntry = N3DSAudio_getPackedSoundEntry(audio, soundIndex); + if (!N3DSAudio_soundLooksLikeMusic(sound) && + packedEntry != NULL && + !N3DSAudio_packedSoundEntryIsPcm16(packedEntry) && + N3DSAudio_getPackedSoundBlob(audio, soundIndex, &packedBlob, &packedBlobSize)) { + if (!N3DSAudio_parseBcwavBlob(packedBlob, packedBlobSize, &cachedSound->bcwav)) return false; + } else { + const char* path = N3DSAudio_getResolvedCachedSoundPath(audio, soundIndex, sound); + if (path == NULL) return false; + if (!N3DSAudio_parseBcwavFile(path, &cachedSound->bcwav)) return false; + } + + cachedSound->headerAvailable = true; + if (out != NULL) *out = cachedSound->bcwav; + return true; +} + +static void N3DSAudio_prewarmSoundCache(N3DSAudioSystem* audio, uint32_t maxSounds, uint32_t byteBudget) { + if (audio == NULL || audio->cachedSounds == NULL || audio->cachedSoundCount == 0) return; + if (maxSounds == 0 || byteBudget == 0) return; + + uint32_t loadedSounds = 0; + uint32_t consumedBytes = 0; + uint32_t scanned = 0; + + while (scanned < audio->cachedSoundCount && loadedSounds < maxSounds && consumedBytes < byteBudget) { + uint32_t soundIndex = audio->cachedSoundPrewarmCursor; + audio->cachedSoundPrewarmCursor++; + if (audio->cachedSoundPrewarmCursor >= audio->cachedSoundCount) { + audio->cachedSoundPrewarmCursor = 0; + } + scanned++; + + N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex]; + if (cachedSound->attempted) continue; + + uint32_t bytesBefore = audio->cachedSoundBytes; + if (N3DSAudio_tryCacheSoundBlob(audio, (int32_t) soundIndex)) { + loadedSounds++; + consumedBytes += audio->cachedSoundBytes - bytesBefore; + } + } +} + +static void N3DSAudio_requestCachePrewarmLocked(N3DSAudioSystem* audio, uint32_t maxSounds, uint32_t byteBudget) { + if (audio == NULL || audio->cachedSounds == NULL || audio->cachedSoundCount == 0) return; + if (maxSounds == 0 || byteBudget == 0) return; + if (!audio->workerEnabled) { + N3DSAudio_prewarmSoundCache(audio, maxSounds, byteBudget); + return; + } + + if (audio->pendingPrewarmSoundCount < maxSounds) { + audio->pendingPrewarmSoundCount = maxSounds; + } + if (audio->pendingPrewarmByteBudget < byteBudget) { + audio->pendingPrewarmByteBudget = byteBudget; + } + if (N3DSAudio_hasActiveStreamPlaybackLocked(audio)) return; + LightEvent_Signal(&audio->workerEvent); +} + +#if N3DS_EAGER_SFX_PRELOAD +static void N3DSAudio_preloadSounds(N3DSAudioSystem* audio, DataWin* dataWin) { + if (audio == NULL || dataWin == NULL || dataWin->sond.count == 0) return; + + audio->cachedSounds = safeCalloc(dataWin->sond.count, sizeof(N3DSCachedSound)); + audio->cachedSoundCount = dataWin->sond.count; + + uint32_t loadedCount = 0; + repeat(dataWin->sond.count, i) { + if (N3DSAudio_tryCacheSoundBlob(audio, (int32_t) i)) loadedCount++; + } + + fprintf( + stderr, + "N3DSAudio: preloaded %lu/%lu likely-SFX blobs (%lu bytes, cap=%lu)\n", + (unsigned long) loadedCount, + (unsigned long) dataWin->sond.count, + (unsigned long) audio->cachedSoundBytes, + (unsigned long) audio->maxCachedSoundBytes + ); +} +#endif + +static N3DSStreamEntry* N3DSAudio_getActiveStreamEntry(N3DSAudioSystem* audio, int32_t soundIndex) { + int32_t slot = N3DSAudio_streamSlotFromSoundIndex(soundIndex); + if (slot < 0 || !audio->streams[slot].active) return NULL; + return &audio->streams[slot]; +} + +static bool N3DSAudio_canUseNativeAdpcmPlayback(const N3DSBcwav* bcwav, bool loop) { +#if N3DS_FORCE_PCM_BCWAV_PLAYBACK + (void) bcwav; + (void) loop; + return false; +#else + if (bcwav == NULL || bcwav->sampleRate == 0 || bcwav->channelCount == 0 || bcwav->channelCount > 2) return false; + if (!loop) return true; + return !bcwav->loop || + bcwav->loopEnd <= bcwav->loopStart || + N3DSAudio_isFrameAlignedSample(bcwav->loopStart); +#endif +} + +static void N3DSAudio_cleanupPartialStreamResources(N3DSAudioSystem* audio, N3DSSoundInstance* inst) { + if (inst == NULL) return; + + if (inst->streamFile != NULL) { + fclose(inst->streamFile); + inst->streamFile = NULL; + } + repeat(inst->channelCount, channelIndex) { + repeat(N3DS_STREAM_BUFFER_COUNT, bufferIndex) { + if (inst->streamAdpcm[channelIndex][bufferIndex] != NULL) { + linearFree(inst->streamAdpcm[channelIndex][bufferIndex]); + inst->streamAdpcm[channelIndex][bufferIndex] = NULL; + } + } + } + repeat(N3DS_STREAM_BUFFER_COUNT, i) { + if (inst->streamPcm[i] != NULL) { + linearFree(inst->streamPcm[i]); + inst->streamPcm[i] = NULL; + } + } + if (inst->ownsStreamBlob && inst->streamBlob != NULL) { + if (inst->streamBlobLinear) linearFree((void*) inst->streamBlob); + else free((void*) inst->streamBlob); + } + inst->streamBlob = NULL; + inst->streamBlobSize = 0; + inst->ownsStreamBlob = false; + inst->streamBlobLinear = false; + + N3DSAudio_releaseChannel(audio, inst->channelId); + N3DSAudio_releaseChannel(audio, inst->secondaryChannelId); + inst->channelId = -1; + inst->secondaryChannelId = -1; + + inst->isStream = false; + inst->useNativeAdpcm = false; + inst->streamFinished = false; + inst->sampleRate = 0; + inst->sampleCount = 0; + inst->channelCount = 0; + inst->baseRate = 0.0f; + memset(&inst->bcwav, 0, sizeof(inst->bcwav)); + N3DSAudio_resetInstanceWaveBufState(inst); +} + +static bool N3DSAudio_startNativeAdpcmPlayback( + N3DSAudioSystem* audio, + N3DSSoundInstance* inst, + const uint8_t* blob, + uint32_t blobSize, + const N3DSBcwav* bcwav +) { + if (audio == NULL || inst == NULL || blob == NULL || bcwav == NULL) return false; + + inst->channelId = N3DSAudio_acquireChannel(audio); + if (inst->channelId < 0) return false; + if (bcwav->channelCount == 2) { + inst->secondaryChannelId = N3DSAudio_acquireChannel(audio); + if (inst->secondaryChannelId < 0) return false; + } + + uint8_t* nativeBlob = N3DSAudio_cloneBlobToLinear(blob, blobSize); + if (nativeBlob == NULL) return false; + + inst->useNativeAdpcm = true; + inst->streamBlob = nativeBlob; + inst->streamBlobSize = blobSize; + inst->ownsStreamBlob = true; + inst->streamBlobLinear = true; + inst->bcwav = *bcwav; + inst->sampleRate = bcwav->sampleRate; + inst->sampleCount = bcwav->sampleCount; + inst->channelCount = bcwav->channelCount; + inst->baseRate = (float) bcwav->sampleRate; + inst->playbackStartTick = svcGetSystemTick(); + N3DSAudio_refreshNonStreamReleaseTick(inst); + + N3DSAudio_rebuildInstanceChannels(audio, inst); + return N3DSAudio_primeNativeAdpcm(inst); +} + +static bool N3DSAudio_startOwnedLinearNativeAdpcmPlayback( + N3DSAudioSystem* audio, + N3DSSoundInstance* inst, + const uint8_t* linearBlob, + uint32_t blobSize, + const N3DSBcwav* bcwav +) { + if (audio == NULL || inst == NULL || linearBlob == NULL || bcwav == NULL) return false; + + inst->channelId = N3DSAudio_acquireChannel(audio); + if (inst->channelId < 0) return false; + if (bcwav->channelCount == 2) { + inst->secondaryChannelId = N3DSAudio_acquireChannel(audio); + if (inst->secondaryChannelId < 0) return false; + } + + inst->useNativeAdpcm = true; + inst->streamBlob = linearBlob; + inst->streamBlobSize = blobSize; + inst->ownsStreamBlob = true; + inst->streamBlobLinear = true; + inst->bcwav = *bcwav; + inst->sampleRate = bcwav->sampleRate; + inst->sampleCount = bcwav->sampleCount; + inst->channelCount = bcwav->channelCount; + inst->baseRate = (float) bcwav->sampleRate; + inst->playbackStartTick = svcGetSystemTick(); + N3DSAudio_refreshNonStreamReleaseTick(inst); + + N3DSAudio_rebuildInstanceChannels(audio, inst); + return N3DSAudio_primeNativeAdpcm(inst); +} + +static bool N3DSAudio_startOwnedPcmPlayback( + N3DSAudioSystem* audio, + N3DSSoundInstance* inst, + int16_t* pcm, + uint32_t sampleRate, + uint32_t sampleCount, + uint8_t channelCount, + bool loop +) { + if (audio == NULL || inst == NULL || pcm == NULL) return false; + if (sampleRate == 0 || sampleCount == 0 || channelCount == 0 || channelCount > 2) return false; + if (!loop) N3DSAudio_softenPcmTail(pcm, sampleCount, channelCount); + + inst->channelId = N3DSAudio_acquireChannel(audio); + if (inst->channelId < 0) return false; + ndspChnSetInterp(inst->channelId, NDSP_INTERP_LINEAR); + + inst->pcmData = pcm; + inst->sampleRate = sampleRate; + inst->sampleCount = sampleCount; + inst->channelCount = channelCount; + inst->baseRate = (float) sampleRate; + inst->playbackStartTick = svcGetSystemTick(); + N3DSAudio_refreshNonStreamReleaseTick(inst); + N3DSAudio_resetInstanceWaveBufState(inst); + inst->waveBufs[0].data_pcm16 = pcm; + inst->waveBufs[0].nsamples = sampleCount; + inst->waveBufs[0].looping = loop; + N3DSAudio_rebuildInstanceChannels(audio, inst); + ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[0]); + return true; +} + +static bool N3DSAudio_startSharedNativeAdpcmPlayback( + N3DSAudioSystem* audio, + N3DSSoundInstance* inst, + const uint8_t* linearBlob, + uint32_t blobSize, + const N3DSBcwav* bcwav +) { + if (audio == NULL || inst == NULL || linearBlob == NULL || bcwav == NULL) return false; + + inst->channelId = N3DSAudio_acquireChannel(audio); + if (inst->channelId < 0) return false; + if (bcwav->channelCount == 2) { + inst->secondaryChannelId = N3DSAudio_acquireChannel(audio); + if (inst->secondaryChannelId < 0) return false; + } + + inst->useNativeAdpcm = true; + inst->streamBlob = linearBlob; + inst->streamBlobSize = blobSize; + inst->ownsStreamBlob = false; + inst->streamBlobLinear = true; + inst->bcwav = *bcwav; + inst->sampleRate = bcwav->sampleRate; + inst->sampleCount = bcwav->sampleCount; + inst->channelCount = bcwav->channelCount; + inst->baseRate = (float) bcwav->sampleRate; + inst->playbackStartTick = svcGetSystemTick(); + N3DSAudio_refreshNonStreamReleaseTick(inst); + + N3DSAudio_rebuildInstanceChannels(audio, inst); + return N3DSAudio_primeNativeAdpcm(inst); +} + +static bool N3DSAudio_primeNativeAdpcm(N3DSSoundInstance* inst) { + if (!inst->useNativeAdpcm || inst->streamBlob == NULL) return false; + + uint32_t loopStart = 0; + uint32_t loopEnd = inst->bcwav.sampleCount; + bool useLoopRegion = inst->loop && + inst->bcwav.loop && + inst->bcwav.loopStart > 0 && + inst->bcwav.loopEnd > inst->bcwav.loopStart; + if (useLoopRegion) { + loopStart = inst->bcwav.loopStart; + loopEnd = inst->bcwav.loopEnd; + } + + N3DSAudio_resetInstanceWaveBufState(inst); + inst->bufferStartSample[0] = 0; + inst->bufferStartSample[1] = loopStart; + + inst->adpcmStates[0].index = inst->bcwav.channels[0].startContext.predictorScale; + inst->adpcmStates[0].history0 = inst->bcwav.channels[0].startContext.yn1; + inst->adpcmStates[0].history1 = inst->bcwav.channels[0].startContext.yn2; + + inst->waveBufs[0].data_adpcm = (uint8_t*) inst->streamBlob + inst->bcwav.channels[0].dataOffset; + inst->waveBufs[0].nsamples = useLoopRegion ? loopStart : inst->bcwav.sampleCount; + inst->waveBufs[0].adpcm_data = &inst->adpcmStates[0]; + inst->waveBufs[0].looping = inst->loop && !useLoopRegion; + + if (inst->secondaryChannelId >= 0) { + inst->adpcmStates[1].index = inst->bcwav.channels[1].startContext.predictorScale; + inst->adpcmStates[1].history0 = inst->bcwav.channels[1].startContext.yn1; + inst->adpcmStates[1].history1 = inst->bcwav.channels[1].startContext.yn2; + + inst->secondaryWaveBufs[0].data_adpcm = (uint8_t*) inst->streamBlob + inst->bcwav.channels[1].dataOffset; + inst->secondaryWaveBufs[0].nsamples = inst->waveBufs[0].nsamples; + inst->secondaryWaveBufs[0].adpcm_data = &inst->adpcmStates[1]; + inst->secondaryWaveBufs[0].looping = inst->waveBufs[0].looping; + } + + if (useLoopRegion) { + inst->adpcmStates[2].index = inst->bcwav.channels[0].loopContext.predictorScale; + inst->adpcmStates[2].history0 = inst->bcwav.channels[0].loopContext.yn1; + inst->adpcmStates[2].history1 = inst->bcwav.channels[0].loopContext.yn2; + + inst->waveBufs[1].data_adpcm = (uint8_t*) inst->streamBlob + inst->bcwav.channels[0].dataOffset + N3DSAudio_adpcmByteOffsetForSample(loopStart); + inst->waveBufs[1].nsamples = loopEnd - loopStart; + inst->waveBufs[1].adpcm_data = &inst->adpcmStates[2]; + inst->waveBufs[1].looping = true; + + if (inst->secondaryChannelId >= 0) { + inst->adpcmStates[3].index = inst->bcwav.channels[1].loopContext.predictorScale; + inst->adpcmStates[3].history0 = inst->bcwav.channels[1].loopContext.yn1; + inst->adpcmStates[3].history1 = inst->bcwav.channels[1].loopContext.yn2; + + inst->secondaryWaveBufs[1].data_adpcm = (uint8_t*) inst->streamBlob + inst->bcwav.channels[1].dataOffset + N3DSAudio_adpcmByteOffsetForSample(loopStart); + inst->secondaryWaveBufs[1].nsamples = inst->waveBufs[1].nsamples; + inst->secondaryWaveBufs[1].adpcm_data = &inst->adpcmStates[3]; + inst->secondaryWaveBufs[1].looping = true; + } + } + + DSP_FlushDataCache((void*) inst->streamBlob, inst->streamBlobSize); + ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[0]); + if (inst->secondaryChannelId >= 0) { + ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[0]); + } + + if (useLoopRegion) { + ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[1]); + if (inst->secondaryChannelId >= 0) { + ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[1]); + } + } + return true; +} + +static void N3DSAudio_releaseInstance(N3DSAudioSystem* audio, N3DSSoundInstance* inst) { + if (!inst->active) return; + if (inst->isStream) { + N3DSDebugLog_event( + "audio_stop", + "instance=%ld sound=%ld native=%d sample=%lu/%lu ch=%ld/%ld", + (long) inst->instanceId, + (long) inst->soundIndex, + inst->useNativeAdpcm ? 1 : 0, + (unsigned long) inst->currentSample, + (unsigned long) inst->sampleCount, + (long) inst->channelId, + (long) inst->secondaryChannelId + ); + } + if (audio != NULL && inst->isStream) { + N3DSAudio_cancelAsyncFillLocked(audio, inst); + } + + N3DSAudio_releaseChannel(audio, inst->channelId); + N3DSAudio_releaseChannel(audio, inst->secondaryChannelId); + + if (inst->pcmData != NULL) linearFree(inst->pcmData); + repeat(inst->channelCount, channelIndex) { + repeat(N3DS_STREAM_BUFFER_COUNT, bufferIndex) { + if (inst->streamAdpcm[channelIndex][bufferIndex] != NULL) { + linearFree(inst->streamAdpcm[channelIndex][bufferIndex]); + } + } + } + repeat(N3DS_STREAM_BUFFER_COUNT, i) { + if (inst->streamPcm[i] != NULL) linearFree(inst->streamPcm[i]); + } + if (inst->streamFile != NULL) fclose(inst->streamFile); + if (inst->ownsStreamBlob) { + if (inst->streamBlobLinear) linearFree((void*) inst->streamBlob); + else free((void*) inst->streamBlob); + } + + memset(inst, 0, sizeof(*inst)); +} + +static N3DSSoundInstance* N3DSAudio_findFreeInstanceInRange(N3DSAudioSystem* audio, int32_t start, int32_t endExclusive) { + u64 nowTick = svcGetSystemTick(); + for (int32_t i = start; i < endExclusive; ++i) { + if (!audio->instances[i].active) return &audio->instances[i]; + } + for (int32_t i = start; i < endExclusive; ++i) { + bool finishedPcm = N3DSAudio_nonStreamPlaybackFinished(&audio->instances[i], nowTick); + if (finishedPcm) { + N3DSAudio_releaseInstance(audio, &audio->instances[i]); + return &audio->instances[i]; + } + } + return NULL; +} + +static N3DSSoundInstance* N3DSAudio_findFreeInstance(N3DSAudioSystem* audio, bool preferStreamSlots) { + int32_t split = N3DS_MAX_SOUND_INSTANCES - N3DS_RESERVED_STREAM_INSTANCES; + if (split < 0) split = 0; + if (split > N3DS_MAX_SOUND_INSTANCES) split = N3DS_MAX_SOUND_INSTANCES; + + if (preferStreamSlots) { + N3DSSoundInstance* inst = N3DSAudio_findFreeInstanceInRange(audio, split, N3DS_MAX_SOUND_INSTANCES); + if (inst != NULL) return inst; + return N3DSAudio_findFreeInstanceInRange(audio, 0, split); + } + + N3DSSoundInstance* inst = N3DSAudio_findFreeInstanceInRange(audio, 0, split); + if (inst != NULL) return inst; + return N3DSAudio_findFreeInstanceInRange(audio, split, N3DS_MAX_SOUND_INSTANCES); +} + +static void N3DSAudio_streamResetDecoder(N3DSSoundInstance* inst) { + inst->currentSample = 0; + inst->pendingSamples = 0; + inst->pendingOffset = 0; + repeat(inst->bcwav.channelCount, i) { + inst->decode[i].nextFrameIndex = 0; + inst->decode[i].predictorScale = inst->bcwav.channels[i].startContext.predictorScale; + inst->decode[i].hist1 = inst->bcwav.channels[i].startContext.yn1; + inst->decode[i].hist2 = inst->bcwav.channels[i].startContext.yn2; + inst->fileCacheFrameBase[i] = 0; + inst->fileCacheFrameCount[i] = 0; + } +} + +static bool N3DSAudio_streamEnsureFileCache(N3DSSoundInstance* inst, uint32_t channelIndex) { + if (inst == NULL || inst->streamFile == NULL || channelIndex >= inst->bcwav.channelCount) return false; + + N3DSStreamDecodeState* state = &inst->decode[channelIndex]; + uint32_t frameIndex = state->nextFrameIndex; + uint32_t cacheBase = inst->fileCacheFrameBase[channelIndex]; + uint32_t cacheCount = inst->fileCacheFrameCount[channelIndex]; + if (frameIndex >= cacheBase && frameIndex < cacheBase + cacheCount) return true; + + uint32_t totalFrames = (inst->sampleCount + 13u) / 14u; + if (frameIndex >= totalFrames) return false; + + uint32_t framesToRead = totalFrames - frameIndex; + if (framesToRead > N3DS_STREAM_ADPCM_CACHE_FRAMES) framesToRead = N3DS_STREAM_ADPCM_CACHE_FRAMES; + + uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + frameIndex * 8u; + uint32_t byteCount = framesToRead * 8u; + if (fseek(inst->streamFile, (long) byteOffset, SEEK_SET) != 0) return false; + if (fread(inst->fileCache[channelIndex], 1, byteCount, inst->streamFile) != byteCount) return false; + + inst->fileCacheFrameBase[channelIndex] = frameIndex; + inst->fileCacheFrameCount[channelIndex] = framesToRead; + return true; +} + +static bool N3DSAudio_streamDecodeNextFrame(N3DSSoundInstance* inst, int16_t outPcm[14 * 2], uint32_t* outSamples) { + if (inst->currentSample >= inst->sampleCount) { + *outSamples = 0; + return true; + } + + int16_t monoSamples[2][14]; + repeat(inst->bcwav.channelCount, channelIndex) { + N3DSStreamDecodeState* state = &inst->decode[channelIndex]; + uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + state->nextFrameIndex * 8u; + const uint8_t* frame = NULL; + uint8_t frameBuffer[8]; + if (inst->streamBlob != NULL) { + if (byteOffset + 8u > inst->streamBlobSize) return false; + frame = inst->streamBlob + byteOffset; + } else { + if (!N3DSAudio_streamEnsureFileCache(inst, channelIndex)) return false; + uint32_t cacheIndex = state->nextFrameIndex - inst->fileCacheFrameBase[channelIndex]; + if (cacheIndex >= inst->fileCacheFrameCount[channelIndex]) return false; + memcpy(frameBuffer, inst->fileCache[channelIndex] + cacheIndex * 8u, sizeof(frameBuffer)); + frame = frameBuffer; + } + state->predictorScale = frame[0]; + N3DSAudio_decodeFrame(&inst->bcwav.channels[channelIndex], frame, &state->hist1, &state->hist2, monoSamples[channelIndex]); + state->nextFrameIndex++; + } + + uint32_t samplesThisFrame = inst->sampleCount - inst->currentSample; + if (samplesThisFrame > 14u) samplesThisFrame = 14u; + repeat(samplesThisFrame, sampleIndex) { + if (inst->bcwav.channelCount == 1) { + outPcm[sampleIndex] = monoSamples[0][sampleIndex]; + } else { + outPcm[sampleIndex * 2 + 0] = monoSamples[0][sampleIndex]; + outPcm[sampleIndex * 2 + 1] = monoSamples[1][sampleIndex]; + } + } + inst->currentSample += samplesThisFrame; + *outSamples = samplesThisFrame; + return true; +} + +static bool N3DSAudio_streamSeekSamples(N3DSSoundInstance* inst, uint32_t targetSample) { + if (targetSample > inst->sampleCount) targetSample = inst->sampleCount; + + N3DSAudio_streamResetDecoder(inst); + if (targetSample == 0) return true; + if (inst->bcwav.loop && + targetSample == inst->bcwav.loopStart && + N3DSAudio_isFrameAlignedSample(targetSample)) { + uint32_t frameIndex = targetSample / 14u; + repeat(inst->bcwav.channelCount, i) { + inst->decode[i].nextFrameIndex = frameIndex; + inst->decode[i].predictorScale = inst->bcwav.channels[i].loopContext.predictorScale; + inst->decode[i].hist1 = inst->bcwav.channels[i].loopContext.yn1; + inst->decode[i].hist2 = inst->bcwav.channels[i].loopContext.yn2; + } + inst->currentSample = targetSample; + return true; + } + + int16_t framePcm[14 * 2]; + uint32_t frameSamples = 0; + while (inst->currentSample + 14u <= targetSample) { + if (!N3DSAudio_streamDecodeNextFrame(inst, framePcm, &frameSamples)) return false; + if (frameSamples == 0) return true; + } + + if (inst->currentSample < targetSample) { + uint32_t baseSample = inst->currentSample; + if (!N3DSAudio_streamDecodeNextFrame(inst, framePcm, &frameSamples)) return false; + uint32_t skip = targetSample - baseSample; + if (skip < frameSamples) { + uint32_t remaining = frameSamples - skip; + size_t stride = inst->bcwav.channelCount; + memcpy(inst->pendingPcm, framePcm + skip * stride, remaining * stride * sizeof(int16_t)); + inst->pendingSamples = remaining; + inst->pendingOffset = 0; + } + inst->currentSample = targetSample; + } + + return true; +} + +static bool N3DSAudio_streamEnsureFileCacheAsync(N3DSSoundInstance* inst, N3DSStreamFillCursor* cursor, uint32_t channelIndex) { + if (inst == NULL || cursor == NULL || inst->streamFile == NULL || channelIndex >= inst->bcwav.channelCount) return false; + + N3DSStreamDecodeState* state = &cursor->decode[channelIndex]; + uint32_t frameIndex = state->nextFrameIndex; + uint32_t cacheBase = cursor->fileCacheFrameBase[channelIndex]; + uint32_t cacheCount = cursor->fileCacheFrameCount[channelIndex]; + if (frameIndex >= cacheBase && frameIndex < cacheBase + cacheCount) return true; + + uint32_t totalFrames = (inst->sampleCount + 13u) / 14u; + if (frameIndex >= totalFrames) return false; + + uint32_t framesToRead = totalFrames - frameIndex; + if (framesToRead > N3DS_STREAM_ADPCM_CACHE_FRAMES) framesToRead = N3DS_STREAM_ADPCM_CACHE_FRAMES; + + uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + frameIndex * 8u; + uint32_t byteCount = framesToRead * 8u; + if (fseek(inst->streamFile, (long) byteOffset, SEEK_SET) != 0) return false; + if (fread(cursor->fileCache[channelIndex], 1, byteCount, inst->streamFile) != byteCount) return false; + + cursor->fileCacheFrameBase[channelIndex] = frameIndex; + cursor->fileCacheFrameCount[channelIndex] = framesToRead; + return true; +} + +static bool N3DSAudio_streamDecodeNextFrameAsync( + N3DSSoundInstance* inst, + N3DSStreamFillCursor* cursor, + int16_t outPcm[14 * 2], + uint32_t* outSamples +) { + if (inst == NULL || cursor == NULL || outPcm == NULL || outSamples == NULL) return false; + if (cursor->currentSample >= inst->sampleCount) { + *outSamples = 0; + return true; + } + + int16_t monoSamples[2][14]; + repeat(inst->bcwav.channelCount, channelIndex) { + N3DSStreamDecodeState* state = &cursor->decode[channelIndex]; + uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + state->nextFrameIndex * 8u; + const uint8_t* frame = NULL; + uint8_t frameBuffer[8]; + if (inst->streamBlob != NULL) { + if (byteOffset + 8u > inst->streamBlobSize) return false; + frame = inst->streamBlob + byteOffset; + } else { + if (!N3DSAudio_streamEnsureFileCacheAsync(inst, cursor, channelIndex)) return false; + uint32_t cacheIndex = state->nextFrameIndex - cursor->fileCacheFrameBase[channelIndex]; + if (cacheIndex >= cursor->fileCacheFrameCount[channelIndex]) return false; + memcpy(frameBuffer, cursor->fileCache[channelIndex] + cacheIndex * 8u, sizeof(frameBuffer)); + frame = frameBuffer; + } + state->predictorScale = frame[0]; + N3DSAudio_decodeFrame(&inst->bcwav.channels[channelIndex], frame, &state->hist1, &state->hist2, monoSamples[channelIndex]); + state->nextFrameIndex++; + } + + uint32_t samplesThisFrame = inst->sampleCount - cursor->currentSample; + if (samplesThisFrame > 14u) samplesThisFrame = 14u; + repeat(samplesThisFrame, sampleIndex) { + if (inst->bcwav.channelCount == 1) { + outPcm[sampleIndex] = monoSamples[0][sampleIndex]; + } else { + outPcm[sampleIndex * 2 + 0] = monoSamples[0][sampleIndex]; + outPcm[sampleIndex * 2 + 1] = monoSamples[1][sampleIndex]; + } + } + cursor->currentSample += samplesThisFrame; + *outSamples = samplesThisFrame; + return true; +} + +static bool N3DSAudio_streamSeekSamplesAsync(N3DSSoundInstance* inst, N3DSStreamFillCursor* cursor, uint32_t targetSample) { + if (inst == NULL || cursor == NULL) return false; + if (targetSample > inst->sampleCount) targetSample = inst->sampleCount; + + cursor->currentSample = 0; + cursor->pendingSamples = 0; + cursor->pendingOffset = 0; + repeat(inst->bcwav.channelCount, i) { + cursor->decode[i].nextFrameIndex = 0; + cursor->decode[i].predictorScale = inst->bcwav.channels[i].startContext.predictorScale; + cursor->decode[i].hist1 = inst->bcwav.channels[i].startContext.yn1; + cursor->decode[i].hist2 = inst->bcwav.channels[i].startContext.yn2; + cursor->fileCacheFrameBase[i] = 0; + cursor->fileCacheFrameCount[i] = 0; + } + if (targetSample == 0) return true; + if (inst->bcwav.loop && + targetSample == inst->bcwav.loopStart && + N3DSAudio_isFrameAlignedSample(targetSample)) { + uint32_t frameIndex = targetSample / 14u; + repeat(inst->bcwav.channelCount, i) { + cursor->decode[i].nextFrameIndex = frameIndex; + cursor->decode[i].predictorScale = inst->bcwav.channels[i].loopContext.predictorScale; + cursor->decode[i].hist1 = inst->bcwav.channels[i].loopContext.yn1; + cursor->decode[i].hist2 = inst->bcwav.channels[i].loopContext.yn2; + } + cursor->currentSample = targetSample; + return true; + } + + int16_t framePcm[14 * 2]; + uint32_t frameSamples = 0; + while (cursor->currentSample + 14u <= targetSample) { + if (!N3DSAudio_streamDecodeNextFrameAsync(inst, cursor, framePcm, &frameSamples)) return false; + if (frameSamples == 0) return true; + } + + if (cursor->currentSample < targetSample) { + uint32_t baseSample = cursor->currentSample; + if (!N3DSAudio_streamDecodeNextFrameAsync(inst, cursor, framePcm, &frameSamples)) return false; + uint32_t skip = targetSample - baseSample; + if (skip < frameSamples) { + uint32_t remaining = frameSamples - skip; + size_t stride = inst->bcwav.channelCount; + memcpy(cursor->pendingPcm, framePcm + skip * stride, remaining * stride * sizeof(int16_t)); + cursor->pendingSamples = remaining; + cursor->pendingOffset = 0; + } + cursor->currentSample = targetSample; + } + + return true; +} + +static bool N3DSAudio_fillStreamWaveBufAsync( + N3DSSoundInstance* inst, + int bufferIndex, + N3DSStreamFillCursor* cursor, + uint32_t* outStartSample, + uint32_t* outProducedSamples +) { + if (inst == NULL || cursor == NULL || outStartSample == NULL || outProducedSamples == NULL) return false; + + size_t stride = inst->bcwav.channelCount; + int16_t* out = inst->streamPcm[bufferIndex]; + uint32_t produced = 0; + uint32_t startSample = cursor->currentSample; + + while (produced < N3DS_STREAM_CHUNK_SAMPLES) { + if (cursor->pendingOffset < cursor->pendingSamples) { + uint32_t take = cursor->pendingSamples - cursor->pendingOffset; + if (take > N3DS_STREAM_CHUNK_SAMPLES - produced) take = N3DS_STREAM_CHUNK_SAMPLES - produced; + memcpy( + out + produced * stride, + cursor->pendingPcm + cursor->pendingOffset * stride, + take * stride * sizeof(int16_t) + ); + cursor->pendingOffset += take; + produced += take; + cursor->currentSample += take; + if (cursor->pendingOffset >= cursor->pendingSamples) { + cursor->pendingSamples = 0; + cursor->pendingOffset = 0; + } + continue; + } + + if (cursor->currentSample >= inst->sampleCount) { + if (inst->loop) { + if (produced > 0) break; + if (!N3DSAudio_streamSeekSamplesAsync(inst, cursor, inst->bcwav.loopStart)) return false; + startSample = cursor->currentSample; + continue; + } + break; + } + + int16_t framePcm[14 * 2]; + uint32_t frameSamples = 0; + if (!N3DSAudio_streamDecodeNextFrameAsync(inst, cursor, framePcm, &frameSamples)) return false; + if (frameSamples == 0) break; + + uint32_t take = frameSamples; + if (take > N3DS_STREAM_CHUNK_SAMPLES - produced) take = N3DS_STREAM_CHUNK_SAMPLES - produced; + memcpy(out + produced * stride, framePcm, take * stride * sizeof(int16_t)); + produced += take; + } + + if (produced == 0) return false; + + DSP_FlushDataCache(out, produced * stride * sizeof(int16_t)); + *outStartSample = startSample; + *outProducedSamples = produced; + return true; +} + +static bool N3DSAudio_fillNativeStreamWaveBufAsync( + N3DSSoundInstance* inst, + int bufferIndex, + N3DSStreamFillCursor* cursor, + uint32_t* outStartSample, + uint32_t* outProducedSamples, + ndspAdpcmData outAdpcmStates[2] +) { + if (inst == NULL || cursor == NULL || outStartSample == NULL || outProducedSamples == NULL || outAdpcmStates == NULL) return false; + if (inst->streamFile == NULL || inst->channelCount == 0) return false; + + uint32_t startSample = cursor->currentSample; + if (startSample >= inst->sampleCount) { + if (!inst->loop) return false; + if (!N3DSAudio_isFrameAlignedSample(inst->bcwav.loopStart)) return false; + if (!N3DSAudio_streamSeekSamplesAsync(inst, cursor, inst->bcwav.loopStart)) return false; + startSample = cursor->currentSample; + } + + uint32_t endSample = startSample + N3DS_STREAM_CHUNK_SAMPLES; + if (endSample > inst->sampleCount) endSample = inst->sampleCount; + if (inst->loop && inst->bcwav.loop && inst->bcwav.loopEnd > inst->bcwav.loopStart && endSample > inst->bcwav.loopEnd) { + endSample = inst->bcwav.loopEnd; + } + uint32_t sampleCount = endSample - startSample; + if (sampleCount == 0) return false; + + repeat(inst->channelCount, channelIndex) { + uint32_t startFrame = startSample / 14u; + uint32_t frameCount = (sampleCount + 13u) / 14u; + uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + startFrame * 8u; + uint32_t byteCount = frameCount * 8u; + if (fseek(inst->streamFile, (long) byteOffset, SEEK_SET) != 0) return false; + if (fread(inst->streamAdpcm[channelIndex][bufferIndex], 1, byteCount, inst->streamFile) != byteCount) return false; + DSP_FlushDataCache(inst->streamAdpcm[channelIndex][bufferIndex], byteCount); + } + + N3DSDspContext primaryContext = N3DSAudio_contextForSample(&inst->bcwav.channels[0], &cursor->decode[0], startSample); + outAdpcmStates[0].index = primaryContext.predictorScale; + outAdpcmStates[0].history0 = primaryContext.yn1; + outAdpcmStates[0].history1 = primaryContext.yn2; + + if (inst->secondaryChannelId >= 0) { + N3DSDspContext secondaryContext = N3DSAudio_contextForSample(&inst->bcwav.channels[1], &cursor->decode[1], startSample); + outAdpcmStates[1].index = secondaryContext.predictorScale; + outAdpcmStates[1].history0 = secondaryContext.yn1; + outAdpcmStates[1].history1 = secondaryContext.yn2; + } else { + memset(&outAdpcmStates[1], 0, sizeof(outAdpcmStates[1])); + } + + bool atLoopBoundary = inst->loop && + inst->bcwav.loop && + inst->bcwav.loopEnd > inst->bcwav.loopStart && + endSample >= inst->bcwav.loopEnd; + + { + N3DSStreamDecodeState nextDecode[2]; + memcpy(nextDecode, cursor->decode, sizeof(nextDecode)); + + uint32_t framesToAdvance = (sampleCount + 13u) / 14u; + repeat(inst->channelCount, channelIndex) { + uint32_t channelSample = startSample; + repeat(framesToAdvance, frameIndex) { + const uint8_t* frame = inst->streamAdpcm[channelIndex][bufferIndex] + frameIndex * 8u; + int16_t scratch[14]; + nextDecode[channelIndex].predictorScale = frame[0]; + N3DSAudio_decodeFrame( + &inst->bcwav.channels[channelIndex], + frame, + &nextDecode[channelIndex].hist1, + &nextDecode[channelIndex].hist2, + scratch + ); + nextDecode[channelIndex].nextFrameIndex++; + + uint32_t remaining = endSample - channelSample; + uint32_t frameSamples = remaining > 14u ? 14u : remaining; + channelSample += frameSamples; + } + } + + if (atLoopBoundary) { + uint32_t loopFrame = inst->bcwav.loopStart / 14u; + repeat(inst->channelCount, channelIndex) { + cursor->decode[channelIndex].nextFrameIndex = loopFrame; + cursor->decode[channelIndex].predictorScale = inst->bcwav.channels[channelIndex].loopContext.predictorScale; + cursor->decode[channelIndex].hist1 = inst->bcwav.channels[channelIndex].loopContext.yn1; + cursor->decode[channelIndex].hist2 = inst->bcwav.channels[channelIndex].loopContext.yn2; + } + cursor->currentSample = inst->bcwav.loopStart; + } else { + memcpy(cursor->decode, nextDecode, sizeof(nextDecode)); + cursor->currentSample = endSample; + } + } + + *outStartSample = startSample; + *outProducedSamples = sampleCount; + return true; +} + +static bool N3DSAudio_fillStreamWaveBuf(N3DSSoundInstance* inst, int bufferIndex) { + size_t stride = inst->bcwav.channelCount; + int16_t* out = inst->streamPcm[bufferIndex]; + uint32_t produced = 0; + uint32_t startSample = inst->currentSample; + + while (produced < N3DS_STREAM_CHUNK_SAMPLES) { + if (inst->pendingOffset < inst->pendingSamples) { + uint32_t take = inst->pendingSamples - inst->pendingOffset; + if (take > N3DS_STREAM_CHUNK_SAMPLES - produced) take = N3DS_STREAM_CHUNK_SAMPLES - produced; + memcpy( + out + produced * stride, + inst->pendingPcm + inst->pendingOffset * stride, + take * stride * sizeof(int16_t) + ); + inst->pendingOffset += take; + produced += take; + inst->currentSample += take; + if (inst->pendingOffset >= inst->pendingSamples) { + inst->pendingSamples = 0; + inst->pendingOffset = 0; + } + continue; + } + + if (inst->currentSample >= inst->sampleCount) { + if (inst->loop) { + if (produced > 0) break; + if (!N3DSAudio_streamSeekSamples(inst, inst->bcwav.loopStart)) return false; + startSample = inst->currentSample; + continue; + } + break; + } + + int16_t framePcm[14 * 2]; + uint32_t frameSamples = 0; + if (!N3DSAudio_streamDecodeNextFrame(inst, framePcm, &frameSamples)) return false; + if (frameSamples == 0) break; + + uint32_t take = frameSamples; + if (take > N3DS_STREAM_CHUNK_SAMPLES - produced) take = N3DS_STREAM_CHUNK_SAMPLES - produced; + memcpy(out + produced * stride, framePcm, take * stride * sizeof(int16_t)); + produced += take; + } + + if (produced == 0) return false; + + memset(&inst->waveBufs[bufferIndex], 0, sizeof(inst->waveBufs[bufferIndex])); + inst->waveBufs[bufferIndex].data_pcm16 = out; + inst->waveBufs[bufferIndex].nsamples = produced; + inst->waveBufs[bufferIndex].looping = false; + inst->bufferStartSample[bufferIndex] = startSample; + DSP_FlushDataCache(out, produced * stride * sizeof(int16_t)); + return true; +} + +static bool N3DSAudio_fillNativeStreamWaveBuf(N3DSSoundInstance* inst, int bufferIndex) { + if (inst == NULL || inst->streamFile == NULL || inst->channelCount == 0) return false; + + uint32_t startSample = inst->currentSample; + if (startSample >= inst->sampleCount) { + if (!inst->loop) return false; + if (!N3DSAudio_isFrameAlignedSample(inst->bcwav.loopStart)) return false; + if (!N3DSAudio_streamSeekSamples(inst, inst->bcwav.loopStart)) return false; + startSample = inst->currentSample; + } + + uint32_t endSample = startSample + N3DS_STREAM_CHUNK_SAMPLES; + if (endSample > inst->sampleCount) endSample = inst->sampleCount; + if (inst->loop && inst->bcwav.loop && inst->bcwav.loopEnd > inst->bcwav.loopStart && endSample > inst->bcwav.loopEnd) { + endSample = inst->bcwav.loopEnd; + } + uint32_t sampleCount = endSample - startSample; + if (sampleCount == 0) return false; + + repeat(inst->channelCount, channelIndex) { + uint32_t startFrame = startSample / 14u; + uint32_t frameCount = (sampleCount + 13u) / 14u; + uint32_t byteOffset = inst->bcwav.channels[channelIndex].dataOffset + startFrame * 8u; + uint32_t byteCount = frameCount * 8u; + if (fseek(inst->streamFile, (long) byteOffset, SEEK_SET) != 0) return false; + if (fread(inst->streamAdpcm[channelIndex][bufferIndex], 1, byteCount, inst->streamFile) != byteCount) return false; + DSP_FlushDataCache(inst->streamAdpcm[channelIndex][bufferIndex], byteCount); + } + + ndspAdpcmData* primaryState = &inst->adpcmStates[bufferIndex * 2]; + N3DSDspContext primaryContext = N3DSAudio_contextForSample(&inst->bcwav.channels[0], &inst->decode[0], startSample); + primaryState->index = primaryContext.predictorScale; + primaryState->history0 = primaryContext.yn1; + primaryState->history1 = primaryContext.yn2; + + memset(&inst->waveBufs[bufferIndex], 0, sizeof(inst->waveBufs[bufferIndex])); + inst->waveBufs[bufferIndex].data_adpcm = inst->streamAdpcm[0][bufferIndex]; + inst->waveBufs[bufferIndex].nsamples = sampleCount; + inst->waveBufs[bufferIndex].adpcm_data = primaryState; + inst->waveBufs[bufferIndex].looping = false; + inst->bufferStartSample[bufferIndex] = startSample; + + if (inst->secondaryChannelId >= 0) { + ndspAdpcmData* secondaryState = &inst->adpcmStates[bufferIndex * 2 + 1]; + N3DSDspContext secondaryContext = N3DSAudio_contextForSample(&inst->bcwav.channels[1], &inst->decode[1], startSample); + secondaryState->index = secondaryContext.predictorScale; + secondaryState->history0 = secondaryContext.yn1; + secondaryState->history1 = secondaryContext.yn2; + + memset(&inst->secondaryWaveBufs[bufferIndex], 0, sizeof(inst->secondaryWaveBufs[bufferIndex])); + inst->secondaryWaveBufs[bufferIndex].data_adpcm = inst->streamAdpcm[1][bufferIndex]; + inst->secondaryWaveBufs[bufferIndex].nsamples = sampleCount; + inst->secondaryWaveBufs[bufferIndex].adpcm_data = secondaryState; + inst->secondaryWaveBufs[bufferIndex].looping = false; + } + + bool atLoopBoundary = inst->loop && + inst->bcwav.loop && + inst->bcwav.loopEnd > inst->bcwav.loopStart && + endSample >= inst->bcwav.loopEnd; + + { + N3DSStreamDecodeState nextDecode[2]; + memcpy(nextDecode, inst->decode, sizeof(nextDecode)); + + uint32_t framesToAdvance = (sampleCount + 13u) / 14u; + repeat(inst->channelCount, channelIndex) { + uint32_t channelSample = startSample; + repeat(framesToAdvance, frameIndex) { + const uint8_t* frame = inst->streamAdpcm[channelIndex][bufferIndex] + frameIndex * 8u; + int16_t scratch[14]; + nextDecode[channelIndex].predictorScale = frame[0]; + N3DSAudio_decodeFrame( + &inst->bcwav.channels[channelIndex], + frame, + &nextDecode[channelIndex].hist1, + &nextDecode[channelIndex].hist2, + scratch + ); + nextDecode[channelIndex].nextFrameIndex++; + + uint32_t remaining = endSample - channelSample; + uint32_t frameSamples = remaining > 14u ? 14u : remaining; + channelSample += frameSamples; + } + } + + if (atLoopBoundary) { + uint32_t loopFrame = inst->bcwav.loopStart / 14u; + repeat(inst->channelCount, channelIndex) { + inst->decode[channelIndex].nextFrameIndex = loopFrame; + inst->decode[channelIndex].predictorScale = inst->bcwav.channels[channelIndex].loopContext.predictorScale; + inst->decode[channelIndex].hist1 = inst->bcwav.channels[channelIndex].loopContext.yn1; + inst->decode[channelIndex].hist2 = inst->bcwav.channels[channelIndex].loopContext.yn2; + } + inst->currentSample = inst->bcwav.loopStart; + } else { + memcpy(inst->decode, nextDecode, sizeof(nextDecode)); + inst->currentSample = endSample; + } + } + + return true; +} + +static bool N3DSAudio_primeStream(N3DSSoundInstance* inst) { + inst->streamFinished = false; + N3DSAudio_resetInstanceWaveBufState(inst); + repeat(N3DS_STREAM_BUFFER_COUNT, i) { + bool queued = inst->useNativeAdpcm ? N3DSAudio_fillNativeStreamWaveBuf(inst, i) : N3DSAudio_fillStreamWaveBuf(inst, i); + if (!queued) break; + ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[i]); + if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) { + ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[i]); + } + } + + repeat(N3DS_STREAM_BUFFER_COUNT, i) { + if (inst->waveBufs[i].status == NDSP_WBUF_QUEUED || inst->waveBufs[i].status == NDSP_WBUF_PLAYING) { + return true; + } + } + return false; +} + +static void N3DSAudio_waitForAsyncFillIdleLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst) { + if (audio == NULL || inst == NULL || !audio->workerEnabled) return; + while (inst->asyncFill.inProgress) { + CondVar_Wait(&audio->workerCond, &audio->lock); + } +} + +static void N3DSAudio_cancelAsyncFillLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst) { + if (audio == NULL || inst == NULL || !inst->isStream) return; + N3DSAudio_waitForAsyncFillIdleLocked(audio, inst); + inst->streamGeneration += 1u; + memset(&inst->asyncFill, 0, sizeof(inst->asyncFill)); +} + +static void N3DSAudio_requestAsyncFillLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst, int bufferIndex) { + if (audio == NULL || inst == NULL || !audio->workerEnabled || !inst->isStream) return; + if (inst->asyncFill.requested || inst->asyncFill.inProgress || inst->asyncFill.ready) return; + + inst->asyncFill.requested = true; + inst->asyncFill.inProgress = false; + inst->asyncFill.ready = false; + inst->asyncFill.success = false; + inst->asyncFill.bufferIndex = bufferIndex; + inst->asyncFill.generation = inst->streamGeneration; + LightEvent_Signal(&audio->workerEvent); +} + +static bool N3DSAudio_applyAsyncFillLocked(N3DSAudioSystem* audio, N3DSSoundInstance* inst) { + if (audio == NULL || inst == NULL || !inst->asyncFill.ready) return false; + + int bufferIndex = inst->asyncFill.bufferIndex; + bool success = inst->asyncFill.success; + bool useNativeAdpcm = inst->useNativeAdpcm; + + if (success) { + N3DSAudio_applyFillCursor(inst, &inst->asyncFill.nextCursor); + + memset(&inst->waveBufs[bufferIndex], 0, sizeof(inst->waveBufs[bufferIndex])); + inst->bufferStartSample[bufferIndex] = inst->asyncFill.bufferStartSample; + if (useNativeAdpcm) { + inst->adpcmStates[bufferIndex * 2] = inst->asyncFill.adpcmStates[0]; + inst->waveBufs[bufferIndex].data_adpcm = inst->streamAdpcm[0][bufferIndex]; + inst->waveBufs[bufferIndex].nsamples = inst->asyncFill.sampleCount; + inst->waveBufs[bufferIndex].adpcm_data = &inst->adpcmStates[bufferIndex * 2]; + inst->waveBufs[bufferIndex].looping = false; + + if (inst->secondaryChannelId >= 0) { + memset(&inst->secondaryWaveBufs[bufferIndex], 0, sizeof(inst->secondaryWaveBufs[bufferIndex])); + inst->adpcmStates[bufferIndex * 2 + 1] = inst->asyncFill.adpcmStates[1]; + inst->secondaryWaveBufs[bufferIndex].data_adpcm = inst->streamAdpcm[1][bufferIndex]; + inst->secondaryWaveBufs[bufferIndex].nsamples = inst->asyncFill.sampleCount; + inst->secondaryWaveBufs[bufferIndex].adpcm_data = &inst->adpcmStates[bufferIndex * 2 + 1]; + inst->secondaryWaveBufs[bufferIndex].looping = false; + } + } else { + inst->waveBufs[bufferIndex].data_pcm16 = inst->streamPcm[bufferIndex]; + inst->waveBufs[bufferIndex].nsamples = inst->asyncFill.sampleCount; + inst->waveBufs[bufferIndex].looping = false; + } + + ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[bufferIndex]); + if (useNativeAdpcm && inst->secondaryChannelId >= 0) { + ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[bufferIndex]); + } + } else { + inst->streamFinished = true; + } + + memset(&inst->asyncFill, 0, sizeof(inst->asyncFill)); + CondVar_Broadcast(&audio->workerCond); + return success; +} + +static bool N3DSAudio_hasActiveStreamPlaybackLocked(const N3DSAudioSystem* audio) { + if (audio == NULL) return false; + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + const N3DSSoundInstance* inst = &audio->instances[i]; + if (!inst->active || !inst->isStream) continue; + if (inst->streamFinished) continue; + return true; + } + return false; +} + +static bool N3DSAudio_runCachePrewarmJob(N3DSAudioSystem* audio) { + if (audio == NULL) return false; + + int32_t soundIndex = -1; + + LightLock_Lock(&audio->lock); + if (audio->workerStop) { + LightLock_Unlock(&audio->lock); + return false; + } + if (audio->pendingPrewarmSoundCount == 0 || audio->pendingPrewarmByteBudget == 0 || audio->cachedSounds == NULL) { + LightLock_Unlock(&audio->lock); + return false; + } + if (N3DSAudio_hasActiveStreamPlaybackLocked(audio)) { + LightLock_Unlock(&audio->lock); + return false; + } + + uint32_t scanned = 0; + while (scanned < audio->cachedSoundCount) { + uint32_t candidateIndex = audio->cachedSoundPrewarmCursor; + audio->cachedSoundPrewarmCursor++; + if (audio->cachedSoundPrewarmCursor >= audio->cachedSoundCount) { + audio->cachedSoundPrewarmCursor = 0; + } + scanned++; + + N3DSCachedSound* cachedSound = &audio->cachedSounds[candidateIndex]; + if (cachedSound->attempted) continue; + + Sound* sound = &audio->base.audioGroups[0]->sond.sounds[candidateIndex]; + if (!N3DSAudio_shouldPreloadSound(sound)) { + cachedSound->attempted = true; + continue; + } + + cachedSound->attempted = true; + soundIndex = (int32_t) candidateIndex; + break; + } + + if (soundIndex < 0) { + audio->pendingPrewarmSoundCount = 0; + audio->pendingPrewarmByteBudget = 0; + LightLock_Unlock(&audio->lock); + return false; + } + LightLock_Unlock(&audio->lock); + + DataWin* dw = audio->base.audioGroups[0]; + Sound* sound = &dw->sond.sounds[soundIndex]; + char* path = NULL; + uint8_t* blob = NULL; + uint32_t blobSize = 0; + N3DSBcwav bcwav; + bool loaded = false; + bool blobOwned = false; + memset(&bcwav, 0, sizeof(bcwav)); + + const uint8_t* packedBlob = NULL; + uint32_t packedBlobSize = 0; + const N3DSPackedSoundBankEntry* packedEntry = N3DSAudio_getPackedSoundEntry(audio, soundIndex); + if (!N3DSAudio_soundLooksLikeMusic(sound) && + packedEntry != NULL && + !N3DSAudio_packedSoundEntryIsPcm16(packedEntry) && + N3DSAudio_getPackedSoundBlob(audio, soundIndex, &packedBlob, &packedBlobSize) && + N3DSAudio_parseBcwavBlob(packedBlob, packedBlobSize, &bcwav) && + N3DSAudio_shouldPrewarmCachedSound(sound, &bcwav)) { + blob = (uint8_t*) packedBlob; + blobSize = packedBlobSize; + loaded = true; + } else { + path = N3DSAudio_resolveSoundPath(audio, sound, soundIndex); + if (path == NULL) { + path = N3DSAudio_tryResolveGeneratedSoundPath(audio, sound, soundIndex); + } + + if (path != NULL && + N3DSAudio_readFileFully(path, &blob, &blobSize) && + N3DSAudio_parseBcwavBlob(blob, blobSize, &bcwav) && + N3DSAudio_shouldPrewarmCachedSound(sound, &bcwav)) { + loaded = true; + blobOwned = true; + } + } + + LightLock_Lock(&audio->lock); + N3DSCachedSound* cachedSound = &audio->cachedSounds[soundIndex]; + if (loaded && (!blobOwned || audio->cachedSoundBytes + blobSize <= audio->maxCachedSoundBytes)) { + cachedSound->available = true; + cachedSound->path = path; + cachedSound->blob = blob; + cachedSound->blobOwned = blobOwned; + cachedSound->blobSize = blobSize; + cachedSound->bcwav = bcwav; + if (blobOwned) audio->cachedSoundBytes += blobSize; + if (audio->pendingPrewarmSoundCount > 0) audio->pendingPrewarmSoundCount--; + if (audio->pendingPrewarmByteBudget > blobSize) audio->pendingPrewarmByteBudget -= blobSize; + else audio->pendingPrewarmByteBudget = 0; + path = NULL; + blob = NULL; + } + LightLock_Unlock(&audio->lock); + + free(path); + if (blobOwned) free(blob); + return true; +} + +static bool N3DSAudio_hasPendingAsyncFillLocked(const N3DSAudioSystem* audio) { + if (audio == NULL || !audio->workerEnabled) return false; + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + const N3DSSoundInstance* inst = &audio->instances[i]; + if (!inst->active || !inst->isStream) continue; + if (inst->asyncFill.requested || inst->asyncFill.inProgress) return true; + } + return false; +} + +static void N3DSAudio_streamWorkerMain(void* arg) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) arg; + if (audio == NULL) threadExit(0); + + while (true) { + LightEvent_Wait(&audio->workerEvent); + + while (true) { + int32_t jobSlot = -1; + int bufferIndex = -1; + uint32_t generation = 0; + bool useNativeAdpcm = false; + N3DSStreamFillCursor cursor; + memset(&cursor, 0, sizeof(cursor)); + + LightLock_Lock(&audio->lock); + if (audio->workerStop) { + LightLock_Unlock(&audio->lock); + threadExit(0); + } + + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + N3DSSoundInstance* inst = &audio->instances[i]; + if (!inst->active || !inst->isStream) continue; + if (!inst->asyncFill.requested || inst->asyncFill.inProgress || inst->asyncFill.ready) continue; + inst->asyncFill.inProgress = true; + jobSlot = (int32_t) i; + bufferIndex = inst->asyncFill.bufferIndex; + generation = inst->asyncFill.generation; + useNativeAdpcm = inst->useNativeAdpcm; + N3DSAudio_captureFillCursor(inst, &cursor); + break; + } + LightLock_Unlock(&audio->lock); + + if (jobSlot < 0) { + if (!N3DSAudio_runCachePrewarmJob(audio)) break; + continue; + } + + N3DSSoundInstance* inst = &audio->instances[jobSlot]; + uint32_t startSample = 0; + uint32_t producedSamples = 0; + ndspAdpcmData adpcmStates[2]; + memset(adpcmStates, 0, sizeof(adpcmStates)); + bool success = useNativeAdpcm + ? N3DSAudio_fillNativeStreamWaveBufAsync(inst, bufferIndex, &cursor, &startSample, &producedSamples, adpcmStates) + : N3DSAudio_fillStreamWaveBufAsync(inst, bufferIndex, &cursor, &startSample, &producedSamples); + + LightLock_Lock(&audio->lock); + inst = &audio->instances[jobSlot]; + if (inst->active && + inst->isStream && + inst->asyncFill.inProgress && + inst->asyncFill.requested && + inst->asyncFill.generation == generation && + inst->asyncFill.bufferIndex == bufferIndex) { + inst->asyncFill.inProgress = false; + inst->asyncFill.ready = true; + inst->asyncFill.success = success; + inst->asyncFill.bufferStartSample = startSample; + inst->asyncFill.sampleCount = producedSamples; + inst->asyncFill.nextCursor = cursor; + inst->asyncFill.adpcmStates[0] = adpcmStates[0]; + inst->asyncFill.adpcmStates[1] = adpcmStates[1]; + } + CondVar_Broadcast(&audio->workerCond); + bool morePending = N3DSAudio_hasPendingAsyncFillLocked(audio); + bool shouldStop = audio->workerStop; + LightLock_Unlock(&audio->lock); + + if (shouldStop) threadExit(0); + if (!morePending) break; + } + } +} + +static bool N3DSAudio_startFileNativeAdpcmStreamPlayback( + N3DSAudioSystem* audio, + N3DSSoundInstance* inst, + const char* path, + const N3DSBcwav* bcwav +) { + if (audio == NULL || inst == NULL || path == NULL || bcwav == NULL) return false; + if (!N3DSAudio_canUseNativeAdpcmPlayback(bcwav, inst->loop)) return false; + + FILE* file = fopen(path, "rb"); + if (file == NULL) return false; + N3DSAudio_configureFileBuffer(file); + + inst->streamFile = file; + inst->channelId = N3DSAudio_acquireChannel(audio); + if (inst->channelId < 0) { + N3DSAudio_cleanupPartialStreamResources(audio, inst); + return false; + } + if (bcwav->channelCount == 2) { + inst->secondaryChannelId = N3DSAudio_acquireChannel(audio); + if (inst->secondaryChannelId < 0) { + N3DSAudio_cleanupPartialStreamResources(audio, inst); + return false; + } + } + + inst->isStream = true; + inst->useNativeAdpcm = true; + inst->sampleRate = bcwav->sampleRate; + inst->sampleCount = bcwav->sampleCount; + inst->channelCount = bcwav->channelCount; + inst->baseRate = (float) bcwav->sampleRate; + inst->bcwav = *bcwav; + + uint32_t maxFrames = (N3DS_STREAM_CHUNK_SAMPLES + 13u) / 14u; + uint32_t bytesPerBuffer = maxFrames * 8u; + repeat(inst->channelCount, channelIndex) { + repeat(N3DS_STREAM_BUFFER_COUNT, bufferIndex) { + inst->streamAdpcm[channelIndex][bufferIndex] = linearAlloc(bytesPerBuffer); + if (inst->streamAdpcm[channelIndex][bufferIndex] == NULL) { + N3DSAudio_cleanupPartialStreamResources(audio, inst); + return false; + } + } + } + + N3DSAudio_streamResetDecoder(inst); + N3DSAudio_rebuildInstanceChannels(audio, inst); + if (!N3DSAudio_primeStream(inst)) { + N3DSAudio_cleanupPartialStreamResources(audio, inst); + return false; + } + return true; +} + +static bool N3DSAudio_startFileStreamPlayback( + N3DSAudioSystem* audio, + N3DSSoundInstance* inst, + const char* path, + const N3DSBcwav* bcwav +) { + if (audio == NULL || inst == NULL || path == NULL || bcwav == NULL) return false; + if (bcwav->sampleRate == 0 || bcwav->channelCount == 0 || bcwav->channelCount > 2) return false; + + FILE* file = fopen(path, "rb"); + if (file == NULL) return false; + N3DSAudio_configureFileBuffer(file); + + inst->channelId = N3DSAudio_acquireChannel(audio); + if (inst->channelId < 0) { + fclose(file); + return false; + } + + inst->streamFile = file; + inst->isStream = true; + inst->sampleRate = bcwav->sampleRate; + inst->sampleCount = bcwav->sampleCount; + inst->channelCount = bcwav->channelCount; + inst->baseRate = (float) bcwav->sampleRate; + inst->bcwav = *bcwav; + + size_t stride = bcwav->channelCount; + size_t samplesPerBuffer = (size_t) N3DS_STREAM_CHUNK_SAMPLES * stride; + repeat(N3DS_STREAM_BUFFER_COUNT, i) { + inst->streamPcm[i] = linearAlloc(samplesPerBuffer * sizeof(int16_t)); + if (inst->streamPcm[i] == NULL) { + return false; + } + } + + N3DSAudio_streamResetDecoder(inst); + N3DSAudio_rebuildInstanceChannels(audio, inst); + return N3DSAudio_primeStream(inst); +} + +static void N3DSAudio_init(AudioSystem* base, DataWin* dataWin, FileSystem* fileSystem) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + if (audio == NULL || audio->initialized) return; + + bool isNew3DS = false; + LightLock_Init(&audio->lock); + LightLock_Init(&audio->missingPathLock); + CondVar_Init(&audio->workerCond); + LightEvent_Init(&audio->workerEvent, RESET_STICKY); + audio->fileSystem = fileSystem; + audio->masterGain = 1.0f; + audio->base.audioGroups = safeCalloc(1, sizeof(DataWin*)); + audio->base.audioGroups[0] = dataWin; + if (R_SUCCEEDED(APT_CheckNew3DS(&isNew3DS))) { + audio->isNew3DS = isNew3DS; + } +#if N3DS_FORCE_OLD3DS_MODE + audio->isNew3DS = false; +#endif + audio->maxCachedSoundBytes = audio->isNew3DS ? N3DS_MAX_PRELOADED_SFX_BYTES_NEW3DS : N3DS_MAX_PRELOADED_SFX_BYTES_OLD3DS; + audio->roomPrewarmSoundCount = audio->isNew3DS ? N3DS_ROOM_PREWARM_SOUND_COUNT_NEW3DS : N3DS_ROOM_PREWARM_SOUND_COUNT_OLD3DS; + audio->roomPrewarmByteBudget = audio->isNew3DS ? N3DS_ROOM_PREWARM_BYTE_BUDGET_NEW3DS : N3DS_ROOM_PREWARM_BYTE_BUDGET_OLD3DS; + audio->backgroundPrewarmSoundCount = audio->isNew3DS ? N3DS_BACKGROUND_PREWARM_SOUND_COUNT_NEW3DS : N3DS_BACKGROUND_PREWARM_SOUND_COUNT_OLD3DS; + audio->backgroundPrewarmByteBudget = audio->isNew3DS ? N3DS_BACKGROUND_PREWARM_BYTE_BUDGET_NEW3DS : N3DS_BACKGROUND_PREWARM_BYTE_BUDGET_OLD3DS; + N3DSAudio_loadPackedSoundBank(audio); + + ndspInit(); + ndspSetOutputMode(NDSP_OUTPUT_STEREO); + N3DSDebugLog_event( + "audio", + "init model=%s cacheCapKB=%lu packedBankKB=%lu", + audio->isNew3DS ? "new3ds" : "old3ds", + (unsigned long) (audio->maxCachedSoundBytes / 1024u), + (unsigned long) (audio->packedSoundBankSize / 1024u) + ); + fprintf( + stderr, + "N3DSAudio: init complete, sounds=%lu, model=%s, cacheCap=%luKB, packedBank=%luKB, roomPrewarm=%lu/%luKB, bgPrewarm=%lu/%luKB\n", + (unsigned long) dataWin->sond.count, + audio->isNew3DS ? "new3ds" : "old3ds", + (unsigned long) (audio->maxCachedSoundBytes / 1024u), + (unsigned long) (audio->packedSoundBankSize / 1024u), + (unsigned long) audio->roomPrewarmSoundCount, + (unsigned long) (audio->roomPrewarmByteBudget / 1024u), + (unsigned long) audio->backgroundPrewarmSoundCount, + (unsigned long) (audio->backgroundPrewarmByteBudget / 1024u) + ); + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + ndspChnReset((int) i); + ndspChnSetInterp((int) i, NDSP_INTERP_LINEAR); + } +#if N3DS_EAGER_SFX_PRELOAD + N3DSAudio_preloadSounds(audio, dataWin); +#else + audio->cachedSounds = safeCalloc(dataWin->sond.count, sizeof(N3DSCachedSound)); + audio->cachedSoundCount = dataWin->sond.count; + fprintf(stderr, "N3DSAudio: eager preload disabled; sounds will cache on first use or the worker thread\n"); +#endif + + audio->workerEnabled = false; +#if N3DS_ENABLE_STREAM_WORKER + if (R_SUCCEEDED(APT_SetAppCpuTimeLimit(N3DS_STREAM_WORKER_CPU_LIMIT))) { + audio->workerThread = threadCreate( + N3DSAudio_streamWorkerMain, + audio, + N3DS_STREAM_WORKER_STACK_SIZE, + N3DS_STREAM_WORKER_PRIORITY, + N3DS_STREAM_WORKER_CORE_ID, + false + ); + audio->workerEnabled = audio->workerThread != NULL; + } +#endif + N3DSDebugLog_event( + "audio", + "worker enabled=%d cpuLimit=%lu nativeAdpcm=%d", + audio->workerEnabled ? 1 : 0, + (unsigned long) N3DS_STREAM_WORKER_CPU_LIMIT, + N3DS_FORCE_PCM_BCWAV_PLAYBACK ? 0 : 1 + ); + audio->initialized = true; +} + +static void N3DSAudio_destroy(AudioSystem* base) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + if (audio->workerEnabled) { + LightLock_Lock(&audio->lock); + audio->workerStop = true; + LightLock_Unlock(&audio->lock); + LightEvent_Signal(&audio->workerEvent); + threadJoin(audio->workerThread, U64_MAX); + threadFree(audio->workerThread); + audio->workerThread = NULL; + audio->workerEnabled = false; + } + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + N3DSAudio_releaseInstance(audio, &audio->instances[i]); + } + repeat(N3DS_MAX_STREAMS, i) { + free(audio->streams[i].path); + } + repeat(audio->cachedSoundCount, i) { + N3DSAudio_releaseCachedSound(&audio->cachedSounds[i]); + } + free(audio->cachedSounds); + free(audio->packedSoundBankEntries); + free(audio->packedSoundBankData); + ndspExit(); + free(audio->base.audioGroups); + free(audio); + + repeat(gN3DSMissingAudioPathCount, i) { + free(gN3DSMissingAudioPaths[i]); + gN3DSMissingAudioPaths[i] = NULL; + } + gN3DSMissingAudioPathCount = 0; +} + +static void N3DSAudio_update(AudioSystem* base, MAYBE_UNUSED float deltaTime) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + u64 nowTick = svcGetSystemTick(); + LightLock_Lock(&audio->lock); +#if !N3DS_EAGER_SFX_PRELOAD + N3DSAudio_requestCachePrewarmLocked(audio, audio->backgroundPrewarmSoundCount, audio->backgroundPrewarmByteBudget); +#endif + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + N3DSSoundInstance* inst = &audio->instances[i]; + if (!inst->active) continue; + + if (!inst->isStream) { + bool finished = N3DSAudio_nonStreamPlaybackFinished(inst, nowTick); + if (finished && !inst->loop) { + N3DSAudio_releaseInstance(audio, inst); + } + continue; + } + + bool anyQueued = false; + repeat(N3DS_STREAM_BUFFER_COUNT, bufferIndex) { + ndspWaveBuf* waveBuf = &inst->waveBufs[bufferIndex]; + bool secondaryReadyToReuse = true; + if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) { + secondaryReadyToReuse = inst->secondaryWaveBufs[bufferIndex].status == NDSP_WBUF_DONE; + } + if (waveBuf->status == NDSP_WBUF_DONE && secondaryReadyToReuse) { + waveBuf->status = NDSP_WBUF_FREE; + if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) { + inst->secondaryWaveBufs[bufferIndex].status = NDSP_WBUF_FREE; + } + } + if (waveBuf->status == NDSP_WBUF_FREE && + inst->asyncFill.ready && + inst->asyncFill.bufferIndex == (int) bufferIndex) { + N3DSAudio_applyAsyncFillLocked(audio, inst); + } + if (waveBuf->status == NDSP_WBUF_FREE && + !inst->streamFinished && + !inst->asyncFill.requested && + !inst->asyncFill.inProgress && + !inst->asyncFill.ready) { + if (audio->workerEnabled) N3DSAudio_requestAsyncFillLocked(audio, inst, (int) bufferIndex); + else { + bool refilled = inst->useNativeAdpcm ? + N3DSAudio_fillNativeStreamWaveBuf(inst, (int) bufferIndex) : + N3DSAudio_fillStreamWaveBuf(inst, (int) bufferIndex); + if (!inst->streamFinished && refilled) { + ndspChnWaveBufAdd(inst->channelId, waveBuf); + if (inst->useNativeAdpcm && inst->secondaryChannelId >= 0) { + ndspChnWaveBufAdd(inst->secondaryChannelId, &inst->secondaryWaveBufs[bufferIndex]); + } + } + } + } + bool primaryBusy = waveBuf->status != NDSP_WBUF_FREE; + bool secondaryBusy = inst->useNativeAdpcm && + inst->secondaryChannelId >= 0 && + inst->secondaryWaveBufs[bufferIndex].status != NDSP_WBUF_FREE; + if (primaryBusy || secondaryBusy) { + anyQueued = true; + } + } + + if (!anyQueued && + !inst->asyncFill.requested && + !inst->asyncFill.inProgress && + !inst->asyncFill.ready) { + inst->streamFinished = true; + N3DSAudio_releaseInstance(audio, inst); + } + } + LightLock_Unlock(&audio->lock); +} + +static int32_t N3DSAudio_playSound(AudioSystem* base, int32_t soundIndex, MAYBE_UNUSED int32_t priority, bool loop) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + char* path = NULL; + bool useStreamingPath = soundIndex >= N3DS_AUDIO_STREAM_INDEX_BASE; + bool useMusicStreamPath = false; + N3DSBcwav streamBcwav; + memset(&streamBcwav, 0, sizeof(streamBcwav)); + Sound* sound = NULL; + N3DSCachedSound* cachedSound = NULL; + const uint8_t* packedBlob = NULL; + uint32_t packedBlobSize = 0; + bool hasPackedBlob = false; + + if (useStreamingPath) { + N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundIndex); + if (stream == NULL) { + LightLock_Unlock(&audio->lock); + return -1; + } + path = safeStrdup(stream->path); + streamBcwav = stream->bcwav; + } else { + DataWin* dw = audio->base.audioGroups[0]; + if (soundIndex < 0 || (uint32_t) soundIndex >= dw->sond.count) { + LightLock_Unlock(&audio->lock); + return -1; + } + sound = &dw->sond.sounds[soundIndex]; + cachedSound = N3DSAudio_getCachedSoundIfAvailable(audio, soundIndex); + if (!N3DSAudio_soundLooksLikeMusic(sound)) { + hasPackedBlob = N3DSAudio_getPackedSoundBlob(audio, soundIndex, &packedBlob, &packedBlobSize); + } + useMusicStreamPath = sound != NULL && loop && N3DSAudio_soundLooksLikeMusic(sound); + if (useMusicStreamPath || (!hasPackedBlob && cachedSound == NULL)) { + const char* resolvedPath = N3DSAudio_getResolvedCachedSoundPath(audio, soundIndex, sound); + if (resolvedPath != NULL) path = safeStrdup(resolvedPath); + } + } + + if (!useStreamingPath && !useMusicStreamPath && loop && N3DSAudio_pathLooksLikeBundledMusic(path)) { + useMusicStreamPath = true; + } + + if (path == NULL && !hasPackedBlob && cachedSound == NULL) { + if (audio->lastResolveFailureSound != soundIndex) { + audio->lastResolveFailureSound = soundIndex; + N3DSDebugLog_event("audio_fail", "resolve failed sound=%ld", (long) soundIndex); + fprintf( + stderr, + "N3DSAudio: could not resolve sound %ld name=%s file=%s audioFile=%ld group=%ld\n", + (long) soundIndex, + sound != NULL && sound->name != NULL ? sound->name : "", + sound != NULL && sound->file != NULL ? sound->file : "", + sound != NULL ? (long) sound->audioFile : -1L, + sound != NULL ? (long) sound->audioGroup : -1L + ); + } + LightLock_Unlock(&audio->lock); + return -1; + } + + bool shouldReplaceExistingInstance = useStreamingPath || (useMusicStreamPath && loop); + if (shouldReplaceExistingInstance) { + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].soundIndex == soundIndex) { + N3DSAudio_releaseInstance(audio, &audio->instances[i]); + } + } + } + + N3DSSoundInstance* inst = N3DSAudio_findFreeInstance(audio, useStreamingPath || useMusicStreamPath); + if (inst == NULL) { + free(path); + LightLock_Unlock(&audio->lock); + return -1; + } + + int32_t slot = (int32_t) (inst - audio->instances); + memset(inst, 0, sizeof(*inst)); + inst->active = true; + inst->loop = loop; + inst->soundIndex = soundIndex; + inst->instanceId = N3DS_SOUND_INSTANCE_ID_BASE + slot; + inst->channelId = -1; + inst->secondaryChannelId = -1; + inst->gain = N3DSAudio_sanitizeGain(sound != NULL ? sound->volume : 1.0f); + inst->pitch = N3DSAudio_sanitizePitch((sound != NULL && sound->pitch > 0.0f) ? sound->pitch : 1.0f); + inst->streamGeneration = 1u; + + if (useStreamingPath || useMusicStreamPath) { + N3DSBcwav musicBcwav; + memset(&musicBcwav, 0, sizeof(musicBcwav)); + + N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundIndex); + if (useStreamingPath) { + if (stream == NULL) { + free(path); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + inst->gain = stream->gain; + inst->pitch = stream->pitch; + musicBcwav = stream->bcwav; + } else { + if (!N3DSAudio_parseBcwavFile(path, &musicBcwav)) { + fprintf(stderr, "N3DSAudio: failed to parse music header %s\n", path); + free(path); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + } + + bool started = N3DSAudio_startFileNativeAdpcmStreamPlayback(audio, inst, path, &musicBcwav); + if (!started) { + N3DSDebugLog_event("audio_fail", "stream start failed sound=%ld path=%s", (long) soundIndex, path); + free(path); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + N3DSDebugLog_event( + "audio_start", + "stream sound=%ld instance=%ld native=%d loop=%d rate=%lu ch=%lu path=%s", + (long) soundIndex, + (long) inst->instanceId, + inst->useNativeAdpcm ? 1 : 0, + loop ? 1 : 0, + (unsigned long) inst->sampleRate, + (unsigned long) inst->channelCount, + path + ); + free(path); + int32_t instanceId = inst->instanceId; + LightLock_Unlock(&audio->lock); + return instanceId; + } + + const uint8_t* blob = NULL; + uint32_t blobSize = 0; + N3DSBcwav bcwav; + const N3DSPackedSoundBankEntry* packedEntry = hasPackedBlob ? N3DSAudio_getPackedSoundEntry(audio, soundIndex) : NULL; + bool uncachedBlobLinear = false; + memset(&bcwav, 0, sizeof(bcwav)); + + if (cachedSound != NULL) { + blob = cachedSound->blob; + blobSize = cachedSound->blobSize; + bcwav = cachedSound->bcwav; + } else if (hasPackedBlob && packedEntry != NULL && N3DSAudio_packedSoundEntryIsPcm16(packedEntry)) { + uint8_t channelCount = N3DSAudio_packedSoundEntryChannelCount(packedEntry); + size_t expectedBytes = (size_t) packedEntry->sampleCount * channelCount * sizeof(int16_t); + if (packedEntry->sampleRate == 0 || + packedEntry->sampleCount == 0 || + channelCount == 0 || + channelCount > 2 || + expectedBytes != packedBlobSize) { + N3DSDebugLog_event("audio_fail", "bank pcm metadata invalid sound=%ld", (long) soundIndex); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + + int16_t* pcm = (int16_t*) N3DSAudio_cloneBlobToLinear(packedBlob, packedBlobSize); + if (pcm == NULL || !N3DSAudio_startOwnedPcmPlayback(audio, inst, pcm, packedEntry->sampleRate, packedEntry->sampleCount, channelCount, loop)) { + if (pcm != NULL) linearFree(pcm); + N3DSDebugLog_event("audio_fail", "bank pcm start failed sound=%ld", (long) soundIndex); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + + int32_t instanceId = inst->instanceId; + LightLock_Unlock(&audio->lock); + return instanceId; + } else if (hasPackedBlob) { + blob = packedBlob; + blobSize = packedBlobSize; + if (!N3DSAudio_getCachedSoundHeader(audio, soundIndex, sound, &bcwav) && + !N3DSAudio_parseBcwavBlob(blob, blobSize, &bcwav)) { + N3DSDebugLog_event("audio_fail", "bank parse failed sound=%ld", (long) soundIndex); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + } else { + uint8_t* uncachedBlob = NULL; + if (!N3DSAudio_readFileFullyLinear(path, &uncachedBlob, &blobSize)) { + N3DSDebugLog_event("audio_fail", "read failed path=%s sound=%ld", path, (long) soundIndex); + fprintf(stderr, "N3DSAudio: failed to read %s\n", path); + free(path); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + blob = uncachedBlob; + uncachedBlobLinear = true; + if (!N3DSAudio_getCachedSoundHeader(audio, soundIndex, sound, &bcwav) && + !N3DSAudio_parseBcwavBlob(blob, blobSize, &bcwav)) { + N3DSDebugLog_event("audio_fail", "blob parse failed sound=%ld path=%s", (long) soundIndex, path); + fprintf(stderr, "N3DSAudio: failed to parse in-memory BCWAV\n"); + linearFree((void*) blob); + free(path); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + } + free(path); + if (cachedSound == NULL && soundIndex >= 0 && (uint32_t) soundIndex < audio->cachedSoundCount) { + N3DSCachedSound* headerCache = &audio->cachedSounds[soundIndex]; + headerCache->headerAttempted = true; + headerCache->headerAvailable = true; + headerCache->bcwav = bcwav; + } + + if (N3DSAudio_canUseNativeAdpcmPlayback(&bcwav, loop)) { + bool started = false; + if (cachedSound != NULL) { + started = N3DSAudio_startNativeAdpcmPlayback(audio, inst, blob, blobSize, &bcwav); + } else if (hasPackedBlob) { + started = N3DSAudio_startNativeAdpcmPlayback(audio, inst, blob, blobSize, &bcwav); + } else { + started = uncachedBlobLinear + ? N3DSAudio_startOwnedLinearNativeAdpcmPlayback(audio, inst, blob, blobSize, &bcwav) + : N3DSAudio_startNativeAdpcmPlayback(audio, inst, blob, blobSize, &bcwav); + } + if (!started) { + N3DSDebugLog_event("audio_fail", "native start failed sound=%ld", (long) soundIndex); + if (cachedSound == NULL && !hasPackedBlob) { + if (uncachedBlobLinear) linearFree((void*) blob); + else free((void*) blob); + } + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + int32_t instanceId = inst->instanceId; + LightLock_Unlock(&audio->lock); + return instanceId; + } + + int16_t* pcm = N3DSAudio_decodeBcwavToPcm(blob, &bcwav); + if (cachedSound == NULL && !hasPackedBlob) { + if (uncachedBlobLinear) linearFree((void*) blob); + else free((void*) blob); + } + if (pcm == NULL) { + N3DSDebugLog_event("audio_fail", "pcm decode failed sound=%ld", (long) soundIndex); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + if (!N3DSAudio_startOwnedPcmPlayback(audio, inst, pcm, bcwav.sampleRate, bcwav.sampleCount, bcwav.channelCount, loop)) { + linearFree(pcm); + N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return -1; + } + + int32_t instanceId = inst->instanceId; + LightLock_Unlock(&audio->lock); + return instanceId; +} + +static void N3DSAudio_prewarmRoom(AudioSystem* base, MAYBE_UNUSED Runner* runner) { +#if !N3DS_EAGER_SFX_PRELOAD + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + N3DSAudio_requestCachePrewarmLocked(audio, audio->roomPrewarmSoundCount, audio->roomPrewarmByteBudget); + LightLock_Unlock(&audio->lock); +#else + (void) base; + (void) runner; +#endif +} + +static void N3DSAudio_stopSound(AudioSystem* base, int32_t soundOrInstance) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + if (N3DSAudio_isInstanceId(soundOrInstance)) { + N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + if (inst != NULL) N3DSAudio_releaseInstance(audio, inst); + LightLock_Unlock(&audio->lock); + return; + } + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) { + N3DSAudio_releaseInstance(audio, &audio->instances[i]); + } + } + LightLock_Unlock(&audio->lock); +} + +static void N3DSAudio_stopAll(AudioSystem* base) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + N3DSAudio_releaseInstance(audio, &audio->instances[i]); + } + LightLock_Unlock(&audio->lock); +} + +static bool N3DSAudio_isPlaying(AudioSystem* base, int32_t soundOrInstance) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + if (N3DSAudio_isInstanceId(soundOrInstance)) { + N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + bool isPlaying = inst != NULL && ndspChnIsPlaying(inst->channelId) && !inst->paused; + LightLock_Unlock(&audio->lock); + return isPlaying; + } + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) { + bool isPlaying = ndspChnIsPlaying(audio->instances[i].channelId) && !audio->instances[i].paused; + LightLock_Unlock(&audio->lock); + return isPlaying; + } + } + LightLock_Unlock(&audio->lock); + return false; +} + +static void N3DSAudio_pauseSound(AudioSystem* base, int32_t soundOrInstance) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + if (N3DSAudio_isInstanceId(soundOrInstance)) { + N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + if (inst != NULL) { + inst->paused = true; + ndspChnSetPaused(inst->channelId, true); + if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, true); + } + LightLock_Unlock(&audio->lock); + return; + } + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + N3DSSoundInstance* inst = &audio->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) { + inst->paused = true; + ndspChnSetPaused(inst->channelId, true); + if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, true); + } + } + LightLock_Unlock(&audio->lock); +} + +static void N3DSAudio_resumeSound(AudioSystem* base, int32_t soundOrInstance) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + if (N3DSAudio_isInstanceId(soundOrInstance)) { + N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + if (inst != NULL) { + inst->paused = false; + ndspChnSetPaused(inst->channelId, false); + if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, false); + } + LightLock_Unlock(&audio->lock); + return; + } + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + N3DSSoundInstance* inst = &audio->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) { + inst->paused = false; + ndspChnSetPaused(inst->channelId, false); + if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, false); + } + } + LightLock_Unlock(&audio->lock); +} + +static void N3DSAudio_pauseAll(AudioSystem* base) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active) { + audio->instances[i].paused = true; + ndspChnSetPaused(audio->instances[i].channelId, true); + if (audio->instances[i].secondaryChannelId >= 0) ndspChnSetPaused(audio->instances[i].secondaryChannelId, true); + } + } + LightLock_Unlock(&audio->lock); +} + +static void N3DSAudio_resumeAll(AudioSystem* base) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active) { + audio->instances[i].paused = false; + ndspChnSetPaused(audio->instances[i].channelId, false); + if (audio->instances[i].secondaryChannelId >= 0) ndspChnSetPaused(audio->instances[i].secondaryChannelId, false); + } + } + LightLock_Unlock(&audio->lock); +} + +static void N3DSAudio_setSoundGain(AudioSystem* base, int32_t soundOrInstance, float gain, MAYBE_UNUSED uint32_t timeMs) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + gain = N3DSAudio_sanitizeGain(gain); + if (N3DSAudio_isInstanceId(soundOrInstance)) { + N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + if (inst == NULL) { + LightLock_Unlock(&audio->lock); + return; + } + inst->gain = gain; + if (inst->soundIndex >= N3DS_AUDIO_STREAM_INDEX_BASE) { + N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, inst->soundIndex); + if (stream != NULL) stream->gain = gain; + } + N3DSAudio_applyMix(audio, inst); + LightLock_Unlock(&audio->lock); + return; + } + N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundOrInstance); + if (stream != NULL) stream->gain = gain; + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + N3DSSoundInstance* inst = &audio->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) { + inst->gain = gain; + N3DSAudio_applyMix(audio, inst); + } + } + LightLock_Unlock(&audio->lock); +} + +static float N3DSAudio_getSoundGain(AudioSystem* base, int32_t soundOrInstance) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + if (N3DSAudio_isInstanceId(soundOrInstance)) { + N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + float gain = inst != NULL ? inst->gain : 0.0f; + LightLock_Unlock(&audio->lock); + return gain; + } + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) { + float gain = audio->instances[i].gain; + LightLock_Unlock(&audio->lock); + return gain; + } + } + N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundOrInstance); + if (stream != NULL) { + float gain = stream->gain; + LightLock_Unlock(&audio->lock); + return gain; + } + LightLock_Unlock(&audio->lock); + return 0.0f; +} + +static void N3DSAudio_setSoundPitch(AudioSystem* base, int32_t soundOrInstance, float pitch) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + pitch = N3DSAudio_sanitizePitch(pitch); + if (N3DSAudio_isInstanceId(soundOrInstance)) { + N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + if (inst == NULL) { + LightLock_Unlock(&audio->lock); + return; + } + inst->pitch = pitch; + if (inst->soundIndex >= N3DS_AUDIO_STREAM_INDEX_BASE) { + N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, inst->soundIndex); + if (stream != NULL) stream->pitch = pitch; + } + N3DSAudio_applyMix(audio, inst); + N3DSAudio_refreshNonStreamReleaseTick(inst); + LightLock_Unlock(&audio->lock); + return; + } + N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundOrInstance); + if (stream != NULL) stream->pitch = pitch; + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + N3DSSoundInstance* inst = &audio->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) { + inst->pitch = pitch; + N3DSAudio_applyMix(audio, inst); + N3DSAudio_refreshNonStreamReleaseTick(inst); + } + } + LightLock_Unlock(&audio->lock); +} + +static float N3DSAudio_getSoundPitch(AudioSystem* base, int32_t soundOrInstance) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + if (N3DSAudio_isInstanceId(soundOrInstance)) { + N3DSSoundInstance* inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + float pitch = inst != NULL ? inst->pitch : 1.0f; + LightLock_Unlock(&audio->lock); + return pitch; + } + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) { + float pitch = audio->instances[i].pitch; + LightLock_Unlock(&audio->lock); + return pitch; + } + } + N3DSStreamEntry* stream = N3DSAudio_getActiveStreamEntry(audio, soundOrInstance); + if (stream != NULL) { + float pitch = stream->pitch; + LightLock_Unlock(&audio->lock); + return pitch; + } + LightLock_Unlock(&audio->lock); + return 1.0f; +} + +static float N3DSAudio_getTrackPosition(AudioSystem* base, int32_t soundOrInstance) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + N3DSSoundInstance* inst = NULL; + if (N3DSAudio_isInstanceId(soundOrInstance)) { + inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + } else { + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) { + inst = &audio->instances[i]; + break; + } + } + } + if (inst == NULL || inst->sampleRate == 0) { + LightLock_Unlock(&audio->lock); + return 0.0f; + } + + if (inst->useNativeAdpcm) { + uint16_t currentSeq = ndspChnGetWaveBufSeq(inst->channelId); + repeat(N3DS_STREAM_BUFFER_COUNT, i) { + if (inst->waveBufs[i].sequence_id == currentSeq) { + uint32_t samplePos = ndspChnGetSamplePos(inst->channelId); + float position = (float) (inst->bufferStartSample[i] + samplePos) / (float) inst->sampleRate; + LightLock_Unlock(&audio->lock); + return position; + } + } + float position = (float) ndspChnGetSamplePos(inst->channelId) / (float) inst->sampleRate; + LightLock_Unlock(&audio->lock); + return position; + } + + if (!inst->isStream) { + float position = (float) ndspChnGetSamplePos(inst->channelId) / (float) inst->sampleRate; + LightLock_Unlock(&audio->lock); + return position; + } + + uint16_t currentSeq = ndspChnGetWaveBufSeq(inst->channelId); + repeat(N3DS_STREAM_BUFFER_COUNT, i) { + if (inst->waveBufs[i].sequence_id == currentSeq) { + uint32_t samplePos = ndspChnGetSamplePos(inst->channelId); + float position = (float) (inst->bufferStartSample[i] + samplePos) / (float) inst->sampleRate; + LightLock_Unlock(&audio->lock); + return position; + } + } + + float position = (float) inst->currentSample / (float) inst->sampleRate; + LightLock_Unlock(&audio->lock); + return position; +} + +static void N3DSAudio_setTrackPosition(AudioSystem* base, int32_t soundOrInstance, float positionSeconds) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + N3DSSoundInstance* inst = NULL; + if (N3DSAudio_isInstanceId(soundOrInstance)) { + inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + } else { + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) { + inst = &audio->instances[i]; + break; + } + } + } + if (inst == NULL || inst->sampleRate == 0) { + LightLock_Unlock(&audio->lock); + return; + } + + uint32_t targetSample = (uint32_t) (positionSeconds * (float) inst->sampleRate); + if (inst->useNativeAdpcm && !inst->isStream) { + if (targetSample != 0) { + N3DSDebugLog_event("audio_seek", "reject native instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample); + LightLock_Unlock(&audio->lock); + return; + } + N3DSDebugLog_event("audio_seek", "native instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample); + N3DSAudio_rebuildInstanceChannels(audio, inst); + if (!N3DSAudio_primeNativeAdpcm(inst)) { + LightLock_Unlock(&audio->lock); + return; + } + if (inst->paused) { + ndspChnSetPaused(inst->channelId, true); + if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, true); + } + LightLock_Unlock(&audio->lock); + return; + } + + if (inst->useNativeAdpcm && inst->isStream) { + N3DSAudio_cancelAsyncFillLocked(audio, inst); + if (!N3DSAudio_isFrameAlignedSample(targetSample)) { + targetSample = (targetSample / 14u) * 14u; + } + N3DSDebugLog_event("audio_seek", "native-stream instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample); + N3DSAudio_rebuildInstanceChannels(audio, inst); + if (!N3DSAudio_streamSeekSamples(inst, targetSample)) { + LightLock_Unlock(&audio->lock); + return; + } + if (!N3DSAudio_primeStream(inst)) { + LightLock_Unlock(&audio->lock); + return; + } + if (inst->paused) { + ndspChnSetPaused(inst->channelId, true); + if (inst->secondaryChannelId >= 0) ndspChnSetPaused(inst->secondaryChannelId, true); + } + LightLock_Unlock(&audio->lock); + return; + } + + if (!inst->isStream) { + N3DSDebugLog_event("audio_seek", "pcm instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample); + N3DSAudio_rebuildInstanceChannels(audio, inst); + N3DSAudio_resetInstanceWaveBufState(inst); + if (targetSample >= inst->sampleCount) targetSample = inst->sampleCount > 0 ? inst->sampleCount - 1 : 0; + inst->waveBufs[0].data_pcm16 = inst->pcmData + (size_t) targetSample * inst->channelCount; + inst->waveBufs[0].nsamples = inst->sampleCount - targetSample; + inst->waveBufs[0].looping = inst->loop; + ndspChnWaveBufAdd(inst->channelId, &inst->waveBufs[0]); + LightLock_Unlock(&audio->lock); + return; + } + + N3DSAudio_cancelAsyncFillLocked(audio, inst); + N3DSDebugLog_event("audio_seek", "stream instance=%ld target=%lu", (long) inst->instanceId, (unsigned long) targetSample); + N3DSAudio_rebuildInstanceChannels(audio, inst); + if (!N3DSAudio_streamSeekSamples(inst, targetSample)) { + LightLock_Unlock(&audio->lock); + return; + } + N3DSAudio_primeStream(inst); + if (inst->paused) ndspChnSetPaused(inst->channelId, true); + LightLock_Unlock(&audio->lock); +} + +static float N3DSAudio_getSoundLength(AudioSystem* base, int32_t soundOrInstance) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + N3DSSoundInstance* inst = NULL; + if (N3DSAudio_isInstanceId(soundOrInstance)) { + inst = N3DSAudio_findInstanceById(audio, soundOrInstance); + } else { + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].soundIndex == soundOrInstance) { + inst = &audio->instances[i]; + break; + } + } + } + if (inst != NULL && inst->sampleRate > 0) { + float length = (float) inst->sampleCount / (float) inst->sampleRate; + LightLock_Unlock(&audio->lock); + return length; + } + + if (soundOrInstance >= N3DS_AUDIO_STREAM_INDEX_BASE) { + int32_t slot = soundOrInstance - N3DS_AUDIO_STREAM_INDEX_BASE; + if (slot >= 0 && slot < N3DS_MAX_STREAMS && audio->streams[slot].active && audio->streams[slot].bcwav.sampleRate > 0) { + float length = (float) audio->streams[slot].bcwav.sampleCount / (float) audio->streams[slot].bcwav.sampleRate; + LightLock_Unlock(&audio->lock); + return length; + } + } + + LightLock_Unlock(&audio->lock); + return 0.0f; +} + +static void N3DSAudio_setMasterGain(AudioSystem* base, float gain) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + audio->masterGain = gain; + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active) N3DSAudio_applyMix(audio, &audio->instances[i]); + } + LightLock_Unlock(&audio->lock); +} + +static void N3DSAudio_setChannelCount(MAYBE_UNUSED AudioSystem* base, MAYBE_UNUSED int32_t count) {} +static void N3DSAudio_groupLoad(MAYBE_UNUSED AudioSystem* base, MAYBE_UNUSED int32_t groupIndex) {} +static bool N3DSAudio_groupIsLoaded(MAYBE_UNUSED AudioSystem* base, MAYBE_UNUSED int32_t groupIndex) { return true; } + +static int32_t N3DSAudio_createStream(AudioSystem* base, const char* filename) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + if (filename == NULL || filename[0] == '\0') { + LightLock_Unlock(&audio->lock); + return -1; + } + + char* path = N3DSAudio_resolveAudioFilePath(audio, filename); + if (path == NULL) { + fprintf(stderr, "N3DSAudio: createStream failed to resolve %s\n", filename); + LightLock_Unlock(&audio->lock); + return -1; + } + + repeat(N3DS_MAX_STREAMS, i) { + if (audio->streams[i].active && audio->streams[i].path != NULL && strcmp(audio->streams[i].path, path) == 0) { + audio->streams[i].refCount += 1u; + free(path); + LightLock_Unlock(&audio->lock); + return N3DS_AUDIO_STREAM_INDEX_BASE + (int32_t) i; + } + } + + repeat(N3DS_MAX_STREAMS, i) { + if (!audio->streams[i].active) { + N3DSBcwav bcwav; + if (!N3DSAudio_parseBcwavFile(path, &bcwav)) { + free(path); + LightLock_Unlock(&audio->lock); + return -1; + } + + audio->streams[i].active = true; + audio->streams[i].path = path; + audio->streams[i].blobData = NULL; + audio->streams[i].blobSize = 0; + audio->streams[i].bcwav = bcwav; + audio->streams[i].gain = 1.0f; + audio->streams[i].pitch = 1.0f; + audio->streams[i].refCount = 1u; + LightLock_Unlock(&audio->lock); + return N3DS_AUDIO_STREAM_INDEX_BASE + (int32_t) i; + } + } + fprintf(stderr, "N3DSAudio: createStream exhausted slots for %s\n", path); + free(path); + LightLock_Unlock(&audio->lock); + return -1; +} + +static bool N3DSAudio_destroyStream(AudioSystem* base, int32_t streamIndex) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + LightLock_Lock(&audio->lock); + int32_t slot = streamIndex - N3DS_AUDIO_STREAM_INDEX_BASE; + if (slot < 0 || slot >= N3DS_MAX_STREAMS || !audio->streams[slot].active) { + LightLock_Unlock(&audio->lock); + return false; + } + + if (audio->streams[slot].refCount > 1u) { + audio->streams[slot].refCount -= 1u; + LightLock_Unlock(&audio->lock); + return true; + } + + repeat(N3DS_MAX_SOUND_INSTANCES, i) { + if (audio->instances[i].active && audio->instances[i].soundIndex == streamIndex) { + N3DSAudio_releaseInstance(audio, &audio->instances[i]); + } + } + + free(audio->streams[slot].blobData); + free(audio->streams[slot].path); + memset(&audio->streams[slot], 0, sizeof(audio->streams[slot])); + LightLock_Unlock(&audio->lock); + return true; +} + +static AudioSystemVtable N3DSAudio_vtable = { + .init = N3DSAudio_init, + .destroy = N3DSAudio_destroy, + .update = N3DSAudio_update, + .playSound = N3DSAudio_playSound, + .stopSound = N3DSAudio_stopSound, + .stopAll = N3DSAudio_stopAll, + .isPlaying = N3DSAudio_isPlaying, + .pauseSound = N3DSAudio_pauseSound, + .resumeSound = N3DSAudio_resumeSound, + .pauseAll = N3DSAudio_pauseAll, + .resumeAll = N3DSAudio_resumeAll, + .setSoundGain = N3DSAudio_setSoundGain, + .getSoundGain = N3DSAudio_getSoundGain, + .setSoundPitch = N3DSAudio_setSoundPitch, + .getSoundPitch = N3DSAudio_getSoundPitch, + .getTrackPosition = N3DSAudio_getTrackPosition, + .setTrackPosition = N3DSAudio_setTrackPosition, + .getSoundLength = N3DSAudio_getSoundLength, + .setMasterGain = N3DSAudio_setMasterGain, + .setChannelCount = N3DSAudio_setChannelCount, + .groupLoad = N3DSAudio_groupLoad, + .groupIsLoaded = N3DSAudio_groupIsLoaded, + .createStream = N3DSAudio_createStream, + .destroyStream = N3DSAudio_destroyStream, + .prewarmRoom = N3DSAudio_prewarmRoom, +}; + +N3DSAudioSystem* N3DSAudioSystem_create(void) { + N3DSAudioSystem* audio = safeCalloc(1, sizeof(N3DSAudioSystem)); + audio->base.vtable = &N3DSAudio_vtable; + audio->masterGain = 1.0f; + audio->lastResolveFailureSound = INT32_MIN; + return audio; +} + +void N3DSAudioSystem_getCacheStats( + AudioSystem* base, + uint32_t* outCachedSounds, + uint32_t* outTotalSounds, + uint32_t* outCachedBytes, + uint32_t* outCacheLimitBytes +) { + N3DSAudioSystem* audio = (N3DSAudioSystem*) base; + if (outCachedSounds != NULL) *outCachedSounds = 0; + if (outTotalSounds != NULL) *outTotalSounds = 0; + if (outCachedBytes != NULL) *outCachedBytes = 0; + if (outCacheLimitBytes != NULL) *outCacheLimitBytes = 0; + if (audio == NULL) return; + + uint32_t cachedSounds = 0; + LightLock_Lock(&audio->lock); + if (audio->cachedSounds != NULL) { + repeat(audio->cachedSoundCount, i) { + if (audio->cachedSounds[i].available) cachedSounds++; + } + } + if (outCachedSounds != NULL) *outCachedSounds = cachedSounds; + if (outTotalSounds != NULL) *outTotalSounds = audio->cachedSoundCount; + if (outCachedBytes != NULL) *outCachedBytes = audio->cachedSoundBytes; + if (outCacheLimitBytes != NULL) *outCacheLimitBytes = audio->maxCachedSoundBytes; + LightLock_Unlock(&audio->lock); +} diff --git a/src/n3ds/n3ds_audio_system.h b/src/n3ds/n3ds_audio_system.h new file mode 100644 index 00000000..81d6126c --- /dev/null +++ b/src/n3ds/n3ds_audio_system.h @@ -0,0 +1,14 @@ +#pragma once + +#include "../audio_system.h" + +typedef struct N3DSAudioSystem N3DSAudioSystem; + +N3DSAudioSystem* N3DSAudioSystem_create(void); +void N3DSAudioSystem_getCacheStats( + AudioSystem* base, + uint32_t* outCachedSounds, + uint32_t* outTotalSounds, + uint32_t* outCachedBytes, + uint32_t* outCacheLimitBytes +); diff --git a/src/n3ds/n3ds_debug_log.c b/src/n3ds/n3ds_debug_log.c new file mode 100644 index 00000000..ba5c2f1f --- /dev/null +++ b/src/n3ds/n3ds_debug_log.c @@ -0,0 +1,120 @@ +#include "n3ds_debug_log.h" + +#if N3DS_DEBUG_BREADCRUMB_LOGGING + +#include <3ds.h> + +#include +#include +#include +#include +#include + +#define N3DS_DEBUG_HISTORY_LOG_PATH "sdmc:/3ds/cinnamon/crash_history.log" +#define N3DS_DEBUG_MARKER_LOG_PATH "sdmc:/3ds/cinnamon/last_breadcrumb.txt" +#define N3DS_DEBUG_MARKER_BUFFER_SIZE 256 + +static FILE* gN3DSDebugHistoryLog = NULL; +static FILE* gN3DSDebugMarkerLog = NULL; +static u64 gN3DSDebugStartTick = 0; +static uint32_t gN3DSDebugEventCounter = 0; +static char gN3DSDebugLastMarker[N3DS_DEBUG_MARKER_BUFFER_SIZE]; + +static double N3DSDebugLog_ticksToMs(u64 ticks) { + return (double) ticks * 1000.0 / (double) SYSCLOCK_ARM11; +} + +static void N3DSDebugLog_openIfNeeded(void) { + if (gN3DSDebugStartTick == 0) gN3DSDebugStartTick = svcGetSystemTick(); + + mkdir("sdmc:/3ds", 0777); + mkdir("sdmc:/3ds/cinnamon", 0777); + + if (gN3DSDebugHistoryLog == NULL) { + gN3DSDebugHistoryLog = fopen(N3DS_DEBUG_HISTORY_LOG_PATH, "a"); + if (gN3DSDebugHistoryLog != NULL) { + setvbuf(gN3DSDebugHistoryLog, NULL, _IOLBF, 0); + } + } + + if (gN3DSDebugMarkerLog == NULL) { + gN3DSDebugMarkerLog = fopen(N3DS_DEBUG_MARKER_LOG_PATH, "w"); + if (gN3DSDebugMarkerLog != NULL) { + setvbuf(gN3DSDebugMarkerLog, NULL, _IONBF, 0); + } + } +} + +void N3DSDebugLog_init(void) { + N3DSDebugLog_openIfNeeded(); + N3DSDebugLog_event("boot", "debug log initialized"); +} + +void N3DSDebugLog_close(void) { + if (gN3DSDebugMarkerLog != NULL) { + fclose(gN3DSDebugMarkerLog); + gN3DSDebugMarkerLog = NULL; + } + if (gN3DSDebugHistoryLog != NULL) { + fclose(gN3DSDebugHistoryLog); + gN3DSDebugHistoryLog = NULL; + } +} + +void N3DSDebugLog_event(const char* tag, const char* fmt, ...) { + N3DSDebugLog_openIfNeeded(); + if (gN3DSDebugHistoryLog == NULL) return; + + char message[160]; + va_list args; + va_start(args, fmt); + vsnprintf(message, sizeof(message), fmt != NULL ? fmt : "", args); + va_end(args); + + double elapsedMs = N3DSDebugLog_ticksToMs(svcGetSystemTick() - gN3DSDebugStartTick); + fprintf( + gN3DSDebugHistoryLog, + "[%10.2f ms] #%06lu %-10s %s\n", + elapsedMs, + (unsigned long) ++gN3DSDebugEventCounter, + tag != NULL ? tag : "event", + message + ); + fflush(gN3DSDebugHistoryLog); +} + +void N3DSDebugLog_setMarker( + const char* stage, + uint32_t frame, + const char* roomName, + int32_t detailA, + int32_t detailB +) { + N3DSDebugLog_openIfNeeded(); + if (gN3DSDebugMarkerLog == NULL) return; + + char line[N3DS_DEBUG_MARKER_BUFFER_SIZE]; + double elapsedMs = N3DSDebugLog_ticksToMs(svcGetSystemTick() - gN3DSDebugStartTick); + snprintf( + line, + sizeof(line), + "t=%10.2fms frame=%lu stage=%s room=%s a=%ld b=%ld", + elapsedMs, + (unsigned long) frame, + stage != NULL ? stage : "", + roomName != NULL ? roomName : "(none)", + (long) detailA, + (long) detailB + ); + + if (strncmp(line, gN3DSDebugLastMarker, sizeof(gN3DSDebugLastMarker)) == 0) { + return; + } + snprintf(gN3DSDebugLastMarker, sizeof(gN3DSDebugLastMarker), "%s", line); + + rewind(gN3DSDebugMarkerLog); + fprintf(gN3DSDebugMarkerLog, "%-240s\n", line); + fflush(gN3DSDebugMarkerLog); +} + +#endif diff --git a/src/n3ds/n3ds_debug_log.h b/src/n3ds/n3ds_debug_log.h new file mode 100644 index 00000000..914df4e6 --- /dev/null +++ b/src/n3ds/n3ds_debug_log.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#ifndef N3DS_DEBUG_BREADCRUMB_LOGGING +#define N3DS_DEBUG_BREADCRUMB_LOGGING 0 +#endif + +#if N3DS_DEBUG_BREADCRUMB_LOGGING +void N3DSDebugLog_init(void); +void N3DSDebugLog_close(void); +void N3DSDebugLog_event(const char* tag, const char* fmt, ...); +void N3DSDebugLog_setMarker( + const char* stage, + uint32_t frame, + const char* roomName, + int32_t detailA, + int32_t detailB +); +#else +#define N3DSDebugLog_init() ((void) 0) +#define N3DSDebugLog_close() ((void) 0) +#define N3DSDebugLog_event(...) ((void) 0) +#define N3DSDebugLog_setMarker(...) ((void) 0) +#endif diff --git a/src/n3ds/n3ds_file_system.c b/src/n3ds/n3ds_file_system.c new file mode 100644 index 00000000..c3f6976a --- /dev/null +++ b/src/n3ds/n3ds_file_system.c @@ -0,0 +1,329 @@ +#include "n3ds_file_system.h" + +#include "../utils.h" + +#include <3ds.h> +#include +#include +#include +#include +#include + +#define N3DS_ENABLE_LOGGING 0 +#define N3DS_FILE_BUFFER_SIZE (32u * 1024u) + +typedef struct N3DSFileSystemResolvedPathEntry { + char* key; + char* value; +} N3DSFileSystemResolvedPathEntry; + +static FILE* gN3DSSdIoLogFile = NULL; + +static double N3DSFileSystem_ticksToMs(u64 ticks) { + return (double) ticks * 1000.0 / (double) SYSCLOCK_ARM11; +} + +static bool N3DSFileSystem_isSdmcPath(const char* path) { + return path != NULL && strncmp(path, "sdmc:/", 6) == 0; +} + +static void N3DSFileSystem_openSdIoLog(void) { +#if !N3DS_ENABLE_LOGGING + return; +#else + if (gN3DSSdIoLogFile != NULL) return; + gN3DSSdIoLogFile = fopen("sdmc:/3ds/cinnamon/sd_io.log", "a"); + if (gN3DSSdIoLogFile != NULL) { + setvbuf(gN3DSSdIoLogFile, NULL, _IOLBF, 0); + } +#endif +} + +static void N3DSFileSystem_logSdIo( + const char* op, + const char* path, + double elapsedMs, + long sizeBytes, + bool ok +) { +#if !N3DS_ENABLE_LOGGING + (void) op; + (void) path; + (void) elapsedMs; + (void) sizeBytes; + (void) ok; + return; +#else + if (path == NULL || !N3DSFileSystem_isSdmcPath(path)) return; + if (elapsedMs < 0.25 && ok) return; + + N3DSFileSystem_openSdIoLog(); + fprintf( + stderr, + "N3DS SDIO: %-10s %7.2f ms %s size=%ld ok=%d\n", + op != NULL ? op : "", + elapsedMs, + path, + sizeBytes, + ok ? 1 : 0 + ); + if (gN3DSSdIoLogFile != NULL) { + fprintf( + gN3DSSdIoLogFile, + "N3DS SDIO: %-10s %7.2f ms %s size=%ld ok=%d\n", + op != NULL ? op : "", + elapsedMs, + path, + sizeBytes, + ok ? 1 : 0 + ); + fflush(gN3DSSdIoLogFile); + } +#endif +} + +static bool N3DSFileSystem_hasScheme(const char* path) { + return path != NULL && strstr(path, ":/") != NULL; +} + +static void N3DSFileSystem_configureFileBuffer(FILE* file) { + if (file == NULL) return; + setvbuf(file, NULL, _IOFBF, N3DS_FILE_BUFFER_SIZE); +} + +static bool N3DSFileSystem_pathExists(const char* path) { + u64 startTick = svcGetSystemTick(); + struct stat st; + bool ok = path != NULL && stat(path, &st) == 0; + N3DSFileSystem_logSdIo("stat", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, ok); + return ok; +} + +static char* N3DSFileSystem_join(const char* base, const char* relativePath) { + if (relativePath == NULL) return NULL; + if (N3DSFileSystem_hasScheme(relativePath)) return safeStrdup(relativePath); + + size_t baseLen = strlen(base); + size_t relLen = strlen(relativePath); + bool needSlash = baseLen > 0 && base[baseLen - 1] != '/' && relLen > 0 && relativePath[0] != '/'; + char* result = safeMalloc(baseLen + relLen + (needSlash ? 2 : 1)); + memcpy(result, base, baseLen); + size_t cursor = baseLen; + if (needSlash) result[cursor++] = '/'; + memcpy(result + cursor, relativePath, relLen); + result[cursor + relLen] = '\0'; + return result; +} + +static char* N3DSFileSystem_getCachedResolvedPath(N3DSFileSystem* n3ds, const char* relativePath) { + if (n3ds == NULL || relativePath == NULL) return NULL; + ptrdiff_t idx = shgeti(n3ds->resolvedPathCache, relativePath); + if (idx < 0) return NULL; + return safeStrdup(n3ds->resolvedPathCache[idx].value); +} + +static void N3DSFileSystem_setCachedResolvedPath(N3DSFileSystem* n3ds, const char* relativePath, const char* resolvedPath) { + if (n3ds == NULL || relativePath == NULL || resolvedPath == NULL) return; + ptrdiff_t idx = shgeti(n3ds->resolvedPathCache, relativePath); + if (idx >= 0) { + free(n3ds->resolvedPathCache[idx].value); + n3ds->resolvedPathCache[idx].value = safeStrdup(resolvedPath); + return; + } + + shput(n3ds->resolvedPathCache, relativePath, safeStrdup(resolvedPath)); +} + +static char* N3DSFileSystem_resolvePath(FileSystem* fs, const char* relativePath) { + N3DSFileSystem* n3ds = (N3DSFileSystem*) fs; + if (relativePath == NULL) return NULL; + if (N3DSFileSystem_hasScheme(relativePath)) return safeStrdup(relativePath); + + char* cachedPath = N3DSFileSystem_getCachedResolvedPath(n3ds, relativePath); + if (cachedPath != NULL) return cachedPath; + + char* romfsPath = N3DSFileSystem_join(n3ds->romfsBasePath, relativePath); + if (N3DSFileSystem_pathExists(romfsPath)) { + N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, romfsPath); + return romfsPath; + } + + char* savePath = N3DSFileSystem_join(n3ds->saveBasePath, relativePath); + N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, savePath); + free(romfsPath); + return savePath; +} + +static bool N3DSFileSystem_fileExists(FileSystem* fs, const char* relativePath) { + u64 startTick = svcGetSystemTick(); + char* path = N3DSFileSystem_resolvePath(fs, relativePath); + struct stat st; + bool ok = path != NULL && stat(path, &st) == 0; + N3DSFileSystem_logSdIo("exists", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, ok); + free(path); + return ok; +} + +static char* N3DSFileSystem_readFileText(FileSystem* fs, const char* relativePath) { + u64 startTick = svcGetSystemTick(); + char* path = N3DSFileSystem_resolvePath(fs, relativePath); + if (path == NULL) return NULL; + + FILE* file = fopen(path, "rb"); + if (file == NULL) { + N3DSFileSystem_logSdIo("readText", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, false); + free(path); + return NULL; + } + N3DSFileSystem_configureFileBuffer(file); + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size < 0) { + fclose(file); + return NULL; + } + + char* text = safeMalloc((size_t) size + 1); + size_t bytesRead = fread(text, 1, (size_t) size, file); + fclose(file); + text[bytesRead] = '\0'; + N3DSFileSystem_logSdIo("readText", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), (long) bytesRead, true); + free(path); + return text; +} + +static bool N3DSFileSystem_writeFileText(FileSystem* fs, const char* relativePath, const char* contents) { + N3DSFileSystem* n3ds = (N3DSFileSystem*) fs; + u64 startTick = svcGetSystemTick(); + char* path = N3DSFileSystem_join(n3ds->saveBasePath, relativePath); + if (path == NULL) return false; + + FILE* file = fopen(path, "wb"); + if (file == NULL) { + N3DSFileSystem_logSdIo("writeText", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, false); + free(path); + return false; + } + N3DSFileSystem_configureFileBuffer(file); + + size_t length = strlen(contents); + bool ok = fwrite(contents, 1, length, file) == length; + fclose(file); + N3DSFileSystem_logSdIo("writeText", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), (long) length, ok); + if (ok) { + N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, path); + } + free(path); + return ok; +} + +static bool N3DSFileSystem_deleteFile(FileSystem* fs, const char* relativePath) { + N3DSFileSystem* n3ds = (N3DSFileSystem*) fs; + u64 startTick = svcGetSystemTick(); + char* path = N3DSFileSystem_join(n3ds->saveBasePath, relativePath); + if (path == NULL) return false; + int rc = remove(path); + N3DSFileSystem_logSdIo("delete", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, rc == 0); + if (rc == 0) { + char* romfsPath = N3DSFileSystem_join(n3ds->romfsBasePath, relativePath); + N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, romfsPath); + free(romfsPath); + } + free(path); + return rc == 0; +} + +static bool N3DSFileSystem_readFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) { + u64 startTick = svcGetSystemTick(); + char* path = N3DSFileSystem_resolvePath(fs, relativePath); + if (path == NULL) return false; + + FILE* file = fopen(path, "rb"); + if (file == NULL) { + N3DSFileSystem_logSdIo("readBin", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), 0, false); + free(path); + return false; + } + N3DSFileSystem_configureFileBuffer(file); + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size < 0) { + fclose(file); + return false; + } + + uint8_t* data = safeMalloc((size_t) size); + size_t bytesRead = fread(data, 1, (size_t) size, file); + fclose(file); + + *outData = data; + *outSize = (int32_t) bytesRead; + N3DSFileSystem_logSdIo("readBin", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), (long) bytesRead, true); + free(path); + return true; +} + +static bool N3DSFileSystem_writeFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) { + N3DSFileSystem* n3ds = (N3DSFileSystem*) fs; + u64 startTick = svcGetSystemTick(); + char* path = N3DSFileSystem_join(n3ds->saveBasePath, relativePath); + if (path == NULL) return false; + + FILE* file = fopen(path, "wb"); + if (file == NULL) { + N3DSFileSystem_logSdIo("writeBin", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), size, false); + free(path); + return false; + } + N3DSFileSystem_configureFileBuffer(file); + + bool ok = fwrite(data, 1, (size_t) size, file) == (size_t) size; + fclose(file); + N3DSFileSystem_logSdIo("writeBin", path, N3DSFileSystem_ticksToMs(svcGetSystemTick() - startTick), size, ok); + if (ok) { + N3DSFileSystem_setCachedResolvedPath(n3ds, relativePath, path); + } + free(path); + return ok; +} + +static FileSystemVtable N3DSFileSystem_vtable = { + .resolvePath = N3DSFileSystem_resolvePath, + .fileExists = N3DSFileSystem_fileExists, + .readFileText = N3DSFileSystem_readFileText, + .writeFileText = N3DSFileSystem_writeFileText, + .deleteFile = N3DSFileSystem_deleteFile, + .readFileBinary = N3DSFileSystem_readFileBinary, + .writeFileBinary = N3DSFileSystem_writeFileBinary, +}; + +N3DSFileSystem* N3DSFileSystem_create(const char* romfsBasePath, const char* saveBasePath) { + N3DSFileSystem* fs = safeCalloc(1, sizeof(N3DSFileSystem)); + fs->base.vtable = &N3DSFileSystem_vtable; + fs->romfsBasePath = safeStrdup(romfsBasePath != NULL ? romfsBasePath : "romfs:/"); + fs->saveBasePath = safeStrdup(saveBasePath != NULL ? saveBasePath : "sdmc:/3ds/cinnamon/"); + fs->resolvedPathCache = NULL; + sh_new_strdup(fs->resolvedPathCache); + return fs; +} + +void N3DSFileSystem_destroy(N3DSFileSystem* fs) { + if (fs == NULL) return; +#if N3DS_ENABLE_LOGGING + if (gN3DSSdIoLogFile != NULL) { + fclose(gN3DSSdIoLogFile); + gN3DSSdIoLogFile = NULL; + } +#endif + free(fs->romfsBasePath); + free(fs->saveBasePath); + repeat(shlen(fs->resolvedPathCache), i) { + free(fs->resolvedPathCache[i].value); + } + shfree(fs->resolvedPathCache); + free(fs); +} diff --git a/src/n3ds/n3ds_file_system.h b/src/n3ds/n3ds_file_system.h new file mode 100644 index 00000000..26767247 --- /dev/null +++ b/src/n3ds/n3ds_file_system.h @@ -0,0 +1,15 @@ +#pragma once + +#include "../file_system.h" + +typedef struct N3DSFileSystemResolvedPathEntry N3DSFileSystemResolvedPathEntry; + +typedef struct { + FileSystem base; + char* romfsBasePath; + char* saveBasePath; + N3DSFileSystemResolvedPathEntry* resolvedPathCache; +} N3DSFileSystem; + +N3DSFileSystem* N3DSFileSystem_create(const char* romfsBasePath, const char* saveBasePath); +void N3DSFileSystem_destroy(N3DSFileSystem* fs); diff --git a/src/n3ds/n3ds_platform_config.h b/src/n3ds/n3ds_platform_config.h new file mode 100644 index 00000000..85613763 --- /dev/null +++ b/src/n3ds/n3ds_platform_config.h @@ -0,0 +1,3 @@ +#pragma once + +#define N3DS_FORCE_OLD3DS_MODE 0 \ No newline at end of file diff --git a/src/n3ds/n3ds_renderer.c b/src/n3ds/n3ds_renderer.c new file mode 100644 index 00000000..7ceba748 --- /dev/null +++ b/src/n3ds/n3ds_renderer.c @@ -0,0 +1,5530 @@ +#include "n3ds_renderer.h" +#include "n3ds_platform_config.h" + +#include "../runner.h" +#include "../text_utils.h" +#include "../utils.h" + +#include <3ds.h> +#include +#include +#include + +#include +#include +#include +#include +#include + +#define N3DS_ENABLE_LOGGING 0 +#define N3DS_ENABLE_DIRECT_ASSETS 1 + +#if !N3DS_ENABLE_LOGGING +#define fprintf(...) ((int) 0) +#endif + +#define N3DS_ATLAS_MAGIC 0x5441334Eu /* N3AT */ +#define N3DS_ATLAS_VERSION_RAW 1u +#define N3DS_ATLAS_VERSION_T3X 2u +#define N3DS_ATLAS_VERSION_FRAGMENTED 3u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILES 4u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS 5u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT 6u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT 7u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED 8u +#define N3DS_DIRECT_ASSET_MAGIC 0x3152444Eu /* NDR1 */ +#define N3DS_DIRECT_ASSET_VERSION 1u +#define N3DS_ROOM_MANIFEST_MAGIC 0x4D52334Eu /* N3RM */ +#define N3DS_ROOM_MANIFEST_VERSION 1u +#define N3DS_DIRECT_ASSET_ENTRY_SIZE 16u +#define N3DS_TOP_WIDTH 400 +#define N3DS_TOP_HEIGHT 240 +#define N3DS_BOTTOM_WIDTH 320 +#define N3DS_BOTTOM_HEIGHT 240 +#define N3DS_SDMC_ASSET_BASE "sdmc:/3ds/cinnamon/gfx" +#define N3DS_ROMFS_ASSET_BASE "romfs:/gfx" +#define N3DS_RENDERER_FILE_BUFFER_SIZE (32u * 1024u) +#define N3DS_TILE_LAYER_CHUNK_SIZE 256u +#define N3DS_TILE_LAYER_CHUNK_TEXTURE_BYTES (N3DS_TILE_LAYER_CHUNK_SIZE * N3DS_TILE_LAYER_CHUNK_SIZE * 2u) +#define N3DS_TILE_LAYER_CHUNK_VRAM_BUDGET (512u * 1024u) +#define N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS 16u +#define N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS 32u +#define N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS_ETC1A4 40u +#define N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS_ETC1A4 80u +#define N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS 12u +#define N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS 32u +#define N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS_ETC1A4 20u +#define N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS_ETC1A4 48u +#define N3DS_PREWARM_BLOB_BYTES_OLD3DS (8u * 1024u * 1024u) +#define N3DS_PREWARM_BLOB_BYTES_NEW3DS (12u * 1024u * 1024u) +#define N3DS_RESIDENT_ATLAS_VRAM_BUDGET_OLD3DS (2560u * 1024u) +#define N3DS_RESIDENT_ATLAS_VRAM_BUDGET_NEW3DS (5120u * 1024u) +#define N3DS_MAX_CACHED_T3X_BYTES_OLD3DS (16u * 1024u * 1024u) +#define N3DS_MAX_CACHED_T3X_BYTES_NEW3DS (24u * 1024u * 1024u) +#define N3DS_MAX_CACHED_DIRECT_T3X_BYTES_OLD3DS (4u * 1024u * 1024u) +#define N3DS_MAX_CACHED_DIRECT_T3X_BYTES_NEW3DS (8u * 1024u * 1024u) +#define N3DS_DIRECT_ASSET_VRAM_BUDGET_OLD3DS (1024u * 1024u) +#define N3DS_DIRECT_ASSET_VRAM_BUDGET_NEW3DS (3072u * 1024u) +// battle screen offset +#define N3DS_TOP_BATTLE_SCENE_Y_OFFSET 200.0f +#define N3DS_TOP_BATTLE_ENEMY_Y_OFFSET 200.0f +#define N3DS_C2D_FLUSH_DRAW_BUDGET 192u +#define N3DS_PERF_LOG_INTERVAL_FRAMES 30u +#define N3DS_ATLAS_TRACE_LOG_PATH "sdmc:/3ds/cinnamon/atlas_trace.log" + +typedef struct { + uint16_t width; + uint16_t height; + uint32_t textureFormat; + uint32_t dataOffset; + uint32_t dataSize; +} N3DSAtlasPageInfo; + +typedef enum { + N3DS_TEXFMT_RGBA5551 = 0, + N3DS_TEXFMT_ETC1A4 = 1, + N3DS_TEXFMT_INDEXED8 = 2, + N3DS_TEXFMT_HYBRID = 3, + N3DS_TEXFMT_L4 = 4, + N3DS_TEXFMT_LA4 = 5, +} N3DSTextureFormat; + +typedef struct { + uint16_t atlasId; + uint16_t x; + uint16_t y; + uint16_t width; + uint16_t height; +} N3DSAtlasItem; + +typedef struct { + uint16_t width; + uint16_t height; + uint32_t fragmentStart; + uint16_t fragmentCount; +} N3DSAtlasItemV3; + +typedef struct { + uint16_t atlasId; + uint16_t x; + uint16_t y; + uint16_t width; + uint16_t height; + uint16_t sourceX; + uint16_t sourceY; +} N3DSAtlasFragment; + +typedef struct { + int16_t bgDef; + uint16_t srcX; + uint16_t srcY; + uint16_t srcW; + uint16_t srcH; + uint16_t atlasId; + uint16_t x; + uint16_t y; + uint16_t width; + uint16_t height; + uint32_t fragmentStart; + uint16_t fragmentCount; +} N3DSTileAtlasEntry; + +typedef struct { + int16_t bgDef; + uint16_t srcX; + uint16_t srcY; + uint16_t srcW; + uint16_t srcH; +} N3DSTileLookupKey; + +typedef struct { + N3DSTileLookupKey key; + uint32_t value; +} N3DSTileEntryMap; + +typedef struct { + C2D_SpriteSheet sheet; + C2D_Image image; + uint8_t* blobData; + uint32_t blobSize; + uint32_t blobLastUsedStamp; + bool ready; + bool failed; + bool pinned; + uint32_t vramBytes; + uint32_t lastUsedStamp; + uint32_t lastUsedFrame; +} N3DSDirectTextureAsset; + +typedef struct { + uint32_t frameCount; + uint32_t sheetFrameCount; + bool sheetLoadAttempted; + bool useFrameFallback; + C2D_Image* sheetFrameImages; + N3DSDirectTextureAsset sheetAsset; + N3DSDirectTextureAsset* frameAssets; +} N3DSDirectSpriteAsset; + +typedef struct { + bool allocated; + int32_t x; + int32_t y; + uint16_t width; + uint16_t height; + C3D_Tex texture; + C3D_RenderTarget* target; + Tex3DS_SubTexture subtex; + C2D_Image image; +} N3DSTileLayerChunk; + +typedef struct { + bool used; + uint32_t chunkCount; + uint32_t vramBytes; + N3DSTileLayerChunk* chunks; +} N3DSTileLayerChunkCache; + +typedef struct { + C3D_Tex texture; + Tex3DS_Texture t3x; + uint8_t* t3xData; + uint32_t t3xSize; + uint32_t blobLastUsedStamp; + bool ready; + bool pinned; + uint16_t width; + uint16_t height; + uint32_t textureFormat; + uint32_t dataOffset; + uint32_t dataSize; + uint32_t lastUsedStamp; + uint32_t lastUsedFrame; +} N3DSLoadedAtlasPage; + +typedef struct { + bool used; + int32_t ownerSpriteIndex; + uint16_t logicalWidth; + uint16_t logicalHeight; + C3D_Tex texture; + Tex3DS_SubTexture subtex; + C2D_Image image; +} N3DSDynamicCaptureTPAG; + +typedef struct { + float localX; + float localY; + uint16_t sourceX; + uint16_t sourceY; + uint16_t sourceWidth; + uint16_t sourceHeight; +} N3DSCachedTextGlyph; + +typedef struct { + char* text; + int32_t textCapacity; + int32_t fontIndex; + int32_t drawHalign; + int32_t drawValign; + int32_t glyphCount; + int32_t glyphCapacity; + N3DSCachedTextGlyph* glyphs; + float appendCursorX; + float appendCursorY; + uint16_t appendPrevCodepoint; + bool appendAtLineStart; +} N3DSCachedTextLayout; + +typedef struct { + char* key; + char* value; +} N3DSResolvedAssetPathEntry; + +typedef struct { + uint32_t dataOffset; + uint32_t dataSize; +} N3DSPackedDirectAssetEntry; + +typedef struct { + char* key; + N3DSPackedDirectAssetEntry value; +} N3DSPackedDirectAssetMapEntry; + +typedef struct { + uint32_t roomIndex; + uint32_t pageStart; + uint32_t pageCount; + uint32_t directSpriteStart; + uint32_t directSpriteCount; + uint32_t directBackgroundStart; + uint32_t directBackgroundCount; +} N3DSRoomManifestEntry; + +typedef struct { + uint16_t pageIndex; + uint8_t flags; + uint8_t reserved; +} N3DSRoomManifestPageRef; + +typedef struct { + Renderer base; + C3D_RenderTarget* topTarget; + C3D_RenderTarget* bottomTarget; + N3DSLoadedAtlasPage* atlasPages; + N3DSAtlasItem* atlasItems; + N3DSAtlasItemV3* atlasItemsV3; + N3DSAtlasFragment* atlasFragments; + N3DSTileAtlasEntry* tileEntries; + N3DSTileEntryMap* tileEntryMap; + N3DSTileLayerChunkCache* tileLayerChunkCaches; + N3DSDirectSpriteAsset* directSpriteAssets; + N3DSDirectTextureAsset* directBackgroundAssets; + N3DSDirectTextureAsset* directFontAssets; + int32_t* tpagToSpriteIndex; + int32_t* tpagToSpriteFrameIndex; + int32_t* tpagToBackgroundIndex; + uint32_t atlasPageCount; + uint32_t atlasItemCount; + uint32_t atlasFragmentCount; + uint32_t tileEntryCount; + uint32_t directSpriteAssetCount; + uint32_t directBackgroundAssetCount; + uint32_t directFontAssetCount; + uint16_t atlasVersion; + uint32_t atlasTextureFormat; + float frameScaleX; + float frameScaleY; + float frameOffsetX; + float frameOffsetY; + float portOffsetX; + float portOffsetY; + int32_t viewX; + int32_t viewY; + float viewScaleX; + float viewScaleY; + bool blendEnabled; + int32_t blendEquation; + int32_t blendSrcFactor; + int32_t blendDstFactor; + bool alphaTestEnabled; + uint8_t alphaTestRef; + uint32_t clearColor; + float clearAlpha; + uint32_t atlasUseCounter; + uint32_t blobUseCounter; + uint32_t residentAtlasPageCount; + uint32_t residentAtlasVRAMBytes; + uint32_t residentAtlasPageLimit; + uint32_t residentAtlasVRAMLimitBytes; + uint32_t directAssetUseCounter; + uint32_t directBlobUseCounter; + uint32_t residentDirectAssetVRAMBytes; + uint32_t residentDirectAssetVRAMLimitBytes; + uint32_t prewarmGpuPageBudget; + uint32_t cachedT3xByteLimit; + uint32_t cachedT3xBytes; + uint32_t cachedDirectT3xByteLimit; + uint32_t cachedDirectT3xBytes; + uint32_t tileLayerChunkVRAMBytes; + uint32_t frameBlobReads; + uint32_t frameDirectBlobReads; + uint32_t framePageImports; + uint32_t frameImportFailures; + uint32_t frameImportEvictions; + uint32_t frameFragmentDraws; + uint32_t frameSpriteDrawCalls; + uint32_t frameSpritePartDrawCalls; + uint32_t frameDirectSpriteHits; + uint32_t frameDirectAssetLoads; + uint32_t frameTextureSwitches; + uint32_t frameTextGlyphDraws; + uint32_t frameTextTenthsMs; + uint32_t pendingC2DDraws; + bool atlasLoaded; + char startupError[160]; + int32_t lastDirectTPAGIndex; + C2D_Image* lastDirectTPAGImage; + uint32_t perfWindowFrames; + uint32_t perfWindowBlobReads; + uint32_t perfWindowPageImports; + uint32_t perfWindowImportFailures; + uint32_t perfWindowImportEvictions; + uint32_t perfWindowFragmentDraws; + uint32_t perfWindowSpriteDrawCalls; + uint32_t perfWindowSpritePartDrawCalls; + uint32_t frameSequence; + const Room* lastPrewarmedRoom; + N3DSCachedTextLayout cachedTextLayout; + N3DSResolvedAssetPathEntry* resolvedAssetPathCache; + N3DSPackedDirectAssetMapEntry* packedDirectAssetMap; + N3DSRoomManifestEntry* roomManifestEntries; + N3DSRoomManifestPageRef* roomManifestPageRefs; + N3DSDynamicCaptureTPAG* dynamicCaptureTPAGs; + uint8_t* atlasTraceMask; + FILE* atlasTraceFile; + FILE* packedAtlasFile; + FILE* packedDirectAssetFile; + uint32_t roomManifestEntryCount; + uint32_t roomManifestPageRefCount; + uint32_t roomManifestRoomCount; + uint32_t baseTPAGCount; + uint32_t dynamicCaptureTPAGCount; + bool bottomScreenGuiActive; + bool topScreenGuiActive; + bool topScreenGui2xActive; + bool topScreenBattleViewActive; + bool textLinearFilterActive; + uint8_t activeSceneTarget; + const C3D_Tex* lastDrawTexture; + float savedFrameScaleX; + float savedFrameScaleY; + float savedFrameOffsetX; + float savedFrameOffsetY; + float savedPortOffsetX; + float savedPortOffsetY; + int32_t savedViewX; + int32_t savedViewY; + float savedViewScaleX; + float savedViewScaleY; + bool isNew3DS; + bool pendingOld3DSAtlasFlush; +} N3DSRenderer; + +enum { + N3DS_SCENE_TARGET_NONE = 0, + N3DS_SCENE_TARGET_TOP = 1, + N3DS_SCENE_TARGET_BOTTOM = 2, +}; + +static void N3DSRenderer_drawSprite(Renderer* base, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha); +static void N3DSRenderer_drawSpritePart(Renderer* base, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha); +static void N3DSRenderer_drawTile(Renderer* base, RoomTile* tile, float offsetX, float offsetY); +static void N3DSRenderer_drawTiled(Renderer* base, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha); +static void N3DSRenderer_drawTiledPart(Renderer* base, int32_t tpagIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint32_t color, float alpha); +static bool N3DSRenderer_getPageIndexForTPAG(N3DSRenderer* renderer, int32_t tpagIndex, uint32_t* outPageIndex); +static N3DSTileAtlasEntry* N3DSRenderer_findTileEntryByKey(N3DSRenderer* renderer, int32_t backgroundIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, uint32_t* outEntryIndex); +static N3DSTileAtlasEntry* N3DSRenderer_findTileEntry(N3DSRenderer* renderer, RoomTile* tile); +static void N3DSRenderer_prewarmPage(N3DSRenderer* renderer, bool* seenPages, uint32_t pageIndex); +static bool N3DSRenderer_prewarmPageBlobOnly(N3DSRenderer* renderer, bool* seenPages, uint32_t pageIndex, uint32_t* remainingBlobBytes); +static void N3DSRenderer_prewarmTPAGBlobOnly(N3DSRenderer* renderer, bool* seenPages, int32_t tpagIndex, uint32_t* remainingBlobBytes); +static void N3DSRenderer_prewarmTileEntryBlobOnly(N3DSRenderer* renderer, bool* seenPages, const N3DSTileAtlasEntry* entry, uint32_t* remainingBlobBytes); +static void N3DSRenderer_prewarmRoomBlobCache(N3DSRenderer* renderer, Runner* runner); +static void N3DSRenderer_preloadFontPages(N3DSRenderer* renderer); +static bool N3DSRenderer_ensurePageBlobLoaded(N3DSRenderer* renderer, uint32_t pageIndex); +static void N3DSRenderer_prewarmRoom(Renderer* base, Runner* runner); +static void N3DSRenderer_freeCachedTextLayout(N3DSCachedTextLayout* layout); +static void N3DSRenderer_appendCachedTextLayoutSuffix(N3DSCachedTextLayout* layout, Font* font, const char* suffix, int32_t suffixLen); +static const N3DSCachedTextLayout* N3DSRenderer_getCachedTextLayout(N3DSRenderer* renderer, Font* font, int32_t fontIndex, const char* text); +static bool N3DSRenderer_tryDrawSingleGlyphTextFast( + Renderer* base, + N3DSRenderer* renderer, + Font* font, + const char* text, + int32_t len, + float x, + float y, + float effectiveXScale, + float effectiveYScale, + float angleDeg, + bool gradient, + int32_t c1, + float alpha, + bool useDirectFontAsset, + const C2D_Image* directFontImage, + const Tex3DS_SubTexture* directFontBaseSubtex, + const N3DSAtlasFragment* fontFragment, + N3DSLoadedAtlasPage* fontPage +); +static bool N3DSRenderer_drawPackedTileEntry(Renderer* base, N3DSRenderer* renderer, const N3DSTileAtlasEntry* tileEntry, float drawX, float drawY, float xscale, float yscale, uint32_t color, float alpha); +static bool N3DSRenderer_isFragmentedAtlasVersion(uint16_t atlasVersion); +static bool N3DSRenderer_isPackedAtlasVersion(uint16_t atlasVersion); +static const char* N3DSRenderer_getTextureFormatName(uint32_t textureFormat); +static void N3DSRenderer_sceneBeginTarget(N3DSRenderer* renderer, uint8_t targetKind, bool force); +static void N3DSRenderer_freeTileLayerChunkCache(N3DSRenderer* renderer, N3DSTileLayerChunkCache* cache); +static void N3DSRenderer_freeDirectTextureAsset(N3DSDirectTextureAsset* asset, N3DSRenderer* renderer); +static void N3DSRenderer_unloadDirectTextureBlob(N3DSDirectTextureAsset* asset, N3DSRenderer* renderer); +static bool N3DSRenderer_evictLRUDirectAsset(N3DSRenderer* renderer, const N3DSDirectTextureAsset* excludeAsset); +static bool N3DSRenderer_evictLRUDirectAssetBlob(N3DSRenderer* renderer, const N3DSDirectTextureAsset* excludeAsset); +static void N3DSRenderer_buildDirectAssetMaps(N3DSRenderer* renderer); +static bool N3DSRenderer_tryDrawDirectMappedSprite(Renderer* base, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha); +static bool N3DSRenderer_tryResolveDirectFontImage(N3DSRenderer* renderer, int32_t fontIndex, C2D_Image* outImage, Tex3DS_SubTexture* outSubtex); +static void N3DSRenderer_drawImage(Renderer* base, C2D_Image* image, float localX, float localY, float width, float height, float pivotX, float pivotY, float angleDeg, uint32_t color, float alpha); +static void N3DSRenderer_getActiveTargetSize(const N3DSRenderer* renderer, float* outW, float* outH); +static bool N3DSRenderer_isScreenRectOffscreen(const N3DSRenderer* renderer, float x, float y, float w, float h); +static bool N3DSRenderer_isScreenRotatedRectOffscreen(const N3DSRenderer* renderer, float x, float y, float w, float h, float angleDeg); +static bool N3DSRenderer_loadPackedDirectAssets(N3DSRenderer* renderer); +static bool N3DSRenderer_loadRoomManifest(N3DSRenderer* renderer); +static const N3DSRoomManifestEntry* N3DSRenderer_findRoomManifestEntry(const N3DSRenderer* renderer, uint32_t roomIndex); +static void N3DSRenderer_prewarmRoomManifestBlobCache(N3DSRenderer* renderer, uint32_t roomIndex, bool* seenPages, uint32_t* remainingBlobBytes); +static bool N3DSRenderer_tryLoadPackedDirectTextureBlob(N3DSRenderer* renderer, N3DSDirectTextureAsset* asset, const char* relativePath); +static N3DSDynamicCaptureTPAG* N3DSRenderer_getDynamicCaptureTPAG(N3DSRenderer* renderer, int32_t tpagIndex); +static void N3DSRenderer_freeDynamicCaptureTPAG(N3DSRenderer* renderer, N3DSDynamicCaptureTPAG* capture); +static int32_t N3DSRenderer_allocDynamicCaptureTPAG(N3DSRenderer* renderer); + +enum { + N3DS_TRACE_KIND_SPRITE = 1u << 0, + N3DS_TRACE_KIND_SPRITE_PART = 1u << 1, + N3DS_TRACE_KIND_FONT = 1u << 2, +}; + +static const char* N3DSRenderer_getTraceKindName(uint8_t traceKind) { + switch (traceKind) { + case N3DS_TRACE_KIND_SPRITE: return "sprite"; + case N3DS_TRACE_KIND_SPRITE_PART: return "sprite_part"; + case N3DS_TRACE_KIND_FONT: return "font"; + default: return "unknown"; + } +} + +static void N3DSRenderer_traceTPAGUsage(N3DSRenderer* renderer, uint8_t traceKind, int32_t tpagIndex) { +#if !N3DS_ENABLE_LOGGING + (void) renderer; + (void) traceKind; + (void) tpagIndex; + return; +#else + if (renderer == NULL || tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return; + if (renderer->atlasTraceMask == NULL) return; + + uint8_t* traceMask = &renderer->atlasTraceMask[tpagIndex]; + if ((*traceMask & traceKind) != 0u) return; + *traceMask |= traceKind; + + const char* kindName = N3DSRenderer_getTraceKindName(traceKind); + if (!N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) { + N3DSAtlasItem* item = &renderer->atlasItems[tpagIndex]; + fprintf( + stderr, + "N3DS TRACE: kind=%s tpag=%d page=%u format=%s\n", + kindName, + (int) tpagIndex, + (unsigned int) item->atlasId, + (item->atlasId < renderer->atlasPageCount) ? N3DSRenderer_getTextureFormatName(renderer->atlasPages[item->atlasId].textureFormat) : "unknown" + ); + if (renderer->atlasTraceFile != NULL) { + fprintf( + renderer->atlasTraceFile, + "kind=%s tpag=%d page=%u format=%s\n", + kindName, + (int) tpagIndex, + (unsigned int) item->atlasId, + (item->atlasId < renderer->atlasPageCount) ? N3DSRenderer_getTextureFormatName(renderer->atlasPages[item->atlasId].textureFormat) : "unknown" + ); + fflush(renderer->atlasTraceFile); + } + return; + } + + N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex]; + char pagesBuf[256]; + size_t cursor = 0; + pagesBuf[0] = '\0'; + repeat(item->fragmentCount, i) { + uint32_t fragmentIndex = item->fragmentStart + (uint32_t) i; + if (fragmentIndex >= renderer->atlasFragmentCount) break; + uint16_t pageIndex = renderer->atlasFragments[fragmentIndex].atlasId; + bool alreadyListed = false; + repeat(i, prevIndex) { + uint32_t prevFragmentIndex = item->fragmentStart + (uint32_t) prevIndex; + if (prevFragmentIndex >= renderer->atlasFragmentCount) break; + if (renderer->atlasFragments[prevFragmentIndex].atlasId == pageIndex) { + alreadyListed = true; + break; + } + } + if (alreadyListed) continue; + int written = snprintf( + pagesBuf + cursor, + sizeof(pagesBuf) - cursor, + "%s%u", + cursor == 0 ? "" : ",", + (unsigned int) pageIndex + ); + if (written <= 0 || (size_t) written >= sizeof(pagesBuf) - cursor) break; + cursor += (size_t) written; + } + + fprintf( + stderr, + "N3DS TRACE: kind=%s tpag=%d fragments=%u pages=%s\n", + kindName, + (int) tpagIndex, + (unsigned int) item->fragmentCount, + pagesBuf[0] != '\0' ? pagesBuf : "none" + ); + if (renderer->atlasTraceFile != NULL) { + fprintf( + renderer->atlasTraceFile, + "kind=%s tpag=%d fragments=%u pages=%s\n", + kindName, + (int) tpagIndex, + (unsigned int) item->fragmentCount, + pagesBuf[0] != '\0' ? pagesBuf : "none" + ); + fflush(renderer->atlasTraceFile); + } +#endif +} + +static void N3DSRenderer_computeFrameLayoutForTarget(N3DSRenderer* renderer, int32_t gameW, int32_t gameH, int32_t targetW, int32_t targetH) { + if (gameW <= 0) gameW = targetW; + if (gameH <= 0) gameH = targetH; + + float sx = (float) targetW / (float) gameW; + float sy = (float) targetH / (float) gameH; + float scale = sx < sy ? sx : sy; + renderer->frameScaleX = scale; + renderer->frameScaleY = scale; + renderer->frameOffsetX = ((float) targetW - ((float) gameW * scale)) * 0.5f; + renderer->frameOffsetY = ((float) targetH - ((float) gameH * scale)) * 0.5f; +} + +static void N3DSRenderer_setTopBattle320x240Layout(N3DSRenderer* renderer, int32_t guiW, int32_t guiH, float yOffset) { + if (renderer == NULL) return; + if (guiW <= 0) guiW = 640; + if (guiH <= 0) guiH = 480; + + renderer->viewX = 0; + renderer->viewY = 0; + renderer->frameScaleX = (float) N3DS_BOTTOM_WIDTH / (float) guiW; + renderer->frameScaleY = (float) N3DS_BOTTOM_HEIGHT / (float) guiH; + renderer->viewScaleX = renderer->frameScaleX; + renderer->viewScaleY = renderer->frameScaleY; + renderer->portOffsetX = 0.0f; + renderer->portOffsetY = 0.0f; + renderer->frameOffsetX = floorf((((float) N3DS_TOP_WIDTH - (float) N3DS_BOTTOM_WIDTH) * 0.5f) + 0.5f); + renderer->frameOffsetY = floorf(yOffset + 0.5f); +} + +static bool N3DSRenderer_isFragmentedAtlasVersion(uint16_t atlasVersion) { + return atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED || + atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || + atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || + atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || + atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || + atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED; +} + +static bool N3DSRenderer_isPackedAtlasVersion(uint16_t atlasVersion) { + return atlasVersion == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED; +} + +static double N3DSRenderer_ticksToMs(u64 ticks) { + return (double) ticks * 1000.0 / (double) SYSCLOCK_ARM11; +} + +static void N3DSRenderer_resetFramePerfCounters(N3DSRenderer* renderer) { + renderer->frameBlobReads = 0; + renderer->framePageImports = 0; + renderer->frameImportFailures = 0; + renderer->frameImportEvictions = 0; + renderer->frameFragmentDraws = 0; + renderer->frameSpriteDrawCalls = 0; + renderer->frameSpritePartDrawCalls = 0; + renderer->frameDirectSpriteHits = 0; + renderer->frameDirectAssetLoads = 0; + renderer->frameTextureSwitches = 0; + renderer->frameTextGlyphDraws = 0; + renderer->frameTextTenthsMs = 0; + renderer->pendingC2DDraws = 0; + renderer->lastDrawTexture = NULL; + renderer->lastDirectTPAGIndex = -1; + renderer->lastDirectTPAGImage = NULL; +} + +static void N3DSRenderer_logPerfWindowIfNeeded(N3DSRenderer* renderer) { +#ifndef N3DS_ENABLE_PERF_LOGS + (void) renderer; + return; +#else + renderer->perfWindowFrames++; + renderer->perfWindowBlobReads += renderer->frameBlobReads; + renderer->perfWindowPageImports += renderer->framePageImports; + renderer->perfWindowImportFailures += renderer->frameImportFailures; + renderer->perfWindowImportEvictions += renderer->frameImportEvictions; + renderer->perfWindowFragmentDraws += renderer->frameFragmentDraws; + renderer->perfWindowSpriteDrawCalls += renderer->frameSpriteDrawCalls; + renderer->perfWindowSpritePartDrawCalls += renderer->frameSpritePartDrawCalls; + + if (renderer->perfWindowFrames < N3DS_PERF_LOG_INTERVAL_FRAMES) return; + + fprintf( + stderr, + "N3DS perf/%lu: blobReads=%lu pageImports=%lu importFail=%lu evictions=%lu fragDraws=%lu spriteCalls=%lu partCalls=%lu resident=%lu blobKB=%lu\n", + (unsigned long) renderer->perfWindowFrames, + (unsigned long) renderer->perfWindowBlobReads, + (unsigned long) renderer->perfWindowPageImports, + (unsigned long) renderer->perfWindowImportFailures, + (unsigned long) renderer->perfWindowImportEvictions, + (unsigned long) renderer->perfWindowFragmentDraws, + (unsigned long) renderer->perfWindowSpriteDrawCalls, + (unsigned long) renderer->perfWindowSpritePartDrawCalls, + (unsigned long) renderer->residentAtlasPageCount, + (unsigned long) (renderer->cachedT3xBytes / 1024u) + ); + + renderer->perfWindowFrames = 0; + renderer->perfWindowBlobReads = 0; + renderer->perfWindowPageImports = 0; + renderer->perfWindowImportFailures = 0; + renderer->perfWindowImportEvictions = 0; + renderer->perfWindowFragmentDraws = 0; + renderer->perfWindowSpriteDrawCalls = 0; + renderer->perfWindowSpritePartDrawCalls = 0; +#endif +} + +static uint16_t N3DS_readU16(const uint8_t* ptr) { + return (uint16_t) (ptr[0] | (ptr[1] << 8)); +} + +static uint32_t N3DS_readU32(const uint8_t* ptr) { + return (uint32_t) ptr[0] | + ((uint32_t) ptr[1] << 8) | + ((uint32_t) ptr[2] << 16) | + ((uint32_t) ptr[3] << 24); +} + +static uint32_t N3DSRenderer_getTextureFormatBytesPerPixel(uint32_t textureFormat) { + switch (textureFormat) { + case N3DS_TEXFMT_ETC1A4: return 1u; + case N3DS_TEXFMT_INDEXED8: return 2u; + case N3DS_TEXFMT_HYBRID: return 1u; + case N3DS_TEXFMT_LA4: return 1u; + case N3DS_TEXFMT_L4: return 0u; + case N3DS_TEXFMT_RGBA5551: + default: return 2u; + } +} + +static GPU_TEXCOLOR N3DSRenderer_getGPUTextureFormat(uint32_t textureFormat) { + switch (textureFormat) { + case N3DS_TEXFMT_ETC1A4: return GPU_ETC1A4; + case N3DS_TEXFMT_INDEXED8: return GPU_RGBA5551; + case N3DS_TEXFMT_HYBRID: return GPU_ETC1A4; + case N3DS_TEXFMT_L4: return GPU_L4; + case N3DS_TEXFMT_LA4: return GPU_LA4; + case N3DS_TEXFMT_RGBA5551: + default: return GPU_RGBA5551; + } +} + +static const char* N3DSRenderer_getTextureFormatName(uint32_t textureFormat) { + switch (textureFormat) { + case N3DS_TEXFMT_ETC1A4: return "etc1a4"; + case N3DS_TEXFMT_INDEXED8: return "indexed8"; + case N3DS_TEXFMT_HYBRID: return "hybrid"; + case N3DS_TEXFMT_L4: return "l4"; + case N3DS_TEXFMT_LA4: return "la4"; + case N3DS_TEXFMT_RGBA5551: + default: return "rgba5551"; + } +} + +static uint32_t N3DSRenderer_getPageVRAMBytes(MAYBE_UNUSED const N3DSRenderer* renderer, const N3DSLoadedAtlasPage* page) { + if (page == NULL) return 0; + uint32_t texelCount = (uint32_t) page->width * (uint32_t) page->height; + switch (page->textureFormat) { + case N3DS_TEXFMT_L4: + return texelCount / 2u; + case N3DS_TEXFMT_LA4: + case N3DS_TEXFMT_ETC1A4: + case N3DS_TEXFMT_HYBRID: + return texelCount; + case N3DS_TEXFMT_INDEXED8: + case N3DS_TEXFMT_RGBA5551: + default: + return texelCount * 2u; + } +} + +static void N3DSRenderer_getActiveTargetSize(const N3DSRenderer* renderer, float* outW, float* outH) { + if (outW == NULL || outH == NULL) return; + + bool useBottomTarget = + renderer != NULL && + (renderer->activeSceneTarget == N3DS_SCENE_TARGET_BOTTOM || renderer->bottomScreenGuiActive); + *outW = useBottomTarget ? (float) N3DS_BOTTOM_WIDTH : (float) N3DS_TOP_WIDTH; + *outH = useBottomTarget ? (float) N3DS_BOTTOM_HEIGHT : (float) N3DS_TOP_HEIGHT; +} + +static bool N3DSRenderer_isScreenRectOffscreen(const N3DSRenderer* renderer, float x, float y, float w, float h) { + if (w < 0.0f) { + x += w; + w = -w; + } + if (h < 0.0f) { + y += h; + h = -h; + } + + float targetW = 0.0f; + float targetH = 0.0f; + N3DSRenderer_getActiveTargetSize(renderer, &targetW, &targetH); + return x >= targetW || y >= targetH || (x + w) <= 0.0f || (y + h) <= 0.0f; +} + +static bool N3DSRenderer_isScreenRotatedRectOffscreen(const N3DSRenderer* renderer, float x, float y, float w, float h, float angleDeg) { + if (fabsf(angleDeg) < 0.001f) { + return N3DSRenderer_isScreenRectOffscreen(renderer, x, y, w, h); + } + + if (w < 0.0f) { + x += w; + w = -w; + } + if (h < 0.0f) { + y += h; + h = -h; + } + + float halfW = w * 0.5f; + float halfH = h * 0.5f; + float centerX = x + halfW; + float centerY = y + halfH; + float radius = sqrtf(halfW * halfW + halfH * halfH); + + float targetW = 0.0f; + float targetH = 0.0f; + N3DSRenderer_getActiveTargetSize(renderer, &targetW, &targetH); + return (centerX - radius) >= targetW || (centerY - radius) >= targetH || + (centerX + radius) <= 0.0f || (centerY + radius) <= 0.0f; +} + +static void N3DSRenderer_freeDirectTextureAsset(N3DSDirectTextureAsset* asset, N3DSRenderer* renderer) { + if (asset == NULL || !asset->ready) { + if (asset != NULL) { + asset->sheet = NULL; + asset->ready = false; + asset->pinned = false; + } + return; + } + + if (asset->sheet != NULL) { + C2D_SpriteSheetFree(asset->sheet); + asset->sheet = NULL; + } + asset->image.tex = NULL; + asset->image.subtex = NULL; + if (renderer != NULL) { + if (renderer->residentDirectAssetVRAMBytes >= asset->vramBytes) renderer->residentDirectAssetVRAMBytes -= asset->vramBytes; + else renderer->residentDirectAssetVRAMBytes = 0; + } + asset->vramBytes = 0; + asset->ready = false; + asset->pinned = false; + asset->lastUsedStamp = 0; + asset->lastUsedFrame = 0; +} + +static void N3DSRenderer_unloadDirectTextureBlob(N3DSDirectTextureAsset* asset, N3DSRenderer* renderer) { + if (asset == NULL || asset->blobData == NULL) return; + + free(asset->blobData); + asset->blobData = NULL; + if (renderer != NULL) { + if (renderer->cachedDirectT3xBytes >= asset->blobSize) renderer->cachedDirectT3xBytes -= asset->blobSize; + else renderer->cachedDirectT3xBytes = 0; + } + asset->blobSize = 0; + asset->blobLastUsedStamp = 0; +} + +static void N3DSRenderer_buildSheetFrameImages(N3DSDirectSpriteAsset* spriteAsset) { + if (spriteAsset == NULL) return; + if (spriteAsset->sheetAsset.sheet == NULL || spriteAsset->sheetFrameCount == 0) return; + if (spriteAsset->sheetFrameImages != NULL) return; + + spriteAsset->sheetFrameImages = safeCalloc(spriteAsset->sheetFrameCount, sizeof(C2D_Image)); + repeat(spriteAsset->sheetFrameCount, i) { + spriteAsset->sheetFrameImages[i] = C2D_SpriteSheetGetImage(spriteAsset->sheetAsset.sheet, i); + } +} + +static void N3DSRenderer_clearDirectAssetPins(N3DSRenderer* renderer) { + if (renderer == NULL) return; + + repeat(renderer->directSpriteAssetCount, spriteIndex) { + N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex]; + spriteAsset->sheetAsset.pinned = false; + repeat(spriteAsset->frameCount, frameIndex) { + spriteAsset->frameAssets[frameIndex].pinned = false; + } + } + + repeat(renderer->directBackgroundAssetCount, bgIndex) { + renderer->directBackgroundAssets[bgIndex].pinned = false; + } + + repeat(renderer->directFontAssetCount, fontIndex) { + renderer->directFontAssets[fontIndex].pinned = false; + } +} + +static void N3DSRenderer_flushRoomDirectAssets(N3DSRenderer* renderer) { + if (renderer == NULL) return; + + repeat(renderer->directSpriteAssetCount, spriteIndex) { + N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex]; + if (!spriteAsset->sheetAsset.pinned) { + N3DSRenderer_freeDirectTextureAsset(&spriteAsset->sheetAsset, renderer); + N3DSRenderer_unloadDirectTextureBlob(&spriteAsset->sheetAsset, renderer); + } + + repeat(spriteAsset->frameCount, frameIndex) { + N3DSDirectTextureAsset* frameAsset = &spriteAsset->frameAssets[frameIndex]; + if (frameAsset->pinned) continue; + N3DSRenderer_freeDirectTextureAsset(frameAsset, renderer); + N3DSRenderer_unloadDirectTextureBlob(frameAsset, renderer); + } + } + + repeat(renderer->directBackgroundAssetCount, bgIndex) { + N3DSRenderer_freeDirectTextureAsset(&renderer->directBackgroundAssets[bgIndex], renderer); + N3DSRenderer_unloadDirectTextureBlob(&renderer->directBackgroundAssets[bgIndex], renderer); + } +} + +static void N3DSRenderer_buildDirectAssetMaps(N3DSRenderer* renderer) { +#if !N3DS_ENABLE_DIRECT_ASSETS + if (renderer == NULL) return; + renderer->directSpriteAssetCount = 0; + renderer->directBackgroundAssetCount = 0; + renderer->directFontAssetCount = 0; + renderer->directSpriteAssets = NULL; + renderer->directBackgroundAssets = NULL; + renderer->directFontAssets = NULL; + renderer->tpagToSpriteIndex = NULL; + renderer->tpagToSpriteFrameIndex = NULL; + renderer->tpagToBackgroundIndex = NULL; + return; +#endif + if (renderer == NULL || renderer->base.dataWin == NULL) return; + + DataWin* dw = renderer->base.dataWin; + uint32_t tpagCount = dw->tpag.count > 0 ? dw->tpag.count : 1u; + renderer->tpagToSpriteIndex = safeMalloc((size_t) tpagCount * sizeof(int32_t)); + renderer->tpagToSpriteFrameIndex = safeMalloc((size_t) tpagCount * sizeof(int32_t)); + renderer->tpagToBackgroundIndex = safeMalloc((size_t) tpagCount * sizeof(int32_t)); + repeat(tpagCount, i) { + renderer->tpagToSpriteIndex[i] = -1; + renderer->tpagToSpriteFrameIndex[i] = -1; + renderer->tpagToBackgroundIndex[i] = -1; + } + + renderer->directSpriteAssetCount = dw->sprt.count; + renderer->directBackgroundAssetCount = dw->bgnd.count; + renderer->directFontAssetCount = dw->font.count; + renderer->directSpriteAssets = safeCalloc(renderer->directSpriteAssetCount > 0 ? renderer->directSpriteAssetCount : 1u, sizeof(N3DSDirectSpriteAsset)); + renderer->directBackgroundAssets = safeCalloc(renderer->directBackgroundAssetCount > 0 ? renderer->directBackgroundAssetCount : 1u, sizeof(N3DSDirectTextureAsset)); + renderer->directFontAssets = safeCalloc(renderer->directFontAssetCount > 0 ? renderer->directFontAssetCount : 1u, sizeof(N3DSDirectTextureAsset)); + + repeat(dw->sprt.count, spriteIndex) { + Sprite* sprite = &dw->sprt.sprites[spriteIndex]; + N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex]; + spriteAsset->frameCount = sprite->textureCount; + if (sprite->textureCount > 0) { + spriteAsset->frameAssets = safeCalloc((size_t) sprite->textureCount, sizeof(N3DSDirectTextureAsset)); + } + + repeat(sprite->textureCount, frameIndex) { + int32_t tpagIndex = sprite->tpagIndices[frameIndex]; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dw->tpag.count) continue; + if (renderer->tpagToSpriteIndex[tpagIndex] < 0) { + renderer->tpagToSpriteIndex[tpagIndex] = (int32_t) spriteIndex; + renderer->tpagToSpriteFrameIndex[tpagIndex] = (int32_t) frameIndex; + } + } + } + + repeat(dw->bgnd.count, bgIndex) { + int32_t tpagIndex = dw->bgnd.backgrounds[bgIndex].tpagIndex; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dw->tpag.count) continue; + if (renderer->tpagToBackgroundIndex[tpagIndex] < 0) { + renderer->tpagToBackgroundIndex[tpagIndex] = (int32_t) bgIndex; + } + } +} + +static bool N3DS_pathExists(const char* path) { + struct stat st; + return path != NULL && stat(path, &st) == 0; +} + +static void N3DSRenderer_configureFileBuffer(FILE* file) { + if (file == NULL) return; + setvbuf(file, NULL, _IOFBF, N3DS_RENDERER_FILE_BUFFER_SIZE); +} + +static char* N3DSRenderer_getCachedAssetPath(N3DSRenderer* renderer, const char* relativePath) { + if (renderer == NULL || relativePath == NULL) return NULL; + ptrdiff_t idx = shgeti(renderer->resolvedAssetPathCache, relativePath); + if (idx < 0) return NULL; + return safeStrdup(renderer->resolvedAssetPathCache[idx].value); +} + +static void N3DSRenderer_setCachedAssetPath(N3DSRenderer* renderer, const char* relativePath, const char* resolvedPath) { + if (renderer == NULL || relativePath == NULL || resolvedPath == NULL) return; + ptrdiff_t idx = shgeti(renderer->resolvedAssetPathCache, relativePath); + if (idx >= 0) { + free(renderer->resolvedAssetPathCache[idx].value); + renderer->resolvedAssetPathCache[idx].value = safeStrdup(resolvedPath); + return; + } + + shput(renderer->resolvedAssetPathCache, relativePath, safeStrdup(resolvedPath)); +} + +static void N3DSRenderer_setStartupError(N3DSRenderer* renderer, const char* message) { + if (renderer == NULL) return; + snprintf( + renderer->startupError, + sizeof(renderer->startupError), + "%s", + message != NULL ? message : "Unknown 3DS renderer startup error" + ); +} + +static bool N3DSRenderer_resolveAssetPath(N3DSRenderer* renderer, const char* relativePath, char* resolvedPath, size_t resolvedPathSize) { + if (relativePath == NULL || resolvedPath == NULL || resolvedPathSize == 0) return false; + + char* cachedPath = N3DSRenderer_getCachedAssetPath(renderer, relativePath); + if (cachedPath != NULL) { + snprintf(resolvedPath, resolvedPathSize, "%s", cachedPath); + free(cachedPath); + return true; + } + + snprintf(resolvedPath, resolvedPathSize, "%s/%s", N3DS_ROMFS_ASSET_BASE, relativePath); + if (N3DS_pathExists(resolvedPath)) { + N3DSRenderer_setCachedAssetPath(renderer, relativePath, resolvedPath); + return true; + } + + snprintf(resolvedPath, resolvedPathSize, "%s/%s", N3DS_SDMC_ASSET_BASE, relativePath); + if (N3DS_pathExists(resolvedPath)) { + N3DSRenderer_setCachedAssetPath(renderer, relativePath, resolvedPath); + return true; + } + + resolvedPath[0] = '\0'; + return false; +} + +static FILE* N3DSRenderer_openAssetFile(N3DSRenderer* renderer, const char* relativePath, char* resolvedPath, size_t resolvedPathSize) { + if (!N3DSRenderer_resolveAssetPath(renderer, relativePath, resolvedPath, resolvedPathSize)) return NULL; + FILE* file = fopen(resolvedPath, "rb"); + N3DSRenderer_configureFileBuffer(file); + return file; +} + +static bool N3DSRenderer_loadPackedDirectAssets(N3DSRenderer* renderer) { + if (renderer == NULL) return false; + if (renderer->packedDirectAssetFile != NULL) return true; + + char resolvedPath[512]; + FILE* file = N3DSRenderer_openAssetFile(renderer, "direct_assets.bin", resolvedPath, sizeof(resolvedPath)); + if (file == NULL) return false; + + uint8_t header[16]; + if (fread(header, 1, sizeof(header), file) != sizeof(header)) { + fclose(file); + return false; + } + + uint32_t magic = N3DS_readU32(header + 0); + uint32_t version = N3DS_readU32(header + 4); + uint32_t entryCount = N3DS_readU32(header + 8); + uint32_t stringTableSize = N3DS_readU32(header + 12); + if (magic != N3DS_DIRECT_ASSET_MAGIC || version != N3DS_DIRECT_ASSET_VERSION) { + fclose(file); + return false; + } + if (entryCount > (UINT32_MAX / N3DS_DIRECT_ASSET_ENTRY_SIZE)) { + fclose(file); + return false; + } + + uint32_t entryTableSize = entryCount * N3DS_DIRECT_ASSET_ENTRY_SIZE; + uint64_t metadataSize64 = 16u + (uint64_t) entryTableSize + (uint64_t) stringTableSize; + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return false; + } + long fileSizeLong = ftell(file); + if (fileSizeLong <= 0 || metadataSize64 > (uint64_t) fileSizeLong) { + fclose(file); + return false; + } + uint32_t fileSize = (uint32_t) fileSizeLong; + if (fseek(file, 16, SEEK_SET) != 0) { + fclose(file); + return false; + } + + uint8_t* entryTable = safeMalloc(entryTableSize > 0 ? (size_t) entryTableSize : 1u); + uint8_t* stringTable = safeMalloc(stringTableSize > 0 ? (size_t) stringTableSize : 1u); + bool ok = true; + if (entryTableSize > 0) { + ok = fread(entryTable, 1, (size_t) entryTableSize, file) == (size_t) entryTableSize; + } + if (ok && stringTableSize > 0) { + ok = fread(stringTable, 1, (size_t) stringTableSize, file) == (size_t) stringTableSize; + } + if (!ok) { + free(entryTable); + free(stringTable); + fclose(file); + return false; + } + + if (renderer->packedDirectAssetMap != NULL) { + shfree(renderer->packedDirectAssetMap); + renderer->packedDirectAssetMap = NULL; + } + sh_new_strdup(renderer->packedDirectAssetMap); + + repeat(entryCount, i) { + const uint8_t* row = entryTable + ((size_t) i * N3DS_DIRECT_ASSET_ENTRY_SIZE); + uint32_t pathOffset = N3DS_readU32(row + 0); + uint32_t dataOffset = N3DS_readU32(row + 4); + uint32_t dataSize = N3DS_readU32(row + 8); + if (pathOffset >= stringTableSize || dataOffset > fileSize || dataSize > fileSize || dataOffset + dataSize > fileSize) { + ok = false; + break; + } + + const char* path = (const char*) (stringTable + pathOffset); + size_t remaining = (size_t) (stringTableSize - pathOffset); + if (memchr(path, '\0', remaining) == NULL) { + ok = false; + break; + } + + N3DSPackedDirectAssetEntry entry = { + .dataOffset = dataOffset, + .dataSize = dataSize, + }; + shput(renderer->packedDirectAssetMap, path, entry); + } + + free(entryTable); + free(stringTable); + if (!ok) { + shfree(renderer->packedDirectAssetMap); + renderer->packedDirectAssetMap = NULL; + fclose(file); + return false; + } + + renderer->packedDirectAssetFile = file; + fprintf(stderr, "N3DS: loaded packed direct texture blob (%u entries)\n", (unsigned int) entryCount); + return true; +} + +static bool N3DSRenderer_loadRoomManifest(N3DSRenderer* renderer) { + if (renderer == NULL) return false; + if (renderer->roomManifestEntries != NULL) return true; + + char resolvedPath[512]; + FILE* file = N3DSRenderer_openAssetFile(renderer, "room_manifest.bin", resolvedPath, sizeof(resolvedPath)); + if (file == NULL) return false; + + uint8_t header[20]; + if (fread(header, 1, sizeof(header), file) != sizeof(header)) { + fclose(file); + return false; + } + + uint32_t magic = N3DS_readU32(header + 0); + uint32_t version = N3DS_readU32(header + 4); + uint32_t entryCount = N3DS_readU32(header + 8); + uint32_t pageRefCount = N3DS_readU32(header + 12); + uint32_t roomCount = N3DS_readU32(header + 16); + if (magic != N3DS_ROOM_MANIFEST_MAGIC || version != N3DS_ROOM_MANIFEST_VERSION) { + fclose(file); + return false; + } + + uint64_t requiredBytes = + sizeof(header) + + (uint64_t) entryCount * sizeof(N3DSRoomManifestEntry) + + (uint64_t) pageRefCount * sizeof(N3DSRoomManifestPageRef); + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return false; + } + long fileSize = ftell(file); + if (fileSize < 0 || requiredBytes > (uint64_t) fileSize) { + fclose(file); + return false; + } + if (fseek(file, (long) sizeof(header), SEEK_SET) != 0) { + fclose(file); + return false; + } + + renderer->roomManifestEntries = safeCalloc(entryCount > 0 ? entryCount : 1u, sizeof(N3DSRoomManifestEntry)); + renderer->roomManifestPageRefs = safeCalloc(pageRefCount > 0 ? pageRefCount : 1u, sizeof(N3DSRoomManifestPageRef)); + renderer->roomManifestEntryCount = entryCount; + renderer->roomManifestPageRefCount = pageRefCount; + renderer->roomManifestRoomCount = roomCount; + + bool ok = true; + if (entryCount > 0) { + ok = fread(renderer->roomManifestEntries, sizeof(N3DSRoomManifestEntry), entryCount, file) == entryCount; + } + if (ok && pageRefCount > 0) { + ok = fread(renderer->roomManifestPageRefs, sizeof(N3DSRoomManifestPageRef), pageRefCount, file) == pageRefCount; + } + fclose(file); + + if (!ok) { + free(renderer->roomManifestEntries); + free(renderer->roomManifestPageRefs); + renderer->roomManifestEntries = NULL; + renderer->roomManifestPageRefs = NULL; + renderer->roomManifestEntryCount = 0; + renderer->roomManifestPageRefCount = 0; + renderer->roomManifestRoomCount = 0; + return false; + } + + fprintf( + stderr, + "N3DS: loaded room manifest (%u rooms, %u entries, %u page refs)\n", + roomCount, + entryCount, + pageRefCount + ); + return true; +} + +static const N3DSRoomManifestEntry* N3DSRenderer_findRoomManifestEntry(const N3DSRenderer* renderer, uint32_t roomIndex) { + if (renderer == NULL || renderer->roomManifestEntries == NULL) return NULL; + + repeat(renderer->roomManifestEntryCount, i) { + const N3DSRoomManifestEntry* entry = &renderer->roomManifestEntries[i]; + if (entry->roomIndex == roomIndex) return entry; + } + return NULL; +} + +static void N3DSRenderer_prewarmRoomManifestBlobCache(N3DSRenderer* renderer, uint32_t roomIndex, bool* seenPages, uint32_t* remainingBlobBytes) { + if (renderer == NULL || seenPages == NULL || remainingBlobBytes == NULL || *remainingBlobBytes == 0) return; + + const N3DSRoomManifestEntry* entry = N3DSRenderer_findRoomManifestEntry(renderer, roomIndex); + if (entry == NULL || entry->pageCount == 0) return; + if (entry->pageStart > renderer->roomManifestPageRefCount) return; + if (entry->pageCount > renderer->roomManifestPageRefCount - entry->pageStart) return; + + repeat(entry->pageCount, i) { + const N3DSRoomManifestPageRef* pageRef = &renderer->roomManifestPageRefs[entry->pageStart + (uint32_t) i]; + (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, pageRef->pageIndex, remainingBlobBytes); + if (*remainingBlobBytes == 0) break; + } +} + +static bool N3DSRenderer_tryLoadPackedDirectTextureBlob(N3DSRenderer* renderer, N3DSDirectTextureAsset* asset, const char* relativePath) { + if (renderer == NULL || asset == NULL || relativePath == NULL) return false; + if (renderer->packedDirectAssetFile == NULL || renderer->packedDirectAssetMap == NULL) return false; + + ptrdiff_t mapIndex = shgeti(renderer->packedDirectAssetMap, relativePath); + if (mapIndex < 0) return false; + + N3DSPackedDirectAssetEntry entry = renderer->packedDirectAssetMap[mapIndex].value; + if (entry.dataSize == 0) return false; + + while (renderer->cachedDirectT3xBytes + entry.dataSize > renderer->cachedDirectT3xByteLimit) { + if (!N3DSRenderer_evictLRUDirectAssetBlob(renderer, asset)) break; + } + + uint8_t* blobData = safeMalloc((size_t) entry.dataSize); + if (blobData == NULL) return false; + + if (fseek(renderer->packedDirectAssetFile, (long) entry.dataOffset, SEEK_SET) != 0) { + free(blobData); + return false; + } + if (fread(blobData, 1, (size_t) entry.dataSize, renderer->packedDirectAssetFile) != (size_t) entry.dataSize) { + free(blobData); + return false; + } + + asset->blobData = blobData; + asset->blobSize = entry.dataSize; + asset->blobLastUsedStamp = ++renderer->directBlobUseCounter; + renderer->cachedDirectT3xBytes += entry.dataSize; + renderer->frameDirectBlobReads++; + return true; +} + +static void N3DSRenderer_unloadPage(N3DSRenderer* renderer, uint32_t pageIndex) { + if (pageIndex >= renderer->atlasPageCount) return; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex]; + if (!page->ready) return; + C2D_Flush(); + renderer->pendingC2DDraws = 0; + renderer->lastDrawTexture = NULL; + + uint32_t pageBytes = N3DSRenderer_getPageVRAMBytes(renderer, page); + + if (page->t3x != NULL) { + Tex3DS_TextureFree(page->t3x); + page->t3x = NULL; + } + C3D_TexDelete(&page->texture); + memset(&page->texture, 0, sizeof(page->texture)); + page->ready = false; + page->lastUsedStamp = 0; + if (renderer->residentAtlasVRAMBytes >= pageBytes) renderer->residentAtlasVRAMBytes -= pageBytes; + else renderer->residentAtlasVRAMBytes = 0; + if (renderer->residentAtlasPageCount > 0) { + renderer->residentAtlasPageCount--; + } +} + +static void N3DSRenderer_unloadPageBlob(N3DSRenderer* renderer, uint32_t pageIndex) { + if (pageIndex >= renderer->atlasPageCount) return; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex]; + if (page->t3xData == NULL) return; + + free(page->t3xData); + page->t3xData = NULL; + if (renderer->cachedT3xBytes >= page->t3xSize) renderer->cachedT3xBytes -= page->t3xSize; + else renderer->cachedT3xBytes = 0; + page->t3xSize = 0; + page->blobLastUsedStamp = 0; +} + +static void N3DSRenderer_clearAtlasPagePins(N3DSRenderer* renderer) { + if (renderer == NULL) return; + repeat(renderer->atlasPageCount, i) { + renderer->atlasPages[i].pinned = false; + } +} + +static void N3DSRenderer_flushRoomAtlasResidencyOld3DS(N3DSRenderer* renderer) { + if (renderer == NULL || renderer->isNew3DS) return; + + repeat(renderer->atlasPageCount, i) { + N3DSLoadedAtlasPage* page = &renderer->atlasPages[i]; + if (!page->ready) continue; + if (page->pinned) continue; + N3DSRenderer_unloadPage(renderer, (uint32_t) i); + } +} + +static bool N3DSRenderer_evictLRUPageBlob(N3DSRenderer* renderer, uint32_t excludePageIndex) { + uint32_t bestIndex = UINT32_MAX; + uint32_t bestStamp = UINT32_MAX; + + repeat(renderer->atlasPageCount, i) { + if ((uint32_t) i == excludePageIndex) continue; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[i]; + if (page->t3xData == NULL || page->t3xSize == 0) continue; + if (page->blobLastUsedStamp < bestStamp) { + bestStamp = page->blobLastUsedStamp; + bestIndex = (uint32_t) i; + } + } + + if (bestIndex == UINT32_MAX) return false; + N3DSRenderer_unloadPageBlob(renderer, bestIndex); + return true; +} + +static bool N3DSRenderer_ensurePageBlobLoaded(N3DSRenderer* renderer, uint32_t pageIndex) { + if (pageIndex >= renderer->atlasPageCount) return false; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex]; + if (page->t3xData != NULL && page->t3xSize > 0) { + page->blobLastUsedStamp = ++renderer->blobUseCounter; + return true; + } + + FILE* pageFile = NULL; + uint32_t size = 0; + uint32_t dataOffset = 0; + bool usingPackedAtlas = false; + char assetPath[256]; + char pageName[32]; + + if (N3DSRenderer_isPackedAtlasVersion(renderer->atlasVersion) && + renderer->packedAtlasFile != NULL && + page->dataSize > 0) { + pageFile = renderer->packedAtlasFile; + size = page->dataSize; + dataOffset = page->dataOffset; + usingPackedAtlas = true; + } else { + const char* extension = page->textureFormat == N3DS_TEXFMT_INDEXED8 ? "i8" : "t3x"; + snprintf(pageName, sizeof(pageName), "page_%03lu.%s", (unsigned long) pageIndex, extension); + pageFile = N3DSRenderer_openAssetFile(renderer, pageName, assetPath, sizeof(assetPath)); + if (pageFile == NULL) { + fprintf(stderr, "N3DS: missing %s\n", pageName); + return false; + } + + fseek(pageFile, 0, SEEK_END); + long t3xSize = ftell(pageFile); + fseek(pageFile, 0, SEEK_SET); + if (t3xSize <= 0) { + fclose(pageFile); + fprintf(stderr, "N3DS: empty %s\n", assetPath); + return false; + } + + size = (uint32_t) t3xSize; + } + while (renderer->cachedT3xBytes + size > renderer->cachedT3xByteLimit) { + if (!N3DSRenderer_evictLRUPageBlob(renderer, pageIndex)) break; + } + + page->t3xData = safeMalloc((size_t) size); + page->t3xSize = size; + if (usingPackedAtlas) { + fseek(pageFile, (long) dataOffset, SEEK_SET); + } + if (fread(page->t3xData, 1, (size_t) size, pageFile) != (size_t) size) { + free(page->t3xData); + page->t3xData = NULL; + page->t3xSize = 0; + if (!usingPackedAtlas) fclose(pageFile); + fprintf(stderr, "N3DS: failed to read atlas page blob %lu\n", (unsigned long) pageIndex); + return false; + } + if (!usingPackedAtlas) fclose(pageFile); + + renderer->cachedT3xBytes += size; + page->blobLastUsedStamp = ++renderer->blobUseCounter; + renderer->frameBlobReads++; + return true; +} + +static bool N3DSRenderer_loadIndexed8Page(N3DSRenderer* renderer, N3DSLoadedAtlasPage* page) { + if (renderer == NULL || page == NULL || page->t3xData == NULL || page->t3xSize == 0) return false; + + size_t pixelCount = (size_t) page->width * (size_t) page->height; + size_t requiredSize = (256u * sizeof(uint16_t)) + pixelCount; + if ((size_t) page->t3xSize < requiredSize) { + fprintf(stderr, "N3DS: indexed8 page blob too small (%lu < %lu)\n", + (unsigned long) page->t3xSize, + (unsigned long) requiredSize); + return false; + } + + if (!C3D_TexInit(&page->texture, page->width, page->height, GPU_RGBA5551)) { + return false; + } + C3D_TexSetFilter(&page->texture, GPU_NEAREST, GPU_NEAREST); + C3D_TexSetWrap(&page->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE); + + uint16_t* expanded = (uint16_t*) linearAlloc(pixelCount * sizeof(uint16_t)); + if (expanded == NULL) { + C3D_TexDelete(&page->texture); + memset(&page->texture, 0, sizeof(page->texture)); + return false; + } + + const uint8_t* paletteBlob = page->t3xData; + const uint8_t* indexBlob = page->t3xData + (256u * sizeof(uint16_t)); + uint16_t palette[256]; + repeat(256u, i) { + palette[i] = (uint16_t) (paletteBlob[i * 2u + 0u] | ((uint16_t) paletteBlob[i * 2u + 1u] << 8)); + } + repeat(pixelCount, i) { + expanded[i] = palette[indexBlob[i]]; + } + + C3D_TexUpload(&page->texture, expanded); + C3D_TexFlush(&page->texture); + linearFree(expanded); + page->ready = true; + renderer->framePageImports++; + return true; +} + +static bool N3DSRenderer_loadPage(N3DSRenderer* renderer, uint32_t pageIndex) { + if (pageIndex >= renderer->atlasPageCount) return false; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex]; + if (page->ready) return true; + + if (renderer->atlasVersion == N3DS_ATLAS_VERSION_RAW) { + char assetPath[256]; + FILE* textureFile = N3DSRenderer_openAssetFile(renderer, "textures.bin", assetPath, sizeof(assetPath)); + if (textureFile == NULL) { + fprintf(stderr, "N3DS: missing textures.bin for raw atlas version\n"); + return false; + } + + if (!C3D_TexInit(&page->texture, page->width, page->height, N3DSRenderer_getGPUTextureFormat(page->textureFormat))) { + fclose(textureFile); + return false; + } + C3D_TexSetFilter(&page->texture, GPU_NEAREST, GPU_NEAREST); + C3D_TexSetWrap(&page->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE); + + uint8_t* textureData = linearAlloc(page->dataSize); + if (textureData == NULL) { + fclose(textureFile); + C3D_TexDelete(&page->texture); + memset(&page->texture, 0, sizeof(page->texture)); + return false; + } + + fseek(textureFile, (long) page->dataOffset, SEEK_SET); + fread(textureData, 1, page->dataSize, textureFile); + fclose(textureFile); + + C3D_TexUpload(&page->texture, textureData); + C3D_TexFlush(&page->texture); + linearFree(textureData); + page->ready = true; + return true; + } + + if (!N3DSRenderer_ensurePageBlobLoaded(renderer, pageIndex)) return false; + + if (page->textureFormat == N3DS_TEXFMT_INDEXED8) { + return N3DSRenderer_loadIndexed8Page(renderer, page); + } + + page->t3x = Tex3DS_TextureImport(page->t3xData, page->t3xSize, &page->texture, NULL, false); + if (page->t3x == NULL) { + return false; + } + + C3D_TexSetFilter(&page->texture, GPU_NEAREST, GPU_NEAREST); + C3D_TexSetWrap(&page->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE); + page->ready = true; + renderer->framePageImports++; + return true; +} + +static bool N3DSRenderer_evictLRUPage(N3DSRenderer* renderer, uint32_t excludePageIndex) { + uint32_t bestIndex = UINT32_MAX; + uint32_t bestStamp = UINT32_MAX; + + repeat(renderer->atlasPageCount, i) { + if ((uint32_t) i == excludePageIndex) continue; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[i]; + if (!page->ready) continue; + if (page->pinned) continue; + if (page->lastUsedFrame == renderer->frameSequence) continue; + if (page->lastUsedStamp < bestStamp) { + bestStamp = page->lastUsedStamp; + bestIndex = (uint32_t) i; + } + } + + if (bestIndex == UINT32_MAX) return false; + N3DSRenderer_unloadPage(renderer, bestIndex); + return true; +} + +static bool N3DSRenderer_ensurePageLoaded(N3DSRenderer* renderer, uint32_t pageIndex) { + if (pageIndex >= renderer->atlasPageCount) return false; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex]; + uint32_t pageBytes = N3DSRenderer_getPageVRAMBytes(renderer, page); + if (page->ready) { + page->lastUsedStamp = ++renderer->atlasUseCounter; + page->lastUsedFrame = renderer->frameSequence; + return true; + } + + while (renderer->residentAtlasPageCount >= renderer->residentAtlasPageLimit || + renderer->residentAtlasVRAMBytes + pageBytes > renderer->residentAtlasVRAMLimitBytes) { + if (!N3DSRenderer_evictLRUPage(renderer, pageIndex)) break; + } + + uint32_t evictionsForLoad = 0; + while (!N3DSRenderer_loadPage(renderer, pageIndex)) { + if (!N3DSRenderer_evictLRUPage(renderer, pageIndex)) { + renderer->frameImportFailures++; + renderer->frameImportEvictions += evictionsForLoad; + fprintf( + stderr, + "N3DS: failed to import page %lu from RAM cache after evicting %lu pages\n", + (unsigned long) pageIndex, + (unsigned long) evictionsForLoad + ); + return false; + } + evictionsForLoad++; + } + + page->lastUsedStamp = ++renderer->atlasUseCounter; + page->lastUsedFrame = renderer->frameSequence; + renderer->residentAtlasPageCount++; + renderer->residentAtlasVRAMBytes += pageBytes; + renderer->frameImportEvictions += evictionsForLoad; + return true; +} + +static bool N3DSRenderer_evictLRUDirectAsset(N3DSRenderer* renderer, const N3DSDirectTextureAsset* excludeAsset) { + if (renderer == NULL) return false; + + N3DSDirectTextureAsset* bestAsset = NULL; + uint32_t bestStamp = UINT32_MAX; + + repeat(renderer->directSpriteAssetCount, spriteIndex) { + N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex]; + N3DSDirectTextureAsset* asset = &spriteAsset->sheetAsset; + if (asset->ready && asset != excludeAsset && asset->lastUsedFrame != renderer->frameSequence && asset->lastUsedStamp < bestStamp) { + if (asset->pinned) continue; + bestStamp = asset->lastUsedStamp; + bestAsset = asset; + } + + repeat(spriteAsset->frameCount, frameIndex) { + asset = &spriteAsset->frameAssets[frameIndex]; + if (!asset->ready || asset == excludeAsset) continue; + if (asset->lastUsedFrame == renderer->frameSequence) continue; + if (asset->pinned) continue; + if (asset->lastUsedStamp < bestStamp) { + bestStamp = asset->lastUsedStamp; + bestAsset = asset; + } + } + } + + repeat(renderer->directBackgroundAssetCount, bgIndex) { + N3DSDirectTextureAsset* asset = &renderer->directBackgroundAssets[bgIndex]; + if (!asset->ready || asset == excludeAsset) continue; + if (asset->lastUsedFrame == renderer->frameSequence) continue; + if (asset->pinned) continue; + if (asset->lastUsedStamp < bestStamp) { + bestStamp = asset->lastUsedStamp; + bestAsset = asset; + } + } + + repeat(renderer->directFontAssetCount, fontIndex) { + N3DSDirectTextureAsset* asset = &renderer->directFontAssets[fontIndex]; + if (!asset->ready || asset == excludeAsset) continue; + if (asset->lastUsedFrame == renderer->frameSequence) continue; + if (asset->pinned) continue; + if (asset->lastUsedStamp < bestStamp) { + bestStamp = asset->lastUsedStamp; + bestAsset = asset; + } + } + + if (bestAsset == NULL) return false; + N3DSRenderer_freeDirectTextureAsset(bestAsset, renderer); + return true; +} + +static bool N3DSRenderer_evictLRUDirectAssetBlob(N3DSRenderer* renderer, const N3DSDirectTextureAsset* excludeAsset) { + if (renderer == NULL) return false; + + N3DSDirectTextureAsset* bestAsset = NULL; + uint32_t bestStamp = UINT32_MAX; + + repeat(renderer->directSpriteAssetCount, spriteIndex) { + N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex]; + N3DSDirectTextureAsset* asset = &spriteAsset->sheetAsset; + if (asset->blobData != NULL && !asset->ready && asset != excludeAsset && asset->blobLastUsedStamp < bestStamp) { + bestStamp = asset->blobLastUsedStamp; + bestAsset = asset; + } + + repeat(spriteAsset->frameCount, frameIndex) { + asset = &spriteAsset->frameAssets[frameIndex]; + if (asset->blobData == NULL || asset->ready || asset == excludeAsset) continue; + if (asset->blobLastUsedStamp < bestStamp) { + bestStamp = asset->blobLastUsedStamp; + bestAsset = asset; + } + } + } + + repeat(renderer->directBackgroundAssetCount, bgIndex) { + N3DSDirectTextureAsset* asset = &renderer->directBackgroundAssets[bgIndex]; + if (asset->blobData == NULL || asset->ready || asset == excludeAsset) continue; + if (asset->blobLastUsedStamp < bestStamp) { + bestStamp = asset->blobLastUsedStamp; + bestAsset = asset; + } + } + + repeat(renderer->directFontAssetCount, fontIndex) { + N3DSDirectTextureAsset* asset = &renderer->directFontAssets[fontIndex]; + if (asset->blobData == NULL || asset->ready || asset == excludeAsset) continue; + if (asset->blobLastUsedStamp < bestStamp) { + bestStamp = asset->blobLastUsedStamp; + bestAsset = asset; + } + } + + if (bestAsset == NULL) return false; + N3DSRenderer_unloadDirectTextureBlob(bestAsset, renderer); + return true; +} + +static bool N3DSRenderer_ensureDirectTextureBlobLoaded(N3DSRenderer* renderer, N3DSDirectTextureAsset* asset, const char* relativePath) { + if (renderer == NULL || asset == NULL || relativePath == NULL) return false; + if (asset->blobData != NULL && asset->blobSize > 0) { + asset->blobLastUsedStamp = ++renderer->directBlobUseCounter; + return true; + } + + if (N3DSRenderer_tryLoadPackedDirectTextureBlob(renderer, asset, relativePath)) { + return true; + } + + char resolvedPath[512]; + if (!N3DSRenderer_resolveAssetPath(renderer, relativePath, resolvedPath, sizeof(resolvedPath))) return false; + + FILE* file = fopen(resolvedPath, "rb"); + if (file == NULL) return false; + N3DSRenderer_configureFileBuffer(file); + + fseek(file, 0, SEEK_END); + long sizeLong = ftell(file); + fseek(file, 0, SEEK_SET); + if (sizeLong <= 0) { + fclose(file); + return false; + } + + uint32_t size = (uint32_t) sizeLong; + while (renderer->cachedDirectT3xBytes + size > renderer->cachedDirectT3xByteLimit) { + if (!N3DSRenderer_evictLRUDirectAssetBlob(renderer, asset)) break; + } + + uint8_t* blobData = safeMalloc((size_t) size); + if (blobData == NULL) { + fclose(file); + return false; + } + + bool ok = fread(blobData, 1, (size_t) size, file) == (size_t) size; + fclose(file); + if (!ok) { + free(blobData); + return false; + } + + asset->blobData = blobData; + asset->blobSize = size; + asset->blobLastUsedStamp = ++renderer->directBlobUseCounter; + renderer->cachedDirectT3xBytes += size; + renderer->frameDirectBlobReads++; + return true; +} + +static bool N3DSRenderer_loadDirectTextureAsset(N3DSRenderer* renderer, N3DSDirectTextureAsset* asset, const char* relativePath) { + if (renderer == NULL || asset == NULL || relativePath == NULL) return false; + if (asset->ready) { + asset->lastUsedStamp = ++renderer->directAssetUseCounter; + asset->lastUsedFrame = renderer->frameSequence; + if (asset->blobData != NULL) asset->blobLastUsedStamp = ++renderer->directBlobUseCounter; + return true; + } + if (asset->failed) return false; + + if (!N3DSRenderer_ensureDirectTextureBlobLoaded(renderer, asset, relativePath)) { + asset->failed = true; + return false; + } + + C2D_SpriteSheet sheet = C2D_SpriteSheetLoadFromMem(asset->blobData, asset->blobSize); + if (sheet == NULL) { + asset->failed = true; + return false; + } + + C2D_Image image = C2D_SpriteSheetGetImage(sheet, 0); + if (image.tex == NULL || image.subtex == NULL) { + C2D_SpriteSheetFree(sheet); + asset->failed = true; + return false; + } + + uint32_t vramBytes = (uint32_t) image.tex->width * (uint32_t) image.tex->height * 2u; + while (renderer->residentDirectAssetVRAMBytes + vramBytes > renderer->residentDirectAssetVRAMLimitBytes) { + if (!N3DSRenderer_evictLRUDirectAsset(renderer, asset)) break; + } + + asset->sheet = sheet; + asset->image = image; + if (asset->image.tex != NULL) { + C3D_TexSetFilter(asset->image.tex, GPU_NEAREST, GPU_NEAREST); + } + asset->ready = true; + asset->failed = false; + asset->vramBytes = vramBytes; + asset->lastUsedStamp = ++renderer->directAssetUseCounter; + asset->lastUsedFrame = renderer->frameSequence; + renderer->residentDirectAssetVRAMBytes += vramBytes; + renderer->frameDirectAssetLoads++; + return true; +} + +static void N3DSRenderer_applyBlendState(N3DSRenderer* renderer) { + if (!renderer->blendEnabled) { + C3D_AlphaBlend(GPU_BLEND_ADD, GPU_BLEND_ADD, GPU_ONE, GPU_ZERO, GPU_ONE, GPU_ZERO); + return; + } + + GPU_BLENDEQUATION equation = GPU_BLEND_ADD; + switch (renderer->blendEquation) { + case bm_subtract: equation = GPU_BLEND_SUBTRACT; break; + case bm_reverse_subtract: equation = GPU_BLEND_REVERSE_SUBTRACT; break; + case bm_min: equation = GPU_BLEND_MIN; break; + case bm_max: equation = GPU_BLEND_MAX; break; + default: break; + } + + GPU_BLENDFACTOR src = GPU_SRC_ALPHA; + GPU_BLENDFACTOR dst = GPU_ONE_MINUS_SRC_ALPHA; + switch (renderer->blendSrcFactor) { + case bm_zero: src = GPU_ZERO; break; + case bm_one: src = GPU_ONE; break; + case bm_src_color: src = GPU_SRC_COLOR; break; + case bm_inv_src_color: src = GPU_ONE_MINUS_SRC_COLOR; break; + case bm_dest_alpha: src = GPU_DST_ALPHA; break; + case bm_inv_dest_alpha: src = GPU_ONE_MINUS_DST_ALPHA; break; + case bm_dest_color: src = GPU_DST_COLOR; break; + case bm_inv_dest_color: src = GPU_ONE_MINUS_DST_COLOR; break; + case bm_src_alpha_sat: src = GPU_SRC_ALPHA_SATURATE; break; + default: src = GPU_SRC_ALPHA; break; + } + switch (renderer->blendDstFactor) { + case bm_zero: dst = GPU_ZERO; break; + case bm_one: dst = GPU_ONE; break; + case bm_src_color: dst = GPU_SRC_COLOR; break; + case bm_inv_src_color: dst = GPU_ONE_MINUS_SRC_COLOR; break; + case bm_src_alpha: dst = GPU_SRC_ALPHA; break; + case bm_dest_alpha: dst = GPU_DST_ALPHA; break; + case bm_inv_dest_alpha: dst = GPU_ONE_MINUS_DST_ALPHA; break; + case bm_dest_color: dst = GPU_DST_COLOR; break; + case bm_inv_dest_color: dst = GPU_ONE_MINUS_DST_COLOR; break; + case bm_src_alpha_sat: dst = GPU_SRC_ALPHA_SATURATE; break; + default: dst = GPU_ONE_MINUS_SRC_ALPHA; break; + } + + C3D_AlphaBlend(equation, equation, src, dst, src, dst); +} + +static void N3DSRenderer_applyAlphaState(N3DSRenderer* renderer) { + if (renderer->alphaTestEnabled) { + C3D_AlphaTest(true, GPU_GREATER, renderer->alphaTestRef); + } else { + C3D_AlphaTest(false, GPU_ALWAYS, 0); + } +} + +static void N3DSRenderer_setDefaultGPUState(N3DSRenderer* renderer) { + if (renderer == NULL) return; + renderer->blendEnabled = true; + renderer->blendEquation = bm_normal; + renderer->blendSrcFactor = bm_src_alpha; + renderer->blendDstFactor = bm_inv_src_alpha; + renderer->alphaTestEnabled = false; + renderer->alphaTestRef = 0; + N3DSRenderer_applyBlendState(renderer); + N3DSRenderer_applyAlphaState(renderer); +} + +static void N3DSRenderer_flushC2DQueue(N3DSRenderer* renderer) { + if (renderer == NULL || renderer->pendingC2DDraws == 0) return; + C2D_Flush(); + renderer->pendingC2DDraws = 0; + renderer->lastDrawTexture = NULL; +} + +static void N3DSRenderer_noteC2DDraws(N3DSRenderer* renderer, uint32_t drawCount) { + if (renderer == NULL || drawCount == 0) return; + renderer->pendingC2DDraws += drawCount; + if (renderer->pendingC2DDraws >= N3DS_C2D_FLUSH_DRAW_BUDGET) { + N3DSRenderer_flushC2DQueue(renderer); + } +} + +static void N3DSRenderer_sceneBeginTarget(N3DSRenderer* renderer, uint8_t targetKind, bool force) { + if (renderer == NULL) return; + if (!force && renderer->activeSceneTarget == targetKind) return; + + N3DSRenderer_flushC2DQueue(renderer); + + if (targetKind == N3DS_SCENE_TARGET_TOP) { + if (renderer->topTarget == NULL) return; + C2D_SceneBegin(renderer->topTarget); + renderer->activeSceneTarget = N3DS_SCENE_TARGET_TOP; + return; + } + + if (targetKind == N3DS_SCENE_TARGET_BOTTOM) { + if (renderer->bottomTarget == NULL) return; + C2D_SceneBegin(renderer->bottomTarget); + renderer->activeSceneTarget = N3DS_SCENE_TARGET_BOTTOM; + return; + } + + renderer->activeSceneTarget = N3DS_SCENE_TARGET_NONE; +} + +static void N3DSRenderer_freeTileLayerChunkCache(N3DSRenderer* renderer, N3DSTileLayerChunkCache* cache) { + if (cache == NULL || cache->chunks == NULL) return; + + repeat(cache->chunkCount, i) { + N3DSTileLayerChunk* chunk = &cache->chunks[i]; + if (chunk->target != NULL) { + C3D_RenderTargetDelete(chunk->target); + chunk->target = NULL; + } + if (chunk->allocated) { + C3D_TexDelete(&chunk->texture); + memset(&chunk->texture, 0, sizeof(chunk->texture)); + chunk->allocated = false; + } + } + + free(cache->chunks); + cache->chunks = NULL; + cache->chunkCount = 0; + if (renderer != NULL) { + if (renderer->tileLayerChunkVRAMBytes >= cache->vramBytes) renderer->tileLayerChunkVRAMBytes -= cache->vramBytes; + else renderer->tileLayerChunkVRAMBytes = 0; + } + cache->vramBytes = 0; + cache->used = false; +} + +static u32 N3DSRenderer_makeColor(uint32_t bgr, float alpha) { + return C2D_Color32( + (u8) BGR_R(bgr), + (u8) BGR_G(bgr), + (u8) BGR_B(bgr), + (u8) (C2D_Clamp(alpha, 0.0f, 1.0f) * 255.0f) + ); +} + +static bool N3DSRenderer_isIdentityTint(uint32_t color, float alpha) { + return color == 0x00FFFFFFu && alpha >= 0.999f; +} + +static void N3DSRenderer_trackTextureUse(N3DSRenderer* renderer, const C2D_Image* image) { + if (renderer == NULL || image == NULL || image->tex == NULL) return; + if (renderer->lastDrawTexture != image->tex) { + renderer->frameTextureSwitches++; + renderer->lastDrawTexture = image->tex; + } +} + +static void N3DSRenderer_applyTextureFilterForScale(N3DSRenderer* renderer, C3D_Tex* texture, float scaleX, float scaleY) { + if (texture == NULL || !Renderer_isFiniteFloat(scaleX) || !Renderer_isFiniteFloat(scaleY)) return; + + if (renderer != NULL && renderer->textLinearFilterActive) { + C3D_TexSetFilter(texture, GPU_LINEAR, GPU_LINEAR); + return; + } + + if (renderer != NULL && (renderer->bottomScreenGuiActive || renderer->topScreenGuiActive || renderer->topScreenBattleViewActive)) { + C3D_TexSetFilter(texture, GPU_NEAREST, GPU_NEAREST); + return; + } + + GPU_TEXTURE_FILTER_PARAM minFilter = (fabsf(scaleX) < 0.999f || fabsf(scaleY) < 0.999f) + ? GPU_LINEAR + : GPU_NEAREST; + C3D_TexSetFilter(texture, minFilter, GPU_NEAREST); +} + +static float N3DSRenderer_snapToPixel(float value) { + if (!Renderer_isFiniteFloat(value)) return value; + return floorf(value + 0.5f); +} + +static float N3DSRenderer_transformScreenX(const N3DSRenderer* renderer, float localX) { + float screenX = renderer->frameOffsetX + renderer->portOffsetX + (localX - (float) renderer->viewX) * renderer->viewScaleX; + return N3DSRenderer_snapToPixel(screenX); +} + +static float N3DSRenderer_transformScreenY(const N3DSRenderer* renderer, float localY) { + float screenY = renderer->frameOffsetY + renderer->portOffsetY + (localY - (float) renderer->viewY) * renderer->viewScaleY; + return N3DSRenderer_snapToPixel(screenY); +} + +static void N3DSRenderer_transformScreenRect( + const N3DSRenderer* renderer, + float localX, + float localY, + float localW, + float localH, + float* outX, + float* outY, + float* outW, + float* outH +) { + float screenX0 = N3DSRenderer_transformScreenX(renderer, localX); + float screenY0 = N3DSRenderer_transformScreenY(renderer, localY); + float screenX1 = N3DSRenderer_transformScreenX(renderer, localX + localW); + float screenY1 = N3DSRenderer_transformScreenY(renderer, localY + localH); + + *outX = screenX0 < screenX1 ? screenX0 : screenX1; + *outY = screenY0 < screenY1 ? screenY0 : screenY1; + *outW = fabsf(screenX1 - screenX0); + *outH = fabsf(screenY1 - screenY0); +} + +static void N3DSRenderer_drawImageFast(Renderer* base, C2D_Image* image, float localX, float localY, float xscale, float yscale, uint32_t color, float alpha) { + if (image == NULL || image->tex == NULL || image->subtex == NULL) return; + if (!Renderer_isFiniteFloat(localX) || !Renderer_isFiniteFloat(localY) || + !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) || + !Renderer_isFiniteFloat(alpha) || xscale == 0.0f || yscale == 0.0f) { + return; + } + + N3DSRenderer* renderer = (N3DSRenderer*) base; + float localW = (float) image->subtex->width * xscale; + float localH = (float) image->subtex->height * yscale; + float screenX = 0.0f; + float screenY = 0.0f; + float screenW = 0.0f; + float screenH = 0.0f; + N3DSRenderer_transformScreenRect(renderer, localX, localY, localW, localH, &screenX, &screenY, &screenW, &screenH); + float screenScaleX = screenW / (float) image->subtex->width; + float screenScaleY = screenH / (float) image->subtex->height; + if (screenW <= 0.0f || screenH <= 0.0f) return; + if (N3DSRenderer_isScreenRectOffscreen(renderer, screenX, screenY, screenW, screenH)) return; + N3DSRenderer_trackTextureUse(renderer, image); + N3DSRenderer_applyTextureFilterForScale(renderer, image->tex, screenScaleX, screenScaleY); + + if (N3DSRenderer_isIdentityTint(color, alpha)) { + C2D_DrawImageAt(*image, screenX, screenY, 0.5f, NULL, screenScaleX, screenScaleY); + } else { + C2D_ImageTint tint; + C2D_PlainImageTint(&tint, N3DSRenderer_makeColor(color, alpha), 1.0f); + C2D_DrawImageAt(*image, screenX, screenY, 0.5f, &tint, screenScaleX, screenScaleY); + } + N3DSRenderer_noteC2DDraws(renderer, 1u); +} + +static bool N3DSRenderer_getSourceCropRect( + N3DSRenderer* renderer, + int32_t tpagIndex, + int32_t* outX, + int32_t* outY, + int32_t* outW, + int32_t* outH +) { + if (renderer == NULL || outX == NULL || outY == NULL || outW == NULL || outH == NULL) return false; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->base.dataWin->tpag.count) return false; + + TexturePageItem* tpag = &renderer->base.dataWin->tpag.items[tpagIndex]; + if (!N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) { + *outX = 0; + *outY = 0; + *outW = (int32_t) tpag->sourceWidth; + *outH = (int32_t) tpag->sourceHeight; + return *outW > 0 && *outH > 0; + } + + if ((uint32_t) tpagIndex >= renderer->atlasItemCount) return false; + N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex]; + if (item->fragmentCount == 0) return false; + + int32_t minX = INT32_MAX; + int32_t minY = INT32_MAX; + int32_t maxX = INT32_MIN; + int32_t maxY = INT32_MIN; + + repeat(item->fragmentCount, fragIndex) { + uint32_t fragmentIndex = item->fragmentStart + (uint32_t) fragIndex; + if (fragmentIndex >= renderer->atlasFragmentCount) break; + N3DSAtlasFragment* fragment = &renderer->atlasFragments[fragmentIndex]; + int32_t fragLeft = fragment->sourceX; + int32_t fragTop = fragment->sourceY; + int32_t fragRight = fragLeft + fragment->width; + int32_t fragBottom = fragTop + fragment->height; + if (minX > fragLeft) minX = fragLeft; + if (minY > fragTop) minY = fragTop; + if (maxX < fragRight) maxX = fragRight; + if (maxY < fragBottom) maxY = fragBottom; + } + + if (minX >= maxX || minY >= maxY) return false; + *outX = minX; + *outY = minY; + *outW = maxX - minX; + *outH = maxY - minY; + return true; +} + +static void N3DSRenderer_fillSubTexture(Tex3DS_SubTexture* subtex, const N3DSLoadedAtlasPage* page, uint16_t x, uint16_t y, uint16_t width, uint16_t height) { + subtex->width = width; + subtex->height = height; + subtex->left = (float) x / (float) page->width; + subtex->top = (float) (page->height - y) / (float) page->height; + subtex->right = (float) (x + width) / (float) page->width; + subtex->bottom = (float) (page->height - (y + height)) / (float) page->height; +} + +static bool N3DSRenderer_resolveFragmentImage(N3DSRenderer* renderer, const N3DSAtlasFragment* fragment, C2D_Image* outImage, Tex3DS_SubTexture* subtex) { + if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) return false; + if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) return false; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId]; + if (!page->ready || fragment->width == 0 || fragment->height == 0) return false; + + N3DSRenderer_fillSubTexture(subtex, page, fragment->x, fragment->y, fragment->width, fragment->height); + outImage->tex = &page->texture; + outImage->subtex = subtex; + return true; +} + +static N3DSDynamicCaptureTPAG* N3DSRenderer_getDynamicCaptureTPAG(N3DSRenderer* renderer, int32_t tpagIndex) { + if (renderer == NULL) return NULL; + if (tpagIndex < 0 || (uint32_t) tpagIndex < renderer->baseTPAGCount) return NULL; + + uint32_t dynamicIndex = (uint32_t) tpagIndex - renderer->baseTPAGCount; + if (dynamicIndex >= renderer->dynamicCaptureTPAGCount) return NULL; + N3DSDynamicCaptureTPAG* capture = &renderer->dynamicCaptureTPAGs[dynamicIndex]; + if (!capture->used || capture->image.tex == NULL || capture->image.subtex == NULL) return NULL; + return capture; +} + +static void N3DSRenderer_freeDynamicCaptureTPAG(N3DSRenderer* renderer, N3DSDynamicCaptureTPAG* capture) { + if (renderer == NULL || capture == NULL || !capture->used) return; + + C2D_Flush(); + renderer->pendingC2DDraws = 0; + renderer->lastDrawTexture = NULL; + C3D_TexDelete(&capture->texture); + memset(capture, 0, sizeof(*capture)); + capture->ownerSpriteIndex = -1; +} + +static int32_t N3DSRenderer_allocDynamicCaptureTPAG(N3DSRenderer* renderer) { + if (renderer == NULL || renderer->base.dataWin == NULL) return -1; + + repeat(renderer->dynamicCaptureTPAGCount, i) { + if (!renderer->dynamicCaptureTPAGs[i].used) { + renderer->dynamicCaptureTPAGs[i].ownerSpriteIndex = -1; + return (int32_t) (renderer->baseTPAGCount + (uint32_t) i); + } + } + + uint32_t dynamicIndex = renderer->dynamicCaptureTPAGCount; + uint32_t newDynamicCount = dynamicIndex + 1u; + renderer->dynamicCaptureTPAGs = safeRealloc( + renderer->dynamicCaptureTPAGs, + (size_t) newDynamicCount * sizeof(N3DSDynamicCaptureTPAG) + ); + memset(&renderer->dynamicCaptureTPAGs[dynamicIndex], 0, sizeof(renderer->dynamicCaptureTPAGs[dynamicIndex])); + renderer->dynamicCaptureTPAGs[dynamicIndex].ownerSpriteIndex = -1; + renderer->dynamicCaptureTPAGCount = newDynamicCount; + + DataWin* dw = renderer->base.dataWin; + uint32_t newTPAGCount = renderer->baseTPAGCount + renderer->dynamicCaptureTPAGCount; + dw->tpag.items = safeRealloc(dw->tpag.items, (size_t) newTPAGCount * sizeof(TexturePageItem)); + memset(&dw->tpag.items[newTPAGCount - 1u], 0, sizeof(TexturePageItem)); + dw->tpag.count = newTPAGCount; + return (int32_t) (newTPAGCount - 1u); +} + +static bool N3DSRenderer_tryLoadDirectSpriteImage(N3DSRenderer* renderer, int32_t spriteIndex, int32_t frameIndex, C2D_Image** outImage) { + if (renderer == NULL || outImage == NULL) return false; + if (spriteIndex < 0 || (uint32_t) spriteIndex >= renderer->directSpriteAssetCount) return false; + + N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex]; + if (frameIndex < 0 || (uint32_t) frameIndex >= spriteAsset->frameCount) return false; + + N3DSDirectTextureAsset* sheetAsset = &spriteAsset->sheetAsset; + bool preferFrameFallback = (spriteIndex == 24); + const char* spriteName = NULL; + bool preferNamedButtonOverride = false; + + if (renderer->base.dataWin != NULL && + (uint32_t) spriteIndex < renderer->base.dataWin->sprt.count) { + spriteName = renderer->base.dataWin->sprt.sprites[spriteIndex].name; + preferNamedButtonOverride = Renderer_isBattleButtonSpriteName(spriteName); + if (spriteName != NULL && + (strstr(spriteName, "spr_mainchara") != NULL || + strstr(spriteName, "spr_f_mainchara") != NULL || + strstr(spriteName, "spr_chara") != NULL || + strstr(spriteName, "spr_heart") != NULL || + strstr(spriteName, "heart_") != NULL || + strstr(spriteName, "spr_soul") != NULL || + strstr(spriteName, "soul") != NULL)) { + preferFrameFallback = true; + spriteAsset->useFrameFallback = true; + } + } + + if (preferFrameFallback) spriteAsset->useFrameFallback = true; + + bool hasLoadedDirectSheet = sheetAsset->ready || (sheetAsset->blobData != NULL && sheetAsset->blobSize > 0); + bool hasLoadedDirectFrame = false; + if (spriteAsset->frameAssets != NULL) { + N3DSDirectTextureAsset* existingFrameAsset = &spriteAsset->frameAssets[frameIndex]; + hasLoadedDirectFrame = existingFrameAsset->ready || (existingFrameAsset->blobData != NULL && existingFrameAsset->blobSize > 0); + } + bool allowColdDirectLoad = preferNamedButtonOverride; + if (!allowColdDirectLoad && !hasLoadedDirectSheet && !hasLoadedDirectFrame) { + return false; + } + + if (preferNamedButtonOverride && spriteAsset->frameAssets != NULL) { + N3DSDirectTextureAsset* frameAsset = &spriteAsset->frameAssets[frameIndex]; + if (frameAsset->ready) { + frameAsset->lastUsedStamp = ++renderer->directAssetUseCounter; + frameAsset->lastUsedFrame = renderer->frameSequence; + *outImage = &frameAsset->image; + return true; + } + if (!frameAsset->failed && spriteName != NULL) { + char overridePath[256]; + const char* overrideBaseName = spriteName; + if (strcmp(spriteName, "spr_talkbt") == 0) { + overrideBaseName = "spr_actbt_center"; + } else if (strcmp(spriteName, "spr_talkbt_hollow") == 0) { + overrideBaseName = "spr_actbt_center_hole"; + } + snprintf(overridePath, sizeof(overridePath), "button_overrides/%s_%d.t3x", overrideBaseName, (int) frameIndex); + if (N3DSRenderer_loadDirectTextureAsset(renderer, frameAsset, overridePath)) { + *outImage = &frameAsset->image; + return true; + } + snprintf(overridePath, sizeof(overridePath), "button_overrides/%s.t3x", overrideBaseName); + if (frameIndex == 0 && N3DSRenderer_loadDirectTextureAsset(renderer, frameAsset, overridePath)) { + *outImage = &frameAsset->image; + return true; + } + } + } + + if (preferFrameFallback && spriteAsset->frameAssets != NULL) { + N3DSDirectTextureAsset* frameAsset = &spriteAsset->frameAssets[frameIndex]; + if (frameAsset->ready) { + frameAsset->lastUsedStamp = ++renderer->directAssetUseCounter; + frameAsset->lastUsedFrame = renderer->frameSequence; + *outImage = &frameAsset->image; + return true; + } + if (!frameAsset->failed) { + char fallbackPath[256]; + snprintf(fallbackPath, sizeof(fallbackPath), "sprites/spr_%05d_frame_%05d.t3x", (int) spriteIndex, (int) frameIndex); + if (N3DSRenderer_loadDirectTextureAsset(renderer, frameAsset, fallbackPath)) { + *outImage = &frameAsset->image; + return true; + } + } + } + + if (sheetAsset->ready && (uint32_t) frameIndex < spriteAsset->sheetFrameCount) { + sheetAsset->lastUsedStamp = ++renderer->directAssetUseCounter; + sheetAsset->lastUsedFrame = renderer->frameSequence; + N3DSRenderer_buildSheetFrameImages(spriteAsset); + if (spriteAsset->sheetFrameImages != NULL) { + *outImage = &spriteAsset->sheetFrameImages[frameIndex]; + return true; + } + *outImage = &sheetAsset->image; + return true; + } + + if (!spriteAsset->sheetLoadAttempted) { + char path[256]; + spriteAsset->sheetLoadAttempted = true; + snprintf(path, sizeof(path), "sprites/spr_%05d.t3x", (int) spriteIndex); + if (N3DSRenderer_loadDirectTextureAsset(renderer, sheetAsset, path) && sheetAsset->sheet != NULL) { + spriteAsset->sheetFrameCount = (uint32_t) C2D_SpriteSheetCount(sheetAsset->sheet); + if ((uint32_t) frameIndex < spriteAsset->sheetFrameCount) { + sheetAsset->lastUsedStamp = ++renderer->directAssetUseCounter; + sheetAsset->lastUsedFrame = renderer->frameSequence; + N3DSRenderer_buildSheetFrameImages(spriteAsset); + if (spriteAsset->sheetFrameImages != NULL) { + *outImage = &spriteAsset->sheetFrameImages[frameIndex]; + return true; + } + *outImage = &sheetAsset->image; + return true; + } + } + spriteAsset->useFrameFallback = true; + } + + if (!spriteAsset->useFrameFallback || spriteAsset->frameAssets == NULL) return false; + + N3DSDirectTextureAsset* frameAsset = &spriteAsset->frameAssets[frameIndex]; + if (frameAsset->ready) { + frameAsset->lastUsedStamp = ++renderer->directAssetUseCounter; + frameAsset->lastUsedFrame = renderer->frameSequence; + *outImage = &frameAsset->image; + return true; + } + if (frameAsset->failed) return false; + + char fallbackPath[256]; + snprintf(fallbackPath, sizeof(fallbackPath), "sprites/spr_%05d_frame_%05d.t3x", (int) spriteIndex, (int) frameIndex); + if (!N3DSRenderer_loadDirectTextureAsset(renderer, frameAsset, fallbackPath)) return false; + *outImage = &frameAsset->image; + return true; +} + +static bool N3DSRenderer_tryLoadDirectBackgroundImage(N3DSRenderer* renderer, int32_t backgroundIndex, C2D_Image** outImage) { + if (renderer == NULL || outImage == NULL) return false; + if (backgroundIndex < 0 || (uint32_t) backgroundIndex >= renderer->directBackgroundAssetCount) return false; + + N3DSDirectTextureAsset* asset = &renderer->directBackgroundAssets[backgroundIndex]; + if (!asset->ready && (asset->blobData == NULL || asset->blobSize == 0)) { + return false; + } + char path[256]; + snprintf(path, sizeof(path), "backgrounds/bg_%05d.t3x", (int) backgroundIndex); + if (!N3DSRenderer_loadDirectTextureAsset(renderer, asset, path)) return false; + *outImage = &asset->image; + return true; +} + +static bool N3DSRenderer_tryResolveDirectFontImage(N3DSRenderer* renderer, int32_t fontIndex, C2D_Image* outImage, Tex3DS_SubTexture* outSubtex) { +#if !N3DS_ENABLE_DIRECT_ASSETS + (void) renderer; + (void) fontIndex; + (void) outImage; + (void) outSubtex; + return false; +#endif + if (renderer == NULL || outImage == NULL || outSubtex == NULL) return false; + if (fontIndex < 0 || (uint32_t) fontIndex >= renderer->directFontAssetCount) return false; + + N3DSDirectTextureAsset* asset = &renderer->directFontAssets[fontIndex]; + char path[256]; + snprintf(path, sizeof(path), "fonts/font_%05d.t3x", (int) fontIndex); + if (!N3DSRenderer_loadDirectTextureAsset(renderer, asset, path)) return false; + if (asset->image.tex == NULL || asset->image.subtex == NULL) return false; + + *outImage = asset->image; + *outSubtex = *asset->image.subtex; + outImage->subtex = outSubtex; + return true; +} + +static bool N3DSRenderer_cropDirectSpriteImage( + const C2D_Image* directImage, + const TexturePageItem* tpag, + C2D_Image* outImage, + Tex3DS_SubTexture* outSubtex, + float* outDrawX, + float* outDrawY, + float x, + float y, + float originX, + float originY, + float xscale, + float yscale +) { + if (directImage == NULL || directImage->tex == NULL || directImage->subtex == NULL || + tpag == NULL || outImage == NULL || outSubtex == NULL || outDrawX == NULL || outDrawY == NULL) { + return false; + } + + int32_t frameW = (int32_t) directImage->subtex->width; + int32_t frameH = (int32_t) directImage->subtex->height; + int32_t cropX = (int32_t) tpag->targetX; + int32_t cropY = (int32_t) tpag->targetY; + int32_t cropW = (int32_t) tpag->sourceWidth; + int32_t cropH = (int32_t) tpag->sourceHeight; + if (frameW <= 0 || frameH <= 0 || cropW <= 0 || cropH <= 0) return false; + + int32_t drawTargetX = cropX; + int32_t drawTargetY = cropY; + if (cropX < 0) { + cropW += cropX; + cropX = 0; + drawTargetX = 0; + } + if (cropY < 0) { + cropH += cropY; + cropY = 0; + drawTargetY = 0; + } + if (cropX + cropW > frameW) cropW = frameW - cropX; + if (cropY + cropH > frameH) cropH = frameH - cropY; + if (cropW <= 0 || cropH <= 0) return false; + + const Tex3DS_SubTexture* baseSubtex = directImage->subtex; + float baseW = (float) baseSubtex->width; + float baseH = (float) baseSubtex->height; + if (baseW <= 0.0f || baseH <= 0.0f) return false; + + *outSubtex = *baseSubtex; + outSubtex->width = (uint16_t) cropW; + outSubtex->height = (uint16_t) cropH; + outSubtex->left = baseSubtex->left + (baseSubtex->right - baseSubtex->left) * ((float) cropX / baseW); + outSubtex->right = baseSubtex->left + (baseSubtex->right - baseSubtex->left) * ((float) (cropX + cropW) / baseW); + outSubtex->top = baseSubtex->top + (baseSubtex->bottom - baseSubtex->top) * ((float) cropY / baseH); + outSubtex->bottom = baseSubtex->top + (baseSubtex->bottom - baseSubtex->top) * ((float) (cropY + cropH) / baseH); + + *outImage = *directImage; + outImage->subtex = outSubtex; + *outDrawX = x + ((float) drawTargetX - originX) * xscale; + *outDrawY = y + ((float) drawTargetY - originY) * yscale; + return true; +} + +static bool N3DSRenderer_tryDrawDirectMappedSprite(Renderer* base, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) { +#if !N3DS_ENABLE_DIRECT_ASSETS + (void) base; + (void) tpagIndex; + (void) x; + (void) y; + (void) originX; + (void) originY; + (void) xscale; + (void) yscale; + (void) angleDeg; + (void) color; + (void) alpha; + return false; +#endif + if (base == NULL) return false; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + C2D_Image* directImage = NULL; + bool isBackgroundImage = false; + + if (renderer->lastDirectTPAGIndex == tpagIndex && + renderer->lastDirectTPAGImage != NULL && + renderer->lastDirectTPAGImage->tex != NULL && + renderer->lastDirectTPAGImage->subtex != NULL) { + directImage = renderer->lastDirectTPAGImage; + } + + if (directImage == NULL && + renderer->tpagToSpriteIndex != NULL && renderer->tpagToSpriteFrameIndex != NULL && + tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) { + int32_t spriteIndex = renderer->tpagToSpriteIndex[tpagIndex]; + int32_t frameIndex = renderer->tpagToSpriteFrameIndex[tpagIndex]; + if (spriteIndex >= 0 && frameIndex >= 0 && N3DSRenderer_tryLoadDirectSpriteImage(renderer, spriteIndex, frameIndex, &directImage)) { + renderer->lastDirectTPAGIndex = tpagIndex; + renderer->lastDirectTPAGImage = directImage; + } + } + + if (directImage == NULL && + renderer->tpagToBackgroundIndex != NULL && + tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) { + int32_t backgroundIndex = renderer->tpagToBackgroundIndex[tpagIndex]; + if (backgroundIndex >= 0 && N3DSRenderer_tryLoadDirectBackgroundImage(renderer, backgroundIndex, &directImage)) { + isBackgroundImage = true; + renderer->lastDirectTPAGIndex = tpagIndex; + renderer->lastDirectTPAGImage = directImage; + } + } + + if (directImage == NULL) return false; + + if (!isBackgroundImage && + renderer->tpagToBackgroundIndex != NULL && + tpagIndex >= 0 && + (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count && + renderer->tpagToBackgroundIndex[tpagIndex] >= 0 && + renderer->tpagToSpriteIndex[tpagIndex] < 0) { + isBackgroundImage = true; + } + + C2D_Image croppedImage; + Tex3DS_SubTexture croppedSubtex; + C2D_Image* drawImage = directImage; + float drawX = x - originX * xscale; + float drawY = y - originY * yscale; + if (!isBackgroundImage && tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) { + TexturePageItem* tpag = &renderer->base.dataWin->tpag.items[tpagIndex]; + if (N3DSRenderer_cropDirectSpriteImage( + directImage, + tpag, + &croppedImage, + &croppedSubtex, + &drawX, + &drawY, + x, + y, + originX, + originY, + xscale, + yscale)) { + drawImage = &croppedImage; + } + } + + renderer->frameDirectSpriteHits++; + if (fabsf(angleDeg) < 0.001f && xscale > 0.0f && yscale > 0.0f) { + N3DSRenderer_drawImageFast(base, drawImage, drawX, drawY, xscale, yscale, color, alpha); + } else { + float pivotX = x - drawX; + float pivotY = y - drawY; + N3DSRenderer_drawImage( + base, + drawImage, + drawX, + drawY, + (float) drawImage->subtex->width * xscale, + (float) drawImage->subtex->height * yscale, + pivotX, + pivotY, + angleDeg, + color, + alpha + ); + } + return true; +} + +static bool N3DSRenderer_loadAtlas(N3DSRenderer* renderer) { + char assetPath[256]; + FILE* atlasFile = N3DSRenderer_openAssetFile(renderer, "atlas.bin", assetPath, sizeof(assetPath)); + if (atlasFile == NULL) { + N3DSRenderer_setStartupError(renderer, "Missing gfx/atlas.bin on SD or ROMFS"); + fprintf(stderr, "N3DS: missing atlas.bin in sdmc:/3ds/cinnamon/gfx or romfs:/gfx\n"); + return false; + } + fprintf(stderr, "N3DS: loading atlas from %s\n", assetPath); + + fseek(atlasFile, 0, SEEK_END); + long atlasSize = ftell(atlasFile); + fseek(atlasFile, 0, SEEK_SET); + if (atlasSize < 24) { + fclose(atlasFile); + N3DSRenderer_setStartupError(renderer, "gfx/atlas.bin is too small or corrupt"); + return false; + } + + uint8_t header[24]; + if (fread(header, 1, sizeof(header), atlasFile) != sizeof(header)) { + fclose(atlasFile); + N3DSRenderer_setStartupError(renderer, "Failed to read gfx/atlas.bin header"); + return false; + } + + uint16_t version = N3DS_readU16(header + 4); + if (N3DS_readU32(header) != N3DS_ATLAS_MAGIC || + (version != N3DS_ATLAS_VERSION_RAW && + version != N3DS_ATLAS_VERSION_T3X && + version != N3DS_ATLAS_VERSION_FRAGMENTED && + version != N3DS_ATLAS_VERSION_FRAGMENTED_TILES && + version != N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS && + version != N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT && + version != N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT && + version != N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED)) { + fclose(atlasFile); + N3DSRenderer_setStartupError(renderer, "gfx/atlas.bin has an invalid header"); + fprintf(stderr, "N3DS: invalid atlas header\n"); + return false; + } + + renderer->atlasVersion = version; + renderer->atlasTextureFormat = N3DS_TEXFMT_RGBA5551; + renderer->atlasPageCount = N3DS_readU16(header + 6); + renderer->atlasItemCount = N3DS_readU32(header + 8); + if (version == N3DS_ATLAS_VERSION_FRAGMENTED || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + renderer->atlasFragmentCount = N3DS_readU32(header + 12); + if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + renderer->tileEntryCount = N3DS_readU32(header + 16); + if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + renderer->atlasTextureFormat = N3DS_readU32(header + 20); + } + } + } + fprintf(stderr, "N3DS: atlas version=%u format=%s pages=%u items=%lu fragments=%lu tiles=%lu\n", + (unsigned int) renderer->atlasVersion, + N3DSRenderer_getTextureFormatName(renderer->atlasTextureFormat), + (unsigned int) renderer->atlasPageCount, + (unsigned long) renderer->atlasItemCount, + (unsigned long) renderer->atlasFragmentCount, + (unsigned long) renderer->tileEntryCount); + + renderer->atlasPages = safeCalloc(renderer->atlasPageCount, sizeof(N3DSLoadedAtlasPage)); + if (version == N3DS_ATLAS_VERSION_FRAGMENTED || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + renderer->atlasItemsV3 = safeCalloc(renderer->atlasItemCount, sizeof(N3DSAtlasItemV3)); + renderer->atlasFragments = safeCalloc(renderer->atlasFragmentCount, sizeof(N3DSAtlasFragment)); + if (renderer->tileEntryCount > 0) { + renderer->tileEntries = safeCalloc(renderer->tileEntryCount, sizeof(N3DSTileAtlasEntry)); + } + } else { + renderer->atlasItems = safeCalloc(renderer->atlasItemCount, sizeof(N3DSAtlasItem)); + } + + size_t cursor = 12; + if (version == N3DS_ATLAS_VERSION_FRAGMENTED) cursor = 16; + if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS) cursor = 20; + if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) cursor = 24; + + size_t metadataSize = cursor; + if (version == N3DS_ATLAS_VERSION_RAW) metadataSize += (size_t) renderer->atlasPageCount * 12u; + else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT) metadataSize += (size_t) renderer->atlasPageCount * 8u; + else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) metadataSize += (size_t) renderer->atlasPageCount * 16u; + else metadataSize += (size_t) renderer->atlasPageCount * 4u; + + if (version == N3DS_ATLAS_VERSION_FRAGMENTED || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + metadataSize += (size_t) renderer->atlasItemCount * 10u; + metadataSize += (size_t) renderer->atlasFragmentCount * 14u; + if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES) metadataSize += (size_t) renderer->tileEntryCount * 20u; + else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || + version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) metadataSize += (size_t) renderer->tileEntryCount * 26u; + } else { + metadataSize += (size_t) renderer->atlasItemCount * 10u; + } + + if ((long) metadataSize > atlasSize) { + fclose(atlasFile); + N3DSRenderer_setStartupError(renderer, "gfx/atlas.bin metadata is truncated"); + return false; + } + + fseek(atlasFile, 0, SEEK_SET); + uint8_t* blob = safeMalloc(metadataSize); + if (fread(blob, 1, metadataSize, atlasFile) != metadataSize) { + free(blob); + fclose(atlasFile); + N3DSRenderer_setStartupError(renderer, "Failed to read gfx/atlas.bin metadata"); + return false; + } + + N3DSAtlasPageInfo* pageInfos = safeCalloc(renderer->atlasPageCount, sizeof(N3DSAtlasPageInfo)); + repeat(renderer->atlasPageCount, i) { + pageInfos[i].width = N3DS_readU16(blob + cursor + 0); + pageInfos[i].height = N3DS_readU16(blob + cursor + 2); + pageInfos[i].textureFormat = renderer->atlasTextureFormat; + if (version == N3DS_ATLAS_VERSION_RAW) { + pageInfos[i].dataOffset = N3DS_readU32(blob + cursor + 4); + pageInfos[i].dataSize = N3DS_readU32(blob + cursor + 8); + cursor += 12; + } else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + pageInfos[i].textureFormat = N3DS_readU32(blob + cursor + 4); + pageInfos[i].dataOffset = N3DS_readU32(blob + cursor + 8); + pageInfos[i].dataSize = N3DS_readU32(blob + cursor + 12); + cursor += 16; + } else if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT) { + pageInfos[i].textureFormat = N3DS_readU32(blob + cursor + 4); + cursor += 8; + } else { + cursor += 4; + } + } + if (version == N3DS_ATLAS_VERSION_FRAGMENTED || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + repeat(renderer->atlasItemCount, i) { + renderer->atlasItemsV3[i].width = N3DS_readU16(blob + cursor + 0); + renderer->atlasItemsV3[i].height = N3DS_readU16(blob + cursor + 2); + renderer->atlasItemsV3[i].fragmentStart = N3DS_readU32(blob + cursor + 4); + renderer->atlasItemsV3[i].fragmentCount = N3DS_readU16(blob + cursor + 8); + cursor += 10; + } + repeat(renderer->atlasFragmentCount, i) { + renderer->atlasFragments[i].atlasId = N3DS_readU16(blob + cursor + 0); + renderer->atlasFragments[i].x = N3DS_readU16(blob + cursor + 2); + renderer->atlasFragments[i].y = N3DS_readU16(blob + cursor + 4); + renderer->atlasFragments[i].width = N3DS_readU16(blob + cursor + 6); + renderer->atlasFragments[i].height = N3DS_readU16(blob + cursor + 8); + renderer->atlasFragments[i].sourceX = N3DS_readU16(blob + cursor + 10); + renderer->atlasFragments[i].sourceY = N3DS_readU16(blob + cursor + 12); + cursor += 14; + } + if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILES || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + repeat(renderer->tileEntryCount, i) { + renderer->tileEntries[i].bgDef = (int16_t) N3DS_readU16(blob + cursor + 0); + renderer->tileEntries[i].srcX = N3DS_readU16(blob + cursor + 2); + renderer->tileEntries[i].srcY = N3DS_readU16(blob + cursor + 4); + renderer->tileEntries[i].srcW = N3DS_readU16(blob + cursor + 6); + renderer->tileEntries[i].srcH = N3DS_readU16(blob + cursor + 8); + renderer->tileEntries[i].atlasId = N3DS_readU16(blob + cursor + 10); + renderer->tileEntries[i].x = N3DS_readU16(blob + cursor + 12); + renderer->tileEntries[i].y = N3DS_readU16(blob + cursor + 14); + renderer->tileEntries[i].width = N3DS_readU16(blob + cursor + 16); + renderer->tileEntries[i].height = N3DS_readU16(blob + cursor + 18); + if (version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + renderer->tileEntries[i].fragmentStart = N3DS_readU32(blob + cursor + 20); + renderer->tileEntries[i].fragmentCount = N3DS_readU16(blob + cursor + 24); + cursor += 26; + } else { + renderer->tileEntries[i].fragmentStart = UINT32_MAX; + renderer->tileEntries[i].fragmentCount = 0; + cursor += 20; + } + } + } + } else { + repeat(renderer->atlasItemCount, i) { + renderer->atlasItems[i].atlasId = N3DS_readU16(blob + cursor + 0); + renderer->atlasItems[i].x = N3DS_readU16(blob + cursor + 2); + renderer->atlasItems[i].y = N3DS_readU16(blob + cursor + 4); + renderer->atlasItems[i].width = N3DS_readU16(blob + cursor + 6); + renderer->atlasItems[i].height = N3DS_readU16(blob + cursor + 8); + cursor += 10; + } + } + free(blob); + if (N3DSRenderer_isPackedAtlasVersion(version)) renderer->packedAtlasFile = atlasFile; + else fclose(atlasFile); + + repeat(renderer->atlasPageCount, i) { + N3DSLoadedAtlasPage* page = &renderer->atlasPages[i]; + page->width = pageInfos[i].width; + page->height = pageInfos[i].height; + page->textureFormat = pageInfos[i].textureFormat; + page->dataOffset = pageInfos[i].dataOffset; + page->dataSize = pageInfos[i].dataSize; + } + free(pageInfos); + if (renderer->tileEntryCount > 0 && renderer->tileEntries != NULL) { + repeat(renderer->tileEntryCount, i) { + N3DSTileLookupKey key = { + .bgDef = renderer->tileEntries[i].bgDef, + .srcX = renderer->tileEntries[i].srcX, + .srcY = renderer->tileEntries[i].srcY, + .srcW = renderer->tileEntries[i].srcW, + .srcH = renderer->tileEntries[i].srcH, + }; + hmput(renderer->tileEntryMap, key, (uint32_t) i); + } + } + fprintf(stderr, "N3DS: atlas metadata ready for %u pages; using lazy page loading (%u resident max)\n", + (unsigned int) renderer->atlasPageCount, + (unsigned int) renderer->residentAtlasPageLimit); + + if (renderer->atlasTextureFormat == N3DS_TEXFMT_ETC1A4) { + renderer->residentAtlasPageLimit = renderer->isNew3DS ? N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS_ETC1A4 : N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS_ETC1A4; + renderer->prewarmGpuPageBudget = renderer->isNew3DS ? N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS_ETC1A4 : N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS_ETC1A4; + fprintf(stderr, "N3DS: ETC1A4 atlas detected; residentLimit=%u prewarm=%u\n", + (unsigned int) renderer->residentAtlasPageLimit, + (unsigned int) renderer->prewarmGpuPageBudget); + } else if (renderer->atlasTextureFormat == N3DS_TEXFMT_INDEXED8) { + fprintf(stderr, "N3DS: indexed8 atlas detected; using temporary CPU expansion to rgba5551 until shader-backed palette sampling lands\n"); + } else if (renderer->atlasTextureFormat == N3DS_TEXFMT_HYBRID || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT || version == N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED) { + renderer->residentAtlasPageLimit = renderer->isNew3DS ? N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS_ETC1A4 : N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS_ETC1A4; + renderer->prewarmGpuPageBudget = renderer->isNew3DS ? N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS_ETC1A4 : N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS_ETC1A4; + fprintf(stderr, "N3DS: hybrid atlas detected; mixed page formats enabled\n"); + } + return true; +} + +static bool N3DSRenderer_resolveImage(N3DSRenderer* renderer, int32_t tpagIndex, C2D_Image* outImage, Tex3DS_SubTexture* subtex) { + if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) return false; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return false; + N3DSAtlasItem* item = &renderer->atlasItems[tpagIndex]; + if ((uint32_t) item->atlasId >= renderer->atlasPageCount) return false; + if (!N3DSRenderer_ensurePageLoaded(renderer, item->atlasId)) return false; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[item->atlasId]; + if (!page->ready || item->width == 0 || item->height == 0) return false; + + N3DSRenderer_fillSubTexture(subtex, page, item->x, item->y, item->width, item->height); + outImage->tex = &page->texture; + outImage->subtex = subtex; + return true; +} + +static bool N3DSRenderer_getPageIndexForTPAG(N3DSRenderer* renderer, int32_t tpagIndex, uint32_t* outPageIndex) { + if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return false; + + if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) { + N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex]; + if (item->fragmentCount == 0) return false; + N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart]; + if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) return false; + *outPageIndex = fragment->atlasId; + return true; + } + + N3DSAtlasItem* item = &renderer->atlasItems[tpagIndex]; + if ((uint32_t) item->atlasId >= renderer->atlasPageCount) return false; + *outPageIndex = item->atlasId; + return true; +} + +static N3DSTileAtlasEntry* N3DSRenderer_findTileEntryByKey(N3DSRenderer* renderer, int32_t backgroundIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, uint32_t* outEntryIndex) { + if (renderer == NULL || renderer->tileEntryMap == NULL || renderer->tileEntries == NULL) return NULL; + if (backgroundIndex < 0 || srcW <= 0 || srcH <= 0) return NULL; + + N3DSTileLookupKey key = { + .bgDef = (int16_t) backgroundIndex, + .srcX = (uint16_t) srcX, + .srcY = (uint16_t) srcY, + .srcW = (uint16_t) srcW, + .srcH = (uint16_t) srcH, + }; + ptrdiff_t mapIndex = hmgeti(renderer->tileEntryMap, key); + if (mapIndex < 0) return NULL; + uint32_t entryIndex = renderer->tileEntryMap[mapIndex].value; + if (entryIndex >= renderer->tileEntryCount) return NULL; + if (outEntryIndex != NULL) *outEntryIndex = entryIndex; + return &renderer->tileEntries[entryIndex]; +} + +static N3DSTileAtlasEntry* N3DSRenderer_findTileEntry(N3DSRenderer* renderer, RoomTile* tile) { + if (renderer == NULL || tile == NULL) return NULL; + return N3DSRenderer_findTileEntryByKey( + renderer, + tile->backgroundDefinition, + tile->sourceX, + tile->sourceY, + (int32_t) tile->width, + (int32_t) tile->height, + NULL + ); +} + +static void N3DSRenderer_prewarmTileEntry(N3DSRenderer* renderer, bool* seenPages, RoomTile* tile) { + N3DSTileAtlasEntry* entry = N3DSRenderer_findTileEntry(renderer, tile); + if (entry == NULL) return; + if (entry->fragmentCount > 0) { + repeat(entry->fragmentCount, i) { + uint32_t fragmentIndex = entry->fragmentStart + (uint32_t) i; + if (fragmentIndex >= renderer->atlasFragmentCount) break; + N3DSRenderer_prewarmPage(renderer, seenPages, renderer->atlasFragments[fragmentIndex].atlasId); + } + } else if (entry->atlasId < renderer->atlasPageCount) { + N3DSRenderer_prewarmPage(renderer, seenPages, entry->atlasId); + } +} + +static bool N3DSRenderer_prewarmPageBlobOnly(N3DSRenderer* renderer, bool* seenPages, uint32_t pageIndex, uint32_t* remainingBlobBytes) { + if (renderer == NULL || seenPages == NULL) return false; + if (pageIndex >= renderer->atlasPageCount || seenPages[pageIndex]) return false; + + N3DSLoadedAtlasPage* page = &renderer->atlasPages[pageIndex]; + seenPages[pageIndex] = true; + + if (page->t3xData != NULL && page->t3xSize > 0) { + page->blobLastUsedStamp = ++renderer->blobUseCounter; + return true; + } + + uint32_t blobSize = page->dataSize; + if (blobSize == 0 && page->t3xSize > 0) blobSize = page->t3xSize; + if (remainingBlobBytes != NULL && blobSize > 0 && *remainingBlobBytes < blobSize) { + return false; + } + + if (!N3DSRenderer_ensurePageBlobLoaded(renderer, pageIndex)) return false; + + if (remainingBlobBytes != NULL && blobSize > 0) { + if (*remainingBlobBytes > blobSize) *remainingBlobBytes -= blobSize; + else *remainingBlobBytes = 0; + } + return true; +} + +static void N3DSRenderer_prewarmTileEntryBlobOnly(N3DSRenderer* renderer, bool* seenPages, const N3DSTileAtlasEntry* entry, uint32_t* remainingBlobBytes) { + if (renderer == NULL || seenPages == NULL || entry == NULL || remainingBlobBytes == NULL || *remainingBlobBytes == 0) return; + + if (entry->fragmentCount > 0) { + repeat(entry->fragmentCount, i) { + uint32_t fragmentIndex = entry->fragmentStart + (uint32_t) i; + if (fragmentIndex >= renderer->atlasFragmentCount) break; + (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, renderer->atlasFragments[fragmentIndex].atlasId, remainingBlobBytes); + if (*remainingBlobBytes == 0) break; + } + } else if (entry->atlasId < renderer->atlasPageCount) { + (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, entry->atlasId, remainingBlobBytes); + } +} + +static void N3DSRenderer_prewarmTPAGBlobOnly(N3DSRenderer* renderer, bool* seenPages, int32_t tpagIndex, uint32_t* remainingBlobBytes) { + if (renderer == NULL || seenPages == NULL || remainingBlobBytes == NULL || *remainingBlobBytes == 0) return; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return; + + if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) { + N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex]; + repeat(item->fragmentCount, i) { + uint32_t fragmentIndex = item->fragmentStart + (uint32_t) i; + if (fragmentIndex >= renderer->atlasFragmentCount) break; + (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, renderer->atlasFragments[fragmentIndex].atlasId, remainingBlobBytes); + if (*remainingBlobBytes == 0) break; + } + return; + } + + (void) N3DSRenderer_prewarmPageBlobOnly(renderer, seenPages, renderer->atlasItems[tpagIndex].atlasId, remainingBlobBytes); +} + +static void N3DSRenderer_prewarmPage(N3DSRenderer* renderer, bool* seenPages, uint32_t pageIndex) { + if (pageIndex >= renderer->atlasPageCount || seenPages[pageIndex]) return; + seenPages[pageIndex] = true; + if (!N3DSRenderer_ensurePageBlobLoaded(renderer, pageIndex)) return; + if (renderer->residentAtlasPageCount < renderer->prewarmGpuPageBudget) { + (void) N3DSRenderer_ensurePageLoaded(renderer, pageIndex); + } +} + +static void N3DSRenderer_prewarmTPAG(N3DSRenderer* renderer, bool* seenPages, int32_t tpagIndex) { + if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return; + + if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) { + N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex]; + repeat(item->fragmentCount, i) { + uint32_t fragmentIndex = item->fragmentStart + (uint32_t) i; + if (fragmentIndex >= renderer->atlasFragmentCount) break; + N3DSRenderer_prewarmPage(renderer, seenPages, renderer->atlasFragments[fragmentIndex].atlasId); + } + return; + } + + N3DSRenderer_prewarmPage(renderer, seenPages, renderer->atlasItems[tpagIndex].atlasId); +} + +static void N3DSRenderer_freeCachedTextLayout(N3DSCachedTextLayout* layout) { + free(layout->text); + free(layout->glyphs); + memset(layout, 0, sizeof(*layout)); + layout->fontIndex = -1; +} + +static void N3DSRenderer_appendCachedTextLayoutSuffix(N3DSCachedTextLayout* layout, Font* font, const char* suffix, int32_t suffixLen) { + if (suffixLen <= 0) return; + + int32_t pos = 0; + float cursorX = layout->appendCursorX; + float cursorY = layout->appendCursorY; + uint16_t prevCodepoint = layout->appendPrevCodepoint; + bool atLineStart = layout->appendAtLineStart; + + while (pos < suffixLen) { + if (TextUtils_isNewlineChar(suffix[pos])) { + pos = TextUtils_skipNewline(suffix, pos, suffixLen); + cursorX = 0.0f; + cursorY += TextUtils_lineStride(font); + prevCodepoint = 0; + atLineStart = true; + continue; + } + + uint16_t ch = TextUtils_decodeUtf8(suffix, suffixLen, &pos); + FontGlyph* glyph = TextUtils_findGlyph(font, ch); + if (glyph == NULL) { + prevCodepoint = 0; + atLineStart = false; + continue; + } + + if (!atLineStart && prevCodepoint != 0) { + FontGlyph* prevGlyph = TextUtils_findGlyph(font, prevCodepoint); + if (prevGlyph != NULL) { + cursorX += TextUtils_getKerningOffset(prevGlyph, ch); + } + } + + if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) { + if (layout->glyphCount >= layout->glyphCapacity) { + layout->glyphCapacity = layout->glyphCapacity > 0 ? layout->glyphCapacity * 2 : 16; + layout->glyphs = safeRealloc(layout->glyphs, (size_t) layout->glyphCapacity * sizeof(N3DSCachedTextGlyph)); + } + + N3DSCachedTextGlyph* cachedGlyph = &layout->glyphs[layout->glyphCount++]; + cachedGlyph->localX = cursorX + (float) glyph->offset; + cachedGlyph->localY = cursorY; + cachedGlyph->sourceX = glyph->sourceX; + cachedGlyph->sourceY = glyph->sourceY; + cachedGlyph->sourceWidth = glyph->sourceWidth; + cachedGlyph->sourceHeight = glyph->sourceHeight; + } + + cursorX += (float) glyph->shift; + prevCodepoint = ch; + atLineStart = false; + } + + layout->appendCursorX = cursorX; + layout->appendCursorY = cursorY; + layout->appendPrevCodepoint = prevCodepoint; + layout->appendAtLineStart = atLineStart; +} + +static const N3DSCachedTextLayout* N3DSRenderer_getCachedTextLayout(N3DSRenderer* renderer, Font* font, int32_t fontIndex, const char* text) { + N3DSCachedTextLayout* layout = &renderer->cachedTextLayout; + if (layout->text != NULL && + layout->fontIndex == fontIndex && + layout->drawHalign == renderer->base.drawHalign && + layout->drawValign == renderer->base.drawValign && + strcmp(layout->text, text) == 0) { + return layout; + } + + int32_t len = (int32_t) strlen(text); + bool canAppendSuffix = + layout->text != NULL && + layout->fontIndex == fontIndex && + layout->drawHalign == 0 && + layout->drawValign == 0 && + renderer->base.drawHalign == 0 && + renderer->base.drawValign == 0 && + len >= 0; + + if (canAppendSuffix) { + int32_t cachedLen = (int32_t) strlen(layout->text); + canAppendSuffix = + len >= cachedLen && + memcmp(text, layout->text, (size_t) cachedLen) == 0; + + if (canAppendSuffix) { + if (len + 1 > layout->textCapacity) { + int32_t newCapacity = layout->textCapacity > 0 ? layout->textCapacity : 16; + while (newCapacity < len + 1) newCapacity *= 2; + layout->text = safeRealloc(layout->text, (size_t) newCapacity); + layout->textCapacity = newCapacity; + } + + memcpy(layout->text + cachedLen, text + cachedLen, (size_t) (len - cachedLen + 1)); + N3DSRenderer_appendCachedTextLayoutSuffix(layout, font, text + cachedLen, len - cachedLen); + return layout; + } + } + + N3DSRenderer_freeCachedTextLayout(layout); + layout->fontIndex = fontIndex; + layout->drawHalign = renderer->base.drawHalign; + layout->drawValign = renderer->base.drawValign; + layout->textCapacity = len + 1; + layout->text = safeMalloc((size_t) layout->textCapacity); + memcpy(layout->text, text, (size_t) (len + 1)); + layout->glyphCapacity = len > 0 ? len : 1; + layout->glyphs = safeCalloc((size_t) layout->glyphCapacity, sizeof(N3DSCachedTextGlyph)); + + int32_t lineCount = TextUtils_countLines(text, len); + float totalHeight = (float) lineCount * TextUtils_lineStride(font); + float valignOffset = 0.0f; + if (renderer->base.drawValign == 1) valignOffset = -totalHeight * 0.5f; + else if (renderer->base.drawValign == 2) valignOffset = -totalHeight; + + layout->appendCursorX = 0.0f; + layout->appendCursorY = valignOffset - (float) font->ascenderOffset; + layout->appendPrevCodepoint = 0; + layout->appendAtLineStart = true; + + if (renderer->base.drawHalign == 0 && renderer->base.drawValign == 0) { + layout->appendCursorY = -(float) font->ascenderOffset; + N3DSRenderer_appendCachedTextLayoutSuffix(layout, font, text, len); + return layout; + } + + int32_t lineStart = 0; + while (lineStart <= len) { + int32_t lineEnd = lineStart; + while (lineEnd < len && !TextUtils_isNewlineChar(text[lineEnd])) lineEnd++; + float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineEnd - lineStart); + float cursorX = 0.0f; + if (renderer->base.drawHalign == 1) cursorX -= lineWidth * 0.5f; + else if (renderer->base.drawHalign == 2) cursorX -= lineWidth; + + int32_t pos = lineStart; + while (pos < lineEnd) { + uint16_t ch = TextUtils_decodeUtf8(text, lineEnd, &pos); + FontGlyph* glyph = TextUtils_findGlyph(font, ch); + if (glyph == NULL) continue; + + if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) { + N3DSCachedTextGlyph* cachedGlyph = &layout->glyphs[layout->glyphCount++]; + cachedGlyph->localX = cursorX + (float) glyph->offset; + cachedGlyph->localY = layout->appendCursorY; + cachedGlyph->sourceX = glyph->sourceX; + cachedGlyph->sourceY = glyph->sourceY; + cachedGlyph->sourceWidth = glyph->sourceWidth; + cachedGlyph->sourceHeight = glyph->sourceHeight; + } + + if (pos < lineEnd) { + int32_t previewPos = pos; + uint16_t nextCh = TextUtils_decodeUtf8(text, lineEnd, &previewPos); + cursorX += (float) glyph->shift + TextUtils_getKerningOffset(glyph, nextCh); + } else { + cursorX += (float) glyph->shift; + } + } + + if (lineEnd >= len) break; + lineStart = TextUtils_skipNewline(text, lineEnd, len); + layout->appendCursorY += TextUtils_lineStride(font); + } + + return layout; +} + +static bool N3DSRenderer_tryDrawSingleGlyphTextFast( + Renderer* base, + N3DSRenderer* renderer, + Font* font, + const char* text, + int32_t len, + float x, + float y, + float effectiveXScale, + float effectiveYScale, + float angleDeg, + bool gradient, + int32_t c1, + float alpha, + bool useDirectFontAsset, + const C2D_Image* directFontImage, + const Tex3DS_SubTexture* directFontBaseSubtex, + const N3DSAtlasFragment* fontFragment, + N3DSLoadedAtlasPage* fontPage +) { + if (text == NULL || len <= 0 || len > 4) return false; + if (TextUtils_isNewlineChar(text[0])) return false; + + int32_t pos = 0; + uint16_t ch = TextUtils_decodeUtf8(text, len, &pos); + if (pos != len) return false; + + float cursorX = 0.0f; + float lineWidth = TextUtils_measureLineWidth(font, text, len); + if (base->drawHalign == 1) cursorX -= lineWidth * 0.5f; + else if (base->drawHalign == 2) cursorX -= lineWidth; + + float cursorY = 0.0f; + float totalHeight = TextUtils_lineStride(font); + if (base->drawValign == 1) cursorY -= totalHeight * 0.5f; + else if (base->drawValign == 2) cursorY -= totalHeight; + cursorY -= (float) font->ascenderOffset; + + FontGlyph* glyph = TextUtils_findGlyph(font, ch); + if (glyph == NULL || glyph->sourceWidth == 0 || glyph->sourceHeight == 0) { + return true; + } + + Tex3DS_SubTexture subtex; + C2D_Image image; + if (useDirectFontAsset) { + if (directFontImage == NULL || directFontBaseSubtex == NULL) return false; + subtex.width = glyph->sourceWidth; + subtex.height = glyph->sourceHeight; + float baseLeft = directFontBaseSubtex->left; + float baseRight = directFontBaseSubtex->right; + float baseTop = directFontBaseSubtex->top; + float baseBottom = directFontBaseSubtex->bottom; + float baseWidth = (float) directFontBaseSubtex->width; + float baseHeight = (float) directFontBaseSubtex->height; + subtex.left = baseLeft + (baseRight - baseLeft) * ((float) glyph->sourceX / baseWidth); + subtex.right = baseLeft + (baseRight - baseLeft) * (((float) glyph->sourceX + (float) glyph->sourceWidth) / baseWidth); + subtex.top = baseTop + (baseBottom - baseTop) * ((float) glyph->sourceY / baseHeight); + subtex.bottom = baseTop + (baseBottom - baseTop) * (((float) glyph->sourceY + (float) glyph->sourceHeight) / baseHeight); + image.tex = directFontImage->tex; + image.subtex = &subtex; + } else { + if (fontFragment == NULL || fontPage == NULL) return false; + image.tex = &fontPage->texture; + image.subtex = &subtex; + uint16_t px = (uint16_t) (fontFragment->x + glyph->sourceX); + uint16_t py = (uint16_t) (fontFragment->y + glyph->sourceY); + N3DSRenderer_fillSubTexture(&subtex, fontPage, px, py, glyph->sourceWidth, glyph->sourceHeight); + } + + float drawX = x + (cursorX + (float) glyph->offset) * effectiveXScale; + float drawY = y + cursorY * effectiveYScale; + renderer->frameTextGlyphDraws++; + if (fabsf(angleDeg) < 0.001f) { + N3DSRenderer_drawImageFast(base, &image, drawX, drawY, effectiveXScale, effectiveYScale, gradient ? (uint32_t) c1 : base->drawColor, alpha); + } else { + N3DSRenderer_drawImage( + base, + &image, + drawX, + drawY, + (float) glyph->sourceWidth * effectiveXScale, + (float) glyph->sourceHeight * effectiveYScale, + 0.0f, + 0.0f, + angleDeg, + gradient ? (uint32_t) c1 : base->drawColor, + alpha + ); + } + + return true; +} + +static void N3DSRenderer_preloadFontPages(N3DSRenderer* renderer) { + DataWin* dw = renderer->base.dataWin; + if (dw == NULL) return; + + repeat(dw->font.count, i) { + Font* font = &dw->font.fonts[i]; + if (font->isSpriteFont || font->tpagIndex < 0) continue; + + uint32_t pageIndex = 0; + if (!N3DSRenderer_getPageIndexForTPAG(renderer, font->tpagIndex, &pageIndex)) continue; + if (!N3DSRenderer_ensurePageLoaded(renderer, pageIndex)) continue; + renderer->atlasPages[pageIndex].pinned = true; + } +} + +static void N3DSRenderer_prewarmRoomBlobCache(N3DSRenderer* renderer, Runner* runner) { + if (renderer == NULL || runner == NULL || runner->currentRoom == NULL || renderer->atlasPageCount == 0) return; + + DataWin* dw = renderer->base.dataWin; + Room* room = runner->currentRoom; + uint32_t remainingBlobBytes = renderer->isNew3DS ? N3DS_PREWARM_BLOB_BYTES_NEW3DS : N3DS_PREWARM_BLOB_BYTES_OLD3DS; + bool* seenPages = safeCalloc(renderer->atlasPageCount, sizeof(bool)); + uint32_t roomIndex = UINT32_MAX; + + if (dw != NULL && dw->room.rooms != NULL && room >= dw->room.rooms && room < (dw->room.rooms + dw->room.count)) { + roomIndex = (uint32_t) (room - dw->room.rooms); + } + + if (roomIndex != UINT32_MAX) { + N3DSRenderer_prewarmRoomManifestBlobCache(renderer, roomIndex, seenPages, &remainingBlobBytes); + if (remainingBlobBytes == 0) goto done; + } + + repeat(8, i) { + RuntimeBackground* bg = &runner->backgrounds[i]; + if (!bg->visible || bg->backgroundIndex < 0) continue; + N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, Renderer_resolveBackgroundTPAGIndex(dw, bg->backgroundIndex), &remainingBlobBytes); + if (remainingBlobBytes == 0) goto done; + } + + repeat(room->layerCount, layerIndex) { + RoomLayer* layer = &room->layers[layerIndex]; + + if (layer->backgroundData != NULL && layer->backgroundData->visible && layer->backgroundData->spriteIndex >= 0) { + N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, Renderer_resolveSpriteTPAGIndex(dw, layer->backgroundData->spriteIndex), &remainingBlobBytes); + if (remainingBlobBytes == 0) goto done; + } + + if (layer->assetsData != NULL) { + repeat(layer->assetsData->legacyTileCount, tileIndex) { + N3DSTileAtlasEntry* entry = N3DSRenderer_findTileEntry(renderer, &layer->assetsData->legacyTiles[tileIndex]); + N3DSRenderer_prewarmTileEntryBlobOnly(renderer, seenPages, entry, &remainingBlobBytes); + if (remainingBlobBytes == 0) goto done; + } + + repeat(layer->assetsData->spriteCount, spriteIndex) { + SpriteInstance* sprite = &layer->assetsData->sprites[spriteIndex]; + int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, sprite->spriteIndex, (int32_t) sprite->frameIndex); + N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, tpagIndex, &remainingBlobBytes); + if (remainingBlobBytes == 0) goto done; + } + } + } + + repeat(room->tileCount, tileIndex) { + N3DSTileAtlasEntry* entry = N3DSRenderer_findTileEntry(renderer, &room->tiles[tileIndex]); + N3DSRenderer_prewarmTileEntryBlobOnly(renderer, seenPages, entry, &remainingBlobBytes); + if (remainingBlobBytes == 0) goto done; + } + + if (runner->tileLayerCaches != NULL) { + repeat(runner->tileLayerCacheCount, cacheIndex) { + TileLayerRenderCache* cache = &runner->tileLayerCaches[cacheIndex]; + if (!cache->built || cache->cells == NULL) continue; + + repeat(arrlen(cache->cells), cellIndex) { + const TileLayerCacheCell* cell = &cache->cells[cellIndex]; + if (cell->n3dsTileEntryIndex >= 0 && (uint32_t) cell->n3dsTileEntryIndex < renderer->tileEntryCount) { + N3DSRenderer_prewarmTileEntryBlobOnly(renderer, seenPages, &renderer->tileEntries[cell->n3dsTileEntryIndex], &remainingBlobBytes); + } else { + N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, cell->tpagIndex, &remainingBlobBytes); + } + if (remainingBlobBytes == 0) goto done; + } + } + } + + repeat(arrlen(runner->instances), instanceIndex) { + Instance* inst = runner->instances[instanceIndex]; + if (inst == NULL || inst->destroyed || !inst->visible || inst->spriteIndex < 0) continue; + int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, inst->spriteIndex, (int32_t) inst->imageIndex); + N3DSRenderer_prewarmTPAGBlobOnly(renderer, seenPages, tpagIndex, &remainingBlobBytes); + if (remainingBlobBytes == 0) goto done; + } + +done: + free(seenPages); +} + +static void N3DSRenderer_prewarmRoom(Renderer* base, Runner* runner) { + if (base == NULL || runner == NULL || runner->currentRoom == NULL) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + Room* room = runner->currentRoom; + bool roomChanged = renderer->lastPrewarmedRoom != room; + + N3DSRenderer_clearDirectAssetPins(renderer); + if (roomChanged) { + N3DSRenderer_flushRoomDirectAssets(renderer); + } + if (roomChanged && !renderer->isNew3DS) { + renderer->pendingOld3DSAtlasFlush = true; + } + renderer->lastPrewarmedRoom = room; + N3DSRenderer_prewarmRoomBlobCache(renderer, runner); +} + +static void N3DSRenderer_computeFrameLayout(N3DSRenderer* renderer, int32_t gameW, int32_t gameH) { + N3DSRenderer_computeFrameLayoutForTarget(renderer, gameW, gameH, N3DS_TOP_WIDTH, N3DS_TOP_HEIGHT); +} + +static void N3DSRenderer_init(Renderer* base, DataWin* dataWin) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + bool isNew3DS = false; + base->dataWin = dataWin; + renderer->baseTPAGCount = dataWin != NULL ? dataWin->tpag.count : 0u; + renderer->topTarget = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT); +#ifndef N3DS_DISABLE_BOTTOM_SCREEN + renderer->bottomTarget = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT); +#endif + renderer->blendEnabled = true; + renderer->blendEquation = bm_normal; + renderer->blendSrcFactor = bm_src_alpha; + renderer->blendDstFactor = bm_inv_src_alpha; + renderer->clearColor = 0x000000; + renderer->clearAlpha = 1.0f; + if (R_SUCCEEDED(APT_CheckNew3DS(&isNew3DS))) { + renderer->isNew3DS = isNew3DS; + } +#if N3DS_FORCE_OLD3DS_MODE + renderer->isNew3DS = false; +#endif + renderer->residentAtlasPageLimit = renderer->isNew3DS ? N3DS_MAX_RESIDENT_ATLAS_PAGES_NEW3DS : N3DS_MAX_RESIDENT_ATLAS_PAGES_OLD3DS; + renderer->residentAtlasVRAMLimitBytes = renderer->isNew3DS ? N3DS_RESIDENT_ATLAS_VRAM_BUDGET_NEW3DS : N3DS_RESIDENT_ATLAS_VRAM_BUDGET_OLD3DS; + renderer->residentDirectAssetVRAMLimitBytes = renderer->isNew3DS ? N3DS_DIRECT_ASSET_VRAM_BUDGET_NEW3DS : N3DS_DIRECT_ASSET_VRAM_BUDGET_OLD3DS; + renderer->prewarmGpuPageBudget = renderer->isNew3DS ? N3DS_PREWARM_GPU_PAGE_BUDGET_NEW3DS : N3DS_PREWARM_GPU_PAGE_BUDGET_OLD3DS; + renderer->cachedT3xByteLimit = renderer->isNew3DS ? N3DS_MAX_CACHED_T3X_BYTES_NEW3DS : N3DS_MAX_CACHED_T3X_BYTES_OLD3DS; + renderer->cachedDirectT3xByteLimit = renderer->isNew3DS ? N3DS_MAX_CACHED_DIRECT_T3X_BYTES_NEW3DS : N3DS_MAX_CACHED_DIRECT_T3X_BYTES_OLD3DS; + renderer->atlasLoaded = N3DSRenderer_loadAtlas(renderer); + if (!renderer->atlasLoaded && renderer->startupError[0] == '\0') { + N3DSRenderer_setStartupError(renderer, "Failed to load 3DS graphics atlas"); + } + N3DSRenderer_buildDirectAssetMaps(renderer); + (void) N3DSRenderer_loadPackedDirectAssets(renderer); + (void) N3DSRenderer_loadRoomManifest(renderer); + renderer->atlasTraceMask = safeCalloc(renderer->atlasItemCount > 0 ? renderer->atlasItemCount : 1u, sizeof(uint8_t)); + renderer->atlasTraceFile = fopen(N3DS_ATLAS_TRACE_LOG_PATH, "wb"); + if (renderer->atlasTraceFile != NULL) { + fprintf(renderer->atlasTraceFile, "N3DS atlas trace start\n"); + fflush(renderer->atlasTraceFile); + } + N3DSRenderer_preloadFontPages(renderer); + C2D_SetTintMode(C2D_TintMult); + fprintf( + stderr, + "N3DSRenderer: model=%s residentLimit=%lu pages residentVRAM=%luKB directVRAM=%luKB prewarm=%lu cacheLimit=%luKB\n", + renderer->isNew3DS ? "new3ds" : "old3ds", + (unsigned long) renderer->residentAtlasPageLimit, + (unsigned long) (renderer->residentAtlasVRAMLimitBytes / 1024u), + (unsigned long) (renderer->residentDirectAssetVRAMLimitBytes / 1024u), + (unsigned long) renderer->prewarmGpuPageBudget, + (unsigned long) (renderer->cachedT3xByteLimit / 1024u) + ); +} + +static void N3DSRenderer_destroy(Renderer* base) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + repeat(renderer->atlasPageCount, i) { + N3DSRenderer_unloadPage(renderer, (uint32_t) i); + N3DSRenderer_unloadPageBlob(renderer, (uint32_t) i); + } + N3DSRenderer_freeCachedTextLayout(&renderer->cachedTextLayout); + repeat(shlen(renderer->resolvedAssetPathCache), i) { + free(renderer->resolvedAssetPathCache[i].value); + } + shfree(renderer->resolvedAssetPathCache); + shfree(renderer->packedDirectAssetMap); + free(renderer->roomManifestEntries); + free(renderer->roomManifestPageRefs); + free(renderer->atlasTraceMask); + if (renderer->atlasTraceFile != NULL) fclose(renderer->atlasTraceFile); + if (renderer->packedAtlasFile != NULL) fclose(renderer->packedAtlasFile); + if (renderer->packedDirectAssetFile != NULL) fclose(renderer->packedDirectAssetFile); + repeat(renderer->directSpriteAssetCount, spriteIndex) { + N3DSDirectSpriteAsset* spriteAsset = &renderer->directSpriteAssets[spriteIndex]; + N3DSRenderer_freeDirectTextureAsset(&spriteAsset->sheetAsset, renderer); + N3DSRenderer_unloadDirectTextureBlob(&spriteAsset->sheetAsset, renderer); + free(spriteAsset->sheetFrameImages); + spriteAsset->sheetFrameImages = NULL; + repeat(spriteAsset->frameCount, frameIndex) { + N3DSRenderer_freeDirectTextureAsset(&spriteAsset->frameAssets[frameIndex], renderer); + N3DSRenderer_unloadDirectTextureBlob(&spriteAsset->frameAssets[frameIndex], renderer); + } + free(spriteAsset->frameAssets); + } + repeat(renderer->directBackgroundAssetCount, bgIndex) { + N3DSRenderer_freeDirectTextureAsset(&renderer->directBackgroundAssets[bgIndex], renderer); + N3DSRenderer_unloadDirectTextureBlob(&renderer->directBackgroundAssets[bgIndex], renderer); + } + repeat(renderer->directFontAssetCount, fontIndex) { + N3DSRenderer_freeDirectTextureAsset(&renderer->directFontAssets[fontIndex], renderer); + N3DSRenderer_unloadDirectTextureBlob(&renderer->directFontAssets[fontIndex], renderer); + } + free(renderer->directSpriteAssets); + free(renderer->directBackgroundAssets); + free(renderer->directFontAssets); + if (renderer->dynamicCaptureTPAGs != NULL) { + repeat(renderer->dynamicCaptureTPAGCount, i) { + N3DSRenderer_freeDynamicCaptureTPAG(renderer, &renderer->dynamicCaptureTPAGs[i]); + } + free(renderer->dynamicCaptureTPAGs); + } + free(renderer->tpagToSpriteIndex); + free(renderer->tpagToSpriteFrameIndex); + free(renderer->tpagToBackgroundIndex); + free(renderer->atlasPages); + free(renderer->atlasItems); + free(renderer->atlasItemsV3); + free(renderer->atlasFragments); + free(renderer->tileEntries); + hmfree(renderer->tileEntryMap); + if (renderer->tileLayerChunkCaches != NULL) { + size_t chunkCacheCount = arrlenu(renderer->tileLayerChunkCaches); + repeat(chunkCacheCount, i) { + N3DSRenderer_freeTileLayerChunkCache(renderer, &renderer->tileLayerChunkCaches[i]); + } + arrfree(renderer->tileLayerChunkCaches); + renderer->tileLayerChunkCaches = NULL; + } + if (renderer->bottomTarget != NULL) { + C3D_RenderTargetDelete(renderer->bottomTarget); + } + if (renderer->topTarget != NULL) { + C3D_RenderTargetDelete(renderer->topTarget); + } + free(renderer); +} + +static void N3DSRenderer_beginFrame(Renderer* base, int32_t gameW, int32_t gameH, MAYBE_UNUSED int32_t windowW, MAYBE_UNUSED int32_t windowH) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + renderer->frameSequence++; + renderer->activeSceneTarget = N3DS_SCENE_TARGET_NONE; + N3DSRenderer_resetFramePerfCounters(renderer); + N3DSRenderer_computeFrameLayout(renderer, gameW, gameH); + + if (renderer->pendingOld3DSAtlasFlush && !renderer->isNew3DS) { + renderer->pendingOld3DSAtlasFlush = false; + N3DSRenderer_clearAtlasPagePins(renderer); + N3DSRenderer_preloadFontPages(renderer); + N3DSRenderer_flushRoomAtlasResidencyOld3DS(renderer); + } + + N3DSRenderer_setDefaultGPUState(renderer); + if (renderer->bottomTarget != NULL) { + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_BOTTOM, true); + C2D_TargetClear(renderer->bottomTarget, C2D_Color32(0, 0, 0, 255)); + } + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, true); + C2D_TargetClear(renderer->topTarget, N3DSRenderer_makeColor(renderer->clearColor, renderer->clearAlpha)); +} + +static void N3DSRenderer_endFrame(MAYBE_UNUSED Renderer* base) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + N3DSRenderer_logPerfWindowIfNeeded(renderer); + N3DSRenderer_flushC2DQueue(renderer); + C2D_Flush(); + renderer->pendingC2DDraws = 0; + renderer->lastDrawTexture = NULL; +} + +void N3DSRenderer_beginOverlay(Renderer* base) { + if (base == NULL) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + N3DSRenderer_flushC2DQueue(renderer); + N3DSRenderer_setDefaultGPUState(renderer); + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false); +} + +void N3DSRenderer_beginBottomScreenGUIEx(Renderer* base, int32_t guiW, int32_t guiH, float scaleX, float scaleY, float offsetX, float offsetY) { +#ifdef N3DS_DISABLE_BOTTOM_SCREEN + (void) base; + (void) guiW; + (void) guiH; + (void) scaleX; + (void) scaleY; + (void) offsetX; + (void) offsetY; + return; +#endif + if (base == NULL) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (renderer->bottomTarget == NULL || renderer->bottomScreenGuiActive) return; + + renderer->savedFrameScaleX = renderer->frameScaleX; + renderer->savedFrameScaleY = renderer->frameScaleY; + renderer->savedFrameOffsetX = renderer->frameOffsetX; + renderer->savedFrameOffsetY = renderer->frameOffsetY; + renderer->savedPortOffsetX = renderer->portOffsetX; + renderer->savedPortOffsetY = renderer->portOffsetY; + renderer->savedViewX = renderer->viewX; + renderer->savedViewY = renderer->viewY; + renderer->savedViewScaleX = renderer->viewScaleX; + renderer->savedViewScaleY = renderer->viewScaleY; + + N3DSRenderer_computeFrameLayoutForTarget(renderer, guiW, guiH, N3DS_BOTTOM_WIDTH, N3DS_BOTTOM_HEIGHT); + renderer->viewX = 0; + renderer->viewY = 0; + renderer->viewScaleX = renderer->frameScaleX * scaleX; + renderer->viewScaleY = renderer->frameScaleY * scaleY; + renderer->portOffsetX = ((float) guiW * (renderer->frameScaleX - renderer->viewScaleX) * 0.5f) + offsetX; + renderer->portOffsetY = ((float) guiH * (renderer->frameScaleY - renderer->viewScaleY) * 0.5f) + offsetY; + renderer->bottomScreenGuiActive = true; + + N3DSRenderer_flushC2DQueue(renderer); + N3DSRenderer_setDefaultGPUState(renderer); + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_BOTTOM, false); +} + +void N3DSRenderer_beginBottomScreenGUI(Renderer* base, int32_t guiW, int32_t guiH) { + N3DSRenderer_beginBottomScreenGUIEx(base, guiW, guiH, 1.0f, 1.0f, 0.0f, 0.0f); +} + +void N3DSRenderer_beginBottomScreenGUIView(Renderer* base, int32_t guiW, int32_t guiH, int32_t viewX, int32_t viewY) { + N3DSRenderer_beginBottomScreenGUIEx(base, guiW, guiH, 1.0f, 1.0f, 0.0f, 0.0f); + N3DSRenderer* renderer = (N3DSRenderer*) base; + renderer->viewX = viewX; + renderer->viewY = viewY; +} + +void N3DSRenderer_endBottomScreenGUI(Renderer* base) { + if (base == NULL) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (!renderer->bottomScreenGuiActive) return; + + renderer->frameScaleX = renderer->savedFrameScaleX; + renderer->frameScaleY = renderer->savedFrameScaleY; + renderer->frameOffsetX = renderer->savedFrameOffsetX; + renderer->frameOffsetY = renderer->savedFrameOffsetY; + renderer->portOffsetX = renderer->savedPortOffsetX; + renderer->portOffsetY = renderer->savedPortOffsetY; + renderer->viewX = renderer->savedViewX; + renderer->viewY = renderer->savedViewY; + renderer->viewScaleX = renderer->savedViewScaleX; + renderer->viewScaleY = renderer->savedViewScaleY; + renderer->bottomScreenGuiActive = false; + + N3DSRenderer_flushC2DQueue(renderer); + N3DSRenderer_applyBlendState(renderer); + N3DSRenderer_applyAlphaState(renderer); + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false); +} + +void N3DSRenderer_beginBottomScreenGUI2x(Renderer* base, int32_t guiW, int32_t guiH) { + N3DSRenderer_beginBottomScreenGUIEx(base, guiW, guiH, 2.0f, 2.0f, 0.0f, 0.0f); +} + +void N3DSRenderer_endBottomScreenGUI2x(Renderer* base) { + N3DSRenderer_endBottomScreenGUI(base); +} + +void N3DSRenderer_beginTopScreenGUI(Renderer* base, int32_t guiW, int32_t guiH) { + if (base == NULL) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (renderer->topScreenGuiActive || renderer->bottomScreenGuiActive) return; + + renderer->savedFrameScaleX = renderer->frameScaleX; + renderer->savedFrameScaleY = renderer->frameScaleY; + renderer->savedFrameOffsetX = renderer->frameOffsetX; + renderer->savedFrameOffsetY = renderer->frameOffsetY; + renderer->savedPortOffsetX = renderer->portOffsetX; + renderer->savedPortOffsetY = renderer->portOffsetY; + renderer->savedViewX = renderer->viewX; + renderer->savedViewY = renderer->viewY; + renderer->savedViewScaleX = renderer->viewScaleX; + renderer->savedViewScaleY = renderer->viewScaleY; + + N3DSRenderer_setTopBattle320x240Layout(renderer, guiW, guiH, N3DS_TOP_BATTLE_SCENE_Y_OFFSET); + renderer->topScreenGuiActive = true; + renderer->topScreenGui2xActive = false; + + N3DSRenderer_flushC2DQueue(renderer); + N3DSRenderer_setDefaultGPUState(renderer); + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false); +} + +void N3DSRenderer_endTopScreenGUI(Renderer* base) { + if (base == NULL) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (!renderer->topScreenGuiActive) return; + + renderer->frameScaleX = renderer->savedFrameScaleX; + renderer->frameScaleY = renderer->savedFrameScaleY; + renderer->frameOffsetX = renderer->savedFrameOffsetX; + renderer->frameOffsetY = renderer->savedFrameOffsetY; + renderer->portOffsetX = renderer->savedPortOffsetX; + renderer->portOffsetY = renderer->savedPortOffsetY; + renderer->viewX = renderer->savedViewX; + renderer->viewY = renderer->savedViewY; + renderer->viewScaleX = renderer->savedViewScaleX; + renderer->viewScaleY = renderer->savedViewScaleY; + renderer->topScreenGuiActive = false; + renderer->topScreenGui2xActive = false; + + N3DSRenderer_flushC2DQueue(renderer); + N3DSRenderer_applyBlendState(renderer); + N3DSRenderer_applyAlphaState(renderer); + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false); +} + +void N3DSRenderer_beginTopScreenGUI2x(Renderer* base, int32_t guiW, int32_t guiH) { + if (base == NULL) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (renderer->topScreenGuiActive || renderer->bottomScreenGuiActive) return; + + renderer->savedFrameScaleX = renderer->frameScaleX; + renderer->savedFrameScaleY = renderer->frameScaleY; + renderer->savedFrameOffsetX = renderer->frameOffsetX; + renderer->savedFrameOffsetY = renderer->frameOffsetY; + renderer->savedPortOffsetX = renderer->portOffsetX; + renderer->savedPortOffsetY = renderer->portOffsetY; + renderer->savedViewX = renderer->viewX; + renderer->savedViewY = renderer->viewY; + renderer->savedViewScaleX = renderer->viewScaleX; + renderer->savedViewScaleY = renderer->viewScaleY; + + N3DSRenderer_setTopBattle320x240Layout(renderer, guiW, guiH, N3DS_TOP_BATTLE_ENEMY_Y_OFFSET); + renderer->viewScaleX *= 2.0f; + renderer->viewScaleY *= 2.0f; + renderer->topScreenGuiActive = true; + renderer->topScreenGui2xActive = true; + + N3DSRenderer_flushC2DQueue(renderer); + N3DSRenderer_setDefaultGPUState(renderer); + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false); +} + +void N3DSRenderer_endTopScreenGUI2x(Renderer* base) { + if (base == NULL) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (!renderer->topScreenGuiActive) return; + + renderer->frameScaleX = renderer->savedFrameScaleX; + renderer->frameScaleY = renderer->savedFrameScaleY; + renderer->frameOffsetX = renderer->savedFrameOffsetX; + renderer->frameOffsetY = renderer->savedFrameOffsetY; + renderer->portOffsetX = renderer->savedPortOffsetX; + renderer->portOffsetY = renderer->savedPortOffsetY; + renderer->viewX = renderer->savedViewX; + renderer->viewY = renderer->savedViewY; + renderer->viewScaleX = renderer->savedViewScaleX; + renderer->viewScaleY = renderer->savedViewScaleY; + renderer->topScreenGuiActive = false; + renderer->topScreenGui2xActive = false; + + N3DSRenderer_flushC2DQueue(renderer); + N3DSRenderer_applyBlendState(renderer); + N3DSRenderer_applyAlphaState(renderer); + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false); +} + +bool N3DSRenderer_isTopScreenGUIActive(Renderer* base) { + if (base == NULL) return false; + return ((N3DSRenderer*) base)->topScreenGuiActive; +} + +bool N3DSRenderer_isTopScreenBattleViewActive(Renderer* base) { + if (base == NULL) return false; + return ((N3DSRenderer*) base)->topScreenBattleViewActive; +} + +void N3DSRenderer_setTopScreenBattleViewActive(Renderer* base, bool active) { + if (base == NULL) return; + ((N3DSRenderer*) base)->topScreenBattleViewActive = active; +} + +static void N3DSRenderer_beginView(Renderer* base, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, int32_t portX, int32_t portY, int32_t portW, int32_t portH, MAYBE_UNUSED float viewAngle) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + renderer->viewX = viewX; + renderer->viewY = viewY; + renderer->portOffsetX = (float) portX * renderer->frameScaleX; + renderer->portOffsetY = (float) portY * renderer->frameScaleY; + renderer->viewScaleX = viewW > 0 ? ((float) portW / (float) viewW) * renderer->frameScaleX : renderer->frameScaleX; + renderer->viewScaleY = viewH > 0 ? ((float) portH / (float) viewH) * renderer->frameScaleY : renderer->frameScaleY; + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, false); +} + +static void N3DSRenderer_endView(MAYBE_UNUSED Renderer* base) {} + +static void N3DSRenderer_beginGUI(Renderer* base, int32_t guiW, int32_t guiH, int32_t portX, int32_t portY, int32_t portW, int32_t portH) { + N3DSRenderer_beginView(base, 0, 0, guiW, guiH, portX, portY, portW, portH, 0.0f); +} + +static void N3DSRenderer_endGUI(Renderer* base) { + N3DSRenderer_endView(base); +} + +static void N3DSRenderer_drawImage(Renderer* base, C2D_Image* image, float localX, float localY, float width, float height, float pivotX, float pivotY, float angleDeg, uint32_t color, float alpha) { + if (image == NULL || image->tex == NULL || image->subtex == NULL) return; + if (!Renderer_isFiniteFloat(localX) || !Renderer_isFiniteFloat(localY) || + !Renderer_isFiniteFloat(width) || !Renderer_isFiniteFloat(height) || + !Renderer_isFiniteFloat(pivotX) || !Renderer_isFiniteFloat(pivotY) || + !Renderer_isFiniteFloat(angleDeg) || !Renderer_isFiniteFloat(alpha) || + width == 0.0f || height == 0.0f || + image->subtex->width == 0.0f || image->subtex->height == 0.0f) { + return; + } + + bool flipX = false; + bool flipY = false; + if (width < 0.0f) { + localX += width; + pivotX -= width; + width = -width; + flipX = true; + } + if (height < 0.0f) { + localY += height; + pivotY -= height; + height = -height; + flipY = true; + } + + Tex3DS_SubTexture flippedSubtex; + C2D_Image drawImage = *image; + if (flipX || flipY) { + flippedSubtex = *image->subtex; + if (flipX) { + float tmp = flippedSubtex.left; + flippedSubtex.left = flippedSubtex.right; + flippedSubtex.right = tmp; + } + if (flipY) { + float tmp = flippedSubtex.top; + flippedSubtex.top = flippedSubtex.bottom; + flippedSubtex.bottom = tmp; + } + drawImage.subtex = &flippedSubtex; + } + + if (fabsf(angleDeg) < 0.001f && fabsf(pivotX) < 0.001f && fabsf(pivotY) < 0.001f) { + float xscale = width / (float) drawImage.subtex->width; + float yscale = height / (float) drawImage.subtex->height; + N3DSRenderer_drawImageFast(base, &drawImage, localX, localY, xscale, yscale, color, alpha); + return; + } + + N3DSRenderer* renderer = (N3DSRenderer*) base; + float screenRectX = 0.0f; + float screenRectY = 0.0f; + float screenRectW = 0.0f; + float screenRectH = 0.0f; + N3DSRenderer_transformScreenRect(renderer, localX, localY, width, height, &screenRectX, &screenRectY, &screenRectW, &screenRectH); + if (screenRectW <= 0.0f || screenRectH <= 0.0f) return; + if (N3DSRenderer_isScreenRotatedRectOffscreen(renderer, screenRectX, screenRectY, screenRectW, screenRectH, angleDeg)) { + return; + } + + float pivotLocalX = localX + pivotX; + float pivotLocalY = localY + pivotY; + float screenX = N3DSRenderer_transformScreenX(renderer, pivotLocalX); + float screenY = N3DSRenderer_transformScreenY(renderer, pivotLocalY); + float screenScaleX = screenRectW / (float) drawImage.subtex->width; + float screenScaleY = screenRectH / (float) drawImage.subtex->height; + float screenPivotX = (width != 0.0f) ? (pivotX / width) * screenRectW : 0.0f; + float screenPivotY = (height != 0.0f) ? (pivotY / height) * screenRectH : 0.0f; + + C2D_DrawParams params = { + .pos = { screenX, screenY, screenRectW, screenRectH }, + .center = { screenPivotX, screenPivotY }, + .depth = 0.5f, + .angle = C3D_AngleFromDegrees(-angleDeg), + }; + N3DSRenderer_trackTextureUse(renderer, &drawImage); + N3DSRenderer_applyTextureFilterForScale(renderer, drawImage.tex, screenScaleX, screenScaleY); + if (N3DSRenderer_isIdentityTint(color, alpha)) { + C2D_DrawImage(drawImage, ¶ms, NULL); + } else { + C2D_ImageTint tint; + C2D_PlainImageTint(&tint, N3DSRenderer_makeColor(color, alpha), 1.0f); + C2D_DrawImage(drawImage, ¶ms, &tint); + } + N3DSRenderer_noteC2DDraws(renderer, 1u); +} + +static bool N3DSRenderer_tryResolveSingleFragmentFontPage(N3DSRenderer* renderer, int32_t tpagIndex, const N3DSAtlasFragment** outFragment, N3DSLoadedAtlasPage** outPage) { + if (!N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) return false; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return false; + + N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex]; + if (item->fragmentCount != 1) return false; + + const N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart]; + if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) return false; + if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) return false; + + N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId]; + if (!page->ready) return false; + + *outFragment = fragment; + *outPage = page; + return true; +} + +static void N3DSRenderer_drawSprite(Renderer* base, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) { + if (!Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) || + !Renderer_isFiniteFloat(originX) || !Renderer_isFiniteFloat(originY) || + !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) || + !Renderer_isFiniteFloat(angleDeg) || !Renderer_isFiniteFloat(alpha) || + xscale == 0.0f || yscale == 0.0f) { + return; + } + + DataWin* dw = base->dataWin; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dw->tpag.count) return; + TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + N3DSDynamicCaptureTPAG* dynamicCapture = N3DSRenderer_getDynamicCaptureTPAG(renderer, tpagIndex); + N3DSRenderer_traceTPAGUsage(renderer, N3DS_TRACE_KIND_SPRITE, tpagIndex); + renderer->frameSpriteDrawCalls++; + if (dynamicCapture != NULL) { + float localX = ((float) tpag->targetX - originX) * xscale; + float localY = ((float) tpag->targetY - originY) * yscale; + float drawX = x + localX; + float drawY = y + localY; + N3DSRenderer_drawImage( + base, + &dynamicCapture->image, + drawX, + drawY, + (float) tpag->sourceWidth * xscale, + (float) tpag->sourceHeight * yscale, + -localX, + -localY, + angleDeg, + color, + alpha + ); + return; + } + if (N3DSRenderer_tryDrawDirectMappedSprite(base, tpagIndex, x, y, originX, originY, xscale, yscale, angleDeg, color, alpha)) { + return; + } + if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) { + if ((uint32_t) tpagIndex >= renderer->atlasItemCount) return; + N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex]; + repeat(item->fragmentCount, fragIndex) { + N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart + fragIndex]; + C2D_Image image; + Tex3DS_SubTexture subtex; + if (!N3DSRenderer_resolveFragmentImage(renderer, fragment, &image, &subtex)) continue; + renderer->frameFragmentDraws++; + float localX = ((float) tpag->targetX + (float) fragment->sourceX - originX) * xscale; + float localY = ((float) tpag->targetY + (float) fragment->sourceY - originY) * yscale; + float drawX = x + localX; + float drawY = y + localY; + N3DSRenderer_drawImage( + base, + &image, + drawX, + drawY, + (float) fragment->width * xscale, + (float) fragment->height * yscale, + -localX, + -localY, + angleDeg, + color, + alpha + ); + } + return; + } + + C2D_Image image; + Tex3DS_SubTexture subtex; + if (!N3DSRenderer_resolveImage(renderer, tpagIndex, &image, &subtex)) return; + + float localX = ((float) tpag->targetX - originX) * xscale; + float localY = ((float) tpag->targetY - originY) * yscale; + float drawX = x + localX; + float drawY = y + localY; + N3DSRenderer_drawImage( + base, + &image, + drawX, + drawY, + (float) tpag->sourceWidth * xscale, + (float) tpag->sourceHeight * yscale, + -localX, + -localY, + angleDeg, + color, + alpha + ); +} + +static void N3DSRenderer_drawSpritePart(Renderer* base, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { + if (srcW <= 0 || srcH <= 0) return; + if (!Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) || + !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) || + !Renderer_isFiniteFloat(angleDeg) || !Renderer_isFiniteFloat(pivotX) || + !Renderer_isFiniteFloat(pivotY) || !Renderer_isFiniteFloat(alpha) || + xscale == 0.0f || yscale == 0.0f) { + return; + } + + N3DSRenderer* renderer = (N3DSRenderer*) base; + N3DSDynamicCaptureTPAG* dynamicCapture = N3DSRenderer_getDynamicCaptureTPAG(renderer, tpagIndex); + N3DSRenderer_traceTPAGUsage(renderer, N3DS_TRACE_KIND_SPRITE_PART, tpagIndex); + renderer->frameSpritePartDrawCalls++; + if (dynamicCapture != NULL) { + DataWin* dw = base->dataWin; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dw->tpag.count) return; + TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; + float logicalW = (float) (tpag->sourceWidth > 0 ? tpag->sourceWidth : dynamicCapture->logicalWidth); + float logicalH = (float) (tpag->sourceHeight > 0 ? tpag->sourceHeight : dynamicCapture->logicalHeight); + float texW = (float) dynamicCapture->subtex.width; + float texH = (float) dynamicCapture->subtex.height; + if (logicalW <= 0.0f || logicalH <= 0.0f || texW <= 0.0f || texH <= 0.0f) return; + + Tex3DS_SubTexture subtex = dynamicCapture->subtex; + float u0 = (float) srcOffX / logicalW; + float v0 = (float) srcOffY / logicalH; + float u1 = (float) (srcOffX + srcW) / logicalW; + float v1 = (float) (srcOffY + srcH) / logicalH; + if (u0 < 0.0f) u0 = 0.0f; + if (v0 < 0.0f) v0 = 0.0f; + if (u1 > 1.0f) u1 = 1.0f; + if (v1 > 1.0f) v1 = 1.0f; + if (u0 >= u1 || v0 >= v1) return; + + float left = dynamicCapture->subtex.left; + float right = dynamicCapture->subtex.right; + float top = dynamicCapture->subtex.top; + float bottom = dynamicCapture->subtex.bottom; + subtex.left = left + (right - left) * u0; + subtex.right = left + (right - left) * u1; + subtex.top = top + (bottom - top) * v0; + subtex.bottom = top + (bottom - top) * v1; + subtex.width = (uint16_t) lroundf((u1 - u0) * texW); + subtex.height = (uint16_t) lroundf((v1 - v0) * texH); + if (subtex.width == 0 || subtex.height == 0) return; + + C2D_Image image = { + .tex = dynamicCapture->image.tex, + .subtex = &subtex, + }; + N3DSRenderer_drawImage( + base, + &image, + x, + y, + (float) srcW * xscale, + (float) srcH * yscale, + pivotX - x, + pivotY - y, + angleDeg, + color, + alpha + ); + return; + } + + C2D_Image* directImage = NULL; + if (renderer->tpagToSpriteIndex != NULL && renderer->tpagToSpriteFrameIndex != NULL && + tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) { + int32_t spriteIndex = renderer->tpagToSpriteIndex[tpagIndex]; + int32_t frameIndex = renderer->tpagToSpriteFrameIndex[tpagIndex]; + if (spriteIndex >= 0 && frameIndex >= 0) { + N3DSRenderer_tryLoadDirectSpriteImage(renderer, spriteIndex, frameIndex, &directImage); + } + } + if (directImage == NULL && renderer->tpagToBackgroundIndex != NULL && + tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) { + int32_t backgroundIndex = renderer->tpagToBackgroundIndex[tpagIndex]; + if (backgroundIndex >= 0) { + N3DSRenderer_tryLoadDirectBackgroundImage(renderer, backgroundIndex, &directImage); + } + } + if (directImage != NULL && directImage->subtex != NULL) { + Tex3DS_SubTexture subtex = *directImage->subtex; + float baseLeft = subtex.left; + float baseRight = subtex.right; + float baseTop = subtex.top; + float baseBottom = subtex.bottom; + float baseWidth = (float) directImage->subtex->width; + float baseHeight = (float) directImage->subtex->height; + if (baseWidth <= 0.0f || baseHeight <= 0.0f) return; + + subtex.width = (uint16_t) srcW; + subtex.height = (uint16_t) srcH; + subtex.left = baseLeft + (baseRight - baseLeft) * ((float) srcOffX / baseWidth); + subtex.right = baseLeft + (baseRight - baseLeft) * (((float) srcOffX + (float) srcW) / baseWidth); + subtex.top = baseTop + (baseBottom - baseTop) * ((float) srcOffY / baseHeight); + subtex.bottom = baseTop + (baseBottom - baseTop) * (((float) srcOffY + (float) srcH) / baseHeight); + + C2D_Image image = { + .tex = directImage->tex, + .subtex = &subtex, + }; + N3DSRenderer_drawImage(base, &image, x, y, (float) srcW * xscale, (float) srcH * yscale, pivotX - x, pivotY - y, angleDeg, color, alpha); + return; + } + if (N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) { + if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return; + N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex]; + int32_t reqLeft = srcOffX; + int32_t reqTop = srcOffY; + int32_t reqRight = srcOffX + srcW; + int32_t reqBottom = srcOffY + srcH; + + repeat(item->fragmentCount, fragIndex) { + N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart + fragIndex]; + int32_t fragLeft = fragment->sourceX; + int32_t fragTop = fragment->sourceY; + int32_t fragRight = fragLeft + fragment->width; + int32_t fragBottom = fragTop + fragment->height; + int32_t clipLeft = reqLeft > fragLeft ? reqLeft : fragLeft; + int32_t clipTop = reqTop > fragTop ? reqTop : fragTop; + int32_t clipRight = reqRight < fragRight ? reqRight : fragRight; + int32_t clipBottom = reqBottom < fragBottom ? reqBottom : fragBottom; + if (clipLeft >= clipRight || clipTop >= clipBottom) continue; + + if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) continue; + if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) continue; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId]; + if (!page->ready) continue; + renderer->frameFragmentDraws++; + + uint16_t px = (uint16_t) (fragment->x + (uint16_t) (clipLeft - fragLeft)); + uint16_t py = (uint16_t) (fragment->y + (uint16_t) (clipTop - fragTop)); + int32_t clipW = clipRight - clipLeft; + int32_t clipH = clipBottom - clipTop; + Tex3DS_SubTexture subtex; + C2D_Image image = { + .tex = &page->texture, + .subtex = &subtex, + }; + N3DSRenderer_fillSubTexture(&subtex, page, px, py, (uint16_t) clipW, (uint16_t) clipH); + float drawX = x + (float) (clipLeft - reqLeft) * xscale; + float drawY = y + (float) (clipTop - reqTop) * yscale; + float localPivotX = pivotX - drawX; + float localPivotY = pivotY - drawY; + N3DSRenderer_drawImage(base, &image, drawX, drawY, (float) clipW * xscale, (float) clipH * yscale, localPivotX, localPivotY, angleDeg, color, alpha); + } + return; + } + + if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return; + N3DSAtlasItem* item = &renderer->atlasItems[tpagIndex]; + if ((uint32_t) item->atlasId >= renderer->atlasPageCount) return; + if (!N3DSRenderer_ensurePageLoaded(renderer, item->atlasId)) return; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[item->atlasId]; + if (!page->ready) return; + + Tex3DS_SubTexture subtex; + C2D_Image image = { + .tex = &page->texture, + .subtex = &subtex, + }; + uint16_t px = (uint16_t) (item->x + srcOffX); + uint16_t py = (uint16_t) (item->y + srcOffY); + N3DSRenderer_fillSubTexture(&subtex, page, px, py, (uint16_t) srcW, (uint16_t) srcH); + N3DSRenderer_drawImage(base, &image, x, y, (float) srcW * xscale, (float) srcH * yscale, pivotX - x, pivotY - y, angleDeg, color, alpha); +} + +static bool N3DSRenderer_tryDrawTileRect( + Renderer* base, + int32_t tpagIndex, + int32_t srcX, + int32_t srcY, + int32_t srcW, + int32_t srcH, + float drawX, + float drawY, + float xscale, + float yscale, + uint32_t color, + float alpha +) { + if (srcW <= 0 || srcH <= 0) return false; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (!N3DSRenderer_isFragmentedAtlasVersion(renderer->atlasVersion)) { + N3DSRenderer_drawSpritePart(base, tpagIndex, srcX, srcY, srcW, srcH, drawX, drawY, xscale, yscale, 0.0f, 0.0f, 0.0f, color, alpha); + return true; + } + + if (tpagIndex < 0 || (uint32_t) tpagIndex >= renderer->atlasItemCount) return false; + N3DSAtlasItemV3* item = &renderer->atlasItemsV3[tpagIndex]; + if (item->fragmentCount == 0) return false; + + int32_t reqLeft = srcX; + int32_t reqTop = srcY; + int32_t reqRight = srcX + srcW; + int32_t reqBottom = srcY + srcH; + bool drewAnything = false; + + if (item->fragmentCount == 1) { + N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart]; + if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) return false; + if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) return false; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId]; + if (!page->ready) return false; + + int32_t fragLeft = fragment->sourceX; + int32_t fragTop = fragment->sourceY; + int32_t fragRight = fragLeft + fragment->width; + int32_t fragBottom = fragTop + fragment->height; + int32_t clipLeft = reqLeft > fragLeft ? reqLeft : fragLeft; + int32_t clipTop = reqTop > fragTop ? reqTop : fragTop; + int32_t clipRight = reqRight < fragRight ? reqRight : fragRight; + int32_t clipBottom = reqBottom < fragBottom ? reqBottom : fragBottom; + if (clipLeft >= clipRight || clipTop >= clipBottom) return false; + + uint16_t px = (uint16_t) (fragment->x + (uint16_t) (clipLeft - fragLeft)); + uint16_t py = (uint16_t) (fragment->y + (uint16_t) (clipTop - fragTop)); + int32_t clipW = clipRight - clipLeft; + int32_t clipH = clipBottom - clipTop; + Tex3DS_SubTexture subtex; + C2D_Image image = { + .tex = &page->texture, + .subtex = &subtex, + }; + N3DSRenderer_fillSubTexture(&subtex, page, px, py, (uint16_t) clipW, (uint16_t) clipH); + renderer->frameFragmentDraws++; + N3DSRenderer_drawImageFast( + base, + &image, + drawX + (float) (clipLeft - reqLeft) * xscale, + drawY + (float) (clipTop - reqTop) * yscale, + xscale, + yscale, + color, + alpha + ); + return true; + } + + repeat(item->fragmentCount, fragIndex) { + N3DSAtlasFragment* fragment = &renderer->atlasFragments[item->fragmentStart + fragIndex]; + int32_t fragLeft = fragment->sourceX; + int32_t fragTop = fragment->sourceY; + int32_t fragRight = fragLeft + fragment->width; + int32_t fragBottom = fragTop + fragment->height; + int32_t clipLeft = reqLeft > fragLeft ? reqLeft : fragLeft; + int32_t clipTop = reqTop > fragTop ? reqTop : fragTop; + int32_t clipRight = reqRight < fragRight ? reqRight : fragRight; + int32_t clipBottom = reqBottom < fragBottom ? reqBottom : fragBottom; + if (clipLeft >= clipRight || clipTop >= clipBottom) continue; + + if ((uint32_t) fragment->atlasId >= renderer->atlasPageCount) continue; + if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) continue; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId]; + if (!page->ready) continue; + + uint16_t px = (uint16_t) (fragment->x + (uint16_t) (clipLeft - fragLeft)); + uint16_t py = (uint16_t) (fragment->y + (uint16_t) (clipTop - fragTop)); + int32_t clipW = clipRight - clipLeft; + int32_t clipH = clipBottom - clipTop; + Tex3DS_SubTexture subtex; + C2D_Image image = { + .tex = &page->texture, + .subtex = &subtex, + }; + N3DSRenderer_fillSubTexture(&subtex, page, px, py, (uint16_t) clipW, (uint16_t) clipH); + renderer->frameFragmentDraws++; + N3DSRenderer_drawImageFast( + base, + &image, + drawX + (float) (clipLeft - reqLeft) * xscale, + drawY + (float) (clipTop - reqTop) * yscale, + xscale, + yscale, + color, + alpha + ); + drewAnything = true; + } + + return drewAnything; +} + +static bool N3DSRenderer_drawPackedTileEntry( + Renderer* base, + N3DSRenderer* renderer, + const N3DSTileAtlasEntry* tileEntry, + float drawX, + float drawY, + float xscale, + float yscale, + uint32_t color, + float alpha +) { + if (tileEntry == NULL) return false; + + if (tileEntry->fragmentCount == 0) { + if (tileEntry->width == 0 || + tileEntry->height == 0 || + tileEntry->atlasId >= renderer->atlasPageCount || + !N3DSRenderer_ensurePageLoaded(renderer, tileEntry->atlasId)) { + return false; + } + + N3DSLoadedAtlasPage* page = &renderer->atlasPages[tileEntry->atlasId]; + if (!page->ready) return false; + + Tex3DS_SubTexture subtex; + C2D_Image image = { + .tex = &page->texture, + .subtex = &subtex, + }; + N3DSRenderer_fillSubTexture(&subtex, page, tileEntry->x, tileEntry->y, tileEntry->width, tileEntry->height); + N3DSRenderer_drawImageFast(base, &image, drawX, drawY, xscale, yscale, color, alpha); + return true; + } + + bool drewAnything = false; + repeat(tileEntry->fragmentCount, fragIndex) { + uint32_t fragmentIndex = tileEntry->fragmentStart + (uint32_t) fragIndex; + if (fragmentIndex >= renderer->atlasFragmentCount) break; + N3DSAtlasFragment* fragment = &renderer->atlasFragments[fragmentIndex]; + if (!N3DSRenderer_ensurePageLoaded(renderer, fragment->atlasId)) continue; + N3DSLoadedAtlasPage* page = &renderer->atlasPages[fragment->atlasId]; + if (!page->ready) continue; + + Tex3DS_SubTexture subtex; + C2D_Image image = { + .tex = &page->texture, + .subtex = &subtex, + }; + N3DSRenderer_fillSubTexture(&subtex, page, fragment->x, fragment->y, fragment->width, fragment->height); + renderer->frameFragmentDraws++; + N3DSRenderer_drawImageFast( + base, + &image, + drawX + (float) fragment->sourceX * xscale, + drawY + (float) fragment->sourceY * yscale, + xscale, + yscale, + color, + alpha + ); + drewAnything = true; + } + + return drewAnything; +} + +static void N3DSRenderer_drawTile(Renderer* base, RoomTile* tile, float offsetX, float offsetY) { + if (base == NULL || tile == NULL) return; + int32_t srcW = (int32_t) tile->width; + int32_t srcH = (int32_t) tile->height; + if (srcW <= 0 || srcH <= 0) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + uint32_t bgr = tile->color & 0x00FFFFFFu; + uint8_t alphaByte = (uint8_t) ((tile->color >> 24) & 0xFFu); + float alpha = (alphaByte == 0) ? 1.0f : (float) alphaByte / 255.0f; + float drawX = (float) tile->x + offsetX; + float drawY = (float) tile->y + offsetY; + if (!Renderer_isFiniteFloat(drawX) || !Renderer_isFiniteFloat(drawY) || + !Renderer_isFiniteFloat(tile->scaleX) || !Renderer_isFiniteFloat(tile->scaleY) || + tile->scaleX == 0.0f || tile->scaleY == 0.0f) { + return; + } + + N3DSTileAtlasEntry* tileEntry = N3DSRenderer_findTileEntry(renderer, tile); + if (tileEntry != NULL && + N3DSRenderer_drawPackedTileEntry(base, renderer, tileEntry, drawX, drawY, tile->scaleX, tile->scaleY, bgr, alpha)) { + return; + } + + int32_t tpagIndex = Renderer_resolveObjectTPAGIndex(base->dataWin, tile); + if (tpagIndex < 0 || (uint32_t) tpagIndex >= base->dataWin->tpag.count) return; + TexturePageItem* tpag = &base->dataWin->tpag.items[tpagIndex]; + + if (N3DSRenderer_tryDrawTileRect( + base, + tpagIndex, + tile->sourceX, + tile->sourceY, + srcW, + srcH, + drawX, + drawY, + tile->scaleX, + tile->scaleY, + bgr, + alpha + )) { + return; + } + + int32_t fallbackSrcX = tile->sourceX - tpag->targetX; + int32_t fallbackSrcY = tile->sourceY - tpag->targetY; + (void) N3DSRenderer_tryDrawTileRect( + base, + tpagIndex, + fallbackSrcX, + fallbackSrcY, + srcW, + srcH, + drawX, + drawY, + tile->scaleX, + tile->scaleY, + bgr, + alpha + ); +} + +static void N3DSRenderer_drawTiled(Renderer* base, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha) { + if (!Renderer_isFiniteFloat(originX) || !Renderer_isFiniteFloat(originY) || + !Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) || + !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) || + !Renderer_isFiniteFloat(roomW) || !Renderer_isFiniteFloat(roomH) || + !Renderer_isFiniteFloat(alpha) || xscale == 0.0f || yscale == 0.0f) { + return; + } + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= base->dataWin->tpag.count) return; + TexturePageItem* tpag = &base->dataWin->tpag.items[tpagIndex]; + if (tpag->boundingWidth == 0 || tpag->boundingHeight == 0) return; + + int32_t cropX = 0; + int32_t cropY = 0; + int32_t cropW = (int32_t) tpag->sourceWidth; + int32_t cropH = (int32_t) tpag->sourceHeight; + if (!N3DSRenderer_getSourceCropRect(renderer, tpagIndex, &cropX, &cropY, &cropW, &cropH)) { + return; + } + if (cropW <= 0 || cropH <= 0) return; + + C2D_Image* directImage = NULL; + if (renderer->tpagToSpriteIndex != NULL && renderer->tpagToSpriteFrameIndex != NULL && + tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) { + int32_t spriteIndex = renderer->tpagToSpriteIndex[tpagIndex]; + int32_t frameIndex = renderer->tpagToSpriteFrameIndex[tpagIndex]; + if (spriteIndex >= 0 && frameIndex >= 0) { + N3DSRenderer_tryLoadDirectSpriteImage(renderer, spriteIndex, frameIndex, &directImage); + } + } + if (directImage == NULL && + renderer->tpagToBackgroundIndex != NULL && + tpagIndex >= 0 && (uint32_t) tpagIndex < renderer->base.dataWin->tpag.count) { + int32_t backgroundIndex = renderer->tpagToBackgroundIndex[tpagIndex]; + if (backgroundIndex >= 0) { + N3DSRenderer_tryLoadDirectBackgroundImage(renderer, backgroundIndex, &directImage); + } + } + if (directImage != NULL && directImage->subtex != NULL) { + cropX = 0; + cropY = 0; + cropW = (int32_t) directImage->subtex->width; + cropH = (int32_t) directImage->subtex->height; + } + + float axScale = fabsf(xscale); + float ayScale = fabsf(yscale); + float tileW = (float) cropW * axScale; + float tileH = (float) cropH * ayScale; + if (tileW <= 0.0f || tileH <= 0.0f) return; + + float startX; + float endX; + float startY; + float endY; + if (tileX) { + startX = fmodf(x - originX * axScale, tileW); + if (startX > 0.0f) startX -= tileW; + endX = roomW; + } else { + startX = x - originX * axScale; + endX = startX + tileW; + } + if (tileY) { + startY = fmodf(y - originY * ayScale, tileH); + if (startY > 0.0f) startY -= tileH; + endY = roomH; + } else { + startY = y - originY * ayScale; + endY = startY + tileH; + } + + float dxLocalX0 = (float) cropX * xscale + originX * (axScale - xscale); + float dyLocalY0 = (float) cropY * yscale + originY * (ayScale - yscale); + float tileGameW = (float) cropW * xscale; + float tileGameH = (float) cropH * yscale; + + int32_t tilesX = tileX ? ((int32_t) ((endX - startX) / tileW) + 1) : 1; + int32_t tilesY = tileY ? ((int32_t) ((endY - startY) / tileH) + 1) : 1; + if (tilesX <= 0 || tilesY <= 0) return; + + float viewRight = (renderer->viewScaleX != 0.0f) ? ((float) renderer->viewX + ((float) N3DS_TOP_WIDTH / fabsf(renderer->viewScaleX))) : (float) renderer->viewX; + float viewBottom = (renderer->viewScaleY != 0.0f) ? ((float) renderer->viewY + ((float) N3DS_TOP_HEIGHT / fabsf(renderer->viewScaleY))) : (float) renderer->viewY; + int32_t startTileX = 0; + int32_t endTileX = tilesX; + int32_t startTileY = 0; + int32_t endTileY = tilesY; + + if (tileX) { + float firstVisibleLeft = startX + dxLocalX0; + float minTileX = floorf((((float) renderer->viewX) - (firstVisibleLeft + tileGameW)) / tileW) + 1.0f; + float maxTileX = ceilf((viewRight - firstVisibleLeft) / tileW); + startTileX = (int32_t) minTileX; + endTileX = (int32_t) maxTileX; + if (startTileX < 0) startTileX = 0; + if (endTileX > tilesX) endTileX = tilesX; + } + + if (tileY) { + float firstVisibleTop = startY + dyLocalY0; + float minTileY = floorf((((float) renderer->viewY) - (firstVisibleTop + tileGameH)) / tileH) + 1.0f; + float maxTileY = ceilf((viewBottom - firstVisibleTop) / tileH); + startTileY = (int32_t) minTileY; + endTileY = (int32_t) maxTileY; + if (startTileY < 0) startTileY = 0; + if (endTileY > tilesY) endTileY = tilesY; + } + + if (startTileX >= endTileX || startTileY >= endTileY) return; + + for (int32_t iy = startTileY; iy < endTileY; iy++) { + float dy = startY + (float) iy * tileH; + if (dy >= endY) break; + + float localTop = dy + dyLocalY0; + float localBottom = localTop + tileGameH; + float localMinY = localTop < localBottom ? localTop : localBottom; + float localMaxY = localTop > localBottom ? localTop : localBottom; + float visTop = localMinY > (float) renderer->viewY ? localMinY : (float) renderer->viewY; + float visBottom = localMaxY < viewBottom ? localMaxY : viewBottom; + if (visTop >= visBottom) continue; + + for (int32_t ix = startTileX; ix < endTileX; ix++) { + float dx = startX + (float) ix * tileW; + if (dx >= endX) break; + + float localLeft = dx + dxLocalX0; + float localRight = localLeft + tileGameW; + float localMinX = localLeft < localRight ? localLeft : localRight; + float localMaxX = localLeft > localRight ? localLeft : localRight; + float visLeft = localMinX > (float) renderer->viewX ? localMinX : (float) renderer->viewX; + float visRight = localMaxX < viewRight ? localMaxX : viewRight; + if (visLeft >= visRight) continue; + + float drawX = dx + dxLocalX0; + float drawY = dy + dyLocalY0; + if (directImage != NULL) { + renderer->frameDirectSpriteHits++; + if (fabsf(xscale - 1.0f) < 0.001f && fabsf(yscale - 1.0f) < 0.001f) { + N3DSRenderer_drawImageFast(base, directImage, drawX, drawY, 1.0f, 1.0f, color, alpha); + } else { + N3DSRenderer_drawImage( + base, + directImage, + drawX, + drawY, + (float) cropW * xscale, + (float) cropH * yscale, + 0.0f, + 0.0f, + 0.0f, + color, + alpha + ); + } + } else if (!N3DSRenderer_tryDrawTileRect(base, tpagIndex, cropX, cropY, cropW, cropH, drawX, drawY, xscale, yscale, color, alpha)) { + N3DSRenderer_drawSpritePart(base, tpagIndex, cropX, cropY, cropW, cropH, drawX, drawY, xscale, yscale, 0.0f, 0.0f, 0.0f, color, alpha); + } + } + } +} + +static void N3DSRenderer_drawTiledPart(Renderer* base, int32_t tpagIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint32_t color, float alpha) { + if (srcW <= 0 || srcH <= 0 || dstW <= 0.0f || dstH <= 0.0f) return; + if (!Renderer_isFiniteFloat(dstX) || !Renderer_isFiniteFloat(dstY) || + !Renderer_isFiniteFloat(dstW) || !Renderer_isFiniteFloat(dstH) || + !Renderer_isFiniteFloat(alpha)) { + return; + } + + N3DSRenderer* renderer = (N3DSRenderer*) base; + int32_t cropX = 0; + int32_t cropY = 0; + int32_t cropW = 0; + int32_t cropH = 0; + if (!N3DSRenderer_getSourceCropRect(renderer, tpagIndex, &cropX, &cropY, &cropW, &cropH)) return; + + int32_t tilesY = (int32_t) (dstH / (float) srcH) + 2; + int32_t tilesX = (int32_t) (dstW / (float) srcW) + 2; + + repeat(tilesY, iy) { + float rowDstY = dstY + (float) iy * (float) srcH; + if (rowDstY >= dstY + dstH) break; + int32_t rowSrcH = srcH; + if (rowDstY + (float) rowSrcH > dstY + dstH) { + rowSrcH = (int32_t) ((dstY + dstH) - rowDstY); + } + if (rowSrcH <= 0) continue; + + int32_t intY1 = srcY > cropY ? srcY : cropY; + int32_t intY2 = (srcY + rowSrcH) < (cropY + cropH) ? (srcY + rowSrcH) : (cropY + cropH); + if (intY1 >= intY2) continue; + + float clipOffY = (float) (intY1 - srcY); + int32_t visH = intY2 - intY1; + + repeat(tilesX, ix) { + float colDstX = dstX + (float) ix * (float) srcW; + if (colDstX >= dstX + dstW) break; + int32_t colSrcW = srcW; + if (colDstX + (float) colSrcW > dstX + dstW) { + colSrcW = (int32_t) ((dstX + dstW) - colDstX); + } + if (colSrcW <= 0) continue; + + int32_t intX1 = srcX > cropX ? srcX : cropX; + int32_t intX2 = (srcX + colSrcW) < (cropX + cropW) ? (srcX + colSrcW) : (cropX + cropW); + if (intX1 >= intX2) continue; + + float clipOffX = (float) (intX1 - srcX); + int32_t visW = intX2 - intX1; + float drawX = colDstX + clipOffX; + float drawY = rowDstY + clipOffY; + + if (!N3DSRenderer_tryDrawTileRect(base, tpagIndex, intX1, intY1, visW, visH, drawX, drawY, 1.0f, 1.0f, color, alpha)) { + N3DSRenderer_drawSpritePart(base, tpagIndex, intX1, intY1, visW, visH, drawX, drawY, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, color, alpha); + } + } + } +} + +static void N3DSRenderer_drawSpritePos(Renderer* base, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) { + if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) || + !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) || + !Renderer_isFiniteFloat(x3) || !Renderer_isFiniteFloat(y3) || + !Renderer_isFiniteFloat(x4) || !Renderer_isFiniteFloat(y4) || + !Renderer_isFiniteFloat(alpha)) { + return; + } + + bool axisAligned = + fabsf(y1 - y2) < 0.01f && + fabsf(x2 - x3) < 0.01f && + fabsf(y3 - y4) < 0.01f && + fabsf(x4 - x1) < 0.01f; + if (!axisAligned) return; + + N3DSRenderer_drawSpritePart( + base, + tpagIndex, + 0, + 0, + (int32_t) lroundf(fabsf(x2 - x1)), + (int32_t) lroundf(fabsf(y4 - y1)), + x1, + y1, + 1.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0xFFFFFF, + alpha + ); +} + +static void N3DSRenderer_drawRectangle(Renderer* base, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline) { + if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) || + !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) || + !Renderer_isFiniteFloat(alpha)) { + return; + } + + N3DSRenderer* renderer = (N3DSRenderer*) base; + float sx1 = N3DSRenderer_transformScreenX(renderer, x1); + float sy1 = N3DSRenderer_transformScreenY(renderer, y1); + float sx2 = N3DSRenderer_transformScreenX(renderer, x2); + float sy2 = N3DSRenderer_transformScreenY(renderer, y2); + float sx = sx1 < sx2 ? sx1 : sx2; + float sy = sy1 < sy2 ? sy1 : sy2; + float sw = fabsf(sx2 - sx1); + float sh = fabsf(sy2 - sy1); + u32 rgba = N3DSRenderer_makeColor(color, alpha); + + if (outline) { + C2D_DrawLine(sx, sy, rgba, sx + sw, sy, rgba, 1.0f, 0.5f); + C2D_DrawLine(sx + sw, sy, rgba, sx + sw, sy + sh, rgba, 1.0f, 0.5f); + C2D_DrawLine(sx + sw, sy + sh, rgba, sx, sy + sh, rgba, 1.0f, 0.5f); + C2D_DrawLine(sx, sy + sh, rgba, sx, sy, rgba, 1.0f, 0.5f); + N3DSRenderer_noteC2DDraws(renderer, 4u); + } else { + C2D_DrawRectSolid(sx, sy, 0.5f, sw, sh, rgba); + N3DSRenderer_noteC2DDraws(renderer, 1u); + } +} + +static void N3DSRenderer_drawLine(Renderer* base, float x1, float y1, float x2, float y2, float width, uint32_t color, float alpha) { + if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) || + !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) || + !Renderer_isFiniteFloat(width) || !Renderer_isFiniteFloat(alpha)) { + return; + } + + N3DSRenderer* renderer = (N3DSRenderer*) base; + float sx1 = N3DSRenderer_transformScreenX(renderer, x1); + float sy1 = N3DSRenderer_transformScreenY(renderer, y1); + float sx2 = N3DSRenderer_transformScreenX(renderer, x2); + float sy2 = N3DSRenderer_transformScreenY(renderer, y2); + if (!Renderer_isFiniteFloat(sx1) || !Renderer_isFiniteFloat(sy1) || + !Renderer_isFiniteFloat(sx2) || !Renderer_isFiniteFloat(sy2)) { + return; + } + + width = fabsf(width); + if (width < 1.0f) width = 1.0f; + + float dx = sx2 - sx1; + float dy = sy2 - sy1; + if ((dx * dx + dy * dy) <= 0.0001f) { + C2D_DrawRectSolid(sx1, sy1, 0.5f, width, width, N3DSRenderer_makeColor(color, alpha)); + N3DSRenderer_noteC2DDraws(renderer, 1u); + return; + } + + u32 rgba = N3DSRenderer_makeColor(color, alpha); + C2D_DrawLine( + sx1, + sy1, + rgba, + sx2, + sy2, + rgba, + width, + 0.5f + ); + N3DSRenderer_noteC2DDraws(renderer, 1u); +} + +static void N3DSRenderer_drawTriangle(Renderer* base, float x1, float y1, float x2, float y2, float x3, float y3, bool outline) { + if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) || + !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) || + !Renderer_isFiniteFloat(x3) || !Renderer_isFiniteFloat(y3)) { + return; + } + + if (outline) { + N3DSRenderer_drawLine(base, x1, y1, x2, y2, 1.0f, base->drawColor, base->drawAlpha); + N3DSRenderer_drawLine(base, x2, y2, x3, y3, 1.0f, base->drawColor, base->drawAlpha); + N3DSRenderer_drawLine(base, x3, y3, x1, y1, 1.0f, base->drawColor, base->drawAlpha); + return; + } + + N3DSRenderer* renderer = (N3DSRenderer*) base; + float sx1 = N3DSRenderer_transformScreenX(renderer, x1); + float sy1 = N3DSRenderer_transformScreenY(renderer, y1); + float sx2 = N3DSRenderer_transformScreenX(renderer, x2); + float sy2 = N3DSRenderer_transformScreenY(renderer, y2); + float sx3 = N3DSRenderer_transformScreenX(renderer, x3); + float sy3 = N3DSRenderer_transformScreenY(renderer, y3); + if (!Renderer_isFiniteFloat(sx1) || !Renderer_isFiniteFloat(sy1) || + !Renderer_isFiniteFloat(sx2) || !Renderer_isFiniteFloat(sy2) || + !Renderer_isFiniteFloat(sx3) || !Renderer_isFiniteFloat(sy3)) { + return; + } + + float minX = sx1 < sx2 ? sx1 : sx2; + if (sx3 < minX) minX = sx3; + float maxX = sx1 > sx2 ? sx1 : sx2; + if (sx3 > maxX) maxX = sx3; + float minY = sy1 < sy2 ? sy1 : sy2; + if (sy3 < minY) minY = sy3; + float maxY = sy1 > sy2 ? sy1 : sy2; + if (sy3 > maxY) maxY = sy3; + if (N3DSRenderer_isScreenRectOffscreen(renderer, minX, minY, maxX - minX, maxY - minY)) { + return; + } + + // Tiny circles and snapped coordinates can collapse adjacent fan triangles to zero area. + // Citro2D appears to dislike those degenerate cases, so skip them before issuing the draw. + float doubledArea = + ((sx2 - sx1) * (sy3 - sy1)) - + ((sy2 - sy1) * (sx3 - sx1)); + if (fabsf(doubledArea) < 0.01f) { + return; + } + + u32 rgba = N3DSRenderer_makeColor(base->drawColor, base->drawAlpha); + C2D_DrawTriangle( + sx1, + sy1, + rgba, + sx2, + sy2, + rgba, + sx3, + sy3, + rgba, + 0.5f + ); + N3DSRenderer_noteC2DDraws(renderer, 1u); +} + +static void N3DSRenderer_drawLineColor(Renderer* base, float x1, float y1, float x2, float y2, float width, uint32_t color1, uint32_t color2, float alpha) { + if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) || + !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) || + !Renderer_isFiniteFloat(width) || !Renderer_isFiniteFloat(alpha)) { + return; + } + + N3DSRenderer* renderer = (N3DSRenderer*) base; + float sx1 = N3DSRenderer_transformScreenX(renderer, x1); + float sy1 = N3DSRenderer_transformScreenY(renderer, y1); + float sx2 = N3DSRenderer_transformScreenX(renderer, x2); + float sy2 = N3DSRenderer_transformScreenY(renderer, y2); + if (!Renderer_isFiniteFloat(sx1) || !Renderer_isFiniteFloat(sy1) || + !Renderer_isFiniteFloat(sx2) || !Renderer_isFiniteFloat(sy2)) { + return; + } + + width = fabsf(width); + if (width < 1.0f) width = 1.0f; + + float dx = sx2 - sx1; + float dy = sy2 - sy1; + if ((dx * dx + dy * dy) <= 0.0001f) { + C2D_DrawRectSolid(sx1, sy1, 0.5f, width, width, N3DSRenderer_makeColor(color1, alpha)); + N3DSRenderer_noteC2DDraws(renderer, 1u); + return; + } + + C2D_DrawLine( + sx1, + sy1, + N3DSRenderer_makeColor(color1, alpha), + sx2, + sy2, + N3DSRenderer_makeColor(color2, alpha), + width, + 0.5f + ); + N3DSRenderer_noteC2DDraws(renderer, 1u); +} + +static void N3DSRenderer_drawTextCommon(Renderer* base, const char* text, float x, float y, float xscale, float yscale, float angleDeg, bool gradient, int32_t c1, int32_t c2, int32_t c3, int32_t c4, float alpha) { + DataWin* dw = base->dataWin; + if (base->drawFont < 0 || (uint32_t) base->drawFont >= dw->font.count) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + bool savedTextLinearFilterActive = renderer->textLinearFilterActive; + renderer->textLinearFilterActive = renderer->topScreenGui2xActive; + u64 textStartTick = svcGetSystemTick(); + Font* font = &dw->font.fonts[base->drawFont]; + float effectiveXScale = xscale * font->scaleX; + float effectiveYScale = yscale * font->scaleY; + if (!font->isSpriteFont && font->tpagIndex >= 0) { + N3DSRenderer_traceTPAGUsage(renderer, N3DS_TRACE_KIND_FONT, font->tpagIndex); + } + int32_t len = (int32_t) strlen(text); + const N3DSAtlasFragment* fontFragment = NULL; + N3DSLoadedAtlasPage* fontPage = NULL; + C2D_Image directFontImage; + Tex3DS_SubTexture directFontBaseSubtex; + bool useDirectFontAsset = + !font->isSpriteFont && + N3DSRenderer_tryResolveDirectFontImage(renderer, base->drawFont, &directFontImage, &directFontBaseSubtex); + bool useSingleFragmentFontFastPath = + !useDirectFontAsset && + !font->isSpriteFont && + N3DSRenderer_tryResolveSingleFragmentFontPage(renderer, font->tpagIndex, &fontFragment, &fontPage); + if (useDirectFontAsset || useSingleFragmentFontFastPath) { + if (N3DSRenderer_tryDrawSingleGlyphTextFast( + base, + renderer, + font, + text, + len, + x, + y, + effectiveXScale, + effectiveYScale, + angleDeg, + gradient, + c1, + alpha, + useDirectFontAsset, + useDirectFontAsset ? &directFontImage : NULL, + useDirectFontAsset ? &directFontBaseSubtex : NULL, + fontFragment, + fontPage)) { + (void) c2; + (void) c3; + (void) c4; + renderer->frameTextTenthsMs += (uint32_t) lround(N3DSRenderer_ticksToMs(svcGetSystemTick() - textStartTick) * 10.0); + renderer->textLinearFilterActive = savedTextLinearFilterActive; + return; + } + + const N3DSCachedTextLayout* layout = N3DSRenderer_getCachedTextLayout(renderer, font, base->drawFont, text); + repeat(layout->glyphCount, i) { + const N3DSCachedTextGlyph* cachedGlyph = &layout->glyphs[i]; + Tex3DS_SubTexture subtex; + C2D_Image image; + if (useDirectFontAsset) { + subtex.width = cachedGlyph->sourceWidth; + subtex.height = cachedGlyph->sourceHeight; + float baseLeft = directFontBaseSubtex.left; + float baseRight = directFontBaseSubtex.right; + float baseTop = directFontBaseSubtex.top; + float baseBottom = directFontBaseSubtex.bottom; + float baseWidth = (float) directFontBaseSubtex.width; + float baseHeight = (float) directFontBaseSubtex.height; + subtex.left = baseLeft + (baseRight - baseLeft) * ((float) cachedGlyph->sourceX / baseWidth); + subtex.right = baseLeft + (baseRight - baseLeft) * (((float) cachedGlyph->sourceX + (float) cachedGlyph->sourceWidth) / baseWidth); + subtex.top = baseTop + (baseBottom - baseTop) * ((float) cachedGlyph->sourceY / baseHeight); + subtex.bottom = baseTop + (baseBottom - baseTop) * (((float) cachedGlyph->sourceY + (float) cachedGlyph->sourceHeight) / baseHeight); + image.tex = directFontImage.tex; + image.subtex = &subtex; + } else { + image.tex = &fontPage->texture; + image.subtex = &subtex; + uint16_t px = (uint16_t) (fontFragment->x + cachedGlyph->sourceX); + uint16_t py = (uint16_t) (fontFragment->y + cachedGlyph->sourceY); + N3DSRenderer_fillSubTexture(&subtex, fontPage, px, py, cachedGlyph->sourceWidth, cachedGlyph->sourceHeight); + } + float drawX = x + cachedGlyph->localX * effectiveXScale; + float drawY = y + cachedGlyph->localY * effectiveYScale; + renderer->frameTextGlyphDraws++; + if (fabsf(angleDeg) < 0.001f) { + N3DSRenderer_drawImageFast(base, &image, drawX, drawY, effectiveXScale, effectiveYScale, gradient ? (uint32_t) c1 : base->drawColor, alpha); + } else { + N3DSRenderer_drawImage(base, &image, drawX, drawY, (float) cachedGlyph->sourceWidth * effectiveXScale, (float) cachedGlyph->sourceHeight * effectiveYScale, 0.0f, 0.0f, angleDeg, gradient ? (uint32_t) c1 : base->drawColor, alpha); + } + } + (void) c2; + (void) c3; + (void) c4; + renderer->frameTextTenthsMs += (uint32_t) lround(N3DSRenderer_ticksToMs(svcGetSystemTick() - textStartTick) * 10.0); + renderer->textLinearFilterActive = savedTextLinearFilterActive; + return; + } + int32_t lineCount = TextUtils_countLines(text, len); + float totalHeight = (float) lineCount * TextUtils_lineStride(font) * effectiveYScale; + float valignOffset = 0.0f; + if (base->drawValign == 1) valignOffset = -totalHeight * 0.5f; + else if (base->drawValign == 2) valignOffset = -totalHeight; + + float cursorY = y + valignOffset - (float) font->ascenderOffset * effectiveYScale; + int32_t lineStart = 0; + + while (lineStart <= len) { + int32_t lineEnd = lineStart; + while (lineEnd < len && !TextUtils_isNewlineChar(text[lineEnd])) lineEnd++; + float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineEnd - lineStart) * effectiveXScale; + float cursorX = x; + if (base->drawHalign == 1) cursorX -= lineWidth * 0.5f; + else if (base->drawHalign == 2) cursorX -= lineWidth; + + int32_t pos = lineStart; + while (pos < lineEnd) { + uint16_t ch = TextUtils_decodeUtf8(text, lineEnd, &pos); + FontGlyph* glyph = TextUtils_findGlyph(font, ch); + if (glyph == NULL) continue; + + uint32_t glyphColor = gradient ? (uint32_t) c1 : base->drawColor; + if (font->isSpriteFont) { + int32_t glyphIndex = (int32_t) (glyph - font->glyphs); + Sprite* sprite = &dw->sprt.sprites[font->spriteIndex]; + if (glyphIndex >= 0 && glyphIndex < (int32_t) sprite->textureCount) { + int32_t glyphTpagIndex = sprite->tpagIndices[glyphIndex]; + if (glyphTpagIndex >= 0 && (uint32_t) glyphTpagIndex < dw->tpag.count) { + TexturePageItem* glyphTpag = &dw->tpag.items[glyphTpagIndex]; + renderer->frameTextGlyphDraws++; + N3DSRenderer_drawSprite( + base, + glyphTpagIndex, + cursorX + (float) glyph->offset * effectiveXScale, + cursorY, + (float) glyphTpag->targetX, + (float) sprite->originY, + effectiveXScale, + effectiveYScale, + angleDeg, + glyphColor, + alpha + ); + } + } + } else { + renderer->frameTextGlyphDraws++; + N3DSRenderer_drawSpritePart( + base, + font->tpagIndex, + glyph->sourceX, + glyph->sourceY, + glyph->sourceWidth, + glyph->sourceHeight, + cursorX + (float) glyph->offset * effectiveXScale, + cursorY, + effectiveXScale, + effectiveYScale, + angleDeg, + 0.0f, + 0.0f, + glyphColor, + alpha + ); + } + + if (pos < lineEnd) { + int32_t previewPos = pos; + uint16_t nextCh = TextUtils_decodeUtf8(text, lineEnd, &previewPos); + cursorX += ((float) glyph->shift + TextUtils_getKerningOffset(glyph, nextCh)) * effectiveXScale; + } else { + cursorX += (float) glyph->shift * effectiveXScale; + } + } + + if (lineEnd >= len) break; + lineStart = TextUtils_skipNewline(text, lineEnd, len); + cursorY += TextUtils_lineStride(font) * effectiveYScale; + } + + (void) c2; + (void) c3; + (void) c4; + renderer->frameTextTenthsMs += (uint32_t) lround(N3DSRenderer_ticksToMs(svcGetSystemTick() - textStartTick) * 10.0); + renderer->textLinearFilterActive = savedTextLinearFilterActive; +} + +static void N3DSRenderer_drawText(Renderer* base, const char* text, float x, float y, float xscale, float yscale, float angleDeg) { + N3DSRenderer_drawTextCommon(base, text, x, y, xscale, yscale, angleDeg, false, 0, 0, 0, 0, base->drawAlpha); +} + +static void N3DSRenderer_drawTextColor(Renderer* base, const char* text, float x, float y, float xscale, float yscale, float angleDeg, int32_t c1, int32_t c2, int32_t c3, int32_t c4, float alpha) { + N3DSRenderer_drawTextCommon(base, text, x, y, xscale, yscale, angleDeg, true, c1, c2, c3, c4, alpha); +} + +static void N3DSRenderer_flush(Renderer* base) { + if (base == NULL) return; + N3DSRenderer_flushC2DQueue((N3DSRenderer*) base); +} + +static void N3DSRenderer_clearScreen(Renderer* base, uint32_t color, float alpha) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + renderer->clearColor = color; + renderer->clearAlpha = alpha; + C2D_TargetClear(renderer->topTarget, N3DSRenderer_makeColor(color, alpha)); +} + +static int32_t N3DSRenderer_createSpriteFromSurface(Renderer* base, int32_t surfaceID, int32_t x, int32_t y, int32_t w, int32_t h, bool removeback, bool smooth, int32_t xorig, int32_t yorig) { + if (base == NULL || base->dataWin == NULL || surfaceID != -1 || w <= 0 || h <= 0) return -1; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (renderer->activeSceneTarget != N3DS_SCENE_TARGET_TOP) return -1; + + float screenRectX = 0.0f; + float screenRectY = 0.0f; + float screenRectW = 0.0f; + float screenRectH = 0.0f; + N3DSRenderer_transformScreenRect(renderer, (float) x, (float) y, (float) w, (float) h, &screenRectX, &screenRectY, &screenRectW, &screenRectH); + + int32_t screenLeft = (int32_t) floorf(screenRectX); + int32_t screenTop = (int32_t) floorf(screenRectY); + int32_t screenRight = (int32_t) ceilf(screenRectX + screenRectW); + int32_t screenBottom = (int32_t) ceilf(screenRectY + screenRectH); + if (screenLeft < 0) screenLeft = 0; + if (screenTop < 0) screenTop = 0; + if (screenRight > N3DS_TOP_WIDTH) screenRight = N3DS_TOP_WIDTH; + if (screenBottom > N3DS_TOP_HEIGHT) screenBottom = N3DS_TOP_HEIGHT; + if (screenLeft >= screenRight || screenTop >= screenBottom) return -1; + + uint16_t captureWidth = (uint16_t) (screenRight - screenLeft); + uint16_t captureHeight = (uint16_t) (screenBottom - screenTop); + size_t pixelCount = (size_t) captureWidth * (size_t) captureHeight; + uint16_t* pixels = (uint16_t*) linearAlloc(pixelCount * sizeof(uint16_t)); + if (pixels == NULL) return -1; + + N3DSRenderer_flushC2DQueue(renderer); + C2D_Flush(); + uint8_t* framebuffer = gfxGetFramebuffer(GFX_TOP, GFX_LEFT, NULL, NULL); + if (framebuffer == NULL) { + linearFree(pixels); + return -1; + } + + uint16_t transparentKey = 0; + bool haveTransparentKey = false; + repeat(captureHeight, row) { + int32_t screenY = screenTop + (int32_t) row; + repeat(captureWidth, col) { + int32_t screenX = screenLeft + (int32_t) col; + size_t fbOffset = ((size_t) screenX * (size_t) N3DS_TOP_HEIGHT + (size_t) ((N3DS_TOP_HEIGHT - 1) - screenY)) * 3u; + uint8_t r = framebuffer[fbOffset + 0u]; + uint8_t g = framebuffer[fbOffset + 1u]; + uint8_t b = framebuffer[fbOffset + 2u]; + uint16_t rgba5551 = (uint16_t) ((((uint16_t) r >> 3) << 11) | (((uint16_t) g >> 3) << 6) | (((uint16_t) b >> 3) << 1) | 1u); + if (!haveTransparentKey) { + transparentKey = rgba5551; + haveTransparentKey = true; + } + if (removeback && rgba5551 == transparentKey) { + rgba5551 &= (uint16_t) ~1u; + } + pixels[(size_t) row * (size_t) captureWidth + (size_t) col] = rgba5551; + } + } + + int32_t spriteIndex = (int32_t) DataWin_allocSpriteSlot(base->dataWin, base->dataWin->sprt.parsedCount); + int32_t tpagIndex = N3DSRenderer_allocDynamicCaptureTPAG(renderer); + if (spriteIndex < 0 || tpagIndex < 0) { + linearFree(pixels); + return -1; + } + + N3DSDynamicCaptureTPAG* capture = &renderer->dynamicCaptureTPAGs[(uint32_t) tpagIndex - renderer->baseTPAGCount]; + if (!C3D_TexInit(&capture->texture, captureWidth, captureHeight, GPU_RGBA5551)) { + linearFree(pixels); + return -1; + } + + C3D_TexSetFilter(&capture->texture, smooth ? GPU_LINEAR : GPU_NEAREST, smooth ? GPU_LINEAR : GPU_NEAREST); + C3D_TexSetWrap(&capture->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE); + C3D_TexUpload(&capture->texture, pixels); + C3D_TexFlush(&capture->texture); + linearFree(pixels); + + capture->used = true; + capture->ownerSpriteIndex = spriteIndex; + capture->logicalWidth = (uint16_t) w; + capture->logicalHeight = (uint16_t) h; + capture->subtex.width = captureWidth; + capture->subtex.height = captureHeight; + capture->subtex.left = 0.0f; + capture->subtex.top = 1.0f; + capture->subtex.right = 1.0f; + capture->subtex.bottom = 0.0f; + capture->image.tex = &capture->texture; + capture->image.subtex = &capture->subtex; + + Sprite* sprite = &base->dataWin->sprt.sprites[spriteIndex]; + const char* preservedName = sprite->name; + TexturePageItem* tpag = &base->dataWin->tpag.items[tpagIndex]; + memset(sprite, 0, sizeof(*sprite)); + sprite->name = preservedName; + sprite->width = (uint32_t) w; + sprite->height = (uint32_t) h; + sprite->transparent = removeback; + sprite->smooth = smooth; + sprite->originX = xorig; + sprite->originY = yorig; + sprite->textureCount = 1u; + sprite->tpagIndices = safeMalloc(sizeof(int32_t)); + sprite->tpagIndices[0] = tpagIndex; + + memset(tpag, 0, sizeof(*tpag)); + tpag->sourceWidth = (uint16_t) w; + tpag->sourceHeight = (uint16_t) h; + tpag->targetWidth = (uint16_t) w; + tpag->targetHeight = (uint16_t) h; + tpag->boundingWidth = (uint16_t) w; + tpag->boundingHeight = (uint16_t) h; + tpag->texturePageId = -1; + return spriteIndex; +} + +static void N3DSRenderer_deleteSprite(Renderer* base, int32_t spriteIndex) { + if (base == NULL || base->dataWin == NULL) return; + DataWin* dw = base->dataWin; + if (spriteIndex < 0 || (uint32_t) spriteIndex >= dw->sprt.count) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + Sprite* sprite = &dw->sprt.sprites[spriteIndex]; + if (sprite->textureCount == 0) return; + + bool releasedDynamicCapture = false; + repeat(renderer->dynamicCaptureTPAGCount, i) { + N3DSDynamicCaptureTPAG* capture = &renderer->dynamicCaptureTPAGs[i]; + if (!capture->used || capture->ownerSpriteIndex != spriteIndex) continue; + N3DSRenderer_freeDynamicCaptureTPAG(renderer, capture); + TexturePageItem* tpag = &dw->tpag.items[renderer->baseTPAGCount + (uint32_t) i]; + memset(tpag, 0, sizeof(*tpag)); + tpag->texturePageId = -1; + releasedDynamicCapture = true; + } + if (!releasedDynamicCapture) return; + + const char* preservedName = sprite->name; + free(sprite->tpagIndices); + sprite->tpagIndices = NULL; + repeat(sprite->maskCount, maskIndex) { + free(sprite->masks[maskIndex]); + } + free(sprite->masks); + memset(sprite, 0, sizeof(*sprite)); + sprite->name = preservedName; +} + +static void N3DSRenderer_gpuSetBlendMode(Renderer* base, int32_t mode) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + renderer->blendEnabled = true; + renderer->blendEquation = mode; + renderer->blendSrcFactor = (mode == bm_add) ? bm_src_alpha : bm_src_alpha; + renderer->blendDstFactor = (mode == bm_add) ? bm_one : bm_inv_src_alpha; + N3DSRenderer_applyBlendState(renderer); +} + +static void N3DSRenderer_gpuSetBlendModeExt(Renderer* base, int32_t sfactor, int32_t dfactor) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + renderer->blendEnabled = true; + renderer->blendEquation = bm_normal; + renderer->blendSrcFactor = sfactor; + renderer->blendDstFactor = dfactor; + N3DSRenderer_applyBlendState(renderer); +} + +static void N3DSRenderer_gpuSetBlendEnable(Renderer* base, bool enable) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + renderer->blendEnabled = enable; + N3DSRenderer_applyBlendState(renderer); +} + +static void N3DSRenderer_gpuSetAlphaTestEnable(Renderer* base, bool enable) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + renderer->alphaTestEnabled = enable; + N3DSRenderer_applyAlphaState(renderer); +} + +static void N3DSRenderer_gpuSetAlphaTestRef(Renderer* base, uint8_t ref) { + N3DSRenderer* renderer = (N3DSRenderer*) base; + renderer->alphaTestRef = ref; + N3DSRenderer_applyAlphaState(renderer); +} + +static void N3DSRenderer_gpuSetColorWriteEnable(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED bool red, MAYBE_UNUSED bool green, MAYBE_UNUSED bool blue, MAYBE_UNUSED bool alpha) {} +static void N3DSRenderer_gpuSetFog(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED bool enable, MAYBE_UNUSED uint32_t color) {} + +static int32_t N3DSRenderer_createSurface(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t width, MAYBE_UNUSED int32_t height) { return -1; } +static bool N3DSRenderer_surfaceExists(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) { return surfaceID == -1; } +static bool N3DSRenderer_setSurfaceTarget(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) { return false; } +static bool N3DSRenderer_resetSurfaceTarget(MAYBE_UNUSED Renderer* base) { return false; } +static float N3DSRenderer_getSurfaceWidth(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) { return 0.0f; } +static float N3DSRenderer_getSurfaceHeight(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) { return 0.0f; } +static void N3DSRenderer_drawSurface(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID, MAYBE_UNUSED float x, MAYBE_UNUSED float y, MAYBE_UNUSED float xscale, MAYBE_UNUSED float yscale, MAYBE_UNUSED float angleDeg, MAYBE_UNUSED uint32_t color, MAYBE_UNUSED float alpha) {} +static void N3DSRenderer_drawSurfacePart(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID, MAYBE_UNUSED int32_t x, MAYBE_UNUSED int32_t y, MAYBE_UNUSED int32_t left, MAYBE_UNUSED int32_t top, MAYBE_UNUSED int32_t width, MAYBE_UNUSED int32_t height, MAYBE_UNUSED float xscale, MAYBE_UNUSED float yscale, MAYBE_UNUSED uint32_t color, MAYBE_UNUSED float alpha) {} +static void N3DSRenderer_drawSurfaceStretched(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID, MAYBE_UNUSED float x, MAYBE_UNUSED float y, MAYBE_UNUSED float width, MAYBE_UNUSED float height) {} +static void N3DSRenderer_surfaceResize(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID, MAYBE_UNUSED int32_t width, MAYBE_UNUSED int32_t height) {} +static void N3DSRenderer_surfaceFree(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t surfaceID) {} +static void N3DSRenderer_surfaceCopy(MAYBE_UNUSED Renderer* base, MAYBE_UNUSED int32_t DestSurfaceID, MAYBE_UNUSED int32_t DestX, MAYBE_UNUSED int32_t DestY, MAYBE_UNUSED int32_t SrcSurfaceID, MAYBE_UNUSED int32_t SrcX, MAYBE_UNUSED int32_t SrcY, MAYBE_UNUSED int32_t SrcW, MAYBE_UNUSED int32_t SrcH, MAYBE_UNUSED bool part) {} + +static RendererVtable N3DSRenderer_vtable = { + .init = N3DSRenderer_init, + .destroy = N3DSRenderer_destroy, + .beginFrame = N3DSRenderer_beginFrame, + .endFrame = N3DSRenderer_endFrame, + .beginView = N3DSRenderer_beginView, + .endView = N3DSRenderer_endView, + .beginGUI = N3DSRenderer_beginGUI, + .endGUI = N3DSRenderer_endGUI, + .drawSprite = N3DSRenderer_drawSprite, + .drawSpritePart = N3DSRenderer_drawSpritePart, + .drawSpritePos = N3DSRenderer_drawSpritePos, + .drawRectangle = N3DSRenderer_drawRectangle, + .drawLine = N3DSRenderer_drawLine, + .drawTriangle = N3DSRenderer_drawTriangle, + .drawLineColor = N3DSRenderer_drawLineColor, + .drawText = N3DSRenderer_drawText, + .drawTextColor = N3DSRenderer_drawTextColor, + .flush = N3DSRenderer_flush, + .clearScreen = N3DSRenderer_clearScreen, + .createSpriteFromSurface = N3DSRenderer_createSpriteFromSurface, + .deleteSprite = N3DSRenderer_deleteSprite, + .gpuSetBlendMode = N3DSRenderer_gpuSetBlendMode, + .gpuSetBlendModeExt = N3DSRenderer_gpuSetBlendModeExt, + .gpuSetBlendEnable = N3DSRenderer_gpuSetBlendEnable, + .gpuSetAlphaTestEnable = N3DSRenderer_gpuSetAlphaTestEnable, + .gpuSetAlphaTestRef = N3DSRenderer_gpuSetAlphaTestRef, + .gpuSetColorWriteEnable = N3DSRenderer_gpuSetColorWriteEnable, + .gpuSetFog = N3DSRenderer_gpuSetFog, + .drawTile = N3DSRenderer_drawTile, + .prewarmRoom = N3DSRenderer_prewarmRoom, + .drawTiled = N3DSRenderer_drawTiled, + .drawTiledPart = N3DSRenderer_drawTiledPart, + .createSurface = N3DSRenderer_createSurface, + .surfaceExists = N3DSRenderer_surfaceExists, + .setSurfaceTarget = N3DSRenderer_setSurfaceTarget, + .resetSurfaceTarget = N3DSRenderer_resetSurfaceTarget, + .getSurfaceWidth = N3DSRenderer_getSurfaceWidth, + .getSurfaceHeight = N3DSRenderer_getSurfaceHeight, + .drawSurface = N3DSRenderer_drawSurface, + .drawSurfacePart = N3DSRenderer_drawSurfacePart, + .drawSurfaceStretched = N3DSRenderer_drawSurfaceStretched, + .surfaceResize = N3DSRenderer_surfaceResize, + .surfaceFree = N3DSRenderer_surfaceFree, + .surfaceCopy = N3DSRenderer_surfaceCopy, +}; + +Renderer* N3DSRenderer_create(void) { + N3DSRenderer* renderer = safeCalloc(1, sizeof(N3DSRenderer)); + renderer->base.vtable = &N3DSRenderer_vtable; + renderer->base.drawColor = 0xFFFFFF; + renderer->base.drawAlpha = 1.0f; + renderer->base.drawFont = -1; + renderer->base.drawHalign = 0; + renderer->base.drawValign = 0; + renderer->base.circlePrecision = 24; + renderer->resolvedAssetPathCache = NULL; + sh_new_strdup(renderer->resolvedAssetPathCache); + renderer->packedDirectAssetMap = NULL; + sh_new_strdup(renderer->packedDirectAssetMap); + return (Renderer*) renderer; +} + +bool N3DSRenderer_isReady(Renderer* base) { + if (base == NULL) return false; + return ((N3DSRenderer*) base)->atlasLoaded; +} + +const char* N3DSRenderer_getStartupError(Renderer* base) { + if (base == NULL) return "Renderer was not created"; + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (renderer->startupError[0] == '\0') return NULL; + return renderer->startupError; +} + +uint32_t N3DSRenderer_getResidentAtlasVRAMBytes(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->residentAtlasVRAMBytes; +} + +uint32_t N3DSRenderer_getResidentAtlasVRAMLimitBytes(Renderer* base) { + if (base == NULL) return 0; + N3DSRenderer* renderer = (N3DSRenderer*) base; + return renderer->residentAtlasVRAMLimitBytes; +} + +uint32_t N3DSRenderer_getResidentAtlasPageCount(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->residentAtlasPageCount; +} + +uint32_t N3DSRenderer_getResidentAtlasPageLimit(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->residentAtlasPageLimit; +} + +uint32_t N3DSRenderer_getResidentDirectAssetVRAMBytes(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->residentDirectAssetVRAMBytes; +} + +uint32_t N3DSRenderer_getResidentDirectAssetVRAMLimitBytes(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->residentDirectAssetVRAMLimitBytes; +} + +uint32_t N3DSRenderer_getFrameFragmentDraws(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->frameFragmentDraws; +} + +uint32_t N3DSRenderer_getFrameSpriteDrawCalls(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->frameSpriteDrawCalls; +} + +uint32_t N3DSRenderer_getFrameSpritePartDrawCalls(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->frameSpritePartDrawCalls; +} + +uint32_t N3DSRenderer_getFrameDirectSpriteHits(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->frameDirectSpriteHits; +} + +uint32_t N3DSRenderer_getFrameDirectAssetLoads(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->frameDirectAssetLoads; +} + +uint32_t N3DSRenderer_getFrameTextureSwitches(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->frameTextureSwitches; +} + +uint32_t N3DSRenderer_getFrameTextGlyphDraws(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->frameTextGlyphDraws; +} + +uint32_t N3DSRenderer_getFrameTextTenthsMs(Renderer* base) { + if (base == NULL) return 0; + return ((N3DSRenderer*) base)->frameTextTenthsMs; +} + +int32_t N3DSRenderer_findTileEntryIndex(Renderer* base, int32_t backgroundIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH) { + if (base == NULL) return -1; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + uint32_t entryIndex = 0; + if (N3DSRenderer_findTileEntryByKey(renderer, backgroundIndex, srcX, srcY, srcW, srcH, &entryIndex) == NULL) { + return -1; + } + return (int32_t) entryIndex; +} + +bool N3DSRenderer_drawCachedTileEntry(Renderer* base, int32_t tileEntryIndex, float drawX, float drawY, float xscale, float yscale, uint32_t color, float alpha) { + if (base == NULL) return false; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (tileEntryIndex < 0 || (uint32_t) tileEntryIndex >= renderer->tileEntryCount) return false; + return N3DSRenderer_drawPackedTileEntry(base, renderer, &renderer->tileEntries[tileEntryIndex], drawX, drawY, xscale, yscale, color, alpha); +} + +int32_t N3DSRenderer_createTileLayerChunkCache(Renderer* base, int32_t roomWidth, int32_t roomHeight, const TileLayerRenderCache* cache) { + if (base == NULL || cache == NULL || cache->rows == NULL || cache->cells == NULL) return -1; + if (roomWidth <= 0 || roomHeight <= 0 || cache->tileWidth == 0 || cache->tileHeight == 0) return -1; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + uint32_t chunkCols = ((uint32_t) roomWidth + (N3DS_TILE_LAYER_CHUNK_SIZE - 1u)) / N3DS_TILE_LAYER_CHUNK_SIZE; + uint32_t chunkRows = ((uint32_t) roomHeight + (N3DS_TILE_LAYER_CHUNK_SIZE - 1u)) / N3DS_TILE_LAYER_CHUNK_SIZE; + uint32_t chunkCount = chunkCols * chunkRows; + if (chunkCount == 0) return -1; + + uint64_t estimatedBytes = (uint64_t) chunkCount * (uint64_t) N3DS_TILE_LAYER_CHUNK_TEXTURE_BYTES; + if (estimatedBytes > (uint64_t) N3DS_TILE_LAYER_CHUNK_VRAM_BUDGET || + (uint64_t) renderer->tileLayerChunkVRAMBytes + estimatedBytes > (uint64_t) N3DS_TILE_LAYER_CHUNK_VRAM_BUDGET) { + return -1; + } + + int32_t cacheIndex = -1; + size_t existingCount = arrlenu(renderer->tileLayerChunkCaches); + repeat(existingCount, i) { + if (!renderer->tileLayerChunkCaches[i].used) { + cacheIndex = (int32_t) i; + break; + } + } + if (cacheIndex < 0) { + N3DSTileLayerChunkCache newCache = {0}; + arrput(renderer->tileLayerChunkCaches, newCache); + cacheIndex = (int32_t) arrlen(renderer->tileLayerChunkCaches) - 1; + } + + N3DSTileLayerChunkCache* chunkCache = &renderer->tileLayerChunkCaches[cacheIndex]; + N3DSRenderer_freeTileLayerChunkCache(renderer, chunkCache); + chunkCache->used = true; + chunkCache->chunkCount = chunkCount; + chunkCache->vramBytes = 0; + chunkCache->chunks = safeCalloc(chunkCount, sizeof(N3DSTileLayerChunk)); + + float savedFrameScaleX = renderer->frameScaleX; + float savedFrameScaleY = renderer->frameScaleY; + float savedFrameOffsetX = renderer->frameOffsetX; + float savedFrameOffsetY = renderer->frameOffsetY; + float savedPortOffsetX = renderer->portOffsetX; + float savedPortOffsetY = renderer->portOffsetY; + int32_t savedViewX = renderer->viewX; + int32_t savedViewY = renderer->viewY; + float savedViewScaleX = renderer->viewScaleX; + float savedViewScaleY = renderer->viewScaleY; + uint8_t savedSceneTarget = renderer->activeSceneTarget; + + C2D_Flush(); + renderer->pendingC2DDraws = 0; + renderer->lastDrawTexture = NULL; + renderer->frameScaleX = 1.0f; + renderer->frameScaleY = 1.0f; + renderer->frameOffsetX = 0.0f; + renderer->frameOffsetY = 0.0f; + renderer->portOffsetX = 0.0f; + renderer->portOffsetY = 0.0f; + renderer->viewX = 0; + renderer->viewY = 0; + renderer->viewScaleX = 1.0f; + renderer->viewScaleY = 1.0f; + N3DSRenderer_applyBlendState(renderer); + N3DSRenderer_applyAlphaState(renderer); + + repeat(chunkRows, chunkRow) { + repeat(chunkCols, chunkCol) { + uint32_t chunkIndexU = (uint32_t) chunkRow * chunkCols + (uint32_t) chunkCol; + N3DSTileLayerChunk* chunk = &chunkCache->chunks[chunkIndexU]; + chunk->x = (int32_t) chunkCol * (int32_t) N3DS_TILE_LAYER_CHUNK_SIZE; + chunk->y = (int32_t) chunkRow * (int32_t) N3DS_TILE_LAYER_CHUNK_SIZE; + chunk->width = (uint16_t) (((uint32_t) roomWidth - (uint32_t) chunk->x) > N3DS_TILE_LAYER_CHUNK_SIZE ? N3DS_TILE_LAYER_CHUNK_SIZE : ((uint32_t) roomWidth - (uint32_t) chunk->x)); + chunk->height = (uint16_t) (((uint32_t) roomHeight - (uint32_t) chunk->y) > N3DS_TILE_LAYER_CHUNK_SIZE ? N3DS_TILE_LAYER_CHUNK_SIZE : ((uint32_t) roomHeight - (uint32_t) chunk->y)); + if (chunk->width == 0 || chunk->height == 0) continue; + + if (!C3D_TexInit(&chunk->texture, N3DS_TILE_LAYER_CHUNK_SIZE, N3DS_TILE_LAYER_CHUNK_SIZE, GPU_RGBA5551)) { + continue; + } + chunk->allocated = true; + C3D_TexSetFilter(&chunk->texture, GPU_NEAREST, GPU_NEAREST); + C3D_TexSetWrap(&chunk->texture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE); + chunk->target = C3D_RenderTargetCreateFromTex(&chunk->texture, GPU_TEXFACE_2D, 0, -1); + if (chunk->target == NULL) { + C3D_TexDelete(&chunk->texture); + memset(&chunk->texture, 0, sizeof(chunk->texture)); + chunk->allocated = false; + continue; + } + + chunk->subtex.width = chunk->width; + chunk->subtex.height = chunk->height; + chunk->subtex.left = 0.0f; + chunk->subtex.top = 1.0f; + chunk->subtex.right = (float) chunk->width / (float) N3DS_TILE_LAYER_CHUNK_SIZE; + chunk->subtex.bottom = 1.0f - ((float) chunk->height / (float) N3DS_TILE_LAYER_CHUNK_SIZE); + chunk->image.tex = &chunk->texture; + chunk->image.subtex = &chunk->subtex; + + C2D_SceneBegin(chunk->target); + renderer->activeSceneTarget = N3DS_SCENE_TARGET_NONE; + C2D_TargetClear(chunk->target, C2D_Color32(0, 0, 0, 0)); + + int32_t startX = chunk->x / (int32_t) cache->tileWidth; + int32_t startY = chunk->y / (int32_t) cache->tileHeight; + int32_t endX = (int32_t) (((int64_t) chunk->x + (int64_t) chunk->width + (int64_t) cache->tileWidth - 1) / (int64_t) cache->tileWidth); + int32_t endY = (int32_t) (((int64_t) chunk->y + (int64_t) chunk->height + (int64_t) cache->tileHeight - 1) / (int64_t) cache->tileHeight); + if (startX < 0) startX = 0; + if (startY < 0) startY = 0; + if (endX > (int32_t) cache->tilesX) endX = (int32_t) cache->tilesX; + if (endY > (int32_t) cache->tilesY) endY = (int32_t) cache->tilesY; + + bool drewAny = false; + for (int32_t ty = startY; ty < endY; ty++) { + const TileLayerCacheRow* rowCache = &cache->rows[ty]; + if (rowCache->count == 0) continue; + + const TileLayerCacheCell* rowCells = &cache->cells[rowCache->start]; + uint32_t startCell = 0; + while (startCell < rowCache->count && (int32_t) rowCells[startCell].tileX < startX) startCell++; + for (uint32_t ci = startCell; ci < rowCache->count; ci++) { + const TileLayerCacheCell* cachedCell = &rowCells[ci]; + if ((int32_t) cachedCell->tileX >= endX) break; + + float xscale = cachedCell->mirror ? -1.0f : 1.0f; + float yscale = cachedCell->flip ? -1.0f : 1.0f; + float dstX = (float) ((int32_t) cachedCell->tileX * (int32_t) cache->tileWidth - chunk->x) + (cachedCell->mirror ? (float) cache->tileWidth : 0.0f); + float dstY = (float) (ty * (int32_t) cache->tileHeight - chunk->y) + (cachedCell->flip ? (float) cache->tileHeight : 0.0f); + + bool drewCell = false; + if (cachedCell->n3dsTileEntryIndex >= 0 && (uint32_t) cachedCell->n3dsTileEntryIndex < renderer->tileEntryCount) { + drewCell = N3DSRenderer_drawPackedTileEntry( + base, + renderer, + &renderer->tileEntries[cachedCell->n3dsTileEntryIndex], + dstX, + dstY, + xscale, + yscale, + 0x00FFFFFFu, + 1.0f + ); + } + + if (!drewCell && cachedCell->tpagIndex >= 0) { + drewCell = N3DSRenderer_tryDrawTileRect( + base, + cachedCell->tpagIndex, + (int32_t) cachedCell->srcX, + (int32_t) cachedCell->srcY, + (int32_t) cache->tileWidth, + (int32_t) cache->tileHeight, + dstX, + dstY, + xscale, + yscale, + 0x00FFFFFFu, + 1.0f + ); + } + + if (drewCell) drewAny = true; + } + } + + C2D_Flush(); + renderer->pendingC2DDraws = 0; + renderer->lastDrawTexture = NULL; + if (!drewAny) { + if (chunk->target != NULL) { + C3D_RenderTargetDelete(chunk->target); + chunk->target = NULL; + } + C3D_TexDelete(&chunk->texture); + memset(&chunk->texture, 0, sizeof(chunk->texture)); + chunk->allocated = false; + } else { + chunkCache->vramBytes += N3DS_TILE_LAYER_CHUNK_TEXTURE_BYTES; + } + } + } + + renderer->tileLayerChunkVRAMBytes += chunkCache->vramBytes; + + renderer->frameScaleX = savedFrameScaleX; + renderer->frameScaleY = savedFrameScaleY; + renderer->frameOffsetX = savedFrameOffsetX; + renderer->frameOffsetY = savedFrameOffsetY; + renderer->portOffsetX = savedPortOffsetX; + renderer->portOffsetY = savedPortOffsetY; + renderer->viewX = savedViewX; + renderer->viewY = savedViewY; + renderer->viewScaleX = savedViewScaleX; + renderer->viewScaleY = savedViewScaleY; + N3DSRenderer_applyBlendState(renderer); + N3DSRenderer_applyAlphaState(renderer); + if (savedSceneTarget == N3DS_SCENE_TARGET_BOTTOM) { + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_BOTTOM, true); + } else { + N3DSRenderer_sceneBeginTarget(renderer, N3DS_SCENE_TARGET_TOP, true); + } + + return cacheIndex; +} + +bool N3DSRenderer_drawTileLayerChunkCache(Renderer* base, int32_t cacheId, float layerOffsetX, float layerOffsetY, float alpha) { + if (base == NULL) return false; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (cacheId < 0 || (size_t) cacheId >= arrlenu(renderer->tileLayerChunkCaches)) return false; + N3DSTileLayerChunkCache* cache = &renderer->tileLayerChunkCaches[cacheId]; + if (!cache->used || cache->chunks == NULL) return false; + + float viewRight = (renderer->viewScaleX != 0.0f) ? ((float) renderer->viewX + ((float) N3DS_TOP_WIDTH / fabsf(renderer->viewScaleX))) : (float) renderer->viewX; + float viewBottom = (renderer->viewScaleY != 0.0f) ? ((float) renderer->viewY + ((float) N3DS_TOP_HEIGHT / fabsf(renderer->viewScaleY))) : (float) renderer->viewY; + bool drewAny = false; + + repeat(cache->chunkCount, i) { + N3DSTileLayerChunk* chunk = &cache->chunks[i]; + if (!chunk->allocated || chunk->target == NULL) continue; + + float localX = (float) chunk->x + layerOffsetX; + float localY = (float) chunk->y + layerOffsetY; + if (localX + (float) chunk->width <= (float) renderer->viewX || + localY + (float) chunk->height <= (float) renderer->viewY || + localX >= viewRight || + localY >= viewBottom) { + continue; + } + + renderer->frameSpriteDrawCalls++; + N3DSRenderer_drawImageFast(base, &chunk->image, localX, localY, 1.0f, 1.0f, 0x00FFFFFFu, alpha); + drewAny = true; + } + + return drewAny; +} + +void N3DSRenderer_destroyTileLayerChunkCache(Renderer* base, int32_t cacheId) { + if (base == NULL) return; + + N3DSRenderer* renderer = (N3DSRenderer*) base; + if (cacheId < 0 || (size_t) cacheId >= arrlenu(renderer->tileLayerChunkCaches)) return; + N3DSRenderer_freeTileLayerChunkCache(renderer, &renderer->tileLayerChunkCaches[cacheId]); +} diff --git a/src/n3ds/n3ds_renderer.h b/src/n3ds/n3ds_renderer.h new file mode 100644 index 00000000..aab13ff6 --- /dev/null +++ b/src/n3ds/n3ds_renderer.h @@ -0,0 +1,36 @@ +#pragma once + +#include "../renderer.h" + +Renderer* N3DSRenderer_create(void); +bool N3DSRenderer_isReady(Renderer* renderer); +const char* N3DSRenderer_getStartupError(Renderer* renderer); +void N3DSRenderer_beginOverlay(Renderer* renderer); +void N3DSRenderer_beginBottomScreenGUIEx(Renderer* renderer, int32_t guiW, int32_t guiH, float scaleX, float scaleY, float offsetX, float offsetY); +void N3DSRenderer_beginBottomScreenGUI(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endBottomScreenGUI(Renderer* renderer); +void N3DSRenderer_beginBottomScreenGUI2x(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endBottomScreenGUI2x(Renderer* renderer); +void N3DSRenderer_beginTopScreenGUI(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endTopScreenGUI(Renderer* renderer); +void N3DSRenderer_beginTopScreenGUI2x(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endTopScreenGUI2x(Renderer* renderer); +bool N3DSRenderer_isTopScreenGUIActive(Renderer* renderer); +bool N3DSRenderer_isTopScreenBattleViewActive(Renderer* renderer); +void N3DSRenderer_setTopScreenBattleViewActive(Renderer* renderer, bool active); +uint32_t N3DSRenderer_getResidentAtlasVRAMBytes(Renderer* renderer); +uint32_t N3DSRenderer_getResidentAtlasVRAMLimitBytes(Renderer* renderer); +uint32_t N3DSRenderer_getResidentAtlasPageCount(Renderer* renderer); +uint32_t N3DSRenderer_getResidentAtlasPageLimit(Renderer* renderer); +uint32_t N3DSRenderer_getResidentDirectAssetVRAMBytes(Renderer* renderer); +uint32_t N3DSRenderer_getResidentDirectAssetVRAMLimitBytes(Renderer* renderer); +uint32_t N3DSRenderer_getFrameFragmentDraws(Renderer* renderer); +uint32_t N3DSRenderer_getFrameSpriteDrawCalls(Renderer* renderer); +uint32_t N3DSRenderer_getFrameSpritePartDrawCalls(Renderer* renderer); +uint32_t N3DSRenderer_getFrameDirectSpriteHits(Renderer* renderer); +uint32_t N3DSRenderer_getFrameDirectAssetLoads(Renderer* renderer); +uint32_t N3DSRenderer_getFrameTextureSwitches(Renderer* renderer); +uint32_t N3DSRenderer_getFrameTextGlyphDraws(Renderer* renderer); +uint32_t N3DSRenderer_getFrameTextTenthsMs(Renderer* renderer); +int32_t N3DSRenderer_findTileEntryIndex(Renderer* renderer, int32_t backgroundIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH); +bool N3DSRenderer_drawCachedTileEntry(Renderer* renderer, int32_t tileEntryIndex, float drawX, float drawY, float xscale, float yscale, uint32_t color, float alpha); diff --git a/src/ps2/stb_impl.c b/src/n3ds/stb_impl.c similarity index 52% rename from src/ps2/stb_impl.c rename to src/n3ds/stb_impl.c index 9df32fd7..61070306 100644 --- a/src/ps2/stb_impl.c +++ b/src/n3ds/stb_impl.c @@ -1,2 +1,2 @@ #define STB_DS_IMPLEMENTATION -#include "stb_ds.h" +#include diff --git a/src/noop_audio_system.c b/src/noop_audio_system.c index 27b62be8..05ae141a 100644 --- a/src/noop_audio_system.c +++ b/src/noop_audio_system.c @@ -96,6 +96,7 @@ static AudioSystemVtable noopVtable = { .groupIsLoaded = noopGroupIsLoaded, .createStream = noopCreateStream, .destroyStream = noopDestroyStream, + .prewarmRoom = NULL, }; NoopAudioSystem* NoopAudioSystem_create(void) { diff --git a/src/noop_file_system.c b/src/noop_file_system.c index fc335fd8..c4e9036c 100644 --- a/src/noop_file_system.c +++ b/src/noop_file_system.c @@ -1,146 +1,146 @@ -#include "noop_file_system.h" -#include "utils.h" - -#include -#include - -#include "stb_ds.h" - -// ===[ In-Memory File Storage ]=== - -typedef struct { - char* key; // file path - char* value; // file contents -} MemoryFileEntry; - -typedef struct { - uint8_t* data; - int32_t size; -} MemoryBinaryData; - -typedef struct { - char* key; // file path - MemoryBinaryData value; -} MemoryBinaryEntry; - -typedef struct { - FileSystem base; - MemoryFileEntry* files; // stb_ds string hashmap - MemoryBinaryEntry* binaryFiles; // stb_ds string hashmap -} NoopFileSystem; - -// ===[ Vtable Implementations ]=== - -static char* noopResolvePath(MAYBE_UNUSED FileSystem* fs, MAYBE_UNUSED const char* relativePath) { - return safeStrdup("./"); -} - -static bool noopFileExists(FileSystem* fs, const char* relativePath) { - NoopFileSystem* nfs = (NoopFileSystem*) fs; - return shgeti(nfs->files, relativePath) >= 0; -} - -static char* noopReadFileText(FileSystem* fs, const char* relativePath) { - NoopFileSystem* nfs = (NoopFileSystem*) fs; - ptrdiff_t idx = shgeti(nfs->files, relativePath); - if (0 > idx) - return nullptr; - return safeStrdup(nfs->files[idx].value); -} - -static bool noopWriteFileText(FileSystem* fs, const char* relativePath, const char* contents) { - NoopFileSystem* nfs = (NoopFileSystem*) fs; - - // If the key already exists, free the old value before overwriting - ptrdiff_t idx = shgeti(nfs->files, relativePath); - if (idx >= 0) { - free(nfs->files[idx].value); - nfs->files[idx].value = safeStrdup(contents); - } else { - shput(nfs->files, relativePath, safeStrdup(contents)); - } - - return true; -} - -static bool noopDeleteFile(FileSystem* fs, const char* relativePath) { - NoopFileSystem* nfs = (NoopFileSystem*) fs; - ptrdiff_t idx = shgeti(nfs->files, relativePath); - if (0 > idx) - return false; - - free(nfs->files[idx].value); - shdel(nfs->files, relativePath); - return true; -} - -static bool noopReadFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) { - NoopFileSystem* nfs = (NoopFileSystem*) fs; - ptrdiff_t idx = shgeti(nfs->binaryFiles, relativePath); - if (0 > idx) - return false; - - MemoryBinaryData* entry = &nfs->binaryFiles[idx].value; - uint8_t* copy = safeMalloc((size_t) entry->size); - memcpy(copy, entry->data, (size_t) entry->size); - *outData = copy; - *outSize = entry->size; - return true; -} - -static bool noopWriteFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) { - NoopFileSystem* nfs = (NoopFileSystem*) fs; - - ptrdiff_t idx = shgeti(nfs->binaryFiles, relativePath); - if (idx >= 0) { - free(nfs->binaryFiles[idx].value.data); - uint8_t* copy = safeMalloc((size_t) size); - memcpy(copy, data, (size_t) size); - nfs->binaryFiles[idx].value.data = copy; - nfs->binaryFiles[idx].value.size = size; - } else { - uint8_t* copy = safeMalloc((size_t) size); - memcpy(copy, data, (size_t) size); - MemoryBinaryData binaryData = { .data = copy, .size = size }; - shput(nfs->binaryFiles, relativePath, binaryData); - } - - return true; -} - -// ===[ Vtable ]=== - -static FileSystemVtable noopFileSystemVtable = { - .resolvePath = noopResolvePath, - .fileExists = noopFileExists, - .readFileText = noopReadFileText, - .writeFileText = noopWriteFileText, - .deleteFile = noopDeleteFile, - .readFileBinary = noopReadFileBinary, - .writeFileBinary = noopWriteFileBinary, -}; - -// ===[ Lifecycle ]=== - -FileSystem* NoopFileSystem_create(void) { - NoopFileSystem* nfs = safeCalloc(1, sizeof(NoopFileSystem)); - nfs->base.vtable = &noopFileSystemVtable; - nfs->files = nullptr; - sh_new_strdup(nfs->files); - nfs->binaryFiles = nullptr; - sh_new_strdup(nfs->binaryFiles); - return (FileSystem*) nfs; -} - -void NoopFileSystem_destroy(FileSystem* fs) { - NoopFileSystem* nfs = (NoopFileSystem*) fs; - repeat(shlen(nfs->files), i) { - free(nfs->files[i].value); - } - shfree(nfs->files); - repeat(shlen(nfs->binaryFiles), i) { - free(nfs->binaryFiles[i].value.data); - } - shfree(nfs->binaryFiles); - free(nfs); -} +#include "noop_file_system.h" +#include "utils.h" + +#include +#include + +#include "stb_ds.h" + +// ===[ In-Memory File Storage ]=== + +typedef struct { + char* key; // file path + char* value; // file contents +} MemoryFileEntry; + +typedef struct { + uint8_t* data; + int32_t size; +} MemoryBinaryData; + +typedef struct { + char* key; // file path + MemoryBinaryData value; +} MemoryBinaryEntry; + +typedef struct { + FileSystem base; + MemoryFileEntry* files; // stb_ds string hashmap + MemoryBinaryEntry* binaryFiles; // stb_ds string hashmap +} NoopFileSystem; + +// ===[ Vtable Implementations ]=== + +static char* noopResolvePath(MAYBE_UNUSED FileSystem* fs, MAYBE_UNUSED const char* relativePath) { + return safeStrdup("./"); +} + +static bool noopFileExists(FileSystem* fs, const char* relativePath) { + NoopFileSystem* nfs = (NoopFileSystem*) fs; + return shgeti(nfs->files, relativePath) >= 0; +} + +static char* noopReadFileText(FileSystem* fs, const char* relativePath) { + NoopFileSystem* nfs = (NoopFileSystem*) fs; + ptrdiff_t idx = shgeti(nfs->files, relativePath); + if (0 > idx) + return nullptr; + return safeStrdup(nfs->files[idx].value); +} + +static bool noopWriteFileText(FileSystem* fs, const char* relativePath, const char* contents) { + NoopFileSystem* nfs = (NoopFileSystem*) fs; + + // If the key already exists, free the old value before overwriting + ptrdiff_t idx = shgeti(nfs->files, relativePath); + if (idx >= 0) { + free(nfs->files[idx].value); + nfs->files[idx].value = safeStrdup(contents); + } else { + shput(nfs->files, relativePath, safeStrdup(contents)); + } + + return true; +} + +static bool noopDeleteFile(FileSystem* fs, const char* relativePath) { + NoopFileSystem* nfs = (NoopFileSystem*) fs; + ptrdiff_t idx = shgeti(nfs->files, relativePath); + if (0 > idx) + return false; + + free(nfs->files[idx].value); + shdel(nfs->files, relativePath); + return true; +} + +static bool noopReadFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) { + NoopFileSystem* nfs = (NoopFileSystem*) fs; + ptrdiff_t idx = shgeti(nfs->binaryFiles, relativePath); + if (0 > idx) + return false; + + MemoryBinaryData* entry = &nfs->binaryFiles[idx].value; + uint8_t* copy = safeMalloc((size_t) entry->size); + memcpy(copy, entry->data, (size_t) entry->size); + *outData = copy; + *outSize = entry->size; + return true; +} + +static bool noopWriteFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) { + NoopFileSystem* nfs = (NoopFileSystem*) fs; + + ptrdiff_t idx = shgeti(nfs->binaryFiles, relativePath); + if (idx >= 0) { + free(nfs->binaryFiles[idx].value.data); + uint8_t* copy = safeMalloc((size_t) size); + memcpy(copy, data, (size_t) size); + nfs->binaryFiles[idx].value.data = copy; + nfs->binaryFiles[idx].value.size = size; + } else { + uint8_t* copy = safeMalloc((size_t) size); + memcpy(copy, data, (size_t) size); + MemoryBinaryData binaryData = { .data = copy, .size = size }; + shput(nfs->binaryFiles, relativePath, binaryData); + } + + return true; +} + +// ===[ Vtable ]=== + +static FileSystemVtable noopFileSystemVtable = { + .resolvePath = noopResolvePath, + .fileExists = noopFileExists, + .readFileText = noopReadFileText, + .writeFileText = noopWriteFileText, + .deleteFile = noopDeleteFile, + .readFileBinary = noopReadFileBinary, + .writeFileBinary = noopWriteFileBinary, +}; + +// ===[ Lifecycle ]=== + +FileSystem* NoopFileSystem_create(void) { + NoopFileSystem* nfs = safeCalloc(1, sizeof(NoopFileSystem)); + nfs->base.vtable = &noopFileSystemVtable; + nfs->files = nullptr; + sh_new_strdup(nfs->files); + nfs->binaryFiles = nullptr; + sh_new_strdup(nfs->binaryFiles); + return (FileSystem*) nfs; +} + +void NoopFileSystem_destroy(FileSystem* fs) { + NoopFileSystem* nfs = (NoopFileSystem*) fs; + repeat(shlen(nfs->files), i) { + free(nfs->files[i].value); + } + shfree(nfs->files); + repeat(shlen(nfs->binaryFiles), i) { + free(nfs->binaryFiles[i].value.data); + } + shfree(nfs->binaryFiles); + free(nfs); +} diff --git a/src/noop_file_system.h b/src/noop_file_system.h index 9f3eb839..1ae29fa8 100644 --- a/src/noop_file_system.h +++ b/src/noop_file_system.h @@ -1,10 +1,10 @@ -#pragma once - -#include "common.h" -#include "file_system.h" - -// Creates an in-memory FileSystem backed by a hashmap instead of real disk I/O -// Files written via writeFileText are kept in memory and can be read back -// Use this as a fallback while you don't have a proper file system implementation for your target! -FileSystem* NoopFileSystem_create(void); -void NoopFileSystem_destroy(FileSystem* fs); +#pragma once + +#include "common.h" +#include "file_system.h" + +// Creates an in-memory FileSystem backed by a hashmap instead of real disk I/O +// Files written via writeFileText are kept in memory and can be read back +// Use this as a fallback while you don't have a proper file system implementation for your target! +FileSystem* NoopFileSystem_create(void); +void NoopFileSystem_destroy(FileSystem* fs); diff --git a/src/profiler.c b/src/profiler.c index e98a80ff..da3134e0 100644 --- a/src/profiler.c +++ b/src/profiler.c @@ -1,177 +1,177 @@ -#include "profiler.h" - -#include -#include -#include - -#include "utils.h" -#include "stb_ds.h" -#include "string_builder.h" - -#if defined(PLATFORM_PS2) -#include -#elif defined(_WIN32) -#include -#else -#include -#endif - -static uint64_t nowNanos(void) { -#if defined(PLATFORM_PS2) - // kBUSCLK is bus clock ticks per second (~147 MHz). - // Split to avoid u64 overflow in ticks * 1e9. - uint64_t t = (uint64_t) GetTimerSystemTime(); - uint64_t clk = (uint64_t) kBUSCLK; - uint64_t sec = t / clk; - uint64_t rem = t % clk; - return sec * 1000000000ull + (rem * 1000000000ull) / clk; -#elif defined(_WIN32) - static LARGE_INTEGER freq; - static bool freqInitialized = false; - if (!freqInitialized) { - QueryPerformanceFrequency(&freq); - freqInitialized = true; - } - LARGE_INTEGER now; - QueryPerformanceCounter(&now); - uint64_t t = (uint64_t) now.QuadPart; - uint64_t f = (uint64_t) freq.QuadPart; - uint64_t sec = t / f; - uint64_t rem = t % f; - return sec * 1000000000ull + (rem * 1000000000ull) / f; -#else - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (uint64_t) ts.tv_sec * 1000000000ull + (uint64_t) ts.tv_nsec; -#endif -} - -Profiler* Profiler_create(void) { - Profiler* p = safeMalloc(sizeof(Profiler)); - p->entries = nullptr; - p->frameDepth = 0; - p->instructionCount = 0; - return p; -} - -void Profiler_destroy(Profiler* p) { - if (p == nullptr) return; - shfree(p->entries); - free(p); -} - -void Profiler_setEnabled(Profiler** slot, bool enabled) { - if (enabled) { - if (*slot == nullptr) *slot = Profiler_create(); - } else { - if (*slot != nullptr) { - Profiler_destroy(*slot); - *slot = nullptr; - } - } -} - -void Profiler_enter(Profiler* p, const char* name) { - if (p == nullptr) return; - if (p->frameDepth >= PROFILER_MAX_DEPTH) return; - ProfilerFrame* f = &p->frameStack[p->frameDepth]; - f->startNanos = nowNanos(); - f->childNanos = 0; - f->startOps = p->instructionCount; - f->childOps = 0; - f->name = name != nullptr ? name : ""; - p->frameDepth++; -} - -void Profiler_exit(Profiler* p) { - if (p == nullptr) return; - if (0 >= p->frameDepth) return; - p->frameDepth--; - ProfilerFrame* f = &p->frameStack[p->frameDepth]; - uint64_t elapsed = nowNanos() - f->startNanos; - uint64_t selfNanos = elapsed > f->childNanos ? elapsed - f->childNanos : 0; - uint64_t totalOps = p->instructionCount - f->startOps; - uint64_t selfOps = totalOps > f->childOps ? totalOps - f->childOps : 0; - - ptrdiff_t i = shgeti(p->entries, f->name); - if (0 > i) { - ProfilerStats stats = { .nanos = selfNanos, .ops = selfOps }; - shput(p->entries, f->name, stats); - } else { - p->entries[i].value.nanos += selfNanos; - p->entries[i].value.ops += selfOps; - } - - if (p->frameDepth > 0) { - p->frameStack[p->frameDepth - 1].childNanos += elapsed; - p->frameStack[p->frameDepth - 1].childOps += totalOps; - } -} - -static int compareEntriesDesc(const void* a, const void* b) { - uint64_t va = ((const ProfilerEntry*) a)->value.nanos; - uint64_t vb = ((const ProfilerEntry*) b)->value.nanos; - if (vb > va) return 1; - if (va > vb) return -1; - return 0; -} - -// Sort entries into a caller-owned buffer. Returns entry count; 0 if nothing to report. -// Also computes the grand total (across all entries, not just topN) in *outTotal. -static size_t collectSorted(const Profiler* p, ProfilerEntry* outSorted, size_t outCap, ProfilerStats* outTotal) { - size_t count = shlen(p->entries); - if (count == 0) return 0; - if (count > outCap) count = outCap; - memcpy(outSorted, p->entries, count * sizeof(ProfilerEntry)); - qsort(outSorted, count, sizeof(ProfilerEntry), compareEntriesDesc); - - ProfilerStats total = { 0 }; - size_t fullCount = shlen(p->entries); - repeat(fullCount, i) { - total.nanos += p->entries[i].value.nanos; - total.ops += p->entries[i].value.ops; - } - *outTotal = total; - return count; -} - -void Profiler_reset(Profiler* p) { - if (p == nullptr) return; - shfree(p->entries); - p->entries = nullptr; -} - -char* Profiler_createReport(const Profiler* p, int topN, int framesInWindow) { - if (p == nullptr) return nullptr; - size_t count = shlen(p->entries); - if (count == 0) return nullptr; - if (0 >= framesInWindow) framesInWindow = 1; - - ProfilerEntry* sorted = (ProfilerEntry*) malloc(count * sizeof(ProfilerEntry)); - if (sorted == nullptr) return nullptr; - ProfilerStats total = { 0 }; - size_t sortedEntriesCount = collectSorted(p, sorted, count, &total); - - size_t limit = sortedEntriesCount; - if (topN > 0 && (size_t) topN < limit) - limit = (size_t) topN; - - StringBuilder stringBuilder = StringBuilder_create(64); - - double frames = (double) framesInWindow; - double totalMs = total.nanos / 1000000.0; - double totalOpsPerFrame = (double) total.ops / frames; - - StringBuilder_appendFormat(&stringBuilder, "GML Profiler (avg %d frames)\n", framesInWindow); - repeat(limit, i) { - double perFrameMs = ((double) sorted[i].value.nanos / (double) 1000000) / frames; - double opsPerFrame = (double) sorted[i].value.ops / frames; - double nsPerOp = sorted[i].value.ops > 0 ? (double) sorted[i].value.nanos / (double) sorted[i].value.ops : (double) 0; - StringBuilder_appendFormat(&stringBuilder, "%.2fms %.0f ops (%.0f ns/op) %s\n", perFrameMs, opsPerFrame, nsPerOp, sorted[i].key); - } - StringBuilder_appendFormat(&stringBuilder, "total %.2fms/frame, %.0f ops/frame (%zu scripts)", totalMs / frames, totalOpsPerFrame, sortedEntriesCount); - char* result = StringBuilder_toString(&stringBuilder); - StringBuilder_free(&stringBuilder); - free(sorted); - return result; -} +#include "profiler.h" + +#include +#include +#include + +#include "utils.h" +#include "stb_ds.h" +#include "string_builder.h" + +#if defined(PLATFORM_PS2) +#include +#elif defined(_WIN32) +#include +#else +#include +#endif + +static uint64_t nowNanos(void) { +#if defined(PLATFORM_PS2) + // kBUSCLK is bus clock ticks per second (~147 MHz). + // Split to avoid u64 overflow in ticks * 1e9. + uint64_t t = (uint64_t) GetTimerSystemTime(); + uint64_t clk = (uint64_t) kBUSCLK; + uint64_t sec = t / clk; + uint64_t rem = t % clk; + return sec * 1000000000ull + (rem * 1000000000ull) / clk; +#elif defined(_WIN32) + static LARGE_INTEGER freq; + static bool freqInitialized = false; + if (!freqInitialized) { + QueryPerformanceFrequency(&freq); + freqInitialized = true; + } + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + uint64_t t = (uint64_t) now.QuadPart; + uint64_t f = (uint64_t) freq.QuadPart; + uint64_t sec = t / f; + uint64_t rem = t % f; + return sec * 1000000000ull + (rem * 1000000000ull) / f; +#else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t) ts.tv_sec * 1000000000ull + (uint64_t) ts.tv_nsec; +#endif +} + +Profiler* Profiler_create(void) { + Profiler* p = safeMalloc(sizeof(Profiler)); + p->entries = nullptr; + p->frameDepth = 0; + p->instructionCount = 0; + return p; +} + +void Profiler_destroy(Profiler* p) { + if (p == nullptr) return; + shfree(p->entries); + free(p); +} + +void Profiler_setEnabled(Profiler** slot, bool enabled) { + if (enabled) { + if (*slot == nullptr) *slot = Profiler_create(); + } else { + if (*slot != nullptr) { + Profiler_destroy(*slot); + *slot = nullptr; + } + } +} + +void Profiler_enter(Profiler* p, const char* name) { + if (p == nullptr) return; + if (p->frameDepth >= PROFILER_MAX_DEPTH) return; + ProfilerFrame* f = &p->frameStack[p->frameDepth]; + f->startNanos = nowNanos(); + f->childNanos = 0; + f->startOps = p->instructionCount; + f->childOps = 0; + f->name = name != nullptr ? name : ""; + p->frameDepth++; +} + +void Profiler_exit(Profiler* p) { + if (p == nullptr) return; + if (0 >= p->frameDepth) return; + p->frameDepth--; + ProfilerFrame* f = &p->frameStack[p->frameDepth]; + uint64_t elapsed = nowNanos() - f->startNanos; + uint64_t selfNanos = elapsed > f->childNanos ? elapsed - f->childNanos : 0; + uint64_t totalOps = p->instructionCount - f->startOps; + uint64_t selfOps = totalOps > f->childOps ? totalOps - f->childOps : 0; + + ptrdiff_t i = shgeti(p->entries, f->name); + if (0 > i) { + ProfilerStats stats = { .nanos = selfNanos, .ops = selfOps }; + shput(p->entries, f->name, stats); + } else { + p->entries[i].value.nanos += selfNanos; + p->entries[i].value.ops += selfOps; + } + + if (p->frameDepth > 0) { + p->frameStack[p->frameDepth - 1].childNanos += elapsed; + p->frameStack[p->frameDepth - 1].childOps += totalOps; + } +} + +static int compareEntriesDesc(const void* a, const void* b) { + uint64_t va = ((const ProfilerEntry*) a)->value.nanos; + uint64_t vb = ((const ProfilerEntry*) b)->value.nanos; + if (vb > va) return 1; + if (va > vb) return -1; + return 0; +} + +// Sort entries into a caller-owned buffer. Returns entry count; 0 if nothing to report. +// Also computes the grand total (across all entries, not just topN) in *outTotal. +static size_t collectSorted(const Profiler* p, ProfilerEntry* outSorted, size_t outCap, ProfilerStats* outTotal) { + size_t count = shlen(p->entries); + if (count == 0) return 0; + if (count > outCap) count = outCap; + memcpy(outSorted, p->entries, count * sizeof(ProfilerEntry)); + qsort(outSorted, count, sizeof(ProfilerEntry), compareEntriesDesc); + + ProfilerStats total = { 0 }; + size_t fullCount = shlen(p->entries); + repeat(fullCount, i) { + total.nanos += p->entries[i].value.nanos; + total.ops += p->entries[i].value.ops; + } + *outTotal = total; + return count; +} + +void Profiler_reset(Profiler* p) { + if (p == nullptr) return; + shfree(p->entries); + p->entries = nullptr; +} + +char* Profiler_createReport(const Profiler* p, int topN, int framesInWindow) { + if (p == nullptr) return nullptr; + size_t count = shlen(p->entries); + if (count == 0) return nullptr; + if (0 >= framesInWindow) framesInWindow = 1; + + ProfilerEntry* sorted = (ProfilerEntry*) malloc(count * sizeof(ProfilerEntry)); + if (sorted == nullptr) return nullptr; + ProfilerStats total = { 0 }; + size_t sortedEntriesCount = collectSorted(p, sorted, count, &total); + + size_t limit = sortedEntriesCount; + if (topN > 0 && (size_t) topN < limit) + limit = (size_t) topN; + + StringBuilder stringBuilder = StringBuilder_create(64); + + double frames = (double) framesInWindow; + double totalMs = total.nanos / 1000000.0; + double totalOpsPerFrame = (double) total.ops / frames; + + StringBuilder_appendFormat(&stringBuilder, "GML Profiler (avg %d frames)\n", framesInWindow); + repeat(limit, i) { + double perFrameMs = ((double) sorted[i].value.nanos / (double) 1000000) / frames; + double opsPerFrame = (double) sorted[i].value.ops / frames; + double nsPerOp = sorted[i].value.ops > 0 ? (double) sorted[i].value.nanos / (double) sorted[i].value.ops : (double) 0; + StringBuilder_appendFormat(&stringBuilder, "%.2fms %.0f ops (%.0f ns/op) %s\n", perFrameMs, opsPerFrame, nsPerOp, sorted[i].key); + } + StringBuilder_appendFormat(&stringBuilder, "total %.2fms/frame, %.0f ops/frame (%zu scripts)", totalMs / frames, totalOpsPerFrame, sortedEntriesCount); + char* result = StringBuilder_toString(&stringBuilder); + StringBuilder_free(&stringBuilder); + free(sorted); + return result; +} diff --git a/src/ps2/gs_renderer.c b/src/ps2/gs_renderer.c deleted file mode 100644 index 1239e684..00000000 --- a/src/ps2/gs_renderer.c +++ /dev/null @@ -1,2175 +0,0 @@ -#include "gs_renderer.h" - -#include -#include -#include -#include -#include -#include - -#include "binary_reader.h" -#include "binary_utils.h" -#include "utils.h" -#include "text_utils.h" -#include "ps2_utils.h" -#include "matrix_math.h" - -#ifdef ENABLE_PS2_RENDERER_LOGS -#define rendererPrintf(...) fprintf(stderr, __VA_ARGS__) -#else -#define rendererPrintf(...) ((void) 0) -#endif - -// ===[ Constants ]=== -#define ATLAS_WIDTH 512 -#define ATLAS_HEIGHT 512 -#define PS2_SCREEN_WIDTH 640.0f -#define PS2_SCREEN_HEIGHT 448.0f -#define TEX_HEADER_SIZE 128 -#define CLUT4_ENTRY_SIZE 64 // 16 colors * 4 bytes -#define CLUT8_ENTRY_SIZE 1024 // 256 colors * 4 bytes - -// ===[ File Loading Helper ]=== - -// Loads an entire file from host into a memalign'd buffer. Returns size via outSize. -// Aborts on failure. -static uint8_t* loadFileRaw(const char* path, uint32_t* outSize) { - char* textureBinPath = PS2Utils_createDevicePath(path); - - FILE* f = fopen(textureBinPath, "rb"); - if (f == nullptr) { - fprintf(stderr, "GsRenderer: Failed to open %s\n", path); - abort(); - } - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - // 128-byte aligned for DMA transfers - uint8_t* data = (uint8_t*) safeMemalign(128, (size_t) size); - - size_t read = fread(data, 1, (size_t) size, f); - fclose(f); - - if (read != (size_t) size) { - fprintf(stderr, "GsRenderer: Short read on %s (expected %ld, got %zu)\n", path, size, read); - abort(); - } - - *outSize = (uint32_t) size; - free(textureBinPath); - return data; -} - -// ===[ Atlas Loading ]=== -static void loadAtlas(GsRenderer* gs) { - char* atlasBinPath = PS2Utils_createDevicePath("ATLAS.BIN"); - FILE* f = fopen(atlasBinPath, "rb"); - if (f == nullptr) { - fprintf(stderr, "GsRenderer: Failed to open %s\n", atlasBinPath); - abort(); - } - - fseek(f, 0, SEEK_END); - size_t fileSize = (size_t) ftell(f); - fseek(f, 0, SEEK_SET); - - BinaryReader reader = BinaryReader_create(f, fileSize); - - uint8_t version = BinaryReader_readUint8(&reader); - if (version != 0) { - fprintf(stderr, "GsRenderer: Unsupported ATLAS.BIN version %u\n", version); - abort(); - } - - gs->atlasTPAGCount = BinaryReader_readUint16(&reader); - gs->atlasTileCount = BinaryReader_readUint16(&reader); - gs->atlasCount = BinaryReader_readUint16(&reader); - - // Parse atlas offset table - gs->atlasOffsets = safeMalloc(gs->atlasCount * sizeof(uint32_t)); - repeat(gs->atlasCount, i) { - gs->atlasOffsets[i] = BinaryReader_readUint32(&reader); - } - - // Parse TPAG entries - gs->atlasTPAGEntries = safeMalloc(gs->atlasTPAGCount * sizeof(AtlasTPAGEntry)); - - repeat(gs->atlasTPAGCount, i) { - AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[i]; - entry->atlasId = BinaryReader_readUint16(&reader); - entry->atlasX = BinaryReader_readUint16(&reader); - entry->atlasY = BinaryReader_readUint16(&reader); - entry->width = BinaryReader_readUint16(&reader); - entry->height = BinaryReader_readUint16(&reader); - entry->cropX = BinaryReader_readUint16(&reader); - entry->cropY = BinaryReader_readUint16(&reader); - entry->cropW = BinaryReader_readUint16(&reader); - entry->cropH = BinaryReader_readUint16(&reader); - entry->clutIndex = BinaryReader_readUint16(&reader); - entry->bpp = BinaryReader_readUint8(&reader); - } - - // Parse tile entries - gs->atlasTileEntries = safeMalloc(gs->atlasTileCount * sizeof(AtlasTileEntry)); - - repeat(gs->atlasTileCount, i) { - AtlasTileEntry* entry = &gs->atlasTileEntries[i]; - entry->bgDef = BinaryReader_readInt16(&reader); - entry->srcX = BinaryReader_readUint16(&reader); - entry->srcY = BinaryReader_readUint16(&reader); - entry->srcW = BinaryReader_readUint16(&reader); - entry->srcH = BinaryReader_readUint16(&reader); - entry->atlasId = BinaryReader_readUint16(&reader); - entry->atlasX = BinaryReader_readUint16(&reader); - entry->atlasY = BinaryReader_readUint16(&reader); - entry->width = BinaryReader_readUint16(&reader); - entry->height = BinaryReader_readUint16(&reader); - entry->cropX = BinaryReader_readUint16(&reader); - entry->cropY = BinaryReader_readUint16(&reader); - entry->cropW = BinaryReader_readUint16(&reader); - entry->cropH = BinaryReader_readUint16(&reader); - entry->clutIndex = BinaryReader_readUint16(&reader); - entry->bpp = BinaryReader_readUint8(&reader); - } - - fclose(f); - - // Build tile entry hashmap for O(1) lookup - gs->tileEntryMap = nullptr; - repeat(gs->atlasTileCount, i) { - AtlasTileEntry* entry = &gs->atlasTileEntries[i]; - TileLookupKey key = { .bgDef = entry->bgDef, .srcX = entry->srcX, .srcY = entry->srcY, .srcW = entry->srcW, .srcH = entry->srcH }; - hmput(gs->tileEntryMap, key, entry); - } - - gs->atlasBpp = safeCalloc(gs->atlasCount, sizeof(uint8_t)); - gs->atlasToChunk = safeMalloc(gs->atlasCount * sizeof(int16_t)); - repeat(gs->atlasCount, i) { - gs->atlasToChunk[i] = -1; - } - - // Build bpp table from TPAG and tile entries - repeat(gs->atlasTPAGCount, i) { - AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[i]; - if (entry->atlasId != 0xFFFF && gs->atlasCount > entry->atlasId) { - gs->atlasBpp[entry->atlasId] = entry->bpp; - } - } - repeat(gs->atlasTileCount, i) { - AtlasTileEntry* entry = &gs->atlasTileEntries[i]; - if (entry->atlasId != 0xFFFF && gs->atlasCount > entry->atlasId) { - gs->atlasBpp[entry->atlasId] = entry->bpp; - } - } - - fprintf(stderr, "GsRenderer: ATLAS.BIN loaded - %u TPAG entries, %u tile entries, %u atlases\n", gs->atlasTPAGCount, gs->atlasTileCount, gs->atlasCount); - - free(atlasBinPath); -} - -// ===[ CLUT Loading and VRAM Upload ]=== -// Each CLUT is uploaded individually to its own VRAM address. This is necessary because -// the PS2 GS VRAM has a block-swizzled layout - bulk-uploading stacked CLUTs and computing -// linear offsets for CBP does NOT work (the BITBLT write path and CLUT read path use -// block-based addressing, so CLUTs don't land at simple linear offsets within a bulk upload). -static void loadAndUploadCLUTs(GsRenderer* gs) { - GSGLOBAL* gsGlobal = gs->gsGlobal; - - // 128-byte aligned temp buffer for DMA transfers (reused for each CLUT send) - // Large enough for one 8bpp CLUT (1024 bytes) - uint8_t* tempBuf = (uint8_t*) safeMemalign(128, CLUT8_ENTRY_SIZE); - - // Load and upload CLUT4 (4bpp palettes: 16 colors * 4 bytes = 64 bytes each) - { - uint32_t clut4FileSize; - uint8_t* clut4Data = loadFileRaw("CLUT4.BIN", &clut4FileSize); - gs->clut4Count = clut4FileSize / CLUT4_ENTRY_SIZE; - fprintf(stderr, "GsRenderer: CLUT4.BIN loaded - %u CLUTs (%u bytes)\n", gs->clut4Count, clut4FileSize); - - gs->clut4VramAddrs = safeMalloc(gs->clut4Count * sizeof(uint32_t)); - - repeat(gs->clut4Count, i) { - // gsKit uploads 4bpp CLUTs as 8x2 CT32 (16 entries in 8-wide, 2-tall grid) - uint32_t vramSize = gsKit_texture_size(8, 2, GS_PSM_CT32); - uint32_t vramAddr = gsKit_vram_alloc(gsGlobal, vramSize, GSKIT_ALLOC_USERBUFFER); - if (vramAddr == GSKIT_ALLOC_ERROR) { - fprintf(stderr, "GsRenderer: Failed to allocate VRAM for CLUT4 index %u\n", i); - abort(); - } - - // Copy to aligned temp buffer for DMA - memcpy(tempBuf, clut4Data + i * CLUT4_ENTRY_SIZE, CLUT4_ENTRY_SIZE); - gsKit_texture_send((u32*) tempBuf, 8, 2, vramAddr, GS_PSM_CT32, 1, GS_CLUT_PALLETE); - gs->clut4VramAddrs[i] = vramAddr; - } - - fprintf(stderr, "GsRenderer: CLUT4 uploaded (%u CLUTs)\n", gs->clut4Count); - free(clut4Data); - } - - // Load and upload CLUT8 (8bpp palettes: 256 colors * 4 bytes = 1024 bytes each) - { - uint32_t clut8FileSize; - uint8_t* clut8Data = loadFileRaw("CLUT8.BIN", &clut8FileSize); - gs->clut8Count = clut8FileSize / CLUT8_ENTRY_SIZE; - fprintf(stderr, "GsRenderer: CLUT8.BIN loaded - %u CLUTs (%u bytes)\n", gs->clut8Count, clut8FileSize); - - gs->clut8VramAddrs = safeMalloc(gs->clut8Count * sizeof(uint32_t)); - - repeat(gs->clut8Count, i) { - // gsKit uploads 8bpp CLUTs as 16x16 CT32 (256 entries in 16-wide, 16-tall grid) - uint32_t vramSize = gsKit_texture_size(16, 16, GS_PSM_CT32); - uint32_t vramAddr = gsKit_vram_alloc(gsGlobal, vramSize, GSKIT_ALLOC_USERBUFFER); - if (vramAddr == GSKIT_ALLOC_ERROR) { - fprintf(stderr, "GsRenderer: Failed to allocate VRAM for CLUT8 index %u\n", i); - abort(); - } - - // 8bpp CLUTs are 1024 bytes; source is 128-byte aligned (1024 is a multiple of 128) - gsKit_texture_send((u32*) (clut8Data + i * CLUT8_ENTRY_SIZE), 16, 16, vramAddr, GS_PSM_CT32, 1, GS_CLUT_PALLETE); - gs->clut8VramAddrs[i] = vramAddr; - } - - fprintf(stderr, "GsRenderer: CLUT8 uploaded (%u CLUTs)\n", gs->clut8Count); - free(clut8Data); - } - - free(tempBuf); - - fprintf(stderr, "GsRenderer: VRAM after CLUTs: 0x%08X / 0x%08X\n", gsGlobal->CurrentPointer, GS_VRAM_SIZE); -} - -// ===[ VRAM Texture Cache (Buddy System with LRU Eviction) ]=== -// Manages a pool of 128KB VRAM chunks for atlas textures. -// 4bpp atlases use 1 chunk, 8bpp atlases use 2 consecutive chunks. - -#define FONTM_RESERVED_VRAM 65536 // 64KB reserved for gsKit's TexManager (FONTM debug overlay) - -// Initializes the chunk pool from the remaining VRAM after CLUTs. -// Reserves 64KB at the end for gsKit's TexManager (used by FONTM). -// Our chunk pool occupies the middle, between CLUTs and the FONTM region. -// -// VRAM layout: [Framebuffers] [CLUTs] [Chunk Pool ...] [64KB FONTM] -static void initTextureCache(GsRenderer* gs) { - gs->textureVramBase = gs->gsGlobal->CurrentPointer; - uint32_t availableVram = GS_VRAM_SIZE - gs->textureVramBase - FONTM_RESERVED_VRAM; - gs->chunkCount = availableVram / VRAM_CHUNK_SIZE; - - gs->chunks = safeMalloc(gs->chunkCount * sizeof(VRAMChunk)); - forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) { - chunk->atlasId = -1; - chunk->lastUsed = 0; - } - - gs->frameCounter = 1; - - // Advance CurrentPointer past our chunk pool so the TexManager only - // manages the 64KB FONTM region at the end of VRAM. - gs->gsGlobal->CurrentPointer = gs->textureVramBase + gs->chunkCount * VRAM_CHUNK_SIZE; - gsKit_TexManager_init(gs->gsGlobal); - - uint32_t fontmVram = GS_VRAM_SIZE - gs->gsGlobal->CurrentPointer; - fprintf(stderr, "GsRenderer: Texture cache initialized - %u chunks (%u KB each), base 0x%08X, %u KB for textures, %u KB for FONTM\n", gs->chunkCount, VRAM_CHUNK_SIZE / 1024, gs->textureVramBase, gs->chunkCount * (VRAM_CHUNK_SIZE / 1024), fontmVram / 1024); -} - -// Find the first run of consecutive free chunks. -// Returns the index of the first chunk, or -1 if not found. -static int32_t findConsecutiveFreeChunks(GsRenderer* gs, int chunksNeeded) { - int consecutive = 0; - forEachIndexed(VRAMChunk, chunk, i, gs->chunks, gs->chunkCount) { - if (0 > chunk->atlasId) { - consecutive++; - if (consecutive >= chunksNeeded) { - return (int32_t) (i - (uint32_t) chunksNeeded + 1); - } - } else { - consecutive = 0; - } - } - return -1; -} - -// Count total free chunks. -static uint32_t countFreeChunks(GsRenderer* gs) { - uint32_t count = 0; - forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) { - if (0 > chunk->atlasId) - count++; - } - return count; -} - -// Find the atlas with the oldest lastUsed time (LRU victim). -// Returns the atlasId, or -1 if no loaded atlases. -static int16_t findLRUVictim(GsRenderer* gs, bool* wasUsedOnThisFrame) { - uint64_t oldest = UINT64_MAX; - int16_t victimAtlas = -1; - forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) { - if (chunk->atlasId >= 0 && oldest > chunk->lastUsed) { - oldest = chunk->lastUsed; - victimAtlas = chunk->atlasId; - } - } - if (victimAtlas != -1) - *wasUsedOnThisFrame = oldest == gs->frameCounter; - return victimAtlas; -} - -// Evict an atlas from the cache, freeing its chunk(s). -static void evictAtlas(GsRenderer* gs, int16_t atlasId) { - forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) { - if (chunk->atlasId == atlasId) { - chunk->atlasId = -1; - chunk->lastUsed = 0; - } - } - - if (atlasId >= 0 && gs->atlasCount > (uint16_t) atlasId) { - gs->atlasToChunk[atlasId] = -1; - } - - uint32_t availableChunks = countFreeChunks(gs); - rendererPrintf("GsRenderer: Evicted atlas %d from VRAM (available chunks = %d)\n", atlasId, availableChunks); -} - -// Defragment the texture cache by evicting all loaded atlases. -// They will be reloaded on-demand as needed during subsequent draw calls. -static void defragTextureCache(GsRenderer* gs) { - rendererPrintf("GsRenderer: Defragmenting VRAM texture cache...\n"); - - forEach(VRAMChunk, chunk, gs->chunks, gs->chunkCount) { - chunk->atlasId = -1; - chunk->lastUsed = 0; - } - - repeat(gs->atlasCount, i) { - gs->atlasToChunk[i] = -1; - } - - rendererPrintf("GsRenderer: Defrag complete - all %u chunks freed\n", gs->chunkCount); -} - -// Allocate consecutive chunks for an atlas. Evicts LRU victims or defrags if needed. -// Returns the first chunk index, or -1 if VRAM is truly exhausted. -static int32_t allocateChunks(GsRenderer* gs, int chunksNeeded) { - // Attempt 1: find free consecutive chunks - int32_t idx = findConsecutiveFreeChunks(gs, chunksNeeded); - if (idx >= 0) return idx; - - // Attempt 2: evict LRU victims one at a time until space is found - repeat(gs->chunkCount, attempts) { - bool wasUsedOnThisFrame = false; - - int16_t victim = findLRUVictim(gs, &wasUsedOnThisFrame); - if (0 > victim) - break; - - // We only need to flush if the victim was used on this frame - // If it wasn't, then we can evict with no care in the world - if (wasUsedOnThisFrame) { - rendererPrintf("GsRenderer: Flushing draw queue before VRAM evicting because atlas was used on the current frame\n"); - gs->evictedAtlasUsedInCurrentFrame = true; - gsKit_queue_exec(gs->gsGlobal); - } - - evictAtlas(gs, victim); - - idx = findConsecutiveFreeChunks(gs, chunksNeeded); - - if (idx >= 0) - return idx; - } - - // At this point we are lost, just flush and hope for the best - gs->evictedAtlasUsedInCurrentFrame = true; - rendererPrintf("GsRenderer: Flushing draw queue before VRAM defrag\n"); - gsKit_queue_exec(gs->gsGlobal); - - // Attempt 3: defrag - evict ALL and let them reload on demand - // Handles fragmentation where enough free chunks exist but aren't consecutive - if (countFreeChunks(gs) >= (uint32_t) chunksNeeded) { - defragTextureCache(gs); - idx = findConsecutiveFreeChunks(gs, chunksNeeded); - - if (idx >= 0) - return idx; - } - - // VRAM truly exhausted - return -1; -} - -// ===[ EE RAM Atlas Cache (Bump Allocator with LRU Eviction + Compaction) ]=== -// Caches uncompressed atlas pixel data in a EE RAM buffer, allowingzero-copy DMA uploads to VRAM without per-upload decompression or temp allocations. - -#define EE_CACHE_CAPACITY (2 * 1024 * 1024) // 2 MiB - -// Uncompressed pixel data size for a 512x512 atlas at the given bpp. -static uint32_t atlasUncompressedSize(uint8_t bpp) { - return (bpp == 4) ? (ATLAS_WIDTH * ATLAS_HEIGHT / 2) : (ATLAS_WIDTH * ATLAS_HEIGHT); -} - -// Decompress atlas pixel data from a compressed buffer (TEX_HEADER_SIZE header + RLE/raw payload). -// Writes uncompressed indexed pixels into outBuf (must be large enough and 128-byte aligned). -static void decompressAtlasPixels(const uint8_t* compressedData, uint8_t* outBuf) { - uint16_t width = BinaryUtils_readUint16(compressedData + 1); - uint16_t height = BinaryUtils_readUint16(compressedData + 3); - uint8_t bpp = BinaryUtils_readUint8(compressedData + 5); - uint32_t pixelDataSize = BinaryUtils_readUint32(compressedData + 6); - uint8_t compressionType = BinaryUtils_readUint8(compressedData + 10); - - uint32_t uncompressedSize = (bpp == 4) ? (uint32_t) ((width * height + 1) / 2) : (uint32_t) (width * height); - const uint8_t* rawData = compressedData + TEX_HEADER_SIZE; - - if (compressionType == 1) { - // RLE decompression - uint32_t srcPos = 0, dstPos = 0; - while (pixelDataSize > srcPos + 1 && uncompressedSize > dstPos) { - uint8_t runLength = rawData[srcPos++]; - uint8_t value = rawData[srcPos++]; - for (uint8_t j = 0; runLength > j && uncompressedSize > dstPos; j++) { - outBuf[dstPos++] = value; - } - } - } else { - memcpy(outBuf, rawData, uncompressedSize); - } -} - -// Initialize the EE RAM cache. Called from gsInit after opening TEXTURES.BIN. -static void initEeCache(GsRenderer* gs) { - gs->eeCacheCapacity = EE_CACHE_CAPACITY; - gs->eeCacheBumpPtr = 0; - gs->eeCache = (uint8_t*) safeMemalign(128, EE_CACHE_CAPACITY); - - gs->eeCacheEntries = safeMalloc(gs->atlasCount * sizeof(EeAtlasCacheEntry)); - repeat(gs->atlasCount, i) { - gs->eeCacheEntries[i].atlasId = -1; - gs->eeCacheEntries[i].offset = 0; - gs->eeCacheEntries[i].size = 0; - gs->eeCacheEntries[i].lastUsed = 0; - } - - // Compute on-disk sizes from offset table - gs->atlasDataSizes = safeMalloc(gs->atlasCount * sizeof(uint32_t)); - - // Get total file size for the last atlas - fseek(gs->texturesFile, 0, SEEK_END); - uint32_t texturesFileSize = (uint32_t) ftell(gs->texturesFile); - - repeat(gs->atlasCount, i) { - if (gs->atlasCount - 1 > i) { - gs->atlasDataSizes[i] = gs->atlasOffsets[i + 1] - gs->atlasOffsets[i]; - } else { - gs->atlasDataSizes[i] = texturesFileSize - gs->atlasOffsets[i]; - } - } -} - -// Preload atlases sequentially into the EE cache until the buffer is full. -// Reads compressed data from disc, decompresses, and stores uncompressed pixels in the cache. -static void preloadEeCache(GsRenderer* gs) { - uint32_t preloaded = 0; - - // Allocate a temp buffer for reading compressed data from disc - uint32_t maxDiskSize = 0; - repeat(gs->atlasCount, i) { - if (gs->atlasDataSizes[i] > maxDiskSize) maxDiskSize = gs->atlasDataSizes[i]; - } - uint8_t* tempBuf = (uint8_t*) safeMemalign(128, maxDiskSize); - - repeat(gs->atlasCount, i) { - uint8_t bpp = gs->atlasBpp[i]; - uint32_t uncompSize = atlasUncompressedSize(bpp); - if (gs->eeCacheBumpPtr + uncompSize > gs->eeCacheCapacity) { - break; - } - - // Read compressed data from disc into temp buffer - uint32_t dataSize = gs->atlasDataSizes[i]; - fseek(gs->texturesFile, (long) gs->atlasOffsets[i], SEEK_SET); - size_t bytesRead = fread(tempBuf, 1, dataSize, gs->texturesFile); - if (bytesRead != dataSize) { - fprintf(stderr, "GsRenderer: EE cache preload short read for atlas %u (expected %u, got %zu)\n", i, dataSize, bytesRead); - break; - } - - // Decompress directly into the EE cache - decompressAtlasPixels(tempBuf, gs->eeCache + gs->eeCacheBumpPtr); - - gs->eeCacheEntries[i].atlasId = (int16_t) i; - gs->eeCacheEntries[i].offset = gs->eeCacheBumpPtr; - gs->eeCacheEntries[i].size = uncompSize; - gs->eeCacheEntries[i].lastUsed = gs->frameCounter; - - gs->eeCacheBumpPtr += uncompSize; - preloaded++; - } - - free(tempBuf); - - fprintf(stderr, "GsRenderer: EE cache initialized - %u MB, %u atlases preloaded (%u KB used)\n", EE_CACHE_CAPACITY / (1024 * 1024), preloaded, gs->eeCacheBumpPtr / 1024); -} - -// Look up an atlas in the EE cache. Returns pointer to cached data or nullptr. -static uint8_t* eeCacheLookup(GsRenderer* gs, uint16_t atlasId) { - if (atlasId >= gs->atlasCount) return nullptr; - if (0 > gs->eeCacheEntries[atlasId].atlasId) return nullptr; - - gs->eeCacheEntries[atlasId].lastUsed = gs->frameCounter; - return gs->eeCache + gs->eeCacheEntries[atlasId].offset; -} - -// Compact the EE cache by closing gaps from evicted entries. -static void compactEeCache(GsRenderer* gs) { - // Collect live entries sorted by offset using insertion sort - // (max 146 atlases, so a stack array + insertion sort is fine) - uint16_t liveIds[256]; // More than enough for 146 atlases - uint32_t liveCount = 0; - - repeat(gs->atlasCount, i) { - if (gs->eeCacheEntries[i].atlasId >= 0) { - // Insertion sort by offset - uint32_t insertPos = liveCount; - while (insertPos > 0 && gs->eeCacheEntries[liveIds[insertPos - 1]].offset > gs->eeCacheEntries[i].offset) { - liveIds[insertPos] = liveIds[insertPos - 1]; - insertPos--; - } - liveIds[insertPos] = (uint16_t) i; - liveCount++; - } - } - - // Walk and memmove each entry down to close gaps - uint32_t writePtr = 0; - repeat(liveCount, i) { - EeAtlasCacheEntry* entry = &gs->eeCacheEntries[liveIds[i]]; - if (entry->offset != writePtr) { - memmove(gs->eeCache + writePtr, gs->eeCache + entry->offset, entry->size); - entry->offset = writePtr; - } - writePtr += entry->size; - } - - gs->eeCacheBumpPtr = writePtr; -} - -// Evict LRU entries until spaceNeeded bytes are available. Returns true on success. -static bool eeCacheEvictLRU(GsRenderer* gs, uint32_t spaceNeeded) { - // Calculate total live bytes to determine how much space we can free - uint32_t liveBytes = 0; - repeat(gs->atlasCount, i) { - if (gs->eeCacheEntries[i].atlasId >= 0) { - liveBytes += gs->eeCacheEntries[i].size; - } - } - - // Evict LRU entries until enough space would be freed after compaction - while (gs->eeCacheCapacity - liveBytes < spaceNeeded) { - // Find entry with smallest lastUsed - uint64_t oldest = UINT64_MAX; - int16_t victimId = -1; - - repeat(gs->atlasCount, i) { - if (gs->eeCacheEntries[i].atlasId >= 0 && oldest > gs->eeCacheEntries[i].lastUsed) { - oldest = gs->eeCacheEntries[i].lastUsed; - victimId = (int16_t) i; - } - } - - if (0 > victimId) { - break; - } - - liveBytes -= gs->eeCacheEntries[victimId].size; - gs->eeCacheEntries[victimId].atlasId = -1; - } - - compactEeCache(gs); - - return gs->eeCacheCapacity - gs->eeCacheBumpPtr >= spaceNeeded; -} - -// Insert atlas data into the EE cache. Evicts LRU entries if needed. -static void eeCacheInsert(GsRenderer* gs, uint16_t atlasId, const uint8_t* data, uint32_t size) { - if (size > gs->eeCacheCapacity) { - // Atlas too large to ever fit in the cache - return; - } - - if (gs->eeCacheBumpPtr + size > gs->eeCacheCapacity) { - if (!eeCacheEvictLRU(gs, size)) { - rendererPrintf("GsRenderer: EE cache eviction failed for atlas %u (%u bytes)\n", atlasId, size); - return; - } - } - - memcpy(gs->eeCache + gs->eeCacheBumpPtr, data, size); - - gs->eeCacheEntries[atlasId].atlasId = (int16_t) atlasId; - gs->eeCacheEntries[atlasId].offset = gs->eeCacheBumpPtr; - gs->eeCacheEntries[atlasId].size = size; - gs->eeCacheEntries[atlasId].lastUsed = gs->frameCounter; - - gs->eeCacheBumpPtr += size; -} - -// Upload atlas pixel data to the given VRAM chunk(s). -// On cache hit: zero-copy DMA directly from EE cache (no decompression, no temp allocations). -// On cache miss: reads compressed data from TEXTURES.BIN, decompresses, inserts into EE cache, then uploads. -static void uploadAtlasToChunk(GsRenderer* gs, uint16_t atlasId, int32_t firstChunk) { - uint8_t* uploadData = eeCacheLookup(gs, atlasId); - uint8_t* tempPixelData = nullptr; // Non-null only if we need to free it after upload - const char* atlasSource = "RAM"; - - if (uploadData == nullptr) { - // Cache miss: read compressed data from TEXTURES.BIN and decompress - uint32_t dataSize = gs->atlasDataSizes[atlasId]; - uint8_t* compressedBuf = (uint8_t*) safeMemalign(128, dataSize); - - fseek(gs->texturesFile, (long) gs->atlasOffsets[atlasId], SEEK_SET); - size_t bytesRead = fread(compressedBuf, 1, dataSize, gs->texturesFile); - if (bytesRead != dataSize) { - fprintf(stderr, "GsRenderer: Short read for atlas %u (expected %u, got %zu)\n", atlasId, dataSize, bytesRead); - abort(); - } - - uint8_t bpp = gs->atlasBpp[atlasId]; - uint32_t uncompSize = atlasUncompressedSize(bpp); - tempPixelData = (uint8_t*) safeMemalign(128, uncompSize); - decompressAtlasPixels(compressedBuf, tempPixelData); - free(compressedBuf); - - // Try to insert uncompressed data into EE cache - eeCacheInsert(gs, atlasId, tempPixelData, uncompSize); - atlasSource = "disk"; - gs->diskLoadsThisFrame++; - - uploadData = eeCacheLookup(gs, atlasId); - if (uploadData != nullptr) { - // Insert succeeded, use cached copy for DMA upload - free(tempPixelData); - tempPixelData = nullptr; - } else { - // EE cache insert failed, upload directly from temp buffer - rendererPrintf("GsRenderer: EE cache insert failed for atlas %u, uploading directly\n", atlasId); - uploadData = tempPixelData; - } - } - - // Upload pixel data to VRAM - uint8_t bpp = gs->atlasBpp[atlasId]; - uint8_t psm = (bpp == 4) ? GS_PSM_T4 : GS_PSM_T8; - uint32_t tbw = ATLAS_WIDTH / 64; - uint32_t vramAddr = gs->textureVramBase + (uint32_t) firstChunk * VRAM_CHUNK_SIZE; - - gsKit_texture_send((u32*) uploadData, ATLAS_WIDTH, ATLAS_HEIGHT, vramAddr, psm, tbw, GS_CLUT_TEXTURE); - - // Update chunk state - int chunksUsed = (bpp == 8) ? 2 : 1; - repeat(chunksUsed, i) { - gs->chunks[firstChunk + i].atlasId = (int16_t) atlasId; - gs->chunks[firstChunk + i].lastUsed = gs->frameCounter; - } - gs->atlasToChunk[atlasId] = (int16_t) firstChunk; - - rendererPrintf("GsRenderer: Atlas %u uploaded to chunk %d (VRAM 0x%08X, %ubpp, src: %s)\n", atlasId, firstChunk, vramAddr, bpp, atlasSource); - - free(tempPixelData); -} - -// Ensure an atlas is loaded into VRAM, using LRU eviction if needed. -// Returns true on success, false on failure. -static bool ensureAtlasLoaded(GsRenderer* gs, uint16_t atlasId) { - if (atlasId >= gs->atlasCount) { - fprintf(stderr, "GsRenderer: Atlas ID %u out of range (max %u)\n", atlasId, gs->atlasCount - 1); - return false; - } - - // Already loaded? Just touch LRU timestamp - if (gs->atlasToChunk[atlasId] >= 0) { - int16_t firstChunk = gs->atlasToChunk[atlasId]; - uint8_t bpp = gs->atlasBpp[atlasId]; - int chunksUsed = (bpp == 8) ? 2 : 1; - - // Track unique atlases per frame (first touch = lastUsed hasn't been updated yet) - if (gs->chunks[firstChunk].lastUsed != gs->frameCounter) { - gs->uniqueAtlasesThisFrame++; - gs->chunksNeededThisFrame += (uint16_t) chunksUsed; - } - - repeat(chunksUsed, i) { - gs->chunks[firstChunk + i].lastUsed = gs->frameCounter; - } - return true; - } - - // Determine how many chunks we need - uint8_t bpp = gs->atlasBpp[atlasId]; - if (bpp != 4 && bpp != 8) { - fprintf(stderr, "GsRenderer: Atlas %u has unknown bpp %u\n", atlasId, bpp); - return false; - } - int chunksNeeded = (bpp == 8) ? 2 : 1; - - // Fresh load is always a new unique atlas this frame - gs->uniqueAtlasesThisFrame++; - gs->chunksNeededThisFrame += (uint16_t) chunksNeeded; - - // Allocate chunks (may evict or defrag) - int32_t chunkIdx = allocateChunks(gs, chunksNeeded); - if (0 > chunkIdx) { - fprintf(stderr, "GsRenderer: VRAM exhausted! Cannot allocate %d chunk(s) for atlas %u (%ubpp)\n", chunksNeeded, atlasId, bpp); - abort(); - } - - // Load TEX file and upload to the allocated chunk(s) - uploadAtlasToChunk(gs, atlasId, chunkIdx); - return true; -} - -// ===[ GSTEXTURE setup for a given TPAG entry ]=== -// Configures a GSTEXTURE struct for rendering a specific atlas region. -// The GSTEXTURE points to the atlas's VRAM location and the appropriate CLUT. -static bool setupTextureForTPAG(GsRenderer* gs, GSTEXTURE* tex, int32_t tpagIndex) { - if (0 > tpagIndex || (uint32_t) tpagIndex >= gs->atlasTPAGCount) return false; - - AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[tpagIndex]; - if (entry->atlasId == 0xFFFF) return false; - - // Ensure the atlas texture is loaded into VRAM (may trigger LRU eviction) - if (!ensureAtlasLoaded(gs, entry->atlasId)) - return false; - - // Compute VRAM address from chunk index - int16_t chunkIdx = gs->atlasToChunk[entry->atlasId]; - uint32_t vramAddr = gs->textureVramBase + (uint32_t) chunkIdx * VRAM_CHUNK_SIZE; - - memset(tex, 0, sizeof(GSTEXTURE)); - tex->Width = ATLAS_WIDTH; - tex->Height = ATLAS_HEIGHT; - tex->TBW = ATLAS_WIDTH / 64; - tex->Vram = vramAddr; - tex->Filter = GS_FILTER_NEAREST; - tex->ClutStorageMode = GS_CLUT_STORAGE_CSM1; - - if (entry->bpp == 4) { - tex->PSM = GS_PSM_T4; - tex->ClutPSM = GS_PSM_CT32; - - if (entry->clutIndex >= gs->clut4Count) { - fprintf(stderr, "GsRenderer: CLUT4 index %u out of range (max %u) for TPAG %d\n", entry->clutIndex, gs->clut4Count - 1, tpagIndex); - abort(); - } - - tex->VramClut = gs->clut4VramAddrs[entry->clutIndex]; - } else { - tex->PSM = GS_PSM_T8; - tex->ClutPSM = GS_PSM_CT32; - - if (entry->clutIndex >= gs->clut8Count) { - fprintf(stderr, "GsRenderer: CLUT8 index %u out of range (max %u) for TPAG %d\n", entry->clutIndex, gs->clut8Count - 1, tpagIndex); - abort(); - } - - tex->VramClut = gs->clut8VramAddrs[entry->clutIndex]; - } - - return true; -} - -// ===[ Tile Lookup and Texture Setup ]=== - -// Finds a tile entry by (bgDef, srcX, srcY, srcW, srcH). Returns nullptr if not found. -static AtlasTileEntry* findTileEntry(GsRenderer* gs, int16_t bgDef, uint16_t srcX, uint16_t srcY, uint16_t srcW, uint16_t srcH) { - TileLookupKey key = { .bgDef = bgDef, .srcX = srcX, .srcY = srcY, .srcW = srcW, .srcH = srcH }; - ptrdiff_t idx = hmgeti(gs->tileEntryMap, key); - if (idx == -1) return nullptr; - return gs->tileEntryMap[idx].value; -} - -// Configures a GSTEXTURE for rendering a tile entry. Same logic as setupTextureForTPAG but for AtlasTileEntry. -static bool setupTextureForTile(GsRenderer* gs, GSTEXTURE* tex, AtlasTileEntry* entry) { - if (entry->atlasId == 0xFFFF) return false; - - if (!ensureAtlasLoaded(gs, entry->atlasId)) - return false; - - int16_t chunkIdx = gs->atlasToChunk[entry->atlasId]; - uint32_t vramAddr = gs->textureVramBase + (uint32_t) chunkIdx * VRAM_CHUNK_SIZE; - - memset(tex, 0, sizeof(GSTEXTURE)); - tex->Width = ATLAS_WIDTH; - tex->Height = ATLAS_HEIGHT; - tex->TBW = ATLAS_WIDTH / 64; - tex->Vram = vramAddr; - tex->Filter = GS_FILTER_NEAREST; - tex->ClutStorageMode = GS_CLUT_STORAGE_CSM1; - - if (entry->bpp == 4) { - tex->PSM = GS_PSM_T4; - tex->ClutPSM = GS_PSM_CT32; - - if (entry->clutIndex >= gs->clut4Count) { - fprintf(stderr, "GsRenderer: CLUT4 index %u out of range (max %u) for tile (bg=%d)\n", entry->clutIndex, gs->clut4Count - 1, entry->bgDef); - abort(); - } - - tex->VramClut = gs->clut4VramAddrs[entry->clutIndex]; - } else { - tex->PSM = GS_PSM_T8; - tex->ClutPSM = GS_PSM_CT32; - - if (entry->clutIndex >= gs->clut8Count) { - fprintf(stderr, "GsRenderer: CLUT8 index %u out of range (max %u) for tile (bg=%d)\n", entry->clutIndex, gs->clut8Count - 1, entry->bgDef); - abort(); - } - - tex->VramClut = gs->clut8VramAddrs[entry->clutIndex]; - } - - return true; -} - -// ===[ Vtable Implementations ]=== - -// Identity blend - source passes through unchanged. Used when GML disables blending but we still need PrimAlphaEnable=ON for TCC. -// Equation: (Cs - 0) * 128/128 + 0 = Cs. -// We do this because disabling blending (setting PrimAlphaEnable to OFF) makes GS stop honoring alpha writes from textures, which breaks masks. -#define GS_ALPHA_NO_BLEND GS_SETREG_ALPHA(0, 2, 2, 2, 0x80) - -// Re-emits the FRAME_1 register with the current FBMSK. Called whenever the color write mask changes. -static void gsApplyFBMask(GsRenderer* gs, u32 fbmsk) { - GSGLOBAL* g = gs->gsGlobal; - u64* p = (u64*) gsKit_heap_alloc(g, 1, 16, GIF_AD); - *p++ = GIF_TAG_AD(1); - *p++ = GIF_AD; - *p++ = GS_SETREG_FRAME(g->ScreenBuffer[g->ActiveBuffer & 1] / 8192, g->Width / 64, g->PSM, fbmsk); - *p++ = GS_FRAME_1 + g->PrimContext; - gs->fbmsk = fbmsk; -} - -// Re-emits the FBA_1 (Framebuffer Alpha) register. fba=1 forces bit 7 of the alpha to 1 at framebuffer writeback (after the blend equation has consumed As, so blending is unaffected). -// fba=0 passes alpha through unchanged - required while the script is in alpha-only write mode so the intended mask value lands in FB.A verbatim. -static void gsApplyFBA(GsRenderer* gs, uint8_t fba) { - if (gs->fba == fba) return; - GSGLOBAL* g = gs->gsGlobal; - u64* p = (u64*) gsKit_heap_alloc(g, 1, 16, GIF_AD); - *p++ = GIF_TAG_AD(1); - *p++ = GIF_AD; - *p++ = (u64) (fba & 1); - *p++ = GS_FBA_1 + g->PrimContext; - gs->fba = fba; -} - -static void gsCommitBlend(GsRenderer* gs) { - gsKit_set_primalpha(gs->gsGlobal, gs->blendEnabled ? gs->currentBlendAlpha : GS_ALPHA_NO_BLEND, 0); -} - -static void gsInit(Renderer* renderer, DataWin* dataWin) { - GsRenderer* gs = (GsRenderer*) renderer; - - renderer->dataWin = dataWin; - renderer->drawColor = 0xFFFFFF; - renderer->drawAlpha = 1.0f; - renderer->drawFont = -1; - renderer->drawHalign = 0; - renderer->drawValign = 0; - - // Enable alpha blending - gs->gsGlobal->PrimAlphaEnable = GS_SETTING_ON; - gs->blendEnabled = true; - gs->currentBlendAlpha = GS_SETREG_ALPHA(0, 1, 0, 1, 0); - - // gsKit defaults Test->AREF to 0x80, but GMS's gpu_get_alphatestref() defaults to 0. Scripts that enable alpha test without calling gpu_set_alphatestref expect ref=0. - // With ATST=GREATER and the post-MODULATE source alpha capped at 0x80, an AREF of 0x80 makes "0x80 > 0x80" fail, hiding all opaque pixels. - gs->gsGlobal->Test->AREF = 0; - gs->gsGlobal->Test->ATST = 6; // GREATER (matches GMS semantics) - gs->gsGlobal->Test->AFAIL = 0; // KEEP - - // Force FB.A bit = 1 on every writeback via the GS FBA register so bm_dest_alpha / bm_inv_dest_alpha see opaque alpha for normal sprites. - // This mimicks how OpenGL works - gsApplyFBA(gs, 1); - - // Alpha blend: (Cs - Cd) * As / 128 + Cd (standard source-over) - gsKit_set_primalpha(gs->gsGlobal, GS_SETREG_ALPHA(0, 1, 0, 1, 0), 0); - - // Load atlas metadata - loadAtlas(gs); - - // Open TEXTURES.BIN and keep it open for on-demand atlas loading - char* texturesBinPath = PS2Utils_createDevicePath("TEXTURES.BIN"); - gs->texturesFile = fopen(texturesBinPath, "rb"); - if (gs->texturesFile == nullptr) { - fprintf(stderr, "GsRenderer: Failed to open %s\n", texturesBinPath); - abort(); - } - setvbuf(gs->texturesFile, nullptr, _IOFBF, 128 * 1024); - free(texturesBinPath); - - // Upload CLUTs to VRAM - loadAndUploadCLUTs(gs); - - // Initialize the texture cache chunk pool (uses remaining VRAM after CLUTs) - initTextureCache(gs); - - // Initialize EE RAM cache for compressed atlas data - initEeCache(gs); - preloadEeCache(gs); - - fprintf(stderr, "GsRenderer: Initialized (textured mode)\n"); -} - -static void gsDestroy(Renderer* renderer) { - GsRenderer* gs = (GsRenderer*) renderer; - if (gs->texturesFile != nullptr) { - fclose(gs->texturesFile); - } - free(gs->atlasOffsets); - free(gs->atlasTPAGEntries); - free(gs->atlasTileEntries); - hmfree(gs->tileEntryMap); - free(gs->chunks); - free(gs->atlasToChunk); - free(gs->atlasBpp); - free(gs->clut4VramAddrs); - free(gs->clut8VramAddrs); - free(gs->eeCache); - free(gs->eeCacheEntries); - free(gs->atlasDataSizes); - free(gs); -} - -static void gsBeginFrame(Renderer* renderer, MAYBE_UNUSED int32_t gameW, MAYBE_UNUSED int32_t gameH, MAYBE_UNUSED int32_t windowW, MAYBE_UNUSED int32_t windowH) { - GsRenderer* gs = (GsRenderer*) renderer; - gs->frameCounter++; - gs->evictedAtlasUsedInCurrentFrame = false; - gs->uniqueAtlasesThisFrame = 0; - gs->chunksNeededThisFrame = 0; - gs->diskLoadsThisFrame = 0; - - // gsKit_setactive (called by sync_flip) re-emits FRAME with FBMSK=0, so any color-write mask we set last frame is gone. Re-apply it here for cases where GML leaves it asserted across frames. - if (gs->fbmsk != 0) { - gsApplyFBMask(gs, gs->fbmsk); - } -} - -static void gsEndFrame(MAYBE_UNUSED Renderer* renderer) { - // No-op: flip happens in main loop -} - -static void gsBeginView(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, MAYBE_UNUSED int32_t portX, MAYBE_UNUSED int32_t portY, MAYBE_UNUSED int32_t portW, MAYBE_UNUSED int32_t portH, MAYBE_UNUSED float viewAngle) { - GsRenderer* gs = (GsRenderer*) renderer; - gs->viewX = viewX; - gs->viewY = viewY; - - // Scale game view to PS2 screen (640x448 NTSC interlaced) - if (viewW > 0 && viewH > 0) { - gs->scaleX = 640.0f / (float) viewW; - gs->scaleY = gs->scaleX; - } else { - gs->scaleX = 2.0f; - gs->scaleY = 2.0f; - } - - // Center vertically - float renderedH = (float) viewH * gs->scaleY; - gs->offsetX = 0.0f; - gs->offsetY = (448.0f - renderedH) / 2.0f; -} - -static void gsEndView(MAYBE_UNUSED Renderer* renderer) { - // No-op -} - -static void gsBeginGUI(Renderer* renderer, int32_t guiW, int32_t guiH, MAYBE_UNUSED int32_t portX, MAYBE_UNUSED int32_t portY, MAYBE_UNUSED int32_t portW, MAYBE_UNUSED int32_t portH) { - GsRenderer* gs = (GsRenderer*) renderer; - gs->viewX = 0; - gs->viewY = 0; - - if (guiW > 0 && guiH > 0) { - gs->scaleX = 640.0f / (float) guiW; - gs->scaleY = gs->scaleX; - } else { - gs->scaleX = 2.0f; - gs->scaleY = 2.0f; - } - - float renderedH = (float) guiH * gs->scaleY; - gs->offsetX = 0.0f; - gs->offsetY = (448.0f - renderedH) / 2.0f; -} - -static void gsEndGUI(MAYBE_UNUSED Renderer* renderer) { - // No-op -} - -static void gsDrawSprite(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) { - GsRenderer* gs = (GsRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; - - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - - // Get crop region from atlas entry (falls back to full bounding box if unmapped) - float cropX = 0.0f, cropY = 0.0f; - float cropW = (float) tpag->boundingWidth; - float cropH = (float) tpag->boundingHeight; - if (gs->atlasTPAGCount > (uint32_t) tpagIndex) { - AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[tpagIndex]; - if (entry->atlasId != 0xFFFF) { - cropX = (float) entry->cropX; - cropY = (float) entry->cropY; - cropW = (float) entry->cropW; - cropH = (float) entry->cropH; - } - } - - // Compute 4 screen-space corners (tristrip Z-pattern: top-left, top-right, bottom-left, bottom-right) - // sx0/sy0 = top-left, sx1/sy1 = top-right, sx2/sy2 = bottom-left, sx3/sy3 = bottom-right - float sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3; - bool hasRotation = angleDeg != 0.0f; - - if (hasRotation) { - // Rotated: compute 4 transformed corners via matrix, same approach as the GLFW renderer - // Position the cropped region within the original bounding box - float localX0 = cropX - originX; - float localY0 = cropY - originY; - float localX1 = cropX + cropW - originX; - float localY1 = cropY + cropH - originY; - - // Build 2D transform: T(x,y) * R(-angleDeg) * S(xscale, yscale) - // Negate angle because Y-down coordinate system - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - Matrix4f transform; - Matrix4f_setTransform2D(&transform, x, y, xscale, yscale, angleRad); - - float gx0, gy0, gx1, gy1, gx2, gy2, gx3, gy3; - Matrix4f_transformPoint(&transform, localX0, localY0, &gx0, &gy0); // top-left - Matrix4f_transformPoint(&transform, localX1, localY0, &gx1, &gy1); // top-right - Matrix4f_transformPoint(&transform, localX0, localY1, &gx2, &gy2); // bottom-left - Matrix4f_transformPoint(&transform, localX1, localY1, &gx3, &gy3); // bottom-right - - // Apply view offset and scale - sx0 = (gx0 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - sy0 = (gy0 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - sx1 = (gx1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - sy1 = (gy1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - sx2 = (gx2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - sy2 = (gy2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - sx3 = (gx3 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - sy3 = (gy3 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - } else { - // Axis-aligned: simple rect math - // Position the cropped region within the original bounding box - float gameX1 = x + (cropX - originX) * xscale; - float gameY1 = y + (cropY - originY) * yscale; - float gameX2 = x + (cropX + cropW - originX) * xscale; - float gameY2 = y + (cropY + cropH - originY) * yscale; - - // Apply view offset and scale - sx0 = (gameX1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - sy0 = (gameY1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - sx1 = (gameX2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - sy1 = (gameY1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - sx2 = (gameX1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - sy2 = (gameY2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - sx3 = (gameX2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - sy3 = (gameY2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - } - - // View frustum culling: skip if entirely off-screen (handles negative scales via min/max) - float minSX = fminf(fminf(sx0, sx1), fminf(sx2, sx3)); - float maxSX = fmaxf(fmaxf(sx0, sx1), fmaxf(sx2, sx3)); - float minSY = fminf(fminf(sy0, sy1), fminf(sy2, sy3)); - float maxSY = fmaxf(fmaxf(sy0, sy1), fmaxf(sy2, sy3)); - if (maxSX < 0.0f || minSX > PS2_SCREEN_WIDTH || maxSY < 0.0f || minSY > PS2_SCREEN_HEIGHT) - return; - - // Set up GSTEXTURE for this TPAG entry - GSTEXTURE tex; - if (!setupTextureForTPAG(gs, &tex, tpagIndex)) { - // Fallback: draw colored quad if no atlas mapping - uint8_t r = BGR_R(color); - uint8_t g = BGR_G(color); - uint8_t b = BGR_B(color); - uint8_t a = alphaToGS(alpha); - u64 fallbackColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - if (hasRotation) { - gsKit_prim_quad(gs->gsGlobal, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3, 0, fallbackColor); - } else { - gsKit_prim_sprite(gs->gsGlobal, sx0, sy0, sx3, sy3, 0, fallbackColor); - } - return; - } - - AtlasTPAGEntry* atlasEntry = &gs->atlasTPAGEntries[tpagIndex]; - - // The atlas entry has the actual sprite dimensions in the atlas (post-crop, post-resize). - // The screen rect covers cropW x cropH game-space pixels, positioned at (cropX, cropY) - // within the original bounding box. The GS hardware stretches the atlas texels to fill. - - // UV coords within the 512x512 atlas (in texels for gsKit) - float u0 = (float) atlasEntry->atlasX; - float v0 = (float) atlasEntry->atlasY; - float u1 = u0 + (float) atlasEntry->width; - float v1 = v0 + (float) atlasEntry->height; - - // GS modulate mode: Output = Texture * Vertex / 128 - // Scale vertex RGB from 0-255 to 0-128 so white (255) becomes 128 (1.0x multiplier) - uint8_t r = BGR_R(color) >> 1; - uint8_t g = BGR_G(color) >> 1; - uint8_t b = BGR_B(color) >> 1; - uint8_t a = alphaToGS(alpha); - u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - - if (hasRotation) { - // Tristrip Z-pattern: needs 4 vertices for rotated quads - gsKit_prim_quad_texture( - gs->gsGlobal, - &tex, - sx0, sy0, u0, v0, // top-left - sx1, sy1, u1, v0, // top-right - sx2, sy2, u0, v1, // bottom-left - sx3, sy3, u1, v1, // bottom-right - 0, - gsColor - ); - } else { - gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx3, sy3, u1, v1, 0, gsColor); - } -} - -static void gsDrawTiled(Renderer* renderer, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha) { - GsRenderer* gs = (GsRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; - - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - - float cropX = 0.0f, cropY = 0.0f; - float cropW = (float) tpag->boundingWidth; - float cropH = (float) tpag->boundingHeight; - if (gs->atlasTPAGCount > (uint32_t) tpagIndex) { - AtlasTPAGEntry* entry = &gs->atlasTPAGEntries[tpagIndex]; - if (entry->atlasId != 0xFFFF) { - cropX = (float) entry->cropX; - cropY = (float) entry->cropY; - cropW = (float) entry->cropW; - cropH = (float) entry->cropH; - } - } - - float axScale = fabsf(xscale); - float ayScale = fabsf(yscale); - float tileW = (float) tpag->boundingWidth * axScale; - float tileH = (float) tpag->boundingHeight * ayScale; - if (0 >= tileW || 0 >= tileH) return; - - float startX, endX, startY, endY; - if (tileX) { - startX = fmodf(x - originX * axScale, tileW); - if (startX > 0) startX -= tileW; - endX = roomW; - } else { - startX = x - originX * axScale; - endX = startX + tileW; - } - if (tileY) { - startY = fmodf(y - originY * ayScale, tileH); - if (startY > 0) startY -= tileH; - endY = roomH; - } else { - startY = y - originY * ayScale; - endY = startY + tileH; - } - - // Per-tile quad layout in world space, derived from gsDrawSprite's axis-aligned path. - // For tile origin dx, the wrapper-equivalent x_call = dx + originX * axScale, then: - // gameX1 = x_call + (cropX - originX) * xscale = dx + cropX * xscale + originX * (axScale - xscale) - // Same shape for Y. Both reduce to dx + cropX*xscale when xscale > 0 (the common case). - float dxLocalX0 = cropX * xscale + originX * (axScale - xscale); - float dyLocalY0 = cropY * yscale + originY * (ayScale - yscale); - float tileGameW = cropW * xscale; - float tileGameH = cropH * yscale; - - GSTEXTURE tex; - if (!setupTextureForTPAG(gs, &tex, tpagIndex)) return; - - AtlasTPAGEntry* atlasEntry = &gs->atlasTPAGEntries[tpagIndex]; - float u0 = (float) atlasEntry->atlasX; - float v0 = (float) atlasEntry->atlasY; - float u1 = u0 + (float) atlasEntry->width; - float v1 = v0 + (float) atlasEntry->height; - - // 0x80 is the exact 1.0x multiplier in GS modulate mode (output = texture * vertex / 128). - // So, to avoid dimming the texture (BGR_R(0xFFFFFF) >> 1 = 0x7F is 0.992x) we'll hand-pick the white case. - uint8_t r, g, b; - if (color == 0xFFFFFFu) { - r = g = b = 0x80; - } else { - r = BGR_R(color) >> 1; - g = BGR_G(color) >> 1; - b = BGR_B(color) >> 1; - } - uint8_t a = alphaToGS(alpha); - u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - - float viewBaseX = -(float) gs->viewX; - float viewBaseY = -(float) gs->viewY; - float viewScaleX = gs->scaleX; - float viewScaleY = gs->scaleY; - float viewOffX = gs->offsetX; - float viewOffY = gs->offsetY; - - // Integer tile counts avoid FP-comparison drift; the inner break handles overshoot at the boundary - int32_t tilesX = tileX ? ((int32_t) ((endX - startX) / tileW) + 1) : 1; - int32_t tilesY = tileY ? ((int32_t) ((endY - startY) / tileH) + 1) : 1; - if (0 >= tilesX || 0 >= tilesY) return; - - repeat(tilesY, iy) { - float dy = startY + (float) iy * tileH; - if (dy >= endY) break; - - float gameY1 = dy + dyLocalY0 + viewBaseY; - float gameY2 = gameY1 + tileGameH; - float sy0 = gameY1 * viewScaleY + viewOffY; - float sy1 = gameY2 * viewScaleY + viewOffY; - - float minSY = sy0 < sy1 ? sy0 : sy1; - float maxSY = sy0 > sy1 ? sy0 : sy1; - if (0.0f > maxSY || minSY > PS2_SCREEN_HEIGHT) continue; - - repeat(tilesX, ix) { - float dx = startX + (float) ix * tileW; - if (endX <= dx) break; - - float gameX1 = dx + dxLocalX0 + viewBaseX; - float gameX2 = gameX1 + tileGameW; - float sx0 = gameX1 * viewScaleX + viewOffX; - float sx1 = gameX2 * viewScaleX + viewOffX; - - float minSX = sx0 < sx1 ? sx0 : sx1; - float maxSX = sx0 > sx1 ? sx0 : sx1; - if (0.0f > maxSX || minSX > PS2_SCREEN_WIDTH) continue; - - gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx1, sy1, u1, v1, 0, gsColor); - } - } -} - -static void gsDrawTiledPart(Renderer* renderer, int32_t tpagIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint32_t color, float alpha) { - GsRenderer* gs = (GsRenderer*) renderer; - DataWin* dw = renderer->dataWin; - if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; - - GSTEXTURE tex; - if (!setupTextureForTPAG(gs, &tex, tpagIndex)) return; - - AtlasTPAGEntry* atlasEntry = &gs->atlasTPAGEntries[tpagIndex]; - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - - // Crop coords in source-page space (matching gsDrawSpritePart's coordinate system). - float cX = (float) atlasEntry->cropX - (float) tpag->targetX; - float cY = (float) atlasEntry->cropY - (float) tpag->targetY; - float cW = (float) atlasEntry->cropW; - float cH = (float) atlasEntry->cropH; - float ratioX = cW > 0.0f ? (float) atlasEntry->width / cW : 1.0f; - float ratioY = cH > 0.0f ? (float) atlasEntry->height / cH : 1.0f; - - uint8_t r, g, b; - if (color == 0xFFFFFFu) { r = g = b = 0x80; } else { r = BGR_R(color) >> 1; g = BGR_G(color) >> 1; b = BGR_B(color) >> 1; } - uint8_t a = alphaToGS(alpha); - u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - - float viewBaseX = -(float) gs->viewX; - float viewBaseY = -(float) gs->viewY; - float viewScaleX = gs->scaleX; - float viewScaleY = gs->scaleY; - float viewOffX = gs->offsetX; - float viewOffY = gs->offsetY; - - int32_t tilesY = (int32_t)(dstH / (float) srcH) + 2; - int32_t tilesX = (int32_t)(dstW / (float) srcW) + 2; - - repeat(tilesY, iy) { - float rowDstY = dstY + (float) iy * (float) srcH; - if (rowDstY >= dstY + dstH) break; - int32_t rowSrcH = srcH < (int32_t)((dstY + dstH) - rowDstY) ? srcH : (int32_t)((dstY + dstH) - rowDstY); - - float intY1 = cY > (float) srcY ? cY : (float) srcY; - float intY2 = (cY + cH) < (float)(srcY + rowSrcH) ? (cY + cH) : (float)(srcY + rowSrcH); - if (intY1 >= intY2) continue; - float clipOffY = intY1 - (float) srcY; - float visH = intY2 - intY1; - float v0 = (float) atlasEntry->atlasY + (intY1 - cY) * ratioY; - float v1 = v0 + visH * ratioY; - - float sy0 = (rowDstY + clipOffY + viewBaseY) * viewScaleY + viewOffY; - float sy1 = sy0 + visH * viewScaleY; - float minSY = sy0 < sy1 ? sy0 : sy1; - float maxSY = sy0 > sy1 ? sy0 : sy1; - if (0.0f > maxSY || minSY > PS2_SCREEN_HEIGHT) continue; - - repeat(tilesX, ix) { - float colDstX = dstX + (float) ix * (float) srcW; - if (colDstX >= dstX + dstW) break; - int32_t colSrcW = srcW < (int32_t)((dstX + dstW) - colDstX) ? srcW : (int32_t)((dstX + dstW) - colDstX); - - float intX1 = cX > (float) srcX ? cX : (float) srcX; - float intX2 = (cX + cW) < (float)(srcX + colSrcW) ? (cX + cW) : (float)(srcX + colSrcW); - if (intX1 >= intX2) continue; - float clipOffX = intX1 - (float) srcX; - float visW = intX2 - intX1; - float u0 = (float) atlasEntry->atlasX + (intX1 - cX) * ratioX; - float u1 = u0 + visW * ratioX; - - float sx0 = (colDstX + clipOffX + viewBaseX) * viewScaleX + viewOffX; - float sx1 = sx0 + visW * viewScaleX; - float minSX = sx0 < sx1 ? sx0 : sx1; - float maxSX = sx0 > sx1 ? sx0 : sx1; - if (0.0f > maxSX || minSX > PS2_SCREEN_WIDTH) continue; - - gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx1, sy1, u1, v1, 0, gsColor); - } - } -} - -static void gsDrawSpritePart(Renderer* renderer, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { - GsRenderer* gs = (GsRenderer*) renderer; - - if (0 > tpagIndex || (uint32_t) tpagIndex >= renderer->dataWin->tpag.count) return; - - // Set up GSTEXTURE for this TPAG entry - GSTEXTURE tex; - bool hasTexture = setupTextureForTPAG(gs, &tex, tpagIndex); - - AtlasTPAGEntry* atlasEntry = hasTexture ? &gs->atlasTPAGEntries[tpagIndex] : nullptr; - TexturePageItem* tpag = &renderer->dataWin->tpag.items[tpagIndex]; - - // srcOffX/srcOffY are in source-page space (Renderer_drawSpritePartExt subtracts tpag->targetX/Y to convert from GML sprite-bounding space). - // The preprocessor's cropX/cropY, however, are in sprite-bounding space (extractFromTPAG builds a boundingWidth x boundingHeight image with pixels offset by targetX/targetY, then cropTransparentBorders runs on that). - // Subtract targetX/targetY here so both sides of the intersection live in the same coordinate system. - float cX = hasTexture ? ((float) atlasEntry->cropX - (float) tpag->targetX) : 0.0f; - float cY = hasTexture ? ((float) atlasEntry->cropY - (float) tpag->targetY) : 0.0f; - float cW = hasTexture ? (float) atlasEntry->cropW : (float) tpag->sourceWidth; - float cH = hasTexture ? (float) atlasEntry->cropH : (float) tpag->sourceHeight; - - float intX1 = fmaxf((float) srcOffX, cX); - float intY1 = fmaxf((float) srcOffY, cY); - float intX2 = fminf((float)(srcOffX + srcW), cX + cW); - float intY2 = fminf((float)(srcOffY + srcH), cY + cH); - - if (intX1 >= intX2 || intY1 >= intY2) return; - - // Compute clip offset and visible region dimensions - float clipOffX = intX1 - (float) srcOffX; - float clipOffY = intY1 - (float) srcOffY; - float visW = intX2 - intX1; - float visH = intY2 - intY1; - - // World-space corners of the visible sub-rect (before rotation) - float gx0 = x + clipOffX * xscale; - float gy0 = y + clipOffY * yscale; - float gx1 = gx0 + visW * xscale; - float gy1 = gy0; - float gx2 = gx0; - float gy2 = gy0 + visH * yscale; - float gx3 = gx1; - float gy3 = gy2; - - if (angleDeg != 0.0f) { - float angleRad = -angleDeg * ((float) M_PI / 180.0f); - float cosA = cosf(angleRad); - float sinA = sinf(angleRad); - float dx, dy, rx, ry; -#define ROTATE_CORNER(gxi, gyi) do { dx = (gxi) - pivotX; dy = (gyi) - pivotY; rx = cosA * dx - sinA * dy + pivotX; ry = sinA * dx + cosA * dy + pivotY; (gxi) = rx; (gyi) = ry; } while(0) - ROTATE_CORNER(gx0, gy0); - ROTATE_CORNER(gx1, gy1); - ROTATE_CORNER(gx2, gy2); - ROTATE_CORNER(gx3, gy3); -#undef ROTATE_CORNER - } - - // Convert game-space corners to screen space - float sx0 = (gx0 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy0 = (gy0 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx1 = (gx1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy1 = (gy1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx2 = (gx2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy2 = (gy2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx3 = (gx3 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy3 = (gy3 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - // View frustum culling - float minSX = fminf(fminf(sx0, sx1), fminf(sx2, sx3)); - float maxSX = fmaxf(fmaxf(sx0, sx1), fmaxf(sx2, sx3)); - float minSY = fminf(fminf(sy0, sy1), fminf(sy2, sy3)); - float maxSY = fmaxf(fmaxf(sy0, sy1), fmaxf(sy2, sy3)); - if (maxSX < 0.0f || minSX > PS2_SCREEN_WIDTH || maxSY < 0.0f || minSY > PS2_SCREEN_HEIGHT) return; - - bool hasRotation = angleDeg != 0.0f; - - if (!hasTexture) { - uint8_t r = BGR_R(color); - uint8_t g = BGR_G(color); - uint8_t b = BGR_B(color); - uint8_t a = alphaToGS(alpha); - u64 fallbackColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - if (hasRotation) { - gsKit_prim_quad(gs->gsGlobal, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3, 0, fallbackColor); - } else { - gsKit_prim_sprite(gs->gsGlobal, sx0, sy0, sx3, sy3, 0, fallbackColor); - } - return; - } - - // Map intersection region to atlas UV space - float ratioX = (cW > 0) ? ((float) atlasEntry->width / cW) : 1.0f; - float ratioY = (cH > 0) ? ((float) atlasEntry->height / cH) : 1.0f; - - float u0 = (float) atlasEntry->atlasX + (intX1 - cX) * ratioX; - float v0 = (float) atlasEntry->atlasY + (intY1 - cY) * ratioY; - float u1 = u0 + visW * ratioX; - float v1 = v0 + visH * ratioY; - - // GS modulate mode: Output = Texture * Vertex / 128 - uint8_t r = BGR_R(color) >> 1; - uint8_t g = BGR_G(color) >> 1; - uint8_t b = BGR_B(color) >> 1; - uint8_t a = alphaToGS(alpha); - u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - - if (hasRotation) { - gsKit_prim_quad_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx1, sy1, u1, v0, sx2, sy2, u0, v1, sx3, sy3, u1, v1, 0, gsColor); - } else { - gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx0, sy0, u0, v0, sx3, sy3, u1, v1, 0, gsColor); - } -} - -static void gsDrawSpritePos(Renderer* renderer, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) { - GsRenderer* gs = (GsRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; - - // Apply view transform. Z-pattern tristrip ordering: (0)=TL, (1)=TR, (2)=BL, (3)=BR. - float sx0 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy0 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx1 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy1 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx2 = (x4 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy2 = (y4 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx3 = (x3 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy3 = (y3 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - float minSX = fminf(fminf(sx0, sx1), fminf(sx2, sx3)); - float maxSX = fmaxf(fmaxf(sx0, sx1), fmaxf(sx2, sx3)); - float minSY = fminf(fminf(sy0, sy1), fminf(sy2, sy3)); - float maxSY = fmaxf(fmaxf(sy0, sy1), fmaxf(sy2, sy3)); - if (maxSX < 0.0f || minSX > PS2_SCREEN_WIDTH || maxSY < 0.0f || minSY > PS2_SCREEN_HEIGHT) return; - - GSTEXTURE tex; - if (!setupTextureForTPAG(gs, &tex, tpagIndex)) { - uint8_t a = alphaToGS(alpha); - u64 fallbackColor = GS_SETREG_RGBAQ(0xFF, 0xFF, 0xFF, a, 0x00); - gsKit_prim_quad(gs->gsGlobal, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3, 0, fallbackColor); - return; - } - - AtlasTPAGEntry* atlasEntry = &gs->atlasTPAGEntries[tpagIndex]; - - // Map the entire atlas entry (the trimmed source content in atlas texels) to the user's quad. - float u0 = (float) atlasEntry->atlasX; - float v0 = (float) atlasEntry->atlasY; - float u1 = u0 + (float) atlasEntry->width; - float v1 = v0 + (float) atlasEntry->height; - - // GS modulate mode: Output = Texture * Vertex / 128 - uint8_t a = alphaToGS(alpha); - u64 gsColor = GS_SETREG_RGBAQ(0x80, 0x80, 0x80, a, 0x00); - - gsKit_prim_quad_texture( - gs->gsGlobal, - &tex, - sx0, sy0, u0, v0, // TL - sx1, sy1, u1, v0, // TR - sx2, sy2, u0, v1, // BL - sx3, sy3, u1, v1, // BR - 0, - gsColor - ); -} - -static void gsDrawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline) { - GsRenderer* gs = (GsRenderer*) renderer; - - uint8_t r = BGR_R(color); - uint8_t g = BGR_G(color); - uint8_t b = BGR_B(color); - uint8_t a = alphaToGS(alpha); - - float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - u64 rectColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - - if (outline) { - // Draw 4 one-pixel-wide edges: top, bottom, left, right - float pw = gs->scaleX; // one pixel width in screen coords - float ph = gs->scaleY; // one pixel height in screen coords - gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2 + pw, sy1 + ph, 0, rectColor); // top - gsKit_prim_sprite(gs->gsGlobal, sx1, sy2, sx2 + pw, sy2 + ph, 0, rectColor); // bottom - gsKit_prim_sprite(gs->gsGlobal, sx1, sy1 + ph, sx1 + pw, sy2, 0, rectColor); // left - gsKit_prim_sprite(gs->gsGlobal, sx2, sy1 + ph, sx2 + pw, sy2, 0, rectColor); // right - } else { - gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, rectColor); - } -} - -static void gsDrawLine(Renderer* renderer, float x1, float y1, float x2, float y2, MAYBE_UNUSED float width, uint32_t color, float alpha) { - GsRenderer* gs = (GsRenderer*) renderer; - - uint8_t r = BGR_R(color); - uint8_t g = BGR_G(color); - uint8_t b = BGR_B(color); - uint8_t a = alphaToGS(alpha); - - float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - u64 lineColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - gsKit_prim_line(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, lineColor); -} - -// PS2 gsKit doesn't support per-vertex colors on lines, so we just use color1 -static void gsDrawLineColor(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, MAYBE_UNUSED uint32_t color2, float alpha) { - renderer->vtable->drawLine(renderer, x1, y1, x2, y2, width, color1, alpha); -} - -// Resolved font state shared between gsDrawText and gsDrawTextColor -typedef struct { - Font* font; - GSTEXTURE tex; // GL equivalent: GLuint texId + int32_t texW/texH - AtlasTPAGEntry* atlasEntry; // GL equivalent: TexturePageItem* fontTpag - float ratioX, ratioY; // atlas-to-original scale (GL doesn't need this, uses texW/texH directly) - Sprite* spriteFontSprite; // source sprite for sprite fonts (nullptr for regular fonts) -} GsFontState; - -// Resolves font texture state -// Returns false if the font can't be drawn -static bool gsResolveFontState(GsRenderer* gs, DataWin* dw, Font* font, GsFontState* state) { - state->font = font; - state->atlasEntry = nullptr; - state->ratioX = 1.0f; - state->ratioY = 1.0f; - state->spriteFontSprite = nullptr; - - if (!font->isSpriteFont) { - int32_t fontTpagIndex = font->tpagIndex; - if (0 > fontTpagIndex) return false; - - if (!setupTextureForTPAG(gs, &state->tex, fontTpagIndex)) return false; - - state->atlasEntry = &gs->atlasTPAGEntries[fontTpagIndex]; - TexturePageItem* fontTpag = &dw->tpag.items[fontTpagIndex]; - - float origW = (float) fontTpag->sourceWidth; - float origH = (float) fontTpag->sourceHeight; - state->ratioX = (origW > 0) ? ((float) state->atlasEntry->width / origW) : 1.0f; - state->ratioY = (origH > 0) ? ((float) state->atlasEntry->height / origH) : 1.0f; - } else if (font->spriteIndex >= 0 && dw->sprt.count > (uint32_t) font->spriteIndex) { - state->spriteFontSprite = &dw->sprt.sprites[font->spriteIndex]; - } - return true; -} - -// Resolves UV coordinates, texture ID, and local position for a single glyph -// Returns false if the glyph can't be drawn -static bool gsResolveGlyph(GsRenderer* gs, DataWin* dw, GsFontState* state, FontGlyph* glyph, float cursorX, float cursorY, GSTEXTURE* outTex, float* outU0, float* outV0, float* outU1, float* outV1, float* outLocalX0, float* outLocalY0) { - Font* font = state->font; - if (font->isSpriteFont && state->spriteFontSprite != nullptr) { - Sprite* sprite = state->spriteFontSprite; - int32_t glyphIndex = (int32_t) (glyph - font->glyphs); - if (0 > glyphIndex || glyphIndex >= (int32_t) sprite->textureCount) return false; - - int32_t tpagIdx = sprite->tpagIndices[glyphIndex]; - if (0 > tpagIdx) return false; - - if (!setupTextureForTPAG(gs, outTex, tpagIdx)) return false; - - AtlasTPAGEntry* glyphAtlas = &gs->atlasTPAGEntries[tpagIdx]; - TexturePageItem* glyphTpag = &dw->tpag.items[tpagIdx]; - float gOrigW = (float) glyphTpag->sourceWidth; - float gOrigH = (float) glyphTpag->sourceHeight; - float gRatioX = (gOrigW > 0) ? ((float) glyphAtlas->width / gOrigW) : 1.0f; - float gRatioY = (gOrigH > 0) ? ((float) glyphAtlas->height / gOrigH) : 1.0f; - - *outU0 = (float) glyphAtlas->atlasX; - *outV0 = (float) glyphAtlas->atlasY; - *outU1 = *outU0 + (float) glyph->sourceWidth * gRatioX; - *outV1 = *outV0 + (float) glyph->sourceHeight * gRatioY; - - *outLocalX0 = cursorX + (float) glyph->offset; - *outLocalY0 = cursorY + (float) ((int32_t) glyphTpag->targetY - sprite->originY); - } else { - *outTex = state->tex; - - *outU0 = (float) state->atlasEntry->atlasX + (float) glyph->sourceX * state->ratioX; - *outV0 = (float) state->atlasEntry->atlasY + (float) glyph->sourceY * state->ratioY; - *outU1 = *outU0 + (float) glyph->sourceWidth * state->ratioX; - *outV1 = *outV0 + (float) glyph->sourceHeight * state->ratioY; - - *outLocalX0 = cursorX + (float) glyph->offset; - *outLocalY0 = cursorY; - } - return true; -} - -static void gsDrawText(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, MAYBE_UNUSED float angleDeg) { - GsRenderer* gs = (GsRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > renderer->drawFont || (uint32_t) renderer->drawFont >= dw->font.count) return; - - Font* font = &dw->font.fonts[renderer->drawFont]; - - GsFontState fontState; - if (!gsResolveFontState(gs, dw, font, &fontState)) return; - - // GS modulate mode: Output = Texture * Vertex / 128 - // Scale vertex RGB from 0-255 to 0-128 so white (255) becomes 1.0x multiplier - uint32_t color = renderer->drawColor; - uint8_t a = alphaToGS(renderer->drawAlpha); - uint8_t r = BGR_R(color) >> 1; - uint8_t g = BGR_G(color) >> 1; - uint8_t b = BGR_B(color) >> 1; - u64 textColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - - float screenScaleX = xscale * font->scaleX * gs->scaleX; - float screenScaleY = yscale * font->scaleY * gs->scaleY; - float screenBaseX = (x - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float screenBaseY = (y - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - int32_t textLen = (int32_t) strlen(text); - - // Vertical alignment - int32_t lineCount = TextUtils_countLines(text, textLen); - float lineStride = TextUtils_lineStride(font); - float valignOffset = 0; - if (renderer->drawValign != 0) { - float totalHeight = (float) lineCount * lineStride; - if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f; - else if (renderer->drawValign == 2) valignOffset = -totalHeight; - } - - float cursorY = valignOffset - (float) font->ascenderOffset; - int32_t lineStart = 0; - - while (textLen >= lineStart) { - // Find end of current line - int32_t lineEnd = lineStart; - while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) { - lineEnd++; - } - - int32_t lineLen = lineEnd - lineStart; - const char* line = text + lineStart; - - // Horizontal alignment - float halignOffset = 0; - if (renderer->drawHalign != 0) { - float lineWidth = TextUtils_measureLineWidth(font, line, lineLen); - if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f; - else if (renderer->drawHalign == 2) halignOffset = -lineWidth; - } - - float cursorX = halignOffset; - - // Draw each glyph - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning) - int32_t pos = 0; - uint16_t ch = 0; - bool hasCh = false; - if (lineLen > pos) { - ch = TextUtils_decodeUtf8(line, lineLen, &pos); - hasCh = true; - } - - while (hasCh) { - FontGlyph* glyph = TextUtils_findGlyph(font, ch); - - uint16_t nextCh = 0; - bool hasNext = lineLen > pos; - if (hasNext) nextCh = TextUtils_decodeUtf8(line, lineLen, &pos); - - if (glyph != nullptr) { - bool resolveOk = true; - if (glyph->sourceWidth > 0 && glyph->sourceHeight > 0) { - GSTEXTURE glyphTex; - float u0 = 0, v0 = 0, u1 = 0, v1 = 0; - float localX0, localY0; - - if (gsResolveGlyph(gs, dw, &fontState, glyph, cursorX, cursorY, &glyphTex, &u0, &v0, &u1, &v1, &localX0, &localY0)) { - float sx1 = localX0 * screenScaleX + screenBaseX; - float sy1 = localY0 * screenScaleY + screenBaseY; - float sx2 = sx1 + (float) glyph->sourceWidth * screenScaleX; - float sy2 = sy1 + (float) glyph->sourceHeight * screenScaleY; - - gsKit_prim_sprite_texture(gs->gsGlobal, &glyphTex, sx1, sy1, u0, v0, sx2, sy2, u1, v1, 0, textColor); - } else { - resolveOk = false; - } - } - - cursorX += (float) glyph->shift; - if (resolveOk && hasNext) { - cursorX += TextUtils_getKerningOffset(glyph, nextCh); - } - } - - ch = nextCh; - hasCh = hasNext; - } - - // Next line - cursorY += lineStride; - if (textLen > lineEnd) { - lineStart = TextUtils_skipNewline(text, lineEnd, textLen); - } else { - break; - } - } -} - -static void gsDrawTextColor(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, MAYBE_UNUSED float angleDeg, int32_t _c1, int32_t _c2, int32_t _c3, int32_t _c4, float alpha) { - GsRenderer* gs = (GsRenderer*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > renderer->drawFont || (uint32_t) renderer->drawFont >= dw->font.count) return; - - Font* font = &dw->font.fonts[renderer->drawFont]; - - GsFontState fontState; - if (!gsResolveFontState(gs, dw, font, &fontState)) return; - - int32_t textLen = (int32_t) strlen(text); - if(textLen == 0) return; - - float screenScaleX = xscale * font->scaleX * gs->scaleX; - float screenScaleY = yscale * font->scaleY * gs->scaleY; - float screenBaseX = (x - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float screenBaseY = (y - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - // Vertical alignment - int32_t lineCount = TextUtils_countLines(text, textLen); - float lineStride = TextUtils_lineStride(font); - float totalHeight = (float) lineCount * lineStride; - float valignOffset = 0; - if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f; - else if (renderer->drawValign == 2) valignOffset = -totalHeight; - - float cursorY = valignOffset - (float) font->ascenderOffset; - int32_t lineStart = 0; - - // get delta's (16.16 format) - int32_t left_r_dx = ((_c2 & 0xff0000) - (_c1 & 0xff0000)) / textLen; - int32_t left_g_dx = ((((_c2 & 0xff00) << 8) - ((_c1 & 0xff00) << 8))) / textLen; - int32_t left_b_dx = ((((_c2 & 0xff) << 16) - ((_c1 & 0xff) << 16))) / textLen; - - int32_t right_r_dx = ((_c3 & 0xff0000) - (_c4 & 0xff0000)) / textLen; - int32_t right_g_dx = ((((_c3 & 0xff00) << 8) - ((_c4 & 0xff00) << 8))) / textLen; - int32_t right_b_dx = ((((_c3 & 0xff) << 16) - ((_c4 & 0xff) << 16))) / textLen; - - int32_t left_delta_r = left_r_dx; - int32_t left_delta_g = left_g_dx; - int32_t left_delta_b = left_b_dx; - int32_t right_delta_r = right_r_dx; - int32_t right_delta_g = right_g_dx; - int32_t right_delta_b = right_b_dx; - - int32_t c1 = _c1; - int32_t c4 = _c4; - - while (textLen >= lineStart) { - // do 16.16 maths - int32_t c2 = ((c1 & 0xff0000) + (left_delta_r & 0xff0000)) & 0xff0000; - c2 |= ((c1 & 0xff00) + (left_delta_g >> 8) & 0xff00) & 0xff00; - c2 |= ((c1 & 0xff) + (left_delta_b >> 16)) & 0xff; - int32_t c3 = ((c4 & 0xff0000) + (right_delta_r & 0xff0000)) & 0xff0000; - c3 |= ((c4 & 0xff00) + (right_delta_g >> 8) & 0xff00) & 0xff00; - c3 |= ((c4 & 0xff) + (right_delta_b >> 16)) & 0xff; - - // GS modulate mode: Output = Texture * Vertex / 128 - // Scale vertex RGB from 0-255 to 0-128 so white (255) becomes 1.0x multiplier - uint8_t ga = alphaToGS(alpha); - uint8_t r1 = BGR_R(c1) >> 1; - uint8_t g1 = BGR_G(c1) >> 1; - uint8_t b1 = BGR_B(c1) >> 1; - u64 textColor1 = GS_SETREG_RGBAQ(r1, g1, b1, ga, 0x00); - - uint8_t r2 = BGR_R(c2) >> 1; - uint8_t g2 = BGR_G(c2) >> 1; - uint8_t b2 = BGR_B(c2) >> 1; - u64 textColor2 = GS_SETREG_RGBAQ(r2, g2, b2, ga, 0x00); - - uint8_t r3 = BGR_R(c3) >> 1; - uint8_t g3 = BGR_G(c3) >> 1; - uint8_t b3 = BGR_B(c3) >> 1; - u64 textColor3 = GS_SETREG_RGBAQ(r3, g3, b3, ga, 0x00); - - uint8_t r4 = BGR_R(c4) >> 1; - uint8_t g4 = BGR_G(c4) >> 1; - uint8_t b4 = BGR_B(c4) >> 1; - u64 textColor4 = GS_SETREG_RGBAQ(r4, g4, b4, ga, 0x00); - - left_delta_r += left_r_dx; - left_delta_g += left_g_dx; - left_delta_b += left_b_dx; - right_delta_r += right_r_dx; - right_delta_g += right_g_dx; - right_delta_b += right_b_dx; - - // Find end of current line - int32_t lineEnd = lineStart; - while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) { - lineEnd++; - } - - int32_t lineLen = lineEnd - lineStart; - const char* line = text + lineStart; - - // Horizontal alignment - float lineWidth = TextUtils_measureLineWidth(font, line, lineLen); - float halignOffset = 0; - if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f; - else if (renderer->drawHalign == 2) halignOffset = -lineWidth; - - float cursorX = halignOffset; - - // Draw each glyph - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning) - int32_t pos = 0; - uint16_t ch = 0; - bool hasCh = false; - if (lineLen > pos) { - ch = TextUtils_decodeUtf8(line, lineLen, &pos); - hasCh = true; - } - - while (hasCh) { - FontGlyph* glyph = TextUtils_findGlyph(font, ch); - - uint16_t nextCh = 0; - bool hasNext = lineLen > pos; - if (hasNext) nextCh = TextUtils_decodeUtf8(line, lineLen, &pos); - - if (glyph != nullptr) { - bool resolveOk = true; - if (glyph->sourceWidth > 0 && glyph->sourceHeight > 0) { - GSTEXTURE glyphTex; - float u0 = 0, v0 = 0, u1 = 0, v1 = 0; - float localX0, localY0; - - if (gsResolveGlyph(gs, dw, &fontState, glyph, cursorX, cursorY, &glyphTex, &u0, &v0, &u1, &v1, &localX0, &localY0)) { - float sx1 = localX0 * screenScaleX + screenBaseX; - float sy1 = localY0 * screenScaleY + screenBaseY; - float sx2 = sx1 + (float) glyph->sourceWidth * screenScaleX; - float sy2 = sy1 + (float) glyph->sourceHeight * screenScaleY; - - gsKit_prim_triangle_goraud_texture_3d(gs->gsGlobal, &glyphTex, - sx1, sy1, 0, u0, v0, - sx2, sy1, 0, u1, v0, - sx2, sy2, 0, u1, v1, - textColor1, textColor2, textColor3); - gsKit_prim_triangle_goraud_texture_3d(gs->gsGlobal, &glyphTex, - sx1, sy1, 0, u0, v0, - sx2, sy2, 0, u1, v1, - sx1, sy2, 0, u0, v1, - textColor1, textColor3, textColor4); - } else { - resolveOk = false; - } - } - - cursorX += (float) glyph->shift; - if (resolveOk && hasNext) { - cursorX += TextUtils_getKerningOffset(glyph, nextCh); - } - } - - ch = nextCh; - hasCh = hasNext; - } - - // Next line - cursorY += lineStride; - if (textLen > lineEnd) { - lineStart = TextUtils_skipNewline(text, lineEnd, textLen); - } else { - break; - } - c4 = c3; // set left edge to be what the last right edge was.... - c1 = c2; // - } -} - -static void gsDrawTriangle(Renderer *renderer, float x1, float y1, float x2, float y2, float x3, float y3, bool outline) -{ - GsRenderer* gs = (GsRenderer*) renderer; - if(outline) - { - gsDrawLine(renderer, x1, y1, x2, y2, 1, renderer->drawColor, 1.0); - gsDrawLine(renderer, x2, y2, x3, y3, 1, renderer->drawColor, 1.0); - gsDrawLine(renderer, x3, y3, x1, y1, 1, renderer->drawColor, 1.0); - } else { - float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx3 = (x3 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy3 = (y3 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - float r = (float) BGR_R(renderer->drawColor); - float g = (float) BGR_G(renderer->drawColor); - float b = (float) BGR_B(renderer->drawColor); - - u64 triColor = GS_SETREG_RGBAQ(r, g, b, alphaToGS(renderer->drawAlpha), 0x00); - gsKit_prim_triangle_gouraud_3d(gs->gsGlobal, sx1, sy1, 0,sx2, sy2, 0,sx3, sy3, 0,triColor, triColor, triColor); - } -} - -static void gsFlush(MAYBE_UNUSED Renderer* renderer) { - // No-op: gsKit queues commands, executed in main loop -} - -static int32_t gsCreateSpriteFromSurface(MAYBE_UNUSED Renderer* renderer, MAYBE_UNUSED int32_t x, MAYBE_UNUSED int32_t y, MAYBE_UNUSED int32_t w, MAYBE_UNUSED int32_t h, MAYBE_UNUSED bool removeback, MAYBE_UNUSED bool smooth, MAYBE_UNUSED int32_t xorig, MAYBE_UNUSED int32_t yorig) { - rendererPrintf("GsRenderer: createSpriteFromSurface not supported on PS2\n"); - return -1; -} - -static void gsDeleteSprite(MAYBE_UNUSED Renderer* renderer, MAYBE_UNUSED int32_t spriteIndex) { - // No-op -} - -// PS2 GS only supports a single blend equation: -// Cv = (A - B) * (C / 128) + D -// Where A, B, D pick from {Cs=0, Cd=1, 0=2} and C picks from {As=0, Ad=1, FIX=2}. -// This is a much smaller space than GL's sf*Cs + df*Cd, so most non-trivial GMS blend modes get approximated. -// The cases that DO map exactly are the ones DELTARUNE relies on for the dest-alpha mask trick (bm_dest_alpha + bm_inv_dest_alpha) and standard alpha blending (bm_normal). -// Anything we cannot express falls back to bm_normal. - -// Builds a GS_SETREG_ALPHA value from (sfactor, dfactor) pair semantics: result = sf*Cs + df*Cd. -// Returns true on exact match, false if the pair was approximated. -static bool gmsFactorPairToGSAlpha(int32_t sf, int32_t df, u64* outAlpha) { - // (src_alpha, inv_src_alpha) -> (Cs - Cd) * As + Cd - if (sf == bm_src_alpha && df == bm_inv_src_alpha) { *outAlpha = GS_SETREG_ALPHA(0, 1, 0, 1, 0); return true; } - // (src_alpha, one) -> Cs*As + Cd - if (sf == bm_src_alpha && df == bm_one) { *outAlpha = GS_SETREG_ALPHA(0, 2, 0, 1, 0); return true; } - // (one, one) -> Cs + Cd (FIX=128 makes C/128 == 1) - if (sf == bm_one && df == bm_one) { *outAlpha = GS_SETREG_ALPHA(0, 2, 2, 1, 0x80); return true; } - // (one, inv_src_alpha) -> approximate as bm_normal (premultiplied alpha case) - if (sf == bm_one && df == bm_inv_src_alpha) { *outAlpha = GS_SETREG_ALPHA(0, 1, 0, 1, 0); return true; } - // (zero, one) -> Cd (no source contribution) - if (sf == bm_zero && df == bm_one) { *outAlpha = GS_SETREG_ALPHA(2, 2, 2, 1, 0); return true; } - // (one, zero) -> Cs (replace dest) - if (sf == bm_one && df == bm_zero) { *outAlpha = GS_SETREG_ALPHA(0, 2, 2, 2, 0x80); return true; } - // (zero, zero) -> 0 (clear) - if (sf == bm_zero && df == bm_zero) { *outAlpha = GS_SETREG_ALPHA(2, 2, 2, 2, 0); return true; } - // (dest_alpha, inv_dest_alpha) -> (Cs - Cd) * Ad + Cd - if (sf == bm_dest_alpha && df == bm_inv_dest_alpha) { *outAlpha = GS_SETREG_ALPHA(0, 1, 1, 1, 0); return true; } - // (inv_dest_alpha, dest_alpha) -> (Cd - Cs) * Ad + Cs - if (sf == bm_inv_dest_alpha && df == bm_dest_alpha) { *outAlpha = GS_SETREG_ALPHA(1, 0, 1, 0, 0); return true; } - // (dest_alpha, one) -> Cs*Ad + Cd - if (sf == bm_dest_alpha && df == bm_one) { *outAlpha = GS_SETREG_ALPHA(0, 2, 1, 1, 0); return true; } - // (zero, src_alpha) -> Cd*As (modulate dest by source alpha) - if (sf == bm_zero && df == bm_src_alpha) { *outAlpha = GS_SETREG_ALPHA(2, 1, 0, 1, 0); return true; } - // (zero, inv_src_alpha) -> Cd * (1 - As) -> (0 - Cd) * As + Cd - if (sf == bm_zero && df == bm_inv_src_alpha) { *outAlpha = GS_SETREG_ALPHA(2, 1, 0, 1, 0); return false; } // off-by-(approximation), tolerable - - // Fallback: behave like bm_normal so things stay roughly visible. - *outAlpha = GS_SETREG_ALPHA(0, 1, 0, 1, 0); - return false; -} - -// Maps the simple GMS blend modes (bm_normal/add/subtract/etc) to a GS ALPHA register value. -static u64 gmsBlendModeToGSAlpha(int32_t mode) { - switch (mode) { - case bm_normal: return GS_SETREG_ALPHA(0, 1, 0, 1, 0); // (Cs-Cd)*As + Cd - case bm_add: return GS_SETREG_ALPHA(0, 2, 0, 1, 0); // Cs*As + Cd - case bm_subtract: return GS_SETREG_ALPHA(1, 0, 2, 2, 0x80); // Cd - Cs (clamped to 0) - case bm_reverse_subtract: return GS_SETREG_ALPHA(2, 0, 0, 1, 0); // Cd - Cs*As - case bm_min: return GS_SETREG_ALPHA(0, 1, 0, 1, 0); // No GS min, fall back to normal - case bm_max: return GS_SETREG_ALPHA(0, 1, 0, 1, 0); // No GS max, fall back to normal - default: return GS_SETREG_ALPHA(0, 1, 0, 1, 0); - } -} - -static void gsGpuSetBlendMode(Renderer* renderer, int32_t mode) { - GsRenderer* gs = (GsRenderer*) renderer; - gs->currentBlendAlpha = gmsBlendModeToGSAlpha(mode); - gsCommitBlend(gs); -} - -static void gsGpuSetBlendModeExt(Renderer* renderer, int32_t sfactor, int32_t dfactor) { - GsRenderer* gs = (GsRenderer*) renderer; - u64 alpha; - if (!gmsFactorPairToGSAlpha(sfactor, dfactor, &alpha) && !gs->blendModeWarned) { - fprintf(stderr, "GsRenderer: blend mode (sf=%d, df=%d) not exactly representable on PS2; approximating\n", sfactor, dfactor); - gs->blendModeWarned = true; - } - gs->currentBlendAlpha = alpha; - gsCommitBlend(gs); -} - -static void gsGpuSetBlendEnable(Renderer* renderer, bool enable) { - GsRenderer* gs = (GsRenderer*) renderer; - // PrimAlphaEnable is OR'd into the PRIM bits gsKit emits with each primitive, - // so toggling it affects every subsequent draw without needing to flush. - if (gs->blendEnabled == enable) return; - gs->blendEnabled = enable; - gsCommitBlend(gs); -} - -static void gsGpuSetAlphaTestEnable(Renderer* renderer, bool enable) { - GsRenderer* gs = (GsRenderer*) renderer; - GSGLOBAL* g = gs->gsGlobal; - // Default to GREATER comparison when first enabling. If the ref hasn't been touched yet, - // initialize to 0 so behavior matches GL (test passes when src_alpha > ref). - if (enable) { - g->Test->ATST = 6; // GREATER - g->Test->AFAIL = 0; // KEEP (skip writing entirely) - } - gsKit_set_test(g, enable ? GS_ATEST_ON : GS_ATEST_OFF); -} - -static void gsGpuSetAlphaTestRef(Renderer* renderer, uint8_t ref) { - GsRenderer* gs = (GsRenderer*) renderer; - GSGLOBAL* g = gs->gsGlobal; - g->Test->AREF = alphaToGS(ref); - g->Test->ATST = 6; // GREATER (matches GMS semantics: pass when src_alpha > ref) - g->Test->AFAIL = 0; // KEEP - // Preset 0 doesn't match any branch in gsKit_set_test, so it just re-emits TEST with current state. - gsKit_set_test(g, 0); -} - -static void gsGpuSetColorWriteEnable(Renderer* renderer, bool red, bool green, bool blue, bool alpha) { - GsRenderer* gs = (GsRenderer*) renderer; - // FBMSK: bit=1 means MASK that bit (don't write). Layout is the conceptual RGBA8888 mapping - // even when the framebuffer is CT16 - the GS remaps the relevant bits internally. - u32 fbmsk = 0; - if (!red) fbmsk |= 0x000000FF; - if (!green) fbmsk |= 0x0000FF00; - if (!blue) fbmsk |= 0x00FF0000; - if (!alpha) fbmsk |= 0xFF000000; - gsApplyFBMask(gs, fbmsk); - - // Alpha-only write mode (color writes off, alpha on) is the dest-alpha mask write half: the script wants the source alpha to land in FB.A verbatim, so disable FBA. - // Anything else keeps FBA=1 so normal blended sprites leave FB.A=1 and the mask read half (bm_dest_alpha / bm_inv_dest_alpha) sees opaque pixels. - bool alphaOnly = !red && !green && !blue && alpha; - gsApplyFBA(gs, alphaOnly ? 0 : 1); -} - -static void gsDrawTile(Renderer* renderer, RoomTile* tile, float offsetX, float offsetY) { - GsRenderer* gs = (GsRenderer*) renderer; - - // Look up the tile in the atlas tile entries - AtlasTileEntry* tileEntry = findTileEntry(gs, (int16_t) tile->backgroundDefinition, (uint16_t) tile->sourceX, (uint16_t) tile->sourceY, (uint16_t) tile->width, (uint16_t) tile->height); - if (tileEntry == nullptr) - return; - - // Set up GSTEXTURE for this tile entry - GSTEXTURE tex; - if (!setupTextureForTile(gs, &tex, tileEntry)) - return; - - // Compute screen rect in game coordinates - float drawX = (float) tile->x + offsetX; - float drawY = (float) tile->y + offsetY; - float drawW = (float) tile->width * tile->scaleX; - float drawH = (float) tile->height * tile->scaleY; - - float sx1 = (drawX - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy1 = (drawY - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx2 = (drawX + drawW - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy2 = (drawY + drawH - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - // View frustum culling - float minSX = (sx1 < sx2) ? sx1 : sx2; - float maxSX = (sx1 > sx2) ? sx1 : sx2; - float minSY = (sy1 < sy2) ? sy1 : sy2; - float maxSY = (sy1 > sy2) ? sy1 : sy2; - if (maxSX < 0.0f || minSX > PS2_SCREEN_WIDTH || maxSY < 0.0f || minSY > PS2_SCREEN_HEIGHT) - return; - - // UV coordinates in atlas texels - float u1 = (float) tileEntry->atlasX; - float v1 = (float) tileEntry->atlasY; - float u2 = u1 + (float) tileEntry->width; - float v2 = v1 + (float) tileEntry->height; - - // Extract alpha from tile color high byte, default to 1.0 if 0 - uint8_t alphaByte = (tile->color >> 24) & 0xFF; - float alpha = (alphaByte == 0) ? 1.0f : (float) alphaByte / 255.0f; - uint32_t bgr = tile->color & 0x00FFFFFF; - - // GS modulate mode: scale RGB from 0-255 to 0-128 - uint8_t r = BGR_R(bgr) >> 1; - uint8_t g = BGR_G(bgr) >> 1; - uint8_t b = BGR_B(bgr) >> 1; - uint8_t a = alphaToGS(alpha); - u64 gsColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - - gsKit_prim_sprite_texture(gs->gsGlobal, &tex, sx1, sy1, u1, v1, sx2, sy2, u2, v2, 0, gsColor); -} - -// ===[ Vtable ]=== - -static RendererVtable gsVtable = { - .init = gsInit, - .destroy = gsDestroy, - .beginFrame = gsBeginFrame, - .endFrame = gsEndFrame, - .beginView = gsBeginView, - .endView = gsEndView, - .beginGUI = gsBeginGUI, - .endGUI = gsEndGUI, - .drawSprite = gsDrawSprite, - .drawSpritePos = gsDrawSpritePos, - .drawSpritePart = gsDrawSpritePart, - .drawRectangle = gsDrawRectangle, - .drawLine = gsDrawLine, - .drawLineColor = gsDrawLineColor, - .drawText = gsDrawText, - .drawTextColor = gsDrawTextColor, - .drawTriangle = gsDrawTriangle, - .flush = gsFlush, - .createSpriteFromSurface = gsCreateSpriteFromSurface, - .deleteSprite = gsDeleteSprite, - .gpuSetBlendMode = gsGpuSetBlendMode, - .gpuSetBlendModeExt = gsGpuSetBlendModeExt, - .gpuSetBlendEnable = gsGpuSetBlendEnable, - .gpuSetAlphaTestEnable = gsGpuSetAlphaTestEnable, - .gpuSetAlphaTestRef = gsGpuSetAlphaTestRef, - .gpuSetColorWriteEnable = gsGpuSetColorWriteEnable, - .drawTile = gsDrawTile, - .drawTiled = gsDrawTiled, - .drawTiledPart = gsDrawTiledPart, -}; - -// ===[ Public API ]=== - -Renderer* GsRenderer_create(GSGLOBAL* gsGlobal) { - GsRenderer* gs = safeCalloc(1, sizeof(GsRenderer)); - gs->base.vtable = &gsVtable; - gs->gsGlobal = gsGlobal; - gs->scaleX = 2.0f; - gs->scaleY = 2.0f; - return (Renderer*) gs; -} diff --git a/src/ps2/gs_renderer.h b/src/ps2/gs_renderer.h deleted file mode 100644 index dffeaa03..00000000 --- a/src/ps2/gs_renderer.h +++ /dev/null @@ -1,142 +0,0 @@ -#pragma once - -#include "common.h" -#include "renderer.h" -#include -#include "stb_ds.h" - -// ===[ Atlas Entry (from ATLAS.BIN TPAG entries) ]=== -typedef struct { - uint16_t atlasId; // TEX atlas index (0xFFFF = not mapped) - uint16_t atlasX; // X offset within the atlas - uint16_t atlasY; // Y offset within the atlas - uint16_t width; // Image width in the atlas (post-crop, post-resize) - uint16_t height; // Image height in the atlas (post-crop, post-resize) - uint16_t cropX; // X offset of cropped content within original bounding box - uint16_t cropY; // Y offset of cropped content within original bounding box - uint16_t cropW; // Pre-resize width of the cropped content - uint16_t cropH; // Pre-resize height of the cropped content - uint16_t clutIndex; // CLUT index within the corresponding CLUT file - uint8_t bpp; // 4 or 8 -} AtlasTPAGEntry; - -// ===[ Atlas Tile Entry (from ATLAS.BIN tile entries) ]=== -typedef struct { - int16_t bgDef; // Background definition index - uint16_t srcX; // Source X in the original background image - uint16_t srcY; // Source Y in the original background image - uint16_t srcW; // Original tile width in pixels - uint16_t srcH; // Original tile height in pixels - uint16_t atlasId; // TEX atlas index (0xFFFF = not mapped) - uint16_t atlasX; // X offset within the atlas - uint16_t atlasY; // Y offset within the atlas - uint16_t width; // Tile width in the atlas (post-crop, post-resize) - uint16_t height; // Tile height in the atlas (post-crop, post-resize) - uint16_t cropX; // X offset of cropped content within original tile - uint16_t cropY; // Y offset of cropped content within original tile - uint16_t cropW; // Pre-resize width of the cropped content - uint16_t cropH; // Pre-resize height of the cropped content - uint16_t clutIndex; // CLUT index within the corresponding CLUT file - uint8_t bpp; // 4 or 8 -} AtlasTileEntry; - -// ===[ Tile Lookup Key (for O(1) hashmap lookup) ]=== -typedef struct { - int16_t bgDef; - uint16_t srcX; - uint16_t srcY; - uint16_t srcW; - uint16_t srcH; -} TileLookupKey; - -// stb_ds hashmap entry: TileLookupKey -> AtlasTileEntry* -typedef struct { - TileLookupKey key; - AtlasTileEntry* value; -} TileEntryMap; - -// ===[ VRAM Chunk (buddy system unit) ]=== -// Each chunk is 128KB of VRAM (fits one 4bpp 512x512 atlas). -// An 8bpp atlas uses 2 consecutive chunks. -#define VRAM_CHUNK_SIZE 131072 // 128KB = gsKit_texture_size(512, 512, GS_PSM_T4) - -typedef struct { - int16_t atlasId; // Which atlas occupies this chunk (-1 = free) - uint64_t lastUsed; // Frame number when last accessed -} VRAMChunk; - -// ===[ EE RAM Atlas Cache Entry ]=== -// Caches uncompressed atlas pixel data in EE RAM for zero-copy VRAM uploads to avoid repeated CDVD reads and decompression -typedef struct { - int16_t atlasId; // Which atlas (-1 = free) - uint32_t offset; // Byte offset within eeCache buffer (128-byte aligned) - uint32_t size; // Total bytes stored (uncompressed indexed pixels: 128KB for 4bpp, 256KB for 8bpp) - uint64_t lastUsed; // Frame counter for LRU -} EeAtlasCacheEntry; - -// ===[ GsRenderer Struct ]=== -typedef struct { - Renderer base; // Must be first field for struct embedding - - GSGLOBAL* gsGlobal; - - // View transform state - float scaleX; - float scaleY; - float offsetX; - float offsetY; - int32_t viewX; - int32_t viewY; - - // ATLAS.BIN data - uint16_t atlasTPAGCount; - uint16_t atlasTileCount; - AtlasTPAGEntry* atlasTPAGEntries; - AtlasTileEntry* atlasTileEntries; - TileEntryMap* tileEntryMap; // stb_ds hashmap: (bgDef, srcX, srcY, srcW, srcH) -> AtlasTileEntry* - - // CLUT VRAM addresses (one per CLUT, individually uploaded) - uint32_t clut4Count; // Number of 4bpp CLUTs - uint32_t* clut4VramAddrs; // Per-CLUT VRAM addresses [clut4Count] - - uint32_t clut8Count; // Number of 8bpp CLUTs - uint32_t* clut8VramAddrs; // Per-CLUT VRAM addresses [clut8Count] - - // TEXTURES.BIN file handle (kept open for on-demand atlas loading) - FILE* texturesFile; - uint32_t* atlasOffsets; // Byte offset of each atlas within TEXTURES.BIN [atlasCount] - - // VRAM texture cache (buddy system with LRU eviction) - uint32_t textureVramBase; // Start of texture region in VRAM (after framebuffers + CLUTs) - uint32_t chunkCount; // Number of 128KB chunks available - VRAMChunk* chunks; // Per-chunk state [chunkCount] - int16_t* atlasToChunk; // atlasId -> first chunk index (-1 = not loaded) [atlasCount] - uint16_t atlasCount; // Number of atlas IDs from ATLAS.BIN header - uint8_t* atlasBpp; // Bits per pixel per atlas (4 or 8), from ATLAS.BIN [atlasCount] - uint64_t frameCounter; // Incremented each frame for LRU tracking - bool evictedAtlasUsedInCurrentFrame; // Used for debugging, true if a atlas that was used on the current frame was evicted (VRAM thrashing) - uint16_t uniqueAtlasesThisFrame; // Number of distinct atlases touched this frame - uint16_t chunksNeededThisFrame; // Total VRAM chunks needed by all atlases touched this frame - uint16_t diskLoadsThisFrame; // Number of atlas loads from TEXTURES.BIN this frame (EE cache misses) - - // EE RAM atlas cache (stores uncompressed atlas pixel data for zero-copy VRAM uploads) - uint8_t* eeCache; // Contiguous buffer with uncompressed texture data - uint32_t eeCacheCapacity; // Total size (See EE_CACHE_CAPACITY) - uint32_t eeCacheBumpPtr; // End of live data - EeAtlasCacheEntry* eeCacheEntries; // Per-atlas cache state [atlasCount] - uint32_t* atlasDataSizes; // On-disk size per atlas (header + compressed data) [atlasCount] - - // GPU state (mirrors what was last sent to GS so we can re-apply after sync_flip clobbers FRAME) - uint32_t fbmsk; // Current FRAME register FBMSK (0 = all channels writable) - uint8_t fba; // Current FBA_1 register value (1 = force FB.A bit to 1 on writeback, 0 = pass through) - bool blendModeWarned; // Set the first time an unsupported blend factor pair is seen - - // gsKit packs PrimAlphaEnable into BOTH the PRIM.ABE bit AND TEX0.TCC. - // So toggling ABE off also forces TCC=0, which makes the GS ignore the texture's per-pixel alpha and pull alpha from TA0 (default 0x00) instead. - // That breaks textured alpha-mask sprites. - // To keep TCC=1 always, we leave PrimAlphaEnable=ON and emulate blend-disable by switching ALPHA to an identity equation (Cs passes through unchanged). - bool blendEnabled; // What the GML last requested via gpu_set_blendenable - u64 currentBlendAlpha; // The ALPHA register value the GML last requested via gpu_set_blendmode[_ext] -} GsRenderer; - -Renderer* GsRenderer_create(GSGLOBAL* gsGlobal); diff --git a/src/ps2/gs_renderer_flat.c b/src/ps2/gs_renderer_flat.c deleted file mode 100644 index ec3b91c0..00000000 --- a/src/ps2/gs_renderer_flat.c +++ /dev/null @@ -1,342 +0,0 @@ -#include "gs_renderer_flat.h" - -#include -#include -#include - -#include "utils.h" -#include "text_utils.h" -#include "ps2_utils.h" - -// ===[ Color Generation ]=== -// Generates a unique color for each tpagIndex so sprites are visually distinguishable. -// Uses a simple hash to spread colors across the RGB space. -static u64 colorForTpagIndex(int32_t tpagIndex, float alpha) { - // Golden ratio hash for good color distribution - uint32_t hash = (uint32_t) tpagIndex * 2654435761u; - uint8_t r = (uint8_t) ((hash >> 0) & 0xFF); - uint8_t g = (uint8_t) ((hash >> 8) & 0xFF); - uint8_t b = (uint8_t) ((hash >> 16) & 0xFF); - - // Ensure colors are never too dark (min brightness ~100) - if (128 > r + g + b) { - r = (uint8_t) (r | 0x80); - g = (uint8_t) (g | 0x40); - } - - uint8_t a = alphaToGS(alpha); - return GS_SETREG_RGBAQ(r, g, b, a, 0x00); -} - -// ===[ Vtable Implementations ]=== - -static void gsInit(Renderer* renderer, DataWin* dataWin) { - GsRendererFlat* gs = (GsRendererFlat*) renderer; - - renderer->dataWin = dataWin; - renderer->drawColor = 0xFFFFFF; - renderer->drawAlpha = 1.0f; - renderer->drawFont = -1; - renderer->drawHalign = 0; - renderer->drawValign = 0; - - // Enable alpha blending on all primitives (sets ABE bit in GS PRIM register) - gs->gsGlobal->PrimAlphaEnable = GS_SETTING_ON; - - // Set alpha blend equation: (Cs - Cd) * As / 128 + Cd (standard src-over blend) - gsKit_set_primalpha(gs->gsGlobal, GS_SETREG_ALPHA(0, 1, 0, 1, 0), 0); - - printf("GsRendererFlat: initialized (colored quads mode, no textures)\n"); - printf("GsRendererFlat: %u sprites, %u TPAG items\n", dataWin->sprt.count, dataWin->tpag.count); -} - -static void gsDestroy(Renderer* renderer) { - GsRendererFlat* gs = (GsRendererFlat*) renderer; - free(gs); -} - -static void gsBeginFrame(Renderer* renderer, MAYBE_UNUSED int32_t gameW, MAYBE_UNUSED int32_t gameH, MAYBE_UNUSED int32_t windowW, MAYBE_UNUSED int32_t windowH) {} - -static void gsEndFrame(MAYBE_UNUSED Renderer* renderer) { - // No-op: flip happens in main loop -} - -static void gsBeginView(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, MAYBE_UNUSED int32_t portX, MAYBE_UNUSED int32_t portY, int32_t portW, int32_t portH, MAYBE_UNUSED float viewAngle) { - GsRendererFlat* gs = (GsRendererFlat*) renderer; - gs->viewX = viewX; - gs->viewY = viewY; - - // Scale game view to PS2 screen (640x448 NTSC interlaced) - // Use uniform scale based on width (640/viewW) so pixels stay square. - if (viewW > 0 && viewH > 0) { - gs->scaleX = 640.0f / (float) viewW; - gs->scaleY = gs->scaleX; - } else { - gs->scaleX = 2.0f; - gs->scaleY = 2.0f; - } - - // Center vertically: offset so the rendered image is centered on the 448px screen - float renderedH = (float) viewH * gs->scaleY; - gs->offsetX = 0.0f; - gs->offsetY = (448.0f - renderedH) / 2.0f; -} - -static void gsEndView(MAYBE_UNUSED Renderer* renderer) { - // No-op -} - -static void gsBeginGUI(Renderer* renderer, int32_t guiW, int32_t guiH, MAYBE_UNUSED int32_t portX, MAYBE_UNUSED int32_t portY, MAYBE_UNUSED int32_t portW, MAYBE_UNUSED int32_t portH) { - GsRendererFlat* gs = (GsRendererFlat*) renderer; - gs->viewX = 0; - gs->viewY = 0; - - if (guiW > 0 && guiH > 0) { - gs->scaleX = 640.0f / (float) guiW; - gs->scaleY = gs->scaleX; - } else { - gs->scaleX = 2.0f; - gs->scaleY = 2.0f; - } - - float renderedH = (float) guiH * gs->scaleY; - gs->offsetX = 0.0f; - gs->offsetY = (448.0f - renderedH) / 2.0f; -} - -static void gsEndGUI(MAYBE_UNUSED Renderer* renderer) { - // No-op -} - -static void gsDrawSprite(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, MAYBE_UNUSED float angleDeg, MAYBE_UNUSED uint32_t color, float alpha) { - GsRendererFlat* gs = (GsRendererFlat*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; - - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - float w = (float) tpag->boundingWidth; - float h = (float) tpag->boundingHeight; - - // Compute screen rect in game coordinates - float gameX1 = x - originX * xscale; - float gameY1 = y - originY * yscale; - float gameX2 = x + (w - originX) * xscale; - float gameY2 = y + (h - originY) * yscale; - - // Apply view offset - gameX1 -= (float) gs->viewX; - gameY1 -= (float) gs->viewY; - gameX2 -= (float) gs->viewX; - gameY2 -= (float) gs->viewY; - - // Scale to screen coordinates - float sx1 = gameX1 * gs->scaleX + gs->offsetX; - float sy1 = gameY1 * gs->scaleY + gs->offsetY; - float sx2 = gameX2 * gs->scaleX + gs->offsetX; - float sy2 = gameY2 * gs->scaleY + gs->offsetY; - - u64 quadColor = colorForTpagIndex(tpagIndex, alpha); - gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, quadColor); -} - -static void gsDrawSpritePart(Renderer* renderer, int32_t tpagIndex, MAYBE_UNUSED int32_t srcOffX, MAYBE_UNUSED int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, MAYBE_UNUSED float angleDeg, MAYBE_UNUSED float pivotX, MAYBE_UNUSED float pivotY, MAYBE_UNUSED uint32_t color, float alpha) { - GsRendererFlat* gs = (GsRendererFlat*) renderer; - - if (0 > tpagIndex || (uint32_t) tpagIndex >= renderer->dataWin->tpag.count) return; - - // Compute screen rect - float gameX1 = x - (float) gs->viewX; - float gameY1 = y - (float) gs->viewY; - float gameX2 = gameX1 + (float) srcW * xscale; - float gameY2 = gameY1 + (float) srcH * yscale; - - float sx1 = gameX1 * gs->scaleX + gs->offsetX; - float sy1 = gameY1 * gs->scaleY + gs->offsetY; - float sx2 = gameX2 * gs->scaleX + gs->offsetX; - float sy2 = gameY2 * gs->scaleY + gs->offsetY; - - u64 quadColor = colorForTpagIndex(tpagIndex, alpha); - gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, quadColor); -} - -static void gsDrawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, MAYBE_UNUSED bool outline) { - GsRendererFlat* gs = (GsRendererFlat*) renderer; - - // BGR to RGB - uint8_t r = BGR_R(color); - uint8_t g = BGR_G(color); - uint8_t b = BGR_B(color); - uint8_t a = alphaToGS(alpha); - - float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - u64 rectColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, rectColor); -} - -static void gsDrawLine(Renderer* renderer, float x1, float y1, float x2, float y2, MAYBE_UNUSED float width, uint32_t color, float alpha) { - GsRendererFlat* gs = (GsRendererFlat*) renderer; - - uint8_t r = BGR_R(color); - uint8_t g = BGR_G(color); - uint8_t b = BGR_B(color); - uint8_t a = alphaToGS(alpha); - - float sx1 = (x1 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy1 = (y1 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx2 = (x2 - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy2 = (y2 - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - u64 lineColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - gsKit_prim_line(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, lineColor); -} - -// PS2 gsKit doesn't support per-vertex colors on lines, so we just use color1 -static void gsDrawLineColor(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, MAYBE_UNUSED uint32_t color2, float alpha) { - renderer->vtable->drawLine(renderer, x1, y1, x2, y2, width, color1, alpha); -} - -static void gsDrawText(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, MAYBE_UNUSED float angleDeg) { - GsRendererFlat* gs = (GsRendererFlat*) renderer; - DataWin* dw = renderer->dataWin; - - if (0 > renderer->drawFont || (uint32_t) renderer->drawFont >= dw->font.count) return; - - Font* font = &dw->font.fonts[renderer->drawFont]; - - // BGR to RGB for text color - uint32_t color = renderer->drawColor; - uint8_t r = BGR_R(color); - uint8_t g = BGR_G(color); - uint8_t b = BGR_B(color); - uint8_t a = alphaToGS(renderer->drawAlpha); - u64 textColor = GS_SETREG_RGBAQ(r, g, b, a, 0x00); - - int32_t textLen = (int32_t) strlen(text); - - // Compute vertical alignment offset - int32_t lineCount = TextUtils_countLines(text, textLen); - float lineStride = TextUtils_lineStride(font); - float totalHeight = (float) lineCount * lineStride; - float valignOffset = 0; - if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f; - else if (renderer->drawValign == 2) valignOffset = -totalHeight; - - float cursorY = valignOffset - (float) font->ascenderOffset; - int32_t lineStart = 0; - - while (textLen >= lineStart) { - // Find end of current line - int32_t lineEnd = lineStart; - while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) { - lineEnd++; - } - - int32_t lineLen = lineEnd - lineStart; - const char* line = text + lineStart; - - // Measure line width for horizontal alignment - float lineWidth = TextUtils_measureLineWidth(font, line, lineLen); - float halignOffset = 0; - if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f; - else if (renderer->drawHalign == 2) halignOffset = -lineWidth; - - float cursorX = halignOffset; - - // Draw each glyph as a colored rectangle - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning) - int32_t pos = 0; - uint16_t ch = 0; - bool hasCh = false; - if (lineLen > pos) { - ch = TextUtils_decodeUtf8(line, lineLen, &pos); - hasCh = true; - } - - while (hasCh) { - FontGlyph* glyph = TextUtils_findGlyph(font, ch); - - uint16_t nextCh = 0; - bool hasNext = lineLen > pos; - if (hasNext) nextCh = TextUtils_decodeUtf8(line, lineLen, &pos); - - if (glyph != nullptr) { - if (glyph->sourceWidth > 0 && glyph->sourceHeight > 0) { - float glyphX = x + (cursorX + (float) glyph->offset) * xscale * font->scaleX; - float glyphY = y + cursorY * yscale * font->scaleY; - float glyphW = (float) glyph->sourceWidth * xscale * font->scaleX; - float glyphH = (float) glyph->sourceHeight * yscale * font->scaleY; - - // Apply view offset and scale to screen coordinates - float sx1 = (glyphX - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy1 = (glyphY - (float) gs->viewY) * gs->scaleY + gs->offsetY; - float sx2 = (glyphX + glyphW - (float) gs->viewX) * gs->scaleX + gs->offsetX; - float sy2 = (glyphY + glyphH - (float) gs->viewY) * gs->scaleY + gs->offsetY; - - gsKit_prim_sprite(gs->gsGlobal, sx1, sy1, sx2, sy2, 0, textColor); - } - - cursorX += (float) glyph->shift; - if (hasNext) cursorX += TextUtils_getKerningOffset(glyph, nextCh); - } - - ch = nextCh; - hasCh = hasNext; - } - - // Advance to next line - cursorY += lineStride; - if (textLen > lineEnd) { - lineStart = TextUtils_skipNewline(text, lineEnd, textLen); - } else { - break; - } - } -} - -static void gsFlush(MAYBE_UNUSED Renderer* renderer) { - // No-op: gsKit queues commands, executed in main loop via gsKit_queue_exec -} - -static int32_t gsCreateSpriteFromSurface(MAYBE_UNUSED Renderer* renderer, MAYBE_UNUSED int32_t x, MAYBE_UNUSED int32_t y, MAYBE_UNUSED int32_t w, MAYBE_UNUSED int32_t h, MAYBE_UNUSED bool removeback, MAYBE_UNUSED bool smooth, MAYBE_UNUSED int32_t xorig, MAYBE_UNUSED int32_t yorig) { - fprintf(stderr, "GsRendererFlat: createSpriteFromSurface not supported on PS2\n"); - return -1; -} - -static void gsDeleteSprite(MAYBE_UNUSED Renderer* renderer, MAYBE_UNUSED int32_t spriteIndex) { - // No-op -} - -// ===[ Constructor ]=== - -static RendererVtable gsVtable = { - .init = gsInit, - .destroy = gsDestroy, - .beginFrame = gsBeginFrame, - .endFrame = gsEndFrame, - .beginView = gsBeginView, - .endView = gsEndView, - .beginGUI = gsBeginGUI, - .endGUI = gsEndGUI, - .drawSprite = gsDrawSprite, - .drawSpritePart = gsDrawSpritePart, - .drawRectangle = gsDrawRectangle, - .drawLine = gsDrawLine, - .drawLineColor = gsDrawLineColor, - .drawText = gsDrawText, - .flush = gsFlush, - .createSpriteFromSurface = gsCreateSpriteFromSurface, - .deleteSprite = gsDeleteSprite, -}; - -Renderer* GsRendererFlat_create(GSGLOBAL* gsGlobal) { - GsRendererFlat* gs = safeCalloc(1, sizeof(GsRendererFlat)); - gs->base.vtable = &gsVtable; - gs->gsGlobal = gsGlobal; - gs->scaleX = 2.0f; - gs->scaleY = 2.0f; - return (Renderer*) gs; -} diff --git a/src/ps2/gs_renderer_flat.h b/src/ps2/gs_renderer_flat.h deleted file mode 100644 index 4f49b41d..00000000 --- a/src/ps2/gs_renderer_flat.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "common.h" -#include "renderer.h" -#include - -// ===[ GsRendererFlat Struct ]=== -// Simple PS2 renderer using gsKit ONE SHOT mode. -// Renders all sprites/text as colored rectangles (no textures). -typedef struct { - Renderer base; // Must be first field for struct embedding - - GSGLOBAL* gsGlobal; - - // View transform state (set each view in beginView) - float scaleX; - float scaleY; - float offsetX; - float offsetY; - int32_t viewX; - int32_t viewY; -} GsRendererFlat; - -Renderer* GsRendererFlat_create(GSGLOBAL* gsGlobal); diff --git a/src/ps2/main.c b/src/ps2/main.c deleted file mode 100644 index cb101601..00000000 --- a/src/ps2/main.c +++ /dev/null @@ -1,942 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "runner.h" -#include "runner_keyboard.h" -#include "vm.h" -#include "../data_win.h" -#include "../json_reader.h" -#include "ps2_file_system.h" -#ifdef ENABLE_PS2_AUDIO -#include "ps2_audio_system.h" -#endif -#include "gs_renderer.h" -#include "noop_audio_system.h" -#include "ps2_utils.h" -#include "stb_ds.h" -#include "utils.h" -#include "../profiler.h" - -#ifdef GPROF_PROFILING -#include -#endif - -// Embedded ps2sdk IRX modules (generated by bin2c at build time) -extern unsigned char freesio2_irx[]; -extern unsigned int size_freesio2_irx; -extern unsigned char mcman_irx[]; -extern unsigned int size_mcman_irx; -extern unsigned char mcserv_irx[]; -extern unsigned int size_mcserv_irx; -extern unsigned char freepad_irx[]; -extern unsigned int size_freepad_irx; -extern unsigned char usbd_irx[]; -extern unsigned int size_usbd_irx; -extern unsigned char ps2kbd_irx[]; -extern unsigned int size_ps2kbd_irx; -#ifdef ENABLE_PS2_AUDIO -extern unsigned char freesd_irx[]; -extern unsigned int size_freesd_irx; -extern unsigned char audsrv_irx[]; -extern unsigned int size_audsrv_irx; -#endif - -// Total main RAM in bytes. -static int MAX_MEMORY_BYTES = 0; - -// Heap ceiling: Captured once at startup (before any game allocations) and used as a stable denominator in the debug overlay. -static int heapCeilingBytes = 0; - -// 256-byte aligned buffers for libpad (one per port) -static char padBuf[2][256] __attribute__((aligned(64))); - -// Controller button to GML key mapping -typedef struct { - uint16_t padButton; - int32_t gmlKey; -} PadMapping; - -static PadMapping* pad1Mappings = nullptr; -static int pad1MappingCount = 0; -static PadMapping* pad2Mappings = nullptr; -static int pad2MappingCount = 0; - -// Previous frame's button state per pad (active-low; 0xFFFF = all released) -static uint16_t prevButtons[2] = {0xFFFF, 0xFFFF}; - -// Whether each port was successfully opened (padPortOpen succeeded) -static bool padOpened[2] = {false, false}; - -// Whether each pad was STABLE last frame (for disconnect/reconnect edge detection) -static bool padWasStable[2] = {false, false}; - -static void parsePadMappings(JsonValue* configRoot, const char* key, PadMapping** outMappings, int* outCount, const char* logLabel) { - JsonValue* mappingsObj = JsonReader_getObject(configRoot, key); - if (mappingsObj == nullptr || !JsonReader_isObject(mappingsObj)) return; - int count = JsonReader_objectLength(mappingsObj); - PadMapping* mappings = safeMalloc(sizeof(PadMapping) * count); - repeat(count, i) { - const char* padButtonStr = JsonReader_getObjectKey(mappingsObj, i); - JsonValue* gmlKeyVal = JsonReader_getObjectValue(mappingsObj, i); - mappings[i].padButton = (uint16_t) atoi(padButtonStr); - mappings[i].gmlKey = (int32_t) JsonReader_getInt(gmlKeyVal); - printf("CONFIG.JSN: %s mapping pad=%d -> gmlKey=%d\n", logLabel, mappings[i].padButton, mappings[i].gmlKey); - } - *outMappings = mappings; - *outCount = count; -} - -static void pollPad(Runner* runner, int port, PadMapping* mappings, int mappingCount, uint16_t* prev, bool* wasStable) { - int state = padGetState(port, 0); - bool stable = (state == PAD_STATE_STABLE); - - if (!stable) { - if (*wasStable) { - // Disconnect edge: release every key whose button was held. - repeat(mappingCount, i) { - uint16_t mask = mappings[i].padButton; - if ((*prev & mask) == 0) { - RunnerKeyboard_onKeyUp(runner->keyboard, mappings[i].gmlKey); - } - } - *prev = 0xFFFF; - *wasStable = false; - } - return; - } - - if (!*wasStable) { - // Reconnect edge: avoid phantom presses from buttons that were held when the pad came back. - *prev = 0xFFFF; - *wasStable = true; - } - - struct padButtonStatus padStatus; - unsigned char padResult = padRead(port, 0, &padStatus); - if (padResult == 0) return; - - uint16_t buttons = padStatus.btns; - repeat(mappingCount, i) { - uint16_t mask = mappings[i].padButton; - int32_t gmlKey = mappings[i].gmlKey; - - // PS2 buttons are active-low: 0 = pressed, 1 = released - bool wasPressed = (*prev & mask) == 0; - bool isPressed = (buttons & mask) == 0; - - if (isPressed && !wasPressed) { - RunnerKeyboard_onKeyDown(runner->keyboard, gmlKey); - } else if (!isPressed && wasPressed) { - RunnerKeyboard_onKeyUp(runner->keyboard, gmlKey); - } - } - *prev = buttons; -} - -// ===[ USB Keyboard ]=== - -static bool kbdAvailable = false; - -// Shift modifier state (left-or-right). -static bool kbdShiftHeld = false; - -// Map a USB HID usage code (as delivered by ps2kbd.irx in RAW mode) to a GML VK code. -static int32_t hidUsageToGmlKey(uint8_t hid) { - // Letters: HID 0x04..0x1D -> ASCII 'A'..'Z' (GML uses uppercase ASCII) - if (hid >= 0x04 && hid <= 0x1D) return (int32_t) ('A' + (hid - 0x04)); - // Numbers: HID 0x1E..0x26 -> '1'..'9', 0x27 -> '0' - if (hid >= 0x1E && hid <= 0x26) return (int32_t) ('1' + (hid - 0x1E)); - if (hid == 0x27) return (int32_t) '0'; - // Special keys need mapping - switch (hid) { - case 0x28: return VK_ENTER; // Enter - case 0x29: return VK_ESCAPE; // Escape - case 0x2A: return VK_BACKSPACE; // Backspace - case 0x2B: return VK_TAB; // Tab - case 0x2C: return VK_SPACE; // Space - case 0x3A: return VK_F1; - case 0x3B: return VK_F2; - case 0x3C: return VK_F3; - case 0x3D: return VK_F4; - case 0x3E: return VK_F5; - case 0x3F: return VK_F6; - case 0x40: return VK_F7; - case 0x41: return VK_F8; - case 0x42: return VK_F9; - case 0x43: return VK_F10; - case 0x44: return VK_F11; - case 0x45: return VK_F12; - case 0x49: return VK_INSERT; - case 0x4A: return VK_HOME; - case 0x4B: return VK_PAGEUP; - case 0x4C: return VK_DELETE; - case 0x4D: return VK_END; - case 0x4E: return VK_PAGEDOWN; - case 0x4F: return VK_RIGHT; - case 0x50: return VK_LEFT; - case 0x51: return VK_DOWN; - case 0x52: return VK_UP; - case 0xE0: case 0xE4: return VK_CONTROL; // Left/Right Ctrl - case 0xE1: case 0xE5: return VK_SHIFT; // Left/Right Shift - case 0xE2: case 0xE6: return VK_ALT; // Left/Right Alt - default: return -1; - } -} - -// Translate a HID usage code to an ASCII character for RunnerKeyboard_onCharacter. -// Also handles when the shift key is held. -static unsigned int hidUsageToAsciiChar(uint8_t hid, bool shift) { - // Letters A-Z: HID 0x04..0x1D - if (hid >= 0x04 && hid <= 0x1D) { - char base = (char) ('a' + (hid - 0x04)); - return (unsigned int) (shift ? (base - 32) : base); - } - - // Digits / top-row symbols: 0x1E..0x26 -> 1..9, 0x27 -> 0 - static const char digitsUnshifted[10] = {'1','2','3','4','5','6','7','8','9','0'}; - static const char digitsShifted[10] = {'!','@','#','$','%','^','&','*','(',')'}; - if (hid >= 0x1E && hid <= 0x26) return (unsigned int) (shift ? digitsShifted[hid - 0x1E] : digitsUnshifted[hid - 0x1E]); - if (hid == 0x27) return (unsigned int) (shift ? digitsShifted[9] : digitsUnshifted[9]); - - switch (hid) { - case 0x28: return (unsigned int) '\r'; // Enter - case 0x2A: return (unsigned int) '\b'; // Backspace - case 0x2B: return (unsigned int) '\t'; // Tab - case 0x2C: return (unsigned int) ' '; // Space - case 0x2D: return (unsigned int) (shift ? '_' : '-'); - case 0x2E: return (unsigned int) (shift ? '+' : '='); - case 0x2F: return (unsigned int) (shift ? '{' : '['); - case 0x30: return (unsigned int) (shift ? '}' : ']'); - case 0x31: return (unsigned int) (shift ? '|' : '\\'); - case 0x33: return (unsigned int) (shift ? ':' : ';'); - case 0x34: return (unsigned int) (shift ? '"' : '\''); - case 0x35: return (unsigned int) (shift ? '~' : '`'); - case 0x36: return (unsigned int) (shift ? '<' : ','); - case 0x37: return (unsigned int) (shift ? '>' : '.'); - case 0x38: return (unsigned int) (shift ? '?' : '/'); - default: return 0; - } -} - -// ===[ Loading Screen ]=== - -// Maximum number of chunk stats we track (24 chunks in data.win, but only some have interesting counts) -#define MAX_CHUNK_STATS 24 - -typedef struct { - char label[16]; - uint32_t count; -} ChunkStat; - -typedef struct { - GSGLOBAL* gsGlobal; - GSFONTM* gsFontM; - ChunkStat stats[MAX_CHUNK_STATS]; - int statCount; -} LoadingScreenState; - -// Draws the bottom-left credits text (shared between status screen and loading screen) -static void drawCreditsText(GSGLOBAL* gs, GSFONTM* fontm) { - u64 darkGray = GS_SETREG_RGBAQ(0x70, 0x70, 0x70, 0x80, 0x00); - float creditsScale = 0.4f; - float lineHeight = 26.0f * creditsScale; - float creditsY = 448.0f - 10.0f - lineHeight * 2.0f; - - char versionText[128]; - snprintf(versionText, sizeof(versionText), "Butterscotch (%s) [%s]", BUTTERSCOTCH_COMMIT_HASH, BUTTERSCOTCH_COMMIT_DATE); - gsKit_fontm_print_scaled(gs, fontm, 10.0f, creditsY, 1, creditsScale, darkGray, versionText); - gsKit_fontm_print_scaled(gs, fontm, 10.0f, creditsY + lineHeight, 1, creditsScale, darkGray, "Created by MrPowerGamerBR (https://mrpowergamerbr.com/)"); -} - -// Draws a simple status screen with "Butterscotch" title, optional game name, and a status message (no progress bar) -// gameName can be nullptr if the game name is not yet known -// Begins a status screen: clears, draws title + optional game name, leaves center align active -static void beginStatusScreen(GSGLOBAL* gs, GSFONTM* fontm, const char* gameName) { - gsKit_clear(gs, GS_SETREG_RGBAQ(0x00, 0x00, 0x00, 0x80, 0x00)); - - u64 title = GS_SETREG_RGBAQ(0x5E, 0x54, 0x92, 0x80, 0x00); - u64 gray = GS_SETREG_RGBAQ(0xAA, 0xAA, 0xAA, 0x80, 0x00); - - fontm->Align = GSKIT_FALIGN_CENTER; - gsKit_fontm_print_scaled(gs, fontm, 320.0f, 180.0f, 1, 0.8f, title, "Butterscotch"); - if (gameName) { - gsKit_fontm_print_scaled(gs, fontm, 320.0f, 210.0f, 1, 0.5f, gray, gameName); - } -} - -// Ends a status screen: draws credits, resets align, flips -static void endStatusScreen(GSGLOBAL* gs, GSFONTM* fontm) { - fontm->Align = GSKIT_FALIGN_LEFT; - drawCreditsText(gs, fontm); - gsKit_queue_exec(gs); - gsKit_sync_flip(gs); -} - -// Draws chunk item counts in the top-left corner (if any stats have been recorded) -static void drawChunkStats(GSGLOBAL* gs, GSFONTM* fontm, LoadingScreenState* loadingState) { - if (!loadingState || loadingState->statCount == 0) - return; - - u64 gray = GS_SETREG_RGBAQ(0xAA, 0xAA, 0xAA, 0x80, 0x00); - fontm->Align = GSKIT_FALIGN_LEFT; - float statsY = 10.0f; - float statsScale = 0.35f; - float statsLineHeight = 14.0f; - char statLine[32]; - - repeat(loadingState->statCount, i) { - snprintf(statLine, sizeof(statLine), "%d %s", loadingState->stats[i].count, loadingState->stats[i].label); - gsKit_fontm_print_scaled(gs, fontm, 10.0f, statsY, 1, statsScale, gray, statLine); - statsY += statsLineHeight; - } -} - -static void drawStatusScreen(GSGLOBAL* gs, GSFONTM* fontm, const char* gameName, const char* statusText, LoadingScreenState* loadingState) { - beginStatusScreen(gs, fontm, gameName); - u64 gray = GS_SETREG_RGBAQ(0xAA, 0xAA, 0xAA, 0x80, 0x00); - gsKit_fontm_print_scaled(gs, fontm, 320.0f, 300.0f, 1, 0.5f, gray, statusText); - drawChunkStats(gs, fontm, loadingState); - endStatusScreen(gs, fontm); -} - -static void loadingScreenCallback(const char* chunkName, int chunkIndex, int totalChunks, DataWin* dataWin, void* userData) { - LoadingScreenState* state = (LoadingScreenState*) userData; - GSGLOBAL* gs = state->gsGlobal; - GSFONTM* fontm = state->gsFontM; - - const char* gameName = dataWin->gen8.displayName ? dataWin->gen8.displayName : "Unknown Game"; - beginStatusScreen(gs, fontm, gameName); - - // Loading bar - u64 white = GS_SETREG_RGBAQ(0xFF, 0xFF, 0xFF, 0x80, 0x00); - u64 barBg = GS_SETREG_RGBAQ(0x40, 0x40, 0x40, 0x80, 0x00); - u64 barFg = GS_SETREG_RGBAQ(0xFF, 0xCC, 0x00, 0x80, 0x00); // Butterscotch yellow - - float barX = 120.0f; - float barY = 300.0f; - float barW = 400.0f; - float barH = 20.0f; - float progress = (float) (chunkIndex + 1) / (float) totalChunks; - - // Bar background (dark gray) - gsKit_prim_sprite(gs, barX, barY, barX + barW, barY + barH, 1, barBg); - - // Bar fill (butterscotch yellow) - float fillW = barW * progress; - if (fillW > 1.0f) { - gsKit_prim_sprite(gs, barX, barY, barX + fillW, barY + barH, 1, barFg); - } - - // Enable alpha blending so the font text doesn't have a black box behind it - gs->PrimAlphaEnable = GS_SETTING_ON; - gsKit_set_primalpha(gs, GS_SETREG_ALPHA(0, 1, 0, 1, 0), 0); - - // Percentage text centered on the bar - char percentText[8]; - snprintf(percentText, sizeof(percentText), "%d%%", (int) (progress * 100)); - gsKit_fontm_print_scaled(gs, fontm, 320.0f, barY + 4.5f, 1, 0.4f, white, percentText); - - // Chunk name text below the bar - char statusText[32]; - snprintf(statusText, sizeof(statusText), "Loading %.4s... (%d/%d)", chunkName, chunkIndex + 1, totalChunks); - gsKit_fontm_print_scaled(gs, fontm, 320.0f, barY + barH + 10.0f, 1, 0.5f, white, statusText); - - // Memory usage below the status text - u64 gray = GS_SETREG_RGBAQ(0xAA, 0xAA, 0xAA, 0x80, 0x00); - void* heapTop = sbrk(0); - int32_t usedBytes = (int32_t) (uintptr_t) heapTop; - char memText[48]; - snprintf(memText, sizeof(memText), "Memory: %.1f/%.1f MB", (double) (usedBytes / (1024.0f * 1024.0f)), (double) (MAX_MEMORY_BYTES / (1024.0f * 1024.0f))); - gsKit_fontm_print_scaled(gs, fontm, 320.0f, barY + barH + 30.0f, 1, 0.4f, gray, memText); - - // Record item counts for already-parsed chunks (callback fires before parsing, so we scan all counts each time and add any newly non-zero ones in the order they appear) - typedef struct { uint32_t* countPtr; const char* label; } CountSource; - CountSource sources[] = { - { &dataWin->sond.count, "sounds" }, - { &dataWin->sprt.count, "sprites" }, - { &dataWin->bgnd.count, "backgrounds" }, - { &dataWin->font.count, "fonts" }, - { &dataWin->objt.count, "objects" }, - { &dataWin->room.count, "rooms" }, - { &dataWin->code.count, "code entries" }, - { &dataWin->txtr.count, "textures" }, - }; - - // sizeof(sources) = size of the ENTIRE array - // So, if we divide the size of the ENTIRE array by the size of a SINGLE entry, we get the number of entries - int arrayLength = sizeof(sources) / sizeof(CountSource); - - repeat(arrayLength, i) { - if (*sources[i].countPtr == 0) - continue; - - // Check if we already recorded this label - bool found = false; - forEach(CountSource, stat, sources, state->statCount) { - if (strcmp(stat->label, sources[i].label) == 0) { - found = true; - break; - } - } - - if (!found && MAX_CHUNK_STATS > state->statCount) { - ChunkStat* stat = &state->stats[state->statCount++]; - snprintf(stat->label, sizeof(stat->label), "%s", sources[i].label); - stat->count = *sources[i].countPtr; - } - } - - drawChunkStats(gs, fontm, state); - - gs->PrimAlphaEnable = GS_SETTING_OFF; - - endStatusScreen(gs, fontm); -} - -int main(int argc, char* argv[]) { - SifInitRpc(0); - sbv_patch_enable_lmb(); - - // Ask the kernel how much main RAM we actually have. - MAX_MEMORY_BYTES = (int) GetMemorySize(); - - // Snapshot the heap ceiling BEFORE anything else allocates. sbrk(0) at this point is the heap frontier after newlib's baseline reservations; mallinfo.uordblks is what newlib has already handed out to startup code. Their sum (added to the sbrk runway) is the total bytes user code could ever cumulatively hold live. - { - struct mallinfo mi = mallinfo(); - heapCeilingBytes = (MAX_MEMORY_BYTES - (int) (uintptr_t) sbrk(0)) + mi.uordblks; - } - - PS2Utils_extractDeviceKey(argv[0]); - - fprintf(stderr, "argv0 is %s, device key is %s\n", argv[0], deviceKey.key); - - PS2Utils_loadFSDrivers(); - - fprintf(stderr, "Loaded FS drivers!\n"); - - const char* dataWinPath = PS2Utils_createDevicePath("DATA.WIN"); - - printf("Butterscotch PS2 - Loading %s\n", dataWinPath); - - // ===[ Initialize gsKit ]=== - // This must happen first so we can show the loading screen during other init steps - GSGLOBAL* gsGlobal = gsKit_init_global(); - gsGlobal->Mode = GS_MODE_NTSC; - gsGlobal->Interlace = GS_INTERLACED; - gsGlobal->Field = GS_FIELD; - gsGlobal->Width = 640; - gsGlobal->Height = 448; - gsGlobal->PSM = GS_PSM_CT16; - gsGlobal->PSMZ = GS_PSMZ_16; - gsGlobal->DoubleBuffering = GS_SETTING_ON; - gsGlobal->ZBuffering = GS_SETTING_OFF; - - gsGlobal->PrimAAEnable = GS_SETTING_OFF; - - dmaKit_init(D_CTRL_RELE_OFF, D_CTRL_MFD_OFF, D_CTRL_STS_UNSPEC, D_CTRL_STD_OFF, D_CTRL_RCYC_8, 1 << DMA_CHANNEL_GIF); - dmaKit_chan_init(DMA_CHANNEL_GIF); - - gsKit_init_screen(gsGlobal); - // Use ONE SHOT mode - gsKit_mode_switch(gsGlobal, GS_ONESHOT); - - // ===[ Initialize FONTM (ROM font) for debug overlay ]=== - GSFONTM* gsFontM = gsKit_init_fontm(); - gsKit_fontm_upload(gsGlobal, gsFontM); - gsFontM->Spacing = 0.95f; - - // ===[ Initialize Controller ]=== - drawStatusScreen(gsGlobal, gsFontM, nullptr, "Initializing controller...", nullptr); - - int ret; - ret = SifExecModuleBuffer(freesio2_irx, size_freesio2_irx, 0, nullptr, nullptr); - if (0 > ret) { - printf("Failed to load freesio2: %d\n", ret); - return 1; - } - ret = SifExecModuleBuffer(mcman_irx, size_mcman_irx, 0, nullptr, nullptr); - if (0 > ret) { - printf("Failed to load mcman: %d\n", ret); - return 1; - } - ret = SifExecModuleBuffer(mcserv_irx, size_mcserv_irx, 0, nullptr, nullptr); - if (0 > ret) { - printf("Failed to load mcserv: %d\n", ret); - return 1; - } - ret = mcInit(MC_TYPE_MC); - if (0 > ret) { - printf("Failed to init libmc: %d\n", ret); - return 1; - } - ret = SifExecModuleBuffer(freepad_irx, size_freepad_irx, 0, nullptr, nullptr); - if (0 > ret) { - printf("Failed to load freepad: %d\n", ret); - return 1; - } - - padInit(0); - padOpened[0] = (padPortOpen(0, 0, padBuf[0]) != 0); - padOpened[1] = (padPortOpen(1, 0, padBuf[1]) != 0); - if (!padOpened[0]) printf("Warning: failed to open pad port 0\n"); - if (!padOpened[1]) printf("Warning: failed to open pad port 1\n"); - - // ===[ Load USB Keyboard IOP Modules ]=== - int usbdRet = SifExecModuleBuffer(usbd_irx, size_usbd_irx, 0, nullptr, nullptr); - if (0 > usbdRet) { - printf("Warning: failed to load usbd: %d (keyboard disabled)\n", usbdRet); - } else { - int kbdRet = SifExecModuleBuffer(ps2kbd_irx, size_ps2kbd_irx, 0, nullptr, nullptr); - if (0 > kbdRet) { - printf("Warning: failed to load ps2kbd: %d (keyboard disabled)\n", kbdRet); - } else if (PS2KbdInit() == 0) { - printf("Warning: PS2KbdInit failed (keyboard disabled)\n"); - } else { - PS2KbdSetReadmode(PS2KBD_READMODE_RAW); - PS2KbdSetBlockingMode(PS2KBD_NONBLOCKING); - kbdAvailable = true; - printf("USB keyboard initialized\n"); - } - } - -#ifdef ENABLE_PS2_AUDIO - // ===[ Load Audio IOP Modules ]=== - ret = SifExecModuleBuffer(freesd_irx, size_freesd_irx, 0, nullptr, nullptr); - if (0 > ret) { - printf("Failed to load freesd: %d\n", ret); - } - ret = SifExecModuleBuffer(audsrv_irx, size_audsrv_irx, 0, nullptr, nullptr); - if (0 > ret) { - printf("Failed to load audsrv: %d\n", ret); - } -#endif - - // Wait for pad to be ready - drawStatusScreen(gsGlobal, gsFontM, nullptr, "Waiting for controller...", nullptr); - - int padState; - do { - padState = padGetState(0, 0); - } while (PAD_STATE_STABLE != padState && PAD_STATE_FINDCTP1 != padState); - - printf("Controller initialized\n"); - - // ===[ Loading Screen State ]=== - LoadingScreenState loadingState = { - .gsGlobal = gsGlobal, - .gsFontM = gsFontM, - }; - - // ===[ Load CONFIG.JSN ]=== - drawStatusScreen(gsGlobal, gsFontM, nullptr, "Loading CONFIG.JSN...", nullptr); - - char* configJsonPath = PS2Utils_createDevicePath("CONFIG.JSN"); - FILE* configFile = fopen(configJsonPath, "rb"); - JsonValue* configRoot = nullptr; - - if (configFile != nullptr) { - fseek(configFile, 0, SEEK_END); - long configSize = ftell(configFile); - fseek(configFile, 0, SEEK_SET); - - char* configJsonText = safeMalloc((size_t) configSize + 1); - size_t configBytesRead = fread(configJsonText, 1, (size_t) configSize, configFile); - configJsonText[configBytesRead] = '\0'; - fclose(configFile); - - configRoot = JsonReader_parse(configJsonText); - free(configJsonText); - } - free(configJsonPath); - - if (configRoot == nullptr) { - drawStatusScreen(gsGlobal, gsFontM, nullptr, "CONFIG.JSN invalid or not found!", nullptr); - while (true) {} - } - - bool lazyLoadRooms = JsonReader_getBool(JsonReader_getObject(configRoot, "lazyLoadRooms")); - StringBooleanEntry* eagerRooms = nullptr; // stb_ds string-keyed set; keys borrowed from configRoot - JsonValue* eagerArr = JsonReader_getObject(configRoot, "eagerlyLoadedRooms"); - int n = JsonReader_arrayLength(eagerArr); - repeat(n, i) { - const char* name = JsonReader_getString(JsonReader_getArrayElement(eagerArr, i)); - if (name != nullptr) shput(eagerRooms, (char*) name, true); - } - - // ===[ Parse data.win ]=== - drawStatusScreen(gsGlobal, gsFontM, nullptr, "Loading data.win...", nullptr); - - DataWin* dataWin = DataWin_parse( - dataWinPath, - (DataWinParserOptions) { - .parseGen8 = true, - .parseOptn = true, - .parseLang = true, - .parseExtn = false, - .parseSond = true, - .parseAgrp = true, - .parseSprt = true, - .parseBgnd = true, - .parsePath = true, - .parseScpt = true, - .parseGlob = true, - .parseShdr = true, - .parseFont = true, - .parseTmln = true, - .parseObjt = true, - .parseRoom = true, - .parseTpag = true, - .parseCode = true, - .parseVari = true, - .parseFunc = true, - .parseStrg = true, - .parseTxtr = false, - .parseAudo = false, - .skipLoadingPreciseMasksForNonPreciseSprites = true, - .lazyLoadRooms = lazyLoadRooms, - .eagerlyLoadedRooms = eagerRooms, - .progressCallback = loadingScreenCallback, - .progressCallbackUserData = &loadingState, - } - ); - free(dataWinPath); - shfree(eagerRooms); - - bool bytecodeVersionSupported = false; -#ifdef ENABLE_BC16 - if (dataWin->gen8.bytecodeVersion == 15 || dataWin->gen8.bytecodeVersion == 16) bytecodeVersionSupported = true; -#endif -#ifdef ENABLE_BC17 - if (dataWin->gen8.bytecodeVersion == 17) bytecodeVersionSupported = true; -#endif - - if (!bytecodeVersionSupported) { - char errorText[128]; - snprintf(errorText, sizeof(errorText), "Unsupported bytecode version %u!", dataWin->gen8.bytecodeVersion); - drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, errorText, &loadingState); - while (true) {} - } - - { - void* heapTop = sbrk(0); - int32_t usedBytes = (int32_t) (uintptr_t) heapTop; - int32_t freeBytes = MAX_MEMORY_BYTES - usedBytes; - printf("Memory after data.win parsing: used=%d bytes (%.1f KB), total=%d bytes (%.1f KB), free=%d bytes (%.1f KB)\n", usedBytes, (double) (usedBytes / 1024.0f), MAX_MEMORY_BYTES, (double) (MAX_MEMORY_BYTES / 1024.0f), freeBytes, (double) (freeBytes / 1024.0f)); - } - - FileSystem* fileSystem = Ps2FileSystem_create(configRoot, dataWin->gen8.displayName); - if (fileSystem == nullptr) { - drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "CONFIG.JSN is missing the fileSystem configuration!", &loadingState); - while (true) {} - } - - drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Creating VM...", &loadingState); - - VMContext* vm = VM_create(dataWin); - - // ===[ Initialize Renderer ]=== - drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Initializing renderer...", &loadingState); - - Renderer* renderer = GsRenderer_create(gsGlobal); - - // ===[ Initialize Audio System ]=== -#ifdef ENABLE_PS2_AUDIO - drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Initializing audio...", &loadingState); - Ps2AudioSystem* ps2Audio = Ps2AudioSystem_create(); - AudioSystem* audioSystem = (AudioSystem*) ps2Audio; -#else - AudioSystem* audioSystem = (AudioSystem*) NoopAudioSystem_create(); -#endif - - drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Creating runner...", &loadingState); - - Runner* runner = Runner_create(dataWin, vm, renderer, fileSystem, audioSystem); - - // Parse disabledObjects from CONFIG.JSN - JsonValue* disabledObjectsArr = JsonReader_getObject(configRoot, "disabledObjects"); - if (disabledObjectsArr != nullptr && JsonReader_isArray(disabledObjectsArr)) { - sh_new_strdup(runner->disabledObjects); - int disabledCount = JsonReader_arrayLength(disabledObjectsArr); - repeat(disabledCount, i) { - JsonValue* elem = JsonReader_getArrayElement(disabledObjectsArr, i); - if (elem != nullptr && JsonReader_isString(elem)) { - const char* objName = JsonReader_getString(elem); - shput(runner->disabledObjects, objName, 1); - printf("Disabled object: %s\n", objName); - } - } - } - - // Parse pad mappings from CONFIG.JSN (one object per controller, both optional) - parsePadMappings(configRoot, "controller1Mappings", &pad1Mappings, &pad1MappingCount, "controller1"); - parsePadMappings(configRoot, "controller2Mappings", &pad2Mappings, &pad2MappingCount, "controller2"); - - { - void* heapTop = sbrk(0); - int32_t usedBytes = (int32_t) (uintptr_t) heapTop; - int32_t freeBytes = MAX_MEMORY_BYTES - usedBytes; - printf("Memory after VM and runner creation: used=%d bytes (%.1f KB), total=%d bytes (%.1f KB), free=%d bytes (%.1f KB)\n", usedBytes, (double) (usedBytes / 1024.0f), MAX_MEMORY_BYTES, (double) (MAX_MEMORY_BYTES / 1024.0f), freeBytes, (double) (freeBytes / 1024.0f)); - } - - drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Initializing first room...", &loadingState); - Runner_initFirstRoom(runner); - - drawStatusScreen(gsGlobal, gsFontM, dataWin->gen8.displayName, "Reticulating splines...", &loadingState); - - // ===[ gprof Profiler Setup ]=== -#ifdef GPROF_PROFILING - // Reset profiler to ignore any data collected during the initialization steps - gprof_stop(nullptr, 0); - - // If not running from host:, load USB mass storage drivers so we can write gmon.out to mass: - if (strcmp(deviceKey.key, "host") != 0) - PS2Utils_loadMassStorageDrivers(); - - gprof_start(); - fprintf(stderr, "gprof: Profiling started!\n"); -#endif - - Gen8* gen8 = &dataWin->gen8; - int32_t gameW = (int32_t) gen8->defaultWindowWidth; - int32_t gameH = (int32_t) gen8->defaultWindowHeight; - - // ===[ Initialize Timer ]=== - InitTimer(kBUSCLK); - StartTimerSystemTime(); - - // ===[ Main Loop ]=== - bool debugOverlayStartEnabled = JsonReader_getBool(JsonReader_getObject(configRoot, "debugOverlayEnabled")); - int debugOverlayState = debugOverlayStartEnabled ? 0 : 2; - uint16_t prevOverlayPadButtons = 0xFFFF; - int profilerFramesInWindow = 0; - static const int PROFILER_WINDOW_FRAMES = 60; -#ifdef ENABLE_VM_GML_PROFILER - char profilerOverlayText[4096]; -#endif - while (!runner->shouldExit) { - u64 frameStartTime = GetTimerSystemTime(); - // ===[ Poll Controller (always poll every vsync) ]=== - // NOTE: We do NOT call RunnerKeyboard_beginFrame here! Pressed/released edges accumulate across vsyncs so that quick taps on non-game-frame - // vsyncs are not lost - // - // beginFrame is called after the game consumes input. - - if (padOpened[0]) pollPad(runner, 0, pad1Mappings, pad1MappingCount, &prevButtons[0], &padWasStable[0]); - if (padOpened[1]) pollPad(runner, 1, pad2Mappings, pad2MappingCount, &prevButtons[1], &padWasStable[1]); - - // ===[ Poll USB Keyboard ]=== - // Drain all pending RAW events this vsync so press/release edges aren't dropped. - if (kbdAvailable) { - PS2KbdRawKey rawKey; - while (PS2KbdReadRaw(&rawKey) > 0) { - int32_t gmlKey = hidUsageToGmlKey(rawKey.key); - - // Track shift modifier locally so we can pick the correct glyph for onCharacter. - if (rawKey.key == 0xE1 || rawKey.key == 0xE5) { - kbdShiftHeld = (rawKey.state == PS2KBD_RAWKEY_DOWN); - } - - if (rawKey.state == PS2KBD_RAWKEY_DOWN) { - if (gmlKey >= 0) RunnerKeyboard_onKeyDown(runner->keyboard, gmlKey); - unsigned int ch = hidUsageToAsciiChar(rawKey.key, kbdShiftHeld); - if (ch != 0) RunnerKeyboard_onCharacter(runner->keyboard, ch); - } else if (rawKey.state == PS2KBD_RAWKEY_UP) { - if (gmlKey >= 0) RunnerKeyboard_onKeyUp(runner->keyboard, gmlKey); - } - } - } - - // R2 on pad1 removes speed cap (ignore waiting for vsync) - bool speedCapRemoved = padWasStable[0] && ((prevButtons[0] & PAD_R2) == 0); - - // Go to next room - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_PAGEUP)) { - DataWin* dw = runner->dataWin; - if ((int32_t) dw->gen8.roomOrderCount > runner->currentRoomOrderPosition + 1) { - int32_t nextIdx = dw->gen8.roomOrder[runner->currentRoomOrderPosition + 1]; - runner->pendingRoom = nextIdx; - runner->audioSystem->vtable->stopAll(runner->audioSystem); - fprintf(stderr, "Debug: Going to next room -> %s\n", dw->room.rooms[nextIdx].name); - } - } - - // Go to previous room - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_PAGEDOWN)) { - DataWin* dw = runner->dataWin; - forEachIndexed(Room, room, i, dw->room.rooms, dw->room.count) { - if (strcmp(room->name, "room_asrielappears") == 0) { - runner->pendingRoom = i; - runner->audioSystem->vtable->stopAll(runner->audioSystem); - break; - } - } - } - - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F12)) { - debugOverlayState = (debugOverlayState + 1) % 3; -#ifdef ENABLE_VM_GML_PROFILER - Profiler_setEnabled(&vm->profiler, debugOverlayState == 1); - profilerFramesInWindow = 0; - profilerOverlayText[0] = '\0'; -#endif - } - - // Reset global interact state because I HATE when I get stuck while moving through rooms - if (RunnerKeyboard_checkPressed(runner->keyboard, VK_F10)) { - int32_t interactVarId = shget(runner->vmContext->globalVarNameMap, "interact"); - - runner->vmContext->globalVars[interactVarId] = RValue_makeInt32(0); - printf("Changed global.interact [%d] value!\n", interactVarId); - } - - // ===[ Game Logic ]=== - uint32_t roomSpeed = runner->currentRoom->speed; - - u64 stepStartTime = GetTimerSystemTime(); - Runner_step(runner); - u64 stepEndTime = GetTimerSystemTime(); - - gsKit_clear(gsGlobal, GS_SETREG_RGBAQ(0x00, 0x00, 0x00, 0x80, 0x00)); - - renderer->vtable->beginFrame(renderer, gameW, gameH, 640, 448); - - // Clear with room background color - if (runner->drawBackgroundColor) { - uint8_t bgR = BGR_R(runner->backgroundColor); - uint8_t bgG = BGR_G(runner->backgroundColor); - uint8_t bgB = BGR_B(runner->backgroundColor); - u64 bgColor = GS_SETREG_RGBAQ(bgR, bgG, bgB, 0x80, 0x00); - gsKit_prim_sprite(gsGlobal, 0, 0, 640, 448, 0, bgColor); - } - - // Render views - u64 drawStartTime = GetTimerSystemTime(); - Runner_drawViews(runner, gameW, gameH, 1.0f, 1.0f, false); - u64 drawEndTime = GetTimerSystemTime(); - - runner->viewCurrent = 0; - - renderer->vtable->endFrame(renderer); - - // Clear pressed/released edges after both Step and Draw have consumed input - // This MUST be after Runner_draw because games CAN handle input in Draw events (e.g. Undertale's naming screen) - RunnerKeyboard_beginFrame(runner->keyboard); - - u64 audioStartTime = GetTimerSystemTime(); - // Update audio system (gain fading, stream to audsrv) - float dt = 1.0f / (float) roomSpeed; - if (0.0f > dt) dt = 0.0f; - if (dt > 0.1f) dt = 0.1f; - runner->audioSystem->vtable->update(runner->audioSystem, dt); - u64 audioEndTime = GetTimerSystemTime(); - - u64 runnerEndTime = GetTimerSystemTime(); - u64 duration = runnerEndTime - frameStartTime; - u64 stepDuration = stepEndTime - stepStartTime; - u64 drawDuration = drawEndTime - drawStartTime; - u64 audioDuration = audioEndTime - audioStartTime; - float tickTime = (float) duration / (float) (kBUSCLK / 1000); - float stepTime = (float) stepDuration / (float) (kBUSCLK / 1000); - float drawTime = (float) drawDuration / (float) (kBUSCLK / 1000); - float audioTime = (float) audioDuration / (float) (kBUSCLK / 1000); - - // ===[ Debug Overlay ]=== - if (debugOverlayState == 0 || debugOverlayState == 1) { - u64 debugColor = GS_SETREG_RGBAQ(0xFF, 0xFF, 0xFF, 0x80, 0x00); - char debugText[512]; - uint32_t vramFreeBytes = GS_VRAM_SIZE - gsGlobal->CurrentPointer; - - // Count atlases loaded in VRAM and EE RAM cache - GsRenderer* gsRenderer = (GsRenderer*) renderer; - uint32_t vramAtlasCount = 0; - uint32_t eeramAtlasCount = 0; - repeat(gsRenderer->atlasCount, ai) { - if (gsRenderer->atlasToChunk[ai] >= 0) vramAtlasCount++; - if (gsRenderer->eeCacheEntries[ai].atlasId >= 0) eeramAtlasCount++; - } - - int freeBytes = heapCeilingBytes - mallinfo().uordblks; - - const char* roomName = runner->currentRoom != nullptr && runner->currentRoom->name != nullptr ? runner->currentRoom->name : "?"; - - const char* thrashIndicator = ""; - if (gsRenderer->chunksNeededThisFrame > gsRenderer->chunkCount) { - thrashIndicator = gsRenderer->diskLoadsThisFrame > 0 ? " [RAM+DISK THRASHING]" : " [RAM THRASHING]"; - } else if (gsRenderer->diskLoadsThisFrame > 0) { - thrashIndicator = " [DISK LOAD]"; - } - - snprintf(debugText, sizeof(debugText), "Room: %s\nTick: %.2fms\nStep: %.2fms\nDraw: %.2fms\nAudio: %.2fms\nFree: %d bytes\nVRAM Free: %lu bytes\nRoom Speed: %u%s\nAtlas: (%u, %u, %u) [%u/%u]%s\nInstances: %d\nStructs: %d", roomName, (double) tickTime, (double) stepTime, (double) drawTime, (double) audioTime, freeBytes, (unsigned long) vramFreeBytes, roomSpeed, speedCapRemoved ? " [UNCAPPED]" : "", vramAtlasCount, eeramAtlasCount, gsRenderer->atlasCount, gsRenderer->chunksNeededThisFrame, gsRenderer->chunkCount, thrashIndicator, (int) arrlen(runner->instances), (int) arrlen(runner->structInstances)); - gsKit_fontm_print_scaled(gsGlobal, gsFontM, 10.0f, 10.0f, 10, 0.6f, debugColor, debugText); - - if (debugOverlayState == 1) { - float profilerY = 10.0f + (15.6f * 10.0f) + 6.0f; - -#ifdef ENABLE_VM_GML_PROFILER - profilerFramesInWindow++; - if (profilerFramesInWindow >= PROFILER_WINDOW_FRAMES) { - char* profilerReport = Profiler_createReport(vm->profiler, 25, profilerFramesInWindow); - if (profilerReport != nullptr) { - snprintf(profilerOverlayText, sizeof(profilerOverlayText), "%s", profilerReport); - free(profilerReport); - } - Profiler_reset(vm->profiler); - profilerFramesInWindow = 0; - } - const char* profilerDisplay = profilerOverlayText[0] != '\0' ? profilerOverlayText : "GML Profiler (collecting...)"; - gsKit_fontm_print_scaled(gsGlobal, gsFontM, 10.0f, profilerY, 10, 0.35f, debugColor, profilerDisplay); -#else - gsKit_fontm_print_scaled(gsGlobal, gsFontM, 10.0f, profilerY, 10, 0.35f, debugColor, "Butterscotch GML Profiler is disabled on this build :("); -#endif - } - } - - // Execute draw queue and flip buffers - gsKit_queue_exec(gsGlobal); - gsKit_sync_flip(gsGlobal); - - // Busy-wait until enough time has elapsed for this frame if needed - if (!speedCapRemoved && roomSpeed > 0) { - u64 targetTicks = kBUSCLK / roomSpeed; - while (targetTicks > GetTimerSystemTime() - frameStartTime) { - // spin spin spin! - } - } - } - - // ===[ gprof Profiler Output ]=== -#ifdef GPROF_PROFILING - { - const char* gprofPath; - if (strcmp(deviceKey.key, "host") == 0) { - gprofPath = "host:gmon.out"; - } else { - gprofPath = "mass:gmon.out"; - } - fprintf(stderr, "gprof: Writing profiling data to %s\n", gprofPath); - gprof_stop(gprofPath, 1); - fprintf(stderr, "gprof: Done\n"); - } -#endif - - // For some god awful reason PCSX2 crashes when destroying the audio system (???) - // runner->audioSystem->vtable->destroy(runner->audioSystem); - runner->audioSystem = nullptr; - renderer->vtable->destroy(renderer); - DataWin_free(dataWin); - - return 0; -} diff --git a/src/ps2/ps2_audio_system.c b/src/ps2/ps2_audio_system.c deleted file mode 100644 index 848b1353..00000000 --- a/src/ps2/ps2_audio_system.c +++ /dev/null @@ -1,1350 +0,0 @@ -#include "ps2_audio_system.h" -#include "ps2_utils.h" -#include "utils.h" - -#include -#include -#include -#include -#include - -// ===[ IMA ADPCM Tables ]=== - -static const int16_t IMA_STEP_TABLE[89] = { - 7, 8, 9, 10, 11, 12, 13, 14, - 16, 17, 19, 21, 23, 25, 28, 31, - 34, 37, 41, 45, 50, 55, 60, 66, - 73, 80, 88, 97, 107, 118, 130, 143, - 157, 173, 190, 209, 230, 253, 279, 307, - 337, 371, 408, 449, 494, 544, 598, 658, - 724, 796, 876, 963, 1060, 1166, 1282, 1411, - 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, - 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, - 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, - 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, - 32767 -}; - -static const int8_t IMA_INDEX_TABLE[16] = { - -1, -1, -1, -1, 2, 4, 6, 8, - -1, -1, -1, -1, 2, 4, 6, 8 -}; - -// ===[ IMA ADPCM Decoder ]=== - -// Decode a block of IMA ADPCM data, updating predictor/stepIndex state in place -// Returns the number of samples written to outPcm (2 per input byte) -static uint32_t imaAdpcmDecodeBlock(const uint8_t* adpcmData, uint32_t adpcmSize, int16_t* outPcm, int32_t* predictor, int32_t* stepIndex) { - uint32_t samplesWritten = 0; - - repeat(adpcmSize, i) { - uint8_t byte = adpcmData[i]; - - // Process two nibbles per byte (low nibble first, then high) - for (int nibbleIdx = 0; 2 > nibbleIdx; nibbleIdx++) { - uint8_t nibble = (nibbleIdx == 0) ? (byte & 0x0F) : ((byte >> 4) & 0x0F); - - int32_t step = IMA_STEP_TABLE[*stepIndex]; - int32_t delta = step >> 3; - if (nibble & 1) delta += step >> 2; - if (nibble & 2) delta += step >> 1; - if (nibble & 4) delta += step; - if (nibble & 8) delta = -delta; - - *predictor += delta; - if (*predictor > 32767) *predictor = 32767; - if (-32768 > *predictor) *predictor = -32768; - - *outPcm++ = (int16_t) *predictor; - samplesWritten++; - - *stepIndex += IMA_INDEX_TABLE[nibble]; - if (0 > *stepIndex) *stepIndex = 0; - if (*stepIndex > 88) *stepIndex = 88; - } - } - - return samplesWritten; -} - -// Convenience wrapper for full-buffer decode (SFX path) -static void imaAdpcmDecode(const uint8_t* adpcmData, uint32_t adpcmSize, int16_t* outPcm) { - int32_t predictor = 0; - int32_t stepIndex = 0; - imaAdpcmDecodeBlock(adpcmData, adpcmSize, outPcm, &predictor, &stepIndex); -} - -// ===[ SOUNDBNK.BIN Parser ]=== - -static void parseSoundBank(Ps2AudioSystem* ps2) { - char* path = PS2Utils_createDevicePath("SOUNDBNK.BIN"); - - FILE* f = fopen(path, "rb"); - free(path); - if (f == nullptr) { - fprintf(stderr, "PS2AudioSystem: Could not open SOUNDBNK.BIN\n"); - return; - } - - // Header: version(u8) + sondEntryCount(u16) + audoEntryCount(u16) + musEntryCount(u16) = 7 bytes - uint8_t version; - fread(&version, 1, 1, f); - fread(&ps2->sondEntryCount, 2, 1, f); - fread(&ps2->audoEntryCount, 2, 1, f); - fread(&ps2->musEntryCount, 2, 1, f); - - fprintf(stderr, "PS2AudioSystem: SOUNDBNK v%d, %d SOND entries, %d AUDO entries, %d MUS entries\n", version, ps2->sondEntryCount, ps2->audoEntryCount, ps2->musEntryCount); - - // Parse SOND entries (12 bytes each) - ps2->sondEntries = safeMalloc(ps2->sondEntryCount * sizeof(Ps2SondEntry)); - for (int i = 0; ps2->sondEntryCount > i; i++) { - uint8_t buf[12]; - fread(buf, 12, 1, f); - ps2->sondEntries[i].audoIndex = *(uint16_t*) &buf[0]; - ps2->sondEntries[i].flags = *(uint32_t*) &buf[2]; - ps2->sondEntries[i].volume = *(int16_t*) &buf[6]; - ps2->sondEntries[i].pitch = *(int16_t*) &buf[8]; - // buf[10..11] reserved - } - - // Parse AUDO entries (20 bytes each) - ps2->audoEntries = safeMalloc(ps2->audoEntryCount * sizeof(Ps2AudoEntry)); - for (int i = 0; ps2->audoEntryCount > i; i++) { - uint8_t buf[20]; - fread(buf, 20, 1, f); - ps2->audoEntries[i].dataOffset = *(uint32_t*) &buf[0]; - ps2->audoEntries[i].dataSize = *(uint32_t*) &buf[4]; - ps2->audoEntries[i].sampleRate = *(uint16_t*) &buf[8]; - ps2->audoEntries[i].channels = buf[10]; - ps2->audoEntries[i].bitsPerSample = buf[11]; - ps2->audoEntries[i].format = buf[12]; - // buf[13..15] reserved - ps2->audoEntries[i].sampleCount = *(uint32_t*) &buf[16]; - } - - // Parse MUS string table + MUS entries - ps2->musEntries = safeMalloc(ps2->musEntryCount * sizeof(Ps2MusEntry)); - - // First pass: read the string table (u8 nameLength + name bytes per entry) - repeat(ps2->musEntryCount, i) { - uint8_t nameLen; - fread(&nameLen, 1, 1, f); - char* name = safeMalloc(nameLen + 1); - fread(name, 1, nameLen, f); - name[nameLen] = '\0'; - ps2->musEntries[i].name = name; - } - - // Second pass: read the MUS entries (16 bytes each) - repeat(ps2->musEntryCount, i) { - uint8_t buf[16]; - fread(buf, 16, 1, f); - ps2->musEntries[i].dataOffset = *(uint32_t*) &buf[0]; - ps2->musEntries[i].dataSize = *(uint32_t*) &buf[4]; - ps2->musEntries[i].sampleRate = *(uint16_t*) &buf[8]; - ps2->musEntries[i].channels = buf[10]; - ps2->musEntries[i].format = buf[11]; - ps2->musEntries[i].sampleCount = *(uint32_t*) &buf[12]; - } - - if (ps2->musEntryCount > 0) { - fprintf(stderr, "PS2AudioSystem: Loaded %d MUS entries\n", ps2->musEntryCount); - } - - fclose(f); -} - -// ===[ SOUNDS.BIN File Handle ]=== - -static void openSoundsBin(Ps2AudioSystem* ps2) { - // SOUNDS.BIN is always alongside the ELF on the boot device, not in the FileSystem mappings - // We keep the file handle open and read on demand to avoid loading everything into EE RAM - char* path = PS2Utils_createDevicePath("SOUNDS.BIN"); - - ps2->soundsFile = fopen(path, "rb"); - if (ps2->soundsFile == nullptr) { - fprintf(stderr, "PS2AudioSystem: Could not open SOUNDS.BIN at %s\n", path); - free(path); - return; - } - - fprintf(stderr, "PS2AudioSystem: Opened SOUNDS.BIN for streaming (%s)\n", path); - free(path); -} - -// ===[ LRU Decoded PCM Cache (SFX) ]=== - -static DecodedPcmEntry* cacheGet(Ps2AudioSystem* ps2, int32_t audoIndex) { - for (int i = 0; LRU_CACHE_SIZE > i; i++) { - if (ps2->cacheEntries[i].audoIndex == audoIndex) { - ps2->cacheEntries[i].lastAccessCounter = ++ps2->cacheAccessCounter; - return &ps2->cacheEntries[i]; - } - } - return nullptr; -} - -static bool cacheIsInUse(Ps2AudioSystem* ps2, int32_t audoIndex) { - repeat(MAX_PS2_SOUND_INSTANCES, i) { - if (ps2->instances[i].active && ps2->instances[i].audoIndex == audoIndex) { - return true; - } - } - return false; -} - -static DecodedPcmEntry* cacheInsert(Ps2AudioSystem* ps2, int32_t audoIndex) { - Ps2AudoEntry* audo = &ps2->audoEntries[audoIndex]; - - // Compute decoded sample count: IMA ADPCM = 2 samples per byte - uint32_t sampleCount = audo->dataSize * 2; - uint32_t pcmBytes = sampleCount * sizeof(int16_t); - - // Find a free slot - DecodedPcmEntry* slot = nullptr; - for (int i = 0; LRU_CACHE_SIZE > i; i++) { - if (0 > ps2->cacheEntries[i].audoIndex) { - slot = &ps2->cacheEntries[i]; - break; - } - } - - // No free slot: evict LRU entry not in active use - if (slot == nullptr) { - uint32_t oldestAccess = UINT32_MAX; - for (int i = 0; LRU_CACHE_SIZE > i; i++) { - DecodedPcmEntry* entry = &ps2->cacheEntries[i]; - if (!cacheIsInUse(ps2, entry->audoIndex) && oldestAccess > entry->lastAccessCounter) { - oldestAccess = entry->lastAccessCounter; - slot = entry; - } - } - - if (slot == nullptr) { - // fprintf(stderr, "PS2AudioSystem: Cache full, all entries in use! Cannot decode audoIndex %" PRId32 "\n", audoIndex); - return nullptr; - } - - // Free the evicted entry's PCM data - free(slot->pcmData); - } - - // Read compressed ADPCM data from SOUNDS.BIN on demand - uint8_t* adpcmBuf = safeMalloc(audo->dataSize); - fseek(ps2->soundsFile, (long) audo->dataOffset, SEEK_SET); - fread(adpcmBuf, 1, audo->dataSize, ps2->soundsFile); - - // Decode IMA ADPCM into new PCM buffer - slot->pcmData = safeMalloc(pcmBytes); - imaAdpcmDecode(adpcmBuf, audo->dataSize, slot->pcmData); - free(adpcmBuf); - - slot->audoIndex = audoIndex; - slot->pcmSampleCount = sampleCount; - slot->pcmDataBytes = pcmBytes; - slot->lastAccessCounter = ++ps2->cacheAccessCounter; - - return slot; -} - -// ===[ Streaming Music ]=== - -// Fill one buffer of a music stream by reading ADPCM from SOUNDS.BIN and decoding -static void streamFillBuffer(Ps2AudioSystem* ps2, Ps2MusicStream* stream, int bufferIndex) { - if (stream->fileOffset >= stream->fileEndOffset) { - // No more data to read - stream->bufferSampleCount[bufferIndex] = 0; - return; - } - - // How many ADPCM bytes remain for this track? - uint32_t remaining = stream->fileEndOffset - stream->fileOffset; - uint32_t toRead = STREAM_ADPCM_CHUNK_BYTES; - if (toRead > remaining) toRead = remaining; - - // Read ADPCM chunk from disc - uint8_t adpcmBuf[STREAM_ADPCM_CHUNK_BYTES]; - fseek(ps2->soundsFile, (long) stream->fileOffset, SEEK_SET); - size_t bytesRead = fread(adpcmBuf, 1, toRead, ps2->soundsFile); - stream->fileOffset += (uint32_t) bytesRead; - - // Decode into the target buffer, continuing decoder state - uint32_t samples = imaAdpcmDecodeBlock(adpcmBuf, (uint32_t) bytesRead, stream->buffers[bufferIndex], &stream->decoderPredictor, &stream->decoderStepIndex); - stream->bufferSampleCount[bufferIndex] = samples; - - if (stream->fileOffset >= stream->fileEndOffset) { - stream->endOfTrack = true; - } -} - -// Reset a stream to the beginning of its track (for looping) -static void streamResetToStart(Ps2AudioSystem* ps2, Ps2MusicStream* stream) { - stream->fileOffset = stream->fileStartOffset; - stream->decoderPredictor = 0; - stream->decoderStepIndex = 0; - stream->endOfTrack = false; - stream->readPosition = 0; - stream->readPositionFrac = 0; - - // Fill both buffers from the start - streamFillBuffer(ps2, stream, 0); - streamFillBuffer(ps2, stream, 1); - stream->activeBuffer = 0; - stream->needsRefill = false; -} - -// ===[ SFX Instance Helpers ]=== - -static Ps2SoundInstance* findFreeSlot(Ps2AudioSystem* ps2) { - // First pass: find an inactive slot - repeat(MAX_PS2_SOUND_INSTANCES, i) { - if (!ps2->instances[i].active) { - return &ps2->instances[i]; - } - } - - // Second pass: evict the lowest-priority ended sound - Ps2SoundInstance* best = nullptr; - repeat(MAX_PS2_SOUND_INSTANCES, i) { - Ps2SoundInstance* inst = &ps2->instances[i]; - if (inst->positionInt >= inst->totalSamples && !inst->loop) { - if (best == nullptr || best->priority > inst->priority) { - best = inst; - } - } - } - - if (best != nullptr) { - best->active = false; - } - - return best; -} - -static Ps2SoundInstance* findSfxInstanceById(Ps2AudioSystem* ps2, int32_t instanceId) { - int32_t slotIndex = instanceId - PS2_SOUND_INSTANCE_ID_BASE; - if (0 > slotIndex || slotIndex >= MAX_PS2_SOUND_INSTANCES) return nullptr; - Ps2SoundInstance* inst = &ps2->instances[slotIndex]; - if (!inst->active || inst->instanceId != instanceId) return nullptr; - return inst; -} - -// Find a music stream by instance ID -static Ps2MusicStream* findMusicStreamById(Ps2AudioSystem* ps2, int32_t instanceId) { - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active && ps2->musicStreams[i].instanceId == instanceId) { - return &ps2->musicStreams[i]; - } - } - return nullptr; -} - -// Get the sample rate for a music stream (from AUDO or MUS entry) -static uint16_t getMusicStreamSampleRate(Ps2AudioSystem* ps2, Ps2MusicStream* stream) { - if (stream->soundIndex >= PS2_AUDIO_STREAM_INDEX_BASE) { - int32_t musIndex = stream->soundIndex - PS2_AUDIO_STREAM_INDEX_BASE; - if (ps2->musEntryCount > musIndex) return ps2->musEntries[musIndex].sampleRate; - return AUDSRV_OUTPUT_FREQ; - } - if ((uint16_t) stream->audoIndex < ps2->audoEntryCount) return ps2->audoEntries[stream->audoIndex].sampleRate; - return AUDSRV_OUTPUT_FREQ; -} - -// ===[ Software Mixer ]=== - -static void mixAudio(Ps2AudioSystem* ps2, int16_t* outBuf, int32_t samplePairs) { - int32_t* accum = ps2->mixAccum; - memset(accum, 0, samplePairs * sizeof(int32_t)); - - // ===[ Mix SFX instances (from LRU cache) ]=== - repeat(MAX_PS2_SOUND_INSTANCES, i) { - Ps2SoundInstance* inst = &ps2->instances[i]; - if (!inst->active || inst->paused) continue; - - DecodedPcmEntry* cache = cacheGet(ps2, inst->audoIndex); - if (cache == nullptr) continue; - - if (inst->positionInt >= inst->totalSamples) { - if (inst->loop) { - inst->positionInt = 0; - inst->positionFrac = 0; - } else { - inst->active = false; - continue; - } - } - - // Hoist per-instance constants out of the per-sample loop - const int16_t* pcm = cache->pcmData; - uint32_t totalSamples = inst->totalSamples; - bool loop = inst->loop; - float gain = inst->currentGain * inst->sondVolume * ps2->masterGain; - int32_t gainQ15 = (int32_t) (gain * 32768.0f); - Ps2AudoEntry* audo = &ps2->audoEntries[inst->audoIndex]; - float stepRate = inst->pitch * inst->sondPitch * ((float) audo->sampleRate / (float) AUDSRV_OUTPUT_FREQ); - uint32_t stepInt = (uint32_t) stepRate; - uint32_t stepFrac = (uint32_t) ((stepRate - (float) stepInt) * 4294967296.0f); - bool nativeRate = (stepInt == 1 && stepFrac == 0); - - uint32_t posInt = inst->positionInt; - uint32_t posFrac = inst->positionFrac; - bool ended = false; - - if (nativeRate) { - // Fast path: no resampling, no fractional position (most SFX at 22050 Hz) - for (int32_t s = 0; samplePairs > s; s++) { - int32_t sample = pcm[posInt]; - accum[s] += (sample * gainQ15) >> 15; - posInt++; - - if (posInt >= totalSamples) { - if (loop) { - posInt = 0; - } else { - ended = true; - break; - } - } - } - } else { - // Resampling path: linear interpolation with 32.32 fixed-point position - for (int32_t s = 0; samplePairs > s; s++) { - int32_t idx0 = (int32_t) posInt; - int32_t idx1 = idx0 + 1; - if ((uint32_t) idx1 >= totalSamples) idx1 = idx0; - - int32_t s0 = pcm[idx0]; - int32_t s1 = pcm[idx1]; - int32_t frac = (int32_t) (posFrac >> 16); - int32_t sample = s0 + ((s1 - s0) * frac >> 16); - - accum[s] += (sample * gainQ15) >> 15; - - uint32_t oldFrac = posFrac; - posFrac += stepFrac; - if (oldFrac > posFrac) posInt++; - posInt += stepInt; - - if (posInt >= totalSamples) { - if (loop) { - posInt = posInt % totalSamples; - posFrac = 0; - } else { - ended = true; - break; - } - } - } - } - - inst->positionInt = posInt; - inst->positionFrac = posFrac; - if (ended) inst->active = false; - } - - // ===[ Mix streaming music instances ]=== - repeat(MAX_MUSIC_STREAMS, i) { - Ps2MusicStream* stream = &ps2->musicStreams[i]; - if (!stream->active || stream->paused) continue; - - // Hoist per-stream constants (pitch/sampleRate don't change mid-mix) - float gain = stream->currentGain * stream->sondVolume * ps2->masterGain; - int32_t gainQ15 = (int32_t) (gain * 32768.0f); - uint16_t streamSampleRate = getMusicStreamSampleRate(ps2, stream); - float stepRate = stream->pitch * stream->sondPitch * ((float) streamSampleRate / (float) AUDSRV_OUTPUT_FREQ); - uint32_t stepInt = (uint32_t) stepRate; - uint32_t stepFrac = (uint32_t) ((stepRate - (float) stepInt) * 4294967296.0f); - bool nativeRate = (stepInt == 1 && stepFrac == 0); - - for (int32_t s = 0; samplePairs > s; s++) { - uint32_t bufSamples = stream->bufferSampleCount[stream->activeBuffer]; - - // Check if we've exhausted the active buffer - if (stream->readPosition >= bufSamples) { - if (stream->needsRefill && stream->endOfTrack) { - if (stream->loop) { - streamResetToStart(ps2, stream); - bufSamples = stream->bufferSampleCount[stream->activeBuffer]; - } else { - stream->active = false; - break; - } - } else { - // Swap to the back buffer (which should have been refilled) - stream->activeBuffer ^= 1; - stream->readPosition = 0; - stream->readPositionFrac = 0; - stream->needsRefill = true; - bufSamples = stream->bufferSampleCount[stream->activeBuffer]; - - if (bufSamples == 0) { - if (stream->loop) { - streamResetToStart(ps2, stream); - bufSamples = stream->bufferSampleCount[stream->activeBuffer]; - } else { - stream->active = false; - break; - } - } - } - } - - int16_t* buf = stream->buffers[stream->activeBuffer]; - int32_t sample; - - if (nativeRate) { - sample = buf[stream->readPosition]; - } else { - int32_t idx0 = (int32_t) stream->readPosition; - int32_t idx1 = idx0 + 1; - if ((uint32_t) idx1 >= bufSamples) idx1 = idx0; - - int32_t s0 = buf[idx0]; - int32_t s1 = buf[idx1]; - int32_t frac = (int32_t) (stream->readPositionFrac >> 16); - sample = s0 + ((s1 - s0) * frac >> 16); - } - - accum[s] += (sample * gainQ15) >> 15; - - uint32_t oldFrac = stream->readPositionFrac; - stream->readPositionFrac += stepFrac; - if (oldFrac > stream->readPositionFrac) stream->readPosition++; - stream->readPosition += stepInt; - } - } - - // ===[ Clamp mono accumulator and duplicate into interleaved stereo ]=== - for (int32_t s = 0; samplePairs > s; s++) { - int32_t v = accum[s]; - if (v > 32767) v = 32767; - if (-32768 > v) v = -32768; - int16_t v16 = (int16_t) v; - outBuf[s * 2] = v16; - outBuf[s * 2 + 1] = v16; - } -} - -// ===[ Vtable Implementations ]=== - -static void ps2Init(AudioSystem* audio, MAYBE_UNUSED DataWin* dataWin, MAYBE_UNUSED FileSystem* fileSystem) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - - // Parse sound bank index - parseSoundBank(ps2); - if (ps2->sondEntries == nullptr || ps2->audoEntries == nullptr) { - fprintf(stderr, "PS2AudioSystem: Failed to parse SOUNDBNK.BIN, audio disabled\n"); - return; - } - - // Open SOUNDS.BIN for streaming (kept open for on-demand reads) - openSoundsBin(ps2); - if (ps2->soundsFile == nullptr) { - fprintf(stderr, "PS2AudioSystem: Failed to open SOUNDS.BIN, audio disabled\n"); - return; - } - - // Initialize LRU cache (all slots empty) - for (int i = 0; LRU_CACHE_SIZE > i; i++) { - ps2->cacheEntries[i].audoIndex = -1; - ps2->cacheEntries[i].pcmData = nullptr; - } - - // Initialize sound instances - memset(ps2->instances, 0, sizeof(ps2->instances)); - memset(ps2->musicStreams, 0, sizeof(ps2->musicStreams)); - ps2->nextInstanceCounter = 0; - ps2->masterGain = 1.0f; - - // Initialize audsrv - int ret = audsrv_init(); - if (ret != 0) { - fprintf(stderr, "PS2AudioSystem: audsrv_init failed (%d)\n", ret); - return; - } - - struct audsrv_fmt_t format; - format.freq = AUDSRV_OUTPUT_FREQ; - format.bits = 16; - format.channels = 2; - - ret = audsrv_set_format(&format); - if (ret != 0) { - fprintf(stderr, "PS2AudioSystem: audsrv_set_format failed (%d)\n", ret); - audsrv_quit(); - return; - } - - audsrv_set_volume(MAX_VOLUME); - - ps2->initialized = true; - fprintf(stderr, "PS2AudioSystem: Initialized (output: %d Hz, 16-bit, stereo)\n", AUDSRV_OUTPUT_FREQ); -} - -static void ps2Destroy(AudioSystem* audio) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - - if (ps2->initialized) { - audsrv_stop_audio(); - audsrv_quit(); - } - - // Close SOUNDS.BIN file handle - if (ps2->soundsFile != nullptr) { - fclose(ps2->soundsFile); - } - - // Free all cached PCM data - for (int i = 0; LRU_CACHE_SIZE > i; i++) { - free(ps2->cacheEntries[i].pcmData); - } - - // Free sound bank entries - free(ps2->sondEntries); - free(ps2->audoEntries); - - // Free MUS entry names - repeat(ps2->musEntryCount, i) { - free(ps2->musEntries[i].name); - } - free(ps2->musEntries); - - free(ps2); -} - -static void ps2Update(AudioSystem* audio, float deltaTime) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - if (!ps2->initialized) return; - - // Cap deltaTime to prevent large fades on lag spikes - if (deltaTime > 0.1f) deltaTime = 0.1f; - - // Update gain fading on SFX instances - repeat(MAX_PS2_SOUND_INSTANCES, i) { - Ps2SoundInstance* inst = &ps2->instances[i]; - if (!inst->active) continue; - - if (inst->fadeTimeRemaining > 0.0f) { - inst->fadeTimeRemaining -= deltaTime; - if (0.0f >= inst->fadeTimeRemaining) { - inst->fadeTimeRemaining = 0.0f; - inst->currentGain = inst->targetGain; - } else { - float t = 1.0f - (inst->fadeTimeRemaining / inst->fadeTotalTime); - inst->currentGain = inst->startGain + (inst->targetGain - inst->startGain) * t; - } - } - } - - // Update gain fading on music streams - repeat(MAX_MUSIC_STREAMS, i) { - Ps2MusicStream* stream = &ps2->musicStreams[i]; - if (!stream->active) continue; - - if (stream->fadeTimeRemaining > 0.0f) { - stream->fadeTimeRemaining -= deltaTime; - if (0.0f >= stream->fadeTimeRemaining) { - stream->fadeTimeRemaining = 0.0f; - stream->currentGain = stream->targetGain; - } else { - float t = 1.0f - (stream->fadeTimeRemaining / stream->fadeTotalTime); - stream->currentGain = stream->startGain + (stream->targetGain - stream->startGain) * t; - } - } - } - - // Refill music stream back buffers (do disc I/O here, outside the mixer loop) - repeat(MAX_MUSIC_STREAMS, i) { - Ps2MusicStream* stream = &ps2->musicStreams[i]; - if (!stream->active || !stream->needsRefill) continue; - // fprintf(stderr, "PS2AudioSystem: Filling music stream %d back buffers...\n", stream->soundIndex); - - int backBuffer = stream->activeBuffer ^ 1; - streamFillBuffer(ps2, stream, backBuffer); - stream->needsRefill = false; - } - - // Fill audsrv ring buffer - int32_t chunkBytes = MIX_BUFFER_SAMPLES * 2 * (int32_t) sizeof(int16_t); - while (audsrv_available() >= chunkBytes) { - // fprintf(stderr, "PS2AudioSystem: Filling audsrv ring buffer... audsrv_available: %d, chunkBytes: %d\n", audsrv_available(), chunkBytes); - mixAudio(ps2, ps2->mixBuffer, MIX_BUFFER_SAMPLES); - audsrv_play_audio((char*) ps2->mixBuffer, chunkBytes); - } - - // fprintf(stderr, "PS2AudioSystem: Finished ticking the audio system\n"); -} - -static int32_t ps2PlaySound(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop) { - // fprintf(stderr, "PS2AudioSystem: Attempting to play sound index %d with priority %d, should loop? %d\n", soundIndex, priority, loop); - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - if (!ps2->initialized) return -1; - - // Check if this is a MUS stream index (created by audio_create_stream) - if (soundIndex >= PS2_AUDIO_STREAM_INDEX_BASE) { - int32_t musIndex = soundIndex - PS2_AUDIO_STREAM_INDEX_BASE; - if (musIndex >= ps2->musEntryCount) return -1; - - Ps2MusEntry* mus = &ps2->musEntries[musIndex]; - - // Find a free music stream slot - Ps2MusicStream* stream = nullptr; - int streamSlot = -1; - repeat(MAX_MUSIC_STREAMS, i) { - if (!ps2->musicStreams[i].active) { - stream = &ps2->musicStreams[i]; - streamSlot = i; - break; - } - } - - if (stream == nullptr) { - return -1; - } - - int32_t instanceId = PS2_SOUND_INSTANCE_ID_BASE + MAX_PS2_SOUND_INSTANCES + streamSlot; - - memset(stream, 0, sizeof(Ps2MusicStream)); - stream->active = true; - stream->soundIndex = soundIndex; - stream->audoIndex = -1; - stream->instanceId = instanceId; - stream->priority = priority; - stream->loop = loop; - stream->paused = false; - stream->currentGain = 1.0f; - stream->targetGain = 1.0f; - stream->startGain = 1.0f; - stream->sondVolume = 1.0f; - stream->pitch = 1.0f; - stream->sondPitch = 1.0f; - - stream->fileStartOffset = mus->dataOffset; - stream->fileEndOffset = mus->dataOffset + mus->dataSize; - stream->fileOffset = mus->dataOffset; - stream->decoderPredictor = 0; - stream->decoderStepIndex = 0; - stream->endOfTrack = false; - - streamFillBuffer(ps2, stream, 0); - streamFillBuffer(ps2, stream, 1); - stream->activeBuffer = 0; - stream->readPosition = 0; - stream->needsRefill = false; - - // fprintf(stderr, "PS2AudioSystem: Streaming MUS '%s', size=%" PRIu32 " bytes, instanceId=%" PRId32 "\n", mus->name, mus->dataSize, instanceId); - - return instanceId; - } - - if (0 > soundIndex || (uint16_t) soundIndex >= ps2->sondEntryCount) { - // fprintf(stderr, "PS2AudioSystem: Invalid sound index %" PRId32 "\n", soundIndex); - return -1; - } - - Ps2SondEntry* sond = &ps2->sondEntries[soundIndex]; - - // 0xFFFF = unmapped sound (no audio data) - if (sond->audoIndex == 0xFFFF) { - return -1; - } - - if (sond->audoIndex >= ps2->audoEntryCount) { - // fprintf(stderr, "PS2AudioSystem: Invalid audo index %d for sound %" PRId32 "\n", sond->audoIndex, soundIndex); - return -1; - } - - // SOND volume and pitch are fixed-point * 256 - // A pitch of 0 means "default" (1.0) in GameMaker - float sondVolume = (float) sond->volume / 256.0f; - float sondPitch = (sond->pitch == 0) ? 1.0f : (float) sond->pitch / 256.0f; - - bool isEmbedded = (sond->flags & 0x01) != 0; - bool isCompressed = (sond->flags & 0x02) != 0; - - // Some sounds are mis-flagged as embedded when they are actually long tracks (example: "spamton_neo_mix_ex_wip" in DELTARUNE Chapter 2) - // Decoding them into the LRU cache would blow EE RAM, so force them through the streaming path when the decoded PCM would exceed the cache budget. - Ps2AudoEntry* audoForSize = &ps2->audoEntries[sond->audoIndex]; - uint32_t decodedPcmBytes = audoForSize->dataSize * 2 * (uint32_t) sizeof(int16_t); - if ((isEmbedded || isCompressed) && decodedPcmBytes > PS2_SFX_CACHE_MAX_BYTES) { - fprintf(stderr, "PS2AudioSystem: Sound %" PRId32 " (audo %d) would need %" PRIu32 " bytes of PCM in the cache! isEmbedded? %s; isCompressed? %s; Streaming instead...\n", soundIndex, sond->audoIndex, decodedPcmBytes, isEmbedded ? "true" : "false", isCompressed ? "true" : "false"); - isEmbedded = false; - isCompressed = false; - } - - if (!isEmbedded && !isCompressed) { - // ===[ Streaming music path ]=== - // Find a free music stream slot - Ps2MusicStream* stream = nullptr; - int streamSlot = -1; - repeat(MAX_MUSIC_STREAMS, i) { - if (!ps2->musicStreams[i].active) { - stream = &ps2->musicStreams[i]; - streamSlot = i; - break; - } - } - - if (stream == nullptr) { - // fprintf(stderr, "PS2AudioSystem: No free music stream slots for sound %" PRId32 "\n", soundIndex); - return -1; - } - - Ps2AudoEntry* audo = &ps2->audoEntries[sond->audoIndex]; - - // Use a separate ID range for music streams (offset by MAX_PS2_SOUND_INSTANCES) - int32_t instanceId = PS2_SOUND_INSTANCE_ID_BASE + MAX_PS2_SOUND_INSTANCES + streamSlot; - - memset(stream, 0, sizeof(Ps2MusicStream)); - stream->active = true; - stream->soundIndex = soundIndex; - stream->audoIndex = sond->audoIndex; - stream->instanceId = instanceId; - stream->priority = priority; - stream->loop = loop; - stream->paused = false; - stream->currentGain = sondVolume; - stream->targetGain = sondVolume; - stream->startGain = sondVolume; - stream->sondVolume = sondVolume; - stream->pitch = 1.0f; - stream->sondPitch = sondPitch; - - // Set up file streaming state - stream->fileStartOffset = audo->dataOffset; - stream->fileEndOffset = audo->dataOffset + audo->dataSize; - stream->fileOffset = audo->dataOffset; - stream->decoderPredictor = 0; - stream->decoderStepIndex = 0; - stream->endOfTrack = false; - - // Fill both buffers initially - streamFillBuffer(ps2, stream, 0); - streamFillBuffer(ps2, stream, 1); - stream->activeBuffer = 0; - stream->readPosition = 0; - stream->needsRefill = false; - - // fprintf(stderr, "PS2AudioSystem: Streaming music soundIndex=%" PRId32 " audoIndex=%d, size=%" PRIu32 " bytes, instanceId=%" PRId32 "\n", soundIndex, sond->audoIndex, audo->dataSize, instanceId); - - return instanceId; - } - - // ===[ Cached SFX path ]=== - // Ensure decoded PCM is in cache - DecodedPcmEntry* cached = cacheGet(ps2, sond->audoIndex); - if (cached == nullptr) { - cached = cacheInsert(ps2, sond->audoIndex); - if (cached == nullptr) { - // fprintf(stderr, "PS2AudioSystem: Failed to cache decoded audio for sound %" PRId32 "\n", soundIndex); - return -1; - } - } - - // Find a free SFX instance slot - Ps2SoundInstance* slot = findFreeSlot(ps2); - if (slot == nullptr) { - // fprintf(stderr, "PS2AudioSystem: No free sound slots for sound %" PRId32 "\n", soundIndex); - return -1; - } - - int32_t slotIndex = (int32_t) (slot - ps2->instances); - - slot->active = true; - slot->soundIndex = soundIndex; - slot->audoIndex = sond->audoIndex; - slot->instanceId = PS2_SOUND_INSTANCE_ID_BASE + slotIndex; - slot->priority = priority; - slot->loop = loop; - slot->paused = false; - slot->positionInt = 0; - slot->positionFrac = 0; - slot->totalSamples = cached->pcmSampleCount; - slot->pitch = 1.0f; - slot->sondPitch = sondPitch; - slot->currentGain = sondVolume; - slot->targetGain = sondVolume; - slot->startGain = sondVolume; - slot->fadeTimeRemaining = 0.0f; - slot->fadeTotalTime = 0.0f; - slot->sondVolume = sondVolume; - - ps2->nextInstanceCounter++; - - return slot->instanceId; -} - -// ===[ Helper: Apply action to SFX instance, music stream, or all matching by soundIndex ]=== -// These helpers handle the dual SFX/music lookup needed by stop/pause/resume/gain/pitch/etc. - -// Find either a SFX instance or music stream by instanceId or soundIndex -// For instanceId lookups (>= PS2_SOUND_INSTANCE_ID_BASE), returns at most one match. -// For soundIndex lookups, iterates all matches via callback. - -typedef void (*InstanceAction)(Ps2SoundInstance* sfx, Ps2MusicStream* music, void* userData); - -static void forEachInstance(Ps2AudioSystem* ps2, int32_t soundOrInstance, InstanceAction action, void* userData) { - if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) { - // MUS stream resource index: match by soundIndex on music streams - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) { - action(nullptr, &ps2->musicStreams[i], userData); - } - } - } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) { - // Lookup by instance ID - Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance); - if (sfx != nullptr) { - action(sfx, nullptr, userData); - return; - } - Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance); - if (music != nullptr) { - action(nullptr, music, userData); - return; - } - } else { - // Lookup by sound index -- apply to all matching - repeat(MAX_PS2_SOUND_INSTANCES, i) { - if (ps2->instances[i].active && ps2->instances[i].soundIndex == soundOrInstance) { - action(&ps2->instances[i], nullptr, userData); - } - } - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) { - action(nullptr, &ps2->musicStreams[i], userData); - } - } - } -} - -static void actionStop(Ps2SoundInstance* sfx, Ps2MusicStream* music, MAYBE_UNUSED void* userData) { - if (sfx != nullptr) sfx->active = false; - if (music != nullptr) music->active = false; -} - -static void actionPause(Ps2SoundInstance* sfx, Ps2MusicStream* music, MAYBE_UNUSED void* userData) { - if (sfx != nullptr) sfx->paused = true; - if (music != nullptr) music->paused = true; -} - -static void actionResume(Ps2SoundInstance* sfx, Ps2MusicStream* music, MAYBE_UNUSED void* userData) { - if (sfx != nullptr) sfx->paused = false; - if (music != nullptr) music->paused = false; -} - -typedef struct { - float gain; - uint32_t timeMs; -} GainParams; - -static void actionSetGain(Ps2SoundInstance* sfx, Ps2MusicStream* music, void* userData) { - GainParams* params = (GainParams*) userData; - float gain = params->gain; - uint32_t timeMs = params->timeMs; - - if (sfx != nullptr) { - if (timeMs == 0) { - sfx->currentGain = gain; - sfx->targetGain = gain; - sfx->fadeTimeRemaining = 0.0f; - } else { - sfx->startGain = sfx->currentGain; - sfx->targetGain = gain; - sfx->fadeTotalTime = (float) timeMs / 1000.0f; - sfx->fadeTimeRemaining = sfx->fadeTotalTime; - } - } - if (music != nullptr) { - if (timeMs == 0) { - music->currentGain = gain; - music->targetGain = gain; - music->fadeTimeRemaining = 0.0f; - } else { - music->startGain = music->currentGain; - music->targetGain = gain; - music->fadeTotalTime = (float) timeMs / 1000.0f; - music->fadeTimeRemaining = music->fadeTotalTime; - } - } -} - -static void actionSetPitch(Ps2SoundInstance* sfx, Ps2MusicStream* music, void* userData) { - float pitch = *(float*) userData; - if (sfx != nullptr) sfx->pitch = pitch; - if (music != nullptr) music->pitch = pitch; -} - -// ===[ Vtable: Stop/Pause/Resume/Gain/Pitch ]=== - -static void ps2StopSound(AudioSystem* audio, int32_t soundOrInstance) { - // fprintf(stderr, "PS2AudioSystem: Stopping sound %d\n", soundOrInstance); - forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionStop, nullptr); -} - -static void ps2StopAll(AudioSystem* audio) { - // fprintf(stderr, "PS2AudioSystem: Stopping all audios!\n"); - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - repeat(MAX_PS2_SOUND_INSTANCES, i) { - ps2->instances[i].active = false; - } - repeat(MAX_MUSIC_STREAMS, i) { - ps2->musicStreams[i].active = false; - } -} - -static bool ps2IsPlaying(AudioSystem* audio, int32_t soundOrInstance) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - - if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) { - // MUS stream resource index: match by soundIndex on music streams - repeat(MAX_MUSIC_STREAMS, i) { - Ps2MusicStream* stream = &ps2->musicStreams[i]; - if (stream->active && stream->soundIndex == soundOrInstance && !stream->paused) return true; - } - return false; - } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) { - Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance); - if (sfx != nullptr) return !sfx->paused; - Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance); - if (music != nullptr) return !music->paused; - return false; - } else { - repeat(MAX_PS2_SOUND_INSTANCES, i) { - Ps2SoundInstance* inst = &ps2->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance && !inst->paused) return true; - } - repeat(MAX_MUSIC_STREAMS, i) { - Ps2MusicStream* stream = &ps2->musicStreams[i]; - if (stream->active && stream->soundIndex == soundOrInstance && !stream->paused) return true; - } - return false; - } -} - -static void ps2PauseSound(AudioSystem* audio, int32_t soundOrInstance) { - // fprintf(stderr, "PS2AudioSystem: Pausing sound %d\n", soundOrInstance); - forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionPause, nullptr); -} - -static void ps2ResumeSound(AudioSystem* audio, int32_t soundOrInstance) { - // fprintf(stderr, "PS2AudioSystem: Resuming sound %d\n", soundOrInstance); - forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionResume, nullptr); -} - -static void ps2PauseAll(AudioSystem* audio) { - // fprintf(stderr, "PS2AudioSystem: Pausing all sounds!\n"); - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - repeat(MAX_PS2_SOUND_INSTANCES, i) { - if (ps2->instances[i].active) ps2->instances[i].paused = true; - } - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active) ps2->musicStreams[i].paused = true; - } -} - -static void ps2ResumeAll(AudioSystem* audio) { - // fprintf(stderr, "PS2AudioSystem: Resuming all sounds!\n"); - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - repeat(MAX_PS2_SOUND_INSTANCES, i) { - if (ps2->instances[i].active) ps2->instances[i].paused = false; - } - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active) ps2->musicStreams[i].paused = false; - } -} - -static void ps2SetSoundGain(AudioSystem* audio, int32_t soundOrInstance, float gain, uint32_t timeMs) { - GainParams params = { .gain = gain, .timeMs = timeMs }; - forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionSetGain, ¶ms); -} - -static float ps2GetSoundGain(AudioSystem* audio, int32_t soundOrInstance) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) { - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) return ps2->musicStreams[i].currentGain; - } - } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) { - Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance); - if (sfx != nullptr) return sfx->currentGain; - Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance); - if (music != nullptr) return music->currentGain; - } else { - repeat(MAX_PS2_SOUND_INSTANCES, i) { - if (ps2->instances[i].active && ps2->instances[i].soundIndex == soundOrInstance) return ps2->instances[i].currentGain; - } - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) return ps2->musicStreams[i].currentGain; - } - } - return 0.0f; -} - -static void ps2SetSoundPitch(AudioSystem* audio, int32_t soundOrInstance, float pitch) { - // fprintf(stderr, "PS2AudioSystem: Setting pitch of sound %d to %f\n", soundOrInstance, pitch); - forEachInstance((Ps2AudioSystem*) audio, soundOrInstance, actionSetPitch, &pitch); -} - -static float ps2GetSoundPitch(AudioSystem* audio, int32_t soundOrInstance) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) { - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) return ps2->musicStreams[i].pitch; - } - } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) { - Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance); - if (sfx != nullptr) return sfx->pitch; - Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance); - if (music != nullptr) return music->pitch; - } else { - repeat(MAX_PS2_SOUND_INSTANCES, i) { - if (ps2->instances[i].active && ps2->instances[i].soundIndex == soundOrInstance) return ps2->instances[i].pitch; - } - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == soundOrInstance) return ps2->musicStreams[i].pitch; - } - } - return 1.0f; -} - -static float ps2GetTrackPosition(AudioSystem* audio, int32_t soundOrInstance) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - - if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) { - // MUS stream resource index - repeat(MAX_MUSIC_STREAMS, i) { - Ps2MusicStream* stream = &ps2->musicStreams[i]; - if (stream->active && stream->soundIndex == soundOrInstance) { - uint32_t bytesConsumed = stream->fileOffset - stream->fileStartOffset; - uint32_t samplesConsumed = bytesConsumed * 2; - return (float) samplesConsumed / (float) getMusicStreamSampleRate(ps2, stream); - } - } - } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) { - Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance); - if (sfx != nullptr && sfx->audoIndex < ps2->audoEntryCount) { - return (float) sfx->positionInt / (float) ps2->audoEntries[sfx->audoIndex].sampleRate; - } - Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance); - if (music != nullptr) { - uint32_t bytesConsumed = music->fileOffset - music->fileStartOffset; - uint32_t samplesConsumed = bytesConsumed * 2; - return (float) samplesConsumed / (float) getMusicStreamSampleRate(ps2, music); - } - } else { - repeat(MAX_PS2_SOUND_INSTANCES, i) { - Ps2SoundInstance* inst = &ps2->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance && inst->audoIndex < ps2->audoEntryCount) { - return (float) inst->positionInt / (float) ps2->audoEntries[inst->audoIndex].sampleRate; - } - } - repeat(MAX_MUSIC_STREAMS, i) { - Ps2MusicStream* stream = &ps2->musicStreams[i]; - if (stream->active && stream->soundIndex == soundOrInstance) { - uint32_t bytesConsumed = stream->fileOffset - stream->fileStartOffset; - uint32_t samplesConsumed = bytesConsumed * 2; - return (float) samplesConsumed / (float) getMusicStreamSampleRate(ps2, stream); - } - } - } - return 0.0f; -} - -// Seek a music stream to a position in seconds -static void seekMusicStream(Ps2AudioSystem* ps2, Ps2MusicStream* music, float positionSeconds) { - float sampleRate = (float) getMusicStreamSampleRate(ps2, music); - uint32_t targetSample = (uint32_t) (positionSeconds * sampleRate); - // Convert sample position to ADPCM byte offset (2 samples per byte) - uint32_t byteOffset = targetSample / 2; - uint32_t maxBytes = music->fileEndOffset - music->fileStartOffset; - if (byteOffset > maxBytes) byteOffset = maxBytes; - - // Reset decoder state and seek - music->fileOffset = music->fileStartOffset + byteOffset; - music->decoderPredictor = 0; - music->decoderStepIndex = 0; - music->endOfTrack = false; - music->readPosition = 0; - - // Re-fill both buffers from the new position - streamFillBuffer(ps2, music, 0); - streamFillBuffer(ps2, music, 1); - music->activeBuffer = 0; - music->needsRefill = false; -} - -static void ps2SetTrackPosition(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds) { - // fprintf(stderr, "PS2AudioSystem: Setting track position of sound %d to %f\n", soundOrInstance, positionSeconds); - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - - if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) { - // MUS stream resource index - repeat(MAX_MUSIC_STREAMS, i) { - Ps2MusicStream* stream = &ps2->musicStreams[i]; - if (stream->active && stream->soundIndex == soundOrInstance) { - seekMusicStream(ps2, stream, positionSeconds); - return; - } - } - } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) { - // SFX track position - Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance); - if (sfx != nullptr && sfx->audoIndex < ps2->audoEntryCount) { - float sampleRate = (float) ps2->audoEntries[sfx->audoIndex].sampleRate; - sfx->positionInt = (uint32_t) (positionSeconds * sampleRate); - sfx->positionFrac = 0; - if (sfx->positionInt >= sfx->totalSamples) { - sfx->positionInt = sfx->totalSamples > 0 ? sfx->totalSamples - 1 : 0; - } - return; - } - // Music stream seek: reset decoder and re-seek - Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance); - if (music != nullptr) { - seekMusicStream(ps2, music, positionSeconds); - } - } else { - // By sound index (apply to first match) - repeat(MAX_PS2_SOUND_INSTANCES, i) { - Ps2SoundInstance* inst = &ps2->instances[i]; - if (inst->active && inst->soundIndex == soundOrInstance && inst->audoIndex < ps2->audoEntryCount) { - float sampleRate = (float) ps2->audoEntries[inst->audoIndex].sampleRate; - inst->positionInt = (uint32_t) (positionSeconds * sampleRate); - inst->positionFrac = 0; - if (inst->positionInt >= inst->totalSamples) { - inst->positionInt = inst->totalSamples > 0 ? inst->totalSamples - 1 : 0; - } - return; - } - } - } -} - -// Total length of a sound in seconds. Looks up the AUDO or MUS entry's decoded sampleCount and divides by sampleRate. -static float ps2GetSoundLength(AudioSystem* audio, int32_t soundOrInstance) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - - int32_t audoIndex = -1; - int32_t musIndex = -1; - - if (soundOrInstance >= PS2_AUDIO_STREAM_INDEX_BASE) { - musIndex = soundOrInstance - PS2_AUDIO_STREAM_INDEX_BASE; - } else if (soundOrInstance >= PS2_SOUND_INSTANCE_ID_BASE) { - Ps2SoundInstance* sfx = findSfxInstanceById(ps2, soundOrInstance); - if (sfx != nullptr) { - audoIndex = sfx->audoIndex; - } else { - Ps2MusicStream* music = findMusicStreamById(ps2, soundOrInstance); - if (music != nullptr && music->soundIndex >= PS2_AUDIO_STREAM_INDEX_BASE) { - musIndex = music->soundIndex - PS2_AUDIO_STREAM_INDEX_BASE; - } - } - } else { - // SOND resource index — map to AUDO via the SOND entry - if (ps2->sondEntryCount > (uint32_t) soundOrInstance) { - uint16_t mapped = ps2->sondEntries[soundOrInstance].audoIndex; - if (mapped != 0xFFFF) audoIndex = mapped; - } - } - - if (ps2->audoEntryCount > (uint32_t) audoIndex) { - if (audoIndex >= 0) { - Ps2AudoEntry* audo = &ps2->audoEntries[audoIndex]; - if (audo->sampleRate == 0 || audo->sampleCount == 0) return 0.0f; - return (float) audo->sampleCount / (float) audo->sampleRate; - } - if (musIndex >= 0) { - Ps2MusEntry* mus = &ps2->musEntries[musIndex]; - if (mus->sampleRate == 0 || mus->sampleCount == 0) return 0.0f; - return (float) mus->sampleCount / (float) mus->sampleRate; - } - } - return 0.0f; -} - -static void ps2SetMasterGain(AudioSystem* audio, float gain) { - // fprintf(stderr, "PS2AudioSystem: Setting master gain to %f\n", gain); - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - ps2->masterGain = gain; -} - -static void ps2SetChannelCount(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t count) { - // No-op: software mixer handles all channels internally -} - -static void ps2GroupLoad(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t groupIndex) { - // No-op: all audio is available from SOUNDS.BIN -} - -static bool ps2GroupIsLoaded(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t groupIndex) { - return true; -} - -static int32_t ps2CreateStream(AudioSystem* audio, const char* filename) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - if (!ps2->initialized) return -1; - - // Look up the filename in the MUS string table - for (int i = 0; ps2->musEntryCount > i; i++) { - if (strcmp(ps2->musEntries[i].name, filename) == 0) { - int32_t streamIndex = PS2_AUDIO_STREAM_INDEX_BASE + i; - fprintf(stderr, "PS2AudioSystem: Created stream %" PRId32 " for '%s'\n", streamIndex, filename); - return streamIndex; - } - } - - fprintf(stderr, "PS2AudioSystem: audio_create_stream: '%s' not found in MUS entries\n", filename); - return -1; -} - -static bool ps2DestroyStream(AudioSystem* audio, int32_t streamIndex) { - Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; - - // Stop all music streams that were playing this stream - repeat(MAX_MUSIC_STREAMS, i) { - if (ps2->musicStreams[i].active && ps2->musicStreams[i].soundIndex == streamIndex) { - ps2->musicStreams[i].active = false; - } - } - - return true; -} - -// ===[ Vtable ]=== - -static AudioSystemVtable ps2AudioSystemVtable = { - .init = ps2Init, - .destroy = ps2Destroy, - .update = ps2Update, - .playSound = ps2PlaySound, - .stopSound = ps2StopSound, - .stopAll = ps2StopAll, - .isPlaying = ps2IsPlaying, - .pauseSound = ps2PauseSound, - .resumeSound = ps2ResumeSound, - .pauseAll = ps2PauseAll, - .resumeAll = ps2ResumeAll, - .setSoundGain = ps2SetSoundGain, - .getSoundGain = ps2GetSoundGain, - .setSoundPitch = ps2SetSoundPitch, - .getSoundPitch = ps2GetSoundPitch, - .getTrackPosition = ps2GetTrackPosition, - .setTrackPosition = ps2SetTrackPosition, - .getSoundLength = ps2GetSoundLength, - .setMasterGain = ps2SetMasterGain, - .setChannelCount = ps2SetChannelCount, - .groupLoad = ps2GroupLoad, - .groupIsLoaded = ps2GroupIsLoaded, - .createStream = ps2CreateStream, - .destroyStream = ps2DestroyStream, -}; - -// ===[ Lifecycle ]=== - -Ps2AudioSystem* Ps2AudioSystem_create(void) { - Ps2AudioSystem* ps2 = safeCalloc(1, sizeof(Ps2AudioSystem)); - ps2->base.vtable = &ps2AudioSystemVtable; - ps2->masterGain = 1.0f; - return ps2; -} diff --git a/src/ps2/ps2_audio_system.h b/src/ps2/ps2_audio_system.h deleted file mode 100644 index bc9545f4..00000000 --- a/src/ps2/ps2_audio_system.h +++ /dev/null @@ -1,175 +0,0 @@ -#pragma once - -#include "common.h" -#include "../audio_system.h" - -#include -#include -#include - -#define MAX_PS2_SOUND_INSTANCES 64 -#define PS2_SOUND_INSTANCE_ID_BASE 100000 -#define LRU_CACHE_SIZE 64 -#define MIX_BUFFER_SAMPLES 512 -#define AUDSRV_OUTPUT_FREQ 22050 - -// Streaming music: decode ADPCM in chunks, double-buffered -// Each buffer holds STREAM_DECODE_SAMPLES decoded PCM samples -#define MAX_MUSIC_STREAMS 4 -#define PS2_AUDIO_STREAM_INDEX_BASE 300000 -#define STREAM_ADPCM_CHUNK_BYTES 4096 -#define STREAM_DECODE_SAMPLES (STREAM_ADPCM_CHUNK_BYTES * 2) // 2 samples per ADPCM byte - -// Maximum decoded PCM size for a single cached SFX. Anything larger is routed through the streaming path. -#define PS2_SFX_CACHE_MAX_BYTES (512 * 1024) - -// ===[ SOUNDBNK.BIN Structs ]=== - -typedef struct { - uint16_t audoIndex; // index into AUDO table, 0xFFFF = unmapped - uint32_t flags; - int16_t volume; // fixed-point: original float * 256 - int16_t pitch; // fixed-point: original float * 256 -} Ps2SondEntry; - -typedef struct { - uint32_t dataOffset; // byte offset in SOUNDS.BIN - uint32_t dataSize; // bytes in SOUNDS.BIN - uint16_t sampleRate; - uint8_t channels; - uint8_t bitsPerSample; - uint8_t format; // 0=PCM, 1=IMA ADPCM - uint32_t sampleCount; // decoded samples per channel; length in seconds = sampleCount / sampleRate -} Ps2AudoEntry; - -// ===[ SOUNDBNK.BIN MUS (Streamed Music) Structs ]=== - -#define MAX_MUS_ENTRIES 256 - -typedef struct { - char* name; // path string (e.g. "mus/field_of_hopes.ogg") - uint32_t dataOffset; // byte offset in SOUNDS.BIN - uint32_t dataSize; // bytes in SOUNDS.BIN - uint16_t sampleRate; - uint8_t channels; - uint8_t format; // 0=PCM, 1=IMA ADPCM - uint32_t sampleCount; // decoded samples per channel; length in seconds = sampleCount / sampleRate -} Ps2MusEntry; - -// ===[ LRU Decoded PCM Cache ]=== - -typedef struct { - int32_t audoIndex; // -1 = empty slot - int16_t* pcmData; - uint32_t pcmSampleCount; // number of mono samples - uint32_t pcmDataBytes; - uint32_t lastAccessCounter; -} DecodedPcmEntry; - -// ===[ Sound Instance ]=== - -typedef struct { - bool active; - int32_t soundIndex; // SOND resource index - int32_t audoIndex; // AUDO resource index - int32_t instanceId; // unique ID returned to GML - int32_t priority; - bool loop; - bool paused; - - // Playback position (32.32 fixed-point for fractional sample stepping) - uint32_t positionInt; - uint32_t positionFrac; - uint32_t totalSamples; - - // Pitch - float pitch; // runtime pitch set by GML - float sondPitch; // SOND resource pitch (fixed-point / 256) - - // Gain / volume - float currentGain; - float targetGain; - float startGain; - float fadeTimeRemaining; - float fadeTotalTime; - float sondVolume; // SOND resource volume (fixed-point / 256) -} Ps2SoundInstance; - -// ===[ Streaming Music Instance ]=== - -typedef struct { - bool active; - int32_t soundIndex; - int32_t audoIndex; - int32_t instanceId; - int32_t priority; - bool loop; - bool paused; - - // Gain / volume (same fields as Ps2SoundInstance) - float currentGain; - float targetGain; - float startGain; - float fadeTimeRemaining; - float fadeTotalTime; - float sondVolume; - float pitch; - float sondPitch; - - // ADPCM file streaming state - uint32_t fileOffset; // current read position in SOUNDS.BIN - uint32_t fileStartOffset; // start offset of this track in SOUNDS.BIN - uint32_t fileEndOffset; // end offset (fileStartOffset + dataSize) - - // IMA ADPCM decoder state (persists across chunks) - int32_t decoderPredictor; - int32_t decoderStepIndex; - - // Double-buffered decoded PCM - int16_t buffers[2][STREAM_DECODE_SAMPLES]; - uint32_t bufferSampleCount[2]; // actual samples decoded in each buffer (may be < STREAM_DECODE_SAMPLES at end of track) - int activeBuffer; // which buffer the mixer is reading from (0 or 1) - uint32_t readPosition; // integer sample position within the active buffer - uint32_t readPositionFrac; // fractional part (32-bit) for pitch resampling - bool needsRefill; // true when the back buffer needs to be filled - bool endOfTrack; // true when we've read all ADPCM data -} Ps2MusicStream; - -// ===[ PS2 Audio System ]=== - -typedef struct { - AudioSystem base; - - // SOUNDBNK.BIN index - uint16_t sondEntryCount; - uint16_t audoEntryCount; - uint16_t musEntryCount; - Ps2SondEntry* sondEntries; - Ps2AudoEntry* audoEntries; - Ps2MusEntry* musEntries; - - // SOUNDS.BIN file handle (streamed on demand, not loaded into RAM) - FILE* soundsFile; - - // LRU decoded PCM cache (for short embedded SFX) - DecodedPcmEntry cacheEntries[LRU_CACHE_SIZE]; - uint32_t cacheAccessCounter; - - // SFX instance slots (embedded sounds, fully decoded in LRU cache) - Ps2SoundInstance instances[MAX_PS2_SOUND_INSTANCES]; - int32_t nextInstanceCounter; - - // Streaming music slots (non-embedded sounds, double-buffered from disc) - Ps2MusicStream musicStreams[MAX_MUSIC_STREAMS]; - - // Mixer output buffer (stereo interleaved) - int16_t mixBuffer[MIX_BUFFER_SAMPLES * 2]; - - // Mixer accumulator (int32 mono; duplicated to L/R at clamp step) - int32_t mixAccum[MIX_BUFFER_SAMPLES]; - - float masterGain; - bool initialized; -} Ps2AudioSystem; - -Ps2AudioSystem* Ps2AudioSystem_create(void); diff --git a/src/ps2/ps2_file_system.c b/src/ps2/ps2_file_system.c deleted file mode 100644 index 62b7023a..00000000 --- a/src/ps2/ps2_file_system.c +++ /dev/null @@ -1,515 +0,0 @@ -#include "ps2_file_system.h" -#include "ps2_utils.h" -#include "../json_reader.h" -#include "../utils.h" - -#include -#include -#include -#include - -#include "stb_ds.h" - -// ===[ Internal Types ]=== - -typedef struct { - char* key; // game-relative file name - char** value; // stb_ds dynamic array of resolved device paths -} Ps2FileMapping; - -// Parsed save icon configuration from CONFIG.JSN "saveIcon" section -typedef struct { - uint32_t bgAlpha; - int32_t bgColors[4][4]; // 4 corners x RGBA - float lightDirs[3][4]; // 3 lights x XYZW - float lightColors[3][4]; // 3 lights x RGBA - float ambient[4]; // RGBA -} SaveIconConfig; - -typedef struct { - FileSystem base; - Ps2FileMapping* mappings; // stb_ds string hashmap - char* gameTitle; // game display name for icon.sys generation - SaveIconConfig saveIconConfig; -} Ps2FileSystem; - -// ===[ Helpers ]=== - -// Expands $BOOT: prefix to the boot device path, or returns a strdup of the input -static char* expandBootPrefix(const char* path) { - const char* bootPrefix = "$BOOT:"; - size_t bootPrefixLen = strlen(bootPrefix); - - if (strncmp(path, bootPrefix, bootPrefixLen) == 0) { - const char* relativePart = path + bootPrefixLen; - return PS2Utils_createDevicePath(relativePart); - } - - return safeStrdup(path); -} - -// ===[ icon.sys Generation ]=== -// icon.sys is a fixed 964-byte file required by the PS2 memory card browser -// Without it, the save directory shows as "Corrupted Data" - -#define ICON_SYS_SIZE 964 - -// Converts an ASCII character to its full-width Shift-JIS encoding (2 bytes) -// The PS2 memory card browser only renders Shift-JIS glyphs, not plain ASCII -// If a character is not supported, it will fall back to a space character -static void asciiToShiftJIS(char c, uint8_t* out) { - if (c == ' ') { - out[0] = 0x81; out[1] = 0x40; - } else if (c >= '0' && '9' >= c) { - out[0] = 0x82; out[1] = 0x4F + (c - '0'); - } else if (c >= 'A' && 'Z' >= c) { - out[0] = 0x82; out[1] = 0x60 + (c - 'A'); - } else if (c >= 'a' && 'z' >= c) { - out[0] = 0x82; out[1] = 0x81 + (c - 'a'); - } else if (c == '!') { - out[0] = 0x81; out[1] = 0x49; - } else if (c == '?') { - out[0] = 0x81; out[1] = 0x48; - } else if (c == '.') { - out[0] = 0x81; out[1] = 0x44; - } else if (c == ',') { - out[0] = 0x81; out[1] = 0x43; - } else if (c == ':') { - out[0] = 0x81; out[1] = 0x46; - } else if (c == '-') { - out[0] = 0x81; out[1] = 0x7C; - } else if (c == '(') { - out[0] = 0x81; out[1] = 0x69; - } else if (c == ')') { - out[0] = 0x81; out[1] = 0x6A; - } else { - // Unsupported character, use full-width space as fallback - out[0] = 0x81; out[1] = 0x40; - } -} - -// Writes the game title as full-width Shift-JIS into the icon.sys title field (68 bytes max) -static void writeShiftJISTitle(uint8_t* titleField, const char* gameTitle) { - size_t srcLen = strlen(gameTitle); - size_t dstPos = 0; - size_t maxBytes = 66; // 68 bytes minus 2 for null terminator safety - - repeat(srcLen, i) { - if (dstPos + 2 > maxBytes) - break; - asciiToShiftJIS(gameTitle[i], titleField + dstPos); - dstPos += 2; // Each Shift-JIS character is 2 bytes - } -} - -static void generateIconSys(uint8_t* buffer, const char* gameTitle, const SaveIconConfig* config) { - memset(buffer, 0, ICON_SYS_SIZE); - - // Magic "PS2D" - memcpy(buffer + 0x000, "PS2D", 4); - - // Offset 4: 2 bytes reserved (0) - // Offset 6: 2 bytes newline offset in title (0 = no line break) - // Offset 8: 4 bytes reserved (0) - // All left as 0 from memset - - // Background transparency (0x00-0x80) - memcpy(buffer + 0x00C, &config->bgAlpha, 4); - - // Background colors: 4 corners x RGBA as int32 (0-255 range) - memcpy(buffer + 0x010, config->bgColors, sizeof(config->bgColors)); - - // Light directions: 3 lights x XYZW as float - memcpy(buffer + 0x050, config->lightDirs, sizeof(config->lightDirs)); - - // Light colors: 3 lights x RGBA as float (0.0-1.0) - memcpy(buffer + 0x080, config->lightColors, sizeof(config->lightColors)); - - // Ambient color: RGBA as float - memcpy(buffer + 0x0B0, config->ambient, sizeof(config->ambient)); - - // Title (68 bytes, full-width Shift-JIS encoded) - writeShiftJISTitle(buffer + 0x0C0, gameTitle); - - // Icon filenames (64 bytes each) at 0x104, 0x144, 0x184 - // All three (normal, copy, delete) reference the same icon file - const char* iconFileName = "ICON.ICO"; - size_t iconNameLen = strlen(iconFileName); - memcpy(buffer + 0x104, iconFileName, iconNameLen); - memcpy(buffer + 0x144, iconFileName, iconNameLen); - memcpy(buffer + 0x184, iconFileName, iconNameLen); -} - -// Copies a file from src to dst (binary). Returns true on success. -static bool copyFile(const char* srcPath, const char* dstPath) { - FILE* src = fopen(srcPath, "rb"); - if (src == nullptr) - return false; - - fseek(src, 0, SEEK_END); - long size = ftell(src); - fseek(src, 0, SEEK_SET); - - uint8_t* data = safeMalloc((size_t) size); - size_t bytesRead = fread(data, 1, (size_t) size, src); - fclose(src); - - FILE* dst = fopen(dstPath, "wb"); - if (dst == nullptr) { - free(data); - return false; - } - - size_t written = fwrite(data, 1, bytesRead, dst); - fclose(dst); - free(data); - return written == bytesRead; -} - -// Copies ICON.ICO from the boot device into the given directory if it doesn't already exist -static void copyIconIcoIfMissing(const char* dirPath) { - size_t dirLen = strlen(dirPath); - size_t pathLen = dirLen + 1 + 8 + 1; // "/ICON.ICO\0" - char* dstPath = safeMalloc(pathLen); - snprintf(dstPath, pathLen, "%s/ICON.ICO", dirPath); - - // Check if it already exists on the memory card - FILE* check = fopen(dstPath, "rb"); - if (check != nullptr) { - fclose(check); - free(dstPath); - return; - } - - // Copy from boot device - char* srcPath = PS2Utils_createDevicePath("ICON.ICO"); - if (copyFile(srcPath, dstPath)) { - fprintf(stderr, "Ps2FileSystem: Copied ICON.ICO to %s\n", dirPath); - } else { - fprintf(stderr, "Ps2FileSystem: Failed to copy ICON.ICO from %s to %s\n", srcPath, dstPath); - } - - free(srcPath); - free(dstPath); -} - -// Writes icon.sys into the given directory if it doesn't already exist -static void writeIconSysIfMissing(const char* dirPath, const char* gameTitle, const SaveIconConfig* config) { - // Build path: "dirPath/icon.sys" - size_t dirLen = strlen(dirPath); - size_t pathLen = dirLen + 1 + 8 + 1; // "/icon.sys\0" - char* iconSysPath = safeMalloc(pathLen); - snprintf(iconSysPath, pathLen, "%s/icon.sys", dirPath); - - // Check if it already exists - FILE* check = fopen(iconSysPath, "rb"); - if (check != nullptr) { - fclose(check); - free(iconSysPath); - return; - } - - // Generate and write - uint8_t buffer[ICON_SYS_SIZE]; - generateIconSys(buffer, gameTitle, config); - - FILE* f = fopen(iconSysPath, "wb"); - if (f != nullptr) { - fwrite(buffer, 1, ICON_SYS_SIZE, f); - fclose(f); - fprintf(stderr, "Ps2FileSystem: Created icon.sys in %s\n", dirPath); - } else { - fprintf(stderr, "Ps2FileSystem: Failed to create icon.sys in %s\n", dirPath); - } - - free(iconSysPath); -} - -// Ensures the parent directory exists for mc0:/mc1: paths by calling mkdir -// Also writes icon.sys and copies ICON.ICO if the directory is newly created -static void ensureParentDirectory(Ps2FileSystem* pfs, const char* path) { - // Only do this for memory card paths - if (strncmp(path, "mc0:", 4) != 0 && strncmp(path, "mc1:", 4) != 0) - return; - - char* pathCopy = safeStrdup(path); - char* lastSlash = strrchr(pathCopy, '/'); - if (lastSlash != nullptr && lastSlash != pathCopy) { - *lastSlash = '\0'; - mkdir(pathCopy, 0777); - writeIconSysIfMissing(pathCopy, pfs->gameTitle, &pfs->saveIconConfig); - copyIconIcoIfMissing(pathCopy); - } - free(pathCopy); -} - -// ===[ Vtable Implementations ]=== - -static char* resolvePath(FileSystem* fs, const char* relativePath) { - Ps2FileSystem* pfs = (Ps2FileSystem*) fs; - ptrdiff_t idx = shgeti(pfs->mappings, relativePath); - if (0 > idx) - return nullptr; - - // Return the first mapped path - if (arrlen(pfs->mappings[idx].value) > 0) - return safeStrdup(pfs->mappings[idx].value[0]); - - return nullptr; -} - -static bool fileExists(FileSystem* fs, const char* relativePath) { - Ps2FileSystem* pfs = (Ps2FileSystem*) fs; - ptrdiff_t idx = shgeti(pfs->mappings, relativePath); - if (0 > idx) - return false; - - char** paths = pfs->mappings[idx].value; - int pathCount = arrlen(paths); - repeat(pathCount, i) { - FILE* f = fopen(paths[i], "rb"); - if (f != nullptr) { - fclose(f); - return true; - } - } - - return false; -} - -static char* readFileText(FileSystem* fs, const char* relativePath) { - Ps2FileSystem* pfs = (Ps2FileSystem*) fs; - ptrdiff_t idx = shgeti(pfs->mappings, relativePath); - if (0 > idx) - return nullptr; - - // For the PlayStation 2 target, we have multiple "search" paths for a specific file - // The reason why we do this is because GameMaker allows files to be in two different folders: The save folder and the bundled folder - // However, hitting the memory card for some specific files that ARE NOT in the memory card is a bit expensive - char** paths = pfs->mappings[idx].value; - int pathCount = arrlen(paths); - repeat(pathCount, i) { - FILE* f = fopen(paths[i], "rb"); - if (f == nullptr) - continue; - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - char* content = safeMalloc((size_t) size + 1); - size_t bytesRead = fread(content, 1, (size_t) size, f); - content[bytesRead] = '\0'; - fclose(f); - return content; - } - - return nullptr; -} - -static bool writeFileText(FileSystem* fs, const char* relativePath, const char* contents) { - Ps2FileSystem* pfs = (Ps2FileSystem*) fs; - ptrdiff_t idx = shgeti(pfs->mappings, relativePath); - if (0 > idx) - return false; - - char** paths = pfs->mappings[idx].value; - if (arrlen(paths) == 0) - return false; - - // Write to the first path (the first path is ALWAYS the writeable path) - const char* writePath = paths[0]; - ensureParentDirectory(pfs, writePath); - - FILE* f = fopen(writePath, "wb"); - if (f == nullptr) - return false; - - size_t len = strlen(contents); - size_t written = fwrite(contents, 1, len, f); - fclose(f); - return written == len; -} - -static bool deleteFile(FileSystem* fs, const char* relativePath) { - Ps2FileSystem* pfs = (Ps2FileSystem*) fs; - ptrdiff_t idx = shgeti(pfs->mappings, relativePath); - if (0 > idx) - return false; - - char** paths = pfs->mappings[idx].value; - if (arrlen(paths) == 0) - return false; - - // Delete the first path - return remove(paths[0]) == 0; -} - -static bool ps2ReadFileBinary(FileSystem* fs, const char* relativePath, uint8_t** outData, int32_t* outSize) { - Ps2FileSystem* pfs = (Ps2FileSystem*) fs; - ptrdiff_t idx = shgeti(pfs->mappings, relativePath); - if (0 > idx) - return false; - - char** paths = pfs->mappings[idx].value; - int pathCount = arrlen(paths); - repeat(pathCount, i) { - FILE* f = fopen(paths[i], "rb"); - if (f == nullptr) - continue; - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - uint8_t* data = safeMalloc((size_t) size); - size_t bytesRead = fread(data, 1, (size_t) size, f); - fclose(f); - - *outData = data; - *outSize = (int32_t) bytesRead; - return true; - } - - return false; -} - -static bool ps2WriteFileBinary(FileSystem* fs, const char* relativePath, const uint8_t* data, int32_t size) { - Ps2FileSystem* pfs = (Ps2FileSystem*) fs; - ptrdiff_t idx = shgeti(pfs->mappings, relativePath); - if (0 > idx) - return false; - - char** paths = pfs->mappings[idx].value; - if (arrlen(paths) == 0) - return false; - - const char* writePath = paths[0]; - ensureParentDirectory(pfs, writePath); - - FILE* f = fopen(writePath, "wb"); - if (f == nullptr) - return false; - - size_t written = fwrite(data, 1, (size_t) size, f); - fclose(f); - return written == (size_t) size; -} - -// ===[ Vtable ]=== - -static FileSystemVtable ps2FileSystemVtable = { - .resolvePath = resolvePath, - .fileExists = fileExists, - .readFileText = readFileText, - .writeFileText = writeFileText, - .deleteFile = deleteFile, - .readFileBinary = ps2ReadFileBinary, - .writeFileBinary = ps2WriteFileBinary, -}; - -// ===[ Lifecycle ]=== - -static SaveIconConfig parseSaveIconConfig(JsonValue* configRoot) { - JsonValue* saveIconObj = JsonReader_getObject(configRoot, "saveIcon"); - requireNotNullMessage(saveIconObj, "CONFIG.JSN is missing the 'saveIcon' section"); - require(JsonReader_isObject(saveIconObj)); - - SaveIconConfig config = {0}; - - // bgAlpha (0x00-0x80) - JsonValue* bgAlphaVal = JsonReader_getObject(saveIconObj, "bgAlpha"); - requireNotNullMessage(bgAlphaVal, "saveIcon.bgAlpha is missing"); - config.bgAlpha = (uint32_t) JsonReader_getDouble(bgAlphaVal); - - // bgColors: array of 4 arrays of 3 ints [R, G, B] (A is always 0) - JsonValue* bgColorsArr = JsonReader_getObject(saveIconObj, "bgColors"); - requireNotNullMessage(bgColorsArr, "saveIcon.bgColors is missing"); - require(JsonReader_isArray(bgColorsArr) && JsonReader_arrayLength(bgColorsArr) == 4); - repeat(4, i) { - JsonValue* corner = JsonReader_getArrayElement(bgColorsArr, i); - JsonReader_readInt32Array(corner, config.bgColors[i], 3); - config.bgColors[i][3] = 0; // A = 0 - } - - // lightDirs: array of 3 arrays of 3 floats [X, Y, Z] (W is always 0.0) - JsonValue* lightDirsArr = JsonReader_getObject(saveIconObj, "lightDirs"); - requireNotNullMessage(lightDirsArr, "saveIcon.lightDirs is missing"); - require(JsonReader_isArray(lightDirsArr) && JsonReader_arrayLength(lightDirsArr) == 3); - repeat(3, i) { - JsonValue* dir = JsonReader_getArrayElement(lightDirsArr, i); - JsonReader_readFloatArray(dir, config.lightDirs[i], 3); - config.lightDirs[i][3] = 0.0f; // W = 0 - } - - // lightColors: array of 3 arrays of 3 floats [R, G, B] (A is always 0.0) - JsonValue* lightColorsArr = JsonReader_getObject(saveIconObj, "lightColors"); - requireNotNullMessage(lightColorsArr, "saveIcon.lightColors is missing"); - require(JsonReader_isArray(lightColorsArr) && JsonReader_arrayLength(lightColorsArr) == 3); - repeat(3, i) { - JsonValue* color = JsonReader_getArrayElement(lightColorsArr, i); - JsonReader_readFloatArray(color, config.lightColors[i], 3); - config.lightColors[i][3] = 0.0f; // A = 0 - } - - // ambient: array of 3 floats [R, G, B] (A is always 0.0) - JsonValue* ambientArr = JsonReader_getObject(saveIconObj, "ambient"); - requireNotNullMessage(ambientArr, "saveIcon.ambient is missing"); - JsonReader_readFloatArray(ambientArr, config.ambient, 3); - config.ambient[3] = 0.0f; // A = 0 - - return config; -} - -FileSystem* Ps2FileSystem_create(JsonValue* configRoot, const char* gameTitle) { - JsonValue* fileSystemObj = JsonReader_getObject(configRoot, "fileSystem"); - require(fileSystemObj != nullptr && JsonReader_isObject(fileSystemObj)); - - Ps2FileSystem* pfs = safeCalloc(1, sizeof(Ps2FileSystem)); - pfs->base.vtable = &ps2FileSystemVtable; - pfs->gameTitle = safeStrdup(gameTitle); - pfs->saveIconConfig = parseSaveIconConfig(configRoot); - pfs->mappings = nullptr; - sh_new_strdup(pfs->mappings); - - int entryCount = JsonReader_objectLength(fileSystemObj); - repeat(entryCount, i) { - const char* gameFileName = JsonReader_getObjectKey(fileSystemObj, i); - JsonValue* pathArray = JsonReader_getObjectValue(fileSystemObj, i); - - require(JsonReader_isArray(pathArray)); - - char** resolvedPaths = nullptr; - int pathCount = JsonReader_arrayLength(pathArray); - repeat(pathCount, j) { - JsonValue* pathElement = JsonReader_getArrayElement(pathArray, j); - require(JsonReader_isString(pathElement)); - - const char* rawPath = JsonReader_getString(pathElement); - char* resolved = expandBootPrefix(rawPath); - arrput(resolvedPaths, resolved); - fprintf(stderr, "Ps2FileSystem: '%s' -> '%s'\n", gameFileName, resolved); - } - - shput(pfs->mappings, gameFileName, resolvedPaths); - } - - fprintf(stderr, "Ps2FileSystem: Loaded %d file mappings\n", (int) shlen(pfs->mappings)); - return (FileSystem*) pfs; -} - -void Ps2FileSystem_destroy(FileSystem* fs) { - Ps2FileSystem* pfs = (Ps2FileSystem*) fs; - free(pfs->gameTitle); - int mappingCount = shlen(pfs->mappings); - repeat(mappingCount, i) { - char** paths = pfs->mappings[i].value; - int pathCount = arrlen(paths); - repeat(pathCount, j) { - free(paths[j]); - } - arrfree(paths); - } - shfree(pfs->mappings); - free(pfs); -} diff --git a/src/ps2/ps2_file_system.h b/src/ps2/ps2_file_system.h deleted file mode 100644 index 25a977ea..00000000 --- a/src/ps2/ps2_file_system.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include "common.h" -#include "../file_system.h" -#include "../json_reader.h" - -// Creates a PS2 file system that maps game-relative file names to PS2 device paths -// using the "fileSystem" object from a parsed CONFIG.JSN root -// -// configRoot: parsed JSON root of CONFIG.JSN (caller retains ownership, not freed here) -// gameTitle: the game's display name (used for icon.sys on memory card saves) -FileSystem* Ps2FileSystem_create(JsonValue* configRoot, const char* gameTitle); -void Ps2FileSystem_destroy(FileSystem* fs); diff --git a/src/ps2/ps2_utils.c b/src/ps2/ps2_utils.c deleted file mode 100644 index 39ac083d..00000000 --- a/src/ps2/ps2_utils.c +++ /dev/null @@ -1,118 +0,0 @@ -#include -#include -#include -#include -#include -#include "../utils.h" -#include "ps2_utils.h" - -PS2DeviceKey deviceKey; -bool deviceKeyLoaded = false; - -void PS2Utils_extractDeviceKey(const char* path) { - require(!deviceKeyLoaded); - - char* pos = strchr(path, ':'); - requireNotNull(pos); - - size_t length = pos - path; - char* result = safeMalloc((length + 1) * sizeof(char)); - strncpy(result, path, length); - result[length] = '\0'; - - // The "result" is the device key as a string (example: "mass" or "host") - deviceKey = (PS2DeviceKey) { - .key = result, - .usesISO9660 = strncmp(result, "cdrom", strlen("cdrom")) == 0, - }; - - deviceKeyLoaded = true; -} - -// Loads the required IOP drivers based on the current device key -// For cdrom devices, this loads the CDVD filesystem modules so we can read from the disc -void PS2Utils_loadFSDrivers() { - require(deviceKeyLoaded); - - if (deviceKey.usesISO9660) { - fprintf(stderr, "PS2Utils: Loading CDVD drivers for device key '%s'\n", deviceKey.key); - - int ret; - ret = SifLoadModule("rom0:CDVDMAN", 0, nullptr); - if (0 > ret) { - fprintf(stderr, "PS2Utils: Failed to load CDVDMAN: %d\n", ret); - abort(); - } - - ret = SifLoadModule("rom0:CDVDFSV", 0, nullptr); - if (0 > ret) { - fprintf(stderr, "PS2Utils: Failed to load CDVDFSV: %d\n", ret); - abort(); - } - - sceCdInit(SCECdINIT); - fprintf(stderr, "PS2Utils: CDVD initialized\n"); - } -} - -#ifdef GPROF_PROFILING -// Embedded USB mass storage IRX modules (generated by bin2c at build time, only in profiler builds) -extern unsigned char usbd_irx[]; -extern unsigned int size_usbd_irx; -extern unsigned char bdm_irx[]; -extern unsigned int size_bdm_irx; -extern unsigned char bdmfs_fatfs_irx[]; -extern unsigned int size_bdmfs_fatfs_irx; -extern unsigned char usbmass_bd_irx[]; -extern unsigned int size_usbmass_bd_irx; - -void PS2Utils_loadMassStorageDrivers() { - require(deviceKeyLoaded); - - fprintf(stderr, "PS2Utils: Loading USB mass storage drivers for gprof output...\n"); - - int ret; - ret = SifExecModuleBuffer(usbd_irx, size_usbd_irx, 0, nullptr, nullptr); - if (0 > ret) { - fprintf(stderr, "PS2Utils: Failed to load usbd: %d\n", ret); - } - - ret = SifExecModuleBuffer(bdm_irx, size_bdm_irx, 0, nullptr, nullptr); - if (0 > ret) { - fprintf(stderr, "PS2Utils: Failed to load bdm: %d\n", ret); - } - - ret = SifExecModuleBuffer(bdmfs_fatfs_irx, size_bdmfs_fatfs_irx, 0, nullptr, nullptr); - if (0 > ret) { - fprintf(stderr, "PS2Utils: Failed to load bdmfs_fatfs: %d\n", ret); - } - - ret = SifExecModuleBuffer(usbmass_bd_irx, size_usbmass_bd_irx, 0, nullptr, nullptr); - if (0 > ret) { - fprintf(stderr, "PS2Utils: Failed to load usbmass_bd: %d\n", ret); - } - - // Wait for USB device detection - sleep(3); - - fprintf(stderr, "PS2Utils: USB mass storage drivers loaded\n"); -} -#endif - -// Creates a path with the device key + path for the loaded device key -// You need to free after using the path! -char* PS2Utils_createDevicePath(const char* path) { - require(deviceKeyLoaded); - - if (deviceKey.usesISO9660) { - size_t len = strlen(deviceKey.key) + 3 + strlen(path) + 2 + 1; - char* devicePath = safeMalloc(len); - snprintf(devicePath, len, "%s:\\%s;1", deviceKey.key, path); - return devicePath; - } else { - size_t len = strlen(deviceKey.key) + 1 + strlen(path) + 1; - char* devicePath = safeMalloc(len); - snprintf(devicePath, len, "%s:%s", deviceKey.key, path); - return devicePath; - } -} diff --git a/src/ps2/ps2_utils.h b/src/ps2/ps2_utils.h deleted file mode 100644 index 61b830bc..00000000 --- a/src/ps2/ps2_utils.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "common.h" -#include -#include - -#define GS_VRAM_SIZE (4 * 1024 * 1024) - -// Clamp alpha to 0.0-1.0, then scale to PS2 GS range (0-128). -// Without clamping, values > 1.0 cause uint8_t overflow/wrapping, making fades repeat. -static inline uint8_t alphaToGS(float alpha) { - if (alpha > 1.0f) alpha = 1.0f; - else if (0.0f > alpha) alpha = 0.0f; - return (uint8_t) (alpha * 128.0f); -} - -typedef struct { - char* key; - bool usesISO9660; -} PS2DeviceKey; - -extern PS2DeviceKey deviceKey; -extern bool deviceKeyLoaded; - -void PS2Utils_extractDeviceKey(const char* path); -void PS2Utils_loadFSDrivers(); -char* PS2Utils_createDevicePath(const char* path); - -#ifdef GPROF_PROFILING -// Loads USB mass storage IOP drivers (usbd, bdm, bdmfs_fatfs, usbmass_bd) -// so gprof can write gmon.out to mass: when not running from host: -void PS2Utils_loadMassStorageDrivers(); -#endif diff --git a/src/renderer.h b/src/renderer.h index ac168031..ace35e7d 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -1,586 +1,747 @@ -#pragma once - -#include "common.h" -#include -#include -#include - -#include "data_win.h" -#include "instance.h" - -// GameMaker Blend Modes -#define bm_complex -1 - -#define bm_normal 0 -#define bm_add 1 -#define bm_max 2 -#define bm_subtract 3 -#define bm_min 4 -#define bm_reverse_subtract 5 - -#define bm_zero 1 -#define bm_one 2 -#define bm_src_color 3 -#define bm_inv_src_color 4 -#define bm_src_alpha 5 -#define bm_inv_src_alpha 6 -#define bm_dest_alpha 7 -#define bm_inv_dest_alpha 8 -#define bm_dest_color 9 -#define bm_inv_dest_color 10 -#define bm_src_alpha_sat 11 - -// Nine-slice tile mode constants -#define NS_STRETCH 0 -#define NS_REPEAT 1 -#define NS_MIRROR 2 -#define NS_BLANKREPEAT 3 -#define NS_HIDE 4 - -// ===[ Renderer Vtable ]=== - -typedef struct Renderer Renderer; - -typedef struct { - void (*init)(Renderer* renderer, DataWin* dataWin); - void (*destroy)(Renderer* renderer); - void (*beginFrame)(Renderer* renderer, int32_t gameW, int32_t gameH, int32_t windowW, int32_t windowH); - void (*endFrame)(Renderer* renderer); - void (*beginView)(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, int32_t portX, int32_t portY, int32_t portW, int32_t portH, float viewAngle); - void (*endView)(Renderer* renderer); - // GUI pass: coordinates are (0,0)..(guiW,guiH) mapped to the current view's port rect. Called after endView. - void (*beginGUI)(Renderer* renderer, int32_t guiW, int32_t guiH, int32_t portX, int32_t portY, int32_t portW, int32_t portH); - void (*endGUI)(Renderer* renderer); - void (*drawSprite)(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha); - void (*drawSpritePart)(Renderer* renderer, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha); - void (*drawSpritePos)(Renderer* renderer, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha); - void (*drawRectangle)(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline); - void (*drawLine)(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color, float alpha); - void (*drawTriangle)(Renderer *renderer, float x1, float y1, float x2, float y2, float x3, float y3, bool outline); - void (*drawLineColor)(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, uint32_t color2, float alpha); - void (*drawText)(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg); - void (*drawTextColor)(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg, int32_t c1, int32_t c2, int32_t c3, int32_t c4, float alpha); - void (*flush)(Renderer* renderer); - int32_t (*createSpriteFromSurface)(Renderer* renderer, int32_t x, int32_t y, int32_t w, int32_t h, bool removeback, bool smooth, int32_t xorig, int32_t yorig); - void (*deleteSprite)(Renderer* renderer, int32_t spriteIndex); - void (*gpuSetBlendMode)(Renderer* renderer, int32_t mode); - void (*gpuSetBlendModeExt)(Renderer* renderer, int32_t sfactor, int32_t dfactor); - void (*gpuSetBlendEnable)(Renderer* renderer, bool enable); - void (*gpuSetAlphaTestEnable)(Renderer* renderer, bool enable); - void (*gpuSetAlphaTestRef)(Renderer* renderer, uint8_t ref); - void (*gpuSetColorWriteEnable)(Renderer* renderer, bool red, bool green, bool blue, bool alpha); - // Optional: platform-specific tile rendering (nullptr = use default drawSpritePart path) - void (*drawTile)(Renderer* renderer, RoomTile* tile, float offsetX, float offsetY); - // Optional: platform-specific tiled draw (nullptr = use default per-tile drawSprite loop). - void (*drawTiled)(Renderer* renderer, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha); - // Optional: tile a source sub-rect (in tpag source-page space) across a dest rect, for nine-slice Repeat/BlankRepeat at angle 0. - // srcX/srcY are post tpag->targetX/Y. nullptr = per-tile drawSpritePart fallback (also used for Mirror and non-zero angle). - void (*drawTiledPart)(Renderer* renderer, int32_t tpagIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint32_t color, float alpha); -} RendererVtable; - -// ===[ Renderer Base Struct ]=== - -struct Renderer { - RendererVtable* vtable; - DataWin* dataWin; - uint32_t drawColor; // BGR format, default 0xFFFFFF (white) - float drawAlpha; // default 1.0 - int32_t drawFont; // default -1 (no font) - int32_t drawHalign; // 0=left, 1=center, 2=right - int32_t drawValign; // 0=top, 1=middle, 2=bottom -}; - -// ===[ Shared Helpers (platform-agnostic) ]=== - -// Resolves a sprite + subimage to a TPAG index, with frame wrapping -static int32_t Renderer_resolveTPAGIndex(DataWin* dataWin, int32_t spriteIndex, int32_t subimg) { - if (0 > spriteIndex || dataWin->sprt.count <= (uint32_t) spriteIndex) return -1; - - Sprite* sprite = &dataWin->sprt.sprites[spriteIndex]; - if (sprite->textureCount == 0) return -1; - - // Wrap subimage index - int32_t frameIndex = subimg % (int32_t) sprite->textureCount; - if (0 > frameIndex) frameIndex += (int32_t) sprite->textureCount; - - return sprite->tpagIndices[frameIndex]; -} - -// Forward declaration: defined further down once drawSpritePartExt is available. -static void Renderer_drawSpriteNineSlice(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float w, float h, bool flipX, bool flipY, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha); - -// Stretched: draw_sprite_stretched(sprite, subimg, x, y, w, h) -static void Renderer_drawSpriteStretched(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float w, float h, uint32_t color, float alpha) { - DataWin* dw = renderer->dataWin; - if (spriteIndex >= 0 && (uint32_t) spriteIndex < dw->sprt.count && dw->sprt.sprites[spriteIndex].nineSliceEnabled) { - Renderer_drawSpriteNineSlice(renderer, spriteIndex, subimg, x, y, w, h, false, false, 0.0f, x, y, color, alpha); - return; - } - - int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); - if (0 > tpagIndex) return; - - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - float xscale = w / (float) tpag->boundingWidth; - float yscale = h / (float) tpag->boundingHeight; - renderer->vtable->drawSprite(renderer, tpagIndex, x, y, 0.0f, 0.0f, xscale, yscale, 0.0f, color, alpha); +#pragma once + +#include "common.h" +#include +#include +#include +#include "matrix_math.h" +#include "data_win.h" +#include "instance.h" + +// GameMaker Blend Modes +#define bm_complex -1 + +#define bm_normal 0 +#define bm_add 1 +#define bm_max 2 +#define bm_subtract 3 +#define bm_min 4 +#define bm_reverse_subtract 5 + +#define bm_zero 1 +#define bm_one 2 +#define bm_src_color 3 +#define bm_inv_src_color 4 +#define bm_src_alpha 5 +#define bm_inv_src_alpha 6 +#define bm_dest_alpha 7 +#define bm_inv_dest_alpha 8 +#define bm_dest_color 9 +#define bm_inv_dest_color 10 +#define bm_src_alpha_sat 11 + +// Nine-slice tile mode constants +#define NS_STRETCH 0 +#define NS_REPEAT 1 +#define NS_MIRROR 2 +#define NS_BLANKREPEAT 3 +#define NS_HIDE 4 + +typedef struct Renderer Renderer; +typedef struct Runner Runner; + +typedef struct { + void (*init)(Renderer* renderer, DataWin* dataWin); + void (*destroy)(Renderer* renderer); + void (*beginFrame)(Renderer* renderer, int32_t gameW, int32_t gameH, int32_t windowW, int32_t windowH); + void (*endFrame)(Renderer* renderer); + void (*beginView)(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, int32_t portX, int32_t portY, int32_t portW, int32_t portH, float viewAngle); + void (*endView)(Renderer* renderer); + // GUI pass: coordinates are (0,0)..(guiW,guiH) mapped to the current view's port rect. Called after endView. + void (*beginGUI)(Renderer* renderer, int32_t guiW, int32_t guiH, int32_t portX, int32_t portY, int32_t portW, int32_t portH); + void (*endGUI)(Renderer* renderer); + void (*drawSprite)(Renderer* renderer, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha); + void (*drawSpritePart)(Renderer* renderer, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha); + void (*drawSpritePos)(Renderer* renderer, int32_t tpagIndex, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha); + void (*drawRectangle)(Renderer* renderer, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline); + void (*drawLine)(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color, float alpha); + void (*drawTriangle)(Renderer *renderer, float x1, float y1, float x2, float y2, float x3, float y3, bool outline); + void (*drawLineColor)(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uint32_t color1, uint32_t color2, float alpha); + void (*drawText)(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg); + void (*drawTextColor)(Renderer* renderer, const char* text, float x, float y, float xscale, float yscale, float angleDeg, int32_t c1, int32_t c2, int32_t c3, int32_t c4, float alpha); + void (*flush)(Renderer* renderer); + void (*clearScreen)(Renderer* renderer, uint32_t color, float alpha); + int32_t (*createSpriteFromSurface)(Renderer* renderer, int32_t surfaceID, int32_t x, int32_t y, int32_t w, int32_t h, bool removeback, bool smooth, int32_t xorig, int32_t yorig); + void (*deleteSprite)(Renderer* renderer, int32_t spriteIndex); + void (*gpuSetBlendMode)(Renderer* renderer, int32_t mode); + void (*gpuSetBlendModeExt)(Renderer* renderer, int32_t sfactor, int32_t dfactor); + void (*gpuSetBlendEnable)(Renderer* renderer, bool enable); + void (*gpuSetAlphaTestEnable)(Renderer* renderer, bool enable); + void (*gpuSetAlphaTestRef)(Renderer* renderer, uint8_t ref); + void (*gpuSetColorWriteEnable)(Renderer* renderer, bool red, bool green, bool blue, bool alpha); + // Optional: when enabled, replaces output RGB with the fog color (preserving alpha) + void (*gpuSetFog)(Renderer* renderer, bool enable, uint32_t color); + // Optional: platform-specific tile rendering (nullptr = use default drawSpritePart path) + void (*drawTile)(Renderer* renderer, RoomTile* tile, float offsetX, float offsetY); + // Optional: called after a room is fully initialized/restored so the renderer can prewarm room-specific resources. + void (*prewarmRoom)(Renderer* renderer, Runner* runner); + // Optional: platform-specific tiled draw (nullptr = use default per-tile drawSprite loop). + void (*drawTiled)(Renderer* renderer, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha); + // Surface Functions + int32_t (*createSurface)(Renderer* renderer, int32_t width, int32_t height); + bool (*surfaceExists)(Renderer* renderer, int32_t surfaceID); + bool (*setSurfaceTarget)(Renderer* renderer, int32_t surfaceID); + bool (*resetSurfaceTarget)(Renderer* renderer); + float (*getSurfaceWidth)(Renderer* renderer, int32_t surfaceID); + float (*getSurfaceHeight)(Renderer* renderer, int32_t surfaceID); + void (*drawSurface)(Renderer* renderer, int32_t surfaceID, float x, float y, float xscale, float yscale, float angleDeg, uint32_t color, float alpha); + void (*drawSurfacePart)(Renderer* renderer, int32_t surfaceID, int32_t x, int32_t y, int32_t left, int32_t top, int32_t width, int32_t height, float xscale, float yscale, uint32_t color, float alpha); + void (*drawSurfaceStretched)(Renderer* renderer, int32_t surfaceID, float x, float y, float width, float height); + void (*surfaceResize)(Renderer* renderer, int32_t surfaceID, int32_t width, int32_t height); + void (*surfaceFree)(Renderer* renderer, int32_t surfaceID); + void (*surfaceCopy)(Renderer* renderer, int32_t DestSurfaceID, int32_t DestX, int32_t DestY, int32_t SrcSurfaceID, int32_t SrcX, int32_t SrcY, int32_t SrcW, int32_t SrcH, bool part); + // Optional: tile a source sub-rect (in tpag source-page space) across a dest rect, for nine-slice Repeat/BlankRepeat at angle 0. + // srcX/srcY are post tpag->targetX/Y. nullptr = per-tile drawSpritePart fallback (also used for Mirror and non-zero angle). + void (*drawTiledPart)(Renderer* renderer, int32_t tpagIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint32_t color, float alpha); +} RendererVtable; + +// ===[ Renderer Base Struct ]=== + +struct Renderer { + RendererVtable* vtable; + DataWin* dataWin; + uint32_t drawColor; // BGR format, default 0xFFFFFF (white) + float drawAlpha; // default 1.0 + int32_t drawFont; // default -1 (no font) + int32_t drawHalign; // 0=left, 1=center, 2=right + int32_t drawValign; // 0=top, 1=middle, 2=bottom + int32_t circlePrecision; // segments used by draw_circle/draw_ellipse, clamped to [4, 64] and rounded down to multiple of 4. Default 24. + bool textMarkupSawEscape; + int32_t textMarkupSkipChars; + //It's The Simplest Way I Found To Restore Previous Thingies For Rendering SORRY + Matrix4f PreviousViewMatrix; + int32_t CPortX; + int32_t CPortY; + int32_t CPortW; + int32_t CPortH; +}; + +// ===[ Shared Helpers (platform-agnostic) ]=== + +static bool Renderer_isFiniteFloat(float value) { + return isfinite(value); +} + +// Resolves a sprite + subimage to a TPAG index, with frame wrapping +static int32_t Renderer_resolveTPAGIndex(DataWin* dataWin, int32_t spriteIndex, int32_t subimg) { + if (0 > spriteIndex || dataWin->sprt.count <= (uint32_t) spriteIndex) return -1; + + Sprite* sprite = &dataWin->sprt.sprites[spriteIndex]; + if (sprite->textureCount == 0) return -1; + + // Wrap subimage index + int32_t frameIndex = subimg % (int32_t) sprite->textureCount; + if (0 > frameIndex) frameIndex += (int32_t) sprite->textureCount; + + return sprite->tpagIndices[frameIndex]; +} + +// Forward declaration: defined further down once drawSpritePartExt is available. +static void Renderer_drawSpriteNineSlice(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float w, float h, bool flipX, bool flipY, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha); + +// Stretched: draw_sprite_stretched(sprite, subimg, x, y, w, h) +static void Renderer_drawSpriteStretched(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float w, float h, uint32_t color, float alpha) { + if (!Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) || + !Renderer_isFiniteFloat(w) || !Renderer_isFiniteFloat(h) || + !Renderer_isFiniteFloat(alpha) || w == 0.0f || h == 0.0f) { + return; + } + + DataWin* dw = renderer->dataWin; + if (spriteIndex >= 0 && (uint32_t) spriteIndex < dw->sprt.count && dw->sprt.sprites[spriteIndex].nineSliceEnabled) { + Renderer_drawSpriteNineSlice(renderer, spriteIndex, subimg, x, y, w, h, false, false, 0.0f, x, y, color, alpha); + return; + } + + int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); + if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; + + TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; + if (tpag->boundingWidth == 0 || tpag->boundingHeight == 0) return; + float xscale = w / (float) tpag->boundingWidth; + float yscale = h / (float) tpag->boundingHeight; + renderer->vtable->drawSprite(renderer, tpagIndex, x, y, 0.0f, 0.0f, xscale, yscale, 0.0f, color, alpha); +} + +// Full version: draw_sprite_ext(sprite, subimg, x, y, xscale, yscale, rot, color, alpha) +static void Renderer_drawSpriteExt(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float xscale, float yscale, float rot, uint32_t color, float alpha) { + if (!Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) || + !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) || + !Renderer_isFiniteFloat(rot) || !Renderer_isFiniteFloat(alpha)) { + return; + } + + DataWin* dw = renderer->dataWin; + int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); + if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; + + Sprite* sprite = &dw->sprt.sprites[spriteIndex]; + + // Nine-slice activates only when the draw scales the sprite away from its native size. At scale 1 there is nothing to slice. + if (sprite->nineSliceEnabled && (xscale != 1.0f || yscale != 1.0f)) { + bool flipX = 0.0f > xscale; + bool flipY = 0.0f > yscale; + float absX = fabsf(xscale); + float absY = fabsf(yscale); + float w = (float) sprite->width * absX; + float h = (float) sprite->height * absY; + float tlX = x - (float) sprite->originX * xscale; // signed: negative xscale shifts tlX right + float tlY = y - (float) sprite->originY * yscale; + Renderer_drawSpriteNineSlice(renderer, spriteIndex, subimg, tlX, tlY, w, h, flipX, flipY, rot, x, y, color, alpha); + return; + } + + renderer->vtable->drawSprite(renderer, tpagIndex, x, y, (float) sprite->originX, (float) sprite->originY, xscale, yscale, rot, color, alpha); +} + +// Convenience: draw_sprite(sprite, subimg, x, y) +static void Renderer_drawSprite(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y) { + Renderer_drawSpriteExt(renderer, spriteIndex, subimg, x, y, 1.0f, 1.0f, 0.0f, 0xFFFFFF, renderer->drawAlpha); +} + +static void Renderer_drawSpritePos(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) { + if (!Renderer_isFiniteFloat(x1) || !Renderer_isFiniteFloat(y1) || + !Renderer_isFiniteFloat(x2) || !Renderer_isFiniteFloat(y2) || + !Renderer_isFiniteFloat(x3) || !Renderer_isFiniteFloat(y3) || + !Renderer_isFiniteFloat(x4) || !Renderer_isFiniteFloat(y4) || + !Renderer_isFiniteFloat(alpha)) { + return; + } + + DataWin* dw = renderer->dataWin; + int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); + if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; + if (renderer->vtable->drawSpritePos == nullptr) return; + + renderer->vtable->drawSpritePos(renderer, tpagIndex, x1, y1, x2, y2, x3, y3, x4, y4, alpha); +} + +static int32_t Renderer_createSurface(Renderer* renderer, int32_t width, int32_t height) { + //if (0 > width) (0 > height) return; + return renderer->vtable->createSurface(renderer, width, height); +} + +static bool Renderer_surfaceExists(Renderer* renderer, int32_t surfaceIndex) { + return renderer->vtable->surfaceExists(renderer, surfaceIndex); +} + +static float Renderer_getSurfaceWidth(Renderer* renderer, int32_t surfaceIndex) { + return renderer->vtable->getSurfaceWidth(renderer, surfaceIndex); +} + +static float Renderer_getSurfaceHeight(Renderer* renderer, int32_t surfaceIndex) { + return renderer->vtable->getSurfaceHeight(renderer, surfaceIndex); +} + + +static bool Renderer_surfaceSetTarget(Renderer* renderer, int32_t surfaceIndex) { + renderer->vtable->flush(renderer); + return renderer->vtable->setSurfaceTarget(renderer, surfaceIndex); +} + +static bool Renderer_surfaceResetTarget(Renderer* renderer) { + renderer->vtable->flush(renderer); + return renderer->vtable->resetSurfaceTarget(renderer); +} + +// Draws part of a sprite with extended parameters (scale, rotation, color, alpha) +static void Renderer_drawSpritePartExt(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t left, int32_t top, int32_t width, int32_t height, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { + if (!Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) || + !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) || + !Renderer_isFiniteFloat(angleDeg) || + !Renderer_isFiniteFloat(pivotX) || !Renderer_isFiniteFloat(pivotY) || + !Renderer_isFiniteFloat(alpha)) { + return; + } + + DataWin* dw = renderer->dataWin; + int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); + if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; + + TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; + + // Clip region to TPAG bounds (same as Renderer_drawSpritePart) + if (tpag->targetX > left) { + int32_t off = tpag->targetX - left; + x += (float) off * xscale; + width -= off; + left = 0; + } else { + left -= tpag->targetX; + } + + if (tpag->targetY > top) { + int32_t off = tpag->targetY - top; + y += (float) off * yscale; + height -= off; + top = 0; + } else { + top -= tpag->targetY; + } + + if (width > tpag->sourceWidth - left) width = tpag->sourceWidth - left; + if (height > tpag->sourceHeight - top) height = tpag->sourceHeight - top; + if (0 >= width || 0 >= height) return; + + renderer->vtable->drawSpritePart(renderer, tpagIndex, left, top, width, height, x, y, xscale, yscale, angleDeg, pivotX, pivotY, color, alpha); +} + +// Partial draw: draw_sprite_part(sprite, subimg, left, top, width, height, x, y) +static void Renderer_drawSpritePart(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t left, int32_t top, int32_t width, int32_t height, float x, float y) { + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, left, top, width, height, x, y, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0xFFFFFF, renderer->drawAlpha); +} + +// Resolves tpag and converts nine-slice bounding-box coords to tpag source-page space for drawTiledPart. +// Returns false if the resulting region is empty. Adjusts all in/out parameters in place. +// aX/aY/aW/aH: source coords (in, out). adX/adY/adW/adH: dest coords (in, out; pass nullptr for axes that don't change). +static bool Renderer_nineSliceAdjustForTiledPart(DataWin* dw, int32_t spriteIndex, int32_t subimg, int32_t* aX, int32_t* aY, int32_t* aW, int32_t* aH, float* adX, float* adY, float* adW, float* adH, int32_t* tpagIndexOut) { + int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); + if (0 > tpagIndex) return false; + *tpagIndexOut = tpagIndex; + TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; + if (tpag->targetX > *aX) { int32_t off = tpag->targetX - *aX; if (adX) *adX += (float) off; if (adW) *adW -= (float) off; *aW -= off; *aX = 0; } else { *aX -= tpag->targetX; } + if (tpag->targetY > *aY) { int32_t off = tpag->targetY - *aY; if (adY) *adY += (float) off; if (adH) *adH -= (float) off; *aH -= off; *aY = 0; } else { *aY -= tpag->targetY; } + if (*aW > tpag->sourceWidth - *aX) *aW = tpag->sourceWidth - *aX; + if (*aH > tpag->sourceHeight - *aY) *aH = tpag->sourceHeight - *aY; + return 0 < *aW && 0 < *aH; +} + +// Tiles srcW x srcH pixels from (srcX, srcY) horizontally across dstW pixels starting at (dstX, dstY). +// Mirror mode flips alternate tiles on the horizontal axis. +static void Renderer_nineSliceTileH(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, uint8_t mode, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { + if (mode != NS_MIRROR && angleDeg == 0.0f && renderer->vtable->drawTiledPart != nullptr) { + int32_t tpagIndex, aX = srcX, aY = srcY, aW = srcW, aH = srcH; + float adX = dstX, adY = dstY, adW = dstW; + if (!Renderer_nineSliceAdjustForTiledPart(renderer->dataWin, spriteIndex, subimg, &aX, &aY, &aW, &aH, &adX, &adY, &adW, nullptr, &tpagIndex) || 0.0f >= adW) return; + renderer->vtable->drawTiledPart(renderer, tpagIndex, aX, aY, aW, aH, adX, adY, adW, (float) aH, color, alpha); + return; + } + float cursor = dstX; + float remaining = dstW; + int32_t tileIndex = 0; + while (remaining > 0.0f) { + bool flipped = (mode == NS_MIRROR) && (tileIndex % 2 == 1); + int32_t drawW = ((float) srcW > remaining) ? (int32_t) remaining : srcW; + int32_t srcLeft = flipped ? (srcX + srcW - drawW) : srcX; + float xs = flipped ? -1.0f : 1.0f; + float drawX = flipped ? (cursor + (float) drawW) : cursor; + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcLeft, srcY, drawW, srcH, drawX, dstY, xs, 1.0f, angleDeg, pivotX, pivotY, color, alpha); + cursor += (float) drawW; + remaining -= (float) drawW; + tileIndex++; + } +} + +// Tiles srcW x srcH pixels from (srcX, srcY) vertically across dstH pixels starting at (dstX, dstY). +// Mirror mode flips alternate tiles on the vertical axis. +static void Renderer_nineSliceTileV(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstH, uint8_t mode, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { + if (mode != NS_MIRROR && angleDeg == 0.0f && renderer->vtable->drawTiledPart != nullptr) { + int32_t tpagIndex, aX = srcX, aY = srcY, aW = srcW, aH = srcH; + float adX = dstX, adY = dstY, adH = dstH; + if (!Renderer_nineSliceAdjustForTiledPart(renderer->dataWin, spriteIndex, subimg, &aX, &aY, &aW, &aH, &adX, &adY, nullptr, &adH, &tpagIndex) || 0.0f >= adH) return; + renderer->vtable->drawTiledPart(renderer, tpagIndex, aX, aY, aW, aH, adX, adY, (float) aW, adH, color, alpha); + return; + } + float cursor = dstY; + float remaining = dstH; + int32_t tileIndex = 0; + while (remaining > 0.0f) { + bool flipped = (mode == NS_MIRROR) && (tileIndex % 2 == 1); + int32_t drawH = ((float) srcH > remaining) ? (int32_t) remaining : srcH; + int32_t srcTop = flipped ? (srcY + srcH - drawH) : srcY; + float ys = flipped ? -1.0f : 1.0f; + float drawY = flipped ? (cursor + (float) drawH) : cursor; + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcX, srcTop, srcW, drawH, dstX, drawY, 1.0f, ys, angleDeg, pivotX, pivotY, color, alpha); + cursor += (float) drawH; + remaining -= (float) drawH; + tileIndex++; + } +} + +// Tiles a 2D region across dstW x dstH. Mirror flips alternate tiles on each axis independently. +static void Renderer_nineSliceTile2D(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint8_t mode, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { + if (mode != NS_MIRROR && angleDeg == 0.0f && renderer->vtable->drawTiledPart != nullptr) { + int32_t tpagIndex, aX = srcX, aY = srcY, aW = srcW, aH = srcH; + float adX = dstX, adY = dstY, adW = dstW, adH = dstH; + if (!Renderer_nineSliceAdjustForTiledPart(renderer->dataWin, spriteIndex, subimg, &aX, &aY, &aW, &aH, &adX, &adY, &adW, &adH, &tpagIndex) || 0.0f >= adW || 0.0f >= adH) return; + renderer->vtable->drawTiledPart(renderer, tpagIndex, aX, aY, aW, aH, adX, adY, adW, adH, color, alpha); + return; + } + float cursorY = dstY; + float remH = dstH; + int32_t tileRow = 0; + while (remH > 0.0f) { + bool flipY = (mode == NS_MIRROR) && (tileRow % 2 == 1); + int32_t drawH = ((float) srcH > remH) ? (int32_t) remH : srcH; + int32_t srcTop = flipY ? (srcY + srcH - drawH) : srcY; + float ys = flipY ? -1.0f : 1.0f; + float drawY = flipY ? (cursorY + (float) drawH) : cursorY; + + float cursorX = dstX; + float remW = dstW; + int32_t tileCol = 0; + while (remW > 0.0f) { + bool flipX = (mode == NS_MIRROR) && (tileCol % 2 == 1); + int32_t drawW = (remW < (float) srcW) ? (int32_t) remW : srcW; + int32_t srcLeft = flipX ? (srcX + srcW - drawW) : srcX; + float xs = flipX ? -1.0f : 1.0f; + float drawX = flipX ? (cursorX + (float) drawW) : cursorX; + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcLeft, srcTop, drawW, drawH, drawX, drawY, xs, ys, angleDeg, pivotX, pivotY, color, alpha); + cursorX += (float) drawW; + remW -= (float) drawW; + tileCol++; + } + + cursorY += (float) drawH; + remH -= (float) drawH; + tileRow++; + } +} + +static void Renderer_drawSpriteNineSlice(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float w, float h, bool flipX, bool flipY, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { + DataWin* dw = renderer->dataWin; + if (0 > spriteIndex || dw->sprt.count <= (uint32_t) spriteIndex) return; + Sprite* sprite = &dw->sprt.sprites[spriteIndex]; + + int32_t L = sprite->nsLeft; + int32_t T = sprite->nsTop; + int32_t R = sprite->nsRight; + int32_t B = sprite->nsBottom; + int32_t sw = (int32_t) sprite->width; + int32_t sh = (int32_t) sprite->height; + int32_t srcCW = sw - L - R; + int32_t srcCH = sh - T - B; + + // Degenerate slice (insets meet or overlap, or zero-size sprite): fall through to a plain stretch. + if (0 >= srcCW || 0 >= srcCH || 0 >= sw || 0 >= sh) { + int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); + if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; + TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; + if (tpag->boundingWidth == 0 || tpag->boundingHeight == 0) return; + renderer->vtable->drawSprite(renderer, tpagIndex, x, y, 0.0f, 0.0f, w / (float) tpag->boundingWidth, h / (float) tpag->boundingHeight, 0.0f, color, alpha); + return; + } + + uint8_t modeTop = sprite->nsTileModes[1]; // top edge + uint8_t modeBottom = sprite->nsTileModes[3]; // bottom edge + uint8_t modeLeft = sprite->nsTileModes[0]; // left edge + uint8_t modeRight = sprite->nsTileModes[2]; // right edge + uint8_t modeCenter = sprite->nsTileModes[4]; // center + + // Flip remaps which source corner/edge content appears at which destination position. + // flipX swaps left <-> right; flipY swaps top <-> bottom. + // dstL/dstR are the dest margin widths; srcXLeft/srcXRight are the source x-offsets. + int32_t dstL = flipX ? R : L; + int32_t dstR = flipX ? L : R; + int32_t dstT = flipY ? B : T; + int32_t dstB = flipY ? T : B; + int32_t srcXLeft = flipX ? (sw - R) : 0; + int32_t srcXRight = flipX ? 0 : (sw - R); + int32_t srcYTop = flipY ? (sh - B) : 0; + int32_t srcYBot = flipY ? 0 : (sh - B); + + float dstCW = w - (float) (L + R); + float dstCH = h - (float) (T + B); + float xsCenter = (dstCW > 0) ? dstCW / (float) srcCW : 0.0f; + float ysCenter = (dstCH > 0) ? dstCH / (float) srcCH : 0.0f; + + // Corners: always drawn at native pixel size regardless of tile mode. + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXLeft, srcYTop, dstL, dstT, x, y, 1.0f, 1.0f, angleDeg, pivotX, pivotY, color, alpha); + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXRight, srcYTop, dstR, dstT, x + w - dstR, y, 1.0f, 1.0f, angleDeg, pivotX, pivotY, color, alpha); + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXLeft, srcYBot, dstL, dstB, x, y + h - dstB, 1.0f, 1.0f, angleDeg, pivotX, pivotY, color, alpha); + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXRight, srcYBot, dstR, dstB, x + w - dstR, y + h - dstB, 1.0f, 1.0f, angleDeg, pivotX, pivotY, color, alpha); + + // Top and bottom edges (horizontal variable axis). Source x is always the center strip (L..sw-R). + if (dstCW > 0) { + if (modeTop == NS_STRETCH) { + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, L, srcYTop, srcCW, dstT, x + dstL, y, xsCenter, 1.0f, angleDeg, pivotX, pivotY, color, alpha); + } else if (modeTop == NS_REPEAT || modeTop == NS_MIRROR || modeTop == NS_BLANKREPEAT) { + Renderer_nineSliceTileH(renderer, spriteIndex, subimg, L, srcYTop, srcCW, dstT, x + dstL, y, dstCW, modeTop, angleDeg, pivotX, pivotY, color, alpha); + } // NS_HIDE: draw nothing + + if (modeBottom == NS_STRETCH) { + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, L, srcYBot, srcCW, dstB, x + dstL, y + h - dstB, xsCenter, 1.0f, angleDeg, pivotX, pivotY, color, alpha); + } else if (modeBottom == NS_REPEAT || modeBottom == NS_MIRROR || modeBottom == NS_BLANKREPEAT) { + Renderer_nineSliceTileH(renderer, spriteIndex, subimg, L, srcYBot, srcCW, dstB, x + dstL, y + h - dstB, dstCW, modeBottom, angleDeg, pivotX, pivotY, color, alpha); + } // NS_HIDE: draw nothing + } + + // Left and right edges (vertical variable axis). Source y is always the center strip (T..sh-B). + if (dstCH > 0) { + if (modeLeft == NS_STRETCH) { + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXLeft, T, dstL, srcCH, x, y + dstT, 1.0f, ysCenter, angleDeg, pivotX, pivotY, color, alpha); + } else if (modeLeft == NS_REPEAT || modeLeft == NS_MIRROR || modeLeft == NS_BLANKREPEAT) { + Renderer_nineSliceTileV(renderer, spriteIndex, subimg, srcXLeft, T, dstL, srcCH, x, y + dstT, dstCH, modeLeft, angleDeg, pivotX, pivotY, color, alpha); + } // NS_HIDE: draw nothing + + if (modeRight == NS_STRETCH) { + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXRight, T, dstR, srcCH, x + w - dstR, y + dstT, 1.0f, ysCenter, angleDeg, pivotX, pivotY, color, alpha); + } else if (modeRight == NS_REPEAT || modeRight == NS_MIRROR || modeRight == NS_BLANKREPEAT) { + Renderer_nineSliceTileV(renderer, spriteIndex, subimg, srcXRight, T, dstR, srcCH, x + w - dstR, y + dstT, dstCH, modeRight, angleDeg, pivotX, pivotY, color, alpha); + } // NS_HIDE: draw nothing + } + + // Center. + if (dstCW > 0 && dstCH > 0) { + if (modeCenter == NS_STRETCH) { + Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, L, T, srcCW, srcCH, x + dstL, y + dstT, xsCenter, ysCenter, angleDeg, pivotX, pivotY, color, alpha); + } else if (modeCenter == NS_REPEAT || modeCenter == NS_MIRROR) { + Renderer_nineSliceTile2D(renderer, spriteIndex, subimg, L, T, srcCW, srcCH, x + dstL, y + dstT, dstCW, dstCH, modeCenter, angleDeg, pivotX, pivotY, color, alpha); + } // NS_BLANKREPEAT and NS_HIDE: draw nothing + } +} + +// Resolves a BGND index to its TPAG index. +static int32_t Renderer_resolveBackgroundTPAGIndex(DataWin* dataWin, int32_t bgndIndex) { + if (0 > bgndIndex || (uint32_t) bgndIndex >= dataWin->bgnd.count) return -1; + return dataWin->bgnd.backgrounds[bgndIndex].tpagIndex; +} + +// Resolves a SPRT index to the TPAG index of its first frame. +static int32_t Renderer_resolveSpriteTPAGIndex(DataWin* dataWin, int32_t sprtIndex) { + if (0 > sprtIndex || (uint32_t) sprtIndex >= dataWin->sprt.count) return -1; + Sprite* spr = &dataWin->sprt.sprites[sprtIndex]; + if (spr->textureCount == 0) return -1; + return spr->tpagIndices[0]; +} + +// Resolves a SPRT or BGND index to a TPAG index +static int32_t Renderer_resolveObjectTPAGIndex(DataWin* dataWin, RoomTile *tile) { + if (!tile->useSpriteDefinition) + return Renderer_resolveBackgroundTPAGIndex(dataWin, tile->backgroundDefinition); + else + return Renderer_resolveSpriteTPAGIndex(dataWin, tile->backgroundDefinition); +} + +static bool Renderer_isBattleButtonSpriteName(const char* spriteName) { + if (spriteName == nullptr) return false; + return strstr(spriteName, "fightbt") != nullptr || + strstr(spriteName, "itembt") != nullptr || + strstr(spriteName, "savebt") != nullptr || + strstr(spriteName, "talkbt") != nullptr || + strstr(spriteName, "sparebt") != nullptr || + strstr(spriteName, "mercybutton_normal") != nullptr || + strstr(spriteName, "actbt_center") != nullptr; +} + +// Tiled draws. +// This will use a specialized vtable->drawTiled implementation, but if it doesn't, it will fall back to "manual" tiled rendering. +static void Renderer_drawTiled(Renderer* renderer, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha) { + if (!Renderer_isFiniteFloat(originX) || !Renderer_isFiniteFloat(originY) || + !Renderer_isFiniteFloat(x) || !Renderer_isFiniteFloat(y) || + !Renderer_isFiniteFloat(xscale) || !Renderer_isFiniteFloat(yscale) || + !Renderer_isFiniteFloat(roomW) || !Renderer_isFiniteFloat(roomH) || + !Renderer_isFiniteFloat(alpha)) { + return; + } + if (0 > tpagIndex || (uint32_t) tpagIndex >= renderer->dataWin->tpag.count) return; + + // Use the renderer's fast drawTiled path if it has one + if (renderer->vtable->drawTiled != nullptr) { + renderer->vtable->drawTiled(renderer, tpagIndex, originX, originY, x, y, xscale, yscale, tileX, tileY, roomW, roomH, color, alpha); + return; + } + + TexturePageItem* tpag = &renderer->dataWin->tpag.items[tpagIndex]; + if (tpag->boundingWidth == 0 || tpag->boundingHeight == 0) return; + + float axScale = fabsf(xscale); + float ayScale = fabsf(yscale); + float tileW = (float) tpag->boundingWidth * axScale; + float tileH = (float) tpag->boundingHeight * ayScale; + if (0 >= tileW || 0 >= tileH) return; + + float startX, endX, startY, endY; + if (tileX) { + startX = fmodf(x - originX * axScale, tileW); + if (startX > 0) startX -= tileW; + endX = roomW; + } else { + startX = x - originX * axScale; + endX = startX + tileW; + } + if (tileY) { + startY = fmodf(y - originY * ayScale, tileH); + if (startY > 0) startY -= tileH; + endY = roomH; + } else { + startY = y - originY * ayScale; + endY = startY + tileH; + } + + for (float dy = startY; endY > dy; dy += tileH) { + for (float dx = startX; endX > dx; dx += tileW) { + renderer->vtable->drawSprite(renderer, tpagIndex, dx + originX * axScale, dy + originY * ayScale, originX, originY, xscale, yscale, 0.0f, color, alpha); + } + } +} + +// Draws a tiled background +static void Renderer_drawBackgroundTiled(Renderer* renderer, int32_t tpagIndex, float bgX, float bgY, bool tileX, bool tileY, float roomW, float roomH, float alpha) { + DataWin* dw = renderer->dataWin; + if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; + + Renderer_drawTiled(renderer, tpagIndex, 0.0f, 0.0f, bgX, bgY, 1.0f, 1.0f, tileX, tileY, roomW, roomH, 0xFFFFFFu, alpha); +} + +// Draws a tiled sprite across the room +static void Renderer_drawSpriteTiled(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float xscale, float yscale, float roomW, float roomH, uint32_t color, float alpha) { + DataWin* dw = renderer->dataWin; + int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); + if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; + + Sprite* sprite = &dw->sprt.sprites[spriteIndex]; + float originX = (float) sprite->originX; + float originY = (float) sprite->originY; + + Renderer_drawTiled(renderer, tpagIndex, originX, originY, x, y, xscale, yscale, true, true, roomW, roomH, color, alpha); +} + +// Default draw: draws instance's sprite using its image_* properties +static void Renderer_drawSelf(Renderer* renderer, Instance* instance) { + if (0 > instance->spriteIndex) return; + + DataWin* dw = renderer->dataWin; + if ((uint32_t) instance->spriteIndex >= dw->sprt.count) return; + + Sprite* sprite = &dw->sprt.sprites[instance->spriteIndex]; + int32_t subimg = (int32_t) instance->imageIndex; + int32_t tpagIndex; + if (instance->cachedDrawSpriteIndex == instance->spriteIndex && instance->cachedDrawSubimg == subimg) { + tpagIndex = instance->cachedDrawTPAGIndex; + } else { + tpagIndex = Renderer_resolveTPAGIndex(dw, instance->spriteIndex, subimg); + instance->cachedDrawSpriteIndex = instance->spriteIndex; + instance->cachedDrawSubimg = subimg; + instance->cachedDrawTPAGIndex = tpagIndex; + } + if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; + + float x = (float) instance->x; + float y = (float) instance->y; + float xscale = (float) instance->imageXscale; + float yscale = (float) instance->imageYscale; + float rot = (float) instance->imageAngle; + float alpha = (float) instance->imageAlpha; + uint32_t color = instance->imageBlend; + + if (sprite->nineSliceEnabled && (xscale != 1.0f || yscale != 1.0f)) { + bool flipX = 0.0f > xscale; + bool flipY = 0.0f > yscale; + float absX = fabsf(xscale); + float absY = fabsf(yscale); + float w = (float) sprite->width * absX; + float h = (float) sprite->height * absY; + float tlX = x - (float) sprite->originX * xscale; + float tlY = y - (float) sprite->originY * yscale; + Renderer_drawSpriteNineSlice(renderer, instance->spriteIndex, subimg, tlX, tlY, w, h, flipX, flipY, rot, x, y, color, alpha); + return; + } + + renderer->vtable->drawSprite(renderer, tpagIndex, x, y, (float) sprite->originX, (float) sprite->originY, xscale, yscale, rot, color, alpha); +} + +// Draws a room tile with layer shift offset applied +static void Renderer_drawTile(Renderer* renderer, RoomTile* tile, float offsetX, float offsetY) { + // If the platform has a dedicated tile renderer, use it (PS2 has separate tile atlas entries) + if (renderer->vtable->drawTile != nullptr) { + renderer->vtable->drawTile(renderer, tile, offsetX, offsetY); + return; + } + + int32_t tpagIndex = Renderer_resolveObjectTPAGIndex(renderer->dataWin, tile); + if (0 > tpagIndex) return; + + TexturePageItem* tpag = &renderer->dataWin->tpag.items[tpagIndex]; + int32_t srcX = tile->sourceX; + int32_t srcY = tile->sourceY; + int32_t srcW = (int32_t) tile->width; + int32_t srcH = (int32_t) tile->height; + float drawX = (float) tile->x + offsetX; + float drawY = (float) tile->y + offsetY; + int32_t contentLeft = 0; + int32_t contentTop = 0; + if (contentLeft > srcX) { + int32_t clip = contentLeft - srcX; + drawX += (float) clip * tile->scaleX; + srcW -= clip; + srcX = contentLeft; + } + if (contentTop > srcY) { + int32_t clip = contentTop - srcY; + drawY += (float) clip * tile->scaleY; + srcH -= clip; + srcY = contentTop; + } + + // Clip right/bottom against the background image's content bounds. + int32_t contentRight = (int32_t) tpag->sourceWidth; + int32_t contentBottom = (int32_t) tpag->sourceHeight; + if (srcX + srcW > contentRight) { + srcW = contentRight - srcX; + } + if (srcY + srcH > contentBottom) { + srcH = contentBottom - srcY; + } + + if (0 >= srcW || 0 >= srcH) return; + + uint32_t bgr = tile->color & 0x00FFFFFF; + uint8_t alphaByte = (tile->color >> 24) & 0xFF; + float alpha = (alphaByte == 0) ? 1.0f : (float) alphaByte / 255.0f; + + renderer->vtable->drawSpritePart(renderer, tpagIndex, srcX, srcY, srcW, srcH, drawX, drawY, tile->scaleX, tile->scaleY, 0.0f, 0.0f, 0.0f, bgr, alpha); +} + +// Native runner clamps to [4, 64] and rounds down to the nearest multiple of 4. +static int32_t Renderer_normalizeCirclePrecision(int32_t precision) { + if (4 > precision) precision = 4; + if (precision > 64) precision = 64; + return precision & 0x7C; } -// Full version: draw_sprite_ext(sprite, subimg, x, y, xscale, yscale, rot, color, alpha) -static void Renderer_drawSpriteExt(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float xscale, float yscale, float rot, uint32_t color, float alpha) { - DataWin* dw = renderer->dataWin; - int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); - if (0 > tpagIndex) return; - - Sprite* sprite = &dw->sprt.sprites[spriteIndex]; - - // Nine-slice activates only when the draw scales the sprite away from its native size. At scale 1 there is nothing to slice. - if (sprite->nineSliceEnabled && (xscale != 1.0f || yscale != 1.0f)) { - bool flipX = 0.0f > xscale; - bool flipY = 0.0f > yscale; - float absX = fabsf(xscale); - float absY = fabsf(yscale); - float w = (float) sprite->width * absX; - float h = (float) sprite->height * absY; - float tlX = x - (float) sprite->originX * xscale; // signed: negative xscale shifts tlX right - float tlY = y - (float) sprite->originY * yscale; - Renderer_drawSpriteNineSlice(renderer, spriteIndex, subimg, tlX, tlY, w, h, flipX, flipY, rot, x, y, color, alpha); - return; - } - - renderer->vtable->drawSprite(renderer, tpagIndex, x, y, (float) sprite->originX, (float) sprite->originY, xscale, yscale, rot, color, alpha); +static int32_t Renderer_getAdaptiveCircleSegments(int32_t precision, float radius, bool outline) { + int32_t segments = Renderer_normalizeCirclePrecision(precision); +#if defined(__3DS__) + float absRadius = fabsf(radius); + int32_t radiusCap = 32; + + if (absRadius <= 3.0f) radiusCap = 8; + else if (absRadius <= 6.0f) radiusCap = 12; + else if (absRadius <= 12.0f) radiusCap = 16; + else if (absRadius <= 24.0f) radiusCap = 24; + else if (!outline && absRadius <= 48.0f) radiusCap = 28; + + if (segments > radiusCap) segments = radiusCap; +#else + (void) radius; + (void) outline; +#endif + if (4 > segments) segments = 4; + return segments; } -// Convenience: draw_sprite(sprite, subimg, x, y) -static void Renderer_drawSprite(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y) { - Renderer_drawSpriteExt(renderer, spriteIndex, subimg, x, y, 1.0f, 1.0f, 0.0f, 0xFFFFFF, renderer->drawAlpha); -} - -static void Renderer_drawSpritePos(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float alpha) { - DataWin* dw = renderer->dataWin; - int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); - if (0 > tpagIndex) return; - - renderer->vtable->drawSpritePos(renderer, tpagIndex, x1, y1, x2, y2, x3, y3, x4, y4, alpha); -} - -// Draws part of a sprite with extended parameters (scale, rotation, color, alpha) -static void Renderer_drawSpritePartExt(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t left, int32_t top, int32_t width, int32_t height, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { - DataWin* dw = renderer->dataWin; - int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); - if (0 > tpagIndex) return; - - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - - // Clip region to TPAG bounds (same as Renderer_drawSpritePart) - if (tpag->targetX > left) { - int32_t off = tpag->targetX - left; - x += (float) off * xscale; - width -= off; - left = 0; - } else { - left -= tpag->targetX; - } - - if (tpag->targetY > top) { - int32_t off = tpag->targetY - top; - y += (float) off * yscale; - height -= off; - top = 0; - } else { - top -= tpag->targetY; - } - - if (width > tpag->sourceWidth - left) width = tpag->sourceWidth - left; - if (height > tpag->sourceHeight - top) height = tpag->sourceHeight - top; - if (0 >= width || 0 >= height) return; - - renderer->vtable->drawSpritePart(renderer, tpagIndex, left, top, width, height, x, y, xscale, yscale, angleDeg, pivotX, pivotY, color, alpha); -} - -// Partial draw: draw_sprite_part(sprite, subimg, left, top, width, height, x, y) -static void Renderer_drawSpritePart(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t left, int32_t top, int32_t width, int32_t height, float x, float y) { - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, left, top, width, height, x, y, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0xFFFFFF, renderer->drawAlpha); -} - -// Resolves tpag and converts nine-slice bounding-box coords to tpag source-page space for drawTiledPart. -// Returns false if the resulting region is empty. Adjusts all in/out parameters in place. -// aX/aY/aW/aH: source coords (in, out). adX/adY/adW/adH: dest coords (in, out; pass nullptr for axes that don't change). -static bool Renderer_nineSliceAdjustForTiledPart(DataWin* dw, int32_t spriteIndex, int32_t subimg, int32_t* aX, int32_t* aY, int32_t* aW, int32_t* aH, float* adX, float* adY, float* adW, float* adH, int32_t* tpagIndexOut) { - int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); - if (0 > tpagIndex) return false; - *tpagIndexOut = tpagIndex; - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - if (tpag->targetX > *aX) { int32_t off = tpag->targetX - *aX; if (adX) *adX += (float) off; if (adW) *adW -= (float) off; *aW -= off; *aX = 0; } else { *aX -= tpag->targetX; } - if (tpag->targetY > *aY) { int32_t off = tpag->targetY - *aY; if (adY) *adY += (float) off; if (adH) *adH -= (float) off; *aH -= off; *aY = 0; } else { *aY -= tpag->targetY; } - if (*aW > tpag->sourceWidth - *aX) *aW = tpag->sourceWidth - *aX; - if (*aH > tpag->sourceHeight - *aY) *aH = tpag->sourceHeight - *aY; - return 0 < *aW && 0 < *aH; -} - -// Tiles srcW x srcH pixels from (srcX, srcY) horizontally across dstW pixels starting at (dstX, dstY). -// Mirror mode flips alternate tiles on the horizontal axis. -static void Renderer_nineSliceTileH(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, uint8_t mode, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { - if (mode != NS_MIRROR && angleDeg == 0.0f && renderer->vtable->drawTiledPart != nullptr) { - int32_t tpagIndex, aX = srcX, aY = srcY, aW = srcW, aH = srcH; - float adX = dstX, adY = dstY, adW = dstW; - if (!Renderer_nineSliceAdjustForTiledPart(renderer->dataWin, spriteIndex, subimg, &aX, &aY, &aW, &aH, &adX, &adY, &adW, nullptr, &tpagIndex) || 0.0f >= adW) return; - renderer->vtable->drawTiledPart(renderer, tpagIndex, aX, aY, aW, aH, adX, adY, adW, (float) aH, color, alpha); - return; - } - float cursor = dstX; - float remaining = dstW; - int32_t tileIndex = 0; - while (remaining > 0.0f) { - bool flipped = (mode == NS_MIRROR) && (tileIndex % 2 == 1); - int32_t drawW = ((float) srcW > remaining) ? (int32_t) remaining : srcW; - int32_t srcLeft = flipped ? (srcX + srcW - drawW) : srcX; - float xs = flipped ? -1.0f : 1.0f; - float drawX = flipped ? (cursor + (float) drawW) : cursor; - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcLeft, srcY, drawW, srcH, drawX, dstY, xs, 1.0f, angleDeg, pivotX, pivotY, color, alpha); - cursor += (float) drawW; - remaining -= (float) drawW; - tileIndex++; - } -} - -// Tiles srcW x srcH pixels from (srcX, srcY) vertically across dstH pixels starting at (dstX, dstY). -// Mirror mode flips alternate tiles on the vertical axis. -static void Renderer_nineSliceTileV(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstH, uint8_t mode, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { - if (mode != NS_MIRROR && angleDeg == 0.0f && renderer->vtable->drawTiledPart != nullptr) { - int32_t tpagIndex, aX = srcX, aY = srcY, aW = srcW, aH = srcH; - float adX = dstX, adY = dstY, adH = dstH; - if (!Renderer_nineSliceAdjustForTiledPart(renderer->dataWin, spriteIndex, subimg, &aX, &aY, &aW, &aH, &adX, &adY, nullptr, &adH, &tpagIndex) || 0.0f >= adH) return; - renderer->vtable->drawTiledPart(renderer, tpagIndex, aX, aY, aW, aH, adX, adY, (float) aW, adH, color, alpha); - return; - } - float cursor = dstY; - float remaining = dstH; - int32_t tileIndex = 0; - while (remaining > 0.0f) { - bool flipped = (mode == NS_MIRROR) && (tileIndex % 2 == 1); - int32_t drawH = ((float) srcH > remaining) ? (int32_t) remaining : srcH; - int32_t srcTop = flipped ? (srcY + srcH - drawH) : srcY; - float ys = flipped ? -1.0f : 1.0f; - float drawY = flipped ? (cursor + (float) drawH) : cursor; - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcX, srcTop, srcW, drawH, dstX, drawY, 1.0f, ys, angleDeg, pivotX, pivotY, color, alpha); - cursor += (float) drawH; - remaining -= (float) drawH; - tileIndex++; - } -} - -// Tiles a 2D region across dstW x dstH. Mirror flips alternate tiles on each axis independently. -static void Renderer_nineSliceTile2D(Renderer* renderer, int32_t spriteIndex, int32_t subimg, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, float dstX, float dstY, float dstW, float dstH, uint8_t mode, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { - if (mode != NS_MIRROR && angleDeg == 0.0f && renderer->vtable->drawTiledPart != nullptr) { - int32_t tpagIndex, aX = srcX, aY = srcY, aW = srcW, aH = srcH; - float adX = dstX, adY = dstY, adW = dstW, adH = dstH; - if (!Renderer_nineSliceAdjustForTiledPart(renderer->dataWin, spriteIndex, subimg, &aX, &aY, &aW, &aH, &adX, &adY, &adW, &adH, &tpagIndex) || 0.0f >= adW || 0.0f >= adH) return; - renderer->vtable->drawTiledPart(renderer, tpagIndex, aX, aY, aW, aH, adX, adY, adW, adH, color, alpha); - return; - } - float cursorY = dstY; - float remH = dstH; - int32_t tileRow = 0; - while (remH > 0.0f) { - bool flipY = (mode == NS_MIRROR) && (tileRow % 2 == 1); - int32_t drawH = ((float) srcH > remH) ? (int32_t) remH : srcH; - int32_t srcTop = flipY ? (srcY + srcH - drawH) : srcY; - float ys = flipY ? -1.0f : 1.0f; - float drawY = flipY ? (cursorY + (float) drawH) : cursorY; - - float cursorX = dstX; - float remW = dstW; - int32_t tileCol = 0; - while (remW > 0.0f) { - bool flipX = (mode == NS_MIRROR) && (tileCol % 2 == 1); - int32_t drawW = (remW < (float) srcW) ? (int32_t) remW : srcW; - int32_t srcLeft = flipX ? (srcX + srcW - drawW) : srcX; - float xs = flipX ? -1.0f : 1.0f; - float drawX = flipX ? (cursorX + (float) drawW) : cursorX; - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcLeft, srcTop, drawW, drawH, drawX, drawY, xs, ys, angleDeg, pivotX, pivotY, color, alpha); - cursorX += (float) drawW; - remW -= (float) drawW; - tileCol++; - } - - cursorY += (float) drawH; - remH -= (float) drawH; - tileRow++; - } -} - -static void Renderer_drawSpriteNineSlice(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float w, float h, bool flipX, bool flipY, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { - DataWin* dw = renderer->dataWin; - if (0 > spriteIndex || dw->sprt.count <= (uint32_t) spriteIndex) return; - Sprite* sprite = &dw->sprt.sprites[spriteIndex]; - - int32_t L = sprite->nsLeft; - int32_t T = sprite->nsTop; - int32_t R = sprite->nsRight; - int32_t B = sprite->nsBottom; - int32_t sw = (int32_t) sprite->width; - int32_t sh = (int32_t) sprite->height; - int32_t srcCW = sw - L - R; - int32_t srcCH = sh - T - B; - - // Degenerate slice (insets meet or overlap, or zero-size sprite): fall through to a plain stretch. - if (0 >= srcCW || 0 >= srcCH || 0 >= sw || 0 >= sh) { - int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); - if (0 > tpagIndex) return; - TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; - renderer->vtable->drawSprite(renderer, tpagIndex, x, y, 0.0f, 0.0f, w / (float) tpag->boundingWidth, h / (float) tpag->boundingHeight, 0.0f, color, alpha); - return; - } - - uint8_t modeTop = sprite->nsTileModes[1]; // top edge - uint8_t modeBottom = sprite->nsTileModes[3]; // bottom edge - uint8_t modeLeft = sprite->nsTileModes[0]; // left edge - uint8_t modeRight = sprite->nsTileModes[2]; // right edge - uint8_t modeCenter = sprite->nsTileModes[4]; // center - - // Flip remaps which source corner/edge content appears at which destination position. - // flipX swaps left <-> right; flipY swaps top <-> bottom. - // dstL/dstR are the dest margin widths; srcXLeft/srcXRight are the source x-offsets. - int32_t dstL = flipX ? R : L; - int32_t dstR = flipX ? L : R; - int32_t dstT = flipY ? B : T; - int32_t dstB = flipY ? T : B; - int32_t srcXLeft = flipX ? (sw - R) : 0; - int32_t srcXRight = flipX ? 0 : (sw - R); - int32_t srcYTop = flipY ? (sh - B) : 0; - int32_t srcYBot = flipY ? 0 : (sh - B); - - float dstCW = w - (float) (L + R); - float dstCH = h - (float) (T + B); - float xsCenter = (dstCW > 0) ? dstCW / (float) srcCW : 0.0f; - float ysCenter = (dstCH > 0) ? dstCH / (float) srcCH : 0.0f; - - // Corners: always drawn at native pixel size regardless of tile mode. - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXLeft, srcYTop, dstL, dstT, x, y, 1.0f, 1.0f, angleDeg, pivotX, pivotY, color, alpha); - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXRight, srcYTop, dstR, dstT, x + w - dstR, y, 1.0f, 1.0f, angleDeg, pivotX, pivotY, color, alpha); - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXLeft, srcYBot, dstL, dstB, x, y + h - dstB, 1.0f, 1.0f, angleDeg, pivotX, pivotY, color, alpha); - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXRight, srcYBot, dstR, dstB, x + w - dstR, y + h - dstB, 1.0f, 1.0f, angleDeg, pivotX, pivotY, color, alpha); - - // Top and bottom edges (horizontal variable axis). Source x is always the center strip (L..sw-R). - if (dstCW > 0) { - if (modeTop == NS_STRETCH) { - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, L, srcYTop, srcCW, dstT, x + dstL, y, xsCenter, 1.0f, angleDeg, pivotX, pivotY, color, alpha); - } else if (modeTop == NS_REPEAT || modeTop == NS_MIRROR || modeTop == NS_BLANKREPEAT) { - Renderer_nineSliceTileH(renderer, spriteIndex, subimg, L, srcYTop, srcCW, dstT, x + dstL, y, dstCW, modeTop, angleDeg, pivotX, pivotY, color, alpha); - } // NS_HIDE: draw nothing - - if (modeBottom == NS_STRETCH) { - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, L, srcYBot, srcCW, dstB, x + dstL, y + h - dstB, xsCenter, 1.0f, angleDeg, pivotX, pivotY, color, alpha); - } else if (modeBottom == NS_REPEAT || modeBottom == NS_MIRROR || modeBottom == NS_BLANKREPEAT) { - Renderer_nineSliceTileH(renderer, spriteIndex, subimg, L, srcYBot, srcCW, dstB, x + dstL, y + h - dstB, dstCW, modeBottom, angleDeg, pivotX, pivotY, color, alpha); - } // NS_HIDE: draw nothing - } - - // Left and right edges (vertical variable axis). Source y is always the center strip (T..sh-B). - if (dstCH > 0) { - if (modeLeft == NS_STRETCH) { - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXLeft, T, dstL, srcCH, x, y + dstT, 1.0f, ysCenter, angleDeg, pivotX, pivotY, color, alpha); - } else if (modeLeft == NS_REPEAT || modeLeft == NS_MIRROR || modeLeft == NS_BLANKREPEAT) { - Renderer_nineSliceTileV(renderer, spriteIndex, subimg, srcXLeft, T, dstL, srcCH, x, y + dstT, dstCH, modeLeft, angleDeg, pivotX, pivotY, color, alpha); - } // NS_HIDE: draw nothing - - if (modeRight == NS_STRETCH) { - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, srcXRight, T, dstR, srcCH, x + w - dstR, y + dstT, 1.0f, ysCenter, angleDeg, pivotX, pivotY, color, alpha); - } else if (modeRight == NS_REPEAT || modeRight == NS_MIRROR || modeRight == NS_BLANKREPEAT) { - Renderer_nineSliceTileV(renderer, spriteIndex, subimg, srcXRight, T, dstR, srcCH, x + w - dstR, y + dstT, dstCH, modeRight, angleDeg, pivotX, pivotY, color, alpha); - } // NS_HIDE: draw nothing - } - - // Center. - if (dstCW > 0 && dstCH > 0) { - if (modeCenter == NS_STRETCH) { - Renderer_drawSpritePartExt(renderer, spriteIndex, subimg, L, T, srcCW, srcCH, x + dstL, y + dstT, xsCenter, ysCenter, angleDeg, pivotX, pivotY, color, alpha); - } else if (modeCenter == NS_REPEAT || modeCenter == NS_MIRROR) { - Renderer_nineSliceTile2D(renderer, spriteIndex, subimg, L, T, srcCW, srcCH, x + dstL, y + dstT, dstCW, dstCH, modeCenter, angleDeg, pivotX, pivotY, color, alpha); - } // NS_BLANKREPEAT and NS_HIDE: draw nothing - } -} - -// Resolves a BGND index to its TPAG index. -static int32_t Renderer_resolveBackgroundTPAGIndex(DataWin* dataWin, int32_t bgndIndex) { - if (0 > bgndIndex || (uint32_t) bgndIndex >= dataWin->bgnd.count) return -1; - return dataWin->bgnd.backgrounds[bgndIndex].tpagIndex; -} - -// Resolves a SPRT index to the TPAG index of its first frame. -static int32_t Renderer_resolveSpriteTPAGIndex(DataWin* dataWin, int32_t sprtIndex) { - if (0 > sprtIndex || (uint32_t) sprtIndex >= dataWin->sprt.count) return -1; - Sprite* spr = &dataWin->sprt.sprites[sprtIndex]; - if (spr->textureCount == 0) return -1; - return spr->tpagIndices[0]; -} - -// Resolves a SPRT or BGND index to a TPAG index -static int32_t Renderer_resolveObjectTPAGIndex(DataWin* dataWin, RoomTile *tile) { - if (!tile->useSpriteDefinition) - return Renderer_resolveBackgroundTPAGIndex(dataWin, tile->backgroundDefinition); - else - return Renderer_resolveSpriteTPAGIndex(dataWin, tile->backgroundDefinition); -} - -// Tiled draws. -// This will use a specialized vtable->drawTiled implementation, but if it doesn't, it will fall back to "manual" tiled rendering. -static void Renderer_drawTiled(Renderer* renderer, int32_t tpagIndex, float originX, float originY, float x, float y, float xscale, float yscale, bool tileX, bool tileY, float roomW, float roomH, uint32_t color, float alpha) { - // Use the renderer's fast drawTiled path if it has one - if (renderer->vtable->drawTiled != nullptr) { - renderer->vtable->drawTiled(renderer, tpagIndex, originX, originY, x, y, xscale, yscale, tileX, tileY, roomW, roomH, color, alpha); - return; - } - - TexturePageItem* tpag = &renderer->dataWin->tpag.items[tpagIndex]; - - float axScale = fabsf(xscale); - float ayScale = fabsf(yscale); - float tileW = (float) tpag->boundingWidth * axScale; - float tileH = (float) tpag->boundingHeight * ayScale; - if (0 >= tileW || 0 >= tileH) return; - - float startX, endX, startY, endY; - if (tileX) { - startX = fmodf(x - originX * axScale, tileW); - if (startX > 0) startX -= tileW; - endX = roomW; - } else { - startX = x - originX * axScale; - endX = startX + tileW; - } - if (tileY) { - startY = fmodf(y - originY * ayScale, tileH); - if (startY > 0) startY -= tileH; - endY = roomH; - } else { - startY = y - originY * ayScale; - endY = startY + tileH; - } - - for (float dy = startY; endY > dy; dy += tileH) { - for (float dx = startX; endX > dx; dx += tileW) { - renderer->vtable->drawSprite(renderer, tpagIndex, dx + originX * axScale, dy + originY * ayScale, originX, originY, xscale, yscale, 0.0f, color, alpha); - } - } -} - -// Draws a tiled background -static void Renderer_drawBackgroundTiled(Renderer* renderer, int32_t tpagIndex, float bgX, float bgY, bool tileX, bool tileY, float roomW, float roomH, float alpha) { - DataWin* dw = renderer->dataWin; - if (0 > tpagIndex || (uint32_t) tpagIndex >= dw->tpag.count) return; - - Renderer_drawTiled(renderer, tpagIndex, 0.0f, 0.0f, bgX, bgY, 1.0f, 1.0f, tileX, tileY, roomW, roomH, 0xFFFFFFu, alpha); -} - -// Draws a tiled sprite across the room -static void Renderer_drawSpriteTiled(Renderer* renderer, int32_t spriteIndex, int32_t subimg, float x, float y, float xscale, float yscale, float roomW, float roomH, uint32_t color, float alpha) { - DataWin* dw = renderer->dataWin; - int32_t tpagIndex = Renderer_resolveTPAGIndex(dw, spriteIndex, subimg); - if (0 > tpagIndex) return; - - Sprite* sprite = &dw->sprt.sprites[spriteIndex]; - float originX = (float) sprite->originX; - float originY = (float) sprite->originY; - - Renderer_drawTiled(renderer, tpagIndex, originX, originY, x, y, xscale, yscale, true, true, roomW, roomH, color, alpha); -} - -// Default draw: draws instance's sprite using its image_* properties -static void Renderer_drawSelf(Renderer* renderer, Instance* instance) { - if (0 > instance->spriteIndex) return; - - int32_t subimg = (int32_t) instance->imageIndex; - Renderer_drawSpriteExt( - renderer, - instance->spriteIndex, - subimg, - (float) instance->x, - (float) instance->y, - (float) instance->imageXscale, - (float) instance->imageYscale, - (float) instance->imageAngle, - instance->imageBlend, - (float) instance->imageAlpha - ); -} - -// Draws a room tile with layer shift offset applied -static void Renderer_drawTile(Renderer* renderer, RoomTile* tile, float offsetX, float offsetY) { - // If the platform has a dedicated tile renderer, use it (PS2 has separate tile atlas entries) - if (renderer->vtable->drawTile != nullptr) { - renderer->vtable->drawTile(renderer, tile, offsetX, offsetY); - return; - } - - int32_t tpagIndex = Renderer_resolveObjectTPAGIndex(renderer->dataWin, tile); - if (0 > tpagIndex) return; - - TexturePageItem* tpag = &renderer->dataWin->tpag.items[tpagIndex]; - - // The tile's sourceX/Y are in the background image's coordinate space (bounding rect). - // The TPAG may have been trimmed: actual content starts at (targetX, targetY) within the - // bounding rect and has size sourceWidth x sourceHeight. We must clamp the tile's source - // rect to the TPAG's content area to avoid sampling adjacent atlas textures. - int32_t srcX = tile->sourceX; - int32_t srcY = tile->sourceY; - int32_t srcW = (int32_t) tile->width; - int32_t srcH = (int32_t) tile->height; - float drawX = (float) tile->x + offsetX; - float drawY = (float) tile->y + offsetY; - - // Clip left/top: if tile starts before the content region - int32_t contentLeft = tpag->targetX; - int32_t contentTop = tpag->targetY; - if (contentLeft > srcX) { - int32_t clip = contentLeft - srcX; - drawX += (float) clip * tile->scaleX; - srcW -= clip; - srcX = contentLeft; - } - if (contentTop > srcY) { - int32_t clip = contentTop - srcY; - drawY += (float) clip * tile->scaleY; - srcH -= clip; - srcY = contentTop; - } - - // Clip right/bottom: if tile extends past the content region - int32_t contentRight = tpag->targetX + tpag->sourceWidth; - int32_t contentBottom = tpag->targetY + tpag->sourceHeight; - if (srcX + srcW > contentRight) { - srcW = contentRight - srcX; - } - if (srcY + srcH > contentBottom) { - srcH = contentBottom - srcY; - } - - if (0 >= srcW || 0 >= srcH) return; - - // Convert from bounding-rect coords to atlas-relative coords (subtract targetX/Y) - int32_t atlasOffX = srcX - tpag->targetX; - int32_t atlasOffY = srcY - tpag->targetY; - - // Extract alpha from high byte, default to 1.0 if alpha byte is 0 - uint8_t alphaByte = (tile->color >> 24) & 0xFF; - float alpha = (alphaByte == 0) ? 1.0f : (float) alphaByte / 255.0f; - uint32_t bgr = tile->color & 0x00FFFFFF; - - renderer->vtable->drawSpritePart(renderer, tpagIndex, atlasOffX, atlasOffY, srcW, srcH, drawX, drawY, tile->scaleX, tile->scaleY, 0.0f, 0.0f, 0.0f, bgr, alpha); -} - -// Mixes 2 colors with a blend factor -static uint32_t Renderer_mixColors(uint32_t color1, uint32_t color2, float blending) { - // Extracts the color values out of each color - uint8_t r1 = BGR_R(color1), g1 = BGR_G(color1), b1 = BGR_B(color1); - uint8_t r2 = BGR_R(color2), g2 = BGR_G(color2), b2 = BGR_B(color2); - - // mixes each color together using linear interpolation - uint8_t mixr = (uint8_t)(r1 * (1 - blending) + r2 * blending); - uint8_t mixg = (uint8_t)(g1 * (1 - blending) + g2 * blending); - uint8_t mixb = (uint8_t)(b1 * (1 - blending) + b2 * blending); - - uint32_t resultColor = ((mixr << 0) | (mixg << 8) | (mixb << 16)) & 0x00FFFFFF; - return resultColor; -} +// draw_circle helper: approximates a circle as a polygon with "circlePrecision" segments. +// Filled: triangle fan from center. Outline: line strip around the perimeter. +static void Renderer_drawCircle(Renderer* renderer, float cx, float cy, float radius, bool outline) { + int32_t segments = Renderer_getAdaptiveCircleSegments(renderer->circlePrecision, radius, outline); + + float step = 6.2831853f / (float) segments; + float prevX = cx + radius; + float prevY = cy; + + for (int32_t i = 1; segments >= i; i++) { + float angle = step * (float) i; + float curX = cx + radius * cosf(angle); + float curY = cy + radius * sinf(angle); + + if (outline) { + renderer->vtable->drawLine(renderer, prevX, prevY, curX, curY, 1.0f, renderer->drawColor, renderer->drawAlpha); + } else { + renderer->vtable->drawTriangle(renderer, cx, cy, prevX, prevY, curX, curY, false); + } + + prevX = curX; + prevY = curY; + } +} diff --git a/src/runner.c b/src/runner.c index 90182367..bec0e606 100644 --- a/src/runner.c +++ b/src/runner.c @@ -1,2799 +1,4456 @@ -#include "runner.h" -#include "data_win.h" -#include "instance.h" -#include "renderer.h" -#include "vm.h" -#include "utils.h" -#include "json_writer.h" -#include "collision.h" - -#include -#include -#include -#include -#include - -#include "debug_overlay.h" -#include "stb_ds.h" - -// ===[ Runtime Layer Teardown Helpers ]=== -void Runner_freeRuntimeLayer(RuntimeLayer* runtimeLayer) { - if (runtimeLayer->dynamicName != nullptr) { - free(runtimeLayer->dynamicName); - runtimeLayer->dynamicName = nullptr; - } - size_t elementCount = arrlenu(runtimeLayer->elements); - repeat(elementCount, i) { - RuntimeLayerElement* el = &runtimeLayer->elements[i]; - if (el->backgroundElement != nullptr) { - free(el->backgroundElement); - el->backgroundElement = nullptr; - } - if (el->spriteElement != nullptr) { - free(el->spriteElement); - el->spriteElement = nullptr; - } - } - arrfree(runtimeLayer->elements); - runtimeLayer->elements = nullptr; -} - -static void freeRuntimeLayersArray(RuntimeLayer** runtimeLayerArray) { - size_t count = arrlenu(*runtimeLayerArray); - repeat(count, i) { - Runner_freeRuntimeLayer(&(*runtimeLayerArray)[i]); - } - arrfree(*runtimeLayerArray); - *runtimeLayerArray = nullptr; -} - -// ===[ Helper: Find event action in object hierarchy ]=== -// Resolves the handler for (objectIndex, eventType, eventSubtype) via the precomputed ResolvedEventTable. -// Returns the CODE chunk handler id, or -1 if the object does not respond. -// If outOwnerObjectIndex is non-null, it is set to the resolved owner objectIndex (-1 if not found). -static int32_t findEventCodeIdAndOwner(Runner* runner, int32_t objectIndex, int32_t eventType, int32_t eventSubtype, int32_t* outOwnerObjectIndex) { - int32_t slot = EventSlotMap_lookup(&runner->eventSlotMap, eventType, eventSubtype); - if (0 > slot) { - if (outOwnerObjectIndex != nullptr) *outOwnerObjectIndex = -1; - return -1; - } - return ResolvedEventTable_lookup(&runner->eventTable, objectIndex, slot, outOwnerObjectIndex); -} - -// ===[ Per-Object Instance Lists ]=== -// Each instance lives in the list of its own object and every ancestor object (descendant-inclusive). -// This mirrors the native runner and lets collision dispatch iterate only the candidate instances for a target object, instead of scanning the whole room per collision event. -// The difference is that the native runner uses a linked list, while we move things manually with memmove. - -void Runner_addInstanceToObjectLists(Runner* runner, Instance* inst) { - DataWin* dataWin = runner->dataWin; - int32_t currentObj = inst->objectIndex; - int32_t depth = 0; - while (currentObj >= 0 && dataWin->objt.count > (uint32_t) currentObj && 32 > depth) { - arrput(runner->instancesByObject[currentObj], inst); - currentObj = dataWin->objt.objects[currentObj].parentId; - depth++; - } - if (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) { - arrput(runner->instancesByExactObject[inst->objectIndex], inst); - } - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); -} - -// Stable remove of inst from list, preserving creation order. Returns true if removed. -static bool removeInstanceFromList(Instance*** listPtr, Instance* inst) { - Instance** list = *listPtr; - int32_t n = (int32_t) arrlen(list); - repeat(n, i) { - if (list[i] == inst) { - if (n - 1 > i) memmove(&list[i], &list[i + 1], (size_t) (n - 1 - i) * sizeof(Instance*)); - arrsetlen(*listPtr, n - 1); - return true; - } - } - return false; -} - -void Runner_removeInstanceFromObjectLists(Runner* runner, Instance* inst) { - DataWin* dataWin = runner->dataWin; - int32_t currentObj = inst->objectIndex; - int32_t depth = 0; - while (currentObj >= 0 && dataWin->objt.count > (uint32_t) currentObj && 32 > depth) { - removeInstanceFromList(&runner->instancesByObject[currentObj], inst); - currentObj = dataWin->objt.objects[currentObj].parentId; - depth++; - } - if (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) { - removeInstanceFromList(&runner->instancesByExactObject[inst->objectIndex], inst); - } - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); -} - -void Runner_clearAllObjectLists(Runner* runner) { - if (runner->instancesByObject == nullptr) return; - uint32_t count = runner->dataWin->objt.count; - repeat(count, i) { - arrsetlen(runner->instancesByObject[i], 0); - if (runner->instancesByExactObject != nullptr) { - arrsetlen(runner->instancesByExactObject[i], 0); - } - } -} - -int32_t Runner_pushInstancesOfObject(Runner* runner, int32_t targetObjIndex) { - int32_t base = (int32_t) arrlen(runner->instanceSnapshots); - - if (0 > targetObjIndex || (uint32_t) targetObjIndex >= runner->dataWin->objt.count) - return base; - - Instance** source = runner->instancesByObject[targetObjIndex]; - int32_t sourceCount = (int32_t) arrlen(source); - - if (0 >= sourceCount) - return base; - - arrsetlen(runner->instanceSnapshots, base + sourceCount); - memcpy(&runner->instanceSnapshots[base], source, (size_t) sourceCount * sizeof(Instance*)); - return base; -} - -void Runner_popInstanceSnapshot(Runner* runner, int32_t base) { - arrsetlen(runner->instanceSnapshots, base); -} - -int32_t Runner_pushInstancesForTarget(Runner* runner, int32_t target) { - int32_t base = (int32_t) arrlen(runner->instanceSnapshots); - if (target >= 0 && 100000 > target) { - return Runner_pushInstancesOfObject(runner, target); - } - if (target == INSTANCE_ALL) { - int32_t total = (int32_t) arrlen(runner->instances); - if (0 >= total) - return base; - arrsetlen(runner->instanceSnapshots, base + total); - memcpy(&runner->instanceSnapshots[base], runner->instances, (size_t) total * sizeof(Instance*)); - return base; - } - if (target >= 100000) { - Instance* inst = hmget(runner->instancesById, target); - if (inst != nullptr) arrput(runner->instanceSnapshots, inst); - return base; - } - return base; -} - -// ===[ Event Execution ]=== - -static void setVMInstanceContext(VMContext* vm, Instance* instance) { - vm->currentInstance = instance; -} - -static void restoreVMInstanceContext(VMContext* vm, Instance* savedInstance) { - vm->currentInstance = savedInstance; -} - -static void executeCode(Runner* runner, Instance* instance, int32_t codeId) { - // GameMaker does use codeIds less than 0, we'll just pretend we didn't hear them... - if (0 > codeId) return; - - VMContext* vm = runner->vmContext; - - // Save instance context - Instance* savedInstance = (Instance*) vm->currentInstance; - - // Save full VM execution state, because VM_executeCode overwrites all of these. - // This is necessary for nested execution (e.g., instance_create triggering a Create - // event while another event's executeLoop is still on the call stack). - uint8_t* savedBytecodeBase = vm->bytecodeBase; - uint32_t savedIP = vm->ip; - uint32_t savedCodeEnd = vm->codeEnd; - const char* savedCodeName = vm->currentCodeName; - RValue* savedLocalVars = vm->localVars; - uint32_t savedLocalVarCount = vm->localVarCount; - IntIntHashMap* savedCodeLocalsSlotMap = vm->currentCodeLocalsSlotMap; - int32_t savedCodeIndex = vm->currentCodeIndex; - int32_t savedStackTop = vm->stack.top; - - // Save stack values (VM_executeCode resets stack.top to 0, which would let - // the nested execution overwrite the caller's stack slot values) - RValue* savedStackValues = nullptr; - if (savedStackTop > 0) { - savedStackValues = safeMalloc((uint32_t) savedStackTop * sizeof(RValue)); - memcpy(savedStackValues, vm->stack.slots, (uint32_t) savedStackTop * sizeof(RValue)); - } - - // Set instance context - setVMInstanceContext(vm, instance); - - // Execute - RValue result = VM_executeCode(vm, codeId); - RValue_free(&result); - - // Restore instance context - restoreVMInstanceContext(vm, savedInstance); - - // Restore VM execution state - vm->bytecodeBase = savedBytecodeBase; - vm->ip = savedIP; - vm->codeEnd = savedCodeEnd; - vm->currentCodeName = savedCodeName; - vm->localVars = savedLocalVars; - vm->localVarCount = savedLocalVarCount; - vm->currentCodeLocalsSlotMap = savedCodeLocalsSlotMap; - vm->currentCodeIndex = savedCodeIndex; - vm->stack.top = savedStackTop; - - // Restore stack values - if (savedStackTop > 0) { - memcpy(vm->stack.slots, savedStackValues, (uint32_t) savedStackTop * sizeof(RValue)); - free(savedStackValues); - } -} - -const char* Runner_getEventName(int32_t eventType, int32_t eventSubtype) { - switch (eventType) { - case EVENT_CREATE: return "Create"; - case EVENT_DESTROY: return "Destroy"; - case EVENT_ALARM: return "Alarm"; - case EVENT_COLLISION: return "Collision"; - case EVENT_STEP: - switch (eventSubtype) { - case STEP_BEGIN: return "BeginStep"; - case STEP_NORMAL: return "NormalStep"; - case STEP_END: return "EndStep"; - default: return "Step"; - } - case EVENT_DRAW: - switch (eventSubtype) { - case DRAW_NORMAL: return "Draw"; - case DRAW_GUI: return "DrawGUI"; - case DRAW_BEGIN: return "DrawBegin"; - case DRAW_END: return "DrawEnd"; - case DRAW_GUI_BEGIN: return "DrawGUIBegin"; - case DRAW_GUI_END: return "DrawGUIEnd"; - case DRAW_PRE: return "DrawPre"; - case DRAW_POST: return "DrawPost"; - default: return "Draw"; - } - case EVENT_KEYBOARD: return "Keyboard"; - case EVENT_OTHER: - switch (eventSubtype) { - case OTHER_OUTSIDE_ROOM: return "OutsideRoom"; - case OTHER_GAME_START: return "GameStart"; - case OTHER_ROOM_START: return "RoomStart"; - case OTHER_ROOM_END: return "RoomEnd"; - case OTHER_END_OF_PATH: return "EndOfPath"; - case OTHER_USER0 + 0: return "UserEvent0"; - case OTHER_USER0 + 1: return "UserEvent1"; - case OTHER_USER0 + 2: return "UserEvent2"; - case OTHER_USER0 + 3: return "UserEvent3"; - case OTHER_USER0 + 4: return "UserEvent4"; - case OTHER_USER0 + 5: return "UserEvent5"; - case OTHER_USER0 + 6: return "UserEvent6"; - case OTHER_USER0 + 7: return "UserEvent7"; - case OTHER_USER0 + 8: return "UserEvent8"; - case OTHER_USER0 + 9: return "UserEvent9"; - case OTHER_USER0 + 10: return "UserEvent10"; - case OTHER_USER0 + 11: return "UserEvent11"; - case OTHER_USER0 + 12: return "UserEvent12"; - case OTHER_USER0 + 13: return "UserEvent13"; - case OTHER_USER0 + 14: return "UserEvent14"; - case OTHER_USER0 + 15: return "UserEvent15"; - default: return "Other"; - } - case EVENT_KEYPRESS: return "KeyPress"; - case EVENT_KEYRELEASE: return "KeyRelease"; - case EVENT_PRECREATE: return "PreCreate"; - default: return "Unknown"; - } -} - -// Executes an already-resolved event handler (see findEventCodeIdAndOwner) and verified codeId >= 0. -static void Runner_executeResolvedEvent(Runner* runner, Instance* instance, int32_t eventType, int32_t eventSubtype, int32_t codeId, int32_t ownerObjectIndex) { - VMContext* vm = runner->vmContext; - int32_t savedEventType = vm->currentEventType; - int32_t savedEventSubtype = vm->currentEventSubtype; - int32_t savedEventObjectIndex = vm->currentEventObjectIndex; - - vm->currentEventType = eventType; - vm->currentEventSubtype = eventSubtype; - vm->currentEventObjectIndex = ownerObjectIndex; - -#ifdef ENABLE_VM_TRACING - if (codeId >= 0 && shlen(vm->eventsToBeTraced) != -1) { - const char* eventName = Runner_getEventName(eventType, eventSubtype); - const char* objectName = runner->dataWin->objt.objects[instance->objectIndex].name; - - bool shouldTrace = shgeti(vm->eventsToBeTraced, "*") != -1 || shgeti(vm->eventsToBeTraced, eventName) != -1 || shgeti(vm->eventsToBeTraced, objectName) != -1; - - if (shouldTrace) { - if (eventType == EVENT_ALARM) { - fprintf(stderr, "Runner: [%s] %s %d (instanceId=%d)\n", objectName, eventName, eventSubtype, instance->instanceId); - } else { - fprintf(stderr, "Runner: [%s] %s (instanceId=%d)\n", objectName, eventName, instance->instanceId); - } - } - } -#endif - - executeCode(runner, instance, codeId); - - vm->currentEventType = savedEventType; - vm->currentEventSubtype = savedEventSubtype; - vm->currentEventObjectIndex = savedEventObjectIndex; -} - -void Runner_executeEventFromObject(Runner* runner, Instance* instance, int32_t startObjectIndex, int32_t eventType, int32_t eventSubtype) { - int32_t ownerObjectIndex = -1; - int32_t codeId = findEventCodeIdAndOwner(runner, startObjectIndex, eventType, eventSubtype, &ownerObjectIndex); - // Fast path: If the codeId is invalid, let's bail out fast - // This way can avoid the need of loading and saving the current state variables - if (0 > codeId) - return; - Runner_executeResolvedEvent(runner, instance, eventType, eventSubtype, codeId, ownerObjectIndex); -} - -void Runner_executeEvent(Runner* runner, Instance* instance, int32_t eventType, int32_t eventSubtype) { - Runner_executeEventFromObject(runner, instance, instance->objectIndex, eventType, eventSubtype); -} - -// Events that GMS 2.3+ routes through the per-object obj_has_event table instead of Perform_Event_All. -// Anything else (BC16 ALWAYS; BC17 non-perObject) goes through Runner_executeEventForAll -static bool eventUsesBC17PerObjectDispatch(int32_t eventType) { - return eventType == EVENT_STEP || eventType == EVENT_ALARM || eventType == EVENT_KEYBOARD || eventType == EVENT_KEYPRESS || eventType == EVENT_KEYRELEASE; -} - -void Runner_executeEventForAll(Runner* runner, int32_t eventType, int32_t eventSubtype) { - int32_t slot = EventSlotMap_lookup(&runner->eventSlotMap, eventType, eventSubtype); - if (slot == -1) return; - - // We always snapshot the iteration list before dispatching so instances spawned during this phase do NOT fire the current event. - Instance** scratch = runner->eventDispatchInstances; - arrsetlen(scratch, 0); - - // On GMS 2.x, the native runner dispatches events in the eventUsesPerObjectDispatch set per-object. Route those through executeEventPerObject to match. - if (DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0) && eventUsesBC17PerObjectDispatch(eventType)) { - ResolvedEventTable* table = &runner->eventTable; - uint32_t entryCount; - SlotResponderEntry* entries = ResolvedEventTable_slotEntries(table, slot, &entryCount); - if (entryCount == 0) return; - - repeat(entryCount, i) { - int32_t concreteObj = entries[i].concreteObjectId; - Instance** bucket = runner->instancesByExactObject[concreteObj]; - int32_t bucketCount = (int32_t) arrlen(bucket); - if (bucketCount == 0) continue; - size_t base = arrlenu(scratch); - arrsetlen(scratch, base + (size_t) bucketCount); - memcpy(&scratch[base], bucket, (size_t) bucketCount * sizeof(Instance*)); - } - runner->eventDispatchInstances = scratch; // arrsetlen may have realloced - - int32_t snapshotCount = (int32_t) arrlen(scratch); - repeat(snapshotCount, i) { - Instance* inst = scratch[i]; - if (!inst->active) continue; - Runner_executeEvent(runner, inst, eventType, eventSubtype); - } - return; - } - - int32_t count = (int32_t) arrlen(runner->instances); - if (count == 0) return; - arrsetlen(scratch, count); - memcpy(scratch, runner->instances, (size_t) count * sizeof(Instance*)); - runner->eventDispatchInstances = scratch; - - repeat(count, i) { - Instance* inst = scratch[i]; - if (!inst->active) continue; - // Skip non-responders without entering Runner_executeEvent. ResolvedEventTable_lookup is a tiny CSR scan; non-responders bail in a few compares and avoid the VM state save/restore overhead inside Runner_executeEventFromObject. - int32_t ownerObjectIndex = -1; - int32_t codeId = ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, slot, &ownerObjectIndex); - if (0 > codeId) continue; - Runner_executeResolvedEvent(runner, inst, eventType, eventSubtype, codeId, ownerObjectIndex); - } -} - -// ===[ Background Scrolling & Drawing ]=== - -void Runner_scrollBackgrounds(Runner* runner) { - repeat(8, i) { - RuntimeBackground* bg = &runner->backgrounds[i]; - if (!bg->visible) continue; - bg->x += bg->speedX; - bg->y += bg->speedY; - } -} - -void Runner_drawBackgrounds(Runner* runner, bool foreground) { - if (runner->renderer == nullptr) return; - DataWin* dataWin = runner->dataWin; - float roomW = (float) runner->currentRoom->width; - float roomH = (float) runner->currentRoom->height; - - repeat(8, i) { - RuntimeBackground* bg = &runner->backgrounds[i]; - if (!bg->visible || bg->foreground != foreground) continue; - if (0 > bg->backgroundIndex) continue; - - int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(dataWin, bg->backgroundIndex); - if (0 > tpagIndex) continue; - - if (bg->stretch) { - // Stretch to fill room dimensions - TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; - float xscale = roomW / (float) tpag->boundingWidth; - float yscale = roomH / (float) tpag->boundingHeight; - runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, 0.0f, 0.0f, 0.0f, 0.0f, xscale, yscale, 0.0f, 0xFFFFFF, bg->alpha); - } else if (bg->tileX || bg->tileY) { - Renderer_drawBackgroundTiled(runner->renderer, tpagIndex, bg->x, bg->y, bg->tileX, bg->tileY, roomW, roomH, bg->alpha); - } else { - // Single placement - runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, bg->x, bg->y, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0xFFFFFF, bg->alpha); - } - } -} - -// ===[ Draw ]=== - -static int compareDrawableDepth(const void* a, const void* b) { - const Drawable* da = (const Drawable*) a; - const Drawable* db = (const Drawable*) b; - // Higher depth draws first (behind), lower depth draws last (in front) - if (da->depth > db->depth) return -1; - if (db->depth > da->depth) return 1; - // At same depth, tiles before instances (tiles are background) - if (da->type < db->type) return -1; - if (db->type < da->type) return 1; - // At same depth and type, preserve original room order (higher index draws later = in front) - if (da->type == DRAWABLE_TILE) { - if (db->tileIndex > da->tileIndex) return -1; - if (da->tileIndex > db->tileIndex) return 1; - } - // At same depth, newer instances (higher instanceId) draw FIRST (behind), older draw LAST (front). - if (da->type == DRAWABLE_INSTANCE && db->type == DRAWABLE_INSTANCE) { - if (db->instance->instanceId > da->instance->instanceId) return 1; - if (da->instance->instanceId > db->instance->instanceId) return -1; - } - return 0; -} - -static void fireDrawSubtype(Runner* runner, Drawable* drawables, int32_t drawableCount, int32_t subtype) { - int32_t slot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_DRAW, subtype); - if (slot == -1) return; - - repeat(drawableCount, i) { - Drawable* d = &drawables[i]; - if (d->type != DRAWABLE_INSTANCE) - continue; - - Instance* inst = d->instance; - if (!inst->active || !inst->visible) - continue; - - int32_t ownerObjectIndex = -1; - int32_t codeId = ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, slot, &ownerObjectIndex); - if (0 > codeId) continue; - Runner_executeResolvedEvent(runner, inst, EVENT_DRAW, subtype, codeId, ownerObjectIndex); - } -} - -// GMS2 tilemap cell bit layout (matches HTML5 Function_Layers.js TileIndex/Mirror/Flip/Rotate masks) -#define GMS2_TILE_INDEX_MASK 0x0007FFFF // bits 0..18 -#define GMS2_TILE_MIRROR_MASK 0x10000000 // bit 28 (horizontal flip) -#define GMS2_TILE_FLIP_MASK 0x20000000 // bit 29 (vertical flip) -#define GMS2_TILE_ROTATE_MASK 0x40000000 // bit 30 (90 CW) - -static void Runner_drawTileLayer(Runner* runner, RoomLayerTilesData* data, float layerOffsetX, float layerOffsetY) { - if (data == nullptr || data->tileData == nullptr) return; - if (0 > data->backgroundIndex) return; - - DataWin* dw = runner->dataWin; - if ((uint32_t) data->backgroundIndex >= dw->bgnd.count) return; - - Background* tileset = &dw->bgnd.backgrounds[data->backgroundIndex]; - if (tileset->gms2TileWidth == 0 || tileset->gms2TileHeight == 0 || tileset->gms2TileColumns == 0) return; - - int32_t tpagIndex = tileset->tpagIndex; - if (0 > tpagIndex) return; - - uint32_t tileW = tileset->gms2TileWidth; - uint32_t tileH = tileset->gms2TileHeight; - uint32_t borderX = tileset->gms2OutputBorderX; - uint32_t borderY = tileset->gms2OutputBorderY; - uint32_t columns = tileset->gms2TileColumns; - - static bool rotateWarned = false; - - repeat(data->tilesY, ty) { - repeat(data->tilesX, tx) { - uint32_t cell = data->tileData[ty * data->tilesX + tx]; - uint32_t tileIndex = cell & GMS2_TILE_INDEX_MASK; - if (tileIndex == 0) continue; // 0 = empty - - uint32_t col = tileIndex % columns; - uint32_t row = tileIndex / columns; - int32_t srcX = (int32_t) (col * (tileW + 2 * borderX) + borderX); - int32_t srcY = (int32_t) (row * (tileH + 2 * borderY) + borderY); - - bool mirror = (cell & GMS2_TILE_MIRROR_MASK) != 0; - bool flip = (cell & GMS2_TILE_FLIP_MASK) != 0; - bool rotate = (cell & GMS2_TILE_ROTATE_MASK) != 0; - - if (rotate && !rotateWarned) { - fprintf(stderr, "Runner: WARNING: GMS2 tile layer has rotated tiles; rotation not yet implemented, drawing unrotated\n"); - rotateWarned = true; - } - - float xscale = mirror ? -1.0f : 1.0f; - float yscale = flip ? -1.0f : 1.0f; - - // With negative scale the quad grows in the opposite direction, so shift the - // destination by one tile to keep the origin at the top-left of the cell. - float dstX = (float) (tx * tileW) + layerOffsetX + (mirror ? (float) tileW : 0.0f); - float dstY = (float) (ty * tileH) + layerOffsetY + (flip ? (float) tileH : 0.0f); - - runner->renderer->vtable->drawSpritePart(runner->renderer, tpagIndex, srcX, srcY, (int32_t) tileW, (int32_t) tileH, dstX, dstY, xscale, yscale, 0.0f, 0.0f, 0.0f, 0xFFFFFF, 1.0f); - } - } -} - -// Returns true if "drawables" is already in compareDrawableDepth order. Used by the sort-dirty path to skip qsort when small depth perturbations didn't actually cross any neighbor. -static bool isDrawableArraySorted(Drawable* drawables, int32_t count) { - for (int32_t i = 1; count > i; i++) { - if (compareDrawableDepth(&drawables[i - 1], &drawables[i]) > 0) return false; - } - return true; -} - -// Refreshes each entry's cached .depth from the live instance/runtime-layer pointer. Tile entries never change depth mid-room so they're left alone. -static void refreshDrawableDepths(Drawable* drawables, int32_t count) { - for (int32_t i = 0; count > i; i++) { - Drawable* d = &drawables[i]; - if (d->type == DRAWABLE_INSTANCE) { - d->depth = d->instance->depth; - } else if (d->type == DRAWABLE_LAYER) { - d->depth = d->runtimeLayer->depth; - } - } -} - -// Rebuilds runner->cachedDrawables when invalidated. Two-tier strategy: -// structureDirty - the SET of entries changed (instance/layer create or destroy, room change). Drop the cache and re-add every instance/tile/runtime-layer, then qsort. -// sortDirty only - the entries are the same but .depth values may have shifted. Refresh depths from the live sources and only qsort if the order actually broke. -static void rebuildDrawableCacheIfDirty(Runner* runner) { - if (runner->drawableListStructureDirty) { - arrsetlen(runner->cachedDrawables, 0); - Room* room = runner->currentRoom; - if (room == nullptr) { - runner->drawableListStructureDirty = false; - runner->drawableListSortDirty = false; - return; - } - - int32_t instanceCount = (int32_t) arrlen(runner->instances); - repeat(instanceCount, i) { - Instance* inst = runner->instances[i]; - Drawable d = { .type = DRAWABLE_INSTANCE, .depth = inst->depth, .instance = inst }; - arrput(runner->cachedDrawables, d); - } - - if (!DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0)) { - repeat(room->tileCount, i) { - RoomTile* tile = &room->tiles[i]; - Drawable d = { .type = DRAWABLE_TILE, .depth = tile->tileDepth, .tileIndex = (int32_t) i }; - arrput(runner->cachedDrawables, d); - } - } else { - size_t runtimeLayersCount = arrlenu(runner->runtimeLayers); - repeat(runtimeLayersCount, i) { - RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; - Drawable d = { .type = DRAWABLE_LAYER, .depth = runtimeLayer->depth, .runtimeLayer = runtimeLayer }; - arrput(runner->cachedDrawables, d); - } - } - - int32_t count = (int32_t) arrlen(runner->cachedDrawables); - if (count > 1) { - qsort(runner->cachedDrawables, count, sizeof(Drawable), compareDrawableDepth); - } - runner->drawableListStructureDirty = false; - runner->drawableListSortDirty = false; - return; - } - - if (runner->drawableListSortDirty) { - int32_t count = (int32_t) arrlen(runner->cachedDrawables); - refreshDrawableDepths(runner->cachedDrawables, count); - if (count > 1 && !isDrawableArraySorted(runner->cachedDrawables, count)) { - qsort(runner->cachedDrawables, count, sizeof(Drawable), compareDrawableDepth); - } - runner->drawableListSortDirty = false; - } -} - -void Runner_draw(Runner* runner) { - Room* room = runner->currentRoom; - - rebuildDrawableCacheIfDirty(runner); - int32_t drawableCount = (int32_t) arrlen(runner->cachedDrawables); - Drawable* drawables = runner->cachedDrawables; - - // Draw non-foreground backgrounds (behind everything) - if (!DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0)) - Runner_drawBackgrounds(runner, false); - - // Fire draw subtypes in correct GameMaker order. fireDrawSubtype walks the cache and filters inline. - fireDrawSubtype(runner, drawables, drawableCount, DRAW_PRE); - fireDrawSubtype(runner, drawables, drawableCount, DRAW_BEGIN); - - // Draw interleaved tiles and instances - repeat(drawableCount, i) { - Drawable* d = &drawables[i]; - if (d->type == DRAWABLE_TILE) { - if (runner->renderer != nullptr) { - RoomTile* tile = &room->tiles[d->tileIndex]; - // Skip tiles whose layer was hidden via tile_layer_hide(). Filtered here (not in the cache) so toggling layer visibility doesn't invalidate. - ptrdiff_t layerIdx = hmgeti(runner->tileLayerMap, tile->tileDepth); - if (layerIdx >= 0 && !runner->tileLayerMap[layerIdx].value.visible) continue; - float offsetX = 0.0f, offsetY = 0.0f; - if (layerIdx >= 0) { - offsetX = runner->tileLayerMap[layerIdx].value.offsetX; - offsetY = runner->tileLayerMap[layerIdx].value.offsetY; - } - -#ifdef ENABLE_VM_TRACING - // Trace tile drawing if requested - if (shlen(runner->vmContext->tilesToBeTraced) > 0) { - DataWin* dataWin = runner->dataWin; - const char* bgName = (tile->backgroundDefinition >= 0 && dataWin->bgnd.count > (uint32_t) tile->backgroundDefinition) ? dataWin->bgnd.backgrounds[tile->backgroundDefinition].name : ""; - const char* roomName = room->name; - - bool shouldTrace = shgeti(runner->vmContext->tilesToBeTraced, "*") != -1 || shgeti(runner->vmContext->tilesToBeTraced, bgName) != -1 || shgeti(runner->vmContext->tilesToBeTraced, roomName) != -1; - - if (shouldTrace) { - int32_t tpagIndex = Renderer_resolveObjectTPAGIndex(dataWin, tile); - if (tpagIndex >= 0) { - TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; - fprintf(stderr, "Runner: [%s] Drawing tile #%d bg=%s(%d) tpag(srcX=%d srcY=%d srcW=%d srcH=%d tgtX=%d tgtY=%d bndW=%d bndH=%d page=%d) tile(srcX=%d srcY=%d w=%u h=%u) at pos=(%d,%d) depth=%d\n", roomName, d->tileIndex, bgName, tile->backgroundDefinition, tpag->sourceX, tpag->sourceY, tpag->sourceWidth, tpag->sourceHeight, tpag->targetX, tpag->targetY, tpag->boundingWidth, tpag->boundingHeight, tpag->texturePageId, tile->sourceX, tile->sourceY, tile->width, tile->height, tile->x, tile->y, tile->tileDepth); - - // Warn if tile source rect exceeds TPAG content bounds - if ((uint32_t) (tile->sourceX + tile->width) > (uint32_t) tpag->sourceWidth || (uint32_t) (tile->sourceY + tile->height) > (uint32_t) tpag->sourceHeight) { - fprintf(stderr, "Runner: [%s] WARNING: Tile #%d source rect (%d,%d %ux%u) exceeds TPAG content bounds (%dx%d)\n", roomName, d->tileIndex, tile->sourceX, tile->sourceY, tile->width, tile->height, tpag->sourceWidth, tpag->sourceHeight); - } - } else { - fprintf(stderr, "Runner: [%s] Drawing tile #%d bg=%s(%d) tpag=UNRESOLVED tile(srcX=%d srcY=%d w=%u h=%u) at pos=(%d,%d) depth=%d\n", roomName, d->tileIndex, bgName, tile->backgroundDefinition, tile->sourceX, tile->sourceY, tile->width, tile->height, tile->x, tile->y, tile->tileDepth); - } - } - } -#endif - - Renderer_drawTile(runner->renderer, tile, offsetX, offsetY); - } - } else if (d->type == DRAWABLE_INSTANCE) { - Instance* inst = d->instance; - // Filter inactive/invisible instances at draw time so the cache doesn't need invalidation when those flags toggle. - if (!inst->active || !inst->visible) continue; - int32_t ownerObjectIndex = -1; - int32_t codeId = findEventCodeIdAndOwner(runner, inst->objectIndex, EVENT_DRAW, DRAW_NORMAL, &ownerObjectIndex); - if (codeId >= 0) { - Runner_executeResolvedEvent(runner, inst, EVENT_DRAW, DRAW_NORMAL, codeId, ownerObjectIndex); - } else if (runner->renderer != nullptr) { - Renderer_drawSelf(runner->renderer, inst); - } - } else if (d->type == DRAWABLE_LAYER) - { - RuntimeLayer* runtimeLayer = d->runtimeLayer; - if (runtimeLayer == nullptr || !runtimeLayer->visible) continue; - float layerOffsetX = runtimeLayer->xOffset; - float layerOffsetY = runtimeLayer->yOffset; - - // Dynamic layers created via layer_create have no parsed RoomLayer, render their runtime elements instead (backgrounds, in the future sprites/tilemaps). - if (runtimeLayer->dynamic) { - if (runner->renderer == nullptr) continue; - - DataWin* dataWin = runner->dataWin; - float roomW = (float) runner->currentRoom->width; - float roomH = (float) runner->currentRoom->height; - - size_t elementCount = arrlenu(runtimeLayer->elements); - repeat(elementCount, j) { - RuntimeLayerElement* layerElement = &runtimeLayer->elements[j]; - if (layerElement->type == RuntimeLayerElementType_Background && layerElement->backgroundElement != nullptr) { - RuntimeBackgroundElement* bg = layerElement->backgroundElement; - if (!bg->visible) continue; - int32_t tpagIndex = Renderer_resolveSpriteTPAGIndex(dataWin, bg->spriteIndex); - if (0 > tpagIndex) continue; - if (bg->stretch) { - TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; - float xscale = roomW / (float) tpag->boundingWidth; - float yscale = roomH / (float) tpag->boundingHeight; - runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, 0.0f, 0.0f, 0.0f, 0.0f, xscale, yscale, 0.0f, bg->blend, bg->alpha); - } else if (bg->htiled || bg->vtiled) { - Renderer_drawBackgroundTiled(runner->renderer, tpagIndex, layerOffsetX + bg->xOffset, layerOffsetY + bg->yOffset, bg->htiled, bg->vtiled, roomW, roomH, bg->alpha); - } else { - runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, layerOffsetX + bg->xOffset, layerOffsetY + bg->yOffset, 0.0f, 0.0f, bg->xScale, bg->yScale, 0.0f, bg->blend, bg->alpha); - } - } - } - continue; - } - - // Parsed layer: look up the RoomLayer by ID and render its data-driven content. - RoomLayer* parsedLayer = Runner_findRoomLayerById(runner, (int32_t) runtimeLayer->id); - if (parsedLayer == nullptr) continue; - if (parsedLayer->type == RoomLayerType_Assets) { - RoomLayerAssetsData* data = parsedLayer->assetsData; - repeat(data->legacyTileCount, j) { - if (runner->renderer != nullptr) { - RoomTile* tile = &data->legacyTiles[j]; - // Check if this tile's layer is hidden via tile_layer_hide() - ptrdiff_t layerIdx = hmgeti(runner->tileLayerMap, tile->tileDepth); - if (layerIdx >= 0 && !runner->tileLayerMap[layerIdx].value.visible) continue; - float offsetX = 0.0f, offsetY = 0.0f; - if (layerIdx >= 0) { - offsetX = runner->tileLayerMap[layerIdx].value.offsetX; - offsetY = runner->tileLayerMap[layerIdx].value.offsetY; - } - -#ifdef ENABLE_VM_TRACING - // Trace tile drawing if requested - if (shlen(runner->vmContext->tilesToBeTraced) > 0) { - DataWin* dataWin = runner->dataWin; - const char* bgName = (tile->backgroundDefinition >= 0 && dataWin->bgnd.count > (uint32_t) tile->backgroundDefinition) ? dataWin->bgnd.backgrounds[tile->backgroundDefinition].name : ""; - const char* roomName = room->name; - - bool shouldTrace = shgeti(runner->vmContext->tilesToBeTraced, "*") != -1 || shgeti(runner->vmContext->tilesToBeTraced, bgName) != -1 || shgeti(runner->vmContext->tilesToBeTraced, roomName) != -1; - - if (shouldTrace) { - int32_t tpagIndex = Renderer_resolveObjectTPAGIndex(dataWin, tile); - if (tpagIndex >= 0) { - TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; - fprintf(stderr, "Runner: [%s] Drawing tile #%d bg=%s(%d) tpag(srcX=%d srcY=%d srcW=%d srcH=%d tgtX=%d tgtY=%d bndW=%d bndH=%d page=%d) tile(srcX=%d srcY=%d w=%u h=%u) at pos=(%d,%d) depth=%d\n", roomName, d->tileIndex, bgName, tile->backgroundDefinition, tpag->sourceX, tpag->sourceY, tpag->sourceWidth, tpag->sourceHeight, tpag->targetX, tpag->targetY, tpag->boundingWidth, tpag->boundingHeight, tpag->texturePageId, tile->sourceX, tile->sourceY, tile->width, tile->height, tile->x, tile->y, tile->tileDepth); - - // Warn if tile source rect exceeds TPAG content bounds - if ((uint32_t) (tile->sourceX + tile->width) > (uint32_t) tpag->sourceWidth || (uint32_t) (tile->sourceY + tile->height) > (uint32_t) tpag->sourceHeight) { - fprintf(stderr, "Runner: [%s] WARNING: Tile #%d source rect (%d,%d %ux%u) exceeds TPAG content bounds (%dx%d)\n", roomName, d->tileIndex, tile->sourceX, tile->sourceY, tile->width, tile->height, tpag->sourceWidth, tpag->sourceHeight); - } - } else { - fprintf(stderr, "Runner: [%s] Drawing tile #%d bg=%s(%d) tpag=UNRESOLVED tile(srcX=%d srcY=%d w=%u h=%u) at pos=(%d,%d) depth=%d\n", roomName, d->tileIndex, bgName, tile->backgroundDefinition, tile->sourceX, tile->sourceY, tile->width, tile->height, tile->x, tile->y, tile->tileDepth); - } - } - } -#endif - - Renderer_drawTile(runner->renderer, tile, offsetX, offsetY); - } - } - - // Sprite elements are rendered from the runtime element list (not the parsed data) so that layer_sprite_destroy can remove them at runtime. - size_t elementCount = arrlenu(runtimeLayer->elements); - repeat(elementCount, j) { - if (runner->renderer == nullptr) break; - RuntimeLayerElement* el = &runtimeLayer->elements[j]; - if (el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) continue; - RuntimeSpriteElement* spr = el->spriteElement; - if (0 > spr->spriteIndex) continue; - Renderer_drawSpriteExt( - runner->renderer, spr->spriteIndex, (int32_t) spr->frameIndex, - spr->x, spr->y, spr->scaleX, - spr->scaleY, spr->rotation, spr->color, - 1.0); - } - } else if(parsedLayer->type == RoomLayerType_Background) { - if (runner->renderer == nullptr) return; - DataWin* dataWin = runner->dataWin; - float roomW = (float) runner->currentRoom->width; - float roomH = (float) runner->currentRoom->height; - RoomLayerBackgroundData* data = parsedLayer->backgroundData; - - int32_t tpagIndex = Renderer_resolveSpriteTPAGIndex(dataWin, data->spriteIndex); - if (0 > tpagIndex) continue; - - if (data->stretch) { - // Stretch to fill room dimensions - TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; - float xscale = roomW / (float) tpag->boundingWidth; - float yscale = roomH / (float) tpag->boundingHeight; - runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, 0.0f, 0.0f, 0.0f, 0.0f, xscale, yscale, 0.0f, 0xFFFFFF, 1.0); - } else if (data->hTiled || data->vTiled) { - Renderer_drawBackgroundTiled(runner->renderer, tpagIndex, layerOffsetX, layerOffsetY, data->hTiled, data->vTiled, roomW, roomH, 1.0); - } else { - // Single placement - runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, layerOffsetX, layerOffsetY, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0xFFFFFF, 1.0); - } - } else if(parsedLayer->type == RoomLayerType_Instances) { - // Instance depth is assigned from layers during room init (initRoom). - // Nothing to do here - instances are drawn from the DRAWABLE_INSTANCE path. - } else if(parsedLayer->type == RoomLayerType_Tiles) { - if (runner->renderer == nullptr) continue; - Runner_drawTileLayer(runner, parsedLayer->tilesData, layerOffsetX, layerOffsetY); - } - } - } - - fireDrawSubtype(runner, drawables, drawableCount, DRAW_END); - - // Draw foreground backgrounds (in front of instances, behind GUI) - Runner_drawBackgrounds(runner, true); - - fireDrawSubtype(runner, drawables, drawableCount, DRAW_POST); -} - -void Runner_drawGUI(Runner* runner) { - rebuildDrawableCacheIfDirty(runner); - Drawable* drawables = runner->cachedDrawables; - int32_t drawableCount = (int32_t) arrlen(drawables); - - fireDrawSubtype(runner, drawables, drawableCount, DRAW_GUI_BEGIN); - fireDrawSubtype(runner, drawables, drawableCount, DRAW_GUI); - fireDrawSubtype(runner, drawables, drawableCount, DRAW_GUI_END); -} - -void Runner_computeViewDisplayScale(Runner* runner, int32_t gameW, int32_t gameH, float* outScaleX, float* outScaleY) { - *outScaleX = 1.0f; - *outScaleY = 1.0f; - - Room* activeRoom = runner->currentRoom; - bool viewsEnabled = (activeRoom->flags & 1) != 0; - if (viewsEnabled) { - int32_t minLeft = INT32_MAX, minTop = INT32_MAX; - int32_t maxRight = INT32_MIN, maxBottom = INT32_MIN; - repeat(MAX_VIEWS, vi) { - RuntimeView* view = &runner->views[vi]; - if (!view->enabled) continue; - if (minLeft > view->portX) minLeft = view->portX; - if (minTop > view->portY) minTop = view->portY; - int32_t right = view->portX + view->portWidth; - int32_t bottom = view->portY + view->portHeight; - if (right > maxRight) maxRight = right; - if (bottom > maxBottom) maxBottom = bottom; - } - if (maxRight > minLeft && maxBottom > minTop) { - *outScaleX = (float) gameW / (float) (maxRight - minLeft); - *outScaleY = (float) gameH / (float) (maxBottom - minTop); - } - } -} - -void Runner_drawViews(Runner* runner, int32_t gameW, int32_t gameH, float displayScaleX, float displayScaleY, bool debugShowCollisionMasks) { - Renderer* renderer = runner->renderer; - Room* activeRoom = runner->currentRoom; - bool anyViewRendered = false; - - bool viewsEnabled = (activeRoom->flags & 1) != 0; - - if (viewsEnabled) { - repeat(MAX_VIEWS, vi) { - RuntimeView* view = &runner->views[vi]; - if (!view->enabled) continue; - - int32_t viewX = view->viewX; - int32_t viewY = view->viewY; - int32_t viewW = view->viewWidth; - int32_t viewH = view->viewHeight; - int32_t portX = (int32_t) ((float) view->portX * displayScaleX + 0.5f); - int32_t portY = (int32_t) ((float) view->portY * displayScaleY + 0.5f); - int32_t portW = (int32_t) ((float) view->portWidth * displayScaleX + 0.5f); - int32_t portH = (int32_t) ((float) view->portHeight * displayScaleY + 0.5f); - float viewAngle = view->viewAngle; - - runner->viewCurrent = (int32_t) vi; - renderer->vtable->beginView(renderer, viewX, viewY, viewW, viewH, portX, portY, portW, portH, viewAngle); - - Runner_draw(runner); - - if (debugShowCollisionMasks) DebugOverlay_drawCollisionMasks(runner); - - renderer->vtable->endView(renderer); - - int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : portW; - int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : portH; - renderer->vtable->beginGUI(renderer, guiW, guiH, portX, portY, portW, portH); - Runner_drawGUI(runner); - renderer->vtable->endGUI(renderer); - - anyViewRendered = true; - } - } - - if (!anyViewRendered) { - // No views enabled: render with default full-screen view - runner->viewCurrent = 0; - renderer->vtable->beginView(renderer, 0, 0, gameW, gameH, 0, 0, gameW, gameH, 0.0f); - Runner_draw(runner); - - if (debugShowCollisionMasks) DebugOverlay_drawCollisionMasks(runner); - - renderer->vtable->endView(renderer); - - int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : gameW; - int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : gameH; - renderer->vtable->beginGUI(renderer, guiW, guiH, 0, 0, gameW, gameH); - Runner_drawGUI(runner); - renderer->vtable->endGUI(renderer); - } - - // Reset view_current to 0 so non-Draw events (Step, Alarm, Create) see view_current = 0 - runner->viewCurrent = 0; -} - -// ===[ Instance Creation Helper ]=== - -static bool isObjectDisabled(Runner* runner, int32_t objectIndex) { - if (runner->disabledObjects == nullptr) return false; - const char* name = runner->dataWin->objt.objects[objectIndex].name; - return shgeti(runner->disabledObjects, name) != -1; -} - -static Instance* createAndInitInstance(Runner* runner, int32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y) { - DataWin* dataWin = runner->dataWin; - require(objectIndex >= 0 && dataWin->objt.count > (uint32_t) objectIndex); - - GameObject* objDef = &dataWin->objt.objects[objectIndex]; - - Instance* inst = Instance_create(instanceId, objectIndex, x, y); - - // Copy properties from object definition - inst->spriteIndex = objDef->spriteId; - inst->visible = objDef->visible; - inst->solid = objDef->solid; - inst->persistent = objDef->persistent; - inst->depth = objDef->depth; - inst->maskIndex = objDef->textureMaskId; - - hmput(runner->instancesById, instanceId, inst); - arrput(runner->instances, inst); - Runner_addInstanceToObjectLists(runner, inst); - runner->drawableListStructureDirty = true; - -#ifdef ENABLE_VM_TRACING - if (shgeti(runner->vmContext->instanceLifecyclesToBeTraced, "*") != -1 || shgeti(runner->vmContext->instanceLifecyclesToBeTraced, objDef->name) != -1) { - fprintf(stderr, "VM: Instance %s (instanceId=%d,objectIndex=%d) created at (%f, %f)\n", objDef->name, instanceId, inst->objectIndex, x, y); - } -#endif - - return inst; -} - -// ===[ Room Management ]=== - -// Collect persistent instances from the previous room (they travel with the player), and free the rest. -// You should re-append them at the tail AFTER creating the new room's own instances, so the iteration order matches the native runner: room-local instances first, persistent arrivals last. -static Instance** takePersistentInstances(Runner* runner) { - Instance** carriedPersistent = nullptr; - int32_t oldCount = (int32_t) arrlen(runner->instances); - repeat(oldCount, i) { - Instance* inst = runner->instances[i]; - if (inst->persistent) { -#ifdef ENABLE_VM_TRACING - GameObject* gameObject = &runner->dataWin->objt.objects[inst->objectIndex]; - if (shgeti(runner->vmContext->instanceLifecyclesToBeTraced, "*") != -1 || shgeti(runner->vmContext->instanceLifecyclesToBeTraced, gameObject->name) != -1) { - fprintf(stderr, "VM: Instance %s (instanceId=%d,objectIndex=%d) has been persisted at (%f, %f) due to room change\n", gameObject->name, inst->instanceId, inst->objectIndex, inst->x, inst->y); - } -#endif - - arrput(carriedPersistent, inst); - } else { -#ifdef ENABLE_VM_TRACING - GameObject* gameObject = &runner->dataWin->objt.objects[inst->objectIndex]; - if (shgeti(runner->vmContext->instanceLifecyclesToBeTraced, "*") != -1 || shgeti(runner->vmContext->instanceLifecyclesToBeTraced, gameObject->name) != -1) { - fprintf(stderr, "VM: Instance %s (instanceId=%d,objectIndex=%d) destroyed at (%f, %f) due to room change\n", gameObject->name, inst->instanceId, inst->objectIndex, inst->x, inst->y); - } -#endif - - hmdel(runner->instancesById, inst->instanceId); - Instance_free(inst); - } - } - - arrfree(runner->instances); - runner->instances = nullptr; - - // The per-object lists referenced both the freed non-persistents and the carried persistents; clear them entirely. - // Persistents are re-added when they return via returnPersistentInstances, and room-local instances are added as they get created. - Runner_clearAllObjectLists(runner); - - return carriedPersistent; -} - -// Append the carried-over persistent instances at the tail of runner->instances and free the temporary array. Pairs with takePersistentInstances. -static void returnPersistentInstances(Runner* runner, Instance** carriedPersistent) { - repeat(arrlen(carriedPersistent), i) { - arrput(runner->instances, carriedPersistent[i]); - Runner_addInstanceToObjectLists(runner, carriedPersistent[i]); - } - arrfree(carriedPersistent); -} - -static void copyRoomViewToRuntimeView(RoomView* roomView, RuntimeView* runtimeView) { - runtimeView->enabled = roomView->enabled; - runtimeView->viewX = roomView->viewX; - runtimeView->viewY = roomView->viewY; - runtimeView->viewWidth = roomView->viewWidth; - runtimeView->viewHeight = roomView->viewHeight; - runtimeView->portX = roomView->portX; - runtimeView->portY = roomView->portY; - runtimeView->portWidth = roomView->portWidth; - runtimeView->portHeight = roomView->portHeight; - runtimeView->borderX = roomView->borderX; - runtimeView->borderY = roomView->borderY; - runtimeView->speedX = roomView->speedX; - runtimeView->speedY = roomView->speedY; - runtimeView->objectId = roomView->objectId; - runtimeView->viewAngle = 0; -} - -static void initRoom(Runner* runner, int32_t roomIndex) { - DataWin* dataWin = runner->dataWin; - require(roomIndex >= 0 && dataWin->room.count > (uint32_t) roomIndex); - - Room* room = &dataWin->room.rooms[roomIndex]; - - // Lazy-room load: if the payload wasn't loaded, read it from the data.win file now before anything touches the room's game objects/tiles/layers. - if (!room->payloadLoaded) { - DataWin_loadRoomPayload(dataWin, roomIndex); - } - - SavedRoomState* savedState = &runner->savedRoomStates[roomIndex]; - - runner->currentRoom = room; - runner->currentRoomIndex = roomIndex; - // Tile set, runtime layers, and instance list all change when entering a room. - runner->drawableListStructureDirty = true; - // It could be the first time we are initializing the grid - if (runner->spatialGrid != nullptr) - SpatialGrid_free(runner->spatialGrid); - runner->spatialGrid = SpatialGrid_create(room->width, room->height); - - // Find position in room order - runner->currentRoomOrderPosition = -1; - repeat(dataWin->gen8.roomOrderCount, i) { - if (dataWin->gen8.roomOrder[i] == roomIndex) { - runner->currentRoomOrderPosition = (int32_t) i; - break; - } - } - - // If this is a persistent room that was previously visited, restore saved state - if (room->persistent && savedState->initialized) { - memcpy(runner->views, savedState->views, sizeof(runner->views)); - - // Restore backgrounds from saved state - memcpy(runner->backgrounds, savedState->backgrounds, sizeof(runner->backgrounds)); - runner->backgroundColor = savedState->backgroundColor; - runner->drawBackgroundColor = savedState->drawBackgroundColor; - - // Restore tile layer map - hmfree(runner->tileLayerMap); - runner->tileLayerMap = savedState->tileLayerMap; - savedState->tileLayerMap = nullptr; - - // Restore runtime layers - freeRuntimeLayersArray(&runner->runtimeLayers); - runner->runtimeLayers = savedState->runtimeLayers; - savedState->runtimeLayers = nullptr; - - Instance** carriedPersistent = takePersistentInstances(runner); - - // The native runner restores the room's own linked list first, then appends persistent arrivals at the tail. - // Event iteration is forward (oldest first), so a persistent instance runs after the room's own instances. - int32_t savedCount = (int32_t) arrlen(savedState->instances); - repeat(savedCount, i) { - arrput(runner->instances, savedState->instances[i]); - Runner_addInstanceToObjectLists(runner, savedState->instances[i]); - } - arrfree(savedState->instances); - savedState->instances = nullptr; - - returnPersistentInstances(runner, carriedPersistent); - - // No Create events, no preCreateCode, no creationCode, no room creation code - fprintf(stderr, "Runner: Room restored (persistent): %s (room %d) with %d instances\n", room->name, roomIndex, (int) arrlen(runner->instances)); - return; - } - - // === Normal room initialization (first visit, or non-persistent room) === - - // Initialize the views from scratch - repeat(MAX_VIEWS, vi) { - copyRoomViewToRuntimeView(&room->views[vi], &runner->views[vi]); - } - - // Reset tile layer state for the new room - hmfree(runner->tileLayerMap); - runner->tileLayerMap = nullptr; - - // Populate runtime layers from parsed room layers (GMS2+ only; empty for GMS1.x). - // Dynamic layers created via layer_create are appended to this array later. - freeRuntimeLayersArray(&runner->runtimeLayers); - uint32_t maxLayerId = 0; - repeat(room->layerCount, i) { - RoomLayer* layerSource = &room->layers[i]; - RuntimeLayer runtimeLayer = { - .id = layerSource->id, - .depth = layerSource->depth, - .visible = layerSource->visible, - .xOffset = layerSource->xOffset, - .yOffset = layerSource->yOffset, - .hSpeed = layerSource->hSpeed, - .vSpeed = layerSource->vSpeed, - .dynamic = false, - .dynamicName = nullptr, - .elements = nullptr, - }; - arrput(runner->runtimeLayers, runtimeLayer); - if (layerSource->id > maxLayerId) maxLayerId = layerSource->id; - } - // Watermark: ensure runtime-allocated IDs (layers + elements) stay above parsed IDs. - if (maxLayerId >= runner->nextLayerId) runner->nextLayerId = maxLayerId + 1; - - // Populate runtime sprite elements for Assets layers, so they can be queried and destroyed via layer_sprite_get_sprite/layer_sprite_destroy - repeat(room->layerCount, i) { - RoomLayer* layerSource = &room->layers[i]; - if (layerSource->type != RoomLayerType_Assets || layerSource->assetsData == nullptr) continue; - RoomLayerAssetsData* assets = layerSource->assetsData; - RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; - repeat(assets->spriteCount, j) { - SpriteInstance* src = &assets->sprites[j]; - RuntimeSpriteElement* spriteElement = safeMalloc(sizeof(RuntimeSpriteElement)); - spriteElement->spriteIndex = src->spriteIndex; - spriteElement->x = src->x; - spriteElement->y = src->y; - spriteElement->scaleX = src->scaleX; - spriteElement->scaleY = src->scaleY; - spriteElement->color = src->color; - spriteElement->animationSpeed = src->animationSpeed; - spriteElement->animationSpeedType = src->animationSpeedType; - spriteElement->frameIndex = src->frameIndex; - spriteElement->rotation = src->rotation; - RuntimeLayerElement el = { - .id = Runner_getNextLayerId(runner), - .type = RuntimeLayerElementType_Sprite, - .backgroundElement = nullptr, - .spriteElement = spriteElement, - }; - arrput(runtimeLayer->elements, el); - } - } - - // Copy room background definitions into mutable runtime state - runner->backgroundColor = room->backgroundColor; - runner->drawBackgroundColor = room->drawBackgroundColor; - repeat(8, i) { - RoomBackground* src = &room->backgrounds[i]; - RuntimeBackground* dst = &runner->backgrounds[i]; - dst->visible = src->enabled; - dst->foreground = src->foreground; - dst->backgroundIndex = src->backgroundDefinition; - dst->x = (float) src->x; - dst->y = (float) src->y; - dst->tileX = (bool) src->tileX; - dst->tileY = (bool) src->tileY; - dst->speedX = (float) src->speedX; - dst->speedY = (float) src->speedY; - dst->stretch = src->stretch; - dst->alpha = 1.0f; - } - - Instance** carriedPersistent = takePersistentInstances(runner); - - // Two-pass instance creation (matches HTML5 runner behavior): - // Pass 1: Create all instance objects so they exist for cross-references - // Pass 2: Fire preCreateCode, CREATE events, and creationCode - // This ensures that when an instance's Create event reads another instance - // (e.g. obj_mainchara reading obj_markerA.x), the target already exists. - - // Pass 1: Create all instances without firing events - repeat(room->gameObjectCount, i) { - RoomGameObject* roomObj = &room->gameObjects[i]; - - // Skip if a persistent instance carried over from the previous room already owns this ID (re-entering the persistent instance's home room, don't create a duplicate!). - if (hmget(runner->instancesById, roomObj->instanceID) != nullptr) continue; - if (isObjectDisabled(runner, roomObj->objectDefinition)) continue; - - Instance* inst = createAndInitInstance(runner, roomObj->instanceID, roomObj->objectDefinition, (GMLReal) roomObj->x, (GMLReal) roomObj->y); - inst->imageXscale = (float) roomObj->scaleX; - inst->imageYscale = (float) roomObj->scaleY; - inst->imageAngle = (float) roomObj->rotation; - inst->imageSpeed = roomObj->imageSpeed; - inst->imageIndex = (float) roomObj->imageIndex; - } - - // In GMS2, instances get their depth from their room layer, not the object definition. - // This must happen before firing Create events so scripts like scr_depth() read the layer depth. - if (DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0)) { - repeat(room->layerCount, li) { - RoomLayer* layer = &room->layers[li]; - if (layer->type != RoomLayerType_Instances || layer->instancesData == nullptr) continue; - RoomLayerInstancesData* layerData = layer->instancesData; - repeat(layerData->instanceCount, ii) { - Instance* inst = hmget(runner->instancesById, layerData->instanceIds[ii]); - if (inst != nullptr) { - inst->depth = layer->depth; - inst->layer = (int32_t) layer->id; - } - } - } - } - - // Append persistent instances carried over from the previous room at the tail, so forward event iteration processes the new room's own instances first and the travelers last. - // We NEED to do this here BEFORE firing the room object's events, to avoid code that relies on persistent instances failing (example: if a object uses instance_number to get the number of instances in the room). - returnPersistentInstances(runner, carriedPersistent); - - // Pass 2: Fire events for newly created instances (in room definition order) - repeat(room->gameObjectCount, i) { - RoomGameObject* roomObj = &room->gameObjects[i]; - - Instance* inst = hmget(runner->instancesById, roomObj->instanceID); - if (inst == nullptr) continue; - - // Skip instances that already had their Create event fired (persistent carry-overs - // that hmget also matches, since instancesById still holds them). - if (inst->createEventFired) continue; - inst->createEventFired = true; - - Runner_executeEvent(runner, inst, EVENT_PRECREATE, 0); - executeCode(runner, inst, roomObj->preCreateCode); - Runner_executeEvent(runner, inst, EVENT_CREATE, 0); - executeCode(runner, inst, roomObj->creationCode); - } - - // Run room creation code - if (room->creationCodeId >= 0 && dataWin->code.count > (uint32_t) room->creationCodeId) { - // Room creation code runs in global context, the native runner creates a fake/dummy instance for the "self" - Instance* dummy = Instance_create(0, -1, 0, 0); - runner->vmContext->currentInstance = dummy; - RValue result = VM_executeCode(runner->vmContext, room->creationCodeId); - RValue_free(&result); - runner->vmContext->currentInstance = nullptr; - Instance_free(dummy); - } - - // Mark this room as initialized for persistent room support - savedState->initialized = true; - - fprintf(stderr, "Runner: Room loaded: %s (room %d) with %d instances\n", room->name, roomIndex, (int) arrlen(runner->instances)); -} - -// Cleans up the runner state, used when freeing the Runner or when restarting the Runner -static void cleanupState(Runner* runner) { - // Drop VM-side RValue holders (globals, stack, call frames) BEFORE freeing any Instance memory. This way any RVALUE_STRUCT refs decrement against still-live struct memory; otherwise we'd free a struct here and then have VM_free's later VM_reset try to decRef a dangling pointer. - if (runner->vmContext != nullptr) { - VM_reset(runner->vmContext); - } - - // Free all instances - repeat(arrlen(runner->instances), i) { - hmdel(runner->instancesById, runner->instances[i]->instanceId); - Instance_free(runner->instances[i]); - } - arrfree(runner->instances); - runner->instances = nullptr; - - // Empty the per-object lists. We keep the outer instancesByObject array allocated so Runner_reset can be reused; Runner_free releases it. - Runner_clearAllObjectLists(runner); - - // Free saved room states - if (runner->savedRoomStates != nullptr) { - repeat(runner->dataWin->room.count, i) { - SavedRoomState* state = &runner->savedRoomStates[i]; - int32_t savedCount = (int32_t) arrlen(state->instances); - repeat(savedCount, j) { - hmdel(runner->instancesById, state->instances[j]->instanceId); - Instance_free(state->instances[j]); - } - arrfree(state->instances); - hmfree(state->tileLayerMap); - freeRuntimeLayersArray(&state->runtimeLayers); - } - free(runner->savedRoomStates); - } - runner->savedRoomStates = nullptr; - - // Free struct instances (created via @@NewGMLObject@@). Anything still here at shutdown is leaked refs or a reference cycle - bulk free regardless of refCount. - repeat(arrlen(runner->structInstances), i) { - Instance* s = runner->structInstances[i]; - hmdel(runner->instancesById, s->instanceId); - s->structRegistryIndex = -1; - Instance_free(s); - } - arrfree(runner->structInstances); - runner->structInstances = nullptr; - - hmfree(runner->instancesById); - runner->instancesById = nullptr; - hmfree(runner->tileLayerMap); - runner->tileLayerMap = nullptr; - freeRuntimeLayersArray(&runner->runtimeLayers); - shfree(runner->disabledObjects); - runner->disabledObjects = nullptr; - - // Free ds_map pool - repeat((int32_t) arrlen(runner->dsMapPool), i) { - DsMapEntry* map = runner->dsMapPool[i]; - if (map != nullptr) { - repeat(shlen(map), j) { - free(map[j].key); - RValue_free(&map[j].value); - } - shfree(map); - } - } - arrfree(runner->dsMapPool); - runner->dsMapPool = nullptr; - - // Free ds_list pool - repeat((int32_t) arrlen(runner->dsListPool), i) { - DsList* list = &runner->dsListPool[i]; - repeat(arrlen(list->items), j) { - RValue_free(&list->items[j]); - } - arrfree(list->items); - } - arrfree(runner->dsListPool); - runner->dsListPool = nullptr; - - // Free mp_grid pool - repeat((int32_t) arrlen(runner->mpGridPool), i) { - free(runner->mpGridPool[i].cells); - } - arrfree(runner->mpGridPool); - runner->mpGridPool = nullptr; - - // Free INI state - if (runner->currentIni != nullptr) { - Ini_free(runner->currentIni); - runner->currentIni = nullptr; - } - free(runner->currentIniPath); - runner->currentIniPath = nullptr; - if (runner->cachedIni != nullptr) { - Ini_free(runner->cachedIni); - runner->cachedIni = nullptr; - } - free(runner->cachedIniPath); - runner->cachedIniPath = nullptr; - - // Free open text files - repeat(MAX_OPEN_TEXT_FILES, i) { - OpenTextFile* file = &runner->openTextFiles[i]; - if (file->isOpen) { - free(file->content); - free(file->writeBuffer); - free(file->filePath); - *file = (OpenTextFile) {0}; - } - } - - if (runner->spatialGrid != nullptr) { - SpatialGrid_free(runner->spatialGrid); - runner->spatialGrid = nullptr; - } -} - -// ===[ Public API ]=== - -void Runner_reset(Runner* runner) { - // This actually sets the default runner values, used for initialization and restarting - cleanupState(runner); - - // Reset VM state - VM_reset(runner->vmContext); - - runner->pendingRoom = -1; - runner->asyncLoadMapId = -1; - runner->gameStartFired = false; - runner->currentRoomIndex = -1; - runner->currentRoomOrderPosition = -1; - runner->nextInstanceId = runner->dataWin->gen8.lastObj + 1; - runner->savedRoomStates = safeCalloc(runner->dataWin->room.count, sizeof(SavedRoomState)); - runner->nextLayerId = 1; - runner->audioSystem->vtable->stopAll(runner->audioSystem); - - // Allocate the per-object instance list array once. - // We don't need to reinitialize the list because the objt.count is fixed for this data.win. - if (runner->instancesByObject == nullptr) { - runner->instancesByObject = safeCalloc(runner->dataWin->objt.count, sizeof(Instance**)); - } - if (runner->instancesByExactObject == nullptr) { - runner->instancesByExactObject = safeCalloc(runner->dataWin->objt.count, sizeof(Instance**)); - } - - // Create the instance used for "self" in GLOB scripts - Instance_free(runner->globalScopeInstance); - runner->globalScopeInstance = Instance_create(0, -1, 0, 0); - - // Reset builtin function state - runner->mpPotMaxrot = 30.0; - runner->mpPotStep = 10.0; - runner->mpPotAhead = 3.0; - runner->mpPotOnSpot = true; - runner->lastMusicInstance = -1; - - arrsetlen(runner->cachedDrawables, 0); - runner->drawableListStructureDirty = true; - runner->drawableListSortDirty = false; -} - -// Populates objectsWithAnyEventOfType[eventType] from the resolved event table: for each event type, the deduplicated list of concrete object indices that respond to ANY subtype of that event. Walks the inverted bySlot index per slot and dedups via a scratch byte set. -// Used by collision dispatch to skip non-collision objects in the outer loop, mirroring how the native obj_has_event table partitions instance iteration by event class. -static void populateObjectsWithAnyEventOfType(Runner* runner) { - int32_t objectCount = (int32_t) runner->dataWin->objt.count; - runner->objectsWithAnyEventOfType = safeCalloc(OBJT_EVENT_TYPE_COUNT, sizeof(int32_t*)); - if (objectCount == 0) return; - - uint8_t* seen = safeCalloc((size_t) objectCount, 1); - - repeat(OBJT_EVENT_TYPE_COUNT, t) { - int16_t* dense = runner->eventSlotMap.denseLookup[t]; - if (dense == nullptr) continue; - int32_t maxSub = runner->eventSlotMap.maxSubtypeByType[t]; - memset(seen, 0, (size_t) objectCount); - - for (int32_t sub = 0; maxSub >= sub; sub++) { - int32_t slot = dense[sub]; - if (0 > slot) continue; - uint32_t entryCount; - SlotResponderEntry* entries = ResolvedEventTable_slotEntries(&runner->eventTable, slot, &entryCount); - repeat(entryCount, i) { - int32_t obj = entries[i].concreteObjectId; - if (obj < 0 || obj >= objectCount) continue; - if (seen[obj]) continue; - seen[obj] = 1; - arrput(runner->objectsWithAnyEventOfType[t], obj); - } - } - } - - free(seen); -} - -Runner* Runner_create(DataWin* dataWin, VMContext* vm, Renderer* renderer, FileSystem* fileSystem, AudioSystem* audioSystem) { - requireNotNull(dataWin); - requireNotNull(vm); - requireNotNull(renderer); - requireNotNull(fileSystem); - requireNotNull(audioSystem); - - Runner* runner = safeCalloc(1, sizeof(Runner)); - runner->dataWin = dataWin; - runner->vmContext = vm; - runner->renderer = renderer; - runner->fileSystem = fileSystem; - runner->audioSystem = audioSystem; - runner->frameCount = 0; - runner->osType = OS_WINDOWS; - runner->keyboard = RunnerKeyboard_create(); - runner->gamepads = RunnerGamepad_create(); - - // Collision compatibility mode is "enabled" for all pre-GM 2022.1 games AND for any post-GM 2022.1 games that have the bit 27 set - runner->collisionCompatibilityMode = (dataWin->detectedFormat.major == 1) || (((dataWin->optn.info >> 27) & 1) != 0); - - // Build the event dispatch acceleration tables. - EventSlotMap_build(&runner->eventSlotMap, dataWin); - ResolvedEventTable_build(&runner->eventTable, dataWin, &runner->eventSlotMap); - - // Create assets map - shdefault(runner->assetsByName, -1); - repeat(dataWin->objt.count, i) { - shput(runner->assetsByName, dataWin->objt.objects[i].name, i); - } - repeat(dataWin->sprt.count, i) { - shput(runner->assetsByName, dataWin->sprt.sprites[i].name, i); - } - repeat(dataWin->sond.count, i) { - shput(runner->assetsByName, dataWin->sond.sounds[i].name, i); - } - repeat(dataWin->bgnd.count, i) { - shput(runner->assetsByName, dataWin->bgnd.backgrounds[i].name, i); - } - repeat(dataWin->path.count, i) { - shput(runner->assetsByName, dataWin->path.paths[i].name, i); - } - repeat(dataWin->scpt.count, i) { - shput(runner->assetsByName, dataWin->scpt.scripts[i].name, i); - } - repeat(dataWin->font.count, i) { - shput(runner->assetsByName, dataWin->font.fonts[i].name, i); - } - repeat(dataWin->tmln.count, i) { - shput(runner->assetsByName, dataWin->tmln.timelines[i].name, i); - } - repeat(dataWin->room.count, i) { - shput(runner->assetsByName, dataWin->room.rooms[i].name, i); - } - - Runner_reset(runner); - - populateObjectsWithAnyEventOfType(runner); - - // Link runner to VM context - vm->runner = (struct Runner*) runner; - - renderer->vtable->init(renderer, dataWin); - audioSystem->vtable->init(audioSystem, dataWin, fileSystem); - - return runner; -} - -static inline void dispatchInstanceCreationEvents(Runner* runner, Instance* inst) { - inst->createEventFired = true; - Runner_executeEvent(runner, inst, EVENT_PRECREATE, 0); - Runner_executeEvent(runner, inst, EVENT_CREATE, 0); -} - -Instance* Runner_createInstance(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex) { - if (isObjectDisabled(runner, objectIndex)) return nullptr; - Instance* inst = createAndInitInstance(runner, runner->nextInstanceId++, objectIndex, x, y); - dispatchInstanceCreationEvents(runner, inst); - return inst; -} - -// Same as Runner_createInstance, but sets depth BEFORE firing Create events so scripts like scr_depth can override. -Instance* Runner_createInstanceWithDepth(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex, int32_t depth) { - if (isObjectDisabled(runner, objectIndex)) return nullptr; - Instance* inst = createAndInitInstance(runner, runner->nextInstanceId++, objectIndex, x, y); - inst->depth = depth; - dispatchInstanceCreationEvents(runner, inst); - return inst; -} - -Instance* Runner_createInstanceWithLayer(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex, int32_t layerId) { - if (isObjectDisabled(runner, objectIndex)) return nullptr; - RuntimeLayer* rl = Runner_findRuntimeLayerById(runner, layerId); - if (rl == nullptr) { - fprintf(stderr, "Runner: instance_create_layer: Layer ID %d not found!\n", layerId); - return nullptr; - } - Instance* inst = createAndInitInstance(runner, runner->nextInstanceId++, objectIndex, x, y); - inst->layer = layerId; - inst->depth = rl->depth; - dispatchInstanceCreationEvents(runner, inst); - return inst; -} - -Instance* Runner_copyInstance(Runner* runner, Instance* source, bool performEvent) { - requireNotNull(source); - if (isObjectDisabled(runner, source->objectIndex)) return nullptr; - - Instance* inst = createAndInitInstance(runner, runner->nextInstanceId++, source->objectIndex, source->x, source->y); - Instance_copyFields(inst, source); - inst->createEventFired = true; - if (performEvent) { - Runner_executeEvent(runner, inst, EVENT_PRECREATE, 0); - Runner_executeEvent(runner, inst, EVENT_CREATE, 0); - } - return inst; -} - -void Runner_destroyInstance(MAYBE_UNUSED Runner* runner, Instance* inst) { - Runner_executeEvent(runner, inst, EVENT_DESTROY, 0); - // A destroyed instance must ALWAYS be not active - // If a destroyed instance is active, then well, something went VERY wrong - inst->active = false; - inst->destroyed = true; - -#ifdef ENABLE_VM_TRACING - GameObject* gameObject = &runner->dataWin->objt.objects[inst->objectIndex]; - if (shgeti(runner->vmContext->instanceLifecyclesToBeTraced, "*") != -1 || shgeti(runner->vmContext->instanceLifecyclesToBeTraced, gameObject->name) != -1) { - fprintf(stderr, "VM: Instance %s (instanceId=%d,objectIndex=%d) destroyed\n", gameObject->name, inst->instanceId, inst->objectIndex); - } -#endif -} - -RuntimeLayer* Runner_findRuntimeLayerById(Runner* runner, int32_t id) { - size_t count = arrlenu(runner->runtimeLayers); - repeat(count, i) { - if ((int32_t) runner->runtimeLayers[i].id == id) - return &runner->runtimeLayers[i]; - } - return nullptr; -} - -RoomLayer* Runner_findRoomLayerById(Runner* runner, int32_t id) { - if (runner->currentRoom == nullptr) return nullptr; - repeat(runner->currentRoom->layerCount, i) { - if ((int32_t) runner->currentRoom->layers[i].id == id) return &runner->currentRoom->layers[i]; - } - return nullptr; -} - -RuntimeLayerElement* Runner_findLayerElementById(Runner* runner, int32_t elementId, RuntimeLayer** outLayer) { - size_t layerCount = arrlenu(runner->runtimeLayers); - repeat(layerCount, i) { - RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; - size_t elementCount = arrlenu(runtimeLayer->elements); - repeat(elementCount, j) { - if ((int32_t) runtimeLayer->elements[j].id == elementId) { - if (outLayer != nullptr) - *outLayer = runtimeLayer; - - return &runtimeLayer->elements[j]; - } - } - } - if (outLayer != nullptr) *outLayer = nullptr; - return nullptr; -} - -uint32_t Runner_getNextLayerId(Runner* runner) { - return runner->nextLayerId++; -} - -// Reaps GML structs whose only remaining ref is the structInstances registry's implicit +1. -// Walks backward so that swap-remove of dead entries doesn't disturb the indexes of entries we haven't visited yet. -static void Runner_sweepDeadStructs(Runner* runner) { - int32_t count = (int32_t) arrlen(runner->structInstances); - for (int32_t i = count - 1; i >= 0; i--) { - Instance* s = runner->structInstances[i]; - if (s->refCount > 1) continue; // still referenced by user code - require(s->refCount == 1); - - // Remove from runner->instancesById so future findInstanceByTarget(id) returns nullptr. - hmdel(runner->instancesById, s->instanceId); - - // O(1) swap-remove from structInstances, keeping structRegistryIndex in sync. - int32_t lastIdx = (int32_t) arrlen(runner->structInstances) - 1; - if (i != lastIdx) { - Instance* moved = runner->structInstances[lastIdx]; - runner->structInstances[i] = moved; - moved->structRegistryIndex = i; - } - arrpop(runner->structInstances); - - s->structRegistryIndex = -1; - s->refCount = 0; // drop the registry's ref; we are about to free - Instance_free(s); - } -} - -void Runner_cleanupDestroyedInstances(Runner* runner) { - int32_t count = (int32_t) arrlen(runner->instances); - int32_t writeIdx = 0; - repeat(count, i) { - Instance* inst = runner->instances[i]; - if (!inst->destroyed) { - runner->instances[writeIdx++] = inst; - } else { - Runner_removeInstanceFromObjectLists(runner, inst); - hmdel(runner->instancesById, inst->instanceId); - Instance_free(inst); - // Cached drawables hold raw Instance* that we just freed; force a rebuild before the next draw. - runner->drawableListStructureDirty = true; - } - } - arrsetlen(runner->instances, writeIdx); -} - -void Runner_initFirstRoom(Runner* runner) { - DataWin* dataWin = runner->dataWin; - require(dataWin->gen8.roomOrderCount > 0); - - int32_t firstRoomIndex = dataWin->gen8.roomOrder[0]; - - // Run global init scripts with the global scope instance as "self" - // In GMS 2.3+ (BC17), GLOB scripts store function declarations on "self" via Pop.v.v - runner->vmContext->currentInstance = runner->globalScopeInstance; - repeat(dataWin->glob.count, i) { - int32_t codeId = dataWin->glob.codeIds[i]; - if (codeId >= 0 && dataWin->code.count > (uint32_t) codeId) { - fprintf(stderr, "Runner: Executing global init script: %s\n", dataWin->code.entries[codeId].name); - RValue result = VM_executeCode(runner->vmContext, codeId); - RValue_free(&result); - } - } - runner->vmContext->currentInstance = nullptr; - - // Initialize the first room - initRoom(runner, firstRoomIndex); - - // Fire Game Start for all instances - Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_GAME_START); - runner->gameStartFired = true; - - // Fire Room Start for all instances - Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ROOM_START); -} - -// ===[ Collision Event Dispatch ]=== - -static void executeCollisionEvent(Runner* runner, Instance* self, Instance* other, int32_t targetObjectIndex) { - VMContext* vm = runner->vmContext; - - // Save event context - int32_t savedEventType = vm->currentEventType; - int32_t savedEventSubtype = vm->currentEventSubtype; - int32_t savedEventObjectIndex = vm->currentEventObjectIndex; - struct Instance* savedOtherInstance = vm->otherInstance; - - // Set collision event context - vm->currentEventType = EVENT_COLLISION; - vm->currentEventSubtype = targetObjectIndex; - vm->otherInstance = other; - - int32_t ownerObjectIndex = -1; - int32_t codeId = findEventCodeIdAndOwner(runner, self->objectIndex, EVENT_COLLISION, targetObjectIndex, &ownerObjectIndex); - - vm->currentEventObjectIndex = ownerObjectIndex; - -#ifdef ENABLE_VM_TRACING - if (codeId >= 0 && shlen(vm->eventsToBeTraced) != -1) { - const char* selfName = runner->dataWin->objt.objects[self->objectIndex].name; - const char* targetName = runner->dataWin->objt.objects[targetObjectIndex].name; - bool shouldTrace = shgeti(vm->eventsToBeTraced, "*") != -1 || shgeti(vm->eventsToBeTraced, "Collision") != -1 || shgeti(vm->eventsToBeTraced, selfName) != -1; - if (shouldTrace) { - fprintf(stderr, "Runner: [%s] Collision with %s (instanceId=%d, otherId=%d)\n", selfName, targetName, self->instanceId, other->instanceId); - } - } -#endif - - executeCode(runner, self, codeId); - - // Restore event context - vm->currentEventType = savedEventType; - vm->currentEventSubtype = savedEventSubtype; - vm->currentEventObjectIndex = savedEventObjectIndex; - vm->otherInstance = savedOtherInstance; -} - -// ===[ Path Adaptation ]=== -// Advances path position and updates instance x/y (HTML5: yyInstance.js Adapt_Path, lines 2755-2881) -// Returns true if end of path was reached (and pathSpeed != 0), to fire OTHER_END_OF_PATH event. -static bool adaptPath(Runner* runner, Instance* inst) { - if (0 > inst->pathIndex) return false; - - DataWin* dataWin = runner->dataWin; - if ((uint32_t) inst->pathIndex >= dataWin->path.count) return false; - - GamePath* path = &dataWin->path.paths[inst->pathIndex]; - if (0.0 >= path->length) return false; - - bool atPathEnd = false; - - GMLReal orient = inst->pathOrientation * M_PI / 180.0; - - // Get current position's speed factor - PathPositionResult cur = GamePath_getPosition(path, inst->pathPosition); - GMLReal sp = cur.speed / (100.0 * inst->pathScale); - - // Advance position (compute in higher precision, truncate to float on store - matches native runner) - inst->pathPosition = (float) (inst->pathPosition + inst->pathSpeed * sp / path->length); - - // Handle end actions if position out of [0,1] - PathPositionResult pos0 = GamePath_getPosition(path, 0.0f); - if (inst->pathPosition >= 1.0f || 0.0f >= inst->pathPosition) { - atPathEnd = (inst->pathSpeed == 0.0f) ? false : true; - - switch (inst->pathEndAction) { - // stop moving - case 0: { - if (inst->pathSpeed >= 0.0f) { - if (inst->pathSpeed != 0.0f) { - inst->pathPosition = 1.0f; - inst->pathIndex = -1; - } - } else { - inst->pathPosition = 0.0f; - inst->pathIndex = -1; - } - break; - } - // continue from start position (restart) - case 1: { - if (0.0f > inst->pathPosition) { - inst->pathPosition += 1.0f; - } else { - inst->pathPosition -= 1.0f; - } - break; - } - // continue from current position - case 2: { - PathPositionResult pos1 = GamePath_getPosition(path, 1.0f); - GMLReal xx = pos1.x - pos0.x; - GMLReal yy = pos1.y - pos0.y; - GMLReal xdif = inst->pathScale * (xx * GMLReal_cos(orient) + yy * GMLReal_sin(orient)); - GMLReal ydif = inst->pathScale * (yy * GMLReal_cos(orient) - xx * GMLReal_sin(orient)); - - if (0.0f > inst->pathPosition) { - inst->pathXStart -= (float) xdif; - inst->pathYStart -= (float) ydif; - inst->pathPosition += 1.0f; - } else { - inst->pathXStart += (float) xdif; - inst->pathYStart += (float) ydif; - inst->pathPosition -= 1.0f; - } - break; - } - // reverse - case 3: { - if (0.0f > inst->pathPosition) { - inst->pathPosition = -inst->pathPosition; - inst->pathSpeed = (float) GMLReal_fabs(inst->pathSpeed); - } else { - inst->pathPosition = 2.0f - inst->pathPosition; - inst->pathSpeed = (float) -GMLReal_fabs(inst->pathSpeed); - } - break; - } - // default: stop - default: { - inst->pathPosition = 1.0f; - inst->pathIndex = -1; - break; - } - } - } - - // Find the new position in the room - PathPositionResult newPos = GamePath_getPosition(path, inst->pathPosition); - GMLReal xx = newPos.x - pos0.x; // relative - GMLReal yy = newPos.y - pos0.y; - - GMLReal newx = inst->pathXStart + inst->pathScale * (xx * GMLReal_cos(orient) + yy * GMLReal_sin(orient)); - GMLReal newy = inst->pathYStart + inst->pathScale * (yy * GMLReal_cos(orient) - xx * GMLReal_sin(orient)); - - // Trick to set the direction: set hspeed/vspeed to delta, which updates direction - inst->hspeed = (float) (newx - inst->x); - inst->vspeed = (float) (newy - inst->y); - Instance_computeSpeedFromComponents(inst); - - // Normal speed should not be used - inst->speed = 0.0f; - inst->hspeed = 0.0f; - inst->vspeed = 0.0f; - - // Set the new position - inst->x = (float) newx; - inst->y = (float) newy; - - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - - return atPathEnd; -} - -static void dispatchCollisionEvents(Runner* runner) { - DataWin* dataWin = runner->dataWin; - // Iterate only the objects that have any collision event in their parent chain. - int32_t* selfObjects = (runner->objectsWithAnyEventOfType != nullptr) ? runner->objectsWithAnyEventOfType[EVENT_COLLISION] : nullptr; - if (selfObjects == nullptr) return; - int32_t selfObjCount = (int32_t) arrlen(selfObjects); - - repeat(selfObjCount, soIdx) { - int32_t selfObjIdx = selfObjects[soIdx]; - Instance** selfBucket = runner->instancesByExactObject[selfObjIdx]; - int32_t selfBucketCount = (int32_t) arrlen(selfBucket); - if (selfBucketCount == 0) continue; - - // Snapshot the self bucket: collision handlers can spawn/destroy/instance_change. Iterating a snapshot also keeps newly-created instances from firing collisions in this same phase. - int32_t selfSnapBase = (int32_t) arrlen(runner->instanceSnapshots); - arrsetlen(runner->instanceSnapshots, selfSnapBase + selfBucketCount); - memcpy(&runner->instanceSnapshots[selfSnapBase], selfBucket, (size_t) selfBucketCount * sizeof(Instance*)); - - repeat(selfBucketCount, si) { - Instance* self = runner->instanceSnapshots[selfSnapBase + si]; - if (!self->active) continue; - - InstanceBBox bboxSelf; - Sprite* sprSelf; - bool selfDirty = true; - - // Walk the parent chain to find all collision event handlers for this object - int32_t currentObj = self->objectIndex; - int depth = 0; - while (currentObj >= 0 && dataWin->objt.count > (uint32_t) currentObj && 32 > depth) { - GameObject* obj = &dataWin->objt.objects[currentObj]; - - ObjectEventList* eventList = &obj->eventLists[EVENT_COLLISION]; - repeat(eventList->eventCount, evtIdx) { - ObjectEvent* evt = &eventList->events[evtIdx]; - int32_t targetObjIndex = (int32_t) evt->eventSubtype; - - if (evt->actionCount == 0 || 0 > evt->actions[0].codeId) continue; - - // Iterate only the descendant-inclusive list for the target object via a snapshot, so nested user code (collision handlers calling instance_exists, with (...), etc.) can push/pop their own snapshots above ours without corrupting this iteration. - int32_t snapBase = Runner_pushInstancesOfObject(runner, targetObjIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t snapIdx = snapBase; snapEnd > snapIdx; snapIdx++) { - Instance* other = runner->instanceSnapshots[snapIdx]; - if (!other->active) continue; - if (other == self) continue; - - // Compute bboxes - if (selfDirty) { - bboxSelf = Collision_computeBBox(dataWin, self); - sprSelf = Collision_getSprite(dataWin, self); - selfDirty = false; - } - InstanceBBox bboxOther = Collision_computeBBox(dataWin, other); - if (!bboxSelf.valid || !bboxOther.valid) continue; - - // AABB overlap test - if (bboxSelf.left >= bboxOther.right || bboxOther.left >= bboxSelf.right || bboxSelf.top >= bboxOther.bottom || bboxOther.top >= bboxSelf.bottom) - continue; - - // Precise collision check if either sprite needs it (per-pixel for sepMasks==1, OBB SAT for rotated sepMasks==2). - Sprite* sprOther = Collision_getSprite(dataWin, other); - bool needsPrecise = (sprSelf != nullptr && sprSelf->sepMasks == 1) || (sprOther != nullptr && sprOther->sepMasks == 1) || Collision_obbNeedsSAT(sprSelf, self) || Collision_obbNeedsSAT(sprOther, other); - - if (needsPrecise) { - if (!Collision_instancesOverlapPrecise(dataWin, runner->collisionCompatibilityMode, self, other, bboxSelf, bboxOther)) continue; - } - - // Collision detected! If either instance is solid, restore both to xprevious/yprevious. - bool hadSolid = self->solid || other->solid; - if (hadSolid) { - self->x = self->xprevious; - self->y = self->yprevious; - if (self->pathIndex >= 0) self->pathPosition = self->pathPositionPrevious; - other->x = other->xprevious; - other->y = other->yprevious; - if (other->pathIndex >= 0) other->pathPosition = other->pathPositionPrevious; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, self); - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, other); - } - - // We don't need to call "SpatialGrid_markInstanceAsDirty" here because *technically* just because a collision happened, doesn't mean that the instances have moved - // And if it DOES move via GML, the variable write handlers will set it to dirty - - executeCollisionEvent(runner, self, other, targetObjIndex); - - // Native parity for solids: collision event can alter path state, so run one - // post-event path adaptation and apply its hspeed/vspeed step. - if (hadSolid && self->active && other->active) { - adaptPath(runner, self); - adaptPath(runner, other); - if (self->hspeed != 0.0f || self->vspeed != 0.0f) { - self->x += self->hspeed; - self->y += self->vspeed; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, self); - } - if (other->hspeed != 0.0f || other->vspeed != 0.0f) { - other->x += other->hspeed; - other->y += other->vspeed; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, other); - } - } - - // The collision event may have moved our instance, so we'll need to regenerate our self attributes! - selfDirty = true; - } - Runner_popInstanceSnapshot(runner, snapBase); - } - - currentObj = obj->parentId; - depth++; - } - } - - arrsetlen(runner->instanceSnapshots, selfSnapBase); - } -} - -// ===[ View Following + Clamping ]=== -// Single-axis follow with border-based scrolling, room clamping, and speed limit. -static int32_t followAxis(int32_t viewPos, int32_t viewSize, int32_t targetPos, uint32_t border, int32_t speed, int32_t roomSize) { - int32_t pos = viewPos; - - // Border-based scrolling - if (2 * (int32_t) border >= viewSize) { - pos = targetPos - viewSize / 2; - } else if (targetPos - (int32_t) border < viewPos) { - pos = targetPos - (int32_t) border; - } else if (targetPos + (int32_t) border > viewPos + viewSize) { - pos = targetPos + (int32_t) border - viewSize; - } - - // Clamp to room bounds - if (0 > pos) pos = 0; - if (pos + viewSize > roomSize) pos = roomSize - viewSize; - - // Speed limit - if (speed >= 0) { - if (pos < viewPos && viewPos - pos > speed) pos = viewPos - speed; - if (pos > viewPos && pos - viewPos > speed) pos = viewPos + speed; - } - - return pos; -} - -static void updateViews(Runner* runner) { - Room* room = runner->currentRoom; - if (!(room->flags & 1)) return; - - repeat(MAX_VIEWS, vi) { - RuntimeView* view = &runner->views[vi]; - if (!view->enabled || 0 > view->objectId) continue; - - // Find first active instance of the target object. - Instance* target = nullptr; - if (view->objectId >= 0 && runner->dataWin->objt.count > (uint32_t) view->objectId) { - Instance** bucket = runner->instancesByObject[view->objectId]; - int32_t bucketCount = (int32_t) arrlen(bucket); - repeat(bucketCount, i) { - if (bucket[i]->active) { target = bucket[i]; break; } - } - } - - if (target != nullptr) { - int32_t ix = (int32_t) GMLReal_floor(target->x); - int32_t iy = (int32_t) GMLReal_floor(target->y); - view->viewX = followAxis(view->viewX, view->viewWidth, ix, view->borderX, view->speedX, (int32_t) room->width); - view->viewY = followAxis(view->viewY, view->viewHeight, iy, view->borderY, view->speedY, (int32_t) room->height); - } - } -} - -static void dispatchOutsideRoomEvents(Runner* runner) { - DataWin* dataWin = runner->dataWin; - int32_t outsideSlot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_OTHER, OTHER_OUTSIDE_ROOM); - if (0 > outsideSlot) return; - ResolvedEventTable* table = &runner->eventTable; - uint32_t entryCount; - SlotResponderEntry* entries = ResolvedEventTable_slotEntries(table, outsideSlot, &entryCount); - if (entryCount == 0) return; - - int32_t roomWidth = (int32_t) runner->currentRoom->width; - int32_t roomHeight = (int32_t) runner->currentRoom->height; - - repeat(entryCount, s) { - int32_t objIdx = entries[s].concreteObjectId; - Instance** bucket = runner->instancesByExactObject[objIdx]; - int32_t bucketCount = (int32_t) arrlen(bucket); - if (bucketCount == 0) continue; - - // All instances in the bucket share the same exact objectIndex, so the handler resolves to one (codeId, owner). - int32_t ownerObjectIndex = -1; - int32_t codeId = ResolvedEventTable_lookup(table, objIdx, outsideSlot, &ownerObjectIndex); - if (0 > codeId) continue; - - // Snapshot the bucket: an Outside Room handler can spawn/destroy/instance_change. - int32_t snapshotBase = (int32_t) arrlen(runner->instanceSnapshots); - arrsetlen(runner->instanceSnapshots, snapshotBase + bucketCount); - memcpy(&runner->instanceSnapshots[snapshotBase], bucket, (size_t) bucketCount * sizeof(Instance*)); - - repeat(bucketCount, i) { - Instance* inst = runner->instanceSnapshots[snapshotBase + i]; - if (!inst->active) continue; - - bool outside; - InstanceBBox bbox = Collision_computeBBox(dataWin, inst); - if (bbox.valid) { - outside = (0 > bbox.right || bbox.left > roomWidth || 0 > bbox.bottom || bbox.top > roomHeight); - } else { - outside = (0 > inst->x || inst->x > roomWidth || 0 > inst->y || inst->y > roomHeight); - } - - if (outside && !inst->outsideRoom) { - Runner_executeResolvedEvent(runner, inst, EVENT_OTHER, OTHER_OUTSIDE_ROOM, codeId, ownerObjectIndex); - if (runner->pendingRoom >= 0) { - arrsetlen(runner->instanceSnapshots, snapshotBase); - return; - } - } - - inst->outsideRoom = outside; - } - - arrsetlen(runner->instanceSnapshots, snapshotBase); - } -} - -static void persistRoomState(Runner* runner, int32_t roomIndex) { - SavedRoomState* state = &runner->savedRoomStates[roomIndex]; - - // Free any previously saved instances (from an earlier visit) - int32_t prevSavedCount = (int32_t) arrlen(state->instances); - repeat(prevSavedCount, i) { - hmdel(runner->instancesById, state->instances[i]->instanceId); - Instance_free(state->instances[i]); - } - arrfree(state->instances); - state->instances = nullptr; - hmfree(state->tileLayerMap); - state->tileLayerMap = nullptr; - freeRuntimeLayersArray(&state->runtimeLayers); - - // Separate persistent instances (travel with player) from room instances (saved) - Instance** keptInstances = nullptr; - int32_t count = (int32_t) arrlen(runner->instances); - repeat(count, i) { - Instance* inst = runner->instances[i]; - if (inst->persistent) { - arrput(keptInstances, inst); - } else if (inst->active) { - arrput(state->instances, inst); - } else { - hmdel(runner->instancesById, inst->instanceId); - Instance_free(inst); - } - } - arrfree(runner->instances); - runner->instances = keptInstances; - - // The per-object lists referenced the full pre-transition instance set (persistents, saved-to-state, and soon-to-be-freed). Only the kept persistents remain live, so rebuild from scratch from the final runner->instances. - Runner_clearAllObjectLists(runner); - repeat((int32_t) arrlen(runner->instances), i) { - Runner_addInstanceToObjectLists(runner, runner->instances[i]); - } - - // Save room visual state - memcpy(state->backgrounds, runner->backgrounds, sizeof(runner->backgrounds)); - memcpy(state->views, runner->views, sizeof(runner->views)); - state->backgroundColor = runner->backgroundColor; - state->drawBackgroundColor = runner->drawBackgroundColor; - - // Transfer tile layer map ownership to saved state - state->tileLayerMap = runner->tileLayerMap; - runner->tileLayerMap = nullptr; - - // Transfer runtime layer ownership to saved state - state->runtimeLayers = runner->runtimeLayers; - runner->runtimeLayers = nullptr; - - state->initialized = true; -} - -void Runner_step(Runner* runner) { - // The snapshot arena is stack-like and every push must be matched with a pop within the same frame. Assert that invariant at the top of each step: a non-zero length here means some site below pushed without popping, and we want a loud failure with the offending length so we can find it instead of silently leaking until the next frame. - requireMessageFormatted(arrlen(runner->instanceSnapshots) == 0, "instanceSnapshots arena was not fully popped at end of previous frame (length=%td)", arrlen(runner->instanceSnapshots)); - - // Check for gamepad connect/disconnect and fire Async System event - for (int i = 0; MAX_GAMEPADS > i; i++) { - GamepadSlot* slot = &runner->gamepads->slots[i]; - if (slot->connected != slot->connectedPrev) { - DsMapEntry* map = nullptr; - arrput(runner->dsMapPool, map); - int32_t mapId = arrlen(runner->dsMapPool) - 1; - - DsMapEntry** mapPtr = &runner->dsMapPool[mapId]; - shput(*mapPtr, safeStrdup("event_type"), RValue_makeOwnedString(safeStrdup(slot->connected ? "gamepad discovered" : "gamepad lost"))); - shput(*mapPtr, safeStrdup("pad_index"), RValue_makeReal((GMLReal) i)); - - runner->asyncLoadMapId = mapId; - Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ASYNC_SYSTEM); - - // Clean up ds_map - mapPtr = &runner->dsMapPool[mapId]; - if (*mapPtr != nullptr) { - repeat(shlen(*mapPtr), j) { - free((*mapPtr)[j].key); - RValue_free(&(*mapPtr)[j].value); - } - shfree(*mapPtr); - *mapPtr = nullptr; - } - runner->asyncLoadMapId = -1; - } - } - - // Save xprevious/yprevious and path_positionprevious for all active instances - int32_t prevCount = (int32_t) arrlen(runner->instances); - repeat(prevCount, i) { - Instance* inst = runner->instances[i]; - if (inst->active) { - inst->xprevious = inst->x; - inst->yprevious = inst->y; - inst->pathPositionPrevious = inst->pathPosition; - } - } - - // Advance image_index by image_speed for all active instances - int32_t animCount = (int32_t) arrlen(runner->instances); - int32_t animEndSlot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_OTHER, OTHER_ANIMATION_END); - repeat(animCount, i) { - Instance* inst = runner->instances[i]; - if (!inst->active) continue; - if (0 > inst->spriteIndex) continue; - - inst->imageIndex += inst->imageSpeed; - - // Wrap image_index (matches HTML5 runner: manual subtract/add instead of using fmod) - Sprite* sprite = &runner->dataWin->sprt.sprites[inst->spriteIndex]; - float frameCount = (float) sprite->textureCount; - bool wrapped = false; - if (inst->imageIndex >= frameCount) { - inst->imageIndex -= frameCount; - wrapped = true; - } else if (0.0f > inst->imageIndex) { - inst->imageIndex += frameCount; - wrapped = true; - } - if (wrapped && animEndSlot >= 0) { - int32_t ownerObjectIndex = -1; - int32_t codeId = ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, animEndSlot, &ownerObjectIndex); - if (codeId >= 0) Runner_executeResolvedEvent(runner, inst, EVENT_OTHER, OTHER_ANIMATION_END, codeId, ownerObjectIndex); - } - } - - // Scroll backgrounds - Runner_scrollBackgrounds(runner); - - // Advance GMS2 layer parallax (hspeed/vspeed per frame) - size_t layerCount = arrlenu(runner->runtimeLayers); - repeat(layerCount, i) { - RuntimeLayer* rl = &runner->runtimeLayers[i]; - rl->xOffset += rl->hSpeed; - rl->yOffset += rl->vSpeed; - } - - // Execute Begin Step for all instances - Runner_executeEventForAll(runner, EVENT_STEP, STEP_BEGIN); - - // Dispatch keyboard events - RunnerKeyboardState* kb = runner->keyboard; - for (int32_t key = 0; GML_KEY_COUNT > key; key++) { - if (kb->keyPressed[key]) { - Runner_executeEventForAll(runner, EVENT_KEYPRESS, key); - } - } - for (int32_t key = 0; GML_KEY_COUNT > key; key++) { - if (kb->keyDown[key]) { - Runner_executeEventForAll(runner, EVENT_KEYBOARD, key); - } - } - for (int32_t key = 0; GML_KEY_COUNT > key; key++) { - if (kb->keyReleased[key]) { - Runner_executeEventForAll(runner, EVENT_KEYRELEASE, key); - } - } - - // Process alarms. Outer loop is over alarm slots (matching the native runner's HandleAlarm), and for each slot we walk only the objects in the event table's bySlot range and only those objects' exact instance buckets. Idle instances are further skipped via activeAlarmMask. - repeat(GML_ALARM_COUNT, alarmIdx) { - int32_t alarmSlot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_ALARM, alarmIdx); - if (0 > alarmSlot) continue; - ResolvedEventTable* table = &runner->eventTable; - uint32_t entryCount; - SlotResponderEntry* entries = ResolvedEventTable_slotEntries(table, alarmSlot, &entryCount); - - repeat(entryCount, s) { - int32_t objIdx = entries[s].concreteObjectId; - Instance** bucket = runner->instancesByExactObject[objIdx]; - int32_t bucketCount = (int32_t) arrlen(bucket); - if (bucketCount == 0) continue; - - // All instances in the bucket share the same exact objectIndex, so the handler resolves to one (codeId, owner). - int32_t ownerObjectIndex = -1; - int32_t codeId = ResolvedEventTable_lookup(table, objIdx, alarmSlot, &ownerObjectIndex); - if (0 > codeId) continue; - - // Snapshot the bucket before dispatch: alarm code can call instance_change/instance_destroy/instance_create which mutate the live bucket. Iterating the snapshot also ensures newly-created instances do not fire alarms in this same phase. - int32_t snapshotBase = (int32_t) arrlen(runner->instanceSnapshots); - arrsetlen(runner->instanceSnapshots, snapshotBase + bucketCount); - memcpy(&runner->instanceSnapshots[snapshotBase], bucket, (size_t) bucketCount * sizeof(Instance*)); - - repeat(bucketCount, i) { - Instance* inst = runner->instanceSnapshots[snapshotBase + i]; - if (!inst->active) continue; - uint16_t bit = (uint16_t) (1u << alarmIdx); - if ((inst->activeAlarmMask & bit) == 0) continue; - -#ifdef ENABLE_VM_TRACING - GameObject* object = &runner->dataWin->objt.objects[inst->objectIndex]; - if (shgeti(runner->vmContext->alarmsToBeTraced, "*") != -1 || shgeti(runner->vmContext->alarmsToBeTraced, object->name) != -1) { - fprintf(stderr, "VM: [%s] Ticking down Alarm[%d] (instanceId=%d), current tick is %d\n", object->name, alarmIdx, inst->instanceId, inst->alarm[alarmIdx]); - } -#endif - - inst->alarm[alarmIdx]--; - if (inst->alarm[alarmIdx] == 0) { - inst->alarm[alarmIdx] = -1; - inst->activeAlarmMask &= (uint16_t) ~bit; - -#ifdef ENABLE_VM_TRACING - if (shgeti(runner->vmContext->alarmsToBeTraced, "*") != -1 || shgeti(runner->vmContext->alarmsToBeTraced, object->name) != -1) { - fprintf(stderr, "VM: [%s] Firing Alarm[%d] (instanceId=%d)\n", object->name, alarmIdx, inst->instanceId); - } -#endif - - Runner_executeResolvedEvent(runner, inst, EVENT_ALARM, alarmIdx, codeId, ownerObjectIndex); - } - } - - arrsetlen(runner->instanceSnapshots, snapshotBase); - } - } - - // Execute Normal Step for all instances - Runner_executeEventForAll(runner, EVENT_STEP, STEP_NORMAL); - - // Apply motion: friction, gravity, then x += hspeed, y += vspeed - int32_t motionCount = (int32_t) arrlen(runner->instances); - int32_t endOfPathSlot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_OTHER, OTHER_END_OF_PATH); - repeat(motionCount, mi) { - Instance* inst = runner->instances[mi]; - if (!inst->active) continue; - - // Friction: reduce speed toward zero (HTML5: AdaptSpeed) - if (inst->friction != 0.0f) { - float ns = (inst->speed > 0.0f) ? inst->speed - inst->friction : inst->speed + inst->friction; - if ((inst->speed > 0.0f && ns < 0.0f) || (inst->speed < 0.0f && ns > 0.0f)) { - inst->speed = 0.0f; - } else if (inst->speed != 0.0f) { - inst->speed = ns; - } - Instance_computeComponentsFromSpeed(inst); - } - - // Gravity: add velocity in gravity_direction (HTML5: AddTo_Speed) - if (inst->gravity != 0.0f) { - GMLReal gravDirRad = inst->gravityDirection * (M_PI / 180.0); - inst->hspeed += (float) (inst->gravity * clampFloat(GMLReal_cos(gravDirRad))); - inst->vspeed -= (float) (inst->gravity * clampFloat(GMLReal_sin(gravDirRad))); - Instance_computeSpeedFromComponents(inst); - } - - // Path adaptation (HTML5: Adapt_Path, runs after friction/gravity, before x+=hspeed) - if (adaptPath(runner, inst) && endOfPathSlot >= 0) { - int32_t ownerObjectIndex = -1; - int32_t codeId = ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, endOfPathSlot, &ownerObjectIndex); - if (codeId >= 0) Runner_executeResolvedEvent(runner, inst, EVENT_OTHER, OTHER_END_OF_PATH, codeId, ownerObjectIndex); - } - - // Apply movement - if (inst->hspeed != 0.0f || inst->vspeed != 0.0f) { - inst->x += inst->hspeed; - inst->y += inst->vspeed; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - } - } - - // Dispatch outside room events - dispatchOutsideRoomEvents(runner); - - // Dispatch collision events - dispatchCollisionEvents(runner); - - // Execute End Step for all instances - Runner_executeEventForAll(runner, EVENT_STEP, STEP_END); - - // Update view following - updateViews(runner); - - // Handle game restart - if (runner->pendingRoom == ROOM_RESTARTGAME) { - // See you soon! - // Free the currently-loaded non-eager room before reset so lazyLoadRooms stays steady-state. - if (runner->dataWin->lazyLoadRooms && runner->currentRoom != nullptr && !runner->currentRoom->eagerlyLoaded) { - DataWin_freeRoomPayload(runner->currentRoom); - } - Runner_reset(runner); - Runner_initFirstRoom(runner); - runner->frameCount++; - return; - } - - // Handle room transition - if (runner->pendingRoom >= 0) { - int32_t oldRoomIndex = runner->currentRoomIndex; - Room* oldRoom = runner->currentRoom; - const char* oldRoomName = oldRoom->name; - - // Fire Room End for all instances - Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ROOM_END); - - int32_t newRoomIndex = runner->pendingRoom; - runner->pendingRoom = -1; - require(runner->dataWin->room.count > (uint32_t) newRoomIndex); - const char* newRoomName = runner->dataWin->room.rooms[newRoomIndex].name; - - fprintf(stderr, "Room changed: %s (room %d) -> %s (room %d)\n", oldRoomName, oldRoomIndex, newRoomName, newRoomIndex); - - // If the old room is persistent, save its instance and visual state - if (oldRoom->persistent) { - persistRoomState(runner, oldRoomIndex); - } - - // Free the outgoing room's payload under lazyLoadRooms, unless it's eagerly pinned or we're restarting the same room (initRoom would just re-load it). - if (runner->dataWin->lazyLoadRooms && !oldRoom->eagerlyLoaded && newRoomIndex != oldRoomIndex) { - DataWin_freeRoomPayload(oldRoom); - } - - // Load new room - initRoom(runner, newRoomIndex); - - // Fire Room Start for all instances - Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ROOM_START); - } - - Runner_cleanupDestroyedInstances(runner); - Runner_sweepDeadStructs(runner); - - runner->frameCount++; -} - -// ===[ State Dump ]=== - -void Runner_dumpState(Runner* runner) { - DataWin* dataWin = runner->dataWin; - VMContext* vm = runner->vmContext; - int32_t instanceCount = (int32_t) arrlen(runner->instances); - - printf("=== Frame %d State Dump ===\n", runner->frameCount); - printf("Room: %s (index %d)\n", runner->currentRoom->name, runner->currentRoomIndex); - printf("Instance count: %d\n", instanceCount); - - repeat(instanceCount, i) { - Instance* inst = runner->instances[i]; - if (!inst->active) continue; - - GameObject* gameObject = nullptr; - const char* objName = ""; - if (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) { - gameObject = &dataWin->objt.objects[inst->objectIndex]; - objName = gameObject->name; - } - - const char* spriteName = ""; - if (inst->spriteIndex >= 0 && dataWin->sprt.count > (uint32_t) inst->spriteIndex) { - spriteName = dataWin->sprt.sprites[inst->spriteIndex].name; - } - - const char* parentName = ""; - if (gameObject != nullptr && gameObject->parentId >= 0 && dataWin->objt.count > (uint32_t) gameObject->parentId) { - parentName = dataWin->objt.objects[gameObject->parentId].name; - } - - printf("\n--- Instance #%d (%s, objectIndex=%d) ---\n", inst->instanceId, objName, inst->objectIndex); - printf(" Position: (%g, %g)\n", (double) inst->x, (double) inst->y); - printf(" Depth: %d\n", inst->depth); - printf(" Sprite: %s (index %d), imageIndex=%g, imageSpeed=%g\n", spriteName, inst->spriteIndex, (double) inst->imageIndex, (double) inst->imageSpeed); - printf(" Scale: (%g, %g), Angle: %g, Alpha: %g, Blend: 0x%06X\n", (double) inst->imageXscale, (double) inst->imageYscale, (double) inst->imageAngle, (double) inst->imageAlpha, inst->imageBlend); - printf(" Visible: %s, Active: %s, Solid: %s, Persistent: %s\n", inst->visible ? "true" : "false", inst->active ? "true" : "false", inst->solid ? "true" : "false", inst->persistent ? "true" : "false"); - printf(" Parent: %s (parentId=%d)\n", parentName, gameObject != nullptr ? gameObject->parentId : -1); - - // Active alarms - bool hasAlarm = false; - repeat(GML_ALARM_COUNT, alarmIdx) { - if (inst->alarm[alarmIdx] >= 0) { - if (!hasAlarm) { printf(" Alarms:"); hasAlarm = true; } - printf(" [%d]=%d", alarmIdx, inst->alarm[alarmIdx]); - } - } - if (hasAlarm) printf("\n"); - - // Self variables - bool hasSelfVars = false; - bool hasSelfArrays = false; - repeat(inst->selfVars.capacity, svIdx) { - IntRValueEntry* entry = &inst->selfVars.entries[svIdx]; - if (entry->key == INT_RVALUE_HASHMAP_EMPTY_KEY) continue; - int32_t varID = entry->key; - RValue val = entry->value; - if (val.type == RVALUE_UNDEFINED) continue; - - const char* varName = "?"; - repeat(dataWin->vari.variableCount, varIdx) { - Variable* var = &dataWin->vari.variables[varIdx]; - if (var->instanceType == INSTANCE_SELF && var->varID == varID) { - varName = var->name; - break; - } - } - - if (val.type == RVALUE_ARRAY && val.array != nullptr) { - if (!hasSelfArrays) { printf(" Self Arrays:\n"); hasSelfArrays = true; } - repeat(GMLArray_length1D(val.array), ai) { - RValue* cell = GMLArray_slot(val.array, ai); - if (cell == nullptr || cell->type == RVALUE_UNDEFINED) continue; - char* innerStr = RValue_toStringFancy(*cell); - printf(" %s[%d] = %s\n", varName, (int) ai, innerStr); - free(innerStr); - } - } else { - if (!hasSelfVars) { printf(" Self Variables:\n"); hasSelfVars = true; } - char* valStr = RValue_toStringFancy(val); - printf(" %s = %s\n", varName, valStr); - free(valStr); - } - } - } - - // Global variables (non-array) - printf("\n=== Global Variables ===\n"); - repeat(dataWin->vari.variableCount, varIdx) { - Variable* var = &dataWin->vari.variables[varIdx]; - if (var->instanceType != INSTANCE_GLOBAL || var->varID < 0) continue; - if ((uint32_t) var->varID >= vm->globalVarCount) continue; - RValue val = vm->globalVars[var->varID]; - if (val.type == RVALUE_UNDEFINED) continue; - - char* valStr = RValue_toStringFancy(val); - printf(" %s = %s\n", var->name, valStr); - free(valStr); - } - - // Global arrays: scan globalVars slots for RVALUE_ARRAY entries - repeat(dataWin->vari.variableCount, varIdx) { - Variable* var = &dataWin->vari.variables[varIdx]; - if (var->instanceType != INSTANCE_GLOBAL || var->varID < 0) continue; - if ((uint32_t) var->varID >= vm->globalVarCount) continue; - RValue val = vm->globalVars[var->varID]; - if (val.type != RVALUE_ARRAY || val.array == nullptr) continue; - repeat(GMLArray_length1D(val.array), ai) { - RValue* cell = GMLArray_slot(val.array, ai); - if (cell == nullptr || cell->type == RVALUE_UNDEFINED) continue; - char* innerStr = RValue_toStringFancy(*cell); - printf(" %s[%d] = %s\n", var->name, (int) ai, innerStr); - free(innerStr); - } - } - - printf("\n=== End Frame %d State Dump ===\n", runner->frameCount); -} - -// ===[ JSON State Dump ]=== - -static void writeRValueJson(JsonWriter* w, RValue val) { - switch (val.type) { - case RVALUE_REAL: - JsonWriter_double(w, val.real); - break; - case RVALUE_INT32: - JsonWriter_int(w, val.int32); - break; -#ifndef NO_RVALUE_INT64 - case RVALUE_INT64: - JsonWriter_int(w, val.int64); - break; -#endif - case RVALUE_STRING: - JsonWriter_string(w, val.string); - break; - case RVALUE_BOOL: - JsonWriter_bool(w, val.int32 != 0); - break; - case RVALUE_UNDEFINED: - JsonWriter_null(w); - break; - case RVALUE_ARRAY: { - // Render arrays as a JSON array. Skips RVALUE_UNDEFINED entries (they read as 0/null anyway). - JsonWriter_beginArray(w); - if (val.array != nullptr) { - repeat(GMLArray_length1D(val.array), ai) { - RValue* cell = GMLArray_slot(val.array, ai); - writeRValueJson(w, cell != nullptr ? *cell : (RValue){ .type = RVALUE_UNDEFINED }); - } - } - JsonWriter_endArray(w); - break; - } -#if IS_BC17_OR_HIGHER_ENABLED - case RVALUE_METHOD: { - char buf[64]; - snprintf(buf, sizeof(buf), "", val.method->codeIndex); - JsonWriter_string(w, buf); - break; - } -#endif - case RVALUE_STRUCT: { - char buf[64]; - snprintf(buf, sizeof(buf), "", val.structInst != nullptr ? val.structInst->instanceId : 0); - JsonWriter_string(w, buf); - break; - } - } -} - -char* Runner_dumpStateJson(Runner* runner) { - DataWin* dataWin = runner->dataWin; - VMContext* vm = runner->vmContext; - int32_t instanceCount = (int32_t) arrlen(runner->instances); - - JsonWriter w = JsonWriter_create(); - - JsonWriter_beginObject(&w); - - JsonWriter_propertyInt(&w, "frame", runner->frameCount); - - // Room info - JsonWriter_key(&w, "room"); - JsonWriter_beginObject(&w); - JsonWriter_propertyString(&w, "name", runner->currentRoom->name); - JsonWriter_propertyInt(&w, "index", runner->currentRoomIndex); - JsonWriter_endObject(&w); - - // Instances - JsonWriter_key(&w, "instances"); - JsonWriter_beginArray(&w); - - repeat(instanceCount, i) { - Instance* inst = runner->instances[i]; - if (!inst->active) continue; - - const char* objName = (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) ? dataWin->objt.objects[inst->objectIndex].name : nullptr; - - const char* spriteName = nullptr; - if (inst->spriteIndex >= 0 && dataWin->sprt.count > (uint32_t) inst->spriteIndex) { - spriteName = dataWin->sprt.sprites[inst->spriteIndex].name; - } - - JsonWriter_beginObject(&w); - - JsonWriter_propertyInt(&w, "instanceId", inst->instanceId); - JsonWriter_propertyString(&w, "objectName", objName); - JsonWriter_propertyInt(&w, "objectIndex", inst->objectIndex); - - // Parent object - const char* parentName = nullptr; - int32_t parentId = -1; - if (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) { - parentId = dataWin->objt.objects[inst->objectIndex].parentId; - if (parentId >= 0 && dataWin->objt.count > (uint32_t) parentId) { - parentName = dataWin->objt.objects[parentId].name; - } - } - JsonWriter_propertyString(&w, "parentObjectName", parentName); - JsonWriter_propertyInt(&w, "parentObjectIndex", parentId); - - JsonWriter_propertyDouble(&w, "x", inst->x); - JsonWriter_propertyDouble(&w, "y", inst->y); - JsonWriter_propertyInt(&w, "depth", inst->depth); - - // Sprite - JsonWriter_key(&w, "sprite"); - JsonWriter_beginObject(&w); - JsonWriter_propertyString(&w, "name", spriteName); - JsonWriter_propertyInt(&w, "index", inst->spriteIndex); - JsonWriter_propertyDouble(&w, "imageIndex", inst->imageIndex); - JsonWriter_propertyDouble(&w, "imageSpeed", inst->imageSpeed); - JsonWriter_endObject(&w); - - // Scale - JsonWriter_key(&w, "scale"); - JsonWriter_beginObject(&w); - JsonWriter_propertyDouble(&w, "x", inst->imageXscale); - JsonWriter_propertyDouble(&w, "y", inst->imageYscale); - JsonWriter_endObject(&w); - - JsonWriter_propertyDouble(&w, "angle", inst->imageAngle); - JsonWriter_propertyDouble(&w, "alpha", inst->imageAlpha); - JsonWriter_propertyInt(&w, "blend", inst->imageBlend); - JsonWriter_propertyBool(&w, "visible", inst->visible); - JsonWriter_propertyBool(&w, "active", inst->active); - JsonWriter_propertyBool(&w, "solid", inst->solid); - JsonWriter_propertyBool(&w, "persistent", inst->persistent); - - // Alarms - JsonWriter_key(&w, "alarms"); - JsonWriter_beginObject(&w); - repeat(GML_ALARM_COUNT, alarmIdx) { - if (inst->alarm[alarmIdx] >= 0) { - char alarmKey[4]; - snprintf(alarmKey, sizeof(alarmKey), "%d", alarmIdx); - JsonWriter_propertyInt(&w, alarmKey, inst->alarm[alarmIdx]); - } - } - JsonWriter_endObject(&w); - - // Self variables (non-array, sparse hashmap) - JsonWriter_key(&w, "selfVariables"); - JsonWriter_beginObject(&w); - repeat(inst->selfVars.capacity, svIdx) { - IntRValueEntry* entry = &inst->selfVars.entries[svIdx]; - if (entry->key == INT_RVALUE_HASHMAP_EMPTY_KEY) continue; - int32_t varID = entry->key; - RValue val = entry->value; - if (val.type == RVALUE_UNDEFINED) continue; - - // Resolve variable name from VARI chunk - const char* varName = "?"; - repeat(dataWin->vari.variableCount, varIdx) { - Variable* var = &dataWin->vari.variables[varIdx]; - if (var->instanceType == INSTANCE_SELF && var->varID == varID) { - varName = var->name; - break; - } - } - - JsonWriter_key(&w, varName); - writeRValueJson(&w, val); - } - JsonWriter_endObject(&w); - JsonWriter_endObject(&w); - } - - JsonWriter_endArray(&w); - - // Tiles - Room* dumpRoom = runner->currentRoom; - JsonWriter_key(&w, "tiles"); - JsonWriter_beginArray(&w); - repeat(dumpRoom->tileCount, tileIdx) { - RoomTile* tile = &dumpRoom->tiles[tileIdx]; - const char* bgName = (tile->backgroundDefinition >= 0 && dataWin->bgnd.count > (uint32_t) tile->backgroundDefinition) ? dataWin->bgnd.backgrounds[tile->backgroundDefinition].name : nullptr; - - JsonWriter_beginObject(&w); - JsonWriter_propertyInt(&w, "index", tileIdx); - JsonWriter_propertyInt(&w, "x", tile->x); - JsonWriter_propertyInt(&w, "y", tile->y); - JsonWriter_propertyInt(&w, "backgroundIndex", tile->backgroundDefinition); - if (bgName != nullptr) { - JsonWriter_propertyString(&w, "backgroundName", bgName); - } else { - JsonWriter_propertyNull(&w, "backgroundName"); - } - JsonWriter_propertyInt(&w, "sourceX", tile->sourceX); - JsonWriter_propertyInt(&w, "sourceY", tile->sourceY); - JsonWriter_propertyInt(&w, "width", tile->width); - JsonWriter_propertyInt(&w, "height", tile->height); - JsonWriter_propertyInt(&w, "depth", tile->tileDepth); - JsonWriter_propertyInt(&w, "instanceID", tile->instanceID); - JsonWriter_propertyDouble(&w, "scaleX", tile->scaleX); - JsonWriter_propertyDouble(&w, "scaleY", tile->scaleY); - JsonWriter_propertyInt(&w, "color", tile->color); - - ptrdiff_t layerIdx = hmgeti(runner->tileLayerMap, tile->tileDepth); - bool visible = (layerIdx >= 0) ? runner->tileLayerMap[layerIdx].value.visible : true; - JsonWriter_propertyBool(&w, "visible", visible); - JsonWriter_endObject(&w); - } - JsonWriter_endArray(&w); - - // Global variables (non-array) - JsonWriter_key(&w, "globalVariables"); - JsonWriter_beginObject(&w); - repeat(dataWin->vari.variableCount, varIdx) { - Variable* var = &dataWin->vari.variables[varIdx]; - if (var->instanceType != INSTANCE_GLOBAL || var->varID < 0) continue; - if ((uint32_t) var->varID >= vm->globalVarCount) continue; - RValue val = vm->globalVars[var->varID]; - if (val.type == RVALUE_UNDEFINED) continue; - - JsonWriter_key(&w, var->name); - writeRValueJson(&w, val); - } - JsonWriter_endObject(&w); - JsonWriter_endObject(&w); - - char* result = JsonWriter_copyOutput(&w); - JsonWriter_free(&w); - return result; -} - -void Runner_free(Runner* runner) { - if (runner == nullptr) return; - - cleanupState(runner); - - if (runner->instancesByObject != nullptr) { - uint32_t objectCount = runner->dataWin->objt.count; - repeat(objectCount, i) { - arrfree(runner->instancesByObject[i]); - } - free(runner->instancesByObject); - runner->instancesByObject = nullptr; - } - if (runner->instancesByExactObject != nullptr) { - uint32_t objectCount = runner->dataWin->objt.count; - repeat(objectCount, i) { - arrfree(runner->instancesByExactObject[i]); - } - free(runner->instancesByExactObject); - runner->instancesByExactObject = nullptr; - } - if (runner->objectsWithAnyEventOfType != nullptr) { - repeat(OBJT_EVENT_TYPE_COUNT, t) { - arrfree(runner->objectsWithAnyEventOfType[t]); - } - free(runner->objectsWithAnyEventOfType); - runner->objectsWithAnyEventOfType = nullptr; - } - arrfree(runner->cachedDrawables); - runner->cachedDrawables = nullptr; - arrfree(runner->instanceSnapshots); - runner->instanceSnapshots = nullptr; - arrfree(runner->eventDispatchInstances); - runner->eventDispatchInstances = nullptr; - ResolvedEventTable_free(&runner->eventTable); - EventSlotMap_destroy(&runner->eventSlotMap); - shfree(runner->assetsByName); - - RunnerKeyboard_free(runner->keyboard); - RunnerGamepad_free(runner->gamepads); - Instance_free(runner->globalScopeInstance); - free(runner); -} +#include "runner.h" +#include "data_win.h" +#include "instance.h" +#include "renderer.h" +#include "vm.h" +#include "utils.h" +#include "json_writer.h" +#include "collision.h" + +#include +#include +#include +#include +#include +#include +#ifdef __3DS__ +#include <3ds.h> +#include "n3ds/n3ds_platform_config.h" +#endif + +#ifndef N3DS_ENABLE_TILE_LAYER_CHUNK_CACHE +#define N3DS_ENABLE_TILE_LAYER_CHUNK_CACHE 0 +#endif + +#include "debug_overlay.h" +// #include "stb_ds.h" + +#ifdef __3DS__ +void N3DSRenderer_beginBottomScreenGUIEx(Renderer* renderer, int32_t guiW, int32_t guiH, float scaleX, float scaleY, float offsetX, float offsetY); +void N3DSRenderer_beginBottomScreenGUI(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_beginBottomScreenGUIView(Renderer* renderer, int32_t guiW, int32_t guiH, int32_t viewX, int32_t viewY); +void N3DSRenderer_endBottomScreenGUI(Renderer* renderer); +void N3DSRenderer_beginBottomScreenGUI2x(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endBottomScreenGUI2x(Renderer* renderer); +void N3DSRenderer_beginTopScreenGUI(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endTopScreenGUI(Renderer* renderer); +void N3DSRenderer_beginTopScreenGUI2x(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endTopScreenGUI2x(Renderer* renderer); +bool N3DSRenderer_isTopScreenGUIActive(Renderer* renderer); +bool N3DSRenderer_isTopScreenBattleViewActive(Renderer* renderer); +void N3DSRenderer_setTopScreenBattleViewActive(Renderer* renderer, bool active); +int32_t N3DSRenderer_findTileEntryIndex(Renderer* renderer, int32_t backgroundIndex, int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH); +bool N3DSRenderer_drawCachedTileEntry(Renderer* renderer, int32_t tileEntryIndex, float drawX, float drawY, float xscale, float yscale, uint32_t color, float alpha); +int32_t N3DSRenderer_createTileLayerChunkCache(Renderer* renderer, int32_t roomWidth, int32_t roomHeight, const TileLayerRenderCache* cache); +bool N3DSRenderer_drawTileLayerChunkCache(Renderer* renderer, int32_t cacheId, float layerOffsetX, float layerOffsetY, float alpha); +void N3DSRenderer_destroyTileLayerChunkCache(Renderer* renderer, int32_t cacheId); +#endif + +#define RoomLayerType_Path 0 +#define RoomLayerType_Background 1 +#define RoomLayerType_Instances 2 +#define RoomLayerType_Assets 3 +#define RoomLayerType_Tiles 4 +#define RoomLayerType_Effect 6 +#define RoomLayerType_Path2 7 + +static uint32_t gRunnerDrawTraceCount = 0; +static int32_t gRunnerLastLoggedRoomIndex = INT32_MIN; +static int32_t gRunnerLastLoggedPendingRoom = INT32_MIN; +static int32_t gRunnerLastLoggedDrawableCount = INT32_MIN; +static int32_t gRunnerLastLoggedInstanceCount = INT32_MIN; + +static double Runner_nowMs(void) { +#ifdef __3DS__ + return (double) svcGetSystemTick() * 1000.0 / (double) SYSCLOCK_ARM11; +#else + return (double) clock() * 1000.0 / (double) CLOCKS_PER_SEC; +#endif +} + +#ifdef __3DS__ +static bool Runner_isOld3DSLike(void) { + static bool initialized = false; + static bool old3DSLike = true; + if (!initialized) { + bool isNew3DS = false; + if (R_SUCCEEDED(APT_CheckNew3DS(&isNew3DS))) { + old3DSLike = !isNew3DS; + } else { + old3DSLike = true; + } +#if N3DS_FORCE_OLD3DS_MODE + old3DSLike = true; +#endif + initialized = true; + } + return old3DSLike; +} +#endif + +static void Runner_logTiming(const char* phase, double startMs, double totalStartMs) { + double nowMs = Runner_nowMs(); + fprintf( + stderr, + "Runner load: %-28s phase=%8.2f ms total=%8.2f ms\n", + phase != NULL ? phase : "", + nowMs - startMs, + nowMs - totalStartMs + ); +} + +static int32_t findEventCodeIdAndOwner(Runner* runner, int32_t objectIndex, int32_t eventType, int32_t eventSubtype, int32_t* outOwnerObjectIndex); +static void Runner_executeResolvedEvent(Runner* runner, Instance* instance, int32_t eventType, int32_t eventSubtype, int32_t codeId, int32_t ownerObjectIndex); + +#ifdef __3DS__ +//bottom screen battlefield offset factor +static const float k3DSBottomBattleFieldScale = 1.0f; +static const float k3DSBottomBattleFieldYOffset = 0.0f; +static const float k3DSTopBattleEnemyInstanceYOffset = 112.0f; + +// Bottom-screen text/dialogue/inventory UI toggle +// Set to 0 to render text boxes on top.. or turn it off in runner menu lmao +#ifndef N3DS_ENABLE_BOTTOM_TEXT_UI +#define N3DS_ENABLE_BOTTOM_TEXT_UI 1 +#endif + +static bool Runner_stringContainsToken(const char* haystack, const char* needle) { + return haystack != NULL && needle != NULL && strstr(haystack, needle) != NULL; +} + +static bool Runner_is3DSValidObjectIndex(Runner* runner, int32_t objectIndex); +static bool Runner_objectMatches3DSNameInHierarchy(Runner* runner, int32_t objectIndex, const char* name); +static bool Runner_objectContains3DSTokenInHierarchy(Runner* runner, int32_t objectIndex, const char* needle); +static bool Runner_is3DSLiveBattleBorderObject(Runner* runner, Instance* inst); +static bool Runner_shouldHideOn3DSTopScreen(Runner* runner, Instance* inst); +static bool Runner_is3DSBattleUIObject(Runner* runner, Instance* inst); +static bool Runner_is3DSBattleBackdropInstance(Runner* runner, Instance* inst); +static bool Runner_shouldOffset3DSTopBattleInstance(Runner* runner, Instance* inst); +static bool Runner_is3DSBattleFieldObject(Runner* runner, Instance* inst); +static bool Runner_is3DSAsrielBattle(Runner* runner); +static bool Runner_is3DSUndyneBattle(Runner* runner); + +static RValue Runner_get3DSGlobal(Runner* runner, const char* name) { + if (runner == NULL || runner->vmContext == NULL || name == NULL) return RValue_makeUndefined(); + + ptrdiff_t idx = shgeti(runner->vmContext->globalVarNameMap, (char*) name); + if (idx < 0) return RValue_makeUndefined(); + + int32_t varID = runner->vmContext->globalVarNameMap[idx].value; + if ((uint32_t) varID >= runner->vmContext->globalVarCount) return RValue_makeUndefined(); + + RValue value = runner->vmContext->globalVars[varID]; + value.ownsReference = false; + return value; +} + +static double Runner_get3DSGlobalReal(Runner* runner, const char* name) { + return (double) RValue_toReal(Runner_get3DSGlobal(runner, name)); +} + +static double Runner_get3DSGlobalArrayReal(Runner* runner, const char* name, int32_t index) { + RValue arr = Runner_get3DSGlobal(runner, name); + if (arr.type != RVALUE_ARRAY || arr.array == NULL) return 0.0; + if (index < 0 || index >= GMLArray_length1D(arr.array)) return 0.0; + + RValue* slot = GMLArray_slot(arr.array, index); + if (slot == NULL) return 0.0; + return (double) RValue_toReal(*slot); +} + +static double Runner_get3DSInstanceRealVar(Runner* runner, Instance* inst, const char* name) { + if (runner == NULL || runner->vmContext == NULL || inst == NULL || name == NULL) return 0.0; + + ptrdiff_t slot = shgeti(runner->vmContext->selfVarNameMap, (char*) name); + if (slot < 0) return 0.0; + + int32_t varID = runner->vmContext->selfVarNameMap[slot].value; + RValue value = Instance_getSelfVar(inst, varID); + return (double) RValue_toReal(value); +} + +static bool Runner_is3DSDodgingBullets(Runner* runner) { + return Runner_get3DSGlobalReal(runner, "turntimer") > 0.0; +} + +static void Runner_prepare3DSDrawBattleState(Runner* runner) { + if (runner == NULL || runner->n3dsDrawBattleStateValid) return; + + runner->n3dsDrawHasTopEnemyDialogue = false; + runner->n3dsDrawAsrielBattle = false; + bool hasLiveBattleBorder = false; + bool hasActiveBattleController = false; + if (runner->instances != NULL) { + int32_t instanceCount = (int32_t) arrlen(runner->instances); + repeat(instanceCount, i) { + Instance* inst = runner->instances[i]; + if (inst == NULL || inst->destroyed || !inst->active) continue; + if (Runner_is3DSLiveBattleBorderObject(runner, inst)) { + if (inst->visible) hasLiveBattleBorder = true; + } + if (inst->visible && + Runner_is3DSValidObjectIndex(runner, inst->objectIndex) && + !Runner_is3DSBattleBackdropInstance(runner, inst)) { + if (Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "asriel")) { + runner->n3dsDrawAsrielBattle = true; + } else if (inst->spriteIndex >= 0 && (uint32_t) inst->spriteIndex < runner->dataWin->sprt.count) { + const char* spriteName = runner->dataWin->sprt.sprites[inst->spriteIndex].name; + if (Runner_stringContainsToken(spriteName, "asriel")) { + runner->n3dsDrawAsrielBattle = true; + } + } + } + if (Runner_is3DSValidObjectIndex(runner, inst->objectIndex) && + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_battlecontroller") && + ((int32_t) Runner_get3DSInstanceRealVar(runner, inst, "drawrect") == 1 || + (int32_t) Runner_get3DSInstanceRealVar(runner, inst, "drawbinfo") == 1)) { + hasActiveBattleController = true; + } + } + } + + runner->n3dsDrawBattleActive = hasLiveBattleBorder && hasActiveBattleController; + runner->n3dsDrawDodgingBullets = runner->n3dsDrawBattleActive && (Runner_get3DSGlobalReal(runner, "turntimer") > 0.0); + runner->n3dsDrawBattleStateValid = true; +} + +static bool Runner_is3DSLiveBattleBorderObject(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + if (!Runner_is3DSValidObjectIndex(runner, inst->objectIndex)) return false; + + return Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_uborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_dborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_lborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_rborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_blackborderer"); +} + +static bool Runner_is3DSBattleActive(Runner* runner) { + if (runner == NULL) return false; + if (runner->n3dsDrawBattleStateValid) return runner->n3dsDrawBattleActive; + Runner_prepare3DSDrawBattleState(runner); + return runner->n3dsDrawBattleActive; +} + +static bool Runner_is3DSValidObjectIndex(Runner* runner, int32_t objectIndex) { + return runner != NULL && + runner->dataWin != NULL && + objectIndex >= 0 && + (uint32_t) objectIndex < runner->dataWin->objt.count; +} + +static const char* Runner_get3DSObjectName(Runner* runner, int32_t objectIndex) { + if (!Runner_is3DSValidObjectIndex(runner, objectIndex)) return NULL; + return runner->dataWin->objt.objects[objectIndex].name; +} + +static bool Runner_objectMatches3DSNameInHierarchy(Runner* runner, int32_t objectIndex, const char* name) { + int32_t depth = 0; + while (Runner_is3DSValidObjectIndex(runner, objectIndex) && depth < 64) { + const char* objectName = Runner_get3DSObjectName(runner, objectIndex); + if (objectName != NULL && strcmp(objectName, name) == 0) return true; + objectIndex = runner->dataWin->objt.objects[objectIndex].parentId; + depth++; + } + return false; +} + +static bool Runner_objectContains3DSTokenInHierarchy(Runner* runner, int32_t objectIndex, const char* token) { + int32_t depth = 0; + while (Runner_is3DSValidObjectIndex(runner, objectIndex) && depth < 64) { + const char* objectName = Runner_get3DSObjectName(runner, objectIndex); + if (Runner_stringContainsToken(objectName, token)) return true; + objectIndex = runner->dataWin->objt.objects[objectIndex].parentId; + depth++; + } + return false; +} + +static bool Runner_is3DSInstanceInsideBattleField(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + + double left = Runner_get3DSGlobalArrayReal(runner, "idealborder", 0); + double right = Runner_get3DSGlobalArrayReal(runner, "idealborder", 1); + double top = Runner_get3DSGlobalArrayReal(runner, "idealborder", 2); + double bottom = Runner_get3DSGlobalArrayReal(runner, "idealborder", 3); + if (right <= left || bottom <= top) return false; + + double marginX = 24.0; + double marginY = 24.0; + double expandedLeft = left - marginX; + double expandedRight = right + marginX; + double expandedTop = top - marginY; + double expandedBottom = bottom + marginY; + + InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); + if (bbox.valid) { + double overlapLeft = fmax((double) bbox.left, expandedLeft); + double overlapRight = fmin((double) bbox.right, expandedRight); + double overlapTop = fmax((double) bbox.top, expandedTop); + double overlapBottom = fmin((double) bbox.bottom, expandedBottom); + if ((overlapRight - overlapLeft) > 4.0 && (overlapBottom - overlapTop) > 4.0) { + return true; + } + } + + double x = inst->x; + double y = inst->y; + return x >= expandedLeft && x <= expandedRight && + y >= expandedTop && y <= expandedBottom; +} + +static bool Runner_shouldCull3DSBottomBattleFieldInstance(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + return false; +} + +static bool Runner_is3DSTopEnemyDialogueObjectIndex(Runner* runner, int32_t objectIndex) { + if (!Runner_is3DSValidObjectIndex(runner, objectIndex)) return false; + + return Runner_objectContains3DSTokenInHierarchy(runner, objectIndex, "blcon") || + Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_blconsm") || + Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_blconwdflowey") || + Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_blconwideslave"); +} + +static bool Runner_is3DSTopEnemyDialogueObject(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + return Runner_is3DSTopEnemyDialogueObjectIndex(runner, inst->objectIndex); +} + +static bool Runner_is3DSBattleWriterObjectIndex(Runner* runner, int32_t objectIndex) { + if (!Runner_is3DSValidObjectIndex(runner, objectIndex)) return false; + + return Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_writer") || + Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_writer_quiz") || + Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_healwriter") || + Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "OBJ_WRITER") || + Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "OBJ_NOMSCWRITER"); +} + +static bool Runner_is3DSTextUIObjectIndex(Runner* runner, int32_t objectIndex) { + if (!Runner_is3DSValidObjectIndex(runner, objectIndex)) return false; + // Writer objects (all variants inherit from obj_base_writer.. a future issue with other games maybe) + if (Runner_is3DSBattleWriterObjectIndex(runner, objectIndex)) return true; + if (Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "OBJ_INSTAWRITER")) return true; + // Dialogue and choice objects.. this maps ALL text in the game so other games that use the same variables will have an issue uh oh + if (Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_dialoguer")) return true; + if (Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_choicer")) return true; + // Overworld game menu, inventory, and other UI + if (Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_overworldcontroller")) return true; + if (Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_gamemenu_fake")) return true; + if (Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_itemswapper")) return true; + if (Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_answernodule")) return true; + if (Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_savepoint_fake")) return true; + if (Runner_objectContains3DSTokenInHierarchy(runner, objectIndex, "songwriter")) return true; + if (Runner_objectContains3DSTokenInHierarchy(runner, objectIndex, "mettatonnn_writer")) return true; + if (Runner_objectMatches3DSNameInHierarchy(runner, objectIndex, "obj_face")) return true; + return false; +} + +static bool Runner_is3DSTextUIObject(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + // If the object is already handled by the battle UI system, exclude it from text UI to prevent double-drawing + if (Runner_is3DSBattleUIObject(runner, inst)) return false; + return Runner_is3DSTextUIObjectIndex(runner, inst->objectIndex); +} + +static bool Runner_has3DSTextUI(Runner* runner) { + if (runner == NULL || runner->instances == NULL) return false; + int32_t instanceCount = (int32_t) arrlen(runner->instances); + repeat(instanceCount, i) { + Instance* inst = runner->instances[i]; + if (inst == NULL || inst->destroyed || !inst->active || !inst->visible) continue; + if (Runner_is3DSTextUIObject(runner, inst)) return true; + } + return false; +} + +static void Runner_prepare3DSTextUIState(Runner* runner) { + if (runner == NULL || runner->n3dsDrawTextUIStateValid) return; + runner->n3dsDrawTextUIActive = false; + if (!N3DS_ENABLE_BOTTOM_TEXT_UI) { runner->n3dsDrawTextUIStateValid = true; return; } + runner->n3dsDrawTextUIActive = Runner_has3DSTextUI(runner); + runner->n3dsDrawTextUIStateValid = true; +} + +static bool Runner_is3DSTextUIObjectCheck(Runner* runner, Instance* inst) { + if (!N3DS_ENABLE_BOTTOM_TEXT_UI) return false; + if (runner == NULL || inst == NULL) return false; + Runner_prepare3DSTextUIState(runner); + return runner->n3dsDrawTextUIActive && Runner_is3DSTextUIObject(runner, inst); +} + +static void Runner_prepare3DSTextUIList(Runner* runner, Drawable* drawables, int32_t drawableCount) { + if (runner == NULL || runner->n3dsTextUIListValid) return; + arrsetlen(runner->n3dsTextUIInstances, 0); + Runner_prepare3DSTextUIState(runner); + if (!runner->n3dsDrawTextUIActive) { runner->n3dsTextUIListValid = true; return; } + repeat(drawableCount, i) { + Drawable* d = &drawables[i]; + if (d->type != DRAWABLE_INSTANCE) continue; + Instance* inst = d->instance; + if (inst == NULL || !inst->active || !inst->visible) continue; + if (Runner_is3DSTextUIObject(runner, inst)) { + arrput(runner->n3dsTextUIInstances, inst); + } + } + runner->n3dsTextUIListValid = true; +} + +static bool Runner_has3DSTopEnemyDialogue(Runner* runner, Drawable* drawables, int32_t drawableCount) { + if (runner == NULL || drawables == NULL || drawableCount <= 0) return false; + + repeat(drawableCount, i) { + Drawable* d = &drawables[i]; + if (d->type != DRAWABLE_INSTANCE) continue; + + Instance* inst = d->instance; + if (inst == NULL || !inst->active || !inst->visible) continue; + if (Runner_is3DSTopEnemyDialogueObject(runner, inst)) return true; + } + + return false; +} + +static bool Runner_is3DSBattleBackdropInstance(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + + if (Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_battlebg") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "battlebg") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "gridfriend")) { + return true; + } + + if (inst->spriteIndex < 0 || (uint32_t) inst->spriteIndex >= runner->dataWin->sprt.count) { + return false; + } + + Sprite* sprite = &runner->dataWin->sprt.sprites[inst->spriteIndex]; + const char* spriteName = sprite->name; + if (Runner_stringContainsToken(spriteName, "battlebg") || + Runner_stringContainsToken(spriteName, "gridfriend")) { + return true; + } + + // Treat room-sized battle backdrops as non-enemy scenery so the enemy-only + // top-screen offset doesn't drag the whole scene downward. + return sprite->width >= 320 || sprite->height >= 240; +} + +static bool Runner_shouldOffset3DSTopBattleInstance(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + Runner_prepare3DSDrawBattleState(runner); + if (!runner->n3dsDrawBattleActive) return false; + if (Runner_is3DSAsrielBattle(runner)) return false; + if (Runner_shouldHideOn3DSTopScreen(runner, inst)) return false; + if (Runner_is3DSBattleBackdropInstance(runner, inst)) return false; + if (Runner_is3DSTopEnemyDialogueObject(runner, inst)) return false; + if (Runner_is3DSBattleWriterObjectIndex(runner, inst->objectIndex)) return false; + return true; +} + +static float Runner_get3DSTopBattleInstanceYOffset(Runner* runner, Instance* inst) { + if (inst == NULL) return 0.0f; + if (Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "asgoreb") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "asgorespear")) { + return 0.0f; + } + return k3DSTopBattleEnemyInstanceYOffset; +} + +static bool Runner_is3DSBattleUIObject(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + if (!Runner_is3DSValidObjectIndex(runner, inst->objectIndex)) return false; + Runner_prepare3DSDrawBattleState(runner); + if (!runner->n3dsDrawBattleActive) return false; + if (Runner_is3DSTopEnemyDialogueObjectIndex(runner, inst->objectIndex)) return false; + if (runner->n3dsDrawHasTopEnemyDialogue && Runner_is3DSBattleWriterObjectIndex(runner, inst->objectIndex)) return false; + + const char* objectName = Runner_get3DSObjectName(runner, inst->objectIndex); + if (objectName == NULL) return false; + + if ( + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_battlecontroller") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_blackborderer") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_uborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_dborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_lborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_rborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_fightbt") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_itembt") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_sparebt") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_talkbt") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_anybt") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_hpname") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_dbulletcontroller") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_sinefi") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_tensionbar") || + Runner_is3DSBattleWriterObjectIndex(runner, inst->objectIndex)) { + return true; + } + + if (Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "border") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "bullet") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "heart") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "soul")) { + return true; + } + + if (inst->spriteIndex >= 0 && (uint32_t) inst->spriteIndex < runner->dataWin->sprt.count) { + const char* spriteName = runner->dataWin->sprt.sprites[inst->spriteIndex].name; + if (Runner_stringContainsToken(spriteName, "bullet") || + Runner_stringContainsToken(spriteName, "heart") || + Runner_stringContainsToken(spriteName, "soul")) { + return true; + } + } + + if (Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "blt") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_asgore_spearswipe") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_asgore_spearswipegen") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_asgorebulparent") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_asgoreattackgen") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_asgore_firehit") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_asgorefakespear") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "menubone") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "bonestab") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "bonewall") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "boneplat") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "boneloop") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "sans_bonebul") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "discoball") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "legline") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "sidedam") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "woshspiral") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "blackbox_rewind") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "answernode") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "target") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "eyeflash")) { + return true; + } + + if (runner->n3dsDrawDodgingBullets && Runner_is3DSInstanceInsideBattleField(runner, inst)) { + return true; + } + + return false; +} + +static bool Runner_is3DSBattleFieldObject(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + if (!Runner_is3DSValidObjectIndex(runner, inst->objectIndex)) return false; + Runner_prepare3DSDrawBattleState(runner); + if (!runner->n3dsDrawBattleActive) return false; + bool isDodgingBullets = runner->n3dsDrawDodgingBullets; + + if ((isDodgingBullets && + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_battlecontroller")) || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_blackborderer") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_uborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_dborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_lborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_rborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_dbulletcontroller") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_sinefi")) { + return true; + } + + if (Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "border") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "bullet") || + (isDodgingBullets && ( + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "heart") || + Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "soul")))) { + return true; + } + + if (inst->spriteIndex >= 0 && (uint32_t) inst->spriteIndex < runner->dataWin->sprt.count) { + const char* spriteName = runner->dataWin->sprt.sprites[inst->spriteIndex].name; + if (Runner_stringContainsToken(spriteName, "bullet") || + (isDodgingBullets && ( + Runner_stringContainsToken(spriteName, "heart") || + Runner_stringContainsToken(spriteName, "soul")))) { + return true; + } + } + + if (isDodgingBullets && Runner_is3DSInstanceInsideBattleField(runner, inst)) { + return true; + } + + return false; +} + +static bool Runner_is3DSBattleFieldBackLayerObject(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + if (!Runner_is3DSValidObjectIndex(runner, inst->objectIndex)) return false; + + if (Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_battlecontroller") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_blackborderer") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_uborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_dborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_lborder") || + Runner_objectMatches3DSNameInHierarchy(runner, inst->objectIndex, "obj_rborder")) { + return true; + } + + if (Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "border")) { + return true; + } + + if (inst->spriteIndex >= 0 && (uint32_t) inst->spriteIndex < runner->dataWin->sprt.count) { + const char* spriteName = runner->dataWin->sprt.sprites[inst->spriteIndex].name; + if (Runner_stringContainsToken(spriteName, "border")) { + return true; + } + } + + return false; +} + +static bool Runner_is3DSAsrielBattle(Runner* runner) { + if (runner == NULL) return false; + Runner_prepare3DSDrawBattleState(runner); + return runner->n3dsDrawBattleActive && runner->n3dsDrawAsrielBattle; +} + +static bool Runner_is3DSUndyneBattle(Runner* runner) { + if (runner == NULL) return false; + Runner_prepare3DSDrawBattleState(runner); + if (!runner->n3dsDrawBattleActive) return false; + if (runner->instances == NULL) return false; + + int32_t instanceCount = (int32_t) arrlen(runner->instances); + repeat(instanceCount, i) { + Instance* inst = runner->instances[i]; + if (inst == NULL || inst->destroyed || !inst->active || !inst->visible) continue; + if (!Runner_is3DSValidObjectIndex(runner, inst->objectIndex)) continue; + if (Runner_is3DSBattleBackdropInstance(runner, inst)) continue; + + if (Runner_objectContains3DSTokenInHierarchy(runner, inst->objectIndex, "undyne")) { + return true; + } + + if (inst->spriteIndex >= 0 && (uint32_t) inst->spriteIndex < runner->dataWin->sprt.count) { + const char* spriteName = runner->dataWin->sprt.sprites[inst->spriteIndex].name; + if (Runner_stringContainsToken(spriteName, "undyne")) { + return true; + } + } + } + + return false; +} + +static void Runner_prepare3DSBattleReplayLists(Runner* runner, Drawable* drawables, int32_t drawableCount) { + if (runner == NULL || runner->n3dsBattleReplayListsValid) return; + + arrsetlen(runner->n3dsBattleFieldInstances, 0); + arrsetlen(runner->n3dsBattleUIInstances, 0); + Runner_prepare3DSDrawBattleState(runner); + if (!runner->n3dsDrawBattleActive) { + runner->n3dsBattleReplayListsValid = true; + return; + } + bool hasTopEnemyDialogue = Runner_has3DSTopEnemyDialogue(runner, drawables, drawableCount); + runner->n3dsDrawHasTopEnemyDialogue = hasTopEnemyDialogue; + + repeat(drawableCount, i) { + Drawable* d = &drawables[i]; + if (d->type != DRAWABLE_INSTANCE) continue; + + Instance* inst = d->instance; + if (inst == NULL || !inst->active || !inst->visible) continue; + + if (Runner_is3DSBattleFieldObject(runner, inst) && + Runner_is3DSBattleFieldBackLayerObject(runner, inst)) { + arrput(runner->n3dsBattleFieldInstances, inst); + } + } + + repeat(drawableCount, i) { + Drawable* d = &drawables[i]; + if (d->type != DRAWABLE_INSTANCE) continue; + + Instance* inst = d->instance; + if (inst == NULL || !inst->active || !inst->visible) continue; + + bool isBattleFieldObject = Runner_is3DSBattleFieldObject(runner, inst); + bool isBattleUIObject = Runner_is3DSBattleUIObject(runner, inst); + if (isBattleFieldObject && !Runner_is3DSBattleFieldBackLayerObject(runner, inst)) { + arrput(runner->n3dsBattleFieldInstances, inst); + } + if (hasTopEnemyDialogue && Runner_is3DSBattleWriterObjectIndex(runner, inst->objectIndex)) { + isBattleUIObject = false; + } + if (isBattleUIObject && !isBattleFieldObject) arrput(runner->n3dsBattleUIInstances, inst); + } + + runner->n3dsBattleReplayListsValid = true; +} + +static int32_t Runner_get3DSGUIResponderCacheIndex(int32_t subtype) { + switch (subtype) { + case DRAW_GUI_BEGIN: return 0; + case DRAW_GUI: return 1; + case DRAW_GUI_END: return 2; + default: return -1; + } +} + +static void Runner_prepare3DSTopScreenGUIInstanceList(Runner* runner, Drawable* drawables, int32_t drawableCount) { + if (runner == NULL || runner->n3dsTopScreenGUIListValid) return; + + arrsetlen(runner->n3dsTopScreenGUIInstances, 0); + Runner_prepare3DSDrawBattleState(runner); + runner->n3dsDrawHasTopEnemyDialogue = Runner_has3DSTopEnemyDialogue(runner, drawables, drawableCount); + + repeat(drawableCount, i) { + Drawable* d = &drawables[i]; + if (d->type != DRAWABLE_INSTANCE) continue; + + Instance* inst = d->instance; + if (inst == NULL || !inst->active || !inst->visible) continue; + if (runner->n3dsDrawBattleActive && Runner_shouldHideOn3DSTopScreen(runner, inst)) continue; + arrput(runner->n3dsTopScreenGUIInstances, inst); + } + + runner->n3dsTopScreenGUIListValid = true; +} + +static N3DSResolvedDrawEvent* Runner_prepare3DSTopScreenGUIResponderList(Runner* runner, Drawable* drawables, int32_t drawableCount, int32_t subtype, int32_t slot) { + int32_t cacheIndex = Runner_get3DSGUIResponderCacheIndex(subtype); + if (runner == NULL || cacheIndex < 0 || slot < 0) return nullptr; + + if (!runner->n3dsTopScreenGUIResponderListsValid[cacheIndex]) { + Runner_prepare3DSTopScreenGUIInstanceList(runner, drawables, drawableCount); + bool hasTopEnemyDialogue = Runner_has3DSTopEnemyDialogue(runner, drawables, drawableCount); + arrsetlen(runner->n3dsTopScreenGUIResponderEvents[cacheIndex], 0); + + int32_t instanceCount = (int32_t) arrlen(runner->n3dsTopScreenGUIInstances); + repeat(instanceCount, i) { + Instance* inst = runner->n3dsTopScreenGUIInstances[i]; + if (inst == NULL || !inst->active || !inst->visible) continue; + if (runner->n3dsDrawBattleActive && + !Runner_is3DSTopEnemyDialogueObject(runner, inst) && + !(hasTopEnemyDialogue && Runner_is3DSBattleWriterObjectIndex(runner, inst->objectIndex))) { + continue; + } + + int32_t ownerObjectIndex = -1; + int32_t codeId = ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, slot, &ownerObjectIndex); + if (0 > codeId) continue; + + N3DSResolvedDrawEvent resolvedEvent = { + .instance = inst, + .codeId = codeId, + .ownerObjectIndex = ownerObjectIndex, + }; + arrput(runner->n3dsTopScreenGUIResponderEvents[cacheIndex], resolvedEvent); + } + + runner->n3dsTopScreenGUIResponderListsValid[cacheIndex] = true; + } + + return runner->n3dsTopScreenGUIResponderEvents[cacheIndex]; +} + +static void Runner_draw3DSBottomBattleUI(Runner* runner, Drawable* drawables, int32_t drawableCount, int32_t subtype, bool drawBattleFieldOnly, float scale, float yOffset) { + if (runner == NULL || runner->renderer == NULL) return; +#ifdef N3DS_DISABLE_BOTTOM_SCREEN + (void) drawables; + (void) drawableCount; + (void) subtype; + (void) drawBattleFieldOnly; + (void) scale; + (void) yOffset; + return; +#endif + int32_t slot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_DRAW, subtype); + if (subtype != DRAW_NORMAL && slot < 0) return; + if (Runner_is3DSAsrielBattle(runner)) return; + Runner_prepare3DSBattleReplayLists(runner, drawables, drawableCount); + if (!runner->n3dsDrawBattleActive) return; + Instance** instances = drawBattleFieldOnly ? runner->n3dsBattleFieldInstances : runner->n3dsBattleUIInstances; + int32_t instanceCount = (int32_t) arrlen(instances); + if (instanceCount <= 0) return; + + int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : (int32_t) runner->dataWin->gen8.defaultWindowWidth; + int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : (int32_t) runner->dataWin->gen8.defaultWindowHeight; + if (guiW <= 0) guiW = 320; + if (guiH <= 0) guiH = 240; + + if (scale == 1.0f && yOffset == 0.0f) { + N3DSRenderer_beginBottomScreenGUI(runner->renderer, guiW, guiH); + } else { + N3DSRenderer_beginBottomScreenGUIEx(runner->renderer, guiW, guiH, scale, scale, 0.0f, yOffset); + } + repeat(instanceCount, i) { + Instance* inst = instances[i]; + if (inst == NULL || !inst->active || !inst->visible) continue; + if (drawBattleFieldOnly && subtype == DRAW_NORMAL && Runner_shouldCull3DSBottomBattleFieldInstance(runner, inst)) { + continue; + } + int32_t ownerObjectIndex = -1; + int32_t codeId = slot >= 0 + ? ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, slot, &ownerObjectIndex) + : -1; + if (codeId >= 0) { + Runner_executeResolvedEvent(runner, inst, EVENT_DRAW, subtype, codeId, ownerObjectIndex); + } else { + if (subtype == DRAW_NORMAL) { + Renderer_drawSelf(runner->renderer, inst); + } + } + } + N3DSRenderer_endBottomScreenGUI(runner->renderer); +} + +static void Runner_draw3DSBottomTextUI(Runner* runner, Drawable* drawables, int32_t drawableCount, int32_t subtype, bool drawBattleFieldOnly, float scale, float yOffset) { + if (runner == NULL || runner->renderer == NULL) return; +#ifdef N3DS_DISABLE_BOTTOM_SCREEN + (void) drawables; + (void) drawableCount; + (void) subtype; + (void) drawBattleFieldOnly; + (void) scale; + (void) yOffset; + return; +#endif + if (!N3DS_ENABLE_BOTTOM_TEXT_UI) return; + (void) drawBattleFieldOnly; + int32_t slot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_DRAW, subtype); + if (subtype != DRAW_NORMAL && slot < 0) return; + Runner_prepare3DSTextUIList(runner, drawables, drawableCount); + if (!runner->n3dsDrawTextUIActive) return; + Instance** instances = runner->n3dsTextUIInstances; + int32_t instanceCount = (int32_t) arrlen(instances); + if (instanceCount <= 0) return; + + int32_t viewIndex = runner->viewCurrent; + int32_t guiW = runner->views[viewIndex].viewWidth; + int32_t guiH = runner->views[viewIndex].viewHeight; + if (guiW <= 0) guiW = 320; + if (guiH <= 0) guiH = 240; + int32_t viewX = runner->views[viewIndex].viewX; + int32_t viewY = runner->views[viewIndex].viewY; + + if (scale == 1.0f && yOffset == 0.0f) { + N3DSRenderer_beginBottomScreenGUIView(runner->renderer, guiW, guiH, viewX, viewY); + } else { + N3DSRenderer_beginBottomScreenGUIEx(runner->renderer, guiW, guiH, scale, scale, 0.0f, yOffset); + } + repeat(instanceCount, i) { + Instance* inst = instances[i]; + if (inst == NULL || !inst->active || !inst->visible) continue; + int32_t ownerObjectIndex = -1; + int32_t codeId = slot >= 0 + ? ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, slot, &ownerObjectIndex) + : -1; + if (codeId >= 0) { + Runner_executeResolvedEvent(runner, inst, EVENT_DRAW, subtype, codeId, ownerObjectIndex); + } else { + if (subtype == DRAW_NORMAL) { + Renderer_drawSelf(runner->renderer, inst); + } + } + } + N3DSRenderer_endBottomScreenGUI(runner->renderer); +} + +static bool Runner_shouldHideOn3DSTopScreen(Runner* runner, Instance* inst) { + if (runner == NULL || runner->renderer == NULL || inst == NULL) return false; + if (Runner_is3DSAsrielBattle(runner)) return false; + if (Runner_is3DSBattleUIObject(runner, inst)) return true; + if (Runner_is3DSTextUIObjectCheck(runner, inst)) return true; + return false; +} +#endif + +// ===[ Runtime Layer Teardown Helpers ]=== +void Runner_freeRuntimeLayer(RuntimeLayer* runtimeLayer) { + if (runtimeLayer->dynamicName != nullptr) { + free(runtimeLayer->dynamicName); + runtimeLayer->dynamicName = nullptr; + } + size_t elementCount = arrlenu(runtimeLayer->elements); + for (size_t i = 0; i < elementCount; i++) { + RuntimeLayerElement* el = &runtimeLayer->elements[i]; + if (el->backgroundElement != nullptr) { + free(el->backgroundElement); + el->backgroundElement = nullptr; + } + if (el->spriteElement != nullptr) { + free(el->spriteElement); + el->spriteElement = nullptr; + } + } + arrfree(runtimeLayer->elements); + runtimeLayer->elements = nullptr; +} + +static void freeRuntimeLayersArray(RuntimeLayer** runtimeLayerArray) { + size_t count = arrlenu(*runtimeLayerArray); + repeat(count, i) { + Runner_freeRuntimeLayer(&(*runtimeLayerArray)[i]); + } + arrfree(*runtimeLayerArray); + *runtimeLayerArray = nullptr; +} + +// ===[ Helper: Find event action in object hierarchy ]=== +// Resolves the handler for (objectIndex, eventType, eventSubtype) via the precomputed ResolvedEventTable. +// Returns the CODE chunk handler id, or -1 if the object does not respond. +// If outOwnerObjectIndex is non-null, it is set to the resolved owner objectIndex (-1 if not found). +static int32_t findEventCodeIdAndOwner(Runner* runner, int32_t objectIndex, int32_t eventType, int32_t eventSubtype, int32_t* outOwnerObjectIndex) { + int32_t slot = EventSlotMap_lookup(&runner->eventSlotMap, eventType, eventSubtype); + if (0 > slot) { + if (outOwnerObjectIndex != nullptr) *outOwnerObjectIndex = -1; + return -1; + } + return ResolvedEventTable_lookup(&runner->eventTable, objectIndex, slot, outOwnerObjectIndex); +} + +// ===[ Per-Object Instance Lists ]=== +// Each instance lives in the list of its own object and every ancestor object (descendant-inclusive). +// This mirrors the native runner and lets collision dispatch iterate only the candidate instances for a target object, instead of scanning the whole room per collision event. +// The difference is that the native runner uses a linked list, while we move things manually with memmove. + +void Runner_addInstanceToObjectLists(Runner* runner, Instance* inst) { + DataWin* dataWin = runner->dataWin; + int32_t currentObj = inst->objectIndex; + int32_t depth = 0; + while (currentObj >= 0 && dataWin->objt.count > (uint32_t) currentObj && 32 > depth) { + arrput(runner->instancesByObject[currentObj], inst); + currentObj = dataWin->objt.objects[currentObj].parentId; + depth++; + } + if (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) { + arrput(runner->instancesByExactObject[inst->objectIndex], inst); + } + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); +} + +// Stable remove of inst from list, preserving creation order. Returns true if removed. +static bool removeInstanceFromList(Instance*** listPtr, Instance* inst) { + Instance** list = *listPtr; + int32_t n = (int32_t) arrlen(list); + repeat(n, i) { + if (list[i] == inst) { + if (n - 1 > i) memmove(&list[i], &list[i + 1], (size_t) (n - 1 - i) * sizeof(Instance*)); + arrsetlen(*listPtr, n - 1); + return true; + } + } + return false; +} + +void Runner_removeInstanceFromObjectLists(Runner* runner, Instance* inst) { + DataWin* dataWin = runner->dataWin; + int32_t currentObj = inst->objectIndex; + int32_t depth = 0; + while (currentObj >= 0 && dataWin->objt.count > (uint32_t) currentObj && 32 > depth) { + removeInstanceFromList(&runner->instancesByObject[currentObj], inst); + currentObj = dataWin->objt.objects[currentObj].parentId; + depth++; + } + if (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) { + removeInstanceFromList(&runner->instancesByExactObject[inst->objectIndex], inst); + } + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); +} + +void Runner_clearAllObjectLists(Runner* runner) { + if (runner->instancesByObject == nullptr) return; + uint32_t count = runner->dataWin->objt.count; + repeat(count, i) { + arrsetlen(runner->instancesByObject[i], 0); + if (runner->instancesByExactObject != nullptr) { + arrsetlen(runner->instancesByExactObject[i], 0); + } + } +} + +int32_t Runner_pushInstancesOfObject(Runner* runner, int32_t targetObjIndex) { + int32_t base = (int32_t) arrlen(runner->instanceSnapshots); + + if (0 > targetObjIndex || (uint32_t) targetObjIndex >= runner->dataWin->objt.count) + return base; + + Instance** source = runner->instancesByObject[targetObjIndex]; + int32_t sourceCount = (int32_t) arrlen(source); + + if (0 >= sourceCount) + return base; + + arrsetlen(runner->instanceSnapshots, base + sourceCount); + memcpy(&runner->instanceSnapshots[base], source, (size_t) sourceCount * sizeof(Instance*)); + return base; +} + +void Runner_popInstanceSnapshot(Runner* runner, int32_t base) { + arrsetlen(runner->instanceSnapshots, base); +} + +int32_t Runner_pushInstancesForTarget(Runner* runner, int32_t target) { + int32_t base = (int32_t) arrlen(runner->instanceSnapshots); + if (target >= 0 && 100000 > target) { + return Runner_pushInstancesOfObject(runner, target); + } + if (target == INSTANCE_ALL) { + int32_t total = (int32_t) arrlen(runner->instances); + if (0 >= total) + return base; + arrsetlen(runner->instanceSnapshots, base + total); + memcpy(&runner->instanceSnapshots[base], runner->instances, (size_t) total * sizeof(Instance*)); + return base; + } + if (target >= 100000) { + Instance* inst = hmget(runner->instancesById, target); + if (inst != nullptr) arrput(runner->instanceSnapshots, inst); + return base; + } + return base; +} + +// ===[ Event Execution ]=== + +static void setVMInstanceContext(VMContext* vm, Instance* instance) { + vm->currentInstance = instance; +} + +static void restoreVMInstanceContext(VMContext* vm, Instance* savedInstance) { + vm->currentInstance = savedInstance; +} + +static void executeCode(Runner* runner, Instance* instance, int32_t codeId) { + // GameMaker does use codeIds less than 0, we'll just pretend we didn't hear them... + if (0 > codeId) return; + + VMContext* vm = runner->vmContext; + + // Save instance context + Instance* savedInstance = (Instance*) vm->currentInstance; + + // Save full VM execution state, because VM_executeCode overwrites all of these. + // This is necessary for nested execution (e.g., instance_create triggering a Create + // event while another event's executeLoop is still on the call stack). + uint8_t* savedBytecodeBase = vm->bytecodeBase; + uint32_t savedIP = vm->ip; + uint32_t savedCodeEnd = vm->codeEnd; + const char* savedCodeName = vm->currentCodeName; + RValue* savedLocalVars = vm->localVars; + uint32_t savedLocalVarCount = vm->localVarCount; + IntIntHashMap* savedCodeLocalsSlotMap = vm->currentCodeLocalsSlotMap; + int32_t savedCodeIndex = vm->currentCodeIndex; + int32_t savedStackTop = vm->stack.top; + + // Save stack values (VM_executeCode resets stack.top to 0, which would let + // the nested execution overwrite the caller's stack slot values) + RValue* savedStackValues = nullptr; + if (savedStackTop > 0) { + savedStackValues = safeMalloc((uint32_t) savedStackTop * sizeof(RValue)); + memcpy(savedStackValues, vm->stack.slots, (uint32_t) savedStackTop * sizeof(RValue)); + } + + // Set instance context + setVMInstanceContext(vm, instance); + + // Execute + RValue result = VM_executeCode(vm, codeId); + RValue_free(&result); + + // Restore instance context + restoreVMInstanceContext(vm, savedInstance); + + // Restore VM execution state + vm->bytecodeBase = savedBytecodeBase; + vm->ip = savedIP; + vm->codeEnd = savedCodeEnd; + vm->currentCodeName = savedCodeName; + vm->localVars = savedLocalVars; + vm->localVarCount = savedLocalVarCount; + vm->currentCodeLocalsSlotMap = savedCodeLocalsSlotMap; + vm->currentCodeIndex = savedCodeIndex; + vm->stack.top = savedStackTop; + + // Restore stack values + if (savedStackTop > 0) { + memcpy(vm->stack.slots, savedStackValues, (uint32_t) savedStackTop * sizeof(RValue)); + free(savedStackValues); + } +} + +const char* Runner_getEventName(int32_t eventType, int32_t eventSubtype) { + switch (eventType) { + case EVENT_CREATE: return "Create"; + case EVENT_DESTROY: return "Destroy"; + case EVENT_ALARM: return "Alarm"; + case EVENT_COLLISION: return "Collision"; + case EVENT_STEP: + switch (eventSubtype) { + case STEP_BEGIN: return "BeginStep"; + case STEP_NORMAL: return "NormalStep"; + case STEP_END: return "EndStep"; + default: return "Step"; + } + case EVENT_DRAW: + switch (eventSubtype) { + case DRAW_NORMAL: return "Draw"; + case DRAW_GUI: return "DrawGUI"; + case DRAW_BEGIN: return "DrawBegin"; + case DRAW_END: return "DrawEnd"; + case DRAW_GUI_BEGIN: return "DrawGUIBegin"; + case DRAW_GUI_END: return "DrawGUIEnd"; + case DRAW_PRE: return "DrawPre"; + case DRAW_POST: return "DrawPost"; + default: return "Draw"; + } + case EVENT_KEYBOARD: return "Keyboard"; + case EVENT_OTHER: + switch (eventSubtype) { + case OTHER_OUTSIDE_ROOM: return "OutsideRoom"; + case OTHER_GAME_START: return "GameStart"; + case OTHER_ROOM_START: return "RoomStart"; + case OTHER_ROOM_END: return "RoomEnd"; + case OTHER_END_OF_PATH: return "EndOfPath"; + case OTHER_USER0 + 0: return "UserEvent0"; + case OTHER_USER0 + 1: return "UserEvent1"; + case OTHER_USER0 + 2: return "UserEvent2"; + case OTHER_USER0 + 3: return "UserEvent3"; + case OTHER_USER0 + 4: return "UserEvent4"; + case OTHER_USER0 + 5: return "UserEvent5"; + case OTHER_USER0 + 6: return "UserEvent6"; + case OTHER_USER0 + 7: return "UserEvent7"; + case OTHER_USER0 + 8: return "UserEvent8"; + case OTHER_USER0 + 9: return "UserEvent9"; + case OTHER_USER0 + 10: return "UserEvent10"; + case OTHER_USER0 + 11: return "UserEvent11"; + case OTHER_USER0 + 12: return "UserEvent12"; + case OTHER_USER0 + 13: return "UserEvent13"; + case OTHER_USER0 + 14: return "UserEvent14"; + case OTHER_USER0 + 15: return "UserEvent15"; + default: return "Other"; + } + case EVENT_KEYPRESS: return "KeyPress"; + case EVENT_KEYRELEASE: return "KeyRelease"; + case EVENT_PRECREATE: return "PreCreate"; + default: return "Unknown"; + } +} + +// Executes an already-resolved event handler (see findEventCodeIdAndOwner) and verified codeId >= 0. +static void Runner_executeResolvedEvent(Runner* runner, Instance* instance, int32_t eventType, int32_t eventSubtype, int32_t codeId, int32_t ownerObjectIndex) { + VMContext* vm = runner->vmContext; + int32_t savedEventType = vm->currentEventType; + int32_t savedEventSubtype = vm->currentEventSubtype; + int32_t savedEventObjectIndex = vm->currentEventObjectIndex; + + vm->currentEventType = eventType; + vm->currentEventSubtype = eventSubtype; + vm->currentEventObjectIndex = ownerObjectIndex; + +#ifdef ENABLE_VM_TRACING + if (codeId >= 0 && shlen(vm->eventsToBeTraced) != -1) { + const char* eventName = Runner_getEventName(eventType, eventSubtype); + const char* objectName = runner->dataWin->objt.objects[instance->objectIndex].name; + + bool shouldTrace = shgeti(vm->eventsToBeTraced, "*") != -1 || shgeti(vm->eventsToBeTraced, eventName) != -1 || shgeti(vm->eventsToBeTraced, objectName) != -1; + + if (shouldTrace) { + if (eventType == EVENT_ALARM) { + fprintf(stderr, "Runner: [%s] %s %d (instanceId=%d)\n", objectName, eventName, eventSubtype, instance->instanceId); + } else { + fprintf(stderr, "Runner: [%s] %s (instanceId=%d)\n", objectName, eventName, instance->instanceId); + } + } + } +#endif + + executeCode(runner, instance, codeId); + + vm->currentEventType = savedEventType; + vm->currentEventSubtype = savedEventSubtype; + vm->currentEventObjectIndex = savedEventObjectIndex; +} + +void Runner_executeEventFromObject(Runner* runner, Instance* instance, int32_t startObjectIndex, int32_t eventType, int32_t eventSubtype) { + int32_t ownerObjectIndex = -1; + int32_t codeId = findEventCodeIdAndOwner(runner, startObjectIndex, eventType, eventSubtype, &ownerObjectIndex); + // Fast path: If the codeId is invalid, let's bail out fast + // This way can avoid the need of loading and saving the current state variables + if (0 > codeId) + return; + Runner_executeResolvedEvent(runner, instance, eventType, eventSubtype, codeId, ownerObjectIndex); +} + +void Runner_executeEvent(Runner* runner, Instance* instance, int32_t eventType, int32_t eventSubtype) { + Runner_executeEventFromObject(runner, instance, instance->objectIndex, eventType, eventSubtype); +} + +// Events that GMS 2.3+ routes through the per-object obj_has_event table instead of Perform_Event_All. +// Anything else (BC16 ALWAYS; BC17 non-perObject) goes through Runner_executeEventForAll +static bool eventUsesBC17PerObjectDispatch(int32_t eventType) { + return eventType == EVENT_STEP || eventType == EVENT_ALARM || eventType == EVENT_KEYBOARD || eventType == EVENT_KEYPRESS || eventType == EVENT_KEYRELEASE; +} + +void Runner_executeEventForAll(Runner* runner, int32_t eventType, int32_t eventSubtype) { + int32_t slot = EventSlotMap_lookup(&runner->eventSlotMap, eventType, eventSubtype); + if (slot == -1) return; + bool profileStepNormal = (eventType == EVENT_STEP && eventSubtype == STEP_NORMAL); + + // We always snapshot the iteration list before dispatching so instances spawned during this phase do NOT fire the current event. + Instance** scratch = runner->eventDispatchInstances; + arrsetlen(scratch, 0); + + // On GMS 2.x, the native runner dispatches events in the eventUsesPerObjectDispatch set per-object. Route those through executeEventPerObject to match. + if (DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0) && eventUsesBC17PerObjectDispatch(eventType)) { + ResolvedEventTable* table = &runner->eventTable; + uint32_t entryCount; + SlotResponderEntry* entries = ResolvedEventTable_slotEntries(table, slot, &entryCount); + if (entryCount == 0) return; + + repeat(entryCount, i) { + int32_t concreteObj = entries[i].concreteObjectId; + Instance** bucket = runner->instancesByExactObject[concreteObj]; + int32_t bucketCount = (int32_t) arrlen(bucket); + if (bucketCount == 0) continue; + + int32_t ownerObjectIndex = -1; + int32_t codeId = ResolvedEventTable_lookup(table, concreteObj, slot, &ownerObjectIndex); + if (codeId < 0) continue; + + int32_t snapshotBase = (int32_t) arrlen(runner->instanceSnapshots); + arrsetlen(runner->instanceSnapshots, snapshotBase + bucketCount); + memcpy(&runner->instanceSnapshots[snapshotBase], bucket, (size_t) bucketCount * sizeof(Instance*)); + + repeat(bucketCount, j) { + Instance* inst = runner->instanceSnapshots[snapshotBase + j]; + if (!inst->active) continue; + double execStartMs = profileStepNormal ? Runner_nowMs() : 0.0; + Runner_executeResolvedEvent(runner, inst, eventType, eventSubtype, codeId, ownerObjectIndex); + if (profileStepNormal && inst->objectIndex >= 0 && (uint32_t) inst->objectIndex < runner->dataWin->objt.count) { + runner->frameStepObjectMsByObject[inst->objectIndex] += Runner_nowMs() - execStartMs; + runner->frameStepObjectCallsByObject[inst->objectIndex]++; + } + } + + arrsetlen(runner->instanceSnapshots, snapshotBase); + } + return; + } + + int32_t count = (int32_t) arrlen(runner->instances); + if (count == 0) return; + arrsetlen(scratch, count); + memcpy(scratch, runner->instances, (size_t) count * sizeof(Instance*)); + runner->eventDispatchInstances = scratch; + + repeat(count, i) { + Instance* inst = scratch[i]; + if (!inst->active) continue; + // Skip non-responders without entering Runner_executeEvent. ResolvedEventTable_lookup is a tiny CSR scan; non-responders bail in a few compares and avoid the VM state save/restore overhead inside Runner_executeEventFromObject. + int32_t ownerObjectIndex = -1; + int32_t codeId = ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, slot, &ownerObjectIndex); + if (0 > codeId) continue; + double execStartMs = profileStepNormal ? Runner_nowMs() : 0.0; + Runner_executeResolvedEvent(runner, inst, eventType, eventSubtype, codeId, ownerObjectIndex); + if (profileStepNormal && inst->objectIndex >= 0 && (uint32_t) inst->objectIndex < runner->dataWin->objt.count) { + runner->frameStepObjectMsByObject[inst->objectIndex] += Runner_nowMs() - execStartMs; + runner->frameStepObjectCallsByObject[inst->objectIndex]++; + } + } +} + +// ===[ Background Scrolling & Drawing ]=== + +void Runner_scrollBackgrounds(Runner* runner) { + repeat(8, i) { + RuntimeBackground* bg = &runner->backgrounds[i]; + if (!bg->visible) continue; + bg->x += bg->speedX; + bg->y += bg->speedY; + } +} + +void Runner_drawBackgrounds(Runner* runner, bool foreground) { + if (runner->renderer == nullptr) return; + double startMs = Runner_nowMs(); + DataWin* dataWin = runner->dataWin; + float roomW = (float) runner->currentRoom->width; + float roomH = (float) runner->currentRoom->height; + + repeat(8, i) { + RuntimeBackground* bg = &runner->backgrounds[i]; + if (!bg->visible || bg->foreground != foreground) continue; + if (0 > bg->backgroundIndex) continue; + + int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(dataWin, bg->backgroundIndex); + if (0 > tpagIndex) continue; + + if (bg->stretch) { + // Stretch to fill room dimensions + TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; + float xscale = roomW / (float) tpag->boundingWidth; + float yscale = roomH / (float) tpag->boundingHeight; + runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, 0.0f, 0.0f, 0.0f, 0.0f, xscale, yscale, 0.0f, 0xFFFFFF, bg->alpha); + } else if (bg->tileX || bg->tileY) { + Renderer_drawBackgroundTiled(runner->renderer, tpagIndex, bg->x, bg->y, bg->tileX, bg->tileY, roomW, roomH, bg->alpha); + } else { + // Single placement + runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, bg->x, bg->y, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0xFFFFFF, bg->alpha); + } + runner->frameDrawBackgrounds++; + } + runner->frameDrawBackgroundMs += Runner_nowMs() - startMs; +} + +// ===[ Draw ]=== + +static int compareDrawableDepth(const void* a, const void* b) { + const Drawable* da = (const Drawable*) a; + const Drawable* db = (const Drawable*) b; + // Higher depth draws first (behind), lower depth draws last (in front) + if (da->depth > db->depth) return -1; + if (db->depth > da->depth) return 1; + // At same depth, tiles before instances (tiles are background) + if (da->type < db->type) return -1; + if (db->type < da->type) return 1; + // At same depth and type, preserve original room order (higher index draws later = in front) + if (da->type == DRAWABLE_TILE) { + if (db->tileIndex > da->tileIndex) return -1; + if (da->tileIndex > db->tileIndex) return 1; + } + // At same depth, newer instances (higher instanceId) draw FIRST (behind), older draw LAST (front). + if (da->type == DRAWABLE_INSTANCE && db->type == DRAWABLE_INSTANCE) { + if (db->instance->instanceId > da->instance->instanceId) return 1; + if (da->instance->instanceId > db->instance->instanceId) return -1; + } + return 0; +} + +static void fireDrawSubtype(Runner* runner, Drawable* drawables, int32_t drawableCount, int32_t subtype) { + int32_t slot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_DRAW, subtype); + if (slot == -1) return; + +#ifdef __3DS__ + if ((subtype == DRAW_GUI_BEGIN || subtype == DRAW_GUI || subtype == DRAW_GUI_END) && runner != NULL) { + Runner_prepare3DSDrawBattleState(runner); + if (runner->n3dsDrawBattleActive) { + int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : (int32_t) runner->dataWin->gen8.defaultWindowWidth; + int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : (int32_t) runner->dataWin->gen8.defaultWindowHeight; + if (guiW <= 0) guiW = 320; + if (guiH <= 0) guiH = 240; + bool useTopEnemyDialogue2x = Runner_has3DSTopEnemyDialogue(runner, drawables, drawableCount); + runner->n3dsDrawHasTopEnemyDialogue = useTopEnemyDialogue2x; + if (useTopEnemyDialogue2x) N3DSRenderer_beginTopScreenGUI2x(runner->renderer, guiW, guiH); + else N3DSRenderer_beginTopScreenGUI(runner->renderer, guiW, guiH); + + N3DSResolvedDrawEvent* responderEvents = Runner_prepare3DSTopScreenGUIResponderList(runner, drawables, drawableCount, subtype, slot); + int32_t responderCount = (int32_t) arrlen(responderEvents); + repeat(responderCount, i) { + N3DSResolvedDrawEvent* resolvedEvent = &responderEvents[i]; + Instance* inst = resolvedEvent->instance; + if (inst == NULL || !inst->active || !inst->visible) continue; + Runner_executeResolvedEvent(runner, inst, EVENT_DRAW, subtype, resolvedEvent->codeId, resolvedEvent->ownerObjectIndex); + } + if (useTopEnemyDialogue2x) N3DSRenderer_endTopScreenGUI2x(runner->renderer); + else N3DSRenderer_endTopScreenGUI(runner->renderer); + return; + } + } +#endif + + repeat(drawableCount, i) { + Drawable* d = &drawables[i]; + if (d->type != DRAWABLE_INSTANCE) + continue; + + Instance* inst = d->instance; + if (!inst->active || !inst->visible) + continue; +#ifdef __3DS__ + if ((subtype == DRAW_GUI_BEGIN || subtype == DRAW_GUI || subtype == DRAW_GUI_END) && + Runner_shouldHideOn3DSTopScreen(runner, inst)) { + continue; + } +#endif + + int32_t ownerObjectIndex = -1; + int32_t codeId = ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, slot, &ownerObjectIndex); + if (0 > codeId) continue; + Runner_executeResolvedEvent(runner, inst, EVENT_DRAW, subtype, codeId, ownerObjectIndex); + } +} + +// GMS2 tilemap cell bit layout (matches HTML5 Function_Layers.js TileIndex/Mirror/Flip/Rotate masks) +#define GMS2_TILE_INDEX_MASK 0x0007FFFF // bits 0..18 +#define GMS2_TILE_MIRROR_MASK 0x10000000 // bit 28 (horizontal flip) +#define GMS2_TILE_FLIP_MASK 0x20000000 // bit 29 (vertical flip) +#define GMS2_TILE_ROTATE_MASK 0x40000000 // bit 30 (90 CW) + +static void Runner_getCurrentViewBounds(Runner* runner, float* outLeft, float* outTop, float* outRight, float* outBottom) { + Room* room = runner->currentRoom; + float left = 0.0f; + float top = 0.0f; + float right = room != nullptr ? (float) room->width : 0.0f; + float bottom = room != nullptr ? (float) room->height : 0.0f; + + if (runner != NULL && runner->drawViewBoundsValid) { + left = (float) runner->drawViewX; + top = (float) runner->drawViewY; + right = left + (float) runner->drawViewWidth; + bottom = top + (float) runner->drawViewHeight; + } else if (room != nullptr && (room->flags & 1) != 0) { + int32_t viewIndex = runner->viewCurrent; + if (viewIndex >= 0 && viewIndex < MAX_VIEWS) { + RuntimeView* view = &runner->views[viewIndex]; + if (view->enabled) { + left = (float) view->viewX; + top = (float) view->viewY; + right = left + (float) view->viewWidth; + bottom = top + (float) view->viewHeight; + } + } + } + + *outLeft = left; + *outTop = top; + *outRight = right; + *outBottom = bottom; +} + +static bool Runner_rectIntersectsCurrentView(Runner* runner, float x, float y, float w, float h) { + float viewLeft, viewTop, viewRight, viewBottom; + Runner_getCurrentViewBounds(runner, &viewLeft, &viewTop, &viewRight, &viewBottom); + return !(x + w <= viewLeft || y + h <= viewTop || x >= viewRight || y >= viewBottom); +} + +static bool Runner_tileIntersectsCurrentView(Runner* runner, RoomTile* tile, float offsetX, float offsetY) { + float width = (float) tile->width * fabsf(tile->scaleX); + float height = (float) tile->height * fabsf(tile->scaleY); + if (width < 1.0f) width = (float) tile->width; + if (height < 1.0f) height = (float) tile->height; + return Runner_rectIntersectsCurrentView(runner, (float) tile->x + offsetX, (float) tile->y + offsetY, width, height); +} + +static bool Runner_instanceIntersectsCurrentView(Runner* runner, Instance* inst) { + if (runner == NULL || inst == NULL) return false; + + InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); + if (!bbox.valid) { + return true; + } + + return Runner_rectIntersectsCurrentView( + runner, + (float) bbox.left, + (float) bbox.top, + (float) (bbox.right - bbox.left), + (float) (bbox.bottom - bbox.top) + ); +} + +static void Runner_freeTileLayerCaches(Runner* runner) { + if (runner == NULL || runner->tileLayerCaches == NULL) return; + + repeat(runner->tileLayerCacheCount, i) { +#ifdef __3DS__ + if (runner->renderer != NULL && runner->tileLayerCaches[i].n3dsChunkCacheId >= 0) { + N3DSRenderer_destroyTileLayerChunkCache(runner->renderer, runner->tileLayerCaches[i].n3dsChunkCacheId); + runner->tileLayerCaches[i].n3dsChunkCacheId = -1; + } +#endif + free(runner->tileLayerCaches[i].rows); + free(runner->tileLayerCaches[i].cells); + runner->tileLayerCaches[i].rows = NULL; + runner->tileLayerCaches[i].cells = NULL; + runner->tileLayerCaches[i].built = false; + } + + free(runner->tileLayerCaches); + runner->tileLayerCaches = NULL; + runner->tileLayerCacheCount = 0; +} + +static void Runner_buildTileLayerCaches(Runner* runner) { + if (runner == NULL || runner->currentRoom == NULL) return; + + Runner_freeTileLayerCaches(runner); + + Room* room = runner->currentRoom; + if (room->layerCount == 0) return; + +#ifdef __3DS__ + if (runner->osType == OS_3DS && Runner_isOld3DSLike()) { + return; + } +#endif + + runner->tileLayerCaches = safeCalloc(room->layerCount, sizeof(TileLayerRenderCache)); + runner->tileLayerCacheCount = room->layerCount; + + repeat(room->layerCount, i) { + RoomLayer* layer = &room->layers[i]; + if (layer->type != RoomLayerType_Tiles || layer->tilesData == NULL) continue; + + RoomLayerTilesData* data = layer->tilesData; + TileLayerRenderCache* cache = &runner->tileLayerCaches[i]; + cache->backgroundIndex = data->backgroundIndex; + cache->n3dsChunkCacheId = -1; + cache->tilesX = data->tilesX; + cache->tilesY = data->tilesY; + + if (data->tileData == NULL || data->tilesX == 0 || data->tilesY == 0 || data->backgroundIndex < 0 || + (uint32_t) data->backgroundIndex >= runner->dataWin->bgnd.count) { + continue; + } + + Background* tileset = &runner->dataWin->bgnd.backgrounds[data->backgroundIndex]; + if (tileset->gms2TileWidth == 0 || tileset->gms2TileHeight == 0 || tileset->gms2TileColumns == 0) { + continue; + } + + cache->tileWidth = tileset->gms2TileWidth; + cache->tileHeight = tileset->gms2TileHeight; + cache->rows = safeCalloc(data->tilesY, sizeof(TileLayerCacheRow)); + + uint32_t borderX = tileset->gms2OutputBorderX; + uint32_t borderY = tileset->gms2OutputBorderY; + uint32_t columns = tileset->gms2TileColumns; + uint32_t totalNonEmpty = 0; + repeat(data->tilesY, ty) { + cache->rows[ty].start = totalNonEmpty; + repeat(data->tilesX, tx) { + uint32_t cell = data->tileData[ty * data->tilesX + tx]; + uint32_t tileIndex = cell & GMS2_TILE_INDEX_MASK; + if (tileIndex == 0) continue; + + uint32_t tileSlot = tileIndex - 1u; + uint32_t col = tileSlot % columns; + uint32_t row = tileSlot / columns; + + TileLayerCacheCell cachedCell = { + .tileX = tx, + .srcX = col * (cache->tileWidth + 2 * borderX) + borderX, + .srcY = row * (cache->tileHeight + 2 * borderY) + borderY, + .tpagIndex = tileset->tpagIndex, + .n3dsTileEntryIndex = -1, + .mirror = (cell & GMS2_TILE_MIRROR_MASK) != 0, + .flip = (cell & GMS2_TILE_FLIP_MASK) != 0, + }; +#ifdef __3DS__ + if (runner->renderer != NULL) { + cachedCell.n3dsTileEntryIndex = N3DSRenderer_findTileEntryIndex( + runner->renderer, + data->backgroundIndex, + (int32_t) cachedCell.srcX, + (int32_t) cachedCell.srcY, + (int32_t) cache->tileWidth, + (int32_t) cache->tileHeight + ); + } +#endif + arrput(cache->cells, cachedCell); + totalNonEmpty++; + } + cache->rows[ty].count = totalNonEmpty - cache->rows[ty].start; + } + + cache->built = true; + } +} + +static uint32_t Runner_findFirstVisibleTileCell(const TileLayerCacheCell* cells, uint32_t count, uint32_t startX) { + uint32_t lo = 0; + uint32_t hi = count; + while (lo < hi) { + uint32_t mid = lo + (hi - lo) / 2u; + if (cells[mid].tileX < startX) { + lo = mid + 1u; + } else { + hi = mid; + } + } + return lo; +} + +void Runner_drawTileLayer(Runner* runner, uint32_t layerIndex, RoomLayerTilesData* data, float layerOffsetX, float layerOffsetY, float alpha) { + double startMs = Runner_nowMs(); + if (data == nullptr || data->tileData == nullptr) return; + if (0 > data->backgroundIndex) return; + if (layerIndex >= runner->tileLayerCacheCount || runner->tileLayerCaches == NULL) return; + + DataWin* dw = runner->dataWin; + if ((uint32_t) data->backgroundIndex >= dw->bgnd.count) return; + + Background* tileset = &dw->bgnd.backgrounds[data->backgroundIndex]; + if (tileset->gms2TileWidth == 0 || tileset->gms2TileHeight == 0 || tileset->gms2TileColumns == 0) return; + + int32_t tpagIndex = tileset->tpagIndex; + if (0 > tpagIndex) return; + + uint32_t tileW = tileset->gms2TileWidth; + uint32_t tileH = tileset->gms2TileHeight; + + static bool rotateWarned = false; + TileLayerRenderCache* cache = NULL; + bool useCachedPath = layerIndex < runner->tileLayerCacheCount && + runner->tileLayerCaches != NULL; + if (useCachedPath) { + cache = &runner->tileLayerCaches[layerIndex]; + useCachedPath = cache->built && cache->cells != NULL && cache->rows != NULL; + } + +#if defined(__3DS__) && N3DS_ENABLE_TILE_LAYER_CHUNK_CACHE + if (useCachedPath && runner->renderer != NULL && runner->osType == OS_3DS) { + if (cache->n3dsChunkCacheId < 0 && runner->currentRoom != NULL) { + cache->n3dsChunkCacheId = N3DSRenderer_createTileLayerChunkCache( + runner->renderer, + runner->currentRoom->width, + runner->currentRoom->height, + cache + ); + } + if (cache->n3dsChunkCacheId >= 0 && + N3DSRenderer_drawTileLayerChunkCache(runner->renderer, cache->n3dsChunkCacheId, layerOffsetX, layerOffsetY, alpha)) { + runner->frameDrawTilesMs += Runner_nowMs() - startMs; + return; + } + } +#endif + + float viewLeft, viewTop, viewRight, viewBottom; + Runner_getCurrentViewBounds(runner, &viewLeft, &viewTop, &viewRight, &viewBottom); + int32_t startX = (int32_t) floorf((viewLeft - layerOffsetX) / (float) tileW); + int32_t startY = (int32_t) floorf((viewTop - layerOffsetY) / (float) tileH); + int32_t endX = (int32_t) ceilf((viewRight - layerOffsetX) / (float) tileW); + int32_t endY = (int32_t) ceilf((viewBottom - layerOffsetY) / (float) tileH); + if (startX < 0) startX = 0; + if (startY < 0) startY = 0; + if (endX > (int32_t) data->tilesX) endX = (int32_t) data->tilesX; + if (endY > (int32_t) data->tilesY) endY = (int32_t) data->tilesY; + if (startX >= endX || startY >= endY) return; + + uint32_t borderX = tileset->gms2OutputBorderX; + uint32_t borderY = tileset->gms2OutputBorderY; + uint32_t columns = tileset->gms2TileColumns; + + for (int32_t ty = startY; ty < endY; ty++) { + if (useCachedPath) { + TileLayerCacheRow* rowCache = &cache->rows[ty]; + if (rowCache->count == 0) continue; + + const TileLayerCacheCell* rowCells = &cache->cells[rowCache->start]; + uint32_t startCell = Runner_findFirstVisibleTileCell(rowCells, rowCache->count, (uint32_t) startX); + for (uint32_t ci = startCell; ci < rowCache->count; ci++) { + const TileLayerCacheCell* cachedCell = &rowCells[ci]; + if ((int32_t) cachedCell->tileX >= endX) break; + + uint32_t cell = data->tileData[ty * data->tilesX + cachedCell->tileX]; + bool rotate = (cell & GMS2_TILE_ROTATE_MASK) != 0; + + if (rotate && !rotateWarned) { + fprintf(stderr, "Runner: WARNING: GMS2 tile layer has rotated tiles; rotation not yet implemented, drawing unrotated\n"); + rotateWarned = true; + } + + float xscale = cachedCell->mirror ? -1.0f : 1.0f; + float yscale = cachedCell->flip ? -1.0f : 1.0f; + + float dstX = (float) (cachedCell->tileX * tileW) + layerOffsetX + (cachedCell->mirror ? (float) tileW : 0.0f); + float dstY = (float) (ty * tileH) + layerOffsetY + (cachedCell->flip ? (float) tileH : 0.0f); + float tileAlpha = alpha; + if (tileAlpha < 0.0f) tileAlpha = 0.0f; + if (tileAlpha > 1.0f) tileAlpha = 1.0f; + uint32_t alphaByte = (uint32_t) lroundf(tileAlpha * 255.0f); +#ifdef __3DS__ + if (runner->renderer != NULL && cachedCell->n3dsTileEntryIndex >= 0) { + if (N3DSRenderer_drawCachedTileEntry( + runner->renderer, + cachedCell->n3dsTileEntryIndex, + dstX, + dstY, + xscale, + yscale, + 0x00FFFFFFu, + tileAlpha + )) { + runner->frameDrawTiles++; + continue; + } + } +#endif + RoomTile tile = { + .x = (int32_t) lroundf(dstX), + .y = (int32_t) lroundf(dstY), + .useSpriteDefinition = false, + .backgroundDefinition = data->backgroundIndex, + .sourceX = (int32_t) cachedCell->srcX, + .sourceY = (int32_t) cachedCell->srcY, + .width = tileW, + .height = tileH, + .tileDepth = 0, + .instanceID = 0, + .scaleX = xscale, + .scaleY = yscale, + .color = (alphaByte << 24) | 0x00FFFFFFu, + }; + Renderer_drawTile(runner->renderer, &tile, 0.0f, 0.0f); + runner->frameDrawTiles++; + } + continue; + } + + for (int32_t tx = startX; tx < endX; tx++) { + uint32_t cell = data->tileData[ty * data->tilesX + tx]; + uint32_t tileIndex = cell & GMS2_TILE_INDEX_MASK; + if (tileIndex == 0) continue; + + bool rotate = (cell & GMS2_TILE_ROTATE_MASK) != 0; + if (rotate && !rotateWarned) { + fprintf(stderr, "Runner: WARNING: GMS2 tile layer has rotated tiles; rotation not yet implemented, drawing unrotated\n"); + rotateWarned = true; + } + + uint32_t tileSlot = tileIndex - 1u; + uint32_t col = tileSlot % columns; + uint32_t row = tileSlot / columns; + float xscale = (cell & GMS2_TILE_MIRROR_MASK) != 0 ? -1.0f : 1.0f; + float yscale = (cell & GMS2_TILE_FLIP_MASK) != 0 ? -1.0f : 1.0f; + float dstX = (float) (tx * (int32_t) tileW) + layerOffsetX + (xscale < 0.0f ? (float) tileW : 0.0f); + float dstY = (float) (ty * (int32_t) tileH) + layerOffsetY + (yscale < 0.0f ? (float) tileH : 0.0f); + float tileAlpha = alpha; + if (tileAlpha < 0.0f) tileAlpha = 0.0f; + if (tileAlpha > 1.0f) tileAlpha = 1.0f; + uint32_t alphaByte = (uint32_t) lroundf(tileAlpha * 255.0f); + + RoomTile tile = { + .x = (int32_t) lroundf(dstX), + .y = (int32_t) lroundf(dstY), + .useSpriteDefinition = false, + .backgroundDefinition = data->backgroundIndex, + .sourceX = (int32_t) (col * (tileW + 2 * borderX) + borderX), + .sourceY = (int32_t) (row * (tileH + 2 * borderY) + borderY), + .width = tileW, + .height = tileH, + .tileDepth = 0, + .instanceID = 0, + .scaleX = xscale, + .scaleY = yscale, + .color = (alphaByte << 24) | 0x00FFFFFFu, + }; + Renderer_drawTile(runner->renderer, &tile, 0.0f, 0.0f); + runner->frameDrawTiles++; + } + } + runner->frameDrawTilesMs += Runner_nowMs() - startMs; +} + +void Runner_resetFrameDrawStats(Runner* runner) { + if (runner == NULL) return; + runner->frameDrawBackgrounds = 0; + runner->frameDrawTiles = 0; + runner->frameDrawInstances = 0; + runner->frameDrawLayerElements = 0; + runner->frameDrawBackgroundMs = 0.0; + runner->frameDrawTilesMs = 0.0; + runner->frameDrawInstancesMs = 0.0; + runner->frameDrawLayerMs = 0.0; + runner->frameDrawGuiMs = 0.0; + runner->frameDrawGuiEventMs = 0.0; + runner->frameDrawBattleReplayMs = 0.0; +} + +// Returns true if "drawables" is already in compareDrawableDepth order. Used by the sort-dirty path to skip qsort when small depth perturbations didn't actually cross any neighbor. +static bool isDrawableArraySorted(Drawable* drawables, int32_t count) { + for (int32_t i = 1; count > i; i++) { + if (compareDrawableDepth(&drawables[i - 1], &drawables[i]) > 0) return false; + } + return true; +} + +// Refreshes each entry's cached .depth from the live instance/runtime-layer pointer. Tile entries never change depth mid-room so they're left alone. +static void refreshDrawableDepths(Drawable* drawables, int32_t count) { + for (int32_t i = 0; count > i; i++) { + Drawable* d = &drawables[i]; + if (d->type == DRAWABLE_INSTANCE) { + d->depth = d->instance->depth; + } else if (d->type == DRAWABLE_LAYER) { + d->depth = d->runtimeLayer->depth; + } + } +} + +// Rebuilds runner->cachedDrawables when invalidated. Two-tier strategy: +// structureDirty - the SET of entries changed (instance/layer create or destroy, room change). Drop the cache and re-add every instance/tile/runtime-layer, then qsort. +// sortDirty only - the entries are the same but .depth values may have shifted. Refresh depths from the live sources and only qsort if the order actually broke. +static void rebuildDrawableCacheIfDirty(Runner* runner) { + if (runner->drawableListStructureDirty) { + arrsetlen(runner->cachedDrawables, 0); + Room* room = runner->currentRoom; + if (room == nullptr) { + runner->drawableListStructureDirty = false; + runner->drawableListSortDirty = false; + return; + } + + int32_t instanceCount = (int32_t) arrlen(runner->instances); + repeat(instanceCount, i) { + Instance* inst = runner->instances[i]; + Drawable d = { .type = DRAWABLE_INSTANCE, .depth = inst->depth }; + d.instance = inst; + arrput(runner->cachedDrawables, d); + } + + if (!DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0)) { + repeat(room->tileCount, i) { + RoomTile* tile = &room->tiles[i]; + Drawable d = { .type = DRAWABLE_TILE, .depth = tile->tileDepth }; + d.tileIndex = (int32_t) i; + arrput(runner->cachedDrawables, d); + } + } else { + size_t runtimeLayersCount = arrlenu(runner->runtimeLayers); + repeat(runtimeLayersCount, i) { + RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; + Drawable d = { .type = DRAWABLE_LAYER, .depth = runtimeLayer->depth }; + d.runtimeLayer = runtimeLayer; + arrput(runner->cachedDrawables, d); + } + } + + int32_t count = (int32_t) arrlen(runner->cachedDrawables); + if (count > 1) { + qsort(runner->cachedDrawables, count, sizeof(Drawable), compareDrawableDepth); + } + runner->drawableListStructureDirty = false; + runner->drawableListSortDirty = false; + return; + } + + if (runner->drawableListSortDirty) { + int32_t count = (int32_t) arrlen(runner->cachedDrawables); + refreshDrawableDepths(runner->cachedDrawables, count); + if (count > 1 && !isDrawableArraySorted(runner->cachedDrawables, count)) { + qsort(runner->cachedDrawables, count, sizeof(Drawable), compareDrawableDepth); + } + runner->drawableListSortDirty = false; + } +} + +void Runner_draw(Runner* runner) { + Room* room = runner->currentRoom; + + rebuildDrawableCacheIfDirty(runner); + int32_t drawableCount = (int32_t) arrlen(runner->cachedDrawables); + Drawable* drawables = runner->cachedDrawables; + int32_t instanceCount = (int32_t) arrlen(runner->instances); + + bool traceDraw = false; + + if (traceDraw) { + char buffer[320]; + snprintf( + buffer, + sizeof(buffer), + "runner: draw room=%s idx=%d pendingRoom=%d instances=%d drawables=%d bgColor=%06X drawBg=%d", + room != nullptr && room->name != nullptr ? room->name : "", + runner->currentRoomIndex, + runner->pendingRoom, + instanceCount, + drawableCount, + runner->backgroundColor & 0xFFFFFFu, + runner->drawBackgroundColor ? 1 : 0 + ); + Runner_platformBootLog(buffer); + + if (drawableCount == 0) { + Runner_platformBootLog("runner: no drawables in current room"); + } + + gRunnerLastLoggedRoomIndex = runner->currentRoomIndex; + gRunnerLastLoggedPendingRoom = runner->pendingRoom; + gRunnerLastLoggedDrawableCount = drawableCount; + gRunnerLastLoggedInstanceCount = instanceCount; + gRunnerDrawTraceCount++; + + int32_t traceInstanceCount = instanceCount; + if (traceInstanceCount > 4) traceInstanceCount = 4; + // repeat(traceInstanceCount, ti) { + // Instance* inst = runner->instances[ti]; + // if (inst == nullptr) continue; + // char instBuffer[256]; + // const char* objectName = + // (inst->objectIndex >= 0 && (uint32_t) inst->objectIndex < runner->dataWin->objt.count && + // runner->dataWin->objt.objects[inst->objectIndex].name != nullptr) + // ? runner->dataWin->objt.objects[inst->objectIndex].name + // : ""; + // snprintf( + // instBuffer, + // sizeof(instBuffer), + // "runner: inst[%d] id=%u obj=%s sprite=%d visible=%d active=%d depth=%d x=%.1f y=%.1f alarm0=%d alarm1=%d", + // ti, + // inst->instanceId, + // objectName, + // inst->spriteIndex, + // inst->visible ? 1 : 0, + // inst->active ? 1 : 0, + // inst->depth, + // inst->x, + // inst->y, + // inst->alarm[0], + // inst->alarm[1] + // ); + // Runner_bootLog(instBuffer); + // } + } + + // Draw non-foreground backgrounds (behind everything) + if (!DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0)) + Runner_drawBackgrounds(runner, false); + +// Fire draw subtypes in correct GameMaker order. fireDrawSubtype walks the cache and filters inline. + fireDrawSubtype(runner, drawables, drawableCount, DRAW_PRE); + fireDrawSubtype(runner, drawables, drawableCount, DRAW_BEGIN); + +#ifdef __3DS__ + Runner_prepare3DSDrawBattleState(runner); + bool n3dsBattleActiveForDraw = runner->n3dsDrawBattleActive; + int32_t n3dsTopScreenDrawInstanceCursor = 0; + int32_t n3dsTopScreenDrawInstanceCount = 0; + if (n3dsBattleActiveForDraw) { + Runner_prepare3DSTopScreenGUIInstanceList(runner, drawables, drawableCount); + n3dsTopScreenDrawInstanceCount = (int32_t) arrlen(runner->n3dsTopScreenGUIInstances); + } +#endif + int32_t drawNormalSlot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_DRAW, DRAW_NORMAL); + + // Draw interleaved tiles and instances + repeat(drawableCount, i) { + Drawable* d = &drawables[i]; + if (d->type == DRAWABLE_TILE) { + if (runner->renderer != nullptr) { + double drawStartMs = Runner_nowMs(); + RoomTile* tile = &room->tiles[d->tileIndex]; + // Skip tiles whose layer was hidden via tile_layer_hide(). Filtered here (not in the cache) so toggling layer visibility doesn't invalidate. + ptrdiff_t layerIdx = hmgeti(runner->tileLayerMap, tile->tileDepth); + if (layerIdx >= 0 && !runner->tileLayerMap[layerIdx].value.visible) continue; + float offsetX = 0.0f, offsetY = 0.0f; + if (layerIdx >= 0) { + offsetX = runner->tileLayerMap[layerIdx].value.offsetX; + offsetY = runner->tileLayerMap[layerIdx].value.offsetY; + } + if (!Runner_tileIntersectsCurrentView(runner, tile, offsetX, offsetY)) continue; + +#ifdef ENABLE_VM_TRACING + // Trace tile drawing if requested + if (shlen(runner->vmContext->tilesToBeTraced) > 0) { + DataWin* dataWin = runner->dataWin; + const char* bgName = (tile->backgroundDefinition >= 0 && dataWin->bgnd.count > (uint32_t) tile->backgroundDefinition) ? dataWin->bgnd.backgrounds[tile->backgroundDefinition].name : ""; + const char* roomName = room->name; + + bool shouldTrace = shgeti(runner->vmContext->tilesToBeTraced, "*") != -1 || shgeti(runner->vmContext->tilesToBeTraced, bgName) != -1 || shgeti(runner->vmContext->tilesToBeTraced, roomName) != -1; + + if (shouldTrace) { + int32_t tpagIndex = Renderer_resolveObjectTPAGIndex(dataWin, tile); + if (tpagIndex >= 0) { + TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; + fprintf(stderr, "Runner: [%s] Drawing tile #%d bg=%s(%d) tpag(srcX=%d srcY=%d srcW=%d srcH=%d tgtX=%d tgtY=%d bndW=%d bndH=%d page=%d) tile(srcX=%d srcY=%d w=%u h=%u) at pos=(%d,%d) depth=%d\n", roomName, d->tileIndex, bgName, tile->backgroundDefinition, tpag->sourceX, tpag->sourceY, tpag->sourceWidth, tpag->sourceHeight, tpag->targetX, tpag->targetY, tpag->boundingWidth, tpag->boundingHeight, tpag->texturePageId, tile->sourceX, tile->sourceY, tile->width, tile->height, tile->x, tile->y, tile->tileDepth); + + // Warn if tile source rect exceeds TPAG content bounds + if ((uint32_t) (tile->sourceX + tile->width) > (uint32_t) tpag->sourceWidth || (uint32_t) (tile->sourceY + tile->height) > (uint32_t) tpag->sourceHeight) { + fprintf(stderr, "Runner: [%s] WARNING: Tile #%d source rect (%d,%d %ux%u) exceeds TPAG content bounds (%dx%d)\n", roomName, d->tileIndex, tile->sourceX, tile->sourceY, tile->width, tile->height, tpag->sourceWidth, tpag->sourceHeight); + } + } else { + fprintf(stderr, "Runner: [%s] Drawing tile #%d bg=%s(%d) tpag=UNRESOLVED tile(srcX=%d srcY=%d w=%u h=%u) at pos=(%d,%d) depth=%d\n", roomName, d->tileIndex, bgName, tile->backgroundDefinition, tile->sourceX, tile->sourceY, tile->width, tile->height, tile->x, tile->y, tile->tileDepth); + } + } + } +#endif + + Renderer_drawTile(runner->renderer, tile, offsetX, offsetY); + runner->frameDrawTiles++; + runner->frameDrawTilesMs += Runner_nowMs() - drawStartMs; + } + } else if (d->type == DRAWABLE_INSTANCE) { + double drawStartMs = Runner_nowMs(); + Instance* inst = d->instance; + // Filter inactive/invisible instances at draw time so the cache doesn't need invalidation when those flags toggle. + if (!inst->active || !inst->visible) continue; +#ifdef __3DS__ + if (n3dsBattleActiveForDraw) { + if (n3dsTopScreenDrawInstanceCursor >= n3dsTopScreenDrawInstanceCount || + runner->n3dsTopScreenGUIInstances[n3dsTopScreenDrawInstanceCursor] != inst) { + continue; + } + n3dsTopScreenDrawInstanceCursor++; + } + if (Runner_is3DSTextUIObjectCheck(runner, inst)) continue; +#endif + int32_t ownerObjectIndex = -1; + int32_t codeId = drawNormalSlot >= 0 + ? ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, drawNormalSlot, &ownerObjectIndex) + : -1; + if (codeId < 0 && !Runner_instanceIntersectsCurrentView(runner, inst)) continue; + bool offsetTopBattleEnemy = false; + double savedY = 0.0; +#ifdef __3DS__ + if (runner->osType == OS_3DS && n3dsBattleActiveForDraw && + Runner_shouldOffset3DSTopBattleInstance(runner, inst)) { + offsetTopBattleEnemy = true; + savedY = inst->y; + inst->y += Runner_get3DSTopBattleInstanceYOffset(runner, inst); + } +#endif + if (codeId >= 0) { + Runner_executeResolvedEvent(runner, inst, EVENT_DRAW, DRAW_NORMAL, codeId, ownerObjectIndex); + } else if (runner->renderer != nullptr) { + Renderer_drawSelf(runner->renderer, inst); + } +#ifdef __3DS__ + if (offsetTopBattleEnemy) { + inst->y = savedY; + } +#endif + runner->frameDrawInstances++; + runner->frameDrawInstancesMs += Runner_nowMs() - drawStartMs; + } else if (d->type == DRAWABLE_LAYER) + { + double layerStartMs = Runner_nowMs(); + RuntimeLayer* runtimeLayer = d->runtimeLayer; + if (runtimeLayer == nullptr || !runtimeLayer->visible) continue; + float layerOffsetX = runtimeLayer->xOffset; + float layerOffsetY = runtimeLayer->yOffset; + + // Dynamic layers created via layer_create have no parsed RoomLayer, render their runtime elements instead (backgrounds, in the future sprites/tilemaps). + if (runtimeLayer->dynamic) { + if (runner->renderer == nullptr) continue; + + DataWin* dataWin = runner->dataWin; + float roomW = (float) runner->currentRoom->width; + float roomH = (float) runner->currentRoom->height; + + size_t elementCount = arrlenu(runtimeLayer->elements); + repeat(elementCount, j) { + RuntimeLayerElement* layerElement = &runtimeLayer->elements[j]; + if (layerElement->type == RuntimeLayerElementType_Background && layerElement->backgroundElement != nullptr) { + RuntimeBackgroundElement* bg = layerElement->backgroundElement; + if (!bg->visible) continue; + int32_t tpagIndex = Renderer_resolveSpriteTPAGIndex(dataWin, bg->spriteIndex); + if (0 > tpagIndex) continue; + if (bg->stretch) { + TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; + if (tpag->boundingWidth == 0 || tpag->boundingHeight == 0) continue; + float xscale = roomW / (float) tpag->boundingWidth; + float yscale = roomH / (float) tpag->boundingHeight; + runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, 0.0f, 0.0f, 0.0f, 0.0f, xscale, yscale, 0.0f, bg->blend, bg->alpha); + } else if (bg->htiled || bg->vtiled) { + Renderer_drawBackgroundTiled(runner->renderer, tpagIndex, layerOffsetX + bg->xOffset, layerOffsetY + bg->yOffset, bg->htiled, bg->vtiled, roomW, roomH, bg->alpha); + } else { + runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, layerOffsetX + bg->xOffset, layerOffsetY + bg->yOffset, 0.0f, 0.0f, bg->xScale, bg->yScale, 0.0f, bg->blend, bg->alpha); + } + runner->frameDrawLayerElements++; + } + } + runner->frameDrawLayerMs += Runner_nowMs() - layerStartMs; + continue; + } + + // Parsed layer: look up the RoomLayer by ID and render its data-driven content. + RoomLayer* parsedLayer = Runner_findRoomLayerById(runner, (int32_t) runtimeLayer->id); + if (parsedLayer == nullptr) continue; + if (traceDraw) { + // char layerBuffer[256]; + // uint32_t runtimeElementCount = (uint32_t) arrlenu(runtimeLayer->elements); + // uint32_t assetsSpriteCount = (parsedLayer->assetsData != nullptr) ? parsedLayer->assetsData->spriteCount : 0u; + // uint32_t assetsTileCount = (parsedLayer->assetsData != nullptr) ? parsedLayer->assetsData->legacyTileCount : 0u; + // uint32_t layerInstanceCount = (parsedLayer->instancesData != nullptr) ? parsedLayer->instancesData->instanceCount : 0u; + // snprintf( + // layerBuffer, + // sizeof(layerBuffer), + // "runner: layer id=%u type=%u depth=%d visible=%d runtimeEls=%u assetsSprites=%u assetsTiles=%u layerInstances=%u", + // parsedLayer->id, + // parsedLayer->type, + // parsedLayer->depth, + // parsedLayer->visible ? 1 : 0, + // runtimeElementCount, + // assetsSpriteCount, + // assetsTileCount, + // layerInstanceCount + // ); + // Runner_bootLog(layerBuffer); + } + if (parsedLayer->type == RoomLayerType_Assets) { + RoomLayerAssetsData* data = parsedLayer->assetsData; + repeat(data->legacyTileCount, j) { + if (runner->renderer != nullptr) { + RoomTile* tile = &data->legacyTiles[j]; + // Check if this tile's layer is hidden via tile_layer_hide() + ptrdiff_t layerIdx = hmgeti(runner->tileLayerMap, tile->tileDepth); + if (layerIdx >= 0 && !runner->tileLayerMap[layerIdx].value.visible) continue; + float offsetX = 0.0f, offsetY = 0.0f; + if (layerIdx >= 0) { + offsetX = runner->tileLayerMap[layerIdx].value.offsetX; + offsetY = runner->tileLayerMap[layerIdx].value.offsetY; + } + if (!Runner_tileIntersectsCurrentView(runner, tile, offsetX, offsetY)) continue; + +#ifdef ENABLE_VM_TRACING + // Trace tile drawing if requested + if (shlen(runner->vmContext->tilesToBeTraced) > 0) { + DataWin* dataWin = runner->dataWin; + const char* bgName = (tile->backgroundDefinition >= 0 && dataWin->bgnd.count > (uint32_t) tile->backgroundDefinition) ? dataWin->bgnd.backgrounds[tile->backgroundDefinition].name : ""; + const char* roomName = room->name; + + bool shouldTrace = shgeti(runner->vmContext->tilesToBeTraced, "*") != -1 || shgeti(runner->vmContext->tilesToBeTraced, bgName) != -1 || shgeti(runner->vmContext->tilesToBeTraced, roomName) != -1; + + if (shouldTrace) { + int32_t tpagIndex = Renderer_resolveObjectTPAGIndex(dataWin, tile); + if (tpagIndex >= 0) { + TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; + fprintf(stderr, "Runner: [%s] Drawing tile #%d bg=%s(%d) tpag(srcX=%d srcY=%d srcW=%d srcH=%d tgtX=%d tgtY=%d bndW=%d bndH=%d page=%d) tile(srcX=%d srcY=%d w=%u h=%u) at pos=(%d,%d) depth=%d\n", roomName, d->tileIndex, bgName, tile->backgroundDefinition, tpag->sourceX, tpag->sourceY, tpag->sourceWidth, tpag->sourceHeight, tpag->targetX, tpag->targetY, tpag->boundingWidth, tpag->boundingHeight, tpag->texturePageId, tile->sourceX, tile->sourceY, tile->width, tile->height, tile->x, tile->y, tile->tileDepth); + + // Warn if tile source rect exceeds TPAG content bounds + if ((uint32_t) (tile->sourceX + tile->width) > (uint32_t) tpag->sourceWidth || (uint32_t) (tile->sourceY + tile->height) > (uint32_t) tpag->sourceHeight) { + fprintf(stderr, "Runner: [%s] WARNING: Tile #%d source rect (%d,%d %ux%u) exceeds TPAG content bounds (%dx%d)\n", roomName, d->tileIndex, tile->sourceX, tile->sourceY, tile->width, tile->height, tpag->sourceWidth, tpag->sourceHeight); + } + } else { + fprintf(stderr, "Runner: [%s] Drawing tile #%d bg=%s(%d) tpag=UNRESOLVED tile(srcX=%d srcY=%d w=%u h=%u) at pos=(%d,%d) depth=%d\n", roomName, d->tileIndex, bgName, tile->backgroundDefinition, tile->sourceX, tile->sourceY, tile->width, tile->height, tile->x, tile->y, tile->tileDepth); + } + } + } +#endif + + Renderer_drawTile(runner->renderer, tile, offsetX, offsetY); + runner->frameDrawTiles++; + runner->frameDrawTilesMs += Runner_nowMs() - layerStartMs; + layerStartMs = Runner_nowMs(); + } + } + + // Sprite elements are rendered from the runtime element list (not the parsed data) so that layer_sprite_destroy can remove them at runtime. + size_t elementCount = arrlenu(runtimeLayer->elements); + repeat(elementCount, j) { + if (runner->renderer == nullptr) break; + RuntimeLayerElement* el = &runtimeLayer->elements[j]; + if (el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) continue; + RuntimeSpriteElement* spr = el->spriteElement; + if (0 > spr->spriteIndex) continue; + Renderer_drawSpriteExt( + runner->renderer, spr->spriteIndex, (int32_t) spr->frameIndex, + spr->x, spr->y, spr->scaleX, + spr->scaleY, spr->rotation, spr->color, + 1.0); + runner->frameDrawLayerElements++; + } + } else if(parsedLayer->type == RoomLayerType_Background) { + if (runner->renderer == nullptr) return; + DataWin* dataWin = runner->dataWin; + float roomW = (float) runner->currentRoom->width; + float roomH = (float) runner->currentRoom->height; + RoomLayerBackgroundData* data = parsedLayer->backgroundData; + + int32_t tpagIndex = Renderer_resolveSpriteTPAGIndex(dataWin, data->spriteIndex); + if (0 > tpagIndex) continue; + + if (data->stretch) { + // Stretch to fill room dimensions + TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; + if (tpag->boundingWidth == 0 || tpag->boundingHeight == 0) continue; + float xscale = roomW / (float) tpag->boundingWidth; + float yscale = roomH / (float) tpag->boundingHeight; + runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, 0.0f, 0.0f, 0.0f, 0.0f, xscale, yscale, 0.0f, 0xFFFFFF, 1.0); + } else if (data->hTiled || data->vTiled) { + Renderer_drawBackgroundTiled(runner->renderer, tpagIndex, layerOffsetX, layerOffsetY, data->hTiled, data->vTiled, roomW, roomH, 1.0); + } else { + // Single placement + runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, layerOffsetX, layerOffsetY, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0xFFFFFF, 1.0); + } + runner->frameDrawBackgroundMs += Runner_nowMs() - layerStartMs; + layerStartMs = Runner_nowMs(); + } else if(parsedLayer->type == RoomLayerType_Instances) { + // Instance depth is assigned from layers during room init (initRoom). + // Nothing to do here - instances are drawn from the DRAWABLE_INSTANCE path. + } else if(parsedLayer->type == RoomLayerType_Tiles) { + if (runner->renderer == nullptr) continue; + float tileAlpha = 1.0f; + ptrdiff_t tileLayerIdx = hmgeti(runner->tileLayerMap, parsedLayer->depth); + if (tileLayerIdx >= 0) { + tileAlpha = runner->tileLayerMap[tileLayerIdx].value.alpha; + } + Runner_drawTileLayer(runner, (uint32_t) i, parsedLayer->tilesData, layerOffsetX, layerOffsetY, tileAlpha); + } + runner->frameDrawLayerMs += Runner_nowMs() - layerStartMs; + } + } + + fireDrawSubtype(runner, drawables, drawableCount, DRAW_END); + + // Draw foreground backgrounds (in front of instances, behind GUI) + Runner_drawBackgrounds(runner, true); + + fireDrawSubtype(runner, drawables, drawableCount, DRAW_POST); +#ifdef __3DS__ + if (runner->renderer != NULL) { + if (Runner_is3DSBattleActive(runner)) { + bool isDodgingBullets = Runner_is3DSDodgingBullets(runner); + bool isAsrielBattle = Runner_is3DSAsrielBattle(runner); + bool isUndyneBattle = Runner_is3DSUndyneBattle(runner); + float battleFieldScale = isDodgingBullets ? ((isUndyneBattle || isAsrielBattle) ? 1.0f : k3DSBottomBattleFieldScale) : 1.0f; + float battleFieldYOffset = isDodgingBullets ? (isAsrielBattle ? 0.0f : k3DSBottomBattleFieldYOffset) : 0.0f; + Runner_draw3DSBottomBattleUI(runner, drawables, drawableCount, DRAW_NORMAL, true, battleFieldScale, battleFieldYOffset); + Runner_draw3DSBottomBattleUI(runner, drawables, drawableCount, DRAW_NORMAL, false, 1.0f, 0.0f); + } + Runner_draw3DSBottomTextUI(runner, drawables, drawableCount, DRAW_NORMAL, false, 1.0f, 0.0f); + } +#endif + if (traceDraw) { + gRunnerDrawTraceCount++; + } +#ifdef __3DS__ + runner->n3dsTopScreenGUIListValid = false; + repeat(3, i) runner->n3dsTopScreenGUIResponderListsValid[i] = false; +#endif +} + +void Runner_drawGUI(Runner* runner) { + rebuildDrawableCacheIfDirty(runner); + Drawable* drawables = runner->cachedDrawables; + int32_t drawableCount = (int32_t) arrlen(drawables); + double phaseStartMs = Runner_nowMs(); + + fireDrawSubtype(runner, drawables, drawableCount, DRAW_GUI_BEGIN); + runner->frameDrawGuiEventMs += Runner_nowMs() - phaseStartMs; +#ifdef __3DS__ + if (runner->renderer != NULL) { + Runner_prepare3DSDrawBattleState(runner); + if (runner->n3dsDrawBattleActive) { + double battleReplayStartMs = Runner_nowMs(); + bool isDodgingBullets = runner->n3dsDrawDodgingBullets; + bool isAsrielBattle = Runner_is3DSAsrielBattle(runner); + bool isUndyneBattle = Runner_is3DSUndyneBattle(runner); + float battleFieldScale = isDodgingBullets ? ((isUndyneBattle || isAsrielBattle) ? 1.0f : k3DSBottomBattleFieldScale) : 1.0f; + float battleFieldYOffset = isDodgingBullets ? (isAsrielBattle ? 0.0f : k3DSBottomBattleFieldYOffset) : 0.0f; + Runner_draw3DSBottomBattleUI(runner, drawables, drawableCount, DRAW_GUI_BEGIN, true, battleFieldScale, battleFieldYOffset); + Runner_draw3DSBottomBattleUI(runner, drawables, drawableCount, DRAW_GUI_BEGIN, false, 1.0f, 0.0f); + runner->frameDrawBattleReplayMs += Runner_nowMs() - battleReplayStartMs; + } + } +#endif + phaseStartMs = Runner_nowMs(); + fireDrawSubtype(runner, drawables, drawableCount, DRAW_GUI); + runner->frameDrawGuiEventMs += Runner_nowMs() - phaseStartMs; +#ifdef __3DS__ + if (runner->renderer != NULL) { + Runner_prepare3DSDrawBattleState(runner); + if (runner->n3dsDrawBattleActive) { + double battleReplayStartMs = Runner_nowMs(); + bool isDodgingBullets = runner->n3dsDrawDodgingBullets; + bool isAsrielBattle = Runner_is3DSAsrielBattle(runner); + bool isUndyneBattle = Runner_is3DSUndyneBattle(runner); + float battleFieldScale = isDodgingBullets ? ((isUndyneBattle || isAsrielBattle) ? 1.0f : k3DSBottomBattleFieldScale) : 1.0f; + float battleFieldYOffset = isDodgingBullets ? (isAsrielBattle ? 0.0f : k3DSBottomBattleFieldYOffset) : 0.0f; + Runner_draw3DSBottomBattleUI(runner, drawables, drawableCount, DRAW_GUI, true, battleFieldScale, battleFieldYOffset); + Runner_draw3DSBottomBattleUI(runner, drawables, drawableCount, DRAW_GUI, false, 1.0f, 0.0f); + runner->frameDrawBattleReplayMs += Runner_nowMs() - battleReplayStartMs; + } + } +#endif + phaseStartMs = Runner_nowMs(); + fireDrawSubtype(runner, drawables, drawableCount, DRAW_GUI_END); + runner->frameDrawGuiEventMs += Runner_nowMs() - phaseStartMs; +#ifdef __3DS__ + if (runner->renderer != NULL) { + Runner_prepare3DSDrawBattleState(runner); + if (runner->n3dsDrawBattleActive) { + double battleReplayStartMs = Runner_nowMs(); + bool isDodgingBullets = runner->n3dsDrawDodgingBullets; + bool isAsrielBattle = Runner_is3DSAsrielBattle(runner); + bool isUndyneBattle = Runner_is3DSUndyneBattle(runner); + float battleFieldScale = isDodgingBullets ? ((isUndyneBattle || isAsrielBattle) ? 1.0f : k3DSBottomBattleFieldScale) : 1.0f; + float battleFieldYOffset = isDodgingBullets ? (isAsrielBattle ? 0.0f : k3DSBottomBattleFieldYOffset) : 0.0f; + Runner_draw3DSBottomBattleUI(runner, drawables, drawableCount, DRAW_GUI_END, true, battleFieldScale, battleFieldYOffset); + Runner_draw3DSBottomBattleUI(runner, drawables, drawableCount, DRAW_GUI_END, false, 1.0f, 0.0f); + runner->frameDrawBattleReplayMs += Runner_nowMs() - battleReplayStartMs; + } + } +#endif +} + +void Runner_computeViewDisplayScale(Runner* runner, int32_t gameW, int32_t gameH, float* outScaleX, float* outScaleY) { + *outScaleX = 1.0f; + *outScaleY = 1.0f; + + Room* activeRoom = runner->currentRoom; + bool viewsEnabled = (activeRoom->flags & 1) != 0; + if (viewsEnabled) { + int32_t minLeft = INT32_MAX, minTop = INT32_MAX; + int32_t maxRight = INT32_MIN, maxBottom = INT32_MIN; + repeat(MAX_VIEWS, vi) { + RuntimeView* view = &runner->views[vi]; + if (!view->enabled) continue; + if (minLeft > view->portX) minLeft = view->portX; + if (minTop > view->portY) minTop = view->portY; + int32_t right = view->portX + view->portWidth; + int32_t bottom = view->portY + view->portHeight; + if (right > maxRight) maxRight = right; + if (bottom > maxBottom) maxBottom = bottom; + } + if (maxRight > minLeft && maxBottom > minTop) { + *outScaleX = (float) gameW / (float) (maxRight - minLeft); + *outScaleY = (float) gameH / (float) (maxBottom - minTop); + } + } +} + +void Runner_drawViews(Runner* runner, int32_t gameW, int32_t gameH, float displayScaleX, float displayScaleY, bool debugShowCollisionMasks) { + Renderer* renderer = runner->renderer; + Room* activeRoom = runner->currentRoom; + bool anyViewRendered = false; + + runner->drawViewBoundsValid = false; + runner->n3dsDrawBattleStateValid = false; + runner->n3dsBattleReplayListsValid = false; + runner->n3dsTopScreenGUIListValid = false; + runner->n3dsDrawTextUIStateValid = false; + runner->n3dsTextUIListValid = false; + repeat(3, i) runner->n3dsTopScreenGUIResponderListsValid[i] = false; + + bool viewsEnabled = (activeRoom->flags & 1) != 0; + + if (viewsEnabled) { + repeat(MAX_VIEWS, vi) { + RuntimeView* view = &runner->views[vi]; + if (!view->enabled) continue; + + int32_t viewX = view->viewX; + int32_t viewY = view->viewY; + int32_t viewW = view->viewWidth; + int32_t viewH = view->viewHeight; + int32_t portX = (int32_t) ((float) view->portX * displayScaleX + 0.5f); + int32_t portY = (int32_t) ((float) view->portY * displayScaleY + 0.5f); + int32_t portW = (int32_t) ((float) view->portWidth * displayScaleX + 0.5f); + int32_t portH = (int32_t) ((float) view->portHeight * displayScaleY + 0.5f); + float viewAngle = view->viewAngle; + +#ifdef __3DS__ + Runner_prepare3DSDrawBattleState(runner); + bool topBattleViewActive = Runner_is3DSBattleActive(runner); + N3DSRenderer_setTopScreenBattleViewActive(renderer, topBattleViewActive); +#endif + + runner->viewCurrent = (int32_t) vi; + runner->drawViewBoundsValid = true; + runner->drawViewX = viewX; + runner->drawViewY = viewY; + runner->drawViewWidth = viewW; + runner->drawViewHeight = viewH; + renderer->vtable->beginView(renderer, viewX, viewY, viewW, viewH, portX, portY, portW, portH, viewAngle); + + Runner_draw(runner); + + if (debugShowCollisionMasks) DebugOverlay_drawCollisionMasks(runner); + + renderer->vtable->endView(renderer); +#ifdef __3DS__ + N3DSRenderer_setTopScreenBattleViewActive(renderer, false); +#endif + + int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : portW; + int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : portH; + renderer->vtable->beginGUI(renderer, guiW, guiH, portX, portY, portW, portH); + double guiStartMs = Runner_nowMs(); + Runner_drawGUI(runner); + runner->frameDrawGuiMs += Runner_nowMs() - guiStartMs; + renderer->vtable->endGUI(renderer); + + anyViewRendered = true; + } + } + + if (!anyViewRendered) { + // No views enabled: render with default full-screen view + runner->viewCurrent = 0; + runner->drawViewBoundsValid = true; + runner->drawViewX = 0; + runner->drawViewY = 0; + runner->drawViewWidth = gameW; + runner->drawViewHeight = gameH; + int32_t portX = 0; + int32_t portY = 0; + int32_t portW = gameW; + int32_t portH = gameH; +#ifdef __3DS__ + Runner_prepare3DSDrawBattleState(runner); + bool topBattleViewActive = Runner_is3DSBattleActive(runner); + N3DSRenderer_setTopScreenBattleViewActive(renderer, topBattleViewActive); +#endif + renderer->vtable->beginView(renderer, 0, 0, gameW, gameH, portX, portY, portW, portH, 0.0f); + Runner_draw(runner); + + if (debugShowCollisionMasks) DebugOverlay_drawCollisionMasks(runner); + + renderer->vtable->endView(renderer); +#ifdef __3DS__ + N3DSRenderer_setTopScreenBattleViewActive(renderer, false); +#endif + + int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : portW; + int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : portH; + renderer->vtable->beginGUI(renderer, guiW, guiH, portX, portY, portW, portH); + double guiStartMs = Runner_nowMs(); + Runner_drawGUI(runner); + runner->frameDrawGuiMs += Runner_nowMs() - guiStartMs; + renderer->vtable->endGUI(renderer); + } + + // Reset view_current to 0 so non-Draw events (Step, Alarm, Create) see view_current = 0 + runner->drawViewBoundsValid = false; + runner->n3dsDrawBattleStateValid = false; + runner->n3dsBattleReplayListsValid = false; + runner->n3dsTopScreenGUIListValid = false; + repeat(3, i) runner->n3dsTopScreenGUIResponderListsValid[i] = false; + runner->viewCurrent = 0; +} + +// ===[ Instance Creation Helper ]=== + +static bool isObjectDisabled(Runner* runner, int32_t objectIndex) { + if (runner->disabledObjects == nullptr) return false; + const char* name = runner->dataWin->objt.objects[objectIndex].name; + return shgeti(runner->disabledObjects, name) != -1; +} + +static Instance* createAndInitInstance(Runner* runner, int32_t instanceId, int32_t objectIndex, GMLReal x, GMLReal y) { + DataWin* dataWin = runner->dataWin; + require(objectIndex >= 0 && dataWin->objt.count > (uint32_t) objectIndex); + + GameObject* objDef = &dataWin->objt.objects[objectIndex]; + + Instance* inst = Instance_create(instanceId, objectIndex, x, y); + + // Copy properties from object definition + inst->spriteIndex = objDef->spriteId; + inst->visible = objDef->visible; + inst->solid = objDef->solid; + inst->persistent = objDef->persistent; + inst->depth = objDef->depth; + inst->maskIndex = objDef->textureMaskId; + + hmput(runner->instancesById, instanceId, inst); + arrput(runner->instances, inst); + Runner_addInstanceToObjectLists(runner, inst); + runner->drawableListStructureDirty = true; + +#ifdef ENABLE_VM_TRACING + if (shgeti(runner->vmContext->instanceLifecyclesToBeTraced, "*") != -1 || shgeti(runner->vmContext->instanceLifecyclesToBeTraced, objDef->name) != -1) { + fprintf(stderr, "VM: Instance %s (instanceId=%d,objectIndex=%d) created at (%f, %f)\n", objDef->name, instanceId, inst->objectIndex, x, y); + } +#endif + + return inst; +} + +// ===[ Room Management ]=== + +// Collect persistent instances from the previous room (they travel with the player), and free the rest. +// You should re-append them at the tail AFTER creating the new room's own instances, so the iteration order matches the native runner: room-local instances first, persistent arrivals last. +static Instance** takePersistentInstances(Runner* runner) { + Instance** carriedPersistent = nullptr; + int32_t oldCount = (int32_t) arrlen(runner->instances); + repeat(oldCount, i) { + Instance* inst = runner->instances[i]; + if (inst->persistent) { +#ifdef ENABLE_VM_TRACING + GameObject* gameObject = &runner->dataWin->objt.objects[inst->objectIndex]; + if (shgeti(runner->vmContext->instanceLifecyclesToBeTraced, "*") != -1 || shgeti(runner->vmContext->instanceLifecyclesToBeTraced, gameObject->name) != -1) { + fprintf(stderr, "VM: Instance %s (instanceId=%d,objectIndex=%d) has been persisted at (%f, %f) due to room change\n", gameObject->name, inst->instanceId, inst->objectIndex, inst->x, inst->y); + } +#endif + + arrput(carriedPersistent, inst); + } else { +#ifdef ENABLE_VM_TRACING + GameObject* gameObject = &runner->dataWin->objt.objects[inst->objectIndex]; + if (shgeti(runner->vmContext->instanceLifecyclesToBeTraced, "*") != -1 || shgeti(runner->vmContext->instanceLifecyclesToBeTraced, gameObject->name) != -1) { + fprintf(stderr, "VM: Instance %s (instanceId=%d,objectIndex=%d) destroyed at (%f, %f) due to room change\n", gameObject->name, inst->instanceId, inst->objectIndex, inst->x, inst->y); + } +#endif + + hmdel(runner->instancesById, inst->instanceId); + Instance_free(inst); + } + } + + arrfree(runner->instances); + runner->instances = nullptr; + + // The per-object lists referenced both the freed non-persistents and the carried persistents; clear them entirely. + // Persistents are re-added when they return via returnPersistentInstances, and room-local instances are added as they get created. + Runner_clearAllObjectLists(runner); + + return carriedPersistent; +} + +// Append the carried-over persistent instances at the tail of runner->instances and free the temporary array. Pairs with takePersistentInstances. +static void returnPersistentInstances(Runner* runner, Instance** carriedPersistent) { + repeat(arrlen(carriedPersistent), i) { + arrput(runner->instances, carriedPersistent[i]); + Runner_addInstanceToObjectLists(runner, carriedPersistent[i]); + } + arrfree(carriedPersistent); +} + +static void copyRoomViewToRuntimeView(RoomView* roomView, RuntimeView* runtimeView) { + runtimeView->enabled = roomView->enabled; + runtimeView->viewX = roomView->viewX; + runtimeView->viewY = roomView->viewY; + runtimeView->viewWidth = roomView->viewWidth; + runtimeView->viewHeight = roomView->viewHeight; + runtimeView->portX = roomView->portX; + runtimeView->portY = roomView->portY; + runtimeView->portWidth = roomView->portWidth; + runtimeView->portHeight = roomView->portHeight; + runtimeView->borderX = roomView->borderX; + runtimeView->borderY = roomView->borderY; + runtimeView->speedX = roomView->speedX; + runtimeView->speedY = roomView->speedY; + runtimeView->objectId = roomView->objectId; + runtimeView->viewAngle = 0; +} + +static void initRoom(Runner* runner, int32_t roomIndex) { + DataWin* dataWin = runner->dataWin; + require(roomIndex >= 0 && dataWin->room.count > (uint32_t) roomIndex); + double roomStartMs = Runner_nowMs(); + double phaseStartMs = roomStartMs; + + Room* room = &dataWin->room.rooms[roomIndex]; + fprintf(stderr, "Runner: Initializing room: %s (index %d)\n", room->name, roomIndex); + + // Lazy-room load: if the payload wasn't loaded, read it from the data.win file now before anything touches the room's game objects/tiles/layers. + if (!room->payloadLoaded) { + DataWin_loadRoomPayload(dataWin, roomIndex); + } + Runner_logTiming("room payload load", phaseStartMs, roomStartMs); + + SavedRoomState* savedState = &runner->savedRoomStates[roomIndex]; + + runner->currentRoom = room; + runner->currentRoomIndex = roomIndex; + // Tile set, runtime layers, and instance list all change when entering a room. + runner->drawableListStructureDirty = true; + Runner_buildTileLayerCaches(runner); + // It could be the first time we are initializing the grid + if (runner->spatialGrid != nullptr) + SpatialGrid_free(runner->spatialGrid); + runner->spatialGrid = SpatialGrid_create(room->width, room->height); + + // Find position in room order + runner->currentRoomOrderPosition = -1; + repeat(dataWin->gen8.roomOrderCount, i) { + if (dataWin->gen8.roomOrder[i] == roomIndex) { + runner->currentRoomOrderPosition = (int32_t) i; + break; + } + } + + // If this is a persistent room that was previously visited, restore saved state + if (room->persistent && savedState->initialized) { + memcpy(runner->views, savedState->views, sizeof(runner->views)); + + // Restore backgrounds from saved state + memcpy(runner->backgrounds, savedState->backgrounds, sizeof(runner->backgrounds)); + runner->backgroundColor = savedState->backgroundColor; + runner->drawBackgroundColor = savedState->drawBackgroundColor; + + // Restore tile layer map + hmfree(runner->tileLayerMap); + runner->tileLayerMap = savedState->tileLayerMap; + savedState->tileLayerMap = nullptr; + + // Restore runtime layers + freeRuntimeLayersArray(&runner->runtimeLayers); + runner->runtimeLayers = savedState->runtimeLayers; + savedState->runtimeLayers = nullptr; + + Instance** carriedPersistent = takePersistentInstances(runner); + + // The native runner restores the room's own linked list first, then appends persistent arrivals at the tail. + // Event iteration is forward (oldest first), so a persistent instance runs after the room's own instances. + int32_t savedCount = (int32_t) arrlen(savedState->instances); + repeat(savedCount, i) { + arrput(runner->instances, savedState->instances[i]); + Runner_addInstanceToObjectLists(runner, savedState->instances[i]); + } + arrfree(savedState->instances); + savedState->instances = nullptr; + + returnPersistentInstances(runner, carriedPersistent); + + // No Create events, no preCreateCode, no creationCode, no room creation code + fprintf(stderr, "Runner: Room restored (persistent): %s (room %d) with %d instances\n", room->name, roomIndex, (int) arrlen(runner->instances)); + phaseStartMs = Runner_nowMs(); + if (runner->renderer != nullptr && runner->renderer->vtable->prewarmRoom != nullptr) { + runner->renderer->vtable->prewarmRoom(runner->renderer, runner); + } + Runner_logTiming("renderer prewarm (restore)", phaseStartMs, roomStartMs); + phaseStartMs = Runner_nowMs(); + if (runner->audioSystem != nullptr && runner->audioSystem->vtable->prewarmRoom != nullptr) { + runner->audioSystem->vtable->prewarmRoom(runner->audioSystem, runner); + } + Runner_logTiming("audio prewarm (restore)", phaseStartMs, roomStartMs); + Runner_logTiming("initRoom persistent total", roomStartMs, roomStartMs); + return; + } + + // === Normal room initialization (first visit, or non-persistent room) === + + // Initialize the views from scratch + repeat(MAX_VIEWS, vi) { + copyRoomViewToRuntimeView(&room->views[vi], &runner->views[vi]); + } + + // Reset tile layer state for the new room + hmfree(runner->tileLayerMap); + runner->tileLayerMap = nullptr; + + // Populate runtime layers from parsed room layers (GMS2+ only; empty for GMS1.x). + // Dynamic layers created via layer_create are appended to this array later. + freeRuntimeLayersArray(&runner->runtimeLayers); + uint32_t maxLayerId = 0; + repeat(room->layerCount, i) { + RoomLayer* layerSource = &room->layers[i]; + RuntimeLayer runtimeLayer = { + .id = layerSource->id, + .depth = layerSource->depth, + .visible = layerSource->visible, + .xOffset = layerSource->xOffset, + .yOffset = layerSource->yOffset, + .hSpeed = layerSource->hSpeed, + .vSpeed = layerSource->vSpeed, + .dynamic = false, + .dynamicName = nullptr, + .elements = nullptr, + }; + arrput(runner->runtimeLayers, runtimeLayer); + if (layerSource->id > maxLayerId) maxLayerId = layerSource->id; + } + // Watermark: ensure runtime-allocated IDs (layers + elements) stay above parsed IDs. + if (maxLayerId >= runner->nextLayerId) runner->nextLayerId = maxLayerId + 1; + + // Populate runtime sprite elements for Assets layers, so they can be queried and destroyed via layer_sprite_get_sprite/layer_sprite_destroy + repeat(room->layerCount, i) { + RoomLayer* layerSource = &room->layers[i]; + if (layerSource->type != RoomLayerType_Assets || layerSource->assetsData == nullptr) continue; + RoomLayerAssetsData* assets = layerSource->assetsData; + RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; + repeat(assets->spriteCount, j) { + SpriteInstance* src = &assets->sprites[j]; + RuntimeSpriteElement* spriteElement = safeMalloc(sizeof(RuntimeSpriteElement)); + spriteElement->spriteIndex = src->spriteIndex; + spriteElement->x = src->x; + spriteElement->y = src->y; + spriteElement->scaleX = src->scaleX; + spriteElement->scaleY = src->scaleY; + spriteElement->color = src->color; + spriteElement->animationSpeed = src->animationSpeed; + spriteElement->animationSpeedType = src->animationSpeedType; + spriteElement->frameIndex = src->frameIndex; + spriteElement->rotation = src->rotation; + RuntimeLayerElement el = { + .id = Runner_getNextLayerId(runner), + .type = RuntimeLayerElementType_Sprite, + .backgroundElement = nullptr, + .spriteElement = spriteElement, + }; + arrput(runtimeLayer->elements, el); + } + } + + // Copy room background definitions into mutable runtime state + runner->backgroundColor = room->backgroundColor; + runner->drawBackgroundColor = room->drawBackgroundColor; + repeat(8, i) { + RoomBackground* src = &room->backgrounds[i]; + RuntimeBackground* dst = &runner->backgrounds[i]; + dst->visible = src->enabled; + dst->foreground = src->foreground; + dst->backgroundIndex = src->backgroundDefinition; + dst->x = (float) src->x; + dst->y = (float) src->y; + dst->tileX = (bool) src->tileX; + dst->tileY = (bool) src->tileY; + dst->speedX = (float) src->speedX; + dst->speedY = (float) src->speedY; + dst->stretch = src->stretch; + dst->alpha = 1.0f; + } + + Instance** carriedPersistent = takePersistentInstances(runner); + + // Two-pass instance creation (matches HTML5 runner behavior): + // Pass 1: Create all instance objects so they exist for cross-references + // Pass 2: Fire preCreateCode, CREATE events, and creationCode + // This ensures that when an instance's Create event reads another instance + // (e.g. obj_mainchara reading obj_markerA.x), the target already exists. + + // Pass 1: Create all instances without firing events + phaseStartMs = Runner_nowMs(); + repeat(room->gameObjectCount, i) { + RoomGameObject* roomObj = &room->gameObjects[i]; + + // Skip if a persistent instance carried over from the previous room already owns this ID (re-entering the persistent instance's home room, don't create a duplicate!). + if (hmget(runner->instancesById, roomObj->instanceID) != nullptr) continue; + if (isObjectDisabled(runner, roomObj->objectDefinition)) continue; + + Instance* inst = createAndInitInstance(runner, roomObj->instanceID, roomObj->objectDefinition, (GMLReal) roomObj->x, (GMLReal) roomObj->y); + inst->imageXscale = (float) roomObj->scaleX; + inst->imageYscale = (float) roomObj->scaleY; + inst->imageAngle = (float) roomObj->rotation; + inst->imageSpeed = roomObj->imageSpeed; + inst->imageIndex = (float) roomObj->imageIndex; + } + Runner_logTiming("instance create pass1", phaseStartMs, roomStartMs); + + // In GMS2, instances get their depth from their room layer, not the object definition. + // This must happen before firing Create events so scripts like scr_depth() read the layer depth. + phaseStartMs = Runner_nowMs(); + if (DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0)) { + repeat(room->layerCount, li) { + RoomLayer* layer = &room->layers[li]; + if (layer->type != RoomLayerType_Instances || layer->instancesData == nullptr) continue; + RoomLayerInstancesData* layerData = layer->instancesData; + repeat(layerData->instanceCount, ii) { + Instance* inst = hmget(runner->instancesById, layerData->instanceIds[ii]); + if (inst != nullptr) { + inst->depth = layer->depth; + inst->layer = (int32_t) layer->id; + } + } + } + } + Runner_logTiming("layer/depth setup", phaseStartMs, roomStartMs); + + // Append persistent instances carried over from the previous room at the tail, so forward event iteration processes the new room's own instances first and the travelers last. + // We NEED to do this here BEFORE firing the room object's events, to avoid code that relies on persistent instances failing (example: if a object uses instance_number to get the number of instances in the room). + returnPersistentInstances(runner, carriedPersistent); + + // Pass 2: Fire events for newly created instances (in room definition order) + phaseStartMs = Runner_nowMs(); + repeat(room->gameObjectCount, i) { + RoomGameObject* roomObj = &room->gameObjects[i]; + + Instance* inst = hmget(runner->instancesById, roomObj->instanceID); + if (inst == nullptr) continue; + + // Skip instances that already had their Create event fired (persistent carry-overs + // that hmget also matches, since instancesById still holds them). + if (inst->createEventFired) continue; + inst->createEventFired = true; + + Runner_executeEvent(runner, inst, EVENT_PRECREATE, 0); + executeCode(runner, inst, roomObj->preCreateCode); + Runner_executeEvent(runner, inst, EVENT_CREATE, 0); + executeCode(runner, inst, roomObj->creationCode); + } + Runner_logTiming("instance create pass2", phaseStartMs, roomStartMs); + + // Run room creation code + phaseStartMs = Runner_nowMs(); + if (room->creationCodeId >= 0 && dataWin->code.count > (uint32_t) room->creationCodeId) { + // Room creation code runs in global context, the native runner creates a fake/dummy instance for the "self" + Instance* dummy = Instance_create(0, -1, 0, 0); + runner->vmContext->currentInstance = dummy; + RValue result = VM_executeCode(runner->vmContext, room->creationCodeId); + RValue_free(&result); + runner->vmContext->currentInstance = nullptr; + Instance_free(dummy); + } + Runner_logTiming("room creation code", phaseStartMs, roomStartMs); + + // Mark this room as initialized for persistent room support + savedState->initialized = true; + + fprintf(stderr, "Runner: Room loaded: %s (room %d) with %d instances\n", room->name, roomIndex, (int) arrlen(runner->instances)); + phaseStartMs = Runner_nowMs(); + if (runner->renderer != nullptr && runner->renderer->vtable->prewarmRoom != nullptr) { + runner->renderer->vtable->prewarmRoom(runner->renderer, runner); + } + Runner_logTiming("renderer prewarm", phaseStartMs, roomStartMs); + phaseStartMs = Runner_nowMs(); + if (runner->audioSystem != nullptr && runner->audioSystem->vtable->prewarmRoom != nullptr) { + runner->audioSystem->vtable->prewarmRoom(runner->audioSystem, runner); + } + Runner_logTiming("audio prewarm", phaseStartMs, roomStartMs); + Runner_logTiming("initRoom total", roomStartMs, roomStartMs); +} + +// Cleans up the runner state, used when freeing the Runner or when restarting the Runner +static void cleanupState(Runner* runner) { + // Drop VM-side RValue holders (globals, stack, call frames) BEFORE freeing any Instance memory. This way any RVALUE_STRUCT refs decrement against still-live struct memory; otherwise we'd free a struct here and then have VM_free's later VM_reset try to decRef a dangling pointer. + if (runner->vmContext != nullptr) { + VM_reset(runner->vmContext); + } + + // Free all instances + repeat(arrlen(runner->instances), i) { + hmdel(runner->instancesById, runner->instances[i]->instanceId); + Instance_free(runner->instances[i]); + } + arrfree(runner->instances); + runner->instances = nullptr; + + // Empty the per-object lists. We keep the outer instancesByObject array allocated so Runner_reset can be reused; Runner_free releases it. + Runner_clearAllObjectLists(runner); + + // Free saved room states + if (runner->savedRoomStates != nullptr) { + repeat(runner->dataWin->room.count, i) { + SavedRoomState* state = &runner->savedRoomStates[i]; + int32_t savedCount = (int32_t) arrlen(state->instances); + repeat(savedCount, j) { + hmdel(runner->instancesById, state->instances[j]->instanceId); + Instance_free(state->instances[j]); + } + arrfree(state->instances); + hmfree(state->tileLayerMap); + freeRuntimeLayersArray(&state->runtimeLayers); + } + free(runner->savedRoomStates); + } + runner->savedRoomStates = nullptr; + + // Free struct instances (created via @@NewGMLObject@@). Anything still here at shutdown is leaked refs or a reference cycle - bulk free regardless of refCount. + repeat(arrlen(runner->structInstances), i) { + Instance* s = runner->structInstances[i]; + hmdel(runner->instancesById, s->instanceId); + s->structRegistryIndex = -1; + Instance_free(s); + } + arrfree(runner->structInstances); + runner->structInstances = nullptr; + + hmfree(runner->instancesById); + runner->instancesById = nullptr; + hmfree(runner->tileLayerMap); + runner->tileLayerMap = nullptr; + Runner_freeTileLayerCaches(runner); + freeRuntimeLayersArray(&runner->runtimeLayers); + shfree(runner->disabledObjects); + runner->disabledObjects = nullptr; + + // Free ds_map pool + repeat((int32_t) arrlen(runner->dsMapPool), i) { + DsMapEntry* map = runner->dsMapPool[i]; + if (map != nullptr) { + repeat(shlen(map), j) { + free(map[j].key); + RValue_free(&map[j].value); + } + shfree(map); + } + } + arrfree(runner->dsMapPool); + runner->dsMapPool = nullptr; + + // Free ds_list pool + repeat((int32_t) arrlen(runner->dsListPool), i) { + DsList* list = &runner->dsListPool[i]; + repeat(arrlen(list->items), j) { + RValue_free(&list->items[j]); + } + arrfree(list->items); + } + arrfree(runner->dsListPool); + runner->dsListPool = nullptr; + + // Free mp_grid pool + repeat((int32_t) arrlen(runner->mpGridPool), i) { + free(runner->mpGridPool[i].cells); + } + arrfree(runner->mpGridPool); + runner->mpGridPool = nullptr; + + arrfree(runner->musicInstanceStack); + runner->musicInstanceStack = nullptr; + + // Free INI state + if (runner->currentIni != nullptr) { + Ini_free(runner->currentIni); + runner->currentIni = nullptr; + } + free(runner->currentIniPath); + runner->currentIniPath = nullptr; + if (runner->cachedIni != nullptr) { + Ini_free(runner->cachedIni); + runner->cachedIni = nullptr; + } + free(runner->cachedIniPath); + runner->cachedIniPath = nullptr; + + // Free open text files + repeat(MAX_OPEN_TEXT_FILES, i) { + OpenTextFile* file = &runner->openTextFiles[i]; + if (file->isOpen) { + free(file->content); + free(file->writeBuffer); + free(file->filePath); + *file = (OpenTextFile) {0}; + } + } + + if (runner->spatialGrid != nullptr) { + SpatialGrid_free(runner->spatialGrid); + runner->spatialGrid = nullptr; + } +} + +// ===[ Public API ]=== + +void Runner_reset(Runner* runner) { + // This actually sets the default runner values, used for initialization and restarting + cleanupState(runner); + + // Reset VM state + VM_reset(runner->vmContext); + + runner->pendingRoom = -1; + runner->asyncLoadMapId = -1; + runner->gameStartFired = false; + runner->currentRoomIndex = -1; + runner->currentRoomOrderPosition = -1; + runner->nextInstanceId = runner->dataWin->gen8.lastObj + 1; + runner->savedRoomStates = safeCalloc(runner->dataWin->room.count, sizeof(SavedRoomState)); + runner->nextLayerId = 1; + runner->audioSystem->vtable->stopAll(runner->audioSystem); + + // Allocate the per-object instance list array once. + // We don't need to reinitialize the list because the objt.count is fixed for this data.win. + if (runner->instancesByObject == nullptr) { + runner->instancesByObject = safeCalloc(runner->dataWin->objt.count, sizeof(Instance**)); + } + if (runner->instancesByExactObject == nullptr) { + runner->instancesByExactObject = safeCalloc(runner->dataWin->objt.count, sizeof(Instance**)); + } + + // Create the instance used for "self" in GLOB scripts + Instance_free(runner->globalScopeInstance); + runner->globalScopeInstance = Instance_create(0, -1, 0, 0); + + // Reset builtin function state + runner->mpPotMaxrot = 30.0; + runner->mpPotStep = 10.0; + runner->mpPotAhead = 3.0; + runner->mpPotOnSpot = true; + runner->lastMusicInstance = -1; + arrsetlen(runner->musicInstanceStack, 0); + + arrsetlen(runner->cachedDrawables, 0); + runner->drawableListStructureDirty = true; + runner->drawableListSortDirty = false; +} + +// Populates objectsWithAnyEventOfType[eventType] from the resolved event table: for each event type, the deduplicated list of concrete object indices that respond to ANY subtype of that event. Walks the inverted bySlot index per slot and dedups via a scratch byte set. +// Used by collision dispatch to skip non-collision objects in the outer loop, mirroring how the native obj_has_event table partitions instance iteration by event class. +static void populateObjectsWithAnyEventOfType(Runner* runner) { + int32_t objectCount = (int32_t) runner->dataWin->objt.count; + runner->objectsWithAnyEventOfType = safeCalloc(OBJT_EVENT_TYPE_COUNT, sizeof(int32_t*)); + if (objectCount == 0) return; + + uint8_t* seen = safeCalloc((size_t) objectCount, 1); + + repeat(OBJT_EVENT_TYPE_COUNT, t) { + int16_t* dense = runner->eventSlotMap.denseLookup[t]; + if (dense == nullptr) continue; + int32_t maxSub = runner->eventSlotMap.maxSubtypeByType[t]; + memset(seen, 0, (size_t) objectCount); + + for (int32_t sub = 0; maxSub >= sub; sub++) { + int32_t slot = dense[sub]; + if (0 > slot) continue; + uint32_t entryCount; + SlotResponderEntry* entries = ResolvedEventTable_slotEntries(&runner->eventTable, slot, &entryCount); + repeat(entryCount, i) { + int32_t obj = entries[i].concreteObjectId; + if (obj < 0 || obj >= objectCount) continue; + if (seen[obj]) continue; + seen[obj] = 1; + arrput(runner->objectsWithAnyEventOfType[t], obj); + } + } + } + + free(seen); +} + +Runner* Runner_create(DataWin* dataWin, VMContext* vm, Renderer* renderer, FileSystem* fileSystem, AudioSystem* audioSystem) { + requireNotNull(dataWin); + requireNotNull(vm); + requireNotNull(renderer); + requireNotNull(fileSystem); + requireNotNull(audioSystem); + + Runner* runner = safeCalloc(1, sizeof(Runner)); + runner->dataWin = dataWin; + runner->vmContext = vm; + runner->renderer = renderer; + runner->fileSystem = fileSystem; + runner->audioSystem = audioSystem; + runner->frameCount = 0; + runner->osType = OS_WINDOWS; + runner->keyboard = RunnerKeyboard_create(); + runner->gamepads = RunnerGamepad_create(); + runner->frameStepTopObjectIndex = -1; + if (dataWin->objt.count > 0) { + runner->frameStepObjectMsByObject = safeCalloc(dataWin->objt.count, sizeof(double)); + runner->frameStepObjectCallsByObject = safeCalloc(dataWin->objt.count, sizeof(uint32_t)); + } + + // Collision compatibility mode is "enabled" for all pre-GM 2022.1 games AND for any post-GM 2022.1 games that have the bit 27 set + runner->collisionCompatibilityMode = (dataWin->detectedFormat.major == 1) || (((dataWin->optn.info >> 27) & 1) != 0); + + // Build the event dispatch acceleration tables. + EventSlotMap_build(&runner->eventSlotMap, dataWin); + ResolvedEventTable_build(&runner->eventTable, dataWin, &runner->eventSlotMap); + + // Create assets map + shdefault(runner->assetsByName, -1); + repeat(dataWin->objt.count, i) { + shput(runner->assetsByName, dataWin->objt.objects[i].name, i); + } + repeat(dataWin->sprt.count, i) { + shput(runner->assetsByName, dataWin->sprt.sprites[i].name, i); + } + repeat(dataWin->sond.count, i) { + shput(runner->assetsByName, dataWin->sond.sounds[i].name, i); + } + repeat(dataWin->bgnd.count, i) { + shput(runner->assetsByName, dataWin->bgnd.backgrounds[i].name, i); + } + repeat(dataWin->path.count, i) { + shput(runner->assetsByName, dataWin->path.paths[i].name, i); + } + repeat(dataWin->scpt.count, i) { + shput(runner->assetsByName, dataWin->scpt.scripts[i].name, i); + } + repeat(dataWin->font.count, i) { + shput(runner->assetsByName, dataWin->font.fonts[i].name, i); + } + repeat(dataWin->tmln.count, i) { + shput(runner->assetsByName, dataWin->tmln.timelines[i].name, i); + } + repeat(dataWin->room.count, i) { + shput(runner->assetsByName, dataWin->room.rooms[i].name, i); + } + + Runner_reset(runner); + + populateObjectsWithAnyEventOfType(runner); + + // Link runner to VM context + vm->runner = (struct Runner*) runner; + + renderer->vtable->init(renderer, dataWin); + audioSystem->vtable->init(audioSystem, dataWin, fileSystem); + + return runner; +} + +static inline void dispatchInstanceCreationEvents(Runner* runner, Instance* inst) { + inst->createEventFired = true; + Runner_executeEvent(runner, inst, EVENT_PRECREATE, 0); + Runner_executeEvent(runner, inst, EVENT_CREATE, 0); +} + +Instance* Runner_createInstance(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex) { + if (isObjectDisabled(runner, objectIndex)) return nullptr; + Instance* inst = createAndInitInstance(runner, runner->nextInstanceId++, objectIndex, x, y); + dispatchInstanceCreationEvents(runner, inst); + return inst; +} + +// Same as Runner_createInstance, but sets depth BEFORE firing Create events so scripts like scr_depth can override. +Instance* Runner_createInstanceWithDepth(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex, int32_t depth) { + if (isObjectDisabled(runner, objectIndex)) return nullptr; + Instance* inst = createAndInitInstance(runner, runner->nextInstanceId++, objectIndex, x, y); + inst->depth = depth; + dispatchInstanceCreationEvents(runner, inst); + return inst; +} + +Instance* Runner_createInstanceWithLayer(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex, int32_t layerId) { + if (isObjectDisabled(runner, objectIndex)) return nullptr; + RuntimeLayer* rl = Runner_findRuntimeLayerById(runner, layerId); + if (rl == nullptr) { + fprintf(stderr, "Runner: instance_create_layer: Layer ID %d not found!\n", layerId); + return nullptr; + } + Instance* inst = createAndInitInstance(runner, runner->nextInstanceId++, objectIndex, x, y); + inst->layer = layerId; + inst->depth = rl->depth; + dispatchInstanceCreationEvents(runner, inst); + return inst; +} + +Instance* Runner_copyInstance(Runner* runner, Instance* source, bool performEvent) { + requireNotNull(source); + if (isObjectDisabled(runner, source->objectIndex)) return nullptr; + + Instance* inst = createAndInitInstance(runner, runner->nextInstanceId++, source->objectIndex, source->x, source->y); + Instance_copyFields(inst, source); + inst->createEventFired = true; + if (performEvent) { + Runner_executeEvent(runner, inst, EVENT_PRECREATE, 0); + Runner_executeEvent(runner, inst, EVENT_CREATE, 0); + } + return inst; +} + +void Runner_destroyInstance(MAYBE_UNUSED Runner* runner, Instance* inst) { + Runner_executeEvent(runner, inst, EVENT_DESTROY, 0); + // A destroyed instance must ALWAYS be not active + // If a destroyed instance is active, then well, something went VERY wrong + inst->active = false; + inst->destroyed = true; + +#ifdef ENABLE_VM_TRACING + GameObject* gameObject = &runner->dataWin->objt.objects[inst->objectIndex]; + if (shgeti(runner->vmContext->instanceLifecyclesToBeTraced, "*") != -1 || shgeti(runner->vmContext->instanceLifecyclesToBeTraced, gameObject->name) != -1) { + fprintf(stderr, "VM: Instance %s (instanceId=%d,objectIndex=%d) destroyed\n", gameObject->name, inst->instanceId, inst->objectIndex); + } +#endif +} + +RuntimeLayer* Runner_findRuntimeLayerById(Runner* runner, int32_t id) { + size_t count = arrlenu(runner->runtimeLayers); + repeat(count, i) { + if ((int32_t) runner->runtimeLayers[i].id == id) + return &runner->runtimeLayers[i]; + } + return nullptr; +} + +RoomLayer* Runner_findRoomLayerById(Runner* runner, int32_t id) { + if (runner->currentRoom == nullptr) return nullptr; + repeat(runner->currentRoom->layerCount, i) { + if ((int32_t) runner->currentRoom->layers[i].id == id) return &runner->currentRoom->layers[i]; + } + return nullptr; +} + +RuntimeLayerElement* Runner_findLayerElementById(Runner* runner, int32_t elementId, RuntimeLayer** outLayer) { + size_t layerCount = arrlenu(runner->runtimeLayers); + repeat(layerCount, i) { + RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; + size_t elementCount = arrlenu(runtimeLayer->elements); + repeat(elementCount, j) { + if ((int32_t) runtimeLayer->elements[j].id == elementId) { + if (outLayer != nullptr) + *outLayer = runtimeLayer; + + return &runtimeLayer->elements[j]; + } + } + } + if (outLayer != nullptr) *outLayer = nullptr; + return nullptr; +} + +uint32_t Runner_getNextLayerId(Runner* runner) { + return runner->nextLayerId++; +} + +// Reaps GML structs whose only remaining ref is the structInstances registry's implicit +1. +// Walks backward so that swap-remove of dead entries doesn't disturb the indexes of entries we haven't visited yet. +static void Runner_sweepDeadStructs(Runner* runner) { + int32_t count = (int32_t) arrlen(runner->structInstances); + for (int32_t i = count - 1; i >= 0; i--) { + Instance* s = runner->structInstances[i]; + if (s->refCount > 1) continue; // still referenced by user code + require(s->refCount == 1); + + // Remove from runner->instancesById so future findInstanceByTarget(id) returns nullptr. + hmdel(runner->instancesById, s->instanceId); + + // O(1) swap-remove from structInstances, keeping structRegistryIndex in sync. + int32_t lastIdx = (int32_t) arrlen(runner->structInstances) - 1; + if (i != lastIdx) { + Instance* moved = runner->structInstances[lastIdx]; + runner->structInstances[i] = moved; + moved->structRegistryIndex = i; + } + arrpop(runner->structInstances); + + s->structRegistryIndex = -1; + s->refCount = 0; // drop the registry's ref; we are about to free + Instance_free(s); + } +} + +void Runner_cleanupDestroyedInstances(Runner* runner) { + int32_t count = (int32_t) arrlen(runner->instances); + int32_t writeIdx = 0; + repeat(count, i) { + Instance* inst = runner->instances[i]; + if (!inst->destroyed) { + runner->instances[writeIdx++] = inst; + } else { + Runner_removeInstanceFromObjectLists(runner, inst); + hmdel(runner->instancesById, inst->instanceId); + Instance_free(inst); + // Cached drawables hold raw Instance* that we just freed; force a rebuild before the next draw. + runner->drawableListStructureDirty = true; + } + } + arrsetlen(runner->instances, writeIdx); +} + +void Runner_initFirstRoom(Runner* runner) { + DataWin* dataWin = runner->dataWin; + require(dataWin->gen8.roomOrderCount > 0); + double initStartMs = Runner_nowMs(); + double phaseStartMs = initStartMs; + + int32_t firstRoomIndex = dataWin->gen8.roomOrder[0]; + fprintf(stderr, "Runner: First room index: %d, room count: %u\n", firstRoomIndex, dataWin->room.count); + + // Run global init scripts with the global scope instance as "self" + // In GMS 2.3+ (BC17), GLOB scripts store function declarations on "self" via Pop.v.v + runner->vmContext->currentInstance = runner->globalScopeInstance; + repeat(dataWin->glob.count, i) { + int32_t codeId = dataWin->glob.codeIds[i]; + if (codeId >= 0 && dataWin->code.count > (uint32_t) codeId) { + fprintf(stderr, "Runner: Executing global init script: %s\n", dataWin->code.entries[codeId].name); + RValue result = VM_executeCode(runner->vmContext, codeId); + RValue_free(&result); + } + } + runner->vmContext->currentInstance = nullptr; + Runner_logTiming("global init scripts", phaseStartMs, initStartMs); + + // Initialize the first room + phaseStartMs = Runner_nowMs(); + initRoom(runner, firstRoomIndex); + Runner_logTiming("initRoom(first)", phaseStartMs, initStartMs); + + // Fire Game Start for all instances + phaseStartMs = Runner_nowMs(); + Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_GAME_START); + runner->gameStartFired = true; + Runner_logTiming("game start events", phaseStartMs, initStartMs); + + // Fire Room Start for all instances + phaseStartMs = Runner_nowMs(); + Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ROOM_START); + Runner_logTiming("room start events", phaseStartMs, initStartMs); + + // Handle room_goto (or game_restart) called during Create/GameStart/RoomStart events. + // This is common in GMS2 games that use an initializer object to jump to the real first room. + // We process room transitions here so the main loop always starts in the correct room, + // without running a wasted step on the initializer room first. + if (runner->pendingRoom == ROOM_RESTARTGAME) { + Runner_logTiming("init pending restart", initStartMs, initStartMs); + Runner_reset(runner); + Runner_initFirstRoom(runner); + return; + } + if (runner->pendingRoom >= 0) { + int32_t newRoomIndex = runner->pendingRoom; + runner->pendingRoom = -1; + require(dataWin->room.count > (uint32_t) newRoomIndex); + fprintf(stderr, "Runner: room_goto called during init, transitioning: %s (room %d) -> %s (room %d)\n", + runner->currentRoom->name, runner->currentRoomIndex, + dataWin->room.rooms[newRoomIndex].name, newRoomIndex); + + // Fire Room End on the init room's instances before leaving + Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ROOM_END); + + if (runner->dataWin->lazyLoadRooms && runner->currentRoom != nullptr && + !runner->currentRoom->eagerlyLoaded && newRoomIndex != runner->currentRoomIndex) { + DataWin_freeRoomPayload(runner->currentRoom); + } + + phaseStartMs = Runner_nowMs(); + initRoom(runner, newRoomIndex); + Runner_logTiming("initRoom(pending)", phaseStartMs, initStartMs); + phaseStartMs = Runner_nowMs(); + Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ROOM_START); + Runner_logTiming("room start pending", phaseStartMs, initStartMs); + } + Runner_logTiming("Runner_initFirstRoom total", initStartMs, initStartMs); +} + +// ===[ Collision Event Dispatch ]=== + +static void executeCollisionEvent(Runner* runner, Instance* self, Instance* other, int32_t targetObjectIndex) { + VMContext* vm = runner->vmContext; + + // Save event context + int32_t savedEventType = vm->currentEventType; + int32_t savedEventSubtype = vm->currentEventSubtype; + int32_t savedEventObjectIndex = vm->currentEventObjectIndex; + struct Instance* savedOtherInstance = vm->otherInstance; + + // Set collision event context + vm->currentEventType = EVENT_COLLISION; + vm->currentEventSubtype = targetObjectIndex; + vm->otherInstance = other; + + int32_t ownerObjectIndex = -1; + int32_t codeId = findEventCodeIdAndOwner(runner, self->objectIndex, EVENT_COLLISION, targetObjectIndex, &ownerObjectIndex); + + vm->currentEventObjectIndex = ownerObjectIndex; + +#ifdef ENABLE_VM_TRACING + if (codeId >= 0 && shlen(vm->eventsToBeTraced) != -1) { + const char* selfName = runner->dataWin->objt.objects[self->objectIndex].name; + const char* targetName = runner->dataWin->objt.objects[targetObjectIndex].name; + bool shouldTrace = shgeti(vm->eventsToBeTraced, "*") != -1 || shgeti(vm->eventsToBeTraced, "Collision") != -1 || shgeti(vm->eventsToBeTraced, selfName) != -1; + if (shouldTrace) { + fprintf(stderr, "Runner: [%s] Collision with %s (instanceId=%d, otherId=%d)\n", selfName, targetName, self->instanceId, other->instanceId); + } + } +#endif + + executeCode(runner, self, codeId); + + // Restore event context + vm->currentEventType = savedEventType; + vm->currentEventSubtype = savedEventSubtype; + vm->currentEventObjectIndex = savedEventObjectIndex; + vm->otherInstance = savedOtherInstance; +} + +// ===[ Path Adaptation ]=== +// Advances path position and updates instance x/y (HTML5: yyInstance.js Adapt_Path, lines 2755-2881) +// Returns true if end of path was reached (and pathSpeed != 0), to fire OTHER_END_OF_PATH event. +static bool adaptPath(Runner* runner, Instance* inst) { + if (0 > inst->pathIndex) return false; + + DataWin* dataWin = runner->dataWin; + if ((uint32_t) inst->pathIndex >= dataWin->path.count) return false; + + GamePath* path = &dataWin->path.paths[inst->pathIndex]; + if (0.0 >= path->length) return false; + + bool atPathEnd = false; + + GMLReal orient = inst->pathOrientation * M_PI / 180.0; + + // Get current position's speed factor + PathPositionResult cur = GamePath_getPosition(path, inst->pathPosition); + GMLReal sp = cur.speed / (100.0 * inst->pathScale); + + // Advance position (compute in higher precision, truncate to float on store - matches native runner) + inst->pathPosition = (float) (inst->pathPosition + inst->pathSpeed * sp / path->length); + + // Handle end actions if position out of [0,1] + PathPositionResult pos0 = GamePath_getPosition(path, 0.0f); + if (inst->pathPosition >= 1.0f || 0.0f >= inst->pathPosition) { + atPathEnd = (inst->pathSpeed == 0.0f) ? false : true; + + switch (inst->pathEndAction) { + // stop moving + case 0: { + if (inst->pathSpeed >= 0.0f) { + if (inst->pathSpeed != 0.0f) { + inst->pathPosition = 1.0f; + inst->pathIndex = -1; + } + } else { + inst->pathPosition = 0.0f; + inst->pathIndex = -1; + } + break; + } + // continue from start position (restart) + case 1: { + if (0.0f > inst->pathPosition) { + inst->pathPosition += 1.0f; + } else { + inst->pathPosition -= 1.0f; + } + break; + } + // continue from current position + case 2: { + PathPositionResult pos1 = GamePath_getPosition(path, 1.0f); + GMLReal xx = pos1.x - pos0.x; + GMLReal yy = pos1.y - pos0.y; + GMLReal xdif = inst->pathScale * (xx * GMLReal_cos(orient) + yy * GMLReal_sin(orient)); + GMLReal ydif = inst->pathScale * (yy * GMLReal_cos(orient) - xx * GMLReal_sin(orient)); + + if (0.0f > inst->pathPosition) { + inst->pathXStart -= (float) xdif; + inst->pathYStart -= (float) ydif; + inst->pathPosition += 1.0f; + } else { + inst->pathXStart += (float) xdif; + inst->pathYStart += (float) ydif; + inst->pathPosition -= 1.0f; + } + break; + } + // reverse + case 3: { + if (0.0f > inst->pathPosition) { + inst->pathPosition = -inst->pathPosition; + inst->pathSpeed = (float) GMLReal_fabs(inst->pathSpeed); + } else { + inst->pathPosition = 2.0f - inst->pathPosition; + inst->pathSpeed = (float) -GMLReal_fabs(inst->pathSpeed); + } + break; + } + // default: stop + default: { + inst->pathPosition = 1.0f; + inst->pathIndex = -1; + break; + } + } + } + + // Find the new position in the room + PathPositionResult newPos = GamePath_getPosition(path, inst->pathPosition); + GMLReal xx = newPos.x - pos0.x; // relative + GMLReal yy = newPos.y - pos0.y; + + GMLReal newx = inst->pathXStart + inst->pathScale * (xx * GMLReal_cos(orient) + yy * GMLReal_sin(orient)); + GMLReal newy = inst->pathYStart + inst->pathScale * (yy * GMLReal_cos(orient) - xx * GMLReal_sin(orient)); + + // Trick to set the direction: set hspeed/vspeed to delta, which updates direction + inst->hspeed = (float) (newx - inst->x); + inst->vspeed = (float) (newy - inst->y); + Instance_computeSpeedFromComponents(inst); + + // Normal speed should not be used + inst->speed = 0.0f; + inst->hspeed = 0.0f; + inst->vspeed = 0.0f; + + // Set the new position + inst->x = (float) newx; + inst->y = (float) newy; + + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + + return atPathEnd; +} + +static void dispatchCollisionEvents(Runner* runner) { + DataWin* dataWin = runner->dataWin; + // Iterate only the objects that have any collision event in their parent chain. + int32_t* selfObjects = (runner->objectsWithAnyEventOfType != nullptr) ? runner->objectsWithAnyEventOfType[EVENT_COLLISION] : nullptr; + if (selfObjects == nullptr) return; + int32_t selfObjCount = (int32_t) arrlen(selfObjects); + + repeat(selfObjCount, soIdx) { + int32_t selfObjIdx = selfObjects[soIdx]; + Instance** selfBucket = runner->instancesByExactObject[selfObjIdx]; + int32_t selfBucketCount = (int32_t) arrlen(selfBucket); + if (selfBucketCount == 0) continue; + + // Snapshot the self bucket: collision handlers can spawn/destroy/instance_change. Iterating a snapshot also keeps newly-created instances from firing collisions in this same phase. + int32_t selfSnapBase = (int32_t) arrlen(runner->instanceSnapshots); + arrsetlen(runner->instanceSnapshots, selfSnapBase + selfBucketCount); + memcpy(&runner->instanceSnapshots[selfSnapBase], selfBucket, (size_t) selfBucketCount * sizeof(Instance*)); + + repeat(selfBucketCount, si) { + Instance* self = runner->instanceSnapshots[selfSnapBase + si]; + if (!self->active) continue; + + InstanceBBox bboxSelf; + Sprite* sprSelf; + bool selfDirty = true; + + // Walk the parent chain to find all collision event handlers for this object + int32_t currentObj = self->objectIndex; + int depth = 0; + while (currentObj >= 0 && dataWin->objt.count > (uint32_t) currentObj && 32 > depth) { + GameObject* obj = &dataWin->objt.objects[currentObj]; + + ObjectEventList* eventList = &obj->eventLists[EVENT_COLLISION]; + repeat(eventList->eventCount, evtIdx) { + ObjectEvent* evt = &eventList->events[evtIdx]; + int32_t targetObjIndex = (int32_t) evt->eventSubtype; + + if (evt->actionCount == 0 || 0 > evt->actions[0].codeId) continue; + + // Iterate only the descendant-inclusive list for the target object via a snapshot, so nested user code (collision handlers calling instance_exists, with (...), etc.) can push/pop their own snapshots above ours without corrupting this iteration. + int32_t snapBase = Runner_pushInstancesOfObject(runner, targetObjIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t snapIdx = snapBase; snapEnd > snapIdx; snapIdx++) { + Instance* other = runner->instanceSnapshots[snapIdx]; + if (!other->active) continue; + if (other == self) continue; + + // Compute bboxes + if (selfDirty) { + bboxSelf = Collision_computeBBox(dataWin, self); + sprSelf = Collision_getSprite(dataWin, self); + selfDirty = false; + } + InstanceBBox bboxOther = Collision_computeBBox(dataWin, other); + if (!bboxSelf.valid || !bboxOther.valid) continue; + + // AABB overlap test + if (bboxSelf.left >= bboxOther.right || bboxOther.left >= bboxSelf.right || bboxSelf.top >= bboxOther.bottom || bboxOther.top >= bboxSelf.bottom) + continue; + + // Precise collision check if either sprite needs it (per-pixel for sepMasks==1, OBB SAT for rotated sepMasks==2). + Sprite* sprOther = Collision_getSprite(dataWin, other); + bool needsPrecise = (sprSelf != nullptr && sprSelf->sepMasks == 1) || (sprOther != nullptr && sprOther->sepMasks == 1) || Collision_obbNeedsSAT(sprSelf, self) || Collision_obbNeedsSAT(sprOther, other); + + if (needsPrecise) { + if (!Collision_instancesOverlapPrecise(dataWin, runner->collisionCompatibilityMode, self, other, bboxSelf, bboxOther)) continue; + } + + // Collision detected! If either instance is solid, restore both to xprevious/yprevious. + bool hadSolid = self->solid || other->solid; + if (hadSolid) { + self->x = self->xprevious; + self->y = self->yprevious; + if (self->pathIndex >= 0) self->pathPosition = self->pathPositionPrevious; + other->x = other->xprevious; + other->y = other->yprevious; + if (other->pathIndex >= 0) other->pathPosition = other->pathPositionPrevious; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, self); + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, other); + } + + // We don't need to call "SpatialGrid_markInstanceAsDirty" here because *technically* just because a collision happened, doesn't mean that the instances have moved + // And if it DOES move via GML, the variable write handlers will set it to dirty + + executeCollisionEvent(runner, self, other, targetObjIndex); + + // Native parity for solids: collision event can alter path state, so run one + // post-event path adaptation and apply its hspeed/vspeed step. + if (hadSolid && self->active && other->active) { + adaptPath(runner, self); + adaptPath(runner, other); + if (self->hspeed != 0.0f || self->vspeed != 0.0f) { + self->x += self->hspeed; + self->y += self->vspeed; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, self); + } + if (other->hspeed != 0.0f || other->vspeed != 0.0f) { + other->x += other->hspeed; + other->y += other->vspeed; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, other); + } + } + + // The collision event may have moved our instance, so we'll need to regenerate our self attributes! + selfDirty = true; + } + Runner_popInstanceSnapshot(runner, snapBase); + } + + currentObj = obj->parentId; + depth++; + } + } + + arrsetlen(runner->instanceSnapshots, selfSnapBase); + } +} + +// ===[ View Following + Clamping ]=== +// Single-axis follow with border-based scrolling, room clamping, and speed limit. +static int32_t followAxis(int32_t viewPos, int32_t viewSize, int32_t targetPos, uint32_t border, int32_t speed, int32_t roomSize) { + int32_t pos = viewPos; + + // Border-based scrolling + if (2 * (int32_t) border >= viewSize) { + pos = targetPos - viewSize / 2; + } else if (targetPos - (int32_t) border < viewPos) { + pos = targetPos - (int32_t) border; + } else if (targetPos + (int32_t) border > viewPos + viewSize) { + pos = targetPos + (int32_t) border - viewSize; + } + + // Clamp to room bounds + if (0 > pos) pos = 0; + if (pos + viewSize > roomSize) pos = roomSize - viewSize; + + // Speed limit + if (speed >= 0) { + if (pos < viewPos && viewPos - pos > speed) pos = viewPos - speed; + if (pos > viewPos && pos - viewPos > speed) pos = viewPos + speed; + } + + return pos; +} + +static void updateViews(Runner* runner) { + Room* room = runner->currentRoom; + if (!(room->flags & 1)) return; + + repeat(MAX_VIEWS, vi) { + RuntimeView* view = &runner->views[vi]; + if (!view->enabled || 0 > view->objectId) continue; + + // Find first active instance of the target object. + Instance* target = nullptr; + if (view->objectId >= 0 && runner->dataWin->objt.count > (uint32_t) view->objectId) { + Instance** bucket = runner->instancesByObject[view->objectId]; + int32_t bucketCount = (int32_t) arrlen(bucket); + repeat(bucketCount, i) { + if (bucket[i]->active) { target = bucket[i]; break; } + } + } + + if (target != nullptr) { + int32_t ix = (int32_t) GMLReal_floor(target->x); + int32_t iy = (int32_t) GMLReal_floor(target->y); + view->viewX = followAxis(view->viewX, view->viewWidth, ix, view->borderX, view->speedX, (int32_t) room->width); + view->viewY = followAxis(view->viewY, view->viewHeight, iy, view->borderY, view->speedY, (int32_t) room->height); + } + } +} + +static void dispatchOutsideRoomEvents(Runner* runner) { + DataWin* dataWin = runner->dataWin; + int32_t outsideSlot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_OTHER, OTHER_OUTSIDE_ROOM); + if (0 > outsideSlot) return; + ResolvedEventTable* table = &runner->eventTable; + uint32_t entryCount; + SlotResponderEntry* entries = ResolvedEventTable_slotEntries(table, outsideSlot, &entryCount); + if (entryCount == 0) return; + + int32_t roomWidth = (int32_t) runner->currentRoom->width; + int32_t roomHeight = (int32_t) runner->currentRoom->height; + + repeat(entryCount, s) { + int32_t objIdx = entries[s].concreteObjectId; + Instance** bucket = runner->instancesByExactObject[objIdx]; + int32_t bucketCount = (int32_t) arrlen(bucket); + if (bucketCount == 0) continue; + + // All instances in the bucket share the same exact objectIndex, so the handler resolves to one (codeId, owner). + int32_t ownerObjectIndex = -1; + int32_t codeId = ResolvedEventTable_lookup(table, objIdx, outsideSlot, &ownerObjectIndex); + if (0 > codeId) continue; + + // Snapshot the bucket: an Outside Room handler can spawn/destroy/instance_change. + int32_t snapshotBase = (int32_t) arrlen(runner->instanceSnapshots); + arrsetlen(runner->instanceSnapshots, snapshotBase + bucketCount); + memcpy(&runner->instanceSnapshots[snapshotBase], bucket, (size_t) bucketCount * sizeof(Instance*)); + + repeat(bucketCount, i) { + Instance* inst = runner->instanceSnapshots[snapshotBase + i]; + if (!inst->active) continue; + + bool outside; + InstanceBBox bbox = Collision_computeBBox(dataWin, inst); + if (bbox.valid) { + outside = (0 > bbox.right || bbox.left > roomWidth || 0 > bbox.bottom || bbox.top > roomHeight); + } else { + outside = (0 > inst->x || inst->x > roomWidth || 0 > inst->y || inst->y > roomHeight); + } + + if (outside && !inst->outsideRoom) { + Runner_executeResolvedEvent(runner, inst, EVENT_OTHER, OTHER_OUTSIDE_ROOM, codeId, ownerObjectIndex); + if (runner->pendingRoom >= 0) { + arrsetlen(runner->instanceSnapshots, snapshotBase); + return; + } + } + + inst->outsideRoom = outside; + } + + arrsetlen(runner->instanceSnapshots, snapshotBase); + } +} + +static void persistRoomState(Runner* runner, int32_t roomIndex) { + SavedRoomState* state = &runner->savedRoomStates[roomIndex]; + + // Free any previously saved instances (from an earlier visit) + int32_t prevSavedCount = (int32_t) arrlen(state->instances); + repeat(prevSavedCount, i) { + hmdel(runner->instancesById, state->instances[i]->instanceId); + Instance_free(state->instances[i]); + } + arrfree(state->instances); + state->instances = nullptr; + hmfree(state->tileLayerMap); + state->tileLayerMap = nullptr; + freeRuntimeLayersArray(&state->runtimeLayers); + + // Separate persistent instances (travel with player) from room instances (saved) + Instance** keptInstances = nullptr; + int32_t count = (int32_t) arrlen(runner->instances); + repeat(count, i) { + Instance* inst = runner->instances[i]; + if (inst->persistent) { + arrput(keptInstances, inst); + } else if (inst->active) { + arrput(state->instances, inst); + } else { + hmdel(runner->instancesById, inst->instanceId); + Instance_free(inst); + } + } + arrfree(runner->instances); + runner->instances = keptInstances; + + // The per-object lists referenced the full pre-transition instance set (persistents, saved-to-state, and soon-to-be-freed). Only the kept persistents remain live, so rebuild from scratch from the final runner->instances. + Runner_clearAllObjectLists(runner); + repeat((int32_t) arrlen(runner->instances), i) { + Runner_addInstanceToObjectLists(runner, runner->instances[i]); + } + + // Save room visual state + memcpy(state->backgrounds, runner->backgrounds, sizeof(runner->backgrounds)); + memcpy(state->views, runner->views, sizeof(runner->views)); + state->backgroundColor = runner->backgroundColor; + state->drawBackgroundColor = runner->drawBackgroundColor; + + // Transfer tile layer map ownership to saved state + state->tileLayerMap = runner->tileLayerMap; + runner->tileLayerMap = nullptr; + + // Transfer runtime layer ownership to saved state + state->runtimeLayers = runner->runtimeLayers; + runner->runtimeLayers = nullptr; + + state->initialized = true; +} + +void Runner_step(Runner* runner) { + // The snapshot arena is stack-like and every push must be matched with a pop within the same frame. Assert that invariant at the top of each step: a non-zero length here means some site below pushed without popping, and we want a loud failure with the offending length so we can find it instead of silently leaking until the next frame. + requireMessageFormatted(arrlen(runner->instanceSnapshots) == 0, "instanceSnapshots arena was not fully popped at end of previous frame (length=%td)", arrlen(runner->instanceSnapshots)); + + runner->frameStepPrepMs = 0.0; + runner->frameStepEventMs = 0.0; + runner->frameStepMotionMs = 0.0; + runner->frameStepCollisionMs = 0.0; + runner->frameStepFinalizeMs = 0.0; + runner->frameStepTopObjectMs = 0.0; + runner->frameStepTopObjectCalls = 0; + runner->frameStepTopObjectIndex = -1; + if (runner->frameStepObjectMsByObject != NULL && runner->frameStepObjectCallsByObject != NULL) { + memset(runner->frameStepObjectMsByObject, 0, (size_t) runner->dataWin->objt.count * sizeof(double)); + memset(runner->frameStepObjectCallsByObject, 0, (size_t) runner->dataWin->objt.count * sizeof(uint32_t)); + } + double phaseStartMs = Runner_nowMs(); + + // Check for gamepad connect/disconnect and fire Async System event + for (int i = 0; MAX_GAMEPADS > i; i++) { + GamepadSlot* slot = &runner->gamepads->slots[i]; + if (slot->connected != slot->connectedPrev) { + DsMapEntry* map = nullptr; + arrput(runner->dsMapPool, map); + int32_t mapId = arrlen(runner->dsMapPool) - 1; + + DsMapEntry** mapPtr = &runner->dsMapPool[mapId]; + shput(*mapPtr, safeStrdup("event_type"), RValue_makeOwnedString(safeStrdup(slot->connected ? "gamepad discovered" : "gamepad lost"))); + shput(*mapPtr, safeStrdup("pad_index"), RValue_makeReal((GMLReal) i)); + + runner->asyncLoadMapId = mapId; + Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ASYNC_SYSTEM); + + // Clean up ds_map + mapPtr = &runner->dsMapPool[mapId]; + if (*mapPtr != nullptr) { + repeat(shlen(*mapPtr), j) { + free((*mapPtr)[j].key); + RValue_free(&(*mapPtr)[j].value); + } + shfree(*mapPtr); + *mapPtr = nullptr; + } + runner->asyncLoadMapId = -1; + } + } + + // Save xprevious/yprevious and path_positionprevious for all active instances + int32_t prevCount = (int32_t) arrlen(runner->instances); + repeat(prevCount, i) { + Instance* inst = runner->instances[i]; + if (inst->active) { + inst->xprevious = inst->x; + inst->yprevious = inst->y; + inst->pathPositionPrevious = inst->pathPosition; + } + } + + // Advance image_index by image_speed for all active instances + int32_t animCount = (int32_t) arrlen(runner->instances); + int32_t animEndSlot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_OTHER, OTHER_ANIMATION_END); + repeat(animCount, i) { + Instance* inst = runner->instances[i]; + if (!inst->active) continue; + if (0 > inst->spriteIndex) continue; + + inst->imageIndex += inst->imageSpeed; + + // Wrap image_index (matches HTML5 runner: manual subtract/add instead of using fmod) + Sprite* sprite = &runner->dataWin->sprt.sprites[inst->spriteIndex]; + float frameCount = (float) sprite->textureCount; + bool wrapped = false; + if (inst->imageIndex >= frameCount) { + inst->imageIndex -= frameCount; + wrapped = true; + } else if (0.0f > inst->imageIndex) { + inst->imageIndex += frameCount; + wrapped = true; + } + if (wrapped && animEndSlot >= 0) { + int32_t ownerObjectIndex = -1; + int32_t codeId = ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, animEndSlot, &ownerObjectIndex); + if (codeId >= 0) Runner_executeResolvedEvent(runner, inst, EVENT_OTHER, OTHER_ANIMATION_END, codeId, ownerObjectIndex); + } + } + + // Scroll backgrounds + Runner_scrollBackgrounds(runner); + + // Advance GMS2 layer parallax (hspeed/vspeed per frame) + size_t layerCount = arrlenu(runner->runtimeLayers); + repeat(layerCount, i) { + RuntimeLayer* rl = &runner->runtimeLayers[i]; + rl->xOffset += rl->hSpeed; + rl->yOffset += rl->vSpeed; + } + runner->frameStepPrepMs += Runner_nowMs() - phaseStartMs; + + // Execute Begin Step for all instances + phaseStartMs = Runner_nowMs(); + Runner_executeEventForAll(runner, EVENT_STEP, STEP_BEGIN); + + // Dispatch keyboard events + RunnerKeyboardState* kb = runner->keyboard; + for (int32_t key = 0; GML_KEY_COUNT > key; key++) { + if (kb->keyPressed[key]) { + Runner_executeEventForAll(runner, EVENT_KEYPRESS, key); + } + } + for (int32_t key = 0; GML_KEY_COUNT > key; key++) { + if (kb->keyDown[key]) { + Runner_executeEventForAll(runner, EVENT_KEYBOARD, key); + } + } + for (int32_t key = 0; GML_KEY_COUNT > key; key++) { + if (kb->keyReleased[key]) { + Runner_executeEventForAll(runner, EVENT_KEYRELEASE, key); + } + } + + // Process alarms. Outer loop is over alarm slots (matching the native runner's HandleAlarm), and for each slot we walk only the objects in the event table's bySlot range and only those objects' exact instance buckets. Idle instances are further skipped via activeAlarmMask. + repeat(GML_ALARM_COUNT, alarmIdx) { + int32_t alarmSlot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_ALARM, alarmIdx); + if (0 > alarmSlot) continue; + ResolvedEventTable* table = &runner->eventTable; + uint32_t entryCount; + SlotResponderEntry* entries = ResolvedEventTable_slotEntries(table, alarmSlot, &entryCount); + + repeat(entryCount, s) { + int32_t objIdx = entries[s].concreteObjectId; + Instance** bucket = runner->instancesByExactObject[objIdx]; + int32_t bucketCount = (int32_t) arrlen(bucket); + if (bucketCount == 0) continue; + + // All instances in the bucket share the same exact objectIndex, so the handler resolves to one (codeId, owner). + int32_t ownerObjectIndex = -1; + int32_t codeId = ResolvedEventTable_lookup(table, objIdx, alarmSlot, &ownerObjectIndex); + if (0 > codeId) continue; + + // Snapshot the bucket before dispatch: alarm code can call instance_change/instance_destroy/instance_create which mutate the live bucket. Iterating the snapshot also ensures newly-created instances do not fire alarms in this same phase. + int32_t snapshotBase = (int32_t) arrlen(runner->instanceSnapshots); + arrsetlen(runner->instanceSnapshots, snapshotBase + bucketCount); + memcpy(&runner->instanceSnapshots[snapshotBase], bucket, (size_t) bucketCount * sizeof(Instance*)); + + repeat(bucketCount, i) { + Instance* inst = runner->instanceSnapshots[snapshotBase + i]; + if (!inst->active) continue; + uint16_t bit = (uint16_t) (1u << alarmIdx); + if ((inst->activeAlarmMask & bit) == 0) continue; + +#ifdef ENABLE_VM_TRACING + GameObject* object = &runner->dataWin->objt.objects[inst->objectIndex]; + if (shgeti(runner->vmContext->alarmsToBeTraced, "*") != -1 || shgeti(runner->vmContext->alarmsToBeTraced, object->name) != -1) { + fprintf(stderr, "VM: [%s] Ticking down Alarm[%d] (instanceId=%d), current tick is %d\n", object->name, alarmIdx, inst->instanceId, inst->alarm[alarmIdx]); + } +#endif + + inst->alarm[alarmIdx]--; + if (inst->alarm[alarmIdx] == 0) { + inst->alarm[alarmIdx] = -1; + inst->activeAlarmMask &= (uint16_t) ~bit; + +#ifdef ENABLE_VM_TRACING + if (shgeti(runner->vmContext->alarmsToBeTraced, "*") != -1 || shgeti(runner->vmContext->alarmsToBeTraced, object->name) != -1) { + fprintf(stderr, "VM: [%s] Firing Alarm[%d] (instanceId=%d)\n", object->name, alarmIdx, inst->instanceId); + } +#endif + + Runner_executeResolvedEvent(runner, inst, EVENT_ALARM, alarmIdx, codeId, ownerObjectIndex); + } + } + + arrsetlen(runner->instanceSnapshots, snapshotBase); + } + } + + // Execute Normal Step for all instances + Runner_executeEventForAll(runner, EVENT_STEP, STEP_NORMAL); + runner->frameStepEventMs += Runner_nowMs() - phaseStartMs; + + // Apply motion: friction, gravity, then x += hspeed, y += vspeed + phaseStartMs = Runner_nowMs(); + int32_t motionCount = (int32_t) arrlen(runner->instances); + int32_t endOfPathSlot = EventSlotMap_lookup(&runner->eventSlotMap, EVENT_OTHER, OTHER_END_OF_PATH); + repeat(motionCount, mi) { + Instance* inst = runner->instances[mi]; + if (!inst->active) continue; + + // Friction: reduce speed toward zero (HTML5: AdaptSpeed) + if (inst->friction != 0.0f) { + float ns = (inst->speed > 0.0f) ? inst->speed - inst->friction : inst->speed + inst->friction; + if ((inst->speed > 0.0f && ns < 0.0f) || (inst->speed < 0.0f && ns > 0.0f)) { + inst->speed = 0.0f; + } else if (inst->speed != 0.0f) { + inst->speed = ns; + } + Instance_computeComponentsFromSpeed(inst); + } + + // Gravity: add velocity in gravity_direction (HTML5: AddTo_Speed) + if (inst->gravity != 0.0f) { + GMLReal gravDirRad = inst->gravityDirection * (M_PI / 180.0); + inst->hspeed += (float) (inst->gravity * clampFloat(GMLReal_cos(gravDirRad))); + inst->vspeed -= (float) (inst->gravity * clampFloat(GMLReal_sin(gravDirRad))); + Instance_computeSpeedFromComponents(inst); + } + + // Path adaptation (HTML5: Adapt_Path, runs after friction/gravity, before x+=hspeed) + if (adaptPath(runner, inst) && endOfPathSlot >= 0) { + int32_t ownerObjectIndex = -1; + int32_t codeId = ResolvedEventTable_lookup(&runner->eventTable, inst->objectIndex, endOfPathSlot, &ownerObjectIndex); + if (codeId >= 0) Runner_executeResolvedEvent(runner, inst, EVENT_OTHER, OTHER_END_OF_PATH, codeId, ownerObjectIndex); + } + + // Apply movement + if (inst->hspeed != 0.0f || inst->vspeed != 0.0f) { + inst->x += inst->hspeed; + inst->y += inst->vspeed; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + } + } + runner->frameStepMotionMs += Runner_nowMs() - phaseStartMs; + + // Dispatch outside room events + phaseStartMs = Runner_nowMs(); + dispatchOutsideRoomEvents(runner); + + // Dispatch collision events + dispatchCollisionEvents(runner); + runner->frameStepCollisionMs += Runner_nowMs() - phaseStartMs; + + // Execute End Step for all instances + phaseStartMs = Runner_nowMs(); + Runner_executeEventForAll(runner, EVENT_STEP, STEP_END); + + // Update view following + updateViews(runner); + + // Handle game restart + if (runner->pendingRoom == ROOM_RESTARTGAME) { + // See you soon! + // Free the currently-loaded non-eager room before reset so lazyLoadRooms stays steady-state. + if (runner->dataWin->lazyLoadRooms && runner->currentRoom != nullptr && !runner->currentRoom->eagerlyLoaded) { + DataWin_freeRoomPayload(runner->currentRoom); + } + Runner_reset(runner); + Runner_initFirstRoom(runner); + runner->frameCount++; + runner->frameStepFinalizeMs += Runner_nowMs() - phaseStartMs; + return; + } + + // Handle room transition + if (runner->pendingRoom >= 0) { + int32_t oldRoomIndex = runner->currentRoomIndex; + Room* oldRoom = runner->currentRoom; + const char* oldRoomName = oldRoom->name; + + // Fire Room End for all instances + Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ROOM_END); + + int32_t newRoomIndex = runner->pendingRoom; + runner->pendingRoom = -1; + require(runner->dataWin->room.count > (uint32_t) newRoomIndex); + const char* newRoomName = runner->dataWin->room.rooms[newRoomIndex].name; + + fprintf(stderr, "Room changed: %s (room %d) -> %s (room %d)\n", oldRoomName, oldRoomIndex, newRoomName, newRoomIndex); + + // If the old room is persistent, save its instance and visual state + if (oldRoom->persistent) { + persistRoomState(runner, oldRoomIndex); + } + + // Free the outgoing room's payload under lazyLoadRooms, unless it's eagerly pinned or we're restarting the same room (initRoom would just re-load it). + if (runner->dataWin->lazyLoadRooms && !oldRoom->eagerlyLoaded && newRoomIndex != oldRoomIndex) { + DataWin_freeRoomPayload(oldRoom); + } + + // Load new room + initRoom(runner, newRoomIndex); + + // Fire Room Start for all instances + Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ROOM_START); + } + + Runner_cleanupDestroyedInstances(runner); + Runner_sweepDeadStructs(runner); + runner->frameStepFinalizeMs += Runner_nowMs() - phaseStartMs; + + if (runner->frameStepObjectMsByObject != NULL && runner->frameStepObjectCallsByObject != NULL) { + repeat(runner->dataWin->objt.count, i) { + double objectMs = runner->frameStepObjectMsByObject[i]; + if (objectMs > runner->frameStepTopObjectMs) { + runner->frameStepTopObjectMs = objectMs; + runner->frameStepTopObjectCalls = runner->frameStepObjectCallsByObject[i]; + runner->frameStepTopObjectIndex = (int32_t) i; + } + } + } + + runner->frameCount++; +} + +// ===[ State Dump ]=== + +void Runner_dumpState(Runner* runner) { + DataWin* dataWin = runner->dataWin; + VMContext* vm = runner->vmContext; + int32_t instanceCount = (int32_t) arrlen(runner->instances); + + printf("=== Frame %d State Dump ===\n", runner->frameCount); + printf("Room: %s (index %d)\n", runner->currentRoom->name, runner->currentRoomIndex); + printf("Instance count: %d\n", instanceCount); + + repeat(instanceCount, i) { + Instance* inst = runner->instances[i]; + if (!inst->active) continue; + + GameObject* gameObject = nullptr; + const char* objName = ""; + if (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) { + gameObject = &dataWin->objt.objects[inst->objectIndex]; + objName = gameObject->name; + } + + const char* spriteName = ""; + if (inst->spriteIndex >= 0 && dataWin->sprt.count > (uint32_t) inst->spriteIndex) { + spriteName = dataWin->sprt.sprites[inst->spriteIndex].name; + } + + const char* parentName = ""; + if (gameObject != nullptr && gameObject->parentId >= 0 && dataWin->objt.count > (uint32_t) gameObject->parentId) { + parentName = dataWin->objt.objects[gameObject->parentId].name; + } + + printf("\n--- Instance #%d (%s, objectIndex=%d) ---\n", inst->instanceId, objName, inst->objectIndex); + printf(" Position: (%g, %g)\n", (double) inst->x, (double) inst->y); + printf(" Depth: %d\n", inst->depth); + printf(" Sprite: %s (index %d), imageIndex=%g, imageSpeed=%g\n", spriteName, inst->spriteIndex, (double) inst->imageIndex, (double) inst->imageSpeed); + printf(" Scale: (%g, %g), Angle: %g, Alpha: %g, Blend: 0x%06X\n", (double) inst->imageXscale, (double) inst->imageYscale, (double) inst->imageAngle, (double) inst->imageAlpha, inst->imageBlend); + printf(" Visible: %s, Active: %s, Solid: %s, Persistent: %s\n", inst->visible ? "true" : "false", inst->active ? "true" : "false", inst->solid ? "true" : "false", inst->persistent ? "true" : "false"); + printf(" Parent: %s (parentId=%d)\n", parentName, gameObject != nullptr ? gameObject->parentId : -1); + + // Active alarms + bool hasAlarm = false; + repeat(GML_ALARM_COUNT, alarmIdx) { + if (inst->alarm[alarmIdx] >= 0) { + if (!hasAlarm) { printf(" Alarms:"); hasAlarm = true; } + printf(" [%d]=%d", alarmIdx, inst->alarm[alarmIdx]); + } + } + if (hasAlarm) printf("\n"); + + // Self variables + bool hasSelfVars = false; + bool hasSelfArrays = false; + repeat(inst->selfVars.capacity, svIdx) { + IntRValueEntry* entry = &inst->selfVars.entries[svIdx]; + if (entry->key == INT_RVALUE_HASHMAP_EMPTY_KEY) continue; + int32_t varID = entry->key; + RValue val = entry->value; + if (val.type == RVALUE_UNDEFINED) continue; + + const char* varName = "?"; + repeat(dataWin->vari.variableCount, varIdx) { + Variable* var = &dataWin->vari.variables[varIdx]; + if (var->instanceType == INSTANCE_SELF && var->varID == varID) { + varName = var->name; + break; + } + } + + if (val.type == RVALUE_ARRAY && val.array != nullptr) { + if (!hasSelfArrays) { printf(" Self Arrays:\n"); hasSelfArrays = true; } + repeat(GMLArray_length1D(val.array), ai) { + RValue* cell = GMLArray_slot(val.array, ai); + if (cell == nullptr || cell->type == RVALUE_UNDEFINED) continue; + char* innerStr = RValue_toStringFancy(*cell); + printf(" %s[%d] = %s\n", varName, (int) ai, innerStr); + free(innerStr); + } + } else { + if (!hasSelfVars) { printf(" Self Variables:\n"); hasSelfVars = true; } + char* valStr = RValue_toStringFancy(val); + printf(" %s = %s\n", varName, valStr); + free(valStr); + } + } + } + + // Global variables (non-array) + printf("\n=== Global Variables ===\n"); + repeat(dataWin->vari.variableCount, varIdx) { + Variable* var = &dataWin->vari.variables[varIdx]; + if (var->instanceType != INSTANCE_GLOBAL || var->varID < 0) continue; + if ((uint32_t) var->varID >= vm->globalVarCount) continue; + RValue val = vm->globalVars[var->varID]; + if (val.type == RVALUE_UNDEFINED) continue; + + char* valStr = RValue_toStringFancy(val); + printf(" %s = %s\n", var->name, valStr); + free(valStr); + } + + // Global arrays: scan globalVars slots for RVALUE_ARRAY entries + repeat(dataWin->vari.variableCount, varIdx) { + Variable* var = &dataWin->vari.variables[varIdx]; + if (var->instanceType != INSTANCE_GLOBAL || var->varID < 0) continue; + if ((uint32_t) var->varID >= vm->globalVarCount) continue; + RValue val = vm->globalVars[var->varID]; + if (val.type != RVALUE_ARRAY || val.array == nullptr) continue; + repeat(GMLArray_length1D(val.array), ai) { + RValue* cell = GMLArray_slot(val.array, ai); + if (cell == nullptr || cell->type == RVALUE_UNDEFINED) continue; + char* innerStr = RValue_toStringFancy(*cell); + printf(" %s[%d] = %s\n", var->name, (int) ai, innerStr); + free(innerStr); + } + } + + printf("\n=== End Frame %d State Dump ===\n", runner->frameCount); +} + +// ===[ JSON State Dump ]=== + +static void writeRValueJson(JsonWriter* w, RValue val) { + switch (val.type) { + case RVALUE_REAL: + JsonWriter_double(w, val.real); + break; + case RVALUE_INT32: + JsonWriter_int(w, val.int32); + break; +#ifndef NO_RVALUE_INT64 + case RVALUE_INT64: + JsonWriter_int(w, val.int64); + break; +#endif + case RVALUE_STRING: + JsonWriter_string(w, val.string); + break; + case RVALUE_BOOL: + JsonWriter_bool(w, val.int32 != 0); + break; + case RVALUE_UNDEFINED: + JsonWriter_null(w); + break; + case RVALUE_ARRAY: { + // Render arrays as a JSON array. Skips RVALUE_UNDEFINED entries (they read as 0/null anyway). + JsonWriter_beginArray(w); + if (val.array != nullptr) { + repeat(GMLArray_length1D(val.array), ai) { + RValue* cell = GMLArray_slot(val.array, ai); + writeRValueJson(w, cell != nullptr ? *cell : (RValue){ .type = RVALUE_UNDEFINED }); + } + } + JsonWriter_endArray(w); + break; + } +#if IS_BC17_OR_HIGHER_ENABLED + case RVALUE_METHOD: { + char buf[64]; + snprintf(buf, sizeof(buf), "", val.method->codeIndex); + JsonWriter_string(w, buf); + break; + } +#endif + case RVALUE_STRUCT: { + char buf[64]; + snprintf(buf, sizeof(buf), "", val.structInst != nullptr ? val.structInst->instanceId : 0); + JsonWriter_string(w, buf); + break; + } + } +} + +RunnerTelemetry Runner_collectTelemetry(Runner* runner) { + RunnerTelemetry telemetry = {0}; + if (runner == NULL) return telemetry; + + telemetry.liveInstances = (uint32_t) arrlen(runner->instances); + telemetry.liveStructInstances = (uint32_t) arrlen(runner->structInstances); + + telemetry.dsMapSlots = (uint32_t) arrlen(runner->dsMapPool); + repeat((int32_t) arrlen(runner->dsMapPool), i) { + if (runner->dsMapPool[i] != NULL) telemetry.dsMapLive++; + } + + telemetry.dsListSlots = (uint32_t) arrlen(runner->dsListPool); + repeat((int32_t) arrlen(runner->dsListPool), i) { + DsList* list = &runner->dsListPool[i]; + if (!list->freed) telemetry.dsListLive++; + telemetry.dsListItems += (uint32_t) arrlen(list->items); + } + + return telemetry; +} + +char* Runner_dumpStateJson(Runner* runner) { + DataWin* dataWin = runner->dataWin; + VMContext* vm = runner->vmContext; + int32_t instanceCount = (int32_t) arrlen(runner->instances); + + JsonWriter w = JsonWriter_create(); + + JsonWriter_beginObject(&w); + + JsonWriter_propertyInt(&w, "frame", runner->frameCount); + + // Room info + JsonWriter_key(&w, "room"); + JsonWriter_beginObject(&w); + JsonWriter_propertyString(&w, "name", runner->currentRoom->name); + JsonWriter_propertyInt(&w, "index", runner->currentRoomIndex); + JsonWriter_endObject(&w); + + // Instances + JsonWriter_key(&w, "instances"); + JsonWriter_beginArray(&w); + + repeat(instanceCount, i) { + Instance* inst = runner->instances[i]; + if (!inst->active) continue; + + const char* objName = (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) ? dataWin->objt.objects[inst->objectIndex].name : nullptr; + + const char* spriteName = nullptr; + if (inst->spriteIndex >= 0 && dataWin->sprt.count > (uint32_t) inst->spriteIndex) { + spriteName = dataWin->sprt.sprites[inst->spriteIndex].name; + } + + JsonWriter_beginObject(&w); + + JsonWriter_propertyInt(&w, "instanceId", inst->instanceId); + JsonWriter_propertyString(&w, "objectName", objName); + JsonWriter_propertyInt(&w, "objectIndex", inst->objectIndex); + + // Parent object + const char* parentName = nullptr; + int32_t parentId = -1; + if (inst->objectIndex >= 0 && dataWin->objt.count > (uint32_t) inst->objectIndex) { + parentId = dataWin->objt.objects[inst->objectIndex].parentId; + if (parentId >= 0 && dataWin->objt.count > (uint32_t) parentId) { + parentName = dataWin->objt.objects[parentId].name; + } + } + JsonWriter_propertyString(&w, "parentObjectName", parentName); + JsonWriter_propertyInt(&w, "parentObjectIndex", parentId); + + JsonWriter_propertyDouble(&w, "x", inst->x); + JsonWriter_propertyDouble(&w, "y", inst->y); + JsonWriter_propertyInt(&w, "depth", inst->depth); + + // Sprite + JsonWriter_key(&w, "sprite"); + JsonWriter_beginObject(&w); + JsonWriter_propertyString(&w, "name", spriteName); + JsonWriter_propertyInt(&w, "index", inst->spriteIndex); + JsonWriter_propertyDouble(&w, "imageIndex", inst->imageIndex); + JsonWriter_propertyDouble(&w, "imageSpeed", inst->imageSpeed); + JsonWriter_endObject(&w); + + // Scale + JsonWriter_key(&w, "scale"); + JsonWriter_beginObject(&w); + JsonWriter_propertyDouble(&w, "x", inst->imageXscale); + JsonWriter_propertyDouble(&w, "y", inst->imageYscale); + JsonWriter_endObject(&w); + + JsonWriter_propertyDouble(&w, "angle", inst->imageAngle); + JsonWriter_propertyDouble(&w, "alpha", inst->imageAlpha); + JsonWriter_propertyInt(&w, "blend", inst->imageBlend); + JsonWriter_propertyBool(&w, "visible", inst->visible); + JsonWriter_propertyBool(&w, "active", inst->active); + JsonWriter_propertyBool(&w, "solid", inst->solid); + JsonWriter_propertyBool(&w, "persistent", inst->persistent); + + // Alarms + JsonWriter_key(&w, "alarms"); + JsonWriter_beginObject(&w); + repeat(GML_ALARM_COUNT, alarmIdx) { + if (inst->alarm[alarmIdx] >= 0) { + char alarmKey[4]; + snprintf(alarmKey, sizeof(alarmKey), "%d", alarmIdx); + JsonWriter_propertyInt(&w, alarmKey, inst->alarm[alarmIdx]); + } + } + JsonWriter_endObject(&w); + + // Self variables (non-array, sparse hashmap) + JsonWriter_key(&w, "selfVariables"); + JsonWriter_beginObject(&w); + repeat(inst->selfVars.capacity, svIdx) { + IntRValueEntry* entry = &inst->selfVars.entries[svIdx]; + if (entry->key == INT_RVALUE_HASHMAP_EMPTY_KEY) continue; + int32_t varID = entry->key; + RValue val = entry->value; + if (val.type == RVALUE_UNDEFINED) continue; + + // Resolve variable name from VARI chunk + const char* varName = "?"; + repeat(dataWin->vari.variableCount, varIdx) { + Variable* var = &dataWin->vari.variables[varIdx]; + if (var->instanceType == INSTANCE_SELF && var->varID == varID) { + varName = var->name; + break; + } + } + + JsonWriter_key(&w, varName); + writeRValueJson(&w, val); + } + JsonWriter_endObject(&w); + JsonWriter_endObject(&w); + } + + JsonWriter_endArray(&w); + + // Tiles + Room* dumpRoom = runner->currentRoom; + JsonWriter_key(&w, "tiles"); + JsonWriter_beginArray(&w); + repeat(dumpRoom->tileCount, tileIdx) { + RoomTile* tile = &dumpRoom->tiles[tileIdx]; + const char* bgName = (tile->backgroundDefinition >= 0 && dataWin->bgnd.count > (uint32_t) tile->backgroundDefinition) ? dataWin->bgnd.backgrounds[tile->backgroundDefinition].name : nullptr; + + JsonWriter_beginObject(&w); + JsonWriter_propertyInt(&w, "index", tileIdx); + JsonWriter_propertyInt(&w, "x", tile->x); + JsonWriter_propertyInt(&w, "y", tile->y); + JsonWriter_propertyInt(&w, "backgroundIndex", tile->backgroundDefinition); + if (bgName != nullptr) { + JsonWriter_propertyString(&w, "backgroundName", bgName); + } else { + JsonWriter_propertyNull(&w, "backgroundName"); + } + JsonWriter_propertyInt(&w, "sourceX", tile->sourceX); + JsonWriter_propertyInt(&w, "sourceY", tile->sourceY); + JsonWriter_propertyInt(&w, "width", tile->width); + JsonWriter_propertyInt(&w, "height", tile->height); + JsonWriter_propertyInt(&w, "depth", tile->tileDepth); + JsonWriter_propertyInt(&w, "instanceID", tile->instanceID); + JsonWriter_propertyDouble(&w, "scaleX", tile->scaleX); + JsonWriter_propertyDouble(&w, "scaleY", tile->scaleY); + JsonWriter_propertyInt(&w, "color", tile->color); + + ptrdiff_t layerIdx = hmgeti(runner->tileLayerMap, tile->tileDepth); + bool visible = (layerIdx >= 0) ? runner->tileLayerMap[layerIdx].value.visible : true; + JsonWriter_propertyBool(&w, "visible", visible); + JsonWriter_endObject(&w); + } + JsonWriter_endArray(&w); + + // Global variables (non-array) + JsonWriter_key(&w, "globalVariables"); + JsonWriter_beginObject(&w); + repeat(dataWin->vari.variableCount, varIdx) { + Variable* var = &dataWin->vari.variables[varIdx]; + if (var->instanceType != INSTANCE_GLOBAL || var->varID < 0) continue; + if ((uint32_t) var->varID >= vm->globalVarCount) continue; + RValue val = vm->globalVars[var->varID]; + if (val.type == RVALUE_UNDEFINED) continue; + + JsonWriter_key(&w, var->name); + writeRValueJson(&w, val); + } + JsonWriter_endObject(&w); + JsonWriter_endObject(&w); + + char* result = JsonWriter_copyOutput(&w); + JsonWriter_free(&w); + return result; +} + +void Runner_free(Runner* runner) { + if (runner == nullptr) return; + + cleanupState(runner); + + if (runner->instancesByObject != nullptr) { + uint32_t objectCount = runner->dataWin->objt.count; + repeat(objectCount, i) { + arrfree(runner->instancesByObject[i]); + } + free(runner->instancesByObject); + runner->instancesByObject = nullptr; + } + if (runner->instancesByExactObject != nullptr) { + uint32_t objectCount = runner->dataWin->objt.count; + repeat(objectCount, i) { + arrfree(runner->instancesByExactObject[i]); + } + free(runner->instancesByExactObject); + runner->instancesByExactObject = nullptr; + } + if (runner->objectsWithAnyEventOfType != nullptr) { + repeat(OBJT_EVENT_TYPE_COUNT, t) { + arrfree(runner->objectsWithAnyEventOfType[t]); + } + free(runner->objectsWithAnyEventOfType); + runner->objectsWithAnyEventOfType = nullptr; + } + arrfree(runner->cachedDrawables); + runner->cachedDrawables = nullptr; + arrfree(runner->instanceSnapshots); + runner->instanceSnapshots = nullptr; + arrfree(runner->eventDispatchInstances); + runner->eventDispatchInstances = nullptr; + arrfree(runner->n3dsBattleFieldInstances); + runner->n3dsBattleFieldInstances = nullptr; + arrfree(runner->n3dsBattleUIInstances); + runner->n3dsBattleUIInstances = nullptr; + arrfree(runner->n3dsTopScreenGUIInstances); + runner->n3dsTopScreenGUIInstances = nullptr; + repeat(3, i) { + arrfree(runner->n3dsTopScreenGUIResponderEvents[i]); + runner->n3dsTopScreenGUIResponderEvents[i] = nullptr; + runner->n3dsTopScreenGUIResponderListsValid[i] = false; + } + free(runner->frameStepObjectMsByObject); + runner->frameStepObjectMsByObject = nullptr; + free(runner->frameStepObjectCallsByObject); + runner->frameStepObjectCallsByObject = nullptr; + ResolvedEventTable_free(&runner->eventTable); + EventSlotMap_destroy(&runner->eventSlotMap); + shfree(runner->assetsByName); + + RunnerKeyboard_free(runner->keyboard); + RunnerGamepad_free(runner->gamepads); + Instance_free(runner->globalScopeInstance); + free(runner); +} diff --git a/src/runner.h b/src/runner.h index 686c931b..e4edb3f8 100644 --- a/src/runner.h +++ b/src/runner.h @@ -1,456 +1,555 @@ -#pragma once - -#include "common.h" -#include "audio_system.h" -#include "data_win.h" -#include "event_table.h" -#include "file_system.h" -#include "ini.h" -#include "instance.h" -#include "renderer.h" -#include "runner_keyboard.h" -#include "spatial_grid.h" -#include "runner_gamepad.h" -#include "vm.h" - -// ===[ Event Type Constants ]=== -#define EVENT_CREATE 0 -#define EVENT_DESTROY 1 -#define EVENT_ALARM 2 -#define EVENT_STEP 3 -#define EVENT_COLLISION 4 -#define EVENT_KEYBOARD 5 -#define EVENT_OTHER 7 -#define EVENT_DRAW 8 -#define EVENT_KEYPRESS 9 -#define EVENT_KEYRELEASE 10 -#define EVENT_PRECREATE 14 - -// ===[ Step Sub-event Constants ]=== -#define STEP_NORMAL 0 -#define STEP_BEGIN 1 -#define STEP_END 2 - -// ===[ Draw Sub-event Constants ]=== -#define DRAW_NORMAL 0 -#define DRAW_GUI 64 -#define DRAW_BEGIN 72 -#define DRAW_END 73 -#define DRAW_GUI_BEGIN 74 -#define DRAW_GUI_END 75 -#define DRAW_PRE 76 -#define DRAW_POST 77 - -// ===[ Other Sub-event Constants ]=== -#define OTHER_OUTSIDE_ROOM 0 -#define OTHER_GAME_START 2 -#define OTHER_ROOM_START 4 -#define OTHER_ROOM_END 5 +#pragma once + +#include "common.h" +#include "audio_system.h" +#include "data_win.h" +#include "event_table.h" +#include "file_system.h" +#include "ini.h" +#include "instance.h" +#include "renderer.h" +#include "runner_keyboard.h" +#include "spatial_grid.h" +#include "runner_gamepad.h" +#include "vm.h" + +// Platform-specific boot log function +void Runner_platformBootLog(const char* message); + +// ===[ Event Type Constants ]=== +#define EVENT_CREATE 0 +#define EVENT_DESTROY 1 +#define EVENT_ALARM 2 +#define EVENT_STEP 3 +#define EVENT_COLLISION 4 +#define EVENT_KEYBOARD 5 +#define EVENT_OTHER 7 +#define EVENT_DRAW 8 +#define EVENT_KEYPRESS 9 +#define EVENT_KEYRELEASE 10 +#define EVENT_PRECREATE 14 + +// ===[ Step Sub-event Constants ]=== +#define STEP_NORMAL 0 +#define STEP_BEGIN 1 +#define STEP_END 2 + +// ===[ Draw Sub-event Constants ]=== +#define DRAW_NORMAL 0 +#define DRAW_GUI 64 +#define DRAW_BEGIN 72 +#define DRAW_END 73 +#define DRAW_GUI_BEGIN 74 +#define DRAW_GUI_END 75 +#define DRAW_PRE 76 +#define DRAW_POST 77 + +// ===[ Other Sub-event Constants ]=== +#define OTHER_OUTSIDE_ROOM 0 +#define OTHER_GAME_START 2 +#define OTHER_ROOM_START 4 +#define OTHER_ROOM_END 5 #define OTHER_ANIMATION_END 7 #define OTHER_END_OF_PATH 8 #define OTHER_USER0 10 +#define OTHER_ASYNC_SOCIAL 70 #define OTHER_ASYNC_SYSTEM 75 - -#define MAX_VIEWS 8 - -// ===[ Operating System Types ]=== -// See GameMaker-HTML5's Globals.js -typedef enum { - OS_UNKNOWN = -1, - OS_WINDOWS, - OS_MACOSX, - OS_PSP, - OS_IOS, - OS_ANDROID, - OS_SYMBIAN, - OS_LINUX, - OS_WINPHONE, - OS_TIZEN, - OS_WIN8NATIVE, - OS_WIIU, - OS_3DS, - OS_PSVITA, - OS_BB10, - OS_PS4, - OS_XBOXONE, - OS_PS3, - OS_XBOX360, - OS_UWP, - OS_AMAZON, - OS_SWITCH, - - OS_LLVM_WIN32 = 65536, - OS_LLVM_MACOSX, - OS_LLVM_PSP, - OS_LLVM_IOS, - OS_LLVM_ANDROID, - OS_LLVM_SYMBIAN, - OS_LLVM_LINUX, - OS_LLVM_WINPHONE -} YoYoOperatingSystem; - -typedef struct { - bool enabled; - int32_t viewX; - int32_t viewY; - int32_t viewWidth; - int32_t viewHeight; - int32_t portX; - int32_t portY; - int32_t portWidth; - int32_t portHeight; - uint32_t borderX; - uint32_t borderY; - int32_t speedX; - int32_t speedY; - int32_t objectId; - float viewAngle; -} RuntimeView; - -typedef struct { - bool visible; - bool foreground; - int32_t backgroundIndex; // BGND resource index (mutable at runtime) - float x, y; // float for sub-pixel scrolling accumulation - bool tileX, tileY; - float speedX, speedY; - bool stretch; - float alpha; -} RuntimeBackground; - + +#define MAX_VIEWS 8 + +// ===[ Operating System Types ]=== +// See GameMaker-HTML5's Globals.js +typedef enum { + OS_UNKNOWN = -1, + OS_WINDOWS, + OS_MACOSX, + OS_PSP, + OS_IOS, + OS_ANDROID, + OS_SYMBIAN, + OS_LINUX, + OS_WINPHONE, + OS_TIZEN, + OS_WIN8NATIVE, + OS_WIIU, + OS_3DS, + OS_PSVITA, + OS_BB10, + OS_PS4, + OS_XBOXONE, + OS_PS3, + OS_XBOX360, + OS_UWP, + OS_AMAZON, + OS_SWITCH, + + OS_LLVM_WIN32 = 65536, + OS_LLVM_MACOSX, + OS_LLVM_PSP, + OS_LLVM_IOS, + OS_LLVM_ANDROID, + OS_LLVM_SYMBIAN, + OS_LLVM_LINUX, + OS_LLVM_WINPHONE +} YoYoOperatingSystem; + +typedef struct { + bool enabled; + int32_t viewX; + int32_t viewY; + int32_t viewWidth; + int32_t viewHeight; + int32_t portX; + int32_t portY; + int32_t portWidth; + int32_t portHeight; + uint32_t borderX; + uint32_t borderY; + int32_t speedX; + int32_t speedY; + int32_t objectId; + float viewAngle; +} RuntimeView; + +typedef struct { + bool visible; + bool foreground; + int32_t backgroundIndex; // BGND resource index (mutable at runtime) + float x, y; // float for sub-pixel scrolling accumulation + bool tileX, tileY; + float speedX, speedY; + bool stretch; + float alpha; +} RuntimeBackground; + typedef struct { bool visible; float offsetX; float offsetY; -} TileLayerState; - -// Mutable background element on a dynamically-created layer (layer_background_create). -// For parsed room layers, RoomLayerBackgroundData is used directly and this struct is unused. -typedef struct { - int32_t spriteIndex; // SPRT index (-1 = none) - bool visible; - bool htiled; - bool vtiled; - bool stretch; - float xScale; - float yScale; - uint32_t blend; // BGR float alpha; - float xOffset; // element-local offset (in addition to layer offset) - float yOffset; -} RuntimeBackgroundElement; - -// Mutable sprite element on an Assets layer. Populated from RoomLayerAssetsData.sprites at room init, can be removed at runtime via layer_sprite_destroy (used by language variant selection). -typedef struct { - int32_t spriteIndex; // SPRT index (-1 = none/destroyed) - int32_t x; - int32_t y; - float scaleX; - float scaleY; - uint32_t color; // BGR + alpha - float animationSpeed; - uint32_t animationSpeedType; - float frameIndex; - float rotation; -} RuntimeSpriteElement; - -// Values match GML layerelementtype_* enum so layer_get_element_type can return them as-is. -typedef enum { - RuntimeLayerElementType_Background = 1, - RuntimeLayerElementType_Sprite = 4, -} RuntimeLayerElementType; - -typedef struct { - uint32_t id; - RuntimeLayerElementType type; - RuntimeBackgroundElement* backgroundElement; // owned; nullptr if type != Background - RuntimeSpriteElement* spriteElement; // owned; nullptr if type != Sprite -} RuntimeLayerElement; - -// Runtime-mutable state for a GMS2 room layer. Parsed layers are populated at room load from RoomLayer and share IDs with the parsed data. -// Dynamic layers are created via layer_create and carry their own name + element list; they don't correspond to any RoomLayer. -typedef struct { - uint32_t id; - int32_t depth; - bool visible; - float xOffset; - float yOffset; - float hSpeed; - float vSpeed; - bool dynamic; // true = created at runtime via layer_create - char* dynamicName; // owned; only populated for dynamic layers - RuntimeLayerElement* elements; // stb_ds array; only populated for dynamic layers -} RuntimeLayer; - -// stb_ds hashmap entry: depth -> tile layer state -typedef struct { - int32_t key; - TileLayerState value; -} TileLayerMapEntry; - -// A single entry in the depth-sorted draw list. Cached on Runner and rebuilt lazily based on Runner.drawableListStructureDirty / drawableListSortDirty. -// Filtering on instance->active/visible and runtimeLayer->visible happens at draw time so toggling those does not require invalidating the cache. -typedef enum { DRAWABLE_TILE, DRAWABLE_INSTANCE, DRAWABLE_LAYER } DrawableType; - -typedef struct { - DrawableType type; - int32_t depth; - union { - Instance* instance; - int32_t tileIndex; - RuntimeLayer* runtimeLayer; - }; -} Drawable; - -// stb_ds hashmap entry for ds_map: string key -> RValue -typedef struct { - char* key; - RValue value; -} DsMapEntry; - -// ds_list: dynamic array of RValues -typedef struct { - RValue* items; // stb_ds dynamic array of RValues - bool freed; // true when the slot is destroyed and available for reuse by ds_list_create (matches native GMS) -} DsList; - -// ===[ GML Buffer System ]=== - -// Buffer type constants (matching GML) -#define GML_BUFFER_FIXED 0 -#define GML_BUFFER_GROW 1 -#define GML_BUFFER_WRAP 2 -#define GML_BUFFER_FAST 3 - -// Buffer data type constants (matching GML) -#define GML_BUFTYPE_U8 1 -#define GML_BUFTYPE_S8 2 -#define GML_BUFTYPE_U16 3 -#define GML_BUFTYPE_S16 4 -#define GML_BUFTYPE_U32 5 -#define GML_BUFTYPE_S32 6 -#define GML_BUFTYPE_F16 7 -#define GML_BUFTYPE_F32 8 -#define GML_BUFTYPE_F64 9 -#define GML_BUFTYPE_BOOL 10 -#define GML_BUFTYPE_STRING 11 -#define GML_BUFTYPE_U64 12 -#define GML_BUFTYPE_TEXT 13 - -// Buffer seek mode constants (matching GML) -#define GML_BUFFER_SEEK_START 0 -#define GML_BUFFER_SEEK_RELATIVE 1 -#define GML_BUFFER_SEEK_END 2 +} TileLayerState; typedef struct { - uint8_t* data; // raw byte storage - int32_t size; // allocated size in bytes - int32_t position; // current read/write cursor - int32_t usedSize; // high-water mark for grow buffers - int32_t alignment; // byte alignment for read/write operations - int32_t type; // GML_BUFFER_FIXED, _GROW, _WRAP, _FAST - bool isValid; // false after buffer_delete (tombstone) -} GmlBuffer; + uint32_t start; + uint32_t count; +} TileLayerCacheRow; -// Motion planning grid used by mp_grid_* builtins. Cell value 1 = blocked. typedef struct { - bool inUse; - GMLReal left; - GMLReal top; - int32_t hcells; - int32_t vcells; - GMLReal cellWidth; - GMLReal cellHeight; - uint8_t* cells; -} MpGrid; + uint32_t tileX; + uint32_t srcX; + uint32_t srcY; + int32_t tpagIndex; + int32_t n3dsTileEntryIndex; + bool mirror; + bool flip; +} TileLayerCacheCell; -// Open text file handle for GML file_text_* functions -#define MAX_OPEN_TEXT_FILES 32 typedef struct { - char* content; // full file content (for read mode) - char* writeBuffer; // accumulated text (for write mode) - char* filePath; // relative path (for write mode, to flush on close) - int32_t readPos; // current byte position in content (read mode) - int32_t contentLen; // length of content string - bool isWriteMode; - bool isOpen; -} OpenTextFile; - -// Saved state for persistent rooms. When leaving a persistent room, instance state -// and visual properties are saved here. When returning, they are restored instead -// of re-creating from the room definition. + bool built; + int32_t backgroundIndex; + int32_t n3dsChunkCacheId; + uint32_t tileWidth; + uint32_t tileHeight; + uint32_t tilesX; + uint32_t tilesY; + TileLayerCacheRow* rows; + TileLayerCacheCell* cells; +} TileLayerRenderCache; + +// Mutable background element on a dynamically-created layer (layer_background_create). +// For parsed room layers, RoomLayerBackgroundData is used directly and this struct is unused. +typedef struct { + int32_t spriteIndex; // SPRT index (-1 = none) + bool visible; + bool htiled; + bool vtiled; + bool stretch; + float xScale; + float yScale; + uint32_t blend; // BGR + float alpha; + float xOffset; // element-local offset (in addition to layer offset) + float yOffset; +} RuntimeBackgroundElement; + +// Mutable sprite element on an Assets layer. Populated from RoomLayerAssetsData.sprites at room init, can be removed at runtime via layer_sprite_destroy (used by language variant selection). +typedef struct { + int32_t spriteIndex; // SPRT index (-1 = none/destroyed) + int32_t x; + int32_t y; + float scaleX; + float scaleY; + uint32_t color; // BGR + alpha + float animationSpeed; + uint32_t animationSpeedType; + float frameIndex; + float rotation; +} RuntimeSpriteElement; + +// Values match GML layerelementtype_* enum so layer_get_element_type can return them as-is. +typedef enum { + RuntimeLayerElementType_Background = 1, + RuntimeLayerElementType_Sprite = 4, +} RuntimeLayerElementType; + +typedef struct { + uint32_t id; + RuntimeLayerElementType type; + RuntimeBackgroundElement* backgroundElement; // owned; nullptr if type != Background + RuntimeSpriteElement* spriteElement; // owned; nullptr if type != Sprite +} RuntimeLayerElement; + +// Runtime-mutable state for a GMS2 room layer. Parsed layers are populated at room load from RoomLayer and share IDs with the parsed data. +// Dynamic layers are created via layer_create and carry their own name + element list; they don't correspond to any RoomLayer. +typedef struct { + uint32_t id; + int32_t depth; + bool visible; + float xOffset; + float yOffset; + float hSpeed; + float vSpeed; + bool dynamic; // true = created at runtime via layer_create + char* dynamicName; // owned; only populated for dynamic layers + RuntimeLayerElement* elements; // stb_ds array; only populated for dynamic layers +} RuntimeLayer; + +// stb_ds hashmap entry: depth -> tile layer state +typedef struct { + int32_t key; + TileLayerState value; +} TileLayerMapEntry; + +// A single entry in the depth-sorted draw list. Cached on Runner and rebuilt lazily based on Runner.drawableListStructureDirty / drawableListSortDirty. +// Filtering on instance->active/visible and runtimeLayer->visible happens at draw time so toggling those does not require invalidating the cache. +typedef enum { DRAWABLE_TILE, DRAWABLE_INSTANCE, DRAWABLE_LAYER } DrawableType; + +typedef struct { + DrawableType type; + int32_t depth; + union { + Instance* instance; + int32_t tileIndex; + RuntimeLayer* runtimeLayer; + }; +} Drawable; + +// stb_ds hashmap entry for ds_map: string key -> RValue +typedef struct { + char* key; + RValue value; +} DsMapEntry; + +// ds_list: dynamic array of RValues +typedef struct { + RValue* items; // stb_ds dynamic array of RValues + bool freed; // true when the slot is destroyed and available for reuse by ds_list_create (matches native GMS) +} DsList; + +// ===[ GML Buffer System ]=== + +// Buffer type constants (matching GML) +#define GML_BUFFER_FIXED 0 +#define GML_BUFFER_GROW 1 +#define GML_BUFFER_WRAP 2 +#define GML_BUFFER_FAST 3 + +// Buffer data type constants (matching GML) +#define GML_BUFTYPE_U8 1 +#define GML_BUFTYPE_S8 2 +#define GML_BUFTYPE_U16 3 +#define GML_BUFTYPE_S16 4 +#define GML_BUFTYPE_U32 5 +#define GML_BUFTYPE_S32 6 +#define GML_BUFTYPE_F16 7 +#define GML_BUFTYPE_F32 8 +#define GML_BUFTYPE_F64 9 +#define GML_BUFTYPE_BOOL 10 +#define GML_BUFTYPE_STRING 11 +#define GML_BUFTYPE_U64 12 +#define GML_BUFTYPE_TEXT 13 + +// Buffer seek mode constants (matching GML) +#define GML_BUFFER_SEEK_START 0 +#define GML_BUFFER_SEEK_RELATIVE 1 +#define GML_BUFFER_SEEK_END 2 + +typedef struct { + uint8_t* data; // raw byte storage + int32_t size; // allocated size in bytes + int32_t position; // current read/write cursor + int32_t usedSize; // high-water mark for grow buffers + int32_t alignment; // byte alignment for read/write operations + int32_t type; // GML_BUFFER_FIXED, _GROW, _WRAP, _FAST + bool isValid; // false after buffer_delete (tombstone) +} GmlBuffer; + +// Motion planning grid used by mp_grid_* builtins. Cell value 1 = blocked. +typedef struct { + bool inUse; + GMLReal left; + GMLReal top; + int32_t hcells; + int32_t vcells; + GMLReal cellWidth; + GMLReal cellHeight; + uint8_t* cells; +} MpGrid; + +// Open text file handle for GML file_text_* functions +#define MAX_OPEN_TEXT_FILES 32 +typedef struct { + char* content; // full file content (for read mode) + char* writeBuffer; // accumulated text (for write mode) + char* filePath; // relative path (for write mode, to flush on close) + int32_t readPos; // current byte position in content (read mode) + int32_t contentLen; // length of content string + bool isWriteMode; + bool isOpen; +} OpenTextFile; + +// Saved state for persistent rooms. When leaving a persistent room, instance state +// and visual properties are saved here. When returning, they are restored instead +// of re-creating from the room definition. typedef struct { bool initialized; Instance** instances; // stb_ds array of saved Instance* - RuntimeBackground backgrounds[8]; - uint32_t backgroundColor; - bool drawBackgroundColor; - TileLayerMapEntry* tileLayerMap; // stb_ds hashmap: depth -> tile layer state - RuntimeLayer* runtimeLayers; // stb_ds array, index-parallel to currentRoom->layers + RuntimeBackground backgrounds[8]; + uint32_t backgroundColor; + bool drawBackgroundColor; + TileLayerMapEntry* tileLayerMap; // stb_ds hashmap: depth -> tile layer state + RuntimeLayer* runtimeLayers; // stb_ds array, index-parallel to currentRoom->layers RuntimeView views[MAX_VIEWS]; } SavedRoomState; +typedef struct { + Instance* instance; + int32_t codeId; + int32_t ownerObjectIndex; +} N3DSResolvedDrawEvent; + typedef struct Runner { - DataWin* dataWin; - VMContext* vmContext; - Renderer* renderer; - FileSystem* fileSystem; - AudioSystem* audioSystem; - Room* currentRoom; - int32_t currentRoomIndex; - int32_t currentRoomOrderPosition; - Instance** instances; // stb_ds array of Instance* - // Per-object instance lists: for each object index, a stb_ds array of Instance*. - // An instance appears in its own object's list AND in every ancestor object's list (descendant-inclusive). - // This lets collision dispatch iterate only the instances of a target object (and its descendants) instead of scanning all instances in the room. - // Must be kept in sync with any instance creation, change, or deletion. - Instance*** instancesByObject; - // Same as instancesByObject but each instance only appears in the bucket of its EXACT objectIndex (no ancestors). Used by event dispatch so we don't double-fire when both a child and its parent declare the same event. - Instance*** instancesByExactObject; - // Precomputed (eventType, eventSubtype) -> dense slot remap. Built once at Runner_create, never mutated. - EventSlotMap eventSlotMap; - // Precomputed per-object and per-slot CSR tables of resolved event handlers. Replaces the per-dispatch parent-chain walk in findEventCodeIdAndOwner. - ResolvedEventTable eventTable; - // Precomputed assets map. - struct { char* key; int32_t value; }* assetsByName; - // For each event type, the deduplicated list of object indices that respond to ANY subtype of that event (including via inheritance). Derived from the event table; used by collision dispatch to skip non-collision objects in the outer loop. - // Length = OBJT_EVENT_TYPE_COUNT. - int32_t** objectsWithAnyEventOfType; - // Reusable scratch array for Runner_executeEventForAll. Pre-grown to avoid stb_ds arrput overhead and repeated allocations on the per-frame dispatch path. Owned via stb_ds; truncated at the start of each call. - Instance** eventDispatchInstances; - // LIFO arena used to snapshot per-object instance lists before iteration. - // Any loop that might fire user code iterates a copy so that in-flight mutations (instance_change swap-remove, spawns, destroys) don't corrupt it. - // Each call pushes its snapshot (append) and pops on normal loop exit; nesting is safe because pushes/pops are LIFO and outer ranges stay untouched under newer pushes. - Instance** instanceSnapshots; - SpatialGrid* spatialGrid; - uint32_t collisionQueryCounter; - int32_t pendingRoom; // -1 = none - bool gameStartFired; - int frameCount; - uint32_t nextInstanceId; - RunnerKeyboardState* keyboard; - RuntimeView views[MAX_VIEWS]; - RunnerGamepadState* gamepads; - RuntimeBackground backgrounds[8]; - uint32_t backgroundColor; // runtime-mutable (BGR format) - bool drawBackgroundColor; - bool shouldExit; - bool debugMode; - void* nativeWindow; - void (*setWindowTitle)(void* window, const char* title); - bool (*windowHasFocus)(void* window); + DataWin* dataWin; + VMContext* vmContext; + Renderer* renderer; + FileSystem* fileSystem; + AudioSystem* audioSystem; + Room* currentRoom; + int32_t currentRoomIndex; + int32_t currentRoomOrderPosition; + Instance** instances; // stb_ds array of Instance* + // Per-object instance lists: for each object index, a stb_ds array of Instance*. + // An instance appears in its own object's list AND in every ancestor object's list (descendant-inclusive). + // This lets collision dispatch iterate only the instances of a target object (and its descendants) instead of scanning all instances in the room. + // Must be kept in sync with any instance creation, change, or deletion. + Instance*** instancesByObject; + // Same as instancesByObject but each instance only appears in the bucket of its EXACT objectIndex (no ancestors). Used by event dispatch so we don't double-fire when both a child and its parent declare the same event. + Instance*** instancesByExactObject; + // Precomputed (eventType, eventSubtype) -> dense slot remap. Built once at Runner_create, never mutated. + EventSlotMap eventSlotMap; + // Precomputed per-object and per-slot CSR tables of resolved event handlers. Replaces the per-dispatch parent-chain walk in findEventCodeIdAndOwner. + ResolvedEventTable eventTable; + // Precomputed assets map. + struct { char* key; int32_t value; }* assetsByName; + // For each event type, the deduplicated list of object indices that respond to ANY subtype of that event (including via inheritance). Derived from the event table; used by collision dispatch to skip non-collision objects in the outer loop. + // Length = OBJT_EVENT_TYPE_COUNT. + int32_t** objectsWithAnyEventOfType; + // Reusable scratch array for Runner_executeEventForAll. Pre-grown to avoid stb_ds arrput overhead and repeated allocations on the per-frame dispatch path. Owned via stb_ds; truncated at the start of each call. + Instance** eventDispatchInstances; + // LIFO arena used to snapshot per-object instance lists before iteration. + // Any loop that might fire user code iterates a copy so that in-flight mutations (instance_change swap-remove, spawns, destroys) don't corrupt it. + // Each call pushes its snapshot (append) and pops on normal loop exit; nesting is safe because pushes/pops are LIFO and outer ranges stay untouched under newer pushes. + Instance** instanceSnapshots; + SpatialGrid* spatialGrid; + uint32_t collisionQueryCounter; + int32_t pendingRoom; // -1 = none + bool gameStartFired; + int frameCount; + uint32_t nextInstanceId; + RunnerKeyboardState* keyboard; + RuntimeView views[MAX_VIEWS]; + RunnerGamepadState* gamepads; + RuntimeBackground backgrounds[8]; + uint32_t backgroundColor; // runtime-mutable (BGR format) + bool drawBackgroundColor; + bool shouldExit; + bool debugMode; + void* nativeWindow; + void (*setWindowTitle)(void* window, const char* title); + bool (*windowHasFocus)(void* window); TileLayerMapEntry* tileLayerMap; // stb_ds hashmap: depth -> tile layer state RuntimeLayer* runtimeLayers; // stb_ds array, index-parallel to currentRoom->layers for parsed entries; dynamic entries appended + TileLayerRenderCache* tileLayerCaches; // array parallel to currentRoom->layers for parsed tile layers + uint32_t tileLayerCacheCount; uint32_t nextLayerId; // counter for IDs of layers/elements created at runtime SavedRoomState* savedRoomStates; // array of size dataWin->room.count, for persistent room support int32_t viewCurrent; // index of the view currently being drawn (for view_current) + bool drawViewBoundsValid; + int32_t drawViewX; + int32_t drawViewY; + int32_t drawViewWidth; + int32_t drawViewHeight; struct { char* key; int value; }* disabledObjects; // stb_ds string hashmap, nullptr = no filtering - struct { int key; Instance* value; }* instancesById; - bool forceDrawDepth; - // Depth-sorted unified list of all drawables (instances + tiles + runtime layers) for the current room. - // Active/visible filtering happens at draw time, so toggling those flags does not invalidate the cache. - // - // Two-tier invalidation: - // structureDirty - the SET of entries changed (instance/layer create or destroy, room change). Full rebuild. - // sortDirty - the entries are the same but .depth values may have shifted. Refresh depths and only re-sort if order broke. Cheap when small depth shifts don't cross neighbors (typical depth=-y games). - Drawable* cachedDrawables; // stb_ds array - bool drawableListStructureDirty; - bool drawableListSortDirty; - // Dummy instance to serve as "self" during GLOB script execution - // In bytecode version 17+, global init scripts store method values on "self" via Pop.v.v - // The real runner uses a persistent YYObjectBase for this, the YYObjectBase is a "parent" of Instance - // For now, we'll use a dummy Instance with objectIndex = -1 as a hack - Instance* globalScopeInstance; - // Struct instances created by @@NewGMLObject@@. Reuses Instance with objectIndex=-1. - // Tracked separately so event/step/draw iteration over runner->instances stays clean. - Instance** structInstances; - int32_t forcedDepth; - - // ===[ Builtin function state ]=== - DsMapEntry** dsMapPool; // stb_ds array of stb_ds hashmaps - DsList* dsListPool; // stb_ds array of DsList - GmlBuffer* gmlBufferPool; // stb_ds array of GmlBuffer - MpGrid* mpGridPool; // stb_ds array of motion-planning grids - - // Motion planning potential field settings - GMLReal mpPotMaxrot; - GMLReal mpPotStep; - GMLReal mpPotAhead; - bool mpPotOnSpot; - + struct { int key; Instance* value; }* instancesById; + bool forceDrawDepth; + // Depth-sorted unified list of all drawables (instances + tiles + runtime layers) for the current room. + // Active/visible filtering happens at draw time, so toggling those flags does not invalidate the cache. + // + // Two-tier invalidation: + // structureDirty - the SET of entries changed (instance/layer create or destroy, room change). Full rebuild. + // sortDirty - the entries are the same but .depth values may have shifted. Refresh depths and only re-sort if order broke. Cheap when small depth shifts don't cross neighbors (typical depth=-y games). + Drawable* cachedDrawables; // stb_ds array + bool drawableListStructureDirty; + bool drawableListSortDirty; + // Dummy instance to serve as "self" during GLOB script execution + // In bytecode version 17+, global init scripts store method values on "self" via Pop.v.v + // The real runner uses a persistent YYObjectBase for this, the YYObjectBase is a "parent" of Instance + // For now, we'll use a dummy Instance with objectIndex = -1 as a hack + Instance* globalScopeInstance; + // Struct instances created by @@NewGMLObject@@. Reuses Instance with objectIndex=-1. + // Tracked separately so event/step/draw iteration over runner->instances stays clean. + Instance** structInstances; + int32_t forcedDepth; + + // ===[ Builtin function state ]=== + DsMapEntry** dsMapPool; // stb_ds array of stb_ds hashmaps + DsList* dsListPool; // stb_ds array of DsList + GmlBuffer* gmlBufferPool; // stb_ds array of GmlBuffer + MpGrid* mpGridPool; // stb_ds array of motion-planning grids + + // Motion planning potential field settings + GMLReal mpPotMaxrot; + GMLReal mpPotStep; + GMLReal mpPotAhead; + bool mpPotOnSpot; + // Legacy audio_play_music / audio_stop_music tracking int32_t lastMusicInstance; - - // INI file state - IniFile* currentIni; - char* currentIniPath; - bool currentIniDirty; - // Some games (like Undertale) open and close the same INI file EVERY SINGLE FRAME! - // While on modern devices this isn't a huge deal, this WILL cause issues on devices that have less than stellar file systems (like the PlayStation 2) - // To avoid unnecessary disk reads, we cache the last-closed INI and reuse it on reopen - IniFile* cachedIni; // Cache of last-closed INI (for fast reopen) - char* cachedIniPath; - - // Text file handles for file_text_* functions - OpenTextFile openTextFiles[MAX_OPEN_TEXT_FILES]; - - // Async map ID - int32_t asyncLoadMapId; - - // Used by the "os_type" built-in - YoYoOperatingSystem osType; - - // GUI layer size (display_set_gui_size). 0 = auto-match the current view's port size. + int32_t* musicInstanceStack; // stb_ds array used to restore the previous legacy music track after nested play/stop flows + + // INI file state + IniFile* currentIni; + char* currentIniPath; + bool currentIniDirty; + // Some games (like Undertale) open and close the same INI file EVERY SINGLE FRAME! + // While on modern devices this isn't a huge deal, this WILL cause issues on devices that have less than stellar file systems (like the PlayStation 2) + // To avoid unnecessary disk reads, we cache the last-closed INI and reuse it on reopen + IniFile* cachedIni; // Cache of last-closed INI (for fast reopen) + char* cachedIniPath; + + // Text file handles for file_text_* functions + OpenTextFile openTextFiles[MAX_OPEN_TEXT_FILES]; + + // Async map ID + int32_t asyncLoadMapId; + + // Used by the "os_type" built-in + YoYoOperatingSystem osType; + + // GUI layer size (display_set_gui_size). 0 = auto-match the current view's port size. int32_t guiWidth; int32_t guiHeight; + // Per-frame draw breakdown for lightweight platform HUD diagnostics. + uint32_t frameDrawBackgrounds; + uint32_t frameDrawTiles; + uint32_t frameDrawInstances; + uint32_t frameDrawLayerElements; + double frameDrawBackgroundMs; + double frameDrawTilesMs; + double frameDrawInstancesMs; + double frameDrawLayerMs; + double frameDrawGuiMs; + double frameDrawGuiEventMs; + double frameDrawBattleReplayMs; + double frameStepPrepMs; + double frameStepEventMs; + double frameStepMotionMs; + double frameStepCollisionMs; + double frameStepFinalizeMs; + double frameStepTopObjectMs; + uint32_t frameStepTopObjectCalls; + int32_t frameStepTopObjectIndex; + double* frameStepObjectMsByObject; + uint32_t* frameStepObjectCallsByObject; + bool n3dsDrawBattleStateValid; + bool n3dsDrawBattleActive; + bool n3dsDrawDodgingBullets; + bool n3dsDrawAsrielBattle; + bool n3dsDrawHasTopEnemyDialogue; + bool n3dsBattleReplayListsValid; + bool n3dsTopScreenGUIListValid; + Instance** n3dsBattleFieldInstances; + Instance** n3dsBattleUIInstances; + Instance** n3dsTopScreenGUIInstances; + // Bottom-screen text/dialogue/inventory UI state (separate from battle) + bool n3dsDrawTextUIStateValid; + bool n3dsDrawTextUIActive; + bool n3dsTextUIListValid; + Instance** n3dsTextUIInstances; + bool n3dsTopScreenGUIResponderListsValid[3]; + N3DSResolvedDrawEvent* n3dsTopScreenGUIResponderEvents[3]; + // GMS legacy (pre 2022.1) collision behavior: AABB overlap treats touching edges as overlap. bool collisionCompatibilityMode; } Runner; +typedef struct { + uint32_t liveInstances; + uint32_t liveStructInstances; + uint32_t dsMapSlots; + uint32_t dsMapLive; + uint32_t dsListSlots; + uint32_t dsListLive; + uint32_t dsListItems; +} RunnerTelemetry; + const char* Runner_getEventName(int32_t eventType, int32_t eventSubtype); void Runner_reset(Runner* runner); Runner* Runner_create(DataWin* dataWin, VMContext* vm, Renderer* renderer, FileSystem* fileSystem, AudioSystem* audioSystem); -void Runner_initFirstRoom(Runner* runner); -void Runner_step(Runner* runner); -void Runner_executeEvent(Runner* runner, Instance* instance, int32_t eventType, int32_t eventSubtype); -void Runner_executeEventFromObject(Runner* runner, Instance* instance, int32_t startObjectIndex, int32_t eventType, int32_t eventSubtype); -void Runner_executeEventForAll(Runner* runner, int32_t eventType, int32_t eventSubtype); -void Runner_draw(Runner* runner); +void Runner_initFirstRoom(Runner* runner); +void Runner_step(Runner* runner); +void Runner_executeEvent(Runner* runner, Instance* instance, int32_t eventType, int32_t eventSubtype); +void Runner_executeEventFromObject(Runner* runner, Instance* instance, int32_t startObjectIndex, int32_t eventType, int32_t eventSubtype); +void Runner_executeEventForAll(Runner* runner, int32_t eventType, int32_t eventSubtype); +void Runner_draw(Runner* runner); void Runner_drawGUI(Runner* runner); -void Runner_drawBackgrounds(Runner* runner, bool foreground); -void Runner_computeViewDisplayScale(Runner* runner, int32_t gameW, int32_t gameH, float* outScaleX, float* outScaleY); -void Runner_drawViews(Runner* runner, int32_t gameW, int32_t gameH, float displayScaleX, float displayScaleY, bool debugShowCollisionMasks); -void Runner_scrollBackgrounds(Runner* runner); -Instance* Runner_createInstance(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex); -Instance* Runner_createInstanceWithDepth(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex, int32_t depth); -Instance* Runner_createInstanceWithLayer(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex, int32_t layerId); -Instance* Runner_copyInstance(Runner* runner, Instance* source, bool performEvent); -void Runner_destroyInstance(Runner* runner, Instance* inst); -void Runner_cleanupDestroyedInstances(Runner* runner); -// Add inst to the per-object lists of its object and every ancestor. -void Runner_addInstanceToObjectLists(Runner* runner, Instance* inst); -// Remove inst from the per-object lists of its object and every ancestor, preserving creation order (stable remove). -void Runner_removeInstanceFromObjectLists(Runner* runner, Instance* inst); -// Reset every per-object list to length 0 without releasing the backing arrays. -void Runner_clearAllObjectLists(Runner* runner); - -// Push a snapshot of instancesByObject[targetObjIndex] onto runner->instanceSnapshots. Returns the base offset where this snapshot begins. -// The length is arrlen(runner->instanceSnapshots) - base. -// Invalid indices or empty buckets push zero entries (base == current arena length). -// Pair with Runner_popInstanceSnapshot(runner, base) when done. -int32_t Runner_pushInstancesOfObject(Runner* runner, int32_t targetObjIndex); -// Push a snapshot matching "target", which GML can pass in several forms: an object index (push the descendant-inclusive bucket), INSTANCE_ALL (push every instance in the room), or an instance ID >= 100000 (push that single instance if it exists). -// Returns base offset for pairing with Runner_popInstanceSnapshot. -int32_t Runner_pushInstancesForTarget(Runner* runner, int32_t target); -// Truncate the snapshot arena back to "base", releasing everything pushed after it. -void Runner_popInstanceSnapshot(Runner* runner, int32_t base); - -void Runner_dumpState(Runner* runner); -char* Runner_dumpStateJson(Runner* runner); -void Runner_free(Runner* runner); +void Runner_resetFrameDrawStats(Runner* runner); +void Runner_drawBackgrounds(Runner* runner, bool foreground); +void Runner_computeViewDisplayScale(Runner* runner, int32_t gameW, int32_t gameH, float* outScaleX, float* outScaleY); +void Runner_drawViews(Runner* runner, int32_t gameW, int32_t gameH, float displayScaleX, float displayScaleY, bool debugShowCollisionMasks); +void Runner_scrollBackgrounds(Runner* runner); +Instance* Runner_createInstance(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex); +Instance* Runner_createInstanceWithDepth(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex, int32_t depth); +Instance* Runner_createInstanceWithLayer(Runner* runner, GMLReal x, GMLReal y, int32_t objectIndex, int32_t layerId); +Instance* Runner_copyInstance(Runner* runner, Instance* source, bool performEvent); +void Runner_destroyInstance(Runner* runner, Instance* inst); +void Runner_cleanupDestroyedInstances(Runner* runner); +// Add inst to the per-object lists of its object and every ancestor. +void Runner_addInstanceToObjectLists(Runner* runner, Instance* inst); +// Remove inst from the per-object lists of its object and every ancestor, preserving creation order (stable remove). +void Runner_removeInstanceFromObjectLists(Runner* runner, Instance* inst); +// Reset every per-object list to length 0 without releasing the backing arrays. +void Runner_clearAllObjectLists(Runner* runner); + +// Push a snapshot of instancesByObject[targetObjIndex] onto runner->instanceSnapshots. Returns the base offset where this snapshot begins. +// The length is arrlen(runner->instanceSnapshots) - base. +// Invalid indices or empty buckets push zero entries (base == current arena length). +// Pair with Runner_popInstanceSnapshot(runner, base) when done. +int32_t Runner_pushInstancesOfObject(Runner* runner, int32_t targetObjIndex); +// Push a snapshot matching "target", which GML can pass in several forms: an object index (push the descendant-inclusive bucket), INSTANCE_ALL (push every instance in the room), or an instance ID >= 100000 (push that single instance if it exists). +// Returns base offset for pairing with Runner_popInstanceSnapshot. +int32_t Runner_pushInstancesForTarget(Runner* runner, int32_t target); +// Truncate the snapshot arena back to "base", releasing everything pushed after it. +void Runner_popInstanceSnapshot(Runner* runner, int32_t base); + +void Runner_dumpState(Runner* runner); +char* Runner_dumpStateJson(Runner* runner); +void Runner_free(Runner* runner); RuntimeLayer* Runner_findRuntimeLayerById(Runner* runner, int32_t id); RoomLayer* Runner_findRoomLayerById(Runner* runner, int32_t id); RuntimeLayerElement* Runner_findLayerElementById(Runner* runner, int32_t elementId, RuntimeLayer** outLayer); uint32_t Runner_getNextLayerId(Runner* runner); +void Runner_drawTileLayer(Runner* runner, uint32_t layerIndex, RoomLayerTilesData* data, float layerOffsetX, float layerOffsetY, float alpha); void Runner_freeRuntimeLayer(RuntimeLayer* runtimeLayer); +RunnerTelemetry Runner_collectTelemetry(Runner* runner); diff --git a/src/runner_keyboard.c b/src/runner_keyboard.c index 02ef25bc..964a7c27 100644 --- a/src/runner_keyboard.c +++ b/src/runner_keyboard.c @@ -1,121 +1,121 @@ -#include "runner_keyboard.h" -#include "utils.h" - -#include -#include - -static bool isValidKey(int32_t key) { - return key >= 0 && GML_KEY_COUNT > key; -} - -RunnerKeyboardState* RunnerKeyboard_create(void) { - RunnerKeyboardState* kb = safeCalloc(1, sizeof(RunnerKeyboardState)); - kb->lastKey = VK_NOKEY; - kb->lastChar[0] = 0; - kb->lastChar[1] = 0; - return kb; -} - -void RunnerKeyboard_free(RunnerKeyboardState* kb) { - free(kb); -} - -void RunnerKeyboard_beginFrame(RunnerKeyboardState* kb) { - memset(kb->keyPressed, 0, sizeof(kb->keyPressed)); - memset(kb->keyReleased, 0, sizeof(kb->keyReleased)); -} - -void RunnerKeyboard_onKeyDown(RunnerKeyboardState* kb, int32_t gmlKeyCode) { - if (!isValidKey(gmlKeyCode)) return; - kb->keyDown[gmlKeyCode] = true; - kb->keyPressed[gmlKeyCode] = true; - kb->lastKey = gmlKeyCode; -} - -void RunnerKeyboard_onKeyUp(RunnerKeyboardState* kb, int32_t gmlKeyCode) { - if (!isValidKey(gmlKeyCode)) return; - kb->keyDown[gmlKeyCode] = false; - kb->keyReleased[gmlKeyCode] = true; -} - -void RunnerKeyboard_onCharacter(RunnerKeyboardState* kb, unsigned int character) { - kb->lastChar[0] = (character >= ' ' && character <= '~') ? (char) character : 0; -} - -bool RunnerKeyboard_check(RunnerKeyboardState* kb, int32_t gmlKeyCode) { - if (gmlKeyCode == VK_ANYKEY) { - for (int32_t i = 2; GML_KEY_COUNT > i; i++) { - if (kb->keyDown[i]) return true; - } - return false; - } - if (gmlKeyCode == VK_NOKEY) { - for (int32_t i = 2; GML_KEY_COUNT > i; i++) { - if (kb->keyDown[i]) return false; - } - return true; - } - if (!isValidKey(gmlKeyCode)) return false; - return kb->keyDown[gmlKeyCode]; -} - -bool RunnerKeyboard_checkPressed(RunnerKeyboardState* kb, int32_t gmlKeyCode) { - if (gmlKeyCode == VK_ANYKEY) { - for (int32_t i = 2; GML_KEY_COUNT > i; i++) { - if (kb->keyPressed[i]) return true; - } - return false; - } - if (gmlKeyCode == VK_NOKEY) { - for (int32_t i = 2; GML_KEY_COUNT > i; i++) { - if (kb->keyPressed[i]) return false; - } - return true; - } - if (!isValidKey(gmlKeyCode)) return false; - return kb->keyPressed[gmlKeyCode]; -} - -bool RunnerKeyboard_checkReleased(RunnerKeyboardState* kb, int32_t gmlKeyCode) { - if (gmlKeyCode == VK_ANYKEY) { - for (int32_t i = 2; GML_KEY_COUNT > i; i++) { - if (kb->keyReleased[i]) return true; - } - return false; - } - if (gmlKeyCode == VK_NOKEY) { - for (int32_t i = 2; GML_KEY_COUNT > i; i++) { - if (kb->keyReleased[i]) return false; - } - return true; - } - if (!isValidKey(gmlKeyCode)) return false; - return kb->keyReleased[gmlKeyCode]; -} - -void RunnerKeyboard_simulatePress(RunnerKeyboardState* kb, int32_t gmlKeyCode) { - if (!isValidKey(gmlKeyCode)) return; - kb->keyDown[gmlKeyCode] = true; - kb->keyPressed[gmlKeyCode] = true; - kb->lastKey = gmlKeyCode; -} - -void RunnerKeyboard_simulateRelease(RunnerKeyboardState* kb, int32_t gmlKeyCode) { - if (!isValidKey(gmlKeyCode)) return; - kb->keyDown[gmlKeyCode] = false; - kb->keyReleased[gmlKeyCode] = true; -} - -void RunnerKeyboard_clear(RunnerKeyboardState* kb, int32_t gmlKeyCode) { - if (gmlKeyCode == VK_ANYKEY) { - memset(kb->keyDown, 0, sizeof(kb->keyDown)); - memset(kb->keyPressed, 0, sizeof(kb->keyPressed)); - memset(kb->keyReleased, 0, sizeof(kb->keyReleased)); - kb->lastKey = VK_NOKEY; - return; - } - if (!isValidKey(gmlKeyCode)) return; - kb->keyDown[gmlKeyCode] = false; - kb->keyPressed[gmlKeyCode] = false; - kb->keyReleased[gmlKeyCode] = false; -} +#include "runner_keyboard.h" +#include "utils.h" + +#include +#include + +static bool isValidKey(int32_t key) { + return key >= 0 && GML_KEY_COUNT > key; +} + +RunnerKeyboardState* RunnerKeyboard_create(void) { + RunnerKeyboardState* kb = safeCalloc(1, sizeof(RunnerKeyboardState)); + kb->lastKey = VK_NOKEY; + kb->lastChar[0] = 0; + kb->lastChar[1] = 0; + return kb; +} + +void RunnerKeyboard_free(RunnerKeyboardState* kb) { + free(kb); +} + +void RunnerKeyboard_beginFrame(RunnerKeyboardState* kb) { + memset(kb->keyPressed, 0, sizeof(kb->keyPressed)); + memset(kb->keyReleased, 0, sizeof(kb->keyReleased)); +} + +void RunnerKeyboard_onKeyDown(RunnerKeyboardState* kb, int32_t gmlKeyCode) { + if (!isValidKey(gmlKeyCode)) return; + kb->keyDown[gmlKeyCode] = true; + kb->keyPressed[gmlKeyCode] = true; + kb->lastKey = gmlKeyCode; +} + +void RunnerKeyboard_onKeyUp(RunnerKeyboardState* kb, int32_t gmlKeyCode) { + if (!isValidKey(gmlKeyCode)) return; + kb->keyDown[gmlKeyCode] = false; + kb->keyReleased[gmlKeyCode] = true; +} + +void RunnerKeyboard_onCharacter(RunnerKeyboardState* kb, unsigned int character) { + kb->lastChar[0] = (character >= ' ' && character <= '~') ? (char) character : 0; +} + +bool RunnerKeyboard_check(RunnerKeyboardState* kb, int32_t gmlKeyCode) { + if (gmlKeyCode == VK_ANYKEY) { + for (int32_t i = 2; GML_KEY_COUNT > i; i++) { + if (kb->keyDown[i]) return true; + } + return false; + } + if (gmlKeyCode == VK_NOKEY) { + for (int32_t i = 2; GML_KEY_COUNT > i; i++) { + if (kb->keyDown[i]) return false; + } + return true; + } + if (!isValidKey(gmlKeyCode)) return false; + return kb->keyDown[gmlKeyCode]; +} + +bool RunnerKeyboard_checkPressed(RunnerKeyboardState* kb, int32_t gmlKeyCode) { + if (gmlKeyCode == VK_ANYKEY) { + for (int32_t i = 2; GML_KEY_COUNT > i; i++) { + if (kb->keyPressed[i]) return true; + } + return false; + } + if (gmlKeyCode == VK_NOKEY) { + for (int32_t i = 2; GML_KEY_COUNT > i; i++) { + if (kb->keyPressed[i]) return false; + } + return true; + } + if (!isValidKey(gmlKeyCode)) return false; + return kb->keyPressed[gmlKeyCode]; +} + +bool RunnerKeyboard_checkReleased(RunnerKeyboardState* kb, int32_t gmlKeyCode) { + if (gmlKeyCode == VK_ANYKEY) { + for (int32_t i = 2; GML_KEY_COUNT > i; i++) { + if (kb->keyReleased[i]) return true; + } + return false; + } + if (gmlKeyCode == VK_NOKEY) { + for (int32_t i = 2; GML_KEY_COUNT > i; i++) { + if (kb->keyReleased[i]) return false; + } + return true; + } + if (!isValidKey(gmlKeyCode)) return false; + return kb->keyReleased[gmlKeyCode]; +} + +void RunnerKeyboard_simulatePress(RunnerKeyboardState* kb, int32_t gmlKeyCode) { + if (!isValidKey(gmlKeyCode)) return; + kb->keyDown[gmlKeyCode] = true; + kb->keyPressed[gmlKeyCode] = true; + kb->lastKey = gmlKeyCode; +} + +void RunnerKeyboard_simulateRelease(RunnerKeyboardState* kb, int32_t gmlKeyCode) { + if (!isValidKey(gmlKeyCode)) return; + kb->keyDown[gmlKeyCode] = false; + kb->keyReleased[gmlKeyCode] = true; +} + +void RunnerKeyboard_clear(RunnerKeyboardState* kb, int32_t gmlKeyCode) { + if (gmlKeyCode == VK_ANYKEY) { + memset(kb->keyDown, 0, sizeof(kb->keyDown)); + memset(kb->keyPressed, 0, sizeof(kb->keyPressed)); + memset(kb->keyReleased, 0, sizeof(kb->keyReleased)); + kb->lastKey = VK_NOKEY; + return; + } + if (!isValidKey(gmlKeyCode)) return; + kb->keyDown[gmlKeyCode] = false; + kb->keyPressed[gmlKeyCode] = false; + kb->keyReleased[gmlKeyCode] = false; +} diff --git a/src/runner_keyboard.h b/src/runner_keyboard.h index f6025dbe..f5de7343 100644 --- a/src/runner_keyboard.h +++ b/src/runner_keyboard.h @@ -1,77 +1,77 @@ -#pragma once - -#include "common.h" -#include -#include - -// GML uses key codes 0-255 (vk_nokey=0, vk_anykey=1, ASCII codes, etc.) -#define GML_KEY_COUNT 256 - -// GML Virtual Key Constants (match Windows VK codes) -#define VK_NOKEY 0 -#define VK_ANYKEY 1 -#define VK_BACKSPACE 8 -#define VK_TAB 9 -#define VK_ENTER 13 -#define VK_SHIFT 16 -#define VK_CONTROL 17 -#define VK_ALT 18 -#define VK_ESCAPE 27 -#define VK_SPACE 32 -#define VK_PAGEUP 33 -#define VK_PAGEDOWN 34 -#define VK_END 35 -#define VK_HOME 36 -#define VK_LEFT 37 -#define VK_UP 38 -#define VK_RIGHT 39 -#define VK_DOWN 40 -#define VK_INSERT 45 -#define VK_DELETE 46 -// 48-57 = '0'-'9', 65-90 = 'A'-'Z' (ASCII) -#define VK_F1 112 -#define VK_F2 113 -#define VK_F3 114 -#define VK_F4 115 -#define VK_F5 116 -#define VK_F6 117 -#define VK_F7 118 -#define VK_F8 119 -#define VK_F9 120 -#define VK_F10 121 -#define VK_F11 122 -#define VK_F12 123 - -typedef struct RunnerKeyboardState { - bool keyDown[GML_KEY_COUNT]; // Currently held - bool keyPressed[GML_KEY_COUNT]; // Just pressed this frame - bool keyReleased[GML_KEY_COUNT]; // Just released this frame - int32_t lastKey; // Last key pressed (for keyboard_key variable) - char lastChar[2]; // Last character pressed (for keyboard_char variable) -} RunnerKeyboardState; - -// Lifecycle -RunnerKeyboardState* RunnerKeyboard_create(void); -void RunnerKeyboard_free(RunnerKeyboardState* kb); - -// Called at the start of each frame to clear pressed/released arrays -void RunnerKeyboard_beginFrame(RunnerKeyboardState* kb); - -// Called by platform layer when a key is pressed/released (gmlKeyCode = GML vk_ code) -void RunnerKeyboard_onKeyDown(RunnerKeyboardState* kb, int32_t gmlKeyCode); -void RunnerKeyboard_onKeyUp(RunnerKeyboardState* kb, int32_t gmlKeyCode); - -// Called by platform layer when a character is typed -void RunnerKeyboard_onCharacter(RunnerKeyboardState* kb, unsigned int character); - -// GML function queries -bool RunnerKeyboard_check(RunnerKeyboardState* kb, int32_t gmlKeyCode); -bool RunnerKeyboard_checkPressed(RunnerKeyboardState* kb, int32_t gmlKeyCode); -bool RunnerKeyboard_checkReleased(RunnerKeyboardState* kb, int32_t gmlKeyCode); - -// Simulated press/release (used by keyboard_key_press/keyboard_key_release GML functions) -void RunnerKeyboard_simulatePress(RunnerKeyboardState* kb, int32_t gmlKeyCode); -void RunnerKeyboard_simulateRelease(RunnerKeyboardState* kb, int32_t gmlKeyCode); - -// Clear a specific key's state -void RunnerKeyboard_clear(RunnerKeyboardState* kb, int32_t gmlKeyCode); +#pragma once + +#include "common.h" +#include +#include + +// GML uses key codes 0-255 (vk_nokey=0, vk_anykey=1, ASCII codes, etc.) +#define GML_KEY_COUNT 256 + +// GML Virtual Key Constants (match Windows VK codes) +#define VK_NOKEY 0 +#define VK_ANYKEY 1 +#define VK_BACKSPACE 8 +#define VK_TAB 9 +#define VK_ENTER 13 +#define VK_SHIFT 16 +#define VK_CONTROL 17 +#define VK_ALT 18 +#define VK_ESCAPE 27 +#define VK_SPACE 32 +#define VK_PAGEUP 33 +#define VK_PAGEDOWN 34 +#define VK_END 35 +#define VK_HOME 36 +#define VK_LEFT 37 +#define VK_UP 38 +#define VK_RIGHT 39 +#define VK_DOWN 40 +#define VK_INSERT 45 +#define VK_DELETE 46 +// 48-57 = '0'-'9', 65-90 = 'A'-'Z' (ASCII) +#define VK_F1 112 +#define VK_F2 113 +#define VK_F3 114 +#define VK_F4 115 +#define VK_F5 116 +#define VK_F6 117 +#define VK_F7 118 +#define VK_F8 119 +#define VK_F9 120 +#define VK_F10 121 +#define VK_F11 122 +#define VK_F12 123 + +typedef struct RunnerKeyboardState { + bool keyDown[GML_KEY_COUNT]; // Currently held + bool keyPressed[GML_KEY_COUNT]; // Just pressed this frame + bool keyReleased[GML_KEY_COUNT]; // Just released this frame + int32_t lastKey; // Last key pressed (for keyboard_key variable) + char lastChar[2]; // Last character pressed (for keyboard_char variable) +} RunnerKeyboardState; + +// Lifecycle +RunnerKeyboardState* RunnerKeyboard_create(void); +void RunnerKeyboard_free(RunnerKeyboardState* kb); + +// Called at the start of each frame to clear pressed/released arrays +void RunnerKeyboard_beginFrame(RunnerKeyboardState* kb); + +// Called by platform layer when a key is pressed/released (gmlKeyCode = GML vk_ code) +void RunnerKeyboard_onKeyDown(RunnerKeyboardState* kb, int32_t gmlKeyCode); +void RunnerKeyboard_onKeyUp(RunnerKeyboardState* kb, int32_t gmlKeyCode); + +// Called by platform layer when a character is typed +void RunnerKeyboard_onCharacter(RunnerKeyboardState* kb, unsigned int character); + +// GML function queries +bool RunnerKeyboard_check(RunnerKeyboardState* kb, int32_t gmlKeyCode); +bool RunnerKeyboard_checkPressed(RunnerKeyboardState* kb, int32_t gmlKeyCode); +bool RunnerKeyboard_checkReleased(RunnerKeyboardState* kb, int32_t gmlKeyCode); + +// Simulated press/release (used by keyboard_key_press/keyboard_key_release GML functions) +void RunnerKeyboard_simulatePress(RunnerKeyboardState* kb, int32_t gmlKeyCode); +void RunnerKeyboard_simulateRelease(RunnerKeyboardState* kb, int32_t gmlKeyCode); + +// Clear a specific key's state +void RunnerKeyboard_clear(RunnerKeyboardState* kb, int32_t gmlKeyCode); diff --git a/src/rvalue.h b/src/rvalue.h index 9b5c1a28..75c4f831 100644 --- a/src/rvalue.h +++ b/src/rvalue.h @@ -1,377 +1,377 @@ -#pragma once -#include -#include "common.h" -#include -#include -#include - -#include "real_type.h" -#include "stb_ds.h" -#include "utils.h" -#include "bytecode_versions.h" - -// Forward declarations -struct GMLArray; -typedef struct GMLArray GMLArray; -void GMLArray_decRef(struct GMLArray* arr); -void GMLArray_incRef(struct GMLArray* arr); - -struct Instance; -typedef struct Instance Instance; -void Instance_structIncRef(struct Instance* inst); -void Instance_structDecRef(struct Instance* inst); -uint32_t Instance_getInstanceId(struct Instance* inst); - -#include "gml_method.h" - -// ===[ GML Data Types (4-bit type codes) ]=== -#define GML_TYPE_DOUBLE 0x0 -#define GML_TYPE_FLOAT 0x1 -#define GML_TYPE_INT32 0x2 -#define GML_TYPE_INT64 0x3 -#define GML_TYPE_BOOL 0x4 -#define GML_TYPE_VARIABLE 0x5 -#define GML_TYPE_STRING 0x6 -#define GML_TYPE_INT16 0xF - -// ===[ RValue - Tagged Union ]=== -typedef enum { - RVALUE_UNDEFINED = 0, - RVALUE_STRING = 1, - RVALUE_INT32 = 2, - RVALUE_INT64 = 3, - RVALUE_BOOL = 4, - RVALUE_REAL = 5, - RVALUE_ARRAY = 6, - RVALUE_METHOD = 7, - RVALUE_STRUCT = 8, -} RValueType; - -typedef struct RValue { - union { - GMLReal real; - int32_t int32; -#ifndef NO_RVALUE_INT64 - int64_t int64; -#endif - const char* string; - GMLArray* array; -#if IS_BC17_OR_HIGHER_ENABLED - GMLMethod* method; -#endif - Instance* structInst; - }; - // We use uint8_t for the type instead of RValueType because a enum value occupies 4 bytes, while uint8_t occupies 1 byte - uint8_t type; - // For RVALUE_STRING: true = the `string` buffer is owned and must be freed on RValue_free. - // For RVALUE_ARRAY, RVALUE_METHOD, RVALUE_STRUCT: true = this RValue holds one strong ref and must decRef on RValue_free. - // Non-owning ("weak") RValues are short-lived views returned by getters, caller must NOT free them. - bool ownsReference; -#if IS_BC17_OR_HIGHER_ENABLED - uint8_t gmlStackType; // GML data type from the instruction that pushed this value -#endif -} __attribute__((aligned(8))) RValue; - -// Helper to initialize .gmlStackType only on BC17+ builds -#if IS_BC17_OR_HIGHER_ENABLED -# define RVALUE_INIT_GMLTYPE(t) .gmlStackType = (t) -#else -# define RVALUE_INIT_GMLTYPE(t) -#endif - -static RValue RValue_makeReal(GMLReal val) { - return (RValue){ .real = val, .type = RVALUE_REAL, RVALUE_INIT_GMLTYPE(GML_TYPE_DOUBLE) }; -} - -static RValue RValue_makeInt32(int32_t val) { - return (RValue){ .int32 = val, .type = RVALUE_INT32, RVALUE_INIT_GMLTYPE(GML_TYPE_INT32) }; -} - -static RValue RValue_makeInt64(int64_t val) { -#ifdef NO_RVALUE_INT64 - // Values that don't fit in int32 get promoted to real instead of clamped, because clamping to INT32_MIN causes arithmetic overflow bugs - // (example: Undertale's mercymod = -99999999999999 in the Asriel fight) - if (val > INT32_MAX || INT32_MIN > val) { - return (RValue){ .real = (GMLReal) val, .type = RVALUE_REAL, RVALUE_INIT_GMLTYPE(GML_TYPE_DOUBLE) }; - } else { - return (RValue){ .int32 = (int32_t) val, .type = RVALUE_INT32, RVALUE_INIT_GMLTYPE(GML_TYPE_INT32) }; - } -#else - return (RValue){ .int64 = val, .type = RVALUE_INT64, RVALUE_INIT_GMLTYPE(GML_TYPE_INT64) }; -#endif -} - -static RValue RValue_makeBool(bool val) { - return (RValue){ .int32 = val ? 1 : 0, .type = RVALUE_BOOL, RVALUE_INIT_GMLTYPE(GML_TYPE_BOOL) }; -} - -static RValue RValue_makeString(const char* val) { - return (RValue){ .string = val, .type = RVALUE_STRING, .ownsReference = false, RVALUE_INIT_GMLTYPE(GML_TYPE_STRING) }; -} - -static RValue RValue_makeOwnedString(char* val) { - return (RValue){ .string = val, .type = RVALUE_STRING, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_STRING) }; -} - -static RValue RValue_makeUndefined(void) { - return (RValue){ .type = RVALUE_UNDEFINED, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; -} - -// Takes ownership: refCount is NOT bumped (caller hands off its ref). The returned RValue decRefs on free. -// Use this when you have a freshly-allocated array (GMLArray_alloc) or after a GMLArray_incRef. -static RValue RValue_makeArray(GMLArray* arr) { - return (RValue){ .array = arr, .type = RVALUE_ARRAY, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; -} - -// Weak view: does not own (no decRef on free). Callers that stash the value long-term must incRef + set ownsString. -static RValue RValue_makeArrayWeak(GMLArray* arr) { - return (RValue){ .array = arr, .type = RVALUE_ARRAY, .ownsReference = false, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; -} - -#if IS_BC17_OR_HIGHER_ENABLED -// Takes ownership: refCount is NOT bumped (caller hands off its ref). The returned RValue decRefs on free. -static RValue RValue_makeMethod(int32_t codeIndex, int32_t boundInstanceId) { - return (RValue){ .method = GMLMethod_create(codeIndex, boundInstanceId), .type = RVALUE_METHOD, .ownsReference = true, .gmlStackType = GML_TYPE_VARIABLE }; -} - -// Weak view: does not own (no decRef on free). Callers that stash the value long-term must incRef + set ownsString. -static RValue RValue_makeMethodWeak(GMLMethod* m) { - return (RValue){ .method = m, .type = RVALUE_METHOD, .ownsReference = false, .gmlStackType = GML_TYPE_VARIABLE }; -} -#endif - -// Takes ownership: refCount is NOT bumped (caller hands off its ref). The returned RValue decRefs on free. -// Use this for the freshly-allocated struct returned by @@NewGMLObject@@, after the caller has already accounted for both the registry's implicit ref and the returned-RValue ref. -static RValue RValue_makeStruct(Instance* inst) { - return (RValue){ .structInst = inst, .type = RVALUE_STRUCT, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; -} - -// Weak view: does not own (no decRef on free). Callers that stash the value long-term must incRef + set ownsString. -static RValue RValue_makeStructWeak(Instance* inst) { - return (RValue){ .structInst = inst, .type = RVALUE_STRUCT, .ownsReference = false, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; -} - -// Makes "val" independent, that is... -// * For strings, it creates an owned copy of it (copying the original string underneath) -// * For arrays/methods/structs, it increments the reference count -// * For anything else, it does nothing because they don't need to be made independent -// -// Useful to store arbitrary RValues in containers (ds_map, ds_list, etc.) -// The caller's original RValue is unaffected and must still be freed normally. -static RValue RValue_makeIndependent(RValue val) { - if (val.type == RVALUE_STRING && val.string != nullptr) { - return RValue_makeOwnedString(safeStrdup(val.string)); - } else if (val.type == RVALUE_ARRAY && val.array != nullptr) { - GMLArray_incRef(val.array); - val.ownsReference = true; - return val; -#if IS_BC17_OR_HIGHER_ENABLED - } else if (val.type == RVALUE_METHOD && val.method != nullptr) { - GMLMethod_incRef(val.method); - val.ownsReference = true; - return val; -#endif - } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) { - Instance_structIncRef(val.structInst); - val.ownsReference = true; - return val; - } - requireMessageFormatted(!val.ownsReference, "Trying to make independent a RValue (type=%d) that owns a reference, but we don't handle it yet! Did you add a new refcounted value to Butterscotch without implementing RValue_makeIndependent for it?", val.type); - return val; -} - -// Converts an RValue to a heap-allocated string representation. -// The caller must free the returned string -static char* RValue_toString(RValue val) { - char buf[64]; - switch (val.type) { - case RVALUE_REAL: - snprintf(buf, sizeof(buf), "%.16g", (double) val.real); - return safeStrdup(buf); - case RVALUE_INT32: - snprintf(buf, sizeof(buf), "%d", val.int32); - return safeStrdup(buf); -#ifndef NO_RVALUE_INT64 - case RVALUE_INT64: - snprintf(buf, sizeof(buf), "%lld", (long long) val.int64); - return safeStrdup(buf); -#endif - case RVALUE_STRING: - return safeStrdup(val.string != nullptr ? val.string : ""); - case RVALUE_BOOL: - return safeStrdup(val.int32 ? "1" : "0"); - case RVALUE_UNDEFINED: - return safeStrdup("undefined"); - case RVALUE_ARRAY: - snprintf(buf, sizeof(buf), "", (void*) val.array); - return safeStrdup(buf); -#if IS_BC17_OR_HIGHER_ENABLED - case RVALUE_METHOD: - snprintf(buf, sizeof(buf), "", val.method->codeIndex); - return safeStrdup(buf); -#endif - case RVALUE_STRUCT: - snprintf(buf, sizeof(buf), "", val.structInst != nullptr ? Instance_getInstanceId(val.structInst) : 0); - return safeStrdup(buf); - } - return safeStrdup(""); -} - -// Converts an RValue to a heap-allocated string representation, used for debug logs. -// The caller must free the returned string -static char* RValue_toStringFancy(RValue val) { - switch (val.type) { - case RVALUE_STRING: { - char* valueAsString = RValue_toString(val); - - // length + quotes (2) + null terminator - int newLength = strlen(valueAsString) + 3; - char* valueWithQuotes = safeCalloc(newLength, sizeof(char)); - snprintf(valueWithQuotes, newLength, "\"%s\"", valueAsString); - - free(valueAsString); - - return valueWithQuotes; - } - default: { - return RValue_toString(val); - } - } -} - -// Converts an RValue to a heap-allocated string with a type tag prefix, used for trace-stack output. -// Examples: int32(42), real(3.14), "hello", bool(true), undefined, -// The caller must free the returned string -static char* RValue_toStringTyped(RValue val) { - char buf[128]; - switch (val.type) { - case RVALUE_REAL: - snprintf(buf, sizeof(buf), "real(%.16g)", (double) val.real); - return safeStrdup(buf); - case RVALUE_INT32: - snprintf(buf, sizeof(buf), "int32(%d)", val.int32); - return safeStrdup(buf); -#ifndef NO_RVALUE_INT64 - case RVALUE_INT64: - snprintf(buf, sizeof(buf), "int64(%lld)", (long long) val.int64); - return safeStrdup(buf); -#endif - case RVALUE_STRING: { - const char* str = val.string != nullptr ? val.string : ""; - size_t needed = strlen(str) + 3; - char* result = safeCalloc(needed, sizeof(char)); - snprintf(result, needed, "\"%s\"", str); - return result; - } - case RVALUE_BOOL: - return safeStrdup(val.int32 ? "bool(true)" : "bool(false)"); - case RVALUE_UNDEFINED: - return safeStrdup("undefined"); - case RVALUE_ARRAY: - snprintf(buf, sizeof(buf), "", (void*) val.array); - return safeStrdup(buf); -#if IS_BC17_OR_HIGHER_ENABLED - case RVALUE_METHOD: - snprintf(buf, sizeof(buf), "method(code=%d, inst=%d)", val.method->codeIndex, val.method->boundInstanceId); - return safeStrdup(buf); -#endif - case RVALUE_STRUCT: - snprintf(buf, sizeof(buf), "struct(id=%u)", val.structInst != nullptr ? Instance_getInstanceId(val.structInst) : 0); - return safeStrdup(buf); - } - return safeStrdup("???"); -} - -static void RValue_free(RValue* val) { - if (val->type == RVALUE_STRING && val->ownsReference && val->string != nullptr) { - free((void*) val->string); - val->string = nullptr; - val->ownsReference = false; - } else if (val->type == RVALUE_ARRAY && val->ownsReference && val->array != nullptr) { - GMLArray_decRef(val->array); - val->array = nullptr; - val->ownsReference = false; -#if IS_BC17_OR_HIGHER_ENABLED - } else if (val->type == RVALUE_METHOD && val->ownsReference && val->method != nullptr) { - GMLMethod_decRef(val->method); - val->method = nullptr; - val->ownsReference = false; -#endif - } else if (val->type == RVALUE_STRUCT && val->ownsReference && val->structInst != nullptr) { - Instance_structDecRef(val->structInst); - val->structInst = nullptr; - val->ownsReference = false; - } -} - -static GMLReal RValue_toReal(RValue val) { - switch (val.type) { - case RVALUE_REAL: return val.real; - case RVALUE_INT32: return (GMLReal) val.int32; -#ifndef NO_RVALUE_INT64 - case RVALUE_INT64: return (GMLReal) val.int64; -#endif - case RVALUE_BOOL: return (GMLReal) val.int32; - case RVALUE_STRING: return GMLReal_strtod(val.string, nullptr); - case RVALUE_ARRAY: return 0.0; -#if IS_BC17_OR_HIGHER_ENABLED - case RVALUE_METHOD: return 0.0; -#endif - case RVALUE_STRUCT: return val.structInst != nullptr ? (GMLReal) Instance_getInstanceId(val.structInst) : 0.0; - default: return 0.0; - } -} - -static int32_t RValue_toInt32(RValue val) { - switch (val.type) { - case RVALUE_REAL: return (int32_t) val.real; - case RVALUE_INT32: return val.int32; -#ifndef NO_RVALUE_INT64 - case RVALUE_INT64: return (int32_t) val.int64; -#endif - case RVALUE_BOOL: return val.int32; - case RVALUE_STRING: return (int32_t) GMLReal_strtod(val.string, nullptr); - case RVALUE_ARRAY: return 0; -#if IS_BC17_OR_HIGHER_ENABLED - case RVALUE_METHOD: return 0; -#endif - case RVALUE_STRUCT: return val.structInst != nullptr ? (int32_t) Instance_getInstanceId(val.structInst) : 0; - default: return 0; - } -} - -static int64_t RValue_toInt64(RValue val) { - switch (val.type) { - case RVALUE_REAL: return (int64_t) val.real; - case RVALUE_INT32: return (int64_t) val.int32; -#ifndef NO_RVALUE_INT64 - case RVALUE_INT64: return val.int64; -#endif - case RVALUE_BOOL: return (int64_t) val.int32; - case RVALUE_STRING: return (int64_t) GMLReal_strtod(val.string, nullptr); - case RVALUE_ARRAY: return 0; -#if IS_BC17_OR_HIGHER_ENABLED - case RVALUE_METHOD: return 0; -#endif - case RVALUE_STRUCT: return val.structInst != nullptr ? (int64_t) Instance_getInstanceId(val.structInst) : 0; - default: return 0; - } -} - -static bool RValue_toBool(RValue val) { - switch (val.type) { - case RVALUE_REAL: return val.real > 0.5; - case RVALUE_INT32: return val.int32 > 0; -#ifndef NO_RVALUE_INT64 - case RVALUE_INT64: return val.int64 > 0; -#endif - case RVALUE_BOOL: return val.int32 != 0; - case RVALUE_STRING: return val.string != nullptr && val.string[0] != '\0'; - case RVALUE_ARRAY: return false; -#if IS_BC17_OR_HIGHER_ENABLED - case RVALUE_METHOD: return true; -#endif - case RVALUE_STRUCT: return val.structInst != nullptr; - default: return false; - } -} +#pragma once +#include +#include "common.h" +#include +#include +#include + +#include "real_type.h" +#include "stb_ds.h" +#include "utils.h" +#include "bytecode_versions.h" + +// Forward declarations +struct GMLArray; +typedef struct GMLArray GMLArray; +void GMLArray_decRef(struct GMLArray* arr); +void GMLArray_incRef(struct GMLArray* arr); + +struct Instance; +typedef struct Instance Instance; +void Instance_structIncRef(struct Instance* inst); +void Instance_structDecRef(struct Instance* inst); +uint32_t Instance_getInstanceId(struct Instance* inst); + +#include "gml_method.h" + +// ===[ GML Data Types (4-bit type codes) ]=== +#define GML_TYPE_DOUBLE 0x0 +#define GML_TYPE_FLOAT 0x1 +#define GML_TYPE_INT32 0x2 +#define GML_TYPE_INT64 0x3 +#define GML_TYPE_BOOL 0x4 +#define GML_TYPE_VARIABLE 0x5 +#define GML_TYPE_STRING 0x6 +#define GML_TYPE_INT16 0xF + +// ===[ RValue - Tagged Union ]=== +typedef enum { + RVALUE_UNDEFINED = 0, + RVALUE_STRING = 1, + RVALUE_INT32 = 2, + RVALUE_INT64 = 3, + RVALUE_BOOL = 4, + RVALUE_REAL = 5, + RVALUE_ARRAY = 6, + RVALUE_METHOD = 7, + RVALUE_STRUCT = 8, +} RValueType; + +typedef struct RValue { + union { + GMLReal real; + int32_t int32; +#ifndef NO_RVALUE_INT64 + int64_t int64; +#endif + const char* string; + GMLArray* array; +#if IS_BC17_OR_HIGHER_ENABLED + GMLMethod* method; +#endif + Instance* structInst; + }; + // We use uint8_t for the type instead of RValueType because a enum value occupies 4 bytes, while uint8_t occupies 1 byte + uint8_t type; + // For RVALUE_STRING: true = the `string` buffer is owned and must be freed on RValue_free. + // For RVALUE_ARRAY, RVALUE_METHOD, RVALUE_STRUCT: true = this RValue holds one strong ref and must decRef on RValue_free. + // Non-owning ("weak") RValues are short-lived views returned by getters, caller must NOT free them. + bool ownsReference; +#if IS_BC17_OR_HIGHER_ENABLED + uint8_t gmlStackType; // GML data type from the instruction that pushed this value +#endif +} __attribute__((aligned(8))) RValue; + +// Helper to initialize .gmlStackType only on BC17+ builds +#if IS_BC17_OR_HIGHER_ENABLED +# define RVALUE_INIT_GMLTYPE(t) .gmlStackType = (t) +#else +# define RVALUE_INIT_GMLTYPE(t) +#endif + +static RValue RValue_makeReal(GMLReal val) { + return (RValue){ .real = val, .type = RVALUE_REAL, RVALUE_INIT_GMLTYPE(GML_TYPE_DOUBLE) }; +} + +static RValue RValue_makeInt32(int32_t val) { + return (RValue){ .int32 = val, .type = RVALUE_INT32, RVALUE_INIT_GMLTYPE(GML_TYPE_INT32) }; +} + +static RValue RValue_makeInt64(int64_t val) { +#ifdef NO_RVALUE_INT64 + // Values that don't fit in int32 get promoted to real instead of clamped, because clamping to INT32_MIN causes arithmetic overflow bugs + // (example: Undertale's mercymod = -99999999999999 in the Asriel fight) + if (val > INT32_MAX || INT32_MIN > val) { + return (RValue){ .real = (GMLReal) val, .type = RVALUE_REAL, RVALUE_INIT_GMLTYPE(GML_TYPE_DOUBLE) }; + } else { + return (RValue){ .int32 = (int32_t) val, .type = RVALUE_INT32, RVALUE_INIT_GMLTYPE(GML_TYPE_INT32) }; + } +#else + return (RValue){ .int64 = val, .type = RVALUE_INT64, RVALUE_INIT_GMLTYPE(GML_TYPE_INT64) }; +#endif +} + +static RValue RValue_makeBool(bool val) { + return (RValue){ .int32 = val ? 1 : 0, .type = RVALUE_BOOL, RVALUE_INIT_GMLTYPE(GML_TYPE_BOOL) }; +} + +static RValue RValue_makeString(const char* val) { + return (RValue){ .string = val, .type = RVALUE_STRING, .ownsReference = false, RVALUE_INIT_GMLTYPE(GML_TYPE_STRING) }; +} + +static RValue RValue_makeOwnedString(char* val) { + return (RValue){ .string = val, .type = RVALUE_STRING, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_STRING) }; +} + +static RValue RValue_makeUndefined(void) { + return (RValue){ .type = RVALUE_UNDEFINED, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; +} + +// Takes ownership: refCount is NOT bumped (caller hands off its ref). The returned RValue decRefs on free. +// Use this when you have a freshly-allocated array (GMLArray_alloc) or after a GMLArray_incRef. +static RValue RValue_makeArray(GMLArray* arr) { + return (RValue){ .array = arr, .type = RVALUE_ARRAY, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; +} + +// Weak view: does not own (no decRef on free). Callers that stash the value long-term must incRef + set ownsString. +static RValue RValue_makeArrayWeak(GMLArray* arr) { + return (RValue){ .array = arr, .type = RVALUE_ARRAY, .ownsReference = false, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; +} + +#if IS_BC17_OR_HIGHER_ENABLED +// Takes ownership: refCount is NOT bumped (caller hands off its ref). The returned RValue decRefs on free. +static RValue RValue_makeMethod(int32_t codeIndex, int32_t boundInstanceId) { + return (RValue){ .method = GMLMethod_create(codeIndex, boundInstanceId), .type = RVALUE_METHOD, .ownsReference = true, .gmlStackType = GML_TYPE_VARIABLE }; +} + +// Weak view: does not own (no decRef on free). Callers that stash the value long-term must incRef + set ownsString. +static RValue RValue_makeMethodWeak(GMLMethod* m) { + return (RValue){ .method = m, .type = RVALUE_METHOD, .ownsReference = false, .gmlStackType = GML_TYPE_VARIABLE }; +} +#endif + +// Takes ownership: refCount is NOT bumped (caller hands off its ref). The returned RValue decRefs on free. +// Use this for the freshly-allocated struct returned by @@NewGMLObject@@, after the caller has already accounted for both the registry's implicit ref and the returned-RValue ref. +static RValue RValue_makeStruct(Instance* inst) { + return (RValue){ .structInst = inst, .type = RVALUE_STRUCT, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; +} + +// Weak view: does not own (no decRef on free). Callers that stash the value long-term must incRef + set ownsString. +static RValue RValue_makeStructWeak(Instance* inst) { + return (RValue){ .structInst = inst, .type = RVALUE_STRUCT, .ownsReference = false, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; +} + +// Makes "val" independent, that is... +// * For strings, it creates an owned copy of it (copying the original string underneath) +// * For arrays/methods/structs, it increments the reference count +// * For anything else, it does nothing because they don't need to be made independent +// +// Useful to store arbitrary RValues in containers (ds_map, ds_list, etc.) +// The caller's original RValue is unaffected and must still be freed normally. +static RValue RValue_makeIndependent(RValue val) { + if (val.type == RVALUE_STRING && val.string != nullptr) { + return RValue_makeOwnedString(safeStrdup(val.string)); + } else if (val.type == RVALUE_ARRAY && val.array != nullptr) { + GMLArray_incRef(val.array); + val.ownsReference = true; + return val; +#if IS_BC17_OR_HIGHER_ENABLED + } else if (val.type == RVALUE_METHOD && val.method != nullptr) { + GMLMethod_incRef(val.method); + val.ownsReference = true; + return val; +#endif + } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) { + Instance_structIncRef(val.structInst); + val.ownsReference = true; + return val; + } + requireMessageFormatted(!val.ownsReference, "Trying to make independent a RValue (type=%d) that owns a reference, but we don't handle it yet! Did you add a new refcounted value to Butterscotch without implementing RValue_makeIndependent for it?", val.type); + return val; +} + +// Converts an RValue to a heap-allocated string representation. +// The caller must free the returned string +static char* RValue_toString(RValue val) { + char buf[64]; + switch (val.type) { + case RVALUE_REAL: + snprintf(buf, sizeof(buf), "%.16g", (double) val.real); + return safeStrdup(buf); + case RVALUE_INT32: + snprintf(buf, sizeof(buf), "%d", val.int32); + return safeStrdup(buf); +#ifndef NO_RVALUE_INT64 + case RVALUE_INT64: + snprintf(buf, sizeof(buf), "%lld", (long long) val.int64); + return safeStrdup(buf); +#endif + case RVALUE_STRING: + return safeStrdup(val.string != nullptr ? val.string : ""); + case RVALUE_BOOL: + return safeStrdup(val.int32 ? "1" : "0"); + case RVALUE_UNDEFINED: + return safeStrdup("undefined"); + case RVALUE_ARRAY: + snprintf(buf, sizeof(buf), "", (void*) val.array); + return safeStrdup(buf); +#if IS_BC17_OR_HIGHER_ENABLED + case RVALUE_METHOD: + snprintf(buf, sizeof(buf), "", val.method->codeIndex); + return safeStrdup(buf); +#endif + case RVALUE_STRUCT: + snprintf(buf, sizeof(buf), "", val.structInst != nullptr ? Instance_getInstanceId(val.structInst) : 0); + return safeStrdup(buf); + } + return safeStrdup(""); +} + +// Converts an RValue to a heap-allocated string representation, used for debug logs. +// The caller must free the returned string +static char* RValue_toStringFancy(RValue val) { + switch (val.type) { + case RVALUE_STRING: { + char* valueAsString = RValue_toString(val); + + // length + quotes (2) + null terminator + int newLength = strlen(valueAsString) + 3; + char* valueWithQuotes = safeCalloc(newLength, sizeof(char)); + snprintf(valueWithQuotes, newLength, "\"%s\"", valueAsString); + + free(valueAsString); + + return valueWithQuotes; + } + default: { + return RValue_toString(val); + } + } +} + +// Converts an RValue to a heap-allocated string with a type tag prefix, used for trace-stack output. +// Examples: int32(42), real(3.14), "hello", bool(true), undefined, +// The caller must free the returned string +static char* RValue_toStringTyped(RValue val) { + char buf[128]; + switch (val.type) { + case RVALUE_REAL: + snprintf(buf, sizeof(buf), "real(%.16g)", (double) val.real); + return safeStrdup(buf); + case RVALUE_INT32: + snprintf(buf, sizeof(buf), "int32(%d)", val.int32); + return safeStrdup(buf); +#ifndef NO_RVALUE_INT64 + case RVALUE_INT64: + snprintf(buf, sizeof(buf), "int64(%lld)", (long long) val.int64); + return safeStrdup(buf); +#endif + case RVALUE_STRING: { + const char* str = val.string != nullptr ? val.string : ""; + size_t needed = strlen(str) + 3; + char* result = safeCalloc(needed, sizeof(char)); + snprintf(result, needed, "\"%s\"", str); + return result; + } + case RVALUE_BOOL: + return safeStrdup(val.int32 ? "bool(true)" : "bool(false)"); + case RVALUE_UNDEFINED: + return safeStrdup("undefined"); + case RVALUE_ARRAY: + snprintf(buf, sizeof(buf), "", (void*) val.array); + return safeStrdup(buf); +#if IS_BC17_OR_HIGHER_ENABLED + case RVALUE_METHOD: + snprintf(buf, sizeof(buf), "method(code=%d, inst=%d)", val.method->codeIndex, val.method->boundInstanceId); + return safeStrdup(buf); +#endif + case RVALUE_STRUCT: + snprintf(buf, sizeof(buf), "struct(id=%u)", val.structInst != nullptr ? Instance_getInstanceId(val.structInst) : 0); + return safeStrdup(buf); + } + return safeStrdup("???"); +} + +static void RValue_free(RValue* val) { + if (val->type == RVALUE_STRING && val->ownsReference && val->string != nullptr) { + free((void*) val->string); + val->string = nullptr; + val->ownsReference = false; + } else if (val->type == RVALUE_ARRAY && val->ownsReference && val->array != nullptr) { + GMLArray_decRef(val->array); + val->array = nullptr; + val->ownsReference = false; +#if IS_BC17_OR_HIGHER_ENABLED + } else if (val->type == RVALUE_METHOD && val->ownsReference && val->method != nullptr) { + GMLMethod_decRef(val->method); + val->method = nullptr; + val->ownsReference = false; +#endif + } else if (val->type == RVALUE_STRUCT && val->ownsReference && val->structInst != nullptr) { + Instance_structDecRef(val->structInst); + val->structInst = nullptr; + val->ownsReference = false; + } +} + +static GMLReal RValue_toReal(RValue val) { + switch (val.type) { + case RVALUE_REAL: return val.real; + case RVALUE_INT32: return (GMLReal) val.int32; +#ifndef NO_RVALUE_INT64 + case RVALUE_INT64: return (GMLReal) val.int64; +#endif + case RVALUE_BOOL: return (GMLReal) val.int32; + case RVALUE_STRING: return GMLReal_strtod(val.string, nullptr); + case RVALUE_ARRAY: return 0.0; +#if IS_BC17_OR_HIGHER_ENABLED + case RVALUE_METHOD: return 0.0; +#endif + case RVALUE_STRUCT: return val.structInst != nullptr ? (GMLReal) Instance_getInstanceId(val.structInst) : 0.0; + default: return 0.0; + } +} + +static int32_t RValue_toInt32(RValue val) { + switch (val.type) { + case RVALUE_REAL: return (int32_t) val.real; + case RVALUE_INT32: return val.int32; +#ifndef NO_RVALUE_INT64 + case RVALUE_INT64: return (int32_t) val.int64; +#endif + case RVALUE_BOOL: return val.int32; + case RVALUE_STRING: return (int32_t) GMLReal_strtod(val.string, nullptr); + case RVALUE_ARRAY: return 0; +#if IS_BC17_OR_HIGHER_ENABLED + case RVALUE_METHOD: return 0; +#endif + case RVALUE_STRUCT: return val.structInst != nullptr ? (int32_t) Instance_getInstanceId(val.structInst) : 0; + default: return 0; + } +} + +static int64_t RValue_toInt64(RValue val) { + switch (val.type) { + case RVALUE_REAL: return (int64_t) val.real; + case RVALUE_INT32: return (int64_t) val.int32; +#ifndef NO_RVALUE_INT64 + case RVALUE_INT64: return val.int64; +#endif + case RVALUE_BOOL: return (int64_t) val.int32; + case RVALUE_STRING: return (int64_t) GMLReal_strtod(val.string, nullptr); + case RVALUE_ARRAY: return 0; +#if IS_BC17_OR_HIGHER_ENABLED + case RVALUE_METHOD: return 0; +#endif + case RVALUE_STRUCT: return val.structInst != nullptr ? (int64_t) Instance_getInstanceId(val.structInst) : 0; + default: return 0; + } +} + +static bool RValue_toBool(RValue val) { + switch (val.type) { + case RVALUE_REAL: return val.real > 0.5; + case RVALUE_INT32: return val.int32 > 0; +#ifndef NO_RVALUE_INT64 + case RVALUE_INT64: return val.int64 > 0; +#endif + case RVALUE_BOOL: return val.int32 != 0; + case RVALUE_STRING: return val.string != nullptr && val.string[0] != '\0'; + case RVALUE_ARRAY: return false; +#if IS_BC17_OR_HIGHER_ENABLED + case RVALUE_METHOD: return true; +#endif + case RVALUE_STRUCT: return val.structInst != nullptr; + default: return false; + } +} diff --git a/src/text_utils.h b/src/text_utils.h index 8c38eee2..10693ac2 100644 --- a/src/text_utils.h +++ b/src/text_utils.h @@ -1,255 +1,365 @@ -#pragma once - -#include "common.h" -#include -#include -#include -#include -#include "data_win.h" -#include "runner.h" -#include "utils.h" - -// ===[ Text Utility Functions ]=== -// Platform-agnostic text measurement and processing helpers. -// Used by both the renderer (for drawing text) and the VM (for string_width/string_height). - -static inline FontGlyph* TextUtils_findGlyph(Font* font, uint16_t ch) { - // Fast path: ASCII codepoints go through a direct LUT, skipping the linear scan. - if (128 > ch) return font->glyphLUT[ch]; - repeat(font->glyphCount, i) { - if (font->glyphs[i].character == ch) return &font->glyphs[i]; - } - return nullptr; -} - -static inline float TextUtils_getKerningOffset(FontGlyph* glyph, uint16_t nextCh) { - repeat(glyph->kerningCount, k) { - if (glyph->kerning[k].character == (int16_t) nextCh) { - return glyph->kerning[k].shiftModifier; - } - } - return 0; -} - -// Decodes a single UTF-8 codepoint from str at *pos, advances *pos past the consumed bytes. -// Returns the codepoint as uint16_t (sufficient for BMP glyphs). Returns 0xFFFD for invalid sequences. -static inline uint16_t TextUtils_decodeUtf8(const char* str, int32_t len, int32_t* pos) { - uint8_t b = (uint8_t) str[*pos]; - if (128 > b) { - // ASCII (0xxxxxxx) - (*pos)++; - return b; - } else if ((b & 0xE0) == 0xC0) { - // 2-byte sequence (110xxxxx 10xxxxxx) - if (len > *pos + 1 && ((uint8_t) str[*pos + 1] & 0xC0) == 0x80) { - uint16_t cp = ((b & 0x1F) << 6) | ((uint8_t) str[*pos + 1] & 0x3F); - *pos += 2; - return cp; - } - } else if ((b & 0xF0) == 0xE0) { - // 3-byte sequence (1110xxxx 10xxxxxx 10xxxxxx) - if (len > *pos + 2 && ((uint8_t) str[*pos + 1] & 0xC0) == 0x80 && ((uint8_t) str[*pos + 2] & 0xC0) == 0x80) { - uint16_t cp = ((b & 0x0F) << 12) | (((uint8_t) str[*pos + 1] & 0x3F) << 6) | ((uint8_t) str[*pos + 2] & 0x3F); - *pos += 3; - return cp; - } - } else if ((b & 0xF8) == 0xF0) { - // 4-byte sequence (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx) - truncated to uint16_t - if (len > *pos + 3 && ((uint8_t) str[*pos + 1] & 0xC0) == 0x80 && ((uint8_t) str[*pos + 2] & 0xC0) == 0x80 && ((uint8_t) str[*pos + 3] & 0xC0) == 0x80) { - *pos += 4; - return 0xFFFD; // Beyond BMP, return replacement character - } - } - // Invalid or truncated sequence - (*pos)++; - return 0xFFFD; -} - -static inline int32_t TextUtils_utf8AdvanceCodepoints(const char* str, int32_t byteLen, int32_t codepointsToSkip) { - int32_t pos = 0; - while (pos < byteLen && codepointsToSkip > 0) { - pos++; - while (pos < byteLen && ((uint8_t)str[pos] & 0xC0) == 0x80) { - pos++; - } - codepointsToSkip--; - } - return pos; -} - -static inline int32_t TextUtils_utf8CodepointCount(const char* str, int32_t byteLen) { - int32_t count = 0; - for (int32_t i = 0; i < byteLen; i++) { - if (((uint8_t)str[i] & 0xC0) != 0x80) { - count++; - } - } - return count; -} - -static inline int32_t TextUtils_utf8EncodeCodepoint(uint32_t cp, char* out) { - if (cp <= 0x7FU) { - out[0] = (char) cp; - return 1; - } - if (cp <= 0x7FFU) { - out[0] = (char) (0xC0U | (cp >> 6)); - out[1] = (char) (0x80U | (cp & 0x3FU)); - return 2; - } - if (cp <= 0xFFFFU) { - out[0] = (char) (0xE0U | (cp >> 12)); - out[1] = (char) (0x80U | ((cp >> 6) & 0x3FU)); - out[2] = (char) (0x80U | (cp & 0x3FU)); - return 3; - } - if (cp <= 0x10FFFFU) { - out[0] = (char) (0xF0U | (cp >> 18)); - out[1] = (char) (0x80U | ((cp >> 12) & 0x3FU)); - out[2] = (char) (0x80U | ((cp >> 6) & 0x3FU)); - out[3] = (char) (0x80U | (cp & 0x3FU)); - return 4; - } - return 0; -} - -// Line stride used for multi-line text. Matches HTML5 runner behavior: -// - When `linesep` is not provided to draw_text, it defaults to `font.TextHeight('M')` -// which is `max_glyph_height * scaleY`. We apply scaleY via the transform matrix already, -// so we return the raw max glyph height here. -// - Falls back to emSize only if the font has no glyphs recorded. -static inline float TextUtils_lineStride(Font* font) { - if (font->maxGlyphHeight > 0) return (float) font->maxGlyphHeight; - return font->emSize; -} - -static inline float TextUtils_measureLineWidth(Font* font, const char* line, int32_t len) { - float width = 0; - int32_t pos = 0; - uint16_t ch = 0; - bool hasCh = false; - if (len > pos) { - ch = TextUtils_decodeUtf8(line, len, &pos); - hasCh = true; - } - - while (hasCh) { - FontGlyph* glyph = TextUtils_findGlyph(font, ch); - - // Decode the next codepoint once - reused for kerning AND as next iteration's ch - uint16_t nextCh = 0; - bool hasNext = len > pos; - if (hasNext) nextCh = TextUtils_decodeUtf8(line, len, &pos); - - if (glyph != nullptr) { - width += glyph->shift; - if (hasNext) width += TextUtils_getKerningOffset(glyph, nextCh); - } - - ch = nextCh; - hasCh = hasNext; - } - return width; -} - -// Result of GML text preprocessing. If owning is true, the caller must free the text pointer. -typedef struct { - const char* text; - bool owning; -} PreprocessedText; - -// Frees the text pointer if it is owning. -static inline void PreprocessedText_free(PreprocessedText pt) { - if (pt.owning) free((char*) pt.text); -} - -// Preprocesses GML text: converts unescaped # to \n, and \# to literal #. -// Uses a fused single-pass approach: scans for # and only allocates if one is found. -static inline PreprocessedText TextUtils_preprocessGmlText(const char* text) { - int32_t len = (int32_t) strlen(text); - - // Scan until we find a # - for (int32_t i = 0; len > i; i++) { - if (text[i] == '#') { - // Found one - allocate and process from here - char* result = safeMalloc(len + 1); - memcpy(result, text, i); - int32_t out = i; - - // Check if the # is escaped (\#) - if (out > 0 && result[out - 1] == '\\') { - result[out - 1] = '#'; - } else { - result[out++] = '\n'; - } - - // Process the rest of the string - for (int32_t j = i + 1; len > j; j++) { - if (text[j] == '#') { - if (out > 0 && result[out - 1] == '\\') { - result[out - 1] = '#'; - } else { - result[out++] = '\n'; - } - } else { - result[out++] = text[j]; - } - } - result[out] = '\0'; - return (PreprocessedText){ .text = result, .owning = true }; - } - } - - // No # found, return original pointer without allocating - return (PreprocessedText){ .text = text, .owning = false }; -} - -// Preprocess GML text ONLY if the runner is not GameMaker: Studio 2 -static inline PreprocessedText TextUtils_preprocessGmlTextIfNeeded(Runner* runner, const char* text) { - if (DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0)) - return (PreprocessedText){ .text = text, .owning = false }; - return TextUtils_preprocessGmlText(text); -} - -// Returns true if c is \r or \n -static inline bool TextUtils_isNewlineChar(char c) { - return c == '\n' || c == '\r'; -} - -// Returns true if c is ' ' or \t -static inline bool TextUtils_isWhitespaceChar(char c) { - return c == ' ' || c == '\t'; -} - -// Counts the number of lines in preprocessed text, treating \r\n and \n\r as single breaks -static inline int32_t TextUtils_countLines(const char* text, int32_t len) { - int32_t count = 1; - for (int32_t i = 0; len > i; i++) { - if (TextUtils_isNewlineChar(text[i])) { - count++; - // Treat \r\n or \n\r as a single line break - if (len > i + 1 && TextUtils_isNewlineChar(text[i + 1]) && text[i] != text[i + 1]) { - i++; - } - } - } - return count; -} - -// Advances lineStart past the newline at lineEnd, treating \r\n and \n\r as single breaks -static inline int32_t TextUtils_skipNewline(const char* text, int32_t lineEnd, int32_t textLen) { - int32_t next = lineEnd + 1; - if (textLen > next && TextUtils_isNewlineChar(text[next]) && text[lineEnd] != text[next]) { - next++; - } - return next; -} - -static char* TextUtils_trimTrailingWhitespace(char* str) { - size_t len = strlen(str); - while (len > 0 && (TextUtils_isWhitespaceChar(str[len - 1]) || TextUtils_isNewlineChar(str[len - 1]))) { - len--; - } - str[len] = '\0'; - return str; -} \ No newline at end of file +#pragma once + +#include "common.h" +#include +#include +#include +#include +#include "data_win.h" +#include "runner.h" +#include "utils.h" + +// ===[ Text Utility Functions ]=== +// Platform-agnostic text measurement and processing helpers. +// Used by both the renderer (for drawing text) and the VM (for string_width/string_height). + +static inline FontGlyph* TextUtils_findGlyph(Font* font, uint16_t ch) { + // Fast path: ASCII codepoints go through a direct LUT, skipping the linear scan. + if (128 > ch) return font->glyphLUT[ch]; + repeat(font->glyphCount, i) { + if (font->glyphs[i].character == ch) return &font->glyphs[i]; + } + return nullptr; +} + +static inline float TextUtils_getKerningOffset(FontGlyph* glyph, uint16_t nextCh) { + repeat(glyph->kerningCount, k) { + if (glyph->kerning[k].character == (int16_t) nextCh) { + return glyph->kerning[k].shiftModifier; + } + } + return 0; +} + +// Decodes a single UTF-8 codepoint from str at *pos, advances *pos past the consumed bytes. + +static inline uint16_t TextUtils_decodeUtf8(const char* str, int32_t len, int32_t* pos) { + uint8_t b = (uint8_t) str[*pos]; + if (128 > b) { + // ASCII (0xxxxxxx) + (*pos)++; + return b; + } else if ((b & 0xE0) == 0xC0) { + // 2-byte sequence (110xxxxx 10xxxxxx) + if (len > *pos + 1 && ((uint8_t) str[*pos + 1] & 0xC0) == 0x80) { + uint16_t cp = ((b & 0x1F) << 6) | ((uint8_t) str[*pos + 1] & 0x3F); + *pos += 2; + return cp; + } + } else if ((b & 0xF0) == 0xE0) { + // 3-byte sequence (1110xxxx 10xxxxxx 10xxxxxx) + if (len > *pos + 2 && ((uint8_t) str[*pos + 1] & 0xC0) == 0x80 && ((uint8_t) str[*pos + 2] & 0xC0) == 0x80) { + uint16_t cp = ((b & 0x0F) << 12) | (((uint8_t) str[*pos + 1] & 0x3F) << 6) | ((uint8_t) str[*pos + 2] & 0x3F); + *pos += 3; + return cp; + } + } else if ((b & 0xF8) == 0xF0) { + // 4-byte sequence (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx) - truncated to uint16_t + if (len > *pos + 3 && ((uint8_t) str[*pos + 1] & 0xC0) == 0x80 && ((uint8_t) str[*pos + 2] & 0xC0) == 0x80 && ((uint8_t) str[*pos + 3] & 0xC0) == 0x80) { + *pos += 4; + return 0xFFFD; // Beyond BMP, return replacement character + } + } + + + (*pos)++; + return (uint16_t) b; +} + + +static inline int32_t TextUtils_utf8AdvanceCodepoints(const char* str, int32_t byteLen, int32_t codepointsToSkip) { + int32_t pos = 0; + while (pos < byteLen && codepointsToSkip > 0) { + pos++; + while (pos < byteLen && ((uint8_t)str[pos] & 0xC0) == 0x80) { + pos++; + } + codepointsToSkip--; + } + return pos; +} + + +static inline int32_t TextUtils_utf8ByteOffsetFromGmlIndex(const char* str, int32_t byteLen, int32_t gmlIndex) { + int32_t toSkip = gmlIndex - 1; + if (toSkip < 0) toSkip = 0; + int32_t offset = TextUtils_utf8AdvanceCodepoints(str, byteLen, toSkip); + if (offset > byteLen) offset = byteLen; + return offset; +} + +static inline int32_t TextUtils_utf8CodepointCount(const char* str, int32_t byteLen) { + int32_t count = 0; + for (int32_t i = 0; i < byteLen; i++) { + if (((uint8_t)str[i] & 0xC0) != 0x80) { + count++; + } + } + return count; +} + +static inline int32_t TextUtils_utf8EncodeCodepoint(uint32_t cp, char* out) { + if (cp <= 0x7FU) { + out[0] = (char) cp; + return 1; + } + if (cp <= 0x7FFU) { + out[0] = (char) (0xC0U | (cp >> 6)); + out[1] = (char) (0x80U | (cp & 0x3FU)); + return 2; + } + if (cp <= 0xFFFFU) { + out[0] = (char) (0xE0U | (cp >> 12)); + out[1] = (char) (0x80U | ((cp >> 6) & 0x3FU)); + out[2] = (char) (0x80U | (cp & 0x3FU)); + return 3; + } + if (cp <= 0x10FFFFU) { + out[0] = (char) (0xF0U | (cp >> 18)); + out[1] = (char) (0x80U | ((cp >> 12) & 0x3FU)); + out[2] = (char) (0x80U | ((cp >> 6) & 0x3FU)); + out[3] = (char) (0x80U | (cp & 0x3FU)); + return 4; + } + return 0; +} + +// Line stride used for multi-line text. Matches HTML5 runner behavior: +// - When `linesep` is not provided to draw_text, it defaults to `font.TextHeight('M')` +// which is `max_glyph_height * scaleY`. We apply scaleY via the transform matrix already, +// so we return the raw max glyph height here. +// - Falls back to emSize only if the font has no glyphs recorded. +static inline float TextUtils_lineStride(Font* font) { + if (font->maxGlyphHeight > 0) return (float) font->maxGlyphHeight; + return font->emSize; +} + +static inline float TextUtils_measureLineWidth(Font* font, const char* line, int32_t len) { + float width = 0; + int32_t pos = 0; + uint16_t ch = 0; + bool hasCh = false; + if (len > pos) { + ch = TextUtils_decodeUtf8(line, len, &pos); + hasCh = true; + } + + while (hasCh) { + FontGlyph* glyph = TextUtils_findGlyph(font, ch); + + // Decode the next codepoint once - reused for kerning AND as next iteration's ch + uint16_t nextCh = 0; + bool hasNext = len > pos; + if (hasNext) nextCh = TextUtils_decodeUtf8(line, len, &pos); + + if (glyph != nullptr) { + width += glyph->shift; + if (hasNext) width += TextUtils_getKerningOffset(glyph, nextCh); + } + + ch = nextCh; + hasCh = hasNext; + } + return width; +} + +// Result of GML text preprocessing. If owning is true, the caller must free the text pointer. +typedef struct { + const char* text; + bool owning; +} PreprocessedText; + +// Frees the text pointer if it is owning. +static inline void PreprocessedText_free(PreprocessedText pt) { + if (pt.owning) free((char*) pt.text); +} + +static inline bool TextUtils_isDeltaruneInlineTextCommand(char c) { + switch (c) { + case 'C': // color + case 'E': // expression/portrait + case 'F': // face/font textbox metadata + case 'M': // voice/sound metadata + case 'S': // speed/sound metadata + case 'T': // typer/textbox metadata + case 'V': // variable/textbox metadata + case 'W': // wait metadata + case 'X': // textbox metadata + case 'Y': // textbox metadata + return true; + default: + return false; + } +} + +static inline bool TextUtils_isDeltaruneInlineTextCommandParam(char c) { + uint8_t b = (uint8_t) c; + return (b >= '0' && b <= '9') || + (b >= 'A' && b <= 'Z') || + (b >= 'a' && b <= 'z') || + b >= 0x80; +} + +static inline bool TextUtils_trySkipDeltaruneInlineTextCommand(const char* text, int32_t len, int32_t i, int32_t* outNext) { + if (text[i] != '\\' || i + 1 >= len) return false; + + char command = text[i + 1]; + if (!TextUtils_isDeltaruneInlineTextCommand(command)) return false; + + int32_t next = i + 2; + if (next < len && TextUtils_isDeltaruneInlineTextCommandParam(text[next])) { + next++; + } + + *outNext = next; + return true; +} + +static inline bool TextUtils_trySkipDeltaruneLeakedTextboxPrefix(const char* text, int32_t len, int32_t i, int32_t* outNext) { + if (i != 0 || len < 3 || text[0] != 'E' || text[2] != '*') return false; + if (!TextUtils_isDeltaruneInlineTextCommandParam(text[1])) return false; + + *outNext = 2; + return true; +} + +static inline bool TextUtils_hasDeltaruneLeakedTextboxPrefix(const char* text, int32_t len) { + int32_t next = 0; + return TextUtils_trySkipDeltaruneLeakedTextboxPrefix(text, len, 0, &next); +} + +static inline bool TextUtils_trySkipDeltaruneTextMarkup(const char* text, int32_t len, int32_t i, int32_t* outNext) { + if (TextUtils_trySkipDeltaruneInlineTextCommand(text, len, i, outNext)) return true; + if (TextUtils_trySkipDeltaruneLeakedTextboxPrefix(text, len, i, outNext)) return true; + return false; +} + +static inline PreprocessedText TextUtils_preprocessGmlDrawText(const char* text, bool convertHashNewlines) { + int32_t len = (int32_t) strlen(text); + bool needsProcessing = false; + + for (int32_t i = 0; i < len; i++) { + int32_t next = i; + if (TextUtils_trySkipDeltaruneTextMarkup(text, len, i, &next)) { + needsProcessing = true; + i = next - 1; + continue; + } + + if (convertHashNewlines && text[i] == '#') { + needsProcessing = true; + } + } + + if (!needsProcessing) { + return (PreprocessedText){ .text = text, .owning = false }; + } + + char* result = safeMalloc(len + 1); + int32_t out = 0; + + for (int32_t i = 0; i < len; i++) { + int32_t next = i; + if (TextUtils_trySkipDeltaruneTextMarkup(text, len, i, &next)) { + i = next - 1; + continue; + } + + if (convertHashNewlines && text[i] == '#') { + if (out > 0 && result[out - 1] == '\\') { + result[out - 1] = '#'; + } else { + result[out++] = '\n'; + } + } else { + result[out++] = text[i]; + } + } + + result[out] = '\0'; + return (PreprocessedText){ .text = result, .owning = true }; +} + +// Preprocesses GML text: converts unescaped # to \n, and \# to literal #. +// Uses a fused single-pass approach: scans for # and only allocates if one is found. +static inline PreprocessedText TextUtils_preprocessGmlText(const char* text) { + int32_t len = (int32_t) strlen(text); + for (int32_t i = 0; len > i; i++) { + if (text[i] == '#') { + char* result = safeMalloc(len + 1); + memcpy(result, text, i); + int32_t out = i; + + if (out > 0 && result[out - 1] == '\\') { + result[out - 1] = '#'; + } else { + result[out++] = '\n'; + } + + for (int32_t j = i + 1; len > j; j++) { + if (text[j] == '#') { + if (out > 0 && result[out - 1] == '\\') { + result[out - 1] = '#'; + } else { + result[out++] = '\n'; + } + } else { + result[out++] = text[j]; + } + } + result[out] = '\0'; + return (PreprocessedText){ .text = result, .owning = true }; + } + } + + // No # found, return original pointer without allocating + return (PreprocessedText){ .text = text, .owning = false }; +} + +// Preprocess GML text ONLY if the runner is not GameMaker: Studio 2 +static inline PreprocessedText TextUtils_preprocessGmlTextIfNeeded(Runner* runner, const char* text) { + bool convertHashNewlines = !DataWin_isVersionAtLeast(runner->dataWin, 2, 0, 0, 0); + return TextUtils_preprocessGmlDrawText(text, convertHashNewlines); +} + +// Returns true if c is \r or \n +static inline bool TextUtils_isNewlineChar(char c) { + return c == '\n' || c == '\r'; +} + +// Returns true if c is ' ' or \t +static inline bool TextUtils_isWhitespaceChar(char c) { + return c == ' ' || c == '\t'; +} + +// Counts the number of lines in preprocessed text, treating \r\n and \n\r as single breaks +static inline int32_t TextUtils_countLines(const char* text, int32_t len) { + int32_t count = 1; + for (int32_t i = 0; len > i; i++) { + if (TextUtils_isNewlineChar(text[i])) { + count++; + // Treat \r\n or \n\r as a single line break + if (len > i + 1 && TextUtils_isNewlineChar(text[i + 1]) && text[i] != text[i + 1]) { + i++; + } + } + } + return count; +} + +static inline int32_t TextUtils_skipNewline(const char* text, int32_t lineEnd, int32_t textLen) { + int32_t next = lineEnd + 1; + if (textLen > next && TextUtils_isNewlineChar(text[next]) && text[lineEnd] != text[next]) { + next++; + } + return next; +} + +static char* TextUtils_trimTrailingWhitespace(char* str) { + size_t len = strlen(str); + while (len > 0 && (TextUtils_isWhitespaceChar(str[len - 1]) || TextUtils_isNewlineChar(str[len - 1]))) { + len--; + } + str[len] = '\0'; + return str; +} diff --git a/src/utils.h b/src/utils.h index e4d59824..326ddf47 100644 --- a/src/utils.h +++ b/src/utils.h @@ -9,16 +9,16 @@ #include "real_type.h" #define forEach(type, item, array, count) \ - for (typeof(count) item##_i_ = 0; item##_i_ < (count); item##_i_++) \ + for (size_t item##_i_ = 0; item##_i_ < (count); item##_i_++) \ for (type* item = &(array)[item##_i_]; item; item = NULL) #define forEachIndexed(type, item, index, array, count) \ - for (typeof(count) index = 0; index < (count); index++) \ + for (size_t index = 0; index < (count); index++) \ for (type* item = &(array)[index]; item; item = NULL) // The "typeof((typeof(n))0" is used to remove the "const" from the typeof -#define repeat(n, it) for (typeof((typeof(n))0) it = 0; it < (n); it++) +#define repeat(n, it) for (size_t it = 0; it < n; it++) #define require(condition) \ do { \ diff --git a/src/vm.c b/src/vm.c index 3a27ed6f..0c13f69a 100644 --- a/src/vm.c +++ b/src/vm.c @@ -1,4203 +1,4536 @@ -#include "vm.h" -#include "vm_builtins.h" -#include "instance.h" -#include "runner.h" -#include "binary_utils.h" -#include "utils.h" -#include "bytecode_versions.h" -#include "profiler.h" -#include "string_builder.h" - -#include -#include -#include -#include - -#include "stb_ds.h" - -// Maximum number of local variables per code entry (stack-allocated arrays in VM_executeCode/VM_callCodeIndex) -#define MAX_CODE_LOCALS 128 - -// ===[ Stack Operations ]=== - -#ifdef ENABLE_VM_TRACING -static bool shouldTraceStack(VMContext* ctx) { - if (shlen(ctx->stackToBeTraced) == 0) return false; - if (ctx->traceBytecodeAfterFrame > ctx->runner->frameCount) return false; - return shgeti(ctx->stackToBeTraced, "*") != -1 || shgeti(ctx->stackToBeTraced, ctx->currentCodeName) != -1; -} - -// Returns a heap-allocated "[elem0, elem1, ..., elemN]" string for the current stack contents (bottom -> top). Caller frees. -static char* formatStackContents(VMContext* ctx) { - StringBuilder sb = StringBuilder_create(256); - StringBuilder_appendChar(&sb, '['); - repeat(ctx->stack.top, si) { - char* typed = RValue_toStringTyped(ctx->stack.slots[si]); - if (si > 0) StringBuilder_append(&sb, ", "); - StringBuilder_append(&sb, typed); - free(typed); - } - StringBuilder_appendChar(&sb, ']'); - char* result = StringBuilder_toString(&sb); - StringBuilder_free(&sb); - return result; -} -#endif - -#if IS_BC17_OR_HIGHER_ENABLED -// Returns the native byte size of a GML data type on the runner's stack. -// This is needed because the Dup instruction encodes byte counts, not slot counts. -// Only used by BC17+ Dup paths; BC16 Dup decodes the operand as a slot count directly. -static int gmlTypeNativeSize(uint8_t gmlType) { - switch (gmlType) { - case GML_TYPE_DOUBLE: return 8; - case GML_TYPE_INT32: return 4; - case GML_TYPE_INT64: return 8; - case GML_TYPE_BOOL: return 4; - case GML_TYPE_VARIABLE: return 16; - case GML_TYPE_STRING: return 4; - case GML_TYPE_INT16: return 4; - default: return 16; - } -} -#endif - -static void stackPush(VMContext* ctx, RValue val) { - require(VM_STACK_SIZE > ctx->stack.top); -#ifdef ENABLE_VM_TRACING - if (shouldTraceStack(ctx)) { - char* valStr = RValue_toStringTyped(val); - ctx->stack.slots[ctx->stack.top++] = val; - char* stackBuf = formatStackContents(ctx); - fprintf(stderr, "VM: [%s] PUSH %s [stack=%d -> %d] %s\n", ctx->currentCodeName, valStr, ctx->stack.top - 1, ctx->stack.top, stackBuf); - free(stackBuf); - free(valStr); - return; - } -#endif - ctx->stack.slots[ctx->stack.top++] = val; -} - -#if IS_BC17_OR_HIGHER_ENABLED -static void stackPushTyped(VMContext* ctx, RValue val, uint8_t gmlStackType) { - if (IS_BC17_OR_HIGHER(ctx)) { - val.gmlStackType = gmlStackType; - } - stackPush(ctx, val); -} -#else -// BC16-only builds don't carry per-slot GML stack type, so this is just a plain push. -// Defined as a macro so the gmlStackType argument (often `instrType2(instr)`) is never computed at call sites. -#define stackPushTyped(ctx, val, gmlStackType) stackPush((ctx), (val)) -#endif - -static RValue stackPop(VMContext* ctx) { - require(ctx->stack.top > 0); - RValue val = ctx->stack.slots[--ctx->stack.top]; -#ifdef ENABLE_VM_TRACING - if (shouldTraceStack(ctx)) { - char* valStr = RValue_toStringTyped(val); - char* stackBuf = formatStackContents(ctx); - fprintf(stderr, "VM: [%s] POP %s [stack=%d -> %d] %s\n", ctx->currentCodeName, valStr, ctx->stack.top + 1, ctx->stack.top, stackBuf); - free(stackBuf); - free(valStr); - } -#endif - return val; -} - -// Helper function that calls stackPop and returns the result as an int32_t -static int32_t stackPopInt32(VMContext* ctx) { - RValue rvalue = stackPop(ctx); - int32_t value = RValue_toInt32(rvalue); - RValue_free(&rvalue); - return value; -} - -static RValue* stackPeek(VMContext* ctx) { - require(ctx->stack.top > 0); - return &ctx->stack.slots[ctx->stack.top - 1]; -} - -// ===[ Instruction Decoding ]=== - -static uint8_t instrOpcode(uint32_t instr) { - return (instr >> 24) & 0xFF; -} - -static uint8_t instrType1(uint32_t instr) { - return (instr >> 16) & 0xF; -} - -static uint8_t instrType2(uint32_t instr) { - return (instr >> 20) & 0xF; -} - -static int16_t instrInstanceType(uint32_t instr) { - return (int16_t) (instr & 0xFFFF); -} - -static uint8_t instrCmpKind(uint32_t instr) { - return (instr >> 8) & 0xFF; -} - -static bool instrHasExtraData(uint32_t instr) { - return (instr & 0x40000000) != 0; -} - -// Jump offset for branch instructions: sign-extend 23 bits, multiply by 4 -static int32_t instrJumpOffset(uint32_t instr) { - return ((int32_t) (instr << 9)) >> 7; -} - -static uint32_t extraDataSize(uint8_t type1) { - switch (type1) { - case GML_TYPE_DOUBLE: return 8; - case GML_TYPE_INT64: return 8; - case GML_TYPE_FLOAT: return 4; - case GML_TYPE_INT32: return 4; - case GML_TYPE_BOOL: return 4; - case GML_TYPE_VARIABLE: return 4; - case GML_TYPE_STRING: return 4; - case GML_TYPE_INT16: return 0; - default: return 0; - } -} - -// ===[ Reference Chain Resolution ]=== - -// Walks reference chains from the bytecode buffer and builds hash maps -// mapping absolute file offsets to resolved operand values. -// The bytecode buffer stays completely read-only. -// Patches bytecode operands in-place so that variable/function reference chain deltas -// are replaced with resolved indices. This avoids needing hash map lookups at runtime. -static void patchReferenceOperands(VMContext* ctx) { - DataWin* dataWin = ctx->dataWin; - uint8_t* buf = dataWin->bytecodeBuffer; - size_t base = dataWin->bytecodeBufferBase; - - // Patch variable operands: replace delta with varIdx (preserving upper 5 bits) - repeat(dataWin->vari.variableCount, varIdx) { - Variable* v = &dataWin->vari.variables[varIdx]; - if (v->occurrences == 0) continue; - - uint32_t addr = v->firstAddress; - repeat(v->occurrences, occ) { - uint32_t operandAddr = addr + 4; - uint32_t operand = BinaryUtils_readUint32(&buf[operandAddr - base]); - uint32_t delta = operand & 0x07FFFFFF; - uint32_t upperBits = operand & 0xF8000000; - - // Patch in-place: upper bits preserved, lower 27 = varIdx - BinaryUtils_writeUint32(&buf[operandAddr - base], upperBits | (varIdx & 0x07FFFFFF)); - - if (v->occurrences > occ + 1) { - addr += delta; - } - } - } - - // Patch function operands: replace delta with funcIdx - repeat(dataWin->func.functionCount, funcIdx) { - Function* f = &dataWin->func.functions[funcIdx]; - if (f->occurrences == 0) continue; - - uint32_t addr = f->firstAddress; - repeat(f->occurrences, occ) { - uint32_t operandAddr = addr + 4; - uint32_t operand = BinaryUtils_readUint32(&buf[operandAddr - base]); - uint32_t delta = operand & 0x07FFFFFF; - - // Patch in-place: store funcIdx directly - BinaryUtils_writeUint32(&buf[operandAddr - base], funcIdx); - - if (f->occurrences > occ + 1) { - addr += delta; - } - } - } -} - -// Resolve a variable operand: returns upper bits | varIndex (read directly from patched bytecode) -static uint32_t resolveVarOperand(const uint8_t* extraData) { - return BinaryUtils_readUint32Aligned(extraData); -} - -// Resolve a function operand: returns funcIndex (read directly from patched bytecode) -static uint32_t resolveFuncOperand(const uint8_t* extraData) { - return BinaryUtils_readUint32Aligned(extraData); -} - -// ===[ Array Operations ]=== -// -// All arrays live as RVALUE_ARRAY (GMLArray*) inside a scalar variable slot (self vars, global vars, or local vars). -// Variable reads return the RValue (which may be an array pointer) and variable writes update the slot directly. -// -// Reads return a weak view of the slot value - callers must incRef + set ownsReference if they want to retain it. -// -// Writes (VARTYPE_ARRAY Pop, BREAK_POPAF, BREAK_PUSHAC materialisation) go through VM_arrayWriteAt, -// which handles: -// * slot-not-yet-an-array -> allocate a fresh GMLArray -// * CoW fork when another scope/slot owns the array (BC16 predicate uses the slot address; BC17+ predicate compares against ctx->currentArrayOwner set by BREAK_SETOWNER) -// * grow-on-write past the current length -// * transfer ownership of "val" into arr->data[index], freeing whatever was there before. -// -// Forward declarations -static Instance* findInstanceByTarget(VMContext* ctx, int32_t target); - -// Read array[index]. Returns RVALUE_UNDEFINED when slot is not an array or when index is out of bounds. -// The returned RValue is a weak view, callers that stash it must strengthen (incRef, strdup). -static RValue VM_arrayReadAt(RValue* slot, int32_t index) { - if (slot == nullptr || slot->type != RVALUE_ARRAY || slot->array == nullptr) { - return (RValue){ .type = RVALUE_UNDEFINED }; - } - RValue* cell = GMLArray_slot(slot->array, index); - if (cell == nullptr) { - return (RValue){ .type = RVALUE_UNDEFINED }; - } - RValue result = *cell; - result.ownsReference = false; - return result; -} - -// Copies "val" into *slot: dup string buffers, incRef arrays. Caller retains "val". -static void storeIntoArraySlot(RValue* slot, RValue val) { - // Free whatever was there (decRefs owned arrays, frees owned strings). - RValue_free(slot); - if (val.type == RVALUE_STRING && val.string != nullptr) { - *slot = RValue_makeOwnedString(safeStrdup(val.string)); - } else if (val.type == RVALUE_ARRAY && val.array != nullptr) { - GMLArray_incRef(val.array); - val.ownsReference = true; - *slot = val; -#if IS_BC17_OR_HIGHER_ENABLED - } else if (val.type == RVALUE_METHOD && val.method != nullptr) { - GMLMethod_incRef(val.method); - val.ownsReference = true; - *slot = val; -#endif - } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) { - Instance_structIncRef(val.structInst); - val.ownsReference = true; - *slot = val; - } else { - val.ownsReference = false; - *slot = val; - } -} - -// Write array[index] = val with CoW semantics. Always makes an independent copy of val, caller retains ownership and must RValue_free(&val) when done. -// `slot` is the RValue* holding the array (e.g. &globalVars[id], &inst->selfVars[..].value, &localVars[slot]). -// Returns the (possibly newly-forked) GMLArray* now in *slot. -static GMLArray* VM_arrayWriteAt(VMContext* ctx, RValue* slot, int32_t index, RValue val) { - require(slot != nullptr); - requireMessageFormatted(index >= 0, "Trying to write to an array using a negative index! Index: %d", index); - - void* intendedOwner; -#if IS_BC17_OR_HIGHER_ENABLED - intendedOwner = IS_BC17_OR_HIGHER(ctx) ? ctx->currentArrayOwner : (void*) slot; -#else - intendedOwner = (void*) slot; -#endif - - // Case 1: slot doesn't hold an array yet, replace whatever's there with a fresh one. - if (slot->type != RVALUE_ARRAY || slot->array == nullptr) { - RValue_free(slot); - GMLArray* fresh = GMLArray_create(0); - fresh->owner = intendedOwner; - *slot = RValue_makeArray(fresh); - GMLArray_growTo(fresh, index + 1); - storeIntoArraySlot(GMLArray_slot(fresh, index), val); - return fresh; - } - - GMLArray* arr = slot->array; - - // Case 2: CoW fork check. - bool needFork; -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx)) { - needFork = (arr->owner != ctx->currentArrayOwner); - } else -#endif - { - needFork = (arr->refCount > 1 && arr->owner != (void*) slot); - } - if (needFork) { - GMLArray* clone = GMLArray_clone(arr, intendedOwner); - GMLArray_decRef(arr); - slot->array = clone; - slot->ownsReference = true; - arr = clone; - } else if (arr->owner == nullptr) { - // Claim ownership on first write to an unowned array (e.g. freshly allocated by a builtin). - arr->owner = intendedOwner; - } - - // Case 3: grow if needed, then write. - GMLArray_growTo(arr, index + 1); - storeIntoArraySlot(GMLArray_slot(arr, index), val); - return arr; -} - -// Public entry point for builtins that materialise an array and return it (layer_get_all). -// Returned RValue holds one strong ref, caller is expected to consume it (stack push / variable write). -// Owner is left null, the first write through a variable slot will claim it. -RValue VM_createArray(MAYBE_UNUSED VMContext* ctx) { - GMLArray* arr = GMLArray_create(0); - return RValue_makeArray(arr); -} - -// Public helper for builtins that populate an array being returned. Copies val, caller retains ownership. -// The arrayRef must be an RVALUE_ARRAY (as returned by VM_createArray). No CoW fork, the returning array has refCount=1 and no scope owner yet, so we write in place. -void VM_arraySet(MAYBE_UNUSED VMContext* ctx, RValue* arrayRef, int32_t index, RValue val) { - require(arrayRef != nullptr && arrayRef->type == RVALUE_ARRAY && arrayRef->array != nullptr); - GMLArray* arr = arrayRef->array; - GMLArray_growTo(arr, index + 1); - storeIntoArraySlot(GMLArray_slot(arr, index), val); -} - -// ===[ Trace Helpers ]=== - -#ifdef ENABLE_VM_TRACING -/** - * @brief Checks if a variable access should be traced. - * - * Matches the trace map entries in order: wildcard "*", bare scope name (e.g. "obj_player" or "global"), - * alternate scope name (e.g. "self" for any instance), or qualified "scope.var" format - * (e.g. "obj_player.x", "global.hp", "self.x"). Short-circuits before formatting - * the qualified name when possible. - * - * @param traceMap The string-boolean hash map of trace filters (from --trace-variable-reads/writes). - * @param scopeName The scope of the variable: an object name (e.g. "obj_player") or "global". - * @param altScopeName An alternate scope name to also match (e.g. "self" for instance variables), or nullptr. - * @param varName The variable name being accessed (e.g. "x"). - * @return true if the access matches a trace filter and should be logged. - */ -static bool shouldTraceVariable(StringBooleanEntry* traceMap, const char* scopeName, const char* altScopeName, const char* varName) { - if (shlen(traceMap) == 0) return false; - if (shgeti(traceMap, "*") != -1) return true; - if (shgeti(traceMap, scopeName) != -1) return true; - if (altScopeName != nullptr && shgeti(traceMap, altScopeName) != -1) return true; - char formatted[strlen(scopeName) + 1 + strlen(varName) + 1]; - snprintf(formatted, sizeof(formatted), "%s.%s", scopeName, varName); - if (shgeti(traceMap, formatted) != -1) return true; - if (altScopeName != nullptr) { - char altFormatted[strlen(altScopeName) + 1 + strlen(varName) + 1]; - snprintf(altFormatted, sizeof(altFormatted), "%s.%s", altScopeName, varName); - if (shgeti(traceMap, altFormatted) != -1) return true; - } - return false; -} -#endif - -// ===[ Array Access Helpers ]=== - -typedef struct { - int32_t arrayIndex; // -1 when not an array access - int32_t instanceType; // Instance type from stack (for VARTYPE_ARRAY / VARTYPE_STACKTOP) - bool isArray; - bool hasInstanceType; // true when instanceType was popped from stack -} ArrayAccess; - -static int32_t resolveInstanceStackTop(VMContext* ctx) { - return stackPopInt32(ctx); -} - -static const char* varTypeToString(uint8_t varType) { - switch (varType) { - case VARTYPE_ARRAY: return "ARRAY"; - case VARTYPE_STACKTOP: return "STACKTOP"; - case VARTYPE_NORMAL: return "NORMAL"; - case VARTYPE_INSTANCE: return "INSTANCE"; - default: return "UNKNOWN"; - } -} - -// Pops array index (and optional stacktop value) from the stack if the varRef -// indicates an array or stacktop access. Returns { .arrayIndex = -1, .isArray = false } -// for plain variable access. -static ArrayAccess popArrayAccess(VMContext* ctx, uint32_t varRef) { - uint8_t varType = (varRef >> 24) & 0xF8; - if (varType == VARTYPE_ARRAY) { - // For array reads, GMS pushes: instanceType then arrayIndex (arrayIndex on top) - int32_t arrayIndex = stackPopInt32(ctx); - int32_t instanceType = stackPopInt32(ctx); - - // BC17: if instanceType is -9 (INSTANCE_STACKTOP), the actual instance is the next stack item. - // This is used for chained access like `command_actor[i].specialsprite[arg]` where the array variable's owning instance is resolved from a computed value on the stack. - if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { - instanceType = resolveInstanceStackTop(ctx); - } - - return (ArrayAccess){ .arrayIndex = arrayIndex, .instanceType = instanceType, .isArray = true, .hasInstanceType = true }; - } - if (varType == VARTYPE_STACKTOP) { - int32_t instanceType = stackPopInt32(ctx); - - // BC17: PushI.e -9 (INSTANCE_STACKTOP) is pushed before the Pop instruction. - // When we pop -9, it means "the real instance type is the next item on the stack". - if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { - instanceType = resolveInstanceStackTop(ctx); - } - return (ArrayAccess){ .arrayIndex = -1, .isArray = false, .hasInstanceType = true, .instanceType = instanceType }; - } - return (ArrayAccess){ .arrayIndex = -1, .isArray = false, .hasInstanceType = false }; -} - -// ===[ Variable Resolution ]=== -static const char* instanceTypeName(int32_t instanceType) { - switch (instanceType) { - case INSTANCE_SELF: return "self"; - case INSTANCE_OTHER: return "other"; - case INSTANCE_GLOBAL: return "global"; - case INSTANCE_LOCAL: return "local"; - case INSTANCE_ARG: return "arg"; - default: return "instance"; - } -} - -// Returns the object name for an instance, or "" for the global scope dummy instance -static const char* instanceObjectName(VMContext* ctx, Instance* inst) { - if (0 > inst->objectIndex) return ""; - return ctx->dataWin->objt.objects[inst->objectIndex].name; -} - -static Variable* resolveVarDef(VMContext* ctx, uint32_t varRef) { - uint32_t varIndex = varRef & 0x07FFFFFF; - require(ctx->dataWin->vari.variableCount > varIndex); - Variable* varDef = &ctx->dataWin->vari.variables[varIndex]; - return varDef; -} - -// Maps a GML local's varID to its slot position in the current code's localVars[] array. -// -// BC16: varIDs for locals are already sequential slot indices (0, 1, 2, ...), so we return the varID unchanged. -// -// BC17+: a single GML local can surface as several VARI chunk entries that share a varID. -// We key by that shared varID via the precomputed currentCodeLocalsSlotMap so reads/writes via any VARI -// entry agree on the same localVars slot. -static uint32_t resolveLocalSlot(VMContext* ctx, int32_t varID) { - if (IS_BC16_OR_BELOW(ctx)) { - return (uint32_t) varID; - } - - // For BC17, we'll allocate the slot dynamically because the data.win CANNOT be trusted to know how localVars the script has - uint32_t slot = IntIntHashMap_getOrInsertSequential(ctx->currentCodeLocalsSlotMap, varID); - // Even though we are dynamically allocating the slots, we are still bound to whatever localVars is allocated to - // So, if a script goes over the MAX_CODE_LOCALS, it would cause unforeseen consequences... - requireMessage(MAX_CODE_LOCALS > slot, "resolveLocalSlot: exceeded MAX_CODE_LOCALS while allocating a slot for an array-only local"); - - // Grow this frame's localVars window to cover `slot` whether the entry is pre-existing or freshly allocated. - // Pre-existing entries can still be past ctx->localVarCount if a nested call to the same code extended the slot map while the outer frame was suspended (the outer frame's localVarCount is captured at call entry and doesn't follow later growth). - if (slot >= ctx->localVarCount) { - for (uint32_t i = ctx->localVarCount; slot >= i; i++) { - ctx->localVars[i] = (RValue){ .type = RVALUE_UNDEFINED }; - } - ctx->localVarCount = slot + 1; - } - return slot; -} - -// Finds an instance by target value. -// target >= 100000: instance ID (find specific instance, including recently-destroyed-but-not-cleaned-up-yet ones so GML code can read properties of an instance just after instance_destroy within the same step). -// target >= 0 && target < 100000: object index (find first ACTIVE instance of that object, checking parent chains) -static Instance* findInstanceByTarget(VMContext* ctx, int32_t target) { - Runner* runner = (Runner*) ctx->runner; - - if (target >= 100000) { - // Instance ID - find specific instance - return hmget(runner->instancesById, target); - } - - // Object index - find first active matching instance via the descendant-inclusive bucket. Pure read, no user code, so we walk the bucket directly without an arena snapshot. - if (target >= 0 && runner->dataWin->objt.count > (uint32_t) target) { - Instance** bucket = runner->instancesByObject[target]; - int32_t bucketCount = (int32_t) arrlen(bucket); - for (int32_t i = 0; bucketCount > i; i++) { - if (bucket[i]->active) return bucket[i]; - } - } - return nullptr; -} - -// Inline read of a non-array, non-builtin variable from a simple scope. -// Returns false when the instanceType isn't covered or the scope's instance pointer is unavailable, so the caller can fall through to the full resolveVariableRead. -// Used by the OP_PUSH/PUSHLOC/PUSHGLB fast paths in executeLoop to skip the entire resolveVariableRead dispatch overhead. -static inline bool tryFastVarRead(VMContext* ctx, int32_t instanceType, Variable* varDef, RValue* out) { - switch (instanceType) { - case INSTANCE_SELF: { - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr) return false; - RValue* slot = IntRValueHashMap_findSlot(&inst->selfVars, varDef->varID); - *out = (slot != nullptr) ? *slot : (RValue){ .type = RVALUE_UNDEFINED }; - out->ownsReference = false; - return true; - } - case INSTANCE_LOCAL: { - uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); - require(ctx->localVarCount > localSlot); - *out = ctx->localVars[localSlot]; - out->ownsReference = false; - return true; - } - case INSTANCE_GLOBAL: { - require(ctx->globalVarCount > (uint32_t) varDef->varID); - *out = ctx->globalVars[varDef->varID]; - out->ownsReference = false; - return true; - } - case INSTANCE_OTHER: { - Instance* inst = (Instance*) ctx->otherInstance; - if (inst == nullptr) return false; - RValue* slot = IntRValueHashMap_findSlot(&inst->selfVars, varDef->varID); - *out = (slot != nullptr) ? *slot : (RValue){ .type = RVALUE_UNDEFINED }; - out->ownsReference = false; - return true; - } - } - return false; -} - -static RValue resolveVariableRead(VMContext* ctx, int32_t instanceType, uint32_t varRef) { - Variable* varDef = resolveVarDef(ctx, varRef); - ArrayAccess access = popArrayAccess(ctx, varRef); - - // Use instance type from stack when available (VARTYPE_ARRAY / VARTYPE_STACKTOP) - int32_t originalInstanceType = instanceType; - if (access.hasInstanceType) { - instanceType = access.instanceType; - } - - // BC17+: Push.v/Pop.v with instrInstanceType == -9 (STACKTOP) and VARTYPE_NORMAL means - // "the instance is on the stack" (e.g. `struct.field` after @@NewGMLObject@@). Pop it here. -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx) && !access.hasInstanceType && instanceType == INSTANCE_STACKTOP) { - instanceType = resolveInstanceStackTop(ctx); - } -#endif - - // Resolve target instance for object/instance references (instanceType >= 0) - Instance* targetInstance = (Instance*) ctx->currentInstance; - if (instanceType >= 0) { - targetInstance = findInstanceByTarget(ctx, instanceType); - if (targetInstance == nullptr) { - const char* varTypeName = varTypeToString((varRef >> 24) & 0xF8); - if (instanceType < 100000 && (uint32_t) instanceType < ctx->dataWin->objt.count) { - GameObject* gameObject = &ctx->dataWin->objt.objects[instanceType]; - fprintf(stderr, "VM: [%s] READ var '%s' on object index %d (%s) but no instance found (varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d)\n", ctx->currentCodeName, varDef->name, instanceType, gameObject->name, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID); - } else { - fprintf(stderr, "VM: [%s] READ var '%s' on instance %d but no instance found (varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d)\n", ctx->currentCodeName, varDef->name, instanceType, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID); - } - return RValue_makeReal(0.0); - } - } else if (instanceType == INSTANCE_OTHER) { - if (ctx->otherInstance != nullptr) { - targetInstance = (Instance*) ctx->otherInstance; - } - } else if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_ARG) { - // BC17: argument0..argument15 via INSTANCE_ARG instance type (builtinVarId pre-resolved at parse time) - int16_t bid = varDef->builtinVarId; - RValue result; - if (bid == BUILTIN_VAR_ARGUMENT_COUNT) { - result = RValue_makeReal((GMLReal) ctx->scriptArgCount); - } else if (bid == BUILTIN_VAR_ARGUMENT) { - // argument[N] array-style access - int32_t idx = access.arrayIndex; - if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > idx && idx >= 0) { - result = ctx->scriptArgs[idx]; - result.ownsReference = false; - } else { - result = RValue_makeUndefined(); - } - } else if (bid >= BUILTIN_VAR_ARGUMENT0 && BUILTIN_VAR_ARGUMENT15 >= bid) { - int32_t argIndex = bid - BUILTIN_VAR_ARGUMENT0; - if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > argIndex) { - result = ctx->scriptArgs[argIndex]; - result.ownsReference = false; - // If we are trying to access the argument via an array (example: argName[i]), we NEED to read INSIDE the array - // Example: - // function init(arg2) { - // var test = arg2[0]; // We NEED to read the [0] from the array - // } - // Without this, the caller gets the whole array back - if (access.isArray && result.type == RVALUE_ARRAY && result.array != nullptr) { - result = VM_arrayReadAt(&result, access.arrayIndex); - } - } else { - result = RValue_makeUndefined(); - } - } else { - fprintf(stderr, "VM: [%s] INSTANCE_ARG read on unknown variable '%s' (builtinVarId=%d)\n", ctx->currentCodeName, varDef->name, bid); - result = RValue_makeUndefined(); - } - return result; - } - -#if IS_BC17_OR_HIGHER_ENABLED - // BC17+: instanceType == INSTANCE_BUILTIN (-6) on a Push.v means "look up this name as a function reference" (emitted for CallV dispatch paths like `@@This@@(); texture_set_interpolation_ext; CallV`). - // Intercept before the builtin-variable path: only treat it as a function if the VARI entry isn't a real built-in variable (varID == -6 with a resolved builtinVarId). - if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_BUILTIN && !(varDef->varID == -6 && varDef->builtinVarId != -1)) { - // `@@This@@(); push.v bltn.; CallV` is also used for `self.method()` where `method` is a user-defined method stored on the instance (e.g. `init = method(...)` on an object). - // CallV pops [func, instance, args], so the instance is sitting right below the func we're about to push. Peek at it and try to read `` off its selfVars first; if the VARI entry has a self scope and the peeked slot resolves to an instance with the field, return that method. Otherwise fall through to global function lookup. - if (varDef->instanceType == INSTANCE_SELF && ctx->stack.top > 0) { - RValue* peek = stackPeek(ctx); - int32_t peekId = RValue_toInt32(*peek); - Instance* peekInst = findInstanceByTarget(ctx, peekId); - if (peekInst != nullptr) { - RValue* peekSlot = IntRValueHashMap_findSlot(&peekInst->selfVars, varDef->varID); - if (peekSlot != nullptr) { - RValue val = *peekSlot; - val.ownsReference = false; - return val; - } - } - } - - // Then try user scripts/code entries (funcMap maps both "funcName" and "gml_Script_funcName") - ptrdiff_t mapIdx = shgeti(ctx->codeIndexByName, varDef->name); - if (mapIdx >= 0) { - int32_t codeIndex = ctx->codeIndexByName[mapIdx].value; - return RValue_makeMethod(codeIndex, -1); - } - // Then try registered built-ins - ptrdiff_t bidx = shgeti(ctx->builtinMap, (char*) varDef->name); - if (bidx >= 0) { - BuiltinFunc bf = ctx->builtinMap[bidx].value; - return (RValue){ .method = GMLMethod_createBuiltin(bf, -1), .type = RVALUE_METHOD, .ownsReference = true, .gmlStackType = GML_TYPE_VARIABLE }; - } - // Unresolved: return a method stub so CallV can log a single "unknown function" and return undefined instead of bailing out with a scary "unresolvable function reference" error. - return (RValue){ .method = GMLMethod_createUnresolved(varDef->name, -1), .type = RVALUE_METHOD, .ownsReference = true, .gmlStackType = GML_TYPE_VARIABLE }; - } -#endif - - // Check for built-in variable (varID == -6 sentinel) - if (varDef->varID == -6) { - // For object/instance references, temporarily swap currentInstance so VMBuiltins reads the correct instance - Instance* savedInstance = (Instance*) ctx->currentInstance; - bool needsInstanceSwap = (instanceType >= 0) || (instanceType == INSTANCE_OTHER); - if (needsInstanceSwap) ctx->currentInstance = targetInstance; - RValue result = VMBuiltins_getVariable(ctx, varDef->builtinVarId, varDef->name, access.arrayIndex); - if (needsInstanceSwap) ctx->currentInstance = savedInstance; - -#ifdef ENABLE_VM_TRACING - // Trace built-in variable reads - if (instanceType == INSTANCE_GLOBAL) { - if (shouldTraceVariable(ctx->varReadsToBeTraced, "global", nullptr, varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(result); - if (access.arrayIndex != -1) { - fprintf(stderr, "VM: [%s] READ global.%s[%d] -> %s (builtin)\n", ctx->currentCodeName, varDef->name, access.arrayIndex, rvalueAsString); - } else { - fprintf(stderr, "VM: [%s] READ global.%s -> %s (builtin)\n", ctx->currentCodeName, varDef->name, rvalueAsString); - } - free(rvalueAsString); - } - } else if (targetInstance != nullptr && targetInstance->objectIndex >= 0 && ctx->dataWin->objt.count > (uint32_t) targetInstance->objectIndex) { - const char* objName = ctx->dataWin->objt.objects[targetInstance->objectIndex].name; - if (shouldTraceVariable(ctx->varReadsToBeTraced, objName, "self", varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(result); - if (access.arrayIndex != -1) { - fprintf(stderr, "VM: [%s] READ %s.%s[%d] -> %s (instanceId=%d) (builtin)\n", ctx->currentCodeName, objName, varDef->name, access.arrayIndex, rvalueAsString, targetInstance->instanceId); - } else { - fprintf(stderr, "VM: [%s] READ %s.%s -> %s (instanceId=%d) (builtin)\n", ctx->currentCodeName, objName, varDef->name, rvalueAsString, targetInstance->instanceId); - } - free(rvalueAsString); - } - } -#endif - - return result; - } - - // Resolve the variable's scalar slot pointer for the target scope. Array-valued vars live inline as RVALUE_ARRAY in the same slot. - // VM_arrayReadAt handles the array indirection when access.isArray, VM_arrayWriteAt handles CoW forking when writing. - RValue* slot = nullptr; - switch (instanceType) { - case INSTANCE_LOCAL: { - uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); - require(ctx->localVarCount > localSlot); - slot = &ctx->localVars[localSlot]; - break; - } - case INSTANCE_GLOBAL: - require(ctx->globalVarCount > (uint32_t) varDef->varID); - slot = &ctx->globalVars[varDef->varID]; - break; - case INSTANCE_SELF: - default: { - Instance* inst = targetInstance; - if (inst == nullptr) { - const char* varTypeName = varTypeToString((varRef >> 24) & 0xF8); - fprintf(stderr, "VM: [%s] Read on self var '%s' but no current instance (instanceType=%d, varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d)\n", ctx->currentCodeName, varDef->name, instanceType, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID); - return RValue_makeReal(0.0); - } - slot = IntRValueHashMap_findSlot(&inst->selfVars, varDef->varID); - // sparse storage: nonexistent entry -> treat as undefined scalar (array reads fall through to VM_arrayReadAt returning undefined) - if (slot == nullptr) { - if (access.isArray) return (RValue){ .type = RVALUE_UNDEFINED }; - return (RValue){ .type = RVALUE_UNDEFINED }; - } - break; - } - } - - // Array access: read array[index] from the slot. - if (access.isArray) { - RValue result = VM_arrayReadAt(slot, access.arrayIndex); -#ifdef ENABLE_VM_TRACING - const char* scopeName = - instanceType == INSTANCE_LOCAL ? "local" : - instanceType == INSTANCE_GLOBAL ? "global" : - (targetInstance != nullptr ? instanceObjectName(ctx, targetInstance) : "self"); - const char* altName = (instanceType == INSTANCE_SELF || instanceType >= 0 || instanceType == INSTANCE_OTHER) ? "self" : nullptr; - if (shouldTraceVariable(ctx->varReadsToBeTraced, scopeName, altName, varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(result); - fprintf(stderr, "VM: [%s] READ %s.%s[%d] -> %s\n", ctx->currentCodeName, scopeName, varDef->name, access.arrayIndex, rvalueAsString); - free(rvalueAsString); - } -#endif - return result; - } - - // Scalar access: return the slot's current value as a weak view (slot retains ownership). - RValue result = *slot; - result.ownsReference = false; - -#ifdef ENABLE_VM_TRACING - // Read tracing for scalar variables - if (instanceType == INSTANCE_GLOBAL) { - if (shouldTraceVariable(ctx->varReadsToBeTraced, "global", nullptr, varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(result); - fprintf(stderr, "VM: [%s] READ global.%s -> %s\n", ctx->currentCodeName, varDef->name, rvalueAsString); - free(rvalueAsString); - } - } else if (instanceType == INSTANCE_SELF || instanceType >= 0) { - Instance* inst = targetInstance; - if (inst != nullptr && shouldTraceVariable(ctx->varReadsToBeTraced, instanceObjectName(ctx, inst), "self", varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(result); - fprintf(stderr, "VM: [%s] READ %s.%s -> %s (instanceId=%d)\n", ctx->currentCodeName, instanceObjectName(ctx, inst), varDef->name, rvalueAsString, inst->instanceId); - free(rvalueAsString); - } - } -#endif - - return result; -} - -// Helper: write a variable value to a single specific instance (always copies, never moves the original val) -static void writeSingleInstanceVariable(VMContext* ctx, Instance* inst, Variable* varDef, ArrayAccess* access, RValue val) { - // Built-in variable (varID == -6 sentinel) - if (varDef->varID == -6) { - Instance* savedInstance = (Instance*) ctx->currentInstance; - ctx->currentInstance = inst; - VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, access->arrayIndex); - ctx->currentInstance = savedInstance; - return; - } - - // Array write - materialise-on-write via VM_arrayWriteAt. getOrInsertUndefined returns the existing slot or inserts an UNDEFINED entry and returns it. - if (access->isArray) { - RValue* slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); - VM_arrayWriteAt((VMContext*) ctx, slot, access->arrayIndex, val); - return; - } - - // Scalar write (Instance_setSelfVar always takes an independent ref; caller still owns "val"). - Instance_setSelfVar(inst, varDef->varID, val); -} - -// Transfer ownership of "val into "*dest", freeing the old value first. -// Strings are duplicated only if the source view is non-owning (so we don't double-free). -// Arrays/methods/structs bump refcount when needed and flip the source's ownsReference flag to take a strong ref. -static inline void writeIntoSlot(RValue* dest, RValue val) { - RValue_free(dest); - if (val.type == RVALUE_STRING && !val.ownsReference && val.string != nullptr) { - *dest = RValue_makeOwnedString(safeStrdup(val.string)); - } else if (val.type == RVALUE_ARRAY && val.array != nullptr) { - if (!val.ownsReference) GMLArray_incRef(val.array); - val.ownsReference = true; - *dest = val; -#if IS_BC17_OR_HIGHER_ENABLED - } else if (val.type == RVALUE_METHOD && val.method != nullptr) { - if (!val.ownsReference) GMLMethod_incRef(val.method); - val.ownsReference = true; - *dest = val; -#endif - } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) { - if (!val.ownsReference) Instance_structIncRef(val.structInst); - val.ownsReference = true; - *dest = val; - } else { - *dest = val; - } -} - -// Force out-of-line so the OP_POP fast path in executeLoop doesn't inline this, because we already have an "optimized" version for common writes -__attribute__((noinline)) -static void resolveVariableWrite(VMContext* ctx, int32_t instanceType, uint32_t varRef, RValue val) { - Variable* varDef = resolveVarDef(ctx, varRef); - - // Fast path: When the varType==VARTYPE_NORMAL... - // * We can skip the popArrayAccess - // * We can skip the BC17 STACKTOP and INSTANCE_ARG branches - // * We can skip the array-write block itself - // * We can skip BOTH instanceType switches - if (varDef->varID >= 0) { - switch (instanceType) { - case INSTANCE_LOCAL: { - uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); - require(ctx->localVarCount > localSlot); - writeIntoSlot(&ctx->localVars[localSlot], val); - return; - } - case INSTANCE_GLOBAL: { - require(ctx->globalVarCount > (uint32_t) varDef->varID); - writeIntoSlot(&ctx->globalVars[varDef->varID], val); - return; - } - case INSTANCE_SELF: { - Instance* inst = (Instance*) ctx->currentInstance; - if (inst != nullptr) { - Instance_setSelfVar(inst, varDef->varID, val); - RValue_free(&val); - return; - } - break; // fall through to slow path so the existing nullptr-instance error gets logged - } - case INSTANCE_OTHER: { - Instance* inst = (Instance*) ctx->otherInstance; - if (inst != nullptr) { - Instance_setSelfVar(inst, varDef->varID, val); - RValue_free(&val); - return; - } - break; // fall through (otherInstance was nullptr, slow path will use currentInstance) - } - } - } - - // The slow path is used for builtin vars, object/instance references (instanceType >= 0), INSTANCE_ARG/STACKTOP, and other miscellaneous things like if we get a nullptr above - ArrayAccess access = popArrayAccess(ctx, varRef); - - // Use instance type from stack when available (VARTYPE_ARRAY / VARTYPE_STACKTOP) - int32_t originalInstanceType = instanceType; - if (access.hasInstanceType) { - instanceType = access.instanceType; - } - - // BC17+: Pop.v with instrInstanceType == -9 (STACKTOP) and VARTYPE_NORMAL means - // "the instance is on the stack" (e.g. `struct.field =` after @@NewGMLObject@@). Pop it here. -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx) && !access.hasInstanceType && instanceType == INSTANCE_STACKTOP) { - instanceType = resolveInstanceStackTop(ctx); - } -#endif - - // GML: writing through an object reference (obj_foo.var = val) sets the variable on ALL instances of that object. The setter (writeSingleInstanceVariable) can run user code, so iterate a snapshot of the bucket. - if (instanceType >= 0 && 100000 > instanceType) { - Runner* runner = (Runner*) ctx->runner; - bool found = false; - int32_t snapBase = Runner_pushInstancesOfObject(runner, instanceType); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (!inst->active) continue; - found = true; - writeSingleInstanceVariable(ctx, inst, varDef, &access, val); -#ifdef ENABLE_VM_TRACING - if (shouldTraceVariable(ctx->varWritesToBeTraced, instanceObjectName(ctx, inst), "self", varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(val); - fprintf(stderr, "VM: [%s] WRITE %s.%s = %s (instanceId=%d, all-instances object write)\n", ctx->currentCodeName, instanceObjectName(ctx, inst), varDef->name, rvalueAsString, inst->instanceId); - free(rvalueAsString); - } -#endif - } - Runner_popInstanceSnapshot(runner, snapBase); - if (!found) { - if (ctx->dataWin->objt.count > (uint32_t) instanceType) { - GameObject* gameObject = &ctx->dataWin->objt.objects[instanceType]; - char* valAsString = RValue_toString(val); - fprintf(stderr, "VM: [%s] WRITE var '%s' on object %d (%s) but no instances found (value=%s)\n", ctx->currentCodeName, varDef->name, instanceType, gameObject->name, valAsString); - free(valAsString); - } - } - RValue_free(&val); - return; - } - - // Resolve target instance for instance ID references (instanceType >= 100000) or special types - Instance* targetInstance = (Instance*) ctx->currentInstance; - if (instanceType >= 0) { - targetInstance = findInstanceByTarget(ctx, instanceType); - if (targetInstance == nullptr) { - const char* varTypeName = varTypeToString((varRef >> 24) & 0xF8); - char* valAsString = RValue_toString(val); - fprintf(stderr, "VM: [%s] WRITE var '%s' on instance %d but no instance found (varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d, value=%s)\n", ctx->currentCodeName, varDef->name, instanceType, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID, valAsString); - free(valAsString); - return; - } - } else if (instanceType == INSTANCE_OTHER) { - if (ctx->otherInstance != nullptr) { - targetInstance = (Instance*) ctx->otherInstance; - } - } else if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_ARG) { - // BC17: write to argument0..argument15 via INSTANCE_ARG instance type (builtinVarId pre-resolved at parse time) - int16_t bid = varDef->builtinVarId; - int32_t writeIndex = -1; - if (bid >= BUILTIN_VAR_ARGUMENT0 && BUILTIN_VAR_ARGUMENT15 >= bid) { - writeIndex = bid - BUILTIN_VAR_ARGUMENT0; - } else if (bid == BUILTIN_VAR_ARGUMENT) { - writeIndex = access.arrayIndex; - } else { - fprintf(stderr, "VM: [%s] INSTANCE_ARG write on unknown variable '%s' (builtinVarId=%d)\n", ctx->currentCodeName, varDef->name, bid); - } - if (writeIndex >= 0 && GML_MAX_ARGUMENTS > writeIndex && ctx->scriptArgs != nullptr) { - RValue_free(&ctx->scriptArgs[writeIndex]); - if (val.type == RVALUE_STRING && val.string != nullptr) { - ctx->scriptArgs[writeIndex] = RValue_makeOwnedString(safeStrdup(val.string)); - } else { - // Transfer ownership from val into scriptArgs: copy the tagged union as-is and neutralize val so the RValue_free below is a no-op for arrays/methods. - ctx->scriptArgs[writeIndex] = val; - val.ownsReference = false; - } - if (writeIndex >= ctx->scriptArgCount) { - ctx->scriptArgCount = writeIndex + 1; - } - } - RValue_free(&val); - return; - } - - // Check for built-in variable (varID == -6 sentinel) - if (varDef->varID == -6) { - // For object/instance references, temporarily swap currentInstance so VMBuiltins writes the correct instance - Instance* savedInstance = (Instance*) ctx->currentInstance; - bool needsInstanceSwap = (instanceType >= 0) || (instanceType == INSTANCE_OTHER); - if (needsInstanceSwap) ctx->currentInstance = targetInstance; - VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, access.arrayIndex); - if (needsInstanceSwap) ctx->currentInstance = savedInstance; - -#ifdef ENABLE_VM_TRACING - // Trace built-in variable writes - if (instanceType == INSTANCE_GLOBAL) { - if (shouldTraceVariable(ctx->varWritesToBeTraced, "global", nullptr, varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(val); - if (access.arrayIndex != -1) { - fprintf(stderr, "VM: [%s] WRITE global.%s[%d] = %s (builtin)\n", ctx->currentCodeName, varDef->name, access.arrayIndex, rvalueAsString); - } else { - fprintf(stderr, "VM: [%s] WRITE global.%s = %s (builtin)\n", ctx->currentCodeName, varDef->name, rvalueAsString); - } - free(rvalueAsString); - } - } else if (targetInstance != nullptr && targetInstance->objectIndex >= 0 && ctx->dataWin->objt.count > (uint32_t) targetInstance->objectIndex) { - const char* objName = ctx->dataWin->objt.objects[targetInstance->objectIndex].name; - if (shouldTraceVariable(ctx->varWritesToBeTraced, objName, "self", varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(val); - if (access.arrayIndex != -1) { - fprintf(stderr, "VM: [%s] WRITE %s.%s[%d] = %s (instanceId=%d) (builtin)\n", ctx->currentCodeName, objName, varDef->name, access.arrayIndex, rvalueAsString, targetInstance->instanceId); - } else { - fprintf(stderr, "VM: [%s] WRITE %s.%s = %s (instanceId=%d) (builtin)\n", ctx->currentCodeName, objName, varDef->name, rvalueAsString, targetInstance->instanceId); - } - free(rvalueAsString); - } - } -#endif - - // VMBuiltins_setVariable reads values (toReal, toInt32, etc.) but does not take ownership - RValue_free(&val); - return; - } - - // Resolve the slot pointer for this scope. For INSTANCE_SELF we materialise a sparse selfVars entry if it doesn't exist so VM_arrayWriteAt has a stable slot to own. - RValue* slot = nullptr; - switch (instanceType) { - case INSTANCE_LOCAL: { - uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); - require(ctx->localVarCount > localSlot); - slot = &ctx->localVars[localSlot]; - break; - } - case INSTANCE_GLOBAL: - require(ctx->globalVarCount > (uint32_t) varDef->varID); - slot = &ctx->globalVars[varDef->varID]; - break; - case INSTANCE_SELF: - default: { - Instance* inst = targetInstance; - if (inst == nullptr) { - const char* varTypeName = varTypeToString((varRef >> 24) & 0xF8); - char* valAsString = RValue_toString(val); - fprintf(stderr, "VM: [%s] Write on self var '%s' but no current instance (instanceType=%d, varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d, value=%s)\n", ctx->currentCodeName, varDef->name, instanceType, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID, valAsString); - free(valAsString); - RValue_free(&val); - return; - } - slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); - break; - } - } - - // Array write via VM_arrayWriteAt (handles CoW fork, grow, owner stamping). - if (access.isArray) { - VM_arrayWriteAt(ctx, slot, access.arrayIndex, val); -#ifdef ENABLE_VM_TRACING - const char* scopeName = - instanceType == INSTANCE_LOCAL ? "local" : - instanceType == INSTANCE_GLOBAL ? "global" : - (targetInstance != nullptr ? instanceObjectName(ctx, targetInstance) : "self"); - const char* altName = (instanceType == INSTANCE_SELF || instanceType >= 0 || instanceType == INSTANCE_OTHER) ? "self" : nullptr; - if (shouldTraceVariable(ctx->varWritesToBeTraced, scopeName, altName, varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(val); - fprintf(stderr, "VM: [%s] WRITE %s.%s[%d] = %s\n", ctx->currentCodeName, scopeName, varDef->name, access.arrayIndex, rvalueAsString); - free(rvalueAsString); - } -#endif - RValue_free(&val); - return; - } - -#ifdef ENABLE_VM_TRACING - bool shouldLogGlobal = false; - bool shouldLogInstance = false; -#endif - - switch (instanceType) { - case INSTANCE_LOCAL: { - uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); - require(ctx->localVarCount > localSlot); - writeIntoSlot(&ctx->localVars[localSlot], val); - return; - } - case INSTANCE_GLOBAL: { - require(ctx->globalVarCount > (uint32_t) varDef->varID); - RValue* dest = &ctx->globalVars[varDef->varID]; - writeIntoSlot(dest, val); -#ifdef ENABLE_VM_TRACING - if (shouldTraceVariable(ctx->varWritesToBeTraced, "global", nullptr, varDef->name)) { - char* rvalueAsString = RValue_toStringTyped(*dest); - fprintf(stderr, "VM: [%s] WRITE global.%s = %s\n", ctx->currentCodeName, varDef->name, rvalueAsString); - free(rvalueAsString); - } -#endif - return; - } - case INSTANCE_SELF: - default: { - // Self or object/instance reference - use sparse hashmap - Instance* inst = targetInstance; - Instance_setSelfVar(inst, varDef->varID, val); -#ifdef ENABLE_VM_TRACING - if (shouldTraceVariable(ctx->varWritesToBeTraced, instanceObjectName(ctx, inst), "self", varDef->name)) { - RValue written = Instance_getSelfVar(inst, varDef->varID); - char* rvalueAsString = RValue_toStringTyped(written); - fprintf(stderr, "VM: [%s] WRITE %s.%s = %s (instanceId=%d)\n", ctx->currentCodeName, instanceObjectName(ctx, inst), varDef->name, rvalueAsString, inst->instanceId); - free(rvalueAsString); - } -#endif - // Instance_setSelfVar always copies strings, so free the original - RValue_free(&val); - return; - } - } -} - -// ===[ Type Conversion ]=== - -static RValue convertValue(RValue val, uint8_t targetType) { - switch (targetType) { - case GML_TYPE_DOUBLE: - return RValue_makeReal(RValue_toReal(val)); - case GML_TYPE_FLOAT: - return RValue_makeReal((GMLReal) (float) RValue_toReal(val)); - case GML_TYPE_INT32: - return RValue_makeInt32(RValue_toInt32(val)); - case GML_TYPE_INT64: - return RValue_makeInt64(RValue_toInt64(val)); - case GML_TYPE_BOOL: - return RValue_makeBool(RValue_toBool(val)); - case GML_TYPE_STRING: { - char* str = RValue_toString(val); - return RValue_makeOwnedString(str); - } - case GML_TYPE_VARIABLE: - // Variable type on stack is just an RValue passthrough - return val; - default: - fprintf(stderr, "VM: Unknown target type 0x%X for conversion\n", targetType); - return val; - } -} - -// ===[ Opcode Handlers ]=== - -static void handlePush(VMContext* ctx, uint32_t instr, const uint8_t* extraData, uint8_t type1) { - switch (type1) { - case GML_TYPE_DOUBLE: - stackPush(ctx, RValue_makeReal(BinaryUtils_readFloat64Aligned(extraData))); - break; - case GML_TYPE_FLOAT: - stackPush(ctx, RValue_makeReal((GMLReal) BinaryUtils_readFloat32Aligned(extraData))); - break; - case GML_TYPE_INT32: - stackPush(ctx, RValue_makeInt32(BinaryUtils_readInt32Aligned(extraData))); - break; - case GML_TYPE_INT64: - stackPush(ctx, RValue_makeInt64(BinaryUtils_readInt64Aligned(extraData))); - break; - case GML_TYPE_BOOL: - stackPush(ctx, RValue_makeBool(BinaryUtils_readInt32Aligned(extraData) != 0)); - break; - case GML_TYPE_VARIABLE: { - int32_t instanceType = (int32_t) instrInstanceType(instr); - uint32_t varRef = resolveVarOperand(extraData); - uint8_t varType = (varRef >> 24) & 0xF8; - // BC17: VARTYPE_INSTANCE encodes (instanceId - 100000) in the instruction's lower 16 bits. - // Add 100000 back so findInstanceByTarget sees the real runtime instance ID. - if (varType == VARTYPE_INSTANCE) instanceType += 100000; -#if IS_BC17_OR_HIGHER_ENABLED - if (varType == VARTYPE_ARRAYPUSHAF || varType == VARTYPE_ARRAYPOPAF) { - // V17: multi-dim first-step. Stack has [scope, firstIndex] (with an optional real-instance slot underneath when scope == -9 INSTANCE_STACKTOP). - // We resolve the variable's top-level array slot, materialise it if needed, then drill into arr->data[firstIndex] (materialising a sub-array there too). - // The sub-array is pushed as a weak ref; subsequent BREAK_PUSHAC/PUSHAF/POPAF consume it. - Variable* varDef = resolveVarDef(ctx, varRef); - int32_t firstIndex = stackPopInt32(ctx); - int32_t scope = stackPopInt32(ctx); - if (IS_BC17_OR_HIGHER(ctx) && scope == INSTANCE_STACKTOP) { - scope = resolveInstanceStackTop(ctx); - } - - // Resolve the slot for this scope. - RValue* slot = nullptr; - switch (scope) { - case INSTANCE_LOCAL: { - uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); - require(ctx->localVarCount > localSlot); - slot = &ctx->localVars[localSlot]; - break; - } - case INSTANCE_GLOBAL: - require(ctx->globalVarCount > (uint32_t) varDef->varID); - slot = &ctx->globalVars[varDef->varID]; - break; - case INSTANCE_SELF: - case INSTANCE_OTHER: { - Instance* inst = (scope == INSTANCE_OTHER && ctx->otherInstance != nullptr) - ? (Instance*) ctx->otherInstance - : (Instance*) ctx->currentInstance; - require(inst != nullptr); - slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); - break; - } - default: { - Instance* inst = findInstanceByTarget(ctx, scope); - if (inst == nullptr) { - fprintf(stderr, "VM: ARRAYPUSHAF: no instance for scope %d varID=%d\n", scope, varDef->varID); - abort(); - } - slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); - break; - } - } - - // Materialise the top-level array in the slot if needed. - if (slot->type != RVALUE_ARRAY || slot->array == nullptr) { - RValue_free(slot); - GMLArray* fresh = GMLArray_create(0); - fresh->owner = IS_BC17_OR_HIGHER(ctx) ? ctx->currentArrayOwner : (void*) slot; - *slot = RValue_makeArray(fresh); - } - GMLArray* top = slot->array; - GMLArray_growTo(top, firstIndex + 1); - RValue* topSlot = GMLArray_slot(top, firstIndex); - // Materialise the sub-array at [firstIndex] if it's not already an array. - if (topSlot->type != RVALUE_ARRAY || topSlot->array == nullptr) { - RValue_free(topSlot); - GMLArray* sub = GMLArray_create(0); - sub->owner = top->owner; - *topSlot = (RValue){ .array = sub, .type = RVALUE_ARRAY, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; - } - // Push a weak ref to the sub-array — short-lived, consumed by the next BREAK op. - stackPush(ctx, RValue_makeArrayWeak(topSlot->array)); - } else -#endif - { - RValue val = resolveVariableRead(ctx, instanceType, varRef); - // Mark as variable-width (16 bytes on native stack) regardless of the RValue's actual type - stackPushTyped(ctx, val, GML_TYPE_VARIABLE); - } - break; - } - case GML_TYPE_STRING: { - int32_t stringIndex = BinaryUtils_readInt32Aligned(extraData); - require(stringIndex >= 0 && ctx->dataWin->strg.count > (uint32_t) stringIndex); - stackPush(ctx, RValue_makeString(ctx->dataWin->strg.strings[stringIndex])); - break; - } - case GML_TYPE_INT16: { - int16_t value = (int16_t) (instr & 0xFFFF); - RValue val = RValue_makeInt32((int32_t) value); - stackPushTyped(ctx, val, GML_TYPE_INT16); - break; - } - default: - fprintf(stderr, "VM: Push with unknown type 0x%X\n", type1); - abort(); - } -} - -#if IS_BC17_OR_HIGHER_ENABLED -// For V17+ VARTYPE_ARRAYPUSHAF/POPAF on a top-level variable: return the slot's GMLArray*, -// materialising a fresh empty one in the slot if it isn't an array yet. Used by PushLoc/Glb/Bltn. -// Pushes a weak ref onto the stack — short-lived, consumed by the next BREAK_PUSHAC/PUSHAF/POPAF. -static void pushTopLevelArrayRef(VMContext* ctx, RValue* slot) { - if (slot->type != RVALUE_ARRAY || slot->array == nullptr) { - RValue_free(slot); - GMLArray* fresh = GMLArray_create(0); - fresh->owner = IS_BC17_OR_HIGHER(ctx) ? ctx->currentArrayOwner : (void*) slot; - *slot = RValue_makeArray(fresh); - } - stackPush(ctx, RValue_makeArrayWeak(slot->array)); -} -#endif - -static void handlePushBltn(VMContext* ctx, uint32_t instr, const uint8_t* extraData) { - uint32_t varRef = resolveVarOperand(extraData); -#if IS_BC17_OR_HIGHER_ENABLED - uint8_t varType = (varRef >> 24) & 0xF8; - if (varType == VARTYPE_ARRAYPUSHAF || varType == VARTYPE_ARRAYPOPAF) { - Variable* varDef = resolveVarDef(ctx, varRef); - int32_t scope = (int32_t) instrInstanceType(instr); - Instance* inst = nullptr; - if (scope == INSTANCE_SELF || scope == -1) { - inst = (Instance*) ctx->currentInstance; - } else if (scope == INSTANCE_OTHER && ctx->otherInstance != nullptr) { - inst = (Instance*) ctx->otherInstance; - } else if (scope >= 0) { - inst = findInstanceByTarget(ctx, scope); - } else { - inst = (Instance*) ctx->currentInstance; - } - if (inst == nullptr) { - fprintf(stderr, "VM: PushBltn ARRAYPUSHAF: no instance for scope %d varID=%d\n", scope, varDef->varID); - abort(); - } - RValue* slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); - pushTopLevelArrayRef(ctx, slot); - return; - } -#endif - RValue val = resolveVariableRead(ctx, (int32_t) instrInstanceType(instr), varRef); - stackPushTyped(ctx, val, GML_TYPE_VARIABLE); -} - -static void handlePushI(VMContext* ctx, uint32_t instr) { - int16_t value = (int16_t) (instr & 0xFFFF); - RValue val = RValue_makeInt32((int32_t) value); - stackPushTyped(ctx, val, GML_TYPE_INT16); -} - -// When storing into a variant variable from an int32/int64 stack source, coerce to real. -// GMS variables normalize integer literals to doubles so subsequent arithmetic routes through the real fast path instead of int32 x int32 wrapping. -static inline RValue coerceIntStoreToReal(RValue val, uint8_t type2) { - if (type2 == GML_TYPE_INT32 || type2 == GML_TYPE_INT64 || type2 == GML_TYPE_INT16) { - if (val.type == RVALUE_INT32) { - return RValue_makeReal((GMLReal) val.int32); - } -#ifndef NO_RVALUE_INT64 - if (val.type == RVALUE_INT64) { - return RValue_makeReal((GMLReal) val.int64); - } -#endif - } - return val; -} - -static void handlePop(VMContext* ctx, uint32_t instr, uint8_t type1, uint8_t type2, uint32_t varRef, uint8_t varType, int32_t instanceType) { - RValue val; - int32_t arrayIndex = -1; - - int32_t originalInstanceType = instanceType; - if (varType == VARTYPE_ARRAY) { - if (type1 == GML_TYPE_VARIABLE) { - // Simple assignment (Pop.v.v): stack bottom-to-top = [value, (realInstance,) instanceType, arrayIndex] - arrayIndex = stackPopInt32(ctx); - instanceType = stackPopInt32(ctx); - - // BC17: -9 (INSTANCE_STACKTOP) means "pop again for the real instance ID/object index" (e.g. `su_actor.specialsprite[0] = ...`) - if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { - instanceType = resolveInstanceStackTop(ctx); - } - - val = stackPop(ctx); - } else { - // Compound assignment (Pop.i.v, etc.): stack bottom-to-top = [(realInstance,) instanceType, arrayIndex, value] - val = stackPop(ctx); - - arrayIndex = stackPopInt32(ctx); - instanceType = stackPopInt32(ctx); - - // BC17: -9 (INSTANCE_STACKTOP) means "pop again for the real instance ID/object index" - if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { - instanceType = resolveInstanceStackTop(ctx); - } - } - } else if (varType == VARTYPE_STACKTOP && type1 == GML_TYPE_VARIABLE) { - // Simple assignment (Pop.v.v) with STACKTOP: stack bottom-to-top = [value, instanceType] - // Pop instanceType first (top), then value (bottom) - instanceType = stackPopInt32(ctx); - - // BC17: -9 (INSTANCE_STACKTOP) means "pop again for the real instance type" - if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { - instanceType = resolveInstanceStackTop(ctx); - } - - val = stackPop(ctx); - - // Clear STACKTOP type bits so resolveVariableWrite's popArrayAccess won't double-pop - varRef = (varRef & 0x07FFFFFF) | ((uint32_t) VARTYPE_NORMAL << 24); - } else { - val = stackPop(ctx); - } - - // Convert if source type differs from destination type. - // For VARTYPE_ARRAY compound assignments (type1 != GML_TYPE_VARIABLE), the type1 field - // indicates the stack layout (compound vs simple), NOT a type conversion target. - // Skip conversion in that case to preserve string values through += operations. - // For compound assignments (type1 != GML_TYPE_VARIABLE) with VARTYPE_ARRAY or VARTYPE_STACKTOP, - // the type1 field indicates the stack layout (compound vs simple), NOT a type conversion target. - // Skip conversion to preserve the actual computed value (e.g. g.image_angle -= 4.5 must not truncate to int). - bool isCompoundAssignment = ((varType == VARTYPE_ARRAY || varType == VARTYPE_STACKTOP) && type1 != GML_TYPE_VARIABLE); - if (type2 != type1 && type1 != GML_TYPE_VARIABLE && !isCompoundAssignment) { - RValue converted = convertValue(val, type1); - RValue_free(&val); - val = converted; - } - - if (type1 == GML_TYPE_VARIABLE && !isCompoundAssignment) { - val = coerceIntStoreToReal(val, type2); - } - - if (varType == VARTYPE_ARRAY) { - Variable* varDef = resolveVarDef(ctx, varRef); - if (varDef->varID == -6) { - // Resolve target instance for built-in array variable writes (e.g. obj_foo.alarm[0] = 2) - if (instanceType >= 0 && 100000 > instanceType) { - // Object reference: write to ALL instances of that object. The setter can run user code, so iterate a snapshot of the bucket. - Runner* runner = (Runner*) ctx->runner; - Instance* savedInstance = (Instance*) ctx->currentInstance; - int32_t snapBase = Runner_pushInstancesOfObject(runner, instanceType); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (!inst->active) continue; - ctx->currentInstance = inst; - VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, arrayIndex); - } - Runner_popInstanceSnapshot(runner, snapBase); - ctx->currentInstance = savedInstance; - } else if (instanceType >= 0) { - // Instance ID reference - Instance* target = findInstanceByTarget(ctx, instanceType); - if (target != nullptr) { - Instance* savedInstance = (Instance*) ctx->currentInstance; - ctx->currentInstance = target; - VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, arrayIndex); - ctx->currentInstance = savedInstance; - } - } else if (instanceType == INSTANCE_OTHER && ctx->otherInstance != nullptr) { - Instance* savedInstance = (Instance*) ctx->currentInstance; - ctx->currentInstance = (Instance*) ctx->otherInstance; - VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, arrayIndex); - ctx->currentInstance = savedInstance; - } else { - // INSTANCE_SELF or other special types: use current instance - VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, arrayIndex); - } - } else { - // Resolve slot for this scope: VM_arrayWriteAt handles CoW + materialisation + grow. - RValue* slot = nullptr; - switch (instanceType) { - case INSTANCE_LOCAL: { - uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); - require(ctx->localVarCount > localSlot); - slot = &ctx->localVars[localSlot]; - break; - } - case INSTANCE_GLOBAL: - require(ctx->globalVarCount > (uint32_t) varDef->varID); - slot = &ctx->globalVars[varDef->varID]; - break; - case INSTANCE_SELF: - default: { - struct Instance* inst = (struct Instance*) ctx->currentInstance; - if (instanceType >= 0) { - inst = findInstanceByTarget(ctx, instanceType); - if (inst == nullptr) { - const char* varTypeName = varTypeToString(varType); - char* valAsString = RValue_toString(val); - if (instanceType < 100000 && (uint32_t) instanceType < ctx->dataWin->objt.count) { - fprintf(stderr, "VM: [%s] WRITE array var '%s[%d]' on object index %d (%s) but no instance found (varType=%s, originalInstanceType=%d, varID=%d, value=%s)\n", ctx->currentCodeName, varDef->name, arrayIndex, instanceType, ctx->dataWin->objt.objects[instanceType].name, varTypeName, originalInstanceType, varDef->varID, valAsString); - } else { - fprintf(stderr, "VM: [%s] WRITE array var '%s[%d]' on instance %d but no instance found (varType=%s, originalInstanceType=%d, varID=%d, value=%s)\n", ctx->currentCodeName, varDef->name, arrayIndex, instanceType, varTypeName, originalInstanceType, varDef->varID, valAsString); - } - free(valAsString); - RValue_free(&val); - return; - } - } else if (instanceType == INSTANCE_OTHER && ctx->otherInstance != nullptr) { - inst = (Instance*) ctx->otherInstance; - } - if (inst == nullptr) { - RValue_free(&val); - return; - } - slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); - break; - } - } - if (slot != nullptr) { - VM_arrayWriteAt(ctx, slot, arrayIndex, val); -#ifdef ENABLE_VM_TRACING - bool isSelfScope = (instanceType != INSTANCE_LOCAL && instanceType != INSTANCE_GLOBAL); - const char* scopeName = instanceType == INSTANCE_LOCAL ? "local" : instanceType == INSTANCE_GLOBAL ? "global" : "self"; - if (shouldTraceVariable(ctx->varWritesToBeTraced, scopeName, isSelfScope ? nullptr : "self", varDef->name)) { - char* rvalueAsString = RValue_toString(val); - fprintf(stderr, "VM: [%s] WRITE %s.%s[%d] = %s\n", ctx->currentCodeName, scopeName, varDef->name, arrayIndex, rvalueAsString); - free(rvalueAsString); - } -#endif - } - RValue_free(&val); - } - } else { - resolveVariableWrite(ctx, instanceType, varRef, val); - } -} - -static void handlePopz(VMContext* ctx) { - RValue val = stackPop(ctx); - RValue_free(&val); -} - -__attribute__((noinline)) -static void handleAddString(VMContext* ctx, RValue a, RValue b, uint8_t resultType) { - if (a.type == RVALUE_STRING && b.type == RVALUE_STRING) { - // String concatenation - const char* sa = a.string != nullptr ? a.string : ""; - const char* sb = b.string != nullptr ? b.string : ""; - size_t lenA = strlen(sa); - size_t lenB = strlen(sb); - char* result = safeMalloc(lenA + lenB + 1); - memcpy(result, sa, lenA); - memcpy(result + lenA, sb, lenB + 1); - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeOwnedString(result), resultType); - } else { - // For anything else, we'll convert to numbers and then sum -#ifndef NO_RVALUE_INT64 - if (a.type == RVALUE_INT64 || b.type == RVALUE_INT64) { - int64_t result = (a.type == RVALUE_STRING) ? (int64_t) GMLReal_strtod(a.string, nullptr) : a.int64; - result += (b.type == RVALUE_STRING) ? (int64_t) GMLReal_strtod(b.string, nullptr) : b.int64; - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeInt64(result), resultType); - return; - } -#endif - if (a.type == RVALUE_INT32 || b.type == RVALUE_INT32) { - int32_t result = (a.type == RVALUE_STRING) ? (int32_t) GMLReal_strtod(a.string, nullptr) : a.int32; - result += (b.type == RVALUE_STRING) ? (int32_t) GMLReal_strtod(b.string, nullptr) : b.int32; - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeInt32(result), resultType); - return; - } - GMLReal result = RValue_toReal(a) + RValue_toReal(b); - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeReal(result), resultType); - } -} - -__attribute__((noinline)) -static void handleMulString(VMContext* ctx, RValue a, RValue b, uint8_t resultType) { - // a.type == RVALUE_STRING; b is the repetition count. - int count = RValue_toInt32(b); - const char* str = a.string != nullptr ? a.string : ""; - size_t len = strlen(str); - if (0 >= count || len == 0) { - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeOwnedString(safeStrdup("")), resultType); - } else { - char* result = safeMalloc(len * count + 1); - repeat(count, i) { - memcpy(result + i * len, str, len); - } - result[len * count] = '\0'; - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeOwnedString(result), resultType); - } -} - -static void handleDiv(VMContext* ctx, uint32_t instr) { - RValue b = stackPop(ctx); - RValue a = stackPop(ctx); - uint8_t type1 = instrType1(instr); - uint8_t type2 = instrType2(instr); - GMLReal divisor = RValue_toReal(b); - // In GameMaker's native runner, ONLY integer/integer division throws a hard error on zero, float/variable types rely on IEEE 754 (produces NaN) - if ((type1 == GML_TYPE_INT32 || type1 == GML_TYPE_INT64) && (type2 == GML_TYPE_INT32 || type2 == GML_TYPE_INT64)) { - requireMessageFormatted(divisor != 0.0, "VM: [%s] DoDiv :: Divide by zero", ctx->currentCodeName); - } - GMLReal result = RValue_toReal(a) / divisor; - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeReal(result), instrType2(instr)); -} - -static void handleRem(VMContext* ctx, uint32_t instr) { - RValue b = stackPop(ctx); - RValue a = stackPop(ctx); - int32_t ib = RValue_toInt32(b); - requireMessageFormatted(ib != 0, "VM: [%s] DoRem :: Divide by zero", ctx->currentCodeName); - int32_t result = RValue_toInt32(a) % ib; - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeInt32(result), instrType2(instr)); -} - -static void handleMod(VMContext* ctx, uint32_t instr) { - RValue b = stackPop(ctx); - RValue a = stackPop(ctx); - GMLReal divisor = RValue_toReal(b); - requireMessageFormatted(divisor != 0.0, "VM: [%s] DoMod :: Divide by zero", ctx->currentCodeName); - GMLReal result = GMLReal_fmod(RValue_toReal(a), divisor); - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeReal(result), instrType2(instr)); -} - -#define SIMPLE_BYTECODE_BITWISE_OPERATION(op) \ - int32_t b = stackPopInt32(ctx); \ - int32_t a = stackPopInt32(ctx); \ - int32_t result = a op b; \ - stackPushTyped(ctx, RValue_makeInt32(result), instrType2(instr)) - -static void handleAnd(VMContext* ctx, uint32_t instr) { - SIMPLE_BYTECODE_BITWISE_OPERATION(&); -} - -static void handleOr(VMContext* ctx, uint32_t instr) { - SIMPLE_BYTECODE_BITWISE_OPERATION(|); -} - -static void handleXor(VMContext* ctx, uint32_t instr) { - SIMPLE_BYTECODE_BITWISE_OPERATION(^); -} - -static void handleNeg(VMContext* ctx, uint32_t instr) { - RValue a = stackPop(ctx); - GMLReal result = -RValue_toReal(a); - RValue_free(&a); - stackPushTyped(ctx, RValue_makeReal(result), instrType1(instr)); -} - -static void handleNot(VMContext* ctx, uint32_t instr) { - uint8_t resultType = instrType1(instr); - int32_t a = stackPopInt32(ctx); - if (GML_TYPE_BOOL == resultType) { - // Logical NOT: compiler emits this for the ! operator on boolean expressions - int32_t result = (a == 0) ? 1 : 0; - stackPushTyped(ctx, RValue_makeBool(result != 0), resultType); - } else { - // Bitwise NOT: used for ~ operator on integer types - int32_t result = ~a; - stackPushTyped(ctx, RValue_makeInt32(result), resultType); - } -} - -static void handleShl(VMContext* ctx, uint32_t instr) { - SIMPLE_BYTECODE_BITWISE_OPERATION(<<); -} - -static void handleShr(VMContext* ctx, uint32_t instr) { - SIMPLE_BYTECODE_BITWISE_OPERATION(>>); -} - -static void handleConv(VMContext* ctx, uint8_t srcType, uint8_t dstType, uint8_t convKey) { - RValue val = stackPop(ctx); - - RValue result; - - switch (convKey) { - // Identity conversions (no-op) - case 0x00: case 0x22: case 0x33: case 0x44: case 0x66: - result = val; - break; - - // Double (0) -> other - case 0x20: result = RValue_makeInt32((int32_t) val.real); break; - case 0x30: result = RValue_makeInt64((int64_t) val.real); break; - case 0x40: result = RValue_makeBool(val.real > 0.5); break; - case 0x50: result = val; break; // Double -> Variable (passthrough) - case 0x60: { char* s = RValue_toString(val); result = RValue_makeOwnedString(s); break; } - case 0xF0: result = RValue_makeInt32((int32_t) val.real); break; - - // Float (1) -> other (float stored as double in our RValue) - case 0x01: result = RValue_makeReal(val.real); break; - case 0x21: result = RValue_makeInt32((int32_t) val.real); break; - case 0x31: result = RValue_makeInt64((int64_t) val.real); break; - case 0x41: result = RValue_makeBool(val.real > 0.5); break; - case 0x51: result = val; break; // Float -> Variable (passthrough) - - // Int32 (2) -> other - case 0x02: result = RValue_makeReal((GMLReal) val.int32); break; - case 0x12: result = RValue_makeReal((GMLReal) val.int32); break; - case 0x32: result = RValue_makeInt64((int64_t) val.int32); break; - case 0x42: result = RValue_makeBool(val.int32 > 0); break; - case 0x52: result = val; break; // Int32 -> Variable (passthrough) - case 0x62: { char* s = RValue_toString(val); result = RValue_makeOwnedString(s); break; } - case 0xF2: result = val; break; - -#ifndef NO_RVALUE_INT64 - // Int64 (3) -> other - case 0x03: result = RValue_makeReal((GMLReal) val.int64); break; - case 0x23: result = RValue_makeInt32((int32_t) val.int64); break; - case 0x43: result = RValue_makeBool(val.int64 > 0); break; - case 0x53: result = val; break; // Int64 -> Variable (passthrough) -#elif IS_BC17_OR_HIGHER_ENABLED - // Int64 (3) -> other (Int64 stored as Int32 when NO_RVALUE_INT64). - // Only emitted on BC17+ builds: BC16 games (Undertale, SURVEY_PROGRAM) never emit Int64 Conv opcodes. - case 0x03: result = RValue_makeReal((GMLReal) val.int32); break; - case 0x23: result = val; break; // Already Int32 - case 0x43: result = RValue_makeBool(val.int32 > 0); break; - case 0x53: result = val; break; // Int64 -> Variable (passthrough) -#endif - - // Bool (4) -> other - case 0x04: result = RValue_makeReal((GMLReal) val.int32); break; - case 0x24: result = RValue_makeInt32(val.int32); break; - case 0x34: result = RValue_makeInt64((int64_t) val.int32); break; - case 0x54: result = val; break; // Bool -> Variable (passthrough) - case 0x64: { char* s = RValue_toString(val); result = RValue_makeOwnedString(s); break; } - - // Variable (5) -> other - case 0x05: result = RValue_makeReal(RValue_toReal(val)); break; - case 0x15: result = RValue_makeReal(RValue_toReal(val)); break; - case 0x25: result = RValue_makeInt32(RValue_toInt32(val)); break; - case 0x35: result = RValue_makeInt64(RValue_toInt64(val)); break; - case 0x45: result = RValue_makeBool(RValue_toBool(val)); break; - case 0x55: result = val; break; // Variable -> Variable (identity) - case 0x65: { char* s = RValue_toString(val); result = RValue_makeOwnedString(s); break; } - case 0xF5: result = RValue_makeInt32(RValue_toInt32(val)); break; - - // String (6) -> other - case 0x06: result = RValue_makeReal(GMLReal_strtod(val.string, nullptr)); break; - case 0x26: result = RValue_makeInt32((int32_t) GMLReal_strtod(val.string, nullptr)); break; - case 0x36: result = RValue_makeInt64((int64_t) GMLReal_strtod(val.string, nullptr)); break; - case 0x46: result = RValue_makeBool(val.string != nullptr && val.string[0] != '\0'); break; - case 0x56: { - // String -> Variable: keep as-is since our RValue handles strings natively - result = val; - break; - } - - // Int16 (F) -> other - case 0x0F: result = RValue_makeReal((GMLReal) val.int32); break; - case 0x2F: result = val; break; - case 0x5F: result = val; break; - - default: - fprintf(stderr, "VM: [%s] Conv unhandled conversion 0x%02X (src=0x%X dst=0x%X)\n", ctx->currentCodeName, convKey, srcType, dstType); - result = val; - break; - } - - // Don't free the old value if we're returning the same value (identity conversion or passthrough) - if (result.string != val.string || result.type != val.type) { - RValue_free(&val); - } - - // Set gmlStackType to the destination type so Dup can compute correct byte sizes (BC17+ only) -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx)) { - result.gmlStackType = dstType; - } -#endif - stackPush(ctx, result); -} - -// Tries to parse a string as a real number, mirroring HTML5 yyCompareVal's behavior: -// trim leading whitespace, then accept a numeric prefix (sign, digits, decimal, exponent). -// Returns true on success, with the parsed value written to *out. -static bool tryParseRealFromString(const char* str, GMLReal* out) { - if (str == nullptr) return false; - while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r') str++; - if (*str == '\0') return false; - char* endPtr = nullptr; - GMLReal value = GMLReal_strtod(str, &endPtr); - if (endPtr == str) return false; - *out = value; - return true; -} - -static void handleCmp(VMContext* ctx, uint32_t instr) { - uint8_t cmpKind = instrCmpKind(instr); - RValue b = stackPop(ctx); - RValue a = stackPop(ctx); - - bool result; - if (a.type == RVALUE_UNDEFINED || b.type == RVALUE_UNDEFINED) { - // Undefined is only == to undefined - bool eq = a.type == b.type; - switch (cmpKind) { - case CMP_EQ: result = eq; break; - case CMP_NEQ: result = !eq; break; - default: result = false; break; - } - } else if (a.type == RVALUE_ARRAY || b.type == RVALUE_ARRAY) { - // Array is only == to the same array - bool eq = (a.type == RVALUE_ARRAY && b.type == RVALUE_ARRAY) && (a.array == b.array); - switch (cmpKind) { - case CMP_EQ: result = eq; break; - case CMP_NEQ: result = !eq; break; - default: result = false; break; - } -#if IS_BC17_OR_HIGHER_ENABLED - } else if (a.type == RVALUE_METHOD || b.type == RVALUE_METHOD) { - // Method is only == to the same method - bool eq = (a.type == RVALUE_METHOD && b.type == RVALUE_METHOD) && (a.method == b.method); - switch (cmpKind) { - case CMP_EQ: result = eq; break; - case CMP_NEQ: result = !eq; break; - default: result = false; break; - } -#endif - } else if (a.type == RVALUE_STRUCT || b.type == RVALUE_STRUCT) { - // Struct is only == to the same struct (identity comparison) - bool eq = (a.type == RVALUE_STRUCT && b.type == RVALUE_STRUCT) && (a.structInst == b.structInst); - switch (cmpKind) { - case CMP_EQ: result = eq; break; - case CMP_NEQ: result = !eq; break; - default: result = false; break; - } - } else if (a.type == RVALUE_STRING && b.type == RVALUE_STRING) { - int cmp = strcmp(a.string != nullptr ? a.string : "", b.string != nullptr ? b.string : ""); - switch (cmpKind) { - case CMP_LT: result = 0 > cmp; break; - case CMP_LTE: result = 0 >= cmp; break; - case CMP_EQ: result = cmp == 0; break; - case CMP_NEQ: result = cmp != 0; break; - case CMP_GTE: result = cmp >= 0; break; - case CMP_GT: result = cmp > 0; break; - default: result = false; break; - } - } else { - // Mixed string/number: coerce strings to reals (matching GameMaker-HTML5 yyCompareVal). - // Don't be fooled, this behavior is not a GameMaker-HTML5 (JavaScript) quirk! Some GameMaker games do use this, - // such as gml_Object_obj_ch2_scene6_Step_0 in DELTARUNE: Chapter 2, where the c_wait uses a string instead of a number - // - // If a string side fails to parse as a number, the values are considered incomparable: false for all comparisons except NEQ. - bool incomparable = false; - GMLReal da = 0.0; - GMLReal db = 0.0; - if (a.type == RVALUE_STRING) { - if (!tryParseRealFromString(a.string, &da)) incomparable = true; - } else { - da = RValue_toReal(a); - } - if (!incomparable) { - if (b.type == RVALUE_STRING) { - if (!tryParseRealFromString(b.string, &db)) incomparable = true; - } else { - db = RValue_toReal(b); - } - } - - if (incomparable) { - switch (cmpKind) { - case CMP_EQ: result = false; break; - case CMP_NEQ: result = true; break; - default: result = false; break; - } - } else { - GMLReal diff = da - db; - // GML uses epsilon-based comparison for all numeric CMP operations - int cmp = GMLReal_fabs(diff) <= GML_MATH_EPSILON ? 0 : (diff < 0 ? -1 : 1); - switch (cmpKind) { - case CMP_LT: result = cmp < 0; break; - case CMP_LTE: result = cmp <= 0; break; - case CMP_EQ: result = cmp == 0; break; - case CMP_NEQ: result = cmp != 0; break; - case CMP_GTE: result = cmp >= 0; break; - case CMP_GT: result = cmp > 0; break; - default: result = false; break; - } - } - } - - RValue_free(&a); - RValue_free(&b); - stackPush(ctx,RValue_makeBool(result)); -} - -#if IS_BC17_OR_HIGHER_ENABLED -// Converts a native byte count to RValue slot count by walking the stack backwards from a given position. -// Only used by BC17+ Dup paths; reads the per-slot gmlStackType which doesn't exist on BC16-only builds. -static int32_t bytesToSlotCount(VMContext* ctx, int32_t nativeBytes, int32_t stackPos) { - int32_t slots = 0; - int32_t remaining = nativeBytes; - while (remaining > 0) { - slots++; - require(stackPos >= slots); - uint8_t slotGmlType = ctx->stack.slots[stackPos - slots].gmlStackType; - remaining -= gmlTypeNativeSize(slotGmlType); - } - require(remaining == 0); // Byte count must align exactly to slot boundaries - return slots; -} -#endif - -static void handleDup(VMContext* ctx, uint32_t instr) { - uint16_t operand = (uint16_t)(instr & 0xFFFF); -#if IS_BC17_OR_HIGHER_ENABLED - uint8_t type1 = instrType1(instr); - int32_t typeSize = gmlTypeNativeSize(type1); - - // Swap mode: bit 15 of operand is set - // The Dup instruction doubles as a stack rotation when bit 15 is set. - // It takes the top N items and moves them below the next M items. - // Bits 0-10: top group size (in native type units) - // Bits 11-14: bottom group size (in native type units) - if (IS_BC17_OR_HIGHER(ctx) && (operand & 0x8000) != 0) { - int32_t topNativeCount = operand & 0x7FF; - int32_t bottomNativeCount = (operand >> 11) & 0xF; - int32_t topBytes = topNativeCount * typeSize; - int32_t bottomBytes = bottomNativeCount * typeSize; - - // Convert byte counts to slot counts - int32_t topSlots = bytesToSlotCount(ctx, topBytes, ctx->stack.top); - int32_t bottomSlots = bytesToSlotCount(ctx, bottomBytes, ctx->stack.top - topSlots); - - int32_t totalSlots = topSlots + bottomSlots; - int32_t baseIdx = ctx->stack.top - totalSlots; - - // Save top group to temp - RValue temp[32]; - for (int32_t i = 0; topSlots > i; i++) { - temp[i] = ctx->stack.slots[ctx->stack.top - topSlots + i]; - } - - // Shift bottom group up to where top group was - for (int32_t i = bottomSlots - 1; i >= 0; i--) { - ctx->stack.slots[baseIdx + topSlots + i] = ctx->stack.slots[baseIdx + i]; - } - - // Place top group at the bottom - for (int32_t i = 0; topSlots > i; i++) { - ctx->stack.slots[baseIdx + i] = temp[i]; - } - return; - } -#endif - - // Normal dup mode - int32_t count; - -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx)) { - // In bytecode 17+, the operand encodes a native element count: total bytes = (operand + 1) * typeSize(type1). - // The native runner's stack stores raw bytes (int=4, double=8, variable=16), but our VM uses uniform RValue slots. - // We walk backward through the stack, summing each slot's native size (tracked via gmlStackType), to find how many slots correspond to the byte count. - int32_t totalBytes = ((int32_t)(operand & 0x7FFF) + 1) * typeSize; - - count = bytesToSlotCount(ctx, totalBytes, ctx->stack.top); - } else { - // Bytecode 16: operand directly encodes how many additional items beyond 1 to duplicate (dup.i 0 = duplicate 1 item, dup.i 1 = duplicate 2 items, etc) - count = (int32_t)(operand & 0xFF) + 1; - require(ctx->stack.top >= count); - } -#else - // Bytecode 16: operand directly encodes how many additional items beyond 1 to duplicate - count = (int32_t)(operand & 0xFF) + 1; - require(ctx->stack.top >= count); -#endif - - // Copy 'count' items from the top of the stack (preserving order) - int32_t startIdx = ctx->stack.top - count; - for (int32_t i = 0; count > i; i++) { - RValue copy = ctx->stack.slots[startIdx + i]; - - // If the value owns a string, duplicate it to avoid double-free. - // For arrays and methods, bump the refcount so each duplicate independently owns a reference. - if (copy.type == RVALUE_STRING && copy.ownsReference && copy.string != nullptr) { - copy.string = safeStrdup(copy.string); - } else if (copy.type == RVALUE_ARRAY && copy.ownsReference && copy.array != nullptr) { - GMLArray_incRef(copy.array); -#if IS_BC17_OR_HIGHER_ENABLED - } else if (copy.type == RVALUE_METHOD && copy.ownsReference && copy.method != nullptr) { - GMLMethod_incRef(copy.method); -#endif - } else if (copy.type == RVALUE_STRUCT && copy.ownsReference && copy.structInst != nullptr) { - Instance_structIncRef(copy.structInst); - } - - stackPush(ctx, copy); - } -} - -// ===[ Function Call Handler ]=== - -static void handleCall(VMContext* ctx, uint32_t instr, const uint8_t* extraData) { - int32_t argCount = instr & 0xFFFF; - uint32_t funcIndex = resolveFuncOperand(extraData); - require(ctx->dataWin->func.functionCount > funcIndex); - - // Pop arguments from stack (args pushed right-to-left, so first arg is on top) - // Use stack-allocated buffer for small arg counts (GMS 1.4 supports up to 16 arguments) - RValue stackArgs[GML_MAX_ARGUMENTS]; - RValue* args = nullptr; - if (argCount > 0) { - args = (GML_MAX_ARGUMENTS >= argCount) ? stackArgs : safeMalloc(argCount * sizeof(RValue)); - repeat(argCount, i) { - args[i] = stackPop(ctx); - } - } - -#ifdef ENABLE_VM_TRACING - const char* funcName = ctx->dataWin->func.functions[funcIndex].name; - bool functionIsBeingTraced = shgeti(ctx->functionCallsToBeTraced, "*") != -1 || shgeti(ctx->functionCallsToBeTraced, funcName) != -1 || shgeti(ctx->functionCallsToBeTraced, ctx->currentCodeName) != -1; - char* functionArgumentList = nullptr; - if (functionIsBeingTraced) { - functionArgumentList = safeStrdup(""); - for (int32_t i = 0; i < argCount; i++) { - char* display = RValue_toStringFancy(args[i]); - - if (i > 0) { - char* tmp = safeMalloc(strlen(functionArgumentList) + 2 + strlen(display) + 1); - sprintf(tmp, "%s, %s", functionArgumentList, display); - free(functionArgumentList); - functionArgumentList = tmp; - } else { - free(functionArgumentList); - functionArgumentList = safeStrdup(display); - } - free(display); - } - - fprintf(stderr, "VM: [%s] Calling function \"%s(%s)\"\n", ctx->currentCodeName, funcName, functionArgumentList); - } -#endif - - // Use cached function resolution to avoid per-call string hash lookups - FuncCallCache* cache = &ctx->funcCallCache[funcIndex]; - - // Fast path: cached builtin function pointer - if (cache->builtin != nullptr) { - BuiltinFunc builtin = (BuiltinFunc) cache->builtin; - RValue result = builtin(ctx, args, argCount); - // Free arguments - if (args != nullptr) { - repeat(argCount, i) { - RValue_free(&args[i]); - } - if (args != stackArgs) free(args); - } - -#ifdef ENABLE_VM_TRACING - if (functionIsBeingTraced) { - char* returnValueAsString = RValue_toStringFancy(result); - fprintf(stderr, "VM: [%s] Built-in function \"%s(%s)\" returned %s\n", ctx->currentCodeName, funcName, functionArgumentList, returnValueAsString); - free(returnValueAsString); - free(functionArgumentList); - } -#endif - - stackPushTyped(ctx, result, GML_TYPE_VARIABLE); - return; - } - - // Fast path: cached script code index - if (cache->scriptCodeIndex >= 0) { - RValue result = VM_callCodeIndex(ctx, cache->scriptCodeIndex, args, argCount); - -#ifdef ENABLE_VM_TRACING - if (functionIsBeingTraced) { - char* returnValueAsString = RValue_toStringFancy(result); - fprintf(stderr, "VM: [%s] Script function \"%s(%s)\" returned %s\n", ctx->currentCodeName, funcName, functionArgumentList, returnValueAsString); - free(returnValueAsString); - free(functionArgumentList); - } -#endif - - // Free arguments (VM_callCodeIndex copies what it needs) - if (args != nullptr) { - repeat(argCount, i) { - RValue_free(&args[i]); - } - if (args != stackArgs) free(args); - } - - stackPushTyped(ctx, result, GML_TYPE_VARIABLE); - return; - } - - // Slow path: unknown function (not cached as builtin or script) -#ifdef ENABLE_VM_STUB_LOGS - const char* unknownFuncName = ctx->dataWin->func.functions[funcIndex].name; - - // Log once per (callingCode, funcName) pair - const char* callerName = VM_getCallerName(ctx); - char* dedupKey = VM_createDedupKey(callerName, unknownFuncName); - - if (ctx->alwaysLogUnknownFunctions || 0 > shgeti(ctx->loggedUnknownFuncs, dedupKey)) { - shput(ctx->loggedUnknownFuncs, dedupKey, true); - fprintf(stderr, "VM: [%s] Unknown function \"%s\"!\n", callerName, unknownFuncName); - } else { - free(dedupKey); - } -#endif - - // Free arguments and push undefined - if (args != nullptr) { - repeat(argCount, i) { - RValue_free(&args[i]); - } - if (args != stackArgs) free(args); - } - -#ifdef ENABLE_VM_TRACING - if (functionIsBeingTraced) { - free(functionArgumentList); - } -#endif - - stackPush(ctx, RValue_makeUndefined()); -} - -#if IS_BC17_OR_HIGHER_ENABLED -// BC17+ CALLV: dynamic call through a variable (method/script reference). -// Stack layout (top -> bottom): function, instance, arg[N-1], ..., arg[0] -// argCount is in the low 16 bits of the instruction. -static void handleCallV(VMContext* ctx, uint32_t instr) { - int32_t argCount = instr & 0xFFFF; - - RValue function = stackPop(ctx); - RValue instance = stackPop(ctx); - - RValue stackArgs[GML_MAX_ARGUMENTS]; - RValue* args = nullptr; - if (argCount > 0) { - args = (GML_MAX_ARGUMENTS >= argCount) ? stackArgs : safeMalloc(argCount * sizeof(RValue)); - repeat(argCount, i) { - args[i] = stackPop(ctx); - } - } - - int32_t codeIndex = -1; - int32_t boundInstance = -1; - BuiltinFunc builtin = nullptr; - const char* unresolvedName = nullptr; - if (function.type == RVALUE_METHOD && function.method != nullptr) { - codeIndex = function.method->codeIndex; - boundInstance = function.method->boundInstanceId; - builtin = (BuiltinFunc) function.method->builtin; - unresolvedName = function.method->unresolvedName; - } - - // Decide target self: prefer method's bound instance, else the stack-provided instance. - int32_t targetInstance = (boundInstance > 0) ? boundInstance : RValue_toInt32(instance); - Instance* savedSelf = ctx->currentInstance; - if (targetInstance != INSTANCE_SELF && targetInstance != 0) { - Instance* target = findInstanceByTarget(ctx, targetInstance); - if (target != nullptr) ctx->currentInstance = target; - } - - RValue result; - if (codeIndex >= 0 && ctx->dataWin->code.count > (uint32_t) codeIndex) { - result = VM_callCodeIndex(ctx, codeIndex, args, argCount); - } else if (builtin != nullptr) { - result = builtin(ctx, args, argCount); - } else if (unresolvedName != nullptr) { -#ifdef ENABLE_VM_STUB_LOGS - const char* callerName = VM_getCallerName(ctx); - char* dedupKey = VM_createDedupKey(callerName, unresolvedName); - if (ctx->alwaysLogUnknownFunctions || 0 > shgeti(ctx->loggedUnknownFuncs, dedupKey)) { - shput(ctx->loggedUnknownFuncs, dedupKey, true); - fprintf(stderr, "VM: [%s] Unknown function \"%s\"! (via CallV)\n", callerName, unresolvedName); - } else { - free(dedupKey); - } -#endif - result = RValue_makeUndefined(); - } else { - fprintf(stderr, "VM: [%s] CALLV with unresolvable function reference (type=%d, codeIndex=%d)\n", ctx->currentCodeName, function.type, codeIndex); - result = RValue_makeUndefined(); - } - - ctx->currentInstance = savedSelf; - - RValue_free(&function); - RValue_free(&instance); - if (args != nullptr) { - repeat(argCount, i) { - RValue_free(&args[i]); - } - if (args != stackArgs) free(args); - } - - stackPushTyped(ctx, result, GML_TYPE_VARIABLE); -} -#endif - -// ===[ With-Statement Helpers (PushEnv/PopEnv) ]=== - -// Checks if objectIndex is or inherits from targetObjectIndex by walking the parent chain. -bool VM_isObjectOrDescendant(DataWin* dataWin, int32_t objectIndex, int32_t targetObjectIndex) { - int32_t currentObj = objectIndex; - int depth = 0; - while (currentObj >= 0 && (uint32_t) currentObj < dataWin->objt.count && 32 > depth) { - if (currentObj == targetObjectIndex) return true; - currentObj = dataWin->objt.objects[currentObj].parentId; - depth++; - } - return false; -} - - -// Sets the VM instance context from an Instance. -static void switchToInstance(VMContext* ctx, Instance* inst) { - ctx->currentInstance = inst; -} - -// Restores VM context from an EnvFrame's saved fields. -static void restoreEnvContext(VMContext* ctx, EnvFrame* frame) { - ctx->currentInstance = frame->savedInstance; - ctx->otherInstance = frame->savedOtherInstance; -} - -static void handlePushEnv(VMContext* ctx, uint32_t instr, uint32_t instrAddr) { - int32_t jumpOffset = instrJumpOffset(instr); - - // Pop target from stack - int32_t target = stackPopInt32(ctx); - // BC17: -9 (INSTANCE_STACKTOP) means "pop again for the real target" - if (IS_BC17_OR_HIGHER(ctx) && target == INSTANCE_STACKTOP) { - target = resolveInstanceStackTop(ctx); - } - - // Create env frame, save current context - EnvFrame* frame = safeMalloc(sizeof(EnvFrame)); - frame->savedInstance = (Instance*) ctx->currentInstance; - frame->savedOtherInstance = (Instance*) ctx->otherInstance; - frame->instanceList = nullptr; - frame->currentIndex = 0; - frame->parent = ctx->envStack; - ctx->envStack = frame; - - // Inside a with-block, "other" refers to the instance that executed the with-statement - ctx->otherInstance = (Instance*) ctx->currentInstance; - - Runner* runner = (Runner*) ctx->runner; - - if (target == INSTANCE_SELF) { - // with(self) - no-op, keep current instance - return; - } - - if (target == INSTANCE_OTHER) { - // with(other) - switch to the instance that was "self" before the nearest enclosing with-block - // For nested with-blocks, other refers to the saved instance from the parent env frame - if (frame->parent != nullptr) { - switchToInstance(ctx, frame->parent->savedInstance); - } else if (ctx->otherInstance != nullptr) { - // No parent env frame, but we have an otherInstance (e.g., from collision events) - switchToInstance(ctx, (Instance*) ctx->otherInstance); - } - // If no parent frame and no otherInstance, keep the saved instance (no-op) - return; - } - - if (target == INSTANCE_NOONE) { - // with(noone) - skip the block entirely - ctx->ip = instrAddr + jumpOffset; - return; - } - - if (target == INSTANCE_ALL) { - // with(all) - iterate over all active instances - int32_t instanceCount = (int32_t) arrlen(runner->instances); - for (int32_t i = 0; instanceCount > i; i++) { - Instance* inst = runner->instances[i]; - if (inst->active) { - arrput(frame->instanceList, inst); - } - } - - if (arrlen(frame->instanceList) == 0) { - // No active instances, skip the block - ctx->ip = instrAddr + jumpOffset; - return; - } - - frame->currentIndex = 0; - switchToInstance(ctx, frame->instanceList[0]); - return; - } - - if (target >= 0 && 100000 > target) { - // Object index - copy the descendant-inclusive list for this object into the frame's own list. frame->instanceList has with-block lifetime (not the snapshot arena's loop lifetime), so we don't use the forEach macro; we just copy directly and filter "active" to match prior semantics (deactivated instances are skipped). - if (ctx->dataWin->objt.count > (uint32_t) target) { - Instance** source = runner->instancesByObject[target]; - int32_t sourceCount = (int32_t) arrlen(source); - for (int32_t i = 0; sourceCount > i; i++) { - Instance* inst = source[i]; - if (inst->active) arrput(frame->instanceList, inst); - } - } - - if (arrlen(frame->instanceList) == 0) { - // No matching instances, skip the block - ctx->ip = instrAddr + jumpOffset; - return; - } - - frame->currentIndex = 0; - switchToInstance(ctx, frame->instanceList[0]); - return; - } - - if (target >= 100000) { - // Instance ID - find specific instance - Instance* inst = hmget(runner->instancesById, target); - if (inst != nullptr && inst->active) { - switchToInstance(ctx, inst); - return; - } - - // Instance not found, skip the block - ctx->ip = instrAddr + jumpOffset; - return; - } - - fprintf(stderr, "VM: [%s] PushEnv with unhandled target %d\n", ctx->currentCodeName, target); - ctx->ip = instrAddr + jumpOffset; -} - -static void handlePopEnv(VMContext* ctx, uint32_t instr, uint32_t instrAddr) { - EnvFrame* frame = ctx->envStack; - require(frame != nullptr); - - // Check for exit magic: PopEnv with 0xF00000 operand means "unwind env stack and exit/return" - if ((instr & 0x00FFFFFF) == 0xF00000) { - // Restore context and pop frame - restoreEnvContext(ctx, frame); - ctx->envStack = frame->parent; - arrfree(frame->instanceList); - free(frame); - return; - } - - // Check if there are more instances to iterate - if (frame->instanceList != nullptr && arrlen(frame->instanceList) > frame->currentIndex + 1) { - frame->currentIndex++; - Instance* nextInst = frame->instanceList[frame->currentIndex]; - // Skip destroyed instances - while (!nextInst->active && arrlen(frame->instanceList) > frame->currentIndex + 1) { - frame->currentIndex++; - nextInst = frame->instanceList[frame->currentIndex]; - } - if (nextInst->active) { - switchToInstance(ctx, nextInst); - // Jump back to the start of the with-block body - int32_t jumpOffset = instrJumpOffset(instr); - ctx->ip = instrAddr + jumpOffset; - return; - } - } - - // Done iterating - restore context and pop frame - restoreEnvContext(ctx, frame); - ctx->envStack = frame->parent; - arrfree(frame->instanceList); - free(frame); -} - -// ===[ Execution Loop ]=== - -static const char* opcodeName(uint8_t opcode) { - switch (opcode) { - case OP_CONV: return "Conv"; - case OP_MUL: return "Mul"; - case OP_DIV: return "Div"; - case OP_REM: return "Rem"; - case OP_MOD: return "Mod"; - case OP_ADD: return "Add"; - case OP_SUB: return "Sub"; - case OP_AND: return "And"; - case OP_OR: return "Or"; - case OP_XOR: return "Xor"; - case OP_NEG: return "Neg"; - case OP_NOT: return "Not"; - case OP_SHL: return "Shl"; - case OP_SHR: return "Shr"; - case OP_CMP: return "Cmp"; - case OP_POP: return "Pop"; - case OP_PUSHI: return "PushI"; - case OP_DUP: return "Dup"; - case OP_RET: return "Ret"; - case OP_EXIT: return "Exit"; - case OP_POPZ: return "Popz"; - case OP_B: return "B"; - case OP_BT: return "BT"; - case OP_BF: return "BF"; - case OP_PUSHENV: return "PushEnv"; - case OP_POPENV: return "PopEnv"; - case OP_PUSH: return "Push"; - case OP_PUSHLOC: return "PushLoc"; - case OP_PUSHGLB: return "PushGlb"; - case OP_PUSHBLTN:return "PushBltn"; - case OP_CALL: return "Call"; - case OP_CALLV: return "CallV"; - case OP_BREAK: return "Break"; - default: return "???"; - } -} - -#ifdef ENABLE_VM_OPCODE_PROFILER -static char gmlTypeChar(uint8_t type); - -static const char* rvalueTypeName(uint8_t type) { - switch (type) { - case RVALUE_REAL: return "REAL"; - case RVALUE_STRING: return "STRING"; - case RVALUE_INT32: return "INT32"; - case RVALUE_INT64: return "INT64"; - case RVALUE_BOOL: return "BOOL"; - case RVALUE_UNDEFINED: return "UNDEF"; - case RVALUE_ARRAY: return "ARRAY"; - case RVALUE_METHOD: return "METHOD"; - case RVALUE_STRUCT: return "STRUCT"; - case 0xF: return "-"; - default: return "???"; - } -} - -static const char* breakSubOpName(int16_t breakType) { - switch (breakType) { - case BREAK_CHKINDEX: return "chkindex"; - case BREAK_PUSHAF: return "pushaf"; - case BREAK_POPAF: return "popaf"; - case BREAK_PUSHAC: return "pushac"; - case BREAK_SETOWNER: return "setowner"; - case BREAK_ISSTATICOK: return "isstaticok"; - case BREAK_SETSTATIC: return "setstatic"; - case BREAK_SAVEAREF: return "savearef"; - case BREAK_RESTOREAREF: return "restorearef"; - default: return "???"; - } -} - -void VM_printOpcodeProfilerReport(const VMContext* ctx) { - if (!ctx->opcodeProfilerEnabled) return; - - typedef struct { uint16_t key; uint64_t count; } CountEntry; - CountEntry entries[256]; - int entryCount = 0; - uint64_t total = 0; - for (int i = 0; 256 > i; i++) { - if (ctx->opcodeCounts[i] > 0) { - entries[entryCount].key = (uint16_t) i; - entries[entryCount].count = ctx->opcodeCounts[i]; - entryCount++; - total += ctx->opcodeCounts[i]; - } - } - - // Simple insertion sort (max 256 entries, runs once at shutdown) - for (int i = 1; entryCount > i; i++) { - CountEntry tmp = entries[i]; - int j = i; - while (j > 0 && entries[j - 1].count < tmp.count) { - entries[j] = entries[j - 1]; - j--; - } - entries[j] = tmp; - } - - fprintf(stderr, "=== Opcode Profiler Report ===\n"); - fprintf(stderr, "Total instructions executed: %llu\n", (unsigned long long) total); - fprintf(stderr, "%-12s %-6s %16s %8s\n", "Opcode", "Hex", "Count", "Pct"); - forEachIndexed(CountEntry, entry, i, entries, entryCount) { - (void) i; - double pct = total > 0 ? (100.0 * (double) entry->count / (double) total) : 0.0; - fprintf(stderr, "%-12s 0x%02X %16llu %7.2f%%\n", opcodeName((uint8_t) entry->key), (uint8_t) entry->key, (unsigned long long) entry->count, pct); - } - - // Per-opcode breakdown by type variant. Sorted within each opcode by count desc. - fprintf(stderr, "\n--- Type variant breakdown (per opcode) ---\n"); - forEachIndexed(CountEntry, entry, idx, entries, entryCount) { - (void) idx; - uint8_t opcode = (uint8_t) entry->key; - const uint64_t* variants = &ctx->opcodeVariantCounts[opcode * 256]; - - CountEntry variantEntries[256]; - int variantCount = 0; - for (int t = 0; 256 > t; t++) { - if (variants[t] > 0) { - variantEntries[variantCount].key = (uint16_t) t; - variantEntries[variantCount].count = variants[t]; - variantCount++; - } - } - for (int i = 1; variantCount > i; i++) { - CountEntry tmp = variantEntries[i]; - int j = i; - while (j > 0 && variantEntries[j - 1].count < tmp.count) { - variantEntries[j] = variantEntries[j - 1]; - j--; - } - variantEntries[j] = tmp; - } - - fprintf(stderr, "%s (0x%02X): %llu total\n", opcodeName(opcode), opcode, (unsigned long long) entry->count); - forEachIndexed(CountEntry, ve, vi, variantEntries, variantCount) { - (void) vi; - uint8_t type1 = (uint8_t) ((ve->key >> 4) & 0xF); - uint8_t type2 = (uint8_t) (ve->key & 0xF); - double vpct = entry->count > 0 ? (100.0 * (double) ve->count / (double) entry->count) : 0.0; - fprintf(stderr, " .%c.%c %16llu %7.2f%%\n", gmlTypeChar(type1), gmlTypeChar(type2), (unsigned long long) ve->count, vpct); - } - - // Runtime RValue type breakdown (a, b types observed at execution time) - { - const uint64_t* rvCounts = &ctx->opcodeRValueTypeCounts[opcode * 256]; - CountEntry rvEntries[256]; - int rvCount = 0; - uint64_t rvTotal = 0; - for (int t = 0; 256 > t; t++) { - if (rvCounts[t] > 0) { - rvEntries[rvCount].key = (uint16_t) t; - rvEntries[rvCount].count = rvCounts[t]; - rvCount++; - rvTotal += rvCounts[t]; - } - } - if (rvCount > 0) { - for (int i = 1; rvCount > i; i++) { - CountEntry tmp = rvEntries[i]; - int j = i; - while (j > 0 && rvEntries[j - 1].count < tmp.count) { - rvEntries[j] = rvEntries[j - 1]; - j--; - } - rvEntries[j] = tmp; - } - fprintf(stderr, " -- runtime types (a, b):\n"); - forEachIndexed(CountEntry, re, ri, rvEntries, rvCount) { - (void) ri; - uint8_t typeA = (uint8_t) ((re->key >> 4) & 0xF); - uint8_t typeB = (uint8_t) (re->key & 0xF); - double rpct = rvTotal > 0 ? (100.0 * (double) re->count / (double) rvTotal) : 0.0; - fprintf(stderr, " (%-6s, %-6s) %16llu %7.2f%%\n", rvalueTypeName(typeA), rvalueTypeName(typeB), (unsigned long long) re->count, rpct); - } - } - } - - // Extended BREAK (0xFF) sub-opcode breakdown - if (opcode == OP_BREAK) { - CountEntry breakEntries[64]; - int breakCount = 0; - for (int i = 0; 64 > i; i++) { - if (ctx->breakSubOpCounts[i] > 0) { - breakEntries[breakCount].key = (uint16_t) i; - breakEntries[breakCount].count = ctx->breakSubOpCounts[i]; - breakCount++; - } - } - for (int i = 1; breakCount > i; i++) { - CountEntry tmp = breakEntries[i]; - int j = i; - while (j > 0 && breakEntries[j - 1].count < tmp.count) { - breakEntries[j] = breakEntries[j - 1]; - j--; - } - breakEntries[j] = tmp; - } - fprintf(stderr, " -- sub-opcodes:\n"); - forEachIndexed(CountEntry, be, bi, breakEntries, breakCount) { - (void) bi; - int16_t breakType = (int16_t) -((int) be->key); - double bpct = entry->count > 0 ? (100.0 * (double) be->count / (double) entry->count) : 0.0; - fprintf(stderr, " %-12s (%4d) %16llu %7.2f%%\n", breakSubOpName(breakType), (int) breakType, (unsigned long long) be->count, bpct); - } - } - } - fprintf(stderr, "==============================\n"); -} -#endif // ENABLE_VM_OPCODE_PROFILER - -// Forward declaration for formatInstruction (defined in disassembler section, used by trace-opcodes) -static void formatInstruction(VMContext* ctx, const uint8_t* bytecodeBase, uint32_t instrAddr, uint32_t instr, const uint8_t* extraData, char* opcodeStr, size_t opcodeSize, char* operandStr, size_t operandSize, char* commentStr, size_t commentSize); - -#if IS_BC17_OR_HIGHER_ENABLED -// ===[ BREAK sub-opcode handlers (BC17+) ]=== - -static void handleBreakChkIndex(VMContext* ctx, uint32_t instrAddr) { - // Validate top-of-stack array index is in [0, 32000) - RValue* top = stackPeek(ctx); - int32_t idx = RValue_toInt32(*top); - if (0 > idx || 32000 <= idx) { - fprintf(stderr, "VM: chkindex out of bounds: %d at offset %u in %s\n", idx, instrAddr, ctx->currentCodeName); - abort(); - } -} - -static void handleBreakPushAF(VMContext* ctx) { - // Pop index + array ref, push array[index]. Array ref is a weak RVALUE_ARRAY pointer. - int32_t idx = stackPopInt32(ctx); - RValue arrayRef = stackPop(ctx); - RValue result; - RValue* cell = arrayRef.type == RVALUE_ARRAY ? GMLArray_slot(arrayRef.array, idx) : nullptr; - if (cell != nullptr) { - result = *cell; - result.ownsReference = false; // weak view - } else { - result = (RValue){ .type = RVALUE_UNDEFINED }; - } - stackPush(ctx, result); - RValue_free(&arrayRef); -} - -static void handleBreakPopAF(VMContext* ctx) { - // Pop index + array ref + value, store value at array[index]. - // CoW via VM_arrayWriteAt requires a slot pointer, since the stack-held arrayRef is a weak view, the real slot is whatever variable holds this array. - // We can't easily recover the slot here, so we write directly into the array (no CoW fork at this level, fork already happened when the top-level variable was first written, or on a PUSHAC materialisation). - // Assert the array is uniquely-owned or matches the current scope owner. A mismatch here means a shared/aliased array is about to be mutated in place, which silently breaks CoW semantics. BC17+ default mode (pass by reference) is expected to satisfy this since fork already happened at the top-level write. If this fires, a CoW path upstream failed to fork. - int32_t idx = stackPopInt32(ctx); - RValue arrayRef = stackPop(ctx); - RValue value = stackPop(ctx); - if (arrayRef.type == RVALUE_ARRAY && arrayRef.array != nullptr && idx >= 0) { - GMLArray* arr = arrayRef.array; - requireMessage(arr->refCount == 1 || arr->owner == ctx->currentArrayOwner, "BREAK_POPAF: Writing through shared/aliased array without prior CoW fork"); - GMLArray_growTo(arr, idx + 1); - storeIntoArraySlot(GMLArray_slot(arr, idx), value); - } - RValue_free(&arrayRef); - RValue_free(&value); -} - -static void handleBreakPushAC(VMContext* ctx, uint32_t instrAddr) { - // Pop index + parent array ref, push sub-array at parent[index]. Materialise a fresh sub-array if the slot isn't already an RVALUE_ARRAY (multi-dim auto-init). - int32_t idx = stackPopInt32(ctx); - RValue arrayRef = stackPop(ctx); - if (arrayRef.type != RVALUE_ARRAY || arrayRef.array == nullptr) { - fprintf(stderr, "VM: pushac on non-array (type=%d) at offset %u in %s\n", arrayRef.type, instrAddr, ctx->currentCodeName); - abort(); - } - GMLArray* parent = arrayRef.array; - GMLArray_growTo(parent, idx + 1); - RValue* parentSlot = GMLArray_slot(parent, idx); - if (parentSlot->type != RVALUE_ARRAY || parentSlot->array == nullptr) { - RValue_free(parentSlot); - GMLArray* sub = GMLArray_create(0); - sub->owner = parent->owner; - *parentSlot = (RValue){ .array = sub, .type = RVALUE_ARRAY, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; - } - stackPush(ctx, RValue_makeArrayWeak(parentSlot->array)); - RValue_free(&arrayRef); -} - -static void handleBreakSetOwner(VMContext* ctx) { - // CoW scope owner for BC17+. - // The bytecode emits this at the top of each script or event, passing a token (usually self-instance ID cast to int) that uniquely identifies the current scope. - // Arrays whose .owner doesn't match fork on write. - RValue value = stackPop(ctx); - int64_t token = RValue_toInt64(value); - ctx->currentArrayOwner = (void*) (intptr_t) token; - RValue_free(&value); -} - -static void handleBreakIsStaticOk(VMContext* ctx) { - // Push bool: has this function's static block already run? - bool initialized = ctx->staticInitialized[ctx->currentCodeIndex]; - stackPush(ctx, RValue_makeBool(initialized)); -} - -static void handleBreakSetStatic(VMContext* ctx) { - // Mark current function's static as initialized - ctx->staticInitialized[ctx->currentCodeIndex] = true; -} - -static void handleBreakSaveARef(VMContext* ctx) { - // Native 2.3: SAVEAREF does `g_pSavedArraySetContainer = g_pArraySetContainer`, doesn't touch the stack. - // `g_pArraySetContainer` is a runner-global set by PUSHAC when traversing multi-dim parents, used by SET_RValue_Array as the container to write into. - // Since our PUSHAC pushes the sub-array directly onto the VM stack instead of stashing it in a container, this is a no-op. - // - // To track if we are doing everything correct, we'll track the savearefBalance to figure out when a game does something wrong. - ctx->savearefBalance++; -} - -static void handleBreakRestoreARef(VMContext* ctx) { - // Native 2.3: restores `g_pArraySetContainer` from the saved slot. No-op here (see BREAK_SAVEAREF). - // A negative balance means RESTOREAREF was emitted without a matching SAVEAREF, which means that we are doing things wrong or it is a bytecode pattern that we don't understand. - requireMessage(ctx->savearefBalance > 0, "BREAK_RESTOREAREF without matching SAVEAREF"); - ctx->savearefBalance--; -} - -static void handleBreak(VMContext* ctx, uint32_t instr, uint32_t instrAddr) { - if (IS_BC16_OR_BELOW(ctx)) return; - int16_t breakType = instrInstanceType(instr); - switch (breakType) { - case BREAK_CHKINDEX: handleBreakChkIndex(ctx, instrAddr); break; - case BREAK_PUSHAF: handleBreakPushAF(ctx); break; - case BREAK_POPAF: handleBreakPopAF(ctx); break; - case BREAK_PUSHAC: handleBreakPushAC(ctx, instrAddr); break; - case BREAK_SETOWNER: handleBreakSetOwner(ctx); break; - case BREAK_ISSTATICOK: handleBreakIsStaticOk(ctx); break; - case BREAK_SETSTATIC: handleBreakSetStatic(ctx); break; - case BREAK_SAVEAREF: handleBreakSaveARef(ctx); break; - case BREAK_RESTOREAREF: handleBreakRestoreARef(ctx); break; - default: - fprintf(stderr, "VM: Unknown BREAK sub-opcode %d at offset %u in %s\n", breakType, instrAddr, ctx->currentCodeName); - abort(); - } -} -#endif - -#define VM_SYNC_IP() do { ctx->ip = ip; } while (0) -#define VM_RELOAD_IP() do { ip = ctx->ip; } while (0) - -static RValue executeLoop(VMContext* ctx) { - // codeEnd and bytecodeBase are invariant for the lifetime of this executeLoop call, so let's hoist them to avoid the compiler emitting code to - // reload the values at the end of every iteration. - const uint32_t codeEnd = ctx->codeEnd; - const uint8_t* const bytecodeBase = ctx->bytecodeBase; - // If you just joined the stream: ip is short for instruction pointer chat - // The ip is mutable, so we need to use VM_SYNC_IP and VM_RELOAD_IP every time an opcode handler may access it or write to it - uint32_t ip = ctx->ip; - - // Some opcodes have their handler or parts of their handler inlined - // Those are opcodes that during real gameplay (using "--profile-opcodes") shown that, with inlining and keeping only the frequently called handle parts, we could squeeze MORE performance from the interpreter! - while (codeEnd > ip) { -#ifdef ENABLE_VM_GML_PROFILER - if (ctx->profiler != nullptr) - Profiler_tickInstruction(ctx->profiler); -#endif - uint32_t instrAddr = ip; - uint32_t instr = BinaryUtils_readUint32Aligned(bytecodeBase + ip); - ip += 4; - - // extraData pointer (may not be used depending on opcode) - const uint8_t* extraData = bytecodeBase + ip; - - // If instruction has extra data (bit 30 set), advance IP past it - if (instrHasExtraData(instr)) { - ip += extraDataSize(instrType1(instr)); - } - - uint8_t opcode = instrOpcode(instr); - -#ifdef ENABLE_VM_OPCODE_PROFILER - if (ctx->opcodeProfilerEnabled) { - ctx->opcodeCounts[opcode]++; - ctx->opcodeVariantCounts[opcode * 256 + instrType1(instr) * 16 + instrType2(instr)]++; - if (opcode == OP_BREAK) { - int16_t breakType = instrInstanceType(instr); - int idx = -breakType; - if (idx >= 0 && 64 > idx) { - ctx->breakSubOpCounts[idx]++; - } - } - // Capture actual runtime RValue types for arithmetic/comparison/conversion ops. - // typeB = 0xF sentinel for unary ops (no second operand). - uint8_t rvTypeA = 0xFF, rvTypeB = 0xF; - switch (opcode) { - case OP_MUL: case OP_DIV: case OP_REM: case OP_MOD: - case OP_ADD: case OP_SUB: case OP_AND: case OP_OR: - case OP_XOR: case OP_SHL: case OP_SHR: case OP_CMP: - if (ctx->stack.top >= 2) { - rvTypeA = ctx->stack.slots[ctx->stack.top - 2].type; - rvTypeB = ctx->stack.slots[ctx->stack.top - 1].type; - } - break; - case OP_NEG: case OP_NOT: case OP_CONV: - if (ctx->stack.top >= 1) { - rvTypeA = ctx->stack.slots[ctx->stack.top - 1].type; - } - break; - } - if (rvTypeA != 0xFF) { - ctx->opcodeRValueTypeCounts[opcode * 256 + (rvTypeA & 0xF) * 16 + (rvTypeB & 0xF)]++; - } - } -#endif - -#ifdef ENABLE_VM_TRACING - if (shlen(ctx->opcodesToBeTraced) > 0 && ctx->runner->frameCount >= ctx->traceBytecodeAfterFrame) { - if (shgeti(ctx->opcodesToBeTraced, "*") != -1 || shgeti(ctx->opcodesToBeTraced, ctx->currentCodeName) != -1) { - char opcodeStr[32], operandStr[256] = "", commentStr[128] = ""; - formatInstruction(ctx, ctx->bytecodeBase, instrAddr, instr, extraData, opcodeStr, sizeof(opcodeStr), operandStr, sizeof(operandStr), commentStr, sizeof(commentStr)); - - char* stackBuf = formatStackContents(ctx); - - if (operandStr[0] != '\0') { - fprintf(stderr, "VM: [%s] @%04X [0x%08X] %s %s [stack=%d] %s\n", ctx->currentCodeName, instrAddr, instr, opcodeStr, operandStr, ctx->stack.top, stackBuf); - } else { - fprintf(stderr, "VM: [%s] @%04X [0x%08X] %s [stack=%d] %s\n", ctx->currentCodeName, instrAddr, instr, opcodeStr, ctx->stack.top, stackBuf); - } - free(stackBuf); - } - } -#endif - - switch (opcode) { - // Push instructions - case OP_PUSH: { - uint8_t type1 = instrType1(instr); - // Inline fast paths for variable reads (not ints, doubles, etc, only VARIABLES) that are "normal" type (not arrays, not stacktop, and not the new fangled BC17 array reads) - if (type1 == GML_TYPE_VARIABLE) { - uint32_t varRef = resolveVarOperand(extraData); - uint8_t varType = (uint8_t) ((varRef >> 24) & 0xF8); - if (varType == VARTYPE_NORMAL) { - Variable* varDef = resolveVarDef(ctx, varRef); - if (varDef->varID >= 0) { - int32_t instanceType = (int32_t) instrInstanceType(instr); - RValue val; - if (tryFastVarRead(ctx, instanceType, varDef, &val)) { - stackPushTyped(ctx, val, GML_TYPE_VARIABLE); - break; - } - } - } - } - handlePush(ctx, instr, extraData, type1); - break; - } - case OP_PUSHLOC: { - uint32_t varRef = resolveVarOperand(extraData); -#if IS_BC17_OR_HIGHER_ENABLED - uint8_t varType = (uint8_t) ((varRef >> 24) & 0xF8); - if (varType == VARTYPE_ARRAYPUSHAF || varType == VARTYPE_ARRAYPOPAF) { - Variable* varDef = resolveVarDef(ctx, varRef); - uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); - require(ctx->localVarCount > localSlot); - pushTopLevelArrayRef(ctx, &ctx->localVars[localSlot]); - break; - } -#endif - // Locals are always non-builtin (varID >= 0); inline the read straight from localVars[]. - Variable* varDef = resolveVarDef(ctx, varRef); - uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); - require(ctx->localVarCount > localSlot); - RValue val = ctx->localVars[localSlot]; - val.ownsReference = false; - stackPushTyped(ctx, val, GML_TYPE_VARIABLE); - break; - } - case OP_PUSHGLB: { - uint32_t varRef = resolveVarOperand(extraData); - // Globals are not ALWAYS non-builtin (varID >= 0), some games may use the deprecated global builtins (like "score") with PUSHGLB. - // So due to that, we'll take the slow path if it is a builtin variable. - // The native runner does NOT handle global arrays from this path, so we don't need to care about them. - Variable* varDef = resolveVarDef(ctx, varRef); - if (varDef->varID == -6) { - RValue val = resolveVariableRead(ctx, INSTANCE_GLOBAL, varRef); - stackPushTyped(ctx, val, GML_TYPE_VARIABLE); - break; - } - // Inline the read straight from globalVars[]. - require(ctx->globalVarCount > (uint32_t) varDef->varID); - RValue val = ctx->globalVars[varDef->varID]; - val.ownsReference = false; - stackPushTyped(ctx, val, GML_TYPE_VARIABLE); - break; - } - case OP_PUSHBLTN: - handlePushBltn(ctx, instr, extraData); - break; - case OP_PUSHI: - handlePushI(ctx, instr); - break; - - // Pop instructions - case OP_POP: { - uint8_t type1 = instrType1(instr); - uint32_t varRef = resolveVarOperand(extraData); - uint8_t varType = (uint8_t) ((varRef >> 24) & 0xF8); - int32_t instanceType = instrInstanceType(instr); - // BC17: VARTYPE_INSTANCE encodes (instanceId - 100000) in the instruction's lower 16 bits. - if (varType == VARTYPE_INSTANCE) instanceType += 100000; - int32_t type2 = instrType2(instr); // source type (what's on stack) - if (type1 == GML_TYPE_VARIABLE && varType == VARTYPE_NORMAL) { - // Inline fast path for the simple variable-assignment case: type1==VARIABLE, which is ~99.998% of all Pops in real workloads - RValue val = stackPop(ctx); - val = coerceIntStoreToReal(val, type2); - resolveVariableWrite(ctx, instanceType, varRef, val); - } else { - handlePop(ctx, instr, type1, type2, varRef, varType, instanceType); - } - break; - } - case OP_POPZ: - handlePopz(ctx); - break; - - // Arithmetic - // We keep the number + number operations inlined in executeLoop, keeping only the slow path for string concat/repetition - case OP_ADD: { - RValue* slotA = &ctx->stack.slots[ctx->stack.top - 2]; - RValue* slotB = &ctx->stack.slots[ctx->stack.top - 1]; - uint8_t aType = slotA->type; - uint8_t bType = slotB->type; - if ((aType == RVALUE_INT32 || aType == RVALUE_REAL) && (bType == RVALUE_INT32 || bType == RVALUE_REAL)) { - if (aType == RVALUE_INT32 && bType == RVALUE_INT32) { - slotA->int32 = slotA->int32 + slotB->int32; - } else { - // Read both operands as locals before writing back, since the union means - // slotA->real and slotA->int32 share storage. - GMLReal aVal = (aType == RVALUE_INT32) ? (GMLReal) slotA->int32 : slotA->real; - GMLReal bVal = (bType == RVALUE_INT32) ? (GMLReal) slotB->int32 : slotB->real; - slotA->real = aVal + bVal; - slotA->type = RVALUE_REAL; - } -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx)) slotA->gmlStackType = instrType2(instr); -#endif - ctx->stack.top--; - } else { - uint8_t resultType = instrType2(instr); - RValue b = stackPop(ctx); - RValue a = stackPop(ctx); - if (a.type == RVALUE_STRING || b.type == RVALUE_STRING) { - handleAddString(ctx, a, b, resultType); - break; - } -#ifndef NO_RVALUE_INT64 - if (a.type == RVALUE_INT64 && b.type == RVALUE_INT64) { - stackPushTyped(ctx, RValue_makeInt64(a.int64 + b.int64), resultType); - break; - } -#endif - GMLReal result = RValue_toReal(a) + RValue_toReal(b); - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeReal(result), resultType); - } - break; - } - case OP_SUB: { - RValue* slotA = &ctx->stack.slots[ctx->stack.top - 2]; - RValue* slotB = &ctx->stack.slots[ctx->stack.top - 1]; - uint8_t aType = slotA->type; - uint8_t bType = slotB->type; - if ((aType == RVALUE_INT32 || aType == RVALUE_REAL) && (bType == RVALUE_INT32 || bType == RVALUE_REAL)) { - if (aType == RVALUE_INT32 && bType == RVALUE_INT32) { - slotA->int32 = slotA->int32 - slotB->int32; - } else { - GMLReal aVal = (aType == RVALUE_INT32) ? (GMLReal) slotA->int32 : slotA->real; - GMLReal bVal = (bType == RVALUE_INT32) ? (GMLReal) slotB->int32 : slotB->real; - slotA->real = aVal - bVal; - slotA->type = RVALUE_REAL; - } -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx)) slotA->gmlStackType = instrType2(instr); -#endif - ctx->stack.top--; - } else { - uint8_t resultType = instrType2(instr); - RValue b = stackPop(ctx); - RValue a = stackPop(ctx); -#ifndef NO_RVALUE_INT64 - if (a.type == RVALUE_INT64 && b.type == RVALUE_INT64) { - stackPushTyped(ctx, RValue_makeInt64(a.int64 - b.int64), resultType); - break; - } -#endif - GMLReal result = RValue_toReal(a) - RValue_toReal(b); - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeReal(result), resultType); - } - break; - } - case OP_MUL: { - RValue* slotA = &ctx->stack.slots[ctx->stack.top - 2]; - RValue* slotB = &ctx->stack.slots[ctx->stack.top - 1]; - uint8_t aType = slotA->type; - uint8_t bType = slotB->type; - if ((aType == RVALUE_INT32 || aType == RVALUE_REAL) && (bType == RVALUE_INT32 || bType == RVALUE_REAL)) { - if (aType == RVALUE_INT32 && bType == RVALUE_INT32) { - slotA->int32 = slotA->int32 * slotB->int32; - } else { - GMLReal aVal = (aType == RVALUE_INT32) ? (GMLReal) slotA->int32 : slotA->real; - GMLReal bVal = (bType == RVALUE_INT32) ? (GMLReal) slotB->int32 : slotB->real; - slotA->real = aVal * bVal; - slotA->type = RVALUE_REAL; - } -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx)) slotA->gmlStackType = instrType2(instr); -#endif - ctx->stack.top--; - } else { - uint8_t resultType = instrType2(instr); - RValue b = stackPop(ctx); - RValue a = stackPop(ctx); - if (a.type == RVALUE_STRING) { - handleMulString(ctx, a, b, resultType); - break; - } -#ifndef NO_RVALUE_INT64 - if (a.type == RVALUE_INT64 && b.type == RVALUE_INT64) { - stackPushTyped(ctx, RValue_makeInt64(a.int64 * b.int64), resultType); - break; - } -#endif - GMLReal result = RValue_toReal(a) * RValue_toReal(b); - RValue_free(&a); - RValue_free(&b); - stackPushTyped(ctx, RValue_makeReal(result), resultType); - } - break; - } - case OP_DIV: handleDiv(ctx, instr); break; - case OP_REM: handleRem(ctx, instr); break; - case OP_MOD: handleMod(ctx, instr); break; - - // Bitwise / Logical - case OP_AND: handleAnd(ctx, instr); break; - case OP_OR: handleOr(ctx, instr); break; - case OP_XOR: handleXor(ctx, instr); break; - case OP_SHL: handleShl(ctx, instr); break; - case OP_SHR: handleShr(ctx, instr); break; - - // Unary - case OP_NEG: handleNeg(ctx, instr); break; - case OP_NOT: handleNot(ctx, instr); break; - - // Type conversion - case OP_CONV: { - uint8_t srcType = instrType1(instr); - uint8_t dstType = instrType2(instr); - uint8_t convKey = (uint8_t) ((dstType << 4) | srcType); - RValue* top = &ctx->stack.slots[ctx->stack.top - 1]; - bool fastHit = false; - - // Inline fast paths for the four conversions that account for ~93% of all Conv opcodes in real workloads - switch (convKey) { - case 0x52: // Int32 -> Variable (pure passthrough; just retag stack slot) - fastHit = true; - break; - case 0x45: // Variable -> Bool - if (top->type == RVALUE_INT32) { - top->int32 = top->int32 > 0 ? 1 : 0; - top->type = RVALUE_BOOL; - fastHit = true; - } else if (top->type == RVALUE_BOOL) { - // Already 0/1; nothing to do - fastHit = true; - } else if (top->type == RVALUE_REAL) { - top->int32 = top->real > (GMLReal) 0.5 ? 1 : 0; - top->type = RVALUE_BOOL; - fastHit = true; - } - break; - case 0x25: // Variable -> Int32 - if (top->type == RVALUE_INT32) { - fastHit = true; - } else if (top->type == RVALUE_BOOL) { - top->type = RVALUE_INT32; - fastHit = true; - } else if (top->type == RVALUE_REAL) { - top->int32 = (int32_t) top->real; - top->type = RVALUE_INT32; - fastHit = true; - } - break; - case 0x02: // Int32 -> Double (Real) - top->real = (GMLReal) top->int32; - top->type = RVALUE_REAL; - fastHit = true; - break; - } - - if (fastHit) { -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx)) top->gmlStackType = dstType; -#endif - } else { - handleConv(ctx, srcType, dstType, convKey); - } - break; - } - - // Comparison - case OP_CMP: { - RValue* slotA = &ctx->stack.slots[ctx->stack.top - 2]; - RValue* slotB = &ctx->stack.slots[ctx->stack.top - 1]; - - // Inline fast path for INT32/INT32 - if (slotA->type == RVALUE_INT32 && slotB->type == RVALUE_INT32) { - int32_t a = slotA->int32; - int32_t b = slotB->int32; - bool result; - switch (instrCmpKind(instr)) { - case CMP_LT: result = b > a; break; - case CMP_LTE: result = b >= a; break; - case CMP_EQ: result = a == b; break; - case CMP_NEQ: result = a != b; break; - case CMP_GTE: result = a >= b; break; - case CMP_GT: result = a > b; break; - default: result = false; break; - } - slotA->int32 = result ? 1 : 0; - slotA->type = RVALUE_BOOL; -#if IS_BC17_OR_HIGHER_ENABLED - if (IS_BC17_OR_HIGHER(ctx)) slotA->gmlStackType = GML_TYPE_BOOL; -#endif - ctx->stack.top--; - } else { - handleCmp(ctx, instr); - } - break; - } - - // Duplicate - case OP_DUP: - handleDup(ctx, instr); - break; - - // Branches - // The reason why these (the branches opcodes) are inlined is because they access ctx->ip - // So, because they are short n' sweet, we prefer to keep them inlined to avoid any reloading shenanigans that the compiler may do - case OP_B: { - int32_t offset = instrJumpOffset(instr); - ip = instrAddr + offset; - break; - } - case OP_BT: { - bool condition = stackPopInt32(ctx) != 0; - if (condition == true) { - int32_t offset = instrJumpOffset(instr); - ip = instrAddr + offset; - } - break; - } - case OP_BF: { - bool condition = stackPopInt32(ctx) != 0; - if (condition == false) { - int32_t offset = instrJumpOffset(instr); - ip = instrAddr + offset; - } - break; - } - - // Function call - case OP_CALL: - VM_SYNC_IP(); - handleCall(ctx, instr, extraData); - break; -#if IS_BC17_OR_HIGHER_ENABLED - case OP_CALLV: - VM_SYNC_IP(); - handleCallV(ctx, instr); - break; -#endif - - // Return - case OP_RET: { - RValue retVal = stackPop(ctx); - return retVal; - } - - // Exit (no return value) - case OP_EXIT: - return RValue_makeUndefined(); - - // Environment (with-statements) - case OP_PUSHENV: - VM_SYNC_IP(); - handlePushEnv(ctx, instr, instrAddr); - VM_RELOAD_IP(); - break; - case OP_POPENV: - VM_SYNC_IP(); - handlePopEnv(ctx, instr, instrAddr); - VM_RELOAD_IP(); - break; - - // Break (extended opcodes in V17+, no-op/debug in V16) - case OP_BREAK: -#if IS_BC17_OR_HIGHER_ENABLED - handleBreak(ctx, instr, instrAddr); -#endif - break; - - default: - fprintf(stderr, "VM: Unknown opcode 0x%02X at offset %u\n", opcode, instrAddr); - abort(); - } - } - - return RValue_makeUndefined(); -} - -// ===[ Public API ]=== - -VMContext* VM_create(DataWin* dataWin) { -#ifdef PLATFORM_PS2 - // Place VMContext in scratchpad RAM - requireMessage(16384 >= sizeof(VMContext), "VMContext exceeds PS2 scratchpad size (16 KB)"); - VMContext* ctx = (VMContext*) 0x70000000; - memset(ctx, 0, sizeof(VMContext)); -#else - VMContext* ctx = safeCalloc(1, sizeof(VMContext)); -#endif - ctx->dataWin = dataWin; - ctx->stack.top = 0; - ctx->selfId = -1; - ctx->otherId = -1; - ctx->callDepth = 0; - ctx->currentEventType = -1; - ctx->currentEventSubtype = -1; - ctx->currentEventObjectIndex = -1; - - ctx->profiler = nullptr; // lazily allocated by Profiler_setEnabled(&ctx->profiler, true) - - // Validate that no code entry exceeds MAX_CODE_LOCALS (the VM uses stack-allocated arrays of this size) - repeat(dataWin->code.count, i) { - CodeEntry* entry = &dataWin->code.entries[i]; - requireMessageFormatted(MAX_CODE_LOCALS > entry->localsCount, "Code %s has too many locals!", entry->name); - } - - VMBuiltins_checkIfBuiltinVarTableIsSorted(); - - // Pre-resolve built-in variable IDs (replaces runtime strcmp chains with O(1) switch dispatch) - repeat(dataWin->vari.variableCount, i) { - Variable* var = &dataWin->vari.variables[i]; - // varID == -6 is the BC16 built-in sentinel. - // In BC17, argument variables have instanceType == -6 (Builtin) with varID >= 0, so we also check instanceType. - if (var->varID == -6 || var->instanceType == -6) { - var->builtinVarId = VMBuiltins_resolveBuiltinVarId(var->name); - } else { - var->builtinVarId = BUILTIN_VAR_UNKNOWN; - } - } - - // Build reference lookup maps (file buffer stays read-only) - patchReferenceOperands(ctx); - - // Scan VARI entries to find max varID for global scope - // Built-in variables have varID == -6 (sentinel), skip those - uint32_t maxGlobalVarID = 0; - forEach(Variable, v, dataWin->vari.variables, dataWin->vari.variableCount) { - if (0 > v->varID) continue; - if (v->instanceType == INSTANCE_GLOBAL) { - if ((uint32_t) v->varID + 1 > maxGlobalVarID) maxGlobalVarID = (uint32_t) v->varID + 1; - } - } - - ctx->globalVarCount = maxGlobalVarID; - ctx->globalVars = safeCalloc(maxGlobalVarID, sizeof(RValue)); - repeat(maxGlobalVarID, i) { - ctx->globalVars[i].type = RVALUE_UNDEFINED; - } - - ctx->currentCodeIndex = -1; - - // V17+ static initialization tracking - if (dataWin->gen8.bytecodeVersion >= 17) { - ctx->staticInitialized = safeCalloc(dataWin->code.count, sizeof(bool)); - } else { - ctx->staticInitialized = nullptr; - } - ctx->currentArrayOwner = nullptr; - ctx->savearefBalance = 0; - - // Find the varID for "creator" self variable (used by instance_create) - ctx->creatorVarID = -1; - forEach(Variable, cv, dataWin->vari.variables, dataWin->vari.variableCount) { - if (cv->instanceType == INSTANCE_SELF && cv->varID >= 0 && strcmp(cv->name, "creator") == 0) { - ctx->creatorVarID = cv->varID; - break; - } - } - - // Build globalVarNameMap: varName -> varID for global variables - ctx->globalVarNameMap = nullptr; - forEach(Variable, v2, dataWin->vari.variables, dataWin->vari.variableCount) { - if (v2->instanceType == INSTANCE_GLOBAL && v2->varID >= 0) { - ptrdiff_t existing = shgeti(ctx->globalVarNameMap, (char*) v2->name); - if (0 > existing) { - shput(ctx->globalVarNameMap, (char*) v2->name, v2->varID); - } - } - } - - // Build selfVarNameMap: varName -> varID for self/instance-scoped variables. - ctx->selfVarNameMap = nullptr; - forEach(Variable, v3, dataWin->vari.variables, dataWin->vari.variableCount) { - if (v3->varID >= 0 && (v3->instanceType == INSTANCE_SELF || 0 > v3->instanceType)) { - ptrdiff_t existing = shgeti(ctx->selfVarNameMap, (char*) v3->name); - if (0 > existing) { - shput(ctx->selfVarNameMap, (char*) v3->name, v3->varID); - } - } - } - - // Build funcName -> codeIndex hash map from SCPT chunk - ctx->codeIndexByName = nullptr; - forEach(Script, s, dataWin->scpt.scripts, dataWin->scpt.count) { - if (s->name != nullptr && s->codeId >= 0) { - if (dataWin->code.count > (uint32_t) s->codeId) { - const char* codeName = dataWin->code.entries[s->codeId].name; - // Map the full code entry name (e.g. "gml_Script_SCR_GAMESTART") - shput(ctx->codeIndexByName, (char*) codeName, s->codeId); - // Also map the bare script name (e.g. "SCR_GAMESTART") - // since the FUNC chunk references use bare names in CALL instructions - shput(ctx->codeIndexByName, (char*) s->name, s->codeId); - } - } - } - - // Also map code entry names directly for non-script code (object events, room creation codes, etc.) - repeat(dataWin->code.count, i) { - const char* codeName = dataWin->code.entries[i].name; - ptrdiff_t existing = shgeti(ctx->codeIndexByName, (char*) codeName); - if (0 > existing) { - shput(ctx->codeIndexByName, (char*) codeName, (int32_t) i); - } - } - - // Build codeName -> CodeLocals* hash map - ctx->codeLocalsMap = nullptr; - repeat(dataWin->func.codeLocalsCount, i) { - CodeLocals* cl = &dataWin->func.codeLocals[i]; - shput(ctx->codeLocalsMap, safeStrdup(cl->name), cl); - // In bytecode 17+, CodeLocals uses "gml_GlobalScript_" prefix but callable CODE entries use "gml_Script_", so we'll map the "gml_Script_" variant too - if (dataWin->gen8.bytecodeVersion >= 17) { - if (strncmp(cl->name, "gml_GlobalScript_", 17) == 0) { - char scriptName[512]; - snprintf(scriptName, sizeof(scriptName), "gml_Script_%s", cl->name + 17); - shput(ctx->codeLocalsMap, safeStrdup(scriptName), cl); - } - } - } - - // BC17+: build per-CodeLocals varID -> slot hmap so resolveLocalSlot is O(1) - // We NEED to do it with the "code.count" because YoYo Games in their infinite wisdom thought "what if... we just didn't include some local variables in the localVars map? heck, sometimes we can just NOT include any CodeLocals!"... fun! - ctx->codeLocalsSlotMaps = nullptr; - if (dataWin->gen8.bytecodeVersion >= 17) { - ctx->codeLocalsSlotMaps = safeCalloc(dataWin->code.count, sizeof(*ctx->codeLocalsSlotMaps)); - } - - // Register built-in functions - VMBuiltins_registerAll(ctx); - - // Pre-resolve all FUNC entries to cached builtin pointers or script code indices. - // This eliminates per-call string hash lookups in handleCall. - ctx->funcCallCacheCount = dataWin->func.functionCount; - ctx->funcCallCache = safeMalloc(dataWin->func.functionCount * sizeof(FuncCallCache)); - repeat(dataWin->func.functionCount, i) { - const char* name = dataWin->func.functions[i].name; - BuiltinFunc builtin = VM_findBuiltin(ctx, name); - ctx->funcCallCache[i].builtin = (void*) builtin; - if (builtin != nullptr) { - ctx->funcCallCache[i].scriptCodeIndex = -1; - } else { - ptrdiff_t mapIdx = shgeti(ctx->codeIndexByName, (char*) name); - ctx->funcCallCache[i].scriptCodeIndex = (mapIdx >= 0) ? ctx->codeIndexByName[mapIdx].value : -1; - } - } - - fprintf(stderr, "VM: Initialized with %u global vars, sparse self vars (hashmap), %u functions mapped\n", ctx->globalVarCount, (uint32_t) shlen(ctx->codeIndexByName)); - - return ctx; -} - -void VM_reset(VMContext* ctx) { - // Reset all global variables to undefined - repeat(ctx->globalVarCount, i) { - RValue_free(&ctx->globalVars[i]); - ctx->globalVars[i].type = RVALUE_UNDEFINED; - } - - // Reset stack - ctx->stack.top = 0; - - // Free any remaining call frames - CallFrame* frame = ctx->callStack; - while (frame != nullptr) { - CallFrame* parent = frame->parent; - free(frame); - frame = parent; - } - ctx->callStack = nullptr; - ctx->callDepth = 0; - - // Free any remaining env frames - EnvFrame* envFrame = ctx->envStack; - while (envFrame != nullptr) { - EnvFrame* parent = envFrame->parent; - arrfree(envFrame->instanceList); - free(envFrame); - envFrame = parent; - } - ctx->envStack = nullptr; - - // Reset execution state - ctx->currentInstance = nullptr; - ctx->otherInstance = nullptr; - ctx->selfId = -1; - ctx->otherId = -1; - ctx->currentEventType = -1; - ctx->currentEventSubtype = -1; - ctx->currentEventObjectIndex = -1; - ctx->scriptArgs = nullptr; - ctx->scriptArgCount = 0; - ctx->currentCodeName = nullptr; - ctx->localVars = nullptr; - ctx->localVarCount = 0; - ctx->currentCodeLocalsSlotMap = nullptr; - ctx->actionRelativeFlag = false; - - fprintf(stderr, "VM: Reset complete (%u global vars cleared)\n", ctx->globalVarCount); -} - -static CodeLocals* resolveCodeLocals(VMContext* ctx, const char* codeName) { - return shget(ctx->codeLocalsMap, (char*) codeName); -} - -// Sets the currentCodeLocalsSlotMap for BC17+ games -static void setCurrentCodeLocalsSlotMap(VMContext* ctx) { - if (IS_BC17_OR_HIGHER(ctx)) { - ctx->currentCodeLocalsSlotMap = &ctx->codeLocalsSlotMaps[ctx->currentCodeIndex]; - } -} - -static uint32_t computeLocalsCount(VMContext* ctx, CodeEntry* code) { - if (IS_BC16_OR_BELOW(ctx)) { - return code->localsCount; - } else { - // We can't trust localVarCount in GM:S 2.3+, so we will get our cached map - // It is NOT the "right" localsCount because it may increase during runtime, but for now, this shall do - return IntIntHashMap_count(&ctx->codeLocalsSlotMaps[ctx->currentCodeIndex]); - } -} - -RValue VM_executeCode(VMContext* ctx, int32_t codeIndex) { - require(codeIndex >= 0 && ctx->dataWin->code.count > (uint32_t) codeIndex); - CodeEntry* code = &ctx->dataWin->code.entries[codeIndex]; - - ctx->bytecodeBase = ctx->dataWin->bytecodeBuffer + (code->bytecodeAbsoluteOffset - ctx->dataWin->bytecodeBufferBase); - ctx->ip = code->offset; - ctx->codeEnd = code->length; - ctx->currentCodeName = code->name; - ctx->currentCodeIndex = codeIndex; - - setCurrentCodeLocalsSlotMap(ctx); - - uint32_t localsCount = computeLocalsCount(ctx, code); - RValue localVars[MAX_CODE_LOCALS] = {0}; - ctx->localVars = localVars; - ctx->localVarCount = localsCount; - - // Reset stack for top-level execution - ctx->stack.top = 0; - - int32_t savedSavearefBalance = ctx->savearefBalance; - ctx->savearefBalance = 0; - -#ifdef ENABLE_VM_GML_PROFILER - Profiler_enter(ctx->profiler, code->name); -#endif - RValue result = executeLoop(ctx); -#ifdef ENABLE_VM_GML_PROFILER - Profiler_exit(ctx->profiler); -#endif - - requireMessage(ctx->savearefBalance == 0, "SAVEAREF/RESTOREAREF imbalance at end of VM_executeCode (unpaired SAVEAREF)"); - ctx->savearefBalance = savedSavearefBalance; - - // Free locals (decRefs owned arrays, frees owned strings) - repeat(ctx->localVarCount, i) { - RValue_free(&ctx->localVars[i]); - } - ctx->localVars = nullptr; - ctx->localVarCount = 0; - - return result; -} - - -RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t argCount) { - require(codeIndex >= 0 && ctx->dataWin->code.count > (uint32_t) codeIndex); - CodeEntry* code = &ctx->dataWin->code.entries[codeIndex]; - - // Save current frame - CallFrame frame = (CallFrame) { - .savedIP = ctx->ip, - .savedCodeEnd = ctx->codeEnd, - .savedBytecodeBase = ctx->bytecodeBase, - .savedLocals = ctx->localVars, - .savedLocalsCount = ctx->localVarCount, - .savedCodeName = ctx->currentCodeName, - .savedSavearefBalance = ctx->savearefBalance, - .savedCodeLocalsSlotMap = ctx->currentCodeLocalsSlotMap, - .savedScriptArgs = ctx->scriptArgs, - .savedScriptArgCount = ctx->scriptArgCount, - .savedCurrentCodeIndex = ctx->currentCodeIndex, - .parent = ctx->callStack, - }; - ctx->callStack = &frame; - ctx->callDepth++; - - // Set up callee - ctx->bytecodeBase = ctx->dataWin->bytecodeBuffer + (code->bytecodeAbsoluteOffset - ctx->dataWin->bytecodeBufferBase); - ctx->ip = code->offset; - ctx->codeEnd = code->length; - ctx->currentCodeName = code->name; - ctx->currentCodeIndex = codeIndex; - - setCurrentCodeLocalsSlotMap(ctx); - - uint32_t localsCount = computeLocalsCount(ctx, code); - // We use fixed-size arrays instead of VLAs because it seems that using multiple VLAs in a single function things get corrupted somehow? - // So when you see this MAX_CODE_LOCALS and GML_MAX_ARGUMENTS, you can shake your fist in the air and say "damn you MIPS!!1" - RValue localVars[MAX_CODE_LOCALS] = {0}; - ctx->localVars = localVars; - ctx->localVarCount = localsCount; - - // Store arguments in scriptArgs (mirrors GMS 1.4's global argument stack). - // Callee takes an INDEPENDENT reference for strings (strdup) and arrays (incRef) so - // the caller's original args remain valid and owner-tracked by the caller. - RValue scriptArgs[GML_MAX_ARGUMENTS] = {0}; - ctx->scriptArgs = scriptArgs; - ctx->scriptArgCount = argCount; - if (argCount > 0 && args != nullptr) { - repeat(argCount, argIdx) { - RValue argCopy = args[argIdx]; - if (argCopy.type == RVALUE_STRING && argCopy.ownsReference && argCopy.string != nullptr) { - argCopy.string = safeStrdup(argCopy.string); - } else if (argCopy.type == RVALUE_ARRAY && argCopy.array != nullptr) { - GMLArray_incRef(argCopy.array); - argCopy.ownsReference = true; -#if IS_BC17_OR_HIGHER_ENABLED - } else if (argCopy.type == RVALUE_METHOD && argCopy.method != nullptr) { - GMLMethod_incRef(argCopy.method); - argCopy.ownsReference = true; -#endif - } else if (argCopy.type == RVALUE_STRUCT && argCopy.structInst != nullptr) { - Instance_structIncRef(argCopy.structInst); - argCopy.ownsReference = true; - } - ctx->scriptArgs[argIdx] = argCopy; - } - } - - ctx->savearefBalance = 0; - - // Execute the callee -#ifdef ENABLE_VM_GML_PROFILER - Profiler_enter(ctx->profiler, code->name); -#endif - RValue result = executeLoop(ctx); -#ifdef ENABLE_VM_GML_PROFILER - Profiler_exit(ctx->profiler); -#endif - - requireMessage(ctx->savearefBalance == 0, "SAVEAREF/RESTOREAREF imbalance at end of VM_callCodeIndex (unpaired SAVEAREF)"); - - // Strengthen result BEFORE freeing callee locals/scriptArgs: if result is a weak view into callee state, the upcoming frees would leave a dangling pointer. - // For owning results, the refCount/string buffer stays valid (the callee transferred one ownership slot to us). - if (result.type == RVALUE_STRING && !result.ownsReference && result.string != nullptr) { - result = RValue_makeOwnedString(safeStrdup(result.string)); - } else if (result.type == RVALUE_ARRAY && !result.ownsReference && result.array != nullptr) { - GMLArray_incRef(result.array); - result.ownsReference = true; -#if IS_BC17_OR_HIGHER_ENABLED - } else if (result.type == RVALUE_METHOD && !result.ownsReference && result.method != nullptr) { - GMLMethod_incRef(result.method); - result.ownsReference = true; -#endif - } else if (result.type == RVALUE_STRUCT && !result.ownsReference && result.structInst != nullptr) { - Instance_structIncRef(result.structInst); - result.ownsReference = true; - } - - // Restore caller frame - CallFrame* saved = ctx->callStack; - ctx->ip = saved->savedIP; - ctx->codeEnd = saved->savedCodeEnd; - ctx->bytecodeBase = saved->savedBytecodeBase; - - // Free callee locals - repeat(ctx->localVarCount, i) { - RValue_free(&ctx->localVars[i]); - } - - // Free callee script args - repeat(ctx->scriptArgCount, i) { - RValue_free(&ctx->scriptArgs[i]); - } - - ctx->localVars = saved->savedLocals; - ctx->localVarCount = saved->savedLocalsCount; - ctx->currentCodeLocalsSlotMap = saved->savedCodeLocalsSlotMap; - ctx->scriptArgs = saved->savedScriptArgs; - ctx->scriptArgCount = saved->savedScriptArgCount; - ctx->currentCodeName = saved->savedCodeName; - ctx->currentCodeIndex = saved->savedCurrentCodeIndex; - ctx->savearefBalance = saved->savedSavearefBalance; - ctx->callStack = saved->parent; - ctx->callDepth--; - - return result; -} - -// ===[ Disassembler ]=== - -static char gmlTypeChar(uint8_t type) { - switch (type) { - case GML_TYPE_DOUBLE: return 'd'; - case GML_TYPE_FLOAT: return 'f'; - case GML_TYPE_INT32: return 'i'; - case GML_TYPE_INT64: return 'l'; - case GML_TYPE_BOOL: return 'b'; - case GML_TYPE_VARIABLE: return 'v'; - case GML_TYPE_STRING: return 's'; - case GML_TYPE_INT16: return 'e'; - default: return '?'; - } -} - -static const char* cmpKindName(uint8_t kind) { - switch (kind) { - case CMP_LT: return "LT"; - case CMP_LTE: return "LTE"; - case CMP_EQ: return "EQ"; - case CMP_NEQ: return "NEQ"; - case CMP_GTE: return "GTE"; - case CMP_GT: return "GT"; - default: return "???"; - } -} - -static const char* varTypeName(uint32_t varRef) { - uint8_t varType = (varRef >> 24) & 0xF8; - switch (varType) { - case VARTYPE_ARRAY: return "Array"; - case VARTYPE_STACKTOP: return "StackTop"; - case VARTYPE_NORMAL: return "Normal"; - case VARTYPE_INSTANCE: return "Instance"; - default: return "Unknown"; - } -} - -static const char* disasmScopeName(VMContext* ctx, int32_t instanceType) { - switch (instanceType) { - case INSTANCE_SELF: return "self"; - case INSTANCE_OTHER: return "other"; - case INSTANCE_ALL: return "all"; - case INSTANCE_NOONE: return "noone"; - case INSTANCE_GLOBAL: return "global"; - case INSTANCE_LOCAL: return "local"; - case INSTANCE_STACKTOP: return "stacktop"; - default: - if (instanceType >= 0 && ctx->dataWin->objt.count > (uint32_t) instanceType) { - return ctx->dataWin->objt.objects[instanceType].name; - } - return "unknown"; - } -} - -// Formats a variable operand for disassembly: "scope.varName [varType]" -// If scopeOverride is set (e.g. "local", "global"), uses that instead of resolving instrInstType. -// Shows VARI instanceType mismatch annotation when scopeOverride is nullptr and types differ. -static void disasmFormatVar(VMContext* ctx, const uint8_t* extraData, const char* scopeOverride, int32_t instrInstType, char* buf, size_t bufSize) { - uint32_t varRef = resolveVarOperand(extraData); - Variable* varDef = resolveVarDef(ctx, varRef); - const char* vType = varTypeName(varRef); - - // For StackTop and Array variable types, the actual instance type comes from the stack at runtime, not from the instruction operand. - // Use the VARI entry's instanceType instead, since the instruction's instanceType is meaningless for these access types. - uint8_t varType = (varRef >> 24) & 0xF8; - if (varType == VARTYPE_STACKTOP || varType == VARTYPE_ARRAY) { - const char* scope = scopeOverride != nullptr ? scopeOverride : disasmScopeName(ctx, varDef->instanceType); - snprintf(buf, bufSize, "%s.%s [%s]", scope, varDef->name, vType); - return; - } - - const char* scope = scopeOverride != nullptr ? scopeOverride : disasmScopeName(ctx, instrInstType); - - if (scopeOverride == nullptr && varDef->instanceType != instrInstType) { - const char* variScope = disasmScopeName(ctx, varDef->instanceType); - snprintf(buf, bufSize, "%s.%s [%s] (VARI: %s, instr: %s)", scope, varDef->name, vType, variScope, scope); - } else { - snprintf(buf, bufSize, "%s.%s [%s]", scope, varDef->name, vType); - } -} - -// Returns stack effect comment for a variable access instruction -static void disasmFormatVarComment(VMContext* ctx, const uint8_t* extraData, bool isPop, char* buf, size_t bufSize) { - uint32_t varRef = resolveVarOperand(extraData); - uint8_t varType = (varRef >> 24) & 0xF8; - if (isPop) { - switch (varType) { - case VARTYPE_ARRAY: snprintf(buf, bufSize, "// pops: [arrayIndex, instanceType, value]"); break; - case VARTYPE_STACKTOP: snprintf(buf, bufSize, "// pops: [instanceType, value]"); break; - default: snprintf(buf, bufSize, "// pops: [value]"); break; - } - } else { - switch (varType) { - case VARTYPE_ARRAY: snprintf(buf, bufSize, "// pops: [arrayIndex, instanceType] -> pushes: [value]"); break; - case VARTYPE_STACKTOP: snprintf(buf, bufSize, "// pops: [instanceType] -> pushes: [value]"); break; - default: snprintf(buf, bufSize, "// pushes: [value]"); break; - } - } -} - -// Formats a single instruction into opcodeStr, operandStr, and commentStr buffers. -// Used by both VM_disassemble and --trace-opcodes. -// bytecodeBase is needed because the disassembler and trace have it from different sources. -static void formatInstruction(VMContext* ctx, const uint8_t* bytecodeBase, uint32_t instrAddr, uint32_t instr, const uint8_t* extraData, - char* opcodeStr, size_t opcodeSize, char* operandStr, size_t operandSize, char* commentStr, size_t commentSize) { - DataWin* dw = ctx->dataWin; - uint8_t opcode = instrOpcode(instr); - uint8_t type1 = instrType1(instr); - uint8_t type2 = instrType2(instr); - int16_t instType = instrInstanceType(instr); - - switch (opcode) { - // Binary arithmetic/logic - case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: - case OP_REM: case OP_MOD: case OP_AND: case OP_OR: - case OP_XOR: case OP_SHL: case OP_SHR: - snprintf(opcodeStr, opcodeSize, "%s.%c.%c", opcodeName(opcode), gmlTypeChar(type1), gmlTypeChar(type2)); - snprintf(commentStr, commentSize, "// pops: [a, b] -> pushes: [result]"); - break; - - // Unary - case OP_NEG: - snprintf(opcodeStr, opcodeSize, "Neg.%c", gmlTypeChar(type1)); - snprintf(commentStr, commentSize, "// pops: [a] -> pushes: [result]"); - break; - case OP_NOT: - snprintf(opcodeStr, opcodeSize, "Not.%c", gmlTypeChar(type1)); - if (type1 == GML_TYPE_BOOL) { - snprintf(commentStr, commentSize, "// pops: [a] -> pushes: [bool] (logical NOT)"); - } else { - snprintf(commentStr, commentSize, "// pops: [a] -> pushes: [int] (bitwise NOT)"); - } - break; - - // Type conversion - case OP_CONV: - snprintf(opcodeStr, opcodeSize, "Conv.%c.%c", gmlTypeChar(type1), gmlTypeChar(type2)); - snprintf(commentStr, commentSize, "// pops: [%c] -> pushes: [%c]", gmlTypeChar(type2), gmlTypeChar(type1)); - break; - - // Comparison - case OP_CMP: - snprintf(opcodeStr, opcodeSize, "Cmp.%c.%c", gmlTypeChar(type1), gmlTypeChar(type2)); - snprintf(operandStr, operandSize, "%s", cmpKindName(instrCmpKind(instr))); - snprintf(commentStr, commentSize, "// pops: [a, b] -> pushes: [bool]"); - break; - - // Push - case OP_PUSH: { - switch (type1) { - case GML_TYPE_DOUBLE: - snprintf(opcodeStr, opcodeSize, "Push.d"); - snprintf(operandStr, operandSize, "%g", BinaryUtils_readFloat64(extraData)); - snprintf(commentStr, commentSize, "// pushes: [double]"); - break; - case GML_TYPE_FLOAT: - snprintf(opcodeStr, opcodeSize, "Push.f"); - snprintf(operandStr, operandSize, "%g", (double) BinaryUtils_readFloat32(extraData)); - snprintf(commentStr, commentSize, "// pushes: [float]"); - break; - case GML_TYPE_INT32: - snprintf(opcodeStr, opcodeSize, "Push.i"); - snprintf(operandStr, operandSize, "%d", BinaryUtils_readInt32(extraData)); - snprintf(commentStr, commentSize, "// pushes: [int32]"); - break; - case GML_TYPE_INT64: - snprintf(opcodeStr, opcodeSize, "Push.l"); - snprintf(operandStr, operandSize, "%lld", (long long) BinaryUtils_readInt64(extraData)); - snprintf(commentStr, commentSize, "// pushes: [int64]"); - break; - case GML_TYPE_BOOL: - snprintf(opcodeStr, opcodeSize, "Push.b"); - snprintf(operandStr, operandSize, "%s", BinaryUtils_readInt32(extraData) != 0 ? "true" : "false"); - snprintf(commentStr, commentSize, "// pushes: [bool]"); - break; - case GML_TYPE_STRING: { - snprintf(opcodeStr, opcodeSize, "Push.s"); - int32_t strIdx = BinaryUtils_readInt32(extraData); - if (strIdx >= 0 && dw->strg.count > (uint32_t) strIdx) { - const char* str = dw->strg.strings[strIdx]; - if (strlen(str) > 60) { - snprintf(operandStr, operandSize, "\"%.57s...\"", str); - } else { - snprintf(operandStr, operandSize, "\"%s\"", str); - } - } else { - snprintf(operandStr, operandSize, "[string:%d]", strIdx); - } - snprintf(commentStr, commentSize, "// pushes: [string]"); - break; - } - case GML_TYPE_VARIABLE: - snprintf(opcodeStr, opcodeSize, "Push.v"); - disasmFormatVar(ctx, extraData, nullptr, (int32_t) instType, operandStr, operandSize); - disasmFormatVarComment(ctx, extraData, false, commentStr, commentSize); - break; - case GML_TYPE_INT16: - snprintf(opcodeStr, opcodeSize, "Push.e"); - snprintf(operandStr, operandSize, "%d", (int32_t) instType); - snprintf(commentStr, commentSize, "// pushes: [int16]"); - break; - default: - snprintf(opcodeStr, opcodeSize, "Push.?"); - snprintf(operandStr, operandSize, "(unknown type 0x%X)", type1); - break; - } - break; - } - - // Scoped pushes - case OP_PUSHLOC: - snprintf(opcodeStr, opcodeSize, "PushLoc.v"); - disasmFormatVar(ctx, extraData, "local", (int32_t) instType, operandStr, operandSize); - disasmFormatVarComment(ctx, extraData, false, commentStr, commentSize); - break; - case OP_PUSHGLB: - snprintf(opcodeStr, opcodeSize, "PushGlb.v"); - disasmFormatVar(ctx, extraData, "global", (int32_t) instType, operandStr, operandSize); - disasmFormatVarComment(ctx, extraData, false, commentStr, commentSize); - break; - case OP_PUSHBLTN: - snprintf(opcodeStr, opcodeSize, "PushBltn.v"); - disasmFormatVar(ctx, extraData, nullptr, (int32_t) instType, operandStr, operandSize); - disasmFormatVarComment(ctx, extraData, false, commentStr, commentSize); - break; - - // PushI (int16 immediate) - case OP_PUSHI: - snprintf(opcodeStr, opcodeSize, "PushI.e"); - snprintf(operandStr, operandSize, "%d", (int32_t) instType); - snprintf(commentStr, commentSize, "// pushes: [int16]"); - break; - - // Pop (store to variable) - case OP_POP: - snprintf(opcodeStr, opcodeSize, "Pop.%c.%c", gmlTypeChar(type1), gmlTypeChar(type2)); - disasmFormatVar(ctx, extraData, nullptr, (int32_t) instType, operandStr, operandSize); - disasmFormatVarComment(ctx, extraData, true, commentStr, commentSize); - break; - - // Unconditional branch - case OP_B: { - snprintf(opcodeStr, opcodeSize, "B"); - int32_t offset = instrJumpOffset(instr); - uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); - snprintf(operandStr, operandSize, "L_%04X (offset: %+d)", target, offset); - break; - } - - // Conditional branches - case OP_BT: { - snprintf(opcodeStr, opcodeSize, "BT"); - int32_t offset = instrJumpOffset(instr); - uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); - snprintf(operandStr, operandSize, "L_%04X (offset: %+d)", target, offset); - snprintf(commentStr, commentSize, "// pops: [bool]"); - break; - } - case OP_BF: { - snprintf(opcodeStr, opcodeSize, "BF"); - int32_t offset = instrJumpOffset(instr); - uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); - snprintf(operandStr, operandSize, "L_%04X (offset: %+d)", target, offset); - snprintf(commentStr, commentSize, "// pops: [bool]"); - break; - } - - // With-statement: PushEnv - case OP_PUSHENV: { - snprintf(opcodeStr, opcodeSize, "PushEnv"); - int32_t offset = instrJumpOffset(instr); - uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); - // Peek at previous instruction to identify the target object - const char* targetName = nullptr; - if (instrAddr >= 4) { - uint32_t prevInstr = BinaryUtils_readUint32(bytecodeBase + instrAddr - 4); - if (instrOpcode(prevInstr) == OP_PUSHI) { - int16_t objIdx = (int16_t) (prevInstr & 0xFFFF); - targetName = disasmScopeName(ctx, (int32_t) objIdx); - } - } - if (targetName != nullptr) { - snprintf(operandStr, operandSize, "%s (target: L_%04X, offset: %+d)", targetName, target, offset); - } else { - snprintf(operandStr, operandSize, "(target: L_%04X, offset: %+d)", target, offset); - } - snprintf(commentStr, commentSize, "// pops: [target]"); - break; - } - - // With-statement: PopEnv - case OP_POPENV: { - snprintf(opcodeStr, opcodeSize, "PopEnv"); - if ((instr & 0x00FFFFFF) == 0xF00000) { - snprintf(operandStr, operandSize, "[exit]"); - } else { - int32_t offset = instrJumpOffset(instr); - uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); - snprintf(operandStr, operandSize, "(target: L_%04X, offset: %+d)", target, offset); - } - break; - } - - // Function call - case OP_CALL: { - snprintf(opcodeStr, opcodeSize, "Call.i"); - int32_t argCount = instr & 0xFFFF; - uint32_t funcIdx = resolveFuncOperand(extraData); - const char* funcName = (dw->func.functionCount > funcIdx) ? dw->func.functions[funcIdx].name : "???"; - snprintf(operandStr, operandSize, "%s(%d)", funcName, argCount); - if (argCount > 0) { - char argList[128] = ""; - int32_t pos = 0; - for (int32_t i = 0; 8 > i && argCount > i; i++) { - if (i > 0) pos += snprintf(argList + pos, sizeof(argList) - pos, ", "); - pos += snprintf(argList + pos, sizeof(argList) - pos, "arg%d", i); - } - if (argCount > 8) snprintf(argList + pos, sizeof(argList) - pos, ", ..."); - snprintf(commentStr, commentSize, "// pops: [%s] -> pushes: [result]", argList); - } else { - snprintf(commentStr, commentSize, "// pushes: [result]"); - } - break; - } - - // Dynamic call through variable/method reference (BC17+) - case OP_CALLV: { - int32_t argCount = instr & 0xFFFF; - snprintf(opcodeStr, opcodeSize, "CallV.v"); - snprintf(operandStr, operandSize, "%d", argCount); - snprintf(commentStr, commentSize, "// pops: [func, instance, %d args] -> pushes: [result]", argCount); - break; - } - - // Duplicate stack items - case OP_DUP: { - uint8_t extra = (uint8_t) (instr & 0xFF); - int32_t count = (int32_t) extra + 1; - snprintf(opcodeStr, opcodeSize, "Dup.%c", gmlTypeChar(type1)); - if (count > 1) { - snprintf(operandStr, operandSize, "%d", count); - snprintf(commentStr, commentSize, "// duplicates %d items", count); - } else { - snprintf(commentStr, commentSize, "// duplicates top item"); - } - break; - } - - // Control flow - case OP_RET: - snprintf(opcodeStr, opcodeSize, "Ret.%c", gmlTypeChar(type1)); - snprintf(commentStr, commentSize, "// pops: [value] (return)"); - break; - case OP_EXIT: - snprintf(opcodeStr, opcodeSize, "Exit.%c", gmlTypeChar(type1)); - snprintf(commentStr, commentSize, "// (end of code)"); - break; - case OP_POPZ: - snprintf(opcodeStr, opcodeSize, "Popz.%c", gmlTypeChar(type1)); - snprintf(commentStr, commentSize, "// pops: [value]"); - break; - - // Break (extended opcodes in V17+) - case OP_BREAK: { - int16_t breakType = (int16_t) instType; - const char* mnemonic; - switch (breakType) { - case BREAK_CHKINDEX: mnemonic = "chkindex"; break; - case BREAK_PUSHAF: mnemonic = "pushaf"; break; - case BREAK_POPAF: mnemonic = "popaf"; break; - case BREAK_PUSHAC: mnemonic = "pushac"; break; - case BREAK_SETOWNER: mnemonic = "setowner"; break; - case BREAK_ISSTATICOK: mnemonic = "isstaticok"; break; - case BREAK_SETSTATIC: mnemonic = "setstatic"; break; - case BREAK_SAVEAREF: mnemonic = "savearef"; break; - case BREAK_RESTOREAREF: mnemonic = "restorearef"; break; - default: mnemonic = nullptr; break; - } - if (mnemonic != nullptr) { - snprintf(opcodeStr, opcodeSize, "%s.%c", mnemonic, gmlTypeChar(type1)); - } else { - snprintf(opcodeStr, opcodeSize, "Break.%c", gmlTypeChar(type1)); - snprintf(operandStr, operandSize, "%d", (int32_t) breakType); - } - break; - } - - default: - snprintf(opcodeStr, opcodeSize, "??? (0x%02X)", opcode); - break; - } -} - -void VM_buildCrossReferences(VMContext* ctx) { - DataWin* dw = ctx->dataWin; - ctx->crossRefMap = nullptr; - - repeat(dw->code.count, callerIdx) { - CodeEntry* code = &dw->code.entries[callerIdx]; - const uint8_t* base = dw->bytecodeBuffer + (code->bytecodeAbsoluteOffset - dw->bytecodeBufferBase); - uint32_t ip = 0; - - while (code->length > ip) { - uint32_t instr = BinaryUtils_readUint32(base + ip); - ip += 4; - const uint8_t* ed = base + ip; - if (instrHasExtraData(instr)) { - ip += extraDataSize(instrType1(instr)); - } - - if (instrOpcode(instr) == OP_CALL) { - uint32_t funcIdx = resolveFuncOperand(ed); - if (dw->func.functionCount > funcIdx) { - const char* funcName = dw->func.functions[funcIdx].name; - ptrdiff_t codeMapIdx = shgeti(ctx->codeIndexByName, (char*) funcName); - if (codeMapIdx >= 0) { - int32_t targetIdx = ctx->codeIndexByName[codeMapIdx].value; - ptrdiff_t mapIdx = hmgeti(ctx->crossRefMap, targetIdx); - if (0 > mapIdx) { - int32_t* callers = nullptr; - arrput(callers, (int32_t) callerIdx); - hmput(ctx->crossRefMap, targetIdx, callers); - } else { - // Deduplicate: don't add the same caller twice - int32_t* callers = ctx->crossRefMap[mapIdx].value; - bool found = false; - for (ptrdiff_t k = 0; arrlen(callers) > k; k++) { - if (callers[k] == (int32_t) callerIdx) { found = true; break; } - } - if (!found) { - arrput(ctx->crossRefMap[mapIdx].value, (int32_t) callerIdx); - } - } - } - } - } - } - } -} - -void VM_disassemble(VMContext* ctx, int32_t codeIndex) { - DataWin* dw = ctx->dataWin; - require(dw->code.count > (uint32_t) codeIndex); - CodeEntry* code = &dw->code.entries[codeIndex]; - - // Header - printf("=== %s (length=%u, locals=%u, args=%u) ===\n", code->name, code->length, code->localsCount, code->argumentsCount); - - // CodeLocals - CodeLocals* locals = resolveCodeLocals(ctx, code->name); - if (locals != nullptr && locals->localVarCount > 0) { - printf("Locals:"); - repeat(locals->localVarCount, i) { - if (i > 0) printf(","); - printf(" [%u] %s", locals->locals[i].varID, locals->locals[i].name); - } - printf("\n"); - } - - // Cross-references - if (ctx->crossRefMap != nullptr) { - ptrdiff_t mapIdx = hmgeti(ctx->crossRefMap, codeIndex); - if (mapIdx >= 0) { - int32_t* callers = ctx->crossRefMap[mapIdx].value; - printf("Called by:"); - for (ptrdiff_t i = 0; arrlen(callers) > i; i++) { - if (i > 0) printf(","); - printf(" %s", dw->code.entries[callers[i]].name); - } - printf("\n"); - } - } - - printf("\n"); - - const uint8_t* bytecodeBase = dw->bytecodeBuffer + (code->bytecodeAbsoluteOffset - dw->bytecodeBufferBase); - uint32_t codeLength = code->length; - - // Pass 1: collect branch targets for labels - struct { uint32_t key; bool value; }* branchTargets = nullptr; - { - uint32_t ip = 0; - while (codeLength > ip) { - uint32_t instrAddr = ip; - uint32_t instr = BinaryUtils_readUint32(bytecodeBase + ip); - ip += 4; - if (instrHasExtraData(instr)) { - ip += extraDataSize(instrType1(instr)); - } - uint8_t opcode = instrOpcode(instr); - if (opcode == OP_B || opcode == OP_BT || opcode == OP_BF || opcode == OP_PUSHENV) { - int32_t offset = instrJumpOffset(instr); - uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); - hmput(branchTargets, target, true); - } - if (opcode == OP_POPENV) { - if ((instr & 0x00FFFFFF) != 0xF00000) { - int32_t offset = instrJumpOffset(instr); - uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); - hmput(branchTargets, target, true); - } - } - } - } - - // Pass 2: print instructions - uint32_t ip = 0; - int32_t envDepth = 0; - - while (codeLength > ip) { - uint32_t instrAddr = ip; - uint32_t instr = BinaryUtils_readUint32(bytecodeBase + ip); - ip += 4; - const uint8_t* extraData = bytecodeBase + ip; - if (instrHasExtraData(instr)) { - ip += extraDataSize(instrType1(instr)); - } - - uint8_t opcode = instrOpcode(instr); - - // PopEnv decreases depth before printing - if (opcode == OP_POPENV && envDepth > 0) envDepth--; - - // Print label if this address is a branch target - if (hmgeti(branchTargets, instrAddr) >= 0) { - printf(" %04X: L_%04X:\n", instrAddr, instrAddr); - } - - int32_t indent = 2 + envDepth * 4; - char opcodeStr[32]; - char operandStr[256] = ""; - char commentStr[128] = ""; - - formatInstruction(ctx, bytecodeBase, instrAddr, instr, extraData, opcodeStr, sizeof(opcodeStr), operandStr, sizeof(operandStr), commentStr, sizeof(commentStr)); - - // Print the formatted line - if (commentStr[0] != '\0') { - printf("%*s%04X: [0x%08X] %-16s %-45s %s\n", indent, "", instrAddr, instr, opcodeStr, operandStr, commentStr); - } else { - printf("%*s%04X: [0x%08X] %-16s %s\n", indent, "", instrAddr, instr, opcodeStr, operandStr); - } - - // PushEnv increases depth after printing - if (opcode == OP_PUSHENV) envDepth++; - } - - hmfree(branchTargets); - printf("\n"); -} - -void VM_registerBuiltin(VMContext* ctx, const char* name, BuiltinFunc func) { - requireMessage(shgeti(ctx->builtinMap, name) == -1, "Trying to register an already registered builtin function!"); - shput(ctx->builtinMap, (char*) name, func); -} - -BuiltinFunc VM_findBuiltin(VMContext* ctx, const char* name) { - ptrdiff_t idx = shgeti(ctx->builtinMap, (char*) name); - if (0 > idx) return nullptr; - return ctx->builtinMap[idx].value; -} - -void VM_free(VMContext* ctx) { - if (ctx == nullptr) return; - - // Reset mutable runtime state - VM_reset(ctx); - - // Free profiler (no-op if never enabled) - Profiler_destroy(ctx->profiler); - ctx->profiler = nullptr; - -#ifdef ENABLE_VM_OPCODE_PROFILER - free(ctx->opcodeVariantCounts); - ctx->opcodeVariantCounts = nullptr; - free(ctx->opcodeRValueTypeCounts); - ctx->opcodeRValueTypeCounts = nullptr; -#endif - - // Free global vars array itself - free(ctx->globalVars); - - // Free hash maps - shfree(ctx->codeIndexByName); - shfree(ctx->globalVarNameMap); - shfree(ctx->selfVarNameMap); - repeat(shlen(ctx->codeLocalsMap), i) { - free(ctx->codeLocalsMap[i].key); - } - shfree(ctx->codeLocalsMap); - - // Free dedup key strings before freeing the hashmaps - repeat(shlen(ctx->loggedUnknownFuncs), i) { - free(ctx->loggedUnknownFuncs[i].key); - } - shfree(ctx->loggedUnknownFuncs); - repeat(shlen(ctx->loggedStubbedFuncs), i) { - free(ctx->loggedStubbedFuncs[i].key); - } - shfree(ctx->loggedStubbedFuncs); -#ifdef ENABLE_VM_TRACING - shfree(ctx->varReadsToBeTraced); - shfree(ctx->varWritesToBeTraced); - shfree(ctx->functionCallsToBeTraced); - shfree(ctx->alarmsToBeTraced); - shfree(ctx->instanceLifecyclesToBeTraced); - shfree(ctx->eventsToBeTraced); - shfree(ctx->opcodesToBeTraced); - shfree(ctx->stackToBeTraced); -#endif - - // Free function call cache - free(ctx->funcCallCache); - - // Free cross-reference map - if (ctx->crossRefMap != nullptr) { - for (ptrdiff_t i = 0; hmlen(ctx->crossRefMap) > i; i++) { - arrfree(ctx->crossRefMap[i].value); - } - hmfree(ctx->crossRefMap); - } - - // Free builtin map - shfree(ctx->builtinMap); - ctx->registeredBuiltinFunctions = false; - - // Free V17+ static tracking - free(ctx->staticInitialized); - - // Free per-code varID -> slot maps (BC17+ only; nullptr otherwise). - if (ctx->codeLocalsSlotMaps != nullptr) { - repeat(ctx->dataWin->code.count, i) { - IntIntHashMap_free(&ctx->codeLocalsSlotMaps[i]); - } - free(ctx->codeLocalsSlotMaps); - ctx->codeLocalsSlotMaps = nullptr; - } - -#ifndef PLATFORM_PS2 - free(ctx); -#endif -} +#include "vm.h" +#include "vm_builtins.h" +#include "instance.h" +#include "runner.h" +#include "binary_utils.h" +#include "utils.h" +#include "bytecode_versions.h" +#include "profiler.h" +#include "string_builder.h" + +#include +#include +#include +#include +#include + +#include "stb_ds.h" + +// Maximum number of local variables per code entry (stack-allocated arrays in VM_executeCode/VM_callCodeIndex) +#define MAX_CODE_LOCALS 128 + +__attribute__((weak)) void VMExec_platformBootLog(const char* message) { + (void) message; +} + +__attribute__((weak)) bool VMExec_shouldTraceArrayOps(void) { + return false; +} + +__attribute__((weak)) bool VMExec_shouldTraceCrashWindow(void) { + return false; +} + +static void VMExec_traceCrashRValue(const char* prefix, const char* codeName, int32_t index, const RValue* val) { + if (!VMExec_shouldTraceCrashWindow() || val == nullptr) return; + + const void* refPtr = nullptr; + switch (val->type) { + case RVALUE_STRING: refPtr = val->string; break; + case RVALUE_ARRAY: refPtr = val->array; break; +#if IS_BC17_OR_HIGHER_ENABLED + case RVALUE_METHOD: refPtr = val->method; break; +#endif + case RVALUE_STRUCT: refPtr = val->structInst; break; + default: break; + } + + char buffer[256]; + snprintf( + buffer, + sizeof(buffer), + "%s code=%s idx=%d type=%d owns=%d gml=%d ref=%p i32=%d", + prefix, + codeName != nullptr ? codeName : "", + index, + val->type, + val->ownsReference ? 1 : 0, +#if IS_BC17_OR_HIGHER_ENABLED + val->gmlStackType, +#else + 0, +#endif + refPtr, + val->int32 + ); + VMExec_platformBootLog(buffer); +} + +static void VMExec_bootLog(const char* message) { + VMExec_platformBootLog(message); +} + +static bool VMExec_shouldTraceBootstrapObject(VMContext* ctx, const char** outObjectName) { + if (ctx == NULL || ctx->currentInstance == NULL || ctx->dataWin == NULL) return false; + Instance* inst = (Instance*) ctx->currentInstance; + if (inst->objectIndex < 0 || (uint32_t) inst->objectIndex >= ctx->dataWin->objt.count) return false; + const char* objectName = ctx->dataWin->objt.objects[inst->objectIndex].name; + if (outObjectName != NULL) *outObjectName = objectName; + if (objectName == NULL) return false; + return strcmp(objectName, "obj_initializer2") == 0 || strcmp(objectName, "obj_time") == 0; +} + +static void VMExec_traceBootstrapCall(VMContext* ctx, const char* fmt, ...) { + static uint32_t traceCount = 0; + static uint32_t suppressedCount = 0; + static char lastBuffer[256] = ""; + const char* objectName = NULL; + if (!VMExec_shouldTraceBootstrapObject(ctx, &objectName)) return; + if (traceCount >= 500) return; + + char detail[192]; + va_list args; + va_start(args, fmt); + vsnprintf(detail, sizeof(detail), fmt, args); + va_end(args); + + char buffer[256]; + snprintf(buffer, sizeof(buffer), "vmcall: obj=%s code=%s %s", objectName, ctx->currentCodeName, detail); + + // Suppress consecutive identical lines but count them + if (strcmp(lastBuffer, buffer) == 0) { + suppressedCount++; + return; + } + + // Before logging a new unique line, flush any suppression summary + if (suppressedCount > 0) { + char suppressMsg[128]; + snprintf(suppressMsg, sizeof(suppressMsg), "vmcall: (repeated %u times)", suppressedCount); + VMExec_bootLog(suppressMsg); + suppressedCount = 0; + traceCount++; + if (traceCount >= 500) return; + } + + snprintf(lastBuffer, sizeof(lastBuffer), "%s", buffer); + VMExec_bootLog(buffer); + traceCount++; +} + +// ===[ Stack Operations ]=== + +#ifdef ENABLE_VM_TRACING +static bool shouldTraceStack(VMContext* ctx) { + if (shlen(ctx->stackToBeTraced) == 0) return false; + if (ctx->traceBytecodeAfterFrame > ctx->runner->frameCount) return false; + return shgeti(ctx->stackToBeTraced, "*") != -1 || shgeti(ctx->stackToBeTraced, ctx->currentCodeName) != -1; +} + +// Returns a heap-allocated "[elem0, elem1, ..., elemN]" string for the current stack contents (bottom -> top). Caller frees. +static char* formatStackContents(VMContext* ctx) { + StringBuilder sb = StringBuilder_create(256); + StringBuilder_appendChar(&sb, '['); + repeat(ctx->stack.top, si) { + char* typed = RValue_toStringTyped(ctx->stack.slots[si]); + if (si > 0) StringBuilder_append(&sb, ", "); + StringBuilder_append(&sb, typed); + free(typed); + } + StringBuilder_appendChar(&sb, ']'); + char* result = StringBuilder_toString(&sb); + StringBuilder_free(&sb); + return result; +} +#endif + +#if IS_BC17_OR_HIGHER_ENABLED +// Returns the native byte size of a GML data type on the runner's stack. +// This is needed because the Dup instruction encodes byte counts, not slot counts. +// Only used by BC17+ Dup paths; BC16 Dup decodes the operand as a slot count directly. +static int gmlTypeNativeSize(uint8_t gmlType) { + switch (gmlType) { + case GML_TYPE_DOUBLE: return 8; + case GML_TYPE_INT32: return 4; + case GML_TYPE_INT64: return 8; + case GML_TYPE_BOOL: return 4; + case GML_TYPE_VARIABLE: return 16; + case GML_TYPE_STRING: return 4; + case GML_TYPE_INT16: return 4; + default: return 16; + } +} +#endif + +static void stackPush(VMContext* ctx, RValue val) { + require(VM_STACK_SIZE > ctx->stack.top); +#ifdef ENABLE_VM_TRACING + if (shouldTraceStack(ctx)) { + char* valStr = RValue_toStringTyped(val); + ctx->stack.slots[ctx->stack.top++] = val; + char* stackBuf = formatStackContents(ctx); + fprintf(stderr, "VM: [%s] PUSH %s [stack=%d -> %d] %s\n", ctx->currentCodeName, valStr, ctx->stack.top - 1, ctx->stack.top, stackBuf); + free(stackBuf); + free(valStr); + return; + } +#endif + ctx->stack.slots[ctx->stack.top++] = val; +} + +#if IS_BC17_OR_HIGHER_ENABLED +static void stackPushTyped(VMContext* ctx, RValue val, uint8_t gmlStackType) { + if (IS_BC17_OR_HIGHER(ctx)) { + val.gmlStackType = gmlStackType; + } + stackPush(ctx, val); +} +#else +// BC16-only builds don't carry per-slot GML stack type, so this is just a plain push. +// Defined as a macro so the gmlStackType argument (often `instrType2(instr)`) is never computed at call sites. +#define stackPushTyped(ctx, val, gmlStackType) stackPush((ctx), (val)) +#endif + +static RValue stackPop(VMContext* ctx) { + require(ctx->stack.top > 0); + RValue val = ctx->stack.slots[--ctx->stack.top]; +#ifdef ENABLE_VM_TRACING + if (shouldTraceStack(ctx)) { + char* valStr = RValue_toStringTyped(val); + char* stackBuf = formatStackContents(ctx); + fprintf(stderr, "VM: [%s] POP %s [stack=%d -> %d] %s\n", ctx->currentCodeName, valStr, ctx->stack.top + 1, ctx->stack.top, stackBuf); + free(stackBuf); + free(valStr); + } +#endif + return val; +} + +// Helper function that calls stackPop and returns the result as an int32_t +static int32_t stackPopInt32(VMContext* ctx) { + RValue rvalue = stackPop(ctx); + int32_t value = RValue_toInt32(rvalue); + RValue_free(&rvalue); + return value; +} + +static RValue* stackPeek(VMContext* ctx) { + require(ctx->stack.top > 0); + return &ctx->stack.slots[ctx->stack.top - 1]; +} + +// ===[ Instruction Decoding ]=== + +static uint8_t instrOpcode(uint32_t instr) { + return (instr >> 24) & 0xFF; +} + +static uint8_t instrType1(uint32_t instr) { + return (instr >> 16) & 0xF; +} + +static uint8_t instrType2(uint32_t instr) { + return (instr >> 20) & 0xF; +} + +static int16_t instrInstanceType(uint32_t instr) { + return (int16_t) (instr & 0xFFFF); +} + +static uint8_t instrCmpKind(uint32_t instr) { + return (instr >> 8) & 0xFF; +} + +static bool instrHasExtraData(uint32_t instr) { + return (instr & 0x40000000) != 0; +} + +// Jump offset for branch instructions: sign-extend 23 bits, multiply by 4 +static int32_t instrJumpOffset(uint32_t instr) { + return ((int32_t) (instr << 9)) >> 7; +} + +static uint32_t extraDataSize(uint8_t type1) { + switch (type1) { + case GML_TYPE_DOUBLE: return 8; + case GML_TYPE_INT64: return 8; + case GML_TYPE_FLOAT: return 4; + case GML_TYPE_INT32: return 4; + case GML_TYPE_BOOL: return 4; + case GML_TYPE_VARIABLE: return 4; + case GML_TYPE_STRING: return 4; + case GML_TYPE_INT16: return 0; + default: return 0; + } +} + +// ===[ Reference Chain Resolution ]=== + +// Walks reference chains from the bytecode buffer and builds hash maps +// mapping absolute file offsets to resolved operand values. +// The bytecode buffer stays completely read-only. +// Patches bytecode operands in-place so that variable/function reference chain deltas +// are replaced with resolved indices. This avoids needing hash map lookups at runtime. +static void patchReferenceOperands(VMContext* ctx) { + DataWin* dataWin = ctx->dataWin; + uint8_t* buf = dataWin->bytecodeBuffer; + size_t base = dataWin->bytecodeBufferBase; + + // Patch variable operands: replace delta with varIdx (preserving upper 5 bits) + repeat(dataWin->vari.variableCount, varIdx) { + Variable* v = &dataWin->vari.variables[varIdx]; + if (v->occurrences == 0) continue; + + uint32_t addr = v->firstAddress; + repeat(v->occurrences, occ) { + uint32_t operandAddr = addr + 4; + uint32_t operand = BinaryUtils_readUint32(&buf[operandAddr - base]); + uint32_t delta = operand & 0x07FFFFFF; + uint32_t upperBits = operand & 0xF8000000; + + // Patch in-place: upper bits preserved, lower 27 = varIdx + BinaryUtils_writeUint32(&buf[operandAddr - base], upperBits | (varIdx & 0x07FFFFFF)); + + if (v->occurrences > occ + 1) { + addr += delta; + } + } + } + + // Patch function operands: replace delta with funcIdx + repeat(dataWin->func.functionCount, funcIdx) { + Function* f = &dataWin->func.functions[funcIdx]; + if (f->occurrences == 0) continue; + + uint32_t addr = f->firstAddress; + repeat(f->occurrences, occ) { + uint32_t operandAddr = addr + 4; + uint32_t operand = BinaryUtils_readUint32(&buf[operandAddr - base]); + uint32_t delta = operand & 0x07FFFFFF; + + // Patch in-place: store funcIdx directly + BinaryUtils_writeUint32(&buf[operandAddr - base], funcIdx); + + if (f->occurrences > occ + 1) { + addr += delta; + } + } + } +} + +// Resolve a variable operand: returns upper bits | varIndex (read directly from patched bytecode) +static uint32_t resolveVarOperand(const uint8_t* extraData) { + return BinaryUtils_readUint32Aligned(extraData); +} + +// Resolve a function operand: returns funcIndex (read directly from patched bytecode) +static uint32_t resolveFuncOperand(const uint8_t* extraData) { + return BinaryUtils_readUint32Aligned(extraData); +} + +// ===[ Array Operations ]=== +// +// All arrays live as RVALUE_ARRAY (GMLArray*) inside a scalar variable slot (self vars, global vars, or local vars). +// Variable reads return the RValue (which may be an array pointer) and variable writes update the slot directly. +// +// Reads return a weak view of the slot value - callers must incRef + set ownsReference if they want to retain it. +// +// Writes (VARTYPE_ARRAY Pop, BREAK_POPAF, BREAK_PUSHAC materialisation) go through VM_arrayWriteAt, +// which handles: +// * slot-not-yet-an-array -> allocate a fresh GMLArray +// * CoW fork when another scope/slot owns the array (BC16 predicate uses the slot address; BC17+ predicate compares against ctx->currentArrayOwner set by BREAK_SETOWNER) +// * grow-on-write past the current length +// * transfer ownership of "val" into arr->data[index], freeing whatever was there before. +// +// Forward declarations +static Instance* findInstanceByTarget(VMContext* ctx, int32_t target); + +// Read array[index]. Returns RVALUE_UNDEFINED when slot is not an array or when index is out of bounds. +// The returned RValue is a weak view, callers that stash it must strengthen (incRef, strdup). +static RValue VM_arrayReadAt(RValue* slot, int32_t index) { + if (slot == nullptr || slot->type != RVALUE_ARRAY || slot->array == nullptr) { + return (RValue){ .type = RVALUE_UNDEFINED }; + } + RValue* cell = GMLArray_slot(slot->array, index); + if (cell == nullptr) { + return (RValue){ .type = RVALUE_UNDEFINED }; + } + RValue result = *cell; + result.ownsReference = false; + return result; +} + +// Copies "val" into *slot: dup string buffers, incRef arrays. Caller retains "val". +static void storeIntoArraySlot(RValue* slot, RValue val) { + // Free whatever was there (decRefs owned arrays, frees owned strings). + RValue_free(slot); + if (val.type == RVALUE_STRING && val.string != nullptr) { + *slot = RValue_makeOwnedString(safeStrdup(val.string)); + } else if (val.type == RVALUE_ARRAY && val.array != nullptr) { + GMLArray_incRef(val.array); + val.ownsReference = true; + *slot = val; +#if IS_BC17_OR_HIGHER_ENABLED + } else if (val.type == RVALUE_METHOD && val.method != nullptr) { + GMLMethod_incRef(val.method); + val.ownsReference = true; + *slot = val; +#endif + } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) { + Instance_structIncRef(val.structInst); + val.ownsReference = true; + *slot = val; + } else { + val.ownsReference = false; + *slot = val; + } +} + +// Write array[index] = val with CoW semantics. Always makes an independent copy of val, caller retains ownership and must RValue_free(&val) when done. +// `slot` is the RValue* holding the array (e.g. &globalVars[id], &inst->selfVars[..].value, &localVars[slot]). +// Returns the (possibly newly-forked) GMLArray* now in *slot. +static GMLArray* VM_arrayWriteAt(VMContext* ctx, RValue* slot, int32_t index, RValue val) { + require(slot != nullptr); + requireMessageFormatted(index >= 0, "Trying to write to an array using a negative index! Index: %d", index); + + void* intendedOwner; +#if IS_BC17_OR_HIGHER_ENABLED + intendedOwner = IS_BC17_OR_HIGHER(ctx) ? ctx->currentArrayOwner : (void*) slot; +#else + intendedOwner = (void*) slot; +#endif + + // Case 1: slot doesn't hold an array yet, replace whatever's there with a fresh one. + if (slot->type != RVALUE_ARRAY || slot->array == nullptr) { + RValue_free(slot); + GMLArray* fresh = GMLArray_create(0); + fresh->owner = intendedOwner; + *slot = RValue_makeArray(fresh); + GMLArray_growTo(fresh, index + 1); + storeIntoArraySlot(GMLArray_slot(fresh, index), val); + return fresh; + } + + GMLArray* arr = slot->array; + + // Case 2: CoW fork check. + bool needFork; +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx)) { + needFork = (arr->owner != ctx->currentArrayOwner); + } else +#endif + { + needFork = (arr->refCount > 1 && arr->owner != (void*) slot); + } + if (needFork) { + GMLArray* clone = GMLArray_clone(arr, intendedOwner); + GMLArray_decRef(arr); + slot->array = clone; + slot->ownsReference = true; + arr = clone; + } else if (arr->owner == nullptr) { + // Claim ownership on first write to an unowned array (e.g. freshly allocated by a builtin). + arr->owner = intendedOwner; + } + + // Case 3: grow if needed, then write. + GMLArray_growTo(arr, index + 1); + storeIntoArraySlot(GMLArray_slot(arr, index), val); + return arr; +} + +// Public entry point for builtins that materialise an array and return it (layer_get_all). +// Returned RValue holds one strong ref, caller is expected to consume it (stack push / variable write). +// Owner is left null, the first write through a variable slot will claim it. +RValue VM_createArray(MAYBE_UNUSED VMContext* ctx) { + GMLArray* arr = GMLArray_create(0); + return RValue_makeArray(arr); +} + +// Public helper for builtins that populate an array being returned. Copies val, caller retains ownership. +// The arrayRef must be an RVALUE_ARRAY (as returned by VM_createArray). No CoW fork, the returning array has refCount=1 and no scope owner yet, so we write in place. +void VM_arraySet(MAYBE_UNUSED VMContext* ctx, RValue* arrayRef, int32_t index, RValue val) { + require(arrayRef != nullptr && arrayRef->type == RVALUE_ARRAY && arrayRef->array != nullptr); + GMLArray* arr = arrayRef->array; + GMLArray_growTo(arr, index + 1); + storeIntoArraySlot(GMLArray_slot(arr, index), val); +} + +// ===[ Trace Helpers ]=== + +#ifdef ENABLE_VM_TRACING +/** + * @brief Checks if a variable access should be traced. + * + * Matches the trace map entries in order: wildcard "*", bare scope name (e.g. "obj_player" or "global"), + * alternate scope name (e.g. "self" for any instance), or qualified "scope.var" format + * (e.g. "obj_player.x", "global.hp", "self.x"). Short-circuits before formatting + * the qualified name when possible. + * + * @param traceMap The string-boolean hash map of trace filters (from --trace-variable-reads/writes). + * @param scopeName The scope of the variable: an object name (e.g. "obj_player") or "global". + * @param altScopeName An alternate scope name to also match (e.g. "self" for instance variables), or nullptr. + * @param varName The variable name being accessed (e.g. "x"). + * @return true if the access matches a trace filter and should be logged. + */ +static bool shouldTraceVariable(StringBooleanEntry* traceMap, const char* scopeName, const char* altScopeName, const char* varName) { + if (shlen(traceMap) == 0) return false; + if (shgeti(traceMap, "*") != -1) return true; + if (shgeti(traceMap, scopeName) != -1) return true; + if (altScopeName != nullptr && shgeti(traceMap, altScopeName) != -1) return true; + char formatted[strlen(scopeName) + 1 + strlen(varName) + 1]; + snprintf(formatted, sizeof(formatted), "%s.%s", scopeName, varName); + if (shgeti(traceMap, formatted) != -1) return true; + if (altScopeName != nullptr) { + char altFormatted[strlen(altScopeName) + 1 + strlen(varName) + 1]; + snprintf(altFormatted, sizeof(altFormatted), "%s.%s", altScopeName, varName); + if (shgeti(traceMap, altFormatted) != -1) return true; + } + return false; +} +#endif + +// ===[ Array Access Helpers ]=== + +typedef struct { + int32_t arrayIndex; // -1 when not an array access + int32_t instanceType; // Instance type from stack (for VARTYPE_ARRAY / VARTYPE_STACKTOP) + bool isArray; + bool hasInstanceType; // true when instanceType was popped from stack +} ArrayAccess; + +static int32_t resolveInstanceStackTop(VMContext* ctx) { + return stackPopInt32(ctx); +} + +static const char* varTypeToString(uint8_t varType) { + switch (varType) { + case VARTYPE_ARRAY: return "ARRAY"; + case VARTYPE_STACKTOP: return "STACKTOP"; + case VARTYPE_NORMAL: return "NORMAL"; + case VARTYPE_INSTANCE: return "INSTANCE"; + default: return "UNKNOWN"; + } +} + +// Pops array index (and optional stacktop value) from the stack if the varRef +// indicates an array or stacktop access. Returns { .arrayIndex = -1, .isArray = false } +// for plain variable access. +static ArrayAccess popArrayAccess(VMContext* ctx, uint32_t varRef) { + uint8_t varType = (varRef >> 24) & 0xF8; + if (varType == VARTYPE_ARRAY) { + // For array reads, GMS pushes: instanceType then arrayIndex (arrayIndex on top) + int32_t arrayIndex = stackPopInt32(ctx); + int32_t instanceType = stackPopInt32(ctx); + + // BC17: if instanceType is -9 (INSTANCE_STACKTOP), the actual instance is the next stack item. + // This is used for chained access like `command_actor[i].specialsprite[arg]` where the array variable's owning instance is resolved from a computed value on the stack. + if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { + instanceType = resolveInstanceStackTop(ctx); + } + + return (ArrayAccess){ .arrayIndex = arrayIndex, .instanceType = instanceType, .isArray = true, .hasInstanceType = true }; + } + if (varType == VARTYPE_STACKTOP) { + int32_t instanceType = stackPopInt32(ctx); + + // BC17: PushI.e -9 (INSTANCE_STACKTOP) is pushed before the Pop instruction. + // When we pop -9, it means "the real instance type is the next item on the stack". + if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { + instanceType = resolveInstanceStackTop(ctx); + } + return (ArrayAccess){ .arrayIndex = -1, .isArray = false, .hasInstanceType = true, .instanceType = instanceType }; + } + return (ArrayAccess){ .arrayIndex = -1, .isArray = false, .hasInstanceType = false }; +} + +// ===[ Variable Resolution ]=== +static const char* instanceTypeName(int32_t instanceType) { + switch (instanceType) { + case INSTANCE_SELF: return "self"; + case INSTANCE_OTHER: return "other"; + case INSTANCE_GLOBAL: return "global"; + case INSTANCE_LOCAL: return "local"; + case INSTANCE_ARG: return "arg"; + default: return "instance"; + } +} + +// BC16 array accesses sometimes arrive with a stack-supplied scope value that does not +// match the instruction's fixed target scope. When that happens on fixed-scope globals +// like global.msg[0] += "...", trusting the stacked value can redirect the read/write +// into an arbitrary object bucket and crash on 3DS. Prefer the instruction scope for +// fixed BC16 scopes and leave BC17's dynamic-stack semantics untouched. +static int32_t normalizeFixedArrayScope(MAYBE_UNUSED VMContext* ctx, int32_t originalInstanceType, int32_t resolvedInstanceType, bool hasStackScope) { + if (!hasStackScope || IS_BC17_OR_HIGHER(ctx)) return resolvedInstanceType; + + switch (originalInstanceType) { + case INSTANCE_GLOBAL: + case INSTANCE_LOCAL: + case INSTANCE_SELF: + case INSTANCE_OTHER: + return originalInstanceType; + default: + return resolvedInstanceType; + } +} + +// Returns the object name for an instance, or "" for the global scope dummy instance +static const char* instanceObjectName(VMContext* ctx, Instance* inst) { + if (0 > inst->objectIndex) return ""; + return ctx->dataWin->objt.objects[inst->objectIndex].name; +} + +static Variable* resolveVarDef(VMContext* ctx, uint32_t varRef) { + uint32_t varIndex = varRef & 0x07FFFFFF; + require(ctx->dataWin->vari.variableCount > varIndex); + Variable* varDef = &ctx->dataWin->vari.variables[varIndex]; + return varDef; +} + +// Maps a GML local's varID to its slot position in the current code's localVars[] array. +// +// BC16: varIDs for locals are already sequential slot indices (0, 1, 2, ...), so we return the varID unchanged. +// +// BC17+: a single GML local can surface as several VARI chunk entries that share a varID. +// We key by that shared varID via the precomputed currentCodeLocalsSlotMap so reads/writes via any VARI +// entry agree on the same localVars slot. +static uint32_t resolveLocalSlot(VMContext* ctx, int32_t varID) { + if (IS_BC16_OR_BELOW(ctx)) { + return (uint32_t) varID; + } + + // For BC17, we'll allocate the slot dynamically because the data.win CANNOT be trusted to know how localVars the script has + uint32_t slot = IntIntHashMap_getOrInsertSequential(ctx->currentCodeLocalsSlotMap, varID); + // Even though we are dynamically allocating the slots, we are still bound to whatever localVars is allocated to + // So, if a script goes over the MAX_CODE_LOCALS, it would cause unforeseen consequences... + requireMessage(MAX_CODE_LOCALS > slot, "resolveLocalSlot: exceeded MAX_CODE_LOCALS while allocating a slot for an array-only local"); + + // Grow this frame's localVars window to cover `slot` whether the entry is pre-existing or freshly allocated. + // Pre-existing entries can still be past ctx->localVarCount if a nested call to the same code extended the slot map while the outer frame was suspended (the outer frame's localVarCount is captured at call entry and doesn't follow later growth). + if (slot >= ctx->localVarCount) { + for (uint32_t i = ctx->localVarCount; slot >= i; i++) { + ctx->localVars[i] = (RValue){ .type = RVALUE_UNDEFINED }; + } + ctx->localVarCount = slot + 1; + } + return slot; +} + +// Finds an instance by target value. +// target >= 100000: instance ID (find specific instance, including recently-destroyed-but-not-cleaned-up-yet ones so GML code can read properties of an instance just after instance_destroy within the same step). +// target >= 0 && target < 100000: object index (find first ACTIVE instance of that object, checking parent chains) +static Instance* findInstanceByTarget(VMContext* ctx, int32_t target) { + Runner* runner = (Runner*) ctx->runner; + + if (target >= 100000) { + // Instance ID - find specific instance + return hmget(runner->instancesById, target); + } + + // Object index - find first active matching instance via the descendant-inclusive bucket. Pure read, no user code, so we walk the bucket directly without an arena snapshot. + if (target >= 0 && runner->dataWin->objt.count > (uint32_t) target) { + Instance** bucket = runner->instancesByObject[target]; + int32_t bucketCount = (int32_t) arrlen(bucket); + for (int32_t i = 0; bucketCount > i; i++) { + if (bucket[i]->active) return bucket[i]; + } + } + return nullptr; +} + +// Inline read of a non-array, non-builtin variable from a simple scope. +// Returns false when the instanceType isn't covered or the scope's instance pointer is unavailable, so the caller can fall through to the full resolveVariableRead. +// Used by the OP_PUSH/PUSHLOC/PUSHGLB fast paths in executeLoop to skip the entire resolveVariableRead dispatch overhead. +static inline bool tryFastVarRead(VMContext* ctx, int32_t instanceType, Variable* varDef, RValue* out) { + switch (instanceType) { + case INSTANCE_SELF: { + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr) return false; + RValue* slot = IntRValueHashMap_findSlot(&inst->selfVars, varDef->varID); + *out = (slot != nullptr) ? *slot : (RValue){ .type = RVALUE_UNDEFINED }; + out->ownsReference = false; + return true; + } + case INSTANCE_LOCAL: { + uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); + require(ctx->localVarCount > localSlot); + *out = ctx->localVars[localSlot]; + out->ownsReference = false; + return true; + } + case INSTANCE_GLOBAL: { + require(ctx->globalVarCount > (uint32_t) varDef->varID); + *out = ctx->globalVars[varDef->varID]; + out->ownsReference = false; + return true; + } + case INSTANCE_OTHER: { + Instance* inst = (Instance*) ctx->otherInstance; + if (inst == nullptr) return false; + RValue* slot = IntRValueHashMap_findSlot(&inst->selfVars, varDef->varID); + *out = (slot != nullptr) ? *slot : (RValue){ .type = RVALUE_UNDEFINED }; + out->ownsReference = false; + return true; + } + } + return false; +} + +static RValue resolveVariableRead(VMContext* ctx, int32_t instanceType, uint32_t varRef) { + Variable* varDef = resolveVarDef(ctx, varRef); + ArrayAccess access = popArrayAccess(ctx, varRef); + + // Use instance type from stack when available (VARTYPE_ARRAY / VARTYPE_STACKTOP) + int32_t originalInstanceType = instanceType; + if (access.hasInstanceType) { + instanceType = access.instanceType; + instanceType = normalizeFixedArrayScope(ctx, originalInstanceType, instanceType, true); + } + + // BC17+: Push.v/Pop.v with instrInstanceType == -9 (STACKTOP) and VARTYPE_NORMAL means + // "the instance is on the stack" (e.g. `struct.field` after @@NewGMLObject@@). Pop it here. +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx) && !access.hasInstanceType && instanceType == INSTANCE_STACKTOP) { + instanceType = resolveInstanceStackTop(ctx); + } +#endif + + // Resolve target instance for object/instance references (instanceType >= 0) + Instance* targetInstance = (Instance*) ctx->currentInstance; + if (instanceType >= 0) { + targetInstance = findInstanceByTarget(ctx, instanceType); + if (targetInstance == nullptr) { + const char* varTypeName = varTypeToString((varRef >> 24) & 0xF8); + if (instanceType < 100000 && (uint32_t) instanceType < ctx->dataWin->objt.count) { + GameObject* gameObject = &ctx->dataWin->objt.objects[instanceType]; + fprintf(stderr, "VM: [%s] READ var '%s' on object index %d (%s) but no instance found (varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d)\n", ctx->currentCodeName, varDef->name, instanceType, gameObject->name, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID); + } else { + fprintf(stderr, "VM: [%s] READ var '%s' on instance %d but no instance found (varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d)\n", ctx->currentCodeName, varDef->name, instanceType, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID); + } + return RValue_makeReal(0.0); + } + } else if (instanceType == INSTANCE_OTHER) { + if (ctx->otherInstance != nullptr) { + targetInstance = (Instance*) ctx->otherInstance; + } + } else if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_ARG) { + // BC17: argument0..argument15 via INSTANCE_ARG instance type (builtinVarId pre-resolved at parse time) + int16_t bid = varDef->builtinVarId; + RValue result; + if (bid == BUILTIN_VAR_ARGUMENT_COUNT) { + result = RValue_makeReal((GMLReal) ctx->scriptArgCount); + } else if (bid == BUILTIN_VAR_ARGUMENT) { + // argument[N] array-style access + int32_t idx = access.arrayIndex; + if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > idx && idx >= 0) { + result = ctx->scriptArgs[idx]; + result.ownsReference = false; + } else { + result = RValue_makeUndefined(); + } + } else if (bid >= BUILTIN_VAR_ARGUMENT0 && BUILTIN_VAR_ARGUMENT15 >= bid) { + int32_t argIndex = bid - BUILTIN_VAR_ARGUMENT0; + if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > argIndex) { + result = ctx->scriptArgs[argIndex]; + result.ownsReference = false; + // If we are trying to access the argument via an array (example: argName[i]), we NEED to read INSIDE the array + // Example: + // function init(arg2) { + // var test = arg2[0]; // We NEED to read the [0] from the array + // } + // Without this, the caller gets the whole array back + if (access.isArray && result.type == RVALUE_ARRAY && result.array != nullptr) { + result = VM_arrayReadAt(&result, access.arrayIndex); + } + } else { + result = RValue_makeUndefined(); + } + } else { + fprintf(stderr, "VM: [%s] INSTANCE_ARG read on unknown variable '%s' (builtinVarId=%d)\n", ctx->currentCodeName, varDef->name, bid); + result = RValue_makeUndefined(); + } + return result; + } + +#if IS_BC17_OR_HIGHER_ENABLED + // BC17+: instanceType == INSTANCE_BUILTIN (-6) on a Push.v means "look up this name as a function reference" (emitted for CallV dispatch paths like `@@This@@(); texture_set_interpolation_ext; CallV`). + // Intercept before the builtin-variable path: only treat it as a function if the VARI entry isn't a real built-in variable (varID == -6 with a resolved builtinVarId). + if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_BUILTIN && !(varDef->varID == -6 && varDef->builtinVarId != -1)) { + // `@@This@@(); push.v bltn.; CallV` is also used for `self.method()` where `method` is a user-defined method stored on the instance (e.g. `init = method(...)` on an object). + // CallV pops [func, instance, args], so the instance is sitting right below the func we're about to push. Peek at it and try to read `` off its selfVars first; if the VARI entry has a self scope and the peeked slot resolves to an instance with the field, return that method. Otherwise fall through to global function lookup. + if (varDef->instanceType == INSTANCE_SELF && ctx->stack.top > 0) { + RValue* peek = stackPeek(ctx); + int32_t peekId = RValue_toInt32(*peek); + Instance* peekInst = findInstanceByTarget(ctx, peekId); + if (peekInst != nullptr) { + RValue* peekSlot = IntRValueHashMap_findSlot(&peekInst->selfVars, varDef->varID); + if (peekSlot != nullptr) { + RValue val = *peekSlot; + val.ownsReference = false; + return val; + } + } + } + + // Then try user scripts/code entries (funcMap maps both "funcName" and "gml_Script_funcName") + ptrdiff_t mapIdx = shgeti(ctx->codeIndexByName, varDef->name); + if (mapIdx >= 0) { + int32_t codeIndex = ctx->codeIndexByName[mapIdx].value; + return RValue_makeMethod(codeIndex, -1); + } + // Then try registered built-ins + ptrdiff_t bidx = shgeti(ctx->builtinMap, (char*) varDef->name); + if (bidx >= 0) { + BuiltinFunc bf = ctx->builtinMap[bidx].value; + RValue rv = { .type = RVALUE_METHOD, .ownsReference = true, .gmlStackType = GML_TYPE_VARIABLE }; + rv.method = GMLMethod_createBuiltin(bf, -1); + return rv; + } + // Unresolved: return a method stub so CallV can log a single "unknown function" and return undefined instead of bailing out with a scary "unresolvable function reference" error. + RValue rv = { .type = RVALUE_METHOD, .ownsReference = true, .gmlStackType = GML_TYPE_VARIABLE }; + rv.method = GMLMethod_createUnresolved(varDef->name, -1); + return rv; + } +#endif + + // Check for built-in variable (varID == -6 sentinel) + if (varDef->varID == -6) { + // For object/instance references, temporarily swap currentInstance so VMBuiltins reads the correct instance + Instance* savedInstance = (Instance*) ctx->currentInstance; + bool needsInstanceSwap = (instanceType >= 0) || (instanceType == INSTANCE_OTHER); + if (needsInstanceSwap) ctx->currentInstance = targetInstance; + RValue result = VMBuiltins_getVariable(ctx, varDef->builtinVarId, varDef->name, access.arrayIndex); + if (needsInstanceSwap) ctx->currentInstance = savedInstance; + +#ifdef ENABLE_VM_TRACING + // Trace built-in variable reads + if (instanceType == INSTANCE_GLOBAL) { + if (shouldTraceVariable(ctx->varReadsToBeTraced, "global", nullptr, varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(result); + if (access.arrayIndex != -1) { + fprintf(stderr, "VM: [%s] READ global.%s[%d] -> %s (builtin)\n", ctx->currentCodeName, varDef->name, access.arrayIndex, rvalueAsString); + } else { + fprintf(stderr, "VM: [%s] READ global.%s -> %s (builtin)\n", ctx->currentCodeName, varDef->name, rvalueAsString); + } + free(rvalueAsString); + } + } else if (targetInstance != nullptr && targetInstance->objectIndex >= 0 && ctx->dataWin->objt.count > (uint32_t) targetInstance->objectIndex) { + const char* objName = ctx->dataWin->objt.objects[targetInstance->objectIndex].name; + if (shouldTraceVariable(ctx->varReadsToBeTraced, objName, "self", varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(result); + if (access.arrayIndex != -1) { + fprintf(stderr, "VM: [%s] READ %s.%s[%d] -> %s (instanceId=%d) (builtin)\n", ctx->currentCodeName, objName, varDef->name, access.arrayIndex, rvalueAsString, targetInstance->instanceId); + } else { + fprintf(stderr, "VM: [%s] READ %s.%s -> %s (instanceId=%d) (builtin)\n", ctx->currentCodeName, objName, varDef->name, rvalueAsString, targetInstance->instanceId); + } + free(rvalueAsString); + } + } +#endif + + return result; + } + + // Resolve the variable's scalar slot pointer for the target scope. Array-valued vars live inline as RVALUE_ARRAY in the same slot. + // VM_arrayReadAt handles the array indirection when access.isArray, VM_arrayWriteAt handles CoW forking when writing. + RValue* slot = nullptr; + switch (instanceType) { + case INSTANCE_LOCAL: { + uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); + require(ctx->localVarCount > localSlot); + slot = &ctx->localVars[localSlot]; + break; + } + case INSTANCE_GLOBAL: + require(ctx->globalVarCount > (uint32_t) varDef->varID); + slot = &ctx->globalVars[varDef->varID]; + break; + case INSTANCE_SELF: + default: { + Instance* inst = targetInstance; + if (inst == nullptr) { + const char* varTypeName = varTypeToString((varRef >> 24) & 0xF8); + fprintf(stderr, "VM: [%s] Read on self var '%s' but no current instance (instanceType=%d, varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d)\n", ctx->currentCodeName, varDef->name, instanceType, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID); + return RValue_makeReal(0.0); + } + slot = IntRValueHashMap_findSlot(&inst->selfVars, varDef->varID); + // sparse storage: nonexistent entry -> treat as undefined scalar (array reads fall through to VM_arrayReadAt returning undefined) + if (slot == nullptr) { + if (access.isArray) return (RValue){ .type = RVALUE_UNDEFINED }; + return (RValue){ .type = RVALUE_UNDEFINED }; + } + break; + } + } + + // Array access: read array[index] from the slot. + if (access.isArray) { + RValue result = VM_arrayReadAt(slot, access.arrayIndex); +#ifdef ENABLE_VM_TRACING + const char* scopeName = + instanceType == INSTANCE_LOCAL ? "local" : + instanceType == INSTANCE_GLOBAL ? "global" : + (targetInstance != nullptr ? instanceObjectName(ctx, targetInstance) : "self"); + const char* altName = (instanceType == INSTANCE_SELF || instanceType >= 0 || instanceType == INSTANCE_OTHER) ? "self" : nullptr; + if (shouldTraceVariable(ctx->varReadsToBeTraced, scopeName, altName, varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(result); + fprintf(stderr, "VM: [%s] READ %s.%s[%d] -> %s\n", ctx->currentCodeName, scopeName, varDef->name, access.arrayIndex, rvalueAsString); + free(rvalueAsString); + } +#endif + return result; + } + + // Scalar access: return the slot's current value as a weak view (slot retains ownership). + RValue result = *slot; + result.ownsReference = false; + +#ifdef ENABLE_VM_TRACING + // Read tracing for scalar variables + if (instanceType == INSTANCE_GLOBAL) { + if (shouldTraceVariable(ctx->varReadsToBeTraced, "global", nullptr, varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(result); + fprintf(stderr, "VM: [%s] READ global.%s -> %s\n", ctx->currentCodeName, varDef->name, rvalueAsString); + free(rvalueAsString); + } + } else if (instanceType == INSTANCE_SELF || instanceType >= 0) { + Instance* inst = targetInstance; + if (inst != nullptr && shouldTraceVariable(ctx->varReadsToBeTraced, instanceObjectName(ctx, inst), "self", varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(result); + fprintf(stderr, "VM: [%s] READ %s.%s -> %s (instanceId=%d)\n", ctx->currentCodeName, instanceObjectName(ctx, inst), varDef->name, rvalueAsString, inst->instanceId); + free(rvalueAsString); + } + } +#endif + + return result; +} + +// Helper: write a variable value to a single specific instance (always copies, never moves the original val) +static void writeSingleInstanceVariable(VMContext* ctx, Instance* inst, Variable* varDef, ArrayAccess* access, RValue val) { + // Built-in variable (varID == -6 sentinel) + if (varDef->varID == -6) { + Instance* savedInstance = (Instance*) ctx->currentInstance; + ctx->currentInstance = inst; + VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, access->arrayIndex); + ctx->currentInstance = savedInstance; + return; + } + + // Array write - materialise-on-write via VM_arrayWriteAt. getOrInsertUndefined returns the existing slot or inserts an UNDEFINED entry and returns it. + if (access->isArray) { + RValue* slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); + VM_arrayWriteAt((VMContext*) ctx, slot, access->arrayIndex, val); + return; + } + + // Scalar write (Instance_setSelfVar always takes an independent ref; caller still owns "val"). + Instance_setSelfVar(inst, varDef->varID, val); +} + +// Transfer ownership of "val into "*dest", freeing the old value first. +// Strings are duplicated only if the source view is non-owning (so we don't double-free). +// Arrays/methods/structs bump refcount when needed and flip the source's ownsReference flag to take a strong ref. +static inline void writeIntoSlot(RValue* dest, RValue val) { + RValue_free(dest); + if (val.type == RVALUE_STRING && !val.ownsReference && val.string != nullptr) { + *dest = RValue_makeOwnedString(safeStrdup(val.string)); + } else if (val.type == RVALUE_ARRAY && val.array != nullptr) { + if (!val.ownsReference) GMLArray_incRef(val.array); + val.ownsReference = true; + *dest = val; +#if IS_BC17_OR_HIGHER_ENABLED + } else if (val.type == RVALUE_METHOD && val.method != nullptr) { + if (!val.ownsReference) GMLMethod_incRef(val.method); + val.ownsReference = true; + *dest = val; +#endif + } else if (val.type == RVALUE_STRUCT && val.structInst != nullptr) { + if (!val.ownsReference) Instance_structIncRef(val.structInst); + val.ownsReference = true; + *dest = val; + } else { + *dest = val; + } +} + +// Promote weak views returned from helpers/builtins into independent caller-owned values. +// This mirrors the strengthening done on script returns in VM_callCodeIndex and prevents +// container-backed temporaries (arrays, structs, methods, strings) from going stale while +// the caller still has the value on its stack. +static inline RValue strengthenReturnValue(RValue val) { + if (val.type == RVALUE_STRING && !val.ownsReference && val.string != nullptr) { + return RValue_makeOwnedString(safeStrdup(val.string)); + } + if (val.type == RVALUE_ARRAY && !val.ownsReference && val.array != nullptr) { + GMLArray_incRef(val.array); + val.ownsReference = true; + return val; + } +#if IS_BC17_OR_HIGHER_ENABLED + if (val.type == RVALUE_METHOD && !val.ownsReference && val.method != nullptr) { + GMLMethod_incRef(val.method); + val.ownsReference = true; + return val; + } +#endif + if (val.type == RVALUE_STRUCT && !val.ownsReference && val.structInst != nullptr) { + Instance_structIncRef(val.structInst); + val.ownsReference = true; + return val; + } + return val; +} + +// Force out-of-line so the OP_POP fast path in executeLoop doesn't inline this, because we already have an "optimized" version for common writes +__attribute__((noinline)) +static void resolveVariableWrite(VMContext* ctx, int32_t instanceType, uint32_t varRef, RValue val) { + Variable* varDef = resolveVarDef(ctx, varRef); + + // Fast path: When the varType==VARTYPE_NORMAL... + // * We can skip the popArrayAccess + // * We can skip the BC17 STACKTOP and INSTANCE_ARG branches + // * We can skip the array-write block itself + // * We can skip BOTH instanceType switches + if (varDef->varID >= 0) { + switch (instanceType) { + case INSTANCE_LOCAL: { + uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); + require(ctx->localVarCount > localSlot); + writeIntoSlot(&ctx->localVars[localSlot], val); + return; + } + case INSTANCE_GLOBAL: { + require(ctx->globalVarCount > (uint32_t) varDef->varID); + writeIntoSlot(&ctx->globalVars[varDef->varID], val); + return; + } + case INSTANCE_SELF: { + Instance* inst = (Instance*) ctx->currentInstance; + if (inst != nullptr) { + Instance_setSelfVar(inst, varDef->varID, val); + RValue_free(&val); + return; + } + break; // fall through to slow path so the existing nullptr-instance error gets logged + } + case INSTANCE_OTHER: { + Instance* inst = (Instance*) ctx->otherInstance; + if (inst != nullptr) { + Instance_setSelfVar(inst, varDef->varID, val); + RValue_free(&val); + return; + } + break; // fall through (otherInstance was nullptr, slow path will use currentInstance) + } + } + } + + // The slow path is used for builtin vars, object/instance references (instanceType >= 0), INSTANCE_ARG/STACKTOP, and other miscellaneous things like if we get a nullptr above + ArrayAccess access = popArrayAccess(ctx, varRef); + + // Use instance type from stack when available (VARTYPE_ARRAY / VARTYPE_STACKTOP) + int32_t originalInstanceType = instanceType; + if (access.hasInstanceType) { + instanceType = access.instanceType; + instanceType = normalizeFixedArrayScope(ctx, originalInstanceType, instanceType, true); + } + + // BC17+: Pop.v with instrInstanceType == -9 (STACKTOP) and VARTYPE_NORMAL means + // "the instance is on the stack" (e.g. `struct.field =` after @@NewGMLObject@@). Pop it here. +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx) && !access.hasInstanceType && instanceType == INSTANCE_STACKTOP) { + instanceType = resolveInstanceStackTop(ctx); + } +#endif + + // GML: writing through an object reference (obj_foo.var = val) sets the variable on ALL instances of that object. The setter (writeSingleInstanceVariable) can run user code, so iterate a snapshot of the bucket. + if (instanceType >= 0 && 100000 > instanceType) { + Runner* runner = (Runner*) ctx->runner; + bool found = false; + int32_t snapBase = Runner_pushInstancesOfObject(runner, instanceType); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (!inst->active) continue; + found = true; + writeSingleInstanceVariable(ctx, inst, varDef, &access, val); +#ifdef ENABLE_VM_TRACING + if (shouldTraceVariable(ctx->varWritesToBeTraced, instanceObjectName(ctx, inst), "self", varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(val); + fprintf(stderr, "VM: [%s] WRITE %s.%s = %s (instanceId=%d, all-instances object write)\n", ctx->currentCodeName, instanceObjectName(ctx, inst), varDef->name, rvalueAsString, inst->instanceId); + free(rvalueAsString); + } +#endif + } + Runner_popInstanceSnapshot(runner, snapBase); + if (!found) { + if (ctx->dataWin->objt.count > (uint32_t) instanceType) { + GameObject* gameObject = &ctx->dataWin->objt.objects[instanceType]; + char* valAsString = RValue_toString(val); + fprintf(stderr, "VM: [%s] WRITE var '%s' on object %d (%s) but no instances found (value=%s)\n", ctx->currentCodeName, varDef->name, instanceType, gameObject->name, valAsString); + free(valAsString); + } + } + RValue_free(&val); + return; + } + + // Resolve target instance for instance ID references (instanceType >= 100000) or special types + Instance* targetInstance = (Instance*) ctx->currentInstance; + if (instanceType >= 0) { + targetInstance = findInstanceByTarget(ctx, instanceType); + if (targetInstance == nullptr) { + const char* varTypeName = varTypeToString((varRef >> 24) & 0xF8); + char* valAsString = RValue_toString(val); + fprintf(stderr, "VM: [%s] WRITE var '%s' on instance %d but no instance found (varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d, value=%s)\n", ctx->currentCodeName, varDef->name, instanceType, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID, valAsString); + free(valAsString); + return; + } + } else if (instanceType == INSTANCE_OTHER) { + if (ctx->otherInstance != nullptr) { + targetInstance = (Instance*) ctx->otherInstance; + } + } else if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_ARG) { + // BC17: write to argument0..argument15 via INSTANCE_ARG instance type (builtinVarId pre-resolved at parse time) + int16_t bid = varDef->builtinVarId; + int32_t writeIndex = -1; + if (bid >= BUILTIN_VAR_ARGUMENT0 && BUILTIN_VAR_ARGUMENT15 >= bid) { + writeIndex = bid - BUILTIN_VAR_ARGUMENT0; + } else if (bid == BUILTIN_VAR_ARGUMENT) { + writeIndex = access.arrayIndex; + } else { + fprintf(stderr, "VM: [%s] INSTANCE_ARG write on unknown variable '%s' (builtinVarId=%d)\n", ctx->currentCodeName, varDef->name, bid); + } + if (writeIndex >= 0 && GML_MAX_ARGUMENTS > writeIndex && ctx->scriptArgs != nullptr) { + RValue_free(&ctx->scriptArgs[writeIndex]); + if (val.type == RVALUE_STRING && val.string != nullptr) { + ctx->scriptArgs[writeIndex] = RValue_makeOwnedString(safeStrdup(val.string)); + } else { + // Transfer ownership from val into scriptArgs: copy the tagged union as-is and neutralize val so the RValue_free below is a no-op for arrays/methods. + ctx->scriptArgs[writeIndex] = val; + val.ownsReference = false; + } + if (writeIndex >= ctx->scriptArgCount) { + ctx->scriptArgCount = writeIndex + 1; + } + } + RValue_free(&val); + return; + } + + // Check for built-in variable (varID == -6 sentinel) + if (varDef->varID == -6) { + // For object/instance references, temporarily swap currentInstance so VMBuiltins writes the correct instance + Instance* savedInstance = (Instance*) ctx->currentInstance; + bool needsInstanceSwap = (instanceType >= 0) || (instanceType == INSTANCE_OTHER); + if (needsInstanceSwap) ctx->currentInstance = targetInstance; + VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, access.arrayIndex); + if (needsInstanceSwap) ctx->currentInstance = savedInstance; + +#ifdef ENABLE_VM_TRACING + // Trace built-in variable writes + if (instanceType == INSTANCE_GLOBAL) { + if (shouldTraceVariable(ctx->varWritesToBeTraced, "global", nullptr, varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(val); + if (access.arrayIndex != -1) { + fprintf(stderr, "VM: [%s] WRITE global.%s[%d] = %s (builtin)\n", ctx->currentCodeName, varDef->name, access.arrayIndex, rvalueAsString); + } else { + fprintf(stderr, "VM: [%s] WRITE global.%s = %s (builtin)\n", ctx->currentCodeName, varDef->name, rvalueAsString); + } + free(rvalueAsString); + } + } else if (targetInstance != nullptr && targetInstance->objectIndex >= 0 && ctx->dataWin->objt.count > (uint32_t) targetInstance->objectIndex) { + const char* objName = ctx->dataWin->objt.objects[targetInstance->objectIndex].name; + if (shouldTraceVariable(ctx->varWritesToBeTraced, objName, "self", varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(val); + if (access.arrayIndex != -1) { + fprintf(stderr, "VM: [%s] WRITE %s.%s[%d] = %s (instanceId=%d) (builtin)\n", ctx->currentCodeName, objName, varDef->name, access.arrayIndex, rvalueAsString, targetInstance->instanceId); + } else { + fprintf(stderr, "VM: [%s] WRITE %s.%s = %s (instanceId=%d) (builtin)\n", ctx->currentCodeName, objName, varDef->name, rvalueAsString, targetInstance->instanceId); + } + free(rvalueAsString); + } + } +#endif + + // VMBuiltins_setVariable reads values (toReal, toInt32, etc.) but does not take ownership + RValue_free(&val); + return; + } + + // Resolve the slot pointer for this scope. For INSTANCE_SELF we materialise a sparse selfVars entry if it doesn't exist so VM_arrayWriteAt has a stable slot to own. + RValue* slot = nullptr; + switch (instanceType) { + case INSTANCE_LOCAL: { + uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); + require(ctx->localVarCount > localSlot); + slot = &ctx->localVars[localSlot]; + break; + } + case INSTANCE_GLOBAL: + require(ctx->globalVarCount > (uint32_t) varDef->varID); + slot = &ctx->globalVars[varDef->varID]; + break; + case INSTANCE_SELF: + default: { + Instance* inst = targetInstance; + if (inst == nullptr) { + const char* varTypeName = varTypeToString((varRef >> 24) & 0xF8); + char* valAsString = RValue_toString(val); + fprintf(stderr, "VM: [%s] Write on self var '%s' but no current instance (instanceType=%d, varType=%s, isArray=%s, originalInstanceType=%d, hasInstanceType=%s, varID=%d, value=%s)\n", ctx->currentCodeName, varDef->name, instanceType, varTypeName, access.isArray ? "true" : "false", originalInstanceType, access.hasInstanceType ? "true" : "false", varDef->varID, valAsString); + free(valAsString); + RValue_free(&val); + return; + } + slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); + break; + } + } + + // Array write via VM_arrayWriteAt (handles CoW fork, grow, owner stamping). + if (access.isArray) { + VM_arrayWriteAt(ctx, slot, access.arrayIndex, val); +#ifdef ENABLE_VM_TRACING + const char* scopeName = + instanceType == INSTANCE_LOCAL ? "local" : + instanceType == INSTANCE_GLOBAL ? "global" : + (targetInstance != nullptr ? instanceObjectName(ctx, targetInstance) : "self"); + const char* altName = (instanceType == INSTANCE_SELF || instanceType >= 0 || instanceType == INSTANCE_OTHER) ? "self" : nullptr; + if (shouldTraceVariable(ctx->varWritesToBeTraced, scopeName, altName, varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(val); + fprintf(stderr, "VM: [%s] WRITE %s.%s[%d] = %s\n", ctx->currentCodeName, scopeName, varDef->name, access.arrayIndex, rvalueAsString); + free(rvalueAsString); + } +#endif + RValue_free(&val); + return; + } + +#ifdef ENABLE_VM_TRACING + bool shouldLogGlobal = false; + bool shouldLogInstance = false; +#endif + + switch (instanceType) { + case INSTANCE_LOCAL: { + uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); + require(ctx->localVarCount > localSlot); + writeIntoSlot(&ctx->localVars[localSlot], val); + return; + } + case INSTANCE_GLOBAL: { + require(ctx->globalVarCount > (uint32_t) varDef->varID); + RValue* dest = &ctx->globalVars[varDef->varID]; + writeIntoSlot(dest, val); +#ifdef ENABLE_VM_TRACING + if (shouldTraceVariable(ctx->varWritesToBeTraced, "global", nullptr, varDef->name)) { + char* rvalueAsString = RValue_toStringTyped(*dest); + fprintf(stderr, "VM: [%s] WRITE global.%s = %s\n", ctx->currentCodeName, varDef->name, rvalueAsString); + free(rvalueAsString); + } +#endif + return; + } + case INSTANCE_SELF: + default: { + // Self or object/instance reference - use sparse hashmap + Instance* inst = targetInstance; + Instance_setSelfVar(inst, varDef->varID, val); +#ifdef ENABLE_VM_TRACING + if (shouldTraceVariable(ctx->varWritesToBeTraced, instanceObjectName(ctx, inst), "self", varDef->name)) { + RValue written = Instance_getSelfVar(inst, varDef->varID); + char* rvalueAsString = RValue_toStringTyped(written); + fprintf(stderr, "VM: [%s] WRITE %s.%s = %s (instanceId=%d)\n", ctx->currentCodeName, instanceObjectName(ctx, inst), varDef->name, rvalueAsString, inst->instanceId); + free(rvalueAsString); + } +#endif + // Instance_setSelfVar always copies strings, so free the original + RValue_free(&val); + return; + } + } +} + +// ===[ Type Conversion ]=== + +static RValue convertValue(RValue val, uint8_t targetType) { + switch (targetType) { + case GML_TYPE_DOUBLE: + return RValue_makeReal(RValue_toReal(val)); + case GML_TYPE_FLOAT: + return RValue_makeReal((GMLReal) (float) RValue_toReal(val)); + case GML_TYPE_INT32: + return RValue_makeInt32(RValue_toInt32(val)); + case GML_TYPE_INT64: + return RValue_makeInt64(RValue_toInt64(val)); + case GML_TYPE_BOOL: + return RValue_makeBool(RValue_toBool(val)); + case GML_TYPE_STRING: { + char* str = RValue_toString(val); + return RValue_makeOwnedString(str); + } + case GML_TYPE_VARIABLE: + // Variable type on stack is just an RValue passthrough + return val; + default: + fprintf(stderr, "VM: Unknown target type 0x%X for conversion\n", targetType); + return val; + } +} + +// ===[ Opcode Handlers ]=== + +static void handlePush(VMContext* ctx, uint32_t instr, const uint8_t* extraData, uint8_t type1) { + switch (type1) { + case GML_TYPE_DOUBLE: + stackPush(ctx, RValue_makeReal(BinaryUtils_readFloat64Aligned(extraData))); + break; + case GML_TYPE_FLOAT: + stackPush(ctx, RValue_makeReal((GMLReal) BinaryUtils_readFloat32Aligned(extraData))); + break; + case GML_TYPE_INT32: + stackPush(ctx, RValue_makeInt32(BinaryUtils_readInt32Aligned(extraData))); + break; + case GML_TYPE_INT64: + stackPush(ctx, RValue_makeInt64(BinaryUtils_readInt64Aligned(extraData))); + break; + case GML_TYPE_BOOL: + stackPush(ctx, RValue_makeBool(BinaryUtils_readInt32Aligned(extraData) != 0)); + break; + case GML_TYPE_VARIABLE: { + int32_t instanceType = (int32_t) instrInstanceType(instr); + uint32_t varRef = resolveVarOperand(extraData); + uint8_t varType = (varRef >> 24) & 0xF8; + // BC17: VARTYPE_INSTANCE encodes (instanceId - 100000) in the instruction's lower 16 bits. + // Add 100000 back so findInstanceByTarget sees the real runtime instance ID. + if (varType == VARTYPE_INSTANCE) instanceType += 100000; +#if IS_BC17_OR_HIGHER_ENABLED + if (varType == VARTYPE_ARRAYPUSHAF || varType == VARTYPE_ARRAYPOPAF) { + // V17: multi-dim first-step. Stack has [scope, firstIndex] (with an optional real-instance slot underneath when scope == -9 INSTANCE_STACKTOP). + // We resolve the variable's top-level array slot, materialise it if needed, then drill into arr->data[firstIndex] (materialising a sub-array there too). + // The sub-array is pushed as a weak ref; subsequent BREAK_PUSHAC/PUSHAF/POPAF consume it. + Variable* varDef = resolveVarDef(ctx, varRef); + int32_t firstIndex = stackPopInt32(ctx); + int32_t scope = stackPopInt32(ctx); + if (IS_BC17_OR_HIGHER(ctx) && scope == INSTANCE_STACKTOP) { + scope = resolveInstanceStackTop(ctx); + } + + // Resolve the slot for this scope. + RValue* slot = nullptr; + switch (scope) { + case INSTANCE_LOCAL: { + uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); + require(ctx->localVarCount > localSlot); + slot = &ctx->localVars[localSlot]; + break; + } + case INSTANCE_GLOBAL: + require(ctx->globalVarCount > (uint32_t) varDef->varID); + slot = &ctx->globalVars[varDef->varID]; + break; + case INSTANCE_SELF: + case INSTANCE_OTHER: { + Instance* inst = (scope == INSTANCE_OTHER && ctx->otherInstance != nullptr) + ? (Instance*) ctx->otherInstance + : (Instance*) ctx->currentInstance; + require(inst != nullptr); + slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); + break; + } + default: { + Instance* inst = findInstanceByTarget(ctx, scope); + if (inst == nullptr) { + fprintf(stderr, "VM: ARRAYPUSHAF: no instance for scope %d varID=%d\n", scope, varDef->varID); + abort(); + } + slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); + break; + } + } + + // Materialise the top-level array in the slot if needed. + if (slot->type != RVALUE_ARRAY || slot->array == nullptr) { + RValue_free(slot); + GMLArray* fresh = GMLArray_create(0); + fresh->owner = IS_BC17_OR_HIGHER(ctx) ? ctx->currentArrayOwner : (void*) slot; + *slot = RValue_makeArray(fresh); + } + GMLArray* top = slot->array; + GMLArray_growTo(top, firstIndex + 1); + RValue* topSlot = GMLArray_slot(top, firstIndex); + // Materialise the sub-array at [firstIndex] if it's not already an array. + if (topSlot->type != RVALUE_ARRAY || topSlot->array == nullptr) { + RValue_free(topSlot); + GMLArray* sub = GMLArray_create(0); + sub->owner = top->owner; + RValue rv = { .type = RVALUE_ARRAY, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; + rv.array = sub; + *topSlot = rv; + } + // Push a weak ref to the sub-array — short-lived, consumed by the next BREAK op. + stackPush(ctx, RValue_makeArrayWeak(topSlot->array)); + } else +#endif + { + RValue val = resolveVariableRead(ctx, instanceType, varRef); + // Mark as variable-width (16 bytes on native stack) regardless of the RValue's actual type + stackPushTyped(ctx, val, GML_TYPE_VARIABLE); + } + break; + } + case GML_TYPE_STRING: { + int32_t stringIndex = BinaryUtils_readInt32Aligned(extraData); + require(stringIndex >= 0 && ctx->dataWin->strg.count > (uint32_t) stringIndex); + stackPush(ctx, RValue_makeString(ctx->dataWin->strg.strings[stringIndex])); + break; + } + case GML_TYPE_INT16: { + int16_t value = (int16_t) (instr & 0xFFFF); + RValue val = RValue_makeInt32((int32_t) value); + stackPushTyped(ctx, val, GML_TYPE_INT16); + break; + } + default: + fprintf(stderr, "VM: Push with unknown type 0x%X\n", type1); + abort(); + } +} + +#if IS_BC17_OR_HIGHER_ENABLED +// For V17+ VARTYPE_ARRAYPUSHAF/POPAF on a top-level variable: return the slot's GMLArray*, +// materialising a fresh empty one in the slot if it isn't an array yet. Used by PushLoc/Glb/Bltn. +// Pushes a weak ref onto the stack — short-lived, consumed by the next BREAK_PUSHAC/PUSHAF/POPAF. +static void pushTopLevelArrayRef(VMContext* ctx, RValue* slot) { + if (slot->type != RVALUE_ARRAY || slot->array == nullptr) { + RValue_free(slot); + GMLArray* fresh = GMLArray_create(0); + fresh->owner = IS_BC17_OR_HIGHER(ctx) ? ctx->currentArrayOwner : (void*) slot; + *slot = RValue_makeArray(fresh); + } + stackPush(ctx, RValue_makeArrayWeak(slot->array)); +} +#endif + +static void handlePushBltn(VMContext* ctx, uint32_t instr, const uint8_t* extraData) { + uint32_t varRef = resolveVarOperand(extraData); +#if IS_BC17_OR_HIGHER_ENABLED + uint8_t varType = (varRef >> 24) & 0xF8; + if (varType == VARTYPE_ARRAYPUSHAF || varType == VARTYPE_ARRAYPOPAF) { + Variable* varDef = resolveVarDef(ctx, varRef); + int32_t scope = (int32_t) instrInstanceType(instr); + Instance* inst = nullptr; + if (scope == INSTANCE_SELF || scope == -1) { + inst = (Instance*) ctx->currentInstance; + } else if (scope == INSTANCE_OTHER && ctx->otherInstance != nullptr) { + inst = (Instance*) ctx->otherInstance; + } else if (scope >= 0) { + inst = findInstanceByTarget(ctx, scope); + } else { + inst = (Instance*) ctx->currentInstance; + } + if (inst == nullptr) { + fprintf(stderr, "VM: PushBltn ARRAYPUSHAF: no instance for scope %d varID=%d\n", scope, varDef->varID); + abort(); + } + RValue* slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); + pushTopLevelArrayRef(ctx, slot); + return; + } +#endif + RValue val = resolveVariableRead(ctx, (int32_t) instrInstanceType(instr), varRef); + stackPushTyped(ctx, val, GML_TYPE_VARIABLE); +} + +static void handlePushI(VMContext* ctx, uint32_t instr) { + int16_t value = (int16_t) (instr & 0xFFFF); + RValue val = RValue_makeInt32((int32_t) value); + stackPushTyped(ctx, val, GML_TYPE_INT16); +} + +// When storing into a variant variable from an int32/int64 stack source, coerce to real. +// GMS variables normalize integer literals to doubles so subsequent arithmetic routes through the real fast path instead of int32 x int32 wrapping. +static inline RValue coerceIntStoreToReal(RValue val, uint8_t type2) { + if (type2 == GML_TYPE_INT32 || type2 == GML_TYPE_INT64 || type2 == GML_TYPE_INT16) { + if (val.type == RVALUE_INT32) { + return RValue_makeReal((GMLReal) val.int32); + } +#ifndef NO_RVALUE_INT64 + if (val.type == RVALUE_INT64) { + return RValue_makeReal((GMLReal) val.int64); + } +#endif + } + return val; +} + +static void handlePop(VMContext* ctx, uint32_t instr, uint8_t type1, uint8_t type2, uint32_t varRef, uint8_t varType, int32_t instanceType) { + RValue val; + int32_t arrayIndex = -1; + + int32_t originalInstanceType = instanceType; + if (varType == VARTYPE_ARRAY) { + if (type1 == GML_TYPE_VARIABLE) { + // Simple assignment (Pop.v.v): stack bottom-to-top = [value, (realInstance,) instanceType, arrayIndex] + arrayIndex = stackPopInt32(ctx); + instanceType = stackPopInt32(ctx); + + // BC17: -9 (INSTANCE_STACKTOP) means "pop again for the real instance ID/object index" (e.g. `su_actor.specialsprite[0] = ...`) + if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { + instanceType = resolveInstanceStackTop(ctx); + } + + instanceType = normalizeFixedArrayScope(ctx, originalInstanceType, instanceType, true); + + val = stackPop(ctx); + } else { + // Compound assignment (Pop.i.v, etc.): stack bottom-to-top = [(realInstance,) instanceType, arrayIndex, value] + val = stackPop(ctx); + + arrayIndex = stackPopInt32(ctx); + instanceType = stackPopInt32(ctx); + + // BC17: -9 (INSTANCE_STACKTOP) means "pop again for the real instance ID/object index" + if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { + instanceType = resolveInstanceStackTop(ctx); + } + + instanceType = normalizeFixedArrayScope(ctx, originalInstanceType, instanceType, true); + } + } else if (varType == VARTYPE_STACKTOP && type1 == GML_TYPE_VARIABLE) { + // Simple assignment (Pop.v.v) with STACKTOP: stack bottom-to-top = [value, instanceType] + // Pop instanceType first (top), then value (bottom) + instanceType = stackPopInt32(ctx); + + // BC17: -9 (INSTANCE_STACKTOP) means "pop again for the real instance type" + if (IS_BC17_OR_HIGHER(ctx) && instanceType == INSTANCE_STACKTOP) { + instanceType = resolveInstanceStackTop(ctx); + } + + val = stackPop(ctx); + + // Clear STACKTOP type bits so resolveVariableWrite's popArrayAccess won't double-pop + varRef = (varRef & 0x07FFFFFF) | ((uint32_t) VARTYPE_NORMAL << 24); + } else { + val = stackPop(ctx); + } + + // Convert if source type differs from destination type. + // For VARTYPE_ARRAY compound assignments (type1 != GML_TYPE_VARIABLE), the type1 field + // indicates the stack layout (compound vs simple), NOT a type conversion target. + // Skip conversion in that case to preserve string values through += operations. + // For compound assignments (type1 != GML_TYPE_VARIABLE) with VARTYPE_ARRAY or VARTYPE_STACKTOP, + // the type1 field indicates the stack layout (compound vs simple), NOT a type conversion target. + // Skip conversion to preserve the actual computed value (e.g. g.image_angle -= 4.5 must not truncate to int). + bool isCompoundAssignment = ((varType == VARTYPE_ARRAY || varType == VARTYPE_STACKTOP) && type1 != GML_TYPE_VARIABLE); + if (type2 != type1 && type1 != GML_TYPE_VARIABLE && !isCompoundAssignment) { + RValue converted = convertValue(val, type1); + RValue_free(&val); + val = converted; + } + + if (type1 == GML_TYPE_VARIABLE && !isCompoundAssignment) { + val = coerceIntStoreToReal(val, type2); + } + + if (varType == VARTYPE_ARRAY) { + Variable* varDef = resolveVarDef(ctx, varRef); + if (varDef->varID == -6) { + // Resolve target instance for built-in array variable writes (e.g. obj_foo.alarm[0] = 2) + if (instanceType >= 0 && 100000 > instanceType) { + // Object reference: write to ALL instances of that object. The setter can run user code, so iterate a snapshot of the bucket. + Runner* runner = (Runner*) ctx->runner; + Instance* savedInstance = (Instance*) ctx->currentInstance; + int32_t snapBase = Runner_pushInstancesOfObject(runner, instanceType); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (!inst->active) continue; + ctx->currentInstance = inst; + VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, arrayIndex); + } + Runner_popInstanceSnapshot(runner, snapBase); + ctx->currentInstance = savedInstance; + } else if (instanceType >= 0) { + // Instance ID reference + Instance* target = findInstanceByTarget(ctx, instanceType); + if (target != nullptr) { + Instance* savedInstance = (Instance*) ctx->currentInstance; + ctx->currentInstance = target; + VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, arrayIndex); + ctx->currentInstance = savedInstance; + } + } else if (instanceType == INSTANCE_OTHER && ctx->otherInstance != nullptr) { + Instance* savedInstance = (Instance*) ctx->currentInstance; + ctx->currentInstance = (Instance*) ctx->otherInstance; + VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, arrayIndex); + ctx->currentInstance = savedInstance; + } else { + // INSTANCE_SELF or other special types: use current instance + VMBuiltins_setVariable(ctx, varDef->builtinVarId, varDef->name, val, arrayIndex); + } + } else { + // Resolve slot for this scope: VM_arrayWriteAt handles CoW + materialisation + grow. + RValue* slot = nullptr; + switch (instanceType) { + case INSTANCE_LOCAL: { + uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); + require(ctx->localVarCount > localSlot); + slot = &ctx->localVars[localSlot]; + break; + } + case INSTANCE_GLOBAL: + require(ctx->globalVarCount > (uint32_t) varDef->varID); + slot = &ctx->globalVars[varDef->varID]; + break; + case INSTANCE_SELF: + default: { + struct Instance* inst = (struct Instance*) ctx->currentInstance; + if (instanceType >= 0) { + inst = findInstanceByTarget(ctx, instanceType); + if (inst == nullptr) { + const char* varTypeName = varTypeToString(varType); + char* valAsString = RValue_toString(val); + if (instanceType < 100000 && (uint32_t) instanceType < ctx->dataWin->objt.count) { + fprintf(stderr, "VM: [%s] WRITE array var '%s[%d]' on object index %d (%s) but no instance found (varType=%s, originalInstanceType=%d, varID=%d, value=%s)\n", ctx->currentCodeName, varDef->name, arrayIndex, instanceType, ctx->dataWin->objt.objects[instanceType].name, varTypeName, originalInstanceType, varDef->varID, valAsString); + } else { + fprintf(stderr, "VM: [%s] WRITE array var '%s[%d]' on instance %d but no instance found (varType=%s, originalInstanceType=%d, varID=%d, value=%s)\n", ctx->currentCodeName, varDef->name, arrayIndex, instanceType, varTypeName, originalInstanceType, varDef->varID, valAsString); + } + free(valAsString); + RValue_free(&val); + return; + } + } else if (instanceType == INSTANCE_OTHER && ctx->otherInstance != nullptr) { + inst = (Instance*) ctx->otherInstance; + } + if (inst == nullptr) { + RValue_free(&val); + return; + } + slot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varDef->varID); + break; + } + } + if (slot != nullptr) { + VM_arrayWriteAt(ctx, slot, arrayIndex, val); +#ifdef ENABLE_VM_TRACING + bool isSelfScope = (instanceType != INSTANCE_LOCAL && instanceType != INSTANCE_GLOBAL); + const char* scopeName = instanceType == INSTANCE_LOCAL ? "local" : instanceType == INSTANCE_GLOBAL ? "global" : "self"; + if (shouldTraceVariable(ctx->varWritesToBeTraced, scopeName, isSelfScope ? nullptr : "self", varDef->name)) { + char* rvalueAsString = RValue_toString(val); + fprintf(stderr, "VM: [%s] WRITE %s.%s[%d] = %s\n", ctx->currentCodeName, scopeName, varDef->name, arrayIndex, rvalueAsString); + free(rvalueAsString); + } +#endif + } + RValue_free(&val); + } + } else { + resolveVariableWrite(ctx, instanceType, varRef, val); + } +} + +static void handlePopz(VMContext* ctx) { + RValue val = stackPop(ctx); + RValue_free(&val); +} + +__attribute__((noinline)) +static void handleAddString(VMContext* ctx, RValue a, RValue b, uint8_t resultType) { + if (a.type == RVALUE_STRING && b.type == RVALUE_STRING) { + // String concatenation + const char* sa = a.string != nullptr ? a.string : ""; + const char* sb = b.string != nullptr ? b.string : ""; + size_t lenA = strlen(sa); + size_t lenB = strlen(sb); + if (lenB == 0) { + if (a.ownsReference && a.string != nullptr) { + RValue_free(&b); + stackPushTyped(ctx, a, resultType); + } else if (lenA == 0 && b.ownsReference && b.string != nullptr) { + RValue_free(&a); + stackPushTyped(ctx, b, resultType); + } else { + char* result = safeMalloc(lenA + 1); + memcpy(result, sa, lenA + 1); + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeOwnedString(result), resultType); + } + } else if (lenA == 0 && b.ownsReference && b.string != nullptr) { + RValue_free(&a); + stackPushTyped(ctx, b, resultType); + } else if (a.ownsReference && a.string != nullptr) { + char* result = safeRealloc((void*) a.string, lenA + lenB + 1); + memcpy(result + lenA, sb, lenB + 1); + a.string = result; + RValue_free(&b); + stackPushTyped(ctx, a, resultType); + } else { + char* result = safeMalloc(lenA + lenB + 1); + memcpy(result, sa, lenA); + memcpy(result + lenA, sb, lenB + 1); + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeOwnedString(result), resultType); + } + } else { + // For anything else, we'll convert to numbers and then sum +#ifndef NO_RVALUE_INT64 + if (a.type == RVALUE_INT64 || b.type == RVALUE_INT64) { + int64_t result = (a.type == RVALUE_STRING) ? (int64_t) GMLReal_strtod(a.string, nullptr) : a.int64; + result += (b.type == RVALUE_STRING) ? (int64_t) GMLReal_strtod(b.string, nullptr) : b.int64; + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeInt64(result), resultType); + return; + } +#endif + if (a.type == RVALUE_INT32 || b.type == RVALUE_INT32) { + int32_t result = (a.type == RVALUE_STRING) ? (int32_t) GMLReal_strtod(a.string, nullptr) : a.int32; + result += (b.type == RVALUE_STRING) ? (int32_t) GMLReal_strtod(b.string, nullptr) : b.int32; + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeInt32(result), resultType); + return; + } + GMLReal result = RValue_toReal(a) + RValue_toReal(b); + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeReal(result), resultType); + } +} + +__attribute__((noinline)) +static void handleMulString(VMContext* ctx, RValue a, RValue b, uint8_t resultType) { + // a.type == RVALUE_STRING; b is the repetition count. + int count = RValue_toInt32(b); + const char* str = a.string != nullptr ? a.string : ""; + size_t len = strlen(str); + if (0 >= count || len == 0) { + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeOwnedString(safeStrdup("")), resultType); + } else { + char* result = safeMalloc(len * count + 1); + repeat(count, i) { + memcpy(result + i * len, str, len); + } + result[len * count] = '\0'; + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeOwnedString(result), resultType); + } +} + +static void handleDiv(VMContext* ctx, uint32_t instr) { + RValue b = stackPop(ctx); + RValue a = stackPop(ctx); + uint8_t type1 = instrType1(instr); + uint8_t type2 = instrType2(instr); + GMLReal divisor = RValue_toReal(b); + // In GameMaker's native runner, ONLY integer/integer division throws a hard error on zero, float/variable types rely on IEEE 754 (produces NaN) + if ((type1 == GML_TYPE_INT32 || type1 == GML_TYPE_INT64) && (type2 == GML_TYPE_INT32 || type2 == GML_TYPE_INT64)) { + requireMessageFormatted(divisor != 0.0, "VM: [%s] DoDiv :: Divide by zero", ctx->currentCodeName); + } + GMLReal result = RValue_toReal(a) / divisor; + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeReal(result), instrType2(instr)); +} + +static void handleRem(VMContext* ctx, uint32_t instr) { + RValue b = stackPop(ctx); + RValue a = stackPop(ctx); + int32_t ib = RValue_toInt32(b); + requireMessageFormatted(ib != 0, "VM: [%s] DoRem :: Divide by zero", ctx->currentCodeName); + int32_t result = RValue_toInt32(a) % ib; + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeInt32(result), instrType2(instr)); +} + +static void handleMod(VMContext* ctx, uint32_t instr) { + RValue b = stackPop(ctx); + RValue a = stackPop(ctx); + GMLReal divisor = RValue_toReal(b); + requireMessageFormatted(divisor != 0.0, "VM: [%s] DoMod :: Divide by zero", ctx->currentCodeName); + GMLReal result = GMLReal_fmod(RValue_toReal(a), divisor); + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeReal(result), instrType2(instr)); +} + +#define SIMPLE_BYTECODE_BITWISE_OPERATION(op) \ + int32_t b = stackPopInt32(ctx); \ + int32_t a = stackPopInt32(ctx); \ + int32_t result = a op b; \ + stackPushTyped(ctx, RValue_makeInt32(result), instrType2(instr)) + +static void handleAnd(VMContext* ctx, uint32_t instr) { + SIMPLE_BYTECODE_BITWISE_OPERATION(&); +} + +static void handleOr(VMContext* ctx, uint32_t instr) { + SIMPLE_BYTECODE_BITWISE_OPERATION(|); +} + +static void handleXor(VMContext* ctx, uint32_t instr) { + SIMPLE_BYTECODE_BITWISE_OPERATION(^); +} + +static void handleNeg(VMContext* ctx, uint32_t instr) { + RValue a = stackPop(ctx); + GMLReal result = -RValue_toReal(a); + RValue_free(&a); + stackPushTyped(ctx, RValue_makeReal(result), instrType1(instr)); +} + +static void handleNot(VMContext* ctx, uint32_t instr) { + uint8_t resultType = instrType1(instr); + int32_t a = stackPopInt32(ctx); + if (GML_TYPE_BOOL == resultType) { + // Logical NOT: compiler emits this for the ! operator on boolean expressions + int32_t result = (a == 0) ? 1 : 0; + stackPushTyped(ctx, RValue_makeBool(result != 0), resultType); + } else { + // Bitwise NOT: used for ~ operator on integer types + int32_t result = ~a; + stackPushTyped(ctx, RValue_makeInt32(result), resultType); + } +} + +static void handleShl(VMContext* ctx, uint32_t instr) { + SIMPLE_BYTECODE_BITWISE_OPERATION(<<); +} + +static void handleShr(VMContext* ctx, uint32_t instr) { + SIMPLE_BYTECODE_BITWISE_OPERATION(>>); +} + +static void handleConv(VMContext* ctx, uint8_t srcType, uint8_t dstType, uint8_t convKey) { + RValue val = stackPop(ctx); + + RValue result; + + switch (convKey) { + // Identity conversions (no-op) + case 0x00: case 0x22: case 0x33: case 0x44: case 0x66: + result = val; + break; + + // Double (0) -> other + case 0x20: result = RValue_makeInt32((int32_t) val.real); break; + case 0x30: result = RValue_makeInt64((int64_t) val.real); break; + case 0x40: result = RValue_makeBool(val.real > 0.5); break; + case 0x50: result = val; break; // Double -> Variable (passthrough) + case 0x60: { char* s = RValue_toString(val); result = RValue_makeOwnedString(s); break; } + case 0xF0: result = RValue_makeInt32((int32_t) val.real); break; + + // Float (1) -> other (float stored as double in our RValue) + case 0x01: result = RValue_makeReal(val.real); break; + case 0x21: result = RValue_makeInt32((int32_t) val.real); break; + case 0x31: result = RValue_makeInt64((int64_t) val.real); break; + case 0x41: result = RValue_makeBool(val.real > 0.5); break; + case 0x51: result = val; break; // Float -> Variable (passthrough) + + // Int32 (2) -> other + case 0x02: result = RValue_makeReal((GMLReal) val.int32); break; + case 0x12: result = RValue_makeReal((GMLReal) val.int32); break; + case 0x32: result = RValue_makeInt64((int64_t) val.int32); break; + case 0x42: result = RValue_makeBool(val.int32 > 0); break; + case 0x52: result = val; break; // Int32 -> Variable (passthrough) + case 0x62: { char* s = RValue_toString(val); result = RValue_makeOwnedString(s); break; } + case 0xF2: result = val; break; + +#ifndef NO_RVALUE_INT64 + // Int64 (3) -> other + case 0x03: result = RValue_makeReal((GMLReal) val.int64); break; + case 0x23: result = RValue_makeInt32((int32_t) val.int64); break; + case 0x43: result = RValue_makeBool(val.int64 > 0); break; + case 0x53: result = val; break; // Int64 -> Variable (passthrough) +#elif IS_BC17_OR_HIGHER_ENABLED + // Int64 (3) -> other (Int64 stored as Int32 when NO_RVALUE_INT64). + // Only emitted on BC17+ builds: BC16 games (Undertale, SURVEY_PROGRAM) never emit Int64 Conv opcodes. + case 0x03: result = RValue_makeReal((GMLReal) val.int32); break; + case 0x23: result = val; break; // Already Int32 + case 0x43: result = RValue_makeBool(val.int32 > 0); break; + case 0x53: result = val; break; // Int64 -> Variable (passthrough) +#endif + + // Bool (4) -> other + case 0x04: result = RValue_makeReal((GMLReal) val.int32); break; + case 0x24: result = RValue_makeInt32(val.int32); break; + case 0x34: result = RValue_makeInt64((int64_t) val.int32); break; + case 0x54: result = val; break; // Bool -> Variable (passthrough) + case 0x64: { char* s = RValue_toString(val); result = RValue_makeOwnedString(s); break; } + + // Variable (5) -> other + case 0x05: result = RValue_makeReal(RValue_toReal(val)); break; + case 0x15: result = RValue_makeReal(RValue_toReal(val)); break; + case 0x25: result = RValue_makeInt32(RValue_toInt32(val)); break; + case 0x35: result = RValue_makeInt64(RValue_toInt64(val)); break; + case 0x45: result = RValue_makeBool(RValue_toBool(val)); break; + case 0x55: result = val; break; // Variable -> Variable (identity) + case 0x65: { char* s = RValue_toString(val); result = RValue_makeOwnedString(s); break; } + case 0xF5: result = RValue_makeInt32(RValue_toInt32(val)); break; + + // String (6) -> other + case 0x06: result = RValue_makeReal(GMLReal_strtod(val.string, nullptr)); break; + case 0x26: result = RValue_makeInt32((int32_t) GMLReal_strtod(val.string, nullptr)); break; + case 0x36: result = RValue_makeInt64((int64_t) GMLReal_strtod(val.string, nullptr)); break; + case 0x46: result = RValue_makeBool(val.string != nullptr && val.string[0] != '\0'); break; + case 0x56: { + // String -> Variable: keep as-is since our RValue handles strings natively + result = val; + break; + } + + // Int16 (F) -> other + case 0x0F: result = RValue_makeReal((GMLReal) val.int32); break; + case 0x2F: result = val; break; + case 0x5F: result = val; break; + + default: + fprintf(stderr, "VM: [%s] Conv unhandled conversion 0x%02X (src=0x%X dst=0x%X)\n", ctx->currentCodeName, convKey, srcType, dstType); + result = val; + break; + } + + // Don't free the old value if we're returning the same value (identity conversion or passthrough) + if (result.string != val.string || result.type != val.type) { + RValue_free(&val); + } + + // Set gmlStackType to the destination type so Dup can compute correct byte sizes (BC17+ only) +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx)) { + result.gmlStackType = dstType; + } +#endif + stackPush(ctx, result); +} + +// Tries to parse a string as a real number, mirroring HTML5 yyCompareVal's behavior: +// trim leading whitespace, then accept a numeric prefix (sign, digits, decimal, exponent). +// Returns true on success, with the parsed value written to *out. +static bool tryParseRealFromString(const char* str, GMLReal* out) { + if (str == nullptr) return false; + while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r') str++; + if (*str == '\0') return false; + char* endPtr = nullptr; + GMLReal value = GMLReal_strtod(str, &endPtr); + if (endPtr == str) return false; + *out = value; + return true; +} + +static void handleCmp(VMContext* ctx, uint32_t instr) { + uint8_t cmpKind = instrCmpKind(instr); + RValue b = stackPop(ctx); + RValue a = stackPop(ctx); + + bool result; + if (a.type == RVALUE_UNDEFINED || b.type == RVALUE_UNDEFINED) { + // Undefined is only == to undefined + bool eq = a.type == b.type; + switch (cmpKind) { + case CMP_EQ: result = eq; break; + case CMP_NEQ: result = !eq; break; + default: result = false; break; + } + } else if (a.type == RVALUE_ARRAY || b.type == RVALUE_ARRAY) { + // Array is only == to the same array + bool eq = (a.type == RVALUE_ARRAY && b.type == RVALUE_ARRAY) && (a.array == b.array); + switch (cmpKind) { + case CMP_EQ: result = eq; break; + case CMP_NEQ: result = !eq; break; + default: result = false; break; + } +#if IS_BC17_OR_HIGHER_ENABLED + } else if (a.type == RVALUE_METHOD || b.type == RVALUE_METHOD) { + // Method is only == to the same method + bool eq = (a.type == RVALUE_METHOD && b.type == RVALUE_METHOD) && (a.method == b.method); + switch (cmpKind) { + case CMP_EQ: result = eq; break; + case CMP_NEQ: result = !eq; break; + default: result = false; break; + } +#endif + } else if (a.type == RVALUE_STRUCT || b.type == RVALUE_STRUCT) { + // Struct is only == to the same struct (identity comparison) + bool eq = (a.type == RVALUE_STRUCT && b.type == RVALUE_STRUCT) && (a.structInst == b.structInst); + switch (cmpKind) { + case CMP_EQ: result = eq; break; + case CMP_NEQ: result = !eq; break; + default: result = false; break; + } + } else if (a.type == RVALUE_STRING && b.type == RVALUE_STRING) { + int cmp = strcmp(a.string != nullptr ? a.string : "", b.string != nullptr ? b.string : ""); + switch (cmpKind) { + case CMP_LT: result = 0 > cmp; break; + case CMP_LTE: result = 0 >= cmp; break; + case CMP_EQ: result = cmp == 0; break; + case CMP_NEQ: result = cmp != 0; break; + case CMP_GTE: result = cmp >= 0; break; + case CMP_GT: result = cmp > 0; break; + default: result = false; break; + } + } else { + // Mixed string/number: coerce strings to reals (matching GameMaker-HTML5 yyCompareVal). + // Don't be fooled, this behavior is not a GameMaker-HTML5 (JavaScript) quirk! Some GameMaker games do use this, + // such as gml_Object_obj_ch2_scene6_Step_0 in DELTARUNE: Chapter 2, where the c_wait uses a string instead of a number + // + // If a string side fails to parse as a number, the values are considered incomparable: false for all comparisons except NEQ. + bool incomparable = false; + GMLReal da = 0.0; + GMLReal db = 0.0; + if (a.type == RVALUE_STRING) { + if (!tryParseRealFromString(a.string, &da)) incomparable = true; + } else { + da = RValue_toReal(a); + } + if (!incomparable) { + if (b.type == RVALUE_STRING) { + if (!tryParseRealFromString(b.string, &db)) incomparable = true; + } else { + db = RValue_toReal(b); + } + } + + if (incomparable) { + switch (cmpKind) { + case CMP_EQ: result = false; break; + case CMP_NEQ: result = true; break; + default: result = false; break; + } + } else { + GMLReal diff = da - db; + // GML uses epsilon-based comparison for all numeric CMP operations + int cmp = GMLReal_fabs(diff) <= GML_MATH_EPSILON ? 0 : (diff < 0 ? -1 : 1); + switch (cmpKind) { + case CMP_LT: result = cmp < 0; break; + case CMP_LTE: result = cmp <= 0; break; + case CMP_EQ: result = cmp == 0; break; + case CMP_NEQ: result = cmp != 0; break; + case CMP_GTE: result = cmp >= 0; break; + case CMP_GT: result = cmp > 0; break; + default: result = false; break; + } + } + } + + RValue_free(&a); + RValue_free(&b); + stackPush(ctx,RValue_makeBool(result)); +} + +#if IS_BC17_OR_HIGHER_ENABLED +// Converts a native byte count to RValue slot count by walking the stack backwards from a given position. +// Only used by BC17+ Dup paths; reads the per-slot gmlStackType which doesn't exist on BC16-only builds. +static int32_t bytesToSlotCount(VMContext* ctx, int32_t nativeBytes, int32_t stackPos) { + int32_t slots = 0; + int32_t remaining = nativeBytes; + while (remaining > 0) { + slots++; + require(stackPos >= slots); + uint8_t slotGmlType = ctx->stack.slots[stackPos - slots].gmlStackType; + remaining -= gmlTypeNativeSize(slotGmlType); + } + require(remaining == 0); // Byte count must align exactly to slot boundaries + return slots; +} +#endif + +static void handleDup(VMContext* ctx, uint32_t instr) { + uint16_t operand = (uint16_t)(instr & 0xFFFF); +#if IS_BC17_OR_HIGHER_ENABLED + uint8_t type1 = instrType1(instr); + int32_t typeSize = gmlTypeNativeSize(type1); + + // Swap mode: bit 15 of operand is set + // The Dup instruction doubles as a stack rotation when bit 15 is set. + // It takes the top N items and moves them below the next M items. + // Bits 0-10: top group size (in native type units) + // Bits 11-14: bottom group size (in native type units) + if (IS_BC17_OR_HIGHER(ctx) && (operand & 0x8000) != 0) { + int32_t topNativeCount = operand & 0x7FF; + int32_t bottomNativeCount = (operand >> 11) & 0xF; + int32_t topBytes = topNativeCount * typeSize; + int32_t bottomBytes = bottomNativeCount * typeSize; + + // Convert byte counts to slot counts + int32_t topSlots = bytesToSlotCount(ctx, topBytes, ctx->stack.top); + int32_t bottomSlots = bytesToSlotCount(ctx, bottomBytes, ctx->stack.top - topSlots); + + int32_t totalSlots = topSlots + bottomSlots; + int32_t baseIdx = ctx->stack.top - totalSlots; + + // Save top group to temp + RValue temp[32]; + for (int32_t i = 0; topSlots > i; i++) { + temp[i] = ctx->stack.slots[ctx->stack.top - topSlots + i]; + } + + // Shift bottom group up to where top group was + for (int32_t i = bottomSlots - 1; i >= 0; i--) { + ctx->stack.slots[baseIdx + topSlots + i] = ctx->stack.slots[baseIdx + i]; + } + + // Place top group at the bottom + for (int32_t i = 0; topSlots > i; i++) { + ctx->stack.slots[baseIdx + i] = temp[i]; + } + return; + } +#endif + + // Normal dup mode + int32_t count; + +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx)) { + // In bytecode 17+, the operand encodes a native element count: total bytes = (operand + 1) * typeSize(type1). + // The native runner's stack stores raw bytes (int=4, double=8, variable=16), but our VM uses uniform RValue slots. + // We walk backward through the stack, summing each slot's native size (tracked via gmlStackType), to find how many slots correspond to the byte count. + int32_t totalBytes = ((int32_t)(operand & 0x7FFF) + 1) * typeSize; + + count = bytesToSlotCount(ctx, totalBytes, ctx->stack.top); + } else { + // Bytecode 16: operand directly encodes how many additional items beyond 1 to duplicate (dup.i 0 = duplicate 1 item, dup.i 1 = duplicate 2 items, etc) + count = (int32_t)(operand & 0xFF) + 1; + require(ctx->stack.top >= count); + } +#else + // Bytecode 16: operand directly encodes how many additional items beyond 1 to duplicate + count = (int32_t)(operand & 0xFF) + 1; + require(ctx->stack.top >= count); +#endif + + // Copy 'count' items from the top of the stack (preserving order) + int32_t startIdx = ctx->stack.top - count; + for (int32_t i = 0; count > i; i++) { + RValue copy = ctx->stack.slots[startIdx + i]; + + // If the value owns a string, duplicate it to avoid double-free. + // For arrays and methods, bump the refcount so each duplicate independently owns a reference. + if (copy.type == RVALUE_STRING && copy.ownsReference && copy.string != nullptr) { + copy.string = safeStrdup(copy.string); + } else if (copy.type == RVALUE_ARRAY && copy.ownsReference && copy.array != nullptr) { + GMLArray_incRef(copy.array); +#if IS_BC17_OR_HIGHER_ENABLED + } else if (copy.type == RVALUE_METHOD && copy.ownsReference && copy.method != nullptr) { + GMLMethod_incRef(copy.method); +#endif + } else if (copy.type == RVALUE_STRUCT && copy.ownsReference && copy.structInst != nullptr) { + Instance_structIncRef(copy.structInst); + } + + stackPush(ctx, copy); + } +} + +// ===[ Function Call Handler ]=== + +static void handleCall(VMContext* ctx, uint32_t instr, const uint8_t* extraData) { + int32_t argCount = instr & 0xFFFF; + uint32_t funcIndex = resolveFuncOperand(extraData); + require(ctx->dataWin->func.functionCount > funcIndex); + const char* funcName = ctx->dataWin->func.functions[funcIndex].name; + + // Pop arguments from stack (args pushed right-to-left, so first arg is on top) + // Use stack-allocated buffer for small arg counts (GMS 1.4 supports up to 16 arguments) + RValue stackArgs[GML_MAX_ARGUMENTS]; + RValue* args = nullptr; + if (argCount > 0) { + args = (GML_MAX_ARGUMENTS >= argCount) ? stackArgs : safeMalloc(argCount * sizeof(RValue)); + repeat(argCount, i) { + args[i] = stackPop(ctx); + } + } + +#ifdef ENABLE_VM_TRACING + bool functionIsBeingTraced = shgeti(ctx->functionCallsToBeTraced, "*") != -1 || shgeti(ctx->functionCallsToBeTraced, funcName) != -1 || shgeti(ctx->functionCallsToBeTraced, ctx->currentCodeName) != -1; + char* functionArgumentList = nullptr; + if (functionIsBeingTraced) { + functionArgumentList = safeStrdup(""); + for (int32_t i = 0; i < argCount; i++) { + char* display = RValue_toStringFancy(args[i]); + + if (i > 0) { + char* tmp = safeMalloc(strlen(functionArgumentList) + 2 + strlen(display) + 1); + sprintf(tmp, "%s, %s", functionArgumentList, display); + free(functionArgumentList); + functionArgumentList = tmp; + } else { + free(functionArgumentList); + functionArgumentList = safeStrdup(display); + } + free(display); + } + + fprintf(stderr, "VM: [%s] Calling function \"%s(%s)\"\n", ctx->currentCodeName, funcName, functionArgumentList); + } +#endif + + // Use cached function resolution to avoid per-call string hash lookups + FuncCallCache* cache = &ctx->funcCallCache[funcIndex]; + + // Fast path: cached builtin function pointer + if (cache->builtin != nullptr) { + VMExec_traceBootstrapCall(ctx, "call builtin=%s argc=%d", funcName, argCount); + if (VMExec_shouldTraceCrashWindow()) { + char buffer[256]; + snprintf( + buffer, + sizeof(buffer), + "vmbuiltin: code=%s func=%s argc=%d", + ctx->currentCodeName != nullptr ? ctx->currentCodeName : "", + funcName != nullptr ? funcName : "", + argCount + ); + VMExec_bootLog(buffer); + } + BuiltinFunc builtin = (BuiltinFunc) cache->builtin; + RValue result = builtin(ctx, args, argCount); + result = strengthenReturnValue(result); + // Free arguments + if (args != nullptr) { + repeat(argCount, i) { + RValue_free(&args[i]); + } + if (args != stackArgs) free(args); + } + +#ifdef ENABLE_VM_TRACING + if (functionIsBeingTraced) { + char* returnValueAsString = RValue_toStringFancy(result); + fprintf(stderr, "VM: [%s] Built-in function \"%s(%s)\" returned %s\n", ctx->currentCodeName, funcName, functionArgumentList, returnValueAsString); + free(returnValueAsString); + free(functionArgumentList); + } +#endif + + stackPushTyped(ctx, result, GML_TYPE_VARIABLE); + return; + } + + // Fast path: cached script code index + if (cache->scriptCodeIndex >= 0) { + const char* targetCodeName = ctx->dataWin->code.entries[cache->scriptCodeIndex].name; + VMExec_traceBootstrapCall(ctx, "call script=%s argc=%d target=%s", funcName, argCount, targetCodeName); + RValue result = VM_callCodeIndex(ctx, cache->scriptCodeIndex, args, argCount); + +#ifdef ENABLE_VM_TRACING + if (functionIsBeingTraced) { + char* returnValueAsString = RValue_toStringFancy(result); + fprintf(stderr, "VM: [%s] Script function \"%s(%s)\" returned %s\n", ctx->currentCodeName, funcName, functionArgumentList, returnValueAsString); + free(returnValueAsString); + free(functionArgumentList); + } +#endif + + // Free arguments (VM_callCodeIndex copies what it needs) + if (args != nullptr) { + repeat(argCount, i) { + RValue_free(&args[i]); + } + if (args != stackArgs) free(args); + } + + stackPushTyped(ctx, result, GML_TYPE_VARIABLE); + return; + } + + // Slow path: unknown function (not cached as builtin or script) +#ifdef ENABLE_VM_STUB_LOGS + const char* unknownFuncName = funcName; + + // Log once per (callingCode, funcName) pair + const char* callerName = VM_getCallerName(ctx); + char* dedupKey = VM_createDedupKey(callerName, unknownFuncName); + + if (ctx->alwaysLogUnknownFunctions || 0 > shgeti(ctx->loggedUnknownFuncs, dedupKey)) { + shput(ctx->loggedUnknownFuncs, dedupKey, true); + fprintf(stderr, "VM: [%s] Unknown function \"%s\"!\n", callerName, unknownFuncName); + } else { + free(dedupKey); + } +#endif + + VMExec_traceBootstrapCall(ctx, "call unresolved=%s argc=%d", funcName, argCount); + + if (VMExec_shouldTraceCrashWindow()) { + fprintf( + stderr, + "vmcleanup: unresolved code=%s func=%s argc=%d\n", + ctx->currentCodeName != nullptr ? ctx->currentCodeName : "", + funcName != nullptr ? funcName : "", + argCount + ); + fflush(stderr); + if (args != nullptr) { + repeat(argCount, i) { + VMExec_traceCrashRValue("vmcleanup: arg", ctx->currentCodeName, i, &args[i]); + } + } + } + + // Free arguments and push undefined + if (args != nullptr) { + repeat(argCount, i) { + VMExec_traceCrashRValue("vmcleanup: free", ctx->currentCodeName, i, &args[i]); + RValue_free(&args[i]); + } + if (args != stackArgs) free(args); + } + +#ifdef ENABLE_VM_TRACING + if (functionIsBeingTraced) { + free(functionArgumentList); + } +#endif + + stackPush(ctx, RValue_makeUndefined()); +} + +#if IS_BC17_OR_HIGHER_ENABLED +// BC17+ CALLV: dynamic call through a variable (method/script reference). +// Stack layout (top -> bottom): function, instance, arg[N-1], ..., arg[0] +// argCount is in the low 16 bits of the instruction. +static void handleCallV(VMContext* ctx, uint32_t instr) { + int32_t argCount = instr & 0xFFFF; + + RValue function = stackPop(ctx); + RValue instance = stackPop(ctx); + + RValue stackArgs[GML_MAX_ARGUMENTS]; + RValue* args = nullptr; + if (argCount > 0) { + args = (GML_MAX_ARGUMENTS >= argCount) ? stackArgs : safeMalloc(argCount * sizeof(RValue)); + repeat(argCount, i) { + args[i] = stackPop(ctx); + } + } + + int32_t codeIndex = -1; + int32_t boundInstance = -1; + BuiltinFunc builtin = nullptr; + const char* unresolvedName = nullptr; + if (function.type == RVALUE_METHOD && function.method != nullptr) { + codeIndex = function.method->codeIndex; + boundInstance = function.method->boundInstanceId; + builtin = (BuiltinFunc) function.method->builtin; + unresolvedName = function.method->unresolvedName; + } + + // Decide target self: prefer method's bound instance, else the stack-provided instance. + int32_t targetInstance = (boundInstance > 0) ? boundInstance : RValue_toInt32(instance); + Instance* savedSelf = ctx->currentInstance; + if (targetInstance != INSTANCE_SELF && targetInstance != 0) { + Instance* target = findInstanceByTarget(ctx, targetInstance); + if (target != nullptr) ctx->currentInstance = target; + } + + RValue result; + if (VMExec_shouldTraceCrashWindow()) { + fprintf( + stderr, + "vmcallv: code=%s argc=%d fnType=%d fnOwns=%d fnRef=%p codeIndex=%d builtin=%p unresolved=%s bound=%d instanceType=%d instanceI32=%d\n", + ctx->currentCodeName != nullptr ? ctx->currentCodeName : "", + argCount, + function.type, + function.ownsReference ? 1 : 0, +#if IS_BC17_OR_HIGHER_ENABLED + function.type == RVALUE_METHOD ? (void*) function.method : nullptr, +#else + nullptr, +#endif + codeIndex, + (void*) builtin, + unresolvedName != nullptr ? unresolvedName : "", + boundInstance, + instance.type, + instance.int32 + ); + fflush(stderr); + VMExec_traceCrashRValue("vmcallv: function", ctx->currentCodeName, -1, &function); + VMExec_traceCrashRValue("vmcallv: instance", ctx->currentCodeName, -1, &instance); + if (args != nullptr) { + repeat(argCount, i) { + VMExec_traceCrashRValue("vmcallv: arg", ctx->currentCodeName, i, &args[i]); + } + } + } + + if (codeIndex >= 0 && ctx->dataWin->code.count > (uint32_t) codeIndex) { + result = VM_callCodeIndex(ctx, codeIndex, args, argCount); + } else if (builtin != nullptr) { + result = builtin(ctx, args, argCount); + } else if (unresolvedName != nullptr) { +#ifdef ENABLE_VM_STUB_LOGS + const char* callerName = VM_getCallerName(ctx); + char* dedupKey = VM_createDedupKey(callerName, unresolvedName); + if (ctx->alwaysLogUnknownFunctions || 0 > shgeti(ctx->loggedUnknownFuncs, dedupKey)) { + shput(ctx->loggedUnknownFuncs, dedupKey, true); + fprintf(stderr, "VM: [%s] Unknown function \"%s\"! (via CallV)\n", callerName, unresolvedName); + } else { + free(dedupKey); + } +#endif + result = RValue_makeUndefined(); + } else { + fprintf(stderr, "VM: [%s] CALLV with unresolvable function reference (type=%d, codeIndex=%d)\n", ctx->currentCodeName, function.type, codeIndex); + result = RValue_makeUndefined(); + } + + ctx->currentInstance = savedSelf; + + RValue_free(&function); + RValue_free(&instance); + if (args != nullptr) { + repeat(argCount, i) { + RValue_free(&args[i]); + } + if (args != stackArgs) free(args); + } + + stackPushTyped(ctx, result, GML_TYPE_VARIABLE); +} +#endif + +// ===[ With-Statement Helpers (PushEnv/PopEnv) ]=== + +// Checks if objectIndex is or inherits from targetObjectIndex by walking the parent chain. +bool VM_isObjectOrDescendant(DataWin* dataWin, int32_t objectIndex, int32_t targetObjectIndex) { + int32_t currentObj = objectIndex; + int depth = 0; + while (currentObj >= 0 && (uint32_t) currentObj < dataWin->objt.count && 32 > depth) { + if (currentObj == targetObjectIndex) return true; + currentObj = dataWin->objt.objects[currentObj].parentId; + depth++; + } + return false; +} + + +// Sets the VM instance context from an Instance. +static void switchToInstance(VMContext* ctx, Instance* inst) { + ctx->currentInstance = inst; +} + +// Restores VM context from an EnvFrame's saved fields. +static void restoreEnvContext(VMContext* ctx, EnvFrame* frame) { + ctx->currentInstance = frame->savedInstance; + ctx->otherInstance = frame->savedOtherInstance; +} + +static void handlePushEnv(VMContext* ctx, uint32_t instr, uint32_t instrAddr) { + int32_t jumpOffset = instrJumpOffset(instr); + + // Pop target from stack + int32_t target = stackPopInt32(ctx); + // BC17: -9 (INSTANCE_STACKTOP) means "pop again for the real target" + if (IS_BC17_OR_HIGHER(ctx) && target == INSTANCE_STACKTOP) { + target = resolveInstanceStackTop(ctx); + } + + // Create env frame, save current context + EnvFrame* frame = safeMalloc(sizeof(EnvFrame)); + frame->savedInstance = (Instance*) ctx->currentInstance; + frame->savedOtherInstance = (Instance*) ctx->otherInstance; + frame->instanceList = nullptr; + frame->currentIndex = 0; + frame->parent = ctx->envStack; + ctx->envStack = frame; + + // Inside a with-block, "other" refers to the instance that executed the with-statement + ctx->otherInstance = (Instance*) ctx->currentInstance; + + Runner* runner = (Runner*) ctx->runner; + + if (target == INSTANCE_SELF) { + // with(self) - no-op, keep current instance + return; + } + + if (target == INSTANCE_OTHER) { + // with(other) - switch to the instance that was "self" before the nearest enclosing with-block + // For nested with-blocks, other refers to the saved instance from the parent env frame + if (frame->parent != nullptr) { + switchToInstance(ctx, frame->parent->savedInstance); + } else if (ctx->otherInstance != nullptr) { + // No parent env frame, but we have an otherInstance (e.g., from collision events) + switchToInstance(ctx, (Instance*) ctx->otherInstance); + } + // If no parent frame and no otherInstance, keep the saved instance (no-op) + return; + } + + if (target == INSTANCE_NOONE) { + // with(noone) - skip the block entirely + ctx->ip = instrAddr + jumpOffset; + return; + } + + if (target == INSTANCE_ALL) { + // with(all) - iterate over all active instances + int32_t instanceCount = (int32_t) arrlen(runner->instances); + for (int32_t i = 0; instanceCount > i; i++) { + Instance* inst = runner->instances[i]; + if (inst->active) { + arrput(frame->instanceList, inst); + } + } + + if (arrlen(frame->instanceList) == 0) { + // No active instances, skip the block + ctx->ip = instrAddr + jumpOffset; + return; + } + + frame->currentIndex = 0; + switchToInstance(ctx, frame->instanceList[0]); + return; + } + + if (target >= 0 && 100000 > target) { + // Object index - copy the descendant-inclusive list for this object into the frame's own list. frame->instanceList has with-block lifetime (not the snapshot arena's loop lifetime), so we don't use the forEach macro; we just copy directly and filter "active" to match prior semantics (deactivated instances are skipped). + if (ctx->dataWin->objt.count > (uint32_t) target) { + Instance** source = runner->instancesByObject[target]; + int32_t sourceCount = (int32_t) arrlen(source); + for (int32_t i = 0; sourceCount > i; i++) { + Instance* inst = source[i]; + if (inst->active) arrput(frame->instanceList, inst); + } + } + + if (arrlen(frame->instanceList) == 0) { + // No matching instances, skip the block + ctx->ip = instrAddr + jumpOffset; + return; + } + + frame->currentIndex = 0; + switchToInstance(ctx, frame->instanceList[0]); + return; + } + + if (target >= 100000) { + // Instance ID - find specific instance + Instance* inst = hmget(runner->instancesById, target); + if (inst != nullptr && inst->active) { + switchToInstance(ctx, inst); + return; + } + + // Instance not found, skip the block + ctx->ip = instrAddr + jumpOffset; + return; + } + + fprintf(stderr, "VM: [%s] PushEnv with unhandled target %d\n", ctx->currentCodeName, target); + ctx->ip = instrAddr + jumpOffset; +} + +static void handlePopEnv(VMContext* ctx, uint32_t instr, uint32_t instrAddr) { + EnvFrame* frame = ctx->envStack; + require(frame != nullptr); + + // Check for exit magic: PopEnv with 0xF00000 operand means "unwind env stack and exit/return" + if ((instr & 0x00FFFFFF) == 0xF00000) { + // Restore context and pop frame + restoreEnvContext(ctx, frame); + ctx->envStack = frame->parent; + arrfree(frame->instanceList); + free(frame); + return; + } + + // Check if there are more instances to iterate + if (frame->instanceList != nullptr && arrlen(frame->instanceList) > frame->currentIndex + 1) { + frame->currentIndex++; + Instance* nextInst = frame->instanceList[frame->currentIndex]; + // Skip destroyed instances + while (!nextInst->active && arrlen(frame->instanceList) > frame->currentIndex + 1) { + frame->currentIndex++; + nextInst = frame->instanceList[frame->currentIndex]; + } + if (nextInst->active) { + switchToInstance(ctx, nextInst); + // Jump back to the start of the with-block body + int32_t jumpOffset = instrJumpOffset(instr); + ctx->ip = instrAddr + jumpOffset; + return; + } + } + + // Done iterating - restore context and pop frame + restoreEnvContext(ctx, frame); + ctx->envStack = frame->parent; + arrfree(frame->instanceList); + free(frame); +} + +// ===[ Execution Loop ]=== + +static const char* opcodeName(uint8_t opcode) { + switch (opcode) { + case OP_CONV: return "Conv"; + case OP_MUL: return "Mul"; + case OP_DIV: return "Div"; + case OP_REM: return "Rem"; + case OP_MOD: return "Mod"; + case OP_ADD: return "Add"; + case OP_SUB: return "Sub"; + case OP_AND: return "And"; + case OP_OR: return "Or"; + case OP_XOR: return "Xor"; + case OP_NEG: return "Neg"; + case OP_NOT: return "Not"; + case OP_SHL: return "Shl"; + case OP_SHR: return "Shr"; + case OP_CMP: return "Cmp"; + case OP_POP: return "Pop"; + case OP_PUSHI: return "PushI"; + case OP_DUP: return "Dup"; + case OP_RET: return "Ret"; + case OP_EXIT: return "Exit"; + case OP_POPZ: return "Popz"; + case OP_B: return "B"; + case OP_BT: return "BT"; + case OP_BF: return "BF"; + case OP_PUSHENV: return "PushEnv"; + case OP_POPENV: return "PopEnv"; + case OP_PUSH: return "Push"; + case OP_PUSHLOC: return "PushLoc"; + case OP_PUSHGLB: return "PushGlb"; + case OP_PUSHBLTN:return "PushBltn"; + case OP_CALL: return "Call"; + case OP_CALLV: return "CallV"; + case OP_BREAK: return "Break"; + default: return "???"; + } +} + +#ifdef ENABLE_VM_OPCODE_PROFILER +static char gmlTypeChar(uint8_t type); + +static const char* rvalueTypeName(uint8_t type) { + switch (type) { + case RVALUE_REAL: return "REAL"; + case RVALUE_STRING: return "STRING"; + case RVALUE_INT32: return "INT32"; + case RVALUE_INT64: return "INT64"; + case RVALUE_BOOL: return "BOOL"; + case RVALUE_UNDEFINED: return "UNDEF"; + case RVALUE_ARRAY: return "ARRAY"; + case RVALUE_METHOD: return "METHOD"; + case RVALUE_STRUCT: return "STRUCT"; + case 0xF: return "-"; + default: return "???"; + } +} + +static const char* breakSubOpName(int16_t breakType) { + switch (breakType) { + case BREAK_CHKINDEX: return "chkindex"; + case BREAK_PUSHAF: return "pushaf"; + case BREAK_POPAF: return "popaf"; + case BREAK_PUSHAC: return "pushac"; + case BREAK_SETOWNER: return "setowner"; + case BREAK_ISSTATICOK: return "isstaticok"; + case BREAK_SETSTATIC: return "setstatic"; + case BREAK_SAVEAREF: return "savearef"; + case BREAK_RESTOREAREF: return "restorearef"; + default: return "???"; + } +} + +void VM_printOpcodeProfilerReport(const VMContext* ctx) { + if (!ctx->opcodeProfilerEnabled) return; + + typedef struct { uint16_t key; uint64_t count; } CountEntry; + CountEntry entries[256]; + int entryCount = 0; + uint64_t total = 0; + for (int i = 0; 256 > i; i++) { + if (ctx->opcodeCounts[i] > 0) { + entries[entryCount].key = (uint16_t) i; + entries[entryCount].count = ctx->opcodeCounts[i]; + entryCount++; + total += ctx->opcodeCounts[i]; + } + } + + // Simple insertion sort (max 256 entries, runs once at shutdown) + for (int i = 1; entryCount > i; i++) { + CountEntry tmp = entries[i]; + int j = i; + while (j > 0 && entries[j - 1].count < tmp.count) { + entries[j] = entries[j - 1]; + j--; + } + entries[j] = tmp; + } + + fprintf(stderr, "=== Opcode Profiler Report ===\n"); + fprintf(stderr, "Total instructions executed: %llu\n", (unsigned long long) total); + fprintf(stderr, "%-12s %-6s %16s %8s\n", "Opcode", "Hex", "Count", "Pct"); + forEachIndexed(CountEntry, entry, i, entries, entryCount) { + (void) i; + double pct = total > 0 ? (100.0 * (double) entry->count / (double) total) : 0.0; + fprintf(stderr, "%-12s 0x%02X %16llu %7.2f%%\n", opcodeName((uint8_t) entry->key), (uint8_t) entry->key, (unsigned long long) entry->count, pct); + } + + // Per-opcode breakdown by type variant. Sorted within each opcode by count desc. + fprintf(stderr, "\n--- Type variant breakdown (per opcode) ---\n"); + forEachIndexed(CountEntry, entry, idx, entries, entryCount) { + (void) idx; + uint8_t opcode = (uint8_t) entry->key; + const uint64_t* variants = &ctx->opcodeVariantCounts[opcode * 256]; + + CountEntry variantEntries[256]; + int variantCount = 0; + for (int t = 0; 256 > t; t++) { + if (variants[t] > 0) { + variantEntries[variantCount].key = (uint16_t) t; + variantEntries[variantCount].count = variants[t]; + variantCount++; + } + } + for (int i = 1; variantCount > i; i++) { + CountEntry tmp = variantEntries[i]; + int j = i; + while (j > 0 && variantEntries[j - 1].count < tmp.count) { + variantEntries[j] = variantEntries[j - 1]; + j--; + } + variantEntries[j] = tmp; + } + + fprintf(stderr, "%s (0x%02X): %llu total\n", opcodeName(opcode), opcode, (unsigned long long) entry->count); + forEachIndexed(CountEntry, ve, vi, variantEntries, variantCount) { + (void) vi; + uint8_t type1 = (uint8_t) ((ve->key >> 4) & 0xF); + uint8_t type2 = (uint8_t) (ve->key & 0xF); + double vpct = entry->count > 0 ? (100.0 * (double) ve->count / (double) entry->count) : 0.0; + fprintf(stderr, " .%c.%c %16llu %7.2f%%\n", gmlTypeChar(type1), gmlTypeChar(type2), (unsigned long long) ve->count, vpct); + } + + // Runtime RValue type breakdown (a, b types observed at execution time) + { + const uint64_t* rvCounts = &ctx->opcodeRValueTypeCounts[opcode * 256]; + CountEntry rvEntries[256]; + int rvCount = 0; + uint64_t rvTotal = 0; + for (int t = 0; 256 > t; t++) { + if (rvCounts[t] > 0) { + rvEntries[rvCount].key = (uint16_t) t; + rvEntries[rvCount].count = rvCounts[t]; + rvCount++; + rvTotal += rvCounts[t]; + } + } + if (rvCount > 0) { + for (int i = 1; rvCount > i; i++) { + CountEntry tmp = rvEntries[i]; + int j = i; + while (j > 0 && rvEntries[j - 1].count < tmp.count) { + rvEntries[j] = rvEntries[j - 1]; + j--; + } + rvEntries[j] = tmp; + } + fprintf(stderr, " -- runtime types (a, b):\n"); + forEachIndexed(CountEntry, re, ri, rvEntries, rvCount) { + (void) ri; + uint8_t typeA = (uint8_t) ((re->key >> 4) & 0xF); + uint8_t typeB = (uint8_t) (re->key & 0xF); + double rpct = rvTotal > 0 ? (100.0 * (double) re->count / (double) rvTotal) : 0.0; + fprintf(stderr, " (%-6s, %-6s) %16llu %7.2f%%\n", rvalueTypeName(typeA), rvalueTypeName(typeB), (unsigned long long) re->count, rpct); + } + } + } + + // Extended BREAK (0xFF) sub-opcode breakdown + if (opcode == OP_BREAK) { + CountEntry breakEntries[64]; + int breakCount = 0; + for (int i = 0; 64 > i; i++) { + if (ctx->breakSubOpCounts[i] > 0) { + breakEntries[breakCount].key = (uint16_t) i; + breakEntries[breakCount].count = ctx->breakSubOpCounts[i]; + breakCount++; + } + } + for (int i = 1; breakCount > i; i++) { + CountEntry tmp = breakEntries[i]; + int j = i; + while (j > 0 && breakEntries[j - 1].count < tmp.count) { + breakEntries[j] = breakEntries[j - 1]; + j--; + } + breakEntries[j] = tmp; + } + fprintf(stderr, " -- sub-opcodes:\n"); + forEachIndexed(CountEntry, be, bi, breakEntries, breakCount) { + (void) bi; + int16_t breakType = (int16_t) -((int) be->key); + double bpct = entry->count > 0 ? (100.0 * (double) be->count / (double) entry->count) : 0.0; + fprintf(stderr, " %-12s (%4d) %16llu %7.2f%%\n", breakSubOpName(breakType), (int) breakType, (unsigned long long) be->count, bpct); + } + } + } + fprintf(stderr, "==============================\n"); +} +#endif // ENABLE_VM_OPCODE_PROFILER + +// Forward declaration for formatInstruction (defined in disassembler section, used by trace-opcodes) +static void formatInstruction(VMContext* ctx, const uint8_t* bytecodeBase, uint32_t instrAddr, uint32_t instr, const uint8_t* extraData, char* opcodeStr, size_t opcodeSize, char* operandStr, size_t operandSize, char* commentStr, size_t commentSize); + +#if IS_BC17_OR_HIGHER_ENABLED +// ===[ BREAK sub-opcode handlers (BC17+) ]=== + +static void handleBreakChkIndex(VMContext* ctx, uint32_t instrAddr) { + // Validate top-of-stack array index is in [0, 32000) + RValue* top = stackPeek(ctx); + int32_t idx = RValue_toInt32(*top); + if (0 > idx || 32000 <= idx) { + fprintf(stderr, "VM: chkindex out of bounds: %d at offset %u in %s\n", idx, instrAddr, ctx->currentCodeName); + abort(); + } +} + +static void handleBreakPushAF(VMContext* ctx) { + // Pop index + array ref, push array[index]. Array ref is a weak RVALUE_ARRAY pointer. + int32_t idx = stackPopInt32(ctx); + RValue arrayRef = stackPop(ctx); + RValue result; + if (VMExec_shouldTraceArrayOps()) { + char buffer[256]; + snprintf( + buffer, + sizeof(buffer), + "vmarray: pushaf code=%s idx=%d type=%d arr=%p owns=%d stack=%d", + ctx->currentCodeName != nullptr ? ctx->currentCodeName : "", + idx, + (int) arrayRef.type, + (void*) arrayRef.array, + arrayRef.ownsReference ? 1 : 0, + ctx->stack.top + ); + VMExec_bootLog(buffer); + } + RValue* cell = arrayRef.type == RVALUE_ARRAY ? GMLArray_slot(arrayRef.array, idx) : nullptr; + if (cell != nullptr) { + result = *cell; + result.ownsReference = false; // weak view + if (VMExec_shouldTraceArrayOps()) { + char buffer[256]; + snprintf( + buffer, + sizeof(buffer), + "vmarray: pushaf-hit code=%s idx=%d cell=%p cellType=%d", + ctx->currentCodeName != nullptr ? ctx->currentCodeName : "", + idx, + (void*) cell, + (int) cell->type + ); + VMExec_bootLog(buffer); + } + } else { + result = (RValue){ .type = RVALUE_UNDEFINED }; + if (VMExec_shouldTraceArrayOps()) { + char buffer[256]; + snprintf( + buffer, + sizeof(buffer), + "vmarray: pushaf-miss code=%s idx=%d type=%d arr=%p", + ctx->currentCodeName != nullptr ? ctx->currentCodeName : "", + idx, + (int) arrayRef.type, + (void*) arrayRef.array + ); + VMExec_bootLog(buffer); + } + } + stackPush(ctx, result); + RValue_free(&arrayRef); +} + +static void handleBreakPopAF(VMContext* ctx) { + // Pop index + array ref + value, store value at array[index]. + // CoW via VM_arrayWriteAt requires a slot pointer, since the stack-held arrayRef is a weak view, the real slot is whatever variable holds this array. + // We can't easily recover the slot here, so we write directly into the array (no CoW fork at this level, fork already happened when the top-level variable was first written, or on a PUSHAC materialisation). + // Assert the array is uniquely-owned or matches the current scope owner. A mismatch here means a shared/aliased array is about to be mutated in place, which silently breaks CoW semantics. BC17+ default mode (pass by reference) is expected to satisfy this since fork already happened at the top-level write. If this fires, a CoW path upstream failed to fork. + int32_t idx = stackPopInt32(ctx); + RValue arrayRef = stackPop(ctx); + RValue value = stackPop(ctx); + if (VMExec_shouldTraceCrashWindow()) { + char buffer[256]; + snprintf( + buffer, + sizeof(buffer), + "vmarray: popaf code=%s idx=%d arrType=%d arrOwns=%d arr=%p", + ctx->currentCodeName != nullptr ? ctx->currentCodeName : "", + idx, + (int) arrayRef.type, + arrayRef.ownsReference ? 1 : 0, + (void*) arrayRef.array + ); + VMExec_bootLog(buffer); + VMExec_traceCrashRValue("vmarray: popaf-array", ctx->currentCodeName, idx, &arrayRef); + VMExec_traceCrashRValue("vmarray: popaf-value", ctx->currentCodeName, idx, &value); + } + if (arrayRef.type == RVALUE_ARRAY && arrayRef.array != nullptr && idx >= 0) { + GMLArray* arr = arrayRef.array; + if (VMExec_shouldTraceCrashWindow()) { + char buffer[256]; + snprintf( + buffer, + sizeof(buffer), + "vmarray: popaf-live code=%s idx=%d refCount=%d owner=%p currentOwner=%p", + ctx->currentCodeName != nullptr ? ctx->currentCodeName : "", + idx, + arr->refCount, + arr->owner, + ctx->currentArrayOwner + ); + VMExec_bootLog(buffer); + } + requireMessage(arr->refCount == 1 || arr->owner == ctx->currentArrayOwner, "BREAK_POPAF: Writing through shared/aliased array without prior CoW fork"); + GMLArray_growTo(arr, idx + 1); + storeIntoArraySlot(GMLArray_slot(arr, idx), value); + } + RValue_free(&arrayRef); + RValue_free(&value); +} + +static void handleBreakPushAC(VMContext* ctx, uint32_t instrAddr) { + // Pop index + parent array ref, push sub-array at parent[index]. Materialise a fresh sub-array if the slot isn't already an RVALUE_ARRAY (multi-dim auto-init). + int32_t idx = stackPopInt32(ctx); + RValue arrayRef = stackPop(ctx); + if (arrayRef.type != RVALUE_ARRAY || arrayRef.array == nullptr) { + fprintf(stderr, "VM: pushac on non-array (type=%d) at offset %u in %s\n", arrayRef.type, instrAddr, ctx->currentCodeName); + abort(); + } + GMLArray* parent = arrayRef.array; + GMLArray_growTo(parent, idx + 1); + RValue* parentSlot = GMLArray_slot(parent, idx); + if (parentSlot->type != RVALUE_ARRAY || parentSlot->array == nullptr) { + RValue_free(parentSlot); + GMLArray* sub = GMLArray_create(0); + sub->owner = parent->owner; + RValue rv = { .type = RVALUE_ARRAY, .ownsReference = true, RVALUE_INIT_GMLTYPE(GML_TYPE_VARIABLE) }; + rv.array = sub; + *parentSlot = rv; + } + stackPush(ctx, RValue_makeArrayWeak(parentSlot->array)); + RValue_free(&arrayRef); +} + +static void handleBreakSetOwner(VMContext* ctx) { + // CoW scope owner for BC17+. + // The bytecode emits this at the top of each script or event, passing a token (usually self-instance ID cast to int) that uniquely identifies the current scope. + // Arrays whose .owner doesn't match fork on write. + RValue value = stackPop(ctx); + int64_t token = RValue_toInt64(value); + ctx->currentArrayOwner = (void*) (intptr_t) token; + RValue_free(&value); +} + +static void handleBreakIsStaticOk(VMContext* ctx) { + // Push bool: has this function's static block already run? + bool initialized = ctx->staticInitialized[ctx->currentCodeIndex]; + stackPush(ctx, RValue_makeBool(initialized)); +} + +static void handleBreakSetStatic(VMContext* ctx) { + // Mark current function's static as initialized + ctx->staticInitialized[ctx->currentCodeIndex] = true; +} + +static void handleBreakSaveARef(VMContext* ctx) { + // Native 2.3: SAVEAREF does `g_pSavedArraySetContainer = g_pArraySetContainer`, doesn't touch the stack. + // `g_pArraySetContainer` is a runner-global set by PUSHAC when traversing multi-dim parents, used by SET_RValue_Array as the container to write into. + // Since our PUSHAC pushes the sub-array directly onto the VM stack instead of stashing it in a container, this is a no-op. + // + // To track if we are doing everything correct, we'll track the savearefBalance to figure out when a game does something wrong. + ctx->savearefBalance++; +} + +static void handleBreakRestoreARef(VMContext* ctx) { + // Native 2.3: restores `g_pArraySetContainer` from the saved slot. No-op here (see BREAK_SAVEAREF). + // A negative balance means RESTOREAREF was emitted without a matching SAVEAREF, which means that we are doing things wrong or it is a bytecode pattern that we don't understand. + requireMessage(ctx->savearefBalance > 0, "BREAK_RESTOREAREF without matching SAVEAREF"); + ctx->savearefBalance--; +} + +static void handleBreak(VMContext* ctx, uint32_t instr, uint32_t instrAddr) { + if (IS_BC16_OR_BELOW(ctx)) return; + int16_t breakType = instrInstanceType(instr); + switch (breakType) { + case BREAK_CHKINDEX: handleBreakChkIndex(ctx, instrAddr); break; + case BREAK_PUSHAF: handleBreakPushAF(ctx); break; + case BREAK_POPAF: handleBreakPopAF(ctx); break; + case BREAK_PUSHAC: handleBreakPushAC(ctx, instrAddr); break; + case BREAK_SETOWNER: handleBreakSetOwner(ctx); break; + case BREAK_ISSTATICOK: handleBreakIsStaticOk(ctx); break; + case BREAK_SETSTATIC: handleBreakSetStatic(ctx); break; + case BREAK_SAVEAREF: handleBreakSaveARef(ctx); break; + case BREAK_RESTOREAREF: handleBreakRestoreARef(ctx); break; + default: + fprintf(stderr, "VM: Unknown BREAK sub-opcode %d at offset %u in %s\n", breakType, instrAddr, ctx->currentCodeName); + abort(); + } +} +#endif + +#define VM_SYNC_IP() do { ctx->ip = ip; } while (0) +#define VM_RELOAD_IP() do { ip = ctx->ip; } while (0) + +static RValue executeLoop(VMContext* ctx) { + // codeEnd and bytecodeBase are invariant for the lifetime of this executeLoop call, so let's hoist them to avoid the compiler emitting code to + // reload the values at the end of every iteration. + const uint32_t codeEnd = ctx->codeEnd; + const uint8_t* const bytecodeBase = ctx->bytecodeBase; + // If you just joined the stream: ip is short for instruction pointer chat + // The ip is mutable, so we need to use VM_SYNC_IP and VM_RELOAD_IP every time an opcode handler may access it or write to it + uint32_t ip = ctx->ip; + + // Some opcodes have their handler or parts of their handler inlined + // Those are opcodes that during real gameplay (using "--profile-opcodes") shown that, with inlining and keeping only the frequently called handle parts, we could squeeze MORE performance from the interpreter! + while (codeEnd > ip) { +#ifdef ENABLE_VM_GML_PROFILER + if (ctx->profiler != nullptr) + Profiler_tickInstruction(ctx->profiler); +#endif + uint32_t instrAddr = ip; + uint32_t instr = BinaryUtils_readUint32Aligned(bytecodeBase + ip); + ip += 4; + + // extraData pointer (may not be used depending on opcode) + const uint8_t* extraData = bytecodeBase + ip; + + // If instruction has extra data (bit 30 set), advance IP past it + if (instrHasExtraData(instr)) { + ip += extraDataSize(instrType1(instr)); + } + + uint8_t opcode = instrOpcode(instr); + +#ifdef ENABLE_VM_OPCODE_PROFILER + if (ctx->opcodeProfilerEnabled) { + ctx->opcodeCounts[opcode]++; + ctx->opcodeVariantCounts[opcode * 256 + instrType1(instr) * 16 + instrType2(instr)]++; + if (opcode == OP_BREAK) { + int16_t breakType = instrInstanceType(instr); + int idx = -breakType; + if (idx >= 0 && 64 > idx) { + ctx->breakSubOpCounts[idx]++; + } + } + // Capture actual runtime RValue types for arithmetic/comparison/conversion ops. + // typeB = 0xF sentinel for unary ops (no second operand). + uint8_t rvTypeA = 0xFF, rvTypeB = 0xF; + switch (opcode) { + case OP_MUL: case OP_DIV: case OP_REM: case OP_MOD: + case OP_ADD: case OP_SUB: case OP_AND: case OP_OR: + case OP_XOR: case OP_SHL: case OP_SHR: case OP_CMP: + if (ctx->stack.top >= 2) { + rvTypeA = ctx->stack.slots[ctx->stack.top - 2].type; + rvTypeB = ctx->stack.slots[ctx->stack.top - 1].type; + } + break; + case OP_NEG: case OP_NOT: case OP_CONV: + if (ctx->stack.top >= 1) { + rvTypeA = ctx->stack.slots[ctx->stack.top - 1].type; + } + break; + } + if (rvTypeA != 0xFF) { + ctx->opcodeRValueTypeCounts[opcode * 256 + (rvTypeA & 0xF) * 16 + (rvTypeB & 0xF)]++; + } + } +#endif + +#ifdef ENABLE_VM_TRACING + if (shlen(ctx->opcodesToBeTraced) > 0 && ctx->runner->frameCount >= ctx->traceBytecodeAfterFrame) { + if (shgeti(ctx->opcodesToBeTraced, "*") != -1 || shgeti(ctx->opcodesToBeTraced, ctx->currentCodeName) != -1) { + char opcodeStr[32], operandStr[256] = "", commentStr[128] = ""; + formatInstruction(ctx, ctx->bytecodeBase, instrAddr, instr, extraData, opcodeStr, sizeof(opcodeStr), operandStr, sizeof(operandStr), commentStr, sizeof(commentStr)); + + char* stackBuf = formatStackContents(ctx); + + if (operandStr[0] != '\0') { + fprintf(stderr, "VM: [%s] @%04X [0x%08X] %s %s [stack=%d] %s\n", ctx->currentCodeName, instrAddr, instr, opcodeStr, operandStr, ctx->stack.top, stackBuf); + } else { + fprintf(stderr, "VM: [%s] @%04X [0x%08X] %s [stack=%d] %s\n", ctx->currentCodeName, instrAddr, instr, opcodeStr, ctx->stack.top, stackBuf); + } + free(stackBuf); + } + } +#endif + + switch (opcode) { + // Push instructions + case OP_PUSH: { + uint8_t type1 = instrType1(instr); + // Inline fast paths for variable reads (not ints, doubles, etc, only VARIABLES) that are "normal" type (not arrays, not stacktop, and not the new fangled BC17 array reads) + if (type1 == GML_TYPE_VARIABLE) { + uint32_t varRef = resolveVarOperand(extraData); + uint8_t varType = (uint8_t) ((varRef >> 24) & 0xF8); + if (varType == VARTYPE_NORMAL) { + Variable* varDef = resolveVarDef(ctx, varRef); + if (varDef->varID >= 0) { + int32_t instanceType = (int32_t) instrInstanceType(instr); + RValue val; + if (tryFastVarRead(ctx, instanceType, varDef, &val)) { + stackPushTyped(ctx, val, GML_TYPE_VARIABLE); + break; + } + } + } + } + handlePush(ctx, instr, extraData, type1); + break; + } + case OP_PUSHLOC: { + uint32_t varRef = resolveVarOperand(extraData); +#if IS_BC17_OR_HIGHER_ENABLED + uint8_t varType = (uint8_t) ((varRef >> 24) & 0xF8); + if (varType == VARTYPE_ARRAYPUSHAF || varType == VARTYPE_ARRAYPOPAF) { + Variable* varDef = resolveVarDef(ctx, varRef); + uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); + require(ctx->localVarCount > localSlot); + pushTopLevelArrayRef(ctx, &ctx->localVars[localSlot]); + break; + } +#endif + // Locals are always non-builtin (varID >= 0); inline the read straight from localVars[]. + Variable* varDef = resolveVarDef(ctx, varRef); + uint32_t localSlot = resolveLocalSlot(ctx, varDef->varID); + require(ctx->localVarCount > localSlot); + RValue val = ctx->localVars[localSlot]; + val.ownsReference = false; + stackPushTyped(ctx, val, GML_TYPE_VARIABLE); + break; + } + case OP_PUSHGLB: { + uint32_t varRef = resolveVarOperand(extraData); + // Globals are not ALWAYS non-builtin (varID >= 0), some games may use the deprecated global builtins (like "score") with PUSHGLB. + // So due to that, we'll take the slow path if it is a builtin variable. + // The native runner does NOT handle global arrays from this path, so we don't need to care about them. + Variable* varDef = resolveVarDef(ctx, varRef); + if (varDef->varID == -6) { + RValue val = resolveVariableRead(ctx, INSTANCE_GLOBAL, varRef); + stackPushTyped(ctx, val, GML_TYPE_VARIABLE); + break; + } + // Inline the read straight from globalVars[]. + require(ctx->globalVarCount > (uint32_t) varDef->varID); + RValue val = ctx->globalVars[varDef->varID]; + val.ownsReference = false; + stackPushTyped(ctx, val, GML_TYPE_VARIABLE); + break; + } + case OP_PUSHBLTN: + handlePushBltn(ctx, instr, extraData); + break; + case OP_PUSHI: + handlePushI(ctx, instr); + break; + + // Pop instructions + case OP_POP: { + uint8_t type1 = instrType1(instr); + uint32_t varRef = resolveVarOperand(extraData); + uint8_t varType = (uint8_t) ((varRef >> 24) & 0xF8); + int32_t instanceType = instrInstanceType(instr); + // BC17: VARTYPE_INSTANCE encodes (instanceId - 100000) in the instruction's lower 16 bits. + if (varType == VARTYPE_INSTANCE) instanceType += 100000; + int32_t type2 = instrType2(instr); // source type (what's on stack) + if (type1 == GML_TYPE_VARIABLE && varType == VARTYPE_NORMAL) { + // Inline fast path for the simple variable-assignment case: type1==VARIABLE, which is ~99.998% of all Pops in real workloads + RValue val = stackPop(ctx); + val = coerceIntStoreToReal(val, type2); + resolveVariableWrite(ctx, instanceType, varRef, val); + } else { + handlePop(ctx, instr, type1, type2, varRef, varType, instanceType); + } + break; + } + case OP_POPZ: + handlePopz(ctx); + break; + + // Arithmetic + // We keep the number + number operations inlined in executeLoop, keeping only the slow path for string concat/repetition + case OP_ADD: { + RValue* slotA = &ctx->stack.slots[ctx->stack.top - 2]; + RValue* slotB = &ctx->stack.slots[ctx->stack.top - 1]; + uint8_t aType = slotA->type; + uint8_t bType = slotB->type; + if ((aType == RVALUE_INT32 || aType == RVALUE_REAL) && (bType == RVALUE_INT32 || bType == RVALUE_REAL)) { + if (aType == RVALUE_INT32 && bType == RVALUE_INT32) { + slotA->int32 = slotA->int32 + slotB->int32; + } else { + // Read both operands as locals before writing back, since the union means + // slotA->real and slotA->int32 share storage. + GMLReal aVal = (aType == RVALUE_INT32) ? (GMLReal) slotA->int32 : slotA->real; + GMLReal bVal = (bType == RVALUE_INT32) ? (GMLReal) slotB->int32 : slotB->real; + slotA->real = aVal + bVal; + slotA->type = RVALUE_REAL; + } +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx)) slotA->gmlStackType = instrType2(instr); +#endif + ctx->stack.top--; + } else { + uint8_t resultType = instrType2(instr); + RValue b = stackPop(ctx); + RValue a = stackPop(ctx); + if (a.type == RVALUE_STRING || b.type == RVALUE_STRING) { + handleAddString(ctx, a, b, resultType); + break; + } +#ifndef NO_RVALUE_INT64 + if (a.type == RVALUE_INT64 && b.type == RVALUE_INT64) { + stackPushTyped(ctx, RValue_makeInt64(a.int64 + b.int64), resultType); + break; + } +#endif + GMLReal result = RValue_toReal(a) + RValue_toReal(b); + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeReal(result), resultType); + } + break; + } + case OP_SUB: { + RValue* slotA = &ctx->stack.slots[ctx->stack.top - 2]; + RValue* slotB = &ctx->stack.slots[ctx->stack.top - 1]; + uint8_t aType = slotA->type; + uint8_t bType = slotB->type; + if ((aType == RVALUE_INT32 || aType == RVALUE_REAL) && (bType == RVALUE_INT32 || bType == RVALUE_REAL)) { + if (aType == RVALUE_INT32 && bType == RVALUE_INT32) { + slotA->int32 = slotA->int32 - slotB->int32; + } else { + GMLReal aVal = (aType == RVALUE_INT32) ? (GMLReal) slotA->int32 : slotA->real; + GMLReal bVal = (bType == RVALUE_INT32) ? (GMLReal) slotB->int32 : slotB->real; + slotA->real = aVal - bVal; + slotA->type = RVALUE_REAL; + } +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx)) slotA->gmlStackType = instrType2(instr); +#endif + ctx->stack.top--; + } else { + uint8_t resultType = instrType2(instr); + RValue b = stackPop(ctx); + RValue a = stackPop(ctx); +#ifndef NO_RVALUE_INT64 + if (a.type == RVALUE_INT64 && b.type == RVALUE_INT64) { + stackPushTyped(ctx, RValue_makeInt64(a.int64 - b.int64), resultType); + break; + } +#endif + GMLReal result = RValue_toReal(a) - RValue_toReal(b); + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeReal(result), resultType); + } + break; + } + case OP_MUL: { + RValue* slotA = &ctx->stack.slots[ctx->stack.top - 2]; + RValue* slotB = &ctx->stack.slots[ctx->stack.top - 1]; + uint8_t aType = slotA->type; + uint8_t bType = slotB->type; + if ((aType == RVALUE_INT32 || aType == RVALUE_REAL) && (bType == RVALUE_INT32 || bType == RVALUE_REAL)) { + if (aType == RVALUE_INT32 && bType == RVALUE_INT32) { + slotA->int32 = slotA->int32 * slotB->int32; + } else { + GMLReal aVal = (aType == RVALUE_INT32) ? (GMLReal) slotA->int32 : slotA->real; + GMLReal bVal = (bType == RVALUE_INT32) ? (GMLReal) slotB->int32 : slotB->real; + slotA->real = aVal * bVal; + slotA->type = RVALUE_REAL; + } +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx)) slotA->gmlStackType = instrType2(instr); +#endif + ctx->stack.top--; + } else { + uint8_t resultType = instrType2(instr); + RValue b = stackPop(ctx); + RValue a = stackPop(ctx); + if (a.type == RVALUE_STRING) { + handleMulString(ctx, a, b, resultType); + break; + } +#ifndef NO_RVALUE_INT64 + if (a.type == RVALUE_INT64 && b.type == RVALUE_INT64) { + stackPushTyped(ctx, RValue_makeInt64(a.int64 * b.int64), resultType); + break; + } +#endif + GMLReal result = RValue_toReal(a) * RValue_toReal(b); + RValue_free(&a); + RValue_free(&b); + stackPushTyped(ctx, RValue_makeReal(result), resultType); + } + break; + } + case OP_DIV: handleDiv(ctx, instr); break; + case OP_REM: handleRem(ctx, instr); break; + case OP_MOD: handleMod(ctx, instr); break; + + // Bitwise / Logical + case OP_AND: handleAnd(ctx, instr); break; + case OP_OR: handleOr(ctx, instr); break; + case OP_XOR: handleXor(ctx, instr); break; + case OP_SHL: handleShl(ctx, instr); break; + case OP_SHR: handleShr(ctx, instr); break; + + // Unary + case OP_NEG: handleNeg(ctx, instr); break; + case OP_NOT: handleNot(ctx, instr); break; + + // Type conversion + case OP_CONV: { + uint8_t srcType = instrType1(instr); + uint8_t dstType = instrType2(instr); + uint8_t convKey = (uint8_t) ((dstType << 4) | srcType); + RValue* top = &ctx->stack.slots[ctx->stack.top - 1]; + bool fastHit = false; + + // Inline fast paths for the four conversions that account for ~93% of all Conv opcodes in real workloads + switch (convKey) { + case 0x52: // Int32 -> Variable (pure passthrough; just retag stack slot) + fastHit = true; + break; + case 0x45: // Variable -> Bool + if (top->type == RVALUE_INT32) { + top->int32 = top->int32 > 0 ? 1 : 0; + top->type = RVALUE_BOOL; + fastHit = true; + } else if (top->type == RVALUE_BOOL) { + // Already 0/1; nothing to do + fastHit = true; + } else if (top->type == RVALUE_REAL) { + top->int32 = top->real > (GMLReal) 0.5 ? 1 : 0; + top->type = RVALUE_BOOL; + fastHit = true; + } + break; + case 0x25: // Variable -> Int32 + if (top->type == RVALUE_INT32) { + fastHit = true; + } else if (top->type == RVALUE_BOOL) { + top->type = RVALUE_INT32; + fastHit = true; + } else if (top->type == RVALUE_REAL) { + top->int32 = (int32_t) top->real; + top->type = RVALUE_INT32; + fastHit = true; + } + break; + case 0x02: // Int32 -> Double (Real) + top->real = (GMLReal) top->int32; + top->type = RVALUE_REAL; + fastHit = true; + break; + } + + if (fastHit) { +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx)) top->gmlStackType = dstType; +#endif + } else { + handleConv(ctx, srcType, dstType, convKey); + } + break; + } + + // Comparison + case OP_CMP: { + RValue* slotA = &ctx->stack.slots[ctx->stack.top - 2]; + RValue* slotB = &ctx->stack.slots[ctx->stack.top - 1]; + + // Inline fast path for INT32/INT32 + if (slotA->type == RVALUE_INT32 && slotB->type == RVALUE_INT32) { + int32_t a = slotA->int32; + int32_t b = slotB->int32; + bool result; + switch (instrCmpKind(instr)) { + case CMP_LT: result = b > a; break; + case CMP_LTE: result = b >= a; break; + case CMP_EQ: result = a == b; break; + case CMP_NEQ: result = a != b; break; + case CMP_GTE: result = a >= b; break; + case CMP_GT: result = a > b; break; + default: result = false; break; + } + slotA->int32 = result ? 1 : 0; + slotA->type = RVALUE_BOOL; +#if IS_BC17_OR_HIGHER_ENABLED + if (IS_BC17_OR_HIGHER(ctx)) slotA->gmlStackType = GML_TYPE_BOOL; +#endif + ctx->stack.top--; + } else { + handleCmp(ctx, instr); + } + break; + } + + // Duplicate + case OP_DUP: + handleDup(ctx, instr); + break; + + // Branches + // The reason why these (the branches opcodes) are inlined is because they access ctx->ip + // So, because they are short n' sweet, we prefer to keep them inlined to avoid any reloading shenanigans that the compiler may do + case OP_B: { + int32_t offset = instrJumpOffset(instr); + ip = instrAddr + offset; + break; + } + case OP_BT: { + bool condition = stackPopInt32(ctx) != 0; + if (condition == true) { + int32_t offset = instrJumpOffset(instr); + ip = instrAddr + offset; + } + break; + } + case OP_BF: { + bool condition = stackPopInt32(ctx) != 0; + if (condition == false) { + int32_t offset = instrJumpOffset(instr); + ip = instrAddr + offset; + } + break; + } + + // Function call + case OP_CALL: + VM_SYNC_IP(); + handleCall(ctx, instr, extraData); + break; +#if IS_BC17_OR_HIGHER_ENABLED + case OP_CALLV: + VM_SYNC_IP(); + handleCallV(ctx, instr); + break; +#endif + + // Return + case OP_RET: { + RValue retVal = stackPop(ctx); + return retVal; + } + + // Exit (no return value) + case OP_EXIT: + return RValue_makeUndefined(); + + // Environment (with-statements) + case OP_PUSHENV: + VM_SYNC_IP(); + handlePushEnv(ctx, instr, instrAddr); + VM_RELOAD_IP(); + break; + case OP_POPENV: + VM_SYNC_IP(); + handlePopEnv(ctx, instr, instrAddr); + VM_RELOAD_IP(); + break; + + // Break (extended opcodes in V17+, no-op/debug in V16) + case OP_BREAK: +#if IS_BC17_OR_HIGHER_ENABLED + handleBreak(ctx, instr, instrAddr); +#endif + break; + + default: + fprintf(stderr, "VM: Unknown opcode 0x%02X at offset %u\n", opcode, instrAddr); + abort(); + } + } + + return RValue_makeUndefined(); +} + +// ===[ Public API ]=== + +VMContext* VM_create(DataWin* dataWin) { +#ifdef PLATFORM_PS2 + // Place VMContext in scratchpad RAM + requireMessage(16384 >= sizeof(VMContext), "VMContext exceeds PS2 scratchpad size (16 KB)"); + VMContext* ctx = (VMContext*) 0x70000000; + memset(ctx, 0, sizeof(VMContext)); +#else + VMContext* ctx = safeCalloc(1, sizeof(VMContext)); +#endif + ctx->dataWin = dataWin; + ctx->stack.top = 0; + ctx->selfId = -1; + ctx->otherId = -1; + ctx->callDepth = 0; + ctx->currentEventType = -1; + ctx->currentEventSubtype = -1; + ctx->currentEventObjectIndex = -1; + + ctx->profiler = nullptr; // lazily allocated by Profiler_setEnabled(&ctx->profiler, true) + + // Validate that no code entry exceeds MAX_CODE_LOCALS (the VM uses stack-allocated arrays of this size) + repeat(dataWin->code.count, i) { + CodeEntry* entry = &dataWin->code.entries[i]; + requireMessageFormatted(MAX_CODE_LOCALS > entry->localsCount, "Code %s has too many locals!", entry->name); + } + + VMBuiltins_checkIfBuiltinVarTableIsSorted(); + + // Pre-resolve built-in variable IDs (replaces runtime strcmp chains with O(1) switch dispatch) + repeat(dataWin->vari.variableCount, i) { + Variable* var = &dataWin->vari.variables[i]; + // varID == -6 is the BC16 built-in sentinel. + // In BC17, argument variables have instanceType == -6 (Builtin) with varID >= 0, so we also check instanceType. + if (var->varID == -6 || var->instanceType == -6) { + var->builtinVarId = VMBuiltins_resolveBuiltinVarId(var->name); + } else { + var->builtinVarId = BUILTIN_VAR_UNKNOWN; + } + } + + // Build reference lookup maps (file buffer stays read-only) + patchReferenceOperands(ctx); + + // Scan VARI entries to find max varID for global scope + // Built-in variables have varID == -6 (sentinel), skip those + uint32_t maxGlobalVarID = 0; + forEach(Variable, v, dataWin->vari.variables, dataWin->vari.variableCount) { + if (0 > v->varID) continue; + if (v->instanceType == INSTANCE_GLOBAL) { + if ((uint32_t) v->varID + 1 > maxGlobalVarID) maxGlobalVarID = (uint32_t) v->varID + 1; + } + } + + ctx->globalVarCount = maxGlobalVarID; + ctx->globalVars = safeCalloc(maxGlobalVarID, sizeof(RValue)); + repeat(maxGlobalVarID, i) { + ctx->globalVars[i].type = RVALUE_UNDEFINED; + } + + ctx->currentCodeIndex = -1; + + // V17+ static initialization tracking + if (dataWin->gen8.bytecodeVersion >= 17) { + ctx->staticInitialized = safeCalloc(dataWin->code.count, sizeof(bool)); + } else { + ctx->staticInitialized = nullptr; + } + ctx->currentArrayOwner = nullptr; + ctx->savearefBalance = 0; + + // Find the varID for "creator" self variable (used by instance_create) + ctx->creatorVarID = -1; + forEach(Variable, cv, dataWin->vari.variables, dataWin->vari.variableCount) { + if (cv->instanceType == INSTANCE_SELF && cv->varID >= 0 && strcmp(cv->name, "creator") == 0) { + ctx->creatorVarID = cv->varID; + break; + } + } + + // Build globalVarNameMap: varName -> varID for global variables + ctx->globalVarNameMap = nullptr; + forEach(Variable, v2, dataWin->vari.variables, dataWin->vari.variableCount) { + if (v2->instanceType == INSTANCE_GLOBAL && v2->varID >= 0) { + ptrdiff_t existing = shgeti(ctx->globalVarNameMap, (char*) v2->name); + if (0 > existing) { + shput(ctx->globalVarNameMap, (char*) v2->name, v2->varID); + } + } + } + + // Build selfVarNameMap: varName -> varID for self/instance-scoped variables. + ctx->selfVarNameMap = nullptr; + forEach(Variable, v3, dataWin->vari.variables, dataWin->vari.variableCount) { + if (v3->varID >= 0 && (v3->instanceType == INSTANCE_SELF || 0 > v3->instanceType)) { + ptrdiff_t existing = shgeti(ctx->selfVarNameMap, (char*) v3->name); + if (0 > existing) { + shput(ctx->selfVarNameMap, (char*) v3->name, v3->varID); + } + } + } + + // Build funcName -> codeIndex hash map from SCPT chunk + ctx->codeIndexByName = nullptr; + forEach(Script, s, dataWin->scpt.scripts, dataWin->scpt.count) { + if (s->name != nullptr && s->codeId >= 0) { + if (dataWin->code.count > (uint32_t) s->codeId) { + const char* codeName = dataWin->code.entries[s->codeId].name; + // Map the full code entry name (e.g. "gml_Script_SCR_GAMESTART") + shput(ctx->codeIndexByName, (char*) codeName, s->codeId); + // Also map the bare script name (e.g. "SCR_GAMESTART") + // since the FUNC chunk references use bare names in CALL instructions + shput(ctx->codeIndexByName, (char*) s->name, s->codeId); + } + } + } + + // Also map code entry names directly for non-script code (object events, room creation codes, etc.) + repeat(dataWin->code.count, i) { + const char* codeName = dataWin->code.entries[i].name; + ptrdiff_t existing = shgeti(ctx->codeIndexByName, (char*) codeName); + if (0 > existing) { + shput(ctx->codeIndexByName, (char*) codeName, (int32_t) i); + } + } + + // Build codeName -> CodeLocals* hash map + ctx->codeLocalsMap = nullptr; + repeat(dataWin->func.codeLocalsCount, i) { + CodeLocals* cl = &dataWin->func.codeLocals[i]; + shput(ctx->codeLocalsMap, safeStrdup(cl->name), cl); + // In bytecode 17+, CodeLocals uses "gml_GlobalScript_" prefix but callable CODE entries use "gml_Script_", so we'll map the "gml_Script_" variant too + if (dataWin->gen8.bytecodeVersion >= 17) { + if (strncmp(cl->name, "gml_GlobalScript_", 17) == 0) { + char scriptName[512]; + snprintf(scriptName, sizeof(scriptName), "gml_Script_%s", cl->name + 17); + shput(ctx->codeLocalsMap, safeStrdup(scriptName), cl); + } + } + } + + // BC17+: build per-CodeLocals varID -> slot hmap so resolveLocalSlot is O(1) + // We NEED to do it with the "code.count" because YoYo Games in their infinite wisdom thought "what if... we just didn't include some local variables in the localVars map? heck, sometimes we can just NOT include any CodeLocals!"... fun! + ctx->codeLocalsSlotMaps = nullptr; + if (dataWin->gen8.bytecodeVersion >= 17) { + ctx->codeLocalsSlotMaps = safeCalloc(dataWin->code.count, sizeof(*ctx->codeLocalsSlotMaps)); + } + + // Register built-in functions + VMBuiltins_registerAll(ctx); + + // Pre-resolve all FUNC entries to cached builtin pointers or script code indices. + // This eliminates per-call string hash lookups in handleCall. + ctx->funcCallCacheCount = dataWin->func.functionCount; + ctx->funcCallCache = safeMalloc(dataWin->func.functionCount * sizeof(FuncCallCache)); + repeat(dataWin->func.functionCount, i) { + const char* name = dataWin->func.functions[i].name; + BuiltinFunc builtin = VM_findBuiltin(ctx, name); + ctx->funcCallCache[i].builtin = (void*) builtin; + if (builtin != nullptr) { + ctx->funcCallCache[i].scriptCodeIndex = -1; + } else { + ptrdiff_t mapIdx = shgeti(ctx->codeIndexByName, (char*) name); + ctx->funcCallCache[i].scriptCodeIndex = (mapIdx >= 0) ? ctx->codeIndexByName[mapIdx].value : -1; + } + } + + fprintf(stderr, "VM: Initialized with %u global vars, sparse self vars (hashmap), %u functions mapped\n", ctx->globalVarCount, (uint32_t) shlen(ctx->codeIndexByName)); + + return ctx; +} + +void VM_reset(VMContext* ctx) { + // Reset all global variables to undefined + repeat(ctx->globalVarCount, i) { + RValue_free(&ctx->globalVars[i]); + ctx->globalVars[i].type = RVALUE_UNDEFINED; + } + + // Reset stack + ctx->stack.top = 0; + + // Free any remaining call frames + CallFrame* frame = ctx->callStack; + while (frame != nullptr) { + CallFrame* parent = frame->parent; + free(frame); + frame = parent; + } + ctx->callStack = nullptr; + ctx->callDepth = 0; + + // Free any remaining env frames + EnvFrame* envFrame = ctx->envStack; + while (envFrame != nullptr) { + EnvFrame* parent = envFrame->parent; + arrfree(envFrame->instanceList); + free(envFrame); + envFrame = parent; + } + ctx->envStack = nullptr; + + // Reset execution state + ctx->currentInstance = nullptr; + ctx->otherInstance = nullptr; + ctx->selfId = -1; + ctx->otherId = -1; + ctx->currentEventType = -1; + ctx->currentEventSubtype = -1; + ctx->currentEventObjectIndex = -1; + ctx->scriptArgs = nullptr; + ctx->scriptArgCount = 0; + ctx->currentCodeName = nullptr; + ctx->localVars = nullptr; + ctx->localVarCount = 0; + ctx->currentCodeLocalsSlotMap = nullptr; + ctx->actionRelativeFlag = false; + + fprintf(stderr, "VM: Reset complete (%u global vars cleared)\n", ctx->globalVarCount); +} + +static CodeLocals* resolveCodeLocals(VMContext* ctx, const char* codeName) { + return shget(ctx->codeLocalsMap, (char*) codeName); +} + +// Sets the currentCodeLocalsSlotMap for BC17+ games +static void setCurrentCodeLocalsSlotMap(VMContext* ctx) { + if (IS_BC17_OR_HIGHER(ctx)) { + ctx->currentCodeLocalsSlotMap = &ctx->codeLocalsSlotMaps[ctx->currentCodeIndex]; + } +} + +static uint32_t computeLocalsCount(VMContext* ctx, CodeEntry* code) { + if (IS_BC16_OR_BELOW(ctx)) { + return code->localsCount; + } else { + // We can't trust localVarCount in GM:S 2.3+, so we will get our cached map + // It is NOT the "right" localsCount because it may increase during runtime, but for now, this shall do + return IntIntHashMap_count(&ctx->codeLocalsSlotMaps[ctx->currentCodeIndex]); + } +} + +// Native script code override table +void VM_registerCodeOverride(VMContext* ctx, const char* codeName, BuiltinFunc func) { + shput(ctx->codeOverrideMap, (char*) codeName, func); +} + +RValue VM_executeCode(VMContext* ctx, int32_t codeIndex) { + require(codeIndex >= 0 && ctx->dataWin->code.count > (uint32_t) codeIndex); + CodeEntry* code = &ctx->dataWin->code.entries[codeIndex]; + + ctx->bytecodeBase = ctx->dataWin->bytecodeBuffer + (code->bytecodeAbsoluteOffset - ctx->dataWin->bytecodeBufferBase); + ctx->ip = code->offset; + ctx->codeEnd = code->length; + ctx->currentCodeName = code->name; + ctx->currentCodeIndex = codeIndex; + if (ctx->codeOverrideMap != nullptr && code->name != nullptr) { + ptrdiff_t overrideIdx = shgeti(ctx->codeOverrideMap, (char*) code->name); + if (overrideIdx >= 0) { + BuiltinFunc nativeFunc = ctx->codeOverrideMap[overrideIdx].value; + if (nativeFunc != nullptr) { + return nativeFunc(ctx, nullptr, 0); + } + } + } + + setCurrentCodeLocalsSlotMap(ctx); + + uint32_t localsCount = computeLocalsCount(ctx, code); + RValue localVars[MAX_CODE_LOCALS]; + if (localsCount > 0) { + memset(localVars, 0, localsCount * sizeof(RValue)); + } + ctx->localVars = localVars; + ctx->localVarCount = localsCount; + + // Reset stack for top-level execution + ctx->stack.top = 0; + + int32_t savedSavearefBalance = ctx->savearefBalance; + ctx->savearefBalance = 0; + +#ifdef ENABLE_VM_GML_PROFILER + Profiler_enter(ctx->profiler, code->name); +#endif + RValue result = executeLoop(ctx); +#ifdef ENABLE_VM_GML_PROFILER + Profiler_exit(ctx->profiler); +#endif + + requireMessage(ctx->savearefBalance == 0, "SAVEAREF/RESTOREAREF imbalance at end of VM_executeCode (unpaired SAVEAREF)"); + ctx->savearefBalance = savedSavearefBalance; + + // Free locals (decRefs owned arrays, frees owned strings) + repeat(ctx->localVarCount, i) { + RValue_free(&ctx->localVars[i]); + } + ctx->localVars = nullptr; + ctx->localVarCount = 0; + + return result; +} + + +RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t argCount) { + require(codeIndex >= 0 && ctx->dataWin->code.count > (uint32_t) codeIndex); + CodeEntry* code = &ctx->dataWin->code.entries[codeIndex]; + + // Save current frame + CallFrame frame = (CallFrame) { + .savedIP = ctx->ip, + .savedCodeEnd = ctx->codeEnd, + .savedBytecodeBase = ctx->bytecodeBase, + .savedLocals = ctx->localVars, + .savedLocalsCount = ctx->localVarCount, + .savedCodeName = ctx->currentCodeName, + .savedSavearefBalance = ctx->savearefBalance, + .savedCodeLocalsSlotMap = ctx->currentCodeLocalsSlotMap, + .savedScriptArgs = ctx->scriptArgs, + .savedScriptArgCount = ctx->scriptArgCount, + .savedCurrentCodeIndex = ctx->currentCodeIndex, + .parent = ctx->callStack, + }; + ctx->callStack = &frame; + ctx->callDepth++; + + // Set up callee + ctx->bytecodeBase = ctx->dataWin->bytecodeBuffer + (code->bytecodeAbsoluteOffset - ctx->dataWin->bytecodeBufferBase); + ctx->ip = code->offset; + ctx->codeEnd = code->length; + ctx->currentCodeName = code->name; + ctx->currentCodeIndex = codeIndex; + + setCurrentCodeLocalsSlotMap(ctx); + + uint32_t localsCount = computeLocalsCount(ctx, code); + // We use fixed-size arrays instead of VLAs because it seems that using multiple VLAs in a single function things get corrupted somehow? + // So when you see this MAX_CODE_LOCALS and GML_MAX_ARGUMENTS, you can shake your fist in the air and say "damn you MIPS!!1" + RValue localVars[MAX_CODE_LOCALS]; + if (localsCount > 0) { + memset(localVars, 0, localsCount * sizeof(RValue)); + } + ctx->localVars = localVars; + ctx->localVarCount = localsCount; + + // Store arguments in scriptArgs (mirrors GMS 1.4's global argument stack). + // Callee takes an INDEPENDENT reference for strings (strdup) and arrays (incRef) so + // the caller's original args remain valid and owner-tracked by the caller. + RValue scriptArgs[GML_MAX_ARGUMENTS]; + if (argCount > 0) { + memset(scriptArgs, 0, (size_t) argCount * sizeof(RValue)); + } + ctx->scriptArgs = scriptArgs; + ctx->scriptArgCount = argCount; + if (argCount > 0 && args != nullptr) { + repeat(argCount, argIdx) { + RValue argCopy = args[argIdx]; + if (argCopy.type == RVALUE_STRING && argCopy.ownsReference && argCopy.string != nullptr) { + argCopy.string = safeStrdup(argCopy.string); + } else if (argCopy.type == RVALUE_ARRAY && argCopy.array != nullptr) { + GMLArray_incRef(argCopy.array); + argCopy.ownsReference = true; +#if IS_BC17_OR_HIGHER_ENABLED + } else if (argCopy.type == RVALUE_METHOD && argCopy.method != nullptr) { + GMLMethod_incRef(argCopy.method); + argCopy.ownsReference = true; +#endif + } else if (argCopy.type == RVALUE_STRUCT && argCopy.structInst != nullptr) { + Instance_structIncRef(argCopy.structInst); + argCopy.ownsReference = true; + } + ctx->scriptArgs[argIdx] = argCopy; + } + } + + ctx->savearefBalance = 0; + + // Execute the callee +#ifdef ENABLE_VM_GML_PROFILER + Profiler_enter(ctx->profiler, code->name); +#endif + RValue result = executeLoop(ctx); +#ifdef ENABLE_VM_GML_PROFILER + Profiler_exit(ctx->profiler); +#endif + + requireMessage(ctx->savearefBalance == 0, "SAVEAREF/RESTOREAREF imbalance at end of VM_callCodeIndex (unpaired SAVEAREF)"); + + // Strengthen result BEFORE freeing callee locals/scriptArgs: if result is a weak view into callee state, the upcoming frees would leave a dangling pointer. + // For owning results, the refCount/string buffer stays valid (the callee transferred one ownership slot to us). + result = strengthenReturnValue(result); + + // Restore caller frame + CallFrame* saved = ctx->callStack; + ctx->ip = saved->savedIP; + ctx->codeEnd = saved->savedCodeEnd; + ctx->bytecodeBase = saved->savedBytecodeBase; + + // Free callee locals + repeat(ctx->localVarCount, i) { + RValue_free(&ctx->localVars[i]); + } + + // Free callee script args + repeat(ctx->scriptArgCount, i) { + RValue_free(&ctx->scriptArgs[i]); + } + + ctx->localVars = saved->savedLocals; + ctx->localVarCount = saved->savedLocalsCount; + ctx->currentCodeLocalsSlotMap = saved->savedCodeLocalsSlotMap; + ctx->scriptArgs = saved->savedScriptArgs; + ctx->scriptArgCount = saved->savedScriptArgCount; + ctx->currentCodeName = saved->savedCodeName; + ctx->currentCodeIndex = saved->savedCurrentCodeIndex; + ctx->savearefBalance = saved->savedSavearefBalance; + ctx->callStack = saved->parent; + ctx->callDepth--; + + return result; +} + +// ===[ Disassembler ]=== + +static char gmlTypeChar(uint8_t type) { + switch (type) { + case GML_TYPE_DOUBLE: return 'd'; + case GML_TYPE_FLOAT: return 'f'; + case GML_TYPE_INT32: return 'i'; + case GML_TYPE_INT64: return 'l'; + case GML_TYPE_BOOL: return 'b'; + case GML_TYPE_VARIABLE: return 'v'; + case GML_TYPE_STRING: return 's'; + case GML_TYPE_INT16: return 'e'; + default: return '?'; + } +} + +static const char* cmpKindName(uint8_t kind) { + switch (kind) { + case CMP_LT: return "LT"; + case CMP_LTE: return "LTE"; + case CMP_EQ: return "EQ"; + case CMP_NEQ: return "NEQ"; + case CMP_GTE: return "GTE"; + case CMP_GT: return "GT"; + default: return "???"; + } +} + +static const char* varTypeName(uint32_t varRef) { + uint8_t varType = (varRef >> 24) & 0xF8; + switch (varType) { + case VARTYPE_ARRAY: return "Array"; + case VARTYPE_STACKTOP: return "StackTop"; + case VARTYPE_NORMAL: return "Normal"; + case VARTYPE_INSTANCE: return "Instance"; + default: return "Unknown"; + } +} + +static const char* disasmScopeName(VMContext* ctx, int32_t instanceType) { + switch (instanceType) { + case INSTANCE_SELF: return "self"; + case INSTANCE_OTHER: return "other"; + case INSTANCE_ALL: return "all"; + case INSTANCE_NOONE: return "noone"; + case INSTANCE_GLOBAL: return "global"; + case INSTANCE_LOCAL: return "local"; + case INSTANCE_STACKTOP: return "stacktop"; + default: + if (instanceType >= 0 && ctx->dataWin->objt.count > (uint32_t) instanceType) { + return ctx->dataWin->objt.objects[instanceType].name; + } + return "unknown"; + } +} + +// Formats a variable operand for disassembly: "scope.varName [varType]" +// If scopeOverride is set (e.g. "local", "global"), uses that instead of resolving instrInstType. +// Shows VARI instanceType mismatch annotation when scopeOverride is nullptr and types differ. +static void disasmFormatVar(VMContext* ctx, const uint8_t* extraData, const char* scopeOverride, int32_t instrInstType, char* buf, size_t bufSize) { + uint32_t varRef = resolveVarOperand(extraData); + Variable* varDef = resolveVarDef(ctx, varRef); + const char* vType = varTypeName(varRef); + + // For StackTop and Array variable types, the actual instance type comes from the stack at runtime, not from the instruction operand. + // Use the VARI entry's instanceType instead, since the instruction's instanceType is meaningless for these access types. + uint8_t varType = (varRef >> 24) & 0xF8; + if (varType == VARTYPE_STACKTOP || varType == VARTYPE_ARRAY) { + const char* scope = scopeOverride != nullptr ? scopeOverride : disasmScopeName(ctx, varDef->instanceType); + snprintf(buf, bufSize, "%s.%s [%s]", scope, varDef->name, vType); + return; + } + + const char* scope = scopeOverride != nullptr ? scopeOverride : disasmScopeName(ctx, instrInstType); + + if (scopeOverride == nullptr && varDef->instanceType != instrInstType) { + const char* variScope = disasmScopeName(ctx, varDef->instanceType); + snprintf(buf, bufSize, "%s.%s [%s] (VARI: %s, instr: %s)", scope, varDef->name, vType, variScope, scope); + } else { + snprintf(buf, bufSize, "%s.%s [%s]", scope, varDef->name, vType); + } +} + +// Returns stack effect comment for a variable access instruction +static void disasmFormatVarComment(VMContext* ctx, const uint8_t* extraData, bool isPop, char* buf, size_t bufSize) { + uint32_t varRef = resolveVarOperand(extraData); + uint8_t varType = (varRef >> 24) & 0xF8; + if (isPop) { + switch (varType) { + case VARTYPE_ARRAY: snprintf(buf, bufSize, "// pops: [arrayIndex, instanceType, value]"); break; + case VARTYPE_STACKTOP: snprintf(buf, bufSize, "// pops: [instanceType, value]"); break; + default: snprintf(buf, bufSize, "// pops: [value]"); break; + } + } else { + switch (varType) { + case VARTYPE_ARRAY: snprintf(buf, bufSize, "// pops: [arrayIndex, instanceType] -> pushes: [value]"); break; + case VARTYPE_STACKTOP: snprintf(buf, bufSize, "// pops: [instanceType] -> pushes: [value]"); break; + default: snprintf(buf, bufSize, "// pushes: [value]"); break; + } + } +} + +// Formats a single instruction into opcodeStr, operandStr, and commentStr buffers. +// Used by both VM_disassemble and --trace-opcodes. +// bytecodeBase is needed because the disassembler and trace have it from different sources. +static void formatInstruction(VMContext* ctx, const uint8_t* bytecodeBase, uint32_t instrAddr, uint32_t instr, const uint8_t* extraData, + char* opcodeStr, size_t opcodeSize, char* operandStr, size_t operandSize, char* commentStr, size_t commentSize) { + DataWin* dw = ctx->dataWin; + uint8_t opcode = instrOpcode(instr); + uint8_t type1 = instrType1(instr); + uint8_t type2 = instrType2(instr); + int16_t instType = instrInstanceType(instr); + + switch (opcode) { + // Binary arithmetic/logic + case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: + case OP_REM: case OP_MOD: case OP_AND: case OP_OR: + case OP_XOR: case OP_SHL: case OP_SHR: + snprintf(opcodeStr, opcodeSize, "%s.%c.%c", opcodeName(opcode), gmlTypeChar(type1), gmlTypeChar(type2)); + snprintf(commentStr, commentSize, "// pops: [a, b] -> pushes: [result]"); + break; + + // Unary + case OP_NEG: + snprintf(opcodeStr, opcodeSize, "Neg.%c", gmlTypeChar(type1)); + snprintf(commentStr, commentSize, "// pops: [a] -> pushes: [result]"); + break; + case OP_NOT: + snprintf(opcodeStr, opcodeSize, "Not.%c", gmlTypeChar(type1)); + if (type1 == GML_TYPE_BOOL) { + snprintf(commentStr, commentSize, "// pops: [a] -> pushes: [bool] (logical NOT)"); + } else { + snprintf(commentStr, commentSize, "// pops: [a] -> pushes: [int] (bitwise NOT)"); + } + break; + + // Type conversion + case OP_CONV: + snprintf(opcodeStr, opcodeSize, "Conv.%c.%c", gmlTypeChar(type1), gmlTypeChar(type2)); + snprintf(commentStr, commentSize, "// pops: [%c] -> pushes: [%c]", gmlTypeChar(type2), gmlTypeChar(type1)); + break; + + // Comparison + case OP_CMP: + snprintf(opcodeStr, opcodeSize, "Cmp.%c.%c", gmlTypeChar(type1), gmlTypeChar(type2)); + snprintf(operandStr, operandSize, "%s", cmpKindName(instrCmpKind(instr))); + snprintf(commentStr, commentSize, "// pops: [a, b] -> pushes: [bool]"); + break; + + // Push + case OP_PUSH: { + switch (type1) { + case GML_TYPE_DOUBLE: + snprintf(opcodeStr, opcodeSize, "Push.d"); + snprintf(operandStr, operandSize, "%g", BinaryUtils_readFloat64(extraData)); + snprintf(commentStr, commentSize, "// pushes: [double]"); + break; + case GML_TYPE_FLOAT: + snprintf(opcodeStr, opcodeSize, "Push.f"); + snprintf(operandStr, operandSize, "%g", (double) BinaryUtils_readFloat32(extraData)); + snprintf(commentStr, commentSize, "// pushes: [float]"); + break; + case GML_TYPE_INT32: + snprintf(opcodeStr, opcodeSize, "Push.i"); + snprintf(operandStr, operandSize, "%d", BinaryUtils_readInt32(extraData)); + snprintf(commentStr, commentSize, "// pushes: [int32]"); + break; + case GML_TYPE_INT64: + snprintf(opcodeStr, opcodeSize, "Push.l"); + snprintf(operandStr, operandSize, "%lld", (long long) BinaryUtils_readInt64(extraData)); + snprintf(commentStr, commentSize, "// pushes: [int64]"); + break; + case GML_TYPE_BOOL: + snprintf(opcodeStr, opcodeSize, "Push.b"); + snprintf(operandStr, operandSize, "%s", BinaryUtils_readInt32(extraData) != 0 ? "true" : "false"); + snprintf(commentStr, commentSize, "// pushes: [bool]"); + break; + case GML_TYPE_STRING: { + snprintf(opcodeStr, opcodeSize, "Push.s"); + int32_t strIdx = BinaryUtils_readInt32(extraData); + if (strIdx >= 0 && dw->strg.count > (uint32_t) strIdx) { + const char* str = dw->strg.strings[strIdx]; + if (strlen(str) > 60) { + snprintf(operandStr, operandSize, "\"%.57s...\"", str); + } else { + snprintf(operandStr, operandSize, "\"%s\"", str); + } + } else { + snprintf(operandStr, operandSize, "[string:%d]", strIdx); + } + snprintf(commentStr, commentSize, "// pushes: [string]"); + break; + } + case GML_TYPE_VARIABLE: + snprintf(opcodeStr, opcodeSize, "Push.v"); + disasmFormatVar(ctx, extraData, nullptr, (int32_t) instType, operandStr, operandSize); + disasmFormatVarComment(ctx, extraData, false, commentStr, commentSize); + break; + case GML_TYPE_INT16: + snprintf(opcodeStr, opcodeSize, "Push.e"); + snprintf(operandStr, operandSize, "%d", (int32_t) instType); + snprintf(commentStr, commentSize, "// pushes: [int16]"); + break; + default: + snprintf(opcodeStr, opcodeSize, "Push.?"); + snprintf(operandStr, operandSize, "(unknown type 0x%X)", type1); + break; + } + break; + } + + // Scoped pushes + case OP_PUSHLOC: + snprintf(opcodeStr, opcodeSize, "PushLoc.v"); + disasmFormatVar(ctx, extraData, "local", (int32_t) instType, operandStr, operandSize); + disasmFormatVarComment(ctx, extraData, false, commentStr, commentSize); + break; + case OP_PUSHGLB: + snprintf(opcodeStr, opcodeSize, "PushGlb.v"); + disasmFormatVar(ctx, extraData, "global", (int32_t) instType, operandStr, operandSize); + disasmFormatVarComment(ctx, extraData, false, commentStr, commentSize); + break; + case OP_PUSHBLTN: + snprintf(opcodeStr, opcodeSize, "PushBltn.v"); + disasmFormatVar(ctx, extraData, nullptr, (int32_t) instType, operandStr, operandSize); + disasmFormatVarComment(ctx, extraData, false, commentStr, commentSize); + break; + + // PushI (int16 immediate) + case OP_PUSHI: + snprintf(opcodeStr, opcodeSize, "PushI.e"); + snprintf(operandStr, operandSize, "%d", (int32_t) instType); + snprintf(commentStr, commentSize, "// pushes: [int16]"); + break; + + // Pop (store to variable) + case OP_POP: + snprintf(opcodeStr, opcodeSize, "Pop.%c.%c", gmlTypeChar(type1), gmlTypeChar(type2)); + disasmFormatVar(ctx, extraData, nullptr, (int32_t) instType, operandStr, operandSize); + disasmFormatVarComment(ctx, extraData, true, commentStr, commentSize); + break; + + // Unconditional branch + case OP_B: { + snprintf(opcodeStr, opcodeSize, "B"); + int32_t offset = instrJumpOffset(instr); + uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); + snprintf(operandStr, operandSize, "L_%04X (offset: %+d)", target, offset); + break; + } + + // Conditional branches + case OP_BT: { + snprintf(opcodeStr, opcodeSize, "BT"); + int32_t offset = instrJumpOffset(instr); + uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); + snprintf(operandStr, operandSize, "L_%04X (offset: %+d)", target, offset); + snprintf(commentStr, commentSize, "// pops: [bool]"); + break; + } + case OP_BF: { + snprintf(opcodeStr, opcodeSize, "BF"); + int32_t offset = instrJumpOffset(instr); + uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); + snprintf(operandStr, operandSize, "L_%04X (offset: %+d)", target, offset); + snprintf(commentStr, commentSize, "// pops: [bool]"); + break; + } + + // With-statement: PushEnv + case OP_PUSHENV: { + snprintf(opcodeStr, opcodeSize, "PushEnv"); + int32_t offset = instrJumpOffset(instr); + uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); + // Peek at previous instruction to identify the target object + const char* targetName = nullptr; + if (instrAddr >= 4) { + uint32_t prevInstr = BinaryUtils_readUint32(bytecodeBase + instrAddr - 4); + if (instrOpcode(prevInstr) == OP_PUSHI) { + int16_t objIdx = (int16_t) (prevInstr & 0xFFFF); + targetName = disasmScopeName(ctx, (int32_t) objIdx); + } + } + if (targetName != nullptr) { + snprintf(operandStr, operandSize, "%s (target: L_%04X, offset: %+d)", targetName, target, offset); + } else { + snprintf(operandStr, operandSize, "(target: L_%04X, offset: %+d)", target, offset); + } + snprintf(commentStr, commentSize, "// pops: [target]"); + break; + } + + // With-statement: PopEnv + case OP_POPENV: { + snprintf(opcodeStr, opcodeSize, "PopEnv"); + if ((instr & 0x00FFFFFF) == 0xF00000) { + snprintf(operandStr, operandSize, "[exit]"); + } else { + int32_t offset = instrJumpOffset(instr); + uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); + snprintf(operandStr, operandSize, "(target: L_%04X, offset: %+d)", target, offset); + } + break; + } + + // Function call + case OP_CALL: { + snprintf(opcodeStr, opcodeSize, "Call.i"); + int32_t argCount = instr & 0xFFFF; + uint32_t funcIdx = resolveFuncOperand(extraData); + const char* funcName = (dw->func.functionCount > funcIdx) ? dw->func.functions[funcIdx].name : "???"; + snprintf(operandStr, operandSize, "%s(%d)", funcName, argCount); + if (argCount > 0) { + char argList[128] = ""; + int32_t pos = 0; + for (int32_t i = 0; 8 > i && argCount > i; i++) { + if (i > 0) pos += snprintf(argList + pos, sizeof(argList) - pos, ", "); + pos += snprintf(argList + pos, sizeof(argList) - pos, "arg%d", i); + } + if (argCount > 8) snprintf(argList + pos, sizeof(argList) - pos, ", ..."); + snprintf(commentStr, commentSize, "// pops: [%s] -> pushes: [result]", argList); + } else { + snprintf(commentStr, commentSize, "// pushes: [result]"); + } + break; + } + + // Dynamic call through variable/method reference (BC17+) + case OP_CALLV: { + int32_t argCount = instr & 0xFFFF; + snprintf(opcodeStr, opcodeSize, "CallV.v"); + snprintf(operandStr, operandSize, "%d", argCount); + snprintf(commentStr, commentSize, "// pops: [func, instance, %d args] -> pushes: [result]", argCount); + break; + } + + // Duplicate stack items + case OP_DUP: { + uint8_t extra = (uint8_t) (instr & 0xFF); + int32_t count = (int32_t) extra + 1; + snprintf(opcodeStr, opcodeSize, "Dup.%c", gmlTypeChar(type1)); + if (count > 1) { + snprintf(operandStr, operandSize, "%d", count); + snprintf(commentStr, commentSize, "// duplicates %d items", count); + } else { + snprintf(commentStr, commentSize, "// duplicates top item"); + } + break; + } + + // Control flow + case OP_RET: + snprintf(opcodeStr, opcodeSize, "Ret.%c", gmlTypeChar(type1)); + snprintf(commentStr, commentSize, "// pops: [value] (return)"); + break; + case OP_EXIT: + snprintf(opcodeStr, opcodeSize, "Exit.%c", gmlTypeChar(type1)); + snprintf(commentStr, commentSize, "// (end of code)"); + break; + case OP_POPZ: + snprintf(opcodeStr, opcodeSize, "Popz.%c", gmlTypeChar(type1)); + snprintf(commentStr, commentSize, "// pops: [value]"); + break; + + // Break (extended opcodes in V17+) + case OP_BREAK: { + int16_t breakType = (int16_t) instType; + const char* mnemonic; + switch (breakType) { + case BREAK_CHKINDEX: mnemonic = "chkindex"; break; + case BREAK_PUSHAF: mnemonic = "pushaf"; break; + case BREAK_POPAF: mnemonic = "popaf"; break; + case BREAK_PUSHAC: mnemonic = "pushac"; break; + case BREAK_SETOWNER: mnemonic = "setowner"; break; + case BREAK_ISSTATICOK: mnemonic = "isstaticok"; break; + case BREAK_SETSTATIC: mnemonic = "setstatic"; break; + case BREAK_SAVEAREF: mnemonic = "savearef"; break; + case BREAK_RESTOREAREF: mnemonic = "restorearef"; break; + default: mnemonic = nullptr; break; + } + if (mnemonic != nullptr) { + snprintf(opcodeStr, opcodeSize, "%s.%c", mnemonic, gmlTypeChar(type1)); + } else { + snprintf(opcodeStr, opcodeSize, "Break.%c", gmlTypeChar(type1)); + snprintf(operandStr, operandSize, "%d", (int32_t) breakType); + } + break; + } + + default: + snprintf(opcodeStr, opcodeSize, "??? (0x%02X)", opcode); + break; + } +} + +void VM_buildCrossReferences(VMContext* ctx) { + DataWin* dw = ctx->dataWin; + ctx->crossRefMap = nullptr; + + repeat(dw->code.count, callerIdx) { + CodeEntry* code = &dw->code.entries[callerIdx]; + const uint8_t* base = dw->bytecodeBuffer + (code->bytecodeAbsoluteOffset - dw->bytecodeBufferBase); + uint32_t ip = 0; + + while (code->length > ip) { + uint32_t instr = BinaryUtils_readUint32(base + ip); + ip += 4; + const uint8_t* ed = base + ip; + if (instrHasExtraData(instr)) { + ip += extraDataSize(instrType1(instr)); + } + + if (instrOpcode(instr) == OP_CALL) { + uint32_t funcIdx = resolveFuncOperand(ed); + if (dw->func.functionCount > funcIdx) { + const char* funcName = dw->func.functions[funcIdx].name; + ptrdiff_t codeMapIdx = shgeti(ctx->codeIndexByName, (char*) funcName); + if (codeMapIdx >= 0) { + int32_t targetIdx = ctx->codeIndexByName[codeMapIdx].value; + ptrdiff_t mapIdx = hmgeti(ctx->crossRefMap, targetIdx); + if (0 > mapIdx) { + int32_t* callers = nullptr; + arrput(callers, (int32_t) callerIdx); + hmput(ctx->crossRefMap, targetIdx, callers); + } else { + // Deduplicate: don't add the same caller twice + int32_t* callers = ctx->crossRefMap[mapIdx].value; + bool found = false; + for (ptrdiff_t k = 0; arrlen(callers) > k; k++) { + if (callers[k] == (int32_t) callerIdx) { found = true; break; } + } + if (!found) { + arrput(ctx->crossRefMap[mapIdx].value, (int32_t) callerIdx); + } + } + } + } + } + } + } +} + +void VM_disassemble(VMContext* ctx, int32_t codeIndex) { + DataWin* dw = ctx->dataWin; + require(dw->code.count > (uint32_t) codeIndex); + CodeEntry* code = &dw->code.entries[codeIndex]; + + // Header + printf("=== %s (length=%u, locals=%u, args=%u) ===\n", code->name, code->length, code->localsCount, code->argumentsCount); + + // CodeLocals + CodeLocals* locals = resolveCodeLocals(ctx, code->name); + if (locals != nullptr && locals->localVarCount > 0) { + printf("Locals:"); + repeat(locals->localVarCount, i) { + if (i > 0) printf(","); + printf(" [%u] %s", locals->locals[i].varID, locals->locals[i].name); + } + printf("\n"); + } + + // Cross-references + if (ctx->crossRefMap != nullptr) { + ptrdiff_t mapIdx = hmgeti(ctx->crossRefMap, codeIndex); + if (mapIdx >= 0) { + int32_t* callers = ctx->crossRefMap[mapIdx].value; + printf("Called by:"); + for (ptrdiff_t i = 0; arrlen(callers) > i; i++) { + if (i > 0) printf(","); + printf(" %s", dw->code.entries[callers[i]].name); + } + printf("\n"); + } + } + + printf("\n"); + + const uint8_t* bytecodeBase = dw->bytecodeBuffer + (code->bytecodeAbsoluteOffset - dw->bytecodeBufferBase); + uint32_t codeLength = code->length; + + // Pass 1: collect branch targets for labels + struct { uint32_t key; bool value; }* branchTargets = nullptr; + { + uint32_t ip = 0; + while (codeLength > ip) { + uint32_t instrAddr = ip; + uint32_t instr = BinaryUtils_readUint32(bytecodeBase + ip); + ip += 4; + if (instrHasExtraData(instr)) { + ip += extraDataSize(instrType1(instr)); + } + uint8_t opcode = instrOpcode(instr); + if (opcode == OP_B || opcode == OP_BT || opcode == OP_BF || opcode == OP_PUSHENV) { + int32_t offset = instrJumpOffset(instr); + uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); + hmput(branchTargets, target, true); + } + if (opcode == OP_POPENV) { + if ((instr & 0x00FFFFFF) != 0xF00000) { + int32_t offset = instrJumpOffset(instr); + uint32_t target = (uint32_t) ((int32_t) instrAddr + offset); + hmput(branchTargets, target, true); + } + } + } + } + + // Pass 2: print instructions + uint32_t ip = 0; + int32_t envDepth = 0; + + while (codeLength > ip) { + uint32_t instrAddr = ip; + uint32_t instr = BinaryUtils_readUint32(bytecodeBase + ip); + ip += 4; + const uint8_t* extraData = bytecodeBase + ip; + if (instrHasExtraData(instr)) { + ip += extraDataSize(instrType1(instr)); + } + + uint8_t opcode = instrOpcode(instr); + + // PopEnv decreases depth before printing + if (opcode == OP_POPENV && envDepth > 0) envDepth--; + + // Print label if this address is a branch target + if (hmgeti(branchTargets, instrAddr) >= 0) { + printf(" %04X: L_%04X:\n", instrAddr, instrAddr); + } + + int32_t indent = 2 + envDepth * 4; + char opcodeStr[32]; + char operandStr[256] = ""; + char commentStr[128] = ""; + + formatInstruction(ctx, bytecodeBase, instrAddr, instr, extraData, opcodeStr, sizeof(opcodeStr), operandStr, sizeof(operandStr), commentStr, sizeof(commentStr)); + + // Print the formatted line + if (commentStr[0] != '\0') { + printf("%*s%04X: [0x%08X] %-16s %-45s %s\n", indent, "", instrAddr, instr, opcodeStr, operandStr, commentStr); + } else { + printf("%*s%04X: [0x%08X] %-16s %s\n", indent, "", instrAddr, instr, opcodeStr, operandStr); + } + + // PushEnv increases depth after printing + if (opcode == OP_PUSHENV) envDepth++; + } + + hmfree(branchTargets); + printf("\n"); +} + +void VM_registerBuiltin(VMContext* ctx, const char* name, BuiltinFunc func) { + requireMessage(shgeti(ctx->builtinMap, name) == -1, "Trying to register an already registered builtin function!"); + shput(ctx->builtinMap, (char*) name, func); +} + +BuiltinFunc VM_findBuiltin(VMContext* ctx, const char* name) { + ptrdiff_t idx = shgeti(ctx->builtinMap, (char*) name); + if (0 > idx) return nullptr; + return ctx->builtinMap[idx].value; +} + +void VM_free(VMContext* ctx) { + if (ctx == nullptr) return; + + // Reset mutable runtime state + VM_reset(ctx); + + // Free profiler (no-op if never enabled) + Profiler_destroy(ctx->profiler); + ctx->profiler = nullptr; + +#ifdef ENABLE_VM_OPCODE_PROFILER + free(ctx->opcodeVariantCounts); + ctx->opcodeVariantCounts = nullptr; + free(ctx->opcodeRValueTypeCounts); + ctx->opcodeRValueTypeCounts = nullptr; +#endif + + // Free global vars array itself + free(ctx->globalVars); + + // Free hash maps + shfree(ctx->codeIndexByName); + shfree(ctx->globalVarNameMap); + shfree(ctx->selfVarNameMap); + shfree(ctx->codeOverrideMap); + repeat(shlen(ctx->codeLocalsMap), i) { + free(ctx->codeLocalsMap[i].key); + } + shfree(ctx->codeLocalsMap); + + // Free dedup key strings before freeing the hashmaps + repeat(shlen(ctx->loggedUnknownFuncs), i) { + free(ctx->loggedUnknownFuncs[i].key); + } + shfree(ctx->loggedUnknownFuncs); + repeat(shlen(ctx->loggedStubbedFuncs), i) { + free(ctx->loggedStubbedFuncs[i].key); + } + shfree(ctx->loggedStubbedFuncs); +#ifdef ENABLE_VM_TRACING + shfree(ctx->varReadsToBeTraced); + shfree(ctx->varWritesToBeTraced); + shfree(ctx->functionCallsToBeTraced); + shfree(ctx->alarmsToBeTraced); + shfree(ctx->instanceLifecyclesToBeTraced); + shfree(ctx->eventsToBeTraced); + shfree(ctx->opcodesToBeTraced); + shfree(ctx->stackToBeTraced); +#endif + + // Free function call cache + free(ctx->funcCallCache); + + // Free cross-reference map + if (ctx->crossRefMap != nullptr) { + for (ptrdiff_t i = 0; hmlen(ctx->crossRefMap) > i; i++) { + arrfree(ctx->crossRefMap[i].value); + } + hmfree(ctx->crossRefMap); + } + + // Free builtin map + shfree(ctx->builtinMap); + ctx->registeredBuiltinFunctions = false; + + // Free V17+ static tracking + free(ctx->staticInitialized); + + // Free per-code varID -> slot maps (BC17+ only; nullptr otherwise). + if (ctx->codeLocalsSlotMaps != nullptr) { + repeat(ctx->dataWin->code.count, i) { + IntIntHashMap_free(&ctx->codeLocalsSlotMaps[i]); + } + free(ctx->codeLocalsSlotMaps); + ctx->codeLocalsSlotMaps = nullptr; + } + +#ifndef PLATFORM_PS2 + free(ctx); +#endif +} diff --git a/src/vm.h b/src/vm.h index 29c6ee6e..b018ea19 100644 --- a/src/vm.h +++ b/src/vm.h @@ -1,289 +1,295 @@ -#pragma once - -#include "common.h" -#include -#include - -#include "data_win.h" -#include "rvalue.h" -#include "utils.h" -#include "profiler.h" -#include "int_int_hashmap.h" - -// ===[ Instance Types (signed 16-bit) ]=== -#define INSTANCE_SELF (-1) -#define INSTANCE_OTHER (-2) -#define INSTANCE_ALL (-3) -#define INSTANCE_NOONE (-4) -#define INSTANCE_GLOBAL (-5) -#define INSTANCE_BUILTIN (-6) -#define INSTANCE_LOCAL (-7) -#define INSTANCE_STACKTOP (-9) -#define INSTANCE_ARG (-15) - -// ===[ Variable Types (upper 5 bits of varRef, extracted with (varRef >> 24) & 0xF8) ]=== -#define VARTYPE_ARRAY 0x00 -#define VARTYPE_STACKTOP 0x80 -#define VARTYPE_NORMAL 0xA0 -#define VARTYPE_INSTANCE 0xE0 - -// ===[ Room Constants ]=== -#define ROOM_RESTARTGAME (-200) // The reason why it is -200 is because the GameMaker-HTML5 runner uses -200 too (see Globals.js) - -// ===[ GML Math Epsilon (used for floating-point comparisons) ]=== -// The real GameMaker runner uses epsilon-based comparison for all numeric CMP operations. -// Default value matches the HTML5 runner's g_GMLMathEpsilon (1e-5 for double precision). -// When using single-precision floats, we use 1e-4 to work around accumulated rounding errors from -// non-IEEE FPUs (example: PS2's R5900 which rounds toward zero instead of round-to-nearest) can -// exceed the default epsilon. -#ifdef USE_FLOAT_REALS -#define GML_MATH_EPSILON 1e-4 -#else -#define GML_MATH_EPSILON 1e-5 -#endif - -// GMS 1.4 supports up to 16 arguments per script call -#define GML_MAX_ARGUMENTS 16 - -// ===[ Comparison Kinds ]=== -#define CMP_LT 1 -#define CMP_LTE 2 -#define CMP_EQ 3 -#define CMP_NEQ 4 -#define CMP_GTE 5 -#define CMP_GT 6 - -// ===[ Opcodes ]=== -#define OP_CONV 0x07 -#define OP_MUL 0x08 -#define OP_DIV 0x09 -#define OP_REM 0x0A -#define OP_MOD 0x0B -#define OP_ADD 0x0C -#define OP_SUB 0x0D -#define OP_AND 0x0E -#define OP_OR 0x0F -#define OP_XOR 0x10 -#define OP_NEG 0x11 -#define OP_NOT 0x12 -#define OP_SHL 0x13 -#define OP_SHR 0x14 -#define OP_CMP 0x15 -#define OP_POP 0x45 -#define OP_PUSHI 0x84 -#define OP_DUP 0x86 -#define OP_CALLV 0x99 -#define OP_RET 0x9C -#define OP_EXIT 0x9D -#define OP_POPZ 0x9E -#define OP_B 0xB6 -#define OP_BT 0xB7 -#define OP_BF 0xB8 -#define OP_PUSHENV 0xBA -#define OP_POPENV 0xBB -#define OP_PUSH 0xC0 -#define OP_PUSHLOC 0xC1 -#define OP_PUSHGLB 0xC2 -#define OP_PUSHBLTN 0xC3 -#define OP_CALL 0xD9 -#define OP_BREAK 0xFF - -// ===[ Extended BREAK Sub-Opcodes (bytecode version 17+) ]=== -// Encoded in bits 0-15 of the BREAK instruction (instrInstanceType field, as int16_t) -#define BREAK_CHKINDEX (-1) // Validate array index bounds -#define BREAK_PUSHAF (-2) // Pop array ref + index, push element (final dimension) -#define BREAK_POPAF (-3) // Pop value + array ref + index, store at index -#define BREAK_PUSHAC (-4) // Pop array ref + index, push sub-array ref (intermediate dimension) -#define BREAK_SETOWNER (-5) // Pop and discard (copy-on-write owner tracking) -#define BREAK_ISSTATICOK (-6) // Push bool: has static init already run for this function? -#define BREAK_SETSTATIC (-7) // Mark current function's static as initialized -#define BREAK_SAVEAREF (-8) // Save top-of-stack array ref for compound assignment -#define BREAK_RESTOREAREF (-9) // Push previously saved array ref - -// ===[ Variable Types for V17 Array Access ]=== -#define VARTYPE_ARRAYPUSHAF 0x10 // Push array reference (read context) -#define VARTYPE_ARRAYPOPAF 0x90 // Push array reference (write context) - -// ===[ FuncCallCache - Cached resolution for CALL instructions ]=== -// Avoids per-call string hash lookups in both the builtin map and funcMap. -// Resolved once during VM_create, then used directly by handleCall. -typedef struct { - void* builtin; // cached BuiltinFunc pointer, or nullptr - int32_t scriptCodeIndex; // cached script code index, or -1 if not a script -} FuncCallCache; - -// ===[ CallFrame - Saved state for script-to-script calls ]=== -typedef struct CallFrame { - uint32_t savedIP; - uint32_t savedCodeEnd; - uint8_t* savedBytecodeBase; - RValue* savedLocals; - uint32_t savedLocalsCount; - const char* savedCodeName; - int32_t savedSavearefBalance; - IntIntHashMap* savedCodeLocalsSlotMap; - RValue* savedScriptArgs; - int32_t savedScriptArgCount; - int32_t savedCurrentCodeIndex; - struct CallFrame* parent; -} CallFrame; - -// ===[ EnvFrame - Saved context for with-statement (PushEnv/PopEnv) ]=== -typedef struct EnvFrame { - struct Instance* savedInstance; - struct Instance* savedOtherInstance; // Saved otherInstance to restore on PopEnv - struct Instance** instanceList; // stb_ds array of matching instances (nullptr for single-instance) - int32_t currentIndex; // Current position in instanceList - struct EnvFrame* parent; -} EnvFrame; - -// ===[ VMStack - Upward-growing array of RValue slots ]=== -#define VM_STACK_SIZE 1024 - -typedef struct { - int32_t top; - RValue slots[VM_STACK_SIZE]; -} VMStack; - -// Forward declarations -struct Runner; -typedef struct VMContext VMContext; - -// ===[ Builtin Functions Manager ]=== -typedef RValue (*BuiltinFunc)(VMContext* ctx, RValue* args, int32_t argCount); - -typedef struct { - char* key; - BuiltinFunc value; -} BuiltinEntry; - -// ===[ VMContext - Holds all VM state ]=== -// Fields are ordered by access frequency so that the hottest data sits in the first bytes of the struct -// This way data can be kept "hot" in the CPU cache or, depending on the platform, in scratchpad RAM -typedef struct VMContext { - // Hot: touched every instruction in the dispatch loop - uint8_t* bytecodeBase; - uint32_t ip; - uint32_t codeEnd; - RValue* localVars; - uint32_t localVarCount; - RValue* globalVars; - uint32_t globalVarCount; - struct Instance* currentInstance; - struct Instance* otherInstance; // "other" instance for collision events - DataWin* dataWin; - struct Runner* runner; - // BC17+: varID -> localVars slot lookup for the current code. Points into codeLocalsSlotMaps[currentCodeIndex] for BC17+, nullptr for BC16. - IntIntHashMap* currentCodeLocalsSlotMap; - FuncCallCache* funcCallCache; - const char* currentCodeName; - int32_t currentCodeIndex; // Index into code.entries for the currently executing code - - // Warm: touched on calls, variable resolution, event dispatch - CallFrame* callStack; - int32_t callDepth; - EnvFrame* envStack; // Environment stack for with-statements (PushEnv/PopEnv) - RValue* scriptArgs; // Arguments passed to current script (nullptr for non-script code) - int32_t scriptArgCount; // Number of arguments passed - int32_t selfId; - int32_t otherId; - // Current event context (set by Runner_executeEvent, -1 when not in an event) - int32_t currentEventType; - int32_t currentEventSubtype; - int32_t currentEventObjectIndex; // objectIndex of the object that owns the executing event handler - // Cached varID for the built-in "creator" self variable (-1 if not found) - int32_t creatorVarID; - uint32_t funcCallCacheCount; - bool traceEventInherited; - bool hasFixedSeed; - bool actionRelativeFlag; // D&D action relative flag (set by action_set_relative) - - // V17+ extended BREAK opcode state - bool* staticInitialized; // Per-code-entry flag for isstaticok/setstatic (allocated in VM_create) - // BC17+: owner token set by BREAK_SETOWNER. Arrays whose .owner mismatches fork on write. - void* currentArrayOwner; - // SAVEAREF/RESTOREAREF balance tracker. - int32_t savearefBalance; - - // Cold: init-only or rare lookups - BuiltinEntry* builtinMap; - bool registeredBuiltinFunctions; - // funcName -> codeIndex hash map (stb_ds) - struct { char* key; int32_t value; }* codeIndexByName; - // codeName -> CodeLocals* hash map (stb_ds) - struct { char* key; CodeLocals* value; }* codeLocalsMap; - // BC17+: A map of CODE indexes -> localVars slot lookup map - IntIntHashMap* codeLocalsSlotMaps; - // varName -> varID hash map for global variables (stb_ds) - struct { char* key; int32_t value; }* globalVarNameMap; - // varName -> varID hash map for self/instance-scoped variables (stb_ds). - struct { char* key; int32_t value; }* selfVarNameMap; - // "codeName\tfuncName" -> true, for deduplicating unknown function warnings - StringBooleanEntry* loggedUnknownFuncs; - // "codeName\tfuncName" -> true, for deduplicating stubbed function warnings - StringBooleanEntry* loggedStubbedFuncs; - // Cross-reference map for disassembler: targetCodeIndex -> stb_ds array of callerCodeIndex - struct { int32_t key; int32_t* value; }* crossRefMap; - bool alwaysLogUnknownFunctions; - bool alwaysLogStubbedFunctions; -#ifdef ENABLE_VM_TRACING - StringBooleanEntry* varReadsToBeTraced; - StringBooleanEntry* varWritesToBeTraced; - StringBooleanEntry* functionCallsToBeTraced; - StringBooleanEntry* alarmsToBeTraced; - StringBooleanEntry* instanceLifecyclesToBeTraced; - StringBooleanEntry* eventsToBeTraced; - StringBooleanEntry* opcodesToBeTraced; - StringBooleanEntry* stackToBeTraced; - StringBooleanEntry* tilesToBeTraced; - // Minimum frameCount before opcode/stack traces are emitted (default 0) - int traceBytecodeAfterFrame; -#endif - Profiler* profiler; - -#ifdef ENABLE_VM_OPCODE_PROFILER - bool opcodeProfilerEnabled; - uint64_t opcodeCounts[256]; - // Per-opcode breakdown by (type1, type2). Heap-allocated when the profiler is enabled (512 KB), nullptr otherwise. - // Indexed as opcodeVariantCounts[opcode * 256 + type1 * 16 + type2]. - uint64_t* opcodeVariantCounts; - // BREAK (0xFF) sub-opcode counts. Indexed by -breakType (so -1 -> [1], -9 -> [9]). Size 64 covers all currently defined sub-ops with room to spare. - uint64_t breakSubOpCounts[64]; - // Per-opcode breakdown by actual runtime RValue types (typeA, typeB) for arithmetic/comparison/conversion ops. - // Indexed as opcodeRValueTypeCounts[opcode * 256 + typeA * 16 + typeB]. typeB = 0xF for unary ops. 512 KB heap-allocated. - uint64_t* opcodeRValueTypeCounts; -#endif - - // Stack at the end because it is a big chunky boi (we don't want it pushing fields around) - VMStack stack; -} VMContext; - -// ===[ Public API ]=== -VMContext* VM_create(DataWin* dataWin); -void VM_reset(VMContext* ctx); -RValue VM_executeCode(VMContext* ctx, int32_t codeIndex); -RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t argCount); -void VM_free(VMContext* ctx); -bool VM_isObjectOrDescendant(DataWin* dataWin, int32_t objectIndex, int32_t targetObjectIndex); -void VM_buildCrossReferences(VMContext* ctx); -void VM_disassemble(VMContext* ctx, int32_t codeIndex); -#ifdef ENABLE_VM_OPCODE_PROFILER -// Prints a sorted summary of opcode execution counts to stderr. Does nothing if the opcode profiler was never enabled. -void VM_printOpcodeProfilerReport(const VMContext* ctx); -#endif -void VM_registerBuiltin(VMContext* ctx, const char* name, BuiltinFunc func); -BuiltinFunc VM_findBuiltin(VMContext* ctx, const char* name); -RValue VM_createArray(VMContext* ctx); -void VM_arraySet(VMContext* ctx, RValue* arrayRef, int32_t index, RValue val); - -static const char* VM_getCallerName(VMContext* ctx) { - return ctx->currentCodeName != nullptr ? ctx->currentCodeName : ""; -} - -static char* VM_createDedupKey(const char* callerName, const char* funcName) { - // Build dedup key: "callerName\tfuncName" - size_t keyLen = strlen(callerName) + 1 + strlen(funcName) + 1; - char* dedupKey = safeMalloc(keyLen); - snprintf(dedupKey, keyLen, "%s\t%s", callerName, funcName); - return dedupKey; -} +#pragma once + +#include "common.h" +#include +#include + +#include "data_win.h" +#include "rvalue.h" +#include "utils.h" +#include "profiler.h" +#include "int_int_hashmap.h" + +// ===[ Instance Types (signed 16-bit) ]=== +#define INSTANCE_SELF (-1) +#define INSTANCE_OTHER (-2) +#define INSTANCE_ALL (-3) +#define INSTANCE_NOONE (-4) +#define INSTANCE_GLOBAL (-5) +#define INSTANCE_BUILTIN (-6) +#define INSTANCE_LOCAL (-7) +#define INSTANCE_STACKTOP (-9) +#define INSTANCE_ARG (-15) + +// ===[ Variable Types (upper 5 bits of varRef, extracted with (varRef >> 24) & 0xF8) ]=== +#define VARTYPE_ARRAY 0x00 +#define VARTYPE_STACKTOP 0x80 +#define VARTYPE_NORMAL 0xA0 +#define VARTYPE_INSTANCE 0xE0 + +// ===[ Room Constants ]=== +#define ROOM_RESTARTGAME (-200) // The reason why it is -200 is because the GameMaker-HTML5 runner uses -200 too (see Globals.js) + +// ===[ GML Math Epsilon (used for floating-point comparisons) ]=== +// The real GameMaker runner uses epsilon-based comparison for all numeric CMP operations. +// Default value matches the HTML5 runner's g_GMLMathEpsilon (1e-5 for double precision). +// When using single-precision floats, we use 1e-4 to work around accumulated rounding errors from +// non-IEEE FPUs (example: PS2's R5900 which rounds toward zero instead of round-to-nearest) can +// exceed the default epsilon. +#ifdef USE_FLOAT_REALS +#define GML_MATH_EPSILON 1e-4 +#else +#define GML_MATH_EPSILON 1e-5 +#endif + +// GMS 1.4 supports up to 16 arguments per script call +#define GML_MAX_ARGUMENTS 16 + +// ===[ Comparison Kinds ]=== +#define CMP_LT 1 +#define CMP_LTE 2 +#define CMP_EQ 3 +#define CMP_NEQ 4 +#define CMP_GTE 5 +#define CMP_GT 6 + +// ===[ Opcodes ]=== +#define OP_CONV 0x07 +#define OP_MUL 0x08 +#define OP_DIV 0x09 +#define OP_REM 0x0A +#define OP_MOD 0x0B +#define OP_ADD 0x0C +#define OP_SUB 0x0D +#define OP_AND 0x0E +#define OP_OR 0x0F +#define OP_XOR 0x10 +#define OP_NEG 0x11 +#define OP_NOT 0x12 +#define OP_SHL 0x13 +#define OP_SHR 0x14 +#define OP_CMP 0x15 +#define OP_POP 0x45 +#define OP_PUSHI 0x84 +#define OP_DUP 0x86 +#define OP_CALLV 0x99 +#define OP_RET 0x9C +#define OP_EXIT 0x9D +#define OP_POPZ 0x9E +#define OP_B 0xB6 +#define OP_BT 0xB7 +#define OP_BF 0xB8 +#define OP_PUSHENV 0xBA +#define OP_POPENV 0xBB +#define OP_PUSH 0xC0 +#define OP_PUSHLOC 0xC1 +#define OP_PUSHGLB 0xC2 +#define OP_PUSHBLTN 0xC3 +#define OP_CALL 0xD9 +#define OP_BREAK 0xFF + +// ===[ Extended BREAK Sub-Opcodes (bytecode version 17+) ]=== +// Encoded in bits 0-15 of the BREAK instruction (instrInstanceType field, as int16_t) +#define BREAK_CHKINDEX (-1) // Validate array index bounds +#define BREAK_PUSHAF (-2) // Pop array ref + index, push element (final dimension) +#define BREAK_POPAF (-3) // Pop value + array ref + index, store at index +#define BREAK_PUSHAC (-4) // Pop array ref + index, push sub-array ref (intermediate dimension) +#define BREAK_SETOWNER (-5) // Pop and discard (copy-on-write owner tracking) +#define BREAK_ISSTATICOK (-6) // Push bool: has static init already run for this function? +#define BREAK_SETSTATIC (-7) // Mark current function's static as initialized +#define BREAK_SAVEAREF (-8) // Save top-of-stack array ref for compound assignment +#define BREAK_RESTOREAREF (-9) // Push previously saved array ref + +// ===[ Variable Types for V17 Array Access ]=== +#define VARTYPE_ARRAYPUSHAF 0x10 // Push array reference (read context) +#define VARTYPE_ARRAYPOPAF 0x90 // Push array reference (write context) + +// ===[ FuncCallCache - Cached resolution for CALL instructions ]=== +// Avoids per-call string hash lookups in both the builtin map and funcMap. +// Resolved once during VM_create, then used directly by handleCall. +typedef struct { + void* builtin; // cached BuiltinFunc pointer, or nullptr + int32_t scriptCodeIndex; // cached script code index, or -1 if not a script +} FuncCallCache; + +// ===[ CallFrame - Saved state for script-to-script calls ]=== +typedef struct CallFrame { + uint32_t savedIP; + uint32_t savedCodeEnd; + uint8_t* savedBytecodeBase; + RValue* savedLocals; + uint32_t savedLocalsCount; + const char* savedCodeName; + int32_t savedSavearefBalance; + IntIntHashMap* savedCodeLocalsSlotMap; + RValue* savedScriptArgs; + int32_t savedScriptArgCount; + int32_t savedCurrentCodeIndex; + struct CallFrame* parent; +} CallFrame; + +// ===[ EnvFrame - Saved context for with-statement (PushEnv/PopEnv) ]=== +typedef struct EnvFrame { + struct Instance* savedInstance; + struct Instance* savedOtherInstance; // Saved otherInstance to restore on PopEnv + struct Instance** instanceList; // stb_ds array of matching instances (nullptr for single-instance) + int32_t currentIndex; // Current position in instanceList + struct EnvFrame* parent; +} EnvFrame; + +// ===[ VMStack - Upward-growing array of RValue slots ]=== +#define VM_STACK_SIZE 1024 + +typedef struct { + int32_t top; + RValue slots[VM_STACK_SIZE]; +} VMStack; + +// Forward declarations +struct Runner; +typedef struct VMContext VMContext; + +// ===[ Builtin Functions Manager ]=== +typedef RValue (*BuiltinFunc)(VMContext* ctx, RValue* args, int32_t argCount); + +typedef struct { + char* key; + BuiltinFunc value; +} BuiltinEntry; + +// ===[ VMContext - Holds all VM state ]=== +// Fields are ordered by access frequency so that the hottest data sits in the first bytes of the struct +// This way data can be kept "hot" in the CPU cache or, depending on the platform, in scratchpad RAM +typedef struct VMContext { + // Hot: touched every instruction in the dispatch loop + uint8_t* bytecodeBase; + uint32_t ip; + uint32_t codeEnd; + RValue* localVars; + uint32_t localVarCount; + RValue* globalVars; + uint32_t globalVarCount; + struct Instance* currentInstance; + struct Instance* otherInstance; // "other" instance for collision events + DataWin* dataWin; + struct Runner* runner; + // BC17+: varID -> localVars slot lookup for the current code. Points into codeLocalsSlotMaps[currentCodeIndex] for BC17+, nullptr for BC16. + IntIntHashMap* currentCodeLocalsSlotMap; + FuncCallCache* funcCallCache; + const char* currentCodeName; + int32_t currentCodeIndex; // Index into code.entries for the currently executing code + + // Warm: touched on calls, variable resolution, event dispatch + CallFrame* callStack; + int32_t callDepth; + EnvFrame* envStack; // Environment stack for with-statements (PushEnv/PopEnv) + RValue* scriptArgs; // Arguments passed to current script (nullptr for non-script code) + int32_t scriptArgCount; // Number of arguments passed + int32_t selfId; + int32_t otherId; + // Current event context (set by Runner_executeEvent, -1 when not in an event) + int32_t currentEventType; + int32_t currentEventSubtype; + int32_t currentEventObjectIndex; // objectIndex of the object that owns the executing event handler + // Cached varID for the built-in "creator" self variable (-1 if not found) + int32_t creatorVarID; + uint32_t funcCallCacheCount; + bool traceEventInherited; + bool hasFixedSeed; + bool actionRelativeFlag; // D&D action relative flag (set by action_set_relative) + + // V17+ extended BREAK opcode state + bool* staticInitialized; // Per-code-entry flag for isstaticok/setstatic (allocated in VM_create) + // BC17+: owner token set by BREAK_SETOWNER. Arrays whose .owner mismatches fork on write. + void* currentArrayOwner; + // SAVEAREF/RESTOREAREF balance tracker. + int32_t savearefBalance; + + // Cold: init-only or rare lookups + BuiltinEntry* builtinMap; + bool registeredBuiltinFunctions; + // funcName -> codeIndex hash map (stb_ds) + struct { char* key; int32_t value; }* codeIndexByName; + // codeName -> CodeLocals* hash map (stb_ds) + struct { char* key; CodeLocals* value; }* codeLocalsMap; + // BC17+: A map of CODE indexes -> localVars slot lookup map + IntIntHashMap* codeLocalsSlotMaps; + // varName -> varID hash map for global variables (stb_ds) + struct { char* key; int32_t value; }* globalVarNameMap; + // varName -> varID hash map for self/instance-scoped variables (stb_ds). + struct { char* key; int32_t value; }* selfVarNameMap; + // codeName -> BuiltinFunc: native overrides for specific GML code entries. + // When VM_executeCode finds the executing code's name in this map it calls + // the native function directly and skips the bytecode interpreter entirely. + // Registered via VM_registerCodeOverride. nullptr until first registration. + struct { char* key; BuiltinFunc value; }* codeOverrideMap; + // "codeName\tfuncName" -> true, for deduplicating unknown function warnings + StringBooleanEntry* loggedUnknownFuncs; + // "codeName\tfuncName" -> true, for deduplicating stubbed function warnings + StringBooleanEntry* loggedStubbedFuncs; + // Cross-reference map for disassembler: targetCodeIndex -> stb_ds array of callerCodeIndex + struct { int32_t key; int32_t* value; }* crossRefMap; + bool alwaysLogUnknownFunctions; + bool alwaysLogStubbedFunctions; +#ifdef ENABLE_VM_TRACING + StringBooleanEntry* varReadsToBeTraced; + StringBooleanEntry* varWritesToBeTraced; + StringBooleanEntry* functionCallsToBeTraced; + StringBooleanEntry* alarmsToBeTraced; + StringBooleanEntry* instanceLifecyclesToBeTraced; + StringBooleanEntry* eventsToBeTraced; + StringBooleanEntry* opcodesToBeTraced; + StringBooleanEntry* stackToBeTraced; + StringBooleanEntry* tilesToBeTraced; + // Minimum frameCount before opcode/stack traces are emitted (default 0) + int traceBytecodeAfterFrame; +#endif + Profiler* profiler; + +#ifdef ENABLE_VM_OPCODE_PROFILER + bool opcodeProfilerEnabled; + uint64_t opcodeCounts[256]; + // Per-opcode breakdown by (type1, type2). Heap-allocated when the profiler is enabled (512 KB), nullptr otherwise. + // Indexed as opcodeVariantCounts[opcode * 256 + type1 * 16 + type2]. + uint64_t* opcodeVariantCounts; + // BREAK (0xFF) sub-opcode counts. Indexed by -breakType (so -1 -> [1], -9 -> [9]). Size 64 covers all currently defined sub-ops with room to spare. + uint64_t breakSubOpCounts[64]; + // Per-opcode breakdown by actual runtime RValue types (typeA, typeB) for arithmetic/comparison/conversion ops. + // Indexed as opcodeRValueTypeCounts[opcode * 256 + typeA * 16 + typeB]. typeB = 0xF for unary ops. 512 KB heap-allocated. + uint64_t* opcodeRValueTypeCounts; +#endif + + // Stack at the end because it is a big chunky boi (we don't want it pushing fields around) + VMStack stack; +} VMContext; + +// ===[ Public API ]=== +VMContext* VM_create(DataWin* dataWin); +void VM_reset(VMContext* ctx); +RValue VM_executeCode(VMContext* ctx, int32_t codeIndex); +RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t argCount); +void VM_free(VMContext* ctx); +bool VM_isObjectOrDescendant(DataWin* dataWin, int32_t objectIndex, int32_t targetObjectIndex); +void VM_buildCrossReferences(VMContext* ctx); +void VM_disassemble(VMContext* ctx, int32_t codeIndex); +#ifdef ENABLE_VM_OPCODE_PROFILER +// Prints a sorted summary of opcode execution counts to stderr. Does nothing if the opcode profiler was never enabled. +void VM_printOpcodeProfilerReport(const VMContext* ctx); +#endif +void VM_registerCodeOverride(VMContext* ctx, const char* codeName, BuiltinFunc func); +void VM_registerBuiltin(VMContext* ctx, const char* name, BuiltinFunc func); +BuiltinFunc VM_findBuiltin(VMContext* ctx, const char* name); +RValue VM_createArray(VMContext* ctx); +void VM_arraySet(VMContext* ctx, RValue* arrayRef, int32_t index, RValue val); + +static const char* VM_getCallerName(VMContext* ctx) { + return ctx->currentCodeName != nullptr ? ctx->currentCodeName : ""; +} + +static char* VM_createDedupKey(const char* callerName, const char* funcName) { + // Build dedup key: "callerName\tfuncName" + size_t keyLen = strlen(callerName) + 1 + strlen(funcName) + 1; + char* dedupKey = safeMalloc(keyLen); + snprintf(dedupKey, keyLen, "%s\t%s", callerName, funcName); + return dedupKey; +} \ No newline at end of file diff --git a/src/vm_builtins.c b/src/vm_builtins.c index 1f258343..9f2b5b89 100644 --- a/src/vm_builtins.c +++ b/src/vm_builtins.c @@ -1,8905 +1,10996 @@ -#include "vm_builtins.h" -#include "binary_utils.h" -#include "instance.h" -#include "json_reader.h" -#include "runner.h" -#include "runner_gamepad.h" -#include "utils.h" - -#include -#include -#include -#include -#include -#include -#ifdef _WIN32 -#include -#endif - -#include "rvalue.h" -#include "stb_ds.h" -#include "text_utils.h" -#include "collision.h" -#include "ini.h" -#include "audio_system.h" -#include "file_system.h" - -#define MAX_BACKGROUNDS 8 - -// ===[ STUB LOGGING ]=== - -#ifdef ENABLE_VM_STUB_LOGS -static void logStubbedFunction(VMContext* ctx, const char* funcName) { - const char* callerName = VM_getCallerName(ctx); - char* dedupKey = VM_createDedupKey(callerName, funcName); - - if (ctx->alwaysLogStubbedFunctions || 0 > shgeti(ctx->loggedStubbedFuncs, dedupKey)) { - // shput stores the key pointer, so don't free it when inserting - shput(ctx->loggedStubbedFuncs, dedupKey, true); - fprintf(stderr, "VM: [%s] Stubbed function \"%s\"!\n", callerName, funcName); - } else { - free(dedupKey); - } -} - -static void logSemiStubbedFunction(VMContext* ctx, const char* funcName) { - const char* callerName = VM_getCallerName(ctx); - char* dedupKey = VM_createDedupKey(callerName, funcName); - - if (ctx->alwaysLogStubbedFunctions || 0 > shgeti(ctx->loggedStubbedFuncs, dedupKey)) { - // shput stores the key pointer, so don't free it when inserting - shput(ctx->loggedStubbedFuncs, dedupKey, true); - fprintf(stderr, "VM: [%s] Semi-Stubbed function \"%s\"!\n", callerName, funcName); - } else { - free(dedupKey); - } -} -#else -#define logStubbedFunction(ctx, funcName) ((void) 0) -#define logSemiStubbedFunction(ctx, funcName) ((void) 0) -#endif - -// Forward declarations -static int32_t resolveLayerIdArg(Runner* runner, RValue arg); - -// ===[ DS_MAP SYSTEM ]=== - -static int32_t dsMapCreate(Runner* runner) { - DsMapEntry* newMap = nullptr; - int32_t id = (int32_t) arrlen(runner->dsMapPool); - arrput(runner->dsMapPool, newMap); - return id; -} - -static DsMapEntry** dsMapGet(Runner* runner, int32_t id) { - if (id < 0 || (int32_t) arrlen(runner->dsMapPool) <= id) return nullptr; - return &runner->dsMapPool[id]; -} - -// ===[ DS_LIST SYSTEM ]=== - -static int32_t dsListCreate(Runner* runner) { - // Reuse a freed slot if available, matching native GameMaker behavior. - // Yes, some games (example: DELTARUNE Chapter 3's obj_board_playercamera_Other_10) rely on ds_list_create reusing the id of a list just destroyed. - int32_t poolSize = (int32_t) arrlen(runner->dsListPool); - repeat(poolSize, i) { - if (runner->dsListPool[i].freed) { - runner->dsListPool[i].freed = false; - runner->dsListPool[i].items = nullptr; - return i; - } - } - DsList newList = { .items = nullptr, .freed = false }; - int32_t id = poolSize; - arrput(runner->dsListPool, newList); - return id; -} - -static DsList* dsListGet(Runner* runner, int32_t id) { - if (0 > id || id >= (int32_t) arrlen(runner->dsListPool)) return nullptr; - if (runner->dsListPool[id].freed) return nullptr; - return &runner->dsListPool[id]; -} - -// ===[ BUILT-IN VARIABLE GET/SET ]=== - -/** - * Gets the argument number from the name - * - * If it returns -1, then the name is not an argument variable - * - * @param name The name - * @return The argument number, -1 if it is not an argument variable - */ -static int extractArgumentNumber(const char* name) { - if (strncmp(name, "argument", 8) == 0) { - char* end; - long argNumber = strtol(name + 8, &end, 10); - if (end == name + 8 || *end != '\0' || 0 > argNumber || argNumber > 15) return -1; - return (int) argNumber; - } - return -1; -} - -static bool isValidAlarmIndex(int alarmIndex) { - return alarmIndex >= 0 && GML_ALARM_COUNT > alarmIndex; -} - -// Sorted (strcmp-order, LC_ALL=C) table of built-in variable names -> enum IDs. -// We use bsearch instead of a HashMap because we don't have *that* many builtin var entries, so it is faster to use bsearch than a HashMap. -// IMPORTANT: Entries MUST stay sorted by name for bsearch to work! -typedef struct { - const char* name; - int16_t id; -} BuiltinVarEntry; - -static const BuiltinVarEntry BUILTIN_VAR_TABLE[] = { - { "alarm", BUILTIN_VAR_ALARM }, - { "application_surface", BUILTIN_VAR_APPLICATION_SURFACE }, - { "argument", BUILTIN_VAR_ARGUMENT }, - { "argument0", BUILTIN_VAR_ARGUMENT0 }, - { "argument1", BUILTIN_VAR_ARGUMENT1 }, - { "argument10", BUILTIN_VAR_ARGUMENT10 }, - { "argument11", BUILTIN_VAR_ARGUMENT11 }, - { "argument12", BUILTIN_VAR_ARGUMENT12 }, - { "argument13", BUILTIN_VAR_ARGUMENT13 }, - { "argument14", BUILTIN_VAR_ARGUMENT14 }, - { "argument15", BUILTIN_VAR_ARGUMENT15 }, - { "argument2", BUILTIN_VAR_ARGUMENT2 }, - { "argument3", BUILTIN_VAR_ARGUMENT3 }, - { "argument4", BUILTIN_VAR_ARGUMENT4 }, - { "argument5", BUILTIN_VAR_ARGUMENT5 }, - { "argument6", BUILTIN_VAR_ARGUMENT6 }, - { "argument7", BUILTIN_VAR_ARGUMENT7 }, - { "argument8", BUILTIN_VAR_ARGUMENT8 }, - { "argument9", BUILTIN_VAR_ARGUMENT9 }, - { "argument_count", BUILTIN_VAR_ARGUMENT_COUNT }, - { "async_load", BUILTIN_VAR_ASYNC_LOAD }, - { "background_alpha", BUILTIN_VAR_BACKGROUND_ALPHA }, - { "background_color", BUILTIN_VAR_BACKGROUND_COLOR }, - { "background_colour", BUILTIN_VAR_BACKGROUND_COLOUR }, - { "background_height", BUILTIN_VAR_BACKGROUND_HEIGHT }, - { "background_hspeed", BUILTIN_VAR_BACKGROUND_HSPEED }, - { "background_index", BUILTIN_VAR_BACKGROUND_INDEX }, - { "background_visible", BUILTIN_VAR_BACKGROUND_VISIBLE }, - { "background_vspeed", BUILTIN_VAR_BACKGROUND_VSPEED }, - { "background_width", BUILTIN_VAR_BACKGROUND_WIDTH }, - { "background_x", BUILTIN_VAR_BACKGROUND_X }, - { "background_y", BUILTIN_VAR_BACKGROUND_Y }, - { "bbox_bottom", BUILTIN_VAR_BBOX_BOTTOM }, - { "bbox_left", BUILTIN_VAR_BBOX_LEFT }, - { "bbox_right", BUILTIN_VAR_BBOX_RIGHT }, - { "bbox_top", BUILTIN_VAR_BBOX_TOP }, - { "buffer_bool", BUILTIN_VAR_BUFFER_BOOL }, - { "buffer_f16", BUILTIN_VAR_BUFFER_F16 }, - { "buffer_f32", BUILTIN_VAR_BUFFER_F32 }, - { "buffer_f64", BUILTIN_VAR_BUFFER_F64 }, - { "buffer_fast", BUILTIN_VAR_BUFFER_FAST }, - { "buffer_fixed", BUILTIN_VAR_BUFFER_FIXED }, - { "buffer_grow", BUILTIN_VAR_BUFFER_GROW }, - { "buffer_s16", BUILTIN_VAR_BUFFER_S16 }, - { "buffer_s32", BUILTIN_VAR_BUFFER_S32 }, - { "buffer_s8", BUILTIN_VAR_BUFFER_S8 }, - { "buffer_seek_end", BUILTIN_VAR_BUFFER_SEEK_END }, - { "buffer_seek_relative", BUILTIN_VAR_BUFFER_SEEK_RELATIVE }, - { "buffer_seek_start", BUILTIN_VAR_BUFFER_SEEK_START }, - { "buffer_string", BUILTIN_VAR_BUFFER_STRING }, - { "buffer_text", BUILTIN_VAR_BUFFER_TEXT }, - { "buffer_u16", BUILTIN_VAR_BUFFER_U16 }, - { "buffer_u32", BUILTIN_VAR_BUFFER_U32 }, - { "buffer_u64", BUILTIN_VAR_BUFFER_U64 }, - { "buffer_u8", BUILTIN_VAR_BUFFER_U8 }, - { "buffer_wrap", BUILTIN_VAR_BUFFER_WRAP }, - { "current_time", BUILTIN_VAR_CURRENT_TIME }, - { "debug_mode", BUILTIN_VAR_DEBUG_MODE }, - { "depth", BUILTIN_VAR_DEPTH }, - { "direction", BUILTIN_VAR_DIRECTION }, - { "false", BUILTIN_VAR_FALSE }, - { "fps", BUILTIN_VAR_FPS }, - { "friction", BUILTIN_VAR_FRICTION }, - { "gp_axislh", BUILTIN_VAR_GP_AXIS_LH }, - { "gp_axislv", BUILTIN_VAR_GP_AXIS_LV }, - { "gp_axisrh", BUILTIN_VAR_GP_AXIS_RH }, - { "gp_axisrv", BUILTIN_VAR_GP_AXIS_RV }, - { "gp_face1", BUILTIN_VAR_GP_FACE1 }, - { "gp_face2", BUILTIN_VAR_GP_FACE2 }, - { "gp_face3", BUILTIN_VAR_GP_FACE3 }, - { "gp_face4", BUILTIN_VAR_GP_FACE4 }, - { "gp_home", BUILTIN_VAR_GP_HOME }, - { "gp_padd", BUILTIN_VAR_GP_PADD }, - { "gp_padl", BUILTIN_VAR_GP_PADL }, - { "gp_padr", BUILTIN_VAR_GP_PADR }, - { "gp_padu", BUILTIN_VAR_GP_PADU }, - { "gp_select", BUILTIN_VAR_GP_SELECT }, - { "gp_shoulderl", BUILTIN_VAR_GP_SHOULDERL }, - { "gp_shoulderlb", BUILTIN_VAR_GP_SHOULDERLB }, - { "gp_shoulderr", BUILTIN_VAR_GP_SHOULDERR }, - { "gp_shoulderrb", BUILTIN_VAR_GP_SHOULDERRB }, - { "gp_start", BUILTIN_VAR_GP_START }, - { "gp_stickl", BUILTIN_VAR_GP_STICKL }, - { "gp_stickr", BUILTIN_VAR_GP_STICKR }, - { "gravity", BUILTIN_VAR_GRAVITY }, - { "gravity_direction", BUILTIN_VAR_GRAVITY_DIRECTION }, - { "hspeed", BUILTIN_VAR_HSPEED }, - { "id", BUILTIN_VAR_ID }, - { "image_alpha", BUILTIN_VAR_IMAGE_ALPHA }, - { "image_angle", BUILTIN_VAR_IMAGE_ANGLE }, - { "image_blend", BUILTIN_VAR_IMAGE_BLEND }, - { "image_index", BUILTIN_VAR_IMAGE_INDEX }, - { "image_number", BUILTIN_VAR_IMAGE_NUMBER }, - { "image_speed", BUILTIN_VAR_IMAGE_SPEED }, - { "image_xscale", BUILTIN_VAR_IMAGE_XSCALE }, - { "image_yscale", BUILTIN_VAR_IMAGE_YSCALE }, - { "keyboard_key", BUILTIN_VAR_KEYBOARD_KEY }, - { "keyboard_lastchar", BUILTIN_VAR_KEYBOARD_LASTCHAR }, - { "keyboard_lastkey", BUILTIN_VAR_KEYBOARD_LASTKEY }, - { "layer", BUILTIN_VAR_LAYER }, - { "mask_index", BUILTIN_VAR_MASK_INDEX }, - { "object_index", BUILTIN_VAR_OBJECT_INDEX }, - { "os_3ds", BUILTIN_VAR_OS_3DS }, - { "os_amazon", BUILTIN_VAR_OS_AMAZON }, - { "os_android", BUILTIN_VAR_OS_ANDROID }, - { "os_bb10", BUILTIN_VAR_OS_BB10 }, - { "os_ios", BUILTIN_VAR_OS_IOS }, - { "os_linux", BUILTIN_VAR_OS_LINUX }, - { "os_llvm_android", BUILTIN_VAR_OS_LLVM_ANDROID }, - { "os_llvm_ios", BUILTIN_VAR_OS_LLVM_IOS }, - { "os_llvm_linux", BUILTIN_VAR_OS_LLVM_LINUX }, - { "os_llvm_macosx", BUILTIN_VAR_OS_LLVM_MACOSX }, - { "os_llvm_psp", BUILTIN_VAR_OS_LLVM_PSP }, - { "os_llvm_symbian", BUILTIN_VAR_OS_LLVM_SYMBIAN }, - { "os_llvm_win32", BUILTIN_VAR_OS_LLVM_WIN32 }, - { "os_llvm_winphone", BUILTIN_VAR_OS_LLVM_WINPHONE }, - { "os_macosx", BUILTIN_VAR_OS_MACOSX }, - { "os_ps3", BUILTIN_VAR_OS_PS3 }, - { "os_ps4", BUILTIN_VAR_OS_PS4 }, - { "os_psp", BUILTIN_VAR_OS_PSP }, - { "os_psvita", BUILTIN_VAR_OS_PSVITA }, - { "os_switch", BUILTIN_VAR_OS_SWITCH }, - { "os_symbian", BUILTIN_VAR_OS_SYMBIAN }, - { "os_tizen", BUILTIN_VAR_OS_TIZEN }, - { "os_type", BUILTIN_VAR_OS_TYPE }, - { "os_unknown", BUILTIN_VAR_OS_UNKNOWN }, - { "os_uwp", BUILTIN_VAR_OS_UWP }, - { "os_wiiu", BUILTIN_VAR_OS_WIIU }, - { "os_win32", BUILTIN_VAR_OS_WIN32 }, - { "os_win8native", BUILTIN_VAR_OS_WIN8NATIVE }, - { "os_windows", BUILTIN_VAR_OS_WINDOWS }, - { "os_winphone", BUILTIN_VAR_OS_WINPHONE }, - { "os_xbox360", BUILTIN_VAR_OS_XBOX360 }, - { "os_xboxone", BUILTIN_VAR_OS_XBOXONE }, - { "path_action_continue", BUILTIN_VAR_PATH_ACTION_CONTINUE }, - { "path_action_restart", BUILTIN_VAR_PATH_ACTION_RESTART }, - { "path_action_reverse", BUILTIN_VAR_PATH_ACTION_REVERSE }, - { "path_action_stop", BUILTIN_VAR_PATH_ACTION_STOP }, - { "path_endaction", BUILTIN_VAR_PATH_ENDACTION }, - { "path_index", BUILTIN_VAR_PATH_INDEX }, - { "path_orientation", BUILTIN_VAR_PATH_ORIENTATION }, - { "path_position", BUILTIN_VAR_PATH_POSITION }, - { "path_positionprevious", BUILTIN_VAR_PATH_POSITIONPREVIOUS }, - { "path_scale", BUILTIN_VAR_PATH_SCALE }, - { "path_speed", BUILTIN_VAR_PATH_SPEED }, - { "persistent", BUILTIN_VAR_PERSISTENT }, - { "pi", BUILTIN_VAR_PI }, - { "room", BUILTIN_VAR_ROOM }, - { "room_first", BUILTIN_VAR_ROOM_FIRST }, - { "room_height", BUILTIN_VAR_ROOM_HEIGHT }, - { "room_persistent", BUILTIN_VAR_ROOM_PERSISTENT }, - { "room_speed", BUILTIN_VAR_ROOM_SPEED }, - { "room_width", BUILTIN_VAR_ROOM_WIDTH }, - { "solid", BUILTIN_VAR_SOLID }, - { "speed", BUILTIN_VAR_SPEED }, - { "sprite_height", BUILTIN_VAR_SPRITE_HEIGHT }, - { "sprite_index", BUILTIN_VAR_SPRITE_INDEX }, - { "sprite_width", BUILTIN_VAR_SPRITE_WIDTH }, - { "sprite_xoffset", BUILTIN_VAR_SPRITE_XOFFSET }, - { "sprite_yoffset", BUILTIN_VAR_SPRITE_YOFFSET }, - { "true", BUILTIN_VAR_TRUE }, - { "undefined", BUILTIN_VAR_UNDEFINED }, - { "view_angle", BUILTIN_VAR_VIEW_ANGLE }, - { "view_current", BUILTIN_VAR_VIEW_CURRENT }, - { "view_hborder", BUILTIN_VAR_VIEW_HBORDER }, - { "view_hport", BUILTIN_VAR_VIEW_HPORT }, - { "view_hspeed", BUILTIN_VAR_VIEW_HSPEED }, - { "view_hview", BUILTIN_VAR_VIEW_HVIEW }, - { "view_object", BUILTIN_VAR_VIEW_OBJECT }, - { "view_vborder", BUILTIN_VAR_VIEW_VBORDER }, - { "view_visible", BUILTIN_VAR_VIEW_VISIBLE }, - { "view_vspeed", BUILTIN_VAR_VIEW_VSPEED }, - { "view_wport", BUILTIN_VAR_VIEW_WPORT }, - { "view_wview", BUILTIN_VAR_VIEW_WVIEW }, - { "view_xport", BUILTIN_VAR_VIEW_XPORT }, - { "view_xview", BUILTIN_VAR_VIEW_XVIEW }, - { "view_yport", BUILTIN_VAR_VIEW_YPORT }, - { "view_yview", BUILTIN_VAR_VIEW_YVIEW }, - { "visible", BUILTIN_VAR_VISIBLE }, - { "vspeed", BUILTIN_VAR_VSPEED }, - { "working_directory", BUILTIN_VAR_WORKING_DIRECTORY }, - { "x", BUILTIN_VAR_X }, - { "xprevious", BUILTIN_VAR_XPREVIOUS }, - { "xstart", BUILTIN_VAR_XSTART }, - { "y", BUILTIN_VAR_Y }, - { "yprevious", BUILTIN_VAR_YPREVIOUS }, - { "ystart", BUILTIN_VAR_YSTART }, -}; - -static int compareBuiltinVarEntry(const void* keyPtr, const void* entryPtr) { - const char* key = (const char*) keyPtr; - const BuiltinVarEntry* entry = (const BuiltinVarEntry*) entryPtr; - return strcmp(key, entry->name); -} - -// Resolves a built-in variable name to its enum ID -int16_t VMBuiltins_resolveBuiltinVarId(const char* name) { - size_t count = sizeof(BUILTIN_VAR_TABLE) / sizeof(BUILTIN_VAR_TABLE[0]); - BuiltinVarEntry* hit = (BuiltinVarEntry*) bsearch(name, BUILTIN_VAR_TABLE, count, sizeof(BuiltinVarEntry), compareBuiltinVarEntry); - return hit == nullptr ? BUILTIN_VAR_UNKNOWN : hit->id; -} - -void VMBuiltins_checkIfBuiltinVarTableIsSorted(void) { - size_t count = sizeof(BUILTIN_VAR_TABLE) / sizeof(BUILTIN_VAR_TABLE[0]); - for (size_t i = 1; count > i; i++) { - int cmp = strcmp(BUILTIN_VAR_TABLE[i - 1].name, BUILTIN_VAR_TABLE[i].name); - requireMessageFormatted(cmp < 0, "BUILTIN_VAR_TABLE not strictly sorted at index %zu: '%s' vs '%s' (cmp=%d). Re-sort (LC_ALL=C) or remove duplicates!", i, BUILTIN_VAR_TABLE[i - 1].name, BUILTIN_VAR_TABLE[i].name, cmp); - } -} - -RValue VMBuiltins_getVariable(VMContext* ctx, int16_t builtinVarId, const char* name, int32_t arrayIndex) { - Instance* inst = (Instance*) ctx->currentInstance; - Runner* runner = (Runner*) ctx->runner; - requireNotNull(runner); - - // In the past Butterscotch used cascading ifs for this, which in my opinion looked nicer AND GCC was converting the ifs into a jump table, so it was all well... - // ...until the code changed enough and the GCC heuristic thought "you know what? let's drop the jump table!" - // So that's why this (and setVariable) are a jump table - switch (builtinVarId) { - // File system - case BUILTIN_VAR_WORKING_DIRECTORY: { - FileSystem* fs = runner->fileSystem; - return RValue_makeOwnedString(fs->vtable->resolvePath(fs, "")); - } - - // OS constants - case BUILTIN_VAR_OS_TYPE: - return RValue_makeReal(runner->osType); - case BUILTIN_VAR_OS_UNKNOWN: - return RValue_makeReal(OS_UNKNOWN); - case BUILTIN_VAR_OS_WIN32: - return RValue_makeReal(OS_WINDOWS); - case BUILTIN_VAR_OS_WINDOWS: - return RValue_makeReal(OS_WINDOWS); - case BUILTIN_VAR_OS_MACOSX: - return RValue_makeReal(OS_MACOSX); - case BUILTIN_VAR_OS_PSP: - return RValue_makeReal(OS_PSP); - case BUILTIN_VAR_OS_IOS: - return RValue_makeReal(OS_IOS); - case BUILTIN_VAR_OS_ANDROID: - return RValue_makeReal(OS_ANDROID); - case BUILTIN_VAR_OS_SYMBIAN: - return RValue_makeReal(OS_SYMBIAN); - case BUILTIN_VAR_OS_LINUX: - return RValue_makeReal(OS_LINUX); - case BUILTIN_VAR_OS_WINPHONE: - return RValue_makeReal(OS_WINPHONE); - case BUILTIN_VAR_OS_TIZEN: - return RValue_makeReal(OS_TIZEN); - case BUILTIN_VAR_OS_WIN8NATIVE: - return RValue_makeReal(OS_WIN8NATIVE); - case BUILTIN_VAR_OS_WIIU: - return RValue_makeReal(OS_WIIU); - case BUILTIN_VAR_OS_3DS: - return RValue_makeReal(OS_3DS); - case BUILTIN_VAR_OS_PSVITA: - return RValue_makeReal(OS_PSVITA); - case BUILTIN_VAR_OS_BB10: - return RValue_makeReal(OS_BB10); - case BUILTIN_VAR_OS_PS4: - return RValue_makeReal(OS_PS4); - case BUILTIN_VAR_OS_XBOXONE: - return RValue_makeReal(OS_XBOXONE); - case BUILTIN_VAR_OS_PS3: - return RValue_makeReal(OS_PS3); - case BUILTIN_VAR_OS_XBOX360: - return RValue_makeReal(OS_XBOX360); - case BUILTIN_VAR_OS_UWP: - return RValue_makeReal(OS_UWP); - case BUILTIN_VAR_OS_AMAZON: - return RValue_makeReal(OS_AMAZON); - case BUILTIN_VAR_OS_SWITCH: - return RValue_makeReal(OS_SWITCH); - case BUILTIN_VAR_OS_LLVM_WIN32: - return RValue_makeReal(OS_LLVM_WIN32); - case BUILTIN_VAR_OS_LLVM_MACOSX: - return RValue_makeReal(OS_LLVM_MACOSX); - case BUILTIN_VAR_OS_LLVM_PSP: - return RValue_makeReal(OS_LLVM_PSP); - case BUILTIN_VAR_OS_LLVM_IOS: - return RValue_makeReal(OS_LLVM_IOS); - case BUILTIN_VAR_OS_LLVM_ANDROID: - return RValue_makeReal(OS_LLVM_ANDROID); - case BUILTIN_VAR_OS_LLVM_SYMBIAN: - return RValue_makeReal(OS_LLVM_SYMBIAN); - case BUILTIN_VAR_OS_LLVM_LINUX: - return RValue_makeReal(OS_LLVM_LINUX); - case BUILTIN_VAR_OS_LLVM_WINPHONE: - return RValue_makeReal(OS_LLVM_WINPHONE); - case BUILTIN_VAR_ASYNC_LOAD: - return RValue_makeReal((GMLReal) runner->asyncLoadMapId); - - // Per-instance properties - case BUILTIN_VAR_IMAGE_SPEED: - if (inst == nullptr) break; - return RValue_makeReal(inst->imageSpeed); - case BUILTIN_VAR_IMAGE_INDEX: - if (inst == nullptr) break; - return RValue_makeReal(inst->imageIndex); - case BUILTIN_VAR_IMAGE_XSCALE: - if (inst == nullptr) break; - return RValue_makeReal(inst->imageXscale); - case BUILTIN_VAR_IMAGE_YSCALE: - if (inst == nullptr) break; - return RValue_makeReal(inst->imageYscale); - case BUILTIN_VAR_IMAGE_ANGLE: - if (inst == nullptr) break; - return RValue_makeReal(inst->imageAngle); - case BUILTIN_VAR_IMAGE_ALPHA: - if (inst == nullptr) break; - return RValue_makeReal(inst->imageAlpha); - case BUILTIN_VAR_IMAGE_BLEND: - if (inst == nullptr) break; - return RValue_makeReal((GMLReal) inst->imageBlend); - case BUILTIN_VAR_IMAGE_NUMBER: { - if (inst == nullptr) break; - if (inst->spriteIndex >= 0) { - Sprite* sprite = &ctx->runner->dataWin->sprt.sprites[inst->spriteIndex]; - return RValue_makeReal((GMLReal) sprite->textureCount); - } - return RValue_makeReal(0.0); - } - case BUILTIN_VAR_SPRITE_INDEX: - if (inst == nullptr) break; - return RValue_makeReal((GMLReal) inst->spriteIndex); - case BUILTIN_VAR_SPRITE_WIDTH: { - if (inst == nullptr) break; - if (inst->spriteIndex >= 0 && runner->dataWin->sprt.count > (uint32_t) inst->spriteIndex) { - return RValue_makeReal((GMLReal) runner->dataWin->sprt.sprites[inst->spriteIndex].width * inst->imageXscale); - } - return RValue_makeReal(0.0); - } - case BUILTIN_VAR_SPRITE_HEIGHT: { - if (inst == nullptr) break; - if (inst->spriteIndex >= 0 && runner->dataWin->sprt.count > (uint32_t) inst->spriteIndex) { - return RValue_makeReal((GMLReal) runner->dataWin->sprt.sprites[inst->spriteIndex].height * inst->imageYscale); - } - return RValue_makeReal(0.0); - } - case BUILTIN_VAR_SPRITE_XOFFSET: { - if (inst == nullptr) break; - if (inst->spriteIndex >= 0 && runner->dataWin->sprt.count > (uint32_t) inst->spriteIndex) { - return RValue_makeReal((GMLReal) runner->dataWin->sprt.sprites[inst->spriteIndex].originX * inst->imageXscale); - } - return RValue_makeReal(0.0); - } - case BUILTIN_VAR_SPRITE_YOFFSET: { - if (inst == nullptr) break; - if (inst->spriteIndex >= 0 && runner->dataWin->sprt.count > (uint32_t) inst->spriteIndex) { - return RValue_makeReal((GMLReal) runner->dataWin->sprt.sprites[inst->spriteIndex].originY * inst->imageYscale); - } - return RValue_makeReal(0.0); - } - case BUILTIN_VAR_BBOX_LEFT: { - if (inst == nullptr) break; - InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); - if (!bbox.valid) return RValue_makeReal(inst->x); - // Compat mode caches bbox values rounded via lrintf so GML reads see integers; modern mode returns the raw float bbox. - if (runner->collisionCompatibilityMode) return RValue_makeReal((GMLReal) llrint(bbox.left)); - return RValue_makeReal(bbox.left); - } - case BUILTIN_VAR_BBOX_RIGHT: { - if (inst == nullptr) break; - InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); - if (!bbox.valid) return RValue_makeReal(inst->x); - // Compat mode caches bbox values rounded via lrintf so GML reads see integers; modern mode returns the raw float bbox. - if (runner->collisionCompatibilityMode) return RValue_makeReal((GMLReal) (llrint(bbox.right) - 1)); - return RValue_makeReal(bbox.right); - } - case BUILTIN_VAR_BBOX_TOP: { - if (inst == nullptr) break; - InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); - if (!bbox.valid) return RValue_makeReal(inst->y); - // Compat mode caches bbox values rounded via lrintf so GML reads see integers; modern mode returns the raw float bbox. - if (runner->collisionCompatibilityMode) return RValue_makeReal((GMLReal) llrint(bbox.top)); - return RValue_makeReal(bbox.top); - } - case BUILTIN_VAR_BBOX_BOTTOM: { - if (inst == nullptr) break; - InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); - if (!bbox.valid) return RValue_makeReal(inst->y); - // Compat mode caches bbox values rounded via lrintf so GML reads see integers; modern mode returns the raw float bbox. - if (runner->collisionCompatibilityMode) return RValue_makeReal((GMLReal) (llrint(bbox.bottom) - 1)); - return RValue_makeReal(bbox.bottom); - } - case BUILTIN_VAR_VISIBLE: - if (inst == nullptr) break; - return RValue_makeBool(inst->visible); - case BUILTIN_VAR_DEPTH: - if (inst == nullptr) break; - return RValue_makeReal((GMLReal) inst->depth); - case BUILTIN_VAR_LAYER: - if (inst == nullptr) break; - return RValue_makeReal((GMLReal) inst->layer); - case BUILTIN_VAR_X: - if (inst == nullptr) break; - return RValue_makeReal(inst->x); - case BUILTIN_VAR_Y: - if (inst == nullptr) break; - return RValue_makeReal(inst->y); - case BUILTIN_VAR_XPREVIOUS: - if (inst == nullptr) break; - return RValue_makeReal(inst->xprevious); - case BUILTIN_VAR_YPREVIOUS: - if (inst == nullptr) break; - return RValue_makeReal(inst->yprevious); - case BUILTIN_VAR_XSTART: - if (inst == nullptr) break; - return RValue_makeReal(inst->xstart); - case BUILTIN_VAR_YSTART: - if (inst == nullptr) break; - return RValue_makeReal(inst->ystart); - case BUILTIN_VAR_MASK_INDEX: - if (inst == nullptr) break; - return RValue_makeReal((GMLReal) inst->maskIndex); - case BUILTIN_VAR_ID: - if (inst == nullptr) break; - return RValue_makeReal((GMLReal) inst->instanceId); - case BUILTIN_VAR_OBJECT_INDEX: - if (inst == nullptr) break; - return RValue_makeReal((GMLReal) inst->objectIndex); - case BUILTIN_VAR_PERSISTENT: - if (inst == nullptr) break; - return RValue_makeBool(inst->persistent); - case BUILTIN_VAR_SOLID: - if (inst == nullptr) break; - return RValue_makeBool(inst->solid); - case BUILTIN_VAR_SPEED: - if (inst == nullptr) break; - return RValue_makeReal(inst->speed); - case BUILTIN_VAR_DIRECTION: - if (inst == nullptr) break; - return RValue_makeReal(inst->direction); - case BUILTIN_VAR_HSPEED: - if (inst == nullptr) break; - return RValue_makeReal(inst->hspeed); - case BUILTIN_VAR_VSPEED: - if (inst == nullptr) break; - return RValue_makeReal(inst->vspeed); - case BUILTIN_VAR_FRICTION: - if (inst == nullptr) break; - return RValue_makeReal(inst->friction); - case BUILTIN_VAR_GRAVITY: - if (inst == nullptr) break; - return RValue_makeReal(inst->gravity); - case BUILTIN_VAR_GRAVITY_DIRECTION: - if (inst == nullptr) break; - return RValue_makeReal(inst->gravityDirection); - case BUILTIN_VAR_ALARM: { - if (inst == nullptr) break; - if (isValidAlarmIndex(arrayIndex)) return RValue_makeReal((GMLReal) inst->alarm[arrayIndex]); - return RValue_makeReal(-1.0); - } - - // Path instance variables - case BUILTIN_VAR_PATH_INDEX: - if (inst == nullptr) break; - return RValue_makeReal((GMLReal) inst->pathIndex); - case BUILTIN_VAR_PATH_POSITION: - if (inst == nullptr) break; - return RValue_makeReal(inst->pathPosition); - case BUILTIN_VAR_PATH_POSITIONPREVIOUS: - if (inst == nullptr) break; - return RValue_makeReal(inst->pathPositionPrevious); - case BUILTIN_VAR_PATH_SPEED: - if (inst == nullptr) break; - return RValue_makeReal(inst->pathSpeed); - case BUILTIN_VAR_PATH_SCALE: - if (inst == nullptr) break; - return RValue_makeReal(inst->pathScale); - case BUILTIN_VAR_PATH_ORIENTATION: - if (inst == nullptr) break; - return RValue_makeReal(inst->pathOrientation); - case BUILTIN_VAR_PATH_ENDACTION: - if (inst == nullptr) break; - return RValue_makeReal((GMLReal) inst->pathEndAction); - - // Room properties - case BUILTIN_VAR_ROOM: - return RValue_makeReal((GMLReal) runner->currentRoomIndex); - case BUILTIN_VAR_ROOM_FIRST: - return RValue_makeReal((GMLReal) runner->dataWin->gen8.roomOrder[0]); - case BUILTIN_VAR_ROOM_SPEED: - return RValue_makeReal((GMLReal) runner->currentRoom->speed); - case BUILTIN_VAR_ROOM_WIDTH: - return RValue_makeReal((GMLReal) runner->currentRoom->width); - case BUILTIN_VAR_ROOM_HEIGHT: - return RValue_makeReal((GMLReal) runner->currentRoom->height); - case BUILTIN_VAR_ROOM_PERSISTENT: - return RValue_makeBool(runner->currentRoom->persistent); - - // View properties - case BUILTIN_VAR_VIEW_CURRENT: - return RValue_makeReal((GMLReal) runner->viewCurrent); - case BUILTIN_VAR_VIEW_XVIEW: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewX); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_YVIEW: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewY); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_WVIEW: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewWidth); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_HVIEW: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewHeight); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_XPORT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].portX); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_YPORT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].portY); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_WPORT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].portWidth); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_HPORT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].portHeight); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_VISIBLE: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeBool(runner->views[arrayIndex].enabled); - return RValue_makeBool(false); - case BUILTIN_VAR_VIEW_ANGLE: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewAngle); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_HBORDER: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].borderX); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_VBORDER: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].borderY); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_OBJECT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].objectId); - return RValue_makeReal(INSTANCE_NOONE); - case BUILTIN_VAR_VIEW_HSPEED: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].speedX); - return RValue_makeReal(0.0); - case BUILTIN_VAR_VIEW_VSPEED: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].speedY); - return RValue_makeReal(0.0); - - // Background properties - case BUILTIN_VAR_BACKGROUND_VISIBLE: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeBool(runner->backgrounds[arrayIndex].visible); - return RValue_makeBool(false); - case BUILTIN_VAR_BACKGROUND_INDEX: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].backgroundIndex); - return RValue_makeReal(-1.0); - case BUILTIN_VAR_BACKGROUND_X: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].x); - return RValue_makeReal(0.0); - case BUILTIN_VAR_BACKGROUND_Y: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].y); - return RValue_makeReal(0.0); - case BUILTIN_VAR_BACKGROUND_HSPEED: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].speedX); - return RValue_makeReal(0.0); - case BUILTIN_VAR_BACKGROUND_VSPEED: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].speedY); - return RValue_makeReal(0.0); - case BUILTIN_VAR_BACKGROUND_WIDTH: { - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) { - int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, runner->backgrounds[arrayIndex].backgroundIndex); - if (tpagIndex >= 0) return RValue_makeReal((GMLReal) runner->dataWin->tpag.items[tpagIndex].boundingWidth); - } - return RValue_makeReal(0.0); - } - case BUILTIN_VAR_BACKGROUND_HEIGHT: { - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) { - int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, runner->backgrounds[arrayIndex].backgroundIndex); - if (tpagIndex >= 0) return RValue_makeReal((GMLReal) runner->dataWin->tpag.items[tpagIndex].boundingHeight); - } - return RValue_makeReal(0.0); - } - case BUILTIN_VAR_BACKGROUND_ALPHA: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].alpha); - return RValue_makeReal(1.0); - case BUILTIN_VAR_BACKGROUND_COLOR: - case BUILTIN_VAR_BACKGROUND_COLOUR: - return RValue_makeReal((GMLReal) runner->backgroundColor); - - // Timing - case BUILTIN_VAR_CURRENT_TIME: { - #ifdef _WIN32 - LARGE_INTEGER freq, counter; - QueryPerformanceFrequency(&freq); - QueryPerformanceCounter(&counter); - GMLReal ms = (GMLReal) counter.QuadPart / (GMLReal) freq.QuadPart * 1000.0; - #else - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - GMLReal ms = (GMLReal) ts.tv_sec * 1000.0 + (GMLReal) ts.tv_nsec / 1000000.0; - #endif - return RValue_makeReal(ms); - } - - // Arguments - case BUILTIN_VAR_ARGUMENT_COUNT: - return RValue_makeReal((GMLReal) ctx->scriptArgCount); - case BUILTIN_VAR_ARGUMENT: { - if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > arrayIndex && arrayIndex >= 0) { - RValue val = ctx->scriptArgs[arrayIndex]; - val.ownsReference = false; - return val; - } - return RValue_makeUndefined(); - } - case BUILTIN_VAR_ARGUMENT0 ... BUILTIN_VAR_ARGUMENT15: { - int argNumber = builtinVarId - BUILTIN_VAR_ARGUMENT0; - if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > argNumber) { - RValue val = ctx->scriptArgs[argNumber]; - val.ownsReference = false; - return val; - } - return RValue_makeUndefined(); - } - - // Keyboard - case BUILTIN_VAR_KEYBOARD_KEY: - return RValue_makeReal((GMLReal) runner->keyboard->lastKey); - case BUILTIN_VAR_KEYBOARD_LASTCHAR: - return RValue_makeString(runner->keyboard->lastChar); - case BUILTIN_VAR_KEYBOARD_LASTKEY: - return RValue_makeReal((GMLReal) runner->keyboard->lastKey); - - // Surfaces - case BUILTIN_VAR_APPLICATION_SURFACE: - return RValue_makeReal(-1.0); // sentinel ID for the application surface - - // Constants that GMS defines - case BUILTIN_VAR_TRUE: - return RValue_makeBool(true); - case BUILTIN_VAR_FALSE: - return RValue_makeBool(false); - case BUILTIN_VAR_PI: - return RValue_makeReal(3.14159265358979323846); - case BUILTIN_VAR_UNDEFINED: - return RValue_makeUndefined(); - - // Path action constants - case BUILTIN_VAR_PATH_ACTION_STOP: - return RValue_makeReal(0.0); - case BUILTIN_VAR_PATH_ACTION_RESTART: - return RValue_makeReal(1.0); - case BUILTIN_VAR_PATH_ACTION_CONTINUE: - return RValue_makeReal(2.0); - case BUILTIN_VAR_PATH_ACTION_REVERSE: - return RValue_makeReal(3.0); - - // Buffer type constants - case BUILTIN_VAR_BUFFER_FIXED: - return RValue_makeReal(GML_BUFFER_FIXED); - case BUILTIN_VAR_BUFFER_GROW: - return RValue_makeReal(GML_BUFFER_GROW); - case BUILTIN_VAR_BUFFER_WRAP: - return RValue_makeReal(GML_BUFFER_WRAP); - case BUILTIN_VAR_BUFFER_FAST: - return RValue_makeReal(GML_BUFFER_FAST); - - // Buffer data type constants - case BUILTIN_VAR_BUFFER_U8: - return RValue_makeReal(GML_BUFTYPE_U8); - case BUILTIN_VAR_BUFFER_S8: - return RValue_makeReal(GML_BUFTYPE_S8); - case BUILTIN_VAR_BUFFER_U16: - return RValue_makeReal(GML_BUFTYPE_U16); - case BUILTIN_VAR_BUFFER_S16: - return RValue_makeReal(GML_BUFTYPE_S16); - case BUILTIN_VAR_BUFFER_U32: - return RValue_makeReal(GML_BUFTYPE_U32); - case BUILTIN_VAR_BUFFER_S32: - return RValue_makeReal(GML_BUFTYPE_S32); - case BUILTIN_VAR_BUFFER_F16: - return RValue_makeReal(GML_BUFTYPE_F16); - case BUILTIN_VAR_BUFFER_F32: - return RValue_makeReal(GML_BUFTYPE_F32); - case BUILTIN_VAR_BUFFER_F64: - return RValue_makeReal(GML_BUFTYPE_F64); - case BUILTIN_VAR_BUFFER_BOOL: - return RValue_makeReal(GML_BUFTYPE_BOOL); - case BUILTIN_VAR_BUFFER_STRING: - return RValue_makeReal(GML_BUFTYPE_STRING); - case BUILTIN_VAR_BUFFER_U64: - return RValue_makeReal(GML_BUFTYPE_U64); - case BUILTIN_VAR_BUFFER_TEXT: - return RValue_makeReal(GML_BUFTYPE_TEXT); - - // Buffer seek mode constants - case BUILTIN_VAR_BUFFER_SEEK_START: - return RValue_makeReal(GML_BUFFER_SEEK_START); - case BUILTIN_VAR_BUFFER_SEEK_RELATIVE: - return RValue_makeReal(GML_BUFFER_SEEK_RELATIVE); - case BUILTIN_VAR_BUFFER_SEEK_END: - return RValue_makeReal(GML_BUFFER_SEEK_END); - - // Gamepad constants - case BUILTIN_VAR_GP_FACE1: - return RValue_makeReal(GP_FACE1); - case BUILTIN_VAR_GP_FACE2: - return RValue_makeReal(GP_FACE2); - case BUILTIN_VAR_GP_FACE3: - return RValue_makeReal(GP_FACE3); - case BUILTIN_VAR_GP_FACE4: - return RValue_makeReal(GP_FACE4); - case BUILTIN_VAR_GP_SHOULDERL: - return RValue_makeReal(GP_SHOULDERL); - case BUILTIN_VAR_GP_SHOULDERR: - return RValue_makeReal(GP_SHOULDERR); - case BUILTIN_VAR_GP_SHOULDERLB: - return RValue_makeReal(GP_SHOULDERLB); - case BUILTIN_VAR_GP_SHOULDERRB: - return RValue_makeReal(GP_SHOULDERRB); - case BUILTIN_VAR_GP_SELECT: - return RValue_makeReal(GP_SELECT); - case BUILTIN_VAR_GP_START: - return RValue_makeReal(GP_START); - case BUILTIN_VAR_GP_STICKL: - return RValue_makeReal(GP_STICKL); - case BUILTIN_VAR_GP_STICKR: - return RValue_makeReal(GP_STICKR); - case BUILTIN_VAR_GP_PADU: - return RValue_makeReal(GP_PADU); - case BUILTIN_VAR_GP_PADD: - return RValue_makeReal(GP_PADD); - case BUILTIN_VAR_GP_PADL: - return RValue_makeReal(GP_PADL); - case BUILTIN_VAR_GP_PADR: - return RValue_makeReal(GP_PADR); - case BUILTIN_VAR_GP_HOME: - return RValue_makeReal(GP_HOME); - case BUILTIN_VAR_GP_AXIS_LH: - return RValue_makeReal(GP_AXIS_LH); - case BUILTIN_VAR_GP_AXIS_LV: - return RValue_makeReal(GP_AXIS_LV); - case BUILTIN_VAR_GP_AXIS_RH: - return RValue_makeReal(GP_AXIS_RH); - case BUILTIN_VAR_GP_AXIS_RV: - return RValue_makeReal(GP_AXIS_RV); - - case BUILTIN_VAR_FPS: - return RValue_makeReal(ctx->dataWin->gen8.gms2FPS); - case BUILTIN_VAR_DEBUG_MODE: - return RValue_makeBool(false); - - default: - break; - } - - fprintf(stderr, "VM: [%s] Unhandled built-in variable read '%s' (arrayIndex=%d)\n", ctx->currentCodeName, name, arrayIndex); - return RValue_makeReal(0.0); -} - -void VMBuiltins_setVariable(VMContext* ctx, int16_t builtinVarId, const char* name, RValue val, int32_t arrayIndex) { - Instance* inst = (Instance*) ctx->currentInstance; - Runner* runner = (Runner*) requireNotNullMessage(ctx->runner, "VM: setVariable called but no runner!"); - requireNotNull(runner); - - switch (builtinVarId) { - // Per-instance properties - case BUILTIN_VAR_IMAGE_SPEED: - if (inst == nullptr) break; - inst->imageSpeed = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_IMAGE_INDEX: { - if (inst == nullptr) break; - inst->imageIndex = (float) RValue_toReal(val); - return; - } - case BUILTIN_VAR_IMAGE_XSCALE: { - if (inst == nullptr) break; - float value = (float) RValue_toReal(val); - bool changed = value != inst->imageXscale; - if (changed) { - inst->imageXscale = value; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - } - return; - } - case BUILTIN_VAR_IMAGE_YSCALE: { - if (inst == nullptr) break; - float value = (float) RValue_toReal(val); - bool changed = value != inst->imageYscale; - if (changed) { - inst->imageYscale = value; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - } - return; - } - case BUILTIN_VAR_IMAGE_ANGLE: { - if (inst == nullptr) break; - float value = (float) RValue_toReal(val); - bool changed = value != inst->imageAngle; - if (changed) { - inst->imageAngle = value; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - } - return; - } - case BUILTIN_VAR_IMAGE_ALPHA: - if (inst == nullptr) break; - inst->imageAlpha = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_IMAGE_BLEND: - if (inst == nullptr) break; - inst->imageBlend = (uint32_t) RValue_toReal(val); - return; - case BUILTIN_VAR_SPRITE_INDEX: { - if (inst == nullptr) break; - int32_t value = RValue_toInt32(val); - bool changed = value != inst->spriteIndex; - if (changed) { - inst->spriteIndex = value; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - } - return; - } - case BUILTIN_VAR_VISIBLE: - if (inst == nullptr) break; - inst->visible = RValue_toBool(val); - return; - case BUILTIN_VAR_DEPTH: { - if (inst == nullptr) break; - int32_t newDepth = RValue_toInt32(val); - if (newDepth != inst->depth) { - inst->depth = newDepth; - ((Runner*) ctx->runner)->drawableListSortDirty = true; - } - return; - } - case BUILTIN_VAR_LAYER: { - if (inst == nullptr) break; - int32_t layerId = resolveLayerIdArg(runner, val); - RuntimeLayer* rl = Runner_findRuntimeLayerById(runner, layerId); - if (rl != nullptr) { - inst->layer = layerId; - if (inst->depth != rl->depth) { - inst->depth = rl->depth; - runner->drawableListSortDirty = true; - } - } - return; - } - case BUILTIN_VAR_X: { - if (inst == nullptr) break; - float value = (float) RValue_toReal(val); - bool changed = value != inst->x; - if (changed) { - inst->x = (float) RValue_toReal(val); - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - } - return; - } - case BUILTIN_VAR_Y: { - if (inst == nullptr) break; - float value = (float) RValue_toReal(val); - bool changed = value != inst->y; - if (changed) { - inst->y = (float) RValue_toReal(val); - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - } - return; - } - case BUILTIN_VAR_PERSISTENT: - if (inst == nullptr) break; - inst->persistent = RValue_toBool(val); - return; - case BUILTIN_VAR_SOLID: - if (inst == nullptr) break; - inst->solid = RValue_toBool(val); - return; - case BUILTIN_VAR_XPREVIOUS: - if (inst == nullptr) break; - inst->xprevious = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_YPREVIOUS: - if (inst == nullptr) break; - inst->yprevious = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_XSTART: - if (inst == nullptr) break; - inst->xstart = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_YSTART: - if (inst == nullptr) break; - inst->ystart = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_MASK_INDEX: { - if (inst == nullptr) break; - int32_t value = RValue_toInt32(val); - bool changed = value != inst->maskIndex; - if (changed) { - inst->maskIndex = value; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - } - return; - } - case BUILTIN_VAR_SPEED: - if (inst == nullptr) break; - inst->speed = (float) RValue_toReal(val); - Instance_computeComponentsFromSpeed(inst); - return; - case BUILTIN_VAR_DIRECTION: { - if (inst == nullptr) break; - GMLReal d = GMLReal_fmod(RValue_toReal(val), 360.0); - if (d < 0.0) d += 360.0; - inst->direction = (float) d; - Instance_computeComponentsFromSpeed(inst); - return; - } - case BUILTIN_VAR_HSPEED: - if (inst == nullptr) break; - inst->hspeed = (float) RValue_toReal(val); - Instance_computeSpeedFromComponents(inst); - return; - case BUILTIN_VAR_VSPEED: - if (inst == nullptr) break; - inst->vspeed = (float) RValue_toReal(val); - Instance_computeSpeedFromComponents(inst); - return; - case BUILTIN_VAR_FRICTION: - if (inst == nullptr) break; - inst->friction = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_GRAVITY: - if (inst == nullptr) break; - inst->gravity = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_GRAVITY_DIRECTION: - if (inst == nullptr) break; - inst->gravityDirection = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_ALARM: { - if (inst == nullptr) break; - if (isValidAlarmIndex(arrayIndex)) { - int32_t newValue = RValue_toInt32(val); - -#ifdef ENABLE_VM_TRACING - if (shgeti(ctx->alarmsToBeTraced, "*") != -1 || shgeti(ctx->alarmsToBeTraced, runner->dataWin->objt.objects[inst->objectIndex].name) != -1) { - fprintf(stderr, "VM: [%s] Setting Alarm[%d] = %d (instanceId=%d)\n", runner->dataWin->objt.objects[inst->objectIndex].name, arrayIndex, newValue, inst->instanceId); - } -#endif - - inst->alarm[arrayIndex] = newValue; - if (newValue > 0) inst->activeAlarmMask |= (uint16_t) (1u << arrayIndex); - else inst->activeAlarmMask &= (uint16_t) ~(1u << arrayIndex); - } - return; - } - - // Path instance variables (writable) - case BUILTIN_VAR_PATH_POSITION: { - if (inst == nullptr) break; - // Native GMS runner clamps path_position to [0.0, 1.0] on set - float pos = (float) RValue_toReal(val); - if (pos < 0.0f) pos = 0.0f; - else if (pos > 1.0f) pos = 1.0f; - inst->pathPosition = pos; - return; - } - case BUILTIN_VAR_PATH_SPEED: - if (inst == nullptr) break; - inst->pathSpeed = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_PATH_SCALE: - if (inst == nullptr) break; - inst->pathScale = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_PATH_ORIENTATION: - if (inst == nullptr) break; - inst->pathOrientation = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_PATH_ENDACTION: - if (inst == nullptr) break; - inst->pathEndAction = RValue_toInt32(val); - return; - - // Keyboard variables - case BUILTIN_VAR_KEYBOARD_KEY: - runner->keyboard->lastKey = RValue_toInt32(val); - return; - case BUILTIN_VAR_KEYBOARD_LASTCHAR: - runner->keyboard->lastChar[0] = val.string[0]; - return; - case BUILTIN_VAR_KEYBOARD_LASTKEY: - runner->keyboard->lastKey = RValue_toInt32(val); - return; - - // View properties - case BUILTIN_VAR_VIEW_XVIEW: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewX = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_YVIEW: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewY = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_WVIEW: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewWidth = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_HVIEW: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewHeight = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_XPORT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].portX = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_YPORT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].portY = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_WPORT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].portWidth = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_HPORT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].portHeight = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_VISIBLE: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].enabled = RValue_toBool(val); - return; - case BUILTIN_VAR_VIEW_ANGLE: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewAngle = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_VIEW_HBORDER: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].borderX = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_VBORDER: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].borderY = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_OBJECT: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].objectId = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_HSPEED: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].speedX = RValue_toInt32(val); - return; - case BUILTIN_VAR_VIEW_VSPEED: - if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].speedY = RValue_toInt32(val); - return; - - // Background properties - case BUILTIN_VAR_BACKGROUND_VISIBLE: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].visible = RValue_toBool(val); - return; - case BUILTIN_VAR_BACKGROUND_INDEX: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].backgroundIndex = RValue_toInt32(val); - return; - case BUILTIN_VAR_BACKGROUND_X: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].x = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_BACKGROUND_Y: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].y = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_BACKGROUND_HSPEED: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].speedX = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_BACKGROUND_VSPEED: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].speedY = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_BACKGROUND_ALPHA: - if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].alpha = (float) RValue_toReal(val); - return; - case BUILTIN_VAR_BACKGROUND_COLOR: - case BUILTIN_VAR_BACKGROUND_COLOUR: - runner->backgroundColor = (uint32_t) RValue_toInt32(val); - return; - - // Room properties - case BUILTIN_VAR_ROOM: - runner->pendingRoom = RValue_toInt32(val); - return; - case BUILTIN_VAR_ROOM_PERSISTENT: - runner->currentRoom->persistent = RValue_toBool(val); - return; - case BUILTIN_VAR_ROOM_WIDTH: - runner->currentRoom->width = (uint32_t) RValue_toInt32(val); - return; - case BUILTIN_VAR_ROOM_HEIGHT: - runner->currentRoom->height = (uint32_t) RValue_toInt32(val); - return; - case BUILTIN_VAR_ROOM_SPEED: - runner->currentRoom->speed = (uint32_t) RValue_toInt32(val); - return; - - // Read-only variables (silently ignore with warning) - case BUILTIN_VAR_OS_TYPE ... BUILTIN_VAR_OS_LLVM_WINPHONE: - case BUILTIN_VAR_BUFFER_FIXED ... BUILTIN_VAR_BUFFER_SEEK_END: - case BUILTIN_VAR_ID: - case BUILTIN_VAR_OBJECT_INDEX: - case BUILTIN_VAR_CURRENT_TIME: - case BUILTIN_VAR_VIEW_CURRENT: - case BUILTIN_VAR_PATH_INDEX: - case BUILTIN_VAR_DEBUG_MODE: - case BUILTIN_VAR_ROOM_FIRST: - case BUILTIN_VAR_GP_FACE1 ... BUILTIN_VAR_GP_AXIS_RV: - fprintf(stderr, "VM: Warning - attempted write to read-only built-in '%s'\n", name); - return; - - // argument[N] - array-style write to script arguments - case BUILTIN_VAR_ARGUMENT: - if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > arrayIndex && arrayIndex >= 0) { - RValue_free(&ctx->scriptArgs[arrayIndex]); - ctx->scriptArgs[arrayIndex] = val; - } - return; - - // Argument variables (argument0..argument15) - case BUILTIN_VAR_ARGUMENT0 ... BUILTIN_VAR_ARGUMENT15: { - int argNumber = builtinVarId - BUILTIN_VAR_ARGUMENT0; - if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > argNumber) { - RValue_free(&ctx->scriptArgs[argNumber]); - ctx->scriptArgs[argNumber] = val; - } - return; - } - - default: - break; - } - - fprintf(stderr, "VM: [%s] Unhandled built-in variable write '%s' (arrayIndex=%d)\n", ctx->currentCodeName, name, arrayIndex); -} - -// ===[ BUILTIN FUNCTION IMPLEMENTATIONS ]=== - -static RValue builtinShowDebugMessage(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) { - fprintf(stderr, "[show_debug_message] Expected at least 1 argument\n"); - return RValue_makeUndefined(); - } - - char* val = RValue_toString(args[0]); - printf("Game: %s\n", val); - free(val); - - return RValue_makeUndefined(); -} - -static RValue builtinStringLength(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeInt32(0); - // GML converts non-string arguments to string before measuring length - RValue value = args[0]; - // Fast path: If the RValue is already a string, just return its length instead of creating a copy - if (value.type == RVALUE_STRING) { - if (value.string == nullptr) - return RValue_makeInt32(0); - int32_t byteLen = (int32_t) strlen(value.string); - return RValue_makeInt32(TextUtils_utf8CodepointCount(value.string, byteLen)); - } - char* str = RValue_toString(value); - int32_t byteLen = (int32_t) strlen(str); - int32_t len = TextUtils_utf8CodepointCount(str, byteLen); - free(str); - return RValue_makeInt32(len); -} - -static RValue builtinStringByteLength(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeInt32(0); - // GML converts non-string arguments to string before measuring length - RValue value = args[0]; - // Fast path: If the RValue is already a string, just return its length instead of creating a copy - if (value.type == RVALUE_STRING) { - if (value.string == nullptr) - return RValue_makeInt32(0); - int32_t byteLen = (int32_t) strlen(value.string); - return RValue_makeInt32(byteLen); - } - char* str = RValue_toString(value); - int32_t byteLen = (int32_t) strlen(str); - free(str); - return RValue_makeInt32(byteLen); -} - -static RValue builtinReal(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(RValue_toReal(args[0])); -} - -static RValue builtinString(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* result = RValue_toString(args[0]); - return RValue_makeOwnedString(result); -} - -static RValue builtinFloor(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_floor(RValue_toReal(args[0]))); -} - -static RValue builtinCeil(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_ceil(RValue_toReal(args[0]))); -} - -static RValue builtinRound(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - // GameMaker's round() uses banker's rounding (round half to even), matching llrint() under the default IEEE 754 rounding mode. - // C's round()/roundf() rounds half away from zero, which produces different results for x.5 values (e.g. round(2.5) is 2 in GML but 3 with round()). - GMLReal v = RValue_toReal(args[0]); -#ifdef USE_FLOAT_REALS - return RValue_makeReal(rintf(v)); -#else - return RValue_makeReal(rint(v)); -#endif -} - -static RValue builtinAbs(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_fabs(RValue_toReal(args[0]))); -} - -static RValue builtinSign(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - GMLReal val = RValue_toReal(args[0]); - GMLReal result = (val > 0.0) ? 1.0 : ((0.0 > val) ? -1.0 : 0.0); - return RValue_makeReal(result); -} - -static RValue builtinMax(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - GMLReal result = -INFINITY; - repeat(argCount, i) { - GMLReal val = RValue_toReal(args[i]); - if (val > result) result = val; - } - return RValue_makeReal(result); -} - -static RValue builtinMin(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - GMLReal result = INFINITY; - repeat(argCount, i) { - GMLReal val = RValue_toReal(args[i]); - if (result > val) result = val; - } - return RValue_makeReal(result); -} - -static RValue builtinPower(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_pow(RValue_toReal(args[0]), RValue_toReal(args[1]))); -} - -static RValue builtinSqrt(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_sqrt(RValue_toReal(args[0]))); -} - -static RValue builtinSqr(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - GMLReal val = RValue_toReal(args[0]); - return RValue_makeReal(val * val); -} - -static RValue builtinIsString(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - return RValue_makeBool(args[0].type == RVALUE_STRING); -} - -static RValue builtinIsReal(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - bool result = args[0].type == RVALUE_REAL || args[0].type == RVALUE_INT32 || args[0].type == RVALUE_INT64 || args[0].type == RVALUE_BOOL; - return RValue_makeBool(result); -} - -static RValue builtinIsUndefined(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(true); - return RValue_makeBool(args[0].type == RVALUE_UNDEFINED); -} - -// ===[ STRING FUNCTIONS ]=== - -static RValue builtinStringUpper(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* result = RValue_toString(args[0]); - for (char* p = result; *p; p++) *p = (char) toupper((unsigned char) *p); - return RValue_makeOwnedString(result); -} - -static RValue builtinStringLower(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* result = RValue_toString(args[0]); - for (char* p = result; *p; p++) *p = (char) tolower((unsigned char) *p); - return RValue_makeOwnedString(result); -} - -static RValue builtinStringCopy(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); - int32_t len = RValue_toInt32(args[2]); - if (0 >= len) { - return RValue_makeOwnedString(safeStrdup("")); - } - - char* str = RValue_toString(args[0]); - int32_t pos = RValue_toInt32(args[1]) - 1; // GMS is 1-based - int32_t strLen = (int32_t) strlen(str); - - if (0 > pos) pos = 0; - - int32_t byteStart = TextUtils_utf8AdvanceCodepoints(str, strLen, pos); - if (byteStart >= strLen) { - free(str); - return RValue_makeOwnedString(safeStrdup("")); - } - - int32_t byteEnd = byteStart + TextUtils_utf8AdvanceCodepoints(str + byteStart, strLen - byteStart, len); - if (byteEnd > strLen) byteEnd = strLen; - - int32_t nbytes = byteEnd - byteStart; - char* result = safeMalloc(nbytes + 1); - memcpy(result, str + byteStart, (size_t) nbytes); - result[nbytes] = '\0'; - - free(str); - - return RValue_makeOwnedString(result); -} - -static RValue builtinStringFormat(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); - if (args[0].type == RVALUE_UNDEFINED) return RValue_makeOwnedString(safeStrdup("undefined")); - - GMLReal val = RValue_toReal(args[0]); - int32_t tot = RValue_toInt32(args[1]); - int32_t dec = RValue_toInt32(args[2]); - if (0 > dec) dec = 0; - if (15 < dec) dec = 15; - - char numBuf[64]; - snprintf(numBuf, sizeof(numBuf), "%.*f", (int) dec, (double) val); - - const char* dot = strchr(numBuf, '.'); - int32_t intLen = (int32_t) (dot ? (dot - numBuf) : (int32_t) strlen(numBuf)); - - int32_t leftPad = (tot > intLen) ? (tot - intLen) : 0; - int32_t numLen = (int32_t) strlen(numBuf); - int32_t totalLen = leftPad + numLen; - - char* result = safeMalloc(totalLen + 1); - for (int32_t i = 0; leftPad > i; i++) result[i] = ' '; - memcpy(result + leftPad, numBuf, (size_t) numLen); - result[totalLen] = '\0'; - return RValue_makeOwnedString(result); -} - -static RValue builtinStringRepeat(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* str = RValue_toString(args[0]); - int32_t count = RValue_toInt32(args[1]); - if (0 >= count || str[0] == '\0') { - free(str); - return RValue_makeOwnedString(safeStrdup("")); - } - - size_t strLen = strlen(str); - size_t totalLen = strLen * (size_t) count; - char* result = safeMalloc(totalLen + 1); - repeat(count, i) { - memcpy(result + i * strLen, str, strLen); - } - result[totalLen] = '\0'; - free(str); - return RValue_makeOwnedString(result); -} - -static RValue builtinStringCount(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeInt32(0); - char* substr = RValue_toString(args[0]); - char* str = RValue_toString(args[1]); - size_t strLen = strlen(str); - size_t substrLen = strlen(substr); - int32_t count = 0; - - if (substrLen > strLen) { - free(substr); - free(str); - return RValue_makeInt32(0); - } - - repeat(strLen, i) { - if (strncmp(str + i, substr, substrLen) == 0) - count++; - } - - free(substr); - free(str); - return RValue_makeInt32(count); -} - -static RValue builtinStringDigits(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* str = RValue_toString(args[0]); - int len = strlen(str); - char* result = (char*)malloc(len + 1); - if (result == NULL) return RValue_makeOwnedString(safeStrdup("")); - - int digitCount = 0; - for (int i = 0; str[i] != '\0'; i++) { - if (isdigit(str[i])) result[digitCount++] = str[i]; - } - - free(str); - result[digitCount] = '\0'; - - if (digitCount == 0) { - free(result); - return RValue_makeOwnedString(safeStrdup("")); - } - - char* exact_result = (char*)realloc(result, digitCount + 1); - return RValue_makeOwnedString(exact_result ? exact_result : result); -} - -static RValue builtinOrd(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount || args[0].type != RVALUE_STRING || args[0].string == nullptr || args[0].string[0] == '\0') { - return RValue_makeReal(0.0); - } - const char* str = args[0].string; - int32_t pos = 0; - uint16_t cp = TextUtils_decodeUtf8(str, (int32_t)strlen(str), &pos); - return RValue_makeReal((GMLReal) cp); -} - -static RValue builtinChr(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); - uint32_t cp = (uint32_t) RValue_toInt32(args[0]); - char buf[5]; - int32_t n = TextUtils_utf8EncodeCodepoint(cp, buf); - if (0 >= n) return RValue_makeOwnedString(safeStrdup("")); - buf[n] = '\0'; - return RValue_makeOwnedString(safeStrdup(buf)); -} - -static RValue builtinStringPos(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - char* needle = RValue_toString(args[0]); - char* haystack = RValue_toString(args[1]); - char* found = strstr(haystack, needle); - if (found == nullptr) { - free(haystack); - free(needle); - return RValue_makeReal(0.0); - } - int32_t byteIndex = (int32_t) (found - haystack); - int32_t charIndex = TextUtils_utf8CodepointCount(haystack, byteIndex) + 1; // 1-based codepoint index - free(haystack); - free(needle); - return RValue_makeReal((GMLReal) charIndex); -} - -static RValue builtinStringCharAt(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* str = RValue_toString(args[0]); - int32_t pos = RValue_toInt32(args[1]) - 1; // 1-based - int32_t strLen = (int32_t) strlen(str); - if (0 > pos || pos >= strLen) { - free(str); - return RValue_makeOwnedString(safeStrdup("")); - } - int32_t byteStart = TextUtils_utf8AdvanceCodepoints(str, strLen, pos); - if (byteStart >= strLen) { - free(str); - return RValue_makeOwnedString(safeStrdup("")); - } - int32_t byteNext = byteStart; - TextUtils_decodeUtf8(str, strLen, &byteNext); - int32_t nbytes = byteNext - byteStart; - char* out = safeMalloc(nbytes + 1); - memcpy(out, str + byteStart, (size_t) nbytes); - out[nbytes] = '\0'; - free(str); - return RValue_makeOwnedString(out); -} - -static RValue builtinStringDelete(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* str = RValue_toString(args[0]); - int32_t pos = RValue_toInt32(args[1]) - 1; // 1-based - int32_t count = RValue_toInt32(args[2]); - int32_t strLen = (int32_t) strlen(str); - - if (0 > pos || pos >= strLen || 0 >= count) return RValue_makeOwnedString(str); - - int32_t byteStart = TextUtils_utf8AdvanceCodepoints(str, strLen, pos); - if (byteStart >= strLen) return RValue_makeOwnedString(str); - - int32_t byteEnd = byteStart + TextUtils_utf8AdvanceCodepoints(str + byteStart, strLen - byteStart, count); - if (byteEnd > strLen) byteEnd = strLen; - - int32_t removeLen = byteEnd - byteStart; - char* result = safeMalloc(strLen - removeLen + 1); - memcpy(result, str, (size_t) byteStart); - memcpy(result + byteStart, str + byteEnd, (size_t) (strLen - byteEnd)); - result[strLen - removeLen] = '\0'; - - free(str); - - return RValue_makeOwnedString(result); -} - -static RValue builtinStringInsert(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* substr = RValue_toString(args[0]); - char* str = RValue_toString(args[1]); - int32_t pos = RValue_toInt32(args[2]) - 1; // 1-based - int32_t strLen = (int32_t) strlen(str); - int32_t subLen = (int32_t) strlen(substr); - - if (0 > pos) pos = 0; - int32_t bytePos = TextUtils_utf8AdvanceCodepoints(str, strLen, pos); - if (bytePos > strLen) bytePos = strLen; - - char* result = safeMalloc(strLen + subLen + 1); - memcpy(result, str, (size_t) bytePos); - memcpy(result + bytePos, substr, (size_t) subLen); - memcpy(result + bytePos + subLen, str + bytePos, (size_t) (strLen - bytePos)); - result[strLen + subLen] = '\0'; - - free(substr); - free(str); - - return RValue_makeOwnedString(result); -} - -static RValue builtinStringReplace(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* str = RValue_toString(args[0]); - char* needle = RValue_toString(args[1]); - int32_t strLen = (int32_t) strlen(str); - int32_t needleLen = (int32_t) strlen(needle); - if (0 == needleLen) { - free(needle); - return RValue_makeOwnedString(str); - } - - char* replacement = RValue_toString(args[2]); - int32_t replacementLen = (int32_t) strlen(replacement); - - // There can be only ONE. - char *appearance = strstr(str, needle); - if (!appearance) { - free(needle); - free(replacement); - return RValue_makeOwnedString(str); - } - - int32_t newLen = strLen - needleLen + replacementLen; - int32_t before = (int32_t) (appearance - str); - char *outputString = safeMalloc(newLen + 1); - - strncpy(outputString, str, before); - strncpy(outputString + before, replacement, replacementLen); - strcpy(outputString + before + replacementLen, appearance + needleLen); - - free(str); - free(needle); - free(replacement); - - return RValue_makeOwnedString(outputString); -} - -static RValue builtinStringReplaceAll(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); - char* str = RValue_toString(args[0]); - char* needle = RValue_toString(args[1]); - int32_t needleLen = (int32_t) strlen(needle); - if (0 == needleLen) { - free(needle); - return RValue_makeOwnedString(str); - } - - char* replacement = RValue_toString(args[2]); - int32_t replacementLen = (int32_t) strlen(replacement); - - // Count occurrences to pre-allocate - int32_t count = 0; - const char* p = str; - while ((p = strstr(p, needle)) != nullptr) { count++; p += needleLen; } - - int32_t strLen = (int32_t) strlen(str); - int32_t resultLen = strLen + count * (replacementLen - needleLen); - char* result = safeMalloc(resultLen + 1); - char* out = result; - p = str; - const char* match; - while ((match = strstr(p, needle)) != nullptr) { - int32_t before = (int32_t) (match - p); - memcpy(out, p, before); - out += before; - memcpy(out, replacement, replacementLen); - out += replacementLen; - p = match + needleLen; - } - strcpy(out, p); - - free(replacement); - free(needle); - free(str); - - return RValue_makeOwnedString(result); -} - -// ===[ MATH FUNCTIONS ]=== - -static RValue builtinDarctan2(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - GMLReal y = RValue_toReal(args[0]); - GMLReal x = RValue_toReal(args[1]); - return RValue_makeReal(GMLReal_atan2(y, x) * (180.0 / M_PI)); -} - -static RValue builtinSin(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_sin(RValue_toReal(args[0]))); -} - -static RValue builtinArcsin(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_asin(RValue_toReal(args[0]))); -} - -static RValue builtinCos(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_cos(RValue_toReal(args[0]))); -} - -static RValue builtinDsin(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_sin(RValue_toReal(args[0]) * (M_PI / 180.0))); -} - -static RValue builtinDcos(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(GMLReal_cos(RValue_toReal(args[0]) * (M_PI / 180.0))); -} - -static RValue builtinDegtorad(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(RValue_toReal(args[0]) * (M_PI / 180.0)); -} - -static RValue builtinRadtodeg(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - return RValue_makeReal(RValue_toReal(args[0]) * (180.0 / M_PI)); -} - -static RValue builtinClamp(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeReal(0.0); - GMLReal val = RValue_toReal(args[0]); - GMLReal lo = RValue_toReal(args[1]); - GMLReal hi = RValue_toReal(args[2]); - if (lo > val) val = lo; - if (val > hi) val = hi; - return RValue_makeReal(val); -} - -static RValue builtinLerp(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeReal(0.0); - GMLReal a = RValue_toReal(args[0]); - GMLReal b = RValue_toReal(args[1]); - GMLReal t = RValue_toReal(args[2]); - return RValue_makeReal(a + (b - a) * t); -} - -static RValue builtinPointDistance(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (4 > argCount) return RValue_makeReal(0.0); - GMLReal dx = RValue_toReal(args[2]) - RValue_toReal(args[0]); - GMLReal dy = RValue_toReal(args[3]) - RValue_toReal(args[1]); - return RValue_makeReal(GMLReal_sqrt(dx * dx + dy * dy)); -} - -static RValue builtinPointInRectangle(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (6 > argCount) return RValue_makeBool(false); - GMLReal px = RValue_toReal(args[0]); - GMLReal py = RValue_toReal(args[1]); - GMLReal x1 = RValue_toReal(args[2]); - GMLReal y1 = RValue_toReal(args[3]); - GMLReal x2 = RValue_toReal(args[4]); - GMLReal y2 = RValue_toReal(args[5]); - return RValue_makeBool(px >= x1 && px <= x2 && py >= y1 && py <= y2); -} - -static RValue builtinDistanceToPoint(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - GMLReal px = RValue_toReal(args[0]); - GMLReal py = RValue_toReal(args[1]); - - Instance* inst = ctx->currentInstance; - int32_t sprIdx = (inst->maskIndex >= 0) ? inst->maskIndex : inst->spriteIndex; - - // Compute bounding box - GMLReal bboxLeft, bboxRight, bboxTop, bboxBottom; - if (0 > sprIdx || (uint32_t) sprIdx >= ctx->dataWin->sprt.count) { - // No sprite/mask: treat bbox as a single point at (x, y) - bboxLeft = inst->x; - bboxRight = inst->x; - bboxTop = inst->y; - bboxBottom = inst->y; - } else { - Sprite* spr = &ctx->dataWin->sprt.sprites[sprIdx]; - bboxLeft = inst->x + inst->imageXscale * (spr->marginLeft - spr->originX); - bboxRight = inst->x + inst->imageXscale * ((spr->marginRight + 1) - spr->originX); - if (bboxLeft > bboxRight) { - GMLReal t = bboxLeft; - bboxLeft = bboxRight; - bboxRight = t; - } - bboxTop = inst->y + inst->imageYscale * (spr->marginTop - spr->originY); - bboxBottom = inst->y + inst->imageYscale * ((spr->marginBottom + 1) - spr->originY); - if (bboxTop > bboxBottom) { - GMLReal t = bboxTop; - bboxTop = bboxBottom; - bboxBottom = t; - } - } - - // Distance from point to nearest edge of bbox (0 if inside) - GMLReal xd = 0.0; - GMLReal yd = 0.0; - if (px > bboxRight) xd = px - bboxRight; - if (px < bboxLeft) xd = px - bboxLeft; - if (py > bboxBottom) yd = py - bboxBottom; - if (py < bboxTop) yd = py - bboxTop; - - return RValue_makeReal(GMLReal_sqrt(xd * xd + yd * yd)); -} - -// distance_to_object(obj) -// Returns the minimum bbox-to-bbox distance between the calling instance and the nearest instance of the given object. -static RValue builtinDistanceToObject(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - - Runner* runner = (Runner*) ctx->runner; - int32_t targetObjIndex = RValue_toInt32(args[0]); - Instance* self = ctx->currentInstance; - - // Compute self bbox - Sprite* selfSpr = Collision_getSprite(ctx->dataWin, self); - if (selfSpr == nullptr) return RValue_makeReal(0.0); - InstanceBBox selfBBox = Collision_computeBBox(ctx->dataWin, self); - if (!selfBBox.valid) return RValue_makeReal(0.0); - - GMLReal minDistSq = 1e20; - - int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (!inst->active || inst == self) continue; - - InstanceBBox otherBBox = Collision_computeBBox(ctx->dataWin, inst); - if (!otherBBox.valid) continue; - - GMLReal xd = 0.0; - GMLReal yd = 0.0; - if (otherBBox.left > selfBBox.right) xd = otherBBox.left - selfBBox.right; - if (selfBBox.left > otherBBox.right) xd = selfBBox.left - otherBBox.right; - if (otherBBox.top > selfBBox.bottom) yd = otherBBox.top - selfBBox.bottom; - if (selfBBox.top > otherBBox.bottom) yd = selfBBox.top - otherBBox.bottom; - - GMLReal distSq = xd * xd + yd * yd; - if (minDistSq > distSq) minDistSq = distSq; - } - Runner_popInstanceSnapshot(runner, snapBase); - - return RValue_makeReal(GMLReal_sqrt(minDistSq)); -} - -static RValue builtinPointDirection(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (4 > argCount) return RValue_makeReal(0.0); - GMLReal dx = RValue_toReal(args[2]) - RValue_toReal(args[0]); - GMLReal dy = RValue_toReal(args[3]) - RValue_toReal(args[1]); - return RValue_makeReal(GMLReal_atan2(-dy, dx) * (180.0 / M_PI)); -} - -static RValue builtinAngleDifference(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - GMLReal src = RValue_toReal(args[0]); - GMLReal dest = RValue_toReal(args[1]); - return RValue_makeReal(GMLReal_fmod(GMLReal_fmod(src - dest, 360.0) + 540.0, 360.0) - 180.0); -} - -static RValue builtinMoveTowardsPoint(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal targetX = RValue_toReal(args[0]); - GMLReal targetY = RValue_toReal(args[1]); - GMLReal spd = RValue_toReal(args[2]); - Instance* inst = ctx->currentInstance; - GMLReal dx = targetX - inst->x; - GMLReal dy = targetY - inst->y; - GMLReal dir = GMLReal_atan2(-dy, dx) * (180.0 / M_PI); - if (dir < 0.0) dir += 360.0; - inst->direction = (float) dir; - inst->speed = (float) spd; - Instance_computeComponentsFromSpeed(inst); - return RValue_makeReal(0.0); -} - -static RValue builtinMoveSnap(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal hsnap = RValue_toReal(args[0]); - GMLReal vsnap = RValue_toReal(args[1]); - Instance* inst = ctx->currentInstance; - if (hsnap > 0.0) { - inst->x = (float) (GMLReal_floor((inst->x / hsnap) + 0.5) * hsnap); - SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); - } - if (vsnap > 0.0) { - inst->y = (float) (GMLReal_floor((inst->y / vsnap) + 0.5) * vsnap); - SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); - } - return RValue_makeReal(0.0); -} - -static RValue builtinLengthdir_x(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - GMLReal len = RValue_toReal(args[0]); - GMLReal dir = RValue_toReal(args[1]) * (M_PI / 180.0); - return RValue_makeReal(len * GMLReal_cos(dir)); -} - -static RValue builtinLengthdir_y(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - GMLReal len = RValue_toReal(args[0]); - GMLReal dir = RValue_toReal(args[1]) * (M_PI / 180.0); - return RValue_makeReal(-len * GMLReal_sin(dir)); -} - -// ===[ RANDOM FUNCTIONS ]=== - -static RValue builtinRandom(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - GMLReal n = RValue_toReal(args[0]); - return RValue_makeReal(((GMLReal) rand() / (GMLReal) RAND_MAX) * n); -} - -static RValue builtinRandomRange(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - GMLReal lo = RValue_toReal(args[0]); - GMLReal hi = RValue_toReal(args[1]); - return RValue_makeReal(lo + ((GMLReal) rand() / (GMLReal) RAND_MAX) * (hi - lo)); -} - -static RValue builtinIrandom(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - int32_t n = RValue_toInt32(args[0]); - if (0 >= n) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) (rand() % (n + 1))); -} - -static RValue builtinIrandomRange(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - int32_t lo = RValue_toInt32(args[0]); - int32_t hi = RValue_toInt32(args[1]); - if (lo > hi) { int32_t tmp = lo; lo = hi; hi = tmp; } - int32_t range = hi - lo + 1; - if (0 >= range) return RValue_makeReal((GMLReal) lo); - return RValue_makeReal((GMLReal) (lo + rand() % range)); -} - -static RValue builtinChoose(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - int32_t idx = rand() % argCount; - // Steal ownership: the caller's RValue_free of args[idx] becomes a no-op, and the returned value owns the ref instead. - RValue val = args[idx]; - if (val.type == RVALUE_STRING && val.string != nullptr && !val.ownsReference) { - return RValue_makeOwnedString(safeStrdup(val.string)); - } - args[idx].ownsReference = false; - return val; -} - -static RValue builtinRandomize(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - if (ctx->hasFixedSeed) return RValue_makeUndefined(); - srand((unsigned int) time(nullptr) + (ctx->runner->frameCount * 2654435761u)); // 2654435761u = Knuth's multiplier - return RValue_makeUndefined(); -} - -// ===[ ROOM FUNCTIONS ]=== - -static RValue builtinGameGetSpeed(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - int32_t type = RValue_toInt32(args[0]); - GMLReal fps = (GMLReal) ctx->runner->currentRoom->speed; - // gamespeed_fps = 0, gamespeed_microseconds = 1 - if (type == 0) return RValue_makeReal(fps); - return RValue_makeReal((GMLReal) 1000000.0 / fps); -} - -static RValue builtinRoomExists(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - int32_t roomId = RValue_toInt32(args[0]); - return RValue_makeBool(roomId >= 0 && (uint32_t) roomId < ctx->runner->dataWin->room.count); -} - -static RValue builtinRoomGetName(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Room* room = &ctx->dataWin->room.rooms[RValue_toInt32(args[0])]; - return RValue_makeOwnedString(safeStrdup(room->name)); -} - -static RValue builtinRoomGotoNext(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_goto_next called but no runner!"); - - int32_t nextPos = runner->currentRoomOrderPosition + 1; - if ((int32_t) runner->dataWin->gen8.roomOrderCount > nextPos) { - runner->pendingRoom = runner->dataWin->gen8.roomOrder[nextPos]; - } else { - fprintf(stderr, "VM: room_goto_next - already at last room!\n"); - } - return RValue_makeUndefined(); -} - -static RValue builtinRoomGotoPrevious(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_goto_previous called but no runner!"); - - int32_t previousPos = runner->currentRoomOrderPosition - 1; - if (previousPos >= 0) { - runner->pendingRoom = runner->dataWin->gen8.roomOrder[previousPos]; - } else { - fprintf(stderr, "VM: room_goto_previous - already at first room!\n"); - } - return RValue_makeUndefined(); -} - -static RValue builtinRoomGoto(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_goto called but no runner!"); - runner->pendingRoom = RValue_toInt32(args[0]); - return RValue_makeUndefined(); -} - -static RValue builtinRoomRestart(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_restart called but no runner!"); - runner->pendingRoom = runner->currentRoomIndex; - return RValue_makeUndefined(); -} - -static RValue builtinRoomNext(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_next called but no runner!"); - int32_t roomId = RValue_toInt32(args[0]); - DataWin* dw = runner->dataWin; - repeat(dw->gen8.roomOrderCount, i) { - if (dw->gen8.roomOrder[i] == roomId && dw->gen8.roomOrderCount > i + 1) { - return RValue_makeReal(dw->gen8.roomOrder[i + 1]); - } - } - return RValue_makeReal(-1); -} - -static RValue builtinRoomPrevious(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_previous called but no runner!"); - int32_t roomId = RValue_toInt32(args[0]); - DataWin* dw = runner->dataWin; - repeat(dw->gen8.roomOrderCount, i) { - if (dw->gen8.roomOrder[i] == roomId && i > 0) { - return RValue_makeReal(dw->gen8.roomOrder[i - 1]); - } - } - return RValue_makeReal(-1); -} - -static RValue builtinRoomSetPersistent(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - - int32_t roomId = RValue_toInt32(args[0]); - bool persistent = RValue_toBool(args[1]); - // The HTML5 room_set_persistent does do this (it checks if the room is null) - if (0 > roomId || (uint32_t) roomId >= ctx->runner->dataWin->room.count) return RValue_makeUndefined(); - ctx->runner->dataWin->room.rooms[roomId].persistent = persistent; - - return RValue_makeUndefined(); -} - -// GMS2 camera compatibility - we treat view index as camera ID -static RValue builtinViewGetCamera(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(-1); - int32_t viewIndex = RValue_toInt32(args[0]); - if (viewIndex >= 0 && MAX_VIEWS > viewIndex) { - return RValue_makeReal(viewIndex); - } - return RValue_makeReal(-1); -} - -static RValue builtinCameraGetViewX(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(-1); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_x called but no runner!"); - int32_t cameraId = RValue_toInt32(args[0]); - if (cameraId >= 0 && MAX_VIEWS > cameraId) { - return RValue_makeReal(runner->views[cameraId].viewX); - } - return RValue_makeReal(-1); -} - -static RValue builtinCameraGetViewY(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(-1); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_y called but no runner!"); - int32_t cameraId = RValue_toInt32(args[0]); - if (cameraId >= 0 && MAX_VIEWS > cameraId) { - return RValue_makeReal(runner->views[cameraId].viewY); - } - return RValue_makeReal(-1); -} - -static RValue builtinCameraGetViewWidth(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(-1); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_width called but no runner!"); - int32_t cameraId = RValue_toInt32(args[0]); - if (cameraId >= 0 && MAX_VIEWS > cameraId) { - return RValue_makeReal(runner->views[cameraId].viewWidth); - } - return RValue_makeReal(-1); -} - -static RValue builtinCameraGetViewHeight(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(-1); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_height called but no runner!"); - int32_t cameraId = RValue_toInt32(args[0]); - if (cameraId >= 0 && MAX_VIEWS > cameraId) { - return RValue_makeReal(runner->views[cameraId].viewHeight); - } - return RValue_makeReal(-1); -} - -static RValue builtinCameraSetViewPos(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(-1); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_set_view_pos called but no runner!"); - int32_t cameraId = RValue_toInt32(args[0]); - int32_t x = RValue_toInt32(args[1]); - int32_t y = RValue_toInt32(args[2]); - if (cameraId >= 0 && MAX_VIEWS > cameraId) { - runner->views[cameraId].viewX = x; - runner->views[cameraId].viewY = y; - } - return RValue_makeUndefined(); -} - -static RValue builtinCameraGetViewTarget(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(-1); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_target called but no runner!"); - int32_t cameraId = RValue_toInt32(args[0]); - if (cameraId >= 0 && MAX_VIEWS > cameraId) { - return RValue_makeReal(runner->views[cameraId].objectId); - } - return RValue_makeReal(-1); -} - -static RValue builtinCameraSetViewTarget(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_set_view_target called but no runner!"); - int32_t cameraId = RValue_toInt32(args[0]); - int32_t objectId = RValue_toInt32(args[1]); - if (cameraId >= 0 && MAX_VIEWS > cameraId) { - runner->views[cameraId].objectId = objectId; - } - return RValue_makeUndefined(); -} - -static RValue cameraGetViewBorder(VMContext* ctx, RValue* args, int32_t argCount, bool wantY) { - if (1 > argCount) return RValue_makeReal(-1); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_border called but no runner!"); - int32_t cameraId = RValue_toInt32(args[0]); - if (cameraId >= 0 && MAX_VIEWS > cameraId) { - RuntimeView v = runner->views[cameraId]; - return RValue_makeReal((wantY ? v.borderY : v.borderX)); - } - return RValue_makeReal(-1); -} - -static RValue builtinCameraGetViewBorderX(VMContext* ctx, RValue* args, int32_t argCount) { - return cameraGetViewBorder(ctx, args, argCount, false); -} - -static RValue builtinCameraGetViewBorderY(VMContext* ctx, RValue* args, int32_t argCount) { - return cameraGetViewBorder(ctx, args, argCount, true); -} - -static RValue builtinCameraSetViewBorder(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeUndefined(); - Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_set_view_border called but no runner!"); - int32_t cameraId = RValue_toInt32(args[0]); - int32_t bx = RValue_toInt32(args[1]); - int32_t by = RValue_toInt32(args[2]); - if (cameraId >= 0 && MAX_VIEWS > cameraId) { - runner->views[cameraId].borderX = (uint32_t) bx; - runner->views[cameraId].borderY = (uint32_t) by; - } - return RValue_makeUndefined(); -} - -// ===[ VARIABLE FUNCTIONS ]=== - -static RValue builtinVariableGlobalExists(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount || args[0].type != RVALUE_STRING) return RValue_makeReal(0.0); - const char* name = args[0].string; - ptrdiff_t idx = shgeti(ctx->globalVarNameMap, (char*) name); - if (0 > idx) return RValue_makeReal(0.0); - int32_t varID = ctx->globalVarNameMap[idx].value; - if (ctx->globalVarCount > (uint32_t) varID && ctx->globalVars[varID].type != RVALUE_UNDEFINED) { - return RValue_makeReal(1.0); - } - return RValue_makeReal(0.0); -} - -static RValue builtinVariableGlobalGet(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount || args[0].type != RVALUE_STRING) return RValue_makeUndefined(); - const char* name = args[0].string; - ptrdiff_t idx = shgeti(ctx->globalVarNameMap, (char*) name); - if (0 > idx) return RValue_makeUndefined(); - int32_t varID = ctx->globalVarNameMap[idx].value; - if (ctx->globalVarCount > (uint32_t) varID) { - RValue val = ctx->globalVars[varID]; - // Duplicate owned strings - if (val.type == RVALUE_STRING && val.ownsReference && val.string != nullptr) { - return RValue_makeOwnedString(safeStrdup(val.string)); - } - // Return a weak view: the global slot retains ownership. The caller's Pop will incRef into the destination slot. - val.ownsReference = false; - return val; - } - return RValue_makeUndefined(); -} - -static RValue builtinVariableGlobalSet(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount || args[0].type != RVALUE_STRING) return RValue_makeUndefined(); - const char* name = args[0].string; - ptrdiff_t idx = shgeti(ctx->globalVarNameMap, (char*) name); - if (0 > idx) return RValue_makeUndefined(); - int32_t varID = ctx->globalVarNameMap[idx].value; - if (ctx->globalVarCount > (uint32_t) varID) { - RValue_free(&ctx->globalVars[varID]); - ctx->globalVars[varID] = RValue_makeIndependent(args[1]); - } - return RValue_makeUndefined(); -} - -// ===[ VARIABLE_INSTANCE ]=== - -static void variableInstanceSetOn(VMContext* ctx, Instance* target, const char* name, RValue val) { - int16_t builtinId = VMBuiltins_resolveBuiltinVarId(name); - if (builtinId != BUILTIN_VAR_UNKNOWN) { - Instance* saved = (Instance*) ctx->currentInstance; - ctx->currentInstance = target; - VMBuiltins_setVariable(ctx, builtinId, name, val, -1); - ctx->currentInstance = saved; - return; - } - // Lookup varID by name from VARI (self scope) - ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) name); - if (0 > slot) { - fprintf(stderr, "variable_instance_set: variable '%s' not found in VARI table\n", name); - return; - } - Instance_setSelfVar(target, ctx->selfVarNameMap[slot].value, val); -} - -static RValue variableInstanceGetOn(VMContext* ctx, Instance* target, const char* name) { - int16_t builtinId = VMBuiltins_resolveBuiltinVarId(name); - if (builtinId != BUILTIN_VAR_UNKNOWN) { - Instance* saved = (Instance*) ctx->currentInstance; - ctx->currentInstance = target; - RValue val = VMBuiltins_getVariable(ctx, builtinId, name, -1); - ctx->currentInstance = saved; - // Duplicate string so caller-owned args cleanup does not affect it - if (val.type == RVALUE_STRING && val.string != nullptr && !val.ownsReference) { - return RValue_makeOwnedString(safeStrdup(val.string)); - } - return val; - } - ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) name); - if (0 > slot) return RValue_makeUndefined(); - RValue val = Instance_getSelfVar(target, ctx->selfVarNameMap[slot].value); - if (val.type == RVALUE_STRING && val.string != nullptr) { - return RValue_makeOwnedString(safeStrdup(val.string)); - } - return val; -} - -static inline bool variableScopedMatches(Instance* inst, bool structOnly) { - return inst->active && (!structOnly || inst->objectIndex == -1); -} - -static bool variableInstanceExistsOn(VMContext* ctx, Instance* target, const char* name) { - if (VMBuiltins_resolveBuiltinVarId(name) != BUILTIN_VAR_UNKNOWN) return true; - ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) name); - if (0 > slot) return false; - return IntRValueHashMap_contains(&target->selfVars, ctx->selfVarNameMap[slot].value); -} - -static RValue variableScopedGet(VMContext* ctx, int32_t id, const char* name, bool structOnly) { - Runner* runner = (Runner*) ctx->runner; - - if (id >= 100000) { - Instance* inst = hmget(runner->instancesById, id); - if (inst != nullptr && variableScopedMatches(inst, structOnly)) return variableInstanceGetOn(ctx, inst, name); - return RValue_makeUndefined(); - } - - // Object index: return value from first matching active instance. - int32_t snapBase = Runner_pushInstancesOfObject(runner, id); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - RValue result = RValue_makeUndefined(); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (variableScopedMatches(inst, structOnly)) { - result = variableInstanceGetOn(ctx, inst, name); - break; - } - } - Runner_popInstanceSnapshot(runner, snapBase); - return result; -} - -static void variableScopedSet(VMContext* ctx, int32_t id, const char* name, RValue val, bool structOnly) { - Runner* runner = (Runner*) ctx->runner; - - if (id >= 100000) { - Instance* inst = hmget(runner->instancesById, id); - if (inst != nullptr && variableScopedMatches(inst, structOnly)) variableInstanceSetOn(ctx, inst, name, val); - return; - } - - // Object index: set on all matching active instances (including descendants). The setter can run user code, so iterate a snapshot. - int32_t snapBase = Runner_pushInstancesOfObject(runner, id); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (variableScopedMatches(inst, structOnly)) variableInstanceSetOn(ctx, inst, name, val); - } - Runner_popInstanceSnapshot(runner, snapBase); -} - -static bool variableScopedExists(VMContext* ctx, int32_t id, const char* name, bool structOnly) { - Runner* runner = (Runner*) ctx->runner; - - if (id >= 100000) { - Instance* inst = hmget(runner->instancesById, id); - if (inst != nullptr && variableScopedMatches(inst, structOnly)) return variableInstanceExistsOn(ctx, inst, name); - return false; - } - - int32_t snapBase = Runner_pushInstancesOfObject(runner, id); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - bool result = false; - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (variableScopedMatches(inst, structOnly)) { - result = variableInstanceExistsOn(ctx, inst, name); - break; - } - } - Runner_popInstanceSnapshot(runner, snapBase); - return result; -} - -static RValue builtinVariableInstanceGet(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount || args[1].type != RVALUE_STRING) return RValue_makeUndefined(); - return variableScopedGet(ctx, RValue_toInt32(args[0]), args[1].string, false); -} - -static RValue builtinVariableInstanceSet(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount || args[1].type != RVALUE_STRING) return RValue_makeUndefined(); - variableScopedSet(ctx, RValue_toInt32(args[0]), args[1].string, args[2], false); - return RValue_makeUndefined(); -} - -static RValue builtinVariableInstanceExists(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount || args[1].type != RVALUE_STRING) return RValue_makeBool(false); - return RValue_makeBool(variableScopedExists(ctx, RValue_toInt32(args[0]), args[1].string, false)); -} - -static RValue builtinVariableStructGet(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount || args[1].type != RVALUE_STRING) return RValue_makeUndefined(); - return variableScopedGet(ctx, RValue_toInt32(args[0]), args[1].string, true); -} - -static RValue builtinVariableStructSet(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount || args[1].type != RVALUE_STRING) return RValue_makeUndefined(); - variableScopedSet(ctx, RValue_toInt32(args[0]), args[1].string, args[2], true); - return RValue_makeUndefined(); -} - -static RValue builtinVariableStructExists(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount || args[1].type != RVALUE_STRING) return RValue_makeBool(false); - return RValue_makeBool(variableScopedExists(ctx, RValue_toInt32(args[0]), args[1].string, true)); -} - -// ===[ METHOD ]=== - -#if IS_BC17_OR_HIGHER_ENABLED -static RValue builtinMethod(VMContext* ctx, MAYBE_UNUSED RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - - int32_t boundInstance = RValue_toInt32(args[0]); - int32_t rawArg = RValue_toInt32(args[1]); - - // In GMS2 BC17+, function references are pushed via `Push.i ` where funcIdx is an index into the FUNC chunk (patched in by patchReferenceOperands). Resolve funcIdx -> codeIndex via function name lookup (same flow as Call.i). - int32_t codeIndex = rawArg; - if (rawArg >= 0 && (uint32_t) rawArg < ctx->dataWin->func.functionCount) { - const char* funcName = ctx->dataWin->func.functions[rawArg].name; - if (funcName != nullptr) { - ptrdiff_t idx = shgeti(ctx->codeIndexByName, (char*) funcName); - if (idx >= 0) { - codeIndex = ctx->codeIndexByName[idx].value; - } - } - } - - // If binding to current self (-1), capture the actual instance ID - if (boundInstance == -1 && ctx->currentInstance != nullptr) { - boundInstance = ((Instance*) ctx->currentInstance)->instanceId; - } - - return RValue_makeMethod(codeIndex, boundInstance); -} -#endif - -// ===[ SCRIPT EXECUTE ]=== - -static RValue builtinScriptExecute(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - - int32_t codeId; - -#if IS_BC17_OR_HIGHER_ENABLED - if (args[0].type == RVALUE_METHOD) { - // If it is a method value, we'll need to extract code index directly - codeId = args[0].method->codeIndex; - } else -#endif - { - // Numeric script/function index - int32_t rawArg = RValue_toInt32(args[0]); - codeId = -1; - -#if IS_BC17_OR_HIGHER_ENABLED - // In GMS 2 BC17+, "scriptName" in source code is compiled as a FUNC-table index (same as builtinMethod). Resolve funcIdx -> codeIndex via codeIndexByName. - if (IS_BC17_OR_HIGHER(ctx) && rawArg >= 0 && ctx->dataWin->func.functionCount > (uint32_t) rawArg) { - const char* funcName = ctx->dataWin->func.functions[rawArg].name; - if (funcName != nullptr) { - ptrdiff_t idx = shgeti(ctx->codeIndexByName, (char*) funcName); - if (idx >= 0) { - codeId = ctx->codeIndexByName[idx].value; - } else { - // Not a user script - might be a builtin function reference - ptrdiff_t bidx = shgeti(ctx->builtinMap, (char*) funcName); - if (bidx >= 0) { - BuiltinFunc bf = ctx->builtinMap[bidx].value; - RValue* scriptArgs = (argCount > 1) ? &args[1] : nullptr; - return bf(ctx, scriptArgs, argCount - 1); - } - } - } - } -#endif - - // Fallback: treat as SCPT index (BC16 and earlier, or when FUNC lookup failed) - if (0 > codeId) { - if (0 > rawArg || (uint32_t) rawArg >= ctx->dataWin->scpt.count) { - fprintf(stderr, "VM: script_execute - invalid script index %d\n", rawArg); - return RValue_makeUndefined(); - } - codeId = ctx->dataWin->scpt.scripts[rawArg].codeId; - } - } - - if (0 > codeId || ctx->dataWin->code.count <= (uint32_t) codeId) { - fprintf(stderr, "VM: script_execute - invalid codeId %d\n", codeId); - return RValue_makeUndefined(); - } - - // Pass remaining args (skip the script index) - RValue* scriptArgs = (argCount > 1) ? &args[1] : nullptr; - int32_t scriptArgCount = argCount - 1; - - // If the method has a bound instance, temporarily swap currentInstance - Instance* savedInstance = (Instance*) ctx->currentInstance; -#if IS_BC17_OR_HIGHER_ENABLED - if (args[0].type == RVALUE_METHOD && args[0].method->boundInstanceId >= 0) { - Runner* runner = (Runner*) ctx->runner; - Instance* bound = hmget(runner->instancesById, args[0].method->boundInstanceId); - if (bound != nullptr) ctx->currentInstance = bound; - } -#endif - - RValue result = VM_callCodeIndex(ctx, codeId, scriptArgs, scriptArgCount); - - ctx->currentInstance = savedInstance; - return result; -} - -// ===[ OS FUNCTIONS ]=== - -static RValue builtinOsGetLanguage(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - return RValue_makeOwnedString(safeStrdup("en")); -} - -static RValue builtinOsGetRegion(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - return RValue_makeOwnedString(safeStrdup("US")); -} - -// ===[ DS_MAP BUILTIN FUNCTIONS ]=== - -static inline ptrdiff_t getValueIndexInMap(DsMapEntry** mapPtr, RValue keyRvalue) { - ptrdiff_t idx; - if (keyRvalue.type == RVALUE_STRING && keyRvalue.string != nullptr) { - // Fast path: No need to convert the RValue to a string if it is already a string - idx = shgeti(*mapPtr, keyRvalue.string); - } else { - char* key = RValue_toString(keyRvalue); - idx = shgeti(*mapPtr, key); - free(key); - } - - return idx; -} - -static RValue builtinDsMapCreate(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - return RValue_makeReal((GMLReal) dsMapCreate(runner)); -} - -static RValue builtinDsMapAdd(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsMapEntry** mapPtr = dsMapGet(runner, id); - if (mapPtr == nullptr) return RValue_makeUndefined(); - - char* key = RValue_toString(args[1]); - - // Only add if key doesn't exist - bool exists = shgeti(*mapPtr, key) != -1; - - if (exists) { - free(key); // Key already exists, we didn't insert it - } else { - shput(*mapPtr, key, RValue_makeIndependent(args[2])); - } - - return RValue_makeUndefined(); -} - -static RValue builtinDsMapSet(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsMapEntry** mapPtr = dsMapGet(runner, id); - if (mapPtr == nullptr) return RValue_makeUndefined(); - - char* key = RValue_toString(args[1]); - - ptrdiff_t existingKeyIndex = shgeti(*mapPtr, key); - - if (existingKeyIndex != -1) { - // If it already exists, we'll get the current value and free it - RValue_free(&(*mapPtr)[existingKeyIndex].value); - } - - shput(*mapPtr, key, RValue_makeIndependent(args[2])); - - if (existingKeyIndex != -1) { - // If it already existed, then shput still owns the old key - // So we'll need to free the created key - free(key); - } - - return RValue_makeUndefined(); -} - -static RValue builtinDsMapReplace(VMContext* ctx, RValue* args, int32_t argCount) { - // ds_map_replace is the same as ds_map_set in GMS 1.4 - return builtinDsMapSet(ctx, args, argCount); -} - -static RValue builtinDsMapFindValue(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsMapEntry** mapPtr = dsMapGet(runner, id); - if (mapPtr == nullptr) return RValue_makeUndefined(); - - ptrdiff_t idx = getValueIndexInMap(mapPtr, args[1]); - - if (0 > idx) return RValue_makeUndefined(); - RValue val = (*mapPtr)[idx].value; - if (val.type == RVALUE_STRING && val.string != nullptr) { - return RValue_makeOwnedString(safeStrdup(val.string)); - } - // Return a weak view: the map retains ownership. The caller's Pop will incRef into the destination slot. - val.ownsReference = false; - return val; -} - -static RValue builtinDsMapExists(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsMapEntry** mapPtr = dsMapGet(runner, id); - if (mapPtr == nullptr) return RValue_makeReal(0.0); - - ptrdiff_t idx = getValueIndexInMap(mapPtr, args[1]); - - return RValue_makeReal(idx >= 0 ? 1.0 : 0.0); -} - -static RValue builtinDsMapFindFirst(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsMapEntry** mapPtr = dsMapGet(runner, id); - if (mapPtr == nullptr || shlen(*mapPtr) == 0) return RValue_makeUndefined(); - return RValue_makeOwnedString(safeStrdup((*mapPtr)[0].key)); -} - -static RValue builtinDsMapFindNext(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsMapEntry** mapPtr = dsMapGet(runner, id); - if (mapPtr == nullptr) return RValue_makeUndefined(); - - ptrdiff_t idx = getValueIndexInMap(mapPtr, args[1]); - if (0 > idx || idx + 1 >= shlen(*mapPtr)) return RValue_makeUndefined(); - return RValue_makeOwnedString(safeStrdup((*mapPtr)[idx + 1].key)); -} - -static RValue builtinDsMapSize(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsMapEntry** mapPtr = dsMapGet(runner, id); - if (mapPtr == nullptr) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) shlen(*mapPtr)); -} - -static RValue builtinDsMapDestroy(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsMapEntry** mapPtr = dsMapGet(runner, id); - if (mapPtr == nullptr) return RValue_makeUndefined(); - // Free all keys and values - for (ptrdiff_t i = 0; shlen(*mapPtr) > i; i++) { - free((*mapPtr)[i].key); - RValue_free(&(*mapPtr)[i].value); - } - shfree(*mapPtr); - *mapPtr = nullptr; - return RValue_makeUndefined(); -} - -// ===[ DS_LIST FUNCTIONS ]=== - -static RValue builtinDsListCreate(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - return RValue_makeReal((GMLReal) dsListCreate(runner)); -} - -static RValue builtinDsListAdd(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsList* list = dsListGet(runner, id); - if (list == nullptr) return RValue_makeUndefined(); - // ds_list_add can take multiple values after the list id - repeat(argCount - 1, i) { - arrput(list->items, RValue_makeIndependent(args[i + 1])); - } - return RValue_makeUndefined(); -} - -static RValue builtinDsListDestroy(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsList* list = dsListGet(runner, id); - if (list == nullptr) return RValue_makeUndefined(); - repeat(arrlen(list->items), i) { - RValue_free(&list->items[i]); - } - arrfree(list->items); - list->items = nullptr; - list->freed = true; - return RValue_makeUndefined(); -} - -static RValue builtinDsListFindValue(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - int32_t pos = RValue_toInt32(args[1]); - DsList* list = dsListGet(runner, id); - if (list == nullptr) return RValue_makeUndefined(); - if (0 > pos || pos >= (int32_t) arrlen(list->items)) return RValue_makeUndefined(); - return RValue_makeIndependent(list->items[pos]); -} - -static RValue builtinDsListSize(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsList* list = dsListGet(runner, id); - if (list == nullptr) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) arrlen(list->items)); -} - -static RValue builtinDsListFindIndex(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - DsList* list = dsListGet(runner, id); - if (list == nullptr) return RValue_makeReal(-1.0); - RValue needle = args[1]; - for (int32_t i = 0; (int32_t) arrlen(list->items) > i; i++) { - RValue item = list->items[i]; - if (item.type != needle.type) continue; - switch (item.type) { - case RVALUE_REAL: - if (item.real == needle.real) return RValue_makeReal((GMLReal) i); - break; - case RVALUE_INT32: - case RVALUE_BOOL: - if (item.int32 == needle.int32) return RValue_makeReal((GMLReal) i); - break; -#ifndef NO_RVALUE_INT64 - case RVALUE_INT64: - if (item.int64 == needle.int64) return RValue_makeReal((GMLReal) i); - break; -#endif - case RVALUE_STRING: - if (item.string != nullptr && needle.string != nullptr && strcmp(item.string, needle.string) == 0) return RValue_makeReal((GMLReal) i); - break; - default: - break; - } - } - return RValue_makeReal(-1.0); -} - -// ===[ ARRAY FUNCTIONS ]=== - -static RValue builtinArrayLength1d(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) GMLArray_length1D(args[0].array)); -} - -// array_push(array, values...) - append one or more values to the end of the array (row 0). BC17+ arrays are mutable references; mutate in place. -static RValue builtinArrayPush(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeUndefined(); - GMLArray* arr = args[0].array; - int32_t startLen = GMLArray_length1D(arr); - int32_t toPush = argCount - 1; - if (toPush > 0) { - GMLArray_growTo(arr, startLen + toPush); - repeat(toPush, i) { - RValue* slot = GMLArray_slot(arr, startLen + i); - RValue val = args[1 + i]; - RValue_free(slot); - *slot = RValue_makeIndependent(val); - } - } - return RValue_makeUndefined(); -} - -// array_insert(array, index, values...) - insert one or more values at "index", shifting the tail up. If "index" is past the end, fill the gap with real 0 (see the yyVariable.js for reference). -static RValue builtinArrayInsert(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeUndefined(); - GMLArray* arr = args[0].array; - int32_t index = (int32_t) RValue_toReal(args[1]); - if (0 > index) index = 0; - int32_t toInsert = argCount - 2; - int32_t oldLen = (arr->rowCount == 0) ? 0 : arr->rows[0].length; - - // Pad with real 0 if index is past the current end - if (index > oldLen) { - GMLArray_growTo(arr, index); - GMLArrayRow* row = &arr->rows[0]; - for (int32_t i = oldLen; index > i; i++) { - RValue_free(&row->data[i]); - row->data[i] = RValue_makeReal(0.0); - } - oldLen = index; - } - - if (0 >= toInsert) return RValue_makeUndefined(); - - GMLArray_growTo(arr, oldLen + toInsert); - GMLArrayRow* row = &arr->rows[0]; - - // Shift tail up by toInsert - int32_t tailLen = oldLen - index; - if (tailLen > 0) memmove(&row->data[index + toInsert], &row->data[index], (size_t) tailLen * sizeof(RValue)); - - // Write inserted values - repeat(toInsert, i) { - row->data[index + i] = RValue_makeIndependent(args[2 + i]); - } - return RValue_makeUndefined(); -} - -// array_resize(array, newSize) - resize row 0 to newSize. Growth fills with undefined, shrinking frees truncated entries. -static RValue builtinArrayResize(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeUndefined(); - GMLArray* arr = args[0].array; - int32_t newSize = (int32_t) RValue_toReal(args[1]); - if (0 > newSize) newSize = 0; - if (arr->rowCount == 0) { - if (newSize == 0) return RValue_makeUndefined(); - GMLArray_growTo(arr, newSize); - return RValue_makeUndefined(); - } - GMLArrayRow* row = &arr->rows[0]; - if (newSize > row->length) { - GMLArray_growTo(arr, newSize); - } else if (row->length > newSize) { - for (int32_t i = newSize; row->length > i; i++) RValue_free(&row->data[i]); - row->length = newSize; - } - return RValue_makeUndefined(); -} - -// array_delete(array, pos, count) - remove `count` entries starting at `pos` from row 0, shifting the tail down. -static RValue builtinArrayDelete(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeUndefined(); - if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeUndefined(); - GMLArray* arr = args[0].array; - if (arr->rowCount == 0) return RValue_makeUndefined(); - GMLArrayRow* row = &arr->rows[0]; - int32_t pos = (int32_t) RValue_toReal(args[1]); - int32_t count = (int32_t) RValue_toReal(args[2]); - if (0 > pos) pos = 0; - if (pos >= row->length || 0 >= count) return RValue_makeUndefined(); - if (count > row->length - pos) count = row->length - pos; - repeat(count, i) RValue_free(&row->data[pos + i]); - int32_t tailStart = pos + count; - int32_t tailLen = row->length - tailStart; - if (tailLen > 0) memmove(&row->data[pos], &row->data[tailStart], (size_t) tailLen * sizeof(RValue)); - row->length -= count; - return RValue_makeUndefined(); -} - -// ===[ COLLISION FUNCTIONS]=== - -static RValue builtinPlaceFree(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeBool(true); - - Runner* runner = (Runner*) ctx->runner; - Instance* caller = (Instance*) ctx->currentInstance; - if (caller == nullptr) return RValue_makeBool(true); - - GMLReal testX = RValue_toReal(args[0]); - GMLReal testY = RValue_toReal(args[1]); - - // Save current position and temporarily move to test position - GMLReal savedX = caller->x; - GMLReal savedY = caller->y; - caller->x = testX; - caller->y = testY; - - InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); - bool free = true; - - if (callerBBox.valid) { - int32_t instanceCount = (int32_t) arrlen(runner->instances); - repeat(instanceCount, i) { - Instance* other = runner->instances[i]; - if (!other->active || !other->solid || other == caller) continue; - - InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); - if (!otherBBox.valid) continue; - - if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { - free = false; - break; - } - } - } - - // Restore original position - caller->x = savedX; - caller->y = savedY; - - return RValue_makeBool(free); -} - -// place_empty(x, y) - returns true if no instance overlaps at position (x, y), checking ALL instances (not just solid) -static bool placeEmptyAt(Runner* runner, Instance* caller, GMLReal testX, GMLReal testY) { - GMLReal savedX = caller->x; - GMLReal savedY = caller->y; - caller->x = testX; - caller->y = testY; - - InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); - bool empty = true; - - if (callerBBox.valid) { - int32_t instanceCount = (int32_t) arrlen(runner->instances); - repeat(instanceCount, i) { - Instance* other = runner->instances[i]; - if (!other->active || other == caller) continue; - - InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); - if (!otherBBox.valid) continue; - - if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { - empty = false; - break; - } - } - } - - caller->x = savedX; - caller->y = savedY; - return empty; -} - -// placeFreeAt - returns true if no SOLID instance overlaps at position (x, y) -static bool placeFreeAt(Runner* runner, Instance* caller, GMLReal testX, GMLReal testY) { - GMLReal savedX = caller->x; - GMLReal savedY = caller->y; - caller->x = testX; - caller->y = testY; - - InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); - bool free = true; - - if (callerBBox.valid) { - int32_t instanceCount = (int32_t) arrlen(runner->instances); - repeat(instanceCount, i) { - Instance* other = runner->instances[i]; - if (!other->active || !other->solid || other == caller) continue; - - InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); - if (!otherBBox.valid) continue; - - if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { - free = false; - break; - } - } - } - - caller->x = savedX; - caller->y = savedY; - return free; -} - -// noCollisionWithObject - returns true if no instance of the given object overlaps at position (x, y) -static bool noCollisionWithObject(Runner* runner, Instance* caller, GMLReal testX, GMLReal testY, int32_t objIndex) { - GMLReal savedX = caller->x; - GMLReal savedY = caller->y; - caller->x = testX; - caller->y = testY; - - InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); - bool free = true; - - if (callerBBox.valid) { - int32_t snapBase = Runner_pushInstancesForTarget(runner, objIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* other = runner->instanceSnapshots[i]; - if (!other->active || other == caller) continue; - - InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); - if (!otherBBox.valid) continue; - - if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { - free = false; - break; - } - } - Runner_popInstanceSnapshot(runner, snapBase); - } - - caller->x = savedX; - caller->y = savedY; - return free; -} - -// Tests whether a position is free for the given collision mode -// objIndex == INSTANCE_ALL with checkall=false: check solid only (place_free) -// objIndex == INSTANCE_ALL with checkall=true: check all instances (place_empty) -// objIndex == specific object/instance: check that specific target (instance_place == noone) -static bool mpTestFree(Runner* runner, Instance* inst, GMLReal x, GMLReal y, int32_t objIndex, bool checkall) { - if (objIndex == INSTANCE_ALL) { - if (checkall) { - return placeEmptyAt(runner, inst, x, y); - } else { - return placeFreeAt(runner, inst, x, y); - } - } else { - return noCollisionWithObject(runner, inst, x, y, objIndex); - } -} - -// place_empty(x, y) - returns true if no instance (solid or not) overlaps at position (x, y) -static RValue builtinPlaceEmpty(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeBool(true); - - Runner* runner = (Runner*) ctx->runner; - Instance* caller = (Instance*) ctx->currentInstance; - if (caller == nullptr) return RValue_makeBool(true); - - GMLReal testX = RValue_toReal(args[0]); - GMLReal testY = RValue_toReal(args[1]); - return RValue_makeBool(placeEmptyAt(runner, caller, testX, testY)); -} - -// ===[ Motion Planning ]=== - -static RValue builtinMpLinearStepCommon(VMContext* ctx, GMLReal goalX, GMLReal goalY, GMLReal stepsize, int32_t objIndex, bool checkall) { - Runner* runner = (Runner*) ctx->runner; - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr) return RValue_makeBool(false); - - // Check whether already at the correct position - if (inst->x == (float) goalX && inst->y == (float) goalY) return RValue_makeBool(true); - - // Check whether close enough for a single step - GMLReal dx = inst->x - goalX; - GMLReal dy = inst->y - goalY; - GMLReal dist = GMLReal_sqrt(dx * dx + dy * dy); - - GMLReal newX, newY; - bool reached; - if (dist <= stepsize) { - newX = goalX; - newY = goalY; - reached = true; - } else { - newX = inst->x + stepsize * (goalX - inst->x) / dist; - newY = inst->y + stepsize * (goalY - inst->y) / dist; - reached = false; - } - - // Check whether free - if (!mpTestFree(runner, inst, newX, newY, objIndex, checkall)) return RValue_makeBool(reached); - - inst->direction = (float) (GMLReal_atan2(-(newY - inst->y), newX - inst->x) * (180.0 / M_PI)); - inst->x = (float) newX; - inst->y = (float) newY; - SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); - return RValue_makeBool(reached); -} - -// mp_linear_step(x, y, stepsize, checkall) -static RValue builtinMpLinearStep(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal goalX = RValue_toReal(args[0]); - GMLReal goalY = RValue_toReal(args[1]); - GMLReal stepsize = RValue_toReal(args[2]); - bool checkall = RValue_toBool(args[3]); - return builtinMpLinearStepCommon(ctx, goalX, goalY, stepsize, INSTANCE_ALL, checkall); -} - -// mp_linear_step_object(x, y, stepsize, obj) -static RValue builtinMpLinearStepObject(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal goalX = RValue_toReal(args[0]); - GMLReal goalY = RValue_toReal(args[1]); - GMLReal stepsize = RValue_toReal(args[2]); - int32_t obj = RValue_toInt32(args[3]); - return builtinMpLinearStepCommon(ctx, goalX, goalY, stepsize, obj, true); -} - - -// Computes the shortest angular difference between two directions (result 0-180) -static GMLReal mpDiffDir(GMLReal dir1, GMLReal dir2) { - while (dir1 <= 0.0) dir1 += 360.0; - while (dir1 >= 360.0) dir1 -= 360.0; - while (dir2 < 0.0) dir2 += 360.0; - while (dir2 >= 360.0) dir2 -= 360.0; - GMLReal result = dir2 - dir1; - if (result < 0.0) result = -result; - if (result > 180.0) result = 360.0 - result; - return result; -} - -// Tries a step in the indicated direction; returns whether successful -// If successful, moves the instance and sets its direction -static bool mpTryDir(GMLReal dir, Runner* runner, Instance* inst, GMLReal speed, int32_t objIndex, bool checkall) { - // See whether angle is acceptable - if (mpDiffDir(dir, inst->direction) > runner->mpPotMaxrot) return false; - - GMLReal dirRad = dir * (M_PI / 180.0); - GMLReal cosDir = GMLReal_cos(dirRad); - GMLReal sinDir = GMLReal_sin(dirRad); - - // Check position a bit ahead - GMLReal aheadX = inst->x + speed * runner->mpPotAhead * cosDir; - GMLReal aheadY = inst->y - speed * runner->mpPotAhead * sinDir; - if (!mpTestFree(runner, inst, aheadX, aheadY, objIndex, checkall)) return false; - - // Check next position - GMLReal nextX = inst->x + speed * cosDir; - GMLReal nextY = inst->y - speed * sinDir; - if (!mpTestFree(runner, inst, nextX, nextY, objIndex, checkall)) return false; - - // OK, so set the position - inst->direction = (float) dir; - inst->x = (float) nextX; - inst->y = (float) nextY; - SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); - return true; -} - -static RValue builtinMpPotentialStepCommon(VMContext* ctx, GMLReal goalX, GMLReal goalY, GMLReal stepsize, int32_t objIndex, bool checkall) { - Runner* runner = (Runner*) ctx->runner; - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr) return RValue_makeBool(false); - - // Check whether already at the correct position - if (inst->x == (float) goalX && inst->y == (float) goalY) return RValue_makeBool(true); - - // Check whether close enough for a single step - GMLReal dx = inst->x - goalX; - GMLReal dy = inst->y - goalY; - GMLReal dist = GMLReal_sqrt(dx * dx + dy * dy); - if (stepsize >= dist) { - if (mpTestFree(runner, inst, goalX, goalY, objIndex, checkall)) { - GMLReal dir = GMLReal_atan2(-(goalY - inst->y), goalX - inst->x) * (180.0 / M_PI); - inst->direction = (float) dir; - inst->x = (float) goalX; - inst->y = (float) goalY; - SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); - } - return RValue_makeBool(true); - } - - // Try directions as much as possible towards the goal - GMLReal goaldir = GMLReal_atan2(-(goalY - inst->y), goalX - inst->x) * (180.0 / M_PI); - GMLReal curdir = 0.0; - while (180.0 > curdir) { - if (mpTryDir(goaldir - curdir, runner, inst, stepsize, objIndex, checkall)) return RValue_makeBool(false); - if (mpTryDir(goaldir + curdir, runner, inst, stepsize, objIndex, checkall)) return RValue_makeBool(false); - curdir += runner->mpPotStep; - } - - // If we did not succeed, a local minima was reached - // To avoid the instance getting stuck we rotate on the spot - if (runner->mpPotOnSpot) { - inst->direction = (float) (inst->direction + runner->mpPotMaxrot); - } - - return RValue_makeBool(false); -} - -// mp_potential_step(x, y, stepsize, checkall) -static RValue builtinMpPotentialStep(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal goalX = RValue_toReal(args[0]); - GMLReal goalY = RValue_toReal(args[1]); - GMLReal stepsize = RValue_toReal(args[2]); - bool checkall = RValue_toBool(args[3]); - return builtinMpPotentialStepCommon(ctx, goalX, goalY, stepsize, INSTANCE_ALL, checkall); -} - -// mp_potential_step_object(x, y, stepsize, obj) -static RValue builtinMpPotentialStepObject(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal goalX = RValue_toReal(args[0]); - GMLReal goalY = RValue_toReal(args[1]); - GMLReal stepsize = RValue_toReal(args[2]); - int32_t obj = RValue_toInt32(args[3]); - return builtinMpPotentialStepCommon(ctx, goalX, goalY, stepsize, obj, true); -} - -// mp_potential_settings(maxrot, rotstep, ahead, onspot) -static RValue builtinMpPotentialSettings(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - GMLReal maxrot = RValue_toReal(args[0]); - GMLReal rotstep = RValue_toReal(args[1]); - GMLReal ahead = RValue_toReal(args[2]); - bool onspot = RValue_toBool(args[3]); - runner->mpPotMaxrot = (maxrot < 1.0) ? 1.0 : maxrot; - runner->mpPotStep = (rotstep < 1.0) ? 1.0 : rotstep; - runner->mpPotAhead = (ahead < 1.0) ? 1.0 : ahead; - runner->mpPotOnSpot = onspot; - return RValue_makeReal(0.0); -} - -// ===[ STUBBED FUNCTIONS ]=== - -#define STUB_RETURN_ZERO(name) \ - static RValue builtin_##name(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { \ - logStubbedFunction(ctx, #name); \ - return RValue_makeReal(0.0); \ - } - -#define STUB_RETURN_TRUE(name) \ - static RValue builtin_##name(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { \ - logStubbedFunction(ctx, #name); \ - return RValue_makeBool(true); \ - } - -#define STUB_RETURN_VALUE(name, value) \ - static RValue builtin_##name(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { \ - logStubbedFunction(ctx, #name); \ - return RValue_makeReal(value); \ - } - -#define STUB_RETURN_UNDEFINED(name) \ - static RValue builtin_##name(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { \ - logStubbedFunction(ctx, #name); \ - return RValue_makeUndefined(); \ - } - -// Steam stubs -STUB_RETURN_ZERO(steam_initialised) -STUB_RETURN_ZERO(steam_stats_ready) -STUB_RETURN_ZERO(steam_file_exists) -STUB_RETURN_UNDEFINED(steam_file_write) -STUB_RETURN_UNDEFINED(steam_file_read) -STUB_RETURN_ZERO(steam_get_persona_name) - -// ===[ Audio Built-in Functions ]=== - -// Helper to get the AudioSystem from VMContext (returns nullptr if no audio) -static AudioSystem* getAudioSystem(VMContext* ctx) { - Runner* runner = (Runner*) ctx->runner; - return runner->audioSystem; -} - - -static RValue builtin_audioChannelNum(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - int32_t count = RValue_toInt32(args[0]); - audio->vtable->setChannelCount(audio, count); - return RValue_makeUndefined(); -} - -static RValue builtin_audioPlaySound(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeReal(-1.0); - - // Do not attempt to play "undefined" sounds (matches GameMaker-HTML5 behavior, and fixes random sound effects on room transitions in DELTARUNE Chapter 2) - if (args[0].type == RVALUE_UNDEFINED) - return RValue_makeReal(-1.0); - - int32_t soundIndex = RValue_toInt32(args[0]); - int32_t priority = RValue_toInt32(args[1]); - bool loop = RValue_toBool(args[2]); - int32_t instanceId = audio->vtable->playSound(audio, soundIndex, priority, loop); - return RValue_makeReal((GMLReal) instanceId); -} - -static RValue builtin_audioStopSound(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - int32_t soundOrInstance = RValue_toInt32(args[0]); - audio->vtable->stopSound(audio, soundOrInstance); - return RValue_makeUndefined(); -} - -static RValue builtin_audioStopAll(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - audio->vtable->stopAll(audio); - runner->lastMusicInstance = -1; - return RValue_makeUndefined(); -} - -static RValue builtin_audioIsPlaying(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeBool(false); - int32_t soundOrInstance = RValue_toInt32(args[0]); - bool playing = audio->vtable->isPlaying(audio, soundOrInstance); - return RValue_makeBool(playing); -} - -static RValue builtin_audioIsPaused(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeBool(false); - int32_t soundOrInstance = RValue_toInt32(args[0]); - bool playing = audio->vtable->isPlaying(audio, soundOrInstance); - return RValue_makeBool(!playing); -} - - -// audio_sound_length(sound) - returns the length of a sound in seconds. -static RValue builtin_audioSoundLength(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeReal(0.0); - int32_t soundOrInstance = RValue_toInt32(args[0]); - float length = audio->vtable->getSoundLength(audio, soundOrInstance); - return RValue_makeReal((GMLReal) length); -} - -static RValue builtin_audioSoundGain(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - int32_t soundOrInstance = RValue_toInt32(args[0]); - float gain = (float) RValue_toReal(args[1]); - uint32_t timeMs = (uint32_t) RValue_toInt32(args[2]); - audio->vtable->setSoundGain(audio, soundOrInstance, gain, timeMs); - return RValue_makeUndefined(); -} - -static RValue builtin_audioSoundPitch(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - int32_t soundOrInstance = RValue_toInt32(args[0]); - float pitch = (float) RValue_toReal(args[1]); - audio->vtable->setSoundPitch(audio, soundOrInstance, pitch); - return RValue_makeUndefined(); -} - -static RValue builtin_audioSoundGetGain(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeReal(0.0); - int32_t soundOrInstance = RValue_toInt32(args[0]); - float gain = audio->vtable->getSoundGain(audio, soundOrInstance); - return RValue_makeReal((GMLReal) gain); -} - -static RValue builtin_audioSoundGetPitch(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeReal(1.0); - int32_t soundOrInstance = RValue_toInt32(args[0]); - float pitch = audio->vtable->getSoundPitch(audio, soundOrInstance); - return RValue_makeReal((GMLReal) pitch); -} - -static RValue builtin_audioMasterGain(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - float gain = (float) RValue_toReal(args[0]); - audio->vtable->setMasterGain(audio, gain); - return RValue_makeUndefined(); -} - -static RValue builtin_audioGroupLoad(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - int32_t groupIndex = RValue_toInt32(args[0]); - audio->vtable->groupLoad(audio, groupIndex); - return RValue_makeUndefined(); -} - -static RValue builtin_audioGroupIsLoaded(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeBool(false); - int32_t groupIndex = RValue_toInt32(args[0]); - bool loaded = audio->vtable->groupIsLoaded(audio, groupIndex); - return RValue_makeBool(loaded); -} - -static RValue builtin_audioPlayMusic(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeReal(-1.0); - int32_t soundIndex = RValue_toInt32(args[0]); - int32_t priority = RValue_toInt32(args[1]); - bool loop = RValue_toBool(args[2]); - Runner* runner = (Runner*) ctx->runner; - int32_t instanceId = audio->vtable->playSound(audio, soundIndex, priority, loop); - runner->lastMusicInstance = instanceId; - return RValue_makeReal((GMLReal) instanceId); -} - -static RValue builtin_audioStopMusic(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - if (runner->lastMusicInstance >= 0) { - audio->vtable->stopSound(audio, runner->lastMusicInstance); - runner->lastMusicInstance = -1; - } - return RValue_makeUndefined(); -} - -static RValue builtin_audioMusicGain(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - if (runner->lastMusicInstance >= 0) { - float gain = (float) RValue_toReal(args[0]); - uint32_t timeMs = (uint32_t) RValue_toInt32(args[1]); - audio->vtable->setSoundGain(audio, runner->lastMusicInstance, gain, timeMs); - } - return RValue_makeUndefined(); -} - -static RValue builtin_audioMusicIsPlaying(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeBool(false); - Runner* runner = (Runner*) ctx->runner; - if (runner->lastMusicInstance >= 0) { - return RValue_makeBool(audio->vtable->isPlaying(audio, runner->lastMusicInstance)); - } - return RValue_makeBool(false); -} - -static RValue builtin_audioPauseSound(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - int32_t soundOrInstance = RValue_toInt32(args[0]); - audio->vtable->pauseSound(audio, soundOrInstance); - return RValue_makeUndefined(); -} - -static RValue builtin_audioResumeSound(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - int32_t soundOrInstance = RValue_toInt32(args[0]); - audio->vtable->resumeSound(audio, soundOrInstance); - return RValue_makeUndefined(); -} - -static RValue builtin_audioPauseAll(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - audio->vtable->pauseAll(audio); - return RValue_makeUndefined(); -} - -static RValue builtin_audioResumeAll(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - audio->vtable->resumeAll(audio); - return RValue_makeUndefined(); -} - -static RValue builtin_audioSoundGetTrackPosition(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeReal(0.0); - int32_t soundOrInstance = RValue_toInt32(args[0]); - float pos = audio->vtable->getTrackPosition(audio, soundOrInstance); - return RValue_makeReal((GMLReal) pos); -} - -static RValue builtin_audioSoundSetTrackPosition(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeUndefined(); - int32_t soundOrInstance = RValue_toInt32(args[0]); - float pos = (float) RValue_toReal(args[1]); - audio->vtable->setTrackPosition(audio, soundOrInstance, pos); - return RValue_makeUndefined(); -} - -static RValue builtin_audioCreateStream(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeReal(-1.0); - char* filename = RValue_toString(args[0]); - int32_t streamIndex = audio->vtable->createStream(audio, filename); - free(filename); - return RValue_makeReal((GMLReal) streamIndex); -} - -static RValue builtin_audioDestroyStream(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - AudioSystem* audio = getAudioSystem(ctx); - if (audio == nullptr) return RValue_makeReal(-1.0); - int32_t streamIndex = RValue_toInt32(args[0]); - bool success = audio->vtable->destroyStream(audio, streamIndex); - return RValue_makeReal(success ? 1.0 : -1.0); -} - -// Application surface stubs -STUB_RETURN_UNDEFINED(application_surface_enable) -STUB_RETURN_UNDEFINED(application_surface_draw_enable) - -// ===[ Gamepad Functions ]=== -static RValue builtinGamepadGetDeviceCount(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) RunnerGamepad_getDeviceCount(runner->gamepads)); -} - -static RValue builtinGamepadIsConnected(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); - int32_t device = RValue_toInt32(args[0]); - return RValue_makeBool(RunnerGamepad_isConnected(runner->gamepads, device)); -} - -static RValue builtinGamepadButtonCheck(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); - int32_t device = RValue_toInt32(args[0]); - int32_t button = RValue_toInt32(args[1]); - bool result = RunnerGamepad_buttonCheck(runner->gamepads, device, button); - return RValue_makeBool(result); -} - -static RValue builtinGamepadButtonCheckPressed(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); - int32_t device = RValue_toInt32(args[0]); - int32_t button = RValue_toInt32(args[1]); - return RValue_makeBool(RunnerGamepad_buttonCheckPressed(runner->gamepads, device, button)); -} - -static RValue builtinGamepadButtonCheckReleased(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); - int32_t device = RValue_toInt32(args[0]); - int32_t button = RValue_toInt32(args[1]); - return RValue_makeBool(RunnerGamepad_buttonCheckReleased(runner->gamepads, device, button)); -} - -static RValue builtinGamepadButtonValue(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t device = RValue_toInt32(args[0]); - int32_t button = RValue_toInt32(args[1]); - return RValue_makeReal(RunnerGamepad_buttonValue(runner->gamepads, device, button)); -} - -static RValue builtinGamepadIsSupported(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); - return RValue_makeBool(true); -} - -static RValue builtinGamepadAxisValue(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t device = RValue_toInt32(args[0]); - int32_t axis = RValue_toInt32(args[1]); - return RValue_makeReal(RunnerGamepad_axisValue(runner->gamepads, device, axis)); -} - -static RValue builtinGamepadGetDescription(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeOwnedString(safeStrdup("")); - int32_t device = RValue_toInt32(args[0]); - const char* desc = RunnerGamepad_getDescription(runner->gamepads, device); - return RValue_makeOwnedString(safeStrdup(desc)); -} - -static RValue builtinGamepadGetGuid(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeOwnedString(safeStrdup("none")); - int32_t device = RValue_toInt32(args[0]); - return RValue_makeOwnedString(safeStrdup(RunnerGamepad_getGuid(runner->gamepads, device))); -} - -static RValue builtinGamepadGetButtonThreshold(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.5); - int32_t device = RValue_toInt32(args[0]); - return RValue_makeReal(RunnerGamepad_getButtonThreshold(runner->gamepads, device)); -} - -static RValue builtinGamepadSetButtonThreshold(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeUndefined(); - int32_t device = RValue_toInt32(args[0]); - float threshold = (float) RValue_toReal(args[1]); - RunnerGamepad_setButtonThreshold(runner->gamepads, device, threshold); - return RValue_makeUndefined(); -} - -static RValue builtinGamepadGetAxisDeadzone(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.15); - int32_t device = RValue_toInt32(args[0]); - return RValue_makeReal(RunnerGamepad_getAxisDeadzone(runner->gamepads, device)); -} - -static RValue builtinGamepadSetAxisDeadzone(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeUndefined(); - int32_t device = RValue_toInt32(args[0]); - float deadzone = (float) RValue_toReal(args[1]); - RunnerGamepad_setAxisDeadzone(runner->gamepads, device, deadzone); - return RValue_makeUndefined(); -} - -static RValue builtinGamepadAxisCount(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t device = RValue_toInt32(args[0]); - return RValue_makeReal(RunnerGamepad_getAxisCount(runner->gamepads, device)); -} - -static RValue builtinGamepadButtonCount(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t device = RValue_toInt32(args[0]); - return RValue_makeReal(RunnerGamepad_getButtonCount(runner->gamepads, device)); -} - -static RValue builtinGamepadHatCount(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t device = RValue_toInt32(args[0]); - return RValue_makeReal(RunnerGamepad_getHatCount(runner->gamepads, device)); -} - -static RValue builtinGamepadHatValue(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t device = RValue_toInt32(args[0]); - int32_t hat = RValue_toInt32(args[1]); - return RValue_makeReal(RunnerGamepad_getHatValue(runner->gamepads, device, hat)); -} - -// ===[ INI Functions ]=== - -static void discardIniCache(Runner* runner) { - if (runner->cachedIni != nullptr) { - Ini_free(runner->cachedIni); - runner->cachedIni = nullptr; - } - free(runner->cachedIniPath); - runner->cachedIniPath = nullptr; -} - -static RValue builtinIniOpen(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - - Runner* runner = (Runner*) ctx->runner; - const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); - - // If the same file is already open, do nothing - if (runner->currentIni != nullptr && runner->currentIniPath != nullptr && strcmp(runner->currentIniPath, path) == 0) { - return RValue_makeUndefined(); - } - - // Close any previously open INI (implicit close, no disk write) - if (runner->currentIni != nullptr) { - Ini_free(runner->currentIni); - runner->currentIni = nullptr; - } - free(runner->currentIniPath); - runner->currentIniPath = nullptr; - - // Check if we have a cached INI for this path - if (runner->cachedIni != nullptr && runner->cachedIniPath != nullptr && strcmp(runner->cachedIniPath, path) == 0) { - runner->currentIni = runner->cachedIni; - runner->currentIniPath = runner->cachedIniPath; - runner->cachedIni = nullptr; - runner->cachedIniPath = nullptr; - runner->currentIniDirty = false; - return RValue_makeUndefined(); - } - - // Cache miss, discard the old cache and read from disk - discardIniCache(runner); - - FileSystem* fs = runner->fileSystem; - - runner->currentIniPath = safeStrdup(path); - - char* content = fs->vtable->readFileText(fs, path); - if (content != nullptr) { - runner->currentIni = Ini_parse(content); - free(content); - } else { - runner->currentIni = Ini_parse(""); - } - - runner->currentIniDirty = false; - - return RValue_makeUndefined(); -} - -static RValue builtinIniClose(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->currentIni != nullptr) { - FileSystem* fs = runner->fileSystem; - - if (runner->currentIniDirty) { - char* serialized = Ini_serialize(runner->currentIni, INI_SERIALIZE_DEFAULT_INITIAL_CAPACITY); - fs->vtable->writeFileText(fs, runner->currentIniPath, serialized); - free(serialized); - } - - // Move to cache instead of freeing - discardIniCache(runner); - runner->cachedIni = runner->currentIni; - runner->cachedIniPath = runner->currentIniPath; - runner->currentIni = nullptr; - runner->currentIniPath = nullptr; - } else { - free(runner->currentIniPath); - runner->currentIniPath = nullptr; - } - - return RValue_makeUndefined(); -} - -static RValue builtinIniReadString(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (3 > argCount || runner->currentIni == nullptr) return RValue_makeOwnedString(safeStrdup("")); - - const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); - const char* key = (args[1].type == RVALUE_STRING ? args[1].string : ""); - - const char* value = Ini_getString(runner->currentIni, section, key); - if (value != nullptr) { - return RValue_makeOwnedString(safeStrdup(value)); - } - - // Return the default value (3rd arg) - if (args[2].type == RVALUE_STRING && args[2].string != nullptr) { - return RValue_makeOwnedString(safeStrdup(args[2].string)); - } - char* str = RValue_toString(args[2]); - return RValue_makeOwnedString(str); -} - -static RValue builtinIniReadReal(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (3 > argCount || runner->currentIni == nullptr) return RValue_makeReal(0.0); - - const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); - const char* key = (args[1].type == RVALUE_STRING ? args[1].string : ""); - - const char* value = Ini_getString(runner->currentIni, section, key); - if (value != nullptr) { - return RValue_makeReal(atof(value)); - } - - return RValue_makeReal(RValue_toReal(args[2])); -} - -static RValue builtinIniWriteString(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (3 > argCount || runner->currentIni == nullptr) return RValue_makeUndefined(); - - const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); - const char* key = (args[1].type == RVALUE_STRING ? args[1].string : ""); - const char* value = (args[2].type == RVALUE_STRING ? args[2].string : ""); - - Ini_setString(runner->currentIni, section, key, value); - runner->currentIniDirty = true; - return RValue_makeUndefined(); -} - -static RValue builtinIniWriteReal(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (3 > argCount || runner->currentIni == nullptr) return RValue_makeUndefined(); - - const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); - const char* key = (args[1].type == RVALUE_STRING ? args[1].string : ""); - char* valueStr = RValue_toString(args[2]); - - Ini_setString(runner->currentIni, section, key, valueStr); - runner->currentIniDirty = true; - free(valueStr); - return RValue_makeUndefined(); -} - -static RValue builtinIniSectionExists(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (1 > argCount || runner->currentIni == nullptr) return RValue_makeBool(false); - - const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); - return RValue_makeBool(Ini_hasSection(runner->currentIni, section)); -} - -// ===[ Text File Functions ]=== - -static int32_t findFreeTextFileSlot(Runner* runner) { - repeat(MAX_OPEN_TEXT_FILES, i) { - if (!runner->openTextFiles[i].isOpen) return (int32_t) i; - } - return -1; -} - -static RValue builtinFileExists(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); - Runner* runner = (Runner*) ctx->runner; - FileSystem* fs = runner->fileSystem; - return RValue_makeBool(fs->vtable->fileExists(fs, path)); -} - -static RValue builtinFileTextOpenRead(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(-1.0); - const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); - Runner* runner = (Runner*) ctx->runner; - FileSystem* fs = runner->fileSystem; - - int32_t slot = findFreeTextFileSlot(runner); - if (0 > slot) { - fprintf(stderr, "Warning: Too many open text files!\n"); - abort(); - } - - char* content = fs->vtable->readFileText(fs, path); - if (content == nullptr) { - // GML returns a valid handle even if the file doesn't exist; eof is immediately true - content = safeStrdup(""); - } - - runner->openTextFiles[slot] = (OpenTextFile) { - .content = content, - .writeBuffer = nullptr, - .filePath = nullptr, - .readPos = 0, - .contentLen = (int32_t) strlen(content), - .isWriteMode = false, - .isOpen = true, - }; - - return RValue_makeReal((GMLReal) slot); -} - -static RValue builtinFileTextOpenWrite(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(-1.0); - const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); - Runner* runner = (Runner*) ctx->runner; - - int32_t slot = findFreeTextFileSlot(runner); - if (0 > slot) { - fprintf(stderr, "Warning: Too many open text files!\n"); - abort(); - } - - runner->openTextFiles[slot] = (OpenTextFile) { - .content = nullptr, - .writeBuffer = safeStrdup(""), - .filePath = safeStrdup(path), - .readPos = 0, - .contentLen = 0, - .isWriteMode = true, - .isOpen = true, - }; - - return RValue_makeReal((GMLReal) slot); -} - -static RValue builtinFileTextClose(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t handle = RValue_toInt32(args[0]); - if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeUndefined(); - - OpenTextFile* file = &runner->openTextFiles[handle]; - if (file->isWriteMode && file->writeBuffer != nullptr && file->filePath != nullptr) { - FileSystem* fs = runner->fileSystem; - fs->vtable->writeFileText(fs, file->filePath, file->writeBuffer); - } - - free(file->content); - free(file->writeBuffer); - free(file->filePath); - *file = (OpenTextFile) {0}; - return RValue_makeUndefined(); -} - -static RValue builtinFileTextReadString(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); - Runner* runner = (Runner*) ctx->runner; - int32_t handle = RValue_toInt32(args[0]); - if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeOwnedString(safeStrdup("")); - - OpenTextFile* file = &runner->openTextFiles[handle]; - if (file->readPos >= file->contentLen) return RValue_makeOwnedString(safeStrdup("")); - - // Read until newline, carriage return, or EOF (does NOT consume the newline) - int32_t start = file->readPos; - while (file->contentLen > file->readPos) { - char c = file->content[file->readPos]; - if (TextUtils_isNewlineChar(c)) - break; - file->readPos++; - } - - int32_t len = file->readPos - start; - char* result = safeMalloc((size_t) len + 1); - memcpy(result, file->content + start, (size_t) len); - result[len] = '\0'; - return RValue_makeOwnedString(result); -} - -static RValue builtinFileTextReadln(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); - Runner* runner = (Runner*) ctx->runner; - int32_t handle = RValue_toInt32(args[0]); - if (0 > handle || MAX_OPEN_TEXT_FILES <= handle || !runner->openTextFiles[handle].isOpen) return RValue_makeOwnedString(safeStrdup("")); - - OpenTextFile* file = &runner->openTextFiles[handle]; - - int size = 0; - int readPos = file->readPos; - - // First we read everything to figure out what will be the size of the string - // Skip past the current line (consume everything up to and including the newline) - while (file->contentLen > readPos) { - char c = file->content[readPos]; - readPos++; - if (c == '\n') - break; - if (c == '\r') { - // Handle \r\n - if (file->contentLen > readPos && file->content[readPos] == '\n') { - readPos++; - } - break; - } - size++; - } - - // Now we copy it because we already know the size of the string! - char* string = safeMalloc(size + 1); // +1 because the last one is null - memcpy(string, file->content + file->readPos, size); - string[size] = '\0'; - file->readPos = readPos; - return RValue_makeOwnedString(string); -} - -static RValue builtinFileTextReadReal(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - int32_t handle = RValue_toInt32(args[0]); - if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeReal(0.0); - - OpenTextFile* file = &runner->openTextFiles[handle]; - if (file->readPos >= file->contentLen) return RValue_makeReal(0.0); - - // strtod will parse the number and advance past it - char* endPtr = nullptr; - GMLReal value = GMLReal_strtod(file->content + file->readPos, &endPtr); - if (endPtr != nullptr) { - file->readPos = (int32_t) (endPtr - file->content); - } - - return RValue_makeReal(value); -} - -static RValue builtinFileTextWriteString(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t handle = RValue_toInt32(args[0]); - if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeUndefined(); - - OpenTextFile* file = &runner->openTextFiles[handle]; - if (!file->isWriteMode) return RValue_makeUndefined(); - - char* str = RValue_toString(args[1]); - size_t oldLen = strlen(file->writeBuffer); - size_t addLen = strlen(str); - file->writeBuffer = safeRealloc(file->writeBuffer, oldLen + addLen + 1); - memcpy(file->writeBuffer + oldLen, str, addLen); - file->writeBuffer[oldLen + addLen] = '\0'; - free(str); - - return RValue_makeUndefined(); -} - -static RValue builtinFileTextWriteln(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t handle = RValue_toInt32(args[0]); - if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeUndefined(); - - OpenTextFile* file = &runner->openTextFiles[handle]; - if (!file->isWriteMode) return RValue_makeUndefined(); - - size_t oldLen = strlen(file->writeBuffer); - file->writeBuffer = safeRealloc(file->writeBuffer, oldLen + 2); - file->writeBuffer[oldLen] = '\n'; - file->writeBuffer[oldLen + 1] = '\0'; - - return RValue_makeUndefined(); -} - -static RValue builtinFileTextWriteReal(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t handle = RValue_toInt32(args[0]); - if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeUndefined(); - - OpenTextFile* file = &runner->openTextFiles[handle]; - if (!file->isWriteMode) return RValue_makeUndefined(); - - char* str = RValue_toString(args[1]); - size_t oldLen = strlen(file->writeBuffer); - size_t addLen = strlen(str); - file->writeBuffer = safeRealloc(file->writeBuffer, oldLen + addLen + 1); - memcpy(file->writeBuffer + oldLen, str, addLen); - file->writeBuffer[oldLen + addLen] = '\0'; - free(str); - - return RValue_makeUndefined(); -} - -static RValue builtinFileTextEof(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(true); - Runner* runner = (Runner*) ctx->runner; - int32_t handle = RValue_toInt32(args[0]); - if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeBool(true); - - OpenTextFile* file = &runner->openTextFiles[handle]; - return RValue_makeBool(file->readPos >= file->contentLen); -} - -static RValue builtinFileDelete(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); - Runner* runner = (Runner*) ctx->runner; - FileSystem* fs = runner->fileSystem; - fs->vtable->deleteFile(fs, path); - return RValue_makeUndefined(); -} - -// Keyboard functions -static RValue builtinKeyboardCheck(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - Runner* runner = (Runner*) ctx->runner; - int32_t key = RValue_toInt32(args[0]); - return RValue_makeBool(RunnerKeyboard_check(runner->keyboard, key)); -} - -static RValue builtinKeyboardCheckPressed(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - Runner* runner = (Runner*) ctx->runner; - int32_t key = RValue_toInt32(args[0]); - return RValue_makeBool(RunnerKeyboard_checkPressed(runner->keyboard, key)); -} - -static RValue builtinKeyboardCheckReleased(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - Runner* runner = (Runner*) ctx->runner; - int32_t key = RValue_toInt32(args[0]); - return RValue_makeBool(RunnerKeyboard_checkReleased(runner->keyboard, key)); -} - -static RValue builtinKeyboardCheckDirect(VMContext* ctx, RValue* args, int32_t argCount) { - // keyboard_check_direct is the same as keyboard_check for our purposes - return builtinKeyboardCheck(ctx, args, argCount); -} - -static RValue builtinKeyboardKeyPress(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t key = RValue_toInt32(args[0]); - RunnerKeyboard_simulatePress(runner->keyboard, key); - return RValue_makeUndefined(); -} - -static RValue builtinKeyboardKeyRelease(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t key = RValue_toInt32(args[0]); - RunnerKeyboard_simulateRelease(runner->keyboard, key); - return RValue_makeUndefined(); -} - -static RValue builtinKeyboardClear(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t key = RValue_toInt32(args[0]); - RunnerKeyboard_clear(runner->keyboard, key); - return RValue_makeUndefined(); -} - -// ===[ Joystick Functions ]=== -static RValue builtinJoystickExists(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); - int32_t id = RValue_toInt32(args[0]) - 1; - return RValue_makeBool(RunnerGamepad_isConnected(runner->gamepads, id)); -} - -static RValue builtinJoystickXpos(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t id = RValue_toInt32(args[0]) - 1; - return RValue_makeReal((GMLReal) RunnerGamepad_axisValue(runner->gamepads, id, GP_AXIS_LH)); -} - -static RValue builtinJoystickYpos(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t id = RValue_toInt32(args[0]) - 1; - return RValue_makeReal((GMLReal) RunnerGamepad_axisValue(runner->gamepads, id, GP_AXIS_LV)); -} - -static RValue builtinJoystickDirection(VMContext* ctx, RValue* args, int32_t argCount) { - // Returns the joystick direction - if (1 > argCount) return RValue_makeReal(101.0); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(101.0); - int32_t id = RValue_toInt32(args[0]) - 1; - float haxis = RunnerGamepad_axisValue(runner->gamepads, id, GP_AXIS_LH); - float vaxis = RunnerGamepad_axisValue(runner->gamepads, id, GP_AXIS_LV); - - int32_t dir = 0; - if (vaxis < -0.3f) { - dir = 6; - } else if (vaxis > 0.3f) { - dir = 0; - } else { - dir = 3; - } - - if (haxis < -0.3f) { - dir += 1; - } else if (haxis > 0.3f) { - dir += 3; - } else { - dir += 2; - } - - return RValue_makeReal(96 + dir); -} - -static RValue builtinJoystickPov(VMContext* ctx, RValue* args, int32_t argCount) { - // Returns the D-pad/POV hat angle in degrees (0=up, 90=right, 180=down, 270=left), - if (1 > argCount) return RValue_makeReal(-1.0); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(-1.0); - int32_t id = RValue_toInt32(args[0]) - 1; - RunnerGamepadState* gp = runner->gamepads; - bool up = RunnerGamepad_buttonCheck(gp, id, GP_PADU); - bool down = RunnerGamepad_buttonCheck(gp, id, GP_PADD); - bool left = RunnerGamepad_buttonCheck(gp, id, GP_PADL); - bool right = RunnerGamepad_buttonCheck(gp, id, GP_PADR); - if (!up && !down && !left && !right) return RValue_makeReal(-1.0); - if (up && right) return RValue_makeReal(45.0); - if (right && down) return RValue_makeReal(135.0); - if (down && left) return RValue_makeReal(225.0); - if (left && up) return RValue_makeReal(315.0); - if (up) return RValue_makeReal(0.0); - if (right) return RValue_makeReal(90.0); - if (down) return RValue_makeReal(180.0); - if (left) return RValue_makeReal(270.0); - return RValue_makeReal(-1.0); -} - -static RValue builtinJoystickCheckButton(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeBool(false); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); - int32_t id = RValue_toInt32(args[0]) - 1; - int32_t button = RawToGPUndertale(RValue_toInt32(args[1])); //UNDERTALE HACK - return RValue_makeBool(RunnerGamepad_buttonCheck(runner->gamepads, id, button)); -} - -static RValue builtinJoystickHasPov(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); - int32_t id = RValue_toInt32(args[0]) - 1; - return RValue_makeBool(RunnerGamepad_isConnected(runner->gamepads, id)); -} - -static RValue builtinJoystickButtons(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t id = RValue_toInt32(args[0]) - 1; - if (!RunnerGamepad_isConnected(runner->gamepads, id)) return RValue_makeReal(0.0); - return RValue_makeReal(GP_BUTTON_COUNT); -} - -static RValue builtinJoystickName(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeOwnedString(safeStrdup("")); - int32_t id = RValue_toInt32(args[0]) - 1; - return RValue_makeOwnedString(safeStrdup(RunnerGamepad_getDescription(runner->gamepads, id))); -} - -static RValue builtinJoystickAxes(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); - int32_t id = RValue_toInt32(args[0]) - 1; - return RValue_makeReal(RunnerGamepad_getAxisCount(runner->gamepads, id)); -} - -// Window stubs -STUB_RETURN_ZERO(window_get_fullscreen) -STUB_RETURN_UNDEFINED(window_set_fullscreen) -STUB_RETURN_UNDEFINED(window_set_size) -STUB_RETURN_UNDEFINED(window_center) -static RValue builtinWindowGetWidth(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - return RValue_makeReal((GMLReal) ctx->dataWin->gen8.defaultWindowWidth); -} - -static RValue builtinWindowGetHeight(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - return RValue_makeReal((GMLReal) ctx->dataWin->gen8.defaultWindowHeight); -} - -static RValue builtinWindowSetCaption(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - char* val = RValue_toString(args[0]); - char windowTitle[256]; - snprintf(windowTitle, sizeof(windowTitle), "Butterscotch - %s", val); - - Runner* runner = (Runner*) ctx->runner; - if (runner->setWindowTitle && runner->nativeWindow) { - runner->setWindowTitle(runner->nativeWindow, windowTitle); - printf("GL: Window title set to: %s\n", val); - } - - free(val); - return RValue_makeUndefined(); -} - -static RValue builtinWindowHasFocus(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - // Always return true when not on GLFW - if (runner == nullptr || runner->nativeWindow == nullptr) { - return RValue_makeBool(true); - } - - if (runner->windowHasFocus) { - return RValue_makeBool(runner->windowHasFocus(runner->nativeWindow)); - } - - return RValue_makeBool(true); -} - -// ===[ Game State Functions ]=== -static RValue builtinGameRestart(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - ctx->runner->pendingRoom = ROOM_RESTARTGAME; - return RValue_makeUndefined(); -} - -static RValue builtinGameEnd(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - runner->shouldExit = true; - return RValue_makeUndefined(); -} -STUB_RETURN_UNDEFINED(game_save) -STUB_RETURN_UNDEFINED(game_load) - -static RValue builtinInstanceNumber(VMContext* ctx, MAYBE_UNUSED RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - int32_t objectIndex = RValue_toInt32(args[0]); - int32_t count = 0; - int32_t snapBase = Runner_pushInstancesOfObject(runner, objectIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - if (runner->instanceSnapshots[i]->active) count++; - } - Runner_popInstanceSnapshot(runner, snapBase); - return RValue_makeReal((GMLReal) count); -} - -static RValue builtinInstanceFind(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(INSTANCE_NOONE); - Runner* runner = (Runner*) ctx->runner; - int32_t objectIndex = RValue_toInt32(args[0]); - int32_t n = RValue_toInt32(args[1]); - int32_t count = 0; - int32_t resultId = INSTANCE_NOONE; - int32_t snapBase = Runner_pushInstancesOfObject(runner, objectIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (!inst->active) continue; - if (count == n) { resultId = inst->instanceId; break; } - count++; - } - Runner_popInstanceSnapshot(runner, snapBase); - return RValue_makeReal((GMLReal) resultId); -} - -static RValue builtinInstanceNearest(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeReal(INSTANCE_NOONE); - Runner* runner = (Runner*) ctx->runner; - GMLReal x = RValue_toReal(args[0]); - GMLReal y = RValue_toReal(args[1]); - GMLReal bestDistSq = 0.0; - int32_t objectIndex = RValue_toInt32(args[2]); - int32_t resultId = INSTANCE_NOONE; - int32_t snapBase = Runner_pushInstancesOfObject(runner, objectIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (!inst->active) continue; - - GMLReal dx = inst->x - x; - GMLReal dy = inst->y - y; - GMLReal distSq = dx * dx + dy * dy; - - if (resultId == INSTANCE_NOONE || distSq < bestDistSq) { - resultId = inst->instanceId; - bestDistSq = distSq; - } - } - Runner_popInstanceSnapshot(runner, snapBase); - return RValue_makeReal((GMLReal) resultId); -} - -static RValue builtinInstanceExists(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - bool found = false; - if (id >= 0 && runner->dataWin->objt.count > (uint32_t) id) { - int32_t snapBase = Runner_pushInstancesOfObject(runner, id); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - if (runner->instanceSnapshots[i]->active) { found = true; break; } - } - Runner_popInstanceSnapshot(runner, snapBase); - } else { - // Instance ID: search for a specific instance - Instance* inst = hmget(runner->instancesById, id); - found = (inst != nullptr && inst->active); - } - return RValue_makeBool(found); -} - -static RValue builtinInstanceDestroy(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (1 > argCount) { - // No args: destroy the current instance - if (ctx->currentInstance != nullptr) { - Runner_destroyInstance(runner, (Instance*) ctx->currentInstance); - } - return RValue_makeUndefined(); - } - // 1 arg: find and destroy matching instances. Destroy events run user code that can spawn/destroy/instance_change other instances; iterate a snapshot of the bucket so those mutations don't corrupt our loop. - int32_t id = RValue_toInt32(args[0]); - if (id >= 0 && runner->dataWin->objt.count > (uint32_t) id) { - int32_t snapBase = Runner_pushInstancesOfObject(runner, id); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (inst->active) Runner_destroyInstance(runner, inst); - } - Runner_popInstanceSnapshot(runner, snapBase); - } else { - Instance* inst = hmget(runner->instancesById, id); - if (inst != nullptr && inst->active) Runner_destroyInstance(runner, inst); - } - return RValue_makeUndefined(); -} - -static RValue builtinInstanceCreate(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - GMLReal x = RValue_toReal(args[0]); - GMLReal y = RValue_toReal(args[1]); - int32_t objectIndex = RValue_toInt32(args[2]); - if (0 > objectIndex || runner->dataWin->objt.count <= (uint32_t) objectIndex) { - fprintf(stderr, "VM: instance_create: objectIndex %d out of range\n", objectIndex); - return RValue_makeReal(0.0); - } - Instance* callerInst = (Instance*) ctx->currentInstance; - Instance* inst = Runner_createInstance(runner, x, y, objectIndex); - if (inst == nullptr) return RValue_makeReal(INSTANCE_NOONE); - if (callerInst != nullptr && ctx->creatorVarID >= 0) { - Instance_setSelfVar(inst, ctx->creatorVarID, RValue_makeReal((GMLReal) callerInst->instanceId)); - } - return RValue_makeReal((GMLReal) inst->instanceId); -} - -static RValue builtinInstanceCopy(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - Instance* source = (Instance*) ctx->currentInstance; - if (source == nullptr) { - fprintf(stderr, "VM: instance_copy: no current instance\n"); - return RValue_makeReal(INSTANCE_NOONE); - } - bool performEvent = argCount > 0 ? RValue_toBool(args[0]) : false; - Instance* inst = Runner_copyInstance(runner, source, performEvent); - if (inst == nullptr) return RValue_makeReal(INSTANCE_NOONE); - return RValue_makeReal((GMLReal) inst->instanceId); -} - -static RValue builtinInstanceCreateLayer(VMContext* ctx, RValue* args, int32_t argCount) { - if (4 > argCount) return RValue_makeReal(INSTANCE_NOONE); - Runner* runner = (Runner*) ctx->runner; - GMLReal x = RValue_toReal(args[0]); - GMLReal y = RValue_toReal(args[1]); - int32_t layerId = resolveLayerIdArg(runner, args[2]); - int32_t objectIndex = RValue_toInt32(args[3]); - - Instance* inst = Runner_createInstanceWithLayer(runner, x, y, objectIndex, layerId); - if (inst == nullptr) return RValue_makeReal(INSTANCE_NOONE); - - Instance* callerInst = (Instance*) ctx->currentInstance; - if (callerInst != nullptr && ctx->creatorVarID >= 0) { - Instance_setSelfVar(inst, ctx->creatorVarID, RValue_makeReal((GMLReal) callerInst->instanceId)); - } - - return RValue_makeReal((GMLReal) inst->instanceId); -} - -static RValue builtinInstanceCreateDepth(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - GMLReal x = RValue_toReal(args[0]); - GMLReal y = RValue_toReal(args[1]); - int32_t depth = RValue_toInt32(args[2]); - int32_t objectIndex = RValue_toInt32(args[3]); - if (0 > objectIndex || runner->dataWin->objt.count <= (uint32_t) objectIndex) { - fprintf(stderr, "VM: instance_create: objectIndex %d out of range\n", objectIndex); - return RValue_makeReal(0.0); - } - Instance* callerInst = (Instance*) ctx->currentInstance; - Instance* inst = Runner_createInstanceWithDepth(runner, x, y, objectIndex, depth); - if (inst == nullptr) return RValue_makeReal(INSTANCE_NOONE); - if (callerInst != nullptr && ctx->creatorVarID >= 0) { - Instance_setSelfVar(inst, ctx->creatorVarID, RValue_makeReal((GMLReal) callerInst->instanceId)); - } - return RValue_makeReal((GMLReal) inst->instanceId); -} - -static RValue builtinInstanceChange(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr) return RValue_makeUndefined(); - - int32_t objectIndex = RValue_toInt32(args[0]); - bool performEvents = RValue_toBool(args[1]); - - if (0 > objectIndex || (uint32_t) objectIndex >= runner->dataWin->objt.count) { - fprintf(stderr, "VM: instance_change: objectIndex %d out of range\n", objectIndex); - return RValue_makeUndefined(); - } - - // Fire destroy event on old object if requested - if (performEvents) { - Runner_executeEvent(runner, inst, EVENT_DESTROY, 0); - } - - // Move the instance between per-object lists before mutating objectIndex so the remove walks the old parent chain and the add walks the new one. - Runner_removeInstanceFromObjectLists(runner, inst); - - // Change object index and copy properties from new object definition - GameObject* newObjDef = &runner->dataWin->objt.objects[objectIndex]; - inst->objectIndex = objectIndex; - Runner_addInstanceToObjectLists(runner, inst); - inst->spriteIndex = newObjDef->spriteId; - inst->visible = newObjDef->visible; - inst->solid = newObjDef->solid; - inst->persistent = newObjDef->persistent; - inst->depth = newObjDef->depth; - inst->maskIndex = newObjDef->textureMaskId; - inst->imageIndex = 0.0; - // The instance pointer is unchanged so this is just a depth shift, not a structural change. - runner->drawableListSortDirty = true; - - // Fire create event on new object if requested - if (performEvents) { - Runner_executeEvent(runner, inst, EVENT_CREATE, 0); - } - - return RValue_makeUndefined(); -} - -static RValue builtinInstanceDeactivateAll(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - bool notme = RValue_toBool(args[0]); - - int instances = arrlen(ctx->runner->instances); - repeat(instances, i) { - Instance* instance = ctx->runner->instances[i]; - - if (!notme || instance != ctx->currentInstance) { - instance->active = false; - } - } - return RValue_makeUndefined(); -} - -static RValue builtinInstanceActivateAll(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - int instances = arrlen(ctx->runner->instances); - repeat(instances, i) { - Instance* instance = ctx->runner->instances[i]; - if (!instance->destroyed) - ctx->runner->instances[i]->active = true; - } - return RValue_makeUndefined(); -} - -static RValue builtinInstanceActivateObject(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t objIndex = RValue_toInt32(args[0]); - - // Per-object buckets retain inactive (deactivated) instances since we only remove on destroy-cleanup, so this still finds them. INSTANCE_ALL falls back to the full instances list. - int32_t snapBase = Runner_pushInstancesForTarget(runner, objIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* instance = runner->instanceSnapshots[i]; - if (!instance->active && !instance->destroyed) instance->active = true; - } - Runner_popInstanceSnapshot(runner, snapBase); - return RValue_makeUndefined(); -} - -static RValue builtinInstanceDeactivateObject(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t objIndex = RValue_toInt32(args[0]); - - int32_t snapBase = Runner_pushInstancesForTarget(runner, objIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* instance = runner->instanceSnapshots[i]; - if (instance->active && !instance->destroyed) instance->active = false; - } - Runner_popInstanceSnapshot(runner, snapBase); - return RValue_makeUndefined(); -} - -static RValue builtinEventInherited(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr || 0 > ctx->currentEventObjectIndex || 0 > ctx->currentEventType) { - fprintf(stderr, "VM: event_inherited called with no event context (inst=%p, eventObjIdx=%d, eventType=%d)\n", (void*) inst, ctx->currentEventObjectIndex, ctx->currentEventType); - return RValue_makeReal(0.0); - } - - DataWin* dataWin = ctx->dataWin; - int32_t ownerObjectIndex = ctx->currentEventObjectIndex; - if ((uint32_t) ownerObjectIndex >= dataWin->objt.count) { - fprintf(stderr, "VM: event_inherited ownerObjectIndex %d out of range\n", ownerObjectIndex); - return RValue_makeReal(0.0); - } - - int32_t parentObjectIndex = dataWin->objt.objects[ownerObjectIndex].parentId; - if (ctx->traceEventInherited) { - fprintf(stderr, "VM: [%s] event_inherited owner=%s(%d) parent=%s(%d) event=%s (instanceId=%d)\n", dataWin->objt.objects[inst->objectIndex].name, dataWin->objt.objects[ownerObjectIndex].name, ownerObjectIndex, (0 > parentObjectIndex) ? "none" : dataWin->objt.objects[parentObjectIndex].name, parentObjectIndex, Runner_getEventName(ctx->currentEventType, ctx->currentEventSubtype), inst->instanceId); - } - if (0 > parentObjectIndex) return RValue_makeReal(0.0); - - Runner_executeEventFromObject(runner, inst, parentObjectIndex, ctx->currentEventType, ctx->currentEventSubtype); - return RValue_makeReal(0.0); -} - -static RValue builtinEventUser(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr) return RValue_makeReal(0.0); - - int32_t subevent = RValue_toInt32(args[0]); - if (0 > subevent || 15 < subevent) return RValue_makeReal(0.0); - - Runner_executeEvent(runner, inst, EVENT_OTHER, OTHER_USER0 + subevent); - return RValue_makeReal(0.0); -} - -static RValue builtinEventPerform(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr) return RValue_makeReal(0.0); - - int32_t eventType = RValue_toInt32(args[0]); - int32_t eventSubtype = RValue_toInt32(args[1]); - - Runner_executeEvent(runner, inst, eventType, eventSubtype); - return RValue_makeReal(0.0); -} - -static RValue builtinActionKillObject(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (ctx->currentInstance != nullptr) { - Runner_destroyInstance(runner, (Instance*) ctx->currentInstance); - } - return RValue_makeUndefined(); -} - -static RValue builtinActionCreateObject(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t objectIndex = RValue_toInt32(args[0]); - GMLReal x = RValue_toReal(args[1]); - GMLReal y = RValue_toReal(args[2]); - if (0 > objectIndex || runner->dataWin->objt.count <= (uint32_t) objectIndex) { - fprintf(stderr, "VM: action_create_object: objectIndex %d out of range\n", objectIndex); - return RValue_makeUndefined(); - } - Instance* callerInst = (Instance*) ctx->currentInstance; - if (ctx->actionRelativeFlag && callerInst != nullptr) { - x += callerInst->x; - y += callerInst->y; - } - Instance* inst = Runner_createInstance(runner, x, y, objectIndex); - if (callerInst != nullptr && ctx->creatorVarID >= 0) { - Instance_setSelfVar(inst, ctx->creatorVarID, RValue_makeReal((GMLReal) callerInst->instanceId)); - } - return RValue_makeUndefined(); -} - -static RValue builtinActionSetRelative(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - ctx->actionRelativeFlag = RValue_toInt32(args[0]) != 0; - return RValue_makeUndefined(); -} - -static RValue builtinActionMove(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - // action_move(direction_string, speed) - // Direction string is 9 chars of '0'/'1' encoding a 3x3 direction grid: - // Pos: 0=UL(225) 1=U(270) 2=UR(315) 3=L(180) 4=STOP 5=R(0) 6=DL(135) 7=D(90) 8=DR(45) - char* dirs = RValue_toString(args[0]); - GMLReal spd = RValue_toReal(args[1]); - - static const GMLReal angles[] = {225, 270, 315, 180, -1, 0, 135, 90, 45}; - - // Collect all enabled directions - int candidates[9]; - int count = 0; - for (int i = 0; 9 > i && dirs[i] != '\0'; i++) { - if (dirs[i] == '1') { - candidates[count++] = i; - } - } - - if (count == 0) { - free(dirs); - return RValue_makeUndefined(); - } - - // Pick one at random - int pick = candidates[0 == count - 1 ? 0 : rand() % count]; - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - if (4 == pick) { - // STOP - if (ctx->actionRelativeFlag) { - inst->speed += (float) spd; - } else { - inst->speed = 0; - } - } else { - GMLReal angle = angles[pick]; - if (ctx->actionRelativeFlag) { - inst->direction += (float) angle; - inst->speed += (float) spd; - } else { - inst->direction = (float) angle; - inst->speed = (float) spd; - } - } - Instance_computeComponentsFromSpeed(inst); - } - free(dirs); - return RValue_makeUndefined(); -} - -static RValue builtinActionMoveTo(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal ax = RValue_toReal(args[0]); - GMLReal ay = RValue_toReal(args[1]); - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - if (ctx->actionRelativeFlag) { - inst->x += (float) ax; - inst->y += (float) ay; - } else { - inst->x = (float) ax; - inst->y = (float) ay; - } - SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); - } - return RValue_makeUndefined(); -} - -static RValue builtinActionSnap(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal hsnap = RValue_toReal(args[0]); - GMLReal vsnap = RValue_toReal(args[1]); - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - if (hsnap > 0.0) { - inst->x = (float) ((int32_t) GMLReal_round(inst->x / hsnap) * hsnap); - SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); - } - if (vsnap > 0.0) { - inst->y = (float) ((int32_t) GMLReal_round(inst->y / vsnap) * vsnap); - SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); - } - } - return RValue_makeUndefined(); -} - -static RValue builtinActionSetFriction(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal val = RValue_toReal(args[0]); - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - if (ctx->actionRelativeFlag) { - inst->friction += (float) val; - } else { - inst->friction = (float) val; - } - } - return RValue_makeUndefined(); -} - -static RValue builtinActionSetGravity(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal dir = RValue_toReal(args[0]); - GMLReal grav = RValue_toReal(args[1]); - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - if (ctx->actionRelativeFlag) { - inst->gravityDirection += (float) dir; - inst->gravity += (float) grav; - } else { - inst->gravityDirection = (float) dir; - inst->gravity = (float) grav; - } - } - return RValue_makeUndefined(); -} - -static RValue builtinActionSetHspeed(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal val = RValue_toReal(args[0]); - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - if (ctx->actionRelativeFlag) { - inst->hspeed += (float) val; - } else { - inst->hspeed = (float) val; - } - Instance_computeSpeedFromComponents(inst); - } - return RValue_makeUndefined(); -} - -static RValue builtinActionSetVspeed(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - GMLReal val = RValue_toReal(args[0]); - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - if (ctx->actionRelativeFlag) { - inst->vspeed += (float) val; - } else { - inst->vspeed = (float) val; - } - Instance_computeSpeedFromComponents(inst); - } - return RValue_makeUndefined(); -} - -// ===[ GML BUFFER SYSTEM ]=== - -static int32_t gmlBufferCreate(Runner* runner, int32_t size, int32_t type, int32_t alignment) { - GmlBuffer buf = {0}; - buf.size = size > 0 ? size : 1; - buf.data = safeCalloc((size_t) buf.size, 1); - buf.position = 0; - buf.usedSize = (type == GML_BUFFER_GROW) ? 0 : buf.size; - buf.alignment = alignment > 0 ? alignment : 1; - buf.type = type; - buf.isValid = true; - int32_t id = (int32_t) arrlen(runner->gmlBufferPool); - arrput(runner->gmlBufferPool, buf); - return id; -} - -static GmlBuffer* gmlBufferGet(Runner* runner, int32_t id) { - if (0 > id || id >= (int32_t) arrlen(runner->gmlBufferPool)) return nullptr; - GmlBuffer* buf = &runner->gmlBufferPool[id]; - if (!buf->isValid) return nullptr; - return buf; -} - -// Aligns position up to the buffer's alignment boundary -static int32_t gmlBufferAlign(int32_t position, int32_t alignment) { - if (1 >= alignment) return position; - return ((position + alignment - 1) / alignment) * alignment; -} - -// Ensures the grow buffer has at least newSize bytes allocated -static void gmlBufferEnsureSize(GmlBuffer* buf, int32_t newSize) { - if (buf->type != GML_BUFFER_GROW || newSize <= buf->size) return; - // Double or use newSize, whichever is larger - int32_t newAlloc = buf->size * 2; - if (newAlloc < newSize) newAlloc = newSize; - buf->data = safeRealloc(buf->data, (size_t) newAlloc); - memset(buf->data + buf->size, 0, (size_t) (newAlloc - buf->size)); - buf->size = newAlloc; -} - -static RValue builtin_bufferCreate(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t size = RValue_toInt32(args[0]); - int32_t type = RValue_toInt32(args[1]); - int32_t alignment = RValue_toInt32(args[2]); - int32_t id = gmlBufferCreate(runner, size, type, alignment); - return RValue_makeReal((GMLReal) id); -} - -static RValue builtin_bufferDelete(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - GmlBuffer* buf = gmlBufferGet(runner, id); - if (buf != nullptr) { - free(buf->data); - buf->data = nullptr; - buf->isValid = false; - } - return RValue_makeUndefined(); -} - -static RValue builtin_bufferWrite(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - int32_t dataType = RValue_toInt32(args[1]); - GmlBuffer* buf = gmlBufferGet(runner, id); - if (buf == nullptr) return RValue_makeUndefined(); - - switch (dataType) { - case GML_BUFTYPE_U8: - case GML_BUFTYPE_BOOL: { - uint8_t val = (uint8_t) RValue_toInt32(args[2]); - gmlBufferEnsureSize(buf, buf->position + 1); - if (buf->size > buf->position) buf->data[buf->position] = val; - buf->position += 1; - break; - } - case GML_BUFTYPE_S8: { - int8_t val = (int8_t) RValue_toInt32(args[2]); - gmlBufferEnsureSize(buf, buf->position + 1); - if (buf->size > buf->position) buf->data[buf->position] = (uint8_t) val; - buf->position += 1; - break; - } - case GML_BUFTYPE_U16: { - uint16_t val = (uint16_t) RValue_toInt32(args[2]); - gmlBufferEnsureSize(buf, buf->position + 2); - if (buf->position + 2 <= buf->size) { - BinaryUtils_writeUint16(buf->data + buf->position, val); - } - buf->position += 2; - break; - } - case GML_BUFTYPE_S16: { - int16_t val = (int16_t) RValue_toInt32(args[2]); - gmlBufferEnsureSize(buf, buf->position + 2); - if (buf->position + 2 <= buf->size) { - BinaryUtils_writeUint16(buf->data + buf->position, (uint16_t) val); - } - buf->position += 2; - break; - } - case GML_BUFTYPE_U32: - case GML_BUFTYPE_S32: { - int32_t val = RValue_toInt32(args[2]); - gmlBufferEnsureSize(buf, buf->position + 4); - if (buf->position + 4 <= buf->size) { - BinaryUtils_writeUint32(buf->data + buf->position, (uint32_t) val); - } - buf->position += 4; - break; - } - case GML_BUFTYPE_F32: { - float val = (float) RValue_toReal(args[2]); - gmlBufferEnsureSize(buf, buf->position + 4); - if (buf->position + 4 <= buf->size) { - BinaryUtils_writeFloat32(buf->data + buf->position, val); - } - buf->position += 4; - break; - } - case GML_BUFTYPE_F64: { - double val = (double) RValue_toReal(args[2]); - gmlBufferEnsureSize(buf, buf->position + 8); - if (buf->position + 8 <= buf->size) { - BinaryUtils_writeFloat64(buf->data + buf->position, val); - } - buf->position += 8; - break; - } - case GML_BUFTYPE_STRING: { - // Writes string bytes + null terminator - char* str = RValue_toString(args[2]); - int32_t len = (int32_t) strlen(str); - int32_t writeLen = len + 1; // include null terminator - gmlBufferEnsureSize(buf, buf->position + writeLen); - if (buf->position + writeLen <= buf->size) { - memcpy(buf->data + buf->position, str, (size_t) writeLen); - } - buf->position += writeLen; - free(str); - break; - } - case GML_BUFTYPE_TEXT: { - // Writes string bytes WITHOUT null terminator - char* str = RValue_toString(args[2]); - int32_t len = (int32_t) strlen(str); - gmlBufferEnsureSize(buf, buf->position + len); - if (buf->position + len <= buf->size) { - memcpy(buf->data + buf->position, str, (size_t) len); - } - buf->position += len; - free(str); - break; - } - default: - fprintf(stderr, "buffer_write: unsupported data type %d\n", dataType); - break; - } - - buf->position = gmlBufferAlign(buf->position, buf->alignment); - if (buf->type == GML_BUFFER_GROW && buf->position > buf->usedSize) { - buf->usedSize = buf->position; - } - - return RValue_makeUndefined(); -} - -static RValue builtin_bufferRead(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - int32_t dataType = RValue_toInt32(args[1]); - GmlBuffer* buf = gmlBufferGet(runner, id); - if (buf == nullptr) return RValue_makeReal(0.0); - - RValue result = RValue_makeReal(0.0); - - switch (dataType) { - case GML_BUFTYPE_U8: - case GML_BUFTYPE_BOOL: { - if (buf->size > buf->position) { - result = RValue_makeReal((GMLReal) buf->data[buf->position]); - } - buf->position += 1; - break; - } - case GML_BUFTYPE_S8: { - if (buf->size > buf->position) { - result = RValue_makeReal((GMLReal) (int8_t) buf->data[buf->position]); - } - buf->position += 1; - break; - } - case GML_BUFTYPE_U16: { - if (buf->position + 2 <= buf->size) { - uint16_t val = BinaryUtils_readUint16(buf->data + buf->position); - result = RValue_makeReal((GMLReal) val); - } - buf->position += 2; - break; - } - case GML_BUFTYPE_S16: { - if (buf->position + 2 <= buf->size) { - result = RValue_makeReal((GMLReal) BinaryUtils_readInt16(buf->data + buf->position)); - } - buf->position += 2; - break; - } - case GML_BUFTYPE_U32: { - if (buf->position + 4 <= buf->size) { - uint32_t val = BinaryUtils_readUint32(buf->data + buf->position); - result = RValue_makeReal((GMLReal) val); - } - buf->position += 4; - break; - } - case GML_BUFTYPE_S32: { - if (buf->position + 4 <= buf->size) { - result = RValue_makeReal((GMLReal) BinaryUtils_readInt32(buf->data + buf->position)); - } - buf->position += 4; - break; - } - case GML_BUFTYPE_F32: { - if (buf->position + 4 <= buf->size) { - float val = BinaryUtils_readFloat32(buf->data + buf->position); - result = RValue_makeReal((GMLReal) val); - } - buf->position += 4; - break; - } - case GML_BUFTYPE_F64: { - if (buf->position + 8 <= buf->size) { - double val = BinaryUtils_readFloat64(buf->data + buf->position); - result = RValue_makeReal((GMLReal) val); - } - buf->position += 8; - break; - } - case GML_BUFTYPE_STRING: { - // Read until null terminator or end of buffer - int32_t start = buf->position; - while (buf->size > buf->position && buf->data[buf->position] != '\0') { - buf->position++; - } - int32_t len = buf->position - start; - char* str = safeMalloc((size_t) len + 1); - memcpy(str, buf->data + start, (size_t) len); - str[len] = '\0'; - // Skip past the null terminator - if (buf->size > buf->position) buf->position++; - result = RValue_makeOwnedString(str); - break; - } - case GML_BUFTYPE_TEXT: { - // Read all remaining bytes as text (no null terminator delimiter) - int32_t start = buf->position; - int32_t len = buf->size - start; - if (0 > len) len = 0; - char* str = safeMalloc((size_t) len + 1); - if (len > 0) memcpy(str, buf->data + start, (size_t) len); - str[len] = '\0'; - buf->position = buf->size; - result = RValue_makeOwnedString(str); - break; - } - default: - fprintf(stderr, "buffer_read: unsupported data type %d\n", dataType); - break; - } - - buf->position = gmlBufferAlign(buf->position, buf->alignment); - return result; -} - -static RValue builtin_bufferSeek(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - int32_t seekMode = RValue_toInt32(args[1]); - int32_t offset = RValue_toInt32(args[2]); - GmlBuffer* buf = gmlBufferGet(runner, id); - if (buf == nullptr) return RValue_makeUndefined(); - - switch (seekMode) { - case GML_BUFFER_SEEK_START: - buf->position = offset; - break; - case GML_BUFFER_SEEK_RELATIVE: - buf->position += offset; - break; - case GML_BUFFER_SEEK_END: { - int32_t endPos = (buf->type == GML_BUFFER_GROW) ? buf->usedSize : buf->size; - buf->position = endPos + offset; - break; - } - } - - // Clamp position - if (0 > buf->position) buf->position = 0; - if (buf->position > buf->size) buf->position = buf->size; - - return RValue_makeUndefined(); -} - -static RValue builtin_bufferTell(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - GmlBuffer* buf = gmlBufferGet(runner, id); - if (buf == nullptr) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) buf->position); -} - -static RValue builtin_bufferGetSize(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - GmlBuffer* buf = gmlBufferGet(runner, id); - if (buf == nullptr) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) ((buf->type == GML_BUFFER_GROW) ? buf->usedSize : buf->size)); -} - -static RValue builtin_bufferLoad(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - FileSystem* fs = runner->fileSystem; - char* filename = RValue_toString(args[0]); - - uint8_t* fileData = nullptr; - int32_t fileSize = 0; - bool ok = fs->vtable->readFileBinary(fs, filename, &fileData, &fileSize); - free(filename); - - if (!ok) return RValue_makeReal(-1.0); - - // Create a fixed buffer with the loaded data - int32_t id = gmlBufferCreate(runner, fileSize, GML_BUFFER_FIXED, 1); - GmlBuffer* buf = gmlBufferGet(runner, id); - free(buf->data); - buf->data = fileData; - buf->size = fileSize; - buf->usedSize = fileSize; - return RValue_makeReal((GMLReal) id); -} - -static RValue builtin_bufferSave(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - FileSystem* fs = runner->fileSystem; - int32_t id = RValue_toInt32(args[0]); - char* filename = RValue_toString(args[1]); - GmlBuffer* buf = gmlBufferGet(runner, id); - - if (buf != nullptr) { - int32_t saveSize = (buf->type == GML_BUFFER_GROW) ? buf->usedSize : buf->size; - fs->vtable->writeFileBinary(fs, filename, buf->data, saveSize); - } - - free(filename); - return RValue_makeUndefined(); -} - -STUB_RETURN_ZERO(buffer_base64_encode) - -// PSN stubs -STUB_RETURN_UNDEFINED(psn_init) -STUB_RETURN_ZERO(psn_default_user) -STUB_RETURN_ZERO(psn_get_leaderboard_score) - -// Draw functions -static RValue builtin_drawSprite(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - float x = (float) RValue_toReal(args[2]); - float y = (float) RValue_toReal(args[3]); - - // If subimg < 0, use the current instance's imageIndex - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - Renderer_drawSprite(runner->renderer, spriteIndex, subimg, x, y); - return RValue_makeUndefined(); -} - -static RValue builtin_drawSpriteExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - float x = (float) RValue_toReal(args[2]); - float y = (float) RValue_toReal(args[3]); - float xscale = (float) RValue_toReal(args[4]); - float yscale = (float) RValue_toReal(args[5]); - float rot = (float) RValue_toReal(args[6]); - uint32_t color = (uint32_t) RValue_toInt32(args[7]); - float alpha = (float) RValue_toReal(args[8]); - - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - Renderer_drawSpriteExt(runner->renderer, spriteIndex, subimg, x, y, xscale, yscale, rot, color, alpha); - return RValue_makeUndefined(); -} - -static RValue builtin_drawSpriteTiled(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - float x = (float) RValue_toReal(args[2]); - float y = (float) RValue_toReal(args[3]); - - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - float roomW = (float) runner->currentRoom->width; - float roomH = (float) runner->currentRoom->height; - Renderer_drawSpriteTiled(runner->renderer, spriteIndex, subimg, x, y, 1.0f, 1.0f, roomW, roomH, 0xFFFFFF, runner->renderer->drawAlpha); - return RValue_makeUndefined(); -} - -static RValue builtin_drawSpriteTiledExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - float x = (float) RValue_toReal(args[2]); - float y = (float) RValue_toReal(args[3]); - float xscale = (float) RValue_toReal(args[4]); - float yscale = (float) RValue_toReal(args[5]); - uint32_t color = (uint32_t) RValue_toInt32(args[6]); - float alpha = (float) RValue_toReal(args[7]); - - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - float roomW = (float) runner->currentRoom->width; - float roomH = (float) runner->currentRoom->height; - Renderer_drawSpriteTiled(runner->renderer, spriteIndex, subimg, x, y, xscale, yscale, roomW, roomH, color, alpha); - return RValue_makeUndefined(); -} - -static RValue builtin_drawSpriteStretched(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - float x = (float) RValue_toReal(args[2]); - float y = (float) RValue_toReal(args[3]); - float w = (float) RValue_toReal(args[4]); - float h = (float) RValue_toReal(args[5]); - - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - Renderer_drawSpriteStretched(runner->renderer, spriteIndex, subimg, x, y, w, h, 0xFFFFFF, runner->renderer->drawAlpha); - return RValue_makeUndefined(); -} - -static RValue builtin_drawSpriteStretchedExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - float x = (float) RValue_toReal(args[2]); - float y = (float) RValue_toReal(args[3]); - float w = (float) RValue_toReal(args[4]); - float h = (float) RValue_toReal(args[5]); - uint32_t color = (uint32_t) RValue_toInt32(args[6]); - float alpha = (float) RValue_toReal(args[7]); - - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - Renderer_drawSpriteStretched(runner->renderer, spriteIndex, subimg, x, y, w, h, color, alpha); - return RValue_makeUndefined(); -} - -static RValue builtin_drawSpritePart(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - int32_t left = RValue_toInt32(args[2]); - int32_t top = RValue_toInt32(args[3]); - int32_t width = RValue_toInt32(args[4]); - int32_t height = RValue_toInt32(args[5]); - float x = (float) RValue_toReal(args[6]); - float y = (float) RValue_toReal(args[7]); - - // If subimg < 0, use the current instance's imageIndex - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - Renderer_drawSpritePart(runner->renderer, spriteIndex, subimg, left, top, width, height, x, y); - return RValue_makeUndefined(); -} - -static RValue builtin_drawSpritePartExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - int32_t left = RValue_toInt32(args[2]); - int32_t top = RValue_toInt32(args[3]); - int32_t width = RValue_toInt32(args[4]); - int32_t height = RValue_toInt32(args[5]); - float x = (float) RValue_toReal(args[6]); - float y = (float) RValue_toReal(args[7]); - float xscale = (float) RValue_toReal(args[8]); - float yscale = (float) RValue_toReal(args[9]); - uint32_t color = (uint32_t) RValue_toInt32(args[10]); - float alpha = (float) RValue_toReal(args[11]); - - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - Renderer_drawSpritePartExt(runner->renderer, spriteIndex, subimg, left, top, width, height, x, y, xscale, yscale, 0.0f, 0.0f, 0.0f, color, alpha); - return RValue_makeUndefined(); -} - -static RValue builtin_drawSpriteGeneral(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - logSemiStubbedFunction(ctx, "draw_sprite_general"); - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - int32_t left = RValue_toInt32(args[2]); - int32_t top = RValue_toInt32(args[3]); - int32_t width = RValue_toInt32(args[4]); - int32_t height = RValue_toInt32(args[5]); - float x = (float) RValue_toReal(args[6]); - float y = (float) RValue_toReal(args[7]); - float xscale = (float) RValue_toReal(args[8]); - float yscale = (float) RValue_toReal(args[9]); - float rot = (float) RValue_toReal(args[10]); - uint32_t c1 = (uint32_t) RValue_toInt32(args[11]); - uint32_t c2 = (uint32_t) RValue_toInt32(args[12]); - uint32_t c3 = (uint32_t) RValue_toInt32(args[13]); - uint32_t c4 = (uint32_t) RValue_toInt32(args[14]); - float alpha = (float) RValue_toReal(args[15]); - - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - Renderer_drawSpritePartExt(runner->renderer, spriteIndex, subimg, left, top, width, height, x, y, xscale, yscale, rot, x, y, c1, alpha); - return RValue_makeUndefined(); -} - - -static RValue builtin_drawSpritePos(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t subimg = RValue_toInt32(args[1]); - float x1 = (float) RValue_toReal(args[2]); - float y1 = (float) RValue_toReal(args[3]); - float x2 = (float) RValue_toReal(args[4]); - float y2 = (float) RValue_toReal(args[5]); - float x3 = (float) RValue_toReal(args[6]); - float y3 = (float) RValue_toReal(args[7]); - float x4 = (float) RValue_toReal(args[8]); - float y4 = (float) RValue_toReal(args[9]); - float alpha = (float) RValue_toReal(args[10]); - - if (0 > subimg && ctx->currentInstance != nullptr) { - subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; - } - - Renderer_drawSpritePos(runner->renderer, spriteIndex, subimg, x1, y1, x2, y2, x3, y3, x4, y4, alpha); - - return RValue_makeUndefined(); -} - -static RValue builtin_drawRectangle(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x1 = (float) RValue_toReal(args[0]); - float y1 = (float) RValue_toReal(args[1]); - float x2 = (float) RValue_toReal(args[2]); - float y2 = (float) RValue_toReal(args[3]); - bool outline = RValue_toBool(args[4]); - - runner->renderer->vtable->drawRectangle(runner->renderer, x1, y1, x2, y2, runner->renderer->drawColor, runner->renderer->drawAlpha, outline); - return RValue_makeUndefined(); -} - -static RValue builtin_drawRectangleColor(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x1 = (float) RValue_toReal(args[0]); - float y1 = (float) RValue_toReal(args[1]); - float x2 = (float) RValue_toReal(args[2]); - float y2 = (float) RValue_toReal(args[3]); - uint32_t color = (uint32_t) RValue_toInt32(args[4]); - bool outline = RValue_toBool(args[8]); - - runner->renderer->vtable->drawRectangle(runner->renderer, x1, y1, x2, y2, color, runner->renderer->drawAlpha, outline); - return RValue_makeUndefined(); -} - -static RValue builtin_drawHealthbar(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x1 = (float) RValue_toReal(args[0]); - float y1 = (float) RValue_toReal(args[1]); - float x2 = (float) RValue_toReal(args[2]); - float y2 = (float) RValue_toReal(args[3]); - float amount = (float) RValue_toReal(args[4]); - - amount = amount / (float)100; // 0 - 1; - float healthbarX = (x1 * (1-amount) + x2 * amount); - //float healthbarY = (y1 * (1-amount) + y2 * amount); - - uint32_t backCol = (uint32_t) RValue_toInt32(args[5]); - uint32_t minCol = (uint32_t) RValue_toInt32(args[6]); - uint32_t maxCol = (uint32_t) RValue_toInt32(args[7]); - uint32_t intermediateColor = Renderer_mixColors(minCol,maxCol,amount); - - int32_t direction = RValue_toInt32(args[8]); - - bool showBack = RValue_toBool(args[9]); - - if (showBack) { - runner->renderer->vtable->drawRectangle(runner->renderer, x1,y1,x2,y2,backCol, runner->renderer->drawAlpha, false); - } - - runner->renderer->vtable->drawRectangle(runner->renderer,x1,y1,healthbarX,y2,intermediateColor, runner->renderer->drawAlpha, false); -} - -static RValue builtin_drawSetColor(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - runner->renderer->drawColor = (uint32_t) RValue_toInt32(args[0]); - } - return RValue_makeUndefined(); -} - -static RValue builtin_drawSetAlpha(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - runner->renderer->drawAlpha = (float) RValue_toReal(args[0]); - } - return RValue_makeUndefined(); -} - -static RValue builtin_drawSetFont(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - runner->renderer->drawFont = RValue_toInt32(args[0]); - } - return RValue_makeUndefined(); -} - -static RValue builtin_drawSetHalign(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - runner->renderer->drawHalign = RValue_toInt32(args[0]); - } - return RValue_makeUndefined(); -} - -static RValue builtin_drawSetValign(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - runner->renderer->drawValign = RValue_toInt32(args[0]); - } - return RValue_makeUndefined(); -} - -static RValue builtin_drawText(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x = (float) RValue_toReal(args[0]); - float y = (float) RValue_toReal(args[1]); - char* str = RValue_toString(args[2]); - - PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); - runner->renderer->vtable->drawText(runner->renderer, processedText.text, x, y, 1.0f, 1.0f, 0.0f); - PreprocessedText_free(processedText); - free(str); - return RValue_makeUndefined(); -} - -static RValue builtin_drawTextTransformed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x = (float) RValue_toReal(args[0]); - float y = (float) RValue_toReal(args[1]); - char* str = RValue_toString(args[2]); - float xscale = (float) RValue_toReal(args[3]); - float yscale = (float) RValue_toReal(args[4]); - float angle = (float) RValue_toReal(args[5]); - - PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); - runner->renderer->vtable->drawText(runner->renderer, processedText.text, x, y, xscale, yscale, angle); - PreprocessedText_free(processedText); - free(str); - return RValue_makeUndefined(); -} - -static RValue builtin_drawTextExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - logSemiStubbedFunction(ctx, "draw_text_ext"); - - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x = (float) RValue_toReal(args[0]); - float y = (float) RValue_toReal(args[1]); - char* str = RValue_toString(args[2]); - int32_t separation = RValue_toInt32(args[3]); - int32_t width = RValue_toInt32(args[4]); - - PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); - runner->renderer->vtable->drawText(runner->renderer, processedText.text, x, y, 1.0f, 1.0f, 0.0f); - PreprocessedText_free(processedText); - free(str); - return RValue_makeUndefined(); -} - -static RValue builtin_drawTextExtTransformed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - logSemiStubbedFunction(ctx, "draw_text_ext_transformed"); - - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x = (float) RValue_toReal(args[0]); - float y = (float) RValue_toReal(args[1]); - char* str = RValue_toString(args[2]); - int32_t separation = RValue_toInt32(args[3]); - int32_t width = RValue_toInt32(args[4]); - float xscale = (float) RValue_toReal(args[5]); - float yscale = (float) RValue_toReal(args[6]); - float angle = (float) RValue_toReal(args[7]); - - PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); - runner->renderer->vtable->drawText(runner->renderer, processedText.text, x, y, xscale, yscale, angle); - PreprocessedText_free(processedText); - free(str); - return RValue_makeUndefined(); -} - -static RValue builtin_drawTextColor(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x = (float) RValue_toReal(args[0]); - float y = (float) RValue_toReal(args[1]); - char* str = RValue_toString(args[2]); - int32_t c1 = (float) RValue_toInt32(args[3]); - int32_t c2 = (float) RValue_toInt32(args[4]); - int32_t c3 = (float) RValue_toInt32(args[5]); - int32_t c4 = (float) RValue_toInt32(args[6]); - float alpha = (float) RValue_toReal(args[7]); - - PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); - runner->renderer->vtable->drawTextColor(runner->renderer, processedText.text, x, y, 1.0f, 1.0f, 0.0f, c1, c2, c3, c4, alpha); - PreprocessedText_free(processedText); - free(str); - return RValue_makeUndefined(); -} - -static RValue builtin_drawTextColorTransformed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x = (float) RValue_toReal(args[0]); - float y = (float) RValue_toReal(args[1]); - char* str = RValue_toString(args[2]); - float xscale = (float) RValue_toReal(args[3]); - float yscale = (float) RValue_toReal(args[4]); - float angle = (float) RValue_toReal(args[5]); - int32_t c1 = (float) RValue_toInt32(args[6]); - int32_t c2 = (float) RValue_toInt32(args[7]); - int32_t c3 = (float) RValue_toInt32(args[8]); - int32_t c4 = (float) RValue_toInt32(args[9]); - float alpha = (float) RValue_toReal(args[10]); - - PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); - runner->renderer->vtable->drawTextColor(runner->renderer, processedText.text, x, y, xscale, yscale, angle, c1, c2, c3, c4, alpha); - PreprocessedText_free(processedText); - free(str); - return RValue_makeUndefined(); -} - -static RValue builtin_drawTextColorExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - logSemiStubbedFunction(ctx, "draw_text_color_ext"); - - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x = (float) RValue_toReal(args[0]); - float y = (float) RValue_toReal(args[1]); - char* str = RValue_toString(args[2]); - int32_t c1 = (float) RValue_toInt32(args[5]); - int32_t c2 = (float) RValue_toInt32(args[6]); - int32_t c3 = (float) RValue_toInt32(args[7]); - int32_t c4 = (float) RValue_toInt32(args[8]); - float alpha = (float) RValue_toReal(args[9]); - - PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); - runner->renderer->vtable->drawTextColor(runner->renderer, processedText.text, x, y, 1.0f, 1.0f, 0.0f, c1, c2, c3, c4, alpha); - PreprocessedText_free(processedText); - free(str); - return RValue_makeUndefined(); -} - -static RValue builtin_drawTextColorExtTransformed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - logSemiStubbedFunction(ctx, "draw_text_color_ext_transformed"); - - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr) return RValue_makeUndefined(); - - float x = (float) RValue_toReal(args[0]); - float y = (float) RValue_toReal(args[1]); - char* str = RValue_toString(args[2]); - float xscale = (float) RValue_toReal(args[5]); - float yscale = (float) RValue_toReal(args[6]); - float angle = (float) RValue_toReal(args[7]); - int32_t c1 = (float) RValue_toInt32(args[8]); - int32_t c2 = (float) RValue_toInt32(args[9]); - int32_t c3 = (float) RValue_toInt32(args[10]); - int32_t c4 = (float) RValue_toInt32(args[11]); - float alpha = (float) RValue_toReal(args[12]); - - PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); - runner->renderer->vtable->drawTextColor(runner->renderer, processedText.text, x, y, xscale, yscale, angle, c1, c2, c3, c4, alpha); - PreprocessedText_free(processedText); - free(str); - return RValue_makeUndefined(); -} - -STUB_RETURN_UNDEFINED(draw_surface) -STUB_RETURN_UNDEFINED(draw_surface_ext) -static RValue builtin_drawBackground(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr || 3 > argCount) return RValue_makeUndefined(); - - int32_t bgIndex = RValue_toInt32(args[0]); - float x = (float) RValue_toReal(args[1]); - float y = (float) RValue_toReal(args[2]); - - int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, bgIndex); - if (0 > tpagIndex) return RValue_makeUndefined(); - - runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, x, y, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0xFFFFFF, runner->renderer->drawAlpha); - return RValue_makeUndefined(); -} - -static RValue builtin_drawBackgroundExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr || 8 > argCount) return RValue_makeUndefined(); - - int32_t bgIndex = RValue_toInt32(args[0]); - float x = (float) RValue_toReal(args[1]); - float y = (float) RValue_toReal(args[2]); - float xscale = (float) RValue_toReal(args[3]); - float yscale = (float) RValue_toReal(args[4]); - float rot = (float) RValue_toReal(args[5]); - uint32_t color = (uint32_t) RValue_toInt32(args[6]); - float alpha = (float) RValue_toReal(args[7]); - - int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, bgIndex); - if (0 > tpagIndex) return RValue_makeUndefined(); - - runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, x, y, 0.0f, 0.0f, xscale, yscale, rot, color, alpha); - return RValue_makeUndefined(); -} - -static RValue builtin_drawBackgroundStretched(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr || 5 > argCount) return RValue_makeUndefined(); - - int32_t bgIndex = RValue_toInt32(args[0]); - float x = (float) RValue_toReal(args[1]); - float y = (float) RValue_toReal(args[2]); - float w = (float) RValue_toReal(args[3]); - float h = (float) RValue_toReal(args[4]); - - int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, bgIndex); - if (0 > tpagIndex) return RValue_makeUndefined(); - - TexturePageItem* tpag = &runner->dataWin->tpag.items[tpagIndex]; - float xscale = w / (float) tpag->boundingWidth; - float yscale = h / (float) tpag->boundingHeight; - - runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, x, y, 0.0f, 0.0f, xscale, yscale, 0.0f, 0xFFFFFF, runner->renderer->drawAlpha); - return RValue_makeUndefined(); -} - -static RValue builtin_drawBackgroundPartExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr || 11 > argCount) return RValue_makeUndefined(); - - int32_t bgIndex = RValue_toInt32(args[0]); - int32_t left = RValue_toInt32(args[1]); - int32_t top = RValue_toInt32(args[2]); - int32_t width = RValue_toInt32(args[3]); - int32_t height = RValue_toInt32(args[4]); - float x = (float) RValue_toReal(args[5]); - float y = (float) RValue_toReal(args[6]); - float xscale = (float) RValue_toReal(args[7]); - float yscale = (float) RValue_toReal(args[8]); - uint32_t color = (uint32_t) RValue_toInt32(args[9]); - float alpha = (float) RValue_toReal(args[10]); - - int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, bgIndex); - if (0 > tpagIndex) return RValue_makeUndefined(); - - runner->renderer->vtable->drawSpritePart(runner->renderer, tpagIndex, left, top, width, height, x, y, xscale, yscale, 0.0f, 0.0f, 0.0f, color, alpha); - return RValue_makeUndefined(); -} - -static RValue builtinBackgroundGetWidth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - int32_t bgIndex = RValue_toInt32(args[0]); - int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(ctx->dataWin, bgIndex); - if (0 > tpagIndex) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) ctx->dataWin->tpag.items[tpagIndex].boundingWidth); -} - -static RValue builtinBackgroundGetHeight(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - int32_t bgIndex = RValue_toInt32(args[0]); - int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(ctx->dataWin, bgIndex); - if (0 > tpagIndex) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) ctx->dataWin->tpag.items[tpagIndex].boundingHeight); -} - -static RValue builtin_draw_self(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr && ctx->currentInstance != nullptr) { - Renderer_drawSelf(runner->renderer, (Instance*) ctx->currentInstance); - } - return RValue_makeUndefined(); -} - -// draw_line(x1, y1, x2, y2) -static RValue builtin_draw_line(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - float x1 = (float) RValue_toReal(args[0]); - float y1 = (float) RValue_toReal(args[1]); - float x2 = (float) RValue_toReal(args[2]); - float y2 = (float) RValue_toReal(args[3]); - runner->renderer->vtable->drawLine(runner->renderer, x1, y1, x2, y2, 1.0f, runner->renderer->drawColor, runner->renderer->drawAlpha); - } - return RValue_makeUndefined(); -} - -// draw_line_width(x1, y1, x2, y2, w) -static RValue builtin_draw_line_width(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - float x1 = (float) RValue_toReal(args[0]); - float y1 = (float) RValue_toReal(args[1]); - float x2 = (float) RValue_toReal(args[2]); - float y2 = (float) RValue_toReal(args[3]); - float w = (float) RValue_toReal(args[4]); - runner->renderer->vtable->drawLine(runner->renderer, x1, y1, x2, y2, w, runner->renderer->drawColor, runner->renderer->drawAlpha); - } - return RValue_makeUndefined(); -} - -// draw_line_width_colour(x1, y1, x2, y2, w, col1, col2) -static RValue builtin_draw_line_width_colour(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - float x1 = (float) RValue_toReal(args[0]); - float y1 = (float) RValue_toReal(args[1]); - float x2 = (float) RValue_toReal(args[2]); - float y2 = (float) RValue_toReal(args[3]); - float w = (float) RValue_toReal(args[4]); - uint32_t col1 = (uint32_t) RValue_toInt32(args[5]); - uint32_t col2 = (uint32_t) RValue_toInt32(args[6]); - runner->renderer->vtable->drawLineColor(runner->renderer, x1, y1, x2, y2, w, col1, col2, runner->renderer->drawAlpha); - } - return RValue_makeUndefined(); -} - -// draw_triangle(x1, y1, x2, y2, x3, y3, outline) -static RValue builtin_draw_triangle(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - float x1 = (float) RValue_toReal(args[0]); - float y1 = (float) RValue_toReal(args[1]); - float x2 = (float) RValue_toReal(args[2]); - float y2 = (float) RValue_toReal(args[3]); - float x3 = (float) RValue_toReal(args[4]); - float y3 = (float) RValue_toReal(args[5]); - bool outline = (float) RValue_toBool(args[6]); - runner->renderer->vtable->drawTriangle(runner->renderer, x1, y1, x2, y2, x3, y3, outline); - } - return RValue_makeUndefined(); -} - -static RValue builtin_draw_set_colour(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - runner->renderer->drawColor = (uint32_t) RValue_toInt32(args[0]); - } - return RValue_makeUndefined(); -} - -static RValue builtin_draw_get_colour(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - return RValue_makeReal((GMLReal) runner->renderer->drawColor); - } - return RValue_makeReal(0.0); -} - -static RValue builtin_draw_get_color(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - return RValue_makeReal((GMLReal) runner->renderer->drawColor); - } - return RValue_makeReal(0.0); -} - -static RValue builtin_draw_get_alpha(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer != nullptr) { - return RValue_makeReal((GMLReal) runner->renderer->drawAlpha); - } - return RValue_makeReal(0.0); -} - -// merge_color(col1, col2, amount) - lerps between two colors -static RValue builtinMergeColor(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t col1 = RValue_toInt32(args[0]); - int32_t col2 = RValue_toInt32(args[1]); - GMLReal amount = RValue_toReal(args[2]); - - int32_t b1 = (col1 >> 16) & 0xFF; - int32_t g1 = (col1 >> 8) & 0xFF; - int32_t r1 = col1 & 0xFF; - - int32_t b2 = (col2 >> 16) & 0xFF; - int32_t g2 = (col2 >> 8) & 0xFF; - int32_t r2 = col2 & 0xFF; - - GMLReal inv = 1.0 - amount; - int32_t r = (int32_t) (r1 * inv + r2 * amount); - int32_t g = (int32_t) (g1 * inv + g2 * amount); - int32_t b = (int32_t) (b1 * inv + b2 * amount); - - return RValue_makeReal((GMLReal) (((b << 16) & 0xFF0000) | ((g << 8) & 0xFF00) | (r & 0xFF))); -} - -// Surface stubs -STUB_RETURN_ZERO(surface_create) -STUB_RETURN_UNDEFINED(surface_free) -STUB_RETURN_UNDEFINED(surface_set_target) -STUB_RETURN_UNDEFINED(surface_reset_target) -STUB_RETURN_ZERO(surface_exists) -// application_surface is surface ID -1 (sentinel); for it, return the window dimensions -static RValue builtinSurfaceGetWidth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t surfaceId = (int32_t) RValue_toReal(args[0]); - if (surfaceId == -1) { - return RValue_makeReal((GMLReal) ctx->dataWin->gen8.defaultWindowWidth); - } - logStubbedFunction(ctx, "surface_get_width"); - return RValue_makeReal(0.0); -} - -static RValue builtinSurfaceGetHeight(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t surfaceId = (int32_t) RValue_toReal(args[0]); - if (surfaceId == -1) { - return RValue_makeReal((GMLReal) ctx->dataWin->gen8.defaultWindowHeight); - } - logStubbedFunction(ctx, "surface_get_height"); - return RValue_makeReal(0.0); -} - -// Sprite functions -static RValue builtin_spriteAdd(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - logStubbedFunction(ctx, "sprite_add"); - // Return 1, so that a sprite_exists check passes - return RValue_makeInt32(1); -} - -static RValue builtin_spriteExists(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (args[0].type == RVALUE_UNDEFINED) return RValue_makeBool(false); - int32_t spriteIndex = RValue_toInt32(args[0]); - if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeBool(false); - return RValue_makeBool(true); -} - -static RValue builtin_spriteGetWidth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); - if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].width); -} - -static RValue builtin_spriteGetHeight(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); - if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].height); -} - -static RValue builtin_spriteGetNumber(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); - if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].textureCount); -} - -static RValue builtin_spriteGetXOffset(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); - if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].originX); -} - -static RValue builtin_spriteGetYOffset(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); - if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].originY); -} - -static RValue builtin_spriteGetName(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); - if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeString(""); - const char* name = ctx->dataWin->sprt.sprites[spriteIndex].name; - return RValue_makeString(name != nullptr ? name : ""); -} - -// sprite_set_offset(sprite_index, xoff, yoff) -static RValue builtin_spriteSetOffset(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); - if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); - ctx->dataWin->sprt.sprites[spriteIndex].originX = (int32_t) RValue_toReal(args[1]); - ctx->dataWin->sprt.sprites[spriteIndex].originY = (int32_t) RValue_toReal(args[2]); - return RValue_makeReal(0.0); -} - -// sprite_create_from_surface(surface_id, x, y, w, h, removeback, smooth, xorig, yorig) -static RValue builtin_spriteCreateFromSurface(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr || runner->renderer->vtable->createSpriteFromSurface == nullptr) return RValue_makeReal(-1); - - // surface_id (arg0) is ignored - we always capture from the application surface (FBO) - int32_t x = RValue_toInt32(args[1]); - int32_t y = RValue_toInt32(args[2]); - int32_t w = RValue_toInt32(args[3]); - int32_t h = RValue_toInt32(args[4]); - bool removeback = RValue_toBool(args[5]); - bool smooth = RValue_toBool(args[6]); - int32_t xorig = RValue_toInt32(args[7]); - int32_t yorig = RValue_toInt32(args[8]); - - int32_t result = runner->renderer->vtable->createSpriteFromSurface(runner->renderer, x, y, w, h, removeback, smooth, xorig, yorig); - return RValue_makeReal((GMLReal) result); -} - -// sprite_delete(sprite_index) -static RValue builtin_spriteDelete(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - if (runner->renderer == nullptr || runner->renderer->vtable->deleteSprite == nullptr) return RValue_makeUndefined(); - - int32_t spriteIndex = RValue_toInt32(args[0]); - runner->renderer->vtable->deleteSprite(runner->renderer, spriteIndex); - return RValue_makeUndefined(); -} - -// Font/text measurement -static RValue builtin_stringWidth(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - Renderer* renderer = runner->renderer; - int32_t fontIndex = renderer->drawFont; - if (0 > fontIndex || renderer->dataWin->font.count <= (uint32_t) fontIndex) return RValue_makeReal(0.0); - - Font* font = &renderer->dataWin->font.fonts[fontIndex]; - char* str = RValue_toString(args[0]); - - PreprocessedText processed = TextUtils_preprocessGmlTextIfNeeded(runner, str); - int32_t textLen = (int32_t) strlen(processed.text); - - // Find the widest line - float maxWidth = 0; - int32_t lineStart = 0; - while (textLen >= lineStart) { - int32_t lineEnd = lineStart; - while (textLen > lineEnd && !TextUtils_isNewlineChar(processed.text[lineEnd])) { - lineEnd++; - } - int32_t lineLen = lineEnd - lineStart; - - float lineWidth = TextUtils_measureLineWidth(font, processed.text + lineStart, lineLen); - if (lineWidth > maxWidth) maxWidth = lineWidth; - - if (textLen > lineEnd) { - lineStart = TextUtils_skipNewline(processed.text, lineEnd, textLen); - } else { - break; - } - } - - PreprocessedText_free(processed); - free(str); - return RValue_makeReal((GMLReal) (maxWidth * font->scaleX)); -} - -static RValue builtin_stringHeight(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - Renderer* renderer = runner->renderer; - int32_t fontIndex = renderer->drawFont; - if (0 > fontIndex || renderer->dataWin->font.count <= (uint32_t) fontIndex) return RValue_makeReal(0.0); - - Font* font = &renderer->dataWin->font.fonts[fontIndex]; - char* str = RValue_toString(args[0]); - - PreprocessedText processed = TextUtils_preprocessGmlTextIfNeeded(runner, str); - int32_t textLen = (int32_t) strlen(processed.text); - int32_t lineCount = TextUtils_countLines(processed.text, textLen); - PreprocessedText_free(processed); - free(str); - - // Match HTML5 runner: string_height = lines * TextHeight('M') = lines * max_glyph_height * scaleY. - return RValue_makeReal((GMLReal) ((float) lineCount * TextUtils_lineStride(font) * font->scaleY)); -} - -STUB_RETURN_ZERO(string_width_ext) -STUB_RETURN_ZERO(string_height_ext) - -// Color functions -static RValue builtinMakeColor(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeReal(0.0); - int32_t r = RValue_toInt32(args[0]); - int32_t g = RValue_toInt32(args[1]); - int32_t b = RValue_toInt32(args[2]); - return RValue_makeReal((GMLReal) (r | (g << 8) | (b << 16))); -} - -static RValue builtinMakeColour(VMContext* ctx, RValue* args, int32_t argCount) { - return builtinMakeColor(ctx, args, argCount); -} - -static RValue builtinMakeColorHsv(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeReal(0.0); - // GML uses 0-255 range for H, S, V - GMLReal h = RValue_toReal(args[0]) / 255.0 * 360.0; - GMLReal s = RValue_toReal(args[1]) / 255.0; - GMLReal v = RValue_toReal(args[2]) / 255.0; - - GMLReal c = v * s; - GMLReal x = c * (1.0 - GMLReal_fabs(GMLReal_fmod(h / 60.0, 2.0) - 1.0)); - GMLReal m = v - c; - - GMLReal r1, g1, b1; - if (360.0 > h && h >= 300.0) { r1 = c; g1 = 0; b1 = x; } - else if (300.0 > h && h >= 240.0) { r1 = x; g1 = 0; b1 = c; } - else if (240.0 > h && h >= 180.0) { r1 = 0; g1 = x; b1 = c; } - else if (180.0 > h && h >= 120.0) { r1 = 0; g1 = c; b1 = x; } - else if (120.0 > h && h >= 60.0) { r1 = x; g1 = c; b1 = 0; } - else { r1 = c; g1 = x; b1 = 0; } - - int32_t r = (int32_t) GMLReal_round((r1 + m) * 255.0); - int32_t g = (int32_t) GMLReal_round((g1 + m) * 255.0); - int32_t b = (int32_t) GMLReal_round((b1 + m) * 255.0); - - return RValue_makeReal((GMLReal) (r | (g << 8) | (b << 16))); -} - -static RValue builtinMakeColourHsv(VMContext* ctx, RValue* args, int32_t argCount) { - return builtinMakeColorHsv(ctx, args, argCount); -} - -// Display stubs -STUB_RETURN_VALUE(display_get_width, 640.0) -STUB_RETURN_VALUE(display_get_height, 480.0) - -static int32_t resolveGuiWidth(Runner* runner) { - if (runner->guiWidth > 0) return runner->guiWidth; - Room* room = runner->currentRoom; - if (room != nullptr) { - repeat(8, vi) { - if (room->views[vi].enabled && room->views[vi].portWidth > 0) { - return room->views[vi].portWidth; - } - } - if (room->width > 0) return (int32_t) room->width; - } - return 320; -} - -static int32_t resolveGuiHeight(Runner* runner) { - if (runner->guiHeight > 0) return runner->guiHeight; - Room* room = runner->currentRoom; - if (room != nullptr) { - repeat(8, vi) { - if (room->views[vi].enabled && room->views[vi].portHeight > 0) { - return room->views[vi].portHeight; - } - } - if (room->height > 0) return (int32_t) room->height; - } - return 240; -} - -static RValue builtinDisplayGetGuiWidth(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - return RValue_makeInt32(resolveGuiWidth(runner)); -} - -static RValue builtinDisplayGetGuiHeight(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - return RValue_makeInt32(resolveGuiHeight(runner)); -} - -static RValue builtinDisplaySetGuiSize(VMContext* ctx, RValue* args, int32_t argCount) { - if (2 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t w = RValue_toInt32(args[0]); - int32_t h = RValue_toInt32(args[1]); - runner->guiWidth = w > 0 ? w : 0; - runner->guiHeight = h > 0 ? h : 0; - return RValue_makeUndefined(); -} - -static RValue builtinDisplaySetGuiMaximise(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - // GMS: display_set_gui_maximise(xscale, yscale, xoffset, yoffset). We don't support scaling yet; reset to auto (match view). - Runner* runner = (Runner*) ctx->runner; - runner->guiWidth = 0; - runner->guiHeight = 0; - return RValue_makeUndefined(); -} - -// place_meeting(x, y, obj) - returns true if the calling instance would collide with obj at position (x, y) -static RValue builtinPlaceMeeting(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeBool(false); - - Runner* runner = (Runner*) ctx->runner; - Instance* caller = (Instance*) ctx->currentInstance; - if (caller == nullptr) return RValue_makeBool(false); - - GMLReal testX = RValue_toReal(args[0]); - GMLReal testY = RValue_toReal(args[1]); - int32_t target = RValue_toInt32(args[2]); - - // Save current position and temporarily move to test position - GMLReal savedX = caller->x; - GMLReal savedY = caller->y; - caller->x = testX; - caller->y = testY; - - InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); - bool found = false; - - SpatialGrid_syncGrid(runner, runner->spatialGrid); - - if (callerBBox.valid) { - SpatialGridQuery query = SpatialGrid_prepareQuery(runner, callerBBox.left, callerBBox.top, callerBBox.right, callerBBox.bottom, target); - - for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx && !found; gx++) { - for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy && !found; gy++) { - Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; - int32_t cellLen = (int32_t) arrlen(cell); - repeat(cellLen, ci) { - Instance* other = cell[ci]; - if (!other->active || other == caller) continue; - if (other->lastCollisionQueryId == query.queryId) continue; - other->lastCollisionQueryId = query.queryId; - - if (query.filterByObject && !VM_isObjectOrDescendant(runner->dataWin, other->objectIndex, target)) continue; - if (query.filterByInstanceId && other->instanceId != (uint32_t) target) continue; - - InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); - if (!otherBBox.valid) continue; - - if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { - found = true; - break; - } - } - } - } - } - - // Restore original position - caller->x = savedX; - caller->y = savedY; - - return RValue_makeBool(found); -} -// collision_line(x1, y1, x2, y2, obj, prec, notme) -static RValue builtinCollisionLine(VMContext* ctx, RValue* args, int32_t argCount) { - if (7 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); - - Runner* runner = (Runner*) ctx->runner; - GMLReal lx1 = RValue_toReal(args[0]); - GMLReal ly1 = RValue_toReal(args[1]); - GMLReal lx2 = RValue_toReal(args[2]); - GMLReal ly2 = RValue_toReal(args[3]); - int32_t targetObjIndex = RValue_toInt32(args[4]); - int32_t prec = RValue_toInt32(args[5]); - int32_t notme = RValue_toInt32(args[6]); - - Instance* self = (Instance*) ctx->currentInstance; - - int32_t resultId = INSTANCE_NOONE; - int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t snapIdx = snapBase; snapEnd > snapIdx; snapIdx++) { - Instance* inst = runner->instanceSnapshots[snapIdx]; - if (!inst->active) continue; - if (notme && inst == self) continue; - - if (!Collision_lineOverlapsInstance(ctx->dataWin, inst, lx1, ly1, lx2, ly2)) continue; - InstanceBBox bbox = Collision_computeBBox(ctx->dataWin, inst); - - // Normalize line left-to-right for clipping - GMLReal xl = lx1, yl = ly1, xr = lx2, yr = ly2; - if (xl > xr) { GMLReal tmp = xl; xl = xr; xr = tmp; tmp = yl; yl = yr; yr = tmp; } - - GMLReal dx = xr - xl; - GMLReal dy = yr - yl; - - // Clip line to bbox horizontally - if (GMLReal_fabs(dx) > 0.0001) { - if (bbox.left > xl) { - GMLReal t = (bbox.left - xl) / dx; - xl = bbox.left; - yl = yl + t * dy; - } - if (xr > bbox.right) { - GMLReal t = (bbox.right - xl) / (xr - xl); - yr = yl + t * (yr - yl); - xr = bbox.right; - } - } - - // Y-bounds check after horizontal clipping - GMLReal clippedTop = GMLReal_fmin(yl, yr); - GMLReal clippedBottom = GMLReal_fmax(yl, yr); - if (bbox.top > clippedBottom || clippedTop >= bbox.bottom) continue; - - // Bbox-only mode: collision confirmed - if (prec == 0) { - resultId = inst->instanceId; - break; - } - - // Precise mode: walk line pixel-by-pixel within bbox - Sprite* spr = Collision_getSprite(ctx->dataWin, inst); - if (spr == nullptr || spr->sepMasks != 1 || spr->masks == nullptr || spr->maskCount == 0) { - // No precise mask available, treat as bbox hit - resultId = inst->instanceId; - break; - } - - // Recompute dx/dy for the clipped segment - GMLReal cdx = xr - xl; - GMLReal cdy = yr - yl; - bool found = false; - - if (GMLReal_fabs(cdy) >= GMLReal_fabs(cdx)) { - // Vertical-major: normalize top-to-bottom - GMLReal xt = xl, yt = yl, xb = xr, yb = yr; - if (yt > yb) { GMLReal tmp = xt; xt = xb; xb = tmp; tmp = yt; yt = yb; yb = tmp; } - GMLReal vdx = xb - xt; - GMLReal vdy = yb - yt; - - int32_t startY = (int32_t) GMLReal_fmax(bbox.top, yt); - int32_t endY = (int32_t) GMLReal_fmin(bbox.bottom, yb); - for (int32_t py = startY; endY >= py && !found; py++) { - GMLReal px = (GMLReal_fabs(vdy) > 0.0001) ? xt + ((GMLReal) py - yt) * vdx / vdy : xt; - if (Collision_pointInInstance(spr, inst, px + 0.5, (GMLReal) py + 0.5)) { - found = true; - } - } - } else { - // Horizontal-major - int32_t startX = (int32_t) GMLReal_fmax(bbox.left, xl); - int32_t endX = (int32_t) GMLReal_fmin(bbox.right, xr); - for (int32_t px = startX; endX >= px && !found; px++) { - GMLReal py = (GMLReal_fabs(cdx) > 0.0001) ? yl + ((GMLReal) px - xl) * cdy / cdx : yl; - if (Collision_pointInInstance(spr, inst, (GMLReal) px + 0.5, py + 0.5)) { - found = true; - } - } - } - - if (!found) continue; - resultId = inst->instanceId; - break; - } - Runner_popInstanceSnapshot(runner, snapBase); - - return RValue_makeReal((GMLReal) resultId); -} - -// rectangle_in_rectangle(px1, py1, px2, py2, x1, y1, x2, y2) -// Returns 0 if rectangle P is outside R, 1 if fully inside, 2 if partially overlapping. -// Matches GameMaker-HTML5 scripts/functions/Function_Collision.js. -static RValue builtinRectangleInRectangle(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (8 > argCount) return RValue_makeReal(0.0); - - GMLReal px1 = RValue_toReal(args[0]); - GMLReal py1 = RValue_toReal(args[1]); - GMLReal px2 = RValue_toReal(args[2]); - GMLReal py2 = RValue_toReal(args[3]); - GMLReal x1 = RValue_toReal(args[4]); - GMLReal y1 = RValue_toReal(args[5]); - GMLReal x2 = RValue_toReal(args[6]); - GMLReal y2 = RValue_toReal(args[7]); - - // Normalize so (1,1) is always top-left and (2,2) is bottom-right. - if (px1 > px2) { GMLReal t = px1; px1 = px2; px2 = t; } - if (py1 > py2) { GMLReal t = py1; py1 = py2; py2 = t; } - if (x1 > x2) { GMLReal t = x1; x1 = x2; x2 = t; } - if (y1 > y2) { GMLReal t = y1; y1 = y2; y2 = t; } - - // Count how many corners of P sit inside R. - int32_t cornersIn = 0; - if (px1 >= x1 && px1 <= x2 && py1 >= y1 && py1 <= y2) cornersIn |= 1; - if (px2 >= x1 && px2 <= x2 && py1 >= y1 && py1 <= y2) cornersIn |= 2; - if (px2 >= x1 && px2 <= x2 && py2 >= y1 && py2 <= y2) cornersIn |= 4; - if (px1 >= x1 && px1 <= x2 && py2 >= y1 && py2 <= y2) cornersIn |= 8; - - if (cornersIn == 15) return RValue_makeReal(1.0); - - if (cornersIn == 0) { - // No P corner is inside R. Check whether R's corners are inside P (R engulfs P partially) - // or the rectangles cross axis-wise (T-intersection). - int32_t rCornersIn = 0; - if (x1 >= px1 && x1 <= px2 && y1 >= py1 && y1 <= py2) rCornersIn |= 1; - if (x2 >= px1 && x2 <= px2 && y1 >= py1 && y1 <= py2) rCornersIn |= 2; - if (x2 >= px1 && x2 <= px2 && y2 >= py1 && y2 <= py2) rCornersIn |= 4; - if (x1 >= px1 && x1 <= px2 && y2 >= py1 && y2 <= py2) rCornersIn |= 8; - if (rCornersIn != 0) return RValue_makeReal(2.0); - - // R crosses P horizontally (R's x-edges within P, P's y-edges within R). - int32_t crossX = 0; - if (x1 >= px1 && x1 <= px2 && py1 >= y1 && py1 <= y2) crossX |= 1; - if (x2 >= px1 && x2 <= px2 && py1 >= y1 && py1 <= y2) crossX |= 2; - if (x2 >= px1 && x2 <= px2 && py2 >= y1 && py2 <= y2) crossX |= 4; - if (x1 >= px1 && x1 <= px2 && py2 >= y1 && py2 <= y2) crossX |= 8; - if (crossX != 0) return RValue_makeReal(2.0); - - // R crosses P vertically (R's y-edges within P, P's x-edges within R). - int32_t crossY = 0; - if (px1 >= x1 && px1 <= x2 && y1 >= py1 && y1 <= py2) crossY |= 1; - if (px2 >= x1 && px2 <= x2 && y1 >= py1 && y1 <= py2) crossY |= 2; - if (px2 >= x1 && px2 <= x2 && y2 >= py1 && y2 <= py2) crossY |= 4; - if (px1 >= x1 && px1 <= x2 && y2 >= py1 && y2 <= py2) crossY |= 8; - if (crossY != 0) return RValue_makeReal(2.0); - - return RValue_makeReal(0.0); - } - - // Some but not all of P's corners are inside R: partial overlap. - return RValue_makeReal(2.0); -} - -// collision_rectangle(x1, y1, x2, y2, obj, prec, notme) -static RValue builtinCollisionRectangle(VMContext* ctx, RValue* args, int32_t argCount) { - if (7 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); - - Runner* runner = (Runner*) ctx->runner; - GMLReal x1 = RValue_toReal(args[0]); - GMLReal y1 = RValue_toReal(args[1]); - GMLReal x2 = RValue_toReal(args[2]); - GMLReal y2 = RValue_toReal(args[3]); - int32_t targetObjIndex = RValue_toInt32(args[4]); - int32_t prec = RValue_toInt32(args[5]); - int32_t notme = RValue_toInt32(args[6]); - - // Normalize rect - if (x1 > x2) { GMLReal tmp = x1; x1 = x2; x2 = tmp; } - if (y1 > y2) { GMLReal tmp = y1; y1 = y2; y2 = tmp; } - - Instance* self = (Instance*) ctx->currentInstance; - - int32_t resultId = INSTANCE_NOONE; - int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t snapIdx = snapBase; snapEnd > snapIdx; snapIdx++) { - Instance* inst = runner->instanceSnapshots[snapIdx]; - if (!inst->active) continue; - if (notme && inst == self) continue; - - if (!Collision_rectOverlapsInstance(ctx->dataWin, inst, x1, y1, x2, y2)) continue; - - InstanceBBox bbox = Collision_computeBBox(ctx->dataWin, inst); - - // Precise check if requested and sprite has precise masks - if (prec != 0) { - Sprite* spr = Collision_getSprite(ctx->dataWin, inst); - if (Collision_hasFrameMasks(spr)) { - // Check if any pixel in the overlap region hits the mask - GMLReal iLeft = GMLReal_fmax(x1, bbox.left); - GMLReal iRight = GMLReal_fmin(x2, bbox.right); - GMLReal iTop = GMLReal_fmax(y1, bbox.top); - GMLReal iBottom = GMLReal_fmin(y2, bbox.bottom); - - bool found = false; - int32_t startX = (int32_t) GMLReal_floor(iLeft); - int32_t endX = (int32_t) GMLReal_ceil(iRight); - int32_t startY = (int32_t) GMLReal_floor(iTop); - int32_t endY = (int32_t) GMLReal_ceil(iBottom); - - for (int32_t py = startY; endY > py && !found; py++) { - for (int32_t px = startX; endX > px && !found; px++) { - if (Collision_pointInInstance(spr, inst, (GMLReal) px + 0.5, (GMLReal) py + 0.5)) { - found = true; - } - } - } - if (!found) continue; - } - } - - resultId = inst->instanceId; - break; - } - Runner_popInstanceSnapshot(runner, snapBase); - - return RValue_makeReal((GMLReal) resultId); -} - -// collision_circle(x, y, radius, obj, prec, notme) -static RValue builtinCollisionCircle(VMContext* ctx, RValue* args, int32_t argCount) { - if (6 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); - - Runner* runner = (Runner*) ctx->runner; - GMLReal cx = RValue_toReal(args[0]); - GMLReal cy = RValue_toReal(args[1]); - GMLReal radius = RValue_toReal(args[2]); - int32_t targetObjIndex = RValue_toInt32(args[3]); - int32_t prec = RValue_toInt32(args[4]); - int32_t notme = RValue_toInt32(args[5]); - - if (0 > radius) radius = -radius; - GMLReal radiusSq = radius * radius; - - Instance* self = (Instance*) ctx->currentInstance; - - GMLReal qx1 = cx - radius; - GMLReal qy1 = cy - radius; - GMLReal qx2 = cx + radius; - GMLReal qy2 = cy + radius; - - SpatialGrid_syncGrid(runner, runner->spatialGrid); - SpatialGridQuery query = SpatialGrid_prepareQuery(runner, qx1, qy1, qx2, qy2, targetObjIndex); - - int32_t resultId = INSTANCE_NOONE; - for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx && resultId == INSTANCE_NOONE; gx++) { - for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy && resultId == INSTANCE_NOONE; gy++) { - Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; - int32_t cellLen = (int32_t) arrlen(cell); - repeat(cellLen, ci) { - Instance* inst = cell[ci]; - if (!inst->active) continue; - if (notme && inst == self) continue; - if (inst->lastCollisionQueryId == query.queryId) continue; - inst->lastCollisionQueryId = query.queryId; - - if (query.filterByObject && !VM_isObjectOrDescendant(ctx->dataWin, inst->objectIndex, targetObjIndex)) continue; - if (query.filterByInstanceId && inst->instanceId != (uint32_t) targetObjIndex) continue; - if (!query.filterByObject && !query.filterByInstanceId && targetObjIndex != INSTANCE_ALL) continue; - - if (!Collision_circleOverlapsInstance(ctx->dataWin, inst, cx, cy, radius)) continue; - - if (prec != 0) { - Sprite* spr = Collision_getSprite(ctx->dataWin, inst); - if (Collision_hasFrameMasks(spr)) { - InstanceBBox bbox = Collision_computeBBox(ctx->dataWin, inst); - GMLReal iLeft = GMLReal_fmax(qx1, bbox.left); - GMLReal iRight = GMLReal_fmin(qx2, bbox.right); - GMLReal iTop = GMLReal_fmax(qy1, bbox.top); - GMLReal iBottom = GMLReal_fmin(qy2, bbox.bottom); - - bool found = false; - int32_t startX = (int32_t) GMLReal_floor(iLeft); - int32_t endX = (int32_t) GMLReal_ceil(iRight); - int32_t startY = (int32_t) GMLReal_floor(iTop); - int32_t endY = (int32_t) GMLReal_ceil(iBottom); - - for (int32_t py = startY; endY > py && !found; py++) { - for (int32_t px = startX; endX > px && !found; px++) { - GMLReal wpx = (GMLReal) px + 0.5; - GMLReal wpy = (GMLReal) py + 0.5; - GMLReal ddx = wpx - cx; - GMLReal ddy = wpy - cy; - if (ddx * ddx + ddy * ddy > radiusSq) continue; - if (Collision_pointInInstance(spr, inst, wpx, wpy)) { - found = true; - } - } - } - if (!found) continue; - } - } - - resultId = inst->instanceId; - break; - } - } - } - - return RValue_makeReal((GMLReal) resultId); -} - -// collision_rectangle_list(x1, y1, x2, y2, obj, prec, notme, list, ordered) -> count -static RValue builtinCollisionRectangleList(VMContext* ctx, RValue* args, int32_t argCount) { - if (8 > argCount) return RValue_makeReal(0.0); - - Runner* runner = (Runner*) ctx->runner; - GMLReal x1 = RValue_toReal(args[0]); - GMLReal y1 = RValue_toReal(args[1]); - GMLReal x2 = RValue_toReal(args[2]); - GMLReal y2 = RValue_toReal(args[3]); - int32_t target = RValue_toInt32(args[4]); - int32_t prec = RValue_toInt32(args[5]); - int32_t notme = RValue_toInt32(args[6]); - int32_t listId = RValue_toInt32(args[7]); - // arg 8 (ordered) is currently ignored; instances are appended in iteration order - - DsList* list = dsListGet(runner, listId); - if (list == nullptr) return RValue_makeReal(0.0); - - if (x1 > x2) { GMLReal tmp = x1; x1 = x2; x2 = tmp; } - if (y1 > y2) { GMLReal tmp = y1; y1 = y2; y2 = tmp; } - - Instance* self = (Instance*) ctx->currentInstance; - int32_t count = 0; - - SpatialGrid_syncGrid(runner, runner->spatialGrid); - SpatialGridQuery query = SpatialGrid_prepareQuery(runner, x1, y1, x2, y2, target); - - for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx; gx++) { - for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy; gy++) { - Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; - int32_t cellLen = (int32_t) arrlen(cell); - repeat(cellLen, ci) { - Instance* inst = cell[ci]; - if (!inst->active) continue; - if (notme && inst == self) continue; - if (inst->lastCollisionQueryId == query.queryId) continue; - inst->lastCollisionQueryId = query.queryId; - - if (query.filterByObject && !VM_isObjectOrDescendant(ctx->dataWin, inst->objectIndex, target)) continue; - if (query.filterByInstanceId && inst->instanceId != (uint32_t) target) continue; - - if (!Collision_rectOverlapsInstance(ctx->dataWin, inst, x1, y1, x2, y2)) continue; - InstanceBBox bbox = Collision_computeBBox(ctx->dataWin, inst); - - if (prec != 0) { - Sprite* spr = Collision_getSprite(ctx->dataWin, inst); - if (Collision_hasFrameMasks(spr)) { - GMLReal iLeft = GMLReal_fmax(x1, bbox.left); - GMLReal iRight = GMLReal_fmin(x2, bbox.right); - GMLReal iTop = GMLReal_fmax(y1, bbox.top); - GMLReal iBottom = GMLReal_fmin(y2, bbox.bottom); - - bool found = false; - int32_t startX = (int32_t) GMLReal_floor(iLeft); - int32_t endX = (int32_t) GMLReal_ceil(iRight); - int32_t startY = (int32_t) GMLReal_floor(iTop); - int32_t endY = (int32_t) GMLReal_ceil(iBottom); - - for (int32_t py = startY; endY > py && !found; py++) { - for (int32_t px = startX; endX > px && !found; px++) { - if (Collision_pointInInstance(spr, inst, (GMLReal) px + 0.5, (GMLReal) py + 0.5)) { - found = true; - } - } - } - if (!found) continue; - } - } - - arrput(list->items, RValue_makeReal((GMLReal) inst->instanceId)); - count++; - } - } - } - - return RValue_makeReal((GMLReal) count); -} - -// collision_point(x, y, obj, prec, notme) -static RValue builtinCollisionPoint(VMContext* ctx, RValue* args, int32_t argCount) { - if (5 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); - - Runner* runner = (Runner*) ctx->runner; - GMLReal px = RValue_toReal(args[0]); - GMLReal py = RValue_toReal(args[1]); - int32_t targetObjIndex = RValue_toInt32(args[2]); - int32_t prec = RValue_toInt32(args[3]); - int32_t notme = RValue_toInt32(args[4]); - - Instance* self = (Instance*) ctx->currentInstance; - - int32_t resultId = INSTANCE_NOONE; - int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t snapIdx = snapBase; snapEnd > snapIdx; snapIdx++) { - Instance* inst = runner->instanceSnapshots[snapIdx]; - if (!inst->active) continue; - if (notme && inst == self) continue; - - if (!Collision_pointInsideInstanceBox(ctx->dataWin, inst, px, py)) continue; - - if (prec != 0) { - Sprite* spr = Collision_getSprite(ctx->dataWin, inst); - if (Collision_hasFrameMasks(spr)) { - if (!Collision_pointInInstance(spr, inst, px, py)) continue; - } - } - - resultId = inst->instanceId; - break; - } - Runner_popInstanceSnapshot(runner, snapBase); - - return RValue_makeReal((GMLReal) resultId); -} - -// instance_place(x, y, obj) - returns colliding instance id at (x, y), or noone -static RValue builtinInstancePlace(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); - - Runner* runner = (Runner*) ctx->runner; - Instance* caller = (Instance*) ctx->currentInstance; - if (caller == nullptr) return RValue_makeReal((GMLReal) INSTANCE_NOONE); - - GMLReal testX = RValue_toReal(args[0]); - GMLReal testY = RValue_toReal(args[1]); - int32_t targetObjIndex = RValue_toInt32(args[2]); - - GMLReal savedX = caller->x; - GMLReal savedY = caller->y; - caller->x = testX; - caller->y = testY; - - InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); - int32_t resultId = INSTANCE_NOONE; - - SpatialGrid_syncGrid(runner, runner->spatialGrid); - - if (callerBBox.valid) { - SpatialGridQuery query = SpatialGrid_prepareQuery(runner, callerBBox.left, callerBBox.top, callerBBox.right, callerBBox.bottom, targetObjIndex); - - for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx && resultId == INSTANCE_NOONE; gx++) { - for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy && resultId == INSTANCE_NOONE; gy++) { - Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; - int32_t cellLen = (int32_t) arrlen(cell); - repeat(cellLen, ci) { - Instance* other = cell[ci]; - if (!other->active || other == caller) continue; - if (other->lastCollisionQueryId == query.queryId) continue; - other->lastCollisionQueryId = query.queryId; - - if (query.filterByObject && !VM_isObjectOrDescendant(runner->dataWin, other->objectIndex, targetObjIndex)) continue; - if (query.filterByInstanceId && other->instanceId != (uint32_t) targetObjIndex) continue; - - InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); - if (!otherBBox.valid) continue; - - if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { - resultId = other->instanceId; - break; - } - } - } - } - } - - caller->x = savedX; - caller->y = savedY; - return RValue_makeReal((GMLReal) resultId); -} - -// instance_position(x, y, obj) -static RValue builtinInstancePosition(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); - - Runner* runner = (Runner*) ctx->runner; - GMLReal px = RValue_toReal(args[0]); - GMLReal py = RValue_toReal(args[1]); - int32_t targetObjIndex = RValue_toInt32(args[2]); - - int32_t resultId = INSTANCE_NOONE; - int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); - int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); - for (int32_t i = snapBase; snapEnd > i; i++) { - Instance* inst = runner->instanceSnapshots[i]; - if (!inst->active) continue; - - if (!Collision_pointInsideInstanceBox(ctx->dataWin, inst, px, py)) continue; - - resultId = inst->instanceId; - break; - } - Runner_popInstanceSnapshot(runner, snapBase); - - return RValue_makeReal((GMLReal) resultId); -} - -// position_meeting(x, y, obj) - returns true if point (x, y) is inside any instance of obj. -static RValue builtinPositionMeeting(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeBool(false); - - Runner* runner = (Runner*) ctx->runner; - GMLReal px = RValue_toReal(args[0]); - GMLReal py = RValue_toReal(args[1]); - int32_t target = RValue_toInt32(args[2]); - - - SpatialGrid_syncGrid(runner, runner->spatialGrid); - SpatialGridQuery query = SpatialGrid_prepareQuery(runner, px, py, px, py, target); - bool found = false; - - for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx && !found; gx++) { - for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy && !found; gy++) { - Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; - int32_t cellLen = (int32_t) arrlen(cell); - repeat(cellLen, ci) { - Instance* other = cell[ci]; - // Keep in mind that we DO NOT skip "self" - if (!other->active) continue; - if (other->lastCollisionQueryId == query.queryId) continue; - other->lastCollisionQueryId = query.queryId; - - if (query.filterByObject && !VM_isObjectOrDescendant(runner->dataWin, other->objectIndex, target)) continue; - if (query.filterByInstanceId && other->instanceId != (uint32_t) target) continue; - - if (!Collision_pointInsideInstanceBox(ctx->dataWin, other, px, py)) continue; - - found = true; - break; - } - } - } - - return RValue_makeBool(found); -} - -// Misc stubs -STUB_RETURN_ZERO(get_timer) -static RValue builtinActionSetAlarm(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t steps = RValue_toInt32(args[0]); - int32_t alarmIndex = RValue_toInt32(args[1]); - - if (0 > alarmIndex || alarmIndex >= GML_ALARM_COUNT) { - return RValue_makeUndefined(); - } - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - Runner* runner = (Runner*) ctx->runner; - -#ifdef ENABLE_VM_TRACING - if (shgeti(ctx->alarmsToBeTraced, "*") != -1 || shgeti(ctx->alarmsToBeTraced, runner->dataWin->objt.objects[inst->objectIndex].name) != -1) { - fprintf(stderr, "VM: [%s] Setting Alarm[%d] = %d (instanceId=%d)\n", runner->dataWin->objt.objects[inst->objectIndex].name, alarmIndex, steps, inst->instanceId); - } -#endif - - inst->alarm[alarmIndex] = steps; - if (steps > 0) inst->activeAlarmMask |= (uint16_t) (1u << alarmIndex); - else inst->activeAlarmMask &= (uint16_t) ~(1u << alarmIndex); - } - - return RValue_makeUndefined(); -} - -static RValue builtinAlarmSet(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t alarmIndex = RValue_toInt32(args[0]); - int32_t value = RValue_toInt32(args[1]); - - if (0 > alarmIndex || alarmIndex >= GML_ALARM_COUNT) { - return RValue_makeUndefined(); - } - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - -#ifdef ENABLE_VM_TRACING - Runner* runner = (Runner*) ctx->runner; - if (shgeti(ctx->alarmsToBeTraced, "*") != -1 || shgeti(ctx->alarmsToBeTraced, runner->dataWin->objt.objects[inst->objectIndex].name) != -1) { - fprintf(stderr, "VM: [%s] Setting Alarm[%d] = %d (instanceId=%d)\n", runner->dataWin->objt.objects[inst->objectIndex].name, alarmIndex, value, inst->instanceId); - } -#endif - - inst->alarm[alarmIndex] = value; - if (value > 0) inst->activeAlarmMask |= (uint16_t) (1u << alarmIndex); - else inst->activeAlarmMask &= (uint16_t) ~(1u << alarmIndex); - } - - return RValue_makeUndefined(); -} - -static RValue builtinAlarmGet(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - int32_t alarmIndex = RValue_toInt32(args[0]); - - if (0 > alarmIndex || alarmIndex >= GML_ALARM_COUNT) { - return RValue_makeReal(-1); - } - - if (ctx->currentInstance != nullptr) { - Instance* inst = (Instance*) ctx->currentInstance; - return RValue_makeReal((GMLReal) inst->alarm[alarmIndex]); - } - - return RValue_makeReal(-1); -} - -static RValue builtinActionIfVariable(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - bool check; - switch (args[0].type) { - case RVALUE_REAL: { - check = args[0].real != 0.0; - break; - } - case RVALUE_INT32: { - check = args[0].int32 != 0; - break; - } -#ifndef NO_RVALUE_INT64 - case RVALUE_INT64: { - check = args[0].int64 != 0; - break; - } -#endif - case RVALUE_BOOL: { - check = args[0].int32 != 0; - break; - } - case RVALUE_STRING: { - check = args[0].string != nullptr && args[0].string[0] != '\0'; - break; - } - default: { - check = false; - break; - } - } - - int32_t idx = check ? 1 : 2; - RValue result = args[idx]; - args[idx].ownsReference = false; // Steal ownership to avoid double-free in handleCall - return result; -} - -STUB_RETURN_UNDEFINED(action_sound) - -// ===[ Tile Layer Functions ]=== - -static TileLayerState* getOrCreateTileLayer(Runner* runner, int32_t depth) { - ptrdiff_t idx = hmgeti(runner->tileLayerMap, depth); - if (0 > idx) { - TileLayerState defaultVal = { .visible = true, .offsetX = 0.0f, .offsetY = 0.0f }; - hmput(runner->tileLayerMap, depth, defaultVal); - idx = hmgeti(runner->tileLayerMap, depth); - } - return &runner->tileLayerMap[idx].value; -} - -static RValue builtinTileLayerHide(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t depth = RValue_toInt32(args[0]); - TileLayerState* layer = getOrCreateTileLayer(runner, depth); - layer->visible = false; - return RValue_makeUndefined(); -} - -static RValue builtinTileLayerShow(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t depth = RValue_toInt32(args[0]); - TileLayerState* layer = getOrCreateTileLayer(runner, depth); - layer->visible = true; - return RValue_makeUndefined(); -} - -static RValue builtinTileLayerShift(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t depth = RValue_toInt32(args[0]); - float dx = (float) RValue_toReal(args[1]); - float dy = (float) RValue_toReal(args[2]); - TileLayerState* layer = getOrCreateTileLayer(runner, depth); - layer->offsetX += dx; - layer->offsetY += dy; - return RValue_makeUndefined(); -} - -// ===[ Layer Functions ]=== - -static RValue builtinLayerForceDrawDepth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - runner->forceDrawDepth = RValue_toBool(args[0]); - runner->forcedDepth = RValue_toInt32(args[1]); - return RValue_makeUndefined(); -} - -static RValue builtinLayerIsDrawDepthForced(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - return RValue_makeBool(runner->forceDrawDepth); -} - -static RValue builtinLayerGetForcedDepth(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - return RValue_makeReal((GMLReal) runner->forcedDepth); -} - -// ===[ GMS2 Layer Runtime API ]=== - -// GMS layer functions accept either a numeric layer id or a layer name string. -// Returns the resolved runtime id, or -1 if no match. -static int32_t resolveLayerIdArg(Runner* runner, RValue arg) { - if (arg.type == RVALUE_STRING) { - const char* name = arg.string; - if (name == nullptr) return -1; - size_t runtimeLayerCount = arrlenu(runner->runtimeLayers); - repeat(runtimeLayerCount, i) { - RuntimeLayer* rl = &runner->runtimeLayers[i]; - if (rl->dynamic && rl->dynamicName != nullptr && strcmp(rl->dynamicName, name) == 0) - return (int32_t) rl->id; - } - if (runner->currentRoom != nullptr) { - repeat(runner->currentRoom->layerCount, i) { - RoomLayer* layer = &runner->currentRoom->layers[i]; - if (layer->name != nullptr && strcmp(layer->name, name) == 0) - return (int32_t) layer->id; - } - } - return -1; - } - return RValue_toInt32(arg); -} - -static void instanceSetLayerActiveState(Runner* runner, int32_t layerId, bool isActive) { - if (0 > layerId || runner->currentRoom == nullptr) return; - - repeat(runner->currentRoom->layerCount, layerIndex) { - RoomLayer* layer = &runner->currentRoom->layers[layerIndex]; - - if ((int32_t) layer->id != layerId) - continue; - - if (layer->type != RoomLayerType_Instances || layer->instancesData == nullptr) - break; - - RoomLayerInstancesData* layerData = layer->instancesData; - - repeat(layerData->instanceCount, instanceIndex) { - Instance* inst = hmget(runner->instancesById, layerData->instanceIds[instanceIndex]); - if (inst != nullptr && !inst->destroyed) - inst->active = isActive; - } - return; - } -} - -static RValue builtinInstanceActivateLayer(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t layerId = resolveLayerIdArg(runner, args[0]); - instanceSetLayerActiveState(runner, layerId, true); - return RValue_makeUndefined(); -} - -static RValue builtinInstanceDeactivateLayer(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t layerId = resolveLayerIdArg(runner, args[0]); - instanceSetLayerActiveState(runner, layerId, false); - return RValue_makeUndefined(); -} - -static RValue builtinLayerGetId(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - char* name = RValue_toString(args[0]); - if (name == nullptr) return RValue_makeReal(-1.0); - int32_t result = -1; - // Check dynamic layers first (they may shadow a parsed layer by name). - size_t runtimeLayerCount = arrlenu(runner->runtimeLayers); - repeat(runtimeLayerCount, i) { - RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; - if (runtimeLayer->dynamic && runtimeLayer->dynamicName != nullptr && strcmp(runtimeLayer->dynamicName, name) == 0) { - result = (int32_t) runtimeLayer->id; - break; - } - } - if (result == -1 && runner->currentRoom != nullptr) { - repeat(runner->currentRoom->layerCount, i) { - RoomLayer* layer = &runner->currentRoom->layers[i]; - if (layer->name != nullptr && strcmp(layer->name, name) == 0) { - result = (int32_t) layer->id; - break; - } - } - } - free(name); - return RValue_makeReal((GMLReal) result); -} - -static RValue builtinLayerExists(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - return RValue_makeBool(Runner_findRuntimeLayerById(runner, id) != nullptr); -} - -static RValue builtinLayerGetName(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer != nullptr && runtimeLayer->dynamic && runtimeLayer->dynamicName != nullptr) - return RValue_makeString(runtimeLayer->dynamicName); - - RoomLayer* roomLayer = Runner_findRoomLayerById(runner, id); - if (roomLayer == nullptr || roomLayer->name == nullptr) - return RValue_makeString(""); - - return RValue_makeString(roomLayer->name); -} - -static RValue builtinLayerGetDepth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer == nullptr) - return RValue_makeUndefined(); - - return RValue_makeReal((GMLReal) runtimeLayer->depth); -} - -static RValue builtinLayerDepth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - int32_t depth = RValue_toInt32(args[1]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer != nullptr && runtimeLayer->depth != depth) { - runtimeLayer->depth = depth; - runner->drawableListSortDirty = true; - } - - return RValue_makeUndefined(); -} - -static RValue builtinLayerGetVisible(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer == nullptr) - return RValue_makeBool(false); - - return RValue_makeBool(runtimeLayer->visible); -} - -static RValue builtinLayerSetVisible(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - bool visible = RValue_toBool(args[1]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer != nullptr) - runtimeLayer->visible = visible; - - return RValue_makeUndefined(); -} - -static RValue builtinLayerGetX(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer == nullptr) - return RValue_makeReal(0.0); - - return RValue_makeReal((GMLReal) runtimeLayer->xOffset); -} - -static RValue builtinLayerX(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - float x = (float) RValue_toReal(args[1]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer != nullptr) - runtimeLayer->xOffset = x; - - return RValue_makeUndefined(); -} - -static RValue builtinLayerGetY(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer == nullptr) - return RValue_makeReal(0.0); - - return RValue_makeReal((GMLReal) runtimeLayer->yOffset); -} - -static RValue builtinLayerY(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - float y = (float) RValue_toReal(args[1]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer != nullptr) - runtimeLayer->yOffset = y; - - return RValue_makeUndefined(); -} - -static RValue builtinLayerGetHspeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer == nullptr) - return RValue_makeReal(0.0); - - return RValue_makeReal((GMLReal) runtimeLayer->hSpeed); -} - -static RValue builtinLayerHspeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - float hs = (float) RValue_toReal(args[1]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer != nullptr) - runtimeLayer->hSpeed = hs; - - return RValue_makeUndefined(); -} - -static RValue builtinLayerGetVspeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer == nullptr) - return RValue_makeReal(0.0); - - return RValue_makeReal((GMLReal) runtimeLayer->vSpeed); -} - -// Creates a new dynamic layer. Signatures: layer_create(depth) or layer_create(depth, name). -static RValue builtinLayerCreate(VMContext* ctx, RValue* args, int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t depth = RValue_toInt32(args[0]); - char* name = nullptr; - if (argCount > 1) { - name = RValue_toString(args[1]); - } - uint32_t id = Runner_getNextLayerId(runner); - RuntimeLayer runtimeLayer = { - .id = id, - .depth = depth, - .visible = true, - .xOffset = 0.0f, .yOffset = 0.0f, - .hSpeed = 0.0f, .vSpeed = 0.0f, - .dynamic = true, - .dynamicName = name, // ownership transferred (nullptr if not provided) - .elements = nullptr, - }; - arrput(runner->runtimeLayers, runtimeLayer); - runner->drawableListStructureDirty = true; - return RValue_makeReal((GMLReal) id); -} - -static RValue builtinLayerDestroy(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - size_t count = arrlenu(runner->runtimeLayers); - repeat(count, i) { - if ((int32_t) runner->runtimeLayers[i].id != id) - continue; - - // Ignore if we are trying to delete a non-dynamic layer - if (!runner->runtimeLayers[i].dynamic) - return RValue_makeUndefined(); - - Runner_freeRuntimeLayer(&runner->runtimeLayers[i]); - arrdel(runner->runtimeLayers, i); - runner->drawableListStructureDirty = true; - break; - } - return RValue_makeUndefined(); -} - -static RValue builtinLayerBackgroundCreate(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t layerId = resolveLayerIdArg(runner, args[0]); - int32_t spriteIndex = RValue_toInt32(args[1]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, layerId); - if (runtimeLayer == nullptr) - return RValue_makeReal(-1.0); - - RuntimeBackgroundElement* bg = safeMalloc(sizeof(RuntimeBackgroundElement)); - bg->spriteIndex = spriteIndex; - bg->visible = true; - bg->htiled = false; - bg->vtiled = false; - bg->stretch = false; - bg->xScale = 1.0f; - bg->yScale = 1.0f; - bg->blend = 0xFFFFFF; - bg->alpha = 1.0f; - bg->xOffset = 0.0f; - bg->yOffset = 0.0f; - RuntimeLayerElement el = { - .id = Runner_getNextLayerId(runner), - .type = RuntimeLayerElementType_Background, - .backgroundElement = bg, - .spriteElement = nullptr, - }; - arrput(runtimeLayer->elements, el); - return RValue_makeReal((GMLReal) el.id); -} - -static RValue builtinLayerBackgroundExists(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t layerId = resolveLayerIdArg(runner, args[0]); - int32_t elementId = RValue_toInt32(args[1]); - - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, layerId); - if (runtimeLayer == nullptr) - return RValue_makeBool(false); - - size_t count = arrlenu(runtimeLayer->elements); - repeat(count, i) { - if ((int32_t) runtimeLayer->elements[i].id == elementId && runtimeLayer->elements[i].type == RuntimeLayerElementType_Background) { - return RValue_makeBool(true); - } - } - return RValue_makeBool(false); -} - -static RuntimeBackgroundElement* findBackgroundElement(Runner* runner, int32_t elementId) { - RuntimeLayerElement* el = Runner_findLayerElementById(runner, elementId, nullptr); - if (el == nullptr || el->type != RuntimeLayerElementType_Background) - return nullptr; - return el->backgroundElement; -} - -static RValue builtinLayerBackgroundVisible(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); - if (bg != nullptr) - bg->visible = RValue_toBool(args[1]); - return RValue_makeUndefined(); -} - -static RValue builtinLayerBackgroundHtiled(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); - if (bg != nullptr) - bg->htiled = RValue_toBool(args[1]); - return RValue_makeUndefined(); -} - -static RValue builtinLayerBackgroundVtiled(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); - if (bg != nullptr) - bg->vtiled = RValue_toBool(args[1]); - return RValue_makeUndefined(); -} - -static RValue builtinLayerBackgroundXscale(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); - if (bg != nullptr) - bg->xScale = (float) RValue_toReal(args[1]); - return RValue_makeUndefined(); -} - -static RValue builtinLayerBackgroundYscale(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); - if (bg != nullptr) - bg->yScale = (float) RValue_toReal(args[1]); - return RValue_makeUndefined(); -} - -static RValue builtinLayerBackgroundStretch(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); - if (bg != nullptr) - bg->stretch = RValue_toBool(args[1]); - return RValue_makeUndefined(); -} - -static RValue builtinLayerBackgroundBlend(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); - if (bg != nullptr) - bg->blend = (uint32_t) RValue_toInt32(args[1]) & 0x00FFFFFF; - return RValue_makeUndefined(); -} - -static RValue builtinLayerBackgroundAlpha(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); - if (bg != nullptr) - bg->alpha = (float) RValue_toReal(args[1]); - return RValue_makeUndefined(); -} - -#if IS_BC17_OR_HIGHER_ENABLED -static RValue builtinLayerGetAllElements(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - - RValue arr = VM_createArray(ctx); - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer == nullptr) - return arr; - - int32_t i = 0; - size_t count = arrlenu(runtimeLayer->elements); - repeat(count, elementIndex) { - VM_arraySet(ctx, &arr, i++, RValue_makeReal((GMLReal) runtimeLayer->elements[elementIndex].id)); - } - return arr; -} -#endif - -static RValue builtinLayerGetElementType(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); - // layerelementtype_undefined == 0 matches GML's return for unknown/missing elements. - if (el == nullptr) - return RValue_makeReal(0.0); - - return RValue_makeReal((GMLReal) el->type); -} - -static RValue builtinLayerSpriteGetSprite(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); - if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) - return RValue_makeReal(-1.0); - - return RValue_makeReal((GMLReal) el->spriteElement->spriteIndex); -} - -static RValue builtinLayerSpriteGetAngle(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); - if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) - return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) el->spriteElement->rotation); -} - - -static RValue builtinLayerSpriteGetX(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); - if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) - return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) el->spriteElement->x); -} - -static RValue builtinLayerSpriteGetY(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); - if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) - return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) el->spriteElement->y); -} - -static RValue builtinLayerSpriteGetXScale(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); - if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) - return RValue_makeReal(1.0); - return RValue_makeReal((GMLReal) el->spriteElement->scaleX); -} - -static RValue builtinLayerSpriteGetYScale(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); - if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) - return RValue_makeReal(1.0); - return RValue_makeReal((GMLReal) el->spriteElement->scaleY); -} - -static RValue builtinLayerSpriteGetSpeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); - if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) - return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) el->spriteElement->animationSpeed); -} - -static RValue builtinLayerSpriteGetIndex(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); - if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) - return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) el->spriteElement->frameIndex); -} - -static RValue builtinLayerSpriteDestroy(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - - RuntimeLayer* owningLayer = nullptr; - RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, &owningLayer); - if (el == nullptr || owningLayer == nullptr || el->type != RuntimeLayerElementType_Sprite) +#include "vm_builtins.h" +#include "binary_utils.h" +#include "instance.h" +#include "json_reader.h" +#include "real_type.h" +#include "runner.h" +#include "runner_gamepad.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +#include "rvalue.h" +#include "stb_ds.h" +#include "text_utils.h" +#include "collision.h" +#include "ini.h" +#include "audio_system.h" +#include "file_system.h" + +#ifdef __3DS__ +#include <3ds.h> +#include <3ds/services/mcuhwc.h> + +void N3DSRenderer_beginBottomScreenGUI(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endBottomScreenGUI(Renderer* renderer); +void N3DSRenderer_beginTopScreenGUI(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endTopScreenGUI(Renderer* renderer); +void N3DSRenderer_beginTopScreenGUI2x(Renderer* renderer, int32_t guiW, int32_t guiH); +void N3DSRenderer_endTopScreenGUI2x(Renderer* renderer); +bool N3DSRenderer_isTopScreenGUIActive(Renderer* renderer); +static void N3DS_setAsrielRainbowInfoLed(Runner* runner); +#endif + +#define MAX_BACKGROUNDS 8 + +static RValue builtinN3DSRenderTopScreen(VMContext* ctx, RValue* args, int32_t argCount); +static bool battleDraw_is3DSBattleActive(VMContext* ctx, Runner* runner); + +#ifdef __3DS__ +static Result g_n3dsAsrielLedInitRc = 0; +static Result g_n3dsAsrielLedSetRc = 0; +static bool g_n3dsAsrielLedTriggered = false; +static int32_t g_n3dsAsrielLedRoomIndex = -1; +static bool g_n3dsDisableBottomScreenOverrides = true; + +void N3DS_getAsrielLedDebugState(int32_t* outInitRc, int32_t* outSetRc, bool* outTriggered, int32_t* outRoomIndex) { + if (outInitRc != NULL) *outInitRc = (int32_t) g_n3dsAsrielLedInitRc; + if (outSetRc != NULL) *outSetRc = (int32_t) g_n3dsAsrielLedSetRc; + if (outTriggered != NULL) *outTriggered = g_n3dsAsrielLedTriggered; + if (outRoomIndex != NULL) *outRoomIndex = g_n3dsAsrielLedRoomIndex; +} + +void N3DS_tryTriggerAsrielLed(Runner* runner) { + if (runner == NULL || runner->osType != OS_3DS) return; + if (runner->instancesByObject == NULL) return; + + int32_t afinalBodyObject = shget(runner->assetsByName, "obj_afinal_body"); + if (afinalBodyObject < 0 || (uint32_t) afinalBodyObject >= runner->dataWin->objt.count) return; + + Instance** bucket = runner->instancesByObject[afinalBodyObject]; + int32_t instanceCount = (int32_t) arrlen(bucket); + repeat(instanceCount, i) { + Instance* inst = bucket[i]; + if (inst == NULL || !inst->active || !inst->visible) continue; + N3DS_setAsrielRainbowInfoLed(runner); + return; + } +} +#else +void N3DS_getAsrielLedDebugState(int32_t* outInitRc, int32_t* outSetRc, bool* outTriggered, int32_t* outRoomIndex) { + if (outInitRc != NULL) *outInitRc = 0; + if (outSetRc != NULL) *outSetRc = 0; + if (outTriggered != NULL) *outTriggered = false; + if (outRoomIndex != NULL) *outRoomIndex = -1; +} + +void N3DS_tryTriggerAsrielLed(MAYBE_UNUSED Runner* runner) { +} +#endif + +static int32_t Color_lerp(int32_t col1, int32_t col2, float amount) { + if (amount < 0.0f) amount = 0.0f; + if (amount > 1.0f) amount = 1.0f; + + float invAmount = 1.0f - amount; + uint32_t r = (uint32_t) roundf((float) BGR_R(col1) * invAmount + (float) BGR_R(col2) * amount); + uint32_t g = (uint32_t) roundf((float) BGR_G(col1) * invAmount + (float) BGR_G(col2) * amount); + uint32_t b = (uint32_t) roundf((float) BGR_B(col1) * invAmount + (float) BGR_B(col2) * amount); + return (int32_t) (r | (g << 8) | (b << 16)); +} + +// ===[ STUB LOGGING ]=== + +#ifdef ENABLE_VM_STUB_LOGS +static void logStubbedFunction(VMContext* ctx, const char* funcName) { + const char* callerName = VM_getCallerName(ctx); + char* dedupKey = VM_createDedupKey(callerName, funcName); + + if (ctx->alwaysLogStubbedFunctions || 0 > shgeti(ctx->loggedStubbedFuncs, dedupKey)) { + // shput stores the key pointer, so don't free it when inserting + shput(ctx->loggedStubbedFuncs, dedupKey, true); + fprintf(stderr, "VM: [%s] Stubbed function \"%s\"!\n", callerName, funcName); + } else { + free(dedupKey); + } +} + +static void logSemiStubbedFunction(VMContext* ctx, const char* funcName) { + const char* callerName = VM_getCallerName(ctx); + char* dedupKey = VM_createDedupKey(callerName, funcName); + + if (ctx->alwaysLogStubbedFunctions || 0 > shgeti(ctx->loggedStubbedFuncs, dedupKey)) { + // shput stores the key pointer, so don't free it when inserting + shput(ctx->loggedStubbedFuncs, dedupKey, true); + fprintf(stderr, "VM: [%s] Semi-Stubbed function \"%s\"!\n", callerName, funcName); + } else { + free(dedupKey); + } +} +#else +#define logStubbedFunction(ctx, funcName) ((void) 0) +#define logSemiStubbedFunction(ctx, funcName) ((void) 0) +#endif + +// Forward declarations +static int32_t resolveLayerIdArg(Runner* runner, RValue arg); + +// ===[ DS_MAP SYSTEM ]=== + +static int32_t dsMapCreate(Runner* runner) { + DsMapEntry* newMap = nullptr; + int32_t id = (int32_t) arrlen(runner->dsMapPool); + arrput(runner->dsMapPool, newMap); + return id; +} + +static DsMapEntry** dsMapGet(Runner* runner, int32_t id) { + if (id < 0 || (int32_t) arrlen(runner->dsMapPool) <= id) return nullptr; + return &runner->dsMapPool[id]; +} + +// ===[ DS_LIST SYSTEM ]=== + +static int32_t dsListCreate(Runner* runner) { + // Reuse a freed slot if available, matching native GameMaker behavior. + // Yes, some games (example: DELTARUNE Chapter 3's obj_board_playercamera_Other_10) rely on ds_list_create reusing the id of a list just destroyed. + int32_t poolSize = (int32_t) arrlen(runner->dsListPool); + repeat(poolSize, i) { + if (runner->dsListPool[i].freed) { + runner->dsListPool[i].freed = false; + runner->dsListPool[i].items = nullptr; + return i; + } + } + DsList newList = { .items = nullptr, .freed = false }; + int32_t id = poolSize; + arrput(runner->dsListPool, newList); + return id; +} + +static DsList* dsListGet(Runner* runner, int32_t id) { + if (0 > id || id >= (int32_t) arrlen(runner->dsListPool)) return nullptr; + if (runner->dsListPool[id].freed) return nullptr; + return &runner->dsListPool[id]; +} + +// ===[ BUILT-IN VARIABLE GET/SET ]=== + +/** + * Gets the argument number from the name + * + * If it returns -1, then the name is not an argument variable + * + * @param name The name + * @return The argument number, -1 if it is not an argument variable + */ +static int extractArgumentNumber(const char* name) { + if (strncmp(name, "argument", 8) == 0) { + char* end; + long argNumber = strtol(name + 8, &end, 10); + if (end == name + 8 || *end != '\0' || 0 > argNumber || argNumber > 15) return -1; + return (int) argNumber; + } + return -1; +} + +static bool isValidAlarmIndex(int alarmIndex) { + return alarmIndex >= 0 && GML_ALARM_COUNT > alarmIndex; +} + +// Sorted (strcmp-order, LC_ALL=C) table of built-in variable names -> enum IDs. +// We use bsearch instead of a HashMap because we don't have *that* many builtin var entries, so it is faster to use bsearch than a HashMap. +// IMPORTANT: Entries MUST stay sorted by name for bsearch to work! +typedef struct { + const char* name; + int16_t id; +} BuiltinVarEntry; + +static const BuiltinVarEntry BUILTIN_VAR_TABLE[] = { + { "alarm", BUILTIN_VAR_ALARM }, + { "application_surface", BUILTIN_VAR_APPLICATION_SURFACE }, + { "argument", BUILTIN_VAR_ARGUMENT }, + { "argument0", BUILTIN_VAR_ARGUMENT0 }, + { "argument1", BUILTIN_VAR_ARGUMENT1 }, + { "argument10", BUILTIN_VAR_ARGUMENT10 }, + { "argument11", BUILTIN_VAR_ARGUMENT11 }, + { "argument12", BUILTIN_VAR_ARGUMENT12 }, + { "argument13", BUILTIN_VAR_ARGUMENT13 }, + { "argument14", BUILTIN_VAR_ARGUMENT14 }, + { "argument15", BUILTIN_VAR_ARGUMENT15 }, + { "argument2", BUILTIN_VAR_ARGUMENT2 }, + { "argument3", BUILTIN_VAR_ARGUMENT3 }, + { "argument4", BUILTIN_VAR_ARGUMENT4 }, + { "argument5", BUILTIN_VAR_ARGUMENT5 }, + { "argument6", BUILTIN_VAR_ARGUMENT6 }, + { "argument7", BUILTIN_VAR_ARGUMENT7 }, + { "argument8", BUILTIN_VAR_ARGUMENT8 }, + { "argument9", BUILTIN_VAR_ARGUMENT9 }, + { "argument_count", BUILTIN_VAR_ARGUMENT_COUNT }, + { "async_load", BUILTIN_VAR_ASYNC_LOAD }, + { "background_alpha", BUILTIN_VAR_BACKGROUND_ALPHA }, + { "background_color", BUILTIN_VAR_BACKGROUND_COLOR }, + { "background_colour", BUILTIN_VAR_BACKGROUND_COLOUR }, + { "background_height", BUILTIN_VAR_BACKGROUND_HEIGHT }, + { "background_hspeed", BUILTIN_VAR_BACKGROUND_HSPEED }, + { "background_index", BUILTIN_VAR_BACKGROUND_INDEX }, + { "background_visible", BUILTIN_VAR_BACKGROUND_VISIBLE }, + { "background_vspeed", BUILTIN_VAR_BACKGROUND_VSPEED }, + { "background_width", BUILTIN_VAR_BACKGROUND_WIDTH }, + { "background_x", BUILTIN_VAR_BACKGROUND_X }, + { "background_y", BUILTIN_VAR_BACKGROUND_Y }, + { "bbox_bottom", BUILTIN_VAR_BBOX_BOTTOM }, + { "bbox_left", BUILTIN_VAR_BBOX_LEFT }, + { "bbox_right", BUILTIN_VAR_BBOX_RIGHT }, + { "bbox_top", BUILTIN_VAR_BBOX_TOP }, + { "buffer_bool", BUILTIN_VAR_BUFFER_BOOL }, + { "buffer_f16", BUILTIN_VAR_BUFFER_F16 }, + { "buffer_f32", BUILTIN_VAR_BUFFER_F32 }, + { "buffer_f64", BUILTIN_VAR_BUFFER_F64 }, + { "buffer_fast", BUILTIN_VAR_BUFFER_FAST }, + { "buffer_fixed", BUILTIN_VAR_BUFFER_FIXED }, + { "buffer_grow", BUILTIN_VAR_BUFFER_GROW }, + { "buffer_s16", BUILTIN_VAR_BUFFER_S16 }, + { "buffer_s32", BUILTIN_VAR_BUFFER_S32 }, + { "buffer_s8", BUILTIN_VAR_BUFFER_S8 }, + { "buffer_seek_end", BUILTIN_VAR_BUFFER_SEEK_END }, + { "buffer_seek_relative", BUILTIN_VAR_BUFFER_SEEK_RELATIVE }, + { "buffer_seek_start", BUILTIN_VAR_BUFFER_SEEK_START }, + { "buffer_string", BUILTIN_VAR_BUFFER_STRING }, + { "buffer_text", BUILTIN_VAR_BUFFER_TEXT }, + { "buffer_u16", BUILTIN_VAR_BUFFER_U16 }, + { "buffer_u32", BUILTIN_VAR_BUFFER_U32 }, + { "buffer_u64", BUILTIN_VAR_BUFFER_U64 }, + { "buffer_u8", BUILTIN_VAR_BUFFER_U8 }, + { "buffer_wrap", BUILTIN_VAR_BUFFER_WRAP }, + { "current_time", BUILTIN_VAR_CURRENT_TIME }, + { "debug_mode", BUILTIN_VAR_DEBUG_MODE }, + { "depth", BUILTIN_VAR_DEPTH }, + { "direction", BUILTIN_VAR_DIRECTION }, + { "false", BUILTIN_VAR_FALSE }, + { "fps", BUILTIN_VAR_FPS }, + { "friction", BUILTIN_VAR_FRICTION }, + { "gp_axislh", BUILTIN_VAR_GP_AXIS_LH }, + { "gp_axislv", BUILTIN_VAR_GP_AXIS_LV }, + { "gp_axisrh", BUILTIN_VAR_GP_AXIS_RH }, + { "gp_axisrv", BUILTIN_VAR_GP_AXIS_RV }, + { "gp_face1", BUILTIN_VAR_GP_FACE1 }, + { "gp_face2", BUILTIN_VAR_GP_FACE2 }, + { "gp_face3", BUILTIN_VAR_GP_FACE3 }, + { "gp_face4", BUILTIN_VAR_GP_FACE4 }, + { "gp_home", BUILTIN_VAR_GP_HOME }, + { "gp_padd", BUILTIN_VAR_GP_PADD }, + { "gp_padl", BUILTIN_VAR_GP_PADL }, + { "gp_padr", BUILTIN_VAR_GP_PADR }, + { "gp_padu", BUILTIN_VAR_GP_PADU }, + { "gp_select", BUILTIN_VAR_GP_SELECT }, + { "gp_shoulderl", BUILTIN_VAR_GP_SHOULDERL }, + { "gp_shoulderlb", BUILTIN_VAR_GP_SHOULDERLB }, + { "gp_shoulderr", BUILTIN_VAR_GP_SHOULDERR }, + { "gp_shoulderrb", BUILTIN_VAR_GP_SHOULDERRB }, + { "gp_start", BUILTIN_VAR_GP_START }, + { "gp_stickl", BUILTIN_VAR_GP_STICKL }, + { "gp_stickr", BUILTIN_VAR_GP_STICKR }, + { "gravity", BUILTIN_VAR_GRAVITY }, + { "gravity_direction", BUILTIN_VAR_GRAVITY_DIRECTION }, + { "hspeed", BUILTIN_VAR_HSPEED }, + { "id", BUILTIN_VAR_ID }, + { "image_alpha", BUILTIN_VAR_IMAGE_ALPHA }, + { "image_angle", BUILTIN_VAR_IMAGE_ANGLE }, + { "image_blend", BUILTIN_VAR_IMAGE_BLEND }, + { "image_index", BUILTIN_VAR_IMAGE_INDEX }, + { "image_number", BUILTIN_VAR_IMAGE_NUMBER }, + { "image_speed", BUILTIN_VAR_IMAGE_SPEED }, + { "image_xscale", BUILTIN_VAR_IMAGE_XSCALE }, + { "image_yscale", BUILTIN_VAR_IMAGE_YSCALE }, + { "keyboard_key", BUILTIN_VAR_KEYBOARD_KEY }, + { "keyboard_lastchar", BUILTIN_VAR_KEYBOARD_LASTCHAR }, + { "keyboard_lastkey", BUILTIN_VAR_KEYBOARD_LASTKEY }, + { "layer", BUILTIN_VAR_LAYER }, + { "mask_index", BUILTIN_VAR_MASK_INDEX }, + { "object_index", BUILTIN_VAR_OBJECT_INDEX }, + { "os_3ds", BUILTIN_VAR_OS_3DS }, + { "os_amazon", BUILTIN_VAR_OS_AMAZON }, + { "os_android", BUILTIN_VAR_OS_ANDROID }, + { "os_bb10", BUILTIN_VAR_OS_BB10 }, + { "os_ios", BUILTIN_VAR_OS_IOS }, + { "os_linux", BUILTIN_VAR_OS_LINUX }, + { "os_llvm_android", BUILTIN_VAR_OS_LLVM_ANDROID }, + { "os_llvm_ios", BUILTIN_VAR_OS_LLVM_IOS }, + { "os_llvm_linux", BUILTIN_VAR_OS_LLVM_LINUX }, + { "os_llvm_macosx", BUILTIN_VAR_OS_LLVM_MACOSX }, + { "os_llvm_psp", BUILTIN_VAR_OS_LLVM_PSP }, + { "os_llvm_symbian", BUILTIN_VAR_OS_LLVM_SYMBIAN }, + { "os_llvm_win32", BUILTIN_VAR_OS_LLVM_WIN32 }, + { "os_llvm_winphone", BUILTIN_VAR_OS_LLVM_WINPHONE }, + { "os_macosx", BUILTIN_VAR_OS_MACOSX }, + { "os_ps3", BUILTIN_VAR_OS_PS3 }, + { "os_ps4", BUILTIN_VAR_OS_PS4 }, + { "os_psp", BUILTIN_VAR_OS_PSP }, + { "os_psvita", BUILTIN_VAR_OS_PSVITA }, + { "os_switch", BUILTIN_VAR_OS_SWITCH }, + { "os_symbian", BUILTIN_VAR_OS_SYMBIAN }, + { "os_tizen", BUILTIN_VAR_OS_TIZEN }, + { "os_type", BUILTIN_VAR_OS_TYPE }, + { "os_unknown", BUILTIN_VAR_OS_UNKNOWN }, + { "os_uwp", BUILTIN_VAR_OS_UWP }, + { "os_wiiu", BUILTIN_VAR_OS_WIIU }, + { "os_win32", BUILTIN_VAR_OS_WIN32 }, + { "os_win8native", BUILTIN_VAR_OS_WIN8NATIVE }, + { "os_windows", BUILTIN_VAR_OS_WINDOWS }, + { "os_winphone", BUILTIN_VAR_OS_WINPHONE }, + { "os_xbox360", BUILTIN_VAR_OS_XBOX360 }, + { "os_xboxone", BUILTIN_VAR_OS_XBOXONE }, + { "path_action_continue", BUILTIN_VAR_PATH_ACTION_CONTINUE }, + { "path_action_restart", BUILTIN_VAR_PATH_ACTION_RESTART }, + { "path_action_reverse", BUILTIN_VAR_PATH_ACTION_REVERSE }, + { "path_action_stop", BUILTIN_VAR_PATH_ACTION_STOP }, + { "path_endaction", BUILTIN_VAR_PATH_ENDACTION }, + { "path_index", BUILTIN_VAR_PATH_INDEX }, + { "path_orientation", BUILTIN_VAR_PATH_ORIENTATION }, + { "path_position", BUILTIN_VAR_PATH_POSITION }, + { "path_positionprevious", BUILTIN_VAR_PATH_POSITIONPREVIOUS }, + { "path_scale", BUILTIN_VAR_PATH_SCALE }, + { "path_speed", BUILTIN_VAR_PATH_SPEED }, + { "persistent", BUILTIN_VAR_PERSISTENT }, + { "pi", BUILTIN_VAR_PI }, + { "room", BUILTIN_VAR_ROOM }, + { "room_first", BUILTIN_VAR_ROOM_FIRST }, + { "room_height", BUILTIN_VAR_ROOM_HEIGHT }, + { "room_persistent", BUILTIN_VAR_ROOM_PERSISTENT }, + { "room_speed", BUILTIN_VAR_ROOM_SPEED }, + { "room_width", BUILTIN_VAR_ROOM_WIDTH }, + { "solid", BUILTIN_VAR_SOLID }, + { "speed", BUILTIN_VAR_SPEED }, + { "sprite_height", BUILTIN_VAR_SPRITE_HEIGHT }, + { "sprite_index", BUILTIN_VAR_SPRITE_INDEX }, + { "sprite_width", BUILTIN_VAR_SPRITE_WIDTH }, + { "sprite_xoffset", BUILTIN_VAR_SPRITE_XOFFSET }, + { "sprite_yoffset", BUILTIN_VAR_SPRITE_YOFFSET }, + { "true", BUILTIN_VAR_TRUE }, + { "undefined", BUILTIN_VAR_UNDEFINED }, + { "view_angle", BUILTIN_VAR_VIEW_ANGLE }, + { "view_camera", BUILTIN_VAR_CAMERA_VIEW }, + { "view_current", BUILTIN_VAR_VIEW_CURRENT }, + { "view_hborder", BUILTIN_VAR_VIEW_HBORDER }, + { "view_hport", BUILTIN_VAR_VIEW_HPORT }, + { "view_hspeed", BUILTIN_VAR_VIEW_HSPEED }, + { "view_hview", BUILTIN_VAR_VIEW_HVIEW }, + { "view_object", BUILTIN_VAR_VIEW_OBJECT }, + { "view_vborder", BUILTIN_VAR_VIEW_VBORDER }, + { "view_visible", BUILTIN_VAR_VIEW_VISIBLE }, + { "view_vspeed", BUILTIN_VAR_VIEW_VSPEED }, + { "view_wport", BUILTIN_VAR_VIEW_WPORT }, + { "view_wview", BUILTIN_VAR_VIEW_WVIEW }, + { "view_xport", BUILTIN_VAR_VIEW_XPORT }, + { "view_xview", BUILTIN_VAR_VIEW_XVIEW }, + { "view_yport", BUILTIN_VAR_VIEW_YPORT }, + { "view_yview", BUILTIN_VAR_VIEW_YVIEW }, + { "visible", BUILTIN_VAR_VISIBLE }, + { "vspeed", BUILTIN_VAR_VSPEED }, + { "working_directory", BUILTIN_VAR_WORKING_DIRECTORY }, + { "x", BUILTIN_VAR_X }, + { "xprevious", BUILTIN_VAR_XPREVIOUS }, + { "xstart", BUILTIN_VAR_XSTART }, + { "y", BUILTIN_VAR_Y }, + { "yprevious", BUILTIN_VAR_YPREVIOUS }, + { "ystart", BUILTIN_VAR_YSTART }, +}; + +static int compareBuiltinVarEntry(const void* keyPtr, const void* entryPtr) { + const char* key = (const char*) keyPtr; + const BuiltinVarEntry* entry = (const BuiltinVarEntry*) entryPtr; + return strcmp(key, entry->name); +} + +// Resolves a built-in variable name to its enum ID +int16_t VMBuiltins_resolveBuiltinVarId(const char* name) { + size_t count = sizeof(BUILTIN_VAR_TABLE) / sizeof(BUILTIN_VAR_TABLE[0]); + BuiltinVarEntry* hit = (BuiltinVarEntry*) bsearch(name, BUILTIN_VAR_TABLE, count, sizeof(BuiltinVarEntry), compareBuiltinVarEntry); + return hit == nullptr ? BUILTIN_VAR_UNKNOWN : hit->id; +} + +void VMBuiltins_checkIfBuiltinVarTableIsSorted(void) { + size_t count = sizeof(BUILTIN_VAR_TABLE) / sizeof(BUILTIN_VAR_TABLE[0]); + for (size_t i = 1; count > i; i++) { + int cmp = strcmp(BUILTIN_VAR_TABLE[i - 1].name, BUILTIN_VAR_TABLE[i].name); + requireMessageFormatted(cmp < 0, "BUILTIN_VAR_TABLE not strictly sorted at index %zu: '%s' vs '%s' (cmp=%d). Re-sort (LC_ALL=C) or remove duplicates!", i, BUILTIN_VAR_TABLE[i - 1].name, BUILTIN_VAR_TABLE[i].name, cmp); + } +} + +#if defined(PLATFORM_PS3) +#include +#endif +RValue VMBuiltins_getVariable(VMContext* ctx, int16_t builtinVarId, const char* name, int32_t arrayIndex) { + Instance* inst = (Instance*) ctx->currentInstance; + Runner* runner = (Runner*) ctx->runner; + requireNotNull(runner); + + // In the past Butterscotch used cascading ifs for this, which in my opinion looked nicer AND GCC was converting the ifs into a jump table, so it was all well... + // ...until the code changed enough and the GCC heuristic thought "you know what? let's drop the jump table!" + // So that's why this (and setVariable) are a jump table + switch (builtinVarId) { + // File system + case BUILTIN_VAR_WORKING_DIRECTORY: { + FileSystem* fs = runner->fileSystem; + return RValue_makeOwnedString(fs->vtable->resolvePath(fs, "")); + } + + // OS constants + case BUILTIN_VAR_OS_TYPE: + return RValue_makeReal(runner->osType); + case BUILTIN_VAR_OS_UNKNOWN: + return RValue_makeReal(OS_UNKNOWN); + case BUILTIN_VAR_OS_WIN32: + return RValue_makeReal(OS_WINDOWS); + case BUILTIN_VAR_OS_WINDOWS: + return RValue_makeReal(OS_WINDOWS); + case BUILTIN_VAR_OS_MACOSX: + return RValue_makeReal(OS_MACOSX); + case BUILTIN_VAR_OS_PSP: + return RValue_makeReal(OS_PSP); + case BUILTIN_VAR_OS_IOS: + return RValue_makeReal(OS_IOS); + case BUILTIN_VAR_OS_ANDROID: + return RValue_makeReal(OS_ANDROID); + case BUILTIN_VAR_OS_SYMBIAN: + return RValue_makeReal(OS_SYMBIAN); + case BUILTIN_VAR_OS_LINUX: + return RValue_makeReal(OS_LINUX); + case BUILTIN_VAR_OS_WINPHONE: + return RValue_makeReal(OS_WINPHONE); + case BUILTIN_VAR_OS_TIZEN: + return RValue_makeReal(OS_TIZEN); + case BUILTIN_VAR_OS_WIN8NATIVE: + return RValue_makeReal(OS_WIN8NATIVE); + case BUILTIN_VAR_OS_WIIU: + return RValue_makeReal(OS_WIIU); + case BUILTIN_VAR_OS_3DS: + return RValue_makeReal(OS_3DS); + case BUILTIN_VAR_OS_PSVITA: + return RValue_makeReal(OS_PSVITA); + case BUILTIN_VAR_OS_BB10: + return RValue_makeReal(OS_BB10); + case BUILTIN_VAR_OS_PS4: + return RValue_makeReal(OS_PS4); + case BUILTIN_VAR_OS_XBOXONE: + return RValue_makeReal(OS_XBOXONE); + case BUILTIN_VAR_OS_PS3: + return RValue_makeReal(OS_PS3); + case BUILTIN_VAR_OS_XBOX360: + return RValue_makeReal(OS_XBOX360); + case BUILTIN_VAR_OS_UWP: + return RValue_makeReal(OS_UWP); + case BUILTIN_VAR_OS_AMAZON: + return RValue_makeReal(OS_AMAZON); + case BUILTIN_VAR_OS_SWITCH: + return RValue_makeReal(OS_SWITCH); + case BUILTIN_VAR_OS_LLVM_WIN32: + return RValue_makeReal(OS_LLVM_WIN32); + case BUILTIN_VAR_OS_LLVM_MACOSX: + return RValue_makeReal(OS_LLVM_MACOSX); + case BUILTIN_VAR_OS_LLVM_PSP: + return RValue_makeReal(OS_LLVM_PSP); + case BUILTIN_VAR_OS_LLVM_IOS: + return RValue_makeReal(OS_LLVM_IOS); + case BUILTIN_VAR_OS_LLVM_ANDROID: + return RValue_makeReal(OS_LLVM_ANDROID); + case BUILTIN_VAR_OS_LLVM_SYMBIAN: + return RValue_makeReal(OS_LLVM_SYMBIAN); + case BUILTIN_VAR_OS_LLVM_LINUX: + return RValue_makeReal(OS_LLVM_LINUX); + case BUILTIN_VAR_OS_LLVM_WINPHONE: + return RValue_makeReal(OS_LLVM_WINPHONE); + case BUILTIN_VAR_ASYNC_LOAD: + return RValue_makeReal((GMLReal) runner->asyncLoadMapId); + + // Per-instance properties + case BUILTIN_VAR_IMAGE_SPEED: + if (inst == nullptr) break; + return RValue_makeReal(inst->imageSpeed); + case BUILTIN_VAR_IMAGE_INDEX: + if (inst == nullptr) break; + return RValue_makeReal(inst->imageIndex); + case BUILTIN_VAR_IMAGE_XSCALE: + if (inst == nullptr) break; + return RValue_makeReal(inst->imageXscale); + case BUILTIN_VAR_IMAGE_YSCALE: + if (inst == nullptr) break; + return RValue_makeReal(inst->imageYscale); + case BUILTIN_VAR_IMAGE_ANGLE: + if (inst == nullptr) break; + return RValue_makeReal(inst->imageAngle); + case BUILTIN_VAR_IMAGE_ALPHA: + if (inst == nullptr) break; + return RValue_makeReal(inst->imageAlpha); + case BUILTIN_VAR_IMAGE_BLEND: + if (inst == nullptr) break; + return RValue_makeReal((GMLReal) inst->imageBlend); + case BUILTIN_VAR_IMAGE_NUMBER: { + if (inst == nullptr) break; + if (inst->spriteIndex >= 0) { + Sprite* sprite = &ctx->runner->dataWin->sprt.sprites[inst->spriteIndex]; + return RValue_makeReal((GMLReal) sprite->textureCount); + } + return RValue_makeReal(0.0); + } + case BUILTIN_VAR_SPRITE_INDEX: + if (inst == nullptr) break; + return RValue_makeReal((GMLReal) inst->spriteIndex); + case BUILTIN_VAR_SPRITE_WIDTH: { + if (inst == nullptr) break; + if (inst->spriteIndex >= 0 && runner->dataWin->sprt.count > (uint32_t) inst->spriteIndex) { + return RValue_makeReal((GMLReal) runner->dataWin->sprt.sprites[inst->spriteIndex].width * inst->imageXscale); + } + return RValue_makeReal(0.0); + } + case BUILTIN_VAR_SPRITE_HEIGHT: { + if (inst == nullptr) break; + if (inst->spriteIndex >= 0 && runner->dataWin->sprt.count > (uint32_t) inst->spriteIndex) { + return RValue_makeReal((GMLReal) runner->dataWin->sprt.sprites[inst->spriteIndex].height * inst->imageYscale); + } + return RValue_makeReal(0.0); + } + case BUILTIN_VAR_SPRITE_XOFFSET: { + if (inst == nullptr) break; + if (inst->spriteIndex >= 0 && runner->dataWin->sprt.count > (uint32_t) inst->spriteIndex) { + return RValue_makeReal((GMLReal) runner->dataWin->sprt.sprites[inst->spriteIndex].originX * inst->imageXscale); + } + return RValue_makeReal(0.0); + } + case BUILTIN_VAR_SPRITE_YOFFSET: { + if (inst == nullptr) break; + if (inst->spriteIndex >= 0 && runner->dataWin->sprt.count > (uint32_t) inst->spriteIndex) { + return RValue_makeReal((GMLReal) runner->dataWin->sprt.sprites[inst->spriteIndex].originY * inst->imageYscale); + } + return RValue_makeReal(0.0); + } + case BUILTIN_VAR_BBOX_LEFT: { + if (inst == nullptr) break; + InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); + if (!bbox.valid) return RValue_makeReal(inst->x); + // Compat mode caches bbox values rounded via lrintf so GML reads see integers; modern mode returns the raw float bbox. + if (runner->collisionCompatibilityMode) return RValue_makeReal((GMLReal) llrint(bbox.left)); + return RValue_makeReal(bbox.left); + } + case BUILTIN_VAR_BBOX_RIGHT: { + if (inst == nullptr) break; + InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); + if (!bbox.valid) return RValue_makeReal(inst->x); + // Compat mode caches bbox values rounded via lrintf so GML reads see integers; modern mode returns the raw float bbox. + if (runner->collisionCompatibilityMode) return RValue_makeReal((GMLReal) (llrint(bbox.right) - 1)); + return RValue_makeReal(bbox.right); + } + case BUILTIN_VAR_BBOX_TOP: { + if (inst == nullptr) break; + InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); + if (!bbox.valid) return RValue_makeReal(inst->y); + // Compat mode caches bbox values rounded via lrintf so GML reads see integers; modern mode returns the raw float bbox. + if (runner->collisionCompatibilityMode) return RValue_makeReal((GMLReal) llrint(bbox.top)); + return RValue_makeReal(bbox.top); + } + case BUILTIN_VAR_BBOX_BOTTOM: { + if (inst == nullptr) break; + InstanceBBox bbox = Collision_computeBBox(runner->dataWin, inst); + if (!bbox.valid) return RValue_makeReal(inst->y); + // Compat mode caches bbox values rounded via lrintf so GML reads see integers; modern mode returns the raw float bbox. + if (runner->collisionCompatibilityMode) return RValue_makeReal((GMLReal) (llrint(bbox.bottom) - 1)); + return RValue_makeReal(bbox.bottom); + } + case BUILTIN_VAR_VISIBLE: + if (inst == nullptr) break; + return RValue_makeBool(inst->visible); + case BUILTIN_VAR_DEPTH: + if (inst == nullptr) break; + return RValue_makeReal((GMLReal) inst->depth); + case BUILTIN_VAR_LAYER: + if (inst == nullptr) break; + return RValue_makeReal((GMLReal) inst->layer); + case BUILTIN_VAR_X: + if (inst == nullptr) break; + return RValue_makeReal(inst->x); + case BUILTIN_VAR_Y: + if (inst == nullptr) break; + return RValue_makeReal(inst->y); + case BUILTIN_VAR_XPREVIOUS: + if (inst == nullptr) break; + return RValue_makeReal(inst->xprevious); + case BUILTIN_VAR_YPREVIOUS: + if (inst == nullptr) break; + return RValue_makeReal(inst->yprevious); + case BUILTIN_VAR_XSTART: + if (inst == nullptr) break; + return RValue_makeReal(inst->xstart); + case BUILTIN_VAR_YSTART: + if (inst == nullptr) break; + return RValue_makeReal(inst->ystart); + case BUILTIN_VAR_MASK_INDEX: + if (inst == nullptr) break; + return RValue_makeReal((GMLReal) inst->maskIndex); + case BUILTIN_VAR_ID: + if (inst == nullptr) break; + return RValue_makeReal((GMLReal) inst->instanceId); + case BUILTIN_VAR_OBJECT_INDEX: + if (inst == nullptr) break; + return RValue_makeReal((GMLReal) inst->objectIndex); + case BUILTIN_VAR_PERSISTENT: + if (inst == nullptr) break; + return RValue_makeBool(inst->persistent); + case BUILTIN_VAR_SOLID: + if (inst == nullptr) break; + return RValue_makeBool(inst->solid); + case BUILTIN_VAR_SPEED: + if (inst == nullptr) break; + return RValue_makeReal(inst->speed); + case BUILTIN_VAR_DIRECTION: + if (inst == nullptr) break; + return RValue_makeReal(inst->direction); + case BUILTIN_VAR_HSPEED: + if (inst == nullptr) break; + return RValue_makeReal(inst->hspeed); + case BUILTIN_VAR_VSPEED: + if (inst == nullptr) break; + return RValue_makeReal(inst->vspeed); + case BUILTIN_VAR_FRICTION: + if (inst == nullptr) break; + return RValue_makeReal(inst->friction); + case BUILTIN_VAR_GRAVITY: + if (inst == nullptr) break; + return RValue_makeReal(inst->gravity); + case BUILTIN_VAR_GRAVITY_DIRECTION: + if (inst == nullptr) break; + return RValue_makeReal(inst->gravityDirection); + case BUILTIN_VAR_ALARM: { + if (inst == nullptr) break; + if (isValidAlarmIndex(arrayIndex)) return RValue_makeReal((GMLReal) inst->alarm[arrayIndex]); + return RValue_makeReal(-1.0); + } + + // Path instance variables + case BUILTIN_VAR_PATH_INDEX: + if (inst == nullptr) break; + return RValue_makeReal((GMLReal) inst->pathIndex); + case BUILTIN_VAR_PATH_POSITION: + if (inst == nullptr) break; + return RValue_makeReal(inst->pathPosition); + case BUILTIN_VAR_PATH_POSITIONPREVIOUS: + if (inst == nullptr) break; + return RValue_makeReal(inst->pathPositionPrevious); + case BUILTIN_VAR_PATH_SPEED: + if (inst == nullptr) break; + return RValue_makeReal(inst->pathSpeed); + case BUILTIN_VAR_PATH_SCALE: + if (inst == nullptr) break; + return RValue_makeReal(inst->pathScale); + case BUILTIN_VAR_PATH_ORIENTATION: + if (inst == nullptr) break; + return RValue_makeReal(inst->pathOrientation); + case BUILTIN_VAR_PATH_ENDACTION: + if (inst == nullptr) break; + return RValue_makeReal((GMLReal) inst->pathEndAction); + + // Room properties + case BUILTIN_VAR_ROOM: + return RValue_makeReal((GMLReal) runner->currentRoomIndex); + case BUILTIN_VAR_ROOM_FIRST: + return RValue_makeReal((GMLReal) runner->dataWin->gen8.roomOrder[0]); + case BUILTIN_VAR_ROOM_SPEED: + return RValue_makeReal((GMLReal) runner->currentRoom->speed); + case BUILTIN_VAR_ROOM_WIDTH: + return RValue_makeReal((GMLReal) runner->currentRoom->width); + case BUILTIN_VAR_ROOM_HEIGHT: + return RValue_makeReal((GMLReal) runner->currentRoom->height); + case BUILTIN_VAR_ROOM_PERSISTENT: + return RValue_makeBool(runner->currentRoom->persistent); + + // View properties + case BUILTIN_VAR_VIEW_CURRENT: + case BUILTIN_VAR_CAMERA_VIEW: + return RValue_makeReal((GMLReal) runner->viewCurrent); + case BUILTIN_VAR_VIEW_XVIEW: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewX); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_YVIEW: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewY); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_WVIEW: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewWidth); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_HVIEW: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewHeight); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_XPORT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].portX); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_YPORT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].portY); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_WPORT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].portWidth); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_HPORT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].portHeight); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_VISIBLE: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeBool(runner->views[arrayIndex].enabled); + return RValue_makeBool(false); + case BUILTIN_VAR_VIEW_ANGLE: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].viewAngle); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_HBORDER: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].borderX); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_VBORDER: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].borderY); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_OBJECT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].objectId); + return RValue_makeReal(INSTANCE_NOONE); + case BUILTIN_VAR_VIEW_HSPEED: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].speedX); + return RValue_makeReal(0.0); + case BUILTIN_VAR_VIEW_VSPEED: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) return RValue_makeReal((GMLReal) runner->views[arrayIndex].speedY); + return RValue_makeReal(0.0); + + // Background properties + case BUILTIN_VAR_BACKGROUND_VISIBLE: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeBool(runner->backgrounds[arrayIndex].visible); + return RValue_makeBool(false); + case BUILTIN_VAR_BACKGROUND_INDEX: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].backgroundIndex); + return RValue_makeReal(-1.0); + case BUILTIN_VAR_BACKGROUND_X: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].x); + return RValue_makeReal(0.0); + case BUILTIN_VAR_BACKGROUND_Y: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].y); + return RValue_makeReal(0.0); + case BUILTIN_VAR_BACKGROUND_HSPEED: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].speedX); + return RValue_makeReal(0.0); + case BUILTIN_VAR_BACKGROUND_VSPEED: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].speedY); + return RValue_makeReal(0.0); + case BUILTIN_VAR_BACKGROUND_WIDTH: { + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) { + int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, runner->backgrounds[arrayIndex].backgroundIndex); + if (tpagIndex >= 0) return RValue_makeReal((GMLReal) runner->dataWin->tpag.items[tpagIndex].boundingWidth); + } + return RValue_makeReal(0.0); + } + case BUILTIN_VAR_BACKGROUND_HEIGHT: { + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) { + int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, runner->backgrounds[arrayIndex].backgroundIndex); + if (tpagIndex >= 0) return RValue_makeReal((GMLReal) runner->dataWin->tpag.items[tpagIndex].boundingHeight); + } + return RValue_makeReal(0.0); + } + case BUILTIN_VAR_BACKGROUND_ALPHA: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) return RValue_makeReal((GMLReal) runner->backgrounds[arrayIndex].alpha); + return RValue_makeReal(1.0); + case BUILTIN_VAR_BACKGROUND_COLOR: + case BUILTIN_VAR_BACKGROUND_COLOUR: + return RValue_makeReal((GMLReal) runner->backgroundColor); + + // Timing + case BUILTIN_VAR_CURRENT_TIME: { + #ifdef _WIN32 + LARGE_INTEGER freq, counter; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&counter); + GMLReal ms = (GMLReal) counter.QuadPart / (GMLReal) freq.QuadPart * 1000.0; + #elif defined(PLATFORM_PS3) + GMLReal ms = (GMLReal) (__builtin_ppc_get_timebase() / sysGetTimebaseFrequency()) / 1000000.0; + #else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + GMLReal ms = (GMLReal) ts.tv_sec * 1000.0 + (GMLReal) ts.tv_nsec / 1000000.0; + #endif + return RValue_makeReal(ms); + } + + // Arguments + case BUILTIN_VAR_ARGUMENT_COUNT: + return RValue_makeReal((GMLReal) ctx->scriptArgCount); + case BUILTIN_VAR_ARGUMENT: { + if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > arrayIndex && arrayIndex >= 0) { + RValue val = ctx->scriptArgs[arrayIndex]; + val.ownsReference = false; + return val; + } + return RValue_makeUndefined(); + } + case BUILTIN_VAR_ARGUMENT0 ... BUILTIN_VAR_ARGUMENT15: { + int argNumber = builtinVarId - BUILTIN_VAR_ARGUMENT0; + if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > argNumber) { + RValue val = ctx->scriptArgs[argNumber]; + val.ownsReference = false; + return val; + } + return RValue_makeUndefined(); + } + + // Keyboard + case BUILTIN_VAR_KEYBOARD_KEY: + return RValue_makeReal((GMLReal) runner->keyboard->lastKey); + case BUILTIN_VAR_KEYBOARD_LASTCHAR: + return RValue_makeString(runner->keyboard->lastChar); + case BUILTIN_VAR_KEYBOARD_LASTKEY: + return RValue_makeReal((GMLReal) runner->keyboard->lastKey); + + // Surfaces + case BUILTIN_VAR_APPLICATION_SURFACE: + return RValue_makeReal(-1.0); // sentinel ID for the application surface + + // Constants that GMS defines + case BUILTIN_VAR_TRUE: + return RValue_makeBool(true); + case BUILTIN_VAR_FALSE: + return RValue_makeBool(false); + case BUILTIN_VAR_PI: + return RValue_makeReal(3.14159265358979323846); + case BUILTIN_VAR_UNDEFINED: + return RValue_makeUndefined(); + + // Path action constants + case BUILTIN_VAR_PATH_ACTION_STOP: + return RValue_makeReal(0.0); + case BUILTIN_VAR_PATH_ACTION_RESTART: + return RValue_makeReal(1.0); + case BUILTIN_VAR_PATH_ACTION_CONTINUE: + return RValue_makeReal(2.0); + case BUILTIN_VAR_PATH_ACTION_REVERSE: + return RValue_makeReal(3.0); + + // Buffer type constants + case BUILTIN_VAR_BUFFER_FIXED: + return RValue_makeReal(GML_BUFFER_FIXED); + case BUILTIN_VAR_BUFFER_GROW: + return RValue_makeReal(GML_BUFFER_GROW); + case BUILTIN_VAR_BUFFER_WRAP: + return RValue_makeReal(GML_BUFFER_WRAP); + case BUILTIN_VAR_BUFFER_FAST: + return RValue_makeReal(GML_BUFFER_FAST); + + // Buffer data type constants + case BUILTIN_VAR_BUFFER_U8: + return RValue_makeReal(GML_BUFTYPE_U8); + case BUILTIN_VAR_BUFFER_S8: + return RValue_makeReal(GML_BUFTYPE_S8); + case BUILTIN_VAR_BUFFER_U16: + return RValue_makeReal(GML_BUFTYPE_U16); + case BUILTIN_VAR_BUFFER_S16: + return RValue_makeReal(GML_BUFTYPE_S16); + case BUILTIN_VAR_BUFFER_U32: + return RValue_makeReal(GML_BUFTYPE_U32); + case BUILTIN_VAR_BUFFER_S32: + return RValue_makeReal(GML_BUFTYPE_S32); + case BUILTIN_VAR_BUFFER_F16: + return RValue_makeReal(GML_BUFTYPE_F16); + case BUILTIN_VAR_BUFFER_F32: + return RValue_makeReal(GML_BUFTYPE_F32); + case BUILTIN_VAR_BUFFER_F64: + return RValue_makeReal(GML_BUFTYPE_F64); + case BUILTIN_VAR_BUFFER_BOOL: + return RValue_makeReal(GML_BUFTYPE_BOOL); + case BUILTIN_VAR_BUFFER_STRING: + return RValue_makeReal(GML_BUFTYPE_STRING); + case BUILTIN_VAR_BUFFER_U64: + return RValue_makeReal(GML_BUFTYPE_U64); + case BUILTIN_VAR_BUFFER_TEXT: + return RValue_makeReal(GML_BUFTYPE_TEXT); + + // Buffer seek mode constants + case BUILTIN_VAR_BUFFER_SEEK_START: + return RValue_makeReal(GML_BUFFER_SEEK_START); + case BUILTIN_VAR_BUFFER_SEEK_RELATIVE: + return RValue_makeReal(GML_BUFFER_SEEK_RELATIVE); + case BUILTIN_VAR_BUFFER_SEEK_END: + return RValue_makeReal(GML_BUFFER_SEEK_END); + + // Gamepad constants + case BUILTIN_VAR_GP_FACE1: + return RValue_makeReal(GP_FACE1); + case BUILTIN_VAR_GP_FACE2: + return RValue_makeReal(GP_FACE2); + case BUILTIN_VAR_GP_FACE3: + return RValue_makeReal(GP_FACE3); + case BUILTIN_VAR_GP_FACE4: + return RValue_makeReal(GP_FACE4); + case BUILTIN_VAR_GP_SHOULDERL: + return RValue_makeReal(GP_SHOULDERL); + case BUILTIN_VAR_GP_SHOULDERR: + return RValue_makeReal(GP_SHOULDERR); + case BUILTIN_VAR_GP_SHOULDERLB: + return RValue_makeReal(GP_SHOULDERLB); + case BUILTIN_VAR_GP_SHOULDERRB: + return RValue_makeReal(GP_SHOULDERRB); + case BUILTIN_VAR_GP_SELECT: + return RValue_makeReal(GP_SELECT); + case BUILTIN_VAR_GP_START: + return RValue_makeReal(GP_START); + case BUILTIN_VAR_GP_STICKL: + return RValue_makeReal(GP_STICKL); + case BUILTIN_VAR_GP_STICKR: + return RValue_makeReal(GP_STICKR); + case BUILTIN_VAR_GP_PADU: + return RValue_makeReal(GP_PADU); + case BUILTIN_VAR_GP_PADD: + return RValue_makeReal(GP_PADD); + case BUILTIN_VAR_GP_PADL: + return RValue_makeReal(GP_PADL); + case BUILTIN_VAR_GP_PADR: + return RValue_makeReal(GP_PADR); + case BUILTIN_VAR_GP_HOME: + return RValue_makeReal(GP_HOME); + case BUILTIN_VAR_GP_AXIS_LH: + return RValue_makeReal(GP_AXIS_LH); + case BUILTIN_VAR_GP_AXIS_LV: + return RValue_makeReal(GP_AXIS_LV); + case BUILTIN_VAR_GP_AXIS_RH: + return RValue_makeReal(GP_AXIS_RH); + case BUILTIN_VAR_GP_AXIS_RV: + return RValue_makeReal(GP_AXIS_RV); + + case BUILTIN_VAR_FPS: + return RValue_makeReal(ctx->dataWin->gen8.gms2FPS); + case BUILTIN_VAR_DEBUG_MODE: + return RValue_makeBool(false); + + default: + break; + } + + fprintf(stderr, "VM: [%s] Unhandled built-in variable read '%s' (arrayIndex=%d)\n", ctx->currentCodeName, name, arrayIndex); + return RValue_makeReal(0.0); +} + +void VMBuiltins_setVariable(VMContext* ctx, int16_t builtinVarId, const char* name, RValue val, int32_t arrayIndex) { + Instance* inst = (Instance*) ctx->currentInstance; + Runner* runner = (Runner*) requireNotNullMessage(ctx->runner, "VM: setVariable called but no runner!"); + requireNotNull(runner); + + switch (builtinVarId) { + // Per-instance properties + case BUILTIN_VAR_IMAGE_SPEED: + if (inst == nullptr) break; + inst->imageSpeed = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_IMAGE_INDEX: { + if (inst == nullptr) break; + inst->imageIndex = (float) RValue_toReal(val); + return; + } + case BUILTIN_VAR_IMAGE_XSCALE: { + if (inst == nullptr) break; + float value = (float) RValue_toReal(val); + bool changed = value != inst->imageXscale; + if (changed) { + inst->imageXscale = value; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + } + return; + } + case BUILTIN_VAR_IMAGE_YSCALE: { + if (inst == nullptr) break; + float value = (float) RValue_toReal(val); + bool changed = value != inst->imageYscale; + if (changed) { + inst->imageYscale = value; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + } + return; + } + case BUILTIN_VAR_IMAGE_ANGLE: { + if (inst == nullptr) break; + float value = (float) RValue_toReal(val); + bool changed = value != inst->imageAngle; + if (changed) { + inst->imageAngle = value; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + } + return; + } + case BUILTIN_VAR_IMAGE_ALPHA: + if (inst == nullptr) break; + inst->imageAlpha = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_IMAGE_BLEND: + if (inst == nullptr) break; + inst->imageBlend = (uint32_t) RValue_toReal(val); + return; + case BUILTIN_VAR_SPRITE_INDEX: { + if (inst == nullptr) break; + int32_t value = RValue_toInt32(val); + bool changed = value != inst->spriteIndex; + if (changed) { + inst->spriteIndex = value; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + } + return; + } + case BUILTIN_VAR_VISIBLE: + if (inst == nullptr) break; + inst->visible = RValue_toBool(val); + return; + case BUILTIN_VAR_DEPTH: { + if (inst == nullptr) break; + int32_t newDepth = RValue_toInt32(val); + if (newDepth != inst->depth) { + inst->depth = newDepth; + ((Runner*) ctx->runner)->drawableListSortDirty = true; + } + return; + } + case BUILTIN_VAR_LAYER: { + if (inst == nullptr) break; + int32_t layerId = resolveLayerIdArg(runner, val); + RuntimeLayer* rl = Runner_findRuntimeLayerById(runner, layerId); + if (rl != nullptr) { + inst->layer = layerId; + if (inst->depth != rl->depth) { + inst->depth = rl->depth; + runner->drawableListSortDirty = true; + } + } + return; + } + case BUILTIN_VAR_X: { + if (inst == nullptr) break; + float value = (float) RValue_toReal(val); + bool changed = value != inst->x; + if (changed) { + inst->x = (float) RValue_toReal(val); + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + } + return; + } + case BUILTIN_VAR_Y: { + if (inst == nullptr) break; + float value = (float) RValue_toReal(val); + bool changed = value != inst->y; + if (changed) { + inst->y = (float) RValue_toReal(val); + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + } + return; + } + case BUILTIN_VAR_PERSISTENT: + if (inst == nullptr) break; + inst->persistent = RValue_toBool(val); + return; + case BUILTIN_VAR_SOLID: + if (inst == nullptr) break; + inst->solid = RValue_toBool(val); + return; + case BUILTIN_VAR_XPREVIOUS: + if (inst == nullptr) break; + inst->xprevious = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_YPREVIOUS: + if (inst == nullptr) break; + inst->yprevious = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_XSTART: + if (inst == nullptr) break; + inst->xstart = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_YSTART: + if (inst == nullptr) break; + inst->ystart = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_MASK_INDEX: { + if (inst == nullptr) break; + int32_t value = RValue_toInt32(val); + bool changed = value != inst->maskIndex; + if (changed) { + inst->maskIndex = value; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + } + return; + } + case BUILTIN_VAR_SPEED: + if (inst == nullptr) break; + inst->speed = (float) RValue_toReal(val); + Instance_computeComponentsFromSpeed(inst); + return; + case BUILTIN_VAR_DIRECTION: { + if (inst == nullptr) break; + GMLReal d = GMLReal_fmod(RValue_toReal(val), 360.0); + if (d < 0.0) d += 360.0; + inst->direction = (float) d; + Instance_computeComponentsFromSpeed(inst); + return; + } + case BUILTIN_VAR_HSPEED: + if (inst == nullptr) break; + inst->hspeed = (float) RValue_toReal(val); + Instance_computeSpeedFromComponents(inst); + return; + case BUILTIN_VAR_VSPEED: + if (inst == nullptr) break; + inst->vspeed = (float) RValue_toReal(val); + Instance_computeSpeedFromComponents(inst); + return; + case BUILTIN_VAR_FRICTION: + if (inst == nullptr) break; + inst->friction = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_GRAVITY: + if (inst == nullptr) break; + inst->gravity = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_GRAVITY_DIRECTION: + if (inst == nullptr) break; + inst->gravityDirection = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_ALARM: { + if (inst == nullptr) break; + if (isValidAlarmIndex(arrayIndex)) { + int32_t newValue = RValue_toInt32(val); + +#ifdef ENABLE_VM_TRACING + if (shgeti(ctx->alarmsToBeTraced, "*") != -1 || shgeti(ctx->alarmsToBeTraced, runner->dataWin->objt.objects[inst->objectIndex].name) != -1) { + fprintf(stderr, "VM: [%s] Setting Alarm[%d] = %d (instanceId=%d)\n", runner->dataWin->objt.objects[inst->objectIndex].name, arrayIndex, newValue, inst->instanceId); + } +#endif + + inst->alarm[arrayIndex] = newValue; + if (newValue > 0) inst->activeAlarmMask |= (uint16_t) (1u << arrayIndex); + else inst->activeAlarmMask &= (uint16_t) ~(1u << arrayIndex); + } + return; + } + + // Path instance variables (writable) + case BUILTIN_VAR_PATH_POSITION: { + if (inst == nullptr) break; + // Native GMS runner clamps path_position to [0.0, 1.0] on set + float pos = (float) RValue_toReal(val); + if (pos < 0.0f) pos = 0.0f; + else if (pos > 1.0f) pos = 1.0f; + inst->pathPosition = pos; + return; + } + case BUILTIN_VAR_PATH_SPEED: + if (inst == nullptr) break; + inst->pathSpeed = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_PATH_SCALE: + if (inst == nullptr) break; + inst->pathScale = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_PATH_ORIENTATION: + if (inst == nullptr) break; + inst->pathOrientation = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_PATH_ENDACTION: + if (inst == nullptr) break; + inst->pathEndAction = RValue_toInt32(val); + return; + + // Keyboard variables + case BUILTIN_VAR_KEYBOARD_KEY: + runner->keyboard->lastKey = RValue_toInt32(val); + return; + case BUILTIN_VAR_KEYBOARD_LASTCHAR: + runner->keyboard->lastChar[0] = val.string[0]; + return; + case BUILTIN_VAR_KEYBOARD_LASTKEY: + runner->keyboard->lastKey = RValue_toInt32(val); + return; + + // View properties + case BUILTIN_VAR_VIEW_XVIEW: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewX = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_YVIEW: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewY = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_WVIEW: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewWidth = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_HVIEW: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewHeight = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_XPORT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].portX = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_YPORT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].portY = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_WPORT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].portWidth = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_HPORT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].portHeight = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_VISIBLE: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].enabled = RValue_toBool(val); + return; + case BUILTIN_VAR_VIEW_ANGLE: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].viewAngle = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_VIEW_HBORDER: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].borderX = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_VBORDER: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].borderY = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_OBJECT: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].objectId = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_HSPEED: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].speedX = RValue_toInt32(val); + return; + case BUILTIN_VAR_VIEW_VSPEED: + if (arrayIndex >= 0 && MAX_VIEWS > arrayIndex) runner->views[arrayIndex].speedY = RValue_toInt32(val); + return; + + // Background properties + case BUILTIN_VAR_BACKGROUND_VISIBLE: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].visible = RValue_toBool(val); + return; + case BUILTIN_VAR_BACKGROUND_INDEX: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].backgroundIndex = RValue_toInt32(val); + return; + case BUILTIN_VAR_BACKGROUND_X: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].x = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_BACKGROUND_Y: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].y = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_BACKGROUND_HSPEED: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].speedX = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_BACKGROUND_VSPEED: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].speedY = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_BACKGROUND_ALPHA: + if (arrayIndex >= 0 && MAX_BACKGROUNDS > arrayIndex) runner->backgrounds[arrayIndex].alpha = (float) RValue_toReal(val); + return; + case BUILTIN_VAR_BACKGROUND_COLOR: + case BUILTIN_VAR_BACKGROUND_COLOUR: + runner->backgroundColor = (uint32_t) RValue_toInt32(val); + return; + + // Room properties + case BUILTIN_VAR_ROOM: + runner->pendingRoom = RValue_toInt32(val); + return; + case BUILTIN_VAR_ROOM_PERSISTENT: + runner->currentRoom->persistent = RValue_toBool(val); + return; + case BUILTIN_VAR_ROOM_WIDTH: + runner->currentRoom->width = (uint32_t) RValue_toInt32(val); + return; + case BUILTIN_VAR_ROOM_HEIGHT: + runner->currentRoom->height = (uint32_t) RValue_toInt32(val); + return; + case BUILTIN_VAR_ROOM_SPEED: + runner->currentRoom->speed = (uint32_t) RValue_toInt32(val); + return; + + // Read-only variables (silently ignore with warning) + case BUILTIN_VAR_OS_TYPE ... BUILTIN_VAR_OS_LLVM_WINPHONE: + case BUILTIN_VAR_BUFFER_FIXED ... BUILTIN_VAR_BUFFER_SEEK_END: + case BUILTIN_VAR_ID: + case BUILTIN_VAR_OBJECT_INDEX: + case BUILTIN_VAR_CURRENT_TIME: + case BUILTIN_VAR_VIEW_CURRENT: + case BUILTIN_VAR_PATH_INDEX: + case BUILTIN_VAR_DEBUG_MODE: + case BUILTIN_VAR_ROOM_FIRST: + case BUILTIN_VAR_GP_FACE1 ... BUILTIN_VAR_GP_AXIS_RV: + fprintf(stderr, "VM: Warning - attempted write to read-only built-in '%s'\n", name); + return; + + // argument[N] - array-style write to script arguments + case BUILTIN_VAR_ARGUMENT: + if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > arrayIndex && arrayIndex >= 0) { + RValue_free(&ctx->scriptArgs[arrayIndex]); + ctx->scriptArgs[arrayIndex] = val; + } + return; + + // Argument variables (argument0..argument15) + case BUILTIN_VAR_ARGUMENT0 ... BUILTIN_VAR_ARGUMENT15: { + int argNumber = builtinVarId - BUILTIN_VAR_ARGUMENT0; + if (ctx->scriptArgs != nullptr && ctx->scriptArgCount > argNumber) { + RValue_free(&ctx->scriptArgs[argNumber]); + ctx->scriptArgs[argNumber] = val; + } + return; + } + + default: + break; + } + + fprintf(stderr, "VM: [%s] Unhandled built-in variable write '%s' (arrayIndex=%d)\n", ctx->currentCodeName, name, arrayIndex); +} + +// ===[ BUILTIN FUNCTION IMPLEMENTATIONS ]=== + +static RValue builtinShowDebugMessage(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) { + fprintf(stderr, "[show_debug_message] Expected at least 1 argument\n"); + return RValue_makeUndefined(); + } + + char* val = RValue_toString(args[0]); + printf("Game: %s\n", val); + free(val); + + return RValue_makeUndefined(); +} + +static RValue builtinStringLength(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeInt32(0); + // GML converts non-string arguments to string before measuring length + RValue value = args[0]; + // Fast path: If the RValue is already a string, just return its length instead of creating a copy + if (value.type == RVALUE_STRING) { + if (value.string == nullptr) + return RValue_makeInt32(0); + int32_t byteLen = (int32_t) strlen(value.string); + int32_t len = TextUtils_utf8CodepointCount(value.string, byteLen); + if (TextUtils_hasDeltaruneLeakedTextboxPrefix(value.string, byteLen)) { + // Deltarune's writer expects the stripped backslash to still exist so + // its \E? inline command consumes the face marker before rendering. + len++; + } + return RValue_makeInt32(len); + } + char* str = RValue_toString(value); + int32_t byteLen = (int32_t) strlen(str); + int32_t len = TextUtils_utf8CodepointCount(str, byteLen); + if (TextUtils_hasDeltaruneLeakedTextboxPrefix(str, byteLen)) { + len++; + } + free(str); + return RValue_makeInt32(len); +} + +// https://docs.vultr.com/clang/examples/remove-all-characters-in-a-string-except-alphabets +void filterAlphabets(char *str) { + char result[strlen(str) + 1]; + int j = 0; + for (int i = 0; str[i] != '\0'; i++) { + if ((str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z')) { + result[j++] = str[i]; + } + } + result[j] = '\0'; // Null-terminate the result string + strcpy(str, result); // Optionally copy back to original string +} + +static RValue builtinStringLetters(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeInt32(0); + char* str = RValue_toString(args[0]); + filterAlphabets(str); + return RValue_makeString(str); +} + +static RValue builtinStringByteLength(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeInt32(0); + // GML converts non-string arguments to string before measuring length + RValue value = args[0]; + // Fast path: If the RValue is already a string, just return its length instead of creating a copy + if (value.type == RVALUE_STRING) { + if (value.string == nullptr) + return RValue_makeInt32(0); + int32_t byteLen = (int32_t) strlen(value.string); + return RValue_makeInt32(byteLen); + } + char* str = RValue_toString(value); + int32_t byteLen = (int32_t) strlen(str); + free(str); + return RValue_makeInt32(byteLen); +} + +static RValue builtinReal(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(RValue_toReal(args[0])); +} + +static RValue builtinString(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* result = RValue_toString(args[0]); + return RValue_makeOwnedString(result); +} + +static RValue builtinFloor(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_floor(RValue_toReal(args[0]))); +} + +static RValue builtinCeil(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_ceil(RValue_toReal(args[0]))); +} + +static RValue builtinRound(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + // GameMaker's round() uses banker's rounding (round half to even), matching llrint() under the default IEEE 754 rounding mode. + // C's round()/roundf() rounds half away from zero, which produces different results for x.5 values (e.g. round(2.5) is 2 in GML but 3 with round()). + GMLReal v = RValue_toReal(args[0]); +#ifdef USE_FLOAT_REALS + return RValue_makeReal(rintf(v)); +#else + return RValue_makeReal(rint(v)); +#endif +} + +static RValue builtinAbs(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_fabs(RValue_toReal(args[0]))); +} + +static RValue builtinSign(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + GMLReal val = RValue_toReal(args[0]); + GMLReal result = (val > 0.0) ? 1.0 : ((0.0 > val) ? -1.0 : 0.0); + return RValue_makeReal(result); +} + +static RValue builtinMax(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + GMLReal result = -INFINITY; + repeat(argCount, i) { + GMLReal val = RValue_toReal(args[i]); + if (val > result) result = val; + } + return RValue_makeReal(result); +} + +static RValue builtinMin(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + GMLReal result = INFINITY; + repeat(argCount, i) { + GMLReal val = RValue_toReal(args[i]); + if (result > val) result = val; + } + return RValue_makeReal(result); +} + +static RValue builtinPower(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_pow(RValue_toReal(args[0]), RValue_toReal(args[1]))); +} + +static RValue builtinSqrt(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_sqrt(RValue_toReal(args[0]))); +} + +static RValue builtinSqr(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + GMLReal val = RValue_toReal(args[0]); + return RValue_makeReal(val * val); +} + +static RValue builtinIsString(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + return RValue_makeBool(args[0].type == RVALUE_STRING); +} + +static RValue builtinIsReal(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + bool result = args[0].type == RVALUE_REAL || args[0].type == RVALUE_INT32 || args[0].type == RVALUE_INT64 || args[0].type == RVALUE_BOOL; + return RValue_makeBool(result); +} + +static RValue builtinIsUndefined(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(true); + return RValue_makeBool(args[0].type == RVALUE_UNDEFINED); +} + +// ===[ STRING FUNCTIONS ]=== + +static RValue builtinStringUpper(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* result = RValue_toString(args[0]); + for (char* p = result; *p; p++) *p = (char) toupper((unsigned char) *p); + return RValue_makeOwnedString(result); +} + +static RValue builtinStringLower(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* result = RValue_toString(args[0]); + for (char* p = result; *p; p++) *p = (char) tolower((unsigned char) *p); + return RValue_makeOwnedString(result); +} + +static RValue builtinStringCopy(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); + int32_t len = RValue_toInt32(args[2]); + if (0 >= len) { + return RValue_makeOwnedString(safeStrdup("")); + } + + char* str = RValue_toString(args[0]); + int32_t pos = RValue_toInt32(args[1]) - 1; // GMS is 1-based + int32_t strLen = (int32_t) strlen(str); + + if (0 > pos) pos = 0; + + int32_t byteStart = TextUtils_utf8AdvanceCodepoints(str, strLen, pos); + if (byteStart >= strLen) { + free(str); + return RValue_makeOwnedString(safeStrdup("")); + } + + int32_t byteEnd = byteStart + TextUtils_utf8AdvanceCodepoints(str + byteStart, strLen - byteStart, len); + if (byteEnd > strLen) byteEnd = strLen; + + int32_t nbytes = byteEnd - byteStart; + char* result = safeMalloc(nbytes + 1); + memcpy(result, str + byteStart, (size_t) nbytes); + result[nbytes] = '\0'; + + free(str); + + return RValue_makeOwnedString(result); +} + +static RValue builtinStringFormat(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); + if (args[0].type == RVALUE_UNDEFINED) return RValue_makeOwnedString(safeStrdup("undefined")); + + GMLReal val = RValue_toReal(args[0]); + int32_t tot = RValue_toInt32(args[1]); + int32_t dec = RValue_toInt32(args[2]); + if (0 > dec) dec = 0; + if (15 < dec) dec = 15; + + char numBuf[64]; + snprintf(numBuf, sizeof(numBuf), "%.*f", (int) dec, (double) val); + + const char* dot = strchr(numBuf, '.'); + int32_t intLen = (int32_t) (dot ? (dot - numBuf) : (int32_t) strlen(numBuf)); + + int32_t leftPad = (tot > intLen) ? (tot - intLen) : 0; + int32_t numLen = (int32_t) strlen(numBuf); + int32_t totalLen = leftPad + numLen; + + char* result = safeMalloc(totalLen + 1); + for (int32_t i = 0; leftPad > i; i++) result[i] = ' '; + memcpy(result + leftPad, numBuf, (size_t) numLen); + result[totalLen] = '\0'; + return RValue_makeOwnedString(result); +} + +static RValue builtinStringRepeat(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* str = RValue_toString(args[0]); + int32_t count = RValue_toInt32(args[1]); + if (0 >= count || str[0] == '\0') { + free(str); + return RValue_makeOwnedString(safeStrdup("")); + } + + size_t strLen = strlen(str); + size_t totalLen = strLen * (size_t) count; + char* result = safeMalloc(totalLen + 1); + repeat(count, i) { + memcpy(result + i * strLen, str, strLen); + } + result[totalLen] = '\0'; + free(str); + return RValue_makeOwnedString(result); +} + +static RValue builtinStringCount(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeInt32(0); + char* substr = RValue_toString(args[0]); + char* str = RValue_toString(args[1]); + size_t strLen = strlen(str); + size_t substrLen = strlen(substr); + int32_t count = 0; + + if (substrLen > strLen) { + free(substr); + free(str); + return RValue_makeInt32(0); + } + + repeat(strLen, i) { + if (strncmp(str + i, substr, substrLen) == 0) + count++; + } + + free(substr); + free(str); + return RValue_makeInt32(count); +} + +static RValue builtinStringDigits(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* str = RValue_toString(args[0]); + int len = strlen(str); + char* result = (char*)malloc(len + 1); + if (result == NULL) return RValue_makeOwnedString(safeStrdup("")); + + int digitCount = 0; + for (int i = 0; str[i] != '\0'; i++) { + if (isdigit(str[i])) result[digitCount++] = str[i]; + } + + free(str); + result[digitCount] = '\0'; + + if (digitCount == 0) { + free(result); + return RValue_makeOwnedString(safeStrdup("")); + } + + char* exact_result = (char*)realloc(result, digitCount + 1); + return RValue_makeOwnedString(exact_result ? exact_result : result); +} + +static RValue builtinOrd(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount || args[0].type != RVALUE_STRING || args[0].string == nullptr || args[0].string[0] == '\0') { + return RValue_makeReal(0.0); + } + const char* str = args[0].string; + int32_t pos = 0; + uint16_t cp = TextUtils_decodeUtf8(str, (int32_t)strlen(str), &pos); + return RValue_makeReal((GMLReal) cp); +} + +static RValue builtinChr(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); + uint32_t cp = (uint32_t) RValue_toInt32(args[0]); + + // Preserve legacy single-byte GameMaker character semantics for 0x00-0xFF. + // Some games, including Deltarune's textbox system, use bytes in the 0xE0-0xFF + // range as inline control characters and expect chr(n) to produce a one-byte + // string rather than a UTF-8 multibyte sequence. + if (cp <= 0xFFU) { + char* out = safeMalloc(2); + out[0] = (char) cp; + out[1] = '\0'; + return RValue_makeOwnedString(out); + } + + char buf[5]; + int32_t n = TextUtils_utf8EncodeCodepoint(cp, buf); + if (0 >= n) return RValue_makeOwnedString(safeStrdup("")); + buf[n] = '\0'; + return RValue_makeOwnedString(safeStrdup(buf)); +} + +static RValue builtinStringPos(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + char* needle = RValue_toString(args[0]); + char* haystack = RValue_toString(args[1]); + char* found = strstr(haystack, needle); + if (found == nullptr) { + free(haystack); + free(needle); + return RValue_makeReal(0.0); + } + int32_t byteIndex = (int32_t) (found - haystack); + int32_t charIndex = TextUtils_utf8CodepointCount(haystack, byteIndex) + 1; // 1-based codepoint index + free(haystack); + free(needle); + return RValue_makeReal((GMLReal) charIndex); +} + +static RValue builtinStringCharAt(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* str = RValue_toString(args[0]); + int32_t pos = RValue_toInt32(args[1]) - 1; // 1-based + int32_t strLen = (int32_t) strlen(str); + if (TextUtils_hasDeltaruneLeakedTextboxPrefix(str, strLen)) { + if (pos == 0) { + free(str); + return RValue_makeOwnedString(safeStrdup("\\")); + } + pos--; + } + if (0 > pos || pos >= strLen) { + free(str); + return RValue_makeOwnedString(safeStrdup("")); + } + int32_t byteStart = TextUtils_utf8AdvanceCodepoints(str, strLen, pos); + if (byteStart >= strLen) { + free(str); + return RValue_makeOwnedString(safeStrdup("")); + } + int32_t byteNext = byteStart; + TextUtils_decodeUtf8(str, strLen, &byteNext); + int32_t nbytes = byteNext - byteStart; + char* out = safeMalloc(nbytes + 1); + memcpy(out, str + byteStart, (size_t) nbytes); + out[nbytes] = '\0'; + free(str); + return RValue_makeOwnedString(out); +} + +static RValue builtinStringDelete(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* str = RValue_toString(args[0]); + int32_t pos = RValue_toInt32(args[1]) - 1; // 1-based + int32_t count = RValue_toInt32(args[2]); + int32_t strLen = (int32_t) strlen(str); + + if (0 > pos || pos >= strLen || 0 >= count) return RValue_makeOwnedString(str); + + int32_t byteStart = TextUtils_utf8AdvanceCodepoints(str, strLen, pos); + if (byteStart >= strLen) return RValue_makeOwnedString(str); + + int32_t byteEnd = byteStart + TextUtils_utf8AdvanceCodepoints(str + byteStart, strLen - byteStart, count); + if (byteEnd > strLen) byteEnd = strLen; + + int32_t removeLen = byteEnd - byteStart; + char* result = safeMalloc(strLen - removeLen + 1); + memcpy(result, str, (size_t) byteStart); + memcpy(result + byteStart, str + byteEnd, (size_t) (strLen - byteEnd)); + result[strLen - removeLen] = '\0'; + + free(str); + + return RValue_makeOwnedString(result); +} + +static RValue builtinStringInsert(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* substr = RValue_toString(args[0]); + char* str = RValue_toString(args[1]); + int32_t pos = RValue_toInt32(args[2]) - 1; // 1-based + int32_t strLen = (int32_t) strlen(str); + int32_t subLen = (int32_t) strlen(substr); + + if (0 > pos) pos = 0; + int32_t bytePos = TextUtils_utf8AdvanceCodepoints(str, strLen, pos); + if (bytePos > strLen) bytePos = strLen; + + char* result = safeMalloc(strLen + subLen + 1); + memcpy(result, str, (size_t) bytePos); + memcpy(result + bytePos, substr, (size_t) subLen); + memcpy(result + bytePos + subLen, str + bytePos, (size_t) (strLen - bytePos)); + result[strLen + subLen] = '\0'; + + free(substr); + free(str); + + return RValue_makeOwnedString(result); +} + +static RValue builtinStringReplace(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* str = RValue_toString(args[0]); + char* needle = RValue_toString(args[1]); + int32_t strLen = (int32_t) strlen(str); + int32_t needleLen = (int32_t) strlen(needle); + if (0 == needleLen) { + free(needle); + return RValue_makeOwnedString(str); + } + + char* replacement = RValue_toString(args[2]); + int32_t replacementLen = (int32_t) strlen(replacement); + + // There can be only ONE. + char *appearance = strstr(str, needle); + if (!appearance) { + free(needle); + free(replacement); + return RValue_makeOwnedString(str); + } + + int32_t newLen = strLen - needleLen + replacementLen; + int32_t before = (int32_t) (appearance - str); + char *outputString = safeMalloc(newLen + 1); + + strncpy(outputString, str, before); + strncpy(outputString + before, replacement, replacementLen); + strcpy(outputString + before + replacementLen, appearance + needleLen); + + free(str); + free(needle); + free(replacement); + + return RValue_makeOwnedString(outputString); +} + +static RValue builtinStringReplaceAll(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeOwnedString(safeStrdup("")); + char* str = RValue_toString(args[0]); + char* needle = RValue_toString(args[1]); + int32_t needleLen = (int32_t) strlen(needle); + if (0 == needleLen) { + free(needle); + return RValue_makeOwnedString(str); + } + + char* replacement = RValue_toString(args[2]); + int32_t replacementLen = (int32_t) strlen(replacement); + + // Count occurrences to pre-allocate + int32_t count = 0; + const char* p = str; + while ((p = strstr(p, needle)) != nullptr) { count++; p += needleLen; } + + int32_t strLen = (int32_t) strlen(str); + int32_t resultLen = strLen + count * (replacementLen - needleLen); + char* result = safeMalloc(resultLen + 1); + char* out = result; + p = str; + const char* match; + while ((match = strstr(p, needle)) != nullptr) { + int32_t before = (int32_t) (match - p); + memcpy(out, p, before); + out += before; + memcpy(out, replacement, replacementLen); + out += replacementLen; + p = match + needleLen; + } + strcpy(out, p); + + free(replacement); + free(needle); + free(str); + + return RValue_makeOwnedString(result); +} + +// ===[ MATH FUNCTIONS ]=== + +static RValue builtinDarctan2(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + GMLReal y = RValue_toReal(args[0]); + GMLReal x = RValue_toReal(args[1]); + return RValue_makeReal(GMLReal_atan2(y, x) * (180.0 / M_PI)); +} + +static RValue builtinSin(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_sin(RValue_toReal(args[0]))); +} + +static RValue builtinArcsin(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_asin(RValue_toReal(args[0]))); +} + +static RValue builtinCos(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_cos(RValue_toReal(args[0]))); +} + +static RValue builtinDsin(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_sin(RValue_toReal(args[0]) * (M_PI / 180.0))); +} + +static RValue builtinDcos(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(GMLReal_cos(RValue_toReal(args[0]) * (M_PI / 180.0))); +} + +static RValue builtinDegtorad(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(RValue_toReal(args[0]) * (M_PI / 180.0)); +} + +static RValue builtinRadtodeg(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + return RValue_makeReal(RValue_toReal(args[0]) * (180.0 / M_PI)); +} + +static RValue builtinClamp(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeReal(0.0); + GMLReal val = RValue_toReal(args[0]); + GMLReal lo = RValue_toReal(args[1]); + GMLReal hi = RValue_toReal(args[2]); + if (lo > val) val = lo; + if (val > hi) val = hi; + return RValue_makeReal(val); +} + +static RValue builtinLerp(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeReal(0.0); + GMLReal a = RValue_toReal(args[0]); + GMLReal b = RValue_toReal(args[1]); + GMLReal t = RValue_toReal(args[2]); + return RValue_makeReal(a + (b - a) * t); +} + +static RValue builtinPointDistance(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (4 > argCount) return RValue_makeReal(0.0); + GMLReal dx = RValue_toReal(args[2]) - RValue_toReal(args[0]); + GMLReal dy = RValue_toReal(args[3]) - RValue_toReal(args[1]); + return RValue_makeReal(GMLReal_sqrt(dx * dx + dy * dy)); +} + +static RValue builtinPointInRectangle(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (6 > argCount) return RValue_makeBool(false); + GMLReal px = RValue_toReal(args[0]); + GMLReal py = RValue_toReal(args[1]); + GMLReal x1 = RValue_toReal(args[2]); + GMLReal y1 = RValue_toReal(args[3]); + GMLReal x2 = RValue_toReal(args[4]); + GMLReal y2 = RValue_toReal(args[5]); + return RValue_makeBool(px >= x1 && px <= x2 && py >= y1 && py <= y2); +} + +static RValue builtinDistanceToPoint(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + GMLReal px = RValue_toReal(args[0]); + GMLReal py = RValue_toReal(args[1]); + + Instance* inst = ctx->currentInstance; + int32_t sprIdx = (inst->maskIndex >= 0) ? inst->maskIndex : inst->spriteIndex; + + // Compute bounding box + GMLReal bboxLeft, bboxRight, bboxTop, bboxBottom; + if (0 > sprIdx || (uint32_t) sprIdx >= ctx->dataWin->sprt.count) { + // No sprite/mask: treat bbox as a single point at (x, y) + bboxLeft = inst->x; + bboxRight = inst->x; + bboxTop = inst->y; + bboxBottom = inst->y; + } else { + Sprite* spr = &ctx->dataWin->sprt.sprites[sprIdx]; + bboxLeft = inst->x + inst->imageXscale * (spr->marginLeft - spr->originX); + bboxRight = inst->x + inst->imageXscale * ((spr->marginRight + 1) - spr->originX); + if (bboxLeft > bboxRight) { + GMLReal t = bboxLeft; + bboxLeft = bboxRight; + bboxRight = t; + } + bboxTop = inst->y + inst->imageYscale * (spr->marginTop - spr->originY); + bboxBottom = inst->y + inst->imageYscale * ((spr->marginBottom + 1) - spr->originY); + if (bboxTop > bboxBottom) { + GMLReal t = bboxTop; + bboxTop = bboxBottom; + bboxBottom = t; + } + } + + // Distance from point to nearest edge of bbox (0 if inside) + GMLReal xd = 0.0; + GMLReal yd = 0.0; + if (px > bboxRight) xd = px - bboxRight; + if (px < bboxLeft) xd = px - bboxLeft; + if (py > bboxBottom) yd = py - bboxBottom; + if (py < bboxTop) yd = py - bboxTop; + + return RValue_makeReal(GMLReal_sqrt(xd * xd + yd * yd)); +} + +// distance_to_object(obj) +// Returns the minimum bbox-to-bbox distance between the calling instance and the nearest instance of the given object. +static RValue builtinDistanceToObject(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + + Runner* runner = (Runner*) ctx->runner; + int32_t targetObjIndex = RValue_toInt32(args[0]); + Instance* self = ctx->currentInstance; + + // Compute self bbox + Sprite* selfSpr = Collision_getSprite(ctx->dataWin, self); + if (selfSpr == nullptr) return RValue_makeReal(0.0); + InstanceBBox selfBBox = Collision_computeBBox(ctx->dataWin, self); + if (!selfBBox.valid) return RValue_makeReal(0.0); + + GMLReal minDistSq = 1e20; + + int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (!inst->active || inst == self) continue; + + InstanceBBox otherBBox = Collision_computeBBox(ctx->dataWin, inst); + if (!otherBBox.valid) continue; + + GMLReal xd = 0.0; + GMLReal yd = 0.0; + if (otherBBox.left > selfBBox.right) xd = otherBBox.left - selfBBox.right; + if (selfBBox.left > otherBBox.right) xd = selfBBox.left - otherBBox.right; + if (otherBBox.top > selfBBox.bottom) yd = otherBBox.top - selfBBox.bottom; + if (selfBBox.top > otherBBox.bottom) yd = selfBBox.top - otherBBox.bottom; + + GMLReal distSq = xd * xd + yd * yd; + if (minDistSq > distSq) minDistSq = distSq; + } + Runner_popInstanceSnapshot(runner, snapBase); + + return RValue_makeReal(GMLReal_sqrt(minDistSq)); +} + +static RValue builtinPointDirection(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (4 > argCount) return RValue_makeReal(0.0); + GMLReal dx = RValue_toReal(args[2]) - RValue_toReal(args[0]); + GMLReal dy = RValue_toReal(args[3]) - RValue_toReal(args[1]); + return RValue_makeReal(GMLReal_atan2(-dy, dx) * (180.0 / M_PI)); +} + +static RValue builtinAngleDifference(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + GMLReal src = RValue_toReal(args[0]); + GMLReal dest = RValue_toReal(args[1]); + return RValue_makeReal(GMLReal_fmod(GMLReal_fmod(src - dest, 360.0) + 540.0, 360.0) - 180.0); +} + +static RValue builtinMoveTowardsPoint(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal targetX = RValue_toReal(args[0]); + GMLReal targetY = RValue_toReal(args[1]); + GMLReal spd = RValue_toReal(args[2]); + Instance* inst = ctx->currentInstance; + GMLReal dx = targetX - inst->x; + GMLReal dy = targetY - inst->y; + GMLReal dir = GMLReal_atan2(-dy, dx) * (180.0 / M_PI); + if (dir < 0.0) dir += 360.0; + inst->direction = (float) dir; + inst->speed = (float) spd; + Instance_computeComponentsFromSpeed(inst); + return RValue_makeReal(0.0); +} + +static RValue builtinMoveSnap(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal hsnap = RValue_toReal(args[0]); + GMLReal vsnap = RValue_toReal(args[1]); + Instance* inst = ctx->currentInstance; + if (hsnap > 0.0) { + inst->x = (float) (GMLReal_floor((inst->x / hsnap) + 0.5) * hsnap); + SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); + } + if (vsnap > 0.0) { + inst->y = (float) (GMLReal_floor((inst->y / vsnap) + 0.5) * vsnap); + SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); + } + return RValue_makeReal(0.0); +} + +static RValue builtinLengthdir_x(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + GMLReal len = RValue_toReal(args[0]); + GMLReal dir = RValue_toReal(args[1]) * (M_PI / 180.0); + return RValue_makeReal(len * GMLReal_cos(dir)); +} + +static RValue builtinLengthdir_y(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + GMLReal len = RValue_toReal(args[0]); + GMLReal dir = RValue_toReal(args[1]) * (M_PI / 180.0); + return RValue_makeReal(-len * GMLReal_sin(dir)); +} + +// ===[ RANDOM FUNCTIONS ]=== + +static RValue builtinRandom(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + GMLReal n = RValue_toReal(args[0]); + return RValue_makeReal(((GMLReal) rand() / (GMLReal) RAND_MAX) * n); +} + +static RValue builtinRandomRange(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + GMLReal lo = RValue_toReal(args[0]); + GMLReal hi = RValue_toReal(args[1]); + return RValue_makeReal(lo + ((GMLReal) rand() / (GMLReal) RAND_MAX) * (hi - lo)); +} + +static RValue builtinIrandom(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + int32_t n = RValue_toInt32(args[0]); + if (0 >= n) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) (rand() % (n + 1))); +} + +static RValue builtinIrandomRange(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + int32_t lo = RValue_toInt32(args[0]); + int32_t hi = RValue_toInt32(args[1]); + if (lo > hi) { int32_t tmp = lo; lo = hi; hi = tmp; } + int32_t range = hi - lo + 1; + if (0 >= range) return RValue_makeReal((GMLReal) lo); + return RValue_makeReal((GMLReal) (lo + rand() % range)); +} + +static RValue builtinChoose(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + int32_t idx = rand() % argCount; + // Steal ownership: the caller's RValue_free of args[idx] becomes a no-op, and the returned value owns the ref instead. + RValue val = args[idx]; + if (val.type == RVALUE_STRING && val.string != nullptr && !val.ownsReference) { + return RValue_makeOwnedString(safeStrdup(val.string)); + } + args[idx].ownsReference = false; + return val; +} + +static RValue builtinRandomize(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + if (ctx->hasFixedSeed) return RValue_makeUndefined(); + srand((unsigned int) time(nullptr) + (ctx->runner->frameCount * 2654435761u)); // 2654435761u = Knuth's multiplier + return RValue_makeUndefined(); +} + +// ===[ ROOM FUNCTIONS ]=== + +static RValue builtinGameGetSpeed(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + int32_t type = RValue_toInt32(args[0]); + GMLReal fps = (GMLReal) ctx->runner->currentRoom->speed; + // gamespeed_fps = 0, gamespeed_microseconds = 1 + if (type == 0) return RValue_makeReal(fps); + return RValue_makeReal((GMLReal) 1000000.0 / fps); +} + +static RValue builtinRoomExists(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + int32_t roomId = RValue_toInt32(args[0]); + return RValue_makeBool(roomId >= 0 && (uint32_t) roomId < ctx->runner->dataWin->room.count); +} + +static RValue builtinRoomGetName(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Room* room = &ctx->dataWin->room.rooms[RValue_toInt32(args[0])]; + return RValue_makeOwnedString(safeStrdup(room->name)); +} + +static RValue builtinRoomGotoNext(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_goto_next called but no runner!"); + + int32_t nextPos = runner->currentRoomOrderPosition + 1; + if ((int32_t) runner->dataWin->gen8.roomOrderCount > nextPos) { + runner->pendingRoom = runner->dataWin->gen8.roomOrder[nextPos]; + } else { + fprintf(stderr, "VM: room_goto_next - already at last room!\n"); + } + return RValue_makeUndefined(); +} + +static RValue builtinRoomGotoPrevious(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_goto_previous called but no runner!"); + + int32_t previousPos = runner->currentRoomOrderPosition - 1; + if (previousPos >= 0) { + runner->pendingRoom = runner->dataWin->gen8.roomOrder[previousPos]; + } else { + fprintf(stderr, "VM: room_goto_previous - already at first room!\n"); + } + return RValue_makeUndefined(); +} + +static RValue builtinRoomGoto(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_goto called but no runner!"); + runner->pendingRoom = RValue_toInt32(args[0]); + return RValue_makeUndefined(); +} + +static RValue builtinRoomRestart(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_restart called but no runner!"); + runner->pendingRoom = runner->currentRoomIndex; + return RValue_makeUndefined(); +} + +static RValue builtinRoomNext(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_next called but no runner!"); + int32_t roomId = RValue_toInt32(args[0]); + DataWin* dw = runner->dataWin; + repeat(dw->gen8.roomOrderCount, i) { + if (dw->gen8.roomOrder[i] == roomId && dw->gen8.roomOrderCount > i + 1) { + return RValue_makeReal(dw->gen8.roomOrder[i + 1]); + } + } + return RValue_makeReal(-1); +} + +static RValue builtinRoomPrevious(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = requireNotNullMessage(ctx->runner, "VM: room_previous called but no runner!"); + int32_t roomId = RValue_toInt32(args[0]); + DataWin* dw = runner->dataWin; + repeat(dw->gen8.roomOrderCount, i) { + if (dw->gen8.roomOrder[i] == roomId && i > 0) { + return RValue_makeReal(dw->gen8.roomOrder[i - 1]); + } + } + return RValue_makeReal(-1); +} + +static RValue builtinRoomSetPersistent(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + + int32_t roomId = RValue_toInt32(args[0]); + bool persistent = RValue_toBool(args[1]); + // The HTML5 room_set_persistent does do this (it checks if the room is null) + if (0 > roomId || (uint32_t) roomId >= ctx->runner->dataWin->room.count) return RValue_makeUndefined(); + ctx->runner->dataWin->room.rooms[roomId].persistent = persistent; + + return RValue_makeUndefined(); +} + +// GMS2 camera compatibility - we treat view index as camera ID +static RValue builtinViewGetCamera(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1); + int32_t viewIndex = RValue_toInt32(args[0]); + if (viewIndex >= 0 && MAX_VIEWS > viewIndex) { + return RValue_makeReal(viewIndex); + } + return RValue_makeReal(-1); +} + +static RValue builtinCameraGetViewX(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_x called but no runner!"); + int32_t cameraId = RValue_toInt32(args[0]); + if (cameraId >= 0 && MAX_VIEWS > cameraId) { + return RValue_makeReal(runner->views[cameraId].viewX); + } + return RValue_makeReal(-1); +} + +static RValue builtinCameraGetViewY(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_y called but no runner!"); + int32_t cameraId = RValue_toInt32(args[0]); + if (cameraId >= 0 && MAX_VIEWS > cameraId) { + return RValue_makeReal(runner->views[cameraId].viewY); + } + return RValue_makeReal(-1); +} + +static RValue builtinCameraGetViewWidth(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_width called but no runner!"); + int32_t cameraId = RValue_toInt32(args[0]); + if (cameraId >= 0 && MAX_VIEWS > cameraId) { + return RValue_makeReal(runner->views[cameraId].viewWidth); + } + return RValue_makeReal(-1); +} + +static RValue builtinCameraGetViewHeight(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_height called but no runner!"); + int32_t cameraId = RValue_toInt32(args[0]); + if (cameraId >= 0 && MAX_VIEWS > cameraId) { + return RValue_makeReal(runner->views[cameraId].viewHeight); + } + return RValue_makeReal(-1); +} + +static RValue builtinCameraSetViewPos(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_set_view_pos called but no runner!"); + int32_t cameraId = RValue_toInt32(args[0]); + int32_t x = RValue_toInt32(args[1]); + int32_t y = RValue_toInt32(args[2]); + if (cameraId >= 0 && MAX_VIEWS > cameraId) { + runner->views[cameraId].viewX = x; + runner->views[cameraId].viewY = y; + } + return RValue_makeUndefined(); +} + +static RValue builtinCameraGetViewTarget(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_target called but no runner!"); + int32_t cameraId = RValue_toInt32(args[0]); + if (cameraId >= 0 && MAX_VIEWS > cameraId) { + return RValue_makeReal(runner->views[cameraId].objectId); + } + return RValue_makeReal(-1); +} + +static RValue builtinCameraSetViewTarget(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_set_view_target called but no runner!"); + int32_t cameraId = RValue_toInt32(args[0]); + int32_t objectId = RValue_toInt32(args[1]); + if (cameraId >= 0 && MAX_VIEWS > cameraId) { + runner->views[cameraId].objectId = objectId; + } + return RValue_makeUndefined(); +} + +static RValue cameraGetViewBorder(VMContext* ctx, RValue* args, int32_t argCount, bool wantY) { + if (1 > argCount) return RValue_makeReal(-1); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_get_view_border called but no runner!"); + int32_t cameraId = RValue_toInt32(args[0]); + if (cameraId >= 0 && MAX_VIEWS > cameraId) { + RuntimeView v = runner->views[cameraId]; + return RValue_makeReal((wantY ? v.borderY : v.borderX)); + } + return RValue_makeReal(-1); +} + +static RValue builtinCameraGetViewBorderX(VMContext* ctx, RValue* args, int32_t argCount) { + return cameraGetViewBorder(ctx, args, argCount, false); +} + +static RValue builtinCameraGetViewBorderY(VMContext* ctx, RValue* args, int32_t argCount) { + return cameraGetViewBorder(ctx, args, argCount, true); +} + +static RValue builtinCameraSetViewBorder(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeUndefined(); + Runner* runner = requireNotNullMessage(ctx->runner, "VM: camera_set_view_border called but no runner!"); + int32_t cameraId = RValue_toInt32(args[0]); + int32_t bx = RValue_toInt32(args[1]); + int32_t by = RValue_toInt32(args[2]); + if (cameraId >= 0 && MAX_VIEWS > cameraId) { + runner->views[cameraId].borderX = (uint32_t) bx; + runner->views[cameraId].borderY = (uint32_t) by; + } + return RValue_makeUndefined(); +} + +// ===[ VARIABLE FUNCTIONS ]=== + +#ifdef ENABLE_VM_TRACING +static const char* variableTraceObjectName(VMContext* ctx, Instance* inst) { + if (0 > inst->objectIndex) return ""; + return ctx->dataWin->objt.objects[inst->objectIndex].name; +} +#endif + +static RValue builtinVariableGlobalExists(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount || args[0].type != RVALUE_STRING) return RValue_makeReal(0.0); + const char* name = args[0].string; + ptrdiff_t idx = shgeti(ctx->globalVarNameMap, (char*) name); + if (0 > idx) return RValue_makeReal(0.0); + int32_t varID = ctx->globalVarNameMap[idx].value; + if (ctx->globalVarCount > (uint32_t) varID && ctx->globalVars[varID].type != RVALUE_UNDEFINED) { + return RValue_makeReal(1.0); + } + return RValue_makeReal(0.0); +} + +static RValue builtinVariableGlobalGet(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount || args[0].type != RVALUE_STRING) return RValue_makeUndefined(); + const char* name = args[0].string; + ptrdiff_t idx = shgeti(ctx->globalVarNameMap, (char*) name); + if (0 > idx) return RValue_makeUndefined(); + int32_t varID = ctx->globalVarNameMap[idx].value; + if (ctx->globalVarCount > (uint32_t) varID) { + RValue val = ctx->globalVars[varID]; +#ifdef ENABLE_VM_TRACING + VM_checkIfVariableShouldBeTracedAndLog(ctx, "global", nullptr, name, val, false, -1, -1, " (variable_global_get)"); +#endif + // Duplicate owned strings + if (val.type == RVALUE_STRING && val.ownsReference && val.string != nullptr) { + return RValue_makeOwnedString(safeStrdup(val.string)); + } + // Return a weak view: the global slot retains ownership. The caller's Pop will incRef into the destination slot. + val.ownsReference = false; + return val; + } + return RValue_makeUndefined(); +} + +static RValue builtinVariableGlobalSet(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount || args[0].type != RVALUE_STRING) return RValue_makeUndefined(); + const char* name = args[0].string; + ptrdiff_t idx = shgeti(ctx->globalVarNameMap, (char*) name); + if (0 > idx) return RValue_makeUndefined(); + int32_t varID = ctx->globalVarNameMap[idx].value; + if (ctx->globalVarCount > (uint32_t) varID) { +#ifdef ENABLE_VM_TRACING + VM_checkIfVariableShouldBeTracedAndLog(ctx, "global", nullptr, name, args[1], true, -1, -1, " (variable_global_set)"); +#endif + RValue_free(&ctx->globalVars[varID]); + ctx->globalVars[varID] = RValue_makeIndependent(args[1]); + } + return RValue_makeUndefined(); +} + +// ===[ VARIABLE_INSTANCE ]=== + +static void variableInstanceSetOn(VMContext* ctx, Instance* target, const char* name, RValue val, MAYBE_UNUSED const char* originBuiltin) { +#ifdef ENABLE_VM_TRACING + char additional[48]; + snprintf(additional, sizeof(additional), " (%s)", originBuiltin); + VM_checkIfVariableShouldBeTracedAndLog(ctx, variableTraceObjectName(ctx, target), "self", name, val, true, -1, target->instanceId, additional); +#endif + int16_t builtinId = VMBuiltins_resolveBuiltinVarId(name); + if (builtinId != BUILTIN_VAR_UNKNOWN) { + Instance* saved = (Instance*) ctx->currentInstance; + ctx->currentInstance = target; + VMBuiltins_setVariable(ctx, builtinId, name, val, -1); + ctx->currentInstance = saved; + return; + } + // Lookup varID by name from VARI (self scope) + ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) name); + if (0 > slot) { + fprintf(stderr, "variable_instance_set: variable '%s' not found in VARI table\n", name); + return; + } + Instance_setSelfVar(target, ctx->selfVarNameMap[slot].value, val); +} + +static RValue variableInstanceGetOn(VMContext* ctx, Instance* target, const char* name, MAYBE_UNUSED const char* originBuiltin) { + int16_t builtinId = VMBuiltins_resolveBuiltinVarId(name); + if (builtinId != BUILTIN_VAR_UNKNOWN) { + Instance* saved = (Instance*) ctx->currentInstance; + ctx->currentInstance = target; + RValue val = VMBuiltins_getVariable(ctx, builtinId, name, -1); + ctx->currentInstance = saved; +#ifdef ENABLE_VM_TRACING + char additional[48]; + snprintf(additional, sizeof(additional), " (%s, builtin)", originBuiltin); + VM_checkIfVariableShouldBeTracedAndLog(ctx, variableTraceObjectName(ctx, target), "self", name, val, false, -1, target->instanceId, additional); +#endif + // Duplicate string so caller-owned args cleanup does not affect it + if (val.type == RVALUE_STRING && val.string != nullptr && !val.ownsReference) { + return RValue_makeOwnedString(safeStrdup(val.string)); + } + return val; + } + ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) name); + if (0 > slot) return RValue_makeUndefined(); + RValue val = Instance_getSelfVar(target, ctx->selfVarNameMap[slot].value); +#ifdef ENABLE_VM_TRACING + char additional[48]; + snprintf(additional, sizeof(additional), " (%s)", originBuiltin); + VM_checkIfVariableShouldBeTracedAndLog(ctx, variableTraceObjectName(ctx, target), "self", name, val, false, -1, target->instanceId, additional); +#endif + if (val.type == RVALUE_STRING && val.string != nullptr) { + return RValue_makeOwnedString(safeStrdup(val.string)); + } + return val; +} + +static inline bool variableScopedMatches(Instance* inst, bool structOnly) { + return inst->active && (!structOnly || inst->objectIndex == -1); +} + +static bool variableInstanceExistsOn(VMContext* ctx, Instance* target, const char* name) { + if (VMBuiltins_resolveBuiltinVarId(name) != BUILTIN_VAR_UNKNOWN) return true; + ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) name); + if (0 > slot) return false; + return IntRValueHashMap_contains(&target->selfVars, ctx->selfVarNameMap[slot].value); +} + +static RValue variableScopedGet(VMContext* ctx, int32_t id, const char* name, bool structOnly, const char* originBuiltin) { + Runner* runner = (Runner*) ctx->runner; + + if (id >= 100000) { + Instance* inst = hmget(runner->instancesById, id); + if (inst != nullptr && variableScopedMatches(inst, structOnly)) return variableInstanceGetOn(ctx, inst, name, originBuiltin); + return RValue_makeUndefined(); + } + + // Object index: return value from first matching active instance. + int32_t snapBase = Runner_pushInstancesOfObject(runner, id); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + RValue result = RValue_makeUndefined(); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (variableScopedMatches(inst, structOnly)) { + result = variableInstanceGetOn(ctx, inst, name, originBuiltin); + break; + } + } + Runner_popInstanceSnapshot(runner, snapBase); + return result; +} + +static void variableScopedSet(VMContext* ctx, int32_t id, const char* name, RValue val, bool structOnly, const char* originBuiltin) { + Runner* runner = (Runner*) ctx->runner; + + if (id >= 100000) { + Instance* inst = hmget(runner->instancesById, id); + if (inst != nullptr && variableScopedMatches(inst, structOnly)) variableInstanceSetOn(ctx, inst, name, val, originBuiltin); + return; + } + + // Object index: set on all matching active instances (including descendants). The setter can run user code, so iterate a snapshot. + int32_t snapBase = Runner_pushInstancesOfObject(runner, id); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (variableScopedMatches(inst, structOnly)) variableInstanceSetOn(ctx, inst, name, val, originBuiltin); + } + Runner_popInstanceSnapshot(runner, snapBase); +} + +static bool variableScopedExists(VMContext* ctx, int32_t id, const char* name, bool structOnly) { + Runner* runner = (Runner*) ctx->runner; + + if (id >= 100000) { + Instance* inst = hmget(runner->instancesById, id); + if (inst != nullptr && variableScopedMatches(inst, structOnly)) return variableInstanceExistsOn(ctx, inst, name); + return false; + } + + int32_t snapBase = Runner_pushInstancesOfObject(runner, id); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + bool result = false; + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (variableScopedMatches(inst, structOnly)) { + result = variableInstanceExistsOn(ctx, inst, name); + break; + } + } + Runner_popInstanceSnapshot(runner, snapBase); + return result; +} + +static RValue builtinVariableInstanceGet(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount || args[1].type != RVALUE_STRING) return RValue_makeUndefined(); + return variableScopedGet(ctx, RValue_toInt32(args[0]), args[1].string, false, "variable_instance_get"); +} + +static RValue builtinVariableInstanceSet(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount || args[1].type != RVALUE_STRING) return RValue_makeUndefined(); + variableScopedSet(ctx, RValue_toInt32(args[0]), args[1].string, args[2], false, "variable_instance_set"); + return RValue_makeUndefined(); +} + +static RValue builtinVariableInstanceExists(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount || args[1].type != RVALUE_STRING) return RValue_makeBool(false); + return RValue_makeBool(variableScopedExists(ctx, RValue_toInt32(args[0]), args[1].string, false)); +} + +static RValue builtinVariableStructGet(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount || args[1].type != RVALUE_STRING) return RValue_makeUndefined(); + return variableScopedGet(ctx, RValue_toInt32(args[0]), args[1].string, true, "variable_struct_get"); +} + +static RValue builtinVariableStructSet(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount || args[1].type != RVALUE_STRING) return RValue_makeUndefined(); + variableScopedSet(ctx, RValue_toInt32(args[0]), args[1].string, args[2], true, "variable_struct_set"); + return RValue_makeUndefined(); +} + +static RValue builtinVariableStructExists(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount || args[1].type != RVALUE_STRING) return RValue_makeBool(false); + return RValue_makeBool(variableScopedExists(ctx, RValue_toInt32(args[0]), args[1].string, true)); +} + +// ===[ METHOD ]=== + +#if IS_BC17_OR_HIGHER_ENABLED +static RValue builtinMethod(VMContext* ctx, MAYBE_UNUSED RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + + int32_t boundInstance = RValue_toInt32(args[0]); + int32_t rawArg = RValue_toInt32(args[1]); + + // In GMS2 BC17+, function references are pushed via `Push.i ` where funcIdx is an index into the FUNC chunk (patched in by patchReferenceOperands). Resolve funcIdx -> codeIndex via function name lookup (same flow as Call.i). + int32_t codeIndex = rawArg; + if (rawArg >= 0 && (uint32_t) rawArg < ctx->dataWin->func.functionCount) { + const char* funcName = ctx->dataWin->func.functions[rawArg].name; + if (funcName != nullptr) { + ptrdiff_t idx = shgeti(ctx->codeIndexByName, (char*) funcName); + if (idx >= 0) { + codeIndex = ctx->codeIndexByName[idx].value; + } + } + } + + // If binding to current self (-1), capture the actual instance ID + if (boundInstance == -1 && ctx->currentInstance != nullptr) { + boundInstance = ((Instance*) ctx->currentInstance)->instanceId; + } + + return RValue_makeMethod(codeIndex, boundInstance); +} +#endif + +// ===[ SCRIPT EXECUTE ]=== + +static RValue builtinScriptExecute(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + + int32_t codeId; + +#if IS_BC17_OR_HIGHER_ENABLED + if (args[0].type == RVALUE_METHOD) { + // If it is a method value, we'll need to extract code index directly + codeId = args[0].method->codeIndex; + } else +#endif + { + // Numeric script/function index + int32_t rawArg = RValue_toInt32(args[0]); + codeId = -1; + +#if IS_BC17_OR_HIGHER_ENABLED + // In GMS 2 BC17+, "scriptName" in source code is compiled as a FUNC-table index (same as builtinMethod). Resolve funcIdx -> codeIndex via codeIndexByName. + if (IS_BC17_OR_HIGHER(ctx) && rawArg >= 0 && ctx->dataWin->func.functionCount > (uint32_t) rawArg) { + const char* funcName = ctx->dataWin->func.functions[rawArg].name; + if (funcName != nullptr) { + ptrdiff_t idx = shgeti(ctx->codeIndexByName, (char*) funcName); + if (idx >= 0) { + codeId = ctx->codeIndexByName[idx].value; + } else { + // Not a user script - might be a builtin function reference + ptrdiff_t bidx = shgeti(ctx->builtinMap, (char*) funcName); + if (bidx >= 0) { + BuiltinFunc bf = ctx->builtinMap[bidx].value; + RValue* scriptArgs = (argCount > 1) ? &args[1] : nullptr; + return bf(ctx, scriptArgs, argCount - 1); + } + } + } + } +#endif + + // Fallback: treat as SCPT index (BC16 and earlier, or when FUNC lookup failed) + if (0 > codeId) { + if (0 > rawArg || (uint32_t) rawArg >= ctx->dataWin->scpt.count) { + fprintf(stderr, "VM: script_execute - invalid script index %d\n", rawArg); + return RValue_makeUndefined(); + } + codeId = ctx->dataWin->scpt.scripts[rawArg].codeId; + } + } + + if (0 > codeId || ctx->dataWin->code.count <= (uint32_t) codeId) { + fprintf(stderr, "VM: script_execute - invalid codeId %d\n", codeId); + return RValue_makeUndefined(); + } + + // Pass remaining args (skip the script index) + RValue* scriptArgs = (argCount > 1) ? &args[1] : nullptr; + int32_t scriptArgCount = argCount - 1; + + // If the method has a bound instance, temporarily swap currentInstance + Instance* savedInstance = (Instance*) ctx->currentInstance; +#if IS_BC17_OR_HIGHER_ENABLED + if (args[0].type == RVALUE_METHOD && args[0].method->boundInstanceId >= 0) { + Runner* runner = (Runner*) ctx->runner; + Instance* bound = hmget(runner->instancesById, args[0].method->boundInstanceId); + if (bound != nullptr) ctx->currentInstance = bound; + } +#endif + + RValue result = VM_callCodeIndex(ctx, codeId, scriptArgs, scriptArgCount); + + ctx->currentInstance = savedInstance; + return result; +} + +static RValue builtinN3DSRenderBottomScreen(VMContext* ctx, RValue* args, int32_t argCount) { + if (argCount < 1) return RValue_makeUndefined(); + + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->renderer == NULL) return RValue_makeUndefined(); + +#ifndef __3DS__ + return builtinScriptExecute(ctx, args, argCount); +#else + if (runner->osType != OS_3DS) { + return builtinScriptExecute(ctx, args, argCount); + } + + if (g_n3dsDisableBottomScreenOverrides) { return RValue_makeUndefined(); - - if (el->spriteElement != nullptr) { - free(el->spriteElement); - el->spriteElement = nullptr; - } - - // Remove the element from the owning layer's element array to keep lookup + iteration tidy. - size_t count = arrlenu(owningLayer->elements); - repeat(count, i) { - if (&owningLayer->elements[i] == el) { - arrdel(owningLayer->elements, i); - break; - } } - +#ifdef N3DS_DISABLE_BOTTOM_SCREEN return RValue_makeUndefined(); -} - -#if IS_BC17_OR_HIGHER_ENABLED -static RValue builtinLayerGetAll(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - RValue arr = VM_createArray(ctx); - int32_t i = 0; - size_t count = arrlenu(runner->runtimeLayers); - repeat(count, layerIndex) { - VM_arraySet(ctx, &arr, i++, RValue_makeReal((GMLReal) runner->runtimeLayers[layerIndex].id)); - } - return arr; -} - -static RValue builtinLayerGetIdAtDepth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t targetDepth = RValue_toInt32(args[0]); - RValue arr = VM_createArray(ctx); - int32_t i = 0; - size_t count = arrlenu(runner->runtimeLayers); - repeat(count, layerIndex) { - if (runner->runtimeLayers[layerIndex].depth == targetDepth) { - VM_arraySet(ctx, &arr, i++, RValue_makeReal((GMLReal) runner->runtimeLayers[layerIndex].id)); - } - } - // When no layer matches, return [-1] instead of an empty array. - if (i == 0) - VM_arraySet(ctx, &arr, 0, RValue_makeReal(-1.0)); - return arr; -} #endif -static RValue builtinLayerVspeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - int32_t id = resolveLayerIdArg(runner, args[0]); - float vs = (float) RValue_toReal(args[1]); - RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); - if (runtimeLayer != nullptr) runtimeLayer->vSpeed = vs; - return RValue_makeUndefined(); + if (!battleDraw_is3DSBattleActive(ctx, runner)) { + return RValue_makeUndefined(); + } + + int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : (int32_t) runner->dataWin->gen8.defaultWindowWidth; + int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : (int32_t) runner->dataWin->gen8.defaultWindowHeight; + if (guiW <= 0) guiW = 320; + if (guiH <= 0) guiH = 240; + + N3DSRenderer_beginBottomScreenGUI(runner->renderer, guiW, guiH); + RValue result = builtinScriptExecute(ctx, args, argCount); + N3DSRenderer_endBottomScreenGUI(runner->renderer); + return result; +#endif +} + +static RValue builtinN3DSRenderTopScreen2x(VMContext* ctx, RValue* args, int32_t argCount) { + if (argCount < 1) return RValue_makeUndefined(); + + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->renderer == NULL) return RValue_makeUndefined(); + +#ifndef __3DS__ + return builtinScriptExecute(ctx, args, argCount); +#else + if (runner->osType != OS_3DS) { + return builtinScriptExecute(ctx, args, argCount); + } + + int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : (int32_t) runner->dataWin->gen8.defaultWindowWidth; + int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : (int32_t) runner->dataWin->gen8.defaultWindowHeight; + if (guiW <= 0) guiW = 320; + if (guiH <= 0) guiH = 240; + + N3DSRenderer_beginTopScreenGUI2x(runner->renderer, guiW, guiH); + RValue result = builtinScriptExecute(ctx, args, argCount); + N3DSRenderer_endTopScreenGUI2x(runner->renderer); + return result; +#endif +} + + +// Native battle controller scripts for bottom screen on 3DS target + +// Helper: look up a GML script code index by name. Returns -1 if not found. +static int32_t battleDraw_resolveScript(VMContext* ctx, const char* name) { + ptrdiff_t idx = shgeti(ctx->codeIndexByName, (char*) name); + if (idx < 0) return -1; + return ctx->codeIndexByName[idx].value; +} + +// Helper: call a no-arg GML script by code index. No-op if index is -1. +static void battleDraw_callScript(VMContext* ctx, int32_t codeIndex) { + if (codeIndex >= 0) VM_callCodeIndex(ctx, codeIndex, NULL, 0); +} + +// Helper: call a 1-arg GML script by code index. +static void battleDraw_callScript1(VMContext* ctx, int32_t codeIndex, RValue arg0) { + if (codeIndex >= 0) VM_callCodeIndex(ctx, codeIndex, &arg0, 1); +} + +// Helper: call a 4-arg GML script by code index. +static void battleDraw_callScript4(VMContext* ctx, int32_t codeIndex, RValue arg0, RValue arg1, RValue arg2, RValue arg3) { + if (codeIndex < 0) return; + RValue args[4] = { arg0, arg1, arg2, arg3 }; + VM_callCodeIndex(ctx, codeIndex, args, 4); +} + +// Helper: read a global variable RValue by name. Returns undefined if missing. +static RValue battleDraw_getGlobal(VMContext* ctx, const char* name) { + ptrdiff_t idx = shgeti(ctx->globalVarNameMap, (char*) name); + if (idx < 0) return RValue_makeUndefined(); + int32_t varID = ctx->globalVarNameMap[idx].value; + if ((uint32_t) varID >= ctx->globalVarCount) return RValue_makeUndefined(); + RValue val = ctx->globalVars[varID]; + val.ownsReference = false; + return val; +} + +// Helper: read global array element global.[index]. Returns real 0 if missing. +static RValue battleDraw_getGlobalArrayElem(VMContext* ctx, const char* name, int32_t index) { + RValue arr = battleDraw_getGlobal(ctx, name); + if (arr.type != RVALUE_ARRAY || arr.array == NULL) return RValue_makeReal(0.0); + if (index < 0 || index >= GMLArray_length1D(arr.array)) return RValue_makeReal(0.0); + RValue* slot = GMLArray_slot(arr.array, index); + if (slot == NULL) return RValue_makeReal(0.0); + RValue val = *slot; + val.ownsReference = false; + return val; +} + +// Helper: check whether any active instance of the given object name exists. +static bool battleDraw_instanceExists(VMContext* ctx, Runner* runner, const char* objectName) { + int32_t objIdx = shget(runner->assetsByName, (char*) objectName); + if (objIdx < 0 || (uint32_t) objIdx >= ctx->dataWin->objt.count) return false; + int32_t snapBase = Runner_pushInstancesOfObject(runner, objIdx); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + bool found = false; + for (int32_t i = snapBase; snapEnd > i; i++) { + if (runner->instanceSnapshots[i]->active) { found = true; break; } + } + Runner_popInstanceSnapshot(runner, snapBase); + return found; +} + +// Helper: get the first active instance of the named object, or NULL. +static Instance* battleDraw_getFirstInstance(VMContext* ctx, Runner* runner, const char* objectName) { + int32_t objIdx = shget(runner->assetsByName, (char*) objectName); + if (objIdx < 0 || (uint32_t) objIdx >= ctx->dataWin->objt.count) return NULL; + int32_t snapBase = Runner_pushInstancesOfObject(runner, objIdx); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + Instance* result = NULL; + for (int32_t i = snapBase; snapEnd > i; i++) { + if (runner->instanceSnapshots[i]->active) { result = runner->instanceSnapshots[i]; break; } + } + Runner_popInstanceSnapshot(runner, snapBase); + return result; +} + +// Helper: read a named self variable from an instance as a real. Returns 0 on failure. +static GMLReal battleDraw_getInstReal(VMContext* ctx, Instance* inst, const char* varName) { + if (inst == NULL) return 0.0; + int16_t builtinId = VMBuiltins_resolveBuiltinVarId(varName); + if (builtinId != BUILTIN_VAR_UNKNOWN) { + RValue v = VMBuiltins_getVariable(ctx, builtinId, varName, 0); + return RValue_toReal(v); + } + ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) varName); + if (slot < 0) return 0.0; + int32_t varID = ctx->selfVarNameMap[slot].value; + RValue v = Instance_getSelfVar(inst, varID); + return RValue_toReal(v); } -// ===[ Array Functions ]=== - -// @@NewGMLArray@@ - GMS2 internal function to create a new array literal (e.g. `[1, 2, 3]`). -// Allocates a fresh GMLArray populated with the argument values. -static RValue builtinNewGMLArray(VMContext* ctx, RValue* args, int32_t argCount) { - RValue arr = VM_createArray(ctx); - repeat(argCount, i) { - VM_arraySet(ctx, &arr, i, args[i]); - } - return arr; -} +static GMLReal nativeOverride_getInstReal(VMContext* ctx, Instance* inst, const char* varName) { + if (ctx == NULL || inst == NULL || varName == NULL) return 0.0; -// array_create - GMS2 internal function to create a new array. -// Allocates a fresh GMLArray populated with the argument values. -static RValue builtinArrayCreate(VMContext* ctx, RValue* args, int32_t argCount) { - RValue arr = VM_createArray(ctx); - RValue fill = (argCount > 1) ? args[1] : RValue_makeUndefined(); - repeat(RValue_toReal(args[0]), i) { - VM_arraySet(ctx, &arr, i, fill); + int16_t builtinId = VMBuiltins_resolveBuiltinVarId(varName); + if (builtinId != BUILTIN_VAR_UNKNOWN) { + Instance* savedSelf = ctx->currentInstance; + ctx->currentInstance = inst; + RValue v = VMBuiltins_getVariable(ctx, builtinId, varName, 0); + ctx->currentInstance = savedSelf; + return RValue_toReal(v); } - return arr; -} - -// @@This@@ - GMS2 internal function returning the current instance's ID. -// Emitted by the GMS2 compiler for expressions like `self` when used as a value. -static RValue builtinThis(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr) return RValue_makeInt32(INSTANCE_SELF); - return RValue_makeInt32((int32_t) inst->instanceId); -} - -// @@Other@@ - GMS2 internal function returning the "other" instance's ID. -// Falls back to the current instance when there is no other (matches GML semantics outside with/collision). -static RValue builtinOther(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Instance* other = (Instance*) ctx->otherInstance; - if (other != nullptr) return RValue_makeInt32((int32_t) other->instanceId); - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr) return RValue_makeInt32(INSTANCE_SELF); - return RValue_makeInt32((int32_t) inst->instanceId); -} -#if IS_BC17_OR_HIGHER_ENABLED -// @@NullObject@@ - GMS2 internal sentinel pushed before "method()" when the GML source is a struct literal or anonymous constructor: the bound self is "nothing yet", and @@NewGMLObject@@ rebinds to the fresh struct. -// We encode it as INSTANCE_NOONE so "method()" stores it as is (its -1 -> current remap does not fire). -static RValue builtinNullObject(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - return RValue_makeInt32(INSTANCE_NOONE); + ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) varName); + if (slot < 0) return 0.0; + return RValue_toReal(Instance_getSelfVar(inst, ctx->selfVarNameMap[slot].value)); } -// @@NewGMLObject@@(methodRef, ...args) - GMS2 internal function that allocates a fresh struct instance, runs the constructor method against it, and returns the new instance ID. -// We reuse Instance (with objectIndex = -1) the same way globalScopeInstance is used for GLOB scripts, instead of introducing a separate struct type. -static RValue builtinNewGMLObject(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) { - fprintf(stderr, "VM: @@NewGMLObject@@ called with no arguments\n"); - return RValue_makeUndefined(); - } - - Runner* runner = (Runner*) ctx->runner; - int32_t codeIndex; - if (args[0].type == RVALUE_METHOD && args[0].method != nullptr) { - codeIndex = args[0].method->codeIndex; - } else { - // Raw funcIdx pushed via "Push.i ; Conv.i.v" (no method() wrapper used when no static binding is needed). - // Resolve via FUNC chunk name -> codeIndexByName, matching builtinMethod's lookup. - int32_t rawArg = RValue_toInt32(args[0]); - codeIndex = rawArg; - if (rawArg >= 0 && (uint32_t) rawArg < ctx->dataWin->func.functionCount) { - const char* funcName = ctx->dataWin->func.functions[rawArg].name; - if (funcName != nullptr) { - ptrdiff_t idx = shgeti(ctx->codeIndexByName, (char*) funcName); - if (idx >= 0) codeIndex = ctx->codeIndexByName[idx].value; - } - } - } - if (0 > codeIndex || (uint32_t) codeIndex > ctx->dataWin->code.count) { - fprintf(stderr, "VM: @@NewGMLObject@@ method has invalid codeIndex %d\n", codeIndex); - return RValue_makeUndefined(); - } - - Instance* structInst = Instance_create(runner->nextInstanceId++, -1, 0, 0); - hmput(runner->instancesById, structInst->instanceId, structInst); - structInst->structRegistryIndex = (int32_t) arrlen(runner->structInstances); - arrput(runner->structInstances, structInst); - // Two refs at birth: one for the registry's implicit ref (structInstances), one for the returned RValue. - structInst->refCount = 2; +static GMLArray* nativeOverride_ensureSelfArray(VMContext* ctx, Instance* inst, const char* varName) { + if (ctx == NULL || inst == NULL || varName == NULL) return NULL; - Instance* savedSelf = (Instance*) ctx->currentInstance; - ctx->currentInstance = structInst; + ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) varName); + if (slot < 0) return NULL; - RValue* ctorArgs = (argCount > 1) ? &args[1] : nullptr; - int32_t ctorArgCount = argCount - 1; - RValue result = VM_callCodeIndex(ctx, codeIndex, ctorArgs, ctorArgCount); - RValue_free(&result); + int32_t varID = ctx->selfVarNameMap[slot].value; + RValue* valueSlot = IntRValueHashMap_getOrInsertUndefined(&inst->selfVars, varID); + if (valueSlot == NULL) return NULL; + if (valueSlot->type == RVALUE_ARRAY && valueSlot->array != NULL) return valueSlot->array; - ctx->currentInstance = savedSelf; - return RValue_makeStruct(structInst); + RValue_free(valueSlot); + *valueSlot = VM_createArray(ctx); + return valueSlot->array; } -#endif -// ===[ PATH FUNCTIONS ]=== +static GMLArray* nativeOverride_ensureGlobalArray(VMContext* ctx, const char* varName) { + if (ctx == NULL || varName == NULL) return NULL; -// path_add() - create a new empty path, return its index -static RValue builtinPathAdd(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Runner* runner = (Runner*) ctx->runner; - PathChunk* pc = &runner->dataWin->path; - uint32_t newIdx = pc->count; - GamePath* paths = (GamePath*) realloc(pc->paths, (newIdx + 1) * sizeof(GamePath)); - if (paths == nullptr) return RValue_makeInt32(-1); - pc->paths = paths; - GamePath* p = &paths[newIdx]; - memset(p, 0, sizeof(GamePath)); - p->name = ""; - p->isSmooth = false; - p->isClosed = false; - p->precision = 4; - p->pointCount = 0; - p->points = nullptr; - p->internalPointCount = 0; - p->internalPoints = nullptr; - p->length = 0.0; - pc->count = newIdx + 1; - return RValue_makeInt32((int32_t) newIdx); -} + ptrdiff_t slot = shgeti(ctx->globalVarNameMap, (char*) varName); + if (slot < 0) return NULL; -// path_clear_points(path) -static RValue builtinPathClearPoints(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t idx = RValue_toInt32(args[0]); - if (0 > idx || (uint32_t) idx >= runner->dataWin->path.count) return RValue_makeUndefined(); - GamePath* p = &runner->dataWin->path.paths[idx]; - free(p->points); - p->points = nullptr; - p->pointCount = 0; - free(p->internalPoints); - p->internalPoints = nullptr; - p->internalPointCount = 0; - p->length = 0.0; - return RValue_makeUndefined(); -} + int32_t varID = ctx->globalVarNameMap[slot].value; + if (varID < 0 || (uint32_t) varID >= ctx->globalVarCount) return NULL; -// path_add_point(path, x, y, speed) -static RValue builtinPathAddPoint(VMContext* ctx, RValue* args, int32_t argCount) { - if (4 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t idx = RValue_toInt32(args[0]); - if (0 > idx || (uint32_t) idx >= runner->dataWin->path.count) return RValue_makeUndefined(); - GamePath* p = &runner->dataWin->path.paths[idx]; - PathPoint* pts = (PathPoint*) realloc(p->points, (p->pointCount + 1) * sizeof(PathPoint)); - if (pts == nullptr) return RValue_makeUndefined(); - p->points = pts; - pts[p->pointCount].x = (float) RValue_toReal(args[1]); - pts[p->pointCount].y = (float) RValue_toReal(args[2]); - pts[p->pointCount].speed = (float) RValue_toReal(args[3]); - p->pointCount++; - GamePath_computeInternal(p); - return RValue_makeUndefined(); -} + RValue* valueSlot = &ctx->globalVars[varID]; + if (valueSlot->type == RVALUE_ARRAY && valueSlot->array != NULL) return valueSlot->array; -// path_exists(path) -static RValue builtinPathExists(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeBool(false); - Runner* runner = (Runner*) ctx->runner; - int32_t idx = RValue_toInt32(args[0]); - bool exists = (idx >= 0) && ((uint32_t) idx < runner->dataWin->path.count); - return RValue_makeBool(exists); + RValue_free(valueSlot); + *valueSlot = VM_createArray(ctx); + return valueSlot->array; } -// path_delete(path) - we don't reclaim the slot (would require remapping indices); zero it out -static RValue builtinPathDelete(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t idx = RValue_toInt32(args[0]); - if (0 > idx || (uint32_t) idx >= runner->dataWin->path.count) return RValue_makeUndefined(); - GamePath* p = &runner->dataWin->path.paths[idx]; - free(p->points); p->points = nullptr; p->pointCount = 0; - free(p->internalPoints); p->internalPoints = nullptr; p->internalPointCount = 0; - p->length = 0.0; - return RValue_makeUndefined(); +static GMLReal nativeOverride_getArrayElemReal(const GMLArray* arr, int32_t index, GMLReal defaultValue) { + if (arr == NULL || index < 0) return defaultValue; + RValue* slot = GMLArray_slot((GMLArray*) arr, index); + if (slot == NULL) return defaultValue; + return RValue_toReal(*slot); } -// ===[ MP_GRID FUNCTIONS ]=== - -static MpGrid* mpGridGet(Runner* runner, int32_t id) { - if (0 > id || (int32_t) arrlen(runner->mpGridPool) <= id) return nullptr; - MpGrid* g = &runner->mpGridPool[id]; - if (!g->inUse) return nullptr; - return g; +static void nativeOverride_setArrayElemReal(GMLArray* arr, int32_t index, GMLReal value) { + if (arr == NULL || index < 0) return; + GMLArray_growTo(arr, index + 1); + RValue* slot = GMLArray_slot(arr, index); + if (slot == NULL) return; + RValue_free(slot); + *slot = RValue_makeReal(value); } -// mp_grid_create(left, top, hcells, vcells, cellwidth, cellheight) -static RValue builtinMpGridCreate(VMContext* ctx, RValue* args, int32_t argCount) { - if (6 > argCount) return RValue_makeInt32(-1); - Runner* runner = (Runner*) ctx->runner; - MpGrid g; - g.inUse = true; - g.left = RValue_toReal(args[0]); - g.top = RValue_toReal(args[1]); - g.hcells = RValue_toInt32(args[2]); - g.vcells = RValue_toInt32(args[3]); - g.cellWidth = RValue_toReal(args[4]); - g.cellHeight = RValue_toReal(args[5]); - if (g.hcells <= 0 || g.vcells <= 0) return RValue_makeInt32(-1); - g.cells = (uint8_t*) calloc((size_t) g.hcells * (size_t) g.vcells, 1); - int32_t id = (int32_t) arrlen(runner->mpGridPool); - arrput(runner->mpGridPool, g); - return RValue_makeInt32(id); +static void nativeOverride_drawSnowDot3DS(Renderer* rend, GMLReal dotX, GMLReal dotY) { + // Approximate the tiny filled circle with three thin bands. + // This stays much cheaper than the generic circle path on 3DS, but looks rounder than a square. + rend->vtable->drawRectangle(rend, (float) (dotX - 2.0), (float) (dotY - 2.5), (float) (dotX + 2.0), (float) (dotY - 1.5), rend->drawColor, rend->drawAlpha, false); + rend->vtable->drawRectangle(rend, (float) (dotX - 2.5), (float) (dotY - 1.5), (float) (dotX + 2.5), (float) (dotY + 1.5), rend->drawColor, rend->drawAlpha, false); + rend->vtable->drawRectangle(rend, (float) (dotX - 2.0), (float) (dotY + 1.5), (float) (dotX + 2.0), (float) (dotY + 2.5), rend->drawColor, rend->drawAlpha, false); } -static RValue builtinMpGridDestroy(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - int32_t id = RValue_toInt32(args[0]); - MpGrid* g = mpGridGet(runner, id); - if (g == nullptr) return RValue_makeUndefined(); - free(g->cells); - g->cells = nullptr; - g->inUse = false; - return RValue_makeUndefined(); +static int32_t nativeOverride_resolveSelfVarId(VMContext* ctx, const char* varName) { + if (ctx == NULL || varName == NULL) return -1; + ptrdiff_t slot = shgeti(ctx->selfVarNameMap, (char*) varName); + if (slot < 0) return -1; + return ctx->selfVarNameMap[slot].value; } -static RValue builtinMpGridClearAll(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); - if (g == nullptr) return RValue_makeUndefined(); - memset(g->cells, 0, (size_t) g->hcells * (size_t) g->vcells); - return RValue_makeUndefined(); +static GMLReal nativeOverride_getSelfVarReal(Instance* inst, int32_t varId, GMLReal defaultValue) { + if (inst == NULL || varId < 0) return defaultValue; + RValue value = Instance_getSelfVar(inst, varId); + if (value.type == RVALUE_UNDEFINED) return defaultValue; + return RValue_toReal(value); } -static RValue builtinMpGridAddCell(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); - if (g == nullptr) return RValue_makeUndefined(); - int32_t cx = RValue_toInt32(args[1]); - int32_t cy = RValue_toInt32(args[2]); - if (cx < 0 || cy < 0 || cx >= g->hcells || cy >= g->vcells) return RValue_makeUndefined(); - g->cells[cx * g->vcells + cy] = 1; - return RValue_makeUndefined(); +static void nativeOverride_setSelfVarReal(Instance* inst, int32_t varId, GMLReal value) { + if (inst == NULL || varId < 0) return; + Instance_setSelfVar(inst, varId, RValue_makeReal(value)); } -static RValue builtinMpGridClearCell(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); - if (g == nullptr) return RValue_makeUndefined(); - int32_t cx = RValue_toInt32(args[1]); - int32_t cy = RValue_toInt32(args[2]); - if (cx < 0 || cy < 0 || cx >= g->hcells || cy >= g->vcells) return RValue_makeUndefined(); - g->cells[cx * g->vcells + cy] = 0; - return RValue_makeUndefined(); +static GMLReal nativeOverride_randomReal(GMLReal maxValue) { + if (maxValue <= 0.0) return 0.0; + return (((GMLReal) rand()) / (((GMLReal) RAND_MAX) + 1.0)) * maxValue; } -static RValue builtinMpGridAddRectangle(VMContext* ctx, RValue* args, int32_t argCount) { - if (5 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); - if (g == nullptr) return RValue_makeUndefined(); - int32_t x1 = RValue_toInt32(args[1]); - int32_t y1 = RValue_toInt32(args[2]); - int32_t x2 = RValue_toInt32(args[3]); - int32_t y2 = RValue_toInt32(args[4]); - if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; - if (x2 >= g->hcells) x2 = g->hcells - 1; - if (y2 >= g->vcells) y2 = g->vcells - 1; - for (int32_t cx = x1; x2 >= cx; cx++) { - for (int32_t cy = y1; y2 >= cy; cy++) { - g->cells[cx * g->vcells + cy] = 1; - } - } - return RValue_makeUndefined(); -} +typedef struct { + bool resolved; + int32_t sprSteamerBottom; + int32_t sprSteamerTop; + int32_t objSteamplume2; + int32_t varTimer; + int32_t varFL; + int32_t varFD; + int32_t varAA; + int32_t varT; +} NativeVentCache; + +static NativeVentCache g_nativeVentCache = { + .resolved = false, + .sprSteamerBottom = -1, + .sprSteamerTop = -1, + .objSteamplume2 = -1, + .varTimer = -1, + .varFL = -1, + .varFD = -1, + .varAA = -1, + .varT = -1, +}; -static RValue builtinMpGridClearRectangle(VMContext* ctx, RValue* args, int32_t argCount) { - if (5 > argCount) return RValue_makeUndefined(); - Runner* runner = (Runner*) ctx->runner; - MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); - if (g == nullptr) return RValue_makeUndefined(); - int32_t x1 = RValue_toInt32(args[1]); - int32_t y1 = RValue_toInt32(args[2]); - int32_t x2 = RValue_toInt32(args[3]); - int32_t y2 = RValue_toInt32(args[4]); - if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; - if (x2 >= g->hcells) x2 = g->hcells - 1; - if (y2 >= g->vcells) y2 = g->vcells - 1; - for (int32_t cx = x1; x2 >= cx; cx++) { - for (int32_t cy = y1; y2 >= cy; cy++) { - g->cells[cx * g->vcells + cy] = 0; - } - } - return RValue_makeUndefined(); -} +static void nativeOverride_ensureVentCache(VMContext* ctx) { + if (g_nativeVentCache.resolved || ctx == NULL || ctx->runner == NULL) return; -static RValue builtinMpGridGetCell(VMContext* ctx, RValue* args, int32_t argCount) { - if (3 > argCount) return RValue_makeInt32(0); Runner* runner = (Runner*) ctx->runner; - MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); - if (g == nullptr) return RValue_makeInt32(0); - int32_t cx = RValue_toInt32(args[1]); - int32_t cy = RValue_toInt32(args[2]); - if (cx < 0 || cy < 0 || cx >= g->hcells || cy >= g->vcells) return RValue_makeInt32(0); - // Native returns -1 for blocked, 0 for clear - return RValue_makeInt32(g->cells[cx * g->vcells + cy] ? -1 : 0); + g_nativeVentCache.sprSteamerBottom = shget(runner->assetsByName, "spr_steamer_bottom"); + g_nativeVentCache.sprSteamerTop = shget(runner->assetsByName, "spr_steamer_top"); + g_nativeVentCache.objSteamplume2 = shget(runner->assetsByName, "obj_steamplume2"); + g_nativeVentCache.varTimer = nativeOverride_resolveSelfVarId(ctx, "timer"); + g_nativeVentCache.varFL = nativeOverride_resolveSelfVarId(ctx, "f_l"); + g_nativeVentCache.varFD = nativeOverride_resolveSelfVarId(ctx, "f_d"); + g_nativeVentCache.varAA = nativeOverride_resolveSelfVarId(ctx, "aa"); + g_nativeVentCache.varT = nativeOverride_resolveSelfVarId(ctx, "t"); + g_nativeVentCache.resolved = true; } -static RValue builtinMpGridDraw(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - return RValue_makeUndefined(); -} +// Native translation of gml_Object_obj_piper_steam_Draw_0. +static RValue builtinPiperSteamDraw(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + if (ctx == NULL || ctx->runner == NULL || ctx->runner->renderer == NULL) return RValue_makeUndefined(); -// mp_grid_path(id, path, xstart, ystart, xgoal, ygoal, allowDiagonals) -// BFS pathfinder: fills `path` with cell-center waypoints from start to goal. -// Returns true if a path was found. -static RValue builtinMpGridPath(VMContext* ctx, RValue* args, int32_t argCount) { - if (7 > argCount) return RValue_makeBool(false); Runner* runner = (Runner*) ctx->runner; - MpGrid* mp = mpGridGet(runner, RValue_toInt32(args[0])); - if (mp == nullptr) return RValue_makeBool(false); - int32_t pathIdx = RValue_toInt32(args[1]); - if (0 > pathIdx || (uint32_t) pathIdx >= runner->dataWin->path.count) return RValue_makeBool(false); - GamePath* pPath = &runner->dataWin->path.paths[pathIdx]; + Instance* self = (Instance*) ctx->currentInstance; + if (self == NULL) return RValue_makeUndefined(); - GMLReal xstart = RValue_toReal(args[2]); - GMLReal ystart = RValue_toReal(args[3]); - GMLReal xgoal = RValue_toReal(args[4]); - GMLReal ygoal = RValue_toReal(args[5]); - bool allowdiag = RValue_toBool(args[6]); + nativeOverride_ensureVentCache(ctx); - // Find the start & goal cells & check them. - int32_t cxs = (int32_t) GMLReal_floor((xstart - mp->left) / mp->cellWidth); - int32_t cys = (int32_t) GMLReal_floor((ystart - mp->top) / mp->cellHeight); - int32_t cxg = (int32_t) GMLReal_floor((xgoal - mp->left) / mp->cellWidth); - int32_t cyg = (int32_t) GMLReal_floor((ygoal - mp->top) / mp->cellHeight); + GMLReal timer = nativeOverride_getSelfVarReal(self, g_nativeVentCache.varTimer, 0.0); + GMLReal f_l = nativeOverride_getSelfVarReal(self, g_nativeVentCache.varFL, 0.0); - if (cxs < 0 || cxs >= mp->hcells || cys < 0 || cys >= mp->vcells) return RValue_makeBool(false); - if (cxg < 0 || cxg >= mp->hcells || cyg < 0 || cyg >= mp->vcells) return RValue_makeBool(false); - if (mp->cells[cxs * mp->vcells + cys]) return RValue_makeBool(false); - if (mp->cells[cxg * mp->vcells + cyg]) return RValue_makeBool(false); + timer += 1.0; + Renderer_drawSprite(runner->renderer, g_nativeVentCache.sprSteamerBottom, 0, self->x, self->y); + Renderer_drawSprite(runner->renderer, g_nativeVentCache.sprSteamerTop, 0, self->x, self->y + (float) (f_l * 3.0)); - // Start the search. - int32_t total = mp->hcells * mp->vcells; - int32_t* dist = (int32_t*) malloc(total * sizeof(int32_t)); - int32_t* qq = (int32_t*) malloc(total * sizeof(int32_t)); - if (dist == nullptr || qq == nullptr) { - free(dist); free(qq); - return RValue_makeBool(false); + if (timer == 30.0) { + f_l = 0.0; + nativeOverride_setSelfVarReal(self, g_nativeVentCache.varFD, 1.0); } - for (int32_t i = 0; total > i; i++) dist[i] = -1; - - int32_t startIdx = cxs * mp->vcells + cys; - int32_t goalIdx = cxg * mp->vcells + cyg; - int32_t head = 0, tail = 0; - dist[startIdx] = 1; - qq[tail++] = startIdx; - bool result = false; - while (tail > head) { - int32_t val = qq[head++]; - int32_t xx = val / mp->vcells; - int32_t yy = val % mp->vcells; - if (xx == cxg && yy == cyg) { - result = true; - break; + if (timer > 30.0 && 50.0 > timer) { + Instance* plume = Runner_createInstance(runner, self->x + 7.0, self->y + 6.0 + f_l * 3.0, g_nativeVentCache.objSteamplume2); + if (plume != NULL && ctx->creatorVarID >= 0) { + Instance_setSelfVar(plume, ctx->creatorVarID, RValue_makeReal((GMLReal) self->instanceId)); } - int32_t d = dist[val] + 1; - bool f1 = (xx > 0) && (yy < mp->vcells - 1) && (dist[(xx - 1) * mp->vcells + (yy + 1)] == -1) && !mp->cells[(xx - 1) * mp->vcells + (yy + 1)]; - bool f2 = (yy < mp->vcells - 1) && (dist[xx * mp->vcells + (yy + 1)] == -1) && !mp->cells[xx * mp->vcells + (yy + 1)]; - bool f3 = (xx < mp->hcells - 1) && (yy < mp->vcells - 1) && (dist[(xx + 1) * mp->vcells + (yy + 1)] == -1) && !mp->cells[(xx + 1) * mp->vcells + (yy + 1)]; - bool f4 = (xx > 0) && (dist[(xx - 1) * mp->vcells + yy] == -1) && !mp->cells[(xx - 1) * mp->vcells + yy]; - bool f6 = (xx < mp->hcells - 1) && (dist[(xx + 1) * mp->vcells + yy] == -1) && !mp->cells[(xx + 1) * mp->vcells + yy]; - bool f7 = (xx > 0) && (yy > 0) && (dist[(xx - 1) * mp->vcells + (yy - 1)] == -1) && !mp->cells[(xx - 1) * mp->vcells + (yy - 1)]; - bool f8 = (yy > 0) && (dist[xx * mp->vcells + (yy - 1)] == -1) && !mp->cells[xx * mp->vcells + (yy - 1)]; - bool f9 = (xx < mp->hcells - 1) && (yy > 0) && (dist[(xx + 1) * mp->vcells + (yy - 1)] == -1) && !mp->cells[(xx + 1) * mp->vcells + (yy - 1)]; - - // Handle horizontal & vertical moves. - if (f4) { - dist[(xx - 1) * mp->vcells + yy] = d; - qq[tail++] = (xx - 1) * mp->vcells + yy; - } - if (f6) { - dist[(xx + 1) * mp->vcells + yy] = d; - qq[tail++] = (xx + 1) * mp->vcells + yy; - } - if (f8) { - dist[xx * mp->vcells + (yy - 1)] = d; - qq[tail++] = xx * mp->vcells + (yy - 1); - } - if (f2) { - dist[xx * mp->vcells + (yy + 1)] = d; - qq[tail++] = xx * mp->vcells + (yy + 1); - } - // Handle diagonal moves (require both cardinal neighbors clear, matching HTML5). - if (allowdiag && f1 && f2 && f4) { - dist[(xx - 1) * mp->vcells + (yy + 1)] = d; - qq[tail++] = (xx - 1) * mp->vcells + (yy + 1); - } - if (allowdiag && f7 && f8 && f4) { - dist[(xx - 1) * mp->vcells + (yy - 1)] = d; - qq[tail++] = (xx - 1) * mp->vcells + (yy - 1); - } - if (allowdiag && f3 && f2 && f6) { - dist[(xx + 1) * mp->vcells + (yy + 1)] = d; - qq[tail++] = (xx + 1) * mp->vcells + (yy + 1); - } - if (allowdiag && f9 && f8 && f6) { - dist[(xx + 1) * mp->vcells + (yy - 1)] = d; - qq[tail++] = (xx + 1) * mp->vcells + (yy - 1); - } - } - - if (!result) { - free(dist); free(qq); - return RValue_makeBool(false); - } - - // Compute the path from back to front. At each step, scan neighbors with dist == val-1 in the order LEFT, RIGHT, UP, DOWN, then diagonals - int32_t chainCap = 16; - int32_t chainLen = 0; - int32_t* chain = (int32_t*) malloc(chainCap * sizeof(int32_t)); - { - int32_t xx = cxg; - int32_t yy = cyg; - chain[chainLen++] = xx * mp->vcells + yy; - while (xx != cxs || yy != cys) { - if (chainLen >= chainCap) { - chainCap *= 2; - chain = (int32_t*) realloc(chain, chainCap * sizeof(int32_t)); - } - int32_t val = dist[xx * mp->vcells + yy]; - bool f1 = (xx > 0) && (yy < mp->vcells - 1) && (dist[(xx - 1) * mp->vcells + (yy + 1)] == val - 1); - bool f2 = (yy < mp->vcells - 1) && (dist[xx * mp->vcells + (yy + 1)] == val - 1); - bool f3 = (xx < mp->hcells - 1) && (yy < mp->vcells - 1) && (dist[(xx + 1) * mp->vcells + (yy + 1)] == val - 1); - bool f4 = (xx > 0) && (dist[(xx - 1) * mp->vcells + yy] == val - 1); - bool f6 = (xx < mp->hcells - 1) && (dist[(xx + 1) * mp->vcells + yy] == val - 1); - bool f7 = (xx > 0) && (yy > 0) && (dist[(xx - 1) * mp->vcells + (yy - 1)] == val - 1); - bool f8 = (yy > 0) && (dist[xx * mp->vcells + (yy - 1)] == val - 1); - bool f9 = (xx < mp->hcells - 1) && (yy > 0) && (dist[(xx + 1) * mp->vcells + (yy - 1)] == val - 1); - - // Four directions movement - if (f4) { xx = xx - 1; } else if (f6) { xx = xx + 1; } else if (f8) { yy = yy - 1; } else if (f2) { yy = yy + 1; } else if (allowdiag && f1) { - xx = xx - 1; - yy = yy + 1; - } else if (allowdiag && f3) { - xx = xx + 1; - yy = yy + 1; - } else if (allowdiag && f7) { - xx = xx - 1; - yy = yy - 1; - } else if (allowdiag && f9) { - xx = xx + 1; - yy = yy - 1; - } else { - // Should be unreachable: BFS reached goal, so a predecessor must exist. - free(chain); - free(dist); - free(qq); - return RValue_makeBool(false); - } - chain[chainLen++] = xx * mp->vcells + yy; - } + f_l += 0.3; + if (f_l >= 3.0) timer = 50.0; } - // Build the output path. - // We walk "chain" in reverse to emit start-first, with explicit overrides so the endpoints are exactly (xstart, ystart) / (xgoal, ygoal) instead of cell centers. - free(pPath->points); - pPath->points = nullptr; - pPath->pointCount = 0; - - // When start cell == goal cell, chain has 1 node but the native runner and GameMaker-HTML5 still emit a 2-point path (start coord + goal coord). - // Without this, the path length is 0, adaptPath early-returns before advancing pathPosition past 1.0, and the OTHER_END_OF_PATH event never fires. - int32_t pointCount = (startIdx == goalIdx) ? 2 : chainLen; - pPath->points = (PathPoint*) malloc(pointCount * sizeof(PathPoint)); - pPath->pointCount = (uint32_t) pointCount; - for (int32_t i = 0; pointCount > i; i++) { - float wx, wy; - if (startIdx == goalIdx) { - wx = (float) (i == 0 ? xstart : xgoal); - wy = (float) (i == 0 ? ystart : ygoal); - } else { - int32_t idx = chain[chainLen - 1 - i]; - int32_t xx = idx / mp->vcells; - int32_t yy = idx % mp->vcells; - wx = (float) (mp->left + (xx + 0.5) * mp->cellWidth); - wy = (float) (mp->top + (yy + 0.5) * mp->cellHeight); - if (i == 0) { wx = (float) xstart; wy = (float) ystart; } - if (i == chainLen - 1) { wx = (float) xgoal; wy = (float) ygoal; } + if (timer >= 50.0 && 90.0 > timer) { + f_l -= 0.1; + if (f_l <= 0.0) { + f_l = 0.0; + timer = 25.0; } - pPath->points[i].x = wx; - pPath->points[i].y = wy; - pPath->points[i].speed = 100.0f; - } - free(chain); - free(dist); - free(qq); - - free(pPath->internalPoints); - pPath->internalPoints = nullptr; - pPath->internalPointCount = 0; - pPath->length = 0.0f; - GamePath_computeInternal(pPath); - - return RValue_makeBool(true); -} - -// path_start(path, speed, endaction, absolute) - HTML5: Assign_Path (yyInstance.js:2695-2743) -static RValue builtinPathStart(VMContext* ctx, RValue* args, int32_t argCount) { - if (4 > argCount) return RValue_makeUndefined(); - - Instance* inst = (Instance*) ctx->currentInstance; - if (inst == nullptr) return RValue_makeUndefined(); - - Runner* runner = (Runner*) ctx->runner; - int32_t pathIdx = RValue_toInt32(args[0]); - GMLReal speed = RValue_toReal(args[1]); - int32_t endAction = RValue_toInt32(args[2]); - bool absolute = RValue_toBool(args[3]); - - // Validate path index - inst->pathIndex = -1; - if (0 > pathIdx) return RValue_makeUndefined(); - if ((uint32_t) pathIdx >= runner->dataWin->path.count) return RValue_makeUndefined(); - - GamePath* path = &runner->dataWin->path.paths[pathIdx]; - if (0.0 >= path->length) return RValue_makeUndefined(); - - inst->pathIndex = pathIdx; - inst->pathSpeed = (float) speed; - - if (inst->pathSpeed >= 0.0f) { - inst->pathPosition = 0.0f; - } else { - inst->pathPosition = 1.0f; - } - - inst->pathPositionPrevious = inst->pathPosition; - inst->pathScale = 1.0f; - inst->pathOrientation = 0.0f; - inst->pathEndAction = endAction; - - if (absolute) { - PathPositionResult startPos = GamePath_getPosition(path, inst->pathSpeed >= 0.0f ? 0.0f : 1.0f); - inst->x = (float) startPos.x; - inst->y = (float) startPos.y; - SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); - - PathPositionResult origin = GamePath_getPosition(path, 0.0f); - inst->pathXStart = (float) origin.x; - inst->pathYStart = (float) origin.y; - } else { - inst->pathXStart = inst->x; - inst->pathYStart = inst->y; } + nativeOverride_setSelfVarReal(self, g_nativeVentCache.varTimer, timer); + nativeOverride_setSelfVarReal(self, g_nativeVentCache.varFL, f_l); return RValue_makeUndefined(); } -// path_get_length(path) - returns total length of the path in pixels -static RValue builtinPathGetLength(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeReal(0.0); - Runner* runner = (Runner*) ctx->runner; - int32_t pathIdx = RValue_toInt32(args[0]); - if (0 > pathIdx) return RValue_makeReal(0.0); - if ((uint32_t) pathIdx >= runner->dataWin->path.count) return RValue_makeReal(0.0); - return RValue_makeReal((GMLReal) runner->dataWin->path.paths[pathIdx].length); -} - -// path_end() - HTML5: Assign_Path(-1,...) -static RValue builtinPathEnd(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { - Instance* inst = (Instance*) ctx->currentInstance; - if (inst != nullptr) { - inst->pathIndex = -1; - } - return RValue_makeUndefined(); -} - -// string_hash_to_newline - converts # to \n in a string -static RValue builtinStringHashToNewline(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) return RValue_makeString(""); - RValue original = args[0]; // This is a copy +// Native translation of gml_Object_obj_steamplume2_Create_0. +static RValue builtinSteamplume2Create(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Instance* self = (Instance*) ctx->currentInstance; + if (self == NULL) return RValue_makeUndefined(); - if (original.type != RVALUE_STRING) { - // Fast path: If the argument is not a string, return a copy of it - return RValue_makeOwnedString(RValue_toString(original)); - } + nativeOverride_ensureVentCache(ctx); - if (original.string == nullptr) { - // Fast path: If the argument is a string but has no value, return an empty string - return RValue_makeString(""); - } + self->friction = 0.1f; + self->vspeed = -6.0f; + self->imageXscale = 0.3f; + self->imageYscale = 0.3f; + self->imageAngle = (float) nativeOverride_randomReal(360.0); + self->hspeed = (float) (0.2 - nativeOverride_randomReal(0.4)); + Instance_computeSpeedFromComponents(self); - PreprocessedText result = TextUtils_preprocessGmlText(original.string); - if (!result.owning) { - // No # found, steal the reference to avoid copying the string - args[0].ownsReference = false; - return original; - } - return RValue_makeOwnedString((char*) result.text); + nativeOverride_setSelfVarReal(self, g_nativeVentCache.varAA, 2.0 - nativeOverride_randomReal(4.0)); + nativeOverride_setSelfVarReal(self, g_nativeVentCache.varT, 0.0); + return RValue_makeUndefined(); } -// json_decode -static RValue builtinJsonDecode(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) { - fprintf(stderr, "[json_decode] Expected at least 1 argument\n"); - return RValue_makeUndefined(); - } +// Native translation of gml_Object_obj_steamplume2_Step_0. +static RValue builtinSteamplume2Step(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + if (ctx == NULL || ctx->runner == NULL) return RValue_makeUndefined(); Runner* runner = (Runner*) ctx->runner; - int32_t mapIndex = dsMapCreate(runner); - DsMapEntry **mapPtr = dsMapGet(runner, mapIndex); - const char* content = args[0].string; - const JsonValue* json = JsonReader_parse(content); - - repeat(JsonReader_objectLength(json), i) { - const char *key = safeStrdup(JsonReader_getObjectKey(json, i)); - RValue val = RValue_makeOwnedString(safeStrdup(JsonReader_getString(JsonReader_getObjectValue(json, i)))); - shput(*mapPtr, key, val); - } - - JsonReader_free(json); - - return RValue_makeReal(mapIndex); -} + Instance* self = (Instance*) ctx->currentInstance; + if (self == NULL) return RValue_makeUndefined(); + + nativeOverride_ensureVentCache(ctx); + + GMLReal t = nativeOverride_getSelfVarReal(self, g_nativeVentCache.varT, 0.0); + GMLReal aa = nativeOverride_getSelfVarReal(self, g_nativeVentCache.varAA, 0.0); + + self->imageXscale += 0.1f; + self->imageYscale += 0.1f; + t += 1.0; + + if (t > 7.0) self->imageAlpha -= 0.08f; + if (self->imageAlpha <= 0.02f) Runner_destroyInstance(runner, self); + + self->imageAngle += (float) aa; + nativeOverride_setSelfVarReal(self, g_nativeVentCache.varT, t); + return RValue_makeUndefined(); +} + +static bool battleDraw_isValidObjectIndex(VMContext* ctx, int32_t objectIndex) { + return ctx != NULL && + ctx->dataWin != NULL && + objectIndex >= 0 && + (uint32_t) objectIndex < ctx->dataWin->objt.count; +} + +static bool battleDraw_objectMatchesNameInHierarchy(VMContext* ctx, int32_t objectIndex, const char* name) { + int32_t depth = 0; + while (battleDraw_isValidObjectIndex(ctx, objectIndex) && depth < 64) { + const char* objectName = ctx->dataWin->objt.objects[objectIndex].name; + if (objectName != NULL && strcmp(objectName, name) == 0) return true; + objectIndex = ctx->dataWin->objt.objects[objectIndex].parentId; + depth++; + } + return false; +} + +static bool battleDraw_is3DSBattleActive(VMContext* ctx, Runner* runner) { + if (ctx == NULL || runner == NULL) return false; + if (runner->instances == NULL || ctx->dataWin == NULL) return false; + + int32_t instanceCount = (int32_t) arrlen(runner->instances); + bool hasLiveBattleBorder = false; + bool hasActiveBattleController = false; + repeat(instanceCount, i) { + Instance* inst = runner->instances[i]; + if (inst == NULL) continue; + if (inst->destroyed || !inst->active) continue; + if (!battleDraw_isValidObjectIndex(ctx, inst->objectIndex)) continue; + + if (battleDraw_objectMatchesNameInHierarchy(ctx, inst->objectIndex, "obj_uborder") || + battleDraw_objectMatchesNameInHierarchy(ctx, inst->objectIndex, "obj_dborder") || + battleDraw_objectMatchesNameInHierarchy(ctx, inst->objectIndex, "obj_lborder") || + battleDraw_objectMatchesNameInHierarchy(ctx, inst->objectIndex, "obj_rborder") || + battleDraw_objectMatchesNameInHierarchy(ctx, inst->objectIndex, "obj_blackborderer")) { + if (inst->visible) hasLiveBattleBorder = true; + } + + if (battleDraw_objectMatchesNameInHierarchy(ctx, inst->objectIndex, "obj_battlecontroller") && + ((int32_t) battleDraw_getInstReal(ctx, inst, "drawrect") == 1 || + (int32_t) battleDraw_getInstReal(ctx, inst, "drawbinfo") == 1)) { + hasActiveBattleController = true; + } + } + + return hasLiveBattleBorder && hasActiveBattleController; +} + +static inline GMLReal battleDraw_instX(Instance* inst) { return inst != NULL ? (GMLReal) inst->x : 0.0; } +static inline GMLReal battleDraw_instY(Instance* inst) { return inst != NULL ? (GMLReal) inst->y : 0.0; } + +// Script index cache — resolved once on first call. +typedef struct { + int32_t scr_binfowrite; + int32_t scr_setfont; + int32_t scr_gettext; + int32_t ossafe_fill_rectangle; + bool resolved; +} BattleDrawScriptCache; + +static BattleDrawScriptCache g_battleDrawCache = { -2, -2, -2, -2, false }; + +static void battleDraw_ensureCache(VMContext* ctx) { + if (g_battleDrawCache.resolved) return; + g_battleDrawCache.scr_binfowrite = battleDraw_resolveScript(ctx, "gml_Script_scr_binfowrite"); + g_battleDrawCache.scr_setfont = battleDraw_resolveScript(ctx, "gml_Script_scr_setfont"); + g_battleDrawCache.scr_gettext = battleDraw_resolveScript(ctx, "gml_Script_scr_gettext"); + g_battleDrawCache.ossafe_fill_rectangle = battleDraw_resolveScript(ctx, "gml_Script_ossafe_fill_rectangle"); + if (g_battleDrawCache.scr_binfowrite < 0) g_battleDrawCache.scr_binfowrite = battleDraw_resolveScript(ctx, "scr_binfowrite"); + if (g_battleDrawCache.scr_setfont < 0) g_battleDrawCache.scr_setfont = battleDraw_resolveScript(ctx, "scr_setfont"); + if (g_battleDrawCache.scr_gettext < 0) g_battleDrawCache.scr_gettext = battleDraw_resolveScript(ctx, "scr_gettext"); + if (g_battleDrawCache.ossafe_fill_rectangle < 0) g_battleDrawCache.ossafe_fill_rectangle = battleDraw_resolveScript(ctx, "ossafe_fill_rectangle"); + g_battleDrawCache.resolved = true; +} + +// Native translation of gml_Object_obj_battlecontroller_Draw_0. +static RValue builtinBattleControllerDraw(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->renderer == NULL) return RValue_makeUndefined(); + +#ifdef __3DS__ + if (runner->osType == OS_3DS && N3DSRenderer_isTopScreenGUIActive(runner->renderer)) { + return RValue_makeUndefined(); + } +#endif + + battleDraw_ensureCache(ctx); + + Instance* self = (Instance*) ctx->currentInstance; + if (self == NULL) return RValue_makeUndefined(); + + Renderer* rend = runner->renderer; + + GMLReal turntimer = RValue_toReal(battleDraw_getGlobal(ctx, "turntimer")); + if (turntimer > 0.0) { + self->depth = -1000; + runner->drawableListSortDirty = true; + rend->drawColor = 0x0000FFu; // c_red (BGR) + ptrdiff_t ttIdx = shgeti(ctx->globalVarNameMap, "turntimer"); + if (ttIdx >= 0) { + int32_t varID = ctx->globalVarNameMap[ttIdx].value; + if ((uint32_t) varID < ctx->globalVarCount) { + RValue_free(&ctx->globalVars[varID]); + ctx->globalVars[varID] = RValue_makeReal(turntimer - 1.0); + } + } + } + + if (battleDraw_instanceExists(ctx, runner, "obj_uborder")) { + self->depth = 5; + runner->drawableListSortDirty = true; + rend->drawColor = 0x000000u; // c_black (BGR) + + if ((int32_t) battleDraw_getInstReal(ctx, self, "drawrect") == 1) { + Instance* uborder = battleDraw_getFirstInstance(ctx, runner, "obj_uborder"); + Instance* rborder = battleDraw_getFirstInstance(ctx, runner, "obj_rborder"); + Instance* dborder = battleDraw_getFirstInstance(ctx, runner, "obj_dborder"); + GMLReal x1 = battleDraw_instX(uborder) + 5.0; + GMLReal y1 = battleDraw_instY(uborder) + 5.0; + GMLReal x2 = battleDraw_instX(rborder); + GMLReal y2 = battleDraw_instY(dborder); + RValue fillArgs[4] = { + RValue_makeReal(x1), RValue_makeReal(y1), + RValue_makeReal(x2), RValue_makeReal(y2) + }; + if (g_battleDrawCache.ossafe_fill_rectangle >= 0) + VM_callCodeIndex(ctx, g_battleDrawCache.ossafe_fill_rectangle, fillArgs, 4); + else + rend->vtable->drawRectangle(rend, (float) x1, (float) y1, (float) x2, (float) y2, rend->drawColor, rend->drawAlpha, false); + } + } + + if (runner->backgroundColor != 0xFFFFFFu && + (int32_t) battleDraw_getInstReal(ctx, self, "drawbinfo") == 1) { + battleDraw_callScript(ctx, g_battleDrawCache.scr_binfowrite); + } + + // read globals shared by the remaining two blocks + GMLReal bmenuno = RValue_toReal(battleDraw_getGlobal(ctx, "bmenuno")); + GMLReal myfight = RValue_toReal(battleDraw_getGlobal(ctx, "myfight")); + GMLReal mnfight = RValue_toReal(battleDraw_getGlobal(ctx, "mnfight")); + + // if (global.bmenuno == 1 && global.myfight == 0 && global.mnfight == 0) + if ((int32_t) bmenuno == 1 && (int32_t) myfight == 0 && (int32_t) mnfight == 0) { + RValue langRV = battleDraw_getGlobal(ctx, "language"); + bool isJa = (langRV.type == RVALUE_STRING && langRV.string != NULL && + strcmp(langRV.string, "ja") == 0); + GMLReal maxwidth = 0.0; + + for (int32_t i = 0; i < 3; i++) { + if ((int32_t) RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "monster", i)) != 1) + continue; + + RValue nameRV = battleDraw_getGlobalArrayElem(ctx, "monstername", i); + const char* monName = (nameRV.type == RVALUE_STRING && nameRV.string != NULL) + ? nameRV.string : ""; + GMLReal width = 0.0; + + if (isJa) { + const uint8_t* p = (const uint8_t*) monName; + while (*p) { + uint32_t cp; + if (*p < 0x80) { cp = *p & 0x7F; p += 1; } + else if (*p < 0xE0) { cp = *p & 0x1F; p += 2; } + else if (*p < 0xF0) { cp = *p & 0x0F; p += 3; } + else { cp = *p & 0x07; p += 4; } + if (cp == 32 || cp >= 65377) width += 13.0; + else if (cp < 8192) width += 16.0; + else width += 26.0; + } + } else { + width = (GMLReal)(TextUtils_utf8CodepointCount(monName, (int32_t) strlen(monName)) * 16); + } + + if (width > maxwidth) maxwidth = width; + } + + // self.xwrite = 190 + maxwidth + GMLReal xwrite = 190.0 + maxwidth; + { + ptrdiff_t slot = shgeti(ctx->selfVarNameMap, "xwrite"); + if (slot >= 0) + Instance_setSelfVar(self, ctx->selfVarNameMap[slot].value, RValue_makeReal(xwrite)); + } + + bool hasSansb = battleDraw_instanceExists(ctx, runner, "obj_sansb"); + + for (int32_t i = 0; i < 3; i++) { + if ((int32_t) RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "monster", i)) != 1) + continue; + if (hasSansb) continue; + + GMLReal lineheight = isJa ? 36.0 : 32.0; + GMLReal y_start = 280.0; + + rend->drawColor = 0x0000FFu; // c_red + float rx1 = (float) xwrite; + float ry1 = (float)(y_start + i * lineheight); + float rx2 = (float)(xwrite + 100.0); + float ry2 = (float)(y_start + i * lineheight + 16.0); + rend->vtable->drawRectangle(rend, rx1, ry1, rx2, ry2, rend->drawColor, rend->drawAlpha, false); + + rend->drawColor = 0x00FF00u; // c_lime + GMLReal mhp = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "monsterhp", i)); + GMLReal mmaxhp = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "monstermaxhp", i)); + float barW = (mmaxhp > 0.0) ? (float)((mhp / mmaxhp) * 100.0) : 0.0f; + rend->vtable->drawRectangle(rend, rx1, ry1, rx1 + barW, ry2, rend->drawColor, rend->drawAlpha, false); + } + } + + // japanese item menu + { + RValue langRV2 = battleDraw_getGlobal(ctx, "language"); + bool isJa2 = (langRV2.type == RVALUE_STRING && langRV2.string != NULL && + strcmp(langRV2.string, "ja") == 0); + + if (isJa2 && bmenuno >= 3.0 && bmenuno < 4.0 && + (int32_t) myfight == 0 && (int32_t) mnfight == 0) { + + int32_t first = (int32_t)((bmenuno - 3.0) * 8.0); + + int32_t fntMain = shget(runner->assetsByName, "fnt_main"); + if (fntMain >= 0) { + RValue fntArg = RValue_makeReal((GMLReal) fntMain); + battleDraw_callScript1(ctx, g_battleDrawCache.scr_setfont, fntArg); + } + + rend->drawColor = 0xFFFFFFu; // c_white + + GMLReal xx = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "idealborder", 0)) + 20.0; + GMLReal yy = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "idealborder", 2)) + 20.0; + + char* lineheaderOwned = NULL; + const char* lineheader = ""; + if (g_battleDrawCache.scr_gettext >= 0) { + RValue keyArg = RValue_makeString("item_menub_header"); + RValue lhRV = VM_callCodeIndex(ctx, g_battleDrawCache.scr_gettext, &keyArg, 1); + if (lhRV.type == RVALUE_STRING && lhRV.string != NULL) { + lineheaderOwned = safeStrdup(lhRV.string); + lineheader = lineheaderOwned; + } + RValue_free(&lhRV); + } + + for (int32_t i = 0; i < 3; i++) { + RValue itemRV = battleDraw_getGlobalArrayElem(ctx, "item", first + i); + if (itemRV.type == RVALUE_UNDEFINED || (int32_t) RValue_toReal(itemRV) == 0) + break; + RValue itemName = battleDraw_getGlobalArrayElem(ctx, "itemnameb", first + i); + const char* itemNameStr = (itemName.type == RVALUE_STRING && itemName.string != NULL) + ? itemName.string : ""; + size_t textLen = strlen(lineheader) + strlen(itemNameStr) + 1; + char* text = (char*) safeMalloc(textLen); + snprintf(text, textLen, "%s%s", lineheader, itemNameStr); + rend->vtable->drawText(rend, text, (float) xx, (float)(yy + i * 36.0), 1.0f, 1.0f, 0.0f); + free(text); + } + + if (lineheaderOwned != NULL) free(lineheaderOwned); + + int32_t num_items = 8; + while (num_items > 0) { + RValue iv = battleDraw_getGlobalArrayElem(ctx, "item", num_items - 1); + if (iv.type != RVALUE_UNDEFINED && (int32_t) RValue_toReal(iv) != 0) break; + num_items--; + } + + if (num_items > 3) { + Instance* objTime = battleDraw_getFirstInstance(ctx, runner, "obj_time"); + GMLReal timeVal = (objTime != NULL) ? battleDraw_getInstReal(ctx, objTime, "time") : 0.0; + + GMLReal ib1 = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "idealborder", 1)); + GMLReal ib2 = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "idealborder", 2)); + GMLReal ib3 = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "idealborder", 3)); + GMLReal xxArrow = ib1 - 30.0; + GMLReal yyArrow = floor((ib2 + ib3) / 2.0) - (5.0 * (2.0 + num_items)); + GMLReal arrow_yofs = round(fmin(fmod(timeVal, 30.0) / 30.0, 0.5) * 6.0); + + int32_t sprArrow = shget(runner->assetsByName, "spr_bitem_ja_arrow"); + + if (first > 0 && sprArrow >= 0 && (uint32_t) sprArrow < ctx->dataWin->sprt.count) { + int32_t tpag = Renderer_resolveTPAGIndex(ctx->dataWin, sprArrow, 0); + if (tpag >= 0) { + Sprite* spr = &ctx->dataWin->sprt.sprites[sprArrow]; + rend->vtable->drawSprite(rend, tpag, + (float) xxArrow, (float)(yyArrow - arrow_yofs), + (float) spr->originX, (float) spr->originY, + 1.0f, 1.0f, 0.0f, 0xFFFFFFu, rend->drawAlpha); + } + } + + yyArrow += 10.0; + + GMLReal bmenucoord3 = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "bmenucoord", 3)); + for (int32_t i = 0; i < num_items; i++) { + int32_t sprIdx = ((first + (int32_t) bmenucoord3) == i) ? 45 : 44; + if ((uint32_t) sprIdx < ctx->dataWin->sprt.count) { + int32_t tpag = Renderer_resolveTPAGIndex(ctx->dataWin, sprIdx, 0); + if (tpag >= 0) { + Sprite* spr = &ctx->dataWin->sprt.sprites[sprIdx]; + rend->vtable->drawSprite(rend, tpag, + (float) xxArrow, (float) yyArrow, + (float) spr->originX, (float) spr->originY, + 1.0f, 1.0f, 0.0f, 0xFFFFFFu, rend->drawAlpha); + } + } + yyArrow += 10.0; + } + + if ((first + 3) < num_items && sprArrow >= 0 && + (uint32_t) sprArrow < ctx->dataWin->sprt.count) { + int32_t tpag = Renderer_resolveTPAGIndex(ctx->dataWin, sprArrow, 0); + if (tpag >= 0) { + Sprite* spr = &ctx->dataWin->sprt.sprites[sprArrow]; + rend->vtable->drawSprite(rend, tpag, + (float) xxArrow, (float)(yyArrow + 10.0 + arrow_yofs), + (float) spr->originX, (float) spr->originY, + 1.0f, -1.0f, 0.0f, 0xFFFFFFu, 1.0f); + } + } + } + } + } + + return RValue_makeUndefined(); +} + +// Native 1:1 translation of gml_Object_obj_blackborderer_Draw_0. +static RValue builtinBlackBordererDraw(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->renderer == NULL) return RValue_makeUndefined(); + +#ifdef __3DS__ + if (runner->osType == OS_3DS && N3DSRenderer_isTopScreenGUIActive(runner->renderer)) { + return RValue_makeUndefined(); + } +#endif + + battleDraw_ensureCache(ctx); + runner->renderer->drawColor = 0x000000u; // c_black (BGR) + + GMLReal ib0 = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "idealborder", 0)); + GMLReal ib1 = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "idealborder", 1)); + GMLReal ib2 = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "idealborder", 2)); + GMLReal ib3 = RValue_toReal(battleDraw_getGlobalArrayElem(ctx, "idealborder", 3)); + + battleDraw_callScript4( + ctx, + g_battleDrawCache.ossafe_fill_rectangle, + RValue_makeReal(ib0 - 60.0), + RValue_makeReal(ib3 + 40.0), + RValue_makeReal(ib1 + 60.0), + RValue_makeReal(ib3) + ); + battleDraw_callScript4( + ctx, + g_battleDrawCache.ossafe_fill_rectangle, + RValue_makeReal(0.0), + RValue_makeReal(ib2), + RValue_makeReal(ib0), + RValue_makeReal(ib3 + 40.0) + ); + battleDraw_callScript4( + ctx, + g_battleDrawCache.ossafe_fill_rectangle, + RValue_makeReal(640.0), + RValue_makeReal(ib2), + RValue_makeReal(ib1), + RValue_makeReal(ib3 + 40.0) + ); + battleDraw_callScript(ctx, g_battleDrawCache.scr_binfowrite); + + return RValue_makeUndefined(); +} + +// Native replacement for gml_Object_obj_snowfloor_Draw_0. +static RValue builtinSnowfloorDraw(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + if (ctx == NULL || ctx->runner == NULL || ctx->runner->renderer == NULL) return RValue_makeUndefined(); + + Runner* runner = (Runner*) ctx->runner; + Renderer* rend = runner->renderer; + Instance* self = (Instance*) ctx->currentInstance; + if (self == NULL) return RValue_makeUndefined(); -static RValue builtinObjectGetSprite(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) { - fprintf(stderr, "[object_get_sprite] Expected at least 1 argument\n"); + GMLArray* dodraw = nativeOverride_ensureSelfArray(ctx, self, "dodraw"); + GMLArray* snowx = nativeOverride_ensureSelfArray(ctx, self, "snowx"); + GMLArray* snowy = nativeOverride_ensureSelfArray(ctx, self, "snowy"); + GMLArray* moveme = nativeOverride_ensureSelfArray(ctx, self, "moveme"); + if (dodraw == NULL || snowx == NULL || snowy == NULL || moveme == NULL) { return RValue_makeUndefined(); } - int32_t id = RValue_toInt32(args[0]); - - return RValue_makeReal(ctx->dataWin->objt.objects[id].spriteId); -} - -// Shared implementation for font_add_sprite and font_add_sprite_ext -static RValue fontAddSpriteImpl(VMContext* ctx, int32_t spriteIndex, uint16_t* charCodes, uint32_t charCount, bool proportional, int32_t sep) { - DataWin* dw = ctx->dataWin; - - if (0 > spriteIndex || (uint32_t) spriteIndex >= dw->sprt.count) { - fprintf(stderr, "[font_add_sprite] Invalid sprite index %d\n", spriteIndex); - return RValue_makeReal(-1.0); - } - - Sprite* sprite = &dw->sprt.sprites[spriteIndex]; - - if (charCount == 0 || sprite->textureCount == 0) { - return RValue_makeReal(-1.0); - } + Instance* mainChara = battleDraw_getFirstInstance(ctx, runner, "obj_mainchara"); + bool mainCharaMoving = mainChara != NULL && (int32_t) nativeOverride_getInstReal(ctx, mainChara, "moving") == 1; + GMLReal bboxLeft = mainChara != NULL ? nativeOverride_getInstReal(ctx, mainChara, "bbox_left") : 0.0; + GMLReal bboxRight = mainChara != NULL ? nativeOverride_getInstReal(ctx, mainChara, "bbox_right") : 0.0; + GMLReal bboxTop = mainChara != NULL ? nativeOverride_getInstReal(ctx, mainChara, "bbox_top") : 0.0; + GMLReal bboxBottom = mainChara != NULL ? nativeOverride_getInstReal(ctx, mainChara, "bbox_bottom") : 0.0; + bool isSnowPuzzleRoom = + runner->currentRoom != NULL && + runner->currentRoom->name != NULL && + strcmp(runner->currentRoom->name, "room_tundra_snowpuzz") == 0; + + rend->drawColor = 0xFFFFFFu; // c_white + + for (int32_t yy = 0; yy < 5; ++yy) { + for (int32_t xx = 0; xx < 5; ++xx) { + int32_t packedIndex = yy * GML_ARRAY_STRIDE + xx; + GMLReal dotX = nativeOverride_getArrayElemReal(snowx, packedIndex, 0.0); + GMLReal dotY = nativeOverride_getArrayElemReal(snowy, packedIndex, 0.0); + GMLReal move = nativeOverride_getArrayElemReal(moveme, packedIndex, 0.0); + + if ((int32_t) nativeOverride_getArrayElemReal(dodraw, packedIndex, 0.0) == 1) { + if (runner->osType == OS_3DS) { + nativeOverride_drawSnowDot3DS(rend, dotX, dotY); + } else { + Renderer_drawCircle(rend, (float) dotX, (float) dotY, 2.8f, false); + } + } - // Limit glyph count to sprite frame count - uint32_t glyphCount = charCount; - if (glyphCount > sprite->textureCount) glyphCount = sprite->textureCount; + if (mainChara != NULL && Collision_circleOverlapsInstance(ctx->dataWin, mainChara, dotX, dotY, 2.0)) { + move = GMLReal_floor((((GMLReal) rand() / (GMLReal) RAND_MAX) * 4.0)) + 2.0; + nativeOverride_setArrayElemReal(moveme, packedIndex, move); + } - // Compute emSize (max bounding height across all frames) and biggestShift - uint32_t maxHeight = 0; - int32_t biggestShift = 0; - repeat(glyphCount, i) { - int32_t tpagIdx = sprite->tpagIndices[i]; - if (0 > tpagIdx) continue; - TexturePageItem* tpag = &dw->tpag.items[tpagIdx]; - if (tpag->boundingHeight > maxHeight) maxHeight = tpag->boundingHeight; - int32_t width = proportional ? (int32_t) tpag->sourceWidth : (int32_t) tpag->boundingWidth; - if (width > biggestShift) biggestShift = width; - } + if (move > 1.0) { + if (mainCharaMoving) { + if (isSnowPuzzleRoom) { + GMLArray* globalFlag = nativeOverride_ensureGlobalArray(ctx, "flag"); + if (globalFlag != NULL && (int32_t) nativeOverride_getArrayElemReal(globalFlag, 64, 0.0) == 0) { + nativeOverride_setArrayElemReal(globalFlag, 64, -1.0); + } + } - // Check if space (0x20) is in the string map - bool hasSpace = false; - repeat(glyphCount, i) { - if (charCodes[i] == 0x20) { hasSpace = true; break; } - } + if (bboxLeft > dotX) dotX -= move; + if (bboxRight < dotX) dotX += move; + if (bboxTop > dotY) dotY -= move; + if (bboxBottom < dotY) dotY += move; - // Allocate glyphs (+ 1 for synthetic space if needed) - uint32_t totalGlyphs = hasSpace ? glyphCount : glyphCount + 1; - FontGlyph* glyphs = safeMalloc(totalGlyphs * sizeof(FontGlyph)); + dotX += ((((GMLReal) rand() / (GMLReal) RAND_MAX) * move) - (move / 2.0)) / 2.0; + dotY += ((((GMLReal) rand() / (GMLReal) RAND_MAX) * move) - (move / 2.0)) / 2.0; - repeat(glyphCount, i) { - int32_t tpagIdx = sprite->tpagIndices[i]; - FontGlyph* glyph = &glyphs[i]; - glyph->character = charCodes[i]; - glyph->kerningCount = 0; - glyph->kerning = nullptr; + nativeOverride_setArrayElemReal(snowx, packedIndex, dotX); + nativeOverride_setArrayElemReal(snowy, packedIndex, dotY); + } - if (0 > tpagIdx) { - glyph->sourceX = 0; - glyph->sourceY = 0; - glyph->sourceWidth = 0; - glyph->sourceHeight = 0; - glyph->shift = (int16_t) sep; - glyph->offset = 0; - continue; + move -= 1.0; + nativeOverride_setArrayElemReal(moveme, packedIndex, move); + } } - - TexturePageItem* tpag = &dw->tpag.items[tpagIdx]; - glyph->sourceX = 0; // not used for sprite fonts (TPAG resolved per glyph) - glyph->sourceY = 0; - glyph->sourceWidth = tpag->sourceWidth; - glyph->sourceHeight = tpag->sourceHeight; - - int32_t advanceWidth = proportional ? (int32_t) tpag->sourceWidth : (int32_t) tpag->boundingWidth; - glyph->shift = (int16_t) (advanceWidth + sep); - - // Horizontal offset: for proportional fonts, no offset; for non-proportional, use target offset minus origin - glyph->offset = proportional ? 0 : (int16_t) ((int32_t) tpag->targetX - sprite->originX); - } - - // Add synthetic space glyph if space is not in the string map - if (!hasSpace) { - FontGlyph* spaceGlyph = &glyphs[glyphCount]; - spaceGlyph->character = 0x20; - spaceGlyph->sourceX = 0; - spaceGlyph->sourceY = 0; - spaceGlyph->sourceWidth = 0; - spaceGlyph->sourceHeight = 0; - spaceGlyph->shift = (int16_t) (biggestShift + sep); - spaceGlyph->offset = 0; - spaceGlyph->kerningCount = 0; - spaceGlyph->kerning = nullptr; - } - - // Grow the font array and create the new font - uint32_t newFontIndex = dw->font.count; - dw->font.count++; - dw->font.fonts = safeRealloc(dw->font.fonts, dw->font.count * sizeof(Font)); - - Font* font = &dw->font.fonts[newFontIndex]; - font->name = "sprite_font"; - font->displayName = "sprite_font"; - font->emSize = (maxHeight > 0) ? maxHeight : sprite->height; - font->bold = false; - font->italic = false; - font->rangeStart = 0; - font->charset = 0; - font->antiAliasing = 0; - font->rangeEnd = 0; - font->tpagIndex = -1; // not used for sprite fonts - font->scaleX = 1.0f; - font->scaleY = 1.0f; - font->ascenderOffset = 0; - font->glyphCount = totalGlyphs; - font->glyphs = glyphs; - font->maxGlyphHeight = maxHeight; // match what HTML5 runner uses for line stride - font->isSpriteFont = true; - font->spriteIndex = spriteIndex; - Font_buildGlyphLUT(font); - - return RValue_makeReal((GMLReal) newFontIndex); -} - -static RValue builtinFontGetName(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) { - fprintf(stderr, "[font_get_name] Expected 1 argument, got 0"); - return RValue_makeUndefined(); - } - - int32_t fontIndex = RValue_toInt32(args[0]); - if (0 > fontIndex || (uint32_t) fontIndex >= ctx->dataWin->font.count) return RValue_makeUndefined(); - return RValue_makeString(ctx->dataWin->font.fonts[fontIndex].name); -} - -// font_add_sprite_ext(sprite, string_map, prop, sep) -static RValue builtinFontAddSpriteExt(VMContext* ctx, RValue* args, int32_t argCount) { - if (4 > argCount) { - fprintf(stderr, "[font_add_sprite_ext] Expected 4 arguments, got %d\n", argCount); - return RValue_makeReal(-1.0); - } - - int32_t spriteIndex = RValue_toInt32(args[0]); - char* stringMap = RValue_toString(args[1]); - bool proportional = RValue_toBool(args[2]); - int32_t sep = RValue_toInt32(args[3]); - - // Decode the string map to get character codes (UTF-8 -> codepoints) - int32_t mapLen = (int32_t) strlen(stringMap); - int32_t mapPos = 0; - uint32_t charCount = 0; - uint16_t charCodes[1024]; - while (mapLen > mapPos && 1024 > charCount) { - charCodes[charCount++] = TextUtils_decodeUtf8(stringMap, mapLen, &mapPos); - } - free(stringMap); - - return fontAddSpriteImpl(ctx, spriteIndex, charCodes, charCount, proportional, sep); -} - -// font_add_sprite(sprite, first, prop, sep) -static RValue builtinFontAddSprite(VMContext* ctx, RValue* args, int32_t argCount) { - if (4 > argCount) { - fprintf(stderr, "[font_add_sprite] Expected 4 arguments, got %d\n", argCount); - return RValue_makeReal(-1.0); } - DataWin* dw = ctx->dataWin; - int32_t spriteIndex = RValue_toInt32(args[0]); - int32_t first = RValue_toInt32(args[1]); - bool proportional = RValue_toBool(args[2]); - int32_t sep = RValue_toInt32(args[3]); - - // Build sequential character codes: first, first+1, first+2, ... - uint32_t frameCount = 0; - if (spriteIndex >= 0 && dw->sprt.count > (uint32_t) spriteIndex) { - frameCount = dw->sprt.sprites[spriteIndex].textureCount; - } - if (frameCount > 1024) frameCount = 1024; - - uint16_t charCodes[1024]; - repeat(frameCount, i) { - charCodes[i] = (uint16_t) (first + (int32_t) i); - } - - return fontAddSpriteImpl(ctx, spriteIndex, charCodes, frameCount, proportional, sep); -} - -static RValue builtinAssetGetIndex(VMContext* ctx, RValue* args, int32_t argCount) { - if (1 > argCount) { - fprintf(stderr, "[asset_get_index] Expected at least 1 argument\n"); - return RValue_makeUndefined(); - } - - char* name = RValue_toString(args[0]); - DataWin* dw = ctx->dataWin; - - int32_t value = shget(ctx->runner->assetsByName, name); - free(name); - return RValue_makeReal(value); -} - -static RValue builtinGpuSetBlendMode(VMContext* ctx, RValue* args, int32_t argCount) { - int mode = RValue_toReal(args[0]); - ctx->runner->renderer->vtable->gpuSetBlendMode(ctx->runner->renderer, mode); - return RValue_makeUndefined(); -} - -static RValue builtinGpuSetBlendModeExt(VMContext* ctx, RValue* args, int32_t argCount) { - int sfactor = RValue_toReal(args[0]); - int dfactor = RValue_toReal(args[1]); - ctx->runner->renderer->vtable->gpuSetBlendModeExt(ctx->runner->renderer, sfactor, dfactor); return RValue_makeUndefined(); } -static bool isBlendEnable = false; -static RValue builtinGpuSetBlendEnable(VMContext* ctx, RValue* args, int32_t argCount) { - bool enable = RValue_toBool(args[0]); - isBlendEnable = enable; - ctx->runner->renderer->vtable->gpuSetBlendEnable(ctx->runner->renderer, enable); - return RValue_makeUndefined(); -} - -static RValue builtinGpuGetBlendEnable(VMContext* ctx, RValue* args, int32_t argCount) { - return RValue_makeBool(isBlendEnable); -} - -static RValue builtinGpuSetAlphaTestEnable(VMContext* ctx, RValue* args, int32_t argCount) { - bool enable = RValue_toBool(args[0]); - ctx->runner->renderer->vtable->gpuSetAlphaTestEnable(ctx->runner->renderer, enable); - return RValue_makeUndefined(); -} - -static RValue builtinGpuSetAlphaTestRef(VMContext* ctx, RValue* args, int32_t argCount) { - ctx->runner->renderer->vtable->gpuSetAlphaTestRef(ctx->runner->renderer, RValue_toInt32(args[0])); - return RValue_makeUndefined(); -} - -static RValue builtinGpuSetColorWriteEnable(VMContext* ctx, RValue* args, int32_t argCount) { - bool r, g, b, a; - if (argCount == 1 && args[0].type == RVALUE_ARRAY && args[0].array != nullptr && GMLArray_length1D(args[0].array) >= 4) { - GMLArray* arr = args[0].array; - r = RValue_toBool(*GMLArray_slot(arr, 0)); - g = RValue_toBool(*GMLArray_slot(arr, 1)); - b = RValue_toBool(*GMLArray_slot(arr, 2)); - a = RValue_toBool(*GMLArray_slot(arr, 3)); - } else if (argCount >= 4) { - r = RValue_toBool(args[0]); - g = RValue_toBool(args[1]); - b = RValue_toBool(args[2]); - a = RValue_toBool(args[3]); - } else { +// Native replacement for gml_Object_obj_lastruins_bg_Step_0. +static RValue builtinLastruinsBgStep(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL) return RValue_makeUndefined(); + + static const float speeds[] = { 0.1f, 0.3f, 0.5f, 0.6f, 1.0f, 1.5f, 2.0f }; + const int32_t layerCount = (int32_t) (sizeof(speeds) / sizeof(speeds[0])); + + for (int32_t i = 0; i < layerCount && i < MAX_BACKGROUNDS; ++i) { + runner->backgrounds[i].x += speeds[i]; + } + + return RValue_makeUndefined(); +} + +#ifdef __3DS__ +static void N3DS_setAsrielRainbowInfoLed(Runner* runner) { + if (runner == NULL || runner->osType != OS_3DS) return; + static bool sMcuHwcInitialized = false; + static const Room* sActiveRoom = NULL; + + if (runner->currentRoom == NULL) { + sActiveRoom = NULL; + g_n3dsAsrielLedTriggered = false; + g_n3dsAsrielLedRoomIndex = -1; + return; + } + g_n3dsAsrielLedRoomIndex = runner->currentRoomIndex; + if (sActiveRoom == runner->currentRoom) return; + + static const uint8_t rainbowStops[7][3] = { + { 255, 0, 0 }, + { 255, 255, 0 }, + { 0, 255, 0 }, + { 0, 255, 255 }, + { 0, 0, 255 }, + { 255, 0, 255 }, + { 255, 0, 0 }, + }; + + InfoLedPattern pattern; + memset(&pattern, 0, sizeof(pattern)); + pattern.delay = 25; + pattern.smoothing = 25; + pattern.loopDelay = 0; + pattern.blinkSpeed = 0; + + for (int32_t i = 0; i < 32; ++i) { + float hue = ((float) i / 32.0f) * 6.0f; + int32_t segment = (int32_t) floorf(hue); + float t = hue - (float) segment; + if (segment < 0) segment = 0; + if (segment > 5) { + segment = 5; + t = 1.0f; + } + + pattern.redPattern[i] = (uint8_t) roundf( + (float) rainbowStops[segment][0] + + ((float) rainbowStops[segment + 1][0] - (float) rainbowStops[segment][0]) * t + ); + pattern.greenPattern[i] = (uint8_t) roundf( + (float) rainbowStops[segment][1] + + ((float) rainbowStops[segment + 1][1] - (float) rainbowStops[segment][1]) * t + ); + pattern.bluePattern[i] = (uint8_t) roundf( + (float) rainbowStops[segment][2] + + ((float) rainbowStops[segment + 1][2] - (float) rainbowStops[segment][2]) * t + ); + } + + if (!sMcuHwcInitialized) { + Result initRc = mcuHwcInit(); + g_n3dsAsrielLedInitRc = initRc; + if (!R_SUCCEEDED(initRc)) { + g_n3dsAsrielLedTriggered = true; + g_n3dsAsrielLedRoomIndex = runner->currentRoomIndex; + return; + } + sMcuHwcInitialized = true; + } else { + g_n3dsAsrielLedInitRc = 0; + } + + Result setRc = MCUHWC_SetInfoLedPattern(&pattern); + g_n3dsAsrielLedSetRc = setRc; + g_n3dsAsrielLedTriggered = true; + g_n3dsAsrielLedRoomIndex = runner->currentRoomIndex; + if (R_SUCCEEDED(setRc)) { + sActiveRoom = runner->currentRoom; + } +} +#endif + +// Native replacement for gml_Object_obj_backgrounder_lastruins_Other_10. +static RValue builtinBackgrounderLastruinsOther10(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->currentRoom == NULL) return RValue_makeUndefined(); + + static const float scrollSpeeds[] = { 0.1f, 0.3f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; + const int32_t layerCount = (int32_t) (sizeof(scrollSpeeds) / sizeof(scrollSpeeds[0])); + const float maxViewX = (float) (runner->currentRoom->width - 320); + float viewX = (float) runner->views[0].viewX; + + if (viewX > maxViewX) viewX = maxViewX; + + for (int32_t i = 0; i < layerCount && i < MAX_BACKGROUNDS; ++i) { + if (viewX >= 0.0f) { + runner->backgrounds[i].x = floorf(viewX - (viewX * scrollSpeeds[i])); + } + if (viewX >= maxViewX) { + runner->backgrounds[i].x = floorf(maxViewX - (maxViewX * scrollSpeeds[i])); + } + } + + return RValue_makeUndefined(); +} + +// ===[ n3ds_render_battle_scene ]=== +static RValue builtinN3DSRenderBattleScene(VMContext* ctx, RValue* args, int32_t argCount) { + if (argCount < 2) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + if (runner == nullptr || runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : (int32_t) runner->dataWin->gen8.defaultWindowWidth; + int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : (int32_t) runner->dataWin->gen8.defaultWindowHeight; + if (guiW <= 0) guiW = 320; + if (guiH <= 0) guiH = 240; + + int32_t extraArgCount = argCount - 2; + int32_t scriptCallArgCount = 1 + extraArgCount; + + RValue localBuf[GML_MAX_ARGUMENTS + 1]; + RValue* callArgs = (scriptCallArgCount <= (GML_MAX_ARGUMENTS + 1)) + ? localBuf + : (RValue*) safeMalloc((size_t) scriptCallArgCount * sizeof(RValue)); + + for (int32_t i = 0; i < extraArgCount; i++) + callArgs[1 + i] = args[2 + i]; + + RValue result; + +#ifndef __3DS__ + callArgs[0] = args[0]; + builtinScriptExecute(ctx, callArgs, scriptCallArgCount); + callArgs[0] = args[1]; + result = builtinScriptExecute(ctx, callArgs, scriptCallArgCount); +#else + if (runner->osType != OS_3DS) { + callArgs[0] = args[0]; + builtinScriptExecute(ctx, callArgs, scriptCallArgCount); + callArgs[0] = args[1]; + result = builtinScriptExecute(ctx, callArgs, scriptCallArgCount); + if (callArgs != localBuf) free(callArgs); + return result; + } + if (!battleDraw_is3DSBattleActive(ctx, runner)) { + if (callArgs != localBuf) free(callArgs); + return RValue_makeUndefined(); + } + if (g_n3dsDisableBottomScreenOverrides) { + if (callArgs != localBuf) free(callArgs); return RValue_makeUndefined(); } - ctx->runner->renderer->vtable->gpuSetColorWriteEnable(ctx->runner->renderer, r, g, b, a); +#ifdef N3DS_DISABLE_BOTTOM_SCREEN + if (callArgs != localBuf) free(callArgs); return RValue_makeUndefined(); -} - -// ===[ REGISTRATION ]=== - -void VMBuiltins_registerAll(VMContext* ctx) { - requireMessage(!ctx->registeredBuiltinFunctions, "Attempting to register all VMBuiltins, but it was already registered!"); - ctx->registeredBuiltinFunctions = true; - - const bool isGMS2 = DataWin_isVersionAtLeast(ctx->dataWin, 2, 0, 0, 0); - - // Core output - VM_registerBuiltin(ctx, "show_debug_message", builtinShowDebugMessage); - - // String functions - VM_registerBuiltin(ctx, "string_length", builtinStringLength); - VM_registerBuiltin(ctx, "string_byte_length", builtinStringByteLength); - VM_registerBuiltin(ctx, "string", builtinString); - VM_registerBuiltin(ctx, "string_upper", builtinStringUpper); - VM_registerBuiltin(ctx, "string_lower", builtinStringLower); - VM_registerBuiltin(ctx, "string_copy", builtinStringCopy); - VM_registerBuiltin(ctx, "string_pos", builtinStringPos); - VM_registerBuiltin(ctx, "string_char_at", builtinStringCharAt); - VM_registerBuiltin(ctx, "string_delete", builtinStringDelete); - VM_registerBuiltin(ctx, "string_insert", builtinStringInsert); - VM_registerBuiltin(ctx, "string_replace", builtinStringReplace); - VM_registerBuiltin(ctx, "string_replace_all", builtinStringReplaceAll); - VM_registerBuiltin(ctx, "string_repeat", builtinStringRepeat); - VM_registerBuiltin(ctx, "string_format", builtinStringFormat); - VM_registerBuiltin(ctx, "string_count", builtinStringCount); - VM_registerBuiltin(ctx, "string_digits", builtinStringDigits); - VM_registerBuiltin(ctx, "ord", builtinOrd); - VM_registerBuiltin(ctx, "chr", builtinChr); - - // Type functions - VM_registerBuiltin(ctx, "real", builtinReal); - VM_registerBuiltin(ctx, "is_string", builtinIsString); - VM_registerBuiltin(ctx, "is_real", builtinIsReal); - VM_registerBuiltin(ctx, "is_undefined", builtinIsUndefined); - - // Math functions - VM_registerBuiltin(ctx, "floor", builtinFloor); - VM_registerBuiltin(ctx, "ceil", builtinCeil); - VM_registerBuiltin(ctx, "round", builtinRound); - VM_registerBuiltin(ctx, "abs", builtinAbs); - VM_registerBuiltin(ctx, "sign", builtinSign); - VM_registerBuiltin(ctx, "max", builtinMax); - VM_registerBuiltin(ctx, "min", builtinMin); - VM_registerBuiltin(ctx, "power", builtinPower); - VM_registerBuiltin(ctx, "sqrt", builtinSqrt); - VM_registerBuiltin(ctx, "sqr", builtinSqr); - VM_registerBuiltin(ctx, "sin", builtinSin); - VM_registerBuiltin(ctx, "arcsin", builtinArcsin); - VM_registerBuiltin(ctx, "cos", builtinCos); - VM_registerBuiltin(ctx, "dsin", builtinDsin); - VM_registerBuiltin(ctx, "dcos", builtinDcos); - VM_registerBuiltin(ctx, "darctan2", builtinDarctan2); - VM_registerBuiltin(ctx, "degtorad", builtinDegtorad); - VM_registerBuiltin(ctx, "radtodeg", builtinRadtodeg); - VM_registerBuiltin(ctx, "clamp", builtinClamp); - VM_registerBuiltin(ctx, "lerp", builtinLerp); - VM_registerBuiltin(ctx, "point_distance", builtinPointDistance); - VM_registerBuiltin(ctx, "point_in_rectangle", builtinPointInRectangle); - VM_registerBuiltin(ctx, "point_direction", builtinPointDirection); - VM_registerBuiltin(ctx, "angle_difference", builtinAngleDifference); - VM_registerBuiltin(ctx, "distance_to_point", builtinDistanceToPoint); - VM_registerBuiltin(ctx, "distance_to_object", builtinDistanceToObject); - VM_registerBuiltin(ctx, "move_towards_point", builtinMoveTowardsPoint); - VM_registerBuiltin(ctx, "action_move_point", builtinMoveTowardsPoint); - VM_registerBuiltin(ctx, "move_snap", builtinMoveSnap); - VM_registerBuiltin(ctx, "lengthdir_x", builtinLengthdir_x); - VM_registerBuiltin(ctx, "lengthdir_y", builtinLengthdir_y); - - // Random - VM_registerBuiltin(ctx, "random", builtinRandom); - VM_registerBuiltin(ctx, "random_range", builtinRandomRange); - VM_registerBuiltin(ctx, "irandom", builtinIrandom); - VM_registerBuiltin(ctx, "irandom_range", builtinIrandomRange); - VM_registerBuiltin(ctx, "choose", builtinChoose); - VM_registerBuiltin(ctx, "randomize", builtinRandomize); - - // Room - VM_registerBuiltin(ctx, "game_get_speed", builtinGameGetSpeed); - VM_registerBuiltin(ctx, "room_exists", builtinRoomExists); - VM_registerBuiltin(ctx, "room_get_name", builtinRoomGetName); - VM_registerBuiltin(ctx, "room_goto_next", builtinRoomGotoNext); - VM_registerBuiltin(ctx, "room_goto_previous", builtinRoomGotoPrevious); - VM_registerBuiltin(ctx, "room_goto", builtinRoomGoto); - VM_registerBuiltin(ctx, "room_restart", builtinRoomRestart); - VM_registerBuiltin(ctx, "room_next", builtinRoomNext); - VM_registerBuiltin(ctx, "room_previous", builtinRoomPrevious); - VM_registerBuiltin(ctx, "room_set_persistent", builtinRoomSetPersistent); - - // GMS2 camera compatibility - VM_registerBuiltin(ctx, "view_get_camera", builtinViewGetCamera); - VM_registerBuiltin(ctx, "camera_get_view_x", builtinCameraGetViewX); - VM_registerBuiltin(ctx, "camera_get_view_y", builtinCameraGetViewY); - VM_registerBuiltin(ctx, "camera_get_view_width", builtinCameraGetViewWidth); - VM_registerBuiltin(ctx, "camera_get_view_height", builtinCameraGetViewHeight); - VM_registerBuiltin(ctx, "camera_set_view_pos", builtinCameraSetViewPos); - VM_registerBuiltin(ctx, "camera_get_view_target", builtinCameraGetViewTarget); - VM_registerBuiltin(ctx, "camera_set_view_target", builtinCameraSetViewTarget); - VM_registerBuiltin(ctx, "camera_get_view_border_x", builtinCameraGetViewBorderX); - VM_registerBuiltin(ctx, "camera_get_view_border_y", builtinCameraGetViewBorderY); - VM_registerBuiltin(ctx, "camera_set_view_border", builtinCameraSetViewBorder); - - // Variables - VM_registerBuiltin(ctx, "variable_global_exists", builtinVariableGlobalExists); - VM_registerBuiltin(ctx, "variable_global_get", builtinVariableGlobalGet); - VM_registerBuiltin(ctx, "variable_global_set", builtinVariableGlobalSet); - VM_registerBuiltin(ctx, "variable_instance_set", builtinVariableInstanceSet); - VM_registerBuiltin(ctx, "variable_instance_get", builtinVariableInstanceGet); - VM_registerBuiltin(ctx, "variable_instance_exists", builtinVariableInstanceExists); - VM_registerBuiltin(ctx, "variable_struct_set", builtinVariableStructSet); - VM_registerBuiltin(ctx, "variable_struct_get", builtinVariableStructGet); - VM_registerBuiltin(ctx, "variable_struct_exists", builtinVariableStructExists); - - // Script - VM_registerBuiltin(ctx, "script_execute", builtinScriptExecute); -#if IS_BC17_OR_HIGHER_ENABLED - VM_registerBuiltin(ctx, "method", builtinMethod); -#endif - - // OS - VM_registerBuiltin(ctx, "os_get_language", builtinOsGetLanguage); - VM_registerBuiltin(ctx, "os_get_region", builtinOsGetRegion); - - // ds_map - VM_registerBuiltin(ctx, "ds_map_create", builtinDsMapCreate); - VM_registerBuiltin(ctx, "ds_map_add", builtinDsMapAdd); - VM_registerBuiltin(ctx, "ds_map_set", builtinDsMapSet); - VM_registerBuiltin(ctx, "ds_map_replace", builtinDsMapReplace); - VM_registerBuiltin(ctx, "ds_map_find_value", builtinDsMapFindValue); - VM_registerBuiltin(ctx, "ds_map_exists", builtinDsMapExists); - VM_registerBuiltin(ctx, "ds_map_find_first", builtinDsMapFindFirst); - VM_registerBuiltin(ctx, "ds_map_find_next", builtinDsMapFindNext); - VM_registerBuiltin(ctx, "ds_map_size", builtinDsMapSize); - VM_registerBuiltin(ctx, "ds_map_destroy", builtinDsMapDestroy); - - // ds_list stubs - VM_registerBuiltin(ctx, "ds_list_create", builtinDsListCreate); - VM_registerBuiltin(ctx, "ds_list_destroy", builtinDsListDestroy); - VM_registerBuiltin(ctx, "ds_list_add", builtinDsListAdd); - VM_registerBuiltin(ctx, "ds_list_size", builtinDsListSize); - VM_registerBuiltin(ctx, "ds_list_find_index", builtinDsListFindIndex); - VM_registerBuiltin(ctx, "ds_list_find_value", builtinDsListFindValue); - - // Array - VM_registerBuiltin(ctx, "array_length_1d", builtinArrayLength1d); - // GM:S 2 alias for array_length_1d - VM_registerBuiltin(ctx, "array_length", builtinArrayLength1d); - VM_registerBuiltin(ctx, "array_push", builtinArrayPush); - VM_registerBuiltin(ctx, "array_resize", builtinArrayResize); - VM_registerBuiltin(ctx, "array_delete", builtinArrayDelete); - VM_registerBuiltin(ctx, "array_insert", builtinArrayInsert); - VM_registerBuiltin(ctx, "array_create", builtinArrayCreate); - - // Steam stubs - VM_registerBuiltin(ctx, "steam_initialised", builtin_steam_initialised); - VM_registerBuiltin(ctx, "steam_stats_ready", builtin_steam_stats_ready); - VM_registerBuiltin(ctx, "steam_file_exists", builtin_steam_file_exists); - VM_registerBuiltin(ctx, "steam_file_write", builtin_steam_file_write); - VM_registerBuiltin(ctx, "steam_file_read", builtin_steam_file_read); - VM_registerBuiltin(ctx, "steam_get_persona_name", builtin_steam_get_persona_name); - - // Audio - VM_registerBuiltin(ctx, "audio_channel_num", builtin_audioChannelNum); - VM_registerBuiltin(ctx, "audio_play_sound", builtin_audioPlaySound); - VM_registerBuiltin(ctx, "audio_stop_sound", builtin_audioStopSound); - VM_registerBuiltin(ctx, "audio_stop_all", builtin_audioStopAll); - VM_registerBuiltin(ctx, "audio_is_playing", builtin_audioIsPlaying); - VM_registerBuiltin(ctx, "audio_is_paused", builtin_audioIsPaused); - VM_registerBuiltin(ctx, "audio_sound_length", builtin_audioSoundLength); - VM_registerBuiltin(ctx, "audio_sound_gain", builtin_audioSoundGain); - VM_registerBuiltin(ctx, "audio_sound_pitch", builtin_audioSoundPitch); - VM_registerBuiltin(ctx, "audio_sound_get_gain", builtin_audioSoundGetGain); - VM_registerBuiltin(ctx, "audio_sound_get_pitch", builtin_audioSoundGetPitch); - VM_registerBuiltin(ctx, "audio_master_gain", builtin_audioMasterGain); - VM_registerBuiltin(ctx, "audio_group_load", builtin_audioGroupLoad); - VM_registerBuiltin(ctx, "audio_group_is_loaded", builtin_audioGroupIsLoaded); - VM_registerBuiltin(ctx, "audio_play_music", builtin_audioPlayMusic); - VM_registerBuiltin(ctx, "audio_stop_music", builtin_audioStopMusic); - VM_registerBuiltin(ctx, "audio_music_gain", builtin_audioMusicGain); - VM_registerBuiltin(ctx, "audio_music_is_playing", builtin_audioMusicIsPlaying); - VM_registerBuiltin(ctx, "audio_pause_sound", builtin_audioPauseSound); - VM_registerBuiltin(ctx, "audio_resume_sound", builtin_audioResumeSound); - VM_registerBuiltin(ctx, "audio_pause_all", builtin_audioPauseAll); - VM_registerBuiltin(ctx, "audio_resume_all", builtin_audioResumeAll); - VM_registerBuiltin(ctx, "audio_sound_get_track_position", builtin_audioSoundGetTrackPosition); - VM_registerBuiltin(ctx, "audio_sound_set_track_position", builtin_audioSoundSetTrackPosition); - VM_registerBuiltin(ctx, "audio_create_stream", builtin_audioCreateStream); - VM_registerBuiltin(ctx, "audio_destroy_stream", builtin_audioDestroyStream); - - // Application surface - VM_registerBuiltin(ctx, "application_surface_enable", builtin_application_surface_enable); - VM_registerBuiltin(ctx, "application_surface_draw_enable", builtin_application_surface_draw_enable); - - // Gamepad - VM_registerBuiltin(ctx, "gamepad_get_device_count", builtinGamepadGetDeviceCount); - VM_registerBuiltin(ctx, "gamepad_is_connected", builtinGamepadIsConnected); - VM_registerBuiltin(ctx, "gamepad_button_check", builtinGamepadButtonCheck); - VM_registerBuiltin(ctx, "gamepad_button_check_pressed", builtinGamepadButtonCheckPressed); - VM_registerBuiltin(ctx, "gamepad_button_check_released", builtinGamepadButtonCheckReleased); - VM_registerBuiltin(ctx, "gamepad_axis_value", builtinGamepadAxisValue); - VM_registerBuiltin(ctx, "gamepad_get_description", builtinGamepadGetDescription); - VM_registerBuiltin(ctx, "gamepad_button_value", builtinGamepadButtonValue); - VM_registerBuiltin(ctx, "gamepad_is_supported", builtinGamepadIsSupported); - VM_registerBuiltin(ctx, "gamepad_get_guid", builtinGamepadGetGuid); - VM_registerBuiltin(ctx, "gamepad_get_button_threshold", builtinGamepadGetButtonThreshold); - VM_registerBuiltin(ctx, "gamepad_set_button_threshold", builtinGamepadSetButtonThreshold); - VM_registerBuiltin(ctx, "gamepad_get_axis_deadzone", builtinGamepadGetAxisDeadzone); - VM_registerBuiltin(ctx, "gamepad_set_axis_deadzone", builtinGamepadSetAxisDeadzone); - VM_registerBuiltin(ctx, "gamepad_axis_count", builtinGamepadAxisCount); - VM_registerBuiltin(ctx, "gamepad_button_count", builtinGamepadButtonCount); - VM_registerBuiltin(ctx, "gamepad_hat_count", builtinGamepadHatCount); - VM_registerBuiltin(ctx, "gamepad_hat_value", builtinGamepadHatValue); - - // INI - VM_registerBuiltin(ctx, "ini_open", builtinIniOpen); - VM_registerBuiltin(ctx, "ini_close", builtinIniClose); - VM_registerBuiltin(ctx, "ini_write_real", builtinIniWriteReal); - VM_registerBuiltin(ctx, "ini_write_string", builtinIniWriteString); - VM_registerBuiltin(ctx, "ini_read_string", builtinIniReadString); - VM_registerBuiltin(ctx, "ini_read_real", builtinIniReadReal); - VM_registerBuiltin(ctx, "ini_section_exists", builtinIniSectionExists); - - // File - VM_registerBuiltin(ctx, "file_exists", builtinFileExists); - VM_registerBuiltin(ctx, "file_text_open_write", builtinFileTextOpenWrite); - VM_registerBuiltin(ctx, "file_text_open_read", builtinFileTextOpenRead); - VM_registerBuiltin(ctx, "file_text_close", builtinFileTextClose); - VM_registerBuiltin(ctx, "file_text_write_string", builtinFileTextWriteString); - VM_registerBuiltin(ctx, "file_text_writeln", builtinFileTextWriteln); - VM_registerBuiltin(ctx, "file_text_write_real", builtinFileTextWriteReal); - VM_registerBuiltin(ctx, "file_text_eof", builtinFileTextEof); - VM_registerBuiltin(ctx, "file_delete", builtinFileDelete); - VM_registerBuiltin(ctx, "file_text_read_string", builtinFileTextReadString); - VM_registerBuiltin(ctx, "file_text_read_real", builtinFileTextReadReal); - VM_registerBuiltin(ctx, "file_text_readln", builtinFileTextReadln); - - // Keyboard - VM_registerBuiltin(ctx, "keyboard_check", builtinKeyboardCheck); - VM_registerBuiltin(ctx, "keyboard_check_pressed", builtinKeyboardCheckPressed); - VM_registerBuiltin(ctx, "keyboard_check_released", builtinKeyboardCheckReleased); - VM_registerBuiltin(ctx, "keyboard_check_direct", builtinKeyboardCheckDirect); - VM_registerBuiltin(ctx, "keyboard_key_press", builtinKeyboardKeyPress); - VM_registerBuiltin(ctx, "keyboard_key_release", builtinKeyboardKeyRelease); - VM_registerBuiltin(ctx, "keyboard_clear", builtinKeyboardClear); - - // Joystick - VM_registerBuiltin(ctx, "joystick_exists", builtinJoystickExists); - VM_registerBuiltin(ctx, "joystick_name", builtinJoystickName); - VM_registerBuiltin(ctx, "joystick_axes", builtinJoystickAxes); - VM_registerBuiltin(ctx, "joystick_xpos", builtinJoystickXpos); - VM_registerBuiltin(ctx, "joystick_ypos", builtinJoystickYpos); - VM_registerBuiltin(ctx, "joystick_direction", builtinJoystickDirection); - VM_registerBuiltin(ctx, "joystick_pov", builtinJoystickPov); - VM_registerBuiltin(ctx, "joystick_check_button", builtinJoystickCheckButton); - VM_registerBuiltin(ctx, "joystick_has_pov", builtinJoystickHasPov); - VM_registerBuiltin(ctx, "joystick_buttons", builtinJoystickButtons); - - // Window - VM_registerBuiltin(ctx, "window_get_fullscreen", builtin_window_get_fullscreen); - VM_registerBuiltin(ctx, "window_set_fullscreen", builtin_window_set_fullscreen); - VM_registerBuiltin(ctx, "window_set_caption", builtinWindowSetCaption); - VM_registerBuiltin(ctx, "window_set_size", builtin_window_set_size); - VM_registerBuiltin(ctx, "window_center", builtin_window_center); - VM_registerBuiltin(ctx, "window_get_width", builtinWindowGetWidth); - VM_registerBuiltin(ctx, "window_get_height", builtinWindowGetHeight); - VM_registerBuiltin(ctx, "window_has_focus", builtinWindowHasFocus); - - // Game - VM_registerBuiltin(ctx, "game_restart", builtinGameRestart); - VM_registerBuiltin(ctx, "game_end", builtinGameEnd); - VM_registerBuiltin(ctx, "game_save", builtin_game_save); - VM_registerBuiltin(ctx, "game_load", builtin_game_load); - - // Instance - VM_registerBuiltin(ctx, "instance_exists", builtinInstanceExists); - VM_registerBuiltin(ctx, "instance_number", builtinInstanceNumber); - VM_registerBuiltin(ctx, "instance_find", builtinInstanceFind); - VM_registerBuiltin(ctx, "instance_nearest", builtinInstanceNearest); - VM_registerBuiltin(ctx, "instance_destroy", builtinInstanceDestroy); - if(!isGMS2) { - VM_registerBuiltin(ctx, "instance_create", builtinInstanceCreate); - } - else { - VM_registerBuiltin(ctx, "instance_create_depth", builtinInstanceCreateDepth); - VM_registerBuiltin(ctx, "instance_create_layer", builtinInstanceCreateLayer); - } - VM_registerBuiltin(ctx, "instance_copy", builtinInstanceCopy); - VM_registerBuiltin(ctx, "instance_change", builtinInstanceChange); - VM_registerBuiltin(ctx, "instance_deactivate_all", builtinInstanceDeactivateAll); - VM_registerBuiltin(ctx, "instance_activate_all", builtinInstanceActivateAll); - VM_registerBuiltin(ctx, "instance_activate_object", builtinInstanceActivateObject); - VM_registerBuiltin(ctx, "instance_deactivate_object", builtinInstanceDeactivateObject); - VM_registerBuiltin(ctx, "instance_activate_layer", builtinInstanceActivateLayer); - VM_registerBuiltin(ctx, "instance_deactivate_layer", builtinInstanceDeactivateLayer); - VM_registerBuiltin(ctx, "action_kill_object", builtinActionKillObject); - VM_registerBuiltin(ctx, "action_create_object", builtinActionCreateObject); - VM_registerBuiltin(ctx, "action_set_relative", builtinActionSetRelative); - VM_registerBuiltin(ctx, "action_move", builtinActionMove); - VM_registerBuiltin(ctx, "action_move_to", builtinActionMoveTo); - VM_registerBuiltin(ctx, "action_snap", builtinActionSnap); - VM_registerBuiltin(ctx, "action_set_friction", builtinActionSetFriction); - VM_registerBuiltin(ctx, "action_set_gravity", builtinActionSetGravity); - VM_registerBuiltin(ctx, "action_set_hspeed", builtinActionSetHspeed); - VM_registerBuiltin(ctx, "action_set_vspeed", builtinActionSetVspeed); - VM_registerBuiltin(ctx, "event_inherited", builtinEventInherited); - VM_registerBuiltin(ctx, "action_inherited", builtinEventInherited); - VM_registerBuiltin(ctx, "event_user", builtinEventUser); - VM_registerBuiltin(ctx, "event_perform", builtinEventPerform); - - // Buffer - VM_registerBuiltin(ctx, "buffer_create", builtin_bufferCreate); - VM_registerBuiltin(ctx, "buffer_delete", builtin_bufferDelete); - VM_registerBuiltin(ctx, "buffer_write", builtin_bufferWrite); - VM_registerBuiltin(ctx, "buffer_read", builtin_bufferRead); - VM_registerBuiltin(ctx, "buffer_seek", builtin_bufferSeek); - VM_registerBuiltin(ctx, "buffer_tell", builtin_bufferTell); - VM_registerBuiltin(ctx, "buffer_get_size", builtin_bufferGetSize); - VM_registerBuiltin(ctx, "buffer_load", builtin_bufferLoad); - VM_registerBuiltin(ctx, "buffer_save", builtin_bufferSave); - VM_registerBuiltin(ctx, "buffer_base64_encode", builtin_buffer_base64_encode); - - // PSN - VM_registerBuiltin(ctx, "psn_init", builtin_psn_init); - VM_registerBuiltin(ctx, "psn_default_user", builtin_psn_default_user); - VM_registerBuiltin(ctx, "psn_get_leaderboard_score", builtin_psn_get_leaderboard_score); - - // Draw - VM_registerBuiltin(ctx, "draw_sprite", builtin_drawSprite); - VM_registerBuiltin(ctx, "draw_sprite_ext", builtin_drawSpriteExt); - VM_registerBuiltin(ctx, "draw_sprite_tiled", builtin_drawSpriteTiled); - VM_registerBuiltin(ctx, "draw_sprite_tiled_ext", builtin_drawSpriteTiledExt); - VM_registerBuiltin(ctx, "draw_sprite_stretched", builtin_drawSpriteStretched); - VM_registerBuiltin(ctx, "draw_sprite_stretched_ext", builtin_drawSpriteStretchedExt); - VM_registerBuiltin(ctx, "draw_sprite_part", builtin_drawSpritePart); - VM_registerBuiltin(ctx, "draw_sprite_part_ext", builtin_drawSpritePartExt); - VM_registerBuiltin(ctx, "draw_sprite_general", builtin_drawSpriteGeneral); - VM_registerBuiltin(ctx, "draw_sprite_pos", builtin_drawSpritePos); - VM_registerBuiltin(ctx, "draw_rectangle", builtin_drawRectangle); - VM_registerBuiltin(ctx, "draw_rectangle_color", builtin_drawRectangleColor); - VM_registerBuiltin(ctx, "draw_rectangle_colour", builtin_drawRectangleColor); - VM_registerBuiltin(ctx, "draw_healthbar", builtin_drawHealthbar); - VM_registerBuiltin(ctx, "draw_set_color", builtin_drawSetColor); - VM_registerBuiltin(ctx, "draw_set_alpha", builtin_drawSetAlpha); - VM_registerBuiltin(ctx, "draw_set_font", builtin_drawSetFont); - VM_registerBuiltin(ctx, "draw_set_halign", builtin_drawSetHalign); - VM_registerBuiltin(ctx, "draw_set_valign", builtin_drawSetValign); - VM_registerBuiltin(ctx, "draw_text", builtin_drawText); - VM_registerBuiltin(ctx, "draw_text_transformed", builtin_drawTextTransformed); - VM_registerBuiltin(ctx, "draw_text_ext", builtin_drawTextExt); - VM_registerBuiltin(ctx, "draw_text_ext_transformed", builtin_drawTextExtTransformed); - VM_registerBuiltin(ctx, "draw_text_color", builtin_drawTextColor); - VM_registerBuiltin(ctx, "draw_text_color_transformed", builtin_drawTextColorTransformed); - VM_registerBuiltin(ctx, "draw_text_color_ext", builtin_drawTextColorExt); - VM_registerBuiltin(ctx, "draw_text_color_ext_transformed", builtin_drawTextColorExtTransformed); - VM_registerBuiltin(ctx, "draw_text_colour", builtin_drawTextColor); - VM_registerBuiltin(ctx, "draw_text_colour_transformed", builtin_drawTextColorTransformed); - VM_registerBuiltin(ctx, "draw_text_colour_ext", builtin_drawTextColorExt); - VM_registerBuiltin(ctx, "draw_text_colour_ext_transformed", builtin_drawTextColorExtTransformed); - VM_registerBuiltin(ctx, "draw_surface", builtin_draw_surface); - VM_registerBuiltin(ctx, "draw_surface_ext", builtin_draw_surface_ext); - if(!isGMS2) { - VM_registerBuiltin(ctx, "draw_background", builtin_drawBackground); - VM_registerBuiltin(ctx, "draw_background_ext", builtin_drawBackgroundExt); - VM_registerBuiltin(ctx, "draw_background_stretched", builtin_drawBackgroundStretched); - VM_registerBuiltin(ctx, "draw_background_part_ext", builtin_drawBackgroundPartExt); - VM_registerBuiltin(ctx, "background_get_width", builtinBackgroundGetWidth); - VM_registerBuiltin(ctx, "background_get_height", builtinBackgroundGetHeight); - } - VM_registerBuiltin(ctx, "draw_self", builtin_draw_self); - VM_registerBuiltin(ctx, "draw_line", builtin_draw_line); - VM_registerBuiltin(ctx, "draw_line_width", builtin_draw_line_width); - VM_registerBuiltin(ctx, "draw_line_width_colour", builtin_draw_line_width_colour); - VM_registerBuiltin(ctx, "draw_line_width_color", builtin_draw_line_width_colour); - VM_registerBuiltin(ctx, "draw_triangle", builtin_draw_triangle); - VM_registerBuiltin(ctx, "draw_set_colour", builtin_draw_set_colour); - VM_registerBuiltin(ctx, "draw_get_colour", builtin_draw_get_colour); - VM_registerBuiltin(ctx, "draw_get_color", builtin_draw_get_color); - VM_registerBuiltin(ctx, "draw_get_alpha", builtin_draw_get_alpha); - - // Color - VM_registerBuiltin(ctx, "merge_color", builtinMergeColor); - VM_registerBuiltin(ctx, "merge_colour", builtinMergeColor); - - // Surface - VM_registerBuiltin(ctx, "surface_create", builtin_surface_create); - VM_registerBuiltin(ctx, "surface_free", builtin_surface_free); - VM_registerBuiltin(ctx, "surface_set_target", builtin_surface_set_target); - VM_registerBuiltin(ctx, "surface_reset_target", builtin_surface_reset_target); - VM_registerBuiltin(ctx, "surface_exists", builtin_surface_exists); - VM_registerBuiltin(ctx, "surface_get_width", builtinSurfaceGetWidth); - VM_registerBuiltin(ctx, "surface_get_height", builtinSurfaceGetHeight); - - // Sprite info - VM_registerBuiltin(ctx, "sprite_add", builtin_spriteAdd); - VM_registerBuiltin(ctx, "sprite_exists", builtin_spriteExists); - VM_registerBuiltin(ctx, "sprite_get_width", builtin_spriteGetWidth); - VM_registerBuiltin(ctx, "sprite_get_height", builtin_spriteGetHeight); - VM_registerBuiltin(ctx, "sprite_get_number", builtin_spriteGetNumber); - VM_registerBuiltin(ctx, "sprite_get_xoffset", builtin_spriteGetXOffset); - VM_registerBuiltin(ctx, "sprite_get_yoffset", builtin_spriteGetYOffset); - VM_registerBuiltin(ctx, "sprite_get_name", builtin_spriteGetName); - VM_registerBuiltin(ctx, "sprite_set_offset", builtin_spriteSetOffset); - VM_registerBuiltin(ctx, "sprite_create_from_surface", builtin_spriteCreateFromSurface); - VM_registerBuiltin(ctx, "sprite_delete", builtin_spriteDelete); - - // Text measurement - VM_registerBuiltin(ctx, "string_width", builtin_stringWidth); - VM_registerBuiltin(ctx, "string_height", builtin_stringHeight); - VM_registerBuiltin(ctx, "string_width_ext", builtin_string_width_ext); - VM_registerBuiltin(ctx, "string_height_ext", builtin_string_height_ext); - - // Color - VM_registerBuiltin(ctx, "make_color_rgb", builtinMakeColor); - VM_registerBuiltin(ctx, "make_colour_rgb", builtinMakeColour); - VM_registerBuiltin(ctx, "make_color_hsv", builtinMakeColorHsv); - VM_registerBuiltin(ctx, "make_colour_hsv", builtinMakeColourHsv); - - // Display - VM_registerBuiltin(ctx, "display_get_width", builtin_display_get_width); - VM_registerBuiltin(ctx, "display_get_height", builtin_display_get_height); - VM_registerBuiltin(ctx, "display_get_gui_width", builtinDisplayGetGuiWidth); - VM_registerBuiltin(ctx, "display_get_gui_height", builtinDisplayGetGuiHeight); - VM_registerBuiltin(ctx, "display_set_gui_size", builtinDisplaySetGuiSize); - VM_registerBuiltin(ctx, "display_set_gui_maximise", builtinDisplaySetGuiMaximise); - VM_registerBuiltin(ctx, "display_set_gui_maximize", builtinDisplaySetGuiMaximise); - - // Collision - VM_registerBuiltin(ctx, "place_meeting", builtinPlaceMeeting); - VM_registerBuiltin(ctx, "collision_rectangle", builtinCollisionRectangle); - VM_registerBuiltin(ctx, "collision_rectangle_list", builtinCollisionRectangleList); - VM_registerBuiltin(ctx, "rectangle_in_rectangle", builtinRectangleInRectangle); - VM_registerBuiltin(ctx, "collision_line", builtinCollisionLine); - VM_registerBuiltin(ctx, "collision_point", builtinCollisionPoint); - VM_registerBuiltin(ctx, "collision_circle", builtinCollisionCircle); - VM_registerBuiltin(ctx, "instance_place", builtinInstancePlace); - VM_registerBuiltin(ctx, "instance_position", builtinInstancePosition); - VM_registerBuiltin(ctx, "position_meeting", builtinPositionMeeting); - VM_registerBuiltin(ctx, "place_free", builtinPlaceFree); - VM_registerBuiltin(ctx, "place_empty", builtinPlaceEmpty); - - // Motion planning - VM_registerBuiltin(ctx, "mp_linear_step", builtinMpLinearStep); - VM_registerBuiltin(ctx, "mp_linear_step_object", builtinMpLinearStepObject); - VM_registerBuiltin(ctx, "mp_potential_step", builtinMpPotentialStep); - VM_registerBuiltin(ctx, "mp_potential_step_object", builtinMpPotentialStepObject); - VM_registerBuiltin(ctx, "mp_potential_settings", builtinMpPotentialSettings); - - // Tile layers - VM_registerBuiltin(ctx, "tile_layer_hide", builtinTileLayerHide); - VM_registerBuiltin(ctx, "tile_layer_show", builtinTileLayerShow); - VM_registerBuiltin(ctx, "tile_layer_shift", builtinTileLayerShift); - - // Layer - VM_registerBuiltin(ctx, "layer_force_draw_depth", builtinLayerForceDrawDepth); - VM_registerBuiltin(ctx, "layer_is_draw_depth_forced", builtinLayerIsDrawDepthForced); - VM_registerBuiltin(ctx, "layer_get_forced_depth", builtinLayerGetForcedDepth); - VM_registerBuiltin(ctx, "layer_get_id", builtinLayerGetId); - VM_registerBuiltin(ctx, "layer_exists", builtinLayerExists); - VM_registerBuiltin(ctx, "layer_get_name", builtinLayerGetName); - VM_registerBuiltin(ctx, "layer_get_depth", builtinLayerGetDepth); - VM_registerBuiltin(ctx, "layer_depth", builtinLayerDepth); - VM_registerBuiltin(ctx, "layer_get_visible", builtinLayerGetVisible); - VM_registerBuiltin(ctx, "layer_set_visible", builtinLayerSetVisible); - VM_registerBuiltin(ctx, "layer_get_x", builtinLayerGetX); - VM_registerBuiltin(ctx, "layer_x", builtinLayerX); - VM_registerBuiltin(ctx, "layer_get_y", builtinLayerGetY); - VM_registerBuiltin(ctx, "layer_y", builtinLayerY); - VM_registerBuiltin(ctx, "layer_get_hspeed", builtinLayerGetHspeed); - VM_registerBuiltin(ctx, "layer_hspeed", builtinLayerHspeed); - VM_registerBuiltin(ctx, "layer_get_vspeed", builtinLayerGetVspeed); - VM_registerBuiltin(ctx, "layer_vspeed", builtinLayerVspeed); -#if IS_BC17_OR_HIGHER_ENABLED - VM_registerBuiltin(ctx, "layer_get_all", builtinLayerGetAll); - VM_registerBuiltin(ctx, "layer_get_all_elements", builtinLayerGetAllElements); #endif - VM_registerBuiltin(ctx, "layer_get_element_type", builtinLayerGetElementType); - VM_registerBuiltin(ctx, "layer_sprite_get_sprite", builtinLayerSpriteGetSprite); - VM_registerBuiltin(ctx, "layer_sprite_get_x", builtinLayerSpriteGetX); - VM_registerBuiltin(ctx, "layer_sprite_get_y", builtinLayerSpriteGetY); - VM_registerBuiltin(ctx, "layer_sprite_get_xscale", builtinLayerSpriteGetXScale); - VM_registerBuiltin(ctx, "layer_sprite_get_yscale", builtinLayerSpriteGetYScale); - VM_registerBuiltin(ctx, "layer_sprite_get_speed", builtinLayerSpriteGetSpeed); - VM_registerBuiltin(ctx, "layer_sprite_get_index", builtinLayerSpriteGetIndex); - VM_registerBuiltin(ctx, "layer_sprite_get_angle", builtinLayerSpriteGetAngle); - VM_registerBuiltin(ctx, "layer_sprite_destroy", builtinLayerSpriteDestroy); -#if IS_BC17_OR_HIGHER_ENABLED - VM_registerBuiltin(ctx, "layer_get_id_at_depth", builtinLayerGetIdAtDepth); -#endif - VM_registerBuiltin(ctx, "layer_create", builtinLayerCreate); - VM_registerBuiltin(ctx, "layer_destroy", builtinLayerDestroy); - VM_registerBuiltin(ctx, "layer_background_create", builtinLayerBackgroundCreate); - VM_registerBuiltin(ctx, "layer_background_exists", builtinLayerBackgroundExists); - VM_registerBuiltin(ctx, "layer_background_visible", builtinLayerBackgroundVisible); - VM_registerBuiltin(ctx, "layer_background_htiled", builtinLayerBackgroundHtiled); - VM_registerBuiltin(ctx, "layer_background_vtiled", builtinLayerBackgroundVtiled); - VM_registerBuiltin(ctx, "layer_background_xscale", builtinLayerBackgroundXscale); - VM_registerBuiltin(ctx, "layer_background_yscale", builtinLayerBackgroundYscale); - VM_registerBuiltin(ctx, "layer_background_stretch", builtinLayerBackgroundStretch); - VM_registerBuiltin(ctx, "layer_background_blend", builtinLayerBackgroundBlend); - VM_registerBuiltin(ctx, "layer_background_alpha", builtinLayerBackgroundAlpha); - - // GMS2 internal - VM_registerBuiltin(ctx, "@@NewGMLArray@@", builtinNewGMLArray); - VM_registerBuiltin(ctx, "@@This@@", builtinThis); - VM_registerBuiltin(ctx, "@@Other@@", builtinOther); -#if IS_BC17_OR_HIGHER_ENABLED - VM_registerBuiltin(ctx, "@@NullObject@@", builtinNullObject); - VM_registerBuiltin(ctx, "@@NewGMLObject@@", builtinNewGMLObject); -#endif - - // Path - VM_registerBuiltin(ctx, "path_start", builtinPathStart); - VM_registerBuiltin(ctx, "path_end", builtinPathEnd); - VM_registerBuiltin(ctx, "path_get_length", builtinPathGetLength); - VM_registerBuiltin(ctx, "path_add", builtinPathAdd); - VM_registerBuiltin(ctx, "path_clear_points", builtinPathClearPoints); - VM_registerBuiltin(ctx, "path_add_point", builtinPathAddPoint); - VM_registerBuiltin(ctx, "path_exists", builtinPathExists); - VM_registerBuiltin(ctx, "path_delete", builtinPathDelete); - - // Motion planning grid - VM_registerBuiltin(ctx, "mp_grid_create", builtinMpGridCreate); - VM_registerBuiltin(ctx, "mp_grid_destroy", builtinMpGridDestroy); - VM_registerBuiltin(ctx, "mp_grid_clear_all", builtinMpGridClearAll); - VM_registerBuiltin(ctx, "mp_grid_add_cell", builtinMpGridAddCell); - VM_registerBuiltin(ctx, "mp_grid_clear_cell", builtinMpGridClearCell); - VM_registerBuiltin(ctx, "mp_grid_add_rectangle", builtinMpGridAddRectangle); - VM_registerBuiltin(ctx, "mp_grid_clear_rectangle", builtinMpGridClearRectangle); - VM_registerBuiltin(ctx, "mp_grid_get_cell", builtinMpGridGetCell); - VM_registerBuiltin(ctx, "mp_grid_draw", builtinMpGridDraw); - VM_registerBuiltin(ctx, "mp_grid_path", builtinMpGridPath); - - // Misc - VM_registerBuiltin(ctx, "get_timer", builtin_get_timer); - VM_registerBuiltin(ctx, "action_if_variable", builtinActionIfVariable); - VM_registerBuiltin(ctx, "action_set_alarm", builtinActionSetAlarm); - VM_registerBuiltin(ctx, "alarm_set", builtinAlarmSet); - VM_registerBuiltin(ctx, "alarm_get", builtinAlarmGet); - VM_registerBuiltin(ctx, "action_sound",builtin_action_sound); - VM_registerBuiltin(ctx, "string_hash_to_newline", builtinStringHashToNewline); - VM_registerBuiltin(ctx, "json_decode", builtinJsonDecode); - VM_registerBuiltin(ctx, "font_add_sprite", builtinFontAddSprite); - VM_registerBuiltin(ctx, "font_add_sprite_ext", builtinFontAddSpriteExt); - VM_registerBuiltin(ctx, "font_get_name", builtinFontGetName); - VM_registerBuiltin(ctx, "object_get_sprite", builtinObjectGetSprite); - VM_registerBuiltin(ctx, "asset_get_index", builtinAssetGetIndex); - VM_registerBuiltin(ctx,"gpu_set_blendmode", builtinGpuSetBlendMode); - VM_registerBuiltin(ctx,"gpu_set_blendmode_ext", builtinGpuSetBlendModeExt); - VM_registerBuiltin(ctx,"gpu_set_blendenable", builtinGpuSetBlendEnable); - VM_registerBuiltin(ctx,"gpu_get_blendenable", builtinGpuSetBlendEnable); - VM_registerBuiltin(ctx,"gpu_set_alphatestenable", builtinGpuSetAlphaTestEnable); - VM_registerBuiltin(ctx,"gpu_set_alphatestref", builtinGpuSetAlphaTestRef); - VM_registerBuiltin(ctx,"gpu_set_colorwriteenable", builtinGpuSetColorWriteEnable); -} - + N3DSRenderer_beginBottomScreenGUI(runner->renderer, guiW, guiH); + callArgs[0] = args[0]; + builtinScriptExecute(ctx, callArgs, scriptCallArgCount); + callArgs[0] = args[1]; + result = builtinScriptExecute(ctx, callArgs, scriptCallArgCount); + N3DSRenderer_endBottomScreenGUI(runner->renderer); +#endif + + if (callArgs != localBuf) free(callArgs); + return result; +} + +// ===[ OS FUNCTIONS ]=== + +static RValue builtinOsGetLanguage(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + return RValue_makeOwnedString(safeStrdup("en")); +} + +static RValue builtinOsGetRegion(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + return RValue_makeOwnedString(safeStrdup("US")); +} + +// ===[ DS_MAP BUILTIN FUNCTIONS ]=== + +static inline ptrdiff_t getValueIndexInMap(DsMapEntry** mapPtr, RValue keyRvalue) { + ptrdiff_t idx; + if (keyRvalue.type == RVALUE_STRING && keyRvalue.string != nullptr) { + // Fast path: No need to convert the RValue to a string if it is already a string + idx = shgeti(*mapPtr, keyRvalue.string); + } else { + char* key = RValue_toString(keyRvalue); + idx = shgeti(*mapPtr, key); + free(key); + } + + return idx; +} + +static RValue builtinDsMapCreate(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + return RValue_makeReal((GMLReal) dsMapCreate(runner)); +} + +static RValue builtinDsMapAdd(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsMapEntry** mapPtr = dsMapGet(runner, id); + if (mapPtr == nullptr) return RValue_makeUndefined(); + + char* key = RValue_toString(args[1]); + + // Only add if key doesn't exist + bool exists = shgeti(*mapPtr, key) != -1; + + if (exists) { + free(key); // Key already exists, we didn't insert it + } else { + shput(*mapPtr, key, RValue_makeIndependent(args[2])); + } + + return RValue_makeUndefined(); +} + +static RValue builtinDsMapSet(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsMapEntry** mapPtr = dsMapGet(runner, id); + if (mapPtr == nullptr) return RValue_makeUndefined(); + + char* key = RValue_toString(args[1]); + + ptrdiff_t existingKeyIndex = shgeti(*mapPtr, key); + + if (existingKeyIndex != -1) { + // If it already exists, we'll get the current value and free it + RValue_free(&(*mapPtr)[existingKeyIndex].value); + } + + shput(*mapPtr, key, RValue_makeIndependent(args[2])); + + if (existingKeyIndex != -1) { + // If it already existed, then shput still owns the old key + // So we'll need to free the created key + free(key); + } + + return RValue_makeUndefined(); +} + +static RValue builtinDsMapReplace(VMContext* ctx, RValue* args, int32_t argCount) { + // ds_map_replace is the same as ds_map_set in GMS 1.4 + return builtinDsMapSet(ctx, args, argCount); +} + +static RValue builtinDsMapFindValue(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsMapEntry** mapPtr = dsMapGet(runner, id); + if (mapPtr == nullptr) return RValue_makeUndefined(); + + ptrdiff_t idx = getValueIndexInMap(mapPtr, args[1]); + + if (0 > idx) return RValue_makeUndefined(); + RValue val = (*mapPtr)[idx].value; + if (val.type == RVALUE_STRING && val.string != nullptr) { + return RValue_makeOwnedString(safeStrdup(val.string)); + } + // Return a weak view: the map retains ownership. The caller's Pop will incRef into the destination slot. + val.ownsReference = false; + return val; +} + +static RValue builtinDsMapExists(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsMapEntry** mapPtr = dsMapGet(runner, id); + if (mapPtr == nullptr) return RValue_makeReal(0.0); + + ptrdiff_t idx = getValueIndexInMap(mapPtr, args[1]); + + return RValue_makeReal(idx >= 0 ? 1.0 : 0.0); +} + +static RValue builtinDsMapFindFirst(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsMapEntry** mapPtr = dsMapGet(runner, id); + if (mapPtr == nullptr || shlen(*mapPtr) == 0) return RValue_makeUndefined(); + return RValue_makeOwnedString(safeStrdup((*mapPtr)[0].key)); +} + +static RValue builtinDsMapFindNext(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsMapEntry** mapPtr = dsMapGet(runner, id); + if (mapPtr == nullptr) return RValue_makeUndefined(); + + ptrdiff_t idx = getValueIndexInMap(mapPtr, args[1]); + if (0 > idx || idx + 1 >= shlen(*mapPtr)) return RValue_makeUndefined(); + return RValue_makeOwnedString(safeStrdup((*mapPtr)[idx + 1].key)); +} + +static RValue builtinDsMapSize(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsMapEntry** mapPtr = dsMapGet(runner, id); + if (mapPtr == nullptr) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) shlen(*mapPtr)); +} + +static RValue builtinDsMapDestroy(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsMapEntry** mapPtr = dsMapGet(runner, id); + if (mapPtr == nullptr) return RValue_makeUndefined(); + // Free all keys and values + for (ptrdiff_t i = 0; shlen(*mapPtr) > i; i++) { + free((*mapPtr)[i].key); + RValue_free(&(*mapPtr)[i].value); + } + shfree(*mapPtr); + *mapPtr = nullptr; + return RValue_makeUndefined(); +} + +// ===[ DS_LIST FUNCTIONS ]=== + +static RValue builtinDsListCreate(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + return RValue_makeReal((GMLReal) dsListCreate(runner)); +} + +static RValue builtinDsListAdd(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsList* list = dsListGet(runner, id); + if (list == nullptr) return RValue_makeUndefined(); + // ds_list_add can take multiple values after the list id + repeat(argCount - 1, i) { + arrput(list->items, RValue_makeIndependent(args[i + 1])); + } + return RValue_makeUndefined(); +} + +static RValue builtinDsListDestroy(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsList* list = dsListGet(runner, id); + if (list == nullptr) return RValue_makeUndefined(); + repeat(arrlen(list->items), i) { + RValue_free(&list->items[i]); + } + arrfree(list->items); + list->items = nullptr; + list->freed = true; + return RValue_makeUndefined(); +} + +static RValue builtinDsListFindValue(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + int32_t pos = RValue_toInt32(args[1]); + DsList* list = dsListGet(runner, id); + if (list == nullptr) return RValue_makeUndefined(); + if (0 > pos || pos >= (int32_t) arrlen(list->items)) return RValue_makeUndefined(); + return RValue_makeIndependent(list->items[pos]); +} + +static RValue builtinDsListSize(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsList* list = dsListGet(runner, id); + if (list == nullptr) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) arrlen(list->items)); +} + +static RValue builtinDsListFindIndex(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + DsList* list = dsListGet(runner, id); + if (list == nullptr) return RValue_makeReal(-1.0); + RValue needle = args[1]; + for (int32_t i = 0; (int32_t) arrlen(list->items) > i; i++) { + RValue item = list->items[i]; + if (item.type != needle.type) continue; + switch (item.type) { + case RVALUE_REAL: + if (item.real == needle.real) return RValue_makeReal((GMLReal) i); + break; + case RVALUE_INT32: + case RVALUE_BOOL: + if (item.int32 == needle.int32) return RValue_makeReal((GMLReal) i); + break; +#ifndef NO_RVALUE_INT64 + case RVALUE_INT64: + if (item.int64 == needle.int64) return RValue_makeReal((GMLReal) i); + break; +#endif + case RVALUE_STRING: + if (item.string != nullptr && needle.string != nullptr && strcmp(item.string, needle.string) == 0) return RValue_makeReal((GMLReal) i); + break; + default: + break; + } + } + return RValue_makeReal(-1.0); +} + +// ===[ ARRAY FUNCTIONS ]=== + +static RValue builtinArrayLength1d(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) GMLArray_length1D(args[0].array)); +} + +// array_push(array, values...) - append one or more values to the end of the array (row 0). BC17+ arrays are mutable references; mutate in place. +static RValue builtinArrayPush(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeUndefined(); + GMLArray* arr = args[0].array; + int32_t startLen = GMLArray_length1D(arr); + int32_t toPush = argCount - 1; + if (toPush > 0) { + GMLArray_growTo(arr, startLen + toPush); + repeat(toPush, i) { + RValue* slot = GMLArray_slot(arr, startLen + i); + RValue val = args[1 + i]; + RValue_free(slot); + *slot = RValue_makeIndependent(val); + } + } + return RValue_makeUndefined(); +} + +// array_insert(array, index, values...) - insert one or more values at "index", shifting the tail up. If "index" is past the end, fill the gap with real 0 (see the yyVariable.js for reference). +static RValue builtinArrayInsert(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeUndefined(); + GMLArray* arr = args[0].array; + int32_t index = (int32_t) RValue_toReal(args[1]); + if (0 > index) index = 0; + int32_t toInsert = argCount - 2; + int32_t oldLen = (arr->rowCount == 0) ? 0 : arr->rows[0].length; + + // Pad with real 0 if index is past the current end + if (index > oldLen) { + GMLArray_growTo(arr, index); + GMLArrayRow* row = &arr->rows[0]; + for (int32_t i = oldLen; index > i; i++) { + RValue_free(&row->data[i]); + row->data[i] = RValue_makeReal(0.0); + } + oldLen = index; + } + + if (0 >= toInsert) return RValue_makeUndefined(); + + GMLArray_growTo(arr, oldLen + toInsert); + GMLArrayRow* row = &arr->rows[0]; + + // Shift tail up by toInsert + int32_t tailLen = oldLen - index; + if (tailLen > 0) memmove(&row->data[index + toInsert], &row->data[index], (size_t) tailLen * sizeof(RValue)); + + // Write inserted values + repeat(toInsert, i) { + row->data[index + i] = RValue_makeIndependent(args[2 + i]); + } + return RValue_makeUndefined(); +} + +// array_resize(array, newSize) - resize row 0 to newSize. Growth fills with undefined, shrinking frees truncated entries. +static RValue builtinArrayResize(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeUndefined(); + GMLArray* arr = args[0].array; + int32_t newSize = (int32_t) RValue_toReal(args[1]); + if (0 > newSize) newSize = 0; + if (arr->rowCount == 0) { + if (newSize == 0) return RValue_makeUndefined(); + GMLArray_growTo(arr, newSize); + return RValue_makeUndefined(); + } + GMLArrayRow* row = &arr->rows[0]; + if (newSize > row->length) { + GMLArray_growTo(arr, newSize); + } else if (row->length > newSize) { + for (int32_t i = newSize; row->length > i; i++) RValue_free(&row->data[i]); + row->length = newSize; + } + return RValue_makeUndefined(); +} + +// array_delete(array, pos, count) - remove `count` entries starting at `pos` from row 0, shifting the tail down. +static RValue builtinArrayDelete(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeUndefined(); + if (args[0].type != RVALUE_ARRAY || args[0].array == nullptr) return RValue_makeUndefined(); + GMLArray* arr = args[0].array; + if (arr->rowCount == 0) return RValue_makeUndefined(); + GMLArrayRow* row = &arr->rows[0]; + int32_t pos = (int32_t) RValue_toReal(args[1]); + int32_t count = (int32_t) RValue_toReal(args[2]); + if (0 > pos) pos = 0; + if (pos >= row->length || 0 >= count) return RValue_makeUndefined(); + if (count > row->length - pos) count = row->length - pos; + repeat(count, i) RValue_free(&row->data[pos + i]); + int32_t tailStart = pos + count; + int32_t tailLen = row->length - tailStart; + if (tailLen > 0) memmove(&row->data[pos], &row->data[tailStart], (size_t) tailLen * sizeof(RValue)); + row->length -= count; + return RValue_makeUndefined(); +} + +// ===[ COLLISION FUNCTIONS]=== + +static RValue builtinPlaceFree(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeBool(true); + + Runner* runner = (Runner*) ctx->runner; + Instance* caller = (Instance*) ctx->currentInstance; + if (caller == nullptr) return RValue_makeBool(true); + + GMLReal testX = RValue_toReal(args[0]); + GMLReal testY = RValue_toReal(args[1]); + + // Save current position and temporarily move to test position + GMLReal savedX = caller->x; + GMLReal savedY = caller->y; + caller->x = testX; + caller->y = testY; + + InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); + bool free = true; + + if (callerBBox.valid) { + int32_t instanceCount = (int32_t) arrlen(runner->instances); + repeat(instanceCount, i) { + Instance* other = runner->instances[i]; + if (!other->active || !other->solid || other == caller) continue; + + InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); + if (!otherBBox.valid) continue; + + if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { + free = false; + break; + } + } + } + + // Restore original position + caller->x = savedX; + caller->y = savedY; + + return RValue_makeBool(free); +} + +// place_empty(x, y) - returns true if no instance overlaps at position (x, y), checking ALL instances (not just solid) +static bool placeEmptyAt(Runner* runner, Instance* caller, GMLReal testX, GMLReal testY) { + GMLReal savedX = caller->x; + GMLReal savedY = caller->y; + caller->x = testX; + caller->y = testY; + + InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); + bool empty = true; + + if (callerBBox.valid) { + int32_t instanceCount = (int32_t) arrlen(runner->instances); + repeat(instanceCount, i) { + Instance* other = runner->instances[i]; + if (!other->active || other == caller) continue; + + InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); + if (!otherBBox.valid) continue; + + if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { + empty = false; + break; + } + } + } + + caller->x = savedX; + caller->y = savedY; + return empty; +} + +// placeFreeAt - returns true if no SOLID instance overlaps at position (x, y) +static bool placeFreeAt(Runner* runner, Instance* caller, GMLReal testX, GMLReal testY) { + GMLReal savedX = caller->x; + GMLReal savedY = caller->y; + caller->x = testX; + caller->y = testY; + + InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); + bool free = true; + + if (callerBBox.valid) { + int32_t instanceCount = (int32_t) arrlen(runner->instances); + repeat(instanceCount, i) { + Instance* other = runner->instances[i]; + if (!other->active || !other->solid || other == caller) continue; + + InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); + if (!otherBBox.valid) continue; + + if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { + free = false; + break; + } + } + } + + caller->x = savedX; + caller->y = savedY; + return free; +} + +// noCollisionWithObject - returns true if no instance of the given object overlaps at position (x, y) +static bool noCollisionWithObject(Runner* runner, Instance* caller, GMLReal testX, GMLReal testY, int32_t objIndex) { + GMLReal savedX = caller->x; + GMLReal savedY = caller->y; + caller->x = testX; + caller->y = testY; + + InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); + bool free = true; + + if (callerBBox.valid) { + int32_t snapBase = Runner_pushInstancesForTarget(runner, objIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* other = runner->instanceSnapshots[i]; + if (!other->active || other == caller) continue; + + InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); + if (!otherBBox.valid) continue; + + if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { + free = false; + break; + } + } + Runner_popInstanceSnapshot(runner, snapBase); + } + + caller->x = savedX; + caller->y = savedY; + return free; +} + +// Tests whether a position is free for the given collision mode +// objIndex == INSTANCE_ALL with checkall=false: check solid only (place_free) +// objIndex == INSTANCE_ALL with checkall=true: check all instances (place_empty) +// objIndex == specific object/instance: check that specific target (instance_place == noone) +static bool mpTestFree(Runner* runner, Instance* inst, GMLReal x, GMLReal y, int32_t objIndex, bool checkall) { + if (objIndex == INSTANCE_ALL) { + if (checkall) { + return placeEmptyAt(runner, inst, x, y); + } else { + return placeFreeAt(runner, inst, x, y); + } + } else { + return noCollisionWithObject(runner, inst, x, y, objIndex); + } +} + +// place_empty(x, y) - returns true if no instance (solid or not) overlaps at position (x, y) +static RValue builtinPlaceEmpty(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeBool(true); + + Runner* runner = (Runner*) ctx->runner; + Instance* caller = (Instance*) ctx->currentInstance; + if (caller == nullptr) return RValue_makeBool(true); + + GMLReal testX = RValue_toReal(args[0]); + GMLReal testY = RValue_toReal(args[1]); + return RValue_makeBool(placeEmptyAt(runner, caller, testX, testY)); +} + +// ===[ Motion Planning ]=== + +static RValue builtinMpLinearStepCommon(VMContext* ctx, GMLReal goalX, GMLReal goalY, GMLReal stepsize, int32_t objIndex, bool checkall) { + Runner* runner = (Runner*) ctx->runner; + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr) return RValue_makeBool(false); + + // Check whether already at the correct position + if (inst->x == (float) goalX && inst->y == (float) goalY) return RValue_makeBool(true); + + // Check whether close enough for a single step + GMLReal dx = inst->x - goalX; + GMLReal dy = inst->y - goalY; + GMLReal dist = GMLReal_sqrt(dx * dx + dy * dy); + + GMLReal newX, newY; + bool reached; + if (dist <= stepsize) { + newX = goalX; + newY = goalY; + reached = true; + } else { + newX = inst->x + stepsize * (goalX - inst->x) / dist; + newY = inst->y + stepsize * (goalY - inst->y) / dist; + reached = false; + } + + // Check whether free + if (!mpTestFree(runner, inst, newX, newY, objIndex, checkall)) return RValue_makeBool(reached); + + inst->direction = (float) (GMLReal_atan2(-(newY - inst->y), newX - inst->x) * (180.0 / M_PI)); + inst->x = (float) newX; + inst->y = (float) newY; + SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); + return RValue_makeBool(reached); +} + +// mp_linear_step(x, y, stepsize, checkall) +static RValue builtinMpLinearStep(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal goalX = RValue_toReal(args[0]); + GMLReal goalY = RValue_toReal(args[1]); + GMLReal stepsize = RValue_toReal(args[2]); + bool checkall = RValue_toBool(args[3]); + return builtinMpLinearStepCommon(ctx, goalX, goalY, stepsize, INSTANCE_ALL, checkall); +} + +// mp_linear_step_object(x, y, stepsize, obj) +static RValue builtinMpLinearStepObject(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal goalX = RValue_toReal(args[0]); + GMLReal goalY = RValue_toReal(args[1]); + GMLReal stepsize = RValue_toReal(args[2]); + int32_t obj = RValue_toInt32(args[3]); + return builtinMpLinearStepCommon(ctx, goalX, goalY, stepsize, obj, true); +} + + +// Computes the shortest angular difference between two directions (result 0-180) +static GMLReal mpDiffDir(GMLReal dir1, GMLReal dir2) { + while (dir1 <= 0.0) dir1 += 360.0; + while (dir1 >= 360.0) dir1 -= 360.0; + while (dir2 < 0.0) dir2 += 360.0; + while (dir2 >= 360.0) dir2 -= 360.0; + GMLReal result = dir2 - dir1; + if (result < 0.0) result = -result; + if (result > 180.0) result = 360.0 - result; + return result; +} + +// Tries a step in the indicated direction; returns whether successful +// If successful, moves the instance and sets its direction +static bool mpTryDir(GMLReal dir, Runner* runner, Instance* inst, GMLReal speed, int32_t objIndex, bool checkall) { + // See whether angle is acceptable + if (mpDiffDir(dir, inst->direction) > runner->mpPotMaxrot) return false; + + GMLReal dirRad = dir * (M_PI / 180.0); + GMLReal cosDir = GMLReal_cos(dirRad); + GMLReal sinDir = GMLReal_sin(dirRad); + + // Check position a bit ahead + GMLReal aheadX = inst->x + speed * runner->mpPotAhead * cosDir; + GMLReal aheadY = inst->y - speed * runner->mpPotAhead * sinDir; + if (!mpTestFree(runner, inst, aheadX, aheadY, objIndex, checkall)) return false; + + // Check next position + GMLReal nextX = inst->x + speed * cosDir; + GMLReal nextY = inst->y - speed * sinDir; + if (!mpTestFree(runner, inst, nextX, nextY, objIndex, checkall)) return false; + + // OK, so set the position + inst->direction = (float) dir; + inst->x = (float) nextX; + inst->y = (float) nextY; + SpatialGrid_markInstanceAsDirty(runner->spatialGrid, inst); + return true; +} + +static RValue builtinMpPotentialStepCommon(VMContext* ctx, GMLReal goalX, GMLReal goalY, GMLReal stepsize, int32_t objIndex, bool checkall) { + Runner* runner = (Runner*) ctx->runner; + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr) return RValue_makeBool(false); + + // Check whether already at the correct position + if (inst->x == (float) goalX && inst->y == (float) goalY) return RValue_makeBool(true); + + // Check whether close enough for a single step + GMLReal dx = inst->x - goalX; + GMLReal dy = inst->y - goalY; + GMLReal dist = GMLReal_sqrt(dx * dx + dy * dy); + if (stepsize >= dist) { + if (mpTestFree(runner, inst, goalX, goalY, objIndex, checkall)) { + GMLReal dir = GMLReal_atan2(-(goalY - inst->y), goalX - inst->x) * (180.0 / M_PI); + inst->direction = (float) dir; + inst->x = (float) goalX; + inst->y = (float) goalY; + SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); + } + return RValue_makeBool(true); + } + + // Try directions as much as possible towards the goal + GMLReal goaldir = GMLReal_atan2(-(goalY - inst->y), goalX - inst->x) * (180.0 / M_PI); + GMLReal curdir = 0.0; + while (180.0 > curdir) { + if (mpTryDir(goaldir - curdir, runner, inst, stepsize, objIndex, checkall)) return RValue_makeBool(false); + if (mpTryDir(goaldir + curdir, runner, inst, stepsize, objIndex, checkall)) return RValue_makeBool(false); + curdir += runner->mpPotStep; + } + + // If we did not succeed, a local minima was reached + // To avoid the instance getting stuck we rotate on the spot + if (runner->mpPotOnSpot) { + inst->direction = (float) (inst->direction + runner->mpPotMaxrot); + } + + return RValue_makeBool(false); +} + +// mp_potential_step(x, y, stepsize, checkall) +static RValue builtinMpPotentialStep(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal goalX = RValue_toReal(args[0]); + GMLReal goalY = RValue_toReal(args[1]); + GMLReal stepsize = RValue_toReal(args[2]); + bool checkall = RValue_toBool(args[3]); + return builtinMpPotentialStepCommon(ctx, goalX, goalY, stepsize, INSTANCE_ALL, checkall); +} + +// mp_potential_step_object(x, y, stepsize, obj) +static RValue builtinMpPotentialStepObject(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal goalX = RValue_toReal(args[0]); + GMLReal goalY = RValue_toReal(args[1]); + GMLReal stepsize = RValue_toReal(args[2]); + int32_t obj = RValue_toInt32(args[3]); + return builtinMpPotentialStepCommon(ctx, goalX, goalY, stepsize, obj, true); +} + +// mp_potential_settings(maxrot, rotstep, ahead, onspot) +static RValue builtinMpPotentialSettings(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + GMLReal maxrot = RValue_toReal(args[0]); + GMLReal rotstep = RValue_toReal(args[1]); + GMLReal ahead = RValue_toReal(args[2]); + bool onspot = RValue_toBool(args[3]); + runner->mpPotMaxrot = (maxrot < 1.0) ? 1.0 : maxrot; + runner->mpPotStep = (rotstep < 1.0) ? 1.0 : rotstep; + runner->mpPotAhead = (ahead < 1.0) ? 1.0 : ahead; + runner->mpPotOnSpot = onspot; + return RValue_makeReal(0.0); +} + +// ===[ STUBBED FUNCTIONS ]=== + +#define STUB_RETURN_ZERO(name) \ + static RValue builtin_##name(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { \ + logStubbedFunction(ctx, #name); \ + return RValue_makeReal(0.0); \ + } + +#define STUB_RETURN_TRUE(name) \ + static RValue builtin_##name(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { \ + logStubbedFunction(ctx, #name); \ + return RValue_makeBool(true); \ + } + +#define STUB_RETURN_VALUE(name, value) \ + static RValue builtin_##name(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { \ + logStubbedFunction(ctx, #name); \ + return RValue_makeReal(value); \ + } + +#define STUB_RETURN_UNDEFINED(name) \ + static RValue builtin_##name(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { \ + logStubbedFunction(ctx, #name); \ + return RValue_makeUndefined(); \ + } + +// Steam stubs +STUB_RETURN_ZERO(steam_initialised) +STUB_RETURN_ZERO(steam_stats_ready) +STUB_RETURN_ZERO(steam_file_exists) +STUB_RETURN_UNDEFINED(steam_file_write) +STUB_RETURN_UNDEFINED(steam_file_read) +STUB_RETURN_ZERO(steam_get_persona_name) + +// ===[ Video stubs ]=== + +typedef struct { + bool isOpen; + bool paused; + bool looping; + bool startDispatched; + bool endDispatched; + GMLReal volume; + GMLReal position; + GMLReal duration; +} VideoStubState; + +static VideoStubState gVideoStubState = { + .volume = 1.0, +}; + +static void cleanupAsyncMap(Runner* runner, int32_t mapId) { + if (mapId < 0 || (int32_t) arrlen(runner->dsMapPool) <= mapId) return; + DsMapEntry** mapPtr = &runner->dsMapPool[mapId]; + if (*mapPtr != nullptr) { + repeat(shlen(*mapPtr), i) { + free((*mapPtr)[i].key); + RValue_free(&(*mapPtr)[i].value); + } + shfree(*mapPtr); + *mapPtr = nullptr; + } +} + +static void dispatchVideoAsync(VMContext* ctx, const char* type) { + Runner* runner = (Runner*) ctx->runner; + int32_t mapId = dsMapCreate(runner); + DsMapEntry** mapPtr = dsMapGet(runner, mapId); + if (mapPtr == nullptr) return; + + shput(*mapPtr, safeStrdup("type"), RValue_makeOwnedString(safeStrdup(type))); + shput(*mapPtr, safeStrdup("event_type"), RValue_makeOwnedString(safeStrdup(type))); + shput(*mapPtr, safeStrdup("status"), RValue_makeReal(0.0)); + + int32_t previousAsyncLoad = runner->asyncLoadMapId; + runner->asyncLoadMapId = mapId; + Runner_executeEventForAll(runner, EVENT_OTHER, OTHER_ASYNC_SOCIAL); + runner->asyncLoadMapId = previousAsyncLoad; + + cleanupAsyncMap(runner, mapId); +} + +static void finishVideoStub(VMContext* ctx) { + if (!gVideoStubState.isOpen) return; + if (!gVideoStubState.startDispatched) { + gVideoStubState.startDispatched = true; + dispatchVideoAsync(ctx, "video_start"); + } + if (!gVideoStubState.isOpen) return; + // Since there is no decoder in this backend, even looped videos complete immediately. + if (!gVideoStubState.endDispatched) { + gVideoStubState.endDispatched = true; + dispatchVideoAsync(ctx, "video_end"); + gVideoStubState.isOpen = false; + gVideoStubState.paused = false; + gVideoStubState.position = gVideoStubState.duration; + } +} + +static RValue builtin_video_open(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + logStubbedFunction(ctx, "video_open"); + if (argCount > 0) { + char* path = RValue_toString(args[0]); + free(path); + } + gVideoStubState.isOpen = true; + gVideoStubState.paused = false; + gVideoStubState.startDispatched = false; + gVideoStubState.endDispatched = false; + gVideoStubState.looping = false; + gVideoStubState.position = 0.0; + gVideoStubState.duration = 0.0; + return RValue_makeUndefined(); +} + +static RValue builtin_video_close(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_close"); + gVideoStubState.isOpen = false; + gVideoStubState.paused = false; + gVideoStubState.startDispatched = false; + gVideoStubState.endDispatched = false; + gVideoStubState.looping = false; + gVideoStubState.position = 0.0; + gVideoStubState.duration = 0.0; + return RValue_makeUndefined(); +} + +static RValue builtin_video_draw(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_draw"); + finishVideoStub(ctx); + + RValue result = VM_createArray(ctx); + // GameMaker documents -2 here as "video finished" on platforms that report it. + VM_arraySet(ctx, &result, 0, RValue_makeReal(-2.0)); + return result; +} + +static RValue builtin_video_get_status(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_get_status"); + finishVideoStub(ctx); + if (!gVideoStubState.isOpen) return RValue_makeReal(0.0); // video_status_closed + return RValue_makeReal(gVideoStubState.paused ? 3.0 : 2.0); // paused / playing +} + +static RValue builtin_video_get_format(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_get_format"); + return RValue_makeReal(0.0); // video_format_rgba +} + +static RValue builtin_video_get_duration(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_get_duration"); + return RValue_makeReal(gVideoStubState.duration); +} + +static RValue builtin_video_get_position(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_get_position"); + return RValue_makeReal(gVideoStubState.position); +} + +static RValue builtin_video_set_volume(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + logStubbedFunction(ctx, "video_set_volume"); + if (argCount > 0) gVideoStubState.volume = RValue_toReal(args[0]); + return RValue_makeUndefined(); +} + +static RValue builtin_video_get_volume(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_get_volume"); + return RValue_makeReal(gVideoStubState.volume); +} + +static RValue builtin_video_pause(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_pause"); + if (gVideoStubState.isOpen) gVideoStubState.paused = true; + return RValue_makeUndefined(); +} + +static RValue builtin_video_resume(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_resume"); + if (gVideoStubState.isOpen) gVideoStubState.paused = false; + return RValue_makeUndefined(); +} + +static RValue builtin_video_enable_loop(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + logStubbedFunction(ctx, "video_enable_loop"); + if (argCount > 0) gVideoStubState.looping = RValue_toBool(args[0]); + return RValue_makeUndefined(); +} + +static RValue builtin_video_is_looping(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "video_is_looping"); + return RValue_makeBool(gVideoStubState.looping); +} + +static RValue builtin_video_seek_to(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + logStubbedFunction(ctx, "video_seek_to"); + if (argCount > 0) gVideoStubState.position = RValue_toReal(args[0]); + return RValue_makeUndefined(); +} + +// ===[ Audio Built-in Functions ]=== + +// Helper to get the AudioSystem from VMContext (returns nullptr if no audio) +static AudioSystem* getAudioSystem(VMContext* ctx) { + Runner* runner = (Runner*) ctx->runner; + return runner->audioSystem; +} + +static RValue builtin_audioExists(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr || audio->vtable == nullptr || argCount < 1) return RValue_makeBool(false); + if (args[0].type == RVALUE_UNDEFINED) return RValue_makeBool(false); + + int32_t soundIndex = RValue_toInt32(args[0]); + if (soundIndex < 0) return RValue_makeBool(false); + + DataWin* dw = audio->audioGroups[0]; + if (dw == nullptr) return RValue_makeBool(false); + return RValue_makeBool((uint32_t) soundIndex < dw->sond.count); +} + +static RValue builtin_audioChannelNum(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + int32_t count = RValue_toInt32(args[0]); + audio->vtable->setChannelCount(audio, count); + return RValue_makeUndefined(); +} + +static void legacyMusicSync(Runner* runner) { + runner->lastMusicInstance = arrlen(runner->musicInstanceStack) > 0 + ? runner->musicInstanceStack[arrlen(runner->musicInstanceStack) - 1] + : -1; +} + +static void legacyMusicPush(Runner* runner, int32_t instanceId) { + if (instanceId < 0) return; + arrput(runner->musicInstanceStack, instanceId); + legacyMusicSync(runner); +} + +static void legacyMusicPop(Runner* runner) { + if (arrlen(runner->musicInstanceStack) > 0) { + arrpop(runner->musicInstanceStack); + } + legacyMusicSync(runner); +} + +static void legacyMusicRemoveInstance(Runner* runner, int32_t instanceId) { + int32_t count = (int32_t) arrlen(runner->musicInstanceStack); + for (int32_t i = 0; i < count; i++) { + if (runner->musicInstanceStack[i] != instanceId) continue; + if (i + 1 < count) { + memmove( + &runner->musicInstanceStack[i], + &runner->musicInstanceStack[i + 1], + (size_t) (count - i - 1) * sizeof(int32_t) + ); + } + arrsetlen(runner->musicInstanceStack, count - 1); + break; + } + legacyMusicSync(runner); +} + +static RValue builtin_audioPlaySound(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeReal(-1.0); + + // Do not attempt to play "undefined" sounds (matches GameMaker-HTML5 behavior, and fixes random sound effects on room transitions in DELTARUNE Chapter 2) + if (args[0].type == RVALUE_UNDEFINED) + return RValue_makeReal(-1.0); + + int32_t soundIndex = RValue_toInt32(args[0]); + int32_t priority = RValue_toInt32(args[1]); + bool loop = RValue_toBool(args[2]); + int32_t instanceId = audio->vtable->playSound(audio, soundIndex, priority, loop); + return RValue_makeReal((GMLReal) instanceId); +} + +static RValue builtin_audioStopSound(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t soundOrInstance = RValue_toInt32(args[0]); + audio->vtable->stopSound(audio, soundOrInstance); + if (soundOrInstance >= 100000) { + legacyMusicRemoveInstance(runner, soundOrInstance); + } + return RValue_makeUndefined(); +} + +static RValue builtin_audioStopAll(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + audio->vtable->stopAll(audio); + arrsetlen(runner->musicInstanceStack, 0); + legacyMusicSync(runner); + return RValue_makeUndefined(); +} + +static RValue builtin_audioIsPlaying(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeBool(false); + int32_t soundOrInstance = RValue_toInt32(args[0]); + bool playing = audio->vtable->isPlaying(audio, soundOrInstance); + return RValue_makeBool(playing); +} + +static RValue builtin_audioIsPaused(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeBool(false); + int32_t soundOrInstance = RValue_toInt32(args[0]); + bool playing = audio->vtable->isPlaying(audio, soundOrInstance); + return RValue_makeBool(!playing); +} + + +// audio_sound_length(sound) - returns the length of a sound in seconds. +static RValue builtin_audioSoundLength(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeReal(0.0); + int32_t soundOrInstance = RValue_toInt32(args[0]); + float length = audio->vtable->getSoundLength(audio, soundOrInstance); + return RValue_makeReal((GMLReal) length); +} + +static RValue builtin_audioSoundGain(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + int32_t soundOrInstance = RValue_toInt32(args[0]); + float gain = (float) RValue_toReal(args[1]); + uint32_t timeMs = (uint32_t) RValue_toInt32(args[2]); + audio->vtable->setSoundGain(audio, soundOrInstance, gain, timeMs); + return RValue_makeUndefined(); +} + +static RValue builtin_audioSoundPitch(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + int32_t soundOrInstance = RValue_toInt32(args[0]); + float pitch = (float) RValue_toReal(args[1]); + audio->vtable->setSoundPitch(audio, soundOrInstance, pitch); + return RValue_makeUndefined(); +} + +static RValue builtin_audioSoundGetGain(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeReal(0.0); + int32_t soundOrInstance = RValue_toInt32(args[0]); + float gain = audio->vtable->getSoundGain(audio, soundOrInstance); + return RValue_makeReal((GMLReal) gain); +} + +static RValue builtin_audioSoundGetPitch(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeReal(1.0); + int32_t soundOrInstance = RValue_toInt32(args[0]); + float pitch = audio->vtable->getSoundPitch(audio, soundOrInstance); + return RValue_makeReal((GMLReal) pitch); +} + +static RValue builtin_audioMasterGain(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + float gain = (float) RValue_toReal(args[0]); + audio->vtable->setMasterGain(audio, gain); + return RValue_makeUndefined(); +} + +static RValue builtin_audioGroupLoad(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + int32_t groupIndex = RValue_toInt32(args[0]); + audio->vtable->groupLoad(audio, groupIndex); + return RValue_makeUndefined(); +} + +static RValue builtin_audioGroupIsLoaded(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeBool(false); + int32_t groupIndex = RValue_toInt32(args[0]); + bool loaded = audio->vtable->groupIsLoaded(audio, groupIndex); + return RValue_makeBool(loaded); +} + +static RValue builtin_audioPlayMusic(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeReal(-1.0); + int32_t soundIndex = RValue_toInt32(args[0]); + int32_t priority = RValue_toInt32(args[1]); + bool loop = RValue_toBool(args[2]); + Runner* runner = (Runner*) ctx->runner; + int32_t instanceId = audio->vtable->playSound(audio, soundIndex, priority, loop); + legacyMusicPush(runner, instanceId); + return RValue_makeReal((GMLReal) instanceId); +} + +static RValue builtin_audioStopMusic(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + if (runner->lastMusicInstance >= 0) { + audio->vtable->stopSound(audio, runner->lastMusicInstance); + legacyMusicPop(runner); + } + return RValue_makeUndefined(); +} + +static RValue builtin_audioMusicGain(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + if (runner->lastMusicInstance >= 0) { + float gain = (float) RValue_toReal(args[0]); + uint32_t timeMs = (uint32_t) RValue_toInt32(args[1]); + audio->vtable->setSoundGain(audio, runner->lastMusicInstance, gain, timeMs); + } + return RValue_makeUndefined(); +} + +static RValue builtin_audioMusicIsPlaying(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + if (runner->lastMusicInstance >= 0) { + return RValue_makeBool(audio->vtable->isPlaying(audio, runner->lastMusicInstance)); + } + return RValue_makeBool(false); +} + +static RValue builtin_audioPauseSound(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + int32_t soundOrInstance = RValue_toInt32(args[0]); + audio->vtable->pauseSound(audio, soundOrInstance); + return RValue_makeUndefined(); +} + +static RValue builtin_audioResumeSound(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + int32_t soundOrInstance = RValue_toInt32(args[0]); + audio->vtable->resumeSound(audio, soundOrInstance); + return RValue_makeUndefined(); +} + +static RValue builtin_audioPauseAll(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + audio->vtable->pauseAll(audio); + return RValue_makeUndefined(); +} + +static RValue builtin_audioResumeAll(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + audio->vtable->resumeAll(audio); + return RValue_makeUndefined(); +} + +static RValue builtin_audioSoundGetTrackPosition(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeReal(0.0); + int32_t soundOrInstance = RValue_toInt32(args[0]); + float pos = audio->vtable->getTrackPosition(audio, soundOrInstance); + return RValue_makeReal((GMLReal) pos); +} + +static RValue builtin_audioSoundSetTrackPosition(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeUndefined(); + int32_t soundOrInstance = RValue_toInt32(args[0]); + float pos = (float) RValue_toReal(args[1]); + audio->vtable->setTrackPosition(audio, soundOrInstance, pos); + return RValue_makeUndefined(); +} + +static RValue builtin_audioCreateStream(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeReal(-1.0); + char* filename = RValue_toString(args[0]); + int32_t streamIndex = audio->vtable->createStream(audio, filename); + free(filename); + return RValue_makeReal((GMLReal) streamIndex); +} + +static RValue builtin_audioDestroyStream(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = getAudioSystem(ctx); + if (audio == nullptr) return RValue_makeReal(-1.0); + int32_t streamIndex = RValue_toInt32(args[0]); + bool success = audio->vtable->destroyStream(audio, streamIndex); + return RValue_makeReal(success ? 1.0 : -1.0); +} + +// Application surface stubs +STUB_RETURN_UNDEFINED(application_surface_enable) +STUB_RETURN_UNDEFINED(application_surface_draw_enable) + +// ===[ Gamepad Functions ]=== +static RValue builtinGamepadGetDeviceCount(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) RunnerGamepad_getDeviceCount(runner->gamepads)); +} + +static RValue builtinGamepadIsConnected(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); + int32_t device = RValue_toInt32(args[0]); + return RValue_makeBool(RunnerGamepad_isConnected(runner->gamepads, device)); +} + +static RValue builtinGamepadButtonCheck(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); + int32_t device = RValue_toInt32(args[0]); + int32_t button = RValue_toInt32(args[1]); + bool result = RunnerGamepad_buttonCheck(runner->gamepads, device, button); + return RValue_makeBool(result); +} + +static RValue builtinGamepadButtonCheckPressed(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); + int32_t device = RValue_toInt32(args[0]); + int32_t button = RValue_toInt32(args[1]); + return RValue_makeBool(RunnerGamepad_buttonCheckPressed(runner->gamepads, device, button)); +} + +static RValue builtinGamepadButtonCheckReleased(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); + int32_t device = RValue_toInt32(args[0]); + int32_t button = RValue_toInt32(args[1]); + return RValue_makeBool(RunnerGamepad_buttonCheckReleased(runner->gamepads, device, button)); +} + +static RValue builtinGamepadButtonValue(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t device = RValue_toInt32(args[0]); + int32_t button = RValue_toInt32(args[1]); + return RValue_makeReal(RunnerGamepad_buttonValue(runner->gamepads, device, button)); +} + +static RValue builtinGamepadIsSupported(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); + return RValue_makeBool(true); +} + +static RValue builtinGamepadAxisValue(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t device = RValue_toInt32(args[0]); + int32_t axis = RValue_toInt32(args[1]); + return RValue_makeReal(RunnerGamepad_axisValue(runner->gamepads, device, axis)); +} + +static RValue builtinGamepadGetDescription(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeOwnedString(safeStrdup("")); + int32_t device = RValue_toInt32(args[0]); + const char* desc = RunnerGamepad_getDescription(runner->gamepads, device); + return RValue_makeOwnedString(safeStrdup(desc)); +} + +static RValue builtinGamepadGetGuid(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeOwnedString(safeStrdup("none")); + int32_t device = RValue_toInt32(args[0]); + return RValue_makeOwnedString(safeStrdup(RunnerGamepad_getGuid(runner->gamepads, device))); +} + +static RValue builtinGamepadGetButtonThreshold(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.5); + int32_t device = RValue_toInt32(args[0]); + return RValue_makeReal(RunnerGamepad_getButtonThreshold(runner->gamepads, device)); +} + +static RValue builtinGamepadSetButtonThreshold(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeUndefined(); + int32_t device = RValue_toInt32(args[0]); + float threshold = (float) RValue_toReal(args[1]); + RunnerGamepad_setButtonThreshold(runner->gamepads, device, threshold); + return RValue_makeUndefined(); +} + +static RValue builtinGamepadGetAxisDeadzone(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.15); + int32_t device = RValue_toInt32(args[0]); + return RValue_makeReal(RunnerGamepad_getAxisDeadzone(runner->gamepads, device)); +} + +static RValue builtinGamepadSetAxisDeadzone(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeUndefined(); + int32_t device = RValue_toInt32(args[0]); + float deadzone = (float) RValue_toReal(args[1]); + RunnerGamepad_setAxisDeadzone(runner->gamepads, device, deadzone); + return RValue_makeUndefined(); +} + +static RValue builtinGamepadAxisCount(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t device = RValue_toInt32(args[0]); + return RValue_makeReal(RunnerGamepad_getAxisCount(runner->gamepads, device)); +} + +static RValue builtinGamepadButtonCount(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t device = RValue_toInt32(args[0]); + return RValue_makeReal(RunnerGamepad_getButtonCount(runner->gamepads, device)); +} + +static RValue builtinGamepadHatCount(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t device = RValue_toInt32(args[0]); + return RValue_makeReal(RunnerGamepad_getHatCount(runner->gamepads, device)); +} + +static RValue builtinGamepadHatValue(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t device = RValue_toInt32(args[0]); + int32_t hat = RValue_toInt32(args[1]); + return RValue_makeReal(RunnerGamepad_getHatValue(runner->gamepads, device, hat)); +} + +// ===[ INI Functions ]=== + +static void discardIniCache(Runner* runner) { + if (runner->cachedIni != nullptr) { + Ini_free(runner->cachedIni); + runner->cachedIni = nullptr; + } + free(runner->cachedIniPath); + runner->cachedIniPath = nullptr; +} + +static RValue builtinIniOpen(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + + Runner* runner = (Runner*) ctx->runner; + const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); + + // If the same file is already open, do nothing + if (runner->currentIni != nullptr && runner->currentIniPath != nullptr && strcmp(runner->currentIniPath, path) == 0) { + return RValue_makeUndefined(); + } + + // Close any previously open INI (implicit close, no disk write) + if (runner->currentIni != nullptr) { + Ini_free(runner->currentIni); + runner->currentIni = nullptr; + } + free(runner->currentIniPath); + runner->currentIniPath = nullptr; + + // Check if we have a cached INI for this path + if (runner->cachedIni != nullptr && runner->cachedIniPath != nullptr && strcmp(runner->cachedIniPath, path) == 0) { + runner->currentIni = runner->cachedIni; + runner->currentIniPath = runner->cachedIniPath; + runner->cachedIni = nullptr; + runner->cachedIniPath = nullptr; + runner->currentIniDirty = false; + return RValue_makeUndefined(); + } + + // Cache miss, discard the old cache and read from disk + discardIniCache(runner); + + FileSystem* fs = runner->fileSystem; + + runner->currentIniPath = safeStrdup(path); + + char* content = fs->vtable->readFileText(fs, path); + if (content != nullptr) { + runner->currentIni = Ini_parse(content); + free(content); + } else { + runner->currentIni = Ini_parse(""); + } + + runner->currentIniDirty = false; + + return RValue_makeUndefined(); +} + +static RValue builtinIniClose(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->currentIni != nullptr) { + FileSystem* fs = runner->fileSystem; + + if (runner->currentIniDirty) { + char* serialized = Ini_serialize(runner->currentIni, INI_SERIALIZE_DEFAULT_INITIAL_CAPACITY); + fs->vtable->writeFileText(fs, runner->currentIniPath, serialized); + free(serialized); + } + + // Move to cache instead of freeing + discardIniCache(runner); + runner->cachedIni = runner->currentIni; + runner->cachedIniPath = runner->currentIniPath; + runner->currentIni = nullptr; + runner->currentIniPath = nullptr; + } else { + free(runner->currentIniPath); + runner->currentIniPath = nullptr; + } + + return RValue_makeUndefined(); +} + +static RValue builtinIniReadString(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (3 > argCount || runner->currentIni == nullptr) return RValue_makeOwnedString(safeStrdup("")); + + const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); + const char* key = (args[1].type == RVALUE_STRING ? args[1].string : ""); + + const char* value = Ini_getString(runner->currentIni, section, key); + if (value != nullptr) { + return RValue_makeOwnedString(safeStrdup(value)); + } + + // Return the default value (3rd arg) + if (args[2].type == RVALUE_STRING && args[2].string != nullptr) { + return RValue_makeOwnedString(safeStrdup(args[2].string)); + } + char* str = RValue_toString(args[2]); + return RValue_makeOwnedString(str); +} + +static RValue builtinIniReadReal(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (3 > argCount || runner->currentIni == nullptr) return RValue_makeReal(0.0); + + const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); + const char* key = (args[1].type == RVALUE_STRING ? args[1].string : ""); + + const char* value = Ini_getString(runner->currentIni, section, key); + if (value != nullptr) { + return RValue_makeReal(atof(value)); + } + + return RValue_makeReal(RValue_toReal(args[2])); +} + +static RValue builtinIniWriteString(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (3 > argCount || runner->currentIni == nullptr) return RValue_makeUndefined(); + + const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); + const char* key = (args[1].type == RVALUE_STRING ? args[1].string : ""); + const char* value = (args[2].type == RVALUE_STRING ? args[2].string : ""); + + Ini_setString(runner->currentIni, section, key, value); + runner->currentIniDirty = true; + return RValue_makeUndefined(); +} + +static RValue builtinIniWriteReal(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (3 > argCount || runner->currentIni == nullptr) return RValue_makeUndefined(); + + const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); + const char* key = (args[1].type == RVALUE_STRING ? args[1].string : ""); + char* valueStr = RValue_toString(args[2]); + + Ini_setString(runner->currentIni, section, key, valueStr); + runner->currentIniDirty = true; + free(valueStr); + return RValue_makeUndefined(); +} + +static RValue builtinIniSectionExists(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (1 > argCount || runner->currentIni == nullptr) return RValue_makeBool(false); + + const char* section = (args[0].type == RVALUE_STRING ? args[0].string : ""); + return RValue_makeBool(Ini_hasSection(runner->currentIni, section)); +} + +// ===[ Text File Functions ]=== + +static int32_t findFreeTextFileSlot(Runner* runner) { + repeat(MAX_OPEN_TEXT_FILES, i) { + if (!runner->openTextFiles[i].isOpen) return (int32_t) i; + } + return -1; +} + +static RValue builtinFileExists(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); + Runner* runner = (Runner*) ctx->runner; + FileSystem* fs = runner->fileSystem; + return RValue_makeBool(fs->vtable->fileExists(fs, path)); +} + +static RValue builtinFileTextOpenRead(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1.0); + const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); + Runner* runner = (Runner*) ctx->runner; + FileSystem* fs = runner->fileSystem; + + int32_t slot = findFreeTextFileSlot(runner); + if (0 > slot) { + fprintf(stderr, "Warning: Too many open text files!\n"); + abort(); + } + + char* content = fs->vtable->readFileText(fs, path); + if (content == nullptr) { + // GML returns a valid handle even if the file doesn't exist; eof is immediately true + content = safeStrdup(""); + } + + runner->openTextFiles[slot] = (OpenTextFile) { + .content = content, + .writeBuffer = nullptr, + .filePath = nullptr, + .readPos = 0, + .contentLen = (int32_t) strlen(content), + .isWriteMode = false, + .isOpen = true, + }; + + return RValue_makeReal((GMLReal) slot); +} + +static RValue builtinFileTextOpenWrite(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1.0); + const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); + Runner* runner = (Runner*) ctx->runner; + + int32_t slot = findFreeTextFileSlot(runner); + if (0 > slot) { + fprintf(stderr, "Warning: Too many open text files!\n"); + abort(); + } + + runner->openTextFiles[slot] = (OpenTextFile) { + .content = nullptr, + .writeBuffer = safeStrdup(""), + .filePath = safeStrdup(path), + .readPos = 0, + .contentLen = 0, + .isWriteMode = true, + .isOpen = true, + }; + + return RValue_makeReal((GMLReal) slot); +} + +static RValue builtinFileTextClose(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t handle = RValue_toInt32(args[0]); + if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeUndefined(); + + OpenTextFile* file = &runner->openTextFiles[handle]; + if (file->isWriteMode && file->writeBuffer != nullptr && file->filePath != nullptr) { + FileSystem* fs = runner->fileSystem; + fs->vtable->writeFileText(fs, file->filePath, file->writeBuffer); + } + + free(file->content); + free(file->writeBuffer); + free(file->filePath); + *file = (OpenTextFile) {0}; + return RValue_makeUndefined(); +} + +static RValue builtinFileTextReadString(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); + Runner* runner = (Runner*) ctx->runner; + int32_t handle = RValue_toInt32(args[0]); + if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeOwnedString(safeStrdup("")); + + OpenTextFile* file = &runner->openTextFiles[handle]; + if (file->readPos >= file->contentLen) return RValue_makeOwnedString(safeStrdup("")); + + // Read until newline, carriage return, or EOF (does NOT consume the newline) + int32_t start = file->readPos; + while (file->contentLen > file->readPos) { + char c = file->content[file->readPos]; + if (TextUtils_isNewlineChar(c)) + break; + file->readPos++; + } + + int32_t len = file->readPos - start; + char* result = safeMalloc((size_t) len + 1); + memcpy(result, file->content + start, (size_t) len); + result[len] = '\0'; + return RValue_makeOwnedString(result); +} + +static RValue builtinFileTextReadln(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); + Runner* runner = (Runner*) ctx->runner; + int32_t handle = RValue_toInt32(args[0]); + if (0 > handle || MAX_OPEN_TEXT_FILES <= handle || !runner->openTextFiles[handle].isOpen) return RValue_makeOwnedString(safeStrdup("")); + + OpenTextFile* file = &runner->openTextFiles[handle]; + + int size = 0; + int readPos = file->readPos; + + // First we read everything to figure out what will be the size of the string + // Skip past the current line (consume everything up to and including the newline) + while (file->contentLen > readPos) { + char c = file->content[readPos]; + readPos++; + if (c == '\n') + break; + if (c == '\r') { + // Handle \r\n + if (file->contentLen > readPos && file->content[readPos] == '\n') { + readPos++; + } + break; + } + size++; + } + + // Now we copy it because we already know the size of the string! + char* string = safeMalloc(size + 1); // +1 because the last one is null + memcpy(string, file->content + file->readPos, size); + string[size] = '\0'; + file->readPos = readPos; + return RValue_makeOwnedString(string); +} + +static RValue builtinFileTextReadReal(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + int32_t handle = RValue_toInt32(args[0]); + if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeReal(0.0); + + OpenTextFile* file = &runner->openTextFiles[handle]; + if (file->readPos >= file->contentLen) return RValue_makeReal(0.0); + + // strtod will parse the number and advance past it + char* endPtr = nullptr; + GMLReal value = GMLReal_strtod(file->content + file->readPos, &endPtr); + if (endPtr != nullptr) { + file->readPos = (int32_t) (endPtr - file->content); + } + + return RValue_makeReal(value); +} + +static RValue builtinFileTextWriteString(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t handle = RValue_toInt32(args[0]); + if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeUndefined(); + + OpenTextFile* file = &runner->openTextFiles[handle]; + if (!file->isWriteMode) return RValue_makeUndefined(); + + char* str = RValue_toString(args[1]); + size_t oldLen = strlen(file->writeBuffer); + size_t addLen = strlen(str); + file->writeBuffer = safeRealloc(file->writeBuffer, oldLen + addLen + 1); + memcpy(file->writeBuffer + oldLen, str, addLen); + file->writeBuffer[oldLen + addLen] = '\0'; + free(str); + + return RValue_makeUndefined(); +} + +static RValue builtinFileTextWriteln(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t handle = RValue_toInt32(args[0]); + if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeUndefined(); + + OpenTextFile* file = &runner->openTextFiles[handle]; + if (!file->isWriteMode) return RValue_makeUndefined(); + + size_t oldLen = strlen(file->writeBuffer); + file->writeBuffer = safeRealloc(file->writeBuffer, oldLen + 2); + file->writeBuffer[oldLen] = '\n'; + file->writeBuffer[oldLen + 1] = '\0'; + + return RValue_makeUndefined(); +} + +static RValue builtinFileTextWriteReal(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t handle = RValue_toInt32(args[0]); + if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeUndefined(); + + OpenTextFile* file = &runner->openTextFiles[handle]; + if (!file->isWriteMode) return RValue_makeUndefined(); + + char* str = RValue_toString(args[1]); + size_t oldLen = strlen(file->writeBuffer); + size_t addLen = strlen(str); + file->writeBuffer = safeRealloc(file->writeBuffer, oldLen + addLen + 1); + memcpy(file->writeBuffer + oldLen, str, addLen); + file->writeBuffer[oldLen + addLen] = '\0'; + free(str); + + return RValue_makeUndefined(); +} + +static RValue builtinFileTextEof(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(true); + Runner* runner = (Runner*) ctx->runner; + int32_t handle = RValue_toInt32(args[0]); + if (0 > handle || handle >= MAX_OPEN_TEXT_FILES || !runner->openTextFiles[handle].isOpen) return RValue_makeBool(true); + + OpenTextFile* file = &runner->openTextFiles[handle]; + return RValue_makeBool(file->readPos >= file->contentLen); +} + +static RValue builtinFileDelete(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + const char* path = (args[0].type == RVALUE_STRING ? args[0].string : ""); + Runner* runner = (Runner*) ctx->runner; + FileSystem* fs = runner->fileSystem; + fs->vtable->deleteFile(fs, path); + return RValue_makeUndefined(); +} + +// Keyboard functions +static RValue builtinKeyboardCheck(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + int32_t key = RValue_toInt32(args[0]); + return RValue_makeBool(RunnerKeyboard_check(runner->keyboard, key)); +} + +static RValue builtinKeyboardCheckPressed(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + int32_t key = RValue_toInt32(args[0]); + return RValue_makeBool(RunnerKeyboard_checkPressed(runner->keyboard, key)); +} + +static RValue builtinKeyboardCheckReleased(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + int32_t key = RValue_toInt32(args[0]); + return RValue_makeBool(RunnerKeyboard_checkReleased(runner->keyboard, key)); +} + +static RValue builtinKeyboardCheckDirect(VMContext* ctx, RValue* args, int32_t argCount) { + // keyboard_check_direct is the same as keyboard_check for our purposes + return builtinKeyboardCheck(ctx, args, argCount); +} + +static RValue builtinKeyboardKeyPress(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t key = RValue_toInt32(args[0]); + RunnerKeyboard_simulatePress(runner->keyboard, key); + return RValue_makeUndefined(); +} + +static RValue builtinKeyboardKeyRelease(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t key = RValue_toInt32(args[0]); + RunnerKeyboard_simulateRelease(runner->keyboard, key); + return RValue_makeUndefined(); +} + +static RValue builtinKeyboardClear(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t key = RValue_toInt32(args[0]); + RunnerKeyboard_clear(runner->keyboard, key); + return RValue_makeUndefined(); +} + +// ===[ Joystick Functions ]=== +static RValue builtinJoystickExists(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); + int32_t id = RValue_toInt32(args[0]) - 1; + return RValue_makeBool(RunnerGamepad_isConnected(runner->gamepads, id)); +} + +static RValue builtinJoystickXpos(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t id = RValue_toInt32(args[0]) - 1; + return RValue_makeReal((GMLReal) RunnerGamepad_axisValue(runner->gamepads, id, GP_AXIS_LH)); +} + +static RValue builtinJoystickYpos(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t id = RValue_toInt32(args[0]) - 1; + return RValue_makeReal((GMLReal) RunnerGamepad_axisValue(runner->gamepads, id, GP_AXIS_LV)); +} + +static RValue builtinJoystickDirection(VMContext* ctx, RValue* args, int32_t argCount) { + // Returns the joystick direction + if (1 > argCount) return RValue_makeReal(101.0); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(101.0); + int32_t id = RValue_toInt32(args[0]) - 1; + float haxis = RunnerGamepad_axisValue(runner->gamepads, id, GP_AXIS_LH); + float vaxis = RunnerGamepad_axisValue(runner->gamepads, id, GP_AXIS_LV); + + int32_t dir = 0; + if (vaxis < -0.3f) { + dir = 6; + } else if (vaxis > 0.3f) { + dir = 0; + } else { + dir = 3; + } + + if (haxis < -0.3f) { + dir += 1; + } else if (haxis > 0.3f) { + dir += 3; + } else { + dir += 2; + } + + return RValue_makeReal(96 + dir); +} + +static RValue builtinJoystickPov(VMContext* ctx, RValue* args, int32_t argCount) { + // Returns the D-pad/POV hat angle in degrees (0=up, 90=right, 180=down, 270=left), + if (1 > argCount) return RValue_makeReal(-1.0); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(-1.0); + int32_t id = RValue_toInt32(args[0]) - 1; + RunnerGamepadState* gp = runner->gamepads; + bool up = RunnerGamepad_buttonCheck(gp, id, GP_PADU); + bool down = RunnerGamepad_buttonCheck(gp, id, GP_PADD); + bool left = RunnerGamepad_buttonCheck(gp, id, GP_PADL); + bool right = RunnerGamepad_buttonCheck(gp, id, GP_PADR); + if (!up && !down && !left && !right) return RValue_makeReal(-1.0); + if (up && right) return RValue_makeReal(45.0); + if (right && down) return RValue_makeReal(135.0); + if (down && left) return RValue_makeReal(225.0); + if (left && up) return RValue_makeReal(315.0); + if (up) return RValue_makeReal(0.0); + if (right) return RValue_makeReal(90.0); + if (down) return RValue_makeReal(180.0); + if (left) return RValue_makeReal(270.0); + return RValue_makeReal(-1.0); +} + +static RValue builtinJoystickCheckButton(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); + int32_t id = RValue_toInt32(args[0]) - 1; + int32_t button = RawToGPUndertale(RValue_toInt32(args[1])); //UNDERTALE HACK + return RValue_makeBool(RunnerGamepad_buttonCheck(runner->gamepads, id, button)); +} + +static RValue builtinJoystickHasPov(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeBool(false); + int32_t id = RValue_toInt32(args[0]) - 1; + return RValue_makeBool(RunnerGamepad_isConnected(runner->gamepads, id)); +} + +static RValue builtinJoystickButtons(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t id = RValue_toInt32(args[0]) - 1; + if (!RunnerGamepad_isConnected(runner->gamepads, id)) return RValue_makeReal(0.0); + return RValue_makeReal(GP_BUTTON_COUNT); +} + +static RValue builtinJoystickName(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeOwnedString(safeStrdup("")); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeOwnedString(safeStrdup("")); + int32_t id = RValue_toInt32(args[0]) - 1; + return RValue_makeOwnedString(safeStrdup(RunnerGamepad_getDescription(runner->gamepads, id))); +} + +static RValue builtinJoystickAxes(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->gamepads == NULL) return RValue_makeReal(0.0); + int32_t id = RValue_toInt32(args[0]) - 1; + return RValue_makeReal(RunnerGamepad_getAxisCount(runner->gamepads, id)); +} + +// Window stubs +STUB_RETURN_ZERO(window_get_fullscreen) +STUB_RETURN_UNDEFINED(window_set_fullscreen) +STUB_RETURN_UNDEFINED(window_set_size) +STUB_RETURN_UNDEFINED(window_center) +static RValue builtinWindowGetWidth(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + return RValue_makeReal((GMLReal) ctx->dataWin->gen8.defaultWindowWidth); +} + +static RValue builtinWindowGetHeight(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + return RValue_makeReal((GMLReal) ctx->dataWin->gen8.defaultWindowHeight); +} + +static RValue builtinWindowSetCaption(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + char* val = RValue_toString(args[0]); + + Runner* runner = (Runner*) ctx->runner; + if (runner->setWindowTitle) { + runner->setWindowTitle(runner->nativeWindow, val); + printf("GL: Window title set to: %s\n", val); + } + + free(val); + return RValue_makeUndefined(); +} + +static RValue builtinWindowHasFocus(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner != nullptr && runner->windowHasFocus) { + return RValue_makeBool(runner->windowHasFocus(runner->nativeWindow)); + } + + return RValue_makeBool(true); +} + +// ===[ Game State Functions ]=== +static RValue builtinGameRestart(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + ctx->runner->pendingRoom = ROOM_RESTARTGAME; + return RValue_makeUndefined(); +} + +static RValue builtinGameEnd(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + runner->shouldExit = true; + return RValue_makeUndefined(); +} +STUB_RETURN_UNDEFINED(game_save) +STUB_RETURN_UNDEFINED(game_load) + +static RValue builtinInstanceNumber(VMContext* ctx, MAYBE_UNUSED RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + int32_t objectIndex = RValue_toInt32(args[0]); + int32_t count = 0; + int32_t snapBase = Runner_pushInstancesOfObject(runner, objectIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + if (runner->instanceSnapshots[i]->active) count++; + } + Runner_popInstanceSnapshot(runner, snapBase); + return RValue_makeReal((GMLReal) count); +} + +static RValue builtinInstanceFind(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(INSTANCE_NOONE); + Runner* runner = (Runner*) ctx->runner; + int32_t objectIndex = RValue_toInt32(args[0]); + int32_t n = RValue_toInt32(args[1]); + int32_t count = 0; + int32_t resultId = INSTANCE_NOONE; + int32_t snapBase = Runner_pushInstancesOfObject(runner, objectIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (!inst->active) continue; + if (count == n) { resultId = inst->instanceId; break; } + count++; + } + Runner_popInstanceSnapshot(runner, snapBase); + return RValue_makeReal((GMLReal) resultId); +} + +static RValue builtinInstanceNearest(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeReal(INSTANCE_NOONE); + Runner* runner = (Runner*) ctx->runner; + GMLReal x = RValue_toReal(args[0]); + GMLReal y = RValue_toReal(args[1]); + GMLReal bestDistSq = 0.0; + int32_t objectIndex = RValue_toInt32(args[2]); + int32_t resultId = INSTANCE_NOONE; + int32_t snapBase = Runner_pushInstancesOfObject(runner, objectIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (!inst->active) continue; + + GMLReal dx = inst->x - x; + GMLReal dy = inst->y - y; + GMLReal distSq = dx * dx + dy * dy; + + if (resultId == INSTANCE_NOONE || distSq < bestDistSq) { + resultId = inst->instanceId; + bestDistSq = distSq; + } + } + Runner_popInstanceSnapshot(runner, snapBase); + return RValue_makeReal((GMLReal) resultId); +} + +static RValue builtinInstanceExists(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + bool found = false; + if (id >= 0 && runner->dataWin->objt.count > (uint32_t) id) { + int32_t snapBase = Runner_pushInstancesOfObject(runner, id); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + if (runner->instanceSnapshots[i]->active) { found = true; break; } + } + Runner_popInstanceSnapshot(runner, snapBase); + } else { + // Instance ID: search for a specific instance + Instance* inst = hmget(runner->instancesById, id); + found = (inst != nullptr && inst->active); + } + return RValue_makeBool(found); +} + +static RValue builtinInstanceDestroy(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (1 > argCount) { + // No args: destroy the current instance + if (ctx->currentInstance != nullptr) { + Runner_destroyInstance(runner, (Instance*) ctx->currentInstance); + } + return RValue_makeUndefined(); + } + // 1 arg: find and destroy matching instances. Destroy events run user code that can spawn/destroy/instance_change other instances; iterate a snapshot of the bucket so those mutations don't corrupt our loop. + int32_t id = RValue_toInt32(args[0]); + if (id >= 0 && runner->dataWin->objt.count > (uint32_t) id) { + int32_t snapBase = Runner_pushInstancesOfObject(runner, id); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (inst->active) Runner_destroyInstance(runner, inst); + } + Runner_popInstanceSnapshot(runner, snapBase); + } else { + Instance* inst = hmget(runner->instancesById, id); + if (inst != nullptr && inst->active) Runner_destroyInstance(runner, inst); + } + return RValue_makeUndefined(); +} + +static RValue builtinInstanceCreate(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + GMLReal x = RValue_toReal(args[0]); + GMLReal y = RValue_toReal(args[1]); + int32_t objectIndex = RValue_toInt32(args[2]); + if (0 > objectIndex || runner->dataWin->objt.count <= (uint32_t) objectIndex) { + fprintf(stderr, "VM: instance_create: objectIndex %d out of range\n", objectIndex); + return RValue_makeReal(0.0); + } + Instance* callerInst = (Instance*) ctx->currentInstance; + Instance* inst = Runner_createInstance(runner, x, y, objectIndex); + if (inst == nullptr) return RValue_makeReal(INSTANCE_NOONE); + if (callerInst != nullptr && ctx->creatorVarID >= 0) { + Instance_setSelfVar(inst, ctx->creatorVarID, RValue_makeReal((GMLReal) callerInst->instanceId)); + } + return RValue_makeReal((GMLReal) inst->instanceId); +} + +static RValue builtinInstanceCopy(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + Instance* source = (Instance*) ctx->currentInstance; + if (source == nullptr) { + fprintf(stderr, "VM: instance_copy: no current instance\n"); + return RValue_makeReal(INSTANCE_NOONE); + } + bool performEvent = argCount > 0 ? RValue_toBool(args[0]) : false; + Instance* inst = Runner_copyInstance(runner, source, performEvent); + if (inst == nullptr) return RValue_makeReal(INSTANCE_NOONE); + return RValue_makeReal((GMLReal) inst->instanceId); +} + +static RValue builtinInstanceCreateLayer(VMContext* ctx, RValue* args, int32_t argCount) { + if (4 > argCount) return RValue_makeReal(INSTANCE_NOONE); + Runner* runner = (Runner*) ctx->runner; + GMLReal x = RValue_toReal(args[0]); + GMLReal y = RValue_toReal(args[1]); + int32_t layerId = resolveLayerIdArg(runner, args[2]); + int32_t objectIndex = RValue_toInt32(args[3]); + + Instance* inst = Runner_createInstanceWithLayer(runner, x, y, objectIndex, layerId); + if (inst == nullptr) return RValue_makeReal(INSTANCE_NOONE); + + Instance* callerInst = (Instance*) ctx->currentInstance; + if (callerInst != nullptr && ctx->creatorVarID >= 0) { + Instance_setSelfVar(inst, ctx->creatorVarID, RValue_makeReal((GMLReal) callerInst->instanceId)); + } + + return RValue_makeReal((GMLReal) inst->instanceId); +} + +static RValue builtinInstanceCreateDepth(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + GMLReal x = RValue_toReal(args[0]); + GMLReal y = RValue_toReal(args[1]); + int32_t depth = RValue_toInt32(args[2]); + int32_t objectIndex = RValue_toInt32(args[3]); + if (0 > objectIndex || runner->dataWin->objt.count <= (uint32_t) objectIndex) { + fprintf(stderr, "VM: instance_create: objectIndex %d out of range\n", objectIndex); + return RValue_makeReal(0.0); + } + Instance* callerInst = (Instance*) ctx->currentInstance; + Instance* inst = Runner_createInstanceWithDepth(runner, x, y, objectIndex, depth); + if (inst == nullptr) return RValue_makeReal(INSTANCE_NOONE); + if (callerInst != nullptr && ctx->creatorVarID >= 0) { + Instance_setSelfVar(inst, ctx->creatorVarID, RValue_makeReal((GMLReal) callerInst->instanceId)); + } + return RValue_makeReal((GMLReal) inst->instanceId); +} + +static RValue builtinInstanceChange(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr) return RValue_makeUndefined(); + + int32_t objectIndex = RValue_toInt32(args[0]); + bool performEvents = RValue_toBool(args[1]); + + if (0 > objectIndex || (uint32_t) objectIndex >= runner->dataWin->objt.count) { + fprintf(stderr, "VM: instance_change: objectIndex %d out of range\n", objectIndex); + return RValue_makeUndefined(); + } + + // Fire destroy event on old object if requested + if (performEvents) { + Runner_executeEvent(runner, inst, EVENT_DESTROY, 0); + } + + // Move the instance between per-object lists before mutating objectIndex so the remove walks the old parent chain and the add walks the new one. + Runner_removeInstanceFromObjectLists(runner, inst); + + // Change object index and copy properties from new object definition + GameObject* newObjDef = &runner->dataWin->objt.objects[objectIndex]; + inst->objectIndex = objectIndex; + Runner_addInstanceToObjectLists(runner, inst); + inst->spriteIndex = newObjDef->spriteId; + inst->visible = newObjDef->visible; + inst->solid = newObjDef->solid; + inst->persistent = newObjDef->persistent; + inst->depth = newObjDef->depth; + inst->maskIndex = newObjDef->textureMaskId; + inst->imageIndex = 0.0; + // The instance pointer is unchanged so this is just a depth shift, not a structural change. + runner->drawableListSortDirty = true; + + // Fire create event on new object if requested + if (performEvents) { + Runner_executeEvent(runner, inst, EVENT_CREATE, 0); + } + + return RValue_makeUndefined(); +} + +static RValue builtinInstanceDeactivateAll(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + bool notme = RValue_toBool(args[0]); + + int instances = arrlen(ctx->runner->instances); + repeat(instances, i) { + Instance* instance = ctx->runner->instances[i]; + + if (!notme || instance != ctx->currentInstance) { + instance->active = false; + } + } + return RValue_makeUndefined(); +} + +static RValue builtinInstanceActivateAll(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + int instances = arrlen(ctx->runner->instances); + repeat(instances, i) { + Instance* instance = ctx->runner->instances[i]; + if (!instance->destroyed) + ctx->runner->instances[i]->active = true; + } + return RValue_makeUndefined(); +} + +static RValue builtinInstanceActivateObject(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t objIndex = RValue_toInt32(args[0]); + + // Per-object buckets retain inactive (deactivated) instances since we only remove on destroy-cleanup, so this still finds them. INSTANCE_ALL falls back to the full instances list. + int32_t snapBase = Runner_pushInstancesForTarget(runner, objIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* instance = runner->instanceSnapshots[i]; + if (!instance->active && !instance->destroyed) instance->active = true; + } + Runner_popInstanceSnapshot(runner, snapBase); + return RValue_makeUndefined(); +} + +static RValue builtinInstanceDeactivateObject(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t objIndex = RValue_toInt32(args[0]); + + int32_t snapBase = Runner_pushInstancesForTarget(runner, objIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* instance = runner->instanceSnapshots[i]; + if (instance->active && !instance->destroyed) instance->active = false; + } + Runner_popInstanceSnapshot(runner, snapBase); + return RValue_makeUndefined(); +} + +static RValue builtinEventInherited(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr || 0 > ctx->currentEventObjectIndex || 0 > ctx->currentEventType) { + fprintf(stderr, "VM: event_inherited called with no event context (inst=%p, eventObjIdx=%d, eventType=%d)\n", (void*) inst, ctx->currentEventObjectIndex, ctx->currentEventType); + return RValue_makeReal(0.0); + } + + DataWin* dataWin = ctx->dataWin; + int32_t ownerObjectIndex = ctx->currentEventObjectIndex; + if ((uint32_t) ownerObjectIndex >= dataWin->objt.count) { + fprintf(stderr, "VM: event_inherited ownerObjectIndex %d out of range\n", ownerObjectIndex); + return RValue_makeReal(0.0); + } + + int32_t parentObjectIndex = dataWin->objt.objects[ownerObjectIndex].parentId; + if (ctx->traceEventInherited) { + fprintf(stderr, "VM: [%s] event_inherited owner=%s(%d) parent=%s(%d) event=%s (instanceId=%d)\n", dataWin->objt.objects[inst->objectIndex].name, dataWin->objt.objects[ownerObjectIndex].name, ownerObjectIndex, (0 > parentObjectIndex) ? "none" : dataWin->objt.objects[parentObjectIndex].name, parentObjectIndex, Runner_getEventName(ctx->currentEventType, ctx->currentEventSubtype), inst->instanceId); + } + if (0 > parentObjectIndex) return RValue_makeReal(0.0); + + Runner_executeEventFromObject(runner, inst, parentObjectIndex, ctx->currentEventType, ctx->currentEventSubtype); + return RValue_makeReal(0.0); +} + +static RValue builtinEventUser(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr) return RValue_makeReal(0.0); + + int32_t subevent = RValue_toInt32(args[0]); + if (0 > subevent || 15 < subevent) return RValue_makeReal(0.0); + + Runner_executeEvent(runner, inst, EVENT_OTHER, OTHER_USER0 + subevent); + return RValue_makeReal(0.0); +} + +static RValue builtinEventPerform(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr) return RValue_makeReal(0.0); + + int32_t eventType = RValue_toInt32(args[0]); + int32_t eventSubtype = RValue_toInt32(args[1]); + + Runner_executeEvent(runner, inst, eventType, eventSubtype); + return RValue_makeReal(0.0); +} + +static RValue builtinActionKillObject(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (ctx->currentInstance != nullptr) { + Runner_destroyInstance(runner, (Instance*) ctx->currentInstance); + } + return RValue_makeUndefined(); +} + +static RValue builtinActionCreateObject(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t objectIndex = RValue_toInt32(args[0]); + GMLReal x = RValue_toReal(args[1]); + GMLReal y = RValue_toReal(args[2]); + if (0 > objectIndex || runner->dataWin->objt.count <= (uint32_t) objectIndex) { + fprintf(stderr, "VM: action_create_object: objectIndex %d out of range\n", objectIndex); + return RValue_makeUndefined(); + } + Instance* callerInst = (Instance*) ctx->currentInstance; + if (ctx->actionRelativeFlag && callerInst != nullptr) { + x += callerInst->x; + y += callerInst->y; + } + Instance* inst = Runner_createInstance(runner, x, y, objectIndex); + if (callerInst != nullptr && ctx->creatorVarID >= 0) { + Instance_setSelfVar(inst, ctx->creatorVarID, RValue_makeReal((GMLReal) callerInst->instanceId)); + } + return RValue_makeUndefined(); +} + +static RValue builtinActionSetRelative(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + ctx->actionRelativeFlag = RValue_toInt32(args[0]) != 0; + return RValue_makeUndefined(); +} + +static RValue builtinActionMove(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + // action_move(direction_string, speed) + // Direction string is 9 chars of '0'/'1' encoding a 3x3 direction grid: + // Pos: 0=UL(225) 1=U(270) 2=UR(315) 3=L(180) 4=STOP 5=R(0) 6=DL(135) 7=D(90) 8=DR(45) + char* dirs = RValue_toString(args[0]); + GMLReal spd = RValue_toReal(args[1]); + + static const GMLReal angles[] = {225, 270, 315, 180, -1, 0, 135, 90, 45}; + + // Collect all enabled directions + int candidates[9]; + int count = 0; + for (int i = 0; 9 > i && dirs[i] != '\0'; i++) { + if (dirs[i] == '1') { + candidates[count++] = i; + } + } + + if (count == 0) { + free(dirs); + return RValue_makeUndefined(); + } + + // Pick one at random + int pick = candidates[0 == count - 1 ? 0 : rand() % count]; + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + if (4 == pick) { + // STOP + if (ctx->actionRelativeFlag) { + inst->speed += (float) spd; + } else { + inst->speed = 0; + } + } else { + GMLReal angle = angles[pick]; + if (ctx->actionRelativeFlag) { + inst->direction += (float) angle; + inst->speed += (float) spd; + } else { + inst->direction = (float) angle; + inst->speed = (float) spd; + } + } + Instance_computeComponentsFromSpeed(inst); + } + free(dirs); + return RValue_makeUndefined(); +} + +static RValue builtinActionMoveTo(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal ax = RValue_toReal(args[0]); + GMLReal ay = RValue_toReal(args[1]); + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + if (ctx->actionRelativeFlag) { + inst->x += (float) ax; + inst->y += (float) ay; + } else { + inst->x = (float) ax; + inst->y = (float) ay; + } + SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); + } + return RValue_makeUndefined(); +} + +static RValue builtinActionSnap(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal hsnap = RValue_toReal(args[0]); + GMLReal vsnap = RValue_toReal(args[1]); + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + if (hsnap > 0.0) { + inst->x = (float) ((int32_t) GMLReal_round(inst->x / hsnap) * hsnap); + SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); + } + if (vsnap > 0.0) { + inst->y = (float) ((int32_t) GMLReal_round(inst->y / vsnap) * vsnap); + SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); + } + } + return RValue_makeUndefined(); +} + +static RValue builtinActionSetFriction(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal val = RValue_toReal(args[0]); + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + if (ctx->actionRelativeFlag) { + inst->friction += (float) val; + } else { + inst->friction = (float) val; + } + } + return RValue_makeUndefined(); +} + +static RValue builtinActionSetGravity(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal dir = RValue_toReal(args[0]); + GMLReal grav = RValue_toReal(args[1]); + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + if (ctx->actionRelativeFlag) { + inst->gravityDirection += (float) dir; + inst->gravity += (float) grav; + } else { + inst->gravityDirection = (float) dir; + inst->gravity = (float) grav; + } + } + return RValue_makeUndefined(); +} + +static RValue builtinActionSetHspeed(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal val = RValue_toReal(args[0]); + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + if (ctx->actionRelativeFlag) { + inst->hspeed += (float) val; + } else { + inst->hspeed = (float) val; + } + Instance_computeSpeedFromComponents(inst); + } + return RValue_makeUndefined(); +} + +static RValue builtinActionSetVspeed(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + GMLReal val = RValue_toReal(args[0]); + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + if (ctx->actionRelativeFlag) { + inst->vspeed += (float) val; + } else { + inst->vspeed = (float) val; + } + Instance_computeSpeedFromComponents(inst); + } + return RValue_makeUndefined(); +} + +// ===[ GML BUFFER SYSTEM ]=== + +static int32_t gmlBufferCreate(Runner* runner, int32_t size, int32_t type, int32_t alignment) { + GmlBuffer buf = {0}; + buf.size = size > 0 ? size : 1; + buf.data = safeCalloc((size_t) buf.size, 1); + buf.position = 0; + buf.usedSize = (type == GML_BUFFER_GROW) ? 0 : buf.size; + buf.alignment = alignment > 0 ? alignment : 1; + buf.type = type; + buf.isValid = true; + int32_t id = (int32_t) arrlen(runner->gmlBufferPool); + arrput(runner->gmlBufferPool, buf); + return id; +} + +static GmlBuffer* gmlBufferGet(Runner* runner, int32_t id) { + if (0 > id || id >= (int32_t) arrlen(runner->gmlBufferPool)) return nullptr; + GmlBuffer* buf = &runner->gmlBufferPool[id]; + if (!buf->isValid) return nullptr; + return buf; +} + +// Aligns position up to the buffer's alignment boundary +static int32_t gmlBufferAlign(int32_t position, int32_t alignment) { + if (1 >= alignment) return position; + return ((position + alignment - 1) / alignment) * alignment; +} + +// Ensures the grow buffer has at least newSize bytes allocated +static void gmlBufferEnsureSize(GmlBuffer* buf, int32_t newSize) { + if (buf->type != GML_BUFFER_GROW || newSize <= buf->size) return; + // Double or use newSize, whichever is larger + int32_t newAlloc = buf->size * 2; + if (newAlloc < newSize) newAlloc = newSize; + buf->data = safeRealloc(buf->data, (size_t) newAlloc); + memset(buf->data + buf->size, 0, (size_t) (newAlloc - buf->size)); + buf->size = newAlloc; +} + +static RValue builtin_bufferCreate(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t size = RValue_toInt32(args[0]); + int32_t type = RValue_toInt32(args[1]); + int32_t alignment = RValue_toInt32(args[2]); + int32_t id = gmlBufferCreate(runner, size, type, alignment); + return RValue_makeReal((GMLReal) id); +} + +static RValue builtin_bufferDelete(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + GmlBuffer* buf = gmlBufferGet(runner, id); + if (buf != nullptr) { + free(buf->data); + buf->data = nullptr; + buf->isValid = false; + } + return RValue_makeUndefined(); +} + +static RValue builtin_bufferWrite(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + int32_t dataType = RValue_toInt32(args[1]); + GmlBuffer* buf = gmlBufferGet(runner, id); + if (buf == nullptr) return RValue_makeUndefined(); + + switch (dataType) { + case GML_BUFTYPE_U8: + case GML_BUFTYPE_BOOL: { + uint8_t val = (uint8_t) RValue_toInt32(args[2]); + gmlBufferEnsureSize(buf, buf->position + 1); + if (buf->size > buf->position) buf->data[buf->position] = val; + buf->position += 1; + break; + } + case GML_BUFTYPE_S8: { + int8_t val = (int8_t) RValue_toInt32(args[2]); + gmlBufferEnsureSize(buf, buf->position + 1); + if (buf->size > buf->position) buf->data[buf->position] = (uint8_t) val; + buf->position += 1; + break; + } + case GML_BUFTYPE_U16: { + uint16_t val = (uint16_t) RValue_toInt32(args[2]); + gmlBufferEnsureSize(buf, buf->position + 2); + if (buf->position + 2 <= buf->size) { + BinaryUtils_writeUint16(buf->data + buf->position, val); + } + buf->position += 2; + break; + } + case GML_BUFTYPE_S16: { + int16_t val = (int16_t) RValue_toInt32(args[2]); + gmlBufferEnsureSize(buf, buf->position + 2); + if (buf->position + 2 <= buf->size) { + BinaryUtils_writeUint16(buf->data + buf->position, (uint16_t) val); + } + buf->position += 2; + break; + } + case GML_BUFTYPE_U32: + case GML_BUFTYPE_S32: { + int32_t val = RValue_toInt32(args[2]); + gmlBufferEnsureSize(buf, buf->position + 4); + if (buf->position + 4 <= buf->size) { + BinaryUtils_writeUint32(buf->data + buf->position, (uint32_t) val); + } + buf->position += 4; + break; + } + case GML_BUFTYPE_F32: { + float val = (float) RValue_toReal(args[2]); + gmlBufferEnsureSize(buf, buf->position + 4); + if (buf->position + 4 <= buf->size) { + BinaryUtils_writeFloat32(buf->data + buf->position, val); + } + buf->position += 4; + break; + } + case GML_BUFTYPE_F64: { + double val = (double) RValue_toReal(args[2]); + gmlBufferEnsureSize(buf, buf->position + 8); + if (buf->position + 8 <= buf->size) { + BinaryUtils_writeFloat64(buf->data + buf->position, val); + } + buf->position += 8; + break; + } + case GML_BUFTYPE_STRING: { + // Writes string bytes + null terminator + char* str = RValue_toString(args[2]); + int32_t len = (int32_t) strlen(str); + int32_t writeLen = len + 1; // include null terminator + gmlBufferEnsureSize(buf, buf->position + writeLen); + if (buf->position + writeLen <= buf->size) { + memcpy(buf->data + buf->position, str, (size_t) writeLen); + } + buf->position += writeLen; + free(str); + break; + } + case GML_BUFTYPE_TEXT: { + // Writes string bytes WITHOUT null terminator + char* str = RValue_toString(args[2]); + int32_t len = (int32_t) strlen(str); + gmlBufferEnsureSize(buf, buf->position + len); + if (buf->position + len <= buf->size) { + memcpy(buf->data + buf->position, str, (size_t) len); + } + buf->position += len; + free(str); + break; + } + default: + fprintf(stderr, "buffer_write: unsupported data type %d\n", dataType); + break; + } + + buf->position = gmlBufferAlign(buf->position, buf->alignment); + if (buf->type == GML_BUFFER_GROW && buf->position > buf->usedSize) { + buf->usedSize = buf->position; + } + + return RValue_makeUndefined(); +} + +static RValue builtin_bufferRead(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + int32_t dataType = RValue_toInt32(args[1]); + GmlBuffer* buf = gmlBufferGet(runner, id); + if (buf == nullptr) return RValue_makeReal(0.0); + + RValue result = RValue_makeReal(0.0); + + switch (dataType) { + case GML_BUFTYPE_U8: + case GML_BUFTYPE_BOOL: { + if (buf->size > buf->position) { + result = RValue_makeReal((GMLReal) buf->data[buf->position]); + } + buf->position += 1; + break; + } + case GML_BUFTYPE_S8: { + if (buf->size > buf->position) { + result = RValue_makeReal((GMLReal) (int8_t) buf->data[buf->position]); + } + buf->position += 1; + break; + } + case GML_BUFTYPE_U16: { + if (buf->position + 2 <= buf->size) { + uint16_t val = BinaryUtils_readUint16(buf->data + buf->position); + result = RValue_makeReal((GMLReal) val); + } + buf->position += 2; + break; + } + case GML_BUFTYPE_S16: { + if (buf->position + 2 <= buf->size) { + result = RValue_makeReal((GMLReal) BinaryUtils_readInt16(buf->data + buf->position)); + } + buf->position += 2; + break; + } + case GML_BUFTYPE_U32: { + if (buf->position + 4 <= buf->size) { + uint32_t val = BinaryUtils_readUint32(buf->data + buf->position); + result = RValue_makeReal((GMLReal) val); + } + buf->position += 4; + break; + } + case GML_BUFTYPE_S32: { + if (buf->position + 4 <= buf->size) { + result = RValue_makeReal((GMLReal) BinaryUtils_readInt32(buf->data + buf->position)); + } + buf->position += 4; + break; + } + case GML_BUFTYPE_F32: { + if (buf->position + 4 <= buf->size) { + float val = BinaryUtils_readFloat32(buf->data + buf->position); + result = RValue_makeReal((GMLReal) val); + } + buf->position += 4; + break; + } + case GML_BUFTYPE_F64: { + if (buf->position + 8 <= buf->size) { + double val = BinaryUtils_readFloat64(buf->data + buf->position); + result = RValue_makeReal((GMLReal) val); + } + buf->position += 8; + break; + } + case GML_BUFTYPE_STRING: { + // Read until null terminator or end of buffer + int32_t start = buf->position; + while (buf->size > buf->position && buf->data[buf->position] != '\0') { + buf->position++; + } + int32_t len = buf->position - start; + char* str = safeMalloc((size_t) len + 1); + memcpy(str, buf->data + start, (size_t) len); + str[len] = '\0'; + // Skip past the null terminator + if (buf->size > buf->position) buf->position++; + result = RValue_makeOwnedString(str); + break; + } + case GML_BUFTYPE_TEXT: { + // Read all remaining bytes as text (no null terminator delimiter) + int32_t start = buf->position; + int32_t len = buf->size - start; + if (0 > len) len = 0; + char* str = safeMalloc((size_t) len + 1); + if (len > 0) memcpy(str, buf->data + start, (size_t) len); + str[len] = '\0'; + buf->position = buf->size; + result = RValue_makeOwnedString(str); + break; + } + default: + fprintf(stderr, "buffer_read: unsupported data type %d\n", dataType); + break; + } + + buf->position = gmlBufferAlign(buf->position, buf->alignment); + return result; +} + +static RValue builtin_bufferSeek(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + int32_t seekMode = RValue_toInt32(args[1]); + int32_t offset = RValue_toInt32(args[2]); + GmlBuffer* buf = gmlBufferGet(runner, id); + if (buf == nullptr) return RValue_makeUndefined(); + + switch (seekMode) { + case GML_BUFFER_SEEK_START: + buf->position = offset; + break; + case GML_BUFFER_SEEK_RELATIVE: + buf->position += offset; + break; + case GML_BUFFER_SEEK_END: { + int32_t endPos = (buf->type == GML_BUFFER_GROW) ? buf->usedSize : buf->size; + buf->position = endPos + offset; + break; + } + } + + // Clamp position + if (0 > buf->position) buf->position = 0; + if (buf->position > buf->size) buf->position = buf->size; + + return RValue_makeUndefined(); +} + +static RValue builtin_bufferTell(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + GmlBuffer* buf = gmlBufferGet(runner, id); + if (buf == nullptr) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) buf->position); +} + +static RValue builtin_bufferGetSize(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + GmlBuffer* buf = gmlBufferGet(runner, id); + if (buf == nullptr) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) ((buf->type == GML_BUFFER_GROW) ? buf->usedSize : buf->size)); +} + +static RValue builtin_bufferLoad(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + FileSystem* fs = runner->fileSystem; + char* filename = RValue_toString(args[0]); + + uint8_t* fileData = nullptr; + int32_t fileSize = 0; + bool ok = fs->vtable->readFileBinary(fs, filename, &fileData, &fileSize); + free(filename); + + if (!ok) return RValue_makeReal(-1.0); + + // Create a fixed buffer with the loaded data + int32_t id = gmlBufferCreate(runner, fileSize, GML_BUFFER_FIXED, 1); + GmlBuffer* buf = gmlBufferGet(runner, id); + free(buf->data); + buf->data = fileData; + buf->size = fileSize; + buf->usedSize = fileSize; + return RValue_makeReal((GMLReal) id); +} + +static RValue builtin_bufferSave(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + FileSystem* fs = runner->fileSystem; + int32_t id = RValue_toInt32(args[0]); + char* filename = RValue_toString(args[1]); + GmlBuffer* buf = gmlBufferGet(runner, id); + + if (buf != nullptr) { + int32_t saveSize = (buf->type == GML_BUFFER_GROW) ? buf->usedSize : buf->size; + fs->vtable->writeFileBinary(fs, filename, buf->data, saveSize); + } + + free(filename); + return RValue_makeUndefined(); +} + +STUB_RETURN_ZERO(buffer_base64_encode) + +// PSN stubs +STUB_RETURN_UNDEFINED(psn_init) +STUB_RETURN_ZERO(psn_default_user) +STUB_RETURN_ZERO(psn_get_leaderboard_score) + +// Draw functions +static RValue builtin_drawSprite(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + float x = (float) RValue_toReal(args[2]); + float y = (float) RValue_toReal(args[3]); + + // If subimg < 0, use the current instance's imageIndex + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + + Renderer_drawSprite(runner->renderer, spriteIndex, subimg, x, y); + return RValue_makeUndefined(); +} + +static RValue builtin_drawSpriteExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + float x = (float) RValue_toReal(args[2]); + float y = (float) RValue_toReal(args[3]); + float xscale = (float) RValue_toReal(args[4]); + float yscale = (float) RValue_toReal(args[5]); + float rot = (float) RValue_toReal(args[6]); + uint32_t color = (uint32_t) RValue_toInt32(args[7]); + float alpha = (float) RValue_toReal(args[8]); + + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + + Renderer_drawSpriteExt(runner->renderer, spriteIndex, subimg, x, y, xscale, yscale, rot, color, alpha); + return RValue_makeUndefined(); +} + +static RValue builtin_drawSpriteTiled(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + float x = (float) RValue_toReal(args[2]); + float y = (float) RValue_toReal(args[3]); + + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + + float roomW = (float) runner->currentRoom->width; + float roomH = (float) runner->currentRoom->height; + Renderer_drawSpriteTiled(runner->renderer, spriteIndex, subimg, x, y, 1.0f, 1.0f, roomW, roomH, 0xFFFFFF, runner->renderer->drawAlpha); + return RValue_makeUndefined(); +} + +static RValue builtin_drawSpriteTiledExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + float x = (float) RValue_toReal(args[2]); + float y = (float) RValue_toReal(args[3]); + float xscale = (float) RValue_toReal(args[4]); + float yscale = (float) RValue_toReal(args[5]); + uint32_t color = (uint32_t) RValue_toInt32(args[6]); + float alpha = (float) RValue_toReal(args[7]); + + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + + float roomW = (float) runner->currentRoom->width; + float roomH = (float) runner->currentRoom->height; + Renderer_drawSpriteTiled(runner->renderer, spriteIndex, subimg, x, y, xscale, yscale, roomW, roomH, color, alpha); + return RValue_makeUndefined(); +} + +static RValue builtin_drawSpriteStretched(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + float x = (float) RValue_toReal(args[2]); + float y = (float) RValue_toReal(args[3]); + float w = (float) RValue_toReal(args[4]); + float h = (float) RValue_toReal(args[5]); + + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + + Renderer_drawSpriteStretched(runner->renderer, spriteIndex, subimg, x, y, w, h, 0xFFFFFF, runner->renderer->drawAlpha); + return RValue_makeUndefined(); +} + +static RValue builtin_drawSpriteStretchedExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + float x = (float) RValue_toReal(args[2]); + float y = (float) RValue_toReal(args[3]); + float w = (float) RValue_toReal(args[4]); + float h = (float) RValue_toReal(args[5]); + uint32_t color = (uint32_t) RValue_toInt32(args[6]); + float alpha = (float) RValue_toReal(args[7]); + + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + + Renderer_drawSpriteStretched(runner->renderer, spriteIndex, subimg, x, y, w, h, color, alpha); + return RValue_makeUndefined(); +} + +static RValue builtin_drawSpritePart(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + int32_t left = RValue_toInt32(args[2]); + int32_t top = RValue_toInt32(args[3]); + int32_t width = RValue_toInt32(args[4]); + int32_t height = RValue_toInt32(args[5]); + float x = (float) RValue_toReal(args[6]); + float y = (float) RValue_toReal(args[7]); + + // If subimg < 0, use the current instance's imageIndex + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + + Renderer_drawSpritePart(runner->renderer, spriteIndex, subimg, left, top, width, height, x, y); + return RValue_makeUndefined(); +} + +static RValue builtin_drawSpritePartExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + int32_t left = RValue_toInt32(args[2]); + int32_t top = RValue_toInt32(args[3]); + int32_t width = RValue_toInt32(args[4]); + int32_t height = RValue_toInt32(args[5]); + float x = (float) RValue_toReal(args[6]); + float y = (float) RValue_toReal(args[7]); + float xscale = (float) RValue_toReal(args[8]); + float yscale = (float) RValue_toReal(args[9]); + uint32_t color = (uint32_t) RValue_toInt32(args[10]); + float alpha = (float) RValue_toReal(args[11]); + + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + +#ifdef __3DS__ + if (runner->osType == OS_3DS) { + int32_t asrielBgSprite = shget(runner->assetsByName, "spr_asrielbg"); + int32_t afinalBodyObject = shget(runner->assetsByName, "obj_afinal_body"); + bool isAfinalBodyDraw = ctx->currentInstance != nullptr && + ((Instance*) ctx->currentInstance)->objectIndex == afinalBodyObject; + if (spriteIndex == asrielBgSprite || isAfinalBodyDraw) { + N3DS_setAsrielRainbowInfoLed(runner); + } + } +#endif + + Renderer_drawSpritePartExt(runner->renderer, spriteIndex, subimg, left, top, width, height, x, y, xscale, yscale, 0.0f, 0.0f, 0.0f, color, alpha); + return RValue_makeUndefined(); +} + +static RValue builtin_drawSpriteGeneral(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + logSemiStubbedFunction(ctx, "draw_sprite_general"); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + int32_t left = RValue_toInt32(args[2]); + int32_t top = RValue_toInt32(args[3]); + int32_t width = RValue_toInt32(args[4]); + int32_t height = RValue_toInt32(args[5]); + float x = (float) RValue_toReal(args[6]); + float y = (float) RValue_toReal(args[7]); + float xscale = (float) RValue_toReal(args[8]); + float yscale = (float) RValue_toReal(args[9]); + float rot = (float) RValue_toReal(args[10]); + uint32_t c1 = (uint32_t) RValue_toInt32(args[11]); + uint32_t c2 = (uint32_t) RValue_toInt32(args[12]); + uint32_t c3 = (uint32_t) RValue_toInt32(args[13]); + uint32_t c4 = (uint32_t) RValue_toInt32(args[14]); + float alpha = (float) RValue_toReal(args[15]); + + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + + Renderer_drawSpritePartExt(runner->renderer, spriteIndex, subimg, left, top, width, height, x, y, xscale, yscale, rot, x, y, c1, alpha); + return RValue_makeUndefined(); +} + + +static RValue builtin_drawSpritePos(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t subimg = RValue_toInt32(args[1]); + float x1 = (float) RValue_toReal(args[2]); + float y1 = (float) RValue_toReal(args[3]); + float x2 = (float) RValue_toReal(args[4]); + float y2 = (float) RValue_toReal(args[5]); + float x3 = (float) RValue_toReal(args[6]); + float y3 = (float) RValue_toReal(args[7]); + float x4 = (float) RValue_toReal(args[8]); + float y4 = (float) RValue_toReal(args[9]); + float alpha = (float) RValue_toReal(args[10]); + + if (0 > subimg && ctx->currentInstance != nullptr) { + subimg = (int32_t) ((Instance*) ctx->currentInstance)->imageIndex; + } + + Renderer_drawSpritePos(runner->renderer, spriteIndex, subimg, x1, y1, x2, y2, x3, y3, x4, y4, alpha); + + return RValue_makeUndefined(); +} + +static RValue builtin_drawRectangle(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x1 = (float) RValue_toReal(args[0]); + float y1 = (float) RValue_toReal(args[1]); + float x2 = (float) RValue_toReal(args[2]); + float y2 = (float) RValue_toReal(args[3]); + bool outline = RValue_toBool(args[4]); + runner->renderer->vtable->drawRectangle(runner->renderer, x1, y1, x2, y2, runner->renderer->drawColor, runner->renderer->drawAlpha, outline); + return RValue_makeUndefined(); +} + +static RValue builtin_drawRectangleColor(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x1 = (float) RValue_toReal(args[0]); + float y1 = (float) RValue_toReal(args[1]); + float x2 = (float) RValue_toReal(args[2]); + float y2 = (float) RValue_toReal(args[3]); + uint32_t color = (uint32_t) RValue_toInt32(args[4]); + + bool outline = RValue_toBool(args[8]); + + runner->renderer->vtable->drawRectangle(runner->renderer, x1, y1, x2, y2, color, runner->renderer->drawAlpha, outline); + return RValue_makeUndefined(); +} + +static RValue builtin_drawHealthbar(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x1 = (float) RValue_toReal(args[0]); + float y1 = (float) RValue_toReal(args[1]); + float x2 = (float) RValue_toReal(args[2]); + float y2 = (float) RValue_toReal(args[3]); + float amount = (float) RValue_toReal(args[4]); + + amount = amount / (float)100; // 0 - 1; + float healthbarX = (x1 * (1-amount) + x2 * amount); + //float healthbarY = (y1 * (1-amount) + y2 * amount); + + uint32_t backCol = (uint32_t) RValue_toInt32(args[5]); + uint32_t minCol = (uint32_t) RValue_toInt32(args[6]); + uint32_t maxCol = (uint32_t) RValue_toInt32(args[7]); + uint32_t intermediateColor = (uint32_t) Color_lerp((int32_t) minCol, (int32_t) maxCol, amount); + + int32_t direction = RValue_toInt32(args[8]); + + bool showBack = RValue_toBool(args[9]); + + if (showBack) { + runner->renderer->vtable->drawRectangle(runner->renderer, x1,y1,x2,y2,backCol, runner->renderer->drawAlpha, false); + } + + runner->renderer->vtable->drawRectangle(runner->renderer,x1,y1,healthbarX,y2,intermediateColor, runner->renderer->drawAlpha, false); +} + +static RValue builtin_drawSetColor(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->drawColor = (uint32_t) RValue_toInt32(args[0]); + } + return RValue_makeUndefined(); +} + +static RValue builtin_drawClear(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + uint32_t color = (uint32_t) RValue_toInt32(args[0]); + runner->renderer->vtable->clearScreen(runner->renderer, color, 1.0f); + } + return RValue_makeUndefined(); +} + +static RValue builtin_drawClearAlpha(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + uint32_t color = (uint32_t) RValue_toInt32(args[0]); + float alpha = RValue_toReal(args[1]); + runner->renderer->vtable->clearScreen(runner->renderer, color, alpha); + } + return RValue_makeUndefined(); +} + +static RValue builtin_drawSetAlpha(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->drawAlpha = (float) RValue_toReal(args[0]); + } + return RValue_makeUndefined(); +} + +static RValue builtin_drawSetFont(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->drawFont = RValue_toInt32(args[0]); + } + return RValue_makeUndefined(); +} + +static RValue builtin_drawSetHalign(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->drawHalign = RValue_toInt32(args[0]); + } + return RValue_makeUndefined(); +} + +static RValue builtin_drawSetValign(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->drawValign = RValue_toInt32(args[0]); + } + return RValue_makeUndefined(); +} + +static bool shouldSkipDeltaruneTextFragment(Renderer* renderer, const char* text) { + if (renderer == nullptr || text == nullptr || text[0] == '\0') return false; + + // Full strings are handled by TextUtils_preprocessGmlTextIfNeeded(). This catches + // Deltarune's typewriter path, which can draw "\E?" textbox commands one byte at a time. + if (text[1] != '\0') { + renderer->textMarkupSawEscape = false; + renderer->textMarkupSkipChars = 0; + return false; + } + + if (renderer->textMarkupSkipChars > 0) { + renderer->textMarkupSkipChars--; + return true; + } + + if (renderer->textMarkupSawEscape) { + renderer->textMarkupSawEscape = false; + if (TextUtils_isDeltaruneInlineTextCommand(text[0])) { + renderer->textMarkupSkipChars = 1; + return true; + } + } + + if (text[0] == '\\') { + renderer->textMarkupSawEscape = true; + return true; + } + + renderer->textMarkupSawEscape = false; + return false; +} + +static float builtin_get3DSTopEnemyDialogueTextScale(Runner* runner) { +#ifdef __3DS__ + if (runner != NULL && + runner->osType == OS_3DS && + runner->renderer != NULL && + runner->n3dsDrawHasTopEnemyDialogue && + N3DSRenderer_isTopScreenGUIActive(runner->renderer)) { + return 2.0f; + } +#endif + return 1.0f; +} + +static void builtin_appendWrappedTextBytes(char** outText, int32_t* outLen, int32_t* outCap, const char* src, int32_t len) { + if (len <= 0) return; + int32_t needed = *outLen + len + 1; + if (needed > *outCap) { + while (needed > *outCap) *outCap *= 2; + *outText = (char*) realloc(*outText, (size_t) *outCap); + } + memcpy(*outText + *outLen, src, (size_t) len); + *outLen += len; + (*outText)[*outLen] = '\0'; +} + +static void builtin_appendWrappedTextChar(char** outText, int32_t* outLen, int32_t* outCap, char ch) { + builtin_appendWrappedTextBytes(outText, outLen, outCap, &ch, 1); +} + +static char* builtin_wrapProcessedTextForWidth(Renderer* renderer, const char* text, float wrapWidth, float xscale) { + if (renderer == NULL || text == NULL || wrapWidth <= 0.0f || fabsf(xscale) <= 0.0001f) return NULL; + + int32_t fontIndex = renderer->drawFont; + if (fontIndex < 0 || (uint32_t) fontIndex >= renderer->dataWin->font.count) return NULL; + + Font* font = &renderer->dataWin->font.fonts[fontIndex]; + float glyphScaleX = fabsf(xscale) * font->scaleX; + if (glyphScaleX <= 0.0001f) return NULL; + + float maxRawWidth = wrapWidth / glyphScaleX; + if (maxRawWidth <= 1.0f) return NULL; + + int32_t textLen = (int32_t) strlen(text); + int32_t outCap = textLen * 2 + 32; + char* outText = (char*) malloc((size_t) outCap); + int32_t outLen = 0; + bool changed = false; + outText[0] = '\0'; + + int32_t lineStart = 0; + while (lineStart <= textLen) { + int32_t lineEnd = lineStart; + while (lineEnd < textLen && !TextUtils_isNewlineChar(text[lineEnd])) lineEnd++; + + int32_t pos = lineStart; + float currentRawWidth = 0.0f; + bool lineHasText = false; + + while (pos < lineEnd) { + int32_t wsStart = pos; + while (pos < lineEnd && TextUtils_isWhitespaceChar(text[pos])) pos++; + int32_t wsLen = pos - wsStart; + + int32_t wordStart = pos; + while (pos < lineEnd && !TextUtils_isWhitespaceChar(text[pos])) pos++; + int32_t wordLen = pos - wordStart; + if (wordLen <= 0) continue; + + float wsWidth = (lineHasText && wsLen > 0) ? TextUtils_measureLineWidth(font, text + wsStart, wsLen) : 0.0f; + float wordWidth = TextUtils_measureLineWidth(font, text + wordStart, wordLen); + + if (lineHasText && currentRawWidth + wsWidth + wordWidth > maxRawWidth) { + builtin_appendWrappedTextChar(&outText, &outLen, &outCap, '\n'); + changed = true; + currentRawWidth = 0.0f; + lineHasText = false; + wsWidth = 0.0f; + } + + if (!lineHasText && wordWidth > maxRawWidth) { + int32_t chunkPos = wordStart; + while (chunkPos < wordStart + wordLen) { + int32_t nextPos = chunkPos; + uint16_t ch = TextUtils_decodeUtf8(text, wordStart + wordLen, &nextPos); + FontGlyph* glyph = TextUtils_findGlyph(font, ch); + float glyphWidth = glyph != NULL ? (float) glyph->shift : 0.0f; + + if (lineHasText && currentRawWidth + glyphWidth > maxRawWidth) { + builtin_appendWrappedTextChar(&outText, &outLen, &outCap, '\n'); + changed = true; + currentRawWidth = 0.0f; + lineHasText = false; + } + + builtin_appendWrappedTextBytes(&outText, &outLen, &outCap, text + chunkPos, nextPos - chunkPos); + currentRawWidth += glyphWidth; + lineHasText = true; + chunkPos = nextPos; + + if (chunkPos < wordStart + wordLen && currentRawWidth >= maxRawWidth) { + builtin_appendWrappedTextChar(&outText, &outLen, &outCap, '\n'); + changed = true; + currentRawWidth = 0.0f; + lineHasText = false; + } + } + continue; + } + + if (lineHasText && wsLen > 0) { + builtin_appendWrappedTextBytes(&outText, &outLen, &outCap, text + wsStart, wsLen); + currentRawWidth += wsWidth; + } + + builtin_appendWrappedTextBytes(&outText, &outLen, &outCap, text + wordStart, wordLen); + currentRawWidth += wordWidth; + lineHasText = true; + } + + if (lineEnd >= textLen) break; + builtin_appendWrappedTextChar(&outText, &outLen, &outCap, '\n'); + lineStart = TextUtils_skipNewline(text, lineEnd, textLen); + } + + if (!changed) { + free(outText); + return NULL; + } + + return outText; +} + +static RValue builtin_drawText(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x = (float) RValue_toReal(args[0]); + float y = (float) RValue_toReal(args[1]); + char* ownedStr = NULL; + const char* str = (args[2].type == RVALUE_STRING && args[2].string != nullptr) ? args[2].string : (ownedStr = RValue_toString(args[2])); + if (shouldSkipDeltaruneTextFragment(runner->renderer, str)) { + free(ownedStr); + return RValue_makeUndefined(); + } + + PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); + float dialogueTextScale = builtin_get3DSTopEnemyDialogueTextScale(runner); + runner->renderer->vtable->drawText(runner->renderer, processedText.text, x, y, dialogueTextScale, dialogueTextScale, 0.0f); + PreprocessedText_free(processedText); + free(ownedStr); + return RValue_makeUndefined(); +} + +static RValue builtin_drawTextTransformed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x = (float) RValue_toReal(args[0]); + float y = (float) RValue_toReal(args[1]); + char* ownedStr = NULL; + const char* str = (args[2].type == RVALUE_STRING && args[2].string != nullptr) ? args[2].string : (ownedStr = RValue_toString(args[2])); + float xscale = (float) RValue_toReal(args[3]); + float yscale = (float) RValue_toReal(args[4]); + float angle = (float) RValue_toReal(args[5]); + if (shouldSkipDeltaruneTextFragment(runner->renderer, str)) { + free(ownedStr); + return RValue_makeUndefined(); + } + + PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); + float dialogueTextScale = builtin_get3DSTopEnemyDialogueTextScale(runner); + runner->renderer->vtable->drawText(runner->renderer, processedText.text, x, y, xscale * dialogueTextScale, yscale * dialogueTextScale, angle); + PreprocessedText_free(processedText); + free(ownedStr); + return RValue_makeUndefined(); +} + +static RValue builtin_drawTextExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x = (float) RValue_toReal(args[0]); + float y = (float) RValue_toReal(args[1]); + char* ownedStr = NULL; + const char* str = (args[2].type == RVALUE_STRING && args[2].string != nullptr) ? args[2].string : (ownedStr = RValue_toString(args[2])); + int32_t separation = RValue_toInt32(args[3]); + int32_t width = RValue_toInt32(args[4]); + if (shouldSkipDeltaruneTextFragment(runner->renderer, str)) { + free(ownedStr); + return RValue_makeUndefined(); + } + + PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); + float dialogueTextScale = builtin_get3DSTopEnemyDialogueTextScale(runner); + char* wrappedText = builtin_wrapProcessedTextForWidth(runner->renderer, processedText.text, (float) width, dialogueTextScale); + const char* drawText = wrappedText != NULL ? wrappedText : processedText.text; + runner->renderer->vtable->drawText(runner->renderer, drawText, x, y, dialogueTextScale, dialogueTextScale, 0.0f); + free(wrappedText); + (void) separation; + PreprocessedText_free(processedText); + free(ownedStr); + return RValue_makeUndefined(); +} + +static RValue builtin_drawTextExtTransformed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x = (float) RValue_toReal(args[0]); + float y = (float) RValue_toReal(args[1]); + char* ownedStr = NULL; + const char* str = (args[2].type == RVALUE_STRING && args[2].string != nullptr) ? args[2].string : (ownedStr = RValue_toString(args[2])); + int32_t separation = RValue_toInt32(args[3]); + int32_t width = RValue_toInt32(args[4]); + float xscale = (float) RValue_toReal(args[5]); + float yscale = (float) RValue_toReal(args[6]); + float angle = (float) RValue_toReal(args[7]); + if (shouldSkipDeltaruneTextFragment(runner->renderer, str)) { + free(ownedStr); + return RValue_makeUndefined(); + } + + PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); + float dialogueTextScale = builtin_get3DSTopEnemyDialogueTextScale(runner); + char* wrappedText = builtin_wrapProcessedTextForWidth(runner->renderer, processedText.text, (float) width, xscale * dialogueTextScale); + const char* drawText = wrappedText != NULL ? wrappedText : processedText.text; + runner->renderer->vtable->drawText(runner->renderer, drawText, x, y, xscale * dialogueTextScale, yscale * dialogueTextScale, angle); + free(wrappedText); + (void) separation; + PreprocessedText_free(processedText); + free(ownedStr); + return RValue_makeUndefined(); +} + +static RValue builtin_drawTextColor(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x = (float) RValue_toReal(args[0]); + float y = (float) RValue_toReal(args[1]); + char* ownedStr = NULL; + const char* str = (args[2].type == RVALUE_STRING && args[2].string != nullptr) ? args[2].string : (ownedStr = RValue_toString(args[2])); + int32_t c1 = (float) RValue_toInt32(args[3]); + int32_t c2 = (float) RValue_toInt32(args[4]); + int32_t c3 = (float) RValue_toInt32(args[5]); + int32_t c4 = (float) RValue_toInt32(args[6]); + float alpha = (float) RValue_toReal(args[7]); + if (shouldSkipDeltaruneTextFragment(runner->renderer, str)) { + free(ownedStr); + return RValue_makeUndefined(); + } + + PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); + float dialogueTextScale = builtin_get3DSTopEnemyDialogueTextScale(runner); + runner->renderer->vtable->drawTextColor(runner->renderer, processedText.text, x, y, dialogueTextScale, dialogueTextScale, 0.0f, c1, c2, c3, c4, alpha); + PreprocessedText_free(processedText); + free(ownedStr); + return RValue_makeUndefined(); +} + +static RValue builtin_drawTextColorTransformed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x = (float) RValue_toReal(args[0]); + float y = (float) RValue_toReal(args[1]); + char* ownedStr = NULL; + const char* str = (args[2].type == RVALUE_STRING && args[2].string != nullptr) ? args[2].string : (ownedStr = RValue_toString(args[2])); + float xscale = (float) RValue_toReal(args[3]); + float yscale = (float) RValue_toReal(args[4]); + float angle = (float) RValue_toReal(args[5]); + int32_t c1 = (float) RValue_toInt32(args[6]); + int32_t c2 = (float) RValue_toInt32(args[7]); + int32_t c3 = (float) RValue_toInt32(args[8]); + int32_t c4 = (float) RValue_toInt32(args[9]); + float alpha = (float) RValue_toReal(args[10]); + if (shouldSkipDeltaruneTextFragment(runner->renderer, str)) { + free(ownedStr); + return RValue_makeUndefined(); + } + + PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); + float dialogueTextScale = builtin_get3DSTopEnemyDialogueTextScale(runner); + runner->renderer->vtable->drawTextColor(runner->renderer, processedText.text, x, y, xscale * dialogueTextScale, yscale * dialogueTextScale, angle, c1, c2, c3, c4, alpha); + PreprocessedText_free(processedText); + free(ownedStr); + return RValue_makeUndefined(); +} + +static RValue builtin_drawTextColorExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x = (float) RValue_toReal(args[0]); + float y = (float) RValue_toReal(args[1]); + char* ownedStr = NULL; + const char* str = (args[2].type == RVALUE_STRING && args[2].string != nullptr) ? args[2].string : (ownedStr = RValue_toString(args[2])); + int32_t c1 = (float) RValue_toInt32(args[5]); + int32_t c2 = (float) RValue_toInt32(args[6]); + int32_t c3 = (float) RValue_toInt32(args[7]); + int32_t c4 = (float) RValue_toInt32(args[8]); + float alpha = (float) RValue_toReal(args[9]); + if (shouldSkipDeltaruneTextFragment(runner->renderer, str)) { + free(ownedStr); + return RValue_makeUndefined(); + } + + PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); + float dialogueTextScale = builtin_get3DSTopEnemyDialogueTextScale(runner); + char* wrappedText = builtin_wrapProcessedTextForWidth(runner->renderer, processedText.text, (float) RValue_toInt32(args[4]), dialogueTextScale); + const char* drawText = wrappedText != NULL ? wrappedText : processedText.text; + runner->renderer->vtable->drawTextColor(runner->renderer, drawText, x, y, dialogueTextScale, dialogueTextScale, 0.0f, c1, c2, c3, c4, alpha); + free(wrappedText); + PreprocessedText_free(processedText); + free(ownedStr); + return RValue_makeUndefined(); +} + +static RValue builtin_drawTextColorExtTransformed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + float x = (float) RValue_toReal(args[0]); + float y = (float) RValue_toReal(args[1]); + char* ownedStr = NULL; + const char* str = (args[2].type == RVALUE_STRING && args[2].string != nullptr) ? args[2].string : (ownedStr = RValue_toString(args[2])); + float xscale = (float) RValue_toReal(args[5]); + float yscale = (float) RValue_toReal(args[6]); + float angle = (float) RValue_toReal(args[7]); + int32_t c1 = (float) RValue_toInt32(args[8]); + int32_t c2 = (float) RValue_toInt32(args[9]); + int32_t c3 = (float) RValue_toInt32(args[10]); + int32_t c4 = (float) RValue_toInt32(args[11]); + float alpha = (float) RValue_toReal(args[12]); + if (shouldSkipDeltaruneTextFragment(runner->renderer, str)) { + free(ownedStr); + return RValue_makeUndefined(); + } + + PreprocessedText processedText = TextUtils_preprocessGmlTextIfNeeded(runner, str); + float dialogueTextScale = builtin_get3DSTopEnemyDialogueTextScale(runner); + char* wrappedText = builtin_wrapProcessedTextForWidth(runner->renderer, processedText.text, (float) RValue_toInt32(args[4]), xscale * dialogueTextScale); + const char* drawText = wrappedText != NULL ? wrappedText : processedText.text; + runner->renderer->vtable->drawTextColor(runner->renderer, drawText, x, y, xscale * dialogueTextScale, yscale * dialogueTextScale, angle, c1, c2, c3, c4, alpha); + free(wrappedText); + PreprocessedText_free(processedText); + free(ownedStr); + return RValue_makeUndefined(); +} + +static RValue builtin_drawBackground(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr || 3 > argCount) return RValue_makeUndefined(); + + int32_t bgIndex = RValue_toInt32(args[0]); + float x = (float) RValue_toReal(args[1]); + float y = (float) RValue_toReal(args[2]); + + int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, bgIndex); + if (0 > tpagIndex) return RValue_makeUndefined(); + + runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, x, y, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0xFFFFFF, runner->renderer->drawAlpha); + return RValue_makeUndefined(); +} + +static RValue builtin_drawBackgroundExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr || 8 > argCount) return RValue_makeUndefined(); + + int32_t bgIndex = RValue_toInt32(args[0]); + float x = (float) RValue_toReal(args[1]); + float y = (float) RValue_toReal(args[2]); + float xscale = (float) RValue_toReal(args[3]); + float yscale = (float) RValue_toReal(args[4]); + float rot = (float) RValue_toReal(args[5]); + uint32_t color = (uint32_t) RValue_toInt32(args[6]); + float alpha = (float) RValue_toReal(args[7]); + + int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, bgIndex); + if (0 > tpagIndex) return RValue_makeUndefined(); + + runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, x, y, 0.0f, 0.0f, xscale, yscale, rot, color, alpha); + return RValue_makeUndefined(); +} + +static RValue builtin_drawBackgroundStretched(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr || 5 > argCount) return RValue_makeUndefined(); + + int32_t bgIndex = RValue_toInt32(args[0]); + float x = (float) RValue_toReal(args[1]); + float y = (float) RValue_toReal(args[2]); + float w = (float) RValue_toReal(args[3]); + float h = (float) RValue_toReal(args[4]); + + int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, bgIndex); + if (0 > tpagIndex) return RValue_makeUndefined(); + + TexturePageItem* tpag = &runner->dataWin->tpag.items[tpagIndex]; + float xscale = w / (float) tpag->boundingWidth; + float yscale = h / (float) tpag->boundingHeight; + + runner->renderer->vtable->drawSprite(runner->renderer, tpagIndex, x, y, 0.0f, 0.0f, xscale, yscale, 0.0f, 0xFFFFFF, runner->renderer->drawAlpha); + return RValue_makeUndefined(); +} + +static RValue builtin_drawBackgroundPartExt(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr || 11 > argCount) return RValue_makeUndefined(); + + int32_t bgIndex = RValue_toInt32(args[0]); + int32_t left = RValue_toInt32(args[1]); + int32_t top = RValue_toInt32(args[2]); + int32_t width = RValue_toInt32(args[3]); + int32_t height = RValue_toInt32(args[4]); + float x = (float) RValue_toReal(args[5]); + float y = (float) RValue_toReal(args[6]); + float xscale = (float) RValue_toReal(args[7]); + float yscale = (float) RValue_toReal(args[8]); + uint32_t color = (uint32_t) RValue_toInt32(args[9]); + float alpha = (float) RValue_toReal(args[10]); + + int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(runner->dataWin, bgIndex); + if (0 > tpagIndex) return RValue_makeUndefined(); + + runner->renderer->vtable->drawSpritePart(runner->renderer, tpagIndex, left, top, width, height, x, y, xscale, yscale, 0.0f, 0.0f, 0.0f, color, alpha); + return RValue_makeUndefined(); +} + +static RValue builtinBackgroundGetWidth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + int32_t bgIndex = RValue_toInt32(args[0]); + int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(ctx->dataWin, bgIndex); + if (0 > tpagIndex) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) ctx->dataWin->tpag.items[tpagIndex].boundingWidth); +} + +static RValue builtinBackgroundGetHeight(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + int32_t bgIndex = RValue_toInt32(args[0]); + int32_t tpagIndex = Renderer_resolveBackgroundTPAGIndex(ctx->dataWin, bgIndex); + if (0 > tpagIndex) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) ctx->dataWin->tpag.items[tpagIndex].boundingHeight); +} + +static RValue builtin_draw_self(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr && ctx->currentInstance != nullptr) { + Renderer_drawSelf(runner->renderer, (Instance*) ctx->currentInstance); + } + return RValue_makeUndefined(); +} + +// draw_line(x1, y1, x2, y2) +static RValue builtin_draw_line(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + float x1 = (float) RValue_toReal(args[0]); + float y1 = (float) RValue_toReal(args[1]); + float x2 = (float) RValue_toReal(args[2]); + float y2 = (float) RValue_toReal(args[3]); + runner->renderer->vtable->drawLine(runner->renderer, x1, y1, x2, y2, 1.0f, runner->renderer->drawColor, runner->renderer->drawAlpha); + } + return RValue_makeUndefined(); +} + +// draw_line_width(x1, y1, x2, y2, w) +static RValue builtin_draw_line_width(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + float x1 = (float) RValue_toReal(args[0]); + float y1 = (float) RValue_toReal(args[1]); + float x2 = (float) RValue_toReal(args[2]); + float y2 = (float) RValue_toReal(args[3]); + float w = (float) RValue_toReal(args[4]); + runner->renderer->vtable->drawLine(runner->renderer, x1, y1, x2, y2, w, runner->renderer->drawColor, runner->renderer->drawAlpha); + } + return RValue_makeUndefined(); +} + +// draw_line_width_colour(x1, y1, x2, y2, w, col1, col2) +static RValue builtin_draw_line_width_colour(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + float x1 = (float) RValue_toReal(args[0]); + float y1 = (float) RValue_toReal(args[1]); + float x2 = (float) RValue_toReal(args[2]); + float y2 = (float) RValue_toReal(args[3]); + float w = (float) RValue_toReal(args[4]); + uint32_t col1 = (uint32_t) RValue_toInt32(args[5]); + uint32_t col2 = (uint32_t) RValue_toInt32(args[6]); + runner->renderer->vtable->drawLineColor(runner->renderer, x1, y1, x2, y2, w, col1, col2, runner->renderer->drawAlpha); + } + return RValue_makeUndefined(); +} + +// draw_triangle(x1, y1, x2, y2, x3, y3, outline) +static RValue builtin_draw_triangle(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + float x1 = (float) RValue_toReal(args[0]); + float y1 = (float) RValue_toReal(args[1]); + float x2 = (float) RValue_toReal(args[2]); + float y2 = (float) RValue_toReal(args[3]); + float x3 = (float) RValue_toReal(args[4]); + float y3 = (float) RValue_toReal(args[5]); + bool outline = (float) RValue_toBool(args[6]); + runner->renderer->vtable->drawTriangle(runner->renderer, x1, y1, x2, y2, x3, y3, outline); + } + return RValue_makeUndefined(); +} + +// draw_circle(x, y, r, outline) +static RValue builtin_drawCircle(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + float x = (float) RValue_toReal(args[0]); + float y = (float) RValue_toReal(args[1]); + float r = (float) RValue_toReal(args[2]); + bool outline = RValue_toBool(args[3]); + Renderer_drawCircle(runner->renderer, x, y, r, outline); + } + return RValue_makeUndefined(); +} + +// draw_set_circle_precision(precision) +static RValue builtin_drawSetCirclePrecision(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->circlePrecision = Renderer_normalizeCirclePrecision(RValue_toInt32(args[0])); + } + return RValue_makeUndefined(); +} + +// draw_get_circle_precision() +static RValue builtin_drawGetCirclePrecision(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + return RValue_makeReal((GMLReal) runner->renderer->circlePrecision); + } + return RValue_makeReal(24.0); +} + +static RValue builtin_draw_set_colour(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->drawColor = (uint32_t) RValue_toInt32(args[0]); + } + return RValue_makeUndefined(); +} + +static RValue builtin_draw_get_colour(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + return RValue_makeReal((GMLReal) runner->renderer->drawColor); + } + return RValue_makeReal(0.0); +} + +static RValue builtin_draw_get_color(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + return RValue_makeReal((GMLReal) runner->renderer->drawColor); + } + return RValue_makeReal(0.0); +} + +static RValue builtin_draw_get_alpha(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + return RValue_makeReal((GMLReal) runner->renderer->drawAlpha); + } + return RValue_makeReal(0.0); +} + +// merge_color(col1, col2, amount) - lerps between two colors +static RValue builtinMergeColor(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t col1 = RValue_toInt32(args[0]); + int32_t col2 = RValue_toInt32(args[1]); + float amount = (float) RValue_toReal(args[2]); + return RValue_makeReal((GMLReal) Color_lerp(col1, col2, amount)); +} + +static int32_t sanitizeSurfaceDimension(GMLReal value) { + if (value <= 0.0) return 1; + if (value > 4096.0) return 4096; + return (int32_t) ceil(value); +} + +static RValue builtin_surface_create(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t width = sanitizeSurfaceDimension(RValue_toReal(args[0])); + int32_t height = sanitizeSurfaceDimension(RValue_toReal(args[1])); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + int32_t surfaceId = Renderer_createSurface(runner->renderer, width,height); + return RValue_makeReal(surfaceId); + } + return RValue_makeReal(0.0); +} + +static RValue builtin_surface_exists(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + bool exists = Renderer_surfaceExists(runner->renderer, surfaceId); + if (exists == true) { + return RValue_makeReal(1.0); + } + } + return RValue_makeReal(0.0); +} + +static RValue builtin_surface_set_target(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + + bool exists = Renderer_surfaceSetTarget(runner->renderer, surfaceId); + + if (exists == true) { + //fprintf(stderr, "Set Surface Target Yes\n"); + return RValue_makeReal(1.0); + } + } + return RValue_makeReal(0.0); +} + +static RValue builtin_surface_reset_target(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + bool exists = Renderer_surfaceResetTarget(runner->renderer); + if (exists == true) { + return RValue_makeReal(1.0); + } + } + return RValue_makeReal(0.0); +} + +static RValue builtin_surface_resize(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + float w = (float) RValue_toReal(args[1]); + float h = (float) RValue_toReal(args[2]); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->vtable->surfaceResize(runner->renderer, surfaceId, w, h); + } + return RValue_makeUndefined(); +} + +static RValue builtin_surface_copy_part(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t destinationID = (int32_t) RValue_toReal(args[0]); + float x = (float) RValue_toReal(args[1]); + float y = (float) RValue_toReal(args[2]); + int32_t sourceID = (int32_t) RValue_toReal(args[3]); + float xs = (float) RValue_toReal(args[4]); + float ys = (float) RValue_toReal(args[5]); + float ws = (float) RValue_toReal(args[6]); + float hs = (float) RValue_toReal(args[7]); + //fprintf(stderr, "Set Surface Target Yes\n"); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->vtable->surfaceCopy(runner->renderer, destinationID, x, y, sourceID, xs, ys, ws, hs, true); + } + return RValue_makeUndefined(); +} + +static RValue builtin_surface_copy(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t destinationID = (int32_t) RValue_toReal(args[0]); + float x = (float) RValue_toReal(args[1]); + float y = (float) RValue_toReal(args[2]); + int32_t sourceID = (int32_t) RValue_toReal(args[3]); + //fprintf(stderr, "Set Surface Target Yes\n"); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->vtable->surfaceCopy(runner->renderer, destinationID, x, y, sourceID, 0.0, 0.0, 0.0, 0.0, false); + } + return RValue_makeUndefined(); +} + +static RValue builtin_surface_free(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->vtable->surfaceFree(runner->renderer, surfaceId); + } + return RValue_makeUndefined(); +} + +static RValue builtin_draw_surface(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + float x = (float) RValue_toReal(args[1]); + float y = (float) RValue_toReal(args[2]); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->vtable->drawSurface(runner->renderer, surfaceId, x, y, 1.0, 1.0, 0.0, 0xFFFFFFFF, 1.0); + } + return RValue_makeUndefined(); +} + +static RValue builtin_draw_surface_ext(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + float x = (float) RValue_toReal(args[1]); + float y = (float) RValue_toReal(args[2]); + float xscale = (float) RValue_toReal(args[3]); + float yscale = (float) RValue_toReal(args[4]); + float rot = (float) RValue_toReal(args[5]); + uint32_t color = (uint32_t) RValue_toInt32(args[6]); + float alpha = (float) RValue_toReal(args[7]); + + + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->vtable->drawSurface(runner->renderer, surfaceId, x, y, xscale, yscale, rot, color, alpha); + } + return RValue_makeUndefined(); +} + +static RValue builtin_draw_surface_part(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + + float left = (float) RValue_toReal(args[1]); + float top = (float) RValue_toReal(args[2]); + float w = (float) RValue_toReal(args[3]); + float h = (float) RValue_toReal(args[4]); + + float x = (float) RValue_toReal(args[5]); + float y = (float) RValue_toReal(args[6]); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + + runner->renderer->vtable->drawSurfacePart(runner->renderer, surfaceId, x, y, left, top, w, h, 1.0, 1.0, 0xFFFFFFFF, 1.0); + } + return RValue_makeUndefined(); +} + +static RValue builtin_draw_surface_part_ext(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + + float left = (float) RValue_toReal(args[1]); + float top = (float) RValue_toReal(args[2]); + float w = (float) RValue_toReal(args[3]); + float h = (float) RValue_toReal(args[4]); + + float x = (float) RValue_toReal(args[5]); + float y = (float) RValue_toReal(args[6]); + + float xscale = (float) RValue_toReal(args[7]); + float yscale = (float) RValue_toReal(args[8]); + uint32_t color = (uint32_t) RValue_toInt32(args[9]); + float alpha = (float) RValue_toReal(args[10]); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + + runner->renderer->vtable->drawSurfacePart(runner->renderer, surfaceId, x, y, left, top, w, h, xscale, yscale, color, alpha); + } + return RValue_makeUndefined(); +} + +static RValue builtin_draw_surface_stretched(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + float x = (float) RValue_toReal(args[1]); + float y = (float) RValue_toReal(args[2]); + float width = (float) RValue_toReal(args[3]); + float height = (float) RValue_toReal(args[4]); + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer != nullptr) { + runner->renderer->vtable->drawSurfaceStretched(runner->renderer, surfaceId, x, y, width, height); + } + return RValue_makeUndefined(); +} + +// application_surface is surface ID -1 (sentinel); for it, return the window dimensions +static RValue builtinSurfaceGetWidth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + Runner* runner = (Runner*) ctx->runner; + if (surfaceId == -1) { + return RValue_makeReal((GMLReal) ctx->dataWin->gen8.defaultWindowWidth); + } else { + return RValue_makeReal(Renderer_getSurfaceWidth(runner->renderer, surfaceId)); + } + + return RValue_makeReal(0.0); +} + +static RValue builtinSurfaceGetHeight(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + Runner* runner = (Runner*) ctx->runner; + if (surfaceId == -1) { + return RValue_makeReal((GMLReal) ctx->dataWin->gen8.defaultWindowHeight); + } else { + return RValue_makeReal(Renderer_getSurfaceHeight(runner->renderer, surfaceId)); + } + + return RValue_makeReal(0.0); +} + +// Sprite functions +static RValue builtin_spriteAdd(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + logStubbedFunction(ctx, "sprite_add"); + // Return 1, so that a sprite_exists check passes + return RValue_makeInt32(1); +} + +static RValue builtin_spriteExists(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (args[0].type == RVALUE_UNDEFINED) return RValue_makeBool(false); + int32_t spriteIndex = RValue_toInt32(args[0]); + if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeBool(false); + return RValue_makeBool(true); +} + +static RValue builtin_spriteGetWidth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); + if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].width); +} + +static RValue builtin_spriteGetHeight(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); + if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].height); +} + +static RValue builtin_spriteGetNumber(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); + if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].textureCount); +} + +static RValue builtin_spriteGetXOffset(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); + if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].originX); +} + +static RValue builtin_spriteGetYOffset(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); + if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) ctx->dataWin->sprt.sprites[spriteIndex].originY); +} + +static RValue builtin_spriteGetName(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); + if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeString(""); + const char* name = ctx->dataWin->sprt.sprites[spriteIndex].name; + return RValue_makeString(name != nullptr ? name : ""); +} + +// sprite_set_offset(sprite_index, xoff, yoff) +static RValue builtin_spriteSetOffset(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t spriteIndex = (int32_t) RValue_toReal(args[0]); + if (0 > spriteIndex || (uint32_t) spriteIndex >= ctx->dataWin->sprt.count) return RValue_makeReal(0.0); + ctx->dataWin->sprt.sprites[spriteIndex].originX = (int32_t) RValue_toReal(args[1]); + ctx->dataWin->sprt.sprites[spriteIndex].originY = (int32_t) RValue_toReal(args[2]); + return RValue_makeReal(0.0); +} + +// sprite_create_from_surface(surface_id, x, y, w, h, removeback, smooth, xorig, yorig) +static RValue builtin_spriteCreateFromSurface(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr || runner->renderer->vtable->createSpriteFromSurface == nullptr) return RValue_makeReal(-1); + + int32_t surfaceId = (int32_t) RValue_toReal(args[0]); + int32_t x = RValue_toInt32(args[1]); + int32_t y = RValue_toInt32(args[2]); + int32_t w = RValue_toInt32(args[3]); + int32_t h = RValue_toInt32(args[4]); + bool removeback = RValue_toBool(args[5]); + bool smooth = RValue_toBool(args[6]); + int32_t xorig = RValue_toInt32(args[7]); + int32_t yorig = RValue_toInt32(args[8]); + + int32_t result = runner->renderer->vtable->createSpriteFromSurface(runner->renderer, surfaceId, x, y, w, h, removeback, smooth, xorig, yorig); + return RValue_makeReal((GMLReal) result); +} + +// sprite_delete(sprite_index) +static RValue builtin_spriteDelete(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + if (runner->renderer == nullptr || runner->renderer->vtable->deleteSprite == nullptr) return RValue_makeUndefined(); + + int32_t spriteIndex = RValue_toInt32(args[0]); + runner->renderer->vtable->deleteSprite(runner->renderer, spriteIndex); + return RValue_makeUndefined(); +} + +// Font/text measurement +static RValue builtin_stringWidth(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + Renderer* renderer = runner->renderer; + int32_t fontIndex = renderer->drawFont; + if (0 > fontIndex || renderer->dataWin->font.count <= (uint32_t) fontIndex) return RValue_makeReal(0.0); + + Font* font = &renderer->dataWin->font.fonts[fontIndex]; + char* str = RValue_toString(args[0]); + + PreprocessedText processed = TextUtils_preprocessGmlTextIfNeeded(runner, str); + int32_t textLen = (int32_t) strlen(processed.text); + + // Find the widest line + float maxWidth = 0; + int32_t lineStart = 0; + while (textLen >= lineStart) { + int32_t lineEnd = lineStart; + while (textLen > lineEnd && !TextUtils_isNewlineChar(processed.text[lineEnd])) { + lineEnd++; + } + int32_t lineLen = lineEnd - lineStart; + + float lineWidth = TextUtils_measureLineWidth(font, processed.text + lineStart, lineLen); + if (lineWidth > maxWidth) maxWidth = lineWidth; + + if (textLen > lineEnd) { + lineStart = TextUtils_skipNewline(processed.text, lineEnd, textLen); + } else { + break; + } + } + + PreprocessedText_free(processed); + free(str); + return RValue_makeReal((GMLReal) (maxWidth * font->scaleX)); +} + +static RValue builtin_stringHeight(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + Renderer* renderer = runner->renderer; + int32_t fontIndex = renderer->drawFont; + if (0 > fontIndex || renderer->dataWin->font.count <= (uint32_t) fontIndex) return RValue_makeReal(0.0); + + Font* font = &renderer->dataWin->font.fonts[fontIndex]; + char* str = RValue_toString(args[0]); + + PreprocessedText processed = TextUtils_preprocessGmlTextIfNeeded(runner, str); + int32_t textLen = (int32_t) strlen(processed.text); + int32_t lineCount = TextUtils_countLines(processed.text, textLen); + PreprocessedText_free(processed); + free(str); + + // Match HTML5 runner: string_height = lines * TextHeight('M') = lines * max_glyph_height * scaleY. + return RValue_makeReal((GMLReal) ((float) lineCount * TextUtils_lineStride(font) * font->scaleY)); +} + +STUB_RETURN_ZERO(string_width_ext) +STUB_RETURN_ZERO(string_height_ext) + +// Color functions +static RValue builtinMakeColor(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeReal(0.0); + int32_t r = RValue_toInt32(args[0]); + int32_t g = RValue_toInt32(args[1]); + int32_t b = RValue_toInt32(args[2]); + return RValue_makeReal((GMLReal) (r | (g << 8) | (b << 16))); +} + +static RValue builtinMakeColour(VMContext* ctx, RValue* args, int32_t argCount) { + return builtinMakeColor(ctx, args, argCount); +} + +static RValue builtinMakeColorHsv(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeReal(0.0); + + // GameMaker: Studio 1.x: Values are wrapped around 256 (example: -1 -> 255, 257 -> 1) + // GameMaker: Studio 2.x+: Clamps values around [0, 255] + // Hue, Saturation, Value + GMLReal hRaw, sRaw, vRaw; + if (DataWin_isVersionAtLeast(ctx->dataWin, 2, 0, 0, 0)) { + hRaw = RValue_toReal(args[0]); + sRaw = RValue_toReal(args[1]); + vRaw = RValue_toReal(args[2]); + if (0.0 > hRaw) hRaw = 0.0; else if (hRaw > 255.0) hRaw = 255.0; + if (0.0 > sRaw) sRaw = 0.0; else if (sRaw > 255.0) sRaw = 255.0; + if (0.0 > vRaw) vRaw = 0.0; else if (vRaw > 255.0) vRaw = 255.0; + } else { + hRaw = (GMLReal) (RValue_toInt32(args[0]) & 0xFF); + sRaw = (GMLReal) (RValue_toInt32(args[1]) & 0xFF); + vRaw = (GMLReal) (RValue_toInt32(args[2]) & 0xFF); + } + + GMLReal s = sRaw / 255.0; + GMLReal v = vRaw / 255.0; + + GMLReal r = v, g = v, b = v; + if (s != 0.0) { + // https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative + GMLReal h = (hRaw * 360.0) / 255.0; + GMLReal hSector = h / 60.0; + if (h == 360.0) hSector = 0.0; + int32_t i = (int32_t) hSector; + GMLReal f = hSector - (GMLReal) i; + GMLReal p = v * (1.0 - s); + GMLReal q = v * (1.0 - s * f); + GMLReal t = v * (1.0 - s * (1.0 - f)); + switch (i) { + case 0: r = v; g = t; b = p; break; + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + default: r = v; g = p; b = q; break; + } + } + + int32_t rOut = (int32_t) (r * 255.0 + 0.5); + int32_t gOut = (int32_t) (g * 255.0 + 0.5); + int32_t bOut = (int32_t) (b * 255.0 + 0.5); + if (0 > rOut) rOut = 0; else if (rOut > 255) rOut = 255; + if (0 > gOut) gOut = 0; else if (gOut > 255) gOut = 255; + if (0 > bOut) bOut = 0; else if (bOut > 255) bOut = 255; + + return RValue_makeReal((GMLReal) (rOut | (gOut << 8) | (bOut << 16))); +} + +static RValue builtinMakeColourHsv(VMContext* ctx, RValue* args, int32_t argCount) { + return builtinMakeColorHsv(ctx, args, argCount); +} + +// Display stubs +STUB_RETURN_VALUE(display_get_width, 640.0) +STUB_RETURN_VALUE(display_get_height, 480.0) + +static int32_t resolveGuiWidth(Runner* runner) { + if (runner->guiWidth > 0) return runner->guiWidth; + Room* room = runner->currentRoom; + if (room != nullptr) { + repeat(8, vi) { + if (room->views[vi].enabled && room->views[vi].portWidth > 0) { + return room->views[vi].portWidth; + } + } + if (room->width > 0) return (int32_t) room->width; + } + return 320; +} + +static int32_t resolveGuiHeight(Runner* runner) { + if (runner->guiHeight > 0) return runner->guiHeight; + Room* room = runner->currentRoom; + if (room != nullptr) { + repeat(8, vi) { + if (room->views[vi].enabled && room->views[vi].portHeight > 0) { + return room->views[vi].portHeight; + } + } + if (room->height > 0) return (int32_t) room->height; + } + return 240; +} + +static RValue builtinDisplayGetGuiWidth(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + return RValue_makeInt32(resolveGuiWidth(runner)); +} + +static RValue builtinDisplayGetGuiHeight(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + return RValue_makeInt32(resolveGuiHeight(runner)); +} + +static RValue builtinDisplaySetGuiSize(VMContext* ctx, RValue* args, int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t w = RValue_toInt32(args[0]); + int32_t h = RValue_toInt32(args[1]); + runner->guiWidth = w > 0 ? w : 0; + runner->guiHeight = h > 0 ? h : 0; + return RValue_makeUndefined(); +} + +static RValue builtinDisplaySetGuiMaximise(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + // GMS: display_set_gui_maximise(xscale, yscale, xoffset, yoffset). We don't support scaling yet; reset to auto (match view). + Runner* runner = (Runner*) ctx->runner; + runner->guiWidth = 0; + runner->guiHeight = 0; + return RValue_makeUndefined(); +} + +// place_meeting(x, y, obj) - returns true if the calling instance would collide with obj at position (x, y) +static RValue builtinPlaceMeeting(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeBool(false); + + Runner* runner = (Runner*) ctx->runner; + Instance* caller = (Instance*) ctx->currentInstance; + if (caller == nullptr) return RValue_makeBool(false); + + GMLReal testX = RValue_toReal(args[0]); + GMLReal testY = RValue_toReal(args[1]); + int32_t target = RValue_toInt32(args[2]); + + // Save current position and temporarily move to test position + GMLReal savedX = caller->x; + GMLReal savedY = caller->y; + caller->x = testX; + caller->y = testY; + + InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); + bool found = false; + + SpatialGrid_syncGrid(runner, runner->spatialGrid); + + if (callerBBox.valid) { + SpatialGridQuery query = SpatialGrid_prepareQuery(runner, callerBBox.left, callerBBox.top, callerBBox.right, callerBBox.bottom, target); + + for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx && !found; gx++) { + for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy && !found; gy++) { + Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; + int32_t cellLen = (int32_t) arrlen(cell); + repeat(cellLen, ci) { + Instance* other = cell[ci]; + if (!other->active || other == caller) continue; + if (other->lastCollisionQueryId == query.queryId) continue; + other->lastCollisionQueryId = query.queryId; + + if (query.filterByObject && !VM_isObjectOrDescendant(runner->dataWin, other->objectIndex, target)) continue; + if (query.filterByInstanceId && other->instanceId != (uint32_t) target) continue; + + InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); + if (!otherBBox.valid) continue; + + if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { + found = true; + break; + } + } + } + } + } + + // Restore original position + caller->x = savedX; + caller->y = savedY; + + return RValue_makeBool(found); +} +// collision_line(x1, y1, x2, y2, obj, prec, notme) +static RValue builtinCollisionLine(VMContext* ctx, RValue* args, int32_t argCount) { + if (7 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); + + Runner* runner = (Runner*) ctx->runner; + GMLReal lx1 = RValue_toReal(args[0]); + GMLReal ly1 = RValue_toReal(args[1]); + GMLReal lx2 = RValue_toReal(args[2]); + GMLReal ly2 = RValue_toReal(args[3]); + int32_t targetObjIndex = RValue_toInt32(args[4]); + int32_t prec = RValue_toInt32(args[5]); + int32_t notme = RValue_toInt32(args[6]); + + Instance* self = (Instance*) ctx->currentInstance; + + int32_t resultId = INSTANCE_NOONE; + int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t snapIdx = snapBase; snapEnd > snapIdx; snapIdx++) { + Instance* inst = runner->instanceSnapshots[snapIdx]; + if (!inst->active) continue; + if (notme && inst == self) continue; + + if (!Collision_lineOverlapsInstance(ctx->dataWin, inst, lx1, ly1, lx2, ly2)) continue; + InstanceBBox bbox = Collision_computeBBox(ctx->dataWin, inst); + + // Normalize line left-to-right for clipping + GMLReal xl = lx1, yl = ly1, xr = lx2, yr = ly2; + if (xl > xr) { GMLReal tmp = xl; xl = xr; xr = tmp; tmp = yl; yl = yr; yr = tmp; } + + GMLReal dx = xr - xl; + GMLReal dy = yr - yl; + + // Clip line to bbox horizontally + if (GMLReal_fabs(dx) > 0.0001) { + if (bbox.left > xl) { + GMLReal t = (bbox.left - xl) / dx; + xl = bbox.left; + yl = yl + t * dy; + } + if (xr > bbox.right) { + GMLReal t = (bbox.right - xl) / (xr - xl); + yr = yl + t * (yr - yl); + xr = bbox.right; + } + } + + // Y-bounds check after horizontal clipping + GMLReal clippedTop = GMLReal_fmin(yl, yr); + GMLReal clippedBottom = GMLReal_fmax(yl, yr); + if (bbox.top > clippedBottom || clippedTop >= bbox.bottom) continue; + + // Bbox-only mode: collision confirmed + if (prec == 0) { + resultId = inst->instanceId; + break; + } + + // Precise mode: walk line pixel-by-pixel within bbox + Sprite* spr = Collision_getSprite(ctx->dataWin, inst); + if (spr == nullptr || spr->sepMasks != 1 || spr->masks == nullptr || spr->maskCount == 0) { + // No precise mask available, treat as bbox hit + resultId = inst->instanceId; + break; + } + + // Recompute dx/dy for the clipped segment + GMLReal cdx = xr - xl; + GMLReal cdy = yr - yl; + bool found = false; + + if (GMLReal_fabs(cdy) >= GMLReal_fabs(cdx)) { + // Vertical-major: normalize top-to-bottom + GMLReal xt = xl, yt = yl, xb = xr, yb = yr; + if (yt > yb) { GMLReal tmp = xt; xt = xb; xb = tmp; tmp = yt; yt = yb; yb = tmp; } + GMLReal vdx = xb - xt; + GMLReal vdy = yb - yt; + + int32_t startY = (int32_t) GMLReal_fmax(bbox.top, yt); + int32_t endY = (int32_t) GMLReal_fmin(bbox.bottom, yb); + for (int32_t py = startY; endY >= py && !found; py++) { + GMLReal px = (GMLReal_fabs(vdy) > 0.0001) ? xt + ((GMLReal) py - yt) * vdx / vdy : xt; + if (Collision_pointInInstance(spr, inst, px + 0.5, (GMLReal) py + 0.5)) { + found = true; + } + } + } else { + // Horizontal-major + int32_t startX = (int32_t) GMLReal_fmax(bbox.left, xl); + int32_t endX = (int32_t) GMLReal_fmin(bbox.right, xr); + for (int32_t px = startX; endX >= px && !found; px++) { + GMLReal py = (GMLReal_fabs(cdx) > 0.0001) ? yl + ((GMLReal) px - xl) * cdy / cdx : yl; + if (Collision_pointInInstance(spr, inst, (GMLReal) px + 0.5, py + 0.5)) { + found = true; + } + } + } + + if (!found) continue; + resultId = inst->instanceId; + break; + } + Runner_popInstanceSnapshot(runner, snapBase); + + return RValue_makeReal((GMLReal) resultId); +} + +// rectangle_in_rectangle(px1, py1, px2, py2, x1, y1, x2, y2) +// Returns 0 if rectangle P is outside R, 1 if fully inside, 2 if partially overlapping. +// Matches GameMaker-HTML5 scripts/functions/Function_Collision.js. +static RValue builtinRectangleInRectangle(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (8 > argCount) return RValue_makeReal(0.0); + + GMLReal px1 = RValue_toReal(args[0]); + GMLReal py1 = RValue_toReal(args[1]); + GMLReal px2 = RValue_toReal(args[2]); + GMLReal py2 = RValue_toReal(args[3]); + GMLReal x1 = RValue_toReal(args[4]); + GMLReal y1 = RValue_toReal(args[5]); + GMLReal x2 = RValue_toReal(args[6]); + GMLReal y2 = RValue_toReal(args[7]); + + // Normalize so (1,1) is always top-left and (2,2) is bottom-right. + if (px1 > px2) { GMLReal t = px1; px1 = px2; px2 = t; } + if (py1 > py2) { GMLReal t = py1; py1 = py2; py2 = t; } + if (x1 > x2) { GMLReal t = x1; x1 = x2; x2 = t; } + if (y1 > y2) { GMLReal t = y1; y1 = y2; y2 = t; } + + // Count how many corners of P sit inside R. + int32_t cornersIn = 0; + if (px1 >= x1 && px1 <= x2 && py1 >= y1 && py1 <= y2) cornersIn |= 1; + if (px2 >= x1 && px2 <= x2 && py1 >= y1 && py1 <= y2) cornersIn |= 2; + if (px2 >= x1 && px2 <= x2 && py2 >= y1 && py2 <= y2) cornersIn |= 4; + if (px1 >= x1 && px1 <= x2 && py2 >= y1 && py2 <= y2) cornersIn |= 8; + + if (cornersIn == 15) return RValue_makeReal(1.0); + + if (cornersIn == 0) { + // No P corner is inside R. Check whether R's corners are inside P (R engulfs P partially) + // or the rectangles cross axis-wise (T-intersection). + int32_t rCornersIn = 0; + if (x1 >= px1 && x1 <= px2 && y1 >= py1 && y1 <= py2) rCornersIn |= 1; + if (x2 >= px1 && x2 <= px2 && y1 >= py1 && y1 <= py2) rCornersIn |= 2; + if (x2 >= px1 && x2 <= px2 && y2 >= py1 && y2 <= py2) rCornersIn |= 4; + if (x1 >= px1 && x1 <= px2 && y2 >= py1 && y2 <= py2) rCornersIn |= 8; + if (rCornersIn != 0) return RValue_makeReal(2.0); + + // R crosses P horizontally (R's x-edges within P, P's y-edges within R). + int32_t crossX = 0; + if (x1 >= px1 && x1 <= px2 && py1 >= y1 && py1 <= y2) crossX |= 1; + if (x2 >= px1 && x2 <= px2 && py1 >= y1 && py1 <= y2) crossX |= 2; + if (x2 >= px1 && x2 <= px2 && py2 >= y1 && py2 <= y2) crossX |= 4; + if (x1 >= px1 && x1 <= px2 && py2 >= y1 && py2 <= y2) crossX |= 8; + if (crossX != 0) return RValue_makeReal(2.0); + + // R crosses P vertically (R's y-edges within P, P's x-edges within R). + int32_t crossY = 0; + if (px1 >= x1 && px1 <= x2 && y1 >= py1 && y1 <= py2) crossY |= 1; + if (px2 >= x1 && px2 <= x2 && y1 >= py1 && y1 <= py2) crossY |= 2; + if (px2 >= x1 && px2 <= x2 && y2 >= py1 && y2 <= py2) crossY |= 4; + if (px1 >= x1 && px1 <= x2 && y2 >= py1 && y2 <= py2) crossY |= 8; + if (crossY != 0) return RValue_makeReal(2.0); + + return RValue_makeReal(0.0); + } + + // Some but not all of P's corners are inside R: partial overlap. + return RValue_makeReal(2.0); +} + +// collision_rectangle(x1, y1, x2, y2, obj, prec, notme) +static RValue builtinCollisionRectangle(VMContext* ctx, RValue* args, int32_t argCount) { + if (7 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); + + Runner* runner = (Runner*) ctx->runner; + GMLReal x1 = RValue_toReal(args[0]); + GMLReal y1 = RValue_toReal(args[1]); + GMLReal x2 = RValue_toReal(args[2]); + GMLReal y2 = RValue_toReal(args[3]); + int32_t targetObjIndex = RValue_toInt32(args[4]); + int32_t prec = RValue_toInt32(args[5]); + int32_t notme = RValue_toInt32(args[6]); + + // Normalize rect + if (x1 > x2) { GMLReal tmp = x1; x1 = x2; x2 = tmp; } + if (y1 > y2) { GMLReal tmp = y1; y1 = y2; y2 = tmp; } + + Instance* self = (Instance*) ctx->currentInstance; + + int32_t resultId = INSTANCE_NOONE; + int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t snapIdx = snapBase; snapEnd > snapIdx; snapIdx++) { + Instance* inst = runner->instanceSnapshots[snapIdx]; + if (!inst->active) continue; + if (notme && inst == self) continue; + + if (!Collision_rectOverlapsInstance(ctx->dataWin, inst, x1, y1, x2, y2)) continue; + + InstanceBBox bbox = Collision_computeBBox(ctx->dataWin, inst); + + // Precise check if requested and sprite has precise masks + if (prec != 0) { + Sprite* spr = Collision_getSprite(ctx->dataWin, inst); + if (Collision_hasFrameMasks(spr)) { + // Check if any pixel in the overlap region hits the mask + GMLReal iLeft = GMLReal_fmax(x1, bbox.left); + GMLReal iRight = GMLReal_fmin(x2, bbox.right); + GMLReal iTop = GMLReal_fmax(y1, bbox.top); + GMLReal iBottom = GMLReal_fmin(y2, bbox.bottom); + + bool found = false; + int32_t startX = (int32_t) GMLReal_floor(iLeft); + int32_t endX = (int32_t) GMLReal_ceil(iRight); + int32_t startY = (int32_t) GMLReal_floor(iTop); + int32_t endY = (int32_t) GMLReal_ceil(iBottom); + + for (int32_t py = startY; endY > py && !found; py++) { + for (int32_t px = startX; endX > px && !found; px++) { + if (Collision_pointInInstance(spr, inst, (GMLReal) px + 0.5, (GMLReal) py + 0.5)) { + found = true; + } + } + } + if (!found) continue; + } + } + + resultId = inst->instanceId; + break; + } + Runner_popInstanceSnapshot(runner, snapBase); + + return RValue_makeReal((GMLReal) resultId); +} + +// collision_circle(x, y, radius, obj, prec, notme) +static RValue builtinCollisionCircle(VMContext* ctx, RValue* args, int32_t argCount) { + if (6 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); + + Runner* runner = (Runner*) ctx->runner; + GMLReal cx = RValue_toReal(args[0]); + GMLReal cy = RValue_toReal(args[1]); + GMLReal radius = RValue_toReal(args[2]); + int32_t targetObjIndex = RValue_toInt32(args[3]); + int32_t prec = RValue_toInt32(args[4]); + int32_t notme = RValue_toInt32(args[5]); + + if (0 > radius) radius = -radius; + GMLReal radiusSq = radius * radius; + + Instance* self = (Instance*) ctx->currentInstance; + + GMLReal qx1 = cx - radius; + GMLReal qy1 = cy - radius; + GMLReal qx2 = cx + radius; + GMLReal qy2 = cy + radius; + + SpatialGrid_syncGrid(runner, runner->spatialGrid); + SpatialGridQuery query = SpatialGrid_prepareQuery(runner, qx1, qy1, qx2, qy2, targetObjIndex); + + int32_t resultId = INSTANCE_NOONE; + for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx && resultId == INSTANCE_NOONE; gx++) { + for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy && resultId == INSTANCE_NOONE; gy++) { + Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; + int32_t cellLen = (int32_t) arrlen(cell); + repeat(cellLen, ci) { + Instance* inst = cell[ci]; + if (!inst->active) continue; + if (notme && inst == self) continue; + if (inst->lastCollisionQueryId == query.queryId) continue; + inst->lastCollisionQueryId = query.queryId; + + if (query.filterByObject && !VM_isObjectOrDescendant(ctx->dataWin, inst->objectIndex, targetObjIndex)) continue; + if (query.filterByInstanceId && inst->instanceId != (uint32_t) targetObjIndex) continue; + if (!query.filterByObject && !query.filterByInstanceId && targetObjIndex != INSTANCE_ALL) continue; + + if (!Collision_circleOverlapsInstance(ctx->dataWin, inst, cx, cy, radius)) continue; + + if (prec != 0) { + Sprite* spr = Collision_getSprite(ctx->dataWin, inst); + if (Collision_hasFrameMasks(spr)) { + InstanceBBox bbox = Collision_computeBBox(ctx->dataWin, inst); + GMLReal iLeft = GMLReal_fmax(qx1, bbox.left); + GMLReal iRight = GMLReal_fmin(qx2, bbox.right); + GMLReal iTop = GMLReal_fmax(qy1, bbox.top); + GMLReal iBottom = GMLReal_fmin(qy2, bbox.bottom); + + bool found = false; + int32_t startX = (int32_t) GMLReal_floor(iLeft); + int32_t endX = (int32_t) GMLReal_ceil(iRight); + int32_t startY = (int32_t) GMLReal_floor(iTop); + int32_t endY = (int32_t) GMLReal_ceil(iBottom); + + for (int32_t py = startY; endY > py && !found; py++) { + for (int32_t px = startX; endX > px && !found; px++) { + GMLReal wpx = (GMLReal) px + 0.5; + GMLReal wpy = (GMLReal) py + 0.5; + GMLReal ddx = wpx - cx; + GMLReal ddy = wpy - cy; + if (ddx * ddx + ddy * ddy > radiusSq) continue; + if (Collision_pointInInstance(spr, inst, wpx, wpy)) { + found = true; + } + } + } + if (!found) continue; + } + } + + resultId = inst->instanceId; + break; + } + } + } + + return RValue_makeReal((GMLReal) resultId); +} + +// collision_rectangle_list(x1, y1, x2, y2, obj, prec, notme, list, ordered) -> count +static RValue builtinCollisionRectangleList(VMContext* ctx, RValue* args, int32_t argCount) { + if (8 > argCount) return RValue_makeReal(0.0); + + Runner* runner = (Runner*) ctx->runner; + GMLReal x1 = RValue_toReal(args[0]); + GMLReal y1 = RValue_toReal(args[1]); + GMLReal x2 = RValue_toReal(args[2]); + GMLReal y2 = RValue_toReal(args[3]); + int32_t target = RValue_toInt32(args[4]); + int32_t prec = RValue_toInt32(args[5]); + int32_t notme = RValue_toInt32(args[6]); + int32_t listId = RValue_toInt32(args[7]); + // arg 8 (ordered) is currently ignored; instances are appended in iteration order + + DsList* list = dsListGet(runner, listId); + if (list == nullptr) return RValue_makeReal(0.0); + + if (x1 > x2) { GMLReal tmp = x1; x1 = x2; x2 = tmp; } + if (y1 > y2) { GMLReal tmp = y1; y1 = y2; y2 = tmp; } + + Instance* self = (Instance*) ctx->currentInstance; + int32_t count = 0; + + SpatialGrid_syncGrid(runner, runner->spatialGrid); + SpatialGridQuery query = SpatialGrid_prepareQuery(runner, x1, y1, x2, y2, target); + + for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx; gx++) { + for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy; gy++) { + Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; + int32_t cellLen = (int32_t) arrlen(cell); + repeat(cellLen, ci) { + Instance* inst = cell[ci]; + if (!inst->active) continue; + if (notme && inst == self) continue; + if (inst->lastCollisionQueryId == query.queryId) continue; + inst->lastCollisionQueryId = query.queryId; + + if (query.filterByObject && !VM_isObjectOrDescendant(ctx->dataWin, inst->objectIndex, target)) continue; + if (query.filterByInstanceId && inst->instanceId != (uint32_t) target) continue; + + if (!Collision_rectOverlapsInstance(ctx->dataWin, inst, x1, y1, x2, y2)) continue; + InstanceBBox bbox = Collision_computeBBox(ctx->dataWin, inst); + + if (prec != 0) { + Sprite* spr = Collision_getSprite(ctx->dataWin, inst); + if (Collision_hasFrameMasks(spr)) { + GMLReal iLeft = GMLReal_fmax(x1, bbox.left); + GMLReal iRight = GMLReal_fmin(x2, bbox.right); + GMLReal iTop = GMLReal_fmax(y1, bbox.top); + GMLReal iBottom = GMLReal_fmin(y2, bbox.bottom); + + bool found = false; + int32_t startX = (int32_t) GMLReal_floor(iLeft); + int32_t endX = (int32_t) GMLReal_ceil(iRight); + int32_t startY = (int32_t) GMLReal_floor(iTop); + int32_t endY = (int32_t) GMLReal_ceil(iBottom); + + for (int32_t py = startY; endY > py && !found; py++) { + for (int32_t px = startX; endX > px && !found; px++) { + if (Collision_pointInInstance(spr, inst, (GMLReal) px + 0.5, (GMLReal) py + 0.5)) { + found = true; + } + } + } + if (!found) continue; + } + } + + arrput(list->items, RValue_makeReal((GMLReal) inst->instanceId)); + count++; + } + } + } + + return RValue_makeReal((GMLReal) count); +} + +// collision_point(x, y, obj, prec, notme) +static RValue builtinCollisionPoint(VMContext* ctx, RValue* args, int32_t argCount) { + if (5 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); + + Runner* runner = (Runner*) ctx->runner; + GMLReal px = RValue_toReal(args[0]); + GMLReal py = RValue_toReal(args[1]); + int32_t targetObjIndex = RValue_toInt32(args[2]); + int32_t prec = RValue_toInt32(args[3]); + int32_t notme = RValue_toInt32(args[4]); + + Instance* self = (Instance*) ctx->currentInstance; + + int32_t resultId = INSTANCE_NOONE; + int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t snapIdx = snapBase; snapEnd > snapIdx; snapIdx++) { + Instance* inst = runner->instanceSnapshots[snapIdx]; + if (!inst->active) continue; + if (notme && inst == self) continue; + + if (!Collision_pointInsideInstanceBox(ctx->dataWin, inst, px, py)) continue; + + if (prec != 0) { + Sprite* spr = Collision_getSprite(ctx->dataWin, inst); + if (Collision_hasFrameMasks(spr)) { + if (!Collision_pointInInstance(spr, inst, px, py)) continue; + } + } + + resultId = inst->instanceId; + break; + } + Runner_popInstanceSnapshot(runner, snapBase); + + return RValue_makeReal((GMLReal) resultId); +} + +// instance_place(x, y, obj) - returns colliding instance id at (x, y), or noone +static RValue builtinInstancePlace(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); + + Runner* runner = (Runner*) ctx->runner; + Instance* caller = (Instance*) ctx->currentInstance; + if (caller == nullptr) return RValue_makeReal((GMLReal) INSTANCE_NOONE); + + GMLReal testX = RValue_toReal(args[0]); + GMLReal testY = RValue_toReal(args[1]); + int32_t targetObjIndex = RValue_toInt32(args[2]); + + GMLReal savedX = caller->x; + GMLReal savedY = caller->y; + caller->x = testX; + caller->y = testY; + + InstanceBBox callerBBox = Collision_computeBBox(runner->dataWin, caller); + int32_t resultId = INSTANCE_NOONE; + + SpatialGrid_syncGrid(runner, runner->spatialGrid); + + if (callerBBox.valid) { + SpatialGridQuery query = SpatialGrid_prepareQuery(runner, callerBBox.left, callerBBox.top, callerBBox.right, callerBBox.bottom, targetObjIndex); + + for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx && resultId == INSTANCE_NOONE; gx++) { + for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy && resultId == INSTANCE_NOONE; gy++) { + Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; + int32_t cellLen = (int32_t) arrlen(cell); + repeat(cellLen, ci) { + Instance* other = cell[ci]; + if (!other->active || other == caller) continue; + if (other->lastCollisionQueryId == query.queryId) continue; + other->lastCollisionQueryId = query.queryId; + + if (query.filterByObject && !VM_isObjectOrDescendant(runner->dataWin, other->objectIndex, targetObjIndex)) continue; + if (query.filterByInstanceId && other->instanceId != (uint32_t) targetObjIndex) continue; + + InstanceBBox otherBBox = Collision_computeBBox(runner->dataWin, other); + if (!otherBBox.valid) continue; + + if (Collision_instancesOverlapPrecise(runner->dataWin, runner->collisionCompatibilityMode, caller, other, callerBBox, otherBBox)) { + resultId = other->instanceId; + break; + } + } + } + } + } + + caller->x = savedX; + caller->y = savedY; + return RValue_makeReal((GMLReal) resultId); +} + +// instance_position(x, y, obj) +static RValue builtinInstancePosition(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeReal((GMLReal) INSTANCE_NOONE); + + Runner* runner = (Runner*) ctx->runner; + GMLReal px = RValue_toReal(args[0]); + GMLReal py = RValue_toReal(args[1]); + int32_t targetObjIndex = RValue_toInt32(args[2]); + + int32_t resultId = INSTANCE_NOONE; + int32_t snapBase = Runner_pushInstancesForTarget(runner, targetObjIndex); + int32_t snapEnd = (int32_t) arrlen(runner->instanceSnapshots); + for (int32_t i = snapBase; snapEnd > i; i++) { + Instance* inst = runner->instanceSnapshots[i]; + if (!inst->active) continue; + + if (!Collision_pointInsideInstanceBox(ctx->dataWin, inst, px, py)) continue; + + resultId = inst->instanceId; + break; + } + Runner_popInstanceSnapshot(runner, snapBase); + + return RValue_makeReal((GMLReal) resultId); +} + +// position_meeting(x, y, obj) - returns true if point (x, y) is inside any instance of obj. +static RValue builtinPositionMeeting(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeBool(false); + + Runner* runner = (Runner*) ctx->runner; + GMLReal px = RValue_toReal(args[0]); + GMLReal py = RValue_toReal(args[1]); + int32_t target = RValue_toInt32(args[2]); + + + SpatialGrid_syncGrid(runner, runner->spatialGrid); + SpatialGridQuery query = SpatialGrid_prepareQuery(runner, px, py, px, py, target); + bool found = false; + + for (int32_t gx = query.range.minGridX; query.range.maxGridX >= gx && !found; gx++) { + for (int32_t gy = query.range.minGridY; query.range.maxGridY >= gy && !found; gy++) { + Instance** cell = runner->spatialGrid->grid[SpatialGrid_cellIndex(runner->spatialGrid, gx, gy)]; + int32_t cellLen = (int32_t) arrlen(cell); + repeat(cellLen, ci) { + Instance* other = cell[ci]; + // Keep in mind that we DO NOT skip "self" + if (!other->active) continue; + if (other->lastCollisionQueryId == query.queryId) continue; + other->lastCollisionQueryId = query.queryId; + + if (query.filterByObject && !VM_isObjectOrDescendant(runner->dataWin, other->objectIndex, target)) continue; + if (query.filterByInstanceId && other->instanceId != (uint32_t) target) continue; + + if (!Collision_pointInsideInstanceBox(ctx->dataWin, other, px, py)) continue; + + found = true; + break; + } + } + } + + return RValue_makeBool(found); +} + +// Misc stubs +STUB_RETURN_ZERO(get_timer) +static RValue builtinActionSetAlarm(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t steps = RValue_toInt32(args[0]); + int32_t alarmIndex = RValue_toInt32(args[1]); + + if (0 > alarmIndex || alarmIndex >= GML_ALARM_COUNT) { + return RValue_makeUndefined(); + } + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + Runner* runner = (Runner*) ctx->runner; + +#ifdef ENABLE_VM_TRACING + if (shgeti(ctx->alarmsToBeTraced, "*") != -1 || shgeti(ctx->alarmsToBeTraced, runner->dataWin->objt.objects[inst->objectIndex].name) != -1) { + fprintf(stderr, "VM: [%s] Setting Alarm[%d] = %d (instanceId=%d)\n", runner->dataWin->objt.objects[inst->objectIndex].name, alarmIndex, steps, inst->instanceId); + } +#endif + + inst->alarm[alarmIndex] = steps; + if (steps > 0) inst->activeAlarmMask |= (uint16_t) (1u << alarmIndex); + else inst->activeAlarmMask &= (uint16_t) ~(1u << alarmIndex); + } + + return RValue_makeUndefined(); +} + +static RValue builtinAlarmSet(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t alarmIndex = RValue_toInt32(args[0]); + int32_t value = RValue_toInt32(args[1]); + + if (0 > alarmIndex || alarmIndex >= GML_ALARM_COUNT) { + return RValue_makeUndefined(); + } + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + +#ifdef ENABLE_VM_TRACING + Runner* runner = (Runner*) ctx->runner; + if (shgeti(ctx->alarmsToBeTraced, "*") != -1 || shgeti(ctx->alarmsToBeTraced, runner->dataWin->objt.objects[inst->objectIndex].name) != -1) { + fprintf(stderr, "VM: [%s] Setting Alarm[%d] = %d (instanceId=%d)\n", runner->dataWin->objt.objects[inst->objectIndex].name, alarmIndex, value, inst->instanceId); + } +#endif + + inst->alarm[alarmIndex] = value; + if (value > 0) inst->activeAlarmMask |= (uint16_t) (1u << alarmIndex); + else inst->activeAlarmMask &= (uint16_t) ~(1u << alarmIndex); + } + + return RValue_makeUndefined(); +} + +static RValue builtinAlarmGet(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + int32_t alarmIndex = RValue_toInt32(args[0]); + + if (0 > alarmIndex || alarmIndex >= GML_ALARM_COUNT) { + return RValue_makeReal(-1); + } + + if (ctx->currentInstance != nullptr) { + Instance* inst = (Instance*) ctx->currentInstance; + return RValue_makeReal((GMLReal) inst->alarm[alarmIndex]); + } + + return RValue_makeReal(-1); +} + +static RValue builtinActionIfVariable(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + bool check; + switch (args[0].type) { + case RVALUE_REAL: { + check = args[0].real != 0.0; + break; + } + case RVALUE_INT32: { + check = args[0].int32 != 0; + break; + } +#ifndef NO_RVALUE_INT64 + case RVALUE_INT64: { + check = args[0].int64 != 0; + break; + } +#endif + case RVALUE_BOOL: { + check = args[0].int32 != 0; + break; + } + case RVALUE_STRING: { + check = args[0].string != nullptr && args[0].string[0] != '\0'; + break; + } + default: { + check = false; + break; + } + } + + int32_t idx = check ? 1 : 2; + RValue result = args[idx]; + args[idx].ownsReference = false; // Steal ownership to avoid double-free in handleCall + return result; +} + +STUB_RETURN_UNDEFINED(action_sound) + +// ===[ Tile Layer Functions ]=== + +static TileLayerState* getOrCreateTileLayer(Runner* runner, int32_t depth) { + ptrdiff_t idx = hmgeti(runner->tileLayerMap, depth); + if (0 > idx) { + TileLayerState defaultVal = { .visible = true, .offsetX = 0.0f, .offsetY = 0.0f, .alpha = 1.0f }; + hmput(runner->tileLayerMap, depth, defaultVal); + idx = hmgeti(runner->tileLayerMap, depth); + } + return &runner->tileLayerMap[idx].value; +} + +static RValue builtinTileLayerHide(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t depth = RValue_toInt32(args[0]); + TileLayerState* layer = getOrCreateTileLayer(runner, depth); + layer->visible = false; + return RValue_makeUndefined(); +} + +static RValue builtinTileLayerShow(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t depth = RValue_toInt32(args[0]); + TileLayerState* layer = getOrCreateTileLayer(runner, depth); + layer->visible = true; + return RValue_makeUndefined(); +} + +static RValue builtinTileLayerShift(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t depth = RValue_toInt32(args[0]); + float dx = (float) RValue_toReal(args[1]); + float dy = (float) RValue_toReal(args[2]); + TileLayerState* layer = getOrCreateTileLayer(runner, depth); + layer->offsetX += dx; + layer->offsetY += dy; + return RValue_makeUndefined(); +} + +// ===[ Layer Functions ]=== + +static RValue builtinLayerForceDrawDepth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + runner->forceDrawDepth = RValue_toBool(args[0]); + runner->forcedDepth = RValue_toInt32(args[1]); + return RValue_makeUndefined(); +} + +static RValue builtinLayerIsDrawDepthForced(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + return RValue_makeBool(runner->forceDrawDepth); +} + +static RValue builtinLayerGetForcedDepth(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + return RValue_makeReal((GMLReal) runner->forcedDepth); +} + +// ===[ GMS2 Layer Runtime API ]=== + +// GMS layer functions accept either a numeric layer id or a layer name string. +// Returns the resolved runtime id, or -1 if no match. +static int32_t resolveLayerIdArg(Runner* runner, RValue arg) { + if (arg.type == RVALUE_STRING) { + const char* name = arg.string; + if (name == nullptr) return -1; + size_t runtimeLayerCount = arrlenu(runner->runtimeLayers); + repeat(runtimeLayerCount, i) { + RuntimeLayer* rl = &runner->runtimeLayers[i]; + if (rl->dynamic && rl->dynamicName != nullptr && strcmp(rl->dynamicName, name) == 0) + return (int32_t) rl->id; + } + if (runner->currentRoom != nullptr) { + repeat(runner->currentRoom->layerCount, i) { + RoomLayer* layer = &runner->currentRoom->layers[i]; + if (layer->name != nullptr && strcmp(layer->name, name) == 0) + return (int32_t) layer->id; + } + } + return -1; + } + return RValue_toInt32(arg); +} + +static void instanceSetLayerActiveState(Runner* runner, int32_t layerId, bool isActive) { + if (0 > layerId || runner->currentRoom == nullptr) return; + + repeat(runner->currentRoom->layerCount, layerIndex) { + RoomLayer* layer = &runner->currentRoom->layers[layerIndex]; + + if ((int32_t) layer->id != layerId) + continue; + + if (layer->type != RoomLayerType_Instances || layer->instancesData == nullptr) + break; + + RoomLayerInstancesData* layerData = layer->instancesData; + + repeat(layerData->instanceCount, instanceIndex) { + Instance* inst = hmget(runner->instancesById, layerData->instanceIds[instanceIndex]); + if (inst != nullptr && !inst->destroyed) + inst->active = isActive; + } + return; + } +} + +static RValue builtinInstanceActivateLayer(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t layerId = resolveLayerIdArg(runner, args[0]); + instanceSetLayerActiveState(runner, layerId, true); + return RValue_makeUndefined(); +} + +static RValue builtinInstanceDeactivateLayer(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t layerId = resolveLayerIdArg(runner, args[0]); + instanceSetLayerActiveState(runner, layerId, false); + return RValue_makeUndefined(); +} + +static RValue builtinLayerGetId(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + char* name = RValue_toString(args[0]); + if (name == nullptr) return RValue_makeReal(-1.0); + int32_t result = -1; + // Check dynamic layers first (they may shadow a parsed layer by name). + size_t runtimeLayerCount = arrlenu(runner->runtimeLayers); + repeat(runtimeLayerCount, i) { + RuntimeLayer* runtimeLayer = &runner->runtimeLayers[i]; + if (runtimeLayer->dynamic && runtimeLayer->dynamicName != nullptr && strcmp(runtimeLayer->dynamicName, name) == 0) { + result = (int32_t) runtimeLayer->id; + break; + } + } + if (result == -1 && runner->currentRoom != nullptr) { + repeat(runner->currentRoom->layerCount, i) { + RoomLayer* layer = &runner->currentRoom->layers[i]; + if (layer->name != nullptr && strcmp(layer->name, name) == 0) { + result = (int32_t) layer->id; + break; + } + } + } + free(name); + return RValue_makeReal((GMLReal) result); +} + +static RValue builtinLayerExists(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + return RValue_makeBool(Runner_findRuntimeLayerById(runner, id) != nullptr); +} + +static RValue builtinLayerGetName(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer != nullptr && runtimeLayer->dynamic && runtimeLayer->dynamicName != nullptr) + return RValue_makeString(runtimeLayer->dynamicName); + + RoomLayer* roomLayer = Runner_findRoomLayerById(runner, id); + if (roomLayer == nullptr || roomLayer->name == nullptr) + return RValue_makeString(""); + + return RValue_makeString(roomLayer->name); +} + +static RValue builtinLayerGetDepth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer == nullptr) + return RValue_makeUndefined(); + + return RValue_makeReal((GMLReal) runtimeLayer->depth); +} + +static RValue builtinLayerDepth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + int32_t depth = RValue_toInt32(args[1]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer != nullptr && runtimeLayer->depth != depth) { + runtimeLayer->depth = depth; + runner->drawableListSortDirty = true; + } + + return RValue_makeUndefined(); +} + +static RValue builtinLayerGetVisible(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer == nullptr) + return RValue_makeBool(false); + + return RValue_makeBool(runtimeLayer->visible); +} + +static RValue builtinLayerSetVisible(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + bool visible = RValue_toBool(args[1]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer != nullptr) + runtimeLayer->visible = visible; + + return RValue_makeUndefined(); +} + +static RValue builtinLayerGetX(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer == nullptr) + return RValue_makeReal(0.0); + + return RValue_makeReal((GMLReal) runtimeLayer->xOffset); +} + +static RValue builtinLayerX(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + float x = (float) RValue_toReal(args[1]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer != nullptr) + runtimeLayer->xOffset = x; + + return RValue_makeUndefined(); +} + +static RValue builtinLayerGetY(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer == nullptr) + return RValue_makeReal(0.0); + + return RValue_makeReal((GMLReal) runtimeLayer->yOffset); +} + +static RValue builtinLayerY(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + float y = (float) RValue_toReal(args[1]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer != nullptr) + runtimeLayer->yOffset = y; + + return RValue_makeUndefined(); +} + +static RValue builtinLayerGetHspeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer == nullptr) + return RValue_makeReal(0.0); + + return RValue_makeReal((GMLReal) runtimeLayer->hSpeed); +} + +static RValue builtinLayerHspeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + float hs = (float) RValue_toReal(args[1]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer != nullptr) + runtimeLayer->hSpeed = hs; + + return RValue_makeUndefined(); +} + +static RValue builtinLayerGetVspeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer == nullptr) + return RValue_makeReal(0.0); + + return RValue_makeReal((GMLReal) runtimeLayer->vSpeed); +} + +// Creates a new dynamic layer. Signatures: layer_create(depth) or layer_create(depth, name). +static RValue builtinLayerCreate(VMContext* ctx, RValue* args, int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t depth = RValue_toInt32(args[0]); + char* name = nullptr; + if (argCount > 1) { + name = RValue_toString(args[1]); + } + uint32_t id = Runner_getNextLayerId(runner); + RuntimeLayer runtimeLayer = { + .id = id, + .depth = depth, + .visible = true, + .xOffset = 0.0f, .yOffset = 0.0f, + .hSpeed = 0.0f, .vSpeed = 0.0f, + .dynamic = true, + .dynamicName = name, // ownership transferred (nullptr if not provided) + .elements = nullptr, + }; + arrput(runner->runtimeLayers, runtimeLayer); + runner->drawableListStructureDirty = true; + return RValue_makeReal((GMLReal) id); +} + +static RValue builtinLayerDestroy(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + size_t count = arrlenu(runner->runtimeLayers); + repeat(count, i) { + if ((int32_t) runner->runtimeLayers[i].id != id) + continue; + + // Ignore if we are trying to delete a non-dynamic layer + if (!runner->runtimeLayers[i].dynamic) + return RValue_makeUndefined(); + + Runner_freeRuntimeLayer(&runner->runtimeLayers[i]); + arrdel(runner->runtimeLayers, i); + runner->drawableListStructureDirty = true; + break; + } + return RValue_makeUndefined(); +} + +static RValue builtinLayerBackgroundCreate(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t layerId = resolveLayerIdArg(runner, args[0]); + int32_t spriteIndex = RValue_toInt32(args[1]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, layerId); + if (runtimeLayer == nullptr) + return RValue_makeReal(-1.0); + + RuntimeBackgroundElement* bg = safeMalloc(sizeof(RuntimeBackgroundElement)); + bg->spriteIndex = spriteIndex; + bg->visible = true; + bg->htiled = false; + bg->vtiled = false; + bg->stretch = false; + bg->xScale = 1.0f; + bg->yScale = 1.0f; + bg->blend = 0xFFFFFF; + bg->alpha = 1.0f; + bg->xOffset = 0.0f; + bg->yOffset = 0.0f; + RuntimeLayerElement el = { + .id = Runner_getNextLayerId(runner), + .type = RuntimeLayerElementType_Background, + .backgroundElement = bg, + .spriteElement = nullptr, + }; + arrput(runtimeLayer->elements, el); + return RValue_makeReal((GMLReal) el.id); +} + +static RValue builtinLayerBackgroundExists(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t layerId = resolveLayerIdArg(runner, args[0]); + int32_t elementId = RValue_toInt32(args[1]); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, layerId); + if (runtimeLayer == nullptr) + return RValue_makeBool(false); + + size_t count = arrlenu(runtimeLayer->elements); + repeat(count, i) { + if ((int32_t) runtimeLayer->elements[i].id == elementId && runtimeLayer->elements[i].type == RuntimeLayerElementType_Background) { + return RValue_makeBool(true); + } + } + return RValue_makeBool(false); +} + +static RuntimeBackgroundElement* findBackgroundElement(Runner* runner, int32_t elementId) { + RuntimeLayerElement* el = Runner_findLayerElementById(runner, elementId, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Background) + return nullptr; + return el->backgroundElement; +} + +static RValue builtinLayerBackgroundVisible(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); + if (bg != nullptr) + bg->visible = RValue_toBool(args[1]); + return RValue_makeUndefined(); +} + +static RValue builtinLayerBackgroundHtiled(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); + if (bg != nullptr) + bg->htiled = RValue_toBool(args[1]); + return RValue_makeUndefined(); +} + +static RValue builtinLayerBackgroundVtiled(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); + if (bg != nullptr) + bg->vtiled = RValue_toBool(args[1]); + return RValue_makeUndefined(); +} + +static RValue builtinLayerBackgroundXscale(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); + if (bg != nullptr) + bg->xScale = (float) RValue_toReal(args[1]); + return RValue_makeUndefined(); +} + +static RValue builtinLayerBackgroundYscale(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); + if (bg != nullptr) + bg->yScale = (float) RValue_toReal(args[1]); + return RValue_makeUndefined(); +} + +static RValue builtinLayerBackgroundStretch(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); + if (bg != nullptr) + bg->stretch = RValue_toBool(args[1]); + return RValue_makeUndefined(); +} + +static RValue builtinLayerBackgroundBlend(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); + if (bg != nullptr) + bg->blend = (uint32_t) RValue_toInt32(args[1]) & 0x00FFFFFF; + return RValue_makeUndefined(); +} + +static RValue builtinLayerBackgroundAlpha(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + RuntimeBackgroundElement* bg = findBackgroundElement(runner, RValue_toInt32(args[0])); + if (bg != nullptr) + bg->alpha = (float) RValue_toReal(args[1]); + return RValue_makeUndefined(); +} + +static RValue builtinLayerTileAlpha(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t layerId = RValue_toInt32(args[0]); + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, layerId); + RoomLayer* roomLayer = Runner_findRoomLayerById(runner, layerId); + if (runtimeLayer == nullptr || roomLayer == nullptr || roomLayer->type != RoomLayerType_Tiles) + return RValue_makeUndefined(); + TileLayerState* layer = getOrCreateTileLayer(runner, runtimeLayer->depth); + layer->alpha = (float) RValue_toReal(args[1]); + return RValue_makeUndefined(); +} + +#if IS_BC17_OR_HIGHER_ENABLED +static RValue builtinLayerGetAllElements(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + + RValue arr = VM_createArray(ctx); + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer == nullptr) + return arr; + + int32_t i = 0; + size_t count = arrlenu(runtimeLayer->elements); + repeat(count, elementIndex) { + VM_arraySet(ctx, &arr, i++, RValue_makeReal((GMLReal) runtimeLayer->elements[elementIndex].id)); + } + return arr; +} +#endif + +static RValue builtinLayerGetElementType(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); + // layerelementtype_undefined == 0 matches GML's return for unknown/missing elements. + if (el == nullptr) + return RValue_makeReal(0.0); + + return RValue_makeReal((GMLReal) el->type); +} + +static RValue builtinLayerTileVisible(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t layerId = RValue_toInt32(args[0]); + bool visible = RValue_toBool(args[1]); + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, layerId); + RoomLayer* roomLayer = Runner_findRoomLayerById(runner, layerId); + if (runtimeLayer == nullptr || roomLayer == nullptr || roomLayer->type != RoomLayerType_Tiles) + return RValue_makeUndefined(); + runtimeLayer->visible = visible; + return RValue_makeUndefined(); +} + +static RValue builtinLayerSpriteGetSprite(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) + return RValue_makeReal(-1.0); + + return RValue_makeReal((GMLReal) el->spriteElement->spriteIndex); +} + +static RValue builtinLayerSpriteGetAngle(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) + return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) el->spriteElement->rotation); +} + + +static RValue builtinLayerSpriteGetX(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) + return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) el->spriteElement->x); +} + +static RValue builtinLayerSpriteGetY(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) + return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) el->spriteElement->y); +} + +static RValue builtinLayerSpriteGetXScale(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) + return RValue_makeReal(1.0); + return RValue_makeReal((GMLReal) el->spriteElement->scaleX); +} + +static RValue builtinLayerSpriteGetYScale(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) + return RValue_makeReal(1.0); + return RValue_makeReal((GMLReal) el->spriteElement->scaleY); +} + +static RValue builtinLayerSpriteGetSpeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) + return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) el->spriteElement->animationSpeed); +} + +static RValue builtinLayerSpriteGetIndex(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, nullptr); + if (el == nullptr || el->type != RuntimeLayerElementType_Sprite || el->spriteElement == nullptr) + return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) el->spriteElement->frameIndex); +} + +static RValue builtinLayerSpriteDestroy(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + + RuntimeLayer* owningLayer = nullptr; + RuntimeLayerElement* el = Runner_findLayerElementById(runner, id, &owningLayer); + if (el == nullptr || owningLayer == nullptr || el->type != RuntimeLayerElementType_Sprite) + return RValue_makeUndefined(); + + if (el->spriteElement != nullptr) { + free(el->spriteElement); + el->spriteElement = nullptr; + } + + // Remove the element from the owning layer's element array to keep lookup + iteration tidy. + size_t count = arrlenu(owningLayer->elements); + repeat(count, i) { + if (&owningLayer->elements[i] == el) { + arrdel(owningLayer->elements, i); + break; + } + } + + return RValue_makeUndefined(); +} + +#if IS_BC17_OR_HIGHER_ENABLED +static RValue builtinLayerTilemapGetId(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1.0); + Runner* runner = (Runner*) ctx->runner; + int32_t layerId = resolveLayerIdArg(runner, args[0]); + if (0 > layerId) return RValue_makeReal(-1.0); + + RoomLayer* foundLayer = Runner_findRoomLayerById(runner, layerId); + if (foundLayer != nullptr && foundLayer->type == RoomLayerType_Tiles) { + return RValue_makeReal(layerId); + } + + return RValue_makeReal(-1.0); +} + +static RValue builtinDrawTilemap(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (3 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t tilemap_layer_id = RValue_toInt32(args[0]); + GMLReal x = RValue_toReal(args[1]); + GMLReal y = RValue_toReal(args[2]); + + RoomLayer* foundLayer = Runner_findRoomLayerById(runner, tilemap_layer_id); + if (foundLayer != nullptr && foundLayer->type == RoomLayerType_Tiles) { + float alpha = 1.0f; + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, tilemap_layer_id); + if (runtimeLayer != nullptr) { + TileLayerState* layer = getOrCreateTileLayer(runner, runtimeLayer->depth); + alpha = layer->alpha; + uint32_t layerIndex = (uint32_t) (runtimeLayer - runner->runtimeLayers); + Runner_drawTileLayer(runner, layerIndex, foundLayer->tilesData, x, y, alpha); + } + } + + return RValue_makeUndefined(); +} + +// tilemap_x / tilemap_y set the runtime layer's draw offset for the tile layer identified by the tilemap element id. +static RValue builtinTilemapX(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t tilemapElementId = RValue_toInt32(args[0]); + GMLReal x = RValue_toReal(args[1]); + + RoomLayer* foundLayer = Runner_findRoomLayerById(runner, tilemapElementId); + if (foundLayer == nullptr || foundLayer->type != RoomLayerType_Tiles) return RValue_makeUndefined(); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, tilemapElementId); + if (runtimeLayer != nullptr) runtimeLayer->xOffset = (float) x; + return RValue_makeUndefined(); +} + +static RValue builtinTilemapY(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (2 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t tilemapElementId = RValue_toInt32(args[0]); + GMLReal y = RValue_toReal(args[1]); + + RoomLayer* foundLayer = Runner_findRoomLayerById(runner, tilemapElementId); + if (foundLayer == nullptr || foundLayer->type != RoomLayerType_Tiles) return RValue_makeUndefined(); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, tilemapElementId); + if (runtimeLayer != nullptr) runtimeLayer->yOffset = (float) y; + return RValue_makeUndefined(); +} + +static RValue builtinTilemapGetX(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1.0); + Runner* runner = (Runner*) ctx->runner; + int32_t tilemapElementId = RValue_toInt32(args[0]); + + RoomLayer* foundLayer = Runner_findRoomLayerById(runner, tilemapElementId); + if (foundLayer == nullptr || foundLayer->type != RoomLayerType_Tiles) return RValue_makeReal(-1.0); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, tilemapElementId); + if (runtimeLayer == nullptr) return RValue_makeReal(-1.0); + return RValue_makeReal((GMLReal) runtimeLayer->xOffset); +} + +static RValue builtinTilemapGetY(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (1 > argCount) return RValue_makeReal(-1.0); + Runner* runner = (Runner*) ctx->runner; + int32_t tilemapElementId = RValue_toInt32(args[0]); + + RoomLayer* foundLayer = Runner_findRoomLayerById(runner, tilemapElementId); + if (foundLayer == nullptr || foundLayer->type != RoomLayerType_Tiles) return RValue_makeReal(-1.0); + + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, tilemapElementId); + if (runtimeLayer == nullptr) return RValue_makeReal(-1.0); + return RValue_makeReal((GMLReal) runtimeLayer->yOffset); +} + +static RValue builtinLayerGetAll(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + RValue arr = VM_createArray(ctx); + int32_t i = 0; + size_t count = arrlenu(runner->runtimeLayers); + repeat(count, layerIndex) { + VM_arraySet(ctx, &arr, i++, RValue_makeReal((GMLReal) runner->runtimeLayers[layerIndex].id)); + } + return arr; +} + +static RValue builtinLayerGetIdAtDepth(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t targetDepth = RValue_toInt32(args[0]); + RValue arr = VM_createArray(ctx); + int32_t i = 0; + size_t count = arrlenu(runner->runtimeLayers); + repeat(count, layerIndex) { + if (runner->runtimeLayers[layerIndex].depth == targetDepth) { + VM_arraySet(ctx, &arr, i++, RValue_makeReal((GMLReal) runner->runtimeLayers[layerIndex].id)); + } + } + // When no layer matches, return [-1] instead of an empty array. + if (i == 0) + VM_arraySet(ctx, &arr, 0, RValue_makeReal(-1.0)); + return arr; +} +#endif + +static RValue builtinLayerVspeed(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + int32_t id = resolveLayerIdArg(runner, args[0]); + float vs = (float) RValue_toReal(args[1]); + RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, id); + if (runtimeLayer != nullptr) runtimeLayer->vSpeed = vs; + return RValue_makeUndefined(); +} + +// ===[ Array Functions ]=== + +// @@NewGMLArray@@ - GMS2 internal function to create a new array literal (e.g. `[1, 2, 3]`). +// Allocates a fresh GMLArray populated with the argument values. +static RValue builtinNewGMLArray(VMContext* ctx, RValue* args, int32_t argCount) { + RValue arr = VM_createArray(ctx); + repeat(argCount, i) { + VM_arraySet(ctx, &arr, i, args[i]); + } + return arr; +} + +// array_create - GMS2 internal function to create a new array. +// Allocates a fresh GMLArray populated with the argument values. +static RValue builtinArrayCreate(VMContext* ctx, RValue* args, int32_t argCount) { + RValue arr = VM_createArray(ctx); + RValue fill = (argCount > 1) ? args[1] : RValue_makeUndefined(); + repeat(RValue_toReal(args[0]), i) { + VM_arraySet(ctx, &arr, i, fill); + } + return arr; +} + +// @@This@@ - GMS2 internal function returning the current instance's ID. +// Emitted by the GMS2 compiler for expressions like `self` when used as a value. +static RValue builtinThis(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr) return RValue_makeInt32(INSTANCE_SELF); + return RValue_makeInt32((int32_t) inst->instanceId); +} + +// @@Other@@ - GMS2 internal function returning the "other" instance's ID. +// Falls back to the current instance when there is no other (matches GML semantics outside with/collision). +static RValue builtinOther(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Instance* other = (Instance*) ctx->otherInstance; + if (other != nullptr) return RValue_makeInt32((int32_t) other->instanceId); + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr) return RValue_makeInt32(INSTANCE_SELF); + return RValue_makeInt32((int32_t) inst->instanceId); +} + +#if IS_BC17_OR_HIGHER_ENABLED +// @@NullObject@@ - GMS2 internal sentinel pushed before "method()" when the GML source is a struct literal or anonymous constructor: the bound self is "nothing yet", and @@NewGMLObject@@ rebinds to the fresh struct. +// We encode it as INSTANCE_NOONE so "method()" stores it as is (its -1 -> current remap does not fire). +static RValue builtinNullObject(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + return RValue_makeInt32(INSTANCE_NOONE); +} + +// @@NewGMLObject@@(methodRef, ...args) - GMS2 internal function that allocates a fresh struct instance, runs the constructor method against it, and returns the new instance ID. +// We reuse Instance (with objectIndex = -1) the same way globalScopeInstance is used for GLOB scripts, instead of introducing a separate struct type. +static RValue builtinNewGMLObject(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) { + fprintf(stderr, "VM: @@NewGMLObject@@ called with no arguments\n"); + return RValue_makeUndefined(); + } + + Runner* runner = (Runner*) ctx->runner; + int32_t codeIndex; + if (args[0].type == RVALUE_METHOD && args[0].method != nullptr) { + codeIndex = args[0].method->codeIndex; + } else { + // Raw funcIdx pushed via "Push.i ; Conv.i.v" (no method() wrapper used when no static binding is needed). + // Resolve via FUNC chunk name -> codeIndexByName, matching builtinMethod's lookup. + int32_t rawArg = RValue_toInt32(args[0]); + codeIndex = rawArg; + if (rawArg >= 0 && (uint32_t) rawArg < ctx->dataWin->func.functionCount) { + const char* funcName = ctx->dataWin->func.functions[rawArg].name; + if (funcName != nullptr) { + ptrdiff_t idx = shgeti(ctx->codeIndexByName, (char*) funcName); + if (idx >= 0) codeIndex = ctx->codeIndexByName[idx].value; + } + } + } + if (0 > codeIndex || (uint32_t) codeIndex > ctx->dataWin->code.count) { + fprintf(stderr, "VM: @@NewGMLObject@@ method has invalid codeIndex %d\n", codeIndex); + return RValue_makeUndefined(); + } + + Instance* structInst = Instance_create(runner->nextInstanceId++, -1, 0, 0); + hmput(runner->instancesById, structInst->instanceId, structInst); + structInst->structRegistryIndex = (int32_t) arrlen(runner->structInstances); + arrput(runner->structInstances, structInst); + // Two refs at birth: one for the registry's implicit ref (structInstances), one for the returned RValue. + structInst->refCount = 2; + + Instance* savedSelf = (Instance*) ctx->currentInstance; + ctx->currentInstance = structInst; + + RValue* ctorArgs = (argCount > 1) ? &args[1] : nullptr; + int32_t ctorArgCount = argCount - 1; + RValue result = VM_callCodeIndex(ctx, codeIndex, ctorArgs, ctorArgCount); + RValue_free(&result); + + ctx->currentInstance = savedSelf; + return RValue_makeStruct(structInst); +} +#endif + +// ===[ PATH FUNCTIONS ]=== + +// path_add() - create a new empty path, return its index +static RValue builtinPathAdd(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = (Runner*) ctx->runner; + PathChunk* pc = &runner->dataWin->path; + uint32_t newIdx = pc->count; + GamePath* paths = (GamePath*) realloc(pc->paths, (newIdx + 1) * sizeof(GamePath)); + if (paths == nullptr) return RValue_makeInt32(-1); + pc->paths = paths; + GamePath* p = &paths[newIdx]; + memset(p, 0, sizeof(GamePath)); + p->name = ""; + p->isSmooth = false; + p->isClosed = false; + p->precision = 4; + p->pointCount = 0; + p->points = nullptr; + p->internalPointCount = 0; + p->internalPoints = nullptr; + p->length = 0.0; + pc->count = newIdx + 1; + return RValue_makeInt32((int32_t) newIdx); +} + +// path_clear_points(path) +static RValue builtinPathClearPoints(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t idx = RValue_toInt32(args[0]); + if (0 > idx || (uint32_t) idx >= runner->dataWin->path.count) return RValue_makeUndefined(); + GamePath* p = &runner->dataWin->path.paths[idx]; + free(p->points); + p->points = nullptr; + p->pointCount = 0; + free(p->internalPoints); + p->internalPoints = nullptr; + p->internalPointCount = 0; + p->length = 0.0; + return RValue_makeUndefined(); +} + +// path_add_point(path, x, y, speed) +static RValue builtinPathAddPoint(VMContext* ctx, RValue* args, int32_t argCount) { + if (4 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t idx = RValue_toInt32(args[0]); + if (0 > idx || (uint32_t) idx >= runner->dataWin->path.count) return RValue_makeUndefined(); + GamePath* p = &runner->dataWin->path.paths[idx]; + PathPoint* pts = (PathPoint*) realloc(p->points, (p->pointCount + 1) * sizeof(PathPoint)); + if (pts == nullptr) return RValue_makeUndefined(); + p->points = pts; + pts[p->pointCount].x = (float) RValue_toReal(args[1]); + pts[p->pointCount].y = (float) RValue_toReal(args[2]); + pts[p->pointCount].speed = (float) RValue_toReal(args[3]); + p->pointCount++; + GamePath_computeInternal(p); + return RValue_makeUndefined(); +} + +// path_exists(path) +static RValue builtinPathExists(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + int32_t idx = RValue_toInt32(args[0]); + bool exists = (idx >= 0) && ((uint32_t) idx < runner->dataWin->path.count); + return RValue_makeBool(exists); +} + +// path_delete(path) - we don't reclaim the slot (would require remapping indices); zero it out +static RValue builtinPathDelete(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t idx = RValue_toInt32(args[0]); + if (0 > idx || (uint32_t) idx >= runner->dataWin->path.count) return RValue_makeUndefined(); + GamePath* p = &runner->dataWin->path.paths[idx]; + free(p->points); p->points = nullptr; p->pointCount = 0; + free(p->internalPoints); p->internalPoints = nullptr; p->internalPointCount = 0; + p->length = 0.0; + return RValue_makeUndefined(); +} + +// ===[ MP_GRID FUNCTIONS ]=== + +static MpGrid* mpGridGet(Runner* runner, int32_t id) { + if (0 > id || (int32_t) arrlen(runner->mpGridPool) <= id) return nullptr; + MpGrid* g = &runner->mpGridPool[id]; + if (!g->inUse) return nullptr; + return g; +} + +// mp_grid_create(left, top, hcells, vcells, cellwidth, cellheight) +static RValue builtinMpGridCreate(VMContext* ctx, RValue* args, int32_t argCount) { + if (6 > argCount) return RValue_makeInt32(-1); + Runner* runner = (Runner*) ctx->runner; + MpGrid g; + g.inUse = true; + g.left = RValue_toReal(args[0]); + g.top = RValue_toReal(args[1]); + g.hcells = RValue_toInt32(args[2]); + g.vcells = RValue_toInt32(args[3]); + g.cellWidth = RValue_toReal(args[4]); + g.cellHeight = RValue_toReal(args[5]); + if (g.hcells <= 0 || g.vcells <= 0) return RValue_makeInt32(-1); + g.cells = (uint8_t*) calloc((size_t) g.hcells * (size_t) g.vcells, 1); + int32_t id = (int32_t) arrlen(runner->mpGridPool); + arrput(runner->mpGridPool, g); + return RValue_makeInt32(id); +} + +static RValue builtinMpGridDestroy(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + int32_t id = RValue_toInt32(args[0]); + MpGrid* g = mpGridGet(runner, id); + if (g == nullptr) return RValue_makeUndefined(); + free(g->cells); + g->cells = nullptr; + g->inUse = false; + return RValue_makeUndefined(); +} + +static RValue builtinMpGridClearAll(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); + if (g == nullptr) return RValue_makeUndefined(); + memset(g->cells, 0, (size_t) g->hcells * (size_t) g->vcells); + return RValue_makeUndefined(); +} + +static RValue builtinMpGridAddCell(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); + if (g == nullptr) return RValue_makeUndefined(); + int32_t cx = RValue_toInt32(args[1]); + int32_t cy = RValue_toInt32(args[2]); + if (cx < 0 || cy < 0 || cx >= g->hcells || cy >= g->vcells) return RValue_makeUndefined(); + g->cells[cx * g->vcells + cy] = 1; + return RValue_makeUndefined(); +} + +static RValue builtinMpGridClearCell(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); + if (g == nullptr) return RValue_makeUndefined(); + int32_t cx = RValue_toInt32(args[1]); + int32_t cy = RValue_toInt32(args[2]); + if (cx < 0 || cy < 0 || cx >= g->hcells || cy >= g->vcells) return RValue_makeUndefined(); + g->cells[cx * g->vcells + cy] = 0; + return RValue_makeUndefined(); +} + +static RValue builtinMpGridAddRectangle(VMContext* ctx, RValue* args, int32_t argCount) { + if (5 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); + if (g == nullptr) return RValue_makeUndefined(); + int32_t x1 = RValue_toInt32(args[1]); + int32_t y1 = RValue_toInt32(args[2]); + int32_t x2 = RValue_toInt32(args[3]); + int32_t y2 = RValue_toInt32(args[4]); + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 >= g->hcells) x2 = g->hcells - 1; + if (y2 >= g->vcells) y2 = g->vcells - 1; + for (int32_t cx = x1; x2 >= cx; cx++) { + for (int32_t cy = y1; y2 >= cy; cy++) { + g->cells[cx * g->vcells + cy] = 1; + } + } + return RValue_makeUndefined(); +} + +static RValue builtinMpGridClearRectangle(VMContext* ctx, RValue* args, int32_t argCount) { + if (5 > argCount) return RValue_makeUndefined(); + Runner* runner = (Runner*) ctx->runner; + MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); + if (g == nullptr) return RValue_makeUndefined(); + int32_t x1 = RValue_toInt32(args[1]); + int32_t y1 = RValue_toInt32(args[2]); + int32_t x2 = RValue_toInt32(args[3]); + int32_t y2 = RValue_toInt32(args[4]); + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 >= g->hcells) x2 = g->hcells - 1; + if (y2 >= g->vcells) y2 = g->vcells - 1; + for (int32_t cx = x1; x2 >= cx; cx++) { + for (int32_t cy = y1; y2 >= cy; cy++) { + g->cells[cx * g->vcells + cy] = 0; + } + } + return RValue_makeUndefined(); +} + +static RValue builtinMpGridGetCell(VMContext* ctx, RValue* args, int32_t argCount) { + if (3 > argCount) return RValue_makeInt32(0); + Runner* runner = (Runner*) ctx->runner; + MpGrid* g = mpGridGet(runner, RValue_toInt32(args[0])); + if (g == nullptr) return RValue_makeInt32(0); + int32_t cx = RValue_toInt32(args[1]); + int32_t cy = RValue_toInt32(args[2]); + if (cx < 0 || cy < 0 || cx >= g->hcells || cy >= g->vcells) return RValue_makeInt32(0); + // Native returns -1 for blocked, 0 for clear + return RValue_makeInt32(g->cells[cx * g->vcells + cy] ? -1 : 0); +} + +static RValue builtinMpGridDraw(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + return RValue_makeUndefined(); +} + +// mp_grid_path(id, path, xstart, ystart, xgoal, ygoal, allowDiagonals) +// BFS pathfinder: fills `path` with cell-center waypoints from start to goal. +// Returns true if a path was found. +static RValue builtinMpGridPath(VMContext* ctx, RValue* args, int32_t argCount) { + if (7 > argCount) return RValue_makeBool(false); + Runner* runner = (Runner*) ctx->runner; + MpGrid* mp = mpGridGet(runner, RValue_toInt32(args[0])); + if (mp == nullptr) return RValue_makeBool(false); + int32_t pathIdx = RValue_toInt32(args[1]); + if (0 > pathIdx || (uint32_t) pathIdx >= runner->dataWin->path.count) return RValue_makeBool(false); + GamePath* pPath = &runner->dataWin->path.paths[pathIdx]; + + GMLReal xstart = RValue_toReal(args[2]); + GMLReal ystart = RValue_toReal(args[3]); + GMLReal xgoal = RValue_toReal(args[4]); + GMLReal ygoal = RValue_toReal(args[5]); + bool allowdiag = RValue_toBool(args[6]); + + // Find the start & goal cells & check them. + int32_t cxs = (int32_t) GMLReal_floor((xstart - mp->left) / mp->cellWidth); + int32_t cys = (int32_t) GMLReal_floor((ystart - mp->top) / mp->cellHeight); + int32_t cxg = (int32_t) GMLReal_floor((xgoal - mp->left) / mp->cellWidth); + int32_t cyg = (int32_t) GMLReal_floor((ygoal - mp->top) / mp->cellHeight); + + if (cxs < 0 || cxs >= mp->hcells || cys < 0 || cys >= mp->vcells) return RValue_makeBool(false); + if (cxg < 0 || cxg >= mp->hcells || cyg < 0 || cyg >= mp->vcells) return RValue_makeBool(false); + if (mp->cells[cxs * mp->vcells + cys]) return RValue_makeBool(false); + if (mp->cells[cxg * mp->vcells + cyg]) return RValue_makeBool(false); + + // Start the search. + int32_t total = mp->hcells * mp->vcells; + int32_t* dist = (int32_t*) malloc(total * sizeof(int32_t)); + int32_t* qq = (int32_t*) malloc(total * sizeof(int32_t)); + if (dist == nullptr || qq == nullptr) { + free(dist); free(qq); + return RValue_makeBool(false); + } + for (int32_t i = 0; total > i; i++) dist[i] = -1; + + int32_t startIdx = cxs * mp->vcells + cys; + int32_t goalIdx = cxg * mp->vcells + cyg; + int32_t head = 0, tail = 0; + dist[startIdx] = 1; + qq[tail++] = startIdx; + + bool result = false; + while (tail > head) { + int32_t val = qq[head++]; + int32_t xx = val / mp->vcells; + int32_t yy = val % mp->vcells; + if (xx == cxg && yy == cyg) { + result = true; + break; + } + int32_t d = dist[val] + 1; + + bool f1 = (xx > 0) && (yy < mp->vcells - 1) && (dist[(xx - 1) * mp->vcells + (yy + 1)] == -1) && !mp->cells[(xx - 1) * mp->vcells + (yy + 1)]; + bool f2 = (yy < mp->vcells - 1) && (dist[xx * mp->vcells + (yy + 1)] == -1) && !mp->cells[xx * mp->vcells + (yy + 1)]; + bool f3 = (xx < mp->hcells - 1) && (yy < mp->vcells - 1) && (dist[(xx + 1) * mp->vcells + (yy + 1)] == -1) && !mp->cells[(xx + 1) * mp->vcells + (yy + 1)]; + bool f4 = (xx > 0) && (dist[(xx - 1) * mp->vcells + yy] == -1) && !mp->cells[(xx - 1) * mp->vcells + yy]; + bool f6 = (xx < mp->hcells - 1) && (dist[(xx + 1) * mp->vcells + yy] == -1) && !mp->cells[(xx + 1) * mp->vcells + yy]; + bool f7 = (xx > 0) && (yy > 0) && (dist[(xx - 1) * mp->vcells + (yy - 1)] == -1) && !mp->cells[(xx - 1) * mp->vcells + (yy - 1)]; + bool f8 = (yy > 0) && (dist[xx * mp->vcells + (yy - 1)] == -1) && !mp->cells[xx * mp->vcells + (yy - 1)]; + bool f9 = (xx < mp->hcells - 1) && (yy > 0) && (dist[(xx + 1) * mp->vcells + (yy - 1)] == -1) && !mp->cells[(xx + 1) * mp->vcells + (yy - 1)]; + + // Handle horizontal & vertical moves. + if (f4) { + dist[(xx - 1) * mp->vcells + yy] = d; + qq[tail++] = (xx - 1) * mp->vcells + yy; + } + if (f6) { + dist[(xx + 1) * mp->vcells + yy] = d; + qq[tail++] = (xx + 1) * mp->vcells + yy; + } + if (f8) { + dist[xx * mp->vcells + (yy - 1)] = d; + qq[tail++] = xx * mp->vcells + (yy - 1); + } + if (f2) { + dist[xx * mp->vcells + (yy + 1)] = d; + qq[tail++] = xx * mp->vcells + (yy + 1); + } + // Handle diagonal moves (require both cardinal neighbors clear, matching HTML5). + if (allowdiag && f1 && f2 && f4) { + dist[(xx - 1) * mp->vcells + (yy + 1)] = d; + qq[tail++] = (xx - 1) * mp->vcells + (yy + 1); + } + if (allowdiag && f7 && f8 && f4) { + dist[(xx - 1) * mp->vcells + (yy - 1)] = d; + qq[tail++] = (xx - 1) * mp->vcells + (yy - 1); + } + if (allowdiag && f3 && f2 && f6) { + dist[(xx + 1) * mp->vcells + (yy + 1)] = d; + qq[tail++] = (xx + 1) * mp->vcells + (yy + 1); + } + if (allowdiag && f9 && f8 && f6) { + dist[(xx + 1) * mp->vcells + (yy - 1)] = d; + qq[tail++] = (xx + 1) * mp->vcells + (yy - 1); + } + } + + if (!result) { + free(dist); free(qq); + return RValue_makeBool(false); + } + + // Compute the path from back to front. At each step, scan neighbors with dist == val-1 in the order LEFT, RIGHT, UP, DOWN, then diagonals + int32_t chainCap = 16; + int32_t chainLen = 0; + int32_t* chain = (int32_t*) malloc(chainCap * sizeof(int32_t)); + { + int32_t xx = cxg; + int32_t yy = cyg; + chain[chainLen++] = xx * mp->vcells + yy; + while (xx != cxs || yy != cys) { + if (chainLen >= chainCap) { + chainCap *= 2; + chain = (int32_t*) realloc(chain, chainCap * sizeof(int32_t)); + } + int32_t val = dist[xx * mp->vcells + yy]; + bool f1 = (xx > 0) && (yy < mp->vcells - 1) && (dist[(xx - 1) * mp->vcells + (yy + 1)] == val - 1); + bool f2 = (yy < mp->vcells - 1) && (dist[xx * mp->vcells + (yy + 1)] == val - 1); + bool f3 = (xx < mp->hcells - 1) && (yy < mp->vcells - 1) && (dist[(xx + 1) * mp->vcells + (yy + 1)] == val - 1); + bool f4 = (xx > 0) && (dist[(xx - 1) * mp->vcells + yy] == val - 1); + bool f6 = (xx < mp->hcells - 1) && (dist[(xx + 1) * mp->vcells + yy] == val - 1); + bool f7 = (xx > 0) && (yy > 0) && (dist[(xx - 1) * mp->vcells + (yy - 1)] == val - 1); + bool f8 = (yy > 0) && (dist[xx * mp->vcells + (yy - 1)] == val - 1); + bool f9 = (xx < mp->hcells - 1) && (yy > 0) && (dist[(xx + 1) * mp->vcells + (yy - 1)] == val - 1); + + // Four directions movement + if (f4) { xx = xx - 1; } else if (f6) { xx = xx + 1; } else if (f8) { yy = yy - 1; } else if (f2) { yy = yy + 1; } else if (allowdiag && f1) { + xx = xx - 1; + yy = yy + 1; + } else if (allowdiag && f3) { + xx = xx + 1; + yy = yy + 1; + } else if (allowdiag && f7) { + xx = xx - 1; + yy = yy - 1; + } else if (allowdiag && f9) { + xx = xx + 1; + yy = yy - 1; + } else { + // Should be unreachable: BFS reached goal, so a predecessor must exist. + free(chain); + free(dist); + free(qq); + return RValue_makeBool(false); + } + chain[chainLen++] = xx * mp->vcells + yy; + } + } + + // Build the output path. + // We walk "chain" in reverse to emit start-first, with explicit overrides so the endpoints are exactly (xstart, ystart) / (xgoal, ygoal) instead of cell centers. + free(pPath->points); + pPath->points = nullptr; + pPath->pointCount = 0; + + // When start cell == goal cell, chain has 1 node but the native runner and GameMaker-HTML5 still emit a 2-point path (start coord + goal coord). + // Without this, the path length is 0, adaptPath early-returns before advancing pathPosition past 1.0, and the OTHER_END_OF_PATH event never fires. + int32_t pointCount = (startIdx == goalIdx) ? 2 : chainLen; + pPath->points = (PathPoint*) malloc(pointCount * sizeof(PathPoint)); + pPath->pointCount = (uint32_t) pointCount; + for (int32_t i = 0; pointCount > i; i++) { + float wx, wy; + if (startIdx == goalIdx) { + wx = (float) (i == 0 ? xstart : xgoal); + wy = (float) (i == 0 ? ystart : ygoal); + } else { + int32_t idx = chain[chainLen - 1 - i]; + int32_t xx = idx / mp->vcells; + int32_t yy = idx % mp->vcells; + wx = (float) (mp->left + (xx + 0.5) * mp->cellWidth); + wy = (float) (mp->top + (yy + 0.5) * mp->cellHeight); + if (i == 0) { wx = (float) xstart; wy = (float) ystart; } + if (i == chainLen - 1) { wx = (float) xgoal; wy = (float) ygoal; } + } + pPath->points[i].x = wx; + pPath->points[i].y = wy; + pPath->points[i].speed = 100.0f; + } + free(chain); + free(dist); + free(qq); + + free(pPath->internalPoints); + pPath->internalPoints = nullptr; + pPath->internalPointCount = 0; + pPath->length = 0.0f; + GamePath_computeInternal(pPath); + + return RValue_makeBool(true); +} + +// path_start(path, speed, endaction, absolute) - HTML5: Assign_Path (yyInstance.js:2695-2743) +static RValue builtinPathStart(VMContext* ctx, RValue* args, int32_t argCount) { + if (4 > argCount) return RValue_makeUndefined(); + + Instance* inst = (Instance*) ctx->currentInstance; + if (inst == nullptr) return RValue_makeUndefined(); + + Runner* runner = (Runner*) ctx->runner; + int32_t pathIdx = RValue_toInt32(args[0]); + GMLReal speed = RValue_toReal(args[1]); + int32_t endAction = RValue_toInt32(args[2]); + bool absolute = RValue_toBool(args[3]); + + // Validate path index + inst->pathIndex = -1; + if (0 > pathIdx) return RValue_makeUndefined(); + if ((uint32_t) pathIdx >= runner->dataWin->path.count) return RValue_makeUndefined(); + + GamePath* path = &runner->dataWin->path.paths[pathIdx]; + if (0.0 >= path->length) return RValue_makeUndefined(); + + inst->pathIndex = pathIdx; + inst->pathSpeed = (float) speed; + + if (inst->pathSpeed >= 0.0f) { + inst->pathPosition = 0.0f; + } else { + inst->pathPosition = 1.0f; + } + + inst->pathPositionPrevious = inst->pathPosition; + inst->pathScale = 1.0f; + inst->pathOrientation = 0.0f; + inst->pathEndAction = endAction; + + if (absolute) { + PathPositionResult startPos = GamePath_getPosition(path, inst->pathSpeed >= 0.0f ? 0.0f : 1.0f); + inst->x = (float) startPos.x; + inst->y = (float) startPos.y; + SpatialGrid_markInstanceAsDirty(ctx->runner->spatialGrid, inst); + + PathPositionResult origin = GamePath_getPosition(path, 0.0f); + inst->pathXStart = (float) origin.x; + inst->pathYStart = (float) origin.y; + } else { + inst->pathXStart = inst->x; + inst->pathYStart = inst->y; + } + + return RValue_makeUndefined(); +} + +// path_get_length(path) - returns total length of the path in pixels +static RValue builtinPathGetLength(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeReal(0.0); + Runner* runner = (Runner*) ctx->runner; + int32_t pathIdx = RValue_toInt32(args[0]); + if (0 > pathIdx) return RValue_makeReal(0.0); + if ((uint32_t) pathIdx >= runner->dataWin->path.count) return RValue_makeReal(0.0); + return RValue_makeReal((GMLReal) runner->dataWin->path.paths[pathIdx].length); +} + +// path_end() - HTML5: Assign_Path(-1,...) +static RValue builtinPathEnd(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + Instance* inst = (Instance*) ctx->currentInstance; + if (inst != nullptr) { + inst->pathIndex = -1; + } + return RValue_makeUndefined(); +} + +// string_hash_to_newline - converts # to \n in a string +static RValue builtinStringHashToNewline(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) return RValue_makeString(""); + RValue original = args[0]; // This is a copy + + if (original.type != RVALUE_STRING) { + // Fast path: If the argument is not a string, return a copy of it + return RValue_makeOwnedString(RValue_toString(original)); + } + + if (original.string == nullptr) { + // Fast path: If the argument is a string but has no value, return an empty string + return RValue_makeString(""); + } + + PreprocessedText result = TextUtils_preprocessGmlText(original.string); + if (!result.owning) { + // No # found, steal the reference to avoid copying the string + args[0].ownsReference = false; + return original; + } + return RValue_makeOwnedString((char*) result.text); +} + +// json_decode +static RValue builtinJsonDecode(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) { + fprintf(stderr, "[json_decode] Expected at least 1 argument\n"); + return RValue_makeUndefined(); + } + + Runner* runner = (Runner*) ctx->runner; + int32_t mapIndex = dsMapCreate(runner); + DsMapEntry **mapPtr = dsMapGet(runner, mapIndex); + const char* content = args[0].string; + const JsonValue* json = JsonReader_parse(content); + + repeat(JsonReader_objectLength(json), i) { + const char *key = safeStrdup(JsonReader_getObjectKey(json, i)); + RValue val = RValue_makeOwnedString(safeStrdup(JsonReader_getString(JsonReader_getObjectValue(json, i)))); + shput(*mapPtr, key, val); + } + + JsonReader_free(json); + + return RValue_makeReal(mapIndex); +} + +static RValue builtinObjectGetSprite(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) { + fprintf(stderr, "[object_get_sprite] Expected at least 1 argument\n"); + return RValue_makeUndefined(); + } + + int32_t id = RValue_toInt32(args[0]); + + return RValue_makeReal(ctx->dataWin->objt.objects[id].spriteId); +} + +// Shared implementation for font_add_sprite and font_add_sprite_ext +static RValue fontAddSpriteImpl(VMContext* ctx, int32_t spriteIndex, uint16_t* charCodes, uint32_t charCount, bool proportional, int32_t sep) { + DataWin* dw = ctx->dataWin; + + if (0 > spriteIndex || (uint32_t) spriteIndex >= dw->sprt.count) { + fprintf(stderr, "[font_add_sprite] Invalid sprite index %d\n", spriteIndex); + return RValue_makeReal(-1.0); + } + + Sprite* sprite = &dw->sprt.sprites[spriteIndex]; + + if (charCount == 0 || sprite->textureCount == 0) { + return RValue_makeReal(-1.0); + } + + // Limit glyph count to sprite frame count + uint32_t glyphCount = charCount; + if (glyphCount > sprite->textureCount) glyphCount = sprite->textureCount; + + // Compute emSize (max bounding height across all frames) and biggestShift + uint32_t maxHeight = 0; + int32_t biggestShift = 0; + repeat(glyphCount, i) { + int32_t tpagIdx = sprite->tpagIndices[i]; + if (0 > tpagIdx) continue; + TexturePageItem* tpag = &dw->tpag.items[tpagIdx]; + if (tpag->boundingHeight > maxHeight) maxHeight = tpag->boundingHeight; + int32_t width = proportional ? (int32_t) tpag->sourceWidth : (int32_t) tpag->boundingWidth; + if (width > biggestShift) biggestShift = width; + } + + // Check if space (0x20) is in the string map + bool hasSpace = false; + repeat(glyphCount, i) { + if (charCodes[i] == 0x20) { hasSpace = true; break; } + } + + // Allocate glyphs (+ 1 for synthetic space if needed) + uint32_t totalGlyphs = hasSpace ? glyphCount : glyphCount + 1; + FontGlyph* glyphs = safeMalloc(totalGlyphs * sizeof(FontGlyph)); + + repeat(glyphCount, i) { + int32_t tpagIdx = sprite->tpagIndices[i]; + FontGlyph* glyph = &glyphs[i]; + glyph->character = charCodes[i]; + glyph->kerningCount = 0; + glyph->kerning = nullptr; + + if (0 > tpagIdx) { + glyph->sourceX = 0; + glyph->sourceY = 0; + glyph->sourceWidth = 0; + glyph->sourceHeight = 0; + glyph->shift = (int16_t) sep; + glyph->offset = 0; + continue; + } + + TexturePageItem* tpag = &dw->tpag.items[tpagIdx]; + glyph->sourceX = 0; // not used for sprite fonts (TPAG resolved per glyph) + glyph->sourceY = 0; + glyph->sourceWidth = tpag->sourceWidth; + glyph->sourceHeight = tpag->sourceHeight; + + int32_t advanceWidth = proportional ? (int32_t) tpag->sourceWidth : (int32_t) tpag->boundingWidth; + glyph->shift = (int16_t) (advanceWidth + sep); + + // Horizontal offset: for proportional fonts, no offset; for non-proportional, use target offset minus origin + glyph->offset = proportional ? 0 : (int16_t) ((int32_t) tpag->targetX - sprite->originX); + } + + // Add synthetic space glyph if space is not in the string map + if (!hasSpace) { + FontGlyph* spaceGlyph = &glyphs[glyphCount]; + spaceGlyph->character = 0x20; + spaceGlyph->sourceX = 0; + spaceGlyph->sourceY = 0; + spaceGlyph->sourceWidth = 0; + spaceGlyph->sourceHeight = 0; + spaceGlyph->shift = (int16_t) (biggestShift + sep); + spaceGlyph->offset = 0; + spaceGlyph->kerningCount = 0; + spaceGlyph->kerning = nullptr; + } + + // Grow the font array and create the new font + uint32_t newFontIndex = dw->font.count; + dw->font.count++; + dw->font.fonts = safeRealloc(dw->font.fonts, dw->font.count * sizeof(Font)); + + Font* font = &dw->font.fonts[newFontIndex]; + font->name = "sprite_font"; + font->displayName = "sprite_font"; + font->emSize = (maxHeight > 0) ? maxHeight : sprite->height; + font->bold = false; + font->italic = false; + font->rangeStart = 0; + font->charset = 0; + font->antiAliasing = 0; + font->rangeEnd = 0; + font->tpagIndex = -1; // not used for sprite fonts + font->scaleX = 1.0f; + font->scaleY = 1.0f; + font->ascenderOffset = 0; + font->glyphCount = totalGlyphs; + font->glyphs = glyphs; + font->maxGlyphHeight = maxHeight; // match what HTML5 runner uses for line stride + font->isSpriteFont = true; + font->spriteIndex = spriteIndex; + Font_buildGlyphLUT(font); + + return RValue_makeReal((GMLReal) newFontIndex); +} + +static RValue builtinFontGetName(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) { + fprintf(stderr, "[font_get_name] Expected 1 argument, got 0"); + return RValue_makeUndefined(); + } + + int32_t fontIndex = RValue_toInt32(args[0]); + if (0 > fontIndex || (uint32_t) fontIndex >= ctx->dataWin->font.count) return RValue_makeUndefined(); + return RValue_makeString(ctx->dataWin->font.fonts[fontIndex].name); +} + +// font_add_sprite_ext(sprite, string_map, prop, sep) +static RValue builtinFontAddSpriteExt(VMContext* ctx, RValue* args, int32_t argCount) { + if (4 > argCount) { + fprintf(stderr, "[font_add_sprite_ext] Expected 4 arguments, got %d\n", argCount); + return RValue_makeReal(-1.0); + } + + int32_t spriteIndex = RValue_toInt32(args[0]); + char* stringMap = RValue_toString(args[1]); + bool proportional = RValue_toBool(args[2]); + int32_t sep = RValue_toInt32(args[3]); + + // Decode the string map to get character codes (UTF-8 -> codepoints) + int32_t mapLen = (int32_t) strlen(stringMap); + int32_t mapPos = 0; + uint32_t charCount = 0; + uint16_t charCodes[1024]; + while (mapLen > mapPos && 1024 > charCount) { + charCodes[charCount++] = TextUtils_decodeUtf8(stringMap, mapLen, &mapPos); + } + free(stringMap); + + return fontAddSpriteImpl(ctx, spriteIndex, charCodes, charCount, proportional, sep); +} + +// font_add_sprite(sprite, first, prop, sep) +static RValue builtinFontAddSprite(VMContext* ctx, RValue* args, int32_t argCount) { + if (4 > argCount) { + fprintf(stderr, "[font_add_sprite] Expected 4 arguments, got %d\n", argCount); + return RValue_makeReal(-1.0); + } + + DataWin* dw = ctx->dataWin; + int32_t spriteIndex = RValue_toInt32(args[0]); + int32_t first = RValue_toInt32(args[1]); + bool proportional = RValue_toBool(args[2]); + int32_t sep = RValue_toInt32(args[3]); + + // Build sequential character codes: first, first+1, first+2, ... + uint32_t frameCount = 0; + if (spriteIndex >= 0 && dw->sprt.count > (uint32_t) spriteIndex) { + frameCount = dw->sprt.sprites[spriteIndex].textureCount; + } + if (frameCount > 1024) frameCount = 1024; + + uint16_t charCodes[1024]; + repeat(frameCount, i) { + charCodes[i] = (uint16_t) (first + (int32_t) i); + } + + return fontAddSpriteImpl(ctx, spriteIndex, charCodes, frameCount, proportional, sep); +} + +static RValue builtinAssetGetIndex(VMContext* ctx, RValue* args, int32_t argCount) { + if (1 > argCount) { + fprintf(stderr, "[asset_get_index] Expected at least 1 argument\n"); + return RValue_makeUndefined(); + } + + char* name = RValue_toString(args[0]); + DataWin* dw = ctx->dataWin; + + int32_t value = shget(ctx->runner->assetsByName, name); + free(name); + return RValue_makeReal(value); +} + +static RValue builtinGpuSetBlendMode(VMContext* ctx, RValue* args, int32_t argCount) { + int mode = RValue_toReal(args[0]); + ctx->runner->renderer->vtable->gpuSetBlendMode(ctx->runner->renderer, mode); + return RValue_makeUndefined(); +} + +static RValue builtinGpuSetBlendModeExt(VMContext* ctx, RValue* args, int32_t argCount) { + int sfactor = RValue_toReal(args[0]); + int dfactor = RValue_toReal(args[1]); + ctx->runner->renderer->vtable->gpuSetBlendModeExt(ctx->runner->renderer, sfactor, dfactor); + return RValue_makeUndefined(); +} + +static bool isBlendEnable = false; +static RValue builtinGpuSetBlendEnable(VMContext* ctx, RValue* args, int32_t argCount) { + bool enable = RValue_toBool(args[0]); + isBlendEnable = enable; + ctx->runner->renderer->vtable->gpuSetBlendEnable(ctx->runner->renderer, enable); + return RValue_makeUndefined(); +} + +static RValue builtinGpuGetBlendEnable(VMContext* ctx, RValue* args, int32_t argCount) { + return RValue_makeBool(isBlendEnable); +} + +static RValue builtinGpuSetAlphaTestEnable(VMContext* ctx, RValue* args, int32_t argCount) { + bool enable = RValue_toBool(args[0]); + ctx->runner->renderer->vtable->gpuSetAlphaTestEnable(ctx->runner->renderer, enable); + return RValue_makeUndefined(); +} + +static RValue builtinGpuSetAlphaTestRef(VMContext* ctx, RValue* args, int32_t argCount) { + ctx->runner->renderer->vtable->gpuSetAlphaTestRef(ctx->runner->renderer, RValue_toInt32(args[0])); + return RValue_makeUndefined(); +} + +static RValue builtinGpuSetFog(VMContext* ctx, RValue* args, int32_t argCount) { + bool enable; + int32_t color; + if (argCount == 1 && args[0].type == RVALUE_ARRAY && args[0].array != nullptr && GMLArray_length1D(args[0].array) >= 2) { + GMLArray* arr = args[0].array; + enable = RValue_toBool(*GMLArray_slot(arr, 0)); + color = RValue_toInt32(*GMLArray_slot(arr, 1)); + } else if (argCount >= 2) { + enable = RValue_toBool(args[0]); + color = RValue_toInt32(args[1]); + } else { + return RValue_makeUndefined(); + } + if (ctx->runner->renderer->vtable->gpuSetFog != nullptr) { + ctx->runner->renderer->vtable->gpuSetFog(ctx->runner->renderer, enable, (uint32_t) color); + } + return RValue_makeUndefined(); +} + +static RValue builtinGpuSetColorWriteEnable(VMContext* ctx, RValue* args, int32_t argCount) { + bool r, g, b, a; + if (argCount == 1 && args[0].type == RVALUE_ARRAY && args[0].array != nullptr && GMLArray_length1D(args[0].array) >= 4) { + GMLArray* arr = args[0].array; + r = RValue_toBool(*GMLArray_slot(arr, 0)); + g = RValue_toBool(*GMLArray_slot(arr, 1)); + b = RValue_toBool(*GMLArray_slot(arr, 2)); + a = RValue_toBool(*GMLArray_slot(arr, 3)); + } else if (argCount >= 4) { + r = RValue_toBool(args[0]); + g = RValue_toBool(args[1]); + b = RValue_toBool(args[2]); + a = RValue_toBool(args[3]); + } else { + return RValue_makeUndefined(); + } + ctx->runner->renderer->vtable->gpuSetColorWriteEnable(ctx->runner->renderer, r, g, b, a); + return RValue_makeUndefined(); +} + +// ===[ REGISTRATION ]=== + +void VMBuiltins_registerAll(VMContext* ctx) { + requireMessage(!ctx->registeredBuiltinFunctions, "Attempting to register all VMBuiltins, but it was already registered!"); + ctx->registeredBuiltinFunctions = true; + + const bool isGMS2 = DataWin_isVersionAtLeast(ctx->dataWin, 2, 0, 0, 0); + + // Core output + VM_registerBuiltin(ctx, "show_debug_message", builtinShowDebugMessage); + + // String functions + VM_registerBuiltin(ctx, "string_length", builtinStringLength); + VM_registerBuiltin(ctx, "string_letters", builtinStringLetters); + VM_registerBuiltin(ctx, "string_byte_length", builtinStringByteLength); + VM_registerBuiltin(ctx, "string", builtinString); + VM_registerBuiltin(ctx, "string_upper", builtinStringUpper); + VM_registerBuiltin(ctx, "string_lower", builtinStringLower); + VM_registerBuiltin(ctx, "string_copy", builtinStringCopy); + VM_registerBuiltin(ctx, "string_pos", builtinStringPos); + VM_registerBuiltin(ctx, "string_char_at", builtinStringCharAt); + VM_registerBuiltin(ctx, "string_delete", builtinStringDelete); + VM_registerBuiltin(ctx, "string_insert", builtinStringInsert); + VM_registerBuiltin(ctx, "string_replace", builtinStringReplace); + VM_registerBuiltin(ctx, "string_replace_all", builtinStringReplaceAll); + VM_registerBuiltin(ctx, "string_repeat", builtinStringRepeat); + VM_registerBuiltin(ctx, "string_format", builtinStringFormat); + VM_registerBuiltin(ctx, "string_count", builtinStringCount); + VM_registerBuiltin(ctx, "string_digits", builtinStringDigits); + VM_registerBuiltin(ctx, "ord", builtinOrd); + VM_registerBuiltin(ctx, "chr", builtinChr); + + // Type functions + VM_registerBuiltin(ctx, "real", builtinReal); + VM_registerBuiltin(ctx, "is_string", builtinIsString); + VM_registerBuiltin(ctx, "is_real", builtinIsReal); + VM_registerBuiltin(ctx, "is_undefined", builtinIsUndefined); + + // Math functions + VM_registerBuiltin(ctx, "floor", builtinFloor); + VM_registerBuiltin(ctx, "ceil", builtinCeil); + VM_registerBuiltin(ctx, "round", builtinRound); + VM_registerBuiltin(ctx, "abs", builtinAbs); + VM_registerBuiltin(ctx, "sign", builtinSign); + VM_registerBuiltin(ctx, "max", builtinMax); + VM_registerBuiltin(ctx, "min", builtinMin); + VM_registerBuiltin(ctx, "power", builtinPower); + VM_registerBuiltin(ctx, "sqrt", builtinSqrt); + VM_registerBuiltin(ctx, "sqr", builtinSqr); + VM_registerBuiltin(ctx, "sin", builtinSin); + VM_registerBuiltin(ctx, "arcsin", builtinArcsin); + VM_registerBuiltin(ctx, "cos", builtinCos); + VM_registerBuiltin(ctx, "dsin", builtinDsin); + VM_registerBuiltin(ctx, "dcos", builtinDcos); + VM_registerBuiltin(ctx, "darctan2", builtinDarctan2); + VM_registerBuiltin(ctx, "degtorad", builtinDegtorad); + VM_registerBuiltin(ctx, "radtodeg", builtinRadtodeg); + VM_registerBuiltin(ctx, "clamp", builtinClamp); + VM_registerBuiltin(ctx, "lerp", builtinLerp); + VM_registerBuiltin(ctx, "point_distance", builtinPointDistance); + VM_registerBuiltin(ctx, "point_in_rectangle", builtinPointInRectangle); + VM_registerBuiltin(ctx, "point_direction", builtinPointDirection); + VM_registerBuiltin(ctx, "angle_difference", builtinAngleDifference); + VM_registerBuiltin(ctx, "distance_to_point", builtinDistanceToPoint); + VM_registerBuiltin(ctx, "distance_to_object", builtinDistanceToObject); + VM_registerBuiltin(ctx, "move_towards_point", builtinMoveTowardsPoint); + VM_registerBuiltin(ctx, "action_move_point", builtinMoveTowardsPoint); + VM_registerBuiltin(ctx, "move_snap", builtinMoveSnap); + VM_registerBuiltin(ctx, "lengthdir_x", builtinLengthdir_x); + VM_registerBuiltin(ctx, "lengthdir_y", builtinLengthdir_y); + + // Random + VM_registerBuiltin(ctx, "random", builtinRandom); + VM_registerBuiltin(ctx, "random_range", builtinRandomRange); + VM_registerBuiltin(ctx, "irandom", builtinIrandom); + VM_registerBuiltin(ctx, "irandom_range", builtinIrandomRange); + VM_registerBuiltin(ctx, "choose", builtinChoose); + VM_registerBuiltin(ctx, "randomize", builtinRandomize); + VM_registerBuiltin(ctx, "randomise", builtinRandomize); + + // Room + VM_registerBuiltin(ctx, "game_get_speed", builtinGameGetSpeed); + VM_registerBuiltin(ctx, "room_exists", builtinRoomExists); + VM_registerBuiltin(ctx, "room_get_name", builtinRoomGetName); + VM_registerBuiltin(ctx, "room_goto_next", builtinRoomGotoNext); + VM_registerBuiltin(ctx, "room_goto_previous", builtinRoomGotoPrevious); + VM_registerBuiltin(ctx, "room_goto", builtinRoomGoto); + VM_registerBuiltin(ctx, "room_restart", builtinRoomRestart); + VM_registerBuiltin(ctx, "room_next", builtinRoomNext); + VM_registerBuiltin(ctx, "room_previous", builtinRoomPrevious); + VM_registerBuiltin(ctx, "room_set_persistent", builtinRoomSetPersistent); + + // GMS2 camera compatibility + VM_registerBuiltin(ctx, "view_get_camera", builtinViewGetCamera); + VM_registerBuiltin(ctx, "camera_get_view_x", builtinCameraGetViewX); + VM_registerBuiltin(ctx, "camera_get_view_y", builtinCameraGetViewY); + VM_registerBuiltin(ctx, "camera_get_view_width", builtinCameraGetViewWidth); + VM_registerBuiltin(ctx, "camera_get_view_height", builtinCameraGetViewHeight); + VM_registerBuiltin(ctx, "camera_set_view_pos", builtinCameraSetViewPos); + VM_registerBuiltin(ctx, "camera_get_view_target", builtinCameraGetViewTarget); + VM_registerBuiltin(ctx, "camera_set_view_target", builtinCameraSetViewTarget); + VM_registerBuiltin(ctx, "camera_get_view_border_x", builtinCameraGetViewBorderX); + VM_registerBuiltin(ctx, "camera_get_view_border_y", builtinCameraGetViewBorderY); + VM_registerBuiltin(ctx, "camera_set_view_border", builtinCameraSetViewBorder); + + // Variables + VM_registerBuiltin(ctx, "variable_global_exists", builtinVariableGlobalExists); + VM_registerBuiltin(ctx, "variable_global_get", builtinVariableGlobalGet); + VM_registerBuiltin(ctx, "variable_global_set", builtinVariableGlobalSet); + VM_registerBuiltin(ctx, "variable_instance_set", builtinVariableInstanceSet); + VM_registerBuiltin(ctx, "variable_instance_get", builtinVariableInstanceGet); + VM_registerBuiltin(ctx, "variable_instance_exists", builtinVariableInstanceExists); + VM_registerBuiltin(ctx, "variable_struct_set", builtinVariableStructSet); + VM_registerBuiltin(ctx, "variable_struct_get", builtinVariableStructGet); + VM_registerBuiltin(ctx, "variable_struct_exists", builtinVariableStructExists); + + // Script + VM_registerBuiltin(ctx, "script_execute", builtinScriptExecute); + VM_registerBuiltin(ctx, "n3ds_render_bottom_screen", builtinN3DSRenderBottomScreen); + VM_registerBuiltin(ctx, "n3ds_render_battle_interface", builtinN3DSRenderBottomScreen); + VM_registerBuiltin(ctx, "n3ds_render_top_screen", builtinN3DSRenderTopScreen); + VM_registerBuiltin(ctx, "n3ds_render_enemy_top_screen", builtinN3DSRenderTopScreen2x); + VM_registerBuiltin(ctx, "n3ds_render_battle_scene", builtinN3DSRenderBattleScene); + VM_registerBuiltin(ctx, "n3ds_battle_controller_draw", builtinBattleControllerDraw); + + // ---- Native code overrides ---- + // Intercept entire GML event bodies without patching data.win. + VM_registerCodeOverride(ctx, "gml_Object_obj_battlecontroller_Draw_0", builtinBattleControllerDraw); + VM_registerCodeOverride(ctx, "gml_Object_obj_blackborderer_Draw_0", builtinBlackBordererDraw); + VM_registerCodeOverride(ctx, "gml_Object_obj_snowfloor_Draw_0", builtinSnowfloorDraw); + VM_registerCodeOverride(ctx, "gml_Object_obj_piper_steam_Draw_0", builtinPiperSteamDraw); + VM_registerCodeOverride(ctx, "gml_Object_obj_steamplume2_Create_0", builtinSteamplume2Create); + VM_registerCodeOverride(ctx, "gml_Object_obj_steamplume2_Step_0", builtinSteamplume2Step); + VM_registerCodeOverride(ctx, "gml_Object_obj_lastruins_bg_Step_0", builtinLastruinsBgStep); + VM_registerCodeOverride(ctx, "gml_Object_obj_backgrounder_lastruins_Other_10", builtinBackgrounderLastruinsOther10); +#if IS_BC17_OR_HIGHER_ENABLED + VM_registerBuiltin(ctx, "method", builtinMethod); +#endif + + // OS + VM_registerBuiltin(ctx, "os_get_language", builtinOsGetLanguage); + VM_registerBuiltin(ctx, "os_get_region", builtinOsGetRegion); + + // ds_map + VM_registerBuiltin(ctx, "ds_map_create", builtinDsMapCreate); + VM_registerBuiltin(ctx, "ds_map_add", builtinDsMapAdd); + VM_registerBuiltin(ctx, "ds_map_set", builtinDsMapSet); + VM_registerBuiltin(ctx, "ds_map_replace", builtinDsMapReplace); + VM_registerBuiltin(ctx, "ds_map_find_value", builtinDsMapFindValue); + VM_registerBuiltin(ctx, "ds_map_exists", builtinDsMapExists); + VM_registerBuiltin(ctx, "ds_map_find_first", builtinDsMapFindFirst); + VM_registerBuiltin(ctx, "ds_map_find_next", builtinDsMapFindNext); + VM_registerBuiltin(ctx, "ds_map_size", builtinDsMapSize); + VM_registerBuiltin(ctx, "ds_map_destroy", builtinDsMapDestroy); + + // ds_list stubs + VM_registerBuiltin(ctx, "ds_list_create", builtinDsListCreate); + VM_registerBuiltin(ctx, "ds_list_destroy", builtinDsListDestroy); + VM_registerBuiltin(ctx, "ds_list_add", builtinDsListAdd); + VM_registerBuiltin(ctx, "ds_list_size", builtinDsListSize); + VM_registerBuiltin(ctx, "ds_list_find_index", builtinDsListFindIndex); + VM_registerBuiltin(ctx, "ds_list_find_value", builtinDsListFindValue); + + // Array + VM_registerBuiltin(ctx, "array_length_1d", builtinArrayLength1d); + // GM:S 2 alias for array_length_1d + VM_registerBuiltin(ctx, "array_length", builtinArrayLength1d); + VM_registerBuiltin(ctx, "array_push", builtinArrayPush); + VM_registerBuiltin(ctx, "array_resize", builtinArrayResize); + VM_registerBuiltin(ctx, "array_delete", builtinArrayDelete); + VM_registerBuiltin(ctx, "array_insert", builtinArrayInsert); + VM_registerBuiltin(ctx, "array_create", builtinArrayCreate); + + // Steam stubs + VM_registerBuiltin(ctx, "steam_initialised", builtin_steam_initialised); + VM_registerBuiltin(ctx, "steam_stats_ready", builtin_steam_stats_ready); + VM_registerBuiltin(ctx, "steam_file_exists", builtin_steam_file_exists); + VM_registerBuiltin(ctx, "steam_file_write", builtin_steam_file_write); + VM_registerBuiltin(ctx, "steam_file_read", builtin_steam_file_read); + VM_registerBuiltin(ctx, "steam_get_persona_name", builtin_steam_get_persona_name); + + // Video playback stubs. The Wii U build does not decode MP4s yet, so these report + // completion and fire the Async Social callback that game code waits on. + VM_registerBuiltin(ctx, "video_open", builtin_video_open); + VM_registerBuiltin(ctx, "video_close", builtin_video_close); + VM_registerBuiltin(ctx, "video_draw", builtin_video_draw); + VM_registerBuiltin(ctx, "video_set_volume", builtin_video_set_volume); + VM_registerBuiltin(ctx, "video_get_volume", builtin_video_get_volume); + VM_registerBuiltin(ctx, "video_pause", builtin_video_pause); + VM_registerBuiltin(ctx, "video_resume", builtin_video_resume); + VM_registerBuiltin(ctx, "video_enable_loop", builtin_video_enable_loop); + VM_registerBuiltin(ctx, "video_is_looping", builtin_video_is_looping); + VM_registerBuiltin(ctx, "video_seek_to", builtin_video_seek_to); + VM_registerBuiltin(ctx, "video_get_duration", builtin_video_get_duration); + VM_registerBuiltin(ctx, "video_get_position", builtin_video_get_position); + VM_registerBuiltin(ctx, "video_get_status", builtin_video_get_status); + VM_registerBuiltin(ctx, "video_get_format", builtin_video_get_format); + + // Audio + VM_registerBuiltin(ctx, "audio_exists", builtin_audioExists); + VM_registerBuiltin(ctx, "sound_exists", builtin_audioExists); // Replaced with audio_exists in GMS2 + VM_registerBuiltin(ctx, "audio_channel_num", builtin_audioChannelNum); + VM_registerBuiltin(ctx, "audio_play_sound", builtin_audioPlaySound); + VM_registerBuiltin(ctx, "audio_stop_sound", builtin_audioStopSound); + VM_registerBuiltin(ctx, "audio_stop_all", builtin_audioStopAll); + VM_registerBuiltin(ctx, "audio_is_playing", builtin_audioIsPlaying); + VM_registerBuiltin(ctx, "audio_is_paused", builtin_audioIsPaused); + VM_registerBuiltin(ctx, "audio_sound_length", builtin_audioSoundLength); + VM_registerBuiltin(ctx, "audio_sound_gain", builtin_audioSoundGain); + VM_registerBuiltin(ctx, "audio_sound_pitch", builtin_audioSoundPitch); + VM_registerBuiltin(ctx, "audio_sound_get_gain", builtin_audioSoundGetGain); + VM_registerBuiltin(ctx, "audio_sound_get_pitch", builtin_audioSoundGetPitch); + VM_registerBuiltin(ctx, "audio_master_gain", builtin_audioMasterGain); + VM_registerBuiltin(ctx, "audio_group_load", builtin_audioGroupLoad); + VM_registerBuiltin(ctx, "audio_group_is_loaded", builtin_audioGroupIsLoaded); + VM_registerBuiltin(ctx, "audio_play_music", builtin_audioPlayMusic); + VM_registerBuiltin(ctx, "audio_stop_music", builtin_audioStopMusic); + VM_registerBuiltin(ctx, "audio_music_gain", builtin_audioMusicGain); + VM_registerBuiltin(ctx, "audio_music_is_playing", builtin_audioMusicIsPlaying); + VM_registerBuiltin(ctx, "audio_pause_sound", builtin_audioPauseSound); + VM_registerBuiltin(ctx, "audio_resume_sound", builtin_audioResumeSound); + VM_registerBuiltin(ctx, "audio_pause_all", builtin_audioPauseAll); + VM_registerBuiltin(ctx, "audio_resume_all", builtin_audioResumeAll); + VM_registerBuiltin(ctx, "audio_sound_get_track_position", builtin_audioSoundGetTrackPosition); + VM_registerBuiltin(ctx, "audio_sound_set_track_position", builtin_audioSoundSetTrackPosition); + VM_registerBuiltin(ctx, "audio_create_stream", builtin_audioCreateStream); + VM_registerBuiltin(ctx, "audio_destroy_stream", builtin_audioDestroyStream); + + // Application surface + VM_registerBuiltin(ctx, "application_surface_enable", builtin_application_surface_enable); + VM_registerBuiltin(ctx, "application_surface_draw_enable", builtin_application_surface_draw_enable); + + // Gamepad + VM_registerBuiltin(ctx, "gamepad_get_device_count", builtinGamepadGetDeviceCount); + VM_registerBuiltin(ctx, "gamepad_is_connected", builtinGamepadIsConnected); + VM_registerBuiltin(ctx, "gamepad_button_check", builtinGamepadButtonCheck); + VM_registerBuiltin(ctx, "gamepad_button_check_pressed", builtinGamepadButtonCheckPressed); + VM_registerBuiltin(ctx, "gamepad_button_check_released", builtinGamepadButtonCheckReleased); + VM_registerBuiltin(ctx, "gamepad_axis_value", builtinGamepadAxisValue); + VM_registerBuiltin(ctx, "gamepad_get_description", builtinGamepadGetDescription); + VM_registerBuiltin(ctx, "gamepad_button_value", builtinGamepadButtonValue); + VM_registerBuiltin(ctx, "gamepad_is_supported", builtinGamepadIsSupported); + VM_registerBuiltin(ctx, "gamepad_get_guid", builtinGamepadGetGuid); + VM_registerBuiltin(ctx, "gamepad_get_button_threshold", builtinGamepadGetButtonThreshold); + VM_registerBuiltin(ctx, "gamepad_set_button_threshold", builtinGamepadSetButtonThreshold); + VM_registerBuiltin(ctx, "gamepad_get_axis_deadzone", builtinGamepadGetAxisDeadzone); + VM_registerBuiltin(ctx, "gamepad_set_axis_deadzone", builtinGamepadSetAxisDeadzone); + VM_registerBuiltin(ctx, "gamepad_axis_count", builtinGamepadAxisCount); + VM_registerBuiltin(ctx, "gamepad_button_count", builtinGamepadButtonCount); + VM_registerBuiltin(ctx, "gamepad_hat_count", builtinGamepadHatCount); + VM_registerBuiltin(ctx, "gamepad_hat_value", builtinGamepadHatValue); + + // INI + VM_registerBuiltin(ctx, "ini_open", builtinIniOpen); + VM_registerBuiltin(ctx, "ini_close", builtinIniClose); + VM_registerBuiltin(ctx, "ini_write_real", builtinIniWriteReal); + VM_registerBuiltin(ctx, "ini_write_string", builtinIniWriteString); + VM_registerBuiltin(ctx, "ini_read_string", builtinIniReadString); + VM_registerBuiltin(ctx, "ini_read_real", builtinIniReadReal); + VM_registerBuiltin(ctx, "ini_section_exists", builtinIniSectionExists); + + // File + VM_registerBuiltin(ctx, "file_exists", builtinFileExists); + VM_registerBuiltin(ctx, "file_text_open_write", builtinFileTextOpenWrite); + VM_registerBuiltin(ctx, "file_text_open_read", builtinFileTextOpenRead); + VM_registerBuiltin(ctx, "file_text_close", builtinFileTextClose); + VM_registerBuiltin(ctx, "file_text_write_string", builtinFileTextWriteString); + VM_registerBuiltin(ctx, "file_text_writeln", builtinFileTextWriteln); + VM_registerBuiltin(ctx, "file_text_write_real", builtinFileTextWriteReal); + VM_registerBuiltin(ctx, "file_text_eof", builtinFileTextEof); + VM_registerBuiltin(ctx, "file_delete", builtinFileDelete); + VM_registerBuiltin(ctx, "file_text_read_string", builtinFileTextReadString); + VM_registerBuiltin(ctx, "file_text_read_real", builtinFileTextReadReal); + VM_registerBuiltin(ctx, "file_text_readln", builtinFileTextReadln); + + // Keyboard + VM_registerBuiltin(ctx, "keyboard_check", builtinKeyboardCheck); + VM_registerBuiltin(ctx, "keyboard_check_pressed", builtinKeyboardCheckPressed); + VM_registerBuiltin(ctx, "keyboard_check_released", builtinKeyboardCheckReleased); + VM_registerBuiltin(ctx, "keyboard_check_direct", builtinKeyboardCheckDirect); + VM_registerBuiltin(ctx, "keyboard_key_press", builtinKeyboardKeyPress); + VM_registerBuiltin(ctx, "keyboard_key_release", builtinKeyboardKeyRelease); + VM_registerBuiltin(ctx, "keyboard_clear", builtinKeyboardClear); + + // Joystick + VM_registerBuiltin(ctx, "joystick_exists", builtinJoystickExists); + VM_registerBuiltin(ctx, "joystick_name", builtinJoystickName); + VM_registerBuiltin(ctx, "joystick_axes", builtinJoystickAxes); + VM_registerBuiltin(ctx, "joystick_xpos", builtinJoystickXpos); + VM_registerBuiltin(ctx, "joystick_ypos", builtinJoystickYpos); + VM_registerBuiltin(ctx, "joystick_direction", builtinJoystickDirection); + VM_registerBuiltin(ctx, "joystick_pov", builtinJoystickPov); + VM_registerBuiltin(ctx, "joystick_check_button", builtinJoystickCheckButton); + VM_registerBuiltin(ctx, "joystick_has_pov", builtinJoystickHasPov); + VM_registerBuiltin(ctx, "joystick_buttons", builtinJoystickButtons); + + // Window + VM_registerBuiltin(ctx, "window_get_fullscreen", builtin_window_get_fullscreen); + VM_registerBuiltin(ctx, "window_set_fullscreen", builtin_window_set_fullscreen); + VM_registerBuiltin(ctx, "window_set_caption", builtinWindowSetCaption); + VM_registerBuiltin(ctx, "window_set_size", builtin_window_set_size); + VM_registerBuiltin(ctx, "window_center", builtin_window_center); + VM_registerBuiltin(ctx, "window_get_width", builtinWindowGetWidth); + VM_registerBuiltin(ctx, "window_get_height", builtinWindowGetHeight); + VM_registerBuiltin(ctx, "window_has_focus", builtinWindowHasFocus); + + // Game + VM_registerBuiltin(ctx, "game_restart", builtinGameRestart); + VM_registerBuiltin(ctx, "game_end", builtinGameEnd); + VM_registerBuiltin(ctx, "game_save", builtin_game_save); + VM_registerBuiltin(ctx, "game_load", builtin_game_load); + + // Instance + VM_registerBuiltin(ctx, "instance_exists", builtinInstanceExists); + VM_registerBuiltin(ctx, "instance_number", builtinInstanceNumber); + VM_registerBuiltin(ctx, "instance_find", builtinInstanceFind); + VM_registerBuiltin(ctx, "instance_nearest", builtinInstanceNearest); + VM_registerBuiltin(ctx, "instance_destroy", builtinInstanceDestroy); + if(!isGMS2) { + VM_registerBuiltin(ctx, "instance_create", builtinInstanceCreate); + } + else { + VM_registerBuiltin(ctx, "instance_create_depth", builtinInstanceCreateDepth); + VM_registerBuiltin(ctx, "instance_create_layer", builtinInstanceCreateLayer); + } + VM_registerBuiltin(ctx, "instance_copy", builtinInstanceCopy); + VM_registerBuiltin(ctx, "instance_change", builtinInstanceChange); + VM_registerBuiltin(ctx, "instance_deactivate_all", builtinInstanceDeactivateAll); + VM_registerBuiltin(ctx, "instance_activate_all", builtinInstanceActivateAll); + VM_registerBuiltin(ctx, "instance_activate_object", builtinInstanceActivateObject); + VM_registerBuiltin(ctx, "instance_deactivate_object", builtinInstanceDeactivateObject); + VM_registerBuiltin(ctx, "instance_activate_layer", builtinInstanceActivateLayer); + VM_registerBuiltin(ctx, "instance_deactivate_layer", builtinInstanceDeactivateLayer); + VM_registerBuiltin(ctx, "action_kill_object", builtinActionKillObject); + VM_registerBuiltin(ctx, "action_create_object", builtinActionCreateObject); + VM_registerBuiltin(ctx, "action_set_relative", builtinActionSetRelative); + VM_registerBuiltin(ctx, "action_move", builtinActionMove); + VM_registerBuiltin(ctx, "action_move_to", builtinActionMoveTo); + VM_registerBuiltin(ctx, "action_snap", builtinActionSnap); + VM_registerBuiltin(ctx, "action_set_friction", builtinActionSetFriction); + VM_registerBuiltin(ctx, "action_set_gravity", builtinActionSetGravity); + VM_registerBuiltin(ctx, "action_set_hspeed", builtinActionSetHspeed); + VM_registerBuiltin(ctx, "action_set_vspeed", builtinActionSetVspeed); + VM_registerBuiltin(ctx, "event_inherited", builtinEventInherited); + VM_registerBuiltin(ctx, "action_inherited", builtinEventInherited); + VM_registerBuiltin(ctx, "event_user", builtinEventUser); + VM_registerBuiltin(ctx, "event_perform", builtinEventPerform); + + // Buffer + VM_registerBuiltin(ctx, "buffer_create", builtin_bufferCreate); + VM_registerBuiltin(ctx, "buffer_delete", builtin_bufferDelete); + VM_registerBuiltin(ctx, "buffer_write", builtin_bufferWrite); + VM_registerBuiltin(ctx, "buffer_read", builtin_bufferRead); + VM_registerBuiltin(ctx, "buffer_seek", builtin_bufferSeek); + VM_registerBuiltin(ctx, "buffer_tell", builtin_bufferTell); + VM_registerBuiltin(ctx, "buffer_get_size", builtin_bufferGetSize); + VM_registerBuiltin(ctx, "buffer_load", builtin_bufferLoad); + VM_registerBuiltin(ctx, "buffer_save", builtin_bufferSave); + VM_registerBuiltin(ctx, "buffer_base64_encode", builtin_buffer_base64_encode); + + // PSN + VM_registerBuiltin(ctx, "psn_init", builtin_psn_init); + VM_registerBuiltin(ctx, "psn_default_user", builtin_psn_default_user); + VM_registerBuiltin(ctx, "psn_get_leaderboard_score", builtin_psn_get_leaderboard_score); + + // Draw + VM_registerBuiltin(ctx, "draw_sprite", builtin_drawSprite); + VM_registerBuiltin(ctx, "draw_sprite_ext", builtin_drawSpriteExt); + VM_registerBuiltin(ctx, "draw_sprite_tiled", builtin_drawSpriteTiled); + VM_registerBuiltin(ctx, "draw_sprite_tiled_ext", builtin_drawSpriteTiledExt); + VM_registerBuiltin(ctx, "draw_sprite_stretched", builtin_drawSpriteStretched); + VM_registerBuiltin(ctx, "draw_sprite_stretched_ext", builtin_drawSpriteStretchedExt); + VM_registerBuiltin(ctx, "draw_sprite_part", builtin_drawSpritePart); + VM_registerBuiltin(ctx, "draw_sprite_part_ext", builtin_drawSpritePartExt); + VM_registerBuiltin(ctx, "draw_sprite_general", builtin_drawSpriteGeneral); + VM_registerBuiltin(ctx, "draw_sprite_pos", builtin_drawSpritePos); + VM_registerBuiltin(ctx, "draw_rectangle", builtin_drawRectangle); + VM_registerBuiltin(ctx, "draw_rectangle_color", builtin_drawRectangleColor); + VM_registerBuiltin(ctx, "draw_rectangle_colour", builtin_drawRectangleColor); + VM_registerBuiltin(ctx, "draw_healthbar", builtin_drawHealthbar); + VM_registerBuiltin(ctx, "draw_set_color", builtin_drawSetColor); + VM_registerBuiltin(ctx, "draw_set_alpha", builtin_drawSetAlpha); + VM_registerBuiltin(ctx, "draw_clear", builtin_drawClear); + VM_registerBuiltin(ctx, "draw_clear_alpha", builtin_drawClearAlpha); + VM_registerBuiltin(ctx, "draw_set_font", builtin_drawSetFont); + VM_registerBuiltin(ctx, "draw_set_halign", builtin_drawSetHalign); + VM_registerBuiltin(ctx, "draw_set_valign", builtin_drawSetValign); + VM_registerBuiltin(ctx, "draw_text", builtin_drawText); + VM_registerBuiltin(ctx, "draw_text_transformed", builtin_drawTextTransformed); + VM_registerBuiltin(ctx, "draw_text_ext", builtin_drawTextExt); + VM_registerBuiltin(ctx, "draw_text_ext_transformed", builtin_drawTextExtTransformed); + VM_registerBuiltin(ctx, "draw_text_color", builtin_drawTextColor); + VM_registerBuiltin(ctx, "draw_text_color_transformed", builtin_drawTextColorTransformed); + VM_registerBuiltin(ctx, "draw_text_color_ext", builtin_drawTextColorExt); + VM_registerBuiltin(ctx, "draw_text_color_ext_transformed", builtin_drawTextColorExtTransformed); + VM_registerBuiltin(ctx, "draw_text_colour", builtin_drawTextColor); + VM_registerBuiltin(ctx, "draw_text_colour_transformed", builtin_drawTextColorTransformed); + VM_registerBuiltin(ctx, "draw_text_colour_ext", builtin_drawTextColorExt); + VM_registerBuiltin(ctx, "draw_text_colour_ext_transformed", builtin_drawTextColorExtTransformed); + VM_registerBuiltin(ctx, "draw_surface", builtin_draw_surface); + VM_registerBuiltin(ctx, "draw_surface_ext", builtin_draw_surface_ext); + VM_registerBuiltin(ctx, "draw_surface_part", builtin_draw_surface_part); + VM_registerBuiltin(ctx, "draw_surface_part_ext", builtin_draw_surface_part_ext); + VM_registerBuiltin(ctx, "draw_surface_stretched", builtin_draw_surface_stretched); + if(!isGMS2) { + VM_registerBuiltin(ctx, "draw_background", builtin_drawBackground); + VM_registerBuiltin(ctx, "draw_background_ext", builtin_drawBackgroundExt); + VM_registerBuiltin(ctx, "draw_background_stretched", builtin_drawBackgroundStretched); + VM_registerBuiltin(ctx, "draw_background_part_ext", builtin_drawBackgroundPartExt); + VM_registerBuiltin(ctx, "background_get_width", builtinBackgroundGetWidth); + VM_registerBuiltin(ctx, "background_get_height", builtinBackgroundGetHeight); + } + VM_registerBuiltin(ctx, "draw_self", builtin_draw_self); + VM_registerBuiltin(ctx, "draw_line", builtin_draw_line); + VM_registerBuiltin(ctx, "draw_line_width", builtin_draw_line_width); + VM_registerBuiltin(ctx, "draw_line_width_colour", builtin_draw_line_width_colour); + VM_registerBuiltin(ctx, "draw_line_width_color", builtin_draw_line_width_colour); + VM_registerBuiltin(ctx, "draw_triangle", builtin_draw_triangle); + VM_registerBuiltin(ctx, "draw_circle", builtin_drawCircle); + VM_registerBuiltin(ctx, "draw_set_circle_precision", builtin_drawSetCirclePrecision); + VM_registerBuiltin(ctx, "draw_get_circle_precision", builtin_drawGetCirclePrecision); + VM_registerBuiltin(ctx, "draw_set_colour", builtin_draw_set_colour); + VM_registerBuiltin(ctx, "draw_get_colour", builtin_draw_get_colour); + VM_registerBuiltin(ctx, "draw_get_color", builtin_draw_get_color); + VM_registerBuiltin(ctx, "draw_get_alpha", builtin_draw_get_alpha); + + // Color + VM_registerBuiltin(ctx, "merge_color", builtinMergeColor); + VM_registerBuiltin(ctx, "merge_colour", builtinMergeColor); + + // Surface + VM_registerBuiltin(ctx, "surface_create", builtin_surface_create); + VM_registerBuiltin(ctx, "surface_free", builtin_surface_free); + VM_registerBuiltin(ctx, "surface_set_target", builtin_surface_set_target); + VM_registerBuiltin(ctx, "surface_reset_target", builtin_surface_reset_target); + VM_registerBuiltin(ctx, "surface_exists", builtin_surface_exists); + VM_registerBuiltin(ctx, "surface_get_width", builtinSurfaceGetWidth); + VM_registerBuiltin(ctx, "surface_get_height", builtinSurfaceGetHeight); + VM_registerBuiltin(ctx, "surface_resize", builtin_surface_resize); + VM_registerBuiltin(ctx, "surface_copy", builtin_surface_copy); + VM_registerBuiltin(ctx, "surface_copy_part", builtin_surface_copy_part); + + // Sprite info + VM_registerBuiltin(ctx, "sprite_add", builtin_spriteAdd); + VM_registerBuiltin(ctx, "sprite_exists", builtin_spriteExists); + VM_registerBuiltin(ctx, "sprite_get_width", builtin_spriteGetWidth); + VM_registerBuiltin(ctx, "sprite_get_height", builtin_spriteGetHeight); + VM_registerBuiltin(ctx, "sprite_get_number", builtin_spriteGetNumber); + VM_registerBuiltin(ctx, "sprite_get_xoffset", builtin_spriteGetXOffset); + VM_registerBuiltin(ctx, "sprite_get_yoffset", builtin_spriteGetYOffset); + VM_registerBuiltin(ctx, "sprite_get_name", builtin_spriteGetName); + VM_registerBuiltin(ctx, "sprite_set_offset", builtin_spriteSetOffset); + VM_registerBuiltin(ctx, "sprite_create_from_surface", builtin_spriteCreateFromSurface); + VM_registerBuiltin(ctx, "sprite_delete", builtin_spriteDelete); + + // Text measurement + VM_registerBuiltin(ctx, "string_width", builtin_stringWidth); + VM_registerBuiltin(ctx, "string_height", builtin_stringHeight); + VM_registerBuiltin(ctx, "string_width_ext", builtin_string_width_ext); + VM_registerBuiltin(ctx, "string_height_ext", builtin_string_height_ext); + + // Color + VM_registerBuiltin(ctx, "make_color_rgb", builtinMakeColor); + VM_registerBuiltin(ctx, "make_colour_rgb", builtinMakeColour); + VM_registerBuiltin(ctx, "make_color_hsv", builtinMakeColorHsv); + VM_registerBuiltin(ctx, "make_colour_hsv", builtinMakeColourHsv); + + // Display + VM_registerBuiltin(ctx, "display_get_width", builtin_display_get_width); + VM_registerBuiltin(ctx, "display_get_height", builtin_display_get_height); + VM_registerBuiltin(ctx, "display_get_gui_width", builtinDisplayGetGuiWidth); + VM_registerBuiltin(ctx, "display_get_gui_height", builtinDisplayGetGuiHeight); + VM_registerBuiltin(ctx, "display_set_gui_size", builtinDisplaySetGuiSize); + VM_registerBuiltin(ctx, "display_set_gui_maximise", builtinDisplaySetGuiMaximise); + VM_registerBuiltin(ctx, "display_set_gui_maximize", builtinDisplaySetGuiMaximise); + + // Collision + VM_registerBuiltin(ctx, "place_meeting", builtinPlaceMeeting); + VM_registerBuiltin(ctx, "collision_rectangle", builtinCollisionRectangle); + VM_registerBuiltin(ctx, "collision_rectangle_list", builtinCollisionRectangleList); + VM_registerBuiltin(ctx, "rectangle_in_rectangle", builtinRectangleInRectangle); + VM_registerBuiltin(ctx, "collision_line", builtinCollisionLine); + VM_registerBuiltin(ctx, "collision_point", builtinCollisionPoint); + VM_registerBuiltin(ctx, "collision_circle", builtinCollisionCircle); + VM_registerBuiltin(ctx, "instance_place", builtinInstancePlace); + VM_registerBuiltin(ctx, "instance_position", builtinInstancePosition); + VM_registerBuiltin(ctx, "position_meeting", builtinPositionMeeting); + VM_registerBuiltin(ctx, "place_free", builtinPlaceFree); + VM_registerBuiltin(ctx, "place_empty", builtinPlaceEmpty); + + // Motion planning + VM_registerBuiltin(ctx, "mp_linear_step", builtinMpLinearStep); + VM_registerBuiltin(ctx, "mp_linear_step_object", builtinMpLinearStepObject); + VM_registerBuiltin(ctx, "mp_potential_step", builtinMpPotentialStep); + VM_registerBuiltin(ctx, "mp_potential_step_object", builtinMpPotentialStepObject); + VM_registerBuiltin(ctx, "mp_potential_settings", builtinMpPotentialSettings); + + // Tile layers + VM_registerBuiltin(ctx, "tile_layer_hide", builtinTileLayerHide); + VM_registerBuiltin(ctx, "tile_layer_show", builtinTileLayerShow); + VM_registerBuiltin(ctx, "tile_layer_shift", builtinTileLayerShift); + + // Layer + VM_registerBuiltin(ctx, "layer_force_draw_depth", builtinLayerForceDrawDepth); + VM_registerBuiltin(ctx, "layer_is_draw_depth_forced", builtinLayerIsDrawDepthForced); + VM_registerBuiltin(ctx, "layer_get_forced_depth", builtinLayerGetForcedDepth); + VM_registerBuiltin(ctx, "layer_get_id", builtinLayerGetId); + VM_registerBuiltin(ctx, "layer_exists", builtinLayerExists); + VM_registerBuiltin(ctx, "layer_get_name", builtinLayerGetName); + VM_registerBuiltin(ctx, "layer_get_depth", builtinLayerGetDepth); + VM_registerBuiltin(ctx, "layer_depth", builtinLayerDepth); + VM_registerBuiltin(ctx, "layer_get_visible", builtinLayerGetVisible); + VM_registerBuiltin(ctx, "layer_set_visible", builtinLayerSetVisible); + VM_registerBuiltin(ctx, "layer_get_x", builtinLayerGetX); + VM_registerBuiltin(ctx, "layer_x", builtinLayerX); + VM_registerBuiltin(ctx, "layer_get_y", builtinLayerGetY); + VM_registerBuiltin(ctx, "layer_y", builtinLayerY); + VM_registerBuiltin(ctx, "layer_get_hspeed", builtinLayerGetHspeed); + VM_registerBuiltin(ctx, "layer_hspeed", builtinLayerHspeed); + VM_registerBuiltin(ctx, "layer_get_vspeed", builtinLayerGetVspeed); + VM_registerBuiltin(ctx, "layer_vspeed", builtinLayerVspeed); +#if IS_BC17_OR_HIGHER_ENABLED + VM_registerBuiltin(ctx, "layer_get_all", builtinLayerGetAll); + VM_registerBuiltin(ctx, "layer_get_all_elements", builtinLayerGetAllElements); +#endif + VM_registerBuiltin(ctx, "layer_get_element_type", builtinLayerGetElementType); + VM_registerBuiltin(ctx, "layer_sprite_get_sprite", builtinLayerSpriteGetSprite); + VM_registerBuiltin(ctx, "layer_sprite_get_x", builtinLayerSpriteGetX); + VM_registerBuiltin(ctx, "layer_sprite_get_y", builtinLayerSpriteGetY); + VM_registerBuiltin(ctx, "layer_sprite_get_xscale", builtinLayerSpriteGetXScale); + VM_registerBuiltin(ctx, "layer_sprite_get_yscale", builtinLayerSpriteGetYScale); + VM_registerBuiltin(ctx, "layer_sprite_get_speed", builtinLayerSpriteGetSpeed); + VM_registerBuiltin(ctx, "layer_sprite_get_index", builtinLayerSpriteGetIndex); + VM_registerBuiltin(ctx, "layer_sprite_get_angle", builtinLayerSpriteGetAngle); + VM_registerBuiltin(ctx, "layer_sprite_destroy", builtinLayerSpriteDestroy); + VM_registerBuiltin(ctx, "layer_tile_visible", builtinLayerTileVisible); +#if IS_BC17_OR_HIGHER_ENABLED + VM_registerBuiltin(ctx, "layer_get_id_at_depth", builtinLayerGetIdAtDepth); + VM_registerBuiltin(ctx, "layer_tilemap_get_id", builtinLayerTilemapGetId); + VM_registerBuiltin(ctx, "draw_tilemap", builtinDrawTilemap); + VM_registerBuiltin(ctx, "tilemap_x", builtinTilemapX); + VM_registerBuiltin(ctx, "tilemap_y", builtinTilemapY); + VM_registerBuiltin(ctx, "tilemap_get_x", builtinTilemapGetX); + VM_registerBuiltin(ctx, "tilemap_get_y", builtinTilemapGetY); +#endif + VM_registerBuiltin(ctx, "layer_create", builtinLayerCreate); + VM_registerBuiltin(ctx, "layer_destroy", builtinLayerDestroy); + VM_registerBuiltin(ctx, "layer_background_create", builtinLayerBackgroundCreate); + VM_registerBuiltin(ctx, "layer_background_exists", builtinLayerBackgroundExists); + VM_registerBuiltin(ctx, "layer_background_visible", builtinLayerBackgroundVisible); + VM_registerBuiltin(ctx, "layer_background_htiled", builtinLayerBackgroundHtiled); + VM_registerBuiltin(ctx, "layer_background_vtiled", builtinLayerBackgroundVtiled); + VM_registerBuiltin(ctx, "layer_background_xscale", builtinLayerBackgroundXscale); + VM_registerBuiltin(ctx, "layer_background_yscale", builtinLayerBackgroundYscale); + VM_registerBuiltin(ctx, "layer_background_stretch", builtinLayerBackgroundStretch); + VM_registerBuiltin(ctx, "layer_background_blend", builtinLayerBackgroundBlend); + VM_registerBuiltin(ctx, "layer_background_alpha", builtinLayerBackgroundAlpha); + VM_registerBuiltin(ctx, "layer_tile_alpha", builtinLayerTileAlpha); + + // GMS2 internal + VM_registerBuiltin(ctx, "@@NewGMLArray@@", builtinNewGMLArray); + VM_registerBuiltin(ctx, "@@This@@", builtinThis); + VM_registerBuiltin(ctx, "@@Other@@", builtinOther); +#if IS_BC17_OR_HIGHER_ENABLED + VM_registerBuiltin(ctx, "@@NullObject@@", builtinNullObject); + VM_registerBuiltin(ctx, "@@NewGMLObject@@", builtinNewGMLObject); +#endif + + // Path + VM_registerBuiltin(ctx, "path_start", builtinPathStart); + VM_registerBuiltin(ctx, "path_end", builtinPathEnd); + VM_registerBuiltin(ctx, "path_get_length", builtinPathGetLength); + VM_registerBuiltin(ctx, "path_add", builtinPathAdd); + VM_registerBuiltin(ctx, "path_clear_points", builtinPathClearPoints); + VM_registerBuiltin(ctx, "path_add_point", builtinPathAddPoint); + VM_registerBuiltin(ctx, "path_exists", builtinPathExists); + VM_registerBuiltin(ctx, "path_delete", builtinPathDelete); + + // Motion planning grid + VM_registerBuiltin(ctx, "mp_grid_create", builtinMpGridCreate); + VM_registerBuiltin(ctx, "mp_grid_destroy", builtinMpGridDestroy); + VM_registerBuiltin(ctx, "mp_grid_clear_all", builtinMpGridClearAll); + VM_registerBuiltin(ctx, "mp_grid_add_cell", builtinMpGridAddCell); + VM_registerBuiltin(ctx, "mp_grid_clear_cell", builtinMpGridClearCell); + VM_registerBuiltin(ctx, "mp_grid_add_rectangle", builtinMpGridAddRectangle); + VM_registerBuiltin(ctx, "mp_grid_clear_rectangle", builtinMpGridClearRectangle); + VM_registerBuiltin(ctx, "mp_grid_get_cell", builtinMpGridGetCell); + VM_registerBuiltin(ctx, "mp_grid_draw", builtinMpGridDraw); + VM_registerBuiltin(ctx, "mp_grid_path", builtinMpGridPath); + + // Misc + VM_registerBuiltin(ctx, "get_timer", builtin_get_timer); + VM_registerBuiltin(ctx, "action_if_variable", builtinActionIfVariable); + VM_registerBuiltin(ctx, "action_set_alarm", builtinActionSetAlarm); + VM_registerBuiltin(ctx, "alarm_set", builtinAlarmSet); + VM_registerBuiltin(ctx, "alarm_get", builtinAlarmGet); + VM_registerBuiltin(ctx, "action_sound",builtin_action_sound); + VM_registerBuiltin(ctx, "string_hash_to_newline", builtinStringHashToNewline); + VM_registerBuiltin(ctx, "json_decode", builtinJsonDecode); + VM_registerBuiltin(ctx, "font_add_sprite", builtinFontAddSprite); + VM_registerBuiltin(ctx, "font_add_sprite_ext", builtinFontAddSpriteExt); + VM_registerBuiltin(ctx, "font_get_name", builtinFontGetName); + VM_registerBuiltin(ctx, "object_get_sprite", builtinObjectGetSprite); + VM_registerBuiltin(ctx, "asset_get_index", builtinAssetGetIndex); + VM_registerBuiltin(ctx,"gpu_set_blendmode", builtinGpuSetBlendMode); + VM_registerBuiltin(ctx,"gpu_set_blendmode_ext", builtinGpuSetBlendModeExt); + VM_registerBuiltin(ctx,"gpu_set_blendenable", builtinGpuSetBlendEnable); + VM_registerBuiltin(ctx,"gpu_get_blendenable", builtinGpuGetBlendEnable); + VM_registerBuiltin(ctx,"gpu_set_alphatestenable", builtinGpuSetAlphaTestEnable); + VM_registerBuiltin(ctx,"gpu_set_alphatestref", builtinGpuSetAlphaTestRef); + VM_registerBuiltin(ctx,"gpu_set_colorwriteenable", builtinGpuSetColorWriteEnable); + VM_registerBuiltin(ctx,"gpu_set_fog", builtinGpuSetFog); + VM_registerBuiltin(ctx,"d3d_set_fog", builtinGpuSetFog); +} + +static RValue builtinN3DSRenderTopScreen(VMContext* ctx, RValue* args, int32_t argCount) { + if (argCount < 1) return RValue_makeUndefined(); + + Runner* runner = (Runner*) ctx->runner; + if (runner == NULL || runner->renderer == NULL) return RValue_makeUndefined(); + +#ifndef __3DS__ + return builtinScriptExecute(ctx, args, argCount); +#else + if (runner->osType != OS_3DS) { + return builtinScriptExecute(ctx, args, argCount); + } + + if (!battleDraw_is3DSBattleActive(ctx, runner)) { + return RValue_makeUndefined(); + } + + int32_t guiW = runner->guiWidth > 0 ? runner->guiWidth : (int32_t) runner->dataWin->gen8.defaultWindowWidth; + int32_t guiH = runner->guiHeight > 0 ? runner->guiHeight : (int32_t) runner->dataWin->gen8.defaultWindowHeight; + if (guiW <= 0) guiW = 320; + if (guiH <= 0) guiH = 240; + + N3DSRenderer_beginTopScreenGUI(runner->renderer, guiW, guiH); + RValue result = builtinScriptExecute(ctx, args, argCount); + N3DSRenderer_endTopScreenGUI(runner->renderer); + return result; +#endif +} diff --git a/src/vm_builtins.h b/src/vm_builtins.h index 0e250d19..95a122dc 100644 --- a/src/vm_builtins.h +++ b/src/vm_builtins.h @@ -1,241 +1,242 @@ -#pragma once - -#include "common.h" -#include "vm.h" - -// ===[ Built-in Variable ID Enum ]=== -typedef enum { - BUILTIN_VAR_UNKNOWN = -1, - - // Instance properties - BUILTIN_VAR_X, - BUILTIN_VAR_Y, - BUILTIN_VAR_XPREVIOUS, - BUILTIN_VAR_YPREVIOUS, - BUILTIN_VAR_XSTART, - BUILTIN_VAR_YSTART, - BUILTIN_VAR_IMAGE_SPEED, - BUILTIN_VAR_IMAGE_INDEX, - BUILTIN_VAR_IMAGE_XSCALE, - BUILTIN_VAR_IMAGE_YSCALE, - BUILTIN_VAR_IMAGE_ANGLE, - BUILTIN_VAR_IMAGE_ALPHA, - BUILTIN_VAR_IMAGE_BLEND, - BUILTIN_VAR_IMAGE_NUMBER, - BUILTIN_VAR_SPRITE_INDEX, - BUILTIN_VAR_SPRITE_WIDTH, - BUILTIN_VAR_SPRITE_HEIGHT, - BUILTIN_VAR_SPRITE_XOFFSET, - BUILTIN_VAR_SPRITE_YOFFSET, - BUILTIN_VAR_BBOX_LEFT, - BUILTIN_VAR_BBOX_RIGHT, - BUILTIN_VAR_BBOX_TOP, - BUILTIN_VAR_BBOX_BOTTOM, - BUILTIN_VAR_VISIBLE, - BUILTIN_VAR_DEPTH, - BUILTIN_VAR_PERSISTENT, - BUILTIN_VAR_SOLID, - BUILTIN_VAR_MASK_INDEX, - BUILTIN_VAR_LAYER, - BUILTIN_VAR_ID, - BUILTIN_VAR_OBJECT_INDEX, - BUILTIN_VAR_SPEED, - BUILTIN_VAR_DIRECTION, - BUILTIN_VAR_HSPEED, - BUILTIN_VAR_VSPEED, - BUILTIN_VAR_FRICTION, - BUILTIN_VAR_GRAVITY, - BUILTIN_VAR_GRAVITY_DIRECTION, - BUILTIN_VAR_ALARM, - - // Path instance variables - BUILTIN_VAR_PATH_INDEX, - BUILTIN_VAR_PATH_POSITION, - BUILTIN_VAR_PATH_POSITIONPREVIOUS, - BUILTIN_VAR_PATH_SPEED, - BUILTIN_VAR_PATH_SCALE, - BUILTIN_VAR_PATH_ORIENTATION, - BUILTIN_VAR_PATH_ENDACTION, - - // Room properties - BUILTIN_VAR_ROOM, - BUILTIN_VAR_ROOM_FIRST, - BUILTIN_VAR_ROOM_SPEED, - BUILTIN_VAR_ROOM_WIDTH, - BUILTIN_VAR_ROOM_HEIGHT, - BUILTIN_VAR_ROOM_PERSISTENT, - - // View properties - BUILTIN_VAR_VIEW_CURRENT, - BUILTIN_VAR_VIEW_XVIEW, - BUILTIN_VAR_VIEW_YVIEW, - BUILTIN_VAR_VIEW_WVIEW, - BUILTIN_VAR_VIEW_HVIEW, - BUILTIN_VAR_VIEW_XPORT, - BUILTIN_VAR_VIEW_YPORT, - BUILTIN_VAR_VIEW_WPORT, - BUILTIN_VAR_VIEW_HPORT, +#pragma once + +#include "common.h" +#include "vm.h" + +// ===[ Built-in Variable ID Enum ]=== +typedef enum { + BUILTIN_VAR_UNKNOWN = -1, + + // Instance properties + BUILTIN_VAR_X, + BUILTIN_VAR_Y, + BUILTIN_VAR_XPREVIOUS, + BUILTIN_VAR_YPREVIOUS, + BUILTIN_VAR_XSTART, + BUILTIN_VAR_YSTART, + BUILTIN_VAR_IMAGE_SPEED, + BUILTIN_VAR_IMAGE_INDEX, + BUILTIN_VAR_IMAGE_XSCALE, + BUILTIN_VAR_IMAGE_YSCALE, + BUILTIN_VAR_IMAGE_ANGLE, + BUILTIN_VAR_IMAGE_ALPHA, + BUILTIN_VAR_IMAGE_BLEND, + BUILTIN_VAR_IMAGE_NUMBER, + BUILTIN_VAR_SPRITE_INDEX, + BUILTIN_VAR_SPRITE_WIDTH, + BUILTIN_VAR_SPRITE_HEIGHT, + BUILTIN_VAR_SPRITE_XOFFSET, + BUILTIN_VAR_SPRITE_YOFFSET, + BUILTIN_VAR_BBOX_LEFT, + BUILTIN_VAR_BBOX_RIGHT, + BUILTIN_VAR_BBOX_TOP, + BUILTIN_VAR_BBOX_BOTTOM, + BUILTIN_VAR_VISIBLE, + BUILTIN_VAR_DEPTH, + BUILTIN_VAR_PERSISTENT, + BUILTIN_VAR_SOLID, + BUILTIN_VAR_MASK_INDEX, + BUILTIN_VAR_LAYER, + BUILTIN_VAR_ID, + BUILTIN_VAR_OBJECT_INDEX, + BUILTIN_VAR_SPEED, + BUILTIN_VAR_DIRECTION, + BUILTIN_VAR_HSPEED, + BUILTIN_VAR_VSPEED, + BUILTIN_VAR_FRICTION, + BUILTIN_VAR_GRAVITY, + BUILTIN_VAR_GRAVITY_DIRECTION, + BUILTIN_VAR_ALARM, + + // Path instance variables + BUILTIN_VAR_PATH_INDEX, + BUILTIN_VAR_PATH_POSITION, + BUILTIN_VAR_PATH_POSITIONPREVIOUS, + BUILTIN_VAR_PATH_SPEED, + BUILTIN_VAR_PATH_SCALE, + BUILTIN_VAR_PATH_ORIENTATION, + BUILTIN_VAR_PATH_ENDACTION, + + // Room properties + BUILTIN_VAR_ROOM, + BUILTIN_VAR_ROOM_FIRST, + BUILTIN_VAR_ROOM_SPEED, + BUILTIN_VAR_ROOM_WIDTH, + BUILTIN_VAR_ROOM_HEIGHT, + BUILTIN_VAR_ROOM_PERSISTENT, + + // View properties + BUILTIN_VAR_VIEW_CURRENT, + BUILTIN_VAR_VIEW_XVIEW, + BUILTIN_VAR_VIEW_YVIEW, + BUILTIN_VAR_VIEW_WVIEW, + BUILTIN_VAR_VIEW_HVIEW, + BUILTIN_VAR_VIEW_XPORT, + BUILTIN_VAR_VIEW_YPORT, + BUILTIN_VAR_VIEW_WPORT, + BUILTIN_VAR_VIEW_HPORT, BUILTIN_VAR_VIEW_VISIBLE, BUILTIN_VAR_VIEW_ANGLE, + BUILTIN_VAR_CAMERA_VIEW, BUILTIN_VAR_VIEW_HBORDER, - BUILTIN_VAR_VIEW_VBORDER, - BUILTIN_VAR_VIEW_OBJECT, - BUILTIN_VAR_VIEW_HSPEED, - BUILTIN_VAR_VIEW_VSPEED, - - // Background properties - BUILTIN_VAR_BACKGROUND_VISIBLE, - BUILTIN_VAR_BACKGROUND_INDEX, - BUILTIN_VAR_BACKGROUND_X, - BUILTIN_VAR_BACKGROUND_Y, - BUILTIN_VAR_BACKGROUND_HSPEED, - BUILTIN_VAR_BACKGROUND_VSPEED, - BUILTIN_VAR_BACKGROUND_WIDTH, - BUILTIN_VAR_BACKGROUND_HEIGHT, - BUILTIN_VAR_BACKGROUND_ALPHA, - BUILTIN_VAR_BACKGROUND_COLOR, - BUILTIN_VAR_BACKGROUND_COLOUR, - - // OS constants - BUILTIN_VAR_OS_TYPE, - BUILTIN_VAR_OS_UNKNOWN, - BUILTIN_VAR_OS_WIN32, - BUILTIN_VAR_OS_WINDOWS, - BUILTIN_VAR_OS_MACOSX, - BUILTIN_VAR_OS_PSP, - BUILTIN_VAR_OS_IOS, - BUILTIN_VAR_OS_ANDROID, - BUILTIN_VAR_OS_SYMBIAN, - BUILTIN_VAR_OS_LINUX, - BUILTIN_VAR_OS_WINPHONE, - BUILTIN_VAR_OS_TIZEN, - BUILTIN_VAR_OS_WIN8NATIVE, - BUILTIN_VAR_OS_WIIU, - BUILTIN_VAR_OS_3DS, - BUILTIN_VAR_OS_PSVITA, - BUILTIN_VAR_OS_BB10, - BUILTIN_VAR_OS_PS4, - BUILTIN_VAR_OS_XBOXONE, - BUILTIN_VAR_OS_PS3, - BUILTIN_VAR_OS_XBOX360, - BUILTIN_VAR_OS_UWP, - BUILTIN_VAR_OS_AMAZON, - BUILTIN_VAR_OS_SWITCH, - BUILTIN_VAR_OS_LLVM_WIN32, - BUILTIN_VAR_OS_LLVM_MACOSX, - BUILTIN_VAR_OS_LLVM_PSP, - BUILTIN_VAR_OS_LLVM_IOS, - BUILTIN_VAR_OS_LLVM_ANDROID, - BUILTIN_VAR_OS_LLVM_SYMBIAN, - BUILTIN_VAR_OS_LLVM_LINUX, - BUILTIN_VAR_OS_LLVM_WINPHONE, - - // Timing - BUILTIN_VAR_CURRENT_TIME, - - // File system - BUILTIN_VAR_WORKING_DIRECTORY, - - // Arguments - BUILTIN_VAR_ARGUMENT_COUNT, - BUILTIN_VAR_ARGUMENT, - BUILTIN_VAR_ARGUMENT0, - BUILTIN_VAR_ARGUMENT1, - BUILTIN_VAR_ARGUMENT2, - BUILTIN_VAR_ARGUMENT3, - BUILTIN_VAR_ARGUMENT4, - BUILTIN_VAR_ARGUMENT5, - BUILTIN_VAR_ARGUMENT6, - BUILTIN_VAR_ARGUMENT7, - BUILTIN_VAR_ARGUMENT8, - BUILTIN_VAR_ARGUMENT9, - BUILTIN_VAR_ARGUMENT10, - BUILTIN_VAR_ARGUMENT11, - BUILTIN_VAR_ARGUMENT12, - BUILTIN_VAR_ARGUMENT13, - BUILTIN_VAR_ARGUMENT14, - BUILTIN_VAR_ARGUMENT15, - - // Keyboard - BUILTIN_VAR_KEYBOARD_KEY, - BUILTIN_VAR_KEYBOARD_LASTCHAR, - BUILTIN_VAR_KEYBOARD_LASTKEY, - - // Surfaces - BUILTIN_VAR_APPLICATION_SURFACE, - - // Constants - BUILTIN_VAR_TRUE, - BUILTIN_VAR_FALSE, - BUILTIN_VAR_PI, - BUILTIN_VAR_UNDEFINED, - - // Path action constants - BUILTIN_VAR_PATH_ACTION_STOP, - BUILTIN_VAR_PATH_ACTION_RESTART, - BUILTIN_VAR_PATH_ACTION_CONTINUE, - BUILTIN_VAR_PATH_ACTION_REVERSE, - - // Buffer type constants - BUILTIN_VAR_BUFFER_FIXED, - BUILTIN_VAR_BUFFER_GROW, - BUILTIN_VAR_BUFFER_WRAP, - BUILTIN_VAR_BUFFER_FAST, - - // Buffer data type constants - BUILTIN_VAR_BUFFER_U8, - BUILTIN_VAR_BUFFER_S8, - BUILTIN_VAR_BUFFER_U16, - BUILTIN_VAR_BUFFER_S16, - BUILTIN_VAR_BUFFER_U32, - BUILTIN_VAR_BUFFER_S32, - BUILTIN_VAR_BUFFER_F16, - BUILTIN_VAR_BUFFER_F32, - BUILTIN_VAR_BUFFER_F64, - BUILTIN_VAR_BUFFER_BOOL, - BUILTIN_VAR_BUFFER_STRING, - BUILTIN_VAR_BUFFER_U64, - BUILTIN_VAR_BUFFER_TEXT, - - // Buffer seek mode constants - BUILTIN_VAR_BUFFER_SEEK_START, - BUILTIN_VAR_BUFFER_SEEK_RELATIVE, - BUILTIN_VAR_BUFFER_SEEK_END, - - // Other - BUILTIN_VAR_FPS, - BUILTIN_VAR_DEBUG_MODE, - - // Gamepad constants - BUILTIN_VAR_GP_FACE1, - BUILTIN_VAR_GP_FACE2, - BUILTIN_VAR_GP_FACE3, - BUILTIN_VAR_GP_FACE4, - BUILTIN_VAR_GP_SHOULDERL, - BUILTIN_VAR_GP_SHOULDERR, - BUILTIN_VAR_GP_SHOULDERLB, - BUILTIN_VAR_GP_SHOULDERRB, - BUILTIN_VAR_GP_SELECT, - BUILTIN_VAR_GP_START, - BUILTIN_VAR_GP_STICKL, - BUILTIN_VAR_GP_STICKR, - BUILTIN_VAR_GP_PADU, - BUILTIN_VAR_GP_PADD, - BUILTIN_VAR_GP_PADL, - BUILTIN_VAR_GP_PADR, - BUILTIN_VAR_GP_HOME, - BUILTIN_VAR_GP_AXIS_LH, - BUILTIN_VAR_GP_AXIS_LV, - BUILTIN_VAR_GP_AXIS_RH, - BUILTIN_VAR_GP_AXIS_RV, - - // Async system - BUILTIN_VAR_ASYNC_LOAD, -} BuiltinVarId; - -void VMBuiltins_registerAll(VMContext* ctx); -int16_t VMBuiltins_resolveBuiltinVarId(const char* name); -// Asserts at startup that the internal builtin-var lookup table is strictly sorted by strcmp order (required for bsearch) and has no duplicates. -void VMBuiltins_checkIfBuiltinVarTableIsSorted(void); -RValue VMBuiltins_getVariable(VMContext* ctx, int16_t builtinVarId, const char* name, int32_t arrayIndex); -void VMBuiltins_setVariable(VMContext* ctx, int16_t builtinVarId, const char* name, RValue val, int32_t arrayIndex); + BUILTIN_VAR_VIEW_VBORDER, + BUILTIN_VAR_VIEW_OBJECT, + BUILTIN_VAR_VIEW_HSPEED, + BUILTIN_VAR_VIEW_VSPEED, + + // Background properties + BUILTIN_VAR_BACKGROUND_VISIBLE, + BUILTIN_VAR_BACKGROUND_INDEX, + BUILTIN_VAR_BACKGROUND_X, + BUILTIN_VAR_BACKGROUND_Y, + BUILTIN_VAR_BACKGROUND_HSPEED, + BUILTIN_VAR_BACKGROUND_VSPEED, + BUILTIN_VAR_BACKGROUND_WIDTH, + BUILTIN_VAR_BACKGROUND_HEIGHT, + BUILTIN_VAR_BACKGROUND_ALPHA, + BUILTIN_VAR_BACKGROUND_COLOR, + BUILTIN_VAR_BACKGROUND_COLOUR, + + // OS constants + BUILTIN_VAR_OS_TYPE, + BUILTIN_VAR_OS_UNKNOWN, + BUILTIN_VAR_OS_WIN32, + BUILTIN_VAR_OS_WINDOWS, + BUILTIN_VAR_OS_MACOSX, + BUILTIN_VAR_OS_PSP, + BUILTIN_VAR_OS_IOS, + BUILTIN_VAR_OS_ANDROID, + BUILTIN_VAR_OS_SYMBIAN, + BUILTIN_VAR_OS_LINUX, + BUILTIN_VAR_OS_WINPHONE, + BUILTIN_VAR_OS_TIZEN, + BUILTIN_VAR_OS_WIN8NATIVE, + BUILTIN_VAR_OS_WIIU, + BUILTIN_VAR_OS_3DS, + BUILTIN_VAR_OS_PSVITA, + BUILTIN_VAR_OS_BB10, + BUILTIN_VAR_OS_PS4, + BUILTIN_VAR_OS_XBOXONE, + BUILTIN_VAR_OS_PS3, + BUILTIN_VAR_OS_XBOX360, + BUILTIN_VAR_OS_UWP, + BUILTIN_VAR_OS_AMAZON, + BUILTIN_VAR_OS_SWITCH, + BUILTIN_VAR_OS_LLVM_WIN32, + BUILTIN_VAR_OS_LLVM_MACOSX, + BUILTIN_VAR_OS_LLVM_PSP, + BUILTIN_VAR_OS_LLVM_IOS, + BUILTIN_VAR_OS_LLVM_ANDROID, + BUILTIN_VAR_OS_LLVM_SYMBIAN, + BUILTIN_VAR_OS_LLVM_LINUX, + BUILTIN_VAR_OS_LLVM_WINPHONE, + + // Timing + BUILTIN_VAR_CURRENT_TIME, + + // File system + BUILTIN_VAR_WORKING_DIRECTORY, + + // Arguments + BUILTIN_VAR_ARGUMENT_COUNT, + BUILTIN_VAR_ARGUMENT, + BUILTIN_VAR_ARGUMENT0, + BUILTIN_VAR_ARGUMENT1, + BUILTIN_VAR_ARGUMENT2, + BUILTIN_VAR_ARGUMENT3, + BUILTIN_VAR_ARGUMENT4, + BUILTIN_VAR_ARGUMENT5, + BUILTIN_VAR_ARGUMENT6, + BUILTIN_VAR_ARGUMENT7, + BUILTIN_VAR_ARGUMENT8, + BUILTIN_VAR_ARGUMENT9, + BUILTIN_VAR_ARGUMENT10, + BUILTIN_VAR_ARGUMENT11, + BUILTIN_VAR_ARGUMENT12, + BUILTIN_VAR_ARGUMENT13, + BUILTIN_VAR_ARGUMENT14, + BUILTIN_VAR_ARGUMENT15, + + // Keyboard + BUILTIN_VAR_KEYBOARD_KEY, + BUILTIN_VAR_KEYBOARD_LASTCHAR, + BUILTIN_VAR_KEYBOARD_LASTKEY, + + // Surfaces + BUILTIN_VAR_APPLICATION_SURFACE, + + // Constants + BUILTIN_VAR_TRUE, + BUILTIN_VAR_FALSE, + BUILTIN_VAR_PI, + BUILTIN_VAR_UNDEFINED, + + // Path action constants + BUILTIN_VAR_PATH_ACTION_STOP, + BUILTIN_VAR_PATH_ACTION_RESTART, + BUILTIN_VAR_PATH_ACTION_CONTINUE, + BUILTIN_VAR_PATH_ACTION_REVERSE, + + // Buffer type constants + BUILTIN_VAR_BUFFER_FIXED, + BUILTIN_VAR_BUFFER_GROW, + BUILTIN_VAR_BUFFER_WRAP, + BUILTIN_VAR_BUFFER_FAST, + + // Buffer data type constants + BUILTIN_VAR_BUFFER_U8, + BUILTIN_VAR_BUFFER_S8, + BUILTIN_VAR_BUFFER_U16, + BUILTIN_VAR_BUFFER_S16, + BUILTIN_VAR_BUFFER_U32, + BUILTIN_VAR_BUFFER_S32, + BUILTIN_VAR_BUFFER_F16, + BUILTIN_VAR_BUFFER_F32, + BUILTIN_VAR_BUFFER_F64, + BUILTIN_VAR_BUFFER_BOOL, + BUILTIN_VAR_BUFFER_STRING, + BUILTIN_VAR_BUFFER_U64, + BUILTIN_VAR_BUFFER_TEXT, + + // Buffer seek mode constants + BUILTIN_VAR_BUFFER_SEEK_START, + BUILTIN_VAR_BUFFER_SEEK_RELATIVE, + BUILTIN_VAR_BUFFER_SEEK_END, + + // Other + BUILTIN_VAR_FPS, + BUILTIN_VAR_DEBUG_MODE, + + // Gamepad constants + BUILTIN_VAR_GP_FACE1, + BUILTIN_VAR_GP_FACE2, + BUILTIN_VAR_GP_FACE3, + BUILTIN_VAR_GP_FACE4, + BUILTIN_VAR_GP_SHOULDERL, + BUILTIN_VAR_GP_SHOULDERR, + BUILTIN_VAR_GP_SHOULDERLB, + BUILTIN_VAR_GP_SHOULDERRB, + BUILTIN_VAR_GP_SELECT, + BUILTIN_VAR_GP_START, + BUILTIN_VAR_GP_STICKL, + BUILTIN_VAR_GP_STICKR, + BUILTIN_VAR_GP_PADU, + BUILTIN_VAR_GP_PADD, + BUILTIN_VAR_GP_PADL, + BUILTIN_VAR_GP_PADR, + BUILTIN_VAR_GP_HOME, + BUILTIN_VAR_GP_AXIS_LH, + BUILTIN_VAR_GP_AXIS_LV, + BUILTIN_VAR_GP_AXIS_RH, + BUILTIN_VAR_GP_AXIS_RV, + + // Async system + BUILTIN_VAR_ASYNC_LOAD, +} BuiltinVarId; + +void VMBuiltins_registerAll(VMContext* ctx); +int16_t VMBuiltins_resolveBuiltinVarId(const char* name); +// Asserts at startup that the internal builtin-var lookup table is strictly sorted by strcmp order (required for bsearch) and has no duplicates. +void VMBuiltins_checkIfBuiltinVarTableIsSorted(void); +RValue VMBuiltins_getVariable(VMContext* ctx, int16_t builtinVarId, const char* name, int32_t arrayIndex); +void VMBuiltins_setVariable(VMContext* ctx, int16_t builtinVarId, const char* name, RValue val, int32_t arrayIndex); diff --git a/src/wiiu/main.c b/src/wiiu/main.c new file mode 100644 index 00000000..98a25159 --- /dev/null +++ b/src/wiiu/main.c @@ -0,0 +1,1194 @@ +#include "../data_win.h" +#include "../vm.h" +#include "../runner.h" +#include "../runner_keyboard.h" + +#include "wiiu_file_system.h" +#include "wiiu_renderer.h" +#include "wiiu_audio_system.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +static int gBootLogFd = -1; +static OSTime gLoadingAnimationStartTime = 0; +static void bootLog(const char* message); + +typedef struct { + OSMutex mutex; + float progress; + int32_t lastChunkIndex; + char* dataWinPath; + DataWin* dataWin; + VMContext* vm; + WiiUFileSystem* fileSystem; + bool completed; + bool failed; +} WiiULoadingState; + +typedef struct { + bool keyHeld[GML_KEY_COUNT]; +} WiiUInputState; + +typedef struct { + uint32_t vpadButton; + int32_t gmlKey; +} WiiUKeyMap; + +typedef struct { + uint32_t wpadButton; + int32_t gmlKey; +} WiiUWiimoteKeyMap; + +static void dataWinProgressCallback( + const char* chunkName, + int chunkIndex, + int totalChunks, + DataWin* dataWin, + void* userData +); + +static const WiiUKeyMap WIIU_KEY_MAPS[] = { + { VPAD_BUTTON_UP, VK_UP }, + { VPAD_BUTTON_DOWN, VK_DOWN }, + { VPAD_BUTTON_LEFT, VK_LEFT }, + { VPAD_BUTTON_RIGHT, VK_RIGHT }, + { VPAD_BUTTON_A, 'Z' }, + { VPAD_BUTTON_B, 'X' }, + { VPAD_BUTTON_X, 'C' }, + { VPAD_BUTTON_Y, 'C' }, + { VPAD_BUTTON_PLUS, VK_ENTER }, + { VPAD_BUTTON_MINUS, VK_BACKSPACE }, + { VPAD_BUTTON_L, VK_PAGEDOWN }, + { VPAD_BUTTON_R, VK_PAGEUP }, + { VPAD_BUTTON_ZL, VK_SHIFT }, +}; + +static const WiiUWiimoteKeyMap WIIU_WIIMOTE_HORIZONTAL_KEY_MAPS[] = { + { WPAD_BUTTON_LEFT, VK_DOWN }, + { WPAD_BUTTON_RIGHT, VK_UP }, + { WPAD_BUTTON_UP, VK_LEFT }, + { WPAD_BUTTON_DOWN, VK_RIGHT }, + { WPAD_BUTTON_PLUS, 'C' }, + { WPAD_BUTTON_MINUS, 'C' }, + { WPAD_BUTTON_2, 'Z' }, + { WPAD_BUTTON_1, 'X' }, +}; + +static const WiiUWiimoteKeyMap WIIU_PRO_CONTROLLER_KEY_MAPS[] = { + { WPAD_PRO_BUTTON_UP, VK_UP }, + { WPAD_PRO_BUTTON_DOWN, VK_DOWN }, + { WPAD_PRO_BUTTON_LEFT, VK_LEFT }, + { WPAD_PRO_BUTTON_RIGHT, VK_RIGHT }, + { WPAD_PRO_BUTTON_A, 'Z' }, + { WPAD_PRO_BUTTON_B, 'X' }, + { WPAD_PRO_BUTTON_X, 'C' }, + { WPAD_PRO_BUTTON_Y, 'C' }, + { WPAD_PRO_BUTTON_PLUS, VK_ENTER }, + { WPAD_PRO_BUTTON_MINUS, VK_BACKSPACE }, + { WPAD_PRO_BUTTON_L, VK_PAGEDOWN }, + { WPAD_PRO_BUTTON_R, VK_PAGEUP }, + { WPAD_PRO_BUTTON_ZL, VK_SHIFT }, +}; + +static const WiiUWiimoteKeyMap WIIU_CLASSIC_CONTROLLER_KEY_MAPS[] = { + { WPAD_CLASSIC_BUTTON_UP, VK_UP }, + { WPAD_CLASSIC_BUTTON_DOWN, VK_DOWN }, + { WPAD_CLASSIC_BUTTON_LEFT, VK_LEFT }, + { WPAD_CLASSIC_BUTTON_RIGHT, VK_RIGHT }, + { WPAD_CLASSIC_BUTTON_A, 'Z' }, + { WPAD_CLASSIC_BUTTON_B, 'X' }, + { WPAD_CLASSIC_BUTTON_X, 'C' }, + { WPAD_CLASSIC_BUTTON_Y, 'C' }, + { WPAD_CLASSIC_BUTTON_PLUS, VK_ENTER }, + { WPAD_CLASSIC_BUTTON_MINUS, VK_BACKSPACE }, + { WPAD_CLASSIC_BUTTON_L, VK_PAGEDOWN }, + { WPAD_CLASSIC_BUTTON_R, VK_PAGEUP }, + { WPAD_CLASSIC_BUTTON_ZL, VK_SHIFT }, +}; + +static void bootLog(const char* message) { + if (gBootLogFd < 0 || message == NULL) return; + + if (strncmp(message, "vm:", 3) == 0) return; + if (strncmp(message, "perf:", 5) == 0) return; + if (strncmp(message, "wiiu_audio: playSound begin", 27) == 0) return; + if (strncmp(message, "wiiu_audio: playSound end", 25) == 0) return; + if (strncmp(message, "wiiu_audio: decodeSound begin", 29) == 0) return; + if (strncmp(message, "wiiu_audio: audo entry", 22) == 0) return; + if (strncmp(message, "wiiu_audio: audo head", 21) == 0) return; + if (strncmp(message, "wiiu_audio: try path=", 21) == 0) return; + if (strncmp(message, "wiiu_audio: external decode ok", 30) == 0) return; + + write(gBootLogFd, message, strlen(message)); + write(gBootLogFd, "\n", 1); + + if ( + strncmp(message, "stage:", 6) == 0 || + strncmp(message, "frame:", 6) == 0 || + strncmp(message, "runner:", 7) == 0 || + strncmp(message, "shutdown:", 9) == 0 || + strncmp(message, "datawin:", 8) == 0 || + strncmp(message, "procui:", 7) == 0 || + strncmp(message, "wiiu_", 5) == 0 || + strncmp(message, "perf:", 5) == 0 + ) { + fsync(gBootLogFd); + } +} + +static void openBootLog(void) { + if (!WHBMountSdCard()) return; + + const char* mountPath = WHBGetSdCardMountPath(); + if (mountPath == NULL) return; + + char logPath[512]; + snprintf(logPath, sizeof(logPath), "%s/wiiu/apps/cinnamon/bootlog.txt", mountPath); + gBootLogFd = open(logPath, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (gBootLogFd < 0) return; + + bootLog("stage: sd mounted"); +} + +static char* duplicateDirname(const char* path) { + const char* lastSlash = strrchr(path, '/'); + if (lastSlash == NULL) return strdup("."); + size_t length = (size_t) (lastSlash - path); + char* dir = malloc(length + 1); + memcpy(dir, path, length); + dir[length] = '\0'; + return dir; +} + +static bool fileExistsAtPath(const char* path) { + if (path == NULL) return false; + struct stat st; + return stat(path, &st) == 0; +} + +static char* buildDefaultDataWinPath(const char* argv0) { + const char* mountPath = WHBGetSdCardMountPath(); + if (mountPath != NULL) { + char sdAppPath[512]; + snprintf(sdAppPath, sizeof(sdAppPath), "%s/wiiu/apps/cinnamon/data.win", mountPath); + if (fileExistsAtPath(sdAppPath)) return strdup(sdAppPath); + } + + if (fileExistsAtPath("/vol/content/data.win")) { + return strdup("/vol/content/data.win"); + } + + char* dir = duplicateDirname(argv0); + size_t dirLen = strlen(dir); + const char suffix[] = "/data.win"; + char* result = malloc(dirLen + sizeof(suffix)); + memcpy(result, dir, dirLen); + memcpy(result + dirLen, suffix, sizeof(suffix)); + free(dir); + return result; +} + +static int32_t clampRenderDimension(int32_t value, int32_t fallback, int32_t maxValue) { + if (value <= 0) value = fallback; + if (value > maxValue) value = maxValue; + return value; +} + +static double elapsedMs(OSTime start, OSTime end) { + return (double) OSTicksToMicroseconds(end - start) / 1000.0; +} + +static void clearAllInputState(WiiUInputState* inputState, RunnerKeyboardState* keyboard) { + if (inputState != NULL) { + memset(inputState->keyHeld, 0, sizeof(inputState->keyHeld)); + } + if (keyboard == NULL) return; + + repeat(GML_KEY_COUNT, i) { + if (keyboard->keyDown[i]) { + RunnerKeyboard_onKeyUp(keyboard, i); + } + } + RunnerKeyboard_beginFrame(keyboard); +} + +static bool waitForForegroundRestore(void) { + bool loggedWait = false; + while (WHBProcIsRunning()) { + if (!loggedWait) { + bootLog("procui: waiting for foreground"); + loggedWait = true; + } + + ProcUIStatus status = ProcUIProcessMessages(TRUE); + switch (status) { + case PROCUI_STATUS_IN_FOREGROUND: + bootLog("procui: foreground restored"); + return true; + case PROCUI_STATUS_RELEASE_FOREGROUND: + bootLog("procui: release foreground"); + ProcUIDrawDoneRelease(); + break; + case PROCUI_STATUS_IN_BACKGROUND: + OSSleepTicks(OSMicrosecondsToTicks(50000)); + break; + case PROCUI_STATUS_EXITING: + bootLog("procui: exiting while backgrounded"); + WHBProcStopRunning(); + return false; + } + } + + if (loggedWait) bootLog("procui: stopped while backgrounded"); + return WHBProcIsRunning(); +} + +static void pumpProcUIState(void) { + if (!ProcUIIsRunning()) return; + ProcUIProcessMessages(FALSE); +} + +static void setLoadingProgress(WiiULoadingState* state, float progress) { + if (state == NULL) return; + if (progress < 0.0f) progress = 0.0f; + if (progress > 1.0f) progress = 1.0f; + + OSLockMutex(&state->mutex); + state->progress = progress; + OSUnlockMutex(&state->mutex); +} + +static float getLoadingProgress(WiiULoadingState* state) { + float progress = 0.0f; + if (state == NULL) return progress; + + OSLockMutex(&state->mutex); + progress = state->progress; + OSUnlockMutex(&state->mutex); + return progress; +} + +static int loadingWorkerThreadMain(int argc, const char** argv) { + (void) argc; + WiiULoadingState* state = (WiiULoadingState*) argv; + if (state == NULL) return 1; + + bootLog("stage: worker before DataWin_parse"); + DataWin* dataWin = DataWin_parse( + state->dataWinPath, + (DataWinParserOptions) { + .parseGen8 = true, + .parseOptn = true, + .parseLang = true, + .parseExtn = true, + .parseSond = true, + .parseAgrp = true, + .parseSprt = true, + .parseBgnd = true, + .parsePath = true, + .parseScpt = true, + .parseGlob = true, + .parseShdr = true, + .parseFont = true, + .parseTmln = true, + .parseObjt = true, + .parseRoom = true, + .parseTpag = true, + .parseCode = true, + .parseVari = true, + .parseFunc = true, + .parseStrg = true, + .parseTxtr = true, + .parseAudo = true, + .skipLoadingPreciseMasksForNonPreciseSprites = true, + .progressCallback = dataWinProgressCallback, + .progressCallbackUserData = state, + } + ); + bootLog("stage: worker after DataWin_parse"); + if (dataWin == NULL) { + OSLockMutex(&state->mutex); + state->failed = true; + state->completed = true; + OSUnlockMutex(&state->mutex); + return 1; + } + setLoadingProgress(state, 0.78f); + + bootLog("stage: worker before VM_create"); + VMContext* vm = VM_create(dataWin); + bootLog("stage: worker after VM_create"); + setLoadingProgress(state, 0.82f); + + WiiUFileSystem* fileSystem = WiiUFileSystem_create(state->dataWinPath); + bootLog("stage: worker after WiiUFileSystem_create"); + setLoadingProgress(state, 0.86f); + + OSLockMutex(&state->mutex); + state->dataWin = dataWin; + state->vm = vm; + state->fileSystem = fileSystem; + state->completed = true; + OSUnlockMutex(&state->mutex); + return 0; +} + +static int32_t resolveMappedKey(const RunnerKeyboardState* keyboard, int32_t gmlKey) { + (void) keyboard; + return gmlKey; +} + +static void setDesiredKeyState(bool* desiredKeys, const RunnerKeyboardState* keyboard, int32_t gmlKey, bool isHeld) { + int32_t mappedKey = resolveMappedKey(keyboard, gmlKey); + if (mappedKey >= 0 && mappedKey < GML_KEY_COUNT && isHeld) { + desiredKeys[mappedKey] = true; + } +} + +static void syncKeyState(WiiUInputState* inputState, RunnerKeyboardState* keyboard, int32_t gmlKey, bool isHeld) { + int32_t mappedKey = resolveMappedKey(keyboard, gmlKey); + bool wasHeld = (mappedKey >= 0 && mappedKey < GML_KEY_COUNT) + ? inputState->keyHeld[mappedKey] + : false; + if (isHeld && !wasHeld) { + RunnerKeyboard_onKeyDown(keyboard, gmlKey); + } else if (!isHeld && wasHeld) { + RunnerKeyboard_onKeyUp(keyboard, gmlKey); + } + if (mappedKey >= 0 && mappedKey < GML_KEY_COUNT) { + inputState->keyHeld[mappedKey] = isHeld; + } +} + +static void accumulateButtonsToDesiredKeys(bool* desiredKeys, RunnerKeyboardState* keyboard, uint32_t held) { + repeat(sizeof(WIIU_KEY_MAPS) / sizeof(WIIU_KEY_MAPS[0]), i) { + int32_t gmlKey = WIIU_KEY_MAPS[i].gmlKey; + if (gmlKey == VK_LEFT || gmlKey == VK_RIGHT || gmlKey == VK_UP || gmlKey == VK_DOWN) { + continue; + } + setDesiredKeyState(desiredKeys, keyboard, gmlKey, (held & WIIU_KEY_MAPS[i].vpadButton) != 0); + } +} + +static void accumulateWiimoteButtonsToDesiredKeys(bool* desiredKeys, RunnerKeyboardState* keyboard, uint32_t held) { + repeat(sizeof(WIIU_WIIMOTE_HORIZONTAL_KEY_MAPS) / sizeof(WIIU_WIIMOTE_HORIZONTAL_KEY_MAPS[0]), i) { + setDesiredKeyState( + desiredKeys, + keyboard, + WIIU_WIIMOTE_HORIZONTAL_KEY_MAPS[i].gmlKey, + (held & WIIU_WIIMOTE_HORIZONTAL_KEY_MAPS[i].wpadButton) != 0 + ); + } +} + +static void accumulateProButtonsToDesiredKeys(bool* desiredKeys, RunnerKeyboardState* keyboard, uint32_t held) { + repeat(sizeof(WIIU_PRO_CONTROLLER_KEY_MAPS) / sizeof(WIIU_PRO_CONTROLLER_KEY_MAPS[0]), i) { + setDesiredKeyState( + desiredKeys, + keyboard, + WIIU_PRO_CONTROLLER_KEY_MAPS[i].gmlKey, + (held & WIIU_PRO_CONTROLLER_KEY_MAPS[i].wpadButton) != 0 + ); + } +} + +static void accumulateClassicButtonsToDesiredKeys(bool* desiredKeys, RunnerKeyboardState* keyboard, uint32_t held) { + repeat(sizeof(WIIU_CLASSIC_CONTROLLER_KEY_MAPS) / sizeof(WIIU_CLASSIC_CONTROLLER_KEY_MAPS[0]), i) { + setDesiredKeyState( + desiredKeys, + keyboard, + WIIU_CLASSIC_CONTROLLER_KEY_MAPS[i].gmlKey, + (held & WIIU_CLASSIC_CONTROLLER_KEY_MAPS[i].wpadButton) != 0 + ); + } +} + +static bool axisPressedNegative(float value, bool wasHeld) { + const float pressDeadzone = 0.40f; + const float releaseDeadzone = 0.24f; + return value <= -(wasHeld ? releaseDeadzone : pressDeadzone); +} + +static bool axisPressedPositive(float value, bool wasHeld) { + const float pressDeadzone = 0.40f; + const float releaseDeadzone = 0.24f; + return value >= (wasHeld ? releaseDeadzone : pressDeadzone); +} + +static void accumulateDirectionalInputToDesiredKeys(bool* desiredKeys, WiiUInputState* inputState, RunnerKeyboardState* keyboard, uint32_t held, const VPADStatus* status) { + bool leftWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_LEFT)]; + bool rightWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_RIGHT)]; + bool upWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_UP)]; + bool downWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_DOWN)]; + + bool leftHeld = (held & VPAD_BUTTON_LEFT) != 0 || + axisPressedNegative(status->leftStick.x, leftWasHeld); + bool rightHeld = (held & VPAD_BUTTON_RIGHT) != 0 || + axisPressedPositive(status->leftStick.x, rightWasHeld); + bool upHeld = (held & VPAD_BUTTON_UP) != 0 || + axisPressedPositive(status->leftStick.y, upWasHeld); + bool downHeld = (held & VPAD_BUTTON_DOWN) != 0 || + axisPressedNegative(status->leftStick.y, downWasHeld); + + setDesiredKeyState(desiredKeys, keyboard, VK_LEFT, leftHeld); + setDesiredKeyState(desiredKeys, keyboard, VK_RIGHT, rightHeld); + setDesiredKeyState(desiredKeys, keyboard, VK_UP, upHeld); + setDesiredKeyState(desiredKeys, keyboard, VK_DOWN, downHeld); +} + +static void accumulateProDirectionalInputToDesiredKeys( + bool* desiredKeys, + WiiUInputState* inputState, + RunnerKeyboardState* keyboard, + uint32_t held, + const KPADStatus* status +) { + bool leftWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_LEFT)]; + bool rightWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_RIGHT)]; + bool upWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_UP)]; + bool downWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_DOWN)]; + + bool leftHeld = (held & WPAD_PRO_BUTTON_LEFT) != 0 || + axisPressedNegative(status->pro.leftStick.x, leftWasHeld); + bool rightHeld = (held & WPAD_PRO_BUTTON_RIGHT) != 0 || + axisPressedPositive(status->pro.leftStick.x, rightWasHeld); + bool upHeld = (held & WPAD_PRO_BUTTON_UP) != 0 || + axisPressedPositive(status->pro.leftStick.y, upWasHeld); + bool downHeld = (held & WPAD_PRO_BUTTON_DOWN) != 0 || + axisPressedNegative(status->pro.leftStick.y, downWasHeld); + + setDesiredKeyState(desiredKeys, keyboard, VK_LEFT, leftHeld); + setDesiredKeyState(desiredKeys, keyboard, VK_RIGHT, rightHeld); + setDesiredKeyState(desiredKeys, keyboard, VK_UP, upHeld); + setDesiredKeyState(desiredKeys, keyboard, VK_DOWN, downHeld); +} + +static void accumulateClassicDirectionalInputToDesiredKeys( + bool* desiredKeys, + WiiUInputState* inputState, + RunnerKeyboardState* keyboard, + uint32_t held, + const KPADStatus* status +) { + bool leftWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_LEFT)]; + bool rightWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_RIGHT)]; + bool upWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_UP)]; + bool downWasHeld = inputState->keyHeld[resolveMappedKey(keyboard, VK_DOWN)]; + + bool leftHeld = (held & WPAD_CLASSIC_BUTTON_LEFT) != 0 || + axisPressedNegative(status->classic.leftStick.x, leftWasHeld); + bool rightHeld = (held & WPAD_CLASSIC_BUTTON_RIGHT) != 0 || + axisPressedPositive(status->classic.leftStick.x, rightWasHeld); + bool upHeld = (held & WPAD_CLASSIC_BUTTON_UP) != 0 || + axisPressedPositive(status->classic.leftStick.y, upWasHeld); + bool downHeld = (held & WPAD_CLASSIC_BUTTON_DOWN) != 0 || + axisPressedNegative(status->classic.leftStick.y, downWasHeld); + + setDesiredKeyState(desiredKeys, keyboard, VK_LEFT, leftHeld); + setDesiredKeyState(desiredKeys, keyboard, VK_RIGHT, rightHeld); + setDesiredKeyState(desiredKeys, keyboard, VK_UP, upHeld); + setDesiredKeyState(desiredKeys, keyboard, VK_DOWN, downHeld); +} + +static void syncDesiredKeysToKeyboard(WiiUInputState* inputState, RunnerKeyboardState* keyboard, const bool* desiredKeys) { + repeat(GML_KEY_COUNT, key) { + bool isHeld = desiredKeys[key]; + bool wasHeld = inputState->keyHeld[key]; + if (isHeld && !wasHeld) { + RunnerKeyboard_onKeyDown(keyboard, key); + } else if (!isHeld && wasHeld) { + RunnerKeyboard_onKeyUp(keyboard, key); + } + inputState->keyHeld[key] = isHeld; + } +} + +void Runner_platformBootLog(const char* message) { bootLog(message); } +void VM_platformBootLog(const char* message) { bootLog(message); } +void DataWin_platformBootLog(const char* message) { bootLog(message); } +void WiiUFileSystem_platformBootLog(const char* message) { bootLog(message); } +void WiiUAudio_platformBootLog(const char* message) { bootLog(message); } +void WiiURenderer_platformBootLog(const char* message) { bootLog(message); } + +#include + +#define DOG_FRAME_W 21 +#define DOG_FRAME_H 83 +#define DOG_FRAME_COUNT 9 +#define DOG_FRAME_STRIDE 21 +#define DOG_FRAME_COPY_W 21 +#define DOG_SCALE 12 +#define DOG_REF_WIDTH 1920 +#define DOG_REF_HEIGHT 1080 +#define DOG_Y_OFFSET -200 + +typedef struct { + uint8_t* pixels; // RGBA8, row-major, DOG_FRAME_COPY_W * DOG_FRAME_H * 4 bytes per frame + int frameCount; + bool loaded; +} DogSprite; + +static DogSprite gDogSprite = { NULL, 0, false }; + +static void dogSprite_load(void) { + if (gDogSprite.loaded) return; + const char* paths[] = { + "/vol/content/loadingDog.png", + "/vol/content/resources/wiiu/loadingDog.png", + }; + + FILE* f = NULL; + repeat(sizeof(paths) / sizeof(paths[0]), i) { + f = fopen(paths[i], "rb"); + if (f != NULL) break; + } + if (f == NULL) { + return; + } + fseek(f, 0, SEEK_END); + long fileSize = ftell(f); + rewind(f); + uint8_t* fileData = (uint8_t*) malloc((size_t) fileSize); + if (fileData == NULL) { fclose(f); return; } + fread(fileData, 1, (size_t) fileSize, f); + fclose(f); + + int w, h, channels; + uint8_t* sheet = stbi_load_from_memory(fileData, (int) fileSize, &w, &h, &channels, 4); + free(fileData); + if (sheet == NULL) { + bootLog("wiiu_loading: failed to decode loadingDog.png"); + return; + } + + int sheetH = (h < DOG_FRAME_H) ? h : DOG_FRAME_H; + int frameCount = w / DOG_FRAME_STRIDE; + if (frameCount > DOG_FRAME_COUNT) frameCount = DOG_FRAME_COUNT; + if (frameCount == 0) { stbi_image_free(sheet); return; } + + size_t frameBytes = (size_t)(DOG_FRAME_COPY_W * sheetH * 4); + gDogSprite.pixels = (uint8_t*) malloc(frameBytes * (size_t) frameCount); + if (gDogSprite.pixels == NULL) { stbi_image_free(sheet); return; } + + for (int fi = 0; fi < frameCount; fi++) { + uint8_t* dst = gDogSprite.pixels + fi * frameBytes; + for (int row = 0; row < sheetH; row++) { + const uint8_t* src = sheet + (row * w + fi * DOG_FRAME_STRIDE) * 4; + memcpy(dst + row * DOG_FRAME_COPY_W * 4, src, (size_t)(DOG_FRAME_COPY_W * 4)); + } + } + + stbi_image_free(sheet); + gDogSprite.frameCount = frameCount; + gDogSprite.loaded = true; + bootLog("wiiu_loading: loadingDog.png loaded"); +} + +static void dogSprite_free(void) { + if (gDogSprite.pixels) { free(gDogSprite.pixels); gDogSprite.pixels = NULL; } + gDogSprite.loaded = false; +} + +static int resolveDogScale(uint32_t bufW, uint32_t bufH) { + float modifier = 1.0f; + + if (bufW < DOG_REF_WIDTH || bufH < DOG_REF_HEIGHT) { + float widthModifier = (float) bufW / (float) DOG_REF_WIDTH; + float heightModifier = (float) bufH / (float) DOG_REF_HEIGHT; + modifier = fminf(widthModifier, heightModifier); + } + + int scale = (int) lroundf((float) DOG_SCALE * modifier); + if (scale < 1) scale = 1; + if (scale > DOG_SCALE) scale = DOG_SCALE; + return scale; +} + +static void blitDogFrame(uint32_t* pixels, uint32_t pitch, + uint32_t bufW, uint32_t bufH, + float progress) { + if (!gDogSprite.loaded || gDogSprite.frameCount == 0) return; + + OSTime now = OSGetTime(); + OSTime startTime = gLoadingAnimationStartTime != 0 ? gLoadingAnimationStartTime : now; + uint64_t elapsedUs = OSTicksToMicroseconds(now - startTime); + uint64_t frameTicks = elapsedUs / 200000ull; + int frameIndex = (int) (frameTicks % (uint64_t) gDogSprite.frameCount); + + int spriteW = DOG_FRAME_COPY_W; + int spriteH = DOG_FRAME_H; + + int scale = resolveDogScale(bufW, bufH); + int scaledW = spriteW * scale; + int scaledH = spriteH * scale; + + int dogX = (int)((int32_t)bufW / 2 - scaledW / 2); + + int targetY = (int)((int32_t)bufH / 2 - scaledH / 2) + DOG_Y_OFFSET; + int maxY = (int) bufH - scaledH; + if (maxY < 0) maxY = 0; + if (targetY < 0) targetY = 0; + if (targetY > maxY) targetY = maxY; + + int startY = -scaledH; + int dogY = (int) lroundf((float) startY + ((float) (targetY - startY) * progress * 0.9)); + if (dogY < startY) dogY = startY; + if (dogY > targetY) dogY = targetY; + + const uint8_t* framePixels = gDogSprite.pixels + + (size_t)(frameIndex * spriteW * spriteH * 4); + + for (int row = 0; row < spriteH; row++) { + for (int col = 0; col < spriteW; col++) { + const uint8_t* sp = framePixels + (row * spriteW + col) * 4; + uint8_t sr = sp[0], sg = sp[1], sb = sp[2], sa = sp[3]; + if (sa == 0 || (sr == 0 && sg == 0 && sb == 0)) continue; + uint32_t packed = ((uint32_t) sr << 24) | ((uint32_t) sg << 16) | + ((uint32_t) sb << 8) | 0xFFu; + for (int sy = 0; sy < scale; sy++) { + int dstY = dogY + row * scale + sy; + if (dstY < 0 || (uint32_t) dstY >= bufH) continue; + for (int sx = 0; sx < scale; sx++) { + int dstX = dogX + col * scale + sx; + if (dstX < 0 || (uint32_t) dstX >= bufW) continue; + pixels[(uint32_t) dstY * pitch + (uint32_t) dstX] = packed; + } + } + } + } +} + +static void fillRectLinear(uint32_t* pixels, uint32_t pitch, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h, + uint8_t r, uint8_t g, uint8_t b) { + uint32_t color = ((uint32_t)r << 24) | ((uint32_t)g << 16) | ((uint32_t)b << 8) | 0xFFu; + for (uint32_t row = y; row < y + h; row++) { + for (uint32_t col = x; col < x + w; col++) { + pixels[row * pitch + col] = color; + } + } +} + +static void drawLoadingBarToBuffer(GX2ColorBuffer* buffer, float progress) { + if (buffer == NULL) return; + + if (progress < 0.0f) progress = 0.0f; + if (progress > 1.0f) progress = 1.0f; + + uint32_t bw = buffer->surface.width; + uint32_t bh = buffer->surface.height; + + GX2Surface linear; + memset(&linear, 0, sizeof(linear)); + linear.dim = GX2_SURFACE_DIM_TEXTURE_2D; + linear.width = bw; + linear.height = bh; + linear.depth = 1; + linear.mipLevels = 1; + linear.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8; + linear.tileMode = GX2_TILE_MODE_LINEAR_ALIGNED; + linear.use = GX2_SURFACE_USE_COLOR_BUFFER; + GX2CalcSurfaceSizeAndAlignment(&linear); + + void* mem = MEMAllocFromDefaultHeapEx(linear.imageSize, linear.alignment); + if (mem == NULL) return; + linear.image = mem; + + uint32_t pitch = linear.pitch; // in pixels, aligned + uint32_t* pixels = (uint32_t*) mem; + + // Background + fillRectLinear(pixels, pitch, 0, 0, bw, bh, 0, 0, 0); + + // Bar geometry + uint32_t barW = bw * 2u / 3u; + uint32_t barH = bh / 18u; + if (barW < 64u) barW = bw > 64u ? bw - 16u : bw; + if (barH < 8u) barH = 8u; + uint32_t barX = (bw > barW) ? (bw - barW) / 2u : 0u; + uint32_t barY = (bh * 4u) / 5u; + if (barY + barH >= bh) barY = bh > barH + 8u ? bh - barH - 8u : 0u; + + uint32_t border = 3u; + uint32_t trackX = barX + border; + uint32_t trackY = barY + border; + uint32_t trackW = barW > border * 2u ? barW - border * 2u : barW; + uint32_t trackH = barH > border * 2u ? barH - border * 2u : barH; + uint32_t fillW = (uint32_t)((float)trackW * progress); + if (progress > 0.0f && fillW == 0u) fillW = 1u; + if (fillW > trackW) fillW = trackW; + + fillRectLinear(pixels, pitch, barX, barY, barW, barH, 204, 179, 61); // gold border + fillRectLinear(pixels, pitch, trackX, trackY, trackW, trackH, 26, 26, 36); // dark trough + if (fillW > 0u) + fillRectLinear(pixels, pitch, trackX, trackY, fillW, trackH, 250, 194, 56); // bright fill + + // draw the annoying dog rolling down from top to center as progress increases. + blitDogFrame(pixels, pitch, bw, bh, progress); + GX2Invalidate(GX2_INVALIDATE_MODE_CPU, mem, linear.imageSize); + GX2CopySurface(&linear, 0, 0, &buffer->surface, 0, 0); + GX2DrawDone(); + + MEMFreeToDefaultHeap(mem); +} + +static void presentStartupFrame(uint8_t r, uint8_t g, uint8_t b) { + GX2ColorBuffer* tv = WHBGfxGetTVColourBuffer(); + GX2ColorBuffer* drc = WHBGfxGetDRCColourBuffer(); + GX2ContextState* tvContext = WHBGfxGetTVContextState(); + GX2ContextState* drcContext = WHBGfxGetDRCContextState(); + if (tv == NULL || drc == NULL || tvContext == NULL || drcContext == NULL) { + return; + } + + float rf = (float) r / 255.0f; + float gf = (float) g / 255.0f; + float bf = (float) b / 255.0f; + + GX2SetContextState(tvContext); + GX2ClearColor(tv, rf, gf, bf, 1.0f); + GX2CopyColorBufferToScanBuffer(tv, GX2_SCAN_TARGET_TV); + + GX2SetContextState(drcContext); + GX2ClearColor(drc, rf, gf, bf, 1.0f); + GX2CopyColorBufferToScanBuffer(drc, GX2_SCAN_TARGET_DRC); + + GX2Flush(); + GX2SwapScanBuffers(); + GX2DrawDone(); +} + +static void presentLoadingProgress(float progress) { + GX2ColorBuffer* tv = WHBGfxGetTVColourBuffer(); + GX2ColorBuffer* drc = WHBGfxGetDRCColourBuffer(); + GX2ContextState* tvContext = WHBGfxGetTVContextState(); + GX2ContextState* drcContext = WHBGfxGetDRCContextState(); + if (tv == NULL || drc == NULL || tvContext == NULL || drcContext == NULL) { + return; + } + + drawLoadingBarToBuffer(tv, progress); + GX2SetContextState(tvContext); + GX2CopyColorBufferToScanBuffer(tv, GX2_SCAN_TARGET_TV); + + drawLoadingBarToBuffer(drc, progress); + GX2SetContextState(drcContext); + GX2CopyColorBufferToScanBuffer(drc, GX2_SCAN_TARGET_DRC); + + GX2Flush(); + GX2SwapScanBuffers(); + GX2DrawDone(); +} + +static void dataWinProgressCallback( + const char* chunkName, + int chunkIndex, + int totalChunks, + DataWin* dataWin, + void* userData +) { + (void) chunkName; + (void) dataWin; + WiiULoadingState* state = (WiiULoadingState*) userData; + if (state == NULL) return; + OSLockMutex(&state->mutex); + if (chunkIndex == state->lastChunkIndex) { + OSUnlockMutex(&state->mutex); + return; + } + state->lastChunkIndex = chunkIndex; + OSUnlockMutex(&state->mutex); + + float parseProgress = totalChunks > 0 ? (float) (chunkIndex + 1) / (float) totalChunks : 0.0f; + if (parseProgress < 0.0f) parseProgress = 0.0f; + if (parseProgress > 1.0f) parseProgress = 1.0f; + + setLoadingProgress(state, 0.10f + parseProgress * 0.65f); +} + +int main(int argc, char* argv[]) { + bool loadingThreadJoined = false; + bool loadingThreadStarted = false; + bool loadingThreadDetached = false; + DataWin* dataWin = NULL; + WiiUFileSystem* fileSystem = NULL; + Runner* runner = NULL; + Renderer* renderer = NULL; + WiiUAudioSystem* audio = NULL; + char* dataWinPath = NULL; + void* loadingThreadStack = NULL; + WiiULoadingState loadingState; + memset(&loadingState, 0, sizeof(loadingState)); + OSThread loadingThread; + + WHBProcInit(); + openBootLog(); + bootLog("stage: after WHBProcInit"); + + if (!WHBGfxInit()) { + bootLog("stage: WHBGfxInit failed"); + WHBProcShutdown(); + return 1; + } + bootLog("stage: after WHBGfxInit"); + presentStartupFrame(0, 0, 0); + presentLoadingProgress(0.02f); + + dogSprite_load(); + + VPADInit(); + WPADInit(); + WPADEnableURCC(true); + KPADInit(); + bootLog("stage: input init complete"); + presentLoadingProgress(0.05f); + + dataWinPath = argc > 1 ? strdup(argv[1]) : buildDefaultDataWinPath(argv[0]); + if (gBootLogFd >= 0) { + char pathBuffer[768]; + snprintf(pathBuffer, sizeof(pathBuffer), "data.win path: %s", dataWinPath); + bootLog(pathBuffer); + } + + OSInitMutexEx(&loadingState.mutex, "cinnamon-loading"); + loadingState.progress = 0.05f; + loadingState.lastChunkIndex = -1; + loadingState.dataWinPath = dataWinPath; + + enum { WIIU_LOADING_THREAD_STACK_SIZE = 256 * 1024 }; + int loadingThreadResult = 0; + loadingThreadStack = MEMAllocFromDefaultHeapEx(WIIU_LOADING_THREAD_STACK_SIZE, 16); + + gLoadingAnimationStartTime = OSGetTime(); + bootLog("stage: before loading worker create"); + if (loadingThreadStack != NULL && + OSCreateThread( + &loadingThread, + loadingWorkerThreadMain, + 0, + (char*) &loadingState, + (uint8_t*) loadingThreadStack + WIIU_LOADING_THREAD_STACK_SIZE, + WIIU_LOADING_THREAD_STACK_SIZE, + 16, + OS_THREAD_ATTRIB_AFFINITY_ANY + )) { + OSSetThreadName(&loadingThread, "cinnamon-loader"); + OSResumeThread(&loadingThread); + loadingThreadStarted = true; + bootLog("stage: loading worker started"); + } else { + bootLog("stage: loading worker create failed"); + if (loadingThreadStack != NULL) { + MEMFreeToDefaultHeap(loadingThreadStack); + loadingThreadStack = NULL; + } + loadingThreadResult = loadingWorkerThreadMain(0, (const char**) &loadingState); + } + + while (WHBProcIsRunning()) { + float progress = getLoadingProgress(&loadingState); + bool completed = false; + bool failed = false; + + OSLockMutex(&loadingState.mutex); + completed = loadingState.completed; + failed = loadingState.failed; + OSUnlockMutex(&loadingState.mutex); + + presentLoadingProgress(progress); + if (completed || failed) break; + OSSleepTicks(OSMicrosecondsToTicks(83333)); + } + + if (!WHBProcIsRunning()) { + goto shutdown; + } + + if (loadingThreadStarted) { + OSJoinThread(&loadingThread, &loadingThreadResult); + loadingThreadJoined = true; + } + if (loadingThreadStack != NULL) { + MEMFreeToDefaultHeap(loadingThreadStack); + loadingThreadStack = NULL; + } + + if (loadingState.failed || loadingThreadResult != 0 || + loadingState.dataWin == NULL || loadingState.vm == NULL || + loadingState.fileSystem == NULL) { + bootLog("stage: loading worker failed"); + goto shutdown; + } + + dataWin = loadingState.dataWin; + fileSystem = loadingState.fileSystem; + loadingState.dataWin = NULL; + loadingState.fileSystem = NULL; + VMContext* vm = loadingState.vm; + loadingState.vm = NULL; + + renderer = WiiURenderer_create(); + bootLog("stage: after WiiURenderer_create"); + renderer->vtable->init(renderer, dataWin); + bootLog("stage: after renderer init"); + + audio = WiiUAudioSystem_create(); + audio->base.vtable->init((AudioSystem*) audio, dataWin, (FileSystem*) fileSystem); + bootLog("stage: after audio init"); + + runner = Runner_create(dataWin, vm, renderer, (FileSystem*) fileSystem, (AudioSystem*) audio); + bootLog("stage: after Runner_create"); + + bootLog("stage: before Runner_initFirstRoom"); + Runner_initFirstRoom(runner); + bootLog("stage: after Runner_initFirstRoom"); + + OSTime lastFrameTime = OSGetTime(); + uint32_t perfFrameCount = 0; + double perfVmMs = 0.0; + double perfRenderMs = 0.0; + bool firstFrameTracePending = true; + WiiUInputState inputState; + memset(&inputState, 0, sizeof(inputState)); + bootLog("stage: before main loop"); + + while (WHBProcIsRunning()) { + if (runner == NULL) { + bootLog("stage: runner null before main loop"); + break; + } + if (runner->shouldExit) { + bootLog("stage: runner requested exit before frame"); + break; + } + + OSTime frameStartTime = OSGetTime(); + bool desiredKeys[GML_KEY_COUNT]; + memset(desiredKeys, 0, sizeof(desiredKeys)); + + VPADStatus vpadStatus; + VPADReadError error; + memset(&vpadStatus, 0, sizeof(vpadStatus)); + if (VPADRead(VPAD_CHAN_0, &vpadStatus, 1, &error) > 0 && error == VPAD_READ_SUCCESS) { + accumulateButtonsToDesiredKeys(desiredKeys, runner->keyboard, vpadStatus.hold); + accumulateDirectionalInputToDesiredKeys(desiredKeys, &inputState, runner->keyboard, vpadStatus.hold, &vpadStatus); + } + + repeat(7, channel) { + KPADStatus kpadStatus; + KPADError kpadError = KPAD_ERROR_OK; + memset(&kpadStatus, 0, sizeof(kpadStatus)); + + if (KPADReadEx((KPADChan) channel, &kpadStatus, 1, &kpadError) <= 0 || kpadError != KPAD_ERROR_OK) { + continue; + } + + switch (kpadStatus.extensionType) { + case WPAD_EXT_CORE: + case WPAD_EXT_MPLUS: + accumulateWiimoteButtonsToDesiredKeys(desiredKeys, runner->keyboard, kpadStatus.hold); + break; + case WPAD_EXT_PRO_CONTROLLER: + accumulateProButtonsToDesiredKeys(desiredKeys, runner->keyboard, kpadStatus.pro.hold); + accumulateProDirectionalInputToDesiredKeys( + desiredKeys, + &inputState, + runner->keyboard, + kpadStatus.pro.hold, + &kpadStatus + ); + break; + case WPAD_EXT_CLASSIC: + case WPAD_EXT_MPLUS_CLASSIC: + accumulateClassicButtonsToDesiredKeys(desiredKeys, runner->keyboard, kpadStatus.classic.hold); + accumulateClassicDirectionalInputToDesiredKeys( + desiredKeys, + &inputState, + runner->keyboard, + kpadStatus.classic.hold, + &kpadStatus + ); + break; + } + } + + syncDesiredKeysToKeyboard(&inputState, runner->keyboard, desiredKeys); + + OSTime vmStart = OSGetTime(); + if (firstFrameTracePending) bootLog("frame: before Runner_step"); + Runner_step(runner); + if (firstFrameTracePending) bootLog("frame: after Runner_step"); + OSTime vmEnd = OSGetTime(); + perfVmMs += elapsedMs(vmStart, vmEnd); + + float deltaTime = (float) OSTicksToMicroseconds(frameStartTime - lastFrameTime) / 1000000.0f; + if (deltaTime < 0.0f) deltaTime = 0.0f; + if (deltaTime > 0.1f) deltaTime = 0.1f; + + if (runner->audioSystem != NULL) { + if (firstFrameTracePending) bootLog("frame: before audio update"); + runner->audioSystem->vtable->update(runner->audioSystem, deltaTime); + if (firstFrameTracePending) bootLog("frame: after audio update"); + } + + Gen8* gen8 = &dataWin->gen8; + int32_t nativeGameW = (int32_t) gen8->defaultWindowWidth; + int32_t nativeGameH = (int32_t) gen8->defaultWindowHeight; + int32_t gameW = clampRenderDimension(nativeGameW, 640, nativeGameW > 0 ? nativeGameW : 640); + int32_t gameH = clampRenderDimension(nativeGameH, 480, nativeGameH > 0 ? nativeGameH : 480); + float portScaleX = nativeGameW > 0 ? (float) gameW / (float) nativeGameW : 1.0f; + float portScaleY = nativeGameH > 0 ? (float) gameH / (float) nativeGameH : 1.0f; + + OSTime renderStart = OSGetTime(); + + if (!WHBProcIsRunning()) { + perfRenderMs += elapsedMs(renderStart, OSGetTime()); + RunnerKeyboard_beginFrame(runner->keyboard); + break; + } + + if (firstFrameTracePending) bootLog("frame: before beginFrame"); + WiiURenderer_setClearColor((WiiURenderer*) renderer, runner->drawBackgroundColor ? runner->backgroundColor : 0x000000); + renderer->vtable->beginFrame(renderer, gameW, gameH, gameW, gameH); + if (firstFrameTracePending) bootLog("frame: after beginFrame"); + + Room* activeRoom = runner->currentRoom; + bool viewsEnabled = (activeRoom->flags & 1) != 0; + bool anyViewRendered = false; + + if (viewsEnabled) { + repeat(8, vi) { + if (!activeRoom->views[vi].enabled) continue; + + runner->viewCurrent = vi; + renderer->vtable->beginView( + renderer, + activeRoom->views[vi].viewX, + activeRoom->views[vi].viewY, + activeRoom->views[vi].viewWidth, + activeRoom->views[vi].viewHeight, + (int32_t) lroundf((float) activeRoom->views[vi].portX * portScaleX), + (int32_t) lroundf((float) activeRoom->views[vi].portY * portScaleY), + (int32_t) lroundf((float) activeRoom->views[vi].portWidth * portScaleX), + (int32_t) lroundf((float) activeRoom->views[vi].portHeight * portScaleY), + runner->views[vi].viewAngle + ); + Runner_draw(runner); + renderer->vtable->endView(renderer); + anyViewRendered = true; + } + } + + if (!anyViewRendered) { + runner->viewCurrent = 0; + if (firstFrameTracePending) bootLog("frame: before Runner_draw"); + renderer->vtable->beginView(renderer, 0, 0, gameW, gameH, 0, 0, gameW, gameH, 0.0f); + Runner_draw(runner); + renderer->vtable->endView(renderer); + if (firstFrameTracePending) bootLog("frame: after Runner_draw"); + } + + runner->viewCurrent = 0; + if (firstFrameTracePending) bootLog("frame: before endFrame"); + renderer->vtable->endFrame(renderer); + if (firstFrameTracePending) bootLog("frame: after endFrame"); + OSTime renderEnd = OSGetTime(); + perfRenderMs += elapsedMs(renderStart, renderEnd); + + RunnerKeyboard_beginFrame(runner->keyboard); + if (firstFrameTracePending) { + bootLog("frame: first frame complete"); + firstFrameTracePending = false; + } + + perfFrameCount++; + if (perfFrameCount >= 60) { + perfFrameCount = 0; + perfVmMs = 0.0; + perfRenderMs = 0.0; + } + + OSTime frameEndTime = OSGetTime(); + double frameElapsedMs = elapsedMs(frameStartTime, frameEndTime); + double targetFrameMs = 1000.0 / ((runner->currentRoom != NULL && runner->currentRoom->speed > 0) + ? (double) runner->currentRoom->speed + : 30.0); + if (frameElapsedMs < targetFrameMs) { + useconds_t remainingUs = (useconds_t)((targetFrameMs - frameElapsedMs) * 1000.0); + if (remainingUs > 0) usleep(remainingUs); + lastFrameTime = OSGetTime(); + } else { + lastFrameTime = frameEndTime; + } + } + + +shutdown: + bootLog("shutdown: begin"); + + if (audio != NULL) { + bootLog("shutdown: before audio destroy"); + audio->base.vtable->destroy((AudioSystem*) audio); + audio = NULL; + bootLog("shutdown: after audio destroy"); + } + + if (loadingThreadStarted && !loadingThreadJoined) { + bootLog("shutdown: before loading thread detach"); + OSDetachThread(&loadingThread); + bootLog("shutdown: after loading thread detach"); + } + + if (ProcUIInForeground()) { + bootLog("shutdown: before GX2DrawDone [fg]"); + GX2DrawDone(); + bootLog("shutdown: after GX2DrawDone"); + WHBGfxShutdown(); + bootLog("shutdown: after WHBGfxShutdown"); + } else { + bootLog("shutdown: GX2Shutdown [bg]"); + GX2Shutdown(); + bootLog("shutdown: after GX2Shutdown"); + } + + bootLog("shutdown: before WHBProcShutdown"); + if (gBootLogFd >= 0) { + close(gBootLogFd); + gBootLogFd = -1; + } + WHBUnmountSdCard(); + WHBProcShutdown(); + return 0; +} diff --git a/src/wiiu/pos_col_gsh.h b/src/wiiu/pos_col_gsh.h new file mode 100644 index 00000000..d4516885 --- /dev/null +++ b/src/wiiu/pos_col_gsh.h @@ -0,0 +1,83 @@ +unsigned char resources_wiiu_shaders_pos_col_gsh[] = { + 0x47, 0x66, 0x78, 0x32, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x4c, 0x4b, 0x7b, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x01, 0xa8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0x8a, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xd0, 0x60, 0x01, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0xd0, 0x60, 0x01, 0x3c, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0f, 0xff, 0xca, 0x70, 0x01, 0x5c, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0xca, 0x70, 0x01, 0x68, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x61, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x00, 0x00, 0x00, 0x61, 0x43, 0x6f, 0x6c, 0x6f, 0x75, 0x72, 0x00, + 0xd0, 0x60, 0x00, 0xf8, 0xd0, 0x60, 0x01, 0x08, 0xca, 0x70, 0x01, 0x3c, + 0xca, 0x70, 0x01, 0x4c, 0x7d, 0x42, 0x4c, 0x4b, 0x00, 0x00, 0x00, 0x28, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x70, 0xd0, 0x60, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x14, 0xd0, 0x60, 0x01, 0x5c, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0xd0, 0x60, 0x01, 0x70, 0x42, 0x4c, 0x4b, 0x7b, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x89, + 0x00, 0x40, 0x01, 0xc0, 0x88, 0x06, 0x00, 0x94, 0x3c, 0xa0, 0x00, 0xc0, + 0x88, 0x06, 0x00, 0x94, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0x00, 0x00, 0x00, 0x80, + 0x00, 0x0d, 0x00, 0x00, 0x42, 0x4c, 0x4b, 0x7b, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x01, 0x1c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x8a, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xd0, 0x60, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0f, 0xff, + 0xd0, 0x60, 0x00, 0xcc, 0x7d, 0x42, 0x4c, 0x4b, 0x00, 0x00, 0x00, 0x28, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xd0, 0x60, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xd0, 0x60, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xd0, 0x60, 0x00, 0xf0, 0x42, 0x4c, 0x4b, 0x7b, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x88, 0x06, 0x20, 0x94, + 0x42, 0x4c, 0x4b, 0x7b, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00 +}; +unsigned int resources_wiiu_shaders_pos_col_gsh_len = 956; diff --git a/src/wiiu/stb_impl.c b/src/wiiu/stb_impl.c new file mode 100644 index 00000000..a12673f3 --- /dev/null +++ b/src/wiiu/stb_impl.c @@ -0,0 +1,6 @@ +#define STBI_NO_THREAD_LOCALS +#define STB_IMAGE_IMPLEMENTATION +#include + +#define STB_DS_IMPLEMENTATION +#include \ No newline at end of file diff --git a/src/wiiu/textured_quad_gsh.h b/src/wiiu/textured_quad_gsh.h new file mode 100644 index 00000000..959da40d --- /dev/null +++ b/src/wiiu/textured_quad_gsh.h @@ -0,0 +1,95 @@ +unsigned char resources_wiiu_shaders_textured_quad_gsh[] = { + 0x47, 0x66, 0x78, 0x32, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x4c, 0x4b, 0x7b, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x01, 0xc8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0xff, 0xff, 0x8b, 0x8a, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xd0, 0x60, 0x01, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0xd0, 0x60, 0x01, 0x3c, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0f, 0xff, 0xca, 0x70, 0x01, 0x6c, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0xca, 0x70, 0x01, 0x78, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0xca, 0x70, 0x01, 0x84, 0x00, 0x00, 0x00, 0x0b, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x61, 0x50, 0x6f, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x61, 0x54, 0x65, 0x78, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x61, 0x43, 0x6f, 0x6c, + 0x6f, 0x75, 0x72, 0x00, 0xd0, 0x60, 0x00, 0xf8, 0xd0, 0x60, 0x01, 0x08, + 0xca, 0x70, 0x01, 0x3c, 0xca, 0x70, 0x01, 0x4c, 0xca, 0x70, 0x01, 0x5c, + 0x7d, 0x42, 0x4c, 0x4b, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x8c, 0xd0, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0xd0, 0x60, 0x01, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0xd0, 0x60, 0x01, 0x8c, 0x42, 0x4c, 0x4b, 0x7b, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x89, 0x01, 0xc0, 0x01, 0xc0, + 0x88, 0x06, 0x80, 0x93, 0x00, 0x40, 0x01, 0xc0, 0xc8, 0x0f, 0x00, 0x94, + 0x3c, 0xa0, 0x00, 0xc0, 0x88, 0x06, 0x00, 0x94, 0x06, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, + 0x00, 0x00, 0x00, 0x80, 0x00, 0x0d, 0x00, 0x00, 0x42, 0x4c, 0x4b, 0x7b, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x01, 0x3c, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, + 0x10, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xd0, 0x60, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x01, + 0xd0, 0x60, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x0f, 0xff, 0xca, 0x70, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x75, 0x54, 0x65, 0x78, 0x74, 0x75, 0x72, 0x65, + 0x00, 0x00, 0x00, 0x00, 0xd0, 0x60, 0x00, 0xcc, 0xd0, 0x60, 0x00, 0xd4, + 0xca, 0x70, 0x00, 0xf0, 0x7d, 0x42, 0x4c, 0x4b, 0x00, 0x00, 0x00, 0x28, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0xd0, 0x60, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0c, 0xd0, 0x60, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0xd0, 0x60, 0x01, 0x08, 0x42, 0x4c, 0x4b, 0x7b, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, + 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xa0, 0x00, 0x00, 0x00, 0xc0, + 0x88, 0x06, 0x20, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0d, 0xf0, 0x00, 0x00, 0x80, 0xfc, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, + 0x00, 0x24, 0x80, 0x00, 0x10, 0x01, 0x00, 0x20, 0x00, 0x28, 0x00, 0x01, + 0x10, 0x01, 0x00, 0x40, 0x00, 0x2c, 0x80, 0x81, 0x10, 0x01, 0x00, 0x60, + 0x42, 0x4c, 0x4b, 0x7b, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00 +}; +unsigned int resources_wiiu_shaders_textured_quad_gsh_len = 1100; diff --git a/src/wiiu/wiiu_audio_system.c b/src/wiiu/wiiu_audio_system.c new file mode 100644 index 00000000..d67e1292 --- /dev/null +++ b/src/wiiu/wiiu_audio_system.c @@ -0,0 +1,1102 @@ +#include "wiiu_audio_system.h" + +#include "../data_win.h" +#include "../utils.h" + +#include + +#include +#include +#include + +#include "stb_ds.h" +#include "../../vendor/stb/vorbis/stb_vorbis.c" + +static void WiiUAudio_bootLog(const char* message); +static bool WiiUAudio_decodeSoundData(WiiUAudioSystem* wiiu, const uint8_t* data, size_t dataSize, WiiUDecodedSound* out); + +static DataWin* WiiUAudio_mainDataWin(WiiUAudioSystem* wiiu) { + return arrlen(wiiu->base.audioGroups) > 0 ? wiiu->base.audioGroups[0] : NULL; +} + +__attribute__((weak)) void WiiUAudio_platformBootLog(const char* message) { + (void) message; +} + +static void WiiUAudio_bootLog(const char* message) { + WiiUAudio_platformBootLog(message); +} + +static uint16_t WiiUAudio_readLe16(const uint8_t* data) { + return (uint16_t) ((uint16_t) data[0] | ((uint16_t) data[1] << 8)); +} + +static uint32_t WiiUAudio_readLe32(const uint8_t* data) { + return + ((uint32_t) data[0]) | + ((uint32_t) data[1] << 8) | + ((uint32_t) data[2] << 16) | + ((uint32_t) data[3] << 24); +} + +static float WiiUAudio_clampSample(float sample) { + if (sample < -1.0f) return -1.0f; + if (sample > 1.0f) return 1.0f; + return sample; +} + +static float WiiUAudio_clampStreamGain(float gain) { + if (gain < 0.0f) return 0.0f; + if (gain > 1.0f) return 1.0f; + return gain; +} + +static void WiiUAudio_resetInstance(WiiUSoundInstance* inst) { + if (inst->audioStream != NULL) { + SDL_FreeAudioStream(inst->audioStream); + } + if (inst->vorbisStream != NULL) { + stb_vorbis_close(inst->vorbisStream); + } + free(inst->streamDecodeBuffer); + free(inst->streamMixBuffer); + memset(inst, 0, sizeof(*inst)); +} + +static bool WiiUAudio_shouldStreamSound(const Sound* sound) { + if (sound == NULL || sound->file == NULL) return false; + if ((sound->flags & 0x01) != 0) return false; + return strncmp(sound->file, "mus_", 4) == 0; +} + +static bool WiiUAudio_ensureStreamScratch(WiiUAudioSystem* wiiu, uint32_t sampleCount) { + if (wiiu->streamScratchSamples < sampleCount) { + float* newScratch = realloc(wiiu->streamScratch, (size_t) sampleCount * sizeof(float)); + if (newScratch == NULL) return false; + wiiu->streamScratch = newScratch; + wiiu->streamScratchSamples = sampleCount; + } + return wiiu->streamScratch != NULL; +} + +static bool WiiUAudio_ensureStreamMixCapacity(WiiUSoundInstance* inst, uint32_t frameCapacity, uint32_t channelCount) { + if (inst->streamMixCapacity >= frameCapacity) return true; + uint32_t newCapacity = inst->streamMixCapacity > 0 ? inst->streamMixCapacity : 1024; + while (newCapacity < frameCapacity) newCapacity *= 2; + float* newBuffer = realloc(inst->streamMixBuffer, (size_t) newCapacity * (size_t) channelCount * sizeof(float)); + if (newBuffer == NULL) return false; + inst->streamMixBuffer = newBuffer; + inst->streamMixCapacity = newCapacity; + return true; +} + +static bool WiiUAudio_tryOpenVorbisStream(WiiUAudioSystem* wiiu, const char* path, WiiUSoundInstance* slot) { + int error = 0; + stb_vorbis* vorbis = stb_vorbis_open_filename(path, &error, NULL); + if (vorbis == NULL) return false; + + stb_vorbis_info info = stb_vorbis_get_info(vorbis); + if (info.channels <= 0 || info.sample_rate <= 0) { + stb_vorbis_close(vorbis); + return false; + } + + SDL_AudioStream* stream = SDL_NewAudioStream( + AUDIO_F32SYS, + (Uint8) info.channels, + info.sample_rate, + AUDIO_F32SYS, + (Uint8) wiiu->audioSpec.channels, + wiiu->audioSpec.freq + ); + if (stream == NULL) { + stb_vorbis_close(vorbis); + return false; + } + + uint32_t decodeFrames = 2048; + float* decodeBuffer = malloc((size_t) decodeFrames * (size_t) info.channels * sizeof(float)); + if (decodeBuffer == NULL) { + SDL_FreeAudioStream(stream); + stb_vorbis_close(vorbis); + return false; + } + + slot->streaming = true; + slot->streamEof = false; + slot->streamSourceChannels = info.channels; + slot->streamSourceRate = info.sample_rate; + slot->audioStream = stream; + slot->vorbisStream = vorbis; + slot->streamDecodeBuffer = decodeBuffer; + slot->streamDecodeFrames = decodeFrames; + return true; +} + +static bool WiiUAudio_tryOpenMusicStream(WiiUAudioSystem* wiiu, const Sound* sound, WiiUSoundInstance* slot) { + if (!WiiUAudio_shouldStreamSound(sound)) return false; + + const char* candidates[3] = { sound->file, NULL, NULL }; + char oggName[512]; + char wavName[512]; + if (strchr(sound->file, '.') == NULL) { + snprintf(oggName, sizeof(oggName), "%s.ogg", sound->file); + snprintf(wavName, sizeof(wavName), "%s.wav", sound->file); + candidates[1] = oggName; + candidates[2] = wavName; + } + + repeat(3, i) { + const char* candidate = candidates[i]; + if (candidate == NULL) continue; + + char* path = wiiu->fileSystem->vtable->resolvePath(wiiu->fileSystem, candidate); + if (path != NULL) { + bool ok = WiiUAudio_tryOpenVorbisStream(wiiu, path, slot); + free(path); + if (ok) return true; + } + + char contentPath[640]; + snprintf(contentPath, sizeof(contentPath), "/vol/content/%s", candidate); + if (WiiUAudio_tryOpenVorbisStream(wiiu, contentPath, slot)) return true; + + snprintf(contentPath, sizeof(contentPath), "./content/%s", candidate); + if (WiiUAudio_tryOpenVorbisStream(wiiu, contentPath, slot)) return true; + } + + return false; +} + +static void WiiUAudio_fillMusicStream(WiiUSoundInstance* inst, uint32_t neededBytes) { + if (!inst->streaming || inst->audioStream == NULL || inst->vorbisStream == NULL) return; + + while ((uint32_t) SDL_AudioStreamAvailable(inst->audioStream) < neededBytes) { + int decodedFrames = stb_vorbis_get_samples_float_interleaved( + inst->vorbisStream, + inst->streamSourceChannels, + inst->streamDecodeBuffer, + (int) (inst->streamDecodeFrames * (uint32_t) inst->streamSourceChannels) + ); + if (decodedFrames <= 0) { + if (inst->loop) { + stb_vorbis_seek_start(inst->vorbisStream); + continue; + } + inst->streamEof = true; + SDL_AudioStreamFlush(inst->audioStream); + break; + } + + SDL_AudioStreamPut( + inst->audioStream, + inst->streamDecodeBuffer, + decodedFrames * inst->streamSourceChannels * (int) sizeof(float) + ); + } +} + +static uint8_t* WiiUAudio_readFileBinary(const char* path, size_t* outSize) { + FILE* file = fopen(path, "rb"); + if (file == NULL) return NULL; + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size <= 0) { + fclose(file); + return NULL; + } + + uint8_t* data = malloc((size_t) size); + if (data == NULL) { + fclose(file); + return NULL; + } + size_t bytesRead = fread(data, 1, (size_t) size, file); + fclose(file); + if (bytesRead != (size_t) size) { + free(data); + return NULL; + } + + *outSize = (size_t) size; + return data; +} + +static bool WiiUAudio_tryDecodeFile(WiiUAudioSystem* wiiu, const char* path, WiiUDecodedSound* out) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), "wiiu_audio: try path=%s", path); + WiiUAudio_bootLog(buffer); + + size_t encodedSize = 0; + uint8_t* encodedData = WiiUAudio_readFileBinary(path, &encodedSize); + if (encodedData == NULL) return false; + + bool ok = WiiUAudio_decodeSoundData(wiiu, encodedData, encodedSize, out); + free(encodedData); + if (ok) WiiUAudio_bootLog("wiiu_audio: external decode ok"); + return ok; +} + +static bool WiiUAudio_convertPcmS16ToDevice( + WiiUAudioSystem* wiiu, + const int16_t* pcm, + uint32_t frameCount, + int32_t srcChannels, + int32_t srcSampleRate, + WiiUDecodedSound* out +) { + SDL_AudioCVT cvt; + if (SDL_BuildAudioCVT( + &cvt, + AUDIO_S16SYS, + (Uint8) srcChannels, + srcSampleRate, + AUDIO_F32SYS, + (Uint8) wiiu->audioSpec.channels, + wiiu->audioSpec.freq) < 0) { + return false; + } + + int srcBytes = (int) ((size_t) frameCount * srcChannels * sizeof(int16_t)); + cvt.len = srcBytes; + cvt.buf = malloc((size_t) cvt.len * cvt.len_mult); + if (cvt.buf == NULL) { + return false; + } + memcpy(cvt.buf, pcm, (size_t) srcBytes); + + if (SDL_ConvertAudio(&cvt) < 0) { + free(cvt.buf); + return false; + } + + out->samples = malloc((size_t) cvt.len_cvt); + if (out->samples == NULL) { + free(cvt.buf); + return false; + } + memcpy(out->samples, cvt.buf, (size_t) cvt.len_cvt); + free(cvt.buf); + + out->sampleCount = (uint32_t) ((size_t) cvt.len_cvt / sizeof(float)); + out->channels = wiiu->audioSpec.channels; + out->sampleRate = wiiu->audioSpec.freq; + out->loaded = true; + return true; +} + +static bool WiiUAudio_decodeVorbis(WiiUAudioSystem* wiiu, const uint8_t* encodedData, size_t encodedSize, WiiUDecodedSound* out) { + int channels = 0; + int sampleRate = 0; + short* decoded = NULL; + int frameCount = stb_vorbis_decode_memory(encodedData, (int) encodedSize, &channels, &sampleRate, &decoded); + if (frameCount <= 0 || decoded == NULL || channels <= 0 || sampleRate <= 0) { + free(decoded); + return false; + } + + bool ok = WiiUAudio_convertPcmS16ToDevice(wiiu, decoded, (uint32_t) frameCount, channels, sampleRate, out); + free(decoded); + return ok; +} + +static bool WiiUAudio_decodeWav(WiiUAudioSystem* wiiu, const uint8_t* encodedData, size_t encodedSize, WiiUDecodedSound* out) { + if (encodedSize < 44) return false; + if (memcmp(encodedData, "RIFF", 4) != 0 || memcmp(encodedData + 8, "WAVE", 4) != 0) return false; + + uint16_t formatTag = 0; + uint16_t channels = 0; + uint32_t sampleRate = 0; + uint16_t bitsPerSample = 0; + const uint8_t* sampleData = NULL; + uint32_t sampleDataSize = 0; + + size_t offset = 12; + while (offset + 8 <= encodedSize) { + const uint8_t* chunk = encodedData + offset; + uint32_t chunkSize = WiiUAudio_readLe32(chunk + 4); + size_t payloadOffset = offset + 8; + size_t paddedSize = (size_t) chunkSize + ((chunkSize & 1U) ? 1U : 0U); + if (payloadOffset + chunkSize > encodedSize) return false; + + if (memcmp(chunk, "fmt ", 4) == 0) { + if (chunkSize < 16) return false; + formatTag = WiiUAudio_readLe16(encodedData + payloadOffset + 0); + channels = WiiUAudio_readLe16(encodedData + payloadOffset + 2); + sampleRate = WiiUAudio_readLe32(encodedData + payloadOffset + 4); + bitsPerSample = WiiUAudio_readLe16(encodedData + payloadOffset + 14); + } else if (memcmp(chunk, "data", 4) == 0) { + sampleData = encodedData + payloadOffset; + sampleDataSize = chunkSize; + } + + offset = payloadOffset + paddedSize; + } + + if (formatTag != 1 || channels == 0 || sampleRate == 0 || sampleData == NULL || sampleDataSize == 0) { + return false; + } + if (bitsPerSample != 8 && bitsPerSample != 16) { + return false; + } + + uint32_t frameCount = bitsPerSample == 8 + ? sampleDataSize / channels + : sampleDataSize / ((uint32_t) channels * 2U); + if (frameCount == 0) return false; + + int16_t* interleaved = malloc((size_t) frameCount * channels * sizeof(int16_t)); + if (interleaved == NULL) return false; + if (bitsPerSample == 8) { + repeat(frameCount, i) { + repeat(channels, ch) { + uint8_t sample = sampleData[(size_t) i * channels + ch]; + interleaved[(size_t) i * channels + ch] = (int16_t) ((((int32_t) sample) - 128) << 8); + } + } + } else { + repeat(frameCount, i) { + repeat(channels, ch) { + const uint8_t* samplePtr = sampleData + (((size_t) i * channels + ch) * 2U); + interleaved[(size_t) i * channels + ch] = (int16_t) WiiUAudio_readLe16(samplePtr); + } + } + } + + bool ok = WiiUAudio_convertPcmS16ToDevice(wiiu, interleaved, frameCount, channels, sampleRate, out); + free(interleaved); + return ok; +} + +static bool WiiUAudio_decodeSoundData(WiiUAudioSystem* wiiu, const uint8_t* data, size_t dataSize, WiiUDecodedSound* out) { + memset(out, 0, sizeof(*out)); + if (WiiUAudio_decodeWav(wiiu, data, dataSize, out)) return true; + if (WiiUAudio_decodeVorbis(wiiu, data, dataSize, out)) return true; + return false; +} + +static void WiiUAudio_callback(void* userdata, Uint8* stream, int len) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) userdata; + uint32_t sampleCount = (uint32_t) (len / (int) sizeof(float)); + + if (wiiu->mixBufferSamples < sampleCount) { + float* newMixBuffer = realloc(wiiu->mixBuffer, (size_t) sampleCount * sizeof(float)); + if (newMixBuffer == NULL) { + memset(stream, 0, (size_t) len); + return; + } + wiiu->mixBuffer = newMixBuffer; + wiiu->mixBufferSamples = sampleCount; + } + + memset(wiiu->mixBuffer, 0, (size_t) sampleCount * sizeof(float)); + + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (!inst->active || inst->paused) continue; + + float gain = inst->currentGain * inst->sondVolume * wiiu->masterGain; + double step = (double) inst->pitch * (double) inst->sondPitch; + if (step <= 0.0) step = 1.0; + + if (inst->streaming) { + gain = WiiUAudio_clampStreamGain(gain); + uint32_t channelCount = (uint32_t) (wiiu->audioSpec.channels > 0 ? wiiu->audioSpec.channels : 2); + uint32_t outFrames = sampleCount / channelCount; + uint32_t neededFrames = (uint32_t) (outFrames * step) + 4; + if (!WiiUAudio_ensureStreamMixCapacity(inst, inst->streamMixFrames + neededFrames, channelCount)) continue; + + uint32_t wantedFrames = neededFrames; + while (inst->streamMixFrames < wantedFrames) { + uint32_t missingFrames = wantedFrames - inst->streamMixFrames; + uint32_t missingBytes = missingFrames * channelCount * (uint32_t) sizeof(float); + WiiUAudio_fillMusicStream(inst, missingBytes); + int availBytes = SDL_AudioStreamAvailable(inst->audioStream); + if (availBytes <= 0) break; + uint32_t freeFrames = inst->streamMixCapacity - inst->streamMixFrames; + uint32_t pullFrames = (uint32_t) availBytes / (channelCount * (uint32_t) sizeof(float)); + if (pullFrames > freeFrames) pullFrames = freeFrames; + if (pullFrames == 0) break; + int gotBytes = SDL_AudioStreamGet( + inst->audioStream, + inst->streamMixBuffer + ((size_t) inst->streamMixFrames * channelCount), + (int) (pullFrames * channelCount * (uint32_t) sizeof(float)) + ); + if (gotBytes <= 0) break; + inst->streamMixFrames += (uint32_t) gotBytes / (channelCount * (uint32_t) sizeof(float)); + } + + uint32_t mixedFrames = 0; + for (uint32_t outFrame = 0; outFrame < outFrames; outFrame++) { + double position = inst->position; + uint32_t frameIndex = (uint32_t) position; + if (frameIndex >= inst->streamMixFrames) { + if (inst->streamEof && SDL_AudioStreamAvailable(inst->audioStream) == 0) { + WiiUAudio_resetInstance(inst); + } + break; + } + + uint32_t nextFrameIndex = frameIndex + 1; + if (nextFrameIndex >= inst->streamMixFrames) { + nextFrameIndex = frameIndex; + } + + float frac = (float) (position - (double) frameIndex); + uint32_t srcBase0 = frameIndex * channelCount; + uint32_t srcBase1 = nextFrameIndex * channelCount; + uint32_t outBase = outFrame * channelCount; + repeat(channelCount, ch) { + float s0 = inst->streamMixBuffer[srcBase0 + ch]; + float s1 = inst->streamMixBuffer[srcBase1 + ch]; + float sample = s0 + (s1 - s0) * frac; + wiiu->mixBuffer[outBase + ch] += sample * gain; + } + inst->position = position + step; + mixedFrames = outFrame + 1; + } + + uint32_t consumedFrames = (uint32_t) inst->position; + if (consumedFrames > inst->streamMixFrames) consumedFrames = inst->streamMixFrames; + if (consumedFrames > 0) { + uint32_t remainingFrames = inst->streamMixFrames - consumedFrames; + if (remainingFrames > 0) { + memmove( + inst->streamMixBuffer, + inst->streamMixBuffer + ((size_t) consumedFrames * channelCount), + (size_t) remainingFrames * channelCount * sizeof(float) + ); + } + inst->streamMixFrames = remainingFrames; + inst->position -= (double) consumedFrames; + if (inst->position < 0.0) inst->position = 0.0; + } + + (void) mixedFrames; + continue; + } + + if (inst->decoded == NULL || inst->decoded->samples == NULL) continue; + WiiUDecodedSound* decoded = inst->decoded; + uint32_t channelCount = (uint32_t) (decoded->channels > 0 ? decoded->channels : 1); + uint32_t frameCount = decoded->sampleCount / channelCount; + if (frameCount == 0) { + WiiUAudio_resetInstance(inst); + continue; + } + + for (uint32_t outPos = 0; outPos + channelCount <= sampleCount; outPos += channelCount) { + double position = inst->position; + uint32_t frameIndex = (uint32_t) position; + if (frameIndex >= frameCount) { + if (inst->loop) { + inst->position = 0.0; + position = 0.0; + frameIndex = 0; + } else { + WiiUAudio_resetInstance(inst); + break; + } + } + + uint32_t nextFrameIndex = frameIndex + 1; + if (nextFrameIndex >= frameCount) { + nextFrameIndex = inst->loop ? 0 : frameIndex; + } + + float frac = (float) (position - (double) frameIndex); + uint32_t srcBase0 = frameIndex * channelCount; + uint32_t srcBase1 = nextFrameIndex * channelCount; + repeat(channelCount, ch) { + float s0 = decoded->samples[srcBase0 + ch]; + float s1 = decoded->samples[srcBase1 + ch]; + float sample = s0 + (s1 - s0) * frac; + wiiu->mixBuffer[outPos + ch] += sample * gain; + } + inst->position = position + step; + } + } + + float* out = (float*) stream; + repeat(sampleCount, i) { + out[i] = WiiUAudio_clampSample(wiiu->mixBuffer[i]); + } +} + +static bool WiiUAudio_decodeSound(WiiUAudioSystem* wiiu, Sound* sound, WiiUDecodedSound* out) { + bool isEmbedded = (sound->flags & 0x01) != 0; + char buffer[256]; + snprintf( + buffer, + sizeof(buffer), + "wiiu_audio: decodeSound begin embedded=%s flags=0x%08X group=%d audioFile=%d file=%s", + isEmbedded ? "true" : "false", + sound->flags, + sound->audioGroup, + sound->audioFile, + sound->file != NULL ? sound->file : "" + ); + WiiUAudio_bootLog(buffer); + + DataWin* dw = WiiUAudio_mainDataWin(wiiu); + if (dw != NULL && sound->audioFile >= 0 && (uint32_t) sound->audioFile < dw->audo.count) { + AudioEntry* entry = &dw->audo.entries[sound->audioFile]; + snprintf( + buffer, + sizeof(buffer), + "wiiu_audio: audo entry idx=%d off=%u size=%u loaded=%s", + sound->audioFile, + entry->dataOffset, + entry->dataSize, + entry->data != NULL ? "true" : "false" + ); + WiiUAudio_bootLog(buffer); + if (entry->data != NULL && entry->dataSize >= 4) { + snprintf( + buffer, + sizeof(buffer), + "wiiu_audio: audo head %02X %02X %02X %02X", + entry->data[0], + entry->data[1], + entry->data[2], + entry->data[3] + ); + WiiUAudio_bootLog(buffer); + } + } + + if (isEmbedded) { + if (dw == NULL || sound->audioFile < 0 || (uint32_t) sound->audioFile >= dw->audo.count) return false; + AudioEntry* entry = &dw->audo.entries[sound->audioFile]; + if (entry->data == NULL) return false; + if (!WiiUAudio_decodeSoundData(wiiu, entry->data, entry->dataSize, out)) { + WiiUAudio_bootLog("wiiu_audio: embedded decode failed"); + return false; + } + return true; + } + + if (sound->file != NULL && sound->file[0] != '\0') { + const char* candidates[3] = { sound->file, NULL, NULL }; + char oggName[512]; + char wavName[512]; + if (strchr(sound->file, '.') == NULL) { + snprintf(oggName, sizeof(oggName), "%s.ogg", sound->file); + snprintf(wavName, sizeof(wavName), "%s.wav", sound->file); + candidates[1] = oggName; + candidates[2] = wavName; + } + + repeat(3, i) { + const char* candidate = candidates[i]; + if (candidate == NULL) continue; + char* path = wiiu->fileSystem->vtable->resolvePath(wiiu->fileSystem, candidate); + if (path != NULL) { + bool ok = WiiUAudio_tryDecodeFile(wiiu, path, out); + free(path); + if (ok) return true; + } + + char contentPath[640]; + snprintf(contentPath, sizeof(contentPath), "/vol/content/%s", candidate); + if (WiiUAudio_tryDecodeFile(wiiu, contentPath, out)) return true; + + snprintf(contentPath, sizeof(contentPath), "./content/%s", candidate); + if (WiiUAudio_tryDecodeFile(wiiu, contentPath, out)) return true; + } + } + + if (dw != NULL && sound->audioFile >= 0 && (uint32_t) sound->audioFile < dw->audo.count) { + AudioEntry* entry = &dw->audo.entries[sound->audioFile]; + if (entry->data != NULL && entry->dataSize > 0) { + WiiUAudio_bootLog("wiiu_audio: trying AUDO fallback"); + if (WiiUAudio_decodeSoundData(wiiu, entry->data, entry->dataSize, out)) { + WiiUAudio_bootLog("wiiu_audio: AUDO fallback decode ok"); + return true; + } + } + } + + WiiUAudio_bootLog("wiiu_audio: external decode failed"); + return false; +} + +static WiiUSoundInstance* WiiUAudio_findInstanceById(WiiUAudioSystem* wiiu, int32_t instanceId) { + int32_t slotIndex = instanceId - WIIU_SOUND_INSTANCE_ID_BASE; + if (slotIndex < 0 || slotIndex >= MAX_WIIU_SOUND_INSTANCES) return NULL; + WiiUSoundInstance* inst = &wiiu->instances[slotIndex]; + if (!inst->active || inst->instanceId != instanceId) return NULL; + return inst; +} + +static WiiUSoundInstance* WiiUAudio_findFreeSlot(WiiUAudioSystem* wiiu) { + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + if (!wiiu->instances[i].active) return &wiiu->instances[i]; + } + + WiiUSoundInstance* best = NULL; + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (!inst->loop) { + if (best == NULL || best->priority > inst->priority) best = inst; + } + } + + if (best != NULL) { + WiiUAudio_resetInstance(best); + } + return best; +} + +static void WiiUAudioSystem_init(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem) { + WiiUAudio_bootLog("wiiu_audio: init begin"); + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + arrput(wiiu->base.audioGroups, dataWin); + wiiu->fileSystem = fileSystem; + wiiu->masterGain = 1.0f; + wiiu->decodedSounds = safeCalloc(dataWin->sond.count, sizeof(WiiUDecodedSound)); + wiiu->loadedGroups = safeCalloc(dataWin->agrp.count > 0 ? dataWin->agrp.count : 1, sizeof(bool)); + + if ((SDL_WasInit(SDL_INIT_AUDIO) & SDL_INIT_AUDIO) == 0) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), "wiiu_audio: SDL_InitSubSystem failed: %s", SDL_GetError()); + WiiUAudio_bootLog(buffer); + return; + } + } + + SDL_AudioSpec desired; + SDL_zero(desired); + desired.freq = 44100; + desired.format = AUDIO_F32SYS; + desired.channels = 2; + desired.samples = 1024; + desired.callback = WiiUAudio_callback; + desired.userdata = wiiu; + + wiiu->deviceId = SDL_OpenAudioDevice(NULL, 0, &desired, &wiiu->audioSpec, 0); + if (wiiu->deviceId == 0) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), "wiiu_audio: SDL_OpenAudioDevice failed: %s", SDL_GetError()); + WiiUAudio_bootLog(buffer); + return; + } + + SDL_PauseAudioDevice(wiiu->deviceId, 0); + wiiu->initialized = true; + WiiUAudio_bootLog("wiiu_audio: init end"); +} + +static void WiiUAudioSystem_destroy(AudioSystem* audio) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId != 0) { + SDL_PauseAudioDevice(wiiu->deviceId, 1); + SDL_LockAudioDevice(wiiu->deviceId); + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (inst->audioStream != NULL) { + SDL_FreeAudioStream(inst->audioStream); + inst->audioStream = NULL; + } + if (inst->vorbisStream != NULL) { + stb_vorbis_close(inst->vorbisStream); + inst->vorbisStream = NULL; + } + inst->streaming = false; + inst->active = false; + } + SDL_UnlockAudioDevice(wiiu->deviceId); + + SDL_CloseAudioDevice(wiiu->deviceId); + wiiu->deviceId = 0; + } + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUAudio_resetInstance(&wiiu->instances[i]); + } + if (wiiu->decodedSounds != NULL) { + DataWin* dw = WiiUAudio_mainDataWin(wiiu); + if (dw != NULL) { + repeat(dw->sond.count, i) { + free(wiiu->decodedSounds[i].samples); + } + } + free(wiiu->decodedSounds); + } + arrfree(wiiu->base.audioGroups); + free(wiiu->loadedGroups); + free(wiiu->mixBuffer); + free(wiiu->streamScratch); + free(audio); +} + +static void WiiUAudioSystem_update(AudioSystem* audio, float deltaTime) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + + SDL_LockAudioDevice(wiiu->deviceId); + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (!inst->active) continue; + + if (inst->fadeTimeRemaining > 0.0f) { + inst->fadeTimeRemaining -= deltaTime; + if (inst->fadeTimeRemaining <= 0.0f) { + inst->fadeTimeRemaining = 0.0f; + inst->currentGain = inst->targetGain; + } else { + float t = 1.0f - (inst->fadeTimeRemaining / inst->fadeTotalTime); + inst->currentGain = inst->startGain + (inst->targetGain - inst->startGain) * t; + } + } + } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static int32_t WiiUAudioSystem_playSound(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + DataWin* dw = WiiUAudio_mainDataWin(wiiu); + if (dw == NULL) return -1; + char buffer[192]; + snprintf(buffer, sizeof(buffer), "wiiu_audio: playSound begin sound=%d priority=%d loop=%s", soundIndex, priority, loop ? "true" : "false"); + WiiUAudio_bootLog(buffer); + + if (soundIndex < 0 || (uint32_t) soundIndex >= dw->sond.count) return -1; + if (wiiu->deviceId == 0) { + WiiUAudio_bootLog("wiiu_audio: SDL device unavailable"); + return -1; + } + + WiiUSoundInstance* slot = WiiUAudio_findFreeSlot(wiiu); + if (slot == NULL) { + WiiUAudio_bootLog("wiiu_audio: no free slot"); + return -1; + } + + Sound* sound = &dw->sond.sounds[soundIndex]; + + SDL_LockAudioDevice(wiiu->deviceId); + WiiUAudio_resetInstance(slot); + slot->active = true; + slot->loop = loop; + slot->soundIndex = soundIndex; + slot->instanceId = WIIU_SOUND_INSTANCE_ID_BASE + (int32_t) (slot - wiiu->instances); + slot->priority = priority; + slot->position = 0.0; + slot->currentGain = 1.0f; + slot->targetGain = 1.0f; + slot->startGain = 1.0f; + slot->pitch = 1.0f; + slot->sondVolume = sound->volume; + slot->sondPitch = sound->pitch <= 0.0f ? 1.0f : sound->pitch; + + bool streamOk = WiiUAudio_tryOpenMusicStream(wiiu, sound, slot); + if (!streamOk) { + WiiUDecodedSound* decoded = &wiiu->decodedSounds[soundIndex]; + if (!decoded->loaded) { + if (!WiiUAudio_decodeSound(wiiu, sound, decoded)) { + SDL_UnlockAudioDevice(wiiu->deviceId); + WiiUAudio_resetInstance(slot); + WiiUAudio_bootLog("wiiu_audio: resolveDecodedSound failed"); + return -1; + } + } + slot->decoded = decoded; + } + SDL_UnlockAudioDevice(wiiu->deviceId); + + snprintf(buffer, sizeof(buffer), "wiiu_audio: playSound end sound=%d instance=%d", soundIndex, slot->instanceId); + WiiUAudio_bootLog(buffer); + return slot->instanceId; +} + +static void WiiUAudioSystem_stopSound(AudioSystem* audio, int32_t soundOrInstance) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + + SDL_LockAudioDevice(wiiu->deviceId); + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + WiiUSoundInstance* inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + if (inst != NULL) WiiUAudio_resetInstance(inst); + } else { + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) WiiUAudio_resetInstance(inst); + } + } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static void WiiUAudioSystem_stopAll(AudioSystem* audio) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + SDL_LockAudioDevice(wiiu->deviceId); + repeat(MAX_WIIU_SOUND_INSTANCES, i) { WiiUAudio_resetInstance(&wiiu->instances[i]); } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static bool WiiUAudioSystem_isPlaying(AudioSystem* audio, int32_t soundOrInstance) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + WiiUSoundInstance* inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + return inst != NULL && inst->active && !inst->paused; + } + + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (inst->active && !inst->paused && inst->soundIndex == soundOrInstance) return true; + } + return false; +} + +static void WiiUAudioSystem_pauseSound(AudioSystem* audio, int32_t soundOrInstance) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + SDL_LockAudioDevice(wiiu->deviceId); + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + WiiUSoundInstance* inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + if (inst != NULL) inst->paused = true; + } else { + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) inst->paused = true; + } + } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static void WiiUAudioSystem_resumeSound(AudioSystem* audio, int32_t soundOrInstance) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + SDL_LockAudioDevice(wiiu->deviceId); + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + WiiUSoundInstance* inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + if (inst != NULL) inst->paused = false; + } else { + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) inst->paused = false; + } + } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static void WiiUAudioSystem_pauseAll(AudioSystem* audio) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + SDL_LockAudioDevice(wiiu->deviceId); + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + if (wiiu->instances[i].active) wiiu->instances[i].paused = true; + } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static void WiiUAudioSystem_resumeAll(AudioSystem* audio) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + SDL_LockAudioDevice(wiiu->deviceId); + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + if (wiiu->instances[i].active) wiiu->instances[i].paused = false; + } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static void WiiUAudioSystem_setSoundGain(AudioSystem* audio, int32_t soundOrInstance, float gain, uint32_t timeMs) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + SDL_LockAudioDevice(wiiu->deviceId); + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + WiiUSoundInstance* inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + if (inst != NULL) { + if (timeMs == 0) { + inst->currentGain = gain; + inst->targetGain = gain; + inst->fadeTimeRemaining = 0.0f; + } else { + inst->startGain = inst->currentGain; + inst->targetGain = gain; + inst->fadeTotalTime = (float) timeMs / 1000.0f; + inst->fadeTimeRemaining = inst->fadeTotalTime; + } + } + } else { + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (!inst->active || inst->soundIndex != soundOrInstance) continue; + if (timeMs == 0) { + inst->currentGain = gain; + inst->targetGain = gain; + inst->fadeTimeRemaining = 0.0f; + } else { + inst->startGain = inst->currentGain; + inst->targetGain = gain; + inst->fadeTotalTime = (float) timeMs / 1000.0f; + inst->fadeTimeRemaining = inst->fadeTotalTime; + } + } + } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static float WiiUAudioSystem_getSoundGain(AudioSystem* audio, int32_t soundOrInstance) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + WiiUSoundInstance* inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + return inst != NULL ? inst->currentGain : 0.0f; + } + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) return inst->currentGain; + } + return 0.0f; +} + +static void WiiUAudioSystem_setSoundPitch(AudioSystem* audio, int32_t soundOrInstance, float pitch) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + if (pitch <= 0.0f) pitch = 1.0f; + + SDL_LockAudioDevice(wiiu->deviceId); + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + WiiUSoundInstance* inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + if (inst != NULL) inst->pitch = pitch; + SDL_UnlockAudioDevice(wiiu->deviceId); + return; + } + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) inst->pitch = pitch; + } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static float WiiUAudioSystem_getSoundPitch(AudioSystem* audio, int32_t soundOrInstance) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + WiiUSoundInstance* inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + return inst != NULL ? inst->pitch : 1.0f; + } + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + WiiUSoundInstance* inst = &wiiu->instances[i]; + if (inst->active && inst->soundIndex == soundOrInstance) return inst->pitch; + } + return 1.0f; +} + +static float WiiUAudioSystem_getTrackPosition(AudioSystem* audio, int32_t soundOrInstance) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + WiiUSoundInstance* inst = NULL; + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + } else { + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + if (wiiu->instances[i].active && wiiu->instances[i].soundIndex == soundOrInstance) { + inst = &wiiu->instances[i]; + break; + } + } + } + if (inst != NULL && inst->decoded != NULL && inst->decoded->sampleRate > 0 && inst->decoded->channels > 0) { + return (float) inst->position / (float) inst->decoded->sampleRate; + } + return 0.0f; +} + +static void WiiUAudioSystem_setTrackPosition(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + if (wiiu->deviceId == 0) return; + SDL_LockAudioDevice(wiiu->deviceId); + WiiUSoundInstance* inst = NULL; + if (soundOrInstance >= WIIU_SOUND_INSTANCE_ID_BASE) { + inst = WiiUAudio_findInstanceById(wiiu, soundOrInstance); + } else { + repeat(MAX_WIIU_SOUND_INSTANCES, i) { + if (wiiu->instances[i].active && wiiu->instances[i].soundIndex == soundOrInstance) { + inst = &wiiu->instances[i]; + break; + } + } + } + if (inst != NULL && inst->decoded != NULL && inst->decoded->channels > 0) { + uint32_t frame = (uint32_t) (positionSeconds * (float) inst->decoded->sampleRate); + uint32_t frameCount = inst->decoded->sampleCount / (uint32_t) inst->decoded->channels; + inst->position = (double) frame; + if ((uint32_t) inst->position >= frameCount) inst->position = 0.0; + } + SDL_UnlockAudioDevice(wiiu->deviceId); +} + +static void WiiUAudioSystem_setMasterGain(AudioSystem* audio, float gain) { + WiiUAudioSystem* wiiu = (WiiUAudioSystem*) audio; + wiiu->masterGain = gain; +} + +static void WiiUAudioSystem_setChannelCount(AudioSystem* audio, int32_t count) { + (void) audio; + (void) count; +} + +static void WiiUAudioSystem_groupLoad(AudioSystem* audio, int32_t groupIndex) { + (void) audio; + (void) groupIndex; +} + +static bool WiiUAudioSystem_groupIsLoaded(AudioSystem* audio, int32_t groupIndex) { + return arrlen(audio->audioGroups) > groupIndex; +} + +static float WiiUAudioSystem_getSoundLength(AudioSystem* audio, int32_t soundOrInstance) { + (void) audio; + (void) soundOrInstance; + return 0.0f; +} + +static int32_t WiiUAudioSystem_createStream(AudioSystem* audio, const char* filename) { + (void) audio; + (void) filename; + return -1; +} + +static bool WiiUAudioSystem_destroyStream(AudioSystem* audio, int32_t streamIndex) { + (void) audio; + (void) streamIndex; + return false; +} + +static AudioSystemVtable WiiUAudioSystemVtable = { + .init = WiiUAudioSystem_init, + .destroy = WiiUAudioSystem_destroy, + .update = WiiUAudioSystem_update, + .playSound = WiiUAudioSystem_playSound, + .stopSound = WiiUAudioSystem_stopSound, + .stopAll = WiiUAudioSystem_stopAll, + .isPlaying = WiiUAudioSystem_isPlaying, + .pauseSound = WiiUAudioSystem_pauseSound, + .resumeSound = WiiUAudioSystem_resumeSound, + .pauseAll = WiiUAudioSystem_pauseAll, + .resumeAll = WiiUAudioSystem_resumeAll, + .setSoundGain = WiiUAudioSystem_setSoundGain, + .getSoundGain = WiiUAudioSystem_getSoundGain, + .setSoundPitch = WiiUAudioSystem_setSoundPitch, + .getSoundPitch = WiiUAudioSystem_getSoundPitch, + .getTrackPosition = WiiUAudioSystem_getTrackPosition, + .setTrackPosition = WiiUAudioSystem_setTrackPosition, + .getSoundLength = WiiUAudioSystem_getSoundLength, + .setMasterGain = WiiUAudioSystem_setMasterGain, + .setChannelCount = WiiUAudioSystem_setChannelCount, + .groupLoad = WiiUAudioSystem_groupLoad, + .groupIsLoaded = WiiUAudioSystem_groupIsLoaded, + .createStream = WiiUAudioSystem_createStream, + .destroyStream = WiiUAudioSystem_destroyStream, +}; + +WiiUAudioSystem* WiiUAudioSystem_create(void) { + WiiUAudio_bootLog("wiiu_audio: create begin"); + WiiUAudioSystem* audio = safeCalloc(1, sizeof(WiiUAudioSystem)); + audio->base.vtable = &WiiUAudioSystemVtable; + audio->masterGain = 1.0f; + WiiUAudio_bootLog("wiiu_audio: create end"); + return audio; +} diff --git a/src/wiiu/wiiu_audio_system.h b/src/wiiu/wiiu_audio_system.h new file mode 100644 index 00000000..1949bd76 --- /dev/null +++ b/src/wiiu/wiiu_audio_system.h @@ -0,0 +1,68 @@ +#pragma once + +#include "../audio_system.h" + +#include + +typedef struct stb_vorbis stb_vorbis; + +typedef struct { + bool loaded; + float* samples; + uint32_t sampleCount; + int32_t channels; + int32_t sampleRate; +} WiiUDecodedSound; + +typedef struct { + bool active; + bool paused; + bool loop; + int32_t soundIndex; + int32_t instanceId; + int32_t priority; + double position; + float currentGain; + float targetGain; + float startGain; + float fadeTimeRemaining; + float fadeTotalTime; + float pitch; + float sondVolume; + float sondPitch; + WiiUDecodedSound* decoded; + bool streaming; + bool streamEof; + int32_t streamSourceChannels; + int32_t streamSourceRate; + SDL_AudioStream* audioStream; + stb_vorbis* vorbisStream; + float* streamDecodeBuffer; + uint32_t streamDecodeFrames; + float* streamMixBuffer; + uint32_t streamMixFrames; + uint32_t streamMixCapacity; +} WiiUSoundInstance; + +#define MAX_WIIU_SOUND_INSTANCES 64 +#define WIIU_SOUND_INSTANCE_ID_BASE 100000 + +typedef struct { + AudioSystem base; + FileSystem* fileSystem; + float masterGain; + bool initialized; + int32_t nextInstanceCounter; + WiiUDecodedSound* decodedSounds; + SDL_AudioDeviceID deviceId; + SDL_AudioSpec audioSpec; + uint32_t debugUpdateCounter; + bool* loadedGroups; + float* mixBuffer; + uint32_t mixBufferSamples; + float* streamScratch; + uint32_t streamScratchSamples; + WiiUSoundInstance instances[MAX_WIIU_SOUND_INSTANCES]; +} WiiUAudioSystem; + +WiiUAudioSystem* WiiUAudioSystem_create(void); \ No newline at end of file diff --git a/src/wiiu/wiiu_file_system.c b/src/wiiu/wiiu_file_system.c new file mode 100644 index 00000000..7fcd8df5 --- /dev/null +++ b/src/wiiu/wiiu_file_system.c @@ -0,0 +1,123 @@ +#include "wiiu_file_system.h" +#include "../utils.h" + +#include +#include +#include +#include +#include + +__attribute__((weak)) void WiiUFileSystem_platformBootLog(const char* message) { + (void) message; +} + +static void WiiUFileSystem_bootLog(const char* message) { + WiiUFileSystem_platformBootLog(message); +} + +static char* WiiUFileSystem_buildFullPath(WiiUFileSystem* fs, const char* relativePath) { + if (relativePath == NULL) return NULL; + if (strncmp(relativePath, "fs:/", 4) == 0 || strncmp(relativePath, "/vol/", 5) == 0) { + return safeStrdup(relativePath); + } + + size_t baseLen = strlen(fs->basePath); + size_t relLen = strlen(relativePath); + char* fullPath = safeMalloc(baseLen + relLen + 1); + memcpy(fullPath, fs->basePath, baseLen); + memcpy(fullPath + baseLen, relativePath, relLen); + fullPath[baseLen + relLen] = '\0'; + return fullPath; +} + +static char* WiiUFileSystem_resolvePath(FileSystem* fs, const char* relativePath) { + return WiiUFileSystem_buildFullPath((WiiUFileSystem*) fs, relativePath); +} + +static bool WiiUFileSystem_fileExists(FileSystem* fs, const char* relativePath) { + char* fullPath = WiiUFileSystem_buildFullPath((WiiUFileSystem*) fs, relativePath); + struct stat st; + bool exists = stat(fullPath, &st) == 0; + free(fullPath); + return exists; +} + +static char* WiiUFileSystem_readFileText(FileSystem* fs, const char* relativePath) { + char* fullPath = WiiUFileSystem_buildFullPath((WiiUFileSystem*) fs, relativePath); + FILE* file = fopen(fullPath, "rb"); + free(fullPath); + if (file == NULL) return NULL; + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size < 0) { + fclose(file); + return NULL; + } + + char* text = safeMalloc((size_t) size + 1); + size_t bytesRead = fread(text, 1, (size_t) size, file); + text[bytesRead] = '\0'; + fclose(file); + return text; +} + +static bool WiiUFileSystem_writeFileText(FileSystem* fs, const char* relativePath, const char* contents) { + char* fullPath = WiiUFileSystem_buildFullPath((WiiUFileSystem*) fs, relativePath); + FILE* file = fopen(fullPath, "wb"); + free(fullPath); + if (file == NULL) return false; + + size_t length = strlen(contents); + size_t written = fwrite(contents, 1, length, file); + bool ok = written == length; + if (ok) { + ok = fflush(file) == 0; + } + if (ok) { + ok = fsync(fileno(file)) == 0; + } + bool closeOk = fclose(file) == 0; + return ok && closeOk; +} + +static bool WiiUFileSystem_deleteFile(FileSystem* fs, const char* relativePath) { + char* fullPath = WiiUFileSystem_buildFullPath((WiiUFileSystem*) fs, relativePath); + int result = remove(fullPath); + free(fullPath); + return result == 0; +} + +static FileSystemVtable WiiUFileSystemVtable = { + .resolvePath = WiiUFileSystem_resolvePath, + .fileExists = WiiUFileSystem_fileExists, + .readFileText = WiiUFileSystem_readFileText, + .writeFileText = WiiUFileSystem_writeFileText, + .deleteFile = WiiUFileSystem_deleteFile, +}; + +WiiUFileSystem* WiiUFileSystem_create(const char* dataWinPath) { + WiiUFileSystem_bootLog("wiiu_fs: create begin"); + WiiUFileSystem* fs = safeCalloc(1, sizeof(WiiUFileSystem)); + fs->base.vtable = &WiiUFileSystemVtable; + + const char* lastSlash = strrchr(dataWinPath, '/'); + if (lastSlash != NULL) { + size_t dirLen = (size_t) (lastSlash - dataWinPath + 1); + fs->basePath = safeMalloc(dirLen + 1); + memcpy(fs->basePath, dataWinPath, dirLen); + fs->basePath[dirLen] = '\0'; + } else { + fs->basePath = safeStrdup("./"); + } + + WiiUFileSystem_bootLog("wiiu_fs: create end"); + return fs; +} + +void WiiUFileSystem_destroy(WiiUFileSystem* fs) { + if (fs == NULL) return; + free(fs->basePath); + free(fs); +} \ No newline at end of file diff --git a/src/wiiu/wiiu_file_system.h b/src/wiiu/wiiu_file_system.h new file mode 100644 index 00000000..c1bcd3da --- /dev/null +++ b/src/wiiu/wiiu_file_system.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../file_system.h" + +typedef struct { + FileSystem base; + char* basePath; +} WiiUFileSystem; + +WiiUFileSystem* WiiUFileSystem_create(const char* dataWinPath); +void WiiUFileSystem_destroy(WiiUFileSystem* fs); \ No newline at end of file diff --git a/src/wiiu/wiiu_renderer.c b/src/wiiu/wiiu_renderer.c new file mode 100644 index 00000000..0153db81 --- /dev/null +++ b/src/wiiu/wiiu_renderer.c @@ -0,0 +1,1513 @@ +#include "wiiu_renderer.h" + +#include "textured_quad_gsh.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../matrix_math.h" +#include +#include "../text_utils.h" +#include "../utils.h" + +#define WIIU_MAX_QUADS 4096 +#define WIIU_VERTICES_PER_QUAD 6 + +// Render to the full Wii U scan-buffer width and center the game's 4:3 image inside it. +#define WIIU_RENDER_WIDTH 854 +#define WIIU_RENDER_HEIGHT 480 + +__attribute__((weak)) void WiiURenderer_platformBootLog(const char* message) { + (void) message; +} + +static void WiiURenderer_bootLog(const char* message) { + WiiURenderer_platformBootLog(message); +} + +static void WiiURenderer_releaseTextureBlob(DataWin* dataWin, uint32_t index); +static void WiiURenderer_emitVertex(WiiURenderer* renderer, WiiUVec2 clip, float u, float v, uint32_t color, float alpha); +static void WiiURenderer_computeOutputLayout(WiiUPresentLayout* layout, uint32_t outputWidth, uint32_t outputHeight, uint32_t sourceWidth, uint32_t sourceHeight, bool preferIntegerScale); +static bool WiiURenderer_initLinearTexture(GX2Texture* texture, uint32_t width, uint32_t height); +static void WiiURenderer_destroyRenderTarget(WiiURenderTarget* target); +static bool WiiURenderer_initRenderTarget(WiiURenderTarget* target, uint32_t width, uint32_t height); +static void WiiURenderer_ensureSceneTarget(WiiURenderer* renderer, uint32_t width, uint32_t height); +static void WiiURenderer_computeIntegerBlitRect(uint32_t sourceWidth, uint32_t sourceHeight, uint32_t targetWidth, uint32_t targetHeight, float* left, float* top, float* right, float* bottom); +static bool WiiURenderer_ensureVertexCapacity(WiiURenderer* renderer, uint32_t neededVertices); +static bool WiiURenderer_mapVertexBuffer(WiiURenderer* renderer); + +static void* WiiURenderer_gpuAlloc(size_t alignment, size_t size) { + return MEMAllocFromDefaultHeapEx((uint32_t) size, (int32_t) alignment); +} + +static void WiiURenderer_gpuFree(void* ptr) { + if (ptr != NULL) { + MEMFreeToDefaultHeap(ptr); + } +} + +static double WiiURenderer_elapsedMs(OSTime start, OSTime end) { + return (double) OSTicksToMicroseconds(end - start) / 1000.0; +} + +static float WiiURenderer_snapPixel(float value) { + return floorf(value + 0.5f); +} + +static void WiiURenderer_updatePresentLayout(WiiURenderer* renderer) { + uint32_t srcW = (renderer->frameWidth > 0) ? (uint32_t) renderer->frameWidth : 640u; + uint32_t srcH = (renderer->frameHeight > 0) ? (uint32_t) renderer->frameHeight : 480u; + WiiURenderer_computeOutputLayout( + &renderer->presentLayout, + WIIU_RENDER_WIDTH, + WIIU_RENDER_HEIGHT, + srcW, + srcH, + false + ); +} + +static void WiiURenderer_computeOutputLayout( + WiiUPresentLayout* layout, + uint32_t outputWidth, + uint32_t outputHeight, + uint32_t sourceWidth, + uint32_t sourceHeight, + bool preferIntegerScale +) { + if (layout == NULL || sourceWidth == 0 || sourceHeight == 0 || outputWidth == 0 || outputHeight == 0) { + return; + } + + float fitScale = fminf( + (float) outputWidth / (float) sourceWidth, + (float) outputHeight / (float) sourceHeight + ); + float scale = fitScale; + + if (preferIntegerScale) { + float integerScale = floorf(fitScale); + if (integerScale >= 2.0f) { + scale = integerScale; + } + } + + float targetWidth = (float) sourceWidth * scale; + float targetHeight = (float) sourceHeight * scale; + if (targetWidth > (float) outputWidth) targetWidth = (float) outputWidth; + if (targetHeight > (float) outputHeight) targetHeight = (float) outputHeight; + + uint32_t snappedWidth = (uint32_t) lroundf(targetWidth); + uint32_t snappedHeight = (uint32_t) lroundf(targetHeight); + if (snappedWidth > outputWidth) snappedWidth = outputWidth; + if (snappedHeight > outputHeight) snappedHeight = outputHeight; + + layout->targetWidth = (float) snappedWidth; + layout->targetHeight = (float) snappedHeight; + layout->xOffset = (outputWidth - snappedWidth) / 2u; + layout->yOffset = (outputHeight - snappedHeight) / 2u; +} + +void WiiURenderer_refreshOutputState(WiiURenderer* renderer) { + if (renderer == NULL) return; + + WiiURenderer_updatePresentLayout(renderer); +} + + + +static void WiiURenderer_destroyTexturePage(WiiUTexturePage* page) { + if (page->texture.surface.image != NULL) { + WiiURenderer_gpuFree(page->texture.surface.image); + } + memset(page, 0, sizeof(*page)); +} + +static void WiiURenderer_destroyRenderTarget(WiiURenderTarget* target) { + if (target->colorBuffer.surface.image != NULL) { + WiiURenderer_gpuFree(target->colorBuffer.surface.image); + } + memset(target, 0, sizeof(*target)); +} + +static bool WiiURenderer_initRenderTarget(WiiURenderTarget* target, uint32_t width, uint32_t height) { + memset(target, 0, sizeof(*target)); + + target->colorBuffer.surface.dim = GX2_SURFACE_DIM_TEXTURE_2D; + target->colorBuffer.surface.width = width; + target->colorBuffer.surface.height = height; + target->colorBuffer.surface.depth = 1; + target->colorBuffer.surface.mipLevels = 1; + target->colorBuffer.surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8; + target->colorBuffer.surface.aa = GX2_AA_MODE1X; + target->colorBuffer.surface.use = GX2_SURFACE_USE_COLOR_BUFFER; + target->colorBuffer.surface.tileMode = GX2_TILE_MODE_DEFAULT; + GX2CalcSurfaceSizeAndAlignment(&target->colorBuffer.surface); + target->colorBuffer.surface.image = WiiURenderer_gpuAlloc(target->colorBuffer.surface.alignment, target->colorBuffer.surface.imageSize); + if (target->colorBuffer.surface.image == NULL) { + WiiURenderer_destroyRenderTarget(target); + return false; + } + memset(target->colorBuffer.surface.image, 0, target->colorBuffer.surface.imageSize); + + target->colorBuffer.viewMip = 0; + target->colorBuffer.viewFirstSlice = 0; + target->colorBuffer.viewNumSlices = 1; + target->colorBuffer.aaBuffer = NULL; + target->colorBuffer.aaSize = 0; + GX2InitColorBufferRegs(&target->colorBuffer); + GX2Invalidate( + GX2_INVALIDATE_MODE_CPU | GX2_INVALIDATE_MODE_COLOR_BUFFER, + target->colorBuffer.surface.image, + target->colorBuffer.surface.imageSize + ); + + memset(&target->presentTexture, 0, sizeof(target->presentTexture)); + target->presentTexture.surface = target->colorBuffer.surface; + target->presentTexture.viewFirstMip = 0; + target->presentTexture.viewNumMips = 1; + target->presentTexture.viewFirstSlice = 0; + target->presentTexture.viewNumSlices = 1; + target->presentTexture.compMap = GX2_COMP_MAP(GX2_SQ_SEL_R, GX2_SQ_SEL_G, GX2_SQ_SEL_B, GX2_SQ_SEL_A); + GX2InitTextureRegs(&target->presentTexture); + + target->ready = true; + return true; +} + +static void WiiURenderer_ensureSceneTarget(WiiURenderer* renderer, uint32_t width, uint32_t height) { + if (width == 0 || height == 0) return; + + if (renderer->sceneTarget.ready && + renderer->sceneTarget.colorBuffer.surface.width == width && + renderer->sceneTarget.colorBuffer.surface.height == height) { + return; + } + + WiiURenderer_destroyRenderTarget(&renderer->sceneTarget); + if (!WiiURenderer_initRenderTarget(&renderer->sceneTarget, width, height)) { + WiiURenderer_bootLog("wiiu_renderer: scene target allocation failed"); + } +} + +static void WiiURenderer_computeIntegerBlitRect( + uint32_t sourceWidth, + uint32_t sourceHeight, + uint32_t targetWidth, + uint32_t targetHeight, + float* left, + float* top, + float* right, + float* bottom +) { + uint32_t scaleX = targetWidth / sourceWidth; + uint32_t scaleY = targetHeight / sourceHeight; + uint32_t scale = scaleX < scaleY ? scaleX : scaleY; + if (scale < 1u) scale = 1u; + + uint32_t rectW = sourceWidth * scale; + uint32_t rectH = sourceHeight * scale; + uint32_t offX = (targetWidth - rectW) / 2u; + uint32_t offY = (targetHeight - rectH) / 2u; + + *left = ((float) offX / (float) targetWidth) * 2.0f - 1.0f; + *right = ((float) (offX + rectW) / (float) targetWidth) * 2.0f - 1.0f; + *top = 1.0f - ((float) offY / (float) targetHeight) * 2.0f; + *bottom = 1.0f - ((float) (offY + rectH) / (float) targetHeight) * 2.0f; +} + + +static bool WiiURenderer_initLinearTexture(GX2Texture* texture, uint32_t width, uint32_t height) { + memset(texture, 0, sizeof(*texture)); + texture->surface.dim = GX2_SURFACE_DIM_TEXTURE_2D; + texture->surface.width = width; + texture->surface.height = height; + texture->surface.depth = 1; + texture->surface.mipLevels = 1; + texture->surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8; + texture->surface.aa = GX2_AA_MODE1X; + texture->surface.use = GX2_SURFACE_USE_TEXTURE; + texture->surface.tileMode = GX2_TILE_MODE_LINEAR_ALIGNED; + GX2CalcSurfaceSizeAndAlignment(&texture->surface); + texture->surface.image = WiiURenderer_gpuAlloc(texture->surface.alignment, texture->surface.imageSize); + if (texture->surface.image == NULL) { + memset(texture, 0, sizeof(*texture)); + return false; + } + memset(texture->surface.image, 0, texture->surface.imageSize); + texture->viewFirstMip = 0; + texture->viewNumMips = 1; + texture->viewFirstSlice = 0; + texture->viewNumSlices = 1; + texture->compMap = GX2_COMP_MAP(GX2_SQ_SEL_R, GX2_SQ_SEL_G, GX2_SQ_SEL_B, GX2_SQ_SEL_A); + GX2InitTextureRegs(texture); + return true; +} + +static bool WiiURenderer_uploadTexturePage(WiiUTexturePage* page, const uint8_t* pixels, uint32_t width, uint32_t height) { + WiiURenderer_destroyTexturePage(page); + if (pixels == NULL || width == 0 || height == 0) return false; + if (!WiiURenderer_initLinearTexture(&page->texture, width, height)) return false; + + uint8_t* dst = (uint8_t*) page->texture.surface.image; + if (dst == NULL) { + WiiURenderer_destroyTexturePage(page); + return false; + } + memset(dst, 0, page->texture.surface.imageSize); + uint32_t dstPitchBytes = page->texture.surface.pitch * 4u; + for (uint32_t y = 0; y < height; ++y) { + memcpy( + dst + (size_t) y * (size_t) dstPitchBytes, + pixels + (size_t) y * (size_t) width * 4u, + (size_t) width * 4u + ); + } + page->ready = true; + return true; +} + +static void WiiURenderer_loadTexturePages(WiiURenderer* renderer, DataWin* dataWin) { + renderer->texturePageCount = dataWin->txtr.count; + if (renderer->texturePageCount > 0) { + renderer->texturePages = calloc(renderer->texturePageCount, sizeof(WiiUTexturePage)); + if (renderer->texturePages == NULL) { + renderer->texturePageCount = 0; + WiiURenderer_bootLog("wiiu_renderer: texture page table allocation failed"); + return; + } + } + + repeat(renderer->texturePageCount, i) { + Texture* tex = &dataWin->txtr.textures[i]; + if (tex->blobSize == 0) continue; + if (tex->blobSize > 50u * 1024u * 1024u) continue; + + uint8_t* pngData = tex->blobData; + if (pngData == NULL) continue; + + int width = 0, height = 0, channels = 0; + uint8_t* pixels = stbi_load_from_memory(pngData, (int) tex->blobSize, + &width, &height, &channels, 4); + + if (pixels == NULL) continue; + + if (width > 0 && height > 0) { + WiiURenderer_uploadTexturePage(&renderer->texturePages[i], pixels, + (uint32_t) width, (uint32_t) height); + } + stbi_image_free(pixels); + } + + repeat(renderer->texturePageCount, i) { + if (!renderer->texturePages[i].ready) continue; + GX2Invalidate(GX2_INVALIDATE_MODE_CPU_TEXTURE, + renderer->texturePages[i].texture.surface.image, + renderer->texturePages[i].texture.surface.imageSize); + } + + { + const uint8_t whitePixel[4] = { 255, 255, 255, 255 }; + WiiURenderer_uploadTexturePage(&renderer->whiteTexture, whitePixel, 1, 1); + GX2Invalidate(GX2_INVALIDATE_MODE_CPU_TEXTURE, + renderer->whiteTexture.texture.surface.image, + renderer->whiteTexture.texture.surface.imageSize); + } +} + +static void WiiURenderer_freeTexturePages(WiiURenderer* renderer) { + repeat(renderer->texturePageCount, i) { + WiiURenderer_destroyTexturePage(&renderer->texturePages[i]); + } + free(renderer->texturePages); + renderer->texturePages = NULL; + renderer->texturePageCount = 0; + WiiURenderer_destroyTexturePage(&renderer->whiteTexture); +} + +static void WiiURenderer_destroyVertexBuffer(WiiURenderer* renderer) { + if (renderer->batchVertices != NULL && GX2RBufferExists(&renderer->batchVertexBuffer)) { + GX2RUnlockBufferEx(&renderer->batchVertexBuffer, 0); + } + if (GX2RBufferExists(&renderer->batchVertexBuffer)) { + GX2RDestroyBufferEx(&renderer->batchVertexBuffer, 0); + } + renderer->batchVertices = NULL; + renderer->batchVertexCapacity = 0; +} + +static bool WiiURenderer_ensureVertexCapacity(WiiURenderer* renderer, uint32_t neededVertices) { + uint32_t needed = renderer->batchVertexCount + neededVertices; + if (needed <= renderer->batchVertexCapacity) return true; + + uint32_t newCapacity = renderer->batchVertexCapacity == 0 ? WIIU_MAX_QUADS * WIIU_VERTICES_PER_QUAD : renderer->batchVertexCapacity; + while (newCapacity < needed) { + newCapacity *= 2; + } + + GX2RBuffer newVertexBuffer = { 0 }; + newVertexBuffer.flags = + GX2R_RESOURCE_BIND_VERTEX_BUFFER | + GX2R_RESOURCE_USAGE_CPU_WRITE | + GX2R_RESOURCE_USAGE_GPU_READ | + GX2R_RESOURCE_USAGE_FORCE_MEM2; + newVertexBuffer.elemSize = sizeof(WiiUBatchVertex); + newVertexBuffer.elemCount = newCapacity; + if (!GX2RCreateBuffer(&newVertexBuffer)) { + WiiURenderer_bootLog("wiiu_renderer: failed to create GX2R vertex buffer"); + return false; + } + + WiiUBatchVertex* newVertices = (WiiUBatchVertex*) GX2RLockBufferEx(&newVertexBuffer, 0); + if (newVertices == NULL) { + WiiURenderer_bootLog("wiiu_renderer: failed to lock new GX2R vertex buffer"); + GX2RDestroyBufferEx(&newVertexBuffer, 0); + return false; + } + + if (renderer->batchVertices != NULL && renderer->batchVertexCount > 0) { + memcpy(newVertices, renderer->batchVertices, (size_t) renderer->batchVertexCount * sizeof(WiiUBatchVertex)); + } + + WiiURenderer_destroyVertexBuffer(renderer); + renderer->batchVertexBuffer = newVertexBuffer; + renderer->batchVertices = newVertices; + renderer->batchVertexCapacity = newCapacity; + return true; +} + +static bool WiiURenderer_mapVertexBuffer(WiiURenderer* renderer) { + if (renderer->batchVertices == NULL && GX2RBufferExists(&renderer->batchVertexBuffer)) { + renderer->batchVertices = (WiiUBatchVertex*) GX2RLockBufferEx(&renderer->batchVertexBuffer, 0); + if (renderer->batchVertices == NULL) { + WiiURenderer_bootLog("wiiu_renderer: failed to lock GX2R vertex buffer"); + return false; + } + } + return renderer->batchVertices != NULL; +} + +static void WiiURenderer_releaseTextureBlob(DataWin* dataWin, uint32_t index) { + if (dataWin == NULL || index >= dataWin->txtr.count) return; + + if (dataWin->txtr.textures[index].blobData != NULL) { + free(dataWin->txtr.textures[index].blobData); + dataWin->txtr.textures[index].blobData = NULL; + } +} + +static void WiiURenderer_pushCommand(WiiURenderer* renderer, const WiiUQuadCommand* command) { + if (renderer->commandCount >= renderer->commandCapacity) { + uint32_t newCapacity = renderer->commandCapacity == 0 ? 1024 : renderer->commandCapacity * 2; + if (newCapacity > 8192) newCapacity = 8192; + if (renderer->commandCount >= newCapacity) return; // drop rather than crash + WiiUQuadCommand* newCommands = realloc(renderer->commands, (size_t) newCapacity * sizeof(WiiUQuadCommand)); + if (newCommands == NULL) { + WiiURenderer_bootLog("wiiu_renderer: command buffer growth failed"); + return; + } + renderer->commands = newCommands; + renderer->commandCapacity = newCapacity; + } + renderer->commands[renderer->commandCount++] = *command; +} + +static void WiiURenderer_bindShader(WiiURenderer* renderer) { + GX2SetShaderMode(GX2_SHADER_MODE_UNIFORM_BLOCK); + GX2SetShaderModeEx( + renderer->shaderGroup.vertexShader->mode, + GX2GetVertexShaderGPRs(renderer->shaderGroup.vertexShader), + GX2GetVertexShaderStackEntries(renderer->shaderGroup.vertexShader), + 0, + 0, + GX2GetPixelShaderGPRs(renderer->shaderGroup.pixelShader), + GX2GetPixelShaderStackEntries(renderer->shaderGroup.pixelShader) + ); + GX2SetFetchShader(&renderer->shaderGroup.fetchShader); + GX2SetVertexShader(renderer->shaderGroup.vertexShader); + GX2SetPixelShader(renderer->shaderGroup.pixelShader); + GX2SetPixelSampler(&renderer->sampler, renderer->textureUnit); + GX2SetStreamOutEnable(FALSE); +} + +static void WiiURenderer_setCommonState(bool blendEnabled) { + GX2SetColorControl(GX2_LOGIC_OP_COPY, blendEnabled ? 0x1 : 0x0, FALSE, TRUE); + GX2SetBlendControl( + GX2_RENDER_TARGET_0, + GX2_BLEND_MODE_SRC_ALPHA, + GX2_BLEND_MODE_INV_SRC_ALPHA, + GX2_BLEND_COMBINE_MODE_ADD, + blendEnabled ? GX2_ENABLE : GX2_DISABLE, + GX2_BLEND_MODE_ONE, + GX2_BLEND_MODE_INV_SRC_ALPHA, + GX2_BLEND_COMBINE_MODE_ADD + ); + GX2SetTargetChannelMasks(GX2_CHANNEL_MASK_RGBA, 0, 0, 0, 0, 0, 0, 0); + GX2SetDepthOnlyControl(FALSE, FALSE, GX2_COMPARE_FUNC_ALWAYS); + GX2SetCullOnlyControl(GX2_FRONT_FACE_CCW, FALSE, FALSE); + GX2SetAlphaTest(TRUE, GX2_COMPARE_FUNC_GREATER, 0.0f); +} + +static void WiiURenderer_emitClipVertex( + WiiURenderer* renderer, + float clipX, + float clipY, + float u, + float v +) { + uint32_t index = renderer->batchVertexCount++; + WiiUBatchVertex* vertex = &renderer->batchVertices[index]; + + vertex->x = clipX; + vertex->y = clipY; + vertex->z = 0.0f; + vertex->w = 1.0f; + vertex->u = u; + vertex->v = v; + vertex->r = 1.0f; + vertex->g = 1.0f; + vertex->b = 1.0f; + vertex->a = 1.0f; +} + +static void WiiURenderer_emitSolidClipVertex( + WiiURenderer* renderer, + float clipX, + float clipY, + uint32_t color, + float alpha +) { + WiiURenderer_emitVertex( + renderer, + (WiiUVec2) { clipX, clipY }, + 0.0f, + 0.0f, + color, + alpha + ); +} + +static void WiiURenderer_flushVerticesWithTexture(WiiURenderer* renderer, uint32_t vertexCount, GX2Texture* texture) { + if (vertexCount == 0 || texture == NULL) return; + + GX2SetPixelTexture(texture, renderer->textureUnit); + + if (renderer->batchVertices != NULL) { + GX2RUnlockBufferEx(&renderer->batchVertexBuffer, 0); + renderer->batchVertices = NULL; + } + GX2RInvalidateBuffer(&renderer->batchVertexBuffer, 0); + GX2RSetAttributeBuffer(&renderer->batchVertexBuffer, 0, sizeof(WiiUBatchVertex), offsetof(WiiUBatchVertex, x)); + GX2RSetAttributeBuffer(&renderer->batchVertexBuffer, 1, sizeof(WiiUBatchVertex), offsetof(WiiUBatchVertex, u)); + GX2RSetAttributeBuffer(&renderer->batchVertexBuffer, 2, sizeof(WiiUBatchVertex), offsetof(WiiUBatchVertex, r)); + GX2DrawEx(GX2_PRIMITIVE_MODE_TRIANGLES, vertexCount, 0, 1); + GX2DrawDone(); + + renderer->perfFlushCount++; +} + +static void WiiURenderer_flushVertices(WiiURenderer* renderer, uint32_t vertexCount, int32_t texturePageId) { + if (vertexCount == 0 || texturePageId < -1) return; + + GX2Texture* texture = texturePageId < 0 + ? &renderer->whiteTexture.texture + : &renderer->texturePages[texturePageId].texture; + WiiURenderer_flushVerticesWithTexture(renderer, vertexCount, texture); +} + +static void WiiURenderer_renderLetterboxMasksToTarget( + WiiURenderer* renderer, + uint32_t targetWidth, + uint32_t targetHeight, + const WiiUPresentLayout* layout +) { + uint32_t presentWidth = (uint32_t) lroundf(layout->targetWidth); + uint32_t presentHeight = (uint32_t) lroundf(layout->targetHeight); + if (presentWidth >= targetWidth && presentHeight >= targetHeight) return; + + float left = ((float) layout->xOffset / (float) targetWidth) * 2.0f - 1.0f; + float right = ((float) (layout->xOffset + presentWidth) / (float) targetWidth) * 2.0f - 1.0f; + WiiURenderer_bindShader(renderer); + WiiURenderer_setCommonState(false); + GX2SetViewport(0.0f, 0.0f, (float) targetWidth, (float) targetHeight, 0.0f, 1.0f); + GX2SetScissor(0, 0, targetWidth, targetHeight); + if (!WiiURenderer_ensureVertexCapacity(renderer, 12)) return; + if (!WiiURenderer_mapVertexBuffer(renderer)) return; + renderer->batchVertexCount = 0; + + #define WIIU_EMIT_MASK_QUAD(x0, y0, x1, y1) \ + do { \ + WiiURenderer_emitSolidClipVertex(renderer, (x0), (y0), 0x000000, 1.0f); \ + WiiURenderer_emitSolidClipVertex(renderer, (x1), (y0), 0x000000, 1.0f); \ + WiiURenderer_emitSolidClipVertex(renderer, (x0), (y1), 0x000000, 1.0f); \ + WiiURenderer_emitSolidClipVertex(renderer, (x0), (y1), 0x000000, 1.0f); \ + WiiURenderer_emitSolidClipVertex(renderer, (x1), (y0), 0x000000, 1.0f); \ + WiiURenderer_emitSolidClipVertex(renderer, (x1), (y1), 0x000000, 1.0f); \ + } while (0) + + if (left > -1.0f) { + WIIU_EMIT_MASK_QUAD(-1.0f, 1.0f, left, -1.0f); + } + if (right < 1.0f) { + WIIU_EMIT_MASK_QUAD(right, 1.0f, 1.0f, -1.0f); + } + + #undef WIIU_EMIT_MASK_QUAD + + if (renderer->batchVertexCount > 0) { + WiiURenderer_flushVertices(renderer, renderer->batchVertexCount, -1); + renderer->batchVertexCount = 0; + } +} + +static WiiUVec2 WiiURenderer_worldToGame(WiiURenderer* renderer, float worldX, float worldY) { + WiiUVec2 result; + result.x = (float) renderer->portX + (worldX - (float) renderer->viewX) * renderer->viewScaleX; + result.y = (float) renderer->portY + (worldY - (float) renderer->viewY) * renderer->viewScaleY; + return result; +} + +static WiiUVec2 WiiURenderer_gameToClip( + const WiiURenderer* renderer, + WiiUVec2 point, + uint32_t targetWidth, + uint32_t targetHeight, + const WiiUPresentLayout* layout +) { + WiiUVec2 result; + float targetX = (float) layout->xOffset + + ((point.x / (float) renderer->frameWidth) * layout->targetWidth); + float targetY = (float) layout->yOffset + + ((point.y / (float) renderer->frameHeight) * layout->targetHeight); + result.x = (targetX / (float) targetWidth) * 2.0f - 1.0f; + result.y = 1.0f - (targetY / (float) targetHeight) * 2.0f; + return result; +} + +static void WiiURenderer_emitVertex(WiiURenderer* renderer, WiiUVec2 clip, float u, float v, uint32_t color, float alpha) { + uint32_t index = renderer->batchVertexCount++; + WiiUBatchVertex* vertex = &renderer->batchVertices[index]; + + vertex->x = clip.x; + vertex->y = clip.y; + vertex->z = 0.0f; + vertex->w = 1.0f; + vertex->u = u; + vertex->v = v; + vertex->r = (float) BGR_R(color) / 255.0f; + vertex->g = (float) BGR_G(color) / 255.0f; + vertex->b = (float) BGR_B(color) / 255.0f; + vertex->a = alpha; +} + +static bool WiiURenderer_quadIntersectsView( + const WiiURenderer* renderer, + WiiUVec2 p00, + WiiUVec2 p10, + WiiUVec2 p01 +) { + WiiUVec2 p11 = { + p10.x + (p01.x - p00.x), + p10.y + (p01.y - p00.y) + }; + + float minX = fminf(fminf(p00.x, p10.x), fminf(p01.x, p11.x)); + float maxX = fmaxf(fmaxf(p00.x, p10.x), fmaxf(p01.x, p11.x)); + float minY = fminf(fminf(p00.y, p10.y), fminf(p01.y, p11.y)); + float maxY = fmaxf(fmaxf(p00.y, p10.y), fmaxf(p01.y, p11.y)); + + float viewMinX = (float) renderer->portX; + float viewMinY = (float) renderer->portY; + float viewMaxX = viewMinX + (float) renderer->portW; + float viewMaxY = viewMinY + (float) renderer->portH; + + return maxX >= viewMinX && minX <= viewMaxX && maxY >= viewMinY && minY <= viewMaxY; +} + +static void WiiURenderer_appendQuad( + WiiURenderer* renderer, + int32_t texturePageId, + WiiUVec2 p00, + WiiUVec2 p10, + WiiUVec2 p01, + float u0, + float v0, + float u1, + float v1, + uint32_t color, + float alpha +) { + if (renderer->frameWidth <= 0 || renderer->frameHeight <= 0) return; + if (!WiiURenderer_quadIntersectsView(renderer, p00, p10, p01)) return; + + WiiUQuadCommand command; + memset(&command, 0, sizeof(command)); + command.texturePageId = texturePageId; + command.gradient = false; + command.p00 = p00; + command.p10 = p10; + command.p01 = p01; + command.u0 = u0; + command.v0 = v0; + command.u1 = u1; + command.v1 = v1; + command.color0 = color; + command.color1 = color; + command.alpha = alpha; + WiiURenderer_pushCommand(renderer, &command); + renderer->queuedQuadCount++; +} + +static void WiiURenderer_appendGradientQuad( + WiiURenderer* renderer, + int32_t texturePageId, + WiiUVec2 p00, + WiiUVec2 p10, + WiiUVec2 p01, + float u0, + float v0, + float u1, + float v1, + uint32_t color0, + uint32_t color1, + float alpha +) { + if (renderer->frameWidth <= 0 || renderer->frameHeight <= 0) return; + if (!WiiURenderer_quadIntersectsView(renderer, p00, p10, p01)) return; + + WiiUQuadCommand command; + memset(&command, 0, sizeof(command)); + command.texturePageId = texturePageId; + command.gradient = true; + command.p00 = p00; + command.p10 = p10; + command.p01 = p01; + command.u0 = u0; + command.v0 = v0; + command.u1 = u1; + command.v1 = v1; + command.color0 = color0; + command.color1 = color1; + command.alpha = alpha; + WiiURenderer_pushCommand(renderer, &command); + renderer->queuedQuadCount++; +} + +static void WiiURenderer_buildQuadVertices( + WiiURenderer* renderer, + const WiiUQuadCommand* command, + uint32_t targetWidth, + uint32_t targetHeight, + const WiiUPresentLayout* layout +) { + if (!WiiURenderer_ensureVertexCapacity(renderer, WIIU_VERTICES_PER_QUAD)) return; + if (!WiiURenderer_mapVertexBuffer(renderer)) return; + WiiUVec2 c00 = WiiURenderer_gameToClip(renderer, command->p00, targetWidth, targetHeight, layout); + WiiUVec2 c10 = WiiURenderer_gameToClip(renderer, command->p10, targetWidth, targetHeight, layout); + WiiUVec2 c01 = WiiURenderer_gameToClip(renderer, command->p01, targetWidth, targetHeight, layout); + WiiUVec2 p11 = { command->p10.x + (command->p01.x - command->p00.x), command->p10.y + (command->p01.y - command->p00.y) }; + WiiUVec2 c11 = WiiURenderer_gameToClip(renderer, p11, targetWidth, targetHeight, layout); + + if (command->gradient) { + WiiURenderer_emitVertex(renderer, c00, command->u0, command->v0, command->color0, command->alpha); + WiiURenderer_emitVertex(renderer, c10, command->u1, command->v0, command->color1, command->alpha); + WiiURenderer_emitVertex(renderer, c01, command->u0, command->v1, command->color0, command->alpha); + WiiURenderer_emitVertex(renderer, c01, command->u0, command->v1, command->color0, command->alpha); + WiiURenderer_emitVertex(renderer, c10, command->u1, command->v0, command->color1, command->alpha); + WiiURenderer_emitVertex(renderer, c11, command->u1, command->v1, command->color1, command->alpha); + } else { + WiiURenderer_emitVertex(renderer, c00, command->u0, command->v0, command->color0, command->alpha); + WiiURenderer_emitVertex(renderer, c10, command->u1, command->v0, command->color0, command->alpha); + WiiURenderer_emitVertex(renderer, c01, command->u0, command->v1, command->color0, command->alpha); + WiiURenderer_emitVertex(renderer, c01, command->u0, command->v1, command->color0, command->alpha); + WiiURenderer_emitVertex(renderer, c10, command->u1, command->v0, command->color0, command->alpha); + WiiURenderer_emitVertex(renderer, c11, command->u1, command->v1, command->color0, command->alpha); + } +} + +static void WiiURenderer_renderCommandsToTarget( + WiiURenderer* renderer, + uint32_t targetWidth, + uint32_t targetHeight, + const WiiUPresentLayout* layout +) { + uint32_t presentWidth = (uint32_t) lroundf(layout->targetWidth); + uint32_t presentHeight = (uint32_t) lroundf(layout->targetHeight); + + GX2SetViewport(0.0f, 0.0f, (float) targetWidth, (float) targetHeight, 0.0f, 1.0f); + GX2SetScissor( + layout->xOffset, + layout->yOffset, + presentWidth, + presentHeight + ); + renderer->batchVertexCount = 0; + + if (renderer->commandCount == 0) return; + + WiiURenderer_bindShader(renderer); + WiiURenderer_setCommonState(true); + int32_t currentTexturePageId = INT32_MIN; + + repeat(renderer->commandCount, i) { + WiiUQuadCommand* command = &renderer->commands[i]; + if (renderer->batchVertexCount > 0 && + (command->texturePageId != currentTexturePageId || + renderer->batchVertexCount + WIIU_VERTICES_PER_QUAD > renderer->batchVertexCapacity)) { + WiiURenderer_flushVertices(renderer, renderer->batchVertexCount, currentTexturePageId); + renderer->batchVertexCount = 0; + } + + currentTexturePageId = command->texturePageId; + WiiURenderer_buildQuadVertices(renderer, command, targetWidth, targetHeight, layout); + } + + if (renderer->batchVertexCount > 0) { + WiiURenderer_flushVertices(renderer, renderer->batchVertexCount, currentTexturePageId); + renderer->batchVertexCount = 0; + } +} + +static void WiiURenderer_renderCommandsToSceneTarget(WiiURenderer* renderer) { + if (!renderer->sceneTarget.ready) return; + + uint32_t targetWidth = renderer->sceneTarget.colorBuffer.surface.width; + uint32_t targetHeight = renderer->sceneTarget.colorBuffer.surface.height; + WiiUPresentLayout layout = { + .targetWidth = (float) targetWidth, + .targetHeight = (float) targetHeight, + .xOffset = 0, + .yOffset = 0, + }; + + WiiURenderer_renderCommandsToTarget(renderer, targetWidth, targetHeight, &layout); + GX2DrawDone(); + GX2Invalidate( + GX2_INVALIDATE_MODE_COLOR_BUFFER | GX2_INVALIDATE_MODE_TEXTURE, + renderer->sceneTarget.colorBuffer.surface.image, + renderer->sceneTarget.colorBuffer.surface.imageSize + ); +} + +static void WiiURenderer_renderPresentTextureToTarget( + WiiURenderer* renderer, + GX2ColorBuffer* targetBuffer, + uint32_t targetWidth, + uint32_t targetHeight, + const WiiUPresentLayout* layout, + GX2Texture* texture +) { + if (targetBuffer == NULL || layout == NULL || texture == NULL) return; + + uint32_t presentWidth = (uint32_t) lroundf(layout->targetWidth); + uint32_t presentHeight = (uint32_t) lroundf(layout->targetHeight); + if (presentWidth == 0 || presentHeight == 0) return; + + float x0 = ((float) layout->xOffset / (float) targetWidth) * 2.0f - 1.0f; + float y0 = 1.0f - ((float) (layout->yOffset + presentHeight) / (float) targetHeight) * 2.0f; + float x1 = ((float) (layout->xOffset + presentWidth) / (float) targetWidth) * 2.0f - 1.0f; + float y1 = 1.0f - ((float) layout->yOffset / (float) targetHeight) * 2.0f; + + GX2SetColorBuffer(targetBuffer, GX2_RENDER_TARGET_0); + WiiURenderer_bindShader(renderer); + WiiURenderer_setCommonState(false); + GX2SetAlphaTest(FALSE, GX2_COMPARE_FUNC_ALWAYS, 0.0f); + GX2SetViewport(0.0f, 0.0f, (float) targetWidth, (float) targetHeight, 0.0f, 1.0f); + GX2SetScissor(layout->xOffset, layout->yOffset, presentWidth, presentHeight); + + if (!WiiURenderer_ensureVertexCapacity(renderer, WIIU_VERTICES_PER_QUAD)) return; + if (!WiiURenderer_mapVertexBuffer(renderer)) return; + renderer->batchVertexCount = 0; + + WiiURenderer_emitClipVertex(renderer, x0, y1, 0.0f, 0.0f); + WiiURenderer_emitClipVertex(renderer, x1, y1, 1.0f, 0.0f); + WiiURenderer_emitClipVertex(renderer, x0, y0, 0.0f, 1.0f); + WiiURenderer_emitClipVertex(renderer, x0, y0, 0.0f, 1.0f); + WiiURenderer_emitClipVertex(renderer, x1, y1, 1.0f, 0.0f); + WiiURenderer_emitClipVertex(renderer, x1, y0, 1.0f, 1.0f); + + WiiURenderer_flushVerticesWithTexture(renderer, renderer->batchVertexCount, texture); + renderer->batchVertexCount = 0; +} + +static bool WiiURenderer_initShaderPipeline(WiiURenderer* renderer) { + memset(&renderer->shaderGroup, 0, sizeof(renderer->shaderGroup)); + if (!WHBGfxLoadGFDShaderGroup(&renderer->shaderGroup, 0, resources_wiiu_shaders_textured_quad_gsh)) { + WiiURenderer_bootLog("wiiu_renderer: failed to load shader group"); + return false; + } + if (!WHBGfxInitShaderAttribute(&renderer->shaderGroup, "aPosition", 0, offsetof(WiiUBatchVertex, x), GX2_ATTRIB_FORMAT_FLOAT_32_32_32_32)) { + WiiURenderer_bootLog("wiiu_renderer: failed to init aPosition"); + WHBGfxFreeShaderGroup(&renderer->shaderGroup); + memset(&renderer->shaderGroup, 0, sizeof(renderer->shaderGroup)); + return false; + } + if (!WHBGfxInitShaderAttribute(&renderer->shaderGroup, "aTexCoord", 0, offsetof(WiiUBatchVertex, u), GX2_ATTRIB_FORMAT_FLOAT_32_32)) { + WiiURenderer_bootLog("wiiu_renderer: failed to init aTexCoord"); + WHBGfxFreeShaderGroup(&renderer->shaderGroup); + memset(&renderer->shaderGroup, 0, sizeof(renderer->shaderGroup)); + return false; + } + if (!WHBGfxInitShaderAttribute(&renderer->shaderGroup, "aColour", 0, offsetof(WiiUBatchVertex, r), GX2_ATTRIB_FORMAT_FLOAT_32_32_32_32)) { + WiiURenderer_bootLog("wiiu_renderer: failed to init aColour"); + WHBGfxFreeShaderGroup(&renderer->shaderGroup); + memset(&renderer->shaderGroup, 0, sizeof(renderer->shaderGroup)); + return false; + } + if (!WHBGfxInitFetchShader(&renderer->shaderGroup)) { + WiiURenderer_bootLog("wiiu_renderer: failed to init fetch shader"); + WHBGfxFreeShaderGroup(&renderer->shaderGroup); + memset(&renderer->shaderGroup, 0, sizeof(renderer->shaderGroup)); + return false; + } + + renderer->textureUnit = 0; + GX2InitSampler(&renderer->sampler, GX2_TEX_CLAMP_MODE_CLAMP, GX2_TEX_XY_FILTER_MODE_POINT); + GX2InitSamplerZMFilter(&renderer->sampler, GX2_TEX_Z_FILTER_MODE_NONE, GX2_TEX_MIP_FILTER_MODE_NONE); + renderer->shaderReady = true; + return true; +} + +static void WiiURenderer_init(Renderer* base, DataWin* dataWin) { + WiiURenderer_bootLog("wiiu_renderer: init begin"); + WiiURenderer* renderer = (WiiURenderer*) base; + base->dataWin = dataWin; + + if (!WiiURenderer_initShaderPipeline(renderer)) { + return; + } + + WiiURenderer_refreshOutputState(renderer); + WiiURenderer_bootLog("wiiu_renderer: manual present layout ready"); + + renderer->clearR = 0; + renderer->clearG = 0; + renderer->clearB = 0; + + WiiURenderer_loadTexturePages(renderer, dataWin); + WiiURenderer_bootLog("wiiu_renderer: gpu path ready"); + WiiURenderer_bootLog("wiiu_renderer: init end"); +} + +static void WiiURenderer_destroy(Renderer* base) { + WiiURenderer* renderer = (WiiURenderer*) base; + + WiiURenderer_freeTexturePages(renderer); + WiiURenderer_destroyRenderTarget(&renderer->sceneTarget); + WiiURenderer_destroyVertexBuffer(renderer); + if (renderer->shaderReady) { + WHBGfxFreeShaderGroup(&renderer->shaderGroup); + } + free(renderer->commands); + free(renderer); +} + +static void WiiURenderer_beginFrame(Renderer* base, int32_t gameW, int32_t gameH, int32_t windowW, int32_t windowH) { + (void) windowW; + (void) windowH; + + WiiURenderer* renderer = (WiiURenderer*) base; + renderer->frameWidth = gameW; + renderer->frameHeight = gameH; + WiiURenderer_ensureSceneTarget(renderer, (uint32_t) gameW, (uint32_t) gameH); + WiiURenderer_updatePresentLayout(renderer); + renderer->viewX = 0; + renderer->viewY = 0; + renderer->viewW = gameW; + renderer->viewH = gameH; + renderer->portX = 0; + renderer->portY = 0; + renderer->portW = gameW; + renderer->portH = gameH; + renderer->viewScaleX = 1.0f; + renderer->viewScaleY = 1.0f; + renderer->commandCount = 0; + renderer->batchVertexCount = 0; + renderer->queuedQuadCount = 0; + if (renderer->commandCapacity > 2048 && renderer->commands != NULL) { + WiiUQuadCommand* resizedCommands = realloc(renderer->commands, (size_t) 2048 * sizeof(WiiUQuadCommand)); + if (resizedCommands != NULL) { + renderer->commands = resizedCommands; + renderer->commandCapacity = 2048; + } + } +} + +static void WiiURenderer_beginView(Renderer* base, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, int32_t portX, int32_t portY, int32_t portW, int32_t portH, float viewAngle) { + (void) viewAngle; + + WiiURenderer* renderer = (WiiURenderer*) base; + renderer->viewX = viewX; + renderer->viewY = viewY; + renderer->viewW = viewW != 0 ? viewW : 1; + renderer->viewH = viewH != 0 ? viewH : 1; + renderer->portX = portX; + renderer->portY = portY; + renderer->portW = portW; + renderer->portH = portH; + renderer->viewScaleX = (float) portW / (float) renderer->viewW; + renderer->viewScaleY = (float) portH / (float) renderer->viewH; +} + +static void WiiURenderer_endView(Renderer* base) { + (void) base; +} + +static void WiiURenderer_beginGUI(Renderer* base, int32_t guiW, int32_t guiH, int32_t portX, int32_t portY, int32_t portW, int32_t portH) { + WiiURenderer_beginView(base, 0, 0, guiW, guiH, portX, portY, portW, portH, 0.0f); +} + +static void WiiURenderer_endGUI(Renderer* base) { + WiiURenderer_endView(base); +} + +static void WiiURenderer_endFrame(Renderer* base) { + WiiURenderer* renderer = (WiiURenderer*) base; + GX2ColorBuffer* tvScan = WHBGfxGetTVColourBuffer(); + GX2ColorBuffer* drcScan = WHBGfxGetDRCColourBuffer(); + bool traceFrame = renderer->debugFrameIndex < 2; + if (!renderer->shaderReady || tvScan == NULL || drcScan == NULL) { + WiiURenderer_bootLog("wiiu_renderer: missing scan buffers"); + return; + } + + if (renderer->debugFrameIndex < 12) { + char summary[160]; + snprintf( + summary, + sizeof(summary), + "wiiu_renderer: frame=%u cmds=%u quads=%u frame=%dx%d view=%d,%d %dx%d port=%d,%d %dx%d", + renderer->debugFrameIndex, + renderer->commandCount, + renderer->queuedQuadCount, + renderer->frameWidth, + renderer->frameHeight, + renderer->viewX, + renderer->viewY, + renderer->viewW, + renderer->viewH, + renderer->portX, + renderer->portY, + renderer->portW, + renderer->portH + ); + WiiURenderer_bootLog(summary); + + uint32_t sampleCount = renderer->commandCount < 8 ? renderer->commandCount : 8; + for (uint32_t i = 0; i < sampleCount; ++i) { + WiiUQuadCommand* command = &renderer->commands[i]; + char detail[256]; + snprintf( + detail, + sizeof(detail), + "wiiu_renderer: cmd[%u] tex=%d uv=(%.4f,%.4f)-(%.4f,%.4f) p00=(%.1f,%.1f) p10=(%.1f,%.1f) p01=(%.1f,%.1f) alpha=%.3f color=%06X grad=%d", + i, + command->texturePageId, + command->u0, + command->v0, + command->u1, + command->v1, + command->p00.x, + command->p00.y, + command->p10.x, + command->p10.y, + command->p01.x, + command->p01.y, + command->alpha, + command->color0 & 0xFFFFFFu, + command->gradient ? 1 : 0 + ); + WiiURenderer_bootLog(detail); + } + } + + OSTime tvStart = 0; + OSTime tvEnd = 0; + OSTime drcStart = 0; + OSTime drcEnd = 0; + WiiUPresentLayout tvLayout = { 0 }; + WiiUPresentLayout drcLayout = { 0 }; + GX2ContextState* tvContext = WHBGfxGetTVContextState(); + + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: before WHBGfxBeginRender"); + WHBGfxBeginRender(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: after WHBGfxBeginRender"); + WiiURenderer_refreshOutputState(renderer); + WiiURenderer_computeOutputLayout( + &drcLayout, + drcScan->surface.width, + drcScan->surface.height, + (uint32_t) renderer->frameWidth, + (uint32_t) renderer->frameHeight, + false + ); + WiiURenderer_computeOutputLayout( + &tvLayout, + tvScan->surface.width, + tvScan->surface.height, + (uint32_t) renderer->frameWidth, + (uint32_t) renderer->frameHeight, + true + ); + + tvStart = OSGetTime(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: before WHBGfxBeginRenderTV"); + WHBGfxBeginRenderTV(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: after WHBGfxBeginRenderTV"); + + GX2SetColorBuffer(&renderer->sceneTarget.colorBuffer, GX2_RENDER_TARGET_0); + GX2SetViewport( + 0.0f, + 0.0f, + (float) renderer->sceneTarget.colorBuffer.surface.width, + (float) renderer->sceneTarget.colorBuffer.surface.height, + 0.0f, + 1.0f + ); + GX2SetScissor(0, 0, renderer->sceneTarget.colorBuffer.surface.width, renderer->sceneTarget.colorBuffer.surface.height); + GX2ClearColor( + &renderer->sceneTarget.colorBuffer, + (float) renderer->clearR / 255.0f, + (float) renderer->clearG / 255.0f, + (float) renderer->clearB / 255.0f, + 1.0f + ); + if (tvContext != NULL) { + GX2SetContextState(tvContext); + } + GX2SetColorBuffer(&renderer->sceneTarget.colorBuffer, GX2_RENDER_TARGET_0); + WiiURenderer_renderCommandsToSceneTarget(renderer); + GX2SetColorBuffer(tvScan, GX2_RENDER_TARGET_0); + GX2SetViewport(0.0f, 0.0f, (float) tvScan->surface.width, (float) tvScan->surface.height, 0.0f, 1.0f); + GX2SetScissor(0, 0, tvScan->surface.width, tvScan->surface.height); + WHBGfxClearColor(0.0f, 0.0f, 0.0f, 1.0f); + WiiURenderer_renderPresentTextureToTarget( + renderer, + tvScan, + tvScan->surface.width, + tvScan->surface.height, + &tvLayout, + &renderer->sceneTarget.presentTexture + ); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: before WHBGfxFinishRenderTV"); + WHBGfxFinishRenderTV(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: after WHBGfxFinishRenderTV"); + tvEnd = OSGetTime(); + + drcStart = OSGetTime(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: before WHBGfxBeginRenderDRC"); + WHBGfxBeginRenderDRC(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: after WHBGfxBeginRenderDRC"); + GX2SetScissor(0, 0, drcScan->surface.width, drcScan->surface.height); + WHBGfxClearColor(0.0f, 0.0f, 0.0f, 1.0f); + WiiURenderer_renderPresentTextureToTarget( + renderer, + drcScan, + drcScan->surface.width, + drcScan->surface.height, + &drcLayout, + &renderer->sceneTarget.presentTexture + ); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: before WHBGfxFinishRenderDRC"); + WHBGfxFinishRenderDRC(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: after WHBGfxFinishRenderDRC"); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: before GX2DrawDone"); + GX2DrawDone(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: after GX2DrawDone"); + drcEnd = OSGetTime(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: before WHBGfxFinishRender"); + WHBGfxFinishRender(); + if (traceFrame) WiiURenderer_bootLog("wiiu_renderer: after WHBGfxFinishRender"); + + renderer->perfSceneMs += 0.0; + renderer->perfRenderTvMs += WiiURenderer_elapsedMs(tvStart, tvEnd); + renderer->perfRenderDrcMs += WiiURenderer_elapsedMs(drcStart, drcEnd); + renderer->perfQuadCount += renderer->queuedQuadCount; + renderer->perfFrameCount++; + + if (renderer->perfFrameCount >= 60) { + char perfBuffer[256]; + snprintf( + perfBuffer, + sizeof(perfBuffer), + "wiiu_renderer: avg over %u frames scene=%.2fms tv=%.2fms drc=%.2fms total=%.2fms quads=%u flush=%u", + renderer->perfFrameCount, + renderer->perfSceneMs / (double) renderer->perfFrameCount, + renderer->perfRenderTvMs / (double) renderer->perfFrameCount, + renderer->perfRenderDrcMs / (double) renderer->perfFrameCount, + (renderer->perfSceneMs + renderer->perfRenderTvMs + renderer->perfRenderDrcMs) / (double) renderer->perfFrameCount, + renderer->perfQuadCount / renderer->perfFrameCount, + renderer->perfFlushCount / renderer->perfFrameCount + ); + WiiURenderer_bootLog(perfBuffer); + renderer->perfFrameCount = 0; + renderer->perfSceneMs = 0.0; + renderer->perfRenderTvMs = 0.0; + renderer->perfRenderDrcMs = 0.0; + renderer->perfFlushCount = 0; + renderer->perfQuadCount = 0; + } + + renderer->debugFrameIndex++; +} + +static void WiiURenderer_drawSprite(Renderer* base, int32_t tpagIndex, float x, float y, float originX, float originY, float xscale, float yscale, float angleDeg, uint32_t color, float alpha) { + WiiURenderer* renderer = (WiiURenderer*) base; + DataWin* dataWin = base->dataWin; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) return; + + TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; + if (tpag->texturePageId < 0 || (uint32_t) tpag->texturePageId >= renderer->texturePageCount) return; + if (!renderer->texturePages[tpag->texturePageId].ready) return; + + GX2Texture* page = &renderer->texturePages[tpag->texturePageId].texture; + x = WiiURenderer_snapPixel(x); + y = WiiURenderer_snapPixel(y); + float localX0 = (float) tpag->targetX - originX; + float localY0 = (float) tpag->targetY - originY; + float localX1 = localX0 + (float) tpag->sourceWidth; + float localY1 = localY0 + (float) tpag->sourceHeight; + + float angleRad = -angleDeg * ((float) M_PI / 180.0f); + Matrix4f transform; + Matrix4f_setTransform2D(&transform, x, y, xscale, yscale, angleRad); + + float w0x, w0y, w1x, w1y, w2x, w2y; + Matrix4f_transformPoint(&transform, localX0, localY0, &w0x, &w0y); + Matrix4f_transformPoint(&transform, localX1, localY0, &w1x, &w1y); + Matrix4f_transformPoint(&transform, localX0, localY1, &w2x, &w2y); + + WiiURenderer_appendQuad( + renderer, + tpag->texturePageId, + WiiURenderer_worldToGame(renderer, w0x, w0y), + WiiURenderer_worldToGame(renderer, w1x, w1y), + WiiURenderer_worldToGame(renderer, w2x, w2y), + (float) tpag->sourceX / (float) page->surface.width, + (float) tpag->sourceY / (float) page->surface.height, + (float) (tpag->sourceX + tpag->sourceWidth) / (float) page->surface.width, + (float) (tpag->sourceY + tpag->sourceHeight) / (float) page->surface.height, + color, + alpha + ); +} + +static void WiiURenderer_drawSpritePart(Renderer* base, int32_t tpagIndex, int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, float x, float y, float xscale, float yscale, float angleDeg, float pivotX, float pivotY, uint32_t color, float alpha) { + WiiURenderer* renderer = (WiiURenderer*) base; + DataWin* dataWin = base->dataWin; + (void) angleDeg; + (void) pivotX; + (void) pivotY; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) return; + + TexturePageItem* tpag = &dataWin->tpag.items[tpagIndex]; + if (tpag->texturePageId < 0 || (uint32_t) tpag->texturePageId >= renderer->texturePageCount) return; + if (!renderer->texturePages[tpag->texturePageId].ready) return; + + GX2Texture* page = &renderer->texturePages[tpag->texturePageId].texture; + x = WiiURenderer_snapPixel(x); + y = WiiURenderer_snapPixel(y); + float x1 = x + (float) srcW * xscale; + float y1 = y + (float) srcH * yscale; + WiiURenderer_appendQuad( + renderer, + tpag->texturePageId, + WiiURenderer_worldToGame(renderer, x, y), + WiiURenderer_worldToGame(renderer, x1, y), + WiiURenderer_worldToGame(renderer, x, y1), + (float) (tpag->sourceX + srcOffX) / (float) page->surface.width, + (float) (tpag->sourceY + srcOffY) / (float) page->surface.height, + (float) (tpag->sourceX + srcOffX + srcW) / (float) page->surface.width, + (float) (tpag->sourceY + srcOffY + srcH) / (float) page->surface.height, + color, + alpha + ); +} + +static void WiiURenderer_drawRectangle(Renderer* base, float x1, float y1, float x2, float y2, uint32_t color, float alpha, bool outline) { + WiiURenderer* renderer = (WiiURenderer*) base; + + if (outline) { + renderer->base.vtable->drawLine(base, x1, y1, x2, y1, 1.0f, color, alpha); + renderer->base.vtable->drawLine(base, x2, y1, x2, y2, 1.0f, color, alpha); + renderer->base.vtable->drawLine(base, x2, y2, x1, y2, 1.0f, color, alpha); + renderer->base.vtable->drawLine(base, x1, y2, x1, y1, 1.0f, color, alpha); + return; + } + + WiiURenderer_appendQuad( + renderer, + -1, + WiiURenderer_worldToGame(renderer, x1, y1), + WiiURenderer_worldToGame(renderer, x2, y1), + WiiURenderer_worldToGame(renderer, x1, y2), + 0.0f, + 0.0f, + 1.0f, + 1.0f, + color, + alpha + ); +} + +static void WiiURenderer_drawLine(Renderer* base, float x1, float y1, float x2, float y2, float width, uint32_t color, float alpha) { + WiiURenderer* renderer = (WiiURenderer*) base; + + WiiUVec2 p0 = WiiURenderer_worldToGame(renderer, x1, y1); + WiiUVec2 p1 = WiiURenderer_worldToGame(renderer, x2, y2); + x1 = p0.x; + y1 = p0.y; + x2 = p1.x; + y2 = p1.y; + width *= fmaxf(renderer->viewScaleX, renderer->viewScaleY); + + float dx = x2 - x1; + float dy = y2 - y1; + float len = sqrtf(dx * dx + dy * dy); + if (len <= 0.0001f) { + WiiURenderer_appendQuad(renderer, -1, (WiiUVec2) { x1, y1 }, (WiiUVec2) { x1 + 1.0f, y1 }, (WiiUVec2) { x1, y1 + 1.0f }, 0.0f, 0.0f, 1.0f, 1.0f, color, alpha); + return; + } + + float half = fmaxf(width, 1.0f) * 0.5f; + float nx = -dy / len * half; + float ny = dx / len * half; + WiiURenderer_appendQuad( + renderer, + -1, + (WiiUVec2) { x1 - nx, y1 - ny }, + (WiiUVec2) { x2 - nx, y2 - ny }, + (WiiUVec2) { x1 + nx, y1 + ny }, + 0.0f, + 0.0f, + 1.0f, + 1.0f, + color, + alpha + ); +} + +static void WiiURenderer_drawLineColor(Renderer* base, float x1, float y1, float x2, float y2, float width, uint32_t color1, uint32_t color2, float alpha) { + WiiURenderer* renderer = (WiiURenderer*) base; + + WiiUVec2 p0 = WiiURenderer_worldToGame(renderer, x1, y1); + WiiUVec2 p1 = WiiURenderer_worldToGame(renderer, x2, y2); + x1 = p0.x; + y1 = p0.y; + x2 = p1.x; + y2 = p1.y; + width *= fmaxf(renderer->viewScaleX, renderer->viewScaleY); + + float dx = x2 - x1; + float dy = y2 - y1; + float len = sqrtf(dx * dx + dy * dy); + if (len <= 0.0001f) { + renderer->base.vtable->drawRectangle(base, x1, y1, x1 + 1.0f, y1 + 1.0f, color1, alpha, false); + return; + } + + float half = fmaxf(width, 1.0f) * 0.5f; + float nx = -dy / len * half; + float ny = dx / len * half; + WiiURenderer_appendGradientQuad( + renderer, + -1, + (WiiUVec2) { x1 - nx, y1 - ny }, + (WiiUVec2) { x2 - nx, y2 - ny }, + (WiiUVec2) { x1 + nx, y1 + ny }, + 0.0f, + 0.0f, + 1.0f, + 1.0f, + color1, + color2, + alpha + ); +} + +static void WiiURenderer_drawText(Renderer* base, const char* text, float x, float y, float xscale, float yscale, float angleDeg) { + WiiURenderer* renderer = (WiiURenderer*) base; + DataWin* dataWin = base->dataWin; + + int32_t fontIndex = base->drawFont; + if (fontIndex < 0 || (uint32_t) fontIndex >= dataWin->font.count) return; + + Font* font = &dataWin->font.fonts[fontIndex]; + int32_t fontTpagIndex = font->tpagIndex; + if (fontTpagIndex < 0 || (uint32_t) fontTpagIndex >= dataWin->tpag.count) return; + + TexturePageItem* fontTpag = &dataWin->tpag.items[fontTpagIndex]; + if (fontTpag->texturePageId < 0 || (uint32_t) fontTpag->texturePageId >= renderer->texturePageCount) return; + if (!renderer->texturePages[fontTpag->texturePageId].ready) return; + + GX2Texture* page = &renderer->texturePages[fontTpag->texturePageId].texture; + PreprocessedText processed = TextUtils_preprocessGmlText(text); + const char* processedText = processed.text; + int32_t textLen = (int32_t) strlen(processedText); + int32_t lineCount = TextUtils_countLines(processedText, textLen); + float totalHeight = (float) lineCount * (float) font->emSize; + float valignOffset = 0.0f; + if (base->drawValign == 1) valignOffset = -totalHeight / 2.0f; + else if (base->drawValign == 2) valignOffset = -totalHeight; + + float angleRad = -angleDeg * ((float) M_PI / 180.0f); + Matrix4f transform; + x = WiiURenderer_snapPixel(x); + y = WiiURenderer_snapPixel(y); + Matrix4f_setTransform2D(&transform, x, y, xscale * font->scaleX, yscale * font->scaleY, angleRad); + + float cursorY = valignOffset; + int32_t lineStart = 0; + repeat(lineCount, lineIdx) { + int32_t lineEnd = lineStart; + while (lineEnd < textLen && !TextUtils_isNewlineChar(processedText[lineEnd])) lineEnd++; + int32_t lineLen = lineEnd - lineStart; + + float lineWidth = TextUtils_measureLineWidth(font, processedText + lineStart, lineLen); + float halignOffset = 0.0f; + if (base->drawHalign == 1) halignOffset = -lineWidth / 2.0f; + else if (base->drawHalign == 2) halignOffset = -lineWidth; + + float cursorX = halignOffset; + int32_t pos = 0; + while (pos < lineLen) { + uint16_t ch = TextUtils_decodeUtf8(processedText + lineStart, lineLen, &pos); + FontGlyph* glyph = TextUtils_findGlyph(font, ch); + if (glyph == NULL) continue; + if (glyph->sourceWidth == 0 || glyph->sourceHeight == 0) { + cursorX += glyph->shift; + continue; + } + + float localX0 = cursorX + glyph->offset; + float localY0 = cursorY; + float localX1 = localX0 + (float) glyph->sourceWidth; + float localY1 = localY0 + (float) glyph->sourceHeight; + + float w0x, w0y, w1x, w1y, w2x, w2y; + Matrix4f_transformPoint(&transform, localX0, localY0, &w0x, &w0y); + Matrix4f_transformPoint(&transform, localX1, localY0, &w1x, &w1y); + Matrix4f_transformPoint(&transform, localX0, localY1, &w2x, &w2y); + + WiiURenderer_appendQuad( + renderer, + fontTpag->texturePageId, + WiiURenderer_worldToGame(renderer, w0x, w0y), + WiiURenderer_worldToGame(renderer, w1x, w1y), + WiiURenderer_worldToGame(renderer, w2x, w2y), + (float) (fontTpag->sourceX + glyph->sourceX) / (float) page->surface.width, + (float) (fontTpag->sourceY + glyph->sourceY) / (float) page->surface.height, + (float) (fontTpag->sourceX + glyph->sourceX + glyph->sourceWidth) / (float) page->surface.width, + (float) (fontTpag->sourceY + glyph->sourceY + glyph->sourceHeight) / (float) page->surface.height, + base->drawColor, + base->drawAlpha + ); + + cursorX += glyph->shift; + if (pos < lineLen) { + int32_t savedPos = pos; + uint16_t nextCh = TextUtils_decodeUtf8(processedText + lineStart, lineLen, &pos); + pos = savedPos; + cursorX += TextUtils_getKerningOffset(glyph, nextCh); + } + } + + cursorY += (float) font->emSize; + lineStart = TextUtils_skipNewline(processedText, lineEnd, textLen); + } + + PreprocessedText_free(processed); +} + +static void WiiURenderer_flush(Renderer* base) { + (void) base; +} + +static int32_t WiiURenderer_createSpriteFromSurface(Renderer* base, int32_t surfaceID, int32_t x, int32_t y, int32_t w, int32_t h, bool removeback, bool smooth, int32_t xorig, int32_t yorig) { + (void) removeback; + (void) surfaceID; + (void) x; + (void) y; + (void) w; + (void) h; + (void) smooth; + (void) xorig; + (void) yorig; + (void) base; + WiiURenderer_bootLog("wiiu_renderer: createSpriteFromSurface unsupported on gpu path"); + return -1; +} + +static void WiiURenderer_deleteSprite(Renderer* base, int32_t spriteIndex) { + (void) base; + (void) spriteIndex; +} + +static RendererVtable WiiURendererVtable = { + .init = WiiURenderer_init, + .destroy = WiiURenderer_destroy, + .beginFrame = WiiURenderer_beginFrame, + .endFrame = WiiURenderer_endFrame, + .beginView = WiiURenderer_beginView, + .endView = WiiURenderer_endView, + .beginGUI = WiiURenderer_beginGUI, + .endGUI = WiiURenderer_endGUI, + .drawSprite = WiiURenderer_drawSprite, + .drawSpritePart = WiiURenderer_drawSpritePart, + .drawRectangle = WiiURenderer_drawRectangle, + .drawLine = WiiURenderer_drawLine, + .drawLineColor = WiiURenderer_drawLineColor, + .drawText = WiiURenderer_drawText, + .flush = WiiURenderer_flush, + .createSpriteFromSurface = WiiURenderer_createSpriteFromSurface, + .deleteSprite = WiiURenderer_deleteSprite, + .drawTile = NULL, + .drawTiled = NULL, +}; + +Renderer* WiiURenderer_create(void) { + WiiURenderer_bootLog("wiiu_renderer: create begin"); + WiiURenderer* renderer = safeCalloc(1, sizeof(WiiURenderer)); + renderer->base.vtable = &WiiURendererVtable; + renderer->base.drawColor = 0xFFFFFF; + renderer->base.drawAlpha = 1.0f; + renderer->base.drawFont = -1; + WiiURenderer_bootLog("wiiu_renderer: create end"); + return (Renderer*) renderer; +} + +void WiiURenderer_setClearColor(WiiURenderer* renderer, uint32_t color) { + renderer->clearR = (uint8_t) BGR_R(color); + renderer->clearG = (uint8_t) BGR_G(color); + renderer->clearB = (uint8_t) BGR_B(color); +} + +void WiiURenderer_runStartupSmokeTest(uint32_t frameCount) { + (void) frameCount; +} diff --git a/src/wiiu/wiiu_renderer.h b/src/wiiu/wiiu_renderer.h new file mode 100644 index 00000000..62f86589 --- /dev/null +++ b/src/wiiu/wiiu_renderer.h @@ -0,0 +1,116 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "../renderer.h" + +typedef struct { + bool ready; + GX2Texture texture; +} WiiUTexturePage; + +typedef struct { + bool ready; + GX2ColorBuffer colorBuffer; + GX2Texture presentTexture; +} WiiURenderTarget; + +typedef struct { + float x; + float y; + float z; + float w; + float u; + float v; + float r; + float g; + float b; + float a; +} WiiUBatchVertex; + +typedef struct { + float targetWidth; + float targetHeight; + uint32_t xOffset; + uint32_t yOffset; +} WiiUPresentLayout; + +typedef struct { + float x; + float y; +} WiiUVec2; + +typedef struct { + int32_t texturePageId; + bool gradient; + WiiUVec2 p00; + WiiUVec2 p10; + WiiUVec2 p01; + float u0; + float v0; + float u1; + float v1; + uint32_t color0; + uint32_t color1; + float alpha; +} WiiUQuadCommand; + +typedef struct { + Renderer base; + + WHBGfxShaderGroup shaderGroup; + GX2Sampler sampler; + uint32_t textureUnit; + bool shaderReady; + + WiiUTexturePage* texturePages; + uint32_t texturePageCount; + WiiUTexturePage whiteTexture; + WiiURenderTarget sceneTarget; + + GX2RBuffer batchVertexBuffer; + WiiUBatchVertex* batchVertices; + uint32_t batchVertexCapacity; + uint32_t batchVertexCount; + WiiUQuadCommand* commands; + uint32_t commandCount; + uint32_t commandCapacity; + uint32_t queuedQuadCount; + + int32_t frameWidth; + int32_t frameHeight; + WiiUPresentLayout presentLayout; + + int32_t viewX; + int32_t viewY; + int32_t viewW; + int32_t viewH; + int32_t portX; + int32_t portY; + int32_t portW; + int32_t portH; + float viewScaleX; + float viewScaleY; + + uint8_t clearR; + uint8_t clearG; + uint8_t clearB; + + uint32_t perfFrameCount; + double perfSceneMs; + double perfRenderTvMs; + double perfRenderDrcMs; + uint32_t perfFlushCount; + uint32_t perfQuadCount; + uint32_t debugFrameIndex; +} WiiURenderer; + +Renderer* WiiURenderer_create(void); +void WiiURenderer_setClearColor(WiiURenderer* renderer, uint32_t color); +void WiiURenderer_runStartupSmokeTest(uint32_t frameCount); \ No newline at end of file diff --git a/tools/n3ds-preprocess/CMakeLists.txt b/tools/n3ds-preprocess/CMakeLists.txt new file mode 100644 index 00000000..b6c21f62 --- /dev/null +++ b/tools/n3ds-preprocess/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.21) +project(n3ds_preprocess C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +add_compile_options(-Wall -Wextra) + +add_executable(n3ds-preprocess + main.c + stb_impl.c + ../../src/binary_reader.c + ../../src/data_win.c + ../../src/gl/image_decoder.c +) + +target_include_directories(n3ds-preprocess PRIVATE + ../../src + ../../vendor + ../../vendor/stb/ds + ../../vendor/stb/image +) + +target_compile_definitions(n3ds-preprocess PRIVATE + IMAGE_DECODER_HAS_BZ2=0 + N3DS_PREPROCESS_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}" + N3DS_PREPROCESS_TEXTURE_OVERRIDE_DIR="${CMAKE_SOURCE_DIR}/resources/3ds/textureOverrides" +) + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_libraries(n3ds-preprocess PRIVATE m) +endif() diff --git a/tools/n3ds-preprocess/main.c b/tools/n3ds-preprocess/main.c new file mode 100644 index 00000000..57aa1f90 --- /dev/null +++ b/tools/n3ds-preprocess/main.c @@ -0,0 +1,5043 @@ +#include "../../src/data_win.h" +#include "../../src/gl/image_decoder.h" +#include "../../src/utils.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define STB_VORBIS_HEADER_ONLY +#include "../../vendor/stb/vorbis/stb_vorbis.c" + +#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MSYS__) +#define N3DS_PREPROCESS_HOST_WINDOWS 1 +#else +#define N3DS_PREPROCESS_HOST_WINDOWS 0 +#endif + +#if N3DS_PREPROCESS_HOST_WINDOWS +#include +#include +#endif + +#if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__MSYS__) +#include +#define MKDIR(path) _mkdir(path) +#else +#include +#include +#include +#include +#define MKDIR(path) mkdir(path, 0777) +#endif + +#if defined(__GNUC__) || defined(__clang__) +#define N3DS_PREPROCESS_MAYBE_UNUSED __attribute__((unused)) +#else +#define N3DS_PREPROCESS_MAYBE_UNUSED +#endif + +#if N3DS_PREPROCESS_HOST_WINDOWS +#define N3DS_PREPROCESS_PATH_SEP "\\" +#else +#define N3DS_PREPROCESS_PATH_SEP "/" +#endif + +#define N3DS_ATLAS_MAGIC 0x5441334Eu /* N3AT */ +#define N3DS_ATLAS_VERSION_T3X 2u +#define N3DS_ATLAS_VERSION_FRAGMENTED 3u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILES 4u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS 5u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_FMT 6u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PAGEFMT 7u +#define N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED 8u +#define N3DS_DIRECT_ASSET_MAGIC 0x3152444Eu /* NDR1 */ +#define N3DS_DIRECT_ASSET_VERSION 1u +#define N3DS_ROOM_MANIFEST_MAGIC 0x4D52334Eu /* N3RM */ +#define N3DS_ROOM_MANIFEST_VERSION 1u +#define N3DS_SOUND_BANK_MAGIC 0x314B4253u /* SBK1 */ +#define N3DS_SOUND_BANK_VERSION 2u +#define N3DS_SOUND_BANK_ENTRY_CHANNEL_MASK 0x000000FFu +#define N3DS_SOUND_BANK_ENTRY_FLAG_LOOP 0x00000100u +#define N3DS_SOUND_BANK_ENTRY_FLAG_PCM16 0x00000200u +#define N3DS_SMALL_TEXTURE_SIZE 256u +#define N3DS_LARGE_TEXTURE_SIZE 512u +#define GMS2_TILE_INDEX_MASK 0x0007FFFFu +#define CWAV_VERSION 0x02010000u +#define CWAV_REF_DSP_ADPCM_INFO 0x0300u +#define CWAV_REF_SAMPLE_DATA 0x1F00u +#define CWAV_REF_INFO_BLOCK 0x7000u +#define CWAV_REF_DATA_BLOCK 0x7001u +#define CWAV_REF_CHANNEL_INFO 0x7100u + +typedef enum { + N3DS_ATLAS_PAGE_MODE_AUTO = 0, + N3DS_ATLAS_PAGE_MODE_FORCE_256, + N3DS_ATLAS_PAGE_MODE_FORCE_512, +} N3DSAtlasPageMode; + +typedef enum { + N3DS_TEXFMT_RGBA5551 = 0, + N3DS_TEXFMT_ETC1A4 = 1, + N3DS_TEXFMT_INDEXED8 = 2, + N3DS_TEXFMT_HYBRID = 3, + N3DS_TEXFMT_L4 = 4, + N3DS_TEXFMT_LA4 = 5, +} N3DSTextureFormat; + +typedef struct { + uint32_t key; + uint32_t value; +} ColorFreqMap; + +typedef struct { + uint16_t width; + uint16_t height; + uint32_t fragmentStart; + uint16_t fragmentCount; +} OutputItem; + +typedef struct { + uint16_t atlasId; + uint16_t x; + uint16_t y; + uint16_t width; + uint16_t height; + uint16_t sourceX; + uint16_t sourceY; +} OutputFragment; + +typedef struct { + int16_t bgDef; + uint16_t srcX; + uint16_t srcY; + uint16_t srcW; + uint16_t srcH; + uint16_t atlasId; + uint16_t x; + uint16_t y; + uint16_t width; + uint16_t height; + uint32_t fragmentStart; + uint16_t fragmentCount; +} OutputTileEntry; + +typedef struct { + int16_t bgDef; + uint16_t srcX; + uint16_t srcY; + uint16_t srcW; + uint16_t srcH; +} TileLookupKey; + +typedef struct { + TileLookupKey key; + uint32_t value; +} TileRequestMap; + +typedef struct { + const char* inputPath; + const char* outputDir; + const char* tex3dsExe; + const char* spriteReplacementDir; + const char* borderAssetDir; + const char* pageFormatOverridesPath; + bool keepPng; + bool dumpPagePreviews; + bool enableTargetedBattleDialogueMono; + bool interactiveMode; + bool inspectRoomMode; + int32_t inspectRoomIndex; + N3DSAtlasPageMode atlasPageMode; + N3DSTextureFormat textureFormat; + char inputPathStorage[1024]; + char outputDirStorage[1024]; + char finalOutputDirStorage[1024]; + char stagingOutputDirStorage[1024]; + char tex3dsExeStorage[1024]; + char spriteReplacementDirStorage[1200]; + char borderAssetDirStorage[1200]; + char toolDirStorage[1024]; + bool stageOutputLocally; + bool spriteReplacementDirAvailable; + bool borderAssetDirAvailable; +} Options; + +typedef struct { + uint16_t width; + uint16_t height; + uint32_t textureFormat; + char path[1024]; + char previewLabel[128]; + char debugNames[2048]; + bool containsSprite; + bool containsFont; + bool containsBackground; + bool containsNonMonoContent; +} OutputPage; + +typedef struct { + uint32_t pageIndex; + uint32_t width; + uint32_t height; + uint32_t cursorX; + uint32_t cursorY; + uint32_t rowHeight; + uint8_t* pixels; +} PackedPage; + +typedef struct { + bool sawFrame; + bool allFramesMonoSafe; +} DirectSpriteFormatState; + +typedef struct { + char relativePath[320]; + char sourcePath[1024]; + uint32_t pathOffset; + uint32_t dataOffset; + uint32_t dataSize; +} DirectAssetPackEntry; + +typedef struct { + uint32_t roomIndex; + uint32_t pageStart; + uint32_t pageCount; + uint32_t directSpriteStart; + uint32_t directSpriteCount; + uint32_t directBackgroundStart; + uint32_t directBackgroundCount; +} N3DSRoomManifestEntry; + +typedef struct { + uint16_t pageIndex; + uint8_t flags; + uint8_t reserved; +} N3DSRoomManifestPageRef; + +static bool fileExists(const char* path); +static bool directoryExists(const char* path); +static bool deleteDirectoryRecursive(const char* path); +static bool ensureDirRecursive(const char* path); +static char* dupParentDir(const char* path); +static bool getProgramDirPath(const char* argv0, char* outDir, size_t outDirSize); +static bool configureStagedOutputDir(Options* options, const char* argv0); +static bool configureSpriteReplacementDir(Options* options, const char* argv0); +static bool configureBorderAssetDir(Options* options, const char* argv0); +static bool syncStagedOutputToDestination(const Options* options); +static bool convertBorderAssets(const Options* options); +static void writeLe32(uint8_t* ptr, uint32_t value); + +static void setDefaultOptions(Options* out) { + memset(out, 0, sizeof(*out)); + out->keepPng = false; + out->dumpPagePreviews = false; + out->enableTargetedBattleDialogueMono = false; + out->interactiveMode = false; + out->atlasPageMode = N3DS_ATLAS_PAGE_MODE_AUTO; + out->textureFormat = N3DS_TEXFMT_HYBRID; + out->stageOutputLocally = true; +} + +static void printUsage(const char* argv0) { + fprintf(stderr, + "Usage: %s [--tex3ds ] [--keep-png]\n" + " [--atlas-page-mode auto|256|512]\n" + " [--texture-format etc1a4|rgba5551|indexed8|hybrid]\n" + " [--l4-dialogue-battle-sprites]\n" + " [--dump-page-previews]\n" + " [--page-format-overrides ]\n" + " [--sprite-replacements ]\n" + " [--borders ]\n" + "\n" + "Running with no arguments on Windows starts an interactive setup.\n" + "\n" + "Outputs:\n" + " /gfx/atlas.bin\n" + " /gfx/direct_assets.bin\n" + " /gfx/room_manifest.bin\n" + " /gfx/page_000.t3x ...\n" + " /audio/sound_bank.bin\n", + argv0 + ); +} + +#if N3DS_PREPROCESS_HOST_WINDOWS +static bool getExecutableDir(char* out, size_t outSize) { + if (out == NULL || outSize == 0) return false; + DWORD len = GetModuleFileNameA(NULL, out, (DWORD) outSize); + if (len == 0 || len >= outSize) return false; + + char* slash = strrchr(out, '\\'); + char* forward = strrchr(out, '/'); + if (forward != NULL && (slash == NULL || forward > slash)) slash = forward; + if (slash == NULL) return false; + *slash = '\0'; + return true; +} + +static bool readRegistryString(HKEY root, const char* subKey, const char* valueName, char* out, DWORD outSize) { + HKEY key = NULL; + DWORD type = REG_SZ; + DWORD size = outSize; + if (RegOpenKeyExA(root, subKey, 0, KEY_READ, &key) != ERROR_SUCCESS) return false; + LONG status = RegQueryValueExA(key, valueName, NULL, &type, (LPBYTE) out, &size); + RegCloseKey(key); + if (status != ERROR_SUCCESS || (type != REG_SZ && type != REG_EXPAND_SZ)) return false; + out[outSize - 1] = '\0'; + return out[0] != '\0'; +} +#endif + +static void applyDefaultToolPaths(Options* out) { + if (out == NULL) return; + +#if N3DS_PREPROCESS_HOST_WINDOWS + if (getExecutableDir(out->toolDirStorage, sizeof(out->toolDirStorage))) { + char adjacentPath[1024]; + + snprintf(adjacentPath, sizeof(adjacentPath), "%s\\tex3ds.exe", out->toolDirStorage); + if (fileExists(adjacentPath)) { + snprintf(out->tex3dsExeStorage, sizeof(out->tex3dsExeStorage), "%s", adjacentPath); + out->tex3dsExe = out->tex3dsExeStorage; + } + } +#endif + + if (out->tex3dsExe == NULL) { + const char* devkitpro = getenv("DEVKITPRO"); + if (devkitpro != NULL && devkitpro[0] != '\0') { +#if N3DS_PREPROCESS_HOST_WINDOWS + snprintf(out->tex3dsExeStorage, sizeof(out->tex3dsExeStorage), "%s/tools/bin/tex3ds.exe", devkitpro); +#else + snprintf(out->tex3dsExeStorage, sizeof(out->tex3dsExeStorage), "%s/tools/bin/tex3ds", devkitpro); +#endif + out->tex3dsExe = out->tex3dsExeStorage; + } else { +#if N3DS_PREPROCESS_HOST_WINDOWS + out->tex3dsExe = "C:/devkitPro/tools/bin/tex3ds.exe"; +#else + out->tex3dsExe = "/opt/devkitpro/tools/bin/tex3ds"; +#endif + } + } + +} + +static bool parseArgs(int argc, char** argv, Options* out) { + setDefaultOptions(out); + + if (argc == 1) { + out->interactiveMode = true; + applyDefaultToolPaths(out); + return true; + } + + if (argc < 3) return false; + + out->inputPath = argv[1]; + out->outputDir = argv[2]; + out->pageFormatOverridesPath = NULL; + applyDefaultToolPaths(out); + + for (int i = 3; i < argc; ++i) { + if (strcmp(argv[i], "--keep-png") == 0) { + out->keepPng = true; + continue; + } + if (strcmp(argv[i], "--dump-page-previews") == 0) { + out->dumpPagePreviews = true; + continue; + } + if (strcmp(argv[i], "--l4-dialogue-battle-sprites") == 0) { + out->enableTargetedBattleDialogueMono = true; + continue; + } + if (strcmp(argv[i], "--page-format-overrides") == 0 && i + 1 < argc) { + out->pageFormatOverridesPath = argv[++i]; + out->dumpPagePreviews = true; + continue; + } + if (strcmp(argv[i], "--sprite-replacements") == 0 && i + 1 < argc) { + out->spriteReplacementDir = argv[++i]; + continue; + } + if (strcmp(argv[i], "--borders") == 0 && i + 1 < argc) { + out->borderAssetDir = argv[++i]; + continue; + } + if (strcmp(argv[i], "--atlas-page-mode") == 0 && i + 1 < argc) { + const char* mode = argv[++i]; + if (strcmp(mode, "auto") == 0) out->atlasPageMode = N3DS_ATLAS_PAGE_MODE_AUTO; + else if (strcmp(mode, "256") == 0) out->atlasPageMode = N3DS_ATLAS_PAGE_MODE_FORCE_256; + else if (strcmp(mode, "512") == 0) out->atlasPageMode = N3DS_ATLAS_PAGE_MODE_FORCE_512; + else { + fprintf(stderr, "Unknown atlas page mode: %s\n", mode); + return false; + } + continue; + } + if (strcmp(argv[i], "--texture-format") == 0 && i + 1 < argc) { + const char* format = argv[++i]; + if (strcmp(format, "etc1a4") == 0) out->textureFormat = N3DS_TEXFMT_ETC1A4; + else if (strcmp(format, "rgba5551") == 0) out->textureFormat = N3DS_TEXFMT_RGBA5551; + else if (strcmp(format, "indexed8") == 0) out->textureFormat = N3DS_TEXFMT_INDEXED8; + else if (strcmp(format, "hybrid") == 0) out->textureFormat = N3DS_TEXFMT_HYBRID; + else { + fprintf(stderr, "Unknown texture format: %s\n", format); + return false; + } + continue; + } + if (strcmp(argv[i], "--tex3ds") == 0 && i + 1 < argc) { + out->tex3dsExe = argv[++i]; + continue; + } + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + return false; + } + + return true; +} + +static bool ensureDir(const char* path) { + if (MKDIR(path) == 0) return true; + return errno == EEXIST; +} + +static bool ensureDirRecursive(const char* path) { + if (path == NULL || path[0] == '\0') return false; + if (ensureDir(path)) return true; + + char temp[1024]; + snprintf(temp, sizeof(temp), "%s", path); + size_t len = strlen(temp); + if (len == 0) return false; + + for (size_t i = 1; i < len; ++i) { + char c = temp[i]; + if (c != '/' && c != '\\') continue; + temp[i] = '\0'; + if (temp[0] != '\0' && !(i == 2 && temp[1] == ':') && !ensureDir(temp)) { + return false; + } + temp[i] = c; + } + + return ensureDir(temp); +} + +static bool ensureOutputDirs(const char* outputDir) { + if (!ensureDirRecursive(outputDir)) { + fprintf(stderr, "Failed to create output dir: %s\n", outputDir); + return false; + } + + char gfxDir[1024]; + snprintf(gfxDir, sizeof(gfxDir), "%s/gfx", outputDir); + if (!ensureDirRecursive(gfxDir)) { + fprintf(stderr, "Failed to create gfx dir: %s\n", gfxDir); + return false; + } + + char audioDir[1024]; + snprintf(audioDir, sizeof(audioDir), "%s/audio", outputDir); + if (!ensureDirRecursive(audioDir)) { + fprintf(stderr, "Failed to create audio dir: %s\n", audioDir); + return false; + } + + char spritesDir[1024]; + snprintf(spritesDir, sizeof(spritesDir), "%s/gfx/sprites", outputDir); + if (!ensureDirRecursive(spritesDir)) { + fprintf(stderr, "Failed to create sprites dir: %s\n", spritesDir); + return false; + } + + char backgroundsDir[1024]; + snprintf(backgroundsDir, sizeof(backgroundsDir), "%s/gfx/backgrounds", outputDir); + if (!ensureDirRecursive(backgroundsDir)) { + fprintf(stderr, "Failed to create backgrounds dir: %s\n", backgroundsDir); + return false; + } + + char bordersDir[1024]; + snprintf(bordersDir, sizeof(bordersDir), "%s/gfx/borders", outputDir); + if (!ensureDirRecursive(bordersDir)) { + fprintf(stderr, "Failed to create borders dir: %s\n", bordersDir); + return false; + } + + char fontsDir[1024]; + snprintf(fontsDir, sizeof(fontsDir), "%s/gfx/fonts", outputDir); + if (!ensureDirRecursive(fontsDir)) { + fprintf(stderr, "Failed to create fonts dir: %s\n", fontsDir); + return false; + } + return true; +} + +static bool getProgramDirPath(const char* argv0, char* outDir, size_t outDirSize) { + if (outDir == NULL || outDirSize == 0) return false; + outDir[0] = '\0'; + +#if N3DS_PREPROCESS_HOST_WINDOWS + if (getExecutableDir(outDir, outDirSize)) return true; +#else + char exePath[1024]; + ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (len > 0 && (size_t) len < sizeof(exePath)) { + exePath[len] = '\0'; + char* slash = strrchr(exePath, '/'); + if (slash != NULL) { + *slash = '\0'; + snprintf(outDir, outDirSize, "%s", exePath); + return true; + } + } +#endif + + if (argv0 != NULL && argv0[0] != '\0') { + char candidate[1024]; + snprintf(candidate, sizeof(candidate), "%s", argv0); + char* slash = strrchr(candidate, '/'); + char* backslash = strrchr(candidate, '\\'); + if (backslash != NULL && (slash == NULL || backslash > slash)) slash = backslash; + if (slash != NULL) { + *slash = '\0'; + if (candidate[0] != '\0') { + snprintf(outDir, outDirSize, "%s", candidate); + return true; + } + } + } + +#if N3DS_PREPROCESS_HOST_WINDOWS + DWORD cwdLen = GetCurrentDirectoryA((DWORD) outDirSize, outDir); + return cwdLen > 0 && cwdLen < outDirSize; +#else + return getcwd(outDir, outDirSize) != NULL; +#endif +} + +static bool filesAreIdentical(const char* pathA, const char* pathB) { + if (pathA == NULL || pathB == NULL) return false; + FILE* fileA = fopen(pathA, "rb"); + FILE* fileB = fopen(pathB, "rb"); + if (fileA == NULL || fileB == NULL) { + if (fileA != NULL) fclose(fileA); + if (fileB != NULL) fclose(fileB); + return false; + } + + bool identical = true; + uint8_t* bufferA = safeMalloc(64u * 1024u); + uint8_t* bufferB = safeMalloc(64u * 1024u); + + while (identical) { + size_t readA = fread(bufferA, 1, 64u * 1024u, fileA); + size_t readB = fread(bufferB, 1, 64u * 1024u, fileB); + if (readA != readB) { + identical = false; + break; + } + if (readA == 0) { + if (ferror(fileA) || ferror(fileB)) identical = false; + break; + } + if (memcmp(bufferA, bufferB, readA) != 0) { + identical = false; + break; + } + } + + free(bufferA); + free(bufferB); + fclose(fileA); + fclose(fileB); + return identical; +} + +static bool copyFileContents(const char* srcPath, const char* dstPath) { + if (srcPath == NULL || dstPath == NULL) return false; + + char* parentDir = dupParentDir(dstPath); + bool ok = ensureDirRecursive(parentDir); + free(parentDir); + if (!ok) return false; + + FILE* src = fopen(srcPath, "rb"); + if (src == NULL) return false; + FILE* dst = fopen(dstPath, "wb"); + if (dst == NULL) { + fclose(src); + return false; + } + + uint8_t* buffer = safeMalloc(64u * 1024u); + ok = true; + while (ok) { + size_t bytesRead = fread(buffer, 1, 64u * 1024u, src); + if (bytesRead > 0 && fwrite(buffer, 1, bytesRead, dst) != bytesRead) ok = false; + if (bytesRead < 64u * 1024u) { + if (ferror(src)) ok = false; + break; + } + } + + free(buffer); + fclose(src); + fclose(dst); + return ok; +} + +static bool syncDirectoryTree(const char* srcDir, const char* dstDir, uint32_t* outUpdatedCount, uint32_t* outSkippedCount) { + if (srcDir == NULL || dstDir == NULL) return false; + if (!directoryExists(srcDir)) return false; + if (!ensureDirRecursive(dstDir)) return false; + +#if N3DS_PREPROCESS_HOST_WINDOWS + char searchPath[1060]; + WIN32_FIND_DATAA findData; + snprintf(searchPath, sizeof(searchPath), "%s\\*", srcDir); + HANDLE findHandle = FindFirstFileA(searchPath, &findData); + if (findHandle == INVALID_HANDLE_VALUE) { + fprintf(stderr, "Failed to enumerate staged directory: %s\n", srcDir); + return false; + } + + bool ok = true; + do { + const char* name = findData.cFileName; + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; + + char srcChild[1060]; + char dstChild[1060]; + snprintf(srcChild, sizeof(srcChild), "%s/%s", srcDir, name); + snprintf(dstChild, sizeof(dstChild), "%s/%s", dstDir, name); + + if ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + if (!syncDirectoryTree(srcChild, dstChild, outUpdatedCount, outSkippedCount)) { + ok = false; + break; + } + continue; + } + + bool needsCopy = !fileExists(dstChild) || !filesAreIdentical(srcChild, dstChild); + if (needsCopy) { + if (!copyFileContents(srcChild, dstChild)) { + fprintf(stderr, "Failed to copy staged asset: %s -> %s\n", srcChild, dstChild); + ok = false; + break; + } + if (outUpdatedCount != NULL) (*outUpdatedCount)++; + } else if (outSkippedCount != NULL) { + (*outSkippedCount)++; + } + } while (FindNextFileA(findHandle, &findData)); + + FindClose(findHandle); + return ok; +#else + DIR* dir = opendir(srcDir); + if (dir == NULL) return false; + + bool ok = true; + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + const char* name = entry->d_name; + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; + + char srcChild[1060]; + char dstChild[1060]; + snprintf(srcChild, sizeof(srcChild), "%s/%s", srcDir, name); + snprintf(dstChild, sizeof(dstChild), "%s/%s", dstDir, name); + + if (directoryExists(srcChild)) { + if (!syncDirectoryTree(srcChild, dstChild, outUpdatedCount, outSkippedCount)) { + ok = false; + break; + } + continue; + } + + bool needsCopy = !fileExists(dstChild) || !filesAreIdentical(srcChild, dstChild); + if (needsCopy) { + if (!copyFileContents(srcChild, dstChild)) { + fprintf(stderr, "Failed to copy staged asset: %s -> %s\n", srcChild, dstChild); + ok = false; + break; + } + if (outUpdatedCount != NULL) (*outUpdatedCount)++; + } else if (outSkippedCount != NULL) { + (*outSkippedCount)++; + } + } + + closedir(dir); + return ok; +#endif +} + +static bool configureStagedOutputDir(Options* options, const char* argv0) { + if (options == NULL || options->outputDir == NULL || options->outputDir[0] == '\0') return false; + if (!options->stageOutputLocally) return true; + + char programDir[1024]; + if (!getProgramDirPath(argv0, programDir, sizeof(programDir))) return false; + + snprintf(options->finalOutputDirStorage, sizeof(options->finalOutputDirStorage), "%s", options->outputDir); +#if N3DS_PREPROCESS_HOST_WINDOWS + uint32_t pid = (uint32_t) GetCurrentProcessId(); +#else + uint32_t pid = (uint32_t) getpid(); +#endif + snprintf(options->stagingOutputDirStorage, sizeof(options->stagingOutputDirStorage), "%s/n3ds-preprocess-staging/run_%lu", programDir, (unsigned long) pid); + if (directoryExists(options->stagingOutputDirStorage) && !deleteDirectoryRecursive(options->stagingOutputDirStorage)) { + return false; + } + + options->outputDir = options->stagingOutputDirStorage; + return true; +} + +static bool configureSpriteReplacementDir(Options* options, const char* argv0) { + if (options == NULL) return false; + + if (options->spriteReplacementDir == NULL || options->spriteReplacementDir[0] == '\0') { + bool foundDefaultDir = false; + +#if defined(N3DS_PREPROCESS_TEXTURE_OVERRIDE_DIR) + snprintf( + options->spriteReplacementDirStorage, + sizeof(options->spriteReplacementDirStorage), + "%s", + N3DS_PREPROCESS_TEXTURE_OVERRIDE_DIR + ); + foundDefaultDir = directoryExists(options->spriteReplacementDirStorage); +#endif + + if (!foundDefaultDir) { + char programDir[1024]; + if (options->toolDirStorage[0] != '\0') { + snprintf(programDir, sizeof(programDir), "%s", options->toolDirStorage); + } else if (!getProgramDirPath(argv0, programDir, sizeof(programDir))) { + return false; + } + + snprintf( + options->spriteReplacementDirStorage, + sizeof(options->spriteReplacementDirStorage), + "%s/Sprite_replacements", + programDir + ); +#if defined(N3DS_PREPROCESS_SOURCE_DIR) + if (!directoryExists(options->spriteReplacementDirStorage)) { + char sourceReplacementDir[1200]; + snprintf( + sourceReplacementDir, + sizeof(sourceReplacementDir), + "%s/Sprite_replacements", + N3DS_PREPROCESS_SOURCE_DIR + ); + if (directoryExists(sourceReplacementDir)) { + snprintf( + options->spriteReplacementDirStorage, + sizeof(options->spriteReplacementDirStorage), + "%s", + sourceReplacementDir + ); + } + } +#endif + } + options->spriteReplacementDir = options->spriteReplacementDirStorage; + } + + options->spriteReplacementDirAvailable = directoryExists(options->spriteReplacementDir); + if (options->spriteReplacementDirAvailable) { + fprintf(stderr, "Using sprite replacements from: %s\n", options->spriteReplacementDir); + } + return true; +} + +static bool configureBorderAssetDir(Options* options, const char* argv0) { + if (options == NULL) return false; + + if (options->borderAssetDir == NULL || options->borderAssetDir[0] == '\0') { + char programDir[1024]; + if (options->toolDirStorage[0] != '\0') { + snprintf(programDir, sizeof(programDir), "%s", options->toolDirStorage); + } else if (!getProgramDirPath(argv0, programDir, sizeof(programDir))) { + return false; + } + + snprintf( + options->borderAssetDirStorage, + sizeof(options->borderAssetDirStorage), + "%s/Borders", + programDir + ); +#if defined(N3DS_PREPROCESS_SOURCE_DIR) + if (!directoryExists(options->borderAssetDirStorage)) { + char sourceBorderDir[1200]; + snprintf( + sourceBorderDir, + sizeof(sourceBorderDir), + "%s/Borders", + N3DS_PREPROCESS_SOURCE_DIR + ); + if (directoryExists(sourceBorderDir)) { + snprintf( + options->borderAssetDirStorage, + sizeof(options->borderAssetDirStorage), + "%s", + sourceBorderDir + ); + } + } +#endif + options->borderAssetDir = options->borderAssetDirStorage; + } + + options->borderAssetDirAvailable = directoryExists(options->borderAssetDir); + if (options->borderAssetDirAvailable) { + fprintf(stderr, "Using border assets from: %s\n", options->borderAssetDir); + } + return true; +} + +static bool syncStagedOutputToDestination(const Options* options) { + if (options == NULL || !options->stageOutputLocally) return false; + if (options->outputDir == NULL || options->outputDir[0] == '\0') return false; + if (options->finalOutputDirStorage[0] == '\0') return false; + + if (!ensureOutputDirs(options->finalOutputDirStorage)) return false; + + uint32_t updatedCount = 0; + uint32_t skippedCount = 0; + if (!syncDirectoryTree(options->outputDir, options->finalOutputDirStorage, &updatedCount, &skippedCount)) { + return false; + } + + fprintf( + stderr, + "Synced staged assets to %s (updated=%u, unchanged=%u)\n", + options->finalOutputDirStorage, + updatedCount, + skippedCount + ); + return true; +} + +static const char* getTex3dsFormatName(N3DSTextureFormat format) { + switch (format) { + case N3DS_TEXFMT_RGBA5551: return "rgba5551"; + case N3DS_TEXFMT_ETC1A4: return "etc1a4"; + case N3DS_TEXFMT_INDEXED8: return "rgba5551"; + case N3DS_TEXFMT_HYBRID: return "etc1a4"; + case N3DS_TEXFMT_L4: return "l4"; + case N3DS_TEXFMT_LA4: return "la4"; + default: return "etc1a4"; + } +} + +static const char* getPageExtension(N3DSTextureFormat format) { + return format == N3DS_TEXFMT_INDEXED8 ? "i8" : "t3x"; +} + +static const char* getTextureFormatLabel(N3DSTextureFormat format) { + switch (format) { + case N3DS_TEXFMT_RGBA5551: return "rgba5551"; + case N3DS_TEXFMT_ETC1A4: return "etc1a4"; + case N3DS_TEXFMT_INDEXED8: return "indexed8"; + case N3DS_TEXFMT_HYBRID: return "hybrid"; + case N3DS_TEXFMT_L4: return "l4"; + case N3DS_TEXFMT_LA4: return "la4"; + default: return "unknown"; + } +} + +static bool tryParseTextureFormatLabel(const char* label, N3DSTextureFormat* outFormat) { + if (label == NULL || outFormat == NULL) return false; + if (strcmp(label, "rgba5551") == 0) { *outFormat = N3DS_TEXFMT_RGBA5551; return true; } + if (strcmp(label, "etc1a4") == 0) { *outFormat = N3DS_TEXFMT_ETC1A4; return true; } + if (strcmp(label, "indexed8") == 0) { *outFormat = N3DS_TEXFMT_INDEXED8; return true; } + if (strcmp(label, "l4") == 0) { *outFormat = N3DS_TEXFMT_L4; return true; } + if (strcmp(label, "la4") == 0) { *outFormat = N3DS_TEXFMT_LA4; return true; } + if (strcmp(label, "hybrid") == 0) { *outFormat = N3DS_TEXFMT_HYBRID; return true; } + return false; +} + +static void sanitizeLabel(char* dst, size_t dstSize, const char* src) { + if (dst == NULL || dstSize == 0) return; + dst[0] = '\0'; + if (src == NULL || src[0] == '\0') { + snprintf(dst, dstSize, "unnamed"); + return; + } + + size_t written = 0; + for (size_t i = 0; src[i] != '\0' && written + 1 < dstSize; ++i) { + char c = src[i]; + bool alnum = (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9'); + if (alnum) { + dst[written++] = c; + } else if (written > 0 && dst[written - 1] != '_') { + dst[written++] = '_'; + } + if (written >= 40) break; + } + while (written > 0 && dst[written - 1] == '_') written--; + if (written == 0) snprintf(dst, dstSize, "unnamed"); + else dst[written] = '\0'; +} + +static void appendDebugName(char* dst, size_t dstSize, const char* name) { + if (dst == NULL || dstSize == 0 || name == NULL || name[0] == '\0') return; + if (strstr(dst, name) != NULL) return; + size_t len = strlen(dst); + if (len > 0) { + snprintf(dst + len, dstSize - len, ", %s", name); + } else { + snprintf(dst, dstSize, "%s", name); + } +} + +static void appendPageDebugName(OutputPage* page, const char* name) { + if (page == NULL || name == NULL || name[0] == '\0') return; + appendDebugName(page->debugNames, sizeof(page->debugNames), name); + if (page->previewLabel[0] == '\0') { + sanitizeLabel(page->previewLabel, sizeof(page->previewLabel), name); + } +} + +static bool containsIgnoreCase(const char* haystack, const char* needle) { + if (haystack == NULL || needle == NULL || needle[0] == '\0') return false; + size_t haystackLen = strlen(haystack); + size_t needleLen = strlen(needle); + if (needleLen > haystackLen) return false; + for (size_t i = 0; i + needleLen <= haystackLen; ++i) { + size_t j = 0; + while (j < needleLen) { + char a = haystack[i + j]; + char b = needle[j]; + if (a >= 'A' && a <= 'Z') a = (char) (a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = (char) (b - 'A' + 'a'); + if (a != b) break; + ++j; + } + if (j == needleLen) return true; + } + return false; +} + +static N3DSTextureFormat chooseHybridPageFormat(const PackedPage* packedPage) { + if (packedPage == NULL || packedPage->pixels == NULL) return N3DS_TEXFMT_RGBA5551; + + ColorFreqMap* uniqueColors = NULL; + uint32_t hardAlphaPixels = 0; + uint64_t edgeEnergy = 0; + uint32_t width = packedPage->width; + uint32_t height = packedPage->height; + + repeat((size_t) width * (size_t) height, i) { + const uint8_t* px = packedPage->pixels + (i * 4u); + uint32_t colorKey = + ((uint32_t) px[0] << 24) | + ((uint32_t) px[1] << 16) | + ((uint32_t) px[2] << 8) | + (uint32_t) px[3]; + if (hmgeti(uniqueColors, colorKey) < 0) { + hmput(uniqueColors, colorKey, 1u); + } + if (px[3] > 0u && px[3] < 255u) hardAlphaPixels++; + } + + repeat(height, y) { + repeat(width, x) { + size_t idx = ((size_t) y * width + x) * 4u; + const uint8_t* px = packedPage->pixels + idx; + if (x + 1u < width) { + const uint8_t* right = px + 4u; + edgeEnergy += (uint64_t) abs((int) px[0] - (int) right[0]); + edgeEnergy += (uint64_t) abs((int) px[1] - (int) right[1]); + edgeEnergy += (uint64_t) abs((int) px[2] - (int) right[2]); + edgeEnergy += (uint64_t) abs((int) px[3] - (int) right[3]); + } + if (y + 1u < height) { + const uint8_t* down = packedPage->pixels + idx + ((size_t) width * 4u); + edgeEnergy += (uint64_t) abs((int) px[0] - (int) down[0]); + edgeEnergy += (uint64_t) abs((int) px[1] - (int) down[1]); + edgeEnergy += (uint64_t) abs((int) px[2] - (int) down[2]); + edgeEnergy += (uint64_t) abs((int) px[3] - (int) down[3]); + } + } + } + + uint32_t uniqueCount = (uint32_t) hmlen(uniqueColors); + hmfree(uniqueColors); + + uint64_t pixelCount = (uint64_t) width * (uint64_t) height; + uint64_t avgEdgeEnergy = pixelCount > 0u ? edgeEnergy / pixelCount : 0u; + + if (hardAlphaPixels > (pixelCount / 64u)) return N3DS_TEXFMT_RGBA5551; + if (uniqueCount <= 192u) return N3DS_TEXFMT_RGBA5551; + if (avgEdgeEnergy >= 52u) return N3DS_TEXFMT_RGBA5551; + return N3DS_TEXFMT_ETC1A4; +} + +static bool spriteLooksLikeDialoguePortrait(const char* spriteName) { + return containsIgnoreCase(spriteName, "face") || + containsIgnoreCase(spriteName, "portrait") || + containsIgnoreCase(spriteName, "mug") || + containsIgnoreCase(spriteName, "dialog") || + containsIgnoreCase(spriteName, "dial") || + containsIgnoreCase(spriteName, "textbox"); +} + +static bool* collectFontTPAGs(const DataWin* dataWin) { + if (dataWin == NULL) return NULL; + bool* fontTPAGs = safeCalloc(dataWin->tpag.count > 0 ? dataWin->tpag.count : 1u, sizeof(bool)); + repeat(dataWin->font.count, i) { + int32_t tpagIndex = dataWin->font.fonts[i].tpagIndex; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) continue; + fontTPAGs[tpagIndex] = true; + } + return fontTPAGs; +} + +static bool* collectBackgroundTPAGs(const DataWin* dataWin) { + if (dataWin == NULL) return NULL; + bool* backgroundTPAGs = safeCalloc(dataWin->tpag.count > 0 ? dataWin->tpag.count : 1u, sizeof(bool)); + repeat(dataWin->bgnd.count, i) { + int32_t tpagIndex = dataWin->bgnd.backgrounds[i].tpagIndex; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) continue; + backgroundTPAGs[tpagIndex] = true; + } + return backgroundTPAGs; +} + +static char** collectTPAGDebugNames(DataWin* dataWin) { + if (dataWin == NULL) return NULL; + char** names = safeCalloc(dataWin->tpag.count > 0 ? dataWin->tpag.count : 1u, sizeof(char*)); + + repeat(dataWin->sprt.count, i) { + Sprite* sprite = &dataWin->sprt.sprites[i]; + repeat(sprite->textureCount, j) { + int32_t tpagIndex = sprite->tpagIndices[j]; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) continue; + if (names[tpagIndex] == NULL) names[tpagIndex] = safeCalloc(1u, 2048u); + appendDebugName(names[tpagIndex], 2048u, sprite->name); + } + } + + repeat(dataWin->bgnd.count, i) { + Background* bg = &dataWin->bgnd.backgrounds[i]; + if (bg->tpagIndex < 0 || (uint32_t) bg->tpagIndex >= dataWin->tpag.count) continue; + if (names[bg->tpagIndex] == NULL) names[bg->tpagIndex] = safeCalloc(1u, 2048u); + appendDebugName(names[bg->tpagIndex], 2048u, bg->name); + } + + return names; +} + +static void freeTPAGDebugNames(char** names, uint32_t count) { + if (names == NULL) return; + repeat(count, i) free(names[i]); + free(names); +} + +static int32_t* buildTPAGToSpriteIndexMap(const DataWin* dataWin) { + if (dataWin == NULL) return NULL; + size_t mapCount = dataWin->tpag.count > 0 ? dataWin->tpag.count : 1u; + int32_t* map = safeMalloc(mapCount * sizeof(int32_t)); + repeat(mapCount, i) map[i] = -1; + + repeat(dataWin->sprt.count, spriteIndex) { + const Sprite* sprite = &dataWin->sprt.sprites[spriteIndex]; + repeat(sprite->textureCount, frameIndex) { + int32_t tpagIndex = sprite->tpagIndices[frameIndex]; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) continue; + if (map[tpagIndex] < 0) map[tpagIndex] = (int32_t) spriteIndex; + } + } + return map; +} + +static int32_t* buildTPAGToSpriteFrameMap(const DataWin* dataWin) { + if (dataWin == NULL) return NULL; + size_t mapCount = dataWin->tpag.count > 0 ? dataWin->tpag.count : 1u; + int32_t* map = safeMalloc(mapCount * sizeof(int32_t)); + repeat(mapCount, i) map[i] = -1; + + repeat(dataWin->sprt.count, spriteIndex) { + const Sprite* sprite = &dataWin->sprt.sprites[spriteIndex]; + repeat(sprite->textureCount, frameIndex) { + int32_t tpagIndex = sprite->tpagIndices[frameIndex]; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) continue; + if (map[tpagIndex] < 0) map[tpagIndex] = (int32_t) frameIndex; + } + } + return map; +} + +static int32_t* buildTPAGToBackgroundIndexMap(const DataWin* dataWin) { + if (dataWin == NULL) return NULL; + size_t mapCount = dataWin->tpag.count > 0 ? dataWin->tpag.count : 1u; + int32_t* map = safeMalloc(mapCount * sizeof(int32_t)); + repeat(mapCount, i) map[i] = -1; + + repeat(dataWin->bgnd.count, bgIndex) { + int32_t tpagIndex = dataWin->bgnd.backgrounds[bgIndex].tpagIndex; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) continue; + if (map[tpagIndex] < 0) map[tpagIndex] = (int32_t) bgIndex; + } + return map; +} + +static int32_t* buildTPAGToFontIndexMap(const DataWin* dataWin) { + if (dataWin == NULL) return NULL; + size_t mapCount = dataWin->tpag.count > 0 ? dataWin->tpag.count : 1u; + int32_t* map = safeMalloc(mapCount * sizeof(int32_t)); + repeat(mapCount, i) map[i] = -1; + + repeat(dataWin->font.count, fontIndex) { + int32_t tpagIndex = dataWin->font.fonts[fontIndex].tpagIndex; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) continue; + if (map[tpagIndex] < 0) map[tpagIndex] = (int32_t) fontIndex; + } + return map; +} + +static int32_t* loadPageFormatOverrides(const char* path, uint32_t pageCount) { + if (path == NULL || pageCount == 0) return NULL; + FILE* file = fopen(path, "rb"); + if (file == NULL) { + fprintf(stderr, "n3ds-preprocess: could not open page format overrides: %s\n", path); + return NULL; + } + + int32_t* overrides = safeMalloc(sizeof(int32_t) * pageCount); + repeat(pageCount, i) overrides[i] = -1; + + char line[512]; + while (fgets(line, sizeof(line), file) != NULL) { + char* cursor = line; + while (*cursor == ' ' || *cursor == '\t') cursor++; + if (*cursor == '#' || *cursor == ';' || *cursor == '\r' || *cursor == '\n' || *cursor == '\0') continue; + + unsigned int pageIndex = 0; + char formatName[64] = {0}; + if (sscanf(cursor, "page_%u = %63s", &pageIndex, formatName) != 2 && + sscanf(cursor, "page_%u:%63s", &pageIndex, formatName) != 2 && + sscanf(cursor, "%u = %63s", &pageIndex, formatName) != 2 && + sscanf(cursor, "%u:%63s", &pageIndex, formatName) != 2) { + continue; + } + + size_t formatLen = strlen(formatName); + while (formatLen > 0 && (formatName[formatLen - 1] == '\r' || formatName[formatLen - 1] == '\n')) { + formatName[--formatLen] = '\0'; + } + + N3DSTextureFormat parsedFormat; + if (pageIndex < pageCount && tryParseTextureFormatLabel(formatName, &parsedFormat)) { + overrides[pageIndex] = (int32_t) parsedFormat; + } + } + + fclose(file); + return overrides; +} + +static bool writePageFormatTemplate(const Options* options, const OutputPage* outputPages, uint32_t packedPageCount) { + if (options == NULL || outputPages == NULL) return false; + char templatePath[1024]; + snprintf(templatePath, sizeof(templatePath), "%s/gfx/page_formats.txt", options->outputDir); + FILE* file = fopen(templatePath, "wb"); + if (file == NULL) return false; + + fprintf(file, "# Edit the format on the right and pass this file back with --page-format-overrides\n"); + fprintf(file, "# Supported formats: rgba5551, etc1a4, indexed8, l4, la4\n"); + repeat(packedPageCount, i) { + const OutputPage* page = &outputPages[i]; + fprintf( + file, + "page_%03lu = %s ; %s\n", + (unsigned long) i, + getTextureFormatLabel((N3DSTextureFormat) page->textureFormat), + page->debugNames[0] != '\0' ? page->debugNames : "unnamed" + ); + } + fclose(file); + return true; +} + +static bool pageCanUseMonoFormat(const OutputPage* page) { + if (page == NULL) return false; + return !page->containsFont && !page->containsBackground && !page->containsNonMonoContent; +} + +static bool pageShouldForceRGBA5551(const OutputPage* page) { + if (page == NULL) return false; + if (page->containsSprite) return true; + if (page->containsBackground) return true; + const char* names = page->debugNames; + if (names == NULL || names[0] == '\0') return false; + + return + containsIgnoreCase(names, "spr_mainchara") || + containsIgnoreCase(names, "spr_f_mainchara") || + containsIgnoreCase(names, "spr_chara") || + containsIgnoreCase(names, "spr_heart") || + containsIgnoreCase(names, "heart_") || + containsIgnoreCase(names, "spr_soul") || + containsIgnoreCase(names, "soul"); +} + +static bool spriteShouldForceDirectRGBA5551(const char* spriteName) { + if (spriteName == NULL || spriteName[0] == '\0') return false; + return + containsIgnoreCase(spriteName, "spr_mainchara") || + containsIgnoreCase(spriteName, "spr_f_mainchara") || + containsIgnoreCase(spriteName, "spr_chara") || + containsIgnoreCase(spriteName, "spr_heart") || + containsIgnoreCase(spriteName, "heart_") || + containsIgnoreCase(spriteName, "spr_soul") || + containsIgnoreCase(spriteName, "soul"); +} + +static bool objectLooksLikeBattleUI(const char* objectName) { + return containsIgnoreCase(objectName, "battlecontroller") || + containsIgnoreCase(objectName, "writer") || + containsIgnoreCase(objectName, "border") || + containsIgnoreCase(objectName, "button") || + containsIgnoreCase(objectName, "blcon") || + containsIgnoreCase(objectName, "bullet") || + containsIgnoreCase(objectName, "soul") || + containsIgnoreCase(objectName, "heart") || + containsIgnoreCase(objectName, "menu") || + containsIgnoreCase(objectName, "target") || + containsIgnoreCase(objectName, "cursor") || + containsIgnoreCase(objectName, "slash"); +} + +static bool roomLooksLikeBattle(const Room* room, const DataWin* dataWin) { + if (room == NULL || dataWin == NULL) return false; + if (containsIgnoreCase(room->name, "battle")) return true; + repeat(room->gameObjectCount, i) { + int32_t objectIndex = room->gameObjects[i].objectDefinition; + if (objectIndex < 0 || (uint32_t) objectIndex >= dataWin->objt.count) continue; + const char* objectName = dataWin->objt.objects[objectIndex].name; + if (containsIgnoreCase(objectName, "battlecontroller")) return true; + } + return false; +} + +static void markSpriteTPAGs(const DataWin* dataWin, int32_t spriteIndex, bool* targetTPAGs) { + if (dataWin == NULL || targetTPAGs == NULL) return; + if (spriteIndex < 0 || (uint32_t) spriteIndex >= dataWin->sprt.count) return; + Sprite* sprite = &dataWin->sprt.sprites[spriteIndex]; + repeat(sprite->textureCount, i) { + int32_t tpagIndex = sprite->tpagIndices[i]; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) continue; + targetTPAGs[tpagIndex] = true; + } +} + +static bool* collectTargetedDialogueBattleTPAGs(DataWin* dataWin) { + if (dataWin == NULL) return NULL; + bool* targetTPAGs = safeCalloc(dataWin->tpag.count > 0 ? dataWin->tpag.count : 1u, sizeof(bool)); + + repeat(dataWin->sprt.count, i) { + if (spriteLooksLikeDialoguePortrait(dataWin->sprt.sprites[i].name)) { + markSpriteTPAGs(dataWin, (int32_t) i, targetTPAGs); + } + } + + repeat(dataWin->room.count, i) { + DataWin_loadRoomPayload(dataWin, (int32_t) i); + Room* room = &dataWin->room.rooms[i]; + if (!roomLooksLikeBattle(room, dataWin)) continue; + + repeat(room->gameObjectCount, objIndex) { + RoomGameObject* roomObject = &room->gameObjects[objIndex]; + if (roomObject->objectDefinition < 0 || (uint32_t) roomObject->objectDefinition >= dataWin->objt.count) continue; + GameObject* object = &dataWin->objt.objects[roomObject->objectDefinition]; + if (object->spriteId < 0 || objectLooksLikeBattleUI(object->name)) continue; + markSpriteTPAGs(dataWin, object->spriteId, targetTPAGs); + } + + repeat(room->layerCount, layerIndex) { + RoomLayer* layer = &room->layers[layerIndex]; + if (layer->assetsData == NULL) continue; + repeat(layer->assetsData->spriteCount, spriteSlot) { + int32_t spriteIndex = layer->assetsData->sprites[spriteSlot].spriteIndex; + markSpriteTPAGs(dataWin, spriteIndex, targetTPAGs); + } + } + } + + return targetTPAGs; +} + +static N3DSTextureFormat chooseTargetedMonoItemFormat( + const uint8_t* rgba, + uint32_t stride, + const TexturePageItem* item +) { + if (rgba == NULL || item == NULL || item->sourceWidth == 0 || item->sourceHeight == 0) return N3DS_TEXFMT_RGBA5551; + + uint32_t visiblePixels = 0; + uint32_t tintedPixels = 0; + bool needsAlpha = false; + + repeat(item->sourceHeight, y) { + repeat(item->sourceWidth, x) { + const uint8_t* px = rgba + ((((size_t) item->sourceY + y) * stride + item->sourceX + x) * 4u); + uint8_t alpha = px[3]; + if (alpha == 0u) { + needsAlpha = true; + continue; + } + visiblePixels++; + if (alpha < 255u) needsAlpha = true; + uint8_t maxRgb = px[0]; + if (px[1] > maxRgb) maxRgb = px[1]; + if (px[2] > maxRgb) maxRgb = px[2]; + uint8_t minRgb = px[0]; + if (minRgb > px[1]) minRgb = px[1]; + if (minRgb > px[2]) minRgb = px[2]; + if ((uint8_t) (maxRgb - minRgb) > 18u) tintedPixels++; + } + } + + if (visiblePixels == 0u) return N3DS_TEXFMT_RGBA5551; + if (tintedPixels > (visiblePixels / 20u)) return N3DS_TEXFMT_RGBA5551; + return needsAlpha ? N3DS_TEXFMT_LA4 : N3DS_TEXFMT_L4; +} + +static bool pixelsLookMonochrome(const uint8_t* rgba, uint32_t width, uint32_t height) { + if (rgba == NULL || width == 0 || height == 0) return false; + + uint32_t visiblePixels = 0; + uint32_t tintedPixels = 0; + + repeat(height, y) { + repeat(width, x) { + const uint8_t* px = rgba + ((((size_t) y * width) + x) * 4u); + if (px[3] == 0u) continue; + + visiblePixels++; + + uint8_t maxRgb = px[0]; + if (px[1] > maxRgb) maxRgb = px[1]; + if (px[2] > maxRgb) maxRgb = px[2]; + + uint8_t minRgb = px[0]; + if (minRgb > px[1]) minRgb = px[1]; + if (minRgb > px[2]) minRgb = px[2]; + + if ((uint8_t) (maxRgb - minRgb) > 18u) tintedPixels++; + } + } + + if (visiblePixels == 0u) return false; + return tintedPixels <= (visiblePixels / 20u); +} + +static bool runTex3ds(const char* tex3dsExe, const char* pngPath, const char* t3xPath, N3DSTextureFormat format) { +#if N3DS_PREPROCESS_HOST_WINDOWS + char tex3dsExeNormalized[1024]; + char pngPathNormalized[1024]; + char t3xPathNormalized[1024]; + char command[4096]; + + snprintf(tex3dsExeNormalized, sizeof(tex3dsExeNormalized), "%s", tex3dsExe); + snprintf(pngPathNormalized, sizeof(pngPathNormalized), "%s", pngPath); + snprintf(t3xPathNormalized, sizeof(t3xPathNormalized), "%s", t3xPath); + for (char* cursor = tex3dsExeNormalized; *cursor != '\0'; ++cursor) { + if (*cursor == '/') *cursor = '\\'; + } + for (char* cursor = pngPathNormalized; *cursor != '\0'; ++cursor) { + if (*cursor == '/') *cursor = '\\'; + } + for (char* cursor = t3xPathNormalized; *cursor != '\0'; ++cursor) { + if (*cursor == '/') *cursor = '\\'; + } + + snprintf( + command, + sizeof(command), + "\"%s\" -f %s -z none -o \"%s\" \"%s\"", + tex3dsExeNormalized, + getTex3dsFormatName(format), + t3xPathNormalized, + pngPathNormalized + ); + intptr_t rc = spawnl( + _P_WAIT, + tex3dsExeNormalized, + tex3dsExeNormalized, + "-f", + getTex3dsFormatName(format), + "-z", + "none", + "-o", + t3xPathNormalized, + pngPathNormalized, + NULL + ); + if (rc != 0) { + fprintf(stderr, "tex3ds failed (%ld): %s\n", (long) rc, command); + return false; + } + return true; +#else + char command[4096]; + snprintf( + command, + sizeof(command), + "\"%s\" -f %s -z none -o \"%s\" \"%s\"", + tex3dsExe, + getTex3dsFormatName(format), + t3xPath, + pngPath + ); + int rc = system(command); + if (rc != 0) { + fprintf(stderr, "tex3ds failed (%d): %s\n", rc, command); + return false; + } + return true; +#endif +} + +static bool runTex3dsAtlas(const char* tex3dsExe, const char* pngGlobPath, const char* t3xPath, N3DSTextureFormat format) { +#if N3DS_PREPROCESS_HOST_WINDOWS + char tex3dsExeNormalized[1024]; + char pngGlobPathNormalized[1024]; + char t3xPathNormalized[1024]; + char command[4096]; + + snprintf(tex3dsExeNormalized, sizeof(tex3dsExeNormalized), "%s", tex3dsExe); + snprintf(pngGlobPathNormalized, sizeof(pngGlobPathNormalized), "%s", pngGlobPath); + snprintf(t3xPathNormalized, sizeof(t3xPathNormalized), "%s", t3xPath); + for (char* cursor = tex3dsExeNormalized; *cursor != '\0'; ++cursor) { + if (*cursor == '/') *cursor = '\\'; + } + for (char* cursor = pngGlobPathNormalized; *cursor != '\0'; ++cursor) { + if (*cursor == '/') *cursor = '\\'; + } + for (char* cursor = t3xPathNormalized; *cursor != '\0'; ++cursor) { + if (*cursor == '/') *cursor = '\\'; + } + + snprintf( + command, + sizeof(command), + "\"%s\" --atlas -f %s -z none -o \"%s\" \"%s\"", + tex3dsExeNormalized, + getTex3dsFormatName(format), + t3xPathNormalized, + pngGlobPathNormalized + ); + intptr_t rc = spawnl( + _P_WAIT, + tex3dsExeNormalized, + tex3dsExeNormalized, + "--atlas", + "-f", + getTex3dsFormatName(format), + "-z", + "none", + "-o", + t3xPathNormalized, + pngGlobPathNormalized, + NULL + ); + if (rc != 0) { + fprintf(stderr, "tex3ds atlas failed (%ld): %s\n", (long) rc, command); + return false; + } + return true; +#else + char command[4096]; + snprintf( + command, + sizeof(command), + "\"%s\" --atlas -f %s -z none -o \"%s\" \"%s\"", + tex3dsExe, + getTex3dsFormatName(format), + t3xPath, + pngGlobPath + ); + int rc = system(command); + if (rc != 0) { + fprintf(stderr, "tex3ds atlas failed (%d): %s\n", rc, command); + return false; + } + return true; +#endif +} + +static uint16_t rgbaToRgb5551(const uint8_t* rgba) { + uint16_t r = (uint16_t) (rgba[0] >> 3); + uint16_t g = (uint16_t) (rgba[1] >> 3); + uint16_t b = (uint16_t) (rgba[2] >> 3); + uint16_t a = rgba[3] >= 128 ? 1u : 0u; + return (uint16_t) ((r << 11) | (g << 6) | (b << 1) | a); +} + +static int compareColorFreqDesc(const void* lhs, const void* rhs) { + const ColorFreqMap* a = (const ColorFreqMap*) lhs; + const ColorFreqMap* b = (const ColorFreqMap*) rhs; + if (a->value < b->value) return 1; + if (a->value > b->value) return -1; + if (a->key < b->key) return -1; + if (a->key > b->key) return 1; + return 0; +} + +static int colorDistanceRgb5551(uint16_t a, uint16_t b) { + int ar = (a >> 11) & 0x1F; + int ag = (a >> 6) & 0x1F; + int ab = (a >> 1) & 0x1F; + int aa = a & 0x1; + int br = (b >> 11) & 0x1F; + int bg = (b >> 6) & 0x1F; + int bb = (b >> 1) & 0x1F; + int ba = b & 0x1; + int dr = ar - br; + int dg = ag - bg; + int db = ab - bb; + int da = aa - ba; + return (dr * dr * 3) + (dg * dg * 4) + (db * db * 2) + (da * da * 2048); +} + +static uint8_t findNearestPaletteIndex(uint16_t color, const uint16_t* palette, uint32_t paletteCount) { + uint32_t bestIndex = 0; + int bestDistance = INT32_MAX; + repeat(paletteCount, i) { + int distance = colorDistanceRgb5551(color, palette[i]); + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = (uint32_t) i; + if (distance == 0) break; + } + } + return (uint8_t) bestIndex; +} + +static bool writeIndexedPage(const char* outputPath, const PackedPage* packedPage) { + if (outputPath == NULL || packedPage == NULL || packedPage->pixels == NULL) return false; + + size_t pixelCount = (size_t) packedPage->width * (size_t) packedPage->height; + ColorFreqMap* colorFreqs = NULL; + bool hasTransparent = false; + + repeat(pixelCount, i) { + const uint8_t* px = packedPage->pixels + (i * 4u); + uint16_t color = rgbaToRgb5551(px); + if ((color & 0x1u) == 0u) hasTransparent = true; + ptrdiff_t existing = hmgeti(colorFreqs, color); + if (existing >= 0) colorFreqs[existing].value++; + else hmput(colorFreqs, color, 1u); + } + + size_t freqCount = arrlen(colorFreqs); + qsort(colorFreqs, freqCount, sizeof(ColorFreqMap), compareColorFreqDesc); + + uint16_t palette[256] = {0}; + uint32_t paletteCount = 0; + if (hasTransparent) palette[paletteCount++] = 0u; + + repeat(freqCount, i) { + uint16_t color = (uint16_t) colorFreqs[i].key; + if (hasTransparent && color == 0u) continue; + if (paletteCount >= 256u) break; + palette[paletteCount++] = color; + } + if (paletteCount == 0u) palette[paletteCount++] = 0u; + + size_t blobSize = 256u * sizeof(uint16_t) + pixelCount; + uint8_t* blob = safeMalloc(blobSize); + memset(blob, 0, blobSize); + repeat(256u, i) { + uint16_t color = palette[i]; + blob[i * 2u + 0u] = (uint8_t) (color & 0xFFu); + blob[i * 2u + 1u] = (uint8_t) ((color >> 8) & 0xFFu); + } + + uint8_t* indices = blob + (256u * sizeof(uint16_t)); + repeat(pixelCount, i) { + const uint8_t* px = packedPage->pixels + (i * 4u); + uint16_t color = rgbaToRgb5551(px); + indices[i] = findNearestPaletteIndex(color, palette, paletteCount); + } + + FILE* file = fopen(outputPath, "wb"); + if (file == NULL) { + fprintf(stderr, "Failed to open indexed page output: %s\n", outputPath); + hmfree(colorFreqs); + free(blob); + return false; + } + + fwrite(blob, 1, blobSize, file); + fclose(file); + hmfree(colorFreqs); + free(blob); + return true; +} + +static bool getFileSize32(const char* path, uint32_t* outSize) { + if (path == NULL || outSize == NULL) return false; + + FILE* file = fopen(path, "rb"); + if (file == NULL) return false; + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fclose(file); + if (size < 0) return false; + + *outSize = (uint32_t) size; + return true; +} + +static bool appendFileToStream(FILE* dst, const char* srcPath) { + if (dst == NULL || srcPath == NULL) return false; + + FILE* src = fopen(srcPath, "rb"); + if (src == NULL) return false; + + uint8_t* buffer = safeMalloc(64u * 1024u); + bool ok = true; + while (ok) { + size_t bytesRead = fread(buffer, 1, 64u * 1024u, src); + if (bytesRead > 0 && fwrite(buffer, 1, bytesRead, dst) != bytesRead) ok = false; + if (bytesRead < 64u * 1024u) { + if (ferror(src)) ok = false; + break; + } + } + + free(buffer); + fclose(src); + return ok; +} + +static bool directAssetPackContainsPath(DirectAssetPackEntry* entries, const char* relativePath) { + if (relativePath == NULL) return false; + size_t entryCount = arrlenu(entries); + repeat(entryCount, i) { + if (strcmp(entries[i].relativePath, relativePath) == 0) return true; + } + return false; +} + +static bool appendDirectAssetPackEntry(const Options* options, const char* relativePath, DirectAssetPackEntry** entries) { + if (options == NULL || relativePath == NULL || entries == NULL) return false; + if (directAssetPackContainsPath(*entries, relativePath)) return true; + + DirectAssetPackEntry entry = {0}; + snprintf(entry.relativePath, sizeof(entry.relativePath), "%s", relativePath); + snprintf(entry.sourcePath, sizeof(entry.sourcePath), "%s/gfx/%s", options->outputDir, relativePath); + if (!fileExists(entry.sourcePath)) return true; + if (!getFileSize32(entry.sourcePath, &entry.dataSize)) { + fprintf(stderr, "Failed to get direct asset size: %s\n", entry.sourcePath); + return false; + } + + arrput(*entries, entry); + return true; +} + +static bool collectDirectAssetPackEntries(const Options* options, const DataWin* dataWin, DirectAssetPackEntry** outEntries) { + if (options == NULL || dataWin == NULL || outEntries == NULL) return false; + + DirectAssetPackEntry* entries = NULL; + char relativePath[320]; + + repeat(dataWin->sprt.count, spriteIndex) { + const Sprite* sprite = &dataWin->sprt.sprites[spriteIndex]; + if (sprite->textureCount <= 0) continue; + + snprintf(relativePath, sizeof(relativePath), "sprites/spr_%05lu.t3x", (unsigned long) spriteIndex); + if (!appendDirectAssetPackEntry(options, relativePath, &entries)) { + arrfree(entries); + return false; + } + + repeat(sprite->textureCount, frameIndex) { + snprintf( + relativePath, + sizeof(relativePath), + "sprites/spr_%05lu_frame_%05zu.t3x", + (unsigned long) spriteIndex, + frameIndex + ); + if (!appendDirectAssetPackEntry(options, relativePath, &entries)) { + arrfree(entries); + return false; + } + } + } + + repeat(dataWin->bgnd.count, bgIndex) { + snprintf(relativePath, sizeof(relativePath), "backgrounds/bg_%05lu.t3x", (unsigned long) bgIndex); + if (!appendDirectAssetPackEntry(options, relativePath, &entries)) { + arrfree(entries); + return false; + } + } + + repeat(dataWin->font.count, fontIndex) { + snprintf(relativePath, sizeof(relativePath), "fonts/font_%05lu.t3x", (unsigned long) fontIndex); + if (!appendDirectAssetPackEntry(options, relativePath, &entries)) { + arrfree(entries); + return false; + } + } + + *outEntries = entries; + return true; +} + +static bool writePackedDirectTextureAssets(const Options* options, const DataWin* dataWin) { + if (options == NULL || dataWin == NULL) return false; + + DirectAssetPackEntry* entries = NULL; + if (!collectDirectAssetPackEntries(options, dataWin, &entries)) { + return false; + } + + char outputPath[1024]; + snprintf(outputPath, sizeof(outputPath), "%s/gfx/direct_assets.bin", options->outputDir); + + uint32_t entryCount = (uint32_t) arrlen(entries); + if (entryCount == 0u) { + remove(outputPath); + arrfree(entries); + return true; + } + + uint32_t stringTableSize = 0u; + repeat(entryCount, i) { + entries[i].pathOffset = stringTableSize; + stringTableSize += (uint32_t) strlen(entries[i].relativePath) + 1u; + } + + uint32_t metadataSize = 16u + entryCount * 16u + stringTableSize; + uint32_t dataOffset = metadataSize; + repeat(entryCount, i) { + entries[i].dataOffset = dataOffset; + dataOffset += entries[i].dataSize; + } + + FILE* file = fopen(outputPath, "wb"); + if (file == NULL) { + fprintf(stderr, "Failed to open packed direct asset output: %s\n", outputPath); + arrfree(entries); + return false; + } + + uint8_t header[16]; + writeLe32(header + 0u, N3DS_DIRECT_ASSET_MAGIC); + writeLe32(header + 4u, N3DS_DIRECT_ASSET_VERSION); + writeLe32(header + 8u, entryCount); + writeLe32(header + 12u, stringTableSize); + bool ok = fwrite(header, 1, sizeof(header), file) == sizeof(header); + + repeat(entryCount, i) { + if (!ok) break; + uint8_t row[16]; + writeLe32(row + 0u, entries[i].pathOffset); + writeLe32(row + 4u, entries[i].dataOffset); + writeLe32(row + 8u, entries[i].dataSize); + writeLe32(row + 12u, 0u); + ok = fwrite(row, 1, sizeof(row), file) == sizeof(row); + } + + repeat(entryCount, i) { + if (!ok) break; + size_t pathBytes = strlen(entries[i].relativePath) + 1u; + ok = fwrite(entries[i].relativePath, 1, pathBytes, file) == pathBytes; + } + + repeat(entryCount, i) { + if (!ok) break; + ok = appendFileToStream(file, entries[i].sourcePath); + } + + fclose(file); + + if (!ok) { + fprintf(stderr, "Failed to write packed direct asset file: %s\n", outputPath); + arrfree(entries); + return false; + } + + repeat(entryCount, i) { + if (!fileExists(entries[i].sourcePath)) continue; + if (remove(entries[i].sourcePath) != 0) { + fprintf(stderr, "Warning: failed to remove packed direct source file: %s\n", entries[i].sourcePath); + } + } + + arrfree(entries); + fprintf(stderr, "n3ds-preprocess: packed %u direct texture assets into gfx/direct_assets.bin\n", entryCount); + return true; +} + +static bool writeAtlasFile( + const char* outputDir, + uint32_t pageCount, + const OutputPage* pages, + uint32_t itemCount, + const OutputItem* items, + uint32_t fragmentCount, + const OutputFragment* fragments, + uint32_t tileEntryCount, + const OutputTileEntry* tileEntries, + N3DSTextureFormat textureFormat +) { + char atlasPath[1024]; + snprintf(atlasPath, sizeof(atlasPath), "%s/gfx/atlas.bin", outputDir); + + uint32_t* pageDataOffsets = safeCalloc(pageCount > 0 ? pageCount : 1u, sizeof(uint32_t)); + uint32_t* pageDataSizes = safeCalloc(pageCount > 0 ? pageCount : 1u, sizeof(uint32_t)); + uint16_t version = N3DS_ATLAS_VERSION_FRAGMENTED_TILE_FRAGMENTS_PACKED; + size_t metadataSize = + 24u + + ((size_t) pageCount * 16u) + + ((size_t) itemCount * 10u) + + ((size_t) fragmentCount * 14u) + + ((size_t) tileEntryCount * 26u); + uint32_t currentOffset = (uint32_t) metadataSize; + + repeat(pageCount, i) { + char pagePath[1024]; + snprintf(pagePath, sizeof(pagePath), "%s/gfx/%s", outputDir, pages[i].path); + if (!getFileSize32(pagePath, &pageDataSizes[i])) { + fprintf(stderr, "Failed to determine atlas page size: %s\n", pagePath); + free(pageDataOffsets); + free(pageDataSizes); + return false; + } + pageDataOffsets[i] = currentOffset; + currentOffset += pageDataSizes[i]; + } + + FILE* file = fopen(atlasPath, "wb"); + if (file == NULL) { + fprintf(stderr, "Failed to open atlas output: %s\n", atlasPath); + free(pageDataOffsets); + free(pageDataSizes); + return false; + } + + uint32_t magic = N3DS_ATLAS_MAGIC; + uint16_t pageCount16 = (uint16_t) pageCount; + uint32_t textureFormat32 = (uint32_t) textureFormat; + + fwrite(&magic, sizeof(magic), 1, file); + fwrite(&version, sizeof(version), 1, file); + fwrite(&pageCount16, sizeof(pageCount16), 1, file); + fwrite(&itemCount, sizeof(itemCount), 1, file); + fwrite(&fragmentCount, sizeof(fragmentCount), 1, file); + fwrite(&tileEntryCount, sizeof(tileEntryCount), 1, file); + fwrite(&textureFormat32, sizeof(textureFormat32), 1, file); + + repeat(pageCount, i) { + fwrite(&pages[i].width, sizeof(uint16_t), 1, file); + fwrite(&pages[i].height, sizeof(uint16_t), 1, file); + fwrite(&pages[i].textureFormat, sizeof(uint32_t), 1, file); + fwrite(&pageDataOffsets[i], sizeof(uint32_t), 1, file); + fwrite(&pageDataSizes[i], sizeof(uint32_t), 1, file); + } + + repeat(itemCount, i) { + fwrite(&items[i].width, sizeof(uint16_t), 1, file); + fwrite(&items[i].height, sizeof(uint16_t), 1, file); + fwrite(&items[i].fragmentStart, sizeof(uint32_t), 1, file); + fwrite(&items[i].fragmentCount, sizeof(uint16_t), 1, file); + } + + repeat(fragmentCount, i) { + fwrite(&fragments[i].atlasId, sizeof(uint16_t), 1, file); + fwrite(&fragments[i].x, sizeof(uint16_t), 1, file); + fwrite(&fragments[i].y, sizeof(uint16_t), 1, file); + fwrite(&fragments[i].width, sizeof(uint16_t), 1, file); + fwrite(&fragments[i].height, sizeof(uint16_t), 1, file); + fwrite(&fragments[i].sourceX, sizeof(uint16_t), 1, file); + fwrite(&fragments[i].sourceY, sizeof(uint16_t), 1, file); + } + + repeat(tileEntryCount, i) { + fwrite(&tileEntries[i].bgDef, sizeof(int16_t), 1, file); + fwrite(&tileEntries[i].srcX, sizeof(uint16_t), 1, file); + fwrite(&tileEntries[i].srcY, sizeof(uint16_t), 1, file); + fwrite(&tileEntries[i].srcW, sizeof(uint16_t), 1, file); + fwrite(&tileEntries[i].srcH, sizeof(uint16_t), 1, file); + fwrite(&tileEntries[i].atlasId, sizeof(uint16_t), 1, file); + fwrite(&tileEntries[i].x, sizeof(uint16_t), 1, file); + fwrite(&tileEntries[i].y, sizeof(uint16_t), 1, file); + fwrite(&tileEntries[i].width, sizeof(uint16_t), 1, file); + fwrite(&tileEntries[i].height, sizeof(uint16_t), 1, file); + fwrite(&tileEntries[i].fragmentStart, sizeof(uint32_t), 1, file); + fwrite(&tileEntries[i].fragmentCount, sizeof(uint16_t), 1, file); + } + + repeat(pageCount, i) { + char pagePath[1024]; + snprintf(pagePath, sizeof(pagePath), "%s/gfx/%s", outputDir, pages[i].path); + if (!appendFileToStream(file, pagePath)) { + fprintf(stderr, "Failed to append packed atlas page: %s\n", pagePath); + fclose(file); + free(pageDataOffsets); + free(pageDataSizes); + return false; + } + } + + fclose(file); + free(pageDataOffsets); + free(pageDataSizes); + return true; +} + +static PackedPage* createPackedPage(MAYBE_UNUSED const Options* options, OutputPage** pages, uint32_t* totalPageCount, uint32_t pageSize, N3DSTextureFormat pageFormat) { + *pages = safeRealloc(*pages, (*totalPageCount + 1u) * sizeof(OutputPage)); + OutputPage* outputPage = &(*pages)[*totalPageCount]; + outputPage->width = (uint16_t) pageSize; + outputPage->height = (uint16_t) pageSize; + outputPage->textureFormat = (uint32_t) pageFormat; + const char* extension = getPageExtension(pageFormat); + snprintf(outputPage->path, sizeof(outputPage->path), "page_%03u.%s", *totalPageCount, extension); + outputPage->containsFont = false; + outputPage->containsBackground = false; + outputPage->containsNonMonoContent = false; + + PackedPage* packedPage = safeCalloc(1, sizeof(PackedPage)); + packedPage->pageIndex = *totalPageCount; + packedPage->width = pageSize; + packedPage->height = pageSize; + packedPage->pixels = safeCalloc((size_t) pageSize * pageSize, 4); + (*totalPageCount)++; + return packedPage; +} + +static uint32_t choosePageSize(const Options* options, uint32_t width, uint32_t height) { + if (options != NULL) { + if (options->atlasPageMode == N3DS_ATLAS_PAGE_MODE_FORCE_256) return N3DS_SMALL_TEXTURE_SIZE; + if (options->atlasPageMode == N3DS_ATLAS_PAGE_MODE_FORCE_512) return N3DS_LARGE_TEXTURE_SIZE; + } + return (width <= N3DS_SMALL_TEXTURE_SIZE && height <= N3DS_SMALL_TEXTURE_SIZE) + ? N3DS_SMALL_TEXTURE_SIZE + : N3DS_LARGE_TEXTURE_SIZE; +} + +static bool packRect(const Options* options, PackedPage*** packedPages, uint32_t* packedPageCount, OutputPage** outputPages, uint32_t* totalPageCount, uint32_t width, uint32_t height, N3DSTextureFormat pageFormat, uint16_t* outPageIndex, uint16_t* outX, uint16_t* outY) { + if (width > N3DS_LARGE_TEXTURE_SIZE || height > N3DS_LARGE_TEXTURE_SIZE) return false; + uint32_t pageSize = choosePageSize(options, width, height); + + repeat(*packedPageCount, i) { + PackedPage* page = (*packedPages)[i]; + if (page->width != pageSize || page->height != pageSize) continue; + if ((N3DSTextureFormat) (*outputPages)[page->pageIndex].textureFormat != pageFormat) continue; + if (page->cursorX + width > page->width) { + page->cursorX = 0; + page->cursorY += page->rowHeight; + page->rowHeight = 0; + } + if (page->cursorY + height > page->height) continue; + + *outPageIndex = (uint16_t) page->pageIndex; + *outX = (uint16_t) page->cursorX; + *outY = (uint16_t) page->cursorY; + page->cursorX += width; + if (height > page->rowHeight) page->rowHeight = height; + return true; + } + + *packedPages = safeRealloc(*packedPages, (*packedPageCount + 1u) * sizeof(PackedPage*)); + PackedPage* page = createPackedPage(options, outputPages, totalPageCount, pageSize, pageFormat); + (*packedPages)[(*packedPageCount)++] = page; + + *outPageIndex = (uint16_t) page->pageIndex; + *outX = 0; + *outY = 0; + page->cursorX = width; + page->rowHeight = height; + return true; +} + +static void blitRect(uint8_t* dst, uint32_t dstStride, uint32_t dstX, uint32_t dstY, const uint8_t* src, uint32_t srcStride, uint32_t srcX, uint32_t srcY, uint32_t width, uint32_t height) { + repeat(height, row) { + memcpy( + dst + (((size_t) dstY + row) * dstStride + dstX) * 4u, + src + (((size_t) srcY + row) * srcStride + srcX) * 4u, + (size_t) width * 4u + ); + } +} + +static void sanitizeSpriteReplacementStem(const char* value, char* out, size_t outSize) { + if (out == NULL || outSize == 0) return; + out[0] = '\0'; + if (value == NULL || value[0] == '\0') return; + + size_t writeIndex = 0; + for (size_t i = 0; value[i] != '\0' && writeIndex + 1 < outSize; ++i) { + unsigned char c = (unsigned char) value[i]; + if (isalnum(c) || c == '_' || c == '-' || c == '.') { + out[writeIndex++] = (char) c; + } else { + out[writeIndex++] = '_'; + } + } + out[writeIndex] = '\0'; +} + +static bool tryLoadSpriteReplacementCandidate( + const Options* options, + const char* filename, + const Sprite* sprite, + uint32_t spriteIndex, + uint32_t frameIndex, + uint32_t expectedWidth, + uint32_t expectedHeight, + uint8_t* outPixels +) { + if (options == NULL || filename == NULL || outPixels == NULL) return false; + if (options->spriteReplacementDir == NULL || options->spriteReplacementDir[0] == '\0') return false; + + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", options->spriteReplacementDir, filename); + if (!fileExists(path)) return false; + + int width = 0; + int height = 0; + int channels = 0; + uint8_t* pixels = stbi_load(path, &width, &height, &channels, 4); + if (pixels == NULL) { + fprintf(stderr, "Warning: failed to load sprite replacement PNG: %s\n", path); + return false; + } + + if (width != (int) expectedWidth || height != (int) expectedHeight) { + fprintf( + stderr, + "Warning: ignoring sprite replacement %s for sprite %05lu/%05lu (%s): expected %lux%lu, got %dx%d\n", + path, + (unsigned long) spriteIndex, + (unsigned long) frameIndex, + sprite != NULL && sprite->name != NULL ? sprite->name : "", + (unsigned long) expectedWidth, + (unsigned long) expectedHeight, + width, + height + ); + stbi_image_free(pixels); + return false; + } + + memcpy(outPixels, pixels, (size_t) expectedWidth * expectedHeight * 4u); + stbi_image_free(pixels); + fprintf( + stderr, + "n3ds-preprocess: sprite replacement %05lu/%05lu <- %s\n", + (unsigned long) spriteIndex, + (unsigned long) frameIndex, + path + ); + return true; +} + +static bool tryApplySpriteReplacement( + const Options* options, + const Sprite* sprite, + uint32_t spriteIndex, + uint32_t frameIndex, + uint32_t expectedWidth, + uint32_t expectedHeight, + uint8_t* outPixels +) { + if (options == NULL || sprite == NULL || outPixels == NULL) return false; + if (!options->spriteReplacementDirAvailable) return false; + + char filename[320]; + char spriteStem[192]; + sanitizeSpriteReplacementStem(sprite->name, spriteStem, sizeof(spriteStem)); + + if (spriteStem[0] != '\0') { + snprintf(filename, sizeof(filename), "%s_frame_%lu.png", spriteStem, (unsigned long) frameIndex); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + + snprintf(filename, sizeof(filename), "%s_frame_%05lu.png", spriteStem, (unsigned long) frameIndex); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + + snprintf(filename, sizeof(filename), "%s_%lu.png", spriteStem, (unsigned long) frameIndex); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + + snprintf(filename, sizeof(filename), "%s_%05lu.png", spriteStem, (unsigned long) frameIndex); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + + if (sprite->textureCount <= 1) { + snprintf(filename, sizeof(filename), "%s.png", spriteStem); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + } + } + + snprintf(filename, sizeof(filename), "spr_%05lu_frame_%lu.png", (unsigned long) spriteIndex, (unsigned long) frameIndex); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + + snprintf(filename, sizeof(filename), "spr_%05lu_frame_%05lu.png", (unsigned long) spriteIndex, (unsigned long) frameIndex); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + + snprintf(filename, sizeof(filename), "spr_%05lu_%lu.png", (unsigned long) spriteIndex, (unsigned long) frameIndex); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + + snprintf(filename, sizeof(filename), "spr_%05lu_%05lu.png", (unsigned long) spriteIndex, (unsigned long) frameIndex); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + + if (sprite->textureCount <= 1) { + snprintf(filename, sizeof(filename), "spr_%05lu.png", (unsigned long) spriteIndex); + if (tryLoadSpriteReplacementCandidate(options, filename, sprite, spriteIndex, frameIndex, expectedWidth, expectedHeight, outPixels)) return true; + } + + return false; +} + +static bool filenameHasPngExtension(const char* filename) { + if (filename == NULL) return false; + size_t len = strlen(filename); + if (len < 5) return false; + const char* ext = filename + len - 4; + return tolower((unsigned char) ext[0]) == '.' && + tolower((unsigned char) ext[1]) == 'p' && + tolower((unsigned char) ext[2]) == 'n' && + tolower((unsigned char) ext[3]) == 'g'; +} + +static bool copyPngStem(char* out, size_t outSize, const char* filename) { + if (out == NULL || outSize == 0 || !filenameHasPngExtension(filename)) return false; + size_t stemLen = strlen(filename) - 4u; + if (stemLen == 0 || stemLen >= outSize) return false; + memcpy(out, filename, stemLen); + out[stemLen] = '\0'; + return true; +} + +static bool convertBorderAssetPng(const Options* options, const char* filename, uint32_t* convertedCount) { + if (options == NULL || filename == NULL) return false; + if (!filenameHasPngExtension(filename)) return true; + + char stem[256]; + if (!copyPngStem(stem, sizeof(stem), filename)) return true; + + char inputPath[1400]; + char outputPath[1400]; + snprintf(inputPath, sizeof(inputPath), "%s/%s", options->borderAssetDir, filename); + snprintf(outputPath, sizeof(outputPath), "%s/gfx/borders/%s.t3x", options->outputDir, stem); + + fprintf(stderr, "n3ds-preprocess: border %s -> %s\n", inputPath, outputPath); + if (!runTex3ds(options->tex3dsExe, inputPath, outputPath, N3DS_TEXFMT_RGBA5551)) { + fprintf(stderr, "Failed to convert border asset: %s\n", inputPath); + return false; + } + + if (convertedCount != NULL) (*convertedCount)++; + return true; +} + +static bool convertBorderAssets(const Options* options) { + if (options == NULL) return false; + if (!options->borderAssetDirAvailable) return true; + + uint32_t convertedCount = 0; + bool ok = true; + +#if N3DS_PREPROCESS_HOST_WINDOWS + char searchPath[1400]; + WIN32_FIND_DATAA findData; + snprintf(searchPath, sizeof(searchPath), "%s\\*.png", options->borderAssetDir); + HANDLE findHandle = FindFirstFileA(searchPath, &findData); + if (findHandle == INVALID_HANDLE_VALUE) { + fprintf(stderr, "Border assets: converted=0\n"); + return true; + } + + do { + if ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) continue; + if (!convertBorderAssetPng(options, findData.cFileName, &convertedCount)) { + ok = false; + break; + } + } while (FindNextFileA(findHandle, &findData)); + + FindClose(findHandle); +#else + DIR* dir = opendir(options->borderAssetDir); + if (dir == NULL) { + fprintf(stderr, "Border assets: converted=0\n"); + return true; + } + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (!convertBorderAssetPng(options, entry->d_name, &convertedCount)) { + ok = false; + break; + } + } + + closedir(dir); +#endif + + if (ok) fprintf(stderr, "Border assets: converted=%u\n", convertedCount); + return ok; +} + +static bool writeDirectTextureAsset(const Options* options, const char* relativePathNoExt, const uint8_t* rgba, uint32_t width, uint32_t height, N3DSTextureFormat format) { + if (options == NULL || relativePathNoExt == NULL || rgba == NULL || width == 0 || height == 0) return false; + + char pngPath[1024]; + char t3xPath[1024]; + snprintf(pngPath, sizeof(pngPath), "%s/%s.png", options->outputDir, relativePathNoExt); + snprintf(t3xPath, sizeof(t3xPath), "%s/%s.t3x", options->outputDir, relativePathNoExt); + + if (!stbi_write_png(pngPath, (int) width, (int) height, 4, rgba, (int) (width * 4u))) { + fprintf(stderr, "Failed to write direct asset PNG: %s\n", pngPath); + return false; + } + if (!runTex3ds(options->tex3dsExe, pngPath, t3xPath, format)) { + return false; + } + if (!options->keepPng) remove(pngPath); + return true; +} + +static bool writeDirectTexturePng(const Options* options, const char* relativePathNoExt, const uint8_t* rgba, uint32_t width, uint32_t height) { + if (options == NULL || relativePathNoExt == NULL || rgba == NULL || width == 0 || height == 0) return false; + + char pngPath[1024]; + snprintf(pngPath, sizeof(pngPath), "%s/%s.png", options->outputDir, relativePathNoExt); + if (!stbi_write_png(pngPath, (int) width, (int) height, 4, rgba, (int) (width * 4u))) { + fprintf(stderr, "Failed to write direct asset PNG: %s\n", pngPath); + return false; + } + return true; +} + +static bool emitDirectSpriteFrameAsset( + const Options* options, + const Sprite* sprite, + uint32_t spriteIndex, + uint32_t frameIndex, + const TexturePageItem* item, + const uint8_t* rgba, + uint32_t stride, + DirectSpriteFormatState* spriteFormatStates +) { + if (options == NULL || sprite == NULL || item == NULL || rgba == NULL) return false; + if (sprite->name != NULL && sprite->name[0] != '\0') { + fprintf( + stderr, + "n3ds-preprocess: sprite frame %05lu/%05lu (%s)\n", + (unsigned long) spriteIndex, + (unsigned long) frameIndex, + sprite->name + ); + } else { + fprintf( + stderr, + "n3ds-preprocess: sprite frame %05lu/%05lu\n", + (unsigned long) spriteIndex, + (unsigned long) frameIndex + ); + } + + uint32_t logicalW = sprite->width > 0 ? sprite->width : (item->boundingWidth > 0 ? item->boundingWidth : item->sourceWidth); + uint32_t logicalH = sprite->height > 0 ? sprite->height : (item->boundingHeight > 0 ? item->boundingHeight : item->sourceHeight); + if (logicalW == 0 || logicalH == 0) return true; + if (logicalW > 1024 || logicalH > 1024) return true; + + uint8_t* framePixels = safeCalloc((size_t) logicalW * (size_t) logicalH, 4u); + int32_t dstX = (int32_t) item->targetX; + int32_t dstY = (int32_t) item->targetY; + int32_t srcX = (int32_t) item->sourceX; + int32_t srcY = (int32_t) item->sourceY; + int32_t copyW = (int32_t) item->sourceWidth; + int32_t copyH = (int32_t) item->sourceHeight; + if (dstX < 0) { + srcX -= dstX; + copyW += dstX; + dstX = 0; + } + if (dstY < 0) { + srcY -= dstY; + copyH += dstY; + dstY = 0; + } + if (dstX + copyW > (int32_t) logicalW) copyW = (int32_t) logicalW - dstX; + if (dstY + copyH > (int32_t) logicalH) copyH = (int32_t) logicalH - dstY; + if (copyW > 0 && copyH > 0 && dstX >= 0 && dstY >= 0 && srcX >= 0 && srcY >= 0) { + blitRect(framePixels, logicalW, (uint32_t) dstX, (uint32_t) dstY, rgba, stride, (uint32_t) srcX, (uint32_t) srcY, (uint32_t) copyW, (uint32_t) copyH); + } + + tryApplySpriteReplacement(options, sprite, spriteIndex, frameIndex, logicalW, logicalH, framePixels); + + if (spriteFormatStates != NULL) { + DirectSpriteFormatState* state = &spriteFormatStates[spriteIndex]; + bool frameMonoSafe = pixelsLookMonochrome(framePixels, logicalW, logicalH); + if (!state->sawFrame) { + state->sawFrame = true; + state->allFramesMonoSafe = frameMonoSafe; + } else if (state->allFramesMonoSafe && !frameMonoSafe) { + state->allFramesMonoSafe = false; + } + } + + char spriteTempRoot[1024]; + char spriteTempDir[1024]; + char relativePath[256]; + snprintf(spriteTempRoot, sizeof(spriteTempRoot), "%s/gfx/sprites/__tmp", options->outputDir); + snprintf(spriteTempDir, sizeof(spriteTempDir), "%s/spr_%05lu", spriteTempRoot, (unsigned long) spriteIndex); + if (!ensureDir(spriteTempRoot) || !ensureDir(spriteTempDir)) { + free(framePixels); + return false; + } + + snprintf(relativePath, sizeof(relativePath), "gfx/sprites/__tmp/spr_%05lu/frame_%05lu", (unsigned long) spriteIndex, (unsigned long) frameIndex); + bool ok = writeDirectTexturePng(options, relativePath, framePixels, logicalW, logicalH); + free(framePixels); + return ok; +} + +static bool finalizeDirectSpriteAssets(const Options* options, const DataWin* dataWin, const DirectSpriteFormatState* spriteFormatStates) { + if (options == NULL || dataWin == NULL) return false; + + char spriteTempRoot[1024]; + snprintf(spriteTempRoot, sizeof(spriteTempRoot), "%s/gfx/sprites/__tmp", options->outputDir); + + fprintf(stderr, "n3ds-preprocess: finalizing direct sprite assets\n"); + + repeat(dataWin->sprt.count, spriteIndex) { + const Sprite* sprite = &dataWin->sprt.sprites[spriteIndex]; + if (sprite->textureCount <= 0) continue; + + char spriteTempDir[1024]; + char globPath[1024]; + char outPath[1024]; + char pngPath[1024]; + char frameOutPath[1024]; + snprintf(spriteTempDir, sizeof(spriteTempDir), "%s/spr_%05lu", spriteTempRoot, (unsigned long) spriteIndex); + if (!directoryExists(spriteTempDir)) continue; + snprintf(globPath, sizeof(globPath), "%s/spr_%05lu/*.png", spriteTempRoot, (unsigned long) spriteIndex); + snprintf(outPath, sizeof(outPath), "%s/gfx/sprites/spr_%05lu.t3x", options->outputDir, (unsigned long) spriteIndex); + bool forceFrameDirect = spriteShouldForceDirectRGBA5551(sprite->name); + bool spriteMonoSafe = + spriteFormatStates != NULL && + spriteFormatStates[spriteIndex].sawFrame && + spriteFormatStates[spriteIndex].allFramesMonoSafe; + N3DSTextureFormat spriteFormat = spriteMonoSafe ? N3DS_TEXFMT_LA4 : N3DS_TEXFMT_RGBA5551; + if (sprite->name != NULL && sprite->name[0] != '\0') { + fprintf( + stderr, + "n3ds-preprocess: sprite %05lu -> %s [%s] (%s)\n", + (unsigned long) spriteIndex, + forceFrameDirect ? "frame files" : "atlas", + getTextureFormatLabel(spriteFormat), + sprite->name + ); + } else { + fprintf( + stderr, + "n3ds-preprocess: sprite %05lu -> %s [%s]\n", + (unsigned long) spriteIndex, + forceFrameDirect ? "frame files" : "atlas", + getTextureFormatLabel(spriteFormat) + ); + } + if (!forceFrameDirect && !runTex3dsAtlas(options->tex3dsExe, globPath, outPath, spriteFormat)) { + fprintf(stderr, "n3ds-preprocess: atlas build failed for sprite %05lu, falling back to per-frame t3x\n", (unsigned long) spriteIndex); + repeat(sprite->textureCount, frameIndex) { + snprintf(pngPath, sizeof(pngPath), "%s/spr_%05lu/frame_%05zu.png", spriteTempRoot, (unsigned long) spriteIndex, frameIndex); + if (!fileExists(pngPath)) continue; + snprintf(frameOutPath, sizeof(frameOutPath), "%s/gfx/sprites/spr_%05lu_frame_%05zu.t3x", options->outputDir, (unsigned long) spriteIndex, frameIndex); + if (!runTex3ds(options->tex3dsExe, pngPath, frameOutPath, spriteFormat)) { + return false; + } + } + } else if (forceFrameDirect) { + repeat(sprite->textureCount, frameIndex) { + snprintf(pngPath, sizeof(pngPath), "%s/spr_%05lu/frame_%05zu.png", spriteTempRoot, (unsigned long) spriteIndex, frameIndex); + if (!fileExists(pngPath)) continue; + snprintf(frameOutPath, sizeof(frameOutPath), "%s/gfx/sprites/spr_%05lu_frame_%05zu.t3x", options->outputDir, (unsigned long) spriteIndex, frameIndex); + if (!runTex3ds(options->tex3dsExe, pngPath, frameOutPath, spriteFormat)) { + return false; + } + } + } + if (!options->keepPng) { + if (!deleteDirectoryRecursive(spriteTempDir)) { + fprintf(stderr, "Failed to remove temporary sprite directory: %s\n", spriteTempDir); + return false; + } + } + } + + if (!options->keepPng && directoryExists(spriteTempRoot)) { + if (!deleteDirectoryRecursive(spriteTempRoot)) { + fprintf(stderr, "Failed to remove temporary sprite root: %s\n", spriteTempRoot); + return false; + } + } + return true; +} + +static bool emitDirectBackgroundAsset( + const Options* options, + uint32_t backgroundIndex, + const TexturePageItem* item, + const uint8_t* rgba, + uint32_t stride +) { + if (options == NULL || item == NULL || rgba == NULL || item->sourceWidth == 0 || item->sourceHeight == 0) return false; + if (item->sourceWidth > 1024 || item->sourceHeight > 1024) return true; + + uint8_t* pixels = safeCalloc((size_t) item->sourceWidth * (size_t) item->sourceHeight, 4u); + blitRect(pixels, item->sourceWidth, 0, 0, rgba, stride, item->sourceX, item->sourceY, item->sourceWidth, item->sourceHeight); + + char relativePath[256]; + snprintf(relativePath, sizeof(relativePath), "gfx/backgrounds/bg_%05lu", (unsigned long) backgroundIndex); + bool ok = writeDirectTextureAsset(options, relativePath, pixels, item->sourceWidth, item->sourceHeight, N3DS_TEXFMT_RGBA5551); + free(pixels); + return ok; +} + +static bool emitDirectFontAsset( + const Options* options, + uint32_t fontIndex, + const TexturePageItem* item, + const uint8_t* rgba, + uint32_t stride +) { + if (options == NULL || item == NULL || rgba == NULL || item->sourceWidth == 0 || item->sourceHeight == 0) return false; + if (item->sourceWidth > 1024 || item->sourceHeight > 1024) return true; + + uint8_t* pixels = safeCalloc((size_t) item->sourceWidth * (size_t) item->sourceHeight, 4u); + blitRect(pixels, item->sourceWidth, 0, 0, rgba, stride, item->sourceX, item->sourceY, item->sourceWidth, item->sourceHeight); + + char relativePath[256]; + snprintf(relativePath, sizeof(relativePath), "gfx/fonts/font_%05lu", (unsigned long) fontIndex); + bool ok = writeDirectTextureAsset(options, relativePath, pixels, item->sourceWidth, item->sourceHeight, N3DS_TEXFMT_RGBA5551); + free(pixels); + return ok; +} + +static PackedPage* findPackedPage(PackedPage** packedPages, uint32_t packedPageCount, uint16_t pageIndex) { + repeat(packedPageCount, i) { + if (packedPages[i]->pageIndex == pageIndex) return packedPages[i]; + } + return NULL; +} + +static void collectLegacyTileRequest(TileLookupKey key, TileLookupKey** outKeys, TileRequestMap** dedupe) { + ptrdiff_t existing = hmgeti(*dedupe, key); + if (existing >= 0) return; + hmput(*dedupe, key, (uint32_t) arrlen(*outKeys)); + arrput(*outKeys, key); +} + +static void collectTileLayerRequests(RoomLayerTilesData* tilesData, DataWin* dataWin, TileLookupKey** outKeys, TileRequestMap** dedupe) { + if (tilesData == NULL || tilesData->tileData == NULL) return; + if (tilesData->backgroundIndex < 0 || (uint32_t) tilesData->backgroundIndex >= dataWin->bgnd.count) return; + + Background* tileset = &dataWin->bgnd.backgrounds[tilesData->backgroundIndex]; + if (tileset->gms2TileWidth == 0 || tileset->gms2TileHeight == 0 || tileset->gms2TileColumns == 0) return; + + uint32_t tileW = tileset->gms2TileWidth; + uint32_t tileH = tileset->gms2TileHeight; + uint32_t borderX = tileset->gms2OutputBorderX; + uint32_t borderY = tileset->gms2OutputBorderY; + uint32_t columns = tileset->gms2TileColumns; + uint32_t totalTiles = tilesData->tilesX * tilesData->tilesY; + + repeat(totalTiles, i) { + uint32_t cell = tilesData->tileData[i]; + uint32_t tileIndex = cell & GMS2_TILE_INDEX_MASK; + if (tileIndex == 0) continue; + + uint32_t tileSlot = tileIndex - 1u; + uint32_t col = tileSlot % columns; + uint32_t row = tileSlot / columns; + collectLegacyTileRequest( + (TileLookupKey) { + .bgDef = (int16_t) tilesData->backgroundIndex, + .srcX = (uint16_t) (col * (tileW + 2u * borderX) + borderX), + .srcY = (uint16_t) (row * (tileH + 2u * borderY) + borderY), + .srcW = (uint16_t) tileW, + .srcH = (uint16_t) tileH, + }, + outKeys, + dedupe + ); + } +} + +static void collectBackgroundTilesetRequests(DataWin* dataWin, TileLookupKey** outKeys, TileRequestMap** dedupe) { + repeat(dataWin->bgnd.count, i) { + Background* bg = &dataWin->bgnd.backgrounds[i]; + if (bg->gms2TileWidth == 0 || bg->gms2TileHeight == 0 || bg->gms2TileColumns == 0 || bg->gms2TileCount == 0) continue; + + uint32_t tileW = bg->gms2TileWidth; + uint32_t tileH = bg->gms2TileHeight; + uint32_t borderX = bg->gms2OutputBorderX; + uint32_t borderY = bg->gms2OutputBorderY; + uint32_t columns = bg->gms2TileColumns; + + repeat(bg->gms2TileCount, tileSlot) { + uint32_t col = (uint32_t) tileSlot % columns; + uint32_t row = (uint32_t) tileSlot / columns; + collectLegacyTileRequest( + (TileLookupKey) { + .bgDef = (int16_t) i, + .srcX = (uint16_t) (col * (tileW + 2u * borderX) + borderX), + .srcY = (uint16_t) (row * (tileH + 2u * borderY) + borderY), + .srcW = (uint16_t) tileW, + .srcH = (uint16_t) tileH, + }, + outKeys, + dedupe + ); + } + } +} + +static void collectLegacyTileRequestsFromRoom(Room* room, DataWin* dataWin, TileLookupKey** outKeys, TileRequestMap** dedupe) { + if (room == NULL) return; + + repeat(room->tileCount, i) { + RoomTile* tile = &room->tiles[i]; + if (tile->useSpriteDefinition || tile->backgroundDefinition < 0 || tile->width == 0 || tile->height == 0) continue; + collectLegacyTileRequest( + (TileLookupKey) { + .bgDef = (int16_t) tile->backgroundDefinition, + .srcX = (uint16_t) tile->sourceX, + .srcY = (uint16_t) tile->sourceY, + .srcW = (uint16_t) tile->width, + .srcH = (uint16_t) tile->height, + }, + outKeys, + dedupe + ); + } + + repeat(room->layerCount, i) { + RoomLayer* layer = &room->layers[i]; + if (layer->tilesData != NULL) { + collectTileLayerRequests(layer->tilesData, dataWin, outKeys, dedupe); + } + if (layer->assetsData == NULL) continue; + repeat(layer->assetsData->legacyTileCount, j) { + RoomTile* tile = &layer->assetsData->legacyTiles[j]; + if (tile->useSpriteDefinition || tile->backgroundDefinition < 0 || tile->width == 0 || tile->height == 0) continue; + collectLegacyTileRequest( + (TileLookupKey) { + .bgDef = (int16_t) tile->backgroundDefinition, + .srcX = (uint16_t) tile->sourceX, + .srcY = (uint16_t) tile->sourceY, + .srcW = (uint16_t) tile->width, + .srcH = (uint16_t) tile->height, + }, + outKeys, + dedupe + ); + } + } +} + +static TileLookupKey* collectLegacyTileRequests(DataWin* dataWin) { + TileLookupKey* keys = NULL; + TileRequestMap* dedupe = NULL; + uint32_t totalRoomTiles = 0; + uint32_t totalLegacyAssetTiles = 0; + uint32_t totalTileLayers = 0; + repeat(dataWin->room.count, i) { + DataWin_loadRoomPayload(dataWin, (int32_t) i); + Room* room = &dataWin->room.rooms[i]; + totalRoomTiles += room->tileCount; + repeat(room->layerCount, layerIndex) { + RoomLayer* layer = &room->layers[layerIndex]; + if (layer->assetsData != NULL) totalLegacyAssetTiles += layer->assetsData->legacyTileCount; + if (layer->tilesData != NULL) totalTileLayers++; + } + collectLegacyTileRequestsFromRoom(&dataWin->room.rooms[i], dataWin, &keys, &dedupe); + } + collectBackgroundTilesetRequests(dataWin, &keys, &dedupe); + fprintf( + stderr, + "n3ds-preprocess: roomTiles=%u assetLegacyTiles=%u tileLayers=%u uniqueTileRects=%u\n", + totalRoomTiles, + totalLegacyAssetTiles, + totalTileLayers, + (unsigned) arrlen(keys) + ); + hmfree(dedupe); + return keys; +} + +static void markRoomManifestOutputItemPages( + uint32_t tpagIndex, + uint32_t totalPageCount, + const OutputItem* items, + const OutputFragment* fragments, + bool* seenPages +) { + if (items == NULL || fragments == NULL || seenPages == NULL) return; + const OutputItem* item = &items[tpagIndex]; + if (item->fragmentCount == 0 || item->fragmentStart == UINT32_MAX) return; + + repeat(item->fragmentCount, i) { + uint32_t fragmentIndex = item->fragmentStart + (uint32_t) i; + uint16_t pageIndex = fragments[fragmentIndex].atlasId; + if (pageIndex < totalPageCount) seenPages[pageIndex] = true; + } +} + +static void markRoomManifestSpritePages( + const DataWin* dataWin, + int32_t spriteIndex, + uint32_t totalPageCount, + const OutputItem* items, + const OutputFragment* fragments, + bool* seenPages +) { + if (dataWin == NULL || items == NULL || fragments == NULL || seenPages == NULL) return; + if (spriteIndex < 0 || (uint32_t) spriteIndex >= dataWin->sprt.count) return; + + const Sprite* sprite = &dataWin->sprt.sprites[spriteIndex]; + repeat(sprite->textureCount, i) { + int32_t tpagIndex = sprite->tpagIndices[i]; + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dataWin->tpag.count) continue; + markRoomManifestOutputItemPages((uint32_t) tpagIndex, totalPageCount, items, fragments, seenPages); + } +} + +static void markRoomManifestBackgroundPages( + const DataWin* dataWin, + int32_t backgroundIndex, + uint32_t totalPageCount, + const OutputItem* items, + const OutputFragment* fragments, + bool* seenPages +) { + if (dataWin == NULL || items == NULL || fragments == NULL || seenPages == NULL) return; + if (backgroundIndex < 0 || (uint32_t) backgroundIndex >= dataWin->bgnd.count) return; + + const Background* background = &dataWin->bgnd.backgrounds[backgroundIndex]; + if (background->tpagIndex < 0 || (uint32_t) background->tpagIndex >= dataWin->tpag.count) return; + markRoomManifestOutputItemPages((uint32_t) background->tpagIndex, totalPageCount, items, fragments, seenPages); +} + +static void markRoomManifestTileEntryPages( + const OutputTileEntry* tileEntry, + uint32_t totalPageCount, + const OutputFragment* fragments, + bool* seenPages +) { + if (tileEntry == NULL || fragments == NULL || seenPages == NULL) return; + + if (tileEntry->atlasId != UINT16_MAX) { + if (tileEntry->atlasId < totalPageCount) seenPages[tileEntry->atlasId] = true; + return; + } + + if (tileEntry->fragmentCount == 0 || tileEntry->fragmentStart == UINT32_MAX) return; + repeat(tileEntry->fragmentCount, i) { + uint32_t fragmentIndex = tileEntry->fragmentStart + (uint32_t) i; + uint16_t pageIndex = fragments[fragmentIndex].atlasId; + if (pageIndex < totalPageCount) seenPages[pageIndex] = true; + } +} + +static bool writeRoomManifestFile( + const char* outputDir, + const DataWin* dataWin, + uint32_t totalPageCount, + const OutputItem* items, + const OutputFragment* fragments, + const OutputTileEntry* packedTileEntries, + uint32_t packedTileEntryCount +) { + if (outputDir == NULL || dataWin == NULL || items == NULL || fragments == NULL) return false; + + char manifestPath[1024]; + snprintf(manifestPath, sizeof(manifestPath), "%s/gfx/room_manifest.bin", outputDir); + + TileRequestMap* tileEntryMap = NULL; + repeat(packedTileEntryCount, i) { + TileLookupKey key = { + .bgDef = packedTileEntries[i].bgDef, + .srcX = packedTileEntries[i].srcX, + .srcY = packedTileEntries[i].srcY, + .srcW = packedTileEntries[i].srcW, + .srcH = packedTileEntries[i].srcH, + }; + hmput(tileEntryMap, key, i); + } + + N3DSRoomManifestEntry* entries = NULL; + N3DSRoomManifestPageRef* pageRefs = NULL; + uint32_t roomWithPagesCount = 0; + + repeat(dataWin->room.count, roomIndex) { + Room* room = &dataWin->room.rooms[roomIndex]; + bool* seenPages = safeCalloc(totalPageCount > 0 ? totalPageCount : 1u, sizeof(bool)); + uint32_t pageStart = (uint32_t) arrlen(pageRefs); + + repeat(8u, i) { + if (room->backgrounds == NULL || !room->backgrounds[i].enabled) continue; + markRoomManifestBackgroundPages(dataWin, room->backgrounds[i].backgroundDefinition, totalPageCount, items, fragments, seenPages); + } + + repeat(room->gameObjectCount, objIndex) { + const RoomGameObject* roomObject = &room->gameObjects[objIndex]; + if (roomObject->objectDefinition < 0 || (uint32_t) roomObject->objectDefinition >= dataWin->objt.count) continue; + const GameObject* object = &dataWin->objt.objects[roomObject->objectDefinition]; + markRoomManifestSpritePages(dataWin, object->spriteId, totalPageCount, items, fragments, seenPages); + } + + repeat(room->layerCount, layerIndex) { + RoomLayer* layer = &room->layers[layerIndex]; + if (layer->backgroundData != NULL) { + markRoomManifestSpritePages(dataWin, layer->backgroundData->spriteIndex, totalPageCount, items, fragments, seenPages); + } + if (layer->assetsData != NULL) { + repeat(layer->assetsData->spriteCount, spriteSlot) { + markRoomManifestSpritePages( + dataWin, + layer->assetsData->sprites[spriteSlot].spriteIndex, + totalPageCount, + items, + fragments, + seenPages + ); + } + } + } + + TileLookupKey* roomTileKeys = NULL; + TileRequestMap* roomTileDedupe = NULL; + collectLegacyTileRequestsFromRoom(room, (DataWin*) dataWin, &roomTileKeys, &roomTileDedupe); + repeat(arrlen(roomTileKeys), tileKeyIndex) { + ptrdiff_t packedTileIndex = hmgeti(tileEntryMap, roomTileKeys[tileKeyIndex]); + if (packedTileIndex < 0) continue; + markRoomManifestTileEntryPages( + &packedTileEntries[tileEntryMap[packedTileIndex].value], + totalPageCount, + fragments, + seenPages + ); + } + arrfree(roomTileKeys); + hmfree(roomTileDedupe); + + repeat(totalPageCount, pageIndex) { + if (!seenPages[pageIndex]) continue; + N3DSRoomManifestPageRef ref = { + .pageIndex = (uint16_t) pageIndex, + .flags = 0, + .reserved = 0, + }; + arrput(pageRefs, ref); + } + + uint32_t pageCount = (uint32_t) arrlen(pageRefs) - pageStart; + if (pageCount > 0) roomWithPagesCount++; + N3DSRoomManifestEntry entry = { + .roomIndex = roomIndex, + .pageStart = pageStart, + .pageCount = pageCount, + .directSpriteStart = 0, + .directSpriteCount = 0, + .directBackgroundStart = 0, + .directBackgroundCount = 0, + }; + arrput(entries, entry); + free(seenPages); + } + + FILE* file = fopen(manifestPath, "wb"); + if (file == NULL) { + hmfree(tileEntryMap); + arrfree(entries); + arrfree(pageRefs); + fprintf(stderr, "Failed to open room manifest output: %s\n", manifestPath); + return false; + } + + uint32_t header[5] = { + N3DS_ROOM_MANIFEST_MAGIC, + N3DS_ROOM_MANIFEST_VERSION, + (uint32_t) arrlen(entries), + (uint32_t) arrlen(pageRefs), + dataWin->room.count, + }; + bool ok = fwrite(header, sizeof(header), 1, file) == 1; + if (ok && arrlen(entries) > 0) ok = fwrite(entries, sizeof(N3DSRoomManifestEntry), arrlen(entries), file) == arrlen(entries); + if (ok && arrlen(pageRefs) > 0) ok = fwrite(pageRefs, sizeof(N3DSRoomManifestPageRef), arrlen(pageRefs), file) == arrlen(pageRefs); + fclose(file); + + fprintf( + stderr, + "n3ds-preprocess: wrote room manifest (%u rooms, %u non-empty, %u page refs) -> %s\n", + dataWin->room.count, + roomWithPagesCount, + (unsigned int) arrlen(pageRefs), + manifestPath + ); + + hmfree(tileEntryMap); + arrfree(entries); + arrfree(pageRefs); + return ok; +} + +static bool flushPackedPages(const Options* options, OutputPage* outputPages, PackedPage** packedPages, uint32_t packedPageCount) { + uint32_t rgba5551Pages = 0; + uint32_t etc1a4Pages = 0; + uint32_t indexed8Pages = 0; + uint32_t l4Pages = 0; + uint32_t la4Pages = 0; + uint32_t monoRejectCount = 0; + uint32_t monoRejectFontCount = 0; + uint32_t monoRejectBackgroundCount = 0; + uint32_t monoRejectNonMonoCount = 0; + uint32_t monoRejectSamplePages[8] = {0}; + uint32_t monoRejectSampleCount = 0; + uint32_t manualMonoRiskCount = 0; + uint32_t manualMonoRiskSamplePages[8] = {0}; + uint32_t manualMonoRiskSampleCount = 0; + int32_t* overrideFormats = loadPageFormatOverrides(options->pageFormatOverridesPath, packedPageCount); + + repeat(packedPageCount, i) { + PackedPage* packedPage = packedPages[i]; + OutputPage* outputPage = &outputPages[packedPage->pageIndex]; + N3DSTextureFormat pageFormat = (N3DSTextureFormat) outputPage->textureFormat; + if (options->textureFormat == N3DS_TEXFMT_HYBRID) { + pageFormat = chooseHybridPageFormat(packedPage); + outputPage->textureFormat = (uint32_t) pageFormat; + snprintf(outputPage->path, sizeof(outputPage->path), "page_%03u.%s", packedPage->pageIndex, getPageExtension(pageFormat)); + } + bool hasManualOverride = overrideFormats != NULL && overrideFormats[packedPage->pageIndex] >= 0; + if (hasManualOverride) { + pageFormat = (N3DSTextureFormat) overrideFormats[packedPage->pageIndex]; + outputPage->textureFormat = (uint32_t) pageFormat; + snprintf(outputPage->path, sizeof(outputPage->path), "page_%03u.%s", packedPage->pageIndex, getPageExtension(pageFormat)); + if ((pageFormat == N3DS_TEXFMT_L4 || pageFormat == N3DS_TEXFMT_LA4) && !pageCanUseMonoFormat(outputPage)) { + manualMonoRiskCount++; + if (manualMonoRiskSampleCount < 8u) { + manualMonoRiskSamplePages[manualMonoRiskSampleCount++] = packedPage->pageIndex; + } + } + } + if (!hasManualOverride && pageShouldForceRGBA5551(outputPage)) { + pageFormat = N3DS_TEXFMT_RGBA5551; + outputPage->textureFormat = (uint32_t) pageFormat; + snprintf(outputPage->path, sizeof(outputPage->path), "page_%03u.%s", packedPage->pageIndex, getPageExtension(pageFormat)); + } + if (!hasManualOverride && (pageFormat == N3DS_TEXFMT_L4 || pageFormat == N3DS_TEXFMT_LA4) && !pageCanUseMonoFormat(outputPage)) { + monoRejectCount++; + if (outputPage->containsFont) monoRejectFontCount++; + if (outputPage->containsBackground) monoRejectBackgroundCount++; + if (outputPage->containsNonMonoContent) monoRejectNonMonoCount++; + if (monoRejectSampleCount < 8u) { + monoRejectSamplePages[monoRejectSampleCount++] = packedPage->pageIndex; + } + pageFormat = N3DS_TEXFMT_RGBA5551; + outputPage->textureFormat = (uint32_t) pageFormat; + snprintf(outputPage->path, sizeof(outputPage->path), "page_%03u.%s", packedPage->pageIndex, getPageExtension(pageFormat)); + } + char pngPath[1024]; + char pagePath[1024]; + char previewPath[1024]; + snprintf(pngPath, sizeof(pngPath), "%s/gfx/page_%03u.png", options->outputDir, packedPage->pageIndex); + snprintf(pagePath, sizeof(pagePath), "%s/gfx/%s", options->outputDir, outputPage->path); + snprintf( + previewPath, + sizeof(previewPath), + "%s/gfx/page_%03u__%s.png", + options->outputDir, + packedPage->pageIndex, + outputPage->previewLabel[0] != '\0' ? outputPage->previewLabel : "unnamed" + ); + + if (pageFormat == N3DS_TEXFMT_INDEXED8) { + if (!writeIndexedPage(pagePath, packedPage)) { + free(overrideFormats); + return false; + } + } else { + if (!stbi_write_png(pngPath, packedPage->width, packedPage->height, 4, packedPage->pixels, packedPage->width * 4)) { + fprintf(stderr, "Failed to write temporary PNG: %s\n", pngPath); + free(overrideFormats); + return false; + } + if (!runTex3ds(options->tex3dsExe, pngPath, pagePath, pageFormat)) { + free(overrideFormats); + return false; + } + if (options->dumpPagePreviews) { + stbi_write_png(previewPath, packedPage->width, packedPage->height, 4, packedPage->pixels, packedPage->width * 4); + } + if (!options->keepPng) remove(pngPath); + } + if (pageFormat == N3DS_TEXFMT_INDEXED8 && options->dumpPagePreviews) { + stbi_write_png(previewPath, packedPage->width, packedPage->height, 4, packedPage->pixels, packedPage->width * 4); + } + switch (pageFormat) { + case N3DS_TEXFMT_RGBA5551: rgba5551Pages++; break; + case N3DS_TEXFMT_ETC1A4: etc1a4Pages++; break; + case N3DS_TEXFMT_INDEXED8: indexed8Pages++; break; + case N3DS_TEXFMT_L4: l4Pages++; break; + case N3DS_TEXFMT_LA4: la4Pages++; break; + default: break; + } + fprintf(stderr, "Wrote packed atlas page %u (%s) -> %s\n", packedPage->pageIndex, getTextureFormatLabel(pageFormat), pagePath); + } + + if (packedPageCount > 0) { + fprintf( + stderr, + "n3ds-preprocess: atlas page formats: rgba5551=%u etc1a4=%u indexed8=%u l4=%u la4=%u total=%u\n", + rgba5551Pages, + etc1a4Pages, + indexed8Pages, + l4Pages, + la4Pages, + packedPageCount + ); + if (monoRejectCount > 0) { + fprintf( + stderr, + "n3ds-preprocess: rejected mono overrides: total=%u font=%u background=%u nonMono=%u", + monoRejectCount, + monoRejectFontCount, + monoRejectBackgroundCount, + monoRejectNonMonoCount + ); + if (monoRejectSampleCount > 0) { + fprintf(stderr, " samplePages="); + repeat(monoRejectSampleCount, i) { + fprintf(stderr, "%s%03u", i == 0 ? "" : ",", monoRejectSamplePages[i]); + } + } + fprintf(stderr, "\n"); + } + if (manualMonoRiskCount > 0) { + fprintf(stderr, "n3ds-preprocess: manual mono overrides flagged as risky: total=%u", manualMonoRiskCount); + if (manualMonoRiskSampleCount > 0) { + fprintf(stderr, " samplePages="); + repeat(manualMonoRiskSampleCount, i) { + fprintf(stderr, "%s%03u", i == 0 ? "" : ",", manualMonoRiskSamplePages[i]); + } + } + fprintf(stderr, "\n"); + } + } + writePageFormatTemplate(options, outputPages, packedPageCount); + free(overrideFormats); + return true; +} + +static bool convertTextures(const Options* options, DataWin* dataWin) { + fprintf(stderr, "n3ds-preprocess: processing textures and direct sprite assets\n"); + + bool gm2022_5 = DataWin_isVersionAtLeast(dataWin, 2022, 5, 0, 0); + OutputPage* pages = NULL; + OutputItem* items = safeCalloc(dataWin->tpag.count, sizeof(OutputItem)); + OutputFragment* fragments = NULL; + TileLookupKey* tileRequests = collectLegacyTileRequests(dataWin); + size_t tileRequestCount = arrlen(tileRequests); + OutputTileEntry* tileEntriesByRequest = safeCalloc(tileRequestCount > 0 ? tileRequestCount : 1u, sizeof(OutputTileEntry)); + bool* tileEntryWritten = safeCalloc(tileRequestCount > 0 ? tileRequestCount : 1u, sizeof(bool)); + OutputTileEntry* packedTileEntries = NULL; + uint32_t totalPageCount = 0; + PackedPage** packedPages = NULL; + uint32_t packedPageCount = 0; + bool* targetedMonoTPAGs = options->enableTargetedBattleDialogueMono ? collectTargetedDialogueBattleTPAGs(dataWin) : NULL; + bool* fontTPAGs = collectFontTPAGs(dataWin); + bool* backgroundTPAGs = collectBackgroundTPAGs(dataWin); + char** tpagDebugNames = collectTPAGDebugNames(dataWin); + int32_t* tpagToSpriteIndex = buildTPAGToSpriteIndexMap(dataWin); + int32_t* tpagToSpriteFrameIndex = buildTPAGToSpriteFrameMap(dataWin); + int32_t* tpagToBackgroundIndex = buildTPAGToBackgroundIndexMap(dataWin); + int32_t* tpagToFontIndex = buildTPAGToFontIndexMap(dataWin); + bool* emittedSpriteFrames = safeCalloc(dataWin->tpag.count > 0 ? dataWin->tpag.count : 1u, sizeof(bool)); + DirectSpriteFormatState* directSpriteFormatStates = safeCalloc(dataWin->sprt.count > 0 ? dataWin->sprt.count : 1u, sizeof(DirectSpriteFormatState)); + bool* emittedBackgrounds = safeCalloc(dataWin->bgnd.count > 0 ? dataWin->bgnd.count : 1u, sizeof(bool)); + bool* emittedFonts = safeCalloc(dataWin->font.count > 0 ? dataWin->font.count : 1u, sizeof(bool)); + + repeat(dataWin->tpag.count, i) { + items[i].fragmentCount = UINT16_MAX; + items[i].fragmentStart = UINT32_MAX; + } + + repeat(dataWin->txtr.count, i) { + Texture* texture = &dataWin->txtr.textures[i]; + if (texture->blobData == NULL || texture->blobSize == 0) { + fprintf(stderr, "Texture page %zu has no embedded blob data; external textures are not supported by this preprocessor.\n", i); + free(pages); + return false; + } + + int width = 0; + int height = 0; + uint8_t* rgba = ImageDecoder_decodeToRgba(texture->blobData, texture->blobSize, gm2022_5, &width, &height); + if (rgba == NULL) { + fprintf(stderr, "Failed to decode TXTR page %zu\n", i); + free(pages); + return false; + } + + if (width <= 0 || height <= 0 || width > 65535 || height > 65535) { + fprintf(stderr, "Invalid decoded size for TXTR page %zu: %dx%d\n", i, width, height); + free(rgba); + free(pages); + return false; + } + + repeat(dataWin->tpag.count, itemIndex) { + TexturePageItem* item = &dataWin->tpag.items[itemIndex]; + if (item->texturePageId != (int16_t) i) continue; + + N3DSTextureFormat itemPageFormat = options->textureFormat; + if (itemPageFormat == N3DS_TEXFMT_HYBRID) itemPageFormat = N3DS_TEXFMT_HYBRID; + N3DSTextureFormat itemMonoFormat = chooseTargetedMonoItemFormat(rgba, (uint32_t) width, item); + bool itemMonoSafe = itemMonoFormat == N3DS_TEXFMT_L4 || itemMonoFormat == N3DS_TEXFMT_LA4; + if (targetedMonoTPAGs != NULL && targetedMonoTPAGs[itemIndex]) { + if (itemMonoSafe) { + itemPageFormat = itemMonoFormat; + } + } + + uint32_t itemWidth = item->sourceWidth; + uint32_t itemHeight = item->sourceHeight; + if (itemWidth == 0 || itemHeight == 0) { + items[itemIndex].width = 0; + items[itemIndex].height = 0; + items[itemIndex].fragmentStart = 0; + items[itemIndex].fragmentCount = 0; + continue; + } + uint32_t fragmentStart = arrlen(fragments); + uint32_t itemChunkSize = choosePageSize(options, itemWidth, itemHeight); + for (uint32_t chunkY = 0; chunkY < itemHeight; chunkY += itemChunkSize) { + for (uint32_t chunkX = 0; chunkX < itemWidth; chunkX += itemChunkSize) { + uint32_t fragWidth = itemWidth - chunkX; + uint32_t fragHeight = itemHeight - chunkY; + if (fragWidth > itemChunkSize) fragWidth = itemChunkSize; + if (fragHeight > itemChunkSize) fragHeight = itemChunkSize; + + uint16_t pageIndex = 0; + uint16_t dstX = 0; + uint16_t dstY = 0; + if (!packRect(options, &packedPages, &packedPageCount, &pages, &totalPageCount, fragWidth, fragHeight, itemPageFormat, &pageIndex, &dstX, &dstY)) { + fprintf(stderr, "Fragment pack failed for TPAG item %zu\n", itemIndex); + free(rgba); + free(items); + free(pages); + arrfree(fragments); + arrfree(tileRequests); + free(tileEntriesByRequest); + free(tileEntryWritten); + arrfree(packedTileEntries); + repeat(packedPageCount, freeIndex) { + free(packedPages[freeIndex]->pixels); + free(packedPages[freeIndex]); + } + free(packedPages); + return false; + } + + PackedPage* dstPage = NULL; + repeat(packedPageCount, pageSlot) { + if (packedPages[pageSlot]->pageIndex == pageIndex) { + dstPage = packedPages[pageSlot]; + break; + } + } + requireNotNull(dstPage); + OutputPage* outputPage = &pages[pageIndex]; + if (tpagDebugNames != NULL && tpagDebugNames[itemIndex] != NULL) { + appendPageDebugName(outputPage, tpagDebugNames[itemIndex]); + } + if (tpagToSpriteIndex != NULL && tpagToSpriteIndex[itemIndex] >= 0) outputPage->containsSprite = true; + if (fontTPAGs != NULL && fontTPAGs[itemIndex]) outputPage->containsFont = true; + if (backgroundTPAGs != NULL && backgroundTPAGs[itemIndex]) outputPage->containsBackground = true; + if (!itemMonoSafe) outputPage->containsNonMonoContent = true; + + blitRect( + dstPage->pixels, + dstPage->width, + dstX, + dstY, + rgba, + (uint32_t) width, + item->sourceX + chunkX, + item->sourceY + chunkY, + fragWidth, + fragHeight + ); + + OutputFragment fragment = { + .atlasId = pageIndex, + .x = dstX, + .y = dstY, + .width = (uint16_t) fragWidth, + .height = (uint16_t) fragHeight, + .sourceX = (uint16_t) chunkX, + .sourceY = (uint16_t) chunkY, + }; + arrput(fragments, fragment); + } + } + + if (tpagToSpriteIndex != NULL && tpagToSpriteFrameIndex != NULL) { + int32_t spriteIndex = tpagToSpriteIndex[itemIndex]; + int32_t frameIndex = tpagToSpriteFrameIndex[itemIndex]; + if (spriteIndex >= 0 && frameIndex >= 0 && (uint32_t) spriteIndex < dataWin->sprt.count && !emittedSpriteFrames[itemIndex]) { + if (!emitDirectSpriteFrameAsset(options, &dataWin->sprt.sprites[spriteIndex], (uint32_t) spriteIndex, (uint32_t) frameIndex, item, rgba, (uint32_t) width, directSpriteFormatStates)) { + fprintf(stderr, "Failed to emit direct sprite asset for sprite %d frame %d\n", spriteIndex, frameIndex); + free(rgba); + free(items); + free(pages); + arrfree(fragments); + arrfree(tileRequests); + free(tileEntriesByRequest); + free(tileEntryWritten); + arrfree(packedTileEntries); + free(targetedMonoTPAGs); + free(fontTPAGs); + free(backgroundTPAGs); + freeTPAGDebugNames(tpagDebugNames, dataWin->tpag.count); + free(tpagToSpriteIndex); + free(tpagToSpriteFrameIndex); + free(tpagToBackgroundIndex); + free(tpagToFontIndex); + free(emittedSpriteFrames); + free(directSpriteFormatStates); + free(emittedBackgrounds); + free(emittedFonts); + repeat(packedPageCount, freeIndex) { + free(packedPages[freeIndex]->pixels); + free(packedPages[freeIndex]); + } + free(packedPages); + return false; + } + emittedSpriteFrames[itemIndex] = true; + } + } + + if (tpagToBackgroundIndex != NULL) { + int32_t backgroundIndex = tpagToBackgroundIndex[itemIndex]; + if (backgroundIndex >= 0 && (uint32_t) backgroundIndex < dataWin->bgnd.count && !emittedBackgrounds[backgroundIndex]) { + if (!emitDirectBackgroundAsset(options, (uint32_t) backgroundIndex, item, rgba, (uint32_t) width)) { + fprintf(stderr, "Failed to emit direct background asset for background %d\n", backgroundIndex); + free(rgba); + free(items); + free(pages); + arrfree(fragments); + arrfree(tileRequests); + free(tileEntriesByRequest); + free(tileEntryWritten); + arrfree(packedTileEntries); + free(targetedMonoTPAGs); + free(fontTPAGs); + free(backgroundTPAGs); + freeTPAGDebugNames(tpagDebugNames, dataWin->tpag.count); + free(tpagToSpriteIndex); + free(tpagToSpriteFrameIndex); + free(tpagToBackgroundIndex); + free(tpagToFontIndex); + free(emittedSpriteFrames); + free(directSpriteFormatStates); + free(emittedBackgrounds); + free(emittedFonts); + repeat(packedPageCount, freeIndex) { + free(packedPages[freeIndex]->pixels); + free(packedPages[freeIndex]); + } + free(packedPages); + return false; + } + emittedBackgrounds[backgroundIndex] = true; + } + } + + if (tpagToFontIndex != NULL) { + int32_t fontIndex = tpagToFontIndex[itemIndex]; + if (fontIndex >= 0 && (uint32_t) fontIndex < dataWin->font.count && !emittedFonts[fontIndex]) { + if (!emitDirectFontAsset(options, (uint32_t) fontIndex, item, rgba, (uint32_t) width)) { + fprintf(stderr, "Failed to emit direct font asset for font %d\n", fontIndex); + free(rgba); + free(items); + free(pages); + arrfree(fragments); + arrfree(tileRequests); + free(tileEntriesByRequest); + free(tileEntryWritten); + arrfree(packedTileEntries); + free(targetedMonoTPAGs); + free(fontTPAGs); + free(backgroundTPAGs); + freeTPAGDebugNames(tpagDebugNames, dataWin->tpag.count); + free(tpagToSpriteIndex); + free(tpagToSpriteFrameIndex); + free(tpagToBackgroundIndex); + free(tpagToFontIndex); + free(emittedSpriteFrames); + free(directSpriteFormatStates); + free(emittedBackgrounds); + free(emittedFonts); + repeat(packedPageCount, freeIndex) { + free(packedPages[freeIndex]->pixels); + free(packedPages[freeIndex]); + } + free(packedPages); + return false; + } + emittedFonts[fontIndex] = true; + } + } + + items[itemIndex].width = item->sourceWidth; + items[itemIndex].height = item->sourceHeight; + items[itemIndex].fragmentStart = fragmentStart; + items[itemIndex].fragmentCount = (uint16_t) (arrlen(fragments) - fragmentStart); + } + + repeat(tileRequestCount, reqIndex) { + if (tileEntryWritten[reqIndex]) continue; + TileLookupKey key = tileRequests[reqIndex]; + if (key.bgDef < 0 || (uint32_t) key.bgDef >= dataWin->bgnd.count) continue; + + Background* bg = &dataWin->bgnd.backgrounds[key.bgDef]; + if (bg->tpagIndex < 0 || (uint32_t) bg->tpagIndex >= dataWin->tpag.count) continue; + + TexturePageItem* item = &dataWin->tpag.items[bg->tpagIndex]; + if (item->texturePageId != (int16_t) i) continue; + + uint8_t* tilePixels = safeCalloc((size_t) key.srcW * key.srcH, 4); + uint8_t* candidateLogical = safeCalloc((size_t) key.srcW * key.srcH, 4); + uint8_t* candidateContent = safeCalloc((size_t) key.srcW * key.srcH, 4); + uint32_t bgLogicalW = item->boundingWidth > 0 ? item->boundingWidth : item->sourceWidth; + uint32_t bgLogicalH = item->boundingHeight > 0 ? item->boundingHeight : item->sourceHeight; + if (bgLogicalW > 0 && bgLogicalH > 0) { + uint8_t* bgLogicalPixels = safeCalloc((size_t) bgLogicalW * bgLogicalH, 4); + if ((uint32_t) item->targetX < bgLogicalW && + (uint32_t) item->targetY < bgLogicalH && + item->sourceWidth > 0 && + item->sourceHeight > 0) { + uint32_t copyW = item->sourceWidth; + uint32_t copyH = item->sourceHeight; + if ((uint32_t) item->targetX + copyW > bgLogicalW) copyW = bgLogicalW - (uint32_t) item->targetX; + if ((uint32_t) item->targetY + copyH > bgLogicalH) copyH = bgLogicalH - (uint32_t) item->targetY; + blitRect( + bgLogicalPixels, + bgLogicalW, + item->targetX, + item->targetY, + rgba, + (uint32_t) width, + item->sourceX, + item->sourceY, + copyW, + copyH + ); + } + + if ((uint32_t) key.srcX < bgLogicalW && (uint32_t) key.srcY < bgLogicalH) { + uint32_t copyW = key.srcW; + uint32_t copyH = key.srcH; + if ((uint32_t) key.srcX + copyW > bgLogicalW) copyW = bgLogicalW - (uint32_t) key.srcX; + if ((uint32_t) key.srcY + copyH > bgLogicalH) copyH = bgLogicalH - (uint32_t) key.srcY; + blitRect( + candidateLogical, + key.srcW, + 0, + 0, + bgLogicalPixels, + bgLogicalW, + key.srcX, + key.srcY, + copyW, + copyH + ); + } + + free(bgLogicalPixels); + } + + if ((uint32_t) key.srcX < item->sourceWidth && (uint32_t) key.srcY < item->sourceHeight) { + uint32_t copyW = key.srcW; + uint32_t copyH = key.srcH; + if ((uint32_t) key.srcX + copyW > item->sourceWidth) copyW = item->sourceWidth - (uint32_t) key.srcX; + if ((uint32_t) key.srcY + copyH > item->sourceHeight) copyH = item->sourceHeight - (uint32_t) key.srcY; + blitRect( + candidateContent, + key.srcW, + 0, + 0, + rgba, + (uint32_t) width, + item->sourceX + key.srcX, + item->sourceY + key.srcY, + copyW, + copyH + ); + } + + uint32_t logicalCoverage = 0; + uint32_t contentCoverage = 0; + repeat((size_t) key.srcW * key.srcH, pxIndex) { + if (candidateLogical[pxIndex * 4u + 3u] != 0) logicalCoverage++; + if (candidateContent[pxIndex * 4u + 3u] != 0) contentCoverage++; + } + memcpy(tilePixels, contentCoverage > logicalCoverage ? candidateContent : candidateLogical, (size_t) key.srcW * key.srcH * 4u); + free(candidateLogical); + free(candidateContent); + + uint16_t pageIndex = 0; + uint16_t dstX = 0; + uint16_t dstY = 0; + if (packRect(options, &packedPages, &packedPageCount, &pages, &totalPageCount, key.srcW, key.srcH, options->textureFormat, &pageIndex, &dstX, &dstY)) { + PackedPage* dstPage = findPackedPage(packedPages, packedPageCount, pageIndex); + requireNotNull(dstPage); + appendPageDebugName(&pages[pageIndex], bg->name); + pages[pageIndex].containsBackground = true; + pages[pageIndex].containsNonMonoContent = true; + blitRect(dstPage->pixels, dstPage->width, dstX, dstY, tilePixels, key.srcW, 0, 0, key.srcW, key.srcH); + + tileEntriesByRequest[reqIndex] = (OutputTileEntry) { + .bgDef = key.bgDef, + .srcX = key.srcX, + .srcY = key.srcY, + .srcW = key.srcW, + .srcH = key.srcH, + .atlasId = pageIndex, + .x = dstX, + .y = dstY, + .width = key.srcW, + .height = key.srcH, + .fragmentStart = UINT32_MAX, + .fragmentCount = 0, + }; + } else { + uint32_t fragmentStart = arrlen(fragments); + uint32_t tileChunkSize = choosePageSize(options, key.srcW, key.srcH); + for (uint32_t chunkY = 0; chunkY < key.srcH; chunkY += tileChunkSize) { + uint32_t fragHeight = key.srcH - chunkY; + if (fragHeight > tileChunkSize) fragHeight = tileChunkSize; + for (uint32_t chunkX = 0; chunkX < key.srcW; chunkX += tileChunkSize) { + uint32_t fragWidth = key.srcW - chunkX; + if (fragWidth > tileChunkSize) fragWidth = tileChunkSize; + + if (!packRect(options, &packedPages, &packedPageCount, &pages, &totalPageCount, (uint16_t) fragWidth, (uint16_t) fragHeight, options->textureFormat, &pageIndex, &dstX, &dstY)) { + fprintf(stderr, "Failed to fragment oversized tile rect bg=%d src=(%u,%u %ux%u) chunk=(%u,%u %ux%u)\n", + key.bgDef, + key.srcX, + key.srcY, + key.srcW, + key.srcH, + chunkX, + chunkY, + fragWidth, + fragHeight + ); + free(tilePixels); + free(items); + free(pages); + arrfree(fragments); + arrfree(tileRequests); + free(tileEntriesByRequest); + free(tileEntryWritten); + arrfree(packedTileEntries); + repeat(packedPageCount, pageIt) { + free(packedPages[pageIt]->pixels); + free(packedPages[pageIt]); + } + free(packedPages); + return false; + } + + PackedPage* dstPage = findPackedPage(packedPages, packedPageCount, pageIndex); + requireNotNull(dstPage); + appendPageDebugName(&pages[pageIndex], bg->name); + pages[pageIndex].containsBackground = true; + pages[pageIndex].containsNonMonoContent = true; + blitRect(dstPage->pixels, dstPage->width, dstX, dstY, tilePixels, key.srcW, chunkX, chunkY, fragWidth, fragHeight); + + OutputFragment fragment = { + .atlasId = pageIndex, + .x = dstX, + .y = dstY, + .width = (uint16_t) fragWidth, + .height = (uint16_t) fragHeight, + .sourceX = (uint16_t) chunkX, + .sourceY = (uint16_t) chunkY, + }; + arrput(fragments, fragment); + } + } + + tileEntriesByRequest[reqIndex] = (OutputTileEntry) { + .bgDef = key.bgDef, + .srcX = key.srcX, + .srcY = key.srcY, + .srcW = key.srcW, + .srcH = key.srcH, + .atlasId = UINT16_MAX, + .x = 0, + .y = 0, + .width = 0, + .height = 0, + .fragmentStart = fragmentStart, + .fragmentCount = (uint16_t) (arrlen(fragments) - fragmentStart), + }; + } + + free(tilePixels); + tileEntryWritten[reqIndex] = true; + } + + free(rgba); + } + + repeat(dataWin->tpag.count, i) { + if (items[i].fragmentCount == UINT16_MAX) { + fprintf(stderr, "TPAG item %zu was not mapped to any output page.\n", i); + free(items); + free(pages); + arrfree(fragments); + arrfree(tileRequests); + free(tileEntriesByRequest); + free(tileEntryWritten); + arrfree(packedTileEntries); + return false; + } + } + + repeat(tileRequestCount, i) { + if (!tileEntryWritten[i]) { + fprintf(stderr, "Warning: no packed legacy tile entry for bg=%d src=(%u,%u %ux%u)\n", + tileRequests[i].bgDef, + tileRequests[i].srcX, + tileRequests[i].srcY, + tileRequests[i].srcW, + tileRequests[i].srcH + ); + continue; + } + arrput(packedTileEntries, tileEntriesByRequest[i]); + } + + bool ok = flushPackedPages(options, pages, packedPages, packedPageCount); + if (ok) ok = finalizeDirectSpriteAssets(options, dataWin, directSpriteFormatStates); + if (ok) ok = writePackedDirectTextureAssets(options, dataWin); + if (ok) ok = writeAtlasFile(options->outputDir, totalPageCount, pages, dataWin->tpag.count, items, arrlen(fragments), fragments, arrlen(packedTileEntries), packedTileEntries, options->textureFormat); + if (ok) ok = writeRoomManifestFile( + options->outputDir, + dataWin, + totalPageCount, + items, + fragments, + packedTileEntries, + (uint32_t) arrlen(packedTileEntries) + ); + free(items); + free(pages); + arrfree(fragments); + arrfree(tileRequests); + free(tileEntriesByRequest); + free(tileEntryWritten); + free(targetedMonoTPAGs); + free(fontTPAGs); + free(backgroundTPAGs); + freeTPAGDebugNames(tpagDebugNames, dataWin->tpag.count); + free(tpagToSpriteIndex); + free(tpagToSpriteFrameIndex); + free(tpagToBackgroundIndex); + free(tpagToFontIndex); + free(emittedSpriteFrames); + free(directSpriteFormatStates); + free(emittedBackgrounds); + free(emittedFonts); + arrfree(packedTileEntries); + repeat(packedPageCount, i) { + free(packedPages[i]->pixels); + free(packedPages[i]); + } + free(packedPages); + return ok; +} + +#define N3DS_AUDIO_SAMPLE_RATE 32000u + +typedef struct { + uint8_t predictorScale; + int16_t yn1; + int16_t yn2; +} EncodedContext; + +typedef struct { + uint32_t sampleRate; + uint32_t sampleCount; + uint32_t channelCount; + int16_t* interleavedPcm; +} WavPcm16; + +typedef struct { + uint8_t* data; + uint32_t dataSize; + uint16_t coefs[16]; + EncodedContext startContext; + EncodedContext loopContext; +} EncodedBcwavChannel; + +typedef struct { + uint16_t coefs[16]; + uint32_t dataOffset; + uint32_t dataSize; + EncodedContext startContext; + EncodedContext loopContext; +} ParsedBcwavChannel; + +typedef struct { + uint32_t sampleRate; + uint32_t sampleCount; + uint32_t loopStart; + uint32_t loopEnd; + bool loop; + uint8_t channelCount; + ParsedBcwavChannel channels[2]; +} ParsedBcwav; + +typedef struct { + int16_t coef1; + int16_t coef2; +} DspPredictorPair; + +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadWavPcm16(const char* path, WavPcm16* out); +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadWavPcm16FromMemory(const uint8_t* data, uint32_t dataSize, WavPcm16* out); +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadVorbisPcm16(const char* path, WavPcm16* out); +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadVorbisPcm16FromMemory(const uint8_t* data, uint32_t dataSize, WavPcm16* out); +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadAudioPcm16(const char* path, WavPcm16* out); +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadAudioPcm16FromMemory(const uint8_t* data, uint32_t dataSize, WavPcm16* out); +static N3DS_PREPROCESS_MAYBE_UNUSED bool resampleWavPcm16(WavPcm16* wav, uint32_t targetRate); +static N3DS_PREPROCESS_MAYBE_UNUSED void freeWavPcm16(WavPcm16* wav); +static N3DS_PREPROCESS_MAYBE_UNUSED bool writeBcwavFile(const char* path, const WavPcm16* wav); + +static const DspPredictorPair N3DS_DSP_PREDICTORS[8] = { + { 0, 0 }, + { 2048, 0 }, + { 1024, 0 }, + { 3072, -1024 }, + { 4096, -2048 }, + { 3584, -1536 }, + { 1536, 512 }, + { 2560, -512 }, +}; + +static uint16_t readLe16(const uint8_t* ptr) { + return (uint16_t) (ptr[0] | (ptr[1] << 8)); +} + +static uint32_t readLe32(const uint8_t* ptr) { + return (uint32_t) ptr[0] | + ((uint32_t) ptr[1] << 8) | + ((uint32_t) ptr[2] << 16) | + ((uint32_t) ptr[3] << 24); +} + +static int16_t readLeS16(const uint8_t* ptr) { + return (int16_t) readLe16(ptr); +} + +static void writeLe16(uint8_t* ptr, uint16_t value) { + ptr[0] = (uint8_t) (value & 0xFFu); + ptr[1] = (uint8_t) ((value >> 8) & 0xFFu); +} + +static void writeLe32(uint8_t* ptr, uint32_t value) { + ptr[0] = (uint8_t) (value & 0xFFu); + ptr[1] = (uint8_t) ((value >> 8) & 0xFFu); + ptr[2] = (uint8_t) ((value >> 16) & 0xFFu); + ptr[3] = (uint8_t) ((value >> 24) & 0xFFu); +} + +static int signExtend4(int nibble) { + return (nibble & 0x8) ? (nibble - 16) : nibble; +} + +static uint32_t align32(uint32_t value) { + return (value + 31u) & ~31u; +} + +static int clampNibble(int value) { + if (value < -8) return -8; + if (value > 7) return 7; + return value; +} + +static int roundDivSigned(int numerator, int denominator) { + if (denominator <= 0) return 0; + if (numerator >= 0) return (numerator + denominator / 2) / denominator; + return -(((-numerator) + denominator / 2) / denominator); +} + +static char* dupParentDir(const char* path) { + const char* slash = strrchr(path, '/'); + const char* backslash = strrchr(path, '\\'); + if (backslash != NULL && (slash == NULL || backslash > slash)) slash = backslash; + if (slash == NULL) return safeStrdup("."); + size_t length = (size_t) (slash - path); + char* result = safeMalloc(length + 1); + memcpy(result, path, length); + result[length] = '\0'; + return result; +} + +static char* joinPath(const char* base, const char* relativePath) { + size_t baseLen = strlen(base); + size_t relLen = strlen(relativePath); + bool needSlash = baseLen > 0 && base[baseLen - 1] != '/' && base[baseLen - 1] != '\\'; + char* result = safeMalloc(baseLen + relLen + (needSlash ? 2 : 1)); + memcpy(result, base, baseLen); + size_t cursor = baseLen; + if (needSlash) result[cursor++] = '/'; + memcpy(result + cursor, relativePath, relLen); + result[cursor + relLen] = '\0'; + return result; +} + +static bool fileExists(const char* path) { + if (path == NULL || path[0] == '\0') return false; +#if N3DS_PREPROCESS_HOST_WINDOWS + DWORD attrs = GetFileAttributesA(path); + return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) == 0; +#else + struct stat st; + return stat(path, &st) == 0 && !S_ISDIR(st.st_mode); +#endif +} + +static bool writeBytesToFile(const char* path, const uint8_t* data, uint32_t size) { + FILE* file = fopen(path, "wb"); + if (file == NULL) return false; + bool ok = fwrite(data, 1, size, file) == size; + fclose(file); + return ok; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool readBytesFromFile(const char* path, uint8_t** outData, uint32_t* outSize) { + if (outData == NULL || outSize == NULL) return false; + *outData = NULL; + *outSize = 0; + + FILE* file = fopen(path, "rb"); + if (file == NULL) return false; + + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return false; + } + long sizeLong = ftell(file); + if (sizeLong <= 0 || fseek(file, 0, SEEK_SET) != 0) { + fclose(file); + return false; + } + + uint8_t* data = safeMalloc((size_t) sizeLong); + bool ok = fread(data, 1, (size_t) sizeLong, file) == (size_t) sizeLong; + fclose(file); + if (!ok) { + free(data); + return false; + } + + *outData = data; + *outSize = (uint32_t) sizeLong; + return true; +} + +static bool pathHasExtension(const char* path, const char* extension) { + if (path == NULL || extension == NULL) return false; + size_t pathLen = strlen(path); + size_t extLen = strlen(extension); + if (pathLen < extLen) return false; + const char* suffix = path + pathLen - extLen; + repeat(extLen, i) { + char a = suffix[i]; + char b = extension[i]; + if (a >= 'A' && a <= 'Z') a = (char) (a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = (char) (b - 'A' + 'a'); + if (a != b) return false; + } + return true; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool isBuiltinAudioInputPath(const char* path) { + return pathHasExtension(path, ".wav") || + pathHasExtension(path, ".ogg") || + pathHasExtension(path, ".oga"); +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool writeBcwavFromInputFile(const char* inputPath, const char* outputPath) { + WavPcm16 wav; + if (!loadAudioPcm16(inputPath, &wav)) { + fprintf(stderr, "In-process BCWAV writer could not decode audio: %s\n", inputPath); + return false; + } + + bool ok = writeBcwavFile(outputPath, &wav); + freeWavPcm16(&wav); + if (!ok) { + fprintf(stderr, "In-process BCWAV writer failed to write %s\n", outputPath); + return false; + } + return true; +} + +static const char* pickAudioTempExtension(const Sound* sound) { + const char* candidates[2] = { sound != NULL ? sound->file : NULL, sound != NULL ? sound->name : NULL }; + repeat(2, i) { + const char* value = candidates[i]; + if (value == NULL || value[0] == '\0') continue; + const char* dot = strrchr(value, '.'); + if (dot != NULL && dot[1] != '\0') return dot; + } + return ".ogg"; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadWavPcm16(const char* path, WavPcm16* out) { + uint8_t* blob = NULL; + uint32_t blobSize = 0; + bool ok = readBytesFromFile(path, &blob, &blobSize) && + loadWavPcm16FromMemory(blob, blobSize, out); + free(blob); + return ok; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadWavPcm16FromMemory(const uint8_t* blob, uint32_t blobSize, WavPcm16* out) { + if (blob == NULL || out == NULL) return false; + memset(out, 0, sizeof(*out)); + + if (blobSize < 44 || memcmp(blob, "RIFF", 4) != 0 || memcmp(blob + 8, "WAVE", 4) != 0) return false; + + uint16_t formatTag = 0; + uint16_t channels = 0; + uint16_t bitsPerSample = 0; + uint32_t sampleRate = 0; + const uint8_t* sampleData = NULL; + uint32_t sampleDataSize = 0; + size_t offset = 12; + while (offset + 8 <= blobSize) { + const uint8_t* chunk = blob + offset; + uint32_t chunkSize = readLe32(chunk + 4); + size_t payloadOffset = offset + 8; + size_t nextOffset = payloadOffset + chunkSize + (chunkSize & 1u); + if (payloadOffset + chunkSize > blobSize) return false; + + if (memcmp(chunk, "fmt ", 4) == 0 && chunkSize >= 16) { + formatTag = readLe16(blob + payloadOffset + 0); + channels = readLe16(blob + payloadOffset + 2); + sampleRate = readLe32(blob + payloadOffset + 4); + bitsPerSample = readLe16(blob + payloadOffset + 14); + } else if (memcmp(chunk, "data", 4) == 0) { + sampleData = blob + payloadOffset; + sampleDataSize = chunkSize; + } + + offset = nextOffset; + } + + if (formatTag != 1 || channels == 0 || channels > 2 || sampleRate == 0 || bitsPerSample != 16 || sampleData == NULL || sampleDataSize == 0) { + return false; + } + + size_t sampleCount = sampleDataSize / sizeof(int16_t); + out->interleavedPcm = safeMalloc(sampleCount * sizeof(int16_t)); + memcpy(out->interleavedPcm, sampleData, sampleCount * sizeof(int16_t)); + out->sampleRate = sampleRate; + out->channelCount = channels; + out->sampleCount = (uint32_t) (sampleCount / channels); + return true; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadVorbisPcm16(const char* path, WavPcm16* out) { + uint8_t* blob = NULL; + uint32_t blobSize = 0; + bool ok = readBytesFromFile(path, &blob, &blobSize) && + loadVorbisPcm16FromMemory(blob, blobSize, out); + free(blob); + return ok; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadVorbisPcm16FromMemory(const uint8_t* data, uint32_t dataSize, WavPcm16* out) { + if (data == NULL || out == NULL || dataSize > INT_MAX) return false; + memset(out, 0, sizeof(*out)); + + int channels = 0; + int sampleRate = 0; + short* decoded = NULL; + int sampleCount = stb_vorbis_decode_memory(data, (int) dataSize, &channels, &sampleRate, &decoded); + if (sampleCount <= 0 || decoded == NULL || channels <= 0 || channels > 2 || sampleRate <= 0) { + free(decoded); + return false; + } + + out->channelCount = (uint32_t) channels; + out->sampleRate = (uint32_t) sampleRate; + out->sampleCount = (uint32_t) sampleCount; + out->interleavedPcm = (int16_t*) decoded; + return true; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadAudioPcm16(const char* path, WavPcm16* out) { + uint8_t* blob = NULL; + uint32_t blobSize = 0; + bool ok = readBytesFromFile(path, &blob, &blobSize) && + loadAudioPcm16FromMemory(blob, blobSize, out); + free(blob); + return ok; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool loadAudioPcm16FromMemory(const uint8_t* data, uint32_t dataSize, WavPcm16* out) { + if (data == NULL || out == NULL || dataSize < 4) return false; + if (dataSize >= 12 && memcmp(data, "RIFF", 4) == 0 && memcmp(data + 8, "WAVE", 4) == 0) { + return loadWavPcm16FromMemory(data, dataSize, out); + } + if (memcmp(data, "OggS", 4) == 0) { + return loadVorbisPcm16FromMemory(data, dataSize, out); + } + return loadVorbisPcm16FromMemory(data, dataSize, out); +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool resampleWavPcm16(WavPcm16* wav, uint32_t targetRate) { + if (wav == NULL || wav->interleavedPcm == NULL || wav->sampleCount == 0 || wav->channelCount == 0 || targetRate == 0) return false; + if (wav->sampleRate == 0) wav->sampleRate = targetRate; + if (wav->sampleRate == targetRate) return true; + + uint64_t dstFrames64 = ((uint64_t) wav->sampleCount * targetRate + wav->sampleRate / 2u) / wav->sampleRate; + if (dstFrames64 == 0 || dstFrames64 > UINT32_MAX) return false; + + uint32_t dstFrames = (uint32_t) dstFrames64; + size_t dstSamples = (size_t) dstFrames * wav->channelCount; + int16_t* dst = safeMalloc(dstSamples * sizeof(int16_t)); + + repeat(dstFrames, dstFrame) { + uint64_t srcPos = (uint64_t) dstFrame * wav->sampleRate; + uint32_t srcFrame = (uint32_t) (srcPos / targetRate); + uint32_t frac = (uint32_t) (srcPos % targetRate); + if (srcFrame >= wav->sampleCount) srcFrame = wav->sampleCount - 1u; + uint32_t nextFrame = srcFrame + 1u < wav->sampleCount ? srcFrame + 1u : srcFrame; + + repeat(wav->channelCount, channel) { + int a = wav->interleavedPcm[(size_t) srcFrame * wav->channelCount + channel]; + int b = wav->interleavedPcm[(size_t) nextFrame * wav->channelCount + channel]; + int value = a + (int) (((int64_t) (b - a) * frac + targetRate / 2u) / targetRate); + if (value < -32768) value = -32768; + if (value > 32767) value = 32767; + dst[(size_t) dstFrame * wav->channelCount + channel] = (int16_t) value; + } + } + + free(wav->interleavedPcm); + wav->interleavedPcm = dst; + wav->sampleCount = dstFrames; + wav->sampleRate = targetRate; + return true; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED void freeWavPcm16(WavPcm16* wav) { + free(wav->interleavedPcm); + memset(wav, 0, sizeof(*wav)); +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool parseBcwavInfo(const uint8_t* info, uint32_t infoSize, uint32_t dataBlockOffset, uint32_t fileSize, ParsedBcwav* out) { + memset(out, 0, sizeof(*out)); + if (infoSize < 0x20 || memcmp(info, "INFO", 4) != 0) return false; + + uint8_t encoding = info[0x08]; + if (encoding != 2) return false; + + out->loop = info[0x09] != 0; + out->sampleRate = readLe32(info + 0x0C); + out->loopStart = readLe32(info + 0x10); + out->loopEnd = readLe32(info + 0x14); + out->sampleCount = out->loopEnd; + + uint32_t tableOffset = 0x1C; + uint32_t channelCount = readLe32(info + tableOffset); + if (channelCount == 0 || channelCount > 2) return false; + if (infoSize < tableOffset + 4 + channelCount * 8) return false; + + out->channelCount = (uint8_t) channelCount; + uint32_t encodedBytes = ((out->sampleCount + 13u) / 14u) * 8u; + + repeat(channelCount, i) { + const uint8_t* channelRef = info + tableOffset + 4 + i * 8; + uint32_t channelInfoOffset = tableOffset + readLe32(channelRef + 4); + if (channelInfoOffset + 0x14 > infoSize) return false; + + const uint8_t* channelInfo = info + channelInfoOffset; + uint32_t sampleOffset = readLe32(channelInfo + 4); + uint32_t adpcmInfoOffset = channelInfoOffset + readLe32(channelInfo + 12); + if (adpcmInfoOffset + 0x2E > infoSize) return false; + + ParsedBcwavChannel* channel = &out->channels[i]; + channel->dataOffset = dataBlockOffset + 8 + sampleOffset; + channel->dataSize = encodedBytes; + if (channel->dataOffset + channel->dataSize > fileSize) return false; + + const uint8_t* adpcmInfo = info + adpcmInfoOffset; + repeat(16, coef) { + channel->coefs[coef] = readLe16(adpcmInfo + coef * 2); + } + channel->startContext.predictorScale = adpcmInfo[0x20]; + channel->startContext.yn1 = readLeS16(adpcmInfo + 0x22); + channel->startContext.yn2 = readLeS16(adpcmInfo + 0x24); + channel->loopContext.predictorScale = adpcmInfo[0x26]; + channel->loopContext.yn1 = readLeS16(adpcmInfo + 0x28); + channel->loopContext.yn2 = readLeS16(adpcmInfo + 0x2A); + } + + return true; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool parseBcwavBlob(const uint8_t* data, uint32_t size, ParsedBcwav* out) { + if (data == NULL || out == NULL) return false; + if (size < 0x40 || memcmp(data, "CWAV", 4) != 0) return false; + uint32_t infoOffset = readLe32(data + 0x18); + uint32_t infoSize = readLe32(data + 0x1C); + uint32_t dataOffset = readLe32(data + 0x24); + if (infoOffset + infoSize > size || dataOffset + 8 > size) return false; + return parseBcwavInfo(data + infoOffset, infoSize, dataOffset, size, out); +} + +static void decodeBcwavFrame(const ParsedBcwavChannel* channel, const uint8_t* frame, int16_t* hist1, int16_t* hist2, int16_t outSamples[14]) { + int predictor = frame[0] >> 4; + int scale = 1 << (frame[0] & 0x0F); + int coef1 = (int16_t) channel->coefs[predictor * 2 + 0]; + int coef2 = (int16_t) channel->coefs[predictor * 2 + 1]; + + int sampleIndex = 0; + for (int byteIndex = 1; byteIndex < 8; ++byteIndex) { + int hi = signExtend4(frame[byteIndex] >> 4); + int lo = signExtend4(frame[byteIndex] & 0x0F); + int nibbles[2] = { hi, lo }; + repeat(2, nibbleIndex) { + int sample = (nibbles[nibbleIndex] * scale) << 11; + sample += 1024 + coef1 * (*hist1) + coef2 * (*hist2); + sample >>= 11; + if (sample > 32767) sample = 32767; + if (sample < -32768) sample = -32768; + outSamples[sampleIndex++] = (int16_t) sample; + *hist2 = *hist1; + *hist1 = (int16_t) sample; + } + } +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool decodeBcwavToPcm(const uint8_t* fileData, const ParsedBcwav* bcwav, WavPcm16* out) { + if (fileData == NULL || bcwav == NULL || out == NULL) return false; + memset(out, 0, sizeof(*out)); + + size_t totalSamples = (size_t) bcwav->sampleCount * bcwav->channelCount; + int16_t* pcm = safeMalloc(totalSamples * sizeof(int16_t)); + if (pcm == NULL) return false; + + int16_t frameSamples[2][14]; + int16_t hist1[2] = { + bcwav->channels[0].startContext.yn1, + bcwav->channels[1].startContext.yn1, + }; + int16_t hist2[2] = { + bcwav->channels[0].startContext.yn2, + bcwav->channels[1].startContext.yn2, + }; + + uint32_t frameCount = (bcwav->sampleCount + 13u) / 14u; + uint32_t sampleCursor = 0; + repeat(frameCount, frameIndex) { + repeat(bcwav->channelCount, channelIndex) { + const uint8_t* frame = fileData + bcwav->channels[channelIndex].dataOffset + frameIndex * 8u; + decodeBcwavFrame(&bcwav->channels[channelIndex], frame, &hist1[channelIndex], &hist2[channelIndex], frameSamples[channelIndex]); + } + + uint32_t samplesThisFrame = bcwav->sampleCount - sampleCursor; + if (samplesThisFrame > 14u) samplesThisFrame = 14u; + repeat(samplesThisFrame, sampleIndex) { + if (bcwav->channelCount == 1) { + pcm[sampleCursor + sampleIndex] = frameSamples[0][sampleIndex]; + } else { + size_t outIndex = ((size_t) sampleCursor + sampleIndex) * 2u; + pcm[outIndex + 0] = frameSamples[0][sampleIndex]; + pcm[outIndex + 1] = frameSamples[1][sampleIndex]; + } + } + sampleCursor += samplesThisFrame; + } + + out->sampleRate = bcwav->sampleRate; + out->sampleCount = bcwav->sampleCount; + out->channelCount = bcwav->channelCount; + out->interleavedPcm = pcm; + return true; +} + +static bool stringContainsIgnoreCase(const char* haystack, const char* needle) { + if (haystack == NULL || needle == NULL || needle[0] == '\0') return false; + size_t needleLen = strlen(needle); + for (const char* cursor = haystack; *cursor != '\0'; ++cursor) { + size_t matched = 0; + while (matched < needleLen) { + char a = cursor[matched]; + if (a == '\0') return false; + char b = needle[matched]; + if (a >= 'A' && a <= 'Z') a = (char) (a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = (char) (b - 'A' + 'a'); + if (a != b) break; + matched++; + } + if (matched == needleLen) return true; + } + return false; +} + +static bool nameLooksLikeSfx(const char* value) { + if (value == NULL || value[0] == '\0') return false; + const char* base = strrchr(value, '/'); + const char* backslash = strrchr(value, '\\'); + if (backslash != NULL && (base == NULL || backslash > base)) base = backslash; + base = (base != NULL) ? base + 1 : value; + + return strncmp(base, "mus_sfx", 7) == 0 || + strncmp(base, "sfx_", 4) == 0 || + strncmp(base, "snd_", 4) == 0 || + stringContainsIgnoreCase(base, "_sfx") || + stringContainsIgnoreCase(base, "sfx_"); +} + +static bool nameLooksLikeMusic(const char* value) { + if (value == NULL || value[0] == '\0') return false; + const char* base = strrchr(value, '/'); + const char* backslash = strrchr(value, '\\'); + if (backslash != NULL && (base == NULL || backslash > base)) base = backslash; + base = (base != NULL) ? base + 1 : value; + if (nameLooksLikeSfx(base)) return false; + if (strncmp(base, "mus_", 4) == 0 || strncmp(base, "bgm_", 4) == 0) return true; + return stringContainsIgnoreCase(base, "music"); +} + +static bool soundLooksLikeSfx(const Sound* sound) { + if (sound == NULL) return false; + return nameLooksLikeSfx(sound->name) || + nameLooksLikeSfx(sound->file) || + stringContainsIgnoreCase(sound->type, "sfx") || + stringContainsIgnoreCase(sound->type, "effect"); +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool soundLooksLikeMusic(const Sound* sound) { + if (sound == NULL) return false; + if (soundLooksLikeSfx(sound)) return false; + return nameLooksLikeMusic(sound->name) || + nameLooksLikeMusic(sound->file) || + stringContainsIgnoreCase(sound->type, "music") || + stringContainsIgnoreCase(sound->type, "stream"); +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool extractBaseNameNoExt(const char* value, char* out, size_t outSize) { + if (out == NULL || outSize == 0) return false; + out[0] = '\0'; + if (value == NULL || value[0] == '\0') return false; + + const char* base = strrchr(value, '/'); + const char* backslash = strrchr(value, '\\'); + if (backslash != NULL && (base == NULL || backslash > base)) base = backslash; + base = (base != NULL) ? base + 1 : value; + if (base[0] == '\0') return false; + + const char* dot = strrchr(base, '.'); + size_t len = (dot != NULL && dot > base) ? (size_t) (dot - base) : strlen(base); + if (len == 0 || len + 1 > outSize) return false; + memcpy(out, base, len); + out[len] = '\0'; + return true; +} + +static bool directoryExists(const char* path) { + if (path == NULL || path[0] == '\0') return false; +#if N3DS_PREPROCESS_HOST_WINDOWS + DWORD attrs = GetFileAttributesA(path); + return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0; +#else + struct stat st; + return stat(path, &st) == 0 && S_ISDIR(st.st_mode); +#endif +} + +#if N3DS_PREPROCESS_HOST_WINDOWS +static bool deleteDirectoryRecursive(const char* path) { + if (path == NULL || path[0] == '\0') return false; + if (!directoryExists(path)) return true; + + char searchPath[1060]; + WIN32_FIND_DATAA findData; + snprintf(searchPath, sizeof(searchPath), "%s\\*", path); + HANDLE findHandle = FindFirstFileA(searchPath, &findData); + if (findHandle != INVALID_HANDLE_VALUE) { + do { + const char* name = findData.cFileName; + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; + + char childPath[1060]; + snprintf(childPath, sizeof(childPath), "%s\\%s", path, name); + if ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + if (!deleteDirectoryRecursive(childPath)) { + FindClose(findHandle); + return false; + } + } else { + if (!DeleteFileA(childPath)) { + FindClose(findHandle); + return false; + } + } + } while (FindNextFileA(findHandle, &findData)); + FindClose(findHandle); + } + + return RemoveDirectoryA(path) != 0; +} +#else +static bool deleteDirectoryRecursive(const char* path) { + if (path == NULL || path[0] == '\0') return false; + if (!directoryExists(path)) return true; + + DIR* dir = opendir(path); + if (dir == NULL) return false; + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + const char* name = entry->d_name; + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; + + char childPath[1060]; + snprintf(childPath, sizeof(childPath), "%s/%s", path, name); + if (directoryExists(childPath)) { + if (!deleteDirectoryRecursive(childPath)) { + closedir(dir); + return false; + } + } else if (remove(childPath) != 0) { + closedir(dir); + return false; + } + } + + closedir(dir); + return rmdir(path) == 0; +} +#endif + +static void trimWhitespace(char* value) { + if (value == NULL) return; + + size_t len = strlen(value); + while (len > 0 && (value[len - 1] == '\r' || value[len - 1] == '\n' || isspace((unsigned char) value[len - 1]))) { + value[--len] = '\0'; + } + + size_t start = 0; + while (value[start] != '\0' && isspace((unsigned char) value[start])) start++; + if (start > 0) memmove(value, value + start, strlen(value + start) + 1); +} + +static void stripOptionalQuotes(char* value) { + if (value == NULL) return; + trimWhitespace(value); + size_t len = strlen(value); + if (len >= 2 && value[0] == '"' && value[len - 1] == '"') { + memmove(value, value + 1, len - 2); + value[len - 2] = '\0'; + } +} + +static bool promptLine(const char* prompt, char* out, size_t outSize) { + if (out == NULL || outSize == 0) return false; + fprintf(stdout, "%s", prompt); + fflush(stdout); + if (fgets(out, (int) outSize, stdin) == NULL) return false; + trimWhitespace(out); + stripOptionalQuotes(out); + return true; +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool getQuotedField(const char* line, int fieldIndex, char* out, size_t outSize) { + if (line == NULL || out == NULL || outSize == 0 || fieldIndex < 0) return false; + int currentField = 0; + const char* cursor = line; + while (*cursor != '\0') { + while (*cursor != '\0' && *cursor != '"') cursor++; + if (*cursor == '\0') return false; + cursor++; + + char buffer[1024]; + size_t written = 0; + while (*cursor != '\0' && *cursor != '"') { + char c = *cursor++; + if (c == '\\' && *cursor == '\\') c = *cursor++; + if (written + 1 < sizeof(buffer)) buffer[written++] = c; + } + if (*cursor != '"') return false; + cursor++; + buffer[written] = '\0'; + + if (currentField == fieldIndex) { + snprintf(out, outSize, "%s", buffer); + return true; + } + currentField++; + } + return false; +} + +static bool pathLooksLikeRootDrive(const char* path) { + return path != NULL && + ((strlen(path) == 2 && path[1] == ':') || + (strlen(path) == 3 && path[1] == ':' && (path[2] == '\\' || path[2] == '/'))); +} + +static bool resolveUndertaleDataWinPath(const char* candidate, char* dataWinPath, size_t dataWinPathSize) { + if (candidate == NULL || candidate[0] == '\0') return false; + + if (fileExists(candidate)) { + if (stringContainsIgnoreCase(candidate, "data.win")) { + snprintf(dataWinPath, dataWinPathSize, "%s", candidate); + return true; + } + return false; + } + + if (!directoryExists(candidate)) return false; + + char directPath[1024]; +#if N3DS_PREPROCESS_HOST_WINDOWS + snprintf(directPath, sizeof(directPath), "%s\\data.win", candidate); +#else + snprintf(directPath, sizeof(directPath), "%s/data.win", candidate); +#endif + if (fileExists(directPath)) { + snprintf(dataWinPath, dataWinPathSize, "%s", directPath); + return true; + } + +#if N3DS_PREPROCESS_HOST_WINDOWS + snprintf(directPath, sizeof(directPath), "%s\\UNDERTALE.exe", candidate); +#else + snprintf(directPath, sizeof(directPath), "%s/UNDERTALE.exe", candidate); +#endif + if (fileExists(directPath)) { +#if N3DS_PREPROCESS_HOST_WINDOWS + snprintf(dataWinPath, dataWinPathSize, "%s\\data.win", candidate); +#else + snprintf(dataWinPath, dataWinPathSize, "%s/data.win", candidate); +#endif + if (fileExists(dataWinPath)) return true; + } + + return false; +} + +static bool normalizeInputDataWinPath(Options* options) { + if (options == NULL || options->inputPath == NULL || options->inputPath[0] == '\0') return false; + if (!resolveUndertaleDataWinPath(options->inputPath, options->inputPathStorage, sizeof(options->inputPathStorage))) { + return false; + } + options->inputPath = options->inputPathStorage; + return true; +} + +#if N3DS_PREPROCESS_HOST_WINDOWS +static bool findUndertaleFromSteamRoot(const char* steamRoot, char* outDataWinPath, size_t outDataWinPathSize) { + if (steamRoot == NULL || steamRoot[0] == '\0') return false; + + char undertaleDir[1024]; + snprintf(undertaleDir, sizeof(undertaleDir), "%s\\steamapps\\common\\Undertale", steamRoot); + if (resolveUndertaleDataWinPath(undertaleDir, outDataWinPath, outDataWinPathSize)) return true; + + char libraryVdfPath[1024]; + snprintf(libraryVdfPath, sizeof(libraryVdfPath), "%s\\steamapps\\libraryfolders.vdf", steamRoot); + FILE* file = fopen(libraryVdfPath, "rb"); + if (file == NULL) return false; + + char line[2048]; + while (fgets(line, sizeof(line), file) != NULL) { + char first[1024]; + char second[1024]; + if (!getQuotedField(line, 0, first, sizeof(first))) continue; + if (!getQuotedField(line, 1, second, sizeof(second))) continue; + + bool looksLikePathField = strcmp(first, "path") == 0; + bool looksLikeLegacyLibraryField = true; + for (size_t i = 0; first[i] != '\0'; ++i) { + if (!isdigit((unsigned char) first[i])) { + looksLikeLegacyLibraryField = false; + break; + } + } + if (!looksLikePathField && !looksLikeLegacyLibraryField) continue; + + char candidateDir[1024]; + snprintf(candidateDir, sizeof(candidateDir), "%s\\steamapps\\common\\Undertale", second); + if (resolveUndertaleDataWinPath(candidateDir, outDataWinPath, outDataWinPathSize)) { + fclose(file); + return true; + } + } + + fclose(file); + return false; +} + +static bool autoDetectUndertaleDataWin(char* outDataWinPath, size_t outDataWinPathSize) { + char steamRoot[1024]; + if (readRegistryString(HKEY_CURRENT_USER, "Software\\Valve\\Steam", "SteamPath", steamRoot, sizeof(steamRoot))) { + if (findUndertaleFromSteamRoot(steamRoot, outDataWinPath, outDataWinPathSize)) return true; + } + if (readRegistryString(HKEY_LOCAL_MACHINE, "SOFTWARE\\WOW6432Node\\Valve\\Steam", "InstallPath", steamRoot, sizeof(steamRoot))) { + if (findUndertaleFromSteamRoot(steamRoot, outDataWinPath, outDataWinPathSize)) return true; + } + + const char* programFilesX86 = getenv("ProgramFiles(x86)"); + const char* programFiles = getenv("ProgramFiles"); + const char* fallbacks[] = { + programFilesX86, + programFiles, + "C:\\Program Files (x86)", + "C:\\Program Files", + }; + for (size_t i = 0; i < sizeof(fallbacks) / sizeof(fallbacks[0]); ++i) { + if (fallbacks[i] == NULL || fallbacks[i][0] == '\0') continue; + snprintf(steamRoot, sizeof(steamRoot), "%s\\Steam", fallbacks[i]); + if (findUndertaleFromSteamRoot(steamRoot, outDataWinPath, outDataWinPathSize)) return true; + } + + return false; +} +#endif + +static bool buildSdOutputDir(const char* sdInputPath, char* outOutputDir, size_t outOutputDirSize) { + if (sdInputPath == NULL || sdInputPath[0] == '\0') return false; + + char temp[1024]; + snprintf(temp, sizeof(temp), "%s", sdInputPath); + stripOptionalQuotes(temp); + + size_t len = strlen(temp); + while (len > 0 && (temp[len - 1] == '/' || temp[len - 1] == '\\')) { + if (pathLooksLikeRootDrive(temp) && len <= 3) break; + temp[--len] = '\0'; + } + + if (pathLooksLikeRootDrive(temp)) { + char driveRoot[4]; + snprintf(driveRoot, sizeof(driveRoot), "%c:", temp[0]); + snprintf( + outOutputDir, + outOutputDirSize, + "%s" N3DS_PREPROCESS_PATH_SEP "3ds" N3DS_PREPROCESS_PATH_SEP "cinnamon", + driveRoot + ); + return true; + } + + const char* lastSlash = strrchr(temp, '/'); + const char* lastBackslash = strrchr(temp, '\\'); + const char* base = lastSlash; + if (lastBackslash != NULL && (base == NULL || lastBackslash > base)) base = lastBackslash; + base = (base != NULL) ? base + 1 : temp; + + if (stringContainsIgnoreCase(temp, "\\3ds\\cinnamon") || stringContainsIgnoreCase(temp, "/3ds/cinnamon")) { + snprintf(outOutputDir, outOutputDirSize, "%s", temp); + return true; + } + if (strcmp(base, "cinnamon") == 0) { + snprintf(outOutputDir, outOutputDirSize, "%s", temp); + return true; + } + if (strcmp(base, "3ds") == 0) { + snprintf(outOutputDir, outOutputDirSize, "%s" N3DS_PREPROCESS_PATH_SEP "cinnamon", temp); + return true; + } + + snprintf(outOutputDir, outOutputDirSize, "%s" N3DS_PREPROCESS_PATH_SEP "3ds" N3DS_PREPROCESS_PATH_SEP "cinnamon", temp); + return true; +} + +static bool promptInteractivePaths(Options* options) { + if (options == NULL) return false; + + fprintf(stdout, "Nintendo 3DS asset preprocessor\n\n"); + +#if N3DS_PREPROCESS_HOST_WINDOWS + if (autoDetectUndertaleDataWin(options->inputPathStorage, sizeof(options->inputPathStorage))) { + fprintf(stdout, "Detected Undertale at: %s\n", options->inputPathStorage); + } else { + char manualPath[1024]; + while (true) { + if (!promptLine("Undertale was not auto-detected. Enter your Undertale folder or data.win path: ", manualPath, sizeof(manualPath))) { + return false; + } + if (resolveUndertaleDataWinPath(manualPath, options->inputPathStorage, sizeof(options->inputPathStorage))) break; + fprintf(stderr, "Could not find data.win there. Try the Undertale install folder or data.win directly.\n"); + } + } +#else + char manualPath[1024]; + while (true) { + if (!promptLine("Enter your Undertale folder or data.win path: ", manualPath, sizeof(manualPath))) { + return false; + } + if (resolveUndertaleDataWinPath(manualPath, options->inputPathStorage, sizeof(options->inputPathStorage))) break; + fprintf(stderr, "Could not find data.win there. Try again.\n"); + } +#endif + + options->inputPath = options->inputPathStorage; + + char sdPath[1024]; + while (true) { + if (!promptLine("Enter your SD card path (for example E:\\ or the mounted SD folder): ", sdPath, sizeof(sdPath))) { + return false; + } + if (sdPath[0] == '\0') { + fprintf(stderr, "Please enter a path.\n"); + continue; + } + if (!directoryExists(sdPath)) { + fprintf(stderr, "That path does not exist. Enter the SD root or an existing folder on the SD card.\n"); + continue; + } + if (!buildSdOutputDir(sdPath, options->outputDirStorage, sizeof(options->outputDirStorage))) { + fprintf(stderr, "Could not build an output path from that SD path.\n"); + continue; + } + break; + } + + options->outputDir = options->outputDirStorage; + + fprintf(stdout, "Writing assets to: %s\n", options->outputDir); + fprintf(stdout, "\n"); + return true; +} + +static void encodeFrame( + const int16_t* channelSamples, + uint32_t sampleCount, + uint32_t frameIndex, + const DspPredictorPair* predictor, + uint8_t shift, + int16_t startHist1, + int16_t startHist2, + uint8_t outFrame[8], + int16_t* outHist1, + int16_t* outHist2, + uint64_t* outError +) { + int scale = 1 << shift; + int16_t hist1 = startHist1; + int16_t hist2 = startHist2; + uint8_t frame[8]; + memset(frame, 0, sizeof(frame)); + frame[0] = (uint8_t) (((int) (predictor - N3DS_DSP_PREDICTORS) << 4) | (shift & 0x0F)); + uint64_t totalError = 0; + + repeat(14, i) { + uint32_t sampleIndex = frameIndex * 14u + (uint32_t) i; + int target = sampleIndex < sampleCount ? channelSamples[sampleIndex] : 0; + int predicted = (1024 + predictor->coef1 * hist1 + predictor->coef2 * hist2) >> 11; + int residual = target - predicted; + int nibble = clampNibble(roundDivSigned(residual, scale)); + int decoded = (((nibble * scale) << 11) + 1024 + predictor->coef1 * hist1 + predictor->coef2 * hist2) >> 11; + int error = target - decoded; + totalError += (uint64_t) (error * error); + + hist2 = hist1; + hist1 = (int16_t) decoded; + + uint8_t encodedNibble = (uint8_t) (nibble < 0 ? nibble + 16 : nibble); + uint32_t byteIndex = 1u + (uint32_t) i / 2u; + if ((i & 1) == 0) { + frame[byteIndex] = (uint8_t) (encodedNibble << 4); + } else { + frame[byteIndex] |= encodedNibble; + } + } + + memcpy(outFrame, frame, sizeof(frame)); + *outHist1 = hist1; + *outHist2 = hist2; + *outError = totalError; +} + +static bool encodeChannelDspAdpcm(const int16_t* channelSamples, uint32_t sampleCount, EncodedBcwavChannel* out) { + memset(out, 0, sizeof(*out)); + uint32_t frameCount = (sampleCount + 13u) / 14u; + out->dataSize = frameCount * 8u; + out->data = safeMalloc(out->dataSize); + out->startContext.predictorScale = 0; + out->startContext.yn1 = 0; + out->startContext.yn2 = 0; + out->loopContext = out->startContext; + + repeat(8, i) { + out->coefs[i * 2 + 0] = (uint16_t) N3DS_DSP_PREDICTORS[i].coef1; + out->coefs[i * 2 + 1] = (uint16_t) N3DS_DSP_PREDICTORS[i].coef2; + } + + int16_t hist1 = 0; + int16_t hist2 = 0; + repeat(frameCount, frameIndex) { + uint8_t bestFrame[8]; + int16_t bestHist1 = hist1; + int16_t bestHist2 = hist2; + uint64_t bestError = UINT64_MAX; + bool found = false; + + repeat(8, predictorIndex) { + repeat(14, shift) { + uint8_t frame[8]; + int16_t nextHist1 = hist1; + int16_t nextHist2 = hist2; + uint64_t error = 0; + encodeFrame( + channelSamples, + sampleCount, + (uint32_t) frameIndex, + &N3DS_DSP_PREDICTORS[predictorIndex], + (uint8_t) shift, + hist1, + hist2, + frame, + &nextHist1, + &nextHist2, + &error + ); + if (!found || error < bestError) { + memcpy(bestFrame, frame, sizeof(bestFrame)); + bestHist1 = nextHist1; + bestHist2 = nextHist2; + bestError = error; + found = true; + } + } + } + + if (!found) return false; + memcpy(out->data + (size_t) frameIndex * 8u, bestFrame, 8u); + if (frameIndex == 0) { + out->startContext.predictorScale = bestFrame[0]; + out->loopContext = out->startContext; + } + hist1 = bestHist1; + hist2 = bestHist2; + } + + return true; +} + +static void freeEncodedChannel(EncodedBcwavChannel* channel) { + free(channel->data); + memset(channel, 0, sizeof(*channel)); +} + +static N3DS_PREPROCESS_MAYBE_UNUSED bool writeBcwavFile(const char* path, const WavPcm16* wav) { + EncodedBcwavChannel channels[2]; + memset(channels, 0, sizeof(channels)); + bool ok = false; + uint32_t encodedSampleCount = ((wav->sampleCount + 13u) / 14u) * 14u; + if (encodedSampleCount == 0) return false; + + repeat(wav->channelCount, channelIndex) { + int16_t* mono = safeCalloc(encodedSampleCount, sizeof(int16_t)); + repeat(wav->sampleCount, sampleIndex) { + mono[sampleIndex] = wav->interleavedPcm[(size_t) sampleIndex * wav->channelCount + channelIndex]; + } + if (!encodeChannelDspAdpcm(mono, encodedSampleCount, &channels[channelIndex])) { + free(mono); + goto cleanup; + } + free(mono); + } + + uint32_t headerSize = 0x40u; + uint32_t infoOffset = headerSize; + uint32_t channelInfoOffset = 0x20u + wav->channelCount * 8u; + uint32_t adpcmInfoOffset = channelInfoOffset + wav->channelCount * 0x14u; + uint32_t infoSize = align32(adpcmInfoOffset + wav->channelCount * 0x2Eu); + uint32_t sampleDataSize = 0; + repeat(wav->channelCount, i) { + sampleDataSize += channels[i].dataSize; + } + uint32_t dataOffset = infoOffset + infoSize; + uint32_t dataPayloadOffset = align32(dataOffset + 8u); + uint32_t dataPayloadPadding = dataPayloadOffset - (dataOffset + 8u); + uint32_t dataSize = 8u + dataPayloadPadding + sampleDataSize; + uint32_t fileSize = dataOffset + dataSize; + uint32_t sampleRate = wav->sampleRate != 0 ? wav->sampleRate : N3DS_AUDIO_SAMPLE_RATE; + + uint8_t* blob = safeCalloc(1, fileSize); + memcpy(blob, "CWAV", 4); + writeLe16(blob + 4, 0xFEFFu); + writeLe16(blob + 6, (uint16_t) headerSize); + writeLe32(blob + 8, CWAV_VERSION); + writeLe32(blob + 0x0C, fileSize); + writeLe16(blob + 0x10, 2u); + writeLe16(blob + 0x14, CWAV_REF_INFO_BLOCK); + writeLe32(blob + 0x18, infoOffset); + writeLe32(blob + 0x1C, infoSize); + writeLe16(blob + 0x20, CWAV_REF_DATA_BLOCK); + writeLe32(blob + 0x24, dataOffset); + writeLe32(blob + 0x28, dataSize); + + uint8_t* info = blob + infoOffset; + memcpy(info, "INFO", 4); + writeLe32(info + 4, infoSize); + info[0x08] = 2; + info[0x09] = 0; + writeLe32(info + 0x0C, sampleRate); + writeLe32(info + 0x10, 0); + writeLe32(info + 0x14, encodedSampleCount); + writeLe32(info + 0x1C, wav->channelCount); + + uint32_t sampleOffset = 0; + repeat(wav->channelCount, channelIndex) { + uint8_t* ref = info + 0x20u + channelIndex * 8u; + uint32_t channelInfoPos = channelInfoOffset + channelIndex * 0x14u; + uint32_t adpcmInfoPos = adpcmInfoOffset + channelIndex * 0x2Eu; + writeLe16(ref + 0, CWAV_REF_CHANNEL_INFO); + writeLe32(ref + 4, channelInfoPos - 0x1Cu); + + uint8_t* channelInfo = info + channelInfoPos; + writeLe16(channelInfo + 0, CWAV_REF_SAMPLE_DATA); + writeLe32(channelInfo + 4, dataPayloadPadding + sampleOffset); + writeLe16(channelInfo + 8, CWAV_REF_DSP_ADPCM_INFO); + writeLe32(channelInfo + 12, adpcmInfoPos - channelInfoPos); + + uint8_t* adpcmInfo = info + adpcmInfoPos; + repeat(16, coefIndex) { + writeLe16(adpcmInfo + coefIndex * 2u, channels[channelIndex].coefs[coefIndex]); + } + adpcmInfo[0x20] = channels[channelIndex].startContext.predictorScale; + writeLe16(adpcmInfo + 0x22, (uint16_t) channels[channelIndex].startContext.yn1); + writeLe16(adpcmInfo + 0x24, (uint16_t) channels[channelIndex].startContext.yn2); + adpcmInfo[0x26] = channels[channelIndex].loopContext.predictorScale; + writeLe16(adpcmInfo + 0x28, (uint16_t) channels[channelIndex].loopContext.yn1); + writeLe16(adpcmInfo + 0x2A, (uint16_t) channels[channelIndex].loopContext.yn2); + + sampleOffset += channels[channelIndex].dataSize; + } + + uint8_t* dataBlock = blob + dataOffset; + memcpy(dataBlock, "DATA", 4); + writeLe32(dataBlock + 4, dataSize); + uint32_t dataCursor = dataPayloadOffset; + repeat(wav->channelCount, channelIndex) { + memcpy(blob + dataCursor, channels[channelIndex].data, channels[channelIndex].dataSize); + dataCursor += channels[channelIndex].dataSize; + } + + ok = writeBytesToFile(path, blob, fileSize); + free(blob); + +cleanup: + repeat(2, i) freeEncodedChannel(&channels[i]); + return ok; +} + +static char* resolveExternalSoundPath(const Options* options, const Sound* sound) { + char* inputDir = dupParentDir(options->inputPath); + const char* candidates[2] = { sound->file, sound->name }; + + repeat(2, candidateIndex) { + const char* base = candidates[candidateIndex]; + if (base == NULL || base[0] == '\0') continue; + + char* direct = joinPath(inputDir, base); + if (fileExists(direct)) { + free(inputDir); + return direct; + } + free(direct); + + if (strchr(base, '.') == NULL) { + const char* exts[] = { ".ogg", ".wav", ".mp3" }; + repeat(3, i) { + char relative[1024]; + snprintf(relative, sizeof(relative), "%s%s", base, exts[i]); + char* candidate = joinPath(inputDir, relative); + if (fileExists(candidate)) { + free(inputDir); + return candidate; + } + free(candidate); + } + } + } + + free(inputDir); + return NULL; +} + +static bool loadPcmForSoundBank( + const Options* options, + const Sound* sound, + const uint8_t* embeddedData, + uint32_t embeddedDataSize, + size_t soundIndex, + WavPcm16* outPcm +) { + if (options == NULL || sound == NULL || outPcm == NULL) return false; + memset(outPcm, 0, sizeof(*outPcm)); + + char* externalSourcePath = resolveExternalSoundPath(options, sound); + const char* inputPath = externalSourcePath; + char tempInputPath[1024]; + bool wroteTempInput = false; + + if (inputPath == NULL && embeddedData != NULL && embeddedDataSize > 0) { + if (loadAudioPcm16FromMemory(embeddedData, embeddedDataSize, outPcm)) { + free(externalSourcePath); + return true; + } + + const char* tempExt = pickAudioTempExtension(sound); + snprintf(tempInputPath, sizeof(tempInputPath), "%s/audio/__tmp_bank_input_%05zu%s", options->outputDir, soundIndex, tempExt); + remove(tempInputPath); + if (!writeBytesToFile(tempInputPath, embeddedData, embeddedDataSize)) { + free(externalSourcePath); + return false; + } + inputPath = tempInputPath; + wroteTempInput = true; + } + + if (inputPath == NULL) { + free(externalSourcePath); + return false; + } + + bool ok = loadAudioPcm16(inputPath, outPcm); + + if (wroteTempInput) remove(tempInputPath); + free(externalSourcePath); + return ok; +} + +static bool buildPackedSoundBank(Options* options, DataWin* dataWin) { + if (options == NULL || dataWin == NULL) return false; + + uint32_t soundCount = (uint32_t) dataWin->sond.count; + uint32_t headerSize = 16u + soundCount * 20u; + uint8_t* header = safeCalloc(1, headerSize); + uint32_t* offsets = safeCalloc(soundCount, sizeof(uint32_t)); + uint32_t* sizes = safeCalloc(soundCount, sizeof(uint32_t)); + uint32_t* sampleRates = safeCalloc(soundCount, sizeof(uint32_t)); + uint32_t* sampleCounts = safeCalloc(soundCount, sizeof(uint32_t)); + uint32_t* flags = safeCalloc(soundCount, sizeof(uint32_t)); + char bankPath[1024]; + FILE* bankFile = NULL; + bool ok = false; + uint32_t packedCount = 0; + uint32_t packedBytes = 0; + + snprintf(bankPath, sizeof(bankPath), "%s/audio/sound_bank.bin", options->outputDir); + bankFile = fopen(bankPath, "wb"); + if (bankFile == NULL) goto cleanup; + if (fseek(bankFile, (long) headerSize, SEEK_SET) != 0) goto cleanup; + + uint32_t cursor = headerSize; + repeat(dataWin->sond.count, soundIndex) { + const Sound* sound = &dataWin->sond.sounds[soundIndex]; + if (soundLooksLikeMusic(sound)) continue; + + WavPcm16 pcm; + const uint8_t* sourceData = NULL; + uint32_t sourceSize = 0; + if (sound->audioFile >= 0 && (uint32_t) sound->audioFile < dataWin->audo.count) { + AudioEntry* entry = &dataWin->audo.entries[sound->audioFile]; + sourceData = entry->data; + sourceSize = entry->dataSize; + } + + if (!loadPcmForSoundBank( + options, + sound, + sourceData, + sourceSize, + soundIndex, + &pcm + )) { + if (sound->name != NULL && sound->name[0] != '\0') { + fprintf(stderr, "Skipping sound bank entry %zu (%s): unsupported or missing audio source\n", soundIndex, sound->name); + } + continue; + } + + if (!resampleWavPcm16(&pcm, N3DS_AUDIO_SAMPLE_RATE)) { + fprintf(stderr, "Skipping sound bank entry %zu (%s): failed to resample audio to %u Hz\n", soundIndex, sound->name != NULL ? sound->name : "", N3DS_AUDIO_SAMPLE_RATE); + freeWavPcm16(&pcm); + continue; + } + + uint32_t sampleRate = pcm.sampleRate != 0 ? pcm.sampleRate : N3DS_AUDIO_SAMPLE_RATE; + uint32_t sampleCount = pcm.sampleCount; + uint32_t channelCount = pcm.channelCount; + uint32_t pcmBytes = (uint32_t) ((size_t) pcm.sampleCount * pcm.channelCount * sizeof(int16_t)); + if (fwrite(pcm.interleavedPcm, 1, pcmBytes, bankFile) != pcmBytes) { + freeWavPcm16(&pcm); + goto cleanup; + } + freeWavPcm16(&pcm); + + offsets[soundIndex] = cursor; + sizes[soundIndex] = pcmBytes; + sampleRates[soundIndex] = sampleRate; + sampleCounts[soundIndex] = sampleCount; + flags[soundIndex] = + channelCount | + N3DS_SOUND_BANK_ENTRY_FLAG_PCM16 | + 0u; + cursor += pcmBytes; + packedCount += 1u; + packedBytes += pcmBytes; + } + + writeLe32(header + 0u, N3DS_SOUND_BANK_MAGIC); + writeLe32(header + 4u, N3DS_SOUND_BANK_VERSION); + writeLe32(header + 8u, soundCount); + writeLe32(header + 12u, headerSize); + repeat(soundCount, soundIndex) { + uint32_t entryOffset = 16u + soundIndex * 20u; + writeLe32(header + entryOffset + 0u, offsets[soundIndex]); + writeLe32(header + entryOffset + 4u, sizes[soundIndex]); + writeLe32(header + entryOffset + 8u, sampleRates[soundIndex]); + writeLe32(header + entryOffset + 12u, sampleCounts[soundIndex]); + writeLe32(header + entryOffset + 16u, flags[soundIndex]); + } + + if (fseek(bankFile, 0, SEEK_SET) != 0) goto cleanup; + if (fwrite(header, 1, headerSize, bankFile) != headerSize) goto cleanup; + + fprintf( + stderr, + "Packed %u sounds into %s (%u bytes)\n", + packedCount, + bankPath, + packedBytes + ); + ok = true; + +cleanup: + free(header); + free(offsets); + free(sizes); + free(sampleRates); + free(sampleCounts); + free(flags); + if (bankFile != NULL) fclose(bankFile); + return ok; +} + +static bool convertStreamedMusicBcwavs(Options* options, DataWin* dataWin) { + if (options == NULL || dataWin == NULL) return false; + + bool allOk = true; + uint32_t convertedCount = 0; + uint32_t preservedCount = 0; + uint32_t skippedCount = 0; + + repeat(dataWin->sond.count, soundIndex) { + const Sound* sound = &dataWin->sond.sounds[soundIndex]; + if (!soundLooksLikeMusic(sound)) continue; + + char baseName[256]; + bool haveBaseName = extractBaseNameNoExt(sound->file, baseName, sizeof(baseName)); + if (!haveBaseName) haveBaseName = extractBaseNameNoExt(sound->name, baseName, sizeof(baseName)); + if (!haveBaseName) snprintf(baseName, sizeof(baseName), "sound_%05zu", soundIndex); + + char outPath[1024]; + snprintf(outPath, sizeof(outPath), "%s/%s.bcwav", options->outputDir, baseName); + + char* externalSourcePath = resolveExternalSoundPath(options, sound); + if (externalSourcePath == NULL) { + if (fileExists(outPath)) { + preservedCount++; + continue; + } + if (sound->name != NULL && sound->name[0] != '\0') { + fprintf(stderr, "Skipping streamed music %zu (%s): no external audio source found\n", soundIndex, sound->name); + } + skippedCount++; + continue; + } + + remove(outPath); + fprintf( + stderr, + "n3ds-preprocess: music %05zu -> %s [%s]\n", + soundIndex, + outPath, + externalSourcePath + ); + + bool ok = writeBcwavFromInputFile(externalSourcePath, outPath); + free(externalSourcePath); + + if (!ok) { + fprintf(stderr, "Failed to write streamed music BCWAV for sound %zu\n", soundIndex); + allOk = false; + continue; + } + + convertedCount++; + } + + fprintf( + stderr, + "Streamed music BCWAVs: converted=%u preserved=%u skipped=%u\n", + convertedCount, + preservedCount, + skippedCount + ); + return allOk; +} + +static bool convertAudio(Options* options, DataWin* dataWin) { + fprintf(stderr, "n3ds-preprocess: processing audio\n"); + fprintf(stderr, "Using in-process audio conversion (WAV and Ogg Vorbis supported; other codecs unavailable).\n"); + + bool ok = convertStreamedMusicBcwavs(options, dataWin); + if (ok) ok = buildPackedSoundBank(options, dataWin); + if (ok) fprintf(stderr, "Packed SFX audio into PCM16 sound_bank.bin at %u Hz\n", N3DS_AUDIO_SAMPLE_RATE); + return ok; +} + +int main(int argc, char** argv) { + Options options; + if (!parseArgs(argc, argv, &options)) { + printUsage(argv[0]); + return 1; + } + + if (options.interactiveMode && !promptInteractivePaths(&options)) { + fprintf(stderr, "Interactive setup was cancelled or failed.\n"); + return 1; + } + + if (options.inputPath == NULL || options.outputDir == NULL) { + printUsage(argv[0]); + return 1; + } + + if (!normalizeInputDataWinPath(&options)) { + fprintf(stderr, "Could not resolve data.win from: %s\n", options.inputPath); + fprintf(stderr, "Pass either the Undertale folder or the full path to data.win.\n"); + return 1; + } + + if (!configureStagedOutputDir(&options, argv[0])) { + fprintf(stderr, "Failed to configure local staging output directory.\n"); + return 1; + } + if (!configureSpriteReplacementDir(&options, argv[0])) { + fprintf(stderr, "Failed to configure sprite replacement directory.\n"); + return 1; + } + if (!configureBorderAssetDir(&options, argv[0])) { + fprintf(stderr, "Failed to configure border asset directory.\n"); + return 1; + } + if (options.stageOutputLocally) { + fprintf(stderr, "Staging generated assets in: %s\n", options.outputDir); + fprintf(stderr, "Final destination: %s\n", options.finalOutputDirStorage); + } + + if (!ensureOutputDirs(options.outputDir)) return 1; + if (!convertBorderAssets(&options)) return 1; + + DataWin* dataWin = DataWin_parse( + options.inputPath, + (DataWinParserOptions) { + .parseGen8 = true, + .parseSond = true, + .parseSprt = true, + .parseBgnd = true, + .parseFont = true, + .parseRoom = true, + .parseTpag = true, + .parseStrg = true, + .parseTxtr = true, + .parseAudo = true, + .skipLoadingPreciseMasksForNonPreciseSprites = true, + } + ); + if (dataWin == NULL) { + fprintf(stderr, "Failed to parse %s\n", options.inputPath); + return 1; + } + + bool ok = convertTextures(&options, dataWin); + if (ok) ok = convertAudio(&options, dataWin); + DataWin_free(dataWin); + + bool syncedStaging = true; + if (options.stageOutputLocally) { + syncedStaging = ok && syncStagedOutputToDestination(&options); + ok = syncedStaging; + } + if (syncedStaging && options.stageOutputLocally && options.stagingOutputDirStorage[0] != '\0' && directoryExists(options.stagingOutputDirStorage)) { + if (!deleteDirectoryRecursive(options.stagingOutputDirStorage)) { + fprintf(stderr, "Warning: failed to remove staging directory: %s\n", options.stagingOutputDirStorage); + } + } else if (options.stageOutputLocally && options.stagingOutputDirStorage[0] != '\0' && directoryExists(options.stagingOutputDirStorage)) { + fprintf(stderr, "Preserving staging directory after failure: %s\n", options.stagingOutputDirStorage); + } + + if (!ok) return 1; + const char* finalOutputDir = options.stageOutputLocally ? options.finalOutputDirStorage : options.outputDir; + fprintf(stderr, "Wrote 3DS assets to %s/gfx and %s/audio\n", finalOutputDir, finalOutputDir); + return 0; +} diff --git a/tools/n3ds-preprocess/stb_impl.c b/tools/n3ds-preprocess/stb_impl.c new file mode 100644 index 00000000..05313351 --- /dev/null +++ b/tools/n3ds-preprocess/stb_impl.c @@ -0,0 +1,11 @@ +#define STBI_NO_THREAD_LOCALS +#define STB_IMAGE_IMPLEMENTATION +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + +#define STB_DS_IMPLEMENTATION +#include + +#include "../../vendor/stb/vorbis/stb_vorbis.c" diff --git a/vendor/stb/ds/stb_ds.h b/vendor/stb/ds/stb_ds.h index b7bdd52b..e84c82d1 100644 --- a/vendor/stb/ds/stb_ds.h +++ b/vendor/stb/ds/stb_ds.h @@ -1,1895 +1,1895 @@ -/* stb_ds.h - v0.67 - public domain data structures - Sean Barrett 2019 - - This is a single-header-file library that provides easy-to-use - dynamic arrays and hash tables for C (also works in C++). - - For a gentle introduction: - http://nothings.org/stb_ds - - To use this library, do this in *one* C or C++ file: - #define STB_DS_IMPLEMENTATION - #include "stb_ds.h" - -TABLE OF CONTENTS - - Table of Contents - Compile-time options - License - Documentation - Notes - Notes - Dynamic arrays - Notes - Hash maps - Credits - -COMPILE-TIME OPTIONS - - #define STBDS_NO_SHORT_NAMES - - This flag needs to be set globally. - - By default stb_ds exposes shorter function names that are not qualified - with the "stbds_" prefix. If these names conflict with the names in your - code, define this flag. - - #define STBDS_SIPHASH_2_4 - - This flag only needs to be set in the file containing #define STB_DS_IMPLEMENTATION. - - By default stb_ds.h hashes using a weaker variant of SipHash and a custom hash for - 4- and 8-byte keys. On 64-bit platforms, you can define the above flag to force - stb_ds.h to use specification-compliant SipHash-2-4 for all keys. Doing so makes - hash table insertion about 20% slower on 4- and 8-byte keys, 5% slower on - 64-byte keys, and 10% slower on 256-byte keys on my test computer. - - #define STBDS_REALLOC(context,ptr,size) better_realloc - #define STBDS_FREE(context,ptr) better_free - - These defines only need to be set in the file containing #define STB_DS_IMPLEMENTATION. - - By default stb_ds uses stdlib realloc() and free() for memory management. You can - substitute your own functions instead by defining these symbols. You must either - define both, or neither. Note that at the moment, 'context' will always be NULL. - @TODO add an array/hash initialization function that takes a memory context pointer. - - #define STBDS_UNIT_TESTS - - Defines a function stbds_unit_tests() that checks the functioning of the data structures. - - Note that on older versions of gcc (e.g. 5.x.x) you may need to build with '-std=c++0x' - (or equivalentally '-std=c++11') when using anonymous structures as seen on the web - page or in STBDS_UNIT_TESTS. - -LICENSE - - Placed in the public domain and also MIT licensed. - See end of file for detailed license information. - -DOCUMENTATION - - Dynamic Arrays - - Non-function interface: - - Declare an empty dynamic array of type T - T* foo = NULL; - - Access the i'th item of a dynamic array 'foo' of type T, T* foo: - foo[i] - - Functions (actually macros) - - arrfree: - void arrfree(T*); - Frees the array. - - arrlen: - ptrdiff_t arrlen(T*); - Returns the number of elements in the array. - - arrlenu: - size_t arrlenu(T*); - Returns the number of elements in the array as an unsigned type. - - arrpop: - T arrpop(T* a) - Removes the final element of the array and returns it. - - arrput: - T arrput(T* a, T b); - Appends the item b to the end of array a. Returns b. - - arrins: - T arrins(T* a, int p, T b); - Inserts the item b into the middle of array a, into a[p], - moving the rest of the array over. Returns b. - - arrinsn: - void arrinsn(T* a, int p, int n); - Inserts n uninitialized items into array a starting at a[p], - moving the rest of the array over. - - arraddnptr: - T* arraddnptr(T* a, int n) - Appends n uninitialized items onto array at the end. - Returns a pointer to the first uninitialized item added. - - arraddnindex: - size_t arraddnindex(T* a, int n) - Appends n uninitialized items onto array at the end. - Returns the index of the first uninitialized item added. - - arrdel: - void arrdel(T* a, int p); - Deletes the element at a[p], moving the rest of the array over. - - arrdeln: - void arrdeln(T* a, int p, int n); - Deletes n elements starting at a[p], moving the rest of the array over. - - arrdelswap: - void arrdelswap(T* a, int p); - Deletes the element at a[p], replacing it with the element from - the end of the array. O(1) performance. - - arrsetlen: - void arrsetlen(T* a, int n); - Changes the length of the array to n. Allocates uninitialized - slots at the end if necessary. - - arrsetcap: - size_t arrsetcap(T* a, int n); - Sets the length of allocated storage to at least n. It will not - change the length of the array. - - arrcap: - size_t arrcap(T* a); - Returns the number of total elements the array can contain without - needing to be reallocated. - - Hash maps & String hash maps - - Given T is a structure type: struct { TK key; TV value; }. Note that some - functions do not require TV value and can have other fields. For string - hash maps, TK must be 'char *'. - - Special interface: - - stbds_rand_seed: - void stbds_rand_seed(size_t seed); - For security against adversarially chosen data, you should seed the - library with a strong random number. Or at least seed it with time(). - - stbds_hash_string: - size_t stbds_hash_string(char *str, size_t seed); - Returns a hash value for a string. - - stbds_hash_bytes: - size_t stbds_hash_bytes(void *p, size_t len, size_t seed); - These functions hash an arbitrary number of bytes. The function - uses a custom hash for 4- and 8-byte data, and a weakened version - of SipHash for everything else. On 64-bit platforms you can get - specification-compliant SipHash-2-4 on all data by defining - STBDS_SIPHASH_2_4, at a significant cost in speed. - - Non-function interface: - - Declare an empty hash map of type T - T* foo = NULL; - - Access the i'th entry in a hash table T* foo: - foo[i] - - Function interface (actually macros): - - hmfree - shfree - void hmfree(T*); - void shfree(T*); - Frees the hashmap and sets the pointer to NULL. - - hmlen - shlen - ptrdiff_t hmlen(T*) - ptrdiff_t shlen(T*) - Returns the number of elements in the hashmap. - - hmlenu - shlenu - size_t hmlenu(T*) - size_t shlenu(T*) - Returns the number of elements in the hashmap. - - hmgeti - shgeti - hmgeti_ts - ptrdiff_t hmgeti(T*, TK key) - ptrdiff_t shgeti(T*, char* key) - ptrdiff_t hmgeti_ts(T*, TK key, ptrdiff_t tempvar) - Returns the index in the hashmap which has the key 'key', or -1 - if the key is not present. - - hmget - hmget_ts - shget - TV hmget(T*, TK key) - TV shget(T*, char* key) - TV hmget_ts(T*, TK key, ptrdiff_t tempvar) - Returns the value corresponding to 'key' in the hashmap. - The structure must have a 'value' field - - hmgets - shgets - T hmgets(T*, TK key) - T shgets(T*, char* key) - Returns the structure corresponding to 'key' in the hashmap. - - hmgetp - shgetp - hmgetp_ts - hmgetp_null - shgetp_null - T* hmgetp(T*, TK key) - T* shgetp(T*, char* key) - T* hmgetp_ts(T*, TK key, ptrdiff_t tempvar) - T* hmgetp_null(T*, TK key) - T* shgetp_null(T*, char *key) - Returns a pointer to the structure corresponding to 'key' in - the hashmap. Functions ending in "_null" return NULL if the key - is not present in the hashmap; the others return a pointer to a - structure holding the default value (but not the searched-for key). - - hmdefault - shdefault - TV hmdefault(T*, TV value) - TV shdefault(T*, TV value) - Sets the default value for the hashmap, the value which will be - returned by hmget/shget if the key is not present. - - hmdefaults - shdefaults - TV hmdefaults(T*, T item) - TV shdefaults(T*, T item) - Sets the default struct for the hashmap, the contents which will be - returned by hmgets/shgets if the key is not present. - - hmput - shput - TV hmput(T*, TK key, TV value) - TV shput(T*, char* key, TV value) - Inserts a pair into the hashmap. If the key is already - present in the hashmap, updates its value. - - hmputs - shputs - T hmputs(T*, T item) - T shputs(T*, T item) - Inserts a struct with T.key into the hashmap. If the struct is already - present in the hashmap, updates it. - - hmdel - shdel - int hmdel(T*, TK key) - int shdel(T*, char* key) - If 'key' is in the hashmap, deletes its entry and returns 1. - Otherwise returns 0. - - Function interface (actually macros) for strings only: - - sh_new_strdup - void sh_new_strdup(T*); - Overwrites the existing pointer with a newly allocated - string hashmap which will automatically allocate and free - each string key using realloc/free - - sh_new_arena - void sh_new_arena(T*); - Overwrites the existing pointer with a newly allocated - string hashmap which will automatically allocate each string - key to a string arena. Every string key ever used by this - hash table remains in the arena until the arena is freed. - Additionally, any key which is deleted and reinserted will - be allocated multiple times in the string arena. - -NOTES - - * These data structures are realloc'd when they grow, and the macro - "functions" write to the provided pointer. This means: (a) the pointer - must be an lvalue, and (b) the pointer to the data structure is not - stable, and you must maintain it the same as you would a realloc'd - pointer. For example, if you pass a pointer to a dynamic array to a - function which updates it, the function must return back the new - pointer to the caller. This is the price of trying to do this in C. - - * The following are the only functions that are thread-safe on a single data - structure, i.e. can be run in multiple threads simultaneously on the same - data structure - hmlen shlen - hmlenu shlenu - hmget_ts shget_ts - hmgeti_ts shgeti_ts - hmgets_ts shgets_ts - - * You iterate over the contents of a dynamic array and a hashmap in exactly - the same way, using arrlen/hmlen/shlen: - - for (i=0; i < arrlen(foo); ++i) - ... foo[i] ... - - * All operations except arrins/arrdel are O(1) amortized, but individual - operations can be slow, so these data structures may not be suitable - for real time use. Dynamic arrays double in capacity as needed, so - elements are copied an average of once. Hash tables double/halve - their size as needed, with appropriate hysteresis to maintain O(1) - performance. - -NOTES - DYNAMIC ARRAY - - * If you know how long a dynamic array is going to be in advance, you can avoid - extra memory allocations by using arrsetlen to allocate it to that length in - advance and use foo[n] while filling it out, or arrsetcap to allocate the memory - for that length and use arrput/arrpush as normal. - - * Unlike some other versions of the dynamic array, this version should - be safe to use with strict-aliasing optimizations. - -NOTES - HASH MAP - - * For compilers other than GCC and clang (e.g. Visual Studio), for hmput/hmget/hmdel - and variants, the key must be an lvalue (so the macro can take the address of it). - Extensions are used that eliminate this requirement if you're using C99 and later - in GCC or clang, or if you're using C++ in GCC. But note that this can make your - code less portable. - - * To test for presence of a key in a hashmap, just do 'hmgeti(foo,key) >= 0'. - - * The iteration order of your data in the hashmap is determined solely by the - order of insertions and deletions. In particular, if you never delete, new - keys are always added at the end of the array. This will be consistent - across all platforms and versions of the library. However, you should not - attempt to serialize the internal hash table, as the hash is not consistent - between different platforms, and may change with future versions of the library. - - * Use sh_new_arena() for string hashmaps that you never delete from. Initialize - with NULL if you're managing the memory for your strings, or your strings are - never freed (at least until the hashmap is freed). Otherwise, use sh_new_strdup(). - @TODO: make an arena variant that garbage collects the strings with a trivial - copy collector into a new arena whenever the table shrinks / rebuilds. Since - current arena recommendation is to only use arena if it never deletes, then - this can just replace current arena implementation. - - * If adversarial input is a serious concern and you're on a 64-bit platform, - enable STBDS_SIPHASH_2_4 (see the 'Compile-time options' section), and pass - a strong random number to stbds_rand_seed. - - * The default value for the hash table is stored in foo[-1], so if you - use code like 'hmget(T,k)->value = 5' you can accidentally overwrite - the value stored by hmdefault if 'k' is not present. - -CREDITS - - Sean Barrett -- library, idea for dynamic array API/implementation - Per Vognsen -- idea for hash table API/implementation - Rafael Sachetto -- arrpop() - github:HeroicKatora -- arraddn() reworking - - Bugfixes: - Andy Durdin - Shane Liesegang - Vinh Truong - Andreas Molzer - github:hashitaku - github:srdjanstipic - Macoy Madson - Andreas Vennstrom - Tobias Mansfield-Williams -*/ - -#ifdef STBDS_UNIT_TESTS -#define _CRT_SECURE_NO_WARNINGS -#endif - -#ifndef INCLUDE_STB_DS_H -#define INCLUDE_STB_DS_H - -#include -#include - -#ifndef STBDS_NO_SHORT_NAMES -#define arrlen stbds_arrlen -#define arrlenu stbds_arrlenu -#define arrput stbds_arrput -#define arrpush stbds_arrput -#define arrpop stbds_arrpop -#define arrfree stbds_arrfree -#define arraddn stbds_arraddn // deprecated, use one of the following instead: -#define arraddnptr stbds_arraddnptr -#define arraddnindex stbds_arraddnindex -#define arrsetlen stbds_arrsetlen -#define arrlast stbds_arrlast -#define arrins stbds_arrins -#define arrinsn stbds_arrinsn -#define arrdel stbds_arrdel -#define arrdeln stbds_arrdeln -#define arrdelswap stbds_arrdelswap -#define arrcap stbds_arrcap -#define arrsetcap stbds_arrsetcap - -#define hmput stbds_hmput -#define hmputs stbds_hmputs -#define hmget stbds_hmget -#define hmget_ts stbds_hmget_ts -#define hmgets stbds_hmgets -#define hmgetp stbds_hmgetp -#define hmgetp_ts stbds_hmgetp_ts -#define hmgetp_null stbds_hmgetp_null -#define hmgeti stbds_hmgeti -#define hmgeti_ts stbds_hmgeti_ts -#define hmdel stbds_hmdel -#define hmlen stbds_hmlen -#define hmlenu stbds_hmlenu -#define hmfree stbds_hmfree -#define hmdefault stbds_hmdefault -#define hmdefaults stbds_hmdefaults - -#define shput stbds_shput -#define shputi stbds_shputi -#define shputs stbds_shputs -#define shget stbds_shget -#define shgeti stbds_shgeti -#define shgets stbds_shgets -#define shgetp stbds_shgetp -#define shgetp_null stbds_shgetp_null -#define shdel stbds_shdel -#define shlen stbds_shlen -#define shlenu stbds_shlenu -#define shfree stbds_shfree -#define shdefault stbds_shdefault -#define shdefaults stbds_shdefaults -#define sh_new_arena stbds_sh_new_arena -#define sh_new_strdup stbds_sh_new_strdup - -#define stralloc stbds_stralloc -#define strreset stbds_strreset -#endif - -#if defined(STBDS_REALLOC) && !defined(STBDS_FREE) || !defined(STBDS_REALLOC) && defined(STBDS_FREE) -#error "You must define both STBDS_REALLOC and STBDS_FREE, or neither." -#endif -#if !defined(STBDS_REALLOC) && !defined(STBDS_FREE) -#include -#define STBDS_REALLOC(c,p,s) realloc(p,s) -#define STBDS_FREE(c,p) free(p) -#endif - -#ifdef _MSC_VER -#define STBDS_NOTUSED(v) (void)(v) -#else -#define STBDS_NOTUSED(v) (void)sizeof(v) -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -// for security against attackers, seed the library with a random number, at least time() but stronger is better -extern void stbds_rand_seed(size_t seed); - -// these are the hash functions used internally if you want to test them or use them for other purposes -extern size_t stbds_hash_bytes(void *p, size_t len, size_t seed); -extern size_t stbds_hash_string(char *str, size_t seed); - -// this is a simple string arena allocator, initialize with e.g. 'stbds_string_arena my_arena={0}'. -typedef struct stbds_string_arena stbds_string_arena; -extern char * stbds_stralloc(stbds_string_arena *a, char *str); -extern void stbds_strreset(stbds_string_arena *a); - -// have to #define STBDS_UNIT_TESTS to call this -extern void stbds_unit_tests(void); - -/////////////// -// -// Everything below here is implementation details -// - -extern void * stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap); -extern void stbds_arrfreef(void *a); -extern void stbds_hmfree_func(void *p, size_t elemsize); -extern void * stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); -extern void * stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode); -extern void * stbds_hmput_default(void *a, size_t elemsize); -extern void * stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); -extern void * stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode); -extern void * stbds_shmode_func(size_t elemsize, int mode); - -#ifdef __cplusplus -} -#endif - -#if defined(__GNUC__) || defined(__clang__) -#define STBDS_HAS_TYPEOF -#ifdef __cplusplus -//#define STBDS_HAS_LITERAL_ARRAY // this is currently broken for clang -#endif -#endif - -#if !defined(__cplusplus) -#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L -#define STBDS_HAS_LITERAL_ARRAY -#endif -#endif - -// this macro takes the address of the argument, but on gcc/clang can accept rvalues -#if defined(STBDS_HAS_LITERAL_ARRAY) && defined(STBDS_HAS_TYPEOF) - #if __clang__ - #define STBDS_ADDRESSOF(typevar, value) ((__typeof__(typevar)[1]){value}) // literal array decays to pointer to value - #else - #define STBDS_ADDRESSOF(typevar, value) ((typeof(typevar)[1]){value}) // literal array decays to pointer to value - #endif -#else -#define STBDS_ADDRESSOF(typevar, value) &(value) -#endif - -#define STBDS_OFFSETOF(var,field) ((char *) &(var)->field - (char *) (var)) - -#define stbds_header(t) ((stbds_array_header *) (t) - 1) -#define stbds_temp(t) stbds_header(t)->temp -#define stbds_temp_key(t) (*(char **) stbds_header(t)->hash_table) - -#define stbds_arrsetcap(a,n) (stbds_arrgrow(a,0,n)) -#define stbds_arrsetlen(a,n) ((stbds_arrcap(a) < (size_t) (n) ? stbds_arrsetcap((a),(size_t)(n)),0 : 0), (a) ? stbds_header(a)->length = (size_t) (n) : 0) -#define stbds_arrcap(a) ((a) ? stbds_header(a)->capacity : 0) -#define stbds_arrlen(a) ((a) ? (ptrdiff_t) stbds_header(a)->length : 0) -#define stbds_arrlenu(a) ((a) ? stbds_header(a)->length : 0) -#define stbds_arrput(a,v) (stbds_arrmaybegrow(a,1), (a)[stbds_header(a)->length++] = (v)) -#define stbds_arrpush stbds_arrput // synonym -#define stbds_arrpop(a) (stbds_header(a)->length--, (a)[stbds_header(a)->length]) -#define stbds_arraddn(a,n) ((void)(stbds_arraddnindex(a, n))) // deprecated, use one of the following instead: -#define stbds_arraddnptr(a,n) (stbds_arrmaybegrow(a,n), (n) ? (stbds_header(a)->length += (n), &(a)[stbds_header(a)->length-(n)]) : (a)) -#define stbds_arraddnindex(a,n)(stbds_arrmaybegrow(a,n), (n) ? (stbds_header(a)->length += (n), stbds_header(a)->length-(n)) : stbds_arrlen(a)) -#define stbds_arraddnoff stbds_arraddnindex -#define stbds_arrlast(a) ((a)[stbds_header(a)->length-1]) -#define stbds_arrfree(a) ((void) ((a) ? STBDS_FREE(NULL,stbds_header(a)) : (void)0), (a)=NULL) -#define stbds_arrdel(a,i) stbds_arrdeln(a,i,1) -#define stbds_arrdeln(a,i,n) (memmove(&(a)[i], &(a)[(i)+(n)], sizeof *(a) * (stbds_header(a)->length-(n)-(i))), stbds_header(a)->length -= (n)) -#define stbds_arrdelswap(a,i) ((a)[i] = stbds_arrlast(a), stbds_header(a)->length -= 1) -#define stbds_arrinsn(a,i,n) (stbds_arraddn((a),(n)), memmove(&(a)[(i)+(n)], &(a)[i], sizeof *(a) * (stbds_header(a)->length-(n)-(i)))) -#define stbds_arrins(a,i,v) (stbds_arrinsn((a),(i),1), (a)[i]=(v)) - -#define stbds_arrmaybegrow(a,n) ((!(a) || stbds_header(a)->length + (n) > stbds_header(a)->capacity) \ - ? (stbds_arrgrow(a,n,0),0) : 0) - -#define stbds_arrgrow(a,b,c) ((a) = stbds_arrgrowf_wrapper((a), sizeof *(a), (b), (c))) - -#define stbds_hmput(t, k, v) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, 0), \ - (t)[stbds_temp((t)-1)].key = (k), \ - (t)[stbds_temp((t)-1)].value = (v)) - -#define stbds_hmputs(t, s) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), &(s).key, sizeof (s).key, STBDS_HM_BINARY), \ - (t)[stbds_temp((t)-1)] = (s)) - -#define stbds_hmgeti(t,k) \ - ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, STBDS_HM_BINARY), \ - stbds_temp((t)-1)) - -#define stbds_hmgeti_ts(t,k,temp) \ - ((t) = stbds_hmget_key_ts_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, &(temp), STBDS_HM_BINARY), \ - (temp)) - -#define stbds_hmgetp(t, k) \ - ((void) stbds_hmgeti(t,k), &(t)[stbds_temp((t)-1)]) - -#define stbds_hmgetp_ts(t, k, temp) \ - ((void) stbds_hmgeti_ts(t,k,temp), &(t)[temp]) - -#define stbds_hmdel(t,k) \ - (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, STBDS_OFFSETOF((t),key), STBDS_HM_BINARY)),(t)?stbds_temp((t)-1):0) - -#define stbds_hmdefault(t, v) \ - ((t) = stbds_hmput_default_wrapper((t), sizeof *(t)), (t)[-1].value = (v)) - -#define stbds_hmdefaults(t, s) \ - ((t) = stbds_hmput_default_wrapper((t), sizeof *(t)), (t)[-1] = (s)) - -#define stbds_hmfree(p) \ - ((void) ((p) != NULL ? stbds_hmfree_func((p)-1,sizeof*(p)),0 : 0),(p)=NULL) - -#define stbds_hmgets(t, k) (*stbds_hmgetp(t,k)) -#define stbds_hmget(t, k) (stbds_hmgetp(t,k)->value) -#define stbds_hmget_ts(t, k, temp) (stbds_hmgetp_ts(t,k,temp)->value) -#define stbds_hmlen(t) ((t) ? (ptrdiff_t) stbds_header((t)-1)->length-1 : 0) -#define stbds_hmlenu(t) ((t) ? stbds_header((t)-1)->length-1 : 0) -#define stbds_hmgetp_null(t,k) (stbds_hmgeti(t,k) == -1 ? NULL : &(t)[stbds_temp((t)-1)]) - -#define stbds_shput(t, k, v) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ - (t)[stbds_temp((t)-1)].value = (v)) - -#define stbds_shputi(t, k, v) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ - (t)[stbds_temp((t)-1)].value = (v), stbds_temp((t)-1)) - -#define stbds_shputs(t, s) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (s).key, sizeof (s).key, STBDS_HM_STRING), \ - (t)[stbds_temp((t)-1)] = (s), \ - (t)[stbds_temp((t)-1)].key = stbds_temp_key((t)-1)) // above line overwrites whole structure, so must rewrite key here if it was allocated internally - -#define stbds_pshput(t, p) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (p)->key, sizeof (p)->key, STBDS_HM_PTR_TO_STRING), \ - (t)[stbds_temp((t)-1)] = (p)) - -#define stbds_shgeti(t,k) \ - ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ - stbds_temp((t)-1)) - -#define stbds_pshgeti(t,k) \ - ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (*(t))->key, STBDS_HM_PTR_TO_STRING), \ - stbds_temp((t)-1)) - -#define stbds_shgetp(t, k) \ - ((void) stbds_shgeti(t,k), &(t)[stbds_temp((t)-1)]) - -#define stbds_pshget(t, k) \ - ((void) stbds_pshgeti(t,k), (t)[stbds_temp((t)-1)]) - -#define stbds_shdel(t,k) \ - (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_OFFSETOF((t),key), STBDS_HM_STRING)),(t)?stbds_temp((t)-1):0) -#define stbds_pshdel(t,k) \ - (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) (k), sizeof (*(t))->key, STBDS_OFFSETOF(*(t),key), STBDS_HM_PTR_TO_STRING)),(t)?stbds_temp((t)-1):0) - -#define stbds_sh_new_arena(t) \ - ((t) = stbds_shmode_func_wrapper(t, sizeof *(t), STBDS_SH_ARENA)) -#define stbds_sh_new_strdup(t) \ - ((t) = stbds_shmode_func_wrapper(t, sizeof *(t), STBDS_SH_STRDUP)) - -#define stbds_shdefault(t, v) stbds_hmdefault(t,v) -#define stbds_shdefaults(t, s) stbds_hmdefaults(t,s) - -#define stbds_shfree stbds_hmfree -#define stbds_shlenu stbds_hmlenu - -#define stbds_shgets(t, k) (*stbds_shgetp(t,k)) -#define stbds_shget(t, k) (stbds_shgetp(t,k)->value) -#define stbds_shgetp_null(t,k) (stbds_shgeti(t,k) == -1 ? NULL : &(t)[stbds_temp((t)-1)]) -#define stbds_shlen stbds_hmlen - -typedef struct -{ - size_t length; - size_t capacity; - void * hash_table; - ptrdiff_t temp; -} stbds_array_header; - -typedef struct stbds_string_block -{ - struct stbds_string_block *next; - char storage[8]; -} stbds_string_block; - -struct stbds_string_arena -{ - stbds_string_block *storage; - size_t remaining; - unsigned char block; - unsigned char mode; // this isn't used by the string arena itself -}; - -#define STBDS_HM_BINARY 0 -#define STBDS_HM_STRING 1 - -enum -{ - STBDS_SH_NONE, - STBDS_SH_DEFAULT, - STBDS_SH_STRDUP, - STBDS_SH_ARENA -}; - -#ifdef __cplusplus -// in C we use implicit assignment from these void*-returning functions to T*. -// in C++ these templates make the same code work -template static T * stbds_arrgrowf_wrapper(T *a, size_t elemsize, size_t addlen, size_t min_cap) { - return (T*)stbds_arrgrowf((void *)a, elemsize, addlen, min_cap); -} -template static T * stbds_hmget_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) { - return (T*)stbds_hmget_key((void*)a, elemsize, key, keysize, mode); -} -template static T * stbds_hmget_key_ts_wrapper(T *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) { - return (T*)stbds_hmget_key_ts((void*)a, elemsize, key, keysize, temp, mode); -} -template static T * stbds_hmput_default_wrapper(T *a, size_t elemsize) { - return (T*)stbds_hmput_default((void *)a, elemsize); -} -template static T * stbds_hmput_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) { - return (T*)stbds_hmput_key((void*)a, elemsize, key, keysize, mode); -} -template static T * stbds_hmdel_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode){ - return (T*)stbds_hmdel_key((void*)a, elemsize, key, keysize, keyoffset, mode); -} -template static T * stbds_shmode_func_wrapper(T *, size_t elemsize, int mode) { - return (T*)stbds_shmode_func(elemsize, mode); -} -#else -#define stbds_arrgrowf_wrapper stbds_arrgrowf -#define stbds_hmget_key_wrapper stbds_hmget_key -#define stbds_hmget_key_ts_wrapper stbds_hmget_key_ts -#define stbds_hmput_default_wrapper stbds_hmput_default -#define stbds_hmput_key_wrapper stbds_hmput_key -#define stbds_hmdel_key_wrapper stbds_hmdel_key -#define stbds_shmode_func_wrapper(t,e,m) stbds_shmode_func(e,m) -#endif - -#endif // INCLUDE_STB_DS_H - - -////////////////////////////////////////////////////////////////////////////// -// -// IMPLEMENTATION -// - -#ifdef STB_DS_IMPLEMENTATION -#include -#include - -#ifndef STBDS_ASSERT -#define STBDS_ASSERT_WAS_UNDEFINED -#define STBDS_ASSERT(x) ((void) 0) -#endif - -#ifdef STBDS_STATISTICS -#define STBDS_STATS(x) x -size_t stbds_array_grow; -size_t stbds_hash_grow; -size_t stbds_hash_shrink; -size_t stbds_hash_rebuild; -size_t stbds_hash_probes; -size_t stbds_hash_alloc; -size_t stbds_rehash_probes; -size_t stbds_rehash_items; -#else -#define STBDS_STATS(x) -#endif - -// -// stbds_arr implementation -// - -//int *prev_allocs[65536]; -//int num_prev; - -void *stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap) -{ - stbds_array_header temp={0}; // force debugging - void *b; - size_t min_len = stbds_arrlen(a) + addlen; - (void) sizeof(temp); - - // compute the minimum capacity needed - if (min_len > min_cap) - min_cap = min_len; - - if (min_cap <= stbds_arrcap(a)) - return a; - - // increase needed capacity to guarantee O(1) amortized - if (min_cap < 2 * stbds_arrcap(a)) - min_cap = 2 * stbds_arrcap(a); - else if (min_cap < 4) - min_cap = 4; - - //if (num_prev < 65536) if (a) prev_allocs[num_prev++] = (int *) ((char *) a+1); - //if (num_prev == 2201) - // num_prev = num_prev; - b = STBDS_REALLOC(NULL, (a) ? stbds_header(a) : 0, elemsize * min_cap + sizeof(stbds_array_header)); - //if (num_prev < 65536) prev_allocs[num_prev++] = (int *) (char *) b; - b = (char *) b + sizeof(stbds_array_header); - if (a == NULL) { - stbds_header(b)->length = 0; - stbds_header(b)->hash_table = 0; - stbds_header(b)->temp = 0; - } else { - STBDS_STATS(++stbds_array_grow); - } - stbds_header(b)->capacity = min_cap; - - return b; -} - -void stbds_arrfreef(void *a) -{ - STBDS_FREE(NULL, stbds_header(a)); -} - -// -// stbds_hm hash table implementation -// - -#ifdef STBDS_INTERNAL_SMALL_BUCKET -#define STBDS_BUCKET_LENGTH 4 -#else -#define STBDS_BUCKET_LENGTH 8 -#endif - -#define STBDS_BUCKET_SHIFT (STBDS_BUCKET_LENGTH == 8 ? 3 : 2) -#define STBDS_BUCKET_MASK (STBDS_BUCKET_LENGTH-1) -#define STBDS_CACHE_LINE_SIZE 64 - -#define STBDS_ALIGN_FWD(n,a) (((n) + (a) - 1) & ~((a)-1)) - -typedef struct -{ - size_t hash [STBDS_BUCKET_LENGTH]; - ptrdiff_t index[STBDS_BUCKET_LENGTH]; -} stbds_hash_bucket; // in 32-bit, this is one 64-byte cache line; in 64-bit, each array is one 64-byte cache line - -typedef struct -{ - char * temp_key; // this MUST be the first field of the hash table - size_t slot_count; - size_t used_count; - size_t used_count_threshold; - size_t used_count_shrink_threshold; - size_t tombstone_count; - size_t tombstone_count_threshold; - size_t seed; - size_t slot_count_log2; - stbds_string_arena string; - stbds_hash_bucket *storage; // not a separate allocation, just 64-byte aligned storage after this struct -} stbds_hash_index; - -#define STBDS_INDEX_EMPTY -1 -#define STBDS_INDEX_DELETED -2 -#define STBDS_INDEX_IN_USE(x) ((x) >= 0) - -#define STBDS_HASH_EMPTY 0 -#define STBDS_HASH_DELETED 1 - -static size_t stbds_hash_seed=0x31415926; - -void stbds_rand_seed(size_t seed) -{ - stbds_hash_seed = seed; -} - -#define stbds_load_32_or_64(var, temp, v32, v64_hi, v64_lo) \ - temp = v64_lo ^ v32, temp <<= 16, temp <<= 16, temp >>= 16, temp >>= 16, /* discard if 32-bit */ \ - var = v64_hi, var <<= 16, var <<= 16, /* discard if 32-bit */ \ - var ^= temp ^ v32 - -#define STBDS_SIZE_T_BITS ((sizeof (size_t)) * 8) - -static size_t stbds_probe_position(size_t hash, size_t slot_count, size_t slot_log2) -{ - size_t pos; - STBDS_NOTUSED(slot_log2); - pos = hash & (slot_count-1); - #ifdef STBDS_INTERNAL_BUCKET_START - pos &= ~STBDS_BUCKET_MASK; - #endif - return pos; -} - -static size_t stbds_log2(size_t slot_count) -{ - size_t n=0; - while (slot_count > 1) { - slot_count >>= 1; - ++n; - } - return n; -} - -static stbds_hash_index *stbds_make_hash_index(size_t slot_count, stbds_hash_index *ot) -{ - stbds_hash_index *t; - t = (stbds_hash_index *) STBDS_REALLOC(NULL,0,(slot_count >> STBDS_BUCKET_SHIFT) * sizeof(stbds_hash_bucket) + sizeof(stbds_hash_index) + STBDS_CACHE_LINE_SIZE-1); - t->storage = (stbds_hash_bucket *) STBDS_ALIGN_FWD((size_t) (t+1), STBDS_CACHE_LINE_SIZE); - t->slot_count = slot_count; - t->slot_count_log2 = stbds_log2(slot_count); - t->tombstone_count = 0; - t->used_count = 0; - - #if 0 // A1 - t->used_count_threshold = slot_count*12/16; // if 12/16th of table is occupied, grow - t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild - t->used_count_shrink_threshold = slot_count* 4/16; // if table is only 4/16th full, shrink - #elif 1 // A2 - //t->used_count_threshold = slot_count*12/16; // if 12/16th of table is occupied, grow - //t->tombstone_count_threshold = slot_count* 3/16; // if tombstones are 3/16th of table, rebuild - //t->used_count_shrink_threshold = slot_count* 4/16; // if table is only 4/16th full, shrink - - // compute without overflowing - t->used_count_threshold = slot_count - (slot_count>>2); - t->tombstone_count_threshold = (slot_count>>3) + (slot_count>>4); - t->used_count_shrink_threshold = slot_count >> 2; - - #elif 0 // B1 - t->used_count_threshold = slot_count*13/16; // if 13/16th of table is occupied, grow - t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild - t->used_count_shrink_threshold = slot_count* 5/16; // if table is only 5/16th full, shrink - #else // C1 - t->used_count_threshold = slot_count*14/16; // if 14/16th of table is occupied, grow - t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild - t->used_count_shrink_threshold = slot_count* 6/16; // if table is only 6/16th full, shrink - #endif - // Following statistics were measured on a Core i7-6700 @ 4.00Ghz, compiled with clang 7.0.1 -O2 - // Note that the larger tables have high variance as they were run fewer times - // A1 A2 B1 C1 - // 0.10ms : 0.10ms : 0.10ms : 0.11ms : 2,000 inserts creating 2K table - // 0.96ms : 0.95ms : 0.97ms : 1.04ms : 20,000 inserts creating 20K table - // 14.48ms : 14.46ms : 10.63ms : 11.00ms : 200,000 inserts creating 200K table - // 195.74ms : 196.35ms : 203.69ms : 214.92ms : 2,000,000 inserts creating 2M table - // 2193.88ms : 2209.22ms : 2285.54ms : 2437.17ms : 20,000,000 inserts creating 20M table - // 65.27ms : 53.77ms : 65.33ms : 65.47ms : 500,000 inserts & deletes in 2K table - // 72.78ms : 62.45ms : 71.95ms : 72.85ms : 500,000 inserts & deletes in 20K table - // 89.47ms : 77.72ms : 96.49ms : 96.75ms : 500,000 inserts & deletes in 200K table - // 97.58ms : 98.14ms : 97.18ms : 97.53ms : 500,000 inserts & deletes in 2M table - // 118.61ms : 119.62ms : 120.16ms : 118.86ms : 500,000 inserts & deletes in 20M table - // 192.11ms : 194.39ms : 196.38ms : 195.73ms : 500,000 inserts & deletes in 200M table - - if (slot_count <= STBDS_BUCKET_LENGTH) - t->used_count_shrink_threshold = 0; - // to avoid infinite loop, we need to guarantee that at least one slot is empty and will terminate probes - STBDS_ASSERT(t->used_count_threshold + t->tombstone_count_threshold < t->slot_count); - STBDS_STATS(++stbds_hash_alloc); - if (ot) { - t->string = ot->string; - // reuse old seed so we can reuse old hashes so below "copy out old data" doesn't do any hashing - t->seed = ot->seed; - } else { - size_t a,b,temp; - memset(&t->string, 0, sizeof(t->string)); - t->seed = stbds_hash_seed; - // LCG - // in 32-bit, a = 2147001325 b = 715136305 - // in 64-bit, a = 2862933555777941757 b = 3037000493 - stbds_load_32_or_64(a,temp, 2147001325, 0x27bb2ee6, 0x87b0b0fd); - stbds_load_32_or_64(b,temp, 715136305, 0, 0xb504f32d); - stbds_hash_seed = stbds_hash_seed * a + b; - } - - { - size_t i,j; - for (i=0; i < slot_count >> STBDS_BUCKET_SHIFT; ++i) { - stbds_hash_bucket *b = &t->storage[i]; - for (j=0; j < STBDS_BUCKET_LENGTH; ++j) - b->hash[j] = STBDS_HASH_EMPTY; - for (j=0; j < STBDS_BUCKET_LENGTH; ++j) - b->index[j] = STBDS_INDEX_EMPTY; - } - } - - // copy out the old data, if any - if (ot) { - size_t i,j; - t->used_count = ot->used_count; - for (i=0; i < ot->slot_count >> STBDS_BUCKET_SHIFT; ++i) { - stbds_hash_bucket *ob = &ot->storage[i]; - for (j=0; j < STBDS_BUCKET_LENGTH; ++j) { - if (STBDS_INDEX_IN_USE(ob->index[j])) { - size_t hash = ob->hash[j]; - size_t pos = stbds_probe_position(hash, t->slot_count, t->slot_count_log2); - size_t step = STBDS_BUCKET_LENGTH; - STBDS_STATS(++stbds_rehash_items); - for (;;) { - size_t limit,z; - stbds_hash_bucket *bucket; - bucket = &t->storage[pos >> STBDS_BUCKET_SHIFT]; - STBDS_STATS(++stbds_rehash_probes); - - for (z=pos & STBDS_BUCKET_MASK; z < STBDS_BUCKET_LENGTH; ++z) { - if (bucket->hash[z] == 0) { - bucket->hash[z] = hash; - bucket->index[z] = ob->index[j]; - goto done; - } - } - - limit = pos & STBDS_BUCKET_MASK; - for (z = 0; z < limit; ++z) { - if (bucket->hash[z] == 0) { - bucket->hash[z] = hash; - bucket->index[z] = ob->index[j]; - goto done; - } - } - - pos += step; // quadratic probing - step += STBDS_BUCKET_LENGTH; - pos &= (t->slot_count-1); - } - } - done: - ; - } - } - } - - return t; -} - -#define STBDS_ROTATE_LEFT(val, n) (((val) << (n)) | ((val) >> (STBDS_SIZE_T_BITS - (n)))) -#define STBDS_ROTATE_RIGHT(val, n) (((val) >> (n)) | ((val) << (STBDS_SIZE_T_BITS - (n)))) - -size_t stbds_hash_string(char *str, size_t seed) -{ - size_t hash = seed; - while (*str) - hash = STBDS_ROTATE_LEFT(hash, 9) + (unsigned char) *str++; - - // Thomas Wang 64-to-32 bit mix function, hopefully also works in 32 bits - hash ^= seed; - hash = (~hash) + (hash << 18); - hash ^= hash ^ STBDS_ROTATE_RIGHT(hash,31); - hash = hash * 21; - hash ^= hash ^ STBDS_ROTATE_RIGHT(hash,11); - hash += (hash << 6); - hash ^= STBDS_ROTATE_RIGHT(hash,22); - return hash+seed; -} - -#ifdef STBDS_SIPHASH_2_4 -#define STBDS_SIPHASH_C_ROUNDS 2 -#define STBDS_SIPHASH_D_ROUNDS 4 -typedef int STBDS_SIPHASH_2_4_can_only_be_used_in_64_bit_builds[sizeof(size_t) == 8 ? 1 : -1]; -#endif - -#ifndef STBDS_SIPHASH_C_ROUNDS -#define STBDS_SIPHASH_C_ROUNDS 1 -#endif -#ifndef STBDS_SIPHASH_D_ROUNDS -#define STBDS_SIPHASH_D_ROUNDS 1 -#endif - -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable:4127) // conditional expression is constant, for do..while(0) and sizeof()== -#endif - -static size_t stbds_siphash_bytes(void *p, size_t len, size_t seed) -{ - unsigned char *d = (unsigned char *) p; - size_t i,j; - size_t v0,v1,v2,v3, data; - - // hash that works on 32- or 64-bit registers without knowing which we have - // (computes different results on 32-bit and 64-bit platform) - // derived from siphash, but on 32-bit platforms very different as it uses 4 32-bit state not 4 64-bit - v0 = ((((size_t) 0x736f6d65 << 16) << 16) + 0x70736575) ^ seed; - v1 = ((((size_t) 0x646f7261 << 16) << 16) + 0x6e646f6d) ^ ~seed; - v2 = ((((size_t) 0x6c796765 << 16) << 16) + 0x6e657261) ^ seed; - v3 = ((((size_t) 0x74656462 << 16) << 16) + 0x79746573) ^ ~seed; - - #ifdef STBDS_TEST_SIPHASH_2_4 - // hardcoded with key material in the siphash test vectors - v0 ^= 0x0706050403020100ull ^ seed; - v1 ^= 0x0f0e0d0c0b0a0908ull ^ ~seed; - v2 ^= 0x0706050403020100ull ^ seed; - v3 ^= 0x0f0e0d0c0b0a0908ull ^ ~seed; - #endif - - #define STBDS_SIPROUND() \ - do { \ - v0 += v1; v1 = STBDS_ROTATE_LEFT(v1, 13); v1 ^= v0; v0 = STBDS_ROTATE_LEFT(v0,STBDS_SIZE_T_BITS/2); \ - v2 += v3; v3 = STBDS_ROTATE_LEFT(v3, 16); v3 ^= v2; \ - v2 += v1; v1 = STBDS_ROTATE_LEFT(v1, 17); v1 ^= v2; v2 = STBDS_ROTATE_LEFT(v2,STBDS_SIZE_T_BITS/2); \ - v0 += v3; v3 = STBDS_ROTATE_LEFT(v3, 21); v3 ^= v0; \ - } while (0) - - for (i=0; i+sizeof(size_t) <= len; i += sizeof(size_t), d += sizeof(size_t)) { - data = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); - data |= (size_t) (d[4] | (d[5] << 8) | (d[6] << 16) | (d[7] << 24)) << 16 << 16; // discarded if size_t == 4 - - v3 ^= data; - for (j=0; j < STBDS_SIPHASH_C_ROUNDS; ++j) - STBDS_SIPROUND(); - v0 ^= data; - } - data = len << (STBDS_SIZE_T_BITS-8); - switch (len - i) { - case 7: data |= ((size_t) d[6] << 24) << 24; // fall through - case 6: data |= ((size_t) d[5] << 20) << 20; // fall through - case 5: data |= ((size_t) d[4] << 16) << 16; // fall through - case 4: data |= (d[3] << 24); // fall through - case 3: data |= (d[2] << 16); // fall through - case 2: data |= (d[1] << 8); // fall through - case 1: data |= d[0]; // fall through - case 0: break; - } - v3 ^= data; - for (j=0; j < STBDS_SIPHASH_C_ROUNDS; ++j) - STBDS_SIPROUND(); - v0 ^= data; - v2 ^= 0xff; - for (j=0; j < STBDS_SIPHASH_D_ROUNDS; ++j) - STBDS_SIPROUND(); - -#ifdef STBDS_SIPHASH_2_4 - return v0^v1^v2^v3; -#else - return v1^v2^v3; // slightly stronger since v0^v3 in above cancels out final round operation? I tweeted at the authors of SipHash about this but they didn't reply -#endif -} - -size_t stbds_hash_bytes(void *p, size_t len, size_t seed) -{ -#ifdef STBDS_SIPHASH_2_4 - return stbds_siphash_bytes(p,len,seed); -#else - unsigned char *d = (unsigned char *) p; - - if (len == 4) { - unsigned int hash = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); - #if 0 - // HASH32-A Bob Jenkin's hash function w/o large constants - hash ^= seed; - hash -= (hash<<6); - hash ^= (hash>>17); - hash -= (hash<<9); - hash ^= seed; - hash ^= (hash<<4); - hash -= (hash<<3); - hash ^= (hash<<10); - hash ^= (hash>>15); - #elif 1 - // HASH32-BB Bob Jenkin's presumably-accidental version of Thomas Wang hash with rotates turned into shifts. - // Note that converting these back to rotates makes it run a lot slower, presumably due to collisions, so I'm - // not really sure what's going on. - hash ^= seed; - hash = (hash ^ 61) ^ (hash >> 16); - hash = hash + (hash << 3); - hash = hash ^ (hash >> 4); - hash = hash * 0x27d4eb2d; - hash ^= seed; - hash = hash ^ (hash >> 15); - #else // HASH32-C - Murmur3 - hash ^= seed; - hash *= 0xcc9e2d51; - hash = (hash << 17) | (hash >> 15); - hash *= 0x1b873593; - hash ^= seed; - hash = (hash << 19) | (hash >> 13); - hash = hash*5 + 0xe6546b64; - hash ^= hash >> 16; - hash *= 0x85ebca6b; - hash ^= seed; - hash ^= hash >> 13; - hash *= 0xc2b2ae35; - hash ^= hash >> 16; - #endif - // Following statistics were measured on a Core i7-6700 @ 4.00Ghz, compiled with clang 7.0.1 -O2 - // Note that the larger tables have high variance as they were run fewer times - // HASH32-A // HASH32-BB // HASH32-C - // 0.10ms // 0.10ms // 0.10ms : 2,000 inserts creating 2K table - // 0.96ms // 0.95ms // 0.99ms : 20,000 inserts creating 20K table - // 14.69ms // 14.43ms // 14.97ms : 200,000 inserts creating 200K table - // 199.99ms // 195.36ms // 202.05ms : 2,000,000 inserts creating 2M table - // 2234.84ms // 2187.74ms // 2240.38ms : 20,000,000 inserts creating 20M table - // 55.68ms // 53.72ms // 57.31ms : 500,000 inserts & deletes in 2K table - // 63.43ms // 61.99ms // 65.73ms : 500,000 inserts & deletes in 20K table - // 80.04ms // 77.96ms // 81.83ms : 500,000 inserts & deletes in 200K table - // 100.42ms // 97.40ms // 102.39ms : 500,000 inserts & deletes in 2M table - // 119.71ms // 120.59ms // 121.63ms : 500,000 inserts & deletes in 20M table - // 185.28ms // 195.15ms // 187.74ms : 500,000 inserts & deletes in 200M table - // 15.58ms // 14.79ms // 15.52ms : 200,000 inserts creating 200K table with varying key spacing - - return (((size_t) hash << 16 << 16) | hash) ^ seed; - } else if (len == 8 && sizeof(size_t) == 8) { - size_t hash = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); - hash |= (size_t) (d[4] | (d[5] << 8) | (d[6] << 16) | (d[7] << 24)) << 16 << 16; // avoid warning if size_t == 4 - hash ^= seed; - hash = (~hash) + (hash << 21); - hash ^= STBDS_ROTATE_RIGHT(hash,24); - hash *= 265; - hash ^= STBDS_ROTATE_RIGHT(hash,14); - hash ^= seed; - hash *= 21; - hash ^= STBDS_ROTATE_RIGHT(hash,28); - hash += (hash << 31); - hash = (~hash) + (hash << 18); - return hash; - } else { - return stbds_siphash_bytes(p,len,seed); - } -#endif -} -#ifdef _MSC_VER -#pragma warning(pop) -#endif - - -static int stbds_is_key_equal(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode, size_t i) -{ - if (mode >= STBDS_HM_STRING) - return 0==strcmp((char *) key, * (char **) ((char *) a + elemsize*i + keyoffset)); - else - return 0==memcmp(key, (char *) a + elemsize*i + keyoffset, keysize); -} - -#define STBDS_HASH_TO_ARR(x,elemsize) ((char*) (x) - (elemsize)) -#define STBDS_ARR_TO_HASH(x,elemsize) ((char*) (x) + (elemsize)) - -#define stbds_hash_table(a) ((stbds_hash_index *) stbds_header(a)->hash_table) - -void stbds_hmfree_func(void *a, size_t elemsize) -{ - if (a == NULL) return; - if (stbds_hash_table(a) != NULL) { - if (stbds_hash_table(a)->string.mode == STBDS_SH_STRDUP) { - size_t i; - // skip 0th element, which is default - for (i=1; i < stbds_header(a)->length; ++i) - STBDS_FREE(NULL, *(char**) ((char *) a + elemsize*i)); - } - stbds_strreset(&stbds_hash_table(a)->string); - } - STBDS_FREE(NULL, stbds_header(a)->hash_table); - STBDS_FREE(NULL, stbds_header(a)); -} - -static ptrdiff_t stbds_hm_find_slot(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode) -{ - void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); - stbds_hash_index *table = stbds_hash_table(raw_a); - size_t hash = mode >= STBDS_HM_STRING ? stbds_hash_string((char*)key,table->seed) : stbds_hash_bytes(key, keysize,table->seed); - size_t step = STBDS_BUCKET_LENGTH; - size_t limit,i; - size_t pos; - stbds_hash_bucket *bucket; - - if (hash < 2) hash += 2; // stored hash values are forbidden from being 0, so we can detect empty slots - - pos = stbds_probe_position(hash, table->slot_count, table->slot_count_log2); - - for (;;) { - STBDS_STATS(++stbds_hash_probes); - bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; - - // start searching from pos to end of bucket, this should help performance on small hash tables that fit in cache - for (i=pos & STBDS_BUCKET_MASK; i < STBDS_BUCKET_LENGTH; ++i) { - if (bucket->hash[i] == hash) { - if (stbds_is_key_equal(a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { - return (pos & ~STBDS_BUCKET_MASK)+i; - } - } else if (bucket->hash[i] == STBDS_HASH_EMPTY) { - return -1; - } - } - - // search from beginning of bucket to pos - limit = pos & STBDS_BUCKET_MASK; - for (i = 0; i < limit; ++i) { - if (bucket->hash[i] == hash) { - if (stbds_is_key_equal(a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { - return (pos & ~STBDS_BUCKET_MASK)+i; - } - } else if (bucket->hash[i] == STBDS_HASH_EMPTY) { - return -1; - } - } - - // quadratic probing - pos += step; - step += STBDS_BUCKET_LENGTH; - pos &= (table->slot_count-1); - } - /* NOTREACHED */ -} - -void * stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) -{ - size_t keyoffset = 0; - if (a == NULL) { - // make it non-empty so we can return a temp - a = stbds_arrgrowf(0, elemsize, 0, 1); - stbds_header(a)->length += 1; - memset(a, 0, elemsize); - *temp = STBDS_INDEX_EMPTY; - // adjust a to point after the default element - return STBDS_ARR_TO_HASH(a,elemsize); - } else { - stbds_hash_index *table; - void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); - // adjust a to point to the default element - table = (stbds_hash_index *) stbds_header(raw_a)->hash_table; - if (table == 0) { - *temp = -1; - } else { - ptrdiff_t slot = stbds_hm_find_slot(a, elemsize, key, keysize, keyoffset, mode); - if (slot < 0) { - *temp = STBDS_INDEX_EMPTY; - } else { - stbds_hash_bucket *b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; - *temp = b->index[slot & STBDS_BUCKET_MASK]; - } - } - return a; - } -} - -void * stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode) -{ - ptrdiff_t temp; - void *p = stbds_hmget_key_ts(a, elemsize, key, keysize, &temp, mode); - stbds_temp(STBDS_HASH_TO_ARR(p,elemsize)) = temp; - return p; -} - -void * stbds_hmput_default(void *a, size_t elemsize) -{ - // three cases: - // a is NULL <- allocate - // a has a hash table but no entries, because of shmode <- grow - // a has entries <- do nothing - if (a == NULL || stbds_header(STBDS_HASH_TO_ARR(a,elemsize))->length == 0) { - a = stbds_arrgrowf(a ? STBDS_HASH_TO_ARR(a,elemsize) : NULL, elemsize, 0, 1); - stbds_header(a)->length += 1; - memset(a, 0, elemsize); - a=STBDS_ARR_TO_HASH(a,elemsize); - } - return a; -} - -static char *stbds_strdup(char *str); - -void *stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode) -{ - size_t keyoffset=0; - void *raw_a; - stbds_hash_index *table; - - if (a == NULL) { - a = stbds_arrgrowf(0, elemsize, 0, 1); - memset(a, 0, elemsize); - stbds_header(a)->length += 1; - // adjust a to point AFTER the default element - a = STBDS_ARR_TO_HASH(a,elemsize); - } - - // adjust a to point to the default element - raw_a = a; - a = STBDS_HASH_TO_ARR(a,elemsize); - - table = (stbds_hash_index *) stbds_header(a)->hash_table; - - if (table == NULL || table->used_count >= table->used_count_threshold) { - stbds_hash_index *nt; - size_t slot_count; - - slot_count = (table == NULL) ? STBDS_BUCKET_LENGTH : table->slot_count*2; - nt = stbds_make_hash_index(slot_count, table); - if (table) - STBDS_FREE(NULL, table); - else - nt->string.mode = mode >= STBDS_HM_STRING ? STBDS_SH_DEFAULT : 0; - stbds_header(a)->hash_table = table = nt; - STBDS_STATS(++stbds_hash_grow); - } - - // we iterate hash table explicitly because we want to track if we saw a tombstone - { - size_t hash = mode >= STBDS_HM_STRING ? stbds_hash_string((char*)key,table->seed) : stbds_hash_bytes(key, keysize,table->seed); - size_t step = STBDS_BUCKET_LENGTH; - size_t pos; - ptrdiff_t tombstone = -1; - stbds_hash_bucket *bucket; - - // stored hash values are forbidden from being 0, so we can detect empty slots to early out quickly - if (hash < 2) hash += 2; - - pos = stbds_probe_position(hash, table->slot_count, table->slot_count_log2); - - for (;;) { - size_t limit, i; - STBDS_STATS(++stbds_hash_probes); - bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; - - // start searching from pos to end of bucket - for (i=pos & STBDS_BUCKET_MASK; i < STBDS_BUCKET_LENGTH; ++i) { - if (bucket->hash[i] == hash) { - if (stbds_is_key_equal(raw_a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { - stbds_temp(a) = bucket->index[i]; - if (mode >= STBDS_HM_STRING) - stbds_temp_key(a) = * (char **) ((char *) raw_a + elemsize*bucket->index[i] + keyoffset); - return STBDS_ARR_TO_HASH(a,elemsize); - } - } else if (bucket->hash[i] == 0) { - pos = (pos & ~STBDS_BUCKET_MASK) + i; - goto found_empty_slot; - } else if (tombstone < 0) { - if (bucket->index[i] == STBDS_INDEX_DELETED) - tombstone = (ptrdiff_t) ((pos & ~STBDS_BUCKET_MASK) + i); - } - } - - // search from beginning of bucket to pos - limit = pos & STBDS_BUCKET_MASK; - for (i = 0; i < limit; ++i) { - if (bucket->hash[i] == hash) { - if (stbds_is_key_equal(raw_a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { - stbds_temp(a) = bucket->index[i]; - return STBDS_ARR_TO_HASH(a,elemsize); - } - } else if (bucket->hash[i] == 0) { - pos = (pos & ~STBDS_BUCKET_MASK) + i; - goto found_empty_slot; - } else if (tombstone < 0) { - if (bucket->index[i] == STBDS_INDEX_DELETED) - tombstone = (ptrdiff_t) ((pos & ~STBDS_BUCKET_MASK) + i); - } - } - - // quadratic probing - pos += step; - step += STBDS_BUCKET_LENGTH; - pos &= (table->slot_count-1); - } - found_empty_slot: - if (tombstone >= 0) { - pos = tombstone; - --table->tombstone_count; - } - ++table->used_count; - - { - ptrdiff_t i = (ptrdiff_t) stbds_arrlen(a); - // we want to do stbds_arraddn(1), but we can't use the macros since we don't have something of the right type - if ((size_t) i+1 > stbds_arrcap(a)) - *(void **) &a = stbds_arrgrowf(a, elemsize, 1, 0); - raw_a = STBDS_ARR_TO_HASH(a,elemsize); - - STBDS_ASSERT((size_t) i+1 <= stbds_arrcap(a)); - stbds_header(a)->length = i+1; - bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; - bucket->hash[pos & STBDS_BUCKET_MASK] = hash; - bucket->index[pos & STBDS_BUCKET_MASK] = i-1; - stbds_temp(a) = i-1; - - switch (table->string.mode) { - case STBDS_SH_STRDUP: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = stbds_strdup((char*) key); break; - case STBDS_SH_ARENA: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = stbds_stralloc(&table->string, (char*)key); break; - case STBDS_SH_DEFAULT: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = (char *) key; break; - default: memcpy((char *) a + elemsize*i, key, keysize); break; - } - } - return STBDS_ARR_TO_HASH(a,elemsize); - } -} - -void * stbds_shmode_func(size_t elemsize, int mode) -{ - void *a = stbds_arrgrowf(0, elemsize, 0, 1); - stbds_hash_index *h; - memset(a, 0, elemsize); - stbds_header(a)->length = 1; - stbds_header(a)->hash_table = h = (stbds_hash_index *) stbds_make_hash_index(STBDS_BUCKET_LENGTH, NULL); - h->string.mode = (unsigned char) mode; - return STBDS_ARR_TO_HASH(a,elemsize); -} - -void * stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode) -{ - if (a == NULL) { - return 0; - } else { - stbds_hash_index *table; - void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); - table = (stbds_hash_index *) stbds_header(raw_a)->hash_table; - stbds_temp(raw_a) = 0; - if (table == 0) { - return a; - } else { - ptrdiff_t slot; - slot = stbds_hm_find_slot(a, elemsize, key, keysize, keyoffset, mode); - if (slot < 0) - return a; - else { - stbds_hash_bucket *b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; - int i = slot & STBDS_BUCKET_MASK; - ptrdiff_t old_index = b->index[i]; - ptrdiff_t final_index = (ptrdiff_t) stbds_arrlen(raw_a)-1-1; // minus one for the raw_a vs a, and minus one for 'last' - STBDS_ASSERT(slot < (ptrdiff_t) table->slot_count); - --table->used_count; - ++table->tombstone_count; - stbds_temp(raw_a) = 1; - STBDS_ASSERT(table->used_count >= 0); - //STBDS_ASSERT(table->tombstone_count < table->slot_count/4); - b->hash[i] = STBDS_HASH_DELETED; - b->index[i] = STBDS_INDEX_DELETED; - - if (mode == STBDS_HM_STRING && table->string.mode == STBDS_SH_STRDUP) - STBDS_FREE(NULL, *(char**) ((char *) a+elemsize*old_index)); - - // if indices are the same, memcpy is a no-op, but back-pointer-fixup will fail, so skip - if (old_index != final_index) { - // swap delete - memmove((char*) a + elemsize*old_index, (char*) a + elemsize*final_index, elemsize); - - // now find the slot for the last element - if (mode == STBDS_HM_STRING) - slot = stbds_hm_find_slot(a, elemsize, *(char**) ((char *) a+elemsize*old_index + keyoffset), keysize, keyoffset, mode); - else - slot = stbds_hm_find_slot(a, elemsize, (char* ) a+elemsize*old_index + keyoffset, keysize, keyoffset, mode); - STBDS_ASSERT(slot >= 0); - b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; - i = slot & STBDS_BUCKET_MASK; - STBDS_ASSERT(b->index[i] == final_index); - b->index[i] = old_index; - } - stbds_header(raw_a)->length -= 1; - - if (table->used_count < table->used_count_shrink_threshold && table->slot_count > STBDS_BUCKET_LENGTH) { - stbds_header(raw_a)->hash_table = stbds_make_hash_index(table->slot_count>>1, table); - STBDS_FREE(NULL, table); - STBDS_STATS(++stbds_hash_shrink); - } else if (table->tombstone_count > table->tombstone_count_threshold) { - stbds_header(raw_a)->hash_table = stbds_make_hash_index(table->slot_count , table); - STBDS_FREE(NULL, table); - STBDS_STATS(++stbds_hash_rebuild); - } - - return a; - } - } - } - /* NOTREACHED */ -} - -static char *stbds_strdup(char *str) -{ - // to keep replaceable allocator simple, we don't want to use strdup. - // rolling our own also avoids problem of strdup vs _strdup - size_t len = strlen(str)+1; - char *p = (char*) STBDS_REALLOC(NULL, 0, len); - memmove(p, str, len); - return p; -} - -#ifndef STBDS_STRING_ARENA_BLOCKSIZE_MIN -#define STBDS_STRING_ARENA_BLOCKSIZE_MIN 512u -#endif -#ifndef STBDS_STRING_ARENA_BLOCKSIZE_MAX -#define STBDS_STRING_ARENA_BLOCKSIZE_MAX (1u<<20) -#endif - -char *stbds_stralloc(stbds_string_arena *a, char *str) -{ - char *p; - size_t len = strlen(str)+1; - if (len > a->remaining) { - // compute the next blocksize - size_t blocksize = a->block; - - // size is 512, 512, 1024, 1024, 2048, 2048, 4096, 4096, etc., so that - // there are log(SIZE) allocations to free when we destroy the table - blocksize = (size_t) (STBDS_STRING_ARENA_BLOCKSIZE_MIN) << (blocksize>>1); - - // if size is under 1M, advance to next blocktype - if (blocksize < (size_t)(STBDS_STRING_ARENA_BLOCKSIZE_MAX)) - ++a->block; - - if (len > blocksize) { - // if string is larger than blocksize, then just allocate the full size. - // note that we still advance string_block so block size will continue - // increasing, so e.g. if somebody only calls this with 1000-long strings, - // eventually the arena will start doubling and handling those as well - stbds_string_block *sb = (stbds_string_block *) STBDS_REALLOC(NULL, 0, sizeof(*sb)-8 + len); - memmove(sb->storage, str, len); - if (a->storage) { - // insert it after the first element, so that we don't waste the space there - sb->next = a->storage->next; - a->storage->next = sb; - } else { - sb->next = 0; - a->storage = sb; - a->remaining = 0; // this is redundant, but good for clarity - } - return sb->storage; - } else { - stbds_string_block *sb = (stbds_string_block *) STBDS_REALLOC(NULL, 0, sizeof(*sb)-8 + blocksize); - sb->next = a->storage; - a->storage = sb; - a->remaining = blocksize; - } - } - - STBDS_ASSERT(len <= a->remaining); - p = a->storage->storage + a->remaining - len; - a->remaining -= len; - memmove(p, str, len); - return p; -} - -void stbds_strreset(stbds_string_arena *a) -{ - stbds_string_block *x,*y; - x = a->storage; - while (x) { - y = x->next; - STBDS_FREE(NULL, x); - x = y; - } - memset(a, 0, sizeof(*a)); -} - -#endif - -////////////////////////////////////////////////////////////////////////////// -// -// UNIT TESTS -// - -#ifdef STBDS_UNIT_TESTS -#include -#ifdef STBDS_ASSERT_WAS_UNDEFINED -#undef STBDS_ASSERT -#endif -#ifndef STBDS_ASSERT -#define STBDS_ASSERT assert -#include -#endif - -typedef struct { int key,b,c,d; } stbds_struct; -typedef struct { int key[2],b,c,d; } stbds_struct2; - -static char buffer[256]; -char *strkey(int n) -{ -#if defined(_WIN32) && defined(__STDC_WANT_SECURE_LIB__) - sprintf_s(buffer, sizeof(buffer), "test_%d", n); -#else - sprintf(buffer, "test_%d", n); -#endif - return buffer; -} - -void stbds_unit_tests(void) -{ -#if defined(_MSC_VER) && _MSC_VER <= 1200 && defined(__cplusplus) - // VC6 C++ doesn't like the template<> trick on unnamed structures, so do nothing! - STBDS_ASSERT(0); -#else - const int testsize = 100000; - const int testsize2 = testsize/20; - int *arr=NULL; - struct { int key; int value; } *intmap = NULL; - struct { char *key; int value; } *strmap = NULL, s; - struct { stbds_struct key; int value; } *map = NULL; - stbds_struct *map2 = NULL; - stbds_struct2 *map3 = NULL; - stbds_string_arena sa = { 0 }; - int key3[2] = { 1,2 }; - ptrdiff_t temp; - - int i,j; - - STBDS_ASSERT(arrlen(arr)==0); - for (i=0; i < 20000; i += 50) { - for (j=0; j < i; ++j) - arrpush(arr,j); - arrfree(arr); - } - - for (i=0; i < 4; ++i) { - arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); - arrdel(arr,i); - arrfree(arr); - arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); - arrdelswap(arr,i); - arrfree(arr); - } - - for (i=0; i < 5; ++i) { - arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); - stbds_arrins(arr,i,5); - STBDS_ASSERT(arr[i] == 5); - if (i < 4) - STBDS_ASSERT(arr[4] == 4); - arrfree(arr); - } - - i = 1; - STBDS_ASSERT(hmgeti(intmap,i) == -1); - hmdefault(intmap, -2); - STBDS_ASSERT(hmgeti(intmap, i) == -1); - STBDS_ASSERT(hmget (intmap, i) == -2); - for (i=0; i < testsize; i+=2) - hmput(intmap, i, i*5); - for (i=0; i < testsize; i+=1) { - if (i & 1) STBDS_ASSERT(hmget(intmap, i) == -2 ); - else STBDS_ASSERT(hmget(intmap, i) == i*5); - if (i & 1) STBDS_ASSERT(hmget_ts(intmap, i, temp) == -2 ); - else STBDS_ASSERT(hmget_ts(intmap, i, temp) == i*5); - } - for (i=0; i < testsize; i+=2) - hmput(intmap, i, i*3); - for (i=0; i < testsize; i+=1) - if (i & 1) STBDS_ASSERT(hmget(intmap, i) == -2 ); - else STBDS_ASSERT(hmget(intmap, i) == i*3); - for (i=2; i < testsize; i+=4) - hmdel(intmap, i); // delete half the entries - for (i=0; i < testsize; i+=1) - if (i & 3) STBDS_ASSERT(hmget(intmap, i) == -2 ); - else STBDS_ASSERT(hmget(intmap, i) == i*3); - for (i=0; i < testsize; i+=1) - hmdel(intmap, i); // delete the rest of the entries - for (i=0; i < testsize; i+=1) - STBDS_ASSERT(hmget(intmap, i) == -2 ); - hmfree(intmap); - for (i=0; i < testsize; i+=2) - hmput(intmap, i, i*3); - hmfree(intmap); - - #if defined(__clang__) || defined(__GNUC__) - #ifndef __cplusplus - intmap = NULL; - hmput(intmap, 15, 7); - hmput(intmap, 11, 3); - hmput(intmap, 9, 5); - STBDS_ASSERT(hmget(intmap, 9) == 5); - STBDS_ASSERT(hmget(intmap, 11) == 3); - STBDS_ASSERT(hmget(intmap, 15) == 7); - #endif - #endif - - for (i=0; i < testsize; ++i) - stralloc(&sa, strkey(i)); - strreset(&sa); - - { - s.key = "a", s.value = 1; - shputs(strmap, s); - STBDS_ASSERT(*strmap[0].key == 'a'); - STBDS_ASSERT(strmap[0].key == s.key); - STBDS_ASSERT(strmap[0].value == s.value); - shfree(strmap); - } - - { - s.key = "a", s.value = 1; - sh_new_strdup(strmap); - shputs(strmap, s); - STBDS_ASSERT(*strmap[0].key == 'a'); - STBDS_ASSERT(strmap[0].key != s.key); - STBDS_ASSERT(strmap[0].value == s.value); - shfree(strmap); - } - - { - s.key = "a", s.value = 1; - sh_new_arena(strmap); - shputs(strmap, s); - STBDS_ASSERT(*strmap[0].key == 'a'); - STBDS_ASSERT(strmap[0].key != s.key); - STBDS_ASSERT(strmap[0].value == s.value); - shfree(strmap); - } - - for (j=0; j < 2; ++j) { - STBDS_ASSERT(shgeti(strmap,"foo") == -1); - if (j == 0) - sh_new_strdup(strmap); - else - sh_new_arena(strmap); - STBDS_ASSERT(shgeti(strmap,"foo") == -1); - shdefault(strmap, -2); - STBDS_ASSERT(shgeti(strmap,"foo") == -1); - for (i=0; i < testsize; i+=2) - shput(strmap, strkey(i), i*3); - for (i=0; i < testsize; i+=1) - if (i & 1) STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); - else STBDS_ASSERT(shget(strmap, strkey(i)) == i*3); - for (i=2; i < testsize; i+=4) - shdel(strmap, strkey(i)); // delete half the entries - for (i=0; i < testsize; i+=1) - if (i & 3) STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); - else STBDS_ASSERT(shget(strmap, strkey(i)) == i*3); - for (i=0; i < testsize; i+=1) - shdel(strmap, strkey(i)); // delete the rest of the entries - for (i=0; i < testsize; i+=1) - STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); - shfree(strmap); - } - - { - struct { char *key; char value; } *hash = NULL; - char name[4] = "jen"; - shput(hash, "bob" , 'h'); - shput(hash, "sally" , 'e'); - shput(hash, "fred" , 'l'); - shput(hash, "jen" , 'x'); - shput(hash, "doug" , 'o'); - - shput(hash, name , 'l'); - shfree(hash); - } - - for (i=0; i < testsize; i += 2) { - stbds_struct s = { i,i*2,i*3,i*4 }; - hmput(map, s, i*5); - } - - for (i=0; i < testsize; i += 1) { - stbds_struct s = { i,i*2,i*3 ,i*4 }; - stbds_struct t = { i,i*2,i*3+1,i*4 }; - if (i & 1) STBDS_ASSERT(hmget(map, s) == 0); - else STBDS_ASSERT(hmget(map, s) == i*5); - if (i & 1) STBDS_ASSERT(hmget_ts(map, s, temp) == 0); - else STBDS_ASSERT(hmget_ts(map, s, temp) == i*5); - //STBDS_ASSERT(hmget(map, t.key) == 0); - } - - for (i=0; i < testsize; i += 2) { - stbds_struct s = { i,i*2,i*3,i*4 }; - hmputs(map2, s); - } - hmfree(map); - - for (i=0; i < testsize; i += 1) { - stbds_struct s = { i,i*2,i*3,i*4 }; - stbds_struct t = { i,i*2,i*3+1,i*4 }; - if (i & 1) STBDS_ASSERT(hmgets(map2, s.key).d == 0); - else STBDS_ASSERT(hmgets(map2, s.key).d == i*4); - //STBDS_ASSERT(hmgetp(map2, t.key) == 0); - } - hmfree(map2); - - for (i=0; i < testsize; i += 2) { - stbds_struct2 s = { { i,i*2 }, i*3,i*4, i*5 }; - hmputs(map3, s); - } - for (i=0; i < testsize; i += 1) { - stbds_struct2 s = { { i,i*2}, i*3, i*4, i*5 }; - stbds_struct2 t = { { i,i*2}, i*3+1, i*4, i*5 }; - if (i & 1) STBDS_ASSERT(hmgets(map3, s.key).d == 0); - else STBDS_ASSERT(hmgets(map3, s.key).d == i*5); - //STBDS_ASSERT(hmgetp(map3, t.key) == 0); - } -#endif -} -#endif - - -/* ------------------------------------------------------------------------------- -This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------- -ALTERNATIVE A - MIT License -Copyright (c) 2019 Sean Barrett -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ------------------------------------------------------------------------------- -ALTERNATIVE B - Public Domain (www.unlicense.org) -This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this -software, either in source code form or as a compiled binary, for any purpose, -commercial or non-commercial, and by any means. -In jurisdictions that recognize copyright laws, the author or authors of this -software dedicate any and all copyright interest in the software to the public -domain. We make this dedication for the benefit of the public at large and to -the detriment of our heirs and successors. We intend this dedication to be an -overt act of relinquishment in perpetuity of all present and future rights to -this software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------- -*/ +/* stb_ds.h - v0.67 - public domain data structures - Sean Barrett 2019 + + This is a single-header-file library that provides easy-to-use + dynamic arrays and hash tables for C (also works in C++). + + For a gentle introduction: + http://nothings.org/stb_ds + + To use this library, do this in *one* C or C++ file: + #define STB_DS_IMPLEMENTATION + #include "stb_ds.h" + +TABLE OF CONTENTS + + Table of Contents + Compile-time options + License + Documentation + Notes + Notes - Dynamic arrays + Notes - Hash maps + Credits + +COMPILE-TIME OPTIONS + + #define STBDS_NO_SHORT_NAMES + + This flag needs to be set globally. + + By default stb_ds exposes shorter function names that are not qualified + with the "stbds_" prefix. If these names conflict with the names in your + code, define this flag. + + #define STBDS_SIPHASH_2_4 + + This flag only needs to be set in the file containing #define STB_DS_IMPLEMENTATION. + + By default stb_ds.h hashes using a weaker variant of SipHash and a custom hash for + 4- and 8-byte keys. On 64-bit platforms, you can define the above flag to force + stb_ds.h to use specification-compliant SipHash-2-4 for all keys. Doing so makes + hash table insertion about 20% slower on 4- and 8-byte keys, 5% slower on + 64-byte keys, and 10% slower on 256-byte keys on my test computer. + + #define STBDS_REALLOC(context,ptr,size) better_realloc + #define STBDS_FREE(context,ptr) better_free + + These defines only need to be set in the file containing #define STB_DS_IMPLEMENTATION. + + By default stb_ds uses stdlib realloc() and free() for memory management. You can + substitute your own functions instead by defining these symbols. You must either + define both, or neither. Note that at the moment, 'context' will always be NULL. + @TODO add an array/hash initialization function that takes a memory context pointer. + + #define STBDS_UNIT_TESTS + + Defines a function stbds_unit_tests() that checks the functioning of the data structures. + + Note that on older versions of gcc (e.g. 5.x.x) you may need to build with '-std=c++0x' + (or equivalentally '-std=c++11') when using anonymous structures as seen on the web + page or in STBDS_UNIT_TESTS. + +LICENSE + + Placed in the public domain and also MIT licensed. + See end of file for detailed license information. + +DOCUMENTATION + + Dynamic Arrays + + Non-function interface: + + Declare an empty dynamic array of type T + T* foo = NULL; + + Access the i'th item of a dynamic array 'foo' of type T, T* foo: + foo[i] + + Functions (actually macros) + + arrfree: + void arrfree(T*); + Frees the array. + + arrlen: + ptrdiff_t arrlen(T*); + Returns the number of elements in the array. + + arrlenu: + size_t arrlenu(T*); + Returns the number of elements in the array as an unsigned type. + + arrpop: + T arrpop(T* a) + Removes the final element of the array and returns it. + + arrput: + T arrput(T* a, T b); + Appends the item b to the end of array a. Returns b. + + arrins: + T arrins(T* a, int p, T b); + Inserts the item b into the middle of array a, into a[p], + moving the rest of the array over. Returns b. + + arrinsn: + void arrinsn(T* a, int p, int n); + Inserts n uninitialized items into array a starting at a[p], + moving the rest of the array over. + + arraddnptr: + T* arraddnptr(T* a, int n) + Appends n uninitialized items onto array at the end. + Returns a pointer to the first uninitialized item added. + + arraddnindex: + size_t arraddnindex(T* a, int n) + Appends n uninitialized items onto array at the end. + Returns the index of the first uninitialized item added. + + arrdel: + void arrdel(T* a, int p); + Deletes the element at a[p], moving the rest of the array over. + + arrdeln: + void arrdeln(T* a, int p, int n); + Deletes n elements starting at a[p], moving the rest of the array over. + + arrdelswap: + void arrdelswap(T* a, int p); + Deletes the element at a[p], replacing it with the element from + the end of the array. O(1) performance. + + arrsetlen: + void arrsetlen(T* a, int n); + Changes the length of the array to n. Allocates uninitialized + slots at the end if necessary. + + arrsetcap: + size_t arrsetcap(T* a, int n); + Sets the length of allocated storage to at least n. It will not + change the length of the array. + + arrcap: + size_t arrcap(T* a); + Returns the number of total elements the array can contain without + needing to be reallocated. + + Hash maps & String hash maps + + Given T is a structure type: struct { TK key; TV value; }. Note that some + functions do not require TV value and can have other fields. For string + hash maps, TK must be 'char *'. + + Special interface: + + stbds_rand_seed: + void stbds_rand_seed(size_t seed); + For security against adversarially chosen data, you should seed the + library with a strong random number. Or at least seed it with time(). + + stbds_hash_string: + size_t stbds_hash_string(char *str, size_t seed); + Returns a hash value for a string. + + stbds_hash_bytes: + size_t stbds_hash_bytes(void *p, size_t len, size_t seed); + These functions hash an arbitrary number of bytes. The function + uses a custom hash for 4- and 8-byte data, and a weakened version + of SipHash for everything else. On 64-bit platforms you can get + specification-compliant SipHash-2-4 on all data by defining + STBDS_SIPHASH_2_4, at a significant cost in speed. + + Non-function interface: + + Declare an empty hash map of type T + T* foo = NULL; + + Access the i'th entry in a hash table T* foo: + foo[i] + + Function interface (actually macros): + + hmfree + shfree + void hmfree(T*); + void shfree(T*); + Frees the hashmap and sets the pointer to NULL. + + hmlen + shlen + ptrdiff_t hmlen(T*) + ptrdiff_t shlen(T*) + Returns the number of elements in the hashmap. + + hmlenu + shlenu + size_t hmlenu(T*) + size_t shlenu(T*) + Returns the number of elements in the hashmap. + + hmgeti + shgeti + hmgeti_ts + ptrdiff_t hmgeti(T*, TK key) + ptrdiff_t shgeti(T*, char* key) + ptrdiff_t hmgeti_ts(T*, TK key, ptrdiff_t tempvar) + Returns the index in the hashmap which has the key 'key', or -1 + if the key is not present. + + hmget + hmget_ts + shget + TV hmget(T*, TK key) + TV shget(T*, char* key) + TV hmget_ts(T*, TK key, ptrdiff_t tempvar) + Returns the value corresponding to 'key' in the hashmap. + The structure must have a 'value' field + + hmgets + shgets + T hmgets(T*, TK key) + T shgets(T*, char* key) + Returns the structure corresponding to 'key' in the hashmap. + + hmgetp + shgetp + hmgetp_ts + hmgetp_null + shgetp_null + T* hmgetp(T*, TK key) + T* shgetp(T*, char* key) + T* hmgetp_ts(T*, TK key, ptrdiff_t tempvar) + T* hmgetp_null(T*, TK key) + T* shgetp_null(T*, char *key) + Returns a pointer to the structure corresponding to 'key' in + the hashmap. Functions ending in "_null" return NULL if the key + is not present in the hashmap; the others return a pointer to a + structure holding the default value (but not the searched-for key). + + hmdefault + shdefault + TV hmdefault(T*, TV value) + TV shdefault(T*, TV value) + Sets the default value for the hashmap, the value which will be + returned by hmget/shget if the key is not present. + + hmdefaults + shdefaults + TV hmdefaults(T*, T item) + TV shdefaults(T*, T item) + Sets the default struct for the hashmap, the contents which will be + returned by hmgets/shgets if the key is not present. + + hmput + shput + TV hmput(T*, TK key, TV value) + TV shput(T*, char* key, TV value) + Inserts a pair into the hashmap. If the key is already + present in the hashmap, updates its value. + + hmputs + shputs + T hmputs(T*, T item) + T shputs(T*, T item) + Inserts a struct with T.key into the hashmap. If the struct is already + present in the hashmap, updates it. + + hmdel + shdel + int hmdel(T*, TK key) + int shdel(T*, char* key) + If 'key' is in the hashmap, deletes its entry and returns 1. + Otherwise returns 0. + + Function interface (actually macros) for strings only: + + sh_new_strdup + void sh_new_strdup(T*); + Overwrites the existing pointer with a newly allocated + string hashmap which will automatically allocate and free + each string key using realloc/free + + sh_new_arena + void sh_new_arena(T*); + Overwrites the existing pointer with a newly allocated + string hashmap which will automatically allocate each string + key to a string arena. Every string key ever used by this + hash table remains in the arena until the arena is freed. + Additionally, any key which is deleted and reinserted will + be allocated multiple times in the string arena. + +NOTES + + * These data structures are realloc'd when they grow, and the macro + "functions" write to the provided pointer. This means: (a) the pointer + must be an lvalue, and (b) the pointer to the data structure is not + stable, and you must maintain it the same as you would a realloc'd + pointer. For example, if you pass a pointer to a dynamic array to a + function which updates it, the function must return back the new + pointer to the caller. This is the price of trying to do this in C. + + * The following are the only functions that are thread-safe on a single data + structure, i.e. can be run in multiple threads simultaneously on the same + data structure + hmlen shlen + hmlenu shlenu + hmget_ts shget_ts + hmgeti_ts shgeti_ts + hmgets_ts shgets_ts + + * You iterate over the contents of a dynamic array and a hashmap in exactly + the same way, using arrlen/hmlen/shlen: + + for (i=0; i < arrlen(foo); ++i) + ... foo[i] ... + + * All operations except arrins/arrdel are O(1) amortized, but individual + operations can be slow, so these data structures may not be suitable + for real time use. Dynamic arrays double in capacity as needed, so + elements are copied an average of once. Hash tables double/halve + their size as needed, with appropriate hysteresis to maintain O(1) + performance. + +NOTES - DYNAMIC ARRAY + + * If you know how long a dynamic array is going to be in advance, you can avoid + extra memory allocations by using arrsetlen to allocate it to that length in + advance and use foo[n] while filling it out, or arrsetcap to allocate the memory + for that length and use arrput/arrpush as normal. + + * Unlike some other versions of the dynamic array, this version should + be safe to use with strict-aliasing optimizations. + +NOTES - HASH MAP + + * For compilers other than GCC and clang (e.g. Visual Studio), for hmput/hmget/hmdel + and variants, the key must be an lvalue (so the macro can take the address of it). + Extensions are used that eliminate this requirement if you're using C99 and later + in GCC or clang, or if you're using C++ in GCC. But note that this can make your + code less portable. + + * To test for presence of a key in a hashmap, just do 'hmgeti(foo,key) >= 0'. + + * The iteration order of your data in the hashmap is determined solely by the + order of insertions and deletions. In particular, if you never delete, new + keys are always added at the end of the array. This will be consistent + across all platforms and versions of the library. However, you should not + attempt to serialize the internal hash table, as the hash is not consistent + between different platforms, and may change with future versions of the library. + + * Use sh_new_arena() for string hashmaps that you never delete from. Initialize + with NULL if you're managing the memory for your strings, or your strings are + never freed (at least until the hashmap is freed). Otherwise, use sh_new_strdup(). + @TODO: make an arena variant that garbage collects the strings with a trivial + copy collector into a new arena whenever the table shrinks / rebuilds. Since + current arena recommendation is to only use arena if it never deletes, then + this can just replace current arena implementation. + + * If adversarial input is a serious concern and you're on a 64-bit platform, + enable STBDS_SIPHASH_2_4 (see the 'Compile-time options' section), and pass + a strong random number to stbds_rand_seed. + + * The default value for the hash table is stored in foo[-1], so if you + use code like 'hmget(T,k)->value = 5' you can accidentally overwrite + the value stored by hmdefault if 'k' is not present. + +CREDITS + + Sean Barrett -- library, idea for dynamic array API/implementation + Per Vognsen -- idea for hash table API/implementation + Rafael Sachetto -- arrpop() + github:HeroicKatora -- arraddn() reworking + + Bugfixes: + Andy Durdin + Shane Liesegang + Vinh Truong + Andreas Molzer + github:hashitaku + github:srdjanstipic + Macoy Madson + Andreas Vennstrom + Tobias Mansfield-Williams +*/ + +#ifdef STBDS_UNIT_TESTS +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef INCLUDE_STB_DS_H +#define INCLUDE_STB_DS_H + +#include +#include + +#ifndef STBDS_NO_SHORT_NAMES +#define arrlen stbds_arrlen +#define arrlenu stbds_arrlenu +#define arrput stbds_arrput +#define arrpush stbds_arrput +#define arrpop stbds_arrpop +#define arrfree stbds_arrfree +#define arraddn stbds_arraddn // deprecated, use one of the following instead: +#define arraddnptr stbds_arraddnptr +#define arraddnindex stbds_arraddnindex +#define arrsetlen stbds_arrsetlen +#define arrlast stbds_arrlast +#define arrins stbds_arrins +#define arrinsn stbds_arrinsn +#define arrdel stbds_arrdel +#define arrdeln stbds_arrdeln +#define arrdelswap stbds_arrdelswap +#define arrcap stbds_arrcap +#define arrsetcap stbds_arrsetcap + +#define hmput stbds_hmput +#define hmputs stbds_hmputs +#define hmget stbds_hmget +#define hmget_ts stbds_hmget_ts +#define hmgets stbds_hmgets +#define hmgetp stbds_hmgetp +#define hmgetp_ts stbds_hmgetp_ts +#define hmgetp_null stbds_hmgetp_null +#define hmgeti stbds_hmgeti +#define hmgeti_ts stbds_hmgeti_ts +#define hmdel stbds_hmdel +#define hmlen stbds_hmlen +#define hmlenu stbds_hmlenu +#define hmfree stbds_hmfree +#define hmdefault stbds_hmdefault +#define hmdefaults stbds_hmdefaults + +#define shput stbds_shput +#define shputi stbds_shputi +#define shputs stbds_shputs +#define shget stbds_shget +#define shgeti stbds_shgeti +#define shgets stbds_shgets +#define shgetp stbds_shgetp +#define shgetp_null stbds_shgetp_null +#define shdel stbds_shdel +#define shlen stbds_shlen +#define shlenu stbds_shlenu +#define shfree stbds_shfree +#define shdefault stbds_shdefault +#define shdefaults stbds_shdefaults +#define sh_new_arena stbds_sh_new_arena +#define sh_new_strdup stbds_sh_new_strdup + +#define stralloc stbds_stralloc +#define strreset stbds_strreset +#endif + +#if defined(STBDS_REALLOC) && !defined(STBDS_FREE) || !defined(STBDS_REALLOC) && defined(STBDS_FREE) +#error "You must define both STBDS_REALLOC and STBDS_FREE, or neither." +#endif +#if !defined(STBDS_REALLOC) && !defined(STBDS_FREE) +#include +#define STBDS_REALLOC(c,p,s) realloc(p,s) +#define STBDS_FREE(c,p) free(p) +#endif + +#ifdef _MSC_VER +#define STBDS_NOTUSED(v) (void)(v) +#else +#define STBDS_NOTUSED(v) (void)sizeof(v) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// for security against attackers, seed the library with a random number, at least time() but stronger is better +extern void stbds_rand_seed(size_t seed); + +// these are the hash functions used internally if you want to test them or use them for other purposes +extern size_t stbds_hash_bytes(void *p, size_t len, size_t seed); +extern size_t stbds_hash_string(char *str, size_t seed); + +// this is a simple string arena allocator, initialize with e.g. 'stbds_string_arena my_arena={0}'. +typedef struct stbds_string_arena stbds_string_arena; +extern char * stbds_stralloc(stbds_string_arena *a, char *str); +extern void stbds_strreset(stbds_string_arena *a); + +// have to #define STBDS_UNIT_TESTS to call this +extern void stbds_unit_tests(void); + +/////////////// +// +// Everything below here is implementation details +// + +extern void * stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap); +extern void stbds_arrfreef(void *a); +extern void stbds_hmfree_func(void *p, size_t elemsize); +extern void * stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); +extern void * stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode); +extern void * stbds_hmput_default(void *a, size_t elemsize); +extern void * stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); +extern void * stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode); +extern void * stbds_shmode_func(size_t elemsize, int mode); + +#ifdef __cplusplus +} +#endif + +#if defined(__GNUC__) || defined(__clang__) +#define STBDS_HAS_TYPEOF +#ifdef __cplusplus +//#define STBDS_HAS_LITERAL_ARRAY // this is currently broken for clang +#endif +#endif + +#if !defined(__cplusplus) +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +#define STBDS_HAS_LITERAL_ARRAY +#endif +#endif + +// this macro takes the address of the argument, but on gcc/clang can accept rvalues +#if defined(STBDS_HAS_LITERAL_ARRAY) && defined(STBDS_HAS_TYPEOF) + #if __clang__ + #define STBDS_ADDRESSOF(typevar, value) ((__typeof__(typevar)[1]){value}) // literal array decays to pointer to value + #else + #define STBDS_ADDRESSOF(typevar, value) ((typeof(typevar)[1]){value}) // literal array decays to pointer to value + #endif +#else +#define STBDS_ADDRESSOF(typevar, value) &(value) +#endif + +#define STBDS_OFFSETOF(var,field) ((char *) &(var)->field - (char *) (var)) + +#define stbds_header(t) ((stbds_array_header *) (t) - 1) +#define stbds_temp(t) stbds_header(t)->temp +#define stbds_temp_key(t) (*(char **) stbds_header(t)->hash_table) + +#define stbds_arrsetcap(a,n) (stbds_arrgrow(a,0,n)) +#define stbds_arrsetlen(a,n) ((stbds_arrcap(a) < (size_t) (n) ? stbds_arrsetcap((a),(size_t)(n)),0 : 0), (a) ? stbds_header(a)->length = (size_t) (n) : 0) +#define stbds_arrcap(a) ((a) ? stbds_header(a)->capacity : 0) +#define stbds_arrlen(a) ((a) ? (ptrdiff_t) stbds_header(a)->length : 0) +#define stbds_arrlenu(a) ((a) ? stbds_header(a)->length : 0) +#define stbds_arrput(a,v) (stbds_arrmaybegrow(a,1), (a)[stbds_header(a)->length++] = (v)) +#define stbds_arrpush stbds_arrput // synonym +#define stbds_arrpop(a) (stbds_header(a)->length--, (a)[stbds_header(a)->length]) +#define stbds_arraddn(a,n) ((void)(stbds_arraddnindex(a, n))) // deprecated, use one of the following instead: +#define stbds_arraddnptr(a,n) (stbds_arrmaybegrow(a,n), (n) ? (stbds_header(a)->length += (n), &(a)[stbds_header(a)->length-(n)]) : (a)) +#define stbds_arraddnindex(a,n)(stbds_arrmaybegrow(a,n), (n) ? (stbds_header(a)->length += (n), stbds_header(a)->length-(n)) : stbds_arrlen(a)) +#define stbds_arraddnoff stbds_arraddnindex +#define stbds_arrlast(a) ((a)[stbds_header(a)->length-1]) +#define stbds_arrfree(a) ((void) ((a) ? STBDS_FREE(NULL,stbds_header(a)) : (void)0), (a)=NULL) +#define stbds_arrdel(a,i) stbds_arrdeln(a,i,1) +#define stbds_arrdeln(a,i,n) (memmove(&(a)[i], &(a)[(i)+(n)], sizeof *(a) * (stbds_header(a)->length-(n)-(i))), stbds_header(a)->length -= (n)) +#define stbds_arrdelswap(a,i) ((a)[i] = stbds_arrlast(a), stbds_header(a)->length -= 1) +#define stbds_arrinsn(a,i,n) (stbds_arraddn((a),(n)), memmove(&(a)[(i)+(n)], &(a)[i], sizeof *(a) * (stbds_header(a)->length-(n)-(i)))) +#define stbds_arrins(a,i,v) (stbds_arrinsn((a),(i),1), (a)[i]=(v)) + +#define stbds_arrmaybegrow(a,n) ((!(a) || stbds_header(a)->length + (n) > stbds_header(a)->capacity) \ + ? (stbds_arrgrow(a,n,0),0) : 0) + +#define stbds_arrgrow(a,b,c) ((a) = stbds_arrgrowf_wrapper((a), sizeof *(a), (b), (c))) + +#define stbds_hmput(t, k, v) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, 0), \ + (t)[stbds_temp((t)-1)].key = (k), \ + (t)[stbds_temp((t)-1)].value = (v)) + +#define stbds_hmputs(t, s) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), &(s).key, sizeof (s).key, STBDS_HM_BINARY), \ + (t)[stbds_temp((t)-1)] = (s)) + +#define stbds_hmgeti(t,k) \ + ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, STBDS_HM_BINARY), \ + stbds_temp((t)-1)) + +#define stbds_hmgeti_ts(t,k,temp) \ + ((t) = stbds_hmget_key_ts_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, &(temp), STBDS_HM_BINARY), \ + (temp)) + +#define stbds_hmgetp(t, k) \ + ((void) stbds_hmgeti(t,k), &(t)[stbds_temp((t)-1)]) + +#define stbds_hmgetp_ts(t, k, temp) \ + ((void) stbds_hmgeti_ts(t,k,temp), &(t)[temp]) + +#define stbds_hmdel(t,k) \ + (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, STBDS_OFFSETOF((t),key), STBDS_HM_BINARY)),(t)?stbds_temp((t)-1):0) + +#define stbds_hmdefault(t, v) \ + ((t) = stbds_hmput_default_wrapper((t), sizeof *(t)), (t)[-1].value = (v)) + +#define stbds_hmdefaults(t, s) \ + ((t) = stbds_hmput_default_wrapper((t), sizeof *(t)), (t)[-1] = (s)) + +#define stbds_hmfree(p) \ + ((void) ((p) != NULL ? stbds_hmfree_func((p)-1,sizeof*(p)),0 : 0),(p)=NULL) + +#define stbds_hmgets(t, k) (*stbds_hmgetp(t,k)) +#define stbds_hmget(t, k) (stbds_hmgetp(t,k)->value) +#define stbds_hmget_ts(t, k, temp) (stbds_hmgetp_ts(t,k,temp)->value) +#define stbds_hmlen(t) ((t) ? (ptrdiff_t) stbds_header((t)-1)->length-1 : 0) +#define stbds_hmlenu(t) ((t) ? stbds_header((t)-1)->length-1 : 0) +#define stbds_hmgetp_null(t,k) (stbds_hmgeti(t,k) == -1 ? NULL : &(t)[stbds_temp((t)-1)]) + +#define stbds_shput(t, k, v) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ + (t)[stbds_temp((t)-1)].value = (v)) + +#define stbds_shputi(t, k, v) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ + (t)[stbds_temp((t)-1)].value = (v), stbds_temp((t)-1)) + +#define stbds_shputs(t, s) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (s).key, sizeof (s).key, STBDS_HM_STRING), \ + (t)[stbds_temp((t)-1)] = (s), \ + (t)[stbds_temp((t)-1)].key = stbds_temp_key((t)-1)) // above line overwrites whole structure, so must rewrite key here if it was allocated internally + +#define stbds_pshput(t, p) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (p)->key, sizeof (p)->key, STBDS_HM_PTR_TO_STRING), \ + (t)[stbds_temp((t)-1)] = (p)) + +#define stbds_shgeti(t,k) \ + ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ + stbds_temp((t)-1)) + +#define stbds_pshgeti(t,k) \ + ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (*(t))->key, STBDS_HM_PTR_TO_STRING), \ + stbds_temp((t)-1)) + +#define stbds_shgetp(t, k) \ + ((void) stbds_shgeti(t,k), &(t)[stbds_temp((t)-1)]) + +#define stbds_pshget(t, k) \ + ((void) stbds_pshgeti(t,k), (t)[stbds_temp((t)-1)]) + +#define stbds_shdel(t,k) \ + (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_OFFSETOF((t),key), STBDS_HM_STRING)),(t)?stbds_temp((t)-1):0) +#define stbds_pshdel(t,k) \ + (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) (k), sizeof (*(t))->key, STBDS_OFFSETOF(*(t),key), STBDS_HM_PTR_TO_STRING)),(t)?stbds_temp((t)-1):0) + +#define stbds_sh_new_arena(t) \ + ((t) = stbds_shmode_func_wrapper(t, sizeof *(t), STBDS_SH_ARENA)) +#define stbds_sh_new_strdup(t) \ + ((t) = stbds_shmode_func_wrapper(t, sizeof *(t), STBDS_SH_STRDUP)) + +#define stbds_shdefault(t, v) stbds_hmdefault(t,v) +#define stbds_shdefaults(t, s) stbds_hmdefaults(t,s) + +#define stbds_shfree stbds_hmfree +#define stbds_shlenu stbds_hmlenu + +#define stbds_shgets(t, k) (*stbds_shgetp(t,k)) +#define stbds_shget(t, k) (stbds_shgetp(t,k)->value) +#define stbds_shgetp_null(t,k) (stbds_shgeti(t,k) == -1 ? NULL : &(t)[stbds_temp((t)-1)]) +#define stbds_shlen stbds_hmlen + +typedef struct +{ + size_t length; + size_t capacity; + void * hash_table; + ptrdiff_t temp; +} stbds_array_header; + +typedef struct stbds_string_block +{ + struct stbds_string_block *next; + char storage[8]; +} stbds_string_block; + +struct stbds_string_arena +{ + stbds_string_block *storage; + size_t remaining; + unsigned char block; + unsigned char mode; // this isn't used by the string arena itself +}; + +#define STBDS_HM_BINARY 0 +#define STBDS_HM_STRING 1 + +enum +{ + STBDS_SH_NONE, + STBDS_SH_DEFAULT, + STBDS_SH_STRDUP, + STBDS_SH_ARENA +}; + +#ifdef __cplusplus +// in C we use implicit assignment from these void*-returning functions to T*. +// in C++ these templates make the same code work +template static T * stbds_arrgrowf_wrapper(T *a, size_t elemsize, size_t addlen, size_t min_cap) { + return (T*)stbds_arrgrowf((void *)a, elemsize, addlen, min_cap); +} +template static T * stbds_hmget_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) { + return (T*)stbds_hmget_key((void*)a, elemsize, key, keysize, mode); +} +template static T * stbds_hmget_key_ts_wrapper(T *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) { + return (T*)stbds_hmget_key_ts((void*)a, elemsize, key, keysize, temp, mode); +} +template static T * stbds_hmput_default_wrapper(T *a, size_t elemsize) { + return (T*)stbds_hmput_default((void *)a, elemsize); +} +template static T * stbds_hmput_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) { + return (T*)stbds_hmput_key((void*)a, elemsize, key, keysize, mode); +} +template static T * stbds_hmdel_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode){ + return (T*)stbds_hmdel_key((void*)a, elemsize, key, keysize, keyoffset, mode); +} +template static T * stbds_shmode_func_wrapper(T *, size_t elemsize, int mode) { + return (T*)stbds_shmode_func(elemsize, mode); +} +#else +#define stbds_arrgrowf_wrapper stbds_arrgrowf +#define stbds_hmget_key_wrapper stbds_hmget_key +#define stbds_hmget_key_ts_wrapper stbds_hmget_key_ts +#define stbds_hmput_default_wrapper stbds_hmput_default +#define stbds_hmput_key_wrapper stbds_hmput_key +#define stbds_hmdel_key_wrapper stbds_hmdel_key +#define stbds_shmode_func_wrapper(t,e,m) stbds_shmode_func(e,m) +#endif + +#endif // INCLUDE_STB_DS_H + + +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION +// + +#ifdef STB_DS_IMPLEMENTATION +#include +#include + +#ifndef STBDS_ASSERT +#define STBDS_ASSERT_WAS_UNDEFINED +#define STBDS_ASSERT(x) ((void) 0) +#endif + +#ifdef STBDS_STATISTICS +#define STBDS_STATS(x) x +size_t stbds_array_grow; +size_t stbds_hash_grow; +size_t stbds_hash_shrink; +size_t stbds_hash_rebuild; +size_t stbds_hash_probes; +size_t stbds_hash_alloc; +size_t stbds_rehash_probes; +size_t stbds_rehash_items; +#else +#define STBDS_STATS(x) +#endif + +// +// stbds_arr implementation +// + +//int *prev_allocs[65536]; +//int num_prev; + +void *stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap) +{ + stbds_array_header temp={0}; // force debugging + void *b; + size_t min_len = stbds_arrlen(a) + addlen; + (void) sizeof(temp); + + // compute the minimum capacity needed + if (min_len > min_cap) + min_cap = min_len; + + if (min_cap <= stbds_arrcap(a)) + return a; + + // increase needed capacity to guarantee O(1) amortized + if (min_cap < 2 * stbds_arrcap(a)) + min_cap = 2 * stbds_arrcap(a); + else if (min_cap < 4) + min_cap = 4; + + //if (num_prev < 65536) if (a) prev_allocs[num_prev++] = (int *) ((char *) a+1); + //if (num_prev == 2201) + // num_prev = num_prev; + b = STBDS_REALLOC(NULL, (a) ? stbds_header(a) : 0, elemsize * min_cap + sizeof(stbds_array_header)); + //if (num_prev < 65536) prev_allocs[num_prev++] = (int *) (char *) b; + b = (char *) b + sizeof(stbds_array_header); + if (a == NULL) { + stbds_header(b)->length = 0; + stbds_header(b)->hash_table = 0; + stbds_header(b)->temp = 0; + } else { + STBDS_STATS(++stbds_array_grow); + } + stbds_header(b)->capacity = min_cap; + + return b; +} + +void stbds_arrfreef(void *a) +{ + STBDS_FREE(NULL, stbds_header(a)); +} + +// +// stbds_hm hash table implementation +// + +#ifdef STBDS_INTERNAL_SMALL_BUCKET +#define STBDS_BUCKET_LENGTH 4 +#else +#define STBDS_BUCKET_LENGTH 8 +#endif + +#define STBDS_BUCKET_SHIFT (STBDS_BUCKET_LENGTH == 8 ? 3 : 2) +#define STBDS_BUCKET_MASK (STBDS_BUCKET_LENGTH-1) +#define STBDS_CACHE_LINE_SIZE 64 + +#define STBDS_ALIGN_FWD(n,a) (((n) + (a) - 1) & ~((a)-1)) + +typedef struct +{ + size_t hash [STBDS_BUCKET_LENGTH]; + ptrdiff_t index[STBDS_BUCKET_LENGTH]; +} stbds_hash_bucket; // in 32-bit, this is one 64-byte cache line; in 64-bit, each array is one 64-byte cache line + +typedef struct +{ + char * temp_key; // this MUST be the first field of the hash table + size_t slot_count; + size_t used_count; + size_t used_count_threshold; + size_t used_count_shrink_threshold; + size_t tombstone_count; + size_t tombstone_count_threshold; + size_t seed; + size_t slot_count_log2; + stbds_string_arena string; + stbds_hash_bucket *storage; // not a separate allocation, just 64-byte aligned storage after this struct +} stbds_hash_index; + +#define STBDS_INDEX_EMPTY -1 +#define STBDS_INDEX_DELETED -2 +#define STBDS_INDEX_IN_USE(x) ((x) >= 0) + +#define STBDS_HASH_EMPTY 0 +#define STBDS_HASH_DELETED 1 + +static size_t stbds_hash_seed=0x31415926; + +void stbds_rand_seed(size_t seed) +{ + stbds_hash_seed = seed; +} + +#define stbds_load_32_or_64(var, temp, v32, v64_hi, v64_lo) \ + temp = v64_lo ^ v32, temp <<= 16, temp <<= 16, temp >>= 16, temp >>= 16, /* discard if 32-bit */ \ + var = v64_hi, var <<= 16, var <<= 16, /* discard if 32-bit */ \ + var ^= temp ^ v32 + +#define STBDS_SIZE_T_BITS ((sizeof (size_t)) * 8) + +static size_t stbds_probe_position(size_t hash, size_t slot_count, size_t slot_log2) +{ + size_t pos; + STBDS_NOTUSED(slot_log2); + pos = hash & (slot_count-1); + #ifdef STBDS_INTERNAL_BUCKET_START + pos &= ~STBDS_BUCKET_MASK; + #endif + return pos; +} + +static size_t stbds_log2(size_t slot_count) +{ + size_t n=0; + while (slot_count > 1) { + slot_count >>= 1; + ++n; + } + return n; +} + +static stbds_hash_index *stbds_make_hash_index(size_t slot_count, stbds_hash_index *ot) +{ + stbds_hash_index *t; + t = (stbds_hash_index *) STBDS_REALLOC(NULL,0,(slot_count >> STBDS_BUCKET_SHIFT) * sizeof(stbds_hash_bucket) + sizeof(stbds_hash_index) + STBDS_CACHE_LINE_SIZE-1); + t->storage = (stbds_hash_bucket *) STBDS_ALIGN_FWD((size_t) (t+1), STBDS_CACHE_LINE_SIZE); + t->slot_count = slot_count; + t->slot_count_log2 = stbds_log2(slot_count); + t->tombstone_count = 0; + t->used_count = 0; + + #if 0 // A1 + t->used_count_threshold = slot_count*12/16; // if 12/16th of table is occupied, grow + t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild + t->used_count_shrink_threshold = slot_count* 4/16; // if table is only 4/16th full, shrink + #elif 1 // A2 + //t->used_count_threshold = slot_count*12/16; // if 12/16th of table is occupied, grow + //t->tombstone_count_threshold = slot_count* 3/16; // if tombstones are 3/16th of table, rebuild + //t->used_count_shrink_threshold = slot_count* 4/16; // if table is only 4/16th full, shrink + + // compute without overflowing + t->used_count_threshold = slot_count - (slot_count>>2); + t->tombstone_count_threshold = (slot_count>>3) + (slot_count>>4); + t->used_count_shrink_threshold = slot_count >> 2; + + #elif 0 // B1 + t->used_count_threshold = slot_count*13/16; // if 13/16th of table is occupied, grow + t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild + t->used_count_shrink_threshold = slot_count* 5/16; // if table is only 5/16th full, shrink + #else // C1 + t->used_count_threshold = slot_count*14/16; // if 14/16th of table is occupied, grow + t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild + t->used_count_shrink_threshold = slot_count* 6/16; // if table is only 6/16th full, shrink + #endif + // Following statistics were measured on a Core i7-6700 @ 4.00Ghz, compiled with clang 7.0.1 -O2 + // Note that the larger tables have high variance as they were run fewer times + // A1 A2 B1 C1 + // 0.10ms : 0.10ms : 0.10ms : 0.11ms : 2,000 inserts creating 2K table + // 0.96ms : 0.95ms : 0.97ms : 1.04ms : 20,000 inserts creating 20K table + // 14.48ms : 14.46ms : 10.63ms : 11.00ms : 200,000 inserts creating 200K table + // 195.74ms : 196.35ms : 203.69ms : 214.92ms : 2,000,000 inserts creating 2M table + // 2193.88ms : 2209.22ms : 2285.54ms : 2437.17ms : 20,000,000 inserts creating 20M table + // 65.27ms : 53.77ms : 65.33ms : 65.47ms : 500,000 inserts & deletes in 2K table + // 72.78ms : 62.45ms : 71.95ms : 72.85ms : 500,000 inserts & deletes in 20K table + // 89.47ms : 77.72ms : 96.49ms : 96.75ms : 500,000 inserts & deletes in 200K table + // 97.58ms : 98.14ms : 97.18ms : 97.53ms : 500,000 inserts & deletes in 2M table + // 118.61ms : 119.62ms : 120.16ms : 118.86ms : 500,000 inserts & deletes in 20M table + // 192.11ms : 194.39ms : 196.38ms : 195.73ms : 500,000 inserts & deletes in 200M table + + if (slot_count <= STBDS_BUCKET_LENGTH) + t->used_count_shrink_threshold = 0; + // to avoid infinite loop, we need to guarantee that at least one slot is empty and will terminate probes + STBDS_ASSERT(t->used_count_threshold + t->tombstone_count_threshold < t->slot_count); + STBDS_STATS(++stbds_hash_alloc); + if (ot) { + t->string = ot->string; + // reuse old seed so we can reuse old hashes so below "copy out old data" doesn't do any hashing + t->seed = ot->seed; + } else { + size_t a,b,temp; + memset(&t->string, 0, sizeof(t->string)); + t->seed = stbds_hash_seed; + // LCG + // in 32-bit, a = 2147001325 b = 715136305 + // in 64-bit, a = 2862933555777941757 b = 3037000493 + stbds_load_32_or_64(a,temp, 2147001325, 0x27bb2ee6, 0x87b0b0fd); + stbds_load_32_or_64(b,temp, 715136305, 0, 0xb504f32d); + stbds_hash_seed = stbds_hash_seed * a + b; + } + + { + size_t i,j; + for (i=0; i < slot_count >> STBDS_BUCKET_SHIFT; ++i) { + stbds_hash_bucket *b = &t->storage[i]; + for (j=0; j < STBDS_BUCKET_LENGTH; ++j) + b->hash[j] = STBDS_HASH_EMPTY; + for (j=0; j < STBDS_BUCKET_LENGTH; ++j) + b->index[j] = STBDS_INDEX_EMPTY; + } + } + + // copy out the old data, if any + if (ot) { + size_t i,j; + t->used_count = ot->used_count; + for (i=0; i < ot->slot_count >> STBDS_BUCKET_SHIFT; ++i) { + stbds_hash_bucket *ob = &ot->storage[i]; + for (j=0; j < STBDS_BUCKET_LENGTH; ++j) { + if (STBDS_INDEX_IN_USE(ob->index[j])) { + size_t hash = ob->hash[j]; + size_t pos = stbds_probe_position(hash, t->slot_count, t->slot_count_log2); + size_t step = STBDS_BUCKET_LENGTH; + STBDS_STATS(++stbds_rehash_items); + for (;;) { + size_t limit,z; + stbds_hash_bucket *bucket; + bucket = &t->storage[pos >> STBDS_BUCKET_SHIFT]; + STBDS_STATS(++stbds_rehash_probes); + + for (z=pos & STBDS_BUCKET_MASK; z < STBDS_BUCKET_LENGTH; ++z) { + if (bucket->hash[z] == 0) { + bucket->hash[z] = hash; + bucket->index[z] = ob->index[j]; + goto done; + } + } + + limit = pos & STBDS_BUCKET_MASK; + for (z = 0; z < limit; ++z) { + if (bucket->hash[z] == 0) { + bucket->hash[z] = hash; + bucket->index[z] = ob->index[j]; + goto done; + } + } + + pos += step; // quadratic probing + step += STBDS_BUCKET_LENGTH; + pos &= (t->slot_count-1); + } + } + done: + ; + } + } + } + + return t; +} + +#define STBDS_ROTATE_LEFT(val, n) (((val) << (n)) | ((val) >> (STBDS_SIZE_T_BITS - (n)))) +#define STBDS_ROTATE_RIGHT(val, n) (((val) >> (n)) | ((val) << (STBDS_SIZE_T_BITS - (n)))) + +size_t stbds_hash_string(char *str, size_t seed) +{ + size_t hash = seed; + while (*str) + hash = STBDS_ROTATE_LEFT(hash, 9) + (unsigned char) *str++; + + // Thomas Wang 64-to-32 bit mix function, hopefully also works in 32 bits + hash ^= seed; + hash = (~hash) + (hash << 18); + hash ^= hash ^ STBDS_ROTATE_RIGHT(hash,31); + hash = hash * 21; + hash ^= hash ^ STBDS_ROTATE_RIGHT(hash,11); + hash += (hash << 6); + hash ^= STBDS_ROTATE_RIGHT(hash,22); + return hash+seed; +} + +#ifdef STBDS_SIPHASH_2_4 +#define STBDS_SIPHASH_C_ROUNDS 2 +#define STBDS_SIPHASH_D_ROUNDS 4 +typedef int STBDS_SIPHASH_2_4_can_only_be_used_in_64_bit_builds[sizeof(size_t) == 8 ? 1 : -1]; +#endif + +#ifndef STBDS_SIPHASH_C_ROUNDS +#define STBDS_SIPHASH_C_ROUNDS 1 +#endif +#ifndef STBDS_SIPHASH_D_ROUNDS +#define STBDS_SIPHASH_D_ROUNDS 1 +#endif + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable:4127) // conditional expression is constant, for do..while(0) and sizeof()== +#endif + +static size_t stbds_siphash_bytes(void *p, size_t len, size_t seed) +{ + unsigned char *d = (unsigned char *) p; + size_t i,j; + size_t v0,v1,v2,v3, data; + + // hash that works on 32- or 64-bit registers without knowing which we have + // (computes different results on 32-bit and 64-bit platform) + // derived from siphash, but on 32-bit platforms very different as it uses 4 32-bit state not 4 64-bit + v0 = ((((size_t) 0x736f6d65 << 16) << 16) + 0x70736575) ^ seed; + v1 = ((((size_t) 0x646f7261 << 16) << 16) + 0x6e646f6d) ^ ~seed; + v2 = ((((size_t) 0x6c796765 << 16) << 16) + 0x6e657261) ^ seed; + v3 = ((((size_t) 0x74656462 << 16) << 16) + 0x79746573) ^ ~seed; + + #ifdef STBDS_TEST_SIPHASH_2_4 + // hardcoded with key material in the siphash test vectors + v0 ^= 0x0706050403020100ull ^ seed; + v1 ^= 0x0f0e0d0c0b0a0908ull ^ ~seed; + v2 ^= 0x0706050403020100ull ^ seed; + v3 ^= 0x0f0e0d0c0b0a0908ull ^ ~seed; + #endif + + #define STBDS_SIPROUND() \ + do { \ + v0 += v1; v1 = STBDS_ROTATE_LEFT(v1, 13); v1 ^= v0; v0 = STBDS_ROTATE_LEFT(v0,STBDS_SIZE_T_BITS/2); \ + v2 += v3; v3 = STBDS_ROTATE_LEFT(v3, 16); v3 ^= v2; \ + v2 += v1; v1 = STBDS_ROTATE_LEFT(v1, 17); v1 ^= v2; v2 = STBDS_ROTATE_LEFT(v2,STBDS_SIZE_T_BITS/2); \ + v0 += v3; v3 = STBDS_ROTATE_LEFT(v3, 21); v3 ^= v0; \ + } while (0) + + for (i=0; i+sizeof(size_t) <= len; i += sizeof(size_t), d += sizeof(size_t)) { + data = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); + data |= (size_t) (d[4] | (d[5] << 8) | (d[6] << 16) | (d[7] << 24)) << 16 << 16; // discarded if size_t == 4 + + v3 ^= data; + for (j=0; j < STBDS_SIPHASH_C_ROUNDS; ++j) + STBDS_SIPROUND(); + v0 ^= data; + } + data = len << (STBDS_SIZE_T_BITS-8); + switch (len - i) { + case 7: data |= ((size_t) d[6] << 24) << 24; // fall through + case 6: data |= ((size_t) d[5] << 20) << 20; // fall through + case 5: data |= ((size_t) d[4] << 16) << 16; // fall through + case 4: data |= (d[3] << 24); // fall through + case 3: data |= (d[2] << 16); // fall through + case 2: data |= (d[1] << 8); // fall through + case 1: data |= d[0]; // fall through + case 0: break; + } + v3 ^= data; + for (j=0; j < STBDS_SIPHASH_C_ROUNDS; ++j) + STBDS_SIPROUND(); + v0 ^= data; + v2 ^= 0xff; + for (j=0; j < STBDS_SIPHASH_D_ROUNDS; ++j) + STBDS_SIPROUND(); + +#ifdef STBDS_SIPHASH_2_4 + return v0^v1^v2^v3; +#else + return v1^v2^v3; // slightly stronger since v0^v3 in above cancels out final round operation? I tweeted at the authors of SipHash about this but they didn't reply +#endif +} + +size_t stbds_hash_bytes(void *p, size_t len, size_t seed) +{ +#ifdef STBDS_SIPHASH_2_4 + return stbds_siphash_bytes(p,len,seed); +#else + unsigned char *d = (unsigned char *) p; + + if (len == 4) { + unsigned int hash = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); + #if 0 + // HASH32-A Bob Jenkin's hash function w/o large constants + hash ^= seed; + hash -= (hash<<6); + hash ^= (hash>>17); + hash -= (hash<<9); + hash ^= seed; + hash ^= (hash<<4); + hash -= (hash<<3); + hash ^= (hash<<10); + hash ^= (hash>>15); + #elif 1 + // HASH32-BB Bob Jenkin's presumably-accidental version of Thomas Wang hash with rotates turned into shifts. + // Note that converting these back to rotates makes it run a lot slower, presumably due to collisions, so I'm + // not really sure what's going on. + hash ^= seed; + hash = (hash ^ 61) ^ (hash >> 16); + hash = hash + (hash << 3); + hash = hash ^ (hash >> 4); + hash = hash * 0x27d4eb2d; + hash ^= seed; + hash = hash ^ (hash >> 15); + #else // HASH32-C - Murmur3 + hash ^= seed; + hash *= 0xcc9e2d51; + hash = (hash << 17) | (hash >> 15); + hash *= 0x1b873593; + hash ^= seed; + hash = (hash << 19) | (hash >> 13); + hash = hash*5 + 0xe6546b64; + hash ^= hash >> 16; + hash *= 0x85ebca6b; + hash ^= seed; + hash ^= hash >> 13; + hash *= 0xc2b2ae35; + hash ^= hash >> 16; + #endif + // Following statistics were measured on a Core i7-6700 @ 4.00Ghz, compiled with clang 7.0.1 -O2 + // Note that the larger tables have high variance as they were run fewer times + // HASH32-A // HASH32-BB // HASH32-C + // 0.10ms // 0.10ms // 0.10ms : 2,000 inserts creating 2K table + // 0.96ms // 0.95ms // 0.99ms : 20,000 inserts creating 20K table + // 14.69ms // 14.43ms // 14.97ms : 200,000 inserts creating 200K table + // 199.99ms // 195.36ms // 202.05ms : 2,000,000 inserts creating 2M table + // 2234.84ms // 2187.74ms // 2240.38ms : 20,000,000 inserts creating 20M table + // 55.68ms // 53.72ms // 57.31ms : 500,000 inserts & deletes in 2K table + // 63.43ms // 61.99ms // 65.73ms : 500,000 inserts & deletes in 20K table + // 80.04ms // 77.96ms // 81.83ms : 500,000 inserts & deletes in 200K table + // 100.42ms // 97.40ms // 102.39ms : 500,000 inserts & deletes in 2M table + // 119.71ms // 120.59ms // 121.63ms : 500,000 inserts & deletes in 20M table + // 185.28ms // 195.15ms // 187.74ms : 500,000 inserts & deletes in 200M table + // 15.58ms // 14.79ms // 15.52ms : 200,000 inserts creating 200K table with varying key spacing + + return (((size_t) hash << 16 << 16) | hash) ^ seed; + } else if (len == 8 && sizeof(size_t) == 8) { + size_t hash = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); + hash |= (size_t) (d[4] | (d[5] << 8) | (d[6] << 16) | (d[7] << 24)) << 16 << 16; // avoid warning if size_t == 4 + hash ^= seed; + hash = (~hash) + (hash << 21); + hash ^= STBDS_ROTATE_RIGHT(hash,24); + hash *= 265; + hash ^= STBDS_ROTATE_RIGHT(hash,14); + hash ^= seed; + hash *= 21; + hash ^= STBDS_ROTATE_RIGHT(hash,28); + hash += (hash << 31); + hash = (~hash) + (hash << 18); + return hash; + } else { + return stbds_siphash_bytes(p,len,seed); + } +#endif +} +#ifdef _MSC_VER +#pragma warning(pop) +#endif + + +static int stbds_is_key_equal(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode, size_t i) +{ + if (mode >= STBDS_HM_STRING) + return 0==strcmp((char *) key, * (char **) ((char *) a + elemsize*i + keyoffset)); + else + return 0==memcmp(key, (char *) a + elemsize*i + keyoffset, keysize); +} + +#define STBDS_HASH_TO_ARR(x,elemsize) ((char*) (x) - (elemsize)) +#define STBDS_ARR_TO_HASH(x,elemsize) ((char*) (x) + (elemsize)) + +#define stbds_hash_table(a) ((stbds_hash_index *) stbds_header(a)->hash_table) + +void stbds_hmfree_func(void *a, size_t elemsize) +{ + if (a == NULL) return; + if (stbds_hash_table(a) != NULL) { + if (stbds_hash_table(a)->string.mode == STBDS_SH_STRDUP) { + size_t i; + // skip 0th element, which is default + for (i=1; i < stbds_header(a)->length; ++i) + STBDS_FREE(NULL, *(char**) ((char *) a + elemsize*i)); + } + stbds_strreset(&stbds_hash_table(a)->string); + } + STBDS_FREE(NULL, stbds_header(a)->hash_table); + STBDS_FREE(NULL, stbds_header(a)); +} + +static ptrdiff_t stbds_hm_find_slot(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode) +{ + void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); + stbds_hash_index *table = stbds_hash_table(raw_a); + size_t hash = mode >= STBDS_HM_STRING ? stbds_hash_string((char*)key,table->seed) : stbds_hash_bytes(key, keysize,table->seed); + size_t step = STBDS_BUCKET_LENGTH; + size_t limit,i; + size_t pos; + stbds_hash_bucket *bucket; + + if (hash < 2) hash += 2; // stored hash values are forbidden from being 0, so we can detect empty slots + + pos = stbds_probe_position(hash, table->slot_count, table->slot_count_log2); + + for (;;) { + STBDS_STATS(++stbds_hash_probes); + bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; + + // start searching from pos to end of bucket, this should help performance on small hash tables that fit in cache + for (i=pos & STBDS_BUCKET_MASK; i < STBDS_BUCKET_LENGTH; ++i) { + if (bucket->hash[i] == hash) { + if (stbds_is_key_equal(a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { + return (pos & ~STBDS_BUCKET_MASK)+i; + } + } else if (bucket->hash[i] == STBDS_HASH_EMPTY) { + return -1; + } + } + + // search from beginning of bucket to pos + limit = pos & STBDS_BUCKET_MASK; + for (i = 0; i < limit; ++i) { + if (bucket->hash[i] == hash) { + if (stbds_is_key_equal(a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { + return (pos & ~STBDS_BUCKET_MASK)+i; + } + } else if (bucket->hash[i] == STBDS_HASH_EMPTY) { + return -1; + } + } + + // quadratic probing + pos += step; + step += STBDS_BUCKET_LENGTH; + pos &= (table->slot_count-1); + } + /* NOTREACHED */ +} + +void * stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) +{ + size_t keyoffset = 0; + if (a == NULL) { + // make it non-empty so we can return a temp + a = stbds_arrgrowf(0, elemsize, 0, 1); + stbds_header(a)->length += 1; + memset(a, 0, elemsize); + *temp = STBDS_INDEX_EMPTY; + // adjust a to point after the default element + return STBDS_ARR_TO_HASH(a,elemsize); + } else { + stbds_hash_index *table; + void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); + // adjust a to point to the default element + table = (stbds_hash_index *) stbds_header(raw_a)->hash_table; + if (table == 0) { + *temp = -1; + } else { + ptrdiff_t slot = stbds_hm_find_slot(a, elemsize, key, keysize, keyoffset, mode); + if (slot < 0) { + *temp = STBDS_INDEX_EMPTY; + } else { + stbds_hash_bucket *b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; + *temp = b->index[slot & STBDS_BUCKET_MASK]; + } + } + return a; + } +} + +void * stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode) +{ + ptrdiff_t temp; + void *p = stbds_hmget_key_ts(a, elemsize, key, keysize, &temp, mode); + stbds_temp(STBDS_HASH_TO_ARR(p,elemsize)) = temp; + return p; +} + +void * stbds_hmput_default(void *a, size_t elemsize) +{ + // three cases: + // a is NULL <- allocate + // a has a hash table but no entries, because of shmode <- grow + // a has entries <- do nothing + if (a == NULL || stbds_header(STBDS_HASH_TO_ARR(a,elemsize))->length == 0) { + a = stbds_arrgrowf(a ? STBDS_HASH_TO_ARR(a,elemsize) : NULL, elemsize, 0, 1); + stbds_header(a)->length += 1; + memset(a, 0, elemsize); + a=STBDS_ARR_TO_HASH(a,elemsize); + } + return a; +} + +static char *stbds_strdup(char *str); + +void *stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode) +{ + size_t keyoffset=0; + void *raw_a; + stbds_hash_index *table; + + if (a == NULL) { + a = stbds_arrgrowf(0, elemsize, 0, 1); + memset(a, 0, elemsize); + stbds_header(a)->length += 1; + // adjust a to point AFTER the default element + a = STBDS_ARR_TO_HASH(a,elemsize); + } + + // adjust a to point to the default element + raw_a = a; + a = STBDS_HASH_TO_ARR(a,elemsize); + + table = (stbds_hash_index *) stbds_header(a)->hash_table; + + if (table == NULL || table->used_count >= table->used_count_threshold) { + stbds_hash_index *nt; + size_t slot_count; + + slot_count = (table == NULL) ? STBDS_BUCKET_LENGTH : table->slot_count*2; + nt = stbds_make_hash_index(slot_count, table); + if (table) + STBDS_FREE(NULL, table); + else + nt->string.mode = mode >= STBDS_HM_STRING ? STBDS_SH_DEFAULT : 0; + stbds_header(a)->hash_table = table = nt; + STBDS_STATS(++stbds_hash_grow); + } + + // we iterate hash table explicitly because we want to track if we saw a tombstone + { + size_t hash = mode >= STBDS_HM_STRING ? stbds_hash_string((char*)key,table->seed) : stbds_hash_bytes(key, keysize,table->seed); + size_t step = STBDS_BUCKET_LENGTH; + size_t pos; + ptrdiff_t tombstone = -1; + stbds_hash_bucket *bucket; + + // stored hash values are forbidden from being 0, so we can detect empty slots to early out quickly + if (hash < 2) hash += 2; + + pos = stbds_probe_position(hash, table->slot_count, table->slot_count_log2); + + for (;;) { + size_t limit, i; + STBDS_STATS(++stbds_hash_probes); + bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; + + // start searching from pos to end of bucket + for (i=pos & STBDS_BUCKET_MASK; i < STBDS_BUCKET_LENGTH; ++i) { + if (bucket->hash[i] == hash) { + if (stbds_is_key_equal(raw_a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { + stbds_temp(a) = bucket->index[i]; + if (mode >= STBDS_HM_STRING) + stbds_temp_key(a) = * (char **) ((char *) raw_a + elemsize*bucket->index[i] + keyoffset); + return STBDS_ARR_TO_HASH(a,elemsize); + } + } else if (bucket->hash[i] == 0) { + pos = (pos & ~STBDS_BUCKET_MASK) + i; + goto found_empty_slot; + } else if (tombstone < 0) { + if (bucket->index[i] == STBDS_INDEX_DELETED) + tombstone = (ptrdiff_t) ((pos & ~STBDS_BUCKET_MASK) + i); + } + } + + // search from beginning of bucket to pos + limit = pos & STBDS_BUCKET_MASK; + for (i = 0; i < limit; ++i) { + if (bucket->hash[i] == hash) { + if (stbds_is_key_equal(raw_a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { + stbds_temp(a) = bucket->index[i]; + return STBDS_ARR_TO_HASH(a,elemsize); + } + } else if (bucket->hash[i] == 0) { + pos = (pos & ~STBDS_BUCKET_MASK) + i; + goto found_empty_slot; + } else if (tombstone < 0) { + if (bucket->index[i] == STBDS_INDEX_DELETED) + tombstone = (ptrdiff_t) ((pos & ~STBDS_BUCKET_MASK) + i); + } + } + + // quadratic probing + pos += step; + step += STBDS_BUCKET_LENGTH; + pos &= (table->slot_count-1); + } + found_empty_slot: + if (tombstone >= 0) { + pos = tombstone; + --table->tombstone_count; + } + ++table->used_count; + + { + ptrdiff_t i = (ptrdiff_t) stbds_arrlen(a); + // we want to do stbds_arraddn(1), but we can't use the macros since we don't have something of the right type + if ((size_t) i+1 > stbds_arrcap(a)) + *(void **) &a = stbds_arrgrowf(a, elemsize, 1, 0); + raw_a = STBDS_ARR_TO_HASH(a,elemsize); + + STBDS_ASSERT((size_t) i+1 <= stbds_arrcap(a)); + stbds_header(a)->length = i+1; + bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; + bucket->hash[pos & STBDS_BUCKET_MASK] = hash; + bucket->index[pos & STBDS_BUCKET_MASK] = i-1; + stbds_temp(a) = i-1; + + switch (table->string.mode) { + case STBDS_SH_STRDUP: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = stbds_strdup((char*) key); break; + case STBDS_SH_ARENA: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = stbds_stralloc(&table->string, (char*)key); break; + case STBDS_SH_DEFAULT: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = (char *) key; break; + default: memcpy((char *) a + elemsize*i, key, keysize); break; + } + } + return STBDS_ARR_TO_HASH(a,elemsize); + } +} + +void * stbds_shmode_func(size_t elemsize, int mode) +{ + void *a = stbds_arrgrowf(0, elemsize, 0, 1); + stbds_hash_index *h; + memset(a, 0, elemsize); + stbds_header(a)->length = 1; + stbds_header(a)->hash_table = h = (stbds_hash_index *) stbds_make_hash_index(STBDS_BUCKET_LENGTH, NULL); + h->string.mode = (unsigned char) mode; + return STBDS_ARR_TO_HASH(a,elemsize); +} + +void * stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode) +{ + if (a == NULL) { + return 0; + } else { + stbds_hash_index *table; + void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); + table = (stbds_hash_index *) stbds_header(raw_a)->hash_table; + stbds_temp(raw_a) = 0; + if (table == 0) { + return a; + } else { + ptrdiff_t slot; + slot = stbds_hm_find_slot(a, elemsize, key, keysize, keyoffset, mode); + if (slot < 0) + return a; + else { + stbds_hash_bucket *b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; + int i = slot & STBDS_BUCKET_MASK; + ptrdiff_t old_index = b->index[i]; + ptrdiff_t final_index = (ptrdiff_t) stbds_arrlen(raw_a)-1-1; // minus one for the raw_a vs a, and minus one for 'last' + STBDS_ASSERT(slot < (ptrdiff_t) table->slot_count); + --table->used_count; + ++table->tombstone_count; + stbds_temp(raw_a) = 1; + STBDS_ASSERT(table->used_count >= 0); + //STBDS_ASSERT(table->tombstone_count < table->slot_count/4); + b->hash[i] = STBDS_HASH_DELETED; + b->index[i] = STBDS_INDEX_DELETED; + + if (mode == STBDS_HM_STRING && table->string.mode == STBDS_SH_STRDUP) + STBDS_FREE(NULL, *(char**) ((char *) a+elemsize*old_index)); + + // if indices are the same, memcpy is a no-op, but back-pointer-fixup will fail, so skip + if (old_index != final_index) { + // swap delete + memmove((char*) a + elemsize*old_index, (char*) a + elemsize*final_index, elemsize); + + // now find the slot for the last element + if (mode == STBDS_HM_STRING) + slot = stbds_hm_find_slot(a, elemsize, *(char**) ((char *) a+elemsize*old_index + keyoffset), keysize, keyoffset, mode); + else + slot = stbds_hm_find_slot(a, elemsize, (char* ) a+elemsize*old_index + keyoffset, keysize, keyoffset, mode); + STBDS_ASSERT(slot >= 0); + b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; + i = slot & STBDS_BUCKET_MASK; + STBDS_ASSERT(b->index[i] == final_index); + b->index[i] = old_index; + } + stbds_header(raw_a)->length -= 1; + + if (table->used_count < table->used_count_shrink_threshold && table->slot_count > STBDS_BUCKET_LENGTH) { + stbds_header(raw_a)->hash_table = stbds_make_hash_index(table->slot_count>>1, table); + STBDS_FREE(NULL, table); + STBDS_STATS(++stbds_hash_shrink); + } else if (table->tombstone_count > table->tombstone_count_threshold) { + stbds_header(raw_a)->hash_table = stbds_make_hash_index(table->slot_count , table); + STBDS_FREE(NULL, table); + STBDS_STATS(++stbds_hash_rebuild); + } + + return a; + } + } + } + /* NOTREACHED */ +} + +static char *stbds_strdup(char *str) +{ + // to keep replaceable allocator simple, we don't want to use strdup. + // rolling our own also avoids problem of strdup vs _strdup + size_t len = strlen(str)+1; + char *p = (char*) STBDS_REALLOC(NULL, 0, len); + memmove(p, str, len); + return p; +} + +#ifndef STBDS_STRING_ARENA_BLOCKSIZE_MIN +#define STBDS_STRING_ARENA_BLOCKSIZE_MIN 512u +#endif +#ifndef STBDS_STRING_ARENA_BLOCKSIZE_MAX +#define STBDS_STRING_ARENA_BLOCKSIZE_MAX (1u<<20) +#endif + +char *stbds_stralloc(stbds_string_arena *a, char *str) +{ + char *p; + size_t len = strlen(str)+1; + if (len > a->remaining) { + // compute the next blocksize + size_t blocksize = a->block; + + // size is 512, 512, 1024, 1024, 2048, 2048, 4096, 4096, etc., so that + // there are log(SIZE) allocations to free when we destroy the table + blocksize = (size_t) (STBDS_STRING_ARENA_BLOCKSIZE_MIN) << (blocksize>>1); + + // if size is under 1M, advance to next blocktype + if (blocksize < (size_t)(STBDS_STRING_ARENA_BLOCKSIZE_MAX)) + ++a->block; + + if (len > blocksize) { + // if string is larger than blocksize, then just allocate the full size. + // note that we still advance string_block so block size will continue + // increasing, so e.g. if somebody only calls this with 1000-long strings, + // eventually the arena will start doubling and handling those as well + stbds_string_block *sb = (stbds_string_block *) STBDS_REALLOC(NULL, 0, sizeof(*sb)-8 + len); + memmove(sb->storage, str, len); + if (a->storage) { + // insert it after the first element, so that we don't waste the space there + sb->next = a->storage->next; + a->storage->next = sb; + } else { + sb->next = 0; + a->storage = sb; + a->remaining = 0; // this is redundant, but good for clarity + } + return sb->storage; + } else { + stbds_string_block *sb = (stbds_string_block *) STBDS_REALLOC(NULL, 0, sizeof(*sb)-8 + blocksize); + sb->next = a->storage; + a->storage = sb; + a->remaining = blocksize; + } + } + + STBDS_ASSERT(len <= a->remaining); + p = a->storage->storage + a->remaining - len; + a->remaining -= len; + memmove(p, str, len); + return p; +} + +void stbds_strreset(stbds_string_arena *a) +{ + stbds_string_block *x,*y; + x = a->storage; + while (x) { + y = x->next; + STBDS_FREE(NULL, x); + x = y; + } + memset(a, 0, sizeof(*a)); +} + +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// UNIT TESTS +// + +#ifdef STBDS_UNIT_TESTS +#include +#ifdef STBDS_ASSERT_WAS_UNDEFINED +#undef STBDS_ASSERT +#endif +#ifndef STBDS_ASSERT +#define STBDS_ASSERT assert +#include +#endif + +typedef struct { int key,b,c,d; } stbds_struct; +typedef struct { int key[2],b,c,d; } stbds_struct2; + +static char buffer[256]; +char *strkey(int n) +{ +#if defined(_WIN32) && defined(__STDC_WANT_SECURE_LIB__) + sprintf_s(buffer, sizeof(buffer), "test_%d", n); +#else + sprintf(buffer, "test_%d", n); +#endif + return buffer; +} + +void stbds_unit_tests(void) +{ +#if defined(_MSC_VER) && _MSC_VER <= 1200 && defined(__cplusplus) + // VC6 C++ doesn't like the template<> trick on unnamed structures, so do nothing! + STBDS_ASSERT(0); +#else + const int testsize = 100000; + const int testsize2 = testsize/20; + int *arr=NULL; + struct { int key; int value; } *intmap = NULL; + struct { char *key; int value; } *strmap = NULL, s; + struct { stbds_struct key; int value; } *map = NULL; + stbds_struct *map2 = NULL; + stbds_struct2 *map3 = NULL; + stbds_string_arena sa = { 0 }; + int key3[2] = { 1,2 }; + ptrdiff_t temp; + + int i,j; + + STBDS_ASSERT(arrlen(arr)==0); + for (i=0; i < 20000; i += 50) { + for (j=0; j < i; ++j) + arrpush(arr,j); + arrfree(arr); + } + + for (i=0; i < 4; ++i) { + arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); + arrdel(arr,i); + arrfree(arr); + arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); + arrdelswap(arr,i); + arrfree(arr); + } + + for (i=0; i < 5; ++i) { + arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); + stbds_arrins(arr,i,5); + STBDS_ASSERT(arr[i] == 5); + if (i < 4) + STBDS_ASSERT(arr[4] == 4); + arrfree(arr); + } + + i = 1; + STBDS_ASSERT(hmgeti(intmap,i) == -1); + hmdefault(intmap, -2); + STBDS_ASSERT(hmgeti(intmap, i) == -1); + STBDS_ASSERT(hmget (intmap, i) == -2); + for (i=0; i < testsize; i+=2) + hmput(intmap, i, i*5); + for (i=0; i < testsize; i+=1) { + if (i & 1) STBDS_ASSERT(hmget(intmap, i) == -2 ); + else STBDS_ASSERT(hmget(intmap, i) == i*5); + if (i & 1) STBDS_ASSERT(hmget_ts(intmap, i, temp) == -2 ); + else STBDS_ASSERT(hmget_ts(intmap, i, temp) == i*5); + } + for (i=0; i < testsize; i+=2) + hmput(intmap, i, i*3); + for (i=0; i < testsize; i+=1) + if (i & 1) STBDS_ASSERT(hmget(intmap, i) == -2 ); + else STBDS_ASSERT(hmget(intmap, i) == i*3); + for (i=2; i < testsize; i+=4) + hmdel(intmap, i); // delete half the entries + for (i=0; i < testsize; i+=1) + if (i & 3) STBDS_ASSERT(hmget(intmap, i) == -2 ); + else STBDS_ASSERT(hmget(intmap, i) == i*3); + for (i=0; i < testsize; i+=1) + hmdel(intmap, i); // delete the rest of the entries + for (i=0; i < testsize; i+=1) + STBDS_ASSERT(hmget(intmap, i) == -2 ); + hmfree(intmap); + for (i=0; i < testsize; i+=2) + hmput(intmap, i, i*3); + hmfree(intmap); + + #if defined(__clang__) || defined(__GNUC__) + #ifndef __cplusplus + intmap = NULL; + hmput(intmap, 15, 7); + hmput(intmap, 11, 3); + hmput(intmap, 9, 5); + STBDS_ASSERT(hmget(intmap, 9) == 5); + STBDS_ASSERT(hmget(intmap, 11) == 3); + STBDS_ASSERT(hmget(intmap, 15) == 7); + #endif + #endif + + for (i=0; i < testsize; ++i) + stralloc(&sa, strkey(i)); + strreset(&sa); + + { + s.key = "a", s.value = 1; + shputs(strmap, s); + STBDS_ASSERT(*strmap[0].key == 'a'); + STBDS_ASSERT(strmap[0].key == s.key); + STBDS_ASSERT(strmap[0].value == s.value); + shfree(strmap); + } + + { + s.key = "a", s.value = 1; + sh_new_strdup(strmap); + shputs(strmap, s); + STBDS_ASSERT(*strmap[0].key == 'a'); + STBDS_ASSERT(strmap[0].key != s.key); + STBDS_ASSERT(strmap[0].value == s.value); + shfree(strmap); + } + + { + s.key = "a", s.value = 1; + sh_new_arena(strmap); + shputs(strmap, s); + STBDS_ASSERT(*strmap[0].key == 'a'); + STBDS_ASSERT(strmap[0].key != s.key); + STBDS_ASSERT(strmap[0].value == s.value); + shfree(strmap); + } + + for (j=0; j < 2; ++j) { + STBDS_ASSERT(shgeti(strmap,"foo") == -1); + if (j == 0) + sh_new_strdup(strmap); + else + sh_new_arena(strmap); + STBDS_ASSERT(shgeti(strmap,"foo") == -1); + shdefault(strmap, -2); + STBDS_ASSERT(shgeti(strmap,"foo") == -1); + for (i=0; i < testsize; i+=2) + shput(strmap, strkey(i), i*3); + for (i=0; i < testsize; i+=1) + if (i & 1) STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); + else STBDS_ASSERT(shget(strmap, strkey(i)) == i*3); + for (i=2; i < testsize; i+=4) + shdel(strmap, strkey(i)); // delete half the entries + for (i=0; i < testsize; i+=1) + if (i & 3) STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); + else STBDS_ASSERT(shget(strmap, strkey(i)) == i*3); + for (i=0; i < testsize; i+=1) + shdel(strmap, strkey(i)); // delete the rest of the entries + for (i=0; i < testsize; i+=1) + STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); + shfree(strmap); + } + + { + struct { char *key; char value; } *hash = NULL; + char name[4] = "jen"; + shput(hash, "bob" , 'h'); + shput(hash, "sally" , 'e'); + shput(hash, "fred" , 'l'); + shput(hash, "jen" , 'x'); + shput(hash, "doug" , 'o'); + + shput(hash, name , 'l'); + shfree(hash); + } + + for (i=0; i < testsize; i += 2) { + stbds_struct s = { i,i*2,i*3,i*4 }; + hmput(map, s, i*5); + } + + for (i=0; i < testsize; i += 1) { + stbds_struct s = { i,i*2,i*3 ,i*4 }; + stbds_struct t = { i,i*2,i*3+1,i*4 }; + if (i & 1) STBDS_ASSERT(hmget(map, s) == 0); + else STBDS_ASSERT(hmget(map, s) == i*5); + if (i & 1) STBDS_ASSERT(hmget_ts(map, s, temp) == 0); + else STBDS_ASSERT(hmget_ts(map, s, temp) == i*5); + //STBDS_ASSERT(hmget(map, t.key) == 0); + } + + for (i=0; i < testsize; i += 2) { + stbds_struct s = { i,i*2,i*3,i*4 }; + hmputs(map2, s); + } + hmfree(map); + + for (i=0; i < testsize; i += 1) { + stbds_struct s = { i,i*2,i*3,i*4 }; + stbds_struct t = { i,i*2,i*3+1,i*4 }; + if (i & 1) STBDS_ASSERT(hmgets(map2, s.key).d == 0); + else STBDS_ASSERT(hmgets(map2, s.key).d == i*4); + //STBDS_ASSERT(hmgetp(map2, t.key) == 0); + } + hmfree(map2); + + for (i=0; i < testsize; i += 2) { + stbds_struct2 s = { { i,i*2 }, i*3,i*4, i*5 }; + hmputs(map3, s); + } + for (i=0; i < testsize; i += 1) { + stbds_struct2 s = { { i,i*2}, i*3, i*4, i*5 }; + stbds_struct2 t = { { i,i*2}, i*3+1, i*4, i*5 }; + if (i & 1) STBDS_ASSERT(hmgets(map3, s.key).d == 0); + else STBDS_ASSERT(hmgets(map3, s.key).d == i*5); + //STBDS_ASSERT(hmgetp(map3, t.key) == 0); + } +#endif +} +#endif + + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2019 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/