Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISMLExtendedAttributeProvider #238

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file modified Mods/ExampleMod/Content/Items/Desc_ExampleItem.uasset
Binary file not shown.
Binary file not shown.
Binary file modified Mods/ExampleMod/Content/RootGameWorld_ExampleMod.uasset
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions Mods/SML/Config/Tags/SMLGameplayTags.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[/Script/GameplayTags.GameplayTagsList]
GameplayTagList=(Tag="SML.Registry.Item.SpecialItemDescriptor",DevComment="Consider an ItemDescriptor a member of \'IncludeSpecial\' for the purposes of ModContentRegistry::IsDescriptorFilteredOut")
4 changes: 2 additions & 2 deletions Mods/SML/SML.uplugin
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"Version": 3,
"VersionName": "3.7.0",
"SemVersion": "3.7.0",
"GameVersion": "269772",
"GameVersion": ">=269772",
"FriendlyName": "Satisfactory Mod Loader",
"Description": "Mod loading and compatibility API for Satisfactory",
"Category": "Modding",
Expand Down Expand Up @@ -36,4 +36,4 @@
"BasePlugin": true
}
]
}
}
98 changes: 98 additions & 0 deletions Mods/SML/Source/SML/Private/Registry/ContentTagRegistry.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#include "Registry/ContentTagRegistry.h"

#include "ModLoading/PluginModuleLoader.h"

DEFINE_LOG_CATEGORY(LogContentTagRegistry);

FFrame* UContentTagRegistry::ActiveScriptFramePtr{};

/** Makes sure provided object instance is valid, crashes with both script call stack and native stack trace if it's not */
#define NOTIFY_INVALID_REGISTRATION(Context) \
{ \
/* Attempt to use cached script frame pointer first, then fallback to global script callstack (which is not available in shipping by default) */ \
const FString ScriptCallstack = UContentTagRegistry::GetCallStackContext(); \
UE_LOG(LogContentTagRegistry, Error, TEXT("Attempt to modify content tags failed: %s"), Context); \
UE_LOG(LogContentTagRegistry, Error, TEXT("Script Callstack: %s"), *ScriptCallstack); \
ensureMsgf( false, TEXT("%s"), *Context ); \
} \

UContentTagRegistry::UContentTagRegistry() {
}

UContentTagRegistry* UContentTagRegistry::Get(const UObject* WorldContext) {
if (const UWorld* World = GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull)) {
return World->GetSubsystem<UContentTagRegistry>();
}
return nullptr;
}

const FGameplayTagContainer UContentTagRegistry::GetGameplayTagContainerFor(const UObject* content) {
const auto record = TagContainerRegistry.Find(content);
return (record == nullptr) ? FGameplayTagContainer() : *record;
}

bool UContentTagRegistry::CanModifyTagsOf(UObject* content, FString& OutMessage) {
if (bRegistryFrozen) {
OutMessage = FString::Printf(TEXT("Attempt to modify content tags of object '%s' in frozen registry. Make sure your tag changes are happening in the 'Initialization' Lifecycle Phase and not 'Post Initialization'. TODO update this message with the timing we decide on."), *GetPathNameSafe(content));
return false;
}
if (!IsValid(content)) {
OutMessage = FString::Printf(TEXT("Attempt to modify content tags of an invalid object."));
return false;
}
// TODO checking to make sure a UClass is being registered and not a joe schmoe arbitrary uobject instance
return true;
}

void UContentTagRegistry::AddGameplayTagsTo(UObject* content, const FGameplayTagContainer tags) {
FString Context;
if (!CanModifyTagsOf(content, Context)) {
NOTIFY_INVALID_REGISTRATION(*Context);
return;
}
auto& record = TagContainerRegistry.FindOrAdd(content);
UE_LOG(LogContentTagRegistry, Verbose, TEXT("Adding tags %s for class %s"), *tags.ToString(), *GetFullNameSafe(content));
record.AppendTags(tags);
}

void UContentTagRegistry::FreezeRegistry() {
bRegistryFrozen = true;
}

bool UContentTagRegistry::ShouldCreateSubsystem(UObject* Outer) const {
UWorld* WorldOuter = CastChecked<UWorld>(Outer);
return FPluginModuleLoader::ShouldLoadModulesForWorld(WorldOuter);
}

void UContentTagRegistry::OnActorPreSpawnInitialization(AActor* Actor) {
OnWorldBeginPlayDelegate.AddWeakLambda(this, [&]() {
FreezeRegistry();
});
}

void UContentTagRegistry::OnWorldBeginPlay(UWorld& InWorld) {
OnWorldBeginPlayDelegate.Broadcast();
}

FString UContentTagRegistry::GetCallStackContext() {
// Prefer script callstack to the native one
if (ActiveScriptFramePtr != nullptr) {
return ActiveScriptFramePtr->Node->GetPathName();
}

// Attempt to capture stack trace
TArray<FProgramCounterSymbolInfo> NativeStackTrace = FPlatformStackWalk::GetStack(1, 10);
if (NativeStackTrace.IsEmpty()) {
FProgramCounterSymbolInfo& Info = NativeStackTrace.Emplace_GetRef();
TCString<ANSICHAR>::Strcpy(Info.Filename, FProgramCounterSymbolInfo::MAX_NAME_LENGTH, "Unknown");
Info.LineNumber = 1;
}

// Find first frame that does not contain internal logic of content registry
int32 FirstExternalFrameIndex = 0;
while (FirstExternalFrameIndex + 1 < NativeStackTrace.Num() &&
FCStringAnsi::Strifind(NativeStackTrace[FirstExternalFrameIndex].Filename, __FILE__) != nullptr) {
FirstExternalFrameIndex++;
}
return FString::Printf(TEXT("%hs:%d"), NativeStackTrace[FirstExternalFrameIndex].Filename, NativeStackTrace[FirstExternalFrameIndex].LineNumber);
}
15 changes: 13 additions & 2 deletions Mods/SML/Source/SML/Private/Registry/ModContentRegistry.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "Registry/ModContentRegistry.h"
#include "Registry/SMLExtendedAttributeProvider.h"
#include "FGGameMode.h"
#include "FGGameState.h"
#include "FGResearchManager.h"
Expand All @@ -19,6 +20,7 @@
#include "Patching/NativeHookManager.h"
#include "Reflection/ReflectionHelper.h"
#include "Engine/AssetManager.h"
#include "GameplayTagsModule.h"
#include "Kismet/BlueprintAssetHelperLibrary.h"
#include "ModLoading/PluginModuleLoader.h"
#include "Resources/FGAnyUndefinedDescriptor.h"
Expand Down Expand Up @@ -888,6 +890,7 @@ bool UModContentRegistry::IsDescriptorFilteredOut( const UObject* ItemDescriptor
}
const TSubclassOf<UFGItemDescriptor> descriptorClass = const_cast<UClass*>(Cast<UClass>(ItemDescriptor));
if (!descriptorClass) {
UE_LOG(LogContentRegistry, Warning, TEXT("IsDescriptorFilteredOut called with non-ItemDescriptor, returning true (filtering out)"));
return true;
}
if (IsResourceFormFilteredOut(UFGItemDescriptor::GetForm(descriptorClass), Flags)) {
Expand All @@ -906,8 +909,16 @@ bool UModContentRegistry::IsDescriptorFilteredOut( const UObject* ItemDescriptor
if (!EnumHasAnyFlags(Flags, EGetObtainableItemDescriptorsFlags::IncludeVehicles) && descriptorClass->IsChildOf<UFGVehicleDescriptor>()) {
return true;
}
if (!EnumHasAnyFlags(Flags, EGetObtainableItemDescriptorsFlags::IncludeSpecial) && ( descriptorClass->IsChildOf<UFGWildCardDescriptor>() || descriptorClass->IsChildOf<UFGAnyUndefinedDescriptor>() || descriptorClass->IsChildOf<UFGOverflowDescriptor>() || descriptorClass->IsChildOf<UFGNoneDescriptor>() )) {
return true;
if (!EnumHasAnyFlags(Flags, EGetObtainableItemDescriptorsFlags::IncludeSpecial)) {
if (descriptorClass->IsChildOf<UFGWildCardDescriptor>() || descriptorClass->IsChildOf<UFGAnyUndefinedDescriptor>() || descriptorClass->IsChildOf<UFGOverflowDescriptor>() || descriptorClass->IsChildOf<UFGNoneDescriptor>()) {
return true;
}
if (descriptorClass->ImplementsInterface(USMLExtendedAttributeProvider::StaticClass())) {
UObject* ItemDescriptorCDO = descriptorClass->GetDefaultObject();
const auto ItemTags = ISMLExtendedAttributeProvider::Execute_GetGameplayTagsContainer(ItemDescriptorCDO);
const auto SmlSpecialTag = FGameplayTag::RequestGameplayTag("SML.Registry.Item.SpecialItemDescriptor", true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since SML has c++, and this tag is used in c++, it makes more sense to register it using native tags, with the new UE5 macros, rather than registering it in a config file and using the find function every time.

In the .h: SML_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_SML_Registry_Item_SpecialItemDescriptor)
In the .cpp: UE_DEFINE_GAMEPLAY_TAG_COMMENT(TAG_SML_Registry_Item_SpecialItemDescriptor, "SML.Registry.Item.SpecialItemDescriptor", "comment goes here")
return ItemTags.HasTag(TAG_SML_Registry_Item_SpecialItemDescriptor);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, but we should then add a tag to examplemod using the config file approach. Any ideas for what that tag could be?

return ItemTags.HasTag(SmlSpecialTag);
}
}
return false;
}
Expand Down
25 changes: 24 additions & 1 deletion Mods/SML/Source/SML/Private/SMLModule.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
#include "SMLModule.h"

#include "GameplayTagsManager.h"
#include "SatisfactoryModLoader.h"
#include "Interfaces/IPluginManager.h"

void AddModGameplayTagIniSearchPath(const IPlugin& Plugin) {
if(Plugin.GetType() == EPluginType::Mod) {
UGameplayTagsManager::Get().AddTagIniSearchPath(Plugin.GetBaseDir() / TEXT("Config/Tags"));
}
}

void RegisterPluginGameplayTagInis() {
// Add all enabled mods to the gameplay tag ini search path
IPluginManager& PluginManager = IPluginManager::Get();
auto EnabledPlugins = PluginManager.GetEnabledPlugins();
for (auto& Plugin : EnabledPlugins) {
AddModGameplayTagIniSearchPath(Plugin.Get());
}
PluginManager.OnNewPluginMounted().AddLambda([](IPlugin& Plugin) {
AddModGameplayTagIniSearchPath(Plugin);
});
}

void FSMLModule::StartupModule() {
//Basic subsystems like logging are initialized on OnInit
FSatisfactoryModLoader::PreInitializeModLoading();

//UObject subsystem and Engine are initialized on PostEngineInit and we need to delay their initialization to that moment
FCoreDelegates::OnPostEngineInit.AddStatic(FSatisfactoryModLoader::InitializeModLoading);

RegisterPluginGameplayTagInis();
}

void FSMLModule::ShutdownModule() {
Expand Down
78 changes: 78 additions & 0 deletions Mods/SML/Source/SML/Public/Registry/ContentTagRegistry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#pragma once

#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "GameplayTagContainer.h"
#include "ContentTagRegistry.generated.h"

DECLARE_LOG_CATEGORY_EXTERN(LogContentTagRegistry, Log, All);

/**
* Manages Gameplay Tag Containers for content classes.
* Enables any mod to apply Unreal Gameplay Tags to any mod's content,
* or the base game's content, at the UClass level.
*
* Add tags to content via this registry's provided avenues,
* all returned tag containers are const.
*
* Tag associations can only be made before save is loaded,
* after that moment the registry is frozen and no changes can be made after that.
*/
UCLASS()
class SML_API UContentTagRegistry : public UWorldSubsystem
{
GENERATED_BODY()
public:
UContentTagRegistry();

/** Retrieves content tag registry instance */
UFUNCTION(BlueprintPure, Category = "Content Tag Registry", DisplayName = "GetContentTagRegistry", meta = (WorldContext = "WorldContext"))
static UContentTagRegistry* Get(const UObject* WorldContext);

/**
* Get Gameplay Tag container for the supplied class.
* Could be empty if there were no tags registered.
* TODO outvar bool for found/not?
*/
UFUNCTION(BlueprintPure, Category = "Content Tag Registry")
const FGameplayTagContainer GetGameplayTagContainerFor(const UObject* content);

/**
* Register gameplay tags from the passed container to the passed class
* TODO FName InRegistrationPluginName?
*/
UFUNCTION(BlueprintCallable, Category = "Content Tag Registry")
void AddGameplayTagsTo(UObject* content, FGameplayTagContainer tags);

// TODO ability to remove tags

// TODO auto register of stuff with ExtendedAttributeProvider being registered in mod content registry

// Freezes the registry. No new registrations are accepted past this point.
void FreezeRegistry();

void OnActorPreSpawnInitialization(AActor* Actor);

// Begin USubsystem interface
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
//virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
// End USubsystem interface

private:
UPROPERTY()
TMap<UObject*, FGameplayTagContainer> TagContainerRegistry;

bool bRegistryFrozen{ false };

DECLARE_MULTICAST_DELEGATE(FOnWorldBeginPlay);
FOnWorldBeginPlay OnWorldBeginPlayDelegate;

static FString GetCallStackContext();

/** Pointer to the currently active script callstack frame, used for debugging purposes */
static FFrame* ActiveScriptFramePtr;

bool CanModifyTagsOf(UObject* content, FString& OutMessage);

};
3 changes: 2 additions & 1 deletion Mods/SML/Source/SML/Public/Registry/ModContentRegistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ enum class EGetObtainableItemDescriptorsFlags : uint8
IncludeCustomizations = 0x04 UMETA( DisplayName = "Include Customizations" ),
IncludeVehicles = 0x08 UMETA( DisplayName = "Include Vehicles" ),
IncludeCreatures = 0x10 UMETA( DisplayName = "Include Creatures" ),
IncludeSpecial = 0x20 UMETA( DisplayName = "Include Special (WildCard, AnyUndefined, Overflow, None)" ),
IncludeSpecial = 0x20 UMETA( DisplayName = "Include Special (WildCard, AnyUndefined, Overflow, None, SML.Registry.Item.SpecialItemDescriptor)" ),
Default = None,
};

Expand Down Expand Up @@ -283,6 +283,7 @@ class SML_API UModContentRegistry : public UWorldSubsystem {
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
// End USubsystem interface

static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector);
private:
friend class FGameObjectRegistryState;
Expand Down
31 changes: 31 additions & 0 deletions Mods/SML/Source/SML/Public/Registry/SMLExtendedAttributeProvider.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "SMLExtendedAttributeProvider.generated.h"

/**
* Unreal reflection stub class of ISMLExtendedAttributeProvider.
* Use this instead of ISMLExtendedAttributeProvider for UObject->Implements<ISMLExtendedAttributeProvider>()
*/
UINTERFACE(Blueprintable)
class SML_API USMLExtendedAttributeProvider : public UInterface {
GENERATED_BODY()
};

/**
* Implement this on an asset to enable it to provide Gameplay Tags to other systems.
*
* Example: Implement on an FGItemDescriptor and provide the SML.Registry.Item.SpecialItemDescriptor tag
* for the item to be considered "special" and filtered out by default by GetObtainableItemDescriptors
*/
class SML_API ISMLExtendedAttributeProvider {
GENERATED_BODY()
public:

/**
* Get the Gameplay Tags container for this asset.
* Remember, since it's a BlueprintNativeEvent, to call from C++ you must call via ISMLExtendedAttributeProvider::Execute_GetGameplayTagsContainer
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="SML|Tags")
FGameplayTagContainer GetGameplayTagsContainer();
};
6 changes: 3 additions & 3 deletions Mods/SML/Source/SML/Public/Tooltip/SMLItemDisplayInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ class SML_API USMLItemDisplayInterface : public UInterface {
class SML_API ISMLItemDisplayInterface {
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent)
UFUNCTION(BlueprintNativeEvent, Category = "SML|Item")
FText GetOverridenItemName(APlayerController* OwningPlayer, const FInventoryStack& InventoryStack);

UFUNCTION(BlueprintNativeEvent)
UFUNCTION(BlueprintNativeEvent, Category = "SML|Item")
FText GetOverridenItemDescription(APlayerController* OwningPlayer, const FInventoryStack& InventoryStack);

UFUNCTION(BlueprintNativeEvent)
UFUNCTION(BlueprintNativeEvent, Category = "SML|Item")
UWidget* CreateDescriptionWidget(APlayerController* OwningPlayer, const FInventoryStack& InventoryStack);
};
Loading