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