diff --git a/lib/CoHModSDK/include/CoHModSDK.hpp b/lib/CoHModSDK/include/CoHModSDK.hpp index e898c66..cb6dee2 100644 --- a/lib/CoHModSDK/include/CoHModSDK.hpp +++ b/lib/CoHModSDK/include/CoHModSDK.hpp @@ -1,5 +1,5 @@ /** - * CoHModSDK - The lightweight modding SDK for Company of Heroes + * CoHModSDK - Shared runtime SDK for Company of Heroes * Copyright (c) 2026 Tosox * * This project is licensed under the Creative Commons @@ -16,100 +16,293 @@ #pragma once #include + #include #include +#include +#include +#include + +#define COHMODSDK_ABI_VERSION 1u + +#define COHMODSDK_HAS_FIELD(structPtr, fieldName) \ + ((structPtr)->size >= offsetof(std::remove_pointer_t, fieldName) + sizeof((structPtr)->fieldName)) + +#if defined(COHMODSDK_RUNTIME_EXPORTS) +#define COHMODSDK_RUNTIME_API extern "C" __declspec(dllexport) +#else +#define COHMODSDK_RUNTIME_API extern "C" __declspec(dllimport) +#endif + +#define COHMODSDK_MODULE_API extern "C" __declspec(dllexport) extern "C" { - /** - * @brief Called once when the SDK loads the mod DLL. - * - * Perform any early setup required for the mod here (e.g., install hooks, patch memory). - */ - __declspec(dllexport) void OnSDKLoad(); - - /** - * @brief Called when the game is starting (after all mods have been loaded). - * - * Use this to initialize features that require the game to be fully running. - */ - __declspec(dllexport) void OnGameStart(); - - /** - * @brief Called when the game is shutting down. - * - * Use this to clean up any hooks, memory patches, or resources before unloading. - */ - __declspec(dllexport) void OnGameShutdown(); - - /** - * @brief Returns the display name of the mod. - * - * @return const char* - Name of the mod. - */ - __declspec(dllexport) const char* GetModName(); - - /** - * @brief Returns the version string of the mod. - * - * @return const char* - Version of the mod. - */ - __declspec(dllexport) const char* GetModVersion(); - - /** - * @brief Returns the author name(s) of the mod. - * - * @return const char* - Author or team name. - */ - __declspec(dllexport) const char* GetModAuthor(); + struct CoHModSDKModContextV1; + + enum CoHModSDKLogLevel : std::uint32_t { + CoHModSDKLogLevel_Debug = 0, + CoHModSDKLogLevel_Info = 1, + CoHModSDKLogLevel_Warning = 2, + CoHModSDKLogLevel_Error = 3, + }; + + enum CoHModSDKConfigType : std::uint32_t { + CoHModSDKConfigType_Bool = 0, + CoHModSDKConfigType_Int = 1, + CoHModSDKConfigType_Float = 2, + CoHModSDKConfigType_Enum = 3, + }; + + enum CoHModSDKConfigFlags : std::uint32_t { + CoHModSDKConfigFlags_None = 0, + CoHModSDKConfigFlags_RestartRequired = 1u << 0, + }; + + struct CoHModSDKRuntimeInitV1 { + std::uint32_t abiVersion; + std::uint32_t size; + const char* loaderDirectory; + const char* modsDirectory; + const char* configDirectory; + const char* logPath; + const char* gameModuleName; + }; + + struct CoHModSDKRuntimeInfoV1 { + std::uint32_t abiVersion; + std::uint32_t size; + const char* runtimeVersion; + const char* loaderDirectory; + const char* modsDirectory; + const char* configDirectory; + const char* logPath; + const char* gameModuleName; + }; + + struct CoHModSDKConfigValueV1 { + CoHModSDKConfigType type; + union { + std::uint32_t boolValue; + std::int32_t intValue; + float floatValue; + std::int32_t enumValue; + }; + }; + + using CoHModSDKConfigChangedCallback = void(*)(const char* modId, const char* optionId, const CoHModSDKConfigValueV1* value, void* userData); + + struct CoHModSDKConfigChoiceV1 { + std::int32_t value; + const char* valueId; + const char* label; + }; + + struct CoHModSDKConfigOptionV1 { + const char* optionId; + const char* category; + const char* label; + const char* description; + CoHModSDKConfigType type; + CoHModSDKConfigValueV1 defaultValue; + float minValue; + float maxValue; + float step; + std::uint32_t flags; + const CoHModSDKConfigChoiceV1* choices; + std::uint32_t choiceCount; + CoHModSDKConfigChangedCallback onChanged; + void* userData; + }; + + struct CoHModSDKConfigSchemaV1 { + const char* modId; + const CoHModSDKConfigOptionV1* options; + std::uint32_t optionCount; + }; + + using CoHModSDKConfigModVisitor = bool(*)(const char* modId, void* userData); + using CoHModSDKConfigOptionVisitor = bool(*)(const CoHModSDKConfigOptionV1* option, const CoHModSDKConfigValueV1* currentValue, void* userData); + + struct CoHModSDKApiV1 { + std::uint32_t abiVersion; + std::uint32_t size; + const CoHModSDKRuntimeInfoV1* (*GetRuntimeInfo)(); + void (*Log)(const CoHModSDKModContextV1* modContext, CoHModSDKLogLevel level, const char* message); + void (*ShowError)(const CoHModSDKModContextV1* modContext, const char* message); + std::optional (*FindPattern)(const char* moduleName, const char* signature); + void (*PatchMemory)(void* destination, const void* source, std::size_t size); + bool (*CreateHook)(void* targetFunction, void* detourFunction, void** originalFunction); + bool (*EnableHook)(void* targetFunction); + bool (*DisableHook)(void* targetFunction); + bool (*RegisterConfigSchema)(const CoHModSDKConfigSchemaV1* schema); + bool (*GetConfigValue)(const char* modId, const char* optionId, CoHModSDKConfigValueV1* outValue); + bool (*SetConfigValue)(const char* modId, const char* optionId, const CoHModSDKConfigValueV1* value); + bool (*EnumerateConfigMods)(CoHModSDKConfigModVisitor visitor, void* userData); + bool (*EnumerateConfigOptions)(const char* modId, CoHModSDKConfigOptionVisitor visitor, void* userData); + }; + + struct CoHModSDKModuleV1 { + std::uint32_t abiVersion; + std::uint32_t size; + const char* modId; + const char* name; + const char* version; + const char* author; + bool (*OnInitialize)(); + bool (*OnModsLoaded)(); + void (*OnShutdown)(); + }; + + COHMODSDK_RUNTIME_API bool CoHModSDKRuntime_Initialize(const CoHModSDKRuntimeInitV1* init); + COHMODSDK_RUNTIME_API void CoHModSDKRuntime_Shutdown(); + COHMODSDK_RUNTIME_API bool CoHModSDKRuntime_RegisterMod(HMODULE modHandle, const CoHModSDKModuleV1* module, const CoHModSDKModContextV1** outContext); + COHMODSDK_RUNTIME_API void CoHModSDKRuntime_UnregisterMod(HMODULE modHandle); + COHMODSDK_RUNTIME_API bool CoHModSDK_GetApi(std::uint32_t abiVersion, const CoHModSDKApiV1** outApi); } +#define COHMODSDK_EXPORT_MODULE(moduleInstance) \ + COHMODSDK_MODULE_API bool CoHMod_GetModule(std::uint32_t abiVersion, const CoHModSDKModuleV1** outModule) { \ + if ((outModule == nullptr) || (abiVersion < COHMODSDK_ABI_VERSION)) { \ + return false; \ + } \ + *outModule = &(moduleInstance); \ + return true; \ + } \ + COHMODSDK_MODULE_API void CoHMod_SetContext(const CoHModSDKModContextV1* modContext) { \ + ::ModSDK::Detail::SetModContext(modContext); \ + } + namespace ModSDK { - namespace Memory { - /** - * @brief Scans a module for a byte pattern signature. - * - * @param moduleName Name of the module (e.g., "WW2Mod.dll"). - * @param signature Pattern string (e.g., "48 8B ?? ?? ?? ?? ?? 48 8B"). - * @param reportError Whether to show an error if the pattern is not found. - * @return std::uintptr_t Address where the pattern was found or 0 if not found. - */ - std::uintptr_t FindPattern(const char* moduleName, const char* signature, bool reportError = true); - - /** - * @brief Patches memory by copying bytes to a destination address. - * - * @param destination Target address to patch. - * @param source Bytes to write. - * @param size Number of bytes to copy. - */ - void PatchMemory(void* destination, const void* source, std::size_t size); - } - - namespace Hooks { - /** - * @brief Creates a hook from a target function to a detour function. - * - * @param targetFunction Pointer to the function to hook. - * @param detourFunction Pointer to the custom function (your detour). - * @param originalFunction Out parameter; will store the pointer to call original later. - * @return true if the hook was created successfully, false otherwise. - */ - bool CreateHook(void* targetFunction, void* detourFunction, void** originalFunction); - - /** - * @brief Enables an individual installed hook. - * - * @param targetFunction Pointer to the function where a hook was created. - * @return true if successfully enabled, false otherwise. - */ - bool EnableHook(void* targetFunction); - - /** - * @brief Disables an individual hook. - * - * @param targetFunction Pointer to the hooked function. - * @return true if successfully disabled, false otherwise. - */ - bool DisableHook(void* targetFunction); - } + namespace Detail { + inline const CoHModSDKModContextV1*& ModContextStorage() { + static const CoHModSDKModContextV1* modContext = nullptr; + return modContext; + } + + inline void SetModContext(const CoHModSDKModContextV1* modContext) { + ModContextStorage() = modContext; + } + + inline const CoHModSDKModContextV1* GetModContext() { + const CoHModSDKModContextV1* modContext = ModContextStorage(); + if (modContext == nullptr) { + throw std::runtime_error("CoHModSDK mod context is unavailable"); + } + + return modContext; + } + + inline const CoHModSDKApiV1& GetApi() { + static const CoHModSDKApiV1* api = []() -> const CoHModSDKApiV1* { + const CoHModSDKApiV1* resolvedApi = nullptr; + if (!CoHModSDK_GetApi(COHMODSDK_ABI_VERSION, &resolvedApi) || (resolvedApi == nullptr)) { + throw std::runtime_error("CoHModSDK runtime API is unavailable"); + } + + return resolvedApi; + }(); + + return *api; + } + } + + namespace Runtime { + inline const CoHModSDKRuntimeInfoV1* GetInfo() { + return Detail::GetApi().GetRuntimeInfo(); + } + + inline void Log(CoHModSDKLogLevel level, const char* message) { + Detail::GetApi().Log(Detail::GetModContext(), level, message); + } + } + + namespace Dialogs { + inline void ShowError(const char* message) { + Detail::GetApi().ShowError(Detail::GetModContext(), message); + } + } + + namespace Memory { + inline std::optional FindPattern(const char* moduleName, const char* signature) { + return Detail::GetApi().FindPattern(moduleName, signature); + } + + inline void PatchMemory(void* destination, const void* source, std::size_t size) { + Detail::GetApi().PatchMemory(destination, source, size); + } + } + + namespace Hooks { + inline bool CreateHook(void* targetFunction, void* detourFunction, void** originalFunction) { + return Detail::GetApi().CreateHook(targetFunction, detourFunction, originalFunction); + } + + inline bool EnableHook(void* targetFunction) { + return Detail::GetApi().EnableHook(targetFunction); + } + + inline bool DisableHook(void* targetFunction) { + return Detail::GetApi().DisableHook(targetFunction); + } + } + + namespace Config { + using Value = CoHModSDKConfigValueV1; + using Choice = CoHModSDKConfigChoiceV1; + using Option = CoHModSDKConfigOptionV1; + using Schema = CoHModSDKConfigSchemaV1; + using Type = CoHModSDKConfigType; + using Flags = CoHModSDKConfigFlags; + using ChangedCallback = CoHModSDKConfigChangedCallback; + using ModVisitor = CoHModSDKConfigModVisitor; + using OptionVisitor = CoHModSDKConfigOptionVisitor; + + inline Value MakeBoolValue(bool value) { + Value result = {}; + result.type = CoHModSDKConfigType_Bool; + result.boolValue = value ? 1u : 0u; + return result; + } + + inline Value MakeIntValue(std::int32_t value) { + Value result = {}; + result.type = CoHModSDKConfigType_Int; + result.intValue = value; + return result; + } + + inline Value MakeFloatValue(float value) { + Value result = {}; + result.type = CoHModSDKConfigType_Float; + result.floatValue = value; + return result; + } + + inline Value MakeEnumValue(std::int32_t value) { + Value result = {}; + result.type = CoHModSDKConfigType_Enum; + result.enumValue = value; + return result; + } + + inline bool RegisterSchema(const Schema& schema) { + return Detail::GetApi().RegisterConfigSchema(&schema); + } + + inline bool GetValue(const char* modId, const char* optionId, Value* outValue) { + return Detail::GetApi().GetConfigValue(modId, optionId, outValue); + } + + inline bool SetValue(const char* modId, const char* optionId, const Value& value) { + return Detail::GetApi().SetConfigValue(modId, optionId, &value); + } + + inline bool EnumerateMods(ModVisitor visitor, void* userData) { + return Detail::GetApi().EnumerateConfigMods(visitor, userData); + } + + inline bool EnumerateOptions(const char* modId, OptionVisitor visitor, void* userData) { + return Detail::GetApi().EnumerateConfigOptions(modId, visitor, userData); + } + } } diff --git a/lib/CoHModSDK/lib/x86/CoHModSDK.lib b/lib/CoHModSDK/lib/x86/CoHModSDK.lib index badca0d..7a20901 100644 Binary files a/lib/CoHModSDK/lib/x86/CoHModSDK.lib and b/lib/CoHModSDK/lib/x86/CoHModSDK.lib differ diff --git a/res/resource.rc b/res/resource.rc index 0af988c..0f19a91 100644 --- a/res/resource.rc +++ b/res/resource.rc @@ -61,8 +61,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,1,0,0 - PRODUCTVERSION 0,1,0,0 + FILEVERSION 1,1,0,0 + PRODUCTVERSION 1,1,0,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -79,12 +79,12 @@ BEGIN BEGIN VALUE "CompanyName", "Tosox" VALUE "FileDescription", "Persists the bot faction when switching their difficulty" - VALUE "FileVersion", "0.1.0.0" + VALUE "FileVersion", "1.1.0" VALUE "InternalName", "PersistentBotFaction" VALUE "LegalCopyright", "Copyright © 2026" VALUE "OriginalFilename", "PersistentBotFaction.dll" VALUE "ProductName", "PersistentBotFaction" - VALUE "ProductVersion", "0.1.0.0" + VALUE "ProductVersion", "1.1.0" END END BLOCK "VarFileInfo" diff --git a/src/dllmain.cpp b/src/dllmain.cpp index f745e57..93a348e 100644 --- a/src/dllmain.cpp +++ b/src/dllmain.cpp @@ -2,111 +2,114 @@ #include -#pragma comment(lib, "CoHModSDK.lib") - -constexpr const char* kApplyPlayerSetupChangePattern = - "8B 41 08 8B 51 0C 8B C8 23 CA 83 F9 FF 74 ?? 8B 0D ?? ?? ?? ?? 56 57 " - "8B 7C 24 24 57 8B 7C 24 24 8B B1 08 1C 00 00"; - -enum PlayerSetupCategory { - SlotReset = 0, - SlotClose = 1, - SlotConfig = 2, -}; - -using ApplyPlayerSetupChangeFn = int(__thiscall*)(void*, int, int, int, int, int, int, void*); - -ApplyPlayerSetupChangeFn oFnApplyPlayerSetupChange = nullptr; -std::unordered_map cachedFactionBySlot; - -void HandleFactionCache(int playerSlot, int setupCategory, int& selectionValue, void* slotAuxData) { - if (playerSlot < 0) { - return; +namespace { + enum PlayerSetupCategory { + SlotReset = 0, + SlotClose = 1, + SlotConfig = 2, + }; + + constexpr const char* kApplyPlayerSetupChangePattern = + "8B 41 08 8B 51 0C 8B C8 23 CA 83 F9 FF 74 ?? 8B 0D ?? ?? ?? ?? 56 57 " + "8B 7C 24 24 57 8B 7C 24 24 8B B1 08 1C 00 00"; + + using ApplyPlayerSetupChangeFn = int(__thiscall*)(void*, int, int, int, int, int, int, void*); + + ApplyPlayerSetupChangeFn oFnApplyPlayerSetupChange = nullptr; + std::unordered_map cachedFactionBySlot; + + void HandleFactionCache(int playerSlot, int setupCategory, int& selectionValue, void* slotAuxData) { + if (playerSlot < 0) { + return; + } + + if ((setupCategory == PlayerSetupCategory::SlotReset) || + (setupCategory == PlayerSetupCategory::SlotClose)) { + cachedFactionBySlot.erase(playerSlot); + return; + } + + if (setupCategory != PlayerSetupCategory::SlotConfig) { + return; + } + + if (slotAuxData == nullptr) { + // Direct faction selection and swap reconfiguration both reach here. + // In both cases the game passes the correct faction for the slot, so + // cache it unconditionally. + cachedFactionBySlot[playerSlot] = selectionValue; + return; + } + + // Difficulty / option change – the game rebuilds slot config from defaults, + // resetting the faction to the team default. Restore the previously + // selected faction from our cache. + const auto cachedFaction = cachedFactionBySlot.find(playerSlot); + if (cachedFaction != cachedFactionBySlot.end()) { + selectionValue = cachedFaction->second; + } } - if ((setupCategory == PlayerSetupCategory::SlotReset) || - (setupCategory == PlayerSetupCategory::SlotClose)) { - cachedFactionBySlot.erase(playerSlot); - return; - } + int __fastcall HookedApplyPlayerSetupChange(void* _this, void* edx, int playerSlot, int setupCategory, int rawTeamValue, + int normalizedTeamValue, int selectionValue, int optionValue, void* slotAuxData) { + HandleFactionCache(playerSlot, setupCategory, selectionValue, slotAuxData); - if (setupCategory != PlayerSetupCategory::SlotConfig) { - return; + return oFnApplyPlayerSetupChange(_this, playerSlot, setupCategory, rawTeamValue, + normalizedTeamValue, selectionValue, optionValue, slotAuxData); } - if (slotAuxData == nullptr) { - // Direct faction selection and swap reconfiguration both reach here. - // In both cases the game passes the correct faction for the slot, so - // cache it unconditionally. - cachedFactionBySlot[playerSlot] = selectionValue; - return; + bool SetupHook() { + const auto tApplyPlayerSetupChangeResult = ModSDK::Memory::FindPattern("RelicCOH.exe", kApplyPlayerSetupChangePattern); + if (!tApplyPlayerSetupChangeResult.has_value()) { + ModSDK::Dialogs::ShowError("Failed to find ApplyPlayerSetupChange"); + return false; + } + + const auto tApplyPlayerSetupChange = reinterpret_cast(tApplyPlayerSetupChangeResult.value()); + if (!ModSDK::Hooks::CreateHook( + tApplyPlayerSetupChange, + reinterpret_cast(&HookedApplyPlayerSetupChange), + reinterpret_cast(&oFnApplyPlayerSetupChange))) { + ModSDK::Dialogs::ShowError("Failed to create ApplyPlayerSetupChange hook"); + return false; + } + + if (!ModSDK::Hooks::EnableHook(tApplyPlayerSetupChange)) { + ModSDK::Dialogs::ShowError("Failed to enable ApplyPlayerSetupChange hook"); + return false; + } + + return true; } - // Difficulty / option change – the game rebuilds slot config from defaults, - // resetting the faction to the team default. Restore the previously - // selected faction from our cache. - const auto cachedFaction = cachedFactionBySlot.find(playerSlot); - if (cachedFaction != cachedFactionBySlot.end()) { - selectionValue = cachedFaction->second; + bool OnInitialize() { + return true; } -} -int __fastcall HookedApplyPlayerSetupChange(void* _this, void* edx, int playerSlot, int setupCategory, int rawTeamValue, - int normalizedTeamValue, int selectionValue, int optionValue, void* slotAuxData) { - HandleFactionCache(playerSlot, setupCategory, selectionValue, slotAuxData); - - return oFnApplyPlayerSetupChange(_this, playerSlot, setupCategory, rawTeamValue, - normalizedTeamValue, selectionValue, optionValue, slotAuxData); -} - -void SetupHook() { - const auto applyPlayerSetupChange = reinterpret_cast(ModSDK::Memory::FindPattern(nullptr, kApplyPlayerSetupChangePattern)); - if (!applyPlayerSetupChange) { - MessageBoxA(nullptr, "Failed to find the player setup change function", "Persistent Bot Faction", MB_ICONERROR); - return; + bool OnModsLoaded() { + return SetupHook(); } - if (!ModSDK::Hooks::CreateHook( - applyPlayerSetupChange, - reinterpret_cast(&HookedApplyPlayerSetupChange), - reinterpret_cast(&oFnApplyPlayerSetupChange))) { - MessageBoxA(nullptr, "Failed to create the player setup change hook", "Persistent Bot Faction", MB_ICONERROR); - return; + void OnShutdown() { + cachedFactionBySlot.clear(); } - if (!ModSDK::Hooks::EnableHook(applyPlayerSetupChange)) { - MessageBoxA(nullptr, "Failed to enable the player setup change hook", "Persistent Bot Faction", MB_ICONERROR); - } + const CoHModSDKModuleV1 kModule = { + .abiVersion = COHMODSDK_ABI_VERSION, + .size = sizeof(CoHModSDKModuleV1), + .modId = "de.tosox.persistentbotfaction", + .name = "Persistent Bot Faction", + .version = "1.1.0", + .author = "Tosox", + .OnInitialize = &OnInitialize, + .OnModsLoaded = &OnModsLoaded, + .OnShutdown = &OnShutdown + }; } - BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) { DisableThreadLibraryCalls(hModule); return TRUE; } -extern "C" { - __declspec(dllexport) void OnSDKLoad() { - // Unused - } - - __declspec(dllexport) void OnGameStart() { - SetupHook(); - } - - __declspec(dllexport) void OnGameShutdown() { - cachedFactionBySlot.clear(); - } - - __declspec(dllexport) const char* GetModName() { - return "Persistent Bot Faction"; - } - - __declspec(dllexport) const char* GetModVersion() { - return "1.0.0"; - } - - __declspec(dllexport) const char* GetModAuthor() { - return "Tosox"; - } -} +COHMODSDK_EXPORT_MODULE(kModule);