diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8ce48654ef858bfa14c891f216af48eb3820eb05..93d693e418fdde59a75556ec96d65e03313e3293 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -42,7 +42,7 @@ include:
 
 variables:
     UNREAL_VERSION: "5.4"
-    CUSTOM_NDISPLAY_CONFIG: "aixcave_5_3_dev.ndisplay"
+    CUSTOM_NDISPLAY_CONFIG: "aixcave_5_4.ndisplay"
 
 stages:
   - analyze
@@ -83,7 +83,7 @@ Generate_Project:
         RUN_SETUP: "false"
         GEN_DEPENDENCIES: "(
             [master@UnrealDTrackPlugin]='https://github.com/VRGroupRWTH/UnrealDTrackPlugin.git'
-            [dev/5.3@RWTHVRCluster]='https://git-ce.rwth-aachen.de/vr-vis/VR-Group/unreal-development/plugins/rwth-vr-cluster-plugin.git'
+            [dev/5.4@RWTHVRCluster]='https://git-ce.rwth-aachen.de/vr-vis/VR-Group/unreal-development/plugins/rwth-vr-cluster-plugin.git'
             )"
 
 Generate_Project_Without_Cluster:
diff --git a/Config/DefaultRWTHVRToolkit.ini b/Config/DefaultRWTHVRToolkit.ini
index 13066ebe26e8ab454486f97bbb982275d9b2587c..5e0f330705848d2342828dce06c75fa670d760d4 100644
--- a/Config/DefaultRWTHVRToolkit.ini
+++ b/Config/DefaultRWTHVRToolkit.ini
@@ -18,4 +18,15 @@
 +FunctionRedirects = (OldName="/Script/RWTHVRToolkit.DirectInteractionComponent.OnBeginGrab",NewName="/Script/RWTHVRToolkit.DirectInteractionComponent.OnBeginInteraction")
 +FunctionRedirects = (OldName="/Script/RWTHVRToolkit.DirectInteractionComponent.OnEndGrab",NewName="/Script/RWTHVRToolkit.DirectInteractionComponent.OnEndInteraction")
 +PropertyRedirects = (OldName="/Script/RWTHVRToolkit.DirectInteractionComponent.PreviousGrabBehavioursInRange",NewName="/Script/RWTHVRToolkit.DirectInteractionComponent.PreviousInteractableComponentsInRange")
-+PropertyRedirects = (OldName="/Script/RWTHVRToolkit.DirectInteractionComponent.CurrentGrabBehavioursInRange",NewName="/Script/RWTHVRToolkit.DirectInteractionComponent.CurrentInteractableComponentsInRange")
\ No newline at end of file
++PropertyRedirects = (OldName="/Script/RWTHVRToolkit.DirectInteractionComponent.CurrentGrabBehavioursInRange",NewName="/Script/RWTHVRToolkit.DirectInteractionComponent.CurrentInteractableComponentsInRange")
++PropertyRedirects = (OldName="/Script/RWTHVRToolkit.TurnComponent.DesktopRotation",NewName="/Script/RWTHVRToolkit.TurnComponent.DesktopTurnCondition")
++PropertyRedirects = (OldName="/Script/RWTHVRToolkit.TurnComponent.Turn",NewName="/Script/RWTHVRToolkit.TurnComponent.XRTurn")
++PropertyRedirects = (OldName="/Script/RWTHVRToolkit.RWTHVRWidgetInteractionComponent.WidgetClickInputAction",NewName="/Script/RWTHVRToolkit.RWTHVRWidgetInteractionComponent.WidgetLeftClickInputAction")
++FunctionRedirects = (OldName="/Script/RWTHVRToolkit.RWTHVRWidgetInteractionComponent.OnBeginClick",NewName="/Script/RWTHVRToolkit.RWTHVRWidgetInteractionComponent.OnBeginLeftClick")
++FunctionRedirects = (OldName="/Script/RWTHVRToolkit.RWTHVRWidgetInteractionComponent.OnEndClick",NewName="/Script/RWTHVRToolkit.RWTHVRWidgetInteractionComponent.OnEndLeftClick")
++FunctionRedirects=(OldName="/Script/RWTHVRToolkit.UBaseInteractionComponent.OnBeginInteraction",NewName="/Script/RWTHVRToolkit.UBaseInteractionComponent.OnBeginInteractionInputAction")
++FunctionRedirects=(OldName="/Script/RWTHVRToolkit.UBaseInteractionComponent.OnEndInteraction",NewName="/Script/RWTHVRToolkit.UBaseInteractionComponent.OnEndInteractionInputAction")
++FunctionRedirects=(OldName="/Script/RWTHVRToolkit.UBaseInteractionComponent.MulticastHoverBehaviourStartRpc",NewName="/Script/RWTHVRToolkit.UBaseInteractionComponent.MulticastHoverBehaviourReplicationStartRpc")
++FunctionRedirects=(OldName="/Script/RWTHVRToolkit.UBaseInteractionComponent.MulticastActionBehaviourStartRpc",NewName="/Script/RWTHVRToolkit.UBaseInteractionComponent.MulticastActionBehaviourReplicationStartRpc")
++FunctionRedirects=(OldName="/Script/RWTHVRToolkit.ActionBehaviour.OnActionStart",NewName="/Script/RWTHVRToolkit.ActionBehaviour.OnActionEvent")
++PropertyRedirects=(OldName="/Script/RWTHVRToolkit.ActionBehaviour.OnActionBeginEvent",NewName="/Script/RWTHVRToolkit.ActionBehaviour.OnActionEventEvent")
\ No newline at end of file
diff --git a/Content/BP_RWTHVRGameModeBase.uasset b/Content/BP_RWTHVRGameModeBase.uasset
index a073a7f7cc247654e724c9d348d57f9bdc55d9ed..3bfda91bae7068c7283ece3e8bbe26477c25bd90 100644
Binary files a/Content/BP_RWTHVRGameModeBase.uasset and b/Content/BP_RWTHVRGameModeBase.uasset differ
diff --git a/Content/Components/Movement/Turn/BP_TurnComponent.uasset b/Content/Components/Movement/Turn/BP_TurnComponent.uasset
index a572594744827a04a0a6a6b1912e80ef9be86297..d45f880d4277580f76c4fc943d97c35c7a3d0386 100644
Binary files a/Content/Components/Movement/Turn/BP_TurnComponent.uasset and b/Content/Components/Movement/Turn/BP_TurnComponent.uasset differ
diff --git a/Content/Components/Movement/Turn/IA_DesktopRotation.uasset b/Content/Components/Movement/Turn/IA_DesktopRotation.uasset
deleted file mode 100644
index 9a780d7a9679fdf2a68a1fca575200140e057a73..0000000000000000000000000000000000000000
Binary files a/Content/Components/Movement/Turn/IA_DesktopRotation.uasset and /dev/null differ
diff --git a/Content/Components/Movement/Turn/IA_DesktopTurn.uasset b/Content/Components/Movement/Turn/IA_DesktopTurn.uasset
new file mode 100644
index 0000000000000000000000000000000000000000..db8e3f16401d6456abf4e8d1476bde839e8c39b1
Binary files /dev/null and b/Content/Components/Movement/Turn/IA_DesktopTurn.uasset differ
diff --git a/Content/Components/Movement/Turn/IA_DesktopTurnCondition.uasset b/Content/Components/Movement/Turn/IA_DesktopTurnCondition.uasset
new file mode 100644
index 0000000000000000000000000000000000000000..bdf5a71cce576dd790298eb478b217e6968d2739
Binary files /dev/null and b/Content/Components/Movement/Turn/IA_DesktopTurnCondition.uasset differ
diff --git a/Content/Components/WidgetInteraction/BP_RWTHVRWidgetInteractionComponent.uasset b/Content/Components/WidgetInteraction/BP_RWTHVRWidgetInteractionComponent.uasset
index 904f5d3363442da0ff916b54041f595a4e023ffc..f6f9bafd520036d434be5501aade85a17a8a6f7a 100644
Binary files a/Content/Components/WidgetInteraction/BP_RWTHVRWidgetInteractionComponent.uasset and b/Content/Components/WidgetInteraction/BP_RWTHVRWidgetInteractionComponent.uasset differ
diff --git a/Content/Components/WidgetInteraction/IA_WidgetClick.uasset b/Content/Components/WidgetInteraction/IA_WidgetClick.uasset
deleted file mode 100644
index 470e02e6edeec5b128dd66f602fc5680e3fb9dbe..0000000000000000000000000000000000000000
Binary files a/Content/Components/WidgetInteraction/IA_WidgetClick.uasset and /dev/null differ
diff --git a/Content/Components/WidgetInteraction/IA_WidgetLeftClick.uasset b/Content/Components/WidgetInteraction/IA_WidgetLeftClick.uasset
new file mode 100644
index 0000000000000000000000000000000000000000..c3e97377bcc821f4ea99df5a92b44594c710564f
Binary files /dev/null and b/Content/Components/WidgetInteraction/IA_WidgetLeftClick.uasset differ
diff --git a/Content/Components/WidgetInteraction/IA_WidgetRightClick.uasset b/Content/Components/WidgetInteraction/IA_WidgetRightClick.uasset
new file mode 100644
index 0000000000000000000000000000000000000000..320bf66bc73fa8714baae5258c680aba5f20261a
Binary files /dev/null and b/Content/Components/WidgetInteraction/IA_WidgetRightClick.uasset differ
diff --git a/Content/Input/Default_IMC/IMC_General.uasset b/Content/Input/Default_IMC/IMC_General.uasset
index 6acdb72a1f1896d89873376ff2a06bdd4788506a..bfc100b8d7b66753e20343576c91f76dc51cfaac 100644
Binary files a/Content/Input/Default_IMC/IMC_General.uasset and b/Content/Input/Default_IMC/IMC_General.uasset differ
diff --git a/Content/Input/Default_IMC/IMC_MovementLeftHand.uasset b/Content/Input/Default_IMC/IMC_MovementLeftHand.uasset
index 61a3048f2f7d7470bcc88dd398b06a5cbd825939..0aed5008caf02d89df79d46b4267a447a5e0d9cb 100644
Binary files a/Content/Input/Default_IMC/IMC_MovementLeftHand.uasset and b/Content/Input/Default_IMC/IMC_MovementLeftHand.uasset differ
diff --git a/Content/Input/Default_IMC/IMC_MovementRightHand.uasset b/Content/Input/Default_IMC/IMC_MovementRightHand.uasset
index fd7e3cd5dd01591ed98c06be64b54ca17221f8c0..153723ca229bab41ed167983d4751d2d7312a5d6 100644
Binary files a/Content/Input/Default_IMC/IMC_MovementRightHand.uasset and b/Content/Input/Default_IMC/IMC_MovementRightHand.uasset differ
diff --git a/Content/Pawn/BP_RWTHVRPawn_Default.uasset b/Content/Pawn/BP_RWTHVRPawn_Default.uasset
index cb05e67f79561281c52e04b21e9bd67726871dbb..145ddf5f938d0a3612c7a609aafa75a30d82063f 100644
Binary files a/Content/Pawn/BP_RWTHVRPawn_Default.uasset and b/Content/Pawn/BP_RWTHVRPawn_Default.uasset differ
diff --git a/Content/Pawn/Base/BP_RWTHVRPawn_Base.uasset b/Content/Pawn/Base/BP_RWTHVRPawn_Base.uasset
index 3616819e69aa1ac0f21047b036be9244b1597c76..cba6c2e1875f45603f51e234dbb9b066017654f9 100644
Binary files a/Content/Pawn/Base/BP_RWTHVRPawn_Base.uasset and b/Content/Pawn/Base/BP_RWTHVRPawn_Base.uasset differ
diff --git a/Source/RWTHVRToolkit/Private/Core/RWTHVRGameModeBase.cpp b/Source/RWTHVRToolkit/Private/Core/RWTHVRGameModeBase.cpp
index 4d9b4b6e4dcdb1447e2d9ff98fbfc5400f000589..9102b16012edc3c0c211e77fa3ec9e7f1b4f9520 100644
--- a/Source/RWTHVRToolkit/Private/Core/RWTHVRGameModeBase.cpp
+++ b/Source/RWTHVRToolkit/Private/Core/RWTHVRGameModeBase.cpp
@@ -7,9 +7,15 @@
 #include "GameFramework/SpectatorPawn.h"
 #include "Kismet/GameplayStatics.h"
 #include "Logging/StructuredLog.h"
+#include "Pawn/ClusterRepresentationActor.h"
 #include "Utility/RWTHVRUtilities.h"
 
 
+ARWTHVRGameModeBase::ARWTHVRGameModeBase(const FObjectInitializer& ObjectInitializer)
+{
+	PlayerStateClass = ARWTHVRPlayerState::StaticClass();
+}
+
 FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId,
 										   const FString& Options, const FString& Portal)
 {
@@ -17,6 +23,7 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle
 	// but I don't really want to introduce a hard dependency here.
 	const FString NodeNameKey = "node";
 	const FString PrimaryNodeIdKey = "PrimaryNodeId";
+	const FString ClusterIdKey = "ClusterId";
 
 	// Check if we're using our custom PlayerState so that we can save the player type there.
 	// If not, just ingore all related args.
@@ -24,6 +31,7 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle
 
 	if (State != nullptr)
 	{
+		int32 ClusterId = -1;
 		if (UGameplayStatics::HasOption(Options, PrimaryNodeIdKey))
 		{
 			const FString PrimaryNodeId = UGameplayStatics::ParseOption(Options, PrimaryNodeIdKey);
@@ -34,10 +42,22 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle
 				? UGameplayStatics::ParseOption(Options, NodeNameKey)
 				: PrimaryNodeId;
 
+			ClusterId = UGameplayStatics::HasOption(Options, ClusterIdKey)
+				? FCString::Atoi(*UGameplayStatics::ParseOption(Options, ClusterIdKey))
+				: -1;
+
 			const EPlayerType Type =
 				NodeName == PrimaryNodeId ? EPlayerType::nDisplayPrimary : EPlayerType::nDisplaySecondary;
 			State->RequestSetPlayerType(Type);
 		}
+		else if (GetNetMode() == NM_Standalone && URWTHVRUtilities::IsRoomMountedMode())
+		{
+			ClusterId = 0;
+			const EPlayerType Type =
+				URWTHVRUtilities::IsPrimaryNode() ? EPlayerType::nDisplayPrimary : EPlayerType::nDisplaySecondary;
+			State->RequestSetPlayerType(Type);
+		}
+		State->SetCorrespondingClusterId(ClusterId);
 	}
 
 	return Super::InitNewPlayer(NewPlayerController, UniqueId, Options, Portal);
@@ -45,8 +65,47 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle
 
 void ARWTHVRGameModeBase::PostLogin(APlayerController* NewPlayer)
 {
-	if (const ARWTHVRPlayerState* State = Cast<ARWTHVRPlayerState>(NewPlayer->PlayerState); State != nullptr)
+	if (ARWTHVRPlayerState* State = Cast<ARWTHVRPlayerState>(NewPlayer->PlayerState); State != nullptr)
 	{
+		// If we're in none-standalone netmode, this is only executed on the server, as the GM only exists there.
+		// On standalone, this is executed on every node.
+		int32 ClusterId = State->GetCorrespondingClusterId();
+		if (ClusterId >= 0) // we're either standalone (0) or in an acutal cluster
+		{
+			AClusterRepresentationActor** ClusterRepresentationPtr = ConnectedClusters.Find(ClusterId);
+			AClusterRepresentationActor* ClusterRepresentation;
+			if (!ClusterRepresentationPtr)
+			{
+				// No actor there yet, spawn it
+				FActorSpawnParameters SpawnParameters;
+				SpawnParameters.Name = FName(*FString::Printf(TEXT("ClusterRepresentation_%d"), ClusterId));
+				SpawnParameters.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested;
+				ClusterRepresentation = GetWorld()->SpawnActor<AClusterRepresentationActor>(SpawnParameters);
+				UE_LOGFMT(Toolkit, Display,
+						  "ARWTHVRGameModeBase: Spawned ClusterRepresentationActor {Name} for Cluster {Id}",
+						  ClusterRepresentation->GetName(), ClusterId);
+				ConnectedClusters.Add(ClusterId, ClusterRepresentation);
+			}
+			else
+			{
+				ClusterRepresentation = *ClusterRepresentationPtr;
+			}
+
+			UE_LOGFMT(Toolkit, Display, "ARWTHVRGameModeBase: Using ClusterRepresentationActor {Name} for Cluster {Id}",
+					  *ClusterRepresentation->GetName(), ClusterId);
+
+			// Double check for sanity
+			check(ClusterRepresentation != nullptr);
+
+			State->SetCorrespondingClusterActor(ClusterRepresentation);
+
+			if (State->GetPlayerType() == EPlayerType::nDisplayPrimary)
+			{
+				// We're the owner of the actor!
+				ClusterRepresentation->SetOwner(NewPlayer);
+			}
+		}
+
 		// Do we already have an auto-possessing pawn possessed?
 		if (NewPlayer->GetPawn() && NewPlayer->GetPawn()->IsValidLowLevelFast())
 		{
@@ -56,10 +115,11 @@ void ARWTHVRGameModeBase::PostLogin(APlayerController* NewPlayer)
 			return;
 		}
 
+		// When we're not in standalone:
 		// If the new player is a secondary nDisplay node, spawn it only as a Spectator
 		// Potentially we can use MustSpectate instead.
 		UClass* PawnClass;
-		if (State->GetPlayerType() == EPlayerType::nDisplaySecondary)
+		if (GetNetMode() != NM_Standalone && State->GetPlayerType() == EPlayerType::nDisplaySecondary)
 		{
 			// For now, simply use the BP approach of spawning the pawn here. Can do this in a better way potentially.
 			PawnClass = SpectatorClass;
@@ -83,11 +143,20 @@ void ARWTHVRGameModeBase::PostLogin(APlayerController* NewPlayer)
 			}
 		}
 
+		if (GetNetMode() == NM_Standalone)
+		{
+			const FName BaseName = PawnClass->HasAnyFlags(RF_ClassDefaultObject)
+				? PawnClass->GetFName()
+				: *PawnClass->GetFName().GetPlainNameString();
+
+			SpawnInfo.Name = BaseName;
+			SpawnInfo.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested;
+		}
+
 		// Spawn and possess the pawn
 		APawn* ResultPawn = GetWorld()->SpawnActor<APawn>(PawnClass, StartSpot->GetTransform(), SpawnInfo);
 		NewPlayer->Possess(ResultPawn);
 	}
 
-
 	Super::PostLogin(NewPlayer);
 }
diff --git a/Source/RWTHVRToolkit/Private/Core/RWTHVRPlayerState.cpp b/Source/RWTHVRToolkit/Private/Core/RWTHVRPlayerState.cpp
index 0ad60f0c44a7853df65507cd23b9943c6cd8530b..4ae4795c85058331aeea4a2ff08df643b3cccd05 100644
--- a/Source/RWTHVRToolkit/Private/Core/RWTHVRPlayerState.cpp
+++ b/Source/RWTHVRToolkit/Private/Core/RWTHVRPlayerState.cpp
@@ -3,8 +3,10 @@
 
 #include "Core/RWTHVRPlayerState.h"
 
+#include "Logging/StructuredLog.h"
 #include "Net/UnrealNetwork.h"
 #include "Net/Core/PushModel/PushModel.h"
+#include "Utility/RWTHVRUtilities.h"
 
 // Boilerplate, copies properties to new state
 void ARWTHVRPlayerState::CopyProperties(class APlayerState* PlayerState)
@@ -17,6 +19,8 @@ void ARWTHVRPlayerState::CopyProperties(class APlayerState* PlayerState)
 		if (IsValid(RWTHVRPlayerState))
 		{
 			RWTHVRPlayerState->SetPlayerType(GetPlayerType());
+			RWTHVRPlayerState->SetCorrespondingClusterId(CorrespondingClusterId);
+			RWTHVRPlayerState->SetCorrespondingClusterActor(CorrespondingClusterActor);
 		}
 	}
 }
@@ -32,6 +36,8 @@ void ARWTHVRPlayerState::OverrideWith(class APlayerState* PlayerState)
 		if (IsValid(RWTHVRPlayerState))
 		{
 			SetPlayerType(RWTHVRPlayerState->GetPlayerType());
+			SetCorrespondingClusterId(RWTHVRPlayerState->CorrespondingClusterId);
+			SetCorrespondingClusterActor(RWTHVRPlayerState->CorrespondingClusterActor);
 		}
 	}
 }
@@ -45,6 +51,8 @@ void ARWTHVRPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& O
 	SharedParams.bIsPushBased = true;
 
 	DOREPLIFETIME_WITH_PARAMS_FAST(ARWTHVRPlayerState, PlayerType, SharedParams);
+	DOREPLIFETIME_WITH_PARAMS_FAST(ARWTHVRPlayerState, CorrespondingClusterId, SharedParams);
+	DOREPLIFETIME_WITH_PARAMS_FAST(ARWTHVRPlayerState, CorrespondingClusterActor, SharedParams);
 }
 
 void ARWTHVRPlayerState::ServerSetPlayerTypeRpc_Implementation(const EPlayerType NewPlayerType)
@@ -57,6 +65,28 @@ void ARWTHVRPlayerState::SetPlayerType(const EPlayerType NewPlayerType)
 	MARK_PROPERTY_DIRTY_FROM_NAME(ARWTHVRPlayerState, PlayerType, this);
 	PlayerType = NewPlayerType;
 }
+void ARWTHVRPlayerState::SetCorrespondingClusterId(int32 NewCorrespondingClusterId)
+{
+	if (!HasAuthority())
+	{
+		UE_LOGFMT(Toolkit, Warning, "ARWTHVRPlayerState: Cannot set cluster Id on non-authority!");
+		return;
+	}
+
+	MARK_PROPERTY_DIRTY_FROM_NAME(ARWTHVRPlayerState, CorrespondingClusterId, this);
+	CorrespondingClusterId = NewCorrespondingClusterId;
+}
+void ARWTHVRPlayerState::SetCorrespondingClusterActor(AClusterRepresentationActor* NewCorrespondingClusterActor)
+{
+	if (!HasAuthority())
+	{
+		UE_LOGFMT(Toolkit, Warning, "ARWTHVRPlayerState: Cannot set cluster actor ref on non-authority!");
+		return;
+	}
+
+	MARK_PROPERTY_DIRTY_FROM_NAME(ARWTHVRPlayerState, CorrespondingClusterActor, this);
+	CorrespondingClusterActor = NewCorrespondingClusterActor;
+}
 
 void ARWTHVRPlayerState::RequestSetPlayerType(const EPlayerType NewPlayerType)
 {
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactables/ActionBehaviour.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactables/ActionBehaviour.cpp
index 4e9c3fd662dd3baa2a30fddd8f516022e4efa9ba..ff1d027208025476ce073a15bd224ca5c9ccb9f0 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/Interactables/ActionBehaviour.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactables/ActionBehaviour.cpp
@@ -6,27 +6,14 @@
 // We disable ticking here, as we are mainly interested in the events
 UActionBehaviour::UActionBehaviour() { PrimaryComponentTick.bCanEverTick = false; }
 
-void UActionBehaviour::OnActionStart(USceneComponent* TriggeredComponent, const UInputAction* InputAction,
+void UActionBehaviour::OnActionEvent(USceneComponent* TriggerComponent, const EInteractionEventType EventType,
 									 const FInputActionValue& Value)
 {
 }
 
-void UActionBehaviour::OnActionEnd(USceneComponent* TriggeredComponent, const UInputAction* InputAction,
-								   const FInputActionValue& Value)
-{
-}
-
 void UActionBehaviour::BeginPlay()
 {
 	Super::BeginPlay();
 
-	OnActionBeginEvent.AddDynamic(this, &UActionBehaviour::OnActionStart);
-	OnActionEndEvent.AddDynamic(this, &UActionBehaviour::OnActionEnd);
-}
-
-// Called every frame
-void UActionBehaviour::TickComponent(float DeltaTime, ELevelTick TickType,
-									 FActorComponentTickFunction* ThisTickFunction)
-{
-	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
+	OnActionEventEvent.AddDynamic(this, &UActionBehaviour::OnActionEvent);
 }
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactables/BaseBehaviour.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactables/BaseBehaviour.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..95488260668c095eb5dece6895555ce808e2fd11
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactables/BaseBehaviour.cpp
@@ -0,0 +1,4 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "Interaction/Interactables/BaseBehaviour.h"
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactables/GrabBehavior.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactables/GrabBehavior.cpp
index 1537243da0b6c92a901decadce793bc8bd819bbb..502a4be532df57e6f3214e859db8eb4729b15482 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/Interactables/GrabBehavior.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactables/GrabBehavior.cpp
@@ -3,10 +3,26 @@
 
 #include "Interaction/Interactables/GrabBehavior.h"
 #include "Interaction/Interactables/InteractableComponent.h"
+#include "Interaction/Interactables/InteractionEventType.h"
 #include "Logging/StructuredLog.h"
+#include "Pawn/Navigation/CollisionHandlingMovement.h"
 #include "Serialization/JsonTypes.h"
 #include "Utility/RWTHVRUtilities.h"
 
+UGrabBehavior::UGrabBehavior()
+{
+	SetIsReplicatedByDefault(true);
+	bExecuteOnServer = true;
+	bExecuteOnAllClients = false;
+}
+
+void UGrabBehavior::BeginPlay()
+{
+	Super::BeginPlay();
+
+	OnActionReplicationStartedOriginatorEvent.AddDynamic(this, &UGrabBehavior::ReplicationOriginaterClientCallback);
+}
+
 UPrimitiveComponent* UGrabBehavior::GetFirstComponentSimulatingPhysics(const AActor* TargetActor)
 {
 	TArray<UPrimitiveComponent*> PrimitiveComponents;
@@ -33,16 +49,79 @@ UPrimitiveComponent* UGrabBehavior::GetHighestParentSimulatingPhysics(UPrimitive
 
 	return Comp;
 }
+void UGrabBehavior::ReplicationOriginaterClientCallback(USceneComponent* TriggerComponent,
+														const EInteractionEventType EventType,
+														const FInputActionValue& Value)
+{
+	const USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggerComponent->GetAttachParent());
+	HandleCollisionHandlingMovement(CurrentAttachParent, EventType);
+}
 
-void UGrabBehavior::OnActionStart(USceneComponent* TriggeredComponent, const UInputAction* InputAction,
+void UGrabBehavior::HandleCollisionHandlingMovement(const USceneComponent* CurrentAttachParent,
+													const EInteractionEventType EventType)
+{
+	auto CHM = CurrentAttachParent->GetOwner()->GetComponentByClass<UCollisionHandlingMovement>();
+	if (!CHM)
+		return;
+
+	if (EventType == EInteractionEventType::InteractionStart)
+	{
+		// Add to ignore list for collision handling movement, if it exists
+		if (bIgnoreGrabbedActorInCollisionMovement)
+		{
+			bWasAddedToIgnore = CHM->AddActorToIgnore(GetOwner());
+		}
+	}
+	else
+	{
+		// If our attach parent has a collision handling component, remove
+		if (bWasAddedToIgnore)
+		{
+			CHM->RemoveActorFromIgnore(GetOwner());
+		}
+	}
+}
+void UGrabBehavior::OnActionEvent(USceneComponent* TriggerComponent, const EInteractionEventType EventType,
 								  const FInputActionValue& Value)
+{
+	if (EventType == EInteractionEventType::InteractionStart)
+	{
+		StartGrab(TriggerComponent);
+	}
+	else
+	{
+		EndGrab(TriggerComponent);
+	}
+}
+
+bool UGrabBehavior::TryRelease()
+{
+	if (!bObjectGrabbed)
+	{
+		UE_LOGFMT(Toolkit, Display, "UGrabBehavior::TryRelease: bObjectGrabbed was false!");
+		return false;
+	}
+
+	if (MyPhysicsComponent)
+	{
+		MyPhysicsComponent->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
+		MyPhysicsComponent->SetSimulatePhysics(bWasSimulatingPhysics);
+	}
+	else
+	{
+		GetOwner()->GetRootComponent()->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
+	}
+	bObjectGrabbed = false;
+	return true;
+}
+void UGrabBehavior::StartGrab(USceneComponent* TriggerComponent)
 {
 	if (bObjectGrabbed)
 	{
 		return;
 	}
 
-	USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggeredComponent->GetAttachParent());
+	USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggerComponent->GetAttachParent());
 	const FAttachmentTransformRules Rules = FAttachmentTransformRules(EAttachmentRule::KeepWorld, false);
 
 	if (MyPhysicsComponent = GetFirstComponentSimulatingPhysics(GetOwner()); MyPhysicsComponent != nullptr)
@@ -72,18 +151,19 @@ void UGrabBehavior::OnActionStart(USceneComponent* TriggeredComponent, const UIn
 		GetOwner()->GetComponents<UInteractableComponent>(Interactables, false);
 		for (UInteractableComponent* Interactable : Interactables)
 		{
-			Interactable->RestrictInteractionToComponent(TriggeredComponent);
+			Interactable->RestrictInteractionToComponent(TriggerComponent);
 		}
 	}
 
 	OnGrabStartEvent.Broadcast(CurrentAttachParent, MyPhysicsComponent);
+
+	// Add to ignore list for collision handling movement, if it exists
+	HandleCollisionHandlingMovement(CurrentAttachParent, InteractionStart);
 }
 
-void UGrabBehavior::OnActionEnd(USceneComponent* TriggeredComponent, const UInputAction* InputAction,
-								const FInputActionValue& Value)
+void UGrabBehavior::EndGrab(USceneComponent* TriggerComponent)
 {
-
-	USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggeredComponent->GetAttachParent());
+	USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggerComponent->GetAttachParent());
 
 	// We try to release the attached component. If it is not succesful we log and return. Otherwise, we continue.
 	if (!TryRelease())
@@ -106,24 +186,7 @@ void UGrabBehavior::OnActionEnd(USceneComponent* TriggeredComponent, const UInpu
 			Interactable->ResetRestrictInteraction();
 		}
 	}
-}
 
-bool UGrabBehavior::TryRelease()
-{
-	if (!bObjectGrabbed)
-	{
-		return false;
-	}
-
-	if (MyPhysicsComponent)
-	{
-		MyPhysicsComponent->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
-		MyPhysicsComponent->SetSimulatePhysics(bWasSimulatingPhysics);
-	}
-	else
-	{
-		GetOwner()->GetRootComponent()->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
-	}
-	bObjectGrabbed = false;
-	return true;
+	// If our attach parent has a collision handling component, remove
+	HandleCollisionHandlingMovement(CurrentAttachParent, InteractionEnd);
 }
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactables/HoverBehaviour.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactables/HoverBehaviour.cpp
index 96897613cd2f536cb9625a9b395ce49181656e70..f78e75fabfa434b1396d7bffc1354adee7f49441 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/Interactables/HoverBehaviour.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactables/HoverBehaviour.cpp
@@ -3,14 +3,13 @@
 
 #include "Interaction/Interactables/HoverBehaviour.h"
 
-void UHoverBehaviour::OnHoverStart(const USceneComponent* TriggeredComponent, FHitResult Hit) {}
-
-void UHoverBehaviour::OnHoverEnd(const USceneComponent* TriggeredComponent) {}
 
+void UHoverBehaviour::OnHoverEvent(const USceneComponent* TriggerComponent, EInteractionEventType EventType,
+								   const FHitResult& Hit)
+{
+}
 void UHoverBehaviour::BeginPlay()
 {
 	Super::BeginPlay();
-
-	OnHoverStartEvent.AddDynamic(this, &UHoverBehaviour::OnHoverStart);
-	OnHoverEndEvent.AddDynamic(this, &UHoverBehaviour::OnHoverEnd);
+	OnHoverEventEvent.AddDynamic(this, &UHoverBehaviour::OnHoverEvent);
 }
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactables/IntenSelect/IntenSelectable.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactables/IntenSelect/IntenSelectable.cpp
index 90a4e2956a3d06f1cb42d6219e4f7f1ff31ca050..276269b3dc0f389621e6415a71887cf3170a9e79 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/Interactables/IntenSelect/IntenSelectable.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactables/IntenSelect/IntenSelectable.cpp
@@ -62,7 +62,8 @@ void UIntenSelectable::HandleOnSelectStartEvents(const UIntenSelectComponent* In
 {
 	for (const UHoverBehaviour* CurrentHoverBehaviour : OnHoverBehaviours)
 	{
-		CurrentHoverBehaviour->OnHoverStartEvent.Broadcast(IntenSelect, HitResult);
+		CurrentHoverBehaviour->OnHoverEventEvent.Broadcast(IntenSelect, EInteractionEventType::InteractionStart,
+														   HitResult);
 	}
 }
 
@@ -70,7 +71,9 @@ void UIntenSelectable::HandleOnSelectEndEvents(const UIntenSelectComponent* Inte
 {
 	for (const UHoverBehaviour* CurrentHoverBehaviour : OnHoverBehaviours)
 	{
-		CurrentHoverBehaviour->OnHoverEndEvent.Broadcast(IntenSelect);
+		FHitResult EmptyHit;
+		CurrentHoverBehaviour->OnHoverEventEvent.Broadcast(IntenSelect, EInteractionEventType::InteractionEnd,
+														   EmptyHit);
 	}
 }
 
@@ -79,8 +82,8 @@ void UIntenSelectable::HandleOnClickStartEvents(UIntenSelectComponent* IntenSele
 	for (const UActionBehaviour* CurrentActionBehaviour : OnActionBehaviours)
 	{
 		FInputActionValue EmptyInputActionValue{};
-		const UInputAction* EmptyInputAction{};
-		CurrentActionBehaviour->OnActionBeginEvent.Broadcast(IntenSelect, EmptyInputAction, EmptyInputActionValue);
+		CurrentActionBehaviour->OnActionEventEvent.Broadcast(IntenSelect, EInteractionEventType::InteractionStart,
+															 EmptyInputActionValue);
 	}
 }
 
@@ -88,8 +91,8 @@ void UIntenSelectable::HandleOnClickEndEvents(UIntenSelectComponent* IntenSelect
 {
 	for (const UActionBehaviour* CurrentActionBehaviour : OnActionBehaviours)
 	{
-		const UInputAction* EmptyInputActionValue{};
-		CurrentActionBehaviour->OnActionEndEvent.Broadcast(IntenSelect, EmptyInputActionValue, InputValue);
+		CurrentActionBehaviour->OnActionEventEvent.Broadcast(IntenSelect, EInteractionEventType::InteractionEnd,
+															 InputValue);
 	}
 }
 
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactables/InteractableComponent.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactables/InteractableComponent.cpp
index 29ee75b08e1d37147671228e167c09e4cde462f8..4f1c256218355ce5c142d010bcc3a460491f08f8 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/Interactables/InteractableComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactables/InteractableComponent.cpp
@@ -4,6 +4,9 @@
 #include "Interaction/Interactables/InteractableComponent.h"
 #include "Interaction/Interactables/ActionBehaviour.h"
 #include "Interaction/Interactables/HoverBehaviour.h"
+#include "Interaction/Interactors/UBaseInteractionComponent.h"
+#include "Logging/StructuredLog.h"
+#include "Utility/RWTHVRUtilities.h"
 
 void UInteractableComponent::RestrictInteractionToComponents(const TArray<USceneComponent*>& Components)
 {
@@ -39,9 +42,8 @@ void UInteractableComponent::BeginPlay()
 	InitDefaultBehaviourReferences();
 }
 
-// This functions dispatches the HoverStart Event to the attached Hover Behaviour Components
-void UInteractableComponent::HandleOnHoverStartEvents(USceneComponent* TriggerComponent,
-													  const EInteractorType Interactor)
+void UInteractableComponent::HandleOnHoverEvents(USceneComponent* TriggerComponent, const EInteractorType Interactor,
+												 const EInteractionEventType EventType)
 {
 	// We early return if there the InteractorFilter is set and the Interactor is allowed.
 	if (!(InteractorFilter == EInteractorType::None || InteractorFilter & Interactor))
@@ -52,34 +54,47 @@ void UInteractableComponent::HandleOnHoverStartEvents(USceneComponent* TriggerCo
 		return;
 
 	// Broadcast event to all HoverBehaviours
-	for (const UHoverBehaviour* b : OnHoverBehaviours)
+	for (UHoverBehaviour* HoverBehaviour : OnHoverBehaviours)
 	{
-		b->OnHoverStartEvent.Broadcast(TriggerComponent, HitResult);
-	}
-}
-
-// This functions dispatches the HoverEnd Event to the attached Hover Behaviour Components
-void UInteractableComponent::HandleOnHoverEndEvents(USceneComponent* TriggerComponent, const EInteractorType Interactor)
-{
-	// We early return if there the InteractorFilter is set and the Interactor is allowed.
-	if (!(InteractorFilter == EInteractorType::None || InteractorFilter & Interactor))
-		return;
-
-	// We early return if the source Interactor is not part of the allowed components
-	if (!IsComponentAllowed(TriggerComponent))
-		return;
-
-	// Broadcast event to all HoverBehaviours
-	for (const UHoverBehaviour* b : OnHoverBehaviours)
-	{
-		b->OnHoverEndEvent.Broadcast(TriggerComponent);
+		// Check if we need to replicate the action to the server
+		if (HoverBehaviour->bExecuteOnServer)
+		{
+			// Request the server to execute our behaviour.
+			// As we can only execute RPCs from an owned object, and we don't necessarily own this actor, pipe it
+			// through the interactor. Because behaviours can also be triggered from non-interactors, only do this on a
+			// successful cast
+
+			auto* InteractorComponent = Cast<UUBaseInteractionComponent>(TriggerComponent);
+			if (!InteractorComponent)
+			{
+				UE_LOGFMT(Toolkit, Warning,
+						  "Interaction: TriggerComponent {TriggerComponent} is not a UUBaseInteractionComponent. Only "
+						  "UUBaseInteractionComponent TriggerComponents can be replicated.",
+						  TriggerComponent->GetName());
+				return;
+			}
+			InteractorComponent->RequestHoverBehaviourReplicationStart(HoverBehaviour, EventType, HitResult);
+
+			// Broadcast local callback
+			HoverBehaviour->OnHoverReplicationStartedOriginatorEvent.Broadcast(TriggerComponent, EventType, HitResult);
+		}
+		else if (HoverBehaviour->bExecuteOnAllClients)
+		{
+			UE_LOGFMT(Toolkit, Warning,
+					  "Interaction: Behaviour {BehaviourName} has bExecuteOnAllClients=true, which requires "
+					  "bExecuteOnServer also set to true, which is not the case.",
+					  HoverBehaviour->GetName());
+		}
+		else // skip replication altogether (default case)
+		{
+			HoverBehaviour->OnHoverEventEvent.Broadcast(TriggerComponent, EventType, HitResult);
+		}
 	}
 }
 
 // This functions dispatches the ActionStart Event to the attached Action Behaviour Components
-void UInteractableComponent::HandleOnActionStartEvents(USceneComponent* TriggerComponent,
-													   const UInputAction* InputAction, const FInputActionValue& Value,
-													   const EInteractorType Interactor)
+void UInteractableComponent::HandleOnActionEvents(USceneComponent* TriggerComponent, const EInteractorType Interactor,
+												  const EInteractionEventType EventType, const FInputActionValue& Value)
 {
 	// We early return if there the InteractorFilter is set and the Interactor is allowed.
 	if (!(InteractorFilter == EInteractorType::None || InteractorFilter & Interactor))
@@ -90,28 +105,41 @@ void UInteractableComponent::HandleOnActionStartEvents(USceneComponent* TriggerC
 		return;
 
 	// Broadcast event to all ActionBehaviours
-	for (const UActionBehaviour* b : OnActionBehaviours)
+	for (UActionBehaviour* ActionBehaviour : OnActionBehaviours)
 	{
-		b->OnActionBeginEvent.Broadcast(TriggerComponent, InputAction, Value);
-	}
-}
-
-// This functions dispatches the ActionEnd Event to the attached Action Behaviour Components
-void UInteractableComponent::HandleOnActionEndEvents(USceneComponent* TriggerComponent, const UInputAction* InputAction,
-													 const FInputActionValue& Value, const EInteractorType Interactor)
-{
-	// We early return if there the InteractorFilter is set and the Interactor is allowed.
-	if (!(InteractorFilter == EInteractorType::None || InteractorFilter & Interactor))
-		return;
-
-	// We early return if the source Interactor is not part of the allowed components
-	if (!IsComponentAllowed(TriggerComponent))
-		return;
-
-	// Broadcast event to all ActionBehaviours
-	for (const UActionBehaviour* b : OnActionBehaviours)
-	{
-		b->OnActionEndEvent.Broadcast(TriggerComponent, InputAction, Value);
+		// Check if we need to replicate the action to the server
+		if (ActionBehaviour->bExecuteOnServer)
+		{
+			// Request the server to execute our behaviour.
+			// As we can only execute RPCs from an owned object, and we don't necessarily own this actor, pipe it
+			// through the interactor. Because behaviours can also be triggered from non-interactors, only do this on a
+			// successful cast
+
+			auto* InteractorComponent = Cast<UUBaseInteractionComponent>(TriggerComponent);
+			if (!InteractorComponent)
+			{
+				UE_LOGFMT(Toolkit, Warning,
+						  "Interaction: TriggerComponent {TriggerComponent} is not a UUBaseInteractionComponent. Only "
+						  "UUBaseInteractionComponent TriggerComponents can be replicated.",
+						  TriggerComponent->GetName());
+				return;
+			}
+			InteractorComponent->RequestActionBehaviourReplicationStart(ActionBehaviour, EventType, Value);
+
+			// Broadcast local callback
+			ActionBehaviour->OnActionReplicationStartedOriginatorEvent.Broadcast(TriggerComponent, EventType, Value);
+		}
+		else if (ActionBehaviour->bExecuteOnAllClients)
+		{
+			UE_LOGFMT(Toolkit, Warning,
+					  "Interaction: Behaviour {BehaviourName} has bExecuteOnAllClients=true, which requires "
+					  "bExecuteOnServer also set to true, which is not the case.",
+					  ActionBehaviour->GetName());
+		}
+		else // skip replication altogether (default case)
+		{
+			ActionBehaviour->OnActionEventEvent.Broadcast(TriggerComponent, EventType, Value);
+		}
 	}
 }
 
@@ -150,4 +178,4 @@ bool UInteractableComponent::IsComponentAllowed(USceneComponent* Component) cons
 		}
 	}
 	return true;
-}
+}
\ No newline at end of file
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactors/DirectInteractionComponent.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactors/DirectInteractionComponent.cpp
index 7c81cb805f86e3d9a4de4bfa2c594ab02a683719..18e515817875cb60c35090b4dbfb4300051b8dac 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/Interactors/DirectInteractionComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactors/DirectInteractionComponent.cpp
@@ -6,6 +6,7 @@
 #include "EnhancedInputComponent.h"
 #include "Interaction/Interactables/InteractableComponent.h"
 #include "Interaction/Interactables/InteractionBitSet.h"
+#include "Interaction/Interactables/InteractionEventType.h"
 
 #include "Kismet/GameplayStatics.h"
 #include "Logging/StructuredLog.h"
@@ -30,7 +31,7 @@ void UDirectInteractionComponent::TickComponent(float DeltaTime, ELevelTick Tick
 	TArray<FHitResult> OutHits;
 	const ETraceTypeQuery TraceType = UEngineTypes::ConvertToTraceType(ECollisionChannel::ECC_PhysicsBody);
 
-	auto DebugTrace = bShowDebugTrace ? EDrawDebugTrace::ForOneFrame : EDrawDebugTrace::None;
+	const auto DebugTrace = bShowDebugTrace ? EDrawDebugTrace::ForOneFrame : EDrawDebugTrace::None;
 
 	UKismetSystemLibrary::SphereTraceMulti(GetWorld(), GetAttachParent()->GetComponentLocation(),
 										   GetAttachParent()->GetComponentLocation(), InteractionSphereRadius,
@@ -59,7 +60,8 @@ void UDirectInteractionComponent::TickComponent(float DeltaTime, ELevelTick Tick
 		if (!PreviousInteractableComponentsInRange.Contains(CurrentInteractableComp))
 		{
 			PreviousInteractableComponentsInRange.AddUnique(CurrentInteractableComp);
-			CurrentInteractableComp->HandleOnHoverStartEvents(this, EInteractorType::Direct);
+			CurrentInteractableComp->HandleOnHoverEvents(this, EInteractorType::Direct,
+														 EInteractionEventType::InteractionStart);
 		}
 	}
 
@@ -71,7 +73,8 @@ void UDirectInteractionComponent::TickComponent(float DeltaTime, ELevelTick Tick
 		if (!CurrentInteractableCompsInRange.Contains(PrevInteractableComp))
 		{
 			ComponentsToRemove.AddUnique(PrevInteractableComp);
-			PrevInteractableComp->HandleOnHoverEndEvents(this, EInteractorType::Direct);
+			PrevInteractableComp->HandleOnHoverEvents(this, EInteractorType::Direct,
+													  EInteractionEventType::InteractionEnd);
 		}
 	}
 
@@ -97,12 +100,12 @@ void UDirectInteractionComponent::SetupPlayerInput(UInputComponent* PlayerInputC
 		return;
 
 	EI->BindAction(InteractionInputAction, ETriggerEvent::Started, this,
-				   &UDirectInteractionComponent::OnBeginInteraction);
+				   &UDirectInteractionComponent::OnBeginInteractionInputAction);
 	EI->BindAction(InteractionInputAction, ETriggerEvent::Completed, this,
-				   &UDirectInteractionComponent::OnEndInteraction);
+				   &UDirectInteractionComponent::OnEndInteractionInputAction);
 }
 
-void UDirectInteractionComponent::OnBeginInteraction(const FInputActionValue& Value)
+void UDirectInteractionComponent::OnBeginInteractionInputAction(const FInputActionValue& Value)
 {
 	const FVector InteractionLocation = GetAttachParent()->GetComponentLocation();
 
@@ -115,7 +118,7 @@ void UDirectInteractionComponent::OnBeginInteraction(const FInputActionValue& Va
 			CurrentInteractableComponentsInRange,
 			[&](auto Element)
 			{ return FVector(Element->GetOwner()->GetActorLocation() - InteractionLocation).Size(); });
-		MinElement->HandleOnActionStartEvents(this, InteractionInputAction, Value, EInteractorType::Direct);
+		MinElement->HandleOnActionEvents(this, EInteractorType::Direct, EInteractionEventType::InteractionStart, Value);
 		CurrentlyInteractedComponents = {MinElement};
 	}
 	else
@@ -125,19 +128,21 @@ void UDirectInteractionComponent::OnBeginInteraction(const FInputActionValue& Va
 											  CurrentInteractableComponentsInRange.Num());
 		for (UInteractableComponent* InteractableComp : CurrentInteractableComponentsInRange)
 		{
-			InteractableComp->HandleOnActionStartEvents(this, InteractionInputAction, Value, EInteractorType::Direct);
+			InteractableComp->HandleOnActionEvents(this, EInteractorType::Direct,
+												   EInteractionEventType::InteractionStart, Value);
 			CurrentlyInteractedComponents.Add(InteractableComp);
 		}
 	}
 }
 
-void UDirectInteractionComponent::OnEndInteraction(const FInputActionValue& Value)
+void UDirectInteractionComponent::OnEndInteractionInputAction(const FInputActionValue& Value)
 {
 	for (auto& Component : CurrentlyInteractedComponents)
 	{
 		if (Component.IsValid())
 		{
-			Component->HandleOnActionEndEvents(this, InteractionInputAction, Value, EInteractorType::Direct);
+			Component->HandleOnActionEvents(this, EInteractorType::Direct, EInteractionEventType::InteractionEnd,
+											Value);
 		}
 	}
 	CurrentlyInteractedComponents.Empty();
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactors/RWTHVRWidgetInteractionComponent.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactors/RWTHVRWidgetInteractionComponent.cpp
index 3a87de65d2cb52a5bce0dd0aa71af9dd3b2f69bb..23e0727a0f17734282db4c1f0051965789b938ca 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/Interactors/RWTHVRWidgetInteractionComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactors/RWTHVRWidgetInteractionComponent.cpp
@@ -51,10 +51,20 @@ void URWTHVRWidgetInteractionComponent::SetupPlayerInput(UInputComponent* Player
 		return;
 	}
 
-	EI->BindAction(WidgetClickInputAction, ETriggerEvent::Started, this,
-				   &URWTHVRWidgetInteractionComponent::OnBeginClick);
-	EI->BindAction(WidgetClickInputAction, ETriggerEvent::Completed, this,
-				   &URWTHVRWidgetInteractionComponent::OnEndClick);
+	if (WidgetLeftClickInputAction)
+	{
+		EI->BindAction(WidgetLeftClickInputAction, ETriggerEvent::Started, this,
+					   &URWTHVRWidgetInteractionComponent::OnBeginLeftClick);
+		EI->BindAction(WidgetLeftClickInputAction, ETriggerEvent::Completed, this,
+					   &URWTHVRWidgetInteractionComponent::OnEndLeftClick);
+	}
+	if (WidgetRightClickInputAction)
+	{
+		EI->BindAction(WidgetRightClickInputAction, ETriggerEvent::Started, this,
+					   &URWTHVRWidgetInteractionComponent::OnBeginRightClick);
+		EI->BindAction(WidgetRightClickInputAction, ETriggerEvent::Completed, this,
+					   &URWTHVRWidgetInteractionComponent::OnEndRightClick);
+	}
 }
 
 // Called every frame
@@ -99,17 +109,29 @@ void URWTHVRWidgetInteractionComponent::SetInteractionRayVisibility(EInteraction
 }
 
 // Forward the click to the WidgetInteraction
-void URWTHVRWidgetInteractionComponent::OnBeginClick(const FInputActionValue& Value)
+void URWTHVRWidgetInteractionComponent::OnBeginLeftClick(const FInputActionValue& Value)
 {
 	PressPointerKey(EKeys::LeftMouseButton);
 }
 
 // Forward the end click to the WidgetInteraction
-void URWTHVRWidgetInteractionComponent::OnEndClick(const FInputActionValue& Value)
+void URWTHVRWidgetInteractionComponent::OnEndLeftClick(const FInputActionValue& Value)
 {
 	ReleasePointerKey(EKeys::LeftMouseButton);
 }
 
+// Forward the click to the WidgetInteraction
+void URWTHVRWidgetInteractionComponent::OnBeginRightClick(const FInputActionValue& Value)
+{
+	PressPointerKey(EKeys::RightMouseButton);
+}
+
+// Forward the end click to the WidgetInteraction
+void URWTHVRWidgetInteractionComponent::OnEndRightClick(const FInputActionValue& Value)
+{
+	ReleasePointerKey(EKeys::RightMouseButton);
+}
+
 void URWTHVRWidgetInteractionComponent::CreateInteractionRay()
 {
 	// Only create a new static mesh component if we haven't gotten one already
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactors/RaycastInteractionComponent.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactors/RaycastInteractionComponent.cpp
index 94afba4b4426fe1b861d787826f0e1cc11d6583b..42e1aeea61a94302b199769e8f79d50314c5cc89 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/Interactors/RaycastInteractionComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactors/RaycastInteractionComponent.cpp
@@ -5,6 +5,7 @@
 
 #include "EnhancedInputComponent.h"
 #include "Interaction/Interactables/InteractableComponent.h"
+#include "Interaction/Interactables/InteractionEventType.h"
 #include "Kismet/KismetSystemLibrary.h"
 
 // Sets default values for this component's properties
@@ -25,14 +26,13 @@ void URaycastInteractionComponent::TickComponent(float DeltaTime, ELevelTick Tic
 
 	UInteractableComponent* NewInteractableComponent = nullptr;
 
-
-	TArray<AActor*> ActorsToIgnore;
 	FHitResult Hit;
 	const ETraceTypeQuery TraceType = UEngineTypes::ConvertToTraceType(ECollisionChannel::ECC_PhysicsBody);
-	FVector TraceStart = GetAttachParent()->GetComponentLocation();
-	FVector TraceEnd = GetAttachParent()->GetComponentLocation() + TraceLength * GetAttachParent()->GetForwardVector();
+	const FVector TraceStart = GetAttachParent()->GetComponentLocation();
+	const FVector TraceEnd =
+		GetAttachParent()->GetComponentLocation() + TraceLength * GetAttachParent()->GetForwardVector();
 
-	auto DebugTrace = bShowDebugTrace ? EDrawDebugTrace::ForOneFrame : EDrawDebugTrace::None;
+	const auto DebugTrace = bShowDebugTrace ? EDrawDebugTrace::ForOneFrame : EDrawDebugTrace::None;
 
 	UKismetSystemLibrary::LineTraceSingle(GetWorld(), TraceStart, TraceEnd, TraceType, true, ActorsToIgnore, DebugTrace,
 										  Hit, true, FColor::Green);
@@ -53,24 +53,28 @@ void URaycastInteractionComponent::TickComponent(float DeltaTime, ELevelTick Tic
 	if (CurrentInteractable != PreviousInteractable)
 	{
 		if (CurrentInteractable && CurrentInteractable->HasInteractionTypeFlag(EInteractorType::Raycast))
-			CurrentInteractable->HandleOnHoverStartEvents(this, EInteractorType::Raycast);
+			CurrentInteractable->HandleOnHoverEvents(this, EInteractorType::Raycast,
+													 EInteractionEventType::InteractionStart);
 		if (PreviousInteractable && PreviousInteractable->HasInteractionTypeFlag(EInteractorType::Raycast))
-			PreviousInteractable->HandleOnHoverEndEvents(this, EInteractorType::Raycast);
+			PreviousInteractable->HandleOnHoverEvents(this, EInteractorType::Raycast,
+													  EInteractionEventType::InteractionEnd);
 	}
 
 	PreviousInteractable = CurrentInteractable;
 }
 
-void URaycastInteractionComponent::OnBeginInteraction(const FInputActionValue& Value)
+void URaycastInteractionComponent::OnBeginInteractionInputAction(const FInputActionValue& Value)
 {
 	if (CurrentInteractable && CurrentInteractable->HasInteractionTypeFlag(EInteractorType::Raycast))
-		CurrentInteractable->HandleOnActionStartEvents(this, InteractionInputAction, Value, EInteractorType::Raycast);
+		CurrentInteractable->HandleOnActionEvents(this, EInteractorType::Raycast,
+												  EInteractionEventType::InteractionStart, Value);
 }
 
-void URaycastInteractionComponent::OnEndInteraction(const FInputActionValue& Value)
+void URaycastInteractionComponent::OnEndInteractionInputAction(const FInputActionValue& Value)
 {
 	if (CurrentInteractable && CurrentInteractable->HasInteractionTypeFlag(EInteractorType::Raycast))
-		CurrentInteractable->HandleOnActionEndEvents(this, InteractionInputAction, Value, EInteractorType::Raycast);
+		CurrentInteractable->HandleOnActionEvents(this, EInteractorType::Raycast, EInteractionEventType::InteractionEnd,
+												  Value);
 }
 
 void URaycastInteractionComponent::SetupPlayerInput(UInputComponent* PlayerInputComponent)
@@ -86,7 +90,7 @@ void URaycastInteractionComponent::SetupPlayerInput(UInputComponent* PlayerInput
 		return;
 
 	EI->BindAction(InteractionInputAction, ETriggerEvent::Started, this,
-				   &URaycastInteractionComponent::OnBeginInteraction);
+				   &URaycastInteractionComponent::OnBeginInteractionInputAction);
 	EI->BindAction(InteractionInputAction, ETriggerEvent::Completed, this,
-				   &URaycastInteractionComponent::OnEndInteraction);
+				   &URaycastInteractionComponent::OnEndInteractionInputAction);
 }
diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactors/UBaseInteractionComponent.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactors/UBaseInteractionComponent.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..67d718c90cdff0dce7dab7cf7bff226f778877c9
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Interaction/Interactors/UBaseInteractionComponent.cpp
@@ -0,0 +1,152 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "Interaction/Interactors/UBaseInteractionComponent.h"
+
+#include "Interaction/Interactables/ActionBehaviour.h"
+#include "Interaction/Interactables/HoverBehaviour.h"
+#include "Logging/StructuredLog.h"
+#include "Utility/RWTHVRUtilities.h"
+
+// Sets default values for this component's properties
+UUBaseInteractionComponent::UUBaseInteractionComponent()
+{
+	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these
+	// features off to improve performance if you don't need them.
+	PrimaryComponentTick.bCanEverTick = true;
+	SetIsReplicatedByDefault(true);
+	// ...
+}
+
+void UUBaseInteractionComponent::SetupPlayerInput(UInputComponent* PlayerInputComponent)
+{
+	IInputExtensionInterface::SetupPlayerInput(PlayerInputComponent);
+}
+
+void UUBaseInteractionComponent::OnBeginInteractionInputAction(const FInputActionValue& Value) {}
+
+void UUBaseInteractionComponent::OnEndInteractionInputAction(const FInputActionValue& Value) {}
+
+void UUBaseInteractionComponent::RequestHoverBehaviourReplicationStart(UHoverBehaviour* Behaviour,
+																	   const EInteractionEventType EventType,
+																	   const FHitResult& Hit)
+{
+	if (GetOwner()->HasLocalNetOwner())
+	{
+		if (GetOwner()->HasAuthority())
+		{
+			HoverBehaviourReplicationStart(Behaviour, EventType, Hit);
+		}
+		else
+		{
+			// RPC
+			ServerRequestHoverBehaviourReplicationStartRpc(Behaviour, EventType, Hit);
+		}
+	}
+	else
+	{
+		UE_LOGFMT(Toolkit, Error,
+				  "Interaction: Trying to replicate HoverBehaviour {HoverBehaviour} in InteractionComponent "
+				  "{InteractionComponent} which has no local net owner!",
+				  Behaviour->GetName(), this->GetName());
+	}
+}
+
+void UUBaseInteractionComponent::HoverBehaviourReplicationStart(UHoverBehaviour* Behaviour,
+																const EInteractionEventType EventType,
+																const FHitResult& Hit)
+{
+	if (!Behaviour->bExecuteOnServer) // this should never happen
+	{
+		UE_LOGFMT(Toolkit, Error,
+				  "Interaction: Trying to execute HoverBehaviour {HoverBehaviour} in InteractionComponent "
+				  "{InteractionComponent} on the server, but bExecuteOnServer is false!",
+				  Behaviour->GetName(), this->GetName());
+		return;
+	}
+
+	// If desired, multicast to all clients (including originator)
+	if (Behaviour->bExecuteOnAllClients)
+	{
+		MulticastHoverBehaviourReplicationStartRpc(Behaviour, EventType, Hit);
+	}
+	else
+	{
+		// Execute the behaviour only on the server
+		Behaviour->OnHoverEventEvent.Broadcast(this, EventType, Hit);
+	}
+}
+
+void UUBaseInteractionComponent::ServerRequestHoverBehaviourReplicationStartRpc_Implementation(
+	UHoverBehaviour* Behaviour, const EInteractionEventType EventType, const FHitResult& Hit)
+{
+	HoverBehaviourReplicationStart(Behaviour, EventType, Hit);
+}
+
+void UUBaseInteractionComponent::MulticastHoverBehaviourReplicationStartRpc_Implementation(
+	UHoverBehaviour* Behaviour, const EInteractionEventType EventType, const FHitResult& Hit)
+{
+	Behaviour->OnHoverEventEvent.Broadcast(this, EventType, Hit);
+}
+
+void UUBaseInteractionComponent::RequestActionBehaviourReplicationStart(UActionBehaviour* Behaviour,
+																		const EInteractionEventType EventType,
+																		const FInputActionValue& Value)
+{
+	if (GetOwner()->HasLocalNetOwner())
+	{
+		if (GetOwner()->HasAuthority())
+		{
+			ActionBehaviourReplicationStart(Behaviour, EventType, Value);
+		}
+		else
+		{
+			// RPC
+			ServerRequestActionBehaviourReplicationStartRpc(Behaviour, EventType, Value);
+		}
+	}
+	else
+	{
+		UE_LOGFMT(Toolkit, Error,
+				  "Interaction: Trying to replicate HoverBehaviour {HoverBehaviour} in InteractionComponent "
+				  "{InteractionComponent} which has no local net owner!",
+				  Behaviour->GetName(), this->GetName());
+	}
+}
+
+void UUBaseInteractionComponent::ActionBehaviourReplicationStart(UActionBehaviour* Behaviour,
+																 const EInteractionEventType EventType,
+																 const FInputActionValue& Value)
+{
+	if (!Behaviour->bExecuteOnServer) // this should never happen
+	{
+		UE_LOGFMT(Toolkit, Error,
+				  "Interaction: Trying to execute ActionBehaviour {ActionBehaviour} in InteractionComponent "
+				  "{InteractionComponent} on the server, but bExecuteOnServer is false!",
+				  Behaviour->GetName(), this->GetName());
+		return;
+	}
+
+	// If desired, multicast to all clients (including originator)
+	if (Behaviour->bExecuteOnAllClients)
+	{
+		MulticastActionBehaviourReplicationStartRpc(Behaviour, EventType, Value);
+	}
+	else
+	{
+		// Execute the behaviour only on the server
+		Behaviour->OnActionEventEvent.Broadcast(this, EventType, Value);
+	}
+}
+
+void UUBaseInteractionComponent::ServerRequestActionBehaviourReplicationStartRpc_Implementation(
+	UActionBehaviour* Behaviour, const EInteractionEventType EventType, const FInputActionValue& Value)
+{
+	ActionBehaviourReplicationStart(Behaviour, EventType, Value);
+}
+
+void UUBaseInteractionComponent::MulticastActionBehaviourReplicationStartRpc_Implementation(
+	UActionBehaviour* Behaviour, const EInteractionEventType EventType, const FInputActionValue& Value)
+{
+	Behaviour->OnActionEventEvent.Broadcast(this, EventType, Value);
+}
diff --git a/Source/RWTHVRToolkit/Private/Pawn/ClusterRepresentationActor.cpp b/Source/RWTHVRToolkit/Private/Pawn/ClusterRepresentationActor.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a280d911113902d4918555b0a6b425d31e97ff8e
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Pawn/ClusterRepresentationActor.cpp
@@ -0,0 +1,170 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "Pawn/ClusterRepresentationActor.h"
+#include "Core/RWTHVRPlayerState.h"
+#include "Kismet/GameplayStatics.h"
+#include "Logging/StructuredLog.h"
+#include "Utility/RWTHVRUtilities.h"
+
+#if PLATFORM_SUPPORTS_CLUSTER
+#include "DisplayClusterRootActor.h"
+#include "IDisplayCluster.h"
+#include "Config/IDisplayClusterConfigManager.h"
+#include "Game/IDisplayClusterGameManager.h"
+#endif
+
+// Sets default values
+AClusterRepresentationActor::AClusterRepresentationActor()
+{
+	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
+	PrimaryActorTick.bCanEverTick = false;
+	bReplicates = true;
+	SetActorEnableCollision(false);
+
+	SetRootComponent(CreateDefaultSubobject<USceneComponent>(TEXT("Origin")));
+}
+
+void AClusterRepresentationActor::BeginPlay()
+{
+	Super::BeginPlay();
+	// will fail if we're in replicated mode and PlayerState has not yet replicated fully
+	// Therefore we also execute this
+	AttachDCRAIfRequired();
+}
+
+void AClusterRepresentationActor::AttachDCRAIfRequired(const ARWTHVRPlayerState* OptionalPlayerState)
+{
+	// We need to identify the correct ClusterRepresentationActor to do the attachment.
+	// 1. Either we are the local net owner -> Primary Node Pawn
+	// This is hard to do as things might not be synchronized yet.
+	// In this case I think this is save to do because either:
+	// - We are in standalone mode, then attach it for all nodes
+	// - If we are a client, this actor has been spawned on the server only. Therefore I assume that if we
+	//   have replicated this actor to our client, we're good to go.
+
+#if PLATFORM_SUPPORTS_CLUSTER
+
+	if (!URWTHVRUtilities::IsRoomMountedMode())
+		return;
+
+	if (bIsAttached)
+	{
+		UE_LOGFMT(Toolkit, Display, "{Name} AttachDCRAIfRequired: Already attached, skipping repeated attachment.",
+				  GetName());
+		return;
+	}
+
+	UE_LOGFMT(Toolkit, Display, "{Name} AttachDCRAIfRequired: Starting DCRA Attachment process.", GetName());
+
+	// This should give us the first local player controller
+	const auto* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 0);
+
+	// Only run this for the local controller - redundant, but double check
+	if (!PlayerController || !PlayerController->IsLocalController())
+	{
+		UE_LOGFMT(Toolkit, Warning,
+				  "{Name} AttachDCRAIfRequired: PlayerController not valid or not locally controlled.", GetName());
+		return;
+	}
+	const auto* PlayerState =
+		OptionalPlayerState != nullptr ? OptionalPlayerState : PlayerController->GetPlayerState<ARWTHVRPlayerState>();
+	if (!PlayerState)
+	{
+		UE_LOGFMT(Toolkit, Error,
+				  "{Name} AttachDCRAIfRequired: PlayerState is not valid or not of type "
+				  "ARWTHVRPlayerState.",
+				  GetName());
+		return;
+	}
+
+	const auto CCA = PlayerState->GetCorrespondingClusterActor();
+
+	if (CCA == nullptr) // this can happen often if property isn't replicated yet, this is okay.
+		return;
+
+	UE_LOGFMT(Toolkit, Display,
+			  "{Name} AttachDCRAIfRequired: Player State is {PlayerState}, PlayerState->CCA is {CCA}.", GetName(),
+			  PlayerState->GetName(), CCA->GetName());
+
+	// The local player this is executed on corresponds to this actor
+	if (CCA == this)
+	{
+		UE_LOGFMT(Toolkit, Display, "{Name} AttachDCRAIfRequired: Attaching DCRA to {Name}.", GetName(),
+				  CCA->GetName());
+
+		bIsAttached = AttachDCRA();
+	}
+#endif
+}
+
+#if PLATFORM_SUPPORTS_CLUSTER
+bool AClusterRepresentationActor::AttachDCRA()
+{
+	if (URWTHVRUtilities::IsRoomMountedMode())
+	{
+		UE_LOGFMT(Toolkit, Display, "{Name}: Trying to attach DCRA", GetName());
+		auto DCRA = IDisplayCluster::Get().GetGameMgr()->GetRootActor();
+
+		if (!IsValid(DCRA))
+		{
+			UE_LOGFMT(Toolkit, Display, "No Valid DCRA in BeginPlay. Spawning manually.");
+
+			DCRA = SpawnDCRA();
+			if (!IsValid(DCRA))
+			{
+				UE_LOGFMT(Toolkit, Error, "Failed to spawn correct DCRA, cannot attach it to ClusterRepresentation.");
+				return false;
+			}
+		}
+		else // if we just spawned the DCRA, it is not yet the primary one and this check makes no sense
+		{
+			if (!DCRA->IsPrimaryRootActor())
+			{
+				UE_LOGFMT(Toolkit, Error, "Found DCRA {Name} is not the primary DCRA of Cluster with Id!",
+						  DCRA->GetName());
+				return false;
+			}
+		}
+
+		bool bAttached = DCRA->AttachToActor(this, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
+		UE_LOGFMT(Toolkit, Display, "Attaching DCRA {DCRA} to {this} returned {Res}", DCRA->GetName(), GetName(),
+				  bAttached);
+
+		DCRA->SetActorEnableCollision(false);
+	}
+	return true;
+}
+
+ADisplayClusterRootActor* AClusterRepresentationActor::SpawnDCRA()
+{
+	UDisplayClusterConfigurationData* ConfigData = IDisplayCluster::Get().GetConfigMgr()->GetConfig();
+
+
+	// Function similar to the one from DisplayClusterGameManager.
+	ADisplayClusterRootActor* RootActor = nullptr;
+	// We need to use generated class as it's the only one available in packaged buidls
+	const FString AssetPath =
+		(ConfigData->Info.AssetPath.EndsWith(TEXT("_C")) ? ConfigData->Info.AssetPath
+														 : ConfigData->Info.AssetPath + TEXT("_C"));
+
+	if (UClass* ActorClass = Cast<UClass>(StaticLoadObject(UClass::StaticClass(), nullptr, *AssetPath)))
+	{
+		// Spawn the actor
+		if (AActor* NewActor = GetWorld()->SpawnActor<AActor>(ActorClass))
+		{
+			RootActor = Cast<ADisplayClusterRootActor>(NewActor);
+
+			if (RootActor)
+			{
+				UE_LOG(Toolkit, Log, TEXT("Instantiated DCRA from asset '%s'"), *ConfigData->Info.AssetPath);
+
+				// Override actor settings in case the config file contains some updates
+				RootActor->OverrideFromConfig(ConfigData);
+			}
+		}
+	}
+	return RootActor;
+}
+
+#endif
diff --git a/Source/RWTHVRToolkit/Private/Pawn/Navigation/CollisionHandlingMovement.cpp b/Source/RWTHVRToolkit/Private/Pawn/Navigation/CollisionHandlingMovement.cpp
index d75c4d401caf16d8485458db2ed4f4e78c4689d3..62bfa4602491506185646dfd769a3876ca4b8ccb 100644
--- a/Source/RWTHVRToolkit/Private/Pawn/Navigation/CollisionHandlingMovement.cpp
+++ b/Source/RWTHVRToolkit/Private/Pawn/Navigation/CollisionHandlingMovement.cpp
@@ -1,6 +1,8 @@
 #include "Pawn/Navigation/CollisionHandlingMovement.h"
 
 #include "Kismet/KismetSystemLibrary.h"
+#include "Logging/StructuredLog.h"
+#include "Utility/RWTHVRUtilities.h"
 
 UCollisionHandlingMovement::UCollisionHandlingMovement(const FObjectInitializer& ObjectInitializer) :
 	Super(ObjectInitializer)
@@ -33,61 +35,76 @@ void UCollisionHandlingMovement::BeginPlay()
 void UCollisionHandlingMovement::TickComponent(float DeltaTime, enum ELevelTick TickType,
 											   FActorComponentTickFunction* ThisTickFunction)
 {
-	SetCapsuleColliderToUserSize();
 
-	FVector InputVector = GetPendingInputVector();
-
-	if (NavigationMode == EVRNavigationModes::NAV_WALK)
+	if (ShouldSkipUpdate(DeltaTime))
 	{
-		// you are only allowed to move horizontally in NAV_WALK
-		// everything else will be handled by stepping-up/gravity
-		// so rotate the input vector onto horizontal plane
-		const FRotator InputRot = FRotator(InputVector.Rotation());
-		const FRotator InputYaw = FRotator(0, InputRot.Yaw, 0);
-		InputVector = InputRot.UnrotateVector(InputVector);
-		InputVector = InputYaw.RotateVector(InputVector);
-		ConsumeInputVector();
-		AddInputVector(InputVector);
+		return;
 	}
 
-
-	if (NavigationMode == EVRNavigationModes::NAV_FLY || NavigationMode == EVRNavigationModes::NAV_WALK)
+	const AController* Controller = PawnOwner->GetController();
+	if (Controller && Controller->IsLocalController())
 	{
-		// if me managed to get into a collision revert the movement since last Tick
-		CheckAndRevertCollisionSinceLastTick();
-		// check whether we are still in collision e.g. if an object has moved and got us into collision
-		MoveOutOfNewDynamicCollisions();
+		SetCapsuleColliderToUserSize();
 
-		if (InputVector.Size() > 0.001)
+		FVector InputVector = GetPendingInputVector();
+
+		if (NavigationMode == EVRNavigationModes::NAV_WALK)
 		{
-			const FVector SafeSteeringInput = GetCollisionSafeVirtualSteeringVec(InputVector, DeltaTime);
-			if (SafeSteeringInput != InputVector)
+			// you are only allowed to move horizontally in NAV_WALK
+			// everything else will be handled by stepping-up/gravity
+			// so rotate the input vector onto horizontal plane
+			const FRotator InputRot = FRotator(InputVector.Rotation());
+			const FRotator InputYaw = FRotator(0, InputRot.Yaw, 0);
+			InputVector = InputRot.UnrotateVector(InputVector);
+			InputVector = InputYaw.RotateVector(InputVector);
+			ConsumeInputVector();
+			AddInputVector(InputVector);
+		}
+
+		if (NavigationMode == EVRNavigationModes::NAV_FLY || NavigationMode == EVRNavigationModes::NAV_WALK)
+		{
+			// if me managed to get into a collision revert the movement since last Tick
+			CheckAndRevertCollisionSinceLastTick();
+			// check whether we are still in collision e.g. if an object has moved and got us into collision
+			MoveOutOfNewDynamicCollisions();
+
+			if (InputVector.Size() > 0.001)
+			{
+				const FVector SafeSteeringInput = GetCollisionSafeVirtualSteeringVec(InputVector, DeltaTime);
+				if (SafeSteeringInput != InputVector)
+				{
+					// if we would move into something if we apply this input (estimating distance by max speed)
+					// we only apply its perpendicular part (unless it is facing away from the collision)
+					ConsumeInputVector();
+					AddInputVector(SafeSteeringInput);
+				}
+			}
+
+			// in case we are in a collision and collision checks are temporarily deactivated, we only allow physical
+			// movement without any checks, otherwise check collision during physical movement
+			if (bCollisionChecksTemporarilyDeactivated)
 			{
-				// if we would move into something if we apply this input (estimating distance by max speed)
-				// we only apply its perpendicular part (unless it is facing away from the collision)
 				ConsumeInputVector();
-				AddInputVector(SafeSteeringInput);
+			}
+			else
+			{
+				// so we add stepping-up (for both walk and fly)
+				// and gravity for walking only
+				MoveByGravityOrStepUp(DeltaTime);
+
+				// if we physically (in the tracking space) walked into something, move the world away (by moving the
+				// pawn)
+				CheckForPhysWalkingCollision();
 			}
 		}
 
-		// in case we are in a collision and collision checks are temporarily deactivated.
-		if (!bCollisionChecksTemporarilyDeactivated)
+		if (NavigationMode == EVRNavigationModes::NAV_NONE)
 		{
-			// so we add stepping-up (for both walk and fly)
-			// and gravity for walking only
-			MoveByGravityOrStepUp(DeltaTime);
-
-			// if we physically (in the tracking space) walked into something, move the world away (by moving the pawn)
-			CheckForPhysWalkingCollision();
+			// just remove whatever input is there
+			ConsumeInputVector();
 		}
 	}
 
-	if (NavigationMode == EVRNavigationModes::NAV_NONE)
-	{
-		// just remove whatever input is there
-		ConsumeInputVector();
-	}
-
 	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
 }
 
@@ -100,6 +117,34 @@ void UCollisionHandlingMovement::SetHeadComponent(USceneComponent* NewHeadCompon
 	CapsuleColliderComponent->SetWorldLocation(FVector(0.0f, 0.0f, -HalfHeight));
 }
 
+bool UCollisionHandlingMovement::AddActorToIgnore(AActor* ActorToIgnore)
+{
+	if (ActorToIgnore && IsValid(ActorToIgnore))
+	{
+		ActorsToIgnore.AddUnique(ActorToIgnore);
+		return true;
+	}
+	else
+	{
+		UE_LOGFMT(Toolkit, Warning, "UCollisionHandlingMovement::AddActorToIgnore: Cannot add invalid actor");
+		return false;
+	}
+}
+
+bool UCollisionHandlingMovement::RemoveActorFromIgnore(AActor* ActorToIgnore)
+{
+	if (ActorToIgnore && IsValid(ActorToIgnore))
+	{
+		const int32 NumRemoved = ActorsToIgnore.Remove(ActorToIgnore);
+		return NumRemoved > 0;
+	}
+	else
+	{
+		UE_LOGFMT(Toolkit, Warning, "UCollisionHandlingMovement::RemoveActorFromIgnore: Cannot remove invalid actor");
+		return false;
+	}
+}
+
 void UCollisionHandlingMovement::SetCapsuleColliderToUserSize() const
 {
 	// the collider should be placed
diff --git a/Source/RWTHVRToolkit/Private/Pawn/Navigation/TurnComponent.cpp b/Source/RWTHVRToolkit/Private/Pawn/Navigation/TurnComponent.cpp
index 9388a11d61314e83624b5a173e831192f07c6b6e..7e3aee8e16027a8d7e55379c38ca4fe0bc2ac537 100644
--- a/Source/RWTHVRToolkit/Private/Pawn/Navigation/TurnComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Pawn/Navigation/TurnComponent.cpp
@@ -37,22 +37,36 @@ void UTurnComponent::SetupPlayerInput(UInputComponent* PlayerInputComponent)
 	// turning
 	if (bAllowTurning)
 	{
-		// no snap turning for desktop mode
-		if (bSnapTurn && !URWTHVRUtilities::IsDesktopMode())
+		if (bSnapTurn)
 		{
-			EI->BindAction(Turn, ETriggerEvent::Started, this, &UTurnComponent::OnBeginSnapTurn);
+			// no snap turning for desktop mode
+			if (!URWTHVRUtilities::IsDesktopMode())
+			{
+				EI->BindAction(XRTurn, ETriggerEvent::Started, this, &UTurnComponent::OnBeginSnapTurn);
+			}
+			else
+			{
+				EI->BindAction(DesktopTurn, ETriggerEvent::Triggered, this, &UTurnComponent::OnBeginTurn);
+			}
 		}
 		else
 		{
-			EI->BindAction(Turn, ETriggerEvent::Triggered, this, &UTurnComponent::OnBeginTurn);
+			if (!URWTHVRUtilities::IsDesktopMode())
+			{
+				EI->BindAction(XRTurn, ETriggerEvent::Triggered, this, &UTurnComponent::OnBeginTurn);
+			}
+			else
+			{
+				EI->BindAction(DesktopTurn, ETriggerEvent::Triggered, this, &UTurnComponent::OnBeginTurn);
+			}
 		}
 	}
 
 	// bind additional functions for desktop rotations
 	if (URWTHVRUtilities::IsDesktopMode())
 	{
-		EI->BindAction(DesktopRotation, ETriggerEvent::Started, this, &UTurnComponent::StartDesktopRotation);
-		EI->BindAction(DesktopRotation, ETriggerEvent::Completed, this, &UTurnComponent::EndDesktopRotation);
+		EI->BindAction(DesktopTurnCondition, ETriggerEvent::Started, this, &UTurnComponent::StartDesktopRotation);
+		EI->BindAction(DesktopTurnCondition, ETriggerEvent::Completed, this, &UTurnComponent::EndDesktopRotation);
 	}
 }
 
diff --git a/Source/RWTHVRToolkit/Private/Pawn/RWTHVRPawn.cpp b/Source/RWTHVRToolkit/Private/Pawn/RWTHVRPawn.cpp
index d50578531749b295d79961ac45eab7ebfd097c7e..812e83ea74b96709e72b7768dbbd5d90bc041f52 100644
--- a/Source/RWTHVRToolkit/Private/Pawn/RWTHVRPawn.cpp
+++ b/Source/RWTHVRToolkit/Private/Pawn/RWTHVRPawn.cpp
@@ -8,8 +8,8 @@
 #include "ILiveLinkClient.h"
 #include "InputMappingContext.h"
 #include "Core/RWTHVRPlayerState.h"
-#include "Kismet/GameplayStatics.h"
 #include "Logging/StructuredLog.h"
+#include "Pawn/ClusterRepresentationActor.h"
 #include "Pawn/InputExtensionInterface.h"
 #include "Pawn/Navigation/CollisionHandlingMovement.h"
 #include "Pawn/ReplicatedCameraComponent.h"
@@ -61,7 +61,7 @@ void ARWTHVRPawn::Tick(float DeltaSeconds)
  * as connections now send their playertype over.
  */
 // This pawn's controller has changed! This is called on both server and owning client. If we are the owning client
-// and the master, request that the DCRA is attached to us.
+// and the master, request that the Cluster is attached to us.
 void ARWTHVRPawn::NotifyControllerChanged()
 {
 	Super::NotifyControllerChanged();
@@ -71,17 +71,17 @@ void ARWTHVRPawn::NotifyControllerChanged()
 	if (HasAuthority())
 	{
 		UE_LOG(Toolkit, Display,
-			   TEXT("ARWTHVRPawn: Player Controller has changed, trying to change DCRA attachment if possible..."));
+			   TEXT("ARWTHVRPawn: Player Controller has changed, trying to change Cluster attachment if possible..."));
 		if (const ARWTHVRPlayerState* State = GetPlayerState<ARWTHVRPlayerState>())
 		{
 			const EPlayerType Type = State->GetPlayerType();
 
 			// Only cluster types are valid here as they are set on connection.
 			// For all other player types this is a race condition
-			if (Type == EPlayerType::nDisplayPrimary || GetNetMode() == NM_Standalone)
+			if (Type == EPlayerType::nDisplayPrimary || Type == EPlayerType::nDisplaySecondary)
 			{
-				UE_LOGFMT(Toolkit, Display, "ARWTHVRPawn: Attaching DCRA to Pawn {Pawn}.", GetName());
-				AttachDCRAtoPawn();
+				UE_LOGFMT(Toolkit, Display, "ARWTHVRPawn: Attaching Cluster to Pawn {Pawn}.", GetName());
+				AttachClustertoPawn();
 			}
 		}
 	}
@@ -249,7 +249,7 @@ void ARWTHVRPawn::MulticastAddDCSyncComponent_Implementation()
 	// for some reason acts different on the nodes, therefore leading to a potential desync when
 	// e.g. colliding with an object while moving.
 
-	if (URWTHVRUtilities::IsRoomMountedMode())
+	if (URWTHVRUtilities::IsRoomMountedMode() && !SyncComponent)
 	{
 		SyncComponent = Cast<USceneComponent>(AddComponentByClass(
 			UDisplayClusterSceneComponentSyncParent::StaticClass(), false, FTransform::Identity, false));
@@ -259,35 +259,35 @@ void ARWTHVRPawn::MulticastAddDCSyncComponent_Implementation()
 #endif
 }
 
-// Executed on the server only: Finds and attaches the CaveSetup Actor, which contains the DCRA to the Pawn.
+// Executed on the server only: Attaches the ClusterRepresentation Actor, which contains the DCRA to the Pawn.
 // It is only executed on the server because attachments are synced to all clients, but not from client to server.
-void ARWTHVRPawn::AttachDCRAtoPawn()
+void ARWTHVRPawn::AttachClustertoPawn()
 {
-	if (!CaveSetupActorClass || !CaveSetupActorClass->IsValidLowLevelFast())
+	if (const ARWTHVRPlayerState* State = GetPlayerState<ARWTHVRPlayerState>())
 	{
-		UE_LOGFMT(Toolkit, Warning, "No CaveSetup Actor class set in pawn!");
-		return;
-	}
-
-	// Find our CaveSetup actor
-	TArray<AActor*> FoundActors;
-	UGameplayStatics::GetAllActorsOfClass(GetWorld(), CaveSetupActorClass, FoundActors);
-
-	if (!FoundActors.IsEmpty())
-	{
-		const auto CaveSetupActor = FoundActors[0];
-		FAttachmentTransformRules AttachmentRules = FAttachmentTransformRules::SnapToTargetNotIncludingScale;
-		CaveSetupActor->AttachToActor(this, AttachmentRules);
-		UE_LOGFMT(Toolkit, Display, "VirtualRealityPawn: Attaching CaveSetup to our pawn!");
+		const auto ClusterActor = State->GetCorrespondingClusterActor();
+		if (!ClusterActor)
+		{
+			UE_LOGFMT(
+				Toolkit, Error,
+				"ARWTHVRPawn::AttachClustertoPawn: GetCorrespondingClusterActor returned null! This won't work on "
+				"the Cave.");
+			return;
+		}
+		const FAttachmentTransformRules AttachmentRules = FAttachmentTransformRules::SnapToTargetNotIncludingScale;
+		bool bAttached = ClusterActor->AttachToComponent(GetRootComponent(), AttachmentRules);
+		// State->GetCorrespondingClusterActor()->OnAttached();
+		UE_LOGFMT(Toolkit, Display,
+				  "ARWTHVRPawn: Attaching corresponding cluster actor to our pawn returned: {Attached}", bAttached);
 	}
 	else
 	{
-		UE_LOGFMT(Toolkit, Warning,
-				  "No CaveSetup Actor found which can be attached to the Pawn! This won't work on the Cave.");
+		UE_LOGFMT(Toolkit, Error,
+				  "ARWTHVRPawn::AttachClustertoPawn: No ARWTHVRPlayerState set! This won't work on the Cave.");
 	}
 
-	// if (HasAuthority()) // Should always be the case here, but double check
-	//	MulticastAddDCSyncComponent();
+	if (HasAuthority()) // Should always be the case here, but double check
+		MulticastAddDCSyncComponent();
 }
 
 void ARWTHVRPawn::SetupMotionControllerSources()
diff --git a/Source/RWTHVRToolkit/Public/Core/RWTHVRGameModeBase.h b/Source/RWTHVRToolkit/Public/Core/RWTHVRGameModeBase.h
index 1469ac36caa9b571b765622766a01ed103b26252..be8e65d9da82cf7acc215bc8154ec9772fc7537c 100644
--- a/Source/RWTHVRToolkit/Public/Core/RWTHVRGameModeBase.h
+++ b/Source/RWTHVRToolkit/Public/Core/RWTHVRGameModeBase.h
@@ -4,6 +4,7 @@
 
 #include "CoreMinimal.h"
 #include "GameFramework/GameModeBase.h"
+#include "Pawn/ClusterRepresentationActor.h"
 #include "RWTHVRGameModeBase.generated.h"
 
 /**
@@ -15,6 +16,8 @@ UCLASS()
 class RWTHVRTOOLKIT_API ARWTHVRGameModeBase : public AGameModeBase
 {
 	GENERATED_BODY()
+public:
+	ARWTHVRGameModeBase(const FObjectInitializer& ObjectInitializer);
 
 protected:
 	/**
@@ -29,4 +32,8 @@ protected:
 	 * possess. If not, spawn a DefaultPawnClass Pawn and Possess it (Should be BP_VirtualRealityPawn to make sense).
 	 */
 	virtual void PostLogin(APlayerController* NewPlayer) override;
+
+private:
+	UPROPERTY()
+	TMap<int32, AClusterRepresentationActor*> ConnectedClusters;
 };
diff --git a/Source/RWTHVRToolkit/Public/Core/RWTHVRPlayerState.h b/Source/RWTHVRToolkit/Public/Core/RWTHVRPlayerState.h
index 4c709e204428e4cbcd20d782823676c5b8e12c7c..040c251a6db94e6564b83d120bfd62d7bb6d57e0 100644
--- a/Source/RWTHVRToolkit/Public/Core/RWTHVRPlayerState.h
+++ b/Source/RWTHVRToolkit/Public/Core/RWTHVRPlayerState.h
@@ -5,8 +5,13 @@
 #include "CoreMinimal.h"
 #include "PlayerType.h"
 #include "GameFramework/PlayerState.h"
+#include "Logging/StructuredLog.h"
+#include "Pawn/ClusterRepresentationActor.h"
+#include "Utility/RWTHVRUtilities.h"
 #include "RWTHVRPlayerState.generated.h"
 
+
+class AClusterRepresentationActor;
 enum class EPlayerType : uint8;
 /**
  * Extension of the PlayerState that additionally holds information about what type the player is.
@@ -22,6 +27,26 @@ private:
 	UPROPERTY(Replicated, Category = PlayerState, BlueprintGetter = GetPlayerType, meta = (AllowPrivateAccess))
 	EPlayerType PlayerType = EPlayerType::Desktop;
 
+	/** Replicated cluster ID for this player. Is -1 in case player is not a cluster*/
+	UPROPERTY(Replicated, Category = PlayerState, BlueprintGetter = GetCorrespondingClusterId,
+			  meta = (AllowPrivateAccess))
+	int32 CorrespondingClusterId = -1;
+
+	/** Replicated cluster actor for this player. Is nullptr in case player is not a cluster.
+	 * As this is not guaranteed to be valid on BeginPlay, we need to do a callback to the CorrespondingClusterActor
+	 * here...
+	 */
+	UPROPERTY(ReplicatedUsing = OnRep_CorrespondingClusterActor)
+	TObjectPtr<AClusterRepresentationActor> CorrespondingClusterActor;
+
+	UFUNCTION()
+	virtual void OnRep_CorrespondingClusterActor()
+	{
+		// Only execute this on the local owning player
+		if (HasLocalNetOwner())
+			CorrespondingClusterActor->AttachDCRAIfRequired(this);
+	}
+
 	UFUNCTION(Reliable, Server)
 	void ServerSetPlayerTypeRpc(EPlayerType NewPlayerType);
 
@@ -31,6 +56,15 @@ public:
 	UFUNCTION(BlueprintGetter)
 	EPlayerType GetPlayerType() const { return PlayerType; }
 
+	UFUNCTION(BlueprintGetter)
+	int32 GetCorrespondingClusterId() const { return CorrespondingClusterId; }
+
+	UFUNCTION(BlueprintGetter)
+	AClusterRepresentationActor* GetCorrespondingClusterActor() const { return CorrespondingClusterActor; }
+
+	void SetCorrespondingClusterId(int32 NewCorrespondingClusterId);
+	void SetCorrespondingClusterActor(AClusterRepresentationActor* NewCorrespondingClusterActor);
+
 	UFUNCTION(BlueprintCallable)
 	void RequestSetPlayerType(EPlayerType NewPlayerType);
 
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactables/ActionBehaviour.h b/Source/RWTHVRToolkit/Public/Interaction/Interactables/ActionBehaviour.h
index 755d876dc3205b0538f6bc394352def55b2da670..bf3afea3a1246994a0bcbfb1d96317083e2770e3 100644
--- a/Source/RWTHVRToolkit/Public/Interaction/Interactables/ActionBehaviour.h
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactables/ActionBehaviour.h
@@ -3,21 +3,18 @@
 #pragma once
 
 #include "CoreMinimal.h"
-#include "InputAction.h"
+#include "BaseBehaviour.h"
 #include "Components/SceneComponent.h"
 #include "InputActionValue.h"
-#include "InteractionBitSet.h"
 #include "ActionBehaviour.generated.h"
 
+enum EInteractionEventType : uint8;
 
-DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnActionBegin, USceneComponent*, TriggeredComponent,
-											   const UInputAction*, InputAction, const FInputActionValue&, Value);
-
-DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnActionEnd, USceneComponent*, TriggeredComponent, const UInputAction*,
-											   InputAction, const FInputActionValue&, Value);
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnActionBegin, USceneComponent*, TriggerComponent,
+											   const EInteractionEventType, EventType, const FInputActionValue&, Value);
 
 UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
-class RWTHVRTOOLKIT_API UActionBehaviour : public USceneComponent
+class RWTHVRTOOLKIT_API UActionBehaviour : public UBaseBehaviour
 {
 	GENERATED_BODY()
 
@@ -26,22 +23,23 @@ public:
 	UActionBehaviour();
 
 	UPROPERTY(BlueprintAssignable)
-	FOnActionBegin OnActionBeginEvent;
-
+	FOnActionBegin OnActionEventEvent;
+
+	/**
+	 * Replication specific:
+	 * This is only executed on the local client which processed the interaction and requested the replication process
+	 * to be started. Can be used e.g. for local effects or things that should be done both on the server and local
+	 * client. Broadcast by UInteractableComponent when the originating client sends a server rpc to start the
+	 * interaction replication.
+	 */
 	UPROPERTY(BlueprintAssignable)
-	FOnActionEnd OnActionEndEvent;
+	FOnActionBegin OnActionReplicationStartedOriginatorEvent;
+
 
 protected:
 	UFUNCTION()
-	virtual void OnActionStart(USceneComponent* TriggeredComponent, const UInputAction* InputAction,
+	virtual void OnActionEvent(USceneComponent* TriggerComponent, const EInteractionEventType EventType,
 							   const FInputActionValue& Value);
 
-	UFUNCTION()
-	virtual void OnActionEnd(USceneComponent* TriggeredComponent, const UInputAction* InputAction,
-							 const FInputActionValue& Value);
-
 	virtual void BeginPlay() override;
-
-	virtual void TickComponent(float DeltaTime, ELevelTick TickType,
-							   FActorComponentTickFunction* ThisTickFunction) override;
 };
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactables/BaseBehaviour.h b/Source/RWTHVRToolkit/Public/Interaction/Interactables/BaseBehaviour.h
new file mode 100644
index 0000000000000000000000000000000000000000..d1f379bb9431749786d2ac871d119e82f5bea46e
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactables/BaseBehaviour.h
@@ -0,0 +1,34 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Components/SceneComponent.h"
+#include "BaseBehaviour.generated.h"
+
+
+UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+class RWTHVRTOOLKIT_API UBaseBehaviour : public USceneComponent
+{
+	GENERATED_BODY()
+
+public:
+	/**
+	 * Replication part
+	 */
+
+	/**
+	 * Defines whether this behaviour will be executed on the server instead of the local client.
+	 * If set to true, an RPC is sent to the server and it will not be run locally.
+	 * */
+	UPROPERTY(EditAnywhere, BlueprintReadWrite)
+	bool bExecuteOnServer = false;
+
+	/**
+	 * Defines whether this behaviour will be executed on all connected clients that are relevant, including the
+	 * local originator client. This only has an effect if bExecuteOnServer is true, as only the server can multicast
+	 * to all other clients.
+	 */
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "bExecuteOnServer"))
+	bool bExecuteOnAllClients = false;
+};
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactables/GrabBehavior.h b/Source/RWTHVRToolkit/Public/Interaction/Interactables/GrabBehavior.h
index d10affdff4aa25e2e4c22818219375580f342c8f..65118646b4ca6edae19772cb3d5bf1df875534ee 100644
--- a/Source/RWTHVRToolkit/Public/Interaction/Interactables/GrabBehavior.h
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactables/GrabBehavior.h
@@ -20,14 +20,13 @@ class RWTHVRTOOLKIT_API UGrabBehavior : public UActionBehaviour
 	GENERATED_BODY()
 
 public:
+	UGrabBehavior();
+
 	UPROPERTY(EditAnywhere, Category = "Grabbing")
 	bool bBlockOtherInteractionsWhileGrabbed = true;
 
-	virtual void OnActionStart(USceneComponent* TriggeredComponent, const UInputAction* InputAction,
-							   const FInputActionValue& Value) override;
-	virtual void OnActionEnd(USceneComponent* TriggeredComponent, const UInputAction* InputAction,
-							 const FInputActionValue& Value) override;
-
+	UPROPERTY(EditAnywhere, Category = "Grabbing")
+	bool bIgnoreGrabbedActorInCollisionMovement = true;
 
 	/**
 	 * Called after the object was successfully attached to the hand
@@ -41,13 +40,25 @@ public:
 	UPROPERTY(BlueprintAssignable)
 	FOnGrabEnd OnGrabEndEvent;
 
+	UPROPERTY()
+	UPrimitiveComponent* MyPhysicsComponent;
+
+	virtual void BeginPlay() override;
+
 	UPrimitiveComponent* GetFirstComponentSimulatingPhysics(const AActor* TargetActor);
 
 	// recursively goes up the hierarchy and returns the highest parent simulating physics
 	UPrimitiveComponent* GetHighestParentSimulatingPhysics(UPrimitiveComponent* Comp);
 
-	UPROPERTY()
-	UPrimitiveComponent* MyPhysicsComponent;
+	UFUNCTION()
+	void ReplicationOriginaterClientCallback(USceneComponent* TriggerComponent, const EInteractionEventType EventType,
+											 const FInputActionValue& Value);
+
+	void HandleCollisionHandlingMovement(const USceneComponent* CurrentAttachParent,
+										 const EInteractionEventType EventType);
+
+	virtual void OnActionEvent(USceneComponent* TriggerComponent, const EInteractionEventType EventType,
+							   const FInputActionValue& Value) override;
 
 	UFUNCTION(BlueprintPure)
 	bool IsObjectGrabbed() const { return bObjectGrabbed; }
@@ -61,7 +72,13 @@ private:
 	 */
 	bool TryRelease();
 
+	void StartGrab(USceneComponent* TriggerComponent);
+
+	void EndGrab(USceneComponent* TriggerComponent);
+
 	bool bObjectGrabbed = false;
 
 	bool bWasSimulatingPhysics;
+
+	bool bWasAddedToIgnore = false;
 };
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactables/HoverBehaviour.h b/Source/RWTHVRToolkit/Public/Interaction/Interactables/HoverBehaviour.h
index 7557653fbf02dc8d1780bbb32804c583324281ad..efdd6eafd797bf60bc07847565bf216537309358 100644
--- a/Source/RWTHVRToolkit/Public/Interaction/Interactables/HoverBehaviour.h
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactables/HoverBehaviour.h
@@ -3,16 +3,16 @@
 #pragma once
 
 #include "CoreMinimal.h"
+#include "BaseBehaviour.h"
+#include "InteractionEventType.h"
 #include "Components/SceneComponent.h"
 #include "HoverBehaviour.generated.h"
 
-
-DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHoverStart, const USceneComponent*, TriggeredComponent, FHitResult,
-											 Hit);
-DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHoverEnd, const USceneComponent*, TriggeredComponent);
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnHoverEvent, const USceneComponent*, TriggeredComponent,
+											   const EInteractionEventType, EventType, const FHitResult&, Hit);
 
 UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
-class RWTHVRTOOLKIT_API UHoverBehaviour : public USceneComponent
+class RWTHVRTOOLKIT_API UHoverBehaviour : public UBaseBehaviour
 {
 	GENERATED_BODY()
 
@@ -22,16 +22,22 @@ public:
 	 * VRPawn) Hit: Hit Result of the trace to get access to e.g. contact point/normals etc.
 	 */
 	UPROPERTY(BlueprintAssignable)
-	FOnHoverStart OnHoverStartEvent;
+	FOnHoverEvent OnHoverEventEvent;
 
+	/**
+	 * Replication specific:
+	 * This is only executed on the local client which processed the interaction and requested the replication process
+	 * to be started. Can be used e.g. for local effects or things that should be done both on the server and local
+	 * client. Broadcast by UInteractableComponent when the originating client sends a server rpc to start the
+	 * interaction replication.
+	 */
 	UPROPERTY(BlueprintAssignable)
-	FOnHoverEnd OnHoverEndEvent;
+	FOnHoverEvent OnHoverReplicationStartedOriginatorEvent;
 
 protected:
 	UFUNCTION()
-	virtual void OnHoverStart(const USceneComponent* TriggeredComponent, FHitResult Hit);
-	UFUNCTION()
-	virtual void OnHoverEnd(const USceneComponent* TriggeredComponent);
+	virtual void OnHoverEvent(const USceneComponent* TriggerComponent, EInteractionEventType EventType,
+							  const FHitResult& Hit);
 
 	virtual void BeginPlay() override;
 };
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractableComponent.h b/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractableComponent.h
index da85ad8b003814098fc21e112de423a097a80535..7ef7078b931eebdc43d94932ebe6d615a8a7a442 100644
--- a/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractableComponent.h
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractableComponent.h
@@ -9,6 +9,8 @@
 #include "InteractableComponent.generated.h"
 
 
+enum EInteractionEventType : uint8;
+class UBaseBehaviour;
 struct FInputActionValue;
 class UActionBehaviour;
 class UHoverBehaviour;
@@ -53,7 +55,7 @@ public:
 	bool bAllowInteractionFromChildGeometry = true;
 
 	UFUNCTION(BlueprintCallable)
-	FORCEINLINE bool HasInteractionTypeFlag(EInteractorType type) { return type & InteractorFilter; }
+	FORCEINLINE bool HasInteractionTypeFlag(EInteractorType Type) { return Type & InteractorFilter; }
 
 	/**
 	 * @brief Restrict interactability to given components (e.g. if an object is grabbed, block interactions from other
@@ -74,12 +76,10 @@ protected:
 	virtual void BeginPlay() override;
 
 public:
-	void HandleOnHoverStartEvents(USceneComponent* TriggerComponent, const EInteractorType Interactor);
-	void HandleOnHoverEndEvents(USceneComponent* TriggerComponent, const EInteractorType Interactor);
-	void HandleOnActionStartEvents(USceneComponent* TriggerComponent, const UInputAction* InputAction,
-								   const FInputActionValue& Value, const EInteractorType Interactor);
-	void HandleOnActionEndEvents(USceneComponent* TriggerComponent, const UInputAction* InputAction,
-								 const FInputActionValue& Value, const EInteractorType Interactor);
+	void HandleOnHoverEvents(USceneComponent* TriggerComponent, const EInteractorType Interactor,
+							 const EInteractionEventType EventType);
+	void HandleOnActionEvents(USceneComponent* TriggerComponent, const EInteractorType Interactor,
+							  const EInteractionEventType EventType, const FInputActionValue& Value);
 
 	/**
 	 * @brief If hover and action behaviors are not explicitly specified, load all existing ones
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractionEventType.h b/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractionEventType.h
new file mode 100644
index 0000000000000000000000000000000000000000..9e24c211276613c6ea0e7b11037d59e05caefba0
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractionEventType.h
@@ -0,0 +1,8 @@
+#pragma once
+
+UENUM(BlueprintType)
+enum EInteractionEventType : uint8
+{
+	InteractionStart = 0,
+	InteractionEnd = 1,
+};
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactors/DirectInteractionComponent.h b/Source/RWTHVRToolkit/Public/Interaction/Interactors/DirectInteractionComponent.h
index 69ef74ff844a727c11a333047a7f011a12d01241..46d2d160358d991a70e232bce73bc6c9a57c52ef 100644
--- a/Source/RWTHVRToolkit/Public/Interaction/Interactors/DirectInteractionComponent.h
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactors/DirectInteractionComponent.h
@@ -3,13 +3,13 @@
 #pragma once
 
 #include "CoreMinimal.h"
+#include "UBaseInteractionComponent.h"
 #include "Components/SceneComponent.h"
 #include "Interaction/Interactables/InteractableComponent.h"
-#include "Pawn/InputExtensionInterface.h"
 #include "DirectInteractionComponent.generated.h"
 
 UCLASS(Abstract, Blueprintable)
-class RWTHVRTOOLKIT_API UDirectInteractionComponent : public USceneComponent, public IInputExtensionInterface
+class RWTHVRTOOLKIT_API UDirectInteractionComponent : public UUBaseInteractionComponent
 {
 	GENERATED_BODY()
 
@@ -20,29 +20,18 @@ public:
 	virtual void TickComponent(float DeltaTime, ELevelTick TickType,
 							   FActorComponentTickFunction* ThisTickFunction) override;
 
-	UPROPERTY(EditAnywhere, Category = "Input")
-	class UInputAction* InteractionInputAction;
-
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Direct Interaction")
 	float InteractionSphereRadius = 15.0;
 
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Direct Interaction")
-	bool bShowDebugTrace = false;
-
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Direct Interaction")
 	bool bOnlyInteractWithClosestActor = false;
 
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Direct Interaction")
-	TArray<AActor*> ActorsToIgnore;
-
 	virtual void SetupPlayerInput(UInputComponent* PlayerInputComponent) override;
 
 private:
-	UFUNCTION()
-	void OnBeginInteraction(const FInputActionValue& Value);
+	virtual void OnBeginInteractionInputAction(const FInputActionValue& Value) override;
 
-	UFUNCTION()
-	void OnEndInteraction(const FInputActionValue& Value);
+	virtual void OnEndInteractionInputAction(const FInputActionValue& Value) override;
 
 	UPROPERTY()
 	TArray<UInteractableComponent*> PreviousInteractableComponentsInRange;
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactors/RWTHVRWidgetInteractionComponent.h b/Source/RWTHVRToolkit/Public/Interaction/Interactors/RWTHVRWidgetInteractionComponent.h
index 4b1139cf89724faea664f4ac4e2e42f9cf1bb9db..34770d50d324a6f2e311f898636aefc1610a5140 100644
--- a/Source/RWTHVRToolkit/Public/Interaction/Interactors/RWTHVRWidgetInteractionComponent.h
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactors/RWTHVRWidgetInteractionComponent.h
@@ -44,14 +44,22 @@ public:
 	TEnumAsByte<EInteractionRayVisibility> InteractionRayVisibility = EInteractionRayVisibility::Invisible;
 
 	UPROPERTY(EditAnywhere, Category = "Input")
-	class UInputAction* WidgetClickInputAction;
+	class UInputAction* WidgetLeftClickInputAction;
+	UPROPERTY(EditAnywhere, Category = "Input")
+	class UInputAction* WidgetRightClickInputAction;
 
 private:
 	UFUNCTION()
-	void OnBeginClick(const FInputActionValue& Value);
+	void OnBeginLeftClick(const FInputActionValue& Value);
+
+	UFUNCTION()
+	void OnEndLeftClick(const FInputActionValue& Value);
+
+	UFUNCTION()
+	void OnBeginRightClick(const FInputActionValue& Value);
 
 	UFUNCTION()
-	void OnEndClick(const FInputActionValue& Value);
+	void OnEndRightClick(const FInputActionValue& Value);
 
 	void CreateInteractionRay();
 	void SetupInteractionRay();
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactors/RaycastInteractionComponent.h b/Source/RWTHVRToolkit/Public/Interaction/Interactors/RaycastInteractionComponent.h
index f5c93123e8ba136e747796825f93309b285109b0..31441d516c5a34923c2ca6d26665a50a9f6069ed 100644
--- a/Source/RWTHVRToolkit/Public/Interaction/Interactors/RaycastInteractionComponent.h
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactors/RaycastInteractionComponent.h
@@ -2,16 +2,15 @@
 
 #pragma once
 
-#include <Pawn/InputExtensionInterface.h>
-
 #include "CoreMinimal.h"
+#include "UBaseInteractionComponent.h"
 #include "Components/SceneComponent.h"
 #include "Interaction/Interactables/InteractableComponent.h"
 #include "RaycastInteractionComponent.generated.h"
 
 
 UCLASS(Abstract, Blueprintable)
-class RWTHVRTOOLKIT_API URaycastInteractionComponent : public USceneComponent, public IInputExtensionInterface
+class RWTHVRTOOLKIT_API URaycastInteractionComponent : public UUBaseInteractionComponent
 {
 	GENERATED_BODY()
 
@@ -23,20 +22,13 @@ public:
 	virtual void TickComponent(float DeltaTime, ELevelTick TickType,
 							   FActorComponentTickFunction* ThisTickFunction) override;
 
-	UPROPERTY(EditAnywhere, Category = "Input")
-	class UInputAction* InteractionInputAction;
-
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Raycast")
 	float TraceLength = 3000.0;
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Raycast")
-	bool bShowDebugTrace = false;
 
 private:
-	UFUNCTION()
-	void OnBeginInteraction(const FInputActionValue& Value);
+	virtual void OnBeginInteractionInputAction(const FInputActionValue& Value) override;
 
-	UFUNCTION()
-	void OnEndInteraction(const FInputActionValue& Value);
+	virtual void OnEndInteractionInputAction(const FInputActionValue& Value) override;
 
 public:
 	virtual void SetupPlayerInput(UInputComponent* PlayerInputComponent) override;
diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactors/UBaseInteractionComponent.h b/Source/RWTHVRToolkit/Public/Interaction/Interactors/UBaseInteractionComponent.h
new file mode 100644
index 0000000000000000000000000000000000000000..ca97fd7e36f1359273734f7b88da77777fdfa7b0
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Interaction/Interactors/UBaseInteractionComponent.h
@@ -0,0 +1,103 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Components/SceneComponent.h"
+#include "Pawn/InputExtensionInterface.h"
+#include "UBaseInteractionComponent.generated.h"
+
+
+struct FInputActionValue;
+enum EInteractionEventType : uint8;
+class UActionBehaviour;
+class UHoverBehaviour;
+
+UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+class RWTHVRTOOLKIT_API UUBaseInteractionComponent : public USceneComponent, public IInputExtensionInterface
+{
+	GENERATED_BODY()
+
+public:
+	// Sets default values for this component's properties
+	UUBaseInteractionComponent();
+
+	virtual void SetupPlayerInput(UInputComponent* PlayerInputComponent) override;
+
+	UPROPERTY(EditAnywhere, Category = "Input")
+	class UInputAction* InteractionInputAction;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Base Interaction")
+	TArray<AActor*> ActorsToIgnore;
+
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Base Interaction")
+	bool bShowDebugTrace = false;
+
+	/*
+	 * Replication part
+	 */
+
+
+	/**
+	 * Requests the start of the replication process for the given HoverBehaviour, EventType and Hit.
+	 * Depending on authority, this executes the replication of the behaviour directly or requests it via a server RPC.
+	 */
+	void RequestHoverBehaviourReplicationStart(UHoverBehaviour* Behaviour, const EInteractionEventType EventType,
+											   const FHitResult& Hit);
+
+	/**
+	 * This is executed on the server/authority. The behaviour is actually executed directly on the server, or
+	 * multicast to all connected clients. The multicast then executes the behaviour.
+	 */
+	void HoverBehaviourReplicationStart(UHoverBehaviour* Behaviour, const EInteractionEventType EventType,
+										const FHitResult& Hit);
+
+	/**
+	 * This is only executed on the local client which processed the interaction and requested the replication process
+	 * to be started. Can be used e.g. for local effects or things that should be done both on the server and local
+	 * client.
+	 */
+	void HoverBehaviourReplicationOriginatorCallback(UHoverBehaviour* Behaviour, const EInteractionEventType EventType,
+													 const FHitResult& Hit);
+
+	/**
+	 * Requests the start of the replication process for the given ActionBehaviour, EventType and the Value of the Input
+	 * Action. Depending on authority, this executes the replication of the behaviour directly or requests it via a
+	 * server RPC.
+	 */
+	void RequestActionBehaviourReplicationStart(UActionBehaviour* Behaviour, const EInteractionEventType EventType,
+												const FInputActionValue& Value);
+
+	/**
+	 * This is executed on the server/authority. The behaviour is actually executed directly on the server, or
+	 * multicast to all connected clients. The multicast then executes the behaviour.
+	 */
+	void ActionBehaviourReplicationStart(UActionBehaviour* Behaviour, const EInteractionEventType EventType,
+										 const FInputActionValue& Value);
+
+
+	// RPCs
+	UFUNCTION(Server, Reliable)
+	void ServerRequestHoverBehaviourReplicationStartRpc(UHoverBehaviour* Behaviour,
+														const EInteractionEventType EventType, const FHitResult& Hit);
+
+	UFUNCTION(NetMulticast, Reliable)
+	void MulticastHoverBehaviourReplicationStartRpc(UHoverBehaviour* Behaviour, const EInteractionEventType EventType,
+													const FHitResult& Hit);
+
+	UFUNCTION(Server, Reliable)
+	void ServerRequestActionBehaviourReplicationStartRpc(UActionBehaviour* Behaviour,
+														 const EInteractionEventType EventType,
+														 const FInputActionValue& Value);
+
+	UFUNCTION(NetMulticast, Reliable)
+	void MulticastActionBehaviourReplicationStartRpc(UActionBehaviour* Behaviour, const EInteractionEventType EventType,
+													 const FInputActionValue& Value);
+
+private:
+	UFUNCTION()
+	virtual void OnBeginInteractionInputAction(const FInputActionValue& Value);
+
+	UFUNCTION()
+	virtual void OnEndInteractionInputAction(const FInputActionValue& Value);
+};
diff --git a/Source/RWTHVRToolkit/Public/Pawn/ClusterRepresentationActor.h b/Source/RWTHVRToolkit/Public/Pawn/ClusterRepresentationActor.h
new file mode 100644
index 0000000000000000000000000000000000000000..7826cb001ff5b5ea035f3e6b96920c3edb215138
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Pawn/ClusterRepresentationActor.h
@@ -0,0 +1,36 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+
+#include "ClusterRepresentationActor.generated.h"
+
+
+class ARWTHVRPlayerState;
+#if PLATFORM_SUPPORTS_CLUSTER
+class ADisplayClusterRootActor;
+#endif
+
+UCLASS()
+class RWTHVRTOOLKIT_API AClusterRepresentationActor : public AActor
+{
+	GENERATED_BODY()
+
+public:
+	// Sets default values for this actor's properties
+	AClusterRepresentationActor();
+
+	virtual void BeginPlay() override;
+
+	void AttachDCRAIfRequired(const ARWTHVRPlayerState* OptionalPlayerState = nullptr);
+
+private:
+	bool bIsAttached = false;
+
+#if PLATFORM_SUPPORTS_CLUSTER
+	bool AttachDCRA();
+	ADisplayClusterRootActor* SpawnDCRA();
+
+#endif
+};
diff --git a/Source/RWTHVRToolkit/Public/Pawn/Navigation/CollisionHandlingMovement.h b/Source/RWTHVRToolkit/Public/Pawn/Navigation/CollisionHandlingMovement.h
index 6166c84a1c6f2aee1adfdd2d7b5080ac027d23cd..b9cfad134d1b57e12d6be08a82b56935f3d4674b 100644
--- a/Source/RWTHVRToolkit/Public/Pawn/Navigation/CollisionHandlingMovement.h
+++ b/Source/RWTHVRToolkit/Public/Pawn/Navigation/CollisionHandlingMovement.h
@@ -42,6 +42,12 @@ public:
 
 	void SetHeadComponent(USceneComponent* NewHeadComponent);
 
+	UFUNCTION(BlueprintCallable)
+	bool AddActorToIgnore(AActor* ActorToIgnore);
+
+	UFUNCTION(BlueprintCallable)
+	bool RemoveActorFromIgnore(AActor* ActorToIgnore);
+
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VR Movement")
 	EVRNavigationModes NavigationMode = EVRNavigationModes::NAV_WALK;
 
diff --git a/Source/RWTHVRToolkit/Public/Pawn/Navigation/TurnComponent.h b/Source/RWTHVRToolkit/Public/Pawn/Navigation/TurnComponent.h
index 96de1a8b08b770ad7716839aae2ce138dee93321..72cbaf558d17f75beea4826690f05324b802b8bf 100644
--- a/Source/RWTHVRToolkit/Public/Pawn/Navigation/TurnComponent.h
+++ b/Source/RWTHVRToolkit/Public/Pawn/Navigation/TurnComponent.h
@@ -35,10 +35,13 @@ public:
 	float SnapTurnAngle = 22.5;
 
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input|Actions")
-	class UInputAction* Turn;
+	class UInputAction* XRTurn;
 
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input|Actions")
-	class UInputAction* DesktopRotation;
+	class UInputAction* DesktopTurn;
+
+	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input|Actions")
+	class UInputAction* DesktopTurnCondition;
 
 	/**
 	 * Called every tick as long as stick input is received to allow for continuous turning
diff --git a/Source/RWTHVRToolkit/Public/Pawn/RWTHVRPawn.h b/Source/RWTHVRToolkit/Public/Pawn/RWTHVRPawn.h
index 260cdc0408e5f2d1ef9d64c7f56ba4325a8ff123..cd81e497181dcfef757b067ccae8c862c022fa34 100644
--- a/Source/RWTHVRToolkit/Public/Pawn/RWTHVRPawn.h
+++ b/Source/RWTHVRToolkit/Public/Pawn/RWTHVRPawn.h
@@ -79,10 +79,6 @@ public:
 	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Pawn|LiveLink")
 	bool bWorldTransform = false;
 
-	/* The class which to search for DCRA attachment. TODO: Make this better it's ugly */
-	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Pawn|LiveLink")
-	TSubclassOf<AActor> CaveSetupActorClass;
-
 protected:
 	virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
 	void AddInputMappingContext(const APlayerController* PC, const UInputMappingContext* Context) const;
@@ -107,8 +103,8 @@ protected:
 	UFUNCTION(Reliable, NetMulticast)
 	void MulticastAddDCSyncComponent();
 
-	/* Attaches the DCRA to the pawn */
-	void AttachDCRAtoPawn();
+	/* Attaches the Cluster representation to the pawn */
+	void AttachClustertoPawn();
 
 	/* Set device specific motion controller sources (None, L/R, Livelink) */
 	void SetupMotionControllerSources();