diff --git a/.gitignore b/.gitignore index 44e12b9..ce00885 100644 --- a/.gitignore +++ b/.gitignore @@ -365,6 +365,10 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd +# Google Drive File Stream +/.tmp.driveupload +/.tmp.drivedownload + ########### # Allowed # ########### diff --git a/EEex-v2.6.6.0/headers/EEex-v2.6.6.0/EEex.h b/EEex-v2.6.6.0/headers/EEex-v2.6.6.0/EEex.h index edc3203..e68852b 100644 --- a/EEex-v2.6.6.0/headers/EEex-v2.6.6.0/EEex.h +++ b/EEex-v2.6.6.0/headers/EEex-v2.6.6.0/EEex.h @@ -69,6 +69,18 @@ namespace EEex { void GameState_Hook_OnInitialized(); void GameState_Hook_OnAfterGlobalVariablesUnmarshalled(); + ////////////////////// + // Priest Spellbook // + ////////////////////// + + int PriestSpell_Hook_OnCanCastPriestSpells(CScreenPriestSpell* pScreen, CGameSprite* pSprite, int spellListResult); + + //////////// + // Object // + //////////// + + bool Object_Hook_ShouldTreatClassAsWildcardMatch(const CAIObjectType* pActualType, const CAIObjectType* pRequestedType); + /////////// // Stats // /////////// diff --git a/EEex-v2.6.6.0/headers/EEex-v2.6.6.0_generated/Baldur-v2.6.6.0_generated.h b/EEex-v2.6.6.0/headers/EEex-v2.6.6.0_generated/Baldur-v2.6.6.0_generated.h index 0b0048d..c74840d 100644 --- a/EEex-v2.6.6.0/headers/EEex-v2.6.6.0_generated/Baldur-v2.6.6.0_generated.h +++ b/EEex-v2.6.6.0/headers/EEex-v2.6.6.0_generated/Baldur-v2.6.6.0_generated.h @@ -157,6 +157,7 @@ struct CSaveGameSlot; struct CSavedGamePartyCreature; struct CSavedGameStoredLocation; struct CScreenMap; +struct CScreenPriestSpell; struct CScreenStore; struct CScreenWorld; struct CSearchBitmap; @@ -12825,6 +12826,14 @@ struct CScreenPriestSpell : CBaldurEngine int m_bControlled; CScreenPriestSpell() = delete; + + typedef int (__thiscall *type_CanCastPriestSpells)(CScreenPriestSpell* pThis, CGameSprite* pSprite); + static type_CanCastPriestSpells p_CanCastPriestSpells; + + int CanCastPriestSpells(CGameSprite* pSprite) + { + return p_CanCastPriestSpells(this, pSprite); + } }; struct CScreenJournal : CBaldurEngine diff --git a/EEex-v2.6.6.0/scripts/generate_bindings/in/bindings.txt b/EEex-v2.6.6.0/scripts/generate_bindings/in/bindings.txt index f4bc78e..0adb5e8 100644 --- a/EEex-v2.6.6.0/scripts/generate_bindings/in/bindings.txt +++ b/EEex-v2.6.6.0/scripts/generate_bindings/in/bindings.txt @@ -67,6 +67,11 @@ struct CScreenMap : CBaldurEngine $nobinding $external_implementation void Override_OnLButtonDblClk(CPoint cPoint); }; +struct CScreenPriestSpell : CBaldurEngine +{ + $nobinding int CanCastPriestSpells(CGameSprite* pSprite); +}; + struct CScreenWorld : CBaldurEngine { $nobinding $external_implementation void Override_EndDialog(byte bForceExecution, byte fullEnd); diff --git a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/EEex.cpp b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/EEex.cpp index 9a80717..d853a10 100644 --- a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/EEex.cpp +++ b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/EEex.cpp @@ -1,8 +1,14 @@ +#include +#include +#include #include +#include #include #include +#include #include +#include #include @@ -121,6 +127,14 @@ struct ExScriptData { std::unordered_map exScriptDataMap{}; +//////////// +// Object // +//////////// + +// exClassWildcardMatches[actualClass][requestedClass] is true when X-CLASS.2DA +// says the requested class wildcard should accept the actual concrete class. +std::array, 256> exClassWildcardMatches{}; + //////////// // Opcode // //////////// @@ -166,6 +180,15 @@ std::unordered_map exUUIDDataMap{}; uint64_t nextUUID = 0; +////////////////////// +// Priest Spellbook // +////////////////////// + +// Suppresses the native return hook while the CScreenPriestSpell Lua method is +// deliberately calling the original engine implementation. This is what makes +// the common Lua pattern `local old = ...; old(...)` resolve to vanilla behavior. +thread_local bool exCanCastPriestSpellsBypassLuaOverride = false; + ///////// // Fix // ///////// @@ -3156,12 +3179,110 @@ void __cdecl EEex::Override_uiDoFile(char* fileName) { //////////////// void initStats(); +void initClassWildcards(); + +static int CScreenPriestSpell_CanCastPriestSpells_Lua(lua_State *const L) { + + CScreenPriestSpell *const self = static_cast(tolua_tousertype_dynamic(L, 1, nullptr, "CScreenPriestSpell")); + if (!self) { + tolua_error(L, "invalid 'self' in calling function 'CanCastPriestSpells'", nullptr); + } + + // This wrapper is the original method Lua sees before mods replace + // CScreenPriestSpell.CanCastPriestSpells. Calling it must not re-enter the + // Lua override hook, otherwise `old(...)` would recursively call the modded + // method instead of the engine implementation. + const bool oldBypass = exCanCastPriestSpellsBypassLuaOverride; + exCanCastPriestSpellsBypassLuaOverride = true; + const int returnVal = self->CanCastPriestSpells(static_cast(tolua_tousertype_dynamic(L, 2, nullptr, "CGameSprite"))); + exCanCastPriestSpellsBypassLuaOverride = oldBypass; + + tolua_pushnumber(L, returnVal); + return 1; +} + +static void registerPriestSpellEngineLuaBindings() { + + lua_State *const L = luaState(); + + // The engine owns the unprefixed CScreenPriestSpell table used by e: UI + // calls. Register after that table exists, but before initialized listeners + // run so mods can capture and replace the method during initialization. + lua_getglobal(L, "CScreenPriestSpell"); + if (lua_istable(L, -1)) { + lua_pushcfunction(L, &CScreenPriestSpell_CanCastPriestSpells_Lua); + lua_setfield(L, -2, "CanCastPriestSpells"); + } + lua_pop(L, 1); +} + +int EEex::PriestSpell_Hook_OnCanCastPriestSpells(CScreenPriestSpell *const pScreen, CGameSprite *const pSprite, const int spellListResult) { + + // The hook is placed before the vanilla final general-state mask check. + // `spellListResult` is the result of the spell-list/class checks already + // performed by the engine; this reproduces the skipped mask check exactly. + int result = pSprite != nullptr && (pSprite->m_baseStats.m_generalState & 0x600) == 0 + ? spellListResult + : 0; + + if (exCanCastPriestSpellsBypassLuaOverride || pScreen == nullptr) { + return result; + } + + lua_State *const L = luaState(); + + lua_getglobal(L, "CScreenPriestSpell"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return result; + } + + lua_getfield(L, -1, "CanCastPriestSpells"); + lua_remove(L, -2); + + const bool bHasOverride = + lua_isfunction(L, -1) + && (!lua_iscfunction(L, -1) || lua_tocfunction(L, -1) != &CScreenPriestSpell_CanCastPriestSpells_Lua); + + lua_pop(L, 1); + + if (!bHasOverride) { + return result; + } + + // While executing the replacement Lua function, any captured call to the + // original C function should bypass this hook and return the vanilla result. + const bool oldBypass = exCanCastPriestSpellsBypassLuaOverride; + exCanCastPriestSpellsBypassLuaOverride = true; + + if (luaCallProtected(L, 3, 1, [&](int) { + lua_getglobal(L, "CScreenPriestSpell"); + lua_getfield(L, -1, "CanCastPriestSpells"); + lua_remove(L, -2); + tolua_pushusertype(L, pScreen, "CScreenPriestSpell"); + tolua_pushusertype(L, pSprite, "CGameSprite"); + lua_pushinteger(L, result); + })) { + if (lua_isboolean(L, -1)) { + result = lua_toboolean(L, -1) ? 1 : 0; + } + else if (lua_isnumber(L, -1)) { + result = static_cast(lua_tointeger(L, -1)); + } + lua_pop(L, 1); + } + + exCanCastPriestSpellsBypassLuaOverride = oldBypass; + return result; +} void EEex::GameState_Hook_OnInitialized() { STUTTER_LOG_START(void, "EEex::GameState_Hook_OnInitialized") initStats(); + initClassWildcards(); + registerPriestSpellEngineLuaBindings(); lua_State *const L = luaState(); luaCallProtected(L, 0, 0, [&](int _) { @@ -3171,6 +3292,28 @@ void EEex::GameState_Hook_OnInitialized() { STUTTER_LOG_END } +//////////// +// Object // +//////////// + +bool EEex::Object_Hook_ShouldTreatClassAsWildcardMatch(const CAIObjectType *const pActualType, const CAIObjectType *const pRequestedType) { + + if (pActualType == nullptr || pRequestedType == nullptr) { + return false; + } + + const byte actualClass = pActualType->m_Class; + const byte requestedClass = pRequestedType->m_Class; + + if (requestedClass == 0 || actualClass == requestedClass) { + return false; + } + + // This helper only answers the extension table. The hook falls back to the + // engine's native class block for exact matches and hardcoded vanilla groups. + return exClassWildcardMatches[actualClass].test(requestedClass); +} + void addNextUUIDLocal(CVariableHash* pVariables) { EngineVal variable; variable->m_name.set("EEEX_NEXTUUID"); @@ -5345,6 +5488,104 @@ void initStats() { } } +std::string getNormalizedIDSLabel(const char *const label) { + + std::string normalizedLabel = label != nullptr ? label : ""; + std::transform(normalizedLabel.begin(), normalizedLabel.end(), normalizedLabel.begin(), [](const unsigned char ch) { + return static_cast(std::toupper(ch)); + }); + + return normalizedLabel; +} + +void initClassWildcards() { + + for (auto& matches : exClassWildcardMatches) { + matches.reset(); + } + + EngineVal pClassIDS{}; + pClassIDS->LoadList("CLASS", false); + + std::unordered_map classIdByLabel{}; + + for (const auto* node = pClassIDS->m_idList.m_pNodeHead; node != nullptr; node = node->pNext) { + + CAIId *const pId = node->data; + const int id = pId->m_id; + const char *const name = pId->m_line.m_pchData; + + if (id < 0 || id > 255) { + FPrint("[!][EEex.dll] CLASS.IDS - Ignoring %s(#%d), X-CLASS.2DA only supports byte-sized class ids\n", name, id); + continue; + } + + classIdByLabel.try_emplace(getNormalizedIDSLabel(name), id); + } + + // X-CLASS.2DA is label-driven on both axes. Resolve every visible row and + // column through CLASS.IDS so the table remains stable if ids are reordered + // or labels differ in case. + EngineVal pXClass2DA{}; + { + const CResRef resref{"X-CLASS"}; + pXClass2DA->Load(&resref); + } + + struct XClassColumn { + int classId; + std::string label; + }; + + // Resolve labels through CLASS.IDS instead of assuming any numeric ids. + std::vector columns{}; + for (int x = 0; x < pXClass2DA->m_nSizeX; ++x) { + + const char *const columnName = pXClass2DA->m_pNamesX->getReference(x)->m_pchData; + const std::string normalizedColumnName = getNormalizedIDSLabel(columnName); + + if (normalizedColumnName.empty()) { + continue; + } + + if (auto itr = classIdByLabel.find(normalizedColumnName); itr != classIdByLabel.end()) { + columns.push_back({itr->second, columnName}); + } + else { + FPrint("[!][EEex.dll] X-CLASS.2DA - Ignoring invalid CLASS.IDS column label: \"%s\"\n", columnName); + } + } + + for (int y = 0; y < pXClass2DA->m_nSizeY; ++y) { + + const char *const rowName = pXClass2DA->m_pNamesY->getReference(y)->m_pchData; + + const auto rowItr = classIdByLabel.find(getNormalizedIDSLabel(rowName)); + if (rowItr == classIdByLabel.end()) { + FPrint("[!][EEex.dll] X-CLASS.2DA - Ignoring invalid CLASS.IDS row label: \"%s\"\n", rowName); + continue; + } + + const int rowClassId = rowItr->second; + + for (const XClassColumn& column : columns) { + + const CString *const strVal = pXClass2DA->GetAt(column.label.c_str(), rowName); + const char *const value = strVal->m_pchData; + + // Only an explicit literal "1" grants a wildcard match. Missing + // cells, defaults, "0", and malformed values are intentionally false. + if (strcmp(value, "1") == 0) { + exClassWildcardMatches[rowClassId].set(column.classId); + } + else if (strcmp(value, "0") != 0 && strcmp(value, "*") != 0) { + FPrint("[!][EEex.dll] X-CLASS.2DA - Invalid %s -> %s value: \"%s\"\n", + rowName, column.label.c_str(), value); + } + } + } +} + template DWORD getLuaProc(const char* name, out_type& out) { if (out = reinterpret_cast(GetProcAddress(luaLibrary(), name)); out == 0) { diff --git a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/Generated/Baldur-v2.6.6.0_generated_internal_pointers.cpp b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/Generated/Baldur-v2.6.6.0_generated_internal_pointers.cpp index 4e470c8..7cd8d30 100644 --- a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/Generated/Baldur-v2.6.6.0_generated_internal_pointers.cpp +++ b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/Generated/Baldur-v2.6.6.0_generated_internal_pointers.cpp @@ -219,6 +219,7 @@ CScreenMap::vtbl* CScreenMap::VFTable; CInfCursor::type_SetCursor CInfCursor::p_SetCursor; CSpell::type_Construct CSpell::p_Construct; CScreenWorld::type_TogglePauseGame CScreenWorld::p_TogglePauseGame; +CScreenPriestSpell::type_CanCastPriestSpells CScreenPriestSpell::p_CanCastPriestSpells; CRuleTables::type_MapCharacterSpecializationToSchool CRuleTables::p_MapCharacterSpecializationToSchool; CRuleTables::type_IsProtectedFromSpell CRuleTables::p_IsProtectedFromSpell; CNetwork::type_ThreadLoop CNetwork::p_ThreadLoop; @@ -557,6 +558,7 @@ void InitBindingsInternal() { attemptFillPointer(TEXT("CInfCursor::SetCursor"), CInfCursor::p_SetCursor); attemptFillPointer(TEXT("CSpell::Construct"), CSpell::p_Construct); attemptFillPointer(TEXT("CScreenWorld::TogglePauseGame"), CScreenWorld::p_TogglePauseGame); + attemptFillPointer(TEXT("CScreenPriestSpell::CanCastPriestSpells"), CScreenPriestSpell::p_CanCastPriestSpells); attemptFillPointer(TEXT("CRuleTables::MapCharacterSpecializationToSchool"), CRuleTables::p_MapCharacterSpecializationToSchool); attemptFillPointer(TEXT("CRuleTables::IsProtectedFromSpell"), CRuleTables::p_IsProtectedFromSpell); attemptFillPointer(TEXT("CNetwork::ThreadLoop"), CNetwork::p_ThreadLoop); diff --git a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/main.cpp b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/main.cpp index a0cb76e..626cd13 100644 --- a/EEex-v2.6.6.0/source/EEex-v2.6.6.0/main.cpp +++ b/EEex-v2.6.6.0/source/EEex-v2.6.6.0/main.cpp @@ -88,6 +88,18 @@ static void exportPatterns() { exportPattern(TEXT("EEex::GameState_Hook_OnInitialized"), EEex::GameState_Hook_OnInitialized); exportPattern(TEXT("EEex::GameState_Hook_OnAfterGlobalVariablesUnmarshalled"), EEex::GameState_Hook_OnAfterGlobalVariablesUnmarshalled); + ////////////////////// + // Priest Spellbook // + ////////////////////// + + exportPattern(TEXT("EEex::PriestSpell_Hook_OnCanCastPriestSpells"), EEex::PriestSpell_Hook_OnCanCastPriestSpells); + + //////////// + // Object // + //////////// + + exportPattern(TEXT("EEex::Object_Hook_ShouldTreatClassAsWildcardMatch"), EEex::Object_Hook_ShouldTreatClassAsWildcardMatch); + /////////// // Stats // ///////////