diff --git a/Content/BP_VirtualRealityPawn.uasset b/Content/BP_VirtualRealityPawn.uasset
index 0a1470567c99b3bc47a7d853c939cb662acb6571..59b1068d003c4eb9241cce12fea142e357f9a6c6 100644
Binary files a/Content/BP_VirtualRealityPawn.uasset and b/Content/BP_VirtualRealityPawn.uasset differ
diff --git a/Content/Components/Movement/Teleportation/VRTeleportVisualizer.uasset b/Content/Components/Movement/Teleportation/VRTeleportVisualizer.uasset
index 8391f7295fbb35e9cb7d8172853ebd0393e5714f..9f96db1fd710b54a94b08f539a568ba1e7358781 100644
Binary files a/Content/Components/Movement/Teleportation/VRTeleportVisualizer.uasset and b/Content/Components/Movement/Teleportation/VRTeleportVisualizer.uasset differ
diff --git a/Content/RWTHVRCluster/CaveSetup.uasset b/Content/RWTHVRCluster/CaveSetup.uasset
new file mode 100644
index 0000000000000000000000000000000000000000..e31ab31976b8e9e40030c8b9fdbf04b50a11114a
Binary files /dev/null and b/Content/RWTHVRCluster/CaveSetup.uasset differ
diff --git a/Content/RWTHVRCluster/Config/ExampleConfigs/NDC_CaveUnwrap.ndisplay b/Content/RWTHVRCluster/Config/ExampleConfigs/NDC_CaveUnwrap.ndisplay
new file mode 100644
index 0000000000000000000000000000000000000000..b3a186da5f2e294797965d3fd53eead5b74767e2
--- /dev/null
+++ b/Content/RWTHVRCluster/Config/ExampleConfigs/NDC_CaveUnwrap.ndisplay
@@ -0,0 +1,677 @@
+{
+	"nDisplay":
+	{
+		"description": "nDisplay configuration",
+		"version": "5.00",
+		"assetPath": "/RWTHVRToolkit/RWTHVRCluster/Config/ExampleConfigs/NDC_CaveUnwrap.NDC_CaveUnwrap",
+		"misc":
+		{
+			"bFollowLocalPlayerCamera": false,
+			"bExitOnEsc": true,
+			"bOverrideViewportsFromExternalConfig": false
+		},
+		"scene":
+		{
+			"xforms":
+			{
+				"cave_origin":
+				{
+					"parentId": "",
+					"location":
+					{
+						"x": 0,
+						"y": 0,
+						"z": 10
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"cave_center":
+				{
+					"parentId": "cave_origin",
+					"location":
+					{
+						"x": 0,
+						"y": 0,
+						"z": 100
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"display_front":
+				{
+					"parentId": "cave_center",
+					"location":
+					{
+						"x": 150,
+						"y": 0,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"display_floor":
+				{
+					"parentId": "cave_center",
+					"location":
+					{
+						"x": 0,
+						"y": 0,
+						"z": -100
+					},
+					"rotation":
+					{
+						"pitch": -90,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"angle_left":
+				{
+					"parentId": "cave_center",
+					"location":
+					{
+						"x": 150,
+						"y": -150,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": -89.999992370605469,
+						"roll": 0
+					}
+				},
+				"display_left":
+				{
+					"parentId": "angle_left",
+					"location":
+					{
+						"x": -1.52587890625e-05,
+						"y": -149.99996948242188,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"angle_right":
+				{
+					"parentId": "cave_center",
+					"location":
+					{
+						"x": 150,
+						"y": 150,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 89.999992370605469,
+						"roll": 0
+					}
+				},
+				"display_right":
+				{
+					"parentId": "angle_right",
+					"location":
+					{
+						"x": 0,
+						"y": 150,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"angle_ceiling":
+				{
+					"parentId": "cave_center",
+					"location":
+					{
+						"x": 150,
+						"y": 0,
+						"z": 100
+					},
+					"rotation":
+					{
+						"pitch": 90,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"display_ceiling":
+				{
+					"parentId": "angle_ceiling",
+					"location":
+					{
+						"x": -7.62939453125e-06,
+						"y": 0,
+						"z": 74.999984741210938
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				}
+			},
+			"cameras":
+			{
+				"DefaultViewPoint":
+				{
+					"interpupillaryDistance": 6.4000000953674316,
+					"swapEyes": false,
+					"stereoOffset": "none",
+					"parentId": "",
+					"location":
+					{
+						"x": 0,
+						"y": 0,
+						"z": 50
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				}
+			},
+			"screens":
+			{
+				"scr_left":
+				{
+					"size":
+					{
+						"width": 300,
+						"height": 200
+					},
+					"parentId": "display_left",
+					"location":
+					{
+						"x": 0,
+						"y": 0,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"scr_front":
+				{
+					"size":
+					{
+						"width": 300,
+						"height": 200
+					},
+					"parentId": "display_front",
+					"location":
+					{
+						"x": 0,
+						"y": 0,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"scr_right":
+				{
+					"size":
+					{
+						"width": 300,
+						"height": 200
+					},
+					"parentId": "display_right",
+					"location":
+					{
+						"x": 0,
+						"y": 0,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"scr_ceiling":
+				{
+					"size":
+					{
+						"width": 300,
+						"height": 150
+					},
+					"parentId": "display_ceiling",
+					"location":
+					{
+						"x": 0,
+						"y": 0,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				},
+				"scr_floor":
+				{
+					"size":
+					{
+						"width": 300,
+						"height": 300
+					},
+					"parentId": "display_floor",
+					"location":
+					{
+						"x": 0,
+						"y": 0,
+						"z": 0
+					},
+					"rotation":
+					{
+						"pitch": 0,
+						"yaw": 0,
+						"roll": 0
+					}
+				}
+			}
+		},
+		"cluster":
+		{
+			"primaryNode":
+			{
+				"id": "node_left",
+				"ports":
+				{
+					"ClusterSync": 41001,
+					"ClusterEventsJson": 41003,
+					"ClusterEventsBinary": 41004
+				}
+			},
+			"sync":
+			{
+				"renderSyncPolicy":
+				{
+					"type": "ethernet",
+					"parameters":
+					{
+					}
+				},
+				"inputSyncPolicy":
+				{
+					"type": "ReplicatePrimary",
+					"parameters":
+					{
+					}
+				}
+			},
+			"network":
+			{
+				"ConnectRetriesAmount": "300",
+				"ConnectRetryDelay": "1000",
+				"GameStartBarrierTimeout": "18000000",
+				"FrameStartBarrierTimeout": "1800000",
+				"FrameEndBarrierTimeout": "1800000",
+				"RenderSyncBarrierTimeout": "1800000"
+			},
+			"failover":
+			{
+				"failoverPolicy": "Disabled"
+			},
+			"nodes":
+			{
+				"node_left":
+				{
+					"host": "127.0.0.1",
+					"sound": true,
+					"fullScreen": false,
+					"renderHeadless": false,
+					"graphicsAdapter": -1,
+					"textureShare": false,
+					"window":
+					{
+						"x": 0,
+						"y": 300,
+						"w": 600,
+						"h": 400
+					},
+					"postprocess":
+					{
+					},
+					"viewports":
+					{
+						"vp_left":
+						{
+							"camera": "DefaultViewPoint",
+							"bufferRatio": 1,
+							"gPUIndex": -1,
+							"allowCrossGPUTransfer": false,
+							"isShared": false,
+							"overscan":
+							{
+								"bEnabled": false,
+								"mode": "percent",
+								"left": 0,
+								"right": 0,
+								"top": 0,
+								"bottom": 0,
+								"oversize": true
+							},
+							"region":
+							{
+								"x": 0,
+								"y": 0,
+								"w": 600,
+								"h": 400
+							},
+							"projectionPolicy":
+							{
+								"type": "simple",
+								"parameters":
+								{
+									"screen": "scr_left"
+								}
+							}
+						}
+					},
+					"outputRemap":
+					{
+						"bEnable": false,
+						"dataSource": "mesh",
+						"staticMeshAsset": "",
+						"externalFile": ""
+					}
+				},
+				"node_front":
+				{
+					"host": "134.130.70.51",
+					"sound": false,
+					"fullScreen": false,
+					"renderHeadless": false,
+					"graphicsAdapter": -1,
+					"textureShare": false,
+					"window":
+					{
+						"x": 0,
+						"y": 0,
+						"w": 600,
+						"h": 400
+					},
+					"postprocess":
+					{
+					},
+					"viewports":
+					{
+						"vp_front":
+						{
+							"camera": "DefaultViewPoint",
+							"bufferRatio": 1,
+							"gPUIndex": -1,
+							"allowCrossGPUTransfer": false,
+							"isShared": false,
+							"overscan":
+							{
+								"bEnabled": false,
+								"mode": "percent",
+								"left": 0,
+								"right": 0,
+								"top": 0,
+								"bottom": 0,
+								"oversize": true
+							},
+							"region":
+							{
+								"x": 0,
+								"y": 0,
+								"w": 600,
+								"h": 400
+							},
+							"projectionPolicy":
+							{
+								"type": "simple",
+								"parameters":
+								{
+									"screen": "scr_front"
+								}
+							}
+						}
+					},
+					"outputRemap":
+					{
+						"bEnable": false,
+						"dataSource": "mesh",
+						"staticMeshAsset": "",
+						"externalFile": ""
+					}
+				},
+				"node_right":
+				{
+					"host": "127.0.0.1",
+					"sound": false,
+					"fullScreen": false,
+					"renderHeadless": false,
+					"graphicsAdapter": -1,
+					"textureShare": false,
+					"window":
+					{
+						"x": 600,
+						"y": 900,
+						"w": 600,
+						"h": 400
+					},
+					"postprocess":
+					{
+					},
+					"viewports":
+					{
+						"vp_right":
+						{
+							"camera": "DefaultViewPoint",
+							"bufferRatio": 1,
+							"gPUIndex": -1,
+							"allowCrossGPUTransfer": false,
+							"isShared": false,
+							"overscan":
+							{
+								"bEnabled": false,
+								"mode": "percent",
+								"left": 0,
+								"right": 0,
+								"top": 0,
+								"bottom": 0,
+								"oversize": true
+							},
+							"region":
+							{
+								"x": 0,
+								"y": 0,
+								"w": 600,
+								"h": 400
+							},
+							"projectionPolicy":
+							{
+								"type": "simple",
+								"parameters":
+								{
+									"screen": "scr_right"
+								}
+							}
+						}
+					},
+					"outputRemap":
+					{
+						"bEnable": false,
+						"dataSource": "mesh",
+						"staticMeshAsset": "",
+						"externalFile": ""
+					}
+				},
+				"node_ceiling":
+				{
+					"host": "127.0.0.1",
+					"sound": false,
+					"fullScreen": false,
+					"renderHeadless": false,
+					"graphicsAdapter": -1,
+					"textureShare": false,
+					"window":
+					{
+						"x": 600,
+						"y": 0,
+						"w": 600,
+						"h": 300
+					},
+					"postprocess":
+					{
+					},
+					"viewports":
+					{
+						"vp_ceiling":
+						{
+							"camera": "DefaultViewPoint",
+							"bufferRatio": 1,
+							"gPUIndex": -1,
+							"allowCrossGPUTransfer": false,
+							"isShared": false,
+							"overscan":
+							{
+								"bEnabled": false,
+								"mode": "percent",
+								"left": 0,
+								"right": 0,
+								"top": 0,
+								"bottom": 0,
+								"oversize": true
+							},
+							"region":
+							{
+								"x": 0,
+								"y": 0,
+								"w": 600,
+								"h": 300
+							},
+							"projectionPolicy":
+							{
+								"type": "simple",
+								"parameters":
+								{
+									"screen": "scr_ceiling"
+								}
+							}
+						}
+					},
+					"outputRemap":
+					{
+						"bEnable": false,
+						"dataSource": "mesh",
+						"staticMeshAsset": "",
+						"externalFile": ""
+					}
+				},
+				"node_floor":
+				{
+					"host": "127.0.0.1",
+					"sound": false,
+					"fullScreen": false,
+					"renderHeadless": false,
+					"graphicsAdapter": -1,
+					"textureShare": false,
+					"window":
+					{
+						"x": 600,
+						"y": 300,
+						"w": 600,
+						"h": 600
+					},
+					"postprocess":
+					{
+					},
+					"viewports":
+					{
+						"vp_floor":
+						{
+							"camera": "DefaultViewPoint",
+							"bufferRatio": 1,
+							"gPUIndex": -1,
+							"allowCrossGPUTransfer": false,
+							"isShared": false,
+							"overscan":
+							{
+								"bEnabled": false,
+								"mode": "percent",
+								"left": 0,
+								"right": 0,
+								"top": 0,
+								"bottom": 0,
+								"oversize": true
+							},
+							"region":
+							{
+								"x": 0,
+								"y": 0,
+								"w": 600,
+								"h": 600
+							},
+							"projectionPolicy":
+							{
+								"type": "simple",
+								"parameters":
+								{
+									"screen": "scr_floor"
+								}
+							}
+						}
+					},
+					"outputRemap":
+					{
+						"bEnable": false,
+						"dataSource": "mesh",
+						"staticMeshAsset": "",
+						"externalFile": ""
+					}
+				}
+			}
+		},
+		"customParameters":
+		{
+			"SampleArg1": "SampleVal1",
+			"SampleArg2": "SampleVal2"
+		},
+		"diagnostics":
+		{
+			"simulateLag": false,
+			"minLagTime": 0.0099999997764825821,
+			"maxLagTime": 0.5
+		}
+	}
+}
\ No newline at end of file
diff --git a/Content/RWTHVRCluster/Config/aixcave.uasset b/Content/RWTHVRCluster/Config/aixcave.uasset
index f2fa965197363c13ea349b2606d6fec0fe2281fa..68bfe315f998e4497f1f030a78d46c6640b7295a 100644
Binary files a/Content/RWTHVRCluster/Config/aixcave.uasset and b/Content/RWTHVRCluster/Config/aixcave.uasset differ
diff --git a/Content/RWTHVRGameMode.uasset b/Content/RWTHVRGameMode.uasset
index bbe367b340d7b98310000099bb7ffa8e3e088b0f..98183e89f709fbd7dbeb90756e70a0a14c67982d 100644
Binary files a/Content/RWTHVRGameMode.uasset and b/Content/RWTHVRGameMode.uasset differ
diff --git a/Source/RWTHVRToolkit/Private/Core/ClientTransformReplication.cpp b/Source/RWTHVRToolkit/Private/Core/ClientTransformReplication.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..27a4cc4afd522ae67fd9c50107a2066980e0da08
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Core/ClientTransformReplication.cpp
@@ -0,0 +1,78 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#include "Core/ClientTransformReplication.h"
+
+#include "Net/UnrealNetwork.h"
+
+UClientTransformReplication::UClientTransformReplication()
+{
+	PrimaryComponentTick.bCanEverTick = true;
+	PrimaryComponentTick.TickGroup = TG_PostUpdateWork;
+	PrimaryComponentTick.SetTickFunctionEnable(true);
+	SetIsReplicatedByDefault(true);
+
+	// Direct transform replication
+	ControllerNetUpdateRate = 100.0f; // 100 htz is default
+	ControllerNetUpdateCount = 0.0f;
+}
+
+// Naive direct transform replication (replace with input rep?)
+void UClientTransformReplication::UpdateState(float DeltaTime)
+{
+	const auto* OwningActor = GetOwner();
+	if (OwningActor && OwningActor->HasLocalNetOwner())
+	{
+		if (GetIsReplicated())
+		{
+			const FVector Loc = OwningActor->GetActorLocation();
+			const FRotator Rot = OwningActor->GetActorRotation();
+
+			if (!Loc.Equals(ReplicatedTransform.Position) || !Rot.Equals(ReplicatedTransform.Rotation))
+			{
+				ControllerNetUpdateCount += DeltaTime;
+				if (ControllerNetUpdateCount >= (1.0f / ControllerNetUpdateRate)) // todo save inverse?
+				{
+					ControllerNetUpdateCount = 0.0f;
+
+					ReplicatedTransform.Position = Loc;
+					ReplicatedTransform.Rotation = Rot;
+					if (GetNetMode() == NM_Client) // why do we differentiate here between netmode and authority?
+					{
+						SendControllerTransform_ServerRpc(ReplicatedTransform);
+					}
+				}
+			}
+		}
+	}
+}
+
+void UClientTransformReplication::TickComponent(float DeltaTime, ELevelTick TickType,
+	FActorComponentTickFunction* ThisTickFunction)
+{
+	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
+	UpdateState(DeltaTime);
+}
+
+void UClientTransformReplication::GetLifetimeReplicatedProps(TArray< class FLifetimeProperty >& OutLifetimeProps) const
+{
+	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
+
+	// Skipping the owner with this as the owner will use the controllers location directly
+	DOREPLIFETIME_CONDITION(UClientTransformReplication, ReplicatedTransform, COND_SkipOwner);
+	DOREPLIFETIME(UClientTransformReplication, ControllerNetUpdateRate);
+}
+
+void UClientTransformReplication::SendControllerTransform_ServerRpc_Implementation(FVRTransformRep NewTransform)
+{
+	// Store new transform and trigger OnRep_Function
+	ReplicatedTransform = NewTransform;
+
+	if (!GetOwner()->HasLocalNetOwner())
+		OnRep_ReplicatedTransform();
+}
+
+bool UClientTransformReplication::SendControllerTransform_ServerRpc_Validate(FVRTransformRep NewTransform)
+{
+	return true;
+	// Optionally check to make sure that player is inside of their bounds and deny it if they aren't?
+}
\ No newline at end of file
diff --git a/Source/RWTHVRToolkit/Private/Core/RWTHVRGameModeBase.cpp b/Source/RWTHVRToolkit/Private/Core/RWTHVRGameModeBase.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bb25b4095a8f707d54de38852acb5dcad5ed6d9a
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Core/RWTHVRGameModeBase.cpp
@@ -0,0 +1,41 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "Core/RWTHVRGameModeBase.h"
+
+#include "Core/RWTHVRPlayerState.h"
+#include "Kismet/GameplayStatics.h"
+#include "Logging/StructuredLog.h"
+
+
+FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId,
+                                           const FString& Options, const FString& Portal)
+{
+	// Used by the DisplayClusterNetDriver/Connection to handshake nodes. Could use their types directly
+	// but I don't really want to introduce a hard dependency here.
+	const FString NodeNameKey = "node";
+	const FString PrimaryNodeIdKey = "PrimaryNodeId";
+
+	// Check if we're using our custom PlayerState so that we can save the player type there.
+	// If not, just ingore all related args.
+	ARWTHVRPlayerState* State = Cast<ARWTHVRPlayerState>(NewPlayerController->PlayerState);
+		
+	if (State != nullptr)
+	{		
+		if (UGameplayStatics::HasOption(Options, PrimaryNodeIdKey))
+		{
+			const FString PrimaryNodeId = UGameplayStatics::ParseOption(Options, PrimaryNodeIdKey);
+
+			// When the primary node is a listen server, it apparently doesn't get the node option...
+			// Could additionally check for listen, but this should be save enough.
+			const FString NodeName = UGameplayStatics::HasOption(Options, NodeNameKey)
+				                         ? UGameplayStatics::ParseOption(Options, NodeNameKey)
+				                         : PrimaryNodeId;
+			
+			const EPlayerType Type = NodeName == PrimaryNodeId ? EPlayerType::nDisplayPrimary : EPlayerType::nDisplaySecondary;			
+			State->RequestSetPlayerType(Type);		
+		}
+	}
+	
+	return Super::InitNewPlayer(NewPlayerController, UniqueId, Options, Portal);	
+}
diff --git a/Source/RWTHVRToolkit/Private/Core/RWTHVRPlayerState.cpp b/Source/RWTHVRToolkit/Private/Core/RWTHVRPlayerState.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..be852ba79babb86064ec6bc5d7f0665136f15740
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Core/RWTHVRPlayerState.cpp
@@ -0,0 +1,71 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "Core/RWTHVRPlayerState.h"
+
+#include "Net/UnrealNetwork.h"
+#include "Net/Core/PushModel/PushModel.h"
+
+// Boilerplate, copies properties to new state
+void ARWTHVRPlayerState::CopyProperties(class APlayerState* PlayerState)
+{
+	Super::CopyProperties(PlayerState);
+
+	if (IsValid(PlayerState))
+	{
+		ARWTHVRPlayerState* RWTHVRPlayerState = Cast<ARWTHVRPlayerState>(PlayerState);
+		if (IsValid(RWTHVRPlayerState))
+		{
+			RWTHVRPlayerState->SetPlayerType(GetPlayerType());
+		}
+	}
+}
+
+// Boilerplate
+void ARWTHVRPlayerState::OverrideWith(class APlayerState* PlayerState)
+{
+	Super::OverrideWith(PlayerState);
+
+	if (IsValid(PlayerState))
+	{
+		const ARWTHVRPlayerState* RWTHVRPlayerState = Cast<ARWTHVRPlayerState>(PlayerState);
+		if (IsValid(RWTHVRPlayerState))
+		{
+			SetPlayerType(RWTHVRPlayerState->GetPlayerType());
+		}
+	}
+}
+
+// Replicate our property similar to the other state properties
+void ARWTHVRPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
+{
+	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
+
+	FDoRepLifetimeParams SharedParams;
+	SharedParams.bIsPushBased = true;
+	
+	DOREPLIFETIME_WITH_PARAMS_FAST(ARWTHVRPlayerState, PlayerType, SharedParams);
+}
+
+void ARWTHVRPlayerState::SetPlayerTypeServerRpc_Implementation(const EPlayerType NewPlayerType)
+{
+	SetPlayerType(NewPlayerType);
+}
+
+void ARWTHVRPlayerState::SetPlayerType(const EPlayerType NewPlayerType)
+{
+	MARK_PROPERTY_DIRTY_FROM_NAME(ARWTHVRPlayerState, PlayerType, this);
+	PlayerType = NewPlayerType;
+}
+
+void ARWTHVRPlayerState::RequestSetPlayerType(const EPlayerType NewPlayerType)
+{
+	if (HasAuthority())
+	{
+		SetPlayerType(NewPlayerType);
+	}
+	else
+	{
+		SetPlayerTypeServerRpc(NewPlayerType);
+	}
+}
diff --git a/Source/RWTHVRToolkit/Private/Core/VRTransformRep.h b/Source/RWTHVRToolkit/Private/Core/VRTransformRep.h
new file mode 100644
index 0000000000000000000000000000000000000000..c9bc577bc904b70b878ef9c14f47f3fd759b6b03
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Core/VRTransformRep.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "CoreMinimal.h"
+#include "VRTransformRep.generated.h"
+
+// Simple custom transform struct for more efficient repliation, from VRE Plugin
+USTRUCT()
+struct RWTHVRTOOLKIT_API FVRTransformRep
+{
+	GENERATED_USTRUCT_BODY()
+
+public:
+	UPROPERTY(Transient)
+	FVector Position;
+
+	UPROPERTY(Transient)
+	FRotator Rotation;
+
+	FVRTransformRep()
+	{
+		Position = FVector::ZeroVector;
+		Rotation = FRotator::ZeroRotator;
+	}
+
+	/**
+	* @param Ar FArchive to read or write from.
+	* @param Map PackageMap used to resolve references to UObject*
+	* @param bOutSuccess return value to signify if the serialization was succesfull (if false, an error will be logged by the calling function)
+	*
+	* @return return true if the serialization was fully mapped. If false, the property will be considered 'dirty' and will replicate again on the next update.
+	* This is needed for UActor* properties. If an actor's Actorchannel is not fully mapped, properties referencing it must stay dirty.
+	* Note that UPackageMap::SerializeObject returns false if an object is unmapped. Generally, you will want to return false from your ::NetSerialize
+	* if you make any calls to ::SerializeObject that return false.
+	*
+	*/
+	bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
+	{
+		bOutSuccess = true;
+		bOutSuccess &= SerializePackedVector<1, 24>(Position, Ar);
+		Rotation.SerializeCompressed(Ar);
+		return bOutSuccess;
+	}
+};
\ No newline at end of file
diff --git a/Source/RWTHVRToolkit/Private/Interaction/GrabComponent.cpp b/Source/RWTHVRToolkit/Private/Interaction/GrabComponent.cpp
index 57a0b05f0996be79561bbbbed5ac0e665b5cbdd1..db49c8cb60a7279ab55bf66f55241c91b9b83b06 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/GrabComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/GrabComponent.cpp
@@ -93,11 +93,19 @@ void UGrabComponent::BeginPlay()
 
 void UGrabComponent::SetupInputActions()
 {
-	auto InputSubsystem = UVirtualRealityUtilities::GetVRPawnLocalPlayerSubsystem(GetWorld());
+	const APawn* Pawn = Cast<APawn>(GetOwner());
+	const APlayerController* PlayerController = Cast<APlayerController>(Pawn->GetController());
+	const ULocalPlayer* LP = PlayerController ? PlayerController->GetLocalPlayer() : nullptr;
+	if (LP == nullptr)
+		return;
+	
+	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = LP->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
 	// add Input Mapping context 
 	InputSubsystem->AddMappingContext(IMCGrab,0);
 	
-	UEnhancedInputComponent* EI = UVirtualRealityUtilities::GetVRPawnInputComponent(GetWorld());
+	UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(Pawn->InputComponent);
+	if (EI == nullptr)
+		return;
 	
 	EI->BindAction(GrabInputAction, ETriggerEvent::Started, this, &UGrabComponent::OnBeginGrab);
 	EI->BindAction(GrabInputAction, ETriggerEvent::Completed, this, &UGrabComponent::OnEndGrab);	
diff --git a/Source/RWTHVRToolkit/Private/Interaction/RaycastSelectionComponent.cpp b/Source/RWTHVRToolkit/Private/Interaction/RaycastSelectionComponent.cpp
index 1ac8cf0e187f57a2c8418c732ba883078d1aed27..0ac53f6aa1d28852d1eff129512f87684cd9fae1 100644
--- a/Source/RWTHVRToolkit/Private/Interaction/RaycastSelectionComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Interaction/RaycastSelectionComponent.cpp
@@ -79,11 +79,19 @@ void URaycastSelectionComponent::TickComponent(float DeltaTime, ELevelTick TickT
 
 void URaycastSelectionComponent::SetupInputActions()
 {
-	auto InputSubsystem = UVirtualRealityUtilities::GetVRPawnLocalPlayerSubsystem(GetWorld());
+	const APawn* Pawn = Cast<APawn>(GetOwner());
+	const APlayerController* PlayerController = Cast<APlayerController>(Pawn->GetController());
+	const ULocalPlayer* LP = PlayerController ? PlayerController->GetLocalPlayer() : nullptr;
+	if (LP == nullptr)
+		return;
+	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = LP->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
+
 	// add Input Mapping context 
 	InputSubsystem->AddMappingContext(IMCRaycastSelection,0);
 	
-	UEnhancedInputComponent* EI = UVirtualRealityUtilities::GetVRPawnInputComponent(GetWorld());
+	UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(Pawn->InputComponent);
+	if (EI == nullptr)
+		return;
 	
 	EI->BindAction(RayCastSelectInputAction, ETriggerEvent::Started, this, &URaycastSelectionComponent::OnBeginSelect);
 	EI->BindAction(RayCastSelectInputAction, ETriggerEvent::Completed, this, &URaycastSelectionComponent::OnEndSelect);	
diff --git a/Source/RWTHVRToolkit/Private/Pawn/BasicVRInteractionComponent.cpp b/Source/RWTHVRToolkit/Private/Pawn/BasicVRInteractionComponent.cpp
index 0a2c13ba1e50dfc2bfc670af9e436e7ea5a043d9..f3e931cc9820e0b4bf99335367706cce43fbee01 100644
--- a/Source/RWTHVRToolkit/Private/Pawn/BasicVRInteractionComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Pawn/BasicVRInteractionComponent.cpp
@@ -3,15 +3,12 @@
 
 #include "Pawn/BasicVRInteractionComponent.h"
 
-
 #include "Interaction/Clickable.h"
 #include "Interaction/Grabable.h"
 #include "Interaction/Targetable.h"
 #include "Interaction/GrabbingBehaviorComponent.h"
 #include "Misc/Optional.h"
 #include "DrawDebugHelpers.h"
-#include "Components/WidgetComponent.h"
-#include "UObject/ConstructorHelpers.h"
 
 DEFINE_LOG_CATEGORY(LogVRInteractionComponent);
 
@@ -24,33 +21,32 @@ UBasicVRInteractionComponent::UBasicVRInteractionComponent()
 
 	// Setup the interaction ray.
 	InteractionRay = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Interaction Ray"));
-	//this ray model has an inlayed cross with flipped normals so it can be seen as a cross in desktop mode where the right hand is attached to the head
-	ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/RWTHVRToolkit/PointingRay/Ray_Mesh"));
-	if (MeshAsset.Object != nullptr)
-	{
-		InteractionRay->SetStaticMesh(MeshAsset.Object);
-	}
+	InteractionRay->SetCastShadow(false);
 	// turns off collisions as the InteractionRay is only meant to visualize the ray
 	InteractionRay->SetCollisionProfileName(TEXT("NoCollision"));
+	// The static mesh reference is set in the blueprint, avoid ConstructorHelpers and hardcoded paths!
+	// this ray model has an inlayed cross with flipped normals so it can be seen as a cross in desktop mode where the right hand is attached to the head
+
 	bShowDebug = false; //otherwise the WidgetInteractionComponent debug vis is shown
-	InteractionSource = EWidgetInteractionSource::Custom; //can also be kept at default (World), this way, however, we efficiently reuse the line traces
-	
+	InteractionSource = EWidgetInteractionSource::Custom;
+	//can also be kept at default (World), this way, however, we efficiently reuse the line traces
 }
 
 void UBasicVRInteractionComponent::BeginPlay()
 {
 	Super::BeginPlay();
-	
+
 	//WidgetInteractionComponent
 	InteractionDistance = MaxClickDistance;
-	InteractionRay->SetRelativeScale3D(FVector(MaxClickDistance / 100.0f, 0.5f, 0.5f)); //the ray model has a length of 100cm (and is a bit too big in Y/Z dir)
+	InteractionRay->SetRelativeScale3D(FVector(MaxClickDistance / 100.0f, 0.5f, 0.5f));
+	//the ray model has a length of 100cm (and is a bit too big in Y/Z dir)
 	SetInteractionRayVisibility(InteractionRayVisibility);
 }
 
 void UBasicVRInteractionComponent::BeginInteraction()
 {
-	if(!InteractionRayEmitter) return;
-	
+	if (!InteractionRayEmitter) return;
+
 	// start and end point for raytracing
 	const FTwoVectors StartEnd = GetHandRay(MaxClickDistance);
 	TOptional<FHitResult> Hit = RaytraceForFirstHit(StartEnd);
@@ -63,16 +59,16 @@ void UBasicVRInteractionComponent::BeginInteraction()
 	SetCustomHitResult(Hit.GetValue());
 	//if !bCanRaytraceEveryTick, you have to click twice, since the first tick it only highlights and can't directly click
 	PressPointerKey(EKeys::LeftMouseButton);
-	
-	
+
+
 	if (HitActor && HitActor->Implements<UGrabable>() && Hit->Distance < MaxGrabDistance)
 	{
 		// call grabable actors function so he reacts to our grab
 		IGrabable::Execute_OnBeginGrab(HitActor);
-		
+
 		// save it for later, is needed every tick
 		Behavior = HitActor->FindComponentByClass<UGrabbingBehaviorComponent>();
-		if ( Behavior == nullptr)
+		if (Behavior == nullptr)
 			HandlePhysicsAndAttachActor(HitActor);
 
 		// we save the grabbedActor in a general form to access all of AActors functions easily later
@@ -86,11 +82,11 @@ void UBasicVRInteractionComponent::BeginInteraction()
 
 void UBasicVRInteractionComponent::EndInteraction()
 {
-	if(!InteractionRayEmitter) return;
+	if (!InteractionRayEmitter) return;
 
 	//end interaction of WidgetInteractionComponent
 	ReleasePointerKey(EKeys::LeftMouseButton);
-	
+
 	// if we didnt grab anyone there is no need to release
 	if (GrabbedActor == nullptr)
 		return;
@@ -101,11 +97,13 @@ void UBasicVRInteractionComponent::EndInteraction()
 	// Detach the Actor
 	if (GrabbedActor->FindComponentByClass<UGrabbingBehaviorComponent>() == nullptr)
 	{
-		if (ComponentSimulatingPhysics) {
+		if (ComponentSimulatingPhysics)
+		{
 			ComponentSimulatingPhysics->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
 			ComponentSimulatingPhysics->SetSimulatePhysics(true);
 		}
-		else {
+		else
+		{
 			GrabbedActor->GetRootComponent()->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
 		}
 	}
@@ -117,15 +115,16 @@ void UBasicVRInteractionComponent::EndInteraction()
 }
 
 // Called every frame
-void UBasicVRInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
+void UBasicVRInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType,
+                                                 FActorComponentTickFunction* ThisTickFunction)
 {
 	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
 
 	if (!InteractionRayEmitter) return;
-	
+
 	// if our Grabable Actor is not constrained we need to calculate the position dynamically
 	if (Behavior != nullptr)
-	{	
+	{
 		// specifies the hand in space
 		const FVector HandPos = InteractionRayEmitter->GetComponentLocation();
 		const FQuat HandQuat = InteractionRayEmitter->GetComponentQuat();
@@ -142,7 +141,7 @@ void UBasicVRInteractionComponent::TickComponent(float DeltaTime, ELevelTick Tic
 
 	if (!Hit.IsSet() || !Hit->GetActor())
 	{
-		if(InteractionRayVisibility==EInteractionRayVisibility::VisibleOnHoverOnly)
+		if (InteractionRayVisibility == EInteractionRayVisibility::VisibleOnHoverOnly)
 		{
 			InteractionRay->SetVisibility(false);
 		}
@@ -150,13 +149,13 @@ void UBasicVRInteractionComponent::TickComponent(float DeltaTime, ELevelTick Tic
 		// Execute leave event on the actor that lost the focus if there was one
 		if (LastActorHit && LastActorHit->Implements<UTargetable>())
 		{
-				ITargetable::Execute_OnTargetedLeave(LastActorHit);
+			ITargetable::Execute_OnTargetedLeave(LastActorHit);
 		}
 
 		LastActorHit = nullptr;
 		return;
 	}
-	
+
 	AActor* HitActor = Hit->GetActor();
 
 	// Execute Leave and enter events when the focused actor changed
@@ -183,9 +182,9 @@ void UBasicVRInteractionComponent::TickComponent(float DeltaTime, ELevelTick Tic
 
 	// widget interaction
 	SetCustomHitResult(Hit.GetValue());
-	if(InteractionRayVisibility==EInteractionRayVisibility::VisibleOnHoverOnly)
+	if (InteractionRayVisibility == EInteractionRayVisibility::VisibleOnHoverOnly)
 	{
-		if(HitActor->Implements<UTargetable>() || HitActor->Implements<UClickable>() || IsOverInteractableWidget())
+		if (HitActor->Implements<UTargetable>() || HitActor->Implements<UClickable>() || IsOverInteractableWidget())
 		{
 			InteractionRay->SetVisibility(true);
 		}
@@ -199,7 +198,7 @@ void UBasicVRInteractionComponent::TickComponent(float DeltaTime, ELevelTick Tic
 
 void UBasicVRInteractionComponent::Initialize(USceneComponent* RayEmitter)
 {
-	if(InteractionRayEmitter) return; /* Return if already initialized */
+	if (InteractionRayEmitter) return; /* Return if already initialized */
 
 	InteractionRayEmitter = RayEmitter;
 
@@ -210,7 +209,7 @@ void UBasicVRInteractionComponent::Initialize(USceneComponent* RayEmitter)
 void UBasicVRInteractionComponent::SetInteractionRayVisibility(EInteractionRayVisibility NewVisibility)
 {
 	InteractionRayVisibility = NewVisibility;
-	if(InteractionRay)
+	if (InteractionRay)
 	{
 		switch (InteractionRayVisibility)
 		{
@@ -221,31 +220,38 @@ void UBasicVRInteractionComponent::SetInteractionRayVisibility(EInteractionRayVi
 		case Invisible:
 			InteractionRay->SetVisibility(false);
 			break;
+		default: ;
 		}
 	}
 
-	if(InteractionRayVisibility==EInteractionRayVisibility::VisibleOnHoverOnly && !bCanRaytraceEveryTick)
+	if (InteractionRayVisibility == EInteractionRayVisibility::VisibleOnHoverOnly && !bCanRaytraceEveryTick)
 	{
-		UE_LOG(LogVRInteractionComponent, Warning, TEXT("VisibleOnHoverOnly needs bCanRaytraceEveryTick=true, so this is set!"));
-		bCanRaytraceEveryTick=true;
+		UE_LOG(LogVRInteractionComponent, Warning,
+		       TEXT("VisibleOnHoverOnly needs bCanRaytraceEveryTick=true, so this is set!"));
+		bCanRaytraceEveryTick = true;
 	}
-	if(InteractionRayVisibility==EInteractionRayVisibility::Visible && !bCanRaytraceEveryTick)
+	if (InteractionRayVisibility == EInteractionRayVisibility::Visible && !bCanRaytraceEveryTick)
 	{
-		UE_LOG(LogVRInteractionComponent, Warning, TEXT("VisibleOnHoverOnly will need two clicks to interact with widgets if bCanRaytraceEveryTick is not set!"));
+		UE_LOG(LogVRInteractionComponent, Warning,
+		       TEXT(
+			       "VisibleOnHoverOnly will need two clicks to interact with widgets if bCanRaytraceEveryTick is not set!"
+		       ));
 	}
 }
 
-void UBasicVRInteractionComponent::HandlePhysicsAndAttachActor(AActor* HitActor)
+void UBasicVRInteractionComponent::HandlePhysicsAndAttachActor(const AActor* HitActor)
 {
 	UPrimitiveComponent* PhysicsSimulatingComp = GetFirstComponentSimulatingPhysics(HitActor);
 	const FAttachmentTransformRules Rules = FAttachmentTransformRules(EAttachmentRule::KeepWorld, false);
-	
-	if (PhysicsSimulatingComp) {
+
+	if (PhysicsSimulatingComp)
+	{
 		PhysicsSimulatingComp->SetSimulatePhysics(false);
-		PhysicsSimulatingComp->AttachToComponent(InteractionRayEmitter, Rules);	
+		PhysicsSimulatingComp->AttachToComponent(InteractionRayEmitter, Rules);
 		ComponentSimulatingPhysics = PhysicsSimulatingComp;
 	}
-	else {
+	else
+	{
 		HitActor->GetRootComponent()->AttachToComponent(InteractionRayEmitter, Rules);
 	}
 }
@@ -262,40 +268,44 @@ FTwoVectors UBasicVRInteractionComponent::GetHandRay(const float Length) const
 TOptional<FHitResult> UBasicVRInteractionComponent::RaytraceForFirstHit(const FTwoVectors& Ray) const
 {
 	const FVector Start = Ray.v1;
-	const FVector End   = Ray.v2;	
-	
+	const FVector End = Ray.v2;
+
 	// will be filled by the Line Trace Function
 	FHitResult Hit;
 
-	FCollisionQueryParams Params; 
+	FCollisionQueryParams Params;
 	Params.AddIgnoredActor(GetOwner()->GetUniqueID()); // prevents actor hitting itself
-	if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECollisionChannel::ECC_Visibility,Params))
+	if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECollisionChannel::ECC_Visibility, Params))
 		return {Hit};
 	else
 		return {};
 }
 
-UPrimitiveComponent* GetFirstComponentSimulatingPhysics(const AActor* TargetActor)
+UPrimitiveComponent* UBasicVRInteractionComponent::GetFirstComponentSimulatingPhysics(const AActor* TargetActor)
 {
 	TArray<UPrimitiveComponent*> PrimitiveComponents;
-	TargetActor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);	
+	TargetActor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);
 
 	// find any component that simulates physics, then traverse the hierarchy
-	for (const auto& Component : PrimitiveComponents) {
-		if (Component->IsSimulatingPhysics()) {
+	for (const auto& Component : PrimitiveComponents)
+	{
+		if (Component->IsSimulatingPhysics())
+		{
 			return GetHighestParentSimulatingPhysics(Component);
-		}	
+		}
 	}
 	return nullptr;
 }
 
 // recursively goes up the hierarchy and returns the highest parent simulating physics
-UPrimitiveComponent* GetHighestParentSimulatingPhysics(UPrimitiveComponent* Comp)
-{	
-	if (Cast<UPrimitiveComponent>(Comp->GetAttachParent()) && Comp->GetAttachParent()->IsSimulatingPhysics()) {
+UPrimitiveComponent* UBasicVRInteractionComponent::GetHighestParentSimulatingPhysics(UPrimitiveComponent* Comp)
+{
+	if (Cast<UPrimitiveComponent>(Comp->GetAttachParent()) && Comp->GetAttachParent()->IsSimulatingPhysics())
+	{
 		return GetHighestParentSimulatingPhysics(Cast<UPrimitiveComponent>(Comp->GetAttachParent()));
 	}
-	else {
+	else
+	{
 		return Comp;
 	}
 }
diff --git a/Source/RWTHVRToolkit/Private/Pawn/ContinuousMovementComponent.cpp b/Source/RWTHVRToolkit/Private/Pawn/ContinuousMovementComponent.cpp
index 5260c0d5dc344a85734346a9fef7ac7fdda0db63..ff3d0e11a9de8653c1ca26d09cf093a025b310ec 100644
--- a/Source/RWTHVRToolkit/Private/Pawn/ContinuousMovementComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Pawn/ContinuousMovementComponent.cpp
@@ -10,49 +10,39 @@
 #include "Utility/VirtualRealityUtilities.h"
 #include "MotionControllerComponent.h"
 
-void UContinuousMovementComponent::BeginPlay()
+void UContinuousMovementComponent::SetupPlayerInput(UInputComponent* PlayerInputComponent)
 {
-	Super::BeginPlay();
-
-	SetupInputActions();
-}
+	Super::SetupPlayerInput(PlayerInputComponent);
 	
+	if (!VRPawn || !VRPawn->HasLocalNetOwner() || !InputSubsystem)
+	{
+		return;
+	}
 
-void UContinuousMovementComponent::SetupInputActions()
-{
-	Super::SetupInputActions();
-
-	const AVirtualRealityPawn* VRPawn = Cast<AVirtualRealityPawn>(GetOwner());
 	// simple way of changing the handedness
-	if(bMoveWithRightHand)
+	if (bMoveWithRightHand)
 	{
 		MovementHand = VRPawn->RightHand;
 		RotationHand = VRPawn->LeftHand;
 		IMCMovement = IMCMovementRight;
-	} else
+	}
+	else
 	{
 		MovementHand = VRPawn->LeftHand;
 		RotationHand = VRPawn->RightHand;
 		IMCMovement = IMCMovementLeft;
 	}
 	
-	const APlayerController* PlayerController = Cast<APlayerController>(VRPawn->GetController());
-	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer());
-	if(!InputSubsystem)
-	{
-		UE_LOG(Toolkit,Error,TEXT("InputSubsystem IS NOT VALID"));
-		return;
-	}
 	// add Input Mapping context 
-	InputSubsystem->AddMappingContext(IMCMovement,0);
-	
-	UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(VRPawn->InputComponent);
-	if(!EI)
+	InputSubsystem->AddMappingContext(IMCMovement, 0);
+
+	UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(PlayerInputComponent);
+	if (!EI)
 	{
-		UE_LOG(Toolkit,Error,TEXT("Cannot cast Input Component to Enhanced Inpu Component in VRPawnMovement"));
+		UE_LOG(Toolkit, Error, TEXT("Cannot cast Input Component to Enhanced Inpu Component in VRPawnMovement"));
 		return;
 	}
-	
+
 	// continuous steering
 	EI->BindAction(Move, ETriggerEvent::Triggered, this, &UContinuousMovementComponent::OnMove);
 	EI->BindAction(MoveUp, ETriggerEvent::Triggered, this, &UContinuousMovementComponent::OnMoveUp);
@@ -62,34 +52,40 @@ void UContinuousMovementComponent::SetupInputActions()
 
 void UContinuousMovementComponent::OnMove(const FInputActionValue& Value)
 {
-	AVirtualRealityPawn* VRPawn = Cast<AVirtualRealityPawn>(GetOwner());
-	const bool bGazeDirected = UVirtualRealityUtilities::IsDesktopMode() || SteeringMode == EVRSteeringModes::STEER_GAZE_DIRECTED;
-	
-	const FVector ForwardDir = bGazeDirected ? VRPawn->HeadCameraComponent->GetForwardVector() : MovementHand->GetForwardVector();
-	const FVector RightDir = bGazeDirected ? VRPawn->HeadCameraComponent->GetRightVector() : MovementHand->GetRightVector();
-	
-	if (VRPawn->Controller != nullptr)
+	if (!VRPawn || !VRPawn->Controller)
+		return;
+
+	const bool bGazeDirected = UVirtualRealityUtilities::IsDesktopMode() || SteeringMode ==
+		EVRSteeringModes::STEER_GAZE_DIRECTED;
+
+	const FVector ForwardDir = bGazeDirected
+		                           ? VRPawn->HeadCameraComponent->GetForwardVector()
+		                           : MovementHand->GetForwardVector();
+	const FVector RightDir = bGazeDirected
+		                         ? VRPawn->HeadCameraComponent->GetRightVector()
+		                         : MovementHand->GetRightVector();
+
+	const FVector2D MoveValue = Value.Get<FVector2D>();
+
+	// Forward/Backward direction
+	if (MoveValue.X != 0.f)
+	{
+		VRPawn->AddMovementInput(ForwardDir, MoveValue.X);
+	}
+
+	// Right/Left direction
+	if (MoveValue.Y != 0.f)
 	{
-		const FVector2D MoveValue = Value.Get<FVector2D>();
- 
-		// Forward/Backward direction
-		if (MoveValue.X != 0.f)
-		{
-			VRPawn->AddMovementInput(ForwardDir, MoveValue.X);
-		}
- 
-		// Right/Left direction
-		if (MoveValue.Y != 0.f)
-		{
-			VRPawn->AddMovementInput(RightDir, MoveValue.Y);
-		}
+		VRPawn->AddMovementInput(RightDir, MoveValue.Y);
 	}
 }
 
 void UContinuousMovementComponent::OnMoveUp(const FInputActionValue& Value)
 {
-	AVirtualRealityPawn* VRPawn = Cast<AVirtualRealityPawn>(GetOwner());
-	const float MoveValue =  Value.Get<FVector2D>().X;
+	if (!VRPawn)
+		return;
+
+	const float MoveValue = Value.Get<FVector2D>().X;
 	//the right hand is rotated on desktop to follow the cursor so it's forward is also changing with cursor position
 	VRPawn->AddMovementInput(FVector::UpVector, MoveValue);
 }
diff --git a/Source/RWTHVRToolkit/Private/Pawn/InputExtensionInterface.cpp b/Source/RWTHVRToolkit/Private/Pawn/InputExtensionInterface.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8c429dc9d4fc49f41ecac59d03754eb52235e417
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Pawn/InputExtensionInterface.cpp
@@ -0,0 +1,12 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "Pawn/InputExtensionInterface.h"
+
+UEnhancedInputLocalPlayerSubsystem* IInputExtensionInterface::GetEnhancedInputLocalPlayerSubsystem(APawn* Pawn) const
+{
+	const APlayerController* PlayerController = Pawn ? Cast<APlayerController>(Pawn->GetController()) : nullptr;
+	const ULocalPlayer* LP = PlayerController ? PlayerController->GetLocalPlayer() : nullptr;
+	
+	return LP->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();	
+}
diff --git a/Source/RWTHVRToolkit/Private/Pawn/MovementComponentBase.cpp b/Source/RWTHVRToolkit/Private/Pawn/MovementComponentBase.cpp
index e0fe2d6d4bf545f34b79e43cfa10ea5fea5cc9ad..b4f680c8bec3453d84537af95927ecd8381b48b4 100644
--- a/Source/RWTHVRToolkit/Private/Pawn/MovementComponentBase.cpp
+++ b/Source/RWTHVRToolkit/Private/Pawn/MovementComponentBase.cpp
@@ -11,49 +11,48 @@
 #include "Pawn/VRPawnInputConfig.h"
 #include "Utility/VirtualRealityUtilities.h"
 
-void UMovementComponentBase::BeginPlay()
+void UMovementComponentBase::SetupPlayerInput(UInputComponent* PlayerInputComponent)
 {
-	Super::BeginPlay();
+	IInputExtensionInterface::SetupPlayerInput(PlayerInputComponent);
 
-	SetupInputActions();
-}
+	VRPawn = Cast<AVirtualRealityPawn>(GetOwner());
 
-void UMovementComponentBase::SetupInputActions()
-{
+	if (!VRPawn || !VRPawn->HasLocalNetOwner())
+	{
+		return;
+	}
 
-	const AVirtualRealityPawn* VRPawn = Cast<AVirtualRealityPawn>(GetOwner());
-	
-	const APlayerController* PlayerController = Cast<APlayerController>(VRPawn->GetController());
-	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer());
-	if(!InputSubsystem)
+	InputSubsystem = GetEnhancedInputLocalPlayerSubsystem(VRPawn);
+	if (!InputSubsystem)
 	{
-		UE_LOG(Toolkit,Error,TEXT("InputSubsystem IS NOT VALID"));
+		UE_LOG(Toolkit, Error, TEXT("InputSubsystem IS NOT VALID"));
 		return;
 	}
-	// add Input Mapping context 
-	InputSubsystem->AddMappingContext(IMCRotation,0);
 	
-	UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(VRPawn->InputComponent);
-	if(!EI)
+	// add Input Mapping context 
+	InputSubsystem->AddMappingContext(IMCRotation, 0);
+
+	UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(PlayerInputComponent);
+	if (!EI)
 	{
-		UE_LOG(Toolkit,Error,TEXT("Cannot cast Input Component to Enhanced Inpu Component in VRPawnMovement"));
+		UE_LOG(Toolkit, Error, TEXT("Cannot cast Input Component to Enhanced Inpu Component in VRPawnMovement"));
 		return;
 	}
-	
 
 	// turning
-	if(bAllowTurning)
+	if (bAllowTurning)
 	{
 		// no snap turning for desktop mode
-		if(bSnapTurn && !UVirtualRealityUtilities::IsDesktopMode())
+		if (bSnapTurn && !UVirtualRealityUtilities::IsDesktopMode())
 		{
 			EI->BindAction(Turn, ETriggerEvent::Started, this, &UMovementComponentBase::OnBeginSnapTurn);
-		} else
+		}
+		else
 		{
 			EI->BindAction(Turn, ETriggerEvent::Triggered, this, &UMovementComponentBase::OnBeginTurn);
 		}
 	}
-	
+
 	// bind additional functions for desktop rotations
 	if (UVirtualRealityUtilities::IsDesktopMode())
 	{
@@ -72,40 +71,41 @@ void UMovementComponentBase::EndDesktopRotation()
 	bApplyDesktopRotation = false;
 }
 
-
 void UMovementComponentBase::OnBeginTurn(const FInputActionValue& Value)
 {
-	AVirtualRealityPawn* VRPawn = Cast<AVirtualRealityPawn>(GetOwner());
+	if (UVirtualRealityUtilities::IsDesktopMode() && !bApplyDesktopRotation)
+		return;
 
-	if(UVirtualRealityUtilities::IsDesktopMode() && !bApplyDesktopRotation) return;
+	if (!VRPawn || !VRPawn->Controller)
+		return;
+	
+	const FVector2D TurnValue = Value.Get<FVector2D>();
 
-	if (VRPawn->Controller != nullptr)
+	if (TurnValue.X != 0.f)
 	{
-		const FVector2D TurnValue = Value.Get<FVector2D>();
- 
-		if (TurnValue.X != 0.f)
-		{
-			VRPawn->AddControllerYawInput(TurnRateFactor * TurnValue.X);
-		}
- 
-		if (TurnValue.Y != 0.f)
+		VRPawn->AddControllerYawInput(TurnRateFactor * TurnValue.X);
+	}
+
+	if (TurnValue.Y != 0.f)
+	{
+		if (UVirtualRealityUtilities::IsDesktopMode() && bApplyDesktopRotation)
 		{
-			if (UVirtualRealityUtilities::IsDesktopMode() && bApplyDesktopRotation)
-			{
-				VRPawn->AddControllerPitchInput(TurnRateFactor * -TurnValue.Y);
-			}
+			VRPawn->AddControllerPitchInput(TurnRateFactor * -TurnValue.Y);
 		}
 	}
 }
 
 void UMovementComponentBase::OnBeginSnapTurn(const FInputActionValue& Value)
 {
-	AVirtualRealityPawn* VRPawn = Cast<AVirtualRealityPawn>(GetOwner());
+	if (!VRPawn || !VRPawn->Controller)
+		return;
+	
 	const FVector2D TurnValue = Value.Get<FVector2D>();
 	if (TurnValue.X > 0.f)
 	{
 		VRPawn->AddControllerYawInput(SnapTurnAngle);
-	} else if (TurnValue.X < 0.f)
+	}
+	else if (TurnValue.X < 0.f)
 	{
 		VRPawn->AddControllerYawInput(-SnapTurnAngle);
 	}
diff --git a/Source/RWTHVRToolkit/Private/Pawn/MovementComponentBase.h b/Source/RWTHVRToolkit/Private/Pawn/MovementComponentBase.h
index 0cc2d2a297fc1f3ba2ba92aadbe5130a2054317d..0a9d869e90f6a8ba0ac61c109e0a1475b084023b 100644
--- a/Source/RWTHVRToolkit/Private/Pawn/MovementComponentBase.h
+++ b/Source/RWTHVRToolkit/Private/Pawn/MovementComponentBase.h
@@ -4,19 +4,20 @@
 
 #include "CoreMinimal.h"
 #include "Components/ActorComponent.h"
+#include "Pawn/InputExtensionInterface.h"
 #include "MovementComponentBase.generated.h"
 
 /**
  * 
  */
 UCLASS(Blueprintable)
-class RWTHVRTOOLKIT_API UMovementComponentBase : public UActorComponent
+class RWTHVRTOOLKIT_API UMovementComponentBase : public UActorComponent, public IInputExtensionInterface
 {
 	GENERATED_BODY()
 
 public:
-
-	virtual void BeginPlay() override;
+	
+	virtual void SetupPlayerInput(UInputComponent* PlayerInputComponent) override;
 
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VR Movement")
 	bool bAllowTurning = true;
@@ -53,14 +54,15 @@ public:
 	void EndDesktopRotation();
 
 protected:
-	void SetupInputActions();
 
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input")
 	class UInputMappingContext* IMCRotation;
 
-private:
+	UPROPERTY()
+	AVirtualRealityPawn* VRPawn;
 
+	UPROPERTY()
+	UEnhancedInputLocalPlayerSubsystem* InputSubsystem;
 
 	bool bApplyDesktopRotation = false;
-	
 };
diff --git a/Source/RWTHVRToolkit/Private/Pawn/ReplicatedCameraComponent.cpp b/Source/RWTHVRToolkit/Private/Pawn/ReplicatedCameraComponent.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1c8a8563d5eab7560adb5b9b95f7f70867ff29b3
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Pawn/ReplicatedCameraComponent.cpp
@@ -0,0 +1,82 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "Pawn/ReplicatedCameraComponent.h"
+
+#include "Net/UnrealNetwork.h"
+
+UReplicatedCameraComponent::UReplicatedCameraComponent()
+{
+	PrimaryComponentTick.bCanEverTick = true;
+	PrimaryComponentTick.SetTickFunctionEnable(true);
+	SetIsReplicatedByDefault(true);
+
+	// Direct transform replication
+	ControllerNetUpdateRate = 100.0f; // 100 htz is default
+	ControllerNetUpdateCount = 0.0f;
+}
+
+// Naive direct transform replication (replace with input rep?)
+
+void UReplicatedCameraComponent::UpdateState(float DeltaTime)
+{
+	if (GetOwner()->HasLocalNetOwner())
+	{
+		if (GetIsReplicated())
+		{
+			const FVector Loc = GetRelativeLocation();
+			const FRotator Rot = GetRelativeRotation();
+
+			if (!Loc.Equals(ReplicatedTransform.Position) || !Rot.Equals(ReplicatedTransform.Rotation))
+			{
+				ControllerNetUpdateCount += DeltaTime;
+				if (ControllerNetUpdateCount >= (1.0f / ControllerNetUpdateRate)) // todo save inverse?
+					{
+					ControllerNetUpdateCount = 0.0f;
+
+					ReplicatedTransform.Position = Loc;
+					ReplicatedTransform.Rotation = Rot;
+					if (GetNetMode() == NM_Client) // why do we differentiate here between netmode and authority?
+						{
+							ServerSendControllerTransformRpc(ReplicatedTransform);
+						}
+					}
+			}
+		}
+	}
+}
+
+void UReplicatedCameraComponent::TickComponent(float DeltaTime, ELevelTick TickType,
+	FActorComponentTickFunction* ThisTickFunction)
+{
+	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
+	UpdateState(DeltaTime);
+}
+
+void UReplicatedCameraComponent::GetLifetimeReplicatedProps(TArray< class FLifetimeProperty >& OutLifetimeProps) const
+{
+	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
+
+	DISABLE_REPLICATED_PRIVATE_PROPERTY(USceneComponent, RelativeLocation);
+	DISABLE_REPLICATED_PRIVATE_PROPERTY(USceneComponent, RelativeRotation);
+	DISABLE_REPLICATED_PRIVATE_PROPERTY(USceneComponent, RelativeScale3D);	
+
+	// Skipping the owner with this as the owner will use the controllers location directly
+	DOREPLIFETIME_CONDITION(UReplicatedCameraComponent, ReplicatedTransform, COND_SkipOwner);
+	DOREPLIFETIME(UReplicatedCameraComponent, ControllerNetUpdateRate);
+}
+
+void UReplicatedCameraComponent::ServerSendControllerTransformRpc_Implementation(FVRTransformRep NewTransform)
+{
+	// Store new transform and trigger OnRep_Function
+	ReplicatedTransform = NewTransform;
+
+	if (!GetOwner()->HasLocalNetOwner())
+		OnRep_ReplicatedTransform();
+}
+
+bool UReplicatedCameraComponent::ServerSendControllerTransformRpc_Validate(FVRTransformRep NewTransform)
+{
+	return true;
+	// Optionally check to make sure that player is inside of their bounds and deny it if they aren't?
+}
\ No newline at end of file
diff --git a/Source/RWTHVRToolkit/Private/Pawn/ReplicatedMotionControllerComponent.cpp b/Source/RWTHVRToolkit/Private/Pawn/ReplicatedMotionControllerComponent.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4c8c06813f90c420dc5d1dee078520a5e6a8d6a2
--- /dev/null
+++ b/Source/RWTHVRToolkit/Private/Pawn/ReplicatedMotionControllerComponent.cpp
@@ -0,0 +1,83 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "Pawn/ReplicatedMotionControllerComponent.h"
+
+
+#include "Net/UnrealNetwork.h"
+
+UReplicatedMotionControllerComponent::UReplicatedMotionControllerComponent()
+{
+	PrimaryComponentTick.bCanEverTick = true;
+	PrimaryComponentTick.SetTickFunctionEnable(true);
+	SetIsReplicatedByDefault(true);
+
+	// Direct transform replication
+	ControllerNetUpdateRate = 100.0f; // 100 htz is default
+	ControllerNetUpdateCount = 0.0f;
+}
+
+// Naive direct transform replication (replace with input rep?)
+
+void UReplicatedMotionControllerComponent::UpdateState(float DeltaTime)
+{
+	if (GetOwner()->HasLocalNetOwner())
+	{
+		if (GetIsReplicated())
+		{
+			const FVector Loc = GetRelativeLocation();
+			const FRotator Rot = GetRelativeRotation();
+
+			if (!Loc.Equals(ReplicatedTransform.Position) || !Rot.Equals(ReplicatedTransform.Rotation))
+			{
+				ControllerNetUpdateCount += DeltaTime;
+				if (ControllerNetUpdateCount >= (1.0f / ControllerNetUpdateRate)) // todo save inverse?
+					{
+					ControllerNetUpdateCount = 0.0f;
+
+					ReplicatedTransform.Position = Loc;
+					ReplicatedTransform.Rotation = Rot;
+					if (GetNetMode() == NM_Client) // why do we differentiate here between netmode and authority?
+						{
+						SendControllerTransform_ServerRpc(ReplicatedTransform);
+						}
+					}
+			}
+		}
+	}
+}
+
+void UReplicatedMotionControllerComponent::TickComponent(float DeltaTime, ELevelTick TickType,
+	FActorComponentTickFunction* ThisTickFunction)
+{
+	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
+	UpdateState(DeltaTime);
+}
+
+void UReplicatedMotionControllerComponent::GetLifetimeReplicatedProps(TArray< class FLifetimeProperty >& OutLifetimeProps) const
+{
+	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
+
+	DISABLE_REPLICATED_PRIVATE_PROPERTY(USceneComponent, RelativeLocation);
+	DISABLE_REPLICATED_PRIVATE_PROPERTY(USceneComponent, RelativeRotation);
+	DISABLE_REPLICATED_PRIVATE_PROPERTY(USceneComponent, RelativeScale3D);	
+
+	// Skipping the owner with this as the owner will use the controllers location directly
+	DOREPLIFETIME_CONDITION(UReplicatedMotionControllerComponent, ReplicatedTransform, COND_SkipOwner);
+	DOREPLIFETIME(UReplicatedMotionControllerComponent, ControllerNetUpdateRate);
+}
+
+void UReplicatedMotionControllerComponent::SendControllerTransform_ServerRpc_Implementation(FVRTransformRep NewTransform)
+{
+	// Store new transform and trigger OnRep_Function
+	ReplicatedTransform = NewTransform;
+
+	if (!GetOwner()->HasLocalNetOwner())
+		OnRep_ReplicatedTransform();
+}
+
+bool UReplicatedMotionControllerComponent::SendControllerTransform_ServerRpc_Validate(FVRTransformRep NewTransform)
+{
+	return true;
+	// Optionally check to make sure that player is inside of their bounds and deny it if they aren't?
+}
\ No newline at end of file
diff --git a/Source/RWTHVRToolkit/Private/Pawn/TeleportationComponent.cpp b/Source/RWTHVRToolkit/Private/Pawn/TeleportationComponent.cpp
index 3ec3aee1c04ebc943a2352dfb1c872f3f23f4c10..00864280dd13027fb3556e69eccdc4d1b9107993 100644
--- a/Source/RWTHVRToolkit/Private/Pawn/TeleportationComponent.cpp
+++ b/Source/RWTHVRToolkit/Private/Pawn/TeleportationComponent.cpp
@@ -16,18 +16,20 @@
 #include "MotionControllerComponent.h"
 
 
-// Called when the game starts
-void UTeleportationComponent::BeginPlay()
+void UTeleportationComponent::SetupPlayerInput(UInputComponent* PlayerInputComponent)
 {
-	Super::BeginPlay();
-
-	SetupInputActions();
+	Super::SetupPlayerInput(PlayerInputComponent);
+	
+	if (!VRPawn || !VRPawn->HasLocalNetOwner() || !InputSubsystem)
+	{
+		return;
+	}
 	
 	TeleportTraceComponent = UNiagaraFunctionLibrary::SpawnSystemAtLocation
 	(
 		GetWorld(),
 		TeleportTraceSystem,
-		GetOwner()->GetActorLocation(),
+		VRPawn->GetActorLocation(),
 		FRotator(0),
 		FVector(1),
 		true,
@@ -38,48 +40,34 @@ void UTeleportationComponent::BeginPlay()
 
 	FActorSpawnParameters SpawnParameters = FActorSpawnParameters();
 	SpawnParameters.Name = "TeleportVisualizer";
-	if(BPTeleportVisualizer)
+	
+	if (BPTeleportVisualizer)
 	{
-		TeleportVisualizer = GetWorld()->SpawnActor<AActor>(BPTeleportVisualizer,GetOwner()->GetActorLocation(),GetOwner()->GetActorRotation(),SpawnParameters);
+		TeleportVisualizer = GetWorld()->SpawnActor<AActor>(BPTeleportVisualizer, VRPawn->GetActorLocation(),
+															VRPawn->GetActorRotation(), SpawnParameters);
 	}
 	TeleportTraceComponent->SetVisibility(false);
 	TeleportVisualizer->SetActorHiddenInGame(true);
-
-}
-
-
-
-void UTeleportationComponent::SetupInputActions()
-{
-	Super::SetupInputActions();
-
-	const AVirtualRealityPawn* VRPawn = Cast<AVirtualRealityPawn>(GetOwner());
-
+	
 	// simple way of changing the handedness
-	if(bMoveWithRightHand)
+	if (bMoveWithRightHand)
 	{
 		TeleportationHand = VRPawn->RightHand;
 		RotationHand = VRPawn->LeftHand;
 		IMCMovement = IMCTeleportRight;
-	} else
+	}
+	else
 	{
 		TeleportationHand = VRPawn->LeftHand;
 		RotationHand = VRPawn->RightHand;
 		IMCMovement = IMCTeleportLeft;
 	}
 	
-	const APlayerController* PlayerController = Cast<APlayerController>(VRPawn->GetController());
-	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer());
-	if(!InputSubsystem)
-	{
-		UE_LOG(Toolkit,Error,TEXT("InputSubsystem IS NOT VALID"));
-		return;
-	}
 	// add Input Mapping context 
 	InputSubsystem->AddMappingContext(IMCMovement,0);
 	
-	UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(VRPawn->InputComponent);
-	if(!EI)
+	UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(PlayerInputComponent);
+	if (!EI)
 	{
 		UE_LOG(Toolkit,Error,TEXT("Cannot cast Input Component to Enhanced Inpu Component in VRPawnMovement"));
 		return;
@@ -107,8 +95,8 @@ void UTeleportationComponent::OnStartTeleportTrace(const FInputActionValue& Valu
 void UTeleportationComponent::UpdateTeleportTrace(const FInputActionValue& Value)
 {
 	// Update the teleport trace
-	FVector StartPosition = TeleportationHand->GetComponentLocation();
-	FVector ForwardVector = TeleportationHand->GetForwardVector();
+	const FVector StartPosition = TeleportationHand->GetComponentLocation();
+	const FVector ForwardVector = TeleportationHand->GetForwardVector();
 
 	TArray<AActor> ActorsToIgnore;
 	
@@ -121,33 +109,30 @@ void UTeleportationComponent::UpdateTeleportTrace(const FInputActionValue& Value
 		ECC_WorldStatic
 	);
 
-	PredictParams.ActorsToIgnore.Add(GetOwner());
+	PredictParams.ActorsToIgnore.Add(VRPawn);
 	PredictParams.ActorsToIgnore.Add(TeleportVisualizer);
 	
 	UGameplayStatics::PredictProjectilePath(GetWorld(),PredictParams,PredictResult);
-	
-	FVector HitLocation = PredictResult.HitResult.Location;
-	bool bValidHit = PredictResult.HitResult.IsValidBlockingHit();
-	// check if this is a valid location to move to
-	FNavLocation OutLocation;
 
-	FNavAgentProperties AgentProperties = FNavAgentProperties(15, 160);
+	const FVector HitLocation = PredictResult.HitResult.Location;
+	const bool bValidHit = PredictResult.HitResult.IsValidBlockingHit();
+	// check if this is a valid location to move to
 	
-	UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());
-	// TODO: does not give valid location
-	const bool bValidProjection = NavSystem->ProjectPointToNavigation(HitLocation,OutLocation,FVector(1,1,1), &AgentProperties);
+	FVector OutLocation;	
+	const bool bValidProjection = IsValidTeleportLocation(PredictResult.HitResult, OutLocation);
 	
-	if(bUseNavMesh)
+	if (bUseNavMesh)
 	{
-		FinalTeleportLocation = OutLocation.Location;
-		if(bValidTeleportLocation != bValidProjection)
+		FinalTeleportLocation = OutLocation;
+		if (bValidTeleportLocation != bValidProjection)
 		{
 			bValidTeleportLocation = bValidProjection;
 			TeleportVisualizer->SetActorHiddenInGame(!bValidTeleportLocation);
 		}
-	} else
+	}
+	else
 	{
-		if(bValidHit)
+		if (bValidHit)
 		{
 			FinalTeleportLocation = HitLocation;
 			TeleportVisualizer->SetActorHiddenInGame(false);
@@ -156,28 +141,36 @@ void UTeleportationComponent::UpdateTeleportTrace(const FInputActionValue& Value
 		} 
 	}
 
-
 	TArray<FVector> PathPoints;
 	PathPoints.Add(StartPosition);
-	for(FPredictProjectilePathPointData PData : PredictResult.PathData)
+	for (FPredictProjectilePathPointData PData : PredictResult.PathData)
 	{
 		PathPoints.Add(PData.Location);
 	}
 	UNiagaraDataInterfaceArrayFunctionLibrary::SetNiagaraArrayVector(TeleportTraceComponent,FName("User.PointArray"),PathPoints);
-	
+}
+
+bool UTeleportationComponent::IsValidTeleportLocation(const FHitResult& Hit, FVector& ProjectedLocation) const
+{
+	UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
+	const FNavAgentProperties& AgentProps = FNavAgentProperties(15, 160);
+	FNavLocation ProjectedNavLocation;
+	const bool bProjectPoint = (NavSys && NavSys->ProjectPointToNavigation(Hit.Location, ProjectedNavLocation, INVALID_NAVEXTENT, &AgentProps));
+	ProjectedLocation = ProjectedNavLocation.Location;
+	return bProjectPoint /*&& Hit.IsValidBlockingHit()*/;
 }
 
 // On button release -> remove trace and teleport user to location
 void UTeleportationComponent::OnEndTeleportTrace(const FInputActionValue& Value)
 {
-
+	if (!VRPawn)
+		return;
 	// End Teleport Trace
 	bTeleportTraceActive = false;
 	TeleportTraceComponent->SetVisibility(false);
 	TeleportVisualizer->SetActorHiddenInGame(true);
 	
 	bValidTeleportLocation = false;
-	GetOwner()->TeleportTo(FinalTeleportLocation,GetOwner()->GetActorRotation());
-	
+	VRPawn->TeleportTo(FinalTeleportLocation, VRPawn->GetActorRotation());
 }
 
diff --git a/Source/RWTHVRToolkit/Private/Pawn/VirtualRealityPawn.cpp b/Source/RWTHVRToolkit/Private/Pawn/VirtualRealityPawn.cpp
index 9ad53fb9f005231377b44bf76471a3f19c5ce1d8..4ac07d8efcef2b83aae88b2d4fd6aeec97b2023b 100644
--- a/Source/RWTHVRToolkit/Private/Pawn/VirtualRealityPawn.cpp
+++ b/Source/RWTHVRToolkit/Private/Pawn/VirtualRealityPawn.cpp
@@ -6,34 +6,38 @@
 #include "GameFramework/PlayerController.h"
 #include "EnhancedInputComponent.h"
 #include "EnhancedInputSubsystems.h"
-#include "MotionControllerComponent.h"
-#include "Camera/CameraComponent.h"
+#include "ILiveLinkClient.h"
+#include "Core/RWTHVRPlayerState.h"
+#include "Kismet/GameplayStatics.h"
+#include "Logging/StructuredLog.h"
+#include "Pawn/ContinuousMovementComponent.h"
+#include "Pawn/ReplicatedCameraComponent.h"
+#include "Pawn/ReplicatedMotionControllerComponent.h"
 #include "Pawn/VRPawnInputConfig.h"
 #include "Pawn/VRPawnMovement.h"
+#include "Roles/LiveLinkTransformTypes.h"
 #include "Utility/VirtualRealityUtilities.h"
 
 AVirtualRealityPawn::AVirtualRealityPawn(const FObjectInitializer& ObjectInitializer)
 	: Super(ObjectInitializer)
 {
-	bUseControllerRotationYaw = true;
-	bUseControllerRotationPitch = true;
-	bUseControllerRotationRoll = true;
 	BaseEyeHeight = 160.0f;
 
 	SetRootComponent(CreateDefaultSubobject<USceneComponent>(TEXT("Origin")));
-	
-	HeadCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
+
+	HeadCameraComponent = CreateDefaultSubobject<UReplicatedCameraComponent>(TEXT("Camera"));
 	HeadCameraComponent->SetupAttachment(RootComponent);
-	HeadCameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, BaseEyeHeight)); //so it is rendered correctly in editor
-	
+	HeadCameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, BaseEyeHeight));
+	//so it is rendered correctly in editor
+
 	PawnMovement = CreateDefaultSubobject<UVRPawnMovement>(TEXT("Pawn Movement"));
 	PawnMovement->SetUpdatedComponent(RootComponent);
 	PawnMovement->SetHeadComponent(HeadCameraComponent);
-	
-	RightHand = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("Right Hand MCC"));
+
+	RightHand = CreateDefaultSubobject<UReplicatedMotionControllerComponent>(TEXT("Right Hand MCC"));
 	RightHand->SetupAttachment(RootComponent);
-	
-	LeftHand = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("Left Hand MCC"));
+
+	LeftHand = CreateDefaultSubobject<UReplicatedMotionControllerComponent>(TEXT("Left Hand MCC"));
 	LeftHand->SetupAttachment(RootComponent);
 
 	BasicVRInteraction = CreateDefaultSubobject<UBasicVRInteractionComponent>(TEXT("Basic VR Interaction"));
@@ -49,51 +53,145 @@ void AVirtualRealityPawn::Tick(float DeltaSeconds)
 		SetCameraOffset();
 		UpdateRightHandForDesktopInteraction();
 	}
+	EvaluateLivelink();
+}
+
+/*
+ * The alternative would be to do this only on the server on possess and check for player state/type,
+ * as connections now send their playertype over.
+ */
+void AVirtualRealityPawn::NotifyControllerChanged()
+{
+	Super::NotifyControllerChanged();
+
+	// Only do this for all local controlled pawns
+	if (IsLocallyControlled())
+	{
+		// Only do this for the master or when we're running in standalone
+		if (UVirtualRealityUtilities::IsRoomMountedMode() && UVirtualRealityUtilities::IsMaster() || GetNetMode() ==
+			NM_Standalone)
+		{
+			if (HasAuthority())
+				AttachDCRAtoPawn();
+			else
+				ServerAttachDCRAtoPawnRpc();
+		}
+	}
 }
 
 void AVirtualRealityPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
 {
+	Super::SetupPlayerInputComponent(PlayerInputComponent);
+
 	APlayerController* PlayerController = Cast<APlayerController>(GetController());
-	if (!PlayerController) {
-		UE_LOG(LogTemp, Error, TEXT("PC Player Controller is invalid"));
+	if (!PlayerController)
+	{
+		UE_LOG(Toolkit, Warning, TEXT("SetupPlayerInputComponent: Player Controller is invalid"));
 		return;
 	}
-	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer());
-	if(!InputSubsystem)
+
+	// Set the control rotation of the PC to zero again. There is a small period of 2 frames where, when the pawn gets possessed,
+	// the PC takes on the rotation of the VR Headset ONLY WHEN SPAWNING ON A CLIENT. Reset the rotation here such that
+	// bUseControllerRotationYaw=true does not pass the wrong yaw value to the pawn initially.
+	// There is probably a checkbox or way of spawning that prevents that in a better way that this, change if found.
+	PlayerController->SetControlRotation(FRotator::ZeroRotator);
+
+	const ULocalPlayer* LP = PlayerController->GetLocalPlayer();
+	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = LP
+		                                                     ? LP->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>()
+		                                                     : nullptr;
+	if (!InputSubsystem)
 	{
-		UE_LOG(Toolkit,Error,TEXT("[VirtualRealiytPawn.cpp] InputSubsystem IS NOT VALID"));
+		UE_LOG(Toolkit, Error, TEXT("[VirtualRealiytPawn.cpp] InputSubsystem IS NOT VALID"));
+		return;
+	}
+
+	SetupMotionControllerSources();
+
+	// Should not do this here but on connection or on possess I think.
+	if (ARWTHVRPlayerState* State = GetPlayerState<ARWTHVRPlayerState>())
+	{
+		// Might not be properly synced yet?
+		const EPlayerType Type = State->GetPlayerType();
+
+		// Don't do anything with the type if it's been set to clustertype or anything.
+		const bool bClusterType = Type == EPlayerType::nDisplayPrimary || Type == EPlayerType::nDisplaySecondary;
+
+		if (!bClusterType && UVirtualRealityUtilities::IsHeadMountedMode())
+		{
+			// Could be too early to call this RPC...
+			State->RequestSetPlayerType(Type);
+		}
 	}
 
-	if(UVirtualRealityUtilities::IsDesktopMode())
+	if (UVirtualRealityUtilities::IsDesktopMode())
 	{
 		PlayerController->bShowMouseCursor = true;
 		PlayerController->bEnableClickEvents = true;
 		PlayerController->bEnableMouseOverEvents = true;
 	}
-	
+
 	InputSubsystem->ClearAllMappings();
 
 	// add Input Mapping context 
-	InputSubsystem->AddMappingContext(IMCBase,0);
-	
+	InputSubsystem->AddMappingContext(IMCBase, 0);
+
 	UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(PlayerInputComponent);
-	
+
 	// old function bindings for grabbing and releasing
 	EI->BindAction(Fire, ETriggerEvent::Started, this, &AVirtualRealityPawn::OnBeginFire);
 	EI->BindAction(Fire, ETriggerEvent::Completed, this, &AVirtualRealityPawn::OnEndFire);
 
-	EI->BindAction(ToggleNavigationMode,ETriggerEvent::Started,this,&AVirtualRealityPawn::OnToggleNavigationMode);	
+	EI->BindAction(ToggleNavigationMode, ETriggerEvent::Started, this, &AVirtualRealityPawn::OnToggleNavigationMode);
+
+	// Set up mappings on input extension components, need to do this nicely
+
+	for (UActorComponent* Comp : GetComponentsByInterface(UInputExtensionInterface::StaticClass()))
+	{
+		Cast<IInputExtensionInterface>(Comp)->SetupPlayerInput(PlayerInputComponent);
+	}
+}
+
+void AVirtualRealityPawn::EvaluateLivelink() const
+{
+	if (UVirtualRealityUtilities::IsRoomMountedMode() && IsLocallyControlled())
+	{
+		if (bDisableLiveLink || HeadSubjectRepresentation.Subject.IsNone() || HeadSubjectRepresentation.Role == nullptr)
+		{
+			return;
+		}
+
+
+		// Get the LiveLink interface and evaluate the current existing frame data for the given Subject and Role.
+		ILiveLinkClient& LiveLinkClient = IModularFeatures::Get().GetModularFeature<ILiveLinkClient>(
+			ILiveLinkClient::ModularFeatureName);
+		FLiveLinkSubjectFrameData SubjectData;
+		const bool bHasValidData = LiveLinkClient.EvaluateFrame_AnyThread(
+			HeadSubjectRepresentation.Subject, HeadSubjectRepresentation.Role, SubjectData);
+
+		if (!bHasValidData)
+			return;
+
+		// Assume we are using a Transform Role to track the components! This is a slightly dangerous assumption, and could be further improved.
+		const FLiveLinkTransformStaticData* StaticData = SubjectData.StaticData.Cast<FLiveLinkTransformStaticData>();
+		const FLiveLinkTransformFrameData* FrameData = SubjectData.FrameData.Cast<FLiveLinkTransformFrameData>();
+
+		if (StaticData && FrameData)
+		{
+			// Finally, apply the transform to this component according to the static data.
+			ApplyLiveLinkTransform(FrameData->Transform, *StaticData);
+		}
+	}
 }
 
-void AVirtualRealityPawn::UpdateRightHandForDesktopInteraction()
+void AVirtualRealityPawn::UpdateRightHandForDesktopInteraction() const
 {
-	APlayerController* PC = Cast<APlayerController>(GetController());
-	if (PC)
+	if (const APlayerController* PC = Cast<APlayerController>(GetController()))
 	{
 		FVector MouseLocation, MouseDirection;
 		PC->DeprojectMousePositionToWorld(MouseLocation, MouseDirection);
-		FRotator HandOrientation = MouseDirection.ToOrientationRotator();
-		if(bMoveRightHandWithMouse)
+		const FRotator HandOrientation = MouseDirection.ToOrientationRotator();
+		if (bMoveRightHandWithMouse)
 		{
 			RightHand->SetWorldRotation(HandOrientation);
 			RightHand->SetRelativeLocation(HeadCameraComponent->GetRelativeLocation());
@@ -106,6 +204,56 @@ void AVirtualRealityPawn::UpdateRightHandForDesktopInteraction()
 	}
 }
 
+// Todo rewrite this in some other way or attach it differently, this is horrible
+void AVirtualRealityPawn::AttachDCRAtoPawn()
+{
+	if (!CaveSetupActorClass || !CaveSetupActorClass->IsValidLowLevelFast())
+	{
+		UE_LOGFMT(Toolkit, Warning, "No CaveSetup Actor class set in pawn!");
+		return;
+	}
+	TArray<AActor*> FoundActors;
+	UGameplayStatics::GetAllActorsOfClass(GetWorld(), CaveSetupActorClass, FoundActors);
+
+	if (!FoundActors.IsEmpty())
+	{
+		const auto CaveSetupActor = FoundActors[0];
+		FAttachmentTransformRules AttachmentRules = FAttachmentTransformRules::SnapToTargetNotIncludingScale;
+		AttachmentRules.RotationRule = EAttachmentRule::KeepWorld;
+		CaveSetupActor->AttachToActor(this, AttachmentRules);
+	}
+	else
+	{
+		UE_LOGFMT(Toolkit, Warning,
+		          "No CaveSetup Actor found which can be attached to the Pawn! This won't work on the Cave.");
+	}
+}
+
+void AVirtualRealityPawn::SetupMotionControllerSources()
+{
+	// Setup Motion Controllers
+
+	FName MotionControllerSourceLeft = EName::None;
+	FName MotionControllerSourceRight = EName::None;
+	if (UVirtualRealityUtilities::IsHeadMountedMode())
+	{
+		MotionControllerSourceLeft = FName("Left");
+		MotionControllerSourceRight = FName("Right");
+	}
+	if (UVirtualRealityUtilities::IsRoomMountedMode() && UVirtualRealityUtilities::IsMaster())
+	{
+		MotionControllerSourceLeft = LeftSubjectRepresentation.Subject;
+		MotionControllerSourceRight = RightSubjectRepresentation.Subject;
+	}
+	LeftHand->SetTrackingMotionSource(MotionControllerSourceLeft);
+	RightHand->SetTrackingMotionSource(MotionControllerSourceRight);
+}
+
+void AVirtualRealityPawn::ServerAttachDCRAtoPawnRpc_Implementation()
+{
+	AttachDCRAtoPawn();
+}
+
 void AVirtualRealityPawn::SetCameraOffset() const
 {
 	// this also incorporates the BaseEyeHeight, if set as static offset,
@@ -119,40 +267,81 @@ void AVirtualRealityPawn::SetCameraOffset() const
 // legacy grabbing
 void AVirtualRealityPawn::OnBeginFire(const FInputActionValue& Value)
 {
-	UE_LOG(LogTemp,Warning,TEXT("BeginFire"));
+	UE_LOG(LogTemp, Warning, TEXT("BeginFire"));
 	BasicVRInteraction->BeginInteraction();
 }
 
 // legacy grabbing
 void AVirtualRealityPawn::OnEndFire(const FInputActionValue& Value)
 {
-	UE_LOG(Toolkit,Log,TEXT("EndFire"));
+	UE_LOG(Toolkit, Log, TEXT("EndFire"));
 	BasicVRInteraction->EndInteraction();
 }
 
-
 void AVirtualRealityPawn::OnToggleNavigationMode(const FInputActionValue& Value)
 {
 	switch (PawnMovement->NavigationMode)
 	{
-		case EVRNavigationModes::NAV_FLY:
-			PawnMovement->NavigationMode = EVRNavigationModes::NAV_WALK;
-			UE_LOG(Toolkit,Log,TEXT("Changed Nav mode to WALK"));
-			break;
-
-		case EVRNavigationModes::NAV_WALK:
-			PawnMovement->NavigationMode = EVRNavigationModes::NAV_GHOST;
-			UE_LOG(Toolkit,Log,TEXT("Changed Nav mode to GHOST"));
-			break;
-		case EVRNavigationModes::NAV_GHOST:
-			PawnMovement->NavigationMode = EVRNavigationModes::NAV_FLY;
-			UE_LOG(Toolkit, Log, TEXT("Changed Nav mode to FLY"));
-			break;
-		default:
-			PawnMovement->NavigationMode = EVRNavigationModes::NAV_WALK;
-			UE_LOG(Toolkit,Log,TEXT("Changed Nav mode to WALK"));
-			break;
+	case EVRNavigationModes::NAV_FLY:
+		PawnMovement->NavigationMode = EVRNavigationModes::NAV_WALK;
+		UE_LOG(Toolkit, Log, TEXT("Changed Nav mode to WALK"));
+		break;
+
+	case EVRNavigationModes::NAV_WALK:
+		PawnMovement->NavigationMode = EVRNavigationModes::NAV_GHOST;
+		UE_LOG(Toolkit, Log, TEXT("Changed Nav mode to GHOST"));
+		break;
+	case EVRNavigationModes::NAV_GHOST:
+		PawnMovement->NavigationMode = EVRNavigationModes::NAV_FLY;
+		UE_LOG(Toolkit, Log, TEXT("Changed Nav mode to FLY"));
+		break;
+	default:
+		PawnMovement->NavigationMode = EVRNavigationModes::NAV_WALK;
+		UE_LOG(Toolkit, Log, TEXT("Changed Nav mode to WALK"));
+		break;
 	}
 }
 
+void AVirtualRealityPawn::ApplyLiveLinkTransform(const FTransform& Transform,
+                                                 const FLiveLinkTransformStaticData& StaticData) const
+{
+	if (StaticData.bIsLocationSupported)
+	{
+		if (bWorldTransform)
+		{
+			HeadCameraComponent->SetWorldLocation(Transform.GetLocation(), false, nullptr,
+			                                      ETeleportType::TeleportPhysics);
+		}
+		else
+		{
+			HeadCameraComponent->SetRelativeLocation(Transform.GetLocation(), false, nullptr,
+			                                         ETeleportType::TeleportPhysics);
+		}
+	}
 
+	if (StaticData.bIsRotationSupported)
+	{
+		if (bWorldTransform)
+		{
+			HeadCameraComponent->SetWorldRotation(Transform.GetRotation(), false, nullptr,
+			                                      ETeleportType::TeleportPhysics);
+		}
+		else
+		{
+			HeadCameraComponent->SetRelativeRotation(Transform.GetRotation(), false, nullptr,
+			                                         ETeleportType::TeleportPhysics);
+		}
+	}
+
+	if (StaticData.bIsScaleSupported)
+	{
+		if (bWorldTransform)
+		{
+			HeadCameraComponent->SetWorldScale3D(Transform.GetScale3D());
+		}
+		else
+		{
+			HeadCameraComponent->SetRelativeScale3D(Transform.GetScale3D());
+		}
+	}
+}
diff --git a/Source/RWTHVRToolkit/Private/Utility/VirtualRealityUtilities.cpp b/Source/RWTHVRToolkit/Private/Utility/VirtualRealityUtilities.cpp
index 82cd37c031d5dec8c41ccdd8eb4089b363c3c3c9..2445cacade32d34f6ca149af691255c6ee81d605 100644
--- a/Source/RWTHVRToolkit/Private/Utility/VirtualRealityUtilities.cpp
+++ b/Source/RWTHVRToolkit/Private/Utility/VirtualRealityUtilities.cpp
@@ -177,42 +177,12 @@ USceneComponent* UVirtualRealityUtilities::GetNamedClusterComponent(const ENamed
 	}
 }
 
-UEnhancedInputComponent* UVirtualRealityUtilities::GetVRPawnInputComponent(const UWorld* World)
-{
-	const APawn* VRPawn = Cast<AVirtualRealityPawn>(UGameplayStatics::GetPlayerPawn(World,0));
-	if(!VRPawn)
-	{
-		UE_LOG(Toolkit, Error,TEXT("[VirtualRealityUtilities.cpp] cannot cast current Pawn to AVirtualRealityPawn"));
-		return nullptr;
-	}
-	return Cast<UEnhancedInputComponent>(VRPawn->InputComponent);
-}
-
-UEnhancedInputLocalPlayerSubsystem* UVirtualRealityUtilities::GetVRPawnLocalPlayerSubsystem(UWorld* World)
-{
-	const APawn* VRPawn = Cast<AVirtualRealityPawn>(UGameplayStatics::GetPlayerPawn(World,0));
-	if(!VRPawn)
-	{
-		UE_LOG(Toolkit, Error,TEXT("[VirtualRealityUtilities.cpp] cannot cast current Pawn to AVirtualRealityPawn"));
-		return nullptr;
-	}
-	const APlayerController* PlayerController = Cast<APlayerController>(VRPawn->GetController());
-	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer());
-	if(!InputSubsystem)
-	{
-		UE_LOG(Toolkit,Error,TEXT("[VirtualRealityUtilities.cpp] InputSubsystem IS NOT VALID"));
-		return nullptr;
-	}
-	return InputSubsystem;
-	
-}
-
 void UVirtualRealityUtilities::ShowErrorAndQuit(UWorld* WorldContext, const FString& Message)
 {
 	UE_LOG(Toolkit, Error, TEXT("%s"), *Message)
 #if WITH_EDITOR
 	const FText Title = FText::FromString(FString("RUNTIME ERROR"));
-	FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Message), &Title);
+	FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Message), Title);
 #endif
 	UKismetSystemLibrary::QuitGame(WorldContext, nullptr, EQuitPreference::Quit, false);
 
diff --git a/Source/RWTHVRToolkit/Public/Core/ClientTransformReplication.h b/Source/RWTHVRToolkit/Public/Core/ClientTransformReplication.h
new file mode 100644
index 0000000000000000000000000000000000000000..347a8335a55f42b7b58e79a8ee293c0d7c700515
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Core/ClientTransformReplication.h
@@ -0,0 +1,69 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Components/ActorComponent.h"
+#include "Core/VRTransformRep.h"
+#include "ClientTransformReplication.generated.h"
+
+
+/*
+* Simple Client Transform Replication Component. Replicates the owning actor's root transform from owning client to server,
+* from there to all other clients.
+*/	
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
+class RWTHVRTOOLKIT_API UClientTransformReplication : public UActorComponent
+{
+	GENERATED_BODY()
+
+public:
+	UClientTransformReplication();
+
+protected:
+	
+	/*
+	* For now, replicate in a naive sending every x ms if the transform has changed.
+	* This is way overkill, as we should only be sending input. However, I am not yet fully sure how
+	* the Unreal Client-Authoritative thingy works and what part simulates e.g. gravity.
+	* As this modifies only the tracking origin, latency should not be that much of an issue, so theoretically
+	* Server-Authority should work here too, in which case we'd just send the x and y input.
+	* Try both ways.
+	*/	
+	
+	/////////////////////////////////////////////////////////////////////////////////////////////////////
+	// Full transform update replication
+	/////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	// Rate to update the position to the server, 100htz is default (same as replication rate, should also hit every tick).
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Networking", meta = (ClampMin = "0", UIMin = "0"))
+	float ControllerNetUpdateRate;
+
+	// Accumulates time until next send
+	float ControllerNetUpdateCount;
+
+	// Replicated transform property - used to replicate from server to all non-owning clients
+	UPROPERTY(EditDefaultsOnly, ReplicatedUsing = OnRep_ReplicatedTransform, Category = "Networking")
+	FVRTransformRep ReplicatedTransform;
+
+	// Called whenever ReplicatedTransform is replicated to clients. Not called on Server/Owning client
+	UFUNCTION()
+	virtual void OnRep_ReplicatedTransform()
+	{
+		// Modify owner position - how does this work in movement components?
+		// For now, directly apply the transforms:
+		auto* OwningActor = GetOwner();
+		if (OwningActor && OwningActor->HasValidRootComponent())
+			OwningActor->SetActorLocationAndRotation(ReplicatedTransform.Position, ReplicatedTransform.Rotation);
+	}
+
+	// Unreliable Server RPC that sends the transform from owning client to the server
+	UFUNCTION(Unreliable, Server, WithValidation)
+	void SendControllerTransform_ServerRpc(FVRTransformRep NewTransform);
+
+	void UpdateState(float DeltaTime);
+
+public:
+	virtual void TickComponent(float DeltaTime, ELevelTick TickType,
+	                           FActorComponentTickFunction* ThisTickFunction) override;
+};
diff --git a/Source/RWTHVRToolkit/Public/Core/PlayerType.h b/Source/RWTHVRToolkit/Public/Core/PlayerType.h
new file mode 100644
index 0000000000000000000000000000000000000000..8364bd2df3751604ed28b12b8b4ff15d28ab3ce6
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Core/PlayerType.h
@@ -0,0 +1,20 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+
+#include "PlayerType.generated.h"
+
+/**
+ * Enum that defines the various player types. Potentially this could be changed into the Enum/FName hybrid that's often
+ * used, which would make this runtime extendable. Probably unnecessary.
+ */
+UENUM(BlueprintType)
+enum class EPlayerType : uint8
+{
+	nDisplayPrimary UMETA(DisplayName = "nDisplay Primary"),
+	nDisplaySecondary UMETA(DisplayName = "nDisplay Secondary"),
+	Desktop UMETA(DisplayName = "Desktop"),
+	HMD UMETA(DisplayName = "HMD")
+};
diff --git a/Source/RWTHVRToolkit/Public/Core/RWTHVRGameModeBase.h b/Source/RWTHVRToolkit/Public/Core/RWTHVRGameModeBase.h
new file mode 100644
index 0000000000000000000000000000000000000000..ba4db0209058f6d813a72a26519f7f79992cba85
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Core/RWTHVRGameModeBase.h
@@ -0,0 +1,22 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "GameFramework/GameModeBase.h"
+#include "RWTHVRGameModeBase.generated.h"
+
+/**
+ * Simple GameModeBase extension that checks for join options such that we can distinguish between primary
+ * and secondary nodes in clusters. Could be moved to a different place as well, but quite reasonable here for now.
+ * Keep in mind that GameMode only exists on the server!
+ */
+UCLASS()
+class RWTHVRTOOLKIT_API ARWTHVRGameModeBase : public AGameModeBase
+{
+	GENERATED_BODY()
+
+protected:
+	virtual FString InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId,
+		const FString& Options, const FString& Portal) override;
+};
diff --git a/Source/RWTHVRToolkit/Public/Core/RWTHVRPlayerState.h b/Source/RWTHVRToolkit/Public/Core/RWTHVRPlayerState.h
new file mode 100644
index 0000000000000000000000000000000000000000..4f9cc9d5639c09587cab91c1ed0bd9dcc0bc0ec3
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Core/RWTHVRPlayerState.h
@@ -0,0 +1,45 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "PlayerType.h"
+#include "GameFramework/PlayerState.h"
+#include "RWTHVRPlayerState.generated.h"
+
+enum class EPlayerType : uint8;
+/**
+ * Extension of the PlayerState that additionally holds information about what type the player is.
+ * E.g. nDisplayPrimary, nDisplaySecondary, Regular (ideally split into HMD | Desktop etc)
+ */
+UCLASS()
+class RWTHVRTOOLKIT_API ARWTHVRPlayerState : public APlayerState
+{
+	GENERATED_BODY()
+
+private:
+	/** Replicated player type for this player*/
+	UPROPERTY(Replicated, Category=PlayerState, BlueprintGetter=GetPlayerType, meta=(AllowPrivateAccess))
+	EPlayerType PlayerType = EPlayerType::Desktop;
+
+	UFUNCTION(Reliable, Server)
+	void SetPlayerTypeServerRpc(EPlayerType NewPlayerType);
+
+	void SetPlayerType(EPlayerType NewPlayerType);
+
+public:
+
+	UFUNCTION(BlueprintGetter)
+	EPlayerType GetPlayerType() const
+	{
+		return PlayerType;
+	}	
+
+	UFUNCTION(BlueprintCallable)
+	void RequestSetPlayerType(EPlayerType NewPlayerType);
+
+	virtual void CopyProperties(APlayerState* PlayerState) override;
+	virtual void OverrideWith(APlayerState* PlayerState) override;
+
+	virtual void GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const override;
+};
diff --git a/Source/RWTHVRToolkit/Public/Fixes/LiveLinkMotionControllerFix.h b/Source/RWTHVRToolkit/Public/Fixes/LiveLinkMotionControllerFix.h
index 3e8bb02b97f5c333281fd3b23df5109f9370b1ce..208a0ad0d52eb3a99e1437bf0c8088f038862921 100644
--- a/Source/RWTHVRToolkit/Public/Fixes/LiveLinkMotionControllerFix.h
+++ b/Source/RWTHVRToolkit/Public/Fixes/LiveLinkMotionControllerFix.h
@@ -13,6 +13,15 @@
 #define LOCTEXT_NAMESPACE "LiveLinkMotionController"
 
 
+/*
+ * Workaround due to an issue regarding LiveLInk Subjects as MotionController sources:
+ * The original FLiveLinkMotionController updates its subject list OnLiveLinkSourcesChanged
+ * instead of OnLiveLinkSubjectsChanged, which leads to the subjects not being actually initialized when queried.
+ * As they are a modular feature, this is a copy & paste of the original class with just that simple line changed.
+ * Works well non-invasive and can be removed if the PR is merged or they fix it another way:
+ * https://github.com/EpicGames/UnrealEngine/pull/10895
+*/
+
 class FLiveLinkMotionControllerFix : public IMotionController
 {
 	// Internal structure for caching enumerated data
diff --git a/Source/RWTHVRToolkit/Public/Pawn/BasicVRInteractionComponent.h b/Source/RWTHVRToolkit/Public/Pawn/BasicVRInteractionComponent.h
index e62fbf21485da813efa4317cd553169af4beb7a0..bf62656ecf13cb0e336c201ffc11d40c9a58ca0f 100644
--- a/Source/RWTHVRToolkit/Public/Pawn/BasicVRInteractionComponent.h
+++ b/Source/RWTHVRToolkit/Public/Pawn/BasicVRInteractionComponent.h
@@ -19,7 +19,7 @@ enum EInteractionRayVisibility
 };
 
 
-UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
+UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class RWTHVRTOOLKIT_API UBasicVRInteractionComponent : public UWidgetInteractionComponent
 {
 	GENERATED_BODY()
@@ -28,53 +28,76 @@ public:
 	// Sets default values for this component's properties
 	UBasicVRInteractionComponent();
 
-	void BeginPlay() override;
+	virtual void BeginPlay() override;
 
-	UFUNCTION(BlueprintCallable) void BeginInteraction(); 
-	UFUNCTION(BlueprintCallable) void EndInteraction();   	
-	
-	// Called every frame
 	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
 
-	UPROPERTY(EditAnywhere,BlueprintReadWrite) float MaxGrabDistance = 50;
-	UPROPERTY(EditAnywhere,BlueprintReadWrite) float MaxClickDistance = 500;
-	// Enable this if you want to interact with Targetable classes or use EInteractionRayVisibility::VisibleOnHoverOnly
-	UPROPERTY(EditAnywhere) bool bCanRaytraceEveryTick = false;
-	UPROPERTY(EditAnywhere) TEnumAsByte<EInteractionRayVisibility> InteractionRayVisibility = EInteractionRayVisibility::Invisible;
+	UFUNCTION(BlueprintCallable)
+	void BeginInteraction();
+	
+	UFUNCTION(BlueprintCallable)
+	void EndInteraction();   	
 
-	UFUNCTION(BlueprintCallable) void Initialize(USceneComponent* RayEmitter);
+	UFUNCTION(BlueprintCallable)
+	void Initialize(USceneComponent* RayEmitter);
 	
-	UFUNCTION(BlueprintCallable, BlueprintPure) AActor* GetGrabbedActor() const { return GrabbedActor;}
-	UFUNCTION(BlueprintCallable, BlueprintPure) USceneComponent* GetInteractionRayEmitter() const { return InteractionRayEmitter;	}
+	UFUNCTION(BlueprintCallable, BlueprintPure)
+	AActor* GetGrabbedActor() const { return GrabbedActor;}
+	
+	UFUNCTION(BlueprintCallable, BlueprintPure)
+	USceneComponent* GetInteractionRayEmitter() const { return InteractionRayEmitter;	}
+
+	UFUNCTION(BlueprintCallable)
+	void SetInteractionRayVisibility(EInteractionRayVisibility NewVisibility);
 
-	UFUNCTION(BlueprintCallable) void SetInteractionRayVisibility(EInteractionRayVisibility NewVisibility);
+	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pawn|Interaction")
+	UStaticMeshComponent* InteractionRay;
+	
+	UPROPERTY(EditAnywhere,BlueprintReadWrite)
+	float MaxGrabDistance = 50;
+	
+	UPROPERTY(EditAnywhere,BlueprintReadWrite)
+	float MaxClickDistance = 500;
+	
+	// Enable this if you want to interact with Targetable classes or use EInteractionRayVisibility::VisibleOnHoverOnly
+	UPROPERTY(EditAnywhere)
+	bool bCanRaytraceEveryTick = false;
+	
+	UPROPERTY(EditAnywhere)
+	TEnumAsByte<EInteractionRayVisibility> InteractionRayVisibility = EInteractionRayVisibility::Invisible;
+	
 private:
 	/* Holding a reference to the actor that is currently being grabbed */
-	UPROPERTY() AActor* GrabbedActor;
+	UPROPERTY()
+	AActor* GrabbedActor;
+	
 	/* Holds a reference to the grabbed actors physics simulating component if there was one*/
-	UPROPERTY() UPrimitiveComponent* ComponentSimulatingPhysics = nullptr;
-	UPROPERTY() UGrabbingBehaviorComponent* Behavior = nullptr;
-	UPROPERTY() USceneComponent* InteractionRayEmitter = nullptr;
-	UPROPERTY() UStaticMeshComponent* InteractionRay = nullptr;
+	UPROPERTY()
+	UPrimitiveComponent* ComponentSimulatingPhysics = nullptr;
+	
+	UPROPERTY()
+	UGrabbingBehaviorComponent* Behavior = nullptr;
+	
+	UPROPERTY()
+	USceneComponent* InteractionRayEmitter = nullptr;
 	
 	/* Stores the reference of the Actor that was hit in the last frame*/
-	UPROPERTY() AActor* LastActorHit = nullptr;
-	void HandlePhysicsAndAttachActor(AActor* HitActor);
+	UPROPERTY()
+	AActor* LastActorHit = nullptr;
+	
+	void HandlePhysicsAndAttachActor(const AActor* HitActor);
 	FTwoVectors GetHandRay(float Length) const;
 	TOptional<FHitResult> RaytraceForFirstHit(const FTwoVectors& Ray) const;
-};
-
-// Free utility functions 
-/*
-	Returns the UPrimitiveComponent simulating physics that is highest in the hierarchy
-*/
-UPrimitiveComponent* GetFirstComponentSimulatingPhysics(const AActor* TargetActor);
-/*
-	Recursive Function
-	If parent component simulates physics returns GetHighestParentSimulatingPhysics(Parent)
-	else returns Comp itself
-*/
-UPrimitiveComponent* GetHighestParentSimulatingPhysics(UPrimitiveComponent* Comp);
-
-
 
+	// Free utility functions 
+	/*
+		Returns the UPrimitiveComponent simulating physics that is highest in the hierarchy
+	*/
+	static UPrimitiveComponent* GetFirstComponentSimulatingPhysics(const AActor* TargetActor);
+	/*
+		Recursive Function
+		If parent component simulates physics returns GetHighestParentSimulatingPhysics(Parent)
+		else returns Comp itself
+	*/
+	static UPrimitiveComponent* GetHighestParentSimulatingPhysics(UPrimitiveComponent* Comp);
+};
diff --git a/Source/RWTHVRToolkit/Public/Pawn/ContinuousMovementComponent.h b/Source/RWTHVRToolkit/Public/Pawn/ContinuousMovementComponent.h
index 586076048e6a196b10008111526b78a996cda26c..e4ea442da36c0d766be850c290f29183f7835184 100644
--- a/Source/RWTHVRToolkit/Public/Pawn/ContinuousMovementComponent.h
+++ b/Source/RWTHVRToolkit/Public/Pawn/ContinuousMovementComponent.h
@@ -3,6 +3,7 @@
 #pragma once
 
 #include "CoreMinimal.h"
+#include "InputExtensionInterface.h"
 #include "Pawn/VirtualRealityPawn.h"
 #include "Pawn/MovementComponentBase.h"
 #include "Components/ActorComponent.h"
@@ -25,27 +26,24 @@ class RWTHVRTOOLKIT_API UContinuousMovementComponent : public UMovementComponent
 	GENERATED_BODY()
 
 public:
-
-	virtual void BeginPlay() override;
 	
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VR Movement")
 	EVRSteeringModes SteeringMode = EVRSteeringModes::STEER_HAND_DIRECTED;
 	
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VR Movement")
 	bool bMoveWithRightHand = true;
-
-
+	
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input")
-	class UInputMappingContext* IMCMovementLeft;
+	UInputMappingContext* IMCMovementLeft;
 
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input")
-	class UInputMappingContext* IMCMovementRight;
+	UInputMappingContext* IMCMovementRight;
 		
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input|Actions")
-	class UInputAction* Move;
+	UInputAction* Move;
 
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input|Actions")
-	class UInputAction* MoveUp;
+	UInputAction* MoveUp;
 	
 	/*Movement Input*/
 	UFUNCTION(BlueprintCallable)
@@ -53,7 +51,8 @@ public:
 
 	UFUNCTION(BlueprintCallable)
 	void OnMoveUp(const FInputActionValue& Value);
-
+	
+	virtual void SetupPlayerInput(UInputComponent* PlayerInputComponent) override;
 
 private:
 
@@ -64,8 +63,5 @@ private:
 	UMotionControllerComponent* MovementHand;
 	
 	UPROPERTY()
-	UMotionControllerComponent* RotationHand;
-
-
-	virtual void SetupInputActions();
+	UMotionControllerComponent* RotationHand;	
 };
diff --git a/Source/RWTHVRToolkit/Public/Pawn/InputExtensionInterface.h b/Source/RWTHVRToolkit/Public/Pawn/InputExtensionInterface.h
new file mode 100644
index 0000000000000000000000000000000000000000..543fe244a7566c34651c353b3b60f392aa90f9e9
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Pawn/InputExtensionInterface.h
@@ -0,0 +1,32 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "UObject/Interface.h"
+#include "InputExtensionInterface.generated.h"
+
+class UEnhancedInputLocalPlayerSubsystem;
+
+/**
+ * Simple interface that can be implemented if your class/component defines input actions.
+ * Use SetupPlayerInput to bind actions, it will be called by VirtualRealityPawn::SetupPlayerInputComponent. 
+ */
+UINTERFACE(MinimalAPI, NotBlueprintable)
+class UInputExtensionInterface : public UInterface
+{
+	GENERATED_BODY()
+};
+
+class RWTHVRTOOLKIT_API IInputExtensionInterface
+{
+	GENERATED_BODY()
+
+public:
+
+	// Called by VirtualRealityPawn::SetupPlayerInputComponent
+	virtual void SetupPlayerInput(UInputComponent* PlayerInputComponent) {}
+
+	// Helper function to get the local player subsystem 
+	virtual UEnhancedInputLocalPlayerSubsystem* GetEnhancedInputLocalPlayerSubsystem(APawn* Pawn) const;
+	
+};
diff --git a/Source/RWTHVRToolkit/Public/Pawn/ReplicatedCameraComponent.h b/Source/RWTHVRToolkit/Public/Pawn/ReplicatedCameraComponent.h
new file mode 100644
index 0000000000000000000000000000000000000000..b211d827423d8497ea2231354160f3b892e4c86a
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Pawn/ReplicatedCameraComponent.h
@@ -0,0 +1,54 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Camera/CameraComponent.h"
+#include "Core/VRTransformRep.h"
+#include "ReplicatedCameraComponent.generated.h"
+
+/**
+ * Simple CameraComponent with added client-side transform replication.
+ */
+UCLASS()
+class RWTHVRTOOLKIT_API UReplicatedCameraComponent : public UCameraComponent
+{
+	GENERATED_BODY()
+public:
+	// Sets default values for this component's properties
+	UReplicatedCameraComponent();
+	
+protected:
+
+	/////////////////////////////////////////////////////////////////////////////////////////////////////
+	// Full transform update replication
+	/////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	// Rate to update the position to the server, 100htz is default (same as replication rate, should also hit every tick).
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Networking", meta = (ClampMin = "0", UIMin = "0"))
+	float ControllerNetUpdateRate;
+
+	// Accumulates time until next send
+	float ControllerNetUpdateCount;
+	
+	UPROPERTY(EditDefaultsOnly, ReplicatedUsing = OnRep_ReplicatedTransform, Category = "Networking")
+	FVRTransformRep ReplicatedTransform;
+	
+	void UpdateState(float DeltaTime);
+
+	UFUNCTION()
+	virtual void OnRep_ReplicatedTransform()
+	{
+		//For now, directly apply the transforms:
+		if (!GetOwner()->HasLocalNetOwner())
+			SetRelativeLocationAndRotation(ReplicatedTransform.Position, ReplicatedTransform.Rotation);
+	}
+
+	UFUNCTION(Unreliable, Server, WithValidation)
+	void ServerSendControllerTransformRpc(FVRTransformRep NewTransform);
+
+public:
+	// Called every frame
+	virtual void TickComponent(float DeltaTime, ELevelTick TickType,
+							   FActorComponentTickFunction* ThisTickFunction) override;
+};
diff --git a/Source/RWTHVRToolkit/Public/Pawn/ReplicatedMotionControllerComponent.h b/Source/RWTHVRToolkit/Public/Pawn/ReplicatedMotionControllerComponent.h
new file mode 100644
index 0000000000000000000000000000000000000000..1d77aa634e2e74f0d35b24ab0ec131933fd81c4b
--- /dev/null
+++ b/Source/RWTHVRToolkit/Public/Pawn/ReplicatedMotionControllerComponent.h
@@ -0,0 +1,55 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MotionControllerComponent.h"
+#include "Core/VRTransformRep.h"
+#include "ReplicatedMotionControllerComponent.generated.h"
+
+/**
+ * Simple MotionControllerComponent with added client-side transform replication.
+ */
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
+class RWTHVRTOOLKIT_API UReplicatedMotionControllerComponent : public UMotionControllerComponent
+{
+	GENERATED_BODY()
+
+public:
+	// Sets default values for this component's properties
+	UReplicatedMotionControllerComponent();
+	
+protected:
+
+	/////////////////////////////////////////////////////////////////////////////////////////////////////
+	// Full transform update replication
+	/////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	// Rate to update the position to the server, 100htz is default (same as replication rate, should also hit every tick).
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Networking", meta = (ClampMin = "0", UIMin = "0"))
+	float ControllerNetUpdateRate;
+
+	// Accumulates time until next send
+	float ControllerNetUpdateCount;
+	
+	UPROPERTY(EditDefaultsOnly, ReplicatedUsing = OnRep_ReplicatedTransform, Category = "Networking")
+	FVRTransformRep ReplicatedTransform;
+	
+	void UpdateState(float DeltaTime);
+
+	UFUNCTION()
+	virtual void OnRep_ReplicatedTransform()
+	{
+		// For now, directly apply the transforms:
+		SetRelativeLocationAndRotation(ReplicatedTransform.Position, ReplicatedTransform.Rotation);
+	}
+
+	UFUNCTION(Unreliable, Server, WithValidation)
+	void SendControllerTransform_ServerRpc(FVRTransformRep NewTransform);
+
+public:
+	// Called every frame
+	virtual void TickComponent(float DeltaTime, ELevelTick TickType,
+	                           FActorComponentTickFunction* ThisTickFunction) override;
+
+};
diff --git a/Source/RWTHVRToolkit/Public/Pawn/TeleportationComponent.h b/Source/RWTHVRToolkit/Public/Pawn/TeleportationComponent.h
index 54c3389aededa63c4bcaa80ed2d05262d2a00e1b..867c122f412b8f2834285287a27d05d63f16ae4a 100644
--- a/Source/RWTHVRToolkit/Public/Pawn/TeleportationComponent.h
+++ b/Source/RWTHVRToolkit/Public/Pawn/TeleportationComponent.h
@@ -3,27 +3,21 @@
 #pragma once
 
 #include "CoreMinimal.h"
+#include "InputExtensionInterface.h"
 #include "Components/ActorComponent.h"
 #include "Pawn/VirtualRealityPawn.h"
 #include "NiagaraComponent.h"
 #include "Kismet/GameplayStaticsTypes.h"
 #include "Pawn/MovementComponentBase.h"
 
-
 #include "TeleportationComponent.generated.h"
 
-
 UCLASS(Blueprintable)
 class RWTHVRTOOLKIT_API UTeleportationComponent : public UMovementComponentBase
 {
 	GENERATED_BODY()
 
-protected:
-	// Called when the game starts
-	virtual void BeginPlay() override;
-
 public:
-
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VR Movement")
 	bool bMoveWithRightHand = true;
 
@@ -34,7 +28,6 @@ public:
 	UPROPERTY(VisibleAnywhere, Category = "VR Movement|Teleport")
 	bool bUseNavMesh = false;
 
-
 	/**
 	 * Speed at which the projectile shoots out from the controller to get the teleport location
 	 * Higher values = larger teleportation range
@@ -43,13 +36,23 @@ public:
 	float TeleportLaunchSpeed = 800;
 
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input")
-	class UInputMappingContext* IMCTeleportLeft;
+	UInputMappingContext* IMCTeleportLeft;
 
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input")
-	class UInputMappingContext* IMCTeleportRight;
+	UInputMappingContext* IMCTeleportRight;
 
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VR Movement|Input|Actions")
-	class UInputAction* Move;
+	UInputAction* Move;
+
+	// Trace Visualization
+	UPROPERTY(EditAnywhere)
+	TSubclassOf<AActor> BPTeleportVisualizer;
+
+	UPROPERTY(EditDefaultsOnly)
+	UNiagaraSystem* TeleportTraceSystem;
+
+	UPROPERTY()
+	UNiagaraComponent* TeleportTraceComponent;
 
 	/*Movement Input*/
 	UFUNCTION(BlueprintCallable)
@@ -57,20 +60,13 @@ public:
 
 	UFUNCTION(BlueprintCallable)
 	void UpdateTeleportTrace(const FInputActionValue& Value);
+	bool IsValidTeleportLocation(const FHitResult& Hit, FVector& ProjectedLocation) const;
 
 	UFUNCTION(BlueprintCallable)
 	void OnEndTeleportTrace(const FInputActionValue& Value);
 
-	// Trace Visualization
-	UPROPERTY(EditAnywhere)
-	TSubclassOf<AActor> BPTeleportVisualizer;
-
-	UPROPERTY(EditDefaultsOnly)
-	UNiagaraSystem* TeleportTraceSystem;
+	virtual void SetupPlayerInput(UInputComponent* PlayerInputComponent) override;
 
-	UPROPERTY()
-	UNiagaraComponent* TeleportTraceComponent;
-	
 private:
 	UPROPERTY()
 	UMotionControllerComponent* TeleportationHand;
@@ -79,10 +75,8 @@ private:
 	UMotionControllerComponent* RotationHand;
 
 	UPROPERTY()
-	class UInputMappingContext* IMCMovement;
-
-	virtual void SetupInputActions();
-
+	UInputMappingContext* IMCMovement;
+	
 	bool bTeleportTraceActive;
 	float TeleportProjectileRadius = 3.6;
 	float RotationArrowRadius = 10.0;
@@ -92,5 +86,4 @@ private:
 
 	UPROPERTY()
 	AActor* TeleportVisualizer;
-	
 };
diff --git a/Source/RWTHVRToolkit/Public/Pawn/VirtualRealityPawn.h b/Source/RWTHVRToolkit/Public/Pawn/VirtualRealityPawn.h
index 1c85f8cf157376ddb7665bb3a02bc04a759abdcd..9238d465709831c05e9f014710a5738e5e532b32 100644
--- a/Source/RWTHVRToolkit/Public/Pawn/VirtualRealityPawn.h
+++ b/Source/RWTHVRToolkit/Public/Pawn/VirtualRealityPawn.h
@@ -4,42 +4,44 @@
 
 #include "BasicVRInteractionComponent.h"
 #include "CoreMinimal.h"
+#include "LiveLinkRole.h"
 #include "Pawn/VRPawnMovement.h"
 #include "VirtualRealityPawn.generated.h"
 
+class UInputMappingContext;
+class UInputAction;
 class UCameraComponent;
-class ULiveLinkComponentController;
 class UMotionControllerComponent;
+struct FLiveLinkTransformStaticData;
 
 /**
- * 
+ * Pawn implementation with additional VR functionality, can be used in the Cave, with an HMD and on desktop.
  */
 UCLASS(Abstract)
 class RWTHVRTOOLKIT_API AVirtualRealityPawn : public APawn
 {
 	GENERATED_BODY()
+
 public:
 	AVirtualRealityPawn(const FObjectInitializer& ObjectInitializer);
 
 	virtual void Tick(float DeltaSeconds) override;
-	
+
+	virtual void NotifyControllerChanged() override;
+
 	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pawn|MotionControllers")
 	UMotionControllerComponent* RightHand;
-	
+
 	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pawn|MotionControllers")
 	UMotionControllerComponent* LeftHand;
 
 	/* Interaction */
 	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pawn|Interaction")
 	UBasicVRInteractionComponent* BasicVRInteraction;
-	
+
 	/* Movement */
 	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pawn|Movement")
 	UVRPawnMovement* PawnMovement;
-	
-	/* CameraComponent */
-	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pawn|Camera")
-	UCameraComponent* HeadCameraComponent;
 
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pawn|Desktop Movement")
 	bool bMoveRightHandWithMouse = true;
@@ -47,34 +49,78 @@ public:
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pawn|Desktop Movement")
 	bool bMoveLeftHandWithMouse = false;
 
+	/* CameraComponent */
+	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pawn|Camera")
+	UCameraComponent* HeadCameraComponent;
+
+	// LiveLink functionality
+
+	/* Set whether nDisplay should disable LiveLink tracking*/
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pawn|LiveLink")
+	bool bDisableLiveLink = false;
+
+	/* Set the LiveLink Subject Representation to be used by this pawn. */
+	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Pawn|LiveLink")
+	FLiveLinkSubjectRepresentation HeadSubjectRepresentation;
+
+	/* Set the LiveLink Subject Representation to be used by this pawn. */
+	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Pawn|LiveLink")
+	FLiveLinkSubjectRepresentation LeftSubjectRepresentation;
+
+	/* Set the LiveLink Subject Representation to be used by this pawn. */
+	UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Pawn|LiveLink")
+	FLiveLinkSubjectRepresentation RightSubjectRepresentation;
+
+	/* Set the transform of the component in world space of in its local reference frame. */
+	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;
 
+	/* LiveLink helper function called on tick */
+	void EvaluateLivelink() const;
+
+	/* Helper function that applies the LiveLink data to this component. Taken from the LiveLink Transform Controller. */
+	void ApplyLiveLinkTransform(const FTransform& Transform, const FLiveLinkTransformStaticData& StaticData) const;
+
 	/* Interaction */
 	UFUNCTION(BlueprintCallable, Category = "Pawn|Interaction")
 	void OnBeginFire(const FInputActionValue& Value);
-	
+
 	UFUNCTION(BlueprintCallable, Category = "Pawn|Interaction")
 	void OnEndFire(const FInputActionValue& Value);
 
 	UFUNCTION(BlueprintCallable)
 	void OnToggleNavigationMode(const FInputActionValue& Value);
-	
+
 	/* Input */
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pawn|Input")
-	class UInputMappingContext* IMCBase;
-	
+	UInputMappingContext* IMCBase;
+
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pawn|Input")
-	class UInputAction* Fire;
+	UInputAction* Fire;
 
 	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pawn|Input")
-	class UInputAction* ToggleNavigationMode;
+	UInputAction* ToggleNavigationMode;
 
-	/**
-	* Fixes camera rotation in desktop mode.
-	*/
+	/* Fixes camera rotation in desktop mode. */
 	void SetCameraOffset() const;
-	void UpdateRightHandForDesktopInteraction();
+	void UpdateRightHandForDesktopInteraction() const;
+
+	/* Replicated functionality */
+
+	/* Ask the server to attach the DCRA to the correct pawn */
+	UFUNCTION(Reliable, Server)
+	void ServerAttachDCRAtoPawnRpc();
+
+	/* Attaches the DCRA to the pawn */
+	void AttachDCRAtoPawn();
 
+	/* Set device specific motion controller sources (None, L/R, Livelink) */
+	void SetupMotionControllerSources();
 };
diff --git a/Source/RWTHVRToolkit/Public/Utility/VirtualRealityUtilities.h b/Source/RWTHVRToolkit/Public/Utility/VirtualRealityUtilities.h
index 831f9a1143bc81d51434677783f133186fe12c45..67169f9935f37ae034e97e112e09e977fe61a154 100644
--- a/Source/RWTHVRToolkit/Public/Utility/VirtualRealityUtilities.h
+++ b/Source/RWTHVRToolkit/Public/Utility/VirtualRealityUtilities.h
@@ -13,7 +13,6 @@
  */
 DECLARE_LOG_CATEGORY_EXTERN(Toolkit, Log, All);
 
-
 UENUM(BlueprintType)
 enum class ENamedClusterComponent : uint8
 {
@@ -70,19 +69,15 @@ public:
 	//Get Component of Display Cluster by it's name, which is specified in the nDisplay config
 	UFUNCTION(BlueprintPure, BlueprintCallable, Category = "DisplayCluster") static USceneComponent* GetClusterComponent(const FString& Name);
 	UFUNCTION(BlueprintPure, BlueprintCallable, Category = "DisplayCluster") static USceneComponent* GetNamedClusterComponent(const ENamedClusterComponent& Component);
-
-	// Helper functions for components to register input events
-	UFUNCTION(BlueprintPure, BlueprintCallable, Category = "VRPawn")
-	static UEnhancedInputComponent* GetVRPawnInputComponent(const UWorld* World);
-	UFUNCTION(BlueprintPure, BlueprintCallable, Category = "VRPawn")
-	static UEnhancedInputLocalPlayerSubsystem* GetVRPawnLocalPlayerSubsystem(UWorld* World);
 	
 	/* Load and create an Object from an asset path. This only works in the constructor */
     template <class T>
+	[[deprecated]]
     static bool LoadAsset(const FString& Path, T*& Result);
-
+	
     /* Finds and returns a class of an asset. This only works in the constructor */
     template <class T>
+	[[deprecated]]
     static bool LoadClass(const FString& Path, TSubclassOf<T>& Result);
 
 	UFUNCTION(BlueprintCallable)
diff --git a/Source/RWTHVRToolkit/RWTHVRToolkit.Build.cs b/Source/RWTHVRToolkit/RWTHVRToolkit.Build.cs
index d68c4ffc1ab3c60308bc525b24edc6d4c7bb92a6..930dc9129b0fbcf2ba9b275595cea42f42866238 100644
--- a/Source/RWTHVRToolkit/RWTHVRToolkit.Build.cs
+++ b/Source/RWTHVRToolkit/RWTHVRToolkit.Build.cs
@@ -36,7 +36,10 @@ public class RWTHVRToolkit : ModuleRules
 		);
 
 		PrivateDependencyModuleNames.AddRange(
-			new string[]{}
+			new string[]
+			{
+				"NetCore"
+			}
 		);
 		if (Target.bBuildEditor == true)
 		{