cmake_minimum_required(VERSION 3.21..3.31)
option(PE_PS5_PAYLOAD "Build only the public SDK logging/filesystem payload" OFF)
if(PE_PS5_PAYLOAD)
    project(pe-ps5-payload LANGUAGES C)
    include(cmake/ps5/payload.cmake)
    return()
endif()
option(PE_PS5_BOOTSTRAP "Build only the isolated PS5 bootstrap milestone" OFF)
if(PE_PS5_BOOTSTRAP)
    project(pe-ps5-bootstrap LANGUAGES NONE)
    include(cmake/ps5/bootstrap.cmake)
    return()
endif()
# Explicit deployment target avoids inheriting the build host's macOS version.
if(CMAKE_HOST_APPLE AND NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET
   AND (NOT DEFINED CMAKE_SYSTEM_NAME OR CMAKE_SYSTEM_NAME STREQUAL "Darwin"))
    set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum supported macOS version")
endif()
option(PE_PS5_GAME "Build the native game with the public PS5 SDK" OFF)
project(parasite-eve-pc VERSION 0.12.0 LANGUAGES C)
if(PE_PS5_GAME)
    include(cmake/ps5/game.cmake)
endif()
option(PE_MACOS_BUNDLE "Package the macOS application bundle" OFF)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)

option(PE_ENABLE_COVERAGE "Measure execution coverage with Clang/LLVM" OFF)
if(PE_ENABLE_COVERAGE)
    if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang")
        message(FATAL_ERROR "PE_ENABLE_COVERAGE requires Clang and its LLVM profile runtime")
    endif()
    add_compile_options(-fprofile-instr-generate -fcoverage-mapping -fprofile-update=atomic)
    add_link_options(-fprofile-instr-generate)
    add_compile_definitions(PE_NATIVE_COVERAGE=1)
endif()

option(BUILD_TESTING "Build original game tests" OFF)
include(CTest)

# Mod host (mods/). Off: the PE_HOOK lines compile to nothing.
option(PE_MODS "Build with the mod host and the mods in mods/" OFF)
if(PE_MODS)
    add_compile_definitions(PE_MODS=1)
endif()

# PsyZ belongs to the original game build, independently of the abandoned frontend.
if(NOT PE_PS5_GAME)
    set(PSYZ_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/psyz)
endif()
add_subdirectory(${PSYZ_DIR}/psyz ${CMAKE_CURRENT_BINARY_DIR}/psyz)
target_compile_definitions(psyz PRIVATE __psyz)

# CHD disc images are read with libchdr (BSD-3-Clause, with its bundled lzma, zstd,
# miniz and dr_flac), vendored in third_party/libchdr. Linking it through psyz gives
# every target that links the disc reader the library and its headers.
set(CHDR_WANT_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/libchdr EXCLUDE_FROM_ALL)
target_link_libraries(psyz PUBLIC chdr-static)

# The original actor/task workspaces are shared by distinct layout views.
# Preserve those accesses instead of assuming different struct tags cannot alias.
# Set this after configuring PsyZ: its SDK/SDL targets retain their own options.
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
    add_compile_options("$<$<COMPILE_LANGUAGE:C>:-Werror=pointer-to-int-cast>"
        "$<$<COMPILE_LANGUAGE:C>:-Werror=int-to-pointer-cast>")
    add_compile_options("$<$<COMPILE_LANGUAGE:C>:-fno-strict-aliasing>"
        "$<$<COMPILE_LANGUAGE:C>:-ffunction-sections>"
        "$<$<COMPILE_LANGUAGE:C>:-fdata-sections>")
    # POSIX walks frame chains; Windows uses its native unwind metadata.
    if(NOT WIN32)
        add_compile_options("$<$<COMPILE_LANGUAGE:C>:-fno-omit-frame-pointer>")
    endif()
elseif(MSVC)
    add_compile_options("$<$<COMPILE_LANGUAGE:C>:/Gy>" "$<$<COMPILE_LANGUAGE:C>:/Gw>")
endif()

# Whole original modules may contain entry points not yet called by each test.
# Keep function-level linking instead of splitting source files for test targets.
if(APPLE)
    add_link_options(-Wl,-dead_strip)
elseif(NOT MSVC)
    add_link_options(-Wl,--gc-sections)
else()
    add_link_options(/OPT:REF)
endif()

function(pe_configure_object_library target_name)
    target_include_directories(${target_name} PRIVATE include)
    if(MSVC)
        target_compile_options(${target_name} PRIVATE /Gy /Gw)
    else()
        target_compile_options(${target_name} PRIVATE -ffunction-sections -fdata-sections)
    endif()
endfunction()

# Original scene/archive module owns file-image I/O and completion flags.
add_library(parasite-eve-original-disc OBJECT src/main/cdrom/scene_assets.c)
pe_configure_object_library(parasite-eve-original-disc)
target_link_libraries(parasite-eve-original-disc PRIVATE psyz)

# Desktop keeps the original download implementation and dependencies by default.
set(pe_desktop_download_default ON)
if(PE_PS5_GAME)
    set(pe_desktop_download_default OFF)
endif()
option(PE_DESKTOP_MOD_DOWNLOADS "Build desktop HD pack download services" ${pe_desktop_download_default})
if(PE_PS5_GAME AND PE_DESKTOP_MOD_DOWNLOADS)
    message(FATAL_ERROR "Desktop HD downloads are unsupported on PS5")
endif()
add_library(pe_download_deps INTERFACE)
# cJSON and miniz are built in every configuration: screenshots (PNG, JSON metadata) and Wardrobe
# packs use them. Only libcurl, for HD pack downloads and webhook uploads, follows the option.
include(FetchContent)
FetchContent_Declare(pe_json URL https://github.com/DaveGamble/cJSON/archive/refs/tags/v1.7.19.tar.gz
    URL_HASH SHA256=7fa616e3046edfa7a28a32d5f9eacfd23f92900fe1f8ccd988c1662f30454562 SOURCE_SUBDIR pe-unused)
FetchContent_Declare(pe_zip URL https://github.com/richgel999/miniz/archive/refs/tags/3.1.1.tar.gz
    URL_HASH SHA256=8bb29c7bd6f22356e5583e794bed4a0b3e6dfcbcadb49974fc9270ccca1e5557 SOURCE_SUBDIR pe-unused)
FetchContent_MakeAvailable(pe_json pe_zip)
add_library(pe_download_json STATIC ${pe_json_SOURCE_DIR}/cJSON.c)
target_include_directories(pe_download_json PUBLIC ${pe_json_SOURCE_DIR})
file(WRITE ${pe_zip_BINARY_DIR}/miniz_export.h "#define MINIZ_EXPORT\n")
add_library(pe_download_zip STATIC ${pe_zip_SOURCE_DIR}/miniz.c ${pe_zip_SOURCE_DIR}/miniz_zip.c
    ${pe_zip_SOURCE_DIR}/miniz_tinfl.c ${pe_zip_SOURCE_DIR}/miniz_tdef.c)
target_include_directories(pe_download_zip PUBLIC ${pe_zip_SOURCE_DIR} ${pe_zip_BINARY_DIR})
target_compile_definitions(pe_download_zip PUBLIC MINIZ_NO_ZLIB_COMPATIBLE_NAMES)
# -std=c11 hides fseeko/ftello, which miniz needs; GCC 14 and newer reject the implicit declaration.
if(NOT WIN32)
    target_compile_definitions(pe_download_zip PRIVATE _POSIX_C_SOURCE=200809L)
endif()
target_link_libraries(pe_download_deps INTERFACE pe_download_json pe_download_zip)
if(PE_DESKTOP_MOD_DOWNLOADS)
    target_compile_definitions(pe_download_deps INTERFACE PE_DESKTOP_MOD_DOWNLOADS=1)
# Desktop mod downloads use native TLS through libcurl.
if(APPLE)
    target_link_libraries(pe_download_deps INTERFACE "-lcurl")
elseif(WIN32)
    set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE)
    set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
    set(CURL_USE_SCHANNEL ON CACHE BOOL "" FORCE)
    set(CURL_USE_OPENSSL OFF CACHE BOOL "" FORCE)
    set(CURL_USE_LIBPSL OFF CACHE BOOL "" FORCE)
    set(CURL_USE_LIBSSH2 OFF CACHE BOOL "" FORCE)
    set(USE_NGHTTP2 OFF CACHE BOOL "" FORCE)
    set(USE_LIBIDN2 OFF CACHE BOOL "" FORCE)
    set(CURL_USE_GSSAPI OFF CACHE BOOL "" FORCE)
    set(CURL_ZLIB OFF CACHE BOOL "" FORCE)
    set(CURL_BROTLI OFF CACHE BOOL "" FORCE)
    set(CURL_ZSTD OFF CACHE BOOL "" FORCE)
    set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE)
    set(HTTP_ONLY ON CACHE BOOL "" FORCE)
    FetchContent_Declare(pe_curl URL https://github.com/curl/curl/releases/download/curl-8_17_0/curl-8.17.0.tar.gz
        URL_HASH SHA256=e8e74cdeefe5fb78b3ae6e90cd542babf788fa9480029cfcee6fd9ced42b7910 EXCLUDE_FROM_ALL)
    FetchContent_MakeAvailable(pe_curl)
    target_link_libraries(pe_download_deps INTERFACE CURL::libcurl)
else()
    find_package(CURL REQUIRED)
    target_link_libraries(pe_download_deps INTERFACE CURL::libcurl)
endif()

else()
    target_compile_definitions(pe_download_deps INTERFACE PE_DESKTOP_MOD_DOWNLOADS=0)
endif()

# The executable starts the original game and compiles its modules in place.
add_executable(parasite-eve-pc src/main/boot/main.c src/main/boot/crash_report.c src/main/boot/crash_report_posix.c)
target_include_directories(parasite-eve-pc PRIVATE include)
target_link_libraries(parasite-eve-pc PRIVATE psyz pe_download_deps ${CMAKE_DL_LIBS})
set(PE_BUILD_ID "0.12-source")
execute_process(COMMAND git rev-parse --short=12 HEAD
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
    OUTPUT_VARIABLE PE_GIT_REVISION OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET RESULT_VARIABLE PE_GIT_RESULT)
if(PE_GIT_RESULT EQUAL 0)
    set(PE_BUILD_ID "${PE_GIT_REVISION}")
endif()
target_compile_definitions(parasite-eve-pc PRIVATE __psyz PE_VERSION="${PROJECT_VERSION}" PE_BUILD_ID="${PE_BUILD_ID}")
if(APPLE AND PE_MACOS_BUNDLE)
    set_target_properties(parasite-eve-pc PROPERTIES
        MACOSX_BUNDLE TRUE
        MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/packaging/macos/Info.plist"
        OUTPUT_NAME "Parasite Eve")
    target_sources(parasite-eve-pc PRIVATE packaging/macos/ParasiteEve.icns)
    set_source_files_properties(packaging/macos/ParasiteEve.icns PROPERTIES
        MACOSX_PACKAGE_LOCATION Resources)
    foreach(notice README.md CHANGELOG.md LICENSE.md THIRD_PARTY_NOTICES.md)
        target_sources(parasite-eve-pc PRIVATE ${notice})
        set_source_files_properties(${notice} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
    endforeach()
    foreach(notice_dir third_party/psyz third_party/psyz/decomp
            third_party/psyz/psyz/src/platform third_party/psyz/psyz/src/psyz
            third_party/psyz/external/SDL/src/video/yuv2rgb)
        target_sources(parasite-eve-pc PRIVATE ${notice_dir}/LICENSE)
        set_source_files_properties(${notice_dir}/LICENSE PROPERTIES
            MACOSX_PACKAGE_LOCATION "Resources/licenses/${notice_dir}")
    endforeach()
    target_sources(parasite-eve-pc PRIVATE third_party/psyz/external/SDL/LICENSE.txt
        third_party/psyz/external/SDL/src/hidapi/LICENSE-bsd.txt)
    set_source_files_properties(third_party/psyz/external/SDL/LICENSE.txt
        third_party/psyz/external/SDL/src/hidapi/LICENSE-bsd.txt PROPERTIES
        MACOSX_PACKAGE_LOCATION Resources/licenses/SDL)
    target_sources(parasite-eve-pc PRIVATE src/main/boot/fonts/OFL-IBMPlex.txt)
    set_source_files_properties(src/main/boot/fonts/OFL-IBMPlex.txt PROPERTIES
        MACOSX_PACKAGE_LOCATION Resources/licenses/IBM-Plex)
endif()
psyz_title(parasite-eve-pc "Parasite Eve")
psyz_web_configure_target(parasite-eve-pc)
psyz_ios_configure_target(parasite-eve-pc
    BUNDLE_IDENTIFIER "dev.parasiteeve.pc" DISPLAY_NAME "Parasite Eve")

# The original motion module has its own copies of the common class
# methods and immutable tables, just as the original room library did.
add_library(parasite-eve-original-motion OBJECT
    src/overlays/room_lib/motion_particles.c
    src/overlays/room_lib/motion_particles_2.c
    src/overlays/room_lib/motion_particles_3.c
    src/overlays/room_lib/class_plant_script.c
    src/overlays/room_lib/class_register_draw_list.c
    src/overlays/room_lib/class_register_tables_gated.c
    src/overlays/room_lib/class_nop6.c)
pe_configure_object_library(parasite-eve-original-motion)
target_link_libraries(parasite-eve-original-motion PRIVATE psyz)
target_compile_definitions(parasite-eve-original-motion PRIVATE PE_OVERLAY=motion)

# The original paired-glow module retains its own common methods/tables.
add_library(parasite-eve-original-paired OBJECT
    src/overlays/room_lib/paired_glow.c
    src/overlays/room_lib/class_plant_script.c
    src/overlays/room_lib/class_register_draw_list.c
    src/overlays/room_lib/class_register_tables.c)
pe_configure_object_library(parasite-eve-original-paired)
target_link_libraries(parasite-eve-original-paired PRIVATE psyz)
target_compile_definitions(parasite-eve-original-paired PRIVATE PE_OVERLAY=paired)

# Hospital falling-burst module, retaining its original common class methods.
add_library(parasite-eve-original-falling OBJECT
    src/overlays/room_lib/falling_sprite_burst.c
    src/overlays/room_lib/class_plant_script.c
    src/overlays/room_lib/class_register_draw_list.c
    src/overlays/room_lib/class_register_tables.c)
pe_configure_object_library(parasite-eve-original-falling)
target_link_libraries(parasite-eve-original-falling PRIVATE psyz)
target_compile_definitions(parasite-eve-original-falling PRIVATE PE_OVERLAY=falling)

# E19 retains its original scene-specific copy of the shared grab step.
add_library(parasite-eve-original-e19 OBJECT
    src/overlays/scene_e19_cruiser/e19.c
    src/overlays/scene_e19_cruiser/e19_class.c
    src/overlays/room_lib/grab_await_stance.c)
pe_configure_object_library(parasite-eve-original-e19)
target_link_libraries(parasite-eve-original-e19 PRIVATE psyz)
target_compile_definitions(parasite-eve-original-e19 PRIVATE PE_OVERLAY=scene_e19_cruiser)

# E08 keeps its original module files and scene-specific script planter.
add_library(parasite-eve-original-e08 OBJECT
    src/overlays/scene_e08_chrysler/e08.c
    src/overlays/scene_e08_chrysler/SceneE08_ModuleStart.c
    src/overlays/room_lib/class_plant_script.c)
pe_configure_object_library(parasite-eve-original-e08)
target_link_libraries(parasite-eve-original-e08 PRIVATE psyz)
target_compile_definitions(parasite-eve-original-e08 PRIVATE PE_OVERLAY=scene_e08_chrysler)

# Compile the original game sources in place. Modules join this target as
# their platform dependencies are made portable; no duplicate game logic.
add_library(parasite-eve-original OBJECT
    src/main/battle/encounter.c
    src/main/battle/setup.c
    src/main/battle/turn.c
    src/main/battle/enemy_actions.c
    src/main/battle/status_hud.c
    src/main/battle/attack.c
    src/main/battle/context.c
    src/main/battle/player_turn.c
    src/main/battle/phase.c
    src/main/battle/enemy_slots.c
    src/main/battle/enemy_movement.c
    src/main/entity/combat.c
    src/main/aya/stats.c
    src/main/item/active_list.c
    src/main/item/actions.c
    src/main/item/swap.c
    src/main/menu/skills.c
    src/main/menu/equip_screen.c
    src/main/menu/equip_select.c
    src/main/menu/item_actions.c
    src/main/menu/item_use_panel.c
    src/main/menu/main_menu.c
    src/main/menu/main_update.c
    src/main/time/counters.c
    src/main/psyq/libsnd/ssinit_c.c
    src/main/save/game_data.c
    src/main/memcard/save_flow.c
    src/main/menu/inventory.c
    src/main/item/armor_capacity.c
    src/main/render/font.c
    src/main/render/font_load.c
    src/main/menu/status_panels.c
    src/main/menu/save_screens.c
    src/main/menu/save_slot.c
    src/main/menu/lists.c
    src/main/menu/item_detail.c
    src/main/battle/commands.c
    src/main/text/tables.c
    src/main/asset/tim_textures.c
    src/main/asset/unload_table.c
    src/main/math/fixed_mul.c
    src/main/math/fixed_div.c
    src/main/math/fixed_round.c
    src/main/math/sqrt_approx.c
    src/main/fieldfx/angle.c
    src/main/psyq/libgte/reg03.c
    src/main/psyq/libgte/fog_02.c
    src/main/psyq/libgte/mtx_000.c
    src/main/psyq/libgte/mtx_001.c
    src/main/psyq/libgte/msc06.c
    src/main/psyq/libgte/mtx_06.c
    src/main/psyq/libgte/cmb_06.c
    src/main/anim/bone_rotations.c
    src/main/render/object.c
    src/main/render/wireframe.c
    src/main/render/frame_update.c
    src/main/entity/frame_update.c
    src/main/boot/init.c
    src/main/boot/frame_loop.c
    src/main/draw/pipeline.c
    src/main/draw/gpu_regs.c
    src/main/draw/prim_queue.c
    src/main/draw/prim_color.c
    src/main/draw/frame.c
    src/main/draw/text.c
    src/main/draw/text_render.c
    src/main/draw/textbox.c
    src/overlays/menu_memcard/list_screen.c
    src/overlays/menu_memcard/video_playback.c
    src/overlays/menu_memcard/video.c
    src/overlays/menu_memcard/glint_effects.c
    src/overlays/menu_memcard/image_effects.c
    src/overlays/mdec_diag/vlc_build.c
    src/overlays/mdec_diag/vlc_c.c
    src/overlays/menu_memcard/page_flip.c
    src/overlays/menu_memcard/image_nodes.c
    src/overlays/menu_memcard/fade.c
    src/main/draw/wipe.c
    src/main/draw/color_quad.c
    src/main/pm/disc_tables.c
    src/main/pm/process.c
    src/main/entity/pool.c
    src/main/entity/allocation.c
    src/main/script/node_pool.c
    src/main/script/sfx_queue.c
    src/main/psyq/libds/dssys_3.c
    src/main/script/node_actions.c
    src/main/script/actor_opcodes.c
    src/main/script/value_ops.c
    src/main/script/flow_opcodes.c
    src/main/script/system_opcodes.c
    src/overlays/fx_common/effects.c
    src/overlays/fx_common/effect_init.c
    src/overlays/fx_common/motion_endpoints.c
    src/overlays/fx_common/camera_path.c
    src/overlays/fx_common/camera.c
    src/overlays/fx_common/model_buffers.c
    src/overlays/boot_display/text_screen.c
    src/overlays/boot_display/playback_file.c
    src/overlays/boot_display/playback.c
    src/overlays/boot_display/playback_stop.c
    src/overlays/boot_display/image_display.c
    src/overlays/boot_display/sprites.c
    src/main/fieldfx/geometry.c
    src/main/fieldfx/model.c
    src/main/fieldfx/sprite_render.c
    src/main/fieldfx/sprite_effects.c
    src/main/fieldfx/emitters.c
    src/main/fieldfx/collision.c
    src/main/fieldfx/glow.c
    src/main/fieldfx/burst.c
    src/main/fieldfx/object_script.c
    src/main/fieldfx/task_list.c
    src/main/fieldfx/vector.c
    src/main/field/actor_collision.c
    src/overlays/room_lib/effect_sequence.c
    src/overlays/room_lib/class_nop0.c
    src/overlays/room_lib/class_plant_script.c
    src/overlays/room_lib/glow_orb.c
    src/overlays/room_lib/three_particles.c
    src/overlays/room_lib/drop_field.c
    src/overlays/room_lib/orbit_set.c
    src/overlays/room_lib/spark_barrage.c
    src/overlays/room_lib/script_arg_pair.c
    src/overlays/room_lib/glow_orb_setup.c
    src/overlays/room_lib/class_spawn.c
    src/overlays/room_lib/class_close_target.c
    src/overlays/room_lib/class_register_active.c
    src/overlays/room_lib/actor_classes.c
    src/overlays/room_lib/actor_class_table.c
    src/overlays/room_lib/window_class.c
    src/overlays/room_lib/window_class_table.c
    src/overlays/room_m272_liberty/m272.c
    src/overlays/scene_e18_cruiser/e18_emitters.c
    src/overlays/scene_e20_cruiser/SceneE20_CommandEffectSlot.c
    src/overlays/scene_e20_cruiser/SceneE20_FlareParticle.c
    src/overlays/scene_e20_cruiser/SceneE20_HoverOrbController.c
    src/overlays/scene_e22_liberty/e22.c
    src/overlays/room_m273_liberty/m273.c
    src/overlays/room_m273_liberty/m273_class.c
    src/overlays/room_m273_liberty/RoomM273_PulseSpriteCallback.c
    src/overlays/room_m273_liberty/RoomEffect_PhasedArcSpriteFlow.c
    src/overlays/room_lib/class_update_callback.c
    src/overlays/room_lib/grab_await_stance.c
    src/overlays/room_m256_museum/m256.c
    src/overlays/room_m256_museum/m256_class.c
    src/overlays/room_m318_chrysler/m318.c
    src/overlays/room_m318_chrysler/m318_player_gate.c
    src/overlays/room_m404_chrysler/m404.c
    src/overlays/room_m349_cruiser/m349.c
    src/overlays/room_m350_cruiser/m350.c
    src/overlays/room_m350_cruiser/m350_callbacks.c
    src/overlays/room_m350_cruiser/RoomEffect_ConfiguredPulseCallback.c
    src/overlays/room_m005_theater/m005.c
    src/overlays/room_m086_central_park/m086.c
    src/overlays/room_m089_central_park/m089.c
    src/overlays/room_m023_theater/m023.c
    src/overlays/room_m123_nypd/m123.c
    src/overlays/room_m123_nypd/RoomM123_SelectSlotPointer.c
    src/overlays/room_lib/spark_attacks.c
    src/overlays/room_lib/seeker_spark.c
    src/overlays/room_lib/capture_frame_sample.c
    src/overlays/room_lib/script_arg.c
    src/overlays/room_lib/script_task_var.c
    src/overlays/room_lib/orbit_flare.c
    src/overlays/room_lib/line_burst.c
    src/overlays/room_lib/script_slot.c
    src/overlays/room_lib/dropped_flare.c
    src/overlays/room_lib/ground_eruption.c
    src/overlays/room_lib/homing_projectile.c
    src/overlays/room_lib/homing_flash.c
    src/overlays/room_lib/twelve_elements.c
    src/overlays/room_lib/floor_walker_class.c
    src/overlays/room_lib/trail_burst_attacks.c
    src/overlays/room_lib/spark_ring_burst.c
    src/overlays/room_lib/sound_burst.c
    src/overlays/room_lib/grab_attack.c
    src/overlays/room_lib/timed_spawn.c
    src/overlays/room_lib/homing_projectile_seeds.c
    src/overlays/room_lib/script_args.c)
pe_configure_object_library(parasite-eve-original)
target_link_libraries(parasite-eve-original PRIVATE psyz)
set_property(SOURCE src/main/boot/init.c APPEND PROPERTY INCLUDE_DIRECTORIES
    "$<TARGET_PROPERTY:SDL3::SDL3,INTERFACE_INCLUDE_DIRECTORIES>")

set(PE_COMMON_GAME_OBJECTS
    $<TARGET_OBJECTS:parasite-eve-original>
    $<TARGET_OBJECTS:parasite-eve-original-motion>
    $<TARGET_OBJECTS:parasite-eve-original-paired>
    $<TARGET_OBJECTS:parasite-eve-original-falling>
    $<TARGET_OBJECTS:parasite-eve-original-e19>
    $<TARGET_OBJECTS:parasite-eve-original-e08>)
if(PE_MODS)
    add_subdirectory(mods)
    list(APPEND PE_COMMON_GAME_OBJECTS $<TARGET_OBJECTS:pe-mods>)
endif()
# What the shared game objects need beyond psyz: zipped .pelang files are read with the mod
# downloads' zip reader (mods/pe_lang/pe_lang_bundle.c).
set(PE_COMMON_GAME_LIBS)
if(PE_MODS AND TARGET pe_download_zip AND "pe_lang" IN_LIST PE_MOD_LIST)
    list(APPEND PE_COMMON_GAME_LIBS pe_download_zip)
endif()

target_sources(parasite-eve-pc PRIVATE ${PE_COMMON_GAME_OBJECTS})

# Original AKAO units compile in place while their SPU/timer dependencies
# are connected. The title frontend uses the original effect playback path.
add_library(parasite-eve-original-akao OBJECT
    src/main/cdrom/track_read.c
    src/main/akao/voice_driver.c
    src/main/akao/voice_bank.c
    src/main/akao/sequencer.c
    src/main/akao/spu_transfer.c
    src/main/psyq/libspu/s_svva.c)
pe_configure_object_library(parasite-eve-original-akao)
target_link_libraries(parasite-eve-original-akao PRIVATE psyz)

set(akao_runtime_targets parasite-eve-pc)
foreach(akao_target IN LISTS akao_runtime_targets)
    target_sources(${akao_target} PRIVATE $<TARGET_OBJECTS:parasite-eve-original-actor-animation>
        $<TARGET_OBJECTS:parasite-eve-original-akao>
        $<TARGET_OBJECTS:parasite-eve-original-shadow>
        $<TARGET_OBJECTS:parasite-eve-original-disc>)
    target_include_directories(${akao_target} PRIVATE include)
    if(MSVC)
        target_link_options(${akao_target} PRIVATE /OPT:REF)
    elseif(APPLE)
        target_link_options(${akao_target} PRIVATE -Wl,-dead_strip)
    else()
        target_link_options(${akao_target} PRIVATE -Wl,--gc-sections)
    endif()
endforeach()

function(pe_add_common_unit_test target_name test_name source_file)
    add_executable(${target_name} ${source_file} ${ARGN} ${PE_COMMON_GAME_OBJECTS})
    target_include_directories(${target_name} PRIVATE include)
    target_link_libraries(${target_name} PRIVATE psyz ${PE_COMMON_GAME_LIBS})
    add_test(NAME ${test_name} COMMAND ${target_name})
endfunction()

if(BUILD_TESTING)
    add_executable(test-skin-collision tests/pe_skin_collision_test.c)
    if(MSVC)
        target_compile_options(test-skin-collision PRIVATE /UNDEBUG)
    else()
        target_compile_options(test-skin-collision PRIVATE -UNDEBUG)
    endif()
    add_test(NAME wardrobe-skin-collision COMMAND test-skin-collision)

    add_executable(test-text-render tests/text_render_test.c src/main/draw/text_render.c)
    target_include_directories(test-text-render PRIVATE include)
    target_compile_definitions(test-text-render PRIVATE __psyz PE_MODS)
    target_link_libraries(test-text-render PRIVATE psyz)
    add_test(NAME text-render COMMAND test-text-render ${CMAKE_SOURCE_DIR}/tests/text_render_rtl_cases.txt)
    add_executable(test-menu-call-abi tests/original_menu_call_abi.c
        src/main/menu/lists.c src/main/menu/widget.c src/main/item/swap.c
        src/main/draw/text.c src/main/draw/text_render.c src/main/psyq/libds/dssys_3.c)
    target_include_directories(test-menu-call-abi PRIVATE include)
    target_compile_definitions(test-menu-call-abi PRIVATE __psyz PE_MODS)
    target_link_libraries(test-menu-call-abi PRIVATE psyz)
    if(MSVC)
        target_compile_options(test-menu-call-abi PRIVATE /UNDEBUG)
    else()
        target_compile_options(test-menu-call-abi PRIVATE -UNDEBUG)
    endif()
    if(MINGW)
        target_compile_options(test-menu-call-abi PRIVATE -fno-asynchronous-unwind-tables)
    endif()
    add_test(NAME original-menu-call-abi COMMAND test-menu-call-abi)
    if(NOT EMSCRIPTEN)
        add_executable(test-crash-report tests/original_input_rules.c
            src/main/boot/crash_report.c src/main/boot/crash_report_posix.c)
        target_include_directories(test-crash-report PRIVATE include)
        target_compile_definitions(test-crash-report PRIVATE PE_CRASH_REPORT_TEST=1)
        target_link_libraries(test-crash-report PRIVATE psyz)
        add_test(NAME host-crash-report COMMAND test-crash-report)
        set_tests_properties(host-crash-report PROPERTIES TIMEOUT 60)
    endif()
    add_executable(test-original-akao-layout tests/original_akao_layout.c)
    target_include_directories(test-original-akao-layout PRIVATE include)
    add_test(NAME original-game-akao-layout COMMAND test-original-akao-layout)

    pe_add_common_unit_test(test-original-math original-game-math tests/original_math.c)
    pe_add_common_unit_test(test-original-gte original-game-gte tests/original_gte.c)
    add_executable(test-psyz-gte-ownership
        ${PSYZ_DIR}/psyz/tests/test_main.c
        ${PSYZ_DIR}/psyz/tests/test_libgte.c
        ${PSYZ_DIR}/ztest/ztest.c)
    target_compile_definitions(test-psyz-gte-ownership PRIVATE __psyz)
    target_include_directories(test-psyz-gte-ownership PRIVATE ${PSYZ_DIR}/ztest)
    target_link_libraries(test-psyz-gte-ownership PRIVATE psyz)
    add_test(NAME original-game-gte-pgxp-ownership COMMAND test-psyz-gte-ownership
        --filter=gte_pgxp_store::* --output=plain)
    set_tests_properties(original-game-gte-pgxp-ownership PROPERTIES TIMEOUT 60
        FAIL_REGULAR_EXPRESSION "ztest: 0 passed|[1-9][0-9]* skipped"
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy;SDL_VIDEODRIVER=dummy")
    add_executable(test-psyz-cd-media
        ${PSYZ_DIR}/psyz/tests/test_main.c
        ${PSYZ_DIR}/psyz/tests/test_libcd.c
        ${PSYZ_DIR}/ztest/ztest.c)
    target_compile_definitions(test-psyz-cd-media PRIVATE __psyz PSYZ_TEST_CD_IO=1 _FILE_OFFSET_BITS=64)
    target_include_directories(test-psyz-cd-media PRIVATE ${PSYZ_DIR}/ztest)
    target_link_libraries(test-psyz-cd-media PRIVATE psyz)
    add_test(NAME original-game-cd-media COMMAND test-psyz-cd-media --output=plain)
    set_tests_properties(original-game-cd-media PROPERTIES TIMEOUT 60
        FAIL_REGULAR_EXPRESSION "ztest: 0 passed|[1-9][0-9]* skipped"
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy;SDL_VIDEODRIVER=dummy")
    pe_add_common_unit_test(test-original-animation original-game-animation tests/original_animation.c)
    pe_add_common_unit_test(test-original-transforms original-game-transforms tests/original_transforms.c)
    pe_add_common_unit_test(test-original-projection original-game-projection tests/original_projection.c)
    pe_add_common_unit_test(test-original-hierarchy original-game-hierarchy tests/original_hierarchy.c)

    add_executable(test-psyz-spu-reference
        ${PSYZ_DIR}/psyz/tests/test_main.c
        ${PSYZ_DIR}/psyz/tests/test_libspu.c
        ${PSYZ_DIR}/ztest/ztest.c)
    target_compile_definitions(test-psyz-spu-reference PRIVATE __psyz)
    target_include_directories(test-psyz-spu-reference PRIVATE ${PSYZ_DIR}/ztest)
    target_link_libraries(test-psyz-spu-reference PRIVATE psyz)
    add_test(NAME original-game-spu-hardware-reference COMMAND test-psyz-spu-reference
        --filter=spu::GaussianHardwareImpulse,spu::GaussianKeyOnPitchPhase,spu::GaussianFmReference,spu::AdpcmOneShotEndPipeline,spu::VoiceGainLatchReference,spu::ReverbHardwareFirImpulse
        --output=plain)
    set_tests_properties(original-game-spu-hardware-reference PROPERTIES TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy;SDL_VIDEODRIVER=dummy"
        LABELS "audio")

    pe_add_common_unit_test(test-original-input-rules original-game-input-rules tests/original_input_rules.c)

    add_executable(test-host-settings tests/original_input_rules.c ${PE_COMMON_GAME_OBJECTS})
    target_compile_definitions(test-host-settings PRIVATE PE_HOST_SETTINGS_TEST=1 __psyz)
    target_include_directories(test-host-settings PRIVATE include)
    target_link_libraries(test-host-settings PRIVATE psyz pe_download_deps)
    # The host links mods whose quick load stops movies and music, so it needs the same
    # AKAO, actor and disc objects as the game; unreachable ones are stripped as there.
    target_sources(test-host-settings PRIVATE $<TARGET_OBJECTS:parasite-eve-original-actor-animation>
        $<TARGET_OBJECTS:parasite-eve-original-akao>
        $<TARGET_OBJECTS:parasite-eve-original-shadow>
        $<TARGET_OBJECTS:parasite-eve-original-disc>
        $<TARGET_OBJECTS:parasite-eve-original-entity-textures>)
    add_test(NAME host-settings COMMAND test-host-settings "${CMAKE_CURRENT_BINARY_DIR}/host-settings-evidence")
    set_tests_properties(host-settings PROPERTIES TIMEOUT 30)

    pe_add_common_unit_test(test-original-pools original-game-pools tests/original_pools.c)
    pe_add_common_unit_test(test-original-nodes original-game-nodes tests/original_nodes.c)

    pe_add_common_unit_test(test-original-queue original-game-queue tests/original_queue.c
        $<TARGET_OBJECTS:parasite-eve-original-actor-animation>
        $<TARGET_OBJECTS:parasite-eve-original-disc>
        $<TARGET_OBJECTS:parasite-eve-original-akao>
        $<TARGET_OBJECTS:parasite-eve-original-camera>)

    pe_add_common_unit_test(test-original-expression original-game-expression tests/original_expression.c
        $<TARGET_OBJECTS:parasite-eve-original-camera>)

    pe_add_common_unit_test(test-original-contact original-game-contact tests/original_contact.c)

    pe_add_common_unit_test(test-original-movement original-game-movement tests/original_movement.c
        src/main/asset/game_state.c
        $<TARGET_OBJECTS:parasite-eve-original-disc>
        $<TARGET_OBJECTS:parasite-eve-original-akao>
        $<TARGET_OBJECTS:parasite-eve-original-camera>)
endif()

add_library(parasite-eve-original-actor-animation OBJECT
    src/main/asset/game_state.c src/main/scene/player_movement.c src/main/psyq/libgte/mtx_004.c)
pe_configure_object_library(parasite-eve-original-actor-animation)

if(BUILD_TESTING)
    add_executable(test-original-actor-animation tests/original_actor_animation.c
        $<TARGET_OBJECTS:parasite-eve-test-disc>
        $<TARGET_OBJECTS:parasite-eve-original-akao>
        $<TARGET_OBJECTS:parasite-eve-original-camera>
        ${PE_COMMON_GAME_OBJECTS}
        $<TARGET_OBJECTS:parasite-eve-original-actor-animation>)
    target_include_directories(test-original-actor-animation PRIVATE include tests)
    target_link_libraries(test-original-actor-animation PRIVATE psyz ${PE_COMMON_GAME_LIBS})
    add_test(NAME original-game-movie-early-exit COMMAND test-original-actor-animation --movie-early-exit)
    set_tests_properties(original-game-movie-early-exit PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 LABELS "game-assets;audio")
    add_test(NAME original-game-movie-audio-lock-order COMMAND test-original-actor-animation --movie-audio-lock-order)
    set_tests_properties(original-game-movie-audio-lock-order PROPERTIES TIMEOUT 20 LABELS "audio")
    add_test(NAME original-game-save-safety COMMAND test-original-actor-animation --save-safety)
    add_test(NAME original-game-actor-animation COMMAND test-original-actor-animation)
    add_test(NAME original-game-actor-distance COMMAND test-original-actor-animation --actor-distance)
    add_test(NAME original-game-animation-event-completion COMMAND test-original-actor-animation --anim-event-completion)
    add_test(NAME original-game-glint COMMAND test-original-actor-animation --glint)
    add_test(NAME original-game-fx-camera COMMAND test-original-actor-animation --fx-camera)
    add_test(NAME original-game-pulled-sprite COMMAND test-original-actor-animation --pulled-sprite)
    add_test(NAME original-game-fx-display COMMAND test-original-actor-animation --fx-display)
    add_test(NAME original-game-fx-effects COMMAND test-original-actor-animation --fx-effects)
    add_test(NAME original-game-fx-scene COMMAND test-original-actor-animation --fx-scene)
    set_tests_properties(original-game-fx-scene PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
    add_test(NAME original-game-fx-step COMMAND test-original-actor-animation --fx-step)
    set_tests_properties(original-game-fx-step PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
    add_test(NAME original-game-credits-text COMMAND test-original-actor-animation --credits-text)
    set_tests_properties(original-game-credits-text PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-credits-images COMMAND test-original-actor-animation --credits-images)
    set_tests_properties(original-game-credits-images PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets;gpu")
    add_test(NAME original-game-credits-sprites COMMAND test-original-actor-animation --credits-sprites)
    set_tests_properties(original-game-credits-sprites PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets;gpu")
    add_test(NAME original-game-credits-playback COMMAND test-original-actor-animation --credits-playback)
    set_tests_properties(original-game-credits-playback PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 90 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-texture-pages COMMAND test-original-actor-animation --texture-pages)
    set_tests_properties(original-game-texture-pages PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets;gpu")
    add_test(NAME original-game-hd-backgrounds COMMAND test-original-actor-animation --native-hd-backgrounds $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-hd-backgrounds PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180 LABELS "game-assets;gpu;hd-assets")
    find_package(Python3 QUIET COMPONENTS Interpreter)
    if(Python3_Interpreter_FOUND)
        if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang" AND NOT CMAKE_CROSSCOMPILING)
            add_test(NAME vsync-display-selection COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_SOURCE_DIR}/tests/vsync_display_selection.py
                ${CMAKE_SOURCE_DIR}/third_party/psyz/psyz/src/platform/sdl3_common.h
                ${CMAKE_C_COMPILER})
            set_tests_properties(vsync-display-selection PROPERTIES TIMEOUT 30)
        endif()
        add_test(NAME original-game-m273-reported-crash COMMAND ${Python3_EXECUTABLE}
            ${CMAKE_SOURCE_DIR}/tests/m273_crash_replay.py
            $<TARGET_FILE:parasite-eve-pc> ${CMAKE_BINARY_DIR}/m273-crash-evidence)
        set_tests_properties(original-game-m273-reported-crash PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 240 LABELS "game-assets;gpu;mods")
        add_test(NAME lighting-editor-assets COMMAND ${Python3_EXECUTABLE}
            ${CMAKE_SOURCE_DIR}/tools/lighting-editor/test_assets.py)
        set_tests_properties(lighting-editor-assets PROPERTIES TIMEOUT 90 SKIP_RETURN_CODE 77 LABELS "tools")
        if(PE_DESKTOP_MOD_DOWNLOADS)   # the HD pack downloader itself
            add_test(NAME hd-mod-download-safety COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_SOURCE_DIR}/tests/hd_mod_download.py $<TARGET_FILE:parasite-eve-pc>)
            set_tests_properties(hd-mod-download-safety PROPERTIES TIMEOUT 90 LABELS "downloads")
        endif()
        add_test(NAME original-game-frame-interpolation COMMAND ${Python3_EXECUTABLE}
            ${CMAKE_SOURCE_DIR}/tests/frame_interpolation.py $<TARGET_FILE:parasite-eve-pc>)
        set_tests_properties(original-game-frame-interpolation PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 420 LABELS "game-assets;gpu")
        if(PE_MODS AND "pe_lang" IN_LIST PE_MOD_LIST)
            # The text renderer with language packs made from the disc: stretches, name letters,
            # a saved name read back, and English as the game drew it before the renderer.
            add_test(NAME original-game-text-renderer COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_SOURCE_DIR}/tests/text_renderer_game.py $<TARGET_FILE:parasite-eve-pc>
                ${CMAKE_BINARY_DIR}/text-renderer-evidence)
            set_tests_properties(original-game-text-renderer PROPERTIES SKIP_RETURN_CODE 77
                TIMEOUT 900 LABELS "game-assets;mods")
            # A right-to-left battle line on screen: the recorded route into the first battle, then Escape.
            add_test(NAME original-game-text-renderer-battle COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_SOURCE_DIR}/tests/text_renderer_game.py $<TARGET_FILE:parasite-eve-pc>
                ${CMAKE_BINARY_DIR}/text-renderer-battle-evidence battle)
            set_tests_properties(original-game-text-renderer-battle PROPERTIES SKIP_RETURN_CODE 77
                TIMEOUT 600 LABELS "game-assets;mods")
            # The Chinese pack made from a fan patch, its letters the patch's own pictures, on screen
            # on Disc 1 and Disc 2. The pack stays out of the repository: PE_TEST_ZH_PACK names it,
            # PE_TEST_DISC2 adds Disc 2 (else skipped).
            add_test(NAME original-game-text-renderer-chinese COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_SOURCE_DIR}/tests/text_renderer_game.py $<TARGET_FILE:parasite-eve-pc>
                ${CMAKE_BINARY_DIR}/text-renderer-chinese-evidence chinese)
            set_tests_properties(original-game-text-renderer-chinese PROPERTIES SKIP_RETURN_CODE 77
                TIMEOUT 600 LABELS "game-assets;mods;localization")
            # The Spanish pack from the USA-disc fan patch on screen, on Disc 1 and Disc 2: its letters
            # (the patch's own pictures and widths), its name grid and its pictures. The pack stays out
            # of the repository: PE_TEST_ES_PACK names it, PE_TEST_DISC2 adds Disc 2 (else skipped).
            add_test(NAME original-game-text-renderer-spanish COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_SOURCE_DIR}/tests/text_renderer_game.py $<TARGET_FILE:parasite-eve-pc>
                ${CMAKE_BINARY_DIR}/text-renderer-spanish-evidence spanish)
            set_tests_properties(original-game-text-renderer-spanish PROPERTIES SKIP_RETURN_CODE 77
                TIMEOUT 600 LABELS "game-assets;mods;localization")
            # The same for the Italian pack from the USA-disc fan patch: its six accented letters in the
            # patch's pictures and widths, the rest the game's own, on Disc 1 and Disc 2. PE_TEST_IT_PACK
            # names it, PE_TEST_DISC2 adds Disc 2 (else skipped).
            add_test(NAME original-game-text-renderer-italian COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_SOURCE_DIR}/tests/text_renderer_game.py $<TARGET_FILE:parasite-eve-pc>
                ${CMAKE_BINARY_DIR}/text-renderer-italian-evidence italian)
            set_tests_properties(original-game-text-renderer-italian PROPERTIES SKIP_RETURN_CODE 77
                TIMEOUT 600 LABELS "game-assets;mods;localization")
            # The same for the German pack from the USA-disc fan patch: every letter in the patch's
            # pictures and widths, its name grid (ä ö ü Ä Ö Ü ß), its pictures, on Disc 1 and Disc 2.
            # PE_TEST_DE_PACK names it, PE_TEST_DISC2 adds Disc 2 (else skipped).
            add_test(NAME original-game-text-renderer-german COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_SOURCE_DIR}/tests/text_renderer_game.py $<TARGET_FILE:parasite-eve-pc>
                ${CMAKE_BINARY_DIR}/text-renderer-german-evidence german)
            set_tests_properties(original-game-text-renderer-german PROPERTIES SKIP_RETURN_CODE 77
                TIMEOUT 600 LABELS "game-assets;mods;localization")
            # The same for the French pack from the USA-disc fan patch: its own letters in the patch's
            # pictures and widths, the game's other letters as the game draws them, its name grid, its
            # pictures, and the menus' spaces the patch's 5 px. PE_TEST_FR_PACK names it, PE_TEST_DISC2
            # adds Disc 2 (else skipped).
            add_test(NAME original-game-text-renderer-french COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_SOURCE_DIR}/tests/text_renderer_game.py $<TARGET_FILE:parasite-eve-pc>
                ${CMAKE_BINARY_DIR}/text-renderer-french-evidence french)
            set_tests_properties(original-game-text-renderer-french PROPERTIES SKIP_RETURN_CODE 77
                TIMEOUT 600 LABELS "game-assets;mods;localization")
        endif()
        # A fan patch made for the Japanese discs read for the USA-disc port: two patches of the
        # test's own on PE_TEST_JA_DISC (Japanese Disc 1); PE_TEST_DISC adds the line-up check.
        add_test(NAME localization-japanese-patch-import COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_ja_patch_import.py)
        set_tests_properties(localization-japanese-patch-import PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 300 LABELS "game-assets;localization")
        # Naming a Chinese patch's letter pictures: letters drawn from the PC's Chinese font
        # (SimSun, or PE_TEST_CJK_FONT), no disc. Not isolated (-I): Pillow may be a user install.
        add_test(NAME localization-chinese-letter-naming COMMAND ${Python3_EXECUTABLE}
            ${CMAKE_SOURCE_DIR}/localization/tests/test_hanzi.py)
        set_tests_properties(localization-chinese-letter-naming PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 120 LABELS "localization")
        # A fan patch's letter pictures as a pack's Chinese letters, and the build options its pack uses.
        add_test(NAME localization-chinese-pack COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_cjk_pictures.py)
        set_tests_properties(localization-chinese-pack PROPERTIES TIMEOUT 120 LABELS "localization")
        # The Chinese pack's dialogue stays on screen wherever the English does, also on the pages no
        # script ties to a box. Needs PE_TEST_DISC (USA disc 1) and PE_TEST_ZH_PACK, else skipped.
        add_test(NAME localization-chinese-box-fit COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_zh_box_fit.py)
        set_tests_properties(localization-chinese-box-fit PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 180 LABELS "game-assets;localization")
        # The Chinese pack's pictures with words (title menu, map names) on the USA discs against the
        # patched Japanese game itself: the real patch laid over the Japanese disc, each picture the
        # pack carries byte for byte the patched one, every piece landing, nothing else changed.
        # Needs PE_TEST_ZH_PACK (the player file), PE_TEST_ZH_PATCH_DIR (the folder with the patch's
        # SLPS_012.30 and SLPS_012.31), PE_TEST_JA_DISC and PE_TEST_DISC (PE_TEST_JA_DISC2 and
        # PE_TEST_DISC2 add the second discs), else skipped.
        add_test(NAME localization-chinese-pictures COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_zh_pictures.py)
        set_tests_properties(localization-chinese-pictures PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 180 LABELS "game-assets;localization")
        # The same for the Spanish pack from the USA-disc fan patch, measured with the patch's own
        # letter widths the pack carries. Needs PE_TEST_DISC and PE_TEST_ES_PACK, else skipped.
        add_test(NAME localization-spanish-box-fit COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_es_box_fit.py)
        set_tests_properties(localization-spanish-box-fit PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 180 LABELS "game-assets;localization")
        # The Spanish pack on both USA discs: every picture piece lands on them, and Disc 2's
        # dialogue is Disc 1's (so the box-fit check covers it). Needs PE_TEST_DISC, PE_TEST_DISC2
        # and PE_TEST_ES_PACK, else skipped.
        add_test(NAME localization-spanish-both-discs COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_es_both_discs.py)
        set_tests_properties(localization-spanish-both-discs PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 240 LABELS "game-assets;localization")
        # The Italian pack from the USA-disc fan patch stays on screen wherever the English does.
        # Needs PE_TEST_DISC and PE_TEST_IT_PACK, else skipped.
        add_test(NAME localization-italian-box-fit COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_it_box_fit.py)
        set_tests_properties(localization-italian-box-fit PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 180 LABELS "game-assets;localization")
        # The Italian pack against the patched game itself: the real patch applied to a copy of each
        # disc; every row, the six accented letters and every picture piece as the patched PS1 game
        # has them. Needs PE_TEST_IT_PATCH, PE_TEST_IT_PACK and PE_TEST_DISC (PE_TEST_DISC2 adds
        # Disc 2), else skipped.
        add_test(NAME localization-italian-matches-patch COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_it_matches_patch.py)
        set_tests_properties(localization-italian-matches-patch PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 300 LABELS "game-assets;localization")
        # The German pack from the USA-disc fan patch stays on screen wherever the English does.
        # Needs PE_TEST_DISC and PE_TEST_DE_PACK, else skipped.
        add_test(NAME localization-german-box-fit COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_de_box_fit.py)
        set_tests_properties(localization-german-box-fit PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 180 LABELS "game-assets;localization")
        # The German pack against the patched game itself: the real patch applied to a copy of each
        # disc; every row (its letter pairs written out), every letter, every picture piece, and
        # nothing else changed but the text and two script boxes. Needs PE_TEST_DE_PATCH_DIR (the
        # folder with PE_GER100B_D1/_D2.xdelta), PE_TEST_DE_PACK and PE_TEST_DISC (PE_TEST_DISC2 adds
        # Disc 2), else skipped.
        add_test(NAME localization-german-matches-patch COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_de_matches_patch.py)
        set_tests_properties(localization-german-matches-patch PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 600 LABELS "game-assets;localization")
        # The French pack from the USA-disc fan patch stays on screen wherever the English does.
        # Needs PE_TEST_DISC and PE_TEST_FR_PACK, else skipped.
        add_test(NAME localization-french-box-fit COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_fr_box_fit.py)
        set_tests_properties(localization-french-box-fit PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 180 LABELS "game-assets;localization")
        # The French pack against the patched game itself: the real patch applied to a copy of each
        # disc; every row (its two-letter codes written out), every letter and pair, every picture
        # piece, and nothing else changed but the text. Needs PE_TEST_FR_PATCH_DIR (the folder with
        # PE_CD1.ppf and PE_CD2.ppf), PE_TEST_FR_PACK and PE_TEST_DISC (PE_TEST_DISC2 adds Disc 2),
        # else skipped.
        add_test(NAME localization-french-matches-patch COMMAND ${Python3_EXECUTABLE} -I
            ${CMAKE_SOURCE_DIR}/localization/tests/test_fr_matches_patch.py)
        set_tests_properties(localization-french-matches-patch PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 600 LABELS "game-assets;localization")
        add_test(NAME original-game-hd-hot-apply COMMAND ${Python3_EXECUTABLE}
            ${CMAKE_SOURCE_DIR}/tests/hd_hot_apply.py $<TARGET_FILE:parasite-eve-pc>)
        set_tests_properties(original-game-hd-hot-apply PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 240 LABELS "game-assets;gpu;hd-assets")
        add_test(NAME original-game-hd-widescreen COMMAND ${Python3_EXECUTABLE}
            ${CMAKE_SOURCE_DIR}/tests/hd_widescreen.py $<TARGET_FILE:parasite-eve-pc>)
        set_tests_properties(original-game-hd-widescreen PROPERTIES SKIP_RETURN_CODE 77
            TIMEOUT 240 LABELS "game-assets;gpu;hd-assets")
    endif()
    add_test(NAME original-game-live-graphics-controls COMMAND test-original-actor-animation --native-graphics-controls $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-live-graphics-controls PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180 LABELS "game-assets;gpu" ENVIRONMENT "SDL_AUDIO_DRIVER=dummy")
    add_test(NAME original-game-story-movie-skip COMMAND test-original-actor-animation --native-movie-skip $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-story-movie-skip PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 120 LABELS "game-assets;audio")
    add_test(NAME original-game-headless-control COMMAND test-original-actor-animation --native-control $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-headless-control PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 120 LABELS "game-assets")
    add_test(NAME original-game-spu-mix-headroom COMMAND test-original-actor-animation --spu-mix-headroom)
    set_tests_properties(original-game-spu-mix-headroom PROPERTIES TIMEOUT 30 LABELS "audio")
    add_test(NAME original-game-spu-pitch-modulation COMMAND test-original-actor-animation --spu-pitch-modulation)
    add_test(NAME original-game-spu-noise COMMAND test-original-actor-animation --spu-noise)
    set_tests_properties(original-game-spu-pitch-modulation original-game-spu-noise PROPERTIES TIMEOUT 30 LABELS "audio")
    add_test(NAME original-game-spu-live-adsr COMMAND test-original-actor-animation --spu-live-adsr)
    set_tests_properties(original-game-spu-live-adsr PROPERTIES TIMEOUT 30 LABELS "audio")
    add_test(NAME original-game-spu-register-serialization COMMAND test-original-actor-animation --spu-register-serialization)
    set_tests_properties(original-game-spu-register-serialization PROPERTIES TIMEOUT 30 LABELS "audio")
    add_test(NAME original-game-seeker-particles COMMAND test-original-actor-animation --seeker-particles)
    set_tests_properties(original-game-seeker-particles PROPERTIES TIMEOUT 30 LABELS "fieldfx")
    add_test(NAME original-game-callback-storage COMMAND test-original-actor-animation --callback-list-storage)
    set_tests_properties(original-game-callback-storage PROPERTIES TIMEOUT 30 LABELS "fieldfx")
    add_test(NAME original-game-tutorial COMMAND test-original-actor-animation --native-tutorial $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-tutorial PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 240 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-diagnostic-hp-arguments COMMAND test-original-actor-animation --native-diagnostic-hp-arguments $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-diagnostic-hp-arguments PROPERTIES TIMEOUT 30)
    add_test(NAME original-game-bootstrap-cli COMMAND parasite-eve-pc --help)
    add_test(NAME original-game-bootstrap-arguments COMMAND parasite-eve-pc --frame-limit invalid)
    set_tests_properties(original-game-bootstrap-arguments PROPERTIES WILL_FAIL TRUE)
    add_test(NAME original-game-bootstrap COMMAND test-original-actor-animation --native-bootstrap $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-bootstrap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180 LABELS "game-assets;gpu")
    add_test(NAME original-game-field-control COMMAND test-original-actor-animation --native-field-control $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-field-control PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 480 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-stage-transition COMMAND test-original-actor-animation --native-stage $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-stage-transition PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-battle-entry COMMAND test-original-actor-animation --native-battle-entry $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-battle-entry PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180 LABELS "game-assets;gpu;audio")
    add_test(NAME original-attack-effect-status-gate COMMAND test-original-actor-animation --attack-effect-status-gate)
    add_test(NAME original-hit-on-released-target COMMAND test-original-actor-animation --hit-on-released-target)
    add_test(NAME original-aya-step-released-target COMMAND test-original-actor-animation --aya-step-released-target)
    add_test(NAME original-missing-action-pose COMMAND test-original-actor-animation --missing-action-pose)
    add_test(NAME original-akao-queue-backpressure COMMAND test-original-actor-animation --akao-queue-backpressure)
    set_tests_properties(original-akao-queue-backpressure PROPERTIES TIMEOUT 30 LABELS "audio")
    add_test(NAME original-akao-bank-lifetime COMMAND test-original-actor-animation --akao-bank-lifetime)
    set_tests_properties(original-akao-bank-lifetime PROPERTIES TIMEOUT 30 LABELS "audio"
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy")
    add_test(NAME original-game-m123-ai COMMAND test-original-actor-animation --m123-ai)
    set_tests_properties(original-game-m123-ai PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m318 COMMAND test-original-actor-animation --m318)
    set_tests_properties(original-game-m318 PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m318-data COMMAND test-original-actor-animation --m318-data)
    set_tests_properties(original-game-m318-data PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m318-load COMMAND test-original-actor-animation --m318-load)
    set_tests_properties(original-game-m318-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m256 COMMAND test-original-actor-animation --m256)
    set_tests_properties(original-game-m256 PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m256-load COMMAND test-original-actor-animation --m256-load)
    set_tests_properties(original-game-m256-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m404 COMMAND test-original-actor-animation --m404)
    set_tests_properties(original-game-m404 PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m404-load COMMAND test-original-actor-animation --m404-load)
    set_tests_properties(original-game-m404-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m350-load COMMAND test-original-actor-animation --m350-load)
    set_tests_properties(original-game-m350-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m350 COMMAND test-original-actor-animation --m350)
    set_tests_properties(original-game-m350 PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m349-load COMMAND test-original-actor-animation --m349-load)
    set_tests_properties(original-game-m349-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-m349 COMMAND test-original-actor-animation --m349)
    set_tests_properties(original-game-m349 PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets")
    add_test(NAME original-game-melissa-encounter COMMAND test-original-actor-animation --native-melissa $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-melissa-encounter PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 240 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-backstage-transition COMMAND test-original-actor-animation --native-backstage $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-backstage-transition PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 300 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-hallway-transition COMMAND test-original-actor-animation --native-hallway $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-hallway-transition PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 300 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-rat-encounter COMMAND test-original-actor-animation --native-rat $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-rat-encounter PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 300 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-m020-items COMMAND test-original-actor-animation --native-m020-items $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-m020-items PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 360 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-m017-survivor COMMAND test-original-actor-animation --native-m017 $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-m017-survivor PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 420 LABELS "game-assets;gpu;audio")
    add_test(NAME original-tool-equipped-donor COMMAND test-original-actor-animation --tool-equipped-donor)
    add_test(NAME original-game-alligator-entry COMMAND test-original-actor-animation --native-alligator-entry $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-alligator-entry PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 900 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-m028-sewer COMMAND test-original-actor-animation --native-m028 $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-m028-sewer PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 720 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-m023-melissa COMMAND test-original-actor-animation --native-m023 $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-m023-melissa PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 600 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-m022-keys COMMAND test-original-actor-animation --native-m022 $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-m022-keys PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 480 LABELS "game-assets;gpu;audio")
    add_test(NAME original-game-carnegie-interaction-timing COMMAND test-original-actor-animation --native-carnegie-interaction-timing $<TARGET_FILE:parasite-eve-pc>)
    set_tests_properties(original-game-carnegie-interaction-timing PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 480 LABELS "game-assets;gpu;audio")
    foreach(variant IN ITEMS early late)
        if(variant STREQUAL "early")
            set(offset -3)
        else()
            set(offset 7)
        endif()
        add_test(NAME original-game-carnegie-interaction-timing-${variant} COMMAND test-original-actor-animation --native-carnegie-interaction-timing $<TARGET_FILE:parasite-eve-pc>)
        set_tests_properties(original-game-carnegie-interaction-timing-${variant} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 480 LABELS "game-assets;gpu;audio" ENVIRONMENT "PE_TEST_CARNEGIE_PRESS_OFFSET=${offset}")
    endforeach()
    foreach(side IN ITEMS left center right away)
        add_test(NAME original-game-carnegie-diary-${side} COMMAND test-original-actor-animation --native-carnegie-diary-range $<TARGET_FILE:parasite-eve-pc>)
        set_tests_properties(original-game-carnegie-diary-${side} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 480 LABELS "game-assets;gpu;audio" ENVIRONMENT "PE_TEST_CARNEGIE_DIARY_SIDE=${side}")
    endforeach()
    add_dependencies(test-original-actor-animation parasite-eve-pc)
    add_test(NAME original-game-fx-frames COMMAND test-original-actor-animation --fx-frames)
    set_tests_properties(original-game-fx-frames PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 120 LABELS "game-assets;gpu")
    add_test(NAME original-game-fx-motion COMMAND test-original-actor-animation --fx-motion)
    set_tests_properties(original-game-fx-motion PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
    add_test(NAME original-game-fx-models COMMAND test-original-actor-animation --fx-models)
    set_tests_properties(original-game-fx-models PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
    add_test(NAME original-m418-chrysler-slots COMMAND test-original-actor-animation --m418-slots-draw)
    set_tests_properties(original-m418-chrysler-slots PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets;room;fieldfx;render")
    add_test(NAME original-m418-chrysler-draw COMMAND test-original-actor-animation --m418-cycles-draw)
    set_tests_properties(original-m418-chrysler-draw PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets;room;fieldfx;render")
    add_test(NAME original-m418-chrysler-update COMMAND test-original-actor-animation --m418-cycles-update)
    set_tests_properties(original-m418-chrysler-update PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets;room;fieldfx")
    add_test(NAME original-m418-chrysler-load COMMAND test-original-actor-animation --m418-load)
    set_tests_properties(original-m418-chrysler-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "game-assets;room")
    add_test(NAME original-m411-chrysler-load COMMAND test-original-actor-animation --m411-load)
    add_test(NAME original-m412-chrysler-load COMMAND test-original-actor-animation --m412-load)
    add_test(NAME original-m422-chrysler-load COMMAND test-original-actor-animation --m422-load)
    add_test(NAME original-m420-chrysler-load COMMAND test-original-actor-animation --m420-load)
    add_test(NAME original-m424-chrysler-load COMMAND test-original-actor-animation --m424-load)
    set_tests_properties(original-m411-chrysler-load original-m412-chrysler-load original-m422-chrysler-load original-m420-chrysler-load original-m424-chrysler-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        LABELS "game-assets;room" ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy")
    add_test(NAME original-all-room-loads COMMAND test-original-actor-animation --all-room-loads)
    set_tests_properties(original-all-room-loads PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 120 LABELS "game-assets;room")
    add_test(NAME original-widescreen-rooms COMMAND test-original-actor-animation --widescreen-rooms)
    set_tests_properties(original-widescreen-rooms PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 600
        LABELS "game-assets;room" ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy")
    add_test(NAME original-widescreen-animations COMMAND test-original-actor-animation --widescreen-rooms)
    set_tests_properties(original-widescreen-animations PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 600
        LABELS "game-assets;room" ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy;PE_WS_ANIMATIONS=1")
    add_test(NAME original-m417-chrysler-load COMMAND test-original-actor-animation --m417-load)
    set_tests_properties(original-m417-chrysler-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        LABELS "game-assets;room" ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy")
    add_test(NAME original-m436-chrysler-load COMMAND test-original-actor-animation --m436-load)
    set_tests_properties(original-m436-chrysler-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        LABELS "game-assets;room" ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy")
    add_test(NAME original-m430-cruiser-load COMMAND test-original-actor-animation --m430-load)
    set_tests_properties(original-m430-cruiser-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        LABELS "game-assets;room" ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy")
    add_test(NAME original-callback-allocation-failure COMMAND test-original-actor-animation --callback-allocation-failure)
    add_test(NAME original-m273-landing-cycle COMMAND test-original-actor-animation --m273-landing-cycle)
    add_test(NAME original-m273-effect-pointers COMMAND test-original-actor-animation --m273-effect-pointers)
    add_test(NAME original-m273-attack-timing COMMAND test-original-actor-animation --m273-attack-timing)
    set_tests_properties(original-m273-attack-timing PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "game-assets;fieldfx")
    add_test(NAME original-m273-liberty-load COMMAND test-original-actor-animation --m273-load)
    add_test(NAME original-m273-action-poses COMMAND test-original-actor-animation --m273-action-poses)
    set_tests_properties(original-m273-action-poses PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "game-assets;battle")
    set_tests_properties(original-m273-liberty-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "game-assets;render")
    add_test(NAME original-m245-trail-burst-load COMMAND test-original-actor-animation --m245-load)
    set_tests_properties(original-m245-trail-burst-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "render")
    add_test(NAME original-m205-sound-burst-load COMMAND test-original-actor-animation --m205-load)
    set_tests_properties(original-m205-sound-burst-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "render")
    add_test(NAME original-m221-spark-ring-burst-load COMMAND test-original-actor-animation --m221-load)
    set_tests_properties(original-m221-spark-ring-burst-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "render")
    add_test(NAME original-m269-grab-load COMMAND test-original-actor-animation --m269-load)
    set_tests_properties(original-m269-grab-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "render")
    add_test(NAME original-m186-twelve-orbit-load COMMAND test-original-actor-animation --m186-load)
    set_tests_properties(original-m186-twelve-orbit-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "render")
    add_test(NAME original-m188-spark-barrage-load COMMAND test-original-actor-animation --m188-load)
    set_tests_properties(original-m188-spark-barrage-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "render")
    add_test(NAME original-m187-orbit-load COMMAND test-original-actor-animation --m187-load)
    set_tests_properties(original-m187-orbit-load PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
        ENVIRONMENT "PSYZ_HEADLESS=1;SDL_AUDIODRIVER=dummy" LABELS "render")
    add_test(NAME original-game-player-input-assets COMMAND test-original-actor-animation --assets)
    set_tests_properties(original-game-player-input-assets PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)

    pe_add_common_unit_test(test-original-floor original-game-floor tests/original_floor.c)
endif()

add_library(parasite-eve-original-camera OBJECT src/main/render/camera.c)
pe_configure_object_library(parasite-eve-original-camera)
target_sources(parasite-eve-pc PRIVATE $<TARGET_OBJECTS:parasite-eve-original-camera>)

if(BUILD_TESTING)
    pe_add_common_unit_test(test-original-camera original-game-camera tests/original_camera.c
        $<TARGET_OBJECTS:parasite-eve-original-disc>
        $<TARGET_OBJECTS:parasite-eve-original-camera>)
    set_tests_properties(original-game-camera PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
endif()

add_library(parasite-eve-original-pad OBJECT src/main/menu/widget.c src/main/menu/input_queue.c)
target_include_directories(parasite-eve-original-pad PRIVATE include)
target_link_libraries(parasite-eve-original-pad PRIVATE psyz)
foreach(pad_target IN LISTS akao_runtime_targets)
    target_sources(${pad_target} PRIVATE $<TARGET_OBJECTS:parasite-eve-original-pad>)
    if(NOT pad_target STREQUAL "parasite-eve-pc")
        target_sources(${pad_target} PRIVATE ${PE_COMMON_GAME_OBJECTS})
    endif()
endforeach()
if(BUILD_TESTING)
    target_sources(test-original-actor-animation PRIVATE $<TARGET_OBJECTS:parasite-eve-original-pad>)
    target_sources(test-original-input-rules PRIVATE $<TARGET_OBJECTS:parasite-eve-original-pad>)
    target_sources(test-host-settings PRIVATE $<TARGET_OBJECTS:parasite-eve-original-pad>
        $<TARGET_OBJECTS:parasite-eve-original-camera>
        $<TARGET_OBJECTS:parasite-eve-original-disc> $<TARGET_OBJECTS:parasite-eve-original-actor-animation>)

    pe_add_common_unit_test(test-original-actor-lifecycle original-game-actor-lifecycle tests/original_actor_lifecycle.c
        $<TARGET_OBJECTS:parasite-eve-original-akao>
        $<TARGET_OBJECTS:parasite-eve-original-camera>
        $<TARGET_OBJECTS:parasite-eve-original-disc>)
    if(MSVC)
        target_link_options(test-original-actor-lifecycle PRIVATE /OPT:REF)
    elseif(APPLE)
        target_link_options(test-original-actor-lifecycle PRIVATE -Wl,-dead_strip)
    else()
        target_compile_options(test-original-actor-lifecycle PRIVATE -ffunction-sections -fdata-sections)
        target_link_options(test-original-actor-lifecycle PRIVATE -Wl,--gc-sections)
    endif()
endif()

# Original area-bank state machine; native I/O binding is still pending.
add_library(parasite-eve-original-entity-textures OBJECT src/main/scene/room_load.c)
pe_configure_object_library(parasite-eve-original-entity-textures)
target_sources(parasite-eve-pc PRIVATE $<TARGET_OBJECTS:parasite-eve-original-entity-textures>)
if(BUILD_TESTING)
    target_sources(test-original-actor-animation PRIVATE $<TARGET_OBJECTS:parasite-eve-original-entity-textures>)

    # Compile the whole module; only this fixture replaces read/poll outcomes.
    # Rename its real I/O symbols to keep them distinct from the fixture doubles.
    add_library(parasite-eve-test-disc OBJECT src/main/cdrom/scene_assets.c)
    pe_configure_object_library(parasite-eve-test-disc)
    target_link_libraries(parasite-eve-test-disc PRIVATE psyz)
    target_compile_definitions(parasite-eve-test-disc PRIVATE
        CdRom_ReadSectorsFromLba=CdRom_ImageReadSectorsFromLba
        CdRom_PollReady=CdRom_ImagePollReady
        Asset_Find08w=Asset_ImageFind08w)
endif()

add_library(parasite-eve-original-shadow OBJECT
    src/overlays/room_lib/mirror_model.c src/main/render/mesh_list.c src/main/render/mesh_entry.c)
pe_configure_object_library(parasite-eve-original-shadow)
target_compile_definitions(parasite-eve-original-shadow PRIVATE __psyz)
target_link_libraries(parasite-eve-original-shadow PRIVATE psyz)

if(BUILD_TESTING)
    # Resident script commands can open the original menu and render transition.
    # Link whole existing modules for every fixture that uses the real table.
    foreach(script_target test-original-queue test-original-movement test-original-actor-animation test-original-camera test-host-settings)
        target_sources(${script_target} PRIVATE $<TARGET_OBJECTS:parasite-eve-original-shadow>)
    endforeach()
    foreach(script_target test-original-queue test-original-movement)
        target_sources(${script_target} PRIVATE $<TARGET_OBJECTS:parasite-eve-original-pad>
            $<TARGET_OBJECTS:parasite-eve-original-entity-textures>)
    endforeach()

    pe_add_common_unit_test(test-original-shadow original-game-shadow tests/original_shadow.c
        $<TARGET_OBJECTS:parasite-eve-original-shadow>)
    target_compile_definitions(test-original-shadow PRIVATE __psyz)
    if(PSYZ_RENDERER STREQUAL "sdl3_gpu")
        add_executable(test-psyz-interpolation
            third_party/psyz/psyz/tests/test_main.c
            third_party/psyz/psyz/tests/test_libgpu.c
            third_party/psyz/psyz/tests/frontbuffer_psyz.c
            third_party/psyz/ztest/ztest.c)
        target_include_directories(test-psyz-interpolation PRIVATE third_party/psyz/ztest)
        target_compile_definitions(test-psyz-interpolation PRIVATE __psyz PSYZ_TEST_SDL_GPU=1)
        target_link_libraries(test-psyz-interpolation PRIVATE psyz)
        add_test(NAME original-gpu-native-attributes COMMAND test-psyz-interpolation --filter=gpu::ps1_polygon_attribute_gradients)
        set_tests_properties(original-gpu-native-attributes PROPERTIES TIMEOUT 90 LABELS "gpu"
            WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/third_party/psyz/psyz/tests"
            ENVIRONMENT "PSYZ_HEADLESS=gpu;SDL_AUDIODRIVER=dummy;PSYZ_VSYNC_LIMITLESS=1")
        add_test(NAME original-gpu-native-coverage COMMAND test-psyz-interpolation --filter=gpu::native_polygon*)
        set_tests_properties(original-gpu-native-coverage PROPERTIES TIMEOUT 30 LABELS "gpu"
            WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/third_party/psyz/psyz/tests"
            FAIL_REGULAR_EXPRESSION "ztest: 0 passed|[1-9][0-9]* skipped"
            ENVIRONMENT "PSYZ_HEADLESS=gpu;SDL_AUDIO_DRIVER=dummy;PSYZ_VSYNC_LIMITLESS=1")
        add_test(NAME original-gpu-frame-interpolation COMMAND test-psyz-interpolation --filter=gpu::interpolated*)
        set_tests_properties(original-gpu-frame-interpolation PROPERTIES TIMEOUT 90 LABELS "gpu"
            WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/third_party/psyz/psyz/tests"
            SKIP_REGULAR_EXPRESSION "requires SDL GPU"
            ENVIRONMENT "PSYZ_HEADLESS=gpu;SDL_AUDIODRIVER=dummy;PSYZ_VSYNC_LIMITLESS=1")
        add_test(NAME original-gpu-hd-coverage COMMAND test-psyz-interpolation
            --filter=gpu::authored*,gpu::selective*)
        set_tests_properties(original-gpu-hd-coverage PROPERTIES
            FAIL_REGULAR_EXPRESSION "ztest: 0 passed|[1-9][0-9]* skipped")
        add_test(NAME original-gpu-hd-reference COMMAND test-psyz-interpolation
            --filter=gpu::queued_hd_sprite_preserves_native_vram_coverage_and_palette_fallback)
        add_test(NAME original-gpu-hd-extension COMMAND test-psyz-interpolation
            --filter=gpu::widescreen_hd_extension_is_display_only)
        set_tests_properties(original-gpu-hd-coverage original-gpu-hd-reference original-gpu-hd-extension PROPERTIES
            TIMEOUT 90 LABELS "gpu"
            WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/third_party/psyz/psyz/tests"
            SKIP_REGULAR_EXPRESSION "requires SDL GPU"
            ENVIRONMENT "PSYZ_HEADLESS=gpu;SDL_AUDIODRIVER=dummy;PSYZ_VSYNC_LIMITLESS=1")
        add_executable(test-gpu-resource-failures tests/gpu_resource_failures.c)
        target_link_libraries(test-gpu-resource-failures PRIVATE psyz)
        foreach(mode upload readback reset resize swapchain-resize pacing)
            add_test(NAME original-gpu-resource-${mode} COMMAND test-gpu-resource-failures --${mode})
            set_tests_properties(original-gpu-resource-${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60
                LABELS "gpu" ENVIRONMENT "PSYZ_HEADLESS=gpu;SDL_AUDIO_DRIVER=dummy")
        endforeach()
        add_test(NAME original-gpu-descriptor-batches COMMAND ${CMAKE_COMMAND}
            -DTEST_BINARY=$<TARGET_FILE:test-original-shadow>
            -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/descriptor-batches
            -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/gpu_descriptor_batches.cmake)
        set_tests_properties(original-gpu-descriptor-batches PROPERTIES TIMEOUT 180 LABELS "gpu")
        add_test(NAME original-widescreen-gpu-capture COMMAND test-original-shadow --widescreen-gpu)
        set_tests_properties(original-widescreen-gpu-capture PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60 LABELS "gpu"
            ENVIRONMENT "PSYZ_HEADLESS=gpu;SDL_AUDIODRIVER=dummy")
    endif()

    pe_add_common_unit_test(test-original-stat-assets original-game-stat-assets tests/original_stat_assets.c
        $<TARGET_OBJECTS:parasite-eve-original-disc>
        $<TARGET_OBJECTS:parasite-eve-original-shadow>
        $<TARGET_OBJECTS:parasite-eve-original-camera>
        $<TARGET_OBJECTS:parasite-eve-original-entity-textures>
        $<TARGET_OBJECTS:parasite-eve-original-pad>
        $<TARGET_OBJECTS:parasite-eve-original-actor-animation>
        $<TARGET_OBJECTS:parasite-eve-original-akao>)
    set_tests_properties(original-game-stat-assets PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)

    # Original-module integration runs without creating a window or GPU device.
    get_property(original_tests DIRECTORY PROPERTY TESTS)
    foreach(original_test IN LISTS original_tests)
        if(PE_ENABLE_COVERAGE)
            set_property(TEST ${original_test} APPEND PROPERTY ENVIRONMENT
                "LLVM_PROFILE_FILE=${CMAKE_CURRENT_BINARY_DIR}/profiles/${original_test}/%p.profraw")
            # Atomic pixel-loop counters are substantially slower than normal
            # execution; keep the same assertions with an instrumented budget.
            get_property(original_timeout TEST ${original_test} PROPERTY TIMEOUT)
            if(original_timeout)
                math(EXPR coverage_timeout "${original_timeout} * 4")
                set_property(TEST ${original_test} PROPERTY TIMEOUT ${coverage_timeout})
            endif()
        endif()
        if(original_test MATCHES "^original-game-")
            set_property(TEST ${original_test} APPEND PROPERTY ENVIRONMENT
                "PSYZ_HEADLESS=1" "SDL_AUDIODRIVER=dummy")
        endif()
    endforeach()

    if(MINGW AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
        # BFD keeps unused COFF functions alive through unwind records. Keep
        # the original fixture graph and its observer variants, compiling whole
        # original modules again for tests. Production objects retain unwind.
        function(pe_coff_fixture_module original_module output_module)
            set(fixture_module "${original_module}-coff-fixture")
            if(NOT TARGET ${fixture_module})
                get_target_property(fixture_files ${original_module} SOURCES)
                get_target_property(fixture_source_dir ${original_module} SOURCE_DIR)
                set(fixture_absolute_files)
                foreach(fixture_file IN LISTS fixture_files)
                    if(IS_ABSOLUTE "${fixture_file}" OR fixture_file MATCHES "^\\$<")
                        list(APPEND fixture_absolute_files "${fixture_file}")
                    else()
                        get_filename_component(fixture_absolute "${fixture_file}"
                            ABSOLUTE BASE_DIR "${fixture_source_dir}")
                        list(APPEND fixture_absolute_files "${fixture_absolute}")
                    endif()
                endforeach()
                add_library(${fixture_module} OBJECT ${fixture_absolute_files})
                foreach(fixture_property INCLUDE_DIRECTORIES COMPILE_DEFINITIONS COMPILE_OPTIONS LINK_LIBRARIES)
                    get_target_property(fixture_value ${original_module} ${fixture_property})
                    if(fixture_value)
                        set_property(TARGET ${fixture_module} PROPERTY ${fixture_property} "${fixture_value}")
                    endif()
                endforeach()
                target_compile_options(${fixture_module} PRIVATE
                    -fno-asynchronous-unwind-tables -fno-unwind-tables)
            endif()
            set(${output_module} ${fixture_module} PARENT_SCOPE)
        endfunction()
        get_property(native_test_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
        foreach(native_test IN LISTS native_test_targets)
            # The reporter must execute real hardware exception dispatch.
            # Stripping its unwind data makes Windows fail-fast before a report.
            if(native_test MATCHES "^test-" AND NOT native_test STREQUAL "test-crash-report")
                get_target_property(native_test_sources ${native_test} SOURCES)
                set(native_fixture_sources)
                foreach(native_source IN LISTS native_test_sources)
                    if(native_source MATCHES "^\\$<TARGET_OBJECTS:([^>]+)>$")
                        pe_coff_fixture_module(${CMAKE_MATCH_1} native_fixture_module)
                        list(APPEND native_fixture_sources "$<TARGET_OBJECTS:${native_fixture_module}>")
                    else()
                        list(APPEND native_fixture_sources ${native_source})
                    endif()
                endforeach()
                set_property(TARGET ${native_test} PROPERTY SOURCES ${native_fixture_sources})
                target_compile_options(${native_test} PRIVATE
                    -fno-asynchronous-unwind-tables -fno-unwind-tables)
            endif()
        endforeach()
    endif()
endif()

# Every program built from the mods' objects also needs what they link (the Wardrobe's character
# packs read zips and pack.json): object libraries don't pass their link libraries on.
if(PE_MODS)
    get_property(pe_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
    foreach(pe_target IN LISTS pe_targets)
        get_target_property(pe_type ${pe_target} TYPE)
        get_target_property(pe_sources ${pe_target} SOURCES)
        if(pe_type STREQUAL "EXECUTABLE" AND pe_sources AND "${pe_sources}" MATCHES "TARGET_OBJECTS:pe-mods")
            target_link_libraries(${pe_target} PRIVATE pe_download_json pe_download_zip)
        endif()
    endforeach()
endif()

if(PE_PS5_GAME)
    target_sources(parasite-eve-pc PRIVATE $<TARGET_OBJECTS:pe-ps5-sdl-core>)
    set_target_properties(parasite-eve-pc PROPERTIES OUTPUT_NAME parasite-eve-ps5 SUFFIX ".elf")
endif()
