cmake_minimum_required(VERSION 3.25)
# The tag is the version (scripts/version.sh, docs/PACKAGING.md).
execute_process(COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/scripts/version.sh OUTPUT_VARIABLE KF_RELEASE_VERSION
  OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE KF_VERSION_FAILED)
if(KF_VERSION_FAILED OR NOT KF_RELEASE_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$")
  message(FATAL_ERROR "scripts/version.sh said '${KF_RELEASE_VERSION}', not MAJOR.MINOR.PATCH")
endif()
project(kings_field VERSION ${KF_RELEASE_VERSION} LANGUAGES C CXX)
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
  set(KF_IOS ON)
  add_compile_definitions(KF_IOS=1)
  include(cmake/ios.cmake)
endif()
# A build for players, from the packaging: it tells the player about new releases.
option(KF_RELEASE "A build for players: asks GitHub for new releases" OFF)
# Verdite1 on the decomp. The layout follows holyland:
#   decomp/       sushi-shi's source port (our fork), a submodule pinned on `port`; never edited
#   patches/      Verdite's edits to its game code, one diff per copied directory (tools/patches.py)
#   port/         the platform layer, renderer and PC extensions: Verdite's own, edited in place
#   libverdite/   the game-agnostic C++ library the Verdite ports share, to become a subtree; knows no game
#   third_party/  imgui, lua, cjson, libchdr
# At configure time the decomp's game code (KF_COPIED) is copied, the patches are applied to the
# copy, and the copy (build/<preset>/decomp_src/) is compiled with port/.
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# compile_commands.json, for clang-tidy (scripts/lint.py)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>)
# On Android the game is a shared library (libmain.so), and every static library goes into it.
if(ANDROID)
  set(CMAKE_POSITION_INDEPENDENT_CODE ON)
  # Plain char is unsigned on arm64 and signed on the PS1, x86-64 and so the other players' machines.
  add_compile_options(-fsigned-char)
  # A crash report walks them (libverdite/src/crash.cpp): nothing else can in a signal handler here.
  add_compile_options(-fno-omit-frame-pointer)
endif()
option(KF_SANITIZE "Enable address and undefined behavior sanitizers" OFF)
# Crash reports name the shipped binary's stack from its own line tables (libverdite/crash.h).
if(CMAKE_BUILD_TYPE STREQUAL "Release")
  add_compile_options(-gline-tables-only)
endif()
if(KF_SANITIZE)
  add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
  add_link_options(-fsanitize=address,undefined)
endif()
# libstdc++'s checked operator[] and front/back on std::array, std::span and std::vector: an index
# past an array inside a struct stays inside the global, where ASan does not see it (fuzz preset).
option(KF_HARDEN "Check standard container bounds (_GLIBCXX_ASSERTIONS)" OFF)
if(KF_HARDEN)
  add_compile_definitions(_GLIBCXX_ASSERTIONS)
endif()
# libFuzzer targets (fuzz preset): everything carries its coverage, the targets link the fuzzer.
option(KF_LIBFUZZER "Build the libFuzzer targets (fuzz_*)" OFF)
if(KF_LIBFUZZER)
  add_compile_options(-fsanitize=fuzzer-no-link)
endif()

# ---- The decomp's game code, patched ------------------------------------------------------------
# From holyland: each configure builds the patched decomp in a staging directory, then copies into
# decomp_src/ only the files whose content changed, so a patch edit recompiles only what it touches.
# KF_COPIED must match COPIED in tools/patches.py.
set(KF_COPIED include/kf/audio include/kf/cutscene include/kf/game include/kf/lib
    src/audio src/cutscene src/game src/lib)
set(KF_DECOMP ${CMAKE_SOURCE_DIR}/decomp)
set(KF_STAGE ${CMAKE_BINARY_DIR}/decomp_stage)
set(KF_SRC ${CMAKE_BINARY_DIR}/decomp_src)
if(NOT EXISTS ${KF_DECOMP}/src/game)
  message(FATAL_ERROR "decomp/src/game is missing: run git submodule update --init")
endif()
find_package(Git REQUIRED)

file(GLOB_RECURSE KF_PATCHES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/patches/*.patch)
list(SORT KF_PATCHES)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${KF_PATCHES})
# moving the decomp pin re-runs configure: git's HEAD file of the submodule changes with it
execute_process(COMMAND ${GIT_EXECUTABLE} -C ${KF_DECOMP} rev-parse --git-path HEAD
  OUTPUT_VARIABLE KF_DECOMP_HEAD OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE rc)
if(rc EQUAL 0)
  get_filename_component(KF_DECOMP_HEAD ${KF_DECOMP_HEAD} ABSOLUTE BASE_DIR ${KF_DECOMP})
  if(EXISTS ${KF_DECOMP_HEAD})
    set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${KF_DECOMP_HEAD})
  endif()
endif()

# decomp/ must be at its pin, unedited: an edit there would be compiled without being in a patch
execute_process(COMMAND ${GIT_EXECUTABLE} -C ${KF_DECOMP} status --porcelain
  OUTPUT_VARIABLE KF_DECOMP_EDITS OUTPUT_STRIP_TRAILING_WHITESPACE
  ERROR_VARIABLE out RESULT_VARIABLE rc)
if(NOT rc EQUAL 0)
  message(FATAL_ERROR "git status failed in ${KF_DECOMP}:\n${out}")
endif()
if(NOT "${KF_DECOMP_EDITS}" STREQUAL "")
  message(FATAL_ERROR "decomp/ has local changes:\n${KF_DECOMP_EDITS}\n"
    "decomp/ must stay at its pin, unedited. Game-code edits go in patches/ (tools/patches.py).")
endif()

# the staging copy: the pinned decomp with the patches applied, rebuilt from scratch every configure
file(REMOVE_RECURSE ${KF_STAGE})
foreach(dir ${KF_COPIED})
  get_filename_component(parent ${KF_STAGE}/${dir} DIRECTORY)
  file(MAKE_DIRECTORY ${parent})
  file(COPY ${KF_DECOMP}/${dir} DESTINATION ${parent})
endforeach()
# its own repo, so that git apply works on paths relative to the copy's root (not this repo's)
execute_process(COMMAND ${GIT_EXECUTABLE} init -q WORKING_DIRECTORY ${KF_STAGE} RESULT_VARIABLE rc
  OUTPUT_VARIABLE out ERROR_VARIABLE out)
if(NOT rc EQUAL 0)
  message(FATAL_ERROR "git init failed in ${KF_STAGE}:\n${out}")
endif()
foreach(patch ${KF_PATCHES})
  execute_process(COMMAND ${GIT_EXECUTABLE} apply --whitespace=nowarn ${patch}
    WORKING_DIRECTORY ${KF_STAGE} RESULT_VARIABLE rc OUTPUT_VARIABLE out ERROR_VARIABLE out)
  if(NOT rc EQUAL 0)
    message(FATAL_ERROR "patch ${patch} does not apply to decomp/ at its pin:\n${out}")
  endif()
endforeach()
file(REMOVE_RECURSE ${KF_STAGE}/.git)

# sync the staging copy into decomp_src/. COPY_FILE leaves a file alone when it is already the
# same, so its timestamp, and the object built from it, survive. decomp_src/ is kept read-only
# between configures (see below), so make it writable for the sync.
if(EXISTS ${KF_SRC})
  file(CHMOD_RECURSE ${KF_SRC}
    FILE_PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ
    DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
endif()
file(GLOB_RECURSE KF_STAGED LIST_DIRECTORIES false RELATIVE ${KF_STAGE} ${KF_STAGE}/*)
foreach(rel ${KF_STAGED})
  get_filename_component(dir ${KF_SRC}/${rel} DIRECTORY)
  file(MAKE_DIRECTORY ${dir})
  file(COPY_FILE ${KF_STAGE}/${rel} ${KF_SRC}/${rel} ONLY_IF_DIFFERENT)
endforeach()
# what decomp_src/ has and the staging copy does not (a patch's removed file, an old folder) goes
file(GLOB_RECURSE KF_OLD LIST_DIRECTORIES true RELATIVE ${KF_SRC} ${KF_SRC}/*)
foreach(rel ${KF_OLD})
  if(NOT EXISTS ${KF_STAGE}/${rel})
    file(REMOVE_RECURSE ${KF_SRC}/${rel})
  endif()
endforeach()
file(REMOVE_RECURSE ${KF_STAGE})
# decomp_src/ is generated: compiler errors point into it, but an edit there is lost at the next
# configure. Its files are read-only so that the edit fails instead; edit work/ (tools/patches.py).
file(CHMOD_RECURSE ${KF_SRC}
  FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ
  DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)

# Verdite's headers first, then the patched decomp's: kf/platform and kf/renderer exist only in port/.
set(KF_INCLUDES ${CMAKE_SOURCE_DIR}/port/include ${KF_SRC}/include)

# ---- Libraries ----------------------------------------------------------------------------------
find_package(Python3 REQUIRED COMPONENTS Interpreter)
# The Nix dev shell supplies a delta derived from the translator's PPF release; otherwise a local
# english-v1.kfdelta in the build directory is used. The script is the decomp's, used as is.
if(DEFINED ENV{KF_ENGLISH_PATCH} AND NOT "$ENV{KF_ENGLISH_PATCH}" STREQUAL "")
  set(KF_ENGLISH_PATCH_DEFAULT "$ENV{KF_ENGLISH_PATCH}")
else()
  set(KF_ENGLISH_PATCH_DEFAULT "${CMAKE_BINARY_DIR}/english-v1.kfdelta")
endif()
set(KF_ENGLISH_PATCH "${KF_ENGLISH_PATCH_DEFAULT}" CACHE FILEPATH
    "English translation delta to embed (optional; never distribute builds containing it)")
# Checked on every build, so a payload added after configuring is picked up;
# the generated file is rewritten only when its content changes.
add_custom_target(kf_english_patch
  COMMAND ${Python3_EXECUTABLE} "${KF_DECOMP}/scripts/english_patch.py" embed
    --input "${KF_ENGLISH_PATCH}" --optional --output "${CMAKE_BINARY_DIR}/generated/english_patch.inc"
  BYPRODUCTS "${CMAKE_BINARY_DIR}/generated/english_patch.inc" VERBATIM)

# The game: every source in the copied game, cutscene and lib folders but the three kf_support takes.
file(GLOB KF_GAME_SOURCES CONFIGURE_DEPENDS
  ${KF_SRC}/src/game/*.cpp ${KF_SRC}/src/cutscene/*.cpp ${KF_SRC}/src/lib/*.cpp)
list(REMOVE_ITEM KF_GAME_SOURCES ${KF_SRC}/src/lib/fixed_math.cpp ${KF_SRC}/src/lib/random.cpp
  ${KF_SRC}/src/lib/resource_decode.cpp)
# The hook layer (docs/HOOK_LAYER_PLAN.md) compiles a copy of each, with its hook points' definitions
# renamed. tools/raw_layer.py writes them; #line keeps messages pointing into decomp_src/.
set(KF_HOOK_SRC ${CMAKE_BINARY_DIR}/hook_src)
list(JOIN KF_GAME_SOURCES "\n" KF_GAME_SOURCES_TEXT)
file(CONFIGURE OUTPUT ${CMAKE_BINARY_DIR}/generated/game_sources.txt CONTENT "@KF_GAME_SOURCES_TEXT@\n" @ONLY)
set(KF_DECLARED_GAME_SOURCES ${KF_GAME_SOURCES})
list(TRANSFORM KF_GAME_SOURCES REPLACE "^${KF_SRC}/" "${KF_HOOK_SRC}/")
set_source_files_properties(${KF_GAME_SOURCES} PROPERTIES GENERATED TRUE
  COMPILE_OPTIONS "-Wall;-Wextra;-fwrapv;-fno-strict-aliasing")

add_library(kf_support STATIC port/platform/files.cpp port/platform/disc.cpp port/platform/assets.cpp
  port/platform/user_folder.cpp port/platform/desktop_entry.cpp port/platform/picked_file.cpp
  port/platform/language.cpp port/platform/translation.cpp port/platform/translation_ppf.cpp
  port/platform/controls.cpp port/renderer/tim.cpp ${KF_SRC}/src/audio/codec.cpp ${KF_SRC}/src/lib/fixed_math.cpp
  ${KF_SRC}/src/lib/random.cpp ${KF_SRC}/src/lib/resource_decode.cpp)
target_sources(kf_support PRIVATE "${CMAKE_BINARY_DIR}/generated/english_patch.inc")
target_include_directories(kf_support PUBLIC ${KF_INCLUDES} PRIVATE "${CMAKE_BINARY_DIR}/generated")
add_dependencies(kf_support kf_english_patch)
target_compile_options(kf_support PRIVATE -Wall -Wextra -Wpedantic -Werror)
target_link_libraries(kf_support PRIVATE verdite SDL3::SDL3)
# Getting the English patch: a phone has no curl or bsdtar, and Windows' tar cannot be relied on
# (patch_archive.h).
if(KF_IOS)
  target_sources(kf_support PRIVATE port/platform/ios.mm port/platform/patch_archive_ios.cpp)
  set_source_files_properties(port/platform/ios.mm PROPERTIES COMPILE_OPTIONS "-fobjc-arc")
  target_link_libraries(kf_support PRIVATE "-framework UniformTypeIdentifiers")
elseif(NOT ANDROID)
  target_sources(kf_support PRIVATE port/platform/patch_archive_tools.cpp)
endif()

# Android builds its libraries from the sources the android nix shell names (KF_*_SOURCE), the same
# versions the desktop links; they would not be found as packages under the NDK's sysroot.
if(ANDROID)
  foreach(source KF_SDL3_SOURCE KF_VULKAN_HEADERS_SOURCE KF_VOLK_SOURCE KF_VMA_SOURCE KF_VK_BOOTSTRAP_SOURCE
      KF_CPPTRACE_SOURCE KF_LIBARCHIVE_SOURCE KF_LIBDATACHANNEL_SOURCE KF_OPENSSL_SOURCE)
    if(NOT DEFINED ENV{${source}})
      message(FATAL_ERROR "${source} is not set: configure Android in the android shell (nix develop .#android)")
    endif()
  endforeach()
endif()

if(ANDROID)
  set(SDL_SHARED ON CACHE BOOL "" FORCE)
  set(SDL_STATIC OFF CACHE BOOL "" FORCE)
  set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
  set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE)
  add_subdirectory($ENV{KF_SDL3_SOURCE} sdl3 EXCLUDE_FROM_ALL SYSTEM)
  # Named outright: nix's cmake does not look in /usr/include, even under the NDK's sysroot.
  set(ZLIB_INCLUDE_DIR ${CMAKE_SYSROOT}/usr/include CACHE PATH "" FORCE)
  target_sources(kf_support PRIVATE port/platform/patch_download_android.cpp)
elseif(KF_IOS)
  # SDL is built for the selected iOS SDK by cmake/ios.cmake.
elseif(WIN32)
  # Windows: the SDL3 MinGW development package is downloaded unless SDL3 is
  # already on CMAKE_PREFIX_PATH.
  find_package(SDL3 CONFIG QUIET)
  if(NOT SDL3_FOUND)
    set(KF_SDL3_VERSION 3.4.16)
    set(KF_SDL3_ROOT "${CMAKE_BINARY_DIR}/SDL3-${KF_SDL3_VERSION}")
    if(NOT EXISTS "${KF_SDL3_ROOT}")
      set(KF_SDL3_ARCHIVE "${CMAKE_BINARY_DIR}/SDL3-devel-${KF_SDL3_VERSION}-mingw.tar.gz")
      message(STATUS "Downloading SDL3 ${KF_SDL3_VERSION} (MinGW)")
      file(DOWNLOAD "https://github.com/libsdl-org/SDL/releases/download/release-${KF_SDL3_VERSION}/SDL3-devel-${KF_SDL3_VERSION}-mingw.tar.gz"
        "${KF_SDL3_ARCHIVE}" STATUS KF_SDL3_STATUS TLS_VERIFY ON)
      list(GET KF_SDL3_STATUS 0 KF_SDL3_CODE)
      if(NOT KF_SDL3_CODE EQUAL 0)
        message(FATAL_ERROR "Cannot download SDL3 (${KF_SDL3_STATUS}). Set CMAKE_PREFIX_PATH to an SDL3 MinGW package instead.")
      endif()
      file(ARCHIVE_EXTRACT INPUT "${KF_SDL3_ARCHIVE}" DESTINATION "${CMAKE_BINARY_DIR}")
    endif()
    find_package(SDL3 CONFIG REQUIRED PATHS "${KF_SDL3_ROOT}/x86_64-w64-mingw32" NO_DEFAULT_PATH)
  endif()
else()
  find_package(SDL3 CONFIG REQUIRED)
endif()

# The English patch's .rar and a .zip, read in memory: only the readers patch_unpack_libarchive.cpp
# names are linked. zlib, for the .zip, is the NDK's on Android and the build image's on Windows.
if(ANDROID OR WIN32)
  if(NOT DEFINED ENV{KF_LIBARCHIVE_SOURCE})
    message(FATAL_ERROR "KF_LIBARCHIVE_SOURCE is not set: it names libarchive's source, which unpacks the English patch")
  endif()
  foreach(off TEST TAR CPIO CAT UNZIP OPENSSL LIBB2 LZ4 LZO LZMA ZSTD BZip2 LIBXML2 EXPAT PCREPOSIX PCRE2POSIX
      LibGCC CNG MBEDTLS NETTLE ACL XATTR ICONV INSTALL)
    set(ENABLE_${off} OFF CACHE BOOL "" FORCE)
  endforeach()
  set(ENABLE_ZLIB ON CACHE BOOL "" FORCE)
  set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)   # its option defaults on, and libchdr reads it too
  add_subdirectory($ENV{KF_LIBARCHIVE_SOURCE} libarchive EXCLUDE_FROM_ALL SYSTEM)
  target_sources(kf_support PRIVATE port/platform/patch_unpack_libarchive.cpp)
  target_include_directories(kf_support PRIVATE $ENV{KF_LIBARCHIVE_SOURCE}/libarchive)
  target_compile_definitions(kf_support PRIVATE LIBARCHIVE_STATIC)
  target_link_libraries(kf_support PRIVATE archive_static)
endif()

# PC extensions: overlay (Dear ImGui), console, settings and Lua mods.
set(KF_IMGUI ${CMAKE_SOURCE_DIR}/third_party/imgui)
file(GLOB KF_LUA_SOURCES ${CMAKE_SOURCE_DIR}/third_party/lua/l*.c)
list(FILTER KF_LUA_SOURCES EXCLUDE REGEX "/(lua|luac|ltests|onelua)\\.c$")
# Lua is compiled as C++, so a Lua error unwinds C++ frames (an exception) instead of jumping past
# their destructors (longjmp); its headers are then included without extern "C".
set_source_files_properties(${KF_LUA_SOURCES} PROPERTIES LANGUAGE CXX)
add_library(lua54 STATIC ${KF_LUA_SOURCES})
if(KF_IOS)
  target_compile_options(lua54 PRIVATE -include "${CMAKE_SOURCE_DIR}/ios/lua_platform.h")
endif()
target_include_directories(lua54 PUBLIC ${CMAKE_SOURCE_DIR}/third_party/lua)
add_library(cjson STATIC third_party/cjson/cJSON.c)
target_include_directories(cjson PUBLIC ${CMAKE_SOURCE_DIR}/third_party/cjson)
# Dear ImGui's core and its SDL platform backend; each renderer brings its own renderer backend.
add_library(kf_imgui STATIC
  ${KF_IMGUI}/imgui.cpp ${KF_IMGUI}/imgui_draw.cpp ${KF_IMGUI}/imgui_tables.cpp ${KF_IMGUI}/imgui_widgets.cpp
  ${KF_IMGUI}/backends/imgui_impl_sdl3.cpp)
target_include_directories(kf_imgui PUBLIC ${KF_IMGUI} ${KF_IMGUI}/backends)
target_link_libraries(kf_imgui PUBLIC SDL3::SDL3)
# Online play (online/client) where libdatachannel's source is at hand: the nix shells. Without it,
# port/ext/online.cpp says the build has none.
set(KF_ONLINE OFF)
if(DEFINED ENV{KF_LIBDATACHANNEL_SOURCE})
  set(KF_ONLINE ON)
endif()
if(ANDROID)
  include(cmake/android_openssl.cmake)
endif()
# The modding API's raw layer (docs/RAW_LAYER_PLAN.md): tables of every global and field, generated
# from the patched headers and the listed file by clang's AST, so a pin move or a patch regenerates
# them. The host's clang parses, whatever compiles the target. The source is rewritten only when it
# changes (Ninja's restat on a byproduct), so most builds do not recompile it.
file(GLOB_RECURSE KF_RAW_HEADERS CONFIGURE_DEPENDS ${KF_SRC}/include/kf/*.h)
find_program(KF_HOST_CLANG NAMES clang++ REQUIRED)
set(KF_RAW_OUT ${CMAKE_BINARY_DIR}/generated)
set(KF_RAW_COMMANDS ${CMAKE_BINARY_DIR}/compile_commands.json)
if(KF_IOS AND CMAKE_GENERATOR STREQUAL "Xcode")
  # Xcode has no compilation database. The generator only needs the host parser's
  # include paths for the patched game headers, independent of the target SDK.
  set(KF_RAW_COMMANDS ${CMAKE_BINARY_DIR}/raw_compile_commands.json)
  file(GENERATE OUTPUT ${KF_RAW_COMMANDS} CONTENT
    "[{\"directory\":\"${CMAKE_SOURCE_DIR}\",\"file\":\"${CMAKE_SOURCE_DIR}/port/ext/mods.cpp\",\"arguments\":[\"${KF_HOST_CLANG}\",\"-std=c++20\",\"-I\",\"${CMAKE_SOURCE_DIR}/port/include\",\"-I\",\"${KF_SRC}/include\",\"-I\",\"${CMAKE_SOURCE_DIR}/libverdite/include\"]}]")
endif()
add_custom_command(OUTPUT ${KF_RAW_OUT}/raw_layer.stamp
  BYPRODUCTS ${KF_RAW_OUT}/raw_layer.cpp ${KF_RAW_OUT}/raw_layer_report.txt ${KF_GAME_SOURCES}
  COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/raw_layer.py --clang ${KF_HOST_CLANG}
    --compile-commands ${KF_RAW_COMMANDS} --decomp-src ${KF_SRC}
    --listed ${CMAKE_SOURCE_DIR}/port/ext/raw_layer.toml --calls-listed ${CMAKE_SOURCE_DIR}/port/ext/call_layer.toml
    --calls-audit ${CMAKE_SOURCE_DIR}/port/ext/call_layer_audit.toml
    --hooks-listed ${CMAKE_SOURCE_DIR}/port/ext/hook_layer.toml
    --game-sources ${KF_RAW_OUT}/game_sources.txt --hook-src ${KF_HOOK_SRC} --out ${KF_RAW_OUT}
  COMMAND ${CMAKE_COMMAND} -E touch ${KF_RAW_OUT}/raw_layer.stamp
  DEPENDS ${CMAKE_SOURCE_DIR}/tools/raw_layer.py ${CMAKE_SOURCE_DIR}/tools/call_layer.py
    ${CMAKE_SOURCE_DIR}/tools/hook_layer.py ${CMAKE_SOURCE_DIR}/port/ext/hook_layer.toml
    ${KF_RAW_OUT}/game_sources.txt ${KF_DECLARED_GAME_SOURCES}
    ${CMAKE_SOURCE_DIR}/port/ext/raw_layer.toml ${CMAKE_SOURCE_DIR}/port/ext/call_layer.toml
    ${CMAKE_SOURCE_DIR}/port/ext/call_layer_audit.toml ${KF_RAW_HEADERS}
  COMMENT "Generating the raw layer" VERBATIM)
add_custom_target(kf_raw_layer DEPENDS ${KF_RAW_OUT}/raw_layer.stamp)

add_library(kf_ext STATIC
  port/ext/console.cpp port/ext/settings.cpp port/ext/mods.cpp port/ext/overlay.cpp
  port/ext/item_text.cpp port/ext/discord.cpp port/ext/options.cpp port/ext/setting_rows.cpp port/ext/settings_page.cpp port/ext/settings_panel.cpp port/ext/setup.cpp port/ext/setup_overlay.cpp port/ext/setup_panel.cpp port/ext/update_notice.cpp port/ext/touch_overlay.cpp port/ext/online.cpp port/ext/online_pose.cpp port/ext/online_playback.cpp port/ext/online_stride.cpp port/ext/online_weapon_hold.cpp port/ext/online_player_model.cpp port/ext/online_sounds.cpp port/ext/coop.cpp port/ext/coop_party.cpp port/ext/coop_message.cpp port/ext/coop_floor_owners.cpp port/ext/coop_floor.cpp port/ext/coop_floor_frames.cpp port/ext/coop_floor_play.cpp port/ext/coop_hits.cpp port/ext/coop_death.cpp port/ext/coop_death_play.cpp port/ext/death_reload.cpp port/ext/death_reload_rule.cpp port/ext/coop_profile.cpp port/ext/coop_refusals.cpp port/ext/online_player_colours.cpp port/ext/title.cpp port/ext/message_reading.cpp port/ext/messages.cpp port/ext/messages_board.cpp port/ext/message_writer.cpp port/ext/mod_play.cpp port/ext/mod_draw.cpp port/ext/mod_menu.cpp port/ext/mod_automap.cpp port/ext/mod_assets.cpp port/ext/mod_audio.cpp port/ext/mod_audio_mix.cpp port/ext/mod_scene.cpp port/ext/mod_scene_shapes.cpp port/ext/mod_rules.cpp port/ext/mod_rule_values.cpp port/ext/call_checks.cpp port/ext/extra_items.cpp port/ext/mod_content.cpp port/ext/mod_content_save.cpp port/ext/mod_floor_check.cpp
  ${KF_RAW_OUT}/raw_layer.cpp)
add_dependencies(kf_ext kf_raw_layer)
set_source_files_properties(${KF_RAW_OUT}/raw_layer.cpp PROPERTIES GENERATED TRUE)
target_include_directories(kf_ext PUBLIC ${KF_INCLUDES} ${KF_IMGUI})
target_compile_definitions(kf_ext PUBLIC KF_EXT=1)
target_link_libraries(kf_ext PUBLIC kf_imgui SDL3::SDL3 verdite lua54 cjson)
target_compile_definitions(kf_ext PRIVATE KF_ONLINE=$<BOOL:${KF_ONLINE}> KF_VERSION="${PROJECT_VERSION}"
  KF_RELEASE=$<BOOL:${KF_RELEASE}>)
target_include_directories(kf_ext PRIVATE "${CMAKE_BINARY_DIR}/generated" ${CMAKE_SOURCE_DIR}/port/ext)
if(KF_ONLINE)
  target_link_libraries(kf_ext PRIVATE verdite_online)
  if(ANDROID)
    target_sources(kf_ext PRIVATE port/ext/online_android_roots.cpp)
  elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    target_sources(kf_ext PRIVATE port/ext/online_linux_roots.cpp)
  endif()
endif()

# libverdite: nothing from the game on its include path.
find_package(Threads REQUIRED)
# cpptrace names a crash report's stack (libverdite/src/crash*.cpp). The nix shell has it; Windows
# builds it from its release, with libdwarf, to read the line tables the exe carries.
if(KF_IOS)
  set(CPPTRACE_GET_SYMBOLS_WITH_LIBDL ON CACHE BOOL "" FORCE)
  set(CPPTRACE_UNWIND_WITH_UNWIND ON CACHE BOOL "" FORCE)
  set(CPPTRACE_DEMANGLE_WITH_CXXABI ON CACHE BOOL "" FORCE)
elseif(ANDROID)
  # No libdwarf on Android: symbols from the dynamic linker, the stack from the unwinder.
  set(CPPTRACE_GET_SYMBOLS_WITH_LIBDL ON CACHE BOOL "" FORCE)
  set(CPPTRACE_UNWIND_WITH_UNWIND ON CACHE BOOL "" FORCE)
  set(CPPTRACE_DEMANGLE_WITH_CXXABI ON CACHE BOOL "" FORCE)
  add_subdirectory($ENV{KF_CPPTRACE_SOURCE} cpptrace EXCLUDE_FROM_ALL SYSTEM)
  # It names exception types with typeid; the rest of the build has no RTTI.
  target_compile_options(cpptrace-lib PRIVATE -frtti)
else()
  find_package(cpptrace CONFIG QUIET)
endif()
if(NOT ANDROID AND NOT cpptrace_FOUND)
  include(FetchContent)
  FetchContent_Declare(cpptrace GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
    GIT_TAG v1.0.4 GIT_SHALLOW TRUE)
  FetchContent_MakeAvailable(cpptrace)
  # cpptrace's exception tracing uses typeid even though the game disables RTTI.
  target_compile_options(cpptrace-lib PRIVATE -frtti)
endif()
# libchdr reads CHD disc images (libverdite/src/disc_image.cpp): rtissera/libchdr 47d79415
# (2026-10-11), vendored unchanged without its tests, docs and contrib, plus the LICENSE its zstd
# copy left out; it carries its LZMA, miniz and zstd.
set(CHDR_WANT_TESTS OFF CACHE BOOL "libchdr's own tests" FORCE)
# Where cpptrace already links the system's zstd (the nix shell's libdwarf does), libchdr uses it
# too, through a `zstd` target it then does not build: its own copy's symbols would otherwise
# override the shared library's for libdwarf.
if(TARGET zstd::libzstd_shared)
  add_library(zstd INTERFACE)
  target_link_libraries(zstd INTERFACE zstd::libzstd_shared)
endif()
add_subdirectory(third_party/libchdr EXCLUDE_FROM_ALL SYSTEM)
if(TARGET zstd::libzstd_shared)
  target_compile_definitions(chdr-static PRIVATE CHDR_SYSTEM_ZSTD)
endif()
add_library(verdite STATIC libverdite/src/agent.cpp libverdite/src/crash.cpp libverdite/src/crash_helper.cpp
  libverdite/src/desktop_entry.cpp libverdite/src/disc_image.cpp libverdite/src/discord.cpp libverdite/src/fuzz.cpp libverdite/src/memory_card.cpp libverdite/src/update_check.cpp
  libverdite/src/menu_pointer.cpp libverdite/src/menu_settings.cpp libverdite/src/page_swipe.cpp libverdite/src/places.cpp libverdite/src/state_trace.cpp libverdite/src/stick.cpp
  libverdite/src/options.cpp libverdite/src/pickup_feed.cpp libverdite/src/setup.cpp libverdite/src/title_menu.cpp libverdite/src/touch.cpp
  libverdite/src/message_sample.cpp libverdite/src/content_ids.cpp libverdite/src/mod_value.cpp libverdite/src/mod_saved.cpp libverdite/src/mods.cpp libverdite/src/raw.cpp libverdite/src/raw_call.cpp libverdite/src/raw_hook.cpp libverdite/src/raw_leaf.cpp libverdite/src/wav.cpp)
target_include_directories(verdite PUBLIC libverdite/include)
target_link_libraries(verdite PUBLIC Threads::Threads lua54 PRIVATE cpptrace::cpptrace chdr-static cjson)
if(WIN32)
  target_link_libraries(verdite PUBLIC ws2_32)
endif()
target_compile_options(verdite PRIVATE -Wall -Wextra -Wpedantic -Werror)

# libverdite's tests, built when doctest is installed (the nix shell has it): ctest --preset linux
find_package(doctest CONFIG QUIET)
if(doctest_FOUND)
  enable_testing()
  add_executable(verdite_tests libverdite/tests/main.cpp libverdite/tests/agent.cpp libverdite/tests/crash.cpp
    libverdite/tests/desktop_entry.cpp libverdite/tests/disc_image.cpp libverdite/tests/discord.cpp libverdite/tests/fuzz.cpp libverdite/tests/memory_card.cpp libverdite/tests/update_check.cpp
    libverdite/tests/menu_pointer.cpp libverdite/tests/menu_settings.cpp libverdite/tests/page_swipe.cpp libverdite/tests/places.cpp libverdite/tests/state_trace.cpp libverdite/tests/stick.cpp
    libverdite/tests/options.cpp libverdite/tests/pickup_feed.cpp libverdite/tests/setup.cpp libverdite/tests/title_menu.cpp libverdite/tests/touch.cpp
    libverdite/tests/message_sample.cpp libverdite/tests/content_ids.cpp libverdite/tests/mod_saved.cpp libverdite/tests/mods.cpp libverdite/tests/raw.cpp libverdite/tests/raw_call.cpp libverdite/tests/raw_hook.cpp libverdite/tests/wav.cpp)
  target_link_libraries(verdite_tests PRIVATE verdite doctest::doctest)
  target_compile_definitions(verdite_tests PRIVATE VERDITE_TEST_DATA="${CMAKE_SOURCE_DIR}/libverdite/tests/data")
  target_compile_options(verdite_tests PRIVATE -Wall -Wextra -Wpedantic -Werror)
  add_test(NAME verdite COMMAND verdite_tests)
endif()

# verdite-online, the client library for online play (online/client)
if(KF_ONLINE)
  add_subdirectory(online/client)
endif()

# The renderer frontend's API-free parts: batching, the texture store and layers, smoothing. Tested on their own.
add_library(kf_frontend STATIC port/renderer/frame_batch.cpp port/renderer/texture_layers.cpp
  port/renderer/world_to_picture.cpp port/renderer/view_smoothing.cpp port/renderer/view_rotation.cpp port/renderer/vertex_carry.cpp port/renderer/model_transform.cpp
  port/renderer/lighting.cpp port/renderer/projection.cpp port/renderer/texture_mips.cpp port/renderer/light_sets.cpp
  port/renderer/textures.cpp)
target_link_libraries(kf_frontend PUBLIC kf_support)
target_compile_options(kf_frontend PRIVATE -Wall -Wextra -Wpedantic -Werror)

# The Vulkan backend: Vulkan 1.1 core, through volk, VMA and vk-bootstrap. Its GLSL shaders are
# compiled to SPIR-V and embedded as arrays (generated/shaders/<name>.h).
if(ANDROID)
  add_subdirectory($ENV{KF_VULKAN_HEADERS_SOURCE} vulkan-headers EXCLUDE_FROM_ALL SYSTEM)
  set(VOLK_PULL_IN_VULKAN OFF CACHE BOOL "" FORCE)
  add_subdirectory($ENV{KF_VOLK_SOURCE} volk EXCLUDE_FROM_ALL SYSTEM)
  target_link_libraries(volk PUBLIC Vulkan::Headers)
  add_subdirectory($ENV{KF_VMA_SOURCE} vma EXCLUDE_FROM_ALL SYSTEM)
  set(VK_BOOTSTRAP_TEST OFF CACHE BOOL "" FORCE)
  set(VK_BOOTSTRAP_INSTALL OFF CACHE BOOL "" FORCE)
  set(VK_BOOTSTRAP_POSITION_INDEPENDENT_CODE ON CACHE BOOL "" FORCE)
  add_subdirectory($ENV{KF_VK_BOOTSTRAP_SOURCE} vk-bootstrap EXCLUDE_FROM_ALL SYSTEM)
else()
  if(NOT KF_IOS)
    find_package(VulkanHeaders CONFIG REQUIRED)
    find_package(volk CONFIG REQUIRED)
  endif()
  if(APPLE)
    # Homebrew supplies the loader and shader tools; keep the two header/source
    # dependencies pinned so a Mac build needs no separate SDK checkout.
    include(FetchContent)
    FetchContent_Declare(VulkanMemoryAllocator SYSTEM
      GIT_REPOSITORY https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
      GIT_TAG 1d8f600fd424278486eade7ed3e877c99f0846b1)
    set(VK_BOOTSTRAP_TEST OFF CACHE BOOL "" FORCE)
    set(VK_BOOTSTRAP_INSTALL OFF CACHE BOOL "" FORCE)
    FetchContent_Declare(vk_bootstrap SYSTEM
      GIT_REPOSITORY https://github.com/charles-lunarg/vk-bootstrap.git
      GIT_TAG 522a579912cbeed7b52c948072d87ce001488ded)
    FetchContent_MakeAvailable(VulkanMemoryAllocator vk_bootstrap)
  else()
    find_package(VulkanMemoryAllocator CONFIG REQUIRED)
    find_package(vk-bootstrap CONFIG REQUIRED)
  endif()
endif()
find_program(KF_GLSLANG glslangValidator REQUIRED)
set(KF_SHADER_DIR ${CMAKE_SOURCE_DIR}/port/renderer/vulkan/shaders)
set(KF_SHADER_OUT ${CMAKE_BINARY_DIR}/generated/shaders)
set(KF_SHADER_HEADERS)
foreach(shader world.vert world.frag surface.frag fullscreen.vert downsample.frag occlusion.frag
    occlusion_blur.frag)
  string(REPLACE "." "_" symbol ${shader})
  add_custom_command(OUTPUT ${KF_SHADER_OUT}/${symbol}.h
    COMMAND ${CMAKE_COMMAND} -E make_directory ${KF_SHADER_OUT}
    COMMAND ${KF_GLSLANG} -V --target-env vulkan1.1 --vn ${symbol} -o ${KF_SHADER_OUT}/${symbol}.h ${KF_SHADER_DIR}/${shader}
    DEPENDS ${KF_SHADER_DIR}/${shader} ${KF_SHADER_DIR}/draw.glsl VERBATIM)
  list(APPEND KF_SHADER_HEADERS ${KF_SHADER_OUT}/${symbol}.h)
endforeach()
# third-party code: built with its own warnings, not ours
add_library(kf_vulkan_libraries STATIC port/renderer/vulkan/vulkan_libraries.cpp
  ${KF_IMGUI}/backends/imgui_impl_vulkan.cpp)
target_compile_definitions(kf_vulkan_libraries PUBLIC VK_NO_PROTOTYPES VMA_STATIC_VULKAN_FUNCTIONS=0
  VMA_DYNAMIC_VULKAN_FUNCTIONS=1 IMGUI_IMPL_VULKAN_USE_VOLK)
target_link_libraries(kf_vulkan_libraries PUBLIC Vulkan::Headers volk::volk GPUOpen::VulkanMemoryAllocator
  vk-bootstrap::vk-bootstrap kf_imgui)
add_library(kf_vulkan STATIC port/renderer/vulkan/vulkan_backend.cpp port/renderer/vulkan/vulkan_device.cpp
  port/renderer/vulkan/vulkan_ambient_occlusion.cpp port/renderer/vulkan/vulkan_downsample.cpp
  port/renderer/vulkan/vulkan_pipelines.cpp port/renderer/vulkan/vulkan_render_pass.cpp port/renderer/vulkan/vulkan_surfaces.cpp
  port/renderer/vulkan/vulkan_resources.cpp port/renderer/vulkan/vulkan_swapchain.cpp ${KF_SHADER_HEADERS})
target_include_directories(kf_vulkan PUBLIC ${KF_INCLUDES} PRIVATE ${KF_SHADER_OUT})
target_link_libraries(kf_vulkan PUBLIC kf_vulkan_libraries kf_frontend SDL3::SDL3)
target_compile_options(kf_vulkan PRIVATE -Wall -Wextra -Wpedantic -Werror)

add_library(kf_host STATIC port/platform/host.cpp port/platform/agent.cpp port/platform/fuzz.cpp port/platform/saves.cpp port/platform/language_runtime.cpp
  port/platform/automap.cpp port/platform/screen.cpp port/platform/analog.cpp port/platform/enemy_turn.cpp port/platform/menu_pointer.cpp port/platform/touch.cpp ${KF_SRC}/src/audio/sound.cpp port/renderer/frontend.cpp port/renderer/null_backend.cpp)
target_sources(kf_host PRIVATE port/platform/crash.cpp port/platform/logcat.cpp)
target_include_directories(kf_host PUBLIC ${KF_INCLUDES} PRIVATE "${CMAKE_BINARY_DIR}/generated")
target_link_libraries(kf_host PUBLIC kf_support kf_frontend kf_vulkan SDL3::SDL3 kf_ext verdite)
if(ANDROID)
  target_link_libraries(kf_host PUBLIC log)
endif()
# The build a crash report names: the version, the commit (checked on every build) and the type.
add_custom_target(kf_build_info
  COMMAND ${CMAKE_COMMAND} -DSOURCE=${CMAKE_SOURCE_DIR} -DOUTPUT=${CMAKE_BINARY_DIR}/generated/build_info.h
    -P ${CMAKE_SOURCE_DIR}/cmake/build_info.cmake
  BYPRODUCTS "${CMAKE_BINARY_DIR}/generated/build_info.h" VERBATIM)
add_dependencies(kf_host kf_build_info)
add_dependencies(kf_ext kf_build_info)   # online play names the build to its room
target_compile_definitions(kf_host PRIVATE KF_VERSION="${PROJECT_VERSION}" KF_BUILD_TYPE="${CMAKE_BUILD_TYPE}")
target_compile_options(kf_host PRIVATE -Wall -Wextra -Wpedantic -Werror)

if(ANDROID)
  # SDL's Java activity loads libSDL3.so and then libmain.so, and calls its SDL_main.
  add_library(verdite1 SHARED port/platform/entry.cpp ${KF_GAME_SOURCES})
  set_target_properties(verdite1 PROPERTIES OUTPUT_NAME main)
else()
  add_executable(verdite1 port/platform/entry.cpp ${KF_GAME_SOURCES})
endif()
target_link_libraries(verdite1 PRIVATE kf_host)
target_compile_definitions(verdite1 PRIVATE KF_VERSION="${PROJECT_VERSION}")
target_compile_options(verdite1 PRIVATE -Wall -Wextra)
if(KF_IOS)
  target_sources(verdite1 PRIVATE ios/Assets.xcassets)
  set_source_files_properties(ios/Assets.xcassets PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
  set_target_properties(verdite1 PROPERTIES
    MACOSX_BUNDLE TRUE OUTPUT_NAME Verdite1
    MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/ios/Info.plist.in"
    XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${KF_IOS_BUNDLE_ID}"
    XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
    XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon
    XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Automatic"
    XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${KF_IOS_TEAM}"
    XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
    XCODE_EMBED_FRAMEWORKS "${KF_MOLTENVK_FRAMEWORK}"
    XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY YES
    XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY YES)
  target_link_libraries(verdite1 PRIVATE "${KF_MOLTENVK_FRAMEWORK}")
endif()
if(NOT ANDROID)
  # The example mods beside the executable, where the game offers them to the user's mods folder.
  add_custom_command(TARGET verdite1 POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E rm -rf $<TARGET_FILE_DIR:verdite1>/mods
    COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/mods $<TARGET_FILE_DIR:verdite1>/mods
    VERBATIM)
endif()
if(ANDROID)
  # The libraries the APK carries, where android/app/build.gradle picks them up: jni/<abi>/.
  set(KF_JNI_DIR ${CMAKE_BINARY_DIR}/jni/${ANDROID_ABI})
  # The NDK keeps each ABI's C++ runtime under its target triple.
  set(KF_TRIPLE_arm64-v8a aarch64-linux-android)
  set(KF_TRIPLE_x86_64 x86_64-linux-android)
  set(KF_CXX_RUNTIME ${ANDROID_TOOLCHAIN_ROOT}/sysroot/usr/lib/${KF_TRIPLE_${ANDROID_ABI}}/libc++_shared.so)
  add_custom_command(TARGET verdite1 POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E make_directory ${KF_JNI_DIR}
    COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:verdite1> $<TARGET_FILE:SDL3::SDL3-shared>
      ${KF_CXX_RUNTIME} ${KF_JNI_DIR}
    VERBATIM)
endif()
if(WIN32)
  # Static C++ runtime: the game folder needs only SDL3.dll beside the exe.
  target_link_options(verdite1 PRIVATE -static)
  # A windowed app (no console window); output goes to kf.log and the F1 console.
  option(KF_WINDOWED "Build without a console window" ON)
  if(KF_WINDOWED)
    target_link_options(verdite1 PRIVATE -mwindows)
  endif()
  add_custom_command(TARGET verdite1 POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:SDL3::SDL3> $<TARGET_FILE_DIR:verdite1>)
endif()
install(TARGETS verdite1 RUNTIME DESTINATION bin BUNDLE DESTINATION .)

# libFuzzer targets, for what comes from outside the game (docs/FUZZING_PLAN.md; run them with
# python3 scripts/fuzz.py --targets). fuzz_codecs is the decomp's own, built against our patches.
if(KF_LIBFUZZER)
  add_executable(fuzz_memory_card libverdite/tests/fuzz/memory_card.cpp)
  target_link_libraries(fuzz_memory_card PRIVATE verdite)
  add_executable(fuzz_disc_image libverdite/tests/fuzz/disc_image.cpp)
  target_link_libraries(fuzz_disc_image PRIVATE verdite)
  add_executable(fuzz_wav libverdite/tests/fuzz/wav.cpp)
  target_link_libraries(fuzz_wav PRIVATE verdite)
  add_executable(fuzz_codecs ${KF_DECOMP}/tests/fuzz_codecs.cpp)
  target_link_libraries(fuzz_codecs PRIVATE kf_support verdite SDL3::SDL3)
  add_executable(fuzz_save_card port/tests/fuzz/save_card.cpp ${KF_GAME_SOURCES})
  target_link_libraries(fuzz_save_card PRIVATE kf_host)
  # What other players and the mesh send (docs/FUZZING_PLAN.md): the decoders alone, and the game
  # state a room's players send put into the game and run.
  add_executable(fuzz_coop_message port/tests/fuzz/coop_message.cpp port/ext/coop_message.cpp
    port/ext/coop_party.cpp port/ext/coop_profile.cpp port/ext/coop_floor_owners.cpp port/ext/coop_floor_frames.cpp
    port/ext/online_pose.cpp port/ext/messages.cpp port/ext/message_reading.cpp port/ext/online_player_colours.cpp
    port/ext/online_player_model.cpp)
  target_link_libraries(fuzz_coop_message PRIVATE kf_frontend)
  add_executable(fuzz_coop_floor port/tests/fuzz/coop_floor.cpp ${KF_GAME_SOURCES})
  target_link_libraries(fuzz_coop_floor PRIVATE kf_host)
  # What a mod's floor and a save's side file hold (docs/CONTENT_LAYER_PLAN.md).
  add_executable(fuzz_mod_floor port/tests/fuzz/mod_floor.cpp port/ext/mod_floor_check.cpp port/ext/mod_assets.cpp port/ext/mod_content_save.cpp)
  target_link_libraries(fuzz_mod_floor PRIVATE kf_frontend verdite)
  foreach(target fuzz_memory_card fuzz_disc_image fuzz_wav fuzz_codecs fuzz_save_card fuzz_coop_message fuzz_coop_floor fuzz_mod_floor)
    target_link_options(${target} PRIVATE -fsanitize=fuzzer)
  endforeach()
endif()

# port/'s tests: the parts that run without a window or GPU.
if(doctest_FOUND)
  add_executable(kf_port_tests port/tests/main.cpp port/tests/renderer.cpp port/tests/enemy_turn.cpp port/platform/enemy_turn.cpp port/tests/death_reload.cpp port/ext/death_reload_rule.cpp
    port/tests/setting_rows.cpp port/ext/setting_rows.cpp
    port/tests/online_pose.cpp port/ext/online_pose.cpp port/tests/online_playback.cpp port/ext/online_playback.cpp
    port/tests/online_stride.cpp port/ext/online_stride.cpp port/tests/online_weapon_hold.cpp port/ext/online_weapon_hold.cpp
    port/tests/coop_party.cpp port/ext/coop_party.cpp port/tests/coop_refusals.cpp port/ext/coop_refusals.cpp
    port/tests/coop_message.cpp port/ext/coop_message.cpp port/tests/coop_floor_owners.cpp port/ext/coop_floor_owners.cpp
    port/tests/coop_floor_frames.cpp port/ext/coop_floor_frames.cpp port/tests/coop_death.cpp port/ext/coop_death.cpp
    port/tests/online_name_bar.cpp port/tests/online_compass.cpp port/tests/online_watched_weapon.cpp port/tests/coop_profile.cpp port/ext/coop_profile.cpp port/tests/online_player_colours.cpp port/ext/online_player_colours.cpp
    port/ext/online_player_model.cpp port/tests/messages.cpp port/ext/messages.cpp
    port/tests/message_reading.cpp port/ext/message_reading.cpp
    port/tests/message_writer.cpp port/ext/message_writer.cpp
    port/tests/mod_play.cpp port/ext/mod_play.cpp port/tests/mod_rule_values.cpp port/ext/mod_rule_values.cpp
    port/tests/mod_assets.cpp port/ext/mod_assets.cpp port/tests/mod_content_save.cpp port/ext/mod_content_save.cpp port/tests/mod_floor_check.cpp port/ext/mod_floor_check.cpp port/tests/mod_audio_mix.cpp port/ext/mod_audio_mix.cpp
    port/tests/mod_scene_shapes.cpp port/ext/mod_scene_shapes.cpp port/tests/world_to_picture.cpp)
  target_link_libraries(kf_port_tests PRIVATE kf_frontend verdite doctest::doctest)
  target_compile_options(kf_port_tests PRIVATE -Wall -Wextra -Wpedantic -Werror)
  add_test(NAME port COMMAND kf_port_tests)
  # The unpacker Android uses, tested here when the nix shell's libarchive is at hand.
  find_package(LibArchive QUIET)
  if(LibArchive_FOUND)
    target_sources(kf_port_tests PRIVATE port/tests/patch_archive.cpp port/platform/patch_unpack_libarchive.cpp)
    target_link_libraries(kf_port_tests PRIVATE LibArchive::LibArchive)
  endif()
  # The Vulkan backend on a real GPU, offscreen, validation layers on: any validation error fails it.
  # It loads the host's Vulkan loader and drivers, as scripts/run-linux.sh does; 77: no device.
  add_executable(kf_vulkan_tests port/tests/vulkan.cpp)
  target_link_libraries(kf_vulkan_tests PRIVATE kf_vulkan kf_frontend kf_support doctest::doctest)
  target_include_directories(kf_vulkan_tests PRIVATE port/renderer/vulkan)
  target_compile_options(kf_vulkan_tests PRIVATE -Wall -Wextra -Wpedantic -Werror)
  set(KF_HOST_LIBRARY_PATH "/usr/lib64" CACHE PATH "Where the host's libvulkan.so.1 is, for the Vulkan tests")
  foreach(device gpu llvmpipe)
    add_test(NAME vulkan_${device} COMMAND kf_vulkan_tests)
    # host layers that insert themselves (frame generators, overlays) stay out of the test
    # No leak detection here (code-style.md): the host drivers' own allocations are reported from
    # modules already unloaded. Vulkan objects we leak are validation errors, which fail the test.
    set(environment KF_VK_VALIDATION=1 LD_LIBRARY_PATH=${KF_HOST_LIBRARY_PATH} VK_LOADER_LAYERS_DISABLE=~implicit~
      ASAN_OPTIONS=detect_leaks=0)
    if(device STREQUAL llvmpipe)
      list(APPEND environment KF_VK_DEVICE=llvmpipe)
    endif()
    set_tests_properties(vulkan_${device} PROPERTIES ENVIRONMENT "${environment}" SKIP_RETURN_CODE 77
      FAIL_REGULAR_EXPRESSION "\\[vulkan\\] error")
  endforeach()
# The check a mod's .TMD replacement passes (port/ext/mod_assets.cpp), run on files:
# scripts/fuzz.py --corrupt-assets runs its mutants through it. Not a test.
add_executable(kf_asset_check port/tests/asset_check.cpp port/ext/mod_assets.cpp)
target_link_libraries(kf_asset_check PRIVATE kf_frontend verdite)
target_compile_options(kf_asset_check PRIVATE -Wall -Wextra -Wpedantic -Werror)
endif()
