From 12a4030d71ecda077743c0314b65958e1d52b774 Mon Sep 17 00:00:00 2001
From: Sebastian Pape <pape@vr.rwth-aachen.de>
Date: Tue, 12 Jan 2021 13:48:51 +0100
Subject: [PATCH] Adding TDW support and auto-kill feature crashed processes

---
 LaunchConfig/tileddisplaywall.cfg             | 283 ++++++++++++++++++
 .../Private/NDisplayLaunchButton.cpp          |  52 +++-
 .../Public/NDisplayLaunchButton.h             |   1 +
 .../Public/NDisplayLaunchButtonSettings.h     |  27 +-
 4 files changed, 360 insertions(+), 3 deletions(-)
 create mode 100644 LaunchConfig/tileddisplaywall.cfg

diff --git a/LaunchConfig/tileddisplaywall.cfg b/LaunchConfig/tileddisplaywall.cfg
new file mode 100644
index 0000000..4999d1a
--- /dev/null
+++ b/LaunchConfig/tileddisplaywall.cfg
@@ -0,0 +1,283 @@
+[projection] id="proj_screen_bottom_right" type="simple" screen="screen_bottom_right"
+[projection] id="proj_screen_bottom_middle" type="simple" screen="screen_bottom_middle"
+[projection] id="proj_screen_bottom_left" type="simple" screen="screen_bottom_left"
+[projection] id="proj_screen_top_right" type="simple" screen="screen_top_right"
+[projection] id="proj_screen_top_middle" type="simple" screen="screen_top_middle"
+[projection] id="proj_screen_top_left" type="simple" screen="screen_top_left"
+# AUTO_CONVERSION, new entities finish
+
+
+#####################################################################
+# nDisplay config file for aixCAVE
+#####################################################################
+
+
+#####################################################################
+# Config info
+#********************************************************************
+# This is a config file header.
+#
+# Properties:
+# version - specifies the version of the configuration file (UE4.xx)
+#********************************************************************
+[info] version="23"
+
+
+#####################################################################
+# Cluster nodes
+#********************************************************************
+# Cluster node is an application instance. It's allowed to use
+# multiple instances on the same PC. Sometimes its necessary.
+#
+# Properties:
+# id       - Unique node name
+# window   - Window ID
+# addr     - Network address (IPv4 only)
+# master   - Specifies if current node is master; default is 'false'
+# port_cs  - Cluster Synchronization port (required on master node only)
+# port_ss  - Swap Synchronization port (required on master node only)
+# port_ce  - Cluster Events port (required on master node only)
+#
+# Optional properties:
+# eye_swap - Swap eyes for this node; default is 'false'
+# sound    - turns on/off sound for this application instance; default is 'false'
+#********************************************************************
+
+[cluster_node] id=node_tr   addr=127.0.0.1   window=wnd_top_right        mono_eye=left
+[cluster_node] id=node_tm   addr=127.0.0.1   window=wnd_top_middle       mono_eye=left
+[cluster_node] id=node_tl   addr=127.0.0.1   window=wnd_top_left         mono_eye=left  port_cs=41001 port_ss=41002 port_ce=41003 master=true sound=true
+[cluster_node] id=node_br   addr=127.0.0.1   window=wnd_bottom_right     mono_eye=left
+[cluster_node] id=node_bm   addr=127.0.0.1   window=wnd_bottom_middle    mono_eye=left
+[cluster_node] id=node_bl   addr=127.0.0.1   window=wnd_bottom_left      mono_eye=left
+
+#####################################################################
+# Application windows
+#********************************************************************
+# The window entitty defines properties of application's game window.
+#
+# Properties:
+# id         - Unique window name
+# fullscreen - Fullscreen or windowed mode
+# winx       - X location
+# winy       - Y location
+# resx       - Width
+# resy       - Height
+# viewports  - Array of viewports
+#********************************************************************
+# Here we have 7 windows. They all run in fullscreen mode on a
+# default output device with individual viewports assigned.
+
+[window] id=wnd_bottom_right          viewports=vp_bottom_right       fullscreen=true
+[window] id=wnd_bottom_middle         viewports=vp_bottom_middle      fullscreen=true
+[window] id=wnd_bottom_left           viewports=vp_bottom_left        fullscreen=true
+[window] id=wnd_top_right             viewports=vp_top_right          fullscreen=true
+[window] id=wnd_top_middle            viewports=vp_top_middle         fullscreen=true
+[window] id=wnd_top_left              viewports=vp_top_left           fullscreen=true
+
+#####################################################################
+# Projection screens
+#********************************************************************
+# Projection screen is a rectangle which determines the camera frustum.
+# Usually the projection screen has the same dimensions as an output
+# display but in some cases it may differ.
+#
+# Properties:
+# id     - unique projection screen name
+# loc    - relative location to the parent component. Location is relative
+#          to the VR root if no parent specified. The pivot is a screen's
+#          center and the values are in meters.
+# rot    - relative rotation to the parent component. Rotation is relative
+#          to the VR root if no parent specified. The pivot is a screen's
+#          center and the values are in degrees.
+# size   - width (X) and height (Y) of the screen. Values are in meters.
+#
+# Optional properties:
+# parent     - ID of parent component in VR hierarchy; default is VR root.
+# tracker_id - ID of tracking device; no tracking by default.
+# tracker_ch - ID of tracking device's channel; no tracking by default.
+#********************************************************************
+# We have 5 output displays. That means we have to have 5 projection
+# screens. Sometimes it's possible to use only one projection screen
+# (Nvidia mosaic/surround + projections with blending) but in this
+# particular case each display is managed by its own PC.
+
+[screen] id=screen_bottom_right    loc="X=0,Y=0,Z=0" rot="P=0,Y=0,R=0" size="X=1.022,Y=0.58"  parent=display_bottom_right
+[screen] id=screen_bottom_middle   loc="X=0,Y=0,Z=0" rot="P=0,Y=0,R=0" size="X=1.022,Y=0.58"  parent=display_bottom_middle
+[screen] id=screen_bottom_left     loc="X=0,Y=0,Z=0" rot="P=0,Y=0,R=0" size="X=1.022,Y=0.58"  parent=display_bottom_left
+[screen] id=screen_top_right       loc="X=0,Y=0,Z=0" rot="P=0,Y=0,R=0" size="X=1.022,Y=0.58"  parent=display_top_right
+[screen] id=screen_top_middle      loc="X=0,Y=0,Z=0" rot="P=0,Y=0,R=0" size="X=1.022,Y=0.58"  parent=display_top_middle
+[screen] id=screen_top_left        loc="X=0,Y=0,Z=0" rot="P=0,Y=0,R=0" size="X=1.022,Y=0.58"  parent=display_top_left
+
+#####################################################################
+# Viewports
+#********************************************************************
+# Viewport is a rectangle area of game window where rendered frame is
+# mapped. Usually the viewport starts at 0:0 and has the same size as
+# its parent window but in some cases these settings may differ.
+#
+# Properties:
+# id     - unique viewport name
+# x      - X coordinate of viewport's top left corner
+# y      - Y coordinate of viewport's top left corner
+# width  - width of viewport in pixels
+# height - height of viewport in pixels
+#********************************************************************
+# In this example we have different output resolutions. Let's enumerate
+# correspondent viewport settings. Each viewport has its own projection
+# screen.
+
+[viewport] id=vp_bottom_right    x=0 y=0 width=480 height=272 projection="proj_screen_bottom_right"
+[viewport] id=vp_bottom_middle  x=0 y=0 width=480 height=272 projection="proj_screen_bottom_middle"
+[viewport] id=vp_bottom_left      x=0 y=0 width=480 height=272 projection="proj_screen_bottom_left"
+
+[viewport] id=vp_top_right          x=0 y=0 width=480 height=272 projection="proj_screen_top_right"
+[viewport] id=vp_top_middle        x=0 y=0 width=480 height=272 projection="proj_screen_top_middle"
+[viewport] id=vp_top_left            x=0 y=0 width=480 height=272 projection="proj_screen_top_left"
+
+#####################################################################
+# Cameras
+#********************************************************************
+# Camera is a predefined point frome where the stereoscopic view built.
+# It's possible to define multiple cameras and swith the active one
+# during runtime. You're free to attach any camera to a tracking device
+# for head tracking. Consider a camera as a viewer's head.
+#
+# Properties:
+# id  - unique camera name
+# loc - relative location to the parent component. Location is relative
+#       to the VR root if no parent specified.
+# rot - relative rotation to the parent component. Rotation is relative
+#       to the VR root if no parent specified.
+#
+# Optional properties:
+# parent     - ID of parent component in VR hierarchy; default is VR root.
+# tracker_id - ID of tracking device; no tracking by default.
+# tracker_ch - ID of tracking device's channel; no tracking by default.
+#********************************************************************
+# In this example we have only one static camera (no tracking).
+[camera] id=camera_dynamic loc="X=0,Y=0,Z=0" parent=shutter_glasses eye_swap="false" eye_dist="64" force_offset="0"
+
+
+#####################################################################
+# Scene nodes (hierarchy transforms)
+#********************************************************************
+# Scene node is an actor component which is basically a transformation
+# matrix. Scene nodes can be helpful to build a component hierarchy, to
+# define some special places (like a socket) within VR space.
+#
+# It might be difficult to understand what VR space origin is. Consider
+# it as a point in space where VR space starts. Any componenent listed
+# in this config file is relative to its parent or this origin.
+#
+# Properties:
+# id  - unique scene node name
+# loc - relative location to the parent component. Location is relative
+#       to the VR root if no parent specified.
+# rot - relative rotation to the parent component. Rotation is relative
+#       to the VR root if no parent specified.
+#
+# Optional properties:
+# parent     - ID of parent component in VR hierarchy; default is VR root.
+# tracker_id - ID of tracking device; no tracking by default.
+# tracker_ch - ID of tracking device's channel; no tracking by default.
+#********************************************************************
+# Here we build our VR hierarchy. We do it in such a way that the center
+# of floor screen is in the VR space origin.
+[scene_node] id=tdw_origin_floor               loc="X=0,Y=0,Z=0"          rot="P=0,Y=0,R=0"
+[scene_node] id=tdw_center                     loc="X=1.95,Y=0,Z=1.72"    rot="P=0,Y=0,R=0"   parent=tdw_origin_floor
+
+[scene_node] id=display_bottom_right           loc="X=0,Y=1.05,Z=-0.302"  rot="P=0,Y=0,R=0"   parent=tdw_center
+[scene_node] id=display_bottom_middle          loc="X=0,Y=0.00,Z=-0.302"  rot="P=0,Y=0,R=0"   parent=tdw_center
+[scene_node] id=display_bottom_left            loc="X=0,Y=-1.05,Z=-0.302" rot="P=0,Y=0,R=0"   parent=tdw_center
+[scene_node] id=display_top_right              loc="X=0,Y=1.05,Z=0.302"   rot="P=0,Y=0,R=0"   parent=tdw_center
+[scene_node] id=display_top_middle             loc="X=0,Y=0.00,Z=0.302"   rot="P=0,Y=0,R=0"   parent=tdw_center
+[scene_node] id=display_top_left               loc="X=0,Y=-1.05,Z=0.302"  rot="P=0,Y=0,R=0"   parent=tdw_center
+
+[scene_node] id=flystick                       loc="X=0,Y=0,Z=0"          rot="P=0,Y=0,R=0"   parent=tdw_origin_floor      tracker_id=dtrack_tracker tracker_ch=1
+[scene_node] id=shutter_glasses                loc="X=0,Y=0,Z=1.80"       rot="P=0,Y=0,R=0"   parent=tdw_origin_floor      tracker_id=dtrack_tracker tracker_ch=0
+
+
+#####################################################################
+# Input devices
+#********************************************************************
+# Input devices are VRPN devices. The nDisplay supports the following
+# types: analog, button and tracker. Many of physical input devices
+# can be connected via VRPN.
+#
+# Properties:
+# id    - nique device name
+# type  - VRPN type (analog, button or tracker).
+# addr  - address of a VRPN server which handles this particular device.
+#         The value must match the following format: DEVICENAME@SERVER_ADDRESS
+#         where DEVICENAME is a VRPN name of this device and SERVER_ADDRESS
+#         is IPv4 address of VRPN server.
+# loc   - relative location to the parent component. Location is relative
+#         to the VR root if no parent specified.
+# rot   - relative rotation to the parent component. Rotation is relative
+#         to the VR root if no parent specified.
+#
+# front (tracker only) - mapping of a tracking system axis to X axis of VR origin
+# right (tracker only) - mapping of a tracking system axis to Y axis of VR origin
+# up    (tracker only) - mapping of a tracking system axis to Z axis of VR origin
+# * The following values are allowed for axes mapping: X, -X, Y, -Y, Z, -Z
+#
+# Optional properties:
+# remap - VRPN device channel remapping. Value format is: "from0:to0,from1:to1,...,fromN:toN".
+#         For example: remap="0:3,1:4,5:2"
+#********************************************************************
+#[input] id=dtrack_axis    type=analog  addr=DTrack2@134.61.201.230
+#[input] id=dtrack_buttons type=buttons addr=DTrack2@134.61.201.230
+#[input] id=dtrack_tracker type=tracker addr=DTrack2@134.61.201.230 loc="X=0,Y=0,Z=0" rot="P=0,Y=0,R=0" right=X up=Y front=-Z
+
+#Flystick
+#   Trigger button
+[input_setup] id=dtrack_buttons ch=0 bind="nDisplay Button 0"
+#   Blue Flystick buttons, from left to right nDisplay Button 1 to 4
+[input_setup] id=dtrack_buttons ch=1 bind="nDisplay Button 4"
+[input_setup] id=dtrack_buttons ch=2 bind="nDisplay Button 3"
+[input_setup] id=dtrack_buttons ch=3 bind="nDisplay Button 2"
+[input_setup] id=dtrack_buttons ch=4 bind="nDisplay Button 1"
+#   Coolie Head Button
+[input_setup] id=dtrack_buttons ch=5 bind="nDisplay Button 5"
+
+# Axes
+#   Coolie head x axis, left/right
+[input_setup] id=dtrack_axis ch=0 bind="nDisplay Analog 0"
+#   Coolie head y axis, up/down
+[input_setup] id=dtrack_axis ch=1 bind="nDisplay Analog 1"
+
+#####################################################################
+# Stereoscopic settings
+#********************************************************************
+# Properties:
+# eye_dist - interoccular distance in meters
+[stereo]
+
+#####################################################################
+# General settings
+#********************************************************************
+# Properties:
+# swap_sync_policy - swap synchronization policy
+#                  - 0 - no synchronization
+#                  - 1 - software swap synchronization
+#                  - 2 - NV swap lock (Nvidia cards only, OpenGL only)
+[general] swap_sync_policy=1
+
+
+#####################################################################
+# Network settings
+#********************************************************************
+# Optional properties:
+# cln_conn_tries_amount - how many times a client tries to connect to a server
+# cln_conn_retry_delay  - delay before next client connection try (milliseconds)
+# game_start_timeout    - timeout before all data is loaded and game started (milliseconds)
+# barrier_wait_timeout  - barrier timeout for both game and render threads (milliseconds)
+[network] cln_conn_tries_amount=300 cln_conn_retry_delay=1000 game_start_timeout=600000 barrier_wait_timeout=600000
+
+
+#####################################################################
+# Custom arguments
+#********************************************************************
+# Any custom arguments available in runtime can be specified here.
+# Format:  ARG_NAME=ARG_VAL
+[custom] Hardware_Platform=TiledDisplayVideo
diff --git a/Source/NDisplayLaunchButton/Private/NDisplayLaunchButton.cpp b/Source/NDisplayLaunchButton/Private/NDisplayLaunchButton.cpp
index 0f2152e..5e58f22 100644
--- a/Source/NDisplayLaunchButton/Private/NDisplayLaunchButton.cpp
+++ b/Source/NDisplayLaunchButton/Private/NDisplayLaunchButton.cpp
@@ -105,6 +105,27 @@ FString FNDisplayLaunchButtonModule::GetConfigPath(FString ConfigName)
 	return FPaths::Combine(IPluginManager::Get().FindPlugin("NDisplayLaunchButton")->GetBaseDir(), TEXT("LaunchConfig"), ConfigName + ".cfg");
 }
 
+/**
+ * Kill an Array of Process-handles after waiting for them for a few seconds
+ * @param Processes - Array of Processes to kill
+ * @param Num_Nodes - Number of Processes in the Array
+ */
+void FNDisplayLaunchButtonModule::KillProcesses(FProcHandle Processes[], const int Num_Nodes)
+{
+	float SecondsToWait = 5;
+	const float CheckInterval = 0.25;
+	FPlatformProcess::ConditionalSleep([Num_Nodes, Processes, CheckInterval, &SecondsToWait]()
+	{
+		SecondsToWait -= CheckInterval;
+		if(SecondsToWait <= 0) return true;
+		for (int i = 0; i < Num_Nodes; i++){
+			if(FPlatformProcess::IsProcRunning(Processes[i])) return false;
+		}
+		return true;
+	}, CheckInterval);
+	for (int i = 0; i < Num_Nodes; i++)	FPlatformProcess::TerminateProc(Processes[i]);
+}
+
 /**
  * Executed on click of the button in the toolbar
  */
@@ -166,7 +187,9 @@ void FNDisplayLaunchButtonModule::PluginButtonClicked()
 		{
 			Processes[i] = FPlatformProcess::CreateProc(*GetEditorExecutableName(), *(Parameters + " " + Windows_Node_Specific_Commands[i]), true, false, false, nullptr, 0, nullptr, nullptr);
 		}
-		FPlatformProcess::WaitForProc(Processes[0]); //wait for only one of them
+		FPlatformProcess::WaitForProc(Processes[0]); /* wait for only one of them */
+
+		KillProcesses(Processes, Num_Nodes); /* Kill potentially crashed processes */
 	}
 	else if (Settings->LaunchType == ButtonLaunchType_TWO_SCREEN)
 	{
@@ -183,7 +206,32 @@ void FNDisplayLaunchButtonModule::PluginButtonClicked()
 		{
 			Processes[i] = FPlatformProcess::CreateProc(*GetEditorExecutableName(), *(Parameters + " " + Windows_Node_Specific_Commands[i]), true, false, false, nullptr, 0, nullptr, nullptr);
 		}
-		FPlatformProcess::WaitForProc(Processes[0]); //wait for only one of them
+		FPlatformProcess::WaitForProc(Processes[0]); /* wait for only one of them */
+		
+		KillProcesses(Processes, Num_Nodes); /* Kill potentially crashed processes */
+	}
+	else if (Settings->LaunchType == ButtonLaunchType_TDW)
+	{
+		const FString Parameters = FString::Printf(TEXT("\"%s\" -game dc_cfg=\"%s\" %s"),*FPaths::GetProjectFilePath(), *GetConfigPath("tileddisplaywall"), *Settings->TiledDisplayWallLaunchParameters);
+		
+		const int Num_Nodes = 6;
+		FString Windows_Node_Specific_Commands[Num_Nodes] = {
+			"dc_node=node_tl  WinX=200 WinY=200  ResX=480 ResY=272" + FString((Settings->TiledDisplayWallLogMasterWindow) ? " -log" : "") + ((Settings->TiledDisplayWallLogToProjectDirTL) ? (" ABSLOG=" + GetFilePathInProject("TiledDisplayWall_TL_Master.log")) : "") + " " + Settings->TiledDisplayWallAdditionalLaunchParametersMaster,
+			"dc_node=node_tm  WinX=693 WinY=200  ResX=480 ResY=272" + ((Settings->TiledDisplayWallLogToProjectDirTM) ? (" ABSLOG = " + GetFilePathInProject("TiledDisplayWall_TM.log")) : ""),
+			"dc_node=node_tr  WinX=1186 WinY=200 ResX=480 ResY=272" + ((Settings->TiledDisplayWallLogToProjectDirTR) ? (" ABSLOG = " + GetFilePathInProject("TiledDisplayWall_TR.log")) : ""),
+			"dc_node=node_bl  WinX=200 WinY=483  ResX=480 ResY=272" + ((Settings->TiledDisplayWallLogToProjectDirBL) ? (" ABSLOG = " + GetFilePathInProject("TiledDisplayWall_BL.log")) : ""),
+			"dc_node=node_bm  WinX=693 WinY=483  ResX=480 ResY=272" + ((Settings->TiledDisplayWallLogToProjectDirBM) ? (" ABSLOG = " + GetFilePathInProject("TiledDisplayWall_BM.log")) : ""),
+			"dc_node=node_br  WinX=1186 WinY=483 ResX=480 ResY=272" + ((Settings->TiledDisplayWallLogToProjectDirBR) ? (" ABSLOG = " + GetFilePathInProject("TiledDisplayWall_BR.log")) : "")
+		};
+
+		FProcHandle Processes[Num_Nodes];
+		for (int i = 0; i < Num_Nodes; i++)
+		{
+			Processes[i] = FPlatformProcess::CreateProc(*GetEditorExecutableName(), *(Parameters + " " + Windows_Node_Specific_Commands[i]), true, false, false, nullptr, 0, nullptr, nullptr);
+		}
+		FPlatformProcess::WaitForProc(Processes[0]); /* wait for only one of them */
+
+		KillProcesses(Processes, Num_Nodes); /* Kill potentially crashed processes */
 	}
 	else if (Settings->LaunchType == ButtonLaunchType_CAVE)
 	{
diff --git a/Source/NDisplayLaunchButton/Public/NDisplayLaunchButton.h b/Source/NDisplayLaunchButton/Public/NDisplayLaunchButton.h
index a0be9e6..f36d47e 100644
--- a/Source/NDisplayLaunchButton/Public/NDisplayLaunchButton.h
+++ b/Source/NDisplayLaunchButton/Public/NDisplayLaunchButton.h
@@ -24,6 +24,7 @@ public:
 	static FString GetEditorExecutableName();
 	static FString GetFilePathInProject(FString FileName);
 	static FString GetConfigPath(FString ConfigName);
+	static void KillProcesses(FProcHandle Processes[], const int Num_Nodes);
 
 	/** This function will be bound to Command. */
 	void PluginButtonClicked();
diff --git a/Source/NDisplayLaunchButton/Public/NDisplayLaunchButtonSettings.h b/Source/NDisplayLaunchButton/Public/NDisplayLaunchButtonSettings.h
index fd45cae..4c9ccc4 100644
--- a/Source/NDisplayLaunchButton/Public/NDisplayLaunchButtonSettings.h
+++ b/Source/NDisplayLaunchButton/Public/NDisplayLaunchButtonSettings.h
@@ -21,7 +21,8 @@ enum ButtonLaunchType
 	ButtonLaunchType_MiniCAVE UMETA(DisplayName = "MiniCAVE"),
 	ButtonLaunchType_TWO_SCREEN UMETA(DisplayName = "Two Screen"),
 	ButtonLaunchType_CAVE UMETA(DisplayName = "CAVE"),
-	ButtonLaunchType_ROLV UMETA(DisplayName = "ROLV")
+	ButtonLaunchType_ROLV UMETA(DisplayName = "ROLV"),
+	ButtonLaunchType_TDW UMETA(DisplayName = "Tiled Display Wall")
 };
 
 UCLASS(config=Engine, defaultconfig, meta=(DisplayName="nDisplay Launch Button"))
@@ -106,4 +107,28 @@ public:
 	FString DTrackIP;
 	UPROPERTY(EditAnywhere, config, Category = "General|ROLV|DTRACK", meta = (DisplayName = "DTrack Port", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_ROLV && StartDTrack"))
 	int DTrackPort = 50105;
+
+	/*
+	* TiledDisplayWall Options
+	*/
+	UPROPERTY(EditAnywhere, config, Category = "General|TiledDisplayWall", meta = (DisplayName = "Launch Parameters", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_TDW"))
+	FString TiledDisplayWallLaunchParameters = "-dc_cluster -dc_dev_mono -windowed -fixedseed -notexturestreaming";
+	UPROPERTY(EditAnywhere, config, Category = "General|TiledDisplayWall", meta = (DisplayName = "Additional Launch Parameters for Master", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_TDW"))
+	FString TiledDisplayWallAdditionalLaunchParametersMaster = "";
+	
+	UPROPERTY(EditAnywhere, config, Category = "General|TiledDisplayWall|Log", meta = (DisplayName = "Open Log Window for Master Node", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_TDW"))
+	bool TiledDisplayWallLogMasterWindow = true;
+	UPROPERTY(EditAnywhere, config, Category = "General|TiledDisplayWall|Log", meta = (DisplayName = "Write Log for TL-Node to Project Directory", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_TDW"))
+	bool TiledDisplayWallLogToProjectDirTL = true;
+	UPROPERTY(EditAnywhere, config, Category = "General|TiledDisplayWall|Log", meta = (DisplayName = "Write Log for TM-Node to Project Directory", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_TDW"))
+	bool TiledDisplayWallLogToProjectDirTM = false;
+	UPROPERTY(EditAnywhere, config, Category = "General|TiledDisplayWall|Log", meta = (DisplayName = "Write Log for TR-Node to Project Directory", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_TDW"))
+	bool TiledDisplayWallLogToProjectDirTR = false;
+	UPROPERTY(EditAnywhere, config, Category = "General|TiledDisplayWall|Log", meta = (DisplayName = "Write Log for BL-Node to Project Directory", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_TDW"))
+	bool TiledDisplayWallLogToProjectDirBL = false;
+	UPROPERTY(EditAnywhere, config, Category = "General|TiledDisplayWall|Log", meta = (DisplayName = "Write Log for BM-Node to Project Directory", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_TDW"))
+	bool TiledDisplayWallLogToProjectDirBM = false;
+	UPROPERTY(EditAnywhere, config, Category = "General|TiledDisplayWall|Log", meta = (DisplayName = "Write Log for BR-Node to Project Directory", EditCondition="LaunchType==ButtonLaunchType::ButtonLaunchType_TDW"))
+	bool TiledDisplayWallLogToProjectDirBR = false;
+
 };
-- 
GitLab