Custom FGameplayEffectContext doesn't work correctly

Hi,

I’m trying to implement a custom FGameplayEffectContext, plan is to extend the original one with some custom attributes. At some point I realized that gameplay effects are not replicated on client(s) anymore while it is working on the servers client (play as listen server).

Infos:

  • I’m following a course/tutorial using UE 5.2 (Win) where it seems to work, while I’m using UE 5.3.1 on MacOS.
  • The general setup to use MyGameplayEffectContext seems to work as I am able set breakpoints to debug it.
  • I checked a posting on TheGames.Dev and Lyra but could not find any

Problem/What happens:
The game is a common ARPG (top-down) using GAS with health/mana and other attributes. Health and mana are setup as a gameplay effects that are replicated on start (to fill up to max health/max mana). This effect is not reaching the clients while it works on the server side. So my health/mana globes for all client players are empty while the server player has it filled (unless I use a dedicated server, then all versions have empty globes).

I could trace my problem down but now I’m lost with the messages and what I should do. So I’d really like to ask for some help to solve this issue :slight_smile:

#pragma once

#include "GameplayEffectTypes.h"
#include "MyAbilityTypes.generated.h"

USTRUCT(BlueprintType)
struct FMyGameplayEffectContext : public FGameplayEffectContext
{
	GENERATED_BODY()

public:
	bool IsBlockedHit() const { return bIsBlockedHit; }
	bool IsCriticalHit() const { return bIsCriticalHit; }

	void SetIsBlockedHit(bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; }
	void SetIsCriticalHit(bool bInIsCriticalHit) { bIsCriticalHit = bInIsCriticalHit; }

	virtual UScriptStruct* GetScriptStruct() const override
	{
		return FGameplayEffectContext::StaticStruct();
	}

	/** Creates a copy of this context, used to duplicate for later modifications */
	virtual FMyGameplayEffectContext* Duplicate() const override
	{
		FMyGameplayEffectContext* NewContext = new FMyGameplayEffectContext();
		*NewContext = *this;
		if (GetHitResult())
		{
			// Does a deep copy of the hit result
			NewContext->AddHitResult(*GetHitResult(), true);
		}
		return NewContext;
	}

	virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess) override;

protected:
	UPROPERTY()
	bool bIsBlockedHit = false;

	UPROPERTY()
	bool bIsCriticalHit = false;
};


template <>
struct TStructOpsTypeTraits<FMyGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FMyGameplayEffectContext>
{
	enum
	{
		WitNetSerializer = true, 
		WithCopy = true
	};
};
#include "MyAbilityTypes.h"

bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
	uint32 RepBits = 0;
	if (Ar.IsSaving())
	{
		if (bReplicateInstigator && Instigator.IsValid())
		{
			RepBits |= 1 << 0;
		}
		if (bReplicateEffectCauser && EffectCauser.IsValid())
		{
			RepBits |= 1 << 1;
		}
		if (AbilityCDO.IsValid())
		{
			RepBits |= 1 << 2;
		}
		if (bReplicateSourceObject && SourceObject.IsValid())
		{
			RepBits |= 1 << 3;
		}
		if (Actors.Num() > 0)
		{
			RepBits |= 1 << 4;
		}
		if (HitResult.IsValid())
		{
			RepBits |= 1 << 5;
		}
		if (bHasWorldOrigin)
		{
			RepBits |= 1 << 6;
		}

		if (bIsBlockedHit)
		{
			RepBits |= 1 << 7;
		}
		if (bIsCriticalHit)
		{
			RepBits |= 1 << 8;
		}
	}

	Ar.SerializeBits(&RepBits, 9);

	if (RepBits & (1 << 0))
	{
		Ar << Instigator;
	}
	if (RepBits & (1 << 1))
	{
		Ar << EffectCauser;
	}
	if (RepBits & (1 << 2))
	{
		Ar << AbilityCDO;
	}
	if (RepBits & (1 << 3))
	{
		Ar << SourceObject;
	}
	if (RepBits & (1 << 4))
	{
		SafeNetSerializeTArray_Default<31>(Ar, Actors);
	}
	if (RepBits & (1 << 5))
	{
		if (Ar.IsLoading())
		{
			if (!HitResult.IsValid())
			{
				HitResult = TSharedPtr<FHitResult>(new FHitResult());
			}
		}
		HitResult->NetSerialize(Ar, Map, bOutSuccess);
	}
	if (RepBits & (1 << 6))
	{
		Ar << WorldOrigin;
		bHasWorldOrigin = true;
	}
	else
	{
		bHasWorldOrigin = false;
	}

	if (RepBits & (1 << 7))
	{
		Ar << bIsBlockedHit;
	}
	if (RepBits & (1 << 8))
	{
		Ar << bIsCriticalHit;
	}

	if (Ar.IsLoading())
	{
		AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
	}

	bOutSuccess = true;
	return true;
}

So I started to trace it down and here are my findings:

  1. When I reduce NetSerialize to use only 7 Bits it works like a charm. I can even swap out original values for my two bools. But whenever I start using the 8th bit it breaks again. Breakpoint works and the function is executed without problems. I guess this is just a strange side effect as I think #2 is really more the issue (but it’s a guess).

  2. I’m pretty sure that there is an error within the header file and it should read like this:

virtual UScriptStruct* GetScriptStruct() const override
{
    return StaticStruct();
}

to return the StaticStruct() value of the current implementation and not the parent one. So I tried that out, result is a fatal error which breaks PIE and crashed UE Editor:

2023-10-21 11:52:21.736518+0200 UnrealEditor-Mac-DebugGame[77998:20062618] [UE] Fatal error: [File:./../Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayEffectTypes.cpp] [Line: 423] 
FGameplayEffectContextHandle::NetSerialize called on data struct MyGameplayEffectContext without a native NetSerialize

I took a look into line 423 and it’s context:

if (ScriptStruct->StructFlags & STRUCT_NetSerializeNative)
{
    ...
} 
else
{
  // This won't work since FStructProperty::NetSerializeItem is deprecrated.
  //	1) we have to manually crawl through the topmost struct's fields since we don't have a FStructProperty for it (just the UScriptProperty)
  //	2) if there are any UStructProperties in the topmost struct's fields, we will assert in FStructProperty::NetSerializeItem.

  ABILITY_LOG(Fatal, TEXT("FGameplayEffectContextHandle::NetSerialize called on data struct %s without a native NetSerialize"), *ScriptStruct->GetName());
}

I checked my code against the tutorials (which clearly not using #2 correctly - but it’s working with UE 5.2 for the tutor). Other posts/examples like Lyra and TheGames.Dev Blog (Creating your own Gameplay Effect Context. – The Games Dev) are showing that #2 is initially wrong and should be changed to MyGameplayEffectContext::StaticStruct().

When I checked out Lyra I saw that there are some compiler directives for IRIS to map the NetSerialize. But I couldn’t get that to work with copy & paste (adapted the macros of course), because the was a C++ compiler error due to vtables. Anyway, I’m not using IRIS as far as I know (at least there is no setting in MyGame.Build.cs or the .uproject file).

I really appreciate any help to solve this issue which is causing me headaches for two full days now :slight_smile:

Okay, I wa able to fix it after some more coffee.

Strange behaviour for me (as a Java/Kotlin and C# developer) in C++: the enum inside the trait is misspelled. So I guess some code is checking for enum names inside (comparing literals?) and the not finding my WithNetSerializer which should be WithNetSerializer of course. Works like a charm now with the correct used NetSerializer and StaticStruct().

3 Likes

Same issue encountered here,and this seems to be the problem for me.
Really hard to find this kind of issues,just cost me 2 cups of coffee thanks to your generous work!