# fable_2 - ReXGlue Recompiled Project
#
# SDK boilerplate lives in generated/rexglue.cmake.

cmake_minimum_required(VERSION 3.25)
project(fable_2 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Needed by the recomp-level patch step below (tools/apply_recomp_patches.py).
find_package(Python3 COMPONENTS Interpreter REQUIRED)

include(generated/rexglue.cmake)

# Sources
set(FABLE2_SOURCES
    src/main.cpp
    src/core/fable2_config.cpp
    src/core/fable2_patches.cpp
    src/core/fable2_hooks.cpp
)

if(WIN32)
    add_executable(fable_2 WIN32 ${FABLE2_SOURCES})
else()
    add_executable(fable_2 ${FABLE2_SOURCES})
endif()

# The app's headers live in subfolders (src/core, src/diagnostics, src/input)
# and are included by bare filename ("fable2_config.h"), so the subfolders
# must be on the include path of both the app and the recompiled code.
set(FABLE2_HEADER_DIRS
    ${CMAKE_CURRENT_SOURCE_DIR}/src/core
    ${CMAKE_CURRENT_SOURCE_DIR}/src/diagnostics
    ${CMAKE_CURRENT_SOURCE_DIR}/src/input)
target_include_directories(fable_2 PRIVATE ${FABLE2_HEADER_DIRS})
if(TARGET fable_2_recomp)
    target_include_directories(fable_2_recomp PRIVATE ${FABLE2_HEADER_DIRS})
endif()

# Stage the Xenos GPU emulation plugin next to the exe (rexglue picks the
# per-configuration variant: rexgpu-xenosd.dll for Debug, rexgpu-xenos.dll
# for Release). Without it the game runs with no GPU emulation.
rexglue_setup_target(fable_2 GPU_PLUGINS xenos)

# The app's probe/diagnostic headers (src/diagnostics/*.h) use the standard CRT
# functions (fopen, getenv, strcat, strncat, ...), which the MSVC/UCRT headers
# mark deprecated when compiled with clang (-Wdeprecated-declarations noise).
# These are safe in this codebase; silence the deprecation on Windows.
target_compile_definitions(fable_2 PRIVATE
    $<$<PLATFORM_ID:Windows>:_CRT_SECURE_NO_WARNINGS>)

# Remote control server (AI input channel) is a debug-only tool: it opens a
# localhost TCP port and injects guest input, so it must NOT ship in Release /
# RelWithDebInfo builds (players shouldn't see a hidden input channel, and it's
# only useful for debugging). Defining FABLE2_REMOTE_CONTROL compiles the pad
# driver registration + server start/stop into the app; without it (non-Debug)
# those are #ifdef'd out entirely and no port is ever opened.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    target_compile_definitions(fable_2 PRIVATE FABLE2_REMOTE_CONTROL)
endif()

# --- Recomp-level patches (Xenia code-region patches, made effective) ------
# This is a RECOMP: guest .text is never executed, so Xenia code patches
# (byte writes to .text) are inert at runtime. tools/apply_recomp_patches.py
# mirrors those patches into codegen's emitted C++ instead, and runs between
# codegen and compiling fable_2_recomp (keyed on the codegen stamp), so the
# patches survive codegen re-runs. Missing anchors fail the build. Skip all
# with FABLE2_RECOMP_PATCHES=0 in the build environment (A/B runs).
if(TARGET fable_2_recomp)
    add_custom_command(
        OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/generated/default/recomp_patches.stamp"
        COMMAND ${Python3_EXECUTABLE}
                ${CMAKE_CURRENT_SOURCE_DIR}/tools/apply_recomp_patches.py
                ${CMAKE_CURRENT_SOURCE_DIR}/generated/default
        COMMAND ${CMAKE_COMMAND} -E touch
                "${CMAKE_CURRENT_SOURCE_DIR}/generated/default/recomp_patches.stamp"
        DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/generated/default/codegen.build.stamp"
                ${CMAKE_CURRENT_SOURCE_DIR}/tools/apply_recomp_patches.py
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        COMMENT "Applying recomp-level patches to generated code"
        VERBATIM
    )
    add_custom_target(fable_2_recomp_patches
        DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/generated/default/recomp_patches.stamp")
    add_dependencies(fable_2_recomp fable_2_recomp_patches)
endif()

# --- Guest function-call tracing (fable2_func_trace.log) -------------------
# Rexglue codegen emits REX_FUNC_PROLOGUE() at the top of every recompiled
# guest function. Force-include src/core/fable2_func_trace.h into fable_2_recomp:
# CMake orders a target's -include after its PCH include, so the header sees
# the generated pch already loaded and redefines REX_FUNC_PROLOGUE() to also
# call Fable2FuncTraceCall(__func__) - every guest function entry then logs
# its name (see the header). No generated files are modified, so codegen
# re-runs never lose the hook. Off by default; enable with
# FABLE2_FUNC_TRACE=1 (env) or Fable2FuncTraceSetEnabled(true) (runtime).
# Appended AFTER fable_2_pch.h in the target's precompile-header list: CMake
# writes the list into cmake_pch.hxx in order, so the hook section at the
# bottom of fable2_func_trace.h (which redefines REX_FUNC_PROLOGUE) runs with
# the generated pch already loaded, and the patched macro is baked into the
# PCH used by every recompiled TU.
if(TARGET fable_2_recomp)
    target_precompile_headers(fable_2_recomp PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src/core/fable2_func_trace.h)
endif()

# NOTE: the build does NOT copy the game content (default.xex, data/, ...) into
# the build directory. fable_2.exe looks for its content in its own directory
# only; to run in place, stage it manually (one-time ~6.5 GB copy):
#   tools\stage_content.cmd <project root> out\build\<preset>
# or point the exe at an existing content tree with --game_data_root=<path>.
# saves/, cache/, logs/, and fable_2.toml are always written next to the exe.

# Stage a copy of the README + the function-tracing launcher next to the exe
# so anyone who gets the build folder (without the repo) knows how to run
# the game and how to capture guest function-call logs.
add_custom_command(TARGET fable_2 POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
          ${CMAKE_CURRENT_SOURCE_DIR}/README.md
          $<TARGET_FILE_DIR:fable_2>/README.md
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
          ${CMAKE_CURRENT_SOURCE_DIR}/tools/fable2-functrace.cmd
          $<TARGET_FILE_DIR:fable_2>/fable2-functrace.cmd
  COMMENT "Staging README.md + fable2-functrace.cmd next to fable_2.exe"
)

# --- F5 external-Lua runner (src/lua/) -----------------------------------
# src/core/fable2_f5_lua.h runs an external script (cvar `f5_lua_path`, default
# "recomp/F5.lua") via CScriptManager::RunScript when F5 is pressed. The game
# indexes /data/ and /data/scripts/ at startup, so the file must already exist
# there before the game starts. The source lives in src/lua/ (git-tracked); on
# every build copy it into BOTH the content root (so stage_content.cmd / a
# --game_data_root=<root> run finds it) and next to the exe (the default game
# data root). Runs in the scripts/recomp/ subfolder so RunScript("recomp/...")
# resolves it.
add_custom_command(TARGET fable_2 POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_directory
          ${CMAKE_CURRENT_SOURCE_DIR}/src/lua
          ${CMAKE_CURRENT_SOURCE_DIR}/data/scripts/recomp
  COMMAND ${CMAKE_COMMAND} -E copy_directory
          ${CMAKE_CURRENT_SOURCE_DIR}/src/lua
          $<TARGET_FILE_DIR:fable_2>/data/scripts/recomp
  COMMENT "Staging src/lua/* -> data/scripts/recomp/ (content root + exe dir)"
)

# The remote-control CLI client is a debug-only feature (see the
# FABLE2_REMOTE_CONTROL gate above), so only stage it for Debug builds.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
  add_custom_command(TARGET fable_2 POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
            ${CMAKE_CURRENT_SOURCE_DIR}/tools/fable2_control.py
            $<TARGET_FILE_DIR:fable_2>/fable2_control.py
    COMMENT "Staging fable2_control.py (debug-only remote control client) next to fable_2.exe"
  )
endif()

# --- Recomp user config (fable2_config.toml) -------------------------------
# Header-only toml++ parser, vendored from the same version the ReXGlue SDK
# uses (thirdparty/rexglue-sdk-src/thirdparty/tomlplusplus), so the app's
# config loader does not depend on the SDK source tree being present.
target_include_directories(fable_2 SYSTEM PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/tomlplusplus/include)

# Stage the human-readable template next to the exe ONCE (configure time,
# copy-if-not-exists): it is user data, so user edits must survive rebuilds.
# The code also recreates it if it is ever deleted (keep that embedded copy
# in sync - see src/core/fable2_config.cpp).
if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/fable2_config.toml")
    file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/fable2_config.toml
         DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
endif()

# Same treatment for the guest-image patch table (fable2_patches.toml, parsed
# by src/core/fable2_patches.cpp): user data, copy-if-not-exists so edits survive
# rebuilds; the code also recreates it if it is ever deleted (keep that
# embedded copy in sync - see src/core/fable2_patches.cpp).
if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/fable2_patches.toml")
    file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/fable2_patches.toml
         DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
endif()

# --- PDB for the Release build (crash debugging) --------------------------
# The recompiled guest functions are named sub_<guest_address>, so a PDB lets
# the debugger resolve a crash PC directly to the offending guest function
# (see tools/crash_capture.cmd + cdb). -g emits CodeView info without
# changing -O3 codegen; /DEBUG makes lld-link emit the .pdb.
if(WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Release")
    target_compile_options(fable_2 PRIVATE -g)
    target_link_options(fable_2 PRIVATE -Xlinker /DEBUG)
    if(TARGET fable_2_recomp)
        target_compile_options(fable_2_recomp PRIVATE -g)
    endif()
endif()

# --- Stable-hash hardening: strip the absolute checkout path from __FILE__ --
# Panic/assert messages embed __FILE__. Map the source tree out so the path is
# relative to the repo root, making the binary hash independent of where the
# tree is checked out (part of the cert-free reproducible-build strategy).
if(WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    target_compile_options(fable_2 PRIVATE
        -ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=
        -fdebug-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=
        -fdebug-prefix-map=${CMAKE_CURRENT_BINARY_DIR}=.)
    if(TARGET fable_2_recomp)
        target_compile_options(fable_2_recomp PRIVATE
            -ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=
            -fdebug-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=
            -fdebug-prefix-map=${CMAKE_CURRENT_BINARY_DIR}=.)
    endif()
endif()

# --- VS_VERSION_INFO (professional identity, stable across builds) --------
# Add a well-formed version resource to the host exe. Skipped (not fatal) when
# no resource compiler is configured (e.g. a bare MSVC-only preset).
if(WIN32 AND CMAKE_RC_COMPILER AND TARGET fable_2)
    target_sources(fable_2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tools/repro/fable2_version.rc)
endif()

# --- Vulkan renderer: Stage 2 (source-built plugin, swappable at launch) --
# The prebuilt plugin above (GPU_PLUGINS xenos) is D3D12-only. The Vulkan
# backend is built from the (unmodified) SDK source via
# tools/build_sdk_vulkan.cmd (a Release build) into a separate tree. Stage it
# next to the exe as rexgpu-xenos-vulkan.dll so the game can swap to Vulkan at
# launch with --gpu_plugin=xenos-vulkan (see Fable2App::OnPreSetup) - no
# rebuild of the game required to switch backends. Release-only: the source
# plugin is a Release build (imports rexruntime.dll), which matches only the
# Release game (the Debug game links rexruntimed.dll). Skipped (with a note) if
# the source plugin hasn't been built yet.
# SDK locations, resolved relative to this repo (no hardcoded paths).
# Override on the command line with:
#   -DREXGLUE_SDK_ROOT=<prebuilt SDK root>    (win-amd64 layout: bin/, include/, lib/)
#   -DREXGLUE_SDK_SOURCE=<SDK source tree>    (optional; Vulkan plugin only)
# The defaults are created by tools/setup_sdk.cmd (prebuilt, auto-run by
# build.cmd) and tools/setup_sdk_src.cmd (source, only for the Vulkan path).
if(NOT DEFINED REXGLUE_SDK_ROOT)
    set(REXGLUE_SDK_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/rexglue-sdk/win-amd64")
endif()
if(NOT DEFINED REXGLUE_SDK_SOURCE)
    set(REXGLUE_SDK_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/rexglue-sdk-src")
endif()
set(FABLE2_SDK_SRC "${REXGLUE_SDK_SOURCE}")
set(FABLE2_SDK_ROOT "${REXGLUE_SDK_ROOT}")
if(WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Release")
    # The SDK stages its outputs under its own out/win-amd64/Release/ tree
    # (REXGLUE_ROOT based). We need both the source plugin AND the source
    # (both-backends) runtime: the prebuilt 0.10.0 runtime is D3D12-only and is
    # missing the 16 Vulkan symbols the source plugin imports, so the Vulkan
    # path must run against the source runtime.
    file(GLOB FABLE2_VULKAN_PLUGIN_CANDIDATES "${FABLE2_SDK_SRC}/out/win-amd64/Release/rexgpu-xenos.dll")
    file(GLOB FABLE2_VULKAN_RUNTIME_CANDIDATES "${FABLE2_SDK_SRC}/out/win-amd64/Release/rexruntime.dll")
    if(FABLE2_VULKAN_PLUGIN_CANDIDATES AND FABLE2_VULKAN_RUNTIME_CANDIDATES)
        list(GET FABLE2_VULKAN_PLUGIN_CANDIDATES 0 FABLE2_VULKAN_PLUGIN)
        list(GET FABLE2_VULKAN_RUNTIME_CANDIDATES 0 FABLE2_VULKAN_RUNTIME)
        # Stage, next to the exe:
        #   rexgpu-xenos-vulkan.dll  - source dual-backend plugin (D3D12+Vulkan;
        #                              fable2.cmd copies it over rexgpu-xenos.dll)
        #   rexruntime-vulkan.dll    - source (both-backends) runtime
        #   rexruntime-d3d12.dll     - prebuilt 0.10.0 runtime (D3D12 backup)
        #   rexgpu-xenos-prebuilt.dll- prebuilt 0.10.0 D3D12 plugin (pristine
        #                              backup for `fable2.cmd prebuilt`)
        #   fable2.cmd               - launcher that picks the runtime + backend
        # The default rexruntime.dll (staged above by GPU_PLUGINS xenos) is the
        # prebuilt one, so a plain `fable_2.exe` launch stays D3D12 (0.10.0)
        # until fable2.cmd restages a source pair.
        add_custom_command(TARGET fable_2 POST_BUILD
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_VULKAN_PLUGIN}"  "$<TARGET_FILE_DIR:fable_2>/rexgpu-xenos-vulkan.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_VULKAN_RUNTIME}" "$<TARGET_FILE_DIR:fable_2>/rexruntime-vulkan.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_SDK_ROOT}/bin/rexruntime.dll" "$<TARGET_FILE_DIR:fable_2>/rexruntime-d3d12.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FABLE2_SDK_ROOT}/bin/rexgpu-xenos.dll" "$<TARGET_FILE_DIR:fable_2>/rexgpu-xenos-prebuilt.dll"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/tools/fable2.cmd" "$<TARGET_FILE_DIR:fable_2>/fable2.cmd"
          COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/tools/fable2-uncapped.cmd" "$<TARGET_FILE_DIR:fable_2>/fable2-uncapped.cmd"
          COMMENT "Staging Vulkan plugin/runtime + D3D12 backups + launcher")
    else()
        message(STATUS "Vulkan SDK not fully built (need rexgpu-xenos.dll + rexruntime.dll under ${FABLE2_SDK_SRC}/out/win-amd64/Release) - run tools/build_sdk_vulkan.cmd to enable 'fable2.cmd vulkan'")
    endif()
endif()

# --- Vulkan renderer: Stage 1 smoke test ----------------------------------
# A self-contained Vulkan pipeline (vendored Khronos headers in
# thirdparty/vulkan + a dynamic vulkan-1.dll loader) that creates a window,
# inits Vulkan, and presents a clear color. Proves the Vulkan foundation in
# this repo before Stage 2 builds the Xenos->Vulkan command processor so the
# game itself renders. Standalone: no SDK/rexruntime dependency.
#   build: build.cmd fable_2_vulkan_smoke
if(WIN32)
    # GUI (WIN32) subsystem on purpose: a console-subsystem process is blocked
    # from creating top-level windows in this environment (CreateWindowExW
    # returns NULL with error 0), whereas the game (GUI/SDL) works.
    add_executable(fable_2_vulkan_smoke WIN32 src/vulkan_smoke.cpp)
    target_include_directories(fable_2_vulkan_smoke PRIVATE
      thirdparty/vulkan
      ${FABLE2_SDK_ROOT}/include
    )
    # Use the SDK's UI framework (rex::ui::Window / SDLWindowedAppContext) from
    # the SHARED rexruntime.dll — the game's proven window path. The SDL_*
    # symbols are NOT exported, but the rex::ui::* ones are, so link the import
    # lib and stage the DLL next to the exe.
    target_link_libraries(fable_2_vulkan_smoke PRIVATE
      ${FABLE2_SDK_ROOT}/lib/rexruntime.lib
      user32 gdi32
    )
    add_custom_command(TARGET fable_2_vulkan_smoke POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy_if_different
              ${FABLE2_SDK_ROOT}/bin/rexruntime.dll
              $<TARGET_FILE_DIR:fable_2_vulkan_smoke>
    )
endif()

# --- Reproducible / stable-hash post-processing (cert-free AV-clean builds) -
# Post-build AV-clean hardening (see plans/av-clean-builds.md +
# tools/repro/clean_pe.py): for the staged release dir it (1) trims the
# rexruntime export table to the curated set -- the union of every symbol a
# consumer actually imports plus the whole rex:: public API -- dropping the
# ~6k std/RTTI/operator/unused-__imp_ symbols WINDOWS_EXPORT_ALL_SYMBOLS leaks
# (an abnormal profile that feeds AV generic heuristics), and (2) pins the
# volatile PE bytes (timestamp, PDB GUID, PDB path). Together they make an
# identical source build to an identical, profile-normal hash that can
# accumulate a benign AV reputation instead of looking like a novel unknown
# file on every rebuild. Idempotent; missing files skipped.
if(WIN32)
    find_package(Python3 COMPONENTS Interpreter QUIET)
    if(Python3_FOUND)
        add_custom_command(TARGET fable_2 POST_BUILD
            COMMAND ${Python3_EXECUTABLE}
                    ${CMAKE_CURRENT_SOURCE_DIR}/tools/repro/clean_pe.py
                    $<TARGET_FILE_DIR:fable_2>
                    --quiet
            COMMENT "Trimming exports + normalizing PE (stable, AV-clean build)"
            VERBATIM)
    else()
        message(STATUS "Python3 not found; skipping stable-hash PE cleaning")
    endif()
endif()
