diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b57472d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,233 @@ +# UniversalSlashingSimulator - CMake Build Configuration +# +# This project builds a DLL that can be injected into Fortnite +# to provide STW (Save the World) gameserver functionality. +# +# Requirements: +# - Windows SDK +# - MinHook library (user provides) +# - Memcury library (user provides) +# - C++17 compiler + +cmake_minimum_required(VERSION 3.16) + +project(UniversalSlashingSimulator + VERSION 0.1.0 + DESCRIPTION "Version-agnostic STW Gameserver Framework" + LANGUAGES CXX +) + +# C++ Standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Windows-specific settings +if(WIN32) + add_definitions(-DWIN32_LEAN_AND_MEAN -DNOMINMAX) + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS OFF) +endif() + +# Build type +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Debug definitions +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + add_definitions(-DUSS_DEBUG) +endif() + +# ============================================================================ +# Source Files +# ============================================================================ + +# Core sources +set(CORE_SOURCES + Core/Logging/Log.cpp + Core/Memory/Memory.cpp + Core/Versioning/VersionResolver.cpp +) + +set(CORE_HEADERS + Core/Common.h + Core/Logging/Log.h + Core/Memory/Memory.h + Core/Versioning/VersionInfo.h + Core/Versioning/VersionResolver.h + Core/Hooks/HookTypes.h +) + +# Engine sources +set(ENGINE_SOURCES + Engine/CoreTypes/ObjectArray.cpp + Engine/CoreTypes/NamePool.cpp + Engine/CoreTypes/OffsetResolver.cpp + Engine/UObject/UObjectWrapper.cpp + Engine/Reflection/PropertyIterator.cpp + Engine/Replication/FastArraySerializer.cpp + Engine/Events/ProcessEventDispatcher.cpp + Engine/EngineCore.cpp +) + +set(ENGINE_HEADERS + Engine/CoreTypes/ObjectArray.h + Engine/CoreTypes/NamePool.h + Engine/CoreTypes/OffsetResolver.h + Engine/UObject/UObjectWrapper.h + Engine/Reflection/PropertyIterator.h + Engine/Replication/FastArraySerializer.h + Engine/Events/ProcessEventDispatcher.h + Engine/EngineCore.h +) + +# STW sources +set(STW_SOURCES + STW/GameMode/STWGameMode.cpp + STW/Missions/MissionManager.cpp + STW/Missions/MissionObjective.cpp + STW/Player/STWPlayerController.cpp + STW/Player/STWPlayerPawn.cpp + STW/Inventory/InventoryManager.cpp + STW/Building/BuildingManager.cpp +) + +set(STW_HEADERS + STW/GameMode/STWGameMode.h + STW/Missions/MissionManager.h + STW/Missions/MissionObjective.h + STW/Missions/MissionTypes.h + STW/Player/STWPlayerController.h + STW/Player/STWPlayerPawn.h + STW/Inventory/InventoryManager.h + STW/Inventory/InventoryTypes.h + STW/Building/BuildingManager.h + STW/Building/BuildingTypes.h +) + +# Entry point +set(ENTRY_SOURCES + Entry/DllMain.cpp +) + +# ============================================================================ +# Library Target +# ============================================================================ + +add_library(${PROJECT_NAME} SHARED + ${CORE_SOURCES} + ${CORE_HEADERS} + ${ENGINE_SOURCES} + ${ENGINE_HEADERS} + ${STW_SOURCES} + ${STW_HEADERS} + ${ENTRY_SOURCES} +) + +# Include directories +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# ============================================================================ +# External Dependencies (User Provides) +# ============================================================================ + +# MinHook - User must provide +# Add to: external/minhook/include and external/minhook/lib +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/external/minhook") + target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/external/minhook/include + ) + find_library(MINHOOK_LIB + NAMES MinHook libMinHook.x64 + PATHS ${CMAKE_CURRENT_SOURCE_DIR}/external/minhook/lib + ) + if(MINHOOK_LIB) + target_link_libraries(${PROJECT_NAME} PRIVATE ${MINHOOK_LIB}) + message(STATUS "Found MinHook: ${MINHOOK_LIB}") + endif() +endif() + +# Memcury - User must provide (header-only typically) +# Add to: external/memcury/include +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/external/memcury") + target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/external/memcury/include + ) + message(STATUS "Found Memcury: ${CMAKE_CURRENT_SOURCE_DIR}/external/memcury") +endif() + +# Windows libraries +target_link_libraries(${PROJECT_NAME} PRIVATE + psapi +) + +# ============================================================================ +# Compiler Settings +# ============================================================================ + +if(MSVC) + target_compile_options(${PROJECT_NAME} PRIVATE + /W4 # Warning level 4 + /WX- # Warnings not as errors (for development) + /MP # Multi-processor compilation + /permissive- # Strict conformance + /Zc:__cplusplus # Report correct C++ version + ) + + # Release optimizations + if(CMAKE_BUILD_TYPE STREQUAL "Release") + target_compile_options(${PROJECT_NAME} PRIVATE + /O2 + /Ob2 + /GL + ) + target_link_options(${PROJECT_NAME} PRIVATE + /LTCG + ) + endif() +else() + target_compile_options(${PROJECT_NAME} PRIVATE + -Wall + -Wextra + -Wpedantic + ) +endif() + +# ============================================================================ +# Output Settings +# ============================================================================ + +set_target_properties(${PROJECT_NAME} PROPERTIES + OUTPUT_NAME "USS" + SUFFIX ".dll" + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib +) + +# ============================================================================ +# Installation +# ============================================================================ + +install(TARGETS ${PROJECT_NAME} + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib +) + +# ============================================================================ +# Summary +# ============================================================================ + +message(STATUS "") +message(STATUS "UniversalSlashingSimulator Configuration:") +message(STATUS " Version: ${PROJECT_VERSION}") +message(STATUS " Build Type: ${CMAKE_BUILD_TYPE}") +message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}") +message(STATUS " Output: ${CMAKE_BINARY_DIR}/bin/USS.dll") +message(STATUS "") +message(STATUS "External Dependencies:") +message(STATUS " MinHook: ${MINHOOK_LIB}") +message(STATUS " Memcury: (header-only, check external/memcury)") +message(STATUS "") diff --git a/Core/Common.h b/Core/Common.h new file mode 100644 index 0000000..67d7943 --- /dev/null +++ b/Core/Common.h @@ -0,0 +1,111 @@ +/** + * UniversalSlashingSimulator - Common Types and Definitions + * + * This file contains common type definitions, macros, and includes + * used throughout the project. All platform-specific definitions + * are centralized here. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include + +namespace USS +{ + using int8 = int8_t; + using int16 = int16_t; + using int32 = int32_t; + using int64 = int64_t; + using uint8 = uint8_t; + using uint16 = uint16_t; + using uint32 = uint32_t; + using uint64 = uint64_t; + + using uintptr = uintptr_t; + using intptr = intptr_t; + + enum class EResult : uint8 + { + Success = 0, + Failed, + NotSupported, + InvalidVersion, + PatternNotFound, + HookFailed, + AlreadyInitialized, + NotInitialized, + + InvalidState, + InvalidParameter, + + InsufficientResources, + InventoryFull, + ItemNotFound, + + BuildingNotFound, + BuildLimitReached, + InvalidPlacement, + + TrapNotFound, + TrapNotReady + }; + + inline const char* ResultToString(EResult Result) + { + switch (Result) + { + case EResult::Success: return "Success"; + case EResult::Failed: return "Failed"; + case EResult::NotSupported: return "NotSupported"; + case EResult::InvalidVersion: return "InvalidVersion"; + case EResult::PatternNotFound: return "PatternNotFound"; + case EResult::HookFailed: return "HookFailed"; + case EResult::AlreadyInitialized: return "AlreadyInitialized"; + case EResult::NotInitialized: return "NotInitialized"; + case EResult::InvalidState: return "InvalidState"; + case EResult::InvalidParameter: return "InvalidParameter"; + case EResult::InsufficientResources:return "InsufficientResources"; + case EResult::InventoryFull: return "InventoryFull"; + case EResult::ItemNotFound: return "ItemNotFound"; + case EResult::BuildingNotFound: return "BuildingNotFound"; + case EResult::BuildLimitReached: return "BuildLimitReached"; + case EResult::InvalidPlacement: return "InvalidPlacement"; + case EResult::TrapNotFound: return "TrapNotFound"; + case EResult::TrapNotReady: return "TrapNotReady"; + default: return "Unknown"; + } + } + + #define USS_INTERFACE class + + #define USS_NON_COPYABLE(ClassName) \ + ClassName(const ClassName&) = delete; \ + ClassName& operator=(const ClassName&) = delete; + + #define USS_NON_MOVABLE(ClassName) \ + ClassName(ClassName&&) = delete; \ + ClassName& operator=(ClassName&&) = delete; + + #ifdef USS_DEBUG + #define USS_LOG(fmt, ...) ::USS::Log::Write(::USS::ELogLevel::Info, fmt, ##__VA_ARGS__) + #define USS_WARN(fmt, ...) ::USS::Log::Write(::USS::ELogLevel::Warning, fmt, ##__VA_ARGS__) + #define USS_ERROR(fmt, ...) ::USS::Log::Write(::USS::ELogLevel::Error, fmt, ##__VA_ARGS__) + #else + #define USS_LOG(fmt, ...) ((void)0) + #define USS_WARN(fmt, ...) ((void)0) + #define USS_ERROR(fmt, ...) ((void)0) + #endif + + #define USS_FATAL(fmt, ...) ::USS::Log::Write(::USS::ELogLevel::Fatal, fmt, ##__VA_ARGS__) + +} \ No newline at end of file diff --git a/Core/Hooks/HookTypes.h b/Core/Hooks/HookTypes.h new file mode 100644 index 0000000..617e8f0 --- /dev/null +++ b/Core/Hooks/HookTypes.h @@ -0,0 +1,376 @@ +/** + * UniversalSlashingSimulator - Hook Types + * + * Hooking system using MinHook for function detouring. + * Requires MinHook library to be linked. + */ + +#pragma once + +#include "../Common.h" +#include "../Logging/Log.h" +#include +#include +#include +#include + +namespace USS +{ + using ProcessEventFn = void(*)(void* Object, void* Function, void* Params); + + namespace Hook + { + // Internal state + namespace Detail + { + inline bool g_bInitialized = false; + inline std::mutex g_Mutex; + } + + /** + * Initialize the hook library (MinHook) + * Must be called before any other Hook functions + */ + inline EResult Initialize() + { + std::lock_guard Lock(Detail::g_Mutex); + + if (Detail::g_bInitialized) + return EResult::AlreadyInitialized; + + MH_STATUS Status = MH_Initialize(); + if (Status != MH_OK) + { + USS_ERROR("MinHook initialization failed: %s", MH_StatusToString(Status)); + return EResult::Failed; + } + + Detail::g_bInitialized = true; + USS_LOG("MinHook initialized successfully"); + return EResult::Success; + } + + /** + * Shutdown the hook library + * Removes all hooks and cleans up + */ + inline void Shutdown() + { + std::lock_guard Lock(Detail::g_Mutex); + + if (!Detail::g_bInitialized) + return; + + MH_DisableHook(MH_ALL_HOOKS); + + MH_STATUS Status = MH_Uninitialize(); + if (Status != MH_OK) + { + USS_WARN("MinHook shutdown warning: %s", MH_StatusToString(Status)); + } + else + { + USS_LOG("MinHook shutdown successfully"); + } + + Detail::g_bInitialized = false; + } + + /** + * Check if hook system is initialized + */ + inline bool IsInitialized() + { + return Detail::g_bInitialized; + } + + /** + * Create and enable a hook in one call + * @param Target - Address of function to hook + * @param Detour - Your detour function + * @param OutOriginal - Receives pointer to original function (trampoline) + * @return EResult::Success on success + */ + template + inline EResult CreateAndEnable(uintptr Target, T Detour, T* OutOriginal) + { + if (!Detail::g_bInitialized) + { + USS_ERROR("Hook::CreateAndEnable called before Initialize"); + return EResult::NotInitialized; + } + + if (Target == 0 || Detour == nullptr) + { + USS_ERROR("Hook::CreateAndEnable - invalid parameters"); + return EResult::InvalidParameter; + } + + void* pTarget = reinterpret_cast(Target); + void* pDetour = reinterpret_cast(Detour); + void** ppOriginal = reinterpret_cast(OutOriginal); + + MH_STATUS Status = MH_CreateHook(pTarget, pDetour, ppOriginal); + if (Status != MH_OK) + { + USS_ERROR("MH_CreateHook failed at 0x%llX: %s", Target, MH_StatusToString(Status)); + return EResult::HookFailed; + } + + Status = MH_EnableHook(pTarget); + if (Status != MH_OK) + { + USS_ERROR("MH_EnableHook failed at 0x%llX: %s", Target, MH_StatusToString(Status)); + MH_RemoveHook(pTarget); + return EResult::HookFailed; + } + + USS_LOG("Hook created and enabled at 0x%llX", Target); + return EResult::Success; + } + + /** + * Create a hook without enabling it + */ + template + inline EResult Create(uintptr Target, T Detour, T* OutOriginal) + { + if (!Detail::g_bInitialized) + return EResult::NotInitialized; + + void* pTarget = reinterpret_cast(Target); + void* pDetour = reinterpret_cast(Detour); + void** ppOriginal = reinterpret_cast(OutOriginal); + + MH_STATUS Status = MH_CreateHook(pTarget, pDetour, ppOriginal); + if (Status != MH_OK) + { + USS_ERROR("MH_CreateHook failed at 0x%llX: %s", Target, MH_StatusToString(Status)); + return EResult::HookFailed; + } + + USS_LOG("Hook created (disabled) at 0x%llX", Target); + return EResult::Success; + } + + /** + * Enable a previously created hook + */ + inline EResult Enable(uintptr Target) + { + if (!Detail::g_bInitialized) + return EResult::NotInitialized; + + void* pTarget = reinterpret_cast(Target); + MH_STATUS Status = MH_EnableHook(pTarget); + + if (Status != MH_OK) + { + USS_ERROR("MH_EnableHook failed at 0x%llX: %s", Target, MH_StatusToString(Status)); + return EResult::HookFailed; + } + + return EResult::Success; + } + + /** + * Disable a hook (can be re-enabled later) + */ + inline EResult Disable(uintptr Target) + { + if (!Detail::g_bInitialized) + return EResult::NotInitialized; + + void* pTarget = reinterpret_cast(Target); + MH_STATUS Status = MH_DisableHook(pTarget); + + if (Status != MH_OK) + { + USS_ERROR("MH_DisableHook failed at 0x%llX: %s", Target, MH_StatusToString(Status)); + return EResult::HookFailed; + } + + return EResult::Success; + } + + /** + * Remove a hook completely + */ + inline EResult Remove(uintptr Target) + { + if (!Detail::g_bInitialized) + return EResult::NotInitialized; + + void* pTarget = reinterpret_cast(Target); + + MH_DisableHook(pTarget); + + MH_STATUS Status = MH_RemoveHook(pTarget); + if (Status != MH_OK) + { + USS_ERROR("MH_RemoveHook failed at 0x%llX: %s", Target, MH_StatusToString(Status)); + return EResult::HookFailed; + } + + USS_LOG("Hook removed at 0x%llX", Target); + return EResult::Success; + } + + /** + * Enable all hooks + */ + inline EResult EnableAll() + { + if (!Detail::g_bInitialized) + return EResult::NotInitialized; + + MH_STATUS Status = MH_EnableHook(MH_ALL_HOOKS); + return (Status == MH_OK) ? EResult::Success : EResult::HookFailed; + } + + /** + * Disable all hooks + */ + inline EResult DisableAll() + { + if (!Detail::g_bInitialized) + return EResult::NotInitialized; + + MH_STATUS Status = MH_DisableHook(MH_ALL_HOOKS); + return (Status == MH_OK) ? EResult::Success : EResult::HookFailed; + } + + /** + * Queue a hook to be enabled (for batch enabling) + */ + inline EResult QueueEnable(uintptr Target) + { + if (!Detail::g_bInitialized) + return EResult::NotInitialized; + + void* pTarget = reinterpret_cast(Target); + MH_STATUS Status = MH_QueueEnableHook(pTarget); + return (Status == MH_OK) ? EResult::Success : EResult::HookFailed; + } + + /** + * Queue a hook to be disabled (for batch disabling) + */ + inline EResult QueueDisable(uintptr Target) + { + if (!Detail::g_bInitialized) + return EResult::NotInitialized; + + void* pTarget = reinterpret_cast(Target); + MH_STATUS Status = MH_QueueDisableHook(pTarget); + return (Status == MH_OK) ? EResult::Success : EResult::HookFailed; + } + + /** + * Apply all queued enable/disable operations + */ + inline EResult ApplyQueued() + { + if (!Detail::g_bInitialized) + return EResult::NotInitialized; + + MH_STATUS Status = MH_ApplyQueued(); + return (Status == MH_OK) ? EResult::Success : EResult::HookFailed; + } + } + + // ======================================================================== + // ProcessEvent Dispatcher + // ======================================================================== + + /** + * Simple ProcessEvent hook manager + * Manages callbacks for ProcessEvent interception + */ + class FSimpleProcessEventDispatcher + { + public: + using PreCallback = std::function; + using PostCallback = std::function; + + static FSimpleProcessEventDispatcher& Get() + { + static FSimpleProcessEventDispatcher Instance; + return Instance; + } + + // Register a pre-ProcessEvent callback + // Return false from callback to block the original function + void RegisterPre(PreCallback Callback) + { + std::lock_guard Lock(m_Mutex); + m_PreCallbacks.push_back(std::move(Callback)); + } + + // Register a post-ProcessEvent callback + void RegisterPost(PostCallback Callback) + { + std::lock_guard Lock(m_Mutex); + m_PostCallbacks.push_back(std::move(Callback)); + } + + // Called from the hook detour - dispatch to pre-callbacks + bool DispatchPre(void* Object, void* Function, void* Params) + { + bool bCallOriginal = true; + + std::vector Callbacks; + { + std::lock_guard Lock(m_Mutex); + Callbacks = m_PreCallbacks; + } + + for (const auto& Cb : Callbacks) + { + if (!Cb(Object, Function, Params)) + bCallOriginal = false; + } + return bCallOriginal; + } + + // Called from the hook detour - dispatch to post-callbacks + void DispatchPost(void* Object, void* Function, void* Params) + { + std::vector Callbacks; + { + std::lock_guard Lock(m_Mutex); + Callbacks = m_PostCallbacks; + } + + for (const auto& Cb : Callbacks) + { + Cb(Object, Function, Params); + } + } + + // Set/get original function pointer + void SetOriginal(ProcessEventFn Original) { m_pOriginal = Original; } + ProcessEventFn GetOriginal() const { return m_pOriginal; } + + void ClearCallbacks() + { + std::lock_guard Lock(m_Mutex); + m_PreCallbacks.clear(); + m_PostCallbacks.clear(); + } + + private: + FSimpleProcessEventDispatcher() : m_pOriginal(nullptr) {} + + std::mutex m_Mutex; + std::vector m_PreCallbacks; + std::vector m_PostCallbacks; + ProcessEventFn m_pOriginal; + }; + + inline FSimpleProcessEventDispatcher& GetSimpleProcessEventDispatcher() + { + return FSimpleProcessEventDispatcher::Get(); + } + +} \ No newline at end of file diff --git a/Core/Logging/Log.cpp b/Core/Logging/Log.cpp new file mode 100644 index 0000000..5bf9c25 --- /dev/null +++ b/Core/Logging/Log.cpp @@ -0,0 +1,167 @@ +/** + * UniversalSlashingSimulator - Logging System Implementation + */ + +#include "Log.h" +#include +#include + +namespace USS +{ + std::mutex Log::s_Mutex; + std::ofstream Log::s_FileStream; + ELogLevel Log::s_MinLevel = ELogLevel::Info; + bool Log::s_bConsoleEnabled = false; + bool Log::s_bFileEnabled = false; + bool Log::s_bInitialized = false; + + EResult Log::Initialize(bool bEnableConsole, const char* LogFilePath) + { + std::lock_guard Lock(s_Mutex); + + if (s_bInitialized) + return EResult::AlreadyInitialized; + + if (bEnableConsole) + { + if (AllocConsole()) + { + FILE* pFile = nullptr; + freopen_s(&pFile, "CONOUT$", "w", stdout); + freopen_s(&pFile, "CONOUT$", "w", stderr); + s_bConsoleEnabled = true; + } + } + + if (LogFilePath != nullptr) + { + s_FileStream.open(LogFilePath, std::ios::out | std::ios::trunc); + if (s_FileStream.is_open()) + { + s_bFileEnabled = true; + } + } + + s_bInitialized = true; + return EResult::Success; + } + + void Log::Shutdown() + { + std::lock_guard Lock(s_Mutex); + + if (!s_bInitialized) + return; + + if (s_bFileEnabled && s_FileStream.is_open()) + { + s_FileStream.close(); + } + + if (s_bConsoleEnabled) + { + FreeConsole(); + } + + s_bConsoleEnabled = false; + s_bFileEnabled = false; + s_bInitialized = false; + } + + void Log::Write(ELogLevel Level, const char* Format, ...) + { + va_list Args; + va_start(Args, Format); + WriteV(Level, Format, Args); + va_end(Args); + } + + void Log::WriteV(ELogLevel Level, const char* Format, va_list Args) + { + if (Level < s_MinLevel) + return; + + char Buffer[4096]; + vsnprintf(Buffer, sizeof(Buffer), Format, Args); + Buffer[sizeof(Buffer) - 1] = '\0'; + + WriteInternal(Level, Buffer); + } + + void Log::SetMinLevel(ELogLevel Level) + { + std::lock_guard Lock(s_Mutex); + s_MinLevel = Level; + } + + const char* Log::GetLevelName(ELogLevel Level) + { + switch (Level) + { + case ELogLevel::Trace: return "TRACE"; + case ELogLevel::Debug: return "DEBUG"; + case ELogLevel::Info: return "INFO"; + case ELogLevel::Warning: return "WARN"; + case ELogLevel::Error: return "ERROR"; + case ELogLevel::Fatal: return "FATAL"; + default: return "UNKNOWN"; + } + } + + void Log::WriteInternal(ELogLevel Level, const char* Message) + { + std::lock_guard Lock(s_Mutex); + + if (!s_bInitialized) + return; + + time_t Now = time(nullptr); + struct tm TimeInfo; + localtime_s(&TimeInfo, &Now); + + char Timestamp[32]; + strftime(Timestamp, sizeof(Timestamp), "%H:%M:%S", &TimeInfo); + + // Format: [HH:MM:SS] [LEVEL] Message + char FormattedMessage[4200]; + snprintf(FormattedMessage, sizeof(FormattedMessage), + "[%s] [%s] %s\n", Timestamp, GetLevelName(Level), Message); + + if (s_bConsoleEnabled) + { + HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + WORD Color = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; + + switch (Level) + { + case ELogLevel::Trace: + case ELogLevel::Debug: + Color = FOREGROUND_INTENSITY; + break; + case ELogLevel::Info: + Color = FOREGROUND_GREEN | FOREGROUND_INTENSITY; + break; + case ELogLevel::Warning: + Color = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; + break; + case ELogLevel::Error: + case ELogLevel::Fatal: + Color = FOREGROUND_RED | FOREGROUND_INTENSITY; + break; + } + + SetConsoleTextAttribute(hConsole, Color); + printf("%s", FormattedMessage); + SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + } + + if (s_bFileEnabled && s_FileStream.is_open()) + { + s_FileStream << FormattedMessage; + s_FileStream.flush(); + } + + OutputDebugStringA(FormattedMessage); + } + +} \ No newline at end of file diff --git a/Core/Logging/Log.h b/Core/Logging/Log.h new file mode 100644 index 0000000..8786add --- /dev/null +++ b/Core/Logging/Log.h @@ -0,0 +1,58 @@ +/** + * UniversalSlashingSimulator - Logging System + * + * Provides thread-safe logging with multiple output targets. + * Supports console, file, and debug output logging. + */ + +#pragma once + +#include "../Common.h" +#include +#include +#include + +namespace USS +{ + enum class ELogLevel : uint8 + { + Trace = 0, + Debug, + Info, + Warning, + Error, + Fatal + }; + + class Log + { + public: + USS_NON_COPYABLE(Log) + USS_NON_MOVABLE(Log) + + static EResult Initialize(bool bEnableConsole = true, const char* LogFilePath = nullptr); + static void Shutdown(); + + static void Write(ELogLevel Level, const char* Format, ...); + + static void WriteV(ELogLevel Level, const char* Format, va_list Args); + + static void SetMinLevel(ELogLevel Level); + + static const char* GetLevelName(ELogLevel Level); + + private: + Log() = default; + ~Log() = default; + + static void WriteInternal(ELogLevel Level, const char* Message); + + static std::mutex s_Mutex; + static std::ofstream s_FileStream; + static ELogLevel s_MinLevel; + static bool s_bConsoleEnabled; + static bool s_bFileEnabled; + static bool s_bInitialized; + }; + +} \ No newline at end of file diff --git a/Core/Memory/Memory.cpp b/Core/Memory/Memory.cpp new file mode 100644 index 0000000..4daee78 --- /dev/null +++ b/Core/Memory/Memory.cpp @@ -0,0 +1,171 @@ +/** + * UniversalSlashingSimulator - Memory Utilities Implementation + */ + +#include "Memory.h" +#include "../Logging/Log.h" +#include +#include + +namespace USS +{ + FModuleInfo Memory::s_BaseModule = { 0, 0, nullptr }; + bool Memory::s_bInitialized = false; + + EResult Memory::Initialize() + { + if (s_bInitialized) + return EResult::AlreadyInitialized; + + HMODULE hModule = GetModuleHandle(nullptr); + if (!hModule) + { + USS_ERROR("Failed to get base module handle"); + return EResult::Failed; + } + + MODULEINFO ModInfo = {}; + if (!GetModuleInformation(GetCurrentProcess(), hModule, &ModInfo, sizeof(ModInfo))) + { + USS_ERROR("Failed to get module information"); + return EResult::Failed; + } + + s_BaseModule.BaseAddress = reinterpret_cast(ModInfo.lpBaseOfDll); + s_BaseModule.Size = ModInfo.SizeOfImage; + s_BaseModule.Name = "FortniteClient-Win64-Shipping.exe"; + + USS_LOG("Memory initialized - Base: 0x%llX, Size: 0x%llX", + s_BaseModule.BaseAddress, s_BaseModule.Size); + + s_bInitialized = true; + return EResult::Success; + } + + const FModuleInfo& Memory::GetBaseModule() + { + return s_BaseModule; + } + + bool Memory::MaskCompare(const uint8* Data, const char* Pattern, const char* Mask) + { + for (; *Mask; ++Mask, ++Data, ++Pattern) + { + if (*Mask == 'x' && *Data != static_cast(*Pattern)) + return false; + } + return true; + } + + FPatternResult Memory::FindPattern(const char* Pattern, const char* Mask) + { + if (!s_bInitialized) + return { false, 0 }; + + return FindPattern(s_BaseModule.BaseAddress, s_BaseModule.Size, Pattern, Mask); + } + + FPatternResult Memory::FindPattern(uintptr Start, size_t Size, const char* Pattern, const char* Mask) + { + FPatternResult Result = { false, 0 }; + + if (!Pattern || !Mask) + return Result; + + size_t MaskLength = strlen(Mask); + if (Size < MaskLength) + return Result; + + size_t SearchSize = Size - MaskLength; + + for (size_t i = 0; i < SearchSize; ++i) + { + const uint8* Address = reinterpret_cast(Start + i); + + if (MaskCompare(Address, Pattern, Mask)) + { + Result.bFound = true; + Result.Address = Start + i; + return Result; + } + } + + return Result; + } + + FPatternResult Memory::FindPatternIDA(const char* Signature) + { + FPatternResult Result = { false, 0 }; + + if (!Signature || !s_bInitialized) + return Result; + + std::vector Pattern; + std::vector Mask; + + std::istringstream Stream(Signature); + std::string Token; + + while (Stream >> Token) + { + if (Token == "?" || Token == "??") + { + Pattern.push_back(0x00); + Mask.push_back('?'); + } + else + { + try + { + int Value = std::stoi(Token, nullptr, 16); + Pattern.push_back(static_cast(Value)); + Mask.push_back('x'); + } + catch (...) + { + USS_ERROR("Invalid signature token: %s", Token.c_str()); + return Result; + } + } + } + + if (Pattern.empty()) + return Result; + + Pattern.push_back('\0'); + Mask.push_back('\0'); + + return FindPattern(Pattern.data(), Mask.data()); + } + + uintptr Memory::ResolveRelative(uintptr Address, int32 InstructionSize, int32 OffsetPosition) + { + if (!IsValidAddress(Address)) + return 0; + + int32 Offset = 0; + if (!Read(Address + OffsetPosition, Offset)) + return 0; + + return Address + InstructionSize + Offset; + } + + bool Memory::IsValidAddress(uintptr Address) + { + if (Address == 0) + return false; + + MEMORY_BASIC_INFORMATION MemInfo = {}; + if (VirtualQuery(reinterpret_cast(Address), &MemInfo, sizeof(MemInfo)) == 0) + return false; + + if (MemInfo.State != MEM_COMMIT) + return false; + + if (MemInfo.Protect & (PAGE_NOACCESS | PAGE_GUARD)) + return false; + + return true; + } + +} \ No newline at end of file diff --git a/Core/Memory/Memory.h b/Core/Memory/Memory.h new file mode 100644 index 0000000..7c638b7 --- /dev/null +++ b/Core/Memory/Memory.h @@ -0,0 +1,114 @@ +/** + * UniversalSlashingSimulator - Memory Utilities + * + * Provides memory manipulation, pattern scanning, and module + * information utilities. All pattern scanning is abstracted + * for future external offset finder integration. + */ + +#pragma once + +#include "../Common.h" +#include + +namespace USS +{ + struct FModuleInfo + { + uintptr BaseAddress; + size_t Size; + const char* Name; + }; + + struct FPatternResult + { + bool bFound; + uintptr Address; + + explicit operator bool() const { return bFound; } + }; + + class Memory + { + public: + USS_NON_COPYABLE(Memory) + USS_NON_MOVABLE(Memory) + + static EResult Initialize(); + + static const FModuleInfo& GetBaseModule(); + + // Pattern scanning with mask + // Pattern: raw bytes to match + // Mask: 'x' = must match, '?' = wildcard + static FPatternResult FindPattern(const char* Pattern, const char* Mask); + + static FPatternResult FindPattern(uintptr Start, size_t Size, const char* Pattern, const char* Mask); + + // Pattern scanning with IDA-style signature + // Example: "48 8B 05 ?? ?? ?? ?? 48 85 C0" + static FPatternResult FindPatternIDA(const char* Signature); + + // Resolve relative address (for RIP-relative instructions) + // Address: Address containing the relative offset + // InstructionSize: Total size of the instruction + // OffsetPosition: Position of the offset within the instruction + static uintptr ResolveRelative(uintptr Address, int32 InstructionSize, int32 OffsetPosition); + + template + static bool Read(uintptr Address, T& OutValue); + + template + static bool Write(uintptr Address, const T& Value); + + static bool IsValidAddress(uintptr Address); + + private: + Memory() = default; + ~Memory() = default; + + static bool MaskCompare(const uint8* Data, const char* Pattern, const char* Mask); + + static FModuleInfo s_BaseModule; + static bool s_bInitialized; + }; + + template + bool Memory::Read(uintptr Address, T& OutValue) + { + if (!IsValidAddress(Address)) + return false; + + __try + { + OutValue = *reinterpret_cast(Address); + return true; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + return false; + } + } + + template + bool Memory::Write(uintptr Address, const T& Value) + { + DWORD OldProtect; + if (!VirtualProtect(reinterpret_cast(Address), sizeof(T), PAGE_EXECUTE_READWRITE, &OldProtect)) + return false; + + __try + { + *reinterpret_cast(Address) = Value; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + VirtualProtect(reinterpret_cast(Address), sizeof(T), OldProtect, &OldProtect); + return false; + } + + VirtualProtect(reinterpret_cast(Address), sizeof(T), OldProtect, &OldProtect); + return true; + } + +} \ No newline at end of file diff --git a/Core/Memory/PatternScanner.h b/Core/Memory/PatternScanner.h new file mode 100644 index 0000000..2369ccf --- /dev/null +++ b/Core/Memory/PatternScanner.h @@ -0,0 +1 @@ +// TODO: @timmie replace with ur offset finder or whatever lol \ No newline at end of file diff --git a/Core/Versioning/VersionInfo.h b/Core/Versioning/VersionInfo.h new file mode 100644 index 0000000..af766e3 --- /dev/null +++ b/Core/Versioning/VersionInfo.h @@ -0,0 +1,111 @@ +/** + * UniversalSlashingSimulator - Version Information + * + * Contains version structures and feature flags used throughout + * the project. The FVersionInfo struct represents detected + * engine and Fortnite version information. + */ + +#pragma once + +#include "../Common.h" + +namespace USS +{ + // Engine version ranges for feature detection + enum class EEngineGeneration : uint8 + { + Unknown = 0, + UE4_16_19, // 4.16 - 4.19 (GNames, UProperty, fixed objects) + UE4_20_22, // 4.20 - 4.22 (chunked objects) + UE4_23_24, // 4.23 - 4.24 (FNamePool) + UE4_25, // 4.25 (FField/FProperty) + UE4_26_27, // 4.26 - 4.27 (TObjectPtr prep) + UE5_0, // 5.0+ (TObjectPtr) + UE5_1_Plus // 5.1+ (further changes) + }; + + // Version information structure + struct FVersionInfo + { + // Engine version + uint32 EngineVersionMajor; // 4 or 5 + uint32 EngineVersionMinor; // 16, 19, 23, 25, 27, etc. + uint32 EngineVersionPatch; // Patch number + + // Fortnite version + double FortniteVersion; // e.g., 1.8, 8.30, 16.00 + uint32 FortniteSeasonMajor; // Season number + uint32 FortniteSeasonMinor; // Minor patch + uint32 FortniteCL; // Changelist number + + // Computed generation + EEngineGeneration Generation; + + // Feature flags (computed from version) + bool bUseFNamePool; // >= 4.23 + bool bUseFField; // >= 4.25 + bool bUseChunkedObjects; // >= 4.21 + bool bUseNewFastArraySerializer;// FN >= 8.30 + bool bUseTObjectPtr; // >= 5.0 + bool bSupportsSTW; // Has STW support + + // Initialize with defaults + FVersionInfo() + : EngineVersionMajor(0) + , EngineVersionMinor(0) + , EngineVersionPatch(0) + , FortniteVersion(0.0) + , FortniteSeasonMajor(0) + , FortniteSeasonMinor(0) + , FortniteCL(0) + , Generation(EEngineGeneration::Unknown) + , bUseFNamePool(false) + , bUseFField(false) + , bUseChunkedObjects(false) + , bUseNewFastArraySerializer(false) + , bUseTObjectPtr(false) + , bSupportsSTW(true) + {} + + // Check if version is valid + bool IsValid() const + { + return EngineVersionMajor > 0 && Generation != EEngineGeneration::Unknown; + } + + // Get engine version as string (e.g., "4.23.1") + std::string GetEngineVersionString() const + { + char Buffer[32]; + snprintf(Buffer, sizeof(Buffer), "%u.%u.%u", + EngineVersionMajor, EngineVersionMinor, EngineVersionPatch); + return Buffer; + } + + // Get Fortnite version as string (e.g., "8.30") + std::string GetFortniteVersionString() const + { + char Buffer[32]; + snprintf(Buffer, sizeof(Buffer), "%.2f", FortniteVersion); + return Buffer; + } + + // Get generation name + const char* GetGenerationName() const + { + switch (Generation) + { + case EEngineGeneration::UE4_16_19: return "UE4.16-4.19"; + case EEngineGeneration::UE4_20_22: return "UE4.20-4.22"; + case EEngineGeneration::UE4_23_24: return "UE4.23-4.24"; + case EEngineGeneration::UE4_25: return "UE4.25"; + case EEngineGeneration::UE4_26_27: return "UE4.26-4.27"; + case EEngineGeneration::UE5_0: return "UE5.0"; + case EEngineGeneration::UE5_1_Plus: return "UE5.1+"; + default: return "Unknown"; + } + } + }; + +} \ No newline at end of file diff --git a/Core/Versioning/VersionResolver.cpp b/Core/Versioning/VersionResolver.cpp new file mode 100644 index 0000000..8ae72e6 --- /dev/null +++ b/Core/Versioning/VersionResolver.cpp @@ -0,0 +1,573 @@ +/** + * UniversalSlashingSimulator - Version Resolver Implementation + * + * Based on version-scanner-example.cpp patterns and comprehensive CL research. + * Complete CL-to-version mapping for Fortnite 1.2 through 19.40. + */ + +#include "VersionResolver.h" +#include "../Memory/Memory.h" +#include "../Memory/PatternScanner.h" +#include "../Logging/Log.h" +#include + +namespace USS +{ + // Complete CL to version mapping table + const FVersionResolver::FCLMapping FVersionResolver::s_CLMappings[] = { + // ======================================================================== + // Chapter 1 - Season 1 (UE 4.16) + // ======================================================================== + { 3541083, 3681159, 4, 16, 1.20 }, // 1.2 + { 3681159, 3700114, 4, 16, 1.50 }, // 1.5 + { 3700114, 3709086, 4, 16, 1.72 }, // 1.7.2 + { 3709086, 3724489, 4, 16, 1.80 }, // 1.8 + { 3724489, 3757339, 4, 16, 1.82 }, // 1.8.2 + { 3757339, 3775276, 4, 16, 1.90 }, // 1.9 + { 3775276, 3790078, 4, 16, 1.91 }, // 1.9.1 + + // ======================================================================== + // Chapter 1 - Season 2 (UE 4.19) + // ======================================================================== + { 3790078, 3807424, 4, 19, 1.10 }, // 1.10 + { 3807424, 3821117, 4, 19, 1.11 }, // 1.11 + { 3821117, 3841827, 4, 19, 2.00 }, // 2.0 + { 3841827, 3847564, 4, 19, 2.10 }, // 2.1 + { 3847564, 3858292, 4, 19, 2.20 }, // 2.2 + { 3858292, 3870737, 4, 19, 2.30 }, // 2.3 + { 3870737, 3889387, 4, 19, 2.40 }, // 2.4 + { 3889387, 3901517, 4, 19, 2.41 }, // 2.4.1 + { 3901517, 3913157, 4, 19, 2.42 }, // 2.4.2 + { 3913157, 3922182, 4, 19, 2.50 }, // 2.5 + + // ======================================================================== + // Chapter 1 - Season 3 (UE 4.20) + // ======================================================================== + { 3922182, 3935073, 4, 20, 3.00 }, // 3.0 + { 3935073, 3942182, 4, 20, 3.10 }, // 3.1 + { 3942182, 3948073, 4, 20, 3.20 }, // 3.2 + { 3948073, 3968866, 4, 20, 3.30 }, // 3.3 + { 3968866, 3989614, 4, 20, 3.40 }, // 3.4 + { 3989614, 4008490, 4, 20, 3.50 }, // 3.5 + { 4008490, 4019403, 4, 20, 3.51 }, // 3.5.1 + { 4019403, 4039451, 4, 20, 3.52 }, // 3.5.2 + { 4039451, 4072250, 4, 20, 3.60 }, // 3.6 + + // ======================================================================== + // Chapter 1 - Season 4 (UE 4.20) + // ======================================================================== + { 4072250, 4117433, 4, 20, 4.00 }, // 4.0 + { 4117433, 4127312, 4, 20, 4.10 }, // 4.1 + { 4127312, 4166199, 4, 20, 4.20 }, // 4.2 + { 4166199, 4205896, 4, 20, 4.30 }, // 4.3 + { 4205896, 4240749, 4, 20, 4.40 }, // 4.4 + { 4240749, 4276938, 4, 20, 4.50 }, // 4.5 + + // ======================================================================== + // Chapter 1 - Season 5 (UE 4.21) + // ======================================================================== + { 4276938, 4336496, 4, 21, 5.00 }, // 5.0 + { 4336496, 4352937, 4, 21, 5.01 }, // 5.0.1 + { 4352937, 4378021, 4, 21, 5.10 }, // 5.1 + { 4378021, 4395664, 4, 21, 5.20 }, // 5.2 + { 4395664, 4417689, 4, 21, 5.21 }, // 5.2.1 + { 4417689, 4442095, 4, 21, 5.30 }, // 5.3 + { 4442095, 4461277, 4, 21, 5.40 }, // 5.4 + { 4461277, 4476098, 4, 21, 5.41 }, // 5.4.1 + + // ======================================================================== + // Chapter 1 - Season 6 (UE 4.21) + // ======================================================================== + { 4476098, 4526925, 4, 21, 6.00 }, // 6.0 + { 4526925, 4541578, 4, 21, 6.01 }, // 6.0.1 + { 4541578, 4573279, 4, 21, 6.10 }, // 6.1 + { 4573279, 4612618, 4, 21, 6.20 }, // 6.2 + { 4612618, 4629139, 4, 21, 6.21 }, // 6.2.1 + { 4629139, 4667333, 4, 21, 6.30 }, // 6.3 + { 4667333, 4683176, 4, 21, 6.31 }, // 6.3.1 + + // ======================================================================== + // Chapter 1 - Season 7 (UE 4.22) + // ======================================================================== + { 4683176, 4741202, 4, 22, 7.00 }, // 7.0 + { 4741202, 4775217, 4, 22, 7.10 }, // 7.1 + { 4775217, 4801627, 4, 22, 7.20 }, // 7.2 + { 4801627, 4834550, 4, 22, 7.30 }, // 7.3 + { 4834550, 4869070, 4, 22, 7.40 }, // 7.4 + + // ======================================================================== + // Chapter 1 - Season 8 (UE 4.22) + // NOTE: 8.30 introduces new FastArraySerializer + // ======================================================================== + { 4869070, 4900175, 4, 22, 8.00 }, // 8.0 + { 4900175, 4937820, 4, 22, 8.10 }, // 8.1 + { 4937820, 4975227, 4, 22, 8.20 }, // 8.2 + { 4975227, 5027463, 4, 22, 8.30 }, // 8.3 (NEW FAS!) + { 5027463, 5046157, 4, 22, 8.40 }, // 8.4 + { 5046157, 5076327, 4, 22, 8.50 }, // 8.5 + { 5076327, 5110300, 4, 22, 8.51 }, // 8.5.1 + + // ======================================================================== + // Chapter 1 - Season 9 (UE 4.23 - FNamePool introduced) + // ======================================================================== + { 5110300, 5176700, 4, 23, 9.00 }, // 9.0 + { 5176700, 5216303, 4, 23, 9.10 }, // 9.1 + { 5216303, 5268528, 4, 23, 9.20 }, // 9.2 + { 5268528, 5332994, 4, 23, 9.30 }, // 9.3 + { 5332994, 5372160, 4, 23, 9.40 }, // 9.4 + { 5372160, 5423082, 4, 23, 9.41 }, // 9.4.1 + + // ======================================================================== + // Chapter 1 - Season 10 (X) (UE 4.23) + // ======================================================================== + { 5423082, 5492370, 4, 23, 10.00 }, // 10.0 + { 5492370, 5545976, 4, 23, 10.10 }, // 10.1 + { 5545976, 5633945, 4, 23, 10.20 }, // 10.2 + { 5633945, 5704620, 4, 23, 10.30 }, // 10.3 + { 5704620, 5826396, 4, 23, 10.31 }, // 10.3.1 + { 5826396, 5878874, 4, 23, 10.40 }, // 10.4 + + // ======================================================================== + // Chapter 2 - Season 1 (UE 4.24) + // ======================================================================== + { 5878874, 6058028, 4, 24, 11.00 }, // 11.0 + { 6058028, 6113816, 4, 24, 11.01 }, // 11.0.1 + { 6113816, 6195466, 4, 24, 11.10 }, // 11.1 + { 6195466, 6316943, 4, 24, 11.20 }, // 11.2 + { 6316943, 6394056, 4, 24, 11.21 }, // 11.2.1 + { 6394056, 6522018, 4, 24, 11.30 }, // 11.3 + { 6522018, 6639283, 4, 24, 11.31 }, // 11.3.1 + { 6639283, 6755567, 4, 24, 11.40 }, // 11.4 + { 6755567, 6870595, 4, 24, 11.50 }, // 11.5 + + // ======================================================================== + // Chapter 2 - Season 2 (UE 4.24) + // ======================================================================== + { 6870595, 7037963, 4, 24, 12.00 }, // 12.0 + { 7037963, 7095426, 4, 24, 12.10 }, // 12.1 + { 7095426, 7190182, 4, 24, 12.20 }, // 12.2 + { 7190182, 7251970, 4, 24, 12.21 }, // 12.2.1 + { 7251970, 7351410, 4, 24, 12.30 }, // 12.3 + { 7351410, 7421103, 4, 24, 12.40 }, // 12.4 + { 7421103, 7499902, 4, 24, 12.41 }, // 12.4.1 + { 7499902, 7609292, 4, 24, 12.50 }, // 12.5 + { 7609292, 7704104, 4, 24, 12.60 }, // 12.6 + { 7704104, 7834553, 4, 24, 12.61 }, // 12.6.1 + + // ======================================================================== + // Chapter 2 - Season 3 (UE 4.24) + // ======================================================================== + { 7834553, 8008725, 4, 24, 13.00 }, // 13.0 + { 8008725, 8090709, 4, 24, 13.20 }, // 13.2 + { 8090709, 8154316, 4, 24, 13.30 }, // 13.3 + { 8154316, 8297117, 4, 24, 13.40 }, // 13.4 + + // ======================================================================== + // Chapter 2 - Season 4 (UE 4.24) + // ======================================================================== + { 8297117, 8490514, 4, 24, 14.00 }, // 14.0 + { 8490514, 8606188, 4, 24, 14.10 }, // 14.1 + { 8606188, 8723043, 4, 24, 14.20 }, // 14.2 + { 8723043, 8775446, 4, 24, 14.30 }, // 14.3 + { 8775446, 8870917, 4, 24, 14.40 }, // 14.4 + { 8870917, 9034168, 4, 24, 14.50 }, // 14.5 + { 9034168, 9141206, 4, 24, 14.60 }, // 14.6 + + // ======================================================================== + // Chapter 2 - Season 5 (UE 4.25 - FField introduced) + // ======================================================================== + { 9141206, 9449003, 4, 25, 15.00 }, // 15.0 + { 9449003, 9562734, 4, 25, 15.10 }, // 15.1 + { 9562734, 9685607, 4, 25, 15.20 }, // 15.2 + { 9685607, 9822221, 4, 25, 15.21 }, // 15.2.1 + { 9822221, 9926083, 4, 25, 15.30 }, // 15.3 + { 9926083, 10033985, 4, 25, 15.40 }, // 15.4 + { 10033985, 10127509, 4, 25, 15.50 }, // 15.5 + + // ======================================================================== + // Chapter 2 - Season 6 (UE 4.26) + // ======================================================================== + { 10127509, 10466661, 4, 26, 16.00 }, // 16.0 + { 10466661, 10639200, 4, 26, 16.10 }, // 16.1 + { 10639200, 10800459, 4, 26, 16.20 }, // 16.2 + { 10800459, 10951243, 4, 26, 16.30 }, // 16.3 + { 10951243, 11100825, 4, 26, 16.40 }, // 16.4 + { 11100825, 11203632, 4, 26, 16.50 }, // 16.5 + + // ======================================================================== + // Chapter 2 - Season 7 (UE 4.26) + // ======================================================================== + { 11203632, 11556442, 4, 26, 17.00 }, // 17.0 + { 11556442, 11724923, 4, 26, 17.10 }, // 17.1 + { 11724923, 11883027, 4, 26, 17.20 }, // 17.2 + { 11883027, 12058785, 4, 26, 17.21 }, // 17.2.1 + { 12058785, 12186007, 4, 26, 17.30 }, // 17.3 + { 12186007, 12343911, 4, 26, 17.40 }, // 17.4 + { 12343911, 12493209, 4, 26, 17.50 }, // 17.5 + + // ======================================================================== + // Chapter 2 - Season 8 (UE 4.26) + // ======================================================================== + { 12493209, 12905909, 4, 26, 18.00 }, // 18.0 + { 12905909, 13039508, 4, 26, 18.10 }, // 18.1 + { 13039508, 13206842, 4, 26, 18.20 }, // 18.2 + { 13206842, 13383027, 4, 26, 18.21 }, // 18.2.1 + { 13383027, 13498980, 4, 26, 18.30 }, // 18.3 + { 13498980, 13692932, 4, 26, 18.40 }, // 18.4 + + // ======================================================================== + // Chapter 3 - Season 1 (UE 4.27) + // ======================================================================== + { 13692932, 14211857, 5, 0, 19.00 }, // 19.0 + { 14211857, 14422223, 5, 0, 19.01 }, // 19.0.1 + { 14422223, 14550713, 5, 0, 19.10 }, // 19.1 + { 14550713, 14786821, 5, 0, 19.20 }, // 19.2 + { 14786821, 14899505, 5, 0, 19.30 }, // 19.3 + { 14899505, 19215531, 5, 0, 19.40 }, // 19.4 + }; + + const size_t FVersionResolver::s_CLMappingCount = sizeof(s_CLMappings) / sizeof(s_CLMappings[0]); + + FVersionResolver::FVersionResolver() + : m_bDetected(false) + { + } + + FVersionResolver& FVersionResolver::Get() + { + static FVersionResolver Instance; + return Instance; + } + + EResult FVersionResolver::DetectVersion() + { + if (m_bDetected) + return EResult::AlreadyInitialized; + + USS_LOG("Starting version detection..."); + + if (TryDetectFromVersionInfo()) + { + USS_LOG("Version detected from embedded version info"); + } + else if (TryDetectFromCL()) + { + USS_LOG("Version detected from CL mapping"); + } + else if (TryDetectFromPatterns()) + { + USS_LOG("Version detected from memory patterns"); + } + else + { + USS_ERROR("Failed to detect version"); + + // Show error message and exit for unsupported version + MessageBoxA( + nullptr, + "UniversalSlashingSimulator could not detect the Fortnite version.\n\n" + "This build supports Fortnite versions 1.2 through 19.40.\n\n" + "Please ensure you are running a supported version.", + "Unsupported Version", + MB_OK | MB_ICONERROR + ); + + return EResult::InvalidVersion; + } + + DetermineGeneration(); + ComputeFeatureFlags(); + + if (!SupportsVersion(m_VersionInfo)) + { + USS_ERROR("Detected version is not supported: FN %.2f", m_VersionInfo.FortniteVersion); + + char ErrorMsg[512]; + snprintf(ErrorMsg, sizeof(ErrorMsg), + "UniversalSlashingSimulator detected Fortnite %.2f (CL %u)\n\n" + "This version is not supported.\n\n" + "Supported versions: 1.2 - 19.40", + m_VersionInfo.FortniteVersion, + m_VersionInfo.FortniteCL); + + MessageBoxA(nullptr, ErrorMsg, "Unsupported Version", MB_OK | MB_ICONERROR); + return EResult::InvalidVersion; + } + + USS_LOG("Detected version: Engine %s, Fortnite %.2f (CL %u)", + m_VersionInfo.GetEngineVersionString().c_str(), + m_VersionInfo.FortniteVersion, + m_VersionInfo.FortniteCL); + + USS_LOG("Generation: %s", m_VersionInfo.GetGenerationName()); + USS_LOG("Features: FNamePool=%d, FField=%d, ChunkedObjects=%d, NewFAS=%d, TObjectPtr=%d", + m_VersionInfo.bUseFNamePool, + m_VersionInfo.bUseFField, + m_VersionInfo.bUseChunkedObjects, + m_VersionInfo.bUseNewFastArraySerializer, + m_VersionInfo.bUseTObjectPtr); + + m_bDetected = true; + return EResult::Success; + } + + const FVersionInfo& FVersionResolver::GetVersionInfo() const + { + return m_VersionInfo; + } + + bool FVersionResolver::SupportsVersion(const FVersionInfo& Info) const + { + if (Info.EngineVersionMajor == 4) + { + return Info.EngineVersionMinor >= 16 && Info.EngineVersionMinor <= 27; + } + else if (Info.EngineVersionMajor == 5) + { + return Info.EngineVersionMinor <= 2; + } + return false; + } + + bool FVersionResolver::IsVersionDetected() const + { + return m_bDetected; + } + + bool FVersionResolver::TryDetectFromVersionInfo() + { + // Try to find version string in memory + // Pattern: "++Fortnite+Release-XX.XX" or similar + + const char* VersionPattern = "2B 2B 46 6F 72 74 6E 69 74 65 2B 52 65 6C 65 61 73 65"; // "++Fortnite+Release" + auto Result = Memory::FindPatternIDA(VersionPattern); + + if (!Result) + { + USS_LOG("Version string pattern not found"); + return false; + } + + // Parse version from string after pattern + // Format: "++Fortnite+Release-XX.XX-CL-XXXXXXX" + char VersionStr[64] = {}; + for (int i = 0; i < 63; ++i) + { + char c = 0; + Memory::Read(Result.Address + i, c); + if (c == 0 || c == '-' && i > 30) + break; + VersionStr[i] = c; + } + + USS_LOG("Found version string: %s", VersionStr); + + const char* CLStart = strstr(VersionStr, "CL-"); + if (CLStart) + { + uint32 CL = 0; + if (sscanf(CLStart, "CL-%u", &CL) == 1 && CL > 0) + { + return MapCLToVersion(CL); + } + } + + return false; + } + + bool FVersionResolver::TryDetectFromCL() + { + // Try to find CL using TimmiesAwesomeOffsetFinder + // This uses pattern scanning to find the GetEngineVersion function + + // Pattern for GetEngineVersion (varies by version) + // @timmie + uintptr_t CLAddr = TAOF::FindPattern( + "48 8D 05 ?? ?? ?? ?? C3 CC CC CC CC CC CC CC CC 48 8D 05 ?? ?? ?? ?? C3" + ); + + if (CLAddr) + { + uintptr_t StringAddr = TAOF::ResolveRelativeAddress(CLAddr, 7, 3); + if (StringAddr) + { + char VersionStr[64] = {}; + for (int i = 0; i < 63; ++i) + { + char c = 0; + Memory::Read(StringAddr + i, c); + if (c == 0) + break; + VersionStr[i] = c; + } + + const char* CLStart = strstr(VersionStr, "-"); + if (CLStart) + { + uint32 CL = 0; + if (sscanf(CLStart + 1, "%u", &CL) == 1 && CL > 0) + { + USS_LOG("Found CL from version function: %u", CL); + return MapCLToVersion(CL); + } + } + } + } + + USS_LOG("CL detection via pattern failed"); + return false; + } + + bool FVersionResolver::TryDetectFromPatterns() + { + // Try to detect version from characteristic patterns + + // @timmie + + // Check for FField (UE 4.25+) + uintptr_t FFieldAddr = TAOF::FindPattern(TAOF::Patterns::ProcessEvent_423Plus); + + // Check for FNamePool (UE 4.23+) + uintptr_t FNamePoolAddr = TAOF::FindPattern(TAOF::Patterns::GNames_FNamePool); + + // Check for older GNames (Pre-4.23) + uintptr_t OldGNamesAddr = TAOF::FindPattern(TAOF::Patterns::GNames_Pre423); + + if (FFieldAddr && FNamePoolAddr) + { + // UE 4.25+ + m_VersionInfo.EngineVersionMajor = 4; + m_VersionInfo.EngineVersionMinor = 25; + m_VersionInfo.FortniteVersion = 15.00; + m_VersionInfo.FortniteCL = 9500000; + USS_LOG("Detected UE 4.25+ from FField pattern"); + return true; + } + else if (FNamePoolAddr) + { + // UE 4.23-4.24 + m_VersionInfo.EngineVersionMajor = 4; + m_VersionInfo.EngineVersionMinor = 23; + m_VersionInfo.FortniteVersion = 9.00; + m_VersionInfo.FortniteCL = 5200000; + USS_LOG("Detected UE 4.23+ from FNamePool pattern"); + return true; + } + else if (OldGNamesAddr) + { + // Pre-4.23 - check for chunked objects (4.21+) + uintptr_t ChunkedAddr = TAOF::FindPattern(TAOF::Patterns::GObjects_421Plus); + if (ChunkedAddr) + { + // UE 4.21-4.22 + m_VersionInfo.EngineVersionMajor = 4; + m_VersionInfo.EngineVersionMinor = 21; + m_VersionInfo.FortniteVersion = 5.00; + m_VersionInfo.FortniteCL = 4300000; + USS_LOG("Detected UE 4.21+ from chunked GObjects pattern"); + } + else + { + // UE 4.16-4.20 + m_VersionInfo.EngineVersionMajor = 4; + m_VersionInfo.EngineVersionMinor = 16; + m_VersionInfo.FortniteVersion = 1.80; + m_VersionInfo.FortniteCL = 3724489; + USS_LOG("Detected UE 4.16-4.20 from old GNames pattern"); + } + return true; + } + + USS_WARN("Pattern-based version detection failed, using fallback..."); + + // Default to baseline version for development/testing + m_VersionInfo.EngineVersionMajor = 4; + m_VersionInfo.EngineVersionMinor = 16; + m_VersionInfo.FortniteVersion = 1.80; + m_VersionInfo.FortniteCL = 3724489; + + return true; + } + + bool FVersionResolver::MapCLToVersion(uint32 CL) + { + for (size_t i = 0; i < s_CLMappingCount; ++i) + { + const auto& Mapping = s_CLMappings[i]; + + if (CL >= Mapping.CLMin && CL < Mapping.CLMax) + { + m_VersionInfo.FortniteCL = CL; + m_VersionInfo.EngineVersionMajor = Mapping.EngineMajor; + m_VersionInfo.EngineVersionMinor = Mapping.EngineMinor; + m_VersionInfo.FortniteVersion = Mapping.FortniteVersion; + m_VersionInfo.FortniteSeasonMajor = static_cast(Mapping.FortniteVersion); + m_VersionInfo.FortniteSeasonMinor = static_cast((Mapping.FortniteVersion - m_VersionInfo.FortniteSeasonMajor) * 100); + + USS_LOG("Mapped CL %u to Fortnite %.2f (UE %u.%u)", + CL, Mapping.FortniteVersion, Mapping.EngineMajor, Mapping.EngineMinor); + return true; + } + } + + USS_WARN("Unknown CL: %u - not in mapping table", CL); + return false; + } + + void FVersionResolver::DetermineGeneration() + { + uint32 Major = m_VersionInfo.EngineVersionMajor; + uint32 Minor = m_VersionInfo.EngineVersionMinor; + + if (Major == 4) + { + if (Minor <= 19) + m_VersionInfo.Generation = EEngineGeneration::UE4_16_19; + else if (Minor <= 22) + m_VersionInfo.Generation = EEngineGeneration::UE4_20_22; + else if (Minor <= 24) + m_VersionInfo.Generation = EEngineGeneration::UE4_23_24; + else if (Minor == 25) + m_VersionInfo.Generation = EEngineGeneration::UE4_25; + else + m_VersionInfo.Generation = EEngineGeneration::UE4_26_27; + } + else if (Major == 5) + { + if (Minor == 0) + m_VersionInfo.Generation = EEngineGeneration::UE5_0; + else + m_VersionInfo.Generation = EEngineGeneration::UE5_1_Plus; + } + else + { + m_VersionInfo.Generation = EEngineGeneration::Unknown; + } + } + + void FVersionResolver::ComputeFeatureFlags() + { + uint32 Major = m_VersionInfo.EngineVersionMajor; + uint32 Minor = m_VersionInfo.EngineVersionMinor; + double FN = m_VersionInfo.FortniteVersion; + + // FNamePool introduced in UE 4.23 (FN 9.0) + m_VersionInfo.bUseFNamePool = (Major == 4 && Minor >= 23) || Major >= 5; + + // FField introduced in UE 4.25 (FN 15.0) + m_VersionInfo.bUseFField = (Major == 4 && Minor >= 25) || Major >= 5; + + // Chunked UObject array in UE 4.21+ (FN 5.0+) + m_VersionInfo.bUseChunkedObjects = (Major == 4 && Minor >= 21) || Major >= 5; + + // New FastArraySerializer in FN 8.30+ + m_VersionInfo.bUseNewFastArraySerializer = FN >= 8.30; + + // TObjectPtr in UE 5.0+ + m_VersionInfo.bUseTObjectPtr = Major >= 5; + + // STW support (check version range) + m_VersionInfo.bSupportsSTW = FN <= 20.00; + } + +} \ No newline at end of file diff --git a/Core/Versioning/VersionResolver.h b/Core/Versioning/VersionResolver.h new file mode 100644 index 0000000..6ef6dcb --- /dev/null +++ b/Core/Versioning/VersionResolver.h @@ -0,0 +1,82 @@ +/** + * UniversalSlashingSimulator - Version Resolver + * + * Detects and resolves the current engine and Fortnite version. + * Uses pattern scanning and memory analysis to determine version. + * All version-specific behavior branching is based on this resolver. + * + * Version detection strategy: + * 1. Try to read engine version from embedded version info + * 2. Fall back to CL-based detection via pattern scanning + * 3. Map CL to known Fortnite/Engine versions + * 4. Compute feature flags based on version + */ + +#pragma once + +#include "../Common.h" +#include "VersionInfo.h" + +namespace USS +{ + USS_INTERFACE IVersionResolver + { + public: + virtual ~IVersionResolver() = default; + + virtual EResult DetectVersion() = 0; + + virtual const FVersionInfo& GetVersionInfo() const = 0; + + virtual bool SupportsVersion(const FVersionInfo& Info) const = 0; + + virtual bool IsVersionDetected() const = 0; + }; + + class FVersionResolver : public IVersionResolver + { + public: + FVersionResolver(); + ~FVersionResolver() override = default; + + USS_NON_COPYABLE(FVersionResolver) + USS_NON_MOVABLE(FVersionResolver) + + EResult DetectVersion() override; + const FVersionInfo& GetVersionInfo() const override; + bool SupportsVersion(const FVersionInfo& Info) const override; + bool IsVersionDetected() const override; + + static FVersionResolver& Get(); + + private: + bool TryDetectFromVersionInfo(); + bool TryDetectFromCL(); + bool TryDetectFromPatterns(); + + bool MapCLToVersion(uint32 CL); + void ComputeFeatureFlags(); + void DetermineGeneration(); + + FVersionInfo m_VersionInfo; + bool m_bDetected; + + struct FCLMapping + { + uint32 CLMin; + uint32 CLMax; + uint32 EngineMajor; + uint32 EngineMinor; + double FortniteVersion; + }; + + static const FCLMapping s_CLMappings[]; + static const size_t s_CLMappingCount; + }; + + inline FVersionResolver& GetVersionResolver() + { + return FVersionResolver::Get(); + } + +} \ No newline at end of file diff --git a/Engine/CoreTypes/NamePool.cpp b/Engine/CoreTypes/NamePool.cpp new file mode 100644 index 0000000..3b16725 --- /dev/null +++ b/Engine/CoreTypes/NamePool.cpp @@ -0,0 +1,330 @@ +/** + * UniversalSlashingSimulator - Name Pool Implementation + */ + +#include "NamePool.h" +#include "../../Core/Memory/Memory.h" +#include "../../Core/Logging/Log.h" +#include "../../Core/Versioning/VersionResolver.h" + +namespace USS +{ + //========================================================================= + // FGNamesArray Implementation (Pre-4.23) + //========================================================================= + + FGNamesArray::FGNamesArray() + : m_BaseAddress(0) + , m_ChunksPtr(0) + , m_NumElements(0) + , m_bInitialized(false) + { + } + + EResult FGNamesArray::Initialize(uintptr Address) + { + if (m_bInitialized) + return EResult::AlreadyInitialized; + + if (Address == 0) + return EResult::Failed; + + m_BaseAddress = Address; + + // GNames is typically a pointer to TNameEntryArray + // TNameEntryArray layout: + // FNameEntry** Chunks[128]; // 0x00 - Array of chunk pointers + // int32 NumElements; // After chunks + // int32 NumChunks; + + if (!Memory::Read(Address, m_ChunksPtr)) + { + USS_ERROR("Failed to read GNames chunks pointer"); + return EResult::Failed; + } + + // Estimate number of elements by reading chunks + // Normally, we'd scan until we find null entries + m_NumElements = 0; + + for (int32 ChunkIdx = 0; ChunkIdx < 128; ++ChunkIdx) + { + uintptr ChunkPtr = 0; + if (!Memory::Read(m_ChunksPtr + (ChunkIdx * sizeof(uintptr)), ChunkPtr)) + break; + + if (ChunkPtr == 0) + break; + + m_NumElements += ElementsPerChunk; + } + + USS_LOG("FGNamesArray initialized: ~%d estimated elements", m_NumElements); + + m_bInitialized = true; + return EResult::Success; + } + + bool FGNamesArray::GetName(int32 ComparisonIndex, FResolvedName& OutName) const + { + if (!IsValidIndex(ComparisonIndex)) + return false; + + int32 ChunkIndex = ComparisonIndex / ElementsPerChunk; + int32 WithinIndex = ComparisonIndex % ElementsPerChunk; + + uintptr ChunkPtr = 0; + if (!Memory::Read(m_ChunksPtr + (ChunkIndex * sizeof(uintptr)), ChunkPtr)) + return false; + + if (ChunkPtr == 0) + return false; + + uintptr EntryPtr = 0; + if (!Memory::Read(ChunkPtr + (WithinIndex * sizeof(uintptr)), EntryPtr)) + return false; + + if (EntryPtr == 0) + return false; + + // Read FNameEntry header + // Index field at 0x00, low bit indicates wide + int32 IndexValue = 0; + if (!Memory::Read(EntryPtr, IndexValue)) + return false; + + OutName.bIsWide = (IndexValue & 1) != 0; + + // Name data starts at offset 0x10 + uintptr NameDataAddr = EntryPtr + NameOffset; + + static char AnsiBuffer[1024]; + static wchar_t WideBuffer[1024]; + + if (OutName.bIsWide) + { + for (int32 i = 0; i < 1023; ++i) + { + wchar_t Char = 0; + if (!Memory::Read(NameDataAddr + (i * sizeof(wchar_t)), Char)) + break; + + WideBuffer[i] = Char; + if (Char == 0) + { + OutName.Length = i; + break; + } + } + WideBuffer[1023] = 0; + OutName.WideName = WideBuffer; + } + else + { + for (int32 i = 0; i < 1023; ++i) + { + char Char = 0; + if (!Memory::Read(NameDataAddr + i, Char)) + break; + + AnsiBuffer[i] = Char; + if (Char == 0) + { + OutName.Length = i; + break; + } + } + AnsiBuffer[1023] = 0; + OutName.AnsiName = AnsiBuffer; + } + + return true; + } + + std::string FGNamesArray::GetNameString(int32 ComparisonIndex) const + { + FResolvedName Name; + if (GetName(ComparisonIndex, Name)) + return Name.ToString(); + return ""; + } + + bool FGNamesArray::IsValidIndex(int32 Index) const + { + return m_bInitialized && Index >= 0 && Index < m_NumElements; + } + + int32 FGNamesArray::Num() const + { + return m_NumElements; + } + + bool FGNamesArray::IsInitialized() const + { + return m_bInitialized; + } + + //========================================================================= + // FNamePoolImpl Implementation (4.23+) + //========================================================================= + + FNamePoolImpl::FNamePoolImpl() + : m_BaseAddress(0) + , m_NumBlocks(0) + , m_bInitialized(false) + { + } + + EResult FNamePoolImpl::Initialize(uintptr Address) + { + if (m_bInitialized) + return EResult::AlreadyInitialized; + + if (Address == 0) + return EResult::Failed; + + m_BaseAddress = Address; + + // FNamePool layout: + // FNameEntryAllocator Entries; + // ... + // + // FNameEntryAllocator: + // void* Lock; // 0x00 + // uint32 CurrentBlock; // 0x08 + // uint32 CurrentByteCursor;// 0x0C + // void* Blocks[8192]; // 0x10 + + uint32 CurrentBlock = 0; + if (!Memory::Read(m_BaseAddress + 0x08, CurrentBlock)) + { + USS_ERROR("Failed to read CurrentBlock from FNamePool"); + return EResult::Failed; + } + + m_NumBlocks = CurrentBlock + 1; + + USS_LOG("FNamePoolImpl initialized: %d blocks", m_NumBlocks); + + m_bInitialized = true; + return EResult::Success; + } + + bool FNamePoolImpl::GetName(int32 ComparisonIndex, FResolvedName& OutName) const + { + if (!m_bInitialized || ComparisonIndex < 0) + return false; + + // Decode ComparisonIndex + // BlockIndex = ComparisonIndex >> 16 + // NameOffset = (ComparisonIndex & 0xFFFF) * 2 + int32 BlockIndex = ComparisonIndex >> 16; + int32 NameOffset = (ComparisonIndex & 0xFFFF) * 2; + + if (BlockIndex >= m_NumBlocks) + return false; + + uintptr BlockPtr = 0; + if (!Memory::Read(m_BaseAddress + BlocksOffset + (BlockIndex * sizeof(uintptr)), BlockPtr)) + return false; + + if (BlockPtr == 0) + return false; + + uintptr EntryAddr = BlockPtr + NameOffset; + + // Read FNameEntryHeader (2 bytes) + // bIsWide : 1 + // Len : 15 + uint16 Header = 0; + if (!Memory::Read(EntryAddr, Header)) + return false; + + OutName.bIsWide = (Header & 1) != 0; + OutName.Length = Header >> 1; + + if (OutName.Length <= 0 || OutName.Length > 1023) + return false; + + uintptr NameDataAddr = EntryAddr + sizeof(uint16); + + static char AnsiBuffer[1024]; + static wchar_t WideBuffer[1024]; + + if (OutName.bIsWide) + { + for (int32 i = 0; i < OutName.Length && i < 1023; ++i) + { + wchar_t Char = 0; + if (!Memory::Read(NameDataAddr + (i * sizeof(wchar_t)), Char)) + break; + WideBuffer[i] = Char; + } + WideBuffer[OutName.Length] = 0; + OutName.WideName = WideBuffer; + } + else + { + for (int32 i = 0; i < OutName.Length && i < 1023; ++i) + { + char Char = 0; + if (!Memory::Read(NameDataAddr + i, Char)) + break; + AnsiBuffer[i] = Char; + } + AnsiBuffer[OutName.Length] = 0; + OutName.AnsiName = AnsiBuffer; + } + + return true; + } + + std::string FNamePoolImpl::GetNameString(int32 ComparisonIndex) const + { + FResolvedName Name; + if (GetName(ComparisonIndex, Name)) + return Name.ToString(); + return ""; + } + + bool FNamePoolImpl::IsValidIndex(int32 Index) const + { + // Can't easily validate without scanning + return m_bInitialized && Index >= 0; + } + + int32 FNamePoolImpl::Num() const + { + // FNamePool doesn't track count directly + // Would need to scan to count + return -1; + } + + bool FNamePoolImpl::IsInitialized() const + { + return m_bInitialized; + } + + //========================================================================= + // Factory Function + //========================================================================= + + std::unique_ptr CreateNamePool() + { + const auto& Version = GetVersionResolver().GetVersionInfo(); + + if (Version.bUseFNamePool) + { + USS_LOG("Creating FNamePoolImpl for UE %s", + Version.GetEngineVersionString().c_str()); + return std::make_unique(); + } + else + { + USS_LOG("Creating FGNamesArray for UE %s", + Version.GetEngineVersionString().c_str()); + return std::make_unique(); + } + } + +} \ No newline at end of file diff --git a/Engine/CoreTypes/NamePool.h b/Engine/CoreTypes/NamePool.h new file mode 100644 index 0000000..e0b129f --- /dev/null +++ b/Engine/CoreTypes/NamePool.h @@ -0,0 +1,168 @@ +/** + * UniversalSlashingSimulator - Name Pool Abstraction + * + * Provides a version-agnostic interface for accessing names (FName). + * Implementations differ between: + * - Pre-4.23: GNames array (TStaticIndirectArrayThreadSafeRead) + * - 4.23+: FNamePool with packed entries + */ + +#pragma once + +#include "../../Core/Common.h" + +namespace USS +{ + struct FResolvedName + { + const char* AnsiName; + const wchar_t* WideName; + bool bIsWide; + int32 Length; + + FResolvedName() + : AnsiName(nullptr) + , WideName(nullptr) + , bIsWide(false) + , Length(0) + {} + + std::string ToString() const + { + if (bIsWide && WideName) + { + std::string Result; + for (int32 i = 0; i < Length && WideName[i]; ++i) + { + Result += static_cast(WideName[i]); + } + return Result; + } + else if (AnsiName) + { + return std::string(AnsiName, Length); + } + return ""; + } + }; + + struct FNameCompact + { + int32 ComparisonIndex; + int32 Number; + + FNameCompact() : ComparisonIndex(0), Number(0) {} + FNameCompact(int32 Index, int32 Num) : ComparisonIndex(Index), Number(Num) {} + + bool operator==(const FNameCompact& Other) const + { + return ComparisonIndex == Other.ComparisonIndex && Number == Other.Number; + } + }; + + USS_INTERFACE INamePool + { + public: + virtual ~INamePool() = default; + + // Get name by comparison index + virtual bool GetName(int32 ComparisonIndex, FResolvedName& OutName) const = 0; + + // Get name as string (convenience) + virtual std::string GetNameString(int32 ComparisonIndex) const = 0; + + // Check if index is valid + virtual bool IsValidIndex(int32 Index) const = 0; + + // Get total number of names + virtual int32 Num() const = 0; + + // Initialize from memory address + virtual EResult Initialize(uintptr Address) = 0; + + // Check if initialized + virtual bool IsInitialized() const = 0; + }; + + // GNames array implementation (Pre-4.23) + class FGNamesArray : public INamePool + { + public: + FGNamesArray(); + ~FGNamesArray() override = default; + + bool GetName(int32 ComparisonIndex, FResolvedName& OutName) const override; + std::string GetNameString(int32 ComparisonIndex) const override; + bool IsValidIndex(int32 Index) const override; + int32 Num() const override; + EResult Initialize(uintptr Address) override; + bool IsInitialized() const override; + + private: + // Internal layout: + // Chunked indirect array with 0x4000 elements per chunk + // Access: GNames.Objects[index / 0x4000][index % 0x4000] + // + // FNameEntry layout (pre-4.23): + // int32 Index; // 0x00 - Upper bits: index, LSB: wide flag + // char Pad[4]; // 0x04 + // FNameEntry* HashNext; // 0x08 + // union { + // char AnsiName[1024]; + // wchar_t WideName[1024]; + // }; // 0x10 + + static constexpr int32 ElementsPerChunk = 0x4000; // 16384 + static constexpr int32 NameOffset = 0x10; // Offset to name data + + uintptr m_BaseAddress; + uintptr m_ChunksPtr; + int32 m_NumElements; + bool m_bInitialized; + }; + + // FNamePool implementation (4.23+) + class FNamePoolImpl : public INamePool + { + public: + FNamePoolImpl(); + ~FNamePoolImpl() override = default; + + bool GetName(int32 ComparisonIndex, FResolvedName& OutName) const override; + std::string GetNameString(int32 ComparisonIndex) const override; + bool IsValidIndex(int32 Index) const override; + int32 Num() const override; + EResult Initialize(uintptr Address) override; + bool IsInitialized() const override; + + private: + // FNamePool layout (4.23+): + // struct FNameEntryAllocator { + // void* Lock; // 0x00 + // uint32 CurrentBlock; // 0x08 + // uint32 CurrentByteCursor;// 0x0C + // void* Blocks[8192]; // 0x10 - Block pointers + // } + // + // ComparisonIndex encoding: + // BlockIndex = ComparisonIndex >> 16 + // NameOffset = (ComparisonIndex & 0xFFFF) * 2 + // + // FNameEntry layout (4.23+): + // struct FNameEntryHeader { + // uint16 bIsWide : 1; + // uint16 Len : 15; + // }; + // Followed by packed name data + + static constexpr int32 MaxBlocks = 8192; + static constexpr uintptr BlocksOffset = 0x10; + + uintptr m_BaseAddress; + int32 m_NumBlocks; + bool m_bInitialized; + }; + + std::unique_ptr CreateNamePool(); + +} \ No newline at end of file diff --git a/Engine/CoreTypes/ObjectArray.cpp b/Engine/CoreTypes/ObjectArray.cpp new file mode 100644 index 0000000..21d4727 --- /dev/null +++ b/Engine/CoreTypes/ObjectArray.cpp @@ -0,0 +1,286 @@ +/** + * UniversalSlashingSimulator - Object Array Implementation + */ + +#include "ObjectArray.h" +#include "../../Core/Memory/Memory.h" +#include "../../Core/Logging/Log.h" +#include "../../Core/Versioning/VersionResolver.h" + +namespace USS +{ + //========================================================================= + // FFixedObjectArray Implementation (UE4.11-4.20) + //========================================================================= + + FFixedObjectArray::FFixedObjectArray() + : m_BaseAddress(0) + , m_ObjectsPtr(0) + , m_NumElements(0) + , m_MaxElements(0) + , m_ItemSize(0x18) // Default: Object(8) + Flags(4) + ClusterIndex(4) + SerialNumber(4) + , m_bInitialized(false) + { + } + + EResult FFixedObjectArray::Initialize(uintptr Address) + { + if (m_bInitialized) + return EResult::AlreadyInitialized; + + if (Address == 0) + return EResult::Failed; + + m_BaseAddress = Address; + + // Read fixed array layout + // Offsets are approximate and may need adjustment per-version + // struct TUObjectArray { + // FUObjectItem* Objects; // 0x00 + // int32 MaxElements; // 0x08 + // int32 NumElements; // 0x0C + // } + + // The FUObjectArray contains TUObjectArray at offset 0x10 + uintptr TUObjectArrayAddr = m_BaseAddress + 0x10; + + if (!Memory::Read(TUObjectArrayAddr + 0x00, m_ObjectsPtr)) + { + USS_ERROR("Failed to read Objects pointer from FUObjectArray"); + return EResult::Failed; + } + + if (!Memory::Read(TUObjectArrayAddr + 0x08, m_MaxElements)) + { + USS_ERROR("Failed to read MaxElements from FUObjectArray"); + return EResult::Failed; + } + + if (!Memory::Read(TUObjectArrayAddr + 0x0C, m_NumElements)) + { + USS_ERROR("Failed to read NumElements from FUObjectArray"); + return EResult::Failed; + } + + USS_LOG("FFixedObjectArray initialized: NumElements=%d, MaxElements=%d", + m_NumElements, m_MaxElements); + + m_bInitialized = true; + return EResult::Success; + } + + int32 FFixedObjectArray::Num() const + { + return m_NumElements; + } + + void* FFixedObjectArray::GetByIndex(int32 Index) const + { + if (!IsValidIndex(Index)) + return nullptr; + + uintptr ItemAddr = m_ObjectsPtr + (Index * m_ItemSize); + + void* Object = nullptr; + if (!Memory::Read(ItemAddr, Object)) + return nullptr; + + return Object; + } + + bool FFixedObjectArray::GetItemByIndex(int32 Index, FObjectItem& OutItem) const + { + if (!IsValidIndex(Index)) + return false; + + uintptr ItemAddr = m_ObjectsPtr + (Index * m_ItemSize); + + if (!Memory::Read(ItemAddr + 0x00, OutItem.Object)) + return false; + if (!Memory::Read(ItemAddr + 0x08, OutItem.Flags)) + return false; + if (!Memory::Read(ItemAddr + 0x0C, OutItem.ClusterIndex)) + return false; + if (!Memory::Read(ItemAddr + 0x10, OutItem.SerialNumber)) + return false; + + return true; + } + + bool FFixedObjectArray::IsValidIndex(int32 Index) const + { + return m_bInitialized && Index >= 0 && Index < m_NumElements; + } + + bool FFixedObjectArray::IsInitialized() const + { + return m_bInitialized; + } + + //========================================================================= + // FChunkedObjectArray Implementation (UE4.21+) + //========================================================================= + + FChunkedObjectArray::FChunkedObjectArray() + : m_BaseAddress(0) + , m_ChunksPtr(0) + , m_NumElements(0) + , m_MaxElements(0) + , m_NumChunks(0) + , m_ItemSize(0x18) + , m_bInitialized(false) + { + } + + EResult FChunkedObjectArray::Initialize(uintptr Address) + { + if (m_bInitialized) + return EResult::AlreadyInitialized; + + if (Address == 0) + return EResult::Failed; + + m_BaseAddress = Address; + + // Read chunked array layout + // struct FChunkedFixedUObjectArray { + // FUObjectItem** Objects; // 0x00 - Chunks array + // FUObjectItem* PreAllocated; // 0x08 + // int32 MaxElements; // 0x10 + // int32 NumElements; // 0x14 + // int32 MaxChunks; // 0x18 + // int32 NumChunks; // 0x1C + // } + + uintptr TUObjectArrayAddr = m_BaseAddress + 0x10; + + if (!Memory::Read(TUObjectArrayAddr + 0x00, m_ChunksPtr)) + { + USS_ERROR("Failed to read Chunks pointer from FChunkedObjectArray"); + return EResult::Failed; + } + + if (!Memory::Read(TUObjectArrayAddr + 0x10, m_MaxElements)) + { + USS_ERROR("Failed to read MaxElements from FChunkedObjectArray"); + return EResult::Failed; + } + + if (!Memory::Read(TUObjectArrayAddr + 0x14, m_NumElements)) + { + USS_ERROR("Failed to read NumElements from FChunkedObjectArray"); + return EResult::Failed; + } + + if (!Memory::Read(TUObjectArrayAddr + 0x1C, m_NumChunks)) + { + USS_ERROR("Failed to read NumChunks from FChunkedObjectArray"); + return EResult::Failed; + } + + USS_LOG("FChunkedObjectArray initialized: NumElements=%d, NumChunks=%d", + m_NumElements, m_NumChunks); + + m_bInitialized = true; + return EResult::Success; + } + + int32 FChunkedObjectArray::Num() const + { + return m_NumElements; + } + + void* FChunkedObjectArray::GetByIndex(int32 Index) const + { + if (!IsValidIndex(Index)) + return nullptr; + + int32 ChunkIndex = Index / ElementsPerChunk; + int32 WithinChunkIndex = Index % ElementsPerChunk; + + // Get chunk pointer + uintptr ChunkPtrAddr = m_ChunksPtr + (ChunkIndex * sizeof(uintptr)); + uintptr ChunkPtr = 0; + + if (!Memory::Read(ChunkPtrAddr, ChunkPtr)) + return nullptr; + + if (ChunkPtr == 0) + return nullptr; + + // Get object from chunk + uintptr ItemAddr = ChunkPtr + (WithinChunkIndex * m_ItemSize); + + void* Object = nullptr; + if (!Memory::Read(ItemAddr, Object)) + return nullptr; + + return Object; + } + + bool FChunkedObjectArray::GetItemByIndex(int32 Index, FObjectItem& OutItem) const + { + if (!IsValidIndex(Index)) + return false; + + int32 ChunkIndex = Index / ElementsPerChunk; + int32 WithinChunkIndex = Index % ElementsPerChunk; + + // Get chunk pointer + uintptr ChunkPtrAddr = m_ChunksPtr + (ChunkIndex * sizeof(uintptr)); + uintptr ChunkPtr = 0; + + if (!Memory::Read(ChunkPtrAddr, ChunkPtr)) + return false; + + if (ChunkPtr == 0) + return false; + + // Get item from chunk + uintptr ItemAddr = ChunkPtr + (WithinChunkIndex * m_ItemSize); + + if (!Memory::Read(ItemAddr + 0x00, OutItem.Object)) + return false; + if (!Memory::Read(ItemAddr + 0x08, OutItem.Flags)) + return false; + if (!Memory::Read(ItemAddr + 0x0C, OutItem.ClusterIndex)) + return false; + if (!Memory::Read(ItemAddr + 0x10, OutItem.SerialNumber)) + return false; + + return true; + } + + bool FChunkedObjectArray::IsValidIndex(int32 Index) const + { + return m_bInitialized && Index >= 0 && Index < m_NumElements; + } + + bool FChunkedObjectArray::IsInitialized() const + { + return m_bInitialized; + } + + //========================================================================= + // Factory Function + //========================================================================= + + std::unique_ptr CreateObjectArray() + { + const auto& Version = GetVersionResolver().GetVersionInfo(); + + if (Version.bUseChunkedObjects) + { + USS_LOG("Creating FChunkedObjectArray for UE %s", + Version.GetEngineVersionString().c_str()); + return std::make_unique(); + } + else + { + USS_LOG("Creating FFixedObjectArray for UE %s", + Version.GetEngineVersionString().c_str()); + return std::make_unique(); + } + } + +} \ No newline at end of file diff --git a/Engine/CoreTypes/ObjectArray.h b/Engine/CoreTypes/ObjectArray.h new file mode 100644 index 0000000..ecdc1b5 --- /dev/null +++ b/Engine/CoreTypes/ObjectArray.h @@ -0,0 +1,150 @@ +/** + * UniversalSlashingSimulator - Object Array Abstraction + * + * Provides a version-agnostic interface for accessing the global + * object array (GObjects). Implementations differ between: + * - UE4.11-4.20: Fixed direct array (FUObjectItem*) + * - UE4.21+: Chunked indirect array (FUObjectItem**) + */ + +#pragma once + +#include "../../Core/Common.h" + +namespace USS +{ + // FUObjectItem flags + enum class EInternalObjectFlags : int32 + { + None = 0, + Native = 1 << 25, + Async = 1 << 26, + AsyncLoading = 1 << 27, + Unreachable = 1 << 28, + PendingKill = 1 << 29, + RootSet = 1 << 30, + NoStrongReference = 1 << 31, + }; + + // Object item wrapper (version-agnostic) + struct FObjectItem + { + void* Object; // UObject* + int32 Flags; + int32 ClusterIndex; + int32 SerialNumber; + + bool IsUnreachable() const + { + return (Flags & static_cast(EInternalObjectFlags::Unreachable)) != 0; + } + + bool IsPendingKill() const + { + return (Flags & static_cast(EInternalObjectFlags::PendingKill)) != 0; + } + + bool IsRootSet() const + { + return (Flags & static_cast(EInternalObjectFlags::RootSet)) != 0; + } + }; + + // Object array interface + USS_INTERFACE IObjectArray + { + public: + virtual ~IObjectArray() = default; + + // Get number of objects + virtual int32 Num() const = 0; + + // Get object by index (returns UObject*) + virtual void* GetByIndex(int32 Index) const = 0; + + // Get object item by index + virtual bool GetItemByIndex(int32 Index, FObjectItem& OutItem) const = 0; + + // Check if index is valid + virtual bool IsValidIndex(int32 Index) const = 0; + + // Initialize from memory address + virtual EResult Initialize(uintptr Address) = 0; + + // Check if initialized + virtual bool IsInitialized() const = 0; + }; + + // Fixed object array implementation (UE4.11-4.20) + class FFixedObjectArray : public IObjectArray + { + public: + FFixedObjectArray(); + ~FFixedObjectArray() override = default; + + int32 Num() const override; + void* GetByIndex(int32 Index) const override; + bool GetItemByIndex(int32 Index, FObjectItem& OutItem) const override; + bool IsValidIndex(int32 Index) const override; + EResult Initialize(uintptr Address) override; + bool IsInitialized() const override; + + private: + // Internal layout for fixed array + // struct FUObjectArray { + // int32 ObjFirstGCIndex; + // int32 ObjLastNonGCIndex; + // int32 MaxObjectsNotConsideredByGC; + // bool OpenForDisregardForGC; + // FUObjectItem* Objects; // Direct pointer + // int32 MaxElements; + // int32 NumElements; + // } + + uintptr m_BaseAddress; + uintptr m_ObjectsPtr; + int32 m_NumElements; + int32 m_MaxElements; + size_t m_ItemSize; + bool m_bInitialized; + }; + + // Chunked object array implementation (UE4.21+) + class FChunkedObjectArray : public IObjectArray + { + public: + FChunkedObjectArray(); + ~FChunkedObjectArray() override = default; + + int32 Num() const override; + void* GetByIndex(int32 Index) const override; + bool GetItemByIndex(int32 Index, FObjectItem& OutItem) const override; + bool IsValidIndex(int32 Index) const override; + EResult Initialize(uintptr Address) override; + bool IsInitialized() const override; + + private: + // Internal layout for chunked array + // struct FChunkedFixedUObjectArray { + // FUObjectItem** Objects; // Pointer to chunk pointers + // FUObjectItem* PreAllocatedObjects; + // int32 MaxElements; + // int32 NumElements; + // int32 MaxChunks; + // int32 NumChunks; + // } + + static constexpr int32 ElementsPerChunk = 64 * 1024; // 65536 + + uintptr m_BaseAddress; + uintptr m_ChunksPtr; + int32 m_NumElements; + int32 m_MaxElements; + int32 m_NumChunks; + size_t m_ItemSize; + bool m_bInitialized; + }; + + std::unique_ptr CreateObjectArray(); + +} \ No newline at end of file diff --git a/Engine/CoreTypes/OffsetResolver.cpp b/Engine/CoreTypes/OffsetResolver.cpp new file mode 100644 index 0000000..debe98c --- /dev/null +++ b/Engine/CoreTypes/OffsetResolver.cpp @@ -0,0 +1,319 @@ +/** + * UniversalSlashingSimulator - Offset Resolver Implementation + * + * STUB IMPLEMENTATION + * This resolver returns placeholder offsets based on version. + * @timmie, this would be replaced by your external offset finder. + */ + +#include "OffsetResolver.h" +#include "../../Core/Logging/Log.h" +#include "../../Core/Memory/Memory.h" +#include + +namespace USS +{ + FStubOffsetResolver::FStubOffsetResolver() + : m_bResolved(false) + { + } + + FStubOffsetResolver& FStubOffsetResolver::Get() + { + static FStubOffsetResolver Instance; + return Instance; + } + + EResult FStubOffsetResolver::ResolveOffsets(const FVersionInfo& Version) + { + if (m_bResolved) + return EResult::AlreadyInitialized; + + USS_LOG("Resolving offsets for %s (FN %.2f)", + Version.GetGenerationName(), + Version.FortniteVersion); + + // Start with baseline offsets + m_Offsets = FOffsetTable(); + + // Apply version-specific adjustments + ApplyVersionSpecificOffsets(Version); + + // Attempt to resolve function addresses via patterns + // These are stubbed - actual patterns would go here + + USS_WARN("Using STUB offsets - external offset finder not connected"); + + m_bResolved = true; + return EResult::Success; + } + + void FStubOffsetResolver::ApplyVersionSpecificOffsets(const FVersionInfo& Version) + { + // Apply offsets based on engine generation + switch (Version.Generation) + { + case EEngineGeneration::UE4_16_19: + // Baseline offsets (Fortnite 1.x - 2.x) + // Controller offsets from inventory_offset_fixes.h + m_Offsets.Controller.BuildPreviewMarker = 0x1788; + m_Offsets.Controller.CurrentBuildableClass = 0x1940; + m_Offsets.Controller.PreviousBuildableClass = 0x1948; + m_Offsets.Controller.EditBuildingActor = 0x1A48; + m_Offsets.Controller.QuickBars = 0x1A88; + m_Offsets.Controller.BuildPreviewMarkerMID = 0x1928; + + // UStruct (no ChildProperties in this version) + m_Offsets.UStruct.SuperStruct = 0x30; + m_Offsets.UStruct.Children = 0x38; + m_Offsets.UStruct.ChildProperties = 0x00; // Not present + m_Offsets.UStruct.PropertiesSize = 0x40; + break; + + case EEngineGeneration::UE4_20_22: + // Season 3-8 (chunked objects) + m_Offsets.Controller.BuildPreviewMarker = 0x1800; + m_Offsets.Controller.CurrentBuildableClass = 0x19C0; + m_Offsets.Controller.PreviousBuildableClass = 0x19C8; + m_Offsets.Controller.QuickBars = 0x1B10; + + m_Offsets.UStruct.SuperStruct = 0x30; + m_Offsets.UStruct.Children = 0x38; + m_Offsets.UStruct.ChildProperties = 0x00; + m_Offsets.UStruct.PropertiesSize = 0x44; + break; + + case EEngineGeneration::UE4_23_24: + // Season 9-14 (FNamePool) + m_Offsets.Controller.BuildPreviewMarker = 0x1880; + m_Offsets.Controller.CurrentBuildableClass = 0x1A40; + m_Offsets.Controller.QuickBars = 0x1B90; + + m_Offsets.UStruct.SuperStruct = 0x30; + m_Offsets.UStruct.Children = 0x38; + m_Offsets.UStruct.ChildProperties = 0x00; + m_Offsets.UStruct.PropertiesSize = 0x48; + break; + + case EEngineGeneration::UE4_25: + // Season 15 (FField introduction) + m_Offsets.Controller.BuildPreviewMarker = 0x1900; + m_Offsets.Controller.QuickBars = 0x1C00; + + // FField now present + m_Offsets.UStruct.SuperStruct = 0x30; + m_Offsets.UStruct.Children = 0x38; // UField* (non-property) + m_Offsets.UStruct.ChildProperties = 0x40; // FField* (properties) + m_Offsets.UStruct.PropertiesSize = 0x48; + + // FField offsets + m_Offsets.FField.ClassPrivate = 0x00; + m_Offsets.FField.Owner = 0x08; + m_Offsets.FField.Next = 0x10; + m_Offsets.FField.NamePrivate = 0x18; + m_Offsets.FField.FlagsPrivate = 0x20; + + // FProperty offsets + m_Offsets.FProperty.ElementSize = 0x38; + m_Offsets.FProperty.Offset = 0x44; + m_Offsets.FProperty.PropertyFlags = 0x48; + break; + + case EEngineGeneration::UE4_26_27: + // Season 16-21 (TObjectPtr prep) + m_Offsets.Controller.BuildPreviewMarker = 0x1980; + m_Offsets.Controller.QuickBars = 0x1C80; + + m_Offsets.UStruct.SuperStruct = 0x30; + m_Offsets.UStruct.Children = 0x38; + m_Offsets.UStruct.ChildProperties = 0x40; + m_Offsets.UStruct.PropertiesSize = 0x4C; + + m_Offsets.FField.ClassPrivate = 0x00; + m_Offsets.FField.Owner = 0x08; + m_Offsets.FField.Next = 0x10; + m_Offsets.FField.NamePrivate = 0x18; + m_Offsets.FField.FlagsPrivate = 0x20; + + m_Offsets.FProperty.ElementSize = 0x38; + m_Offsets.FProperty.Offset = 0x44; + m_Offsets.FProperty.PropertyFlags = 0x48; + break; + + case EEngineGeneration::UE5_0: + case EEngineGeneration::UE5_1_Plus: + // Chapter 4+ (TObjectPtr, UE5) + m_Offsets.Controller.BuildPreviewMarker = 0x1A00; + m_Offsets.Controller.QuickBars = 0x1D00; + + // UE5 adjustments (TObjectPtr adds padding) + m_Offsets.UObject.Class = 0x10; // May change to TObjectPtr + m_Offsets.UObject.Outer = 0x20; // May change to TObjectPtr + + m_Offsets.UStruct.SuperStruct = 0x30; + m_Offsets.UStruct.Children = 0x40; + m_Offsets.UStruct.ChildProperties = 0x48; + m_Offsets.UStruct.PropertiesSize = 0x50; + + m_Offsets.FField.ClassPrivate = 0x00; + m_Offsets.FField.Owner = 0x08; + m_Offsets.FField.Next = 0x10; + m_Offsets.FField.NamePrivate = 0x18; + m_Offsets.FField.FlagsPrivate = 0x20; + + m_Offsets.FProperty.ElementSize = 0x40; + m_Offsets.FProperty.Offset = 0x4C; + m_Offsets.FProperty.PropertyFlags = 0x50; + break; + + default: + USS_WARN("Unknown engine generation, using baseline offsets"); + break; + } + + USS_LOG("Applied version-specific offsets for %s", + Version.GetGenerationName()); + } + + const FOffsetTable& FStubOffsetResolver::GetOffsets() const + { + return m_Offsets; + } + + bool FStubOffsetResolver::IsResolved() const + { + return m_bResolved; + } + + int32 FStubOffsetResolver::GetOffset(EOffsetCategory Category, const char* Name) const + { + if (!m_bResolved || !Name) + return -1; + + // Simple string matching for offset lookup + // In production, this would use a hash map + + switch (Category) + { + case EOffsetCategory::UObject: + if (strcmp(Name, "Vtable") == 0) return m_Offsets.UObject.Vtable; + if (strcmp(Name, "ObjectFlags") == 0) return m_Offsets.UObject.ObjectFlags; + if (strcmp(Name, "InternalIndex") == 0) return m_Offsets.UObject.InternalIndex; + if (strcmp(Name, "Class") == 0) return m_Offsets.UObject.Class; + if (strcmp(Name, "Name") == 0) return m_Offsets.UObject.Name; + if (strcmp(Name, "Outer") == 0) return m_Offsets.UObject.Outer; + break; + + case EOffsetCategory::UField: + // UField offsets (pre-4.25) + if (strcmp(Name, "Next") == 0) return 0x30; // Default UField::Next offset + break; + + case EOffsetCategory::UStruct: + if (strcmp(Name, "SuperStruct") == 0) return m_Offsets.UStruct.SuperStruct; + if (strcmp(Name, "Children") == 0) return m_Offsets.UStruct.Children; + if (strcmp(Name, "ChildProperties") == 0) return m_Offsets.UStruct.ChildProperties; + if (strcmp(Name, "PropertiesSize") == 0) return m_Offsets.UStruct.PropertiesSize; + if (strcmp(Name, "PropertyLink") == 0) return 0x50; // Default PropertyLink offset + break; + + case EOffsetCategory::UProperty: + // UProperty offsets (pre-4.25) + if (strcmp(Name, "ArrayDim") == 0) return 0x38; + if (strcmp(Name, "ElementSize") == 0) return 0x3C; + if (strcmp(Name, "PropertyFlags") == 0) return 0x40; + if (strcmp(Name, "Offset_Internal") == 0) return 0x4C; + break; + + case EOffsetCategory::FField: + // FField offsets (4.25+) + if (strcmp(Name, "ClassPrivate") == 0) return 0x00; + if (strcmp(Name, "Owner") == 0) return 0x08; + if (strcmp(Name, "Next") == 0) return 0x20; + if (strcmp(Name, "NamePrivate") == 0) return 0x28; + break; + + case EOffsetCategory::FProperty: + // FProperty offsets (4.25+) + if (strcmp(Name, "ArrayDim") == 0) return 0x38; + if (strcmp(Name, "ElementSize") == 0) return 0x3C; + if (strcmp(Name, "PropertyFlags") == 0) return 0x40; + if (strcmp(Name, "Offset_Internal") == 0) return 0x4C; + break; + + case EOffsetCategory::FFieldClass: + // FFieldClass offsets (4.25+) + if (strcmp(Name, "Name") == 0) return 0x00; + break; + + case EOffsetCategory::Controller: + if (strcmp(Name, "BuildPreviewMarker") == 0) return m_Offsets.Controller.BuildPreviewMarker; + if (strcmp(Name, "CurrentBuildableClass") == 0) return m_Offsets.Controller.CurrentBuildableClass; + if (strcmp(Name, "QuickBars") == 0) return m_Offsets.Controller.QuickBars; + break; + + default: + break; + } + + USS_WARN("Unknown offset: %s", Name); + return -1; + } + + int32 FStubOffsetResolver::GetOffset(const char* CategoryName, const char* Name) const + { + if (!CategoryName || !Name) + return -1; + + // Map string category to enum + EOffsetCategory Category = EOffsetCategory::UObject; + + if (strcmp(CategoryName, "UObject") == 0) + Category = EOffsetCategory::UObject; + else if (strcmp(CategoryName, "UField") == 0) + Category = EOffsetCategory::UField; + else if (strcmp(CategoryName, "UStruct") == 0) + Category = EOffsetCategory::UStruct; + else if (strcmp(CategoryName, "UClass") == 0) + Category = EOffsetCategory::UClass; + else if (strcmp(CategoryName, "UFunction") == 0) + Category = EOffsetCategory::UFunction; + else if (strcmp(CategoryName, "UProperty") == 0) + Category = EOffsetCategory::UProperty; + else if (strcmp(CategoryName, "FField") == 0) + Category = EOffsetCategory::FField; + else if (strcmp(CategoryName, "FProperty") == 0) + Category = EOffsetCategory::FProperty; + else if (strcmp(CategoryName, "FFieldClass") == 0) + Category = EOffsetCategory::FFieldClass; + else if (strcmp(CategoryName, "Controller") == 0) + Category = EOffsetCategory::Controller; + else if (strcmp(CategoryName, "Pawn") == 0) + Category = EOffsetCategory::Pawn; + else if (strcmp(CategoryName, "Actor") == 0) + Category = EOffsetCategory::Actor; + else + { + USS_WARN("Unknown category: %s", CategoryName); + return -1; + } + + return GetOffset(Category, Name); + } + + uintptr FStubOffsetResolver::GetFunctionAddress(const char* Name) const + { + if (!m_bResolved || !Name) + return 0; + + if (strcmp(Name, "GObjects") == 0) return m_Offsets.Functions.GObjects; + if (strcmp(Name, "GNames") == 0) return m_Offsets.Functions.GNames; + if (strcmp(Name, "GWorld") == 0) return m_Offsets.Functions.GWorld; + if (strcmp(Name, "ProcessEvent") == 0) return m_Offsets.Functions.ProcessEvent; + if (strcmp(Name, "StaticLoadObject") == 0) return m_Offsets.Functions.StaticLoadObject; + if (strcmp(Name, "SpawnActor") == 0) return m_Offsets.Functions.SpawnActor; + + return 0; + } + +} \ No newline at end of file diff --git a/Engine/CoreTypes/OffsetResolver.h b/Engine/CoreTypes/OffsetResolver.h new file mode 100644 index 0000000..6357f71 --- /dev/null +++ b/Engine/CoreTypes/OffsetResolver.h @@ -0,0 +1,246 @@ +/** + * UniversalSlashingSimulator - Offset Resolver + * + * Provides stubbed offset resolution for engine and game structures. + * All offsets are stubbed and designed to be replaced by an external + * offset finder library in the future. @timmie + * + * IMPORTANT: No hardcoded offsets should exist outside this module. + * All offset access must go through this resolver. + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../../Core/Versioning/VersionInfo.h" + +namespace USS +{ + // Offset categories + enum class EOffsetCategory : uint8 + { + UObject, + UField, // Pre-4.25 field base + UStruct, + UClass, + UFunction, + UProperty, // Pre-4.25 + FField, // 4.25+ + FProperty, // 4.25+ + FFieldClass, // 4.25+ field class info + Actor, // Actor base + Controller, // Player controller + Pawn, // Player pawn + Inventory, // Inventory system + Building, // Building system + Mission, // Mission system + }; + + // Complete offset table for all required offsets + struct FOffsetTable + { + // UObject offsets + struct + { + int32 Vtable; // 0x00 + int32 ObjectFlags; // 0x08 + int32 InternalIndex; // 0x0C + int32 Class; // 0x10 + int32 Name; // 0x18 + int32 Outer; // 0x20 + } UObject; + + // UField offsets (pre-4.25) + struct + { + int32 Next; // UField::Next + } UField; + + // UStruct offsets + struct + { + int32 SuperStruct; // UStruct::SuperStruct + int32 Children; // UStruct::Children (UField*) + int32 ChildProperties; // UStruct::ChildProperties (FField*, 4.25+) + int32 PropertiesSize; // UStruct::PropertiesSize + int32 MinAlignment; // UStruct::MinAlignment + } UStruct; + + // UClass offsets + struct + { + int32 ClassDefaultObject; + int32 ClassConstructor; + } UClass; + + // UFunction offsets + struct + { + int32 FunctionFlags; + int32 NumParms; + int32 ParmsSize; + int32 ReturnValueOffset; + int32 Func; // Native function pointer + } UFunction; + + // UProperty offsets (pre-4.25) + struct + { + int32 ElementSize; + int32 Offset; + int32 PropertyFlags; + int32 Next; + } UProperty; + + // FField offsets (4.25+) + struct + { + int32 ClassPrivate; + int32 Owner; + int32 Next; + int32 NamePrivate; + int32 FlagsPrivate; + } FField; + + // FProperty offsets (4.25+) + struct + { + int32 ElementSize; + int32 Offset; + int32 PropertyFlags; + } FProperty; + + // Controller offsets (version-specific) + struct + { + int32 BuildPreviewMarker; // 0x1788 in baseline + int32 CurrentBuildableClass; // 0x1940 in baseline + int32 PreviousBuildableClass; // 0x1948 in baseline + int32 EditBuildingActor; // 0x1A48 in baseline + int32 QuickBars; // 0x1A88 in baseline + int32 BuildPreviewMarkerMID; // 0x1928 in baseline + int32 CheatManager; + int32 PlayerState; + } Controller; + + // Pawn offsets + struct + { + int32 Controller; + int32 PlayerState; + int32 CustomizationLoadout; + int32 CharacterParts; + } Pawn; + + // Inventory offsets + struct + { + int32 WorldInventory; + int32 ItemInstances; + int32 ReplicatedEntries; + } Inventory; + + // Building offsets + struct + { + int32 BuildingActor; + int32 BuildingMaterial; + int32 BuildingEditMode; + } Building; + + // Global function addresses (stubbed) + struct + { + uintptr StaticConstructObject_Internal; + uintptr StaticLoadObject; + uintptr SpawnActor; + uintptr ProcessEvent; + uintptr GObjects; + uintptr GNames; + uintptr GWorld; + } Functions; + + // Initialize with default values + FOffsetTable() + { + memset(this, 0, sizeof(*this)); + + // Set known baseline offsets (UE 4.16) + // These are placeholders - actual offsets come from resolver + + // UObject (stable across versions) + UObject.Vtable = 0x00; + UObject.ObjectFlags = 0x08; + UObject.InternalIndex = 0x0C; + UObject.Class = 0x10; + UObject.Name = 0x18; + UObject.Outer = 0x20; + + // UField + UField.Next = 0x28; + + // UStruct (varies significantly) + UStruct.SuperStruct = 0x30; + UStruct.Children = 0x38; + UStruct.ChildProperties = 0x00; // 0 = not present (pre-4.25) + UStruct.PropertiesSize = 0x40; + UStruct.MinAlignment = 0x44; + } + }; + + // Offset resolver interface + USS_INTERFACE IOffsetResolver + { + public: + virtual ~IOffsetResolver() = default; + + // Resolve offsets for the current version + virtual EResult ResolveOffsets(const FVersionInfo& Version) = 0; + + // Get resolved offset table + virtual const FOffsetTable& GetOffsets() const = 0; + + // Check if offsets are resolved + virtual bool IsResolved() const = 0; + + // Get specific offset by category and name + virtual int32 GetOffset(EOffsetCategory Category, const char* Name) const = 0; + + // Get specific offset by category string and name (convenience) + virtual int32 GetOffset(const char* CategoryName, const char* Name) const = 0; + + // Get function address + virtual uintptr GetFunctionAddress(const char* Name) const = 0; + }; + + // Stub offset resolver (returns placeholder values) + class FStubOffsetResolver : public IOffsetResolver + { + public: + FStubOffsetResolver(); + ~FStubOffsetResolver() override = default; + + EResult ResolveOffsets(const FVersionInfo& Version) override; + const FOffsetTable& GetOffsets() const override; + bool IsResolved() const override; + int32 GetOffset(EOffsetCategory Category, const char* Name) const override; + int32 GetOffset(const char* CategoryName, const char* Name) const override; + uintptr GetFunctionAddress(const char* Name) const override; + + // Get singleton + static FStubOffsetResolver& Get(); + + private: + void ApplyVersionSpecificOffsets(const FVersionInfo& Version); + + FOffsetTable m_Offsets; + bool m_bResolved; + }; + + // Convenience function + inline IOffsetResolver& GetOffsetResolver() + { + return FStubOffsetResolver::Get(); + } + +} \ No newline at end of file diff --git a/Engine/EngineCore.cpp b/Engine/EngineCore.cpp new file mode 100644 index 0000000..98513f1 --- /dev/null +++ b/Engine/EngineCore.cpp @@ -0,0 +1,465 @@ +/** + * UniversalSlashingSimulator - Engine Core Implementation + */ + +#include "EngineCore.h" +#include "../Core/Memory/Memory.h" +#include "../Core/Logging/Log.h" +#include "../Core/Versioning/VersionResolver.h" +#include "../Core/Hooks/HookTypes.h" + +namespace USS +{ + FEngineCore::FEngineCore() + : m_GObjectsAddress(0) + , m_GNamesAddress(0) + , m_GWorldAddress(0) + { + } + + FEngineCore::~FEngineCore() + { + Shutdown(); + } + + FEngineCore& FEngineCore::Get() + { + static FEngineCore Instance; + return Instance; + } + + EResult FEngineCore::Initialize() + { + if (m_Status.bFullyInitialized) + return EResult::AlreadyInitialized; + + USS_LOG("=== UniversalSlashingSimulator Engine Core ==="); + USS_LOG("Initializing engine core..."); + + EResult Result = Memory::Initialize(); + if (Result != EResult::Success) + { + USS_ERROR("Failed to initialize memory utilities"); + return Result; + } + + Result = InitializeVersion(); + if (Result != EResult::Success) + { + USS_ERROR("Failed to detect version"); + return Result; + } + + Result = InitializeOffsets(); + if (Result != EResult::Success) + { + USS_ERROR("Failed to resolve offsets"); + return Result; + } + + Result = InitializeObjectArray(); + if (Result != EResult::Success) + { + USS_ERROR("Failed to initialize object array"); + return Result; + } + + Result = InitializeNamePool(); + if (Result != EResult::Success) + { + USS_ERROR("Failed to initialize name pool"); + return Result; + } + + Result = InitializeHooks(); + if (Result != EResult::Success) + { + USS_WARN("Hook initialization returned: %s (@timmie must implement)", ResultToString(Result)); + // Non-fatal - hooks are stubbed until @timmie implements + } + + m_Status.bFullyInitialized = true; + USS_LOG("Engine core initialized successfully"); + + return EResult::Success; + } + + void FEngineCore::Shutdown() + { + if (!m_Status.bFullyInitialized) + return; + + USS_LOG("Shutting down engine core..."); + + Hook::Shutdown(); + + m_pNamePool.reset(); + m_pObjectArray.reset(); + + m_Status = FEngineCoreStatus(); + + USS_LOG("Engine core shutdown complete"); + } + + EResult FEngineCore::InitializeVersion() + { + USS_LOG("Detecting engine version..."); + + EResult Result = GetVersionResolver().DetectVersion(); + if (Result != EResult::Success) + return Result; + + m_Status.bVersionResolved = true; + return EResult::Success; + } + + EResult FEngineCore::InitializeOffsets() + { + USS_LOG("Resolving offsets..."); + + const auto& Version = GetVersionResolver().GetVersionInfo(); + EResult Result = FStubOffsetResolver::Get().ResolveOffsets(Version); + if (Result != EResult::Success) + return Result; + + m_Status.bOffsetsResolved = true; + return EResult::Success; + } + + EResult FEngineCore::InitializeObjectArray() + { + USS_LOG("Initializing object array..."); + + m_GObjectsAddress = FindGObjectsAddress(); + if (m_GObjectsAddress == 0) + { + USS_ERROR("Failed to find GObjects address"); + return EResult::PatternNotFound; + } + + USS_LOG("GObjects at 0x%llX", m_GObjectsAddress); + + m_pObjectArray = CreateObjectArray(); + if (!m_pObjectArray) + { + USS_ERROR("Failed to create object array"); + return EResult::Failed; + } + + EResult Result = m_pObjectArray->Initialize(m_GObjectsAddress); + if (Result != EResult::Success) + { + USS_ERROR("Failed to initialize object array"); + return Result; + } + + USS_LOG("Object array initialized with %d objects", m_pObjectArray->Num()); + + m_Status.bObjectArrayInitialized = true; + return EResult::Success; + } + + EResult FEngineCore::InitializeNamePool() + { + USS_LOG("Initializing name pool..."); + + m_GNamesAddress = FindGNamesAddress(); + if (m_GNamesAddress == 0) + { + USS_ERROR("Failed to find GNames address"); + return EResult::PatternNotFound; + } + + USS_LOG("GNames at 0x%llX", m_GNamesAddress); + + m_pNamePool = CreateNamePool(); + if (!m_pNamePool) + { + USS_ERROR("Failed to create name pool"); + return EResult::Failed; + } + + EResult Result = m_pNamePool->Initialize(m_GNamesAddress); + if (Result != EResult::Success) + { + USS_ERROR("Failed to initialize name pool"); + return Result; + } + + USS_LOG("Name pool initialized"); + + m_Status.bNamePoolInitialized = true; + return EResult::Success; + } + + EResult FEngineCore::InitializeHooks() + { + USS_LOG("Initializing hooks (STUB - @timmie must implement with Memcury/MinHook)..."); + + EResult Result = Hook::Initialize(); + if (Result != EResult::Success) + { + USS_WARN("Hook::Initialize() returned %s", ResultToString(Result)); + } + + // TODO: @timmie creates ProcessEvent hook here + // See HookTypes.h for implementation guide + + m_Status.bHooksInitialized = true; + return EResult::Success; + } + + uintptr FEngineCore::FindGObjectsAddress() + { + const auto& Version = GetVersionResolver().GetVersionInfo(); + + // Pattern varies by version, @timmie might need to check + FPatternResult Result = { false, 0 }; + + if (Version.EngineVersionMajor == 4 && Version.EngineVersionMinor <= 22) + { + // UE 4.16-4.22 GObjects pattern + Result = Memory::FindPatternIDA( + "48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? E8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B D6" + ); + + if (Result) + { + // Resolve relative address + return Memory::ResolveRelative(Result.Address, 7, 3); + } + } + else + { + // UE 4.23+ GObjects pattern + Result = Memory::FindPatternIDA( + "48 8B 05 ?? ?? ?? ?? 48 8B 0C C8 48 8D 04 D1" + ); + + if (Result) + { + return Memory::ResolveRelative(Result.Address, 7, 3); + } + } + + USS_WARN("GObjects pattern not found, using fallback..."); + + // Fallback: Return 0 (stub - would implement alternative detection) + return 0; + } + + uintptr FEngineCore::FindGNamesAddress() + { + const auto& Version = GetVersionResolver().GetVersionInfo(); + + FPatternResult Result = { false, 0 }; + + if (Version.bUseFNamePool) + { + // UE 4.23+ FNamePool pattern + Result = Memory::FindPatternIDA( + "48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? C6 05 ?? ?? ?? ?? 01" + ); + + if (Result) + { + return Memory::ResolveRelative(Result.Address, 7, 3); + } + } + else + { + // Pre-4.23 GNames pattern + Result = Memory::FindPatternIDA( + "48 8B 05 ?? ?? ?? ?? 48 85 C0 75 50 B9" + ); + + if (Result) + { + return Memory::ResolveRelative(Result.Address, 7, 3); + } + } + + USS_WARN("GNames pattern not found, using fallback..."); + return 0; + } + + uintptr FEngineCore::FindGWorldAddress() + { + FPatternResult Result = Memory::FindPatternIDA( + "48 8B 1D ?? ?? ?? ?? ?? ?? ?? 10 4C 8D 4D ?? 4C" + ); + + if (Result) + { + return Memory::ResolveRelative(Result.Address, 7, 3); + } + + USS_WARN("GWorld pattern not found"); + return 0; + } + + const FVersionInfo& FEngineCore::GetVersionInfo() const + { + return GetVersionResolver().GetVersionInfo(); + } + + const FOffsetTable& FEngineCore::GetOffsets() const + { + return GetOffsetResolver().GetOffsets(); + } + + UObjectWrapper FEngineCore::FindObject(const char* FullName) const + { + if (!m_pObjectArray || !FullName) + return UObjectWrapper(); + + // Simple linear search - in production would use hash + int32 Num = m_pObjectArray->Num(); + for (int32 i = 0; i < Num; ++i) + { + void* Obj = m_pObjectArray->GetByIndex(i); + if (Obj) + { + UObjectWrapper Wrapper(Obj); + if (Wrapper.GetFullName() == FullName) + return Wrapper; + } + } + + return UObjectWrapper(); + } + + UObjectWrapper FEngineCore::FindObjectByName(const char* Name) const + { + if (!m_pObjectArray || !Name) + return UObjectWrapper(); + + int32 Num = m_pObjectArray->Num(); + for (int32 i = 0; i < Num; ++i) + { + void* Obj = m_pObjectArray->GetByIndex(i); + if (Obj) + { + UObjectWrapper Wrapper(Obj); + if (Wrapper.GetName() == Name) + return Wrapper; + } + } + + return UObjectWrapper(); + } + + UClassWrapper FEngineCore::FindClass(const char* ClassName) const + { + if (!m_pObjectArray || !ClassName) + return UClassWrapper(); + + // Search for Class with matching name + std::string TargetName = ClassName; + int32 Num = m_pObjectArray->Num(); + + for (int32 i = 0; i < Num; ++i) + { + void* Obj = m_pObjectArray->GetByIndex(i); + if (Obj) + { + UObjectWrapper Wrapper(Obj); + + // Check if this is a UClass + if (Wrapper.IsA("Class")) + { + if (Wrapper.GetName() == TargetName) + return UClassWrapper(Obj); + } + } + } + + return UClassWrapper(); + } + + void* FEngineCore::FindLocalPlayerController() const + { + // Search for the local player controller + // Typically named "PlayerController" or "FortPlayerController" + if (!m_pObjectArray) + return nullptr; + + int32 Num = m_pObjectArray->Num(); + for (int32 i = 0; i < Num; ++i) + { + void* Obj = m_pObjectArray->GetByIndex(i); + if (Obj) + { + UObjectWrapper Wrapper(Obj); + std::string ClassName = Wrapper.GetObjectClassName(); + + // Look for FortPlayerController or FortPlayerControllerAthena + if (ClassName.find("FortPlayerController") != std::string::npos && + ClassName.find("_C") != std::string::npos) + { + return Obj; + } + } + } + + return nullptr; + } + + void* FEngineCore::GetWorld() const + { + if (m_GWorldAddress == 0) + return nullptr; + + void* World = nullptr; + Memory::Read(m_GWorldAddress, World); + return World; + } + + std::string FEngineCore::GetObjectName(void* Object) const + { + if (!Object) + return ""; + + UObjectWrapper Wrapper(Object); + return Wrapper.GetName(); + } + + std::string FEngineCore::GetObjectClassName(void* Object) const + { + if (!Object) + return ""; + + UObjectWrapper Wrapper(Object); + return Wrapper.GetObjectClassName(); + } + + void* FEngineCore::GetObjectClass(void* Object) const + { + if (!Object) + return nullptr; + + // Read UClass* from UObject (typically at offset 0x10) + const auto& Offsets = GetOffsets(); + int32 ClassOffset = Offsets.UObject.Class; + if (ClassOffset == 0) + ClassOffset = 0x10; // Default offset + + void* Class = nullptr; + Memory::Read(reinterpret_cast(Object) + ClassOffset, Class); + return Class; + } + + std::string FEngineCore::GetNameFromIndex(int32 NameIndex) const + { + if (!m_pNamePool) + return ""; + + return m_pNamePool->GetNameString(NameIndex); + } + + IOffsetResolver& FEngineCore::GetOffsetResolver() const + { + return FStubOffsetResolver::Get(); + } + +} \ No newline at end of file diff --git a/Engine/EngineCore.h b/Engine/EngineCore.h new file mode 100644 index 0000000..4d468e4 --- /dev/null +++ b/Engine/EngineCore.h @@ -0,0 +1,138 @@ +/** + * UniversalSlashingSimulator - Engine Core + * + * Central initialization and management of all engine abstractions. + * Provides unified access to version resolver, object array, name pool, + * and other core engine systems. + */ + +#pragma once + +#include "../Core/Common.h" +#include "../Core/Versioning/VersionInfo.h" +#include "CoreTypes/ObjectArray.h" +#include "CoreTypes/NamePool.h" +#include "CoreTypes/OffsetResolver.h" +#include "UObject/UObjectWrapper.h" +#include + +namespace USS +{ + // Engine core initialization status + struct FEngineCoreStatus + { + bool bVersionResolved; + bool bOffsetsResolved; + bool bObjectArrayInitialized; + bool bNamePoolInitialized; + bool bHooksInitialized; + bool bFullyInitialized; + + FEngineCoreStatus() + : bVersionResolved(false) + , bOffsetsResolved(false) + , bObjectArrayInitialized(false) + , bNamePoolInitialized(false) + , bHooksInitialized(false) + , bFullyInitialized(false) + {} + }; + + // Engine core manager + class FEngineCore + { + public: + USS_NON_COPYABLE(FEngineCore) + USS_NON_MOVABLE(FEngineCore) + + // Get singleton instance + static FEngineCore& Get(); + + // Initialize engine core (all subsystems) + EResult Initialize(); + + // Shutdown engine core + void Shutdown(); + + // Get initialization status + const FEngineCoreStatus& GetStatus() const { return m_Status; } + + // Check if fully initialized + bool IsInitialized() const { return m_Status.bFullyInitialized; } + + // Access to core systems + const FVersionInfo& GetVersionInfo() const; + const FOffsetTable& GetOffsets() const; + IObjectArray* GetObjectArray() const { return m_pObjectArray.get(); } + INamePool* GetNamePool() const { return m_pNamePool.get(); } + + // Object lookup utilities + UObjectWrapper FindObject(const char* FullName) const; + UObjectWrapper FindObjectByName(const char* Name) const; + UClassWrapper FindClass(const char* ClassName) const; + + // Player/World utilities + void* FindLocalPlayerController() const; + void* GetWorld() const; + + // Object name utilities + std::string GetObjectName(void* Object) const; + std::string GetObjectClassName(void* Object) const; + void* GetObjectClass(void* Object) const; + std::string GetNameFromIndex(int32 NameIndex) const; + + // Offset resolver access + IOffsetResolver& GetOffsetResolver() const; + + // Object iteration + template + void ForEachObject(Callback&& Func) const + { + if (!m_pObjectArray) + return; + + int32 Num = m_pObjectArray->Num(); + for (int32 i = 0; i < Num; ++i) + { + void* Obj = m_pObjectArray->GetByIndex(i); + if (Obj) + { + if (!Func(UObjectWrapper(Obj))) + break; // Callback returned false, stop iteration + } + } + } + + private: + FEngineCore(); + ~FEngineCore(); + + // Initialization steps + EResult InitializeVersion(); + EResult InitializeOffsets(); + EResult InitializeObjectArray(); + EResult InitializeNamePool(); + EResult InitializeHooks(); + + // Find addresses via patterns + uintptr FindGObjectsAddress(); + uintptr FindGNamesAddress(); + uintptr FindGWorldAddress(); + + FEngineCoreStatus m_Status; + + std::unique_ptr m_pObjectArray; + std::unique_ptr m_pNamePool; + + uintptr m_GObjectsAddress; + uintptr m_GNamesAddress; + uintptr m_GWorldAddress; + }; + + // Convenience function + inline FEngineCore& GetEngineCore() + { + return FEngineCore::Get(); + } + +} \ No newline at end of file diff --git a/Engine/Events/ProcessEventDispatcher.cpp b/Engine/Events/ProcessEventDispatcher.cpp new file mode 100644 index 0000000..69ad3d2 --- /dev/null +++ b/Engine/Events/ProcessEventDispatcher.cpp @@ -0,0 +1,492 @@ +/** + * UniversalSlashingSimulator - ProcessEvent Dispatcher Implementation + */ + +#include "ProcessEventDispatcher.h" +#include "../../Core/Memory/Memory.h" +#include "../../Core/Logging/Log.h" +#include "../EngineCore.h" +#include + +namespace USS +{ + //========================================================================= + // FProcessEventContext Methods + //========================================================================= + + const FParsedParameter* FProcessEventContext::GetParam(const char* Name) const + { + if (!Name) + return nullptr; + + for (const auto& Param : Params) + { + if (Param.Name == Name) + { + return &Param; + } + } + return nullptr; + } + + //========================================================================= + // FEventFilter Methods + //========================================================================= + + bool FEventFilter::Matches(const FProcessEventContext& Context) const + { + if (!ObjectClassFilter.empty()) + { + if (Context.ObjectClassName.find(ObjectClassFilter) == std::string::npos) + return false; + } + + if (!FunctionNameFilter.empty()) + { + if (Context.FunctionName != FunctionNameFilter) + return false; + } + + if (!FunctionNamePrefix.empty()) + { + if (Context.FunctionName.find(FunctionNamePrefix) != 0) + return false; + } + + if (bServerOnly && !Context.bIsRPC) + return false; + + if (bClientOnly && Context.bIsRPC) + return false; + + return true; + } + + //========================================================================= + // FProcessEventDispatcher Implementation + //========================================================================= + + static FProcessEventDispatcher g_ProcessEventDispatcher; + + FProcessEventDispatcher& GetProcessEventDispatcher() + { + return g_ProcessEventDispatcher; + } + + FProcessEventDispatcher::FProcessEventDispatcher() + : m_bInitialized(false) + , m_NextHandlerId(1) + , m_bHandlersDirty(false) + , m_TotalEventsProcessed(0) + , m_TotalEventsHandled(0) + , m_TotalEventsBlocked(0) + { + } + + FProcessEventDispatcher::~FProcessEventDispatcher() + { + Shutdown(); + } + + EResult FProcessEventDispatcher::Initialize() + { + if (m_bInitialized) + return EResult::AlreadyInitialized; + + USS_LOG("Initializing ProcessEvent Dispatcher..."); + + if (!GetPropertyIterator().IsInitialized()) + { + EResult Result = GetPropertyIterator().Initialize(); + if (Result != EResult::Success) + { + USS_ERROR("Failed to initialize property iterator"); + return Result; + } + } + + m_Handlers.clear(); + m_FunctionCache.clear(); + m_TotalEventsProcessed = 0; + m_TotalEventsHandled = 0; + m_TotalEventsBlocked = 0; + + USS_LOG("ProcessEvent Dispatcher initialized"); + + m_bInitialized = true; + return EResult::Success; + } + + void FProcessEventDispatcher::Shutdown() + { + if (!m_bInitialized) + return; + + USS_LOG("Shutting down ProcessEvent Dispatcher..."); + USS_LOG(" Events processed: %llu", m_TotalEventsProcessed); + USS_LOG(" Events handled: %llu", m_TotalEventsHandled); + USS_LOG(" Events blocked: %llu", m_TotalEventsBlocked); + + m_Handlers.clear(); + m_FunctionCache.clear(); + + m_bInitialized = false; + } + + bool FProcessEventDispatcher::OnProcessEvent(void* Object, void* Function, void* Parameters) + { + if (!m_bInitialized) + return true; + + ++m_TotalEventsProcessed; + + if (m_bHandlersDirty) + { + SortHandlers(); + } + + if (m_Handlers.empty()) + return true; + + FProcessEventContext Context; + Context.Object = Object; + Context.Function = Function; + Context.Parameters = Parameters; + + Context.ObjectName = GetEngineCore().GetObjectName(Object); + Context.ObjectClassName = GetEngineCore().GetObjectClassName(Object); + Context.FunctionName = GetEngineCore().GetObjectName(Function); + + Context.bIsRPC = (Context.FunctionName.find("Server") == 0) || + (Context.FunctionName.find("Client") == 0); + Context.bIsMulticast = (Context.FunctionName.find("Multicast") != std::string::npos); + + bool bNeedsParsing = false; + for (const auto& Handler : m_Handlers) + { + if (Handler.bEnabled && Handler.Filter.Matches(Context)) + { + bNeedsParsing = true; + break; + } + } + + if (bNeedsParsing) + { + ParseParameters(Function, Parameters, Context); + } + + bool bAllowExecution = true; + + for (const auto& Handler : m_Handlers) + { + if (!Handler.bEnabled) + continue; + + if (!Handler.Filter.Matches(Context)) + continue; + + ++m_TotalEventsHandled; + + bool bContinue = Handler.Handler(Context); + + if (!bContinue) + { + bAllowExecution = false; + ++m_TotalEventsBlocked; + break; + } + } + + return bAllowExecution; + } + + int32 FProcessEventDispatcher::RegisterHandler( + const std::string& Name, + const FEventFilter& Filter, + FProcessEventHandler Handler, + int32 Priority) + { + if (!Handler) + return 0; + + FRegisteredHandler Registration; + Registration.HandlerId = m_NextHandlerId++; + Registration.Name = Name; + Registration.Filter = Filter; + Registration.Handler = std::move(Handler); + Registration.Priority = Priority; + Registration.bEnabled = true; + + m_Handlers.push_back(std::move(Registration)); + m_bHandlersDirty = true; + + USS_LOG("Registered ProcessEvent handler: %s (ID: %d, Priority: %d)", + Name.c_str(), Registration.HandlerId, Priority); + + return Registration.HandlerId; + } + + void FProcessEventDispatcher::UnregisterHandler(int32 HandlerId) + { + auto It = std::remove_if(m_Handlers.begin(), m_Handlers.end(), + [HandlerId](const FRegisteredHandler& H) { + return H.HandlerId == HandlerId; + }); + + if (It != m_Handlers.end()) + { + USS_LOG("Unregistered ProcessEvent handler ID: %d", HandlerId); + m_Handlers.erase(It, m_Handlers.end()); + } + } + + void FProcessEventDispatcher::SetHandlerEnabled(int32 HandlerId, bool bEnabled) + { + for (auto& Handler : m_Handlers) + { + if (Handler.HandlerId == HandlerId) + { + Handler.bEnabled = bEnabled; + USS_LOG("Handler %d %s", HandlerId, bEnabled ? "enabled" : "disabled"); + break; + } + } + } + + bool FProcessEventDispatcher::ParseParameters(void* Function, void* Parameters, FProcessEventContext& OutContext) + { + if (!Function || !Parameters) + return false; + + std::vector ParamInfos; + if (!GetFunctionInfo(Function, ParamInfos)) + return false; + + OutContext.Params.clear(); + OutContext.Params.reserve(ParamInfos.size()); + + for (const auto& PropInfo : ParamInfos) + { + if (!(PropInfo.PropertyFlags & EPropertyFlags::CPF_Parm)) + continue; + + FParsedParameter Param; + if (ReadParameterValue(Parameters, PropInfo, Param)) + { + if (PropInfo.PropertyFlags & EPropertyFlags::CPF_ReturnParm) + { + OutContext.bHasReturnValue = true; + } + OutContext.Params.push_back(std::move(Param)); + } + } + + return true; + } + + bool FProcessEventDispatcher::ReadParameterValue(void* Parameters, const FPropertyInfo& PropInfo, FParsedParameter& OutParam) + { + if (!Parameters) + return false; + + OutParam.Name = PropInfo.Name; + OutParam.Type = PropInfo.Type; + OutParam.Offset = PropInfo.Offset; + OutParam.Size = PropInfo.ElementSize; + OutParam.Flags = PropInfo.PropertyFlags; + + uintptr ParamAddr = reinterpret_cast(Parameters) + PropInfo.Offset; + + switch (PropInfo.Type) + { + case EPropertyType::BoolProperty: + Memory::Read(ParamAddr, OutParam.BoolValue); + break; + + case EPropertyType::ByteProperty: + case EPropertyType::Int8Property: + { + int8 Value = 0; + Memory::Read(ParamAddr, Value); + OutParam.IntValue = Value; + break; + } + + case EPropertyType::Int16Property: + case EPropertyType::UInt16Property: + { + int16 Value = 0; + Memory::Read(ParamAddr, Value); + OutParam.IntValue = Value; + break; + } + + case EPropertyType::IntProperty: + case EPropertyType::UInt32Property: + Memory::Read(ParamAddr, OutParam.IntValue); + break; + + case EPropertyType::Int64Property: + case EPropertyType::UInt64Property: + Memory::Read(ParamAddr, OutParam.Int64Value); + break; + + case EPropertyType::FloatProperty: + Memory::Read(ParamAddr, OutParam.FloatValue); + break; + + case EPropertyType::DoubleProperty: + Memory::Read(ParamAddr, OutParam.DoubleValue); + break; + + case EPropertyType::ObjectProperty: + case EPropertyType::ClassProperty: + case EPropertyType::InterfaceProperty: + case EPropertyType::WeakObjectProperty: + case EPropertyType::LazyObjectProperty: + case EPropertyType::SoftObjectProperty: + Memory::Read(ParamAddr, OutParam.PointerValue); + break; + + case EPropertyType::NameProperty: + { + // FName is typically int32 ComparisonIndex + int32 Number + int32 NameIndex = 0; + Memory::Read(ParamAddr, NameIndex); + OutParam.StringValue = GetEngineCore().GetNameFromIndex(NameIndex); + break; + } + + case EPropertyType::StrProperty: + { + // FString is TArray + // Try to read as FString + uintptr DataPtr = 0; + int32 Len = 0; + Memory::Read(ParamAddr, DataPtr); + Memory::Read(ParamAddr + sizeof(uintptr), Len); + + if (DataPtr && Len > 0 && Len < 4096) + { + // Read as wide string + std::wstring WideStr(Len, L'\0'); + for (int32 i = 0; i < Len; ++i) + { + wchar_t Ch = 0; + Memory::Read(DataPtr + (i * sizeof(wchar_t)), Ch); + WideStr[i] = Ch; + } + + // Convert to narrow + OutParam.StringValue.reserve(Len); + for (wchar_t Ch : WideStr) + { + if (Ch == 0) break; + OutParam.StringValue += static_cast(Ch); + } + } + break; + } + + case EPropertyType::StructProperty: + // Store pointer to struct data + OutParam.PointerValue = reinterpret_cast(ParamAddr); + break; + + case EPropertyType::ArrayProperty: + // Store pointer to array + OutParam.PointerValue = reinterpret_cast(ParamAddr); + break; + + default: + // Store raw pointer for unknown types + OutParam.PointerValue = reinterpret_cast(ParamAddr); + break; + } + + return true; + } + + bool FProcessEventDispatcher::WriteParameterValue(void* Parameters, const FPropertyInfo& PropInfo, const FParsedParameter& Param) + { + if (!Parameters) + return false; + + uintptr ParamAddr = reinterpret_cast(Parameters) + PropInfo.Offset; + + switch (PropInfo.Type) + { + case EPropertyType::BoolProperty: + return Memory::Write(ParamAddr, Param.BoolValue); + + case EPropertyType::IntProperty: + return Memory::Write(ParamAddr, Param.IntValue); + + case EPropertyType::Int64Property: + return Memory::Write(ParamAddr, Param.Int64Value); + + case EPropertyType::FloatProperty: + return Memory::Write(ParamAddr, Param.FloatValue); + + case EPropertyType::DoubleProperty: + return Memory::Write(ParamAddr, Param.DoubleValue); + + case EPropertyType::ObjectProperty: + return Memory::Write(ParamAddr, Param.PointerValue); + + default: + return false; + } + } + + bool FProcessEventDispatcher::GetFunctionInfo(void* Function, std::vector& OutParams) + { + if (!Function) + return false; + + // Check cache + auto It = m_FunctionCache.find(Function); + if (It != m_FunctionCache.end()) + { + OutParams = It->second; + return true; + } + + // Parse function parameters using property iterator + std::vector Params; + + GetPropertyIterator().ForEachProperty(Function, + [&Params](const FPropertyInfo& Info) -> bool { + if (Info.PropertyFlags & EPropertyFlags::CPF_Parm) + { + Params.push_back(Info); + } + return true; // Continue + }, + false); // Don't include super (functions don't inherit params) + + // Sort by offset to ensure correct parameter order + std::sort(Params.begin(), Params.end(), + [](const FPropertyInfo& A, const FPropertyInfo& B) { + return A.Offset < B.Offset; + }); + + // Cache result + m_FunctionCache[Function] = Params; + OutParams = std::move(Params); + + return true; + } + + void FProcessEventDispatcher::SortHandlers() + { + std::sort(m_Handlers.begin(), m_Handlers.end(), + [](const FRegisteredHandler& A, const FRegisteredHandler& B) { + return A.Priority > B.Priority; + }); + + m_bHandlersDirty = false; + } + +} \ No newline at end of file diff --git a/Engine/Events/ProcessEventDispatcher.h b/Engine/Events/ProcessEventDispatcher.h new file mode 100644 index 0000000..0361f84 --- /dev/null +++ b/Engine/Events/ProcessEventDispatcher.h @@ -0,0 +1,403 @@ +/** + * UniversalSlashingSimulator - ProcessEvent Dispatcher + * + * Provides version-agnostic ProcessEvent handling with automatic + * parameter parsing based on UFunction reflection. + * + * ProcessEvent signature varies slightly between versions: + * - All versions: void ProcessEvent(UFunction*, void* Parms) + * + * Parameter handling varies: + * - Pre-4.25: Parameters parsed via UProperty chain + * - 4.25+: Parameters parsed via FProperty chain + * + * This dispatcher abstracts the version differences and provides + * type-safe parameter access for hooked functions. + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../../Core/Hooks/HookTypes.h" +#include "../Reflection/PropertyIterator.h" +#include +#include +#include +#include + +namespace USS +{ + /** + * Parsed function parameter + */ + struct FParsedParameter + { + std::string Name; + EPropertyType Type; + int32 Offset; + int32 Size; + uint64 Flags; + + // Value storage (for common types) + union + { + bool BoolValue; + int32 IntValue; + int64 Int64Value; + float FloatValue; + double DoubleValue; + void* PointerValue; + }; + + // For string/name types + std::string StringValue; + + FParsedParameter() + : Type(EPropertyType::Unknown) + , Offset(0) + , Size(0) + , Flags(0) + , PointerValue(nullptr) + {} + + bool IsOutParam() const + { + return (Flags & EPropertyFlags::CPF_OutParm) != 0; + } + + bool IsReturnParam() const + { + return (Flags & EPropertyFlags::CPF_ReturnParm) != 0; + } + + bool IsReferenceParam() const + { + return (Flags & EPropertyFlags::CPF_ReferenceParm) != 0; + } + }; + + /** + * Parsed ProcessEvent call context + */ + struct FProcessEventContext + { + void* Object; // UObject* calling ProcessEvent + void* Function; // UFunction* being called + void* Parameters; // Raw parameter block + + std::string ObjectName; + std::string ObjectClassName; + std::string FunctionName; + + std::vector Params; + + // Timing info + double Timestamp; + + // Flags + bool bIsRPC; // Remote procedure call + bool bIsMulticast; // Multicast delegate + bool bHasReturnValue; // Function has return value + + FProcessEventContext() + : Object(nullptr) + , Function(nullptr) + , Parameters(nullptr) + , Timestamp(0.0) + , bIsRPC(false) + , bIsMulticast(false) + , bHasReturnValue(false) + {} + + /** + * Get parameter by name + */ + const FParsedParameter* GetParam(const char* Name) const; + + /** + * Get parameter value by name (templated for type safety) + */ + template + bool GetParamValue(const char* Name, T& OutValue) const; + + /** + * Set output parameter value + */ + template + bool SetOutParamValue(const char* Name, const T& Value); + }; + + /** + * ProcessEvent handler callback type + */ + using FProcessEventHandler = std::function; + + /** + * Event filter for selective handling + */ + struct FEventFilter + { + std::string ObjectClassFilter; // Match object class name (empty = all) + std::string FunctionNameFilter; // Match function name (empty = all) + std::string FunctionNamePrefix; // Match function name prefix + bool bServerOnly; // Only server RPC calls + bool bClientOnly; // Only client RPC calls + + FEventFilter() + : bServerOnly(false) + , bClientOnly(false) + {} + + bool Matches(const FProcessEventContext& Context) const; + }; + + /** + * Registered event handler + */ + struct FRegisteredHandler + { + int32 HandlerId; + std::string Name; + FEventFilter Filter; + FProcessEventHandler Handler; + int32 Priority; // Higher = called first + bool bEnabled; + + FRegisteredHandler() + : HandlerId(0) + , Priority(0) + , bEnabled(true) + {} + }; + + /** + * ProcessEvent Dispatcher + * + * Central hub for ProcessEvent interception. Parses function + * parameters using version-appropriate reflection and dispatches + * to registered handlers. + */ + class FProcessEventDispatcher + { + public: + FProcessEventDispatcher(); + ~FProcessEventDispatcher(); + + /** + * Initialize the dispatcher + */ + EResult Initialize(); + + /** + * Shutdown and cleanup + */ + void Shutdown(); + + /** + * Main entry point - called from ProcessEvent hook + * @param Object - UObject* calling ProcessEvent + * @param Function - UFunction* being executed + * @param Parameters - Parameter block + * @return true to allow original execution, false to skip + */ + bool OnProcessEvent(void* Object, void* Function, void* Parameters); + + /** + * Register an event handler + * @param Name - Handler name for debugging + * @param Filter - Event filter criteria + * @param Handler - Callback function + * @param Priority - Execution priority (higher = first) + * @return Handler ID for later removal + */ + int32 RegisterHandler( + const std::string& Name, + const FEventFilter& Filter, + FProcessEventHandler Handler, + int32 Priority = 0); + + /** + * Unregister a handler by ID + */ + void UnregisterHandler(int32 HandlerId); + + /** + * Enable/disable a handler + */ + void SetHandlerEnabled(int32 HandlerId, bool bEnabled); + + /** + * Get handler count + */ + int32 GetHandlerCount() const { return static_cast(m_Handlers.size()); } + + /** + * Check if initialized + */ + bool IsInitialized() const { return m_bInitialized; } + + // Statistics + uint64 GetTotalEventsProcessed() const { return m_TotalEventsProcessed; } + uint64 GetTotalEventsHandled() const { return m_TotalEventsHandled; } + uint64 GetTotalEventsBlocked() const { return m_TotalEventsBlocked; } + + private: + /** + * Parse function parameters into context + */ + bool ParseParameters(void* Function, void* Parameters, FProcessEventContext& OutContext); + + /** + * Read parameter value from raw memory + */ + bool ReadParameterValue(void* Parameters, const FPropertyInfo& PropInfo, FParsedParameter& OutParam); + + /** + * Write parameter value to raw memory + */ + bool WriteParameterValue(void* Parameters, const FPropertyInfo& PropInfo, const FParsedParameter& Param); + + /** + * Get cached function info or parse it + */ + bool GetFunctionInfo(void* Function, std::vector& OutParams); + + /** + * Sort handlers by priority + */ + void SortHandlers(); + + // State + bool m_bInitialized; + int32 m_NextHandlerId; + + // Registered handlers (sorted by priority) + std::vector m_Handlers; + bool m_bHandlersDirty; + + // Function parameter cache (UFunction* -> params) + std::unordered_map> m_FunctionCache; + + // Statistics + uint64 m_TotalEventsProcessed; + uint64 m_TotalEventsHandled; + uint64 m_TotalEventsBlocked; + }; + + /** + * Global dispatcher accessor + */ + FProcessEventDispatcher& GetProcessEventDispatcher(); + + //========================================================================= + // Template Implementations + //========================================================================= + + template + bool FProcessEventContext::GetParamValue(const char* Name, T& OutValue) const + { + const FParsedParameter* Param = GetParam(Name); + if (!Param) + return false; + + // Type-specific extraction + if constexpr (std::is_same_v) + { + if (Param->Type == EPropertyType::BoolProperty) + { + OutValue = Param->BoolValue; + return true; + } + } + else if constexpr (std::is_same_v) + { + if (Param->Type == EPropertyType::IntProperty || + Param->Type == EPropertyType::ByteProperty) + { + OutValue = Param->IntValue; + return true; + } + } + else if constexpr (std::is_same_v) + { + if (Param->Type == EPropertyType::Int64Property) + { + OutValue = Param->Int64Value; + return true; + } + } + else if constexpr (std::is_same_v) + { + if (Param->Type == EPropertyType::FloatProperty) + { + OutValue = Param->FloatValue; + return true; + } + } + else if constexpr (std::is_same_v) + { + if (Param->Type == EPropertyType::DoubleProperty) + { + OutValue = Param->DoubleValue; + return true; + } + } + else if constexpr (std::is_same_v) + { + if (Param->Type == EPropertyType::StrProperty || + Param->Type == EPropertyType::NameProperty) + { + OutValue = Param->StringValue; + return true; + } + } + else if constexpr (std::is_pointer_v) + { + if (Param->Type == EPropertyType::ObjectProperty || + Param->Type == EPropertyType::ClassProperty) + { + OutValue = static_cast(Param->PointerValue); + return true; + } + } + + return false; + } + + template + bool FProcessEventContext::SetOutParamValue(const char* Name, const T& Value) + { + for (auto& Param : Params) + { + if (Param.Name == Name && Param.IsOutParam()) + { + // Write value to parameter memory + if (Parameters && Param.Offset >= 0) + { + void* ParamPtr = static_cast(Parameters) + Param.Offset; + + if constexpr (std::is_same_v) + { + *static_cast(ParamPtr) = Value; + Param.BoolValue = Value; + } + else if constexpr (std::is_same_v) + { + *static_cast(ParamPtr) = Value; + Param.IntValue = Value; + } + else if constexpr (std::is_same_v) + { + *static_cast(ParamPtr) = Value; + Param.FloatValue = Value; + } + // Add more types as needed + + return true; + } + } + } + return false; + } + +} \ No newline at end of file diff --git a/Engine/Reflection/PropertyIterator.cpp b/Engine/Reflection/PropertyIterator.cpp new file mode 100644 index 0000000..0716f59 --- /dev/null +++ b/Engine/Reflection/PropertyIterator.cpp @@ -0,0 +1,620 @@ +/** + * UniversalSlashingSimulator - Property Iterator Implementation + */ + +#include "PropertyIterator.h" +#include "../../Core/Memory/Memory.h" +#include "../../Core/Logging/Log.h" +#include "../../Core/Versioning/VersionResolver.h" +#include "../EngineCore.h" + +namespace USS +{ + //========================================================================= + // FPropertyInfo Methods + //========================================================================= + + bool FPropertyInfo::IsEditable() const + { + return (PropertyFlags & EPropertyFlags::CPF_Edit) != 0; + } + + bool FPropertyInfo::IsBlueprintVisible() const + { + return (PropertyFlags & EPropertyFlags::CPF_BlueprintVisible) != 0; + } + + bool FPropertyInfo::IsNative() const + { + return true; + } + + bool FPropertyInfo::IsReplicated() const + { + return (PropertyFlags & EPropertyFlags::CPF_Net) != 0; + } + + bool FPropertyInfo::IsSaveGame() const + { + return (PropertyFlags & EPropertyFlags::CPF_SaveGame) != 0; + } + + //========================================================================= + // FUPropertyIterator Implementation (Pre-4.25) + //========================================================================= + + FUPropertyIterator::FUPropertyIterator() + : m_ChildrenOffset(0) + , m_PropertyLinkOffset(0) + , m_SuperStructOffset(0) + , m_UProperty_ArrayDimOffset(0) + , m_UProperty_ElementSizeOffset(0) + , m_UProperty_PropertyFlagsOffset(0) + , m_UProperty_OffsetOffset(0) + , m_UProperty_NextOffset(0) + , m_bInitialized(false) + { + } + + EResult FUPropertyIterator::Initialize() + { + if (m_bInitialized) + return EResult::AlreadyInitialized; + + USS_LOG("Initializing UProperty iterator (Pre-4.25 mode)..."); + + // Get offsets from resolver + auto& Offsets = GetEngineCore().GetOffsetResolver(); + + // UStruct offsets + m_ChildrenOffset = Offsets.GetOffset("UStruct", "Children"); + m_PropertyLinkOffset = Offsets.GetOffset("UStruct", "PropertyLink"); + m_SuperStructOffset = Offsets.GetOffset("UStruct", "SuperStruct"); + + // UProperty offsets (derived from UField which is derived from UObject) + m_UProperty_NextOffset = Offsets.GetOffset("UField", "Next"); + m_UProperty_ArrayDimOffset = Offsets.GetOffset("UProperty", "ArrayDim"); + m_UProperty_ElementSizeOffset = Offsets.GetOffset("UProperty", "ElementSize"); + m_UProperty_PropertyFlagsOffset = Offsets.GetOffset("UProperty", "PropertyFlags"); + m_UProperty_OffsetOffset = Offsets.GetOffset("UProperty", "Offset_Internal"); + + // Use defaults if not resolved + if (m_ChildrenOffset == 0) m_ChildrenOffset = 0x48; // Typical for UE4.19 + if (m_PropertyLinkOffset == 0) m_PropertyLinkOffset = 0x50; + if (m_SuperStructOffset == 0) m_SuperStructOffset = 0x40; + if (m_UProperty_NextOffset == 0) m_UProperty_NextOffset = 0x30; + if (m_UProperty_ArrayDimOffset == 0) m_UProperty_ArrayDimOffset = 0x38; + if (m_UProperty_ElementSizeOffset == 0) m_UProperty_ElementSizeOffset = 0x3C; + if (m_UProperty_PropertyFlagsOffset == 0) m_UProperty_PropertyFlagsOffset = 0x40; + if (m_UProperty_OffsetOffset == 0) m_UProperty_OffsetOffset = 0x4C; + + USS_LOG("UProperty iterator offsets:"); + USS_LOG(" UStruct::Children = 0x%X", m_ChildrenOffset); + USS_LOG(" UStruct::PropertyLink = 0x%X", m_PropertyLinkOffset); + USS_LOG(" UProperty::Next = 0x%X", m_UProperty_NextOffset); + USS_LOG(" UProperty::ArrayDim = 0x%X", m_UProperty_ArrayDimOffset); + USS_LOG(" UProperty::Offset = 0x%X", m_UProperty_OffsetOffset); + + m_bInitialized = true; + return EResult::Success; + } + + bool FUPropertyIterator::IsInitialized() const + { + return m_bInitialized; + } + + void FUPropertyIterator::ForEachProperty(void* Struct, FPropertyCallback Callback, bool bIncludeSuper) + { + if (!m_bInitialized || !Struct || !Callback) + return; + + // Use PropertyLink for optimized iteration + void* Property = GetPropertyLink(Struct); + + while (Property) + { + FPropertyInfo Info; + if (ReadPropertyInfo(Property, Info)) + { + if (!Callback(Info)) + return; // Callback requested stop + } + + Property = GetNextProperty(Property); + } + + // Include super class properties if requested + if (bIncludeSuper) + { + void* SuperStruct = nullptr; + if (Memory::Read(reinterpret_cast(Struct) + m_SuperStructOffset, SuperStruct)) + { + if (SuperStruct) + { + ForEachProperty(SuperStruct, Callback, true); + } + } + } + } + + bool FUPropertyIterator::FindProperty(void* Struct, const char* PropertyName, FPropertyInfo& OutInfo) + { + if (!m_bInitialized || !Struct || !PropertyName) + return false; + + bool bFound = false; + std::string TargetName(PropertyName); + + ForEachProperty(Struct, [&](const FPropertyInfo& Info) -> bool { + if (Info.Name == TargetName) + { + OutInfo = Info; + bFound = true; + return false; // Stop iteration + } + return true; // Continue + }, true); + + return bFound; + } + + bool FUPropertyIterator::FindPropertyByOffset(void* Struct, int32 Offset, FPropertyInfo& OutInfo) + { + if (!m_bInitialized || !Struct) + return false; + + bool bFound = false; + + ForEachProperty(Struct, [&](const FPropertyInfo& Info) -> bool { + if (Info.Offset == Offset) + { + OutInfo = Info; + bFound = true; + return false; + } + return true; + }, true); + + return bFound; + } + + int32 FUPropertyIterator::GetPropertyCount(void* Struct, bool bIncludeSuper) + { + if (!m_bInitialized || !Struct) + return 0; + + int32 Count = 0; + + ForEachProperty(Struct, [&](const FPropertyInfo&) -> bool { + ++Count; + return true; + }, bIncludeSuper); + + return Count; + } + + void* FUPropertyIterator::GetPropertyListHead(void* Struct) + { + return GetPropertyLink(Struct); + } + + bool FUPropertyIterator::ReadPropertyInfo(void* Property, FPropertyInfo& OutInfo) + { + if (!Property) + return false; + + uintptr PropAddr = reinterpret_cast(Property); + + OutInfo.PropertyPtr = Property; + + // Get name from UObject (property is UObject-derived) + OutInfo.Name = GetEngineCore().GetObjectName(Property); + + // Get class name + OutInfo.ClassName = GetPropertyClassName(Property); + OutInfo.Type = ClassifyProperty(OutInfo.ClassName); + + // Read property data + Memory::Read(PropAddr + m_UProperty_ArrayDimOffset, OutInfo.ArrayDim); + Memory::Read(PropAddr + m_UProperty_ElementSizeOffset, OutInfo.ElementSize); + Memory::Read(PropAddr + m_UProperty_PropertyFlagsOffset, OutInfo.PropertyFlags); + Memory::Read(PropAddr + m_UProperty_OffsetOffset, OutInfo.Offset); + + return true; + } + + void* FUPropertyIterator::GetPropertyLink(void* Struct) + { + if (!Struct) + return nullptr; + + void* PropertyLink = nullptr; + Memory::Read(reinterpret_cast(Struct) + m_PropertyLinkOffset, PropertyLink); + return PropertyLink; + } + + void* FUPropertyIterator::GetNextProperty(void* Property) + { + if (!Property) + return nullptr; + + void* Next = nullptr; + Memory::Read(reinterpret_cast(Property) + m_UProperty_NextOffset, Next); + return Next; + } + + std::string FUPropertyIterator::GetPropertyClassName(void* Property) + { + if (!Property) + return ""; + + // Get UClass* of the property + void* Class = GetEngineCore().GetObjectClass(Property); + if (Class) + { + return GetEngineCore().GetObjectName(Class); + } + return ""; + } + + EPropertyType FUPropertyIterator::ClassifyProperty(const std::string& ClassName) + { + if (ClassName == "ByteProperty") return EPropertyType::ByteProperty; + if (ClassName == "Int8Property") return EPropertyType::Int8Property; + if (ClassName == "Int16Property") return EPropertyType::Int16Property; + if (ClassName == "IntProperty") return EPropertyType::IntProperty; + if (ClassName == "Int64Property") return EPropertyType::Int64Property; + if (ClassName == "UInt16Property") return EPropertyType::UInt16Property; + if (ClassName == "UInt32Property") return EPropertyType::UInt32Property; + if (ClassName == "UInt64Property") return EPropertyType::UInt64Property; + if (ClassName == "FloatProperty") return EPropertyType::FloatProperty; + if (ClassName == "DoubleProperty") return EPropertyType::DoubleProperty; + if (ClassName == "BoolProperty") return EPropertyType::BoolProperty; + if (ClassName == "StrProperty") return EPropertyType::StrProperty; + if (ClassName == "NameProperty") return EPropertyType::NameProperty; + if (ClassName == "TextProperty") return EPropertyType::TextProperty; + if (ClassName == "ObjectProperty") return EPropertyType::ObjectProperty; + if (ClassName == "ClassProperty") return EPropertyType::ClassProperty; + if (ClassName == "InterfaceProperty") return EPropertyType::InterfaceProperty; + if (ClassName == "WeakObjectProperty") return EPropertyType::WeakObjectProperty; + if (ClassName == "LazyObjectProperty") return EPropertyType::LazyObjectProperty; + if (ClassName == "SoftObjectProperty") return EPropertyType::SoftObjectProperty; + if (ClassName == "SoftClassProperty") return EPropertyType::SoftClassProperty; + if (ClassName == "StructProperty") return EPropertyType::StructProperty; + if (ClassName == "ArrayProperty") return EPropertyType::ArrayProperty; + if (ClassName == "MapProperty") return EPropertyType::MapProperty; + if (ClassName == "SetProperty") return EPropertyType::SetProperty; + if (ClassName == "DelegateProperty") return EPropertyType::DelegateProperty; + if (ClassName == "MulticastDelegateProperty") return EPropertyType::MulticastDelegateProperty; + if (ClassName == "MulticastInlineDelegateProperty") return EPropertyType::MulticastInlineDelegateProperty; + if (ClassName == "MulticastSparseDelegateProperty") return EPropertyType::MulticastSparseDelegateProperty; + if (ClassName == "EnumProperty") return EPropertyType::EnumProperty; + if (ClassName == "FieldPathProperty") return EPropertyType::FieldPathProperty; + return EPropertyType::Unknown; + } + + //========================================================================= + // FFFieldPropertyIterator Implementation (4.25+) + //========================================================================= + + FFFieldPropertyIterator::FFFieldPropertyIterator() + : m_ChildPropertiesOffset(0) + , m_SuperStructOffset(0) + , m_FField_ClassOffset(0) + , m_FField_OwnerOffset(0) + , m_FField_NextOffset(0) + , m_FField_NameOffset(0) + , m_FProperty_ArrayDimOffset(0) + , m_FProperty_ElementSizeOffset(0) + , m_FProperty_PropertyFlagsOffset(0) + , m_FProperty_OffsetOffset(0) + , m_FFieldClass_NameOffset(0) + , m_bInitialized(false) + { + } + + EResult FFFieldPropertyIterator::Initialize() + { + if (m_bInitialized) + return EResult::AlreadyInitialized; + + USS_LOG("Initializing FField property iterator (4.25+ mode)..."); + + // Get offsets from resolver + auto& Offsets = GetEngineCore().GetOffsetResolver(); + + // UStruct offsets (ChildProperties is new in 4.25) + m_ChildPropertiesOffset = Offsets.GetOffset("UStruct", "ChildProperties"); + m_SuperStructOffset = Offsets.GetOffset("UStruct", "SuperStruct"); + + // FField offsets + m_FField_ClassOffset = Offsets.GetOffset("FField", "ClassPrivate"); + m_FField_OwnerOffset = Offsets.GetOffset("FField", "Owner"); + m_FField_NextOffset = Offsets.GetOffset("FField", "Next"); + m_FField_NameOffset = Offsets.GetOffset("FField", "NamePrivate"); + + // FProperty offsets + m_FProperty_ArrayDimOffset = Offsets.GetOffset("FProperty", "ArrayDim"); + m_FProperty_ElementSizeOffset = Offsets.GetOffset("FProperty", "ElementSize"); + m_FProperty_PropertyFlagsOffset = Offsets.GetOffset("FProperty", "PropertyFlags"); + m_FProperty_OffsetOffset = Offsets.GetOffset("FProperty", "Offset_Internal"); + + // FFieldClass offset + m_FFieldClass_NameOffset = Offsets.GetOffset("FFieldClass", "Name"); + + // Use defaults if not resolved (typical for FN Chapter 2+) + if (m_ChildPropertiesOffset == 0) m_ChildPropertiesOffset = 0x50; + if (m_SuperStructOffset == 0) m_SuperStructOffset = 0x40; + if (m_FField_ClassOffset == 0) m_FField_ClassOffset = 0x00; + if (m_FField_OwnerOffset == 0) m_FField_OwnerOffset = 0x08; + if (m_FField_NextOffset == 0) m_FField_NextOffset = 0x20; + if (m_FField_NameOffset == 0) m_FField_NameOffset = 0x28; + if (m_FProperty_ArrayDimOffset == 0) m_FProperty_ArrayDimOffset = 0x38; + if (m_FProperty_ElementSizeOffset == 0) m_FProperty_ElementSizeOffset = 0x3C; + if (m_FProperty_PropertyFlagsOffset == 0) m_FProperty_PropertyFlagsOffset = 0x40; + if (m_FProperty_OffsetOffset == 0) m_FProperty_OffsetOffset = 0x4C; + if (m_FFieldClass_NameOffset == 0) m_FFieldClass_NameOffset = 0x00; + + USS_LOG("FField property iterator offsets:"); + USS_LOG(" UStruct::ChildProperties = 0x%X", m_ChildPropertiesOffset); + USS_LOG(" FField::Next = 0x%X", m_FField_NextOffset); + USS_LOG(" FField::NamePrivate = 0x%X", m_FField_NameOffset); + USS_LOG(" FProperty::Offset = 0x%X", m_FProperty_OffsetOffset); + + m_bInitialized = true; + return EResult::Success; + } + + bool FFFieldPropertyIterator::IsInitialized() const + { + return m_bInitialized; + } + + void FFFieldPropertyIterator::ForEachProperty(void* Struct, FPropertyCallback Callback, bool bIncludeSuper) + { + if (!m_bInitialized || !Struct || !Callback) + return; + + // Iterate ChildProperties chain (FField*) + void* Field = GetChildProperties(Struct); + + while (Field) + { + FPropertyInfo Info; + if (ReadPropertyInfo(Field, Info)) + { + if (!Callback(Info)) + return; + } + + Field = GetNextField(Field); + } + + // Include super class properties if requested + if (bIncludeSuper) + { + void* SuperStruct = nullptr; + if (Memory::Read(reinterpret_cast(Struct) + m_SuperStructOffset, SuperStruct)) + { + if (SuperStruct) + { + ForEachProperty(SuperStruct, Callback, true); + } + } + } + } + + bool FFFieldPropertyIterator::FindProperty(void* Struct, const char* PropertyName, FPropertyInfo& OutInfo) + { + if (!m_bInitialized || !Struct || !PropertyName) + return false; + + bool bFound = false; + std::string TargetName(PropertyName); + + ForEachProperty(Struct, [&](const FPropertyInfo& Info) -> bool { + if (Info.Name == TargetName) + { + OutInfo = Info; + bFound = true; + return false; + } + return true; + }, true); + + return bFound; + } + + bool FFFieldPropertyIterator::FindPropertyByOffset(void* Struct, int32 Offset, FPropertyInfo& OutInfo) + { + if (!m_bInitialized || !Struct) + return false; + + bool bFound = false; + + ForEachProperty(Struct, [&](const FPropertyInfo& Info) -> bool { + if (Info.Offset == Offset) + { + OutInfo = Info; + bFound = true; + return false; + } + return true; + }, true); + + return bFound; + } + + int32 FFFieldPropertyIterator::GetPropertyCount(void* Struct, bool bIncludeSuper) + { + if (!m_bInitialized || !Struct) + return 0; + + int32 Count = 0; + + ForEachProperty(Struct, [&](const FPropertyInfo&) -> bool { + ++Count; + return true; + }, bIncludeSuper); + + return Count; + } + + void* FFFieldPropertyIterator::GetPropertyListHead(void* Struct) + { + return GetChildProperties(Struct); + } + + bool FFFieldPropertyIterator::ReadPropertyInfo(void* Field, FPropertyInfo& OutInfo) + { + if (!Field) + return false; + + uintptr FieldAddr = reinterpret_cast(Field); + + OutInfo.PropertyPtr = Field; + + // Get name from FField (stored as FName) + // FName is at m_FField_NameOffset + int32 NameIndex = 0; + if (Memory::Read(FieldAddr + m_FField_NameOffset, NameIndex)) + { + OutInfo.Name = GetEngineCore().GetNameFromIndex(NameIndex); + } + + // Get class name from FFieldClass + OutInfo.ClassName = GetFieldClassName(Field); + OutInfo.Type = ClassifyProperty(OutInfo.ClassName); + + // Read property data + Memory::Read(FieldAddr + m_FProperty_ArrayDimOffset, OutInfo.ArrayDim); + Memory::Read(FieldAddr + m_FProperty_ElementSizeOffset, OutInfo.ElementSize); + Memory::Read(FieldAddr + m_FProperty_PropertyFlagsOffset, OutInfo.PropertyFlags); + + // Note: In 4.25+, Offset_Internal might be uint16 instead of int32 + // We read as int32 but only lower 16 bits may be valid + int32 RawOffset = 0; + Memory::Read(FieldAddr + m_FProperty_OffsetOffset, RawOffset); + OutInfo.Offset = RawOffset; + + return true; + } + + void* FFFieldPropertyIterator::GetChildProperties(void* Struct) + { + if (!Struct) + return nullptr; + + void* ChildProperties = nullptr; + Memory::Read(reinterpret_cast(Struct) + m_ChildPropertiesOffset, ChildProperties); + return ChildProperties; + } + + void* FFFieldPropertyIterator::GetNextField(void* Field) + { + if (!Field) + return nullptr; + + void* Next = nullptr; + Memory::Read(reinterpret_cast(Field) + m_FField_NextOffset, Next); + return Next; + } + + std::string FFFieldPropertyIterator::GetFieldClassName(void* Field) + { + if (!Field) + return ""; + + uintptr FieldAddr = reinterpret_cast(Field); + + // Get FFieldClass* from Field + void* FieldClass = nullptr; + if (!Memory::Read(FieldAddr + m_FField_ClassOffset, FieldClass)) + return ""; + + if (!FieldClass) + return ""; + + // FFieldClass has Name as FName at offset 0 + int32 NameIndex = 0; + if (Memory::Read(reinterpret_cast(FieldClass) + m_FFieldClass_NameOffset, NameIndex)) + { + return GetEngineCore().GetNameFromIndex(NameIndex); + } + + return ""; + } + + EPropertyType FFFieldPropertyIterator::ClassifyProperty(const std::string& ClassName) + { + // Same classification as UProperty version + if (ClassName == "ByteProperty") return EPropertyType::ByteProperty; + if (ClassName == "Int8Property") return EPropertyType::Int8Property; + if (ClassName == "Int16Property") return EPropertyType::Int16Property; + if (ClassName == "IntProperty") return EPropertyType::IntProperty; + if (ClassName == "Int64Property") return EPropertyType::Int64Property; + if (ClassName == "UInt16Property") return EPropertyType::UInt16Property; + if (ClassName == "UInt32Property") return EPropertyType::UInt32Property; + if (ClassName == "UInt64Property") return EPropertyType::UInt64Property; + if (ClassName == "FloatProperty") return EPropertyType::FloatProperty; + if (ClassName == "DoubleProperty") return EPropertyType::DoubleProperty; + if (ClassName == "BoolProperty") return EPropertyType::BoolProperty; + if (ClassName == "StrProperty") return EPropertyType::StrProperty; + if (ClassName == "NameProperty") return EPropertyType::NameProperty; + if (ClassName == "TextProperty") return EPropertyType::TextProperty; + if (ClassName == "ObjectProperty") return EPropertyType::ObjectProperty; + if (ClassName == "ClassProperty") return EPropertyType::ClassProperty; + if (ClassName == "InterfaceProperty") return EPropertyType::InterfaceProperty; + if (ClassName == "WeakObjectProperty") return EPropertyType::WeakObjectProperty; + if (ClassName == "LazyObjectProperty") return EPropertyType::LazyObjectProperty; + if (ClassName == "SoftObjectProperty") return EPropertyType::SoftObjectProperty; + if (ClassName == "SoftClassProperty") return EPropertyType::SoftClassProperty; + if (ClassName == "StructProperty") return EPropertyType::StructProperty; + if (ClassName == "ArrayProperty") return EPropertyType::ArrayProperty; + if (ClassName == "MapProperty") return EPropertyType::MapProperty; + if (ClassName == "SetProperty") return EPropertyType::SetProperty; + if (ClassName == "DelegateProperty") return EPropertyType::DelegateProperty; + if (ClassName == "MulticastDelegateProperty") return EPropertyType::MulticastDelegateProperty; + if (ClassName == "MulticastInlineDelegateProperty") return EPropertyType::MulticastInlineDelegateProperty; + if (ClassName == "MulticastSparseDelegateProperty") return EPropertyType::MulticastSparseDelegateProperty; + if (ClassName == "EnumProperty") return EPropertyType::EnumProperty; + if (ClassName == "FieldPathProperty") return EPropertyType::FieldPathProperty; + return EPropertyType::Unknown; + } + + //========================================================================= + // Factory & Global Accessor + //========================================================================= + + static std::unique_ptr g_PropertyIterator; + + std::unique_ptr CreatePropertyIterator() + { + const auto& Version = GetVersionResolver().GetVersionInfo(); + + if (Version.bUseFField) + { + USS_LOG("Creating FFFieldPropertyIterator for UE %s", + Version.GetEngineVersionString().c_str()); + return std::make_unique(); + } + else + { + USS_LOG("Creating FUPropertyIterator for UE %s", + Version.GetEngineVersionString().c_str()); + return std::make_unique(); + } + } + + IPropertyIterator& GetPropertyIterator() + { + if (!g_PropertyIterator) + { + g_PropertyIterator = CreatePropertyIterator(); + g_PropertyIterator->Initialize(); + } + return *g_PropertyIterator; + } + +} \ No newline at end of file diff --git a/Engine/Reflection/PropertyIterator.h b/Engine/Reflection/PropertyIterator.h new file mode 100644 index 0000000..73f48f1 --- /dev/null +++ b/Engine/Reflection/PropertyIterator.h @@ -0,0 +1,374 @@ +/** + * UniversalSlashingSimulator - Property Iterator Abstraction + * + * Provides version-agnostic iteration over class properties. + * Handles the UProperty vs FField/FProperty difference: + * - Pre-4.25: UProperty is UObject-derived, linked via PropertyLink + * - 4.25+: FField/FProperty is separate hierarchy, linked via Next + * + * This is critical for reflection and finding struct offsets. + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../../Core/Versioning/VersionInfo.h" +#include +#include + +namespace USS +{ + /** + * Property type categories + */ + enum class EPropertyType : uint8 + { + Unknown = 0, + + // Numeric types + ByteProperty, + Int8Property, + Int16Property, + IntProperty, + Int64Property, + UInt16Property, + UInt32Property, + UInt64Property, + FloatProperty, + DoubleProperty, + + // Boolean + BoolProperty, + + // String types + StrProperty, + NameProperty, + TextProperty, + + // Object references + ObjectProperty, + ClassProperty, + InterfaceProperty, + WeakObjectProperty, + LazyObjectProperty, + SoftObjectProperty, + SoftClassProperty, + + // Structs + StructProperty, + + // Containers + ArrayProperty, + MapProperty, + SetProperty, + + // Delegates + DelegateProperty, + MulticastDelegateProperty, + MulticastInlineDelegateProperty, + MulticastSparseDelegateProperty, + + // Enum + EnumProperty, + + // Special + FieldPathProperty, + }; + + /** + * Resolved property information + */ + struct FPropertyInfo + { + std::string Name; + std::string ClassName; // Property class name (e.g., "IntProperty") + EPropertyType Type; + + int32 Offset; // Offset within struct/class + int32 ElementSize; // Size of single element + int32 ArrayDim; // Static array dimension (usually 1) + + uint64 PropertyFlags; // EPropertyFlags + + void* PropertyPtr; // Native pointer (UProperty* or FProperty*) + void* OwnerStruct; // Owning UStruct* + + // For struct properties + void* InnerStruct; // UScriptStruct* for StructProperty + + // For array properties + void* InnerProperty; // Inner property for arrays + + // For object properties + void* PropertyClass; // UClass* for object references + + FPropertyInfo() + : Type(EPropertyType::Unknown) + , Offset(0) + , ElementSize(0) + , ArrayDim(1) + , PropertyFlags(0) + , PropertyPtr(nullptr) + , OwnerStruct(nullptr) + , InnerStruct(nullptr) + , InnerProperty(nullptr) + , PropertyClass(nullptr) + {} + + bool IsValid() const { return PropertyPtr != nullptr; } + + // Common property flag checks + bool IsEditable() const; + bool IsBlueprintVisible() const; + bool IsNative() const; + bool IsReplicated() const; + bool IsSaveGame() const; + }; + + /** + * Property flags (common subset across versions) + */ + namespace EPropertyFlags + { + constexpr uint64 CPF_Edit = 0x0000000000000001; + constexpr uint64 CPF_ConstParm = 0x0000000000000002; + constexpr uint64 CPF_BlueprintVisible = 0x0000000000000004; + constexpr uint64 CPF_ExportObject = 0x0000000000000008; + constexpr uint64 CPF_BlueprintReadOnly = 0x0000000000000010; + constexpr uint64 CPF_Net = 0x0000000000000020; + constexpr uint64 CPF_EditFixedSize = 0x0000000000000040; + constexpr uint64 CPF_Parm = 0x0000000000000080; + constexpr uint64 CPF_OutParm = 0x0000000000000100; + constexpr uint64 CPF_ZeroConstructor = 0x0000000000000200; + constexpr uint64 CPF_ReturnParm = 0x0000000000000400; + constexpr uint64 CPF_DisableEditOnTemplate = 0x0000000000000800; + constexpr uint64 CPF_Transient = 0x0000000000002000; + constexpr uint64 CPF_Config = 0x0000000000004000; + constexpr uint64 CPF_DisableEditOnInstance = 0x0000000000010000; + constexpr uint64 CPF_EditConst = 0x0000000000020000; + constexpr uint64 CPF_GlobalConfig = 0x0000000000040000; + constexpr uint64 CPF_InstancedReference = 0x0000000000080000; + constexpr uint64 CPF_DuplicateTransient = 0x0000000000200000; + constexpr uint64 CPF_SaveGame = 0x0000000001000000; + constexpr uint64 CPF_NoClear = 0x0000000002000000; + constexpr uint64 CPF_ReferenceParm = 0x0000000008000000; + constexpr uint64 CPF_BlueprintAssignable = 0x0000000010000000; + constexpr uint64 CPF_Deprecated = 0x0000000020000000; + constexpr uint64 CPF_RepNotify = 0x0000000100000000; + constexpr uint64 CPF_Interp = 0x0000000200000000; + constexpr uint64 CPF_NonTransactional = 0x0000000400000000; + constexpr uint64 CPF_EditorOnly = 0x0000000800000000; + constexpr uint64 CPF_NoDestructor = 0x0000001000000000; + constexpr uint64 CPF_AutoWeak = 0x0000004000000000; + constexpr uint64 CPF_ContainsInstancedReference = 0x0000008000000000; + constexpr uint64 CPF_AssetRegistrySearchable = 0x0000010000000000; + constexpr uint64 CPF_SimpleDisplay = 0x0000020000000000; + constexpr uint64 CPF_AdvancedDisplay = 0x0000040000000000; + constexpr uint64 CPF_Protected = 0x0000080000000000; + constexpr uint64 CPF_BlueprintCallable = 0x0000100000000000; + constexpr uint64 CPF_BlueprintAuthorityOnly = 0x0000200000000000; + constexpr uint64 CPF_TextExportTransient = 0x0000400000000000; + constexpr uint64 CPF_NonPIEDuplicateTransient = 0x0000800000000000; + constexpr uint64 CPF_ExposeOnSpawn = 0x0001000000000000; + constexpr uint64 CPF_PersistentInstance = 0x0002000000000000; + constexpr uint64 CPF_UObjectWrapper = 0x0004000000000000; + constexpr uint64 CPF_HasGetValueTypeHash = 0x0008000000000000; + constexpr uint64 CPF_NativeAccessSpecifierPublic = 0x0010000000000000; + constexpr uint64 CPF_NativeAccessSpecifierProtected = 0x0020000000000000; + constexpr uint64 CPF_NativeAccessSpecifierPrivate = 0x0040000000000000; + constexpr uint64 CPF_SkipSerialization = 0x0080000000000000; + } + + /** + * Property iterator callback + */ + using FPropertyCallback = std::function; + + /** + * Property iterator interface + */ + USS_INTERFACE IPropertyIterator + { + public: + virtual ~IPropertyIterator() = default; + + /** + * Iterate all properties of a UStruct + * @param Struct - UStruct* to iterate + * @param Callback - Called for each property, return false to stop + * @param bIncludeSuper - Include inherited properties + */ + virtual void ForEachProperty(void* Struct, FPropertyCallback Callback, bool bIncludeSuper = true) = 0; + + /** + * Find property by name + * @param Struct - UStruct* to search + * @param PropertyName - Name to find + * @param OutInfo - Output property info + * @return true if found + */ + virtual bool FindProperty(void* Struct, const char* PropertyName, FPropertyInfo& OutInfo) = 0; + + /** + * Find property by offset + * @param Struct - UStruct* to search + * @param Offset - Offset to find + * @param OutInfo - Output property info + * @return true if found + */ + virtual bool FindPropertyByOffset(void* Struct, int32 Offset, FPropertyInfo& OutInfo) = 0; + + /** + * Get property count + */ + virtual int32 GetPropertyCount(void* Struct, bool bIncludeSuper = true) = 0; + + /** + * Get children/property list head + */ + virtual void* GetPropertyListHead(void* Struct) = 0; + + /** + * Initialize the iterator + */ + virtual EResult Initialize() = 0; + + /** + * Check if initialized + */ + virtual bool IsInitialized() const = 0; + }; + + /** + * UProperty-based iterator (Pre-4.25) + * + * In this version, properties are UObject-derived: + * UProperty : UField : UObject + * + * UStruct layout (relevant parts): + * - Children : UField* (first child field) + * - PropertyLink : UProperty* (optimized property iteration) + * + * UProperty layout: + * - UField base (includes UObject + Next pointer) + * - ArrayDim : int32 + * - ElementSize : int32 + * - PropertyFlags : uint64 + * - Offset_Internal : int32 + * - ... + */ + class FUPropertyIterator : public IPropertyIterator + { + public: + FUPropertyIterator(); + ~FUPropertyIterator() override = default; + + void ForEachProperty(void* Struct, FPropertyCallback Callback, bool bIncludeSuper = true) override; + bool FindProperty(void* Struct, const char* PropertyName, FPropertyInfo& OutInfo) override; + bool FindPropertyByOffset(void* Struct, int32 Offset, FPropertyInfo& OutInfo) override; + int32 GetPropertyCount(void* Struct, bool bIncludeSuper = true) override; + void* GetPropertyListHead(void* Struct) override; + EResult Initialize() override; + bool IsInitialized() const override; + + private: + bool ReadPropertyInfo(void* Property, FPropertyInfo& OutInfo); + void* GetPropertyLink(void* Struct); + void* GetNextProperty(void* Property); + std::string GetPropertyClassName(void* Property); + EPropertyType ClassifyProperty(const std::string& ClassName); + + // Offsets within UStruct + int32 m_ChildrenOffset; // Offset to Children field + int32 m_PropertyLinkOffset; // Offset to PropertyLink field + int32 m_SuperStructOffset; // Offset to SuperStruct field + + // Offsets within UProperty + int32 m_UProperty_ArrayDimOffset; + int32 m_UProperty_ElementSizeOffset; + int32 m_UProperty_PropertyFlagsOffset; + int32 m_UProperty_OffsetOffset; + int32 m_UProperty_NextOffset; // UField::Next + + bool m_bInitialized; + }; + + /** + * FField/FProperty-based iterator (4.25+) + * + * In this version, properties use a separate hierarchy: + * FProperty : FField (NOT UObject-derived) + * + * UStruct layout (relevant parts): + * - ChildProperties : FField* (new field chain) + * - Children : UField* (still exists for functions, etc.) + * + * FField layout: + * - ClassPrivate : FFieldClass* + * - Owner : FFieldVariant + * - Next : FField* + * - NamePrivate : FName + * - FlagsPrivate : EObjectFlags + * + * FProperty layout: + * - FField base + * - ArrayDim : int32 + * - ElementSize : int32 + * - PropertyFlags : EPropertyFlags + * - Offset_Internal : uint16 (changed from int32!) + * - ... + */ + class FFFieldPropertyIterator : public IPropertyIterator + { + public: + FFFieldPropertyIterator(); + ~FFFieldPropertyIterator() override = default; + + void ForEachProperty(void* Struct, FPropertyCallback Callback, bool bIncludeSuper = true) override; + bool FindProperty(void* Struct, const char* PropertyName, FPropertyInfo& OutInfo) override; + bool FindPropertyByOffset(void* Struct, int32 Offset, FPropertyInfo& OutInfo) override; + int32 GetPropertyCount(void* Struct, bool bIncludeSuper = true) override; + void* GetPropertyListHead(void* Struct) override; + EResult Initialize() override; + bool IsInitialized() const override; + + private: + bool ReadPropertyInfo(void* Field, FPropertyInfo& OutInfo); + void* GetChildProperties(void* Struct); + void* GetNextField(void* Field); + std::string GetFieldClassName(void* Field); + EPropertyType ClassifyProperty(const std::string& ClassName); + + // Offsets within UStruct + int32 m_ChildPropertiesOffset; // Offset to ChildProperties (FField*) + int32 m_SuperStructOffset; // Offset to SuperStruct + + // Offsets within FField + int32 m_FField_ClassOffset; + int32 m_FField_OwnerOffset; + int32 m_FField_NextOffset; + int32 m_FField_NameOffset; + + // Offsets within FProperty + int32 m_FProperty_ArrayDimOffset; + int32 m_FProperty_ElementSizeOffset; + int32 m_FProperty_PropertyFlagsOffset; + int32 m_FProperty_OffsetOffset; + + // FFieldClass contains class name + int32 m_FFieldClass_NameOffset; + + bool m_bInitialized; + }; + + /** + * Factory function to create appropriate property iterator + */ + std::unique_ptr CreatePropertyIterator(); + + /** + * Global property iterator accessor + */ + IPropertyIterator& GetPropertyIterator(); + +} \ No newline at end of file diff --git a/Engine/Replication/FastArraySerializer.cpp b/Engine/Replication/FastArraySerializer.cpp new file mode 100644 index 0000000..8b67f06 --- /dev/null +++ b/Engine/Replication/FastArraySerializer.cpp @@ -0,0 +1,566 @@ +/** + * UniversalSlashingSimulator - Fast Array Serializer Implementation + */ + +#include "FastArraySerializer.h" +#include "../../Core/Memory/Memory.h" +#include "../../Core/Logging/Log.h" +#include "../../Core/Versioning/VersionResolver.h" + +namespace USS +{ + //========================================================================= + // FLegacyFastArraySerializer Implementation (Pre-8.30) + //========================================================================= + + FLegacyFastArraySerializer::FLegacyFastArraySerializer() + : m_FastArrayPtr(nullptr) + , m_ItemSize(0) + , m_ItemsOffset(0) + , m_ItemsDataPtr(0) + , m_ItemsNum(0) + , m_ItemsMax(0) + , m_IDCounterOffset(0x60) // Default offset + , m_bInitialized(false) + { + } + + EResult FLegacyFastArraySerializer::Initialize(void* FastArrayPtr, size_t ItemSize, int32 ItemsOffset) + { + if (!FastArrayPtr) + return EResult::InvalidParameter; + + m_FastArrayPtr = FastArrayPtr; + m_ItemSize = ItemSize; + m_ItemsOffset = ItemsOffset; + + uintptr BaseAddr = reinterpret_cast(FastArrayPtr); + + // Read TArray structure at ItemsOffset + // TArray layout: Data*, Num, Max + uintptr TArrayAddr = BaseAddr + ItemsOffset; + + if (!Memory::Read(TArrayAddr + 0x00, m_ItemsDataPtr)) + { + USS_ERROR("Failed to read Items.Data from FastArraySerializer"); + return EResult::Failed; + } + + if (!Memory::Read(TArrayAddr + 0x08, m_ItemsNum)) + { + USS_ERROR("Failed to read Items.Num from FastArraySerializer"); + return EResult::Failed; + } + + if (!Memory::Read(TArrayAddr + 0x0C, m_ItemsMax)) + { + USS_ERROR("Failed to read Items.Max from FastArraySerializer"); + return EResult::Failed; + } + + USS_LOG("LegacyFastArraySerializer initialized: Num=%d, Max=%d, ItemSize=%zu", + m_ItemsNum, m_ItemsMax, m_ItemSize); + + m_bInitialized = true; + return EResult::Success; + } + + int32 FLegacyFastArraySerializer::Num() const + { + if (!m_bInitialized) + return 0; + + // Re-read in case it changed + int32 CurrentNum = 0; + uintptr TArrayAddr = reinterpret_cast(m_FastArrayPtr) + m_ItemsOffset; + Memory::Read(TArrayAddr + 0x08, CurrentNum); + return CurrentNum; + } + + void* FLegacyFastArraySerializer::GetItem(int32 Index) const + { + if (!m_bInitialized || Index < 0 || Index >= Num()) + return nullptr; + + // Re-read data pointer in case array was reallocated + uintptr DataPtr = 0; + uintptr TArrayAddr = reinterpret_cast(m_FastArrayPtr) + m_ItemsOffset; + Memory::Read(TArrayAddr + 0x00, DataPtr); + + if (DataPtr == 0) + return nullptr; + + return reinterpret_cast(DataPtr + (Index * m_ItemSize)); + } + + int32 FLegacyFastArraySerializer::GetItemReplicationID(int32 Index) const + { + void* Item = GetItem(Index); + if (!Item) + return -1; + + int32 ReplicationID = -1; + Memory::Read(reinterpret_cast(Item) + ReplicationIDOffset, ReplicationID); + return ReplicationID; + } + + int32 FLegacyFastArraySerializer::GetItemReplicationKey(int32 Index) const + { + void* Item = GetItem(Index); + if (!Item) + return -1; + + int32 ReplicationKey = -1; + Memory::Read(reinterpret_cast(Item) + ReplicationKeyOffset, ReplicationKey); + return ReplicationKey; + } + + bool FLegacyFastArraySerializer::IsItemDirty(int32 Index) const + { + // In legacy format, items are dirty if ReplicationKey differs from cached + // For now, assume all items need checking + return true; + } + + void FLegacyFastArraySerializer::MarkItemDirty(int32 Index) + { + void* Item = GetItem(Index); + if (!Item) + return; + + // Increment ReplicationKey + int32 CurrentKey = GetItemReplicationKey(Index); + Memory::Write(reinterpret_cast(Item) + ReplicationKeyOffset, CurrentKey + 1); + } + + void FLegacyFastArraySerializer::MarkAllDirty() + { + int32 Count = Num(); + for (int32 i = 0; i < Count; ++i) + { + MarkItemDirty(i); + } + } + + int32 FLegacyFastArraySerializer::GetArrayReplicationKey() const + { + // Legacy format doesn't have a separate array replication key + // Return max item key as approximation + int32 MaxKey = 0; + int32 Count = Num(); + for (int32 i = 0; i < Count; ++i) + { + int32 Key = GetItemReplicationKey(i); + if (Key > MaxKey) + MaxKey = Key; + } + return MaxKey; + } + + int32 FLegacyFastArraySerializer::GetIDCounter() const + { + if (!m_bInitialized) + return 0; + + int32 IDCounter = 0; + uintptr BaseAddr = reinterpret_cast(m_FastArrayPtr); + Memory::Read(BaseAddr + m_IDCounterOffset, IDCounter); + return IDCounter; + } + + void FLegacyFastArraySerializer::IncrementArrayReplicationKey() + { + // No-op for legacy format + } + + void FLegacyFastArraySerializer::RegisterChangeCallback(FFastArrayChangeCallback Callback) + { + if (Callback) + { + m_Callbacks.push_back(std::move(Callback)); + } + } + + bool FLegacyFastArraySerializer::IsInitialized() const + { + return m_bInitialized; + } + + //========================================================================= + // FNewFastArraySerializer Implementation (Post-8.30) + //========================================================================= + + FNewFastArraySerializer::FNewFastArraySerializer() + : m_FastArrayPtr(nullptr) + , m_ItemSize(0) + , m_ItemsOffset(0) + , m_ItemsDataPtr(0) + , m_ItemsNum(0) + , m_ItemsMax(0) + , m_ArrayReplicationKeyOffset(0x68) // Default for post-8.30 + , m_IDCounterOffset(0x6C) + , m_DeltaFlagsOffset(0x70) + , m_CachedArrayReplicationKey(0) + , m_bInitialized(false) + { + } + + EResult FNewFastArraySerializer::Initialize(void* FastArrayPtr, size_t ItemSize, int32 ItemsOffset) + { + if (!FastArrayPtr) + return EResult::InvalidParameter; + + m_FastArrayPtr = FastArrayPtr; + m_ItemSize = ItemSize; + m_ItemsOffset = ItemsOffset; + + uintptr BaseAddr = reinterpret_cast(FastArrayPtr); + + // Read TArray structure + uintptr TArrayAddr = BaseAddr + ItemsOffset; + + if (!Memory::Read(TArrayAddr + 0x00, m_ItemsDataPtr)) + { + USS_ERROR("Failed to read Items.Data from FastArraySerializer"); + return EResult::Failed; + } + + if (!Memory::Read(TArrayAddr + 0x08, m_ItemsNum)) + { + USS_ERROR("Failed to read Items.Num from FastArraySerializer"); + return EResult::Failed; + } + + if (!Memory::Read(TArrayAddr + 0x0C, m_ItemsMax)) + { + USS_ERROR("Failed to read Items.Max from FastArraySerializer"); + return EResult::Failed; + } + + // Read ArrayReplicationKey (new in post-8.30) + Memory::Read(BaseAddr + m_ArrayReplicationKeyOffset, m_CachedArrayReplicationKey); + + USS_LOG("NewFastArraySerializer initialized: Num=%d, Max=%d, ArrayKey=%d", + m_ItemsNum, m_ItemsMax, m_CachedArrayReplicationKey); + + m_bInitialized = true; + return EResult::Success; + } + + int32 FNewFastArraySerializer::Num() const + { + if (!m_bInitialized) + return 0; + + int32 CurrentNum = 0; + uintptr TArrayAddr = reinterpret_cast(m_FastArrayPtr) + m_ItemsOffset; + Memory::Read(TArrayAddr + 0x08, CurrentNum); + return CurrentNum; + } + + void* FNewFastArraySerializer::GetItem(int32 Index) const + { + if (!m_bInitialized || Index < 0 || Index >= Num()) + return nullptr; + + uintptr DataPtr = 0; + uintptr TArrayAddr = reinterpret_cast(m_FastArrayPtr) + m_ItemsOffset; + Memory::Read(TArrayAddr + 0x00, DataPtr); + + if (DataPtr == 0) + return nullptr; + + return reinterpret_cast(DataPtr + (Index * m_ItemSize)); + } + + int32 FNewFastArraySerializer::GetItemReplicationID(int32 Index) const + { + void* Item = GetItem(Index); + if (!Item) + return -1; + + int32 ReplicationID = -1; + Memory::Read(reinterpret_cast(Item) + ReplicationIDOffset, ReplicationID); + return ReplicationID; + } + + int32 FNewFastArraySerializer::GetItemReplicationKey(int32 Index) const + { + void* Item = GetItem(Index); + if (!Item) + return -1; + + int32 ReplicationKey = -1; + Memory::Read(reinterpret_cast(Item) + ReplicationKeyOffset, ReplicationKey); + return ReplicationKey; + } + + bool FNewFastArraySerializer::IsItemDirty(int32 Index) const + { + void* Item = GetItem(Index); + if (!Item) + return false; + + // Compare item's MostRecentArrayReplicationKey with current ArrayReplicationKey + int32 ItemArrayKey = 0; + Memory::Read(reinterpret_cast(Item) + MostRecentArrayReplicationKeyOffset, ItemArrayKey); + + return ItemArrayKey != GetArrayReplicationKey(); + } + + void FNewFastArraySerializer::MarkItemDirty(int32 Index) + { + void* Item = GetItem(Index); + if (!Item) + return; + + uintptr ItemAddr = reinterpret_cast(Item); + + // Increment ReplicationKey + int32 CurrentKey = GetItemReplicationKey(Index); + Memory::Write(ItemAddr + ReplicationKeyOffset, CurrentKey + 1); + + // Update MostRecentArrayReplicationKey to current + int32 ArrayKey = GetArrayReplicationKey(); + Memory::Write(ItemAddr + MostRecentArrayReplicationKeyOffset, ArrayKey); + + // Notify callbacks + FFastArrayChange Change; + Change.Type = FFastArrayChange::EChangeType::Modified; + Change.Index = Index; + Change.ReplicationID = GetItemReplicationID(Index); + NotifyChange(Change); + } + + void FNewFastArraySerializer::MarkAllDirty() + { + IncrementArrayReplicationKey(); + + int32 Count = Num(); + for (int32 i = 0; i < Count; ++i) + { + MarkItemDirty(i); + } + } + + int32 FNewFastArraySerializer::GetArrayReplicationKey() const + { + if (!m_bInitialized) + return 0; + + int32 ArrayKey = 0; + uintptr BaseAddr = reinterpret_cast(m_FastArrayPtr); + Memory::Read(BaseAddr + m_ArrayReplicationKeyOffset, ArrayKey); + return ArrayKey; + } + + int32 FNewFastArraySerializer::GetIDCounter() const + { + if (!m_bInitialized) + return 0; + + int32 IDCounter = 0; + uintptr BaseAddr = reinterpret_cast(m_FastArrayPtr); + Memory::Read(BaseAddr + m_IDCounterOffset, IDCounter); + return IDCounter; + } + + void FNewFastArraySerializer::IncrementArrayReplicationKey() + { + if (!m_bInitialized) + return; + + int32 CurrentKey = GetArrayReplicationKey(); + uintptr BaseAddr = reinterpret_cast(m_FastArrayPtr); + Memory::Write(BaseAddr + m_ArrayReplicationKeyOffset, CurrentKey + 1); + } + + void FNewFastArraySerializer::RegisterChangeCallback(FFastArrayChangeCallback Callback) + { + if (Callback) + { + m_Callbacks.push_back(std::move(Callback)); + } + } + + bool FNewFastArraySerializer::IsInitialized() const + { + return m_bInitialized; + } + + void FNewFastArraySerializer::NotifyChange(const FFastArrayChange& Change) + { + for (const auto& Callback : m_Callbacks) + { + if (Callback) + { + Callback(Change); + } + } + } + + //========================================================================= + // Factory Function + //========================================================================= + + std::unique_ptr CreateFastArraySerializer() + { + const auto& Version = GetVersionResolver().GetVersionInfo(); + + if (Version.bUseNewFastArraySerializer) + { + USS_LOG("Creating NewFastArraySerializer (post-8.30 format)"); + return std::make_unique(); + } + else + { + USS_LOG("Creating LegacyFastArraySerializer (pre-8.30 format)"); + return std::make_unique(); + } + } + + //========================================================================= + // FFastArrayChangeDetector Implementation + //========================================================================= + + FFastArrayChangeDetector::FFastArrayChangeDetector() + : m_Serializer(nullptr) + , m_LastArrayReplicationKey(0) + , m_LastItemCount(0) + { + } + + EResult FFastArrayChangeDetector::Initialize(IFastArraySerializer* Serializer) + { + if (!Serializer || !Serializer->IsInitialized()) + return EResult::InvalidParameter; + + m_Serializer = Serializer; + Reset(); + return EResult::Success; + } + + int32 FFastArrayChangeDetector::DetectChanges(std::vector& OutChanges) + { + OutChanges.clear(); + + if (!m_Serializer) + return 0; + + int32 CurrentCount = m_Serializer->Num(); + int32 CurrentArrayKey = m_Serializer->GetArrayReplicationKey(); + + // Check for array-level reset + if (CurrentArrayKey != m_LastArrayReplicationKey && m_Serializer->IsNewFormat()) + { + // Array key changed - could be mass update + // Check each item for changes + } + + // Detect removals (items that were present but aren't now) + for (size_t i = 0; i < m_LastItemIDs.size(); ++i) + { + int32 OldID = m_LastItemIDs[i]; + bool bFound = false; + + for (int32 j = 0; j < CurrentCount; ++j) + { + if (m_Serializer->GetItemReplicationID(j) == OldID) + { + bFound = true; + break; + } + } + + if (!bFound) + { + FFastArrayChange Change; + Change.Type = FFastArrayChange::EChangeType::Removed; + Change.Index = static_cast(i); + Change.ReplicationID = OldID; + OutChanges.push_back(Change); + } + } + + // Detect additions and modifications + for (int32 i = 0; i < CurrentCount; ++i) + { + int32 CurrentID = m_Serializer->GetItemReplicationID(i); + int32 CurrentKey = m_Serializer->GetItemReplicationKey(i); + + // Check if this item existed before + bool bIsNew = true; + bool bIsModified = false; + + for (size_t j = 0; j < m_LastItemIDs.size(); ++j) + { + if (m_LastItemIDs[j] == CurrentID) + { + bIsNew = false; + + // Check if key changed (item was modified) + if (j < m_LastItemKeys.size() && m_LastItemKeys[j] != CurrentKey) + { + bIsModified = true; + } + break; + } + } + + if (bIsNew) + { + FFastArrayChange Change; + Change.Type = FFastArrayChange::EChangeType::Added; + Change.Index = i; + Change.ReplicationID = CurrentID; + OutChanges.push_back(Change); + } + else if (bIsModified) + { + FFastArrayChange Change; + Change.Type = FFastArrayChange::EChangeType::Modified; + Change.Index = i; + Change.ReplicationID = CurrentID; + OutChanges.push_back(Change); + } + } + + // Update cached state + m_LastArrayReplicationKey = CurrentArrayKey; + m_LastItemCount = CurrentCount; + + m_LastItemIDs.clear(); + m_LastItemKeys.clear(); + m_LastItemIDs.reserve(CurrentCount); + m_LastItemKeys.reserve(CurrentCount); + + for (int32 i = 0; i < CurrentCount; ++i) + { + m_LastItemIDs.push_back(m_Serializer->GetItemReplicationID(i)); + m_LastItemKeys.push_back(m_Serializer->GetItemReplicationKey(i)); + } + + return static_cast(OutChanges.size()); + } + + void FFastArrayChangeDetector::Reset() + { + if (!m_Serializer) + return; + + m_LastArrayReplicationKey = m_Serializer->GetArrayReplicationKey(); + m_LastItemCount = m_Serializer->Num(); + + m_LastItemIDs.clear(); + m_LastItemKeys.clear(); + m_LastItemIDs.reserve(m_LastItemCount); + m_LastItemKeys.reserve(m_LastItemCount); + + for (int32 i = 0; i < m_LastItemCount; ++i) + { + m_LastItemIDs.push_back(m_Serializer->GetItemReplicationID(i)); + m_LastItemKeys.push_back(m_Serializer->GetItemReplicationKey(i)); + } + } + +} \ No newline at end of file diff --git a/Engine/Replication/FastArraySerializer.h b/Engine/Replication/FastArraySerializer.h new file mode 100644 index 0000000..bdd10ce --- /dev/null +++ b/Engine/Replication/FastArraySerializer.h @@ -0,0 +1,329 @@ +/** + * UniversalSlashingSimulator - Fast Array Serializer Abstraction + * + * Provides version-agnostic handling of FFastArraySerializer, which changed + * significantly at Fortnite 8.30 (around UE4.22). + * + * FFastArraySerializer is used for efficient replication of dynamic arrays + * like inventory items, building pieces, and other game state. + * + * Pre-8.30 Layout: + * - Simple array with per-element dirty tracking + * - ReplicationID, ReplicationKey per item + * + * Post-8.30 Layout: + * - Restructured with ArrayReplicationKey + * - Different delta serialization format + * - IDCounter for unique element identification + */ + +#pragma once + +#include "../../Core/Common.h" +#include +#include + +namespace USS +{ + /** + * Fast array item base - common to all versions + */ + struct FFastArrayItem + { + int32 ReplicationID; // Unique identifier for this item + int32 ReplicationKey; // Change counter + + // Cached state + bool bIsDirty; + bool bIsNew; + bool bIsRemoved; + + FFastArrayItem() + : ReplicationID(-1) + , ReplicationKey(-1) + , bIsDirty(false) + , bIsNew(false) + , bIsRemoved(false) + {} + }; + + /** + * Fast array change event + */ + struct FFastArrayChange + { + enum class EChangeType + { + None, + Added, + Modified, + Removed, + Reset, + }; + + EChangeType Type; + int32 Index; + int32 ReplicationID; + + FFastArrayChange() + : Type(EChangeType::None) + , Index(-1) + , ReplicationID(-1) + {} + }; + + /** + * Callback for array changes + */ + using FFastArrayChangeCallback = std::function; + + /** + * Fast array serializer interface + * + * This handles the version differences in how Fortnite + * serializes and replicates dynamic arrays. + */ + USS_INTERFACE IFastArraySerializer + { + public: + virtual ~IFastArraySerializer() = default; + + /** + * Initialize from a native FFastArraySerializer* + * @param FastArrayPtr - Pointer to native struct + * @param ItemSize - Size of each array element + * @param ItemsOffset - Offset to Items array within struct + */ + virtual EResult Initialize(void* FastArrayPtr, size_t ItemSize, int32 ItemsOffset) = 0; + + /** + * Get number of items in array + */ + virtual int32 Num() const = 0; + + /** + * Get item by index + * @param Index - Array index + * @return Pointer to item data, or nullptr if invalid + */ + virtual void* GetItem(int32 Index) const = 0; + + /** + * Get item's replication ID + */ + virtual int32 GetItemReplicationID(int32 Index) const = 0; + + /** + * Get item's replication key + */ + virtual int32 GetItemReplicationKey(int32 Index) const = 0; + + /** + * Check if item is dirty (needs replication) + */ + virtual bool IsItemDirty(int32 Index) const = 0; + + /** + * Mark item as dirty + */ + virtual void MarkItemDirty(int32 Index) = 0; + + /** + * Mark all items as dirty + */ + virtual void MarkAllDirty() = 0; + + /** + * Get array replication key (post-8.30) + */ + virtual int32 GetArrayReplicationKey() const = 0; + + /** + * Get ID counter (post-8.30) + */ + virtual int32 GetIDCounter() const = 0; + + /** + * Increment array replication key + */ + virtual void IncrementArrayReplicationKey() = 0; + + /** + * Register callback for array changes + */ + virtual void RegisterChangeCallback(FFastArrayChangeCallback Callback) = 0; + + /** + * Check if using new serializer format (post-8.30) + */ + virtual bool IsNewFormat() const = 0; + + /** + * Check if initialized + */ + virtual bool IsInitialized() const = 0; + }; + + /** + * Pre-8.30 Fast Array Serializer + * + * Layout: + * struct FFastArraySerializer { + * TArray Items; // 0x00 - Standard TArray + * TMap ItemMap; // 0x10 - ReplicationID -> Index + * int32 IDCounter; // 0x60 - Next ID to assign + * }; + * + * Each item contains: + * int32 ReplicationID; + * int32 ReplicationKey; + * int32 MostRecentArrayReplicationKey; + */ + class FLegacyFastArraySerializer : public IFastArraySerializer + { + public: + FLegacyFastArraySerializer(); + ~FLegacyFastArraySerializer() override = default; + + EResult Initialize(void* FastArrayPtr, size_t ItemSize, int32 ItemsOffset) override; + int32 Num() const override; + void* GetItem(int32 Index) const override; + int32 GetItemReplicationID(int32 Index) const override; + int32 GetItemReplicationKey(int32 Index) const override; + bool IsItemDirty(int32 Index) const override; + void MarkItemDirty(int32 Index) override; + void MarkAllDirty() override; + int32 GetArrayReplicationKey() const override; + int32 GetIDCounter() const override; + void IncrementArrayReplicationKey() override; + void RegisterChangeCallback(FFastArrayChangeCallback Callback) override; + bool IsNewFormat() const override { return false; } + bool IsInitialized() const override; + + private: + void* m_FastArrayPtr; + size_t m_ItemSize; + int32 m_ItemsOffset; + + // Cached TArray info + uintptr m_ItemsDataPtr; + int32 m_ItemsNum; + int32 m_ItemsMax; + + // Internal offsets within item + static constexpr int32 ReplicationIDOffset = 0; + static constexpr int32 ReplicationKeyOffset = 4; + + // Offset to IDCounter within FFastArraySerializer + int32 m_IDCounterOffset; + + std::vector m_Callbacks; + bool m_bInitialized; + }; + + /** + * Post-8.30 Fast Array Serializer + * + * Layout (restructured): + * struct FFastArraySerializer { + * TArray Items; // 0x00 + * FFastArraySerializerGuidReferences GuidReferencesMap; // 0x10 + * int32 ArrayReplicationKey; // 0x68 (new!) + * int32 IDCounter; // 0x6C + * EFastArraySerializerDeltaFlags DeltaFlags; // 0x70 + * }; + * + * Each item now contains: + * FFastArraySerializerItem base: + * int32 ReplicationID; + * int32 ReplicationKey; + * int32 MostRecentArrayReplicationKey; + */ + class FNewFastArraySerializer : public IFastArraySerializer + { + public: + FNewFastArraySerializer(); + ~FNewFastArraySerializer() override = default; + + EResult Initialize(void* FastArrayPtr, size_t ItemSize, int32 ItemsOffset) override; + int32 Num() const override; + void* GetItem(int32 Index) const override; + int32 GetItemReplicationID(int32 Index) const override; + int32 GetItemReplicationKey(int32 Index) const override; + bool IsItemDirty(int32 Index) const override; + void MarkItemDirty(int32 Index) override; + void MarkAllDirty() override; + int32 GetArrayReplicationKey() const override; + int32 GetIDCounter() const override; + void IncrementArrayReplicationKey() override; + void RegisterChangeCallback(FFastArrayChangeCallback Callback) override; + bool IsNewFormat() const override { return true; } + bool IsInitialized() const override; + + private: + void NotifyChange(const FFastArrayChange& Change); + + void* m_FastArrayPtr; + size_t m_ItemSize; + int32 m_ItemsOffset; + + // Cached TArray info + uintptr m_ItemsDataPtr; + int32 m_ItemsNum; + int32 m_ItemsMax; + + // Internal offsets within item + static constexpr int32 ReplicationIDOffset = 0; + static constexpr int32 ReplicationKeyOffset = 4; + static constexpr int32 MostRecentArrayReplicationKeyOffset = 8; + + // Offsets within FFastArraySerializer (post-8.30) + int32 m_ArrayReplicationKeyOffset; + int32 m_IDCounterOffset; + int32 m_DeltaFlagsOffset; + + int32 m_CachedArrayReplicationKey; + + std::vector m_Callbacks; + bool m_bInitialized; + }; + + /** + * Factory function to create appropriate serializer based on version + */ + std::unique_ptr CreateFastArraySerializer(); + + /** + * Helper to detect fast array changes between ticks + */ + class FFastArrayChangeDetector + { + public: + FFastArrayChangeDetector(); + + /** + * Initialize with a fast array serializer + */ + EResult Initialize(IFastArraySerializer* Serializer); + + /** + * Detect changes since last check + * @param OutChanges - Output list of changes + * @return Number of changes detected + */ + int32 DetectChanges(std::vector& OutChanges); + + /** + * Reset tracking state + */ + void Reset(); + + private: + IFastArraySerializer* m_Serializer; + int32 m_LastArrayReplicationKey; + std::vector m_LastItemKeys; // ReplicationKey per item + std::vector m_LastItemIDs; // ReplicationID per item + int32 m_LastItemCount; + }; + +} \ No newline at end of file diff --git a/Engine/UObject/UObjectWrapper.cpp b/Engine/UObject/UObjectWrapper.cpp new file mode 100644 index 0000000..7d963df --- /dev/null +++ b/Engine/UObject/UObjectWrapper.cpp @@ -0,0 +1,548 @@ +/** + * UniversalSlashingSimulator - UObject Wrapper Implementation + */ + +#include "UObjectWrapper.h" +#include "../../Core/Memory/Memory.h" +#include "../../Core/Versioning/VersionResolver.h" +#include "../CoreTypes/OffsetResolver.h" +#include + +namespace USS +{ + // Static name pool instance (lazy initialized) + static std::unique_ptr g_pNamePool; + + static INamePool* GetNamePoolInstance() + { + if (!g_pNamePool) + { + g_pNamePool = CreateNamePool(); + // Note: Initialization happens separately via EngineCore + } + return g_pNamePool.get(); + } + + //========================================================================= + // FNameWrapper Implementation + //========================================================================= + + std::string FNameWrapper::GetName() const + { + if (m_ComparisonIndex <= 0) + return "None"; + + INamePool* Pool = GetNamePoolInstance(); + if (!Pool || !Pool->IsInitialized()) + return ""; + + return Pool->GetNameString(m_ComparisonIndex); + } + + std::string FNameWrapper::GetFullName() const + { + std::string Name = GetName(); + + if (m_Number > 0) + { + Name += "_"; + Name += std::to_string(m_Number); + } + + return Name; + } + + //========================================================================= + // UObjectWrapper Implementation + //========================================================================= + + bool UObjectWrapper::IsValid() const + { + return m_pObject != nullptr && Memory::IsValidAddress(reinterpret_cast(m_pObject)); + } + + int32 UObjectWrapper::GetObjectFlags() const + { + if (!IsValid()) + return 0; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + int32 Flags = 0; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UObject.ObjectFlags, + Flags + ); + + return Flags; + } + + int32 UObjectWrapper::GetInternalIndex() const + { + if (!IsValid()) + return -1; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + int32 Index = -1; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UObject.InternalIndex, + Index + ); + + return Index; + } + + UClassWrapper UObjectWrapper::GetClass() const + { + if (!IsValid()) + return UClassWrapper(); + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + void* ClassPtr = nullptr; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UObject.Class, + ClassPtr + ); + + return UClassWrapper(ClassPtr); + } + + FNameWrapper UObjectWrapper::GetFName() const + { + if (!IsValid()) + return FNameWrapper(); + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + uintptr NameAddr = reinterpret_cast(m_pObject) + Offsets.UObject.Name; + + int32 ComparisonIndex = 0; + int32 Number = 0; + + Memory::Read(NameAddr + 0, ComparisonIndex); + Memory::Read(NameAddr + 4, Number); + + return FNameWrapper(ComparisonIndex, Number); + } + + UObjectWrapper UObjectWrapper::GetOuter() const + { + if (!IsValid()) + return UObjectWrapper(); + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + void* OuterPtr = nullptr; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UObject.Outer, + OuterPtr + ); + + return UObjectWrapper(OuterPtr); + } + + std::string UObjectWrapper::GetName() const + { + return GetFName().GetFullName(); + } + + std::string UObjectWrapper::GetFullName() const + { + if (!IsValid()) + return ""; + + UClassWrapper Class = GetClass(); + std::string ClassName = Class.IsValid() ? Class.GetName() : "Unknown"; + + return ClassName + " " + GetPathName(); + } + + std::string UObjectWrapper::GetPathName() const + { + if (!IsValid()) + return ""; + + std::string Path; + UObjectWrapper Current = GetOuter(); + + // Build path from outermost to innermost + std::vector Parts; + Parts.push_back(GetName()); + + while (Current.IsValid()) + { + Parts.push_back(Current.GetName()); + Current = Current.GetOuter(); + } + + // Reverse and join + for (auto It = Parts.rbegin(); It != Parts.rend(); ++It) + { + if (!Path.empty()) + Path += "."; + Path += *It; + } + + return Path; + } + + std::string UObjectWrapper::GetObjectClassName() const + { + if (!IsValid()) + return ""; + + UClassWrapper Class = GetClass(); + if (!Class.IsValid()) + return ""; + + return Class.GetName(); + } + + bool UObjectWrapper::IsA(const UClassWrapper& Class) const + { + if (!IsValid() || !Class.IsValid()) + return false; + + UClassWrapper MyClass = GetClass(); + return MyClass.IsChildOf(Class); + } + + bool UObjectWrapper::IsA(const char* ClassName) const + { + if (!IsValid() || !ClassName) + return false; + + UClassWrapper MyClass = GetClass(); + while (MyClass.IsValid()) + { + if (MyClass.GetName() == ClassName) + return true; + MyClass = MyClass.GetSuperClass(); + } + + return false; + } + + //========================================================================= + // UClassWrapper Implementation + //========================================================================= + + UClassWrapper UClassWrapper::GetSuperClass() const + { + if (!IsValid()) + return UClassWrapper(); + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + void* SuperPtr = nullptr; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UStruct.SuperStruct, + SuperPtr + ); + + return UClassWrapper(SuperPtr); + } + + UObjectWrapper UClassWrapper::GetDefaultObject() const + { + if (!IsValid()) + return UObjectWrapper(); + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + void* CDOPtr = nullptr; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UClass.ClassDefaultObject, + CDOPtr + ); + + return UObjectWrapper(CDOPtr); + } + + bool UClassWrapper::IsChildOf(const UClassWrapper& Parent) const + { + if (!IsValid() || !Parent.IsValid()) + return false; + + if (m_pObject == Parent.m_pObject) + return true; + + UClassWrapper Super = GetSuperClass(); + while (Super.IsValid()) + { + if (Super.m_pObject == Parent.m_pObject) + return true; + Super = Super.GetSuperClass(); + } + + return false; + } + + //========================================================================= + // UStructWrapper Implementation + //========================================================================= + + UStructWrapper UStructWrapper::GetSuperStruct() const + { + if (!IsValid()) + return UStructWrapper(); + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + void* SuperPtr = nullptr; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UStruct.SuperStruct, + SuperPtr + ); + + return UStructWrapper(SuperPtr); + } + + int32 UStructWrapper::GetPropertiesSize() const + { + if (!IsValid()) + return 0; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + int32 Size = 0; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UStruct.PropertiesSize, + Size + ); + + return Size; + } + + int32 UStructWrapper::GetMinAlignment() const + { + if (!IsValid()) + return 1; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + int32 Alignment = 1; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UStruct.MinAlignment, + Alignment + ); + + return Alignment; + } + + UStructWrapper::PropertyIterator UStructWrapper::GetProperties() const + { + if (!IsValid()) + return PropertyIterator(nullptr, false); + + const auto& Version = GetVersionResolver().GetVersionInfo(); + const auto& Offsets = GetOffsetResolver().GetOffsets(); + + void* FirstProperty = nullptr; + bool bIsFField = Version.bUseFField; + + if (bIsFField && Offsets.UStruct.ChildProperties != 0) + { + // UE 4.25+: Use ChildProperties (FField*) + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UStruct.ChildProperties, + FirstProperty + ); + } + else + { + // Pre-4.25: Use Children (UField*), filter to properties + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UStruct.Children, + FirstProperty + ); + } + + return PropertyIterator(FirstProperty, bIsFField); + } + + //========================================================================= + // PropertyIterator Implementation + //========================================================================= + + UStructWrapper::PropertyIterator::PropertyIterator(void* First, bool bIsFField) + : m_pCurrent(First) + , m_bIsFField(bIsFField) + { + } + + void UStructWrapper::PropertyIterator::Next() + { + if (!m_pCurrent) + return; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + void* NextPtr = nullptr; + + if (m_bIsFField) + { + Memory::Read( + reinterpret_cast(m_pCurrent) + Offsets.FField.Next, + NextPtr + ); + } + else + { + Memory::Read( + reinterpret_cast(m_pCurrent) + Offsets.UField.Next, + NextPtr + ); + } + + m_pCurrent = NextPtr; + } + + std::string UStructWrapper::PropertyIterator::GetName() const + { + if (!m_pCurrent) + return ""; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + + int32 ComparisonIndex = 0; + int32 Number = 0; + + if (m_bIsFField) + { + uintptr NameAddr = reinterpret_cast(m_pCurrent) + Offsets.FField.NamePrivate; + Memory::Read(NameAddr + 0, ComparisonIndex); + Memory::Read(NameAddr + 4, Number); + } + else + { + // UProperty is a UObject, name at standard offset + uintptr NameAddr = reinterpret_cast(m_pCurrent) + Offsets.UObject.Name; + Memory::Read(NameAddr + 0, ComparisonIndex); + Memory::Read(NameAddr + 4, Number); + } + + return FNameWrapper(ComparisonIndex, Number).GetFullName(); + } + + int32 UStructWrapper::PropertyIterator::GetOffset() const + { + if (!m_pCurrent) + return -1; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + int32 Offset = 0; + + if (m_bIsFField) + { + Memory::Read( + reinterpret_cast(m_pCurrent) + Offsets.FProperty.Offset, + Offset + ); + } + else + { + Memory::Read( + reinterpret_cast(m_pCurrent) + Offsets.UProperty.Offset, + Offset + ); + } + + return Offset; + } + + int32 UStructWrapper::PropertyIterator::GetElementSize() const + { + if (!m_pCurrent) + return 0; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + int32 Size = 0; + + if (m_bIsFField) + { + Memory::Read( + reinterpret_cast(m_pCurrent) + Offsets.FProperty.ElementSize, + Size + ); + } + else + { + Memory::Read( + reinterpret_cast(m_pCurrent) + Offsets.UProperty.ElementSize, + Size + ); + } + + return Size; + } + + //========================================================================= + // UFunctionWrapper Implementation + //========================================================================= + + uint32 UFunctionWrapper::GetFunctionFlags() const + { + if (!IsValid()) + return 0; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + uint32 Flags = 0; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UFunction.FunctionFlags, + Flags + ); + + return Flags; + } + + uint8 UFunctionWrapper::GetNumParms() const + { + if (!IsValid()) + return 0; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + uint8 NumParms = 0; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UFunction.NumParms, + NumParms + ); + + return NumParms; + } + + uint16 UFunctionWrapper::GetParmsSize() const + { + if (!IsValid()) + return 0; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + uint16 Size = 0; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UFunction.ParmsSize, + Size + ); + + return Size; + } + + void* UFunctionWrapper::GetNativeFunc() const + { + if (!IsValid()) + return nullptr; + + const auto& Offsets = GetOffsetResolver().GetOffsets(); + void* Func = nullptr; + + Memory::Read( + reinterpret_cast(m_pObject) + Offsets.UFunction.Func, + Func + ); + + return Func; + } + +} \ No newline at end of file diff --git a/Engine/UObject/UObjectWrapper.h b/Engine/UObject/UObjectWrapper.h new file mode 100644 index 0000000..58e03dc --- /dev/null +++ b/Engine/UObject/UObjectWrapper.h @@ -0,0 +1,193 @@ +/** + * UniversalSlashingSimulator - UObject Wrapper + * + * Provides version-agnostic access to Unreal Engine objects. + * Wraps raw pointers and provides safe access to object members + * using the offset resolver. + * + * This is the foundation for all object manipulation in USS. + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../CoreTypes/NamePool.h" +#include "../CoreTypes/OffsetResolver.h" + +namespace USS +{ + // Forward declarations + class UClassWrapper; + class UStructWrapper; + class UFunctionWrapper; + + // FName wrapper for version-agnostic name access + class FNameWrapper + { + public: + FNameWrapper() : m_ComparisonIndex(0), m_Number(0) {} + FNameWrapper(int32 Index, int32 Number) : m_ComparisonIndex(Index), m_Number(Number) {} + + // Get the name string + std::string GetName() const; + + // Get name with number suffix if non-zero + std::string GetFullName() const; + + // Check if valid + bool IsValid() const { return m_ComparisonIndex > 0; } + + // Comparison + bool operator==(const FNameWrapper& Other) const + { + return m_ComparisonIndex == Other.m_ComparisonIndex && + m_Number == Other.m_Number; + } + + bool operator!=(const FNameWrapper& Other) const + { + return !(*this == Other); + } + + // Raw access + int32 GetComparisonIndex() const { return m_ComparisonIndex; } + int32 GetNumber() const { return m_Number; } + + private: + int32 m_ComparisonIndex; + int32 m_Number; + }; + + // UObject wrapper for version-agnostic object access + class UObjectWrapper + { + public: + UObjectWrapper() : m_pObject(nullptr) {} + explicit UObjectWrapper(void* Object) : m_pObject(Object) {} + + // Check if valid + bool IsValid() const; + explicit operator bool() const { return IsValid(); } + + // Get raw pointer + void* GetRaw() const { return m_pObject; } + + // Get raw pointer as specific type + template + T* GetAs() const { return static_cast(m_pObject); } + + // Object properties + int32 GetObjectFlags() const; + int32 GetInternalIndex() const; + UClassWrapper GetClass() const; + FNameWrapper GetFName() const; + UObjectWrapper GetOuter() const; + + // Get object name as string + std::string GetName() const; + + // Get full path name (Outer.Outer.Name) + std::string GetFullName() const; + + // Get path name (/Path/To/Object) + std::string GetPathName() const; + + // Get class name (name of the UClass) + // NOTE: Named GetObjectClassName to avoid Windows macro conflict + std::string GetObjectClassName() const; + + // Alias for GetObjectClassName (avoid Windows GetClassName macro) + std::string GetClassNameStr() const { return GetObjectClassName(); } + + // Type checking + bool IsA(const UClassWrapper& Class) const; + bool IsA(const char* ClassName) const; + + // Comparison + bool operator==(const UObjectWrapper& Other) const { return m_pObject == Other.m_pObject; } + bool operator!=(const UObjectWrapper& Other) const { return m_pObject != Other.m_pObject; } + + protected: + void* m_pObject; + }; + + // UClass wrapper + class UClassWrapper : public UObjectWrapper + { + public: + UClassWrapper() : UObjectWrapper() {} + explicit UClassWrapper(void* Class) : UObjectWrapper(Class) {} + + // Get super class + UClassWrapper GetSuperClass() const; + + // Get class default object + UObjectWrapper GetDefaultObject() const; + + // Check if this class is a child of another + bool IsChildOf(const UClassWrapper& Parent) const; + }; + + // UStruct wrapper for struct iteration + class UStructWrapper : public UObjectWrapper + { + public: + UStructWrapper() : UObjectWrapper() {} + explicit UStructWrapper(void* Struct) : UObjectWrapper(Struct) {} + + // Get super struct + UStructWrapper GetSuperStruct() const; + + // Get struct size + int32 GetPropertiesSize() const; + + // Get minimum alignment + int32 GetMinAlignment() const; + + // Property iteration (version-aware) + // Pre-4.25: Iterates UProperty via Children + // 4.25+: Iterates FProperty via ChildProperties + class PropertyIterator + { + public: + PropertyIterator(void* First, bool bIsFField); + + bool IsValid() const { return m_pCurrent != nullptr; } + void Next(); + + // Property info + std::string GetName() const; + int32 GetOffset() const; + int32 GetElementSize() const; + + void* GetRaw() const { return m_pCurrent; } + + private: + void* m_pCurrent; + bool m_bIsFField; + }; + + PropertyIterator GetProperties() const; + }; + + // UFunction wrapper + class UFunctionWrapper : public UStructWrapper + { + public: + UFunctionWrapper() : UStructWrapper() {} + explicit UFunctionWrapper(void* Function) : UStructWrapper(Function) {} + + // Get function flags + uint32 GetFunctionFlags() const; + + // Get number of parameters + uint8 GetNumParms() const; + + // Get parameters size + uint16 GetParmsSize() const; + + // Get native function pointer + void* GetNativeFunc() const; + }; + +} \ No newline at end of file diff --git a/Entry/DllMain.cpp b/Entry/DllMain.cpp new file mode 100644 index 0000000..db55091 --- /dev/null +++ b/Entry/DllMain.cpp @@ -0,0 +1,336 @@ +/** + * UniversalSlashingSimulator - DLL Entry Point + * + * Main entry point for the DLL. Initializes all core systems + * + * Injection flow: + * 1. DLL_PROCESS_ATTACH + * 2. Create initialization thread + * 3. Initialize logging + * 4. Parse command-line arguments + * 5. Initialize engine core + * 6. Initialize STW systems (GameMode, Missions, Inventory, Building) + * + * Command-Line Arguments (USS-specific): + * -USS_Mission= Mission blueprint to load + * -USS_Map= Map to load + * -USS_Difficulty=<1-140> Difficulty level + * -USS_MaxPlayers=<1-4> Maximum players + * -USS_Zone= Zone name + * -USS_NoMissions Disable missions + * -USS_NoInventory Disable inventory + * -USS_NoBuilding Disable building + * -USS_Debug Enable debug mode + */ + +#include "../Core/Common.h" +#include "../Core/Logging/Log.h" +#include "../Engine/EngineCore.h" +#include "../STW/GameMode/STWGameMode.h" +#include "../STW/Missions/MissionManager.h" +#include "../STW/Inventory/InventoryManager.h" +#include "../STW/Building/BuildingManager.h" +#include +#include + +namespace USS +{ + // Forward declarations + DWORD WINAPI InitializationThread(LPVOID lpParam); + void ParseCommandLineArguments(FSTWGameConfig& Config); + bool GetCommandLineArg(const char* ArgName, std::string& OutValue); + bool HasCommandLineArg(const char* ArgName); + int GetCommandLineArgInt(const char* ArgName, int DefaultValue); + + // Global module handle + static HMODULE g_hModule = nullptr; + + // Initialization state + static bool g_bInitialized = false; + static bool g_bDebugMode = false; + + /** + * Parse command-line arguments and populate game config + */ + void ParseCommandLineArguments(FSTWGameConfig& Config) + { + // Get command line + const char* CmdLine = GetCommandLineA(); + if (!CmdLine) + return; + + USS_LOG("Parsing command line: %s", CmdLine); + + // Mission blueprint + std::string MissionArg; + if (GetCommandLineArg("-USS_Mission", MissionArg)) + { + Config.MissionBlueprint = MissionArg; + USS_LOG(" Mission: %s", MissionArg.c_str()); + } + + // Map name + std::string MapArg; + if (GetCommandLineArg("-USS_Map", MapArg)) + { + Config.MapName = MapArg; + USS_LOG(" Map: %s", MapArg.c_str()); + } + + // Zone name + std::string ZoneArg; + if (GetCommandLineArg("-USS_Zone", ZoneArg)) + { + Config.ZoneName = ZoneArg; + USS_LOG(" Zone: %s", ZoneArg.c_str()); + } + + // Difficulty level + int Difficulty = GetCommandLineArgInt("-USS_Difficulty", Config.DifficultyLevel); + if (Difficulty >= 1 && Difficulty <= 140) + { + Config.DifficultyLevel = Difficulty; + Config.DefaultDifficulty = Difficulty; + USS_LOG(" Difficulty: %d", Difficulty); + } + + // Max players + int MaxPlayers = GetCommandLineArgInt("-USS_MaxPlayers", Config.MaxPlayers); + if (MaxPlayers >= 1 && MaxPlayers <= 4) + { + Config.MaxPlayers = MaxPlayers; + USS_LOG(" MaxPlayers: %d", MaxPlayers); + } + + // Feature flags + if (HasCommandLineArg("-USS_NoMissions")) + { + Config.bEnableMissions = false; + USS_LOG(" Missions: DISABLED"); + } + + if (HasCommandLineArg("-USS_NoInventory")) + { + Config.bEnableInventory = false; + USS_LOG(" Inventory: DISABLED"); + } + + if (HasCommandLineArg("-USS_NoBuilding")) + { + Config.bEnableBuilding = false; + USS_LOG(" Building: DISABLED"); + } + + if (HasCommandLineArg("-USS_Debug")) + { + g_bDebugMode = true; + USS_LOG(" Debug Mode: ENABLED"); + } + } + + /** + * Get a command-line argument value + * Format: -ArgName=Value or -ArgName="Value With Spaces" + */ + bool GetCommandLineArg(const char* ArgName, std::string& OutValue) + { + const char* CmdLine = GetCommandLineA(); + if (!CmdLine || !ArgName) + return false; + + // Find argument + const char* ArgPos = strstr(CmdLine, ArgName); + if (!ArgPos) + return false; + + // Skip to value (after '=') + const char* ValueStart = ArgPos + strlen(ArgName); + if (*ValueStart != '=') + return false; + + ValueStart++; // Skip '=' + + // Handle quoted values + if (*ValueStart == '"') + { + ValueStart++; + const char* ValueEnd = strchr(ValueStart, '"'); + if (ValueEnd) + { + OutValue = std::string(ValueStart, ValueEnd - ValueStart); + return true; + } + } + else + { + // Unquoted - read until space or end + const char* ValueEnd = ValueStart; + while (*ValueEnd && *ValueEnd != ' ' && *ValueEnd != '\t') + ValueEnd++; + + OutValue = std::string(ValueStart, ValueEnd - ValueStart); + return true; + } + + return false; + } + + /** + * Check if a command-line flag is present + */ + bool HasCommandLineArg(const char* ArgName) + { + const char* CmdLine = GetCommandLineA(); + if (!CmdLine || !ArgName) + return false; + + return strstr(CmdLine, ArgName) != nullptr; + } + + /** + * Get an integer command-line argument + */ + int GetCommandLineArgInt(const char* ArgName, int DefaultValue) + { + std::string Value; + if (!GetCommandLineArg(ArgName, Value)) + return DefaultValue; + + try + { + return std::stoi(Value); + } + catch (...) + { + return DefaultValue; + } + } + + DWORD WINAPI InitializationThread(LPVOID lpParam) + { + EResult Result = Log::Initialize(true, "USS_Log.txt"); + if (Result != EResult::Success) + { + MessageBoxA(nullptr, "Failed to initialize logging", "USS Error", MB_ICONERROR); + return 1; + } + + USS_LOG("========================================"); + USS_LOG(" UniversalSlashingSimulator v0.1.0"); + USS_LOG(" STW Gameserver Framework"); + USS_LOG("========================================"); + USS_LOG(""); + + // Wait for game to be ready + // TODO: find trigger in log, perhaps we need a way to trick the game into going to the STW lobby in headless? + USS_LOG("Waiting for game initialization..."); + Sleep(5000); // Placeholder + + USS_LOG("Initializing engine core..."); + Result = GetEngineCore().Initialize(); + + if (Result != EResult::Success) + { + USS_FATAL("Engine core initialization failed: %s", ResultToString(Result)); + MessageBoxA(nullptr, "Failed to initialize engine core", "USS Error", MB_ICONERROR); + return 1; + } + + // Log version info + const auto& Version = GetEngineCore().GetVersionInfo(); + USS_LOG(""); + USS_LOG("=== Version Information ==="); + USS_LOG("Engine: UE %s", Version.GetEngineVersionString().c_str()); + USS_LOG("Fortnite: %.2f", Version.FortniteVersion); + USS_LOG("CL: %u", Version.FortniteCL); + USS_LOG("Generation: %s", Version.GetGenerationName()); + USS_LOG(""); + USS_LOG("=== Feature Flags ==="); + USS_LOG("FNamePool: %s", Version.bUseFNamePool ? "Yes" : "No"); + USS_LOG("FField: %s", Version.bUseFField ? "Yes" : "No"); + USS_LOG("ChunkedObjects: %s", Version.bUseChunkedObjects ? "Yes" : "No"); + USS_LOG("NewFastArray: %s", Version.bUseNewFastArraySerializer ? "Yes" : "No"); + USS_LOG("TObjectPtr: %s", Version.bUseTObjectPtr ? "Yes" : "No"); + USS_LOG(""); + + // Initialize STW systems + USS_LOG("Initializing STW systems..."); + + // GameMode configuration with defaults + FSTWGameConfig GameConfig; + GameConfig.bEnableMissions = true; + GameConfig.bEnableInventory = true; + GameConfig.bEnableBuilding = true; + GameConfig.MaxPlayers = 4; + GameConfig.DefaultDifficulty = 1; + GameConfig.DifficultyLevel = 1; + GameConfig.MissionType = EMissionType::FarmsteadDefense; + GameConfig.ZoneName = "Zone_Onboarding_FarmsteadFort"; + GameConfig.MissionBlueprint = "Mission_FarmsteadFort_C"; + + // Parse command-line arguments to override defaults + USS_LOG(""); + USS_LOG("=== Command Line Configuration ==="); + ParseCommandLineArguments(GameConfig); + USS_LOG(""); + + // Initialize GameMode (this initializes all subsystems) + Result = GetSTWGameMode().Initialize(GameConfig); + if (Result != EResult::Success) + { + USS_ERROR("STW GameMode initialization failed: %s", ResultToString(Result)); + // Continue anyway - partial functionality may still work + } + else + { + USS_LOG("STW systems initialized successfully"); + } + + USS_LOG(""); + USS_LOG("UniversalSlashingSimulator initialized successfully"); + USS_LOG("========================================"); + + g_bInitialized = true; + return 0; + } + + void Shutdown() + { + if (!g_bInitialized) + return; + + USS_LOG("Shutting down UniversalSlashingSimulator..."); + + GetSTWGameMode().Shutdown(); + + GetEngineCore().Shutdown(); + + Log::Shutdown(); + + g_bInitialized = false; + } + +} + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) +{ + switch (dwReason) + { + case DLL_PROCESS_ATTACH: + DisableThreadLibraryCalls(hModule); + USS::g_hModule = hModule; + + CreateThread(nullptr, 0, USS::InitializationThread, nullptr, 0, nullptr); + break; + + case DLL_PROCESS_DETACH: + USS::Shutdown(); + break; + + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + } + + return TRUE; +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8437360 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# UniversalSlashingSimulator + +## WIP Universal STW Gameserver \ No newline at end of file diff --git a/STW/Building/BuildingManager.cpp b/STW/Building/BuildingManager.cpp new file mode 100644 index 0000000..f5cd50b --- /dev/null +++ b/STW/Building/BuildingManager.cpp @@ -0,0 +1,830 @@ +/** + * UniversalSlashingSimulator - Building Manager Implementation + */ + +#include "BuildingManager.h" +#include "../Inventory/InventoryManager.h" +#include "../../Core/Logging/Log.h" +#include "../../Engine/EngineCore.h" + +namespace USS +{ + static FBuildingManager g_LocalBuildingManager; + + FBuildingManager& GetLocalBuildingManager() + { + return g_LocalBuildingManager; + } + + FBuildingManager::FBuildingManager() + { + m_BuildPreview = FBuildPreview(); + } + + FBuildingManager::~FBuildingManager() + { + Shutdown(); + } + + EResult FBuildingManager::Initialize(FInventoryManager* InventoryManager) + { + USS_LOG("Initializing Building Manager..."); + + m_pInventoryManager = InventoryManager; + + m_Buildings.clear(); + m_Traps.clear(); + m_GridToBuildingId.clear(); + m_EventCallbacks.clear(); + + m_bIsInBuildMode = false; + m_bIsPlacingTrap = false; + m_CurrentBuildType = EBuildingType::None; + m_CurrentMaterial = EBuildingMaterial::Wood; + + // Find building manager actors in world + // TODO: Locate AFortBuildingManager + + USS_LOG("Building Manager initialized"); + return EResult::Success; + } + + void FBuildingManager::Shutdown() + { + ExitBuildMode(); + ExitTrapPlacementMode(); + + m_Buildings.clear(); + m_Traps.clear(); + m_GridToBuildingId.clear(); + m_EventCallbacks.clear(); + + m_pInventoryManager = nullptr; + m_BuildingManagerActor = UObjectWrapper(); + m_TrapManagerActor = UObjectWrapper(); + } + + void FBuildingManager::Update() + { + // Update building construction progress + for (auto& Pair : m_Buildings) + { + FBuildingPiece& Building = Pair.second; + + if (Building.State == EBuildingState::Building) + { + // Progress construction + // TODO: Check actual build progress from engine + } + else if (Building.State == EBuildingState::Repairing) + { + // Progress repair + float DeltaTime = 1.0f / 30.0f; // Placeholder + Building.Stats.CurrentHealth += Building.Stats.RepairRate * DeltaTime; + + if (Building.Stats.CurrentHealth >= Building.Stats.MaxHealth) + { + Building.Stats.CurrentHealth = Building.Stats.MaxHealth; + Building.State = EBuildingState::Built; + } + } + } + + // Update traps + float DeltaTime = 1.0f / 30.0f; // Placeholder + UpdateTraps(DeltaTime); + } + + EResult FBuildingManager::EnterBuildMode(EBuildingType Type) + { + if (Type == EBuildingType::None || Type == EBuildingType::Trap) + { + return EResult::InvalidParameter; + } + + m_bIsInBuildMode = true; + m_CurrentBuildType = Type; + + // Initialize preview + m_BuildPreview = FBuildPreview(); + m_BuildPreview.Type = Type; + m_BuildPreview.Material = m_CurrentMaterial; + m_BuildPreview.Cost = GetDefaultBuildCost(m_CurrentMaterial); + + USS_LOG("Entered build mode: Type=%d, Material=%d", + static_cast(Type), static_cast(m_CurrentMaterial)); + + return EResult::Success; + } + + void FBuildingManager::ExitBuildMode() + { + if (m_bIsInBuildMode) + { + m_bIsInBuildMode = false; + m_CurrentBuildType = EBuildingType::None; + m_BuildPreview = FBuildPreview(); + + USS_LOG("Exited build mode"); + } + } + + void FBuildingManager::SetBuildMaterial(EBuildingMaterial Material) + { + m_CurrentMaterial = Material; + + if (m_bIsInBuildMode) + { + m_BuildPreview.Material = Material; + m_BuildPreview.Cost = GetDefaultBuildCost(Material); + } + + USS_LOG("Set build material: %d", static_cast(Material)); + } + + void FBuildingManager::CycleMaterial() + { + switch (m_CurrentMaterial) + { + case EBuildingMaterial::Wood: + SetBuildMaterial(EBuildingMaterial::Stone); + break; + case EBuildingMaterial::Stone: + SetBuildMaterial(EBuildingMaterial::Metal); + break; + case EBuildingMaterial::Metal: + default: + SetBuildMaterial(EBuildingMaterial::Wood); + break; + } + } + + void FBuildingManager::UpdateBuildPreview(float LocationX, float LocationY, float LocationZ, float Rotation) + { + if (!m_bIsInBuildMode) + return; + + m_BuildPreview.LocationX = LocationX; + m_BuildPreview.LocationY = LocationY; + m_BuildPreview.LocationZ = LocationZ; + m_BuildPreview.Rotation = Rotation; + + // Validate placement + m_BuildPreview.bIsOverlapping = CheckOverlap(LocationX, LocationY, LocationZ); + m_BuildPreview.bIsFloating = !CheckSupport(LocationX, LocationY, LocationZ, m_BuildPreview.Type); + + m_BuildPreview.bIsValidPlacement = !m_BuildPreview.bIsOverlapping && !m_BuildPreview.bIsFloating; + + // Check resources + if (m_pInventoryManager) + { + int32 Wood = m_pInventoryManager->GetWoodCount(); + int32 Stone = m_pInventoryManager->GetStoneCount(); + int32 Metal = m_pInventoryManager->GetMetalCount(); + + m_BuildPreview.bCanAfford = m_BuildPreview.Cost.CanAfford(Wood, Stone, Metal); + } + } + + EResult FBuildingManager::ConfirmBuild() + { + if (!m_bIsInBuildMode) + { + return EResult::InvalidState; + } + + if (!m_BuildPreview.bIsValidPlacement) + { + USS_WARN("Cannot build: Invalid placement"); + return EResult::InvalidPlacement; + } + + if (!m_BuildPreview.bCanAfford) + { + USS_WARN("Cannot build: Insufficient resources"); + return EResult::InsufficientResources; + } + + if (IsAtBuildLimit()) + { + USS_WARN("Cannot build: At build limit"); + return EResult::BuildLimitReached; + } + + // Consume resources + if (m_pInventoryManager) + { + const FBuildingCost& Cost = m_BuildPreview.Cost; + + if (Cost.WoodCost > 0) + m_pInventoryManager->ConsumeResources(EResourceType::Wood, Cost.WoodCost); + if (Cost.StoneCost > 0) + m_pInventoryManager->ConsumeResources(EResourceType::Stone, Cost.StoneCost); + if (Cost.MetalCost > 0) + m_pInventoryManager->ConsumeResources(EResourceType::Metal, Cost.MetalCost); + } + + // Create building + FBuildingPiece NewBuilding; + NewBuilding.BuildingId = GenerateBuildingId(); + NewBuilding.Type = m_BuildPreview.Type; + NewBuilding.Material = m_BuildPreview.Material; + NewBuilding.Tier = EBuildingTier::Tier1; + NewBuilding.State = EBuildingState::Building; + NewBuilding.Stats = GetDefaultBuildStats(m_BuildPreview.Material); + NewBuilding.Cost = m_BuildPreview.Cost; + NewBuilding.bIsPlayerBuilt = true; + + // Apply build speed multiplier + NewBuilding.Stats.BuildTime /= m_BuildSpeedMultiplier; + + // Grid position (TODO: implement proper grid snapping) + NewBuilding.GridX = static_cast(m_BuildPreview.LocationX / 512.0f); + NewBuilding.GridY = static_cast(m_BuildPreview.LocationY / 512.0f); + NewBuilding.GridZ = static_cast(m_BuildPreview.LocationZ / 512.0f); + NewBuilding.Rotation = m_BuildPreview.Rotation; + + // Store building + m_Buildings[NewBuilding.BuildingId] = NewBuilding; + + // Register in grid lookup + uint64 GridKey = (static_cast(NewBuilding.GridX) << 40) | + (static_cast(NewBuilding.GridY) << 20) | + static_cast(NewBuilding.GridZ); + m_GridToBuildingId[GridKey] = NewBuilding.BuildingId; + + // Notify + FBuildingChangeEvent Event; + Event.Type = FBuildingChangeEvent::EChangeType::Built; + Event.BuildingId = NewBuilding.BuildingId; + Event.NewHealth = NewBuilding.Stats.CurrentHealth; + NotifyChange(Event); + + USS_LOG("Built %d at grid (%d, %d, %d) - ID: %s", + static_cast(NewBuilding.Type), + NewBuilding.GridX, NewBuilding.GridY, NewBuilding.GridZ, + NewBuilding.BuildingId.c_str()); + + return EResult::Success; + } + + void FBuildingManager::CancelBuild() + { + m_BuildPreview = FBuildPreview(); + m_BuildPreview.Type = m_CurrentBuildType; + m_BuildPreview.Material = m_CurrentMaterial; + } + + const FBuildingPiece* FBuildingManager::GetBuilding(const std::string& BuildingId) const + { + auto It = m_Buildings.find(BuildingId); + return (It != m_Buildings.end()) ? &It->second : nullptr; + } + + const FBuildingPiece* FBuildingManager::GetBuildingAtGrid(int32 X, int32 Y, int32 Z) const + { + uint64 GridKey = (static_cast(X) << 40) | + (static_cast(Y) << 20) | + static_cast(Z); + + auto It = m_GridToBuildingId.find(GridKey); + if (It != m_GridToBuildingId.end()) + { + return GetBuilding(It->second); + } + return nullptr; + } + + std::vector FBuildingManager::GetPlayerBuildings(const std::string& PlayerId) const + { + std::vector Result; + + for (const auto& Pair : m_Buildings) + { + if (Pair.second.OwnerId == PlayerId || Pair.second.bIsPlayerBuilt) + { + Result.push_back(&Pair.second); + } + } + + return Result; + } + + int32 FBuildingManager::GetPlayerBuildingCount(const std::string& PlayerId) const + { + int32 Count = 0; + + for (const auto& Pair : m_Buildings) + { + if (Pair.second.OwnerId == PlayerId || Pair.second.bIsPlayerBuilt) + { + ++Count; + } + } + + return Count; + } + + EResult FBuildingManager::DamageBuilding(const std::string& BuildingId, float Damage, void* DamageCauser) + { + auto It = m_Buildings.find(BuildingId); + if (It == m_Buildings.end()) + { + return EResult::BuildingNotFound; + } + + FBuildingPiece& Building = It->second; + float OldHealth = Building.Stats.CurrentHealth; + + // Apply damage resistance + float ActualDamage = Damage * (1.0f - Building.Stats.DamageResistance); + Building.Stats.CurrentHealth -= ActualDamage; + + FBuildingChangeEvent Event; + Event.BuildingId = BuildingId; + Event.OldHealth = OldHealth; + Event.NewHealth = Building.Stats.CurrentHealth; + Event.Damage = ActualDamage; + Event.DamageCauser = DamageCauser; + + if (Building.Stats.CurrentHealth <= 0.0f) + { + Building.Stats.CurrentHealth = 0.0f; + Building.State = EBuildingState::Destroying; + + Event.Type = FBuildingChangeEvent::EChangeType::Destroyed; + + // Remove from grid + uint64 GridKey = (static_cast(Building.GridX) << 40) | + (static_cast(Building.GridY) << 20) | + static_cast(Building.GridZ); + m_GridToBuildingId.erase(GridKey); + + // Remove traps attached to this building + std::vector TrapsToRemove; + for (const auto& TrapPair : m_Traps) + { + if (TrapPair.second.AttachedBuildingId == BuildingId) + { + TrapsToRemove.push_back(TrapPair.first); + } + } + for (const auto& TrapId : TrapsToRemove) + { + m_Traps.erase(TrapId); + } + + m_Buildings.erase(It); + + USS_LOG("Building destroyed: %s", BuildingId.c_str()); + } + else + { + Building.State = EBuildingState::Damaged; + Event.Type = FBuildingChangeEvent::EChangeType::Damaged; + } + + NotifyChange(Event); + + return EResult::Success; + } + + EResult FBuildingManager::RepairBuilding(const std::string& BuildingId, float Amount) + { + auto It = m_Buildings.find(BuildingId); + if (It == m_Buildings.end()) + { + return EResult::BuildingNotFound; + } + + FBuildingPiece& Building = It->second; + + if (Building.IsFullHealth()) + { + return EResult::Success; // Already full + } + + float OldHealth = Building.Stats.CurrentHealth; + Building.Stats.CurrentHealth += Amount; + + if (Building.Stats.CurrentHealth >= Building.Stats.MaxHealth) + { + Building.Stats.CurrentHealth = Building.Stats.MaxHealth; + Building.State = EBuildingState::Built; + } + else + { + Building.State = EBuildingState::Repairing; + } + + FBuildingChangeEvent Event; + Event.Type = FBuildingChangeEvent::EChangeType::Repaired; + Event.BuildingId = BuildingId; + Event.OldHealth = OldHealth; + Event.NewHealth = Building.Stats.CurrentHealth; + NotifyChange(Event); + + return EResult::Success; + } + + EResult FBuildingManager::UpgradeBuilding(const std::string& BuildingId) + { + auto It = m_Buildings.find(BuildingId); + if (It == m_Buildings.end()) + { + return EResult::BuildingNotFound; + } + + FBuildingPiece& Building = It->second; + + if (Building.Tier >= EBuildingTier::Tier3) + { + USS_WARN("Building already at max tier"); + return EResult::InvalidState; + } + + // Check upgrade cost (STW uses different resources for upgrades) + // TODO: Consume upgrade materials + + // Upgrade tier + Building.Tier = static_cast(static_cast(Building.Tier) + 1); + Building.State = EBuildingState::Upgrading; + + // Increase stats + Building.Stats.MaxHealth *= 1.5f; + Building.Stats.CurrentHealth = Building.Stats.MaxHealth; + Building.Stats.DamageResistance += 0.05f; + + FBuildingChangeEvent Event; + Event.Type = FBuildingChangeEvent::EChangeType::Upgraded; + Event.BuildingId = BuildingId; + Event.NewHealth = Building.Stats.CurrentHealth; + NotifyChange(Event); + + USS_LOG("Upgraded building %s to tier %d", BuildingId.c_str(), static_cast(Building.Tier)); + + Building.State = EBuildingState::Built; + + return EResult::Success; + } + + EResult FBuildingManager::DemolishBuilding(const std::string& BuildingId) + { + auto It = m_Buildings.find(BuildingId); + if (It == m_Buildings.end()) + { + return EResult::BuildingNotFound; + } + + const FBuildingPiece& Building = It->second; + + // Refund partial resources (50%) + if (m_pInventoryManager) + { + FInventoryItem Refund; + Refund.Category = EItemCategory::Resource; + + if (Building.Cost.WoodCost > 0) + { + Refund.TemplateId = "Resource:Wood"; + Refund.Count = Building.Cost.WoodCost / 2; + m_pInventoryManager->AddItem(Refund); + } + if (Building.Cost.StoneCost > 0) + { + Refund.TemplateId = "Resource:Stone"; + Refund.Count = Building.Cost.StoneCost / 2; + m_pInventoryManager->AddItem(Refund); + } + if (Building.Cost.MetalCost > 0) + { + Refund.TemplateId = "Resource:Metal"; + Refund.Count = Building.Cost.MetalCost / 2; + m_pInventoryManager->AddItem(Refund); + } + } + + // Remove from grid + uint64 GridKey = (static_cast(Building.GridX) << 40) | + (static_cast(Building.GridY) << 20) | + static_cast(Building.GridZ); + m_GridToBuildingId.erase(GridKey); + + // Remove traps + std::vector TrapsToRemove; + for (const auto& TrapPair : m_Traps) + { + if (TrapPair.second.AttachedBuildingId == BuildingId) + { + TrapsToRemove.push_back(TrapPair.first); + } + } + for (const auto& TrapId : TrapsToRemove) + { + m_Traps.erase(TrapId); + } + + m_Buildings.erase(It); + + FBuildingChangeEvent Event; + Event.Type = FBuildingChangeEvent::EChangeType::Destroyed; + Event.BuildingId = BuildingId; + NotifyChange(Event); + + USS_LOG("Demolished building: %s", BuildingId.c_str()); + + return EResult::Success; + } + + EResult FBuildingManager::EditBuilding(const std::string& BuildingId, EBuildingType NewType) + { + auto It = m_Buildings.find(BuildingId); + if (It == m_Buildings.end()) + { + return EResult::BuildingNotFound; + } + + // Can only edit walls/floors/etc + FBuildingPiece& Building = It->second; + + if (Building.Type == EBuildingType::Trap || + Building.Type == EBuildingType::DefenseBuild || + Building.Type == EBuildingType::StructuralBuild) + { + return EResult::InvalidParameter; + } + + Building.Type = NewType; + + USS_LOG("Edited building %s to type %d", BuildingId.c_str(), static_cast(NewType)); + + return EResult::Success; + } + + EResult FBuildingManager::EnterTrapPlacementMode(ETrapType TrapType, const std::string& TrapItemId) + { + if (TrapType == ETrapType::None) + { + return EResult::InvalidParameter; + } + + m_bIsPlacingTrap = true; + m_CurrentTrapType = TrapType; + m_CurrentTrapItemId = TrapItemId; + + USS_LOG("Entered trap placement mode: Type=%d", static_cast(TrapType)); + + return EResult::Success; + } + + void FBuildingManager::ExitTrapPlacementMode() + { + if (m_bIsPlacingTrap) + { + m_bIsPlacingTrap = false; + m_CurrentTrapType = ETrapType::None; + m_CurrentTrapItemId.clear(); + + USS_LOG("Exited trap placement mode"); + } + } + + EResult FBuildingManager::ConfirmTrapPlacement(const std::string& AttachedBuildingId) + { + if (!m_bIsPlacingTrap) + { + return EResult::InvalidState; + } + + // Verify building exists + const FBuildingPiece* Building = GetBuilding(AttachedBuildingId); + if (!Building) + { + return EResult::BuildingNotFound; + } + + // Create trap + FTrapInstance NewTrap; + NewTrap.TrapId = GenerateTrapId(); + NewTrap.Type = m_CurrentTrapType; + NewTrap.State = EBuildingState::Built; + NewTrap.AttachedBuildingId = AttachedBuildingId; + NewTrap.GridX = Building->GridX; + NewTrap.GridY = Building->GridY; + NewTrap.GridZ = Building->GridZ; + NewTrap.bIsArmed = true; + + // Set default trap stats + NewTrap.Stats.Damage = 50.0f * m_TrapDamageMultiplier; + NewTrap.Stats.ReloadTime = 2.0f; + NewTrap.Stats.MaxDurability = 30; + NewTrap.Stats.CurrentDurability = 30; + + // Consume trap item from inventory + if (m_pInventoryManager && !m_CurrentTrapItemId.empty()) + { + m_pInventoryManager->RemoveItem(m_CurrentTrapItemId, 1); + } + + m_Traps[NewTrap.TrapId] = NewTrap; + + FBuildingChangeEvent Event; + Event.Type = FBuildingChangeEvent::EChangeType::TrapPlaced; + Event.BuildingId = NewTrap.TrapId; + NotifyChange(Event); + + USS_LOG("Placed trap %s on building %s", NewTrap.TrapId.c_str(), AttachedBuildingId.c_str()); + + return EResult::Success; + } + + const FTrapInstance* FBuildingManager::GetTrap(const std::string& TrapId) const + { + auto It = m_Traps.find(TrapId); + return (It != m_Traps.end()) ? &It->second : nullptr; + } + + std::vector FBuildingManager::GetTrapsOnBuilding(const std::string& BuildingId) const + { + std::vector Result; + + for (const auto& Pair : m_Traps) + { + if (Pair.second.AttachedBuildingId == BuildingId) + { + Result.push_back(&Pair.second); + } + } + + return Result; + } + + EResult FBuildingManager::TriggerTrap(const std::string& TrapId) + { + auto It = m_Traps.find(TrapId); + if (It == m_Traps.end()) + { + return EResult::TrapNotFound; + } + + FTrapInstance& Trap = It->second; + + if (!Trap.IsReady()) + { + return EResult::TrapNotReady; + } + + ProcessTrapTrigger(Trap); + + return EResult::Success; + } + + EResult FBuildingManager::ReloadTrap(const std::string& TrapId) + { + auto It = m_Traps.find(TrapId); + if (It == m_Traps.end()) + { + return EResult::TrapNotFound; + } + + FTrapInstance& Trap = It->second; + Trap.CooldownRemaining = 0.0f; + Trap.bIsArmed = true; + + USS_LOG("Reloaded trap: %s", TrapId.c_str()); + + return EResult::Success; + } + + bool FBuildingManager::IsAtBuildLimit() const + { + return static_cast(m_Buildings.size()) >= m_BuildLimit; + } + + void FBuildingManager::ApplyConstructorPerks(float BuildSpeed, float TrapDamage, int32 ExtraBuildLimit) + { + m_BuildSpeedMultiplier = BuildSpeed; + m_TrapDamageMultiplier = TrapDamage; + m_BuildLimit += ExtraBuildLimit; + + USS_LOG("Applied constructor perks: BuildSpeed=%.2f, TrapDamage=%.2f, ExtraLimit=%d", + BuildSpeed, TrapDamage, ExtraBuildLimit); + } + + void FBuildingManager::RegisterEventCallback(FBuildingEventCallback Callback) + { + if (Callback) + { + m_EventCallbacks.push_back(std::move(Callback)); + } + } + + void FBuildingManager::OnProcessEvent(void* Object, void* Function, void* Params) + { + // Handle building-related ProcessEvents + } + + std::string FBuildingManager::GenerateBuildingId() const + { + char Buffer[64]; + snprintf(Buffer, sizeof(Buffer), "bld_%u", ++m_BuildingIdCounter); + return Buffer; + } + + std::string FBuildingManager::GenerateTrapId() const + { + char Buffer[64]; + snprintf(Buffer, sizeof(Buffer), "trap_%u", ++m_TrapIdCounter); + return Buffer; + } + + bool FBuildingManager::ValidatePlacement(const FBuildPreview& Preview) const + { + return Preview.bIsValidPlacement && Preview.bCanAfford; + } + + bool FBuildingManager::CheckOverlap(float X, float Y, float Z) const + { + int32 GridX = static_cast(X / 512.0f); + int32 GridY = static_cast(Y / 512.0f); + int32 GridZ = static_cast(Z / 512.0f); + + return GetBuildingAtGrid(GridX, GridY, GridZ) != nullptr; + } + + bool FBuildingManager::CheckSupport(float X, float Y, float Z, EBuildingType Type) const + { + // Floors can be placed on ground or supported by walls + // Walls need floor below or adjacent wall + // Ramps need floor or ground + + // Simplified: Always allow for now + // TODO: Would check adjacent buildings and terrain + + return true; + } + + void FBuildingManager::NotifyChange(const FBuildingChangeEvent& Event) + { + for (const auto& Callback : m_EventCallbacks) + { + if (Callback) + { + Callback(Event); + } + } + } + + void FBuildingManager::UpdateTraps(float DeltaTime) + { + for (auto& Pair : m_Traps) + { + FTrapInstance& Trap = Pair.second; + + // Update cooldown + if (Trap.CooldownRemaining > 0.0f) + { + Trap.CooldownRemaining -= DeltaTime; + if (Trap.CooldownRemaining < 0.0f) + { + Trap.CooldownRemaining = 0.0f; + } + } + + // TODO: Check for enemies in range and trigger + } + } + + void FBuildingManager::ProcessTrapTrigger(FTrapInstance& Trap) + { + Trap.bIsTriggered = true; + Trap.Stats.CurrentDurability -= Trap.Stats.UsesPerActivation; + Trap.CooldownRemaining = Trap.Stats.ReloadTime; + + FBuildingChangeEvent Event; + Event.Type = FBuildingChangeEvent::EChangeType::TrapTriggered; + Event.BuildingId = Trap.TrapId; + NotifyChange(Event); + + USS_LOG("Trap triggered: %s (durability: %d/%d)", + Trap.TrapId.c_str(), + Trap.Stats.CurrentDurability, + Trap.Stats.MaxDurability); + + if (Trap.Stats.CurrentDurability <= 0) + { + Trap.bIsArmed = false; + Trap.State = EBuildingState::Destroying; + + FBuildingChangeEvent DestroyEvent; + DestroyEvent.Type = FBuildingChangeEvent::EChangeType::TrapDestroyed; + DestroyEvent.BuildingId = Trap.TrapId; + NotifyChange(DestroyEvent); + + USS_LOG("Trap exhausted: %s", Trap.TrapId.c_str()); + } + + Trap.bIsTriggered = false; + } + +} \ No newline at end of file diff --git a/STW/Building/BuildingManager.h b/STW/Building/BuildingManager.h new file mode 100644 index 0000000..d9b07b6 --- /dev/null +++ b/STW/Building/BuildingManager.h @@ -0,0 +1,322 @@ +/** + * UniversalSlashingSimulator - Building Manager + * + * Manages STW building system: construction, traps, upgrades, and repairs. + */ + +#pragma once + +#include "BuildingTypes.h" +#include "../../Engine/UObject/UObjectWrapper.h" +#include +#include +#include + +namespace USS +{ + // Forward declarations + class FInventoryManager; + + /** + * Callback for building events + */ + using FBuildingEventCallback = std::function; + + /** + * Building Manager + * + * Handles all building operations including: + * - Building placement and preview + * - Material selection and switching + * - Trap placement and management + * - Building upgrades (STW tiers) + * - Repair and demolition + * - Build limit tracking + */ + class FBuildingManager + { + public: + FBuildingManager(); + ~FBuildingManager(); + + // Lifecycle + EResult Initialize(FInventoryManager* InventoryManager); + void Shutdown(); + void Update(); + + // =========================================== + // Building Operations + // =========================================== + + /** + * Enter build mode for a specific type + */ + EResult EnterBuildMode(EBuildingType Type); + + /** + * Exit build mode + */ + void ExitBuildMode(); + + /** + * Check if currently in build mode + */ + bool IsInBuildMode() const { return m_bIsInBuildMode; } + + /** + * Get current build type being previewed + */ + EBuildingType GetCurrentBuildType() const { return m_CurrentBuildType; } + + /** + * Set building material + */ + void SetBuildMaterial(EBuildingMaterial Material); + + /** + * Get current building material + */ + EBuildingMaterial GetBuildMaterial() const { return m_CurrentMaterial; } + + /** + * Cycle to next material + */ + void CycleMaterial(); + + /** + * Update build preview position + */ + void UpdateBuildPreview(float LocationX, float LocationY, float LocationZ, float Rotation); + + /** + * Get current build preview + */ + const FBuildPreview& GetBuildPreview() const { return m_BuildPreview; } + + /** + * Confirm placement at current preview location + */ + EResult ConfirmBuild(); + + /** + * Cancel current build operation + */ + void CancelBuild(); + + // =========================================== + // Building Access + // =========================================== + + /** + * Get building by ID + */ + const FBuildingPiece* GetBuilding(const std::string& BuildingId) const; + + /** + * Get building at grid position + */ + const FBuildingPiece* GetBuildingAtGrid(int32 X, int32 Y, int32 Z) const; + + /** + * Get all buildings owned by player + */ + std::vector GetPlayerBuildings(const std::string& PlayerId) const; + + /** + * Get total building count + */ + int32 GetBuildingCount() const { return static_cast(m_Buildings.size()); } + + /** + * Get player's building count + */ + int32 GetPlayerBuildingCount(const std::string& PlayerId) const; + + // =========================================== + // Building Modifications + // =========================================== + + /** + * Damage a building + */ + EResult DamageBuilding(const std::string& BuildingId, float Damage, void* DamageCauser); + + /** + * Repair a building + */ + EResult RepairBuilding(const std::string& BuildingId, float Amount); + + /** + * Upgrade building to next tier (STW) + */ + EResult UpgradeBuilding(const std::string& BuildingId); + + /** + * Demolish a building (refund partial materials) + */ + EResult DemolishBuilding(const std::string& BuildingId); + + /** + * Edit building piece (change type/shape) + */ + EResult EditBuilding(const std::string& BuildingId, EBuildingType NewType); + + // =========================================== + // Trap Operations + // =========================================== + + /** + * Enter trap placement mode + */ + EResult EnterTrapPlacementMode(ETrapType TrapType, const std::string& TrapItemId); + + /** + * Exit trap placement mode + */ + void ExitTrapPlacementMode(); + + /** + * Check if in trap placement mode + */ + bool IsInTrapPlacementMode() const { return m_bIsPlacingTrap; } + + /** + * Confirm trap placement + */ + EResult ConfirmTrapPlacement(const std::string& AttachedBuildingId); + + /** + * Get trap by ID + */ + const FTrapInstance* GetTrap(const std::string& TrapId) const; + + /** + * Get traps attached to building + */ + std::vector GetTrapsOnBuilding(const std::string& BuildingId) const; + + /** + * Trigger trap manually (for testing) + */ + EResult TriggerTrap(const std::string& TrapId); + + /** + * Reload/reset trap + */ + EResult ReloadTrap(const std::string& TrapId); + + // =========================================== + // STW-Specific Features + // =========================================== + + /** + * Get current build limit (based on constructor bonus, etc.) + */ + int32 GetBuildLimit() const { return m_BuildLimit; } + + /** + * Set build limit + */ + void SetBuildLimit(int32 Limit) { m_BuildLimit = Limit; } + + /** + * Check if at build limit + */ + bool IsAtBuildLimit() const; + + /** + * Get building speed multiplier (constructor bonus) + */ + float GetBuildSpeedMultiplier() const { return m_BuildSpeedMultiplier; } + + /** + * Set building speed multiplier + */ + void SetBuildSpeedMultiplier(float Multiplier) { m_BuildSpeedMultiplier = Multiplier; } + + /** + * Get trap damage multiplier (constructor bonus) + */ + float GetTrapDamageMultiplier() const { return m_TrapDamageMultiplier; } + + /** + * Set trap damage multiplier + */ + void SetTrapDamageMultiplier(float Multiplier) { m_TrapDamageMultiplier = Multiplier; } + + /** + * Apply constructor perks + */ + void ApplyConstructorPerks(float BuildSpeed, float TrapDamage, int32 ExtraBuildLimit); + + // =========================================== + // Events + // =========================================== + + /** + * Register callback for building events + */ + void RegisterEventCallback(FBuildingEventCallback Callback); + + /** + * Handle ProcessEvent for building-related functions + */ + void OnProcessEvent(void* Object, void* Function, void* Params); + + private: + // Internal helpers + std::string GenerateBuildingId() const; + std::string GenerateTrapId() const; + + bool ValidatePlacement(const FBuildPreview& Preview) const; + bool CheckOverlap(float X, float Y, float Z) const; + bool CheckSupport(float X, float Y, float Z, EBuildingType Type) const; + + void NotifyChange(const FBuildingChangeEvent& Event); + + void UpdateTraps(float DeltaTime); + void ProcessTrapTrigger(FTrapInstance& Trap); + + // State + bool m_bIsInBuildMode = false; + bool m_bIsPlacingTrap = false; + + EBuildingType m_CurrentBuildType = EBuildingType::None; + EBuildingMaterial m_CurrentMaterial = EBuildingMaterial::Wood; + ETrapType m_CurrentTrapType = ETrapType::None; + std::string m_CurrentTrapItemId; + + FBuildPreview m_BuildPreview; + + // Building storage + std::unordered_map m_Buildings; + std::unordered_map m_Traps; + + // Grid lookup (for fast position-based queries) + std::unordered_map m_GridToBuildingId; + + // ID counters + mutable uint32 m_BuildingIdCounter = 0; + mutable uint32 m_TrapIdCounter = 0; + + // STW bonuses + int32 m_BuildLimit = 1000; + float m_BuildSpeedMultiplier = 1.0f; + float m_TrapDamageMultiplier = 1.0f; + + // References + FInventoryManager* m_pInventoryManager = nullptr; + + UObjectWrapper m_BuildingManagerActor; + UObjectWrapper m_TrapManagerActor; + + // Event callbacks + std::vector m_EventCallbacks; + }; + + /** + * Global building manager accessor + */ + FBuildingManager& GetLocalBuildingManager(); + +} \ No newline at end of file diff --git a/STW/Building/BuildingTypes.h b/STW/Building/BuildingTypes.h new file mode 100644 index 0000000..f68f249 --- /dev/null +++ b/STW/Building/BuildingTypes.h @@ -0,0 +1,382 @@ +/** + * UniversalSlashingSimulator - Building System Types + * + * Defines STW building structures, traps, and construction types. + */ + +#pragma once + +#include "../../Core/Common.h" +#include +#include + +namespace USS +{ + /** + * Building piece types + */ + enum class EBuildingType : uint8 + { + None = 0, + Wall, + Floor, + Ramp, + Roof, + Stair, + + // STW-specific + Trap, + StructuralBuild, // Mission objectives like radar towers + DefenseBuild, // Atlas, amplifiers, etc. + }; + + /** + * Building material tiers + */ + enum class EBuildingMaterial : uint8 + { + Wood = 0, + Stone, + Metal, + + // Special materials (STW) + Tier2, // Upgraded wood + Tier3, // Upgraded stone/brick + Tier4, // Upgraded metal + }; + + /** + * Building upgrade level (STW progression) + */ + enum class EBuildingTier : uint8 + { + Tier1 = 1, + Tier2 = 2, + Tier3 = 3, + }; + + /** + * Trap types in STW + */ + enum class ETrapType : uint8 + { + None = 0, + + // Floor traps + FloorSpikes, + FloorFreeze, + FloorLauncher, + FloorElectric, + FloorRetractSpikes, + FloorBroadside, + + // Wall traps + WallDarts, + WallDynamo, + WallLights, + WallLauncher, + WallSpikes, + + // Ceiling traps + CeilingZapper, + CeilingGas, + CeilingDropTrap, + CeilingElectric, + CeilingTire, + + // Defender posts + DefenderPost, + }; + + /** + * Trap targeting behavior + */ + enum class ETrapTargeting : uint8 + { + None = 0, + Proximity, // Triggers when enemy is near + Path, // Targets pathing enemies + Random, // Random targeting + Strongest, // Targets highest HP + Closest, // Targets closest enemy + }; + + /** + * Building state + */ + enum class EBuildingState : uint8 + { + None = 0, + Previewing, // Ghost preview before placement + Placing, // In placement mode + Building, // Construction in progress + Built, // Fully constructed + Damaged, // Has taken damage + Upgrading, // Being upgraded + Repairing, // Being repaired + Destroying, // Being demolished + }; + + /** + * Resource cost for building + */ + struct FBuildingCost + { + int32 WoodCost = 0; + int32 StoneCost = 0; + int32 MetalCost = 0; + + // STW ingredients + int32 NutsAndBolts = 0; + int32 PlanksCount = 0; + int32 RoughOre = 0; + + bool CanAfford(int32 Wood, int32 Stone, int32 Metal) const + { + return Wood >= WoodCost && Stone >= StoneCost && Metal >= MetalCost; + } + }; + + /** + * Building piece stats + */ + struct FBuildingStats + { + float MaxHealth = 100.0f; + float CurrentHealth = 100.0f; + float BuildTime = 3.0f; // Seconds to build + float RepairRate = 10.0f; // Health per second when repairing + float DamageResistance = 0.0f; // Percentage damage reduction + + // Material-specific modifiers + float FireResistance = 0.0f; + float PhysicalResistance = 0.0f; + float EnergyResistance = 0.0f; + }; + + /** + * Trap stats + */ + struct FTrapStats + { + float Damage = 0.0f; + float ReloadTime = 0.0f; // Seconds between activations + float Range = 0.0f; // Trigger range + float KnockbackForce = 0.0f; + float SlowPercent = 0.0f; // Movement slow on hit + float StunDuration = 0.0f; + + int32 MaxDurability = 0; + int32 CurrentDurability = 0; + int32 UsesPerActivation = 1; // How many uses per trigger + + ETrapTargeting Targeting = ETrapTargeting::Proximity; + + // Status effects + bool bAppliesAffliction = false; + bool bAppliesSnare = false; + bool bAppliesStun = false; + bool bAppliesFreeze = false; + }; + + /** + * Building piece data + */ + struct FBuildingPiece + { + std::string BuildingId; + EBuildingType Type = EBuildingType::None; + EBuildingMaterial Material = EBuildingMaterial::Wood; + EBuildingTier Tier = EBuildingTier::Tier1; + EBuildingState State = EBuildingState::None; + + FBuildingStats Stats; + FBuildingCost Cost; + + // Grid position + int32 GridX = 0; + int32 GridY = 0; + int32 GridZ = 0; + float Rotation = 0.0f; + + // Owner info + std::string OwnerId; + bool bIsPlayerBuilt = false; + + // Native actor reference + void* BuildingActor = nullptr; + + float GetHealthPercent() const + { + return (Stats.MaxHealth > 0.0f) ? (Stats.CurrentHealth / Stats.MaxHealth) : 0.0f; + } + + bool IsFullHealth() const + { + return Stats.CurrentHealth >= Stats.MaxHealth; + } + + bool IsDamaged() const + { + return Stats.CurrentHealth < Stats.MaxHealth; + } + }; + + /** + * Trap instance data + */ + struct FTrapInstance + { + std::string TrapId; + ETrapType Type = ETrapType::None; + EBuildingState State = EBuildingState::None; + + FTrapStats Stats; + FBuildingCost Cost; + + // Attached building + std::string AttachedBuildingId; + + // Placement + int32 GridX = 0; + int32 GridY = 0; + int32 GridZ = 0; + float Rotation = 0.0f; + + // State + float CooldownRemaining = 0.0f; + int32 TotalKills = 0; + bool bIsArmed = true; + bool bIsTriggered = false; + + // Owner + std::string OwnerId; + + // Native reference + void* TrapActor = nullptr; + + bool IsReady() const + { + return bIsArmed && CooldownRemaining <= 0.0f && Stats.CurrentDurability > 0; + } + + float GetDurabilityPercent() const + { + return (Stats.MaxDurability > 0) ? + (static_cast(Stats.CurrentDurability) / Stats.MaxDurability) : 0.0f; + } + }; + + /** + * Build preview ghost data + */ + struct FBuildPreview + { + EBuildingType Type = EBuildingType::None; + EBuildingMaterial Material = EBuildingMaterial::Wood; + + // Preview position + float LocationX = 0.0f; + float LocationY = 0.0f; + float LocationZ = 0.0f; + float Rotation = 0.0f; + + // Validity + bool bIsValidPlacement = false; + bool bCanAfford = false; + bool bIsOverlapping = false; + bool bIsFloating = false; + + FBuildingCost Cost; + }; + + /** + * Building change event data + */ + struct FBuildingChangeEvent + { + enum class EChangeType + { + Built, + Destroyed, + Damaged, + Repaired, + Upgraded, + TrapPlaced, + TrapTriggered, + TrapDestroyed, + }; + + EChangeType Type; + std::string BuildingId; + std::string PlayerId; + + float OldHealth = 0.0f; + float NewHealth = 0.0f; + float Damage = 0.0f; + + void* DamageCauser = nullptr; + }; + + /** + * Default building costs by material + */ + inline FBuildingCost GetDefaultBuildCost(EBuildingMaterial Material) + { + FBuildingCost Cost; + + switch (Material) + { + case EBuildingMaterial::Wood: + Cost.WoodCost = 10; + break; + case EBuildingMaterial::Stone: + Cost.StoneCost = 10; + break; + case EBuildingMaterial::Metal: + Cost.MetalCost = 10; + break; + default: + Cost.WoodCost = 10; + break; + } + + return Cost; + } + + /** + * Default building stats by material + */ + inline FBuildingStats GetDefaultBuildStats(EBuildingMaterial Material) + { + FBuildingStats Stats; + + switch (Material) + { + case EBuildingMaterial::Wood: + Stats.MaxHealth = 150.0f; + Stats.BuildTime = 3.0f; + Stats.FireResistance = -0.25f; // Weak to fire + break; + case EBuildingMaterial::Stone: + Stats.MaxHealth = 300.0f; + Stats.BuildTime = 4.0f; + Stats.PhysicalResistance = 0.1f; + break; + case EBuildingMaterial::Metal: + Stats.MaxHealth = 500.0f; + Stats.BuildTime = 5.0f; + Stats.PhysicalResistance = 0.15f; + Stats.EnergyResistance = -0.25f; // Weak to energy + break; + default: + Stats.MaxHealth = 150.0f; + Stats.BuildTime = 3.0f; + break; + } + + Stats.CurrentHealth = Stats.MaxHealth; + return Stats; + } + +} \ No newline at end of file diff --git a/STW/GameMode/STWGameMode.cpp b/STW/GameMode/STWGameMode.cpp new file mode 100644 index 0000000..d5ab224 --- /dev/null +++ b/STW/GameMode/STWGameMode.cpp @@ -0,0 +1,374 @@ +/** + * UniversalSlashingSimulator - STW Game Mode Implementation + * + * Based on farmstead_plate.cpp from PolarisV2-STW. + */ + +#include "STWGameMode.h" +#include "../../Core/Logging/Log.h" +#include "../../Core/Hooks/HookTypes.h" +#include "../../Engine/EngineCore.h" +#include "../Missions/MissionManager.h" +#include "../Inventory/InventoryManager.h" +#include "../Building/BuildingManager.h" +#include "../Player/STWPlayerController.h" +#include + +namespace USS +{ + FSTWGameMode::FSTWGameMode() + : m_State(ESTWGameState::None) + , m_bWorldReady(false) + , m_bPlayersLoaded(false) + { + } + + FSTWGameMode::~FSTWGameMode() + { + Shutdown(); + } + + FSTWGameMode& FSTWGameMode::Get() + { + static FSTWGameMode Instance; + return Instance; + } + + EResult FSTWGameMode::Initialize(const FSTWGameConfig& Config) + { + if (m_State != ESTWGameState::None) + return EResult::AlreadyInitialized; + + USS_LOG("Initializing STW GameMode..."); + USS_LOG(" Zone: %s", Config.ZoneName.c_str()); + USS_LOG(" Mission: %s", Config.MissionBlueprint.c_str()); + + m_Config = Config; + SetState(ESTWGameState::Initializing); + + // Create managers + m_pMissionManager = std::make_unique(); + m_pInventoryManager = std::make_unique(); + m_pBuildingManager = std::make_unique(); + + // Register for ProcessEvent callbacks (@timmie implements hooks) + // When hooks are implemented, register a callback that forwards to OnProcessEvent: + // Hook::GetProcessEventDispatcher().RegisterPre([this](void* Obj, void* Func, void* Params) -> bool { + // OnProcessEvent(Obj, Func, Params); + // return true; + // }); + + // Initialize managers + if (m_pMissionManager->Initialize() != EResult::Success) + { + USS_WARN("Mission manager initialization incomplete"); + } + + if (m_pInventoryManager->Initialize(nullptr) != EResult::Success) + { + USS_WARN("Inventory manager initialization incomplete"); + } + + if (m_pBuildingManager->Initialize(m_pInventoryManager.get()) != EResult::Success) + { + USS_WARN("Building manager initialization incomplete"); + } + + SetState(ESTWGameState::WaitingForWorld); + + USS_LOG("STW GameMode initialized, waiting for world..."); + return EResult::Success; + } + + void FSTWGameMode::Shutdown() + { + if (m_State == ESTWGameState::None || m_State == ESTWGameState::Shutdown) + return; + + USS_LOG("Shutting down STW GameMode..."); + + SetState(ESTWGameState::Shutdown); + + m_pLocalPlayer.reset(); + m_pBuildingManager.reset(); + m_pInventoryManager.reset(); + m_pMissionManager.reset(); + + m_World = UObjectWrapper(); + m_GameState = UObjectWrapper(); + m_GameMode = UObjectWrapper(); + + m_bWorldReady = false; + m_bPlayersLoaded = false; + + m_State = ESTWGameState::None; + + USS_LOG("STW GameMode shutdown complete"); + } + + void FSTWGameMode::Update() + { + // Called each tick - update managers + if (m_pMissionManager) + m_pMissionManager->Update(); + + if (m_pBuildingManager) + m_pBuildingManager->Update(); + } + + void FSTWGameMode::SetState(ESTWGameState NewState) + { + if (m_State == NewState) + return; + + ESTWGameState OldState = m_State; + m_State = NewState; + + USS_LOG("GameMode state: %s -> %s", + GetGameStateName(OldState), + GetGameStateName(NewState)); + + // Notify callbacks + for (const auto& Callback : m_StateChangeCallbacks) + { + Callback(OldState, NewState); + } + } + + void FSTWGameMode::SetLocalPlayer(std::unique_ptr Player) + { + m_pLocalPlayer = std::move(Player); + } + + void FSTWGameMode::RegisterStateChangeCallback(StateChangeCallback Callback) + { + m_StateChangeCallbacks.push_back(std::move(Callback)); + } + + // @timmie: replace with normal VFT / hooking system when available + void FSTWGameMode::OnProcessEvent(void* Object, void* Function, void* Params) + { + if (!Function) + return; + + // Get function name + UFunctionWrapper FuncWrapper(Function); + std::string FuncName = FuncWrapper.GetName(); + + // Handle specific events based on current state + if (FuncName.find("ReadyToStartMatch") != std::string::npos) + { + OnReadyToStartMatch(); + } + else if (FuncName.find("ServerHandleMissionEvent_ToggledEditMode") != std::string::npos) + { + OnToggleEditMode(Params); + } + else if (FuncName.find("ServerHandleMissionEvent_StartLeavingZone") != std::string::npos) + { + OnStartLeavingZone(Params); + } + else if (FuncName.find("ServerCraftSchematic") != std::string::npos) + { + OnCraftSchematic(Params); + } + else if (FuncName.find("Tick") != std::string::npos) + { + Update(); + } + + // Forward to sub-managers + if (m_pMissionManager) + m_pMissionManager->OnProcessEvent(Object, Function, Params); + + if (m_pBuildingManager && m_pLocalPlayer) + m_pBuildingManager->OnProcessEvent(Object, Function, Params); + } + + void FSTWGameMode::OnReadyToStartMatch() + { + if (m_State != ESTWGameState::WaitingForWorld) + return; + + USS_LOG("ReadyToStartMatch received"); + + InitializeWorld(); + OnWorldReady(); + + // Transition to waiting for players + SetState(ESTWGameState::WaitingForPlayers); + + // For single-player, immediately proceed + OnAllPlayersLoaded(); + } + + void FSTWGameMode::OnWorldReady() + { + USS_LOG("World is ready"); + m_bWorldReady = true; + + // Cache world references + m_World = GetEngineCore().FindObjectByName("PersistentLevel"); + + // Load husk assets into memory + LoadHuskAssets(); + } + + void FSTWGameMode::OnAllPlayersLoaded() + { + if (m_bPlayersLoaded) + return; + + USS_LOG("All players loaded"); + m_bPlayersLoaded = true; + + SetState(ESTWGameState::LoadingMission); + + // Spawn local player + SpawnLocalPlayer(); + + // Setup inventory + SetupInventory(); + + // Initialize mission + InitializeMission(); + + SetState(ESTWGameState::MissionActive); + } + + void FSTWGameMode::OnMissionEvent(const char* EventName, void* Params) + { + USS_LOG("Mission event: %s", EventName); + + if (m_pMissionManager) + { + m_pMissionManager->OnMissionEvent(EventName, Params); + } + } + + void FSTWGameMode::OnToggleEditMode(void* Params) + { + USS_LOG("Edit mode toggled"); + + // Toggle between build mode and normal mode + if (m_pBuildingManager) + { + if (m_pBuildingManager->IsInBuildMode()) + { + m_pBuildingManager->ExitBuildMode(); + } + else + { + m_pBuildingManager->EnterBuildMode(EBuildingType::Wall); + } + } + } + + void FSTWGameMode::OnStartLeavingZone(void* Params) + { + USS_LOG("Starting to leave zone"); + + SetState(ESTWGameState::LeavingZone); + + // TODO: Handle zone exit + } + + void FSTWGameMode::OnCraftSchematic(void* Params) + { + USS_LOG("Craft schematic requested"); + + // TODO: Parse schematic ID from Params and call CraftItem + // if (m_pInventoryManager) + // { + // m_pInventoryManager->CraftItem(schematicId, 1); + // } + } + + void FSTWGameMode::InitializeWorld() + { + USS_LOG("Initializing world..."); + + // TODO: Additional world initialization + // - Patch gameplay abilities + // - Setup replication + } + + void FSTWGameMode::InitializeMission() + { + USS_LOG("Initializing mission..."); + + if (!m_pMissionManager) + return; + + // Create mission based on config + FMissionConfig MissionConfig; + MissionConfig.Type = m_Config.MissionType; + MissionConfig.DifficultyLevel = m_Config.DifficultyLevel; + MissionConfig.BlueprintPath = m_Config.MissionBlueprint; + + m_pMissionManager->StartMission(MissionConfig); + } + + void FSTWGameMode::SpawnLocalPlayer() + { + USS_LOG("Spawning local player..."); + + // Find local player controller from engine + void* LocalController = GetEngineCore().FindLocalPlayerController(); + + if (!LocalController) + { + USS_WARN("Local player controller not found yet"); + return; + } + + // Create player controller wrapper + m_pLocalPlayer = std::make_unique(LocalController); + + if (!m_pLocalPlayer->IsValid()) + { + USS_ERROR("Failed to wrap local player controller"); + m_pLocalPlayer.reset(); + return; + } + + USS_LOG("Local player spawned successfully"); + } + + void FSTWGameMode::SetupInventory() + { + USS_LOG("Setting up inventory..."); + + if (!m_pInventoryManager || !m_pLocalPlayer) + return; + + // Re-initialize inventory with the player controller + m_pInventoryManager->Initialize(m_pLocalPlayer->GetNative()); + } + + void FSTWGameMode::LoadHuskAssets() + { + USS_LOG("Loading husk assets into memory..."); + + // Based on athena_plate.cpp husk loading + const char* HuskAssets[] = { + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn.HuskPawn_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Fire.HuskPawn_Fire_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Ice.HuskPawn_Ice_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Lightning.HuskPawn_Lightning_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Beehive.HuskPawn_Beehive_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Bombshell.HuskPawn_Bombshell_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Bombshell_Poison.HuskPawn_Bombshell_Poison_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Dwarf.HuskPawn_Dwarf_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Dwarf_Fire.HuskPawn_Dwarf_Fire_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Dwarf_Ice.HuskPawn_Dwarf_Ice_C", + "/Game/Characters/Enemies/Husk/Blueprints/HuskPawn_Dwarf_Lightning.HuskPawn_Dwarf_Lightning_C", + }; + + // TODO: Implement FindOrLoadObject via engine core + // For each asset, call engine's StaticLoadObject + + USS_LOG("Loaded %zu husk asset types", sizeof(HuskAssets) / sizeof(HuskAssets[0])); + } + +} \ No newline at end of file diff --git a/STW/GameMode/STWGameMode.h b/STW/GameMode/STWGameMode.h new file mode 100644 index 0000000..fed43a6 --- /dev/null +++ b/STW/GameMode/STWGameMode.h @@ -0,0 +1,192 @@ +/** + * UniversalSlashingSimulator - STW Game Mode + * + * Core STW game mode controller. Manages the game lifecycle, + * coordinates systems, and handles ProcessEvent routing. + * + * Based on FarmsteadPlate from PolarisV2-STW. + * + * Lifecycle: + * 1. Initialize() - Called after engine core ready + * 2. OnWorldReady() - Called when world is loaded + * 3. OnPlayersLoaded() - Called when all players joined + * 4. OnMissionStart() - Mission gameplay begins + * 5. OnMissionEnd() - Mission complete/failed + * 6. Shutdown() - Cleanup + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../../Engine/UObject/UObjectWrapper.h" +#include "../Missions/MissionTypes.h" +#include + +namespace USS +{ + // Forward declarations + class FMissionManager; + class FInventoryManager; + class FBuildingManager; + class FSTWPlayerController; + + // Game mode state + enum class ESTWGameState : uint8 + { + None, + Initializing, + WaitingForWorld, + WaitingForPlayers, + LoadingMission, + MissionActive, + MissionComplete, + MissionFailed, + LeavingZone, + Shutdown + }; + + // Game mode configuration + struct FSTWGameConfig + { + // Zone configuration + std::string ZoneName; // e.g., "Zone_Onboarding_FarmsteadFort" + std::string MissionBlueprint; // e.g., "Mission_FarmsteadFort_C" + std::string MapName; // Map to load + + // Player settings + int32 MaxPlayers; + bool bAllowBots; + + // Mission settings + EMissionType MissionType; + int32 DifficultyLevel; + int32 DefaultDifficulty; // Default difficulty for the zone + + // Feature toggles + bool bEnableMissions; + bool bEnableInventory; + bool bEnableBuilding; + + FSTWGameConfig() + : MaxPlayers(4) + , bAllowBots(true) + , MissionType(EMissionType::FarmsteadDefense) + , DifficultyLevel(1) + , DefaultDifficulty(1) + , bEnableMissions(true) + , bEnableInventory(true) + , bEnableBuilding(true) + { + } + }; + + // STW Game Mode + class FSTWGameMode + { + public: + USS_NON_COPYABLE(FSTWGameMode) + USS_NON_MOVABLE(FSTWGameMode) + + static FSTWGameMode& Get(); + + // Lifecycle + EResult Initialize(const FSTWGameConfig& Config); + void Shutdown(); + void Update(); // Called each tick + + // State + ESTWGameState GetState() const { return m_State; } + bool IsInitialized() const { return m_State != ESTWGameState::None; } + bool IsMissionActive() const { return m_State == ESTWGameState::MissionActive; } + + // Configuration + const FSTWGameConfig& GetConfig() const { return m_Config; } + + // Manager access + FMissionManager* GetMissionManager() const { return m_pMissionManager.get(); } + FInventoryManager* GetInventoryManager() const { return m_pInventoryManager.get(); } + FBuildingManager* GetBuildingManager() const { return m_pBuildingManager.get(); } + + // Player management + FSTWPlayerController* GetLocalPlayer() const { return m_pLocalPlayer.get(); } + void SetLocalPlayer(std::unique_ptr Player); + + // Event handlers (called from ProcessEvent hook) + void OnProcessEvent(void* Object, void* Function, void* Params); + + // Callbacks for external systems + using StateChangeCallback = std::function; + void RegisterStateChangeCallback(StateChangeCallback Callback); + + private: + FSTWGameMode(); + ~FSTWGameMode(); + + // State transitions + void SetState(ESTWGameState NewState); + + // Event handlers + void OnReadyToStartMatch(); + void OnWorldReady(); + void OnAllPlayersLoaded(); + void OnMissionEvent(const char* EventName, void* Params); + void OnToggleEditMode(void* Params); + void OnStartLeavingZone(void* Params); + void OnCraftSchematic(void* Params); + + // Initialization helpers + void InitializeWorld(); + void InitializeMission(); + void SpawnLocalPlayer(); + void SetupInventory(); + void LoadHuskAssets(); + + // State + ESTWGameState m_State; + FSTWGameConfig m_Config; + bool m_bWorldReady; + bool m_bPlayersLoaded; + + // Managers + std::unique_ptr m_pMissionManager; + std::unique_ptr m_pInventoryManager; + std::unique_ptr m_pBuildingManager; + + // Player + std::unique_ptr m_pLocalPlayer; + + // Callbacks + std::vector m_StateChangeCallbacks; + + // Engine references (cached) + UObjectWrapper m_World; + UObjectWrapper m_GameState; + UObjectWrapper m_GameMode; + }; + + // Convenience function + inline FSTWGameMode& GetSTWGameMode() + { + return FSTWGameMode::Get(); + } + + // State name helper + inline const char* GetGameStateName(ESTWGameState State) + { + switch (State) + { + case ESTWGameState::None: return "None"; + case ESTWGameState::Initializing: return "Initializing"; + case ESTWGameState::WaitingForWorld: return "WaitingForWorld"; + case ESTWGameState::WaitingForPlayers: return "WaitingForPlayers"; + case ESTWGameState::LoadingMission: return "LoadingMission"; + case ESTWGameState::MissionActive: return "MissionActive"; + case ESTWGameState::MissionComplete: return "MissionComplete"; + case ESTWGameState::MissionFailed: return "MissionFailed"; + case ESTWGameState::LeavingZone: return "LeavingZone"; + case ESTWGameState::Shutdown: return "Shutdown"; + default: return "Unknown"; + } + } + +} \ No newline at end of file diff --git a/STW/Inventory/InventoryManager.cpp b/STW/Inventory/InventoryManager.cpp new file mode 100644 index 0000000..a435a9e --- /dev/null +++ b/STW/Inventory/InventoryManager.cpp @@ -0,0 +1,639 @@ +/** + * UniversalSlashingSimulator - Inventory Manager Implementation + */ + +#include "InventoryManager.h" +#include "../../Core/Logging/Log.h" +#include "../../Engine/EngineCore.h" + +namespace USS +{ + static FInventoryManager g_LocalInventoryManager; + + FInventoryManager& GetLocalInventoryManager() + { + return g_LocalInventoryManager; + } + + FInventoryManager::FInventoryManager() + : m_MaxSlots(200) + , m_MaxStackSize(999) + , m_ItemIdCounter(0) + { + } + + FInventoryManager::~FInventoryManager() + { + Shutdown(); + } + + EResult FInventoryManager::Initialize(void* PlayerController) + { + USS_LOG("Initializing Inventory Manager..."); + + m_PlayerController = UObjectWrapper(PlayerController); + + // Initialize quickbars + for (int32 i = 0; i < 2; ++i) + { + m_Quickbars[i].QuickbarIndex = i; + m_Quickbars[i].CurrentSlot = 0; + m_Quickbars[i].Slots.resize(i == 0 ? 6 : 4); // 6 weapon slots, 4 build slots + + for (size_t j = 0; j < m_Quickbars[i].Slots.size(); ++j) + { + m_Quickbars[i].Slots[j].SlotIndex = static_cast(j); + m_Quickbars[i].Slots[j].bIsEmpty = true; + } + } + + // Find inventory components + // TODO: Traverse to find UFortInventory on controller + + USS_LOG("Inventory Manager initialized"); + return EResult::Success; + } + + void FInventoryManager::Shutdown() + { + m_Items.clear(); + m_SlotToItem.clear(); + m_EventCallbacks.clear(); + + m_PlayerController = UObjectWrapper(); + m_InventoryComponent = UObjectWrapper(); + m_QuickbarComponent = UObjectWrapper(); + } + + void FInventoryManager::Update() + { + // Sync inventory state from engine if needed + } + + const FInventoryItem* FInventoryManager::GetItem(const std::string& ItemId) const + { + auto It = m_Items.find(ItemId); + return (It != m_Items.end()) ? &It->second : nullptr; + } + + const FInventoryItem* FInventoryManager::GetItemBySlot(int32 SlotIndex) const + { + auto It = m_SlotToItem.find(SlotIndex); + if (It != m_SlotToItem.end()) + { + return GetItem(It->second); + } + return nullptr; + } + + std::vector FInventoryManager::GetItemsByCategory(EItemCategory Category) const + { + std::vector Result; + + for (const auto& Pair : m_Items) + { + if (Pair.second.Category == Category) + { + Result.push_back(&Pair.second); + } + } + + return Result; + } + + int32 FInventoryManager::GetItemCount(const std::string& TemplateId) const + { + int32 Total = 0; + + for (const auto& Pair : m_Items) + { + if (Pair.second.TemplateId == TemplateId) + { + Total += Pair.second.Count; + } + } + + return Total; + } + + bool FInventoryManager::HasItem(const std::string& TemplateId, int32 MinCount) const + { + return GetItemCount(TemplateId) >= MinCount; + } + + EResult FInventoryManager::AddItem(const FInventoryItem& Item) + { + // Check if we can stack with existing + for (auto& Pair : m_Items) + { + if (Pair.second.TemplateId == Item.TemplateId && + Pair.second.Count < Pair.second.MaxStackSize) + { + int32 CanAdd = Pair.second.MaxStackSize - Pair.second.Count; + int32 ToAdd = (Item.Count < CanAdd) ? Item.Count : CanAdd; + + Pair.second.Count += ToAdd; + + FInventoryChangeEvent Event; + Event.Type = FInventoryChangeEvent::EChangeType::Modified; + Event.ItemId = Pair.first; + Event.OldCount = Pair.second.Count - ToAdd; + Event.NewCount = Pair.second.Count; + NotifyChange(Event); + + USS_LOG("Stacked %d %s (total: %d)", ToAdd, Item.ItemName.c_str(), Pair.second.Count); + + // All added by stacking + if (ToAdd >= Item.Count) + return EResult::Success; + + // Need to create new stack for remainder + FInventoryItem Remainder = Item; + Remainder.Count = Item.Count - ToAdd; + return AddItem(Remainder); + } + } + + // Need new slot + int32 FreeSlot = FindFreeSlot(); + if (FreeSlot < 0) + { + USS_WARN("Inventory full, cannot add item: %s", Item.ItemName.c_str()); + return EResult::InventoryFull; + } + + // Create new item + FInventoryItem NewItem = Item; + NewItem.ItemId = GenerateItemId(); + NewItem.SlotIndex = FreeSlot; + + m_Items[NewItem.ItemId] = NewItem; + m_SlotToItem[FreeSlot] = NewItem.ItemId; + + FInventoryChangeEvent Event; + Event.Type = FInventoryChangeEvent::EChangeType::Added; + Event.ItemId = NewItem.ItemId; + Event.OldCount = 0; + Event.NewCount = NewItem.Count; + Event.SlotIndex = FreeSlot; + NotifyChange(Event); + + USS_LOG("Added item: %s x%d (slot %d)", NewItem.ItemName.c_str(), NewItem.Count, FreeSlot); + + return EResult::Success; + } + + EResult FInventoryManager::RemoveItem(const std::string& ItemId, int32 Count) + { + auto It = m_Items.find(ItemId); + if (It == m_Items.end()) + { + return EResult::ItemNotFound; + } + + FInventoryItem& Item = It->second; + int32 OldCount = Item.Count; + + if (Count >= Item.Count) + { + // Remove entirely + int32 Slot = Item.SlotIndex; + m_SlotToItem.erase(Slot); + m_Items.erase(It); + + FInventoryChangeEvent Event; + Event.Type = FInventoryChangeEvent::EChangeType::Removed; + Event.ItemId = ItemId; + Event.OldCount = OldCount; + Event.NewCount = 0; + Event.SlotIndex = Slot; + NotifyChange(Event); + + USS_LOG("Removed item: %s", ItemId.c_str()); + } + else + { + // Reduce count + Item.Count -= Count; + + FInventoryChangeEvent Event; + Event.Type = FInventoryChangeEvent::EChangeType::Modified; + Event.ItemId = ItemId; + Event.OldCount = OldCount; + Event.NewCount = Item.Count; + NotifyChange(Event); + + USS_LOG("Reduced item %s: %d -> %d", ItemId.c_str(), OldCount, Item.Count); + } + + return EResult::Success; + } + + EResult FInventoryManager::MoveItem(const std::string& ItemId, int32 NewSlot) + { + auto It = m_Items.find(ItemId); + if (It == m_Items.end()) + return EResult::ItemNotFound; + + if (NewSlot < 0 || NewSlot >= m_MaxSlots) + return EResult::InvalidParameter; + + FInventoryItem& Item = It->second; + int32 OldSlot = Item.SlotIndex; + + // Check if target slot is occupied + auto TargetIt = m_SlotToItem.find(NewSlot); + if (TargetIt != m_SlotToItem.end()) + { + // Swap items + const std::string& TargetItemId = TargetIt->second; + auto TargetItemIt = m_Items.find(TargetItemId); + + if (TargetItemIt != m_Items.end()) + { + TargetItemIt->second.SlotIndex = OldSlot; + m_SlotToItem[OldSlot] = TargetItemId; + } + } + else + { + m_SlotToItem.erase(OldSlot); + } + + Item.SlotIndex = NewSlot; + m_SlotToItem[NewSlot] = ItemId; + + USS_LOG("Moved item %s: slot %d -> %d", ItemId.c_str(), OldSlot, NewSlot); + + return EResult::Success; + } + + EResult FInventoryManager::StackItems(const std::string& SourceId, const std::string& TargetId) + { + auto SourceIt = m_Items.find(SourceId); + auto TargetIt = m_Items.find(TargetId); + + if (SourceIt == m_Items.end() || TargetIt == m_Items.end()) + return EResult::ItemNotFound; + + FInventoryItem& Source = SourceIt->second; + FInventoryItem& Target = TargetIt->second; + + if (Source.TemplateId != Target.TemplateId) + return EResult::InvalidParameter; + + int32 CanAdd = Target.MaxStackSize - Target.Count; + int32 ToAdd = (Source.Count < CanAdd) ? Source.Count : CanAdd; + + Target.Count += ToAdd; + Source.Count -= ToAdd; + + if (Source.Count <= 0) + { + m_SlotToItem.erase(Source.SlotIndex); + m_Items.erase(SourceIt); + } + + return EResult::Success; + } + + int32 FInventoryManager::GetWoodCount() const + { + const FInventoryItem* Item = FindResourceItem(EResourceType::Wood); + return Item ? Item->Count : 0; + } + + int32 FInventoryManager::GetStoneCount() const + { + const FInventoryItem* Item = FindResourceItem(EResourceType::Stone); + return Item ? Item->Count : 0; + } + + int32 FInventoryManager::GetMetalCount() const + { + const FInventoryItem* Item = FindResourceItem(EResourceType::Metal); + return Item ? Item->Count : 0; + } + + EResult FInventoryManager::ConsumeResources(EResourceType Type, int32 Amount) + { + // Find resource item + for (auto& Pair : m_Items) + { + if (Pair.second.Category == EItemCategory::Resource) + { + // TODO: Check actual resource type matches + if (Pair.second.Count >= Amount) + { + return RemoveItem(Pair.first, Amount); + } + } + } + + return EResult::InsufficientResources; + } + + const FQuickbar* FInventoryManager::GetQuickbar(int32 Index) const + { + if (Index >= 0 && Index < 2) + { + return &m_Quickbars[Index]; + } + return nullptr; + } + + int32 FInventoryManager::GetCurrentQuickbarSlot(int32 QuickbarIndex) const + { + if (QuickbarIndex >= 0 && QuickbarIndex < 2) + { + return m_Quickbars[QuickbarIndex].CurrentSlot; + } + return -1; + } + + EResult FInventoryManager::SetQuickbarSlot(int32 QuickbarIndex, int32 SlotIndex, const std::string& ItemId) + { + if (QuickbarIndex < 0 || QuickbarIndex >= 2) + return EResult::InvalidParameter; + + FQuickbar& Quickbar = m_Quickbars[QuickbarIndex]; + + if (SlotIndex < 0 || SlotIndex >= static_cast(Quickbar.Slots.size())) + return EResult::InvalidParameter; + + Quickbar.Slots[SlotIndex].ItemId = ItemId; + Quickbar.Slots[SlotIndex].bIsEmpty = ItemId.empty(); + + return EResult::Success; + } + + EResult FInventoryManager::ClearQuickbarSlot(int32 QuickbarIndex, int32 SlotIndex) + { + return SetQuickbarSlot(QuickbarIndex, SlotIndex, ""); + } + + EResult FInventoryManager::SelectQuickbarSlot(int32 QuickbarIndex, int32 SlotIndex) + { + if (QuickbarIndex < 0 || QuickbarIndex >= 2) + return EResult::InvalidParameter; + + FQuickbar& Quickbar = m_Quickbars[QuickbarIndex]; + + if (SlotIndex < 0 || SlotIndex >= static_cast(Quickbar.Slots.size())) + return EResult::InvalidParameter; + + Quickbar.CurrentSlot = SlotIndex; + + USS_LOG("Selected quickbar %d slot %d", QuickbarIndex, SlotIndex); + + return EResult::Success; + } + + const FInventoryItem* FInventoryManager::GetEquippedWeapon() const + { + return GetItem(m_EquippedWeaponId); + } + + const FInventoryItem* FInventoryManager::GetEquippedPickaxe() const + { + return GetItem(m_EquippedPickaxeId); + } + + EResult FInventoryManager::EquipItem(const std::string& ItemId) + { + auto It = m_Items.find(ItemId); + if (It == m_Items.end()) + return EResult::ItemNotFound; + + FInventoryItem& Item = It->second; + + if (Item.Category == EItemCategory::Weapon || Item.Category == EItemCategory::Melee) + { + m_EquippedWeaponId = ItemId; + Item.bIsEquipped = true; + + FInventoryChangeEvent Event; + Event.Type = FInventoryChangeEvent::EChangeType::Equipped; + Event.ItemId = ItemId; + NotifyChange(Event); + + USS_LOG("Equipped weapon: %s", Item.ItemName.c_str()); + } + + return EResult::Success; + } + + EResult FInventoryManager::UnequipItem(const std::string& ItemId) + { + auto It = m_Items.find(ItemId); + if (It == m_Items.end()) + return EResult::ItemNotFound; + + FInventoryItem& Item = It->second; + Item.bIsEquipped = false; + + if (m_EquippedWeaponId == ItemId) + { + m_EquippedWeaponId.clear(); + } + + FInventoryChangeEvent Event; + Event.Type = FInventoryChangeEvent::EChangeType::Unequipped; + Event.ItemId = ItemId; + NotifyChange(Event); + + return EResult::Success; + } + + EResult FInventoryManager::SwapWeaponSlots(int32 SlotA, int32 SlotB) + { + // Swap in primary quickbar + if (SlotA < 0 || SlotB < 0) + return EResult::InvalidParameter; + + FQuickbar& Primary = m_Quickbars[0]; + + if (SlotA >= static_cast(Primary.Slots.size()) || + SlotB >= static_cast(Primary.Slots.size())) + return EResult::InvalidParameter; + + std::swap(Primary.Slots[SlotA], Primary.Slots[SlotB]); + Primary.Slots[SlotA].SlotIndex = SlotA; + Primary.Slots[SlotB].SlotIndex = SlotB; + + return EResult::Success; + } + + float FInventoryManager::GetItemDurability(const std::string& ItemId) const + { + const FInventoryItem* Item = GetItem(ItemId); + return Item ? Item->Durability : 0.0f; + } + + EResult FInventoryManager::UseItemDurability(const std::string& ItemId, float Amount) + { + auto It = m_Items.find(ItemId); + if (It == m_Items.end()) + return EResult::ItemNotFound; + + FInventoryItem& Item = It->second; + Item.Durability -= Amount; + + if (Item.Durability < 0.0f) + Item.Durability = 0.0f; + + if (Item.Durability <= 0.0f) + { + USS_LOG("Item broken: %s", Item.ItemName.c_str()); + } + + return EResult::Success; + } + + EResult FInventoryManager::RepairItem(const std::string& ItemId) + { + auto It = m_Items.find(ItemId); + if (It == m_Items.end()) + return EResult::ItemNotFound; + + FInventoryItem& Item = It->second; + Item.Durability = Item.MaxDurability; + + USS_LOG("Repaired item: %s", Item.ItemName.c_str()); + + return EResult::Success; + } + + bool FInventoryManager::IsItemBroken(const std::string& ItemId) const + { + const FInventoryItem* Item = GetItem(ItemId); + return Item ? (Item->Durability <= 0.0f) : true; + } + + bool FInventoryManager::CanCraftItem(const std::string& SchematicId) const + { + // TODO: Look up recipe and check if we have materials + return false; + } + + EResult FInventoryManager::CraftItem(const std::string& SchematicId, int32 Count) + { + if (!CanCraftItem(SchematicId)) + { + return EResult::InsufficientResources; + } + + // TODO: Consume materials and create item + USS_LOG("Crafted item from schematic: %s x%d", SchematicId.c_str(), Count); + + return EResult::Success; + } + + std::vector FInventoryManager::GetAvailableRecipes() const + { + // TODO: Return recipes player can craft + return {}; + } + + int32 FInventoryManager::GetAmmoCount(const std::string& AmmoType) const + { + return GetItemCount(AmmoType); + } + + EResult FInventoryManager::ConsumeAmmo(const std::string& AmmoType, int32 Amount) + { + for (auto& Pair : m_Items) + { + if (Pair.second.Category == EItemCategory::Ammo && + Pair.second.TemplateId == AmmoType) + { + return RemoveItem(Pair.first, Amount); + } + } + + return EResult::ItemNotFound; + } + + EResult FInventoryManager::ReloadWeapon(const std::string& WeaponId) + { + auto It = m_Items.find(WeaponId); + if (It == m_Items.end()) + return EResult::ItemNotFound; + + FInventoryItem& Weapon = It->second; + + // TODO: Determine ammo type for weapon and consume from inventory + Weapon.AmmoCount = Weapon.MaxAmmo; + + USS_LOG("Reloaded weapon: %s", Weapon.ItemName.c_str()); + + return EResult::Success; + } + + void FInventoryManager::RegisterEventCallback(FInventoryEventCallback Callback) + { + if (Callback) + { + m_EventCallbacks.push_back(std::move(Callback)); + } + } + + void FInventoryManager::OnProcessEvent(void* Object, void* Function, void* Params) + { + // Handle inventory-related ProcessEvents + } + + void FInventoryManager::SyncFromEngine() + { + // TODO: Read inventory state from engine UFortInventory + } + + void FInventoryManager::SyncToEngine() + { + // TODO: Write inventory changes to engine UFortInventory + } + + void FInventoryManager::NotifyChange(const FInventoryChangeEvent& Event) + { + for (const auto& Callback : m_EventCallbacks) + { + if (Callback) + { + Callback(Event); + } + } + } + + int32 FInventoryManager::FindFreeSlot() const + { + for (int32 i = 0; i < m_MaxSlots; ++i) + { + if (m_SlotToItem.find(i) == m_SlotToItem.end()) + { + return i; + } + } + return -1; + } + + std::string FInventoryManager::GenerateItemId() const + { + char Buffer[64]; + snprintf(Buffer, sizeof(Buffer), "item_%u", ++m_ItemIdCounter); + return Buffer; + } + + const FInventoryItem* FInventoryManager::FindResourceItem(EResourceType Type) const + { + // TODO: would check actual resource type + for (const auto& Pair : m_Items) + { + if (Pair.second.Category == EItemCategory::Resource) + { + return &Pair.second; + } + } + return nullptr; + } + +} \ No newline at end of file diff --git a/STW/Inventory/InventoryManager.h b/STW/Inventory/InventoryManager.h new file mode 100644 index 0000000..9ed3818 --- /dev/null +++ b/STW/Inventory/InventoryManager.h @@ -0,0 +1,137 @@ +/** + * UniversalSlashingSimulator - Inventory Manager + * + * Manages player inventory, quickbars, and item operations. + * Handles STW-specific inventory logic like durability, crafting, etc. + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../../Engine/UObject/UObjectWrapper.h" +#include "InventoryTypes.h" +#include +#include +#include + +namespace USS +{ + // Inventory event callback + using FInventoryEventCallback = std::function; + + class FInventoryManager + { + public: + FInventoryManager(); + ~FInventoryManager(); + + USS_NON_COPYABLE(FInventoryManager) + USS_NON_MOVABLE(FInventoryManager) + + // Lifecycle + EResult Initialize(void* PlayerController); + void Shutdown(); + void Update(); + + // Item queries + const FInventoryItem* GetItem(const std::string& ItemId) const; + const FInventoryItem* GetItemBySlot(int32 SlotIndex) const; + std::vector GetItemsByCategory(EItemCategory Category) const; + int32 GetItemCount(const std::string& TemplateId) const; + bool HasItem(const std::string& TemplateId, int32 MinCount = 1) const; + + // Item operations + EResult AddItem(const FInventoryItem& Item); + EResult RemoveItem(const std::string& ItemId, int32 Count = 1); + EResult MoveItem(const std::string& ItemId, int32 NewSlot); + EResult StackItems(const std::string& SourceId, const std::string& TargetId); + + // Resource shortcuts + int32 GetWoodCount() const; + int32 GetStoneCount() const; + int32 GetMetalCount() const; + EResult ConsumeResources(EResourceType Type, int32 Amount); + + // Quickbar + const FQuickbar* GetQuickbar(int32 Index) const; + int32 GetCurrentQuickbarSlot(int32 QuickbarIndex) const; + EResult SetQuickbarSlot(int32 QuickbarIndex, int32 SlotIndex, const std::string& ItemId); + EResult ClearQuickbarSlot(int32 QuickbarIndex, int32 SlotIndex); + EResult SelectQuickbarSlot(int32 QuickbarIndex, int32 SlotIndex); + + // Equipment + const FInventoryItem* GetEquippedWeapon() const; + const FInventoryItem* GetEquippedPickaxe() const; + EResult EquipItem(const std::string& ItemId); + EResult UnequipItem(const std::string& ItemId); + EResult SwapWeaponSlots(int32 SlotA, int32 SlotB); + + // Durability (STW-specific) + float GetItemDurability(const std::string& ItemId) const; + EResult UseItemDurability(const std::string& ItemId, float Amount); + EResult RepairItem(const std::string& ItemId); + bool IsItemBroken(const std::string& ItemId) const; + + // Crafting (STW-specific) + bool CanCraftItem(const std::string& SchematicId) const; + EResult CraftItem(const std::string& SchematicId, int32 Count = 1); + std::vector GetAvailableRecipes() const; + + // Ammo + int32 GetAmmoCount(const std::string& AmmoType) const; + EResult ConsumeAmmo(const std::string& AmmoType, int32 Amount); + EResult ReloadWeapon(const std::string& WeaponId); + + // Inventory limits + int32 GetMaxInventorySlots() const { return m_MaxSlots; } + int32 GetUsedSlots() const { return static_cast(m_Items.size()); } + int32 GetFreeSlots() const { return m_MaxSlots - GetUsedSlots(); } + bool HasFreeSlot() const { return GetFreeSlots() > 0; } + + // Events + void RegisterEventCallback(FInventoryEventCallback Callback); + void OnProcessEvent(void* Object, void* Function, void* Params); + + // Sync with engine + void SyncFromEngine(); + void SyncToEngine(); + + private: + void NotifyChange(const FInventoryChangeEvent& Event); + int32 FindFreeSlot() const; + std::string GenerateItemId() const; + const FInventoryItem* FindResourceItem(EResourceType Type) const; + + // Items by ID + std::unordered_map m_Items; + + // Slot mapping + std::unordered_map m_SlotToItem; + + // Quickbars (0 = primary/weapons, 1 = secondary/build) + FQuickbar m_Quickbars[2]; + + // Currently equipped + std::string m_EquippedWeaponId; + std::string m_EquippedPickaxeId; + + // Limits + int32 m_MaxSlots; + int32 m_MaxStackSize; + + // Engine references + UObjectWrapper m_PlayerController; + UObjectWrapper m_InventoryComponent; // UFortInventory* + UObjectWrapper m_QuickbarComponent; // UFortQuickBars* + + // Callbacks + std::vector m_EventCallbacks; + + // ID counter for generated IDs + mutable uint32 m_ItemIdCounter; + }; + + // Global inventory access (for current local player) + FInventoryManager& GetLocalInventoryManager(); + +} \ No newline at end of file diff --git a/STW/Inventory/InventoryTypes.h b/STW/Inventory/InventoryTypes.h new file mode 100644 index 0000000..6199356 --- /dev/null +++ b/STW/Inventory/InventoryTypes.h @@ -0,0 +1,273 @@ +/** + * UniversalSlashingSimulator - Inventory Types + * + * Type definitions for STW inventory system. + * Handles items, weapons, resources, schematics, etc. + */ + +#pragma once + +#include "../../Core/Common.h" +#include +#include + +// Forward declare types from BuildingTypes.h to avoid circular dependency +// Full definitions are in BuildingTypes.h - include that for ETrapType and FTrapStats + +namespace USS +{ + // Item rarity (STW uses different names than BR) + enum class EItemRarity : uint8 + { + Common, // Gray + Uncommon, // Green + Rare, // Blue + Epic, // Purple + Legendary, // Orange + Mythic // Gold (only for certain heroes/schematics) + }; + + // Item category + enum class EItemCategory : uint8 + { + None, + Weapon, // Ranged weapons + Melee, // Melee weapons + Trap, // Traps (floor, wall, ceiling) + Resource, // Wood, Stone, Metal + Crafting, // Crafting materials + Ammo, // Ammunition + Consumable, // Healing items, etc. + Gadget, // Adrenaline Rush, Turrets, etc. + Hero, // Hero characters + Schematic, // Weapon/trap schematics + Survivor, // Survivor squad members + Defender, // Defenders + LootDrop // Loot llamas, etc. + }; + + // Weapon type + enum class EWeaponType : uint8 + { + AssaultRifle, + Shotgun, + SMG, + Pistol, + Sniper, + ExplosiveLauncher, + Bow, + Sword, + Axe, + Hammer, + Spear, + Scythe, + Club, + Hardware + }; + + // NOTE: ETrapType is defined in BuildingTypes.h + // Include "../Building/BuildingTypes.h" if we need trap type definitions + + // Resource type + enum class EResourceType : uint8 + { + Wood, + Stone, + Metal, + + // Crafting tiers + Copper, // Tier 1 + Silver, // Tier 2 + Malachite, // Tier 3 + Obsidian, // Tier 4 + Shadowshard, // Tier 4 (alt) + Brightcore, // Tier 5 + Sunbeam, // Tier 5 (alt) + + // Other crafting + Twine, + Rough, // Rough Ore + Mineral, // Mineral Powder + Mechanical, // Mechanical Parts + Duct, // Duct Tape + Bacon, // Batteries, etc. + Herb, + Flower, + Resin + }; + + // Item instance (runtime item in inventory) + struct FInventoryItem + { + std::string ItemId; // Unique ID + std::string TemplateId; // Item definition template + std::string ItemName; // Display name + + EItemCategory Category; + EItemRarity Rarity; + + int32 Count; // Stack count + int32 MaxStackSize; + int32 Level; // Item level (power level) + + // Weapon-specific + float Durability; // Current durability + float MaxDurability; + int32 AmmoCount; // Current ammo in clip + int32 MaxAmmo; + + // Schematic-specific + int32 SchematicLevel; // Evolution level + int32 SchematicTier; // Material tier + + // Slot info + int32 SlotIndex; + bool bIsEquipped; + bool bIsFavorite; + + FInventoryItem() + : Category(EItemCategory::None) + , Rarity(EItemRarity::Common) + , Count(1) + , MaxStackSize(1) + , Level(1) + , Durability(100.0f) + , MaxDurability(100.0f) + , AmmoCount(0) + , MaxAmmo(0) + , SchematicLevel(1) + , SchematicTier(1) + , SlotIndex(-1) + , bIsEquipped(false) + , bIsFavorite(false) + {} + }; + + // Quickbar slot (weapon/ability slots) + struct FQuickbarSlot + { + int32 SlotIndex; + std::string ItemId; // Linked inventory item + bool bIsEmpty; + bool bIsEnabled; + + FQuickbarSlot() + : SlotIndex(-1) + , bIsEmpty(true) + , bIsEnabled(true) + {} + }; + + // Quickbar (primary = weapons, secondary = build, etc.) + struct FQuickbar + { + int32 QuickbarIndex; + int32 CurrentSlot; + std::vector Slots; + + FQuickbar() + : QuickbarIndex(-1) + , CurrentSlot(0) + {} + }; + + // Weapon stats (from schematic) + struct FWeaponStats + { + float Damage; + float FireRate; + float ReloadTime; + float MagazineSize; + float Range; + float CritChance; + float CritDamage; + float Impact; // Stagger/knockback + float DurabilityPerUse; + + // Element + std::string ElementType; // Fire, Water, Nature, Physical, Energy + float ElementDamagePercent; + + FWeaponStats() + : Damage(0.0f) + , FireRate(1.0f) + , ReloadTime(1.0f) + , MagazineSize(30) + , Range(1000.0f) + , CritChance(0.05f) + , CritDamage(0.5f) + , Impact(0.0f) + , DurabilityPerUse(0.01f) + , ElementDamagePercent(0.0f) + {} + }; + + // NOTE: FTrapStats is defined in BuildingTypes.h + // Include "../Building/BuildingTypes.h" if we need trap stats definitions + + // Crafting recipe + struct FCraftingRecipe + { + std::string ResultTemplateId; + int32 ResultCount; + + struct FIngredient + { + std::string TemplateId; + int32 Count; + }; + + std::vector Ingredients; + + FCraftingRecipe() + : ResultCount(1) + {} + }; + + // Loot drop definition + struct FLootDrop + { + std::string LootId; + std::string LootTableId; + float DropChance; + int32 MinCount; + int32 MaxCount; + EItemRarity MinRarity; + EItemRarity MaxRarity; + + FLootDrop() + : DropChance(1.0f) + , MinCount(1) + , MaxCount(1) + , MinRarity(EItemRarity::Common) + , MaxRarity(EItemRarity::Legendary) + {} + }; + + // Inventory change event + struct FInventoryChangeEvent + { + enum class EChangeType : uint8 + { + Added, + Removed, + Modified, + Equipped, + Unequipped + }; + + EChangeType Type; + std::string ItemId; + int32 OldCount; + int32 NewCount; + int32 SlotIndex; + + FInventoryChangeEvent() + : Type(EChangeType::Modified) + , OldCount(0) + , NewCount(0) + , SlotIndex(-1) + {} + }; + +} \ No newline at end of file diff --git a/STW/Missions/MissionManager.cpp b/STW/Missions/MissionManager.cpp new file mode 100644 index 0000000..f41364e --- /dev/null +++ b/STW/Missions/MissionManager.cpp @@ -0,0 +1,425 @@ +/** + * UniversalSlashingSimulator - Mission Manager Implementation + */ + +#include "MissionManager.h" +#include "MissionObjective.h" +#include "../../Core/Logging/Log.h" +#include "../../Engine/EngineCore.h" + +namespace USS +{ + FMissionManager::FMissionManager() + : m_State(EMissionState::None) + , m_Score(0) + { + m_WaveInfo = {}; + } + + FMissionManager::~FMissionManager() + { + Shutdown(); + } + + EResult FMissionManager::Initialize() + { + USS_LOG("Initializing Mission Manager..."); + + m_State = EMissionState::None; + m_Objectives.clear(); + m_EventCallbacks.clear(); + m_Score = 0; + + // Cache engine references + // TODO: Find AFortMissionManager in world + m_MissionManagerActor = GetEngineCore().FindObjectByName("FortMissionManager"); + + USS_LOG("Mission Manager initialized"); + return EResult::Success; + } + + void FMissionManager::Shutdown() + { + if (m_State != EMissionState::None) + { + AbortMission(); + } + + m_Objectives.clear(); + m_EventCallbacks.clear(); + m_MissionActor = UObjectWrapper(); + m_MissionManagerActor = UObjectWrapper(); + m_AIDirector = UObjectWrapper(); + } + + void FMissionManager::Update() + { + if (!IsActive()) + return; + + // Update all objectives + for (auto& Objective : m_Objectives) + { + if (Objective) + { + Objective->Update(); + } + } + + // Check for mission completion + CheckObjectivesComplete(); + } + + EResult FMissionManager::StartMission(const FMissionConfig& Config) + { + if (m_State != EMissionState::None && m_State != EMissionState::Complete && + m_State != EMissionState::Failed) + { + USS_WARN("Cannot start mission - already active"); + return EResult::InvalidState; + } + + USS_LOG("Starting mission: %s (Type: %d, Difficulty: %d)", + Config.MissionName.c_str(), + static_cast(Config.Type), + Config.Difficulty); + + m_Config = Config; + m_Score = 0; + m_Objectives.clear(); + + // Initialize wave info + m_WaveInfo.CurrentWave = 0; + m_WaveInfo.MaxWaves = Config.MaxWaves > 0 ? Config.MaxWaves : 1; + m_WaveInfo.EnemiesRemaining = 0; + m_WaveInfo.EnemiesSpawned = 0; + m_WaveInfo.bIsActive = false; + + // Create default objectives based on mission type + CreateDefaultObjectives(); + + SetState(EMissionState::Loading); + + // TODO: Would spawn mission actors, initialize AI director, etc. + // For now, transition directly to active + SetState(EMissionState::Active); + + USS_LOG("Mission started with %d objectives", static_cast(m_Objectives.size())); + + return EResult::Success; + } + + void FMissionManager::EndMission(bool bSuccess) + { + if (!IsActive() && m_State != EMissionState::Loading) + { + USS_WARN("Cannot end mission - not active"); + return; + } + + USS_LOG("Ending mission: %s", bSuccess ? "SUCCESS" : "FAILURE"); + + // End any active wave + if (m_WaveInfo.bIsActive) + { + EndWave(); + } + + // Calculate result + CalculateResult(); + m_Result.bSuccess = bSuccess; + + SetState(bSuccess ? EMissionState::Complete : EMissionState::Failed); + + // Notify callbacks + OnMissionEvent(bSuccess ? "MissionComplete" : "MissionFailed", &m_Result); + } + + void FMissionManager::AbortMission() + { + USS_LOG("Aborting mission"); + + if (m_WaveInfo.bIsActive) + { + EndWave(); + } + + m_Result = FMissionResult(); + m_Result.bSuccess = false; + + SetState(EMissionState::None); + + OnMissionEvent("MissionAborted", nullptr); + } + + void FMissionManager::AddObjective(std::unique_ptr Objective) + { + if (Objective) + { + USS_LOG("Adding objective: %s", Objective->GetDisplayText().c_str()); + m_Objectives.push_back(std::move(Objective)); + } + } + + FMissionObjective* FMissionManager::GetObjective(int32 Index) const + { + if (Index >= 0 && Index < static_cast(m_Objectives.size())) + { + return m_Objectives[Index].get(); + } + return nullptr; + } + + void FMissionManager::UpdateObjectiveProgress(int32 Index, int32 Progress) + { + FMissionObjective* Objective = GetObjective(Index); + if (Objective) + { + Objective->SetProgress(Progress); + } + } + + void FMissionManager::StartWave(int32 WaveNumber) + { + if (m_State != EMissionState::Active && m_State != EMissionState::DefensePhase) + { + USS_WARN("Cannot start wave - mission not active"); + return; + } + + USS_LOG("Starting wave %d/%d", WaveNumber, m_WaveInfo.MaxWaves); + + m_WaveInfo.CurrentWave = WaveNumber; + m_WaveInfo.bIsActive = true; + m_WaveInfo.EnemiesSpawned = 0; + m_WaveInfo.EnemiesRemaining = 0; + m_WaveInfo.WaveStartTime = 0.0f; // Would get from engine + + SetState(EMissionState::DefensePhase); + + OnMissionEvent("WaveStarted", &m_WaveInfo); + } + + void FMissionManager::EndWave() + { + if (!m_WaveInfo.bIsActive) + return; + + USS_LOG("Ending wave %d", m_WaveInfo.CurrentWave); + + m_WaveInfo.bIsActive = false; + + OnMissionEvent("WaveEnded", &m_WaveInfo); + + // Check if all waves complete + if (m_WaveInfo.CurrentWave >= m_WaveInfo.MaxWaves) + { + USS_LOG("All waves complete"); + OnMissionEvent("AllWavesComplete", nullptr); + } + else + { + // Return to active state between waves + SetState(EMissionState::Active); + } + } + + void FMissionManager::AddScore(int32 Points) + { + m_Score += Points; + USS_LOG("Score: %d (+%d)", m_Score, Points); + } + + void FMissionManager::OnProcessEvent(void* Object, void* Function, void* Params) + { + // Route relevant ProcessEvent calls to mission system + // TODO: Would check function name and dispatch accordingly + // Prefferrably we would avoid PE alltogether and hook into specific game events + + // Example event routing: + // "ServerHandleEnemyKilled" -> OnMissionEvent("EnemyKilled", Params) + // "ServerHandleSurvivorRescued" -> OnMissionEvent("SurvivorRescued", Params) + } + + void FMissionManager::OnMissionEvent(const char* EventName, void* Params) + { + // Dispatch to objectives + for (auto& Objective : m_Objectives) + { + if (Objective) + { + Objective->OnEvent(EventName, Params); + } + } + + // Dispatch to registered callbacks + for (const auto& Callback : m_EventCallbacks) + { + if (Callback) + { + Callback(EventName, Params); + } + } + } + + void FMissionManager::RegisterEventCallback(FMissionEventCallback Callback) + { + if (Callback) + { + m_EventCallbacks.push_back(std::move(Callback)); + } + } + + FMissionResult FMissionManager::GetResult() const + { + return m_Result; + } + + void FMissionManager::SetState(EMissionState NewState) + { + if (m_State != NewState) + { + EMissionState OldState = m_State; + m_State = NewState; + + USS_LOG("Mission state: %d -> %d", static_cast(OldState), static_cast(NewState)); + } + } + + void FMissionManager::CreateDefaultObjectives() + { + // Create objectives based on mission type + switch (m_Config.Type) + { + case EMissionType::FarmsteadDefense: + { + FObjectiveDefinition DefendDef; + DefendDef.Type = EObjectiveType::Defend; + DefendDef.DisplayText = "Defend the Atlas"; + DefendDef.TimeLimit = 480.0f; // 8 minutes + DefendDef.bIsPrimary = true; + AddObjective(CreateObjective(DefendDef)); + break; + } + + case EMissionType::SurvivorsRescue: + { + FObjectiveDefinition RescueDef; + RescueDef.Type = EObjectiveType::Rescue; + RescueDef.DisplayText = "Rescue Survivors"; + RescueDef.TargetCount = 6; + RescueDef.bIsPrimary = true; + AddObjective(CreateObjective(RescueDef)); + break; + } + + case EMissionType::EncampmentDestruction: + { + FObjectiveDefinition DestroyDef; + DestroyDef.Type = EObjectiveType::Kill; + DestroyDef.DisplayText = "Destroy Encampments"; + DestroyDef.TargetCount = 5; + DestroyDef.bIsPrimary = true; + AddObjective(CreateObjective(DestroyDef)); + break; + } + + case EMissionType::StormShieldDefense: + { + FObjectiveDefinition DefendDef; + DefendDef.Type = EObjectiveType::Defend; + DefendDef.DisplayText = "Defend the Storm Shield"; + DefendDef.bIsPrimary = true; + AddObjective(CreateObjective(DefendDef)); + break; + } + + case EMissionType::RadarGridConstruction: + { + FObjectiveDefinition BuildDef; + BuildDef.Type = EObjectiveType::Build; + BuildDef.DisplayText = "Build Radar Towers"; + BuildDef.TargetCount = 5; + BuildDef.bIsPrimary = true; + AddObjective(CreateObjective(BuildDef)); + break; + } + + default: + { + // Generic kill objective + FObjectiveDefinition KillDef; + KillDef.Type = EObjectiveType::Kill; + KillDef.DisplayText = "Eliminate Enemies"; + KillDef.TargetCount = 50; + KillDef.bIsPrimary = true; + AddObjective(CreateObjective(KillDef)); + break; + } + } + } + + void FMissionManager::CheckObjectivesComplete() + { + bool bAllPrimaryComplete = true; + bool bAnyPrimaryFailed = false; + + for (const auto& Objective : m_Objectives) + { + if (Objective && Objective->GetDefinition().bIsPrimary) + { + if (Objective->IsFailed()) + { + bAnyPrimaryFailed = true; + break; + } + + if (!Objective->IsComplete()) + { + bAllPrimaryComplete = false; + } + } + } + + if (bAnyPrimaryFailed) + { + EndMission(false); + } + else if (bAllPrimaryComplete) + { + EndMission(true); + } + } + + void FMissionManager::CalculateResult() + { + m_Result.FinalScore = m_Score; + m_Result.WavesCompleted = m_WaveInfo.CurrentWave; + m_Result.TotalWaves = m_WaveInfo.MaxWaves; + m_Result.bSuccess = true; + + // Count objective completions + m_Result.ObjectivesCompleted = 0; + m_Result.TotalObjectives = static_cast(m_Objectives.size()); + + for (const auto& Objective : m_Objectives) + { + if (Objective && Objective->IsComplete()) + { + m_Result.ObjectivesCompleted++; + } + else if (Objective && Objective->IsFailed() && Objective->GetDefinition().bIsPrimary) + { + m_Result.bSuccess = false; + } + } + + USS_LOG("Mission result: Score=%d, Objectives=%d/%d, Waves=%d/%d", + m_Result.FinalScore, + m_Result.ObjectivesCompleted, + m_Result.TotalObjectives, + m_Result.WavesCompleted, + m_Result.TotalWaves); + } + +} \ No newline at end of file diff --git a/STW/Missions/MissionManager.h b/STW/Missions/MissionManager.h new file mode 100644 index 0000000..01ff878 --- /dev/null +++ b/STW/Missions/MissionManager.h @@ -0,0 +1,99 @@ +/** + * UniversalSlashingSimulator - Mission Manager + * + * Manages mission lifecycle, objectives, waves, and scoring. + * Based on AFortMission and AFortMissionManager from STW. + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../../Engine/UObject/UObjectWrapper.h" +#include "MissionTypes.h" +#include "MissionObjective.h" +#include +#include +#include + +namespace USS +{ + // Mission event callback + using FMissionEventCallback = std::function; + + class FMissionManager + { + public: + FMissionManager(); + ~FMissionManager(); + + USS_NON_COPYABLE(FMissionManager) + USS_NON_MOVABLE(FMissionManager) + + // Lifecycle + EResult Initialize(); + void Shutdown(); + void Update(); + + // Mission control + EResult StartMission(const FMissionConfig& Config); + void EndMission(bool bSuccess); + void AbortMission(); + + // State + EMissionState GetState() const { return m_State; } + const FMissionConfig& GetConfig() const { return m_Config; } + bool IsActive() const { return m_State == EMissionState::Active || m_State == EMissionState::DefensePhase; } + + // Objectives + void AddObjective(std::unique_ptr Objective); + FMissionObjective* GetObjective(int32 Index) const; + int32 GetObjectiveCount() const { return static_cast(m_Objectives.size()); } + void UpdateObjectiveProgress(int32 Index, int32 Progress); + + // Waves + void StartWave(int32 WaveNumber); + void EndWave(); + const FWaveInfo& GetWaveInfo() const { return m_WaveInfo; } + + // Scoring + int32 GetScore() const { return m_Score; } + void AddScore(int32 Points); + + // Events + void OnProcessEvent(void* Object, void* Function, void* Params); + void OnMissionEvent(const char* EventName, void* Params); + + // Callbacks + void RegisterEventCallback(FMissionEventCallback Callback); + + // Result + FMissionResult GetResult() const; + + private: + void SetState(EMissionState NewState); + void CreateDefaultObjectives(); + void CheckObjectivesComplete(); + void CalculateResult(); + + // State + EMissionState m_State; + FMissionConfig m_Config; + FWaveInfo m_WaveInfo; + int32 m_Score; + + // Objectives + std::vector> m_Objectives; + + // Engine references (cached) + UObjectWrapper m_MissionActor; // AFortMission* + UObjectWrapper m_MissionManagerActor; // AFortMissionManager* + UObjectWrapper m_AIDirector; // AFortAIDirector* + + // Callbacks + std::vector m_EventCallbacks; + + // Result + FMissionResult m_Result; + }; + +} \ No newline at end of file diff --git a/STW/Missions/MissionObjective.cpp b/STW/Missions/MissionObjective.cpp new file mode 100644 index 0000000..87d65a2 --- /dev/null +++ b/STW/Missions/MissionObjective.cpp @@ -0,0 +1,321 @@ +/** + * UniversalSlashingSimulator - Mission Objective Implementation + */ + +#include "MissionObjective.h" +#include "../../Core/Logging/Log.h" + +namespace USS +{ + // ======================================================================== + // FMissionObjective (Base) + // ======================================================================== + + FMissionObjective::FMissionObjective(const FObjectiveDefinition& Definition) + : m_Definition(Definition) + , m_State(EObjectiveState::Inactive) + , m_CurrentProgress(0) + { + } + + void FMissionObjective::Update() + { + // Base implementation does nothing + // Derived classes override for time-based updates + } + + void FMissionObjective::OnEvent(const char* EventName, void* Params) + { + // Base implementation does nothing + // Derived classes handle specific events + } + + float FMissionObjective::GetProgressPercent() const + { + if (m_Definition.TargetCount <= 0) + return m_State == EObjectiveState::Completed ? 1.0f : 0.0f; + + return static_cast(m_CurrentProgress) / static_cast(m_Definition.TargetCount); + } + + void FMissionObjective::AddProgress(int32 Amount) + { + SetProgress(m_CurrentProgress + Amount); + } + + void FMissionObjective::SetProgress(int32 Progress) + { + int32 OldProgress = m_CurrentProgress; + m_CurrentProgress = Progress; + + if (m_CurrentProgress < 0) + m_CurrentProgress = 0; + + if (m_CurrentProgress != OldProgress) + { + OnProgressChanged(); + + // Auto-complete if target reached + if (m_Definition.TargetCount > 0 && m_CurrentProgress >= m_Definition.TargetCount) + { + Complete(); + } + } + } + + void FMissionObjective::Complete() + { + if (m_State != EObjectiveState::Completed && m_State != EObjectiveState::Failed) + { + SetState(EObjectiveState::Completed); + USS_LOG("Objective completed: %s", m_Definition.DisplayText.c_str()); + } + } + + void FMissionObjective::Fail() + { + if (m_State != EObjectiveState::Completed && m_State != EObjectiveState::Failed) + { + SetState(EObjectiveState::Failed); + USS_LOG("Objective failed: %s", m_Definition.DisplayText.c_str()); + } + } + + void FMissionObjective::OnProgressChanged() + { + USS_LOG("Objective progress: %s - %d/%d", + m_Definition.DisplayText.c_str(), + m_CurrentProgress, + m_Definition.TargetCount); + } + + void FMissionObjective::OnStateChanged(EObjectiveState OldState) + { + // Base implementation does nothing + } + + void FMissionObjective::SetState(EObjectiveState NewState) + { + if (m_State != NewState) + { + EObjectiveState OldState = m_State; + m_State = NewState; + OnStateChanged(OldState); + } + } + + // ======================================================================== + // FKillObjective + // ======================================================================== + + FKillObjective::FKillObjective(const FObjectiveDefinition& Definition) + : FMissionObjective(Definition) + { + // Could parse target class from definition metadata + } + + void FKillObjective::OnEvent(const char* EventName, void* Params) + { + if (m_State != EObjectiveState::Active) + return; + + // Handle enemy killed event + // TODO: Check if EventName matches kill event, + // validate killed actor class, increment progress + if (strcmp(EventName, "EnemyKilled") == 0) + { + // TODO: Validate target class if specified + AddProgress(1); + } + } + + // ======================================================================== + // FCollectObjective + // ======================================================================== + + FCollectObjective::FCollectObjective(const FObjectiveDefinition& Definition) + : FMissionObjective(Definition) + { + } + + void FCollectObjective::OnEvent(const char* EventName, void* Params) + { + if (m_State != EObjectiveState::Active) + return; + + if (strcmp(EventName, "ItemCollected") == 0) + { + // TODO: Validate item class + AddProgress(1); + } + } + + // ======================================================================== + // FDefendObjective + // ======================================================================== + + FDefendObjective::FDefendObjective(const FObjectiveDefinition& Definition) + : FMissionObjective(Definition) + , m_DefendTimeRemaining(Definition.TimeLimit) + , m_CurrentHealth(100.0f) + , m_MaxHealth(100.0f) + , m_bDefenseActive(false) + { + } + + void FDefendObjective::Update() + { + if (m_State != EObjectiveState::Active || !m_bDefenseActive) + return; + + // TODO: Get delta time from engine + float DeltaTime = 1.0f / 30.0f; // Placeholder 30fps + + m_DefendTimeRemaining -= DeltaTime; + + if (m_DefendTimeRemaining <= 0.0f) + { + m_DefendTimeRemaining = 0.0f; + Complete(); + } + } + + void FDefendObjective::OnEvent(const char* EventName, void* Params) + { + if (m_State != EObjectiveState::Active) + return; + + if (strcmp(EventName, "DefenseStarted") == 0) + { + m_bDefenseActive = true; + USS_LOG("Defense phase started"); + } + else if (strcmp(EventName, "ObjectDamaged") == 0) + { + // TODO: Extract damage amount from Params + float Damage = 10.0f; // Placeholder + m_CurrentHealth -= Damage; + + if (m_CurrentHealth <= 0.0f) + { + m_CurrentHealth = 0.0f; + Fail(); + } + } + } + + float FDefendObjective::GetHealthPercent() const + { + if (m_MaxHealth <= 0.0f) + return 1.0f; + return m_CurrentHealth / m_MaxHealth; + } + + // ======================================================================== + // FExploreObjective + // ======================================================================== + + FExploreObjective::FExploreObjective(const FObjectiveDefinition& Definition) + : FMissionObjective(Definition) + , m_bLocationReached(false) + { + } + + void FExploreObjective::OnEvent(const char* EventName, void* Params) + { + if (m_State != EObjectiveState::Active) + return; + + if (strcmp(EventName, "LocationReached") == 0) + { + // TODO: Validate location matches objective + m_bLocationReached = true; + Complete(); + } + } + + // ======================================================================== + // FBuildObjective + // ======================================================================== + + FBuildObjective::FBuildObjective(const FObjectiveDefinition& Definition) + : FMissionObjective(Definition) + { + } + + void FBuildObjective::OnEvent(const char* EventName, void* Params) + { + if (m_State != EObjectiveState::Active) + return; + + if (strcmp(EventName, "BuildingPlaced") == 0) + { + // TODO: Validate building class if specified + AddProgress(1); + } + } + + // ======================================================================== + // FRescueObjective + // ======================================================================== + + FRescueObjective::FRescueObjective(const FObjectiveDefinition& Definition) + : FMissionObjective(Definition) + , m_SurvivorsLost(0) + , m_MaxLosses(0) // Could be set from definition + { + } + + void FRescueObjective::OnEvent(const char* EventName, void* Params) + { + if (m_State != EObjectiveState::Active) + return; + + if (strcmp(EventName, "SurvivorRescued") == 0) + { + AddProgress(1); + } + else if (strcmp(EventName, "SurvivorLost") == 0) + { + m_SurvivorsLost++; + + if (m_MaxLosses > 0 && m_SurvivorsLost >= m_MaxLosses) + { + Fail(); + } + } + } + + // ======================================================================== + // Factory + // ======================================================================== + + std::unique_ptr CreateObjective(const FObjectiveDefinition& Definition) + { + switch (Definition.Type) + { + case EObjectiveType::Kill: + return std::make_unique(Definition); + + case EObjectiveType::Collect: + return std::make_unique(Definition); + + case EObjectiveType::Defend: + return std::make_unique(Definition); + + case EObjectiveType::Explore: + return std::make_unique(Definition); + + case EObjectiveType::Build: + return std::make_unique(Definition); + + case EObjectiveType::Rescue: + return std::make_unique(Definition); + + default: + USS_WARN("Unknown objective type: %d", static_cast(Definition.Type)); + return std::make_unique(Definition); + } + } + +} \ No newline at end of file diff --git a/STW/Missions/MissionObjective.h b/STW/Missions/MissionObjective.h new file mode 100644 index 0000000..5c1902d --- /dev/null +++ b/STW/Missions/MissionObjective.h @@ -0,0 +1,150 @@ +/** + * UniversalSlashingSimulator - Mission Objective + * + * Base class for mission objectives. Derived classes handle + * specific objective types (kill, collect, defend, etc.). + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../../Engine/UObject/UObjectWrapper.h" +#include "MissionTypes.h" +#include + +namespace USS +{ + // Base mission objective class + class FMissionObjective + { + public: + FMissionObjective(const FObjectiveDefinition& Definition); + virtual ~FMissionObjective() = default; + + USS_NON_COPYABLE(FMissionObjective) + + // Core interface + virtual void Update(); + virtual void OnEvent(const char* EventName, void* Params); + + // State + EObjectiveState GetState() const { return m_State; } + bool IsComplete() const { return m_State == EObjectiveState::Completed; } + bool IsFailed() const { return m_State == EObjectiveState::Failed; } + + // Progress + int32 GetCurrentProgress() const { return m_CurrentProgress; } + int32 GetTargetProgress() const { return m_Definition.TargetCount; } + float GetProgressPercent() const; + + // Definition + const FObjectiveDefinition& GetDefinition() const { return m_Definition; } + EObjectiveType GetType() const { return m_Definition.Type; } + const std::string& GetDisplayText() const { return m_Definition.DisplayText; } + + // Progress updates + void AddProgress(int32 Amount); + void SetProgress(int32 Progress); + void Complete(); + void Fail(); + + protected: + virtual void OnProgressChanged(); + virtual void OnStateChanged(EObjectiveState OldState); + + void SetState(EObjectiveState NewState); + + FObjectiveDefinition m_Definition; + EObjectiveState m_State; + int32 m_CurrentProgress; + + // Cached engine references + UObjectWrapper m_ObjectiveActor; // AFortObjective* + }; + + // Kill enemies objective + class FKillObjective : public FMissionObjective + { + public: + FKillObjective(const FObjectiveDefinition& Definition); + + void OnEvent(const char* EventName, void* Params) override; + + private: + std::string m_TargetActorClass; // Specific enemy class to kill, empty = any + }; + + // Collect items objective + class FCollectObjective : public FMissionObjective + { + public: + FCollectObjective(const FObjectiveDefinition& Definition); + + void OnEvent(const char* EventName, void* Params) override; + + private: + std::string m_ItemClass; // Item class to collect + }; + + // Defend location/object objective + class FDefendObjective : public FMissionObjective + { + public: + FDefendObjective(const FObjectiveDefinition& Definition); + + void Update() override; + void OnEvent(const char* EventName, void* Params) override; + + // Defend-specific + float GetDefendTimeRemaining() const { return m_DefendTimeRemaining; } + float GetDefendTimerTotal() const { return m_Definition.TimeLimit; } + float GetHealthPercent() const; + + private: + float m_DefendTimeRemaining; + float m_CurrentHealth; + float m_MaxHealth; + bool m_bDefenseActive; + }; + + // Explore/reach location objective + class FExploreObjective : public FMissionObjective + { + public: + FExploreObjective(const FObjectiveDefinition& Definition); + + void OnEvent(const char* EventName, void* Params) override; + + private: + bool m_bLocationReached; + }; + + // Build structures objective + class FBuildObjective : public FMissionObjective + { + public: + FBuildObjective(const FObjectiveDefinition& Definition); + + void OnEvent(const char* EventName, void* Params) override; + + private: + std::string m_BuildingClass; // Specific building type, empty = any + }; + + // Rescue survivors objective + class FRescueObjective : public FMissionObjective + { + public: + FRescueObjective(const FObjectiveDefinition& Definition); + + void OnEvent(const char* EventName, void* Params) override; + + private: + int32 m_SurvivorsLost; + int32 m_MaxLosses; + }; + + // Factory function to create objective by type + std::unique_ptr CreateObjective(const FObjectiveDefinition& Definition); + +} \ No newline at end of file diff --git a/STW/Missions/MissionTypes.h b/STW/Missions/MissionTypes.h new file mode 100644 index 0000000..3d8ff42 --- /dev/null +++ b/STW/Missions/MissionTypes.h @@ -0,0 +1,316 @@ +/** + * UniversalSlashingSimulator - Mission Types + * + * Types and enums for the STW mission system. + */ + +#pragma once + +#include "../../Core/Common.h" +#include +#include + +namespace USS +{ + // Mission types from STW + enum class EMissionType : uint8 + { + Unknown = 0, + + // Core mission types + FarmsteadDefense, // Defend the base (Homebase) + SurvivorsRescue, // Rescue survivors + EncampmentDestroy, // Destroy encampments + EncampmentDestruction, // Alias for EncampmentDestroy + RadarBuild, // Build radar towers + RadarGridConstruction, // Alias for RadarBuild + DataRetrieval, // Retrieve data + + // Storm Shield + StormShieldDefense, // SSD missions + + // Special types + EliminateAndCollect, // Kill and collect + DeliverTheBomb, // DTB mission + RepairTheShelter, // RTS mission + EvacuateTheShelter, // Evac mission + RideTheLightning, // RTL mission + CategoryStorm, // 4-atlas + RetrieveTheData, // RTD mission + LaunchTheBalloon, // LTB mission + + // Horde modes + ChallengeTheHorde, + FrostniteEndurance, + + // Wargames + Wargames, + + Max + }; + + // Mission state + enum class EMissionState : uint8 + { + None, + Loading, + Setup, // Players setting up defenses + Active, // Mission in progress + DefensePhase, // Defending objective + Intermission, // Between waves + Completed, + Complete = Completed, // Alias + Failed, + Abandoned + }; + + // Objective state + enum class EObjectiveState : uint8 + { + Inactive, + Active, + Completed, + Failed, + Optional + }; + + // Objective type + enum class EObjectiveType : uint8 + { + Unknown = 0, + DefendLocation, // Defend a specific point + Defend = DefendLocation, // Alias + KillEnemies, // Kill X enemies + Kill = KillEnemies, // Alias + CollectItems, // Collect X items + Collect = CollectItems, // Alias + BuildStructures, // Build X structures + Build = BuildStructures, // Alias + RescueSurvivors, // Rescue X survivors + Rescue = RescueSurvivors, // Alias + DestroyObjects, // Destroy X objects + EscortPayload, // Escort/deliver payload + ActivateDevice, // Activate a device + SurviveWaves, // Survive X waves + Timer, // Time-based objective + Explore, // Exploration objective + }; + + // Difficulty tier + enum class EDifficultyTier : uint8 + { + VeryLow = 0, + Low, + Medium, + High, + VeryHigh, + Extreme, + Max + }; + + // Mission alert type + enum class EMissionAlert : uint8 + { + None = 0, + Storm, + MiniBeoss, + ElementalModifier, + MutantStorm, + GroupMission, + }; + + // Mission configuration + struct FMissionConfig + { + EMissionType Type; + std::string BlueprintPath; // e.g., "Mission_FarmsteadFort_C" + std::string ZoneName; + int32 DifficultyLevel; // 1-140+ + EDifficultyTier DifficultyTier; + EMissionAlert AlertType; + + // Time limits (seconds, 0 = no limit) + int32 SetupTimeLimit; + int32 MissionTimeLimit; + + // Wave settings + int32 WaveCount; + int32 MaxWaves; // Alias for WaveCount + int32 EnemiesPerWave; + + FMissionConfig() + : Type(EMissionType::Unknown) + , DifficultyLevel(1) + , DifficultyTier(EDifficultyTier::Low) + , AlertType(EMissionAlert::None) + , SetupTimeLimit(0) + , MissionTimeLimit(0) + , WaveCount(1) + , MaxWaves(1) + , EnemiesPerWave(20) + {} + }; + + // Objective definition + struct FObjectiveDefinition + { + EObjectiveType Type; + std::string Name; + std::string DisplayText; // Display name for UI + std::string Description; + + int32 TargetCount; // How many to complete + int32 CurrentCount; // Current progress + float TimeLimit; // Time limit in seconds (0 = no limit) + + bool bIsRequired; // Required for mission success + bool bIsPrimary; // Primary objective + bool bIsBonus; // Bonus objective + + EObjectiveState State; + + FObjectiveDefinition() + : Type(EObjectiveType::Unknown) + , TargetCount(1) + , CurrentCount(0) + , TimeLimit(0.0f) + , bIsRequired(true) + , bIsPrimary(true) + , bIsBonus(false) + , State(EObjectiveState::Inactive) + {} + + float GetProgress() const + { + if (TargetCount <= 0) return 0.0f; + return static_cast(CurrentCount) / static_cast(TargetCount); + } + + bool IsComplete() const + { + return CurrentCount >= TargetCount; + } + }; + + // Wave info + struct FWaveInfo + { + int32 WaveNumber; + int32 CurrentWave; // Alias for WaveNumber + int32 TotalWaves; + int32 MaxWaves; // Alias for TotalWaves + int32 EnemiesRemaining; + int32 EnemiesTotal; + int32 EnemiesSpawned; // Total enemies spawned so far + float TimeRemaining; // Seconds + double WaveStartTime; // When this wave started + bool bIsDefenseWave; + bool bIsActive; // Is wave currently active + + FWaveInfo() + : WaveNumber(0) + , CurrentWave(0) + , TotalWaves(1) + , MaxWaves(1) + , EnemiesRemaining(0) + , EnemiesTotal(0) + , EnemiesSpawned(0) + , TimeRemaining(0.0f) + , WaveStartTime(0.0) + , bIsDefenseWave(false) + , bIsActive(false) + {} + }; + + // Mission rewards + struct FMissionReward + { + std::string ItemId; + int32 Quantity; + int32 Rarity; // 0=common, 1=uncommon, etc. + bool bIsBonusReward; + + FMissionReward() + : Quantity(1) + , Rarity(0) + , bIsBonusReward(false) + {} + }; + + // Mission result + struct FMissionResult + { + EMissionState FinalState; + bool bSuccess; // Did mission succeed + float CompletionPercentage; + int32 ScoreEarned; + int32 FinalScore; // Alias for ScoreEarned + int32 XPEarned; + + // Wave completion stats + int32 WavesCompleted; + int32 TotalWaves; + + // Objective stats + int32 ObjectivesCompleted; + int32 TotalObjectives; + + std::vector Rewards; + + FMissionResult() + : FinalState(EMissionState::None) + , bSuccess(false) + , CompletionPercentage(0.0f) + , ScoreEarned(0) + , FinalScore(0) + , XPEarned(0) + , WavesCompleted(0) + , TotalWaves(0) + , ObjectivesCompleted(0) + , TotalObjectives(0) + {} + }; + + // Helper functions + inline const char* GetMissionTypeName(EMissionType Type) + { + switch (Type) + { + case EMissionType::FarmsteadDefense: return "Defend the Base"; + case EMissionType::SurvivorsRescue: return "Rescue the Survivors"; + case EMissionType::EncampmentDestroy: return "Destroy the Encampments"; + case EMissionType::RadarBuild: return "Build the Radar"; + case EMissionType::DataRetrieval: return "Retrieve the Data"; + case EMissionType::StormShieldDefense: return "Storm Shield Defense"; + case EMissionType::DeliverTheBomb: return "Deliver the Bomb"; + case EMissionType::RepairTheShelter: return "Repair the Shelter"; + case EMissionType::EvacuateTheShelter: return "Evacuate the Shelter"; + case EMissionType::RideTheLightning: return "Ride the Lightning"; + case EMissionType::CategoryStorm: return "Category Storm"; + case EMissionType::RetrieveTheData: return "Retrieve the Data"; + case EMissionType::LaunchTheBalloon: return "Launch the Balloon"; + case EMissionType::ChallengeTheHorde: return "Challenge the Horde"; + case EMissionType::FrostniteEndurance: return "Frostnite"; + case EMissionType::Wargames: return "Wargames"; + default: return "Unknown"; + } + } + + inline const char* GetMissionStateName(EMissionState State) + { + switch (State) + { + case EMissionState::None: return "None"; + case EMissionState::Loading: return "Loading"; + case EMissionState::Setup: return "Setup"; + case EMissionState::Active: return "Active"; + case EMissionState::DefensePhase: return "Defense"; + case EMissionState::Intermission: return "Intermission"; + case EMissionState::Completed: return "Completed"; + case EMissionState::Failed: return "Failed"; + case EMissionState::Abandoned: return "Abandoned"; + default: return "Unknown"; + } + } + +} \ No newline at end of file diff --git a/STW/Player/STWPlayerController.cpp b/STW/Player/STWPlayerController.cpp new file mode 100644 index 0000000..42280ee --- /dev/null +++ b/STW/Player/STWPlayerController.cpp @@ -0,0 +1,397 @@ +/** + * UniversalSlashingSimulator - STW Player Controller Implementation + */ + +#include "STWPlayerController.h" +#include "STWPlayerPawn.h" +#include "../../Core/Logging/Log.h" +#include "../../Engine/EngineCore.h" + +namespace USS +{ + // ======================================================================== + // FSTWPlayerController + // ======================================================================== + + FSTWPlayerController::FSTWPlayerController() + : m_ReadyState(EPlayerReadyState::NotReady) + , m_TeamIndex(0) + , m_SquadSlot(-1) + , m_bInBuildMode(false) + , m_bInfoDirty(true) + { + } + + FSTWPlayerController::FSTWPlayerController(void* InController) + : m_Controller(InController) + , m_ReadyState(EPlayerReadyState::NotReady) + , m_TeamIndex(0) + , m_SquadSlot(-1) + , m_bInBuildMode(false) + , m_bInfoDirty(true) + { + if (IsValid()) + { + UpdateFromNative(); + } + } + + FSTWPlayerController::~FSTWPlayerController() + { + m_pPawn.reset(); + } + + bool FSTWPlayerController::IsValid() const + { + return m_Controller.IsValid(); + } + + FSTWPlayerInfo FSTWPlayerController::GetPlayerInfo() const + { + if (m_bInfoDirty && IsValid()) + { + // Read from native controller + // TODO: Read PlayerState properties + + m_CachedInfo.bIsValid = true; + m_CachedInfo.TeamIndex = m_TeamIndex; + m_CachedInfo.SquadSlot = m_SquadSlot; + m_CachedInfo.ReadyState = m_ReadyState; + + // TODO: Read from APlayerState + // m_CachedInfo.PlayerName = ... + // m_CachedInfo.PlayerId = ... + + m_bInfoDirty = false; + } + + return m_CachedInfo; + } + + std::string FSTWPlayerController::GetPlayerName() const + { + if (!IsValid()) + return ""; + + // TODO: Read from PlayerState->GetPlayerName() + return m_CachedInfo.PlayerName; + } + + std::string FSTWPlayerController::GetPlayerId() const + { + if (!IsValid()) + return ""; + + // TODO: Read from PlayerState->GetUniqueId() + return m_CachedInfo.PlayerId; + } + + FSTWPlayerPawn* FSTWPlayerController::GetPawn() const + { + return m_pPawn.get(); + } + + void FSTWPlayerController::SetPawn(void* InPawn) + { + if (InPawn) + { + m_pPawn = std::make_unique(InPawn); + USS_LOG("Pawn set for player: %s", GetPlayerName().c_str()); + } + else + { + m_pPawn.reset(); + } + } + + bool FSTWPlayerController::HasPawn() const + { + return m_pPawn && m_pPawn->IsValid(); + } + + void FSTWPlayerController::SetReadyState(EPlayerReadyState State) + { + if (m_ReadyState != State) + { + m_ReadyState = State; + m_bInfoDirty = true; + + USS_LOG("Player %s ready state: %d", GetPlayerName().c_str(), static_cast(State)); + } + } + + void FSTWPlayerController::SetTeamInfo(int32 TeamIndex, int32 SquadSlot) + { + m_TeamIndex = TeamIndex; + m_SquadSlot = SquadSlot; + m_bInfoDirty = true; + } + + void FSTWPlayerController::ActivateAbility(int32 AbilityIndex) + { + if (!HasPawn()) + return; + + // TODO: Call ability activation on pawn's AbilitySystemComponent + USS_LOG("Activating ability %d for player %s", AbilityIndex, GetPlayerName().c_str()); + } + + void FSTWPlayerController::DeactivateAbility(int32 AbilityIndex) + { + if (!HasPawn()) + return; + + USS_LOG("Deactivating ability %d", AbilityIndex); + } + + bool FSTWPlayerController::IsAbilityReady(int32 AbilityIndex) const + { + if (!HasPawn()) + return false; + + // TODO: Check ability cooldown + return true; + } + + void FSTWPlayerController::UseGadget(int32 GadgetSlot) + { + if (!HasPawn()) + return; + + USS_LOG("Using gadget slot %d", GadgetSlot); + // TODO: Trigger gadget use + } + + bool FSTWPlayerController::IsGadgetReady(int32 GadgetSlot) const + { + // TODO: Check gadget cooldown + return true; + } + + void FSTWPlayerController::EnterBuildMode() + { + if (!m_bInBuildMode) + { + m_bInBuildMode = true; + USS_LOG("Entering build mode"); + // TODO: Set building mode state on controller + } + } + + void FSTWPlayerController::ExitBuildMode() + { + if (m_bInBuildMode) + { + m_bInBuildMode = false; + USS_LOG("Exiting build mode"); + } + } + + void FSTWPlayerController::SendChatMessage(const char* Message) + { + if (!IsValid() || !Message) + return; + + USS_LOG("Chat from %s: %s", GetPlayerName().c_str(), Message); + // TODO: Broadcast to other players + } + + void FSTWPlayerController::ShowNotification(const char* Message, float Duration) + { + if (!IsValid() || !Message) + return; + + // TODO: Call client RPC to show notification + USS_LOG("Notification to %s: %s", GetPlayerName().c_str(), Message); + } + + void FSTWPlayerController::ServerAcknowledgePossession(void* Pawn) + { + SetPawn(Pawn); + } + + void FSTWPlayerController::ServerSetReadyState(EPlayerReadyState State) + { + SetReadyState(State); + } + + void FSTWPlayerController::ServerRequestRespawn() + { + if (!IsValid()) + return; + + USS_LOG("Respawn requested for %s", GetPlayerName().c_str()); + // TODO: Handle respawn logic + } + + void FSTWPlayerController::OnProcessEvent(void* Function, void* Params) + { + // Handle controller-specific events + // // prefferrably avoid PE where possible + // TODO: Check function name and dispatch + } + + void FSTWPlayerController::UpdateFromNative() + { + if (!IsValid()) + return; + + // Read current state from native controller + // TODO: Read team, ready state, etc. from engine + m_bInfoDirty = true; + } + + // ======================================================================== + // FPlayerControllerManager + // ======================================================================== + + FPlayerControllerManager& FPlayerControllerManager::Get() + { + static FPlayerControllerManager Instance; + return Instance; + } + + EResult FPlayerControllerManager::Initialize() + { + USS_LOG("Initializing Player Controller Manager..."); + m_Players.clear(); + return EResult::Success; + } + + void FPlayerControllerManager::Shutdown() + { + USS_LOG("Shutting down Player Controller Manager"); + m_Players.clear(); + } + + void FPlayerControllerManager::Update() + { + // Update all player controllers + for (auto& Pair : m_Players) + { + if (Pair.second && Pair.second->HasPawn()) + { + Pair.second->GetPawn()->Update(); + } + } + } + + FSTWPlayerController* FPlayerControllerManager::RegisterPlayer(void* Controller) + { + if (!Controller) + return nullptr; + + auto It = m_Players.find(Controller); + if (It != m_Players.end()) + { + return It->second.get(); + } + + auto NewController = std::make_unique(Controller); + FSTWPlayerController* Result = NewController.get(); + m_Players[Controller] = std::move(NewController); + + USS_LOG("Registered player controller: %p", Controller); + + return Result; + } + + void FPlayerControllerManager::UnregisterPlayer(void* Controller) + { + auto It = m_Players.find(Controller); + if (It != m_Players.end()) + { + USS_LOG("Unregistered player controller: %p", Controller); + m_Players.erase(It); + } + } + + FSTWPlayerController* FPlayerControllerManager::GetPlayer(void* Controller) const + { + auto It = m_Players.find(Controller); + return (It != m_Players.end()) ? It->second.get() : nullptr; + } + + FSTWPlayerController* FPlayerControllerManager::GetPlayerById(const char* PlayerId) const + { + if (!PlayerId) + return nullptr; + + for (const auto& Pair : m_Players) + { + if (Pair.second && Pair.second->GetPlayerId() == PlayerId) + { + return Pair.second.get(); + } + } + + return nullptr; + } + + FSTWPlayerController* FPlayerControllerManager::GetPlayerByIndex(int32 Index) const + { + if (Index < 0 || Index >= static_cast(m_Players.size())) + return nullptr; + + auto It = m_Players.begin(); + std::advance(It, Index); + return It->second.get(); + } + + std::vector FPlayerControllerManager::GetPlayersOnTeam(int32 TeamIndex) const + { + std::vector Result; + + for (const auto& Pair : m_Players) + { + if (Pair.second && Pair.second->GetTeamIndex() == TeamIndex) + { + Result.push_back(Pair.second.get()); + } + } + + return Result; + } + + int32 FPlayerControllerManager::GetReadyPlayerCount() const + { + int32 Count = 0; + + for (const auto& Pair : m_Players) + { + if (Pair.second && Pair.second->IsReady()) + { + Count++; + } + } + + return Count; + } + + bool FPlayerControllerManager::AreAllPlayersReady() const + { + if (m_Players.empty()) + return false; + + for (const auto& Pair : m_Players) + { + if (Pair.second && !Pair.second->IsReady() && !Pair.second->IsInGame()) + { + return false; + } + } + + return true; + } + + void FPlayerControllerManager::OnPlayerJoined(void* Controller) + { + RegisterPlayer(Controller); + } + + void FPlayerControllerManager::OnPlayerLeft(void* Controller) + { + UnregisterPlayer(Controller); + } + +} \ No newline at end of file diff --git a/STW/Player/STWPlayerController.h b/STW/Player/STWPlayerController.h new file mode 100644 index 0000000..9b1a78f --- /dev/null +++ b/STW/Player/STWPlayerController.h @@ -0,0 +1,190 @@ +/** + * UniversalSlashingSimulator - STW Player Controller + * + * Wrapper for AFortPlayerController with STW-specific functionality. + * Handles player input, abilities, and communication with server. + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../../Engine/UObject/UObjectWrapper.h" +#include +#include + +namespace USS +{ + // Forward declarations + class FSTWPlayerPawn; + class FInventoryManager; + + // Player state for STW + enum class EPlayerReadyState : uint8 + { + NotReady, + Ready, + InGame, + Spectating + }; + + // Player info structure + struct FSTWPlayerInfo + { + std::string PlayerName; + std::string PlayerId; // EpicID/AccountID + int32 TeamIndex; + int32 SquadSlot; + int32 PowerLevel; + int32 CommanderLevel; + EPlayerReadyState ReadyState; + bool bIsPartyLeader; + bool bIsValid; + + FSTWPlayerInfo() + : TeamIndex(0) + , SquadSlot(-1) + , PowerLevel(1) + , CommanderLevel(1) + , ReadyState(EPlayerReadyState::NotReady) + , bIsPartyLeader(false) + , bIsValid(false) + {} + }; + + // STW Player Controller wrapper + class FSTWPlayerController + { + public: + FSTWPlayerController(); + explicit FSTWPlayerController(void* InController); + ~FSTWPlayerController(); + + USS_NON_COPYABLE(FSTWPlayerController) + + // Validity + bool IsValid() const; + void* GetNative() const { return m_Controller.GetRaw(); } + + // Player info + FSTWPlayerInfo GetPlayerInfo() const; + std::string GetPlayerName() const; + std::string GetPlayerId() const; + + // Pawn management + FSTWPlayerPawn* GetPawn() const; + void SetPawn(void* InPawn); + bool HasPawn() const; + + // State + EPlayerReadyState GetReadyState() const { return m_ReadyState; } + void SetReadyState(EPlayerReadyState State); + bool IsReady() const { return m_ReadyState == EPlayerReadyState::Ready; } + bool IsInGame() const { return m_ReadyState == EPlayerReadyState::InGame; } + + // Team + int32 GetTeamIndex() const { return m_TeamIndex; } + int32 GetSquadSlot() const { return m_SquadSlot; } + void SetTeamInfo(int32 TeamIndex, int32 SquadSlot); + + // Abilities (STW heroes have abilities) + void ActivateAbility(int32 AbilityIndex); + void DeactivateAbility(int32 AbilityIndex); + bool IsAbilityReady(int32 AbilityIndex) const; + + // Gadgets + void UseGadget(int32 GadgetSlot); + bool IsGadgetReady(int32 GadgetSlot) const; + + // Building mode + void EnterBuildMode(); + void ExitBuildMode(); + bool IsInBuildMode() const { return m_bInBuildMode; } + + // Communication + void SendChatMessage(const char* Message); + void ShowNotification(const char* Message, float Duration = 3.0f); + + // Server RPCs (stubs - actual implementation calls engine functions) + void ServerAcknowledgePossession(void* Pawn); + void ServerSetReadyState(EPlayerReadyState State); + void ServerRequestRespawn(); + + // Event handling + void OnProcessEvent(void* Function, void* Params); + + private: + void UpdateFromNative(); + + UObjectWrapper m_Controller; // AFortPlayerController* + std::unique_ptr m_pPawn; + + // Cached state + EPlayerReadyState m_ReadyState; + int32 m_TeamIndex; + int32 m_SquadSlot; + bool m_bInBuildMode; + + // Cached player info + mutable FSTWPlayerInfo m_CachedInfo; + mutable bool m_bInfoDirty; + }; + + // Player controller manager - tracks all connected players + class FPlayerControllerManager + { + public: + static FPlayerControllerManager& Get(); + + USS_NON_COPYABLE(FPlayerControllerManager) + USS_NON_MOVABLE(FPlayerControllerManager) + + // Lifecycle + EResult Initialize(); + void Shutdown(); + void Update(); + + // Player management + FSTWPlayerController* RegisterPlayer(void* Controller); + void UnregisterPlayer(void* Controller); + FSTWPlayerController* GetPlayer(void* Controller) const; + FSTWPlayerController* GetPlayerById(const char* PlayerId) const; + + // Iteration + int32 GetPlayerCount() const { return static_cast(m_Players.size()); } + FSTWPlayerController* GetPlayerByIndex(int32 Index) const; + + template + void ForEachPlayer(Callback&& Func) const + { + for (const auto& Pair : m_Players) + { + if (Pair.second) + { + if (!Func(Pair.second.get())) + break; + } + } + } + + // Team queries + std::vector GetPlayersOnTeam(int32 TeamIndex) const; + int32 GetReadyPlayerCount() const; + bool AreAllPlayersReady() const; + + // Events + void OnPlayerJoined(void* Controller); + void OnPlayerLeft(void* Controller); + + private: + FPlayerControllerManager() = default; + ~FPlayerControllerManager() = default; + + std::unordered_map> m_Players; + }; + + inline FPlayerControllerManager& GetPlayerControllerManager() + { + return FPlayerControllerManager::Get(); + } + +} \ No newline at end of file diff --git a/STW/Player/STWPlayerPawn.cpp b/STW/Player/STWPlayerPawn.cpp new file mode 100644 index 0000000..4577f2f --- /dev/null +++ b/STW/Player/STWPlayerPawn.cpp @@ -0,0 +1,400 @@ +/** + * UniversalSlashingSimulator - STW Player Pawn Implementation + */ + +#include "STWPlayerPawn.h" +#include "../../Core/Logging/Log.h" + +namespace USS +{ + FSTWPlayerPawn::FSTWPlayerPawn() + : m_State(EPawnState::None) + , m_CurrentHealth(100.0f) + , m_CurrentShield(0.0f) + , m_HeroClass(EHeroClass::Soldier) + , m_PowerLevel(1) + , m_bIsSprinting(false) + , m_bIsJumping(false) + , m_bIsCrouching(false) + , m_bIsAiming(false) + , m_bIsFiring(false) + , m_DBNOTimer(0.0f) + , m_DBNOMaxTime(20.0f) + , m_LocationX(0.0f) + , m_LocationY(0.0f) + , m_LocationZ(0.0f) + , m_RotationPitch(0.0f) + , m_RotationYaw(0.0f) + , m_RotationRoll(0.0f) + { + } + + FSTWPlayerPawn::FSTWPlayerPawn(void* InPawn) + : FSTWPlayerPawn() + { + m_Pawn = UObjectWrapper(InPawn); + + if (IsValid()) + { + m_State = EPawnState::Alive; + UpdateFromNative(); + } + } + + FSTWPlayerPawn::~FSTWPlayerPawn() + { + } + + bool FSTWPlayerPawn::IsValid() const + { + return m_Pawn.IsValid(); + } + + void FSTWPlayerPawn::Update() + { + if (!IsValid()) + return; + + // Update cached values from engine + UpdateFromNative(); + + // Update ability cooldowns + float DeltaTime = 1.0f / 30.0f; // Placeholder + UpdateAbilityCooldowns(DeltaTime); + + // Update DBNO timer + if (m_State == EPawnState::DBNO) + { + m_DBNOTimer -= DeltaTime; + if (m_DBNOTimer <= 0.0f) + { + Die(); + } + } + } + + float FSTWPlayerPawn::GetHealthPercent() const + { + if (m_Stats.MaxHealth <= 0.0f) + return 0.0f; + return m_CurrentHealth / m_Stats.MaxHealth; + } + + float FSTWPlayerPawn::GetShieldPercent() const + { + if (m_Stats.MaxShield <= 0.0f) + return 0.0f; + return m_CurrentShield / m_Stats.MaxShield; + } + + void FSTWPlayerPawn::SetHealth(float Health) + { + m_CurrentHealth = Health; + + if (m_CurrentHealth < 0.0f) + m_CurrentHealth = 0.0f; + if (m_CurrentHealth > m_Stats.MaxHealth) + m_CurrentHealth = m_Stats.MaxHealth; + + UpdateState(); + } + + void FSTWPlayerPawn::SetShield(float Shield) + { + m_CurrentShield = Shield; + + if (m_CurrentShield < 0.0f) + m_CurrentShield = 0.0f; + if (m_CurrentShield > m_Stats.MaxShield) + m_CurrentShield = m_Stats.MaxShield; + } + + void FSTWPlayerPawn::ApplyDamage(float Damage, void* DamageCauser) + { + if (m_State != EPawnState::Alive) + return; + + float RemainingDamage = Damage; + + // Shield absorbs first + if (m_CurrentShield > 0.0f) + { + float ShieldDamage = (RemainingDamage < m_CurrentShield) ? RemainingDamage : m_CurrentShield; + m_CurrentShield -= ShieldDamage; + RemainingDamage -= ShieldDamage; + } + + // Then health + if (RemainingDamage > 0.0f) + { + m_CurrentHealth -= RemainingDamage; + } + + OnDamageReceived(Damage, DamageCauser); + UpdateState(); + } + + void FSTWPlayerPawn::Heal(float Amount) + { + if (m_State != EPawnState::Alive && m_State != EPawnState::DBNO) + return; + + m_CurrentHealth += Amount; + if (m_CurrentHealth > m_Stats.MaxHealth) + m_CurrentHealth = m_Stats.MaxHealth; + + USS_LOG("Healed for %.1f, health: %.1f", Amount, m_CurrentHealth); + } + + void FSTWPlayerPawn::AddShield(float Amount) + { + m_CurrentShield += Amount; + if (m_CurrentShield > m_Stats.MaxShield) + m_CurrentShield = m_Stats.MaxShield; + + USS_LOG("Added shield %.1f, total: %.1f", Amount, m_CurrentShield); + } + + void FSTWPlayerPawn::SetHeroClass(EHeroClass HeroClass) + { + m_HeroClass = HeroClass; + + // Apply default stats based on hero class + switch (HeroClass) + { + case EHeroClass::Soldier: + m_Stats.WeaponDamageMultiplier = 1.1f; + m_Stats.AbilityDamageMultiplier = 1.0f; + break; + + case EHeroClass::Constructor: + m_Stats.BuildingSpeedMultiplier = 1.2f; + m_Stats.TrapDamageMultiplier = 1.1f; + m_Stats.MaxHealth = 120.0f; + break; + + case EHeroClass::Ninja: + m_Stats.MoveSpeed = 660.0f; + m_Stats.SprintSpeed = 935.0f; + m_Stats.AbilityDamageMultiplier = 1.1f; + m_Stats.MaxHealth = 80.0f; + break; + + case EHeroClass::Outlander: + m_Stats.HarvestingEfficiency = 1.24f; + m_Stats.MoveSpeed = 620.0f; + break; + } + } + + void FSTWPlayerPawn::SetPowerLevel(int32 Level) + { + m_PowerLevel = Level; + // TODO: Scale stats based on power level + } + + void FSTWPlayerPawn::ApplyHeroStats(const FHeroStats& Stats) + { + m_Stats = Stats; + } + + const FAbilityInfo* FSTWPlayerPawn::GetAbility(int32 Index) const + { + if (Index >= 0 && Index < 3) + { + return &m_Abilities[Index]; + } + return nullptr; + } + + bool FSTWPlayerPawn::CanActivateAbility(int32 Index) const + { + if (Index < 0 || Index >= 3) + return false; + + const FAbilityInfo& Ability = m_Abilities[Index]; + return !Ability.bIsActive && !Ability.bIsOnCooldown && IsAlive(); + } + + void FSTWPlayerPawn::ActivateAbility(int32 Index) + { + if (!CanActivateAbility(Index)) + return; + + FAbilityInfo& Ability = m_Abilities[Index]; + Ability.bIsActive = true; + + USS_LOG("Activated ability %d: %s", Index, Ability.AbilityName.c_str()); + + // TODO: Actually trigger ability on engine side + } + + void FSTWPlayerPawn::DeactivateAbility(int32 Index) + { + if (Index < 0 || Index >= 3) + return; + + FAbilityInfo& Ability = m_Abilities[Index]; + + if (Ability.bIsActive) + { + Ability.bIsActive = false; + Ability.bIsOnCooldown = true; + Ability.CurrentCooldown = Ability.Cooldown; + + USS_LOG("Deactivated ability %d, cooldown: %.1fs", Index, Ability.Cooldown); + } + } + + void FSTWPlayerPawn::UpdateAbilityCooldowns(float DeltaTime) + { + for (int32 i = 0; i < 3; ++i) + { + FAbilityInfo& Ability = m_Abilities[i]; + + if (Ability.bIsOnCooldown) + { + Ability.CurrentCooldown -= DeltaTime; + + if (Ability.CurrentCooldown <= 0.0f) + { + Ability.CurrentCooldown = 0.0f; + Ability.bIsOnCooldown = false; + } + } + } + } + + void FSTWPlayerPawn::GetLocation(float& OutX, float& OutY, float& OutZ) const + { + OutX = m_LocationX; + OutY = m_LocationY; + OutZ = m_LocationZ; + } + + void FSTWPlayerPawn::GetRotation(float& OutPitch, float& OutYaw, float& OutRoll) const + { + OutPitch = m_RotationPitch; + OutYaw = m_RotationYaw; + OutRoll = m_RotationRoll; + } + + void FSTWPlayerPawn::SetLocation(float X, float Y, float Z) + { + m_LocationX = X; + m_LocationY = Y; + m_LocationZ = Z; + + // TODO: Actually set location on engine pawn + } + + void FSTWPlayerPawn::SetRotation(float Pitch, float Yaw, float Roll) + { + m_RotationPitch = Pitch; + m_RotationYaw = Yaw; + m_RotationRoll = Roll; + + // TODO: Actually set rotation on engine pawn + } + + void* FSTWPlayerPawn::GetCurrentWeapon() const + { + // TODO: Read from pawn's CurrentWeapon + return nullptr; + } + + void FSTWPlayerPawn::EquipWeapon(int32 Slot) + { + USS_LOG("Equipping weapon slot %d", Slot); + // TODO: Trigger weapon equip + } + + void FSTWPlayerPawn::EnterDBNO() + { + if (m_State != EPawnState::Alive) + return; + + m_State = EPawnState::DBNO; + m_DBNOTimer = m_DBNOMaxTime; + + USS_LOG("Player entered DBNO state"); + } + + void FSTWPlayerPawn::ReviveFromDBNO(void* Reviver) + { + if (m_State != EPawnState::DBNO) + return; + + m_State = EPawnState::Alive; + m_CurrentHealth = m_Stats.MaxHealth * 0.3f; // Revive with 30% health + + OnRevived(Reviver); + USS_LOG("Player revived from DBNO"); + } + + void FSTWPlayerPawn::Die() + { + if (m_State == EPawnState::Dead) + return; + + EPawnState OldState = m_State; + m_State = EPawnState::Dead; + m_CurrentHealth = 0.0f; + + OnDeath(nullptr); + USS_LOG("Player died (was in state: %d)", static_cast(OldState)); + } + + void FSTWPlayerPawn::OnDamageReceived(float Damage, void* DamageCauser) + { + USS_LOG("Damage received: %.1f, health: %.1f, shield: %.1f", + Damage, m_CurrentHealth, m_CurrentShield); + } + + void FSTWPlayerPawn::OnDeath(void* Killer) + { + USS_LOG("Player death event"); + } + + void FSTWPlayerPawn::OnRevived(void* Reviver) + { + USS_LOG("Player revived event"); + } + + void FSTWPlayerPawn::OnProcessEvent(void* Function, void* Params) + { + // Handle pawn-specific events + } + + void FSTWPlayerPawn::UpdateFromNative() + { + if (!IsValid()) + return; + + // TODO: Read actual values from engine pawn + // m_CurrentHealth = Read health from pawn + // m_CurrentShield = Read shield from pawn + // Read location/rotation + // Read movement state + } + + void FSTWPlayerPawn::UpdateState() + { + if (m_State == EPawnState::Dead) + return; + + if (m_CurrentHealth <= 0.0f) + { + if (m_State == EPawnState::Alive) + { + // In STW, players go DBNO first + EnterDBNO(); + } + else if (m_State == EPawnState::DBNO) + { + Die(); + } + } + } + +} \ No newline at end of file diff --git a/STW/Player/STWPlayerPawn.h b/STW/Player/STWPlayerPawn.h new file mode 100644 index 0000000..b6b2b99 --- /dev/null +++ b/STW/Player/STWPlayerPawn.h @@ -0,0 +1,209 @@ +/** + * UniversalSlashingSimulator - STW Player Pawn + * + * Wrapper for AFortPlayerPawn with STW-specific functionality. + * Handles hero abilities, stats, and player-specific game logic. + */ + +#pragma once + +#include "../../Core/Common.h" +#include "../../Engine/UObject/UObjectWrapper.h" +#include + +namespace USS +{ + // Hero class types + enum class EHeroClass : uint8 + { + Soldier, + Constructor, + Ninja, + Outlander + }; + + // Pawn state + enum class EPawnState : uint8 + { + None, + Alive, + DBNO, // Down But Not Out + Dead, + Spectating + }; + + // Hero stats structure + struct FHeroStats + { + float MaxHealth; + float MaxShield; + float HealthRegenRate; + float ShieldRegenRate; + float MoveSpeed; + float SprintSpeed; + float JumpHeight; + float AbilityDamageMultiplier; + float WeaponDamageMultiplier; + float BuildingSpeedMultiplier; + float HarvestingEfficiency; + float TrapDamageMultiplier; + + FHeroStats() + : MaxHealth(100.0f) + , MaxShield(100.0f) + , HealthRegenRate(0.0f) + , ShieldRegenRate(5.0f) + , MoveSpeed(600.0f) + , SprintSpeed(850.0f) + , JumpHeight(400.0f) + , AbilityDamageMultiplier(1.0f) + , WeaponDamageMultiplier(1.0f) + , BuildingSpeedMultiplier(1.0f) + , HarvestingEfficiency(1.0f) + , TrapDamageMultiplier(1.0f) + {} + }; + + // Hero ability info + struct FAbilityInfo + { + std::string AbilityName; + std::string AbilityClass; // UClass name + float Cooldown; + float CurrentCooldown; + int32 AbilityIndex; + bool bIsActive; + bool bIsOnCooldown; + + FAbilityInfo() + : Cooldown(0.0f) + , CurrentCooldown(0.0f) + , AbilityIndex(-1) + , bIsActive(false) + , bIsOnCooldown(false) + {} + }; + + // STW Player Pawn wrapper + class FSTWPlayerPawn + { + public: + FSTWPlayerPawn(); + explicit FSTWPlayerPawn(void* InPawn); + ~FSTWPlayerPawn(); + + USS_NON_COPYABLE(FSTWPlayerPawn) + + // Validity + bool IsValid() const; + void* GetNative() const { return m_Pawn.GetRaw(); } + + // Update + void Update(); + + // State + EPawnState GetState() const { return m_State; } + bool IsAlive() const { return m_State == EPawnState::Alive; } + bool IsDBNO() const { return m_State == EPawnState::DBNO; } + bool IsDead() const { return m_State == EPawnState::Dead; } + + // Health/Shield + float GetHealth() const { return m_CurrentHealth; } + float GetMaxHealth() const { return m_Stats.MaxHealth; } + float GetHealthPercent() const; + float GetShield() const { return m_CurrentShield; } + float GetMaxShield() const { return m_Stats.MaxShield; } + float GetShieldPercent() const; + + void SetHealth(float Health); + void SetShield(float Shield); + void ApplyDamage(float Damage, void* DamageCauser = nullptr); + void Heal(float Amount); + void AddShield(float Amount); + + // Hero info + EHeroClass GetHeroClass() const { return m_HeroClass; } + const std::string& GetHeroName() const { return m_HeroName; } + const FHeroStats& GetStats() const { return m_Stats; } + int32 GetPowerLevel() const { return m_PowerLevel; } + + void SetHeroClass(EHeroClass HeroClass); + void SetPowerLevel(int32 Level); + void ApplyHeroStats(const FHeroStats& Stats); + + // Abilities + int32 GetAbilityCount() const { return 3; } // STW heroes have 3 abilities + const FAbilityInfo* GetAbility(int32 Index) const; + bool CanActivateAbility(int32 Index) const; + void ActivateAbility(int32 Index); + void DeactivateAbility(int32 Index); + void UpdateAbilityCooldowns(float DeltaTime); + + // Position/Movement + void GetLocation(float& OutX, float& OutY, float& OutZ) const; + void GetRotation(float& OutPitch, float& OutYaw, float& OutRoll) const; + void SetLocation(float X, float Y, float Z); + void SetRotation(float Pitch, float Yaw, float Roll); + + bool IsSprinting() const { return m_bIsSprinting; } + bool IsJumping() const { return m_bIsJumping; } + bool IsCrouching() const { return m_bIsCrouching; } + + // Combat + void* GetCurrentWeapon() const; + void EquipWeapon(int32 Slot); + bool IsAiming() const { return m_bIsAiming; } + bool IsFiring() const { return m_bIsFiring; } + + // DBNO (Down But Not Out) system + float GetDBNOTimer() const { return m_DBNOTimer; } + float GetDBNOMaxTime() const { return m_DBNOMaxTime; } + void EnterDBNO(); + void ReviveFromDBNO(void* Reviver = nullptr); + void Die(); + + // Events + void OnDamageReceived(float Damage, void* DamageCauser); + void OnDeath(void* Killer); + void OnRevived(void* Reviver); + void OnProcessEvent(void* Function, void* Params); + + private: + void UpdateFromNative(); + void UpdateState(); + + UObjectWrapper m_Pawn; // AFortPlayerPawn* + + // State + EPawnState m_State; + float m_CurrentHealth; + float m_CurrentShield; + + // Hero info + EHeroClass m_HeroClass; + std::string m_HeroName; + FHeroStats m_Stats; + int32 m_PowerLevel; + + // Abilities + FAbilityInfo m_Abilities[3]; + + // Movement state + bool m_bIsSprinting; + bool m_bIsJumping; + bool m_bIsCrouching; + + // Combat state + bool m_bIsAiming; + bool m_bIsFiring; + + // DBNO + float m_DBNOTimer; + float m_DBNOMaxTime; + + // Cached position (updated each frame) + float m_LocationX, m_LocationY, m_LocationZ; + float m_RotationPitch, m_RotationYaw, m_RotationRoll; + }; + +} \ No newline at end of file diff --git a/UniversalSlashingSimulator.sln b/UniversalSlashingSimulator.sln new file mode 100644 index 0000000..3a99417 --- /dev/null +++ b/UniversalSlashingSimulator.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.0.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UniversalSlashingSimulator", "UniversalSlashingSimulator.vcxproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {12345678-9ABC-DEF0-1234-567890ABCDEF} + EndGlobalSection +EndGlobal diff --git a/UniversalSlashingSimulator.vcxproj b/UniversalSlashingSimulator.vcxproj new file mode 100644 index 0000000..3f92ffe --- /dev/null +++ b/UniversalSlashingSimulator.vcxproj @@ -0,0 +1,168 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + UniversalSlashingSimulator + 10.0 + UniversalSlashingSimulator + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + + $(SolutionDir)bin\$(Configuration)\ + $(SolutionDir)obj\$(Configuration)\ + USS + .dll + + + $(SolutionDir)bin\$(Configuration)\ + $(SolutionDir)obj\$(Configuration)\ + USS + .dll + + + + + Level4 + true + WIN32_LEAN_AND_MEAN;NOMINMAX;_CRT_SECURE_NO_WARNINGS;USS_DEBUG;_DEBUG;_WINDOWS;_USRDLL;USS_EXPORTS;%(PreprocessorDefinitions) + true + stdcpp17 + $(ProjectDir);$(ProjectDir)external\minhook\include;$(ProjectDir)external\memcury\include;%(AdditionalIncludeDirectories) + true + MultiThreadedDebugDLL + ProgramDatabase + Disabled + + + Windows + true + psapi.lib;%(AdditionalDependencies) + $(ProjectDir)external\minhook\lib;%(AdditionalLibraryDirectories) + + + + + + Level4 + true + true + true + WIN32_LEAN_AND_MEAN;NOMINMAX;_CRT_SECURE_NO_WARNINGS;NDEBUG;_WINDOWS;_USRDLL;USS_EXPORTS;%(PreprocessorDefinitions) + true + stdcpp17 + $(ProjectDir);$(ProjectDir)external\minhook\include;$(ProjectDir)external\memcury\include;%(AdditionalIncludeDirectories) + true + MultiThreadedDLL + MaxSpeed + AnySuitable + true + + + Windows + true + true + true + psapi.lib;%(AdditionalDependencies) + $(ProjectDir)external\minhook\lib;%(AdditionalLibraryDirectories) + UseLinkTimeCodeGeneration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UniversalSlashingSimulator.vcxproj.filters b/UniversalSlashingSimulator.vcxproj.filters new file mode 100644 index 0000000..cc95aba --- /dev/null +++ b/UniversalSlashingSimulator.vcxproj.filters @@ -0,0 +1,212 @@ + + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + + + {A3C2E5D1-9B8F-4C7E-B6A5-D4F3E2C1B0A9} + + + {B4D3C2E1-0A9F-5D8E-C7B6-E5F4D3C2B1A0} + + + {C5E4D3F2-1B0A-6E9F-D8C7-F6E5D4C3B2A1} + + + {D6F5E4G3-2C1B-7F0A-E9D8-G7F6E5D4C3B2} + + + {E7G6F5H4-3D2C-8G1B-F0E9-H8G7F6E5D4C3} + + + {F8H7G6I5-4E3D-9H2C-G1F0-I9H8G7F6E5D4} + + + {A9I8H7J6-5F4E-0I3D-H2G1-J0I9H8G7F6E5} + + + {B0J9I8K7-6G5F-1J4E-I3H2-K1J0I9H8G7F6} + + + {C1K0J9L8-7H6G-2K5F-J4I3-L2K1J0I9H8G7} + + + {D2L1K0M9-8I7H-3L6G-K5J4-M3L2K1J0I9H8} + + + {E3M2L1N0-9J8I-4M7H-L6K5-N4M3L2K1J0I9} + + + {F4N3M2O1-0K9J-5N8I-M7L6-O5N4M3L2K1J0} + + + {G5O4N3P2-1L0K-6O9J-N8M7-P6O5N4M3L2K1} + + + {H6P5O4Q3-2M1L-7P0K-O9N8-Q7P6O5N4M3L2} + + + {I7Q6P5R4-3N2M-8Q1L-P0O9-R8Q7P6O5N4M3} + + + + + + + + Core\Logging + + + Core\Memory + + + Core\Versioning + + + + + Engine\CoreTypes + + + Engine\CoreTypes + + + Engine\CoreTypes + + + Engine\UObject + + + Engine\Reflection + + + Engine\Replication + + + Engine\Events + + + Engine + + + + + STW\GameMode + + + STW\Missions + + + STW\Missions + + + STW\Player + + + STW\Player + + + STW\Inventory + + + STW\Building + + + + + Entry + + + + + + + + Core + + + Core\Logging + + + Core\Memory + + + Core\Memory + + + Core\Versioning + + + Core\Versioning + + + Core\Hooks + + + + + Engine\CoreTypes + + + Engine\CoreTypes + + + Engine\CoreTypes + + + Engine\UObject + + + Engine\Reflection + + + Engine\Replication + + + Engine\Events + + + Engine + + + + + STW\GameMode + + + STW\Missions + + + STW\Missions + + + STW\Missions + + + STW\Player + + + STW\Player + + + STW\Inventory + + + STW\Inventory + + + STW\Building + + + STW\Building + + + diff --git a/UniversalSlashingSimulator.vcxproj.user b/UniversalSlashingSimulator.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/UniversalSlashingSimulator.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/external/memcury/memcury.h b/external/memcury/memcury.h new file mode 100644 index 0000000..347fd7b --- /dev/null +++ b/external/memcury/memcury.h @@ -0,0 +1,1217 @@ +#pragma once + +/* + Memcury is a single-header file library for memory manipulation in C++. + + Containers: + -PE::Address: A pointer container. + -PE::Section: Portable executable section container for internal usage. + + Modules: + -Scanner: + -Constructors: + -Default: Takes a pointer to start the scanning from. + -FindPattern: Finds a pattern in memory. + -FindStringRef: Finds a string reference in memory, supports all types of strings. + -Functions: + -SetTargetModule: Sets the target module for the scanner. + -ScanFor: Scans for a byte(s) near the current address. + -FindFunctionBoundary: Finds the boundary of a function near the current address. + -RelativeOffset: Gets the relative offset of the current address. + -AbsoluteOffset: Gets the absolute offset of the current address. + -GetAs: Gets the current address as a type. + -Get: Gets the current address as an int64. + + -TrampolineHook: + -Constructors: + -Default: Takes a pointer pointer to the target function and a pointer to the hook function. + -Functions: + -Commit: Commits the hook. + -Revert: Reverts the hook. + -Toggle: Toggles the hook on\off. + + -VEHHook: + -Functions: + -Init: Initializes the VEH Hook system. + -AddHook: Adds a hook to the VEH Hook system. + -RemoveHook: Removes a hook from the VEH Hook system. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#pragma comment(lib, "Dbghelp.lib") + +#undef min +#undef max + +#define MemcuryAssert(cond) \ + if (!(cond)) \ + { \ + MessageBoxA(nullptr, #cond, __FUNCTION__, MB_ICONERROR | MB_OK); \ + Memcury::Safety::FreezeCurrentThread(); \ + } + +#define MemcuryAssertM(cond, msg) \ + if (!(cond)) \ + { \ + MessageBoxA(nullptr, msg, __FUNCTION__, MB_ICONERROR | MB_OK); \ + Memcury::Safety::FreezeCurrentThread(); \ + } + +#define MemcuryThrow(msg) \ + MessageBoxA(nullptr, msg, __FUNCTION__, MB_ICONERROR | MB_OK); \ + Memcury::Safety::FreezeCurrentThread(); + +namespace Memcury +{ + extern "C" IMAGE_DOS_HEADER __ImageBase; + + inline auto GetCurrentModule() -> HMODULE + { + return reinterpret_cast(&__ImageBase); + } + + namespace Util + { + template + constexpr static auto IsInRange(T value, T min, T max) -> bool + { + return value >= min && value < max; + } + + constexpr auto StrHash(const char* str, int h = 0) -> unsigned int + { + return !str[h] ? 5381 : (StrHash(str, h + 1) * 33) ^ str[h]; + } + + inline auto IsSamePage(void* A, void* B) -> bool + { + MEMORY_BASIC_INFORMATION InfoA; + if (!VirtualQuery(A, &InfoA, sizeof(InfoA))) + { + return true; + } + + MEMORY_BASIC_INFORMATION InfoB; + if (!VirtualQuery(B, &InfoB, sizeof(InfoB))) + { + return true; + } + + return InfoA.BaseAddress == InfoB.BaseAddress; + } + + inline auto GetModuleStartAndEnd() -> std::pair + { + auto HModule = GetCurrentModule(); + auto NTHeaders = reinterpret_cast((uintptr_t)HModule + reinterpret_cast((uintptr_t)HModule)->e_lfanew); + + uintptr_t dllStart = (uintptr_t)HModule; + uintptr_t dllEnd = (uintptr_t)HModule + NTHeaders->OptionalHeader.SizeOfImage; + + return { dllStart, dllEnd }; + } + + inline auto CopyToClipboard(std::string str) + { + auto mem = GlobalAlloc(GMEM_FIXED, str.size() + 1); + memcpy(mem, str.c_str(), str.size() + 1); + + OpenClipboard(nullptr); + EmptyClipboard(); + SetClipboardData(CF_TEXT, mem); + CloseClipboard(); + + GlobalFree(mem); + } + } + + namespace Safety + { + enum class ExceptionMode + { + None, + CatchDllExceptionsOnly, + CatchAllExceptions + }; + + static auto FreezeCurrentThread() -> void + { + SuspendThread(GetCurrentThread()); + } + + static auto PrintStack(CONTEXT* ctx) -> void + { + STACKFRAME64 stack; + memset(&stack, 0, sizeof(STACKFRAME64)); + + auto process = GetCurrentProcess(); + auto thread = GetCurrentThread(); + + SymInitialize(process, NULL, TRUE); + + bool result; + DWORD64 displacement = 0; + + char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)] { 0 }; + char name[256] { 0 }; + char module[256] { 0 }; + + PSYMBOL_INFO symbolInfo = (PSYMBOL_INFO)buffer; + + for (ULONG frame = 0;; frame++) + { + result = StackWalk64( + IMAGE_FILE_MACHINE_AMD64, + process, + thread, + &stack, + ctx, + NULL, + SymFunctionTableAccess64, + SymGetModuleBase64, + NULL); + + if (!result) + break; + + symbolInfo->SizeOfStruct = sizeof(SYMBOL_INFO); + symbolInfo->MaxNameLen = MAX_SYM_NAME; + SymFromAddr(process, (ULONG64)stack.AddrPC.Offset, &displacement, symbolInfo); + + HMODULE hModule = NULL; + lstrcpyA(module, ""); + GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (const wchar_t*)(stack.AddrPC.Offset), &hModule); + + if (hModule != NULL) + GetModuleFileNameA(hModule, module, 256); + + printf("[%lu] Name: %s - Address: %p - Module: %s\n", frame, symbolInfo->Name, (void*)symbolInfo->Address, module); + } + } + + template + auto MemcuryGlobalHandler(EXCEPTION_POINTERS* ExceptionInfo) -> long + { + auto [dllStart, dllEnd] = Util::GetModuleStartAndEnd(); + + if constexpr (mode == ExceptionMode::CatchDllExceptionsOnly) + { + if (!Util::IsInRange(ExceptionInfo->ContextRecord->Rip, dllStart, dllEnd)) + { + return EXCEPTION_CONTINUE_SEARCH; + } + } + + auto message = std::format("Memcury caught an exception at [{:x}]\nPress Yes if you want the address to be copied to your clipboard", ExceptionInfo->ContextRecord->Rip); + if (MessageBoxA(nullptr, message.c_str(), "Error", MB_ICONERROR | MB_YESNO) == IDYES) + { + std::string clip = std::format("{:x}", ExceptionInfo->ContextRecord->Rip); + Util::CopyToClipboard(clip); + } + + PrintStack(ExceptionInfo->ContextRecord); + + FreezeCurrentThread(); + + return EXCEPTION_EXECUTE_HANDLER; + } + + template + static auto SetExceptionMode() -> void + { + SetUnhandledExceptionFilter(MemcuryGlobalHandler); + } + } + + namespace Globals + { + constexpr const bool bLogging = true; + + inline const char* moduleName = nullptr; + } + + namespace ASM + { + //@todo: this whole namespace needs a rework, should somehow make this more modern and less ugly. + enum MNEMONIC : uint8_t + { + JMP_REL8 = 0xEB, + JMP_REL32 = 0xE9, + JMP_EAX = 0xE0, + CALL = 0xE8, + LEA = 0x8D, + CDQ = 0x99, + CMOVL = 0x4C, + CMOVS = 0x48, + CMOVNS = 0x49, + NOP = 0x90, + INT3 = 0xCC, + RETN_REL8 = 0xC2, + RETN = 0xC3, + NONE = 0x00 + }; + + constexpr int SIZE_OF_JMP_RELATIVE_INSTRUCTION = 5; + constexpr int SIZE_OF_JMP_ABSLOUTE_INSTRUCTION = 13; + + constexpr auto MnemonicToString(MNEMONIC e) -> const char* + { + switch (e) + { + case JMP_REL8: + return "JMP_REL8"; + case JMP_REL32: + return "JMP_REL32"; + case JMP_EAX: + return "JMP_EAX"; + case CALL: + return "CALL"; + case LEA: + return "LEA"; + case CDQ: + return "CDQ"; + case CMOVL: + return "CMOVL"; + case CMOVS: + return "CMOVS"; + case CMOVNS: + return "CMOVNS"; + case NOP: + return "NOP"; + case INT3: + return "INT3"; + case RETN_REL8: + return "RETN_REL8"; + case RETN: + return "RETN"; + case NONE: + return "NONE"; + default: + return "UNKNOWN"; + } + } + + constexpr auto Mnemonic(const char* s) -> MNEMONIC + { + switch (Util::StrHash(s)) + { + case Util::StrHash("JMP_REL8"): + return JMP_REL8; + case Util::StrHash("JMP_REL32"): + return JMP_REL32; + case Util::StrHash("JMP_EAX"): + return JMP_EAX; + case Util::StrHash("CALL"): + return CALL; + case Util::StrHash("LEA"): + return LEA; + case Util::StrHash("CDQ"): + return CDQ; + case Util::StrHash("CMOVL"): + return CMOVL; + case Util::StrHash("CMOVS"): + return CMOVS; + case Util::StrHash("CMOVNS"): + return CMOVNS; + case Util::StrHash("NOP"): + return NOP; + case Util::StrHash("INT3"): + return INT3; + case Util::StrHash("RETN_REL8"): + return RETN_REL8; + case Util::StrHash("RETN"): + return RETN; + default: + return NONE; + } + } + + inline auto byteIsA(uint8_t byte, MNEMONIC opcode) -> bool + { + return byte == opcode; + } + + inline auto byteIsAscii(uint8_t byte) -> bool + { + static constexpr bool isAscii[0x100] = { + false, false, false, false, false, false, false, false, false, true, true, false, false, true, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false + }; + + return isAscii[byte]; + } + + inline bool isJump(uint8_t byte) + { + return byte >= 0x70 && byte <= 0x7F; + } + + static auto pattern2bytes(const char* pattern) -> std::vector + { + auto bytes = std::vector {}; + const auto start = const_cast(pattern); + const auto end = const_cast(pattern) + strlen(pattern); + + for (auto current = start; current < end; ++current) + { + if (*current == '?') + { + ++current; + if (*current == '?') + ++current; + bytes.push_back(-1); + } + else + { + bytes.push_back(strtoul(current, ¤t, 16)); + } + } + return bytes; + } + } + + namespace PE + { + inline auto SetCurrentModule(const char* moduleName) -> void + { + Globals::moduleName = moduleName; + } + + inline auto GetModuleBase() -> uintptr_t + { + return reinterpret_cast(GetModuleHandleA(Globals::moduleName)); + } + + inline auto GetDOSHeader() -> PIMAGE_DOS_HEADER + { + return reinterpret_cast(GetModuleBase()); + } + + inline auto GetNTHeaders() -> PIMAGE_NT_HEADERS + { + return reinterpret_cast(GetModuleBase() + GetDOSHeader()->e_lfanew); + } + + class Address + { + uintptr_t _address; + + public: + Address() + { + _address = 0; + } + + Address(uintptr_t address) + : _address(address) + { + } + + Address(void* address) + : _address(reinterpret_cast(address)) + { + } + + auto operator=(uintptr_t address) -> Address + { + _address = address; + return *this; + } + + auto operator=(void* address) -> Address + { + _address = reinterpret_cast(address); + return *this; + } + + auto operator+(uintptr_t offset) -> Address + { + return Address(_address + offset); + } + + bool operator>(uintptr_t offset) + { + return _address > offset; + } + + bool operator>(Address address) + { + return _address > address._address; + } + + bool operator<(uintptr_t offset) + { + return _address < offset; + } + + bool operator<(Address address) + { + return _address < address._address; + } + + bool operator>=(uintptr_t offset) + { + return _address >= offset; + } + + bool operator>=(Address address) + { + return _address >= address._address; + } + + bool operator<=(uintptr_t offset) + { + return _address <= offset; + } + + bool operator<=(Address address) + { + return _address <= address._address; + } + + bool operator==(uintptr_t offset) + { + return _address == offset; + } + + bool operator==(Address address) + { + return _address == address._address; + } + + bool operator!=(uintptr_t offset) + { + return _address != offset; + } + + bool operator!=(Address address) + { + return _address != address._address; + } + + auto RelativeOffset(uint32_t offset) -> Address + { + _address = ((_address + offset + 4) + *(int32_t*)(_address + offset)); + return *this; + } + + auto AbsoluteOffset(uint32_t offset) -> Address + { + _address = _address + offset; + return *this; + } + + auto Jump() -> Address + { + if (ASM::isJump(*reinterpret_cast(_address))) + { + UINT8 toSkip = *reinterpret_cast(_address + 1); + _address = _address + 2 + toSkip; + } + + return *this; + } + + auto Get() -> uintptr_t + { + return _address; + } + + template + auto GetAs() -> T + { + return reinterpret_cast(_address); + } + + auto IsValid() -> bool + { + return _address != 0; + } + }; + + class Section + { + public: + std::string sectionName; + IMAGE_SECTION_HEADER rawSection; + + static auto GetAllSections() -> std::vector
+ { + std::vector
sections; + + auto sectionsSize = GetNTHeaders()->FileHeader.NumberOfSections; + auto section = IMAGE_FIRST_SECTION(GetNTHeaders()); + + for (WORD i = 0; i < sectionsSize; i++, section++) + { + auto secName = std::string((char*)section->Name); + + sections.push_back({ secName, *section }); + } + + return sections; + } + + static auto GetSection(std::string sectionName) -> Section + { + for (auto& section : GetAllSections()) + { + if (section.sectionName == sectionName) + { + return section; + } + } + + MemcuryThrow("Section not found"); + return Section {}; + } + + auto GetSectionSize() -> uint32_t + { + return rawSection.Misc.VirtualSize; + } + + auto GetSectionStart() -> Address + { + return Address(GetModuleBase() + rawSection.VirtualAddress); + } + + auto GetSectionEnd() -> Address + { + return Address(GetSectionStart() + GetSectionSize()); + } + + auto isInSection(Address address) -> bool + { + return address >= GetSectionStart() && address < GetSectionEnd(); + } + }; + } + + class Scanner + { + PE::Address _address; + + public: + Scanner(PE::Address address) + : _address(address) + { + } + + static auto SetTargetModule(const char* moduleName) -> void + { + PE::SetCurrentModule(moduleName); + } + + static auto FindPatternEx(HANDLE handle, const char* pattern, const char* mask, uint64_t begin, uint64_t end) -> Scanner + { + auto scan = [](const char* pattern, const char* mask, char* begin, unsigned int size) -> char* + { + size_t patternLen = strlen(mask); + for (unsigned int i = 0; i < size - patternLen; i++) + { + bool found = true; + for (unsigned int j = 0; j < patternLen; j++) + { + if (mask[j] != '?' && pattern[j] != *(begin + i + j)) + { + found = false; + break; + } + } + + if (found) + return (begin + i); + } + return nullptr; + }; + + uint64_t match = NULL; + SIZE_T bytesRead; + char* buffer = nullptr; + MEMORY_BASIC_INFORMATION mbi = { 0 }; + + uint64_t curr = begin; + + for (uint64_t curr = begin; curr < end; curr += mbi.RegionSize) + { + if (!VirtualQueryEx(handle, (void*)curr, &mbi, sizeof(mbi))) + continue; + + if (mbi.State != MEM_COMMIT || mbi.Protect == PAGE_NOACCESS) + continue; + + buffer = new char[mbi.RegionSize]; + + if (ReadProcessMemory(handle, mbi.BaseAddress, buffer, mbi.RegionSize, &bytesRead)) + { + char* internalAddr = scan(pattern, mask, buffer, (unsigned int)bytesRead); + + if (internalAddr != nullptr) + { + match = curr + (uint64_t)(internalAddr - buffer); + break; + } + } + } + delete[] buffer; + + MemcuryAssertM(match != 0, "FindPatternEx return nullptr"); + + return Scanner(match); + } + + static auto FindPatternEx(HANDLE handle, const char* sig) -> Scanner + { + char pattern[100]; + char mask[100]; + + char lastChar = ' '; + unsigned int j = 0; + + for (unsigned int i = 0; i < strlen(sig); i++) + { + if ((sig[i] == '?' || sig[i] == '*') && (lastChar != '?' && lastChar != '*')) + { + pattern[j] = mask[j] = '?'; + j++; + } + + else if (isspace(lastChar)) + { + pattern[j] = lastChar = (char)strtol(&sig[i], 0, 16); + mask[j] = 'x'; + j++; + } + lastChar = sig[i]; + } + pattern[j] = mask[j] = '\0'; + + auto module = (uint64_t)GetModuleHandle(nullptr); + + return FindPatternEx(handle, pattern, mask, module, module + Memcury::PE::GetNTHeaders()->OptionalHeader.SizeOfImage); + } + + static auto FindPattern(const char* signature) -> Scanner + { + PE::Address add { nullptr }; + + const auto sizeOfImage = PE::GetNTHeaders()->OptionalHeader.SizeOfImage; + auto patternBytes = ASM::pattern2bytes(signature); + const auto scanBytes = reinterpret_cast(PE::GetModuleBase()); + + const auto s = patternBytes.size(); + const auto d = patternBytes.data(); + + for (auto i = 0ul; i < sizeOfImage - s; ++i) + { + bool found = true; + for (auto j = 0ul; j < s; ++j) + { + if (scanBytes[i + j] != d[j] && d[j] != -1) + { + found = false; + break; + } + } + + if (found) + { + add = reinterpret_cast(&scanBytes[i]); + break; + } + } + + MemcuryAssertM(add != 0, "FindPattern return nullptr"); + + return Scanner(add); + } + + // Supports wide and normal strings both std and pointers + template + static auto FindStringRef(T string, bool find_first = false) -> Scanner + { + PE::Address add { nullptr }; + + constexpr auto bIsWide = std::is_same::value; + constexpr auto bIsChar = std::is_same::value; + + constexpr auto bIsPtr = bIsWide || bIsChar; + + auto textSection = PE::Section::GetSection(".text"); + auto rdataSection = PE::Section::GetSection(".rdata"); + + const auto scanBytes = reinterpret_cast(textSection.GetSectionStart().Get()); + + // scan only text section + for (DWORD i = 0x0; i < textSection.GetSectionSize(); i++) + { + if ((scanBytes[i] == ASM::CMOVL || scanBytes[i] == ASM::CMOVS) && scanBytes[i + 1] == ASM::LEA) + { + auto stringAdd = PE::Address(&scanBytes[i]).RelativeOffset(3); + + // Check if the string is in the .rdata section + if (rdataSection.isInSection(stringAdd)) + { + auto strBytes = stringAdd.GetAs(); + + // Check if the first char is printable + if (ASM::byteIsAscii(strBytes[0])) + { + if constexpr (!bIsPtr) + { + typedef T::value_type char_type; + + auto lea = stringAdd.GetAs(); + + T leaT(lea); + + if (leaT == string) + { + add = PE::Address(&scanBytes[i]); + if (find_first) + break; + } + } + else + { + auto lea = stringAdd.GetAs(); + + if constexpr (bIsWide) + { + if (wcscmp(string, lea) == 0) + { + add = PE::Address(&scanBytes[i]); + if (find_first) + break; + } + } + else + { + if (strcmp(string, lea) == 0) + { + add = PE::Address(&scanBytes[i]); + if (find_first) + break; + } + } + } + } + } + } + } + + MemcuryAssertM(add != 0, "FindStringRef return nullptr"); + + return Scanner(add); + } + + auto Jump() -> Scanner + { + _address.Jump(); + return *this; + } + + auto ScanFor(std::vector opcodesToFind, bool forward = true, int toSkip = 0) -> Scanner + { + const auto scanBytes = _address.GetAs(); + + for (auto i = (forward ? 1 : -1); forward ? (i < 2048) : (i > -2048); forward ? i++ : i--) + { + bool found = true; + + for (int k = 0; k < opcodesToFind.size() && found; k++) + { + if (opcodesToFind[k] == -1) + continue; + found = opcodesToFind[k] == scanBytes[i + k]; + } + + if (found) + { + _address = &scanBytes[i]; + if (toSkip != 0) + { + return ScanFor(opcodesToFind, forward, toSkip - 1); + } + + break; + } + } + + return *this; + } + + auto FindFunctionBoundary(bool forward = false) -> Scanner + { + const auto scanBytes = _address.GetAs(); + + for (auto i = (forward ? 1 : -1); forward ? (i < 2048) : (i > -2048); forward ? i++ : i--) + { + auto& byte = scanBytes[i]; + auto& next = forward ? scanBytes[i + 1] : scanBytes[i - 1]; + + if ( // ASM::byteIsA(scanBytes[i], ASM::MNEMONIC::JMP_REL8) || + // ASM::byteIsA(scanBytes[i], ASM::MNEMONIC::JMP_REL32) || + // ASM::byteIsA(scanBytes[i], ASM::MNEMONIC::JMP_EAX) || + // ASM::byteIsA(scanBytes[i], ASM::MNEMONIC::RETN_REL8) || + (ASM::byteIsA(byte, ASM::MNEMONIC::RETN) && ASM::byteIsA(next, ASM::MNEMONIC::INT3)) + || (ASM::byteIsA(byte, ASM::MNEMONIC::INT3) && ASM::byteIsA(next, ASM::MNEMONIC::INT3))) + { + _address = (uintptr_t)&scanBytes[i + 1]; + break; + } + } + + return *this; + } + + auto RelativeOffset(uint32_t offset) -> Scanner + { + _address.RelativeOffset(offset); + + return *this; + } + + auto AbsoluteOffset(uint32_t offset) -> Scanner + { + _address.AbsoluteOffset(offset); + + return *this; + } + + template + auto GetAs() -> T + { + return _address.GetAs(); + } + + auto Get() -> uintptr_t + { + return _address.Get(); + } + + auto IsValid() -> bool + { + return _address.IsValid(); + } + }; + + /* Bad don't use it tbh... */ + class TrampolineHook + { + void** originalFunctionPtr; + PE::Address originalFunction; + PE::Address hookFunction; + PE::Address allocatedPage; + std::vector restore; + + void PointToCodeIfNot(PE::Address& ptr) + { + auto bytes = ptr.GetAs(); + + if (ASM::byteIsA(bytes[0], ASM::MNEMONIC::JMP_REL32)) + { + ptr = bytes + 5 + *(int32_t*)&bytes[1]; + } + } + + void* AllocatePageNearAddress(void* targetAddr) + { + SYSTEM_INFO sysInfo; + GetSystemInfo(&sysInfo); + const uint64_t PAGE_SIZE = sysInfo.dwPageSize; + + uint64_t startAddr = (uint64_t(targetAddr) & ~(PAGE_SIZE - 1)); // round down to nearest page boundary + uint64_t minAddr = std::min(startAddr - 0x7FFFFF00, (uint64_t)sysInfo.lpMinimumApplicationAddress); + uint64_t maxAddr = std::max(startAddr + 0x7FFFFF00, (uint64_t)sysInfo.lpMaximumApplicationAddress); + + uint64_t startPage = (startAddr - (startAddr % PAGE_SIZE)); + + for (uint64_t pageOffset = 1; pageOffset; pageOffset++) + { + uint64_t byteOffset = pageOffset * PAGE_SIZE; + uint64_t highAddr = startPage + byteOffset; + uint64_t lowAddr = (startPage > byteOffset) ? startPage - byteOffset : 0; + + bool needsExit = highAddr > maxAddr && lowAddr < minAddr; + + if (highAddr < maxAddr) + { + void* outAddr = VirtualAlloc((void*)highAddr, PAGE_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (outAddr) + return outAddr; + } + + if (lowAddr > minAddr) + { + void* outAddr = VirtualAlloc((void*)lowAddr, PAGE_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (outAddr != nullptr) + return outAddr; + } + + if (needsExit) + { + break; + } + } + + return nullptr; + } + + void WriteAbsoluteJump(void* jumpLocation, void* destination) + { + uint8_t absJumpInstructions[] = { + ASM::Mnemonic("CMOVNS"), 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, addr + 0x41, 0xFF, 0xE2 // jmp r10 + }; + + auto destination64 = (uint64_t)destination; + memcpy(&absJumpInstructions[2], &destination64, sizeof(destination64)); + memcpy(jumpLocation, absJumpInstructions, sizeof(absJumpInstructions)); + } + + uintptr_t PrepareRestore() + { + /* + This is not a correct way to do it at all, since not all functions sub from the stack + This needs so much more tests, but it works for now. + */ + + Scanner scanner(originalFunction); + scanner.ScanFor({ 0x48, 0x83, 0xEC }); // sub rsp + + auto restoreSize = scanner.Get() - originalFunction.Get(); + + MemcuryAssert(restoreSize > 0 && restoreSize < 0x100); + + restore.reserve(restoreSize); + for (auto i = 0; i < restoreSize; i++) + { + restore.push_back(originalFunction.GetAs()[i]); + } + + return restoreSize; + } + + void WriteRestore() + { + auto restorePtr = allocatedPage + ASM::SIZE_OF_JMP_ABSLOUTE_INSTRUCTION + 2; + + memcpy(restorePtr.GetAs(), restore.data(), restore.size()); + + *originalFunctionPtr = restorePtr.GetAs(); + + // Write a jump back to where the execution should resume + restorePtr.AbsoluteOffset((uint32_t)restore.size()); + + auto contuineExecution = originalFunction + restore.size(); + + WriteAbsoluteJump(restorePtr.GetAs(), contuineExecution.GetAs()); + } + + auto PrepareJMPInstruction(uint64_t dst) + { + uint8_t bytes[5] = { ASM::Mnemonic("JMP_REL32"), 0x0, 0x0, 0x0, 0x0 }; + + const uint64_t relAddr = dst - (originalFunction.Get() + ASM::SIZE_OF_JMP_RELATIVE_INSTRUCTION); + memcpy(bytes + 1, &relAddr, 4); + + return std::move(bytes); + } + + bool IsHooked() + { + return originalFunction.GetAs()[0] == ASM::Mnemonic("JMP_REL32"); + } + + public: + TrampolineHook(void** originalFunction, void* hookFunction) + { + this->originalFunctionPtr = originalFunction; + + this->originalFunction = *originalFunction; + this->hookFunction = hookFunction; + + PointToCodeIfNot(this->originalFunction); + PointToCodeIfNot(this->hookFunction); + }; + + bool Commit() + { + auto fnStart = originalFunction.GetAs(); + + auto restoreSize = PrepareRestore(); + + if (!allocatedPage.IsValid()) + { + allocatedPage = AllocatePageNearAddress(fnStart); + } + + memset(allocatedPage.GetAs(), ASM::MNEMONIC::INT3, 0x1000); + + WriteAbsoluteJump(allocatedPage.GetAs(), hookFunction.GetAs()); + + DWORD oldProtect; + VirtualProtect(fnStart, 1024, PAGE_EXECUTE_READWRITE, &oldProtect); + + auto jmpInstruction = PrepareJMPInstruction(allocatedPage.Get()); + + WriteRestore(); + + memset(fnStart, ASM::MNEMONIC::INT3, restoreSize); + memcpy(fnStart, jmpInstruction, ASM::SIZE_OF_JMP_RELATIVE_INSTRUCTION); + + return true; + } + + bool Revert() + { + auto fnStart = originalFunction.GetAs(); + + DWORD oldProtect; + VirtualProtect(fnStart, 1024, PAGE_EXECUTE_READWRITE, &oldProtect); + + memcpy(fnStart, restore.data(), restore.size()); + + *originalFunctionPtr = originalFunction.GetAs(); + + // VirtualFree(allocatedPage.GetAs(), 0x1000, MEM_RELEASE); + + return true; + } + + auto Toggle() + { + if (IsHooked()) + Revert(); + else + Commit(); + + return IsHooked(); + } + }; + + namespace VEHHook + { + struct HOOK_INFO + { + void* Original; + void* Detour; + + HOOK_INFO(void* Original, void* Detour) + : Original(Original) + , Detour(Detour) + { + } + }; + + inline std::vector Hooks; + inline std::vector HookProtections; + inline HANDLE ExceptionHandler; + + inline long Handler(EXCEPTION_POINTERS* Exception) + { + if (Exception->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) + { + auto Itr = std::find_if(Hooks.begin(), Hooks.end(), [Rip = Exception->ContextRecord->Rip](const HOOK_INFO& Hook) + { return Hook.Original == (void*)Rip; }); + if (Itr != Hooks.end()) + { + Exception->ContextRecord->Rip = (uintptr_t)Itr->Detour; + } + + Exception->ContextRecord->EFlags |= 0x100; // SINGLE_STEP_FLAG + + return EXCEPTION_CONTINUE_EXECUTION; + } + else if (Exception->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) + { + // TODO: find a way to only vp the function that about to get executed + for (auto& Hook : Hooks) + { + DWORD dwOldProtect; + VirtualProtect(Hook.Original, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &dwOldProtect); + } + + return EXCEPTION_CONTINUE_EXECUTION; + } + + return EXCEPTION_CONTINUE_SEARCH; + } + + inline bool Init() + { + if (ExceptionHandler == nullptr) + { + ExceptionHandler = AddVectoredExceptionHandler(true, (PVECTORED_EXCEPTION_HANDLER)Handler); + } + return ExceptionHandler != nullptr; + } + + inline bool AddHook(void* Target, void* Detour) + { + if (ExceptionHandler == nullptr) + { + return false; + } + + if (Util::IsSamePage(Target, Detour)) + { + return false; + } + + if (!VirtualProtect(Target, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &HookProtections.emplace_back())) + { + HookProtections.pop_back(); + return false; + } + + Hooks.emplace_back(Target, Detour); + return true; + } + + inline bool RemoveHook(void* Original) + { + auto Itr = std::find_if(Hooks.begin(), Hooks.end(), [Original](const HOOK_INFO& Hook) + { return Hook.Original == Original; }); + + if (Itr == Hooks.end()) + { + return false; + } + + const auto ProtItr = HookProtections.begin() + std::distance(Hooks.begin(), Itr); + Hooks.erase(Itr); + + DWORD dwOldProtect; + bool Ret = VirtualProtect(Original, 1, *ProtItr, &dwOldProtect); + HookProtections.erase(ProtItr); + + return false; + } + } +} \ No newline at end of file diff --git a/external/minhook/include/MinHook.h b/external/minhook/include/MinHook.h new file mode 100644 index 0000000..492d83f --- /dev/null +++ b/external/minhook/include/MinHook.h @@ -0,0 +1,185 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#if !(defined _M_IX86) && !(defined _M_X64) && !(defined __i386__) && !(defined __x86_64__) + #error MinHook supports only x86 and x64 systems. +#endif + +#include + +// MinHook Error Codes. +typedef enum MH_STATUS +{ + // Unknown error. Should not be returned. + MH_UNKNOWN = -1, + + // Successful. + MH_OK = 0, + + // MinHook is already initialized. + MH_ERROR_ALREADY_INITIALIZED, + + // MinHook is not initialized yet, or already uninitialized. + MH_ERROR_NOT_INITIALIZED, + + // The hook for the specified target function is already created. + MH_ERROR_ALREADY_CREATED, + + // The hook for the specified target function is not created yet. + MH_ERROR_NOT_CREATED, + + // The hook for the specified target function is already enabled. + MH_ERROR_ENABLED, + + // The hook for the specified target function is not enabled yet, or already + // disabled. + MH_ERROR_DISABLED, + + // The specified pointer is invalid. It points the address of non-allocated + // and/or non-executable region. + MH_ERROR_NOT_EXECUTABLE, + + // The specified target function cannot be hooked. + MH_ERROR_UNSUPPORTED_FUNCTION, + + // Failed to allocate memory. + MH_ERROR_MEMORY_ALLOC, + + // Failed to change the memory protection. + MH_ERROR_MEMORY_PROTECT, + + // The specified module is not loaded. + MH_ERROR_MODULE_NOT_FOUND, + + // The specified function is not found. + MH_ERROR_FUNCTION_NOT_FOUND +} +MH_STATUS; + +// Can be passed as a parameter to MH_EnableHook, MH_DisableHook, +// MH_QueueEnableHook or MH_QueueDisableHook. +#define MH_ALL_HOOKS NULL + +#ifdef __cplusplus +extern "C" { +#endif + + // Initialize the MinHook library. You must call this function EXACTLY ONCE + // at the beginning of your program. + MH_STATUS WINAPI MH_Initialize(VOID); + + // Uninitialize the MinHook library. You must call this function EXACTLY + // ONCE at the end of your program. + MH_STATUS WINAPI MH_Uninitialize(VOID); + + // Creates a hook for the specified target function, in disabled state. + // Parameters: + // pTarget [in] A pointer to the target function, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal); + + // Creates a hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszProcName [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApi( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal); + + // Creates a hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszProcName [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + // ppTarget [out] A pointer to the target function, which will be used + // with other functions. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApiEx( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal, LPVOID *ppTarget); + + // Removes an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget); + + // Enables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // enabled in one go. + MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget); + + // Disables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // disabled in one go. + MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget); + + // Queues to enable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be enabled. + MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget); + + // Queues to disable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be disabled. + MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget); + + // Applies all queued changes in one go. + MH_STATUS WINAPI MH_ApplyQueued(VOID); + + // Translates the MH_STATUS to its name as a string. + const char * WINAPI MH_StatusToString(MH_STATUS status); + +#ifdef __cplusplus +} +#endif diff --git a/external/minhook/lib/libMinHook.x64.lib b/external/minhook/lib/libMinHook.x64.lib new file mode 100644 index 0000000..e211ad6 Binary files /dev/null and b/external/minhook/lib/libMinHook.x64.lib differ diff --git a/external/minhook/lib/libMinHook.x86.lib b/external/minhook/lib/libMinHook.x86.lib new file mode 100644 index 0000000..b15b79b Binary files /dev/null and b/external/minhook/lib/libMinHook.x86.lib differ