From dd64e7cd2370852fba80657465aabcdf909ea98e Mon Sep 17 00:00:00 2001 From: Epp-code <7049620+Epp-code@users.noreply.github.com> Date: Sat, 30 Nov 2024 04:29:04 -0800 Subject: [PATCH] Reworked BlueprintHookManager to better-prevent hooks from corrupting the code or each other (#320) --- .../Private/Patching/BlueprintHookManager.cpp | 280 +++++++++++++----- .../Toolkit/KismetBytecodeDisassembler.cpp | 205 +++++++------ .../Public/Patching/BlueprintHookManager.h | 18 +- .../Toolkit/KismetBytecodeDisassembler.h | 13 +- 4 files changed, 335 insertions(+), 181 deletions(-) diff --git a/Mods/SML/Source/SML/Private/Patching/BlueprintHookManager.cpp b/Mods/SML/Source/SML/Private/Patching/BlueprintHookManager.cpp index 76d7791327..20b9ebbe95 100644 --- a/Mods/SML/Source/SML/Private/Patching/BlueprintHookManager.cpp +++ b/Mods/SML/Source/SML/Private/Patching/BlueprintHookManager.cpp @@ -12,7 +12,7 @@ DEFINE_LOG_CATEGORY(LogBlueprintHookManager); #define WRITE_UNALIGNED(Arr, Type, Value) \ Arr.AddUninitialized(sizeof(Type)); \ - FPlatformMemory::WriteUnaligned(&AppendedCode[Arr.Num() - sizeof(Type)], (Type) Value); + FPlatformMemory::WriteUnaligned(&Arr[Arr.Num() - sizeof(Type)], (Type) Value); void UBlueprintHookManager::HandleHookedFunctionCall(FFrame& Stack, int32 HookOffset) { FFunctionHookInfo& FunctionHookInfo = HookedFunctions.FindChecked(Stack.Node); @@ -20,10 +20,10 @@ void UBlueprintHookManager::HandleHookedFunctionCall(FFrame& Stack, int32 HookOf } #if DEBUG_BLUEPRINT_HOOKING -void DebugDumpFunctionScriptCode(UFunction* Function, int32 HookOffset, const FString& Postfix) { - const FString FileLocation = FPaths::RootDir() + FString::Printf(TEXT("BlueprintHookingDebug_%s_%s_at_%d_%s.json"), *Function->GetOuter()->GetName(), *Function->GetName(), HookOffset, *Postfix); +void DebugDumpFunctionScriptCode(UFunction* Function, int32 HookOffset, int32 ResolvedHookOffset, const FString& Postfix) { + const FString FileLocation = FPaths::RootDir() + FString::Printf(TEXT("BlueprintHookingDebug_%s_%s_at_%d_resolved_%d_%s.json"), *Function->GetOuter()->GetName(), *Function->GetName(), HookOffset, ResolvedHookOffset, *Postfix); - FKismetBytecodeDisassemblerJson Disassembler; + FSMLKismetBytecodeDisassembler Disassembler; const TArray> Statements = Disassembler.SerializeFunction(Function); FString OutJsonString; @@ -35,102 +35,200 @@ void DebugDumpFunctionScriptCode(UFunction* Function, int32 HookOffset, const FS } #endif -void UBlueprintHookManager::InstallBlueprintHook(UFunction* Function, int32 HookOffset) { +void UBlueprintHookManager::InstallBlueprintHook(UFunction* Function, const int32 OriginalHookOffset, const int32 ResolvedHookOffset) { TArray& OriginalCode = Function->Script; - checkf(OriginalCode.Num() > HookOffset, TEXT("Invalid hook: HookOffset > Script.Num()")); + fgcheckf(OriginalCode.Num() > ResolvedHookOffset, TEXT("Invalid hook: Resolved HookOffset > Script.Num()")); #if DEBUG_BLUEPRINT_HOOKING - DebugDumpFunctionScriptCode(Function, HookOffset, TEXT("BeforeHook")); + DebugDumpFunctionScriptCode(Function, OriginalHookOffset, ResolvedHookOffset, TEXT("BeforeHook")); #endif - - //Minimum amount of bytes required to insert unconditional jump with code offset - const int32 MinBytesRequired = 1 + sizeof(CodeSkipSizeType); + // Will assert if the resolved hook offset is not at properly-aligned, parseable statement FSMLKismetBytecodeDisassembler Disassembler; - int32 BytesAvailable = 0; - - //Walk over statements until we collect enough bytes for a replacement - //(or until we consumed all statements in the function's code) - while (BytesAvailable < MinBytesRequired && (HookOffset + BytesAvailable) < OriginalCode.Num()) { - const int32 CurrentStatementIndex = HookOffset + BytesAvailable; - int32 OutStatementLength; - - const bool bValid = Disassembler.GetStatementLength(Function, CurrentStatementIndex, OutStatementLength); - checkf(bValid, TEXT("Provided hook offset is not a valid statement index: %d"), HookOffset); - BytesAvailable += OutStatementLength; - } + int32 OutStatementLength; + fgcheckf( + Disassembler.GetStatementLength(Function, ResolvedHookOffset, OutStatementLength), + TEXT("Cannot install a blueprint hook at the requested hook offset, as it is not aligned with the beginning of a statement within the target function.") ); - //Check that we collected enough bytes - if (BytesAvailable < MinBytesRequired) { - //If we are here, it means we consumed all the statements in the function's code - //And still don't have enough space for inserting a jump. In that case, we append additional - //EX_EndOfScript instructions until we have enough place - const int32 BytesToAppend = MinBytesRequired - BytesAvailable; - OriginalCode.AddUninitialized(BytesToAppend); - FPlatformMemory::Memset(&OriginalCode[OriginalCode.Num() - BytesToAppend], EX_EndOfScript, BytesToAppend); - BytesAvailable = MinBytesRequired; + // First we go over the existing code and add UBlueprintHookManager::JumpBytesRequired to all + // the offsets. Afterwards we will move the relevant code and add the jump. Doing it in this + // order just means we don't have to worry about errantly changing the jump offset we will add. + TArray> DisassembledFunction = Disassembler.SerializeFunction(Function); + for (TSharedPtr JsonValue : DisassembledFunction) { + ModifyOffsetsForNewHookOffset(OriginalCode, JsonValue->AsObject(), ResolvedHookOffset); } + //Add enough room to add the jump. This will shift all the post-hookoffset bytes down so adding + //the jump at the resolved offset doesn't overwrite valid instructions and data + OriginalCode.InsertUninitialized(ResolvedHookOffset, UBlueprintHookManager::JumpBytesRequired); + //Generate code required for calling our hook - //We use EX_CallMath for speed since our inserted function doesn't need context, and is fine with being called on CDO TArray AppendedCode; //Make sure hook function is not NULL, otherwise we may experience weird crashes later UFunction* HookCallFunction = UBlueprintHookManager::StaticClass()->FindFunctionByName(TEXT("ExecuteBPHook")); - check(HookCallFunction); + fgcheck(HookCallFunction); //We use EX_CallMath for speed since our inserted function doesn't need context, and is fine with being called on CDO //EX_CallMath requires just UFunction object pointer and argument list AppendedCode.Add(EX_CallMath); WRITE_UNALIGNED(AppendedCode, ScriptPointerType, HookCallFunction); - //Begin writing function parameters - we have just hook offset constant + //Begin writing function parameters - we have just the original hook offset constant. + //The hook function needs this because it stores the user hooks according to their original offset, not the resolved offset. AppendedCode.Add(EX_IntConst); - WRITE_UNALIGNED(AppendedCode, int32, HookOffset); + WRITE_UNALIGNED(AppendedCode, int32, OriginalHookOffset); AppendedCode.Add(EX_EndFunctionParms); - - //Append original code that we replaced earlier with unconditional jump - AppendedCode.AddUninitialized(BytesAvailable); - FPlatformMemory::Memcpy(&AppendedCode[AppendedCode.Num() - BytesAvailable], &OriginalCode[HookOffset], BytesAvailable); - - //Insert jump to original location for running code after hook + //Insert jump to after the resolved hook offset location for running code after hook AppendedCode.Add(EX_Jump); - const int32 JumpDestination = HookOffset + BytesAvailable; + const int32 JumpDestination = ResolvedHookOffset + UBlueprintHookManager::JumpBytesRequired; WRITE_UNALIGNED(AppendedCode, CodeSkipSizeType, JumpDestination); - //Finish generated code with EX_EndOfScript to avoid any surprises - AppendedCode.Add(EX_EndOfScript); - - //Append generated code to the end of the function's original code now const int32 StartOfAppendedCode = OriginalCode.Num(); OriginalCode.Append(AppendedCode); - //Fill space with EX_EndOfScript before replacement for safety - FPlatformMemory::Memset(&OriginalCode[HookOffset], EX_EndOfScript, BytesAvailable); - //Actually insert jump to the start of appended code to original hook location - OriginalCode[HookOffset] = EX_Jump; - FPlatformMemory::WriteUnaligned(&OriginalCode[HookOffset + 1], StartOfAppendedCode); + OriginalCode[ResolvedHookOffset] = EX_Jump; + FPlatformMemory::WriteUnaligned(&OriginalCode[ResolvedHookOffset + 1], StartOfAppendedCode); #if DEBUG_BLUEPRINT_HOOKING - DebugDumpFunctionScriptCode(Function, HookOffset, TEXT("AfterHook")); + DebugDumpFunctionScriptCode(Function, OriginalHookOffset, ResolvedHookOffset, TEXT("AfterHook")); #endif } -int32 UBlueprintHookManager::PreProcessHookOffset(UFunction* Function, int32 HookOffset) { - if (HookOffset == EPredefinedHookOffset::Return) { - //For now Kismet Compiler will always generate only one Return node, so all - //execution paths will end up either with executing it directly or jumping to it - //So we need to hook only in one place to handle all possible execution paths - FSMLKismetBytecodeDisassembler Disassembler; - int32 ReturnOffset; - const bool bIsValid = Disassembler.FindFirstStatementOfType(Function, 0, EX_Return, ReturnOffset); - checkf(bIsValid, TEXT("EX_Return not found for function %s"), *Function->GetPathName()); - return ReturnOffset; +void UBlueprintHookManager::ModifyOffsetsForNewHookOffset(TArray& Script, TSharedPtr Expression, int32 HookOffset) +{ + int32 Opcode; + if (!Expression->TryGetNumberField(TEXT("Opcode"), Opcode)) { + // No opcode means it's not an instruction, so we can just return; + return; + } + + //A computed jump could do anything at runtime - it could jump to before or after the hook, so we have no way of knowing how + //it needs to be modified to work correctly. For now, the only predictable solution is to forbid hooking functions with them. + fgcheckf(Opcode != EX_ComputedJump, TEXT("Cannot hook a blueprint function that contains an EX_ComputedJump instruction. There's no way to guarantee it would not crash at some point.")); + + int32 OpcodeIndex = Expression->GetIntegerField(TEXT("OpcodeIndex")); + int32 IndexAfterOpcode = OpcodeIndex + 1; + + // This switch was created by comparing to the serialization done in FSMLKismetBytecodeDisassembler::SerializeExpression to + // identify which opcodes can jump to offsets and where exactly those jump targets reside in the script. + switch (Opcode) + { + case EX_Jump: + case EX_JumpIfNot: + case EX_Skip: + case EX_PushExecutionFlow: + { + int32 IndexOfCurrentJumpOffset = IndexAfterOpcode; + int32 CurrentJumpOffset = Expression->GetIntegerField(TEXT("Offset")); + if (CurrentJumpOffset > HookOffset) { + FPlatformMemory::WriteUnaligned(&Script[IndexOfCurrentJumpOffset], (CodeSkipSizeType)(CurrentJumpOffset + UBlueprintHookManager::JumpBytesRequired)); + } + break; + } + case EX_ClassContext: + case EX_Context: + case EX_Context_FailSilent: + { + // Context instructions' SkipOffsetForNull are not absolute - they are relative to the opcode index of the enclosed Expression. For all + // known examples, they also just point to right after the Context because the intent is the value tells the code where to continue + // executing when the Context is null/invalid, and that is quite naturally going to be the next statement. But there's nothing preventing + // the offset from jumping to anywhere, so we'll handle the general case to be future-proof. + // + // SkipOffsetForNull only needs to be adjusted if the inserting hook offset is between the Context instruction and the absolute address + // of the jump offset, because inserting the hook pushes them away from each other. + + int32 CurrentSkipOffset = Expression->GetIntegerField(TEXT("SkipOffsetForNull")); + int32 AbsoluteJumpOffset = Expression->GetObjectField(TEXT("Expression"))->GetIntegerField(TEXT("OpcodeIndex")) + CurrentSkipOffset; + + int32 JumpAdjustment = 0; + // Jumping backwards to before the Context instruction and we're hooking at/after the absolute jump offset and at/before the context instruction + if (AbsoluteJumpOffset < OpcodeIndex && HookOffset >= AbsoluteJumpOffset && HookOffset <= OpcodeIndex) + { + // Jump an extra jump instruction backwards + JumpAdjustment = -UBlueprintHookManager::JumpBytesRequired; + } + // Jumping forwards to after the Context instruction and we're hooking at/after the next statement and BEFORE the absolute jump offset + else if (AbsoluteJumpOffset > OpcodeIndex && HookOffset > OpcodeIndex && HookOffset < AbsoluteJumpOffset) + { + // Jump an extra jump instruction forwards + JumpAdjustment = UBlueprintHookManager::JumpBytesRequired; + } + + if (JumpAdjustment != 0) { + // The offset is just past the Context object, so we have to get its size so we can write to the correct place + TSharedPtr Context = Expression->GetObjectField(TEXT("Context")); + int32 SizeOfContext = Context->GetIntegerField(TEXT("OpSizeInBytes")); + int32 IndexOfCurrentSkipOffset = IndexAfterOpcode + SizeOfContext; + FPlatformMemory::WriteUnaligned(&Script[IndexOfCurrentSkipOffset], (CodeSkipSizeType)(CurrentSkipOffset + JumpAdjustment)); + } + break; + } + case EX_SwitchValue: + { + int32 CurrentJumpOffset = Expression->GetIntegerField(TEXT("OffsetToSwitchEnd")); + if (CurrentJumpOffset > HookOffset) { + // The switch end offset is just past the a single Word field holding the number of cases in the switch + int32 IndexOfCurrentJumpOffset = IndexAfterOpcode + sizeof(uint16); + FPlatformMemory::WriteUnaligned(&Script[IndexOfCurrentJumpOffset], (CodeSkipSizeType)(CurrentJumpOffset + UBlueprintHookManager::JumpBytesRequired)); + IndexOfCurrentJumpOffset += sizeof(CodeSkipSizeType); // Move past the offset we just wrote + int32 SwitchOpSizeInBytes = Expression->GetObjectField(TEXT("Expression"))->GetIntegerField(TEXT("OpSizeInBytes")); + IndexOfCurrentJumpOffset += SwitchOpSizeInBytes; // Move past the switch expression + // Each case in the switch has an absolute offset reference to the next case, so we have to adjust each of them, as well + TArray> Cases = Expression->GetArrayField(TEXT("Cases")); + for (TSharedPtr& Case : Cases) { + TSharedPtr NextCase = Case->AsObject(); + int32 CaseValueSizeInBytes = NextCase->GetObjectField(TEXT("CaseValue"))->GetIntegerField(TEXT("OpSizeInBytes")); + IndexOfCurrentJumpOffset += CaseValueSizeInBytes; + int32 OffsetToNextCase = NextCase->GetIntegerField(TEXT("OffsetToNextCase")); + FPlatformMemory::WriteUnaligned(&Script[IndexOfCurrentJumpOffset], (CodeSkipSizeType)(OffsetToNextCase + UBlueprintHookManager::JumpBytesRequired)); + IndexOfCurrentJumpOffset += sizeof(CodeSkipSizeType); + int32 CaseResultSizeInBytes = NextCase->GetObjectField(TEXT("CaseResult"))->GetIntegerField(TEXT("OpSizeInBytes")); + IndexOfCurrentJumpOffset += CaseResultSizeInBytes; + } + } + break; + } + case EX_AutoRtfmTransact: + { + int32 CurrentJumpOffset = Expression->GetIntegerField(TEXT("Offset")); + if (CurrentJumpOffset > HookOffset) { + // The offset is beyond a single TransactionId int + int32 IndexOfCurrentJumpOffset = IndexAfterOpcode + sizeof(int32); + FPlatformMemory::WriteUnaligned(&Script[IndexOfCurrentJumpOffset], (CodeSkipSizeType)(CurrentJumpOffset + UBlueprintHookManager::JumpBytesRequired)); + } + break; + } + case EX_SkipOffsetConst: + { + int32 CurrentJumpOffset = Expression->GetIntegerField(TEXT("Value")); + if (CurrentJumpOffset > HookOffset) { + int32 IndexOfCurrentJumpOffset = IndexAfterOpcode; + FPlatformMemory::WriteUnaligned(&Script[IndexOfCurrentJumpOffset], (CodeSkipSizeType)(CurrentJumpOffset + UBlueprintHookManager::JumpBytesRequired)); + } + break; + } + } + + // Now we search all the children of this node for jump instructions that need to be updated + for (auto& Pair : Expression->Values) { + TSharedPtr* ExpressionValue; + if (Pair.Value->TryGetObject(ExpressionValue)) { + ModifyOffsetsForNewHookOffset(Script, *ExpressionValue, HookOffset); + continue; + } + TArray>* ArrayValue; + if (Pair.Value->TryGetArray(ArrayValue)) { + for (TSharedPtr Value : *ArrayValue) { + if (Value->TryGetObject(ExpressionValue)) { + ModifyOffsetsForNewHookOffset(Script, *ExpressionValue, HookOffset); + } + } + } } - return HookOffset; } void FFunctionHookInfo::InvokeBlueprintHook(FFrame& Frame, int32 HookOffset) { @@ -143,23 +241,20 @@ void FFunctionHookInfo::InvokeBlueprintHook(FFrame& Frame, int32 HookOffset) { void FFunctionHookInfo::RecalculateReturnStatementOffset(UFunction* Function) { FSMLKismetBytecodeDisassembler Disassembler; - int32 ReturnInstructionOffset; - Disassembler.FindFirstStatementOfType(Function, 0, EX_Return, ReturnInstructionOffset); - this->ReturnStatementOffset = ReturnInstructionOffset; + ReturnStatementOffset = Disassembler.GetReturnStatementOffset(Function); } -void UBlueprintHookManager::HookBlueprintFunction(UFunction* Function, const TFunction& Hook, int32 HookOffset) { +void UBlueprintHookManager::HookBlueprintFunction(UFunction* Function, const TFunction& Hook, const int32 HookOffset) { #if !WITH_EDITOR - checkf(Function->Script.Num(), TEXT("HookBPFunction: Function provided is not implemented in BP")); - + fgcheckf(Function, TEXT("HookBPFunction: Function provided is null")); + fgcheckf(Function->Script.Num(), TEXT("HookBPFunction: Function provided is not implemented in BP")); + //Make sure to add outer UClass to root set to avoid it being Garbage Collected //Because otherwise after GC script byte code will be reloaded, without our hooks applied UClass* OuterUClass = Function->GetTypedOuter(); - check(OuterUClass); + fgcheck(OuterUClass); HookedClasses.AddUnique(OuterUClass); - - HookOffset = PreProcessHookOffset(Function, HookOffset); - + #if UE_BLUEPRINT_EVENTGRAPH_FASTCALLS if (Function->EventGraphFunction != nullptr) { UE_LOG(LogBlueprintHookManager, Warning, TEXT("Attempt to hook event graph call stub function with fast-call enabled, disabling fast call for that function")); @@ -169,16 +264,47 @@ void UBlueprintHookManager::HookBlueprintFunction(UFunction* Function, const TFu } #endif + FSMLKismetBytecodeDisassembler Disassembler; + bool IsFirstTimeFunctionEverHooked = !HookedFunctions.Contains(Function); FFunctionHookInfo& FunctionHookInfo = HookedFunctions.FindOrAdd(Function); - TArray>& InstalledHooks = FunctionHookInfo.CodeOffsetByHookList.FindOrAdd(HookOffset); + if (IsFirstTimeFunctionEverHooked) + { + FunctionHookInfo.OriginalReturnStatementOffset = Disassembler.GetReturnStatementOffset(Function); + } + + //Each new offset modifies the code but we need to keep track by original offsets because callers cannot know how the code has been modified by other hooks. + int32 StoredHookOffset = HookOffset == EPredefinedHookOffset::Return ? FunctionHookInfo.OriginalReturnStatementOffset : HookOffset; + TArray>& InstalledHooks = FunctionHookInfo.CodeOffsetByHookList.FindOrAdd(StoredHookOffset); if (InstalledHooks.Num() == 0) { - //First time function is hooked at this offset, call InstallBlueprintHook - InstallBlueprintHook(Function, HookOffset); - //Update cached return instruction offset + //First time function is hooked at this requested offset. We need to resolve what the offset actually is. + + int32 ResolvedHookOffset = HookOffset; + if (ResolvedHookOffset == EPredefinedHookOffset::Return) { + //Special case for hooking the return, which has an absolute location that we can directly find. + //For now Kismet Compiler will always generate only one Return node, so all + //execution paths will end up either with executing it directly or jumping to it + //So we need to hook only in one place to handle all possible execution paths + ResolvedHookOffset = Disassembler.GetReturnStatementOffset(Function); + } else { + //Each new offset moves the subsequent code by UBlueprintHookManager::JumpBytesRequired to make room for the hook jump. + //So the resolved offset must be increased by JumpBytesRequired for every hook installed earlier than it in the instruction list. + TArray HookOffsetKeys; + FunctionHookInfo.CodeOffsetByHookList.GetKeys(HookOffsetKeys); + for (int32 ExistingHookedOffset : HookOffsetKeys) { + if (ExistingHookedOffset < HookOffset) { + ResolvedHookOffset += UBlueprintHookManager::JumpBytesRequired; + } + } + } + + InstallBlueprintHook(Function, StoredHookOffset, ResolvedHookOffset); + + //Update cached return instruction offset because we've edited the function and moved instructions around FunctionHookInfo.RecalculateReturnStatementOffset(Function); } //Add provided hook into the array InstalledHooks.Add(Hook); + #endif } diff --git a/Mods/SML/Source/SML/Private/Toolkit/KismetBytecodeDisassembler.cpp b/Mods/SML/Source/SML/Private/Toolkit/KismetBytecodeDisassembler.cpp index 8c002d62e1..cf421d0727 100644 --- a/Mods/SML/Source/SML/Private/Toolkit/KismetBytecodeDisassembler.cpp +++ b/Mods/SML/Source/SML/Private/Toolkit/KismetBytecodeDisassembler.cpp @@ -3,9 +3,12 @@ #include "Toolkit/PropertyTypeHandler.h" TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int32& ScriptIndex) { - EExprToken Opcode = (EExprToken) ReadByte(ScriptIndex); + int32 OpcodeIndex = ScriptIndex; + EExprToken Opcode = (EExprToken)ReadByte(ScriptIndex); TSharedPtr Result = MakeShareable(new FJsonObject()); - + + Result->SetNumberField(TEXT("Opcode"), Opcode); + Result->SetNumberField(TEXT("OpcodeIndex"), OpcodeIndex); switch (Opcode) { case EX_Cast: { @@ -19,8 +22,8 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 Result->SetStringField(TEXT("Inst"), TEXT("Cast")); // A type conversion. uint8 ConversionType = ReadByte(ScriptIndex); - checkf(CastNameTable[ConversionType] != nullptr, TEXT("Unsupported cast type %d"), ConversionType); - + fgcheckf(CastNameTable[ConversionType] != nullptr, TEXT("Unsupported cast type %d"), ConversionType); + Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); break; } @@ -31,14 +34,14 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 TArray> Values; ReadInt(ScriptIndex); //Skip element amount - + while (Script[ScriptIndex] != EX_EndSet) { TSharedPtr Expression = SerializeExpression(ScriptIndex); Values.Add(MakeShareable(new FJsonValueObject(Expression))); } ScriptIndex++; //Skip EX_EndSet Result->SetArrayField(TEXT("Values"), Values); - break; + break; } case EX_SetConst: { @@ -51,7 +54,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 TArray> Values; ReadInt(ScriptIndex); //Skip element amount - + while (Script[ScriptIndex] != EX_EndSetConst) { TSharedPtr Expression = SerializeExpression(ScriptIndex); Values.Add(MakeShareable(new FJsonValueObject(Expression))); @@ -67,11 +70,11 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 TArray> Values; ReadInt(ScriptIndex); //Skip element amount - + while (Script[ScriptIndex] != EX_EndMap) { TSharedPtr KeyExpression = SerializeExpression(ScriptIndex); TSharedPtr ValueExpression = SerializeExpression(ScriptIndex); - + TSharedRef Pair = MakeShareable(new FJsonObject()); Pair->SetObjectField(TEXT("Key"), KeyExpression); Pair->SetObjectField(TEXT("Value"), ValueExpression); @@ -89,19 +92,19 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 FEdGraphPinType KeyPropPinType; FSMLPropertyTypeHelper::ConvertPropertyToPinType(KeyProp, KeyPropPinType); Result->SetObjectField(TEXT("KeyProperty"), FSMLPropertyTypeHelper::SerializeGraphPinType(KeyPropPinType, SelfScope.Get())); - + FProperty* ValProp = ReadPointer(ScriptIndex); FEdGraphPinType ValuePropPinType; FSMLPropertyTypeHelper::ConvertPropertyToPinType(ValProp, ValuePropPinType); Result->SetObjectField(TEXT("ValueProperty"), FSMLPropertyTypeHelper::SerializeGraphPinType(ValuePropPinType, SelfScope.Get())); - + TArray> Values; ReadInt(ScriptIndex); //Skip element amount - + while (Script[ScriptIndex] != EX_EndMapConst) { TSharedPtr KeyExpression = SerializeExpression(ScriptIndex); TSharedPtr ValueExpression = SerializeExpression(ScriptIndex); - + TSharedRef Pair = MakeShareable(new FJsonObject()); Pair->SetObjectField(TEXT("Key"), KeyExpression); Pair->SetObjectField(TEXT("Value"), ValueExpression); @@ -109,13 +112,13 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 } ScriptIndex++; //Skip EX_EndMapConst Result->SetArrayField(TEXT("Values"), Values); - break; + break; } case EX_ObjToInterfaceCast: { Result->SetStringField(TEXT("Inst"), TEXT("ObjToInterfaceCast")); UClass* InterfaceClass = ReadPointer(ScriptIndex); - + Result->SetStringField(TEXT("InterfaceClass"), InterfaceClass->GetPathName()); Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); break; @@ -124,7 +127,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 { Result->SetStringField(TEXT("Inst"), TEXT("CrossInterfaceCast")); UClass* InterfaceClass = ReadPointer(ScriptIndex); - + Result->SetStringField(TEXT("InterfaceClass"), InterfaceClass->GetPathName()); Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); break; @@ -133,7 +136,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 { Result->SetStringField(TEXT("Inst"), TEXT("InterfaceToObjCast")); UClass* ObjectClass = ReadPointer(ScriptIndex); - + Result->SetStringField(TEXT("ObjectClass"), ObjectClass->GetPathName()); Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); break; @@ -152,26 +155,26 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_LetObj: { Result->SetStringField(TEXT("Inst"), TEXT("LetObj")); - + Result->SetObjectField(TEXT("Variable"), SerializeExpression(ScriptIndex)); Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); - break; + break; } case EX_LetWeakObjPtr: { Result->SetStringField(TEXT("Inst"), TEXT("LetWeakObjPtr")); - + Result->SetObjectField(TEXT("Variable"), SerializeExpression(ScriptIndex)); Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); - break; + break; } case EX_LetBool: { Result->SetStringField(TEXT("Inst"), TEXT("LetBool")); - + Result->SetObjectField(TEXT("Variable"), SerializeExpression(ScriptIndex)); Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); - break; + break; } case EX_LetValueOnPersistentFrame: { @@ -183,9 +186,9 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 FEdGraphPinType PropertyType; FSMLPropertyTypeHelper::ConvertPropertyToPinType(Property, PropertyType); Result->SetObjectField(TEXT("PropertyType"), FSMLPropertyTypeHelper::SerializeGraphPinType(PropertyType, SelfScope.Get())); - + Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); - break; + break; } case EX_StructMemberContext: { @@ -196,17 +199,17 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 FSMLPropertyTypeHelper::ConvertPropertyToPinType(Property, PropertyPinType); Result->SetObjectField(TEXT("PropertyType"), FSMLPropertyTypeHelper::SerializeGraphPinType(PropertyPinType, SelfScope.Get())); Result->SetStringField(TEXT("PropertyName"), Property->GetName()); - + Result->SetObjectField(TEXT("StructExpression"), SerializeExpression(ScriptIndex)); break; } case EX_LetDelegate: { Result->SetStringField(TEXT("Inst"), TEXT("LetDelegate")); - + Result->SetObjectField(TEXT("Variable"), SerializeExpression(ScriptIndex)); Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); - break; + break; } case EX_LocalVirtualFunction: { @@ -241,17 +244,17 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_LetMulticastDelegate: { Result->SetStringField(TEXT("Inst"), TEXT("LetMulticastDelegate")); - + Result->SetObjectField(TEXT("Variable"), SerializeExpression(ScriptIndex)); Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); - break; + break; } case EX_ComputedJump: { Result->SetStringField(TEXT("Inst"), TEXT("ComputedJump")); Result->SetObjectField(TEXT("OffsetExpression"), SerializeExpression(ScriptIndex)); - break; + break; } case EX_Jump: @@ -264,11 +267,11 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_LocalVariable: { Result->SetStringField(TEXT("Inst"), TEXT("LocalVariable")); - + FProperty* Property = ReadPointer(ScriptIndex); FEdGraphPinType PropertyPinType; FSMLPropertyTypeHelper::ConvertPropertyToPinType(Property, PropertyPinType); - + Result->SetObjectField(TEXT("VariableType"), FSMLPropertyTypeHelper::SerializeGraphPinType(PropertyPinType, SelfScope.Get())); Result->SetStringField(TEXT("VariableName"), Property->GetName()); break; @@ -276,11 +279,11 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_DefaultVariable: { Result->SetStringField(TEXT("Inst"), TEXT("DefaultVariable")); - + FProperty* Property = ReadPointer(ScriptIndex); FEdGraphPinType PropertyPinType; FSMLPropertyTypeHelper::ConvertPropertyToPinType(Property, PropertyPinType); - + Result->SetObjectField(TEXT("VariableType"), FSMLPropertyTypeHelper::SerializeGraphPinType(PropertyPinType, SelfScope.Get())); Result->SetStringField(TEXT("VariableName"), Property->GetName()); break; @@ -288,11 +291,11 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_InstanceVariable: { Result->SetStringField(TEXT("Inst"), TEXT("InstanceVariable")); - + FProperty* Property = ReadPointer(ScriptIndex); FEdGraphPinType PropertyPinType; FSMLPropertyTypeHelper::ConvertPropertyToPinType(Property, PropertyPinType); - + Result->SetObjectField(TEXT("VariableType"), FSMLPropertyTypeHelper::SerializeGraphPinType(PropertyPinType, SelfScope.Get())); Result->SetStringField(TEXT("VariableName"), Property->GetName()); break; @@ -300,11 +303,11 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_LocalOutVariable: { Result->SetStringField(TEXT("Inst"), TEXT("LocalOutVariable")); - + FProperty* Property = ReadPointer(ScriptIndex); FEdGraphPinType PropertyPinType; FSMLPropertyTypeHelper::ConvertPropertyToPinType(Property, PropertyPinType); - + Result->SetObjectField(TEXT("VariableType"), FSMLPropertyTypeHelper::SerializeGraphPinType(PropertyPinType, SelfScope.Get())); Result->SetStringField(TEXT("VariableName"), Property->GetName()); break; @@ -312,11 +315,11 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_ClassSparseDataVariable: { Result->SetStringField(TEXT("Inst"), TEXT("ClassSparseDataVariable")); - + FProperty* Property = ReadPointer(ScriptIndex); FEdGraphPinType PropertyPinType; FSMLPropertyTypeHelper::ConvertPropertyToPinType(Property, PropertyPinType); - + Result->SetObjectField(TEXT("VariableType"), FSMLPropertyTypeHelper::SerializeGraphPinType(PropertyPinType, SelfScope.Get())); Result->SetStringField(TEXT("VariableName"), Property->GetName()); break; @@ -380,20 +383,20 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_Return: { Result->SetStringField(TEXT("Inst"), TEXT("Return")); - Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); + Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); break; } case EX_CallMath: { Result->SetStringField(TEXT("Inst"), TEXT("CallMath")); - + UFunction* StackNode = ReadPointer(ScriptIndex); Result->SetStringField(TEXT("Function"), StackNode->GetName()); //EX_CallMath will never have EX_Context instructions because they don't need any context, //And because of that we need to record context type manually. It should always be a native class though. UClass* MemberParentClass = StackNode->GetOuterUClass(); - check(MemberParentClass->HasAllClassFlags(CLASS_Native)); + fgcheck(MemberParentClass->HasAllClassFlags(CLASS_Native)); Result->SetStringField(TEXT("ContextClass"), MemberParentClass->GetPathName()); @@ -427,15 +430,15 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 UFunction* StackNode = ReadPointer(ScriptIndex); UClass* DelegateSignatureParent = StackNode->GetOuterUClass(); const bool bIsSelfContext = DelegateSignatureParent == SelfScope; - + TSharedPtr DelegateSignatureFunction = MakeShareable(new FJsonObject()); - + DelegateSignatureFunction->SetBoolField(TEXT("IsSelfContext"), bIsSelfContext); DelegateSignatureFunction->SetStringField("MemberParent", DelegateSignatureParent->GetPathName()); DelegateSignatureFunction->SetStringField(TEXT("MemberName"), StackNode->GetName()); - + Result->SetObjectField(TEXT("DelegateSignatureFunction"), DelegateSignatureFunction); - + Result->SetObjectField(TEXT("Delegate"), SerializeExpression(ScriptIndex)); TArray> Parameters; @@ -479,7 +482,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 // Code offset for NULL expressions CodeSkipSizeType SkipCount = ReadSkipCount(ScriptIndex); Result->SetNumberField(TEXT("SkipOffsetForNull"), SkipCount); - + // Property corresponding to the r-value data, in case the l-value needs to be mem-zero'd FProperty* Field = ReadPointer(ScriptIndex); if (Field) { @@ -488,7 +491,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 Result->SetObjectField(TEXT("RValuePropertyType"), FSMLPropertyTypeHelper::SerializeGraphPinType(FieldPinType, SelfScope.Get())); Result->SetStringField(TEXT("RValuePropertyName"), Field->GetName()); } - + Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); break; } @@ -496,7 +499,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 { int32 ConstValue = ReadInt(ScriptIndex); Result->SetStringField(TEXT("Inst"), TEXT("IntConst")); - Result->SetNumberField(TEXT("Value"), ConstValue); + Result->SetNumberField(TEXT("Value"), ConstValue); break; } case EX_Int64Const: @@ -517,14 +520,14 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 { CodeSkipSizeType ConstValue = ReadSkipCount(ScriptIndex); Result->SetStringField(TEXT("Inst"), TEXT("SkipOffsetConst")); - Result->SetNumberField(TEXT("Value"), ConstValue); + Result->SetNumberField(TEXT("Value"), ConstValue); break; } case EX_FloatConst: { float ConstValue = ReadFloat(ScriptIndex); Result->SetStringField(TEXT("Inst"), TEXT("FloatConst")); - Result->SetNumberField(TEXT("Value"), ConstValue); + Result->SetNumberField(TEXT("Value"), ConstValue); break; } case EX_DoubleConst: @@ -538,14 +541,14 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 { FString ConstValue = ReadString8(ScriptIndex); Result->SetStringField(TEXT("Inst"), TEXT("StringConst")); - Result->SetStringField(TEXT("Value"), ConstValue); + Result->SetStringField(TEXT("Value"), ConstValue); break; } case EX_UnicodeStringConst: { FString ConstValue = ReadString16(ScriptIndex); Result->SetStringField(TEXT("Inst"), TEXT("UnicodeStringConst")); - Result->SetStringField(TEXT("Value"), ConstValue); + Result->SetStringField(TEXT("Value"), ConstValue); break; } case EX_TextConst: @@ -589,7 +592,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EBlueprintTextLiteralType::StringTableEntry: { Result->SetStringField(TEXT("TextLiteralType"), TEXT("StringTableEntry")); - + ReadPointer(ScriptIndex); // String Table asset (if any) const FString TableIdString = ReadString(ScriptIndex); const FString KeyString = ReadString(ScriptIndex); @@ -599,7 +602,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 break; } default: - checkf(false, TEXT("Unknown EBlueprintTextLiteralType! Please update FKismetBytecodeDisassembler::ProcessCommon to handle this type of text.")); + fgcheckf(false, TEXT("Unknown EBlueprintTextLiteralType! Please update FSMLKismetBytecodeDisassembler::SerializeExpression to handle this type of text.")); break; } break; @@ -643,7 +646,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 double Pitch = ReadDouble(ScriptIndex); double Yaw = ReadDouble(ScriptIndex); double Roll = ReadDouble(ScriptIndex); - + Result->SetNumberField(TEXT("Pitch"), Pitch); Result->SetNumberField(TEXT("Yaw"), Yaw); Result->SetNumberField(TEXT("Roll"), Roll); @@ -676,7 +679,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_TransformConst: { Result->SetStringField(TEXT("Inst"), TEXT("TransformConst")); - + double RotX = ReadDouble(ScriptIndex); double RotY = ReadDouble(ScriptIndex); double RotZ = ReadDouble(ScriptIndex); @@ -708,7 +711,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 Scale->SetNumberField(TEXT("Y"), ScaleY); Scale->SetNumberField(TEXT("Z"), ScaleZ); Result->SetObjectField(TEXT("Scale"), Scale); - + break; } case EX_StructConst: @@ -716,7 +719,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 Result->SetStringField(TEXT("Inst"), TEXT("StructConst")); UScriptStruct* Struct = ReadPointer(ScriptIndex); Result->SetStringField(TEXT("Struct"), Struct->GetPathName()); - + ReadInt(ScriptIndex); //Skip serialized structure size (not particularly useful really) // TODO: Change this once structs/classes can be declared as explicitly editor only @@ -747,15 +750,15 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 { Result->SetStringField(TEXT("Inst"), TEXT("SetArray")); Result->SetObjectField(TEXT("LeftSideExpression"), SerializeExpression(ScriptIndex)); - - TArray> Values; - while(Script[ScriptIndex] != EX_EndArray) { - TSharedPtr Value = SerializeExpression(ScriptIndex); - Values.Add(MakeShareable(new FJsonValueObject(Value))); - } + + TArray> Values; + while (Script[ScriptIndex] != EX_EndArray) { + TSharedPtr Value = SerializeExpression(ScriptIndex); + Values.Add(MakeShareable(new FJsonValueObject(Value))); + } ScriptIndex++; //Skip over EX_EndArray Result->SetArrayField(TEXT("Values"), Values); - break; + break; } case EX_ArrayConst: { @@ -768,7 +771,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 TArray> Values; ReadInt(ScriptIndex); //Skip element amount - + while (Script[ScriptIndex] != EX_EndArrayConst) { TSharedPtr Expression = SerializeExpression(ScriptIndex); Values.Add(MakeShareable(new FJsonValueObject(Expression))); @@ -807,7 +810,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 UClass* Class = ReadPointer(ScriptIndex); Result->SetStringField(TEXT("Class"), Class->GetPathName()); - Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); + Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); break; } case EX_DynamicCast: @@ -957,7 +960,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 Result->SetStringField(TEXT("EventType"), TEXT("TunnelEndOfThread")); break; default: - checkf(0, TEXT("Unhandled instrumentation event type: %d"), EventType); + fgcheckf(0, TEXT("Unhandled instrumentation event type: %d"), EventType); break; } break; @@ -970,19 +973,19 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_SwitchValue: { Result->SetStringField(TEXT("Inst"), TEXT("SwitchValue")); - + const uint16 NumCases = ReadWord(ScriptIndex); const CodeSkipSizeType AfterSkip = ReadSkipCount(ScriptIndex); Result->SetObjectField(TEXT("Expression"), SerializeExpression(ScriptIndex)); Result->SetNumberField(TEXT("OffsetToSwitchEnd"), AfterSkip); - + TArray> Cases; for (uint16 CaseIndex = 0; CaseIndex < NumCases; ++CaseIndex) { TSharedPtr CaseObject = MakeShareable(new FJsonObject()); CaseObject->SetObjectField(TEXT("CaseValue"), SerializeExpression(ScriptIndex)); const CodeSkipSizeType OffsetToNextCase = ReadSkipCount(ScriptIndex); - + CaseObject->SetNumberField(TEXT("OffsetToNextCase"), OffsetToNextCase); CaseObject->SetObjectField(TEXT("CaseResult"), SerializeExpression(ScriptIndex)); Cases.Add(MakeShareable(new FJsonValueObject(CaseObject))); @@ -1002,20 +1005,20 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 { Result->SetStringField(TEXT("Inst"), TEXT("AutoRtfmTransact")); // Code offset. - int32 TransactionId = ReadInt(ScriptIndex); + int32 TransactionId = ReadInt(ScriptIndex); CodeSkipSizeType SkipCount = ReadSkipCount(ScriptIndex); Result->SetNumberField(TEXT("TransactionId"), TransactionId); Result->SetNumberField(TEXT("Offset"), SkipCount); - + TArray> Params; ReadInt(ScriptIndex); //Skip element amount - + while (Script[ScriptIndex] != EX_AutoRtfmStopTransact) { TSharedPtr Expression = SerializeExpression(ScriptIndex); Params.Add(MakeShareable(new FJsonValueObject(Expression))); } - + Result->SetArrayField(TEXT("Params"), Params); Result->SetObjectField(TEXT("End"), SerializeExpression(ScriptIndex)); @@ -1024,7 +1027,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case EX_AutoRtfmStopTransact: { Result->SetStringField(TEXT("Inst"), TEXT("AutoRtfmStopTransact")); - + int32 TransactionId = ReadInt(ScriptIndex); Result->SetNumberField(TEXT("TransactionId"), TransactionId); @@ -1038,7 +1041,7 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 case AbortingExit: ModeText = TEXT("AbortingExit"); break; case AbortingExitAndAbortParent: ModeText = TEXT("AbortingExitAndAbortParent"); break; } - + Result->SetStringField(TEXT("EndMode"), ModeText); break; } @@ -1050,16 +1053,19 @@ TSharedPtr FSMLKismetBytecodeDisassembler::SerializeExpression(int3 } default: { - // This should never occur. - checkf(0, TEXT("Unknown bytecode 0x%02X"), (uint8) Opcode); + // This should only really occur if the caller has passed an incorrect index that is not the start of an instruction. + fgcheckf(0, TEXT("Unknown bytecode 0x%02X at ScriptIndex %d. Either a new opcode has been added or the supplied index does not contain a valid instruction!"), (uint8)Opcode, ScriptIndex); break; } } + + Result->SetNumberField(TEXT("OpSizeInBytes"), ScriptIndex - OpcodeIndex); //Make sure no instruction identifier is ever missing from returned json object - check(Result->HasField(TEXT("Inst"))); + fgcheck(Result->HasField(TEXT("Inst"))); return Result; } + TArray> FSMLKismetBytecodeDisassembler::SerializeFunction(UStruct* Function) { this->Script = Function->Script; this->SelfScope = Function->GetTypedOuter(); @@ -1068,14 +1074,10 @@ TArray> FSMLKismetBytecodeDisassembler::SerializeFunction int32 ScriptIndex = 0; while (ScriptIndex < Script.Num()) { - const int32 StatementIndex = ScriptIndex; - TSharedPtr StatementObject = SerializeExpression(ScriptIndex); - - //Append statement index because several instructions can jump to statements (but not to separate expressions inside of statements!) - StatementObject->SetNumberField(TEXT("StatementIndex"), StatementIndex); + TSharedPtr StatementObject = SerializeStatement(ScriptIndex); Statements.Add(MakeShareable(new FJsonValueObject(StatementObject))); } - + return Statements; } @@ -1088,7 +1090,7 @@ bool FSMLKismetBytecodeDisassembler::FindFirstStatementOfType(UStruct* Function, const int32 StatementIndex = ScriptIndex; const uint8 StatementOpcode = Script[ScriptIndex]; FString ResultString; - SerializeExpression(ScriptIndex); + SerializeStatement(ScriptIndex); if (StatementOpcode == ExpectedStatementOpcode) { OutStatementIndex = StatementIndex; return true; @@ -1106,7 +1108,7 @@ bool FSMLKismetBytecodeDisassembler::GetStatementLength(UStruct* Function, int32 int32 ScriptIndex = 0; while (ScriptIndex < Script.Num()) { const int32 StatementIndex = ScriptIndex; - SerializeExpression(ScriptIndex); + SerializeStatement(ScriptIndex); if (StatementIndex == ExpectedStatementIndex) { //This is the statement we are looking for, compute difference and return it as length OutStatementLength = ScriptIndex - StatementIndex; @@ -1118,6 +1120,21 @@ bool FSMLKismetBytecodeDisassembler::GetStatementLength(UStruct* Function, int32 return false; } +int32 FSMLKismetBytecodeDisassembler::GetReturnStatementOffset(UFunction* Function) { + int32 ReturnStatementOffset; + //For now Kismet Compiler will always generate exactly one Return node + const bool bIsValid = FindFirstStatementOfType(Function, 0, EX_Return, ReturnStatementOffset); + fgcheckf(bIsValid, TEXT("EX_Return not found for function %s"), *Function->GetPathName()); + return ReturnStatementOffset; +} + +TSharedPtr FSMLKismetBytecodeDisassembler::SerializeStatement(int32& StatementIndex) +{ + const int32 StartingStatementIndex = StatementIndex; + TSharedPtr StatementObject = SerializeExpression(StatementIndex); + StatementObject->SetNumberField(TEXT("StatementIndex"), StartingStatementIndex); + return StatementObject; +} int32 FSMLKismetBytecodeDisassembler::ReadInt(int32& ScriptIndex) { int32 Value = Script[ScriptIndex]; ++ScriptIndex; @@ -1208,11 +1225,11 @@ FString FSMLKismetBytecodeDisassembler::ReadString(int32& ScriptIndex) { switch (Opcode) { case EX_StringConst: - return ReadString8(ScriptIndex); + return ReadString8(ScriptIndex); case EX_UnicodeStringConst: - return ReadString16(ScriptIndex); + return ReadString16(ScriptIndex); default: - checkf(false, TEXT("FKismetBytecodeDisassembler::ReadString - Unexpected opcode. Expected %d or %d, got %d"), (int)EX_StringConst, (int)EX_UnicodeStringConst, (int)Opcode); + fgcheckf(false, TEXT("FKismetBytecodeDisassembler::ReadString - Unexpected opcode. Expected %d or %d, got %d"), (int)EX_StringConst, (int)EX_UnicodeStringConst, (int)Opcode); break; } diff --git a/Mods/SML/Source/SML/Public/Patching/BlueprintHookManager.h b/Mods/SML/Source/SML/Public/Patching/BlueprintHookManager.h index 8192b939d7..240938e2bc 100644 --- a/Mods/SML/Source/SML/Public/Patching/BlueprintHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/BlueprintHookManager.h @@ -15,6 +15,7 @@ struct FFunctionHookInfo { GENERATED_BODY() private: TMap>> CodeOffsetByHookList; + int32 OriginalReturnStatementOffset; int32 ReturnStatementOffset; friend class UBlueprintHookManager; public: @@ -45,20 +46,23 @@ class SML_API UBlueprintHookManager : public UEngineSubsystem { * Multiple hooks bound to one hook offset will be processed in the order they were registered * UClass holding Function will be added to root set to avoid getting Garbage Collected */ - void HookBlueprintFunction(UFunction* Function, const TFunction& Hook, int32 HookOffset); + void HookBlueprintFunction(UFunction* Function, const TFunction& Hook, const int32 HookOffset); private: + //Minimum amount of bytes required to insert unconditional jump with code offset + static const int32 JumpBytesRequired = 1 + sizeof(CodeSkipSizeType); + /** Actually performs bytecode modification to install hook */ - static void InstallBlueprintHook(UFunction* Function, int32 HookOffset); - - /** Does preprocessing to hook offset to handle predefined hook locations */ - static int32 PreProcessHookOffset(UFunction* Function, int32 HookOffset); - + static void InstallBlueprintHook(UFunction* Function, const int32 OriginalHookOffset, const int32 ResolvedHookOffset); + + /** Called by InstallBlueprintHook to modify the bytecode based on the desired hookoffset **/ + static void ModifyOffsetsForNewHookOffset(TArray& Script, TSharedPtr Expression, int32 HookOffset); + /** Called when hook is executed */ void HandleHookedFunctionCall(FFrame& Frame, int32 HookOffset); /** This function is just a stub for UHT to generate reflection data, it is not actually implemented. */ UFUNCTION(BlueprintInternalUseOnly, CustomThunk) - static void ExecuteBPHook(int32 HookOffset) { check(0); }; + static void ExecuteBPHook(int32 HookOffset) { fgcheck(0); }; DECLARE_FUNCTION(execExecuteBPHook) { //StepCompiledIn is not used here since this function cannot be called from BP directly, it can only diff --git a/Mods/SML/Source/SML/Public/Toolkit/KismetBytecodeDisassembler.h b/Mods/SML/Source/SML/Public/Toolkit/KismetBytecodeDisassembler.h index 91ca132cbe..6cd29d48ff 100644 --- a/Mods/SML/Source/SML/Public/Toolkit/KismetBytecodeDisassembler.h +++ b/Mods/SML/Source/SML/Public/Toolkit/KismetBytecodeDisassembler.h @@ -5,21 +5,28 @@ class SML_API FSMLKismetBytecodeDisassembler { public: - /** Converts a single expression into json object */ - TSharedPtr SerializeExpression(int32& ScriptIndex); - /** Parses a block of statements until it hits return */ TArray> SerializeFunction(UStruct* Function); /** Computes length of the statement in bytes and returns it. Returns false if given index does not correspond to any statement (e.g if it is inside of some statement) */ bool GetStatementLength(UStruct* Function, int32 StatementIndex, int32& OutStatementLength); + /** Gets the offset of the return statement. For now Kismet Compiler will always generate only one Return node, so there's always exactly one. Will assert if no return statement is found. */ + int32 GetReturnStatementOffset(UFunction* Function); + /** Returns index of the first statement using given opcode */ bool FindFirstStatementOfType(UStruct* Function, int32 StartIndex, uint8 StatementOpcode, int32& OutStatementIndex); + private: TWeakObjectPtr SelfScope; TArray Script; + /** Internal utility function to convert a single statement into json object, provided Script and SelfScope have been set. Will include a StatementIndex in the returned json */ + TSharedPtr SerializeStatement(int32& StatementIndex); + + /** Converts a single expression into json object, provided Script and SelfScope have been set */ + TSharedPtr SerializeExpression(int32& ScriptIndex); + //Begin script bytecode parsing methods int32 ReadInt(int32& ScriptIndex); uint64 ReadQword(int32& ScriptIndex);