Skip to content

Commit

Permalink
Item Tooltip Subsystem, BP Hooking fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Archengius committed May 13, 2020
1 parent ee513d7 commit bb3e52a
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 23 deletions.
7 changes: 7 additions & 0 deletions Source/SML/SatisfactoryModLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "mod/toolkit/FGAssetDumper.h"
#include "mod/ModSubsystems.h"
#include "player/BuildMenuTweaks.h"
#include "tooltip/ItemTooltipHandler.h"
#include "util/FuncNames.h"

bool checkGameVersion(const long targetVersion) {
Expand Down Expand Up @@ -164,11 +165,13 @@ namespace SML {
SML::Logging::info(TEXT("Resolving mod dependencies"));
modHandlerPtr->checkDependencies();

//C++ hooks can be registered very early in the engine initialization
modHandlerPtr->attachLoadingHooks();
InitializePlayerComponent();
RegisterVersionCheckHooks();
RegisterMainMenuHooks();
FSubsystemInfoHolder::SetupHooks();

SUBSCRIBE_METHOD(WIN_STACK_WALK_GET_DOWNSTREAM_STORAGE_FUNC_DESC, FWindowsPlatformStackWalk::GetDownstreamStorage, [](auto& Call) {
FString OriginalResult = Call();
AppendSymbolSearchPaths(OriginalResult);
Expand Down Expand Up @@ -212,7 +215,11 @@ namespace SML {
modHandlerPtr->loadMods(*bootstrapAccessors);
SML::Logging::info(TEXT("Post Initialization finished!"));
flushDebugSymbols();

//Blueprint hooks are registered here, after engine initialization
GRegisterBuildMenuHooks();
UItemTooltipHandler::GRegisterHooking();

if (getSMLConfig().dumpGameAssets) {
SML::Logging::info(TEXT("Game Asset Dump requested in configuration, performing..."));
SML::dumpSatisfactoryAssets(TEXT("/Game/FactoryGame/"), TEXT("FGBlueprints.json"));
Expand Down
12 changes: 12 additions & 0 deletions Source/SML/mod/actor/SMLInitMod.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "FGSchematicManager.h"
#include "SML/util/Logging.h"
#include "FGResearchManager.h"
#include "tooltip/ItemTooltipHandler.h"

void ASMLInitMod::Init_Implementation() {
}
Expand All @@ -19,6 +20,8 @@ void ASMLInitMod::PreLoadModContent() {
}
}

static TArray<FString> ProviderClassNamesRegistered;

void ASMLInitMod::LoadModContent() {
AFGSchematicManager* schematicManager = AFGSchematicManager::Get(this);
//No need to register AvailableSchematics on client side, they are replicated
Expand Down Expand Up @@ -52,6 +55,15 @@ void ASMLInitMod::LoadModContent() {
ChatCommandSubsystem->RegisterCommand(RegistrarEntry);
}
}
//Register tooltip providers
for (UClass* ProviderClass : GlobalItemTooltipProviders) {
FString ClassName = ProviderClass->GetPathName();
if (!ProviderClassNamesRegistered.Contains(ClassName)) {
ProviderClassNamesRegistered.Add(ClassName);
UObject* ProviderObject = NewObject<UObject>(UItemTooltipHandler::StaticClass(), ProviderClass);
UItemTooltipHandler::RegisterGlobalTooltipProvider(ProviderObject);
}
}
}

void ASMLInitMod::PlayerJoined_Implementation(AFGPlayerController* Player) {
Expand Down
8 changes: 8 additions & 0 deletions Source/SML/mod/actor/SMLInitMod.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,12 @@ class SML_API ASMLInitMod : public AActor {
*/
UPROPERTY(EditDefaultsOnly, Category = Advanced)
TArray<TSubclassOf<UModSubsystemHolder>> mModSubsystems;

/**
* List of classes for objects implementing ISMLItemTooltipProvider
* These will be registered on startup and used to obtain additional description
* text/widget for all items
*/
UPROPERTY(EditDefaultsOnly, Category = Advanced)
TArray<UClass*> GlobalItemTooltipProviders;
};
9 changes: 7 additions & 2 deletions Source/SML/mod/blueprint_hooking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,19 @@ void InstallBlueprintHook(UFunction* Function, const FHookKey& HookKey) {
//basically EX_Jump + CodeSkipSizeType;
const int32 MinBytesRequired = 1 + sizeof(CodeSkipSizeType);
int32 BytesToMove = SML::GetMinInstructionReplaceLength(Function, MinBytesRequired, HookKey.HookOffset);
SML::Logging::info(TEXT("InstallBlueprintHook: Address: "), reinterpret_cast<int64>(Function), TEXT(", Code Size: "), Function->Script.Num());

if (BytesToMove < 0) {
//Not enough bytes in method body to fit jump into, append required amount of bytes and fill them with EX_EndOfScript
const int32 BytesToAppend = -BytesToMove;
OriginalCode.AddUninitialized(BytesToAppend);
FPlatformMemory::Memset(&OriginalCode[OriginalCode.Num() - BytesToMove], EX_EndOfScript, BytesToAppend);
FPlatformMemory::Memset(&OriginalCode[OriginalCode.Num() - BytesToAppend], EX_EndOfScript, BytesToAppend);
//If we are here, that means Script.Num() - HookOffset is less than MinBytesRequired
//So to move instructions properly, we just move MinBytesRequired
BytesToMove = MinBytesRequired;
}
const int32 JumpDestination = HookKey.HookOffset + BytesToMove;
SML::Logging::info(TEXT("InstallBlueprintHook: Address: "), reinterpret_cast<int64>(Function), TEXT(", Code Size: "), Function->Script.Num());
SML::Logging::info(TEXT("InstallBlueprintHook: Verified Code Size: "), OriginalCode.Num());
SML::Logging::info(TEXT("InstallBlueprintHook: Min Bytes: "), MinBytesRequired, TEXT(", Hook Offset: "), HookKey.HookOffset, TEXT(", Jump Destination: "), JumpDestination);

//Generate code to call function & code we stripped by jump
Expand Down Expand Up @@ -104,6 +108,7 @@ int32 PreProcessHookOffset(UFunction* Function, int32 HookOffset) {
const int32 ReturnOffset = SML::FindReturnStatementOffset(Function);
checkf(ReturnOffset != -1, TEXT("EX_Return not found for function"));
SML::Logging::info(TEXT("Return Offset: "), ReturnOffset);
SML::Logging::info(TEXT("Instruction at Offset: "), Function->Script[ReturnOffset], TEXT(", EX_Return: "), EX_Return);
return ReturnOffset;
}
return HookOffset;
Expand Down
18 changes: 18 additions & 0 deletions Source/SML/mod/blueprint_hooking.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ class SML_API FBlueprintHookHelper {
check(Property);
return Property->GetPropertyValuePtr_InContainer(FramePointer.Locals, ArrayIndex);
}

//Retrieves variable passed to method as [out] parameter (pass by reference basically), e.g which
//Can be set by blueprint method and changes will be visible to caller
//These variables are stored separately from other local variables, so
//GetLocalVarPtr won't work on them, you should use this method instead
template<typename T>
typename T::TCppType* GetOutVariablePtr(const TCHAR* VariableName = TEXT("ReturnValue")) {
FOutParmRec* Out = FramePointer.OutParms;
check(Out);
while (Out->Property->GetName() != VariableName) {
Out = Out->NextOutParm;
check(Out);
}
check(Out->Property->GetName() == VariableName);
T* Property = Cast<T>(Out->Property);
check(Property);
return Property->GetPropertyValuePtr(Out->PropAddr);
}
};

typedef void(HookSignature)(FBlueprintHookHelper& HookHelper);
Expand Down
36 changes: 15 additions & 21 deletions Source/SML/mod/toolkit/BPCodeDumper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class FParseFrame {
void* Params;
UFunction* Func;
FlowStackType FlowStack;
bool Return = false;
int32 ubergraphOffset = -1;

FParseFrame(UObject* Context, UFunction* Func) : Context(Context), Code(Func->Script.GetData()), Func(Func) {
Expand Down Expand Up @@ -147,8 +146,7 @@ class FParseFrame {

TArray<TSharedPtr<FJsonValue>> ParseClosure(FParseFrame& Stack, uint8* ReturnValue) {
TArray<TSharedPtr<FJsonValue>> Result;
Stack.Return = false;
while (*Stack.Code != EX_Return && !Stack.Return) {
while (*Stack.Code != EX_Return) {
TSharedPtr<FJsonObject> Inst = Stack.Step(ReturnValue, true);
if (Inst.IsValid()) Result.Add(MakeShareable(new FJsonValueObject(Inst)));
}
Expand Down Expand Up @@ -190,7 +188,6 @@ INSTRUCTION_HANDLER(EX_DefaultVariable)
INSTRUCTION(EX_DefaultVariable)

INSTRUCTION_HANDLER(EX_Return)
Stack.Return = true;
return true;
}
INSTRUCTION(EX_Return)
Expand Down Expand Up @@ -1014,23 +1011,23 @@ int32 SML::GetMinInstructionReplaceLength(UFunction* Function, uint32 BytesRequi
Frame.Code += StartOffset;
const bool bHasReturnParam = Function->ReturnValueOffset != MAX_uint16;
void* ReturnValue = bHasReturnParam ? ((uint8*)Frame.Params + Function->ReturnValueOffset) : nullptr;
Frame.Return = false;
const uint64 CodePointerBase = reinterpret_cast<uint64>(Frame.Code);
while (*Frame.Code != EX_Return && !Frame.Return) {

while (*Frame.Code != EX_Return) {
Frame.Step(ReturnValue, true);
const uint64 CodePointer = reinterpret_cast<uint64>(Frame.Code);
const uint64 Offset = CodePointer - CodePointerBase;
if (Offset >= BytesRequired)
return Offset;
}
//Not enough bytes before return to actually fit MinBytes
//Try to compute largest possible replacement using Script length
//And if it fits, return it.
const uint32 ActuallyBytesHere = Function->Script.Num() - StartOffset;
if (ActuallyBytesHere >= BytesRequired)
return BytesRequired;
const int32 BytesLacking = BytesRequired - ActuallyBytesHere;
return -BytesLacking;
//Not enough bytes before return to actually fit MinBytes before return
//return after it will also have EX_Nothing or EX_LocalOutVariable and then EX_Nothing
//so if we can't fit we should before we replace entire remaining part
const uint32 BytesRemaining = Function->Script.Num() - StartOffset;
if (BytesRemaining >= BytesRequired)
return BytesRemaining; //return full bytes remaining, because after return we have 2 more instructions generated
const int32 BytesLacking = BytesRequired - BytesRemaining;
return -BytesLacking; //Otherwise return how much bytes we lack
}
return -((int32) BytesRequired);
}
Expand All @@ -1041,16 +1038,13 @@ int32 SML::FindReturnStatementOffset(UFunction* Function) {
FParseFrame Frame = FParseFrame(Context, Function);
const bool bHasReturnParam = Function->ReturnValueOffset != MAX_uint16;
void* ReturnValue = bHasReturnParam ? ((uint8*)Frame.Params + Function->ReturnValueOffset) : nullptr;
Frame.Return = false;
const uint64 CodePointerBase = (uint64)Frame.Code;
while (true) {
const uint64 CodePointer = (uint64)Frame.Code;
if (*Frame.Code == EX_Return)
return CodePointer - CodePointerBase;
while (*Frame.Code != EX_Return) {
Frame.Step(ReturnValue, true);
if (Frame.Return)
return CodePointer - CodePointerBase;
}
//Will always point to EX_Return at that point, so just compute offset
const uint64 CodePointer = (uint64)Frame.Code;
return CodePointer - CodePointerBase;
}
return -1;
}
Expand Down
143 changes: 143 additions & 0 deletions Source/SML/tooltip/ItemTooltipHandler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#include "ItemTooltipHandler.h"


#include "TextBlock.h"
#include "WidgetBlueprintLibrary.h"
#include "mod/blueprint_hooking.h"
#include "util/Logging.h"

//Overwrites delegates bound to title & description widgets to use FTooltipHookHelper, add custom item widget
void ApplyItemOverridesToTooltip(UWidget* TooltipWidget, APlayerController* OwningPlayer, const FInventoryStack& InventoryStack) {
//Gather UProperty exposed by tooltip widget
UClass* TooltipWidgetClass = TooltipWidget->GetClass();
UObjectProperty* TitleWidgetProperty = Cast<UObjectProperty>(TooltipWidgetClass->FindPropertyByName(TEXT("mTitle")));
UObjectProperty* DescriptionWidgetProperty = Cast<UObjectProperty>(TooltipWidgetClass->FindPropertyByName(TEXT("mDescription")));
check(TitleWidgetProperty && DescriptionWidgetProperty);

//Retrieve references to some stuff
UTextBlock* NameBlock = Cast<UTextBlock>(TitleWidgetProperty->GetObjectPropertyValue_InContainer(TooltipWidget));
UTextBlock* DescriptionBlock = Cast<UTextBlock>(DescriptionWidgetProperty->GetObjectPropertyValue_InContainer(TooltipWidget));
//Retrieve parent panel, it will hold name, description and recipe blocks
UPanelWidget* ParentPanel = NameBlock->GetParent();

//Spawn custom widget in parent panel and add it
UItemStackContextWidget* ContextWidget = NewObject<UItemStackContextWidget>(ParentPanel);
ContextWidget->InventoryStack = InventoryStack;
ContextWidget->PlayerController = OwningPlayer;
ContextWidget->Visibility = ESlateVisibility::Collapsed;
ParentPanel->AddChild(ContextWidget);
//Rebind text delegates to custom widget
NameBlock->TextDelegate.BindUFunction(ContextWidget, TEXT("GetItemName"));
DescriptionBlock->TextDelegate.BindUFunction(ContextWidget, TEXT("GetItemDescription"));

//Append custom widgets to description
TArray<UUserWidget*> Widgets = UItemTooltipHandler::CreateDescriptionWidgets(OwningPlayer, InventoryStack);
for (UUserWidget* Widget : Widgets) {
ParentPanel->AddChild(Widget);
}
}

FText UItemStackContextWidget::GetItemName() const {
return UItemTooltipHandler::GetItemName(PlayerController, InventoryStack);
}

FText UItemStackContextWidget::GetItemDescription() const {
return UItemTooltipHandler::GetItemDescription(PlayerController, InventoryStack);
}

FInventoryStack GetStackFromSlot(UObject* SlotWidget) {
//Retrieve fields relevant to owner inventory
UObjectProperty* InventoryProperty = Cast<UObjectProperty>(SlotWidget->GetClass()->FindPropertyByName(TEXT("mCachedInventoryComponent")));
UIntProperty* SlotIndexProperty = Cast<UIntProperty>(SlotWidget->GetClass()->FindPropertyByName(TEXT("mSlotIdx")));
check(InventoryProperty && SlotIndexProperty);

FInventoryStack ResultStack{};
//Access inventory if it's not a null pointer
UFGInventoryComponent* InventoryComponent = Cast<UFGInventoryComponent>(InventoryProperty->GetObjectPropertyValue_InContainer(SlotWidget));
if (InventoryComponent) {
const int32 SlotIndex = SlotIndexProperty->GetPropertyValue_InContainer(SlotWidget);
InventoryComponent->GetStackFromIndex(SlotIndex, ResultStack);
}
return ResultStack;
}

static TArray<ISMLItemTooltipProvider*> GlobalTooltipProviders;

void UItemTooltipHandler::GRegisterHooking() {
//Hook into InventorySlot widget to apply tooltip overrides
UClass* InventorySlot = LoadObject<UClass>(NULL, TEXT("/Game/FactoryGame/Interface/UI/InGame/InventorySlots/Widget_InventorySlot.Widget_InventorySlot_C"));
check(InventorySlot);
UFunction* Function = InventorySlot->FindFunctionByName(TEXT("GetTooltipWidget"));
HookBlueprintFunction(Function, [](FBlueprintHookHelper& HookHelper) {
UUserWidget* TooltipWidget = Cast<UUserWidget>(*HookHelper.GetOutVariablePtr<UObjectProperty>());
UUserWidget* SlotWidget = Cast<UUserWidget>(HookHelper.GetContext());
if (TooltipWidget != nullptr) {
APlayerController* OwningPlayer = SlotWidget->GetOwningPlayer();
const FInventoryStack InventoryStack = GetStackFromSlot(SlotWidget);
if (InventoryStack.Item.IsValid()) {
ApplyItemOverridesToTooltip(TooltipWidget, OwningPlayer, InventoryStack);
}
}
}, EPredefinedHookOffset::Return);
}

void UItemTooltipHandler::RegisterGlobalTooltipProvider(UObject* TooltipProvider) {
void* InterfaceAddress = TooltipProvider->GetInterfaceAddress(USMLItemTooltipProvider::StaticClass());
if (InterfaceAddress) {
//Pin UObject implementing interface so it won't be garbage collected
TooltipProvider->AddToRoot();
ISMLItemTooltipProvider* Provider = static_cast<ISMLItemTooltipProvider*>(InterfaceAddress);
GlobalTooltipProviders.Add(Provider);
}
}

FText UItemTooltipHandler::GetItemName(APlayerController* OwningPlayer, const FInventoryStack& InventoryStack) {
UClass* ItemClass = InventoryStack.Item.ItemClass;
UClass* InterfaceClass = USMLItemDisplayInterface::StaticClass();
if (ItemClass->ImplementsInterface(InterfaceClass)) {
UObject* ItemObject = ItemClass->GetDefaultObject();
ISMLItemDisplayInterface* Result = static_cast<ISMLItemDisplayInterface*>(ItemObject->GetInterfaceAddress(InterfaceClass));
return Result->GetItemName(OwningPlayer, InventoryStack);
}
return UFGItemDescriptor::GetItemName(ItemClass);
}

#define APPEND_IF_NOT_EMPTY(expr) { FString _Result = expr; if (!_Result.IsEmpty()) DescriptionText.Add(_Result); }

FText UItemTooltipHandler::GetItemDescription(APlayerController* OwningPlayer, const FInventoryStack& InventoryStack) {
UClass* ItemClass = InventoryStack.Item.ItemClass;
UClass* InterfaceClass = USMLItemDisplayInterface::StaticClass();
TArray<FString> DescriptionText;
APPEND_IF_NOT_EMPTY(UFGItemDescriptor::GetItemDescription(ItemClass).ToString());
if (ItemClass->ImplementsInterface(InterfaceClass)) {
UObject* ItemObject = ItemClass->GetDefaultObject();
ISMLItemDisplayInterface* Result = static_cast<ISMLItemDisplayInterface*>(ItemObject->GetInterfaceAddress(InterfaceClass));
APPEND_IF_NOT_EMPTY(Result->GetItemDescription(OwningPlayer, InventoryStack).ToString());
}
for (ISMLItemTooltipProvider* Provider : GlobalTooltipProviders) {
APPEND_IF_NOT_EMPTY(Provider->GetItemDescription(OwningPlayer, InventoryStack).ToString());
}
return FText::FromString(FString::Join(DescriptionText, TEXT("\n")));
}

TArray<UUserWidget*> UItemTooltipHandler::CreateDescriptionWidgets(APlayerController* OwningPlayer, const FInventoryStack& InventoryStack) {
TArray<UUserWidget*> ResultWidgets;
UClass* ItemClass = InventoryStack.Item.ItemClass;
UClass* InterfaceClass = USMLItemDisplayInterface::StaticClass();
if (ItemClass->ImplementsInterface(InterfaceClass)) {
UObject* ItemObject = ItemClass->GetDefaultObject();
ISMLItemDisplayInterface* Result = static_cast<ISMLItemDisplayInterface*>(ItemObject->GetInterfaceAddress(InterfaceClass));
UUserWidget* NewWidget = Result->CreateDescriptionWidget(OwningPlayer, InventoryStack);
if (NewWidget) {
ResultWidgets.Add(NewWidget);
}
}
for (ISMLItemTooltipProvider* Provider : GlobalTooltipProviders) {
UUserWidget* ProviderWidget = Provider->CreateDescriptionWidget(OwningPlayer, InventoryStack);
if (ProviderWidget) {
ResultWidgets.Add(ProviderWidget);
}
}
return ResultWidgets;
}

Loading

0 comments on commit bb3e52a

Please sign in to comment.