mahlendur/_work/Data/Scripts/Content/LeGo/HookEngine.d
Daniel Heße 96b7bfd52d Fixed some bugs, did some testing.
Everything should be working by now, up until final battle (still needs testing).

Updated LeGo to 2.8.0
2022-10-03 18:16:51 +02:00

492 lines
18 KiB
D

/***********************************\
HOOKENGINE
\***********************************/
//---------------------
// Register variables
//---------------------
var int EAX;
var int ECX;
var int EDX;
var int EBX;
var int ESP;
var int EBP;
var int ESI;
var int EDI;
//---------------------
// Overwrite instances
//---------------------
var int HookOverwriteInstances; // self, other, item
//========================================
// [intern] Hook controller
//========================================
func void _Hook(var int evtHAddr, // ESP-44
var int _edi, // ESP-40 // Function parameters in order of popad (reverse order of pushad)
var int _esi, // ESP-36
var int _ebp, // ESP-32
var int _esp, // ESP-28
var int _ebx, // ESP-24
var int _edx, // ESP-20
var int _ecx, // ESP-16
var int _eax) { // ESP-12
// Backup use-instance before anything else. Temporary variable for now, because it's done before locals()
var int _instBak_temp; _instBak_temp = MEM_GetUseInstance();
// Local backup for recursive hooks
locals();
// Secure register variables locally for recursive hooks
var int eaxBak; eaxBak = EAX;
var int ecxBak; ecxBak = ECX;
var int edxBak; edxBak = EDX;
var int ebxBak; ebxBak = EBX;
var int espBak; espBak = ESP;
var int ebpBak; ebpBak = EBP;
var int esiBak; esiBak = ESI;
var int ediBak; ediBak = EDI;
// Get address of yINSTANCE_HELP by symbol index 0
const int instHlpAddr = 0;
if (!instHlpAddr) {
instHlpAddr = MEM_GetSymbolByIndex(0)+zCParSymbol_offset_offset;
};
// Also secure global instances
var int selfBak; selfBak = _@(self);
var int otherBak; otherBak = _@(other);
var int itemBak; itemBak = _@(item);
var int iHlpBak; iHlpBak = MEM_ReadInt(instHlpAddr);
var int instBak; instBak = _instBak_temp;
// Update register variables
EAX = _eax;
ECX = _ecx;
EDX = _edx;
EBX = _ebx;
ESP = _esp;
EBP = _ebp;
ESI = _esi;
EDI = _edi;
// Check whether Ikarus is initialized for hooks that happen during level change
if (!_@(MEM_Parser)) {
MEM_InitLabels();
MEM_InitGlobalInst();
};
// Iterate over all registered event handler functions
var zCArray a; a = _^(evtHAddr);
repeat(i, a.numInArray); var int i;
// Clear data stack in-between function calls
MEM_Parser.datastack_sptr = 0;
// Do not overwrite the global instances by default
HookOverwriteInstances = FALSE;
// Obtain hooking function
var int funcID; funcID = MEM_ReadIntArray(a.array, i);
var zCPar_Symbol fncSymb; fncSymb = _^(MEM_GetSymbolByIndex(funcID));
// Supply function arguments if expected
var int stackOffset; stackOffset = 4;
repeat(j, fncSymb.bitfield & zCPar_Symbol_bitfield_ele); var int j;
var zCPar_Symbol symb; symb = _^(MEM_GetSymbolByIndex(funcID+1+j));
var int stackValue; stackValue = MEM_ReadInt(ESP+stackOffset); stackOffset += 4;
if ((symb.bitfield & zCPar_Symbol_bitfield_type) == zPAR_TYPE_STRING) {
// Either zString or zString* on stack
var string str; str = "";
if (stackValue) {
if (stackValue == zString__vtbl) {
str = MEM_ReadString(ESP+stackOffset);
stackOffset += sizeof_zString-4;
} else if (MEM_ReadInt(stackValue) == zString__vtbl) {
str = MEM_ReadString(stackValue);
};
};
MEM_PushStringParam(str);
} else if ((symb.bitfield & zCPar_Symbol_bitfield_type) == zPAR_TYPE_INSTANCE) {
// Either symbol index or pointer on stack
if (stackValue > 0) && (stackValue < currSymbolTableLength) { // Exclude yINSTANCE_HELP
var zCPar_Symbol symb2; symb2 = _^(MEM_GetSymbolByIndex(stackValue));
if ((symb2.bitfield & zCPar_Symbol_bitfield_type) == zPAR_TYPE_INSTANCE) {
stackValue = symb2.offset;
};
};
symb.offset = stackValue;
MEM_PushInstParam(funcID+1+j);
} else {
// Otherwise push value directly (integer/float/...)
MEM_PushIntParam(stackValue);
};
end;
// Call the function
MEM_CallByID(funcID);
// Assign EAX from return value
if (fncSymb.offset) && (MEM_Parser.datastack_sptr > 0) {
if (fncSymb.offset == (zPAR_TYPE_INT >> 12)) || (fncSymb.offset == (zPAR_TYPE_FLOAT >> 12)) {
// Safety checks on stack integrity
if (MEM_Parser.datastack_sptr >= 2) {
var int sPtr; sPtr = MEM_Parser.datastack_sptr; // Stack pointer is constantly changing so copy it
var int tok; tok = contentParserAddress + zCParser_datastack_stack_offset + (sPtr-1)*4;
if (MEM_ReadInt(tok) == zPAR_TOK_PUSHINT) || (MEM_ReadInt(tok) == zPAR_TOK_PUSHVAR) {
// There is indeed a valid return value
EAX = MEM_PopIntResult();
};
};
} else {
// Strings are not supported, because we would need a unique string for each hook. Who frees the memory?
// Instances are not supported, because they are ambiguous: Return a pointer or a symbol ID?
// Since EAX is a 32-bit register, any non-simple data-type should be manually returned as a pointer
MEM_Error("HOOKENGINE: Only integer/float return values are supported. Return a pointer if necessary.");
// No need to clean up the stack here
};
};
// Restore global instances in between function calls
if (!HookOverwriteInstances) {
MEM_AssignInstSuppressNullWarning = TRUE;
self = _^(selfBak);
other = _^(otherBak);
item = _^(itemBak);
MEM_AssignInstSuppressNullWarning = FALSE;
};
MEM_WriteInt(instHlpAddr, iHlpBak);
MEM_SetUseInstance(instBak);
// Stack registers should be kept read-only in between function calls
ESP = _esp; // Stack pointer is read-only
end;
// Update modifiable registers on stack (ESP points to the position before pushad)
MEM_WriteInt(ESP-40, EDI);
MEM_WriteInt(ESP-36, ESI);
MEM_WriteInt(ESP-32, EBP);
MEM_WriteInt(ESP-24, EBX);
MEM_WriteInt(ESP-20, EDX);
MEM_WriteInt(ESP-16, ECX);
MEM_WriteInt(ESP-12, EAX);
// Restore register variables for recursive hooks
EDI = ediBak;
ESI = esiBak;
EBP = ebpBak;
ESP = espBak;
EBX = ebxBak;
EDX = edxBak;
ECX = ecxBak;
EAX = eaxBak;
// Just to be safe: restore again at the very end of the function
MEM_SetUseInstance(instBak);
};
//-------------------------
// Hash table of all hooks
//-------------------------
const int _Hook_htbl = 0;
//========================================
// Engine hook
//========================================
func void HookEngineI(var int address, var int oldInstr, var int function) {
var int SymbID; // Symbol index of 'function'
var int ptr; // Pointer to temporary memory of the old instruction
var int relAdr; // Relative address from 'address' to new assembly code
var int absAdr; // Absolute address
// ----- Safety checks -----
if (oldInstr < 5) {
PrintDebug("HOOKENGINE: oldInstr is too small. The minimum required length is 5 bytes.");
return;
};
SymbID = function;
if (SymbID == -1) {
PrintDebug("HOOKENGINE: The provided deadalus function was not found.");
return;
};
// ----- Find event handler in hash table -----
if (!_Hook_htbl) {
_Hook_htbl = _HT_Create();
};
// ----- Hook already present -----
if (_HT_Has(_Hook_htbl, address)) {
// Add deadalus function (new listener) to event handler once
MEM_PushIntParam(_HT_Get(_Hook_htbl, address));
MEM_PushIntParam(SymbID);
MEM_Call(EventPtr_AddOnceI); // EventPtr_AddOnceI(_HT_Get(_Hook_htbl, address), SymbID);
return;
};
// ----- Create event and add function as listener -----
MEM_Call(EventPtr_Create);
var int ev; ev = MEM_PopIntResult(); // var int ev; ev = Event_Create();
MEM_PushIntParam(ev);
MEM_PushIntParam(SymbID);
MEM_Call(EventPtr_AddI); // EventPtr_AddI(ev, SymbID);
_HT_Insert(_Hook_htbl, ev, address);
// ----- Backup old instruction -----
ptr = MEM_Alloc(oldInstr);
MEM_CopyBytes(address, ptr, oldInstr);
// ----- Allocate new stream for assembly code -----
ASM_Open(129 + oldInstr + 6 + 1); // Asm code + oldInstr + retn + 1
// ----- Treat possibly protected memory -----
MemoryProtectionOverride(address, oldInstr+3);
// ----- Add jump from engine function to new code -----
relAdr = ASMINT_CurrRun-address-5;
MEM_WriteByte(address + 0, ASMINT_OP_jmp);
MEM_WriteInt (address + 1, relAdr);
// ----- Write new assembly code -----
// Set up stack and backup general purpose registers
ASM_2(ASMINT_OP_subESPplus); ASM_1(8);
ASM_1(ASMINT_OP_pusha); // ESP -= 32 (8*4)
// Increase pushed ESP to correct it for use within Daedalus hook
ASM_2(ASMINT_OP_movESPtoEAX);
ASM_2(ASMINT_OP_addImToEAX); ASM_1(32+8); // 32 bytes popa, 8 bytes data stack backup
ASM_3(ASMINT_OP_movEAXtoESPplus); ASM_1(12); // Current stack position of pushed ESP
// Allocate memory for backing up Daedalus data stack for hooking external engine functions
ASM_1(ASMINT_OP_pushIm); ASM_4(MEMINT_SwitchG1G2(1024, 2048) * 4); // Data stack size
ASM_1(ASMINT_OP_call); ASM_4(malloc_adr-ASM_Here()-4);
ASM_2(ASMINT_OP_addImToESP); ASM_1(4); // Clean up 1 parameter from stack
ASM_3(ASMINT_OP_movEAXtoESPplus); ASM_1(32+4); // Save pointer on the stack
// Backup Daedalus data stack
ASM_1(ASMINT_OP_pushIm); ASM_4(MEMINT_SwitchG1G2(1024, 2048) * 4);
ASM_1(ASMINT_OP_pushIm); ASM_4(ContentParserAddress+zCParser_datastack_stack_offset);
ASM_1(ASMINT_OP_pushEAX);
ASM_1(ASMINT_OP_call); ASM_4(memcpy_adr-ASM_Here()-4);
ASM_2(ASMINT_OP_addImToESP); ASM_1(12); // Clean up 3 parameters from stack
// Backup Daedalus data stack "pointer"
ASM_1(ASMINT_OP_movMemToEAX); ASM_4(ContentParserAddress+zCParser_datastack_sptr_offset);
ASM_3(ASMINT_OP_movEAXtoESPplus); ASM_1(32); // Save pointer on the stack
// Call deadalus hook function
ASM_1(ASMINT_OP_pushIm); ASM_4(ev);
ASM_1(ASMINT_OP_pushIm); ASM_4(MEM_GetFuncID(_Hook));
ASM_1(ASMINT_OP_pushIm); ASM_4(parser);
ASM_1(ASMINT_OP_call); ASM_4(zParser__CallFunc-ASM_Here()-4);
ASM_2(ASMINT_OP_addImToESP); ASM_1(12); // 3*4: parser, _Hook, address
// Restore Daedalus stack "pointer"
ASM_3(ASMINT_OP_movESPplusToEAX); ASM_1(32);
ASM_2(ASMINT_OP_movEAXToMem); ASM_4(ContentParserAddress+zCParser_datastack_sptr_offset);
// Restore Daedalus stack
ASM_1(ASMINT_OP_pushIm); ASM_4(MEMINT_SwitchG1G2(1024, 2048) * 4);
ASM_3(ASMINT_OP_pushESPplus); ASM_1(32+4+4); // Current stack position of memory pointer
ASM_1(ASMINT_OP_pushIm); ASM_4(ContentParserAddress+zCParser_datastack_stack_offset);
ASM_1(ASMINT_OP_call); ASM_4(memcpy_adr-ASM_Here()-4);
ASM_2(ASMINT_OP_addImToESP); ASM_1(12); // Clean up 3 parameters from stack
// Free memory
ASM_3(ASMINT_OP_pushESPplus); ASM_1(32+4); // Current stack position of memory pointer
ASM_1(ASMINT_OP_call); ASM_4(free_adr-ASM_Here()-4);
ASM_2(ASMINT_OP_addImToESP); ASM_1(4); // Clean up 1 parameter from stack
// Clean up stack and pop altered registers
ASM_1(ASMINT_OP_popa);
ASM_2(ASMINT_OP_addImToESP); ASM_1(8);
// Resolve relative jump address of a possible third party hook at the same address
if (MEM_ReadByte(ptr) == ASMINT_OP_jmp) || (MEM_ReadByte(ptr) == ASMINT_OP_call) {
relAdr = MEM_ReadInt(ptr+1); // Relative jump from old address
absAdr = relAdr+5+address;
relAdr = absAdr-ASM_Here()-5; // Relative jump from new address
MEM_WriteInt(ptr+1, relAdr);
};
// Append original instruction
MEM_CopyBytes(ptr, ASMINT_Cursor, oldInstr);
MEM_Free(ptr);
ASMINT_Cursor += oldInstr;
// Return to engine function
ASM_1(ASMINT_OP_pushIm); ASM_4(address + oldInstr);
ASM_1(ASMINT_OP_retn);
var int i; i = ASM_Close();
};
func void HookEngineF(var int address, var int oldInstr, var func function) {
HookEngineI(address, oldInstr, MEM_GetFuncID(function));
};
func void HookEngine(var int address, var int oldInstr, var string function) {
HookEngineI(address, oldInstr, MEM_FindParserSymbol(STR_Upper(function)));
};
// Wrapper function for naming consistency
func void HookEngineS(var int address, var int oldInstr, var string function) {
HookEngine(address, oldInstr, function);
};
//========================================
// Check if address is hooked
//========================================
func int IsHooked(var int address) {
if (!_Hook_htbl) {
return FALSE;
};
return _HT_Has(_Hook_htbl, address);
};
//========================================
// Check if function hooks engine
//========================================
func int IsHookI(var int address, var int function) {
if (!IsHooked(address)) {
return FALSE;
};
var int ev; ev = _HT_Get(_Hook_htbl, address);
var int SymbID; SymbID = function;
// Check if listener exists
MEM_PushIntParam(ev);
MEM_PushIntParam(SymbID);
MEM_Call(EventPtr_HasI); // EventPtr_HasI(ev, SymbID)
return MEM_PopIntResult(); // This line is redundant (left here for readability)
};
func int IsHookF(var int address, var func function) {
return IsHookI(address, MEM_GetFuncID(function));
};
func int IsHook(var int address, var string function) {
return IsHookI(address, MEM_FindParserSymbol(STR_Upper(function)));
};
//========================================
// Remove listener (and engine hook)
//========================================
func void RemoveHookI(var int address, var int oldInstr, var int function) {
if (!IsHookI(address, function)) {
return;
};
var int ev; ev = _HT_Get(_Hook_htbl, address);
var int SymbID; SymbID = function;
// Remove listener
MEM_PushIntParam(ev);
MEM_PushIntParam(SymbID);
MEM_Call(EventPtr_RemoveI); // EventPtr_RemoveI(ev, SymbID)
// Is event empty now?
MEM_PushIntParam(ev);
MEM_Call(EventPtr_Empty); // EventPtr_Empty(ev);
if (MEM_PopIntResult()) && (oldInstr >= 5) {
/* If RemoveHookI is called with oldInstr == 0, the hook (ASM jump) remains.
* This is good for adding and removing a listener/hook frequently.
*/
// Remove hook from hash table
_HT_Remove(_Hook_htbl, address);
// Delete event
MEM_PushIntParam(ev);
MEM_Call(EventPtr_Delete); // EventPtr_Delete(ev);
// Check integrity of opcode at address (expecting jump)
if (MEM_ReadByte(address) != ASMINT_OP_jmp) {
MEM_Error("HOOKENGINE: Hook was invalidated by overwritten opcode");
return;
};
// Remove engine hook
var int newCodeAddr; newCodeAddr = MEM_ReadInt(address+1)+address+5;
var int rvtCodeAddr; rvtCodeAddr = newCodeAddr+129; // Original code
var int relAdr; var int absAdr;
// Revert relative jump address (see above)
if (MEM_ReadByte(rvtCodeAddr) == ASMINT_OP_jmp) || (MEM_ReadByte(rvtCodeAddr) == ASMINT_OP_call) {
relAdr = MEM_ReadInt(rvtCodeAddr+1); // Relative jump from new address
absAdr = relAdr+5+rvtCodeAddr;
relAdr = absAdr-address-5; // Relative jump from old address (reconstruct original jump)
MEM_WriteInt(rvtCodeAddr+1, relAdr);
};
// Replace jump with original instruction
MEM_CopyBytes(rvtCodeAddr, address, oldInstr);
// Free previously allocated space in memory (does this work as expected?)
MEM_Free(newCodeAddr);
};
};
func void RemoveHookF(var int address, var int oldInstr, var func function) {
RemoveHookI(address, oldInstr, MEM_GetFuncID(function));
};
func void RemoveHook(var int address, var int oldInstr, var string function) {
RemoveHookI(address, oldInstr, MEM_FindParserSymbol(STR_Upper(function)));
};
//========================================
// Replace Engine Function
//========================================
func void ReplaceEngineFuncI(var int funcAddr, var int thiscall_numParams, var int replaceFunc) {
// Check if already hooked
if (IsHooked(funcAddr)) {
if (!IsHookI(funcAddr, replaceFunc)) {
MEM_Error("Cannot replace/disable engine function: Address already hooked");
};
return;
};
// Write return at beginning of function
MemoryProtectionOverride(funcAddr, 3);
if (thiscall_numParams) {
MEM_WriteByte(funcAddr, /*C2*/ 194); // retn
MEM_WriteByte(funcAddr+1, thiscall_numParams*4);
MEM_WriteByte(funcAddr+2, 0);
} else {
MEM_WriteByte(funcAddr, ASMINT_OP_retn);
};
// Hook on top of return instruction
if (replaceFunc != /*NOFUNC*/ -1) {
HookEngineI(funcAddr, 5, replaceFunc);
};
};
func void ReplaceEngineFuncF(var int funcAddr, var int thiscall_numParams, var func replaceFunc) {
ReplaceEngineFuncI(funcAddr, thiscall_numParams, MEM_GetFuncID(replaceFunc));
};
func void ReplaceEngineFunc(var int funcAddr, var int thiscall_numParams, var string replaceFunc) {
ReplaceEngineFuncI(funcAddr, thiscall_numParams, MEM_FindParserSymbol(STR_Upper(replaceFunc)));
};
// Simple replace functions for return values
func void Hook_ReturnFalse() {
EAX = FALSE;
};
func void Hook_ReturnTrue() {
EAX = TRUE;
};
//========================================
// Disable Engine Function
//========================================
func void DisableEngineFunc(var int funcAddr, var int thiscall_numParams) {
ReplaceEngineFuncI(funcAddr, thiscall_numParams, -1);
};