cmake_minimum_required(VERSION 3.24)
set(P2R_VERSION_STRING "0.1.3-beta")  # shown in the launcher, compared by the update check, names the release zip
project(P2Recomp VERSION 0.1.3 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

function(p2_warnings target)
    if(MSVC)
        target_compile_options(${target} PRIVATE /W4)
    else()
        target_compile_options(${target} PRIVATE -Wall -Wextra)
    endif()
endfunction()

# Runtime: everything the shipped executable links (loader, HLE, GE, CPU runtime).
add_library(p2_core STATIC
    src/cpu/runtime.cpp
    src/cpu/vfpu.cpp
    src/ge/ge_interpreter.cpp
    src/gfx/guest_texture.cpp
    src/gfx/png.cpp
    src/gfx/soft_gpu.cpp
    src/gfx/button_prompts.cpp
    src/gfx/texture_enhance.cpp
    src/gfx/texture_pack.cpp
    src/hle/hle.cpp
    src/hle/kernel/fiber.cpp
    src/hle/kernel/kernel.cpp
    src/hle/modules/atrac.cpp
    src/hle/modules/audio.cpp
    src/hle/modules/sas.cpp
    src/hle/modules/mpeg.cpp
    src/hle/modules/ctrl.cpp
    src/hle/modules/display.cpp
    src/hle/modules/iofilemgr.cpp
    src/hle/modules/sysmem.cpp
    src/hle/modules/system.cpp
    src/hle/modules/threadman.cpp
    src/hle/modules/utility.cpp
    src/hle/sceGe.cpp
    src/loader/imports.cpp
    src/loader/prx_loader.cpp
    src/vfs/iso9660.cpp
)
target_include_directories(p2_core PUBLIC src)
target_include_directories(p2_core PRIVATE third_party)
if(WIN32)
    target_link_libraries(p2_core PUBLIC winmm dbghelp)  # timeBeginPeriod, fault backtraces
endif()
p2_warnings(p2_core)

# Offline recompiler: MIPS analysis + C++ emission, and its CLI.
add_library(p2_recomp_lib STATIC src/recomp/analysis.cpp src/recomp/emitter.cpp)
target_link_libraries(p2_recomp_lib PUBLIC p2_core)
p2_warnings(p2_recomp_lib)
add_executable(p2_recomp tools/recomp/main.cpp)
target_link_libraries(p2_recomp PRIVATE p2_recomp_lib)

# Generated code is large and full of labels/branches the warnings would flag.
function(p2_generated_sources target)
    if(MSVC)
        target_compile_options(${target} PRIVATE /bigobj)
    endif()
endfunction()

# Recompiled game code, regenerated whenever the ELF or the recompiler changes.
# Only available after tools/unpack_eboot.py produced game/EBOOT.ELF.
set(P2_GAME_ELF ${CMAKE_SOURCE_DIR}/game/EBOOT.ELF)
set(P2_GENERATED_DIR ${CMAKE_BINARY_DIR}/generated)
set(P2_CHUNKS 64)
if(EXISTS ${P2_GAME_ELF})
    set(P2_GAME_SRCS ${P2_GENERATED_DIR}/recomp_table.cpp)
    math(EXPR last "${P2_CHUNKS} - 1")
    foreach(i RANGE ${last})
        string(LENGTH "${i}" len)
        if(len EQUAL 1)
            set(i "0${i}")
        endif()
        list(APPEND P2_GAME_SRCS ${P2_GENERATED_DIR}/recomp_${i}.cpp)
    endforeach()
    add_custom_command(
        OUTPUT ${P2_GAME_SRCS} ${P2_GENERATED_DIR}/recomp_funcs.h
        COMMAND p2_recomp ${P2_GAME_ELF} ${P2_GENERATED_DIR} ${P2_CHUNKS}
        DEPENDS p2_recomp ${P2_GAME_ELF}
        COMMENT "Recompiling EBOOT.ELF")
    add_library(p2_game_code STATIC ${P2_GAME_SRCS})
    target_include_directories(p2_game_code PUBLIC ${P2_GENERATED_DIR})
    target_link_libraries(p2_game_code PUBLIC p2_core)
    p2_generated_sources(p2_game_code)

    # SDL3 (prebuilt Visual C++ package, downloaded once into the build tree).
    set(P2_SDL3_VERSION 3.2.20)
    set(P2_SDL3_DIR ${CMAKE_BINARY_DIR}/_deps/SDL3-${P2_SDL3_VERSION})
    if(NOT EXISTS ${P2_SDL3_DIR}/cmake/SDL3Config.cmake)
        set(P2_SDL3_ZIP ${CMAKE_BINARY_DIR}/_deps/SDL3-devel-${P2_SDL3_VERSION}-VC.zip)
        file(DOWNLOAD
            https://github.com/libsdl-org/SDL/releases/download/release-${P2_SDL3_VERSION}/SDL3-devel-${P2_SDL3_VERSION}-VC.zip
            ${P2_SDL3_ZIP} SHOW_PROGRESS)
        file(ARCHIVE_EXTRACT INPUT ${P2_SDL3_ZIP} DESTINATION ${CMAKE_BINARY_DIR}/_deps)
    endif()
    find_package(SDL3 CONFIG REQUIRED PATHS ${P2_SDL3_DIR} NO_DEFAULT_PATH)

    # Vulkan headers and the glslang shader compiler (no Vulkan SDK needed), downloaded once.
    set(P2_DEPS ${CMAKE_BINARY_DIR}/_deps)
    set(P2_VULKAN_HEADERS ${P2_DEPS}/Vulkan-Headers-1.3.296/include)
    if(NOT EXISTS ${P2_VULKAN_HEADERS}/vulkan/vulkan.h)
        file(DOWNLOAD https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/v1.3.296.zip
            ${P2_DEPS}/Vulkan-Headers-1.3.296.zip SHOW_PROGRESS)
        file(ARCHIVE_EXTRACT INPUT ${P2_DEPS}/Vulkan-Headers-1.3.296.zip DESTINATION ${P2_DEPS})
    endif()
    set(P2_GLSLANG ${P2_DEPS}/glslang/bin/glslang.exe)
    if(NOT EXISTS ${P2_GLSLANG})
        file(DOWNLOAD https://github.com/KhronosGroup/glslang/releases/download/main-tot/glslang-main-windows-x86_64-release.zip
            ${P2_DEPS}/glslang.zip SHOW_PROGRESS)
        file(ARCHIVE_EXTRACT INPUT ${P2_DEPS}/glslang.zip DESTINATION ${P2_DEPS}/glslang)
    endif()
    # GLSL -> SPIR-V, embedded as uint32_t arrays (kGeVertSpv / kGeFragSpv).
    set(P2_SHADER_DIR ${CMAKE_BINARY_DIR}/shaders)
    file(MAKE_DIRECTORY ${P2_SHADER_DIR})
    add_custom_command(OUTPUT ${P2_SHADER_DIR}/ge_vert.spv.h
        COMMAND ${P2_GLSLANG} -V --vn kGeVertSpv -o ${P2_SHADER_DIR}/ge_vert.spv.h ${CMAKE_SOURCE_DIR}/shaders/ge.vert
        DEPENDS ${CMAKE_SOURCE_DIR}/shaders/ge.vert COMMENT "Compiling ge.vert")
    add_custom_command(OUTPUT ${P2_SHADER_DIR}/ge_frag.spv.h
        COMMAND ${P2_GLSLANG} -V --vn kGeFragSpv -o ${P2_SHADER_DIR}/ge_frag.spv.h ${CMAKE_SOURCE_DIR}/shaders/ge.frag
        DEPENDS ${CMAKE_SOURCE_DIR}/shaders/ge.frag COMMENT "Compiling ge.frag")

    add_custom_command(OUTPUT ${P2_SHADER_DIR}/post_vert.spv.h
        COMMAND ${P2_GLSLANG} -V --vn kPostVertSpv -o ${P2_SHADER_DIR}/post_vert.spv.h ${CMAKE_SOURCE_DIR}/shaders/post.vert
        DEPENDS ${CMAKE_SOURCE_DIR}/shaders/post.vert COMMENT "Compiling post.vert")
    add_custom_command(OUTPUT ${P2_SHADER_DIR}/post_frag.spv.h
        COMMAND ${P2_GLSLANG} -V --vn kPostFragSpv -o ${P2_SHADER_DIR}/post_frag.spv.h ${CMAKE_SOURCE_DIR}/shaders/post.frag
        DEPENDS ${CMAKE_SOURCE_DIR}/shaders/post.frag COMMENT "Compiling post.frag")

    add_custom_command(OUTPUT ${P2_SHADER_DIR}/interp_mv_frag.spv.h
        COMMAND ${P2_GLSLANG} -V --vn kInterpMvFragSpv -o ${P2_SHADER_DIR}/interp_mv_frag.spv.h ${CMAKE_SOURCE_DIR}/shaders/interp_mv.frag
        DEPENDS ${CMAKE_SOURCE_DIR}/shaders/interp_mv.frag COMMENT "Compiling interp_mv.frag")
    add_custom_command(OUTPUT ${P2_SHADER_DIR}/interp_frag.spv.h
        COMMAND ${P2_GLSLANG} -V --vn kInterpFragSpv -o ${P2_SHADER_DIR}/interp_frag.spv.h ${CMAKE_SOURCE_DIR}/shaders/interp.frag
        DEPENDS ${CMAKE_SOURCE_DIR}/shaders/interp.frag COMMENT "Compiling interp.frag")


    # Dear ImGui (launcher UI), downloaded once.
    set(P2_IMGUI ${P2_DEPS}/imgui-1.91.9b)
    if(NOT EXISTS ${P2_IMGUI}/imgui.h)
        file(DOWNLOAD https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b.zip ${P2_DEPS}/imgui.zip SHOW_PROGRESS)
        file(ARCHIVE_EXTRACT INPUT ${P2_DEPS}/imgui.zip DESTINATION ${P2_DEPS})
    endif()
    set(P2_IMGUI_SOURCES ${P2_IMGUI}/imgui.cpp ${P2_IMGUI}/imgui_draw.cpp ${P2_IMGUI}/imgui_tables.cpp
        ${P2_IMGUI}/imgui_widgets.cpp ${P2_IMGUI}/backends/imgui_impl_sdl3.cpp ${P2_IMGUI}/backends/imgui_impl_sdlrenderer3.cpp)

    # rcheevos (MIT): the RetroAchievements client library, built from source.
    set(P2_RCHEEVOS ${P2_DEPS}/rcheevos-11.6.0)
    if(NOT EXISTS ${P2_RCHEEVOS}/include/rc_client.h)
        file(DOWNLOAD https://github.com/RetroAchievements/rcheevos/archive/refs/tags/v11.6.0.zip ${P2_DEPS}/rcheevos.zip SHOW_PROGRESS)
        file(ARCHIVE_EXTRACT INPUT ${P2_DEPS}/rcheevos.zip DESTINATION ${P2_DEPS})
    endif()
    file(GLOB P2_RCHEEVOS_SOURCES ${P2_RCHEEVOS}/src/rc_client.c ${P2_RCHEEVOS}/src/rc_compat.c ${P2_RCHEEVOS}/src/rc_util.c
        ${P2_RCHEEVOS}/src/rc_version.c ${P2_RCHEEVOS}/src/rapi/*.c ${P2_RCHEEVOS}/src/rcheevos/*.c ${P2_RCHEEVOS}/src/rhash/*.c
        ${P2_RCHEEVOS}/src/rurl/*.c)
    add_library(rcheevos STATIC ${P2_RCHEEVOS_SOURCES})
    target_include_directories(rcheevos PUBLIC ${P2_RCHEEVOS}/include PRIVATE ${P2_RCHEEVOS}/src)
    target_compile_definitions(rcheevos PUBLIC RC_CLIENT_SUPPORTS_HASH RC_DISABLE_LUA PRIVATE _CRT_SECURE_NO_WARNINGS)

    # FFmpeg (LGPL shared build): only libavcodec's ATRAC3/ATRAC3+ decoders are used, for the music.
    set(P2_FFMPEG_NAME ffmpeg-n8.1.3-18-g46d899d7a0-win64-lgpl-shared-8.1)
    set(P2_FFMPEG ${P2_DEPS}/${P2_FFMPEG_NAME})
    if(NOT EXISTS ${P2_FFMPEG}/include/libavcodec/avcodec.h)
        file(DOWNLOAD https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-10-16-13-04/${P2_FFMPEG_NAME}.zip
            ${P2_DEPS}/ffmpeg.zip SHOW_PROGRESS)
        file(ARCHIVE_EXTRACT INPUT ${P2_DEPS}/ffmpeg.zip DESTINATION ${P2_DEPS})
    endif()
    file(GLOB P2_FFMPEG_DLLS ${P2_FFMPEG}/bin/avcodec-*.dll ${P2_FFMPEG}/bin/avutil-*.dll ${P2_FFMPEG}/bin/swresample-*.dll)

    # Executable icon: built from the game's own ICON0.PNG when the ISO is next to the sources (never committed).
    set(P2_ICON_RC "")
    set(P2_ISO_FILE ${CMAKE_SOURCE_DIR}/Persona_2_Innocent_Sin_USA_PSP.iso)
    find_package(Python3 QUIET COMPONENTS Interpreter)
    if(WIN32 AND Python3_FOUND AND EXISTS ${P2_ISO_FILE})
        set(P2_ICON ${CMAKE_BINARY_DIR}/icon/p2.ico)
        execute_process(COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/make_icon.py ${P2_ISO_FILE} ${P2_ICON} RESULT_VARIABLE P2_ICON_RESULT OUTPUT_QUIET ERROR_QUIET)
        if(P2_ICON_RESULT EQUAL 0)
            enable_language(RC)
            file(WRITE ${CMAKE_BINARY_DIR}/icon/p2.rc "1 ICON \"${P2_ICON}\"
")
            set(P2_ICON_RC ${CMAKE_BINARY_DIR}/icon/p2.rc)
        endif()
    endif()

    add_executable(p2r ${P2_ICON_RC} src/main.cpp src/platform/window.cpp src/platform/settings.cpp src/platform/launcher.cpp src/platform/audio.cpp src/platform/video.cpp src/platform/hd_worker.cpp src/platform/hd_prepare.cpp src/platform/save_import.cpp src/platform/save_picker.cpp src/platform/retroachievements.cpp src/platform/toast.cpp src/platform/http.cpp src/platform/update_check.cpp src/platform/game_files.cpp
        ${P2_IMGUI_SOURCES} src/gfx/vulkan_gpu.cpp
        ${P2_SHADER_DIR}/ge_vert.spv.h ${P2_SHADER_DIR}/ge_frag.spv.h
        ${P2_SHADER_DIR}/post_vert.spv.h ${P2_SHADER_DIR}/post_frag.spv.h
        ${P2_SHADER_DIR}/interp_mv_frag.spv.h ${P2_SHADER_DIR}/interp_frag.spv.h)
    target_include_directories(p2r PRIVATE ${P2_FFMPEG}/include ${P2_VULKAN_HEADERS} ${P2_SHADER_DIR} ${P2_IMGUI} ${P2_IMGUI}/backends)
    target_compile_definitions(p2r PRIVATE P2R_VERSION="${P2R_VERSION_STRING}")
    target_include_directories(p2r PRIVATE ${P2_RCHEEVOS}/src/rhash ${CMAKE_SOURCE_DIR}/third_party)
    if(EXISTS ${P2_GAME_ELF})
        file(MD5 ${P2_GAME_ELF} P2_ELF_MD5)  # the executable the game code was recompiled from
        target_compile_definitions(p2r PRIVATE P2R_ELF_MD5="${P2_ELF_MD5}")
    endif()
    target_link_libraries(p2r PRIVATE p2_game_code SDL3::SDL3 rcheevos winhttp psapi ${P2_FFMPEG}/lib/avcodec.lib ${P2_FFMPEG}/lib/avutil.lib)
    add_custom_command(TARGET p2r POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:p2r> ${P2_FFMPEG_DLLS} $<TARGET_FILE_DIR:p2r>
        COMMAND_EXPAND_LISTS)
endif()

option(P2_BUILD_PRXDECRYPT "Build the EBOOT decryption tool (fetches GPL pspdecrypt)" OFF)
if(P2_BUILD_PRXDECRYPT)
    add_subdirectory(tools/prxdecrypt)
endif()

include(CTest)
if(BUILD_TESTING)
    # Tests that take an argument also run their real-EBOOT checks once
    # tools/unpack_eboot.py has produced game/EBOOT.ELF; otherwise those are skipped.
    foreach(t ge_interpreter_test prx_loader_test hle_imports_test recomp_analysis_test kernel_test soft_gpu_test savedata_test)
        add_executable(${t} tests/${t}.cpp)
        target_link_libraries(${t} PRIVATE p2_recomp_lib)
        p2_warnings(${t})
        add_test(NAME ${t} COMMAND ${t} ${P2_GAME_ELF})
    endforeach()

    # Execution test: hand-assembled MIPS (tests/recomp_fixture.h) is recompiled at
    # build time, then the generated C++ is compiled into the test and run.
    set(FIXTURE_DIR ${CMAKE_BINARY_DIR}/recomp_fixture)
    set(FIXTURE_SRCS ${FIXTURE_DIR}/recomp_00.cpp ${FIXTURE_DIR}/recomp_table.cpp)
    add_executable(recomp_fixture_gen tests/recomp_fixture_gen.cpp)
    target_link_libraries(recomp_fixture_gen PRIVATE p2_recomp_lib)
    add_custom_command(
        OUTPUT ${FIXTURE_SRCS} ${FIXTURE_DIR}/recomp_funcs.h
        COMMAND recomp_fixture_gen ${FIXTURE_DIR}
        DEPENDS recomp_fixture_gen
        COMMENT "Recompiling MIPS test fixture")
    add_executable(recomp_exec_test tests/recomp_exec_test.cpp ${FIXTURE_SRCS})
    target_include_directories(recomp_exec_test PRIVATE ${FIXTURE_DIR} tests)
    target_link_libraries(recomp_exec_test PRIVATE p2_core)
    p2_generated_sources(recomp_exec_test)
    add_test(NAME recomp_exec_test COMMAND recomp_exec_test)

    # Links the whole recompiled game: proves every generated call resolves.
    if(TARGET p2_game_code)
        add_executable(recomp_game_test tests/recomp_game_test.cpp)
        target_link_libraries(recomp_game_test PRIVATE p2_game_code p2_recomp_lib)
        add_test(NAME recomp_game_test COMMAND recomp_game_test ${P2_GAME_ELF})
    endif()
endif()
