cmake_minimum_required(VERSION 3.20)
project(pes2017recomp CXX)

# ARMv8.2-A baseline for the phone build: without it the NDK emits calls to the
# outline-atomics helpers for every guest lwarx/stwcx. (std::atomic CAS) instead
# of one LSE instruction. Every phone since the Snapdragon 845 / Cortex-A75 has it;
# the app checks /proc/cpuinfo before loading (NativeBridge.kt). Turn off for a
# build that must run on ARMv8.0 cores.
option(PES_ARMV82 "Build for ARMv8.2-A (LSE atomics, fp16, dotprod) on Android" ON)
if(ANDROID AND PES_ARMV82)
    string(APPEND CMAKE_C_FLAGS " -march=armv8.2-a+fp16+dotprod")
    string(APPEND CMAKE_CXX_FLAGS " -march=armv8.2-a+fp16+dotprod")
endif()

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# For the checks that assert rather than report, so `ctest` is a yes or a no.
# Everything registered runs without game files, a disc or a GPU.
enable_testing()
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

# Android brings its own window, input and audio (runtime/android/), so SDL is
# off there whatever the cache says -- and a default the Gradle build does not
# have to repeat.
if(ANDROID)
    set(PES17_USE_SDL OFF CACHE BOOL "Use SDL3 (not on Android)" FORCE)
endif()
option(PES17_USE_SDL "Use SDL3 for gamepad input (and later window/audio)" ON)
option(PES17_TRACE_INDIRECT "Report indirect calls with no recompiled target instead of jumping to null" ON)
option(PES17_LOGGING "Emit the runtime's log" ON)
option(PES17_PORTABLE "Link the C++ runtime statically, for a binary that runs on other machines" OFF)
option(PES17_DIAGNOSTICS "Build the traces, dumps, statistics, reports and test harness (runtime/diagnostics.h)" ON)

# ---- which executable ------------------------------------------------------
# PES 2017 ships one executable, default.xex; build.sh recompiles it into ppc/.
set(PES17_TITLE "sp")
set(PES17_VERSION "tu0")
set(PES17_PPC_DIR "${CMAKE_SOURCE_DIR}/ppc")

file(GLOB PPC_SOURCES CONFIGURE_DEPENDS "${PES17_PPC_DIR}/ppc_recomp.*.cpp")
list(APPEND PPC_SOURCES "${PES17_PPC_DIR}/ppc_func_mapping.cpp")
if(NOT PPC_SOURCES)
    message(FATAL_ERROR "${PES17_PPC_DIR} is empty -- run TITLE=${PES17_TITLE} VERSION=${PES17_VERSION} ./build.sh first to generate it")
endif()

add_library(ppc_recomp STATIC ${PPC_SOURCES})
set(SIMDE_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/XenonRecomp/thirdparty/simde")
if(NOT EXISTS "${SIMDE_INCLUDE_DIR}/simde/simde-common.h" AND EXISTS "${CMAKE_SOURCE_DIR}/third_party/simde/simde/simde-common.h")
    set(SIMDE_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/simde")
endif()
target_include_directories(ppc_recomp PUBLIC
    "${PES17_PPC_DIR}"
    "${SIMDE_INCLUDE_DIR}"
    "${SIMDE_INCLUDE_DIR}/simde")
# The generated code is machine output; its warnings are noise.
target_compile_options(ppc_recomp PRIVATE -w)
# PowerPC separate fmul/fadd must stay separate (fused forms are emitted
# explicitly where the guest fuses), and the generated accesses alias through
# the guest base pointer: contraction and strict aliasing would change results.
target_compile_options(ppc_recomp PRIVATE -ffp-contract=off -fno-strict-aliasing)
# The console's hardware registers are plain memory in this address space. The
# recompiler marks the stores that address them (eieio follows), and this header
# routes those through a hook so the XMA decoder sees its kick, lock and clear
# registers written. Force-included: ppc/ is regenerated by build.sh.
target_compile_options(ppc_recomp PRIVATE -include "${CMAKE_SOURCE_DIR}/runtime/mmio_hook.h")
option(PES_TRAP_INF "Report stores of infinity (arm with files/trap_inf); slows every store" OFF)
if(PES_TRAP_INF)
    target_compile_definitions(ppc_recomp PRIVATE PES_TRAP_INF=1)
endif()
if(NOT PES17_LOGGING)
    # Compiles every LOGI/LOGW/LOGE/LOGK call to nothing, warnings included.
    add_compile_definitions(PES17_NO_LOGGING=1)
endif()

if(PES17_TRACE_INDIRECT)
    target_compile_definitions(ppc_recomp PUBLIC PES17_TRACE_INDIRECT=1)
endif()

# ---- runtime ---------------------------------------------------------------
file(GLOB RUNTIME_SOURCES CONFIGURE_DEPENDS
     "${CMAKE_SOURCE_DIR}/runtime/*.cpp"
     "${CMAKE_SOURCE_DIR}/runtime/kernel/*.cpp"
     "${CMAKE_SOURCE_DIR}/runtime/gpu/*.cpp"
     "${CMAKE_SOURCE_DIR}/runtime/apu/*.cpp"
     "${CMAKE_SOURCE_DIR}/runtime/install/*.cpp"
     "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/*.cpp"
     "${CMAKE_SOURCE_DIR}/runtime/gpu/xe/*.cpp")

# Xenia's shader interpreter, vendored with shims for the rex/ headers it
# includes; runs memory-export vertex shaders on the CPU (runtime/gpu/xe).
set_source_files_properties(
    "${CMAKE_SOURCE_DIR}/runtime/gpu/xe/interpreter.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/xe/memexport.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/xe/ucode.cpp"
    PROPERTIES INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/runtime/gpu/xe/shim")

if(ANDROID)
    # The window, the touch pad, the AAudio sink, the driver loader and the
    # JNI surface the app talks to (docs/android.md).
    file(GLOB ANDROID_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/runtime/android/*.cpp")
    list(APPEND RUNTIME_SOURCES ${ANDROID_SOURCES} "${CMAKE_SOURCE_DIR}/launcher/disc.cpp")
    # The title-update package reader (dependency-free STFS): the TU6 apps
    # apply the update on the device after copying the disc.
    list(APPEND RUNTIME_SOURCES "${CMAKE_SOURCE_DIR}/launcher/package.cpp")
    # XenonRecomp's patcher, which turns the disc's executables into the
    # update's. Vendored in third_party/XenonRecomp, so present in every checkout;
    # kept conditional in case the tree is trimmed, where TU6 apps report that the
    # APK must come from CI instead of failing to link.
    set(XENON "${CMAKE_SOURCE_DIR}/third_party/XenonRecomp")
    if(EXISTS "${XENON}/XenonUtils/xex_patcher.cpp" AND EXISTS "${XENON}/XenonUtils/xex_patcher.h")
        enable_language(C)
        list(APPEND RUNTIME_SOURCES
            "${XENON}/XenonUtils/xex_patcher.cpp"
            "${XENON}/XenonUtils/memory_mapped_file.cpp"
            "${XENON}/thirdparty/libmspack/libmspack/mspack/lzxd.c"
            "${XENON}/thirdparty/tiny-AES-c/aes.c")
        set(PES17_XENON_INCLUDES
            "${XENON}/XenonUtils"
            "${XENON}/thirdparty/simde"
            "${XENON}/thirdparty/libmspack/libmspack/mspack"
            "${XENON}/thirdparty/tiny-AES-c"
            "${XENON}/thirdparty/TinySHA1")
    endif()
endif()

# The glue build.sh generates from the recompiled output -- a stub for every
# kernel import the runtime lacks, a tracing hook for every D3D9 entry point --
# lives beside that output, since it is different for each title.
# ---- online service --------------------------------------------------------
# What the title's network calls reach (runtime/online/service.h): "none" keeps
# system link on this machine, "lan" is plain UDP between machines, "steam" is
# Steam, with lan behind it for when Steam is not running. A backend is the one
# file runtime/online/<name>.cpp; the runtime tries them in service.cpp's order.
set(PES17_ONLINE "none" CACHE STRING "Online service: none, lan or steam")
if(PES17_ONLINE STREQUAL "none")
    set(PES17_ONLINE_BACKENDS "")
elseif(PES17_ONLINE STREQUAL "lan")
    set(PES17_ONLINE_BACKENDS lan)
elseif(PES17_ONLINE STREQUAL "steam")
    set(PES17_ONLINE_BACKENDS steam lan)
else()
    message(FATAL_ERROR "PES17_ONLINE=${PES17_ONLINE}: none, lan or steam")
endif()
set(PES17_ONLINE_SOURCES "")
set(PES17_ONLINE_DEFINITIONS "")
foreach(backend ${PES17_ONLINE_BACKENDS})
    list(APPEND PES17_ONLINE_SOURCES "${CMAKE_SOURCE_DIR}/runtime/online/${backend}.cpp")
    string(TOUPPER "${backend}" backend_upper)
    list(APPEND PES17_ONLINE_DEFINITIONS "PES17_ONLINE_${backend_upper}=1")
endforeach()
message(STATUS "Online service: ${PES17_ONLINE}")

# On Android there is no process of ours to be the executable: the app's
# activity is the process, and this is what it loads (libpes17.so, System
# .loadLibrary("pes17"), runtime/android/jni.cpp). Everywhere else it is the
# program itself.
if(ANDROID)
    add_library(pes17 SHARED ${RUNTIME_SOURCES}
        "${CMAKE_SOURCE_DIR}/runtime/online/service.cpp"
        ${PES17_ONLINE_SOURCES}
        "${PES17_PPC_DIR}/stubs_generated.cpp")
else()
    add_executable(pes17 ${RUNTIME_SOURCES}
        "${CMAKE_SOURCE_DIR}/runtime/online/service.cpp"
        ${PES17_ONLINE_SOURCES}
        "${PES17_PPC_DIR}/stubs_generated.cpp")
endif()
target_link_libraries(pes17 PRIVATE ppc_recomp)
# ppc_recomp_shared.h declares a function taking uint8_t before every includer has <cstdint>.
target_compile_options(pes17 PRIVATE $<$<COMPILE_LANGUAGE:CXX>:SHELL:-include cstdint>)
target_compile_definitions(pes17 PRIVATE ${PES17_ONLINE_DEFINITIONS})
if(PES17_DIAGNOSTICS)
    target_compile_definitions(pes17 PRIVATE PES17_DIAGNOSTICS=1)
else()
    target_compile_definitions(pes17 PRIVATE PES17_DIAGNOSTICS=0)
endif()
target_include_directories(pes17 PRIVATE "${CMAKE_SOURCE_DIR}/runtime")
# The runtime checks the disc's executable against this hash before installing.
if(NOT PES17_XEX_SHA256_SP)
    if(EXISTS "${CMAKE_SOURCE_DIR}/pes/default.xex")
        set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/pes/default.xex")
        file(SHA256 "${CMAKE_SOURCE_DIR}/pes/default.xex" PES17_XEX_SHA256_SP)
    else()
        message(FATAL_ERROR "pes/default.xex is missing -- build.sh extracts it from the ISO")
    endif()
endif()
set(PES17_XEX_SHA256_MP "${PES17_XEX_SHA256_SP}")
set(PES17_DISC_SHA256_SP "${PES17_XEX_SHA256_SP}")
set(PES17_DISC_SHA256_MP "${PES17_XEX_SHA256_SP}")
target_compile_definitions(pes17 PRIVATE
    PES17_XEX_SHA256_SP="${PES17_XEX_SHA256_SP}" PES17_XEX_SHA256_MP="${PES17_XEX_SHA256_MP}"
    PES17_DISC_SHA256_SP="${PES17_DISC_SHA256_SP}" PES17_DISC_SHA256_MP="${PES17_DISC_SHA256_MP}"
    PES17_UPDATE_SHA256_PATCH_MP="" PES17_UPDATE_SHA256_DLC1_UI_MP="" PES17_UPDATE_SHA256_DLC2_UI_MP="")
if(PES17_XENON_INCLUDES)
    target_include_directories(pes17 PRIVATE ${PES17_XENON_INCLUDES})
    target_compile_definitions(pes17 PRIVATE PES17_HAVE_XEX_PATCHER=1)
endif()
if(WIN32)
    # One executable with nothing beside it: the C++ runtime and the threads
    # linked in. The recompiled code nests deep on the host stack, and a thread
    # Windows starts gets the size the executable asks for: 16 MB, where Linux
    # gives 8.
    target_link_libraries(pes17 PRIVATE ws2_32 iphlpapi winmm bcrypt)
    target_link_options(pes17 PRIVATE -static -Wl,--stack,16777216)
    # A player's build opens no console window (runtime/platform.cpp).
    if(NOT PES17_DIAGNOSTICS)
        target_link_options(pes17 PRIVATE -mwindows)
    endif()
elseif(ANDROID)
    include(cmake/android.cmake)
else()
    # -rdynamic so backtrace_symbols can resolve the sub_XXXXXXXX guest functions.
    set_target_properties(pes17 PROPERTIES ENABLE_EXPORTS ON)
    target_link_options(pes17 PRIVATE -rdynamic)
    if(PES17_PORTABLE)
        target_link_options(pes17 PRIVATE -static-libstdc++ -static-libgcc)
    endif()
endif()

if(PES17_USE_SDL)
    include(FetchContent)
    FetchContent_Declare(SDL3
        GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
        GIT_TAG release-3.2.8
        GIT_SHALLOW TRUE)
    set(SDL_SHARED OFF CACHE BOOL "" FORCE)
    set(SDL_STATIC ON  CACHE BOOL "" FORCE)
    set(SDL_TEST   OFF CACHE BOOL "" FORCE)
    FetchContent_MakeAvailable(SDL3)
    target_link_libraries(pes17 PRIVATE SDL3::SDL3-static)
    target_compile_definitions(pes17 PRIVATE PES17_USE_SDL=1)
endif()

# ---- XMA decoder ----------------------------------------------------------
add_subdirectory(third_party/ffmpeg-xenia)
target_link_libraries(pes17 PRIVATE ffmpeg_xma)

# ---- Vulkan ----------------------------------------------------------------
# Loaded through the loader rather than linked to a driver, so a machine without
# a device still builds and runs; the presenter falls back to headless.
if(WIN32)
    include(cmake/vulkan-windows.cmake)
    add_dependencies(pes17 vulkan_import_library)
elseif(ANDROID)
    # The NDK's sysroot carries the headers and a libvulkan.so, but this build
    # links neither: which Vulkan the process talks to is decided while it
    # runs -- the system's, or a driver the player imported
    # (runtime/gpu/vulkan/loader.cpp, runtime/android/driver.cpp). Setting
    # this here keeps the Vulkan block below as it is.
    set(Vulkan_FOUND TRUE)
    set(Vulkan_LIBRARY "loaded at run time")
else()
    find_package(Vulkan)
endif()
if(Vulkan_FOUND)
    if(NOT ANDROID)
        target_link_libraries(pes17 PRIVATE Vulkan::Vulkan)
    endif()
    target_compile_definitions(pes17 PRIVATE PES17_HAVE_VULKAN=1)
    message(STATUS "Vulkan found: ${Vulkan_LIBRARY}")
else()
    message(STATUS "Vulkan not found -- building without a window")
endif()

if(NOT WIN32 AND NOT ANDROID)
    target_link_libraries(pes17 PRIVATE pthread)
endif()

# The two tools below are for working on the renderer, on Linux.
if(NOT ANDROID)
# ---- offline shader translation -------------------------------------------
# The translator has to live in the runtime, because the title hands the GPU
# microcode while it runs. Building it as a tool too gives a fast loop against
# captured shaders and spirv-val, without launching the game.
add_executable(translate-shader
    "${CMAKE_SOURCE_DIR}/tools/translate_shader.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/shader_translator.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/pipeline.cpp"
    # The device this tool brings up is the runtime's own, so it answers for the
    # same Snapdragon workaround (bc_decoder.cpp, vk::bc::AdrenoMode).
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/bc_decoder.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/spirv.cpp")
target_include_directories(translate-shader PRIVATE "${CMAKE_SOURCE_DIR}/runtime")
# --compile puts a translated module in front of the real driver, which is a
# stronger check than spirv-val: it is the compiler that would run the shader.
if(Vulkan_FOUND)
    target_link_libraries(translate-shader PRIVATE Vulkan::Vulkan)
    target_compile_definitions(translate-shader PRIVATE PES17_HAVE_VULKAN=1)
endif()

# ---- offline texture upload ------------------------------------------------
# The upload hands the driver the guest's own block bytes, so reading the image
# back returns exactly what went in. That is a test, and it needs neither the
# game nor a picture -- unlike the untiler, which needs both.
add_executable(upload-texture
    "${CMAKE_SOURCE_DIR}/tools/upload_texture.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/texture.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/texture_cache.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/texture_store.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/bc_decoder.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/image_memory.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/texture_formats.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/pipeline.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/shader_translator.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/spirv.cpp")
target_include_directories(upload-texture PRIVATE "${CMAKE_SOURCE_DIR}/runtime")
if(Vulkan_FOUND)
    target_link_libraries(upload-texture PRIVATE Vulkan::Vulkan)
    target_compile_definitions(upload-texture PRIVATE PES17_HAVE_VULKAN=1)
endif()

# ---- the software decoder's speed ------------------------------------------
# Not a check: a number. The decoder runs on every texture uploaded where the
# driver cannot sample block compression, so the loops in it are worth timing
# the way the comments there quote -- a 2048x2048 decode, per format, with the
# output buffer reused as the runtime reuses its scratch.
add_executable(bc-bench
    "${CMAKE_SOURCE_DIR}/tools/bc_bench.cpp"
    "${CMAKE_SOURCE_DIR}/runtime/gpu/vulkan/bc_decoder.cpp")
target_include_directories(bc-bench PRIVATE "${CMAKE_SOURCE_DIR}/runtime")
if(Vulkan_FOUND)
    target_link_libraries(bc-bench PRIVATE Vulkan::Vulkan)
    target_compile_definitions(bc-bench PRIVATE PES17_HAVE_VULKAN=1)
endif()

# ---- the checks `ctest` runs -----------------------------------------------
# The three of them are defined in a project of their own (tests/CMakeLists.txt)
# so they can be configured and run on a tree that cannot build the runtime: this
# file refuses to configure without the recompiled guest code, which is made from
# the disc's executables and is in no repository, so a CI runner has none of it
# (.github/workflows/tests.yml). Including it here is what keeps a local `ctest`
# running exactly what CI runs.
add_subdirectory(tests)
endif()

# ---- launcher --------------------------------------------------------------
# What a player opens (launcher/): it installs the game and starts it. One
# executable with nothing beside it, like the game's: Dear ImGui on SDL's own
# renderer, the fonts as arrays, and XenonRecomp's patcher, which applies the
# title update, compiled in from its checkout.
if(PES17_USE_SDL)
    enable_language(C)
    FetchContent_Declare(imgui
        GIT_REPOSITORY https://github.com/ocornut/imgui.git
        GIT_TAG v1.91.9b
        GIT_SHALLOW TRUE)
    FetchContent_MakeAvailable(imgui)

    # third_party/fonts/*.ttf as C arrays.
    set(PES17_FONTS_SOURCE "${CMAKE_BINARY_DIR}/launcher_fonts.cpp")
    set(fonts_text "// Generated by CMake from third_party/fonts/.\n")
    foreach(font "Menu=BarlowCondensed-Regular" "Heading=BarlowCondensed-Medium" "Body=BarlowSemiCondensed-Regular")
        string(REPLACE "=" ";" font "${font}")
        list(GET font 0 font_name)
        list(GET font 1 font_file)
        set(font_path "${CMAKE_SOURCE_DIR}/third_party/fonts/${font_file}.ttf")
        set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${font_path}")
        file(READ "${font_path}" font_hex HEX)
        string(LENGTH "${font_hex}" font_length)
        math(EXPR font_size "${font_length} / 2")
        string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," font_bytes "${font_hex}")
        string(APPEND fonts_text "extern const unsigned char kFont${font_name}[] = { ${font_bytes} };\n"
                                 "extern const unsigned kFont${font_name}Size = ${font_size};\n")
    endforeach()
    # The window's icon the same way (tools/make_artwork.py makes it).
    set(icon_path "${CMAKE_SOURCE_DIR}/launcher/icon/launcher.bmp")
    set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${icon_path}")
    file(READ "${icon_path}" icon_hex HEX)
    string(LENGTH "${icon_hex}" icon_length)
    math(EXPR icon_size "${icon_length} / 2")
    string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," icon_bytes "${icon_hex}")
    string(APPEND fonts_text "extern const unsigned char kIcon[] = { ${icon_bytes} };\n"
                             "extern const unsigned kIconSize = ${icon_size};\n")
    file(GENERATE OUTPUT "${PES17_FONTS_SOURCE}" CONTENT "${fonts_text}")

    # The game draws its sign-in screen (runtime/signin.cpp) with the same
    # fonts, rasterised by the stb_truetype that comes with Dear ImGui.
    target_sources(pes17 PRIVATE "${PES17_FONTS_SOURCE}")
    target_include_directories(pes17 PRIVATE "${imgui_SOURCE_DIR}")
    target_compile_definitions(pes17 PRIVATE PES17_SIGNIN_PICTURE=1)

    set(XENON "${CMAKE_SOURCE_DIR}/third_party/XenonRecomp")
    file(GLOB LAUNCHER_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/launcher/*.cpp")
    add_executable(launcher ${LAUNCHER_SOURCES}
        "${PES17_FONTS_SOURCE}"
        "${CMAKE_SOURCE_DIR}/runtime/install/crypto.cpp"
        "${CMAKE_SOURCE_DIR}/runtime/install/xex.cpp"
        "${CMAKE_SOURCE_DIR}/runtime/install/spawn.cpp"
        "${imgui_SOURCE_DIR}/imgui.cpp"
        "${imgui_SOURCE_DIR}/imgui_draw.cpp"
        "${imgui_SOURCE_DIR}/imgui_tables.cpp"
        "${imgui_SOURCE_DIR}/imgui_widgets.cpp"
        "${imgui_SOURCE_DIR}/backends/imgui_impl_sdl3.cpp"
        "${imgui_SOURCE_DIR}/backends/imgui_impl_sdlrenderer3.cpp"
        "${XENON}/XenonUtils/xex_patcher.cpp"
        "${XENON}/XenonUtils/memory_mapped_file.cpp"
        "${XENON}/thirdparty/libmspack/libmspack/mspack/lzxd.c"
        "${XENON}/thirdparty/tiny-AES-c/aes.c")
    set_target_properties(launcher PROPERTIES OUTPUT_NAME pes17-launcher)
    target_include_directories(launcher PRIVATE
        "${imgui_SOURCE_DIR}" "${imgui_SOURCE_DIR}/backends"
        "${XENON}/XenonUtils" "${XENON}/thirdparty/simde"
        "${XENON}/thirdparty/libmspack/libmspack/mspack"
        "${XENON}/thirdparty/tiny-AES-c" "${XENON}/thirdparty/TinySHA1")
    target_compile_definitions(launcher PRIVATE ${PES17_HASH_DEFINITIONS} NOMINMAX)
    # What release this is, for the launcher to look for a newer one: the
    # release workflow gives the tag. A build without one does not update.
    set(PES17_RELEASE_TAG "" CACHE STRING "The release tag this build is published as, e.g. v0.3.0")
    if(WIN32)
        set(PES17_RELEASE_KIND "windows-${PES17_ONLINE}")
    else()
        set(PES17_RELEASE_KIND "linux-${PES17_ONLINE}")
    endif()
    set_source_files_properties("${CMAKE_SOURCE_DIR}/launcher/update.cpp" PROPERTIES COMPILE_DEFINITIONS
        "PES17_RELEASE_TAG=\"${PES17_RELEASE_TAG}\";PES17_RELEASE_KIND=\"${PES17_RELEASE_KIND}\"")
    # The decoder the game has for its sound, for the menu sounds (launcher/sound.cpp).
    target_link_libraries(launcher PRIVATE SDL3::SDL3-static ffmpeg_xma)
    if(WIN32)
        # The program's icon, as Explorer and the taskbar show it.
        enable_language(RC)
        target_sources(launcher PRIVATE "${CMAKE_SOURCE_DIR}/launcher/icon/launcher.rc")
        target_include_directories(launcher PRIVATE "${CMAKE_SOURCE_DIR}/launcher/compat")
        target_link_libraries(launcher PRIVATE urlmon)
        target_link_options(launcher PRIVATE -static)
        if(NOT PES17_DIAGNOSTICS)
            target_link_options(launcher PRIVATE -mwindows)
        endif()
    else()
        target_link_libraries(launcher PRIVATE pthread)
        if(PES17_PORTABLE)
            target_link_options(launcher PRIVATE -static-libstdc++ -static-libgcc)
        endif()
    endif()
endif()
